Контекст и цель проекта
Интернет-магазин «Викишоп» запустил новый сервис, позволяющий пользователям редактировать и дополнять описания товаров по принципу вики-сообществ. Клиенты могут предлагать свои правки и комментировать изменения других пользователей. Для поддержания здоровой атмосферы в сообществе магазину потребовался инструмент, который автоматически обнаруживает токсичные комментарии и отправляет их на модерацию.
Задача проекта: Обучить модель классифицировать комментарии на позитивные и негативные (токсичные) с метрикой качества F1 не менее 0.75.
Данные: Набор данных toxic_comments.csv с текстом комментариев и бинарной меткой токсичности.
Архитектура решения
Подготовка данных
Работа с текстовыми данными требует особого подхода к предобработке:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
import re
import warnings
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.metrics import f1_score, classification_report, confusion_matrix
from sklearn.pipeline import Pipeline
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.svm import LinearSVC
import nltk
from nltk.corpus import wordnet
from nltk.stem import WordNetLemmatizer
from nltk import pos_tag
from nltk.tokenize import word_tokenize
# Скачивание ресурсов NLP
nltk.download('punkt', quiet=True)
nltk.download('punkt_tab', quiet=True)
nltk.download('averaged_perceptron_tagger', quiet=True)
nltk.download('averaged_perceptron_tagger_eng', quiet=True)
nltk.download('wordnet', quiet=True)
nltk.download('omw-1.4', quiet=True)
warnings.filterwarnings('ignore')
pd.set_option('display.max_colwidth', 120)
RANDOM_STATE = 42
|
Загрузка и анализ данных
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# Загрузка данных
df = pd.read_csv('toxic_comments.csv')
print(f"Размер датасета: {df.shape}")
print(f"Количество токсичных комментариев: {df['toxic'].sum()}")
print(f"Доля токсичных комментариев: {df['toxic'].mean():.2%}")
# Примеры комментариев
print("\nПримеры токсичных комментариев:")
for i, row in df[df['toxic'] == 1].head(3).iterrows():
print(f"{i}: {row['text'][:100]}...")
print("\nПримеры нетоксичных комментариев:")
for i, row in df[df['toxic'] == 0].head(3).iterrows():
print(f"{i}: {row['text'][:100]}...")
|
Проблема несбалансированных данных: В задачах классификации текста часто встречается дисбаланс классов. В нашем случае токсичные комментарии составляют меньшинство, что требует особого подхода к оценке модели.
Предобработка текста
Лемматизация и очистка текста
Для улучшения качества классификации необходимо привести текст к нормальной форме:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
def get_wordnet_pos(treebank_tag):
"""Преобразование тегов частей речи из Treebank в WordNet."""
if treebank_tag.startswith('J'):
return wordnet.ADJ
elif treebank_tag.startswith('V'):
return wordnet.VERB
elif treebank_tag.startswith('N'):
return wordnet.NOUN
elif treebank_tag.startswith('R'):
return wordnet.ADV
else:
return wordnet.NOUN
def preprocess_text(text):
"""Полная предобработка текста: очистка, токенизация, лемматизация."""
if not isinstance(text, str):
return ""
# Очистка текста
text = text.lower()
text = re.sub(r'[^a-zA-Z\s]', '', text)
text = re.sub(r'\s+', ' ', text).strip()
# Токенизация
tokens = word_tokenize(text)
# Определение частей речи и лемматизация
lemmatizer = WordNetLemmatizer()
pos_tags = pos_tag(tokens)
lemmatized_tokens = []
for token, tag in pos_tags:
wordnet_pos = get_wordnet_pos(tag)
lemma = lemmatizer.lemmatize(token, wordnet_pos)
lemmatized_tokens.append(lemma)
return ' '.join(lemmatized_tokens)
# Применение предобработки ко всему датасету
df['processed_text'] = df['text'].apply(preprocess_text)
|
Векторизация текста с TF-IDF
TF-IDF (Term Frequency-Inverse Document Frequency) — один из наиболее эффективных методов представления текста в числовом виде:
1
2
3
4
5
6
7
8
9
10
11
12
|
# Разделение данных на обучающую и тестовую выборки
X = df['processed_text']
y = df['toxic']
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=RANDOM_STATE, stratify=y
)
print(f"Обучающая выборка: {X_train.shape[0]} примеров")
print(f"Тестовая выборка: {X_test.shape[0]} примеров")
print(f"Доля токсичных в обучающей: {y_train.mean():.2%}")
print(f"Доля токсичных в тестовой: {y_test.mean():.2%}")
|
Обучение моделей
Сравнение подходов
Для задачи классификации текста были выбраны две модели:
- Logistic Regression — линейная модель, хорошо работающая с разреженными данными
- LinearSVC — метод опорных векторов с линейным ядром
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
# Пайплайн для Logistic Regression
lr_pipeline = Pipeline([
('tfidf', TfidfVectorizer(
max_features=5000,
ngram_range=(1, 2),
min_df=5,
max_df=0.7
)),
('clf', LogisticRegression(
random_state=RANDOM_STATE,
class_weight='balanced',
max_iter=1000
))
])
# Пайплайн для LinearSVC
svc_pipeline = Pipeline([
('tfidf', TfidfVectorizer(
max_features=5000,
ngram_range=(1, 2),
min_df=5,
max_df=0.7
)),
('clf', LinearSVC(
random_state=RANDOM_STATE,
class_weight='balanced',
max_iter=10000
))
])
# Обучение моделей
lr_pipeline.fit(X_train, y_train)
svc_pipeline.fit(X_train, y_train)
|
Оптимизация гиперпараметров
Для улучшения качества модели был проведен поиск оптимальных гиперпараметров:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
# Параметры для Grid Search
param_grid = {
'tfidf__max_features': [3000, 5000, 10000],
'tfidf__ngram_range': [(1, 1), (1, 2)],
'clf__C': [0.1, 1, 10]
}
# Grid Search для Logistic Regression
grid_search_lr = GridSearchCV(
lr_pipeline,
param_grid,
cv=3,
scoring='f1',
n_jobs=-1,
verbose=1
)
grid_search_lr.fit(X_train, y_train)
print(f"Лучшие параметры для Logistic Regression: {grid_search_lr.best_params_}")
print(f"Лучший F1-score: {grid_search_lr.best_score_:.4f}")
|
Оценка качества моделей
Метрики классификации
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
def evaluate_model(model, X_test, y_test, model_name):
"""Полная оценка модели с выводом метрик."""
y_pred = model.predict(X_test)
print(f"\n=== {model_name} ===")
print(f"F1 Score: {f1_score(y_test, y_pred):.4f}")
print("\nClassification Report:")
print(classification_report(y_test, y_pred))
# Матрица ошибок
cm = confusion_matrix(y_test, y_pred)
print("Confusion Matrix:")
print(f"True Negatives: {cm[0, 0]}")
print(f"False Positives: {cm[0, 1]}")
print(f"False Negatives: {cm[1, 0]}")
print(f"True Positives: {cm[1, 1]}")
return y_pred
# Оценка обеих моделей
lr_pred = evaluate_model(lr_pipeline, X_test, y_test, "Logistic Regression")
svc_pred = evaluate_model(svc_pipeline, X_test, y_test, "LinearSVC")
|
Анализ важных признаков
Для интерпретации модели можно проанализировать наиболее важные признаки:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
def get_top_features(model, vectorizer, n=20):
"""Получение наиболее важных признаков для классификации."""
feature_names = vectorizer.get_feature_names_out()
coefficients = model.named_steps['clf'].coef_[0]
# Создание DataFrame с признаками и их весами
features_df = pd.DataFrame({
'feature': feature_names,
'coefficient': coefficients
})
# Топ признаков для токсичных комментариев
toxic_top = features_df.nlargest(n, 'coefficient')
# Топ признаков для нетоксичных комментариев
non_toxic_top = features_df.nsmallest(n, 'coefficient')
return toxic_top, non_toxic_top
# Получение топ признаков для лучшей модели
vectorizer = lr_pipeline.named_steps['tfidf']
toxic_top, non_toxic_top = get_top_features(lr_pipeline, vectorizer, 15)
print("Топ признаков для токсичных комментариев:")
print(toxic_top[['feature', 'coefficient']].to_string(index=False))
print("\nТоп признаков для нетоксичных комментариев:")
print(non_toxic_top[['feature', 'coefficient']].to_string(index=False))
|
Ключевые особенности проекта
Основные достижения:
- Реализован полный пайплайн обработки текста: от очистки до классификации
- Достигнута метрика F1-score > 0.75, что превышает требования проекта
- Сравнены две модели машинного обучения с подробным анализом результатов
- Разработана система интерпретации модели через анализ важных признаков
Вызовы и решения:
- Проблема: Несбалансированные данные (токсичные комментарии в меньшинстве)
- Решение: Использование
class_weight='balanced' в моделях
- Проблема: Шум в текстовых данных (орфографические ошибки, сленг)
- Решение: Комплексная предобработка с лемматизацией и очисткой
- Проблема: Выбор оптимальных гиперпараметров
- Решение: Grid Search с кросс-валидацией
Технологический стек
| Технология |
Назначение |
Версия |
| Python |
Основной язык программирования |
3.9+ |
| pandas |
Обработка и анализ данных |
1.5+ |
| scikit-learn |
Машинное обучение и предобработка |
1.3+ |
| nltk |
Обработка естественного языка |
3.8+ |
| NumPy |
Научные вычисления |
1.24+ |
| Jupyter Notebook |
Интерактивная разработка |
6.5+ |
| re (регулярные выражения) |
Очистка текста |
встроенный |
Практическое применение
Разработанная модель может быть интегрирована в реальную систему модерации:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
class CommentModerationSystem:
"""Система автоматической модерации комментариев."""
def __init__(self, model, threshold=0.5):
self.model = model
self.threshold = threshold
def moderate_comment(self, text):
"""Модерация отдельного комментария."""
# Предобработка текста
processed_text = preprocess_text(text)
# Предсказание вероятности токсичности
if hasattr(self.model, 'predict_proba'):
proba = self.model.predict_proba([processed_text])[0, 1]
else:
# Для LinearSVC используем decision_function
decision = self.model.decision_function([processed_text])[0]
proba = 1 / (1 + np.exp(-decision))
# Принятие решения
is_toxic = proba >= self.threshold
return {
'text': text,
'processed_text': processed_text,
'toxicity_probability': proba,
'is_toxic': is_toxic,
'action': 'send_to_moderation' if is_toxic else 'publish',
'confidence': 'high' if abs(proba - 0.5) > 0.3 else 'medium'
}
def batch_moderate(self, comments):
"""Пакетная модерация комментариев."""
results = []
for comment in comments:
results.append(self.moderate_comment(comment))
return pd.DataFrame(results)
# Пример использования
moderation_system = CommentModerationSystem(lr_pipeline)
test_comments = [
"This product is amazing! Highly recommended!",
"You are an idiot and your product is garbage.",
"Could be better, but overall not bad."
]
results = moderation_system.batch_moderate(test_comments)
print(results[['text', 'toxicity_probability', 'action']])
|
Заключение
Проект демонстрирует практическое применение методов NLP и машинного обучения для решения реальной бизнес-задачи — автоматической модерации пользовательского контента. Разработанная система позволяет эффективно фильтровать токсичные комментарии, снижая нагрузку на модераторов-людей и поддерживая здоровую атмосферу в сообществе.
Дальнейшее развитие:
- Эксперименты с более сложными моделями (BERT, RoBERTa)
- Добавление мультиязычной поддержки
- Интеграция с реальной системой комментариев интернет-магазина
- Создание дашборда для мониторинга качества модерации
- Реализация обратной связи от модераторов для дообучения модели
Ссылки и ресурсы
Посмотреть полный проект с кодом и подробным анализом можно на GitHub:
GitHub Repository: Wikishop Toxic Comments Classification