Всем привет! Меня зовут Владимир Ганзюк

Работаю инженером НСИ и изучаю для себя 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, а его сложность О(n^3), код написан больше на читаемость, а не оптимизацию. 1000 позиций Natasha обрабатывала около 1 минуты, в то время как Pymorph справился с объемом в два раза больше за двое меньшее время. Это так, небольшое отступление, если кто столкнется с подобной ситуацией с выбором библиотеки.

Изменение падежа слова, например, может понадобиться для переноса транспортируемой среды в формат «Транспортировка» + среда для другого атрибута.

Вывод:

Интересно узнать мнение других специалистов. Возможно, кто-то воспользовался искусственным интеллектом в подобной ситуации. Надеюсь, кому-то поможет данный способ.


Ганзюк Владимир, инженер нормативно-справочной информации (НСИ)

Комментарии (13)


  1. CBET_TbMbI
    22.07.2024 22:13
    +9

    Как расслабиться инженеру на работе при помощи Python?

    Мой вариант: запрограммировать массажное кресло.


    1. ganzyukvolodya Автор
      22.07.2024 22:13

      Шикарный вариант!) А рядом бы ещё пшеничный смузи


      1. randomsimplenumber
        22.07.2024 22:13
        +2

        пшеничный смузи

        Виноградный тоже неплохо ;)


  1. Shaitanbabai
    22.07.2024 22:13
    +2

    Кейс прекрасный. Ад в номенклатурных справочниках - постоянная проблема. Как-то на одном заводе (на самом деле почти на каждом так) разбирались с неликвидами. А их так процентов 30 запасов, а в деньгах на 1М$ было. Вот то же самое, ага. Так что этот кейс - это "прям деньги". Респект.

    Хотя есть небольшая индульгенция у тех, кто "косячит" - они обязаны вводить данные в базу в строгом соответствии с первичным документами. И если ДТ в них указано как "солярка", то в базе данных нужна сшивка-интерпретатор, сохраняющая и бумажные, и "нормализованные" наименования.

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

    Например, то же ДТ, ставшее ТД. Тут не хватает агрегирующего классификатора всех видов топлива. Тогда уровнем ниже был бы дизель(ное), бензин, итд. А на третьем уровне его марка и/или определяющее свойство. На четвертом ГОСТ, ТУ и пр. Т.е. нужно не просто исправить морфологию внутри фразы, но и сделать ее декомпозицию. А потом, если нужно, снова композицию в корректном виде. Но, как правило для решения конкретной бизнес-задачи достаточно второго уровня.


    1. ganzyukvolodya Автор
      22.07.2024 22:13

      Спасибо!)
      Понимаю и полностью согласен, я прям прочувствовал всю боль в "Ад в номенклатурных справочниках - постоянная проблема".


  1. sshmakov
    22.07.2024 22:13

    Не лишний раз упомянуть автора библиотеки pymorphy2 от @kmike


  1. 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":
    


    1. ganzyukvolodya Автор
      22.07.2024 22:13
      +1

      Хорошее замечание по поводу библиотеки. Да, я использую pymorphy3, это стоит упомянуть.
      Как я понял, проект pymorphy2 был заброшен автором ещё в 20-ом году, на github это обсуждали.

      Да, декоратор должен дать прирост в скорости

      Приятно получить такую критику, в которой все по факту. Спасибо!)


      1. Andrey_Solomatin
        22.07.2024 22:13

        Не нужен там декоратор, просто уберите повторения for word in words:


    1. ganzyukvolodya Автор
      22.07.2024 22:13

      Только что проверил, как и ожидалось, декоратор дал прирост, но совсем немного в 2-3 секунды!) Из выгрузки, которая описывалась в статье.
      Думаю результат будет более заметным, если выгрузка будет объемнее.


  1. Luboff_sky
    22.07.2024 22:13

    А как фильтрануть "лампа гОлАгеговая". Или "ОсСциЛограф"?)))


    1. ganzyukvolodya Автор
      22.07.2024 22:13

      К сожалению, от опечаток никто не застрахован, особенно инженера на производстве, когда все делается на скорую руку.
      Библиотека pymorph3 слова с ошибками: "голагеговая" и "оссцилограф" определяет как надо - прилагательное и существительное соответственно.

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

      Если взять к примеру опечатку из выгрузки "Газы реакции/ котловая вода", то именно слово вместе со слэшем "реакции/" pymorph3 не сможет определить и выдаст результат, как UNKN, то есть токен не удалось распознать.
      В этом случае перебор будет отличаться от входящего значения, и при сравнении измененных позиций сразу станет понятно (сразу скажу таких случаев было немного)

      Лично я просматривал измененные позиции, что позволило вдобавок ещё навести красоту и отредактировать рабочую среду.


  1. ABATAPA
    22.07.2024 22:13

    Скорость работы алгоритма составила 29 секунд.

    Давно скорость измеряется в секундах? Тогда уж "время работы алгоритма — 29 секунд."