В этой статье я проведу вас через процесс решения реальной проблемы НЛП. Статья будет разделена на следующие части:

  1. История проблемы и источник данных.
  2. Изучение набора данных и его формата.
  3. Очистка и предварительная обработка данных.
  4. Особенности.
  5. Визуализация функций и удаление лишних.
  6. Векторизация текстовых объектов.
  7. Применение машинного обучения для классификации.

Прежде чем мы углубимся в детали, я хочу остановиться на нескольких вещах. Я постараюсь изо всех сил, чтобы мои объяснения были простыми и краткими, поскольку моя цель здесь - не сбивать с толку (ага), а заставить всех, кто читает эту статью, будь то опытные специалисты по данным или те, кто ограниченно знаком с предметом, понимают что делается и почему.

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

Вы можете найти код Python здесь (также ссылка в конце статьи). Все, о чем я здесь расскажу, можно найти в коде. Вы также можете просмотреть файлы на моем Github. Ладно, давай прямо в это дело.

Часть 1. История проблемы и источник данных

Данные взяты из справки по соревнованиям Kaggle в 2017 году. Соревнование было организовано Quora и имело приз за первое место в размере 12 500 долларов США. Quora - это сайт вопросов и ответов, на который приходят миллионы вопросов, не все из которых являются новыми и уникальными. Многие из них уже были заданы на Quora и получили исчерпывающие ответы.

Если разрешить дублирование, это повлияет на качество ответов, что отрицательно скажется на опыте человека, задающего вопросы, человека, отвечающего на вопросы, и человека, ищущего ответ в Интернете (представьте, что вы ищете в Google вопрос и находите 3 результата на Quora. вместо 1). Однако эта проблема характерна не только для Quora, и у многих организаций есть аналогичные проблемы (например, Stackoverflow).

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

Наша задача в этом проекте - разработать часть машинного обучения. Нас не интересует, как Quora генерирует подмножество, мы предполагаем, что оно есть и работает должным образом.

Процесс можно резюмировать следующим образом:

Шаг 1. Получите запрос на публикацию нового вопроса от пользователя.

Шаг 2: проанализируйте вопрос на высоком уровне и создайте подмножество вопросов, которые похожи и уже существуют в базе данных.

Шаг 3: объедините новый вопрос со всеми вопросами в подмножестве и примените машинное обучение, чтобы определить, не является ли какая-либо из пар дубликатом.

Шаг 4. В зависимости от результатов шага 3 примите соответствующие меры.

Нас интересует только шаг 3 в этом проекте.

Часть 2: Изучение набора данных и его формата

На веб-странице соревнования Kaggle у нас есть доступ к наборам данных о поездах и тестах.

Набор состоит из 404 290 пар вопросов (строк) и 6 переменных (столбцов). Столбцы - это идентификатор строки, вопрос 1, вопрос 2, идентификатор вопроса 1, идентификатор вопроса 2 и метка класса, которая равна 0 для неповторяющихся пар и 1 для повторяющихся пар. Идентификаторы вопросов (qids) однозначно определяют каждый вопрос.

Набор тестов состоит из более чем миллиона пар вопросов (многие из которых специально созданы на компьютере для предотвращения мошенничества в конкурсе). Набор тестов состоит из 3 столбцов: идентификатора строки, вопроса 1 и вопроса 2. Я работаю на довольно обычном компьютере и не могу правильно загрузить набор тестов. Вместо этого я решил поработать исключительно на тренировочном наборе, на котором я выполнил разделение на 70–30 поездов.

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

После разделения наш поезд и тестовый набор содержат 63% неповторяющихся пар и 27% повторяющихся пар. Распределение не идеальное, но вполне работоспособное.

Часть 3: Очистка и предварительная обработка данных

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

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

  • Преобразование текста в нижний регистр.
  • Удалите знаки препинания.
  • Замените некоторые числовые значения строками (например: 1 000 000 на 1 м).
  • Удалите HTML-теги.
  • Замените некоторые символы их строковыми эквивалентами (например: $,% @ и т. Д.).
  • Избавьтесь от слов («не делайте» становится «не делайте»).
  • Удаление стоп-слова.

Я намеренно не проводил стемминг или лемматизацию. Это процессы, которые сводят слова к базовым формам. Например, плавание становится плаванием. Однако с помощью стемминга мы можем получить очень необычные результаты, например, необычное становится необычным (каламбур). Когда мы доходим до векторизации наших текстовых данных, это сильно отбрасывает векторы Word2Vec, поскольку они предварительно обучены правильному тексту. Лемматизация может сработать, но мне нужно время, чтобы сделать это на моем компьютере. Не стесняйтесь опробовать его и прокомментировать ниже, как это повлияло на ваши результаты.

Удаление стоп-слов - это процесс удаления стоп-слов из наших текстовых данных. Стоп-слова - это слова, не имеющие смыслового значения. Для многих языков существуют хорошо известные наборы стоп-слов. Вот несколько примеров английских стоп-слов: я, я, я и т. Д. Для удаления стоп-слов я использовал NLTK, популярную библиотеку НЛП для Python. У них есть полный набор стоп-слов для многих языков. Вы можете просмотреть английские стоп-слова НЛТК (обновленные на момент написания этой статьи) здесь.

Я абсолютно не хочу ложно завышать размер этой статьи, храня фрагменты кода повсюду. Я чувствую это довольно долго. В начале и в конце этой статьи вы найдете ссылку Google Colaboratory, которая ведет к записной книжке. Вы также найдете ссылку на мой Github, если хотите просмотреть код там. Код хорошо прокомментирован и задокументирован. Не стесняйтесь читать его, просматривая статью.

Часть 4: Featurization

Featurization - это процесс, при котором мы извлекаем функции из наших данных. Эти функции - это то, что мы будем вводить в наши алгоритмы машинного обучения (вместе с нашими векторами слов, но мы скоро до них доберемся).

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

Характеристики токенов - это функции, которые извлекаются из анализа токенов в вопросе (токеном в данном случае является каждое слово). Я извлек следующие особенности токена:

  1. q1_len: количество символов в вопросе 1.
  2. q2_len: количество рассматриваемых символов 2.
  3. q1_words: количество слов в вопросе 1.
  4. q2_words: количество рассматриваемых слов 2.
  5. words_total: сумма q1_words и q2_words.
  6. words_common: количество слов, которые встречаются в вопросе 1 и 2, повторяющиеся вхождения не учитываются.
  7. words_shared: доля от слов_общих до слов_общего.
  8. cwc_min: это отношение количества общих слов к длине меньшего вопроса.
  9. cwc_max: это отношение количества общих слов к длине большего вопроса.
  10. csc_min: это отношение количества общих стоп-слов к меньшему количеству стоп-слов среди двух вопросов.
  11. csc_max: это отношение количества общих стоп-слов к большему количеству стоп-слов среди двух вопросов.
  12. ctc_min: это отношение количества общих токенов к меньшему количеству токенов среди двух вопросов.
  13. ctc_max: это отношение количества общих токенов к большему количеству токенов среди двух вопросов.
  14. last_word_eq: 1, если последнее слово в двух вопросах одинаковое, 0 в противном случае.
  15. first_word_eq: 1, если первое слово в двух вопросах одинаковое, 0 в противном случае.
  16. num_common_adj: количество общих прилагательных в вопросе1 и вопрос2.
  17. num_common_prn: количество общих имен собственных в question1 и question2.
  18. num_common_n: количество существительных (неправильных), часто встречающихся в question1 и question2.

FuzzyWuzzy - это библиотека Python, в которой есть несколько методов для сравнения эквивалентности строк. Я настоятельно рекомендую потратить 15 минут, чтобы пройти по этой ссылке, в которой объясняются различные функции FuzzyWuzzy. Я использовал различные методы сравнения строк из FuzzyWuzzy для извлечения нечетких функций. Нечеткие особенности:

  1. fuzz_ratio: оценка fuzz_ratio от fuzzywuzzy.
  2. fuzz_partial_ratio: частичное_отношение fuzzy от fuzzywuzzy.
  3. token_sort_ratio: token_sort_ratio от fuzzywuzzy.
  4. token_set_ratio: token_set_ratio от fuzzywuzzy.

Наконец, я извлек некоторые особенности из длины данных. Характеристики длины:

  1. mean_len: среднее значение длины двух вопросов (количество слов).
  2. abs_len_diff: абсолютная разница между длиной двух вопросов (количеством слов).
  3. longest_substr_ratio: отношение длины самой длинной подстроки среди двух вопросов к длине меньшего вопроса.

Ничего себе, и вот так у нас получилось 25 функций.

Часть 5: Визуализация функций и удаление лишних

Давайте визуализируем некоторые из них и посмотрим, как они выглядят.

На приведенном ниже графике показано разделение классов, выполненное с помощью функции words_shared. На мой взгляд, неплохо. Есть четкое разделение. Метка класса 0 указывает, что вопросы не дублируются, а метка класса 1 указывает, что вопросы дублируются. На диаграмме мы можем ясно видеть более высокие значения для повторяющихся вопросов. Это был бы пример хорошей функции.

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

Важно помнить, что одномерный анализ (анализ каждой функции по отдельности) не является окончательным для этой функции. Функции могут взаимодействовать с другими функциями и давать значимые результаты. В нашем случае разумно, что если вопрос 1 и вопрос 2 имеют одинаковую длину, они могут быть похожими, а если они имеют очень разную длину, они, вероятно, не являются одинаковыми вопросами.

Точно так же один q1_len не дает много информации.

Затем у нас есть несколько хороших, но не очень хороших функций. Примером такой функции может быть функция csc_min.

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

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

Некоторые функции предоставляли очень похожие дистрибутивы, что в основном делало их избыточными. Функции ctc имеют то же распределение, что и функции cwc. Из-за этого я откажусь от функций ctc. Кроме того, fuzz_ratio и token_sort_ratio также очень похожи друг на друга. Поскольку token_sort_ratio считается несколько более мощным, чем fuzz_ratio, я откажусь от fuzz_ratio.

Я также построил облака слов для повторяющихся и не повторяющихся пар.

Ниже показано облако слов для повторяющихся пар.

Ниже показано облако слов для неповторяющихся пар.

Похоже, слова «различаются» и «лучший» довольно часто встречаются в обоих классах. Это говорит нам о том, что эти слова бесполезны для различения классов. Я добавил эти два слова в свой набор стоп-слов и снова запустил предварительную обработку. Не буду врать, это достаточно субъективный подход. Если бы у меня было намного больше времени, я бы более объективно убрал такие слова.

Часть 6: Векторизация текстовых функций

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

Эта статья не посвящена подробному объяснению различных типов векторов слов, а также их преимуществ и недостатков. Это заслуживает отдельной статьи (а может, двух).

Короче говоря, компьютерам трудно понимать строки и текстовые символы. У людей это очень хорошо получается. Однако компьютеры преуспевают в извлечении значения из чисел, которые для людей могут быть просто искаженной ерундой. Проще говоря, вектор слов - это числовое представление текстовых данных. Существует много способов преобразования (векторизации) текстовых данных в их числовое представление (векторы). Если вы хотите узнать об этом больше, используйте такие методы, как Пакет слов (BoW), Частота термина, обратная частота документа (TF-IDF) и Word2Vec - хорошая отправная точка. Также существует множество продвинутых векторных представлений, таких как BERT и RoBERTa. В этом проекте я использовал TF-IDF и средневзвешенное значение Word2Vec по IDF.

Проще говоря, TF-IDF - это отношение количества раз, когда слово (термин) встречается в конкретном вопросе, к количеству раз, когда это слово встречается во всех вопросах (весь наш корпус). Более высокое значение TF-IDF указывает на более важное слово.

Предположим, слово «выполнять». Это слово может встречаться в любом контексте. Даже если TF (числитель) высокий, частота документов, DF (знаменатель) также, вероятно, будет высокой. Это приведет к низкому значению TF-IDF. Теперь предположим, что это слово «факторизация». Это довольно специфичное для предметной области слово, которое чаще всего встречается в математическом контексте. DF будет намного ниже, чем для слова «выполнять». Таким образом, в предложении «Как мне выполнить факторизацию» слово «факторизация» дает нам много семантической информации, тогда как слово «выполнить» почти ничего не дает.

Примечание: хороших и плохих слов не бывает. Это очень специфично для корпуса. Если, например, мы работали с медицинским корпусом, слова «пациент» или «болезнь» почти не имели бы значения. Помните об этом при использовании TF-IDF в своих проектах.

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

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

Если угол близок к 0, косинусное расстояние будет очень близко к 1 (отлично, векторы очень похожи). Например, слово любовь и слово обожать будут иметь косинусное сходство около 1. Если угол равен 90, косинусное расстояние равно 0 (без корреляции). Например, слово автомобиль и слово университет. Если угол равен 180, косинусное расстояние равно -1 (полностью противоположные векторы). Например, слово мир и слово война.

Я сказал, что использую Word2Vec со средневзвешенным значением IDF. Позвольте мне объяснить, что это такое. Каждое слово в предложении имеет собственное числовое представление Word2Vec. Каждое слово также имеет собственное значение IDF. Умножьте вектор каждого слова на его значение IDF, и вы получите векторы, взвешенные по IDF. Сложите все взвешенные IDF векторы в предложение и разделите на количество слов в предложении и «стрелку», вы получите средневзвешенные IDF векторы Word2Vec.

Для двух типов векторизации, которые я выполнил, это были результирующие размеры:

  • Векторизация TF-IDF: 73 459 измерений в предложении. Поскольку мы будем складывать два предложения рядом друг с другом, результирующие размеры будут 146 918. Это много! Это потому, что были использованы все уникальные слова в корпусе (которых у нас было 73 459), но результаты могут вас удивить. Вот вам упражнение: попробуйте ограничить количество измерений, скажем, 10 000 (используйте атрибут max_features в Sklearn’s TfidfVectorizer) и посмотрите, есть ли разница в результатах 😉.
  • Word2Vec, средневзвешенное значение TF-IDF: 300 измерений в предложении. Если сложить два вопроса рядом друг с другом, получится всего 600 измерений.

По сравнению с Word2Vec модель TF-IDF имеет экспоненциально больше возможностей. Однако вас может удивить то, что модель TF-IDF на самом деле работает намного быстрее (почти в 3 раза), чем модель Word2Vec. Это связано с тем, что векторы TF-IDF являются разреженными векторами, что означает, что большинство значений являются нулями, тогда как векторы Word2Vec являются плотными векторами.

Часть 7. Применение машинного обучения для классификации

Итак, теперь у нас есть данные, мы очистили и предварительно обработали их, извлекли функции и векторизовали наш текст. Теперь мы готовы продолжить и сделать некоторые классификации. Я выбрал Логистическая регрессия, Линейная SVM и Деревья решений с градиентным усилением (GBDT) . GBDT в целом очень эффективны, но требуют много времени для выполнения, а логистическая регрессия отлично работает с большими измерениями. Я сделал несколько разных моделей данных. Они есть:

  1. Модель данных 1: только извлеченные функции (характеристики маркера + нечеткие функции + характеристики длины). Количество измерений - 22.
  2. Модель данных 2: извлеченные признаки + косинусное сходство модели Word2Vec. Количество измерений - 23.
  3. Модель данных 3: только векторы TF-IDF. Количество измерений - 146 918.
  4. Модель данных 4: только средневзвешенные векторы Word2Vec IDF. Количество измерений 600.
  5. Модель данных 5: извлеченные признаки + косинусное сходство + IDF-средневзвешенные векторы Word2Vec. Количество измерений - 623.
  6. Модель данных 6: извлеченные признаки + векторы TF-IDF. Количество измерений - 146 940.

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

Для логистической регрессии и линейной SVM настроенными гиперпараметрами были альфа и штраф. Альфа - это константа, на которую умножается член регуляризации. Более высокий альфа указывает на более сильную регуляризацию. Более сильная регуляризация помогает в обобщении, но чрезмерно мощная регуляризация вызовет систематическую ошибку (недостаточную подгонку) в модели, в то время как очень слабая регуляризация приведет к увеличению дисперсии (чрезмерной подгонке) модели. Штраф - это тип применяемой регуляризации (L1, L2 или ElasticNet).

ГБДТ - сложная тема, и в этой статье не место для их объяснения. Они как бы противоположны случайным лесам (забавный факт, во время соревнований Quora использовала случайный лес для этой самой задачи). В то время как случайные леса представляют собой целую группу деревьев решений с низким смещением и высокой дисперсией (очень глубокими), GBDT представляют собой целую группу деревьев решений с высоким смещением и низкой дисперсией (очень мелкие). Гиперпараметры, настроенные для моей модели GBDT, - это количество раундов повышения и максимальная глубина каждого дерева.

Для логистической регрессии и линейной SVM я использовал SGDClassifier Sklearn (потеря журнала для логистической регрессии и потеря петли для линейной SVM). Для GBDT я использовал xgBoost’s Sklearn API. Каждая из этих моделей также была откалибрована с помощью Sklearn Calibration Classifer.

Метрика, которую мы пытаемся оптимизировать, - это потеря журнала. Это показатель, по которому оценивалась конкуренция, и поэтому мы оптимизируем его. Наиболее заметные результаты заключаются в следующем:

Для моделей логистической регрессии и линейных SVM-моделей:

Модель 6 превзошла все остальные модели. На втором месте оказалась модель 5 с тестовым проигрышем 0,491 (для обоих алгоритмов).

Давайте посмотрим на матрицы путаницы для этих двух результатов.

Матрицы путаницы логистической регрессии:

Матрицы путаницы линейных SVM:

Как видим, результаты очень и очень идентичны.

Для модели GBDT,

Вау, это значительное улучшение по сравнению с двумя другими алгоритмами. Однако цена, которую придется заплатить, - это время на обучение и классификацию. Кроме того, для двух других алгоритмов потери поезда и тестовые потери были аналогичными, что указывало на отсутствие переполнения. Для GBDT разрыв больше, что указывает на то, что мы приближаемся к переобучению, и на самом деле с тестом, который я провел с максимальной глубиной 8 и ›500 раундов усиления, было ясно, что мы переборщили. Мне комфортно с максимальной глубиной 6 и 500 патронов. Удивительно, но у обеих моделей одинаковые потери в тестах. Давайте проанализируем матрицы путаницы, чтобы получить лучшее представление.

Матрицы путаницы модели 5:

Матрицы путаницы модели 6:

Из матриц путаницы мы видим, что модель 5 даже немного превосходит модель 6. Но возникает вопрос: стоит ли увеличение на 0,05 в 3 раза больше времени обучения? На мой взгляд, это не так.

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

Спасибо, что дочитали до конца. Как и было обещано, вы можете найти блокнот в Google Colaboratory здесь и на моем Github.