Узнайте, как легко создавать, обучать и проверять рекуррентную нейронную сеть

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

Я начну с определения первого необычного термина в названии: Анализ настроений - очень частый термин в классификации текстов и, по сути, использует обработку естественного языка (довольно часто называемую просто НЛП) + машинное обучение. интерпретировать и классифицировать эмоции в текстовой информации. Представьте себе задачу определить, является ли отзыв о продукте положительным или отрицательным; вы могли бы сделать это сами, просто прочитав это, не так ли? Но что происходит, когда компания, в которой вы работаете, продает 2 тыс. Товаров каждый день? Вы делаете вид, что читаете все обзоры и классифицируете их вручную? Честно говоря, ваша работа была бы худшей из всех. Вот где на помощь приходит анализ настроений, который облегчает вашу жизнь и работу.

Давай займемся этим вопросом

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

  • Удалите URL-адреса и адреса электронной почты из каждого образца, потому что они не принесут смысла.
  • Удалите знаки препинания - иначе ваша модель не поймет, что «хорошо!» и «хорошо» на самом деле означают одно и то же.
  • Весь текст в нижнем регистре - потому что вы хотите сделать вводимый текст как можно более общим и избежать того, чтобы, например, «Хорошо» в начале фразы понималось иначе, чем «хорошо» в другом примере.
  • Удалите стоп-слова, потому что они только добавляют шум и не делают данные более значимыми. Кстати, стоп-слова относятся к наиболее распространенным словам в языке, таким как «я», «имею», «есть» и так далее. Надеюсь, вы уловили, потому что официального списка стоп-слов нет.
  • Стемминг / лемматизация: этот шаг не является обязательным, но для большинства специалистов по данным считается решающим. Я покажу вам, что это НЕ ТАК важно для достижения хороших результатов. Выделение основы и лемматизация - очень похожие задачи, оба стремятся извлечь корневые слова из каждого слова предложения данных корпуса. Лемматизация обычно возвращает действительные слова (которые существуют), в то время как методы выделения корня возвращают (в большинстве случаев) сокращенные слова, поэтому лемматизация чаще используется в реальных реализациях. Вот как работают лемматизаторы и стеммеры: предположим, вы хотите найти корень слова «забота»: «Забота» - ›Лемматизация -› «Забота». С другой стороны: «Забота» - ›Основание -› «Автомобиль»; вы поняли суть? Вы можете изучить и то, и другое, и, очевидно, реализовать любое из них, если этого требует бизнес.
  • Преобразование набора данных (текста) в числовые тензоры - обычно называется векторизацией. Если вы помните некоторые строки выше, я объяснил, что, как и все другие нейронные сети, модели глубокого обучения не принимают в качестве входных данных необработанный текст: они работают только с числовыми тензорами, поэтому этот шаг не подлежит обсуждению. Есть несколько способов сделать это; Например, если вы собираетесь использовать классическую модель машинного обучения (не DL), вам определенно следует использовать CountVectorizer, TFIDF Vectorizer или просто базовый, но не очень хороший подход: Bag-Of-Words. Тебе решать. Однако, если вы собираетесь внедрить глубокое обучение, вы, возможно, знаете, что лучший способ - превратить ваши текстовые данные (которые можно понимать как последовательности слов или последовательности символов) в векторы с плавающей запятой низкой размерности - не делайте этого. волнуйтесь, я объясню это немного позже.

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

def depure_data(data):
    
    #Removing URLs with a regular expression
    url_pattern = re.compile(r'https?://\S+|www\.\S+')
    data = url_pattern.sub(r'', data)

    # Remove Emails
    data = re.sub('\S*@\S*\s?', '', data)

    # Remove new line characters
    data = re.sub('\s+', ' ', data)

    # Remove distracting single quotes
    data = re.sub("\'", "", data)
        
    return data

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

def sent_to_words(sentences):
    for sentence in sentences:
        yield(gensim.utils.simple_preprocess(str(sentence),     deacc=True))

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

def detokenize(text):
    return TreebankWordDetokenizer().detokenize(text)

Чтобы запустить все в правильном порядке, вам просто нужно запустить это:

temp = []
#Splitting pd.Series to list
data_to_list = train['selected_text'].values.tolist()
for i in range(len(data_to_list)):
    temp.append(depure_data(data_to_list[i]))
data_words = list(sent_to_words(temp))
data = []
for i in range(len(data_words)):
    data.append(detokenize(data_words[i]))
print(data[:5])

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

['I`d have responded, if I were going',
 'Sooo SAD',
 'bullying me',
 'leave me alone',
 'Sons of ****,']

К этому:

['have responded if were going', 'sooo sad', 'bullying me', 'leave me alone', 'sons of']

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

От предложений к вложениям слов

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

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

Есть два способа получить вложения слов:

  • Используйте предварительно обученный стек встраивания слов поверх вашей модели, так же, как вы использовали бы предварительно обученный слой NN (или группу слоев) - подход очень редко.
  • Изучите вложения слов с нуля. Чтобы добиться этого, вы должны начать со случайных векторов слов и постепенно выучить значимые слова, точно так же, как NN изучает их веса. Это вариант, который мы будем использовать, и на самом деле разумно изучать новое пространство встраивания с каждой новой задачей. К счастью, этот шаг очень прост с TensorFlow или Keras, и вы можете реализовать встраивание слов точно так же, как еще один слой в стеке NN.

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

from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
from keras import regularizers

max_words = 5000
max_len = 200

tokenizer = Tokenizer(num_words=max_words)
tokenizer.fit_on_texts(data)
sequences = tokenizer.texts_to_sequences(data)
tweets = pad_sequences(sequences, maxlen=max_len)
print(tweets)

Результат, который вы получите, будет выглядеть так:

[[   0    0    0 ...   68  146   41]
 [   0    0    0 ...    0  397   65]
 [   0    0    0 ...    0    0   11]
 ...
 [   0    0    0 ...  372   10    3]
 [   0    0    0 ...   24  542    4]
 [   0    0    0 ... 2424  199  657]]

Что означает предыдущий шаг? Давайте возьмем определение из официальной документации Keras, вы поправите суть:

Эта функция преобразует список (длины num_samples) последовательностей (списки целых чисел) в массив 2D Numpy формы (num_samples, num_timesteps). num_timesteps - это либо аргумент maxlen, если он указан, либо длина самой длинной последовательности в списке.

Последовательности, которые короче num_timesteps, дополняются value, пока они не станут num_timesteps длинными.

Последовательности длиннее num_timesteps обрезаются, чтобы они соответствовали желаемой длине.

Слой встраивания

Чрезвычайно важно помнить, что независимо от того, используете ли вы TensorFlow или любой другой Astraction API, например Keras , вы должны получить тот же результат в конце обучения. . В этой возможности мы будем использовать Keras по очевидным причинам: его очень легко реализовать. Вот как вы создаете слой встраивания:

from keras.layers import Embedding
embedding_layer = Embedding(1000, 64)

Вышеупомянутый слой принимает двумерные целочисленные тензоры формы (образцы, длина_последовательности) и по крайней мере два аргумента: количество возможных токенов и размерность вложений (здесь 1000 и 64 соответственно). Чтобы быть более образным, просто представьте, что слой внедрения - это словарь, который связывает целочисленные индексы с плотными векторами. Наконец, он возвращает трехмерный тензор формы с плавающей запятой (samples, sequence_length, embedding_dimensionality), который теперь может обрабатываться нашей нейронной сетью. Давайте поговорим об этой теме, особенно о рекуррентных нейронных сетях, которые лучше всего подходят для обработки текстовых последовательностей.

Рекуррентные нейронные сети стали проще

Обычно другие типы нейронных сетей, такие как плотно связанные сети или сверточные сети, не имеют памяти, это означает, что каждый отдельный вход обрабатывается независимо и не связан с другими. Это противоположно тому, что вы обычно делаете при чтении абзаца: когда вы читаете, вы сохраняете в памяти то, что читали в предыдущих строках, верно? Вы понимаете весь смысл, и это в точности тот же принцип, что и RNN. Они обрабатывают последовательности, повторяя элементы последовательности и сохраняя информацию, относящуюся к тому, что было обработано на данный момент. Если честно, математика, скрытая под капотом RNN, - это тема, которую вы должны изучить самостоятельно, чтобы понять ее логику. Я предлагаю вам прочитать Learning TensorFlow Тома Хоупа (доступно здесь), в котором очень легко объясняется весь процесс.

В этой статье я реализую три типа RNN: одну модель LSTM (долгосрочной краткосрочной памяти), двунаправленную LSTM и очень редко используемую модель Conv1D. В качестве бонуса я показываю, как реализовать модель SimpleRNN, но, честно говоря, она нигде не используется в производственной среде, потому что она чрезвычайно проста.

Слои LSTM

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

from keras.models import Sequential
from keras import layers
from keras import regularizers
from keras import backend as K
from keras.callbacks import ModelCheckpoint
model1 = Sequential()
model1.add(layers.Embedding(max_words, 20)) #The embedding layer
model1.add(layers.LSTM(15,dropout=0.5)) #Our LSTM layer
model1.add(layers.Dense(3,activation='softmax'))


model1.compile(optimizer='rmsprop',loss='categorical_crossentropy', metrics=['accuracy'])

checkpoint1 = ModelCheckpoint("best_model1.hdf5", monitor='val_accuracy', verbose=1,save_best_only=True, mode='auto', period=1,save_weights_only=False)
history = model1.fit(X_train, y_train, epochs=70,validation_data=(X_test, y_test),callbacks=[checkpoint1])

В приведенном выше коде следует выделить несколько моментов: при реализации модели Keras Sequential все дело в наложении слоев. Слои LSTM (как и все другие слои RNN) могут принимать несколько аргументов, но те, которые я определил, равны 15, что является количеством скрытых единиц в слое (должно быть положительным целым числом и представляет размерность выходного пространства) и коэффициент отсева слоя. Отсев - один из наиболее эффективных и наиболее часто используемых методов регуляризации для сетевых сетей. Он заключается в случайном отключении скрытых модулей во время обучения, таким образом сеть не на 100% полагается на все свои нейроны, а вместо этого заставляет сам, чтобы найти более значимые закономерности в данных, чтобы увеличить метрику, которую вы пытаетесь оптимизировать. Есть несколько других аргументов, которые необходимо передать, вы можете найти полную документацию здесь, но для этого конкретного примера эти настройки позволят добиться хороших результатов.

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

model0 = Sequential()
model0.add(layers.Embedding(max_words, 15))
model0.add(layers.SimpleRNN(15,return_sequences=True))
model0.add(layers.SimpleRNN(15))
model0.add(layers.Dense(3,activation='softmax'))

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

При компиляции модели я использую оптимизатор RMSprop со скоростью обучения по умолчанию, но на самом деле это зависит от каждого разработчика. Кто-то любит Адама, кто-то Ададелту и так далее. Если честно, в большинстве случаев достаточно RMSprop или Adam. Если вы не знаете, что такое оптимизатор, это просто механизм, который постоянно вычисляет градиент потерь и определяет, как двигаться против функции потерь, чтобы найти ее глобальные минимумы и, следовательно, найти лучшие параметры сети (модель ядра и его веса смещения). В качестве функции потерь я использую category_crossentropy (проверьте таблицу), которая обычно используется, когда вы имеете дело с задачами многоклассовой классификации. С другой стороны, вы должны использовать binary_crossentropy, когда требуется двоичная классификация.

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

Это оценка валидации, полученная этой архитектурой NN в ее последнюю эпоху:

Epoch 70/70
645/645 [==============================] - ETA: 0s - loss: 0.3090 - accuracy: 0.8881
Epoch 00070: val_accuracy did not improve from 0.84558

Давайте сравним это с более сложной сетью.

Двунаправленные слои

Вот как выглядит реализация BidRNN в нашем примере:

model2 = Sequential()
model2.add(layers.Embedding(max_words, 40, input_length=max_len))
model2.add(layers.Bidirectional(layers.LSTM(20,dropout=0.6)))
model2.add(layers.Dense(3,activation='softmax'))
model2.compile(optimizer='rmsprop',loss='categorical_crossentropy', metrics=['accuracy'])
checkpoint2 = ModelCheckpoint("best_model2.hdf5", monitor='val_accuracy', verbose=1,save_best_only=True, mode='auto', period=1,save_weights_only=False)
history = model2.fit(X_train, y_train, epochs=70,validation_data=(X_test, y_test),callbacks=[checkpoint2])

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

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

Epoch 70/70
644/645 [============================>.] - ETA: 0s - loss: 0.2876 - accuracy: 0.8965
Epoch 00070: val_accuracy did not improve from 0.84849

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

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

Идем еще дальше - одномерные сверточные нейронные сети

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

Он использует те же принципы, что и классические 2D ConvNets, используемые для классификации изображений. Сверточные слои извлекают участки из тензоров 1D / 2D (в зависимости от типа задачи и слоя) и применяют одинаковые сверточные преобразования к каждому из них (получая в качестве вывода несколько подпоследовательностей). Я не буду углубляться в такое объяснение, потому что это выходит за рамки данной статьи, но если вы хотите полностью понять, как работают эти слои, я бы посоветовал вам ознакомиться с ранее рекомендованной книгой. Самым важным фактом этих слоев является то, что они могут распознавать образцы в последовательности - образец, изученный в определенной позиции в предложении, позже может быть идентифицирован в другом месте или даже в другом предложении.

Вот как реализованы 1D ConvNets:

model3.add(layers.Embedding(max_words, 40, input_length=max_len))
model3.add(layers.Conv1D(20, 6, activation='relu',kernel_regularizer=regularizers.l1_l2(l1=2e-3, l2=2e-3),bias_regularizer=regularizers.l2(2e-3)))
model3.add(layers.MaxPooling1D(5))
model3.add(layers.Conv1D(20, 6, activation='relu',kernel_regularizer=regularizers.l1_l2(l1=2e-3, l2=2e-3),bias_regularizer=regularizers.l2(2e-3)))
model3.add(layers.GlobalMaxPooling1D())
model3.add(layers.Dense(3,activation='softmax'))
model3.compile(optimizer='rmsprop',loss='categorical_crossentropy',metrics=['acc'])
history = model3.fit(X_train, y_train, epochs=70,validation_data=(X_test, y_test))

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

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

Epoch 70/70
645/645 [==============================] - 5s 7ms/step - loss: 0.3096 - acc: 0.9173 - val_loss: 0.5819 - val_acc: 0.8195

И его наилучшая полученная точность проверки составила около 82%. Он очень быстро переоснащается, даже когда я реализовал очень радикальную регуляризацию.

Проверка нашей лучшей модели

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

Из изображения выше мы можем сделать вывод: 81% положительных оценок были классифицированы как положительные, 80% отрицательных оценок были классифицированы как отрицательные и 91% нейтральных оценок были классифицированы как нейтральные. Это не самые лучшие прогнозы, но они являются хорошей базой для работы над еще лучшими моделями. В бизнес-сценарии вам потребуется около 95% в самых простых случаях.

Если вы хотите проверить, как это работает на вводе, сделанном вами, просто вычислите следующие строки:

sentiment = ['Neutral','Negative','Positive']
sequence = tokenizer.texts_to_sequences(['this data science article is the best ever'])
test = pad_sequences(sequence, maxlen=max_len)
sentiment[np.around(best_model.predict(test), decimals=0).argmax(axis=1)[0]]

И вывод будет:

'Positive'

Последние мысли

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

Есть несколько способов выполнить такую ​​задачу. Вы можете использовать Google Cloud Platform, пойти по пути Azure, даже по более дешевому пути Heroku, но давайте будем честными: большинство крупнейших компаний принимают AWS в качестве основного поставщика общедоступного облака, и у этих ребят есть фантастическая платформа для создания и обучения. и развернуть модели машинного обучения: AWS SageMaker; где есть масса документации. Я опубликую еще одно пошаговое руководство о том, как легко развертывать на нем модели. Я надеюсь увидеть вас там!