Часть 1 — Основы

ссылка на мой Github для получения дополнительного кода: https://github.com/charliezcr/Sentiment-Analysis-of-Movie-Reviews/blob/main/sa_p1.ipynb

Когда у вас есть большое количество обзоров фильмов, как вы можете узнать, являются ли они комплиментами или критическими замечаниями? Поскольку объем набора данных велик, вы не можете аннотировать их один за другим, а должны использовать инструменты обработки естественного языка для классификации тональности текста. Особенно в Python мощные пакеты, такие как nltk и scikit-learn, могут помочь нам в классификации текста. В этом проекте я провел анализ настроений рецензий на фильмы из набора данных reviews on imdb из набора данных Sentiment Labeled Sentences Data Set репозитория машинного обучения UCI.

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

В этом наборе данных есть 1000 отзывов о фильмах, в том числе 500 положительных (комплименты) и 500 отрицательных (критика). Например, "Очень, очень, очень медленный, бесцельный фильм о несчастном, сбивающемся с пути молодом человеке" помечается как отрицательный отзыв.

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

Чтобы еще больше очистить данные, нам нужно составить основу слов, чтобы слова с разной интонацией можно было считать одними и теми же токенами, потому что они передают одинаковую семантику. например «бедствие» и «бедствие» будут иметь корень как «бедствие».

После предварительной обработки текста у нас есть 2 списка данных. «метки» — это список целей нашей классификации. «предварительно обработанный» — это будущие функции классификации. Для предложений с «предварительной обработкой» мы переводим необработанный текст в совершенно нечитаемый текст. Например, 'Очень, очень, очень медленный, бесцельный фильм о несчастном, блуждающем по течению молодом человеке.' предварительно обрабатывается как 'очень, очень, очень медленный, бесцельный фильм о бедствие дрейфует молодой человек '

Вы можете задаться вопросом: мы переводим необработанный текст в этот нечитаемый текст, потому что мы хотим, чтобы каждый токен передал важную семантику. Тогда почему бы не убрать стоп-слова, потому что они не передают важную семантику, но встречаются очень часто, например, «а» и «о»? В следующем разделе извлечения функций мы собираемся использовать TF-IDF, чтобы позаботиться об этих стоп-словах.

Извлечение признаков

После предварительной обработки текста мы собираемся извлечь функции из наших очищенных данных. Мы собираемся использовать векторизатор TF-IDF в качестве встраивания слов для векторизации и нормализации текста.

TF-IDF расшифровывается как частота документа, обратная частоте. Он оценивает важность токена для документа в корпусе. TF-IDF делает данные нашей моделью, потому что нормализует частоту терминов или просто количество слов. Это также уменьшает шум стоп-слов.

В этом случае мы возьмем вариант TF-IDF. Формула обычного TF-IDF находится здесь. В отличие от исходного TF-IDF, мы используем sublinear_tf, заменяя TF на WF = 1 + log(TF). Этот вариант решает проблему, заключающуюся в том, что двадцать вхождений термина в документе на самом деле не несут в двадцать раз больше значимости, чем одно вхождение. очень медленный, бесцельный фильм о бедствующем, блуждающем по течению молодом человеке. очень появилось три раза. Поэтому для нашего набора данных нам нужно применить сублинейное масштабирование TF. Это резко повышает точность предсказания наших моделей позже.

После извлечения признаков у нас есть взвешенная по Tf-IDF матрица терминов документа, сохраненная в формате Compressed Sparse Row. Каждая цель — это настроение этого предложения. «1» означает положительный результат, а «0» — отрицательный. Но чтобы данные соответствовали нашей модели, нам нужно разделить наши данные на функции и цели. Train_test_split от Scikit-learn для случайного перемешивания данных и разделения их на набор для обучения и набор для тестирования. В этом конкретном случае я буду использовать 1/5 всего набора данных для тестирования, а остальные 4/5 — в качестве обучающего набора. Вот код всего процесса предварительной обработки:

from nltk.stem import PorterStemmer    # stem the words
from nltk.tokenize import word_tokenize # tokenize the sentences into tokens
from string import punctuation
from sklearn.feature_extraction.text import TfidfVectorizer # vectorize the texts
from sklearn.model_selection import train_test_split # split the testing and training sets
def preprocess(path):
    '''generate cleaned dataset
    
    Args:
        path(string): the path of the file of testing data
    Returns:
        X_train (list): the list of features of training data
        X_test (list): the list of features of test data
        y_train (list): the list of targets of training data ('1' or '0')
        y_test (list): the list of targets of training data ('1' or '0')
    '''
    
    # text preprocessing: iterate through the original file and 
    with open(path, encoding='utf-8') as file:
        # record all words and its label
        labels = []
        preprocessed = []
        for line in file:
            # get sentence and label
            sentence, label = line.strip('\n').split('\t')
            labels.append(int(label))
            
            # remove punctuation and numbers
            for ch in punctuation+'0123456789':
                sentence = sentence.replace(ch,' ')
            # tokenize the words and stem them
            words = []
            for w in word_tokenize(sentence):
                words.append(PorterStemmer().stem(w))
            preprocessed.append(' '.join(words))
    
    # vectorize the texts
    vectorizer = TfidfVectorizer(stop_words='english', sublinear_tf=True)
    X = vectorizer.fit_transform(preprocessed)
    # split the testing and training sets
    X_train, X_test, y_train, y_test = train_test_split(X, labels, test_size=0.2)
    return X_train, X_test, y_train, y_test
X_train, X_test, y_train, y_test = preprocess('imdb_labelled.txt')

Моделирование

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

from sklearn.metrics import accuracy_score
from sklearn.metrics import plot_confusion_matrix
from matplotlib import pyplot as plt
from time import time
def classify(clf, todense=False):
    '''to classify the data using machine learning models
    
    Args:
        clf: the model chosen to analyze the data
        todense(bool): whether to make the sparse matrix dense
        
    '''
    global X_train, X_test, y_train, y_test
    t = time()
    if todense:
        clf.fit(X_train.todense(), y_train)
        y_pred = clf.predict(X_test.todense())
    else:
        clf.fit(X_train, y_train)
        y_pred = clf.predict(X_test)
    print(f'Time cost of {str(clf)}: {round(time()-t,2)}s\nThe accuracy of {str(clf)}: {accuracy_score(y_test,y_pred)}\n')

Поскольку цель является категориальной и дихотомической, функции не имеют предполагаемого распределения, модели, которые мы можем использовать для классификации текста, — это логистическая регрессия, классификатор стохастического градиентного спуска (SGDClassifier), классификатор опорных векторов (SVC) и нейронная сеть (MLPClassifier). Поскольку наши данные о функциях скудны, SVC и SGD полезны. Среди трех типов наивных байесовских классификаторов (бернуллиевский, полиномиальный и гауссовский) нам нужно выбрать полиномиальный, потому что функции нормализуются TF-IDF. Эти функции не соответствуют распределению Гаусса или Бернулли.

Ниже я рассмотрю производительность каждой выбранной модели. В этой части я не буду настраивать параметры для каждой модели, но сделаю это в будущем.

Технически мы также можем использовать линейный дискриминантный анализ. Однако вычисление разреженных матриц, подобных нашим данным объектов, требует больших вычислительных ресурсов. Точность этой модели также низкая. Поэтому в этот раз мы не будем рассматривать LDA. Вот производительность LDA:

from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
classify(LinearDiscriminantAnalysis(),todense=True)

Затраты времени на LinearDiscriminantAnalysis(): 0,79 с
Точность LinearDiscriminantAnalysis(): 0,71

Вот характеристики выбранных моделей:

from sklearn.linear_model import LogisticRegression
from sklearn.naive_bayes import MultinomialNB
from sklearn.svm import SVC
from sklearn.linear_model import SGDClassifier
from sklearn.neural_network import MLPClassifier
for model in [LogisticRegression(), MultinomialNB(), SVC(), SGDClassifier(), MLPClassifier()]:
    classify(model)

Стоимость времени для LogisticRegression(): 0,03 с
Точность LogisticRegression(): 0,825

Стоимость времени для MultinomialNB(): 0,0 с
Точность MultinomialNB(): 0,825

Стоимость времени для SVC(): 0,09 с
Точность SVC(): 0,835

Стоимость времени для SGDClassifier(): 0,0 с
Точность SGDClassifier(): 0,82

Стоимость времени для MLPClassifier(): 3,47 с
Точность MLPClassifier(): 0,81

ансамблевое обучение

Хотя мы хотим повысить точность предсказания наших моделей, мы также хотим избежать переобучения, чтобы мы могли использовать модели для предсказания других наборов данных. Построение ансамблевого метода является решением этой проблемы. Для каждого обзора мы собираемся позволить каждой выбранной модели голосовать за свой собственный прогноз и использовать режим всех голосов для создания прогноза ансамбля. Выбранными моделями являются логистическая регрессия, MultinomialNB, SVC и SGD. Поскольку нейронные сети требуют сложной настройки и отнимают много времени, я не буду включать MLPClassifier в этот ансамбль обучения. Из приведенной ниже оценки точности и матрицы путаницы видно, что, несмотря на увеличение временных затрат, производительность ансамблевой модели является удовлетворительной.

from statistics import mode
def ensemble(models):
    '''to ensemble the models and classify the data based on each model's vote
    
    Args:
        models: the list of models chosen to analyze the data
        
    '''
    global X_train, X_test, y_train, y_test
    t = time()
    # iterate through all the models and collect all their predictions
    y_preds = []
    for clf in models:
        clf.fit(X_train, y_train)
        y_preds.append(clf.predict(X_test))
    
    # Count their votes and get the mode of each prediction as the decision
    y_pred = []
    for i in range(len(y_preds[0])):
        y_pred.append(mode([y[i] for y in y_preds]))
    print(f'Time cost: {round(time()-t,2)}s\nAccuracy: {accuracy_score(y_test,y_pred)}\n')
    plot_confusion_matrix(clf, X_test, y_test, values_format = 'd',display_labels=['positive','negative'])
ensemble([LogisticRegression(),MultinomialNB(),SVC(),SGDClassifier()])

Стоимость времени: 0,12 с
Точность: 0,83