- Произвольный текст или языковой профиль на английском языке
- Необходимо построить модель генерации текста
В рамках данной лабораторной работы, как и в случае с третьей, вы будете работать с N-граммами.
Например, word = 'sunny'
, unigrams = ['s', 'u', 'n', 'n', 'y']
,
bigrams = ['su', 'un', 'nn', 'ny']
, trigrams = ['sun', 'unn', 'nny']
.
Таким образом N-грамма состоит из нескольких букв. Для каждой из букв в N-грамме мы можем говорить о ее соседях - некотором контексте данной буквы - других буквах. Так как можно посчитать, сколько раз та или иная буква встречается в определённой N-грамме - то есть как часто буква встречается в определённом контексте - можно говорить о вероятностном распределении буквы в N-граммах из текста.
Пример для модели с n = 3
, то есть с три-граммами:
Контекст: I am th
Задача: продолжить эту последовательность.
Важно: мы не бёрём весь текст целиком, а только последние
n-1
букв.
Представим, что у нас есть заранее вычисленное частотное распределение букв по N-граммам:
frequencies = {
('t', 'h', 'o'): 234,
('_', 't', 'h'): 345,
('_', 'a', 'm'): 1978,
('a', 'm', '_'): 1340,
('_', 'I', 'I'): 2034,
('t', 'h', 'e'): 20000
}
Алгоритм генерации текста:
- Из контекста берём последовательность из
n-1
- в нашем случае это последние две буквы:('t', 'h')
; - Находим все три-граммы, которые начинаются с
('t', 'h')
(у нас их две); - Смотрим, для какой из три-грамм частота больше:
F('t', 'h', 'e') > F('t', 'h', 'o')
- Берём последнее слово из три-граммы с наибольшей частотой появления и добавляем в выходной текст;
- В нашем случае берём
e
и добавляем к выходному тексту, получается следующее:I am the
.
- В нашем случае берём
- Повторяем шаги 1-4 при дальнейшей генерации.
Критерии остановки генерации текста могут быть различны - о них подробнее непосредственно в реализации.
Функция принимает на вход текст и возвращает кортеж токенов-букв.
В качестве разделителя используется пробел.
Если на вход подается некорректное значение, возвращается значение -1
.
К каждой слове также прибавляется специальный символ: _
.
Вставка этих символов обязательна для корректной работы алгоритма в дальнейшем.
Например, text = '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', '_') )
Интерфейс:
def tokenize_by_letters(text: str) -> tuple or int:
pass
Вам необходимо создать собственный класс LetterStorage
.
Однако, он должен быть наследником класса Storage
,
который уже реализован за вас и находится в модуле storage.py
.
Класс Storage
уже реализует необходимый для вас функционал
по хранению и кодированию произвольных элементов. То есть в
том числе его можно использовать для хранения и кодирования букв.
Сделав класс LetterStorage
наследником Storage
, нам останется
лишь адаптировать имеющийся функционал под нужды хранения именно букв.
Интерфейс:
class LetterStorage(Storage):
pass
Нам необходимо переопределить метод update
родительского
класса Storage
, поскольку мы хотим добавлять конкретно
буквы в класс LetterStorage
, а токенизированные буквы
хранятся в формате кортежа из кортежей, например:
( ('_', 's', 'h', 'e', '_'), ('_', 'i', 's', '_'), ('_', 'h', 'a', 'p', 'p', 'y', '_'), ('_', 'h', 'e', '_'), ('_', 'i', 's', '_'), ('_', 'h', 'a', 'p', 'p', 'y', '_') )
Итак, метод update
класса LetterStorage
принимает
на вход токенизированный корпус из элементов-букв.
Метод заполняет хранилище заданными буквами.
В случае возникновения ошибки, метод возвращает -1
.
Интерфейс:
class LetterStorage(Storage):
...
def update(self, elements: tuple) -> int:
pass
Вам необходимо знать, сколько букв присутствует в хранилище букв. Это может пригодиться в дальнейшем при работе с языковыми профилями, когда мы будем иметь, например, два профиля английского языка, из которых нужно выбрать один для дальнейшей работы. В таком случае, мы можем сравнить наполнение хранилищ у каждого из профилей и взять тот профиль, у которого хранилище содержит больше букв.
Метод не принимает на вход никаких пользовательских параметров.
Метод возвращает количество букв в хранилище на момент вызова метода.
Если хранилище пустое, то метод возвращает значение -1
.
Интерфейс:
class LetterStorage(Storage):
...
def get_letter_count(self) ->int:
pass
Функция кодирует корпус из букв,
используя заполненный экземпляр класса LetterStorage
,
и возвращает кортеж с закодированными буквами.
Кодирование заключается в замене букв на соответствующие идентификаторы.
Например, для заданного токенизированного корпуса:
(
('_', 's', 'h', 'e', '_'), ('_', 'i', 's', '_'),
('_', 'h', 'a', 'p', 'p', 'y', '_'), ('_', 'h', 'e', '_'),
('_', 'i', 's', '_'), ('_', 'h', 'a', 'p', 'p', 'y', '_')
)
Будет получен следующий закодированный корпус:
(
(1, 2, 3, 4, 1), (1, 5, 2, 1),
(1, 3, 6, 7, 7, 8, 1), (1, 3, 4, 1),
(1, 5, 2, 1), (1, 3, 6, 7, 7, 8, 1)
)
В случае некорректного ввода функция возвращает пустой кортеж.
Интерфейс:
def encode_corpus(storage: LetterStorage, corpus: tuple) -> tuple:
pass
Функция декодирует корпус из букв,
используя заполненный экземпляр класса LetterStorage
,
и возвращает строку с декодированными буквами.
Декодирование заключается в замене цифр на
соответствующие буквы из хранилища.
Например, для заданного токенизированного корпуса:
(
(1, 2, 3, 4, 1), (1, 5, 2, 1),
(1, 3, 6, 7, 7, 8, 1), (1, 3, 4, 1),
(1, 5, 2, 1), (1, 3, 6, 7, 7, 8, 1)
)
Будет получен следующий декодированный корпус:
(
('_', 's', 'h', 'e', '_'), ('_', 'i', 's', '_'),
('_', 'h', 'a', 'p', 'p', 'y', '_'), ('_', 'h', 'e', '_'),
('_', 'i', 's', '_'), ('_', 'h', 'a', 'p', 'p', 'y', '_')
)
В случае некорректного ввода функция возвращает пустой кортеж.
Интерфейс:
def decode_sentence(storage: LetterStorage, sentence: tuple) -> tuple:
pass
Продемонстрируйте обработку большого текста.
Задача: Напишите код в start.py
для демонстрации обработки текста.
- Токенизируйте текст, который хранится в файле
reference_text.txt
. - Заполните хранилище класса
LetterStorage
буквами. - Выведите количество букв в вашем хранилище.
- Выведите 5 букв с наименьшим идентификатором, а также 5 букв с наибольшим идентификатором.
Для дальнейшей работы вам необходимо
проимпортировать класс LanguageProfile
из модуля language_profile.py
.
Импортированный класс понадобится нам в дальнейшем для использования в модели генерации текста.
Идея генерации текста с помощью N-грамм состоит в следующем: последняя буква в N-грамме может быть найдена исходя из предшествующих ей букв.
В начале данного описания была рассмотрена идея выбора
следующей буквы на основе частотного распределения N-грамм.
Эта идея позволит нам создать модель генерации текста,
которая внутри себя будет содержать информацию о языковой
модели текста на основе языковых профилей LanguageProfile
.
Модели необходимо следующее:
- Собранный языковой профиль на определенном языке (класс для работы с языковыми профилями мы импортировали на предыдущем шаге).
Интерфейс:
class NGramTextGenerator:
def __init__(self, profile):
pass
Пример создания экземпляра класса:
n_gram_text_generator = NGramTextGenerator(en_profile)
print(n_gram_text_generator.profile) # will return an instance of LanguageProfile class
print(n_gram_text_generator._used_n_grams) # will return a list of n_grams that were already used for generation
en_profile
- это экземпляр классаLanguageProfile
для английского языка.self._used_n_grams
- это служебное поле, в которое помещаются уже использованные в генерации N-граммы для избежания повторов или зацикливаний генерации.
Для генерации буквы нам необходимо:
- Контекст (последние
n-1
букв); - Частотное распределение N-грамм в языке.
Самым простым способом для генерации следующей буквы на основе имеющихся данных будет следующий алгоритм:
- Найти все N-граммы, которые начинаются с заданного контекста;
- Выбрать из них ту, которая имеет наибольшую частоту встречаемости;
- Взять последнюю букву из найденной N-граммы.
Интерфейс:
class NGramTextGenerator:
...
def _generate_letter(self, context: tuple) -> int:
pass
Если приходят некорректные значения (или некорректный ввод context
,
или его неправильный размер), то возвращается значение -1
.
Если нет ни одной нграммы, которая бы соответствовала контексту, нужно найти самую частую нграмму среди всех нграмм, взять её последнюю букву и вернуть в виде предсказания.
Все выбранные для генерации N-граммы должны
быть помещены в список использованных N-грамм
self._used_n_grams
и не учитываться при
дальнейшей генерации для избежания зацикливания
алгоритма. Если в профиле не осталось никаких
N-грамм для генерации, то метод _generate_letter
очищает список использованных N-грамм self._used_n_grams
и пытается еще раз сгенерировать букву. Если
букву не удалось сгенерировать и в таком случае,
то метод возвращает значение -1
.
Примечание: Поскольку наш алгоритм берет наиболее частотные N-граммы, то ему будет несложно постоянно выбирать одни и те же частотные N-граммы при генерации как одной буквы, так и нескольких слов или целого предложения. Поэтому, во избежания выбора одних и тех же вариантов предлагается убирать использованные N-граммы. Генерация в таком случае становится более осмысленной и менее повторяющейся.
Пример:
Есть модель с размером N-грамм равным 2,
она уже обучена на тексте 'She is happy. He is happy.'
,
выделены N-граммы, вычислены их частоты.
Дан контекст: (3, )
. Пусть 3
соответствует букве h
(в вашей реализации может быть другой код).
Необходимо по этому контексту предсказать следующую букву.
Находим все возможные N-граммы, которые начинаются с токенов из контекста, и их частоты:
(3, 4): 2,
(3, 6): 2
Выбираем из этих N-грамм ту, которая имеет наибольшую частоту - в данном случае (3, 4)
(две N-граммы с одинаковым контекстом, у обоих одинаковые частоты - берем первую).
Токен e
с идентификатором 4
и будет предсказанной буквой.
В итоге получаем последовательность he
. Если вы сделали всё правильно,
то у вас также должен получиться такой набор букв.
Так как мы разбивали буквы и добавляли символ _
для индикации начала и конца слов,
у нас есть N-граммы, содержащие данный токен. Это позволяет нам выделить критерий,
по которому генерация текста может останавливаться:
- Если следующий сгенерированный токен =
_
, то происходит остановка генерации слова. - Если для заданного слова алгоритм не может найти конец, то генерацию предлагается
остановить по задаваемому параметру
word_max_length
, который по умолчанию равен 15.
Для получения символа _
вы можете обратиться к соответствующему
методу вашего хранилища:
generator = NGramTextGenerator(en_profile)
print(generator.profile.storage.get_special_token_id()) # вернет вам id символа _
Интерфейс:
class NGramTextGenerator:
...
def _generate_word(self, context: tuple, word_max_length=15) -> tuple:
pass
Если приходят некорректные значения, то возвращается пустой кортеж.
Если размер заданного контекста больше или равен параметру word_max_length
,
то возвращается заданный контекст со вставленным специальным символом '_'
в конце.
Пример:
Для текста 'She is happy. He is happy.'
и контекста (3,)
,
будет сгенерировано следующее слово:
(3, 4, 1)
, где 1
- идентификатор токена _
, обозначающий
начало и конец слова. В декодированном формате будет получено слово he
.
Теперь, когда есть способ генерации одного слова, можно переходить к предложению.
Алгоритм генерации предложения можно представить следующим образом:
- По заданному контексту происходит генерация слова
- Например для контекста
(1, 2)
было сгенерировано слово(1, 2, 5, 6, 1)
.
- Например для контекста
- Новый контекст образуется из последних
n-1
сгенерированных токенов (включая токен_
)- То есть новый контекст для второго слова в нашем случае будет
(6, 1)
.
- То есть новый контекст для второго слова в нашем случае будет
- Шаги 1-2 повторяются для нового контекста.
Критерием остановки генерации текста будет задаваемый параметр слов word_limit
.
Интерфейс:
class NGramTextGenerator:
...
def generate_sentence(self, context: tuple, word_limit: int) -> tuple:
pass
В качестве возвращаемого значения будет кортеж из кортежей - закодированных слов.
Если приходят некорректные значения, то возвращается пустой кортеж.
Пример:
Есть языковой профиль en_profile
, построенный на биграммах
для текста 'She is happy. He is happy.'
.
Дан контекст: (3,)
и необходимое количество слов: 2
.
Необходимо исходя из этой информации сгенерировать предложение.
Пусть идентификатор токена _
равен 1
.
Мы вызываем метод self._generate_word
столь раз,
сколько слов было задано, в нашем случае два раза.
Первое сгенерированное слово: (3, 4, 1)
,
второе сгенерированное слово: (1, 3, 6, 7, 7, 8, 1)
.
Сгенерированное предложения целиком:
((3, 4, 1), (1, 3, 6, 7, 7, 8, 1))
.
Обратите внимание, что в первом слове нет символа
начала предложения _
, поскольку контекст мы можем
задавать не обязательно с символа начала предложения.
Сгенерированное предложение в декодированном формате:
he happy
После того, как мы сгенерировали последовательность, нам нужно уметь переводить ее в обычный текст, который сможет прочитать пользователь.
Функция принимает на вход декодированную последовательность в виде кортежа кортежей.
Функция возвращает строковую последовательность.
Правила построения строковой последовательности:
- Два символа
_
подряд конвертируются в пробел. - Символ
_
в начале последовательности (если есть) преобразуется в пустую строку. - Символ
_
в конце последовательности преобразуется в завершающий знак.
. - Первая буква в последовательности становится заглавной.
Интерфейс:
def translate_sentence_to_plain_text(decoded_corpus: tuple) -> str:
pass
Например, декодированная последовательность:
(('h', 'e', '_'), ('_', 'h', 'a', 'p', 'p', 'y', '_'))
Сконвертируется в:
He happy.
Продемонстрируйте генерацию нескольких слов.
Задача: Напишите код в start.py
для демонстрации работы генератора текста.
- Создайте экземпляр класса
LanguageProfile
, основанный на обработанном тексте. - Сгенерируйте несколько предложений длиной по 5-10 слов.
- Декодируйте предложения и сконвертируйте их в строковый формат.
Попробуйте проинтерпретировать сгенерированный текст.
Вероятность последовательности букв рассчитывается следующим образом:
Это алгоритм максимального правдоподобия,
который позволяет расчитать вероятность появления буквы w
после последовательности из n-1
букв следующим образом:
- Количество раз, которое буква
w
встретилось после последовательности изn-1
/ количество раз, которое последовательностьn-1
встретилась в корпусе.
У нас уже есть модель для генерирования текста, в которой используется относительно простой алгоритм. Мы можем сделать другой алгоритм генерации, но оставить тот же интерфейс, что и у предыдущей модели. Здесь мы можем прибегнуть к наследованию.
Сейчас мы генерируем текст следующим образом:
generator = NGramTextGenerator(...)
sentence = generator.generate_sentence(...)
Мы хотим лишь немного улучшить генерацию за счет алгоритма максимального правдоподобия. Так как и в случае с алгоритмом максимального правдоподобия генерация основывается на N-граммах, а также сохраняется интерфейс программы для генерации, то логичнее всего сделать наследника и переопределить нужные методы.
Интерфейс:
class LikelihoodBasedTextGenerator(NGramTextGenerator):
pass
Тогда, интерфейс для генерации останется прежним, а реализация изменится:
generator = LikelihoodBasedTextGenerator(...)
sentence = generator.generate_sentence(...)
Способ создания экземпляра класса остаётся таким же, как и у NGramTextGenerator
.
Подумайте, нужно ли явно задавать конструктор?
Мы можем добавить метод для подсчёта вероятности появления буквы в данном контексте.
Вероятность появления буквы w
после последовательности из n-1
букв рассчитывается следующим образом:
- Количество раз, которое буква
w
встретилось после последовательности изn-1
/ количество раз, которое последовательностьn-1
букв встретилась в корпусе.
Интерфейс функции вычисления максимального правдоподобия:
class LikelihoodBasedTextGenerator(NGramTextGenerator):
...
def _calculate_maximum_likelihood(self, letter: int, context: tuple) -> float:
pass
Важно Аргумент
context
:n-1
до текущей буквыletter
.
Если контекста нет в хранилище N-грамм, то возвращается 0.0
.
Если приходят некорректные значения, то возвращается значение -1
.
Пример:
Есть модель с размером N-грамм равным 3, она уже обучена на некотором тексте, выделены N-граммы, вычислены их частоты.
Дан контекст: (1, 3)
и идентификатор 6
.
Необходимо вычислить вероятность появления данного слова в данном контексте.
Мы находим частотность триграммы (1, 3, 6)
-
сколько раз данная триграмма встречалась в тексте.
Находим количество триграмм, которые начинаются с
данного контекста - (1, 3)
, берем сумму частот таких триграмм.
n_gram_frequencies = { (1, 3, 6): 2, (1, 3, 4): 1 }
Пусть частотность триграммы (1, 3, 6)
равна 2
,
а сумма частот у всех триграмм, начинающихся с (1, 3)
равна 3
.
Тогда вероятность появления буквы с идентификатором 6
в данном контексте равняется отношению этих величин: 2/3
.
Если бы мы искали вероятность буквы с идентификатором 4
в том же контексте (1, 3)
, то мы бы получили значение 1/3
.
Теперь необходимо переопределить генерацию следующей буквы с использованием алгоритма максимального правдоподобия, описанного в предыдущем шаге.
С использованием алгоритма максимального правдоподобия генерация слова будет выглядеть следующим образом:
- Для каждого слова из имеющихся в словаре посчитать вероятность его
появления после
n-1
слов - контекста; - Выбрать слово с наибольшей вероятностью.
Интерфейс:
class LikelihoodBasedTextGenerator(NGramTextGenerator):
...
def _generate_letter(self, context: tuple) -> int:
pass
Если приходят некорректные значения
(или некорректный ввод context
, или его неправильный размер),
то возвращается значение -1
.
Если данного контекста нет в словаре N-грамм,
то возвращать необходимо наиболее частотную букву-униграмму
(то есть, вам также необходимо создавать и иметь в
вашем языковом профиле униграммы). Обратите внимание:
если самая частотная буква это символ '_'
и ваш контекст
также оканчивается на этот символ, то вам нужно взять следующую
частотную букву, иначе ваша генерация собьется.
Метод не использует поле self._used_n_grams
.
Пример:
Есть модель с размером N-грамм равным 3,
она уже обучена на некотором тексте,
выделены N-граммы, вычислены их частоты.
Дан контекст: (1, 3)
. Необходимо по этому
контексту сгенерировать слово.
Частотное распределение триграмм следующее:
{(1, 2, 3): 1, (2, 3, 4): 1,
(3, 4, 1): 2, (1, 5, 2): 2,
(5, 2, 1): 2, (1, 3, 6): 2,
(3, 6, 7): 2, (6, 7, 7): 2,
(7, 7, 8): 2, (7, 8, 1): 2,
(1, 3, 4): 1
}
Изначально, мы будем поочередно вызывать
вспомогательный метод для подсчета максимального
правдоподобия для контекста (1, 3)
и букв
с идентификаторами 6
и 4
. Максимальное
правдоподобие будет у триграммы (1, 3, 6)
- 0.6[6]
,
тогда как у триграммы только (1, 3, 4)
- 0.3[3]
.
Логика методов _generate_word
и generate_sentence
остаётся без изменений,
поэтому мы не будем их переопределять в классе LikelihoodBasedTextGenerator
.
Продемонстрируйте генерацию нескольких слов с использованием алгоритма максимального правдоподобия.
Задача: Напишите код в start.py
для демонстрации работы генератора текста.
- Создайте экземпляр класса
LanguageProfile
, основанный на обработанном тексте. - Сгенерируйте несколько предложений длиной по 5-10 слов используя генератор, работающий на алгоритме максимального правдоподобия.
- Сравните результаты генерации с обычным генератором, который вы сделали в шаге 7.
Может возникнуть ситуация, когда контекста и/или буквы нет в хранилище N-грамм или в словаре соответственно. Это ведёт к тому, что у нас нет вероятностного распределения и, соответственно, мы не можем предсказать следующую букву.
Одним из способов решения данной проблемы является использование алгоритма back-off в языковой модели N-грамм.
Суть заключается в следующем:
- Если не была найдена N-грамма размера
n
- то есть такого контекста для буквыw
- происходит переход к модели языка, которая была обучена с размером N-грамм =n-1
. И уже с помощью данных этой модели происходит предсказание; - Если же не было найдено N-грамм с размером
n-1
, то происходит переход к модели сn-2
- таким образом можно дойти доn = 1
, то есть до уни-грамм. - Чем большая размерность N-грамм участвует в генерации, тем точнее будет проходить генерация (например, генерация для триграмм будет точнее и более правдоподобнее, чем для униграмм). С другой стороны, взяв слишком большой контекст, например 15-граммы, мы редко сможем найти необходимый контекст для генерации.
- В случае, когда модель не может найти, например, 4-грамму для заданного контекста, при Back-off генерации, модель всегда может сузить этот контекст на одну букву и уже пытаться найти соответствующую 3-грамму (и так далее), и тогда вероятность найти соответствующий контекст повышается, но с понижением размерности N-грамм из-за снижения размерности контекста снижается и точность генерации.
- В итоге, при Back-off генерации, мы сможем всегда гарантированно получить варианты для генерации, поскольку у модели всегда есть униграммы, которые отвечают за конкретные буквы алфавита языка.
Так как и в этой модели мы будем предсказывать
букву-слово-предложение, что очень похоже на
задачи NGramTextGenerator
- только с несколько
другими условиями, представляется возможным оставить
тот же самый интерфейс и использовать наследование.
Создадим класс, который будет наследовать класс NGramTextGenerator
:
class BackOffGenerator(NGramTextGenerator):
pass
Для генерации буквы нам необходимо:
- Контекст (последние
n-1
букв); - Распределение N-грамм в языке;
- N-граммы разных размерностей.
Алгоритм генерации буквы очень похож на тот,
что используется в базовой модели NGramTextGenerator
,
с той модификацией, что если мы не находим такого контекста,
который нам был задан, мы переходим на модель с n-1
длиной N-грамм.
Таким образом можно перейти и на ступень n-2
и так далее.
Если в каждой из моделей не было найдено подходящего контекста, происходит переход к уни-граммам, которые должны быть собраны в языковом профиле. В качестве генерируемой буквы выбирается самая частотная.
Обратите внимание: метод также использует список self._used_n_grams
,
чтобы помещать туда уже использованные N-граммы.
Интерфейс:
class BackOffGenerator(NGramTextGenerator):
...
def _generate_letter(self, context: tuple) -> int:
pass
Если приходят некорректные значения, то возвращается значение -1
.
Пример:
Есть модель с размером N-грамм равным 4,
она уже обучена на тексте 'She is happy. He is happy.'
,
выделены N-граммы, вычислены их частоты.
Также были переданы обученные модели с размерами N-грамм
1, 2, 3 и 4. Дан контекст: (1, 6, 3)
, число слов: 1
.
Необходимо по этому контексту сгенерировать предложение
из одного слова соответственно.
- Модель пытается найти 4-грамму, которая начинается на
(1, 6, 3)
, но не может ее найти, так как ее просто нет. - Модель пробует сузить контекст и ищет триграмму, которая
начинается на
(6, 3)
- снова неудача. - Модель снова сужает контекст, теперь она ищет биграмму, которая
начинается на контекст
(3,)
- такие присутствуют:(3, 4): 2
, и(3, 6): 2
- модель берет первую биграмму(3, 4)
, так как она окажется первая после сортировки. - Модель получает последовательность
(1, 6, 3, 4)
. Модель снова ищет 4-грамму с контекстом уже равным(6, 3, 4)
. Такой 4-граммы снова нет. - Модель переходит на меньший контекст
(3, 4)
, ищет триграмму. Модель находит единственную частотную триграмму(3, 4, 1)
. - Модель завершает генерацию последовательности
(1, 6, 3, 4, 1)
, поскольку найден символ1
, обозначающий конец слова, а в параметрах мы указали генерацию одного слова.
Частотные распределения для текста из примера выше для вашей самопроверки:
{
(1, 2, 3, 4): 1, (2, 3, 4, 1): 1,
(1, 5, 2, 1): 2, (1, 3, 6, 7): 2,
(3, 6, 7, 7): 2, (6, 7, 7, 8): 2,
(7, 7, 8, 1): 2, (1, 3, 4, 1): 1
}
{
(1, 2, 3): 1, (2, 3, 4): 1, (3, 4, 1): 2,
(1, 5, 2): 2, (5, 2, 1): 2, (1, 3, 6): 2,
(3, 6, 7): 2, (6, 7, 7): 2, (7, 7, 8): 2,
(7, 8, 1): 2, (1, 3, 4): 1
}
{
(1, 2): 1, (2, 3): 1, (3, 4): 2, (4, 1): 2,
(1, 5): 2, (5, 2): 2, (2, 1): 2, (1, 3): 3,
(3, 6): 2, (6, 7): 2, (7, 7): 2, (7, 8): 2,
(8, 1): 2
}
{
(1,): 12, (2,): 3, (3,): 4, (4,): 2,
(5,): 2, (6,): 2, (7,): 4, (8,): 2
}
Идентификаторы в хранилище LetterStorage
:
{'_': 1, 's': 2, 'h': 3, 'e': 4, 'i': 5, 'a': 6, 'p': 7, 'y': 8}
Логика методов _generate_word
и generate_sentence
остаётся без изменений.
В предыдущей лабораторной работе вы работали с публичными языковыми профилями, доступными по ссылке: https://github.com/shuyo/language-detection. У вас есть возможность сгенерировать текст любого из языков, доступных в этих языковых профилях.
Для такой генерации нам понадобится адаптировать наш класс LanguageProfile
для более корректной работы с публичными языковыми профилями.
Поскольку у нас уже есть реализация LanguageProfile
, вам предлагается
использовать ее в качестве основы. Для этого мы поместили класс LanguageProfile
в модуль language_profile.py
. Импортируйте класс LanguageProfile
.
Так как нам необходимо расширить функционал нашего языкового профиля и добавить поддержку публичных языковых профилей, то вам предлагается прибегнуть к наследованию.
Интерфейс:
class PublicLanguageProfile(LanguageProfile):
pass
Теперь нам нужно уметь корректно открывать публичные языковые профили для задачи генерации текста.
При открытии языкового профиля для генерации текста необходимо учесть следующие особенности:
- Пробелы в N-граммах в публичных профилях должны заменяться
на символы
_
, которые в нашем формате обозначают начало или конец слова. - Заглавные буквы должны переводится в нижний регистр.
- Если при переводе в нижний регистр, у вас получились одинаковые N-граммы, то вам просто необходимо сложить частоты этих N-грамм.
- Важно: Следует помнить, что как и в лабораторной работе №3,
вам необходимо создавать экземпляры класса
NGramTrie
для каждого типа N-грамм, которые лежат у вас в публичном профиле. Сам классNGramTrie
лежит в модулеlanguage_profile.py
. А именно, после открытия публичного языкового профиля должны быть созданы экземпляры классаNgramTrie
для всех размеров N-грамм, присутствующих в публичном языковом профиле.
Если метод успешно выполнил открытие публичного профиля,
то возвращается значение 0
, в противном случае значение -1
.
Интерфейс:
class PublicLanguageProfile(LanguageProfile):
...
def open(self, file_name: str) -> int:
pass
Шаг 14. Продемонстрируйте работу трех генераторов на экзотическом языке (Выполнение шагов 12-14 соответствует 10 баллам)
Продемонстрируйте работу трех генераторов на примере непальского языка (файл лежит в вашей рабочей директории).
Задача: Откройте языковой профиль непальского языка ne
и проведите предобработку текста.
- Сгенерируйте по 1 предложению с использованием каждого из написанных вами генераторов.
- Декодируйте последовательности и переведите их в текст.
- Переведите полученные последовательности в гугл-переводчике.
Как вы помните, у базового генератора есть особенность - мы отмечаем уже использованные N-граммы и больше не используем их в генерации. Попробуйте запустить генерацию текста на базовом генераторе (скажем, 50 итераций), а затем сгенерируйте предложение на 51 итерации и выведете его. Проанализируйте, насколько сильно оно отличается от первых вариантов генерации. В какую сторону оно отличается?