Мультиклассовая классификация текста с помощью Doc2Vec и логистической регрессии
Цель состоит в том, чтобы с помощью Doc2Vec и логистической регрессии классифицировать жалобы потребителей на финансирование по 12 заранее определенным классам.
Doc2vec - это инструмент NLP для представления документов в виде вектора и является обобщением метода word2vec.
Чтобы понять doc2vec, желательно понять подход word2vec. Однако полные математические подробности выходят за рамки этой статьи. Если вы новичок в word2vec и doc2vec, следующие ресурсы могут помочь вам начать работу:
- Распределенные представления слов и фраз и их композиционность
- Распределенные представления приговоров и документов
- Мягкое введение в Doc2Vec
- Учебное пособие Gensim Doc2Vec по набору данных настроения IMDB
- Учебник по классификации документов с встраиванием слов
Используя тот же набор данных, что и при Многовековой классификации текста с помощью 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. Я с нетерпением жду любых вопросов.