Занятие 1

Лабораторная 1

In [ ]:
# Раскомментируйте при первом запуске:
!pip install nltk pymorphy3 spacy numpy seaborn scikit-learn
!pip install --upgrade numpy pandas scipy matplotlib Jinja2
!python -m spacy download ru_core_news_sm
In [10]:
import nltk
import spacy
import re
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
import numpy as np
from nltk.corpus import stopwords
from collections import Counter
import pymorphy3
In [12]:
# Загрузка ресурсов NLTK
nltk.download('punkt_tab', quiet=True)
nltk.download('stopwords', quiet=True)

# Инициализация инструментов
morph = pymorphy3.MorphAnalyzer()

nlp_spacy = spacy.load("ru_core_news_sm")
russian_stopwords = stopwords.words("russian")

# Настройка стиля визуализаций
sns.set(style="whitegrid", font_scale=1.3)
plt.rcParams['font.family'] = 'DejaVu Sans'  # Поддержка кириллицы в графиках
plt.rcParams['figure.figsize'] = (12, 6)

Часть 1

1. Примеры текстов для обработки

In [ ]:
texts = [
    "Привет! Как твои дела? Я работаю над проектом по машинному обучению.",
    "Вчера я работал весь день, а сегодня буду работать ещё усерднее.",
    "Кошки и собаки — лучшие домашние животные. Кошка спит на диване.",
    "Apple основана Стивом Джобсом в 1976 году в Калифорнии.",
    "Этот фильм ужасный! Полный провал и разочарование.",
    "Прекрасная погода сегодня. Идеально для прогулки в парке!",
    "NLP — это классно! Машинное обучение — это мощно!",
    "Завтра я куплю кофе и чай в магазине неподалёку."
]

print("Исходные тексты для обработки:\n")
for i, text in enumerate(texts, 1):
    print(f"{i}. {text}")

2. Токенизация: сравнение NLTK vs spaCy

In [ ]:
# Пример текста
sample_text = texts[5]
print(f"\nИсходный текст:\n«{sample_text}»\n")

# NLTK токенизация
tokens_nltk = nltk.word_tokenize(sample_text, language='russian')
print(f"NLTK tokens ({len(tokens_nltk)} шт.):\n{tokens_nltk}\n")

# spaCy токенизация
doc_spacy = nlp_spacy(sample_text)
tokens_spacy = [token.text for token in doc_spacy]
print(f"spaCy tokens ({len(tokens_spacy)} шт.):\n{tokens_spacy}\n")

Задача 1

Существует ли какая-то разница между NLTK и spaCy с точки зрения представленных текстов?

In [ ]:

3. Полный пайплайн предобработки текста

In [ ]:
def preprocess_text_nltk(text, remove_stopwords=True, lemmatize=True):
    """
    Полная предобработка текста с использованием NLTK + pymorphy2

    Args:
        text: исходный текст
        remove_stopwords: удалять стоп-слова (по умолчанию True)
        lemmatize: выполнять лемматизацию (по умолчанию True)

    Returns:
        tokens: список обработанных токенов
    """
    # 1. Приведение к нижнему регистру
    text = text.lower()

    # 2. Токенизация
    tokens = nltk.word_tokenize(text, language='russian')

    # 3. Удаление пунктуации и не-буквенных символов
    tokens = [token for token in tokens if token.isalpha()]

    # 4. Удаление стоп-слов
    if remove_stopwords:
        tokens = [token for token in tokens if token not in russian_stopwords]

    # 5. Лемматизация
    if lemmatize:
        tokens = [morph.parse(token)[0].normal_form for token in tokens]

    return tokens


def preprocess_text_spacy(text, remove_stopwords=True, lemmatize=True):
    """
    Полная предобработка текста с использованием spaCy

    Args:
        text: исходный текст
        remove_stopwords: удалять стоп-слова (по умолчанию True)
        lemmatize: использовать леммы из spaCy (по умолчанию True)

    Returns:
        tokens: список обработанных токенов
    """
    # Обработка через spaCy
    doc = nlp_spacy(text.lower())

    tokens = []
    for token in doc:
        # Пропускаем стоп-слова, пунктуацию, пробелы
        if token.is_punct or token.is_space:
            continue
        if remove_stopwords and token.text in russian_stopwords:
            continue

        # Выбираем лемму или исходную форму
        token_text = token.lemma_ if lemmatize else token.text
        tokens.append(token_text)

    return tokens


sample_idx = 0
original = texts[sample_idx]
print(f"\nИсходный текст #{sample_idx+1}:\n«{original}»\n")

# Обработка через NLTK + pymorphy3
processed_nltk = preprocess_text_nltk(original)
print(f"Результат (NLTK + pymorphy3):\n{processed_nltk}\n")

# Обработка через spaCy
processed_spacy = preprocess_text_spacy(original)
print(f"Результат (spaCy):\n{processed_spacy}\n")

# Сравнение
print(f"Статистика:")
print(f"  • Исходная длина: {len(original.split())} слов")
print(f"  • После обработки (NLTK): {len(processed_nltk)} лемм")
print(f"  • После обработки (spaCy): {len(processed_spacy)} лемм")

Задача 2

Существует ли какая-то разница между NLTK и spaCy с точки зрения представленных текстов?

In [ ]:

4. Обработка всего корпуса и анализ

In [ ]:
# Обработка всех текстов
processed_corpus = []
original_lengths = []
processed_lengths = []

for text in texts:
    # Сохраняем длину исходного текста (по словам)
    original_lengths.append(len(text.split()))

    # Обработка текста
    tokens = preprocess_text_nltk(text)
    processed_corpus.append(tokens)
    processed_lengths.append(len(tokens))

# Создание DataFrame для анализа
df_stats = pd.DataFrame({
    'text': texts,
    'original_len': original_lengths,
    'processed_len': processed_lengths,
    'reduction_pct': [(1 - pl/ol)*100 for ol, pl in zip(original_lengths, processed_lengths)]
})

print("\nСтатистика обработки корпуса:")
display(df_stats[['original_len', 'processed_len', 'reduction_pct']].round(1))

print(f"\n📊 Среднее сокращение длины после предобработки: {df_stats['reduction_pct'].mean():.1f}%")
print("   (удалены стоп-слова, пунктуация и приведены формы к леммам)")

5. Визуализация распределения длин текстов

In [8]:
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# График 1: Сравнение исходных и обработанных длин
x = np.arange(len(texts))
width = 0.35

axes[0].bar(x - width/2, df_stats['original_len'], width, label='Исходная длина', alpha=0.8, color='#4e79a7')
axes[0].bar(x + width/2, df_stats['processed_len'], width, label='После обработки', alpha=0.8, color='#f28e2b')
axes[0].set_xlabel('Номер текста')
axes[0].set_ylabel('Количество слов/лемм')
axes[0].set_title('Сравнение длин до и после предобработки')
axes[0].legend()
axes[0].grid(axis='y', alpha=0.3)

# График 2: Распределение длин обработанных текстов
sns.histplot(processed_lengths, bins=range(min(processed_lengths), max(processed_lengths)+2),
             kde=True, color='#59a14f', ax=axes[1], edgecolor='black')
axes[1].axvline(np.mean(processed_lengths), color='red', linestyle='--',
                label=f'Среднее: {np.mean(processed_lengths):.1f}')
axes[1].set_xlabel('Длина текста (количество лемм)')
axes[1].set_ylabel('Частота')
axes[1].set_title('Распределение длин обработанных текстов')
axes[1].legend()
axes[1].grid(axis='y', alpha=0.3)

plt.tight_layout()
plt.savefig('text_lengths_distribution.png', dpi=150, bbox_inches='tight')
plt.show()

Часть 2 (Классические методы векторизации)

In [3]:
import numpy as np
import pandas as pd
from pandas.io.formats import style
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
from sklearn.preprocessing import normalize
import nltk
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize

from sklearn.model_selection import train_test_split
In [ ]:
# Загрузка стоп-слов для русского языка
nltk.download('stopwords', quiet=True)
russian_stopwords = stopwords.words('russian')

# Настройка визуализаций
sns.set(style="whitegrid", font_scale=1.2)
plt.rcParams['font.family'] = 'DejaVu Sans'
plt.rcParams['figure.figsize'] = (12, 6)

mini_corpus = [
    "кофе вкусный ароматный",
    "кофе горький невкусный",
    "чай вкусный травяной"
]

vocab = sorted(set(word for text in mini_corpus for word in text.split()))
print(f"Словарь (|V| = {len(vocab)}): {vocab}\n")

# One-Hot Encoding (для одного слова)
print("→ One-Hot Encoding для слова 'кофе':")
one_hot_coffee = np.zeros(len(vocab))
one_hot_coffee[vocab.index('кофе')] = 1
print(f"  Вектор: {one_hot_coffee.astype(int)}")
print("  Минусы: огромные разреженные векторы, нет семантики\n")

# Bag of Words
print("→ Bag of Words для корпуса:")
vectorizer_bow = CountVectorizer(vocabulary=vocab, lowercase=False)
X_bow = vectorizer_bow.fit_transform(mini_corpus)
df_bow = pd.DataFrame(X_bow.toarray(), columns=vocab, index=[f"Док {i+1}" for i in range(3)])
print(df_bow, "\n")

# TF-IDF
print("→ TF-IDF для корпуса:")
vectorizer_tfidf = TfidfVectorizer(vocabulary=vocab, lowercase=False)
X_tfidf = vectorizer_tfidf.fit_transform(mini_corpus)
df_tfidf = pd.DataFrame(X_tfidf.toarray(), columns=vocab, index=[f"Док {i+1}" for i in range(3)])
print(df_tfidf.round(3), "\n")

print("💡 Ключевое отличие:")
print("  • BoW: слово 'кофе' имеет одинаковый вес в Док 1 и Док 2 (1)")
print("  • TF-IDF: 'кофе' получает меньший вес, так как встречается в 2 из 3 документов")
print("  • Слово 'травяной' получает максимальный вес — уникально для Док 3!")
print()
In [ ]:
# Реалистичный корпус отзывов на русском языке
positive_reviews = [
    "Отличный фильм, очень понравился актёрский состав и сюжет захватывающий",
    "Прекрасное обслуживание в ресторане, вкусная еда и уютная атмосфера",
    "Замечательный отель, чистые номера, дружелюбный персонал, рекомендую",
    "Удивительный концерт, потрясающая энергетика и профессиональное исполнение",
    "Восхитительный кофе в этой кофейне, ароматный и насыщенный вкус"
]

negative_reviews = [
    "Ужасный сервис, грубый персонал и долгое ожидание в очереди",
    "Кошмарный фильм, скучный сюжет и отвратительная игра актёров",
    "Плохое качество еды в ресторане, холодные блюда и завышенные цены",
    "Разочарование полное, грязный номер и шумные соседи всю ночь",
    "Невкусный кофе, горький привкус и крошится печенье к нему"
]

# Объединяем корпус и создаём метки
corpus = positive_reviews + negative_reviews
labels = ['позитив'] * 5 + ['негатив'] * 5

# Создаём DataFrame для удобства
df_corpus = pd.DataFrame({
    'text': corpus,
    'sentiment': labels,
    'id': range(1, 11)
})

print("\nКорпус отзывов:")
display(df_corpus[['id', 'sentiment', 'text']].style.set_properties(**{'text-align': 'left'}))
print(f"\nСтатистика корпуса: {len(corpus)} документов ({df_corpus['sentiment'].value_counts().to_dict()})")
print()
In [ ]:
# Векторизация с настройками для русского языка
vectorizer = TfidfVectorizer(
    tokenizer=lambda x: nltk.word_tokenize(x.lower(), language='russian'),
    stop_words=russian_stopwords,
    min_df=1,      # минимальная частота документа (включаем все слова)
    max_df=0.9,    # исключаем слова, встречающиеся в 90%+ документов
    token_pattern=None  # отключаем стандартный паттерн, используем tokenizer
)

# Преобразуем корпус в матрицу TF-IDF
X_tfidf_full = vectorizer.fit_transform(corpus)
feature_names = vectorizer.get_feature_names_out()

print(f"✅ TF-IDF матрица построена!")
print(f"   Размерность: {X_tfidf_full.shape[0]} документов × {X_tfidf_full.shape[1]} уникальных слов")
print(f"   Пример признаков: {feature_names[:10].tolist()} ...\n")

# Создаём DataFrame для удобного анализа
df_tfidf = pd.DataFrame(
    X_tfidf_full.toarray(),
    columns=feature_names,
    index=[f"{labels[i]}_{i+1}" for i in range(len(corpus))]
)

# Округляем для читаемости
print("Фрагмент TF-IDF матрицы (первые 8 слов):")
display(df_tfidf.iloc[:, :8].round(3))
print()
In [ ]:
# Разделяем матрицу по категориям
pos_mask = np.array(labels) == 'позитив'
neg_mask = np.array(labels) == 'негатив'

# Средние TF-IDF веса для каждой категории
pos_avg = X_tfidf_full[pos_mask].mean(axis=0).A1  # .A1 преобразует matrix в array
neg_avg = X_tfidf_full[neg_mask].mean(axis=0).A1

# Создаём DataFrame для анализа
df_analysis = pd.DataFrame({
    'word': feature_names,
    'tfidf_pos': pos_avg,
    'tfidf_neg': neg_avg,
    'diff': pos_avg - neg_avg  # разница показывает дискриминативную силу
})

# Топ-5 слов для позитивных отзывов (высокий вес в позитиве)
top_pos = df_analysis.nlargest(5, 'tfidf_pos')[['word', 'tfidf_pos', 'tfidf_neg', 'diff']]
top_pos.columns = ['Слово', 'TF-IDF (позитив)', 'TF-IDF (негатив)', 'Разница']

# Топ-5 слов для негативных отзывов (высокий вес в негативе)
top_neg = df_analysis.nlargest(5, 'tfidf_neg')[['word', 'tfidf_pos', 'tfidf_neg', 'diff']]
top_neg.columns = ['Слово', 'TF-IDF (позитив)', 'TF-IDF (негатив)', 'Разница']

print("ТОП-5 слов, характерных для ПОЗИТИВНЫХ отзывов:")
display(top_pos.round(3).style.background_gradient(cmap='Greens', subset=['TF-IDF (позитив)']))

print("ТОП-5 слов, характерных для НЕГАТИВНЫХ отзывов:")
display(top_neg.round(3).style.background_gradient(cmap='Reds', subset=['TF-IDF (негатив)']))

Задача 3

Попали ли в том какие нибудь неинформативные слова? Почему?

Задача 4 (классификация текстов с помощью TF-IDF)

In [8]:
# Создаём сбалансированный датасет из 100 отзывов (50 позитивных + 50 негативных)
# Отзывы имитируют реальные тексты с разнообразной лексикой и структурой

positive_reviews = [
    "Отличный товар, полностью соответствует описанию! Быстрая доставка, упаковка целая.",
    "Очень доволен покупкой. Качество на высоте, рекомендую всем друзьям.",
    "Прекрасный сервис! Консультант помог выбрать идеальный вариант под мои задачи.",
    "Фильм просто шедевр! Актёрская игра потрясающая, сюжет держит в напряжении до конца.",
    "Уютный ресторан с потрясающей атмосферой. Еда свежая, официанты внимательные.",
    "Отель превзошёл все ожидания: чистота, комфортная кровать, вежливый персонал.",
    "Смартфон работает безупречно уже полгода. Батарея держится целый день, камера супер.",
    "Курс оказался невероятно полезным. Материал структурирован, преподаватель объясняет понятно.",
    "Кофе в этой кофейне — лучший в городе! Ароматный, насыщенный, бариста настоящий профессионал.",
    "Заказ пришёл раньше срока. Товар качественный, продавец общительный и ответственный.",
    "Восхитительный спектакль! Эмоции переполняют ещё несколько дней после просмотра.",
    "Мастер в автосервисе — настоящий профессионал. Объяснил все нюансы, починил быстро.",
    "Книга захватила с первой страницы. Персонажи живые, сюжет не предсказуемый.",
    "Доставка еды оперативная, блюда горячие и свежие. Особенно понравилась пицца!",
    "Фитнес-клуб оборудован современными тренажёрами. Тренер подобрал идеальную программу.",
    "Отличное соотношение цены и качества. Товар прослужит долго, не жалко потраченных денег.",
    "Приложение очень удобное и интуитивно понятное. Все функции работают стабильно.",
    "Врач внимательный и компетентный. Выслушал, провёл диагностику, назначил эффективное лечение.",
    "Платье сидит идеально по фигуре. Ткань приятная к телу, швы аккуратные.",
    "Поездка прошла на высшем уровне! Гид интересно рассказывал, маршруты продуманы отлично.",
    "Батарея ноутбука держится 8 часов — идеально для работы в дороге.",
    "Дети в восторге от игрушки! Качественные материалы, яркие цвета, безопасно.",
    "Салон красоты: уютно, чисто, мастера работают аккуратно и профессионально.",
    "Доставили цветы точно в срок. Свежие бутоны, красивая упаковка, получатель в восторге.",
    "Ресторан с панорамным видом на город. Романтичная атмосфера, идеально для свидания.",
    "Тренер по йоге терпеливый и внимательный.",
    "Смарт-часы точно отслеживают пульс и шаги. Дизайн стильный, батарея держится неделю.",
    "Косметика не вызвала аллергии, кожа стала мягче и сияющей после первого применения.",
    "Такси приехало за 3 минуты. Водитель вежливый, машина чистая, цена адекватная.",
    "Онлайн-курс по программированию структурирован логично. Практические задания полезные.",
    "Массаж снял напряжение в спине после тяжёлой рабочей недели. Мастер знает своё дело.",
    "Беспроводные наушники не выпадают при беге, звук чистый, шумоподавление работает отлично.",
    "Детский лагерь организован на высшем уровне. Программа насыщенная, педагоги ответственные.",
    "Стиральная машина работает тихо, бельё выходит чистым и свежим.",
    "Концерт любимой группы превзошёл все ожидания! Заряд позитива на месяц вперёд.",
    "Очки пришли с правильными диоптриями. Оправа лёгкая, не давит на переносицу.",
    "Доставка мебели включала сборку. Мастера приехали вовремя, собрали качественно.",
    "Парфюм держится весь день, ноты раскрываются постепенно. Комплименты от коллег обеспечены!",
    "Сантехник устранил засор за 15 минут. Цены прозрачные, без скрытых платежей.",
    "Отели в путешествии были подобраны идеально: удобное расположение, чистота, хороший завтрак.",
    "Электросамокат легко складывается, батареи хватает на 25 км. Отличная альтернатива метро.",
    "Книжный магазин с уютной атмосферой. Персонал помог найти редкое издание.",
    "Фотограф сделал потрясающие снимки на свадьбе. Каждый кадр передаёт эмоции момента.",
    "Доставка продуктов: всё свежее, упаковано аккуратно, сроки соблюдены.",
    "Курс по инвестициям изменил моё отношение к финансам. Практические советы реально работают.",
    "Рюкзак вместительный, но при этом лёгкий. Отлично подходит для ежедневных поездок.",
    "Спа-процедуры расслабили полностью. Атмосфера уюта, ароматерапия, тёплые полотенца.",
    "Онлайн-репетитор помог ребёнку подготовиться к экзамену. Оценка выросла с тройки до пятёрки!",
    "Беспроводная мышь эргономичная, не устаёт рука даже после 8 часов работы.",
    "Кофемашина проста в управлении. Капучино получается с густой пенкой, как в кофейне.",
    "Путешествие с этим туроператором — сплошное удовольствие. Никакого стресса и накладок."
]

negative_reviews = [
    "Ужасное качество товара! Сломался через неделю использования, деньги на ветер.",
    "Доставка задержалась на 10 дней без уведомления. Упаковка помята, товар повреждён.",
    "Грубый персонал в магазине. На вопросы отвечали с раздражением, возвращать товар пришлось через суд.",
    "Фильм скучный и предсказуемый. Актёры играют без эмоций, сюжет нелогичный.",
    "Ресторан с антисанитарией: грязные столы, мухи летают над едой. Больше никогда не приду!",
    "Отель оказался полным разочарованием: в номере плесень, соседи шумят всю ночь, завтрак скудный.",
    "Смартфон тормозит даже при открытии сообщений. Батарея садится за 3 часа, перегревается.",
    "Курс поверхностный, материал изложен сумбурно. Преподаватель не отвечает на вопросы.",
    "Кофе горький и водянистый. Бариста невнимательный, перепутал заказ.",
    "Продавец скрыл дефекты товара. Пришлось тратить время на возврат и споры.",
    "Спектакль унылый и затянутый. Актёры забывали реплики, публика засыпала.",
    "Мастер в сервисе сделал всё хуже. Теперь машина шумит ещё сильнее, а платить пришлось в полном объёме.",
    "Книга написана корявым языком, сюжет примитивный. Пожалел о потраченном времени.",
    "Доставка еды заняла 2 часа. Блюда приехали холодными, половина заказа отсутствовала.",
    "В фитнес-клубе старые тренажёры, сломанные. Тренер только деньги берёт, а заниматься не хочет.",
    "Товар не соответствует фото на сайте. Цвет другой, размер не совпадает, качество низкое.",
    "Приложение постоянно вылетает. Интерфейс неудобный, функции работают с ошибками.",
    "Врач поверхностно осмотрел, выписал неподходящие лекарства. Симптомы усилились.",
    "Платье село после первой стирки. Ткань линяет, швы расползлись.",
    "Экскурсия превратилась в кошмар: гид опоздал на час, маршрут сократил вдвое.",
    "Батарея ноутбука держится меньше часа. Новый аккумулятор стоит как половина устройства.",
    "Игрушка сделана из дешёвого пластика, ребёнок поранился об острые края.",
    "В салоне красоты не соблюдают гигиену: один и тот же инструмент для всех клиентов.",
    "Цветы завяли через сутки. Бутоны были подпорченные ещё при доставке.",
    "Ресторан с завышенными ценами. Порции микроскопические, вкус посредственный.",
    "Тренер по йоге кричал на новичков. Атмосфера напряжённая, никто не чувствует себя комфортно.",
    "Смарт-часы показывают неверный пульс. Заряд держится меньше суток, несмотря на заявленные 7 дней.",
    "Косметика вызвала сильную аллергию. Кожа покраснела, появилось раздражение.",
    "Такси опоздало на 40 минут. Водитель грубил, маршрут выбрал самый длинный.",
    "Онлайн-курс — просто пересказ бесплатных материалов из интернета. Обман!",
    "Массаж был болезненным и некачественным. Мастер не знал анатомии, после сеанса болело сильнее.",
    "Наушники постоянно отключаются от телефона. Звук хриплый, микрофон не передаёт голос.",
    "В детском лагере антисанитария: грязные постельные принадлежности, питание плохое.",
    "Стиральная машина гремит как трактор. После стирки бельё мокрое, приходится перестирывать.",
    "Концерт отменили за час до начала без объяснения причин. Деньги не вернули.",
    "Очки с неправильными диоптриями. Головная боль после 10 минут ношения.",
    "Мебель доставили с царапинами и сколами. Сборщики повредили дверной косяк.",
    "Парфюм оказался подделкой: резкий химический запах, держится меньше часа.",
    "Сантехник приехал пьяным, ничего не починил. Взял деньги и ушёл.",
    "Туроператор обманул: отели не соответствовали описанию, перелёты задерживали.",
    "Электросамокат сломался после первой же поездки по лёгкому бездорожью.",
    "В книжном магазине нет системы скидок для постоянных клиентов. Цены завышены.",
    "Фотограф прислал смазанные и тёмные снимки. Пересъёмку отказался делать.",
    "Продукты приехали с истёкшим сроком годности. Мясо имело странный запах.",
    "Курс по инвестициям — развод. Советы привели к убыткам, автор скрылся после оплаты.",
    "Рюкзак порвался через неделю. Молния заклинила, лямки оторвались.",
    "В спа-салоне грязные простыни, массажист опаздывал. Полный провал!",
    "Репетитор не знал предмет. Ребёнок получил двойку, время и деньги потрачены впустую.",
    "Мышь начала двоить клики через месяц. Гарантию отказались оформлять.",
    "Кофемашина протекает, кофе разбрызгивается по всей кухне. Возврат осложнили."
]

# Создаём датафрейм
texts = positive_reviews + negative_reviews
labels = [1] * len(positive_reviews) + [0] * len(negative_reviews)  # 1 = позитив, 0 = негатив

df = pd.DataFrame({
    'text': texts,
    'label': labels,
    'sentiment': ['позитив' if l == 1 else 'негатив' for l in labels]
})

# Перемешиваем датасет для объективности
df = df.sample(frac=1, random_state=42).reset_index(drop=True)
In [ ]:
def preprocess_text(text):
    """Полная предобработка текста: токенизация + лемматизация + фильтрация"""
    # Приведение к нижнему регистру
    text = text.lower()

    # Удаление цифр и специальных символов (оставляем только буквы и пробелы)
    text = re.sub(r'[^а-яёa-z\s]', ' ', text)

    # Токенизация
    tokens = word_tokenize(text, language='russian')

    # Фильтрация: только буквы, длина > 2, не стоп-слово
    tokens = [
        token for token in tokens
        if token.isalpha()
        and len(token) > 2
        and token not in russian_stopwords
    ]
    tokens = [morph.parse(token)[0].normal_form for token in tokens]

    return ' '.join(tokens)

# Применяем предобработку
print("🔄 Применяем предобработку ко всем отзывам...")
df['processed_text'] = df['text'].apply(preprocess_text)

# Демонстрация результата
print("\nПример предобработки:")
original = df.loc[0, 'text']
processed = df.loc[0, 'processed_text']
print(f"Исходный текст:\n   \"{original[:100]}...\"")
print(f"\nПосле обработки:\n   \"{processed}\"")
print()
In [ ]:
# Настройки векторизатора
vectorizer = TfidfVectorizer(
    max_features=500,      # Ограничиваем словарь 500 наиболее информативных слов
    min_df=2,              # Игнорируем слова, встречающиеся менее чем в 2 документах
    max_df=0.8,            # Игнорируем слова, встречающиеся в >80% документов (частые слова)
    ngram_range=(1, 2),    # Используем униграммы и биграммы ("отличный фильм", "не работает")
    token_pattern=r'(?u)\b[а-яё]{3,}\b'  # Только русские слова длиной >=3
)

# Разделение на обучающую и тестовую выборки (80/20)
X_train, X_test, y_train, y_test = train_test_split(
    df['processed_text'],
    df['label'],
    test_size=0.2,
    random_state=42,
    stratify=df['label']  # Сохраняем баланс классов
)

# Обучение векторизатора и преобразование текстов
X_train_tfidf = vectorizer.fit_transform(X_train)
X_test_tfidf = vectorizer.transform(X_test)

print(f"✅ TF-IDF матрица построена:")
print(f"   • Обучающая выборка: {X_train_tfidf.shape[0]} документов × {X_train_tfidf.shape[1]} признаков")
print(f"   • Тестовая выборка:  {X_test_tfidf.shape[0]} документов × {X_test_tfidf.shape[1]} признаков")
print(f"   • Пример признаков: {vectorizer.get_feature_names_out()[:15].tolist()} ...")
  1. vectorizer.fit_transform(X_train_raw)

Этот вызов делает два действия подряд:

  • fit(X_train_raw) — обучение векторизатора

Анализирует обучающие тексты. Строит словарь: список всех уникальных слов (токенов), которые будут использоваться как признаки. Вычисляет IDF (Inverse Document Frequency) для каждого слова: Насколько редко слово встречается во всём обучающем корпусе? 📌 Результат: векторизатор «запоминает»: Какие слова использовать (словарь), Какой IDF-вес у каждого слова.

  • transform(X_train_raw) — преобразование

Преобразует каждый текст из X_train_raw в вектор TF-IDF на основе построенного словаря и IDF.

Результат: Разреженная матрица формы (n_samples, n_features), где: n_samples = количество документов в обучающей выборке, n_features = размер словаря. Каждая ячейка содержит TF-IDF вес слова в документе.

  1. vectorizer.transform(X_test_raw)

Берёт тот же самый словарь и те же IDF-веса, что были построены на обучающей выборке. Для каждого документа в X_test_raw:

  • Извлекает только те слова, которые присутствуют в обучающем словаре.
  • Игнорирует все остальные слова (даже если они важны!).
  • Вычисляет TF-IDF с использованием тех же IDF-значений, что и для обучающих данных.
  • Формирует вектор той же размерности, что и у X_train.

Обучите и протестируйте какую-нибудь модель машинного обучения. Оцените полученные результаты.

In [ ]:
# ВАААААААААААШ КОД

Часть 3: работа с большим датасетов

In [15]:
from sklearn.datasets import fetch_20newsgroups
newsgroups_train = fetch_20newsgroups(subset='train')

Немного о датасете:

  1. Общее количество документов: 18 828.
  2. Размер обучающей выборки: 11 314 документа.
  3. Размер тестовой выборки: 7 532 документа.
  4. Средняя длина документа: 200–300 слов.
  5. Уникальные слова: около 100 000
In [ ]:
# Название классов
newsgroups_train.target_names
In [ ]:
# кол-во объектов в обучающей выборке
newsgroups_train.filenames.shape

Выберем 4 классов из 20 (с ними и будем работать).

In [ ]:
categories = ['alt.atheism', 'soc.religion.christian', 'comp.graphics', 'sci.med']
newsgroups_train = fetch_20newsgroups(subset='train',
                                      categories=categories)

print(newsgroups_train.data[2])
In [18]:
# === 1. Импорты ===
import nltk
import string
import numpy as np
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize
from nltk.stem import WordNetLemmatizer
from sklearn.datasets import fetch_20newsgroups
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, classification_report
In [19]:
# === 2. Загрузка языковых ресурсов NLTK ===
nltk.download('punkt', quiet=True)        # токенизатор
nltk.download('stopwords', quiet=True)    # список стоп-слов
nltk.download('wordnet', quiet=True)      # словарь для лемматизации
nltk.download('omw-1.4', quiet=True)      # мультиязычная поддержка WordNet
Out[19]:
True
In [31]:
# === 3. Настройка компонентов предобработки ===
stop_words = set(stopwords.words('english'))  # преобразуем в set для быстрого поиска
lemmatizer = WordNetLemmatizer()
punctuation_translator = str.maketrans('', '', string.punctuation)
In [33]:
def custom_tokenizer(text):
    """Преобразует текст в список лемматизированных слов без шума."""
    # Удаляем всю пунктуацию (быстро и надёжно)
    text = text.translate(punctuation_translator)

    # Разбиваем на слова и приводим к нижнему регистру
    tokens = word_tokenize(text.lower())

    # Фильтруем:
    # - только буквы (isalpha),
    # - длина > 2 (чтобы отсечь "a", "an", "it" и т.п.),
    # - не стоп-слово
    tokens = [
        token for token in tokens
        if token.isalpha() and len(token) > 2 and token not in stop_words
    ]

    # Лемматизируем каждое слово (приводим к нормальной форме: "cats" → "cat")
    return [lemmatizer.lemmatize(token) for token in tokens]
In [34]:
# === 4. Загрузка данных ===
categories = ['alt.atheism', 'soc.religion.christian', 'comp.graphics', 'sci.med']
newsgroups_train = fetch_20newsgroups(subset='train', categories=categories, remove=('headers', 'footers', 'quotes'))
newsgroups_test = fetch_20newsgroups(subset='test', categories=categories, remove=('headers', 'footers', 'quotes'))

X_train_raw = newsgroups_train.data  # исходные тексты (обучающая выборка)
y_train = newsgroups_train.target    # метки классов (целевая переменная)
X_test_raw = newsgroups_test.data    # исходные тексты (тестовая выборка)
y_test = newsgroups_test.target      # метки классов (для оценки)
In [35]:
# === 5. Векторизация текстов с помощью TF-IDF ===
vectorizer = TfidfVectorizer(
    tokenizer=custom_tokenizer,  # используем нашу функцию вместо стандартной
    lowercase=False,             # уже обрабатываем регистр внутри tokenizer
    token_pattern=None,          # отключаем встроенный паттерн (иначе предупреждение)
    max_features=10000,          # ограничиваем словарь до 10k самых важных слов
    min_df=2,                    # игнорируем слова, встретившиеся менее чем в 2 документах
    max_df=0.9                   # игнорируем слова, встретившиеся в >90% документов
)

# Обучаем векторизатор ТОЛЬКО на обучающей выборке
X_train = vectorizer.fit_transform(X_train_raw)

# Применяем ТОТ ЖЕ векторизатор к тестовой выборке (без fit!)
X_test = vectorizer.transform(X_test_raw)
In [ ]:
# === 6. Обучение модели ===
clf = LogisticRegression(max_iter=1000, random_state=42)
clf.fit(X_train, y_train)
In [ ]:
# === 7. Оценка качества ===
y_pred = clf.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)

print("Подробный отчёт по классам:")
print(classification_report(y_test, y_pred, target_names=newsgroups_test.target_names))

Задача 5

  1. Используйте другие модели машинного обучения для решения данной задачи.
  2. Как влияют значения min_df и max_df на точность модели?
  3. Как ещё можно улучшить точность работы алгоритма?
In [ ]:
# Сгенерированный Вами код

Часть 4 (Эмбеддинги)

In [ ]:
! pip install gensim
In [28]:
# Загрузка предобученных эмбеддингов

import gensim.downloader as api
embeddings_pretrained = api.load('glove-twitter-25')
  • Словарь содержит ~1.2 млн слов.
  • Векторы фиксированы — размера 25
In [ ]:
vector_size = embeddings_pretrained.vector_size
print("Размерность вектора:", vector_size)

vocab_size = len(embeddings_pretrained)
print("Количество слов в словаре:", vocab_size)

# Эмбеддинг слова hello
vec = embeddings_pretrained['hello']  # → массив из 25 чисел

Задача 6

Расчитайте косинусное расстояние между двумя "близкими" и двумя "далёкими" словами.

In [ ]:
# Сгенерированный Вами код
In [ ]:
# Вновь загружаем данные из датасета выше
categories = ['alt.atheism', 'soc.religion.christian', 'comp.graphics', 'sci.med']

newsgroups_train = fetch_20newsgroups(
    subset='train',
    categories=categories,
    remove=('headers', 'footers', 'quotes')
)
newsgroups_test = fetch_20newsgroups(
    subset='test',
    categories=categories,
    remove=('headers', 'footers', 'quotes')
)

X_train_raw = newsgroups_train.data
y_train = newsgroups_train.target
X_test_raw = newsgroups_test.data
y_test = newsgroups_test.target

print(f"Обучающих документов: {len(X_train_raw)}")
print(f"Тестовых документов: {len(X_test_raw)}")
print(f"Категории: {newsgroups_train.target_names}")
In [24]:
def document_vector(text, embeddings_model):
    """
    Преобразует текст в вектор путём УСРЕДНЕНИЯ векторов его слов.
    """
    tokens = custom_tokenizer(text)
    # Оставляем только слова, известные модели
    known_tokens = [token for token in tokens if token in embeddings_model]

    if not known_tokens:
        # Если ни одно слово не найдено — нулевой вектор
        return np.zeros(embeddings_model.vector_size, dtype='float32')

    # Усредняем векторы
    word_vectors = [embeddings_model[token] for token in known_tokens]
    return np.mean(word_vectors, axis=0)
In [ ]:
# === 7. Векторизация всех текстов ===
print("\n🧮 Векторизуем обучающие и тестовые данные...")

# Обучающая выборка
X_train = np.array([document_vector(text, embeddings_pretrained) for text in X_train_raw])

# Тестовая выборка
X_test = np.array([document_vector(text, embeddings_pretrained) for text in X_test_raw])

print(f"✅ X_train.shape: {X_train.shape}")
print(f"✅ X_test.shape: {X_test.shape}")
In [ ]:
# === 8. Обучение и оценка модели ===
print("\n Обучаем логистическую регрессию...")
clf = LogisticRegression(max_iter=1000, random_state=42)
clf.fit(X_train, y_train)

# Предсказания
y_pred = clf.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)

print(f"\n Точность (Accuracy): {accuracy:.4f} ({accuracy * 100:.2f}%)")

# Подробный отчёт
print("\n Отчёт по классам:")
print(classification_report(
    y_test, y_pred,
    target_names=newsgroups_test.target_names
))

Задача 7

Мы получили плохой результат. Как его улучшить? Добейтесь weighted avg > 0.815.

In [ ]:
# Сгенерированный Вами код