Реализация методов выбора признаков при классификации текста

Размер переменных или функций называется размерностью набора данных. В методах классификации текста размер объектов можно перечислить в большом количестве. В этом посте мы собираемся реализовать технику уменьшения размерности декомпозиции tf-idf с использованием линейного дискриминантного анализа-LDA.

Наш путь в этом исследовании:

1. Подготовка набора данных

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

3. Применение методов фильтрации

4. Применение линейного дискриминантного анализа.

5. Построение классификатора случайного леса.

6. Сравнение результатов

Все исходные коды и записные книжки были загружены в этот репозиторий Github.

Постановка проблемы

Повышение точности и сокращение времени процесса классификации текста.

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

Мы используем 515 000 данных об отелях в Европе из наборов данных Kaggle. Данные были взяты с Booking.com. Все данные в файле уже доступны всем. Данные изначально принадлежат Booking.com, и вы можете загрузить его с помощью этого профиля на Kaggle. Необходимый нам набор данных содержит 515 000 положительных и отрицательных отзывов.

Импортировать библиотеки

Наиболее важные библиотеки, которые мы использовали, - это Scikit-Learn и pandas.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import requests
import json
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.model_selection import train_test_split 
from sklearn.ensemble import RandomForestClassifier
from sklearn.feature_selection import VarianceThreshold
from sklearn.metrics import accuracy_score, roc_auc_score
from sklearn.preprocessing import StandardScaler
from nltk.stem.snowball import SnowballStemmer
from string import punctuation
from textblob import TextBlob
import re

Подготовка набора данных

Мы будем работать только над двумя категориями: положительные и отрицательные. Поэтому мы выбираем 5000 строк для каждой категории и копируем их в фрейм данных Pandas (5000 для каждой части). Для этого проекта мы использовали блокнот Kaggle, поэтому набор данных был загружен как локальный файл. Если вы используете другой инструмент или работаете как скрипт, вы можете его скачать. давайте взглянем на набор данных:

fields = ['Positive_Review', 'Negative_Review']
df = pd.read_csv(
    '../input/515k-hotel-reviews-data-in-europe/Hotel_Reviews.csv',
    usecols= fields, nrows=5000)
df.head()

Создание слов с помощью NLTK SnowballStemmer:

stemmer = SnowballStemmer('english')
df['Positive_Review'] = df['Positive_Review'].apply(
    lambda x:' '.join([stemmer.stem(y) for y in x.split()]))
df['Negative_Review'] = df['Negative_Review'].apply(
    lambda x: ' '.join([stemmer.stem(y) for y in x.split()]))

Удаление стоп-слов:

Исключите стоп-слова с пониманием списка countwordsfree.com и pandas.DataFrame.apply.

url = "https://countwordsfree.com/stopwords/english/json"
response = pd.DataFrame(data = json.loads(requests.get(url).text))
SW = list(response['words'])
df['Positive_Review'] = df['Positive_Review'].apply(
    lambda x: ' '.join([word for word in x.split() if word not in (SW)]))
df['Negative_Review'] = df['Negative_Review'].apply(
    lambda x: ' '.join([word for word in x.split() if word not in (SW)]))

Удаление чисел и инициализация набора данных:

df_Positive = df['Positive_Review'].copy()
df_Positive = df_Positive.str.replace('\d+', '')
df_Negative = df['Negative_Review'].copy()
df_Negative = df_Negative.str.replace('\d+', '')

Преобразует текст в векторные признаки

Пакет слов и TF-IDF - это два метода, которые используются для определения темы документа. Разница между ними в том, что BoW использует количество раз, когда слово появляется в документе в качестве метрики, тогда как TF-IDF дает каждому слову вес при обнаружении темы. Другими словами, в TF-IDF вместо количества слов используются оценки слов, поэтому можно сказать, что TF-IDF измеряет релевантность, а не частоту.

В этой части мы использовали Scikit-Learn TfidfVectorizer для каждого столбца и построили модель N-грамм для векторизации, которую можно использовать в качестве входных данных для оценщика. Затем мы добавляем столбец Target с заданным значением «1» для положительных отзывов и «0» для отрицательных. Обратите внимание, что мы определили min_df = 2.

tfidf = TfidfVectorizer(min_df=2,max_df=0.5, ngram_range=(1,3))
features = tfidf.fit_transform(df_Positive)
df_Positive = pd.DataFrame(
    features.todense(),
    columns=tfidf.get_feature_names()
)
df_Positive['Target'] = '1'
df_Positive.head()

Как видите, результат показывает 7934 функции, что является большим числом, а в столбце Target указано значение «1».

Так же поступим и с отрицательными отзывами:

tfidf = TfidfVectorizer(min_df=2,max_df=.05, ngram_range=(1,3))
features = tfidf.fit_transform(df_Negative)
df_Negative = pd.DataFrame(
    features.todense(),
    columns=tfidf.get_feature_names()
)
df_Negative['Target'] = '0'
df_Negative.head()

Всего имеется 6 754 функции, и целевое значение «0» для отрицательных отзывов. Теперь мы собираемся объединить эти два набора данных, добавив один к другому. Взаимные особенности будут рассматриваться как одно, но цели будут разными.

df = df_Positive.append(df_Negative)
df.shape
#out[] (10000, 12676)

Таким образом, окончательный набор данных содержит 10 000 экземпляров (по 5 000 для каждого) и 12 676 функций, что означает, что 2 012 функций являются взаимными ((7934 + 6754) -12676).

#change the value of NaN to Zero
df = df.fillna(0)

Определить разделение поездов / тестов:

#define x and y:
x = df.drop('Target',axis=1)
y = df['Target']
x.shape, y.shape
#out[] ((10000, 12675), (10000,))
#define train-test-slpit:
x_train, x_test, y_train, y_test = train_test_split(
    x, y, test_size = 0.2, random_state = 0, stratify = y)

Применение методов фильтрации - Удаление постоянных функций

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

constant_filter = VarianceThreshold(threshold = 0.0002)
constant_filter.fit(x_train)
feature_list = x_train[x_train.columns[
    constant_filter.get_support(indices=True)]]
print('Number of selected features: ' ,len(list(feature_list)),'\n')
print('List of selected features: \n' ,list(feature_list))

Вы можете увидеть список выбранных функций. Он показывает количество из 716 слов, которые были выделены ранее. Этот список может помочь нам проанализировать качество нашей продукции путем анализа списка и связи их элементов с нашими темами (положительные и отрицательные отзывы).

x_train_filter = constant_filter.transform(x_train)
x_test_filter = constant_filter.transform(x_test)
x_train_filter.shape, x_test_filter.shape, x_train.shape
#out[] ((8000, 716), (2000, 716), (8000, 12675))

Итак, результат показывает нам, что 94,35% наших функций являются постоянными, и модель их отбросила. Поэтому мы удалили 11 959 функций, что является значительным числом. Итак, нам нужно определить новый поезд, протестировать DataFrame:

x_train_filter = pd.DataFrame(x_train_filter)
x_test_filter = pd.DataFrame(x_test_filter)

Применение методов фильтрации - удаление коррелированных функций

Коррелированные объекты - это объекты, которые расположены близко друг к другу в линейном пространстве. Функция их удаления написана ниже.

def get_correlation(data, threshold):
    corr_col = set()
    cormat = data.corr()
    for i in range(len(cormat.columns)):
        for j in range(i):
            if abs(cormat.iloc[i,j]) > threshold:
                colname = cormat.columns[i]
                corr_col.add(colname)
    return corr_col
corr_features = get_correlation(x_train_filter, 0.70)

Затем мы определяем новые DataFrames после удаления коррелированных функций. результат показывает, что 23 коррелированных объекта были удалены.

x_train_uncorr = x_train_filter.drop(labels= corr_features, axis = 1)
x_test_uncorr = x_test_filter.drop(labels= corr_features, axis = 1)
x_train_uncorr = pd.DataFrame(x_train_uncorr)
x_test_uncorr = pd.DataFrame(x_test_uncorr)
x_train_uncorr.shape, x_test_uncorr.shape
out[] ((8000, 693), (2000, 693), (8000, 716))

Применение линейного дискриминантного анализа - LDA

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

from sklearn.discriminant_analysis import LinearDiscriminantAnalysis as LDA
lda = LDA(n_components=1)
x_train_lda = lda.fit_transform(x_train_uncorr, y_train)
x_test_lda = lda.fit_transform(x_test_uncorr, y_test)

Создание классификатора случайного леса (RFC)

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

def runRandomForest(x_train, x_test, y_train, y_test):
    clf = RandomForestClassifier(n_estimators=100, random_state=0, n_jobs=-1)
    clf.fit(x_train, y_train)
    y_pred = clf.predict(x_test)
    print ('accracy is: ', accuracy_score(y_test, y_pred))

Запустите классификатор случайного леса с использованием модели LDA:

#Run calssifier using LDA model
%%time
runRandomForest(x_train_lda, x_test_lda, y_train, y_test)

Точность модели LDA составляет 97,95 процента, а время на стене - около 709 мс. В модели без LDA результат все еще приемлем, и это связано с отличной векторизацией tf-idf. Но, как вы можете видеть ниже, время на стене составляет 14 секунд, что в 19,74 раза медленнее, чем у модели LDA.

##Run calssifier without using LDA model
%%time
runRandomForest(x_train, x_test, y_train, y_test)

Все исходные коды и записные книжки были загружены в этот репозиторий Github. Я с нетерпением жду отзывов. Свяжитесь со мной.

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