用Keras开发字符级神经网络语言模型

语言模型可根据序列中出现的特定单词来预测下一个单词。可以使用神经网络在字符级别上开发语言模型。基于字符的语言模型有一个最大的优点,就是在处理单词、标点符号和其他文档结构的时候,能保持较小的词汇量和较强的灵活性。但所付出的代价是模型较大、训练较慢。然而,在神经网络语言模型领域,基于字符的模型为语言建模提供了一种通用、灵活和强大的方法。

在本教程中,你将了解到如何开发基于字符的神经网络语言模型。

学习完本教程,你将学会:

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

教程概述

本教程分为四个部分:

  1. Sing a Song of Sixpence(译者注:一首英文童谣)
  2. 数据准备
  3. 训练语言模型
  4. 生成文本

Sing a Song of Sixpence

童谣“Sing a Song of Sixpence”在西方人人都会唱。我们将用它来开发基于字符的语言模型。

这首童谣很短,所以模型的拟合会很快,但不能太短,那样我们就不会看到任何有意思的东西。下面是这首童谣完整歌词:

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.

复制这段文本,并将其保存到当前工作目录中的一个新文件中,文件名为“rhyme.txt”。

数据准备

第一步是准备文本数据。我们将首先定义语言模型的类型。

语言模型设计

语言模型必须用文本进行训练,在基于字符的语言模型中,输入和输出序列必须是字符。用于输入的字符的个数决定了需要提供给模型以引出第一个预测字符的字符数。在第一个字符生成之后,可以将其添加到输入序列上,作为模型的输入以生成下一个字符。

序列越长,则为模型提供的上下文也越多,同时,模型将耗费更长的时间来进行训练。我们这个模型使用的字符的长度是10。

下面我们将把原始文本转换成模型可以学习的形式。

加载文本

童谣的歌词必须加载到内存之后才能使用。下面是一个名为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

可以使用这个函数来加载名为“rhyme.txt”的文件,并将内容放入内存中。然后将文件的内容打印到屏幕上进行完整性检查。

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

净化文本

接下来,需要净化加载的文本。

这里我们不会做太多的事情,只是删除所有的换行符,转换成一段按空格进行分割的长字符序列。

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

你可能需要探索一下净化数据的其他一些方法,例如将文本转换为小写字母或删除标点符号,以减少最终的词汇量,这样可以开发出更小、更精简的模型。

创建序列

长字符列表有了,下面就可以创建用于训练模型的输入输出序列了。

每个输入序列包含十个字符和一个输出字符,因此,每个序列包含了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))

运行这段代码,我们可以看到,用来训练语言模型的序列其实只有不到400个字符。

Total Sequences: 399

保存序列

最后,将准备好的数据保存到文件中,后面在开发模型的时候再加载。

下面是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()

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

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

完整的例子

将上面那些代码片段组合到一起,组成下面这份完整的代码:

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

运行该示例,将生成“char_seqiences.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
ong of sixp
ng of sixpe
...

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

训练语言模型

本章节将为上面准备好的序列数据开发一个神经语言模型。该模型将读取已经编码的字符,并预测出序列的下一个字符。

加载数据

第一步是从"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')

序列编码

字符序列必须编码为整数。也就是说每个字符都会被分配一个指定的整数值,每个字符序列都会被编码为一个整数序列。

我们可以根据原始输入数据来创建映射关系。该映射关系是字符值映射到整数值的字典。

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)

运行这段代码,我们可以看到输入数据中的字符剔重后有38个。

Vocabulary Size: 38

分割输入和输出

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

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

接下来,将对每个字符进行独热编码,也就是说,每个字符都会变成一个向量。这为神经网络提供了更精确的输入表示,还为网络预测提供了一个明确的目标。

我们可以使用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)

现在,我们已经为模型的拟合做好准备了。

模型拟合

该模型使用了一个针对独热编码输入序列采用10个时间步长和38个特征的输入层进行定义。我们在X输入数据上使用第二和第三个维度,而不是指定这些数字。这是因为当序列的长度或词汇表的大小发生改变的实话,无需改变模型的定义。

该模型有一个包含75个存储器单元的LSTM隐藏层,通过一些试验和错误进行选择。

该模型有一个完全连接的输出层,输出一个词汇表中所有字符概率分布的向量。在输出层上使用softmax激活函数来确保输出具有概率分布的属性。

# define model
model = Sequential()
model.add(LSTM(75, input_shape=(X.shape[1], X.shape[2])))
model.add(Dense(vocab_size, activation='softmax'))
print(model.summary())

运行这段代码将打印出网络的概要信息以进行完整性检查。

_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
lstm_1 (LSTM)                (None, 75)                34200
_
dense_1 (Dense)              (None, 38)                2888
=================================================================
Total params: 37,088
Trainable params: 37,088
Non-trainable params: 0
_

该模型将执行100次训练迭代来进行拟合。

# compile model
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
# fit model
model.fit(X, y, epochs=100, verbose=2)

保存模型

模型拟合完成之后,将其保存到文件以备后面使用。Keras提供了save()函数,可以使用该函数将模型保存到单个文件中,包括权重和拓扑信息。

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

另外还要保存从字符到整数的映射关系,因为在使用模型的时候,需要对任意的输入进行编码,并对模型的输出进行解码。

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

完整的例子

将上面那些代码片段组合到一起,组成下面这份基于字符的神经网络语言模型的完整代码:

from numpy import array
from pickle import dump
from keras.utils import to_categorical
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

# 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 = Sequential()
model.add(LSTM(75, input_shape=(X.shape[1], X.shape[2])))
model.add(Dense(vocab_size, activation='softmax'))
print(model.summary())
# compile model
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
# 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'))

这个例程的运行时间可能需要一分钟。

...
Epoch 96/100
0s - loss: 0.2193 - acc: 0.9950
Epoch 97/100
0s - loss: 0.2124 - acc: 0.9950
Epoch 98/100
0s - loss: 0.2054 - acc: 0.9950
Epoch 99/100
0s - loss: 0.1982 - acc: 0.9950
Epoch 100/100
0s - loss: 0.1910 - acc: 0.9950

运行结束之后,会在当前目录生成两个文件,model.h5mapping.pkl

接下来,看一下如何使用这个学习过的模型。

生成文本

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

加载模型

第一步是加载文件“model.h5”中的模型,可以使用Keras API中的load_model()函数进行加载。

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

还需要加载文件“mapping.pkl”文件中的字典,用于将字符映射为整数。

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

下面可以使用这个模型了。

生成字符

为了启动生成过程,必须提供包含10个字符的序列作为模型的输入。

首先,字符序列必须使用加载进来的映射关系编码为整数值。

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

接下来,使用Keras中的pad_sequences()函数对整数值进行独热编码,并将序列重塑为三个维度。因为我们只有一个序列,而且LSTM需要所有的输入都有三个维度(样本、时间步长、特征)。

# one hot encode
encoded = to_categorical(encoded, num_classes=len(mapping))
encoded = encoded.reshape(1, encoded.shape[0], encoded.shape[1])

下面,就可以使用模型来预测序列中的下一个字符了。

使用predict_classes()而不是predict()来直接选择具有最高概率的字符整数。

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

可以通过查找映射中的整数-字符关系来对整数进行解码。

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

这个字符随后可以添加到输入序列中去。然后通过截断输入序列文本中的第一个字符来确保输入序列是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))
        encoded = encoded.reshape(1, encoded.shape[0], encoded.shape[1])
        # 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

完整的例子

将上面那些代码片段组合到一起,组成下面这份基于字符的神经网络语言模型的完整代码:

from pickle import load
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
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))
        encoded = encoded.reshape(1, encoded.shape[0], encoded.shape[1])
        # 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', 20))
# test mid-line
print(generate_seq(model, mapping, 10, 'king was i', 20))
# test not in original
print(generate_seq(model, mapping, 10, 'hello worl', 20))

运行该示例将生成三个文本序列。

第一个是测试这个模型在从童谣的开头进行预测的话表现如何。第二个是测试从某一行的中间开始预测表现如何。最后是测试模型遇到从未见过的字符序列时表现如何。

Sing a song of sixpence, A poc
king was in his counting house
hello worls e pake wofey. The

我们可以看到,这个模型在前两个例子中的表现得还不错,符合预期。但对于新的文本来说,预测的结果就有点匪夷所思了。

总结

通过阅读本教程,你已经学会了如何开发基于字符的神经网络语言模型,包括:

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

文章原标题《How to Develop a Character-Based Neural Language Model in Keras》,作者: Jason Brownlee,译者:夏天,审校:主题曲。

文章为简译,更为详细的内容请查看原文

本文由北邮@爱可可-爱生活老师推荐,阿里云组织翻译。

时间: 2024-11-17 00:09:29

用Keras开发字符级神经网络语言模型的相关文章

Keras词级自然语言模型

更多深度文章,请关注云计算频道:https://yq.aliyun.com/cloud 语言模型是许多自然语言处理模型(如机器翻译和语音识别)中的关键元素,它可以根据给出的单词序列预测到序列中的下一个单词.在选择语言模型的框架时需要注意与语言模型的目的匹配. 本教程分为5个部分; 他们是: 1.语言建模框架. 2.模型1:单字输入,单字输出序列. 3.模型2:逐行序列. 4.模型3:双字输入,单字输出序列. 1.语言建模框架 语言模型是挑战自然语言处理问题(如机器翻译和语音识别)较大模型中的关键

Facebook通过10亿单词构建有效的神经网络语言模型

由于在语言识别.机器翻译和语言建模等领域表现出了优异的性能,为序列预测而设计的神经网络最近再次引起了人们的兴趣,但是这些模型都是计算密集型的,成本非常高.比如在语言建模领域,最新的成果依然需要在大规模GPU集群上训练几周的时间,虽然效果不错,但是这些计算密集型的实践对大规模计算基础设施的依赖性非常强,这限制了其在学术和生产环境中的研究速度与应用. 针对这一计算瓶颈,Facebook AI 研究院(FAIR)设计了一个新的.几乎是为GPU量身定制的softmax函数,能够非常有效地通过大规模词库训

linux驱动开发--字符设备:静态分配设备号

字符设备(char device)         采用字节流方式访问的设备称为字符设备,通常智能采用顺序访问方式,也有极少数可以前后移动访问指针的设备(如:帧捕捉卡等设备).系统标准字符设备,例如:字符中断.串口等设备.常见待开发设备驱动的字符设备,例如:触摸屏.键盘.视频捕捉设备.音频设备等. 设备号 主设备号     用于标识设备类型,内核代码根据该号码对应设备文件和对应的设备驱动程序 次设备号     用于标识通类型的不同设备个体,驱动程序根据该号码辨别具体操作的是哪个设备个体. 设备号

linux驱动开发--字符设备:简单的file_operations示例

字符设备结构struct cdev 内核使用该结构来表示一个字符设备,在<linux/cdev.h>中定义. 重要成员: struct kobject kobj;//设备对象 struct module *owner;//该设备的拥有者驱动模块 struct file_operations *ops;//设备操作集合 struct list_head list;//内核维护的字符设备链表成员 dev_t dev;//字符设备号 unsigned int count;//设备个数 文件结构 st

linux驱动开发--字符设备:添加文件指针偏移的功能

设备移位操作llseek 对应lseek系统调用的设备移位操作为llseek 默认情况为允许设备移位操作 大部分字符设备提供的都是数据流而不是一个数据区,比如串口,对于这些设备而言移位操作毫无意义 设备可选择是否支持移位操作 支持设备移位操作loff_t (*llseek)(struct file *filp, loff_t off, int whence);filp:待操作的设备文件file结构体指针off:待操作的定位偏移值(可正可负)whence:待操作的定位起始位置返回:返回移位后的新文

linux驱动开发--字符设备:动态分配设备号

设备号的动态分配 int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);dev:保存分配到的设备号baseminor:希望分配的起始次设备号count:需要分配的设备号数目name:设备名称(出现在/proc/devices)         返回:成功返回0, 失败返回负值 请求内核动态分配count个设备号,且次设备号从baseminor开始 /** *Copyrig

linux驱动开发--字符设备:创建一组设备节点

cdev改进 为设备驱动支持多个设备个体做准备,针对cdev进行改进 将代表字符设备的cdev对象包含在设备驱动定义的私有数据结构体中 对设备驱动私有数据结构体采用内核内存分配方式为其分配内存 将为每个设备添加cdev对象和创建设备节点封装为一个独立函数 支持多个设备个体 为设备驱动支持多个设备个体对驱动进行改进 循环调用为每个设备添加cdev对象和创建设备节点而封装的独立函数实现在系统中添加对多个设备个体的支持 /** *Copyright (c) 2013.TianYuan *All rig

linux驱动开发--字符设备:信号量

信号量的使用 信号量(semaphore)是用于保护临界区的一种常用方法,它的使用和自旋锁类似.与自旋锁相同,只有得到信号量的进程才能执行临界区代码.但是与自旋锁不同的是当获取不到信号量时,进程不会原地打转而是进入休眠等待状态. 关于信号量的操作函数: 1.定义信号量 struct semaphore sem; 2.初始化信号量 sema_init(struct semaphore *sem, int val);//用于初始化信号量并设置sem为val init_MUTEX(struct sem

linux驱动开发--字符设备:自动创建设备节点

自动创建设备文件 定义在<linux/device.h>中 class结构:该结构体类型变量对应一个设备类,被创建的类存放在/sys目录下面 device结构:该结构体类型变量对应设备,被创建的设备存放于/sys目录下面 在加载驱动模块时,用户空间中的udev会自动响应device_create()函数,在/sys下寻找对应的类,从而为这个设备在/dev目录下创建设备文件 内核版本问题: 在内核2.4版本中使用devfs_register 在内核2.6早起版本中使用class_device_r