AI 开发者按,本文的作者是数据科学家 Ma?l Fabien。在过去的几个月里,他在个人博客上写了 100 多篇文章。这个内容量相当可观。他突然想到一个主意:训练一个能像他一样说话的语言生成模型。
为此,他写了一篇文章分享了生成一个像人一样说话的神经网络模型的过程和相关代码,他的文章内容如下:
我想训练一个能像我一样说话的语言生成模型,或者更具体地说,一个可以像我一样写作的模型。它可以完美的说明语言生成的主要概念、使用 keras 实现语言生成模型,以及我的模型的局限性。
本文的全部代码都可以在这个 repo 中找到:
在我们开始之前,我想分享这个意外发现的资源——Kaggle Kernel,它对理解语言生成算法结构来说是非常有用资源。
语言生成
自然语言生成的目的是生成有意义的自然语言。
大多数情况下,内容是作为单个单词的序列生成的。总的来说,它的工作原理如下:
你训练一个模型来预测序列中的下一个单词
你给经过训练的模型一个输入
重复上面的步骤 n 次,生成接下来的 n 个单词
序列预测的过程
1.创建数据集
第一步是构建一个数据集,以便我们稍后将要构建的网络可以理解这个数据集。首先导入以下包:
from keras.layers import Embedding, LSTM, Dense, Dropout ?
from keras.callbacks import EarlyStopping ?
from keras.models import Sequential ?
import keras.utils as ku ?
import pandas as pd ?
import numpy as np ?
import string, os
a.加载数据
我写的每一篇文章的标题都遵循这个模板:
这是我们通常不希望在最终数据集中包含的内容类型。相反,我们将关注文本本身。
所有文章都写在一个单独的 Markdown 文件中。标题基本上包含了标题、图片标题等信息。
B.句子标记
然后,打开每一篇文章,并将每一篇文章的内容添加到列表中。但是,由于我们的目标是生成句子,而不是生成整篇文章,因此我们将把每一篇文章拆分成一个句子列表,并将每个句子附加到「all_sentences」列表中:
all_sentences= []
for file in glob.glob("*.md"):
?f = open(file,'r')
?txt = f.read().replace("\n", " ")
?try:
? ? ?sent_text = nltk.sent_tokenize(''.join(txt.split("---")[2]).strip())
? ? ?for k in sent_text :
? ? ? ? ?all_sentences.append(k)
?except :
? ? ?pass
总的来说,我们有超过 6800 个训练的句子。目前的过程如下:
句子拆分
c. N-gram 创建
然后,我的想法是根据一起出现的单词创建 N-grams。为此,我们需要:
在语料库上安装一个标记器,将索引与每个标记相关联
把语料库中的每个句子分解成一系列的标记
存储一起发生的标记序列
可通过下图来理解这个过程:
N-gram 创建
接下来,让我们来实现它。我们首先需要安装标记器:
tokenizer = Tokenizer()
tokenizer.fit_on_texts(all_sentences)
total_words = len(tokenizer.word_index) + 1
变量「total_words」包含已使用的不同单词的总数,这里的数值是 8976。然后,对于每个句子,获取相应的标记并生成 N-grams:
input_sequences = [] ?
# For each sentence ?
for sent in all_sentences: ?
# Get the corresponding token ?
token_list = tokenizer.texts_to_sequences([sent])[0] ?
# Create the corresponding n-grams ?
for i in range(1, len(token_list)): ?
n_gram_sequence = token_list[:i+1] ?
input_sequences.append(n_gram_sequence) ?
「token_list」变量将语句作为标记序列包含:
然后,'n_gram_sequences' 序列创建 n-grams。它从前两个单词开始,然后逐渐添加单词:
d.Padding
我们现在面临的问题是:不是所有的序列都有相同的长度!那么,如何解决这个问题?
我们将使用 Padding。Padding 在变量“input_sequences”的每一行之前添加 0 序列,这样每一行的长度就与最长的行的长度相同了。
Padding 的解释
为了将所有句子填充到句子的最大长度,我们必须首先找到最长的句子:
max_sequence_len = max([len(x) for x in input_sequences])
在我的例子里面它等于 792。好吧,对单个句子来说它已经够大了!由于我的博客包含了一些代码和教程,我希望这一句话是由 python 代码编写的。让我们绘制序列长度的直方图:
import matplotlib.pyplot as plt ?
plt.figure(figsize=(12,8)) ?
plt.hist([len(x) for x in input_sequences], bins=50) ?
plt.axvline(max_sequence_len, c="r") ?
plt.title("Sequence Length") ?
plt.show() ?
序列长度
在单个句子中,很少有例子会超过 200 个单词。如果把最大序列长度设为 200 会如何?
max_sequence_len = 200
input_sequences = np.array(pad_sequences(input_sequences, maxlen=max_sequence_len, padding='pre'))
其输出结果为:
e.拆分 X 和 Y
现在我们有了固定长度的数组,其中大多数在实际序列之前填充了 0。好吧,我们怎么把它变成一个训练集?我们需要拆分 X 和 Y!记住,我们的目标是预测序列中的下一个单词。因此,我们必须将除最后一个标记外的所有标记作为 X,并将最后一个标记作为 Y。
拆分 X 和 Y
在 python 中,它就和下面的语句一样简单:
X, y = input_sequences[:,:-1],input_sequences[:,-1]
我们现在将这个问题看作一个多分类任务。像往常一样,我们必须首先对 y 进行 one-hot 编码,以获得一个稀疏矩阵,该矩阵在对应于该标记的列中包含 1,在其他位置包含 0:
在 python 中,使用「keras utils to_categorical」:
y = ku.to_categorical(y, num_classes=total_words)
X 的 shape 是?(164496, 199),Y 的 shape 是?(164496, 8976)。
我们有大约 165000 个训练样本。X 是 199 列宽,因为它对应于我们允许的最长序列(200-1,要预测的标签)。Y 有 8976 列,对应于所有词汇的稀疏矩阵。数据集现在准备好了!
2.建立模型
我们将使用 Long Short-Term Memory networks (LSTM)。LSTM 的一个重要优点是能够理解对整个序列的依赖性,因此,句子的开头可能会对要预测的第 15 个单词也产生影响。另一方面,递归神经网络(RNNs)只意味着依赖于网络的前一个状态,只有前一个词才能帮助预测下一个状态。如果选择 RNN,我们很快就会错过上下文,因此,LSTM 应该是目前的最佳选择。
a.模型架构
由于训练可以非常(非常)(非常)(非常)(非常)(不开玩笑)长,我们将构建一个简单的 1 Embedding + 1 LSTM 层 + 1 密集网络:
def create_model(max_sequence_len, total_words):
?input_len = max_sequence_len - 1
?model = Sequential()
?# Add Input Embedding Layer
?model.add(Embedding(total_words, 10, input_length=input_len))
?# Add Hidden Layer 1 - LSTM Layer
?model.add(LSTM(100))
?model.add(Dropout(0.1))
?# Add Output Layer
?model.add(Dense(total_words, activation='softmax'))
?model.compile(loss='categorical_crossentropy', optimizer='adam')
?return model
model = create_model(max_sequence_len, total_words)
model.summary()
首先,我们添加一个 embedding 层。我们将其传递到一个有 100 个神经元的 LSTM 中,添加一个 dropout 来控制神经元共适应,最后是一个稠密层。注意,我们在最后一层应用一个 softmax 激活函数来获得输出属于每个类的概率。由于损失是一个多分类问题,因此使用的损失是分类交叉熵。
模型大体情况如下:
模型概览
b.训练模型
我们终于可以开始训练模型啦!
model.fit(X, y, batch_size=256, epochs=100, verbose=True)
然后模型的训练就开始啦:
在 CPU上,一个 epoch 大约需要 8 分钟。在 GPU 上(例如在 Colab 中),你应该修改使用的 Keras LSTM 网络,因为它不能在 GPU 上使用。相反,你需要:
# Modify Import
from keras.layers import Embedding, LSTM, Dense, Dropout, CuDNNLSTM
# In the Moddel
...
?model.add(CuDNNLSTM(100))
...
我倾向于在几个步骤中停止训练,以便进行样本预测,并在给定交叉熵的几个值时控制模型的质量。
以下是我的结果:
3.生成序列
如果你读到这里,接下来就是你所期望的了:生成新的句子!要生成句子,我们需要对输入文本应用相同的转换。我们将构建一个循环,在给定的迭代次数内生成下一个单词:
input_txt = "Machine"
for _ in range(10):
?# Get tokens
?token_list = tokenizer.texts_to_sequences([input_txt])[0] ? ?# Pad the sequence
?token_list = pad_sequences([token_list], maxlen=max_sequence_len-1, padding='pre') ? ?# Predict the class
?predicted = model.predict_classes(token_list, verbose=0)
?output_word = ""
?# Get the corresponding work
?for word,index in tokenizer.word_index.items():
? ? ?if index == predicted:
? ? ? ? ?output_word = word
? ? ? ? ?break
?input_txt += " "+output_word
当损失在 3.1 左右时,以「google」作为输入生成的句子如下:
Google is a large amount of data produced worldwide!
这并没有什么实际意义,但它成功地将谷歌与大数据的概念联系起来。这是相当令人印象深刻的,因为它仅仅依赖于单词的共现,而没有整合任何语法概念。
如果我们在训练中稍等一段时间,让损失减少到 2.5,并给它输入「Random Forest」:
Random Forest is a fully managed service distributed designed to support a large amount of startups vision infrastructure。
同样,生成的内容没有意义,但语法结构相当正确。
损失在大约 50 个 epoch 后开始分化,并从未低于 2.5。
我想我们已经达到了这个方法的极限:
模型仍然很简单
训练数据不够清晰
数据量非常有限
也就是说,我发现结果非常有趣,例如,经过训练的模型可以很容易地部署在 Flask WebApp 上。
结论
我希望这篇文章对你有用。我试图说明语言生成的主要概念、挑战和限制。当然,与本文讨论的方法相比,更大的网络和更好的数据无疑是改进的源泉。
雷锋网雷锋网雷锋网
领取专属 10元无门槛券
私享最新 技术干货