Мультиклассовая классификация текста с помощью Doc2Vec и логистической регрессии

Цель состоит в том, чтобы с помощью Doc2Vec и логистической регрессии классифицировать жалобы потребителей на финансирование по 12 заранее определенным классам.

Doc2vec - это инструмент NLP для представления документов в виде вектора и является обобщением метода word2vec.

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

Используя тот же набор данных, что и при Многовековой классификации текста с помощью Scikit-Learn, в этой статье мы классифицируем повествования о жалобах по продуктам, используя методы doc2vec в Gensim. Давайте начнем!

Данные

Цель состоит в том, чтобы разделить жалобы на потребительское финансирование по 12 заранее определенным классам. Данные можно скачать с data.gov.

import pandas as pd
import numpy as np
from tqdm import tqdm
tqdm.pandas(desc="progress-bar")
from gensim.models import Doc2Vec
from sklearn import utils
from sklearn.model_selection import train_test_split
import gensim
from sklearn.linear_model import LogisticRegression
from gensim.models.doc2vec import TaggedDocument
import re
import seaborn as sns
import matplotlib.pyplot as plt
df = pd.read_csv('Consumer_Complaints.csv')
df = df[['Consumer complaint narrative','Product']]
df = df[pd.notnull(df['Consumer complaint narrative'])]
df.rename(columns = {'Consumer complaint narrative':'narrative'}, inplace = True)
df.head(10)

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

df.shape

(318718, 2)

df.index = range(318718)
df['narrative'].apply(lambda x: len(x.split(' '))).sum()

63420212

У нас более 63 миллионов слов, это относительно большой набор данных.

Изучение

cnt_pro = df['Product'].value_counts()
plt.figure(figsize=(12,4))
sns.barplot(cnt_pro.index, cnt_pro.values, alpha=0.8)
plt.ylabel('Number of Occurrences', fontsize=12)
plt.xlabel('Product', fontsize=12)
plt.xticks(rotation=90)
plt.show();

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

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

def print_complaint(index):
    example = df[df.index == index][['narrative', 'Product']].values[0]
    if len(example) > 0:
        print(example[0])
        print('Product:', example[1])
print_complaint(12)

print_complaint(20)

Предварительная обработка текста

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

from bs4 import BeautifulSoup
def cleanText(text):
    text = BeautifulSoup(text, "lxml").text
    text = re.sub(r'\|\|\|', r' ', text) 
    text = re.sub(r'http\S+', r'<URL>', text)
    text = text.lower()
    text = text.replace('x', '')
    return text
df['narrative'] = df['narrative'].apply(cleanText)

Следующие шаги включают разделение на поезд / тест 70/30, удаление стоп-слов и токенизацию текста с помощью токенизатора NLTK. Для нашей первой попытки мы помечаем каждую жалобу соответствующим продуктом.

train, test = train_test_split(df, test_size=0.3, random_state=42)
import nltk
from nltk.corpus import stopwords
def tokenize_text(text):
    tokens = []
    for sent in nltk.sent_tokenize(text):
        for word in nltk.word_tokenize(sent):
            if len(word) < 2:
                continue
            tokens.append(word.lower())
    return tokens
train_tagged = train.apply(
    lambda r: TaggedDocument(words=tokenize_text(r['narrative']), tags=[r.Product]), axis=1)
test_tagged = test.apply(
    lambda r: TaggedDocument(words=tokenize_text(r['narrative']), tags=[r.Product]), axis=1)

Вот как выглядит запись об обучении - пример описания жалобы с пометкой «Кредитная отчетность».

train_tagged.values[30]

Настройка обучающих и оценочных моделей Doc2Vec

Сначала мы создаем экземпляр модели doc2vec - Распределенный пакет слов (DBOW). В архитектуре word2vec два имени алгоритма - «непрерывный пакет слов» (CBOW) и «skip-gram» (SG); в архитектуре doc2vec соответствующими алгоритмами являются «распределенная память» (DM) и «распределенный пакет слов» (DBOW).

Распределенный мешок слов (DBOW)

DBOW - это модель doc2vec, аналогичная модели Skip-gram в word2vec. Векторы абзацев получаются путем обучения нейронной сети задаче прогнозирования распределения вероятностей слов в абзаце с учетом случайно выбранного слова из абзаца.

Мы будем варьировать следующие параметры:

  • Если dm=0, используется распределенный пакет слов (PV-DBOW); если _2 _, используется «распределенная память» (PV-DM).
  • 300-мерные векторы признаков.
  • min_count=2, игнорирует все слова с общей частотой ниже этой.
  • negative=5, определяет, сколько «шумовых слов» нужно нарисовать.
  • hs=0, а отрицательное значение не равно нулю, будет использоваться отрицательная выборка.
  • sample=0, порог для настройки того, какие слова с более высокой частотой случайным образом подвергаются понижающей дискретизации.
  • workers=cores, используйте это множество рабочих потоков для обучения модели (= более быстрое обучение на многоядерных машинах).
import multiprocessing
cores = multiprocessing.cpu_count()

Создание словарного запаса

model_dbow = Doc2Vec(dm=0, vector_size=300, negative=5, hs=0, min_count=2, sample = 0, workers=cores)
model_dbow.build_vocab([x for x in tqdm(train_tagged.values)])

Обучение модели doc2vec в Gensim довольно просто, мы инициализируем модель и тренируемся в течение 30 эпох.

%%time
for epoch in range(30):
    model_dbow.train(utils.shuffle([x for x in tqdm(train_tagged.values)]), total_examples=len(train_tagged.values), epochs=1)
    model_dbow.alpha -= 0.002
    model_dbow.min_alpha = model_dbow.alpha

Построение окончательного векторного объекта для классификатора

def vec_for_learning(model, tagged_docs):
    sents = tagged_docs.values
    targets, regressors = zip(*[(doc.tags[0], model.infer_vector(doc.words, steps=20)) for doc in sents])
    return targets, regressorsdef vec_for_learning(model, tagged_docs):
    sents = tagged_docs.values
    targets, regressors = zip(*[(doc.tags[0], model.infer_vector(doc.words, steps=20)) for doc in sents])
    return targets, regressors

Обучите классификатор логистической регрессии.

y_train, X_train = vec_for_learning(model_dbow, train_tagged)
y_test, X_test = vec_for_learning(model_dbow, test_tagged)
logreg = LogisticRegression(n_jobs=1, C=1e5)
logreg.fit(X_train, y_train)
y_pred = logreg.predict(X_test)
from sklearn.metrics import accuracy_score, f1_score
print('Testing accuracy %s' % accuracy_score(y_test, y_pred))
print('Testing F1 score: {}'.format(f1_score(y_test, y_pred, average='weighted')))

Точность тестирования 0,6683609437751004

Оценка F1 при тестировании: 0,651646431211616

Распределенная память (DM)

Распределенная память (DM) действует как память, которая запоминает то, что отсутствует в текущем контексте, или как тема абзаца. В то время как векторы слов представляют концепцию слова, вектор документа предназначен для представления концепции документа. Мы снова создаем экземпляр модели Doc2Vec с размером вектора с 300 словами и повторяем весь обучающий корпус 30 раз.

model_dmm = Doc2Vec(dm=1, dm_mean=1, vector_size=300, window=10, negative=5, min_count=1, workers=5, alpha=0.065, min_alpha=0.065)
model_dmm.build_vocab([x for x in tqdm(train_tagged.values)])

%%time
for epoch in range(30):
    model_dmm.train(utils.shuffle([x for x in tqdm(train_tagged.values)]), total_examples=len(train_tagged.values), epochs=1)
    model_dmm.alpha -= 0.002
    model_dmm.min_alpha = model_dmm.alpha

Обучите классификатор логистической регрессии

y_train, X_train = vec_for_learning(model_dmm, train_tagged)
y_test, X_test = vec_for_learning(model_dmm, test_tagged)
logreg.fit(X_train, y_train)
y_pred = logreg.predict(X_test)
print('Testing accuracy %s' % accuracy_score(y_test, y_pred))
print('Testing F1 score: {}'.format(f1_score(y_test, y_pred, average='weighted')))

Точность тестирования 0,47498326639892907

Оценка F1 при тестировании: 0,4445833078167434

Сопряжение моделей

Согласно Учебнику Gensim doc2vec по набору данных тональности IMDB, объединение вектора абзаца из распределенного набора слов (DBOW) и распределенной памяти (DM) улучшает производительность. Мы будем следовать, объединяя модели вместе для оценки.

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

model_dbow.delete_temporary_training_data(keep_doctags_vectors=True, keep_inference=True)
model_dmm.delete_temporary_training_data(keep_doctags_vectors=True, keep_inference=True)

Соедините две модели.

from gensim.test.test_doc2vec import ConcatenatedDoc2Vec
new_model = ConcatenatedDoc2Vec([model_dbow, model_dmm])

Построение векторов признаков.

def get_vectors(model, tagged_docs):
    sents = tagged_docs.values
    targets, regressors = zip(*[(doc.tags[0], model.infer_vector(doc.words, steps=20)) for doc in sents])
    return targets, regressors

Тренируйте логистическую регрессию

y_train, X_train = get_vectors(new_model, train_tagged)
y_test, X_test = get_vectors(new_model, test_tagged)
logreg.fit(X_train, y_train)
y_pred = logreg.predict(X_test)
print('Testing accuracy %s' % accuracy_score(y_test, y_pred))
print('Testing F1 score: {}'.format(f1_score(y_test, y_pred, average='weighted')))

Точность тестирования 0,6778572623828648

Оценка F1 при тестировании: 0,664561533967402

Результат улучшился на 1%.

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

Блокнот Jupyter для вышеприведенного анализа можно найти на Github. Я с нетерпением жду любых вопросов.