Это 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)
Обучите языковую модель и сделайте выборку из нее
Учитывая языковую модель и фрагмент начального текста, новый текст может быть сгенерирован путем повторения следующих операций:
- Учитывая существующий текст, получить распределение вероятностей следующего символа из модели;
- Перевесить распределение в соответствии с определенной температурой;
- Произвольная выборка следующего символа в соответствии с перевзвешенным распределением;
- Добавьте новые символы в конец текста.
Перед обучением модели мы сначала написали «функцию выборки», которая отвечает за перевзвешивание исходного распределения вероятности, полученного моделью, и извлечение из него индекса символа:
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:
Раунд 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,代码同前一个例子,这里省略了
...
Я использовал некоторые статьи Лу Синя для обучения, и конечный результат, вероятно, был таким:
Видно, что в этих предложениях нет смысла, и смотреть на них очень неудобно. Таким образом, мы также можем изменить метод сегментации токенов, но не сегментации слов, а сегментации предложений:
text = text.replace(',', ' ,').replace('。', ' 。').replace('?', ' ?').replace(':', ' :')
token_text = tf.keras.preprocessing.text.text_to_word_sequence(text, split=' ')
Остальные места практически не изменились, поэтому вы также можете получить более интересный текст. Например, это результат моей тренировки с некоторыми статьями Юй Цюйю:
Это все еще грязно и бессмысленно, но, по крайней мере, выглядит немного удобнее.
Если вы хотите дождаться хорошего результата, проще всего увеличить данные и параметры сети. Или напрямую использовать GPT-3, CPM этих больших сетей :)