ЛР4
Рассказ на защите

Сценарий защиты

Рассказ на защите

Номера в квадратных скобках это счётчик кода после перезапуска и «Выполнить все». Положи рядом ноутбук и проговори вслух.

Сначала: «Моя задача определить один из четырёх доменов статьи по её заголовку и аннотации. Сравниваю три представления текста и два классификатора».

[1] · import requests

Сначала скачиваю снимок 10000 статей из OpenAlex. Сбор выполнялся по доменам, аннотации восстановлены из позиций слов. Здесь также сохранён код запроса новых данных.

Почему не скачиваешь каждый запуск?

Чтобы API и обновления меток не меняли результаты перед сдачей.

[2] · print('Всего записей:', len(df))

Показываю число записей, четыре равных класса и распределения длин. Проверил заполненность полей.

Сколько классов?

Четыре, по 2500 статей.

[3] · import sys, subprocess

Привожу текст к нижнему регистру, убираю числа и пунктуацию, лемматизирую с частью речи и удаляю стоп-слова. Заголовок и аннотация объединены.

Можно до split?

Да: записи обрабатываются независимо, словари NLTK уже готовы.

[4] · from sklearn.naive_bayes import MultinomialNB

Создаю два классификатора: наивный Байес и линейный SVC с random_state=42.

Чем отличаются?

Байес использует вероятности токенов по классам, SVC ищет линейные разделяющие границы.

[5] · from sklearn.model_selection import train_test_split, GridSearchCV

Делю данные 80:20 до векторизации, со стратификацией. Функция evaluate замеряет векторизацию, обучение и предсказание и выводит отчёт и матрицу ошибок для обеих моделей.

Как здесь исключена утечка?

fit_transform только для train, для test transform.

[6] · from collections import Counter

В своём BoW fit считает частоты и фиксирует словарь. Transform записывает счётчики в CSR-матрицу. Неизвестные слова игнорируются.

Тут можно без цикла?

Строки и переменные токены подсчитываю циклами; это разрешено. Большую матрицу храню разреженно.

[7] · bow = BagOfWords(ngram_range=(1, 2), max_features=15)

На маленьких предложениях показываю униграммы и биграммы, максимум 15 признаков.

Почему можно toarray?

Таблица маленькая. На всём корпусе так делать не стоит.

[8] · for ngrams, limit in [((1, 1), 5000), ((1, 1), 15000), ((1, 2), 15000)]:

Проверяю три комбинации размера словаря и n-грамм с двумя моделями. Смотрю macro-F1 и время, не выбираю настройки по test.

Тогда как выбираешь?

Следующий GridSearch выбирает по train-CV.

[9] · # Сравнение одинаковых словарей: проверяем каждое значение, а не только метрику.

Сначала доказываю, что свой BoW совпадает со sklearn при одном словаре. Затем GridSearch в Pipeline перебирает восемь настроек на трёх фолдах для каждой пары представление/модель.

Зачем Pipeline?

Чтобы словарь учился заново внутри учебной части каждого фолда.

[10] · def keywords(matrix, feature_names, targets, top_n=20):

Общий топ это сумма счётчиков. Для домена беру контраст с остальными, чтобы найти специфичные, а не просто общие частые слова.

Какой контраст?

Логарифм отношения сглаженных нормированных частот.

[11] · class TFIDF(BagOfWords):

Мой TF-IDF наследует токенизацию и словарь BoW. Fit считает DF и IDF, transform умножает TF на IDF и нормализует строки L2.

Почему без цикла для IDF?

Это формула над массивом DF, вычисляется NumPy.

[12] · tfidf = TFIDF(ngram_range=(1, 2), max_features=15)

Показываю маленькую таблицу TF-IDF и ручную проверку IDF для двух документов.

Что такое +1?

Сглаживание. В формуле задания IDF=ln((N+1)/(DF+1))+1.

[13] · for ngrams, limit in [((1, 1), 5000), ((1, 1), 15000), ((1, 2), 15000)]:

Сравниваю те же три настройки собственного TF-IDF на двух классификаторах.

Почему одинаковые настройки?

Чтобы сравнение с BoW было понятнее.

[14] · own_tf = TFIDF((1, 2), 15000).fit(train_text)

Проверяю TF-IDF со sklearn до 1e-12. Потом оцениваю уже выбранные GridSearch конфигурации на test.

Как совпало при relative TF?

После L2 общий множитель 1/длина сокращается.

[15] · keyword_tf = TfidfVectorizer(max_features=15000, min_df=3)

Показываю топы по TF-IDF и число пересечений с BoW. Объясняю разницу: редкость и нормализация длины.

Это числа упоминаний?

Нет, суммы весов TF-IDF.

[16] · def svd_gradient_descent(A, k, learning_rate=0.001, epochs=100):

Минимизирую средний квадрат ошибки восстановления A. Оба градиента считаю матрично по старым U и V_T, потом обновляю.

По формуле без цикла?

Да: формулы error @ V_T.T и U.T @ error. Цикл только по эпохам.

[17] · # Примеры - значения матрицы Result должны приближаться к значениям исходной матрицам

Смотрю произведения множителей, они должны приближать исходные матрицы. После исходного теста сравниваю MSE за 1000 и 10000 эпох.

Почему не сравниваешь U?

Множители не единственные; важна их реконструкция.

[18] · print('MSE A:', np.mean((U @ V_T - A)**2))

Дополнительно проверяю MSE реконструкции. На второй матрице видно, зачем нужно больше эпох.

[19] · from sklearn.decomposition import TruncatedSVD

Слова сортирую. Для каждой пары в окне добавляю оба направления. Статьи не соединяю. Возвращаю CSR и словарь индексов.

Это точно GloVe?

Это упрощённая матрица контекста с SVD, предусмотренная шаблоном. Полный GloVe имеет другую целевую функцию.

[20] · # Тесты

Запускаю исходные тесты. При окне 1 apple-banana встречаются дважды, матрица совпадает с ожидаемой целиком.

Матрица симметричная?

Да, потому что считаются оба направления пары.

[21] · # Для своего корпуса строю матрицу сам: скачивать pickle не нужно.

Строю собственную матрицу на train, оставляю 12000 частых слов. Не скачиваю чужой pickle и не использую его случайный корпус.

Зачем ограничивать словарь?

Память и время SVD. Это ограничение отдельно указано.

[22] · # log(1+x) уменьшает влияние очень частых пар.

Применяю log(1+x), сжимаю матрицу до двух координат и рисую несколько слов.

Двух координат достаточно?

Для картинки, но не для поиска хороших соседей.

[23] · len(word2ind) # vocabulary size

Показываю размер словаря контекстной модели.

Он равен словарю GloVe?

Нет. У готового GloVe 400000 слов.

[24] · from sklearn.metrics.pairwise import cosine_similarity

Считаю косинусные близости ко всем словам сразу, исключаю само слово и возвращаю top_n.

Без цикла?

Близости вычисляются матрично; цикл только оформляет небольшой список ответа.

[25] · M_reduced = TruncatedSVD(n_components=312, random_state=42).fit_transform(log_M)

Для соседей использую 312 компонент. Печатаю настоящие ближайшие слова и дополнительно проверяю реконструкции MSE.

Почему соседи могут быть странными?

Маленький неоднородный корпус и ограниченное представление.

[26] · def load_embedding_model():

Загружаю готовую glove-wiki-gigaword-200: 400000 слов, по 200 чисел. Первый запуск скачивает модель, следующие используют кеш.

Нужен GPU?

Нет, здесь достаточно CPU и оперативной памяти.

[27] · for word in ['algorithm', 'ecology', 'expression', 'gene', 'python', 'treatment']:

Сравниваю соседей тех же слов в предобученной модели. Вызов исправлен на topn=10.

Что было не так в шаблоне?

Второй позиционный аргумент most_similar это negative, а не число соседей.

[28] · from pprint import pprint

Исследую bank, нахожу тройки синоним/антоним с нужным неравенством и показываю ассоциации профессий с направлением he-she.

Это доказывает свойства людей?

Нет, это свойства векторов, обученных на текстах.

[29] · def mean_embeddings(texts):

Усредняю известные векторы слов каждой статьи. Считаю покрытие и качество LinearSVC. Показываю минимальную компоненту, чтобы объяснить, почему NB не применим.

Почему NB нельзя?

Есть отрицательные компоненты, а MultinomialNB требует неотрицательные признаки.

Числа, которые показываю

Test macro-F1: BoW + NB: 0.8439; BoW + SVC: 0.8300; TFIDF + NB: 0.8368; TFIDF + SVC: 0.8680; GloVe + SVC: 0.8186. Гиперпараметры произношу из таблицы GridSearch, а не выбираю по этим test-числам.

В конце: письменные ответы

Покажи вывод о сравнении моделей, пояснения трёх экспериментов и ответ про OpenAlex. «Метки автоматические, поэтому оцениваю согласие с разметчиком. Feedback loop возникнет, если предсказания вернутся в новые обучающие метки».

Перед сдачей Colab

  1. Загрузи lab4_solved.ipynb. Тип среды CPU.
  2. Выполни все ячейки. Для первой загрузки GloVe нужны интернет и время.
  3. Убедись, что нет красных ошибок, числа совпадают с письменными выводами.
  4. Настрой просмотр по ссылке и открой ссылку в окне без входа, чтобы проверить доступ.
Ловушка. Локальный nbclient не является запуском внутри Colab. Если версия библиотек отличается, сверяй числа своего экрана.