Всем привет! Меня зовут Владимир Ганзюк
Работаю инженером НСИ и изучаю для себя C#, но не сталкиваясь с Python, наткнулся я как-то случайно на одну очень интересную библиотеку Pymorhp.
Pymorph – морфологический анализатор для русского языка, использует словари из OpenCorpora. Исходный код можно получить на github. Документация к библиотеке написана достаточно хорошо.
Предыстория:
Попивая чаек в поте лица, я получил сообщение от руководителя, что на следующие два спринта мне необходимо проверить и унифицировать один из атрибутов на миллиард позиций.
Ладно, на самом деле попивал я кофе, а позиций было и вправду огромное количество. К сожалению, все это можно сделать только вручную (так думали все).
Суть:
В выгрузке из нашей системы имеется рабочая среда, естественно, таких сред может быть несколько. Перечисление сред изначально было через «;», через какое-то время кто-то подумал, что неплохо будет всё-таки использовать «,». Это исправить можно и в Excel, но наименование рабочей среды должно начинаться с существительного + прилагательного. Вот несколько примеров:
Топливо дизельное;
Газ углеводородный, фракция пропан-пропиленовая, газ природный;
Фракция бутан-бутиленовая.
В системе рабочие среды были внесены некорректно, к примеру: «Бутан бутиленовая фракция», «Бензин; вода», «Газ углеводородный, сухой газ».
Как мы помним правило: существительное должно стоять на первом месте, и запятая должна разделять элементы перечисления. Поэтому необходимо было произвести унификацию.
За вечер написал костыль (функцию) sorting_dictionary:
def sorting_dictionary(dictionary):
sorted_dic, dic, result_words = [], [], []
for index in dictionary.keys():
sentence_in_cell = dictionary[index].split(", ")
for words in sentence_in_cell:
words = words.split()
for word in words:
p = morph.parse(word)[0]
if p.tag.POS == "NOUN":
sorted_dic.append(word)
for word in words:
p = morph.parse(word)[0]
if p.tag.POS == "PRED":
sorted_dic.append(word)
elif p.tag.POS == "PREP":
sorted_dic.append(word)
for word in words:
p = morph.parse(word)[0]
if p.tag.POS == "ADJF":
sorted_dic.append(word.lower())
for word in words:
p = morph.parse(word)[0]
item_list = ['CONJ', 'PRTF']
for item in item_list:
if item == p.tag.POS:
sorted_dic.append(word)
for word in words:
p = morph.parse(word)[0]
item_list = ["VERB", "INFN"]
for item in item_list:
if item == p.tag.POS:
sorted_dic.append(word)
for word in words:
p = morph.parse(word)[0]
if p.tag.POS == None:
sorted_dic.append(word)
words2 = " ".join(sorted_dic)
result_words.append(words2)
words2 = " "
sorted_dic.clear()
res_join = ", ".join(result_words)
dic.append(upcase_first_letter(res_join))
result_words.clear()
return dic
Вкратце, функция принимает на вход словарь, в который уже заранее были внесены все значения из файла Excel с использованием библиотеки openpyxl и «;» была заменена на «,».
Часть речи слова получаем через атрибут POS: p.tag.POS. Если запрашиваемая характеристика для данного тега не определена, то возвращается None. Обозначения для граммем, можно получить тут:
Эта функция возвращает уже отсортированный вариант словаря.
Результаты:
В выгрузке по цеху было 2193 позиций, которые необходимо было каждую вручную проверить.
Функция изменила 565 позиций, а это значит, что 1628 позиций уже отсеялись как правильные. В основном это легкие по типу: «Азот», «Бензин нестабильный», которая Pymorph определяет без проблем.
Из 565 измененных позиций 121 оказалась некорректной, например: «Раствор свежий щелочи», хотя правильный вариант - «Раствор щелочи свежий». Также есть проблема со скобками, к примеру: «Смесь газопродуктовая (бензин, ВСГ)», функция возвращает, как «Смесь (бензин газопродуктовая, ВСГ)».
Скорость работы алгоритма составила 29 секунд. Неплохо, да? Но у данной библиотеки иногда возникают сложности с определением части речи.
К примеру, если отойти от сред и разобрать предложение по словам «Мама мыла раму», то именно слово «мыла» оно определит, как существительное единственного числа с более высокой вероятностью, чем глагол. Да, у данной библиотеки ещё есть score – оценка вероятности того, что данный разбор правильный. Цитата из документации к библиотеке «то, как нужно разбирать слово, зависит от соседних слов; pymorphy2 работает только на уровне отдельных слов»
Также очень много позиций «Вакуумный газойль; Дизельное топливо», где «газойль» и «топливо» должны стоять на первом месте, функция вернула, как «Газойль вакуумный, топливо дизельное». Даже на удивление нашел такую среду из выгрузки: «27% водный раствор амина, Н2S – до 10% масс., азот», функция вернула абсолютно правильный вариант «Раствор амина водный 27%, Н2S – до 10% масс., азот»
Данная библиотека также в состоянии изменять падеж слова. Например, в библиотеке Natasha, которую я тоже пробовал, определить падеж можно, а вот изменить его, к сожалению, не получилось. Да и в отличие от Pymorph, Natasha очень медленная, т.к. Yargy реализует алгоритм Early parser, а его сложность , код написан больше на читаемость, а не оптимизацию. 1000 позиций Natasha обрабатывала около 1 минуты, в то время как Pymorph справился с объемом в два раза больше за двое меньшее время. Это так, небольшое отступление, если кто столкнется с подобной ситуацией с выбором библиотеки.
Изменение падежа слова, например, может понадобиться для переноса транспортируемой среды в формат «Транспортировка» + среда для другого атрибута.
Вывод:
Интересно узнать мнение других специалистов. Возможно, кто-то воспользовался искусственным интеллектом в подобной ситуации. Надеюсь, кому-то поможет данный способ.
Ганзюк Владимир, инженер нормативно-справочной информации (НСИ)
Комментарии (13)
Shaitanbabai
22.07.2024 22:13+2Кейс прекрасный. Ад в номенклатурных справочниках - постоянная проблема. Как-то на одном заводе (на самом деле почти на каждом так) разбирались с неликвидами. А их так процентов 30 запасов, а в деньгах на 1М$ было. Вот то же самое, ага. Так что этот кейс - это "прям деньги". Респект.
Хотя есть небольшая индульгенция у тех, кто "косячит" - они обязаны вводить данные в базу в строгом соответствии с первичным документами. И если ДТ в них указано как "солярка", то в базе данных нужна сшивка-интерпретатор, сохраняющая и бумажные, и "нормализованные" наименования.
Решить ряд идейных проблем кода думаю можно было бы, именно, что подключив к его работе ИИ (промт-инжиниринг), сделав "нейросотрудника", верно интерпретирующего не только части речи, но и общеупотребительные формулировки, учитывая значимость конкретного слова-ключа для идентификации.
Например, то же ДТ, ставшее ТД. Тут не хватает агрегирующего классификатора всех видов топлива. Тогда уровнем ниже был бы дизель(ное), бензин, итд. А на третьем уровне его марка и/или определяющее свойство. На четвертом ГОСТ, ТУ и пр. Т.е. нужно не просто исправить морфологию внутри фразы, но и сделать ее декомпозицию. А потом, если нужно, снова композицию в корректном виде. Но, как правило для решения конкретной бизнес-задачи достаточно второго уровня.
ganzyukvolodya Автор
22.07.2024 22:13Спасибо!)
Понимаю и полностью согласен, я прям прочувствовал всю боль в "Ад в номенклатурных справочниках - постоянная проблема".
CrazyElf
22.07.2024 22:13+3У меня есть замечание и предложение:
Библиотека из вашего кода всё же называется
pymorphy2
, аPymorph
- это что-то древнее и давно не поддерживаемое, вы явно используете не эту библиотеку. Хорошо бы привести текст в соответствие. И более того, библиотекаpymorphy2
уже тоже несколько лет как заброшена, вместо неё теперьpymorphy3
, рекомендуется использовать её.Если у вас часто встречаются одинаковые слова, то советую попробовать вынести
morph.parse(word)[0]
в отдельную функцию, на которую повесить кеширующий декоратор. Для больших текстов у меня это давало ускорение раз в 10. Но тут сильно зависит от повторяемости слов. Что-то такое:
from functools import lru_cache @lru_cache def morph(word): return morph.parse(word)[0]
Вернее, вы, кажется, используете только
.tag.POS
от этого, имеет смысл сразу его и возвращать и кешировать тогда:@lru_cache def get_pos(word): return morph.parse(word)[0].tag.POS ... pos = get_pos(word) if pos == "PRED":
ganzyukvolodya Автор
22.07.2024 22:13+1Хорошее замечание по поводу библиотеки. Да, я использую pymorphy3, это стоит упомянуть.
Как я понял, проект pymorphy2 был заброшен автором ещё в 20-ом году, на github это обсуждали.
Да, декоратор должен дать прирост в скорости
Приятно получить такую критику, в которой все по факту. Спасибо!)Andrey_Solomatin
22.07.2024 22:13Не нужен там декоратор, просто уберите повторения
for word in words:
ganzyukvolodya Автор
22.07.2024 22:13Только что проверил, как и ожидалось, декоратор дал прирост, но совсем немного в 2-3 секунды!) Из выгрузки, которая описывалась в статье.
Думаю результат будет более заметным, если выгрузка будет объемнее.
Luboff_sky
22.07.2024 22:13А как фильтрануть "лампа гОлАгеговая". Или "ОсСциЛограф"?)))
ganzyukvolodya Автор
22.07.2024 22:13К сожалению, от опечаток никто не застрахован, особенно инженера на производстве, когда все делается на скорую руку.
Библиотека pymorph3 слова с ошибками: "голагеговая" и "оссцилограф" определяет как надо - прилагательное и существительное соответственно.
Что касается орфографии, к счастью, в Excel есть словарь, который можно запустить для проверки.
Если взять к примеру опечатку из выгрузки "Газы реакции/ котловая вода", то именно слово вместе со слэшем "реакции/" pymorph3 не сможет определить и выдаст результат, как UNKN, то есть токен не удалось распознать.
В этом случае перебор будет отличаться от входящего значения, и при сравнении измененных позиций сразу станет понятно (сразу скажу таких случаев было немного)
Лично я просматривал измененные позиции, что позволило вдобавок ещё навести красоту и отредактировать рабочую среду.
ABATAPA
22.07.2024 22:13Скорость работы алгоритма составила 29 секунд.
Давно скорость измеряется в секундах? Тогда уж "время работы алгоритма — 29 секунд."
CBET_TbMbI
Мой вариант: запрограммировать массажное кресло.
ganzyukvolodya Автор
Шикарный вариант!) А рядом бы ещё пшеничный смузи
randomsimplenumber
Виноградный тоже неплохо ;)