自然语言处理(19:(第五章4.)使用LSTM的语言模型)
Time LSTM 层的实现完成了,现在我们来实现正题——语言模型。一、LSTM的语言模型这里实现的语言模型和上一章几乎是一样的,唯一的区别是,上一章使用 Time RNN层的地方这次使用Time LSTM层,如下图所示。由上图可知,这里和上一章实现的语言模型的差别在于使用了 LSTM。我们将上图右图中的神经网络实现为Rnnlm类。Rnnlm类和上一章介绍的SimpleRnnlm类几乎相同,但是增
系列文章目录
第五章 1:Gated RNN(门控RNN)
第五章 2:梯度消失和LSTM
第五章 3:LSTM的实现
第五章 4:使用LSTM的语言模型
第五章 5:进一步改进RNNLM(以及总结)
文章目录
目录
前言
Time LSTM 层的实现完成了,现在我们来实现正题——语言模型。
一、LSTM的语言模型
这里实现的语言模型和上一章几乎是一样的,唯一的区别是,上一章使用 Time RNN层的地方这次使用Time LSTM层,如下图所示。

由上图可知,这里和上一章实现的语言模型的差别在于使用了 LSTM。我们将上图右图中的神经网络实现为Rnnlm类。Rnnlm类和上一章介绍的SimpleRnnlm类几乎相同,但是增加了一些新方法。下面给出使用 LSTM层实现的Rnnlm类的代码。
from common.time_layers import * # 这里面都是实现的TimeEmbedding, TimeLSTM,TimeAffine类
# 在前几章都可以找到
from common.base_model import BaseModel
"""
class BaseModel:
def __init__(self):
self.params, self.grads = None, None
def forward(self, *args):
raise NotImplementedError
def backward(self, *args):
raise NotImplementedError
def save_params(self, file_name=None):
if file_name is None:
file_name = self.__class__.__name__ + '.pkl'
params = [p.astype(np.float16) for p in self.params]
if GPU:
params = [to_cpu(p) for p in params]
with open(file_name, 'wb') as f:
pickle.dump(params, f)
def load_params(self, file_name=None):
if file_name is None:
file_name = self.__class__.__name__ + '.pkl'
if '/' in file_name:
file_name = file_name.replace('/', os.sep)
if not os.path.exists(file_name):
raise IOError('No file: ' + file_name)
with open(file_name, 'rb') as f:
params = pickle.load(f)
params = [p.astype('f') for p in params]
if GPU:
params = [to_gpu(p) for p in params]
for i, param in enumerate(self.params):
param[...] = params[i]
"""
# ------------------------------
# 具体你可以问Deepseek,我这里只给出了核心代码
# ------------------------------
class Rnnlm(BaseModel):
def __init__(self, vocab_size=10000, wordvec_size=100, hidden_size=100):
V, D, H = vocab_size, wordvec_size, hidden_size
rn = np.random.randn
# 初始化权重
embed_W = (rn(V, D) / 100).astype('f')
lstm_Wx = (rn(D, 4 * H) / np.sqrt(D)).astype('f')
lstm_Wh = (rn(H, 4 * H) / np.sqrt(H)).astype('f')
lstm_b = np.zeros(4 * 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),
TimeLSTM(lstm_Wx, lstm_Wh, lstm_b, stateful=True),
TimeAffine(affine_W, affine_b)
]
self.loss_layer = TimeSoftmaxWithLoss()
self.lstm_layer = self.layers[1]
# 将所有的权重和梯度整理到列表中
self.params, self.grads = [], []
for layer in self.layers:
self.params += layer.params
self.grads += layer.grads
def predict(self, xs):
for layer in self.layers:
xs = layer.forward(xs)
return xs
def forward(self, xs, ts):
score = self.predict(xs)
loss = self.loss_layer.forward(score, ts)
return loss
def backward(self, dout=1):
dout = self.loss_layer.backward(dout)
for layer in reversed(self.layers):
dout = layer.backward(dout)
return dout
def reset_state(self):
self.lstm_layer.reset_state()
def save_params(self, file_name="Rnnlm.pkl"):
with open(file_name, 'wb') as f:
pickle.dump(self.params, f)
def load_params(self, file_name="Rnnlm.pkl"):
with open(file_name, 'rb') as f:
self.params = pickle.load(f)
Rnnlm 类将到Softmax 层为止的处理实现为predict()方法,这个方法在后面章节进行文本生成时还会用到。此外,该类还添加了用于读写参数的save_params() 和 load_params() 方法。剩下的实现与上一章的SimpleRnnlm 类相同。
下面,我们在PTB数据集上学习这个网络。这次我们使用PTB数据 集的所有训练数据进行学习(上一章中只使用了PTB数据集的一部分),代码如下所示
# coding: utf-8
import sys
sys.path.append('..')
from common.optimizer import SGD
from common.trainer import RnnlmTrainer
from common.util import eval_perplexity
from dataset import ptb
from rnnlm import Rnnlm
# 设定超参数
batch_size = 20
wordvec_size = 100
hidden_size = 100 # RNN的隐藏状态向量的元素个数
time_size = 35 # RNN的展开大小
lr = 20.0
max_epoch = 4
max_grad = 0.25
# 读入训练数据
corpus, word_to_id, id_to_word = ptb.load_data('train')
corpus_test, _, _ = ptb.load_data('test')
vocab_size = len(word_to_id)
xs = corpus[:-1]
ts = corpus[1:]
# 生成模型
model = Rnnlm(vocab_size, wordvec_size, hidden_size)
optimizer = SGD(lr)
trainer = RnnlmTrainer(model, optimizer)
# 1.应用梯度裁剪进行学习
trainer.fit(xs, ts, max_epoch, batch_size, time_size, max_grad,
eval_interval=20)
trainer.plot(ylim=(0, 500))
# 2.基于测试数据进行评价
model.reset_state()
ppl_test = eval_perplexity(model, corpus_test)
print('test perplexity: ', ppl_test)
# 3.保存参数
model.save_params()
这里给出的代码和上一章的代码有很多相同的地方, 因此这里重点介绍不同的地方。首先,代码1处使用RnnlmTrainer类进行模型的学习。RnnlmTrainer 类的fit() 方法求模型的梯度,更新模型的参数。 另外,在方法内部,通过指定max_grad参数,从而应用梯度裁剪。顺便说一 下,fit() 方法内部进行的实现如下所示(伪代码)

我们在第五章第四节(自然语言处理(16:(第五章1.)Gated RNN)-CSDN博客)将梯度裁剪实现为了clip_grads(grads, max_grad),这里使用该方法进行梯度裁剪。
另外,通过1.处的fit()方法的参数eval_interval=20,每20次迭代对 困惑度进行1次评价。因为这次的数据量很大,所以没有对每个epoch进行 评价,而是每20次迭代评价1次。后面我们会将评价结果用plot()方法绘 制成图。 在学习结束后,在代码2.处使用测试数据对困惑度进行评价。这里需要 注意的是,此时需要先重置模型的状态(LSTM的隐藏状态和记忆单元)。 此外,因为评价困惑度的函数eval_perplexity()在common/util.py中已经实 现,所以直接使用即可。 最后,在代码3.处将学习好的参数保存到外部文件。在下一章生成句子 时,将会使用这些学习好的权重参数。 以上就是RNNLM的学习代码。执行代码后,在终端上会输出下图的结果(我在CPU上运行,比较慢,但是数据少,我们可以等一会(约10分钟))

每20次迭代输出1次困惑度的值。我们来看一下结果, 刚开始的困惑度为10000.84,这意味着下一个单词的候选个数能减少到 10 000 个左右。因为这次数据集的词汇量是10 000个,所以这是什么也没 学习的状态,相当于猜测。但是随着学习的进行,困惑度开始变好。实际上,当迭代超过300次时,困惑度已经降到了400以下。现在,我们看一下困惑度的演变图,如下图所示。

在这次的实验中,一共进行了4个epoch的学习(按迭代来算,相当于1327 * 4 次)。如图所示,困惑度顺利下降,最终达到100左右。基于最终的测试数据的评价(源代码2.处)结果为136.07...。该结果在每次执行时都不相同,但是都在135前后。换句话说,我们的模型成长到了能将下一个单词的候选个数(从10 000个)缩小到136个左右的水平。 那么,136这样的困惑度在实践中是什么水平呢?说实话,这并不是一个很好的结果。在2017年的一个研究中,PTB数据集上的困惑度已经降到了60以下。我们的模型还有很大的改进空间,下面我们就来进一步改进现有的RNNLM。
更多推荐

所有评论(0)