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

В этом руководстве я исследую некоторые методы интеллектуального анализа текста для анализа тональности. Сначала мы потратим некоторое время на подготовку текстовых данных. Это будет включать в себя очистку текстовых данных, удаление стоп-слов и стемминга. Для этой цели приятно работать с набором данных Twitter об настроениях авиакомпаний США на Kaggle. Он содержит текст твита и одну переменную с тремя возможными значениями тональности.

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

Мы сравним производительность по трем показателям: точность, отзывчивость и оценка F1.

Начнем с импорта пакетов и настройки некоторых параметров.

import numpy as np 
import pandas as pd 
pd.set_option('display.max_colwidth', -1)
from time import time
import re
import string
import os
import emoji
from pprint import pprint
import collections
import matplotlib.pyplot as plt
import seaborn as sns
sns.set(style="darkgrid")
sns.set(font_scale=1.3)
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.model_selection import GridSearchCV
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline, FeatureUnion
from sklearn.metrics import classification_report
from sklearn.naive_bayes import MultinomialNB
from sklearn.linear_model import LogisticRegression
from sklearn.externals import joblib
import gensim
from nltk.corpus import stopwords
from nltk.stem import PorterStemmer
from nltk.tokenize import word_tokenize
import warnings
warnings.filterwarnings('ignore')
np.random.seed(37)

Загрузка данных

Мы читаем файл, разделенный запятыми, который мы загрузили из наборов данных Kaggle. Мы перемешиваем фрейм данных на случай сортировки классов. Для этого хорошо подходит применение метода reindex к permutation исходным индексам. В этом блокноте мы будем работать с переменной text и переменной airline_sentiment.

df = pd.read_csv('../input/Tweets.csv')
df = df.reindex(np.random.permutation(df.index))
df = df[['text', 'airline_sentiment']]

Исследовательский анализ данных

Целевая переменная

Мы будем предсказывать три ярлыка класса: отрицательный, нейтральный или положительный.

Ярлыки классов несбалансированы, как мы можем видеть на диаграмме ниже. Это то, что мы должны помнить на этапе обучения модели. С factorplot пакета seaborn мы можем визуализировать распределение целевой переменной.

sns.factorplot(x="airline_sentiment", data=df, kind="count", size=6, aspect=1.5, palette="PuBuGn_d")
plt.show();

Входная переменная

Чтобы проанализировать text variable, мы создаем класс TextCounts. В этом классе мы вычисляем базовую статистику по текстовой переменной.

  • count_words: количество слов в твите
  • count_mentions: переходы на другие учетные записи Twitter начинаются с символа @
  • count_hashtags: количество слов-тегов, которым предшествует #
  • count_capital_words: количество слов в верхнем регистре иногда используется для «крика» и выражения (отрицательных) эмоций.
  • count_excl_quest_marks: количество вопросительных или восклицательных знаков
  • count_urls: количество ссылок в твите, которым предшествуют http (s)
  • count_emojis: количество смайлов, которые могут быть хорошим признаком настроения
class TextCounts(BaseEstimator, TransformerMixin):
    
    def count_regex(self, pattern, tweet):
        return len(re.findall(pattern, tweet))
    
    def fit(self, X, y=None, **fit_params):
        # fit method is used when specific operations need to be done on the train data, but not on the test data
        return self
    
    def transform(self, X, **transform_params):
        count_words = X.apply(lambda x: self.count_regex(r'\w+', x)) 
        count_mentions = X.apply(lambda x: self.count_regex(r'@\w+', x))
        count_hashtags = X.apply(lambda x: self.count_regex(r'#\w+', x))
        count_capital_words = X.apply(lambda x: self.count_regex(r'\b[A-Z]{2,}\b', x))
        count_excl_quest_marks = X.apply(lambda x: self.count_regex(r'!|\?', x))
        count_urls = X.apply(lambda x: self.count_regex(r'http.?://[^\s]+[\s]?', x))
        # We will replace the emoji symbols with a description, which makes using a regex for counting easier
        # Moreover, it will result in having more words in the tweet
        count_emojis = X.apply(lambda x: emoji.demojize(x)).apply(lambda x: self.count_regex(r':[a-z_&]+:', x))
        
        df = pd.DataFrame({'count_words': count_words
                           , 'count_mentions': count_mentions
                           , 'count_hashtags': count_hashtags
                           , 'count_capital_words': count_capital_words
                           , 'count_excl_quest_marks': count_excl_quest_marks
                           , 'count_urls': count_urls
                           , 'count_emojis': count_emojis
                          })
        
        return df
tc = TextCounts()
df_eda = tc.fit_transform(df.text)
df_eda['airline_sentiment'] = df.airline_sentiment

Было бы интересно посмотреть, как переменные TextStats связаны с переменной класса. Итак, мы пишем функцию show_dist, которая предоставляет описательную статистику и график для каждого целевого класса.

def show_dist(df, col):
    print('Descriptive stats for {}'.format(col))
    print('-'*(len(col)+22))
    print(df.groupby('airline_sentiment')[col].describe())
    bins = np.arange(df[col].min(), df[col].max() + 1)
    g = sns.FacetGrid(df, col='airline_sentiment', size=5, hue='airline_sentiment', palette="PuBuGn_d")
    g = g.map(sns.distplot, col, kde=False, norm_hist=True, bins=bins)
    plt.show()

Ниже вы можете найти распределение количества слов в твите по целевым классам. Для краткости мы ограничимся только этой переменной. Графики для всех переменных TextCounts находятся в записной книжке на Github.

  • Количество слов, используемых в твитах, довольно невелико. Наибольшее количество слов - 36, и есть даже твиты, содержащие всего 2 слова. Поэтому при очистке данных нужно соблюдать осторожность, чтобы не удалить слишком много слов. Но обработка текста будет быстрее. Отрицательные твиты содержат больше слов, чем нейтральные или положительные.
  • Во всех твитах есть хотя бы одно упоминание. Это результат извлечения твитов на основе упоминаний в данных Twitter. Кажется, нет никакой разницы в количестве упоминаний относительно настроения.
  • Большинство твитов не содержат хеш-тегов. Таким образом, эта переменная не будет сохранена во время обучения модели. Опять же, нет разницы в количестве хэш-тегов в зависимости от настроения.
  • Большинство твитов не содержат слов с заглавной буквы, и мы не видим разницы в распределении настроений.
  • В положительных твитах, кажется, используется немного больше восклицательных или вопросительных знаков.
  • Большинство твитов не содержат URL.
  • В большинстве твитов смайлики не используются.

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

Прежде чем мы начнем использовать текст твитов, нам нужно его очистить. Мы сделаем это в классе CleanText . С этим классом мы выполним следующие действия:

  • удалите упоминания, так как мы хотим обобщить и на твиты других авиакомпаний.
  • удалите знак хэш-тега (#), но не сам тег, так как он может содержать информацию
  • установить все слова в нижний регистр
  • удалите все знаки препинания, включая вопросительный и восклицательный знаки
  • удалите URL-адреса, так как они не содержат полезной информации. Мы не заметили разницы в количестве URL-адресов, используемых между классами тональности.
  • не забудьте преобразовать смайлы в одно слово.
  • удалить цифры
  • удалить стоп-слова
  • примените PorterStemmer, чтобы сохранить основу слов
class CleanText(BaseEstimator, TransformerMixin):
    def remove_mentions(self, input_text):
        return re.sub(r'@\w+', '', input_text)
    
    def remove_urls(self, input_text):
        return re.sub(r'http.?://[^\s]+[\s]?', '', input_text)
    
    def emoji_oneword(self, input_text):
        # By compressing the underscore, the emoji is kept as one word
        return input_text.replace('_','')
    
    def remove_punctuation(self, input_text):
        # Make translation table
        punct = string.punctuation
        trantab = str.maketrans(punct, len(punct)*' ')  # Every punctuation symbol will be replaced by a space
        return input_text.translate(trantab)
    def remove_digits(self, input_text):
        return re.sub('\d+', '', input_text)
    
    def to_lower(self, input_text):
        return input_text.lower()
    
    def remove_stopwords(self, input_text):
        stopwords_list = stopwords.words('english')
        # Some words which might indicate a certain sentiment are kept via a whitelist
        whitelist = ["n't", "not", "no"]
        words = input_text.split() 
        clean_words = [word for word in words if (word not in stopwords_list or word in whitelist) and len(word) > 1] 
        return " ".join(clean_words) 
    
    def stemming(self, input_text):
        porter = PorterStemmer()
        words = input_text.split() 
        stemmed_words = [porter.stem(word) for word in words]
        return " ".join(stemmed_words)
    
    def fit(self, X, y=None, **fit_params):
        return self
    
    def transform(self, X, **transform_params):
        clean_X = X.apply(self.remove_mentions).apply(self.remove_urls).apply(self.emoji_oneword).apply(self.remove_punctuation).apply(self.remove_digits).apply(self.to_lower).apply(self.remove_stopwords).apply(self.stemming)
        return clean_X

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

ct = CleanText()
sr_clean = ct.fit_transform(df.text)
sr_clean.sample(5)

рад, пари, птица, желаю улететь на юг зимой
балл upc code check baggag tell luggag vacat day tri swimsuit
vx jfk la dirti plane нестандартный
сказать средняя работа требуется оценка время прибытия просьба работать с ноутбуком
sure busi go els airlin travel имя кэтрин сотело

Одним из побочных эффектов очистки текста является то, что в некоторых строках не осталось слов в тексте. Для CountVectorizer и TfIdfVectorizer это не проблема. Однако для алгоритма Word2Vec это вызывает ошибку. Существуют разные стратегии работы с этими недостающими значениями.

  • Удалите всю строку, но в производственной среде это нежелательно.
  • Вписать отсутствующее значение в какой-нибудь текст-заполнитель, например * [no_text] *
  • При применении Word2Vec: используйте среднее всех векторов

Здесь мы будем использовать замещающий текст.

empty_clean = sr_clean == ''
print('{} records have no words left after text cleaning'.format(sr_clean[empty_clean].count()))
sr_clean.loc[empty_clean] = '[no_text]'

Теперь, когда у нас есть очищенный текст твитов, мы можем посмотреть, какие слова используются чаще всего. Ниже мы покажем 20 самых популярных слов. Наиболее частое слово - «полет».

cv = CountVectorizer()
bow = cv.fit_transform(sr_clean)
word_freq = dict(zip(cv.get_feature_names(), np.asarray(bow.sum(axis=0)).ravel()))
word_counter = collections.Counter(word_freq)
word_counter_df = pd.DataFrame(word_counter.most_common(20), columns = ['word', 'freq'])
fig, ax = plt.subplots(figsize=(12, 10))
sns.barplot(x="word", y="freq", data=word_counter_df, palette="PuBuGn_d", ax=ax)
plt.show();

Создание тестовых данных

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

Сначала мы объединяем переменные TextCounts с переменной CleanText. Изначально я совершил ошибку, выполнив TextCounts и CleanText в GridSearchCV. Это заняло слишком много времени, поскольку эти функции применяются при каждом запуске GridSearch. Достаточно запустить их только один раз.

df_model = df_eda
df_model['clean_text'] = sr_clean
df_model.columns.tolist()

Итак, df_model теперь содержит несколько переменных. Но нашим векторизаторам (см. Ниже) понадобится только переменная clean_text. TextCountsvariables могут быть добавлены как таковые. Для выбора столбцов я написал класс ColumnExtractor ниже.

class ColumnExtractor(TransformerMixin, BaseEstimator):
    def __init__(self, cols):
        self.cols = cols
    def transform(self, X, **transform_params):
        return X[self.cols]
    def fit(self, X, y=None, **fit_params):
        return self
X_train, X_test, y_train, y_test = train_test_split(df_model.drop('airline_sentiment', axis=1), df_model.airline_sentiment, test_size=0.1, random_state=37)

Настройка гиперпараметров и перекрестная проверка

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

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

Чтобы найти лучшие параметры как для векторизатора, так и для классификатора, мы создаем файл Pipeline.

Метрики оценки

По умолчанию GridSearchCV использует счетчик по умолчанию для вычисления best_score_. И для MultiNomialNb, и для LogisticRegression этот показатель по умолчанию - точность.

В нашей функции grid_vect мы дополнительно генерируем classification_report на тестовых данных. Это дает некоторые интересные показатели для каждого целевого класса. Здесь это может быть более подходящим. Этими показателями являются точность, отзывчивость и оценка F1 .

  • Точность : сколько строк мы предсказали как определенный класс, сколько мы предсказали правильно?
  • Вспомните : сколько из всех строк определенного класса мы правильно спрогнозировали?
  • Оценка F1 : среднее гармоническое точность и отзывчивость.

С помощью элементов матрицы путаницы мы можем вычислить точность и отзыв.

# Based on http://scikit-learn.org/stable/auto_examples/model_selection/grid_search_text_feature_extraction.html
def grid_vect(clf, parameters_clf, X_train, X_test, parameters_text=None, vect=None, is_w2v=False):
    
    textcountscols = ['count_capital_words','count_emojis','count_excl_quest_marks','count_hashtags'
                      ,'count_mentions','count_urls','count_words']
    
    if is_w2v:
        w2vcols = []
        for i in range(SIZE):
            w2vcols.append(i)
        features = FeatureUnion([('textcounts', ColumnExtractor(cols=textcountscols))
                                 , ('w2v', ColumnExtractor(cols=w2vcols))]
                                , n_jobs=-1)
    else:
        features = FeatureUnion([('textcounts', ColumnExtractor(cols=textcountscols))
                                 , ('pipe', Pipeline([('cleantext', ColumnExtractor(cols='clean_text')), ('vect', vect)]))]
                                , n_jobs=-1)
    
    pipeline = Pipeline([
        ('features', features)
        , ('clf', clf)
    ])
    
    # Join the parameters dictionaries together
    parameters = dict()
    if parameters_text:
        parameters.update(parameters_text)
    parameters.update(parameters_clf)
    # Make sure you have scikit-learn version 0.19 or higher to use multiple scoring metrics
    grid_search = GridSearchCV(pipeline, parameters, n_jobs=-1, verbose=1, cv=5)
    
    print("Performing grid search...")
    print("pipeline:", [name for name, _ in pipeline.steps])
    print("parameters:")
    pprint(parameters)
    t0 = time()
    grid_search.fit(X_train, y_train)
    print("done in %0.3fs" % (time() - t0))
    print()
    print("Best CV score: %0.3f" % grid_search.best_score_)
    print("Best parameters set:")
    best_parameters = grid_search.best_estimator_.get_params()
    for param_name in sorted(parameters.keys()):
        print("\t%s: %r" % (param_name, best_parameters[param_name]))
        
    print("Test score with best_estimator_: %0.3f" % grid_search.best_estimator_.score(X_test, y_test))
    print("\n")
    print("Classification Report Test Data")
    print(classification_report(y_test, grid_search.best_estimator_.predict(X_test)))
                        
    return grid_search

Сетки параметров для GridSearchCV

В поиске по сетке мы исследуем работу классификатора. Набор параметров, используемых для тестирования производительности, указан ниже.

# Parameter grid settings for the vectorizers (Count and TFIDF)
parameters_vect = {
    'features__pipe__vect__max_df': (0.25, 0.5, 0.75),
    'features__pipe__vect__ngram_range': ((1, 1), (1, 2)),
    'features__pipe__vect__min_df': (1,2)
}

# Parameter grid settings for MultinomialNB
parameters_mnb = {
    'clf__alpha': (0.25, 0.5, 0.75)
}

# Parameter grid settings for LogisticRegression
parameters_logreg = {
    'clf__C': (0.25, 0.5, 1.0),
    'clf__penalty': ('l1', 'l2')
}

Классификаторы

Здесь мы сравним производительность MultinomialNB и LogisticRegression.

mnb = MultinomialNB()
logreg = LogisticRegression()

CountVectorizer

Чтобы использовать слова в классификаторе, нам нужно преобразовать слова в числа. Sklearn’s CountVectorizer принимает все слова во всех твитах, присваивает идентификатор и подсчитывает частоту появления слова в твите. Затем мы используем этот набор слов в качестве входных данных для классификатора. Этот набор слов представляет собой скудный набор данных. Это означает, что в каждой записи будет много нулей для слов, не встречающихся в твите.

countvect = CountVectorizer()
# MultinomialNB
best_mnb_countvect = grid_vect(mnb, parameters_mnb, X_train, X_test, parameters_text=parameters_vect, vect=countvect)
joblib.dump(best_mnb_countvect, '../output/best_mnb_countvect.pkl')
# LogisticRegression
best_logreg_countvect = grid_vect(logreg, parameters_logreg, X_train, X_test, parameters_text=parameters_vect, vect=countvect)
joblib.dump(best_logreg_countvect, '../output/best_logreg_countvect.pkl')

Векторизатор TF-IDF

Одна из проблем с CountVectorizer заключается в том, что часто встречаются слова. Эти слова могут не иметь дискриминационной информации. Таким образом их можно удалить. TF-IDF (частота термина - обратная частота документа) может использоваться для уменьшения веса этих часто встречающихся слов.

tfidfvect = TfidfVectorizer()
# MultinomialNB
best_mnb_tfidf = grid_vect(mnb, parameters_mnb, X_train, X_test, parameters_text=parameters_vect, vect=tfidfvect)
joblib.dump(best_mnb_tfidf, '../output/best_mnb_tfidf.pkl')
# LogisticRegression
best_logreg_tfidf = grid_vect(logreg, parameters_mnb, X_train, X_test, parameters_text=parameters_vect, vect=tfidfvect)
joblib.dump(best_logreg_tfidf, '../output/best_logreg_tfidf.pkl')

Word2Vec

Другой способ преобразования слов в числовые значения - использовать Word2Vec. Word2Vec отображает каждое слово в многомерном пространстве. Это делается с учетом контекста, в котором слово появляется в твитах. В результате похожие слова также находятся близко друг к другу в многомерном пространстве.

Алгоритм Word2Vec является частью пакета gensim.

Алгоритм Word2Vec использует в качестве входных данных списки слов. Для этого мы используем метод word_tokenize пакета nltk.

SIZE = 50
X_train['clean_text_wordlist'] = X_train.clean_text.apply(lambda x : word_tokenize(x))
X_test['clean_text_wordlist'] = X_test.clean_text.apply(lambda x : word_tokenize(x))
model = gensim.models.Word2Vec(X_train.clean_text_wordlist
, min_count=1
, size=SIZE
, window=5
, workers=4)
model.most_similar('plane', topn=3)

Модель Word2Vec предоставляет словарь слов всех твитов. Для каждого слова у вас также есть свои векторные значения. Количество значений вектора равно выбранному размеру. Это измерения, по которым каждое слово отображается в многомерном пространстве. Слова, встречаемость которых меньше min_count, не сохраняются в словаре.

Побочным эффектом параметра min_count является то, что некоторые твиты могут не иметь векторных значений. Это будет тот случай, когда слово (а) в твите встречается меньше чем min_count твитов. Из-за небольшого корпуса твитов в нашем случае есть риск, что это произойдет. Таким образом, мы устанавливаем значение min_count равным 1.

Твиты могут иметь разное количество векторов в зависимости от количества содержащихся в них слов. Чтобы использовать этот результат для моделирования, мы вычислим среднее значение всех векторов на твит. Таким образом, у нас будет одинаковое количество (т.е. размер) входных переменных для каждого твита.

Мы делаем это с помощью функции compute_avg_w2v_vector. В этой функции мы также проверяем, встречаются ли слова в твите в словаре модели Word2Vec. В противном случае возвращается список, заполненный 0,0. Иначе среднее из векторов слов.

def compute_avg_w2v_vector(w2v_dict, tweet):
    list_of_word_vectors = [w2v_dict[w] for w in tweet if w in w2v_dict.vocab.keys()]
    
    if len(list_of_word_vectors) == 0:
        result = [0.0]*SIZE
    else:
        result = np.sum(list_of_word_vectors, axis=0) / len(list_of_word_vectors)
        
    return result
X_train_w2v = X_train['clean_text_wordlist'].apply(lambda x: compute_avg_w2v_vector(model.wv, x))
X_test_w2v = X_test['clean_text_wordlist'].apply(lambda x: compute_avg_w2v_vector(model.wv, x))

Это дает нам серию с вектором размерности, равным SIZE. Теперь мы разделим этот вектор и создадим DataFrame с каждым значением вектора в отдельном столбце. Таким образом мы можем объединить переменные Word2Vec с другими переменными TextCounts. Нам нужно повторно использовать индекс X_train и X_test. В противном случае это вызовет проблемы (дубликаты) в конкатенации позже.

X_train_w2v = pd.DataFrame(X_train_w2v.values.tolist(), index= X_train.index)
X_test_w2v = pd.DataFrame(X_test_w2v.values.tolist(), index= X_test.index)
# Concatenate with the TextCounts variables
X_train_w2v = pd.concat([X_train_w2v, X_train.drop(['clean_text', 'clean_text_wordlist'], axis=1)], axis=1)
X_test_w2v = pd.concat([X_test_w2v, X_test.drop(['clean_text', 'clean_text_wordlist'], axis=1)], axis=1)

Мы рассматриваем только LogisticRegression, поскольку у нас есть отрицательные значения в векторах Word2Vec. MultinomialNB предполагает, что переменные имеют полиномиальное распределение. Поэтому они не могут содержать отрицательные значения.

best_logreg_w2v = grid_vect(logreg, parameters_logreg, X_train_w2v, X_test_w2v, is_w2v=True)
joblib.dump(best_logreg_w2v, '../output/best_logreg_w2v.pkl')

Заключение

  • Оба классификатора достигают наилучших результатов при использовании функций CountVectorizer.
  • Логистическая регрессия превосходит полиномиальный наивный байесовский классификатор
  • Наилучшая производительность в тестовом наборе обеспечивается LogisticRegression с функциями CountVectorizer.

Лучшие параметры:

  • C значение 1
  • L2 регуляризация
  • max_df: 0,5 или максимальная частота документа 50%.
  • min_df: 1 или слова должны появиться как минимум в 2 твитах
  • ngram_range: (1, 2), используются оба отдельных слова как биграммы

Метрики оценки:

  • Точность теста 81,3%. Это лучше, чем базовая производительность прогнозирования класса большинства (здесь отрицательное мнение) для всех наблюдений. Базовый уровень даст точность 63%.
  • Точность довольно высока для всех трех классов. Например, из всех случаев, которые мы прогнозируем как отрицательные, 80% отрицательных.
  • Отзыв для нейтрального класса низкий. Из всех нейтральных случаев в наших тестовых данных мы прогнозируем нейтральность только 48%.

Применяйте лучшую модель к новым твитам

Для развлечения мы будем использовать лучшую модель и применять ее к некоторым новым твитам, содержащим «@VirginAmerica». Я отобрал вручную 3 отрицательных и 3 положительных твита.

Благодаря GridSearchCV мы теперь знаем, какие гиперпараметры являются лучшими. Итак, теперь мы можем обучить лучшую модель всем обучающим данным, включая тестовые данные, которые мы разделили ранее.

textcountscols = ['count_capital_words','count_emojis','count_excl_quest_marks','count_hashtags'
,'count_mentions','count_urls','count_words']
features = FeatureUnion([('textcounts', ColumnExtractor(cols=textcountscols))
, ('pipe', Pipeline([('cleantext', ColumnExtractor(cols='clean_text'))
, ('vect', CountVectorizer(max_df=0.5, min_df=1, ngram_range=(1,2)))]))]
, n_jobs=-1)
pipeline = Pipeline([
('features', features)
, ('clf', LogisticRegression(C=1.0, penalty='l2'))
])
best_model = pipeline.fit(df_model.drop('airline_sentiment', axis=1), df_model.airline_sentiment)
# Applying on new positive tweets
new_positive_tweets = pd.Series(["Thank you @VirginAmerica for you amazing customer support team on Tuesday 11/28 at @EWRairport and returning my lost bag in less than 24h! #efficiencyiskey #virginamerica"
,"Love flying with you guys ask these years. Sad that this will be the last trip 😂 @VirginAmerica #LuxuryTravel"
,"Wow @VirginAmerica main cabin select is the way to fly!! This plane is nice and clean & I have tons of legroom! Wahoo! NYC bound! ✈️"])
df_counts_pos = tc.transform(new_positive_tweets)
df_clean_pos = ct.transform(new_positive_tweets)
df_model_pos = df_counts_pos
df_model_pos['clean_text'] = df_clean_pos
best_model.predict(df_model_pos).tolist()
# Applying on new negative tweets
new_negative_tweets = pd.Series(["@VirginAmerica shocked my initially with the service, but then went on to shock me further with no response to what my complaint was. #unacceptable @Delta @richardbranson"
,"@VirginAmerica this morning I was forced to repack a suitcase w a medical device because it was barely overweight - wasn't even given an option to pay extra. My spouses suitcase then burst at the seam with the added device and had to be taped shut. Awful experience so far!"
,"Board airplane home. Computer issue. Get off plane, traverse airport to gate on opp side. Get on new plane hour later. Plane too heavy. 8 volunteers get off plane. Ohhh the adventure of travel ✈️ @VirginAmerica"])
df_counts_neg = tc.transform(new_negative_tweets)
df_clean_neg = ct.transform(new_negative_tweets)
df_model_neg = df_counts_neg
df_model_neg['clean_text'] = df_clean_neg
best_model.predict(df_model_neg).tolist()

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

Надеюсь, вам понравился этот рассказ. И если да, не стесняйтесь хлопать в ладоши.