Анализ тональности / классификация текста с использованием RNN (Bi-LSTM) (рекуррентная нейронная сеть)

Существует множество приложений классификации текста. Например, обнаружение языка вражды, классификация намерений и систематизация новостных статей. В центре внимания этой статьи находится анализ настроений, который представляет собой проблему классификации текста. Мы будем классифицировать комментарии IMDB на два класса: положительные и отрицательные.

Мы используем Python и Jupyter Notebook для разработки нашей системы, библиотеки, которые мы будем использовать, включают Keras, Gensim, Numpy, Pandas, Regex (re) и NLTK. Мы также будем использовать Google News Word2Vec Модель. Полный код и данные можно скачать здесь.

Исследование данных

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

import pandas as pd
data = pd.read_csv('imdb_labelled.tsv', 
                   header = None, 
                   delimiter='\t')
data.columns = ['Text', 'Label']
df.head()

Затем мы проверяем форму данных

data.shape

Теперь мы видим распределение классов. У нас есть 386 положительных и 362 отрицательных примера.

data.Label.value_counts()

Очистка данных

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

import re
def remove_punct(text):
    text_nopunct = ''
    text_nopunct = re.sub('['+string.punctuation+']', '', text)
    return text_nopunct
data['Text_Clean'] = data['Text'].apply(lambda x: remove_punct(x))

На следующем этапе мы токенизируем комментарии с помощью word_tokenize NLTK. Если мы передадим в word_tokenize строку «Токенизация - это просто». Результатом будет ["Tokenizing", "is", "easy"].

from nltk import word_tokenize
tokens = [word_tokenize(sen) for sen in data.Text_Clean]

Затем мы записываем данные в нижний регистр.

def lower_token(tokens): 
    return [w.lower() for w in tokens]    
    
lower_tokens = [lower_token(token) for token in tokens]

После ввода данных в нижний регистр стоп-слова удаляются из данных с помощью стоп-слов NLTK.

from nltk.corpus import stopwords
stoplist = stopwords.words('english')
def removeStopWords(tokens): 
    return [word for word in tokens if word not in stoplist]
filtered_words = [removeStopWords(sen) for sen in lower_tokens]
data['Text_Final'] = [' '.join(sen) for sen in filtered_words]
data['tokens'] = filtered_words

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

pos = []
neg = []
for l in data.Label:
    if l == 0:
        pos.append(0)
        neg.append(1)
    elif l == 1:
        pos.append(1)
        neg.append(0)
data['Pos']= pos
data['Neg']= neg
data = data[['Text_Final', 'tokens', 'Label', 'Pos', 'Neg']]
data.head()

Разделение данных на тестовую и обучающую

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

data_train, data_test = train_test_split(data, 
                                         test_size=0.10, 
                                         random_state=42)

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

all_training_words = [word for tokens in data_train["tokens"] for word in tokens]
training_sentence_lengths = [len(tokens) for tokens in data_train["tokens"]]
TRAINING_VOCAB = sorted(list(set(all_training_words)))
print("%s words total, with a vocabulary size of %s" % (len(all_training_words), len(TRAINING_VOCAB)))
print("Max sentence length is %s" % max(training_sentence_lengths))

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

all_test_words = [word for tokens in data_test[“tokens”] for word in tokens]
test_sentence_lengths = [len(tokens) for tokens in data_test[“tokens”]]
TEST_VOCAB = sorted(list(set(all_test_words)))
print(“%s words total, with a vocabulary size of %s” % (len(all_test_words), len(TEST_VOCAB)))
print(“Max sentence length is %s” % max(test_sentence_lengths))

Загрузка модели Word2Vec Новостей Google

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

word2vec_path = 'GoogleNews-vectors-negative300.bin.gz'
word2vec = models.KeyedVectors.load_word2vec_format(word2vec_path, binary=True)

Последовательности Tokenize и Pad

Каждому слову присваивается целое число, и это целое число помещается в список. Поскольку все обучающие предложения должны иметь одинаковую форму ввода, мы дополняем предложения.

Например, если у нас есть предложение «Как работает последовательность текста и заполнение». Каждому слову присвоен номер. Мы предполагаем, как = 1, text = 2, to = 3, sequence = 4 и = 5, padding = 6, работает = 7. После вызоваtext_to_sequences наше предложение будет выглядеть как [1, 2, 3, 4, 5, 6, 7]. Теперь предположим, что MAX_SEQUENCE_LENGTH = 10. После заполнения наше предложение будет выглядеть как [0, 0, 0, 1, 2, 3, 4, 5, 6, 7]

Мы делаем то же самое и для данных тестирования. Для полного кода посетите.

tokenizer = Tokenizer(num_words=len(TRAINING_VOCAB), lower=True, char_level=False)
tokenizer.fit_on_texts(data_train[“Text_Final”].tolist())
training_sequences = tokenizer.texts_to_sequences(data_train[“Text_Final”].tolist())
train_word_index = tokenizer.word_index
print(‘Found %s unique tokens.’ % len(train_word_index))
train_cnn_data = pad_sequences(training_sequences, 
                               maxlen=MAX_SEQUENCE_LENGTH)

Теперь мы получим вложения из модели Google News Word2Vec и сохраним их в соответствии с порядковым номером, который мы присвоили каждому слову. Если нам не удалось получить вложения, мы сохраняем случайный вектор для этого слова.

train_embedding_weights = np.zeros((len(train_word_index)+1, 
 EMBEDDING_DIM))for word,index in train_word_index.items():
 train_embedding_weights[index,:] = word2vec[word] if word in word2vec else np.random.rand(EMBEDDING_DIM)print(train_embedding_weights.shape)

Определение RNN

Текст в виде последовательности передается в RNN. Матрица вложений передается в embedding_layer. Вывод слоя внедрения передается на уровень LSTM. Эта модель имеет 256 ячеек LSTM. Здесь мы игнорируем скрытые состояния всех ячеек и берем вывод только из последней ячейки LSTM. Результат передается на слой Dense, затем применяется слой Dropout и затем слой Final Dense.

model.summary () напечатает краткую сводку всех слоев с выходными формами.

def rnn(embeddings, 
        max_sequence_length, 
        num_words, 
        embedding_dim, 
        labels_index):
    
    embedding_layer = Embedding(num_words,
                                embedding_dim,
                                weights=[embeddings],
                                input_length=max_sequence_length,
                                trainable=False)
    
    sequence_input = Input(shape=(max_sequence_length,),
                                  dtype='int32')
    embedded_sequences = embedding_layer(sequence_input)
    lstm = LSTM(256)(embedded_sequences)
    
    x = Dense(128, activation='relu')(lstm)
    x = Dropout(0.2)(x)
    preds = Dense(labels_index, activation='sigmoid')(x)
    model = Model(sequence_input, preds)
    model.compile(loss='binary_crossentropy',
                  optimizer='adam',
                  metrics=['acc'])
    model.summary()
    return model

Теперь выполним функцию.

model = rnn(train_embedding_weights, 
            MAX_SEQUENCE_LENGTH, 
            len(train_word_index)+1, 
            EMBEDDING_DIM, 
            len(list(label_names)))

Тренировочная РНН

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

num_epochs = 5
batch_size = 32
hist = model.fit(x_train, 
                 y_tr, 
                 epochs=num_epochs, 
                 validation_split=0.1, 
                 shuffle=True, 
                 batch_size=batch_size)

Тестирование модели

Вау! всего за пять итераций и небольшой набор данных мы смогли получить точность 80%.

predictions = model.predict(test_cnn_data, 
                            batch_size=1024, 
                            verbose=1)
labels = [1, 0]
prediction_labels=[]
for p in predictions:
    prediction_labels.append(labels[np.argmax(p)])
sum(data_test.Label==prediction_labels)/len(prediction_labels)