Рубрики
Статьи

Как научить нейросеть генерировать текст с помощью LSTM в PyTorch

Статья написана по мотивам очередной домашки в MADE, где мы учили нейронку писать стихи с помощью рекурентных сетей.

Для начала, договоримся, что будем делать «глупую» нейросеть, которая не разбирается в языке, построении фраз, предложений, смыслах, а просто учится предсказывать следующий символ по предыдущему тексту.

Из плюсов такого подхода — не нужно париться о пунктуации, больших и маленьких буквах, нейронка их расставит сама. Из минусов — такая нейросеть «туповата».

Так же можно генерировать любые последовательности. К примеру, музыку. MIDI конвертируем в формат ABC, получаем текст (точнее, последовательность символов). На этом тексте учимся, генерируем свой, превращаем его обратно в MIDI и получаем «нейромузыку».

Если генерировать тексты, будет получаться что-то вроде:

Потом ахали на наших специалистов: они, не имея вакцины, не зная, как мы действуем сейчас: малейшее подозрение — мы проверяем человека, надо на карантин — пожалуйста, давайте на карантин. Именно точечно, профессионально надо действовать, а не территориально не сможем, это невозможно. Особенно там, но полоза. Потому что они живут, как обычно (находятся не только в каких-то все обсуждать, но и проблема. Надо «желтые жилеты» вернуть с улиц. У кого-то выборы. У кого-то еще чего-то. И каждый начинает вертерах на неменех, не дали было спросы.

Нейросеть, обученная на изречениях бессменного лидера РБ

А иногда выходит что-то совсем прекрасное:

Стоит ли любить?
Стоит ли любить?
Стоит ли любить?
Стоит и приплыли.

Нейросеть, обученная на хокку

Практическое применение этому мне сложно придумать. Скорее, это — про побаловаться. Ну и сотворить что-то забавное. Fake news этом не сделаешь. Точнее, сделаешь, но они будут наредкость забавны.

И важный момент. Если что-то непонятно или просто неинтересно, а попробовать генерировать текст хочется, то достаточно просто перейти по ссылке в Google Colab в конце статьи, закинуть туда свой файлик с текстом и запустить все ячейки с кодом.

Итак, поехали.

1. Импортируем нужные библиотеки

Из внешних библиотек нам понадобятся только numpy и pytorch:

from collections import Counter

import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np

2. Готовим данные для сети

Токенизируем текст. Превращаем его в индексы. Т.е. достаем все уникальные символы: буквы, пробелы, знаки препинания в тексте и каждому символу присваиваем число. Например:

  • ‘a’ => 1
  • ‘Щ’ => 2
  • ‘!’ => 3

Это будем называть словарем. А числа — индексами.

Делаем прямой и обратный (индексы в символы) словарь. А потом проходимся по всему тексту и превращаем с помощью составленного нами словаря каждый символ в индекс.

TRAIN_TEXT_FILE_PATH = 'train_text.txt'

with open(TRAIN_TEXT_FILE_PATH) as text_file:
    text_sample = text_file.readlines()
text_sample = ' '.join(text_sample)

def text_to_seq(text_sample):
    char_counts = Counter(text_sample)
    char_counts = sorted(char_counts.items(), key = lambda x: x[1], reverse=True)

    sorted_chars = [char for char, _ in char_counts]
    print(sorted_chars)
    char_to_idx = {char: index for index, char in enumerate(sorted_chars)}
    idx_to_char = {v: k for k, v in char_to_idx.items()}
    sequence = np.array([char_to_idx[char] for char in text_sample])
    
    return sequence, char_to_idx, idx_to_char

sequence, char_to_idx, idx_to_char = text_to_seq(text_sample)

3. Генерируем батчи из текста

Генерируем из последовательности наших индексов батчи (сразу несколько строк текста) для обучения сети. Не будем усложнять, просто достанем несколько случайных строк из текста фиксированной длины.

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

Условно:

  • Обучающая выборка: Привет как дел
  • Таргет: ривет как дела

Размерность тензора батча: [BATCH_SIZE x SEQ_LEN x 1]

SEQ_LEN = 256
BATCH_SIZE = 16

def get_batch(sequence):
    trains = []
    targets = []
    for _ in range(BATCH_SIZE):
        batch_start = np.random.randint(0, len(sequence) - SEQ_LEN)
        chunk = sequence[batch_start: batch_start + SEQ_LEN]
        train = torch.LongTensor(chunk[:-1]).view(-1, 1)
        target = torch.LongTensor(chunk[1:]).view(-1, 1)
        trains.append(train)
        targets.append(target)
    return torch.stack(trains, dim=0), torch.stack(targets, dim=0)

4. Пишем функцию, которая генерирует текст

Теперь напишем функцию, которая предсказывает текст с помощью нашей обученной нейросети. Это удобно сделать заранее, чтобы смотреть, что генерирует сеть во время обучения.

Сеть предсказывает нам вероятности следующей буквы, и мы с помощью этих вероятностей достаем случайно по одной букве. Если повторить операцию 1000 раз, получим текст из 1000 символов.

Параметр start_text нам нужен, чтобы было что-то, для чего предсказывать следующий символ. У нас этот символ по умолчанию — пробел, и задача сети сначала — предсказать следующий символ после пробела. Потом — следующий после этих 2-х символов. И т.д.

Параметр temp — это уровень «случайности» генерируемого текста. Так называемая «температура» с отсылкой к понятию «энтропии». То, на что делим логиты в softmax. Поставим высокую — вероятность каждой буквы будет почти одинакова и текст превратится в случайную белиберду. Поставим низкую — каждый раз будем предсказывать одно и то же и можем зациклиться на одной фразе.

def evaluate(model, char_to_idx, idx_to_char, start_text=' ', prediction_len=200, temp=0.3):
    hidden = model.init_hidden()
    idx_input = [char_to_idx[char] for char in start_text]
    train = torch.LongTensor(idx_input).view(-1, 1, 1).to(device)
    predicted_text = start_text
    
    _, hidden = model(train, hidden)
        
    inp = train[-1].view(-1, 1, 1)
    
    for i in range(prediction_len):
        output, hidden = model(inp.to(device), hidden)
        output_logits = output.cpu().data.view(-1)
        p_next = F.softmax(output_logits / temp, dim=-1).detach().cpu().data.numpy()        
        top_index = np.random.choice(len(char_to_idx), p=p_next)
        inp = torch.LongTensor([top_index]).view(-1, 1, 1).to(device)
        predicted_char = idx_to_char[top_index]
        predicted_text += predicted_char
    
    return predicted_text

5. Создаем класс нашей нейросети

И наконец наша маленькая и уютная нейросеть. Она работает так:

  1. Превращаем каждый символ на входе сети в вектор (так называемный эмбеддинг).
  2. Скармливаем эти векторы нашему LSTM слою. У этого слоя есть особенность: он работает не независимо для каждого символа, а помнит, что к нему раньше приходило на вход. Притом, помнит не все: ненужное он умеет забывать. Такие слои называют рекуррентными и часто используют при работе с последовательностями.
  3. Выходы из LSTM слоя пропускаем через Dropout. Этот слой «мешает» сети учиться, чтобы ей сложнее было выучить весть текст.
  4. Дальше отправляем выход из Dropout на линейный слой размерности словаря, чтобы на выходе получить столько чисел, сколько у нас символов в словаре. Потом мы этот вектор чисел будем превращать в «вероятности» каждого символа с помощью функции softmax.
class TextRNN(nn.Module):
    
    def __init__(self, input_size, hidden_size, embedding_size, n_layers=1):
        super(TextRNN, self).__init__()
        
        self.input_size = input_size
        self.hidden_size = hidden_size
        self.embedding_size = embedding_size
        self.n_layers = n_layers

        self.encoder = nn.Embedding(self.input_size, self.embedding_size)
        self.lstm = nn.LSTM(self.embedding_size, self.hidden_size, self.n_layers)
        self.dropout = nn.Dropout(0.2)
        self.fc = nn.Linear(self.hidden_size, self.input_size)
        
    def forward(self, x, hidden):
        x = self.encoder(x).squeeze(2)
        out, (ht1, ct1) = self.lstm(x, hidden)
        out = self.dropout(out)
        x = self.fc(out)
        return x, (ht1, ct1)
    
    def init_hidden(self, batch_size=1):
        return (torch.zeros(self.n_layers, batch_size, self.hidden_size, requires_grad=True).to(device),
               torch.zeros(self.n_layers, batch_size, self.hidden_size, requires_grad=True).to(device))

6. Создаем нейросеть и обучаем ее

Теперь создаем нейросеть и обучаем ее. LSTM блок принимает немного другой формат батча:

[SEQ_LEN x BATCH_SIZE x 1], поэтому делаем permute для тензоров train и target, чтобы поменять 0 и 1 размерность местами.

Параметры нейросети, которые может понадобиться подкрутить:

  • hidden_size — влияет на сложность сети. Стоит повышать для текстов большого размера. Если выставить большое значение для текста маленького размера, то сеть просто выучит весь текст и будет генерировать его же.
  • n_layers — опять же, влияет на сложность сети. Грубо говоря, позволяет делать несколько LSTM слоев подряд просто меняя эту цифру.
  • embedding_size — размер обучаемого эмбеддинга. Можно выставить в несколько раз меньше размера словаря (числа уникальных символов в тексте) или примерно такой же. Больше — нет смысла.

Дальше — стандартный для PyTorch цикл обучения нейросети: выбираем функцию потерь, оптимизатор и настраиваем расписание, по которому меняем шаг оптимизатора. В нашем случае снижаем шаг в 2 раза, если ошибка (loss) не падает 5 шагов подряд.

Если очень грубо, то здесь мы много раз подаем в нейросеть разные кусочки текста и учим ее делать все меньше и меньше ошибок, когда она предсказывает следующую букву по предыдущему тексту.

device = torch.device('cuda') if torch.cuda.is_available() else torch.device('cpu')
model = TextRNN(input_size=len(idx_to_char), hidden_size=128, embedding_size=128, n_layers=2)
model.to(device)

criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-2, amsgrad=True)
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
    optimizer, 
    patience=5, 
    verbose=True, 
    factor=0.5
)

n_epochs = 50000
loss_avg = []

for epoch in range(n_epochs):
    model.train()
    train, target = get_batch(sequence)
    train = train.permute(1, 0, 2).to(device)
    target = target.permute(1, 0, 2).to(device)
    hidden = model.init_hidden(BATCH_SIZE)

    output, hidden = model(train, hidden)
    loss = criterion(output.permute(1, 2, 0), target.squeeze(-1).permute(1, 0))
    
    loss.backward()
    optimizer.step()
    optimizer.zero_grad()
    
    loss_avg.append(loss.item())
    if len(loss_avg) >= 50:
        mean_loss = np.mean(loss_avg)
        print(f'Loss: {mean_loss}')
        scheduler.step(mean_loss)
        loss_avg = []
        model.eval()
        predicted_text = evaluate(model, char_to_idx, idx_to_char)
        print(predicted_text)

При обучении должно получиться на выходе что-то вроде:

Loss: 3.0337722063064576
В то не не посто на на не не на на не на помотророне на онот потарити не сем посто нат на рото сто на но на то тосте на посто нал на на на на росмоно посто но это воме на не вотато не не но на не нам на
Loss: 2.4729482173919677
В поторовововать оточели не не тора на на на на подовать не то не на подени не на вобомать не не прорус не сто на на сторы домем водем не сто не это соторовоста вомать на на проденить не бостова сто пор
Loss: 2.2126503944396974
В это надо поторовать на содолько не полодали не то начения. Нако не надо на работать на на полили на на наши наши подули сотовать на нало обранить на на постовать не наши на поставил в ирания стравом м
Loss: 2.028042938709259
В кому просторы на наши на комперами на только полоблимать должны поровать не том приратить подурать рас совеловали полжно от востание в мостам в сего принимали сотрать в нам нам от проблимать больно на
.......
.......
.......
Loss: 0.5439809119701385
В ибыратий от безработицы и доклады. Авторов мы сегодня по тому сли нас дороге, чтобы они смогли выйти и пережил уже многие психозом особенно надо выживать на эту тему в этот пережил уже многие психозы,
Loss: 0.5482879960536957
В ибывать на сегодняшний день отношение домерми вде выписов из меня частся с вами и серация — ни одного сказать, что мы сейчас делаем, они понятный их сполоси, которые основной пофтомие потери людям за 
Loss: 0.5576688635349274
В конятно к этой пришетов не проблема станок их работать, потому что это повторилось.
 Результатом стал устойчивый рост благосостояния населения. Мы точно дошло предложено. Политически запрещаю. Давайте

Можете остановить обучение в любой момент. К примеру, тогда, когда ошибка (Loss) перестанет снижаться.

7. Генерируем текст

Итак, наша сеть обучилась. Давайте что-нибудь сгенерируем:

model.eval()
print(evaluate(
    model, 
    char_to_idx, 
    idx_to_char, 
    temp=0.3, 
    prediction_len=1000, 
    start_text='. '
    )
)

Рекомендую поиграть с параметром temp и, конечно же, start_text. С помощью start_text можно попробовать «задать тему/направление» для генерируемого текста. Желательно, ту, которая есть в тексте, на котором сеть училась, конечно.

Надеюсь, все получилось и нигде ничего не свалилось.

Не хочу ничего делать, хочу сразу генерировать текст

Так тоже можно. Просто откройте jupyter notebook в Google Colab, скопируйте туда файлик с текстом и выполните все ячейки.

Открыть готовый код в Google Colab

После того, как открыли, нужно скопировать туда и файл с текстом, на котором будет тренироваться нейросеть.

Просто откройте список файлов и перетяните туда свой файл с текстом.

Вот и все. Можно развлекаться. Просто запустите подряд все ячейки с кодом. Когда будете учить нейросеть (предпоследняя ячейка), можете не ждать, пока обучение дойдет до конца и остановить в любой момент.

И еще крайне рекомендую в настройках Colab «Runtime» => «Change runtime type» => «Hardware accelerator» выбрать «GPU». На видеокарте сеть учится в разы быстрее.

На всякий случай, выложил код на github.

Если есть какие-то вопросы, что-то не работает или вы нашли баг, пишите, постараюсь помочь.

P.S.: если получится сгенерировать что-то забавное, сбросьте это сюда в комментарии 🙂

Автор: Алексей Ярошенко

Data Scientist / ML Engineer. Раньше занимался интернет-маркетингом и учил людей контексту. Сертифицированный тренер Google в Беларуси.

16 ответов к “Как научить нейросеть генерировать текст с помощью LSTM в PyTorch”

Круто! Не слишком сложно и в конце получаешь наглядный результат.

Я работал с генецией текста ботами, но там сложные ветвления и подстановки были + бот затачивался человеком. Тут вообще можно исключить работу человека с текстом.

Какие подходы можно использовать для генерации более осмысленного текста? Пробывали GPT2? Мне кажется, что все нейросети страдают от специфики корпуса текста который используется для обучения.

Спасибо 🙂

Пока не пробовал, как разберусь нормально с трансформерами, может и попробую сгенерировать что-то более осмысленное с GPT-2.

На одной конфе ребята из VK рассказывали про что-то подобное с попытками заменить саппорт для частых вопросов нейронкой. Не знаю, насколько это у них прижилось.

Обучил на цитатах из «пацанских пабликов» в вк:

«Я не боюсь никого кроме Бога, потому что дни с тобой — самые лучшие дни»

Это шедевр

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

В статье — самый «глупый» подход без какой-либо языковой модели 🙂 Конечно, текст можно генерировать, даже подавая на вход картинку. Если интересно, смотрите архитектуры на базе трансформеров + разные вариации seq2seq.

А вообще, нейронка не должна «заучивать» текст. Если она все же заучила текст, это переобучение: или текст слишком маленький, или модель слишком сложная.

Спасибо за статью.
Подскажите, как сохранить обученную модель для дальнейщего использования?
Чтобы не обучать сеть каждый раз.

«Сторона 2017 году потому самый от того, что он выстрелила подобной раз с вернуться мне подумала и получились в место были сейчас и совершенно слабыми своей стороны и взял на собой мостовать и своей дело подобного на дверь о том, что он не все больше в свое время не просто закрывая на стороны в положить день»

Примерно так генерирует у меня при temp=0.352, prediction_len=300, start_text=’Сторона ‘

В обучении принимало участие около 120 мегабайт художественных текстов.

Не понимаю фразы:
Открыть готовый код в Google Colab

После того, как открыли, нужно скопировать туда и файл с текстом, на котором будет тренироваться нейросеть.

ЧТо значит:скопировать туда и файл с текстом.

Не понимаю, как скопировать и куда. ЧТо скопировать имя, куда это имя вставить
Если сам файл копировать то как. Есть команда скопировать файл?. Тогда куда его скопировать

Скопировать в колаб файл — просто перетянуть из своей папки на компьютере в папку на колаб текстовый файлик мышкой. Файлик должен называться «train_text.txt»

Специально же картинку сделал 🙂

colab

Везде бардак
В 2000 столкнулся с тем, что в МЛМ кто попало,как попало объясняет
Общаться не возможно
Жена уехала жить в США
Общаться не возможно в США, она пишет

Везде типа мультика «Простоквашино», когда они вместе писали письмо:» Здравствуйте папа и мама…а на днях я линять стал…Ваш сын, дядя шарик»

Я писал в Си и других,там жесткий порядок для групповой разработки
Там автоматически делается заготовка со стилем и комментариями и ты следуешь этому стилю
Но в питоне каждый как попало пишет
И что делают: берут ДЕФОРМИРУЮТ ПЕРВОИСТОЧНИК и вот,он теперь то же гений.

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

1. я купил одyнy книгу,там код в книге не соответствует коду в GitHub
Код в книге не рабочий и в GitHub не рабочий и все время меняется.Получается,что народ тестирует код у этого придурка

2. Со второй и третьей книгой такая же ерунда

Моя голова уже забита всякой тарабарщиной, которая ни к селу ни к городу,зато автора раскручиваем в инет

Я уже боюсь лезть в GitHub

Ну я привык последовательно делать,ведь физически нельзя сколотить что то, а потом для этого купить гвозди

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

Извините, но это кругом,в политике и в экономике

Извините,я последовательно,как в физической реальности все читаю и делаю и что пишите:
«Открыть готовый код в Google Colab

После того, как открыли, нужно скопировать туда и файл с текстом, на котором будет тренироваться нейросеть.»
Если бы вы написали:» …Перетащить мышкой из GitHub в …»

Это называется в лингвистике ошибкой Элипсисиса и еще тут не точности. Какие у человека ошибки в последовательности изложения, такие ошибки в физической реальности, т.е бардак и хаос
Получается либо надо уже иметь опыт, но тогда не нужна ваша программа, либо надо все прочитать, потом восстановить правильные,точные и последовательные действия

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

Меня все больше тошнит от этой сраной жизни среди идиотов

Вы писали про технологическую сингулярность,что вытесняет техника с рабочих мест.Об этом и Маркс писал, Манифесте
Но люди сами также отравляют себе жизнь из-за словоблудия не только в библии об этом, но и Витгенштейн об этом писал

Спасибо вам за помощь
Извините, а не могли бы вы дать
1. файл train_text.txt, его содержание ( ДатаСет сейчас называют)
2. Как запустить после обучения тестирование
3. Приблизительно, что должно получиться поле тестирования

У всех авторов эти моменты не объясняются
Говорят другие программисты, что надо раз увидеть своими глазами со стороны и такие вопросы отпадут на всегда

— Почти все авторы зачем то долго рассказывают про устройство мозга
— Потом про высшую математику
— Дале начинается поэзия, аналогии из личной жизни и все такое

— Ни слова ни у кого про алгоритм
— Архитектуру дают которая не вписывается
— Соответствие блоков архитектуры коду отсутствует и ты долго гадаешь, перебираешь варианты
— Блок схемы нет
— Алгоритм и соответствие коду отсутствует
( в Дельфи,например, в описании ссылаются на номера строк кода)
— Комментарии к функциям отсутствуют ( вместо этого высшая математика)
— я часто встречаю просто … объяснения, например про свертку в виде интеграла. На кой черт это надо для понимания работы
— существует методология черного ящика, когда описывают тестовый сигнал на входе и то, что будет на выходе, но этим пользуются все технари, но не программисты

Результат, все умеют пользоваться, но не знают принципы работы,
все объясняют одно и то же про мозг,математику и собственную жизнь

Я начинал с радиоэлектроники, но её в Питере уничтожили (промышленность и науку). А там были именно такие принципы
Значение тестового сигнала, порядок и принципы настройки параметров, пути улучшения и развития, создание на основе новых технологий

Сейчас многие дают Датасет, результаты обучения и тестирования,
но я нашел только для классификации, но не для генерации

Давно заинтересовался Экспертными системами в прикладных науках
Тут вечное счастье и работа.
Но я делал упор на логическое программирование и не знал,
что нейронки получили быстрое развитие
Сейчас надо любой простой чат бот подключить к сайту и дальше делать гибридную систему
Но делать чат бот на регулярных не хочется, хочется разобраться с нейронками,тем более до цели не много осталось
Извините, повторю просьбу
Не могли бы вы дать
1. файл train_text.txt, его содержание
2. Как запустить тестирование после обучения
3. Приблизительно, что должно получиться поле тестирования

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

С удовольствием бы участвовал с вами,помогал вам,продвигал, дополнял бы ваши идеи и проекты

Ни фига не понимаю
Мне надо на вопрос, на одно предложение по обученной теме,
дать подходящий ответ или что то похожее на ответ
Открыть готовый код в Google Colab пока не получилось
Помогите пожалуйста

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте как обрабатываются ваши данные комментариев.