Альтернативное понимание контекста с помощью статистической языковой модели

  • Tutorial
ALM

В интернете полно статей на тему основанных на N-граммах языковых моделей. При этом, готовых для работы библиотек довольно мало.

Есть KenLM, SriLM и IRSTLM. Они популярны и используются во многих крупных проектах. Но есть проблемы:

  1. Библиотеки старые, не развиваются.
  2. Плохо поддерживают русский язык.
  3. Работают только с чистым, специально подготовленным, текстом
  4. Плохо поддерживают UTF-8. Например, SriLM с флагом tolower ломает кодировку.

Из списка немного выделяется KenLM. Регулярно поддерживается и не имеет проблем с UTF-8, но она также требовательна к качеству текста.

Когда-то мне потребовалась библиотека для сборки языковой модели. После многих проб и ошибок пришёл к выводу, что подготовка датасета для обучения языковой модели — слишком сложный и долгий процесс. Особенно, если это русский язык! А ведь хотелось как-то всё автоматизировать.

В своих исследованиях отталкивался от библиотеки SriLM. Сразу отмечу, что это не заимствование кода и не fork SriLM. Весь код написан полностью с нуля.

Небольшой текстовый пример:


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

Отсутствие пробела между предложениями — довольно частая опечатка. Такую ошибку сложно найти в большом объеме данных, при этом она ломает токенизатор.

После обработки, в языковой модели появится такая N-грамма:


-0.3009452 трактор!С лязгом выкатился

Разумеется, существует множество других проблем, опечаток, спецсимволов, аббревиатур, различных математических формул… Всё это нужно правильно обрабатывать.

ANYKS LM (ALM)


Библиотека поддерживает только операционные системы Linux, MacOS X и FreeBSD. Windows у меня нет и поддержка не планируется.

Краткое описание функционала


  1. Поддержка UTF-8 без сторонних зависимостей.
  2. Поддержка форматов данных: Arpa, Vocab, Map Sequence, N-grams, Binary alm dictionary.
  3. Поддержка алгоритмов сглаживания: Kneser-Nay, Modified Kneser-Nay, Witten-Bell, Additive, Good-Turing, Absolute discounting.
  4. Нормализация входных корпусов, приведение слов к нижнему регистру, умная токенизация, поддержка чёрного и белого списков.
  5. Замена частот, замена N-грамм, добавление новых N-грамм с частотами, удаление N-грамм.
  6. Прунинг — сокращение числа N-грамм, которые не соответствуют указанным критериям качества.
  7. Смешивание языковых моделей: статическое, алгоритмом Байеса и линейно-логарифмическое.
  8. Чистка плохих N-грамм — удаление N-грамм, у которых обратная частота backoff выше основной частоты.
  9. Восстановление повреждённых N-грамм в языковой модели с последующим перерасчётом их backoff-частот.
  10. Поддержка специализированных токенов слов, таких как: числа, римские числа, диапазоны чисел, аббревиатуры, любые другие пользовательские токены с помощью скриптов написанных на языке Python3.
  11. Обработка «грязных текстов», извлечение правильного контекста из текстовых файлов.
  12. Переопределение 〈unk〉 токена в другое слово.
  13. Предобработка N-грамм перед добавлением в языковую модель с помощью скриптов на языке Python3.
  14. Бинарный контейнер языковой модели поддерживает сжатие, шифрование и установку копирайтов.
  15. Удобная визуализация хода процесса сборки языковой модели. Реализовано несколько видов визуализаций: текстовая, графическая в виде индикатора процесса, логирование в файлы или консоль.
  16. В отличие от остальных языковых моделей, ALM гарантированно собирает все N-граммы из текста независимо от их длины (кроме Modified Kneser-Nay). Также есть возможность принудительного учёта всех редко-встречаемых N-грамм, даже если они встретились всего 1 раз.

Из стандартных форматов языковых моделей поддерживается только формат ARPA. Честно, не вижу смысла поддерживать весь зоопарк всевозможных форматов.

Формат ARPA чувствителен к регистру символов и это тоже определённая проблема.

Иногда полезно знать только наличие в N-грамме конкретных данных. Например, нужно понимать наличие в N-грамме цифр, а их значение не столь важно.

Пример:


Спешите купить, распродажа в нашем магазине только 2 дня

В результате в языковую модель попадёт N-грамма:


-0.09521468 только 2 дня

Конкретное число, в данном случае, значения не имеет. Распродажа в магазине может идти и 1 и 3 и сколько угодно дней.

Для решения подобной задачи в ALM применяется классовая токенизация.

Поддерживаемые токены


Стандартные:

〈s〉 — Токен начала предложения
〈/s〉 — Токен конца предложения
〈unk〉 — Токен неизвестного слова

Нестандартные:

〈url〉 — Токен url адреса
〈num〉 — Токен чисел (арабское или римское)
〈date〉 — Токен даты (18.07.2004 | 07/18/2004)
〈time〉 — Токен времени (15:44:56)
〈abbr〉 — Токен аббревиатуры (1-й | 2-е | 20-я)
〈anum〉 — Токен псевдо-числа (T34 | 895-M-86 | 39km)
〈math〉 — Токен математической операции (+ | — | = | / | * | ^)
〈range〉 — Токен диапазона чисел (1-2 | 100-200 | 300-400)
〈aprox〉 — Токен приблизительного числа (~93 | ~95.86 | 10~20)
〈score〉 — Токен числового счёта (4:3 | 01:04)
〈dimen〉 — Токен габаритных размеров (200x300 | 1920x1080)
〈fract〉 — Токен числовой дроби (5/20 | 192/864)
〈punct〉 — Токен знаков пунктуации (. |… |, |! |? |: | ;)
〈specl〉 — Токен спецсимвола (~ | @ | # | № | % | & | $ | § | ±)
〈isolat〉 — Токен символов изоляции (" | ' | « | » | „ | “ | ` | ( | ) | [ | ] | { | })

Конечно, поддержку каждого из токенов можно отключить, если такие N-граммы нужны.

Если необходимо обрабатывать другие теги (например, нужно находить в тексте названия стран), ALM поддерживает подключение внешних скриптов на Python3.

Пример скрипта детектирования токенов:


# -*- coding: utf-8 -*-

def init():
    """
    Метод инициализации: выполняется только один раз при запуске приложения
    """

def run(token, word):
    """
    Метод запуска обработки: запускается при извлечении слова из текста
    @token название токена слова
    @word обрабатываемое слово
    """
    if token and (token == "<usa>"):
        if word and (word.lower() == "сша"): return "ok"
    elif token and (token == "<russia>"):
        if word and (word.lower() == "россия"): return "ok"
    return "no"

Такой скрипт добавляет к списку стандартных тегов еще два тега:〈usa〉 и 〈russia〉.

Кроме скрипта детектирования токенов есть поддержка скрипта предобработки обрабатываемых слов. Этот скрипт может изменить слово перед добавлением слова в языковую модель.

Пример скрипта предобработки слов:


# -*- coding: utf-8 -*-

def init():
    """
    Метод инициализации: выполняется только один раз при запуске приложения
    """

def run(word, context):
    """
    Метод запуска обработки: запускается при извлечении слова из текста
    @word обрабатываемое слово
    @context последовательность предыдущих слов в виде массива
    """
    return word

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

Текстовые форматы языковой модели, поддерживаемые ALM


ARPA:


\data\
ngram 1=52
ngram 2=68
ngram 3=15

\1-grams:
-1.807052	1-й	-0.30103
-1.807052	2	-0.30103
-1.807052	3~4	-0.30103
-2.332414	как	-0.394770
-3.185530	после	-0.311249
-3.055896	того	-0.441649
-1.150508	</s>
-99	<s>	-0.3309932
-2.112406	<unk>
-1.807052	T358	-0.30103
-1.807052	VII	-0.30103
-1.503878	Грека	-0.39794
-1.807052	Греку	-0.30103
-1.62953	Ехал	-0.30103
...

\2-grams:
-0.29431	1-й передал
-0.29431	2 ложки
-0.29431	3~4 дня
-0.8407791	<s> Ехал
-1.328447	после того	-0.477121
...

\3-grams:
-0.09521468	рак на руке
-0.166590	после того как
...

\end\

ARPA — стандартный текстовый формат языковой модели естественного языка, используемый в Sphinx/CMU и Kaldi.
Частота N-грамма Обратная частота
-1.328447 после того -0.477121

NGRAMS:


\data\
ad=1
cw=23832
unq=9390

ngram 1=9905
ngram 2=21907
ngram 3=306

\1-grams:
<s>	2022 | 1
<num>	117 | 1
<unk>	19 | 1
<abbr>	16 | 1
<range>	7 | 1
</s>	2022 | 1
А	244 | 1
а	244 | 1
б	11 | 1
в	762 | 1
выборах	112 | 1
обзорах	224 | 1
половозрелые	1 | 1
небесах	86 | 1
изобретали	978 | 1
яблочную	396 | 1
джинсах	108 | 1
классах	77 | 1
трассах	32 | 1
...

\2-grams:
<s> <num>	7 | 1
<s> <unk>	1 | 1
<s> а	84 | 1
<s> в	83 | 1
<s> и	57 | 1
и классные	82 | 1
и валютные	11 | 1
и несправедливости	24 | 1
снилось являлось	18 | 1
нашлось никого	31 | 1
соответственно вы	45 | 1
соответственно дома	97 | 1
соответственно наша	71 | 1
...

\3-grams:
<s> <num> </s>	3 | 1
<s> а в	6 | 1
<s> а я	4 | 1
<s> а на	2 | 1
<s> а то	3 | 1
можно и нужно	2 | 1
будет хорошо </s>	2 | 1
пейзажи за окном	2 | 1
статусы для одноклассников	2 | 1
только в одном	2 | 1
работа связана с	2 | 1
говоря про то	2 | 1
отбеливания зубов </s>	2 | 1
продолжение следует </s>	3 | 1
препараты от варикоза	2 | 1
...

\end\

Ngrams – нестандартный текстовый формат языковой модели, является модификацией формата ARPA.
N-грамма Встречаемость в корпусе Встречаемость в документах
только в одном 2 1

Описание:

  • ad - Количество документов в корпусе
  • cw - Количество слов во всех документах корпуса
  • unq - Количество уникальных собранных слов

VOCAB:


\data\
ad=1
cw=23832
unq=9390

\words:
33	а	244 | 1 | 0.010238 | 0.000000 | -3.581616
34	б	11 | 1 | 0.000462 | 0.000000 | -6.680889
35	в	762 | 1 | 0.031974 | 0.000000 | -2.442838
40	ж	12 | 1 | 0.000504 | 0.000000 | -6.593878
330344	был	47 | 1 | 0.001972 | 0.000000 | -5.228637
335190	вам	17 | 1 | 0.000713 | 0.000000 | -6.245571
335192	дам	1 | 1 | 0.000042 | 0.000000 | -9.078785
335202	нам	22 | 1 | 0.000923 | 0.000000 | -5.987742
335206	сам	7 | 1 | 0.000294 | 0.000000 | -7.132874
335207	там	29 | 1 | 0.001217 | 0.000000 | -5.711489
2282019644	похожесть	1 | 1 | 0.000042 | 0.000000 | -9.078785
2282345502	новый	10 | 1 | 0.000420 | 0.000000 | -6.776199
2282416889	белый	2 | 1 | 0.000084 | 0.000000 | -8.385637
3009239976	гражданский	1 | 1 | 0.000042 | 0.000000 | -9.078785
3009763109	банкиры	1 | 1 | 0.000042 | 0.000000 | -9.078785
3013240091	геныч	1 | 1 | 0.000042 | 0.000000 | -9.078785
3014009989	преступлениях	1 | 1 | 0.000042 | 0.000000 | -9.078785
3015727462	тысяч	2 | 1 | 0.000084 | 0.000000 | -8.385637
3025113549	позаботьтесь	1 | 1 | 0.000042 | 0.000000 | -9.078785
3049820849	комментарием	1 | 1 | 0.000042 | 0.000000 | -9.078785
3061388599	компьютерная	1 | 1 | 0.000042 | 0.000000 | -9.078785
3063804798	шаблонов	1 | 1 | 0.000042 | 0.000000 | -9.078785
3071212736	завидной	1 | 1 | 0.000042 | 0.000000 | -9.078785
3074971025	холодной	1 | 1 | 0.000042 | 0.000000 | -9.078785
3075044360	выходной	1 | 1 | 0.000042 | 0.000000 | -9.078785
3123271427	делаешь	1 | 1 | 0.000042 | 0.000000 | -9.078785
3123322362	читаешь	1 | 1 | 0.000042 | 0.000000 | -9.078785
3126399411	готовится	1 | 1 | 0.000042 | 0.000000 | -9.078785
…

Vocab – нестандартный текстовый формат словаря в языковой модели.
Идентификатор слова Слово Встречаемость в корпусе Встречаемость в документах tf tf-idf wltf
2282345502 новый 10 1 0.000420 0.000000 -6.776199

Описание:

  • oc - встречаемость в корпусе
  • dc - встречаемость в документах
  • tf - (term frequency — частота слова) — отношение числа вхождений некоторого слова к общему числу слов документа. Таким образом, оценивается важность слова в пределах отдельного документа, рассчитывается как: [tf = oc / cw]
  • idf - (inverse document frequency — обратная частота документа) — инверсия частоты, с которой некоторое слово встречается в документах коллекции, рассчитывается как: [idf = log(ad / dc)]
  • tf-idf - рассчитывается как: [tf-idf = tf * idf]
  • wltf - рейтинг слова, рассчитывается как: [wltf = 1 + log(tf * dc)]

MAP:


1:{2022,1,0}|42:{57,1,0}|279603:{2,1,0}
1:{2022,1,0}|42:{57,1,0}|320749:{2,1,0}
1:{2022,1,0}|42:{57,1,0}|351283:{2,1,0}
1:{2022,1,0}|42:{57,1,0}|379815:{3,1,0}
1:{2022,1,0}|42:{57,1,0}|26122748:{3,1,0}
1:{2022,1,0}|44:{6,1,0}
1:{2022,1,0}|48:{1,1,0}
1:{2022,1,0}|51:{11,1,0}|335967:{3,1,0}
1:{2022,1,0}|53:{14,1,0}|371327:{3,1,0}
1:{2022,1,0}|53:{14,1,0}|40260976:{7,1,0}
1:{2022,1,0}|65:{68,1,0}|34:{2,1,0}
1:{2022,1,0}|65:{68,1,0}|3277:{3,1,0}
1:{2022,1,0}|65:{68,1,0}|278003:{2,1,0}
1:{2022,1,0}|65:{68,1,0}|320749:{2,1,0}
1:{2022,1,0}|65:{68,1,0}|11353430797:{2,1,0}
1:{2022,1,0}|65:{68,1,0}|34270133320:{2,1,0}
1:{2022,1,0}|65:{68,1,0}|51652356484:{2,1,0}
1:{2022,1,0}|65:{68,1,0}|66967237546:{2,1,0}
1:{2022,1,0}|2842:{11,1,0}|42:{7,1,0}
…

Map — содержимое файла, имеет чисто техническое значение. Используется совместно с файлом vocab, можно объединять несколько языковых моделей, модифицировать, хранить, распространять и экспортировать в любые форматы (arpa, ngrams, binary alm).

Вспомогательные форматы текстовых файлов, которые поддерживает ALM


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

ALM решает эту проблему при помощи файла с внешне-похожими буквами.

p  р
c  с
o  о
t  т
k  к
e  е
a  а
h  н
x  х
b  в
m  м

Искомая буква Разделитель Буква на замену
t \t т

Если, при обучении языковой модели, передать файлы со списком доменов первого уровня и аббревиатур, то можно помочь ALM с более точным детектированием классовых тегов 〈url〉 и 〈abbr〉.

Файл списка аббревиатур:


г
р
США
ул
руб
рус
чел
…


Файл списка доменных зон:


ru
su
cc
net
com
org
info
…

Для более точного детектирования токена 〈url〉 следует добавить свои доменные зоны первого уровня (все доменные зоны из примера уже предустановлены).

Бинарный контейнер языковой модели ALM


Для сборки бинарного контейнера языковой модели необходимо составить файл в формате JSON с описанием своих параметров.

Параметры JSON:


{
	"aes": 128,
	"name": "Name dictionary",
	"author": "Name author",
	"lictype": "License type",
	"lictext": "License text",
	"contacts": "Contacts data",
	"password": "Password if needed",
	"copyright": "Copyright author"
}

Описание:

  • aes — Размер шифрования AES (128, 192, 256) бит
  • name — Название словаря
  • author — Автор словаря
  • lictype — Тип лицензии
  • lictext — Текст лицензии
  • contacts — Контактные данные автора
  • password — Пароль шифрования (если требуется), шифрование производится только при установки пароля
  • copyright — Копирайт владельца словаря

Все параметры являются опциональными кроме названия контейнера.

Примеры работы библиотеки ALM


Работа токенизатора


На входе токенизатор получает текст, а на выходе формирует JSON.

$ echo 'Hello World?' | ./alm -alphabet "abcdefghijklmnopqrstuvwxyzабвгдеёжзийклмнопрстуфхцчшщъыьэюя" -method tokens

terminal

Тест:

Hello World?

Результат:

[
	["Hello","World","?"]
]

Попробуем что-то сложнее…

$ echo 'неожиданно из подворотни в Олега ударил яркий прожектор патрульный трактор???с лязгом выкатился и остановился возле мальчика....' | ./alm -alphabet "abcdefghijklmnopqrstuvwxyzабвгдеёжзийклмнопрстуфхцчшщъыьэюя" -method tokens

terminal

Тест:

неожиданно из подворотни в Олега ударил яркий прожектор патрульный трактор???с лязгом выкатился и остановился возле мальчика....

Результат:

[
	[
		"Неожиданно",
		"из",
		"подворотни",
		"в",
		"Олега",
		"ударил",
		"яркий",
		"прожектор",
		"патрульный",
		"трактор",
		"?",
		"?",
		"?"
],[
		"С",
		"лязгом",
		"выкатился",
		"и",
		"остановился",
		"возле",
		"мальчика",
		".",
		".",
		".",
		"."
	]
]

Как видите, токенизатор корректно отработал и исправил основные ошибки.
Немного поменяем текст и посмотрим результат.

$ echo 'неожиданно из подворотни в Олега ударил яркий прожектор патрульный трактор...с лязгом выкатился и остановился возле мальчика.' | ./alm -alphabet "abcdefghijklmnopqrstuvwxyzабвгдеёжзийклмнопрстуфхцчшщъыьэюя" -method tokens

terminal

Тест:

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

Результат:

[
	[
		"Неожиданно",
		"из",
		"подворотни",
		"в",
		"Олега",
		"ударил",
		"яркий",
		"прожектор",
		"патрульный",
		"трактор",
		".",
		".",
		".",
		"с",
		"лязгом",
		"выкатился",
		"и",
		"остановился",
		"возле",
		"мальчика",
		"."
	]
]

Как видим, результат изменился. Теперь попробуем что-то другое.

$ echo 'Контрастный душ в течение 5–7 мин. также может стать простым средством восстановления в процессе тренировки при следующей методике применения: 1 мин. горячая вода ( +37–38°), затем 5–10 сек. – холодная вода ( +12–15°) и т.д.»| |()' | ./alm -alphabet "abcdefghijklmnopqrstuvwxyzабвгдеёжзийклмнопрстуфхцчшщъыьэюя" -method tokens

terminal

Тест:

Контрастный душ в течение 5–7 мин. также может стать простым средством восстановления в процессе тренировки при следующей методике применения: 1 мин. горячая вода ( +37–38°), затем 5–10 сек. – холодная вода ( +12–15°) и т.д.»| |()

Результат:

[
	[
		"Контрастный",
		"душ",
		"в",
		"течение",
		"5–7",
		"мин.",
		"также",
		"может",
		"стать",
		"простым",
		"средством",
		"восстановления",
		"в",
		"процессе",
		"тренировки",
		"при",
		"следующей",
		"методике",
		"применения",
		":",
		"1",
		"мин.",
		"горячая",
		"вода",
		"(",
		"+37–38°",
		")",
		",",
		"затем",
		"5–10",
		"сек.",
		"–",
		"холодная",
		"вода",
		"(",
		"+12–15°",
		")",
		"и",
		"т.д.",
		"»",
		"|",
		"|",
		"(",
		")"
	]
]

Объединим все обратно в текст


Сначала восстановим первый тест.

$ echo '[["Hello","World","?"]]' | ./alm -alphabet "abcdefghijklmnopqrstuvwxyzабвгдеёжзийклмнопрстуфхцчшщъыьэюя" -method tokens

terminal

Тест:

[["Hello","World","?"]]

Результат:

Hello World?

Восстановим теперь более сложный тест.

$ echo '[["Неожиданно","из","подворотни","в","Олега","ударил","яркий","прожектор","патрульный","трактор","?","?","?"],["С","лязгом","выкатился","и","остановился","возле","мальчика",".",".",".","."]]' | ./alm -alphabet "abcdefghijklmnopqrstuvwxyzабвгдеёжзийклмнопрстуфхцчшщъыьэюя" -method tokens

terminal

Тест:

[["Неожиданно","из","подворотни","в","Олега","ударил","яркий","прожектор","патрульный","трактор","?","?","?"],["С","лязгом","выкатился","и","остановился","возле","мальчика",".",".",".","."]]

Результат:

Неожиданно из подворотни в Олега ударил яркий прожектор патрульный трактор???
С лязгом выкатился и остановился возле мальчика….

Как видим, токенизатор смог восстановить сломанный изначально текст.

Продолжаем дальше.

$ echo '[["Неожиданно","из","подворотни","в","Олега","ударил","яркий","прожектор","патрульный","трактор",".",".",".","с","лязгом","выкатился","и","остановился","возле","мальчика","."]]' | ./alm -alphabet "abcdefghijklmnopqrstuvwxyzабвгдеёжзийклмнопрстуфхцчшщъыьэюя" -method tokens

terminal

Тест:

[["Неожиданно","из","подворотни","в","Олега","ударил","яркий","прожектор","патрульный","трактор",".",".",".","с","лязгом","выкатился","и","остановился","возле","мальчика","."]]

Результат:

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

Ну и наконец, проверим самый сложный вариант.

$ echo '[["Контрастный","душ","в","течение","5–7","мин.","также","может","стать","простым","средством","восстановления","в","процессе","тренировки","при","следующей","методике","применения",":","1","мин.","горячая","вода","(","+37–38°",")",",","затем","5–10","сек.","–","холодная","вода","(","+12–15°",")","и","т.д.","»","|","|","(",")"]]' | ./alm -alphabet "abcdefghijklmnopqrstuvwxyzабвгдеёжзийклмнопрстуфхцчшщъыьэюя" -method tokens

terminal

Тест:

[["Контрастный","душ","в","течение","5–7","мин.","также","может","стать","простым","средством","восстановления","в","процессе","тренировки","при","следующей","методике","применения",":","1","мин.","горячая","вода","(","+37–38°",")",",","затем","5–10","сек.","–","холодная","вода","(","+12–15°",")","и","т.д.","»","|","|","(",")"]]

Результат:

Контрастный душ в течение 5–7 мин. также может стать простым средством восстановления в процессе тренировки при следующей методике применения: 1 мин. горячая вода (+37–38°), затем 5–10 сек. – холодная вода (+12–15°) и т.д.» || ()

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

Обучение языковой модели


$ ./alm -alphabet "abcdefghijklmnopqrstuvwxyzабвгдеёжзийклмнопрстуфхцчшщъыьэюя" -size 3 -smoothing wittenbell -method train -debug 1 -w-arpa ./lm.arpa -w-map ./lm.map -w-vocab ./lm.vocab -w-ngram ./lm.ngrams -allow-unk -interpolate -corpus ./text.txt -threads 0 -train-segments

Опишу подробнее параметры сборки.

  • size — Размер длины N-граммы (размер установлен в 3 граммы)
  • smoothing — Алгоритм сглаживания (алгоритм выбран Witten-Bell)
  • method — Метод работы (метод указан обучение)
  • debug — Режим отладки (установлен индикатор статуса обучения)
  • w-arpa — После обучения будет произведён экспорт языковой модели в файл ARPA
  • w-map — После обучения будет произведён экспорт языковой модели в файл MAP
  • w-vocab — После обучения будет произведён экспорт словаря в файл VOCAB
  • w-ngram — После обучения будет произведён экспорт языковой модели в файл NGRAM
  • allow-unk — В языковой модели разрешено использовать тег 〈unk〉
  • interpolate — Необходимо при обучении выполнять интерполяцию
  • corpus — Адрес текстового корпуса для обучения. Это может быть и текстовый файл, и каталог содержащий текстовые файлы
  • threads — Использовать при обучении многопоточность (0 — для обучения будут отданы все доступные ядра процессора, > 0 количество ядер участвующих в обучении)
  • train-segments — Обучающий корпус будет равномерно сегментирован по всем ядрам

Более подробную информацию можно получить с помощью флага [-help].

terminal

Расчёт перплексии


$ echo "неожиданно из подворотни в Олега ударил яркий прожектор патрульный трактор???с лязгом выкатился и остановился возле мальчика...." | ./alm -alphabet "abcdefghijklmnopqrstuvwxyzабвгдеёжзийклмнопрстуфхцчшщъыьэюя" -method ppl -debug 2 -r-arpa ./lm.arpa -confidence -threads 0

terminal

Тест:

неожиданно из подворотни в Олега ударил яркий прожектор патрульный трактор???с лязгом выкатился и остановился возле мальчика….

Результат:

info: <s> Неожиданно из подворотни в олега ударил яркий прожектор патрульный трактор <punct> <punct> <punct> </s>

info: p( неожиданно | <s> ) = [2gram] 0.00209192 [ -2.67945500 ] / 0.99999999
info: p( из | неожиданно ...) = [3gram] 0.91439744 [ -0.03886500 ] / 1.00000035
info: p( подворотни | из ...) = [3gram] 0.86302624 [ -0.06397600 ] / 0.99999998
info: p( в | подворотни ...) = [3gram] 0.98003368 [ -0.00875900 ] / 1.00000088
info: p( олега | в ...) = [3gram] 0.85783547 [ -0.06659600 ] / 0.99999955
info: p( ударил | олега ...) = [3gram] 0.95238819 [ -0.02118600 ] / 0.99999897
info: p( яркий | ударил ...) = [3gram] 0.97959599 [ -0.00895300 ] / 1.00000090
info: p( прожектор | яркий ...) = [3gram] 0.97959599 [ -0.00895300 ] / 1.00000090
info: p( патрульный | прожектор ...) = [3gram] 0.97959599 [ -0.00895300 ] / 1.00000090
info: p( трактор | патрульный ...) = [3gram] 0.97959599 [ -0.00895300 ] / 1.00000090
info: p( <punct> | трактор ...) = [3gram] 0.78127873 [ -0.10719400 ] / 1.00000031
info: p( <punct> | <punct> ...) = [2gram] 0.29417110 [ -0.53140000 ] / 0.99999998
info: p( <punct> | <punct> ...) = [3gram] 0.51262054 [ -0.29020400 ] / 0.99999998
info: p( </s> | <punct> ...) = [3gram] 0.45569787 [ -0.34132300 ] / 0.99999998

info: 1 sentences, 13 words, 0 OOVs
info: 0 zeroprobs, logprob= -4.18477000 ppl= 1.99027067 ppl1= 2.09848266

info: <s> С лязгом выкатился и остановился возле мальчика <punct> <punct> <punct> <punct> </s>

info: p( с | <s> ) = [2gram] 0.00809597 [ -2.09173100 ] / 0.99999999
info: p( лязгом | с ...) = [3gram] 0.19675329 [ -0.70607800 ] / 0.99999972
info: p( выкатился | лязгом ...) = [3gram] 0.97959599 [ -0.00895300 ] / 1.00000090
info: p( и | выкатился ...) = [3gram] 0.98007204 [ -0.00874200 ] / 0.99999931
info: p( остановился | и ...) = [3gram] 0.85785325 [ -0.06658700 ] / 1.00000018
info: p( возле | остановился ...) = [3gram] 0.81482810 [ -0.08893400 ] / 1.00000027
info: p( мальчика | возле ...) = [3gram] 0.93507404 [ -0.02915400 ] / 1.00000058
info: p( <punct> | мальчика ...) = [3gram] 0.76391493 [ -0.11695500 ] / 0.99999971
info: p( <punct> | <punct> ...) = [2gram] 0.29417110 [ -0.53140000 ] / 0.99999998
info: p( <punct> | <punct> ...) = [3gram] 0.51262054 [ -0.29020400 ] / 0.99999998
info: p( <punct> | <punct> ...) = [3gram] 0.51262054 [ -0.29020400 ] / 0.99999998
info: p( </s> | <punct> ...) = [3gram] 0.45569787 [ -0.34132300 ] / 0.99999998

info: 1 sentences, 11 words, 0 OOVs
info: 0 zeroprobs, logprob= -4.57026500 ppl= 2.40356248 ppl1= 2.60302678

info: 2 sentences, 24 words, 0 OOVs
info: 0 zeroprobs, logprob= -8.75503500 ppl= 2.23975957 ppl1= 2.31629103

info: work time shifting: 0 seconds

Думаю, тут особо нечего комментировать, поэтому продолжим дальше.

Проверка существования контекста


$ echo "<s> Сегодня сыграл и в Олега ударил яркий прожектор патрульный трактор с корпоративным сектором </s>" | ./alm -alphabet "abcdefghijklmnopqrstuvwxyzабвгдеёжзийклмнопрстуфхцчшщъыьэюя" -method checktext -debug 1 -r-arpa ./lm.arpa -confidence

terminal

Тест:

<s> Сегодня сыграл и в Олега ударил яркий прожектор патрульный трактор с корпоративным сектором </s>

Результат:

YES | <s> Сегодня сыграл и в Олега ударил яркий прожектор патрульный трактор с корпоративным сектором </s>

Результат показывает, что проверяемый текст имеет верный контекст с точки зрения собранной языковой модели.

Флаг [-confidence] — означает, что языковая модель будет загружена так, как она была собрана, без перетокенизации.

Исправление регистров слов


$ echo "неожиданно из подворотни в Олега ударил яркий прожектор патрульный трактор???с лязгом выкатился и остановился возле мальчика...." | ./alm -alphabet "abcdefghijklmnopqrstuvwxyzабвгдеёжзийклмнопрстуфхцчшщъыьэюя" -method fixcase -debug 1 -r-arpa ./lm.arpa -confidence

terminal

Тест:

неожиданно из подворотни в Олега ударил яркий прожектор патрульный трактор???с лязгом выкатился и остановился возле мальчика....

Результат:

Неожиданно из подворотни в Олега ударил яркий прожектор патрульный трактор??? С лязгом выкатился и остановился возле мальчика....

Регистры в тексте восстанавливаются с учётом контекста языковой модели.

Вышеописанные библиотеки для работы со статистическими языковыми моделями чувствительны к регистру слов. Например, N-грамма «в Москве завтра дождь» не то же самое, что N-грамма «в москве завтра дождь», это совершенно разные N-граммы. Но как быть, если требуется учёт регистров слов и, в то же время, дублировать одни и те же N-граммы нерационально? ALM все N-граммы представляет в нижнем регистре. Тем самым исключается возможность дублирования N-грамм. ALM также ведёт свой рейтинг регистров слов в каждой N-грамме. При экспорте в текстовый формат языковой модели, регистры восстанавливаются в зависимости от их рейтинга.

Проверка количества N-грамм


$ echo "неожиданно из подворотни в Олега ударил яркий прожектор патрульный трактор???с лязгом выкатился и остановился возле мальчика...." | ./alm -alphabet "abcdefghijklmnopqrstuvwxyzабвгдеёжзийклмнопрстуфхцчшщъыьэюя" -method counts -debug 1 -r-arpa ./lm.arpa -confidence

terminal

Тест:

неожиданно из подворотни в Олега ударил яркий прожектор патрульный трактор???с лязгом выкатился и остановился возле мальчика....

Результат:

10 [3gram] | 
выполняется по размеру N-граммы в языковой модели, есть возможность выполнять проверку по биграммам и триграммам.

Проверка количества N-грамм выполняется по размеру N-граммы в языковой модели. Ещё есть возможность выполнить проверку по биграммам и триграммам.

Проверка по биграммам


$ echo "неожиданно из подворотни в Олега ударил яркий прожектор патрульный трактор???с лязгом выкатился и остановился возле мальчика...." | ./alm -alphabet "abcdefghijklmnopqrstuvwxyzабвгдеёжзийклмнопрстуфхцчшщъыьэюя" -method counts -ngrams bigram -debug 1 -r-arpa ./lm.arpa -confidence

terminal

Тест:

неожиданно из подворотни в Олега ударил яркий прожектор патрульный трактор???с лязгом выкатился и остановился возле мальчика....

Результат:

12 [2gram] | неожиданно из подворотни в Олега ударил яркий прожектор патрульный трактор???с лязгом выкатился и остановился возле мальчика….

Проверка по триграммам


$ echo "неожиданно из подворотни в Олега ударил яркий прожектор патрульный трактор???с лязгом выкатился и остановился возле мальчика...." | ./alm -alphabet "abcdefghijklmnopqrstuvwxyzабвгдеёжзийклмнопрстуфхцчшщъыьэюя" -method counts -ngrams trigram -debug 1 -r-arpa ./lm.arpa -confidence

terminal

Тест:

неожиданно из подворотни в Олега ударил яркий прожектор патрульный трактор???с лязгом выкатился и остановился возле мальчика....

Результат:

10 [3gram] | неожиданно из подворотни в Олега ударил яркий прожектор патрульный трактор???с лязгом выкатился и остановился возле мальчика….

Поиск N-грамм в тексте


$ echo "Особое место занимает чудотворная икона Лобзание Христа Иудою" | ./alm -alphabet "abcdefghijklmnopqrstuvwxyzабвгдеёжзийклмнопрстуфхцчшщъыьэюя" -method find -debug 1 -r-arpa ./lm.arpa -confidence

terminal

Тест:

Особое место занимает чудотворная икона Лобзание Христа Иудою

Результат:

<s> Особое
Особое место
место занимает
занимает чудотворная
чудотворная икона
икона Лобзание
Лобзание Христа
Христа Иудою
Иудою </s>

Список N-грамм, которые найдены в тексте. Пояснять тут особо нечего.

Переменные окружения


Все параметры можно передавать через переменные окружения. Переменные начинаются с префикса ALM_ и должны записываться в верхнем регистре. В остальном названия переменных соответствуют параметрам приложения.

Если одновременно указаны и параметры приложения, и переменные окружения, то приоритет отдаётся параметрам приложения.

$ export $ALM_SMOOTHING=wittenbell
$ export $ALM_W-ARPA=./lm.arpa

Таким образом, можно автоматизировать процесс сборки. Например, через BASH-скрипты.

Заключение


Понимаю, что есть более перспективные технологии вроде RnnLM или Bert. Но уверен, что статистические N-граммные модели еще долго будут актуальными.
На эту работу ушло много времени и сил. Занимался библиотекой в свободное от основной работы время, по ночам и выходным. Тестами код не покрывал, возможны ошибки и баги. Буду благодарен за тестирование. Также я открыт к предложениям по доработке и новому функционалу библиотеки. ALM распространяется под лицензией MIT, что позволяет использовать её практически без ограничений.

Надеюсь получить комментарии, критику, предложения.

Сайт проекта
Репозиторий проекта
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 24

    0
    Библиотеки старые, не развиваются.
    Плохо поддерживают русский язык.
    Работают только с чистым, специально подготовленным, текстом
    Плохо поддерживают UTF-8. Например, SriLM с флагом tolower ломает кодировку.
    Из списка немного выделяется KenLM. Регулярно поддерживается и не имеет проблем с UTF-8, но она также требовательна к качеству текста.

    Можно глупый вопрос в лоб.
    KenLM вроде работает с русским. Даже питоний враппер есть.
    Да и никто не мешает просто все эти "проблемы" решить пре-процессингом на питоне без C++. Даже будет мало кода и код легко поддерживать.


    Соответственно глупый вопрос — зачем еще одна библиотека?


    https://github.com/anyks/alm/#training-using-your-own-features

    А токены из нескольких букв?
    Или нужно делать "замену алфавита"?


    Requirements Python3

    Может я пропустил что-то, а интерфейса для вызова на питоне нет?


    https://github.com/anyks/alm/#build-on-linux-and-freebsd

    А вы планируете поддерживать пакеты в apt / pip?
    Это повысит аудиторию проекта раз в 10.

      0
      1. Про KenLM, я так и написал, что она поддерживает русский язык и utf-8. Но она же наверное и самая требовательная к качеству текста, например если размер N-граммы выше 3.

      2. Свои токены можно устанавливать любого размера без ограничения.

      3. Интерфейса для вызова на питоне пока нет, но это в планах. Те скрипты на питоне, их интерпретацией занимается ALM, запуская внутри себя.

      4. Поддержка pip будет сразу, как будет готов мост для питона.
        0
        Насчёт препроцессинга на питоне, здесь основная идея в токенизаторе, если он плохой, то и собственно обрабатывать нечего. Токенизатор даже в KenLM, работает исключительно по пробелам а значит с неподготовленным за ранее текстом он работать будет плохо. Именно для решения этой задачи и задумывалась библиотека.
        0
        Классно, что появляется много новых открытых проектов. Спасибо вам за открытый проект.
        Но… я бы не доверил делать качественную токенизацию какой-то плохо понимаемой мной библиотеке на С, когда вместо этого можно написать простой и понятный скрипт на питоне.

        Поэтому я бы вам предложил из одной библиотеки сделать две:
        одну для токенизации (на питоне), вторую — для быстрого моделирования языка, на С или С++.
        В качестве LM библиотеки сейчас вполне хорошо работает KenLM, особенно полезно его умение грузить 10 гб файлы с диска через mmap за долю секунды, потому что от языковой модели в первую очередь требуется скорость (как скорость работы, так и скорость загрузки!).
        Я уверен, что ваша языковая модель ещё долго не будет дотягивать до такого уровня скорости, а вот хорошая токенизация из коробки — отличная штука даже сама по себе. Да и будет возможность сравнить вашу токенизацию с тем, что предлагают пользователю готовые питоновские библиотеки токенизации, скажем, razdel или nltk.
        От токенизатора в первую очередь требуется управляемость, и для этого нет языка лучше питона.
        Ведь, скорее всего, поиск сокращений (abbr), под которыми вы в одном месте документации почему-то понимаете слова типа «1-я», а в другом месте — «МВД», «СНИЛС» и «см.», будет неправильно работать на словах типа «рис.», «см.» (или слишком агрессивно, или никак), далее, наверняка у вас кривое правило для ссылок (https://example.com/1,2,3#сноска-1 — это ссылка до какого момента, и как учитывается остальная часть?), и тому подобное. Вообще, это всё лучше всего доверить быстрому NER, пусть он их правильно размечает. Работа на правилах здесь — полумера, ценность которой вообще непонятна: ну, половину сокращений оно найдет правильно, половину не найдёт. И как потом такой LM пользоваться?
        Да и деление на предложения… скажем так, весьма нетривиальная штука. Всяко вы делаете это деление некачественно.
        Если же не задумываться о токенизации, то, опять же, просто разбиваем текст по пробелам и знакам препинания, забиваем на деление на предложения, и живём счастливо… Возможно, удаляем знаки препинания, но явно не объединяем точку и запятую в один класс! У них же совсем разные роли!
        Итак, чтобы не возиться с C, вы сейчас предлагаете медленное питоновское расширение для токенизации, чтобы делать её каждый раз при генерации LM — но токенизация, по сути, это отдельный этап, лучше её сделать один раз, а потом уже экспериментировать с настройками языковой модели. Да, про эту мысль разбиения на этапы мало написано (точнее, никак!) в документации к KenLM, но со временем все к этому приходят, и часть примеров по использованию KenLM именно так и говорит: сделайте отдельный скрипт для токенизации, а потом тренируйте kenlm на получившемся файле (и со стандартными настройками, ведь вы всё равно не понимаете, какой у этих настроек внутренний смысл, скажем, что такое backoff, и почему делить вероятности нужно только между не встречающимися словами, как у вас написано в тексте… и правда, почему? вероятности сочетающихся слов тоже как-то смещены, так что я вот тоже не понимаю научный смысл вашего smoothing...).

        А ещё я пока что вообще не понимаю, как у вас работает питоновское расширение. Где декларируется, что есть тег "", но нету тега ""? Почему оно проверяет на наличие каждого тега независимо, это же медленнее в 10 раз, если у нас 10 тегов… И как сказать, что слова типа "" нужно полностью пропускать, вместо слов типа нужно поставить тег , а остальные слова нужно заменять на их леммы в словаре… И как это всё потом можно будет использовать из клиентского кода на питоне… там же во многих случаях нужно будет применять совпадающий с серверной версией токенизатор…
        В общем, меня очень сильно смущает в вашем проекте то, что 90%, а может даже 100% всего проекта можно было бы и не делать, а взять готовые проверенные компоненты, и из них сделать всё то же самое…
          0

          Имхо, токенизация для одного домена — собирается за выходные из говна и палок зная домен.
          И аналогично допинывается по скорости.


          А для всех доменов — писать общую либу токенизации это благие намерения.

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

            $ echo 'На рис. 1 изображена ваза, см. наш каталог. Заходи к нам на сайт https://example.com/1,2,3#сноска-1 за более подробной информацией' | ./bin/alm -alphabet "abcdefghijklmnopqrstuvwxyzабвгдеёжзийклмнопрстуфхцчшщъыьэюя" -method tokens
            

            image
            Тест:
            На рис. 1 изображена ваза, см. наш каталог. Заходи к нам на сайт https://example.com/1,2,3#сноска-1 за более подробной информацией
            

            Результат:
            [
                [
                    "На",
                    "рис.",
                    "1",
                    "изображена",
                    "ваза",
                    ",",
                    "см.",
                    "наш",
                    "каталог",
                    "."
                ],[
                    "Заходи",
                    "к",
                    "нам",
                    "на",
                    "сайт",
                    "https://example.com/1,2,3#сноска-1",
                    "за",
                    "более",
                    "подробной",
                    "информацией"
                ]
            ]
            

            $ echo '[["На","рис.","1","изображена","ваза",",","см.","наш","каталог","."],["Заходи","к","нам","на","сайт","https://example.com/1,2,3#сноска-1","за","более","подробной","информацией"]]' | ./bin/alm -alphabet "abcdefghijklmnopqrstuvwxyzабвгдеёжзийклмнопрстуфхцчшщъыьэюя" -method tokens
            

            image
            [["На","рис.","1","изображена","ваза",",","см.","наш","каталог","."],["Заходи","к","нам","на","сайт","https://example.com/1,2,3#сноска-1","за","более","подробной","информацией"]]
            

            Результат:
            На рис. 1 изображена ваза, см. наш каталог.
            Заходи к нам на сайт https://example.com/1,2,3#сноска-1 за более подробной информацией
            


            P.S. ALM тоже загружает данные с диска через mmap.

              0
              К любым правилам легко подбирается контрпример.
              Если у вас правило разбиения предложений на ". [Большая буква]" идёт первым, то оно будет глючить на таких примерах:
              Люди любят г. Волгоград, имеющий историю, уходящую в века.
              Если вы его сдвинете позже выделения слов, то токенизация для «Люди любят рис. Нью-дели имеет историю, уходящую в века» сломается.
              А правило на ссылки будет глючить на часто встречающихся прилепленных буквах и скобочках к ссылке:
              Наш сайт выпивка.рф объявляет набор гостей. Ваш адрес: выпивка.рф/1,2 человека уже зашли, будь третьим! (Напоминаю: выпивка.рф/1)
              А уж сколько у вас будет проблем с прямой и косвенной речью… И мы ещё не касались токенизации слов с дефисом (и в целом проблемы разделения дефисов и тире).
              Ну и руками тестировать токенизацию бесперспективно, посмотрите хотя бы на github.com/natasha/razdel и их метрики.

              P.S. и было бы более продуктивно, если бы вы убрали скриншоты, и оставили только вход и выход программы в текстовом виде. Тяжело листать 3 страницы, в которых лишь 3 уникальных строки, а всё остальное — их повторы. Скажем, так:
              Вход:
              На рис. 1 изображена ваза, см. наш каталог. Заходи к нам на сайт https://example.com/1,2,3#сноска-1 за более подробной информацией
              Результат:
              [["На","рис.","1","изображена","ваза",",","см.","наш","каталог","."],["Заходи","к","нам","на","сайт","https://example.com/1,2,3#сноска-1","за","более","подробной","информацией"]]

                0
                Я понимаю о чем вы говорите и от части даже согласен, но выходит, что мы играем в угадайку тыкая пальцем в небо. Вы предполагаете, что будет ошибка и преподносите это как истину, при этом ничего не проверяя. Да, какие-то недочеты будут, угадать все, везде и вся невозможно. Если вернуться к контексту то KenLM не умеет даже этого. А ALM не ограничивает разработчика в использовании стороннего препроцессинга.
                  0
                  Проверил предложенные вами варианты, самому стало интересно и вот результат.

                  Первый вариант
                  echo 'Люди любят г. Волгоград, имеющий историю, уходящую в века.' | ./bin/alm -alphabet "abcdefghijklmnopqrstuvwxyzабвгдеёжзийклмнопрстуфхцчшщъыьэюя" -method tokens
                  

                  Тест:
                  Люди любят г. Волгоград, имеющий историю, уходящую в века.
                  

                  Результат:
                  [
                      [
                          "Люди",
                          "любят",
                          "г.",
                          "Волгоград",
                          ",",
                          "имеющий",
                          "историю",
                          ",",
                          "уходящую",
                          "в",
                          "века",
                          "."
                      ]
                  ]
                  

                  echo '[["Люди","любят","г.","Волгоград",",","имеющий","историю",",","уходящую","в","века","."]]' | ./bin/alm -alphabet "abcdefghijklmnopqrstuvwxyzабвгдеёжзийклмнопрстуфхцчшщъыьэюя" -method tokens
                  

                  Тест:
                  [["Люди","любят","г.","Волгоград",",","имеющий","историю",",","уходящую","в","века","."]]
                  

                  Результат:
                  Люди любят г. Волгоград, имеющий историю, уходящую в века.
                  

                  Второй вариант
                  echo 'Люди любят рис. Нью дели имеет историю, уходящую в века' | ./bin/alm -alphabet "abcdefghijklmnopqrstuvwxyzабвгдеёжзийклмнопрстуфхцчшщъыьэюя" -method tokens
                  

                  Тест:
                  Люди любят рис. Нью дели имеет историю, уходящую в века
                  

                  Результат:
                  [
                      [
                          "Люди",
                          "любят",
                          "рис",
                          "."
                      ],[
                          "Нью",
                          "дели",
                          "имеет",
                          "историю",
                          ",",
                          "уходящую",
                          "в",
                          "века"
                      ]
                  ]
                  

                  echo '[["Люди","любят","рис","."],["Нью","дели","имеет","историю",",","уходящую","в","века"]]' | ./bin/alm -alphabet "abcdefghijklmnopqrstuvwxyzабвгдеёжзийклмнопрстуфхцчшщъыьэюя" -method tokens
                  

                  Тест:
                  [["Люди","любят","рис","."],["Нью","дели","имеет","историю",",","уходящую","в","века"]]
                  

                  Результат:
                  Люди любят рис.
                  Нью дели имеет историю, уходящую в века
                  

                  Третий вариант
                  echo 'Наш сайт выпивка.рф объявляет набор гостей. Ваш адрес: выпивка.рф/1,2 человека уже зашли, будь третьим! (Напоминаю: выпивка.рф/1)' | ./bin/alm -alphabet "abcdefghijklmnopqrstuvwxyzабвгдеёжзийклмнопрстуфхцчшщъыьэюя" -method tokens
                  

                  Тест:
                  Наш сайт выпивка.рф объявляет набор гостей. Ваш адрес: выпивка.рф/1,2 человека уже зашли, будь третьим! (Напоминаю: выпивка.рф/1)
                  

                  Результат:
                  [
                      [
                          "Наш",
                          "сайт",
                          "выпивка.рф",
                          "объявляет",
                          "набор",
                          "гостей",
                          "."
                      ],[
                          "Ваш",
                          "адрес",
                          ":",
                          "выпивка.рф/1,2",
                          "человека",
                          "уже",
                          "зашли",
                          ",",
                          "будь",
                          "третьим",
                          "!"
                      ],[
                          "(",
                          "Напоминаю",
                          ":",
                          "выпивка.рф/1",
                          ")"
                      ]
                  ]
                  

                  echo '[["Наш","сайт","выпивка.рф","объявляет","набор","гостей","."],["Ваш","адрес",":","выпивка.рф/1,2","человека","уже","зашли",",","будь","третьим","!"],["(","Напоминаю",":","выпивка.рф/1",")"]]' | ./bin/alm -alphabet "abcdefghijklmnopqrstuvwxyzабвгдеёжзийклмнопрстуфхцчшщъыьэюя" -method tokens
                  

                  Тест:
                  [["Наш","сайт","выпивка.рф","объявляет","набор","гостей","."],["Ваш","адрес",":","выпивка.рф/1,2","человека","уже","зашли",",","будь","третьим","!"],["(","Напоминаю",":","выпивка.рф/1",")"]]
                  

                  Результат:
                  Наш сайт выпивка.рф объявляет набор гостей.
                  Ваш адрес: выпивка.рф/1,2 человека уже зашли, будь третьим!
                  (Напоминаю: выпивка.рф/1)
                  


                  Как видим, все тесты отработали правильно.
                    0
                    Пожалуйста, уберите лишний вывод, ну мешает же! Оставьте только две строчки: вход и выход. Детокенизация не нужна, картинки тоже.
                    Во-первых, одни и те же правила не смогут отличить «г.» от «рис.», разве что вы как-то отдельно обрабатываете эти слова или же у вас не система на правилах, а что-то иное.
                    Давайте добавим сразу все варианты, с запятой после слова, и без неё:
                    «Так. Я люблю г. Волгоград, я люблю рис. Волгоград, расположенный на реке, красив.
                    Я люблю гор. Волгоград, я люблю лес. Волгоград, расположенный на реке, красив.»
                    И да, в вашем коде без дебаггера хрен разберёшься, как я и думал:
                    github.com/anyks/alm/blob/master/src/tokenizer.cpp#L988
                    Даже несмотря на комментарии. Чего стоит только эвристика на слова длиной 4 символа.
                    И, кажется, «я.» будет считать аббревиатурой.
                    Да и классы символов захардкожены.
                    В общем, поддерживать кроме вас такой код никто не сможет, а уж тем более, добавить в токенайзер поддержку другого языка…
                    А так — правила для русского языка вроде бы вы неплохие сделали, можно будет попробовать их использовать.

                    Во-вторых, тут ошибка:
                    >[[«Наш»,«сайт»,«выпивка.рф»,«объявляет»,«набор»,«гостей»,"."],[«Ваш»,«адрес»,":",«выпивка.рф/1,2»,«человека»,«уже»,«зашли»,",",«будь»,«третьим»,"!"],["(",«Напоминаю»,":",«выпивка.рф/1»,")"]]
                    А должно быть «выпивка.рф/1»,«2», «человека».
                    К тексту из социальных сетей ваш токенайзер ещё рано подпускать, там с расположением пробелов проблемы, да и с большими буквами тоже. Туда — только нейросети.
                      0
                      Хочу заметить, что очевидно перед данным решением стояла не только задача токенизации, а более объемная задача статистической языковой модели. Жаль это осталось без рассмотрения.

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

                      Классы символов не захардкожены. В статье описано какие символы к каким токенам относятся.
                      Есть механизм переопределения и отключения токенов.

                      выпивка.рф/1,2 — отработал правильно, такой формат URI разрешен спецификацией.
                      В данном варианте не каждый человек поймёт, а мысли пока приложения читать не умеют.

                      >> А так — правила для русского языка вроде бы вы неплохие сделали, можно будет попробовать их использовать.

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

                      >> К тексту из социальных сетей ваш токенайзер ещё рано подпускать, там с расположением пробелов проблемы, да и с большими буквами тоже. Туда — только нейросети.

                      Чтобы обучить нейросеть, нужна разметка и эмбеддинг.
                      И повторюсь, статья не о токенизаторе а о языковой модели, токенизатор это небольшой функционал библиотеки.
                      Если для работы вы используете KenLM, то в ALM все те же самые подходы также будут работать.
                      Токенизатор не ограничивает функционал и никто вам, не мешает чистить текст перед обучением своими скриптами.

                      P.S. Я не думаю, что код в SriLM или KenLM проще или что есть много желающих его дописать.
                        0
                        >Хочу заметить, что очевидно перед данным решением стояла не только задача токенизации, а более объемная задача статистической языковой модели. Жаль это осталось без рассмотрения.
                        Надеюсь, теперь вы понимаете, как ваша разработка видится со стороны.
                        При продуктивизации пользователи всегда фокусируются на одной части продукта.
                        У вас же — два, а в перспективе даже три разных продукта, с разным позиционированием и разными пользователями.
                        ALM как LM — международный продукт для N-gram LM для замены KenLM,
                        русский токенайзер — российский продукт с более широким применением, чем LM — ведь хорошая токенизация нужна любому парсингу и пониманию текста.
                        А потом ещё будут и статистические модели, снова будет отдельный продукт: какие-то готовые модели, наверное, будете делать и распространять.

                        Если же вы будете совмещать эти вещи вместе, то кто-то будет на него смотреть как на «токенизатор с хардкодом для русского языка без API на питоне и зачем-то сбоку прикрученной LM», кто-то — как на «неплохую LM с зачем-то сбоку прикрученным токенизатором», и так далее.
                        Один продукт должен хорошо решать одну задачу, а не две, но средненько.
                        Продукты могут друг с другом взаимодействовать, но зачем им при этом быть одним неделимым продуктом? К тому же, в вашем случае, даже взаимодействия между ними никакого нет, просто один продукт использует результаты другого.
                          0
                          В python библиотека будет доступна уже в эти выходные. Можно использовать токенизатор отдельно от языковой модели. Я все же не согласен с тем, что токенизатор не нужен языковой модели, в моём понимании языковая модель сильно зависит от токенизатора. Во всяком случае я предусмотрел возможности его отключения и в ситуациях когда он не нужен, можно работать также как допустим с KenLM или SriLM.

                          На русский язык упор был сделан потому, что в нашем мире принято его в лучшем случае учитывать как данность нежели полноценно его поддерживать. Но хочу заметить, жестко ничего не вшито в библиотеку, что относится к русскому языку и мешает другим языкам. В равной степени поддерживаются все европейские языки.
                            0
                            А вот скажите, какой алфавит должен быть у английского? Какой у французского? Какой у русского? Одинаковый? Как вы будете обрабатывать документ со смесью языков? А датасет? У вас «don't» и «c'est» будут неправильно токенизироваться, и, кстати, вы не сможете наверное нормализовать don't до do not. И умляуты обработать… Прямая речь оформляется по-разному, значит, у вас будут проблемы с границами предложений. Дефисы…
                            В «l'astronomie» обычно должно выделяться «l'» как токен, например. А попробуйте теперь токенизировать «On nous dit qu'aujourd'hui c'est le cas, encore faudra-t-il l'évaluer» вашей библиотекой.
                            Прям эффект Даннинга — Крюгера в чистом виде. Мало знаете про токенизацию в разных языках, но думаете, что всё знаете.
                              0
                              Послушайте себя, это не конструктивный диалог. Может мы перестанем играть в угадайку? Пришлите конкретный результат, собственно тестирование я просил уже много раз. Вы же не знаете как он работает но зато много предположений. Если будут ошибки, я буду исправлять. Я ни разу не преподносил токенизатор как панацею от всех болезней и сразу сказал, что возможны баги.

                              Насчёт нормализации текстов don't -> do not — это не задача токенизатора, это совершенно уже другой продукт.

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

                                0
                                Как я его сейчас протестирую сам? Это надо его ставить долго сейчас, запускать. Подождём pip install хотя бы. Далее, какой смысл тестировать руками на нескольких предложениях? Надо делать датасет для тестирования.
                                «Если будут ошибки, я буду исправлять.» — очень медленный процесс, и я считаю его несколько бестолковым в ИИ-задачах, уж извините.
                                Слишком много примеров парсеров на правилах, которые люди дописывают годами, а потом «внезапно» их существенно обходят нейросети. Да и свой такой же опыт есть, и с правилами, и с нейросетями. Токенайзер типа вашего я делал, представьте себе. Многоязычный. Давно, лет 6 назад, наверное.
                                И вообще, возьмите датасет из razdel, да протестируйте. Всё открыто, там сотни тысяч предложений.
                                  0
                                  Версия для Python скоро будет, я сообщу. Конечно надо тестировать на датасете а не руками. В razdel разве есть датасет для французского языка?
                                    0
                                    >В razdel разве есть датасет для французского языка?
                                    Так вам для французского правильный ответ нужен? Как-то так будет норм, преимущественно апостроф влево докладывается, кажется, иногда всё же вправо бывает что ли… не помню уже: On|nous|dit|qu'|aujourd'|hui|c'|est|le|cas|,|encore|faudra|-|t|-|il|l'| évaluer. Но иногда лучше восстанавливать изначальные слова: qu' -> que, для тех же ASR LM.
                                    А в английском где-то налево, где-то направо, в принципе часто можно склеивать.
                                    А как разделять -t -il — по-разному можно, в идеале я бы дефис слева доложил к этим словам, а не отдельно сложил, и уж точно не превращал в [punct]. Вообще, для ASR пунктуацию удаляют, для OCR обычно оставляют, для spell checking оставляют. Нужно смотреть. И так же нужно смотреть особенности каждого европейского языка.
                                    Конечно, это максимум единицы процентов качества, но в некоторых более популярных случаях будет очень обидно, если LM будет косячить из-за токенайзера, а починить это потом крайне трудно, поэтому я считаю, что уж если делать кастомный токенайзер, то делать хороший, а иначе можно взять любой готовый.
                                    Датасеты для кучи языков можно взять в датасетах universaldependencies.org для морфо-синтаксического парсинга. Правда, там не будет слитного написания знаков препинания (но можно вручную рандомно зааугментировать датасет), но вот дефисы там будут, и предложения можно будет посклеивать и посмотреть, правильно ли ваша либа их потом разрезает.
                                      0
                                      Спасибо!
                0
                Юрий — Вас подтолкнули на очень неоднозначный пример, но поскольку он решился именно с таким результатом, мне стала интересна логика принятия решения.

                Если чуть подробнее, то в строке: "На рис. 1 изображена ваза, см. наш каталог." кроется двойная неоднозначность при токенизации и разборе:
                1. токенизация: давайте попробуем другую фразу, скажем в «Смотри на этот рис. 4 месяц 1912 года стал трагедией для Титаника.». Здесь «рис» — это конец предложения, даже будучи сокращением слова рисунок.
                2. анализ текста: при попытке лемматизации Вашего предложения или переделанного мной, на «рис.» мы получим минимум два значения — сокращение от рисунок и «рис» — как растение. При этом, контекст многих предложений не даст однозначного ответа, какое из двух слов имелось ввиду.

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

                Заранее благодарен за ответ.
                  0
                  avl33, ведь это — просто токенизатор, неоднозначности в таком виде он снимать не умеет.

                  Предложенный вами вариант будет интерпретирован как аббревиатура.

                  Токенизатор это только часть функционала библиотеки, основной функционал это — языковая модель.

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

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

                  По поводу: «имеет верный контекст с точки зрения собранной языковой модели.»

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

                  Текст:
                  Если поставить simple.c в конец, то получается, что использование символа ...
                  

                  Собранные N-граммы длины 3:
                  Если поставить simple.c
                  поставить simple.c в
                  simple.c в конец,
                  в конец, то
                  конец, то получается,
                  то получается, что
                  получается, что использование
                  что использование символа
                  


                  Из этого мы имеем сломанные N-граммы от которых пользы ноль

                  simple.c в конец,
                  в конец, то
                  конец, то получается,
                  то получается, что
                  получается, что использование
                  


                  Конечно, большинство людей скажут, мол за ранее удаляем все знаки препинания и задача решена. Но нет, в этом случае мы получаем также сломанные N-граммы выбитые из контекста. Другие скажут, берём n-граммы между знаками препинаний, но тогда мы сильно теряем в контексте получая мало информации. Куда правильнее знаки препинания учитывать также отдельными N-граммами, да неоднозначности будут, да они всегда будут и не важно какое приложение работает с текстом, будь нейросетевые решения или решения основанные на алгоритмах и шаблонах.

                  Токенизатор ALM соберет эти N-граммы таким образом:
                  Если поставить simple.c
                  поставить simple.c в
                  simple.c в конец
                  в конец <punct>
                  конец <punct> то
                  <punct> то получается
                  то получается <punct>
                  получается <punct> что
                  <punct> что использование
                  что использование символа
                  


                  Таким образом мы не теряем контекст и учитываем его более правильно. Для большинства вариантов текстов, этого достаточно. Бывает, что знаки препинания очень важны и их следует разделять, учитывать как отдельные N-граммы, чтобы точка была — точкой а запятая — запятой. Для этого — предусмотрен механизм с помощью скриптов python, переопределять N-граммы по своему. Собственно это и есть подконтрольный токенизатор. В данном случае, каждая новая N-грамма будет передана в скрипт и разработчик по своему мнению может её обработать.
                    0
                    ведь это — просто токенизатор, неоднозначности в таком виде он снимать не умеет.

                    Предложенный вами вариант будет интерпретирован как аббревиатура.

                    Токенизатор это только часть функционала библиотеки, основной функционал это — языковая модель.


                    Мой вопрос как раз и касался того, что токенизатор дал однозначный ответ в неоднозначной ситуации ещё до того, как модель была проанализирована. Вот эта неоднозначность: "На рис. 1 изображена ваза".

                    Токенизатор принял однозначное решение, что это — одно предложение.

                    Именно это меня заинтересовало в первую очередь, отсюда и возник вопрос, каков универсальный алгоритм принятия решения токенизатором в подобной ситуации. Для большей наглядности я дал чуть другой пример: "Смотри на этот рис. 4 месяц 1912 года стал трагедией для Титаника".

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

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

                    Ну, я во всяком случае, получаю на своих тестах, именно такой результат. И мои тесты показывают, что словарь сокращений — путь назад от автоматизации.

                    Поскольку моё видение и тесты не есть истина в последней инстанции, мне и стала интересна Ваша логика для токенизатора. Кстати, по статистике — пояснительные сокращения после цифр в тексте появляются в десятки процентов раз больше, чем до.
                      0
                      Я с вами согласен, по этому в ALM предусмотрены механизмы свободы действий, все, что не нужно в конкретной работе — отключается. В одной ситуации нужно учитывать знаки препинания как они есть, в другой удобнее когда это единый тег. Токенизатор также хорошо работает по обычным пробелам, текст можно обрабатывать отдельно.

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

                      В выходные будет доступна версия библиотеки для python где все методы ALM (за исключением методов обучения), будут доступны для свободного использования.

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

                Самое читаемое