17  项目: 开发文本生成的神经语言模型

语言模型可以根据序列中已经观察到的单词来预测序列中下一个单词的概率,神经网络模型是用于开发统计语言模型的优选方法,它们使用分布式表示,具有相似含义的不同单词具有相似的表示,它们在进行预测时可以使用最近观察到的单词的较宽的上下文。在本教程中,您将了解如何使用Python中的深度学习库开发统计语言模型。完成本教程后,您将了解:

  • 如何准备文本以开发基于单词的语言模型。
  • 如何设计和拟合具有可训练词嵌入层和LSTM隐藏层的神经语言模型。

如何使用学习的语言模型生成具有与源文本类似的统计属性的新文本。

17.1 教程概述

本教程分为以下几部分:

  1. 柏拉图的理想国
  2. 数据准备
  3. 训练语言模型
  4. 使用语言模型

17.2 柏拉图的理想国

理想国是古典希腊哲学家柏拉图最著名的作品,它以对话的形式讲述了城市国家内秩序和正义,整个文本在公共领域免费都可免费获得,Project Gutenberg网站上提供了多种格式文本,您可以在此处下载整本书(或书籍)的ASCII文本版本(您可能需要打开两次URL):

下载书籍文本并使用文件名republic.txt将其直接放在当前工作中。在文本编辑器中打开文件并删除前后问题。这包括一开始的书籍详细信息,长篇分析以及最后的许可证信息。案文应以:

BOOK I.

I went down yesterday to the Piraeus with Glaucon the son of Ariston, ...

最后:

... And it shall be well with us both in this life and in the pilgrimage of a thousand

years which we have been describing.

将清理后的版本保存为当前工作目录中的republic_clean.txt。该文件应该是大约15,802行文本。现在我们可以为这个文本开发一个语言模型。

 

17.3 数据准备

我们将从准备建模数据开始。第一步是查看数据。

17.3.1 查看文本

在编辑器中打开文本,然后查看文本数据。例如,这是第一个对话框:

BOOK I.

 

I went down yesterday to the Piraeus with Glaucon the son of Ariston,

that I might offer up my prayers to the goddess (Bendis, the Thracian

Artemis.); and also because I wanted to see in what manner they would

celebrate the festival, which was a new thing. I was delighted with the

procession of the inhabitants; but that of the Thracians was equally,

if not more, beautiful. When we had finished our prayers and viewed the

spectacle, we turned in the direction of the city; and at that instant

Polemarchus the son of Cephalus chanced to catch sight of us from a

distance as we were starting on our way home, and told his servant to

run and bid us wait for him. The servant took hold of me by the cloak

behind, and said: Polemarchus desires you to wait.

 

I turned round, and asked him where his master was.

 

There he is, said the youth, coming after you, if you will only wait.

 

Certainly we will, said Glaucon; and in a few minutes Polemarchus

appeared, and with him Adeimantus, Glaucon's brother, Niceratus the son

of Nicias, and several others who had been at the procession.

 

Polemarchus said to me: I perceive, Socrates, that you and your

companion are already on your way to the city.

 

You are not far wrong, I said.

...

您认为我们在准备数据时需要处理什么?这是我从快速看到的内容:

  • /章标题(例如BOOK I.)。
  • 标点符号很多(例如 - ; - ,? - 等等)。
  • 奇怪的名字(例如Polemarchus)。
  • 一些漫长的独白,持续数百行。
  • 一些引用的对话框(例如'...')。

这些观察以及更多建议可以给我们准备文本数据的方式提出更好的线索,我们准备数据的具体方式实际上取决于我们打算如何对其进行建模,而这又取决于我们打算如何使用文本。

17.3.2 语言模型设计

在本教程中,我们将开发一个文本模型,然后我们可以使用它来生成新的文本序列,语言模型将是统计模型,并且给其输入一定文本序列可以预测跟在其后单词的概率,然后预测的单词将作为输入输入,进而生成下一个单词。关键的设计决策是输入序列应该有多长,它们需要足够长以允许模型能够学习到要预测的单词的上下文,此输入长度也是在模型用于生成新序列时的种子文本的长度。

没有一层不变的设计思路和策略。如果有足够的时间和资源,我们可以尝试在不同大小的输入序列条件下模型的学习能力。相反如果条件有限,我们可以根据输入序列的总长度随机选择一个值作为输入字符的长度,比如50个字的长度,有些随意。我们以选择的长度处理数据,以便模型能处理自包含的句子,并填充或截断文本以满足每个输入序列的长度要求。您可以将此作为本教程的扩展进行尝试。

相反,为了使示例保持简洁,我们将让所有文本以数据流的方式输入模型并训练,以预测文本中句子,段落甚至书籍或章节中的下一个单词。现在我们有一个模型设计,我们可以看看将原始文本转换为100个输入字到1个输出字的序列,准备好拟合模型。

17.3.3 加载文本

第一步是将文本加载到内存中。我们定义一个函数来将整个文本文件加载到内存中并返回它。函数名为load_doc(),如下所示。给定文件名,它返回一个加载文本序列。

# load doc into memory
def load_doc(filename):
    # open the file as read only
    file = open(filename, 'r' )
    # read all text
    text = file.read()
    # close the file
    file.close()
    return text

代码清单17.1:将文本加载到内存中的函数

使用此函数,我们可以在文件中加载文档的清洁版本republic_clean.txt如下:

# load document
in_filename = 'republic_clean.txt'
doc = load_doc(in_filename)
print(doc[:200])

代码清单17.2:将文本加载到内存的示例

运行此代码段会加载文档并打印前200个字符作为完整性检查。

BOOK I.

 

I went down yesterday to the Piraeus with Glaucon the son of Ariston,

that I might offer up my prayers to the goddess (Bendis, the Thracian

Artemis.); and also because I wanted to see in what

代码清单17.3:将文本加载到内存的示例输出

到现在为止还挺好。接下来,让我们清理文本。

17.3.4 清洗文本

我们需要将原始文本转换为一系列标记或单词,我们可以将其用作训练模型的源,基于查看原始文本(上文),下面是我们将执行的一些特定操作来清理文本,您可能希望自己尝试更多清洁操作作为扩展。

  • ' - '替换为空格,以便我们可以更好地分割单词。
  • 基于空白区域的分词。
  • 从单词中删除所有标点符号以减少词汇量大小(例如'What'变为'What')。
  • 删除所有非字母的单词以删除独立的标点符号。
  • 将所有单词标准化为小写以减少词汇量。

词汇量大小与语言建模有很大关系,较小的词汇表需要较小的模型,训练会很快。我们可以在一个函数中按此顺序实现每个清理操作,下面是函数clean_doc(),它将加载的文档作为参数并返回一个干净的标记数组。

# turn a doc into clean tokens
def clean_doc(doc):
    # replace ' -- ' with a space ' '
    doc = doc.replace('--', ' ')
    # split into tokens by white space
    tokens = doc.split()
    # prepare regex for char filtering
    re_punc = re.compile('[%s]' % re.escape(string.punctuation))
    # remove punctuation from each word
    tokens = [re_punc.sub('', w) for w in tokens]
    # remove remaining tokens that are not alphabetic
    tokens = [word for word in tokens if word.isalpha()]
    # make lower case
    tokens = [word.lower() for word in tokens]
    return tokens

代码清单17.4:清理文本的功能

我们可以在加载的文档上运行此清理操作,并打印出一些标记和统计信息作为完整性检查。

# clean document
tokens = clean_doc(doc)
print(tokens[:200])
print( ' Total Tokens: %d ' % len(tokens))
print( ' Unique Tokens: %d ' % len(set(tokens)))

代码清单17.5:清理文本的示例

首先,我们可以看到一个很好的令牌列表,它看起来比原始文本更清晰。我们可以删除Book I章节标记等,但这是一个好的开始。

['book', 'i', 'i', 'went', 'down', 'yesterday', 'to', 'the', 'piraeus', 'with', 'glaucon', 'the', 'son', 'of', 'ariston', 'that', 'i', 'might', 'offer', 'up', 'my', 'prayers', 'to', 'the', 'goddess', 'bendis', 'the', 'thracian', 'artemis', 'and', 'also', 'because', 'i', 'wanted', 'to', 'see', 'in', 'what', 'manner', 'they', 'would', 'celebrate', 'the', 'festival', 'which', 'was', 'a', 'new', 'thing', 'i', 'was', 'delighted', 'with', 'the', 'procession', 'of', 'the', 'inhabitants', 'but', 'that', 'of', 'the', 'thracians', 'was', 'equally', 'if', 'not', 'more', 'beautiful', 'when', 'we', 'had', 'finished', 'our', 'prayers', 'and', 'viewed', 'the', 'spectacle', 'we', 'turned', 'in', 'the', 'direction', 'of', 'the', 'city', 'and', 'at', 'that', 'instant', 'polemarchus', 'the', 'son', 'of', 'cephalus', 'chanced', 'to', 'catch', 'sight', 'of', 'us', 'from', 'a', 'distance', 'as', 'we', 'were', 'starting', 'on', 'our', 'way', 'home', 'and', 'told', 'his', 'servant', 'to', 'run', 'and', 'bid', 'us', 'wait', 'for', 'him', 'the', 'servant', 'took', 'hold', 'of', 'me', 'by', 'the', 'cloak', 'behind', 'and', 'said', 'polemarchus', 'desires', 'you', 'to', 'wait', 'i', 'turned', 'round', 'and', 'asked', 'him', 'where', 'his', 'master', 'was', 'there', 'he', 'is', 'said', 'the', 'youth', 'coming', 'after', 'you', 'if', 'you', 'will', 'only', 'wait', 'certainly', 'we', 'will', 'said', 'glaucon', 'and', 'in', 'a', 'few', 'minutes', 'polemarchus', 'appeared', 'and', 'with', 'him', 'adeimantus', 'glaucons', 'brother', 'niceratus', 'the', 'son', 'of', 'nicias', 'and', 'several', 'others', 'who', 'had', 'been', 'at', 'the', 'procession', 'polemarchus', 'said']

代码清单17.6:标记化和干净文本的示例输出

我们还获得了干净文档的一些统计信息。我们可以看到,干净的文本中只有不到120,000个单词,而且词汇量不到7,500个单词。这个很小,适合这些数据的模型应该可以在适度的硬件上进行管理。

Total Tokens: 118684

Unique Tokens: 7409

代码清单17.7:汇总干净文本属性的示例输出

接下来,我们可以看看如何将标记转换成序列并将它们保存到文件中。

17.3.5 保存干净的文字

我们可以将长标记列表转换成50个输入字和1个输出字的序列,也就是说,51个单词的序列。我们从标记第51开始迭代标记列表,其位置之前的50个标记作为输入序列,当前位置标记作为输出,然后重复这个过程到标记列表的末尾。我们将标记转换为以空格分隔的字符串,以便以后存储在文件中。下面列出了将干净标记列表拆分为长度为51个标记序列的代码。

# organize into sequences of tokens
length = 50 + 1
sequences = list()
for i in range(length, len(tokens)):
    # select sequence of tokens
    seq = tokens[i - length:i]
    # convert into a line
    line = ' '.join(seq)
    # store
    sequences.append(line)
print(' Total Sequences: %d ' % len(sequences))

代码清单17.8:将文档拆分为文本序列

运行此片段会创建一长串的行。在列表上打印统计数据,我们得到了118,633种训练模式来训练我们的模型。

 Total Sequences: 118633

代码清单17.9:将文档拆分为序列的示例输出

接下来,我们可以将序列保存到新文件中以便以后加载,定义save_doc()函数来保存文本行到文件,它以行列表和文件名作为参数,以ASCII格式写入,每行一行。

# save tokens to file, one dialog per line
def save_doc(lines, filename):
    data = '\n' .join(lines)
    file = open(filename, 'w' )
    file.write(data)
    file.close()

代码清单17.10:将文本序列保存到文件的函数

调用此函数将训练序列保存到文件republic_sequences.txt中。

# save sequences to file
out_filename = 'republic_sequences.txt'
save_doc(sequences, out_filename)

代码清单17.11:将序列保存到文件的示例

使用文本编辑器查看文件,你会看到每一行都沿着一个单词移动,最后一个新单词被预测;例如,以下是前3行:

book i i went down yesterday to the piraeus with glaucon the son of ariston that i might offer up my prayers to the goddess bendis the thracian artemis and also because i wanted to see in what manner they would celebrate the festival which was a new thing i was

i i went down yesterday to the piraeus with glaucon the son of ariston that i might offer up my prayers to the goddess bendis the thracian artemis and also because i wanted to see in what manner they would celebrate the festival which was a new thing i was delighted

i went down yesterday to the piraeus with glaucon the son of ariston that i might offer up my prayers to the goddess bendis the thracian artemis and also because i wanted to see in what manner they would celebrate the festival which was a new thing i was delighted with

代码清单17.12:保存到文件的序列的示例内容

17.3.6 完整的例子

将所有这些结合在一起,下面提供了完整的代码清单。

import string
import re
# load doc into memory
def load_doc(filename):
    # open the file as read only
    file = open(filename, 'r' )
    # read all text
    text = file.read()
    # close the file
    file.close()
    return text
# turn a doc into clean tokens
def clean_doc(doc):
    # replace '--' with a space ' '
    doc = doc.replace( '--' , ' ' )
    # split into tokens by white space
    tokens = doc.split()
    # prepare regex for char filtering
    re_punc = re.compile( '[%s]' % re.escape(string.punctuation))
    # remove punctuation from each word
    tokens = [re_punc.sub( '' , w) for w in tokens]
    # remove remaining tokens that are not alphabetic
    tokens = [word for word in tokens if word.isalpha()]
    # make lower case
    tokens = [word.lower() for word in tokens]
    return tokens
# save tokens to file, one dialog per line
def save_doc(lines, filename):
    data = '\n' .join(lines)
    file = open(filename, 'w' )
    file.write(data)
    file.close()
# load document
in_filename = 'republic_clean.txt'
doc = load_doc(in_filename)
print(doc[:200])
# clean document
tokens = clean_doc(doc)
print(tokens[:200])
print( ' Total Tokens: %d ' % len(tokens))
print( ' Unique Tokens: %d ' % len(set(tokens)))
# organize into sequences of tokens
length = 50 + 1
sequences = list()
for i in range(length, len(tokens)):
    # select sequence of tokens
    seq = tokens[i-length:i]
    # convert into a line
    line = ' ' .join(seq)
    # store
    sequences.append(line)
print( ' Total Sequences: %d ' % len(sequences))
# save sequences to file
out_filename = 'republic_sequences.txt'
save_doc(sequences, out_filename)

代码清单17.13:为建模准备文本数据的完整示例

现在当前工作目录中应该有republic_sequences.txt文件存在了。接下来,让我们看看如何使语言模型拟合这些数据。

17.4 训练语言模型

我们现在可以用准备好的数据来训练语言模型了,该模型是神经语言模型,它有一些独特的特点:

  • 它使用单词的分布式表示,具有相似含义的不同单词具有相似的表示。
  • 它在训练模型的同时学习表示。
  • 它学会使用最后100个单词的上下文预测下一个单词的概率。

具体来说,我们使用Embedding层来学习单词的表示,使用长期短期记忆(LSTM)循环神经网络来学习根据其上下文预测单词,我们从加载训练数据开始讲述。

17.4.1 加载序列

我们可以使用上一节定义的load_doc()函数加载训练数据,加载后,我们可以通过换行符将数据拆分为单独的训练序列。下面的代码段从当前工作目录加载republic_sequences.txt文件。

# load doc into memory
def load_doc(filename):
    # open the file as read only
    file = open(filename, 'r' )
    # read all text
    text = file.read()
    # close the file
    file.close()
    return text
# load
in_filename = 'republic_sequences.txt'
doc = load_doc(in_filename)
lines = doc.split( '\n' )

代码清单17.14:从文件加载干净的序列

接下来,我们可以编码训练数据。

17.4.2 编码序列

单词嵌入层期望输入序列由整数组成。我们可以将词汇表中的每个单词映射到一个唯一的整数,并对输入序列进行编码。当我们进行预测时,我们可以将预测转换为数字并在同一映射中查找其关联的单词。要执行此编码,我们可以使用Keras APITokenizer类。

首先,必须在整个训练数据集上拟合Tokenizer的一个实例,这意味着它会找到数据中的所有唯一单词,并为每个单词分配一个唯一的整数。然后我们可以使用拟合好的Tokenizer对所有训练序列进行编码,将每个序列从单词列表转换为整数列表。

# integer encode sequences of words
tokenizer = Tokenizer()
tokenizer.fit_on_texts(lines)
sequences = tokenizer.texts_to_sequences(lines)

代码清单17.15:在加载的序列上训练一个tokenizer

我们可以通过Tokenizer对象上word_index属性访问单词到整数的映射。我们使用嵌入层时需要知道词汇表的大小,这个我们可以通过计算映射字典的大小来确定词汇表大小。

单词映射从1开始到单词总数的值(例如7,409),Embedding层需要为此词汇表中的每个单词分配一个向量表示,从索引1到最大索引,并且因为数组的索引是从零开始,所以词汇结尾的单词索引将是7,409这意味着数组的长度必须为7,409 + 1。因此,在为Embedding层指定词汇表大小时,需要比实际词汇大1的大小。

# vocabulary size
vocab_size = len(tokenizer.word_index) + 1

代码清单17.16:计算词汇表的大小。

17.4.3 序列化输入和输出

现在我们来编码了输入序列,我们需要将它们拆分为输入(X)和输出(y)元素。我们可以通过数组切片来完成,拆分后,我们对输出单词进行one-hot编码,这意味着将它从一个整数转换为0值众多的向量,0的个数就是词汇表的大小,只在该单词索引位置处为1

这样,模型就可以学习预测下一个单词的概率分布,并且有这样一个事实存在,除了下一个单词概率为1之外,所有其他所有单词的都是0Keras提供了to_categorical(),可用于对每个输入 - 输出序列对的输出字进行one-hot编码。

最后,我们需要为嵌入层指定输入序列的长度,之前准备的输入序列是50个单词,因为我们在设计模型是最好不要这么明确指定一个具体的值,最通用的方法是是使用输入数据形状的第二个维度(列数),这样,如果在准备数据时更改序列的长度,则无需更改此数据加载代码这是常规的设计套路。

# separate into input and output
sequences = array(sequences)
X, y = sequences[:,:-1], sequences[:,-1]
y = to_categorical(y, num_classes=vocab_size)
seq_length = X.shape[1]

代码清单17.17:将文本数据拆分为输入和输出序列

17.4.4 适合模型

我们现在可以开始在训练数据上定义和拟合我们的语言模型了,如前所述,学习嵌入需要知道词汇表的大小和输入序列的长度,还需要一个参数来指定将用于表示每个单词的维度,也就是说,嵌入向量空间的大小。

常用值为50,100300我们在这里使用200,但你也可考虑测试更小或更大的值。我们将使用两个LSTM隐藏层,每层有400个隐藏单元,更多的记忆单元和更深的网络构成的神经网络语言模型可以获得更好的性能。(译者注:原文参数造成模型的精确过低,译者调整到代码的数字,为了尽快收敛和提供精度,译者增加了BatchNormalization层和dropout层,不然最后的生成文本很难看)

具有400个神经元的全连接层连接到LSTM隐藏层以解释从序列提取的特征,输出层将下一个单词预测为单个向量,该单个向量是词汇表的大小,其中词汇表中的每个单词具有概率。softmax激活函数用于确保输出具有归一化概率的特征。

# define the model
def define_model(vocab_size, seq_length):
    model = Sequential()
    model.add(Embedding(vocab_size, 200, input_length=seq_length))
    model.add(LSTM(400, return_sequences=True))

model.add(BatchNormalization())
    model.add(LSTM(400))
    model.add(Dropout(0.5))
    model.add(Dense(400, activation='relu'))
    model.add(Dense(vocab_size, activation='softmax'))
    # compile network
    model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
    # summarize defined model
    model.summary()
    plot_model(model, to_file='model.png', show_shapes=True)
    return model

代码清单17.18:定义语言模型

打印定义的网络摘要作为完整性检查,以确保我们构建了我们的预期。

_________________________________________________________________

Layer (type)                 Output Shape              Param #   

=================================================================

embedding_1 (Embedding)      (None, 50, 200)           1482000   

_________________________________________________________________

lstm_1 (LSTM)                (None, 50, 400)           961600    

_________________________________________________________________

batch_normalization_1 (Batch (None, 50, 400)           1600      

_________________________________________________________________

lstm_2 (LSTM)                (None, 400)               1281600   

_________________________________________________________________

dropout_1 (Dropout)          (None, 400)               0         

_________________________________________________________________

dense_1 (Dense)              (None, 400)               160400    

_________________________________________________________________

dense_2 (Dense)              (None, 7410)              2971410   

=================================================================

Total params: 6,858,610

Trainable params: 6,857,810

Non-trainable params: 800

_________________________________________________________________

代码清单17.19:汇总已定义模型的示例输出

然后将定义的模型的图表保存到名为model.png的文件中。

17.1:定义的基于单词的语言模型的图。

编译该模型,指定拟合模型所需的分类交叉熵损失。从技术上讲,该模型学习多类分类任务,这是此类问题的合适损失函数,使用Adam算法实现小批量梯度下降并且评估模型的准确性。最后,该模型在训练数据上训练100个迭代,为了加快速度批量大小为128在没有GPU的计算机上的训练可能需要几个小时,您可以使用更大的批量大小和/或更少的训练加快速度。

在训练期间,您还能看到性能摘要,包括在每次批次更新结束时从训练数据评估的损失和准确度,你可能会得到不同的结果,但是预测序列中下一个单词的准确度可能只有50%左右,甚至不到40%,这也不坏,我们的目标不是100%准确(例如记忆文本的模型),而仅仅只是一个捕捉文本本质的模型练习。

......

Epoch 95/100

118633/118633 [==============================] - 70s 586us/step - loss: 2.5825 - acc: 0.3843

Epoch 96/100

118633/118633 [==============================] - 70s 591us/step - loss: 2.5819 - acc: 0.3840

Epoch 97/100

118633/118633 [==============================] - 70s 588us/step - loss: 2.5652 - acc: 0.3859

Epoch 98/100

118633/118633 [==============================] - 70s 588us/step - loss: 2.5567 - acc: 0.3892

Epoch 99/100

118633/118633 [==============================] - 70s 588us/step - loss: 2.5602 - acc: 0.3873

Epoch 100/100

118633/118633 [==============================] - 70s 589us/step - loss: 2.5419 - acc: 0.3905

代码清单17.20:训练语言模型的示例输出

17.4.5 保存模型

在运行结束时,训练的模型将保存到文件中。在这里,我们使用Keras模型API将模型保存到当前工作目录的model.h5文件中,之后,当我们加载此模型进行预测时,还需要将种子单词映射为整数,这些映射在Tokenizer的实例对象中,可以使用Pickle保存它。

# save the model to file
model.save( 'model.h5' )
# save the tokenizer
dump(tokenizer, open( 'tokenizer.pkl' , 'wb' ))

代码清单17.21:将拟合模型和Tokenizer保存到文件中。

17.4.6 完整的例子

我们可以把所有这些放在一起,下面列出了拟合语言模型的完整示例。

from numpy import array
from pickle import dump
from keras.preprocessing.text import Tokenizer
from keras.utils.vis_utils import plot_model
from keras.utils import to_categorical
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import LSTM
from keras.layers import Embedding
from keras.layers import Dropout
from keras.layers import BatchNormalization
# load doc into memory
def load_doc(filename):
    # open the file as read only
    file = open(filename, 'r')
    # read all text
    text = file.read()
    # close the file
    file.close()
    return text
# define the model
def define_model(vocab_size, seq_length):
    model = Sequential()
    model.add(Embedding(vocab_size, 200, input_length=seq_length))
    model.add(LSTM(400, return_sequences=True))
    model.add(BatchNormalization())
    model.add(LSTM(400))
    model.add(Dropout(0.5))
    model.add(Dense(400, activation='relu'))
    model.add(Dense(vocab_size, activation='softmax'))
    # compile network
    model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])

    # summarize defined model
    model.summary()
    plot_model(model, to_file='model.png', show_shapes=True)
    return model
# load
in_filename = 'republic_sequences.txt'
doc = load_doc(in_filename)
lines = doc.split('\n')
# integer encode sequences of words
tokenizer = Tokenizer()
tokenizer.fit_on_texts(lines)
sequences = tokenizer.texts_to_sequences(lines)
# vocabulary size
vocab_size = len(tokenizer.word_index) + 1
# separate into input and output
sequences = array(sequences)
X, y = sequences[:, :-1], sequences[:, -1]
y = to_categorical(y, num_classes=vocab_size)
seq_length = X.shape[1]
# define model
model = define_model(vocab_size, seq_length)
# fit model
model.fit(X, y, batch_size=128, epochs=100)
# save the model to file
model.save('model.h5')
# save the tokenizer
dump(tokenizer, open('tokenizer.pkl', 'wb'))

代码清单17.22:完成语言模型训练的示例。

17.5 使用语言模型

既然我们有一个训练好的语言模型,我们就可以使用它。在这种情况下,我们可以使用它来生成与源文本具有相同统计属性的新文本序列,但在本例子中这个想法有点不容实现,主要原因是训练的程度不够,但它给出了语言模型学到的具体例子。我们将首先再次加载训练序列。

17.5.1 加载数据

我们可以使用上一节中的相同代码来加载文本的训练数据序列。具体来说load_doc()函数。

# load doc into memory
def load_doc(filename):
    # open the file as read only
    file = open(filename, 'r')
    # read all text
    text = file.read()
    # close the file
    file.close()
    return text
# load cleaned text sequences
in_filename = 'republic_sequences.txt'
doc = load_doc(in_filename)
lines = doc.split('\n')

代码清单17.23:从文件加载干净的序列

我们需要文本,以便我们可以选择源序列作为模型的输入,以生成新的文本序列,该模型将需要50个单词作为输入。稍后,我们需要指定预期的输入长度,我们可以通过计算加载数据的一行长度,并从同一行上的减去1来确定种子文本的长度。

seq_length = len(lines[0].split()) - 1

代码清单17.24:计算预期的输入长度

17.5.2 加载模型

我们现在可以从文件加载模型。Keras提供了load_model()函数来加载模型,随时可以使用。

# load the model
model = load_model( 'model.h5' )

代码清单17.25:从文件加载保存的模型

我们还可以使用Pickle API从文件加载tokenizer

# load the tokenizer
tokenizer = load(open( 'tokenizer.pkl' , 'rb' ))

代码清单17.26:从文件加载保存的Tokenizer

我们准备使用加载的模型。

17.5.3 生成文本

生成文本的第一步是准备种子输入文本,为此,我们将从输入文本中选择一行随机文本,选择后,我们将打印它,以便我们了解所使用的内容。

# select a seed text
seed_text = lines[randint(0,len(lines))]
print(seed_text + '\n' )

代码清单17.27:选择随机示例作为种子文本。

接下来,我们可以一次生成一个新单词。首先,必须使用我们在训练模型时使用的相同标记器将种子文本编码为整数。

encoded = tokenizer.texts_to_sequences([seed_text])[0]

代码清单17.28:对选定的种子文本进行编码。

该模型调用model.predict_classes()直接预测下一个单词,该类将返回具有最高概率的单词的索引。

# predict probabilities for each word
yhat = model.predict_classes(encoded, verbose=0)

代码清单17.29:预测序列中的下一个单词

然后,我们在Tokenizer的映射中查找索引以获取关联的单词。

out_word = ''
for word, index in tokenizer.word_index.items():
    if index == yhat:
        out_word = word
        break

代码清单17.30:将预测的整数映射到已知词汇表中的单词。

然后,我们将此单词附加到种子文本后面并重复该过程,值得注意的是,输入序列将变得太长,在输入序列编码为整数后,我们将其截断为所需的长度即可,Keras提供了pad_sequence()函数,我们可以使用它来执行此截断。

encoded = pad_sequences([encoded], maxlen=seq_length, truncating= 'pre' )

代码清单17.31:填充编码序列

我们将上面这些操作放在名为generate_seq()的函数中,该函数以模型、标记生成器、输入序列长度、种子文本和要生成的单词数作为输入,然后返回由模型生成的一系列单词。

# generate a sequence from a language model
def generate_seq(model, tokenizer, seq_length, seed_text, n_words):
    result = list()
    in_text = seed_text
    # generate a fixed number of words
    for _ in range(n_words):
        # encode the text as integer
        encoded = tokenizer.texts_to_sequences([in_text])[0]
        # truncate sequences to a fixed length
        encoded = pad_sequences([encoded], maxlen=seq_length, truncating='pre')
        # predict probabilities for each word
        yhat = model.predict_classes(encoded, verbose=0)
        # map predicted word index to word
        out_word = ''
        for word, index in tokenizer.word_index.items():
            if index == yhat:
                out_word = word
                break
        # append to input
        in_text += ' ' + out_word
        result.append(out_word)
    return ' '.join(result)

代码清单17.32:在给定模型和种子文本的情况下生成一系列单词的函数。

我们现在准备在给定一些种子文本的情况下生成一系列新单词。

# generate new text
generated = generate_seq(model, tokenizer, seq_length, seed_text, 50)
print(generated)

代码清单17.33:生成文本序列的示例

综上所述,下面列出了从学习语言模型生成文本的完整代码清单。

from random import randint
from pickle import load
from keras.models import load_model
from keras.preprocessing.sequence import pad_sequences

# load doc into memory
def load_doc(filename):
    # open the file as read only
    file = open(filename, 'r')
    # read all text
    text = file.read()
    # close the file
    file.close()
    return text
# generate a sequence from a language model
def generate_seq(model, tokenizer, seq_length, seed_text, n_words):
    result = list()
    in_text = seed_text
    # generate a fixed number of words
    for _ in range(n_words):
        # encode the text as integer
        encoded = tokenizer.texts_to_sequences([in_text])[0]
        # truncate sequences to a fixed length
        encoded = pad_sequences([encoded], maxlen=seq_length, truncating='pre')
        # predict probabilities for each word
        yhat = model.predict_classes(encoded, verbose=0)
        # map predicted word index to word
        out_word = ''
        for word, index in tokenizer.word_index.items():
            if index == yhat:
                out_word = word
                break
        # append to input
        in_text += ' ' + out_word
        result.append(out_word)
    return ' '.join(result)
# load cleaned text sequences
in_filename = 'republic_sequences.txt'
doc = load_doc(in_filename)
lines = doc.split('\n')
seq_length = len(lines[0].split()) - 1
# load the model
model = load_model('model.h5')
# load the tokenizer
tokenizer = load(open('tokenizer.pkl', 'rb'))
# select a seed text
seed_text = lines[randint(0, len(lines))]
print(seed_text + '\n')
# generate new text
generated = generate_seq(model, tokenizer, seq_length, seed_text, 50)
print(generated)

代码清单17.34:生成文本序列的完整示例

首先运行该示例打印种子文本。

be compounded of many elements certainly not her immortality is demonstrated by the previous argument and there are many other proofs but to see her as she really is not as we now behold her marred by communion with the body and other miseries you must contemplate her with the eye

代码清单17.35:选择种子文本的示例输出

然后打印50个生成的文本。

意:鉴于神经网络的随机性,您的具体结果可能会有所不同。考虑运行几次示例。

of the maintenance of freedom in the state constitutes courage in the individual and the other poets not to be reared and educated is not this a host of liberty brave men for if they are grown up talking concerning justice and secondly in the days of the state but

代码清单17.36:生成文本的示例输出

你可以看到文本看似合理其实没有意义,实际上,添加连接将有助于解释种子和生成的文本,然而,生成的文本以正确的顺序获得正确的单词需要其他的处理或者更大量的训练,尝试运行该示例几次以查看生成的文本的其他示例。


0 条 查看最新 评论

没有评论
暂时无法发表评论