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

简介: 通过学习本教程,你将学会如何开发基于字符的神经网络语言模型,包括:如何针对基于字符的语言建模准备文本,如何使用LSTM开发基于字符的语言模型,如何使用训练过的基于字符的语言模型来生成文本。

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

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

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

  • 如何针对基于字符的语言建模准备文本。
  • 如何使用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.h5和mapping.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,译者:夏天,审校:主题曲。

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

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

相关文章
|
Linux 开发工具 Android开发
FFmpeg开发笔记(六十)使用国产的ijkplayer播放器观看网络视频
ijkplayer是由Bilibili基于FFmpeg3.4研发并开源的播放器,适用于Android和iOS,支持本地视频及网络流媒体播放。本文详细介绍如何在新版Android Studio中导入并使用ijkplayer库,包括Gradle版本及配置更新、导入编译好的so文件以及添加直播链接播放代码等步骤,帮助开发者顺利进行App调试与开发。更多FFmpeg开发知识可参考《FFmpeg开发实战:从零基础到短视频上线》。
2327 2
FFmpeg开发笔记(六十)使用国产的ijkplayer播放器观看网络视频
|
JSON 中间件 Go
Go 网络编程:HTTP服务与客户端开发
Go 语言的 `net/http` 包功能强大,可快速构建高并发 HTTP 服务。本文从创建简单 HTTP 服务入手,逐步讲解请求与响应对象、URL 参数处理、自定义路由、JSON 接口、静态文件服务、中间件编写及 HTTPS 配置等内容。通过示例代码展示如何使用 `http.HandleFunc`、`http.ServeMux`、`http.Client` 等工具实现常见功能,帮助开发者掌握构建高效 Web 应用的核心技能。
678 61
Vite使用svg-企业级开发(支持本地svg和网络svg渲染)
本教程介绍如何在Vite项目中集成SVG图标插件。首先安装`vite-plugin-svg-icons`,配置插件指向SVG图标目录,并注册全局组件。接着创建SVG图标组件,支持内部图标与外部图片展示。通过简单配置,即可在页面中灵活使用各类SVG图标,提升开发效率。
646 0
|
开发者
鸿蒙仓颉语言开发教程:网络请求和数据解析
本文介绍了在仓颉开发语言中实现网络请求的方法,以购物应用的分类列表为例,详细讲解了从权限配置、发起请求到数据解析的全过程。通过示例代码,帮助开发者快速掌握如何在网络请求中处理数据并展示到页面上,减少开发中的摸索成本。
鸿蒙仓颉语言开发教程:网络请求和数据解析
|
监控 安全 网络协议
恶意软件无处逃!国内版“Manus”AiPy开发Windows沙箱工具,进程行为+网络传输层级监控! 头像 豪气的
NImplant.exe 是一款后渗透测试工具,可实现远程管理与持久化控制。其优点包括无文件技术、加密通信和插件扩展,但也存在被检测风险及配置复杂等问题。为深入分析其行为,我们基于 aipy 开发了 Windows 沙箱工具,针对桌面上的 NImplant.exe 进行多维度分析,涵盖进程行为、网络连接(如 TCP 请求、目标 IP/域名)、文件控制等,并生成传输层监控报告与沙箱截图。结果显示,aipy 工具响应迅速,报告清晰易读,满足分析需求。
|
网络协议 物联网
VB6网络通信软件上位机开发,TCP网络通信,读写数据并处理,完整源码下载
本文介绍使用VB6开发网络通信上位机客户端程序,涵盖Winsock控件的引入与使用,包括连接服务端、发送数据(如通过`Winsock1.SendData`方法)及接收数据(利用`Winsock1_DataArrival`事件)。代码实现TCP网络通信,可读写并处理16进制数据,适用于自动化和工业控制领域。提供完整源码下载,适合学习VB6网络程序开发。 下载链接:[完整源码](http://xzios.cn:86/WJGL/DownLoadDetial?Id=20)
657 12
鸿蒙开发:切换至基于rcp的网络请求
本文的内容主要是把之前基于http封装的库,修改为当前的Remote Communication Kit(远场通信服务),无非就是通信的方式变了,其他都大差不差。
669 4
鸿蒙开发:切换至基于rcp的网络请求
|
XML 开发工具 Android开发
FFmpeg开发笔记(五十六)使用Media3的Exoplayer播放网络视频
ExoPlayer最初是为了解决Android早期MediaPlayer控件对网络视频兼容性差的问题而推出的。现在,Android官方已将其升级并纳入Jetpack的Media3库,使其成为音视频操作的统一引擎。新版ExoPlayer支持多种协议,解决了设备和系统碎片化问题,可在整个Android生态中一致运行。通过修改`build.gradle`文件、布局文件及Activity代码,并添加必要的权限,即可集成并使用ExoPlayer进行网络视频播放。具体步骤包括引入依赖库、配置播放界面、编写播放逻辑以及添加互联网访问权限。
2293 1
FFmpeg开发笔记(五十六)使用Media3的Exoplayer播放网络视频
|
存储 网络协议 物联网
C 语言物联网开发之网络通信与数据传输难题
本文探讨了C语言在物联网开发中遇到的网络通信与数据传输挑战,分析了常见问题并提出了优化策略,旨在提高数据传输效率和系统稳定性。
|
安全 网络安全 数据安全/隐私保护
网络安全与信息安全:关于网络安全漏洞、加密技术、安全意识等方面的知识分享安卓与iOS开发中的线程管理比较
【8月更文挑战第30天】本文将探讨网络安全与信息安全的重要性,并分享关于网络安全漏洞、加密技术和安全意识的知识。我们将了解常见的网络攻击类型和防御策略,以及如何通过加密技术和提高安全意识来保护个人和组织的信息安全。

热门文章

最新文章