15  如何开发基于字符的神经语言模型

语言模型可以根据序列中前面的特定单词预测序列中的下一个单词。你可以使用神经网络开发字符级的语言模型,基于字符的语言模型的好处是在处理任何单词、标点符号和其他文档结构时仅需要很小的词汇量,而且更灵活,但这是以较大模型和训练缓慢为代价的,然而,在神经语言模型领域,基于字符的模型为语言建模提供了通用、灵活和强大的方法,尽管大和训练缓慢也是值得的。在本教程中,您将了解如何开发基于字符的神经语言模型。完成本教程后,您将了解:

  • 如何为基于字符的语言建模准备文本。
  • 如何使用LSTM开发基于字符的语言模型。
  • 如何使用训练好的基于语言的语言模型来生成文本。

15.1 教程概述

本教程分为以下几部分:

  1. 唱一首六便士之歌
  2. 数据准备
  3. 训练语言模型
  4. 生成文本

15.2 唱一首六便士之歌

童谣Sing a Song of Sixpence在西方是众所周知的。第一节知道的人非常多,完整版的是4节,我们将用4节版的这首童谣来开发基于字符级的语言模型。它很短,所以训练模型会很快,但不会短得以至于我们看不到任何有趣的东西,我们将用作源文本的完整4节版本如下所示。

Sing a song of sixpence,

A pocket full of rye.

Four and twenty blackbirds,

Baked in a pie.

 

When the pie was opened

The birds began to sing;

Wasn't that a dainty dish,

To set before the king.

 

The king was in his counting house,

Counting out his money;

The queen was in the parlour,

Eating bread and honey.

 

The maid was in the garden,

Hanging out the clothes,

When down came a blackbird

And pecked off her nose.

清单15.1:唱一首六便士之歌

复制文本并将其保存在当前工作目录中的新文件中,并带有文件名rhyme.txt

15.3 数据准备

第一步是准备文本数据。我们将从定义语言模型类型开始。

15.3.1 语言模型设计

语言模型必须放在文本数据上训练,对于字符级的语言模型,输入和输出序列必须是字符流。用作输入的字符数同时也是为了引出第一预测字符而需要提供给模型的字符的数目,生成第一个字符后,可将其附加到输入序列并用作模型的输入以生成下一个字符。

较长的序列为模型训练和学习提供了更多的上下文,可以更好的预测接下来要输出的字符,但是需要更长的时间来训练,并在生成文本时对起点文本要求也长。我们规定模型使用长度为10个字符的序列来给模型训练和学习提供上下文信息,没有很多文本,10个字符仅能表示很少的单词。我们现在可以将原始文本转换为我们的模型可以学习的形式输入和输出字符序列。

15.3.2 加载文字

我们首先必须将文本加载到内存,然后我们可以使用它。下面定义一个名为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

代码清单15.2:将文档加载到内存的函数

我们用rhyme.txt的文件名作为参数调用此函数,将文本加载到内存中。然后将文件的内容打印到屏幕上作完整性检查。

# load text
raw_text = load_doc( 'rhyme.txt' )
print(raw_text)

代码清单15.3:将文档加载到内存中

 

15.3.3 清洗文字

接下来,我们需要清理加载的文本,在这个例子中我们不会做太多,具体来说,我们删除所有换行符,最后得到一个只由空格分隔的长字符序列。

# clean
tokens = raw_text.split()
raw_text = ' ' .join(tokens)
print(raw_text)

代码清单15.4:对加载的文档进行Tokenize

您可以尝试其他数据清理方法,例如将案例规范化为小写或删除标点符号,以减少最终的词汇量大小并开发更小更精简的模型。

15.3.4 创建序列

现在我们有了很长的字符列表,我们可以创建用于训练模型的输入输出序列。每个输入序列将是10个字符,带有一个输出字符,每个序列长11个字符,我们通过枚举文本中的字符来创建序列,从索引10处的第11个字符开始。

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

代码清单15.5:将文本转换为固定长度的序列

运行此片段,我们可以看到我们最终只有不到400个字符序列来训练我们的语言模型。

Total Sequences: 399

代码清单15.6:将文本转换为固定长度序列的示例输出

15.3.5 保存序列

最后,我们可以将准备好的数据保存到文件中,以便我们可以在开发模型时加载它。定义save_doc()函数将字符串列表保存到文件中,每行一个,函数参数为字符串列表和文件名。

# 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()

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

我们调用这个函数并将我们准备好的序列保存到char_sequences.txt文件中,文件保存在当前的工作目录中。

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

代码清单15.8:将序列保存到文件

15.3.6 完整的例子

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

# 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
# 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 text
raw_text = load_doc('rhyme.txt')
print(raw_text)
# clean
tokens = raw_text.split()
raw_text = ' '.join(tokens)
# organize into sequences of characters
length = 10
sequences = list()
for i in range(length, len(raw_text)):
    # select sequence of tokens
    seq = raw_text[i - length:i + 1]
    # store
    sequences.append(seq)
print('Total Sequences: %d' % len(sequences))
# save sequences to file
out_filename = 'char_sequences.txt'
save_doc(sequences, out_filename)

代码清单15.9:准备文本数据的完整示例

运行该示例以创建char_sequences.txt文件。看看里面你应该看到如下内容:

Sing a song

ing a song

ng a song o

g a song of

 a song of

a song of s

 song of si

song of six

......

代码清单15.10:输出文件的示例

我们下面准备训练基于字符的神经语言模型。

15.4 训练语言模型

在本节中,我们将为准备好的序列数据开发神经语言模型,该模型将读取编码字符并预测序列中的下一个字符。将使用长短期记忆递归神经网络隐藏层来从输入序列学习上下文以进行预测。

15.4.1 加载数据

第一步是从char_sequences.txt加载准备好的字符序列数据,使用上一节中定义的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
in_filename = 'char_sequences.txt'
raw_text = load_doc(in_filename)
lines = raw_text.split('\n')

代码清单15.11:加载准备好的文本数据

15.4.2 编码序列

字符序列必须编码为整数,这意味着将为每个唯一字符分配一个特定的整数值,并且每个字符序列将被编码为整数序列,我们可以在原始输入数据中给定一组排序的唯一字符来创建映射。映射是字符值到整数值的字典。

chars = sorted(list(set(raw_text)))
mapping = dict((c, i) for i, c in enumerate(chars))

代码清单15.12:在字符和整数之间创建一个映射。

接下来把字符序列转成数字序列,完成编码,我们可以一次处理一个字符序列,并使用字典映射查找每个字符的整数值。

sequences = list()
for line in lines:
    # integer encode line
    encoded_seq = [mapping[char] for char in line]
    # store
    sequences.append(encoded_seq)

代码清单15.13:整数编码字符序列

结果是整数列表的列表,我们稍后需要知道词汇量的大小,就是字典映的大小。

# vocabulary size
vocab_size = len(mapping)
print( ' Vocabulary Size: %d ' % vocab_size)

代码清单15.14:汇总词汇表的大小。

运行一下,我们可以看到输入序列数据中有38个唯不同的字符。

Vocabulary Size: 38

代码清单15.15:汇总词汇表大小的示例输出

15.4.3 拆分输入和输出

现在序列已经使用整数编码,我们现在可以将序列分成输入和输出字符序列,使用简单的数组切片来完成此操作。

sequences = array(sequences)
X, y = sequences[:, :-1], sequences[:, -1]

代码清单15.16:将序列拆分为输入和输出元素

接下来,对每个字符进行one-hot编码,也就是说,每个字符就变成一个长度为词汇表长度(38)大小的向量,这个向量仅在字典中序号处的位置为1,其他位置均为0,这杨作主要是为网络提供了更精确的输入表示,它还为网络预测提供了一个明确的目标,训练过程模型给出输出字符的概率分布,并与真实的实际的输入字符进行比较,这个实际的向量除了字符处的概率为1之外,其他的均为0,这是预测输出字符的最理想形式。我们可以使用Keras API中的to_categorical()函数对输入和输出序列进行热编码。

sequences = [to_categorical(x, num_classes=vocab_size) for x in X]
X = array(sequences)
y = to_categorical(y, num_classes=vocab_size)

代码清单15.17:将序列转换为可供训练的格式

我们现在准备好适应这个模型。

15.4.4 拟合模型

模型结构如下:一个输入层和一个全连接输出层,输入层输入参数是大小为10个时间步长的序列和长度为38单个字符one-hot编码。我们使用输入数据X的参数作为第二维和第三维,而不是指定这些数字。这样,如果我们更改序列的长度或词汇表的大小,我们就不需要更改模型定义。该模型具有单个LSTM隐藏层,具有75个隐藏单元,最后是全连接的输出层,该输出层输出一个向量,其中概率分布跨越词汇表中的所有字符,在输出层上使用softmax激活函数以确保输出具有概率分布的属性。

# define the model
def define_model(X):
    model = Sequential()
    model.add(LSTM(75, input_shape=(X.shape[1], X.shape[2])))
    model.add(Dense(vocab_size, activation= 'softmax' ))
    # compile model
    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

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

该模型学习的是多类分类问题,因此我们使用针对此类问题的分类对数损失函数categorical_crossentropy,梯度下降使用Adam算法,并且在每次批量更新结束时记录模型的准确性。该模型训练100epoch(迭代次数)。运行此命令会将已定义网络的摘要打印。

_________________________________________________________________

Layer (type)                 Output Shape              Param #   

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

lstm_2 (LSTM)                (None, 75)                34200     

_________________________________________________________________

dense_2 (Dense)              (None, 38)                2888      

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

Total params: 37,088

Trainable params: 37,088

Non-trainable params: 0

_________________________________________________________________

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

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

15.1:定义的基于字符的语言模型的图。

15.4.5 保存模型

在模型拟合后,我们将其保存到文件中供以后使用。Keras模型API提供了save()函数,我们可以使用它将模型保存到单个文件中,包括权重和拓扑信息。

# save the model to file
model.save( 'model.h5' )

代码清单15.20:将训练后的模型保存到文件。

我们还保存了从字符到整数的映射,在使用模型和解码模型的任何输出时,我们需要对任何输入进行编码。

# save the mapping
dump(mapping, open( 'mapping.pkl' , 'wb' ))

代码清单15.21:将字符映射到整数到文件。

15.4.6 完整的例子

将所有这些结合在一起,下面列出了训练基于字符的神经语言模型的完整代码清单。

from numpy import array
from pickle import dump
from keras.utils import to_categorical
from keras.utils.vis_utils import plot_model
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import LSTM
# 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(X):
    model = Sequential()
    model.add(LSTM(75, input_shape=(X.shape[1], X.shape[2])))
    model.add(Dense(vocab_size, activation= 'softmax' ))
    # compile model
    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 = 'char_sequences.txt'
raw_text = load_doc(in_filename)
lines = raw_text.split( '\n' )
# integer encode sequences of characters
chars = sorted(list(set(raw_text)))
mapping = dict((c, i) for i, c in enumerate(chars))
sequences = list()
for line in lines:
    # integer encode line
    encoded_seq = [mapping[char] for char in line]
    # store
    sequences.append(encoded_seq)
# vocabulary size
vocab_size = len(mapping)
print( 'Vocabulary Size: %d' % vocab_size)
# separate into input and output
sequences = array(sequences)
X, y = sequences[:,:-1], sequences[:,-1]
sequences = [to_categorical(x, num_classes=vocab_size) for x in X]
X = array(sequences)
y = to_categorical(y, num_classes=vocab_size)
# define model
model = define_model(X)
# fit model
model.fit(X, y, epochs=100, verbose=2)
# save the model to file
model.save( 'model.h5' )
# save the mapping
dump(mapping, open( 'mapping.pkl' , 'wb' ))

代码清单15.22:培训语言模型的完整示例

运行该示例可能需要一分钟。您将看到模型很好地学习了问题,或许可以很好地生成令人惊讶的字符序列。

......

Epoch 94/100

 - 0s - loss: 0.2356 - acc: 0.9925

Epoch 95/100

 - 0s - loss: 0.2304 - acc: 0.9950

Epoch 96/100

 - 0s - loss: 0.2217 - acc: 0.9925

Epoch 97/100

 - 0s - loss: 0.2118 - acc: 0.9950

Epoch 98/100

 - 0s - loss: 0.2024 - acc: 0.9925

Epoch 99/100

 - 0s - loss: 0.1941 - acc: 0.9950

Epoch 100/100

 - 0s - loss: 0.1868 - acc: 0.9925

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

在运行结束时,您将有两个文件保存到当前工作目录,model.h5mapping.pkl。接下来,我们可以看一下使用学习模型。

15.5 生成文本

我们将使用学习的语言模型生成具有相同统计特性的新文本序列。

15.5.1 加载模型

第一步加载上一节保存的模型文件model.h5。我们可以使用Keras API load_model()函数。

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

代码清单15.24:加载保存的模型

我们还需要加载pickle字典,以便将字符映射到mapping.pkl文件中的整数。我们使用Pickle API加载对象。

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

代码清单15.25:加载字符数字映射模型

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

15.5.2 生成字符

我们必须提供10个字符的序列作为模型的输入,以便开始生成过程,我们将手动选择这些,需要以与为模型准备训练数据相同的方式准备给定的输入序列,首先,必须使用加载的映射对字符序列进行整数编码。

# encode the characters as integers
encoded = [mapping[char] for char in in_text]

代码清单15.26:将输入文本编码为整数

接下来,整数需要使用Kerasto_categorical()函数转变为one-hot编码。

# one hot encode
encoded = to_categorical(encoded, num_classes=len(mapping))

代码清单15.27one-hot编码文本的数字编码

然后我们可以使用该模型来预测序列后的下一个字符。我们使用predict_classes()而不是predict()直接为具有最高概率的字符选择整数,而不是在整个字符集中获得完整的概率分布。

# predict character
yhat = model.predict_classes(encoded, verbose=0)

代码清单15.28:预测序列中的下一个字符

然后,我们可以通过查找映射来解码此整数,以查看它映射到的字符。

out_char = ''
for char, index in mapping.items():
    if index == yhat:
        out_char = char
        break

代码清单15.29:将预测的整数映射回一个字符。

然后可以将此字符添加到输入序列最后。然后,我们截掉输入序列文本中的第一个字符来确保输入序列是10个字符,我们使用Keras API中的pad_sequences()函数来执行此截断操作。将所有这些放在一起,定义一个名为generate_seq()的新函数,用于使用加载的模型生成新的文本序列。

# generate a sequence of characters with a language model
def generate_seq(model, mapping, seq_length, seed_text, n_chars):
    in_text = seed_text
    # generate a fixed number of characters
    for _ in range(n_chars):
        # encode the characters as integers
        encoded = [mapping[char] for char in in_text]
        # truncate sequences to a fixed length
        encoded = pad_sequences([encoded], maxlen=seq_length, truncating='pre')
        # one hot encode
        encoded = to_categorical(encoded, num_classes=len(mapping))
        # predict character
        yhat = model.predict_classes(encoded, verbose=0)
        # reverse map integer to character
        out_char = ''
        for char, index in mapping.items():
            if index == yhat:
                out_char = char
                break
        # append to input
        in_text += char
    return in_text

代码清单15.30:用于预测给定种子文本的字符序列的函数

15.5.3 完整的例子

将所有这些结合在一起,下面列出了使用拟合神经语言模型生成文本的完整示例。from pickle import load
from numpy import array
from keras.models import load_model
from keras.utils import to_categorical
from keras.preprocessing.sequence import pad_sequences
# generate a sequence of characters with a language model
# generate a sequence of characters with a language model
def generate_seq(model, mapping, seq_length, seed_text, n_chars):
    in_text = seed_text
    # generate a fixed number of characters
    for _ in range(n_chars):
        # encode the characters as integers
        encoded = [mapping[char] for char in in_text]
        # truncate sequences to a fixed length
        encoded = pad_sequences([encoded], maxlen=seq_length, truncating='pre')
        # one hot encode
        encoded = to_categorical(encoded, num_classes=len(mapping))
        # predict character
        yhat = model.predict_classes(encoded, verbose=0)
        # reverse map integer to character
        out_char = ''
        for char, index in mapping.items():
            if index == yhat:
                out_char = char
                break
        # append to input
        in_text += char
    return in_text
# load the model
model = load_model( 'model.h5' )
# load the mapping
mapping = load(open( 'mapping.pkl' , 'rb' ))
# test start of rhyme
print(generate_seq(model, mapping, 10, 'Sing a son' , 200))
# test mid-line
print(generate_seq(model, mapping, 10, 'king was i' , 200))
# test not in original
print(generate_seq(model, mapping, 10, 'hello worl' , 200))

代码清单15.31:使用拟合模型生成字符的完整示例

运行该示例会生成三个文本序列,第一个是测试模型在从押韵开始时的作用;第二个是测试,看看它在一行开头的效果如何;最后一个例子是一个测试,看看它对前面从未见过的一系列字符有多好。

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

Sing a song of sixpence, A poc

king was in his counting house

hello worl, hee ing ws cee oee

代码清单15.32:生成字符序列的示例输出

我们可以看到,正如我们所期望的那样,模型在前两个例子中表现得非常好。我们还可以看到模型仍然为新文本生成了一些东西,但这是无稽之谈。


0 条 查看最新 评论

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