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

Как мы делаем ML на Java

Уровень сложностиСредний
Время на прочтение7 мин
Количество просмотров406

Привет, Хабр. Меня зовут Лёша Круглик, я занимаюсь коммерческой разработкой около 7 лет. Писал код для Epam, Альфа Банка и Яндекса, а последние года три занимаюсь разработкой в Okko.

Как-то солнечным питерским днём мы с коллегами запускали процесс стажировок и обсуждали перфоманс и код-ревью. Код — это источник правды для разработчика. Руководитель оценивает его и находит точки роста, подсказывает, что можно улучшить. Эта концепция неплохо работает в небольших командах, где тимлид может посвятить время оценке кода и поиску хороших ресурсов для обучения. Но чем больше команда, тем больше времени будет на это уходить. 

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

Основные идеи эксперимента

  • Развитие — вечный двигатель разработчика

  • Код — источник его правды

  • Code review — инструмент обратной связи

  • Просматривать все комменты лиду сложно, долго, практически невозможно

  • Вывод — автоматизация с помощью ML

Почему Java

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

Чаще всего для этого берется Python, Torch и строится модель. Но Python — интерпретируемый язык, у него нет преимуществ многопоточного программирования, которое ускорило бы обработку моделей и результатов. 

Мы же хотели построить архитектуру, которая бы применялась для анализа текстов в реальном времени. Нам хотелось получать результат сразу по актуальной базе знаний, дополняемой одновременно с тем, как оставили какой-то комментарий. Поэтому нам была важна возможность асинхронной обработки и получение результатов сразу из множества источников.

Мы решили объединить несколько подходов — это NLP и Java. Можно и Kotlin, но Java — всегда в сердечке.

(NLP) Анализ текста

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

NLP в целом — это что-то вроде математической лингвистики. Мы преобразуем человеческую речь в понятный для компьютера язык. Поэтому первое, о чём мы поговорим — как разбить предложение и представить его в виде чисел, чтобы использовать в модели.

Самый простой и понятный на текущий момент способ представления конкретных предложений на языке компьютера — это векторизация. Есть несколько техник векторизации.

Bag of words (BOW)

Оно же «пакет слов» — это представление текста, которое описывает характер присутствия слов в документе. Оно подразумевает наличие словаря (списка уникальных слов, присутствующих в тексте) и меры присутствия таких слов. При этом вся информация о порядке и структуре слов отбрасывается — модели важно только то, встречаются ли известные слова в документе.

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

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

Также после того, как мы векторизировали слова в понятные для компьютера числа, появляется ещё один вопрос — а как их сравнивать? Как понять, что два предложения схожи?

Для этого используется подход косинусной схожести (Cosine Similarity). Формула сама по себе представляет собой скалярное произведение векторов, поделенное на модули данных векторов.

Мы смотрим на то, как близко расположены векторы между собой. Вот пример:

Hi и World сами по себе расположены достаточно далеко друг от друга. Но когда мы используем наш словарь, то получается, что Hi, World и Hello, World похожи. При преобразовании в векторы они будут находиться достаточно близко друг к другу — практически близнецы.

NGram

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

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

Каждое слово отдельно несет свой смысл, но у выражения уже другие и смысл, и контекст. 

Чтобы перевести это на понятный компьютеру язык используется подход NGram — мы разбиваем предложение не по одному слову, а по N слов, и можем представить их в виде униграм, биграм, триграм и так далее. При обработке естественного языка мы можем использовать один из этих грамов, но их выбор зависит от контекста: например, если мы хотим создать модель на основе древних русских сказок, имеет смысл использовать не униграм, а биграм. Для длинных словосочетаний — триграм.

Porter Stemming

В естественных языках в грамматическую основу слова входит не только корень, но и приставки и суффиксы — эти части не изменяются при склонении. 

Близнец, близнецу, близнецы, близнецам — для компьютера это разные слова. Чтобы преобразовать их в понятные единицы (по сути, в основу), можно использовать Porter Stemmer — он оптимален для русского языка.

 

TF-IDF

Тут уже возникает ещё одна проблема, свойственная всем естественным языкам — слова-паразиты и междометия. Они не несут в себе смысловой нагрузки и для обработки не так важны.

Кроме слов-паразитов часто встречаются слова, которые используются в качестве связки («собственно», «кстати» и прочие вводные), но не несут смысла для компьютера. Поэтому при обработке важно отделить их от тех, что важны для обучения модели. 

Для этого существует подход TF-IDF, который позволяет подсчитать частоту вхождения слов и инвертированную частоту вхождения слов на весь корпус документов.

В итоге мы получаем массив чисел для каждого слова. Чем ближе число к единице, тем наиболее важно слово — и его нужно учитывать.

Word2Vec

При векторизации используются немного другие алгоритмы. Никто не берет весь массив слов из словаря и не встраивает их в векторное представление, потому что это дорого.

Для этого используются как раз нейронные сети. Они строятся по Word2Vec — праотцом всех современных языковых моделей. Это контекстно-ориентированный подход, который работает по принципу «скажи мне, кто твой друг, и я скажу, кто ты» — определяет, в каком контексте используется слово, какие слова находятся рядом, преобразовывает его в конкретный вектор заданной длины.

Для реализации описанных выше подходов можно использовать набор существующих библиотек. По сути они нативные, потому что все пишут на Python или Go. Но когда речь заходит о скорости чаще всего такие штуки пишут на C++.

Так как мы хотим реализовать наш подход на Java, использовать будем библиотеки deeplearning4j и nd4j. 

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

Использовать библиотеку deeplearning4j достаточно просто. Берём какой-то набор слов — он будет корпусом, на котором будет обучать модель. Закидываем его в SentenceIterator, который будет идти по строчкам и каждому предложению, разбивая его на части. В примере указан файл txt, но лучше создать базу данных. 

Создаём процессор для обработки перечисленными ранее методами, а дальше всё довольно просто. Берём word2vec, задаём параметры:

  • какие базы берём, 

  • как итерируемся,

  • как токенизируем.

Параметры индивидуальны и зависят от того, что вы хотите обучать.

Для наполненной модели делаем вызываем метод feat и получаем, по сути, уже массив векторов. С ней можем рассчитывать косинусное расстояние между ними, подсчитать, на сколько эти слова близки друг к другу. 

Когда мы из каждого слова в предложении создаём вектор, мы суммируем и получаем расстояние между двумя предложениями, выкинув ненужные слова с помощью TF-IDF. Это особенно полезно, когда у нас есть большие тексты — например, если мы хотим обработать статью с Хабра и сравнить её с комментарием. Мы можем выделить ключевые слова и делать автоматические теги на какой-то контент. 

Например, в предложении «Грубо говоря, косинусное расстояние — это мера сходства»  слова грубо, говоря, это — не ключевые, и будут иметь низкую оценку по TF-IDF. Слова косинусное, расстояние, сходства — ключевые и получат высокую оценку.

Архитектура

Возвращаемся к нашему эксперименту. Использование Java и отказ от Database Sharing позволяет нам построить асинхронную обработку модели — мы постоянно получаем данные и не ждём, пока наполнится модель, чтобы запустить её обучение через какой-то промежуток времени. Асинхронность позволяет нам получать комментарии через Bitbucket, закидывать их в базу знаний и сразу прогонять через Word2Vec.

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

В рамках предложенной архитектуры база данных разделяется на:

  • модель, обученную на word2vec;

  • базу знаний — оставленные комментарии, статьи, заметки, обращения пользователей и т.д.

Для взаимодействия с пользователем сделали Telegram Bot, который принимает запросы через Telegram API webhook. Компонент education-recommendation-chat-bot формирует через них команду для Kafka и асинхронно передает запрос компоненту growth-manager. 

Компонент на основании описанных ранее подходов, обученной модели и существующей базы знаний формирует рекомендации. В то же время компонент growth-manager в режиме реального времени предподготавливает и формирует базу знаний. Сейчас основным источником знаний является Bitbucket, который на основании webhook отправляет информацию об оставленных комментариях. 

Через компоненты мы можем асинхронно дообучать и получать данные: стоит новому пользователю написать комментарий, мы сразу же найдём похожие предложения. Всё это делаем через Kafka внешними API, которые будут связывать нас с клиентами.

Что получилось?

В итоге мы реализовали описанный подход — научились создавать автоматические рекомендации по разным темам. Сейчас активно учимся парсить Хабр и создаем привязку тем к определённым статьям, чтобы можно было рекомендовать не просто темы, а выдавать целый список литературы. 

Выглядит это вот так:

Где ещё может работать модель

Мы предполагаем, что она может работать в разных областях:

  • Обращения пользователей в поддержку,

  • Рекомендации и автодополнения текста,

  • Формирование раздела схожих карточек и товаров,

  • Поиск схожих тикетов Jira и ранее заведенных багов,

  • Автоматическое формирование тегов.

Но сейчас фокус нашей модели — обучение сотрудников, и мы продолжим развивать её в этом направлении.

Теги:
Хабы:
0
Комментарии0

Публикации

Информация

Сайт
okko.tv
Дата регистрации
Дата основания
Численность
501–1 000 человек
Местоположение
Россия