Здравствуйте, это моя третья статья на хабре, ранее я писал статью о языковой модели ALM. Сейчас, я хочу познакомить вас с системой исправления опечаток ASC (реализованной на основе ALM).
Да, систем исправления опечаток существует огромное количество, у всех есть свои сильные и слабые стороны, из открытых систем я могу выделить одну наиболее перспективную JamSpell, с ней и будем сравнивать. Есть ещё подобная система от DeepPavlov, про которую многие могут подумать, но я с ней так и не подружился.
Список возможностей:
- Исправление ошибок в словах с разницей до 4-х дистанций по Левенштейну.
- Исправление опечаток в словах (вставка, удаление, замещение, перестановка) символов.
- Ёфикация с учётом контекста.
- Простановка регистра первой буквы слова, для (имён собственных и названий) с учётом контекста.
- Разбиение объединённых слов на отдельные слова, с учётом контекста.
- Выполнение анализа текста без корректировки исходного текста.
- Поиск в тексте наличия (ошибок, опечаток, неверного контекста).
Поддерживаемые операционные системы:
- MacOS X
- FreeBSD
- Linux
Написана система на С++11, есть порт для Python3
Готовые словари
Название | Размер (Гб) | Оперативная память (Гб) | Размер N-грамм | Язык |
---|---|---|---|---|
wittenbell-3-big.asc | 1.97 | 15.6 | 3 | RU |
wittenbell-3-middle.asc | 1.24 | 9.7 | 3 | RU |
mkneserney-3-middle.asc | 1.33 | 9.7 | 3 | RU |
wittenbell-3-single.asc | 0.772 | 5.14 | 3 | RU |
wittenbell-5-single.asc | 1.37 | 10.7 | 5 | RU |
Тестирование
Для проверки работы системы использовались данные соревнованияисправления опечаток 2016 года от Dialog21. Для тестирования использовался обученный бинарный словарь:wittenbell-3-middle.asc
Проводимый тест | Precision | Recall | FMeasure |
---|---|---|---|
Режим исправления опечаток | 76.97 | 62.71 | 69.11 |
Режим исправления ошибок | 73.72 | 60.53 | 66.48 |
Думаю, излишне добавлять другие данные, при желании каждый может повторить тест, все используемые материалы в тестировании прикладываю ниже.
Материалы использовавшиеся в тестировании
- test.txt- Текст для тестирования
- correct.txt- Текст корректных вариантов
- evaluate.py- Скрипт Python3 для расчёта результатов коррекции
Теперь, интересно сравнить, работу самих систем исправления опечаток в равных условиях, обучим два разных опечаточника на одних и тех же текстовых данных и проведём тест.
Для сравнения возьмём систему исправления опечаток, которую я упоминал выше JamSpell.
ASC vs JamSpell
$ git clone --recursive https://github.com/anyks/asc.git$ cd ./asc$ mkdir ./build$ cd ./build$ cmake ..$ make
JamSpell
$ git clone https://github.com/bakwc/JamSpell.git$ cd ./JamSpell$ mkdir ./build$ cd ./build$ cmake ..$ make
train.json
{ "ext": "txt", "size": 3, "alter": {"е":"ё"}, "debug": 1, "threads": 0, "method": "train", "allow-unk": true, "reset-unk": true, "confidence": true, "interpolate": true, "mixed-dicts": true, "only-token-words": true, "locale": "en_US.UTF-8", "smoothing": "wittenbell", "pilots": ["а","у","в","о","с","к","б","и","я","э","a","i","o","e","g"], "corpus": "./texts/correct.txt", "w-bin": "./dictionary/3-middle.asc", "w-vocab": "./train/lm.vocab", "w-arpa": "./train/lm.arpa", "mix-restwords": "./similars/letters.txt", "alphabet": "абвгдеёжзийклмнопрстуфхцчшщъыьэюяabcdefghijklmnopqrstuvwxyz", "bin-code": "ru", "bin-name": "Russian", "bin-author": "You name", "bin-copyright": "You company LLC", "bin-contacts": "site: https://example.com, e-mail: info@example.com", "bin-lictype": "MIT", "bin-lictext": "... License text ...", "embedding-size": 28, "embedding": { "а": 0, "б": 1, "в": 2, "г": 3, "д": 4, "е": 5, "ё": 5, "ж": 6, "з": 7, "и": 8, "й": 8, "к": 9, "л": 10, "м": 11, "н": 12, "о": 0, "п": 13, "р": 14, "с": 15, "т": 16, "у": 17, "ф": 18, "х": 19, "ц": 20, "ч": 21, "ш": 21, "щ": 21, "ъ": 22, "ы": 23, "ь": 22, "э": 5, "ю": 24, "я": 25, "<": 26, ">": 26, "~": 26, "-": 26, "+": 26, "=": 26, "*": 26, "/": 26, ":": 26, "%": 26, "|": 26, "^": 26, "&": 26, "#": 26, "'": 26, "\\": 26, "0": 27, "1": 27, "2": 27, "3": 27, "4": 27, "5": 27, "6": 27, "7": 27, "8": 27, "9": 27, "a": 0, "b": 2, "c": 15, "d": 4, "e": 5, "f": 18, "g": 3, "h": 12, "i": 8, "j": 6, "k": 9, "l": 10, "m": 11, "n": 12, "o": 0, "p": 14, "q": 13, "r": 14, "s": 15, "t": 16, "u": 24, "v": 21, "w": 22, "x": 19, "y": 17, "z": 7 }}
$ ./asc -r-json ./train.json
Приведу также пример на языке Python3
import ascasc.setSize(3)asc.setAlmV2()asc.setThreads(0)asc.setLocale("en_US.UTF-8")asc.setOption(asc.options_t.uppers)asc.setOption(asc.options_t.allowUnk)asc.setOption(asc.options_t.resetUnk)asc.setOption(asc.options_t.mixDicts)asc.setOption(asc.options_t.tokenWords)asc.setOption(asc.options_t.confidence)asc.setOption(asc.options_t.interpolate)asc.setAlphabet("абвгдеёжзийклмнопрстуфхцчшщъыьэюяabcdefghijklmnopqrstuvwxyz")asc.setPilots(["а","у","в","о","с","к","б","и","я","э","a","i","o","e","g"])asc.setSubstitutes({'p':'р','c':'с','o':'о','t':'т','k':'к','e':'е','a':'а','h':'н','x':'х','b':'в','m':'м'})def statusArpa1(status): print("Build arpa", status)def statusArpa2(status): print("Write arpa", status)def statusVocab(status): print("Write vocab", status)def statusIndex(text, status): print(text, status)def status(text, status): print(text, status)asc.collectCorpus("./texts/correct.txt", asc.smoothing_t.wittenBell, 0.0, False, False, status)asc.buildArpa(statusArpa1)asc.writeArpa("./train/lm.arpa", statusArpa2)asc.writeVocab("./train/lm.vocab", statusVocab)asc.setCode("RU")asc.setLictype("MIT")asc.setName("Russian")asc.setAuthor("You name")asc.setCopyright("You company LLC")asc.setLictext("... License text ...")asc.setContacts("site: https://example.com, e-mail: info@example.com")asc.setEmbedding({ "а": 0, "б": 1, "в": 2, "г": 3, "д": 4, "е": 5, "ё": 5, "ж": 6, "з": 7, "и": 8, "й": 8, "к": 9, "л": 10, "м": 11, "н": 12, "о": 0, "п": 13, "р": 14, "с": 15, "т": 16, "у": 17, "ф": 18, "х": 19, "ц": 20, "ч": 21, "ш": 21, "щ": 21, "ъ": 22, "ы": 23, "ь": 22, "э": 5, "ю": 24, "я": 25, "<": 26, ">": 26, "~": 26, "-": 26, "+": 26, "=": 26, "*": 26, "/": 26, ":": 26, "%": 26, "|": 26, "^": 26, "&": 26, "#": 26, "'": 26, "\\": 26, "0": 27, "1": 27, "2": 27, "3": 27, "4": 27, "5": 27, "6": 27, "7": 27, "8": 27, "9": 27, "a": 0, "b": 2, "c": 15, "d": 4, "e": 5, "f": 18, "g": 3, "h": 12, "i": 8, "j": 6, "k": 9, "l": 10, "m": 11, "n": 12, "o": 0, "p": 14, "q": 13, "r": 14, "s": 15, "t": 16, "u": 24, "v": 21, "w": 22, "x": 19, "y": 17, "z": 7}, 28)asc.saveIndex("./dictionary/3-middle.asc", "", 128, statusIndex)
JamSpell
$ ./main/jamspell train ../test_data/alphabet_ru.txt ../test_data/correct.txt ./model.bin
spell.json
{ "debug": 1, "threads": 0, "method": "spell", "spell-verbose": true, "confidence": true, "mixed-dicts": true, "asc-split": true, "asc-alter": true, "asc-esplit": true, "asc-rsplit": true, "asc-uppers": true, "asc-hyphen": true, "asc-wordrep": true, "r-text": "./texts/test.txt", "w-text": "./texts/output.txt", "r-bin": "./dictionary/3-middle.asc"}
$ ./asc -r-json ./spell.json
Пример на языке Python3
import ascasc.setAlmV2()asc.setThreads(0)asc.setOption(asc.options_t.uppers)asc.setOption(asc.options_t.ascSplit)asc.setOption(asc.options_t.ascAlter)asc.setOption(asc.options_t.ascESplit)asc.setOption(asc.options_t.ascRSplit)asc.setOption(asc.options_t.ascUppers)asc.setOption(asc.options_t.ascHyphen)asc.setOption(asc.options_t.ascWordRep)asc.setOption(asc.options_t.mixDicts)asc.setOption(asc.options_t.confidence)def status(text, status): print(text, status)asc.loadIndex("./dictionary/3-middle.asc", "", status)f1 = open('./texts/test.txt')f2 = open('./texts/output.txt', 'w')for line in f1.readlines(): res = asc.spell(line) f2.write("%s\n" % res[0])f2.close()f1.close()
JamSpell
Так-как версия для Python у меня не собралась, пришлось написать небольшое приложение на C++
#include <fstream>#include <iostream>#include <jamspell/spell_corrector.hpp>// Если используется BOOST#ifdef USE_BOOST_CONVERT#include <boost/locale/encoding_utf.hpp>// Если нужно использовать стандартную библиотеку#else#include <codecvt>#endifusing namespace std;/** * convert Метод конвертирования строки utf-8 в строку * @param str строка utf-8 для конвертирования * @return обычная строка */const string convert(const wstring & str){// Результат работы функцииstring result = "";// Если строка переданаif(!str.empty()){// Если используется BOOST#ifdef USE_BOOST_CONVERT// Объявляем конвертерusing boost::locale::conv::utf_to_utf;// Выполняем конвертирование в utf-8 строкуresult = utf_to_utf <char> (str.c_str(), str.c_str() + str.size());// Если нужно использовать стандартную библиотеку#else// Устанавливаем тип для конвертера UTF-8using convert_type = codecvt_utf8 <wchar_t, 0x10ffff, little_endian>;// Объявляем конвертерwstring_convert <convert_type, wchar_t> conv;// wstring_convert <codecvt_utf8 <wchar_t>> conv;// Выполняем конвертирование в utf-8 строкуresult = conv.to_bytes(str);#endif}// Выводим результатreturn result;}/** * convert Метод конвертирования строки в строку utf-8 * @param str строка для конвертирования * @return строка в utf-8 */const wstring convert(const string & str){// Результат работы функцииwstring result = L"";// Если строка переданаif(!str.empty()){// Если используется BOOST#ifdef USE_BOOST_CONVERT// Объявляем конвертерusing boost::locale::conv::utf_to_utf;// Выполняем конвертирование в utf-8 строкуresult = utf_to_utf <wchar_t> (str.c_str(), str.c_str() + str.size());// Если нужно использовать стандартную библиотеку#else// Объявляем конвертер// wstring_convert <codecvt_utf8 <wchar_t>> conv;wstring_convert <codecvt_utf8_utf16 <wchar_t, 0x10ffff, little_endian>> conv;// Выполняем конвертирование в utf-8 строкуresult = conv.from_bytes(str);#endif}// Выводим результатreturn result;}/** * safeGetline Функция извлечения строки из текста * @param is файловый поток * @param t строка для извлечения текста * @return файловый поток */istream & safeGetline(istream & is, string & t){// Очищаем строкуt.clear();istream::sentry se(is, true);streambuf * sb = is.rdbuf();for(;;){int c = sb->sbumpc();switch(c){ case '\n': return is;case '\r':if(sb->sgetc() == '\n') sb->sbumpc();return is;case streambuf::traits_type::eof():if(t.empty()) is.setstate(ios::eofbit);return is;default: t += (char) c;}}}/*** main Главная функция приложения*/int main(){// Создаём корректорNJamSpell::TSpellCorrector corrector;// Загружаем модель обученияcorrector.LoadLangModel("model.bin");// Открываем файл на чтениеifstream file1("./test_data/test.txt", ios::in);// Если файл открытif(file1.is_open()){// Строка чтения из файлаstring line = "", res = "";// Открываем файл на чтениеofstream file2("./test_data/output.txt", ios::out);// Если файл открытif(file2.is_open()){// Считываем до тех пор пока все удачноwhile(file1.good()){// Считываем строку из файлаsafeGetline(file1, line);// Если текст получен, выполняем коррекциюif(!line.empty()){// Получаем исправленный текстres = convert(corrector.FixFragment(convert(line)));// Если текст получен, записываем его в файлif(!res.empty()){// Добавляем перенос строкиres.append("\n");// Записываем результат в файлfile2.write(res.c_str(), res.size());}}}// Закрываем файлfile2.close();}// Закрываем файлfile1.close();} return 0;}
Компилируем и запускаем
$ g++ -std=c++11 -I../JamSpell -L./build/jamspell -L./build/contrib/cityhash -L./build/contrib/phf -ljamspell_lib -lcityhash -lphf ./test.cpp -o ./bin/test$ ./bin/test
Результаты
$ python3 evaluate.py ./texts/test.txt ./texts/correct.txt ./texts/output.txt
ASC
Precision | Recall | FMeasure |
---|---|---|
92.13 | 82.51 | 87.05 |
JamSpell
Precision | Recall | FMeasure |
---|---|---|
77.87 | 63.36 | 69.87 |
Одной из главных возможностей ASC обучение на грязных данных. В отрытом доступе найти текстовые корпуса без ошибок и опечаток практически не реально. Исправлять руками терабайты данных не хватит жизни, а работать с этим как-то надо.
Принцип обучения который предлагаю я
- Собираем языковую модель на грязных данных
- Удаляем все редко встречающиеся слова и N-граммы в собранной языковой модели
- Добавляем одиночные слова для более правильной работы системы исправления опечаток.
- Собираем бинарный словарь
Приступим
Предположим, что у нас есть несколько корпусов разной тематики, логичнее обучить их отдельно, потом объединить.
{"size": 3,"debug": 1,"threads": 0,"ext": "txt","method": "train","allow-unk": true,"mixed-dicts": true,"only-token-words": true,"smoothing": "wittenbell","locale": "en_US.UTF-8","w-abbr": "./output/alm.abbr","w-map": "./output/alm.map","w-vocab": "./output/alm.vocab","w-words": "./output/words.txt","corpus": "./texts/corpus","abbrs": "./abbrs/abbrs.txt","goodwords": "./texts/whitelist/words.txt","badwords": "./texts/blacklist/garbage.txt","mix-restwords": "./texts/similars/letters.txt","alphabet": "абвгдеёжзийклмнопрстуфхцчшщъыьэюяabcdefghijklmnopqrstuvwxyz"}
$ ./alm -r-json ./collect.json
- size Мы собираем N-граммы длиной 3
- debug Выводим индикатор выполнения сбора данных
- threads Для сборки используем все доступные ядра
- ext Указываем расширение файлов в каталоге которые пригодны для обучения
- allow-unk Разрешаем хранить токенunkв языковой модели
- mixed-dicts Разрешаем исправлять слова с замещёнными буквами из других языков
- only-token-words Собираем не целиком N-граммы как есть а только последовательности нормальных слов
- smoothing Используем алгоритм сглаживания wittenbell (на данном этапе он не применяется, но какой-то алгоритм сглаживания указать нужно)
- locale Устанавливаем локаль окружения (можно не указывать)
- w-abbr Сохраняем собранные суффиксы цифровых аббревиатур
- w-map Сохраняем карту последовательности как промежуточный результат
- w-vocab Сохраняем собранный словарь
- w-words Сохраняем список собранных уникальных слов (на всякий случай)
- corpus Используем для сборки каталог с текстовыми данными корпуса
- abbrs Используем в обучении, общеупотребимые аббревиатуры, такие как (США, ФСБ, КГБ ...)
- goodwords Используем заранее подготовленный белый список слов
- badwords Используем заранее подготовленный чёрный список слов
- mix-restwords Используем файл с похожими символами разных языков
- alphabet Указываем алфавит используемый при обучении (алфавит всегда должен быть указан один и тот же)
Версия на Python
import alm# Мы собираем N-граммы длиной 3alm.setSize(3)# Для сборки используем все доступные ядраalm.setThreads(0)# Устанавливаем локаль окружения (можно не указывать)alm.setLocale("en_US.UTF-8")# Указываем алфавит используемый при обучении (алфавит всегда должен быть указан один и тот же)alm.setAlphabet("абвгдеёжзийклмнопрстуфхцчшщъыьэюяabcdefghijklmnopqrstuvwxyz")# Устанавливаем похожие символы разных языковalm.setSubstitutes({'p':'р','c':'с','o':'о','t':'т','k':'к','e':'е','a':'а','h':'н','x':'х','b':'в','m':'м'})# Разрешаем хранить токен <unk> в языковой моделиalm.setOption(alm.options_t.allowUnk)# Разрешаем исправлять слова с замещёнными буквами из других языковalm.setOption(alm.options_t.mixDicts)# Собираем не целиком N-граммы как есть а только последовательности нормальных словalm.setOption(alm.options_t.tokenWords)# Используем алгоритм сглаживания wittenbell (на данном этапе он не применяется, но какой-то алгоритм сглаживания указать нужно)alm.init(alm.smoothing_t.wittenBell)# Используем в обучении, общеупотребимые аббревиатуры, такие как (США, ФСБ, КГБ ...)f = open('./abbrs/abbrs.txt')for abbr in f.readlines(): abbr = abbr.replace("\n", "") alm.addAbbr(abbr)f.close()# Используем заранее подготовленный белый список словf = open('./texts/whitelist/words.txt')for word in f.readlines(): word = word.replace("\n", "") alm.addGoodword(word)f.close()# Используем заранее подготовленный чёрный список словf = open('./texts/blacklist/garbage.txt')for word in f.readlines(): word = word.replace("\n", "") alm.addBadword(word)f.close()def status(text, status): print(text, status)def statusWords(status): print("Write words", status)def statusVocab(status): print("Write vocab", status)def statusMap(status): print("Write map", status)def statusSuffix(status): print("Write suffix", status)# Выполняем сборку языковой моделиalm.collectCorpus("./texts/corpus", status)# Выполняем сохранение списка собранных уникальных словalm.writeWords("./output/words.txt", statusWords)# Выполняем сохранение словаряalm.writeVocab("./output/alm.vocab", statusVocab)# Выполняем сохранение карты последовательностиalm.writeMap("./output/alm.map", statusMap)# Выполняем сохранение списка суффиксов цифровых аббревиатурalm.writeSuffix("./output/alm.abbr", statusSuffix)
Таким образом, мы собираем все наши корпуса
{ "size": 3, "debug": 1, "allow-unk": true, "method": "vprune", "vprune-wltf": -15.0, "locale": "en_US.UTF-8", "smoothing": "wittenbell", "r-map": "./corpus1/alm.map", "r-vocab": "./corpus1/alm.vocab", "w-map": "./output/alm.map", "w-vocab": "./output/alm.vocab", "goodwords": "./texts/whitelist/words.txt", "badwords": "./texts/blacklist/garbage.txt", "alphabet": "абвгдеёжзийклмнопрстуфхцчшщъыьэюяabcdefghijklmnopqrstuvwxyz"}
$ ./alm -r-json ./prune.json
- size Мы используем N-граммы длиной 3
- debug Выводим индикатор выполнения прунинга словаря
- allow-unk Разрешаем хранить токенunkв языковой модели
- vprune-wltf Минимально-разрешённый вес слова в словаре (все, что ниже удаляется)
- locale Устанавливаем локаль окружения (можно не указывать)
- smoothing Используем алгоритм сглаживания wittenbell (на данном этапе он не применяется, но какой-то алгоритм сглаживания указать нужно)
- r-map Кара последовательности собранная на предыдущем этапе
- r-vocab Словарь собранный на предыдущем этапе
- w-map Сохраняем карту последовательности как промежуточный результат
- w-vocab Сохраняем собранный словарь
- goodwords Используем заранее подготовленный белый список слов
- badwords Используем заранее подготовленный чёрный список слов
- alphabet Указываем алфавит используемый при обучении (алфавит всегда должен быть указан один и тот же)
Версия на Python
import alm# Мы собираем N-граммы длиной 3alm.setSize(3)# Для сборки используем все доступные ядраalm.setThreads(0)# Устанавливаем локаль окружения (можно не указывать)alm.setLocale("en_US.UTF-8")# Указываем алфавит используемый при обучении (алфавит всегда должен быть указан один и тот же)alm.setAlphabet("абвгдеёжзийклмнопрстуфхцчшщъыьэюяabcdefghijklmnopqrstuvwxyz")# Разрешаем хранить токен <unk> в языковой моделиalm.setOption(alm.options_t.allowUnk)# Используем алгоритм сглаживания wittenbell (на данном этапе он не применяется, но какой-то алгоритм сглаживания указать нужно)alm.init(alm.smoothing_t.wittenBell)# Используем заранее подготовленный белый список словf = open('./texts/whitelist/words.txt')for word in f.readlines(): word = word.replace("\n", "") alm.addGoodword(word)f.close()# Используем заранее подготовленный чёрный список словf = open('./texts/blacklist/garbage.txt')for word in f.readlines(): word = word.replace("\n", "") alm.addBadword(word)f.close()def statusPrune(status): print("Prune data", status)def statusReadVocab(text, status): print("Read vocab", text, status)def statusWriteVocab(status): print("Write vocab", status)def statusReadMap(text, status): print("Read map", text, status)def statusWriteMap(status): print("Write map", status)# Выполняем загрузкусловаряalm.readVocab("./corpus1/alm.vocab", statusReadVocab)# Выполняем загрузку карты последовательностиalm.readMap("./corpus1/alm.map", statusReadMap)# Выполняем прунинг словаряalm.pruneVocab(-15.0, 0, 0, statusPrune)# Выполняем сохранение словаряalm.writeVocab("./output/alm.vocab", statusWriteVocab)# Выполняем сохранение карты последовательностиalm.writeMap("./output/alm.map", statusWriteMap)
{ "size": 3, "debug": 1, "allow-unk": true, "method": "merge", "mixed-dicts": "true", "locale": "en_US.UTF-8", "smoothing": "wittenbell", "r-words": "./texts/words", "r-map": "./corpus1", "r-vocab": "./corpus1", "w-map": "./output/alm.map", "w-vocab": "./output/alm.vocab", "goodwords": "./texts/whitelist/words.txt", "badwords": "./texts/blacklist/garbage.txt", "mix-restwords": "./texts/similars/letters.txt", "alphabet": "абвгдеёжзийклмнопрстуфхцчшщъыьэюяabcdefghijklmnopqrstuvwxyz"}
$ ./alm -r-json ./merge.json
- size Мы используем N-граммы длиной 3
- debug Выводим индикатор выполнения загрузки данных
- allow-unk Разрешаем хранить токенunkв языковой модели
- mixed-dicts Разрешаем исправлять слова с замещёнными буквами из других языков
- locale Устанавливаем локаль окружения (можно не указывать)
- smoothing Используем алгоритм сглаживания wittenbell (на данном этапе он не применяется, но какой-то алгоритм сглаживания указать нужно)
- r-words Указываем каталог или файл с словами которые нужно добавить в словарь
- r-map Указываем каталог с файлами карт последовательности, собранных и пропруненных на предыдущих этапах
- r-vocab Указываем каталог с файлами словарей, собранных и пропруненных на предыдущих этапах
- w-map Сохраняем карту последовательности как промежуточный результат
- w-vocab Сохраняем собранный словарь
- goodwords Используем заранее подготовленный белый список слов
- badwords Используем заранее подготовленный чёрный список слов
- alphabet Указываем алфавит используемый при обучении (алфавит всегда должен быть указан один и тот же)
Версия на Python
import alm# Мы собираем N-граммы длиной 3alm.setSize(3)# Для сборки используем все доступные ядраalm.setThreads(0)# Устанавливаем локаль окружения (можно не указывать)alm.setLocale("en_US.UTF-8")# Указываем алфавит используемый при обучении (алфавит всегда должен быть указан один и тот же)alm.setAlphabet("абвгдеёжзийклмнопрстуфхцчшщъыьэюяabcdefghijklmnopqrstuvwxyz")# Устанавливаем похожие символы разных языковalm.setSubstitutes({'p':'р','c':'с','o':'о','t':'т','k':'к','e':'е','a':'а','h':'н','x':'х','b':'в','m':'м'})# Разрешаем хранить токен <unk> в языковой моделиalm.setOption(alm.options_t.allowUnk)# Разрешаем исправлять слова с замещёнными буквами из других языковalm.setOption(alm.options_t.mixDicts)# Используем алгоритм сглаживания wittenbell (на данном этапе он не применяется, но какой-то алгоритм сглаживания указать нужно)alm.init(alm.smoothing_t.wittenBell)# Используем заранее подготовленный белый список словf = open('./texts/whitelist/words.txt')for word in f.readlines(): word = word.replace("\n", "") alm.addGoodword(word)f.close()# Используем заранее подготовленный чёрный список словf = open('./texts/blacklist/garbage.txt')for word in f.readlines(): word = word.replace("\n", "") alm.addBadword(word)f.close()# Используем файл с словами которые нужно добавить в словарьf = open('./texts/words.txt')for word in f.readlines(): word = word.replace("\n", "") alm.addWord(word)f.close()def statusReadVocab(text, status): print("Read vocab", text, status)def statusWriteVocab(status): print("Write vocab", status)def statusReadMap(text, status): print("Read map", text, status)def statusWriteMap(status): print("Write map", status)# Выполняем загрузку словаряalm.readVocab("./corpus1", statusReadVocab)# Выполняем загрузку карты последовательностиalm.readMap("./corpus1", statusReadMap)# Выполняем сохранение словаряalm.writeVocab("./output/alm.vocab", statusWriteVocab)# Выполняем сохранение карты последовательностиalm.writeMap("./output/alm.map", statusWriteMap)
{ "size": 3, "debug": 1, "allow-unk": true, "reset-unk": true, "interpolate": true, "method": "train", "locale": "en_US.UTF-8", "smoothing": "wittenbell", "r-map": "./output/alm.map", "r-vocab": "./output/alm.vocab", "w-arpa": "./output/alm.arpa", "w-words": "./output/words.txt", "alphabet": "абвгдеёжзийклмнопрстуфхцчшщъыьэюяabcdefghijklmnopqrstuvwxyz"}
$ ./alm -r-json ./train.json
- size Мы используем N-граммы длиной 3
- debug Выводим индикатор обучения языковой модели
- allow-unk Разрешаем хранить токенunkв языковой модели
- reset-unk Выполняем сброс значения частоты, дляunkтокена в языковой модели
- interpolate Выполнять интерполяцию при расчётах частот
- locale Устанавливаем локаль окружения (можно не указывать)
- smoothing Используем алгоритм сглаживания wittenbell
- r-map Указываем файл карты последовательности, собранной на предыдущих этапах
- r-vocab Указываем файл словаря, собранного на предыдущих этапах
- w-arpa Указываем адрес файла ARPA, для сохранения
- w-words Указываем адрес файла, для сохранения уникальных слов (на всякий случай)
- alphabet Указываем алфавит используемый при обучении (алфавит всегда должен быть указан один и тот же)
Версия на Python
import alm# Мы собираем N-граммы длиной 3alm.setSize(3)# Для сборки используем все доступные ядраalm.setThreads(0)# Устанавливаем локаль окружения (можно не указывать)alm.setLocale("en_US.UTF-8")# Указываем алфавит используемый при обучении (алфавит всегда должен быть указан один и тот же)alm.setAlphabet("абвгдеёжзийклмнопрстуфхцчшщъыьэюяabcdefghijklmnopqrstuvwxyz")# Устанавливаем похожие символы разных языковalm.setSubstitutes({'p':'р','c':'с','o':'о','t':'т','k':'к','e':'е','a':'а','h':'н','x':'х','b':'в','m':'м'})# Разрешаем хранить токен <unk> в языковой моделиalm.setOption(alm.options_t.allowUnk)# Выполняем сброс значения частоты токена <unk> в языковой моделиalm.setOption(alm.options_t.resetUnk)# Разрешаем исправлять слова с замещёнными буквами из других языковalm.setOption(alm.options_t.mixDicts)# Разрешаем выполнять интерполяцию при расчётахalm.setOption(alm.options_t.interpolate)# Используем алгоритм сглаживания wittenbell (на данном этапе он не применяется, но какой-то алгоритм сглаживания указать нужно)alm.init(alm.smoothing_t.wittenBell)def statusReadVocab(text, status): print("Read vocab", text, status)def statusReadMap(text, status): print("Read map", text, status)def statusBuildArpa(status): print("Build ARPA", status)def statusWriteMap(status): print("Write map", status)def statusWriteArpa(status): print("Write ARPA", status)def statusWords(status): print("Write words", status)# Выполняем загрузку словаряalm.readVocab("./output/alm.vocab", statusReadVocab)# Выполняем загрузку карты последовательностиalm.readMap("./output/alm.map", statusReadMap)# Выполняем расчёты частот языковой моделиalm.buildArpa(statusBuildArpa)# Выполняем запись языковой модели в файл ARPAalm.writeArpa("./output/alm.arpa", statusWriteArpa)# Выполняем сохранение словаряalm.writeWords("./output/words.txt", statusWords)
{"size": 3,"debug": 1,"threads": 0,"confidence": true,"mixed-dicts": true,"method": "train","alter": {"е":"ё"},"locale": "en_US.UTF-8","smoothing": "wittenbell","pilots": ["а","у","в","о","с","к","б","и","я","э","a","i","o","e","g"],"w-bin": "./dictionary/3-single.asc","r-abbr": "./output/alm.abbr","r-vocab": "./output/alm.vocab","r-arpa": "./output/alm.arpa","abbrs": "./texts/abbrs/abbrs.txt","goodwords": "./texts/whitelist/words.txt","badwords": "./texts/blacklist/garbage.txt","alters": "./texts/alters/yoficator.txt","upwords": "./texts/words/upp","mix-restwords": "./texts/similars/letters.txt","alphabet": "абвгдеёжзийклмнопрстуфхцчшщъыьэюяabcdefghijklmnopqrstuvwxyz","bin-code": "ru","bin-name": "Russian","bin-author": "You name","bin-copyright": "You company LLC","bin-contacts": "site: https://example.com, e-mail: info@example.com","bin-lictype": "MIT","bin-lictext": "... License text ...","embedding-size": 28,"embedding": { "а": 0, "б": 1, "в": 2, "г": 3, "д": 4, "е": 5, "ё": 5, "ж": 6, "з": 7, "и": 8, "й": 8, "к": 9, "л": 10, "м": 11, "н": 12, "о": 0, "п": 13, "р": 14, "с": 15, "т": 16, "у": 17, "ф": 18, "х": 19, "ц": 20, "ч": 21, "ш": 21, "щ": 21, "ъ": 22, "ы": 23, "ь": 22, "э": 5, "ю": 24, "я": 25, "<": 26, ">": 26, "~": 26, "-": 26, "+": 26, "=": 26, "*": 26, "/": 26, ":": 26, "%": 26, "|": 26, "^": 26, "&": 26, "#": 26, "'": 26, "\\": 26, "0": 27, "1": 27, "2": 27, "3": 27, "4": 27, "5": 27, "6": 27, "7": 27, "8": 27, "9": 27, "a": 0, "b": 2, "c": 15, "d": 4, "e": 5, "f": 18, "g": 3, "h": 12, "i": 8, "j": 6, "k": 9, "l": 10, "m": 11, "n": 12, "o": 0, "p": 14, "q": 13, "r": 14, "s": 15, "t": 16, "u": 24, "v": 21, "w": 22, "x": 19, "y": 17, "z": 7}}
$ ./asc -r-json ./train.json
- size Мы используем N-граммы длиной 3
- debug Выводим индикатор обучения опечаточника
- threads Для сборки используем все доступные ядра
- confidence Разрешаем загружать данные из ARPA так-как они есть, без перетокенизации
- mixed-dicts Разрешаем исправлять слова с замещёнными буквами из других языков
- alter Альтернативные буквы (буквы которые замещают другие буквы в словаре, в нашем случае, это буква Ё)
- locale Устанавливаем локаль окружения (можно не указывать)
- smoothing Используем алгоритм сглаживания wittenbell (на данном этапе он не применяется, но какой-то алгоритм сглаживания указать нужно)
- pilots Устанавливаем список пилотных слов (слова состоящие из одной буквы)
- w-bin Устанавливаем адрес для сохранения бинарного контейнера
- r-abbr Указываем каталог с файлами, собранных суффиксов цифровых аббревиатур на предыдущих этапах
- r-vocab Указываем файл словаря, собранного на предыдущих этапах
- r-arpa Указываем файл ARPA, собранный на предыдущем этапе
- abbrs Используем в обучении, общеупотребимые аббревиатуры, такие как (США, ФСБ, КГБ ...)
- goodwords Используем заранее подготовленный белый список слов
- badwords Используем заранее подготовленный чёрный список слов
- alters Используем файл со словами содержащими альтернативные буквы, которые используются всегда однозначно (синтаксис файла аналогичен списку похожих букв в разных алфавитах)
- upwords Используем файл со списком слов, которые всегда употребляются с заглавной буквы (названия, имена, фамилии...)
- mix-restwords Используем файл с похожими символами разных языков
- alphabet Указываем алфавит используемый при обучении (алфавит всегда должен быть указан один и тот же)
- bin-code Устанавливаем код языка в словаре
- bin-name Устанавливаем название словаря
- bin-author Устанавливаем имя автора словаря
- bin-copyright Устанавливаем копирайт словаря
- bin-contacts Устанавливаем контактные данные автора словаря
- bin-lictype Устанавливаем тип лицензии словаря
- bin-lictext Устанавливаем текст лицензии словаря
- embedding-size Устанавливаем размер блока внутреннего эмбеддинга
- embedding Устанавливаем параметры блока внутреннего эмбеддинга (не обязательно, влияет на точность подбора кандидатов)
Версия на Python
import asc# Мы собираем N-граммы длиной 3asc.setSize(3)# Для сборки используем все доступные ядраasc.setThreads(0)# Устанавливаем локаль окружения (можно не указывать)asc.setLocale("en_US.UTF-8")# Разрешаем исправлять регистр у слов в начале предложенийasc.setOption(asc.options_t.uppers)# Разрешаем хранить токен <unk> в языковой моделиasc.setOption(asc.options_t.allowUnk)# Выполняем сброс значения частоты токена <unk> в языковой моделиasc.setOption(asc.options_t.resetUnk)# Разрешаем исправлять слова с замещенными буквами из других языковasc.setOption(asc.options_t.mixDicts)# Разрешаем загружать данные из ARPA так-как они есть, без перетокенизацииasc.setOption(asc.options_t.confidence)# Указываем алфавит используемый при обучении (алфавит всегда должен быть указан один и тот же)asc.setAlphabet("абвгдеёжзийклмнопрстуфхцчшщъыьэюяabcdefghijklmnopqrstuvwxyz")# Указываем список пилотных слов (слова которые состоят из одной буквы)asc.setPilots(["а","у","в","о","с","к","б","и","я","э","a","i","o","e","g"])# Устанавливаем похожие символы разных языковasc.setSubstitutes({'p':'р','c':'с','o':'о','t':'т','k':'к','e':'е','a':'а','h':'н','x':'х','b':'в','m':'м'})# Загружаем файл заранее подготовленный белый список словf = open('./texts/whitelist/words.txt')for word in f.readlines(): word = word.replace("\n", "") asc.addGoodword(word)f.close()# Загружаем файл заранее подготовленный чёрный список словf = open('./texts/blacklist/garbage.txt')for word in f.readlines(): word = word.replace("\n", "") asc.addBadword(word)f.close()# Загружаем файл суффиксов цифровых аббревиатурf = open('./output/alm.abbr')for word in f.readlines(): word = word.replace("\n", "") asc.addSuffix(word)f.close()# Загружаем файл общеупотребимые аббревиатуры, такие как (США, ФСБ, КГБ ...)f = open('./texts/abbrs/abbrs.txt')for abbr in f.readlines(): abbr = abbr.replace("\n", "") asc.addAbbr(abbr)f.close()# Загружаем файл со списком слов, которые всегда употребляются с заглавной буквы (названия, имена, фамилии...)f = open('./texts/words/upp/words.txt')for word in f.readlines(): word = word.replace("\n", "") asc.addUWord(word)f.close()# Устанавливаем альтернативную буквуasc.addAlt("е", "ё")# Загружаем файл со словами содержащими альтернативные буквы, которые используются всегда однозначно (синтаксис файла аналогичен списку похожих букв в разных алфавитах)f = open('./texts/alters/yoficator.txt')for words in f.readlines(): words = words.replace("\n", "") words = words.split('\t') asc.addAlt(words[0], words[1])f.close()def statusIndex(text, status): print(text, status)def statusBuildIndex(status): print("Build index", status)def statusArpa(status): print("Read arpa", status)def statusVocab(status): print("Read vocab", status)# Выполняем загрузку данные языковой модели из файла ARPAasc.readArpa("./output/alm.arpa", statusArpa)# Выполняем загрузку словаряasc.readVocab("./output/alm.vocab", statusVocab)# Устанавливаем код языка в словареasc.setCode("RU")# Устанавливаем тип лицензии словаряasc.setLictype("MIT")# Устанавливаем название словаряasc.setName("Russian")# Устанавливаем имя автора словаряasc.setAuthor("You name")# Устанавливаем копирайт словаряasc.setCopyright("You company LLC")# Устанавливаем текст лицензии словаряasc.setLictext("... License text ...")# Устанавливаем контактные данные автора словаряasc.setContacts("site: https://example.com, e-mail: info@example.com")# Устанавливаем параметры блока внутреннего эмбеддинга (не обязательно, влияет на точность подбора кандидатов)asc.setEmbedding({ "а": 0, "б": 1, "в": 2, "г": 3, "д": 4, "е": 5, "ё": 5, "ж": 6, "з": 7, "и": 8, "й": 8, "к": 9, "л": 10, "м": 11, "н": 12, "о": 0, "п": 13, "р": 14, "с": 15, "т": 16, "у": 17, "ф": 18, "х": 19, "ц": 20, "ч": 21, "ш": 21, "щ": 21, "ъ": 22, "ы": 23, "ь": 22, "э": 5, "ю": 24, "я": 25, "<": 26, ">": 26, "~": 26, "-": 26, "+": 26, "=": 26, "*": 26, "/": 26, ":": 26, "%": 26, "|": 26, "^": 26, "&": 26, "#": 26, "'": 26, "\\": 26, "0": 27, "1": 27, "2": 27, "3": 27, "4": 27, "5": 27, "6": 27, "7": 27, "8": 27, "9": 27, "a": 0, "b": 2, "c": 15, "d": 4, "e": 5, "f": 18, "g": 3, "h": 12, "i": 8, "j": 6, "k": 9, "l": 10, "m": 11, "n": 12, "o": 0, "p": 14, "q": 13, "r": 14, "s": 15, "t": 16, "u": 24, "v": 21, "w": 22, "x": 19, "y": 17, "z": 7}, 28)# Выполняем сборку индекса бинарного словаряasc.buildIndex(statusBuildIndex)# Выполняем сохранение индекса бинарного словаряasc.saveIndex("./dictionary/3-middle.asc", "", 128, statusIndex)
Я понимаю, что не каждый человек сможет обучить свой бинарный словарь, на это нужны текстовые корпуса и значительные вычислительные ресурсы. По этому ASC способная работать с одним лишь файлом ARPA в качестве основного словаря.
{ "ad": 13, "cw": 38120, "debug": 1, "threads": 0, "method": "spell", "alter": {"е":"ё"}, "asc-split": true, "asc-alter": true, "confidence": true, "asc-esplit": true, "asc-rsplit": true, "asc-uppers": true, "asc-hyphen": true, "mixed-dicts": true, "asc-wordrep": true, "spell-verbose": true, "r-text": "./texts/test.txt", "w-text": "./texts/output.txt", "upwords": "./texts/words/upp", "r-arpa": "./dictionary/alm.arpa", "r-abbr": "./dictionary/alm.abbr", "abbrs": "./texts/abbrs/abbrs.txt", "alters": "./texts/alters/yoficator.txt", "mix-restwords": "./similars/letters.txt", "goodwords": "./texts/whitelist/words.txt", "badwords": "./texts/blacklist/garbage.txt", "pilots": ["а","у","в","о","с","к","б","и","я","э","a","i","o","e","g"], "alphabet": "абвгдеёжзийклмнопрстуфхцчшщъыьэюяabcdefghijklmnopqrstuvwxyz", "embedding-size": 28, "embedding": { "а": 0, "б": 1, "в": 2, "г": 3, "д": 4, "е": 5, "ё": 5, "ж": 6, "з": 7, "и": 8, "й": 8, "к": 9, "л": 10, "м": 11, "н": 12, "о": 0, "п": 13, "р": 14, "с": 15, "т": 16, "у": 17, "ф": 18, "х": 19, "ц": 20, "ч": 21, "ш": 21, "щ": 21, "ъ": 22, "ы": 23, "ь": 22, "э": 5, "ю": 24, "я": 25, "<": 26, ">": 26, "~": 26, "-": 26, "+": 26, "=": 26, "*": 26, "/": 26, ":": 26, "%": 26, "|": 26, "^": 26, "&": 26, "#": 26, "'": 26, "\\": 26, "0": 27, "1": 27, "2": 27, "3": 27, "4": 27, "5": 27, "6": 27, "7": 27, "8": 27, "9": 27, "a": 0, "b": 2, "c": 15, "d": 4, "e": 5, "f": 18, "g": 3, "h": 12, "i": 8, "j": 6, "k": 9, "l": 10, "m": 11, "n": 12, "o": 0, "p": 14, "q": 13, "r": 14, "s": 15, "t": 16, "u": 24, "v": 21, "w": 22, "x": 19, "y": 17, "z": 7 }}
$ ./asc -r-json ./spell.json
Версия на Python
import asc# Для сборки используем все доступные ядраasc.setThreads(0)# Разрешаем исправлять регистр у слов в начале предложенийasc.setOption(asc.options_t.uppers)# Разрешаем выполнять сплитыasc.setOption(asc.options_t.ascSplit)# Разрешаем выполнять Ёфикациюasc.setOption(asc.options_t.ascAlter)# Разрешаем выполнять сплит слов с ошибкамиasc.setOption(asc.options_t.ascESplit)# Разрешаем удалять лишние пробелы между словамиasc.setOption(asc.options_t.ascRSplit)# Разрешаем выполнять корректировку регистров словasc.setOption(asc.options_t.ascUppers)# Разрешаем выполнять сплит по дефисамasc.setOption(asc.options_t.ascHyphen)# Разрешаем удалять повторяющиеся словаasc.setOption(asc.options_t.ascWordRep)# Разрешаем исправлять слова с замещенными буквами из других языковasc.setOption(asc.options_t.mixDicts)# Разрешаем загружать данные из ARPA так-как они есть, без перетокенизацииasc.setOption(asc.options_t.confidence)# Указываем алфавит используемый при обучении (алфавит всегда должен быть указан один и тот же)asc.setAlphabet("абвгдеёжзийклмнопрстуфхцчшщъыьэюяabcdefghijklmnopqrstuvwxyz")# Указываем список пилотных слов (слова которые состоят из одной буквы)asc.setPilots(["а","у","в","о","с","к","б","и","я","э","a","i","o","e","g"])# Устанавливаем похожие символы разных языковasc.setSubstitutes({'p':'р','c':'с','o':'о','t':'т','k':'к','e':'е','a':'а','h':'н','x':'х','b':'в','m':'м'})# Загружаем файл заранее подготовленный белый список словf = open('./texts/whitelist/words.txt')for word in f.readlines(): word = word.replace("\n", "") asc.addGoodword(word)f.close()# Загружаем файл заранее подготовленный чёрный список словf = open('./texts/blacklist/garbage.txt')for word in f.readlines(): word = word.replace("\n", "") asc.addBadword(word)f.close()# Загружаем файл суффиксов цифровых аббревиатурf = open('./output/alm.abbr')for word in f.readlines(): word = word.replace("\n", "") asc.addSuffix(word)f.close()# Загружаем файл общеупотребимые аббревиатуры, такие как (США, ФСБ, КГБ ...)f = open('./texts/abbrs/abbrs.txt')for abbr in f.readlines(): abbr = abbr.replace("\n", "") asc.addAbbr(abbr)f.close()# Загружаем файл со списком слов, которые всегда употребляются с заглавной буквы (названия, имена, фамилии...)f = open('./texts/words/upp/words.txt')for word in f.readlines(): word = word.replace("\n", "") asc.addUWord(word)f.close()# Устанавливаем альтернативную буквуasc.addAlt("е", "ё")# Загружаем файл со словами содержащими альтернативные буквы, которые используются всегда однозначно (синтаксис файла аналогичен списку похожих букв в разных алфавитах)f = open('./texts/alters/yoficator.txt')for words in f.readlines(): words = words.replace("\n", "") words = words.split('\t') asc.addAlt(words[0], words[1])f.close()def statusArpa(status): print("Read arpa", status)def statusIndex(status): print("Build index", status)# Выполняем загрузку данные языковой модели из файла ARPAasc.readArpa("./dictionary/alm.arpa", statusArpa)# Устанавливаем характеристики словаря (38120 слов полученных при обучении и 13 документов используемых в обучении)asc.setAdCw(38120, 13)# Устанавливаем параметры блока внутреннего эмбеддинга (не обязательно, влияет на точность подбора кандидатов)asc.setEmbedding({ "а": 0, "б": 1, "в": 2, "г": 3, "д": 4, "е": 5, "ё": 5, "ж": 6, "з": 7, "и": 8, "й": 8, "к": 9, "л": 10, "м": 11, "н": 12, "о": 0, "п": 13, "р": 14, "с": 15, "т": 16, "у": 17, "ф": 18, "х": 19, "ц": 20, "ч": 21, "ш": 21, "щ": 21, "ъ": 22, "ы": 23, "ь": 22, "э": 5, "ю": 24, "я": 25, "<": 26, ">": 26, "~": 26, "-": 26, "+": 26, "=": 26, "*": 26, "/": 26, ":": 26, "%": 26, "|": 26, "^": 26, "&": 26, "#": 26, "'": 26, "\\": 26, "0": 27, "1": 27, "2": 27, "3": 27, "4": 27, "5": 27, "6": 27, "7": 27, "8": 27, "9": 27, "a": 0, "b": 2, "c": 15, "d": 4, "e": 5, "f": 18, "g": 3, "h": 12, "i": 8, "j": 6, "k": 9, "l": 10, "m": 11, "n": 12, "o": 0, "p": 14, "q": 13, "r": 14, "s": 15, "t": 16, "u": 24, "v": 21, "w": 22, "x": 19, "y": 17, "z": 7}, 28)# Выполняем сборку индекса бинарного словаряasc.buildIndex(statusIndex)f1 = open('./texts/test.txt')f2 = open('./texts/output.txt', 'w')for line in f1.readlines(): res = asc.spell(line) f2.write("%s\n" % res[0])f2.close()f1.close()
P.S. Для тех, кто не хочет вообще ничего собирать и обучать, я поднял web версию ASC. Нужно также учитывать то, что система исправления опечаток это не всезнающая система и скормить туда весь русский язык невозможно. Исправлять любые тексты ASC не будет, под каждую тематику нужно обучать отдельно.