途牛旅行网网站建设,江西网站制作的公司哪家好,北京做网站开发公司有哪些,wordpress搜索栏目录
随时间反向传播
实践
模型的使用
脏数据
“未知”词条的处理
字符级建模#xff08;英文#xff09;
生成聊天文章
进一步生成文本
文本生成的问题#xff1a;内容不受控
其他记忆机制
更深的网络 尽管在序列数据中#xff0c;循环神经网络为对各种语言关系…目录
随时间反向传播
实践
模型的使用
脏数据
“未知”词条的处理
字符级建模英文
生成聊天文章
进一步生成文本
文本生成的问题内容不受控
其他记忆机制
更深的网络 尽管在序列数据中循环神经网络为对各种语言关系建模因此也可能是因果关系提供了诸多便利但是存在一个主要缺陷当传递了两个词条后前面词条几乎完全失去了它的作用。第一个节点对第三个节点第一个时刻再过两个时刻后的所有作用都将被中间时刻引入的新数据彻底抹平。这对网络的基本结构很重要但却和人类语言中的常见情景相违背实际中即使词条在句子中相隔很远也可能是深度关联的。
如果名词和动词在序列中相隔多个时刻那么在这个更长的句子中循环神经网络很难理解主语和主动词之间的关系。对于新句子循环网络可能会过于强调动词和主语之间的联系而低估主语与谓语主动词之间的联系。也就是说在这里失去了句子的主语和谓语动词之间的关联性。在循环网络中当我们遍历每个句子时权重会衰减得过快。
这里面临的挑战是建立一个网络其在新句子中都能“领悟”到相同的核心思想。我们需要的是能够在整个输入序列中记住过去的方法。长短期记忆LSTM则是我们需要的一种方法。
长短期记忆网络的现代版本通常使用一种特殊的神经网络单元称为门控循环单元GRU。门控循环单元可以有效地保持长、短期记忆使LSTM能够更精确地处理长句子或文档。事实上LSTM工作的非常好它在几乎所有涉及时间序列、离散序列和NLP领域问题的应用中都取代了循环神经网络。
LSTM对于循环网络的每一层都引入了状态的概念。状态作为网络的记忆。可以把上述过程看成是在面向对象编程中为类添加属性。每个训练样本都会更新记忆状态的属性。
在LSTM中管理存储在状态记忆中信息的规则就是经过训练的神经网络本身。它们可以通过训练来学习要记住什么同时循环网络的其余部分会学习预测目标标签。随着记忆和状态的引入我们可以开始学习依赖关系这些依赖关系不仅可以扩展到一两个词条甚至还可以扩展到每个数据样本的整体。有了这些长期依赖关系就可以开始考虑超越文字本身的关于语言更深层次的东西。
有了LSTM模型可以开始学习人类习以为常和在潜意识层面上处理的语言模式。有了这些模式不仅可以更精确地预测样本类别还可以开始使用这些语言模型生成新的文本。 如上图就像在一般的循环网络中一样记忆状态受输入的影响同时也影响层的输出。但是这种记忆状态在时间序列句子或文档的所有时刻会持续存在。因此每个输入都会对记忆状态和隐藏层的输出产生影响。记忆状态的神奇之处在于它在学习使用标准的反向传播需要记住的信息的同时还学习输出信息。
首先我们展开一个标准的循环神经网络并添加记忆单元下图中看起来与一般的循环神经网络相似。但是除了向下一个时刻提供激活函数的输出这里还添加了一个也经过网络各时刻的记忆状态。在每个时刻的迭代中隐藏层循环单元都可以访问记忆单元。这个记忆单元的添加以及与其交互的机制使它与传统的神经网络层有很大的不同。LSTM层只是一个极为特例化的循环神经网络。 下面仔细看其中的一个元胞现在每个元胞不再是一系列输入权重和应用于这些权重的激活函数而是稍微复杂的结构。与前面一样到每一层或元胞的输入是当前时刻输入和前一个时刻输出的组合。当信息流入这个元胞而不是权重向量时它现在需经过3个门遗忘门、输入/候选门和输出门。如下图 这些门中的每一个都由一个前馈网络层和一个激活函数构成其中的前馈网络包含将要学习的一系列权重。从技术上讲其中一个门由两个前向路径组成因此在这个层中将有4组权重需要学习。权重和激活函数的旨在控制信息以不同数量流经元胞同时也控制信息到达元胞状态或记忆的所有路径。
下面是开发示例使用之前循环神经网络的例子将SimpleRNN替换成LSTM
maxlen200
batch_size32
embedding_dims300
epochs2import numpy as npX_trainpad_turnc(X_train,maxlen)
X_testpad_turnc(X_test,maxlen)
X_trainnp.reshape(X_train,(len(X_train),maxlen,embedding_dims))
y_trainnp.array(y_train)
X_testnp.reshape(X_test,(len(X_test),maxlen,embedding_dims))
y_testnp.array(y_test)from keras.api.models import Sequential
from keras.api.layers import Dense,Dropout,Flatten,SimpleRNN,LSTM
num_neurons50
modelSequential()
model.add(LSTM(num_neurons,return_sequencesTrue,input_shape(maxlen,embedding_dims)
))
model.add(Dropout(0.2))
model.add(Flatten())
model.add(Dense(1,activationsigmoid))
model.compile(rmsprop,binary_crossentropy,metrics[accuracy])
print(model.summary()) 虽然只修改了导入库的部分和其中一行Keras代码但有很多事情都发生了改变。
从模型摘要中可以看到对应相同的神经元LSTM需要训练的参数比SimpleRNN中的要多的多。
在LSTM中记忆将由一个向量来表示这个向量与元胞中神经元的元素数量相同。上面的例子只有50个神经元因此记忆单元将是一个由50个元素长的浮点数向量。 如上图是门在网络中的运行情况。通过元胞的“旅程”不是一条单一的道路它有多个分支。
我们从第一个样本中获取第一个词条并将其300个元素的向量表示传递到第一个LSTM元胞。在进入元胞的过程中数据的向量表示与前一个时刻的向量输出第一个时刻的向量为0拼接起来。在本例中我们将得到一个长度为30050个元素的向量。有时我们会看到向量后面加一个代表偏置项的1。因为偏置项在传递到激活函数之前总是将其相关权重乘以值1所以有时会从输入向量表示中省略该输入以使图更易理解。
在道路的第一个分叉处我们将拼接起来的输入向量的副本传递到似乎会预示厄运的遗忘门遗忘门的目标是根据给定的输入学习要遗忘元胞的多少记忆 想要遗忘和想要记住的想法一样重要。作为人类当我们从文本中获取某些信息时例如名词是单数还是复数我们想要保留这些信息以便在之后的句子中能识别出与之匹配的正确的动词词形变换或形容词形式。在罗曼斯语序中我们也必须识别一个名称的性别然后在句子中使用它。但是输入序列会经常的从一个名词装换到另一个名词因为输入序列可以由多个短语、句子甚至是文档组成。由于新的思想是在后面的语句中表达的名词是复数的事实可能与后面不相关的文本没有任何关系。 A thinker sees his own actions as experiments and questions--as attempts to find out something. Success and failure are for him answers above all. 在这句中动词“see”与名词“thinker”搭配我们遇到的下一个主动动词时第二个句子中的“to be”。这时“be”动词变形成“are”与“Success and failure”匹配。如果把它和句子中的第一个名词“thinker”搭配起来就会使用错误的动词形式“is”因此LSTM不仅必须对序列中的长期依赖关系建模而且同样重要的是还必须随着新依赖关系的出现而忘记长期依赖关系这就是遗忘门的作用在我们的记忆元胞中为相关的记忆腾出空间。
网络并不基于这类显式表示进行工作。网络试图找到一组权重用它们乘以来自词条序列的输入以便以最小化误差的方式更新记忆元胞和输出。令人惊讶的是它们竟然能工作并且工作得很好。
遗忘门本身只是一个前馈网络它由n个神经元组成每个神经元的权重个数为mn1在本例中遗忘门由50个神经元每个神经元有351300501个权重。因我们希望遗忘门中的每个神经元的输出值在0到1之间所以遗忘门的激活函数是Sigmoid函数。 遗忘门的输出向量是某种掩码多孔掩码它会遗忘记忆向量的某些元素当遗忘门输出值接近于1时对于该时刻关联元素中更多的记忆知识会被保留它越接近于0遗忘的记忆知识就越多 通过检查核对上述模型的遗忘门能够主动忘记一些东西。我们最好学会记住一些新的事物否则它很快就会被遗忘的。就会在“遗忘门”中原因我们将使用一个小网络根据两件事来学习需要假如多少记忆到目前为止的输入和上一个时刻的输出。这是在候选门中发生的事情。
候选门内部有两个独立的神经元它们做两件事
决定哪些输入向量元素值得记住类似于遗忘门中的掩码将记住的输入元素按规定路线放置到正确的记忆“槽”
候选门的第一部分是一个具有Sigmoid激活函数的神经元其目标是学习要更新记忆向量的哪些输入值。这个神经元很像遗忘门中的掩码。
这个门的第二部分决定使用多大的值来更新记忆。第二部分使用一个tanh激活函数它强制输出值在-1到1之间这两个向量的输出是按元素相乘然后将相乘得到的结果向量按元素加到记忆寄存器从而记住新的细节 这个门同时学习要提取哪些值以及这些特定值的大小。掩码和大小称为添加到记忆状态的值与遗忘门一样候选门会学习在将不适合信息添加到元胞的记忆之前屏蔽掉它们。
所以我们希望旧的、不相关的信息被遗忘而新的信息能够被记住然后我们就到达了元胞的最后一个门输出门。
到目前为止在穿越元胞的过程中只向元胞的记忆写入了内容现在是时候利用整个结构了。输出门接收输入这仍然是t时刻元胞的输入和t-1时刻元胞的输出的拼接并将其传递到输出门。
拼接的输入被传递到n个神经元的权重中然后使用Sigmoid激活函数来传递一个n维浮点数向量就像SimpleRNN的输出一样。但是不同于通过细胞壁来传递信息在网络中我们通过暂停部分输出来传递信息。
现在够级的记忆结构已经准备完成了它将对我们应该输出什么进行权衡这将是通过使用记忆创建最后一个掩码来判断的。这个掩码也是一种门但是要尽量避免使用门这个术语吟哦我这个掩码没有任何学习过的参数这有别于前面描述的3个门。
由记忆创建的掩码是对记忆状态的每个元素使用tanh函数它提供了一个在-1和1之间的n维浮点数向量。然后将掩码向量与输入门第一步中计算的原始向量按元素相乘。得到的n维结果向量作为元胞在t时刻的整数输出最终从元胞中传出 因此在获得了t时刻的输入和t-1时刻的输出以及输入序列中的所有细节之后元胞的记忆就知道在t时刻最后一个词输出什么是最重要的。
随时间反向传播
LSTM的学习与其他神经网络一样也是通过反向传播算法。基本RNN易于受到梯度消失影响是因为在任意给定时刻导数都是权重的一个决定因素因此当我们结合不同的学习率往之前时刻词条传播时经过几次迭代之后权重和学习率可能会将梯度缩小到0。在反向传播结束时相当于序列的开始对权重的更新要么很小要么就为0当权重稍大时也会出现类似的问题梯度爆炸并不与网络增大成比例。
LSTM通过记忆状态避免了这个问题。每个门中的神经元都是通过它们输入进的函数的导数来更新的即那些在前向传递时更新记忆状态的函数。所以在任何给定的时刻当一般的链式法则反向应用于前向传播时对神经元的更新只依赖于当前时刻和前一个时刻的记忆状态。这样整个函数的误差在每个时刻都能“更接近”神经元这就是所谓的误差传播。
实践
对之前的例子将Keras的SimpleRNN层替换成Keras的LSTM层分类器的其他所有部分都将保持不变。
将序列填充/截断为200个词条
#收集数据并做好准备
datasetpre_process_data(C:\\Users\\86185\\PycharmProjects\\pythonProject\\NLP\\aclImdb\\train)
vectorized_datatokenize_and_vectorize(dataset)
exceptedcollect_excepted(dataset)
split_pointint(len(vectorized_data)*0.8)
#将数据划分为训练集和测试集
X_trainvectorized_data[:split_point]
y_trainexcepted[:split_point]
X_testvectorized_data[split_point:]
y_testexcepted[split_point:]
#声明超参数
maxlen200
#在反向传播和更新权重之前需要传递给网络的样本数
batch_size32
#需要传递进Convert的词条向量的长度
embedding_dims300
epochs2import numpy as np
#进一步准备数据使每个序列的长度相等
X_trainpad_turnc(X_train,maxlen)
X_testpad_turnc(X_test,maxlen)
X_trainnp.reshape(X_train,(len(X_train),maxlen,embedding_dims))
y_trainnp.array(y_train)
X_testnp.reshape(X_test,(len(X_test),maxlen,embedding_dims))
y_testnp.array(y_test)
然后使用新的LSTM层构建模型
from keras.api.models import Sequential
from keras.api.layers import Dense,Dropout,Flatten,SimpleRNN,LSTM
num_neurons50
modelSequential()
model.add(LSTM(num_neurons,return_sequencesTrue,input_shape(maxlen,embedding_dims)
))
model.add(Dropout(0.2))
model.add(Flatten())
model.add(Dense(1,activationsigmoid))
model.compile(rmsprop,binary_crossentropy,metrics[accuracy])
print(model.summary()) 训练并保存模型
model.fit(X_train,y_train,batch_sizebatch_size,epochsepochs,validation_data(X_test,y_test))model_structuremodel.to_json()
with open(lstm_model1.json,w) as json_file:json_file.write(model_structure)
model.save_weights(lstm_weight1.weights.h5) 与简单的RNN相比验证精确率有巨大的提升。当词条之间的关系非常重要时可以看到通过为模型提供记忆可以获得巨大的收益。该算法的优势在于它可以学习看到的词条之间的关系。网络现在能够对这些关系建模尤其是在提供的代价函数的上下文中。
模型的使用
有了训练好的模型就可以开始尝试各种样本短语并查看模型的表现。
尝试欺骗这个模型在负向的语境中使用快乐的词尝试长短语、短短语、矛盾短语
重新加载LSTM模型
from keras.api.models import model_from_json
with open(lstm_model1.json,r) as json_file:json_stringjson_file.read()
modelmodel_from_json(json_string)model.load_weights(lstm_weight1.weights.h5)使用模型预测一个样本
sample_1
I hate that the dismal weather had me down for so long,when will it break! Ugh,when does happiness return? The sun is blinding and the puffy clouds are too thin. I cant wait for the weekend.
vec_listtokenize_and_vectorize([(1,sample_1)])
test_vec_listpad_turnc(vec_list,maxlenmaxlen)
test_vecnp.reshape(test_vec_list,(len(test_vec_list),maxlen,embedding_dims))
print(Samples sentinemnt ,1-pos,2-neg:{}.format(model.predict_tpye(test_vec)))
print(Raw output of sigmoid function:{}.format(model.predict(test_vec))) 当尝试各种可能性时除离散的情感分类之外还需要观察Sigmoid函数的原始输出。predict()方法在设置阈值之前显示原始的Sigmoid激活函数输出结果因此可以看到0到1之间的一个连续值。任何输出值大于0.5的语句都归为正向类小于0.5的都归为负向类。当尝试不同样本时我们将了解模型对其预测的信息有多强这将有助于分析我们的抽查结果。
要密切关注分类错误的样本正向的和负向的。如果Sigmoid输出接近0.5就意味着对于这个样本模型只是随机抛硬币。然后我们可以查看为什么这个短语对模型来说是模糊的但不要用人类的思维看待它的表现而是从统计学的角度思考。试想模型“看到了”什么文档这个被分类错误的样本中出现的词是否罕见它们是在语料库中罕见还是为我们训练语言模型的语料库中罕见该样本中的所有词是否都存在于模型的词汇表中
通过这个过程来检查概率并输入预测错误的数据这将有助于我们建立机器学习的直觉这样我们就可以在未来构建更好的NLP流水线。这是通过人脑“反向传播”来解决模型调优问题的办法。
脏数据
之前在同样的数据上用不同的神经网络模型以完全相同的方式进行处理虽然可以看到模型类型的变化以及在给定数据集上的性能表现但也确实做出了一些损害数据完整的选择或者说“弄脏了”数据。
将每个样本填充或截断到400或其他特定数字个词条对卷积神经网络非常重要这样过滤器就可以“扫描”长度一致的向量。卷积网络也能输出一个长度一致的向量。对输出来说保持维数的一致是很重要的因为在链的末端输出将进入一个全连接的前馈层这个前馈层需要一个固定长度的向量作为输入。
类似的循环神经网络的实现包括简单的RNN和LSTM都在努力构造一个固定长度的思想向量我们可以将其传递到一个前馈层进行分类。一个对象的固定长度的向量表示如思想向量通常也被称为嵌入。因此思想向量的大小是相同的我们必须将网络展开至相同的时刻词条数。下面看网络占到为400个时刻的选择如何
def test_len(data,maxlen):total_lentruncatedexactpadded0for sample in data:total_lentotal_lenlen(sample)if len(sample)maxlen:truncatedtruncated1elif len(sample)maxlen:paddedpadded1else:exactexact1print(Paddes:{}.format(padded))print(Equal:{}.format(exact))print(Truncated:{}.format(truncated))print(Avg length:{}.format(total_len/len(data)))datasetpre_process_data(C:\\Users\\86185\\PycharmProjects\\pythonProject\\NLP\\aclImdb\\train)
vectorized_datatokenize_and_vectorize(dataset)
test_len(vectorized_data,400) 从结果来看400是一个偏高的数字现在将maxlen天回到140并让LSTM再尝试一次
import numpy as np
#声明超参数
maxlen140
#在反向传播和更新权重之前需要传递给网络的样本数
batch_size32
#需要传递进Convert的词条向量的长度
embedding_dims300
epochs2num_neurons50#收集数据并做好准备
datasetpre_process_data(C:\\Users\\86185\\PycharmProjects\\pythonProject\\NLP\\aclImdb\\train)
vectorized_datatokenize_and_vectorize(dataset)
exceptedcollect_excepted(dataset)
split_pointint(len(vectorized_data)*0.8)
#将数据划分为训练集和测试集
X_trainvectorized_data[:split_point]
y_trainexcepted[:split_point]
X_testvectorized_data[split_point:]
y_testexcepted[split_point:]X_trainpad_turnc(X_train,maxlen)
X_testpad_turnc(X_test,maxlen)
X_trainnp.reshape(X_train,(len(X_train),maxlen,embedding_dims))
y_trainnp.array(y_train)
X_testnp.reshape(X_test,(len(X_test),maxlen,embedding_dims))
y_testnp.array(y_test)
运行优化后的LSTM
modelSequential()
model.add(LSTM(num_neurons,return_sequencesTrue,input_shape(maxlen,embedding_dims)
))
model.add(Dropout(0.2))
model.add(Flatten())
model.add(Dense(1,activationsigmoid))
model.compile(rmsprop,binary_crossentropy,metrics[accuracy])
print(model.summary()) 训练一个更小的LSTM
model.fit(X_train,y_train,batch_sizebatch_size,epochsepochs,validation_data(X_test,y_test))model_structuremodel.to_json()
with open(lstm_model2.json,w) as json_file:json_file.write(model_structure)
model.save_weights(lstm_weight2.weights.h5) 这样训练的速度更快验证准确率也几乎没有下降74.2%对比75.2%。只使用1/3时刻的样本但训练时间将减少一大半以上。只有1/3的LSTM时刻需要计算并且在前馈层中只有一小半的权重需要学习。但重要的是反向传播每次只需走一半的距离。
但是精确率变低了因为在这两个模型中都包含一个dropout层。dropout层有助于防止过拟合因此当我们减小模型的自由度或减少训练周期数时验证精确率只会变得更低。
由于神经网络的强大功能以及它们学习复杂模式的能力人们时常忘记一个设计良好的神经网络善于学习丢弃噪声和系统偏差。我们把所有那些零向量都加进来无意中给数据带来了很大的偏差。即使所有输入都为零向量每个节点的偏置项元素也仍然会给它一些信号。但最终网络将学会完全忽略这些元素会将偏置项元素的权重特别调整为零从而专注于样本中包含有意义信息的部分。
所以优化后的LSTM虽然没能学到更多信息但是它可以学得更快。但是这里最重要的一点是要注意测试集样本的长度与训练集样本的长度有关。如果训练集是由数千个词条长的文档组成的那么将只有3个词条长度的文档填充到1000个词条就可能无法得到一个精确的分类结果。反之将一个1000个词条的文档截断到3个词条对于在3个词条长的文档中训练的小模型同样也会造成困扰。
“未知”词条的处理
在数据处理方面直接丢弃“未知”词条可能会带来麻烦。“未知”词条基本就是在预训练的Word2vec模型中找不到的词其列表非常大。直接丢弃这么多数据尤其是试图对词序列建模时通常会造成很大的问题。
当词嵌入词汇中不包含“不dont”这个词时类似与 I dontt like this movie 的句子可能会变成 I like this movie。
Word2vec中忽略了许多词条这些词条可能很重要也可能不重要。丢弃这些未知词条是一种处理策略但是还有其他策略可选。我们可以使用或训练一个词嵌入模型该模型中的每一个词条都会对应一个向量但是这样做会付出昂贵的代价。
有两种常见的方法可以在不增加计算需求的情况下提供更好的结果。这两种方法都涉及用新的向量表示替代未知的词条。第一种方法有些反直觉对于没有由向量建模的每个词条从现有词嵌入模型中随机选择一个向量并使用它。我们可以很容易的看出这会使人类感到困惑。
模型会解决一些小问题就像在之前的示例中不管它们一样。要记住我们并不是要显式地对训练集中的每个语句建模我们的目的是在训练集中创建一种通用的语言模型。这样就会存在一些异常值但我们不希望存在太多的异常值以至于描述主要语言模型时偏离模型。
第二种也是更常见的方法是在重构原始输入时用一个特定的词条替换词向量库中没有的所有词条这个特定的词条通常称为“UNK”未知词条。这个向量本身要么是在对原始嵌入建模时选择的要么是随机选择的理想情况是远离空间中已知的向量。
与填充一样网络可以学习如何绕过这些未知的词条并围绕它们得出自己的结论。
字符级建模英文
词是有含义的。用这些基本的模块来对自然语言建模看起来很自然使用这些模型从原子结构的角度来描述含义、情感、意图和其他一切似乎也很自然。但是英文中词不是原子性的它们是由单位更小的词、词干、音素等组成但更重要的一点是它们更基本的也都是由一系列字符构成的。
在对语言建模时许多含义隐藏在字符里面。语音语调、头韵、韵律——如果我们把它们分解到字符级别可以对所有这些建模。人类不需要分解得如此细致就可以为语言建模但是从建模中产生的定义非常复杂并不容易传授给机器这就是为什么要给字符建模。对于我们见过的字符当我们查看文本中哪个字符出现在哪个字符后面时可以发现文本中的许多固有的模式。
在这个范式中空格、逗号、句号都变成了另一个字符当网络从序列中学习含义时如果把它们分解成单个的字符模型就会被迫的来寻找这些更低层级的模式。当注意到有一些音节后面时重复的这可能是押韵的后缀可能是一种带有意义的模式或许代表着愉快或嘲笑的情感随着学习了足够大的训练集这些模式开始显现。因为英语中不同的字母要比词要少得多所以需要关心的输入向量相对也就少了。
然而在字符级别训练模型仍是很棘手的。在字符级别发现的模式和长期以来关系在不同的语调中可能会有很大的差异我们或许可以找到这些模式但它们可能不具有泛化性。
下面在同一样本数据集中在字符级别上尝试LSTM。首先需要用不通过的方式处理数据获取数据并通过标签排序
#收集数据并做好准备
datasetpre_process_data(C:\\Users\\86185\\PycharmProjects\\pythonProject\\NLP\\aclImdb\\train)
exceptedcollect_excepted(dataset)
然后需要决定将网络展开至多远所以需要观察数据样本中平均有多少个字符
def avg_len(data):total_len0for sample in data:total_lentotal_lenlen(sample[1])return total_len/len(data)print(avg_len(dataset)) 可以看到平均字符数是1325这代表网络将要被展开的很远需要等很长时间才能训练完成这个模型。
接下来清除一些与文本的自然语言无关的词条数据。此函数过滤出了数据集中HTML标签中的一些无用字符。实际上数据应该被更彻底地清洗
def clean_data(data):new_data[]VALIDabcdefghijklmnopqrstuvwxyz0123456789\?!.,:; for sample in data:new_sample[]for char in sample[1].lower():if char in VALID:new_sample.append(char)else:new_sample.append(UNK)new_data.append(new_sample)return new_datalistified_dataclean_data(dataset)
这里使用了‘UNK’表示列表中所有不出现在VALID列表中的单个字符。
然后将样本填充或截断到指定的maxlen长度。这里我们引入了另一用于填充的“单字符”——‘PAD’
def char_pad_trunc(data,maxlen1500):new_dataset[]for sample in data:if len(sample) maxlen:new_datasample[:maxlen]elif len(sample) maxlen:padsmaxlen-len(sample)new_datasample[PAD]*padselse:new_datasamplenew_dataset.append(new_data)return new_dataset
这里选择1500作为maxlen来获得比平均样本长度略多的样本数据但是应该尽量避免使用会带来过多噪声的PAD。考虑词的长度会帮助我们做出选择。在固定的字符长度下与完全由简单的单音节词组成的样本相比具有大量长词的样本可能被欠采样。与所有机器学习问题一样了解数据集和它的输入、输出非常重要。
下面使用独热编码字符而不是使用Word2vec。因此需要创建一个词条字符字典该字典被映射到一个整数索引。我们还将创建一个字典来映射相反的内容
def create_dicts(data):charsset()for sample in data:chars.update(set(sample))char_indicesdict((c,i) for i,c in enumerate(chars))indices_chardict((i,c) for i,c in enumerate(chars))return char_indices,indices_char
然后可以使用该字典来建立索引的输入向量而不是词条本身
def onehot_encode(dataset,char_indices,maxlen1500):Xnp.zeros((len(dataset),maxlen,len(char_indices.keys())))for i,sentence in enumerate(dataset):for t,char in enumerate(sentence):X[i,t,char_indices[char]]1return X
加载和预处理IMDB数据
datasetpre_process_data(C:\\Users\\86185\\PycharmProjects\\pythonProject\\NLP\\aclImdb\\train)
exceptedcollect_excepted(dataset)
listified_dataclean_data(dataset)common_length_datachar_pad_trunc(listified_data,maxlen1500)
char_indices,indices_charcreate_dicts(common_length_data)
encode_dataonehot_encode(common_length_data,char_indices,1500)
将数据集划分为训练集和测试集比例分别占80%、20%
split_pointint(len(encode_data)*0.8)
X_trainencode_data[:split_point]
y_trainexcepted[:split_point]
X_testencode_data[split_point:]
y_testexcepted[split_point:]
建立基于字符的LSTM网络
from keras.api.models import Sequential
from keras.api.layers import Dense,Dropout,Embedding,Flatten,LSTMnum_neurons40
maxlen1500
modelSequential()model.add(LSTM(num_neurons,return_sequencesTrue,input_shape(maxlen,len(char_indices.keys()))
))
model.add(Dropout(0.2))
model.add(Flatten())
model.add(Dense(1,activationsigmoid))
model.compile(rmsprop,binary_crossentropy,metrics[accuracy])
print(model.summary()) 这样在构建LSTM模型方面就变得更高效。这个最新的基于字符的模型只需要训练7.4万个参数而优化后的基于词的LSTM需要训练8万个参数。这个简单的模型应该训练得更快并能更好地推广到新文本因为它具有较小的过拟合自由度。
现在尝试一下看基于字符的LSTM模型需要提供什么参数 保存模型
model_structuremodel.to_json()
with open(char_lstm_model3.json,w) as json_file:json_file.write(model_structure)
model.save_weights(char_lstm_weights3.weights.h5)
最终的到的是85%的训练集精确率和59%的验证精确率说明模型出现了过拟合。模型开始缓慢地学习训练集的情感。虽然耗时是很久的但验证精确率却没有比随机猜测提高很多在后期的训练周期中甚至可能变得更差。
这可能是很多情况导致的对数据集来说模型可能过于强大这意味着它有足够的参数可以为训练集的20000个样本特有的模式进行建模但对于关注情感的通用语言模型则没有用处。如果LSTM网络层的dropout百分率更高或神经元更少这一问题可能会得到缓解。如果没有认定模型定义的参数量过于庞大那么更多的标注数据也会有所帮助。但是高质量的标注数据通常是最难获得的。
与词级LSTM模型甚至卷积神经网络相比这个回报有限的模型在硬件和时间上有巨大的开销。这是因为如果有更多、更广泛的数据集则字符级模型会非常擅长对语言建模。或者说在提供一套专项领域的训练集时它能为一种特定的语言类型建模。
生成聊天文章
如果能以特定的“风格”或“看法”生成新的文本我们肯定会拥有一个非常有趣的聊天机器人。当然能够生成具有给定风格的新文本并不能保证聊天机器人会谈论我们希望它谈论的事情。但是我们可以使用这种方法在给定的一组参数中生成大量文本例如相应某类用户的风格然后对于一个给定的查询可以基于这个新的、更大的文本语料库索引和搜索最有可能的回复。
就像一个马尔科夫链根据出现在1-gram、2-gram或者n-gram后的词预测序列将要出现的下一个词LSTM模型也可以基于它刚刚看到的词学习预测下一个词出现的频率。这就是记忆带来的好处。马尔科夫链只使用n-gram以及在n-gram之后出现的词的频率信息来进行搜索。RNN模型也做了类似的事情它基于前几项的下一项的信息进行编码但是有了LSTM的记忆状态模型可以在更大的上下文中判断最合适的下一项。而且我们还可以根据之前文档出现过的字符来预测下一个字符。这种粒度级别超出了基本的马尔科夫链。
LSTM模型学习的真正核心是LSTM元胞本身但我们确实在围绕特定分类任务的成功和失败来训练模型这种方法不一定能帮助我们的模型学习语言的一般表现形式我们训练它只关注那些包含了强烈情感的序列。
因此我们应该使用训练样本本身而不是使用训练集的情感标签来作为学习的目标。对于样本中的每个词条我们希望LSTM模型能学会预测下一个词条下左图。这与词向量嵌入方法非常相似只是我们将通过2-gram而不是skip-gram来训练网络。以这种方法训练的词生成器模型可以很好的工作我们使用这个方法可以直接获得字符级别的表示下右图。 这里将关心每一个时刻的输出而不是最后一个时刻输出得到的思想向量。误差仍然会由每一个时刻随时间反向传播回到开始时刻但是误差是有每个时刻级别的输出确定的。在某种意义上其他LSTM分类器中也是这样的但在其他分类器中直到序列末尾才确定误差。只有在序列的末尾才有一个聚合的输出用来输入链末端的前馈层。尽管如此反向传播仍然以相同的方式工作者——通过调整序列末尾的所有权来聚合误差。
所以要做的第一件事就是调整训练集的标签。输出向量对比的不是给定的分类标签而是序列中下一个字符的独热编码。
我们可以回到更简单的模型这次不是试图预测每个后续字符而是预测给定序列的下一个字符。如果去掉关键词参数return_sequencesTrue这与其他LSTM层相同这样做将使LSTM模型聚焦于序列中最后一个时刻的返回值 进一步生成文本
简单的字符级建模是通向更复杂模型的必经之路——这些模型不仅可以获取拼写等细节还可以获取语法和标点符号。而且当这些模型学习这些语法细节时它们也开始学习文本的节奏和韵律。
Keras文档提供了一个很好的例子。对于寻找像音调和词选择这样深奥的概念电影评论数据集有两个难以解决的问题它是多样化的由许多作者写成每个作者都有自己的写作风格和个性找到它们之间的共同点十分困难对于学习基于字符的通用语言模型它是一个非常小的数据集。为了解决上述问题我们需要一个在样本风格和音调更一致的数据集或者一个大得多的数据集这里选择前者。这里选择莎士比亚作品的样本
from nltk.corpus import gutenbergprint(gutenberg.fileids()) 上面给出的是莎士比亚的3部戏剧。我们将获取它们的资源并将它们拼接到一个大字符串中
text
for txt in gutenberg.fileids():if shakespeare in txt:texttextgutenberg.raw(txt).lower()
charssorted(list(set(text)))
char_indicesdict((c,i) for i,c in enumerate(chars))
indices_chardict((i,c) for i,c in enumerate(chars))
print(长度{}chars{}.format(len(text),len(chars))) 格式样式
print(text[:500]) 接下来把原始文本分层数据样本每个样本都有固定的maxlen个字符。为了增加数据集的大小并关注它们一致的语言模式Keras示例过采样了数据并细分为半冗余块。从开头处取40个字符从开头移到第3个字符从那里取40个字符移到第6个字符……以此类推。
要记住这个模型的目标是在给定前40个字符的情况下学习预测任意序列中的第41个字符。因此我们将构建一组半冗余序列的训练集每个序列有40个字符长
maxlen40
step3
sentences[]
next_chars[]
for i in range(0,len(text)-maxlen,step):sentences.append(text[i:imaxlen])next_chars.append(text[imaxlen])
print(nb sequences:,len(sentences)) 所以现在拥有了123168个训练样本和在它们之后的字符即模型的目标标签
Xnp.zeros((len(sentences),maxlen,len(chars)),dtypenp.bool)
ynp.zeros((len(sentences),len(chars)),dtypenp.bool)
for i,sentence in enumerate(sentences):for t,char in enumerate(sentence):X[i,t,char_indices[char]]1y[i,char_indices[next_chars[i]]]1
然后在数据集中对每个样本的每个字符进行独热编码并将其存储在列表X中我们还会将独热编码的“答案”存储在列表y中然后构造模型
from keras.api.models import Sequential
from keras.api.layers import Dense,Activation
from keras.api.layers import LSTM
from keras.api.optimizers import RMSpropmodelSequential()
model.add(LSTM(128,input_shape(maxlen,len(chars))
))
model.add(Dense(len(chars)))
model.add(Activation(softmax))
optimizerRMSprop(learning_rate0.01)
model.compile(losscategorical_crossentropy,optimizeroptimizer)
print(model.summary()) 这和之前看起来有些不同在本例中LSTM元胞隐藏层中的num_neuron为128。虽然128比分类器中使用的要多很多但是我们是在试图为在复现给定文本的音调中更复杂的行为进行建模。然后优化器是通过一个变量定义的这也是到目前为止一直在使用的。因为学习率参数从其默认值0.001调整为现在的值这里将其分开。值得注意的是RMSProp的工作原理是通过使用“该权重最近梯度大小的平均值”来调整学习率以更新各个权重。
下一个不同之处是我们试图最小化的损失函数。到目前为止它一直是binary_crossentropy。我们只需要确定单个神经元的阈值但是在这里我们已经将最后一次中的Dense(1)替换成Dense(len(chars))。因此网络在每个时刻的输出将是一个50维的向量len(chars)50。我们将使用softmax作为激活函数因此输出向量将等效为整个50维向量上的概率分布该向量中的值之和总是为1。使用categorical_crossentropy试图使结果的概率分布与独热编码预期字符之间的差异最小化。
最后一个主要的变化是没有dropout。因为我们要对这个数据集进行特定的建模而没有兴趣将其推广到其他问题所以过拟合不仅是可以的还是理想的
epochs6
batch_size128
model_structuremodel.to_json()
with open(shake_lstm_model.json,w) as json_file:json_file.write(model_structure)
for i in range(5):model.fit(X,y,batch_sizebatch_size,epochsepochs)model.save_weights(shake_lstm_weights_{}.weights.h5.format(i)) 这里设置每隔6个训练周期保存模型一次并继续训练。如果它的损失不再减少就不需要继续训练那么我们可以安全的停止这个过程并在之前的几个训练周期里保存好权重集。我们发现需要20-30个训练周期才能从这个数据集中获得还不错的结果。我们可以查看扩展数据集。莎士比亚的作品是可以公开获取的如果是从不同的来源获得的则需要通过适当的预处理来确保作品的一致性。还好基于字符的模型不比担心分词器和分句器的不一致但是保持字符一致性的方法选择会很重要。
因为输出向量是描述50个可能的输出字符丧的概率分布的50维向量所以可以从该分布中采样。Keras示例有一个辅助函数来完成这一任务
import random
def sample(preds,temperature1.0):predsnp.asarray(preds).astype(float64)predsnp.log(preds)/temperatureexp_predsnp.exp(preds)predsexp_preds/np.sum(exp_preds)probasnp.random.multinomial(1,preds,1)return np.argmax(probas)
由于网络的最后一次是softmax因此输出向量将是网络所有可能输出的概率分布。通过查看输出向量中的最大值我们可以看到网络认为出现概率最高的下一个字符。用更清楚的话来说输出向量最大值的索引该值介于0到1 之间将与预期词条的独热编码的索引相关联。
但是在这里我们并不是要精确地重新创建输入文本而是要重新创建接下来可能出现的文本就像在马尔科夫链中一样下一个词条是根据下一个词条的概率随机选择的而不是最常出现的下一个词条。
log函数除以temperature的效果是使概率分布变平temperature1或变尖temperature1。因此小于1的temperature或称调用参数中的多样性倾向于试图更严格的重新创建原始文本。而大于temperature会产生更多样化的结果但是随着分布变平学习到的模式开始被遗忘我们就会回到胡言乱语的状态。多样性越高就会越有趣。
numpy随机函数multinomial(num_samples,probability_list,size)将从分布中生成num_samples个样本其可能的结果由probability_list描述它将输出一个长度为size的列表该列表等于实验运行的次数。在这种情况下我们只从概率分布中抽取一次我们只需要一个样本。
当我们进行预测时Keras示例有一个遍历不同的temperature值的循环因此每个预测都将看到一系列不同的输出而这些输出基于sample函数从概率分布进行采样所使用的temperature值
import sysstart_indexrandom.randint(0,len(text)-maxlen-1)
for diversity in [0.2,0.5,1.0]:print()print(---- diversity,diversity)generatedsentencetext[start_index:start_indexmaxlen]generatedgeneratedsentenceprint(---- Generateing with seed: sentence)sys.stdout.write(generated)for i in range(400):xnp.zeros((1,maxlen,len(chars)))for t,char in enumerate(sentence):x[0,t,char_indices[char]]1predsmodel.predict(x,verbose0)[0]next_indexsample(preds,diversity)next_charindices_chat[next_index]generatedgeneratednext_charsentencesentence[1:]next_charsys.stdout.write(next_char)sys.stdout.flush()print()
从源文本中提取40maxlen个字符的随机块并预测接下来会出现什么字符。然后将预测的字符追加到输入句子中删除第一个字符并肩这40个字符作为新的输入再次预测。每次将预测的字符写入控制台并执行flush()函数以便字符即刻进入控制台。如果预测的字符恰好是一个换行符那么这一行的文本就结束了但是生成器将继续工作从它刚刚输出的前40个字符预测下一行。
我们将会得到像下面这样的结果
---- diversity 0.2
---- Generateing with seed: cb. well then,
now haue you considerd o
cb. well then,
now haue you considerd oobu0,hexg.9t;e5m;luk.g3y];!w[rw4?q9)ym2?w92ææebv?t2ng qo,3)qx?s3rtv43wlsi z1æs-rfdx,p06[w-[ruoæxs 1rlkwcq2pt,a)c]wazoh3(? m:fo:ur,oe09ei49z229aunnjæ[g3s6![00rskx9;jm;alurxe2j-æhq1d)fh!æ:3?exmæl]tbk4r,o
xbk1m: 4)6sb3 w5
w.s;,a5slyzg[s55yn5r5(ca1f?,i2bqa5h]jk3mbr49
y]s?[w)mtu u??g?-gyf5 bsr,x0,y!em,y,obi(pe60-9kn-
b3ez14(j549z.ki
æ43mdiuin6t
0[mayu5xu9yle[vo.5x-0yntmk3!]f.
fx,9m---- diversity 0.5
---- Generateing with seed: cb. well then,
now haue you considerd o
cb. well then,
now haue you considerd os,)9
:maqvvta.3pp3r1dq;5xmscqrase,d[ilm])w05et0o3scv!e,0,smi(p;]er.(zx! ci h?wufk-?x,jk-4z
t3æ)z2ts](.vg- dnq]z
4yelj(qos]4y?)).3;w,syk(?:c440dk:5?4b!nis5](r]l-iph4qcni-ewp.1fkrbi5):cb9)[l0-(,l9
!fzn[up25q.xu;:j
6r)4
9)9.?e.o)2a;?!6æ;6ir](ne5d0cwb:)-tærps05y)td]mu!?(: rdylh
ca0c;[w;bhfdwh614d(iz[wgæ9j3i:[h9yr,1r2rk1;l)em)!;5
æw2tm.p0[z)-jhbew3-9 -ei0s([!cm,g(c?bf;[b42d!]l[qyq9rt;c;4n99x6---- diversity 1.0
---- Generateing with seed: cb. well then,
now haue you considerd o
cb. well then,
now haue you considerd oew(a
.q;lppmgil:iv0:n1;nk-y.4f.geq4yyspw[dtrddt-00e c w)ybcl44)udo-u-l
)ciyz[05490nxf[u!mc!!fp,b[g2!,;q1me1a?:h(r.mæaaj!h0[r96!-isi
ao:n!6v,
lg ,-rmihcf]i2:0rf9 ]5rc4d,i65x
t:n:)dnlmu)hy6slmrv1n3bx-?kekf?t.tx3wvt3
u56]n-bbs;0;:]3o1
(.u4
4[qqyjx[i?qiæ
jw[wk-a36b
s.3qtgzbmu]py920.c4a4
egp)-j,udba-æe.ue3wd
i[2niri4mw;v]rwrie;hryaæ.6;m,r-,ojcss;v-gs)ohv)09pf;q ]][q.q6r q:p;6c3h1cku]52l sk
文本生成的问题内容不受控
我们仅仅基于示例文本来生成新文本并且我们还可以学习如何从示例文本中提取出其写作风格。但是我们却无法控制生成的文本内容这有些反直觉。上下文内容受限于原始数据如果没有其他内容那么将会限制模型的词汇量。但还是如果给定一个输入我们就可以按照我们认为原作者或作者可能会说的话进行训练。从这种模型中能够期待的最好结果是它们的说话方式特别是它们怎样从一个种子句开始把话说完。这个种子句不一定来自训练文本本身。因为这个模型是针对字符进行训练的所以我们可以使用全新的词作为种子并获得有趣的结果。
其他记忆机制
LSTM是循环神经网络基本概念的一种扩展同样也存在其他各种形式的扩展。所有这些扩展无外乎都是元胞内门的数量或运算的一些微调。例如门控循环单元将遗忘门和候选门中的候选选择分支组合成一个更新门。这个门减少了需要学习的参数数量并且已经被证实可以与标准LSTM相媲美同时计算开销也要小得多。Keras提供了一个GRU层我们可以像使用LSTM一样使用它
from keras.api.models import Sequential
from keras.api.layers import GRUmodelSequential()
model.add(GRU(num_neurons,return_sequencesTrue,input_shapeX[0].shape))
另一种技术是使用具有窥视孔连接的LSTM。Keras没有直接实现的代码但是晚上的几个示例通过扩展Keras的LSTM类来实现这一点。其思想是标准LSTM元胞中的每个门都可以直接访问当前记忆状态并将其作为输入的一部分。所有的门包含与记忆状态相同维度的额外权重。然后每个门的输入是该时刻元胞的输入、前一个时刻元胞的输出和记忆状态本身的拼装。在时间序列数据中这使得时间序列事件建模更加精确。虽然它们并没有专门在NLP领域工作但这个概念在这里也是有效的。
更深的网络
将记忆单元看作是对名词/动词对货句子与句子之间动词时态引用的特点表示进行编码非常方便但这并不是实际发行的事情。假设训练顺利的话这不过刚好是网络学习的语言模式的一个副产品。与所有神经网络一样分层允许模型在训练数据中形成更复杂的模式表示。我们也可以轻松的堆叠LSTM层 训练堆叠层在计算上代价非常昂贵但在Keras中把它们堆叠起来只需要几秒
from keras.api.models import Sequential
from keras.api.layers import LSTM
modelSequential()
model.add(LSTM(num_neurons,return_sequencesTrue,input_shapeX[0].shape
))
model.add(LSTM(num_neurons_2,return_sequencesTrue))
要注意的是加入要正确的构建模型需要在第一层和中间层使用参数return_sequencesTrue。这个要求是有意义的因为每个时刻的输出都需要作为下一层时刻的输入。
但是要记住创建一个能够表示比训练数据中存在的更复杂的关系的模型可能会导致奇怪的结果。简单地在模型上叠加层虽然很有趣但很少是构建最有用的模型的解决方案。