系列文章目录
第一章 1:同义词词典和基于计数方法语料库预处理
第一章 2:基于计数方法的分布式表示和假设,共现矩阵,向量相似度
第一章 3:基于计数方法的改进以及总结
第二章 1:word2vec
第二章 2:word2vec和CBOW模型的初步实现
第二章 3:CBOW模型的完整实现
第二章 4:CBOW模型的补充和skip-gram模型的理论
第三章 1:word2vec的高速化(CBOW的改进)
第三章 2:word2vec高速化(CBOW的二次改进)
第三章 3:改进版word2vec的学习以及总结
第四章 1:RNN(RNN的前置知识和引入)
第四章 2:RNN(RNN的正式介绍)
第四章 3:RNN的实现
第四章 4:处理时序数据的层的实现
第四章 5:RNNLM的学习和评价
文章目录
目录
系列文章目录
前言
一、RNNLM的实现
二、语言模型的评价
三、RNNLM的学习代码
四、RNNLM的Trainer类
总结
前言
在前几节中,实现RNNLM所需要的层都已经准备好了,现在我们来实现RNNLM, 并对其进行训练,然后再评价一下它的结果。最后总结整个第4章(可以查看博主前几次的文章)
警告:本节的内容(代码)比较多,因为有部分代码我封装了,为了方便同志们复现我又给出来了,所以文章难免会字多一些(10000字左右),还请见谅!!!编写不易,
希望三连加关注!!
一、RNNLM的实现
这里我们将RNNLM使用的网络实现为SimpleRnnlm类,其层结构如图
如上图所示,SimpleRnnlm类是一个堆叠了4个Time层的神经网络(有几层在前面文章写过自然语言处理(14:处理时序数据的层的实现)-CSDN博客,自然语言处理(13:RNN的实现)-CSDN博客,)。 我们先来看一下初始化的代码:
import numpy as np
from common.time_layers import *# 以上导入的类,博主在前几章介绍过了,除了TimeEmbedding类好像没实现"""class TimeEmbedding:def __init__(self, W):self.params = [W]self.grads = [np.zeros_like(W)]self.layers = Noneself.W = Wdef forward(self, xs):N, T = xs.shapeV, D = self.W.shapeout = np.empty((N, T, D), dtype='f')self.layers = []for t in range(T):layer = Embedding(self.W)out[:, t, :] = layer.forward(xs[:, t])self.layers.append(layer)return outdef backward(self, dout):N, T, D = dout.shapegrad = 0for t in range(T):layer = self.layers[t]layer.backward(dout[:, t, :])grad += layer.grads[0]self.grads[0][...] = gradreturn None"""class SimpleRnnlm:def __init__(self, vocab_size, wordvec_size, hidden_size):V, D, H = vocab_size, wordvec_size, hidden_sizern = np.random.randn# 初始化权重embed_W = (rn(V, D) / 100).astype('f')rnn_Wx = (rn(D, H) / np.sqrt(D)).astype('f')rnn_Wh = (rn(H, H) / np.sqrt(H)).astype('f')rnn_b = np.zeros(H).astype('f')affine_W = (rn(H, V) / np.sqrt(H)).astype('f')affine_b = np.zeros(V).astype('f')# 生成层(以下所需的类前几章有哦)self.layers = [TimeEmbedding(embed_W),TimeRNN(rnn_Wx, rnn_Wh, rnn_b, stateful=True),TimeAffine(affine_W, affine_b)]self.loss_layer = TimeSoftmaxWithLoss()self.rnn_layer = self.layers[1]# 将所有的权重和梯度整理到列表中self.params, self.grads = [], []for layer in self.layers:self.params += layer.paramsself.grads += layer.grads
这里,对各个层使用的参数(权重和偏置)进行初始化,生成必要的层。 假设使用Truncated BPTT进行学习,将Time RNN层的stateful设置为True,如此Time RNN层就可以继承上一时刻的隐藏状态。 另外,在上面的初始化代码中,RNN层和Affine层使用了“Xavier初始值”。如下图所示,在上一层的节点数是n的情况下,使用标准差为的分布作为Xavier初始值(不懂可以问deepseek哦),顺便说一下,标准差可以直观地解释为表 示数据分散程度的指标。
(注:在深度学习中,权重的初始值非常重要。同样,对RNN而言,权重的初始值也很重要。通过 设置好的初始值,学习的进展和最终的精度都会有很大变化。另外,在语言模型的相关研究中,经常使用0.01 * np.random.uniform(...) 这样的经过缩放的均匀分布。至于为什么,是因为前人的“经验”)
接着,我们来实现forward()方法、backward()方法和reset_state() 方法。
def forward(self, xs, ts):for layer in self.layers:xs = layer.forward(xs)loss = self.loss_layer.forward(xs, ts)return lossdef backward(self, dout=1):dout = self.loss_layer.backward(dout)for layer in reversed(self.layers):dout = layer.backward(dout)return doutdef reset_state(self):self.rnn_layer.reset_state()
可以看出实现非常简单。在各个层中,正向传播和反向传播都正确地进行了实现。因此,我们只要以正确的顺序调用forward()(或者backward()) 即可。方便起见,这里将重设网络状态的方法实现为reset_state()。以上 就是对SimpleRnnlm 类的说明。
二、语言模型的评价
SimpleRnnlm的实现结束了,接下来要做的就是向这个网络输入数据进行学习。在实现用于学习的代码之前,我们先来讨论一下语言模型的评价方法。 语言模型基于给定的已经出现的单词(信息)输出将要出现的单词的概 率分布。困惑度(perplexity)常被用作评价语言模型的预测性能的指标。 简单地说,困惑度表示“概率的倒数”(这个解释在数据量为1时严格一 致)。为了说明概率的倒数,我们仍旧考虑“you say goodbye and i say hello.” 这一语料库。假设在向语言模型“模型1”传入单词you时会输出下图的左面所示的概率分布。此时,下一个出现的单词是say的概率为0.8,这是一 个相当不错的预测。取这个概率的倒数,可以计算出困惑度为1/0.8 = 1.25。
而上图右侧的模型(“模型2”)预测出的正确单词的概率为0.2,这显然是一个很差的预测,此时的困惑度为 1/0.2 = 5。 总结一下,“模型1”能准确地预测,困惑度是1.25;“模型2”的预测未能命中,困惑度是5.0。此例表明,困惑度越小越好。
那么,如何直观地解释值1.25和5.0呢?它们可以解释为“分叉度”。 所谓分叉度,是指下一个可以选择的选项的数量(下一个可能出现的单词的候选个数)。在刚才的例子中,好的预测模型的分叉度是1.25,这意味着下一个要出现的单词的候选个数可以控制在1个左右。而在差的模型中,下一 个单词的候选个数有5个。
(如上面的例子所示,基于困惑度可以评价模型的预测性能。好的模型可以高概率地预测出正确单词,所以困惑度较小(困惑度的最小值是1.0);而差的模型只能低概率地预测出正确单词,困惑度较大。)
以上都是输入数据为1个时的困惑度。那么,在输入数据为多个的情况 下,结果会怎样呢?我们可以根据下面的式子进行计算:
这里,假设数据量为N个。tn是one-hot向量形式的正确解标签,tnk表示第n个数据的第k个值,ynk表示概率分布(神经网络中的Softmax的输出)。顺便说一下,L是神经网络的损失,和mini-batch的交叉熵损失式完全相同(可以问Deepseek哦),使用这个L计算出的就是困惑度。
上面式子(损失函数)看上去有些复杂,但是前面我们介绍的数据量为1时的“概率的倒数”“分叉度”“候选个数”等在这里也通用。也就是说,困惑度越小, 分叉度越小,表明模型越好。
三、RNNLM的学习代码
下面,我们使用PTB数据集进行学习,不过这里仅使用PTB数据集 (训练数据)的前1000个单词。这是因为在本节实现的RNNLM中,即便使用所有的训练数据,也得不出好的结果。下一章我们将对它进行改进。下面我们先来看一下学习用的代码:
import matplotlib.pyplot as plt
import numpy as np
from common.optimizer import SGD
from dataset import ptb
from simple_rnnlm import SimpleRnnlm"""
# ptb.pytry:import urllib.request
except ImportError:raise ImportError('Use Python3!')
import pickle
import numpy as npurl_base = 'https://raw.githubusercontent.com/tomsercu/lstm/master/data/'
key_file = {'train':'ptb.train.txt','test':'ptb.test.txt','valid':'ptb.valid.txt'
}
save_file = {'train':'ptb.train.npy','test':'ptb.test.npy','valid':'ptb.valid.npy'
}
vocab_file = 'ptb.vocab.pkl'dataset_dir = os.path.dirname(os.path.abspath(__file__))def _download(file_name):file_path = dataset_dir + '/' + file_nameif os.path.exists(file_path):returnprint('Downloading ' + file_name + ' ... ')try:urllib.request.urlretrieve(url_base + file_name, file_path)except urllib.error.URLError:import sslssl._create_default_https_context = ssl._create_unverified_contexturllib.request.urlretrieve(url_base + file_name, file_path)print('Done')def load_vocab():vocab_path = dataset_dir + '/' + vocab_fileif os.path.exists(vocab_path):with open(vocab_path, 'rb') as f:word_to_id, id_to_word = pickle.load(f)return word_to_id, id_to_wordword_to_id = {}id_to_word = {}data_type = 'train'file_name = key_file[data_type]file_path = dataset_dir + '/' + file_name_download(file_name)words = open(file_path).read().replace('\n', '<eos>').strip().split()for i, word in enumerate(words):if word not in word_to_id:tmp_id = len(word_to_id)word_to_id[word] = tmp_idid_to_word[tmp_id] = wordwith open(vocab_path, 'wb') as f:pickle.dump((word_to_id, id_to_word), f)return word_to_id, id_to_worddef load_data(data_type='train'):''':param data_type: 数据的种类:'train' or 'test' or 'valid (val)':return:'''if data_type == 'val': data_type = 'valid'save_path = dataset_dir + '/' + save_file[data_type]word_to_id, id_to_word = load_vocab()if os.path.exists(save_path):corpus = np.load(save_path)return corpus, word_to_id, id_to_wordfile_name = key_file[data_type]file_path = dataset_dir + '/' + file_name_download(file_name)words = open(file_path).read().replace('\n', '<eos>').strip().split()corpus = np.array([word_to_id[w] for w in words])np.save(save_path, corpus)return corpus, word_to_id, id_to_wordif __name__ == '__main__':for data_type in ('train', 'val', 'test'):load_data(data_type)""""""class SGD:'''随机梯度下降法(Stochastic Gradient Descent)'''def __init__(self, lr=0.01):self.lr = lrdef update(self, params, grads):for i in range(len(params)):params[i] -= self.lr * grads[i]"""# 设定超参数
batch_size = 10
wordvec_size = 100
hidden_size = 100
time_size = 5 # Truncated BPTT的时间跨度大小
lr = 0.1
max_epoch = 100# 读入训练数据(缩小了数据集)
corpus, word_to_id, id_to_word = ptb.load_data('train')
corpus_size = 1000
corpus = corpus[:corpus_size]
vocab_size = int(max(corpus) + 1)xs = corpus[:-1] # 输入
ts = corpus[1:] # 输出(监督标签)
data_size = len(xs)
print('corpus size: %d, vocabulary size: %d' % (corpus_size, vocab_size))# 学习用的参数
max_iters = data_size // (batch_size * time_size)
time_idx = 0
total_loss = 0
loss_count = 0
ppl_list = []# 生成模型
model = SimpleRnnlm(vocab_size, wordvec_size, hidden_size)
optimizer = SGD(lr)# 1.计算读入mini-batch的各笔样本数据的开始位置
jump = (corpus_size - 1) // batch_size
offsets = [i * jump for i in range(batch_size)]for epoch in range(max_epoch):for iter in range(max_iters):# 2.获取mini-batchbatch_x = np.empty((batch_size, time_size), dtype='i')batch_t = np.empty((batch_size, time_size), dtype='i')for t in range(time_size):for i, offset in enumerate(offsets):batch_x[i, t] = xs[(offset + time_idx) % data_size]batch_t[i, t] = ts[(offset + time_idx) % data_size]time_idx += 1# 计算梯度,更新参数loss = model.forward(batch_x, batch_t)model.backward()optimizer.update(model.params, model.grads)total_loss += lossloss_count += 1# 3.各个epoch的困惑度评价ppl = np.exp(total_loss / loss_count)print('| epoch %d | perplexity %.2f'% (epoch+1, ppl))ppl_list.append(float(ppl))total_loss, loss_count = 0, 0
以上就是学习用的代码,这和我们之前看到的神经网络的学习基本上是 一样的。不过,从宏观上看,仍有两点和之前的学习代码不同,即“数据的 输入方式”和“困惑度的计算”。这里,我们将重点关注这两点,并对代码进行说明。
首先是数据的输入方式。这里我们使用Truncated BPTT进行学习, 因此数据需要按顺序输入,并且mini-batch的各批次要平移读入数据的开 始位置。在源代码1.处,计算各批次读入数据的开始位置offsets。offsets 的各个元素中存放了读入数据的开始位置(偏移量)。 接着,在源代码2.处,按顺序读入数据。首先准备容器batch_x和 batch_t,然后依次增加time_idx变量,将time_idx处的数据从语料库中取 出。这里利用1.中计算好的offsets,各批次增加偏移量。另外,当读入语 料库的位置超过语料库大小时,为了回到语料库的开头处,将当前位置除以 语料库大小后的余数作为索引使用。 最后,基于上面损失函数式子L计算困惑度,这在代码3.处完成。为了求每个 epoch 的困惑度,需要计算每个epoch的平均损失,然后再据此求困惑度。 以上就是对代码的说明,现在我们看一下学习结果。在上面的代码中, 各个epoch的困惑度的结果都保存在了perplexity_list中,我们可以将它绘制出来,如下所示。
(只需要在上面代码基础上,最后加以下代码)
# 绘制图形
x = np.arange(len(ppl_list))
plt.plot(x, ppl_list, label='train')
plt.xlabel('epochs')
plt.ylabel('perplexity')
plt.show()
由上图可知,随着学习的进行,困惑度稳步下降。一开始超过300的困惑度到最后接近1(最小值)了。不过这里使用的是很小的语料库,在实际情况下,当语料库增大时,现在的模型根本无法招架。下一章我们将指出当前RNNLM存在的问题,并进行改进。
四、RNNLM的Trainer类
用于学习RNNLM的RnnlmTrainer类,其内部封装了刚才的RNNLM的学习。将刚才的学习代码重构为RnnlmTrainer类,结果如下。
from common.optimizer import SGD # 上面也有哦
from common.trainer import RnnlmTrainer # 在下面
from dataset import ptb # 上面有ptb.py的代码哦
from simple_rnnlm import SimpleRnnlm # 上面也实现过了昂,页面往上翻"""
class RnnlmTrainer:def __init__(self, model, optimizer):self.model = modelself.optimizer = optimizerself.time_idx = Noneself.ppl_list = Noneself.eval_interval = Noneself.current_epoch = 0def get_batch(self, x, t, batch_size, time_size):batch_x = np.empty((batch_size, time_size), dtype='i')batch_t = np.empty((batch_size, time_size), dtype='i')data_size = len(x)jump = data_size // batch_sizeoffsets = [i * jump for i in range(batch_size)] # mini-batch的各笔样本数据的开始位置for time in range(time_size):for i, offset in enumerate(offsets):batch_x[i, time] = x[(offset + self.time_idx) % data_size]batch_t[i, time] = t[(offset + self.time_idx) % data_size]self.time_idx += 1return batch_x, batch_tdef fit(self, xs, ts, max_epoch=10, batch_size=20, time_size=35,max_grad=None, eval_interval=20):data_size = len(xs)max_iters = data_size // (batch_size * time_size)self.time_idx = 0self.ppl_list = []self.eval_interval = eval_intervalmodel, optimizer = self.model, self.optimizertotal_loss = 0loss_count = 0start_time = time.time()for epoch in range(max_epoch):for iters in range(max_iters):batch_x, batch_t = self.get_batch(xs, ts, batch_size, time_size)# 计算梯度,更新参数loss = model.forward(batch_x, batch_t)model.backward()params, grads = remove_duplicate(model.params, model.grads) # 将共享的权重整合为1个if max_grad is not None:clip_grads(grads, max_grad)optimizer.update(params, grads)total_loss += lossloss_count += 1# 评价困惑度if (eval_interval is not None) and (iters % eval_interval) == 0:ppl = np.exp(total_loss / loss_count)elapsed_time = time.time() - start_timeprint('| epoch %d | iter %d / %d | time %d[s] | perplexity %.2f'% (self.current_epoch + 1, iters + 1, max_iters, elapsed_time, ppl))self.ppl_list.append(float(ppl))total_loss, loss_count = 0, 0self.current_epoch += 1def plot(self, ylim=None):x = numpy.arange(len(self.ppl_list))if ylim is not None:plt.ylim(*ylim)plt.plot(x, self.ppl_list, label='train')plt.xlabel('iterations (x' + str(self.eval_interval) + ')')plt.ylabel('perplexity')plt.show()"""# 设定超参数
batch_size = 10
wordvec_size = 100
hidden_size = 100 # RNN的隐藏状态向量的元素个数
time_size = 5 # RNN的展开大小
lr = 0.1
max_epoch = 100# 读入训练数据
corpus, word_to_id, id_to_word = ptb.load_data('train')
corpus_size = 1000 # 缩小测试用的数据集
corpus = corpus[:corpus_size]
vocab_size = int(max(corpus) + 1)
xs = corpus[:-1] # 输入
ts = corpus[1:] # 输出(监督标签)# 生成模型
model = SimpleRnnlm(vocab_size, wordvec_size, hidden_size)
optimizer = SGD(lr)
trainer = RnnlmTrainer(model, optimizer)trainer.fit(xs, ts, max_epoch, batch_size, time_size)
trainer.plot()
如上所示,首先使用model和optimizer初始化RnnlmTrainer类,然后 调用fit(),完成学习。此时,RnnlmTrainer类的内部将执行上一节进行的 一系列操作,具体如下所示。
1.按顺序生成mini-batch
2.调用模型的正向传播和反向传播
3.使用优化器更新权重
4.评价困惑度
使用RnnlmTrainer 类,可以避免每次写重复的代码。
总结
本章的主题是RNN。RNN通过数据的循环,从过去继承数据并传递 到现在和未来。如此,RNN层的内部获得了记忆隐藏状态的能力。本书 中我们花了很多时间说明RNN层的结构,并实现了RNN层(和Time RNN层 )。 本章还利用RNN创建了语言模型。语言模型给单词序列赋概率值。特 别地,条件语言模型从已经出现的单词序列计算下一个将要出现的单词的概 率。通过构成利用了RNN的神经网络,理论上无论多么长的时序数据,都 可以将它的重要信息记录在RNN的隐藏状态中。但是,在实际问题中,这 样一来,许多情况下学习将无法顺利进行。下一章我们将指出RNN存在的 问题,并研究替代RNN的LSTM层或GRU层。这些层在处理时序数据方 面非常重要,被广泛用于前沿研究。