Лабораторная работа №3. Генерация текста с помощью n-грамм
Full API
Дано
Текст на английском языке (
assets/Harry_Potter.txt
), который загружен и сохранен в переменнуюtext
вstart.py
.Языковой профиль английского языка (
assets/en_own.json
).
Генерация текста — одна из прикладных задач обработки естественного языка (Natural Language Processing, NLP). На данный момент существует много больших языковых моделей, которые могут генерировать тексты, близкие к написанным человеком.
Задача генерации текста реализует возможность языковой модели на основе исходного текста предсказывать последующее слово и генерировать осмысленный текст.
Существуют различные алгоритмы, позволяющие генерировать последовательности. По ходу выполнения лабораторной работы Вы познакомитесь с тремя алгоритмами:
Терминология
В рамках лабораторной работы Вы будете работать с n-граммами.
- N-граммы
Последовательность из n элементов, включенная в другую последовательность.
В настоящей лабораторной мы будем работать с n-граммами, состоящими из закодированных токенов текста.
Допустим, у нас есть следующий закодированный текст:
(1, 2, 3, 0, 4, 1, 0, 2, 5, 6, 6, 7, 0, 2, 3, 0, 4, 1, 0, 2, 5, 6, 6, 7, 0)
Каждое число в этой последовательности однозначно соответствут какому-либо токену.
Тогда биграммы (n-граммы размера 2) имеют следующий вид:
((1, 2), (2, 3), (3, 0), (0, 4), (4, 1), (1, 0), (0, 2), (2, 5),
(5, 6), (6, 6), (6, 7), (7, 0), (0, 2), (2, 3), (3, 0), (0, 4), (4, 1), (1, 0),
(0, 2), (2, 5), (5, 6), (6, 6), (6, 7), (7, 0))
;
Иными словами, это все подпоследовательности длины 2, которые можно извлечь из этого текста.
Триграммы (N-граммы размера 3) имеют следующий вид:
((1, 2, 3), (2, 3, 0), (3, 0, 4), (0, 4, 1), (4, 1, 0), (1, 0, 2),
(0, 2, 5), (2, 5, 6), (5, 6, 6), (6, 6, 7), (6, 7, 0), (7, 0, 2), (0, 2, 3),
(2, 3, 0), (3, 0, 4), (0, 4, 1), (4, 1, 0), (1, 0, 2), (0, 2, 5), (2, 5, 6),
(5, 6, 6), (6, 6, 7), (6, 7, 0))
Что необходимо сделать
Шаг 0. Начать работу над лабораторной (вместе с преподавателем на практике)
Измените файлы
main.py
иstart.py
.Закоммитьте изменения и создайте новый Pull Request.
Important
Код, выполняющий все требуемые действия, должен быть написан в
функции main
в модуле start.py
.
Для этого реализуйте функции в модуле main.py
и импортируйте их в start.py
.
Вызов функции в файле start.py
:
if __name__ == '__main__':
main()
В рамках данной лабораторной работы нельзя использовать сторонние модули, а также стандартные модули collections и itertools.
Обратите внимание, что в файле target_score.txt
необходимо выставить
желаемую оценку: 4, 6, 8 или 10. Чем выше желаемая оценка, тем большее
количество тестов запускается при проверке вашего Pull Request.
Шаг 1. Объявить сущность по обработке текста, кодированию и декодированию
Для работы с текстом в первую очередь необходимо научиться предобрабатывать сырые
текстовые данные. В этом нам поможет класс
lab_3_generate_by_ngrams.main.TextProcessor
, который Вы реализуете в
ходе выполнения первого шага. В зону ответственности данного класса входят любые
манипуляции с текстом, включая его очистку, токенизацию, кодирование и декодирование.
Данный этап работы является ключевым, так как благодаря нему становится возможным
использование языковых моделей, которые выявляют закономерности только в числовых данных.
Класс имеет следующие внутренние атрибуты:
self._end_of_word_token
- это специальный символ_
, обозначающий конец слова при посимвольной токенизации;self._storage
- словарь для хранения буквы-токена и ее идентификатора, который на данном этапе должен быть заполнен специальным символом и его идентификатором равным0
.
Important
Оба эти атрибута являются защищенными, то есть обращение к ним за пределами методов этого класса не предполагается.
Шаг 1.1. Токенизировать заданную последовательность
Реализуйте метод
lab_3_generate_by_ngrams.main.TextProcessor._tokenize()
,
который позволяет разбить текст на токены.
Текст должен быть приведен к нижнему регистру и очищен от знаков препинания и цифр.
Токеном в данном случае является один буквенный символ.
В качестве разделителя между словами используется end_of_word_token
.
Метод разбивает текст на токены, вставляя после каждого слова специальный токен
конца слова end_of_word_token
, который сохранен в соответствующем атрибуте экземпляра
класса. Границей слова в данной работе выступает любой пробельный символ или их сочетание.
Если слово состоит полностью из цифр и знаков препинания, то вставлять токен конца слова после него не нужно. Иными словами, в токенизированной последовательности не должно быть двух токенов конца слова подряд.
Note
Разделитель добавляется после последнего слова, если текст заканчивается пробелом или знаком препинания. В противном случае, разделитель после последнего слова не добавляется.
Например, строка 'She is happy. He is happy.'
должна быть
токенизирована следующим образом:
('s', 'h', 'e', '_', 'i', 's', '_', 'h', 'a', 'p', 'p', 'y', '_',
'h', 'e', '_', 'i', 's', '_', 'h', 'a', 'p', 'p', 'y', '_')
,
Строка 'She is happy. He is happy'
токенизируется так:
('s', 'h', 'e', '_', 'i', 's', '_', 'h', 'a', 'p', 'p', 'y', '_',
'h', 'e', '_', 'i', 's', '_', 'h', 'a', 'p', 'p', 'y')
Note
Если на вход подается аргумент неправильного типа, то есть не
строка, или строка пустая, или если при токенизации не было найдено
ни одной буквы, то возвращается значение None
.
Шаг 1.2. Добавить букву в хранилище
На этом шаге Вам нужно каждой букве из заданного текста присвоить некоторый уникальный идентификатор. Это необходимо для того, чтобы работать не со строками напрямую, а с числами, которые их представляют.
Для этого реализуйте метод
lab_3_generate_by_ngrams.main.TextProcessor._put()
.
Note
Идентификатор - значение, которое однозначно указывает
на токен и равно длине _storage
(атрибут объекта данного
класса) на момент добавления буквы. Идентификатор специального
символа _
принимает значение 0
.
Правила корректного заполнения хранилища:
Идентификаторы уникальны и однозначно указывают на токен (например, при добавлении нового токена можно использовать длину хранилища _storage).
Для одной и той же буквы существует ровно один идентификатор;
Одинаковых идентификаторов у двух разных букв быть не может;
Если буква уже существовала в хранилище, идентификатор остается прежним;
Идентификатор специального символа _ должен принимать значение 0.
Например, если на вход подается буква 's'
, то хранилище будет
выглядеть следующим образом - {'_': 0, 's': 1}
.
Note
Если на вход подается некорректное значение (аргумент
неправильного типа, то есть не строка, или длина строки отлична от 1),
возвращается None
.
Important
Данный метод является защищенным - его использование за пределами методов класса не предполагается.
Шаг 1.3. Получить идентификатор буквы
Для успешного кодирования текста Вам необходимо научиться получать идентификатор для каждой буквы из токенизированного текста.
Для этого реализуйте метод
lab_3_generate_by_ngrams.main.TextProcessor.get_id()
.
Например, в хранилище вида {'_': 0, 's': 1}
для 's'
метод вернет идентификатор 1
.
Note
Если на вход подается некорректное значение аргумента (то есть тип аргумента не строка), или буква отсутствует в хранилище, возвращается None.
Шаг 1.4. Получить букву по идентификатору
Теперь сделаем обратный процесс.
Для декодирования, реализуйте метод
lab_3_generate_by_ngrams.main.TextProcessor.get_token()
,
который получает букву по заданному идентификатору.
Например, в хранилище вида {'_': 0, 's': 1}
для идентификатора
1
метод вернет 's'
.
Note
Если на вход подается неизвестный (отсутствует в хранилище)
или некорректный (не соответствует типу данных
int
) идентификатор, возвращается None
.
Шаг 1.5. Закодировать текст
Реализуйте метод
lab_3_generate_by_ngrams.main.TextProcessor.encode()
,
который кодирует текст. Он обязательно должен вызывать методы
lab_3_generate_by_ngrams.main.TextProcessor._tokenize()
,
lab_3_generate_by_ngrams.main.TextProcessor._put()
и
lab_3_generate_by_ngrams.main.TextProcessor.get_id()
.
Например, возьмем текст "She is happy. He is happy."
и
заполним по нему хранилище:
{'_': 0, 's': 1, 'h': 2, 'e': 3, 'i': 4, 'a': 5, 'p': 6, 'y': 7}
.
В результате кодирования текста выше должен получиться кортеж,
в котором каждый элемент соответствует идентификатору
буквы из хранилища _storage
:
encoded_corpus = (1, 2, 3, 0, 4, 1, 0, 2, 5, 6, 6, 7, 0, 2, 3, 0, 4, 1, 0, 2, 5, 6,
6, 7, 0)
Note
Если на вход подаются некорректные значения (текст не является
строкой или строка пустая), возвращается None
. Если какой-либо
из методов, который вызывается в данном методе, возвращает None
,
метод также должен вернуть None
.
Шаг 1.6. Декодировать текст в токенизированную последовательность
Теперь, когда у нас есть выделенные токены и присвоенные им числовые идентификаторы, мы можем декодировать любую последовательность.
Реализуйте метод
lab_3_generate_by_ngrams.main.TextProcessor._decode()
,
который позволяет преобразовать закодированный текст в кортеж,
состоящий из буквенных и специальных символов.
Метод обязательно должен вызывать
lab_3_generate_by_ngrams.main.TextProcessor.get_token()
.
Например, для закодированного корпуса
(1, 2, 3, 0, 4, 1, 0, 2, 5, 6, 6, 7, 0, 2, 3, 0, 4, 1, 0, 2, 5, 6, 6, 7, 0)
у вас должен получиться кортеж следующего вида:
decoded_corpus = ('s', 'h', 'e', '_', 'i', 's', '_', 'h', 'a', 'p', 'p', 'y', '_',
'h', 'e', '_', 'i', 's', '_', 'h', 'a', 'p', 'p', 'y', '_')
Note
Если на вход подаются некорректные значения (аргумент не является
кортежем или кортеж пустой), если токенизированный текст или идентификатор
буквы принимают значение None
, то метод возвращается None
.
Important
Данный метод является защищенным - его использование за пределами методов класса не предполагается.
Шаг 1.7. Декодировать текст в строковый формат
Реализуйте метод
lab_3_generate_by_ngrams.main.TextProcessor._postprocess_decoded_text()
,
который позволяет перейти от токенизированного текста в формате
кортежа к тексту в строковом формате.
При этом на выходе строка должна соответствовать следующим требованиям:
Строка должна начинаться с заглавной буквы.
В конце строки ставится
.
.
Important
Специальные токены конца слова должны быть конвертированы в пробелы. При этом пробел после последнего слова в тексте ставить не нужно.
Например, для декодированного на предыдущем шаге корпуса
('s', 'h', 'e', '_', 'i', 's', '_', 'h', 'a', 'p', 'p', 'y', '_',
'h', 'e', '_', 'i', 's', '_', 'h', 'a', 'p', 'p', 'y', '_')
должен получиться следующий текст: She is happy he is happy.
.
Note
Если на вход подаются некорректные значения (токенизированный
текст не является кортежем или кортеж пустой), возвращается
None
.
Important
Данный метод является защищенным - его использование за пределами методов класса не предполагается.
Шаг 1.8. Декодировать текст
Наконец, применим полную логику перехода от закодированного текста к декодированному.
Реализуйте метод
lab_3_generate_by_ngrams.main.TextProcessor.decode()
,
который преобразует закодированный текст - кортеж с
идентификаторами - в текст в виде строки.
Метод обязательно должен вызывать методы
lab_3_generate_by_ngrams.main.TextProcessor._decode()
и
lab_3_generate_by_ngrams.main.TextProcessor._postprocess_decoded_text()
.
Например, для закодированного корпуса
(1, 2, 3, 0, 4, 1, 0, 2, 5, 6, 6, 7, 0, 2, 3, 0, 4, 1, 0, 2, 5, 6, 6, 7, 0)
у вас должен получиться следующий текст: She is happy he is happy.
.
Note
Если на вход подаются некорректные значения (закодированный
текст не является кортежем или отсутствует), возвращается None
.
Если какой-либо из использованных методов возвращает None
,
необходимо так же вернуть None
.
Данный метод является публичным и инкапсулирует в себе внутреннюю логику обработки текста.
Шаг 1.9. Получить специальный токен
В данном классе атрибут self._end_of_word_token
является защищенным:
мы не хотим допустить его изменения пользователем.
Однако узнать, какой именно токен используется для разделения слов, все-таки
иногда необходимо.
Для этого реализуем метод
lab_3_generate_by_ngrams.main.TextProcessor.get_end_of_word_token()
,
который возвращает значение внутреннего атрибута self._end_of_word_token
.
Шаг 1.10. Продемонстрировать результаты в start.py
Important
Выполнение Шага 1 соответствует 4 баллам.
Продемонстрируйте результат кодирования и декодирования
в функции main()
модуля start.py
, используя текст на
английском языке (переменная text
).
В качестве специального токена конца слова используйте строку '_'
.
Шаг 2. Создать структуру для хранения и обработки n-грамм
Теперь у Вас есть все, чтобы создать свою простейшую языковую модель на основе n-грамм для решения задачи генерации текста.
Класс lab_3_generate_by_ngrams.main.NGramLanguageModel
позволяет собрать n-граммы из заданного закодированного текста и
сгенерировать следующую букву последовательности.
Допустим, у нас есть закодированный текст, который выглядит
следующим образом: text = (1, 2, 3, 0, 4, 1, 0, 2, 5, 6, 6, 7, 0,
2, 3, 0, 4, 1, 0, 2, 5, 6, 6, 7, 0)
.
Если нам необходимо заполнить NGramLanguageModel
с N=2
,
то мы получим следующие биграммы: ((1, 2), (2, 3), (3, 0), (0, 4),
(4, 1), (1, 0), (0, 2), (2, 5), (5, 6), (6, 6), (6, 7), (7, 0), (0, 2),
(2, 3), (3, 0), (0, 4), (4, 1), (1, 0), (0, 2), (2, 5), (5, 6), (6, 6),
(6, 7), (7, 0))
.
Если N=3
, то мы получим следующие триграммы:
((1, 2, 3), (2, 3, 0), (3, 0, 4), (0, 4, 1), (4, 1, 0), (1, 0, 2),
(0, 2, 5), (2, 5, 6), (5, 6, 6), (6, 6, 7), (6, 7, 0), (7, 0, 2),
(0, 2, 3), (2, 3, 0), (3, 0, 4), (0, 4, 1), (4, 1, 0), (1, 0, 2),
(0, 2, 5), (2, 5, 6), (5, 6, 6), (6, 6, 7), (6, 7, 0))
.
Шаг 2.1. Объявить сущность языковой модели
Перейдем к реализации абстракции, которая хранит в себе языковое представление в виде n-грамм и на их основе предсказывает следующий токен.
Создайте класс lab_3_generate_by_ngrams.main.NGramLanguageModel
.
Описание внутренних атрибутов:
self._encoded_corpus
- закодированный текст;self._n_gram_size
- размер n–грамм, который в данном случае должен принимать значения от 2 до 5;self._n_gram_frequencies
- частотный словарь n–грамм, в котором ключами выступают n-граммы, а значениями - вероятность появления последнего токена данной n-граммы в контексте, задаваемом n-граммой.
На момент инициализации атрибут self._n_gram_frequencies`
является пустым словарем,
его заполнение произойдет далее.
Шаг 2.2. Извлечь n-граммы из закодированного корпуса
Как уже упоминалось, каждый экземпляр класса NGramLanguageModel
содержит языковое
представление в виде n-грамм. Для этого нам необходимо извлечь из корпуса n-граммы
заданного размера.
Реализуйте метод
lab_3_generate_by_ngrams.main.NGramLanguageModel._extract_n_grams()
,
который извлекает из закодированного корпуса n-граммы, размер которых
указан в атрибуте self._n_gram_size
.
Например, для закодированного корпуса
(1, 2, 3, 0, 4, 1, 0, 2, 5, 6, 6, 7, 0, 2, 3, 0, 4, 1, 0, 2, 5, 6, 6, 7, 0)
при n_gram_size = 2
метод должен вернуть следующий кортеж:
((1, 2), (2, 3), (3, 0), (0, 4), (4, 1), (1, 0), (0, 2), (2, 5), (5, 6), (6, 6),
(6, 7), (7, 0), (0, 2), (2, 3), (3, 0), (0, 4), (4, 1), (1, 0), (0, 2), (2, 5),
(5, 6), (6, 6), (6, 7), (7, 0))
.
Note
Если на вход подаются некорректные значения (корпус не является
кортежем или кортеж пустой), возвращается None
.
Important
Данный метод является защищенным - его использование за пределами методов класса не предполагается.
Шаг 2.3. Создать частотный словарь n-грамм
Реализуйте метод
lab_3_generate_by_ngrams.main.NGramLanguageModel.build()
,
который заполняет атрибут self._n_gram_frequencies
, где ключом
является n-грамма, а значением - вероятность появления последнего токена данной
n-граммы в контексте. Метод обязательно должен вызывать
lab_3_generate_by_ngrams.main.NGramLanguageModel._extract_n_grams()
.
В данной работе вам необходимо научиться считать вероятность появления последнего токена с учетом некоторого контекста. Давайте посмотрим на общий вид вычисления данной вероятности:
Для понимания указанной формулы необходимо знать, что такое условная вероятность. Условная вероятность \({P(w_{n}|w_{n−N+1}...w_{n−1})}\) – вероятность наступления одного события (в данном случае события \(P(w_{n})\)) при условии, что другое событие (то есть событие \(P(w_{n−N+1}...w_{n−1})\)) уже произошло.
В данном случае:
n
- длина заданной последовательности;N
- размер n-грамм.
Предположим, что у нас есть последовательность (4, 3, 2, 1, 7)
и мы хотим узнать вероятность
того, что следующий токен (5)
. Тогда контекст для заданной последовательности при N=3
имеет
следующий вид: (1, 7)
. Необходимо найти \({P((5)|(1, 7))}\).
Как мы можем оценить данную вероятность?
Оценка максимального правдоподобия (MLE) - метод, который позволяет оценить вероятность. Мы получаем оценку для параметров модели n-грамм, нормализуя значения из корпуса, чтобы они находились в диапазоне от 0 до 1.
Таким образом, в общем виде формула расчёта вероятности выглядит следующим образом:
\(С(w_{n−1},w_{n})\) - количество вхождений n-граммы, то есть её абсолютная частота;
\(С(w_{n−1})\) - сколько раз в корпусе встречаются n-граммы, имеющие такое же начало, то есть такие же первые N-1 символов.
То есть, чтобы вычислить вероятность токена (5)
с учетом предыдущего
контекста (1, 7)
, необходимо вычислить количество триграмм (1, 7, 5)
и нормализовать
по сумме всех триграмм, которые также начинаются с контекста (1, 7)
.
Например, дан следующий набор триграмм:
n_grams = (
(1, 2, 3), (2, 3, 0), (3, 0, 4), (0, 4, 1), (4, 1, 0),
(1, 0, 2), (0, 2, 5), (2, 5, 6), (5, 6, 6), (6, 6, 7),
(6, 7, 0), (7, 0, 2), (0, 2, 3), (2, 3, 0), (3, 0, 4),
(0, 4, 1), (4, 1, 0), (1, 0, 2), (0, 2, 5), (2, 5, 6),
(5, 6, 6), (6, 6, 7), (6, 7, 0))
Тогда, для триграммы (0, 2, 5)
значение \(P(w_{1},w_{2})\) равно 2
,
так как данная n-грамма встретилась только один раз в кортеже со всеми n-граммами.
Значение \(P(w_{1})\) равно 3
, так как именно столько раз n-грамма
(0, 2)
, имеющая такое же начало, встретилась среди всех n-грамм,
содержащих первые N-1 символов.
Следовательно, для n-граммы (0, 2, 5)
частотный словарь будет заполнен
следующем образом:
frequencies = {
(0, 2, 5): 0.6666666666666666}
Например, для закодированного корпуса
(1, 2, 3, 0, 4, 1, 0, 2, 5, 6, 6, 7, 0, 2, 3, 0, 4, 1, 0, 2, 5, 6, 6, 7, 0)
при n_gram_size = 3
должен получиться частотный словарь вида:
frequencies = {
(1, 2, 3): 1.0,
(2, 3, 0): 1.0,
(3, 0, 4): 1.0,
(0, 4, 1): 1.0,
(4, 1, 0): 1.0,
(1, 0, 2): 1.0,
(0, 2, 5): 0.6666666666666666,
(2, 5, 6): 1.0,
(5, 6, 6): 1.0,
(6, 6, 7): 1.0,
(6, 7, 0): 1.0,
(7, 0, 2): 1.0,
(0, 2, 3): 0.3333333333333333}
Note
Если метод принимает на вход закодированный текст не в виде кортежа или
если кортеж пустой, а также, если некорректно извлекаются n-граммы,
то возвращается 1
, если происходит корректное заполнение частотного
словаря, метод возвращает 0
.
Продемонстрируйте результат работы данного метода в функции main()
модуля
start.py
.
Шаг 2.4. Получить размер n-грамм
В данном классе атрибут self._n_gram_size
является защищенным:
мы не хотим допустить его изменения пользователем.
Однако узнать, какой именно токен используется для разделения слов, все-таки
иногда необходимо.
Реализуйте метод
lab_3_generate_by_ngrams.main.NGramLanguageModel.get_n_gram_size()
,
который возвращает значение внутреннего атрибута self._n_gram_size
.
Шаг 2.5. Сгенерировать следующий токен
Для генерации текста Вам необходимо научиться по заданному контексту определять следующую букву в последовательности.
Реализуйте метод
lab_3_generate_by_ngrams.main.NGramLanguageModel.generate_next_token()
.
Данный метод принимает на вход последовательность закодированных токенов. Метод отсекает контекст, по которому необходимо определить вероятность каждого из потенциальных следующих токенов, то есть N-1 последних элементов последовательности. N в данном случае - размер n-грамм, хранимых моделью.
Например, для n-граммы (1, 2, 3, 0)
, контекстом является (2, 3, 0)
.
Ваша задача, используя частотный словарь и извлеченный контекст, выделить все n-граммы, начинающиеся с данного контекста. Последние элементы выделенных n-грамм и будут токенами, которые могут продолжить данную последовательность. Метод возвращает словарь, ключами которого являются буквы, а значениями - частоты n-грамм, из которых данная буква была выявлена по контексту.
Например, метод принимает на вход следующую последовательность: (7, 5, 6, 6)
.
Для данной последовательности при n_gram_size = 3
контекстом является
(6, 6)
.
Пусть мы имеем частотный словарь следующего вида:
frequencies = {
(1, 2, 3): 1.0,
(2, 3, 0): 1.0,
(3, 0, 4): 1.0,
(0, 4, 1): 1.0,
(4, 1, 0): 1.0,
(1, 0, 2): 1.0,
(0, 2, 5): 0.6666666666666666,
(2, 5, 6): 1.0,
(5, 6, 6): 1.0,
(6, 6, 7): 1.0,
(6, 7, 0): 1.0,
(7, 0, 2): 1.0,
(0, 2, 3): 0.3333333333333333}
Тогда, у нас есть только одна n-грамма, а именно (6, 6, 7)
, с частотой
1.0
, в которой присутствует контекст (6, 6)
.
Метод должен вернуть следующий словарь: {7: 1.0}
,
в котором ключ - буква, найденная по контексту (6, 6)
, а
значение - частота n-граммы (6, 6, 7)
.
В словаре таких пар ключ-значение может быть несколько, поэтому необходимо произвести двойную сортировку пар: по значению и по ключу в порядке убывания.
Note
Если введенная последовательность не является кортежем, кортеж пустой или
неправильная длина последовательности (длина последовательности меньше, чем
длина контекста), возвращается None
.
Шаг 3. Создать жадный генератор текста
Шаг 3.1. Объявить сущность генератора
Теперь мы полностью готовы к реализации генерации текста. Начнем с самого простого принципа генерации.
Жадный алгоритм - это алгоритм генерации, в котором следующим токеном всегда выбирается наиболее вероятный элемент, предполагая, что конечное решение также будет наиболее вероятный.
Для того, чтобы сгенерировать последовательности заданной длины,
создайте класс lab_3_generate_by_ngrams.main.GreedyTextGenerator
.
Описание внутренних атрибутов:
self._model
- экземпляр классаNGramLanguageModel
;self._text_processor
- экземпляр классаTextProcessor
.
Шаг 3.2. Сгенерировать последовательность
Реализуйте метод
lab_3_generate_by_ngrams.main.GreedyTextGenerator.run()
,
который генерирует последовательность указанной длины.
Прежде чем создать жадный алгоритм генерации, необходимо сделать несколько подготовительных преобразований:
Закодировать заданную последовательность (
prompt
). Для этого используйте экземпляр классаTextProcessor
, который хранится в соответствующем атрибуте;Получить из языковой модели контекст для генерации. Напоминаем, что длина контекста зависит от размера n-грамм.
Алгоритм создания жадного генератора:
Получить все буквы-кандидаты по контексту. Используйте уже реализованный метод класса
NGramLanguageModel
.Сгенерировать указанное количество букв, каждый раз выбирая кандидата с наибольшей частотой из словаря и добавляя его к последовательности.
Следует учесть, что:
В случае, если буквы-кандидаты не были найдены, то генерация прекращается, чтобы избежать процесса зацикливания.
С добавлением новой буквы к исходной последовательности, контекст изменяется, так как меняется последовательность.
Метод возвращает сгенерированный текст в виде строки.
Note
Если на вход подаются некорректные значения (длина последовательности
не типа int
, заданная последовательность не является
строкой или последовательность пустая), если вызываемые методы возвращают
значение None
, метод возвращает значение None
.
В данном методе необходимо использовать методы
lab_3_generate_by_ngrams.main.TextProcessor.encode()
и
lab_3_generate_by_ngrams.main.TextProcessor.decode()
,
а также методы
lab_3_generate_by_ngrams.main.NGramLanguageModel.get_n_gram_size()
и
lab_3_generate_by_ngrams.main.NGramLanguageModel.generate_next_token()
.
Например, при следующих значениях:
seq_len = 6
prompt = 'She is'
метод возвращает следующий результат 'She is happy.'
.
Шаг 3.3. Продемонстрировать результаты в start.py
Important
Выполнение Шагов 2 и 3 соответствует 6 баллам.
Продемонстрируйте результат работы жадного алгоритма генерации текста
в функции main()
модуля start.py
.
Попробуйте в качестве затравки использовать ‘Vernon’.
Пусть размер n-грамм будет равен 7, а длина последовательности - 51 .
Шаг 4. Создание алгоритма Beam Search
На данном этапе мы предлагаем Вам реализовать генератор, используя алгоритм лучевого поиска.
Принципиальное отличие данного алгоритма от жадного заключается в том, что для
жадного алгоритма оптимальная последовательность может быть достигнута, даже если
на каком-то шаге алгоритм выберет не самое вероятное продолжение, в то время, как
лучевой поиск строит дерево поиска. На каждом уровне дерева алгоритм генерирует
все возможные варианты продолжения последовательности, сортируя их в порядке
убывания вероятности. Для того чтобы не происходило сильное ветвление дерева,
на каждом уровне выбирается заранее определенное количество наиболее вероятных
вариантов, которое называется шириной луча. Далее разворачиваются только эти
состояния. При ширине луча 1
лучевой поиск идентичен жадному поиску.
Вы спросите, почему мы не генерируем все возможные варианты продолжения последовательности, чтобы затем выбирать наилучшую? Ответ: подобный алгоритм является очень дорогим с точки зрения вычислительных ресурсов. Именно поэтому алгоритм Beam Search с шириной луча - компромисс.
Рассмотрим работу алгоритма лучевого поиска на примере. Допустим, нам дана следующая закодированная последовательность, которую необходимо продолжить:
encoded_prompt = (1, 2, 3, 0, 4, 1)
частотный словарь n-грамм при n_gram_size = 3
:
frequencies = {
(1, 2, 3): 1.0,
(2, 3, 0): 1.0,
(3, 0, 4): 1.0,
(0, 4, 1): 1.0,
(4, 1, 0): 1.0,
(1, 0, 2): 1.0,
(0, 2, 5): 0.6666666666666666,
(2, 5, 6): 1.0,
(5, 6, 6): 1.0,
(6, 6, 7): 1.0,
(6, 7, 0): 1.0,
(7, 0, 2): 1.0,
(0, 2, 3): 0.3333333333333333}
и значение ширины луча: beam width = 3
.
Сначала языковая модель выявляет контекст из закодированной последовательности, который в данном случае имеет следующий вид:
(4, 1)
.В соответствии с частотным словарем, есть только одна буква-кандидат (удовлетворяет заданному значению ширины луча) для продолжения контекста
0
с частотой встречаемости n–граммы0.08695652173913043
.Таким образом, у нас получается только один вариант продолжения последовательности (дерево ветвится только на одно состояние, что удовлетворяет заданному значению ширины луча) с вероятностью, которая в нашем случае считается, как разность вероятности исходной последовательности и логарифмированной частоты n–граммы:
{(1, 2, 3, 0, 4, 1, 0): 2.4423470353692043}
.
Important
Изначальная вероятность последовательности (в данном примере
последовательностью является (1, 2, 3, 0, 4, 1)
) принимает значение
0.0
.
Теперь нам снова необходимо получить контекст с помощью языковой модели из обновленной
закодированной последовательности, который в данном случае имеет следующий вид: (1, 0)
.
Далее шаги 2 и 3 повторяются до тех пор, пока не будет достигнута желаемая длина последовательности.
Important
Обратите внимание, что на шагах 2 и 3 количество рассматриваемых для дальнейшей генерации букв-кандидатов и вариантов продолжения последовательности не должно превышать значение ширины луча.
Шаг 4.1. Объявить сущность генератора
Теперь Ваша задача - написать алгоритм лучевого поиска.
Для этого создайте класс lab_3_generate_by_ngrams.main.BeamSearcher
.
Описание внутренних атрибутов класса:
self._beam_width
- ширина луча, заполняется значением аргументаbeam_width
;self._model
- экземпляр классаNGramLanguageModel
, заполняется значением аргументаlanguage_model
.
Шаг 4.2. Получить буквы-кандидаты для генерации
Реализуйте метод
lab_3_generate_by_ngrams.main.BeamSearcher.get_next_token()
,
который позволит Вам получать следующую букву для генерации и вероятность
появления этого токена в заданном контексте.
Данный метод обязательно должен вызывать метод
lab_3_generate_by_ngrams.main.NGramLanguageModel.generate_next_token()
.
Note
Помните, что количество возвращаемых букв-кандидатов не должно превышать значение ширины луча.
Например, если метод получает на вход следующую последовательность:
sequence = (1, 2, 3, 0, 4, 1)
контекстом для которой является кортеж (4, 1)
,
и частотный словарь, который хранит
следующие значения при n_gram_size = 3
:
frequencies = {
(1, 2, 3): 1.0,
(2, 3, 0): 1.0,
(3, 0, 4): 1.0,
(0, 4, 1): 1.0,
(4, 1, 0): 1.0,
(1, 0, 2): 1.0,
(0, 2, 5): 0.6666666666666666,
(2, 5, 6): 1.0,
(5, 6, 6): 1.0,
(6, 6, 7): 1.0,
(6, 7, 0): 1.0,
(7, 0, 2): 1.0,
(0, 2, 3): 0.3333333333333333}
то метод возвращает: [(0, 1.0)]
Note
Если аргументы имеют некорректный тип данных, на вход подается пустая
последовательность или метод generate_next_token
возвращает
None
, то данный метод возвращает None
. В случае, если словарь с буквами
для генерации пустой, то метод возвращает пустой список.
Шаг 4.3. Получить варианты итоговых последовательностей
Реализуйте метод
lab_3_generate_by_ngrams.main.BeamSearcher.continue_sequence()
,
который позволяет получить варианты продолжения последовательностей.
В данном методе необходимо заполнить существующий словарь итоговых последовательностей-кандидатов новыми последовательностями, которые состоят из текущей последовательности и нового токена.
Для каждой новой последовательности в словаре необходимо посчитать ее вероятность следующим образом: разность вероятности текущей последовательности (то есть последовательности, которую мы расширяем новой буквой) и логарифмированной частоты буквы-кандидата.
Почему нам необходимо логарифмировать частоту встречаемости буквы?
В данном случае нам необходимо получить вероятность события, когда текущая
последовательность, которую мы хотим продолжить, встречается с буквой-кандидатом.
Данная вероятность вычисляется как произведение вероятностей текущей последовательности
и буквы. Но так как вероятность текущей последовательности и вероятность
буквы изменяются в диапазоне от 0
до 1
, то при перемножении вероятности n–грамм
становятся настолько маленькими, что все обращаются в 0
.
В таком случае возникает проблема максимального правдоподобия.
Чтобы избежать побочного эффекта от данной ситуации, необходимо применить логарифмирование.
Логарифм преобразует результат из произведения в сумму,
что значительно упрощает дальнейший анализ.
Результаты не являются численно стабильными: они имеют тенденцию либо быстро сходиться к
нулю, либо к бесконечности, в зависимости от того,
принимает ли вероятность значение меньше или больше 1
.
Более того, логарифм инверсирует порядок, то есть (log(0.2) > log(0.8))
,
поэтому при вычислении вероятности мы берем -log
.
Следует учесть, что:
Словарь с итоговыми последовательностями-кандидатами изменяется внутри метода, являясь одновременно и входным, и выходным значением. Хорошим тоном является создание функции, которая не изменяет объекты за ее пределами, а так как словарь - изменяемый тип данных, манипуляции с ним отразятся на объекте за пределами функции. Подумайте, как можно избежать побочного эффекта в данном случае;
В словарях с последовательностями-кандидатами необходимо удалять элемент с текущей последовательностью (то есть с последовательностью до расширения), так как мы уже расширили её и вероятность её расширенных версий будет выше.
Метод возвращает словарь из новых последовательностей-кандидатов и вероятностью встречаемости.
Например, метод принимает на вход следующие значения аргументов:
sequence = (1, 2, 3, 0, 4, 1, 0, 2)
next_tokens = [(5, 0.6666666666666666), (3, 0.3333333333333333)]
sequence_candidates = {(1, 2, 3, 0, 4, 1, 0, 2): 0.0}
и выводит следующий словарь с последовательностями-кандидатами:
sequence_candidates = {
(1, 2, 3, 0, 4, 1, 0, 2, 5): 0.40546510810816444,
(1, 2, 3, 0, 4, 1, 0, 2, 3): 1.0986122886681098}
Note
Если аргументы имеют некорректный тип данных, на вход подаются пустые
аргументы, длина списков букв-кандидатов превышает значение ширины луча
или последовательность, которую мы хотим продолжить, отсутствует в
последовательностях-кандидатах, то метод возвращается None
.
Шаг 4.4. Удалить последовательности, которые не соответствуют значению ширины луча
Реализуйте метод
lab_3_generate_by_ngrams.main.BeamSearcher.prune_sequence_candidates()
,
который фильтрует недостаточно вероятные последовательности.
Important
Помните, что количество итоговых последовательностей не должно превышать значение ширины луча.
Метод принимает на вход последовательность кандидатов sequence_candidates
в виде словаря,
ключом которого является последовательность, а значением - вероятность встречаемости данной
последовательности.
Note
В случае, если подаются некорректные значения (последовательность не является словарем или последовательность пустая), возвращаемое значение None.
Если значения корректные, то необходимо:
Произвести двойную сортировку последовательностей-кандидатов следующим образом: по значению и по ключу в порядке убывания.
Удалить последовательности, которые не входят в топ-n наиболее вероятных, где n ширина луча.
Метод возвращает словарь, ключами которого являются последовательности, которые входят в топ-N наиболее вероятных, а значениями - их вероятность.
Important
В силу применения логарифма, наиболее вероятными последовательностями являются те, у которых соответствующее значение ниже.
Количество топ-N последовательностей определяется параметром beam_width.
Например, на вход подается следующая последовательность кандидатов:
sequence_candidates = {
(1, 2, 3, 0, 4, 1, 0, 2, 5, 6, 6, 7, 0, 2, 5): 0.8109302162163289,
(1, 2, 3, 0, 4, 1, 0, 2, 5, 6, 6, 7, 0, 2, 3): 1.5040773967762742,
(1, 2, 3, 0, 4, 1, 0, 2, 3, 0, 4, 1, 0, 2, 5): 1.5040773967762742,
(1, 2, 3, 0, 4, 1, 0, 2, 3, 0, 4, 1, 0, 2, 3): 2.1972245773362196}
после сортировки получаем следующий список последовательностей:
sorted_sequences = [
(1, 2, 3, 0, 4, 1, 0, 2, 3, 0, 4, 1, 0, 2, 3),
(1, 2, 3, 0, 4, 1, 0, 2, 5, 6, 6, 7, 0, 2, 3),
(1, 2, 3, 0, 4, 1, 0, 2, 3, 0, 4, 1, 0, 2, 5),
(1, 2, 3, 0, 4, 1, 0, 2, 5, 6, 6, 7, 0, 2, 5)]
кандидаты на удаление при
beam_width = 3
:
sequences_to_remove = [(1, 2, 3, 0, 4, 1, 0, 2, 3, 0, 4, 1, 0, 2, 3)]
В результате метод выведет следующий словарь:
result = {
(1, 2, 3, 0, 4, 1, 0, 2, 5, 6, 6, 7, 0, 2, 5): 0.8109302162163289,
(1, 2, 3, 0, 4, 1, 0, 2, 5, 6, 6, 7, 0, 2, 3): 1.5040773967762742,
(1, 2, 3, 0, 4, 1, 0, 2, 3, 0, 4, 1, 0, 2, 5): 1.5040773967762742}
Шаг 5. Создать генератор текста на основе алгоритма Beam Search
Шаг 5.1. Объявить сущность генератора
Теперь у Вас есть все, чтобы создать генератор.
Для этого создайте класс
lab_3_generate_by_ngrams.main.BeamSearchTextGenerator
.
Описание внутренних атрибутов класса:
self._text_processor
- экземпляр классаTextProcessor
;self._beam_width
- ширина луча;self.beam_searchers
- экземпляр классаBeamSearcher
. В качестве аргументов экземпляр класса принимает значение атрибутаself._beam_width
и языковую модель.
Important
Экземпляры инициализируются в __init__
.
Шаг 5.2. Получить следующий токен
Ваша задача - создать генератор на основе алгоритма Beam Search.
Для того чтобы получить следующую букву для генерации последовательности,
необходимо вызвать метод для нулевого экземпляра класса BeamSearcher
,
который хранится в атрибуте self.beam_searchers
данного класса.
Реализуйте метод
lab_3_generate_by_ngrams.main.BeamSearchTextGenerator._get_next_token()
.
Он обязательно должен использовать метод
lab_3_generate_by_ngrams.main.BeamSearcher.get_next_token()
.
Note
Если аргумент имеет некорректный тип данных, то есть не
является кортежем, или кортеж пустой, то метод возвращается None
.
Если используемые методы возвращают None
, то метод также возвращает
None
.
Шаг 5.3. Сгенерировать последовательность
Реализуйте метод
lab_3_generate_by_ngrams.main.BeamSearchTextGenerator.run()
,
который принимает на вход количество букв для генерации
и начало последовательности в строковом виде. Данный метод позволяет получить
готовый сгенерированный текст.
Создание генератора:
Создайте словарь с последовательностями-кандидатами, ключом которого изначально является закодированное начало последовательности, а значением - частота закодированной последовательности, принимающая значение
0.0
.Получите топ-n (где n - ширина луча) букв-кандидатов для продолжения текущей последовательности.
Получите все вариантов последовательностей для дальнейшей генерации. Используйте для реализации данного пункта экземпляр класса
BeamSearcher
.Фильтрация последовательностей, то есть удаление всех тех последовательностей, которые не являются топ-n (где n - ширина луча). Теперь это новые последовательности-кандидаты. Используйте для реализации данного пункта нулевой экземпляр класса
BeamSearcher
.Повторяйте шаги 3-5 до тех пор, пока не будет достигнута желаемая длина сгенерированной последовательности.
Выберите наилучшую из всех последовательностей. Помните, что максимальной вероятности соответствует минимальное значение.
Декодируйте наилучшую последовательность в текст.
В данном методе необходимо использовать методы
lab_3_generate_by_ngrams.main.TextProcessor.encode()
и
lab_3_generate_by_ngrams.main.TextProcessor.decode()
,
а также методы
lab_3_generate_by_ngrams.main.BeamSearcher.get_next_token()
,
lab_3_generate_by_ngrams.main.BeamSearcher.continue_sequence()
и
lab_3_generate_by_ngrams.main.BeamSearcher.prune_sequence_candidates()
.
Note
Если на вход подаются некорректные значения (количество букв для
генерации не является целым числом или значение является неположительным,
а также последовательность не является строкой или строка пустая),
то метод возвращается None
.
Если любой используемые методы возвращают None
, то метод также возвращает
None
.
Например, метод принимает на вход длину генерируемой последовательности равную 1
и
начало последовательности 'She i'
.
Мы имеем частотный словарь для триграмм и значение ширины луча:
frequencies = {
(1, 2, 3): 1.0,
(2, 3, 0): 1.0,
(3, 0, 4): 1.0,
(0, 4, 1): 1.0,
(4, 1, 0): 1.0,
(1, 0, 2): 1.0,
(0, 2, 5): 0.6666666666666666,
(2, 5, 6): 1.0,
(5, 6, 6): 1.0,
(6, 6, 7): 1.0,
(6, 7, 0): 1.0,
(7, 0, 2): 1.0,
(0, 2, 3): 0.3333333333333333}
beam_width = 3
Закодированная начальная последовательность имеет следующий вид: (1, 2, 3, 0, 4)
.
Следовательно, модель должна выявить следующий контекст: (0, 4)
.
Из частотного словаря получаем, что кандидат, чтобы продолжить последовательность, у
нас один: [(1, 1.0)]
.
В таком случае, текущим кандидатом является последовательность (1, 2, 3, 0, 4, 1)
.
Так как кандидат один, то нет кандидатов для удаления (в случае, если кандидатов было
четыре, то после их сортировки необходимо было оставить количество, заданное шириной луча).
Метод возвращает следующий результат 'She is.'
.
Шаг 5.4. Продемонстрировать результаты в start.py
Important
Выполнение Шагов 4 и 5 соответствует 8 баллам.
Продемонстрируйте результат работы алгоритма лучевого поиска
в функции main()
модуля start.py
.
Попробуйте в качестве затравки использовать 'Vernon'
.
Пусть размер n-грамм будет равен 7, а длина последовательности - 56.
Шаг 6. Заполнить хранилище новыми n-граммами
Прежде чем приступить к созданию генератора текста на основе алгоритма
BackOff, мы предлагаем Вам расширить хранилище с токенами и их идентификаторами
новыми значениями, которые хранятся в виде словаря в файле с расширением .json
.
Реализация данного шага позволит Вам улучшить результат генерации, так как
словарь составлен на большом текстовом материале.
Вам необходимо расширить класс TextProcessor
новым методом
lab_3_generate_by_ngrams.main.TextProcessor.fill_from_ngrams()
.
Получая на вход словарь из файла с языковым профилем английского языка, вам необходимо:
Отобрать только те n-граммы, длина которых равна
1
.Если n-грамма является буквой, то необходимо этой буквой пополнить хранилище
_storage
.
В данном методе необходимо использовать метод
lab_3_generate_by_ngrams.main.TextProcessor._put()
Note
Если на вход подается некорректное значение (аргумент
неправильного типа, то есть не словарь, или словарь пустой),
возвращается None
.
Например, если метод принимает на вход следующий словарь:
content = {
"freq": {
"h ap": 8629,
" ape": 3042,
"apex": 624}}
то в результате должно получиться хранилище, заполненное следующим образом:
storage = {'_': 0, 'h': 1, 'a': 2, 'p': 3, 'e': 4, 'x': 5}
Шаг 7. Установить значение атрибута self._n_gram_frequencies
Вам необходимо расширить класс NGramLanguageModel
новым методом
lab_3_generate_by_ngrams.main.NGramLanguageModel.set_n_grams()
.
Данный метод позволяет присвоить атрибуту self._n_gram_frequencies
получаемое на вход значение.
Note
Если на вход подается некорректное значение (аргумент
неправильного типа, то есть не словарь, или словарь пустой),
возвращается None
.
Шаг 8. Создать генератор текста на основе алгоритма BackOff
Теперь у Вас есть все, чтобы реализовать последний в рамках данной лабораторной алгоритм генерации текста, а именно алгоритм BackOff.
Принципиальное отличие данного алгоритма заключается в том, что мы получаем на вход не одну языковую модель, а сразу несколько и при генерации текста выбираем наиболее вероятную букву-кандидата, анализируя все возможные n-граммы.
Шаг 8.1. Объявить сущность по расширению языковой модели на основе n-грамм
Создайте класс lab_3_generate_by_ngrams.main.NGramLanguageModelReader
.
Описание внутренних атрибутов класса:
self._json_path
- путь к файлу с расширением.json
, который содержит языковой профиль английского языка (en_own.json
);self._eow_token
- специальный символ конца слова в формате строки;self._content
- содержимое файла с расширением.json
. Для чтения и сохранения файлов данного типа используйте стандартный модульjson
;self._text_processor
- экземпляр классаTextProcessor
.
На данном шаге Вам также необходимо заполнить хранилище _storage
, используя
содержимое файла с расширением .json
, которое хранится в атрибуте
self._content
.
Шаг 8.1.1 Получить экземпляр класса NGramLanguageModel
Реализуйте метод
lab_3_generate_by_ngrams.main.NGramLanguageModelReader.load()
,
который позволяет получить языковую модель для определенного размера n-грамм.
Во-первых, необходимо каждую n-грамму, полученную из файла .json
,
привести к определенному формату:
Заменить пробельные символы на специальный символ конца слова;
Выбрать только те токены в n-грамме, которые являются буквой или специальным символом;
Привести к нижнему регистру.
Если заданный размер n-грамм соотносится с длиной n-грамм из файла, то необходимо кодировать n-грамму из языкового профиля.
Создайте частотный словарь с n-граммами из файла и вероятностями появления последнего токена данной n-граммы в контексте, задаваемом n-граммой. Воспользуйтесь формулой условной вероятности:
\(P(w_{1},w_{2})\) - количество вхождений n-граммы, то есть её абсолютная частота;
\(P(w_{1})\) - сколько раз в корпусе встречаются n-граммы, имеющие такое же начало, то есть такие же первые N-1 символов.
Создайте экземпляр класса NGramLanguageModel
со следующими аргументами:
Закодированный текст, который принимает значение
None
;Размер n-грамм.
Установите значение защищенного атрибута self._n_gram_frequencies
класса
NGramLanguageModel
Note
Если на вход подается некорректное значение размера n-грамм (значение
не относится к типу int
, аргумент не содержит никакого значения или
размер находится за пределами от 2 до 5), то метод возвращает None
.
Метод обязательно должен вызывать методы
lab_3_generate_by_ngrams.main.TextProcessor.get_id()
и
lab_3_generate_by_ngrams.main.NGramLanguageModel.set_n_grams()
.
Шаг 8.1.2 Получить экземпляр класса TextProcessor
В данном классе атрибут self._text_processor
является защищенным:
мы не хотим допустить его изменения пользователем.
Однако узнать, какой именно токен используется для разделения слов, все-таки
иногда необходимо.
Для этого реализуем метод
lab_3_generate_by_ngrams.main.NGramLanguageModelReader.get_text_processor()
,
который возвращает значение внутреннего атрибута self._text_processor
.
Метод не принимает никаких аргументов.
Шаг 8.2. Объявить сущность по созданию BackOff генератора
Создайте класс lab_3_generate_by_ngrams.main.BackOffGenerator
.
Описание внутренних атрибутов класса:
self._language_models
- словарь, ключом которого является размер n–грамм, а значением - экземпляр классаNGramLanguageModel
. Экземпляр класса вы можете получить из спискаlanguage_models
;self._text_processor
- экземпляр классаTextProcessor
.
Шаг 8.2.1 Получить буквы-кандидаты для генерации
Реализуйте метод
lab_3_generate_by_ngrams.main.BackOffGenerator._get_next_token()
,
который возвращает словарь, ключами которого являются буквы-кандидаты,
а значениями - частоты n-грамм, из которых данная буква была выявлена по контексту.
В данном методе необходимо использовать методы
lab_3_generate_by_ngrams.main.NGramLanguageModel.get_n_gram_size()
и
lab_3_generate_by_ngrams.main.NGramLanguageModel.generate_next_token()
.
Вам необходимо:
Получить все возможные размеры n–грамм, используя внутренний атрибут данного класса, и отсортировать его в порядке убывания N–грамм.
Для каждой n–граммы получить экземпляр класса
NGramLanguageModel
.Получить контекст c помощью языковой модели для дальнейшей генерации буквы-кандидата.
Получить все буквы-кандидаты для генерации в виде словаря.
Перейти к n–грамме меньшего размера и повторить шаги 2-4.
Note
Если на вход подается некорректное значение аргумента (аргумента
не является кортежем или кортеж пустой), то метод возвращает None
.
Если список размеров N–грамм пустой или вызываемые методы возвращают None
,
то метод также возвращает None
Important
Данный метод является защищенным - его использование за пределами методов класса не предполагается.
Шаг 8.2.2 Генерация последовательности
Important
Выполнение Шагов 6-8 соответствует 10 баллам.
Реализуйте метод
lab_3_generate_by_ngrams.main.BackOffGenerator.run()
,
который принимает на вход длину последовательности для генерации
и начало последовательности в строковом виде.
В данном методе необходимо использовать методы
lab_3_generate_by_ngrams.main.TextProcessor.encode()
и
lab_3_generate_by_ngrams.main.TextProcessor.decode()
,
а также метод
lab_3_generate_by_ngrams.main.BackOffGenerator._get_next_token()
.
Перед тем как непосредственно приступить к генерации, необходимо закодировать заданную
последовательность (prompt
).
Вам следует:
Получить все буквы-кандидаты для генерации. Используйте уже реализованный метод класса
BackOffGenerator
.Сгенерировать указанное количество букв, каждый раз выбирая кандидата с наибольшей вероятностью из словаря и добавляя его к последовательности.
Следует учесть, что в случае если буквы-кандидаты не были найдены, то генерация прекращается. Метод возвращает сгенерированный текст в виде строки.
Note
Если на вход подаются некорректные значения (длина последовательности
не типа int
, заданная последовательность не является
строкой или последовательность пустая), если вызываемые методы возвращают
значение None
, возвращается None
.