Генерация текста LSTM для глубокого обучения Python

глубокое обучение Python

Это 13-й день моего участия в августовском испытании обновлений. Узнайте подробности события:Испытание августовского обновления

Deep Learning with Python

Эта статья — одна из серии заметок, которые я написал, изучая Deep Learning with Python (второе издание, Франсуа Шолле). Содержимое статьи конвертировано из блокнотов Jupyter в Markdown, вы можете перейти наGitHubилиGiteeнайти оригинал.ipynbноутбук.

ты можешь идтиЧитайте оригинальный текст этой книги онлайн на этом сайте(Английский). Автор этой книги также дает соответствиеJupyter notebooks.

Эта статьяГлава 8 Генеративное глубокое обучение (Chapter 8. Generative deep learning) одной из записок.

Генерация текста с помощью LSTM

8.1 Text generation with LSTM

Кто-то сказал раньше: «генерация последовательных данных — это самое близкое к мечтам компьютеров», а создание последовательностей компьютерами — увлекательная вещь. Мы будем использовать генерацию текста в качестве примера, чтобы изучить, как можно использовать рекуррентные нейронные сети для генерации данных последовательности. Эту технологию также можно использовать для создания музыки, синтеза речи, создания диалогов с чат-ботами и даже для написания сценариев фильмов, и это лишь некоторые из них.

Фактически алгоритм LSTM, с которым мы сейчас знакомы, был впервые разработан для генерации текста посимвольно.

Генерация данных последовательности

Общий метод создания последовательностей с помощью глубокого обучения заключается в обучении сети (обычно RNN или CNN), вводе предыдущего токена и прогнозировании следующего токена в последовательности.

Чтобы выразить это в терминологии: сеть, которая может моделировать вероятность следующего токена с учетом предыдущего токена, называется «языковой моделью». Языковые модели фиксируют статистическую структуру языка — «скрытое пространство». После обучения языковой модели введите исходную текстовую строку (называемую «условными данными», кондиционирующими данными), образец из языковой модели, вы можете создать новый токен, добавить новый токен к условным данным, ввести его снова и повторить Процесс Последовательности любой длины могут быть сгенерированы.

Давайте начнем с простого примера: возьмем слой LSTM, возьмем строку из N символов из текстового корпуса и обучим модель генерировать N+1-й символ. Результатом модели является выполнение softmax, чтобы получить распределение вероятностей следующего символа по всем возможным символам. Эта модель называется «моделью нейронного языка на уровне символов».

使用语言模型逐个字符生成文本的过程

стратегия выборки

При генерации текста с помощью моделей нейронного языка на уровне символов наиболее важным вопросом является выбор следующего символа. Вот несколько распространенных методов:

  • Жадная выборка: всегда выбирайте следующий наиболее вероятный символ. Этот метод может привести к повторяющимся, предсказуемым строкам и, возможно, к бессвязным значениям. (ассоциация метода ввода)
  • Чистая случайная выборка: следующий символ выбирается из равномерного распределения вероятностей, где каждый символ имеет одинаковую вероятность. Это слишком случайно, чтобы генерировать интересный контент. (Это комбинация случайных выходных символов)
  • стохастическая выборка: согласно результатам языковой модели, если существует вероятность 0,3, что следующим символом будет e, то у вас есть 30% вероятность его выбора. Немного рандома, чтобы сделать генерируемый контент болееслучайныйБогатый вариациями, но не полностью случайный, вывод может быть более интересным.

Случайная выборка выглядит хорошо и креативно, но проблема в том, что вы не можете контролировать степень случайности: большая случайность может быть творческой, но может быть случайным выводом; меньшая случайность может быть ближе к реальным словам, но слишком жесткая и предсказуемая.

Чтобы контролировать степень случайности в процессе выборки, вводится параметр: «температура softmax», который используется для представления энтропии распределения вероятностей выборки, то есть насколько неожиданным или предсказуемым будет следующий выбранный символ:

  • Более высокая температура: выборочное распределение с большей энтропией будет генерировать более неожиданные и неструктурированные данные;
  • Более низкая температура: соответствует меньшей случайности, что приводит к более предсказуемым данным.

对同一个概率分布进行不同的重新加权:更低的温度=更确定,更高的温度=更随机

Конкретная реализация заключается в том, что при заданном значении температуры выходные данные 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 на уровне символов.

подготовка данных

Во-первых, нам нужно большое количество текстовых данных (корпуса) для обучения языковой модели. Вы можете найти один или несколько текстовых файлов достаточно большого размера: Википедия, различные книги и т.д. Здесь мы решили использовать некоторые из работ Ницше (английский перевод), так что языковая модель, которую мы изучаем, будет иметь стиль письма и тему Ницше. (Много лет я сам писал дикие модели и играл в них, используя Лу Синя?)

Загрузите корпус и преобразуйте его во все строчные буквы:

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

Далее нам нужно преобразовать текст в данные (векторизация): извлечь длину из текстаmaxlenпоследовательность (имеется частичное перекрытие между последовательностями), выполняется однократное кодирование, а затем упаковывается в(sequences, maxlen, unique_characters)форма. При этом нужно еще и массив подготовитьy, содержащий соответствующие цели, т. е. символы, которые появляются после каждой извлеченной последовательности (также с горячим кодированием):

# 将字符序列向量化

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 + слой Dense, активированный softmax. (На самом деле нет необходимости использовать 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:

第一轮输出结果

Раунд 30:

第30轮输出结果

Раунд 59:

第59轮输出结果

При использовании большего количества данных для обучения более крупной модели время обучения увеличивается, а сгенерированные образцы будут более согласованными и реалистичными. Но в любом случае, сгенерированный таким образом текст не имеет никакого смысла. Все, что делает машина, — это выборка данных из статистических моделей, она не понимает ни человеческого языка, ни того, о чем говорит.

Генерация текста на основе встраивания слов

Если мы хотим сгенерировать китайский текст, у нас слишком много китайских иероглифов, и я не думаю, что это хороший выбор — делать это посимвольно. Таким образом, вы можете рассмотреть возможность создания текста на основе встраивания слов. На основе предыдущей генерации текста LSTM на уровне символов метод кодирования/декодирования немного изменен, и добавлен слой внедрения для достижения генерации текста на основе встраивания основного слова:

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,代码同前一个例子,这里省略了
        ...

Я использовал некоторые статьи Лу Синя для обучения, и конечный результат, вероятно, был таким:

截屏2021-08-13 14.58.34.png

Видно, что в этих предложениях нет смысла, и смотреть на них очень неудобно. Таким образом, мы также можем изменить метод сегментации токенов, но не сегментации слов, а сегментации предложений:

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

Остальные места практически не изменились, поэтому вы также можете получить более интересный текст. Например, это результат моей тренировки с некоторыми статьями Юй Цюйю:

截屏2021-08-13 14.58.15.png

Это все еще грязно и бессмысленно, но, по крайней мере, выглядит немного удобнее.

Если вы хотите дождаться хорошего результата, проще всего увеличить данные и параметры сети. Или напрямую использовать GPT-3, CPM этих больших сетей :)