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

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

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

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

Двоичный векторизатор

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

Представьте себе следующие два предложения:

"Я пошел в продуктовый магазин"

"Я ходил в кинотеатр"

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

sentence_1 = 'I went to the grocery store'
sentence_2 = 'I went to the movie theater'
vocab = set(
   list(
      sentence_1.split(' ')+sentence_2.split(' ')
  )
)

Наш объект словаря теперь содержит отдельные слова корпуса:

  • Я пошел в продуктовый магазин, в кино, в театр

Если мы закажем наш словарь:

vocab.sort()

В результате получаем список со следующими элементами:

grocery, I, movie, store, the, theater, to, went

Давайте приступим к созданию массива нулей с количеством элементов нашего словаря, где каждое слово w в позиции j нашего списка будет отображаться в позицию j нашего массива:

import numpy as np
array_words = np.zeros(len(vocab))

В нашем примере массив будет содержать следующие элементы:

[0, 0, 0, 0, 0, 0, 0]

Мы можем сопоставить наши предложения с этим массивом, превратив каждый элемент j в 1, когда в предложении присутствует слово w - давайте начнем с нашего первого предложения 'I пошел в продуктовый магазин '- и соответствующим образом обновим наш массив:

[1, 1, 0, 1, 1, 0, 1, 1]

Одновременная визуализация нашего списка словарей и массива сделает это более явным:

grocery, I, movie, store, the, theater, to, went
[1,      1,     0,     1,   1,       0,  1,    1]

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

grocery, I, movie, store, the, theater, to, went
[0,      1,     1,     0,   1,       1,  1,    1]

Создание обоих этих массивов в numpy:

array_words = np.array([
  [1,1,0,1,1,0,1,1],
  [0,1,1,0,1,1,1,1]
])

Теперь у нас есть простое математическое представление наших предложений - к счастью, нам не нужно делать это вручную для всех предложений в конкретном корпусе, поскольку scikit-learn как отличная реализация этого в feature_extraction.text в функции CountVectorizer.

Представьте, что наши предложения представлены в виде списка:

sentence_list = ['I went to the grocery store',
'I went to the movie theater']

Мы можем определить объект CountVectorizer с двоичным, установленным на True (предупреждение спойлера, это параметр, который проводит линию между чистым CountVectorizer и BinaryVectorizer!) И tokenizer равно str.split - не особо переживайте по поводу этого последнего варианта, но это просто способ имитировать тот же массив, который мы делали раньше (без этой опции « будет удален из вывода, поскольку отдельные буквы удаляются по умолчанию в scikit-learn реализация векторизатора):

from sklearn.feature_extraction.text import CountVectorizer
cvec = CountVectorizer(tokenizer=str.split, binary=True)

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

cvec.fit_transform(sentence_list).todense()

Обратите внимание на метод todense (), вызываемый после метода fit_transform. Мы делаем это, потому что изначально fit_transform сохраняет полученный объект в формате разреженной матрицы из-за вопросов сжатия пространства - мы обсудим это подробнее позже.

Давайте посмотрим на наш массив, созданный по приведенной выше инструкции:

[[1, 1, 0, 1, 1, 0, 1, 1],
 [0, 1, 1, 0, 1, 1, 1, 1]]

Знакомо? Это то же самое, что мы делали раньше вручную! Вы можете обобщить этот подход для любого набора предложений или документов, которые у вас есть.

Теперь давайте зададим вопрос, который будет следующим предложением:

«Я пошел в продуктовый магазин, а потом пошел в магазин велосипедов»

Слова ‘go’, ‘to’, ‘the’ и ‘store’ встречаются в нашем предложении дважды. В подходе бинарного векторизатора массив только отмечает наличие (1) или отсутствие (0) слова в предложении. Для приложения НЛП может иметь смысл иметь реальное количество слов - давайте посмотрим, как мы можем сделать это с помощью простого изменения.

Граф Векторизатор

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

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

Вспомните наш первый объект словаря:

grocery, I, movie, store, the, theater, to, went

Давайте добавим слова ‘and’, ‘then’ и ‘bike’:

and, bike, grocery, I, movie, store, the, theater, then, to, went

Это увеличит размер нашего массива! Давайте сопоставим предложение «Я пошел в продуктовый магазин, а затем в магазин велосипедов» с новым массивом, который происходит из словаря, сохраняя двоичный формат:

and, bike, grocery, I, movie, store, the, theater, then, to, went
[1,     1,       1, 1,     0,     1,   1,       0,    1,  1,    1]

Теперь, если вместо двоичного векторизатора у нас есть векторизатор подсчета, который подсчитывает слова, у нас есть следующее:

and, bike, grocery, I, movie, store, the, theater, then, to, went
[1,     1,       1, 1,     0,     2,   2,       0,    1,  2,   2]

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

Здесь для этого массива предложений будет показано более высокое значение для слов 'store', 'the', 'to' и 'пошел' - если это что-то, что хорошо для вашего приложения NLP, действительно зависит от того, как вы хотите, чтобы ваш массив передавал информацию из корпуса и какой тип модели вы строите.

Реализация в scikit-learn очень похожа на то, что мы делали раньше:

cvec_pure = CountVectorizer(tokenizer=str.split, binary=False)

Двоичный в данном случае имеет значение False, и создаст более «чистый» векторизатор счетчика. Binary = False фактически является аргументом по умолчанию для объекта CountVectorizer, если вы не объявляете аргумент при вызове функции.

Обновление нашего списка предложений:

sentence_list = ['I went to the grocery store',
'I went to the movie theater',
'I went to the grocery store and then went to the bike store']

И применив наш новый векторизатор счетчиков к нашим предложениям:

cvec_pure.fit_transform(sentence_list).todense()

Вот наш результирующий массив для трех предложений:

[[0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1],
 [0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1],
 [1, 1, 1, 1, 0, 2, 2, 0, 1, 2, 2]]

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

TF-IDF

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

Распространенным способом подхода к корпусу в целом при создании массивов для функций является использование метода матрицы частотно-обратного документа с обратной связью по частоте или обычно называемого TF-IDF.

Формула для TF-IDF кажется сложной, но на самом деле она очень проста - мы воспользуемся самой простой реализацией формулы (есть и другие версии, например, сглаженная, подробности: https: //stats.stackexchange .com / questions / 166812 / почему-добавить-один-в-обратном-документе-частоте ):

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

sentence_list = ['I went to the grocery store',
'I went to the movie theater',
'I went to the grocery store and then went to the bike store']

Давайте получим нашу оценку TF-IDF для слова store (назовем его i) в третьем предложении (назовем его j). Сколько раз слово i встречается в тексте j?

Ответ 2! В этом предложении у нас в два раза больше слова store. Мы можем обновить нашу формулу, поскольку знаем первый член:

Теперь давайте вычислим правую часть уравнения - нам нужно, сколько раз слово i встречается во всех документах, которые есть в нашем корпусе. В этом случае слово store появляется в двух документах, которые у нас есть - мы также можем вычислить N, которое представляет собой количество документов / предложений, которые у нас есть:

Возвращаемое значение из этой формулы составляет приблизительно 0,81,, что является показателем TF-IDF, учитываемым для слова store. в третьем предложении - это значение заменит потенциальную 1 в нашем двоичном векторизаторе или 2 в векторизаторе счетчика для этого значения в массиве.

Каким образом слово будет иметь больший вес в конкретном предложении? Две гипотезы:

  • а) Либо слово i чаще встречается в документе j.
  • б) Слово встречается реже во всем корпусе.

Моделирование обоих сценариев, начиная со сценария а) - если бы слово store появилось в нашем тексте 4 раза, наша оценка была бы выше:

В сценарии b) вы также можете повысить показатель TF-IDF для определенного слова в тексте, если слово встречается реже в корпусе - давайте представим, что хранилище слов появится только в одном из наших предложений, зафиксировав значение 2 для первый срок:

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

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

Опять же, нам не нужно делать все вычисления самостоятельно! В scikit-learn есть классная реализация, которую мы можем использовать:

from sklearn.feature_extraction.text import TfidfVectorizer
tf_idf = TfidfVectorizer(tokenizer=str.split)

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

['and', 'bike', 'grocery', 'i', 'movie', 'store', 'the', 'theater', 'then', 'to', 'went']
[0.31 ,  0.31 ,     0.236,0.18,       0,   0.471, 0.366,         0, 
0.31 , 0.366, 0.366]

Обратите внимание на то, что Store имеет наивысший балл по TFIDF в предложении - это потому, что две вещи:

  • Store повторяется в этом предложении, следовательно, левая часть уравнения имеет более высокое значение.
  • Из повторяющихся слов (store, go, the и to), store - более редкое слово для всего корпуса.

Вы также можете заметить, что значения в реализации scikit-learn находятся в диапазоне от 0 до 1, а не совсем то, что мы получили с вашей упрощенной версией формулы. Это связано с тем, что реализация scikit-learn по умолчанию выполняет нормализацию и сглаживание (вы можете проверить norm и smooth_idf параметры функции.

Размерность

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

Для всех вышеперечисленных подходов реализации scikit-learn имеют два аргумента, которые помогут вам справиться с высокими уровнями размерности:

  • min_df: получает целочисленное значение, которое действует как порог для минимального количества документов N (или процента, если вы передаете число с плавающей запятой), в которых должно присутствовать слово, чтобы его можно было рассматривать как столбец массива.
  • max_features: Получает целое число, которое задает максимальное количество столбцов, которые могут быть у ваших массивов.

Оба этих подхода приводят к потере информации, и их использование в нашем конвейере НЛП, как всегда, зависит от вашего приложения.

Давайте посмотрим на пример нашего векторизатора подсчета с min_df, установленным на 2:

cvec_limit = CountVectorizer(tokenizer=str.split, binary=False, min_df=2)

В возвращаемом массиве вместо размера словаря будут только слова, которые появляются в двух твитах из нашей следующей базы:

sentence_list = ['I went to the grocery store',
'I went to the movie theater',
'I went to the grocery store and then went to the bike store']

Если вы проверите имена функций, у вас будет только массив из 6 столбцов, который соответствует словам:

['grocery', 'i', 'store', 'the', 'to', 'went']

И именно эти слова присутствуют как минимум в двух из трех наших твитов!

Проделайте тот же эксперимент с параметром max_features, чтобы проверить, смогли ли вы понять его интуицию!

Вывод

Во-первых, и это самое важное, вот небольшая суть, которую вы можете использовать в своих проектах:

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

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

Помимо техник, которые я вам показал, проводятся дополнительные исследования - например, превращение векторов Word (https://en.wikipedia.org/wiki/Word_embedding) в векторы предложений или документов, но мы сохраним это для другой пост!

Спасибо, что нашли время прочитать этот пост! Не стесняйтесь добавлять меня в LinkedIn (https://www.linkedin.com/in/ivobernardo/) и проверять веб-сайт моей компании (https://daredata.engineering/home).

Если вы хотите пройти обучение по Analytics, вы также можете посетить мою страницу в Udemy (https://www.udemy.com/user/ivo-bernardo/)

Этот пример взят из моего Курса НЛП для абсолютных новичков, доступного на платформе Udemy - курс подходит для новичков и людей, которые хотят изучить основы обработки естественного языка. Курс также содержит более 50 упражнений по программированию, которые позволят вам попрактиковаться в изучении новых концепций.