likes
comments
collection
share

Python 深度学习之 LSTM 文本生成

作者站长头像
站长
· 阅读数 24

Deep Learning with Python

这篇文章是我学习《Deep Learning with Python》(第二版,François Chollet 著) 时写的系列笔记之一。文章的内容是从 Jupyter notebooks 转成 Markdown 的,你可以去 GitHubGitee 找到原始的 .ipynb 笔记本。

你可以去这个网站在线阅读这本书的正版原文(英文)。这本书的作者也给出了配套的 Jupyter notebooks

本文为 第8章 生成式深度学习 (Chapter 8. Generative deep learning) 的笔记之一。

使用 LSTM 生成文本

8.1 Text generation with LSTM

以前有人说过:“generating sequential data is the closest computers get to dreaming”,让计算机生成序列是很有魅力的事情。我们将以文本生成为例,探讨如何将循环神经网络用于生成序列数据。这项技术也可以用于音乐的生成、语音合成、聊天机器人对话生成、甚至是电影剧本的编写等等。

其实,我们现在熟知的 LSTM 算法,最早被开发出来的时候,就是用于逐字符地生成文本的。

序列数据的生成

用深度学习生成序列的通用方法,就是训练一个网络(一般用 RNN 或 CNN),输入前面的 Token,预测序列中接下来的 Token。

说的术语化一些:给定前面的 Token,能够对下一个 Token 的概率进行建模的网络叫作「语言模型(language model)」。语言模型能够捕捉到语言的统计结构 ——「潜在空间(latent space)」。训练好一个语言模型,输入初始文本字符串(称为「条件数据」,conditioning data),从语言模型中采样,就可以生成新 Token,把新的 Token 加入条件数据中,再次输入,重复这个过程就可以生成出任意长度的序列。

我们从一个简单的例子开始:用一个 LSTM 层,输入文本语料的 N 个字符组成的字符串,训练模型来生成第 N+1 个字符。模型的输出是做 softmax,在所有可能的字符上,得到下一个字符的概率分布。这个模型叫作「字符级的神经语言模型」(character-level neural language model)。

Python 深度学习之 LSTM 文本生成

采样策略

使用字符级的神经语言模型生成文本时,最重要的问题是如何选择下一个字符。这里有几张常用方法:

  • 贪婪采样(greedy sampling):始终选择可能性最大的下一个字符。这个方法很可能得到重复的、可预测的字符串,而且可能意思不连贯。(输入法联想)
  • 纯随机采样:从均匀概率分布中抽取下一个字符,其中每个字符的概率相同。这样随机性太高,几乎不会生成出有趣的内容。(就是胡乱输出字符的组合)
  • 随机采样(stochastic sampling):根据语言模型的结果,如果下一个字符是 e 的概率为 0.3,那么你会有 30% 的概率选择它。有一点的随机性,让生成的内容更随意富有变化,但又不是完全随机,输出可以比较有意思。

随机采样看上去很好,很有创造性,但有个问题是无法控制随机性的大小:随机性越大,可能富有创造性,但可能胡乱输出;随机性越小,可能更接近真实词句,但太死板、可预测。

为了在采样过程中控制随机性的大小,引入一个参数:「softmax 温度」(softmax temperature),用于表示采样概率分布的熵,即表示所选择的下一个字符会有多么出人意料或多么可预测:

  • 更高的温度:熵更大的采样分布,会生成更加出人意料、更加无结构的数据;
  • 更低的温度:对应更小的随机性,会生成更加可预测的数据。

Python 深度学习之 LSTM 文本生成

具体的实现是,给定 temperature 值,对模型的 softmax 输出重新加权,得到新的概率分布:

import numpy as np

def rewight_distribution(original_distributon, temperature=0.5):
    '''
    对于不同的 softmax 温度,对概率分布进行重新加权
    '''
    distribution = np.log(original_distribution) / temperature
    distribution = np.exp(distribution)
    return distribution / np.sum(distribution)

字符级 LSTM 文本生成实现

理论就上面那些了,现在,我们要用 Keras 来实现字符级的 LSTM 文本生成了。

数据准备

首先,我们需要大量的文本数据(语料,corpus)来训练语言模型。可以去找足够大的一个或多个文本文件:维基百科、各种书籍等都可。这里我们选择用一些尼采的作品(英文译本),这样我们学习出来的语言模型将是有尼采的写作风格和主题的。(多年来,我自己写野生模型玩,都是用鲁迅😂)

下载语料,并将其转换为全小写:

from tensorflow import keras
import numpy as np

path = keras.utils.get_file(
    'nietzsche.txt', 
    origin='https://s3.amazonaws.com/text-datasets/nietzsche.txt')
text = open(path).read().lower()
print('Corpus length:', len(text))

输出结果:

Corpus length: 600893

接下来,我们要把文本做成数据 (向量化):从 text 里提取长度为 maxlen 的序列(序列之间存在部分重叠),进行 one-hot 编码,然后打包成 (sequences, maxlen, unique_characters) 形状的。同时,还需要准备数组 y,包含对应的目标,即在每一个所提取的序列之后出现的字符(也是 one-hot 编码的):

# 将字符序列向量化

maxlen = 60     # 每个序列的长度
step = 3        # 每 3 个字符采样一个新序列
sentences = []  # 保存所提取的序列
next_chars = [] # sentences 的下一个字符

for i in range(0, len(text) - maxlen, step):
    sentences.append(text[i: i+maxlen])
    next_chars.append(text[i+maxlen])
print('Number of sequences:', len(sentences))

chars = sorted(list(set(text)))
char_indices = dict((char, chars.index(char)) for char in chars)
# 插:上面这两行代码 6
print('Unique characters:', len(chars))

print('Vectorization...')

x = np.zeros((len(sentences), maxlen, len(chars)), dtype=np.bool)
y = np.zeros((len(sentences), len(chars)), dtype=np.bool)

for i, sentence in enumerate(sentences):
    for t, char in enumerate(sentence):
        x[i, t, char_indices[char]] = 1
    y[i, char_indices[next_chars[i]]] = 1

输出信息:

Number of sequences: 200278

Unique characters: 57

构建网络

我们要用到的网络其实很简单,一个 LSTM 层 + 一个 softmax 激活的 Dense 层就可以了。(其实并不一定要用 LSTM,用一维卷积层也是可以生成序列的)

用于预测下一个字符的单层 LSTM 模型:

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense

model = Sequential()
model.add(LSTM(128, input_shape=(maxlen, len(chars))))
model.add(Dense(len(chars), activation='softmax'))

模型编译配置:

from tensorflow.keras import optimizers

optimizer = optimizers.RMSprop(lr=0.01)
model.compile(loss='categorical_crossentropy',
              optimizer=optimizer)

训练语言模型并从中采样

给定一个语言模型和一个种子文本片段,就可以通过重复以下操作来生成新的文本:

  1. 给定目前已有文本,从模型中得到下一个字符的概率分布;
  2. 根据某个温度对分布进行重新加权;
  3. 根据重新加权后的分布对下一个字符进行随机采样;
  4. 将新字符添加到文本末尾。

在训练模型之前,我们先把「采样函数」写了,这个函数负责对模型得到的原始概率分布重新加权,并从中抽取一个字符索引:

def sample(preds, temperature=1.0):
    preds = np.asarray(preds).astype('float64')
    preds = np.log(preds) / temperature
    exp_preds = np.exp(preds)
    preds = exp_preds / np.sum(exp_preds)
    probas = np.random.multinomial(1, preds, 1)
    return np.argmax(probas)

最后,再来训练并生成文本。我们在每轮完成后都使用一系列不同的温度值来生成文本,这样就可以看到,随着模型收敛,生成的文本如何变化,以及温度对采样策略的影响:

# 文本生成循环

import random

for epoch in range(1, 60):    # 训练 60 个轮次
    print(f'👉\033[1;35m epoch {epoch} \033[0m')    # print('epoch', epoch)
    
    model.fit(x, y,
              batch_size=128,
              epochs=1)
    
    start_index = random.randint(0, len(text) - maxlen - 1)
    generated_text = text[start_index: start_index + maxlen]
    print(f'  📖 Generating with seed: "\033[1;32;43m{generated_text}\033[0m"')    # print(f' Generating with seed: "{generated_text}"')
    
    for temperature in [0.2, 0.5, 1.0, 1.2]:
        print(f'\n   \033[1;36m 🌡️ temperature: {temperature}\033[0m')    # print('\n  temperature:', temperature)
        print(generated_text, end='')
        for i in range(400):    # 生成 400 个字符
            # one-hot 编码目前有的文本
            sampled = np.zeros((1, maxlen, len(chars)))
            for t, char in enumerate(generated_text):
                sampled[0, t, char_indices[char]] = 1
            
            # 预测,采样,生成下一字符
            preds = model.predict(sampled, verbose=0)[0]
            next_index = sample(preds, temperature)
            next_char = chars[next_index]
            print(next_char, end='')
            
            generated_text = generated_text[1:] + next_char
            
    print('\n' + '-' * 20)

这个运行起来会输出巨多结果:

第 1 轮:

Python 深度学习之 LSTM 文本生成

第 30 轮:

Python 深度学习之 LSTM 文本生成

第 59 轮:

Python 深度学习之 LSTM 文本生成

利用更多的数据训练更大的模型,训练时间更长,生成的样本会更连贯、更真实。但是,无论如何,利用这种方式生成的文本并没有任何意义。机器所做的仅仅是从统计模型中对数据进行采样,它并没有理解人类的语言,也不知道自己在说什么。

基于词嵌入的文本生成

如果要生成中文文本,我们的汉字太多了,逐字符去做我认为不是很好的选择。所以可以考虑基于词嵌入来生成文本。在之前的字符级 LSTM 文本生成的基础上,将编码/解码方式稍作修改、添加 Embedding 层即可实现一个初级的基于词嵌入的文本生成:

import random
import tensorflow as tf
from tensorflow.keras import optimizers
from tensorflow.keras import layers
from tensorflow.keras import models
from tensorflow import keras
import numpy as np

import jieba    # 使用 jieba 做中文分词
import os

os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"

# 导入文本

path = '~/CDFMLR/txt_zh_cn.txt'
text = open(path).read().lower()
print('Corpus length:', len(text))

# 将文本序列向量化

maxlen = 60     # 每个序列的长度
step = 3        # 每 3 个 token 采样一个新序列
sentences = []  # 保存所提取的序列
next_tokens = []  # sentences 的下一个 token

token_text = list(jieba.cut(text))

tokens = list(set(token_text))
tokens_indices = {token: tokens.index(token) for token in tokens}
print('Number of tokens:', len(tokens))

for i in range(0, len(token_text) - maxlen, step):
    sentences.append(
        list(map(lambda t: tokens_indices[t], token_text[i: i+maxlen])))
    next_tokens.append(tokens_indices[token_text[i+maxlen]])
print('Number of sequences:', len(sentences))

# 将目标 one-hot 编码
next_tokens_one_hot = []
for i in next_tokens:
    y = np.zeros((len(tokens),), dtype=np.bool)
    y[i] = 1
    next_tokens_one_hot.append(y)

# 做成数据集
dataset = tf.data.Dataset.from_tensor_slices((sentences, next_tokens_one_hot))
dataset = dataset.shuffle(buffer_size=4096)
dataset = dataset.batch(128)
dataset = dataset.prefetch(buffer_size=tf.data.experimental.AUTOTUNE)


# 构建、编译模型

model = models.Sequential([
    layers.Embedding(len(tokens), 256),
    layers.LSTM(256),
    layers.Dense(len(tokens), activation='softmax')
])

optimizer = optimizers.RMSprop(lr=0.1)
model.compile(loss='categorical_crossentropy',
              optimizer=optimizer)

# 采样函数: 对模型得到的原始概率分布重新加权,并从中抽取一个 token 索引
def sample(preds, temperature=1.0):
    preds = np.asarray(preds).astype('float64')
    preds = np.log(preds) / temperature
    exp_preds = np.exp(preds)
    preds = exp_preds / np.sum(exp_preds)
    probas = np.random.multinomial(1, preds, 1)
    return np.argmax(probas)

# 训练模型

callbacks_list = [
    ..., # 在每轮完成后保存权重
    ..., # 不再改善时降低学习率
    ..., # 不再改善时中断训练
]

model.fit(dataset, epochs=30, callbacks=callbacks_list)

# 文本生成

start_index = random.randint(0, len(text) - maxlen - 1)
generated_text = text[start_index: start_index + maxlen]
print(f' 📖 Generating with seed: "{generated_text}"')

for temperature in [0.2, 0.5, 1.0, 1.2]:
    print('\n  🌡️ temperature:', temperature)
    print(generated_text, end='')
    for i in range(100):    # 生成 100 个 token
        # 编码当前文本
        text_cut = jieba.cut(generated_text)
        sampled = []
        for i in text_cut:
            if i in tokens_indices:
                sampled.append(tokens_indices[i])
            else:
                sampled.append(0)

        # 预测,采样,生成下一个 token,代码同前一个例子,这里省略了
        ...

我用一些鲁迅的文章去训练,最终得到的结果大概是这样的:

Python 深度学习之 LSTM 文本生成

可以看到,这些句子都说不通,看着很难受。所以我们还可以把分 token 的方法改一改,不是分词,而是去分句子:

text = text.replace(',', ' ,').replace('。', ' 。').replace('?', ' ?').replace(':', ' :')
token_text = tf.keras.preprocessing.text.text_to_word_sequence(text, split=' ')

其他的地方基本不变,这样也可以得到比较有意思的文本。比如这是我用一些余秋雨的文章去训练的结果:

Python 深度学习之 LSTM 文本生成

虽然还是杂乱的,没有意义的,但起码看起来更舒服一点了。

如果想要等好的结果,最简单的方式就是增加加数据,增加网络参数。或者,直接用 GPT-3,CPM 这些超大的网络:)

转载自:https://juejin.cn/post/6995777526308012069
评论
请登录