ЛР4
Модели, метрики и подбор

Разобраться с нуля

Модели, метрики и подбор

Представление текста и классификатор это два разных шага. Сравниваем NB и SVC на одних и тех же статьях.

Классификатор
Программа, выбирающая метку по числам.
MultinomialNB
Наивный Байес: оценивает класс по частотам слов.
LinearSVC
Ищет линейные границы между классами.
Гиперпараметр
Настройка до обучения: размер словаря, n-граммы.
GridSearchCV
Перебор настроек с проверкой на нескольких частях train.
Pipeline
Цепочка векторизатор → модель, обучаемая внутри каждого CV-фолда.
Macro-F1
Средний F1 четырёх классов, каждый имеет одинаковый вес.
На пальцах. BoW и TF-IDF это формат конспекта. NB и SVC это два разных студента, которые учатся читать такой конспект.

Байес без страшных формул

Если слова gene и cell гораздо чаще встречались в биологии, новая статья с ними получит высокую оценку Life Sciences. «Наивный» значит, что слова условно считаются независимыми при фиксированном классе. В реальности они связаны, но метод всё равно полезен.

SVC

Представь статьи точками. SVC проводит границы с зазором между классами. При 15000 словах это 15000 координат, а не плоский рисунок. Для четырёх классов sklearn строит отдельные линейные решения.

Как читать classification_report

Precision: из названных биологией сколько правда биология? Recall: из всех биологических статей сколько нашли? F1 уравновешивает precision и recall. Support: сколько настоящих статей класса на тесте.

precision = TP/(TP+FP)
recall = TP/(TP+FN)
F1 = 2PR/(P+R)

Например: 80 из 100 найденных статей верны, а настоящих 120. Precision=0.8, recall=0.6667, F1≈0.7273. Это учебный расчёт, не метрика нашего прогона.

Confusion matrix

Строка: настоящий домен. Столбец: предсказанный. Диагональ это правильные ответы. Большое число вне диагонали показывает, какие области путаются.

Правильный GridSearch

2 размера словаря × 2 варианта n-грамм × 2 min_df = 8 настроек на пару векторизатор/модель. Три CV-фолда, всего 24 обучения на пару. Test не выбирает настройки.

Ловушка. Лучшее число на test нельзя использовать для бесконечного подбора. Итоговую конфигурацию выбирают по CV. Секунды в таблицах относятся к этому компьютеру.

Где Pipeline учит словарь при CV?

Код и построчный разбор

Код исходной ячейки 12: from sklearn.naive_bayes import MultinomialNB
from sklearn.naive_bayes import MultinomialNB
from sklearn.svm import LinearSVC

clf1 = MultinomialNB()
# Прямой решатель и больше итераций: на BoW стандартного лимита не хватало.
clf2 = LinearSVC(random_state=42, dual=False, max_iter=5000)
  1. 1: from sklearn.naive_bayes import MultinomialNB
    Подключает готовые инструменты; import сам по себе модель не обучает.
  2. 2: from sklearn.svm import LinearSVC
    Подключает готовые инструменты; import сам по себе модель не обучает.
  3. 4: clf1 = MultinomialNB()
    Вычисляет правую часть и сохраняет результат в clf1.
  4. 5: # Прямой решатель и больше итераций: на BoW стандартного лимита не хватало.
    Комментарий объясняет причину следующего действия.
  5. 6: clf2 = LinearSVC(random_state=42, dual=False, max_iter=5000)
    Вычисляет правую часть и сохраняет результат в clf2.
Код исходной ячейки 13: from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.metrics import classification_report, confusion_matrix, f1_score, accuracy_score
from sklearn.pipeline import Pipeline
from sklearn.base import clone
from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer

train_text, test_text, y_train, y_test = train_test_split(
    df['title_abstract_preprocessed'], df['domain'], test_size=0.2, random_state=42, stratify=df['domain'])
print('Train:', len(train_text), '| test:', len(test_text))
labels = sorted(df['domain'].unique())
results = []

def evaluate(name, vectorizer, report=True):
    start = perf_counter()
    X_train = vectorizer.fit_transform(train_text)
    X_test = vectorizer.transform(test_text)
    vector_time = perf_counter() - start
    for title, prototype in [('NB', clf1), ('SVC', clf2)]:
        model = clone(prototype)
        start = perf_counter()
        model.fit(X_train, y_train)
        fit_time = perf_counter() - start
        start = perf_counter()
        pred = model.predict(X_test)
        pred_time = perf_counter() - start
        results.append({'representation': name, 'model': title, 'macro_f1': f1_score(y_test, pred, average='macro'),
                        'accuracy': accuracy_score(y_test, pred), 'vector_s': vector_time, 'fit_s': fit_time, 'predict_s': pred_time})
        if report:
            print(name, title)
            print(classification_report(y_test, pred, labels=labels, zero_division=0, digits=3))
            plt.figure(figsize=(7, 5))
            sns.heatmap(confusion_matrix(y_test, pred, labels=labels), annot=True, fmt='d',
                        xticklabels=labels, yticklabels=labels, cmap='YlOrBr')
            plt.xlabel('Предсказанный домен'); plt.ylabel('Настоящий домен')
            plt.title(name + ' / ' + title)
            plt.xticks(rotation=25, ha='right'); plt.yticks(rotation=0)
            plt.tight_layout(); plt.show()
    return vectorizer
  1. 1: from sklearn.model_selection import train_test_split, GridSearchCV
    Подключает готовые инструменты; import сам по себе модель не обучает.
  2. 2: from sklearn.metrics import classification_report, confusion_matrix, f1_score, accuracy_score
    Подключает готовые инструменты; import сам по себе модель не обучает.
  3. 3: from sklearn.pipeline import Pipeline
    Подключает готовые инструменты; import сам по себе модель не обучает.
  4. 4: from sklearn.base import clone
    Подключает готовые инструменты; import сам по себе модель не обучает.
  5. 5: from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
    Подключает готовые инструменты; import сам по себе модель не обучает.
  6. 7: train_text, test_text, y_train, y_test = train_test_split(
    Делит строки на обучение и тест, сохраняя доли классов.
  7. 8: df['title_abstract_preprocessed'], df['domain'], test_size=0.2, random_state=42, stratify=df['domain'])
    Вычисляет правую часть и сохраняет результат в df['title_abstract_preprocessed'], df['domain'], test_size.
  8. 9: print('Train:', len(train_text), '| test:', len(test_text))
    Печатает число или пояснение, чтобы проверить результат выполнения.
  9. 10: labels = sorted(df['domain'].unique())
    Сортирует значения, чтобы порядок слов и столбцов был воспроизводимым.
  10. 11: results = []
    Вычисляет правую часть и сохраняет результат в results.
  11. 13: def evaluate(name, vectorizer, report=True):
    Объявляет функцию: код внутри выполняется при её вызове.
  12. 14: start = perf_counter()
    Замеряет время, чтобы отдельно оценить затраты на этот этап.
  13. 15: X_train = vectorizer.fit_transform(train_text)
    Выучивает словарь и статистику по обучающим текстам и сразу строит их матрицу.
  14. 16: X_test = vectorizer.transform(test_text)
    Применяет уже выученную статистику, не добавляя слов из test.
  15. 17: vector_time = perf_counter() - start
    Замеряет время, чтобы отдельно оценить затраты на этот этап.
  16. 18: for title, prototype in [('NB', clf1), ('SVC', clf2)]:
    Повторяет вложенный блок для каждого элемента указанной последовательности.
  17. 19: model = clone(prototype)
    Вычисляет правую часть и сохраняет результат в model.
  18. 20: start = perf_counter()
    Замеряет время, чтобы отдельно оценить затраты на этот этап.
  19. 21: model.fit(X_train, y_train)
    Обучает объект по переданным данным; test здесь использовать нельзя.
  20. 22: fit_time = perf_counter() - start
    Замеряет время, чтобы отдельно оценить затраты на этот этап.
  21. 23: start = perf_counter()
    Замеряет время, чтобы отдельно оценить затраты на этот этап.
  22. 24: pred = model.predict(X_test)
    Получает предсказанные домены из обученного классификатора.
  23. 25: pred_time = perf_counter() - start
    Замеряет время, чтобы отдельно оценить затраты на этот этап.
  24. 26: results.append({'representation': name, 'model': title, 'macro_f1': f1_score(y_test, pred, average='macro'),
    Добавляет один результат в список; порядок сохраняется.
  25. 27: 'accuracy': accuracy_score(y_test, pred), 'vector_s': vector_time, 'fit_s': fit_time, 'predict_s': pred_time})
    Продолжает выражение предыдущей строки: здесь заданы данные или аргументы вызова.
  26. 28: if report:
    Выполняет вложенный блок только при истинном условии.
  27. 29: print(name, title)
    Печатает число или пояснение, чтобы проверить результат выполнения.
  28. 30: print(classification_report(y_test, pred, labels=labels, zero_division=0, digits=3))
    Выводит precision, recall, F1 и support отдельно по каждому классу.
  29. 31: plt.figure(figsize=(7, 5))
    Настраивает график, подписи или выводит его на экран.
  30. 32: sns.heatmap(confusion_matrix(y_test, pred, labels=labels), annot=True, fmt='d',
    Считает таблицу настоящих и предсказанных меток.
  31. 33: xticklabels=labels, yticklabels=labels, cmap='YlOrBr')
    Вычисляет правую часть и сохраняет результат в xticklabels.
  32. 34: plt.xlabel('Предсказанный домен'); plt.ylabel('Настоящий домен')
    Настраивает график, подписи или выводит его на экран.
  33. 35: plt.title(name + ' / ' + title)
    Настраивает график, подписи или выводит его на экран.
  34. 36: plt.xticks(rotation=25, ha='right'); plt.yticks(rotation=0)
    Настраивает график, подписи или выводит его на экран.
  35. 37: plt.tight_layout(); plt.show()
    Настраивает график, подписи или выводит его на экран.
  36. 38: return vectorizer
    Возвращает результат функции вызывающему коду.
Код исходной ячейки 23: # Сравнение одинаковых словарей: проверяем каждое значение, а не только метрику.
# Сравнение одинаковых словарей: проверяем каждое значение, а не только метрику.
check_own = BagOfWords((1, 2), 15000).fit(train_text)
check_sk = CountVectorizer(vocabulary=check_own.vocabulary, ngram_range=(1, 2))
assert (check_own.transform(train_text) != check_sk.transform(train_text)).nnz == 0
assert (check_own.transform(test_text) != check_sk.transform(test_text)).nnz == 0
print('Матрицы BoW совпадают со sklearn при одном словаре')
evaluate('sklearn BoW fixed vocabulary', check_sk)

searches = {}
for kind, vec in [('BoW', CountVectorizer()), ('TFIDF', TfidfVectorizer())]:
    for title, prototype in [('NB', clf1), ('SVC', clf2)]:
        pipe = Pipeline([('vec', vec), ('clf', clone(prototype))])
        grid = {'vec__max_features': [5000, 15000], 'vec__ngram_range': [(1, 1), (1, 2)], 'vec__min_df': [1, 3]}
        start = perf_counter()
        search = GridSearchCV(pipe, grid, scoring='f1_macro', cv=3, n_jobs=1)
        search.fit(train_text, y_train)
        searches[(kind, title)] = search
        print(kind, title, 'CV macro-F1:', round(search.best_score_, 4), search.best_params_, 'сек:', round(perf_counter()-start, 1))
# Здесь только train-CV; test для подбора не используется.
  1. 1: # Сравнение одинаковых словарей: проверяем каждое значение, а не только метрику.
    Комментарий объясняет причину следующего действия.
  2. 2: check_own = BagOfWords((1, 2), 15000).fit(train_text)
    Обучает объект по переданным данным; test здесь использовать нельзя.
  3. 3: check_sk = CountVectorizer(vocabulary=check_own.vocabulary, ngram_range=(1, 2))
    Вычисляет правую часть и сохраняет результат в check_sk.
  4. 4: assert (check_own.transform(train_text) != check_sk.transform(train_text)).nnz == 0
    Проверяет ожидаемое свойство; если оно нарушено, останавливает запуск.
  5. 5: assert (check_own.transform(test_text) != check_sk.transform(test_text)).nnz == 0
    Проверяет ожидаемое свойство; если оно нарушено, останавливает запуск.
  6. 6: print('Матрицы BoW совпадают со sklearn при одном словаре')
    Печатает число или пояснение, чтобы проверить результат выполнения.
  7. 7: evaluate('sklearn BoW fixed vocabulary', check_sk)
    Продолжает выражение предыдущей строки: здесь заданы данные или аргументы вызова.
  8. 9: searches = {}
    Вычисляет правую часть и сохраняет результат в searches.
  9. 10: for kind, vec in [('BoW', CountVectorizer()), ('TFIDF', TfidfVectorizer())]:
    Повторяет вложенный блок для каждого элемента указанной последовательности.
  10. 11: for title, prototype in [('NB', clf1), ('SVC', clf2)]:
    Повторяет вложенный блок для каждого элемента указанной последовательности.
  11. 12: pipe = Pipeline([('vec', vec), ('clf', clone(prototype))])
    Вычисляет правую часть и сохраняет результат в pipe.
  12. 13: grid = {'vec__max_features': [5000, 15000], 'vec__ngram_range': [(1, 1), (1, 2)], 'vec__min_df': [1, 3]}
    Вычисляет правую часть и сохраняет результат в grid.
  13. 14: start = perf_counter()
    Замеряет время, чтобы отдельно оценить затраты на этот этап.
  14. 15: search = GridSearchCV(pipe, grid, scoring='f1_macro', cv=3, n_jobs=1)
    Вычисляет правую часть и сохраняет результат в search.
  15. 16: search.fit(train_text, y_train)
    Обучает объект по переданным данным; test здесь использовать нельзя.
  16. 17: searches[(kind, title)] = search
    Вычисляет правую часть и сохраняет результат в searches[(kind, title)].
  17. 18: print(kind, title, 'CV macro-F1:', round(search.best_score_, 4), search.best_params_, 'сек:', round(perf_counter()-start, 1))
    Замеряет время, чтобы отдельно оценить затраты на этот этап.
  18. 19: # Здесь только train-CV; test для подбора не используется.
    Комментарий объясняет причину следующего действия.

Результаты нашего прогона

ВекторыМодельCV macro-F1Test macro-F1Настройки
BoWNB0.83880.843915000 признаков, [1, 1], min_df=1
BoWSVC0.82450.830015000 признаков, [1, 2], min_df=1
TFIDFNB0.84110.836815000 признаков, [1, 2], min_df=3
TFIDFSVC0.86640.868015000 признаков, [1, 1], min_df=1
GloVe meanSVCНе подбирался0.8186200 координат

Настройки выбирались по CV. Колонка test это окончательная оценка; 0.9 означает высокий F1, но F1 не равен доле правильных ответов.

Вопросы защиты

Зачем две модели?

Чтобы сравнить влияние представления текста: полезность TF-IDF зависит от классификатора.

Почему macro-F1?

Каждый домен равноправен. Сейчас классы сбалансированы, но macro-F1 явно проверяет качество каждого.

Что значит min_df=3?

Исключить слово, встретившееся менее чем в трёх обучающих документах.

Какая модель лучше?

Смотри реальные результаты ниже и ответ в ноутбуке, не угадывай заранее.

Одна фразаНастройки выбираю по train-CV внутри Pipeline, итоговое качество измеряю на неизменном test.