Как стать автором
Обновить

Мечты о хорошем спеллчекере без лишних заморочек

Время на прочтение13 мин
Количество просмотров4.3K

Работая с текстами, часто приходится сталкиваться с проблемой грязных данных. Опечатки, орфографические ошибки, случайный CAPS LOCK. И это ещё не затрагивая всю огромную беду с непопулярными жаргонизмами и локальными аббревиатурами/названиями. Практически любой даже самый мощный и чудесный алгоритм анализа, не будучи предварительно подготовленным, на моменте обнаружения фразы «две однёрки» пошатнётся и икнёт. А если таких фраз в тексте будет много, то алгоритм просто сойдёт с рельсов, и полезный эффект нивелируется.

— ..И что сказал?

— Нецензурную брань, ошибки и случайный капс пропустить?

— Да.

— [' '].

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

            При очищении текстов, конечно же, хочется обойтись без потерь данных. А с этим есть, может быть, основная загвоздка. При применении спеллчекеров всегда есть риск лишиться специфических слов. Обычно проблемы возникают при анализе текстов, содержащих:

Ø  жаргон, сленг, просторечия;

Ø  редкие имена и фамилии;

Ø  названия и наименования (особенно на фоне постоянного обновления языка);

Ø  аббревиатуры и термины, редко (или не) употребляемые вне области.

            Именно такие слова обычно включают в себя значительную часть смысловой нагрузки текстов. Фактически, при использовании некачественного спеллчекера мы обрекаем выборку на потенциальную смысловую ущербность: алгоритмы разберутся и поправят наши правильные слова на свои, ещё более правильные. Можно попытаться засунуть в алгоритм вообще все существующие слова, но это чревато ошибками вроде «Барье» → «барье» как название географического объекта (Люблянское барье, болотный массив в Словении), вместо «Дарье» как дательный падеж распространённого женского имени. Такие ошибки не сделают данные лучше.

            Исходя из этих соображений, я провела небольшое исследование альтернатив для исправления ошибок. Мне требовалось выяснить их потенциал для построения на их основе качественного дополняемого алгоритма, и сейчас я хочу поделиться результатами. При анализе подходов к решению данной задачи я в первую очередь рассматривала известные мне и применяемые в задаче проверки написания библиотеки языка Python:

Ø  Textblob

Ø  Pyenchant

Ø  Jamspell

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

import jamspell
g_corrector = jamspell.TSpellCorrector()
g_corrector.LoadLangModel('ru_small.bin')  # файл с моделью
def clear_text(text):
	return g_corrector.FixFragment(text)

И на вход ему можно подавать целый текст. При применении Pyenchant и Textblob, видимо, предполагается подавать данные по одному слову. По крайней мере, я так и делала. К тому же я поместила туда функцию solver, которая принимает список из предположений модели (для одного слова) и выбирает из них наиболее похожее на правду, используя модуль для нечётких сравнений строк rapidfuzz. Вообще-то у rapidfuzz есть некоторые возможности для ускорения, но в моей конфигурации кода (с пословной передачей слов) я не придумала, как их эффективно задействовать. Из-за этого применение solver очень замедляло процесс обработки, и к тому же, как мне показалось, не давало значимого прироста в качестве — модули и так проводят подобный анализ, и первое из предположений обычно самое близкое. В итоге я отказалась от использования solver , но в коде оставила, чтобы поэкспериментировать с этим можно было потом.

            Код для Textblob выглядел следующим образом.

from textblob.en import Spelling
from rapidfuzz import fuzz
spelling = Spelling('/home/iris/Repos/venviroments/orphograpy_reasearch/data/dicts/freq_dict/plain_dict.txt')


def __solver(word, suggestions):
    qualities = [fuzz.ratio(word, s) for s in suggestions]
    if len(qualities) > 0:
        return suggestions[qualities.index(max(qualities))][0]
    return word


def clear_word(word, use_solver=True):
    suggestions = spelling.suggest(word)
    if suggestions.count(word) > 0:
        return word
    if use_solver:
        return __solver(word, suggestions)
    else:
        return suggestions[0][0]

	Код алгоритма для проверки работы Pyenchant у меня был такой.

from rapidfuzz import fuzz
import enchant

g_dictionary = enchant.Dict("ru_RU")


def __solver(word, suggestions):
    qualities = [fuzz.ratio(word, s) for s in suggestions]
    if len(qualities) > 0:
        return suggestions[qualities.index(max(qualities))]
    return word


def clear_word(word, use_solver=True):
    if not g_dictionary.check(word):
        suggestions = g_dictionary.suggest(word)
        if use_solver:
            return __solver(word, suggestions)
        elif len(suggestions) > 0:
            return suggestions[0]
    return word

Здесь сразу отмечу возможные улучшения алгоритмов. Во-первых, для Textblob можно написать алгоритм, который будет исправлять целый текст — его тогда надо передавать в качестве параметра при инициализации переменной textblob. Однако в дальнейшем мной выполнялись тесты, связанные с исправлением одного слова за раз и представленные выше алгоритмы были вполне достаточны. Также отмечу, что при использовании solver вместо fuzz.ratio() в методе решения можно выбрать другую метрику близости или расстояния, а можно даже написать свою реализацию метрики, которая будет учитывать схожесть отдельных букв в конкретной задаче. Скажем, при распознавании текста с изображений случайная замена Б→Ь гораздо более вероятна, чем, например, Б→П или Б→Ж, так что у меня имеется гипотеза о том, что если назначить им сообразный вес при вычислении метрики, то можно ожидать прироста качества в некоторых случаях, но в эти детали я не стала углубляться.

            Гораздо интереснее было оценить качество существующих алгоритмов на каких-нибудь сложных данных. Для примера у меня были датасеты имён и фамилий на русском языке. Эти данные выглядят примерно так.

ID

Surnames

PeoplesCount

100093

Иванковский

176

100094

Иванник

247

100095

Иванников

9000

100096

Иванникова

11000

100097

Иванничкин

0

100098

Иванничкина

0

100099

Иваннов

195

100100

Иваннова

118

100101

Иванов

562000

100102

Иванова

689000

… 

Здесь в колонке PeoplesCount указано количество людей с соответствующим именем или фамилией. Мне неизвестно, как были получены эти данные, но я решила использовать их для отсеивания редких имён из выборки. На основе этих данных с помощью следующего кода построила что-то типа тестовой выборки — в имена вносится контролируемое количество ошибок трёх типов: 1) замена случайной буквы на другую; 2) вставка буквы в случайное место; 3) удаление случайной буквы из имени, если только оно не короче пяти символов.

import random
import os
import pandas as pd

letters = 'ёйцукенгшщзхъфывапролджэячсмитьбю'


def commit_metamorph(word, k):  # функция для искажения буквы
	ind = random.randint(0, len(word))
	letter = letters[min(random.randint(0, len(letters)), len(letters) - 1)]
	# k - тип искажения
	# 0 -> изменение, 1 -> вставка, 2 -> удаление
	if k == 0:
		return word[:min(ind, len(word)-1)] + letter + word[ind+1:]
	if k == 1:
		return word[:ind] + letter + word[ind:]
	if k == 2:
		if len(word) < 5:
			return word
		return word[:min(ind, len(word)-1)] + word[ind+1:]


def generate_spoiled_data(df_data,
                          morph_list,
                          max_one_name=5,
                          num_substitution=1):
	if 'word' not in df_data.columns:
		print('No words in dataframe')
		return None

	true_names = df_data['word'].tolist()
	true_names_labels = []

	num_sets = len(morph_list)
	all_spoiled_names = []
	for i in range(num_sets):
		all_spoiled_names.append([])
	for name in true_names:
		name = name.lower()
		num_for_name = random.randint(0, max(max_one_name, 0))
		for i in range(num_for_name):
			true_names_labels.append(name.capitalize())
				for k_set in morph_list:
				spoiled_name = name
				for j in range(max(num_substitution, 1)):
					ind = random.randint(0, len(k_set)-1)
					k = k_set[ind]
					spoiled_name = commit_metamorph(spoiled_name, k)
				all_spoiled_names[morph_list.index(k_set)].append(spoiled_name.capitalize())

	df_test_data = pd.DataFrame({'word': true_names_labels})
	for i in range(num_sets):
		df_test_data[i+1] = all_spoiled_names[i]
	return df_test_data

Хотя вообще-то проверять орфографию в фамилиях выглядит затеей, заранее обречённой на полный провал. Дело в том, что многие фамилии (ещё когда самый эффективный спеллчекер был учителем русского языка и литературы) писались чёрт-те как. Многочисленные ошибки в паспортах породили на свет великое множество самых разнообразных фамилий, и на текстуальном уровне невозможно определить, когда эта ошибка старая и нужная, а когда — свежая, и надо её убрать.

            Но для анализа модулей далее нужно было подготовить алгоритмы для них. Их код уже был выше, однако к нему нужно добавить подключаемые русскоязычные модели. Для тестирования Jamspell была взята минимальная модель анализа русского языка, предлагаемая разработчиками модуля с настоятельной рекомендацией обучить свою (модель можно найти тут https://github.com/bakwc/JamSpell по ссылке на ru.tar.gz в конце README.md).

            Тестирование Pyenchant проводилось с подключением Hunspell-словаря, собранного и постоянно пополняемого экспертами. Эта модель не специфицирована под имена и фамилии, и в ней отсутствует буква «ё», зато она находится в открытом доступе (ссылка https://code.google.com/archive/p/hunspell-ru/).

            Для проверки Textblob искала готовые модули и решения для русского языка, однако ничего подобного не было. В итоге сымитировала обучение на корпусе, подобное тому, которое можно найти здесь https://stackabuse.com/spelling-correction-in-python-with-textblob/. Правда, надо оговорить, что там оно всё-таки на английском и может использовать сведения об английском языке, поставляемые вместе с библиотекой в файлах en-morphology.txt, en-context.txt и т.д., хотя мне и неизвестно, используются ли они на самом деле при работе спеллчекера. Итак, для имитации я взяла частотный словарь русского языка, собранный экспертами в 2011 году на основании НКРЯ (ссылка http://dict.ruslang.ru/freq.php), и выбрала оттуда в новый файл только леммы и количество слов в корпусе, и только для слов, которые не содержали пробелы и дефисы («административно-правовой»), а кроме того собрала количества слов, встречающихся по нескольку раз в одном написании: это были слова, размеченные на разные части речи («а» как союз и «а» как междометие). В итоге, теоретически, должен был получиться файл, содержащий примерно то, что получилось бы, если бы алгоритм прошёлся по корпусу, как это было сделано в упомянутой в начале абзаца статье.

            Код всей этой прелести я разместила на github, в репозитории https://github.com/IrizGem/Spellchecker_researchs, в папке orphography_research.

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

            Для проведения испытаний были сгенерированы три выборки: с чистыми добавлениями, заменами и удалениями соответственно — а также четвёртая выборка со случайными изменениями всех трёх типов. Кроме того, алгоритмы запускались на чистых данных, чтобы посмотреть их в работе, так сказать, без шума и оценить их собственное искажение. Алгоритмы тестировались последовательно на одних и тех же данных, так что зависимость результатов от разницы случайных выборок снижена (но всё ещё присутствует, так как от выборки к выборке может меняться количество простых и сложных имён для каждой из моделей, оценить которое довольно сложно).

            Первый прогон всех трёх моделей был на 4187 образцах, сгенерированных с двумя изменениями, проводился последовательнои занял порядка 80 минут. Из них алгоритм на Jamspell работал 41 секунду, алгоритм Pyenchant — около 24 минут, а Textblob — оставшиеся 55. На скриншоте ниже показан итог для трёх алгоритмов. Были получены метрики качества по каждой выборке (только изменения, только вставки, только удаления, смеси из всех трёх типов и чистые данные).

Второй прогон производился аналогично первому, но на выборке из 4183 образцов всего с одним изменением (что, пожалуй, ближе к возможным реальным данным), и длился примерно столько же, за исключением того, что Jamspell, по неизвестной причине, отработал выборку за 5 секунд (что выглядит подозрительно быстро, ведь выборка практически та же), а на отработке Textblob расчёты приостанавливались на ночь, и число прогресс-бара получилось более 15 часов, хотя по факту считалось оно, как и в первый раз, где-то около часа.

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

1.     Касательно быстродействия. Алгоритм Pyenchant работает быстрее алгоритма Textblob примерно в два раза. Но при сравнении с алгоритмом Jamspell, они оба выглядят совершенно неконкурентно по скорости. Собственно, кажется, где-то в репозитории Jamspell было сравнение скоростей с другими алгоритмами (в том числе с Hunspell), из которого следует то же самое.

2.     Относительно качества для разных ошибок. Все три модели имеют заметный прирост качества на выборке с чистыми вставками, а на выборках с изменениями и удалениями их качество несколько ниже — где-то на 20-25%. Это может быть связано с тем, что только при добавлении правильные буквы остаются в слове. Технически, задача подобрать букву для имени или фамилии и должна быть сложнее. Допустим, существуют фамилии «СтирижнЯков» и «СтрижнИков», приходит слово «СтрижнХков», и ничто не подсказывает, какой из двух вариантов предпочтительнее, в то время как с добавлением буквы нужная буква в слове бы осталась, и выбор был бы обусловлен ею.

3.     Проверка собственных искажений. Модели запускались на чистых данных, чтобы оценить количество экземпляров выборки, которые в моделях гарантированно отсутствуют. Если чистая фамилия алгоритмом была модифицирована, то значит она ему неизвестна — он заменил её на другое, близкое по написанию, слово. Притом если чистая фамилия алгоритмом модифицирована не была, то это ещё не значит, что она известна: она просто не была исправлена. Процент собственных искажений будет равен 100% минус процент правильно распознанных чистых слов (пятое число на скриншоте). Фактически, в процент правильно распознанных чистых слов входят известные фамилии и неизвестные фамилии, для которых в модели нет критически похожих слов (и поэтому они не были исправлены моделью), а в процент собственного искажения — неизвестные фамилии, для которых есть критически похожие слова. Иными словами, это данные, которыми мы гарантированно можем обогатить модель (но не все такие данные).

4.     Textblob. Алгоритм Textblob хуже всего справился с испытаниями: результат на смешанных типах ошибок для 2-х изменений равен 7.5%, для 1-ого изменения — где-то 15%. Из правильных фамилий (чистые данные) она не искажает где-то 65%, то есть примерно 35% процентов всей выборки гарантированно неизвестно модели. Ещё, по видимости, он существенно хуже справляется со случаем изменения буквы, 0.4% для 2-х случаев и 0.18% для 1‑го, при том удаление имеет 10% и 9% соответственно. Кстати, отчего-то эти результаты не повысились, а даже немного упали на эксперименте с 1-ой правкой вместо 2-х. Вообще из результата алгоритма можно заключить, что либо моя имитация обучения не сработала, и для использования Textblob необходимо на самом деле обучать модель на корпусе (если там результат будет лучше, то она действительно хранит какие-то дополнительные данные), либо перенос качества Textblob с английского на другой язык стоит выполнять ещё более внимательно, прикладывая туда файлы морфологии языка и т.д. (тогда её использование с русским языком будет затруднено).

5.     Jamspell. Алгоритм Jamspell показал наибольшее качество на тестовых данных. Точность на смешанных типах ошибок для 2-х правок 33%, для 1-ой — 61,5%. Процент собственных искажений модели держится на уровне 1,5%. С одной стороны, это можно интерпретировать как хороший признак: возможно, модель настолько хорошо понимает язык, что не исправляет даже странные и не встречавшиеся ранее фамилии. С другой стороны, можно из такого маленького процента сделать вывод, что у нас не такой большой запас улучшения качества, и, возможно, мы близки к пределу модели. Но вообще-то, суммируя всё, что мы знаем о модели, надо вспомнить, что это минимальная модель, которую разработчик настоятельно советует использовать только для первичных тестирований. Итак, скорее всего, модель в принципе не очень-то много слов знает, а такое низкое собственное искажение отражает лишь способность модели не исправлять то, что выглядит совершенно непонятно. Тем не менее, сочетание такого низкого собственного искажения с 60% верных предположений на смешанной выборке выглядят довольно вдохновляюще для дальнейшей работы с данной библиотекой. И, кстати, это без использования контекста, то есть Jamspell ещё дал остальным немного форы.

6.     Pyenchant. В свою очередь, алгоритм на Pyenchant справился немного хуже алгоритма на Jamspell, однако есть смысл их сравнивать. Качество на смешанной выборке: 27% для 2-х правок и 49% для 1-ой. Почти по всем результатам они отличаются с Jamspell где-то на 10-15% с отставанием Pyenchant. При этом собственное искажение этой модели было на уровне 29% — то есть где-то такой процент фамилий можно гарантированно добавить в Hunspell-словарь, который используется моделью. Такой процент собственного искажения вызван ещё и тем, что во взятом нами словаре довольно много редких слов, в том числе таких, которые похожи на имена или фамилии из тестовых данных.

7.     Возможности дообучения. Чтобы улучшить алгоритм с Textblob, нужен хороший корпус, время и много разбираться с переносимостью качества библиотеки с английского на русский язык (и пока у меня нет фактических свидетельств, что оно вообще переносимо). Чтобы улучшить алгоритм с Jamspell, нужен просто хороший корпус и время. Но для сохранения качества подобных алгоритмов нужно будет с определённой периодичностью дообучать модель, пополняя корпус новыми текстами. На этом фоне особенно интересно, что «дообучить» Pyenchant можно фактически вручную. Pyenchant, а точнее Hunspell в его начинке, очень простой и прозрачный инструмент. Чтобы внести новые слова в модель нужно добавить их в .dic файл, и единственная сложность тут — корректно определить префиксы, что вполне реализуемо. Таким образом, можно добавлять и аббревиатуры, и термины, используя лишь знания эксперта области, а не поддерживая целый корпус.

            Итак, по итогу этого небольшого исследования я выяснила некоторые перспективы для создания хорошего спеллчекера для русскоязычных текстов на Python. Из трёх я выделила для себя два возможных пути: дообучать Jamspell или дополнять Hunspell-словарь.

            «Из коробки» лучше себя показывает Jamspell, и работа с ним предполагает сбор корпуса, но, к тому же, у него есть дополнительные технические трудности, поскольку для оптимизации ресурсов Jamspell написан на C++ и задействует swig (пакет для использования C++-штук из модулей на других языках, в том числе на Python). Естественно, можно этих сложностей избежать, и найти другую похожую библиотеку с похожим быстродействием и качеством или найти способ установить требуемый пакет на корпоративный компьютер, но, тем не менее, эта проблема пока не решена.

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

            Ну, а про хороший спеллчекер без заморочек я всё выяснила — такого, вероятно, просто не бывает.

Теги:
Хабы:
Всего голосов 2: ↑2 и ↓0+2
Комментарии13

Публикации

Истории

Ближайшие события

7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань