← Назад к вопросам

NLP: Классификация текстов

2.4 Senior🔥 151 комментариев
#NLP и обработка текста#Машинное обучение

Условие

Вам нужно построить модель для классификации отзывов клиентов по тональности (положительный/отрицательный/нейтральный).

Опишите:

  1. Как предобработать текст
  2. Какие методы векторизации использовать
  3. Какие модели попробовать (классические ML vs BERT)
  4. Как оценить качество

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Решение

1. Предобработка текста

import pandas as pd
import numpy as np
import re
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize
from nltk.stem import SnowballStemmer, WordNetLemmatizer
import nltk

# Загрузить необходимые ресурсы
nltk.download('punkt')
nltk.download('stopwords')
nltk.download('wordnet')

# Загрузка данных
df = pd.read_csv('reviews.csv')
print(f"Размер датасета: {df.shape}")
print(f"\nПримеры отзывов:")
print(df['text'].head())

# === ШАГ 1: БАЗОВАЯ ОЧИСТКА ===

def preprocess_text(text):
    # Приведение к нижнему регистру
    text = text.lower()
    
    # Удаление URL
    text = re.sub(r'http\S+|www.\S+', '', text)
    
    # Удаление email
    text = re.sub(r'\S+@\S+', '', text)
    
    # Удаление специальных символов (сохраняем букву, цифру, пробел)
    text = re.sub(r'[^a-zA-Zа-яА-Я0-9\s]', '', text)
    
    # Удаление лишних пробелов
    text = re.sub(r'\s+', ' ', text).strip()
    
    return text

df['text_clean'] = df['text'].apply(preprocess_text)

print(f"\nПосле базовой очистки:")
print(df['text_clean'].head())

# === ШАГ 2: ТОКЕНИЗАЦИЯ ===

def tokenize_text(text):
    tokens = word_tokenize(text)
    return tokens

df['tokens'] = df['text_clean'].apply(tokenize_text)

print(f"\nПример токенизации:")
print(f"'{df['text_clean'].iloc[0]}' →")
print(df['tokens'].iloc[0][:20])

# === ШАГ 3: УДАЛЕНИЕ STOPWORDS (СЛУЖЕБНЫЕ СЛОВА) ===

stop_words = set(stopwords.words('english'))

def remove_stopwords(tokens):
    return [token for token in tokens if token not in stop_words and len(token) > 2]

df['tokens_no_stop'] = df['tokens'].apply(remove_stopwords)

print(f"\nПосле удаления stopwords:")
print(df['tokens_no_stop'].iloc[0][:20])

# === ШАГ 4: ЛЕММАТИЗАЦИЯ (ПРИВЕДЕНИЕ К БАЗОВОЙ ФОРМЕ) ===

lemmatizer = WordNetLemmatizer()

def lemmatize_tokens(tokens):
    return [lemmatizer.lemmatize(token, pos='v') for token in tokens]

df['tokens_lemma'] = df['tokens_no_stop'].apply(lemmatize_tokens)

print(f"\nПосле лемматизации:")
print(df['tokens_lemma'].iloc[0][:20])

# === ШАГ 5: СТЕММИНГ (АЛЬТЕРНАТИВА ЛЕММАТИЗАЦИИ) ===

stemmer = SnowballStemmer('english')

def stem_tokens(tokens):
    return [stemmer.stem(token) for token in tokens]

df['tokens_stem'] = df['tokens_no_stop'].apply(stem_tokens)

print(f"""
=== ЛЕММАТИЗАЦИЯ VS СТЕММИНГ ===

Лемматизация:
  + Точнее (использует словари)
  + Результат понятнее
  - Медленнее
  Пример: running → run, better → good

Стемминг:
  + Быстрее (просто обрезает суффиксы)
  - Менее точен
  Пример: running → runn, better → better

Выбор: Для продакшена обычно лемматизация
""")

2. Методы векторизации

2.1 Bag of Words (BoW)

from sklearn.feature_extraction.text import CountVectorizer

bow_vectorizer = CountVectorizer(
    max_features=5000,  # топ 5000 слов
    min_df=2,  # минимум в 2 документах
    max_df=0.8,  # максимум в 80% документов
    stop_words='english',
    ngram_range=(1, 2)  # uni и bigrams
)

X_bow = bow_vectorizer.fit_transform(df['text_clean'])

print(f"\nBag of Words:")
print(f"  Форма матрицы: {X_bow.shape} (документы x признаки)")
print(f"  Разреженность: {1 - X_bow.nnz / (X_bow.shape[0] * X_bow.shape[1]):.2%}")
print(f"  Топ 10 слов: {bow_vectorizer.get_feature_names_out()[:10]}")

2.2 TF-IDF

from sklearn.feature_extraction.text import TfidfVectorizer

tfidf_vectorizer = TfidfVectorizer(
    max_features=5000,
    min_df=2,
    max_df=0.8,
    stop_words='english',
    ngram_range=(1, 2),
    sublinear_tf=True  # применяет log(TF)
)

X_tfidf = tfidf_vectorizer.fit_transform(df['text_clean'])

print(f"\nTF-IDF:")
print(f"  TF (Term Frequency): как часто слово в документе")
print(f"  IDF (Inverse Document Frequency): редкость слова во всех документах")
print(f"  TF-IDF = TF * IDF (важные и редкие слова получают больший вес)")
print(f"\n  Форма матрицы: {X_tfidf.shape}")
print(f"  Разреженность: {1 - X_tfidf.nnz / (X_tfidf.shape[0] * X_tfidf.shape[1]):.2%}")

2.3 Word Embeddings (Word2Vec)

from gensim.models import Word2Vec

# Обучение Word2Vec на текстах
sentences = df['tokens_lemma'].tolist()

w2v_model = Word2Vec(
    sentences=sentences,
    vector_size=100,  # размерность вектора
    window=5,  # контекстное окно
    min_count=2,  # минимум вхождений слова
    workers=4,
    sg=1  # Skip-gram (лучше для малых датасетов)
)

print(f"\nWord2Vec:")
print(f"  Размерность вектора: {w2v_model.vector_size}")
print(f"  Словарь: {len(w2v_model.wv)} слов")
print(f"  Пример: вектор для 'good': {w2v_model.wv['good'][:5]}...")

# Усреднение векторов слова в документ
def get_w2v_vector(tokens, model, vector_size=100):
    vectors = [model.wv[token] for token in tokens if token in model.wv]
    if vectors:
        return np.mean(vectors, axis=0)
    return np.zeros(vector_size)

X_w2v = np.array([get_w2v_vector(tokens, w2v_model) for tokens in df['tokens_lemma']])

print(f"\n  Матрица документов: {X_w2v.shape}")

2.4 Pre-trained Word Embeddings (GloVe, FastText)

# Загрузка GloVe (требует предварительной загрузки файла)
# Альтернатива: использовать gensim.downloader

import gensim.downloader as api

# Загружаем предобученные embeddings
glove_vectors = api.load('glove-wiki-gigaword-300')

print(f"\nPre-trained GloVe:")
print(f"  Размер словаря: {len(glove_vectors)}")
print(f"  Размерность: 300")
print(f"  Пример синонимов 'good': {glove_vectors.most_similar('good', topn=5)}")

3. Модели классификации

3.1 Классические ML модели

from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.svm import LinearSVC
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score
import lightgbm as lgb

# Кодирование целевой переменной
label_mapping = {'negative': 0, 'neutral': 1, 'positive': 2}
df['label_encoded'] = df['label'].map(label_mapping)

# Разделение данных
X_train, X_test, y_train, y_test = train_test_split(
    X_tfidf, df['label_encoded'], test_size=0.2, random_state=42, stratify=df['label_encoded']
)

print(f"\nTrain: {X_train.shape}, Test: {X_test.shape}")
print(f"Распределение классов (train): {np.bincount(y_train)}")

# === МОДЕЛЬ 1: Логистическая регрессия ===

lr_model = LogisticRegression(
    max_iter=1000,
    class_weight='balanced',
    C=1.0,  # регуляризация
    random_state=42
)

lr_model.fit(X_train, y_train)
y_pred_lr = lr_model.predict(X_test)

print(f"\n=== LOGISTIC REGRESSION ===")
print(f"  Accuracy: {accuracy_score(y_test, y_pred_lr):.4f}")
print(f"  Precision: {precision_score(y_test, y_pred_lr, average='weighted'):.4f}")
print(f"  Recall: {recall_score(y_test, y_pred_lr, average='weighted'):.4f}")
print(f"  F1-Score: {f1_score(y_test, y_pred_lr, average='weighted'):.4f}")

# === МОДЕЛЬ 2: Random Forest ===

rf_model = RandomForestClassifier(
    n_estimators=100,
    max_depth=15,
    class_weight='balanced',
    random_state=42,
    n_jobs=-1
)

rf_model.fit(X_train, y_train)
y_pred_rf = rf_model.predict(X_test)

print(f"\n=== RANDOM FOREST ===")
print(f"  Accuracy: {accuracy_score(y_test, y_pred_rf):.4f}")
print(f"  F1-Score: {f1_score(y_test, y_pred_rf, average='weighted'):.4f}")

# === МОДЕЛЬ 3: LightGBM (обычно лучше) ===

lgb_model = lgb.LGBMClassifier(
    n_estimators=100,
    learning_rate=0.05,
    max_depth=7,
    num_leaves=31,
    class_weight='balanced',
    random_state=42
)

lgb_model.fit(X_train, y_train)
y_pred_lgb = lgb_model.predict(X_test)

print(f"\n=== LIGHTGBM ===")
print(f"  Accuracy: {accuracy_score(y_test, y_pred_lgb):.4f}")
print(f"  F1-Score: {f1_score(y_test, y_pred_lgb, average='weighted'):.4f}")

# === МОДЕЛЬ 4: SVM ===

svm_model = LinearSVC(
    C=1.0,
    class_weight='balanced',
    max_iter=2000,
    random_state=42
)

svm_model.fit(X_train, y_train)
y_pred_svm = svm_model.predict(X_test)

print(f"\n=== LINEAR SVM ===")
print(f"  Accuracy: {accuracy_score(y_test, y_pred_svm):.4f}")
print(f"  F1-Score: {f1_score(y_test, y_pred_svm, average='weighted'):.4f}")

3.2 BERT (Transformers)

from transformers import AutoTokenizer, AutoModelForSequenceClassification
import torch

print(f"\n=== BERT (TRANSFORMERS) ===")

# Загрузка предобученной модели
model_name = 'distilbert-base-uncased-finetuned-sst-2-english'
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSequenceClassification.from_pretrained(model_name)

# Примеры
test_texts = [
    "This product is amazing! I love it.",
    "Terrible quality, waste of money.",
    "It's okay, nothing special."
]

print(f"\nПримеры BERT прогнозов:")
for text in test_texts:
    inputs = tokenizer(text, return_tensors='pt')
    outputs = model(**inputs)
    logits = outputs.logits
    predicted_class = logits.argmax().item()
    sentiment = ['negative', 'positive'][predicted_class]
    confidence = torch.softmax(logits, dim=1).max().item()
    print(f"  '{text}' → {sentiment} ({confidence:.2%})")

print(f"""
=== FINE-TUNING BERT НА СВОИХ ДАННЫХ ===

Шаги:
1. Подготовить данные (текст + метка)
2. Токенизировать с помощью tokenizer
3. Создать DataLoader
4. Обучить модель с small learning rate (2e-5)
5. Сохранить fine-tuned модель

Время обучения: ~30 минут на GPU для 10k примеров
Точность: обычно 85-95% для 3-class sentiment
""")

3.3 Сравнение подходов

МетодБоW+LRTF-IDF+LGBWord2Vec+NNBERT
Скорость обученияОчень быстроБыстроСреднееМедленно
Точность75%82%80%90%+
Требуемые данные10050010001000+
Требуемо GPUНетНетДаДа
ИнтерпретацияОтличнаяХорошаяСредняяПлохая

Рекомендация: Начните с TF-IDF + LightGBM (быстро и точно), потом fine-tune BERT если нужна еще точность.

4. Оценка качества

from sklearn.metrics import (
    confusion_matrix,
    classification_report,
    roc_auc_score,
    roc_curve
)
import matplotlib.pyplot as plt
import seaborn as sns

# === CONFUSION MATRIX ===

cm = confusion_matrix(y_test, y_pred_lgb)

fig, ax = plt.subplots(figsize=(8, 6))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', ax=ax,
            xticklabels=['negative', 'neutral', 'positive'],
            yticklabels=['negative', 'neutral', 'positive'])
ax.set_title('Confusion Matrix')
ax.set_ylabel('True Label')
ax.set_xlabel('Predicted Label')
plt.tight_layout()
plt.savefig('confusion_matrix.png')

# === CLASSIFICATION REPORT ===

print(f"\n=== CLASSIFICATION REPORT ===")
print(classification_report(
    y_test, y_pred_lgb,
    target_names=['negative', 'neutral', 'positive']
))

# === МЕТРИКИ ПО КЛАССАМ ===

from sklearn.metrics import precision_recall_fscore_support

precision, recall, f1, _ = precision_recall_fscore_support(
    y_test, y_pred_lgb, labels=[0, 1, 2]
)

for i, label in enumerate(['negative', 'neutral', 'positive']):
    print(f"\n{label.upper()}:")
    print(f"  Precision: {precision[i]:.4f} (из предсказанных, сколько верно)")
    print(f"  Recall: {recall[i]:.4f} (из реальных, сколько поймали)")
    print(f"  F1-Score: {f1[i]:.4f} (гармоническое среднее)")

print(f"""
=== ИНТЕРПРЕТАЦИЯ МЕТРИК ===

Accuracy: (TP + TN) / Total
- Доля верных предсказаний
- Может быть обманчива при дисбалансе классов
- Целевое значение: > 85%

Precision: TP / (TP + FP)
- Из всех положительных предсказаний, сколько верны
- Важна когда дорого false positive (например, spam)
- Целевое значение: > 80%

Recall: TP / (TP + FN)
- Из всех реальных положительных, сколько найти
- Важна когда дорого false negative (упустить положительный)
- Целевое значение: > 80%

F1-Score: 2 * (Precision * Recall) / (Precision + Recall)
- Гармоническое среднее Precision и Recall
- Лучше использовать при дисбалансе классов
- Целевое значение: > 0.80
""")
NLP: Классификация текстов | PrepBro