Если вы человек, который когда-либо пытался написать фрагмент кода, вы обязательно столкнетесь с переполнением стека (это так знаменито). Для тех, кто живет в скале, Stack Overflow предоставляет одну из крупнейших платформ контроля качества для программистов. Пользователи публикуют вопросы / сомнения, а их коллеги стараются предложить решения наиболее полезным способом. Чем лучше ответ, тем больше он получит голосов, что также повысит репутацию пользователя.

Учитывая его популярность, можно с уверенностью сказать, что там есть масса данных. Однако такой огромный объем информации также затрудняет поиск решения, которое вы ищете. Это не такая уж большая проблема для ветеранов программирования и других опытных профессионалов, потому что они знают правильные ключевые слова, необходимые для получения правильного ответа. Однако для маленького программиста это вызывает серьезную озабоченность. Например, если ему нужно научиться 'как создать сервер' с помощью Python, маловероятно, что он будет использовать термины 'Django' или ' Flask » в поле поиска. Таким образом, это может отпугнуть пользователя от использования платформы.

Следовательно, для того, чтобы разобраться в этом беспорядке, необходима оптимальная поисковая машина. Однако в настоящее время поисковая система Stack Overflow страдает от нескольких собственных ошибок. Позвольте мне проиллюстрировать это на примере:

Допустим, я новичок в NodeJS и хочу запустить свое приложение. Итак, я перехожу к Stack Overflow и набираю следующее: «Узел - как запустить node app.js?». Вот что я получаю:

Однако существует результат для «Узел - как запустить app.js?»:

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

«Хьюстон, у нас проблема!»

Но зачем беспокоиться, спросите вы? Основным источником дохода Stack Overflow является Доход от рекламы. Следовательно, их цель - максимизировать вовлеченность пользователей, чтобы продвигать больше рекламы и, таким образом, зарабатывать больше денег. Из-за неоптимальной производительности их поисковой системы пользователю было бы трудно избавиться от своих сомнений через свой веб-сайт, и поэтому он решил бы использовать для своих целей более сложную поисковую систему, такую ​​как Google.

Проблема возникает, когда Google предлагает ресурс, отличный от Stackoverflow. Каждый пользователь, покидающий свой веб-сайт, теряет деньги, которые потенциально мог бы заработать.

Возможное решение

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

Обработка естественного языка (НЛП) прошла долгий путь с момента своего появления в 20 веке. Это подполе искусственного интеллекта за последние несколько лет зарекомендовало себя очень хорошо благодаря быстрым процессорам и сложной архитектуре моделей и, таким образом, имеет огромный потенциал для решения различных задач понимания языка.

(Для просмотра анимированной версии нажмите здесь)

Решение разбито на 2 подзадачи:

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

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

  • Сбор данных вопросов / ответов Stackoverflow из Google BigQuery
  • Предварительная обработка и нормализация собранных данных
  • Отфильтровать только самые распространенные теги (использовано 500 тегов)
  • Обучение встраиванию слов в Word2Vec
  • Обучение классификатора тегов с использованием модели LSTM (подсказка: вы не можете использовать двоичную кросс-энтропийную потерю для его обучения. Узнайте больше, чтобы узнать почему)
  • Использование обученных встраиваний слов для создания встраиваний предложений для всех вопросов в нашей базе данных
  • Сравнение запроса с каждым предложением и ранжирование семантически похожих результатов с использованием меры, основанной на косинусном расстоянии.
  • Создание веб-приложения с использованием ReactJS и Flask для развертывания обученной модели

Для людей с фетишами блок-схем:

Ничего особенного, правда? Если вы знакомы с каждой из этих концепций, не стесняйтесь сразу переходить к коду (он также хорошо документирован * codegasm *).



Детские программисты, продолжайте читать :)

Небольшое примечание: я не буду объяснять каждую строку кода в статье (записные книжки jupyter в репозитории уже позаботятся об этом). Вместо этого я бы сосредоточился на ключевых результатах и ​​интуиции, стоящей за ними.

1. Сбор данных

Чтобы понять и извлечь уроки из данных, мне нужно собрать вопросы и ответы, которые были опубликованы на Stack Overflow. Таким образом, мне нужно следующее:

  • Заголовок
  • Тело вопроса
  • Ответы на этот вопрос
  • Голоса за каждый ответ

Из-за огромного количества данных о переполнении стека и улучшенных проверок работоспособности я ограничил данные только вопросами, относящимися к «Python». Однако весь процесс можно воспроизвести и для других тем.

Набор данных Google BigQuery включает архив содержимого Stack Overflow, включая сообщения, голоса, теги и значки. Этот набор данных обновляется для отражения содержимого Stack Overflow в Internet Archive, а также доступен через Stack Exchange Data Explorer. Более подробная информация о наборе данных представлена ​​в Kaggle Stackoverflow Dataset.

Необходимые данные для нашей задачи можно собрать с помощью следующего SQL-запроса:

SELECT q.id, q.title, q.body, q.tags, a.body as answers, a.score FROM 'bigquery-public-data.stackoverflow.posts_questions' AS q INNER JOIN 'bigquery-public-data.stackoverflow.posts_answers' AS a ON q.id = a.parent_id WHERE q.tags LIKE '%python%' LIMIT 500000

Этот запрос объединяет 2 таблицы (stackoverflow.posts_questions и stackoverflow.posts_answers) и собирает необходимые данные для 500000 вопросов, связанных с python. Таким образом, каждая строка содержит вопрос и один из ответов на него. (Примечание: могут быть строки с одинаковыми вопросами, но с уникальными ответами).

2. Предварительная обработка и нормализация данных.

Предварительная обработка данных 101 - Проверьте отсутствующие значения:

df.isna().sum()
--------------
id         0
title      0
body       0
tags       0
answers    0
score      0
dtype: int64

Как я упоминал ранее, наш набор данных может содержать строки с одинаковыми вопросами, но с уникальными ответами. Мы хотим объединить все эти разные строки в одну, суммируя голоса за каждый ответ и объединяя все ответы.

Max score before:  5440 
Max score after:  9730

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

  1. Преобразование необработанного текста в токены
  2. Преобразование токенов в нижний регистр
  3. Удалить знаки препинания
  4. Удалить стоп-слова

Примечание. Я пропустил удаление числовых данных, так как чувствовал, что это приведет к удалению ценной контекстной информации.

Я также пропустил этап «Стемминг / лемматизация», потому что я не хотел изменять термины, относящиеся к предметной области, используемые в нашем корпусе, и рисковал потерять ценную информацию.

Если вы внимательно посмотрите на набор данных, то заметите, что необработанный текст для вопросов и ответов дается вместе с разметкой HTML, с которой он изначально отображался в StackOverflow. Обычно они относятся к тегам p , h1-h6 и тегам code. Поскольку мне нужен только текстовый раздел каждого сообщения, я выполнил следующие шаги:

  • Я создал новый столбец функций под названием «post_corpus», объединив заголовок, текст вопроса и все ответы (будет использоваться позже для обучения встраиванию Word2Vec)
  • Я добавил заголовок к тексту вопроса
  • Я пропустил разделы «код», потому что они не содержат полезной информации для нашей задачи.
  • Я создал URL-адреса для каждого вопроса, добавив https://stackoverflow.com/questions/ к идентификатору вопроса.
  • Я создал 2 функции для анализа настроений, используя библиотеку Textblob с открытым исходным кодом.

У каждого сообщения есть переменное количество разных тегов. Чтобы сузить широкий выбор для более точной модели, я решил использовать 20 наиболее распространенных тегов. Планируется фильтровать только те данные, которые содержат хотя бы один из most_common_tags

['python',  'python-3.x',  'django',  'pandas',  'python-2.7',  'numpy',  'list',  'matplotlib',  'dictionary',  'regex',  'dataframe',  'tkinter',  'string',  'csv',  'flask',  'arrays',  'tensorflow',  'json',  'beautifulsoup',  'selenium']

Хорошо, всего лишь последний, чертовски скучный этап обработки данных, а затем мы перейдем к самому интересному :)

  • Я создал отдельный столбец для "processing_title", потому что я хотел сохранить исходный заголовок, потому что я хотел отображать исходные заголовки в приложении.
  • Я также нормализовал числовые «баллы»

3. Отфильтруйте только самые распространенные теги.

Хотя мы отфильтровали множество тегов на этапе обработки данных, все еще существует множество «случайных» тегов, которые могут появляться только один или два раза по сравнению с тысячами. вхождений для других тегов. Это увеличивает размеры достоверных данных, что нежелательно для нашей модели.

Таким образом, мы извлекаем 500 самых популярных тегов на основе их появления. (Учитывая, что у нас есть ~ 140 тыс. точек данных, 500 тегов кажется хорошим числом для экспериментов)

Наконец, мы изменили данные тега, чтобы включить теги только из одного из этих 500 тегов, для большей точности модели.

4. Обучение встраиванию слов с помощью Word2Vec.

Чтобы наша модель понимала необработанные текстовые данные, нам необходимо векторизовать их. Bag of Words и TF-IDF - очень распространенные подходы к векторизации. Однако, поскольку я буду использовать искусственную нейронную сеть в качестве моей модели (LSTM), разреженный характер BOW и TFIDF может создать проблему. Поэтому я решил использовать вложения слов, которые представляют собой плотные векторные представления и поэтому идеально подходят для нашей нейронной сети.

Люди общаются в StackOverflow очень технически, и они используют очень специфический словарь слов, поэтому использовать предварительно обученные вложения слов не рекомендуется (хотя у Google есть много хороших), потому что они обучены простому английскому языку. текст такого Шекспира и не сможет понять отношения между словами в нашем словаре. Поэтому я решил обучить модель Word Embeddings с нуля, используя Word2Vec.

Столбец post_corpus в нашем наборе данных начинает светиться здесь. Поскольку Word2Vec - это неконтролируемая модель, которой просто требуется корпус для обучения, нам нужно предоставить ей как можно больше текста, чтобы она могла понять словарь. Таким образом, мы используем post_corpus для обучения Word2Vec, потому что это комбинация заголовка, вопроса и всех ответов для сообщения.

После успешного обучения мы получаем следующие результаты:

Terms most similar to "django"
[('flask', 0.5827779173851013), ('project', 0.5168731212615967), ('mezzanine', 0.5122816562652588), ('wagtail', 0.5001770257949829), ('drf', 0.4827461242675781), ('framework', 0.48031285405158997), ('cms', 0.47275760769844055), ('admin', 0.467496395111084), ('database', 0.4659809470176697), ('app', 0.46219539642333984)]
--------------------------------------------------------------------
Terms most similar to "api"
[('apis', 0.6121899485588074), ('webservice', 0.5226354598999023), ('service', 0.49891555309295654), ('framework', 0.4883273243904114), ('postman', 0.47500693798065186), ('webhook', 0.4574393630027771), ('rpc', 0.4385871887207031), ('oauth2', 0.41829735040664673), ('twilio', 0.4138619303703308), ('application', 0.4100519120693207)]
--------------------------------------------------------------------
Terms most similar to "gunicorn"
[('uwsgi', 0.5529206991195679), ('nginx', 0.5103358030319214), ('000080', 0.4971828758716583), ('supervisord', 0.4751521050930023), ('arbiterpy', 0.4701758027076721), ('iis', 0.46567484736442566), ('apache2', 0.45948249101638794), ('web1', 0.45084959268569946), ('fastcgi', 0.43996310234069824), ('supervisor', 0.43604230880737305)]
--------------------------------------------------------------------
Terms most similar to "server"
[('webserver', 0.5781407356262207), ('servers', 0.48877859115600586), ('application', 0.488214373588562), ('app', 0.4767988622188568), ('vps', 0.4679219126701355), ('client', 0.46672070026397705), ('localhost', 0.46468669176101685), ('service', 0.457424521446228), ('apache', 0.4540043771266937), ('nginx', 0.4490607976913452)]

Чертовски круто, правда?

5. Обучение классификатора тегов с использованием модели LSTM.

В этом разделе рассматривается наша первая подзадача:

«Учитывая запрос пользователя, мы хотели бы предсказать, к каким тегам данный запрос лучше всего относится»

Подготовка данных для обучения модели включает:

  1. One-Hot кодирование достоверных данных
  2. Разделение на обучающую и тестовую наборы
  3. Токенизация и заполнение
  4. Создание матрицы вложения

(Я включу сюда код, однако подробные объяснения для каждого раздела см. В записной книжке jupyter в репозитории)

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

Секретный соус - ФУНКЦИЯ ПОТЕРИ: Поскольку здесь мы имеем дело с проблемой классификации с несколькими метками, мы не можем обучить модель, используя двоичную кросс-энтропийную потерю. Это связано с тем, что потеря бинарной кросс-энтропии подтолкнет вашу модель к предсказанию одного или двух тегов, которые включены в основную истину, и не будет наказывать ее за пропуск других.

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

Затем мы можем получить теги для любого предложения с помощью следующей функции:

Test Case: selecting n1d array nd array numpy
--------------------------------------------------------------------
Predicted: [('arrays', 'numpy', 'python')]
Ground Truth: [('numpy', 'python')]

Test Case: taking info file
--------------------------------------------------------------------
Predicted: [('python',)]
Ground Truth: [('python', 'python-2.6', 'python-2.7')]

Test Case: python find txt continue causing fault
--------------------------------------------------------------------
Predicted: [('python',)]
Ground Truth: [('python', 'python-3.x')]

Test Case: fabric rsync read error connection reset peer 104
--------------------------------------------------------------------
Predicted: [('django', 'python')]
Ground Truth: [('django', 'python', 'windows')]

Test Case: fllter pandas dataframe multiple columns
--------------------------------------------------------------------
Predicted: [('dataframe', 'pandas', 'python')]
Ground Truth: [('pandas', 'python')]

Кажется, на данный момент достаточно. Выполнив эту подзадачу, перейдем к следующей, давай давай ~

6. Использование обученных встраиваний слов для встраивания предложений для всех вопросов в нашей базе данных.

Подзадача 2 требует следующего:

«На основе запроса пользователя вернуть существующий вопрос, который наиболее точно соответствует запросу пользователя»

Чтобы сравнить, насколько похожи два предложения, мы можем найти «расстояние» между ними. Чтобы можно было вычислить такое расстояние, предложения должны принадлежать одному векторному пространству. Это делается с помощью встраивания предложений. Одна из самых популярных мер расстояния, которую я также использовал, - это косинусное расстояние. Чем меньше косинусное расстояние, тем выше сходство между двумя векторами.

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

7. Сравнение запроса с каждым предложением и ранжирование семантически похожих результатов с использованием меры, основанной на косинусном расстоянии.

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

где q = запрос пользователя и t = существующий вопрос

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

Выполнение следующего фрагмента кода может показать вам алгоритм в действии:

По пользовательскому запросу «Объединить списки списков» получаем:

Веб-приложение просто переводит эти результаты через пользовательский интерфейс.

Я не собираюсь обсуждать, как я создал веб-приложение в этом посте, вы можете проверить этот код в самом репозитории. Если у вас есть какие-либо сомнения или какие-либо конструктивные отзывы (ядовитые ненавистники могут сосать ди ..), пожалуйста, не стесняйтесь оставлять ответ ниже. Я был бы рад получить известие от вас 😄

А пока Чао ~