Привет, Точка на связи! Аналитик Никитин Александр и Head of ML Андрей Румянцев разобрались как с помощью машинного обучения смерджить несколько наборов данных из открытых источников и не сойти с ума. Open data, TF-IDF, faiss, pgvector, трансформеры и удивительное завершение нашего приключения — всё это под катом.

Точка делает мир удобным для бизнеса. Наша главная цель — создавать качественный сервис, вызывающий уважение и преданность предпринимателей на долгие годы. Чтобы быть эффективнее и привлекательнее для клиентов, Точка использует большое количество внешних и внутренних данных.

Мы используем внешние данные как источник знаний о мире и себе в этом мире. Это могут быть как данные, полученные от партнеров, так и открытые данные из внешних источников. О последних и пойдет речь.

Открытые данные

Концепция открытых данных подразумевает, что определенные данные должны быть свободно доступны для использования и дальнейшей републикации без ограничений авторского права, патентов и других механизмов контроля. Открытые данные могут публиковаться различными источниками. Наибольший массив данных формируется государственными органами и службами:

  • Федеральная налоговая служба России (ЕГРЮЛ и ЕГРИП, специальные реестры, сведения о блокировках счетов, информационный ресурс бухгалтерской отчетности).

  • Федеральное казначейство (закупки).

  • Министерство экономического развития (Федресурс).

  • Верховный суд РФ (арбитражные дела).

  • Федеральная служба судебных приставов России (исполнительные производства).

  • и т.д.

В теории звучит неплохо. Бери и пользуйся. Однако, на практике нельзя просто так взять, скачать и начать использовать набор данных. Разные форматы файлов, структура файлов и верстка страниц постоянно меняется, нередко скудная документация и иногда недоступность самих данных для скачивания — всё это превращает добычу открытых данных в целое приключение. Чаще всего из разряда «Let’s go. In and out. 20 minutes adventure». Эта тема боль требует отдельной статьи / цикла статей для ее раскрытия.

Давайте представим, что нам удалось справиться со всеми трудностями по затягиванию данных, не уронить ни одного сайта (но это не точно) и победить тонны врагов. Теперь в нашем хранилище есть стабильные и обновляемые наборы данных. Победа? Ну почти. Осталось их только связать между собой.

Почему данные не хотят дружить между собой

Сами данные может и хотят, а вот их составители обычно отвечают только за свой набор данных. А что будет с датасетами дальше, уже редко кого интересует. Возьмем, например, данные Федеральной службы судебных приставов (ФССП). Данные содержат информацию об исполнительном производстве, предмете исполнения, сумму непогашенной задолженности, контактные данные судебного пристава-исполнителя и т.д. Казалось бы, чего еще можно пожелать? Так вот, по должнику мы имеем только название компании и ее адрес. Ни идентификационного номера налогоплательщика (ИНН), ни основного государственного регистрационного номера (ОГРН), по которым было бы легко связывать эти данные с данными из других источников и реестров — нет.

Связать данные ФССП напрямую только по названию юридического лица и/или адресу проблематично. Во-первых, в некоторых реестрах может быть указан только ИНН/ОГРН, а названия может и не быть. Во-вторых, названия компаний далеко не уникальны. Например, топ-5 названий обществ с ограниченной ответственностью в ЕГРЮЛ на момент написания статьи выглядел следующим образом:

Топ-5 названий компаний по встречаемости в ЕГРЮЛ
Топ-5 названий компаний по встречаемости в ЕГРЮЛ

В-третьих, названия юридических лиц можно записывать по-разному: кто-то ставит кавычки, кто-то — нет, где-то форма ведения бизнеса записана полностью, где-то — сокращенно и т.д. В-четвертых, информация об адресе может быть устаревшей; в реестрах могут быть разные адреса: в одном — регистрации, в другом — фактический. В-пятых, даже немного поработав с открытыми данными, можно с уверенностью сказать, что есть десятки различных способов записать один и тот же адрес компании по-разному. Надеюсь, мысль понятна и дальше можно не продолжать.

В общем, чтобы успешно связывать данные ФССП с другими источниками, нам нужны ИНН и ОГРН. Их мы и решили подтянуть из ЕГРЮЛ, проделав следующие шаги:

  • Подготовка данных.

  • Формирование датасета для обучения нейронной сети.

  • Обучение нейронной сети.

  • Деплой модели в продакшн.

Подробнее, как мы это сделали, расскажем ниже.

Подготовка данных

Сперва перечислим данные, которые мы будем использовать:

  • данные ФССП с fssp.gov.ru без ИНН и ОГРН;

  • размеченный историчный набор данных ФССП с уже подставленными ИНН и ОГРН;

  • данные ЕГРЮЛ с ИНН и ОГРН.

Из всего массива данных нам понадобится информация о названии компании, ее адресе, ИНН и ОГРН. Подготовка данных заключалась в нормализации адресов и названий компаний из различных источников: и в ФССП, и в ЕГРЮЛ адреса и названия должны быть написаны одинаково. Тут все стандартно:

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

  • унифицируем сокращения организационно-правовых форм с помощью найденной в интернете Инструкции Госкомстата

  • переставляем сокращения в начало названий

  • получаем устраивающий нас вариант для названий юридических лиц:

Пример унификации названий компании из ЕГРЮЛ и ФССП
Пример унификации названий компании из ЕГРЮЛ и ФССП

Аналогичные преобразования были проделаны и с адресами компаний. Дополнительная сложность в работе с адресами заключалась в том, что иногда в адресе отсутствовала информация о регионе или населенном пункте. В таком случае эту информацию нам удавалось вытаскивать из почтового индекса и заполнять ей обнаруженные пробелы:

Пример унификации адресов компаний из ЕГРЮЛ и ФССП
Пример унификации адресов компаний из ЕГРЮЛ и ФССП

Имея подготовленные и унифицированные адреса и названия компаний, мы можем собрать конкатенации названий компаний и их адресов из ФССП и такие же — из ЕГРЮЛ. Далее — следующий этап нашего проекта.

Формирование датасета для обучения нейронной сети

Теперь мы будем сравнивать конкатенации из набора данных ФССП с конкатенациями из ЕГРЮЛ и находить для каждой из них случайную конкатенацию из ста наиболее близких по матрице векторов.

Как упоминалось выше, у нас уже есть размеченный набор данных, по которым мы можем собрать позитивные примеры матчинга ФССП и ЕГРЮЛ. Однако, чтобы наша нейросеть быстрее обучалась и в то же самое время слишком не переобучилась на тренировочных данных, нам необходимо не только «хвалить» ее на верно найденных совпадениях, но и иметь данные, по которым мы будем накладывать «штрафы». На роль таких данных как раз и подойдет случайное значение из ста ближайших по матрице векторов. Однако, это значение не является правильным ответом. То есть похожее, но не совсем. Например, улица в адресе совпадет, благо улица Ленина есть практически во всех населенных пунктах нашей страны, а номер дома, название города и номер региона — нет.

Конечно, можно было бы брать не случайный из ста похожих, а просто случайный негативный пример. Но тогда задача, которую решает наша нейронная сеть, будет слишком простой. Однако, когда мы начнем применять её в реальных задачах, мы поймём, что такая постановка не делает нашу нейросеть полезной.

Теперь немного об инструментах. Для построения матрицы векторов будем использовать TfidfVectorizer, а для нахождения случайных похожих пар — библиотеку faiss.

TF-IDF (Term Frequency — Inverse Document Frequency) — показывает какой вес имеет то или иное слово для данного текста, в то же самое время принимая во внимание, как редко данное слово встречается во всем наборе рассматриваемых текстов (документов).

Расшифруем составляющие формулы:

  • tf — как часто слово появляется в тексте;

  • N — общее число текстов в наборе/коллекции текстов;

  • df — число текстов, содержащих слово;

  • log — используют, чтобы убрать доминанту idf из формулы, т.к. без логарифма idf будет иметь слишком большой вес по сравнению с tf.

А теперь пример:

Слово «мир» встречается в тексте из 100 слов 3 раза, значит tf = 3/100 = 0.03. В нашем наборе 10000 текстов, «мир» есть в 10 из них — idf = log(10000/10) = 3. Следовательно, tf-idf = 0.03 x 3 = 0.09.

TfidfVectorizer библиотеки scikit-learn подсчитывает tf-idf для каждого из слов в тексте и на выходе выдает массив векторов значений tf-idf.

После проведения векторизации будем искать случайные похожие пары для конкатенаций названий компаний и их адресов из набора ФССП в наборе ЕГРЮЛ. Если набор данных относительно небольшой, то для этой цели можно использовать стандартный модуль KDTree из той же scikit-learn. Однако, в нашем случае KDTree не позволил получить требуемый результат за вменяемое количество времени, поэтому нам пришлось обратиться к библиотеке faiss.

Faiss — это библиотека, которая позволяет искать ближайших соседей и кластеризовать данные в векторном пространстве. Со слов разработчиков, faiss может эффективно работать с наборами в миллиарды строк.

Библиотека написана на C++, а ее использование идет через Python и работу с Numpy arrays. Высокая скорость работы достигается за счет индексации векторов, а затем — использования диаграмм Вороного для кластеризации.

Внутри одного кластера все точки находятся ближе к центру именно этого кластера (центроида), а не другого. Таким образом, при поиске ближайшего вектора нам не надо пробегаться по всему набору векторов: достаточно сравнить его с имеющимися центроидами и затем искать уже внутри кластера с ближайшим центроидом. Если результаты поиска недостаточно точны, то мы просто увеличиваем количество кластеров, в которых будем искать в окрестностях найденного центроида. Также мы можем добиться ускорения алгоритма поиска за счет сжатия самих векторов с помощью Product Quantization (подробнее здесь). Прирост в производительности можно получить и за счет перехода от использования CPU на GPU. Faiss позволяет это сделать без каких-либо проблем.

В результате, после применения TfidfVectorizer и faiss мы имеем негативные примеры матчинга данных ЕГРЮЛ и ФССП, когда конкатенации адресов и названий компаний из разных источников похожи, но не совпадают. Из позитивных и негативных примеров матчинга мы и получаем итоговый датасет, на котором будем тренировать нашу нейронную сеть.

Обучение нейронной сети

Итак, у нас есть датасет, и мы хотим научить нейронную сеть различать одинаковые пары от разных пар конкатенаций из адресов и наименований компаний. Данная задача называется Semantic Textual Similarity (STS) и решается обычно через Metric Learning. Фактически, мы хотим научить нейросеть так векторизовать наши конкатенации адресов и названий, чтобы одинаковые в семантическом смысле примеры имели одинаковое или очень близкое с точки зрения какой-либо метрики векторное представление. В качестве метрики возьмем косинусное расстояние и будем оптимизировать его в ходе обучения.

Для начала мы взяли предобученную модель rubert-base-cased-sentence c Hugging Face. Это 12-слойный RuBERT-трансформер, дообученный на нескольких больших русскоязычных датасетах. Используя эту модель и библиотеку Sentence-Transformers, мы сходу получили точность предсказаний на валидационном датасете порядка 77%. Для начала неплохо. Но не зря же мы готовили данные на предыдущем этапе?

Поэтому дальше мы стали дообучать модель на ранее полученных данных. Обучали сиамские сети, чтобы получать эмбеддинги наших адресов и наименований компании таким образом, чтобы оптимизировать косинусное расстояние между ними. И вот, спустя около 4 дней и 20 эпох обучения нашей нейросети на nvidia RTX A5000, наша функция потерь перестала падать. Мы замерили точность предсказаний на валидационном датасете и получили значение 91%. Уже много лучше, поэтому мы решили на данном этапе остановиться на таком значении точности и перейти к выкатке модели в продакшн. Конечно, есть еще много пространства для улучшений модели, например, использование Triplet Loss, но оставим все это для дальнейших изысканий.

Функция потерь по ходу обучения

Деплой модели в продакшн

Теперь, когда нам нужно восстановить ИНН для компании из датасета ФССП, мы формируем вектор от преобразованной конкатенации адреса и названия компании. А дальше берем и из всех полученных таким же образом векторов из ЕГРЮЛ ищем самый близкий с точки зрения косинусного расстояния. Звучит достаточно просто, не так ли?

Но в этом всём есть одна большая проблема: на момент написания статьи в ЕГРЮЛ было зарегистрировано несколько миллионов компаний — считать косинусное расстояние между вектором компании из ФССП и всеми векторами из ЕГРЮЛ в лоб непозволительно долго. Мы, конечно, можем вернуться к KDTree или faiss, но в этих подходах в проде тоже есть определенные проблемы: нам нужно держать построенные индексы библиотек в оперативной памяти, а это десятки гигабайт.

Тут нам на помощь приходит замечательное расширение для Postgres — pgvector. Работает он, конечно, медленнее, чем faiss, но, тем не менее, достаточно быстро для наших целей. Также он позволяет держать подсчитанные для компаний из ЕГРЮЛ вектора в БД, а не в оперативной памяти.

Итак, теперь у нас точно есть всё для нашего матчера компаний из открытых источников. Он состоит из двух компонентов:

  • Джоба, которая проверяет в ЕГРЮЛ наличие новых компаний или компаний с изменениями в названии/адресе. Если такие имеются, то для них нейросетью формируются вектора, которые складываются в нашу БД.

  • Джоба, которая проверяет наличие компаний без указанного ИНН в датасетах ФССП. Для каждой такой компании формируем нейросетью вектор, при помощи pgvector находим ближайший вектор из ЕГРЮЛ и получаем соответствующий ему ИНН.

Всё это заворачиваем в Docker и выкатываем в наш общий кластер Kubernetes. Победа!

Заключение

Мы приложили немного усилий, и нам удалось подружить два открытых набора данных между собой. Зачем? Ну например, теперь у нас появилась возможность добавить данные ФССП в модель кредитного скоринга: так ее точность улучшится, а наша эффективность повысится. Это далеко не единственное применение открытым данным в процессах и продуктах Точки. Но это уже тема для других статей. Так что stay tuned!

Upd. Пока мы писали статью, ФССП частично добавила ИНН должников в предоставляемый набор данных. Трудно в это поверить, но это так! Расстроились ли мы, что наша модель потеряла некоторую актуальность? Нисколечко! Ведь изложенный выше подход можно использовать для любого источника данных, в котором отсутствуют данные ИНН/ОГРН. И к сожалению, таких данных по-прежнему хватает.

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


  1. Ananiev_Genrih
    19.11.2022 16:35

    Александр, отличная статья, спасибо! Начал после статьи знакомиться с Faiss и в чем-то это пересекалось с FastText от того же Facebook. Не понял зачем они сделали отдельную библиотеку вместо расширения функционала действующей (видимо были какие-то причины). Учитывая что он как раз про классификацию - пробовали его?