Русский
Русский
English
Статистика
Реклама

Anyks

ANYKS Spell-checker

20.09.2020 22:11:04 | Автор: admin
image

Здравствуйте, это моя третья статья на хабре, ранее я писал статью о языковой модели ALM. Сейчас, я хочу познакомить вас с системой исправления опечаток ASC (реализованной на основе ALM).

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

Список возможностей:


  1. Исправление ошибок в словах с разницей до 4-х дистанций по Левенштейну.
  2. Исправление опечаток в словах (вставка, удаление, замещение, перестановка) символов.
  3. Ёфикация с учётом контекста.
  4. Простановка регистра первой буквы слова, для (имён собственных и названий) с учётом контекста.
  5. Разбиение объединённых слов на отдельные слова, с учётом контекста.
  6. Выполнение анализа текста без корректировки исходного текста.
  7. Поиск в тексте наличия (ошибок, опечаток, неверного контекста).


Поддерживаемые операционные системы:


  • 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


Установка
ASC
$ 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


Обучение
ASC

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


Тестирование
ASC

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 обучение на грязных данных. В отрытом доступе найти текстовые корпуса без ошибок и опечаток практически не реально. Исправлять руками терабайты данных не хватит жизни, а работать с этим как-то надо.

Принцип обучения который предлагаю я


  1. Собираем языковую модель на грязных данных
  2. Удаляем все редко встречающиеся слова и N-граммы в собранной языковой модели
  3. Добавляем одиночные слова для более правильной работы системы исправления опечаток.
  4. Собираем бинарный словарь

Приступим


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

Сборка корпуса с помощью ALM
collect.json
{"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)

Таким образом, мы собираем все наши корпуса

Прунинг собранного корпуса с помощью ALM
prune.json
{    "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)


Объединение собранных данных с помощью ALM
merge.json
{    "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)


Обучение языковой модели с помощью ALM
train.json
{    "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)


Обучение spell-checker ASC
train.json
{"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 в качестве основного словаря.

Пример работы
spell.json
{    "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 не будет, под каждую тематику нужно обучать отдельно.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru