Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

3.5. 循环神经网络(RNN)实战:LSTM自动生成文本

本文承接自上一篇文章 3.3. 循环神经网络(RNN)理论(进阶),没看过的建议先看上文。

3.5.1. 准备工作

请在终端输入指令以下载和安装:

pip install keras tensorflow scikit-learn

这里需要使用到我的txt文件,我把它上传到了GitCode,你点击链接即可查看和下载。我们使用Mary Shelley的Frankenstein(中文名《弗兰肯斯坦》《科学怪人》)作为训练数据。

把下载好后的数据放到你的Python项目下即可。

3.5.2. 模型架构

在这篇文章中我们会使用长短期记忆网络(LSTM)模型来进行文本生成。我们要把文本内容转化之后的字典喂给模型,然后让它基于前20个字符预测第21个字符,计算准确率。

每个输入样本是长度为 20 的时间步(字符)序列;LSTM 层有 20 个单元,输出层神经元数取决于文本中不同字符的个数。

3.5.3. 敲代码

Step 1: 加载数据

首先我们把数据加载进来,顺便去掉换行符:

# 加载数据  
data = open('Frankenstein_by_Mary_Shelley.txt').read()  
  
# 移除换行符  
data = data.replace('\n', ' ').replace('\r', '')

Step 2: 建立字典

文本中每一个字符我们都需要在字典中建立一个字段来对应,首先我们需要去掉重复的字符,然后再建立字典:

# 字符去重  
letters = list(set(data))  
  
# 打印检查  
print(len(letters))  
  
# 建立字典   
char_to_int = {b:a for a, b in enumerate(letters)}
  • 通过 set(data) 将文本数据中的所有字符去重,保留每种字符的唯一值(如字母、数字、标点符号等)。list(set(data)) 就是将这些唯一的字符变成一个列表,存储在变量 letters 中。

  • 字典 char_to_int 的键是相应的字符,值是从序号 0 开始的整数,这样可以通过字符转为数据再转为one-hot格式。

输出:

74

这74个不同的字符(大小写字母、数字、标点等)就组成了所有的文本内容。

Step 3: 输入处理

我们接下来需要对字符串进行批量处理,在这里我们先建立一个data_processing方法,之后我们可以通过调用它来提取模型能够接收到的X和y数据:

# 字符批量处理  
import numpy as np  
from tensorflow.keras.utils import to_categorical  
  
#实现输入字符文章的批量处理,输入整个字符、滑动窗口大小、转化字典  
def data_processing(data, slide, num_letters, char_to_int):  
    # 滑动窗门提取数据  
    def extract_data(data, slide):  
        x = []  
        y = []  
        for i in range(len(data) - slide):  
            x.append([a for a in data[i:i + slide]])  
            y.append(data[i + slide])  
        return x, y  
  
    # 字符到数字的批量处理  
    def char_to_int_data(x, y, char_to_int):  
        x_to_int = []  
        y_to_int = []  
        for i in range(len(x)):  
            x_to_int.append([char_to_int[char] for char in x[i]])  
            y_to_int.append([char_to_int[char] for char in y[i]])  
        return x_to_int, y_to_int  
  
    char_data = extract_data(data, slide)  
    int_data = char_to_int_data(char_data[0], char_data[1], char_to_int)  
    Input = int_data[0]  
    Output = list(np.array(int_data[1]).flatten())  
    Input_Reshaped = np.array(Input).reshape(len(Input), slide)  
    new_ = np.zeros((Input_Reshaped.shape[0], Input_Reshaped.shape[1], num_letters))  
    for i in range(Input_Reshaped.shape[0]):  
        for j in range(Input_Reshaped.shape[1]):  
            new_[i, j, :] = to_categorical(Input_Reshaped[i, j], num_letters)  
    return new_, Output

extract_data 函数:

  • 作用:通过滑动窗口,从输入字符序列中提取样本。
    • x:窗口中的字符列表(长度为窗口大小)。
    • y:窗口后紧跟的目标字符(预测值)。
  • 逻辑:
    • x 通过 [a for a in data[i:i + slide]] 获取当前窗口范围内的字符。
    • y 取窗口后第一个字符 data[i + slide]。
    • 如:slide=3,输入数据data="abcdefg",结果:
      • x = [['a', 'b', 'c'], ['b', 'c', 'd'], ['c', 'd', 'e'], ['d', 'e', 'f']]
      • y = ['d', 'e', 'f', 'g']

char_to_int_data 函数:

  • 作用:将字符序列 x 和目标字符 y 转换成整数序列。
    • 使用 char_to_int 字典获取每个字符在字典中的整数索引。
    • 如:输入 x = [['a', 'b', 'c'], ['b', 'c', 'd']],y = ['d', 'e'],字典 {'a': 0, 'b': 1, 'c': 2, 'd': 3, 'e': 4},结果:
      • x_to_int = [[0, 1, 2], [1, 2, 3]]
      • y_to_int = [[3], [4]](之后在 Output 中会被展平为 [3, 4])。

转换为模型输入:

  • 通过 extract_data 和 char_to_int_data 提取并完成字符到整数的转换;
  • 原始的整数输入 Input 的形状为 [样本数, 窗口大小];
  • Output 是目标字符序列,以一维数组形式存储。

one-hot编码处理:

  • 作用:将整数输入 Input_Reshaped 转换为独热编码的 3D 输入 new_。
    • Input_Reshaped[i, j] 是窗口的第 i 个样本中第 j 个字符的整数值。
    • to_categorical(Input_Reshaped[i, j], num_letters) 构建字符的独热编码,维度为 [num_letters]。
    • 每个样本的编码结果存储在 new_,其形状为 [样本数, 窗口大小, num_letters]。

最终输出:

  • new_:one-hot编码的输入数组,形状 [样本数, 窗口大小, 字符表大小],适用于LSTM这类深度学习模型。
  • Output:目标序列的整数数组,一维形式,长度为样本数。

Step4: 获取模型的输入

我们接下来使用data_processing方法提取模型能够接收到的X和y数据,同时也确认一下时间步长(time step)为20(模型基于前20个字符预测):

#时间步长  
time_step = 20  
  
# 获取模型的输入数据  
X, y = data_processing(data, time_step, len(letters), char_to_int)

# 检查数据结构  
print(X.shape)  
print(len(y))

输出:

(421397, 20, 74)
421397
  • (421397, 20, 74):

    • 421397:表示滑动窗口提取到的样本数量,即构造出的有多少个 [输入序列 -> 目标字符] 样本。这取决于输入文本长度 len(data) 和滑动窗口大小 time_step 的关系。
    • 20:表示每个样本的时间步数 (滑动窗口大小 time_step),也就是每个输入序列包含 20 个字符。这个值由代码中的参数 time_step = 20 决定。
    • 74:表示字符总种类数,即one-hot编码的维度。这个值来源于字符表的大小 len(letters),表示输入数据中有 74 个独一无二的字符(包括字母、标点符号、空格等)。
  • y 是样本对应的目标字符的整数列表。每个样本有一个目标字符,因此 y 的长度与样本数量相同,也就是 421397。

Step5: 数据分离

把数据分为训练集和测试集:

# 数据分离  
from sklearn.model_selection import train_test_split  
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=10)  
  
# 把y_train改为one-hot编码  
y_train_category = to_categorical(y_train, num_classes=len(letters))
  • 将 y_train 转换为one-hot 编码的主要原因是为了能交给LSTM模型来训练

Step 6: 建立模型

模型接收长度为 20 的时间步序列;LSTM 有 20 个单元,输出层神经元数取决于文本中不同字符的个数,也就是字典的长度:

# 建立模型  
from tensorflow.keras.models import Sequential  
from tensorflow.keras.layers import Dense, LSTM  
  
model = Sequential()  
  
model.add(LSTM(units=20, input_shape=(time_step, len(letters)), activation='relu'))  
model.add(Dense(len(letters), activation='softmax'))

Step 7: 配置模型

# 配置模型  
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])

Step 8: 训练模型

把训练集数据喂给模型即可:

# 训练模型  
model.fit(X_train, y_train_category, epochs=20, batch_size=1000)

batch_size=1000:

  • 每次训练(梯度更新)所使用的样本数量。
  • 数据会被分割为多个批次,每批包含 1000 个样本。
    • 举例:如果训练样本总数为 50,000,每个批次是 1,000,则一个 epoch 内需要 50 次更新。
    • 当然如果你内存多可以不用设置这个参数,一次性把所有数据写入内存(要占恐怖的15GB左右,还没有算IDE)

Step 9: 评估模型

我们通过准确率来评估模型:

# 训练集准确率  
_, train_accuracy = model.evaluate(X_train, y_train_category)  
print(f"Train Accuracy: {train_accuracy}")  
  
# 测试集准确率  
y_test_category = to_categorical(y_test, num_classes=len(letters))  
_, test_accuracy = model.evaluate(X_test, y_test_category)  
print(f"Test Accuracy: {test_accuracy}")

输出:

Train Accuracy: 0.4766451120376587
Test Accuracy: 0.4727812111377716

别看这个准确率还没到50%,但是要知道计算机需要从74个不同字符中猜出一个答案能到这个正确率已经非常不错了。盲猜的正确率只有1/74,也就是1.35%,而模型运算出的结果正确率比它高30多倍。