Это был проект для класса 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-грамм и использование нейронных сетей также может улучшить прогнозы.
- Некоторые комментарии, которые мы наблюдали, были объективно оскорблениями, но не были классифицированы как таковые. Просмотр каждого комментария и обнаружение этих ошибок также может помочь