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

Что такое классификация в машинном обучении?

Существует два основных типа подходов обучения с учителем в их простейшей форме. Во-первых, у вас может возникнуть проблема регрессии, когда вы пытаетесь предсказать непрерывную переменную, например температуру или цену акции. Вторая проблема связана с классификацией, когда вы хотите предсказать категориальную переменную, такую ​​как пройдено/не пройдено или спам/ветом. Кроме того, у нас могут возникнуть проблемы с бинарной классификацией, которые мы рассмотрим здесь только с двумя результатами, и с мультиклассовой классификацией с более чем двумя результатами.

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

Мы хотим предпринять несколько шагов, чтобы подготовить наши данные для машинного обучения. Для текста мы хотим очистить его, удалив ненужные символы или цифры, мы хотим удалить стоп-слова или слова, которые появляются слишком часто, и мы также должны очистить или лемматизировать слова, которые берут такие слова, как running и runs, и приводят их к корневой форме run. Мы можем захотеть создать новые столбцы или функции, которые помогут вам в процессе классификации машинного обучения.

В этом примере мы будем использовать набор данных Обзоры женской одежды для электронной коммерции на Kaggle, который доступен для использования в разделе CC0: Public Domain.

Как всегда, давайте загрузим все библиотеки, которые нам нужны для выполнения этого анализа:

# Gereral Imports
import numpy as np
import pandas as pd
import re
import string
from timeit import timeit

# Machine Learning Imports
from sklearn.model_selection import train_test_split
from sklearn.metrics import ConfusionMatrixDisplay
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn import metrics 
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import RepeatedStratifiedKFold
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.preprocessing import MinMaxScaler
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline

# Model Persistence Imports
from joblib import dump, load

# Test Processing Imports
import nltk
from nltk.stem import PorterStemmer

# Plotting Imports
import matplotlib.pyplot as plt
%matplotlib inline

Разработка функций

Этот набор данных содержит обзоры, рейтинги, названия отделов и возраст человека, написавшего обзор. Рейтинговая система представляет собой классическую систему от 1 до 5 звезд. Перед очисткой давайте импортируем данные и создадим несколько ключевых столбцов, которые мы будем использовать в нашей модели, также известной как Разработка функций. Мы будем использовать функцию read_csv для чтения данных и создания фрейма данных с именем df.

  1. Поскольку мы выполняем бинарную классификацию, наша переменная Target должна иметь значение 1 или 0. В пятизвездочной системе отзывов мы можем взять отзывы 4 и 5 и сделать их положительными, а затем оставить остальные, 1, 2 и 3 отзывы отрицательными. >.
  2. Этот конкретный набор отзывов имеет поля Заголовок и Текст отзыва. Мы можем объединить эти два столбца в новый столбец с именем Текст, чтобы упростить обработку.
  3. В качестве еще одной функции нашей модели мы можем создать новый столбец, представляющий Общую длину текста отзыва.

Примечание. Один из шагов, который я здесь не показываю, — это EDA или исследовательский анализ данных. Я предлагаю вам всегда делать это перед созданием модели. Вы можете найти мой процесс в моем посте Исследовательский анализ данных.

# Import the data
df = pd.read_csv("ClothingReviews.csv")

# add a column for positive or negative based on the 5 star review
df['Target'] = df['Rating'].apply(lambda c: 0 if c < 4 else 1)

# Combine the title and text into a single column
df['Text'] = df['Title'] + ' ' + df['Review Text']

# Create a new column that is the length of the text field
df['text_len'] = df.apply(lambda row: len(row['Text']), axis = 1)

Очистка текста

Далее нам нужно очистить текст. Я создал функцию, адаптированную практически к любой ситуации очистки НЛП. Давайте посмотрим на текст перед текстом:

' Love this dress!  it\'s sooo pretty.  i happened to find it in a store, 
and i\'m glad i did bc i never would have ordered it online bc it\'s 
petite.  i bought a petite and am 5\'8".  i love the length on me- 
hits just a little below the knee.  would definitely be a true 
midi on someone who is truly petite.'

И затем функция, используемая для обработки строк:

# Cleaning Function
def process_string(text):

    final_string = ""

    # Convert the text to lowercase
    text = text.lower()

    # Remove punctuation
    translator = str.maketrans('', '', string.punctuation)
    text = text.translate(translator)

    # Remove stop words and useless words
    text = text.split()
    useless_words = nltk.corpus.stopwords.words("english")
    text_filtered = [word for word in text if not word in useless_words]

    # Remove numbers
    text_filtered = [re.sub('\w*\d\w*', '', w) for w in text_filtered]

    # Stem the text with NLTK PorterStemmer
    stemmer = PorterStemmer() 
    text_stemmed = [stemmer.stem(y) for y in text_filtered]

    # Join the words back into a string
    final_string = ' '.join(text_stemmed)

    return final_string

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

df['Text_Processed'] = df['Text'].apply(lambda x: process_string(x))
'love dress sooo pretti happen find store im glad bc never would 
order onlin bc petit bought petit  love length hit littl knee would 
definit true midi someon truli petit'

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

Проверка несбалансированных данных

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

df['Target'].value_counts()
1    17433
0     5193
Name: Target, dtype: int64

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

Строительство трубопровода

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

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

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

  • TfidfVectorizer: векторизатор TF-IDF преобразует текст в числовые значения. Для отличного описания этого, проверьте мой пост на BoW и TF-IDF. Здесь мы трансформируем наш столбец Text_Processed.
  • MinMaxScaler: преобразует все числовые значения в диапазон от 0 до 1. Большинство алгоритмов машинного обучения не обрабатывают данные с широким диапазоном значений; всегда рекомендуется масштабировать данные. Подробнее об этом можно прочитать в документации Scikit-Learn. Здесь мы масштабируем наш столбец text_len.

Второй — создание самого конвейера. В конвейере перечислены шаги, которые мы хотим выполнить с данными. У нас довольно простой Pipeline, но вы можете добавить столько шагов, сколько захотите. У нас есть следующее.

  1. prep: это преобразование столбца сверху. Он будет векторизовать наш текст и масштабировать его длину.
  2. clf: здесь мы выбираем экземпляр нашего классификатора. Вы можете видеть, что это передается в нашу функцию, и мы передаем его в функцию для тестирования разных классификаторов с одними и теми же данными.

Примечание. Экземпляр классификатора — это просто сам класс. Например, LogisticRegression() является экземпляром LogisticRegression.

def create_pipe(clf):

    column_trans = ColumnTransformer(
            [('Text', TfidfVectorizer(), 'Text_Processed'),
             ('Text Length', MinMaxScaler(), ['text_len'])],
            remainder='drop') 

    pipeline = Pipeline([('prep',column_trans),
                         ('clf', clf)])

    return pipeline

Выбор модели с помощью перекрестной проверки

При построении модели машинного обучения рекомендуется использовать выбор модели. Выбор модели позволяет протестировать различные алгоритмы на ваших данных и определить, какой из них работает лучше всего. Во-первых, мы разделим наш набор данных на наборы данных X и y. X представляет все функции нашей модели, а y будет представлять целевую переменную. Цель — это переменная, которую мы пытаемся предсказать.

X = df[['Text_Processed', 'text_len']]
y = df['Target']

Теперь пришло время провести перекрестную проверку двух классификаторов. Перекрестная проверка – это процесс разделения данных на n различных разделений, которые вы затем используете для проверки своей модели. Перекрестная проверка важна, потому что в некоторых случаях наблюдения из набора train могут не соответствовать наблюдениям из набора test. Поэтому вы избегаете этого, просматривая разные срезы данных. Подробнее об этом читайте в Документации Scikit-Learn.

Важно. Одна вещь, на которую я хочу обратить особое внимание, — это RepeatedStratifiedKFold для перекрестной проверки. Стратифицированныйперекрестный валидатор гарантирует, что в случаенесбалансированныхданных разделысохраняют относительные частоты классовв разделении.

Наконец, мы будем использовать классификатор, который поддерживает параметр class_weight, связанный с несбалансированными данными. Несколько моделей из Scikit-Learn поддерживают это, и, просто установив значение balanced, мы можем учитывать несбалансированные данные. Другие методы включают SMOTE (синтетическая генерация наблюдений меньшинства для балансировки данных, но это отличное место для начала. Подробнее о работе с несбалансированными данными вы можете прочитать в другом моем посте.

models = {'LogReg' : LogisticRegression(random_state=42, 
                                        class_weight='balanced', 
                                        max_iter=500),
          'RandomForest' : RandomForestClassifier(
                                        class_weight='balanced', 
                                        random_state=42)}

for name, model, in models.items():
    clf = model
    pipeline = create_pipe(clf)
    cv = RepeatedStratifiedKFold(n_splits=10, 
                                 n_repeats=3, 
                                 random_state=1)
    %time scores = cross_val_score(pipeline, X, y, 
                             scoring='f1_weighted', cv=cv, 
                             n_jobs=-1, error_score='raise')
    print(name, ': Mean f1 Weighted: %.3f and StdDev: (%.3f)' % \
        (np.mean(scores), np.std(scores)))
CPU times: user 23.2 s, sys: 10.7 s, total: 33.9 s
Wall time: 15.5 s
LogReg : Mean f1 Weighted: 0.878 and StdDev: (0.005)
CPU times: user 3min, sys: 2.35 s, total: 3min 2s
Wall time: 3min 2s
RandomForest : Mean f1 Weighted: 0.824 and StdDev: (0.008)

В приведенной выше функции вы можете видеть, что оценка выполняется с помощью f1_weighted. Выбор правильной метрики — это целая дискуссия, которую важно понять. Я уже писал о том, как выбрать правильную метрику оценки.

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

Поскольку мы не предпочитаем предсказывать положительные отзывы по сравнению с отрицательными, в данном случае я выбрал оценку F1. F1 по определению является гармоническим средним значением точности и полноты, объединяющим их в единую метрику. Однако есть также способ указать этой метрике оценивать несбалансированные данные с помощью флага weighted. Как говорится в документации Sciki-learn:

Рассчитайте метрики для каждой метки и найдите их среднее значение, взвешенное по поддержке (количество истинных экземпляров для каждой метки). Это изменяет «макро» для учета дисбаланса меток; это может привести к F-оценке между точностью и отзывом.

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

Еще раз, пожалуйста, прочитайте мое полное объяснение Оценки модели, чтобы дать вам лучшее представление о том, какую из них использовать и когда.

Обучение и проверка модели

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

# Make training and test sets 
X_train, X_test, y_train, y_test = train_test_split(X, 
                                                    y, 
                                                    test_size=0.33, 
                                                    random_state=53)

Затем быстрая функция подберет нашу модель и оценит ее с помощью classification_report и матрицы путаницы (CM). Я считаю, что очень важно запускать отчет о классификации вместе с CM, и он покажет вам все ваши ключевые показатели оценки и расскажет, как работает модель. CM — отличный способ визуализировать результаты.

def fit_and_print(pipeline, name):

    pipeline.fit(X_train, y_train)
    y_pred = pipeline.predict(X_test)

    print(metrics.classification_report(y_test, y_pred, digits=3))

    ConfusionMatrixDisplay.from_predictions(y_test, 
                                            y_pred, 
                                            cmap=plt.cm.YlOrBr)

    plt.tight_layout()
    plt.ylabel('True label')
    plt.xlabel('predicted label')
    plt.tight_layout()
    plt.savefig('classification_1.png', dpi=300)
clf = LogisticRegression(random_state=42, 
                         class_weight='balanced', 
                         max_iter=500)
pipeline = create_pipe(clf)
fit_and_print(pipeline, 'Logistic Regression')
              precision    recall  f1-score   support

           0      0.681     0.855     0.758      1715
           1      0.953     0.881     0.915      5752

    accuracy                          0.875      7467
   macro avg      0.817     0.868     0.837      7467
weighted avg      0.891     0.875     0.879      7467

Результаты есть! Отчет о классификации показывает нам все, что нам нужно. Поскольку мы сказали, что не обязательно оптимизировать для положительного или отрицательного класса, мы будем использовать столбец f1-score. Мы видим класс 0, выполненный в классе 0.758, и класс 1 в классе 0.915. Вы можете ожидать, что больший класс будет работать лучше, если у вас есть несбалансированные данные, но вы можете использовать некоторые из вышеперечисленных шагов, чтобы повысить производительность модели.

В 92% случаев модель правильно классифицирует отзывы для положительного класса и 76% времени для отрицательного класса. Это впечатляет, если просто посмотреть на текст, отправленный пользователем на проверку!

Сохранение модели

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

# Save the model to disk
dump(pipeline, 'binary.joblib') 

# Load the model from disk when you're ready to continue
pipeline = load('binary.joblib')

Тестирование на новых данных

Теперь для демонстрации того, что действительно важно. Насколько хорошо ваша модель предсказывает новые наблюдения, которых она никогда раньше не видела? В производственной среде этот процесс будет сильно отличаться, но мы можем смоделировать его, создав несколько новых обзоров для быстрого примера. Я также создал функцию, которая преобразует список строк в правильно отформатированный фрейм данных с очищенным текстом и столбцом text_len.

def create_test_data(x):

    x = process_string(x)
    length = len(x)

    d = {'Text_Processed' : x,
        'text_len' : length}

    df = pd.DataFrame(d, index=[0])

    return df
# Manually Generated Reviews
revs = ['This dress is gorgeous and I love it and would gladly recommend it to all of my friends.',
        'This skirt has really horrible quality and I hate it!',
        'A super cute top with the perfect fit.',
        'The most gorgeous pair of jeans I have seen.',
        'This item is too little and tight.']
# Print the predictions on new data
print('Returns 1 for Positive reviews and 0 for Negative reviews:','\n')
for rev in revs:
    c_res = pipeline.predict(create_test_data(rev))
    print(rev, '=', c_res)
Returns 1 for Positive reviews and 0 for Negative reviews: 

This dress is gorgeous and I love it and would gladly recommend it to all of my friends. = [1]
This skirt has really horrible quality and I hate it! = [0]
A super cute top with the perfect fit. = [1]
The most gorgeous pair of jeans I have seen. = [1]
This item is too little and tight. = [0]

Заключение

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

Есть еще два шага, которые я пропустил, о которых я расскажу позже. Выбор функций и Настройка гиперпараметров. В зависимости от сложности ваших данных, вы захотите углубиться в выбор функций и выжать из своей модели максимальную производительность, ознакомьтесь с настройкой гиперпараметров.

Весь код для этого поста доступен здесь на GitHub

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