Узнайте, как использовать RNN для пометки слов в корпусе английского языка их тегом части речи (POS)

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

Затем два важных передовых варианта RNN, которые позволили обучать большие сети на реальных наборах данных. Хотя RNN способны решать множество задач последовательности, их архитектура сама по себе является их самым большим врагом из-за проблем, связанных с увеличением и исчезновением градиентов, которые возникают во время обучения RNN. Эта проблема решается двумя популярными стробированными архитектурами RNN - Long, Short Term Memory (LSTM) и Gated Recurrent Unit (GRU). Мы рассмотрим все эти модели в отношении тегов POS.

Маркировка POS - Обзор

Процесс классификации слов по их частям речи и присвоению им соответствующих ярлыков известен как тегирование части речи или просто тегирование POS. . В библиотеке NLTK есть несколько корпусов, содержащих слова и их теги POS. Я буду использовать корпуса с тегами POS, например treebank, conll2000, и коричневый от NLTK, чтобы продемонстрировать ключевые концепции. Для непосредственного доступа к кодам на Kaggle опубликована соответствующая записная книжка.



В следующей таблице представлена ​​информация о некоторых основных тегах:

Макет статьи

  1. Предварительная обработка данных
  2. Вложения слов
  3. Ванильный РНН
  4. LSTM
  5. ГРУ
  6. Двунаправленный LSTM
  7. Оценка модели

Импорт набора данных

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

# Importing and Loading the data into data frame
# load POS tagged corpora from NLTK
treebank_corpus = treebank.tagged_sents(tagset='universal')
brown_corpus = brown.tagged_sents(tagset='universal')
conll_corpus = conll2000.tagged_sents(tagset='universal')

# Merging the dataframes to create a master df
tagged_sentences = treebank_corpus + brown_corpus + conll_corpus

1. Предварительная обработка данных

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

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

# let's look at the data
tagged_sentences[7]

Разделите данные словами (X) и тегами (Y)

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

X = [] # store input sequence
Y = [] # store output sequence
for sentence in tagged_sentences:
 X_sentence = []
 Y_sentence = []
 for entity in sentence: 
 X_sentence.append(entity[0]) # entity[0] contains the word
 Y_sentence.append(entity[1]) # entity[1] contains corresponding tag
 
 X.append(X_sentence)
 Y.append(Y_sentence)
num_words = len(set([word.lower() for sentence in X for word in sentence]))
num_tags   = len(set([word.lower() for sentence in Y for word in sentence]))
print("Total number of tagged sentences: {}".format(len(X)))
print("Vocabulary size: {}".format(num_words))
print("Total number of tags: {}".format(num_tags))

# let’s look at first data point
# this is one data point that will be fed to the RNN
print(‘sample X: ‘, X[0], ‘\n’)
print(‘sample Y: ‘, Y[0], ‘\n’)

# In this many-to-many problem, the length of each input and output sequence must be the same.
# Since each word is tagged, it’s important to make sure that the length of input sequence equals the output sequence
print(“Length of first input sequence : {}”.format(len(X[0])))
print(“Length of first output sequence : {}”.format(len(Y[0])))

Следующее, что нам нужно выяснить, это как мы будем передавать эти входные данные в RNN. Если нам нужно передать слова в качестве входных данных в какие-либо нейронные сети, то мы, по сути, должны преобразовать их в числа. Нам нужно создать вложение слов или горячие векторы, то есть вектор числовой формы каждого слова. Для начала мы сначала закодируем ввод и вывод, которые дадут слепой уникальный идентификатор каждому слову во всем корпусе входных данных. С другой стороны, у нас есть матрица Y (теги / выходные данные). У нас есть двенадцать тегов POS, каждый из которых рассматривается как класс, и каждый тег pos преобразуется в одноразовое кодирование длины двенадцать. Мы будем использовать функцию Tokenizer () из библиотеки Keras для кодирования текстовой последовательности в целочисленную последовательность.

Векторизуйте X и Y

# encode X
word_tokenizer = Tokenizer()              # instantiate tokeniser
word_tokenizer.fit_on_texts(X)            # fit tokeniser on data
# use the tokeniser to encode input sequence
X_encoded = word_tokenizer.texts_to_sequences(X)  
# encode Y
tag_tokenizer = Tokenizer()
tag_tokenizer.fit_on_texts(Y)
Y_encoded = tag_tokenizer.texts_to_sequences(Y)
# look at first encoded data point
print("** Raw data point **", "\n", "-"*100, "\n")
print('X: ', X[0], '\n')
print('Y: ', Y[0], '\n')
print()
print("** Encoded data point **", "\n", "-"*100, "\n")
print('X: ', X_encoded[0], '\n')
print('Y: ', Y_encoded[0], '\n')

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

Последовательности пэдов

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

# Pad each sequence to MAX_SEQ_LENGTH using KERAS’ pad_sequences() function. 
# Sentences longer than MAX_SEQ_LENGTH are truncated.
# Sentences shorter than MAX_SEQ_LENGTH are padded with zeroes.
# Truncation and padding can either be ‘pre’ or ‘post’. 
# For padding we are using ‘pre’ padding type, that is, add zeroes on the left side.
# For truncation, we are using ‘post’, that is, truncate a sentence from right side.
# sequences greater than 100 in length will be truncated
MAX_SEQ_LENGTH = 100
X_padded = pad_sequences(X_encoded, maxlen=MAX_SEQ_LENGTH, padding=”pre”, truncating=”post”)
Y_padded = pad_sequences(Y_encoded, maxlen=MAX_SEQ_LENGTH, padding=”pre”, truncating=”post”)
# print the first sequence
print(X_padded[0], "\n"*3)
print(Y_padded[0])

2. Вложения слов

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

Однако для представления каждого тега в Y мы просто будем использовать схему однократного кодирования, поскольку в наборе данных всего 12 тегов, и у LSTM не возникнет проблем с изучением собственного представления этих тегов.

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

  1. модель word2vec
  2. Модель перчатки

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

Размер встраиваемого слова: (VOCABULARY_SIZE, EMBEDDING_DIMENSION)

Используйте вложения слов для входных последовательностей (X)

# word2vec
path = ‘../input/wordembeddings/GoogleNews-vectors-negative300.bin’
# load word2vec using the following function present in the gensim library
word2vec = KeyedVectors.load_word2vec_format(path, binary=True)
# assign word vectors from word2vec model
# each word in word2vec model is represented using a 300 dimensional vector
EMBEDDING_SIZE  = 300  
VOCABULARY_SIZE = len(word_tokenizer.word_index) + 1
# create an empty embedding matix
embedding_weights = np.zeros((VOCABULARY_SIZE, EMBEDDING_SIZE))
# create a word to index dictionary mapping
word2id = word_tokenizer.word_index
# copy vectors from word2vec model to the words present in corpus
for word, index in word2id.items():
    try:
        embedding_weights[index, :] = word2vec[word]
    except KeyError:
        pass

Используйте однократное кодирование для выходных последовательностей (Y)

# use Keras’ to_categorical function to one-hot encode Y
Y = to_categorical(Y)

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

Перед использованием RNN мы должны убедиться, что размеры данных соответствуют ожиданиям RNN. В общем, RNN ожидает следующей формы

Форма X: (#samples, #timesteps, #features)

Форма Y: (#samples, #timesteps, #features)

Теперь могут быть различные варианты формы, которую вы используете для питания RNN, в зависимости от типа архитектуры. Поскольку проблема, над которой мы работаем, имеет архитектуру «многие ко многим», входные и выходные данные включают количество временных шагов, которое представляет собой не что иное, как длину последовательности. Но обратите внимание, что тензор X не имеет третьего измерения, то есть количества функций. Это потому, что мы собираемся использовать встраивание слов перед подачей данных в RNN, и, следовательно, нет необходимости явно упоминать третье измерение. Это потому, что, когда вы используете слой Embedding () в Keras, данные обучения будут автоматически преобразованы в (#samples, #timesteps, #features), где #features будет размер внедрения (и обратите внимание, что слой внедрения всегда является самым первым слоем RNN). При использовании слоя внедрения нам нужно только изменить форму данных на (#samples, #timesteps), что мы и сделали. Однако обратите внимание, что вам нужно будет придать ему форму (#samples, #timesteps, #features), если вы не используете слой Embedding () в Keras.

3. Ванильный РНН

Затем давайте построим модель RNN. Мы собираемся использовать вложения слов для представления слов. Теперь во время обучения модели вы также можете обучать вложениям слов вместе с весами сети. Их часто называют весами встраивания. Во время обучения веса внедрения будут рассматриваться как обычные веса сети, которые обновляются на каждой итерации.

В следующих нескольких разделах мы попробуем следующие три модели RNN:

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

Неинициализированные фиксированные вложения

Начнем с первого эксперимента: обычная RNN с произвольно инициализированным, необучаемым встраиванием. Для этой RNN мы не будем использовать предварительно обученные вложения слов. Мы будем использовать случайно инициализированные вложения. Более того, мы не будем обновлять веса вложений.

# create architecture
rnn_model = Sequential()
# create embedding layer — usually the first layer in text problems
# vocabulary size — number of unique words in data
rnn_model.add(Embedding(input_dim = VOCABULARY_SIZE, 
# length of vector with which each word is represented
 output_dim = EMBEDDING_SIZE, 
# length of input sequence
 input_length = MAX_SEQ_LENGTH, 
# False — don’t update the embeddings
 trainable = False 
))
# add an RNN layer which contains 64 RNN cells
# True — return whole sequence; False — return single output of the end of the sequence
rnn_model.add(SimpleRNN(64, 
 return_sequences=True
))
# add time distributed (output at each sequence) layer
rnn_model.add(TimeDistributed(Dense(NUM_CLASSES, activation=’softmax’)))
#compile model
rnn_model.compile(loss      =  'categorical_crossentropy',
                  optimizer =  'adam',
                  metrics   =  ['acc'])
# check summary of the model
rnn_model.summary()

#fit model
rnn_training = rnn_model.fit(X_train, Y_train, batch_size=128, epochs=10, validation_data=(X_validation, Y_validation))

Здесь мы видим, что по прошествии десяти эпох он дает довольно приличную точность около 95%. Кроме того, ниже мы видим здоровую кривую роста.

Неинициализированные обучаемые вложения

Затем попробуйте вторую модель - RNN с произвольно инициализированными обучаемыми вложениями. Здесь мы позволим вложениям обучаться в сети. Все, что я делаю, это меняю параметр trainable на true, то есть trainable = True. Остальное все остается таким же, как указано выше. Проверив сводку модели, мы увидим, что все параметры стали обучаемыми. т.е. обучаемые параметры равны общим параметрам.

# check summary of the model
rnn_model.summary()

При подгонке модели точность значительно выросла. Он увеличился примерно до 98,95% за счет тренировок гирь для встраивания. Следовательно, встраивание существенно влияет на работу сети.

Теперь попробуем вложения word2vec и посмотрим, улучшит ли это нашу модель.

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

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

Сетевая архитектура такая же, как и выше, но вместо того, чтобы начинать с произвольной матрицы встраивания, мы будем использовать предварительно обученные веса встраивания (weights = [embedding_weights]) из word2vec. В данном случае точность повысилась до приблизительно 99,04%.

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

4. LSTM

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

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

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

Затем мы построим модель LSTM вместо RNN. Нам просто нужно заменить слой RNN слоем LSTM.

# create architecture
lstm_model = Sequential()
# vocabulary size — number of unique words in data
# length of vector with which each word is represented
lstm_model.add(Embedding(input_dim = VOCABULARY_SIZE, 
 output_dim = EMBEDDING_SIZE, 
# length of input sequence
input_length = MAX_SEQ_LENGTH, 
# word embedding matrix
weights = [embedding_weights],
# True — update embeddings_weight matrix
trainable = True 
))
# add an LSTM layer which contains 64 LSTM cells
# True — return whole sequence; False — return single output of the end of the sequence
lstm_model.add(LSTM(64, return_sequences=True))
lstm_model.add(TimeDistributed(Dense(NUM_CLASSES, activation=’softmax’)))
#compile model
rnn_model.compile(loss      =  'categorical_crossentropy',
                  optimizer =  'adam',
                  metrics   =  ['acc'])
# check summary of the model
rnn_model.summary()

lstm_training = lstm_model.fit(X_train, Y_train, batch_size=128, epochs=10, validation_data=(X_validation, Y_validation))

Модель LSTM также принесла незначительные улучшения. Однако, если мы используем модель LSTM в других задачах, таких как языковой перевод, добавление подписей к изображениям, прогнозирование временных рядов и т. Д., Вы можете увидеть значительное повышение производительности.

5. ГРУ

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

Давайте теперь построим модель ГРУ. Затем мы также сравним производительность RNN, LSTM и модели GRU.

# create architecture
lstm_model = Sequential()
# vocabulary size — number of unique words in data
# length of vector with which each word is represented
lstm_model.add(Embedding(input_dim = VOCABULARY_SIZE, 
 output_dim = EMBEDDING_SIZE, 
# length of input sequence
input_length = MAX_SEQ_LENGTH, 
# word embedding matrix
weights = [embedding_weights],
# True — update embeddings_weight matrix
trainable = True 
))
# add an LSTM layer which contains 64 LSTM cells
# True — return whole sequence; False — return single output of the end of the sequence
lstm_model.add(GRU(64, return_sequences=True))
lstm_model.add(TimeDistributed(Dense(NUM_CLASSES, activation=’softmax’)))
#compile model
rnn_model.compile(loss      =  'categorical_crossentropy',
                  optimizer =  'adam',
                  metrics   =  ['acc'])
# check summary of the model
rnn_model.summary()

В ГРУ уменьшено количество параметров по сравнению с LSTM. Таким образом, мы получаем значительный прирост вычислительной эффективности без какого-либо снижения производительности модели.

gru_training = gru_model.fit(X_train, Y_train, batch_size=128, epochs=10, validation_data=(X_validation, Y_validation))

Точность модели остается такой же, как и у LSTM. Но мы видели, что LSTM занимает больше времени, чем GRU и RNN. Это было ожидаемо, поскольку параметры в LSTM и GRU равны 4x и 3x нормальному RNN, соответственно.

6. Двунаправленный LSTM

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

Эти два типа задач называются автономной и оперативной обработкой последовательности соответственно.

Теперь есть ловкий трюк, который вы можете использовать с автономными задачами - поскольку сеть имеет доступ ко всей последовательности до того, как делать прогнозы, почему бы не использовать эту задачу, чтобы заставить сеть «смотреть на будущие элементы в последовательность 'во время обучения, надеясь, что это улучшит обучение сети?

Эту идею используют так называемые двунаправленные RNN.

Используя двунаправленные RNN, вы почти наверняка добьетесь лучших результатов. Однако для обучения двунаправленных RNN требуется почти вдвое больше времени, поскольку количество параметров сети увеличивается. Следовательно, у вас есть компромисс между временем обучения и производительностью. Решение использовать двунаправленную RNN зависит от имеющихся у вас вычислительных ресурсов и желаемой производительности.

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

# create architecture
bidirect_model = Sequential()
bidirect_model.add(Embedding(input_dim = VOCABULARY_SIZE,
 output_dim = EMBEDDING_SIZE,
 input_length = MAX_SEQ_LENGTH,
 weights = [embedding_weights],
 trainable = True
))
bidirect_model.add(Bidirectional(LSTM(64, return_sequences=True)))
bidirect_model.add(TimeDistributed(Dense(NUM_CLASSES, activation=’softmax’)))
#compile model
bidirect_model.compile(loss='categorical_crossentropy',
              optimizer='adam',
              metrics=['acc'])
# check summary of model
bidirect_model.summary()

Вы можете видеть, что количество параметров увеличилось. Это значительно увеличивает количество параметров.

bidirect_training = bidirect_model.fit(X_train, Y_train, batch_size=128, epochs=10, validation_data=(X_validation, Y_validation))

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

7. Оценка модели

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

loss, accuracy = rnn_model.evaluate(X_test, Y_test, verbose = 1)
print(“Loss: {0},\nAccuracy: {1}”.format(loss, accuracy))

loss, accuracy = lstm_model.evaluate(X_test, Y_test, verbose = 1)
print(“Loss: {0},\nAccuracy: {1}”.format(loss, accuracy))

loss, accuracy = gru_model.evaluate(X_test, Y_test, verbose = 1)
print(“Loss: {0},\nAccuracy: {1}”.format(loss, accuracy))

loss, accuracy = bidirect_model.evaluate(X_test, Y_test, verbose = 1)
print("Loss: {0},\nAccuracy: {1}".format(loss, accuracy))

Если у вас есть какие-либо вопросы, рекомендации или критические замечания, со мной можно связаться через LinkedIn или в разделе комментариев.