Цель этой статьи - помочь читателю понять, как использовать встраивание слов и глубокое обучение при создании классификатора текста.

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

Наконец, демонстрация всех концепций будет применена к набору данных, опубликованному в твиттере, о том, является ли твит о стихийном бедствии или нет.

Основными технологиями, используемыми в этой статье, являются Python и Keras API.

Полностью функционирующий конвейер классификации текста с набором данных из Twitter можно найти здесь: https://github.com/Eligijus112/twitter-genuine-tweets.

Файл вложений Word, который используется в этой статье, можно найти здесь: https://nlp.stanford.edu/projects/glove/.

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

  • Разделите данные на текст (X) и ярлыки (Y)
  • Предварительная обработка X
  • Создайте матрицу встраивания слов из X
  • Создайте тензорный ввод из X
  • Обучите модель глубокого обучения, используя тензорные входные данные и метки (Y)
  • Делайте прогнозы на основе новых данных

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

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

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

«Папа» = [0,1548, 0,4848, 1,864]

«Мама» = [0,8785, 0,8974, 2,794]

Короче говоря, вложения слов - это числовые векторы, представляющие строки.

На практике представления слов являются 100-, 200- или 300-мерными векторами, и они обучаются на очень больших текстах.

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

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

Например,

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

Здесь 7 уникальных слов. Используя один вектор с горячим кодированием, мы представляем каждое слово следующим образом:

Clark = [1, 0, 0, 0, 0, 0, 0]
likes = [0, 1, 0, 0, 0, 0, 0]
to = [0, 0, 1, 0, 0, 0, 0]
walk = [0, 0, 0, 1, 0, 0, 0]
in = [0, 0, 0, 0, 1, 0, 0]
the = [0, 0, 0, 0, 0, 1, 0]
park = [0, 0, 0, 0, 0, 0, 1]

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

Clark = [0.13, 0.61]
likes = [0.23, 0.66]
to = [0.55, 0.11]
walk = [0.03, 0.01]
in = [0.15, 0.69]
the = [0.99, 0.00]
park = [0.98, 0.12]

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

Вложения слов создаются с использованием нейронной сети с одним входным слоем, одним скрытым слоем и одним выходным слоем.

Подробнее о создании встраивания слов читайте в статье:



Чтобы компьютер мог определить, какой текст «хороший», а какой «плохой», нам нужно пометить его. Может быть любое количество классов, и сами классы могут означать очень широкий спектр вещей. Построим текст:

d = [
  ('This article is awesome', 1),
  ('There are just too much words here', 0), 
  ('The math is actually wrong here', 0),
  ('I really enjoy learning new stuff', 1),
  ('I am kinda lazy so I just skim these texts', 0),
  ('Who cares about AI?', 0),
  ('I will surely be a better person after reading this!', 1),
  ('The author is pretty cute :)', 1)
]

У нас есть 8 кортежей, где первая координата - это текст, а вторая координата - это метка. Метка 0 означает негативное настроение, а метка 1 означает позитивное настроение. Чтобы построить работающую модель, нам потребуется намного больше данных (в моей практике тысяча или более помеченных точек данных начали бы давать хорошие результаты, если бы было только два класса и классы были бы сбалансированы).

Давайте сделаем классическую предварительную обработку текста:

X_train = [x[0] for x in d] # Text
Y_train = [y[1] for y in d] # Label
X_train = [clean_text(x) for x in X_train]

Очищенный текст (X_train):

'this article is awesome'
'there are just too much words here'
'the math is actually wrong here'
'i really enjoy learning new stuff'
'i am kinda lazy so i just skim these texts'
'who cares about ai'
'i will surely be a better person after reading this'
'the author is pretty cute'

Ярлыки (Y_train):

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

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

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

Обычный формат, в котором хранятся вложения слов, - это текстовый документ.

Назовем указанный выше файл внедрения mini_embedding.txt. Для быстрого копирования и вставки используйте:

beautiful 1.5804182 0.25605154
boy -0.4558624 -1.5827272
can 0.9358587 -0.68037164
children -0.51683635 1.4153042
daughter 1.1436981 0.66987246
family -0.33289963 0.9955545

В этом примере размер внедрения равен 2, но в слове embeddings из ссылки https://nlp.stanford.edu/projects/glove/ размер равен 300. В любом случае t он Структура состоит в том, что слово является первым элементом, за которым следуют коэффициенты, разделенные пробелами. Координаты заканчиваются, когда в конце строки появляется новый разделитель строк.

Для чтения таких текстовых документов создадим класс:

Предположим, у вас есть файл внедрения в папке embeddings.

embedding = Embeddings(
  'embeddings/mini_embedding.txt', 
  vector_dimension=2
)
embedding_matrix = embedding.create_embedding_matrix()

Мы еще не сканировали никакие документы, поэтому матрица внедрения вернет все слова из файла mini_embeddings.txt:

array([[ 1.58041823,  0.25605154],
       [-0.4558624 , -1.58272719],
       [ 0.93585873, -0.68037164],
       [-0.51683635,  1.41530418],
       [ 1.1436981 ,  0.66987246],
       [-0.33289963,  0.99555451]])

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

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

Предварительно обученные вложения слов в модели глубокого обучения помещаются в матрицу и используются во входном слое как веса. Из документации Keras API https://keras.io/layers/embeddings/:

keras.layers.Embedding(input_dim, output_dim,...)
Turns positive integers (indexes) into dense vectors of fixed size. eg. [[4], [20]] -> [[0.25, 0.1], [0.6, -0.2]]
This layer can only be used as the first layer in a deep learning model.

Двумя основными входными аргументами являются input_dim и output_dim.

input_dim равно общему количеству уникальных слов в нашем тексте (или определенному количеству уникальных слов, которые определяет пользователь).

output_dim равен размерам встраиваемого вектора.

Для создания уникального словаря слов мы будем использовать метод Tokenizer () из библиотеки Keras.

from keras.preprocessing.text import Tokenizer
tokenizer = Tokenizer()
tokenizer.fit_on_texts(X_train)

Напоминаем, что наш предварительно обработанный X_train:

'this article is awesome'
'there are just too much words here'
'the math is actually wrong here'
'i really enjoy learning new stuff'
'i am kinda lazy so i just skim these texts'
'who cares about ai'
'i will surely be a better person after reading this'
'the author is pretty cute'

Метод Tokenizer () создает внутренний словарь уникальных слов и присваивает каждому слову целое число. Вывод tokenizer.word_index:

{'i': 1,
 'is': 2,
 'this': 3,
 'just': 4,
 'here': 5,
 'the': 6,
 'article': 7,
 'awesome': 8,
 'there': 9,
 'are': 10,
 'too': 11,
 'much': 12,
 'words': 13,
 'math': 14,
 'actually': 15,
 'wrong': 16,
 'really': 17,
 'enjoy': 18,
 'learning': 19,
 'new': 20,
 'stuff': 21,
 'am': 22,
 'kinda': 23,
 'lazy': 24,
 'so': 25,
 'skim': 26,
 'these': 27,
 'texts': 28,
 'who': 29,
 'cares': 30,
 'about': 31,
 'ai': 32,
 'will': 33,
 'surely': 34,
 'be': 35,
 'a': 36,
 'better': 37,
 'person': 38,
 'after': 39,
 'reading': 40,
 'author': 41,
 'pretty': 42,
 'cute': 43}

В наших текстах X_train 43 уникальных слова. Позволяет преобразовать тексты в индексированные списки:

tokenizer.texts_to_sequences(X_train)
[[3, 7, 2, 8],
 [9, 10, 4, 11, 12, 13, 5],
 [6, 14, 2, 15, 16, 5],
 [1, 17, 18, 19, 20, 21],
 [1, 22, 23, 24, 25, 1, 4, 26, 27, 28],
 [29, 30, 31, 32],
 [1, 33, 34, 35, 36, 37, 38, 39, 40, 3],
 [6, 41, 2, 42, 43]]

Первое предложение в нашей матрице X_train «эта статья потрясающая» преобразуется в список [3, 7, 2, 8]. Эти индексы представляют собой ключевые значения в словаре, созданном токенизатором:

{...
 'is': 2,
 'this': 3,
 ...
 'article': 7,
 'awesome': 8,
 ...}

Метод text_to_sequence () дает нам список списков, каждый элемент которого имеет разные размеры и не структурирован. Любая модель машинного обучения должна знать количество измерений признаков, и это число должно быть одинаковым как для обучения, так и для прогнозирования новых наблюдений. Чтобы преобразовать последовательности в хорошо структурированную матрицу для обучения глубокому обучению, мы будем использовать метод pad_sequances () от Кераса:

import numpy as np
from keras.preprocessing.sequence import pad_sequences
# Getting the biggest sentence
max_len = np.max([len(text.split()) for text in X_train])
# Creating the padded matrices
X_train_NN = tokenizer.texts_to_sequences(X_train)
X_train_NN = pad_sequences(string_list, maxlen=max_len)

Объект X_train_NN выглядит так:

array([[ 0,  0,  0,  0,  0,  0,  3,  7,  2,  8],
       [ 0,  0,  0,  9, 10,  4, 11, 12, 13,  5],
       [ 0,  0,  0,  0,  6, 14,  2, 15, 16,  5],
       [ 0,  0,  0,  0,  1, 17, 18, 19, 20, 21],
       [ 1, 22, 23, 24, 25,  1,  4, 26, 27, 28],
       [ 0,  0,  0,  0,  0,  0, 29, 30, 31, 32],
       [ 1, 33, 34, 35, 36, 37, 38, 39, 40,  3],
       [ 0,  0,  0,  0,  0,  6, 41,  2, 42, 43]])

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

Чтобы создать аккуратный ввод для нейронной сети с использованием предварительно обработанного текста, я использую свой определенный класс TextToTensor:

Тензор - это контейнер, в котором могут храниться данные в N измерениях. Вектор может содержать данные в одном измерении, матрица - в двух, а тензор - в N. Подробнее о тензорах:

Https://www.kdnuggets.com/2018/05/wtf-tensor.html

Полное использование TextToTensor:

# Tokenizing the text
tokenizer = Tokenizer()
tokenizer.fit_on_texts(X_train)
# Getting the longest sentence
max_len = np.max([len(text.split()) for text in X_train])
# Converting to tensor
TextToTensor_instance = TextToTensor(
tokenizer=tokenizer,
max_len=max_len
)
X_train_NN = TextToTensor_instance.string_to_tensor(X_train)

Теперь, когда мы можем создать тензор из текстов, мы можем начать использовать слой Встраивание из Keras API.

from keras.models import Sequential
from keras.layers import Embedding

model = Sequential()
model.add(Embedding(
  input_dim=44, 
  output_dim=3, 
  input_length=max_len))

model.compile('rmsprop', 'mse')
output_array = model.predict(X_train_NN)[0]

Обратите внимание, что в слое «Встраивание» input_dim равно 44, но в наших текстах всего 43 уникальных слова. Это связано с определением встраивания в Keras API:

input_dim: int ›0. Размер словаря, т. е. максимальный целочисленный индекс +1.

Output_array выглядит так:

array([[-0.03353775,  0.01123261,  0.03025569],
       [-0.03353775,  0.01123261,  0.03025569],
       [-0.03353775,  0.01123261,  0.03025569],
       [-0.03353775,  0.01123261,  0.03025569],
       [-0.03353775,  0.01123261,  0.03025569],
       [-0.03353775,  0.01123261,  0.03025569],
       [ 0.04183744, -0.00413301,  0.04792741],
       [-0.00870543, -0.00829206,  0.02079277],
       [ 0.02819189, -0.04957005,  0.03384084],
       [ 0.0394035 , -0.02159669,  0.01720046]], dtype=float32)

Входная последовательность (первый элемент X_train_NN):

array([0, 0, 0, 0, 0, 0, 3, 7, 2, 8])

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

Чтобы решить эту проблему, мы будем использовать предварительно обученные вложения слов из отдела НЛП Стэнфордского университета (https://nlp.stanford.edu/projects/glove/). Для создания матрицы вложения мы будем использовать ранее определенный метод.

Предположим, что X_train снова является списком предварительно обработанного текста.

embed_path = 'embeddings\\glove.840B.300d.txt'
embed_dim = 300
# Tokenizing the text
tokenizer = Tokenizer()
tokenizer.fit_on_texts(X_train)
# Creating the embedding matrix
embedding = Embeddings(embed_path, embed_dim)
embedding_matrix = embedding.create_embedding_matrix(tokenizer, len(tokenizer.word_counts))

В то время как документ glove.840B.300d.txt содержит сотни тысяч уникальных слов, окончательная форма матрицы внедрения - (44, 300). Это потому, что мы хотим сохранить как можно больше памяти, а количество уникальных слов во всем нашем документе равно 44. Сохранение координат всех остальных слов из текстового документа было бы пустой тратой, потому что мы не стали бы их нигде использовать.

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

from keras.models import Sequential
from keras.layers import Embedding
# Converting to tensor
TextToTensor_instance = TextToTensor(
tokenizer=tokenizer,
max_len=max_len
)
X_train_NN = TextToTensor_instance.string_to_tensor(X_train)
model = Sequential()
model.add(Embedding(
  input_dim=44, 
  output_dim=300, 
  input_length=max_len,
  weights=[embedding_matrix]))

model.compile('rmsprop', 'mse')
output_array = model.predict(X_train_NN)[0]

Форма output_array теперь (10, 300), а результат выглядит так:

array([[ 0.18733 ,  0.40595 , -0.51174 , ...,  0.16495 ,  0.18757 ,
         0.53874 ],
       [ 0.18733 ,  0.40595 , -0.51174 , ...,  0.16495 ,  0.18757 ,
         0.53874 ],
       [ 0.18733 ,  0.40595 , -0.51174 , ...,  0.16495 ,  0.18757 ,
         0.53874 ],
       ...,
       [-0.34338 ,  0.1677  , -0.1448  , ...,  0.095014, -0.073342,
         0.47798 ],
       [-0.087595,  0.35502 ,  0.063868, ...,  0.03446 , -0.15027 ,
         0.40673 ],
       [ 0.16718 ,  0.30593 , -0.13682 , ..., -0.035268,  0.1281  ,
         0.023683]], dtype=float32)

К этому моменту мы рассмотрели:

  • Что такое встраивание слов
  • Создание тензоров из текста
  • Создание матрицы встраивания слов
  • Что такое слой Keras Embedded
  • Как использовать матрицу встраивания

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

# Importing generic python packages
import pandas as pd
# Reading the data
train = pd.read_csv('data/train.csv')[['text', 'target']]
test = pd.read_csv('data/test.csv')
# Creating the input for the pipeline
X_train = train['text'].tolist()
Y_train = train['target'].tolist()
X_test = test['text'].tolist()

Форма данных поезда - (7613, 2), что означает 7613 твитов, с которыми можно работать. Проверим распределение твитов:

train.groupby(['target'], as_index=False).count()

Как мы видим, классы, по крайней мере, для случая реальных данных, сбалансированы.

Пример «хороших» твитов:

[
'Our Deeds are the Reason of this #earthquake May ALLAH Forgive us all',
'Forest fire near La Ronge Sask. Canada',
"All residents asked to 'shelter in place' are being notified by officers. No other evacuation or shelter in place orders are expected",
'13,000 people receive #wildfires evacuation orders in California ',
'Just got sent this photo from Ruby #Alaska as smoke from #wildfires pours into a school'
]

Пример плохих твитов:

[
"What's up man?",
'I love fruits',
'Summer is lovely',
'My car is so fast',
'What a goooooooaaaaaal!!!!!!'
]

Давайте сделаем предварительную обработку текста и посмотрим на главные слова:

# Counting the number of words
from collections import Counter
# Plotting functions
import matplotlib.pyplot as plt
X_train = [clean_text(text) for text in X_train]
Y_train = np.asarray(Y_train)
# Tokenizing the text
tokenizer = Tokenizer()
tokenizer.fit_on_texts(X_train)
# Getting the most frequent words
d1 = train.loc[train['target']==1, 'text'].tolist()
d0 = train.loc[train['target']==0, 'text'].tolist()
d1 = [clean_text(x, stop_words=stop_words) for x in d1]
d0 = [clean_text(x, stop_words=stop_words) for x in d0]
d1_text = ' '.join(d1).split()
d0_text = ' '.join(d0).split()
topd1 = Counter(d1_text)
topd0 = Counter(d0_text)
topd1 = topd1.most_common(20)
topd0 = topd0.most_common(20)
plt.bar(range(len(topd1)), [val[1] for val in topd1], align='center')
plt.xticks(range(len(topd1)), [val[0] for val in topd1])
plt.xticks(rotation=70)
plt.title('Disaster tweets')
plt.show()
plt.bar(range(len(topd0)), [val[1] for val in topd0], align='center')
plt.xticks(range(len(topd0)), [val[0] for val in topd0])
plt.xticks(rotation=70)
plt.title('Not disaster tweets')
plt.show()

Слова «не катастрофа» более общие, чем слова «катастрофа». Можно ожидать, что вложения GloVe и модель глубокого обучения смогут уловить эти различия.

Распределение количества слов в каждом твите:

Из приведенного выше распределения можно сказать, что создание входных тензоров с размером столбца 20 исключает только очень небольшое количество слов в твитах. Что касается профессионалов, мы выиграем много вычислительного времени.

Архитектура модели глубокого обучения следующая:

Конвейер, который охватывает все, что было упомянуто в этой статье, также определяется как класс в python:

Весь код и весь рабочий конвейер можно найти здесь:



Для обучения модели используйте код:

results = Pipeline(
X_train=X_train,
Y_train=Y_train,
embed_path='embeddings\\glove.840B.300d.txt',
embed_dim=300,
stop_words=stop_words,
X_test=X_test,
max_len=20,
epochs=10,
batch_size=256
)

Теперь создадим два текста:

good = [«Пожар в Вильнюсе! Где пожарная команда ??? #скорая медицинская помощь"]

bad = [«Суши или пицца? Жизнь тяжела :(("]

TextToTensor_instance = TextToTensor(
tokenizer=results.tokenizer,
max_len=20
)
# Converting to tensors
good_nn = TextToTensor_instance.string_to_tensor(good)
bad_nn = TextToTensor_instance.string_to_tensor(bad)
# Forecasting
p_good = results.model.predict(good_nn)[0][0]
p_bad = results.model.predict(bad_nn)[0][0]

Значения p_bad = 0,014 и p_good = 0,963. Эти вероятности относятся к вопросу о том, является ли твит о катастрофе или нет. Итак, твит о суши получил очень низкий балл, а твит о костре получил высокий балл. Это означает, что логика, представленная в этой статье, работает как минимум с надуманными предложениями.