Това беше проект за клас Node Pro на Forge (бивш HackCville) от Андрю Лин, Джон Сън и Шоаиб Рана и Ерик Джес.

Целта на този класификатор е да предвиди дали даден текст е обида или не.

Ще изградим класификатор на обиди, използвайки този набор от данни. Класификаторът, който изграждаме, ще бъде достатъчно добър, за да бъде в топ 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()

„харесвам“, „хора“ и „получавам“ влизат в топ 3 на най-често срещаните думи. Първата ругатня идва на позиция 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() едновременно в колоната за коментар на рамката с данни, използвайки fit_transform().

fit() ще научи речника от колоната за коментари

transform() ще кодира колоната за коментар във вектор

Сега, когато имаме нашата торба с думи във вектор, който нарекохме 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

Не се нуждаем от колони за дата и коментар, тъй като те са във формат, в който компютърът не може да ги разбере, така че ще ги премахнем. Също така ще изпуснем колоната Insult от обучителната променлива 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"]

Избор на модел

Това е забавната част! Вече можем да тестваме различни модели и да видим как се представят.

Моделите, които решихме да тестваме, бяха Random Forest Classifier, Logistic Regression, Support Vector Machine, Adaboosted Decision Tree и Bagging Decision Tree Classifier.

Крайната цел беше да ги поставим в VotingClassifier с трудно гласуване.

Нека изградим нашия първи модел, Random Forest Classifier, и да го монтираме.

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 и точността изглеждат добре. Спирането на тези показатели ще ви накара да пропуснете случилото се с Recall и F1-Score.

Припомнянето може да се разглежда като „от всички наблюдения, означени с 1 (в този случай), какъв процент прогнозирахте правилно.“

F1-резултат е средната хармонична стойност на припомнянето и прецизността.

Матрицата на объркването също може да хвърли светлина върху случилото се.

print(confusion_matrix(y_test, pred))

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

Използвахме полиномиално ядро ​​за машината за поддържащи вектори и RandomizedSearchCV установи, че степен две е най-подходяща за това, което търси. Начинът, по който опорните вектори начертаха границите на решението, означаваше, че не може да класифицира правилно истинския клас в това ядро. Това означава, че това ядро ​​вероятно не е правилното ядро, което да се използва, за да се поберат тези данни и вероятно е направена функционална грешка във формата.

Някои от другите ядра, които можехме да опитаме, включват линейна и радиална базисна функция на Гаус. Ще продължим оттам, тъй като отнема много време за обучение и намиране на оптималните хиперпараметри без свързване към GPU.

Поради тези резултати ще искаме да изключим машината за поддържащи вектори от нашия модел на ансамбъла.

Последният модел на ансамбъл

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

Нека го сглобим, както следва:

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 няма обширен речник, така че ако види дума като „noob“, той просто ще й присвои чувство 0. Използването на друга библиотека, която има по-голям речник, ще подобри този класификатор
  • Нашият подход на торба с думи можеше да бъде по-изчистен, тъй като получаваше много подобни думи като например „америка“, „американец“ и „американци“
  • Разширяването в n-грами и използването на невронни мрежи също може да подобри прогнозите
  • Някои от коментарите, които наблюдавахме, бяха обективни обиди, но не бяха класифицирани като такива. Преглеждането на всеки коментар и намирането на тези грешки също може да помогне