用Python打造一个AI作家为你写诗

从短篇故事到长达5万词的小说,机器正以不可思议的方式“把玩”文字。网上已经涌现很多例子,越来越多人让机器创作文字作品。

 

其实,由于自然语言处理(NLP)领域的重大进步,如今计算机的确能够理解上下文并自行编写故事。比如去年我们曾分享过美国一位程序员小哥,由于《冰与火之歌》迟迟不出第六部,于是自己忍不住让AI创作了第六部,结果虽然有些无厘头,但读起来也满满的“冰火”范儿。

那我们自己能创建这样一个会写故事的 AI 吗?

本文,我们将使用 Python 和文本生成的概念来构建一个机器学习模型,最后它可以用莎士比亚的风格写出十四行诗。

 

我们开始吧!

 

本文内容概览

 

  1. 什么是文本生成器?

  2. 文本生成的不同步骤

  • 导入环境依赖项

  • 加载数据

  • 创建字符/字映射

  • 数据预处理

  • 搭建模型

  • 生成文本

3. 试验不同的模型

  • 更训练有素的模型

  • 更深层的模型

  • 更宽泛的模型

  • 一个巨大的模型

 

什么是文本生成器?

如今,有大量的数据可以归类为序列数据,常见形式为音频、视频、文本、时间序列、传感器数据等。这种类型数据有种特殊情况,如果在特定时间帧内发生两个事件,则在事件 B 之前发生事件 A 与在事件 B 之后发生事件 A 完全是两种不同的情况。

 

但是,在传统的机器学习问题中,一个特定的数据点是否先于另一个数据点被记录下来并不重要。这种考虑让我们有不同的方式解决序列预测问题。

 

文本,即一个接一个排列的一串串字符,其实是很难破解的。这是因为在处理文本时,用先前存在的序列训练的模型或许能做出非常准确的预测,但一旦出现一处错误的预测,就有可能使整个句子变得毫无意义。然而,如果出现数值序列预测问题,即使预测完全失败,仍然有可能被视为有效的预测(可能具有高偏差)。但是,这一点人们往往注意不到。

这就是文本生成器的一大棘手问题!

 

文本生成通常包含以下步骤:

1.导入环境依赖项

2.加载数据

3.创建字符/字映射

4.数据预处理

5.创建模型

6.生成文本

我们来仔细看看每一个步骤。

 

导入环境依赖项

这一步没什么说的,我们需要导入我们研究所需的所有资料库。

import numpy as npimport pandas as pdfrom keras.models import Sequentialfrom keras.layers import Densefrom keras.layers import Dropoutfrom keras.layers import LSTMfrom keras.utils import np_utils

 

加载数据

text=(open("/Users/pranjal/Desktop/text_generator/sonnets.txt").read())text=text.lower()

 

这一步中,我们加载下载的所有莎士比亚十四行诗的合集

http://www.gutenberg.org/ebooks/1041?msg=welcome_stranger

我清洗了文件,删除了开始和结尾部分,你可以从我的 git 库上下载。文件格式为 text。然后将这些内容转换为小写字母,以尽可能减少单词数量(稍后会详细介绍)。

 

创建字符/字词映射

映射是我们为文本中的字符/单词分配任意数字的一个步骤。通过这种方式,会将每个字符/单词映射到一个数字上。这很重要,因为机器理解数字比理解文本要容易的多,并且这也会让训练过程更容易。

characters = sorted(list(set(text)))n_to_char = {n:char for n, char in enumerate(characters)}char_to_n = {char:n for n, char in enumerate(characters)}

 

我创建了一个字典,其中包含分配给文本中每个唯一字符的数字。所有唯一字符首先存为字符类型,然后变为枚举类型。

 

这里还必须指出,我使用了字符级映射,而不是单词映射。但是,相比于基于字符的模型,基于单词的模型显示出更高的准确性。这是因为后一种模式需要一个更大的网络来学习长期相关关系,因为它不仅要记住单词的顺序,还必须学会去预测一个单词在语法上的正确性。但是,基于单词的模型已经可以满足后者(即:预测一个单词在语法上的正确性)。

 

但由于这是一个小数据集(有17,670个单词),并且唯一单词的数量(4,605个数字)构成了大约四分之一的数据,所以在这样的映射上进行训练并不是一个明智的决定。这是因为如果我们假设所有唯一单词在数量上都是相同的(这不是真的),那么我们会在整个训练数据集中大约出现四次单词,这不足以构建文本生成器。

 

数据预处理

这是搭建 LSTM 模型时最棘手的部分。将手头的数据转换为可相互转换的格式是一项艰巨的任务。

 

我把这个过程分解成更细小的部分,让你更容易理解。

X = []Y = []length = len(text)seq_length = 100 for i in range(0, length-seq_length, 1):    sequence = text[i:i + seq_length]    label =text[i + seq_length]    X.append([char_to_n[char] for char in sequence])    Y.append(char_to_n[label])

 

这里,X 是我们的训练数组,Y 是我们的目标数组。

seq_length 是我们在预测特定字符之前要考虑的字符序列的长度。

该 for 循环被用于迭代文本的整个长度,并创建此类序列(存储在X)和它们的真值(存储在Y)。现在,很难在这里看到真实值的概念。让我们以一个例子来理解这一点:

 

对于长度为4的序列和文本“ hello india ”,我们可以使用我们的X和Y(不易编码为易理解的数字),如下所示:

 

640?wx_fmt=jpeg

 

现在,LSTMs 以(number_of_sequences,length_of_sequence,number_of_features)形式接受输入,该输入不是数组的当前格式。另外,我们需要将数组 Y 转换为 one-hot 编码格式。

X_modified = np.reshape(X, (len(X), seq_length, 1))X_modified = X_modified / float(len(characters))Y_modified = np_utils.to_categorical(Y)

 

我们首先将数组 X 重塑成我们所需的维度(用来定义变量)。然后,我们调整 X_modified 的值,这样我们的神经网络可以训练得更快,而且陷入局部最小值的机会也更小。此外,我们的 Y_modified 是一种 one-hot 编码,用于删除可能在映射字符的过程中引入的任何序数关系。也就是说,与'z'相比,'a'可能被分配给较小的数字, 但这并不表明两者之间的任何关系。

 

我们的最终数组将如下所示:

640?wx_fmt=jpeg

 

搭建模型

 

model = Sequential()model.add(LSTM(400, input_shape=(X_modified.shape[1], X_modified.shape[2]), return_sequences=True))model.add(Dropout(0.2))model.add(LSTM(400))model.add(Dropout(0.2))model.add(Dense(Y_modified.shape[1], activation='softmax'))model.compile(loss='categorical_crossentropy', optimizer='adam')

 

我们需要搭建一个具有两个 LSTM 层的序列模型,每层有 400 个单元。第一层需要输入形状。为了让下一个 LSTM 层能够处理相同的序列,我们输入 return_sequences 参数为 True。

 

此外,还添加了 dropout 率为 20% 的 dropout 层以检查其是否过拟合。最后一层输出一个提供了字符输出的独热编码矢量。

 

生成文本

string_mapped = X[99]# generating charactersfor i in range(seq_length):   x = np.reshape(string_mapped,(1,len(string_mapped), 1))   x = x / float(len(characters))   pred_index = np.argmax(model.predict(x, verbose=0))   seq = [n_to_char[value] for value in string_mapped]   string_mapped.append(pred_index)string_mapped = string_mapped[1:len(string_mapped)]

 

我们从 X 数组中的随机行开始,它是一个由 100 个字符组成的数组。在此之后,我们的目标是预测 X 之后的另外 100 个字符。将输入进行重塑,按先前的方式缩放,这样就能预测出具有最大概率的下一个字符。

 

Seq 用来存储迄今预测到的字符串的解码格式。接下来,会更新新的字符串,这样会移除第一个字符,并且被预测的新字符也包括在内。

 

你可以到我的 git 库上查看完整代码。我也提供了训练文件,笔记和训练后的模型权重以供参考。(见文末)

 

试验不同的模型

以批次大小100训练1个周期后,基线模型输出结果如下:

 

's the riper should by time decease,his tender heir might bear his memory:but thou, contracted toet she the the the the the the the thethi the the the the the the the the the the the the the the the the thethi the the the the the the the the the the the the the the the the thethi the the the the the the the the the the the the the the the the thethi the the the the the the the the the the the the the the the the thethi the the the the the the the the th'

 

这个...

640?wx_fmt=jpeg

 

这个输出很明显没有多大意义。这只不过是重复同样的预测,就好像它陷入循环一样。这是因为和我们已经训练过的微型模型相比,语言预测模型太复杂了。

我们试试再训练这个模型,但训练时间再加长一点。

 

更训练有素的模型

这次我们以批次大小 50 将模型训练了 100 个周期,我们至少获得了一个不重复的字符序列,其中包含了很多合理的字词。此外,模型还学会了生成类似于十四行诗的词语结构。

 

'The riper should by time decease,
his tender heir might bear his memory:
but thou, contracted to thine own besire,
that in the breath ther doomownd wron to ray,
dorh part nit backn oy steresc douh dxcel;
for that i have beauty lekeng norirness,
for all the foowing of a former sight,
which in the remame douh a foure to his,
that in the very bumees of toue mart detenese;
how ap i am nnw love, he past doth fiamee.
to diserace but in the orsths of are orider,
waie agliemt would have me '

 

但是,这种模式还不足以制作出高质量的内容。所以接下来我们会和其他人一样,当深度学习模式没有产生好的结果时,搭建更深层次的模型架构。

 

更深层的模型

鲁迅曾经说过:如果模型不好,增加层数!

640?wx_fmt=jpeg

我的模型也要这样。我们再添加一个有 400 个单元的 LSTM 层,然后添加一个20% dropout率的 dropout 层,并查看我们得到的结果。

 

"The riper should by time decease,
his tender heir might bear his memory:
but thou, contracted to the world's false sporoe,
with eyes so dond touls be thy domfornds,
which for memorion of the seasons new;
mike own self-love to shou art constant
how can i then be oy love doth give,
the rose looks fair, but fairer bomments age.
now with the seas that i have seen dychl
that mot to teed of you, if h ho bontent,
and all the ress of the heartai thee;
the world will was iis bound and farty "

 

结果很有趣。语法正确率大大增强,保持了十四行诗结构和标点符号的完整。但是,模型仍然需要很多改进。我们接着尝试探索更宽泛的、拥有更多单元的网络。

 

更宽泛的模型

我在两个 LSTM 层的每一层上又增加了700个单元。这样调整以后,模型写出了以下诗歌:

 

"The riper should by time decease,
his tender heir might bear his memory:
but thou, contracted to thine own bright eyes,
feed'st thy sigh aare so beuider poise,
oor maty dis surre that gairs mot me love thee;
when i braye the would and lays in the eesire.
than thmu disgrmed stand with my bootr still nsentente;
tell ia to thou art thou wilt woon'thy sook,
and touain then tor, give thy soue semping.
whose whod the better of your befaiss nu sante,
though i, suoll oas, and i lose bouh twa"

 

起初这有点令人失望,因为这些词语已经失去了意义。但是,有趣的是,我们可以看到正渐渐出现十四行诗的韵律。也就是说模型正试图理解诗歌!但是,我们不能用无意义的词写诗,对吧?我们接着把所有信息放入一个巨大的模型中。

 

一个巨大的模型

我把模型的层数增加到了三层,每层有 700 个单元,然后将模型训练了 100 个周期。最终模型生成了一首气势恢宏的诗歌,瞧一瞧:

 

"The riper should by time decease,
his tender heir might bear his memory:
but thou, contracted to thine own bright eyes,
feed'st thy light's flame with self-substantial fuel,
my beept is she breat oe bath dasehr ill:
tirse do i pine and turfeit day by day,
or gluttoning on all, or all away.
Lxxvi
why is my verse so barren of new pride,
so far from variation or quick change?
why with the time do i not glance aside
to new-found methods, and to compounds strange?
why write i stil"

 

 

其中一些诗句不仅读起来非常睿智,而且还学会了押韵。如果将输入模型的数据正确清洗的话,我们会得到一个更合乎情理的诗歌。但毕竟刚开始嘛,模型的性能已经很让人惊喜了。我们创造的这位 AI 诗人比我们很多人都有诗意!

 

结尾笔记

文本生成器是否有效,关键是它生成相关故事的能力。在输出层面上,许多模型正慢慢实现,能够生成真实的、与人类编写的文本区难以区分的语言文本。

 

总之,从创建原创作品到重新生成遗失的内容,文本生成器可以得到很好的应用。这种文本生成器的一个革命性应用可能是我们可以训练它们编写和操作代码。想象一页,假如计算机程序和算法可以根据需要自行修改,这会是个怎样的世界。

 

附本项目代码库:https://github.com/pranjal52/text_generators