Это был проект для класса Forge (ранее HackCville) Node Pro, созданный Эндрю Линем, Джоном Саном, Шоаибом Рана и Эриком Джессом.

Цель этого классификатора — предсказать, является ли данный текст оскорблением.

Мы построим классификатор оскорблений, используя этот набор данных. Классификатор, который мы создадим, будет достаточно хорош, чтобы войти в топ-45 конкурса Kaggle, из которого взят этот набор данных. В качестве показателя оценки они использовали AUC.

Давайте начнем.

Во-первых, мы импортируем некоторые из необходимых нам пакетов:

import pandas as pd
import numpy as np
import textblob

Взгляните на данные

Давайте посмотрим, что представляет собой этот набор данных.

text = pd.read_csv("train.csv")
test = pd.read_csv("test.csv") # used later for evaluation
text.head()

Как видите, есть комментарии, которые не имеют абсолютно никакого смысла (пятая строка). Если мы присмотримся повнимательнее, то увидим, что это похоже на какой-то язык форматирования.

text["Comment"][4]
#This is what it outputs
"C\\xe1c b\\u1ea1n xu\\u1ed1ng \\u0111\\u01b0\\u1eddng bi\\u1ec3u t\\xecnh 2011 c\\xf3 \\xf4n ho\\xe0 kh\\xf4ng ? \\nC\\xe1c ng\\u01b0 d\\xe2n ng\\u1ed3i cu\\xed \\u0111\\u1ea7u chi\\u1ee5 nh\\u1ee5c c\\xf3 \\xf4n ho\\xe0 kh\\xf4ng ?\\nC\\xe1c n\\xf4ng d\\xe2n gi\\u1eef \\u0111\\u1ea5t \\u1edf V\\u0103n Giang, C\\u1ea7n Th\\u01a1 c\\xf3 \\xf4n ho\\xe0 kh\\xf4ng ?\\n.................\\nR\\u1ed1t cu\\u1ed9c \\u0111\\u01b0\\u1ee3c g\\xec\\xa0 th\\xec ch\\xfang ta \\u0111\\xe3 bi\\u1ebft !\\nAi c\\u0169ng y\\xeau chu\\u1ed9ng ho\\xe0 b\\xecnh, nh\\u01b0ng \\u0111\\xf4i khi ho\\xe0 b\\xecnh ch\\u1ec9 th\\u1eadt s\\u1ef1 \\u0111\\u1ebfn sau chi\\u1ebfn tranh m\\xe0 th\\xf4i.\\nKh\\xf4ng c\\xf2n con \\u0111\\u01b0\\u1eddng n\\xe0o ch\\u1ecdn kh\\xe1c \\u0111\\xe2u, \\u0111\\u1eebng m\\u01a1 th\\xeam n\\u01b0\\xe3."

Нам просто придется пока игнорировать эти экземпляры, так как мы делаем быстрый и простой классификатор.

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

import seaborn as sns
sns.countplot(x = "Insult", data = text)
prevalence = sum(text["Insult"]) / len(text["Insult"])

Распространенность составила 0,27, что означает, что 27% комментариев помечены как оскорбления. Это достаточно сбалансировано.

Мы также можем увидеть, как выглядят 50 самых распространенных слов. Проделав некоторую предобработку и фильтрацию стоп-слов, мы приходим к такому фрагменту кода:

import plotly.express as px

fig = px.bar(visdata, y='Count', title="50 most frequent words, less stop words and spaces")
fig.update_layout(
    xaxis_title="Word",
    autosize=False,
    width=1500,
    height=1200,
    paper_bgcolor="LightSteelBlue",
)
fig.show()

«нравится», «люди» и «получать» входят в тройку самых распространенных слов. Первое ругательство стоит на позиции 9.

Преобразование комментариев во что-то понятное компьютеру

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

from sklearn.feature_extraction.text import CountVectorizer
vectorizer = CountVectorizer(analyzer='word', max_features = 500, stop_words="english") # We'll just get the 500 most common words
X2 = vectorizer.fit_transform(text["Comment"])
test_set = vectorizer.transform(test["Comment"]) # for later

Итак, что только что произошло, так это то, что мы создали объект CountVectorizer, а затем сказали, что хотим получить 500 наиболее распространенных слов, используя max_features = 500. Stop_words исключит наиболее распространенные слова в английском языке, такие как «a», «the», « an» и т. д. Затем мы одновременно вызывали fit() и transform() в столбце Comment фрейма данных, используя fit_transform().

fit() изучит словарь из столбца «Комментарий».

transform() закодирует столбец Comment в вектор

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

top500 = pd.DataFrame(data=X2.toarray(),
columns=vectorizer.get_feature_names())
top500_test = pd.DataFrame(data=test_set.toarray(), columns=vectorizer.get_feature_names()) # for later

Теперь нам нужно подумать о том, что делает оскорбление оскорблением. Мы придумали такие функции, как тональность, субъективность, наличие «вас» и длина комментария.

Мы полагали, что оскорбления будут носить негативный характер и будут более субъективными. Мы также подумали, что если в комментарии есть слово «вы», то, скорее всего, комментарий будет оскорблением. Бывший. Вы…, Вы… и т. д. Длина была добавлена ​​сюда в качестве экспериментальной функции.

text["sentiment"] = text.Comment.apply(lambda x: textblob.TextBlob(x).sentiment.polarity)
text["subjectivity"] = text.Comment.apply(lambda x: textblob.TextBlob(x).sentiment.subjectivity)
text["You"] = text["Comment"].str.lower().str.contains("you")
text["You"] = np.where(text["You"], 1, 0)
text["Length"] = text.Comment.apply(lambda x: len(x))
# We do this similarly for the test set as well

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

full = pd.concat([text, top500], axis=1)
test_full = pd.concat([test, top500_test], axis=1) # for later
full

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

# Train Set
X = full.drop(["Insult", "Date", "Comment"], axis=1)
y = text["Insult"]
# Test Set
X_test = test_full.drop(["Insult", "Date", "Comment", "Usage"], axis=1)
y_test = test["Insult"]

Выбор модели

Это самое интересное! Теперь мы можем протестировать разные модели и посмотреть, как они работают.

Модели, которые мы решили протестировать, — это классификатор случайного леса, логистическая регрессия, машина опорных векторов, дерево решений Adaboosted и классификатор дерева решений в мешках.

Конечная цель заключалась в том, чтобы поместить их в классификатор VotingClassifier с жестким голосованием.

Давайте создадим нашу первую модель, классификатор случайного леса, и подгоним ее.

from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import RandomizedSearchCV
param_grid = [
{'n_estimators': np.arange(1, 1000),
'max_features': np.arange(100,500),
'max_depth': np.arange(30, 150),
'max_samples': np.arange(1000,2500),
'max_leaf_nodes': np.arange(10, 500),
'min_samples_split' : np.arange(2, 50),
'min_weight_fraction_leaf' : np.arange(0, 0.5)
}]
rf_clf = RandomForestClassifier(random_state=42)
grid_search = RandomizedSearchCV(rf_clf, param_grid, n_iter = 8, cv=5,scoring='neg_mean_squared_error', random_state = 42, return_train_score=True)
grid_search.fit(X, y)
forest_model = grid_search.best_estimator_
forest_model

Мы использовали RandomizedSearchCV, чтобы найти лучшие гиперпараметры из выбора, указанного в param_grid.

Только на этот раз мы увидим, какие гиперпараметры оказались лучшими.

RandomForestClassifier(bootstrap=True, ccp_alpha=0.0, class_weight=None, criterion='gini', max_depth=144, max_features=182, max_leaf_nodes=160, max_samples=1630,                        min_impurity_decrease=0.0, min_impurity_split=None,                        min_samples_leaf=1, min_samples_split=38,                        min_weight_fraction_leaf=0.0, n_estimators=665,                        n_jobs=None, oob_score=False, random_state=42, verbose=0,                        warm_start=False)

Хорошо, теперь мы можем оценить его и посмотреть, как он работает сам по себе.

pred = forest_model.predict(X_test)
fpr, tpr, thresholds = metrics.roc_curve(y_test, pred, pos_label=1)
print("AUC:", metrics.auc(fpr, tpr))
print("Accuracy:", accuracy_score(y_test,pred))
print("Recall:", recall_score(y_test, pred))
print("Precision Score:", precision_score(y_test, pred)
print("F1 Score:", f1_score(y_test, pred))

Это показатели:

AUC: 0.7219574750280994 
Accuracy: 0.8273517189270873 
Recall: 0.5007215007215007 
Precision Score: 0.75764192139738 
F1 Score: 0.6029539530842745

Одна только эта модель позволила бы нам занять 41-е место в таблице лидеров.

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

# Support Vector Machine Metrics
AUC: 0.7213072381956723 
Accuracy: 0.7385719682659615 
Recall: 0.001443001443001443 
Precision Score: 1.0 
F1 Score: 0.0028818443804034585

Что случилось? AUC и точность, кажется, в порядке. Остановившись на этих показателях, вы упустите то, что произошло с отзывом и F1-Score.

Отзыв можно рассматривать как «из всех наблюдений, помеченных как 1 (в данном случае), какой процент вы предсказали правильно».

F1-Score – это среднее гармоническое для отзыва и точности.

Матрица путаницы также может пролить свет на то, что произошло.

print(confusion_matrix(y_test, pred))

# The Confusion Matrix
[[1954    0]  
[ 692    1]]

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

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

Из-за этих результатов мы хотим исключить машину опорных векторов из нашей модели ансамбля.

Окончательная модель ансамбля

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

Давайте составим это следующим образом:

from sklearn.ensemble import VotingClassifier
estimators = [('log', log_model), ('rf', forest_model), ('bag', bag_clf)]
voting_total = VotingClassifier(estimators=estimators, voting="hard")
voting_total.fit(X, y)

Теперь давайте посмотрим, как это работает.

final_pred = voting_total.predict(X_test)
fpr, tpr, thresholds = metrics.roc_curve(y_test, final_pred, pos_label=1)
print("AUC:", metrics.auc(fpr, tpr))
print("Accuracy:", accuracy_score(y_test,final_pred ))
print("Recall:", recall_score(y_test, final_pred ))
print("Precision Score:", precision_score(y_test, final_pred ))
print("F1 Score:", f1_score(y_test, final_pred ))
print(confusion_matrix(y_test, final_pred ))

Окончательные показатели

AUC: 0.7240758218240306 
Accuracy: 0.8277295051001133 
Recall: 0.5064935064935064 
Precision Score: 0.7548387096774194 
F1 Score: 0.6062176165803108 
# The Confusion Matrix
[[1840  114]  
[ 342  351]]

AUC, которая представляет собой площадь под кривой рабочей характеристики приемника (ROC), а также показатель оценки, используемый конкурентами, является самым высоким из любой отдельной модели.

Этот AUC по-прежнему оставит нас на 41-м месте, но он лучше любой отдельной модели.

Если вы вспомните (без каламбура), то отзыв классификатора можно рассматривать как «из всех наблюдений, помеченных как 1 (в данном случае), какой процент вы предсказали правильно». Ранее мы подсчитали, что распространенность составила 0,27, поэтому модель правильно получает 50% комментариев, помеченных как «оскорбление», на самом деле не так уж и плохо. Однако его следует улучшить.

И вот он, быстрый и простой классификатор оскорблений.

Демонстрация

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

Как бы вы это сделали:

insult = "You're bad noob"
# Turning the text into something the model can take in
sentiment = textblob.TextBlob(insult).sentiment.polarity
subjectivity = textblob.TextBlob(insult).sentiment.subjectivity
length = len(insult)
indexofbad = vectorizer.get_feature_names().index('bad')
lst = [0] * 504
lst[0] = sentiment
lst[1] = subjectivity
lst[2] = 1
lst[3] = length
lst[indexofbad] = 1
lst = np.array(lst)
insult_transformed = lst.reshape(1, -1)

Теперь, когда мы предварительно обработали текст во что-то, что может воспринять модель, мы просто делаем:

voting_total.predict(insult_transformed)

И вывод:

array([1])

Это означает, что он классифицировал это как оскорбление!

Будущие улучшения

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

  • Есть лучшие способы расчета настроений и субъективности чего-либо.
  • В nltk есть модуль SentimentAnalyzer, который будет работать намного лучше, чем Textblob, и в нем также есть функции извлечения функций.
  • Textblob не имеет обширного словарного запаса, поэтому, если он увидит такое слово, как «нуб», он просто присвоит ему тональность 0. Использование другой библиотеки с большим словарем улучшит этот классификатор.
  • Наш подход к набору слов мог бы быть более чистым, поскольку он получал очень похожие слова, такие как, например, «америка», «американец» и «американцы».
  • Расширение до n-грамм и использование нейронных сетей также может улучшить прогнозы.
  • Некоторые комментарии, которые мы наблюдали, были объективно оскорблениями, но не были классифицированы как таковые. Просмотр каждого комментария и обнаружение этих ошибок также может помочь