4 Commits

Author SHA1 Message Date
Mihail Savostikov
79f51105fd test result 2026-10-25 21:59:59 +03:00
Mihail Savostikov
84ae1c2408 modificate gitignore 2026-10-25 21:55:44 +03:00
Mihail Savostikov
b00e3c964e modificate gitignore 2026-10-25 21:26:19 +03:00
Mihail Savostikov
c131815fc7 New tests and his starting scripts 2026-10-25 19:08:11 +03:00
84 changed files with 1029 additions and 9460 deletions

1
.gitignore vendored
View File

@@ -10,3 +10,4 @@ lab1/data/processed/
lab1/features/ lab1/features/
lab1/results/* lab1/results/*
!lab1/results/README.md !lab1/results/README.md

View File

@@ -53,15 +53,5 @@ nlp/
методическое описание метрик и классификаторов. методическое описание метрик и классификаторов.
- [`lab1/FEATURES.md`](lab1/FEATURES.md) — признаки и формулы. - [`lab1/FEATURES.md`](lab1/FEATURES.md) — признаки и формулы.
- [`lab1/TUNING.md`](lab1/TUNING.md) — настройка параметров и перебор. - [`lab1/TUNING.md`](lab1/TUNING.md) — настройка параметров и перебор.
- [`lab1/RESULTS_ANALYSIS.md`](lab1/RESULTS_ANALYSIS.md) — анализ результатов
follow-up серии и выводы.
- [`lab1/FINAL_REPORT.md`](lab1/FINAL_REPORT.md) — итоговый отчёт по всей
работе и экспериментам.
- [`lab1/FINAL_RESULTS.csv`](lab1/FINAL_RESULTS.csv) — сводная таблица
результатов.
- [`lab1/FOLLOWUP_EXPERIMENTS.md`](lab1/FOLLOWUP_EXPERIMENTS.md) — протокол
follow-up экспериментов.
- [`lab1/PRESENTATION_OUTLINE.md`](lab1/PRESENTATION_OUTLINE.md) — структура - [`lab1/PRESENTATION_OUTLINE.md`](lab1/PRESENTATION_OUTLINE.md) — структура
презентации. презентации.
- [`lab1/presentation/README.md`](lab1/presentation/README.md) — готовая
презентация, сборка и исходные изображения.

14
lab1/.gitignore vendored
View File

@@ -7,17 +7,3 @@ __pycache__/
/features/ /features/
/results/* /results/*
!/results/README.md !/results/README.md
/presentation/preview/
/presentation/latex/*.aux
/presentation/latex/*.log
/presentation/latex/*.nav
/presentation/latex/*.out
/presentation/latex/*.snm
/presentation/latex/*.toc
/presentation/latex/*.synctex.gz
/presentation/latex/preview/
/report/build/
/dist/stage/

View File

@@ -1,529 +0,0 @@
# Архив лабораторной работы №1
**Тема:** классификация текстов на основе стилометрических характеристик.
**Материал:** реплики персонажей игры Stardew Valley.
**Задача:** по одной реплике определить, какому из 34 персонажей она
принадлежит, и проверить, какие стилометрические признаки несут этот сигнал.
Это не авторская атрибуция: все реплики написаны одним автором. Мы различаем
**речевые маски персонажей** — намеренно созданные автором различия в лексике,
синтаксисе, пунктуации и разговорных маркерах.
---
## 1. Что в архиве
| Путь | Что это |
|---|---|
| `lab1/report/REPORT.pdf` | Итоговый отчёт, 21 страница. Главный документ. |
| `lab1/report/REPORT.docx` | Редактируемая версия отчёта. |
| `lab1/report/REPORT.md` | Исходник отчёта в Markdown. |
| `lab1/report/figures/` | Рисунки отчёта. |
| `lab1/presentation/stardew_stylometry.pdf` | Презентация для защиты. |
| `lab1/presentation/stardew_stylometry.pptx` | Редактируемая презентация. |
| `lab1/presentation/latex/` | LaTeX-исходник и PDF презентации. |
| `lab1/presentation/images/`, `generated/` | Исходные изображения и диаграммы. |
| `lab1/stylometry/` | Основной код проекта. |
| `lab1/scripts/` | Проверка окружения, запуск серий, сборка отчёта, упаковка. |
| `lab1/tests/` | 33 теста (`pytest`). |
| `lab1/configs/default.toml` | Конфигурация по умолчанию. |
| `lab1/data/dialogues.csv` | Корпус: 2005 реплик, cp1251, разделитель `;`. |
| `lab1/data/dialogues.xlsx` | Тот же корпус в Excel. |
| `lab1/data/stats_per_character.xlsx` | Статистика по персонажам. |
| `lab1/data/metadata/` | Метаданные персонажей для анализа ошибок. |
| `lab1/data/corpus_report_*.txt` | Отчёты о состоянии корпуса до/после очистки. |
| `lab1/FINAL_RESULTS.csv` | Сводная таблица финальных результатов. |
| `lab1/FINAL_REPORT.md` | Расширенная версия отчёта с деталями экспериментов. |
| `lab1/EXPERIMENTS.md` | Протокол экспериментов и команд. |
| `lab1/FEATURES.md` | Признаки, формулы, обоснование. |
| `lab1/METRICS_AND_CLASSIFIERS.md` | Метрики и классификаторы. |
| `lab1/TUNING.md` | Настройка гиперпараметров. |
| `lab1/RUNBOOK.md` | Подробная инструкция по запуску полной серии. |
| `lab1/RESULTS_ANALYSIS.md` | Анализ результатов и ошибок. |
| `lab1/FOLLOWUP_EXPERIMENTS.md` | Дополнительные эксперименты. |
| `lab1/PRESENTATION_OUTLINE.md` | Структура презентации. |
| `lab1/exports/nlp_lab1_final_results_20261026_120000.tar.gz` | Финальные результаты в компактном виде. |
| `lab1/requirements.txt` | Основные зависимости. |
| `lab1/requirements-report.txt` | Зависимости для пересборки отчёта. |
Полный список файлов с размерами и SHA-256 — в `MANIFEST.txt`.
Контрольные суммы ключевых файлов — в `CHECKSUMS.sha256`.
---
## 2. Требования
### 2.1. Обязательные
- Python **3.11+** (проверено на 3.14);
- Linux, macOS или Windows с WSL;
- около 1 ГБ свободного места с виртуальным окружением и NLTK;
- интернет — только для установки пакетов и скачивания NLTK.
### 2.2. Для пересборки отчёта
- `pandoc` 3.x;
- LibreOffice (`libreoffice --headless`);
- Python-пакеты `python-docx`, `PyMuPDF` (см. `requirements-report.txt`);
- шрифты с кириллицей: Times New Roman или Liberation Serif, Liberation Mono.
### 2.3. Для пересборки LaTeX-презентации
- XeLaTeX;
- пакеты Beamer, tcolorbox, TikZ, fontspec, babel-russian;
- шрифт Liberation Sans.
---
## 3. Установка
Все команды выполняются из каталога `lab1`:
```bash
cd lab1
```
### 3.1. Виртуальное окружение
```bash
python -m venv .venv
source .venv/bin/activate
```
Windows PowerShell:
```powershell
python -m venv .venv
.venv\Scripts\activate
```
Если окружение уже есть в корне проекта, можно использовать его:
```bash
source ../.venv/bin/activate
```
### 3.2. Зависимости
```bash
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
```
Для пересборки отчёта дополнительно:
```bash
python -m pip install -r requirements-report.txt
```
### 3.3. Ресурсы NLTK
```bash
python scripts/download_nltk_data.py
```
Скрипт скачивает `punkt`, `punkt_tab`, `averaged_perceptron_tagger`,
`averaged_perceptron_tagger_eng`, `stopwords` в `lab1/nltk_data`.
Без этих ресурсов POS-признаки отключатся, а результаты будут неполными.
Если машина без интернета, скопируйте готовый каталог `nltk_data` в `lab1/`.
### 3.4. Проверка
```bash
python scripts/check_environment.py
python -m pytest -q
```
Ожидается:
```text
Окружение готово.
33 passed
```
`check_environment.py` печатает версии библиотек, SHA-256 корпуса, число
реплик, персонажей и словоупотреблений, проверяет токенизатор и POS-теггер.
---
## 4. Быстрый запуск
### 4.1. Аудит корпуса
```bash
python -m stylometry.cli audit
```
Создаёт:
- `data/processed/audit.md` — отчёт по корпусу;
- `data/processed/lines.csv` — нормализованные реплики;
- `data/processed/figures/class_distribution.png`.
Проверьте: 2005 реплик, 34 персонажа, 38 207 словоупотреблений, точных
дублей нет.
### 4.2. Ручные признаки
```bash
python -m stylometry.cli features
```
Создаёт:
- `features/stylometric.csv` — матрица 2005 × 454;
- `features/feature_dictionary.csv` — описание признаков.
### 4.3. Одна модель
```bash
python -m stylometry.cli run \
--preset stylometry_pos_lsa \
--model logreg \
--folds 5 \
--repeats 5
```
Результаты появятся в `results/<preset>_<model>_<timestamp>/`:
`summary.md`, `summary.json`, `fold_metrics.csv`, `pooled_metrics.csv`,
`per_class_report.csv`, `confusion_matrix.csv`, `oof_predictions.csv`.
### 4.4. Полная серия
```bash
bash scripts/run_all_experiments.sh
```
Скрипт последовательно запускает все стадии и пишет лог в
`results/logs/run_all_<timestamp>.log`. Ориентировочное время — 1,5–3 часа
на обычном ноутбуке. Быстрый режим без финальных чисел:
```bash
QUICK=1 bash scripts/run_all_experiments.sh
```
### 4.5. Финальные серии
```bash
python scripts/run_final_experiments.py \
--stages nested topic calibration ensemble extra \
--out results/final
```
Эта команда воспроизводит вложенный выбор конфигурации, контроль содержания,
коррекцию приоров, ансамбли и дополнительные конфигурации. Именно эти
результаты сведены в `FINAL_RESULTS.csv` и в отчёте.
---
## 5. Что делает каждый модуль
| Модуль | Назначение |
|---|---|
| `stylometry/io.py` | Чтение CSV/XLSX, проверка колонок, SHA-256 корпуса. |
| `stylometry/cleaning.py` | Unicode-нормализация, плейсхолдеры, пробелы. |
| `stylometry/tokenization.py` | NLTK: токенизация, предложения, POS; резервный режим. |
| `stylometry/features/` | 454 ручных признака в 11 группах. |
| `stylometry/lsa.py` | TF-IDF + TruncatedSVD для word/content/fw/char/pos. |
| `stylometry/pipelines.py` | Объединение блоков признаков, модели, сетки параметров. |
| `stylometry/cv.py` | Повторная стратифицированная и вложенная кросс-валидация. |
| `stylometry/evaluation.py` | Macro-F1, accuracy, balanced accuracy, MCC, каппа, top-k, log loss, Brier, бутстрэп. |
| `stylometry/stats.py` | Мак-Немар, Уилкоксон, Фридман, поправка Холма. |
| `stylometry/delta.py` | Дельта Барроуза: MFW, z-нормирование, манхэттенское и косинусное расстояние. |
| `stylometry/verification.py` | Верификация «свой/чужой»: ROC-AUC, EER, TAR@FAR. |
| `stylometry/clustering.py` | Иерархическая и k-means кластеризация, ARI, NMI, силуэт. |
| `stylometry/plotting.py` | Диаграммы классов, scree, матрицы ошибок, PCA, t-SNE. |
| `stylometry/metadata.py` | Метаданные персонажей, анализ подгрупп. |
| `stylometry/cli.py` | Единая точка входа для всех команд. |
| `main.py` | Короткая обёртка над `stylometry.cli`. |
### Группы ручных признаков
1. Длина.
2. Лексика.
3. Словарное богатство.
4. Символы.
5. Пунктуация.
6. Функциональные слова.
7. Местоимения.
8. Части речи.
9. Синтаксис.
10. Читаемость.
11. Разговорные маркеры.
Формулы и обоснование — в `FEATURES.md`.
---
## 6. Данные и разметка
### 6.1. Формат
`data/dialogues.csv`:
- кодировка cp1251;
- разделитель `;`;
- колонки `Реплика` и `Персонаж`;
- 2005 строк, 34 класса.
`data/dialogues.xlsx` — тот же корпус для Excel.
`data/stats_per_character.xlsx` — число реплик и длины по персонажам.
`data/metadata/` — пол, возраст, вид и роль для анализа ошибок; в признаки
классификатора эти поля не подаются.
### 6.2. Правила разметки
1. Одна реплика — один объект.
2. Метка — имя говорящего.
3. Один объект относится ровно к одному классу.
4. Соответствие реплики и метки проверено вручную.
5. Служебные метки `<PLAYER>`, `<FARM>`, `<PET>`, `<SPOUSE>`, `<TIME>`,
`<NOUN>`, `<NAME>` заменены нейтральными словами; их число сохранено
отдельным признаком.
### 6.3. Очистка и шум
Нормализуется только Unicode: варианты кавычек, тире, многоточий и пробелов.
Пунктуация, регистр и стоп-слова **сохраняются** — это стилометрический
сигнал.
Шумом считались точные дубликаты, технические артефакты, общие короткие
формулы и плейсхолдеры. Корпус прошёл ручную фильтрацию; точные правила
фильтрации не сохранились, поэтому исходное распределение восстановить
нельзя. Это ограничение зафиксировано в отчёте.
### 6.4. Ограничение по объёму
Формальные требования: не менее 90 текстов по 30 предложений и примерно
равные классы. В этом корпусе единица анализа — реплика (в среднем 19 слов),
классы неравномерны (15–139 реплик). По согласованию с преподавателем
эксперимент выполнен на уровне реплик; в отчёте раздел 2.5 честно описывает
это отклонение. Для компенсации используются веса классов и macro-F1.
---
## 7. Метрики и результаты
Основная метрика — **macro-F1**: средняя F-мера по 34 классам, каждый
персонаж весит одинаково. Дополнительно считаются accuracy, balanced
accuracy, top-3/top-5, MCC, каппа Коэна, log loss, Brier, матрица ошибок,
бутстрэп-интервалы.
| Конфигурация | Macro-F1 |
|---|---:|
| Мягкое голосование трёх моделей | **0,3036 ± 0,0189** |
| ExtraTrees, `char_max_features=10000` | 0,2757 ± 0,0219 |
| Вложенный выбор конфигурации | 0,2746 ± 0,0233 |
| ExtraTrees, полный набор | 0,2499 ± 0,0209 |
| Логистическая регрессия, lean + content-LSA | 0,2466 ± 0,0191 |
| Случайный лес, `char_max_features=10000` | 0,2583 ± 0,0066 |
| Только ручные признаки | 0,1051 ± 0,0038 |
| Случайный уровень | 0,029 |
На отложенной выборке 80/20 мягкое голосование дало 0,3333.
Контроль содержания: после замены существительных, глаголов, прилагательных,
наречий и числительных маркером `CONTENT` macro-F1 падает с 0,2483 до 0,1129.
Остаток всё ещё выше случайного уровня, но основной сигнал —
лексико-тематический.
---
## 8. Как пересобрать отчёт
Отчёт собирается из `report/REPORT.md`:
```bash
cd lab1
python scripts/build_report.py
```
Скрипт:
1. вызывает `pandoc` и делает `report/build/REPORT_raw.docx`;
2. оформляет стили, поля, колонтитулы, таблицы и рисунки;
3. сохраняет `report/REPORT.docx`;
4. конвертирует DOCX в `report/REPORT.pdf` через LibreOffice;
5. проверяет, что в PDF есть все основные разделы.
Только DOCX:
```bash
python scripts/build_report.py --skip-pdf
```
Требования: `pandoc`, `libreoffice`, `python-docx`, `PyMuPDF`.
Рисунки лежат в `report/figures/`; Markdown ссылается на них относительно
`report/`.
---
## 9. Как пересобрать презентацию
### 9.1. PPTX
В архиве лежит **финальная** версия `stardew_stylometry.pptx` (30 слайдов)
после ручных правок. Скрипт `build_deck.py` — генератор исходной версии
презентации; он создаёт 32 слайда и **перезапишет** финальный файл, если его
запустить. Не запускайте скрипт, если хотите сохранить ручные правки: для
изменений открывайте PPTX в PowerPoint или LibreOffice.
Если нужно пересобрать исходную версию с нуля:
```bash
cd lab1/presentation
python build_deck.py
```
Скрипт использует `python-pptx`, `Pillow` и `matplotlib`. Диаграммы уже лежат
в `presentation/generated/`; для их перегенерации нужны результаты серий
(`results/final/...`), которых нет в архиве — сначала запустите эксперименты
из раздела 4.
### 9.2. LaTeX/PDF
```bash
cd lab1/presentation/latex
xelatex stardew_stylometry.tex
xelatex stardew_stylometry.tex
```
Нужны XeLaTeX, Beamer, tcolorbox, TikZ и шрифт Liberation Sans. Готовый PDF —
`stardew_stylometry.pdf`. Изображения подтягиваются из `../images/` и
`../generated/`.
---
## 10. Где смотреть результаты
- `lab1/report/REPORT.pdf` — итоговый отчёт со всеми числами и выводами.
- `lab1/FINAL_RESULTS.csv` — сводная таблица всех финальных конфигураций.
- `lab1/FINAL_REPORT.md` — расширенное описание экспериментов.
- `lab1/RESULTS_ANALYSIS.md` — анализ ошибок, классов-аттракторов и длины.
- `lab1/exports/nlp_lab1_final_results_20261026_120000.tar.gz` — исходные
`summary.json`, `fold_metrics.csv`, `confusion_matrix.csv` и предсказания
финальных серий.
- `lab1/presentation/generated/` — диаграммы для отчёта и презентации.
---
## 11. Частые проблемы
### `check_environment.py` не находит POS-теггер
Повторите:
```bash
python scripts/download_nltk_data.py
```
Проверьте, что каталог `lab1/nltk_data` существует и не пуст. Если сеть
недоступна, скопируйте `nltk_data` с другой машины.
### `pytest` не устанавливается
Убедитесь, что активировано виртуальное окружение и версия Python не ниже
3.11:
```bash
python --version
python -m pip install -r requirements.txt
```
### `python -m stylometry.cli` не находит модуль
Команда должна выполняться из каталога `lab1`, а не из корня репозитория:
```bash
cd lab1
python -m stylometry.cli audit
```
### LibreOffice не собирает PDF
Проверьте:
```bash
libreoffice --version
```
На сервере без графической сессии используйте именно `--headless`
(это уже делает `build_report.py`). Если LibreOffice нет, отчёт можно
открыть в Word/LibreOffice вручную и экспортировать в PDF.
### Кириллица в PDF выглядит квадратами
Установите шрифты Liberation Serif/Sans и Liberation Mono. В отчёте
используются Times New Roman (с запасным Liberation Serif) и Liberation Mono.
### Полная серия идёт слишком долго
Запускайте в `tmux` или `nohup`, см. `RUNBOOK.md`, раздел 5.4. Для проверки
работоспособности используйте `QUICK=1`.
---
## 12. Что исключено из архива
Чтобы архив оставался компактным и воспроизводимым, в него не включены:
- `.venv/` — виртуальное окружение;
- `.git/` — история репозитория;
- `nltk_data/` — скачивается скриптом;
- `data/processed/` — аудит и промежуточные таблицы;
- `features/` — матрица признаков (создаётся `features`);
- `results/` — промежуточные результаты серий;
- большие `exports/*.tar.gz` — кроме компактного финального архива;
- `presentation/preview/` — PNG-превью слайдов;
- `__pycache__/`, `.pytest_cache/` — кэши;
- `presentation/stardew_stylometry.bak` — резервная копия;
- служебные файлы LaTeX (`.aux`, `.log`, `.nav`, `.out`, `.snm`, `.toc`).
Всё это восстанавливается командами из разделов 3–4.
---
## 13. Проверка целостности архива
```bash
sha256sum -c CHECKSUMS.sha256
```
`MANIFEST.txt` содержит список всех файлов архива с размерами и SHA-256.
Если контрольные суммы совпали, архив распакован полностью.
Минимальная проверка после распаковки:
```bash
cd lab1
python scripts/check_environment.py
python -m pytest -q
python -m stylometry.cli audit
```
---
## 14. Атрибуция
Диалоги Stardew Valley принадлежат ConcernedApe. Корпус используется
исключительно в учебных и исследовательских целях. Код проекта можно
использовать и изменять для учебных задач.
---
## 15. Шпаргалка команд
```bash
# из lab1
python scripts/check_environment.py # проверить окружение
python -m pytest -q # 33 теста
python -m stylometry.cli audit # аудит корпуса
python -m stylometry.cli features # 454 ручных признака
python -m stylometry.cli run \
--preset stylometry_pos_lsa --model logreg --repeats 5
bash scripts/run_all_experiments.sh # полная серия
python scripts/run_final_experiments.py \
--stages nested topic calibration ensemble extra --out results/final
python scripts/build_report.py # отчёт DOCX + PDF
```

View File

@@ -1,674 +0,0 @@
# Итоговый отчёт по лабораторной работе
## Стилометрическая классификация реплик персонажей Stardew Valley
---
## Аннотация
Работа посвящена проверке того, можно ли стилометрически различить персонажей
игры Stardew Valley по отдельным репликам, если все реплики написаны одним
автором. Корпус содержит 2005 реплик, принадлежащих 34 персонажам; классы
неравномерны (от 15 до 139 реплик). Основная задача — многоклассовая
классификация; основная метрика — макро-усреднённая F-мера (macro-F1).
В работе построены ручные стилометрические признаки, символьные и
частеречные n-граммы, а также несколько вариантов латентно-семантического
анализа (LSA). Сравнивались логистическая регрессия, линейный метод опорных
векторов, наивный байесовский классификатор, случайный лес, сверхслучайные
деревья, метод ближайших соседей и дельта Барроуза. Использована повторная
стратифицированная перекрёстная проверка, вложенная настройка гиперпараметров,
контроль перестановкой меток и анализ ошибок.
Лучший наблюдаемый результат — мягкое голосование трёх моделей: macro-F1
около **0,304** при 5-блочной перекрёстной проверке и **0,333** на отдельной
стратифицированной отложенной выборке. Однако состав ансамбля выбирался по
результатам предыдущих серий, поэтому более консервативная оценка —
**0,275** для модели ExtraTrees с расширенным символьным словарём при
вложенном выборе конфигурации. Случайный уровень для 34 классов составляет
около **0,029**.
Главный содержательный вывод: персонажи действительно различимы, но
основной вклад дают лексико-тематические признаки и символьные n-граммы, а не
функциональные слова и синтаксис. При замене содержательных слов на
нейтральный маркер качество ExtraTrees падает с 0,248 до 0,113. Следовательно,
результат корректно интерпретировать как различимость речевых масок по
лексико-тематическому профилю, а не как доказательство «чистого» авторского
или персонажного идиолекта.
---
## 1. Введение
### 1.1. Постановка задачи
Рассматривается задача классификации коротких текстов — игровых реплик — по
принадлежности персонажу. Каждая реплика относится ровно к одному из 34
классов. Требуется:
1. построить признаки, отражающие стилометрические свойства реплик;
2. сравнить несколько семейств классификаторов;
3. оценить качество с учётом дисбаланса классов и малого объёма данных;
4. проверить, обусловлен ли результат стилем или содержанием;
5. сформулировать ограничения и направления дальнейшей работы.
### 1.2. Исследовательский вопрос
**Вопрос:** различимы ли 34 персонажа Stardew Valley по стилометрическим
признакам отдельных реплик, если все реплики написаны одним автором?
Это не авторская атрибуция. Автор один; речь идёт о классификации **речевых
масок персонажей** (character idiolect). Положительный результат означает,
что созданные автором речевые маски статистически различимы, но не означает,
что у персонажей разные авторы.
### 1.3. Гипотезы
| Обозначение | Гипотеза |
|---|---|
| H1 | Функциональные слова, частеречные признаки и символьные n-граммы различают персонажей выше случайного уровня |
| H2 | На коротких репликах частеречные и символьные признаки устойчивее словарного богатства и читаемости |
| H3 | LSA по всем словам отражает тему, а LSA по частеречным последовательностям — стиль |
| H4 | Качество зависит от длины реплики |
| H5 | Ошибки концентрируются вокруг персонажей со сходным возрастом, полом или ролью |
---
## 2. Корпус
### 2.1. Объём и разметка
| Параметр | Значение |
|---|---|
| Реплик | 2005 |
| Персонажей | 34 |
| Словоупотреблений | около 38,2 тыс. |
| Средняя длина реплики | 19,1 слова |
| Медиана | 17 слов |
| Минимум / максимум | 3 / 78 слов |
| Классы | 15–139 реплик |
| Точные дубли реплик | 0 |
Корпус хранится в кодировке cp1251; предусмотрено чтение XLSX. Разметка
содержит только текст реплики и имя персонажа. Метаданные о поле, возрасте,
виде и роли персонажей хранятся отдельно и используются только для анализа
ошибок, не как признаки классификатора.
### 2.2. Ограничения корпуса
- Корпус прошёл ручную фильтрацию: короткие и общие фразы удалены. Точные
правила очистки не сохранились, поэтому воспроизвести исходное
распределение невозможно.
- Реплики короткие; признаки словарного богатства и читаемости на них
нестабильны.
- Классы неравномерны; редкие персонажи имеют широкие доверительные
интервалы.
- Нет идентификаторов сцен и диалогов; нельзя учесть корреляцию реплик
внутри одной сцены.
- Все тексты написаны одним автором.
---
## 3. Методика
### 3.1. Предобработка
1. Приведение Unicode: многоточия, кавычки, тире, неразрывные пробелы.
2. Замена плейсхолдеров `<PLAYER>`, `<FARM>`, `<PET>`, `<SPOUSE>`, `<TIME>`,
`<NOUN>`, `<NAME>` на нейтральные слова; количество плейсхолдеров
сохраняется отдельным признаком.
3. Токенизация и разбиение на предложения средствами NLTK.
4. Частеречная разметка Penn Treebank с переводом в Universal POS.
5. Схлопывание подряд идущих знаков препинания в один тег `PUNCT` для
частеречных последовательностей.
Не выполнялись: удаление стоп-слов, удаление пунктуации, лемматизация,
удаление коротких реплик. Эти элементы рассматриваются как стилометрический
сигнал.
### 3.2. Признаковое пространство
Ручные признаки сгруппированы в 11 семейств; всего около 454 признака.
| Группа | Примеры | Статус |
|---|---|---|
| Длина | число токенов, слов, символов, предложений; длина предложения | Основная, но чувствительная к длине |
| Лексика | распределение длин слов, доля слов с апострофом, заглавных, цифр | Основная |
| Словарное богатство | TTR, Guiraud, Herdan, Yule's K, Simpson's D, hapax/dis, Honoré | Предварительная |
| Символы | частоты букв, регистр, цифры, пунктуация | Основная |
| Пунктуация | запятые, точки, восклицательные, вопросительные, многоточия | Основная |
| Функциональные слова | около 150 закрытых слов и 12 категорий | Основная |
| Местоимения | 1-е, 2-е, 3-е лицо, возвратные, притяжательные | Основная |
| Части речи | Universal POS, Penn Treebank, время, модальность, герундий | Основная |
| Синтаксис-прокси | число клауз, подчинительные союзы, вопросы, пассив-прокси | Основная |
| Читаемость | Flesch, FK grade, Gunning Fog, SMOG, Coleman-Liau, ARI | Предварительная |
| Разговорные маркеры | междометия, смех, хеджи, интенсификаторы, вежливость, сокращения | Основная |
Дополнительно используются:
- TF-IDF по символьным n-граммам (`char_wb`, 2–5, до 5000–10000 признаков);
- TF-IDF по последовательностям Universal POS (1–3, до 500 признаков);
- LSA по вариантам `word`, `word2`, `content`, `fw`, `char`, `pos`.
### 3.3. Латентно-семантический анализ
LSA реализован как последовательность:
```text
TF-IDF -> усечённое сингулярное разложение (TruncatedSVD) -> L2-нормализация
```
Варианты:
| Вариант | Что отражает |
|---|---|
| `word` | лексику и тему |
| `word2` | лексику и устойчивые сочетания |
| `content` | содержание после удаления стоп-слов |
| `fw` | функциональные слова и синтаксис |
| `char` | морфологию и пунктуацию |
| `pos` | частеречные последовательности |
Число компонент подбиралось по объяснённой дисперсии и внешней метрике.
Для оценки использовались 50–400 компонент; для `content`-LSA наблюдался
монотонный рост качества до 400 компонент.
### 3.4. Классификаторы
| Модель | Краткая характеристика |
|---|---|
| Dummy | нижняя граница; предсказывает самый частый класс |
| MultinomialNB | вероятностная модель для неотрицательных частотных признаков |
| LogisticRegression | линейная модель с L2-регуляризацией и весами классов |
| LinearSVC | линейный метод опорных векторов с максимизацией зазора |
| RandomForest | ансамбль решающих деревьев |
| ExtraTrees | ансамбль деревьев со случайными порогами |
| kNN | классификация по ближайшим объектам с косинусным расстоянием |
| Delta | дельта Барроуза по наиболее частотным словам |
Для логистической регрессии, SVM и деревьев использовались веса классов.
Для MultinomialNB LSA-блоки исключались, поскольку компоненты SVD могут быть
отрицательными.
### 3.5. Схема валидации
- Основная схема: `RepeatedStratifiedKFold(n_splits=5, n_repeats=3–5)`.
- Все обучаемые преобразования (TF-IDF, SVD, масштабирование) настраивались
только на обучающих блоках.
- Для настройки гиперпараметров использовалась вложенная перекрёстная
проверка: внешние 5 блоков, внутренние 3 блока.
- Дополнительно выполнены:
- вложенный выбор конфигурации признаков и модели;
- стратифицированная отложенная выборка 80/20;
- контроль перестановкой меток;
- анализ почти-дублей;
- маскирование содержательных слов;
- коррекция априорных вероятностей классов;
- мягкое голосование и стековое объединение.
### 3.6. Метрики
Основная метрика — macro-F1. Дополнительно использовались:
- доля правильных ответов (accuracy);
- сбалансированная доля правильных ответов;
- коэффициент корреляции Мэтьюса (MCC);
- каппа Коэна;
- точность попадания истинного класса в тройку и пятёрку лучших;
- логарифмическая потеря и оценка Брайера для вероятностных моделей;
- точность, полнота и F-мера по каждому классу;
- матрица ошибок;
- бутстрэп-интервалы;
- критерий Мак-Немара, критерий Уилкоксона, критерий Фридмана и поправка
Холма—Бонферрони.
Случайный уровень для 34 классов: macro-F1 ≈ 0,029, top-3 ≈ 0,088,
top-5 ≈ 0,147.
---
## 4. Эксперименты
### 4.1. Исходная серия
Исходная серия включала:
- нижние границы: длина, символьные n-граммы, POS, функциональные слова;
- ручную стилометрию;
- объединение ручных признаков с символьными и частеречными n-граммами;
- варианты LSA;
- сравнение классификаторов;
- поочерёдное исключение групп признаков;
- дельту Барроуза;
- верификацию «свой/чужой»;
- кластеризацию;
- анализ длины и подгрупп.
### 4.2. Follow-up серия коллеги
В follow-up серии было выполнено 68 запусков. Основные результаты:
| Конфигурация | Macro-F1 | Доля правильных ответов | Top-3 |
|---|---:|---:|---:|
| `char_max10000_rf` | 0,2583 ± 0,0066 | 0,2391 | 0,4197 |
| `model_et_default` | 0,2551 ± 0,0073 | 0,2387 | 0,4170 |
| `groups_lean2_content300` | 0,2510 ± 0,0040 | 0,2379 | 0,4111 |
| `stylometry_pos_content300` | 0,2485 ± 0,0042 | 0,2337 | 0,4175 |
| `ref_logreg` | 0,1785 ± 0,0060 | 0,1679 | 0,3322 |
| `stylometry` | 0,1051 ± 0,0038 | 0,1012 | 0,2338 |
| `lsa_content_k400` | 0,2114 ± 0,0029 | 0,2059 | 0,3767 |
| `lsa_pos_k300` | 0,0667 ± 0,0040 | 0,0663 | 0,1556 |
Контроль перестановкой меток дал 0,0270–0,0282, то есть случайный уровень.
Почти-дубликаты (13 пар с косинусной близостью ≥ 0,8) оказались внутри одного
персонажа; межклассовых почти-дублей не обнаружено.
### 4.3. Новые эксперименты
#### 4.3.1. Вложенный выбор конфигурации
Во внешнем цикле из 5 блоков и внутреннем цикле из 3 блоков сравнивались
восемь конфигураций:
- ExtraTrees на полном наборе;
- ExtraTrees без LSA;
- ExtraTrees с lean-набором и `content`-LSA (300 и 400 компонент);
- логистическая регрессия с lean-набором и `content`-LSA(300);
- случайный лес с `char_max_features` 10000 и 15000;
- ExtraTrees с `char_max_features=10000`.
Выбор выполнялся только по внутренним блокам. Результат:
- macro-F1 = **0,2746 ± 0,0233**;
- в 14 из 15 внешних блоков выбрана конфигурация ExtraTrees с
`char_max_features=10000`;
- в 1 из 15 — ExtraTrees с lean-набором и `content`-LSA(400).
Это наиболее консервативная оценка, поскольку конфигурация выбирается внутри
обучающей части каждого внешнего блока.
#### 4.3.2. Финальные кандидаты
| Конфигурация | Macro-F1 | Доля правильных ответов | Top-3 |
|---|---:|---:|---:|
| ExtraTrees, `char_max_features=10000` | **0,2757 ± 0,0219** | 0,2633 | 0,4554 |
| ExtraTrees, полный набор | 0,2499 ± 0,0209 | 0,2374 | 0,4234 |
| ExtraTrees, lean + content-LSA(300) | 0,2539 ± 0,0237 | 0,2354 | 0,4339 |
| ExtraTrees, lean + content-LSA(400) | 0,2511 ± 0,0212 | 0,2279 | 0,4219 |
| LogisticRegression, lean + content-LSA(300) | 0,2466 ± 0,0191 | 0,2349 | 0,4030 |
#### 4.3.3. Мягкое голосование и стековое объединение
Ансамбль из трёх моделей:
- ExtraTrees на полном наборе;
- ExtraTrees с `char_max_features=10000`;
- логистическая регрессия с lean-набором и `content`-LSA(300).
Результат мягкого голосования:
- macro-F1 = **0,3036 ± 0,0189**;
- доля правильных ответов = 0,2808;
- top-3 = 0,4823.
Вариант с четырьмя моделями (добавлен случайный лес с
`char_max_features=10000`) дал 0,2983 ± 0,0187; статистически он не лучше
трёхмодельного варианта.
Стековое объединение с мета-классификатором логистической регрессии дало
0,2259 ± 0,0163, то есть заметно хуже мягкого голосования. Это указывает на
переобучение мета-классификатора при малом числе объектов.
#### 4.3.4. Контроль содержания
Для проверки того, обусловлен ли результат темой, содержательные слова
(существительные, глаголы, прилагательные, наречия, числительные) были
заменены на нейтральный маркер `CONTENT`. Служебные слова, пунктуация и
частеречные теги сохранялись.
| Условие | Модель | Macro-F1 |
|---|---|---:|
| Исходный текст, стилевой набор | ExtraTrees | 0,2483 ± 0,0201 |
| Маскированный текст, стилевой набор | ExtraTrees | 0,1129 ± 0,0162 |
| Исходный текст, стилевой набор | LogisticRegression | 0,1467 ± 0,0213 |
| Маскированный текст, стилевой набор | LogisticRegression | 0,1067 ± 0,0125 |
Снижение для ExtraTrees составляет около 0,135 macro-F1, то есть более
половины качества. Стилевой сигнал после маскирования сохраняется, но он
существенно слабее содержательного.
#### 4.3.5. Коррекция априорных вероятностей
Проверена коррекция вероятностей вида
\[
p'_{ic} \propto p_{ic} \cdot \pi_c^{-\alpha},
\]
где \(\pi_c\) — априорная частота класса. Параметр \(\alpha\) выбирался по
схеме «один повтор — отложенный, остальные — обучающие».
Среднее качество:
- без коррекции: 0,2529;
- с коррекцией: 0,2507;
- среднее изменение: −0,0022.
Простая коррекция априорных вероятностей не улучшает macro-F1. Это означает,
что проблема редких классов не решается только изменением приоров; нужны
более тонкие правила принятия решений или дополнительные данные.
#### 4.3.6. Дополнительные конфигурации
| Конфигурация | Macro-F1 |
|---|---:|
| RandomForest, `char_max_features=15000` | 0,2570 ± 0,0139 |
| LogisticRegression, lean + content-LSA(400) | 0,2532 ± 0,0188 |
| LogisticRegression, lean + content-LSA(500) | 0,2542 ± 0,0180 |
| ExtraTrees с весами блоков | 0,2478 ± 0,0208 |
Увеличение символьного словаря с 10000 до 15000 не дало прироста.
Увеличение числа компонент `content`-LSA с 300 до 500 также не дало
устойчивого улучшения. Веса блоков не улучшили результат.
#### 4.3.7. Отложенная выборка
На стратифицированной отложенной выборке 80/20 (401 объект):
| Модель | Macro-F1 |
|---|---:|
| Мягкое голосование трёх моделей | **0,3333** |
| ExtraTrees, полный набор | 0,2793 |
| RandomForest, `char_max_features=10000` | 0,2554 |
| LogisticRegression, lean + content-LSA(300) | 0,2459 |
Отложенная выборка даёт более высокие значения, чем перекрёстная проверка,
но это одно разбиение; его интервал широкий, поэтому результаты нельзя
считать окончательными.
---
## 5. Результаты и обсуждение
### 5.1. Основной результат
Персонажи статистически различимы. Лучшая оценка по перекрёстной проверке —
0,304 macro-F1 для мягкого голосования; консервативная оценка с вложенным
выбором конфигурации — 0,275. Случайный уровень — 0,029.
Для интерпретации удобно перевести macro-F1 в кратность превышения
случайного уровня:
- 0,275 / 0,029 ≈ 9,5;
- 0,304 / 0,029 ≈ 10,5.
Это заметный сигнал, но не достаточный для практической системы
автоматического определения персонажа по одной реплике.
### 5.2. Что даёт основной вклад
Сравнение условий:
| Условие | Macro-F1 |
|---|---:|
| Только длина | 0,035 |
| Только ручные стилометрические признаки | 0,105 |
| Ручные + символьные и частеречные n-граммы | 0,182 |
| То же + word/pos-LSA(100) | 0,1785 |
| То же + content-LSA(300) | 0,2485 |
| ExtraTrees с расширенным символьным словарём | 0,276 |
| Мягкое голосование | 0,304 |
Основной прирост дают:
1. символьные n-граммы;
2. `content`-LSA, то есть содержание и тема;
3. нелинейная модель (деревья) для совместного использования признаков.
Функциональные слова и POS-LSA сами по себе слабы. Это важный
отрицательный результат: «чистый синтаксис» в текущем виде недостаточен.
### 5.3. Стиль или содержание
Контроль содержания показывает:
- исходный стилевой набор: 0,2483;
- после маскирования содержательных слов: 0,1129.
Следовательно, более половины качества связано с лексическим содержанием.
Остающийся сигнал 0,113 в 3,9 раза выше случайного уровня 0,029, то есть
некоторый стилевой компонент присутствует, но он существенно слабее
тематического.
Корректная формулировка:
> Персонажи различимы по лексико-тематическому профилю и символьным
> n-граммам; наличие собственно стилистического сигнала подтверждается лишь
> частично и требует дальнейшего тематического контроля.
### 5.4. Ошибки модели
Для лучшей одиночной модели ExtraTrees с `char_max_features=10000`
(первый повтор, 2005 реплик):
- Abigail получает 17,7% всех предсказаний при истинной доле 6,9%;
precision 0,099, recall 0,252, F1 0,142;
- худшие F1: Kent 0,000, Wizard 0,000, Penny 0,021, Mister Qi 0,087,
Leah 0,110, Abigail 0,142, Linus 0,146, Jodi 0,147;
- лучшие F1: Evelyn 0,646, Willy 0,581, Pam 0,522, Pierre 0,513,
Leo 0,508, Vincent 0,438.
Мягкое голосование улучшает часть редких классов: Wizard F1 возрастает до
0,30, Dwarf — до 0,30, Vincent — до 0,46. Однако Kent по-прежнему имеет
F1 = 0, а Abigail остаётся «hub»-классом.
Основные потоки ошибок: Sebastian → Abigail (31), Sam → Abigail (22),
Alex → Abigail (22), Elliott → Abigail (21). Это не демографическая
путаница, а коллапс редких классов в несколько популярных.
### 5.5. Длина реплики
Для ExtraTrees с расширенным символьным словарём:
| Длина | Macro-F1 |
|---|---:|
| 0–5 слов | 0,097 |
| 6–10 | 0,244 |
| 11–15 | 0,233 |
| 16–20 | 0,239 |
| 21–30 | **0,295** |
| 31+ | 0,271 |
Зависимость немонотонная: максимум приходится на 21–30 слов, после 30 слов
качество снижается. Гипотеза «чем длиннее, тем лучше» не подтверждается.
### 5.6. Подгруппы
Для ExtraTrees с расширенным символьным словарём:
- возраст: elder 0,440, child 0,424, adult 0,249, teen 0,201;
- пол: male 0,282, unknown 0,242, female 0,236;
- вид: monster 0,287, human 0,263, unknown 0,033.
Это F1 внутри группы персонажей, а не качество предсказания группы.
Различия описательные; они не доказывают, что модель обязана путать
персонажей внутри группы.
### 5.7. Верификация и кластеризация
- Верификация: ROC-AUC 0,630–0,633, EER 0,412–0,415, top-1 0,133–0,141.
- Кластеризация: ARI = 0, NMI 0,14–0,53 для k = 3/5/8.
Обе постановки показывают, что в признаковом пространстве нет устойчивых
кластеров, соответствующих персонажам. Это ограничивает интерпретацию
результатов классификации.
### 5.8. Статистическая надёжность
Мягкое голосование статистически лучше одиночной модели ExtraTrees с
`char_max_features=10000` на общих повторах:
| Повтор | Разница macro-F1 | McNemar p |
|---|---:|---:|
| 0 | +0,0287 | 0,012 |
| 1 | +0,0380 | < 0,00001 |
| 2 | +0,0163 | 0,0012 |
Разница положительна во всех трёх повторах. Однако состав ансамбля
выбирался по предыдущим результатам, поэтому окончательная оценка должна
быть подтверждена внешней выборкой.
При 46 парных сравнениях необходима поправка Холма—Бонферрони. Эффекты
+0,04…+0,08 её переживают; малые различия +0,003…+0,009 — нет.
---
## 6. Ограничения
1. **Один корпус.** Все результаты получены на 2005 репликах одной игры.
Внешняя валидация на другом корпусе не проводилась.
2. **Один автор.** Работа измеряет различимость речевых масок, а не
авторскую атрибуцию.
3. **Выбор признаков.** `char_max_features=10000`, `content`-LSA(k=300–400) и
lean-наборы выбирались по результатам на тестовых блоках предыдущих серий.
Вложенный выбор конфигурации снижает этот риск (0,275), но не устраняет
его полностью.
4. **Дисбаланс классов.** От 15 до 139 реплик; редкие классы имеют широкие
интервалы, а hub-классы забирают чужие предсказания.
5. **Короткие тексты.** Признаки словарного богатства и читаемости
нестабильны.
6. **Тематический конфликт.** `content`-LSA и символьные n-граммы могут
кодировать тему, сезоны, подарки, работу и имена. Маскирование содержания
подтверждает сильный тематический вклад.
7. **Нет идентификаторов сцен.** Разбиение по отдельным репликам не
учитывает корреляцию внутри сцен и диалогов.
8. **Статистическая мощность.** При 34 классах и 2005 объектах даже
доверительные интервалы по редким классам остаются широкими.
9. **Калибровка.** Простая коррекция априорных вероятностей не улучшила
macro-F1; более сложные методы (temperature scaling, оптимизация порогов)
не проверялись.
10. **Стековое объединение** показало худший результат, вероятно, из-за
переобучения мета-классификатора на малом числе объектов.
---
## 7. Выводы
1. **Гипотеза о различимости персонажей подтверждается частично.**
Консервативная оценка — 0,275 macro-F1; лучшая наблюдаемая — 0,304.
Случайный уровень — 0,029.
2. **Основной сигнал — лексико-тематический.** Символьные n-граммы и
`content`-LSA дают основной прирост; функциональные слова и POS-LSA сами
по себе слабы. Маскирование содержательных слов снижает качество более
чем вдвое.
3. **Наиболее устойчивая одиночная модель — ExtraTrees с
`char_max_features=10000`.** Она выбрана в 14 из 15 внешних блоков
вложенной процедуры и даёт 0,2757 ± 0,0219.
4. **Мягкое голосование трёх моделей — лучший наблюдаемый результат:**
0,3036 ± 0,0189 при перекрёстной проверке и 0,3333 на отложенной
выборке. Однако состав ансамбля выбирался по предыдущим данным; для
строгого вывода нужна внешняя валидация.
5. **`content`-LSA — сильный, но тематический сигнал.** Рост до k=400
устойчив, но интерпретировать его как «стиль» нельзя без тематического
контроля.
6. **Главная проблема — редкие классы и hub-коллапс.** Abigail получает
21–23% предсказаний при истинной доле 6,9%; Kent, Wizard, Penny и
Mister Qi почти не находятся. Простая коррекция приоров не помогает.
7. **Для строгого стилометрического вывода необходимы:**
внешний корпус, тематически сбалансированные подвыборки, вложенный выбор
признаков и более тонкая калибровка решающих правил.
---
## 8. Воспроизводимость
### 8.1. Основные документы
- `README.md` — установка и быстрый старт;
- `RUNBOOK.md` — пошаговая инструкция для полной серии;
- `EXPERIMENTS.md` — протокол экспериментов;
- `FEATURES.md` — описание признаков;
- `METRICS_AND_CLASSIFIERS.md` — описание метрик и классификаторов;
- `TUNING.md` — параметры и порядок настройки;
- `FOLLOWUP_EXPERIMENTS.md` — follow-up серия коллеги;
- `RESULTS_ANALYSIS.md` — анализ follow-up результатов;
- `FINAL_RESULTS.csv` — сводная таблица ключевых результатов;
- этот отчёт — `FINAL_REPORT.md`.
### 8.2. Команды
```bash
cd lab1
# базовая серия
bash scripts/run_all_experiments.sh
# follow-up серия
python scripts/run_followup_experiments.py # если доступен в ветке результатов
# или
bash scripts/run_followup_experiments.sh
# новые эксперименты
python scripts/run_final_experiments.py \
--stages nested topic calibration ensemble extra \
--out results/final
# финалисты и ансамбли
python scripts/run_finalists.py
```
### 8.3. Артефакты
- `lab1/exports/nlp_lab1_results_20261026_054117.tar.gz` — архив follow-up
серии;
- `results/final/` — новые эксперименты;
- `results/final/nested_3repeats/` — вложенный выбор конфигурации;
- `results/final/finalists/` — финалисты и ансамбли;
- `results/final/topic_control/` — контроль содержания;
- `results/final/calibration/` — коррекция приоров;
- `results/final/extra/` — дополнительные конфигурации и отложенная выборка.
---
## 9. Направления дальнейшей работы
1. **Внешняя валидация** на другом корпусе, другой игре или другом разбиении
по сценам и диалогам.
2. **Тематический контроль:** сбалансированные по темам подвыборки, удаление
тематических слов, adversarial-классификатор темы.
3. **Вложенный выбор признаков и ансамблей** внутри внешнего цикла, включая
`char_max_features`, `content`-LSA(k) и состав ансамбля.
4. **Калибровка редких классов:** temperature scaling, bias correction,
оптимизация порогов и правил декодирования по macro-F1.
5. **Ансамбли:** мягкое голосование с весами, stacking с регуляризацией,
отбор базовых моделей по внутренней проверке.
6. **Расширение корпуса:** больше реплик на редких персонажей, разметка сцен
и диалогов, возможно, другие игры или авторы для внешней проверки.
7. **Улучшение частеречной разметки:** сравнение теггеров, ручная проверка
ошибок на разговорных конструкциях.
8. **Содержательная интерпретация:** анализ тем, характерных для персонажей,
и отделение тематического сигнала от собственно стилистического.
---
## Приложение A. Сводная таблица результатов
См. `FINAL_RESULTS.csv`.
## Приложение B. Основные метрики
См. `METRICS_AND_CLASSIFIERS.md`.
## Приложение C. Признаки
См. `FEATURES.md` и `features/feature_dictionary.csv`.
## Приложение D. Протокол экспериментов
См. `EXPERIMENTS.md` и `FOLLOWUP_EXPERIMENTS.md`.

View File

@@ -1,30 +0,0 @@
stage,label,model,folds,repeats,macro_f1_mean,macro_f1_std,accuracy,top3,note
previous,ref_logreg,logreg,5,5,0.1785,0.0060,0.1679,0.3322,базовый объединённый набор
previous,ref_no_lsa,logreg,5,5,0.1815,0.0054,0.1708,0.3395,без LSA
previous,stylometry,logreg,5,5,0.1051,0.0038,0.1012,0.2338,только ручные признаки
previous,char_max10000_rf,random_forest,5,5,0.2583,0.0066,0.2391,0.4197,предыдущий максимум
previous,model_et_default,extra_trees,5,5,0.2551,0.0073,0.2387,0.4170,деревья по умолчанию
previous,groups_lean2_content300,logreg,5,5,0.2510,0.0040,0.2379,0.4111,lean + content-LSA
previous,lsa_content_k400,logreg,5,5,0.2114,0.0029,0.2059,0.3767,только content-LSA
previous,lsa_pos_k300,logreg,5,5,0.0667,0.0040,0.0663,0.1556,только POS-LSA
nested,nested_feature_selection,mixed,5,3,0.2746,0.0233,0.2603,NA,выбор конфигурации внутри outer-фолдов
finalists,et_char10000,extra_trees,5,5,0.2757,0.0219,0.2633,0.4554,char_max_features=10000
finalists,et_full,extra_trees,5,5,0.2499,0.0209,0.2374,0.4234,полный набор
finalists,et_lean_content300,extra_trees,5,5,0.2539,0.0237,0.2354,0.4339,lean + content-LSA 300
finalists,et_lean_content400,extra_trees,5,5,0.2511,0.0212,0.2279,0.4219,lean + content-LSA 400
finalists,logreg_lean_content300,logreg,5,5,0.2466,0.0191,0.2349,0.4030,lean + content-LSA 300
ensemble,voting_et_char_lean,soft_voting,5,3,0.3036,0.0189,0.2808,0.4823,ET full + ET char10000 + logreg lean content300
ensemble,voting_four,soft_voting,5,3,0.2983,0.0187,0.2823,0.4813,то же + RF char10000
ensemble,stacking,stacking,5,1,0.2259,0.0163,0.2264,0.3865,мета-классификатор logreg
topic,topic_original_et,extra_trees,5,3,0.2483,0.0201,0.2379,0.4208,исходный текст; стилевой набор
topic,topic_masked_et,extra_trees,5,3,0.1129,0.0162,0.1220,0.2683,содержательные слова заменены на CONTENT
topic,topic_original_logreg,logreg,5,3,0.1467,0.0213,0.1440,0.3017,исходный текст; стилевой набор
topic,topic_masked_logreg,logreg,5,3,0.1067,0.0125,0.1046,0.2362,содержательные слова заменены на CONTENT
extra,rf_char15000,random_forest,5,3,0.2570,0.0139,0.2468,NA,увеличение char-словаря не даёт прироста
extra,logreg_lean_content400,logreg,5,3,0.2532,0.0188,0.2389,NA,content-LSA 400
extra,logreg_lean_content500,logreg,5,3,0.2542,0.0180,0.2401,NA,content-LSA 500
extra,et_block_weights,extra_trees,5,3,0.2478,0.0208,0.2366,NA,веса блоков не улучшают
holdout,voting_holdout,soft_voting,1,1,0.3333,NA,0.2968,NA,стратифицированная отложенная выборка 80/20
holdout,et_full_holdout,extra_trees,1,1,0.2793,NA,0.2469,NA,стратифицированная отложенная выборка 80/20
holdout,rf_char10000_holdout,random_forest,1,1,0.2554,NA,0.2369,NA,стратифицированная отложенная выборка 80/20
holdout,logreg_lean_content300_holdout,logreg,1,1,0.2459,NA,0.2269,NA,стратифицированная отложенная выборка 80/20
1 stage label model folds repeats macro_f1_mean macro_f1_std accuracy top3 note
2 previous ref_logreg logreg 5 5 0.1785 0.0060 0.1679 0.3322 базовый объединённый набор
3 previous ref_no_lsa logreg 5 5 0.1815 0.0054 0.1708 0.3395 без LSA
4 previous stylometry logreg 5 5 0.1051 0.0038 0.1012 0.2338 только ручные признаки
5 previous char_max10000_rf random_forest 5 5 0.2583 0.0066 0.2391 0.4197 предыдущий максимум
6 previous model_et_default extra_trees 5 5 0.2551 0.0073 0.2387 0.4170 деревья по умолчанию
7 previous groups_lean2_content300 logreg 5 5 0.2510 0.0040 0.2379 0.4111 lean + content-LSA
8 previous lsa_content_k400 logreg 5 5 0.2114 0.0029 0.2059 0.3767 только content-LSA
9 previous lsa_pos_k300 logreg 5 5 0.0667 0.0040 0.0663 0.1556 только POS-LSA
10 nested nested_feature_selection mixed 5 3 0.2746 0.0233 0.2603 NA выбор конфигурации внутри outer-фолдов
11 finalists et_char10000 extra_trees 5 5 0.2757 0.0219 0.2633 0.4554 char_max_features=10000
12 finalists et_full extra_trees 5 5 0.2499 0.0209 0.2374 0.4234 полный набор
13 finalists et_lean_content300 extra_trees 5 5 0.2539 0.0237 0.2354 0.4339 lean + content-LSA 300
14 finalists et_lean_content400 extra_trees 5 5 0.2511 0.0212 0.2279 0.4219 lean + content-LSA 400
15 finalists logreg_lean_content300 logreg 5 5 0.2466 0.0191 0.2349 0.4030 lean + content-LSA 300
16 ensemble voting_et_char_lean soft_voting 5 3 0.3036 0.0189 0.2808 0.4823 ET full + ET char10000 + logreg lean content300
17 ensemble voting_four soft_voting 5 3 0.2983 0.0187 0.2823 0.4813 то же + RF char10000
18 ensemble stacking stacking 5 1 0.2259 0.0163 0.2264 0.3865 мета-классификатор logreg
19 topic topic_original_et extra_trees 5 3 0.2483 0.0201 0.2379 0.4208 исходный текст; стилевой набор
20 topic topic_masked_et extra_trees 5 3 0.1129 0.0162 0.1220 0.2683 содержательные слова заменены на CONTENT
21 topic topic_original_logreg logreg 5 3 0.1467 0.0213 0.1440 0.3017 исходный текст; стилевой набор
22 topic topic_masked_logreg logreg 5 3 0.1067 0.0125 0.1046 0.2362 содержательные слова заменены на CONTENT
23 extra rf_char15000 random_forest 5 3 0.2570 0.0139 0.2468 NA увеличение char-словаря не даёт прироста
24 extra logreg_lean_content400 logreg 5 3 0.2532 0.0188 0.2389 NA content-LSA 400
25 extra logreg_lean_content500 logreg 5 3 0.2542 0.0180 0.2401 NA content-LSA 500
26 extra et_block_weights extra_trees 5 3 0.2478 0.0208 0.2366 NA веса блоков не улучшают
27 holdout voting_holdout soft_voting 1 1 0.3333 NA 0.2968 NA стратифицированная отложенная выборка 80/20
28 holdout et_full_holdout extra_trees 1 1 0.2793 NA 0.2469 NA стратифицированная отложенная выборка 80/20
29 holdout rf_char10000_holdout random_forest 1 1 0.2554 NA 0.2369 NA стратифицированная отложенная выборка 80/20
30 holdout logreg_lean_content300_holdout logreg 1 1 0.2459 NA 0.2269 NA стратифицированная отложенная выборка 80/20

View File

@@ -1,636 +0,0 @@
# Follow-up эксперименты
Документ продолжает `EXPERIMENTS.md`. Здесь зафиксировано, что показали
предварительные прогоны в `results/`, какие вопросы остались открытыми и какая
серия экспериментов их закрывает. Запуск серии — `scripts/run_followup_experiments.sh`,
сводка — `scripts/analyze_followup.py`.
---
## 1. Что показали текущие прогоны
### 1.1. Важная методическая поправка
В `run_metadata.json`, `benchmark/summary.csv`, `ablation.csv`,
`lsa_sweep.csv`, `delta_sweep.csv` и `length_bins.csv` основные числа часто
берутся из **repeat 0**, а не как среднее по повторам. Например, у основного
запуска `stylometry_pos_lsa/logreg` `run_metadata.metrics.macro_f1 = 0.1798`,
но среднее по пяти повторам — `0.1785 ± 0.0060`, а по 25 фолдам —
`0.1755 ± 0.0189`. Разница между repeat 0 и средним может быть больше, чем
эффект, который мы пытаемся измерить.
Поэтому в таблице ниже числа пересчитаны по `pooled_metrics.csv` (среднее по
повторам). Новый скрипт `scripts/analyze_followup.py` делает то же самое для
всех follow-up запусков.
### 1.2. Сводка результатов
| Постановка | Macro-F1, mean ± std | Повторов | Комментарий |
|---|---:|---:|---|
| `random_forest` по умолчанию | **0.2180 ± 0.0053** | 3 | лучший наблюдавшийся результат |
| `random_forest`, depth 30, leaf 1 | 0.2007 ± 0.0040 | 3 | ручная сетка не проверяла `max_depth=None` |
| `linear_svc`, C = 0.3 | 0.1878 ± 0.0052 | 3 | |
| `linear_svc`, C = 1 | 0.1871 ± 0.0048 | 3 | |
| `logreg`, nested CV | 0.1827 | 1 outer | настройка внутри фолдов |
| `stylometry_pos` / `logreg` | 0.1815 ± 0.0054 | 5 | без LSA |
| `stylometry_pos_lsa` / `logreg` | 0.1785 ± 0.0060 | 5 | LSA не улучшает |
| `logreg`, C = 10 | 0.1843 ± 0.0048 | 3 | |
| `lsa_content` / `logreg` | 0.1324 ± 0.0011 | 3 | тема даёт сигнал, но меньше полного набора |
| `lsa_char` / `logreg` | 0.1127 ± 0.0007 | 3 | |
| `stylometry` (только ручные) | 0.1051 ± 0.0038 | 5 | |
| `lsa_word` / `logreg` | 0.0943 ± 0.0020 | 3 | |
| `fw` / `logreg` | 0.0598 ± 0.0039 | 3 | |
| `lsa_pos` / `logreg` | 0.0478 ± 0.0012 | 3 | |
| `length` / `logreg` | 0.0354 ± 0.0032 | 5 | |
| `char` / `multinomial_nb` | 0.0274 ± 0.0018 | 3 | |
| `pos` / `multinomial_nb` | 0.0203 ± 0.0012 | 3 | |
| `length` / `dummy` | 0.0038 | 1 | |
Случайный уровень для 34 классов — около 0.029 по macro-F1. Почти все
осмысленные модели выше него, но разброс между повторами и seed-ами
сопоставим с разницей между многими конфигурациями.
### 1.3. Что уже можно утверждать
1. **Символьные и POS n-граммы — основной сигнал.** Переход от `stylometry`
(0.105) к `stylometry_pos` (0.182) даёт около +0.077 macro-F1. Это больше,
чем даёт любой LSA-блок.
2. **LSA в текущем виде не оправдан.** `stylometry_pos` (0.1815) и
`stylometry_pos_lsa` (0.1785) статистически неразличимы: McNemar
`p = 0.86`, bootstrap-разница `+0.0021 [-0.0108, +0.0157]`. LSA стоит
либо убрать, либо пересобрать (другие варианты, число компонент, веса).
3. **Random forest по умолчанию лучше линейных моделей.** 0.2180 против
0.1815–0.1878. При этом ручная сетка `max_depth ∈ {10, 20, 30}` не
проверяла `max_depth=None`, поэтому «лучшая» точка сетки (0.2007) хуже
дефолта. Нужна сетка вокруг дефолта: `max_depth=None`, `min_samples_leaf`,
`max_features`, `n_estimators`, `class_weight`.
4. **Абляция намекает на шумные группы.** При 2 повторах удаление
`function_words` (+0.0041), `pos` (+0.0026), `lexical` (+0.0020) не
ухудшало результат. Это в пределах шума, но достаточно, чтобы проверить
«худые» наборы на 5 повторах.
5. **Качество зависит от длины.** Для основного запуска (repeat 0):
31+ слов — 0.2055, 21–30 — 0.1781, 11–15 — 0.1385. Гипотеза H4
поддерживается для длинных реплик; группа 0–5 слов (n = 42) имеет слишком
широкий интервал.
6. **Ошибки группируются.** Частые пары: Abigail↔Haley, Emily↔Leah/Penny,
Sam↔Sebastian/Alex, Vincent↔Jas. Это согласуется с возрастными и
гендерными подгруппами, но не доказывает, что модель обязана их путать.
7. **Класс-уровень неоднороден.** Худшие F1 (repeat 0): Kent 0.00,
Marnie 0.035, Jodi 0.075, Penny 0.091, Maru 0.093. Лучшие: Willy 0.393,
Evelyn 0.367, Vincent 0.333. Редкие классы (Mister Qi — 15 реплик,
Sandy — 21, Dwarf — 23, Wizard — 24) дают широкие интервалы.
8. **Верификация и кластеризация слабые.** ROC-AUC = 0.630, EER = 0.412,
TAR@FAR=1% = 0.035, top-1 = 0.133. Кластеризация: ARI = 0, NMI ≈ 0.31–0.35.
Это ограничивает интерпретацию: 34-классовая задача различает персонажей
лишь частично, и «идеальных» групп нет.
9. **Near-duplicates почти нет.** При cosine ≥ 0.9 по char 3–5 всего 3 пары,
при ≥ 0.95 — одна, и все они внутри одного персонажа. Утечки между
классами через почти-дубли не видно; `StratifiedGroupKFold` не обязателен,
но проверка включена в follow-up как диагностика.
Follow-up пересмотрел пункты 2, 3 и 5: LSA оказался полезен в варианте
`content` и при k = 300–400, ExtraTrees обошёл random forest, а зависимость
качества от длины реплики оказалась немонотонной. Подробности и числа — в §7.
---
## 2. Открытые вопросы и гипотезы follow-up
| Обозначение | Гипотеза | Как проверяем |
|---|---|---|
| F-H1 | Настроенный random forest устойчиво лучше линейных моделей на ≥ 0.02 macro-F1 | F1, F2, F7; парные сравнения по повторам |
| F-H2 | LSA-блок не улучшает `stylometry_pos`; content-LSA отражает тему, а не стиль | F3; `stylometry_pos` vs `+content` vs `+pos` vs `+word/pos` |
| F-H3 | «Худые» наборы без `function_words`, `pos`, `vocabulary`, `readability` не хуже полного | F4; абляция на 5 повторах + lean-конфиги |
| F-H4 | Настройка `min_df`, `max_features`, `ngram_range` для char/POS даёт ≥ 0.01 | F5; сетка на 3 повторах, финалисты на 5 |
| F-H5 | `class_weight="balanced"` повышает macro-F1, но снижает accuracy; компромисс нужно измерить | F6; balanced vs none для logreg/SVC/RF |
| F-H6 | Ранжирование моделей устойчиво к seed | F7; seeds 7, 2024, 1337 |
| F-H7 | Ошибки концентрируются внутри возрастных/гендерных групп | F8; subgroup-report и анализ confusions |
---
## 3. Матрица экспериментов
Все запуски идут с `folds=5`. Опорный seed — 42. Если не указано иное,
повторность 5. Полный список команд и параметров — в
`scripts/run_followup_experiments.sh`; ниже — назначение стадий.
### F0. Воспроизведение опорных результатов
| Label | Что запускается | Повторов | Зачем |
|---|---|---:|---|
| `ref_logreg` | `stylometry_pos_lsa` + logreg | 5 | опорная точка 0.1785 |
| `ref_nested_logreg` | то же, `--nested` | 1 outer | честная оценка с настройкой C |
| `ref_no_lsa` | `stylometry_pos` + logreg | 5 | вклад LSA |
| `ref_stylometry` | `stylometry` + logreg | 5 | вклад n-грамм |
### F1. Сравнение моделей
| Label | Модель | Повторов |
|---|---|---:|
| `model_rf_default` | random forest по умолчанию | 5 |
| `model_et_default` | extra trees по умолчанию | 5 |
| `model_svc_default` | linear SVC, C=1 | 5 |
| `model_knn_default` | kNN | 3 |
| `model_rf_nested`, `model_et_nested`, `model_svc_nested` | те же с `--nested` | 1 outer |
### F2. Сетка вокруг random forest
Сетка строится от дефолта, потому что именно дефолт показал лучший результат.
- `max_depth ∈ {None, 30, 40}`, `min_samples_leaf ∈ {1, 2, 3}`;
- `max_features ∈ {sqrt, 0.05, 0.1}`, `n_estimators ∈ {400, 800}`;
- `class_weight ∈ {balanced_subsample (default), balanced, none}`;
- extra trees: `max_depth ∈ {None, 30}`, `min_samples_leaf ∈ {1, 2}`.
Повторность 3. Лучшая точка сетки выбирается автоматически по среднему pooled
macro-F1, затем перезапускается на `REPEATS_MAIN` повторах как
`rf_grid_best_5` и сравнивается с `model_rf_default` парно. Выбор точки по
тестовым метрикам остаётся разведочным: честная оценка — только по
`final_rf_nested`.
### F3. LSA: тема против стиля
| Label | Конфигурация | Повторов |
|---|---|---:|
| `lsa_sweep` | варианты content/char/word/pos/fw, k ∈ {100, 200, 300, 400} | 3 |
| `lsa_content_k300`, `lsa_content_k400` | LSA content отдельно | 5 |
| `lsa_char_k300`, `lsa_pos_k300` | LSA char/pos отдельно | 5 |
| `stylometry_pos_content300` | stylometry_pos + content-LSA(k=300) | 5 |
| `stylometry_pos_pos300` | stylometry_pos + POS-LSA(k=300) | 5 |
| `stylometry_pos_lsa_k300` | stylometry_pos + word/pos-LSA(k=300) | 5 |
Ключевое сравнение — `stylometry_pos_content300` vs `ref_no_lsa`. Если
content-LSA не даёт прироста, LSA-блок можно исключить из финальной модели.
Если даёт, это скорее тематический сигнал, и его нужно обсуждать отдельно,
а не выдавать за «стиль».
### F4. Абляция и lean-наборы
| Label | Что исключено |
|---|---|
| `ablate_full` | по одной группе, 5 повторов |
| `groups_lean1` | `function_words`, `pos` |
| `groups_lean2` | `function_words`, `pos`, `vocabulary`, `readability` |
| `groups_lean2_content300` | то же + content-LSA(k=300) |
| `groups_lean1_rf`, `groups_lean2_rf` | лучшие lean-наборы с random forest |
### F5. Символьные и POS n-граммы
- char: `min_df ∈ {2, 5}` (общий параметр для всех TF-IDF/LSA-блоков, не
только для char), `char_max_features ∈ {5000, 10000}`,
`ngram_range ∈ {(2,4), (3,5)}` при базовых `(2,5)`;
- POS: `pos_ngram_range ∈ {(1,2), (1,4)}`, `pos_max_features ∈ {500, 1000}`.
Сетка на logreg, 3 повтора. Два лучших варианта проверяются с random forest
на 5 повторах (`min_df2_rf`, `char_max10000_rf`).
### F6. Балансировка классов
`class_weight="balanced"` (по умолчанию) против `None` для logreg, linear SVC
и random forest; для RF дополнительно `"balanced"` вместо
`"balanced_subsample"`. 5 повторов. Смотреть не только macro-F1, но и
accuracy, balanced accuracy и F1 по редким классам.
### F7. Устойчивость к seed
`ref_logreg` и `model_rf_default` с seed ∈ {7, 2024, 1337} (seed 42 уже есть в
F0/F1). Для RF дополнительно меняется `random_state`. 5 повторов. Если
преимущество RF сохраняется в 3/3 дополнительных seed-ах, вывод устойчив.
### F8. Финальная модель и отчёты
- `final_rf`: random forest, 10 повторов;
- `final_rf_nested`: nested CV для честной оценки;
- `length-report`, `subgroup-report` по gender/age_group/species;
- `compare` с `ref_logreg` и `ref_no_lsa`;
- `verify` для `stylometry_pos_lsa` с seed 42/7/2024;
- `delta` по cosine для 500/1000/2000 признаков;
- `cluster` для k = 3, 5, 8.
### F9. Контроли
- `control_length_logreg` и `control_length_dummy` — нижние границы в той же
схеме CV;
- near-duplicates по char cosine ≥ 0.8 сохраняются в
`results/followup/negative_control/near_duplicates.csv`;
- контроль перестановкой меток: та же модель и CV на перемешанных метках,
seeds 42/7/2024. Эмпирический chance-level должен быть около 0.02–0.03;
если он заметно выше, в пайплайне есть утечка или артефакт.
---
## 4. Как считать результат
1. **Основная метрика** — среднее pooled macro-F1 по повторам, а не repeat 0.
Рядом указывать std, min/max и bootstrap-интервал (последний в текущем коде
считается по repeat 0 — это описательная оценка, не замена разбросу по
повторам).
2. **Парные сравнения** — по общим повторам: разница macro-F1, McNemar на
каждом повторе, bootstrap-разница. В `comparisons.csv` есть
`n_positive`, `mean_mcnemar_p`, `n_ci_excludes_zero`. Сравнение
корректно только при одинаковых разбиениях (тот же seed и число фолдов);
`analyze_followup.py` сопоставляет строки по `line_id` и берёт только общие
номера повторов.
3. **Правило решения (консервативное):** улучшение считаем убедительным,
если одновременно:
- средняя разница ≥ 0.01;
- разница положительна минимум в 4 из 5 повторов (или в 3 из 3, если
повторов 3);
- McNemar даёт p < 0.05 в большинстве повторов;
- bootstrap-интервал разницы не содержит ноль в большинстве повторов.
Если эффект меньше 0.01 или знак скачет, выбираем более простую модель.
4. **Множественные сравнения.** Для итоговых утверждений применять
Holm—Bonferroni к семейству парных p-value; в сводке сырые p-value.
5. **Не выбирать признаки/гиперпараметры по тестовым метрикам.** Сетка F2/F5 —
разведочная; финальные числа берутся из nested CV или из конфигурации,
выбранной до просмотра теста.
6. **Вторичные метрики:** accuracy, balanced accuracy, top-3/top-5,
per-class F1, length bins, subgroup F1, confusions. Macro-F1 — главная, но
не единственная.
---
## 5. Запуск
Из каталога `lab1`:
```bash
# Полная follow-up серия (около 4–4.5 часов на 12 ядрах)
PYTHON=.venv/bin/python bash scripts/run_followup_experiments.sh
# Быстрая проверка пайплайна: 1 повтор, без сетки и seed-робастности
QUICK=1 PYTHON=.venv/bin/python bash scripts/run_followup_experiments.sh
# Без тяжёлой сетки и seed-ов, но с финальной моделью
RUN_GRID=0 RUN_SEEDS=0 PYTHON=.venv/bin/python bash scripts/run_followup_experiments.sh
```
Переменные:
| Переменная | По умолчанию | Значение |
|---|---:|---|
| `PYTHON` | `python` | интерпретатор с зависимостями |
| `FOLLOWUP_DIR` | `results/followup` | куда складывать результаты (удобно для дымового теста) |
| `RUN_ID` | текущее время | суффикс имени лога |
| `QUICK` | 0 | 1 — все повторы 1, без сетки и seed-ов |
| `REPEATS_MAIN` | 5 | опорные запуски и финалисты |
| `REPEATS_GRID` | 3 | сетка RF/ET, kNN, delta |
| `REPEATS_SWEEP` | 3 | LSA-sweep |
| `REPEATS_NGRAM` | 3 | сетка n-грамм |
| `REPEATS_FINAL` | 10 | финальный RF |
| `RUN_GRID` | 1 | стадия F2 |
| `RUN_SEEDS` | 1 | стадия F7 |
| `RUN_FINAL` | 1 | стадия F8 |
| `RUN_CONTROLS` | 1 | стадия F9 |
| `SKIP_TESTS` | 0 | 1 — не запускать pytest |
| `FORCE` | 0 | 1 — перезаписать существующие label-каталоги |
| `SEED` | 42 | опорный seed |
Результаты:
```text
results/followup/
ref_logreg/ # один каталог на эксперимент
run_metadata.json
pooled_metrics.csv
per_class_report.csv
...
command.txt # точная команда
configs/ # сгенерированные TOML
comparisons/
negative_control/
summary.csv
summary.md
comparisons.csv
comparisons.md
manifest.tsv
results/logs/run_followup_<timestamp>.log
```
Сводка пересчитывается отдельно:
```bash
.venv/bin/python scripts/analyze_followup.py \
--followup-dir results/followup \
--pairs final_rf:ref_logreg final_rf:ref_no_lsa model_rf_default:ref_logreg
```
### 5.1. Проверка пайплайна
Серия прогнана целиком в дымовом режиме (1 повтор, 1 seed, временный
`FOLLOWUP_DIR`, 80 минут): все стадии отработали, `summary.csv` собрал 60
запусков с pooled-метриками, `comparisons.md` и JSON по парам сформированы,
`pytest` — 33 теста. Контроль
перестановкой меток дал macro-F1 0.0270–0.0282 (среднее 0.0276) при ожидаемом
случайном уровне ≈ 0.029 — признаков утечки не видно.
Числа дымового прогона в отчёт не идут (1 повтор), но два сигнала стоит
проверить в основной серии: `extra_trees` и `char_max_features=10000` на
одном разбиении обошли RF, а `stylometry_pos + content`-LSA(k=300) — прежний
`stylometry_pos + word/pos`-LSA(k=100). Именно это и проверяют F2, F3 и F5
на 3–5 повторах.
---
## 6. Время
Полная серия измерена на этой машине (12 ядер, sklearn 1.9.1): старт
25.10.2026 21:08:25 MSK, финиш 26.10.2026 01:19:52 MSK, итого
**4 ч 11 мин**. Это меньше первоначальной оценки 5–7 часов, которая была
получена линейным пересчётом дымового прогона и не учитывала, что заметная
часть времени — фиксированные затраты (загрузка корпуса, построение
признаков, отчёты), а не повторная CV.
| Стадия | Время (замер) |
|---|---:|
| F0: окружение, pytest, опорные запуски | 13 мин |
| F1: сравнение моделей + nested | 13 мин |
| F2: сетка RF/ET + F2.1 | 72 мин |
| F3: LSA-sweep + подтверждения | 33 мин |
| F4: абляция + lean-наборы | 43 мин |
| F5: символьные и POS n-граммы | 19 мин |
| F6: class_weight | 10 мин |
| F7: устойчивость к seed | 17 мин |
| F8: финал, verify, delta, cluster | 10 мин |
| F9: контроли, near-duplicates, перестановка, сводка | 20 мин |
`QUICK=1` на этой машине занял около **80 минут** (1 повтор, без сетки
RF/ET и seed-робастности, но со sweep, финалом и отчётами). Время зависит
от числа потоков sklearn и от стоимости LSA; на 12 ядрах `n_jobs=-1`
у GridSearchCV конкурирует с остальными стадиями.
---
## 7. Результаты полной серии
Серия прошла 25.10.2026 21:08:25 — 26.10.2026 01:19:52 MSK (4 ч 11 мин,
12 ядер). В `results/followup/` лежат 66 запусков; после серии отдельно
добавлены два nested-запуска для финалистов (`char_max10000_rf_nested`,
`groups_lean2_content300_nested`). Всего в `summary.csv` 68 строк.
Сводка — `summary.csv` / `summary.md`, парные сравнения —
`comparisons.csv` / `comparisons.md`, лог —
`results/logs/run_followup_20261025_210825.log`.
### 7.1. Ранжирование
`Δ` — разница с `ref_logreg` (0.1785 по 5 повторам). У nested-запусков
в колонке «повторов» стоит 1: это одна вложенная CV из 5 outer-фолдов,
а метрика посчитана по pooled OOF-предсказаниям, поэтому std по повторам
для них не определён.
| Label | Модель | Повторов | Macro-F1, mean ± std | Δ | Accuracy | Balanced acc. |
|---|---|---:|---:|---:|---:|---:|
| `char_max10000_rf` | random_forest | 5 | **0.2583 ± 0.0066** | +0.0798 | 0.2391 | 0.2473 |
| `char_max10000_rf_nested` | random_forest | 1 (5 folds) | **0.2579** | +0.0794 | 0.2394 | 0.2521 |
| `groups_lean2_content300_nested` | logreg | 1 (5 folds) | **0.2572** | +0.0788 | 0.2369 | 0.2587 |
| `model_et_default` | extra_trees | 5 | **0.2551 ± 0.0073** | +0.0767 | 0.2387 | 0.2418 |
| `et_dNone_l2` | extra_trees | 3 | **0.2538 ± 0.0089** | +0.0753 | 0.2416 | 0.2609 |
| `groups_lean2_content300` | logreg | 5 | **0.2510 ± 0.0040** | +0.0725 | 0.2379 | 0.2644 |
| `model_et_nested` | extra_trees | 1 (5 folds) | **0.2486** | +0.0701 | 0.2344 | 0.2478 |
| `stylometry_pos_content300` | logreg | 5 | **0.2485 ± 0.0042** | +0.0700 | 0.2337 | 0.2574 |
| `rf_grid_best_5` | random_forest | 5 | **0.2294 ± 0.0082** | +0.0509 | 0.2253 | 0.2317 |
| `model_rf_default` | random_forest | 5 | **0.2213 ± 0.0065** | +0.0428 | 0.2108 | 0.2114 |
| `final_rf` | random_forest | 10 | **0.2216 ± 0.0062** | +0.0431 | 0.2094 | 0.2112 |
| `final_rf_nested` | random_forest | 1 (5 folds) | **0.2163** | +0.0378 | 0.2055 | 0.2102 |
| `model_svc_default` | linear_svc | 5 | **0.1880 ± 0.0067** | +0.0096 | 0.1840 | 0.1869 |
| `ref_no_lsa` | logreg | 5 | **0.1815 ± 0.0054** | +0.0031 | 0.1708 | 0.1988 |
| `ref_logreg` | logreg | 5 | **0.1785 ± 0.0060** | 0.0000 | 0.1679 | 0.1874 |
| `classweight_logreg_none` | logreg | 5 | **0.1496 ± 0.0077** | −0.0289 | 0.1655 | 0.1367 |
| `ref_stylometry` | logreg | 5 | **0.1051 ± 0.0038** | −0.0733 | 0.1012 | 0.1023 |
| `control_length_logreg` | logreg | 5 | **0.0354 ± 0.0032** | −0.1430 | 0.0395 | 0.0585 |
| `control_length_dummy` | dummy | 1 | **0.0038** | −0.1747 | 0.0693 | 0.0294 |
Случайный уровень для 34 классов ≈ 0.029; контроль перестановкой меток дал
0.0276. Почти все осмысленные конфигурации выше него, но не все различия
между ними значимы.
Главные наблюдения:
1. **Лучшие результаты — деревья с расширенным char-словарём и ExtraTrees.**
`char_max10000_rf` (0.2583) и `model_et_default` (0.2551) идут впереди
дефолтного RF (0.2213) на +0.033…+0.037. Nested-оценки подтверждают
масштаб: 0.2579 для char-RF и 0.2486 для ET. Разница между этими двумя
лидерами мала (≈ +0.003; см. 7.3), поэтому выбирать между ними нужно
по простоте и устойчивости, а не по третьему знаку.
2. **Лучший logreg — `groups_lean2_content300`** (0.2510; nested 0.2572).
Он не уступает деревьям, но выигрывает за счёт `content`-LSA, а не за счёт
«стилевых» признаков. Это важно для интерпретации (см. 7.2 и 7.5).
3. **Сетка вокруг RF почти ничего не дала.** `rf_grid_best_5` = 0.2294,
то есть +0.008 к дефолту, и это в пределах шума. Основной прирост дал
не `max_depth`/`min_samples_leaf`, а `char_max_features=10000`.
4. **LSA нельзя оценивать одной точкой.** word+pos LSA при k=100 был
нейтрален, `content`-LSA при k=300–400 — сильный, POS-LSA — вредный.
Зависимость от k монотонная: content 100 → 0.133, 200 → 0.180,
300 → 0.198, 400 → 0.213 (sweep, 3 повтора).
5. **`class_weight` критичен для logreg.** Без балансировки macro-F1 падает
на 0.029, а accuracy почти не меняется. Для деревьев эффект < 0.005
и не значим.
6. **Разброс по seed мал, разброс по фолдам — нет.** У RF std по повторам
0.003–0.008; у nested logreg отдельные фолды дают 0.219–0.290. Поэтому
одна nested-оценка точнее по среднему, но не даёт интервала по повторам.
Nested-оценка `groups_lean2_content300` (0.2572) даже выше среднего по
5 повторам (0.2510) — это не прирост от настройки `C`, а следствие
конкретного разбиения; сравнивать нужно средние по повторам, а nested
использовать как оценку без оптимизма по гиперпараметрам.
### 7.2. Проверка гипотез F-H1…F-H7
| Гипотеза | Вердикт | Главные числа |
|---|---|---|
| F-H1: настроенный RF устойчиво лучше линейных моделей на ≥ 0.02 | **Подтверждена** | RF vs logreg: +0.0428 (5/5, p = 0.0006); ET vs logreg: +0.0767; nested: RF +0.0336, ET +0.0659; seed-разброс 0.221–0.222 против 0.178–0.181 |
| F-H2: LSA не улучшает `stylometry_pos`; content-LSA — тема, не стиль | **Пересмотрена** | word+pos k=100: +0.0031 (3/5, p = 0.63); content k=300: **+0.0669** (5/5, p < 0.0001); POS-LSA k=300: −0.0130; `content` k=400 отдельно: 0.2114, выше полного logreg 0.1785 |
| F-H3: lean-наборы без `function_words`, `pos`, `vocabulary`, `readability` не хуже полного | **Подтверждена (эффект ≈ 0)** | lean1 +0.0059, lean2 +0.0023 (p = 0.30/0.46); lean+RF +0.004…+0.006 (p = 0.52/0.58); выигрыша нет, но и потери нет |
| F-H4: настройка `min_df`, `max_features`, `ngram_range` даёт ≥ 0.01 | **Частично** | `min_df`, char/POS n-граммы, `pos_max_features` — все < 0.005 и не значимы; `char_max_features=10000`: logreg +0.0070 (p = 0.051), RF **+0.0369** (5/5, p = 0.0032) |
| F-H5: balanced class weights повышают macro-F1 ценой accuracy | **Частично** | logreg balanced vs none: +0.0289 macro-F1, accuracy 0.1679 vs 0.1655 (цены по accuracy нет), balanced accuracy 0.187 vs 0.137; SVC: −0.003; RF: −0.001…−0.004, но balanced accuracy 0.242 vs 0.211 |
| F-H6: ранжирование моделей устойчиво к seed | **Подтверждена** | для seed 7 / 2024 / 1337: RF 0.2221 / 0.2206 / 0.2210, logreg 0.1779 / 0.1805 / 0.1800; разрыв 0.040–0.044 во всех seed |
| F-H7: ошибки концентрируются внутри возрастных/гендерных групп | **Не подтверждена** | F1 по подгруппам различается (elder 0.33, child 0.28, adult 0.19, teen 0.16), но доля ошибок внутри своей группы ≈ случайной: gender 0.449 против 0.472, age 0.384 против 0.360; ошибки стягиваются в hub-классы (Abigail) |
**F-H2 — главный пересмотр.** Исходный вывод «LSA не оправдан» был верен
только для конфигурации по умолчанию: `word`+`pos`, k=100. При k=300–400
и варианте `content` LSA даёт большой прирост, а одна `content`-LSA(k=400)
без ручных признаков обходит полный `stylometry_pos_lsa` с logreg. При этом
`content`-LSA строится по содержательным словам и их биграммам, то есть
кодирует лексику и тему (сезоны, работа, подарки, события), а не только
идиолект. Поэтому прирост нельзя записывать в «стиль»: это может быть
«о чём персонаж говорит» вместо «как он говорит». Прямого тематического
контроля в серии нет — это следующий обязательный эксперимент (7.6).
**F-H7 — не подтверждена в исходной формулировке.** Различия в F1 по
подгруппам есть, но они не означают, что модель путает персонажей внутри
группы чаще случайного. Фактически ошибки концентрируются не в демографии,
а в нескольких «hub»-классах: Abigail получает 23.3% всех предсказаний
(её истинная доля — 6.9%), precision 0.09, recall 0.30. Крупнейшие потоки
ошибок: Sebastian → Abigail (353 раза за 10 повторов), Haley → Abigail (328),
Alex → Abigail (280), Penny → Abigail (266). Это ближе к коллапсу редких
классов в один популярный, чем к возрастной путанице.
### 7.3. Парные сравнения
Правило решения из §4: улучшение убедительно, если Δ ≥ 0.01, знак
положителен минимум в 4/5 повторов, McNemar p < 0.05 в большинстве
повторов и bootstrap-интервал не содержит ноль. `mean_ci` — средний
интервал по повторам; `n_ci` — сколько повторов его не содержат.
| A vs B | Δ macro-F1 | Повторов в плюс | McNemar p | Bootstrap CI | CI ∌ 0 |
|---|---:|---:|---:|---:|---:|
| `model_et_default` vs `ref_logreg` | +0.0767 | 5/5 | < 0.0001 | [+0.0540, +0.0988] | 5/5 |
| `groups_lean2_content300_nested` vs `ref_nested_logreg` | +0.0745 | 1/1 | < 0.0001 | [+0.0531, +0.0954] | 1/1 |
| `groups_lean2_content300` vs `ref_logreg` | +0.0725 | 5/5 | < 0.0001 | [+0.0529, +0.0917] | 5/5 |
| `stylometry_pos_content300` vs `ref_logreg` | +0.0700 | 5/5 | < 0.0001 | [+0.0526, +0.0873] | 5/5 |
| `stylometry_pos_content300` vs `ref_no_lsa` | +0.0669 | 5/5 | < 0.0001 | [+0.0496, +0.0829] | 5/5 |
| `model_rf_default` vs `ref_logreg` | +0.0428 | 5/5 | 0.0006 | [+0.0199, +0.0652] | 5/5 |
| `char_max10000_rf_nested` vs `model_rf_nested` | +0.0416 | 1/1 | < 0.0001 | [+0.0256, +0.0578] | 1/1 |
| `char_max10000_rf` vs `model_rf_default` | +0.0369 | 5/5 | 0.0032 | [+0.0201, +0.0535] | 5/5 |
| `model_et_default` vs `model_rf_default` | +0.0338 | 5/5 | 0.0113 | [+0.0176, +0.0497] | 5/5 |
| `model_et_nested` vs `model_rf_nested` | +0.0323 | 1/1 | 0.0002 | [+0.0175, +0.0473] | 1/1 |
| `stylometry_pos_lsa_k300` vs `ref_logreg` | +0.0258 | 5/5 | < 0.0001 | [+0.0145, +0.0371] | 5/5 |
| `char_max10000_rf_nested` vs `model_et_nested` | +0.0093 | 1/1 | 0.5812 | [−0.0064, +0.0262] | 0/1 |
| `rf_grid_best_5` vs `model_rf_default` | +0.0081 | 4/5 | 0.2607 | [−0.0045, +0.0222] | 2/5 |
| `char_max10000_rf` vs `groups_lean2_content300` | +0.0073 | 5/5 | 0.6593 | [−0.0165, +0.0297] | 0/5 |
| `char_max10000` vs `ref_logreg` | +0.0070 | 3/3 | 0.0514 | [+0.0016, +0.0125] | 2/3 |
| `groups_lean2_content300_nested` vs `groups_lean2_content300` | +0.0044 | 1/1 | 0.8126 | [−0.0037, +0.0117] | 0/1 |
| `char_max10000_rf` vs `model_et_default` | +0.0031 | 3/5 | 0.5732 | [−0.0145, +0.0212] | 0/5 |
| `ref_no_lsa` vs `ref_logreg` | +0.0031 | 3/5 | 0.6317 | [−0.0100, +0.0167] | 0/5 |
| `groups_lean2_content300` vs `stylometry_pos_content300` | +0.0025 | 3/5 | 0.4839 | [−0.0115, +0.0171] | 0/5 |
| `char_max10000_rf_nested` vs `groups_lean2_content300_nested` | +0.0006 | 1/1 | 0.8440 | [−0.0208, +0.0226] | 0/1 |
| `stylometry_pos_pos300` vs `ref_no_lsa` | −0.0130 | 0/5 | 0.0671 | [−0.0246, −0.0017] | 3/5 |
| `classweight_logreg_none` vs `ref_logreg` | −0.0289 | 0/5 | 0.6541 | [−0.0462, −0.0112] | 5/5 |
| `control_length_logreg` vs `ref_logreg` | −0.1430 | 0/5 | < 0.0001 | [−0.1611, −0.1215] | 5/5 |
Полные 46 сравнений — в `results/followup/comparisons.csv` и
`comparisons.md`; в таблице выше оставлены те, на которые опираются выводы.
Что важно в этой таблице:
- Прирост `content`-LSA и lean+content — самый большой и устойчивый:
+0.067…+0.073, 5/5 повторов, p < 0.0001, интервал далеко от нуля.
- Деревья обходят logreg устойчиво: +0.043 (RF) и +0.077 (ET) относительно
`ref_logreg`, 5/5 повторов, p ≤ 0.0006. В nested-постановке разрыв
сохраняется: char-RF +0.042 относительно `model_rf_nested`, lean+content
+0.075 относительно `ref_nested_logreg` (оба p < 0.0001).
- **ET значимо лучше дефолтного RF**: +0.0338, 5/5, p = 0.011,
CI [+0.0176, +0.0497]. Это не шум, и именно ET, а не RF, стоит брать
за базу для деревьев.
- **Два лидера между собой неразличимы.** `char_max10000_rf` vs
`model_et_default`: +0.0031, 3/5, p = 0.57, CI накрывает ноль. В nested:
`char_max10000_rf_nested` vs `model_et_nested` +0.0093 (p = 0.58),
vs `groups_lean2_content300_nested` +0.0006 (p = 0.84). Выбирать между
char-RF, ET и lean+content по этим данным нельзя — разница меньше
неопределённости разбиения.
- `char_max10000_rf` vs дефолтный RF — редкий случай, когда настройка
одного параметра признаков даёт > 0.03 и подтверждается на всех повторах.
- Сетка `max_depth`/`min_samples_leaf` даёт лишь +0.0081 (4/5, p = 0.26,
CI содержит ноль), то есть подбор глубины деревьев почти бесполезен.
- Старый LSA(word+pos, k=100) действительно нейтрален (+0.0031, 3/5),
но тот же вариант при k=300 даёт +0.0258 (5/5, p < 0.0001). Значит,
вопрос не в «LSA вообще», а в числе компонент: при k=100 сигнал теряется.
POS-LSA при k=300 по-прежнему вредит (−0.0130, 0/5).
- `class_weight=none` для logreg хуже по macro-F1, хотя McNemar не значим:
обе модели часто ошибаются, но balanced-версия лучше вытягивает редкие
классы. Это видно по balanced accuracy (0.187 против 0.137).
Сырые p-value не поправлены на множественность (46 пар). Эффекты
+0.04…+0.07 с p < 0.001 переживут Holm—Bonferroni; пограничные
(+0.003…+0.009) — нет, и их не стоит интерпретировать как выигрыш.
### 7.4. Диагностика финальной модели (`final_rf`, 10 повторов)
- **Длина реплики.** Зависимость немонотонная: 0–5 слов — 0.081 (n = 42),
6–10 — 0.173, 11–15 — 0.182, 16–20 — 0.194, 21–30 — **0.231**,
31+ — 0.168. Прежний вывод «чем длиннее, тем лучше» не подтвердился:
после 30 слов качество падает, вероятно из-за накопления тематических
и синтаксических различий.
- **Подгруппы.** Elder 0.331, child 0.280, adult 0.190, teen 0.164;
gender: male 0.205, female 0.190, unknown 0.229; species: monster 0.269,
human 0.201, unknown 0.035. Это F1 внутри группы, а не качество
предсказания группы.
- **Классы.** Средний F1 по 10 повторам: медиана 0.193, минимум 0.020
(Wizard), максимум 0.550 (Evelyn). Худшие: Wizard 0.020, Penny 0.056,
Jodi 0.069, Maru 0.075, Mister Qi 0.081; лучшие: Evelyn 0.550, Willy 0.494,
Pam 0.493, Pierre 0.470, Vincent 0.397. Семь классов имеют F1 < 0.1.
- **Hub-классы.** 23.3% предсказаний — Abigail при истинной доле 6.9%;
Alex — 12.3% при 6.0%. Abigail: precision 0.09, recall 0.30, F1 0.14.
Это главный источник ошибок и главный резерв для калибровки prior/
порогов.
- **Верификация.** ROC-AUC 0.630–0.633, EER 0.412–0.415, TAR@FAR=1%
0.035–0.038, top-1 0.133–0.141 для seed 42/7/2024. Выше случайного
(1/34 = 0.029), но практически бесполезно для open-set сценария.
- **Кластеризация.** ARI = 0 для k = 3/5/8 (и agglomerative, и k-means);
NMI 0.14–0.53. Персонажи не образуют устойчивых кластеров в этом
признаковом пространстве.
- **Delta (MFW, cosine).** 2000 признаков — 0.2426, 1000 — 0.2299,
500 — 0.1604 (3 повтора). Больше признаков лучше; потолок не найден.
- **Near-duplicates.** 13 пар с cosine ≥ 0.8, 3 ≥ 0.9, 1 ≥ 0.95; все —
внутри одного персонажа, межклассовых почти-дублей нет. Утечки через
дубли не видно.
- **Контроль перестановкой.** Macro-F1 0.0270 / 0.0274 / 0.0282 для seed
42/7/2024 (среднее 0.0276) при случайном уровне ≈ 0.029. Пайплайн
не подглядывает в метки.
- **Нижние границы.** Length+logreg 0.0354, dummy 0.0038. Основной результат
(0.22–0.26) далеко от обеих границ.
### 7.5. Что это меняет в выводах
1. **«Финальная» модель в текущем виде — не лучшая.** `final_rf` — это
дефолтный random forest (0.2216), тогда как ExtraTrees (0.2551) и
RF с `char_max_features=10000` (0.2583) заметно лучше. В отчёте стоит
либо переименовать `final_rf` в `baseline_rf`, либо заменить финальную
модель на ET/char-RF и пересчитать отчёты.
2. **Главный сигнал — лексика, а не «стиль».** Сильнее всего работают
`content`-LSA (тема/лексика) и char-ngram-словарь. Ручные стилометрические
признаки дают лишь 0.105. Формулировка «модель различает идиолект»
слишком сильная; корректнее — «модель различает персонажей по
лексико-тематическому профилю и символьным n-граммам».
3. **Вложенная CV подтверждает лидеров, но не закрывает выбор признаков.**
Nested-оценки (0.2579 / 0.2486 / 0.2572) честнее по гиперпараметрам,
но `char_max_features=10000` и `content`-LSA(k=300) выбирались на тех же
данных. Для строгого вывода нужна внешняя валидация или вложенный выбор
признаков.
4. **Основная неопределённость — не seed, а разбиение и признаки.**
Разброс по seed ≤ 0.008, а по фолдам nested logreg — до 0.07. Поэтому
одна цифра без интервала вводит в заблуждение; нужно указывать mean ± std
по повторам и отдельно fold-разброс.
5. **Ошибки — это hub-коллапс, а не демографическая путаница.**
Калибровка prior/порогов и работа с редкими классами дадут больше, чем
дальнейший подбор глубины деревьев.
### 7.6. Следующие эксперименты
1. **Nested CV с выбором признаков внутри внешнего цикла.** Сейчас nested
крутит только `C`/`max_depth`/`min_samples_leaf`; `char_max_features`
и `k` LSA фиксированы. Нужен внешний цикл, в котором выбираются и они,
либо отдельный held-out набор для выбора признаков.
2. **`content`-LSA k=400/500 + lean2.** Sweep показывает монотонный рост
до k=400; стоит проверить k=400 и 500 с `groups_lean2_content*` и
`stylometry_pos_content*`, желательно сразу с nested-оценкой.
3. **Тематический контроль.** Проверить, сохраняется ли прирост
`content`-LSA, если: (а) сбалансировать реплики по темам/сезонам,
(б) удалить тематические слова, (в) обучить adversarial-классификатор
темы и убрать её компоненту. Без этого нельзя утверждать, что модель
ловит стиль, а не «о чём говорят».
4. **Калибровка hub-классов.** Prior correction, temperature scaling или
пороги на OOF для Abigail/Alex; оптимизировать macro-F1 декодированием,
а не только обучением. Смотреть precision/recall по редким классам.
5. **Отчёты и статистика.** Заменить `final_rf` на реального лидера,
добавить Holm—Bonferroni для семейства парных сравнений, считать
иерархические интервалы (повтор × фолд), а не только repeat-0 bootstrap.
6. **Внешняя валидация.** Другой корпус, другая игра или хотя бы разбиение
по сценам/диалогам, а не по отдельным репликам. Только оно покажет,
переносится ли лексико-тематический сигнал за пределы этого корпуса.
---
## 8. Ограничения
- Корпус 2005 реплик, 34 класса, минимум 15 реплик на класс. Доверительные
интервалы по редким персонажам останутся широкими даже при 10 повторах.
- Повторная CV с одними и теми же 2005 репликами не заменяет внешнюю
валидацию на новом корпусе.
- `content`-LSA может отражать тему (сезоны, подарки, работа), а не идиолект.
Поэтому его вклад нельзя интерпретировать как «стиль» без отдельного
контроля (например, обучения на тематически сбалансированных подвыборках).
- `verify` и `cluster` не имеют параметра `repeats`; для них меняется только
seed/разбиение, поэтому выводы по ним слабее, чем по CV.
- При интерпретации subgroup-метрик нужно помнить, что это F1 внутри группы
персонажей, а не качество предсказания самой группы.

View File

@@ -136,51 +136,6 @@ python -m stylometry.cli subgroup-report \
| `lsa_word`, `lsa_pos` | только соответствующий вариант LSA | | `lsa_word`, `lsa_pos` | только соответствующий вариант LSA |
| `length` | только признаки длины (нижняя граница) | | `length` | только признаки длины (нижняя граница) |
## Отчёт
Итоговый отчёт:
- **`report/REPORT.pdf`** — основной документ, 21 страница;
- **`report/REPORT.docx`** — редактируемая версия;
- **`report/REPORT.md`** — исходник Markdown;
- **`report/figures/`** — рисунки.
Пересборка:
```bash
python scripts/build_report.py
```
Для сборки нужны `pandoc`, LibreOffice и пакеты из
`requirements-report.txt`. Скрипт сам оформляет титул, колонтитулы, таблицы,
нумерацию рисунков и проверяет, что в PDF есть все разделы.
## Презентация
Есть две версии:
- **`presentation/stardew_stylometry.pptx`** — редактируемая PPTX-версия
(30 слайдов после ручных правок);
- **`presentation/stardew_stylometry.pdf`** — PDF этой версии;
- **`presentation/latex/stardew_stylometry.tex`** — академическая
Beamer-версия;
- **`presentation/latex/stardew_stylometry.pdf`** — собранный PDF LaTeX-версии
(30 страниц).
LaTeX-версия содержит три части на трёх докладчиков, фон и курицу на каждом
слайде, титульную заставку, изображение персонажей, две круговые диаграммы,
матрицы ошибок, F-меры по персонажам, приложение с полной матрицей и финальный
мем. Формул почти нет: одна компактная запись обозначений на слайде постановки
задачи.
Сборка LaTeX:
```bash
cd presentation/latex
xelatex stardew_stylometry.tex
xelatex stardew_stylometry.tex
```
## Конфигурация ## Конфигурация
Файл `configs/default.toml` задаёт параметры по умолчанию: Файл `configs/default.toml` задаёт параметры по умолчанию:
@@ -199,10 +154,6 @@ python -m stylometry.cli run --config configs/default.toml \
## Основные документы ## Основные документы
- **`START_HERE.md`** — краткая инструкция: что открыть и как проверить
окружение за три минуты.
- **`ARCHIVE_README.md`** — подробное описание архива: состав, установка,
запуск, структура модулей, пересборка отчёта и презентации.
- **`RUNBOOK.md`** — пошаговая инструкция для запуска полной серии - **`RUNBOOK.md`** — пошаговая инструкция для запуска полной серии
экспериментов: установка, проверка окружения, все команды, упаковка экспериментов: установка, проверка окружения, все команды, упаковка
результатов и типичные ошибки. результатов и типичные ошибки.
@@ -213,14 +164,6 @@ python -m stylometry.cli run --config configs/default.toml \
выбора; дополняет машинно-читаемый `features/feature_dictionary.csv`. выбора; дополняет машинно-читаемый `features/feature_dictionary.csv`.
- **`TUNING.md`** — где менять параметры классификаторов, признаки, LSA и веса - **`TUNING.md`** — где менять параметры классификаторов, признаки, LSA и веса
блоков; рекомендуемый порядок улучшения результатов. блоков; рекомендуемый порядок улучшения результатов.
- **`RESULTS_ANALYSIS.md`** — независимый анализ follow-up результатов:
ранжирование моделей, статистические сравнения, ошибки, ограничения и
выводы.
- **`FINAL_REPORT.md`** — итоговый отчёт по всей работе: методика, все серии
экспериментов, результаты, обсуждение и выводы.
- **`FINAL_RESULTS.csv`** — сводная таблица ключевых результатов.
- **`FOLLOWUP_EXPERIMENTS.md`** — протокол и результаты follow-up серии,
подготовленные коллегой.
- **`METRICS_AND_CLASSIFIERS.md`** — методическое описание метрик и - **`METRICS_AND_CLASSIFIERS.md`** — методическое описание метрик и
классификаторов: что показывает каждая метрика, как она считается, как классификаторов: что показывает каждая метрика, как она считается, как
интерпретировать значения, чем отличаются модели и как объяснять это на интерпретировать значения, чем отличаются модели и как объяснять это на

View File

@@ -1,389 +0,0 @@
# Анализ результатов follow-up серии
Дата анализа: 26.10.2026.
> Дополнительные эксперименты, вложенный выбор конфигурации, контроль
> содержания, калибровка и ансамбли описаны в итоговом отчёте
> `FINAL_REPORT.md`; сводная таблица — в `FINAL_RESULTS.csv`.
Источник результатов:
- ветка `origin/opencode_test`, коммит `ac7c6f0`;
- основной архив `lab1/exports/nlp_lab1_results_20261026_054117.tar.gz`;
- сводки `results/followup/summary.csv`, `summary.md`, `comparisons.csv`,
`comparisons.md`;
- документ коллеги `FOLLOWUP_EXPERIMENTS.md`.
Контрольные суммы архивов совпали. В основном архиве — 68 запусков; ключевые
конфигурации выполнены с 5 повторами 5-блочной стратифицированной
перекрёстной проверки, seed 42. Дополнительно есть nested-запуски, контроли
перестановкой меток, анализ почти-дублей, длины реплик и подгрупп.
Настоящий документ — независимая сводка: что можно утверждать, что нельзя,
и какие эксперименты нужны дальше.
---
## 1. Краткий ответ
1. **Персонажей можно различать, но сигнал умеренный.** Лучшая конфигурация
даёт macro-F1 около **0,258** при случайном уровне **0,029**; top-3 —
около **0,42** при случайном **0,088**. Это в 7–9 раз выше случайного
уровня, но не «почти безошибочная» классификация.
2. **Основной сигнал — лексико-тематический, а не чисто стилистический.**
Ручные стилометрические признаки дают 0,105; добавление символьных и
частеречных n-грамм поднимает до 0,182; `content`-LSA — до 0,248–0,258.
Функциональные слова и POS-LSA сами по себе слабы (0,06–0,07).
3. **Лучшие кандидаты — `char_max10000_rf` и `model_et_default`.** Они
статистически неразличимы: +0,0031, 3/5 повторов, p = 0,57,
доверительный интервал накрывает ноль. `groups_lean2_content300` тоже
близок (0,251), но опирается на `content`-LSA, то есть скорее на тему.
4. **`final_rf` — не лучшая модель.** Дефолтный random forest даёт 0,222,
тогда как ExtraTrees — 0,255, а RF с `char_max_features=10000` — 0,258.
В отчёте `final_rf` нужно переименовать в `baseline_rf` или заменить.
5. **Ошибки — это коллапс в «hub»-классы, а не демографическая путаница.**
Abigail получает 21–23% всех предсказаний при истинной доле 6,9%;
precision 0,087, recall 0,266. Редкие классы (Kent, Wizard, Penny,
Mister Qi) почти не находятся.
6. **Строгий вывод о стиле пока невозможен.** `content`-LSA и широкие
символьные n-граммы могут кодировать содержание и тему. Нужен
тематический контроль и внешняя валидация.
---
## 2. Ранжирование моделей
`Δ` — разница с `ref_logreg` (0,1785). Для nested-запусков указан один
внешний цикл из 5 блоков, поэтому std по повторам равен нулю.
| Label | Модель | Повторов | Macro-F1 | Δ | Accuracy | Top-3 |
|---|---|---:|---:|---:|---:|---:|
| `char_max10000_rf` | random forest | 5 | **0,2583 ± 0,0066** | +0,0798 | 0,2391 | 0,4197 |
| `char_max10000_rf_nested` | random forest, nested | 1×5 | **0,2579** | +0,0794 | 0,2394 | 0,4075 |
| `groups_lean2_content300_nested` | logreg, nested | 1×5 | **0,2572** | +0,0788 | 0,2369 | 0,4065 |
| `model_et_default` | extra trees | 5 | **0,2551 ± 0,0073** | +0,0767 | 0,2387 | 0,4170 |
| `et_dNone_l2` | extra trees | 3 | 0,2538 ± 0,0089 | +0,0753 | 0,2416 | 0,4308 |
| `groups_lean2_content300` | logreg | 5 | 0,2510 ± 0,0040 | +0,0725 | 0,2379 | 0,4111 |
| `model_et_nested` | extra trees, nested | 1×5 | 0,2486 | +0,0701 | 0,2344 | 0,4259 |
| `stylometry_pos_content300` | logreg | 5 | 0,2485 ± 0,0042 | +0,0700 | 0,2337 | 0,4175 |
| `rf_grid_best_5` | random forest | 5 | 0,2294 ± 0,0082 | +0,0509 | 0,2253 | 0,4004 |
| `model_rf_default` | random forest | 5 | 0,2213 ± 0,0065 | +0,0428 | 0,2108 | 0,3893 |
| `final_rf` | random forest, 10 повторов | 10 | 0,2216 ± 0,0062 | +0,0431 | 0,2094 | 0,3865 |
| `model_svc_default` | linear SVC | 5 | 0,1880 ± 0,0067 | +0,0096 | 0,1840 | 0,3386 |
| `ref_no_lsa` | logreg, `stylometry_pos` | 5 | 0,1815 ± 0,0054 | +0,0031 | 0,1708 | 0,3395 |
| `ref_logreg` | logreg, `stylometry_pos_lsa` | 5 | 0,1785 ± 0,0060 | 0 | 0,1679 | 0,3322 |
| `stylometry` | logreg, только ручные признаки | 5 | 0,1051 ± 0,0038 | −0,0733 | 0,1012 | 0,2338 |
| `lsa_content_k400` | logreg, только content-LSA | 5 | 0,2114 ± 0,0029 | +0,0329 | 0,2059 | 0,3767 |
| `lsa_pos_k300` | logreg, только POS-LSA | 5 | 0,0667 ± 0,0040 | −0,1118 | 0,0663 | 0,1556 |
| `control_length_logreg` | logreg, только длина | 5 | 0,0354 ± 0,0032 | −0,1430 | 0,0395 | 0,1345 |
| `control_length_dummy` | dummy | 1 | 0,0038 | −0,1747 | 0,0693 | 0,1022 |
Случайный уровень: macro-F1 ≈ 0,029, accuracy ≈ 0,029, top-3 ≈ 0,088.
Контроль перестановкой меток дал 0,0270–0,0282, то есть ровно случайный
уровень. Признаков утечки через метки не видно.
---
## 3. Статистическая надёжность
Главные парные сравнения:
| Сравнение | Δ macro-F1 | Повторов в плюс | McNemar p | Bootstrap CI |
|---|---:|---:|---:|---:|
| `char_max10000_rf` vs `ref_logreg` | +0,0798 | 5/5 | < 0,0001 | [+0,053, +0,099] |
| `model_et_default` vs `ref_logreg` | +0,0767 | 5/5 | < 0,0001 | [+0,054, +0,099] |
| `groups_lean2_content300` vs `ref_logreg` | +0,0725 | 5/5 | < 0,0001 | [+0,053, +0,092] |
| `stylometry_pos_content300` vs `ref_no_lsa` | +0,0669 | 5/5 | < 0,0001 | [+0,050, +0,083] |
| `char_max10000_rf` vs `model_rf_default` | +0,0369 | 5/5 | 0,0032 | [+0,020, +0,053] |
| `model_et_default` vs `model_rf_default` | +0,0338 | 5/5 | 0,0113 | [+0,018, +0,050] |
| `char_max10000_rf` vs `model_et_default` | +0,0031 | 3/5 | 0,5732 | [−0,014, +0,021] |
| `char_max10000_rf` vs `groups_lean2_content300` | +0,0073 | 5/5 | 0,6593 | [−0,017, +0,030] |
Выводы:
- преимущество деревьев над логистической регрессией устойчиво и велико;
- ExtraTrees устойчиво лучше дефолтного random forest;
- `char_max10000_rf`, ExtraTrees и `groups_lean2_content300` между собой
статистически неразличимы;
- малые различия (+0,003…+0,009) не переживают множественную поправку и
не должны трактоваться как выигрыш.
Для 46 парных сравнений нужна поправка Холма—Бонферрони. Эффекты
+0,04…+0,08 с p < 0,001 её переживут; пограничные +0,003…+0,009 — нет.
---
## 4. Что именно даёт качество
| Компонент | Macro-F1 | Интерпретация |
|---|---:|---|
| Только длина | 0,035 | Слабая нижняя граница |
| Только ручные стилометрические признаки | 0,105 | Стиль/структура без лексических n-грамм |
| Ручные + char/POS n-граммы | 0,1815 | Основной прирост +0,077 |
| То же + word/pos-LSA(k=100) | 0,1785 | LSA по умолчанию не помогает |
| То же + content-LSA(k=300) | 0,2485 | Сильный прирост, но это тема/лексика |
| Только content-LSA(k=400) | 0,2114 | Контент сам по себе сильнее ручной стилометрии |
| Только POS-LSA(k=300) | 0,0667 | Синтаксис в таком виде слаб |
| Только функциональные слова | 0,0598 | Стиль без лексики слаб |
| RF + char_max_features=10000 | 0,2583 | Лучший результат |
Это главный содержательный вывод: **модель различает персонажей в первую
очередь по лексико-тематическому профилю и символьным n-граммам**, а не по
«чистому» синтаксису или функциональным словам. Формулировка «модель выучила
идиолект» слишком сильная. Корректнее: «модель различает персонажей по
устойчивым лексическим и тематическим маркерам, которые автор закрепил за
речевыми масками».
Дополнительное подтверждение: lean-набор без `function_words`, `pos`,
`vocabulary` и `readability` даёт 0,1808, то есть **не хуже** полного
`stylometry_pos_lsa` (0,1785). Удаление «стилевых» групп не ухудшает
результат, потому что основной сигнал несут char- n-граммы и лексика.
---
## 5. LSA: тема или стиль
Зависимость от числа компонент для `content`-LSA монотонная:
| k | Macro-F1 |
|---:|---:|
| 100 | 0,1334 |
| 200 | 0,1796 |
| 300 | 0,1984 |
| 400 | 0,2128 |
Для сравнения: `word` при k=400 — 0,1781, `char` при k=400 — 0,1787,
`pos` — около 0,071, `fw` — около 0,074.
Интерпретация:
- `content`-LSA строится по содержательным словам и их биграммам; это
кодирует **о чём говорит персонаж**, а не только **как**;
- сильный рост при k=300–400 означает, что тематическая вариативность
богата и хорошо разделяет персонажей;
- `word+pos` LSA при k=100 была нейтральна, но это не значит, что «LSA
вообще не работает»: важен вариант и число компонент;
- POS-LSA при добавлении к полному набору **ухудшает** результат
(`stylometry_pos_pos300` = 0,1685 против 0,1815), то есть синтаксический
LSA в текущем виде шумит.
Пока нет тематического контроля, прирост `content`-LSA нельзя записывать в
стилометрию. Нужны: сбалансированные по темам подвыборки, удаление
тематических слов или adversarial-контроль темы.
---
## 6. Ошибки модели
Для лучшей модели `char_max10000_rf` (таблицы `per_class_report.csv` и
`confusion_matrix.csv` относятся к repeat 0; доли предсказаний проверены по
всем пяти повторам):
- Abigail получает **21–23%** всех предсказаний при истинной доле **6,9%**;
precision 0,087, recall 0,266, F1 0,131.
- Alex получает 11–12% при истинной доле 6,0%.
- Худшие F1: Kent 0,000, Wizard 0,048, Penny 0,050, Mister Qi 0,069,
Leah 0,083, Jodi 0,098, Linus 0,105.
- Лучшие F1: Evelyn 0,645, Willy 0,578, Pam 0,534, Pierre 0,518,
Leo 0,440, Vincent 0,393, Krobus 0,392.
- Крупнейшие потоки ошибок: Sebastian → Abigail (40), Haley → Abigail (31),
Alex → Abigail (28), Penny → Abigail (27), Sam → Abigail (25).
- Это **hub-коллапс**: редкие и средние классы стягиваются в несколько
популярных, а не путаются по возрасту или полу.
`groups_lean2_content300` ведёт себя иначе: Abigail получает только 3,6%
предсказаний, а Wizard F1 = 0,449. Ошибки моделей комплементарны, поэтому
ансамбль может быть полезен.
### 6.1. Длина реплики
Для `char_max10000_rf`, repeat 0:
| Длина | Macro-F1 | Доверительный интервал |
|---|---:|---:|
| 0–5 слов | 0,088 | [0,033; 0,122] |
| 6–10 | 0,218 | [0,153; 0,258] |
| 11–15 | 0,222 | [0,170; 0,258] |
| 16–20 | 0,239 | [0,190; 0,271] |
| 21–30 | **0,285** | [0,238; 0,315] |
| 31+ | 0,240 | [0,180; 0,273] |
Зависимость немонотонная: после 30 слов качество падает. Гипотеза «чем
длиннее, тем лучше» не подтверждается; длинные реплики накапливают
тематические и синтаксические различия.
### 6.2. Подгруппы
Для `char_max10000_rf`, repeat 0:
- возраст: elder 0,406, child 0,378, adult 0,232, teen 0,196;
- пол: male 0,266, unknown 0,244, female 0,218;
- вид: monster 0,295, human 0,246, unknown 0,056.
Это F1 **внутри группы персонажей**, а не качество предсказания группы.
Различия описательные; они не доказывают, что модель обязана путать
персонажей внутри группы.
### 6.3. Верификация и кластеризация
- Верификация: ROC-AUC 0,630–0,633, EER 0,412–0,415, TAR@FAR=1% 0,035–0,038,
top-1 0,133–0,141. Выше случайного, но практически слабо.
- Кластеризация: ARI = 0 для k = 3/5/8, NMI 0,14–0,53. Устойчивых кластеров
персонажей в этом пространстве нет.
---
## 7. Ансамбль: предварительный результат
По out-of-fold предсказаниям (одинаковые повторы и seed) я проверил простое
голосование:
| Комбинация | Macro-F1 |
|---|---:|
| `char_max10000_rf` + `model_et_default` | 0,2583 |
| + `groups_lean2_content300` | **0,2728 ± 0,0024** |
| + `stylometry_pos_content300` | 0,2751 ± 0,0036 |
Это **не** валидная финальная модель: голосование построено постфактум по
тем же OOF-предсказаниям, которые использовались для выбора конфигураций.
Но результат показывает, что ошибки моделей комплементарны и что
`VotingClassifier`/stacking — перспективное направление. Проверять его нужно
в отдельном nested-эксперименте, а не по этим числам.
---
## 8. Методологические ограничения
1. **Выбор признаков на тех же данных.** `char_max_features=10000`,
`content`-LSA(k=300/400) и lean-наборы выбирались по результатам на
тестовых фолдах. Nested CV в текущем виде настраивает только `C`,
`max_depth` и `min_samples_leaf`; выбор признаков остаётся снаружи.
Поэтому 0,258 может быть оптимистичной оценкой. Нужен внешний holdout или
nested-выбор признаков.
2. **Множественные сравнения.** 46 пар; мелкие эффекты не переживают
поправку Холма—Бонферрони.
3. **Один корпус.** Все 2005 реплик — из одной игры; внешней валидации нет.
Разбиение по репликам не учитывает сцены и диалоги.
4. **Один автор.** Это классификация речевых масок, а не авторская
атрибуция.
5. **Короткие тексты и дисбаланс.** 15–139 реплик на класс; редкие классы
имеют широкие интервалы, а hub-классы забирают предсказания.
6. **Тематический конфликт.** `content`-LSA и char- n-граммы могут кодировать
тему, сезоны, подарки, работу и имена. Без контроля темы нельзя утверждать,
что модель выучила стиль.
7. **McNemar и macro-F1 не всегда согласованы.** Например, отключение
`class_weight` у logreg снижает macro-F1 на 0,029, но McNemar даёт
p = 0,65, потому что общая accuracy почти не меняется. Для выводов о
macro-F1 нужны повторные разницы и bootstrap, а не только McNemar.
8. **`final_rf` — не финальная модель.** Это дефолтный RF с 0,222; он
проигрывает ET и char-RF.
---
## 9. Выводы
1. **Гипотеза о различимости персонажей подтверждается частично.**
Лучшая модель: macro-F1 ≈ 0,258, top-3 ≈ 0,42, accuracy ≈ 0,239 при
34 классах. Это заметно выше случайного уровня, но далеко от надёжной
практической классификации.
2. **Основной вклад дают лексика, символьные n-граммы и content-LSA.**
Ручные стилометрические признаки и POS/функциональные слова сами по себе
слабы. Поэтому результат нельзя интерпретировать как «чистый идиолект».
3. **Лучшие модели — ExtraTrees и RF с расширенным char-словарём.**
Они статистически неразличимы. ET проще и не требует выбора
`char_max_features`; char-RF даёт максимум, но его конфигурация выбрана
на тех же данных.
4. **`content`-LSA — сильный, но тематический сигнал.** Он улучшает
результат, однако требует тематического контроля; POS-LSA в текущем виде
вредит.
5. **Главная проблема — не seed, а hub-коллапс и редкие классы.**
Abigail, Alex и ещё несколько классов забирают чужие предсказания.
Калибровка prior/порогов и работа с редкими классами перспективнее
дальнейшего перебора глубины деревьев.
6. **Ансамбль моделей перспективен, но пока не проверен корректно.**
Предварительное голосование даёт 0,273; нужен отдельный nested-эксперимент.
7. **Для строгого вывода о стиле нужны внешняя валидация и тематический
контроль.** Без них корректная формулировка: «персонажи различимы по
лексико-тематическому профилю и символьным n-граммам».
---
## 10. Что делать дальше
### Приоритет 0 — честная финальная оценка
1. Выбрать одну финальную модель: `model_et_default` (проще) или
`char_max10000_rf` (максимум).
2. Провести nested CV, в котором внутри внешнего цикла выбираются не только
гиперпараметры, но и `char_max_features`, `min_df`, `ngram_range` и
`content`-LSA(k).
3. Либо зафиксировать конфигурацию до анализа и проверить на отдельном
holdout/новом корпусе.
4. Заменить `final_rf` на реального лидера или переименовать его в
`baseline_rf`.
### Приоритет 1 — тематический контроль
- сбалансировать реплики по темам/сезонам;
- удалить тематические слова и проверить падение `content`-LSA;
- обучить классификатор темы и проверить, не объясняет ли она результат;
- сравнить `content`-LSA с `pos`/`fw` на тематически однородных подвыборках.
### Приоритет 2 — ошибки и калибровка
- prior correction / temperature scaling для hub-классов;
- пороги на OOF, оптимизирующие macro-F1, а не только accuracy;
- анализ `Abigail`, `Alex`, `Kent`, `Wizard`, `Penny`;
- отдельный отчёт по редким классам.
### Приоритет 3 — ансамбли и признаки
- `VotingClassifier`/stacking по OOF-вероятностям;
- `content`-LSA k=400/500 с lean-набором;
- char `max_features` 10000–15000, `min_df` 2–3;
- block weights;
- внешняя валидация на другом корпусе или разбиении по сценам.
---
## 11. Артефакты
```text
lab1/exports/nlp_lab1_results_20261026_054117.tar.gz
lab1/FOLLOWUP_EXPERIMENTS.md
lab1/scripts/analyze_followup.py
lab1/scripts/run_followup_experiments.sh
```
После распаковки архива:
```text
results/followup/summary.csv
results/followup/summary.md
results/followup/comparisons.csv
results/followup/comparisons.md
results/followup/<label>/
```
Команда для повторного анализа:
```bash
python scripts/analyze_followup.py \
--followup-dir results/followup \
--pairs char_max10000_rf:ref_logreg \
char_max10000_rf:model_et_default \
groups_lean2_content300:ref_logreg
```
---
## 12. Итоговая формулировка для отчёта
> Персонажи Stardew Valley статистически различимы по отдельным репликам:
> лучшая модель достигает macro-F1 ≈ 0,26 и top-3 ≈ 0,42 при 34 классах
> (случайный уровень 0,029 и 0,088 соответственно). Однако основной вклад
> дают лексико-тематические признаки и символьные n-граммы, а не
> функциональные слова и синтаксис. Поэтому результат корректно
> интерпретировать как различимость речевых масок по лексико-тематическому
> профилю; для утверждения о «чистом стиле» необходимы тематический контроль
> и внешняя валидация.

View File

@@ -464,34 +464,6 @@ python -m stylometry.cli compare \
--- ---
## 6.16. Дополнительная серия для итогового отчёта
После основной и follow-up серий можно запустить дополнительные эксперименты:
вложенный выбор конфигурации, контроль содержания, калибровку, ансамбли и
дополнительные конфигурации.
```bash
python scripts/run_final_experiments.py \
--stages nested topic calibration ensemble extra \
--out results/final
python scripts/run_finalists.py
```
Что делают стадии:
- `nested` — выбирает конфигурацию признаков и модель внутри внешних блоков
перекрёстной проверки;
- `topic` — заменяет содержательные слова на нейтральный маркер и проверяет,
сохраняется ли качество;
- `calibration` — проверяет коррекцию априорных вероятностей классов;
- `ensemble` — мягкое голосование и стековое объединение;
- `extra` — дополнительные конфигурации и стратифицированная отложенная
выборка.
Результаты сохраняются в `results/final/`; сводка — `FINAL_RESULTS.csv`,
итоговый отчёт — `FINAL_REPORT.md`.
## 7. Если серия прервалась ## 7. Если серия прервалась
Скрипт останавливается при первой ошибке. Уже созданные каталоги результатов Скрипт останавливается при первой ошибке. Уже созданные каталоги результатов

View File

@@ -1,81 +0,0 @@
# С чего начать
Это архив лабораторной работы №1 по курсу «Автоматический анализ текстов»:
стилометрическая классификация реплик персонажей Stardew Valley.
## Что внутри
- `lab1/report/REPORT.pdf` — **главный отчёт** (21 страница).
- `lab1/report/REPORT.docx` — редактируемая версия отчёта.
- `lab1/report/REPORT.md` — исходник отчёта.
- `lab1/presentation/stardew_stylometry.pdf` — презентация для защиты.
- `lab1/presentation/stardew_stylometry.pptx` — редактируемая презентация.
- `lab1/presentation/latex/` — LaTeX-исходник презентации.
- `lab1/stylometry/` — код: чтение корпуса, признаки, LSA, модели,
кросс-валидация, метрики, верификация.
- `lab1/data/` — корпус и метаданные.
- `lab1/tests/` — 33 теста.
- `lab1/FINAL_RESULTS.csv` — сводная таблица результатов.
- `lab1/exports/nlp_lab1_final_results_20261026_120000.tar.gz` — компактный
архив финальных результатов (без промежуточных матриц).
## Проверка за три минуты
Все команды выполняются из каталога `lab1`:
```bash
cd lab1
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
python scripts/download_nltk_data.py
python scripts/check_environment.py
python -m pytest -q
```
Ожидаемый результат:
- `check_environment.py` заканчивается строкой `Окружение готово.`;
- `pytest` печатает `33 passed`;
- в корпусе 2005 реплик и 34 персонажа.
Если всё совпало, отчёт и презентацию можно открывать сразу. Для повторного
запуска экспериментов смотрите `ARCHIVE_README.md`.
## Главный результат
| Что | Значение |
|---|---:|
| Корпус | 2005 реплик, 34 персонажа |
| Основная метрика | macro-F1 |
| Мягкое голосование трёх моделей | **0,3036 ± 0,0189** |
| Консервативная оценка (вложенный выбор) | **0,2746 ± 0,0233** |
| Случайный уровень | **0,029** |
Основной сигнал — лексико-тематический. После замены содержательных слов
нейтральным маркером macro-F1 падает с 0,248 до 0,113. Поэтому в отчёте
результат сформулирован как различимость **речевых масок по
лексико-тематическому профилю**, а не как доказательство «чистого» стиля.
## Что читать дальше
1. `ARCHIVE_README.md` — подробная инструкция: установка, запуск, структура
проекта, что делает каждый модуль, как пересобрать отчёт и презентацию.
2. `lab1/RUNBOOK.md` — пошаговый запуск полной серии экспериментов.
3. `lab1/README.md` — краткое описание проекта и команд.
4. `lab1/FINAL_REPORT.md` — расширенная версия отчёта с деталями.
5. `lab1/EXPERIMENTS.md` — протокол экспериментов.
6. `lab1/FEATURES.md` — формулы и обоснование признаков.
7. `lab1/METRICS_AND_CLASSIFIERS.md` — метрики и классификаторы.
## Важно
- Ресурсы NLTK (`nltk_data`, около 80 МБ) в архив не включены: они
скачиваются командой `python scripts/download_nltk_data.py`.
- Виртуальное окружение, промежуточные матрицы признаков и сырые результаты
серий в архив не включены: они восстанавливаются скриптами.
- Диалоги Stardew Valley принадлежат ConcernedApe; корпус используется
исключительно в учебных и исследовательских целях.

407
lab1/TUNING_EXPERIMENTS.md Normal file
View File

@@ -0,0 +1,407 @@
# Серия настройки (T1–T11)
Документ описывает **дополнительные** эксперименты поверх полной серии
`scripts/run_all_experiments.sh`. Они не заменяют протокол E0–E16 из
`EXPERIMENTS.md` и не повторяют его запуски с теми же параметрами.
Полная серия измеряет качество на **значениях по умолчанию**. Эта серия
проверяет, меняется ли macro-F1, если изменить регуляризацию, состав
признаков, n-граммы, веса блоков, число компонент LSA или гиперпараметры
деревьев.
Справочник «где в коде лежит параметр» — `TUNING.md`. Здесь — что запускать,
зачем и как интерпретировать.
---
## 1. Зачем эта серия
Исследовательский вопрос тот же: различимы ли 34 персонажа по одной реплике.
Базовый ответ даёт `stylometry_pos_lsa` + `logreg` из полной серии.
Дополнительно нужно понять:
- не слишком ли слабая или сильная регуляризация при 2005 объектах и 34 классах;
- не вредят ли нестабильные группы (`vocabulary`, `readability`) и признаки длины;
- не переобучаются ли широкие символьные n-граммы;
- не давит ли один блок признаков на остальные после L2-нормировки;
- не лучше ли другое число компонент LSA, чем `k=100`;
- не лучше ли более мелкие деревья, чем лес без ограничения глубины;
- даёт ли дельта Барроуза что-то на краях диапазона MFW (50 и 1000 слов).
Главная метрика по-прежнему **macro-F1** с бутстрэп-интервалом. Разница в
1–2 процентных пункта часто шум: сравнивайте запуски через `compare`
(Мак-Немар и бутстрэп-разница), а не «на глаз» по одному числу.
---
## 2. Что нельзя путать с полной серией
| Полная серия (`run_all_experiments.sh`) | Эта серия |
|---|---|
| Базовые пресеты, абляции leave-one-out, LSA 50/100/200, benchmark моделей, nested CV | Перебор **внутри** лучшего пресета |
| Logreg/SVM с `C=1`, `class_weight="balanced"` | Другие `C`, отключение весов классов |
| Char `[2,5]` / 5000, POS `[1,3]` / 500, `min_df=3` | Узкие и широкие n-граммы |
| Равные веса блоков | Заданные веса |
| Дельта 100/200/500 MFW | Дельта 50 и 1000 MFW |
| Extra trees нет | Один контролируемый запуск extra trees |
Не запускайте эту серию вместо E0–E16. Сначала нужна база: каталог
`results/stylometry_pos_lsa_logreg_<timestamp>/` с 5 повторами (или хотя бы 3).
---
## 3. Как запускать
Все команды — из каталога `lab1`. Схема CV та же, что в протоколе:
5 блоков, seed `42`. Для черновика достаточно 1 повтора, для отчёта — 3
(лучшие 1–2 конфигурации потом перепроверьте с 5).
### 3.1. Вся серия скриптом
```bash
cd lab1
bash scripts/run_tuning_experiments.sh
```
Быстрый прогон:
```bash
REPEATS=1 bash scripts/run_tuning_experiments.sh
```
Другой интерпретатор:
```bash
PYTHON=../.venv/bin/python bash scripts/run_tuning_experiments.sh
```
Лог: `results/logs/tune_<timestamp>.log`.
Каждый `run` создаёт **новый** каталог `results/<preset>_<model>_<timestamp>/`.
У двух запусков с одним пресетом имена отличаются только меткой времени;
какой это был вариант, смотрите в `run_metadata.json` (`model_params`,
`feature_config`).
### 3.2. Один эксперимент вручную
Конфиги лежат в `configs/tune_*.toml`. Каждый файл — **одна** точка перебора
признаков. Модель и пресет задаются в команде, не в имени файла.
```bash
python -m stylometry.cli run \
--config configs/tune_no_vocab_readability.toml \
--preset stylometry_pos_lsa \
--model logreg \
--repeats 3 \
--seed 42
```
Параметры классификатора toml не требуют — их передаёт `--model-params`.
### 3.3. Сравнение с базой
```bash
python -m stylometry.cli compare \
--run-a results/stylometry_pos_lsa_logreg_<база> \
--run-b results/stylometry_pos_lsa_logreg_<тюнинг>
```
Оба запуска должны иметь одинаковые `--folds` и `--seed`. Иначе парное
сравнение некорректно.
---
## 4. Конфиги признаков
Файлы не «включают серию целиком»: каждый подменяет только указанные поля
`FeatureConfig`. Остальное берётся из пресета `stylometry_pos_lsa`.
| Файл | Что меняет |
|---|---|
| `configs/tune_no_vocab_readability.toml` | группы без `vocabulary` и `readability` |
| `configs/tune_no_vocab_readability_length.toml` | ещё без `length` |
| `configs/tune_char_narrow.toml` | char `[2,4]`, 3000 признаков, POS 500, `min_df=5` |
| `configs/tune_char_wide.toml` | char `[2,6]`, 8000, POS `[1,4]` / 800, `min_df=2` |
| `configs/tune_block_weights.toml` | веса `stylo/char/pos/lsa_word/lsa_pos` |
| `configs/tune_lsa_k150.toml` | `lsa_components = 150` |
| `configs/tune_lsa_k300.toml` | `lsa_components = 300` |
---
## 5. Эксперименты
Базовый пресет везде `stylometry_pos_lsa`, если не сказано иное.
Ориентир для сравнения — pooled macro-F1 базового logreg из полной серии.
### T1. Логистическая регрессия: `C`
**Зачем.** `C` — обратная сила L2-регуляризации. На широком признаковом
пространстве (ручные признаки + n-граммы + LSA) модель с `C=1` может
переобучаться или, наоборот, быть слишком простой. Нужно найти диапазон,
а не одно «магическое» число.
**Что меняется.** `C ∈ {0.03, 0.1, 0.3, 1, 3, 10}`. Остальное как в базе
(`class_weight="balanced"`). Значение `C=1` совпадает с полной серией;
его можно не гонять, если база уже есть.
**Как запускать.**
```bash
for c in 0.03 0.1 0.3 1 3 10; do
python -m stylometry.cli run --preset stylometry_pos_lsa --model logreg \
--repeats 3 --seed 42 --model-params "{\"C\": $c}"
done
```
**Как читать.** Меньше `C` — проще модель. Если macro-F1 растёт при уменьшении
`C`, база переобучалась. Если растёт только при `C ≥ 3`, регуляризации в базе
было слишком много. Смотрите также log loss: он показывает калибровку, не
только точность меток.
### T2. Линейный SVM: `C`
**Зачем.** Та же логика, другой критерий (зазор вместо логистической потери).
SVM в полной серии тоже шёл с `C=1`. Если лучший `C` у SVM и logreg близки,
вывод про регуляризацию устойчив.
**Как запускать.**
```bash
for c in 0.03 0.1 0.3 1 3 10; do
python -m stylometry.cli run --preset stylometry_pos_lsa --model linear_svc \
--repeats 3 --seed 42 --model-params "{\"C\": $c}"
done
```
**Ограничение.** У `linear_svc` нет вероятностей: log loss не считается,
top-k строится по решающей функции.
### T3. Logreg без балансировки классов
**Зачем.** В корпусе 15–139 реплик на персонажа. `class_weight="balanced"`
почти наверняка нужен, но это надо показать: иначе macro-F1 может казаться
низким «из-за модели», хотя виноват сдвиг к частым классам (Abigail и т.д.).
**Как запускать.**
```bash
python -m stylometry.cli run --preset stylometry_pos_lsa --model logreg \
--repeats 3 --seed 42 --model-params '{"class_weight": null}'
```
**Как читать.** Если accuracy почти не падает, а macro-F1 и balanced accuracy
падают, модель без весов игнорирует редких персонажей. Это аргумент в пользу
macro-F1 как основной метрики.
### T4. Без `vocabulary` и `readability`
**Зачем.** На репликах ~19 слов TTR, индексы Honore/Yule и формулы читаемости
нестабильны (`FEATURES.md`, гипотеза H2). Полная серия делает leave-one-out
(`ablate`): удаляет **по одной** группе. Здесь обе шумные группы снимаются
сразу — ближе к тому, как собирали бы «очищенный» основной набор.
**Конфиг.** `configs/tune_no_vocab_readability.toml`
```bash
python -m stylometry.cli run --config configs/tune_no_vocab_readability.toml \
--preset stylometry_pos_lsa --model logreg --repeats 3 --seed 42
```
**Как читать.** Рост или отсутствие падения macro-F1 значит, что эти группы
не несут устойчивого сигнала (или мешают). Сильное падение — в протоколе их
нельзя вычёркивать без оговорки.
### T5. Ещё без `length`
**Зачем.** Гипотеза H4: длинные реплики классифицируются лучше. Если качество
не падает после удаления length-признаков, различимость персонажей не сводится
к «кто говорит длиннее». Если падает — часть результата объясняется длиной,
это нужно явно написать в отчёте.
**Конфиг.** `configs/tune_no_vocab_readability_length.toml`
```bash
python -m stylometry.cli run --config configs/tune_no_vocab_readability_length.toml \
--preset stylometry_pos_lsa --model logreg --repeats 3 --seed 42
```
Имеет смысл сопоставить с `length-report` по базовому запуску.
### T6. Символьные и POS n-граммы: узкие vs широкие
**Зачем.** В полной серии фиксированы char 2–5 / 5000 и POS 1–3 / 500.
Узкий вариант (короче n-граммы, выше `min_df`) проверяет переобучение на
редких орфографических шаблонах. Широкий (2–6, больше признаков, `min_df=2`)
проверяет, не теряем ли морфологию и пунктуационные привычки.
**Конфиги.** `tune_char_narrow.toml`, `tune_char_wide.toml`
```bash
python -m stylometry.cli run --config configs/tune_char_narrow.toml \
--preset stylometry_pos_lsa --model logreg --repeats 3 --seed 42
python -m stylometry.cli run --config configs/tune_char_wide.toml \
--preset stylometry_pos_lsa --model logreg --repeats 3 --seed 42
```
**Как читать.** Если wide лучше на обучении блоков, но pooled macro-F1 не выше
базы (или хуже на редких классах) — переобучение. Если narrow не хуже базы —
лишняя размерность не нужна.
### T7. Веса блоков
**Зачем.** После L2-нормировки каждый блок имеет единичную норму и конкурирует
наравне, даже если один из них шумный. В базе веса равны. Здесь ослабляются
char и особенно `lsa_word` (тематический риск, гипотеза H3), POS и стилометрия
остаются сильнее.
**Конфиг.** `configs/tune_block_weights.toml`
```text
stylo = 1.0
char = 0.5
pos = 0.7
lsa_word = 0.3
lsa_pos = 0.5
```
```bash
python -m stylometry.cli run --config configs/tune_block_weights.toml \
--preset stylometry_pos_lsa --model logreg --repeats 3 --seed 42
```
**Как читать.** Веса — такой же гиперпараметр, как `C`. Один удачный набор не
доказывает оптимум. Если macro-F1 вырос и `lsa_word` при этом ослаблен, это
дополнительный довод, что тема мешала стилю.
### T8. Число компонент LSA: 150 и 300
**Зачем.** В полной серии `lsa-sweep` смотрит `k ∈ {50, 100, 200}` по
**отдельным** вариантам LSA. В основном пресете всегда `k=100` сразу для
`word` и `pos`. Здесь проверяется, помогает ли больше компонент **в
объединённом** наборе, плюс недостающие точки 150 и 300 в переборе вариантов.
**Конфиги.** `tune_lsa_k150.toml`, `tune_lsa_k300.toml`
```bash
python -m stylometry.cli run --config configs/tune_lsa_k150.toml \
--preset stylometry_pos_lsa --model logreg --repeats 3 --seed 42
python -m stylometry.cli run --config configs/tune_lsa_k300.toml \
--preset stylometry_pos_lsa --model logreg --repeats 3 --seed 42
python -m stylometry.cli lsa-sweep \
--variants word,content,fw,char,pos \
--components 150,300 \
--model logreg --repeats 1 --seed 42
```
**Как читать.** Рост `k` без роста macro-F1 — лишние компоненты ловят шум.
Сравнивайте `word`/`content` с `pos`/`fw`: если выигрыш только у word-LSA,
это снова тема, а не стиль.
### T9. Случайный лес: глубина и лист
**Зачем.** В `benchmark` лес идёт с дефолтами (`n_estimators=400`,
`max_depth=None`, `min_samples_leaf=1`). На 2005×34 такая модель легко
запоминает обучающие блоки. Сначала ограничивают глубину и увеличивают лист,
а не число деревьев.
**Как запускать.**
```bash
for depth in 10 20 30; do
for leaf in 2 3 5; do
python -m stylometry.cli run --preset stylometry_pos_lsa --model random_forest \
--repeats 3 --seed 42 \
--model-params "{\"max_depth\": $depth, \"min_samples_leaf\": $leaf}"
done
done
python -m stylometry.cli run --preset stylometry_pos_lsa --model random_forest \
--repeats 3 --seed 42 \
--model-params '{"max_depth": 20, "min_samples_leaf": 3, "max_features": 0.1}'
```
**Как читать.** Если мелкие деревья не хуже (или лучше) дефолтного леса из
benchmark, сложность в базе была лишней. Сравнивайте с logreg, а не только
друг с другом: лес может выигрывать accuracy и проигрывать macro-F1.
### T10. Сверхслучайные деревья
**Зачем.** Extra trees не входили в `benchmark` полной серии. Случайные пороги
обычно снижают дисперсию относительно RF. Один контролируемый запуск
(не полный перебор) проверяет, есть ли смысл включать модель в сравнение.
```bash
python -m stylometry.cli run --preset stylometry_pos_lsa --model extra_trees \
--repeats 3 --seed 42 \
--model-params '{"max_depth": 20, "min_samples_leaf": 3}'
```
Если результат заметно хуже logreg, модель достаточно упомянуть как проверку,
не раздувая сетку.
### T11. Дельта Барроуза: 50 и 1000 MFW
**Зачем.** Классическая стилометрия на самых частотных словах. Полная серия
уже смотрит 100/200/500. Края диапазона: 50 слов — очень компактный профиль,
1000 — больше лексики и больше темы. На коротких репликах дельта обычно
слабее ML; это независимая нижняя граница, не конкурент основному пресету.
```bash
python -m stylometry.cli delta \
--features 50,1000 --metrics manhattan,cosine --repeats 2 --seed 42
```
`zscore` в CLI не переключается (в коде по умолчанию включён), поэтому его
здесь нет.
---
## 6. Рекомендуемый порядок, если запускаете вручную
Не гоните все циклы сразу, если времени мало. Порядок совпадает с `TUNING.md`:
1. T4, T5 — состав групп (дешево и часто полезно).
2. T6 — n-граммы.
3. T8 — `k` LSA в объединённом наборе.
4. T7 — веса, если после T4–T6 остался шумный блок.
5. T1, затем T2 — `C`.
6. T3 — один раз, для отчёта про дисбаланс.
7. T9, T10 — только если линейные модели уже сняты.
8. T11 — короткая классическая проверка.
После отбора 1–2 лучших конфигураций:
```bash
python -m stylometry.cli run --preset stylometry_pos_lsa --model logreg \
--repeats 5 --seed 42 --nested
```
Вложенная CV с расширенной сеткой честнее, чем выбор `C` по тем же OOF,
что потом публикуете. Узкая сетка `{0.1, 1, 10}` уже есть в полной серии;
имеет смысл `--nested` только если T1 сдвинул разумный диапазон (например,
к `{0.03, 0.1, 0.3}`). Для этого нужно изменить `PARAM_GRIDS` в
`stylometry/pipelines.py` — это уже правка кода, не конфиг.
---
## 7. Как фиксировать и чего не делать
Для каждого запуска сохраняются стандартные файлы: `run_metadata.json`,
`pooled_metrics.csv`, `bootstrap_ci.json`, `per_class_report.csv`,
матрица ошибок. Перед сравнением откройте `run_metadata.json` и убедитесь,
что отличаются только задуманные поля.
Нельзя:
- выбирать победителя по тестовой выборке вне CV;
- менять `--folds` или `--seed` «чтобы стало лучше»;
- сравнивать `QUICK=1` / `REPEATS=1` с финальными 5 повторами как равные;
- объявлять улучшение без интервала и `compare`;
- подменять macro-F1 долей правильных ответов.
Ограничения корпуса те же, что в `EXPERIMENTS.md`: короткие реплики, один
автор, ручная фильтрация, дисбаланс 15–139. Настройка модели эти ограничения
не снимает.

View File

@@ -0,0 +1,13 @@
# Ослабить потенциально шумные блоки после L2-нормировки.
[cv]
folds = 5
repeats = 3
seed = 42
[features.block_weights]
stylo = 1.0
char = 0.5
pos = 0.7
lsa_word = 0.3
lsa_pos = 0.5

View File

@@ -0,0 +1,13 @@
# Более короткие char n-граммы, меньше признаков, строже min_df.
[cv]
folds = 5
repeats = 3
seed = 42
[features]
char_ngram_range = [2, 4]
char_max_features = 3000
pos_ngram_range = [1, 3]
pos_max_features = 500
min_df = 5

View File

@@ -0,0 +1,13 @@
# Более длинные n-граммы и больше признаков: риск переобучения.
[cv]
folds = 5
repeats = 3
seed = 42
[features]
char_ngram_range = [2, 6]
char_max_features = 8000
pos_ngram_range = [1, 4]
pos_max_features = 800
min_df = 2

View File

@@ -0,0 +1,7 @@
[cv]
folds = 5
repeats = 3
seed = 42
[features]
lsa_components = 150

View File

@@ -0,0 +1,7 @@
[cv]
folds = 5
repeats = 3
seed = 42
[features]
lsa_components = 300

View File

@@ -0,0 +1,20 @@
# Без нестабильных групп на коротких репликах.
# Остальные поля пресета (n-граммы, LSA) не переопределяются.
[cv]
folds = 5
repeats = 3
seed = 42
[features]
groups = [
"length",
"lexical",
"characters",
"punctuation",
"function_words",
"pronouns",
"pos",
"syntax",
"dialogue",
]

View File

@@ -0,0 +1,18 @@
# То же + без length: проверка, не объясняется ли сигнал длиной реплики.
[cv]
folds = 5
repeats = 3
seed = 42
[features]
groups = [
"lexical",
"characters",
"punctuation",
"function_words",
"pronouns",
"pos",
"syntax",
"dialogue",
]

Binary file not shown.

View File

@@ -1 +0,0 @@
635ed9d56653259be30733fe8b80316e8ce1f3b32ac178170f5deef0f3e39d79 nlp_lab1_submission_20261101.zip

View File

@@ -1 +0,0 @@
fd4aecdf5f7ecfe74b55324f0b4700e25adaa9f6bdf035edbd3017e58152838f exports/nlp_lab1_final_results_20261026_120000.tar.gz

View File

@@ -1,8 +0,0 @@
Дата: 26.10.2026
Git commit: 73c32d10c4ef7689704d8114aa22fd6f4a2e870f
Архив: exports/nlp_lab1_final_results_20261026_120000.tar.gz
Содержимое: results/final
Серии: nested (вложенный выбор конфигурации), topic (контроль содержания),
calibration (коррекция приоров), ensemble (мягкое голосование и stacking),
extra (дополнительные конфигурации и отложенная выборка), finalists.
Ключевые результаты: см. FINAL_RESULTS.csv и FINAL_REPORT.md.

View File

@@ -1 +0,0 @@
fac6cd6286209a20afa732dd8da257fe39208e00cdc8542e73496800ca0aff5e /home/folen/Homework/NLP/lab1/exports/nlp_lab1_results_20261025_173103.tar.gz

View File

@@ -1,254 +0,0 @@
Дата: Вс 25 окт 2026 17:31:03 MSK
Каталог: /home/folen/Homework/NLP/lab1
Интерпретатор: python
=== Проверка окружения ===
Python: 3.11.2 (/home/folen/Homework/NLP/.venv/bin/python)
Каталог проекта: /home/folen/Homework/NLP/lab1
Версии библиотек:
numpy: 2.4.6
pandas: 3.1.0
scipy: 1.17.1
sklearn: 1.9.1
nltk: 3.10.4
matplotlib: 3.11.2
Корпус:
файл: /home/folen/Homework/NLP/lab1/data/dialogues.csv
SHA-256: 8946f08159daddb9d89c9d323205d4a7fb4bc617d16e300ff6c7606cb05030f7
реплик: 2005
персонажей: 34
словоупотреблений: 38207
NLTK:
токенизатор: NLTK
POS-теггер: доступен
пример токенов: ['Oh', ',', 'that', "'s", 'right', '...', 'I', 'heard']
пример POS: ['INTJ', 'PUNCT', 'DET', 'AUX', 'ADJ', 'PUNCT', 'PRON', 'VERB']
Окружение готово.
{"versions": {"numpy": "2.4.6", "pandas": "3.1.0", "scipy": "1.17.1", "sklearn": "1.9.1", "nltk": "3.10.4", "matplotlib": "3.11.2"}, "n_lines": 2005}
Git commit: 10e2444d2342e2d02c94a06de884ad7bbee83bca
Содержимое results:
results/ablation_stylometry_pos_lsa_20261025_170116/ablation.csv
results/benchmark_stylometry_pos_lsa_20261025_165348/summary.csv
results/char_multinomial_nb_20261025_155832/bootstrap_ci.json
results/char_multinomial_nb_20261025_155832/confusion_matrix.csv
results/char_multinomial_nb_20261025_155832/confusion_matrix_normalized.csv
results/char_multinomial_nb_20261025_155832/fold_metrics.csv
results/char_multinomial_nb_20261025_155832/fold_summary.csv
results/char_multinomial_nb_20261025_155832/oof_predictions.csv
results/char_multinomial_nb_20261025_155832/per_class_report.csv
results/char_multinomial_nb_20261025_155832/pooled_metrics.csv
results/char_multinomial_nb_20261025_155832/run_metadata.json
results/char_multinomial_nb_20261025_155832/summary.md
results/char_multinomial_nb_20261025_155832/top_confusions.csv
results/clustering_stylometry_pos_lsa_20261025_172241/character_centroids.csv
results/clustering_stylometry_pos_lsa_20261025_172241/clustering_metrics.csv
results/delta_20261025_172113/delta_sweep.csv
results/fw_logreg_20261025_160058/bootstrap_ci.json
results/fw_logreg_20261025_160058/confusion_matrix.csv
results/fw_logreg_20261025_160058/confusion_matrix_normalized.csv
results/fw_logreg_20261025_160058/fold_metrics.csv
results/fw_logreg_20261025_160058/fold_summary.csv
results/fw_logreg_20261025_160058/oof_predictions.csv
results/fw_logreg_20261025_160058/per_class_report.csv
results/fw_logreg_20261025_160058/pooled_metrics.csv
results/fw_logreg_20261025_160058/run_metadata.json
results/fw_logreg_20261025_160058/summary.md
results/fw_logreg_20261025_160058/top_confusions.csv
results/length_dummy_20261025_155747/bootstrap_ci.json
results/length_dummy_20261025_155747/confusion_matrix.csv
results/length_dummy_20261025_155747/confusion_matrix_normalized.csv
results/length_dummy_20261025_155747/fold_metrics.csv
results/length_dummy_20261025_155747/fold_summary.csv
results/length_dummy_20261025_155747/oof_predictions.csv
results/length_dummy_20261025_155747/per_class_report.csv
results/length_dummy_20261025_155747/pooled_metrics.csv
results/length_dummy_20261025_155747/run_metadata.json
results/length_dummy_20261025_155747/summary.md
results/length_dummy_20261025_155747/top_confusions.csv
results/length_logreg_20261025_155719/bootstrap_ci.json
results/length_logreg_20261025_155719/confusion_matrix.csv
results/length_logreg_20261025_155719/confusion_matrix_normalized.csv
results/length_logreg_20261025_155719/fold_metrics.csv
results/length_logreg_20261025_155719/fold_summary.csv
results/length_logreg_20261025_155719/oof_predictions.csv
results/length_logreg_20261025_155719/per_class_report.csv
results/length_logreg_20261025_155719/pooled_metrics.csv
results/length_logreg_20261025_155719/run_metadata.json
results/length_logreg_20261025_155719/summary.md
results/length_logreg_20261025_155719/top_confusions.csv
results/length_logreg_20261025_155813/bootstrap_ci.json
results/length_logreg_20261025_155813/confusion_matrix.csv
results/length_logreg_20261025_155813/confusion_matrix_normalized.csv
results/length_logreg_20261025_155813/fold_metrics.csv
results/length_logreg_20261025_155813/fold_summary.csv
results/length_logreg_20261025_155813/oof_predictions.csv
results/length_logreg_20261025_155813/per_class_report.csv
results/length_logreg_20261025_155813/pooled_metrics.csv
results/length_logreg_20261025_155813/run_metadata.json
results/length_logreg_20261025_155813/summary.md
results/length_logreg_20261025_155813/top_confusions.csv
results/logs/run_all_20261025_150915.log
results/logs/run_all_20261025_151312.log
results/logs/run_all_20261025_152356.log
results/logs/run_all_20261025_155738.log
results/lsa_char_logreg_20261025_164836/bootstrap_ci.json
results/lsa_char_logreg_20261025_164836/confusion_matrix.csv
results/lsa_char_logreg_20261025_164836/confusion_matrix_normalized.csv
results/lsa_char_logreg_20261025_164836/fold_metrics.csv
results/lsa_char_logreg_20261025_164836/fold_summary.csv
results/lsa_char_logreg_20261025_164836/oof_predictions.csv
results/lsa_char_logreg_20261025_164836/per_class_report.csv
results/lsa_char_logreg_20261025_164836/pooled_metrics.csv
results/lsa_char_logreg_20261025_164836/run_metadata.json
results/lsa_char_logreg_20261025_164836/summary.md
results/lsa_char_logreg_20261025_164836/top_confusions.csv
results/lsa_content_logreg_20261025_164717/bootstrap_ci.json
results/lsa_content_logreg_20261025_164717/confusion_matrix.csv
results/lsa_content_logreg_20261025_164717/confusion_matrix_normalized.csv
results/lsa_content_logreg_20261025_164717/fold_metrics.csv
results/lsa_content_logreg_20261025_164717/fold_summary.csv
results/lsa_content_logreg_20261025_164717/oof_predictions.csv
results/lsa_content_logreg_20261025_164717/per_class_report.csv
results/lsa_content_logreg_20261025_164717/pooled_metrics.csv
results/lsa_content_logreg_20261025_164717/run_metadata.json
results/lsa_content_logreg_20261025_164717/summary.md
results/lsa_content_logreg_20261025_164717/top_confusions.csv
results/lsa_fw_logreg_20261025_164750/bootstrap_ci.json
results/lsa_fw_logreg_20261025_164750/confusion_matrix.csv
results/lsa_fw_logreg_20261025_164750/confusion_matrix_normalized.csv
results/lsa_fw_logreg_20261025_164750/fold_metrics.csv
results/lsa_fw_logreg_20261025_164750/fold_summary.csv
results/lsa_fw_logreg_20261025_164750/oof_predictions.csv
results/lsa_fw_logreg_20261025_164750/per_class_report.csv
results/lsa_fw_logreg_20261025_164750/pooled_metrics.csv
results/lsa_fw_logreg_20261025_164750/run_metadata.json
results/lsa_fw_logreg_20261025_164750/summary.md
results/lsa_fw_logreg_20261025_164750/top_confusions.csv
results/lsa_pos_logreg_20261025_164912/bootstrap_ci.json
results/lsa_pos_logreg_20261025_164912/confusion_matrix.csv
results/lsa_pos_logreg_20261025_164912/confusion_matrix_normalized.csv
results/lsa_pos_logreg_20261025_164912/fold_metrics.csv
results/lsa_pos_logreg_20261025_164912/fold_summary.csv
results/lsa_pos_logreg_20261025_164912/oof_predictions.csv
results/lsa_pos_logreg_20261025_164912/per_class_report.csv
results/lsa_pos_logreg_20261025_164912/pooled_metrics.csv
results/lsa_pos_logreg_20261025_164912/run_metadata.json
results/lsa_pos_logreg_20261025_164912/summary.md
results/lsa_pos_logreg_20261025_164912/top_confusions.csv
results/lsa_sweep_20261025_164924/lsa_sweep.csv
results/lsa_word2_logreg_20261025_164641/bootstrap_ci.json
results/lsa_word2_logreg_20261025_164641/confusion_matrix.csv
results/lsa_word2_logreg_20261025_164641/confusion_matrix_normalized.csv
results/lsa_word2_logreg_20261025_164641/fold_metrics.csv
results/lsa_word2_logreg_20261025_164641/fold_summary.csv
results/lsa_word2_logreg_20261025_164641/oof_predictions.csv
results/lsa_word2_logreg_20261025_164641/per_class_report.csv
results/lsa_word2_logreg_20261025_164641/pooled_metrics.csv
results/lsa_word2_logreg_20261025_164641/run_metadata.json
results/lsa_word2_logreg_20261025_164641/summary.md
results/lsa_word2_logreg_20261025_164641/top_confusions.csv
results/lsa_word_logreg_20261025_164608/bootstrap_ci.json
results/lsa_word_logreg_20261025_164608/confusion_matrix.csv
results/lsa_word_logreg_20261025_164608/confusion_matrix_normalized.csv
results/lsa_word_logreg_20261025_164608/fold_metrics.csv
results/lsa_word_logreg_20261025_164608/fold_summary.csv
results/lsa_word_logreg_20261025_164608/oof_predictions.csv
results/lsa_word_logreg_20261025_164608/per_class_report.csv
results/lsa_word_logreg_20261025_164608/pooled_metrics.csv
results/lsa_word_logreg_20261025_164608/run_metadata.json
results/lsa_word_logreg_20261025_164608/summary.md
results/lsa_word_logreg_20261025_164608/top_confusions.csv
results/nested_stylometry_pos_lsa_linear_svc_20261025_172442/best_params.json
results/nested_stylometry_pos_lsa_linear_svc_20261025_172442/bootstrap_ci.json
results/nested_stylometry_pos_lsa_linear_svc_20261025_172442/confusion_matrix.csv
results/nested_stylometry_pos_lsa_linear_svc_20261025_172442/confusion_matrix_normalized.csv
results/nested_stylometry_pos_lsa_linear_svc_20261025_172442/fold_metrics.csv
results/nested_stylometry_pos_lsa_linear_svc_20261025_172442/fold_summary.csv
results/nested_stylometry_pos_lsa_linear_svc_20261025_172442/oof_predictions.csv
results/nested_stylometry_pos_lsa_linear_svc_20261025_172442/per_class_report.csv
results/nested_stylometry_pos_lsa_linear_svc_20261025_172442/pooled_metrics.csv
results/nested_stylometry_pos_lsa_linear_svc_20261025_172442/run_metadata.json
results/nested_stylometry_pos_lsa_linear_svc_20261025_172442/summary.md
results/nested_stylometry_pos_lsa_linear_svc_20261025_172442/top_confusions.csv
results/nested_stylometry_pos_lsa_logreg_20261025_172348/best_params.json
results/nested_stylometry_pos_lsa_logreg_20261025_172348/bootstrap_ci.json
results/nested_stylometry_pos_lsa_logreg_20261025_172348/confusion_matrix.csv
results/nested_stylometry_pos_lsa_logreg_20261025_172348/confusion_matrix_normalized.csv
results/nested_stylometry_pos_lsa_logreg_20261025_172348/fold_metrics.csv
results/nested_stylometry_pos_lsa_logreg_20261025_172348/fold_summary.csv
results/nested_stylometry_pos_lsa_logreg_20261025_172348/oof_predictions.csv
results/nested_stylometry_pos_lsa_logreg_20261025_172348/per_class_report.csv
results/nested_stylometry_pos_lsa_logreg_20261025_172348/pooled_metrics.csv
results/nested_stylometry_pos_lsa_logreg_20261025_172348/run_metadata.json
results/nested_stylometry_pos_lsa_logreg_20261025_172348/summary.md
results/nested_stylometry_pos_lsa_logreg_20261025_172348/top_confusions.csv
results/pos_multinomial_nb_20261025_155848/bootstrap_ci.json
results/pos_multinomial_nb_20261025_155848/confusion_matrix.csv
results/pos_multinomial_nb_20261025_155848/confusion_matrix_normalized.csv
results/pos_multinomial_nb_20261025_155848/fold_metrics.csv
results/pos_multinomial_nb_20261025_155848/fold_summary.csv
results/pos_multinomial_nb_20261025_155848/oof_predictions.csv
results/pos_multinomial_nb_20261025_155848/per_class_report.csv
results/pos_multinomial_nb_20261025_155848/pooled_metrics.csv
results/pos_multinomial_nb_20261025_155848/run_metadata.json
results/pos_multinomial_nb_20261025_155848/summary.md
results/pos_multinomial_nb_20261025_155848/top_confusions.csv
results/README.md
results/stylometry_linear_svc_20261025_164018/bootstrap_ci.json
results/stylometry_linear_svc_20261025_164018/confusion_matrix.csv
results/stylometry_linear_svc_20261025_164018/confusion_matrix_normalized.csv
results/stylometry_linear_svc_20261025_164018/fold_metrics.csv
results/stylometry_linear_svc_20261025_164018/fold_summary.csv
results/stylometry_linear_svc_20261025_164018/oof_predictions.csv
results/stylometry_linear_svc_20261025_164018/per_class_report.csv
results/stylometry_linear_svc_20261025_164018/pooled_metrics.csv
results/stylometry_linear_svc_20261025_164018/run_metadata.json
results/stylometry_linear_svc_20261025_164018/summary.md
results/stylometry_linear_svc_20261025_164018/top_confusions.csv
results/stylometry_logreg_20261025_160722/bootstrap_ci.json
results/stylometry_logreg_20261025_160722/confusion_matrix.csv
results/stylometry_logreg_20261025_160722/confusion_matrix_normalized.csv
results/stylometry_logreg_20261025_160722/fold_metrics.csv
results/stylometry_logreg_20261025_160722/fold_summary.csv
results/stylometry_logreg_20261025_160722/oof_predictions.csv
results/stylometry_logreg_20261025_160722/per_class_report.csv
results/stylometry_logreg_20261025_160722/pooled_metrics.csv
results/stylometry_logreg_20261025_160722/run_metadata.json
results/stylometry_logreg_20261025_160722/summary.md
results/stylometry_logreg_20261025_160722/top_confusions.csv
results/stylometry_pos_logreg_20261025_164246/bootstrap_ci.json
results/stylometry_pos_logreg_20261025_164246/confusion_matrix.csv
results/stylometry_pos_logreg_20261025_164246/confusion_matrix_normalized.csv
results/stylometry_pos_logreg_20261025_164246/fold_metrics.csv
results/stylometry_pos_logreg_20261025_164246/fold_summary.csv
results/stylometry_pos_logreg_20261025_164246/oof_predictions.csv
results/stylometry_pos_logreg_20261025_164246/per_class_report.csv
results/stylometry_pos_logreg_20261025_164246/pooled_metrics.csv
results/stylometry_pos_logreg_20261025_164246/run_metadata.json
results/stylometry_pos_logreg_20261025_164246/summary.md
results/stylometry_pos_logreg_20261025_164246/top_confusions.csv
results/stylometry_pos_lsa_logreg_20261025_164534/bootstrap_ci.json
results/stylometry_pos_lsa_logreg_20261025_164534/comparison_main_vs_pos.json
results/stylometry_pos_lsa_logreg_20261025_164534/confusion_matrix.csv
results/stylometry_pos_lsa_logreg_20261025_164534/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_logreg_20261025_164534/fold_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_164534/fold_summary.csv
results/stylometry_pos_lsa_logreg_20261025_164534/length_bins.csv
results/stylometry_pos_lsa_logreg_20261025_164534/oof_predictions.csv
results/stylometry_pos_lsa_logreg_20261025_164534/per_class_report.csv
results/stylometry_pos_lsa_logreg_20261025_164534/pooled_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_164534/run_metadata.json
results/stylometry_pos_lsa_logreg_20261025_164534/subgroup_age_group_confusions.csv
results/stylometry_pos_lsa_logreg_20261025_164534/subgroup_age_group_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_164534/subgroup_gender_confusions.csv
results/stylometry_pos_lsa_logreg_20261025_164534/subgroup_gender_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_164534/subgroup_species_confusions.csv
results/stylometry_pos_lsa_logreg_20261025_164534/subgroup_species_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_164534/summary.md
results/stylometry_pos_lsa_logreg_20261025_164534/top_confusions.csv
results/verification_stylometry_pos_lsa_20261025_172237/fold_metrics.csv
results/verification_stylometry_pos_lsa_20261025_172237/pair_scores.csv
results/verification_stylometry_pos_lsa_20261025_172237/summary.json

View File

@@ -1 +0,0 @@
866cbec84ad3c79cdf7cdee14a534d736894037f68af88e4629262a5821cbefb /home/folen/Homework/NLP/lab1/exports/nlp_lab1_results_20261025_192444.tar.gz

View File

@@ -1,486 +0,0 @@
Дата: Вс 25 окт 2026 19:24:44 MSK
Каталог: /home/folen/Homework/NLP/lab1
Интерпретатор: python
=== Проверка окружения ===
Python: 3.11.2 (/home/folen/Homework/NLP/.venv/bin/python)
Каталог проекта: /home/folen/Homework/NLP/lab1
Версии библиотек:
numpy: 2.4.6
pandas: 3.1.0
scipy: 1.17.1
sklearn: 1.9.1
nltk: 3.10.4
matplotlib: 3.11.2
Корпус:
файл: /home/folen/Homework/NLP/lab1/data/dialogues.csv
SHA-256: 8946f08159daddb9d89c9d323205d4a7fb4bc617d16e300ff6c7606cb05030f7
реплик: 2005
персонажей: 34
словоупотреблений: 38207
NLTK:
токенизатор: NLTK
POS-теггер: доступен
пример токенов: ['Oh', ',', 'that', "'s", 'right', '...', 'I', 'heard']
пример POS: ['INTJ', 'PUNCT', 'DET', 'AUX', 'ADJ', 'PUNCT', 'PRON', 'VERB']
Окружение готово.
{"versions": {"numpy": "2.4.6", "pandas": "3.1.0", "scipy": "1.17.1", "sklearn": "1.9.1", "nltk": "3.10.4", "matplotlib": "3.11.2"}, "n_lines": 2005}
Git commit: 5a4a19e0df4cae19348ac76d242afb451d8e6aa0
Содержимое results:
results/ablation_stylometry_pos_lsa_20261025_170116/ablation.csv
results/benchmark_stylometry_pos_lsa_20261025_165348/summary.csv
results/char_multinomial_nb_20261025_155832/bootstrap_ci.json
results/char_multinomial_nb_20261025_155832/confusion_matrix.csv
results/char_multinomial_nb_20261025_155832/confusion_matrix_normalized.csv
results/char_multinomial_nb_20261025_155832/fold_metrics.csv
results/char_multinomial_nb_20261025_155832/fold_summary.csv
results/char_multinomial_nb_20261025_155832/oof_predictions.csv
results/char_multinomial_nb_20261025_155832/per_class_report.csv
results/char_multinomial_nb_20261025_155832/pooled_metrics.csv
results/char_multinomial_nb_20261025_155832/run_metadata.json
results/char_multinomial_nb_20261025_155832/summary.md
results/char_multinomial_nb_20261025_155832/top_confusions.csv
results/clustering_stylometry_pos_lsa_20261025_172241/character_centroids.csv
results/clustering_stylometry_pos_lsa_20261025_172241/clustering_metrics.csv
results/delta_20261025_172113/delta_sweep.csv
results/fw_logreg_20261025_160058/bootstrap_ci.json
results/fw_logreg_20261025_160058/confusion_matrix.csv
results/fw_logreg_20261025_160058/confusion_matrix_normalized.csv
results/fw_logreg_20261025_160058/fold_metrics.csv
results/fw_logreg_20261025_160058/fold_summary.csv
results/fw_logreg_20261025_160058/oof_predictions.csv
results/fw_logreg_20261025_160058/per_class_report.csv
results/fw_logreg_20261025_160058/pooled_metrics.csv
results/fw_logreg_20261025_160058/run_metadata.json
results/fw_logreg_20261025_160058/summary.md
results/fw_logreg_20261025_160058/top_confusions.csv
results/length_dummy_20261025_155747/bootstrap_ci.json
results/length_dummy_20261025_155747/confusion_matrix.csv
results/length_dummy_20261025_155747/confusion_matrix_normalized.csv
results/length_dummy_20261025_155747/fold_metrics.csv
results/length_dummy_20261025_155747/fold_summary.csv
results/length_dummy_20261025_155747/oof_predictions.csv
results/length_dummy_20261025_155747/per_class_report.csv
results/length_dummy_20261025_155747/pooled_metrics.csv
results/length_dummy_20261025_155747/run_metadata.json
results/length_dummy_20261025_155747/summary.md
results/length_dummy_20261025_155747/top_confusions.csv
results/length_logreg_20261025_155719/bootstrap_ci.json
results/length_logreg_20261025_155719/confusion_matrix.csv
results/length_logreg_20261025_155719/confusion_matrix_normalized.csv
results/length_logreg_20261025_155719/fold_metrics.csv
results/length_logreg_20261025_155719/fold_summary.csv
results/length_logreg_20261025_155719/oof_predictions.csv
results/length_logreg_20261025_155719/per_class_report.csv
results/length_logreg_20261025_155719/pooled_metrics.csv
results/length_logreg_20261025_155719/run_metadata.json
results/length_logreg_20261025_155719/summary.md
results/length_logreg_20261025_155719/top_confusions.csv
results/length_logreg_20261025_155813/bootstrap_ci.json
results/length_logreg_20261025_155813/confusion_matrix.csv
results/length_logreg_20261025_155813/confusion_matrix_normalized.csv
results/length_logreg_20261025_155813/fold_metrics.csv
results/length_logreg_20261025_155813/fold_summary.csv
results/length_logreg_20261025_155813/oof_predictions.csv
results/length_logreg_20261025_155813/per_class_report.csv
results/length_logreg_20261025_155813/pooled_metrics.csv
results/length_logreg_20261025_155813/run_metadata.json
results/length_logreg_20261025_155813/summary.md
results/length_logreg_20261025_155813/top_confusions.csv
results/logs/run_all_20261025_150915.log
results/logs/run_all_20261025_151312.log
results/logs/run_all_20261025_152356.log
results/logs/run_all_20261025_155738.log
results/lsa_char_logreg_20261025_164836/bootstrap_ci.json
results/lsa_char_logreg_20261025_164836/confusion_matrix.csv
results/lsa_char_logreg_20261025_164836/confusion_matrix_normalized.csv
results/lsa_char_logreg_20261025_164836/fold_metrics.csv
results/lsa_char_logreg_20261025_164836/fold_summary.csv
results/lsa_char_logreg_20261025_164836/oof_predictions.csv
results/lsa_char_logreg_20261025_164836/per_class_report.csv
results/lsa_char_logreg_20261025_164836/pooled_metrics.csv
results/lsa_char_logreg_20261025_164836/run_metadata.json
results/lsa_char_logreg_20261025_164836/summary.md
results/lsa_char_logreg_20261025_164836/top_confusions.csv
results/lsa_content_logreg_20261025_164717/bootstrap_ci.json
results/lsa_content_logreg_20261025_164717/confusion_matrix.csv
results/lsa_content_logreg_20261025_164717/confusion_matrix_normalized.csv
results/lsa_content_logreg_20261025_164717/fold_metrics.csv
results/lsa_content_logreg_20261025_164717/fold_summary.csv
results/lsa_content_logreg_20261025_164717/oof_predictions.csv
results/lsa_content_logreg_20261025_164717/per_class_report.csv
results/lsa_content_logreg_20261025_164717/pooled_metrics.csv
results/lsa_content_logreg_20261025_164717/run_metadata.json
results/lsa_content_logreg_20261025_164717/summary.md
results/lsa_content_logreg_20261025_164717/top_confusions.csv
results/lsa_fw_logreg_20261025_164750/bootstrap_ci.json
results/lsa_fw_logreg_20261025_164750/confusion_matrix.csv
results/lsa_fw_logreg_20261025_164750/confusion_matrix_normalized.csv
results/lsa_fw_logreg_20261025_164750/fold_metrics.csv
results/lsa_fw_logreg_20261025_164750/fold_summary.csv
results/lsa_fw_logreg_20261025_164750/oof_predictions.csv
results/lsa_fw_logreg_20261025_164750/per_class_report.csv
results/lsa_fw_logreg_20261025_164750/pooled_metrics.csv
results/lsa_fw_logreg_20261025_164750/run_metadata.json
results/lsa_fw_logreg_20261025_164750/summary.md
results/lsa_fw_logreg_20261025_164750/top_confusions.csv
results/lsa_pos_logreg_20261025_164912/bootstrap_ci.json
results/lsa_pos_logreg_20261025_164912/confusion_matrix.csv
results/lsa_pos_logreg_20261025_164912/confusion_matrix_normalized.csv
results/lsa_pos_logreg_20261025_164912/fold_metrics.csv
results/lsa_pos_logreg_20261025_164912/fold_summary.csv
results/lsa_pos_logreg_20261025_164912/oof_predictions.csv
results/lsa_pos_logreg_20261025_164912/per_class_report.csv
results/lsa_pos_logreg_20261025_164912/pooled_metrics.csv
results/lsa_pos_logreg_20261025_164912/run_metadata.json
results/lsa_pos_logreg_20261025_164912/summary.md
results/lsa_pos_logreg_20261025_164912/top_confusions.csv
results/lsa_sweep_20261025_164924/lsa_sweep.csv
results/lsa_sweep_20261025_185049/lsa_sweep.csv
results/lsa_word2_logreg_20261025_164641/bootstrap_ci.json
results/lsa_word2_logreg_20261025_164641/confusion_matrix.csv
results/lsa_word2_logreg_20261025_164641/confusion_matrix_normalized.csv
results/lsa_word2_logreg_20261025_164641/fold_metrics.csv
results/lsa_word2_logreg_20261025_164641/fold_summary.csv
results/lsa_word2_logreg_20261025_164641/oof_predictions.csv
results/lsa_word2_logreg_20261025_164641/per_class_report.csv
results/lsa_word2_logreg_20261025_164641/pooled_metrics.csv
results/lsa_word2_logreg_20261025_164641/run_metadata.json
results/lsa_word2_logreg_20261025_164641/summary.md
results/lsa_word2_logreg_20261025_164641/top_confusions.csv
results/lsa_word_logreg_20261025_164608/bootstrap_ci.json
results/lsa_word_logreg_20261025_164608/confusion_matrix.csv
results/lsa_word_logreg_20261025_164608/confusion_matrix_normalized.csv
results/lsa_word_logreg_20261025_164608/fold_metrics.csv
results/lsa_word_logreg_20261025_164608/fold_summary.csv
results/lsa_word_logreg_20261025_164608/oof_predictions.csv
results/lsa_word_logreg_20261025_164608/per_class_report.csv
results/lsa_word_logreg_20261025_164608/pooled_metrics.csv
results/lsa_word_logreg_20261025_164608/run_metadata.json
results/lsa_word_logreg_20261025_164608/summary.md
results/lsa_word_logreg_20261025_164608/top_confusions.csv
results/nested_stylometry_pos_lsa_linear_svc_20261025_172442/best_params.json
results/nested_stylometry_pos_lsa_linear_svc_20261025_172442/bootstrap_ci.json
results/nested_stylometry_pos_lsa_linear_svc_20261025_172442/confusion_matrix.csv
results/nested_stylometry_pos_lsa_linear_svc_20261025_172442/confusion_matrix_normalized.csv
results/nested_stylometry_pos_lsa_linear_svc_20261025_172442/fold_metrics.csv
results/nested_stylometry_pos_lsa_linear_svc_20261025_172442/fold_summary.csv
results/nested_stylometry_pos_lsa_linear_svc_20261025_172442/oof_predictions.csv
results/nested_stylometry_pos_lsa_linear_svc_20261025_172442/per_class_report.csv
results/nested_stylometry_pos_lsa_linear_svc_20261025_172442/pooled_metrics.csv
results/nested_stylometry_pos_lsa_linear_svc_20261025_172442/run_metadata.json
results/nested_stylometry_pos_lsa_linear_svc_20261025_172442/summary.md
results/nested_stylometry_pos_lsa_linear_svc_20261025_172442/top_confusions.csv
results/nested_stylometry_pos_lsa_logreg_20261025_172348/best_params.json
results/nested_stylometry_pos_lsa_logreg_20261025_172348/bootstrap_ci.json
results/nested_stylometry_pos_lsa_logreg_20261025_172348/confusion_matrix.csv
results/nested_stylometry_pos_lsa_logreg_20261025_172348/confusion_matrix_normalized.csv
results/nested_stylometry_pos_lsa_logreg_20261025_172348/fold_metrics.csv
results/nested_stylometry_pos_lsa_logreg_20261025_172348/fold_summary.csv
results/nested_stylometry_pos_lsa_logreg_20261025_172348/oof_predictions.csv
results/nested_stylometry_pos_lsa_logreg_20261025_172348/per_class_report.csv
results/nested_stylometry_pos_lsa_logreg_20261025_172348/pooled_metrics.csv
results/nested_stylometry_pos_lsa_logreg_20261025_172348/run_metadata.json
results/nested_stylometry_pos_lsa_logreg_20261025_172348/summary.md
results/nested_stylometry_pos_lsa_logreg_20261025_172348/top_confusions.csv
results/pos_multinomial_nb_20261025_155848/bootstrap_ci.json
results/pos_multinomial_nb_20261025_155848/confusion_matrix.csv
results/pos_multinomial_nb_20261025_155848/confusion_matrix_normalized.csv
results/pos_multinomial_nb_20261025_155848/fold_metrics.csv
results/pos_multinomial_nb_20261025_155848/fold_summary.csv
results/pos_multinomial_nb_20261025_155848/oof_predictions.csv
results/pos_multinomial_nb_20261025_155848/per_class_report.csv
results/pos_multinomial_nb_20261025_155848/pooled_metrics.csv
results/pos_multinomial_nb_20261025_155848/run_metadata.json
results/pos_multinomial_nb_20261025_155848/summary.md
results/pos_multinomial_nb_20261025_155848/top_confusions.csv
results/README.md
results/stylometry_linear_svc_20261025_164018/bootstrap_ci.json
results/stylometry_linear_svc_20261025_164018/confusion_matrix.csv
results/stylometry_linear_svc_20261025_164018/confusion_matrix_normalized.csv
results/stylometry_linear_svc_20261025_164018/fold_metrics.csv
results/stylometry_linear_svc_20261025_164018/fold_summary.csv
results/stylometry_linear_svc_20261025_164018/oof_predictions.csv
results/stylometry_linear_svc_20261025_164018/per_class_report.csv
results/stylometry_linear_svc_20261025_164018/pooled_metrics.csv
results/stylometry_linear_svc_20261025_164018/run_metadata.json
results/stylometry_linear_svc_20261025_164018/summary.md
results/stylometry_linear_svc_20261025_164018/top_confusions.csv
results/stylometry_logreg_20261025_160722/bootstrap_ci.json
results/stylometry_logreg_20261025_160722/confusion_matrix.csv
results/stylometry_logreg_20261025_160722/confusion_matrix_normalized.csv
results/stylometry_logreg_20261025_160722/fold_metrics.csv
results/stylometry_logreg_20261025_160722/fold_summary.csv
results/stylometry_logreg_20261025_160722/oof_predictions.csv
results/stylometry_logreg_20261025_160722/per_class_report.csv
results/stylometry_logreg_20261025_160722/pooled_metrics.csv
results/stylometry_logreg_20261025_160722/run_metadata.json
results/stylometry_logreg_20261025_160722/summary.md
results/stylometry_logreg_20261025_160722/top_confusions.csv
results/stylometry_pos_logreg_20261025_164246/bootstrap_ci.json
results/stylometry_pos_logreg_20261025_164246/confusion_matrix.csv
results/stylometry_pos_logreg_20261025_164246/confusion_matrix_normalized.csv
results/stylometry_pos_logreg_20261025_164246/fold_metrics.csv
results/stylometry_pos_logreg_20261025_164246/fold_summary.csv
results/stylometry_pos_logreg_20261025_164246/oof_predictions.csv
results/stylometry_pos_logreg_20261025_164246/per_class_report.csv
results/stylometry_pos_logreg_20261025_164246/pooled_metrics.csv
results/stylometry_pos_logreg_20261025_164246/run_metadata.json
results/stylometry_pos_logreg_20261025_164246/summary.md
results/stylometry_pos_logreg_20261025_164246/top_confusions.csv
results/stylometry_pos_lsa_linear_svc_20261025_184431/bootstrap_ci.json
results/stylometry_pos_lsa_linear_svc_20261025_184431/confusion_matrix.csv
results/stylometry_pos_lsa_linear_svc_20261025_184431/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_linear_svc_20261025_184431/fold_metrics.csv
results/stylometry_pos_lsa_linear_svc_20261025_184431/fold_summary.csv
results/stylometry_pos_lsa_linear_svc_20261025_184431/oof_predictions.csv
results/stylometry_pos_lsa_linear_svc_20261025_184431/per_class_report.csv
results/stylometry_pos_lsa_linear_svc_20261025_184431/pooled_metrics.csv
results/stylometry_pos_lsa_linear_svc_20261025_184431/run_metadata.json
results/stylometry_pos_lsa_linear_svc_20261025_184431/summary.md
results/stylometry_pos_lsa_linear_svc_20261025_184431/top_confusions.csv
results/stylometry_pos_lsa_linear_svc_20261025_184538/bootstrap_ci.json
results/stylometry_pos_lsa_linear_svc_20261025_184538/confusion_matrix.csv
results/stylometry_pos_lsa_linear_svc_20261025_184538/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_linear_svc_20261025_184538/fold_metrics.csv
results/stylometry_pos_lsa_linear_svc_20261025_184538/fold_summary.csv
results/stylometry_pos_lsa_linear_svc_20261025_184538/oof_predictions.csv
results/stylometry_pos_lsa_linear_svc_20261025_184538/per_class_report.csv
results/stylometry_pos_lsa_linear_svc_20261025_184538/pooled_metrics.csv
results/stylometry_pos_lsa_linear_svc_20261025_184538/run_metadata.json
results/stylometry_pos_lsa_linear_svc_20261025_184538/summary.md
results/stylometry_pos_lsa_linear_svc_20261025_184538/top_confusions.csv
results/stylometry_pos_lsa_linear_svc_20261025_184646/bootstrap_ci.json
results/stylometry_pos_lsa_linear_svc_20261025_184646/confusion_matrix.csv
results/stylometry_pos_lsa_linear_svc_20261025_184646/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_linear_svc_20261025_184646/fold_metrics.csv
results/stylometry_pos_lsa_linear_svc_20261025_184646/fold_summary.csv
results/stylometry_pos_lsa_linear_svc_20261025_184646/oof_predictions.csv
results/stylometry_pos_lsa_linear_svc_20261025_184646/per_class_report.csv
results/stylometry_pos_lsa_linear_svc_20261025_184646/pooled_metrics.csv
results/stylometry_pos_lsa_linear_svc_20261025_184646/run_metadata.json
results/stylometry_pos_lsa_linear_svc_20261025_184646/summary.md
results/stylometry_pos_lsa_linear_svc_20261025_184646/top_confusions.csv
results/stylometry_pos_lsa_linear_svc_20261025_184759/bootstrap_ci.json
results/stylometry_pos_lsa_linear_svc_20261025_184759/confusion_matrix.csv
results/stylometry_pos_lsa_linear_svc_20261025_184759/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_linear_svc_20261025_184759/fold_metrics.csv
results/stylometry_pos_lsa_linear_svc_20261025_184759/fold_summary.csv
results/stylometry_pos_lsa_linear_svc_20261025_184759/oof_predictions.csv
results/stylometry_pos_lsa_linear_svc_20261025_184759/per_class_report.csv
results/stylometry_pos_lsa_linear_svc_20261025_184759/pooled_metrics.csv
results/stylometry_pos_lsa_linear_svc_20261025_184759/run_metadata.json
results/stylometry_pos_lsa_linear_svc_20261025_184759/summary.md
results/stylometry_pos_lsa_linear_svc_20261025_184759/top_confusions.csv
results/stylometry_pos_lsa_linear_svc_20261025_184916/bootstrap_ci.json
results/stylometry_pos_lsa_linear_svc_20261025_184916/confusion_matrix.csv
results/stylometry_pos_lsa_linear_svc_20261025_184916/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_linear_svc_20261025_184916/fold_metrics.csv
results/stylometry_pos_lsa_linear_svc_20261025_184916/fold_summary.csv
results/stylometry_pos_lsa_linear_svc_20261025_184916/oof_predictions.csv
results/stylometry_pos_lsa_linear_svc_20261025_184916/per_class_report.csv
results/stylometry_pos_lsa_linear_svc_20261025_184916/pooled_metrics.csv
results/stylometry_pos_lsa_linear_svc_20261025_184916/run_metadata.json
results/stylometry_pos_lsa_linear_svc_20261025_184916/summary.md
results/stylometry_pos_lsa_linear_svc_20261025_184916/top_confusions.csv
results/stylometry_pos_lsa_linear_svc_20261025_185037/bootstrap_ci.json
results/stylometry_pos_lsa_linear_svc_20261025_185037/confusion_matrix.csv
results/stylometry_pos_lsa_linear_svc_20261025_185037/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_linear_svc_20261025_185037/fold_metrics.csv
results/stylometry_pos_lsa_linear_svc_20261025_185037/fold_summary.csv
results/stylometry_pos_lsa_linear_svc_20261025_185037/oof_predictions.csv
results/stylometry_pos_lsa_linear_svc_20261025_185037/per_class_report.csv
results/stylometry_pos_lsa_linear_svc_20261025_185037/pooled_metrics.csv
results/stylometry_pos_lsa_linear_svc_20261025_185037/run_metadata.json
results/stylometry_pos_lsa_linear_svc_20261025_185037/summary.md
results/stylometry_pos_lsa_linear_svc_20261025_185037/top_confusions.csv
results/stylometry_pos_lsa_logreg_20261025_164534/bootstrap_ci.json
results/stylometry_pos_lsa_logreg_20261025_164534/comparison_main_vs_pos.json
results/stylometry_pos_lsa_logreg_20261025_164534/confusion_matrix.csv
results/stylometry_pos_lsa_logreg_20261025_164534/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_logreg_20261025_164534/fold_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_164534/fold_summary.csv
results/stylometry_pos_lsa_logreg_20261025_164534/length_bins.csv
results/stylometry_pos_lsa_logreg_20261025_164534/oof_predictions.csv
results/stylometry_pos_lsa_logreg_20261025_164534/per_class_report.csv
results/stylometry_pos_lsa_logreg_20261025_164534/pooled_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_164534/run_metadata.json
results/stylometry_pos_lsa_logreg_20261025_164534/subgroup_age_group_confusions.csv
results/stylometry_pos_lsa_logreg_20261025_164534/subgroup_age_group_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_164534/subgroup_gender_confusions.csv
results/stylometry_pos_lsa_logreg_20261025_164534/subgroup_gender_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_164534/subgroup_species_confusions.csv
results/stylometry_pos_lsa_logreg_20261025_164534/subgroup_species_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_164534/summary.md
results/stylometry_pos_lsa_logreg_20261025_164534/top_confusions.csv
results/stylometry_pos_lsa_logreg_20261025_183404/bootstrap_ci.json
results/stylometry_pos_lsa_logreg_20261025_183404/confusion_matrix.csv
results/stylometry_pos_lsa_logreg_20261025_183404/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_logreg_20261025_183404/fold_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_183404/fold_summary.csv
results/stylometry_pos_lsa_logreg_20261025_183404/oof_predictions.csv
results/stylometry_pos_lsa_logreg_20261025_183404/per_class_report.csv
results/stylometry_pos_lsa_logreg_20261025_183404/pooled_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_183404/run_metadata.json
results/stylometry_pos_lsa_logreg_20261025_183404/summary.md
results/stylometry_pos_lsa_logreg_20261025_183404/top_confusions.csv
results/stylometry_pos_lsa_logreg_20261025_183524/bootstrap_ci.json
results/stylometry_pos_lsa_logreg_20261025_183524/confusion_matrix.csv
results/stylometry_pos_lsa_logreg_20261025_183524/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_logreg_20261025_183524/fold_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_183524/fold_summary.csv
results/stylometry_pos_lsa_logreg_20261025_183524/oof_predictions.csv
results/stylometry_pos_lsa_logreg_20261025_183524/per_class_report.csv
results/stylometry_pos_lsa_logreg_20261025_183524/pooled_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_183524/run_metadata.json
results/stylometry_pos_lsa_logreg_20261025_183524/summary.md
results/stylometry_pos_lsa_logreg_20261025_183524/top_confusions.csv
results/stylometry_pos_lsa_logreg_20261025_183653/bootstrap_ci.json
results/stylometry_pos_lsa_logreg_20261025_183653/confusion_matrix.csv
results/stylometry_pos_lsa_logreg_20261025_183653/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_logreg_20261025_183653/fold_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_183653/fold_summary.csv
results/stylometry_pos_lsa_logreg_20261025_183653/oof_predictions.csv
results/stylometry_pos_lsa_logreg_20261025_183653/per_class_report.csv
results/stylometry_pos_lsa_logreg_20261025_183653/pooled_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_183653/run_metadata.json
results/stylometry_pos_lsa_logreg_20261025_183653/summary.md
results/stylometry_pos_lsa_logreg_20261025_183653/top_confusions.csv
results/stylometry_pos_lsa_logreg_20261025_183839/bootstrap_ci.json
results/stylometry_pos_lsa_logreg_20261025_183839/confusion_matrix.csv
results/stylometry_pos_lsa_logreg_20261025_183839/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_logreg_20261025_183839/fold_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_183839/fold_summary.csv
results/stylometry_pos_lsa_logreg_20261025_183839/oof_predictions.csv
results/stylometry_pos_lsa_logreg_20261025_183839/per_class_report.csv
results/stylometry_pos_lsa_logreg_20261025_183839/pooled_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_183839/run_metadata.json
results/stylometry_pos_lsa_logreg_20261025_183839/summary.md
results/stylometry_pos_lsa_logreg_20261025_183839/top_confusions.csv
results/stylometry_pos_lsa_logreg_20261025_184052/bootstrap_ci.json
results/stylometry_pos_lsa_logreg_20261025_184052/confusion_matrix.csv
results/stylometry_pos_lsa_logreg_20261025_184052/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_logreg_20261025_184052/fold_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_184052/fold_summary.csv
results/stylometry_pos_lsa_logreg_20261025_184052/oof_predictions.csv
results/stylometry_pos_lsa_logreg_20261025_184052/per_class_report.csv
results/stylometry_pos_lsa_logreg_20261025_184052/pooled_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_184052/run_metadata.json
results/stylometry_pos_lsa_logreg_20261025_184052/summary.md
results/stylometry_pos_lsa_logreg_20261025_184052/top_confusions.csv
results/stylometry_pos_lsa_logreg_20261025_184325/bootstrap_ci.json
results/stylometry_pos_lsa_logreg_20261025_184325/confusion_matrix.csv
results/stylometry_pos_lsa_logreg_20261025_184325/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_logreg_20261025_184325/fold_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_184325/fold_summary.csv
results/stylometry_pos_lsa_logreg_20261025_184325/oof_predictions.csv
results/stylometry_pos_lsa_logreg_20261025_184325/per_class_report.csv
results/stylometry_pos_lsa_logreg_20261025_184325/pooled_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_184325/run_metadata.json
results/stylometry_pos_lsa_logreg_20261025_184325/summary.md
results/stylometry_pos_lsa_logreg_20261025_184325/top_confusions.csv
results/stylometry_pos_lsa_random_forest_20261025_190013/bootstrap_ci.json
results/stylometry_pos_lsa_random_forest_20261025_190013/confusion_matrix.csv
results/stylometry_pos_lsa_random_forest_20261025_190013/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_random_forest_20261025_190013/fold_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_190013/fold_summary.csv
results/stylometry_pos_lsa_random_forest_20261025_190013/oof_predictions.csv
results/stylometry_pos_lsa_random_forest_20261025_190013/per_class_report.csv
results/stylometry_pos_lsa_random_forest_20261025_190013/pooled_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_190013/run_metadata.json
results/stylometry_pos_lsa_random_forest_20261025_190013/summary.md
results/stylometry_pos_lsa_random_forest_20261025_190013/top_confusions.csv
results/stylometry_pos_lsa_random_forest_20261025_190125/bootstrap_ci.json
results/stylometry_pos_lsa_random_forest_20261025_190125/confusion_matrix.csv
results/stylometry_pos_lsa_random_forest_20261025_190125/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_random_forest_20261025_190125/fold_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_190125/fold_summary.csv
results/stylometry_pos_lsa_random_forest_20261025_190125/oof_predictions.csv
results/stylometry_pos_lsa_random_forest_20261025_190125/per_class_report.csv
results/stylometry_pos_lsa_random_forest_20261025_190125/pooled_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_190125/run_metadata.json
results/stylometry_pos_lsa_random_forest_20261025_190125/summary.md
results/stylometry_pos_lsa_random_forest_20261025_190125/top_confusions.csv
results/stylometry_pos_lsa_random_forest_20261025_190237/bootstrap_ci.json
results/stylometry_pos_lsa_random_forest_20261025_190237/confusion_matrix.csv
results/stylometry_pos_lsa_random_forest_20261025_190237/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_random_forest_20261025_190237/fold_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_190237/fold_summary.csv
results/stylometry_pos_lsa_random_forest_20261025_190237/oof_predictions.csv
results/stylometry_pos_lsa_random_forest_20261025_190237/per_class_report.csv
results/stylometry_pos_lsa_random_forest_20261025_190237/pooled_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_190237/run_metadata.json
results/stylometry_pos_lsa_random_forest_20261025_190237/summary.md
results/stylometry_pos_lsa_random_forest_20261025_190237/top_confusions.csv
results/stylometry_pos_lsa_random_forest_20261025_190354/bootstrap_ci.json
results/stylometry_pos_lsa_random_forest_20261025_190354/confusion_matrix.csv
results/stylometry_pos_lsa_random_forest_20261025_190354/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_random_forest_20261025_190354/fold_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_190354/fold_summary.csv
results/stylometry_pos_lsa_random_forest_20261025_190354/oof_predictions.csv
results/stylometry_pos_lsa_random_forest_20261025_190354/per_class_report.csv
results/stylometry_pos_lsa_random_forest_20261025_190354/pooled_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_190354/run_metadata.json
results/stylometry_pos_lsa_random_forest_20261025_190354/summary.md
results/stylometry_pos_lsa_random_forest_20261025_190354/top_confusions.csv
results/stylometry_pos_lsa_random_forest_20261025_190510/bootstrap_ci.json
results/stylometry_pos_lsa_random_forest_20261025_190510/confusion_matrix.csv
results/stylometry_pos_lsa_random_forest_20261025_190510/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_random_forest_20261025_190510/fold_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_190510/fold_summary.csv
results/stylometry_pos_lsa_random_forest_20261025_190510/oof_predictions.csv
results/stylometry_pos_lsa_random_forest_20261025_190510/per_class_report.csv
results/stylometry_pos_lsa_random_forest_20261025_190510/pooled_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_190510/run_metadata.json
results/stylometry_pos_lsa_random_forest_20261025_190510/summary.md
results/stylometry_pos_lsa_random_forest_20261025_190510/top_confusions.csv
results/stylometry_pos_lsa_random_forest_20261025_190626/bootstrap_ci.json
results/stylometry_pos_lsa_random_forest_20261025_190626/confusion_matrix.csv
results/stylometry_pos_lsa_random_forest_20261025_190626/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_random_forest_20261025_190626/fold_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_190626/fold_summary.csv
results/stylometry_pos_lsa_random_forest_20261025_190626/oof_predictions.csv
results/stylometry_pos_lsa_random_forest_20261025_190626/per_class_report.csv
results/stylometry_pos_lsa_random_forest_20261025_190626/pooled_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_190626/run_metadata.json
results/stylometry_pos_lsa_random_forest_20261025_190626/summary.md
results/stylometry_pos_lsa_random_forest_20261025_190626/top_confusions.csv
results/stylometry_pos_lsa_random_forest_20261025_190749/bootstrap_ci.json
results/stylometry_pos_lsa_random_forest_20261025_190749/confusion_matrix.csv
results/stylometry_pos_lsa_random_forest_20261025_190749/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_random_forest_20261025_190749/fold_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_190749/fold_summary.csv
results/stylometry_pos_lsa_random_forest_20261025_190749/oof_predictions.csv
results/stylometry_pos_lsa_random_forest_20261025_190749/per_class_report.csv
results/stylometry_pos_lsa_random_forest_20261025_190749/pooled_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_190749/run_metadata.json
results/stylometry_pos_lsa_random_forest_20261025_190749/summary.md
results/stylometry_pos_lsa_random_forest_20261025_190749/top_confusions.csv
results/stylometry_pos_lsa_random_forest_20261025_190910/bootstrap_ci.json
results/stylometry_pos_lsa_random_forest_20261025_190910/confusion_matrix.csv
results/stylometry_pos_lsa_random_forest_20261025_190910/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_random_forest_20261025_190910/fold_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_190910/fold_summary.csv
results/stylometry_pos_lsa_random_forest_20261025_190910/oof_predictions.csv
results/stylometry_pos_lsa_random_forest_20261025_190910/per_class_report.csv
results/stylometry_pos_lsa_random_forest_20261025_190910/pooled_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_190910/run_metadata.json
results/stylometry_pos_lsa_random_forest_20261025_190910/summary.md
results/stylometry_pos_lsa_random_forest_20261025_190910/top_confusions.csv
results/stylometry_pos_lsa_random_forest_20261025_191031/bootstrap_ci.json
results/stylometry_pos_lsa_random_forest_20261025_191031/confusion_matrix.csv
results/stylometry_pos_lsa_random_forest_20261025_191031/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_random_forest_20261025_191031/fold_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_191031/fold_summary.csv
results/stylometry_pos_lsa_random_forest_20261025_191031/oof_predictions.csv
results/stylometry_pos_lsa_random_forest_20261025_191031/per_class_report.csv
results/stylometry_pos_lsa_random_forest_20261025_191031/pooled_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_191031/run_metadata.json
results/stylometry_pos_lsa_random_forest_20261025_191031/summary.md
results/stylometry_pos_lsa_random_forest_20261025_191031/top_confusions.csv
results/verification_stylometry_pos_lsa_20261025_172237/fold_metrics.csv
results/verification_stylometry_pos_lsa_20261025_172237/pair_scores.csv
results/verification_stylometry_pos_lsa_20261025_172237/summary.json

View File

@@ -0,0 +1 @@
5f48946e4ecf6b1888df5bd99796457b9f49dacffd4da835ce8b6ed69895e2c4 /home/folen/homework/NLP/lab1/exports/nlp_lab1_results_20261025_210212.tar.gz

View File

@@ -0,0 +1,389 @@
Дата: Вс 25 окт 2026 21:02:12 MSK
Каталог: /home/folen/homework/NLP/lab1
Интерпретатор: python
=== Проверка окружения ===
Python: 3.11.2 (/home/folen/homework/NLP/lab1/.venv/bin/python)
Каталог проекта: /home/folen/homework/NLP/lab1
Версии библиотек:
numpy: 2.4.6
pandas: 3.1.0
scipy: 1.17.1
sklearn: 1.9.1
nltk: 3.10.4
matplotlib: 3.11.2
Корпус:
файл: /home/folen/homework/NLP/lab1/data/dialogues.csv
SHA-256: 8946f08159daddb9d89c9d323205d4a7fb4bc617d16e300ff6c7606cb05030f7
реплик: 2005
персонажей: 34
словоупотреблений: 38207
NLTK:
токенизатор: NLTK
POS-теггер: доступен
пример токенов: ['Oh', ',', 'that', "'s", 'right', '...', 'I', 'heard']
пример POS: ['INTJ', 'PUNCT', 'DET', 'AUX', 'ADJ', 'PUNCT', 'PRON', 'VERB']
Окружение готово.
{"versions": {"numpy": "2.4.6", "pandas": "3.1.0", "scipy": "1.17.1", "sklearn": "1.9.1", "nltk": "3.10.4", "matplotlib": "3.11.2"}, "n_lines": 2005}
Git commit: c131815fc7b4c941d64d9ca7c7a00ea15df20df3
Содержимое results:
results/ablation_stylometry_pos_lsa_20261025_185002/ablation.csv
results/delta_20261025_201339/delta_sweep.csv
results/length_logreg_20261025_172732/bootstrap_ci.json
results/length_logreg_20261025_172732/confusion_matrix.csv
results/length_logreg_20261025_172732/confusion_matrix_normalized.csv
results/length_logreg_20261025_172732/fold_metrics.csv
results/length_logreg_20261025_172732/fold_summary.csv
results/length_logreg_20261025_172732/oof_predictions.csv
results/length_logreg_20261025_172732/per_class_report.csv
results/length_logreg_20261025_172732/pooled_metrics.csv
results/length_logreg_20261025_172732/run_metadata.json
results/length_logreg_20261025_172732/summary.md
results/length_logreg_20261025_172732/top_confusions.csv
results/logs/tune_20261025_191658.log
results/lsa_sweep_20261025_195217/lsa_sweep.csv
results/README.md
results/stylometry_pos_lsa_extra_trees_20261025_201326/bootstrap_ci.json
results/stylometry_pos_lsa_extra_trees_20261025_201326/confusion_matrix.csv
results/stylometry_pos_lsa_extra_trees_20261025_201326/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_extra_trees_20261025_201326/fold_metrics.csv
results/stylometry_pos_lsa_extra_trees_20261025_201326/fold_summary.csv
results/stylometry_pos_lsa_extra_trees_20261025_201326/oof_predictions.csv
results/stylometry_pos_lsa_extra_trees_20261025_201326/per_class_report.csv
results/stylometry_pos_lsa_extra_trees_20261025_201326/pooled_metrics.csv
results/stylometry_pos_lsa_extra_trees_20261025_201326/run_metadata.json
results/stylometry_pos_lsa_extra_trees_20261025_201326/summary.md
results/stylometry_pos_lsa_extra_trees_20261025_201326/top_confusions.csv
results/stylometry_pos_lsa_linear_svc_20261025_192936/bootstrap_ci.json
results/stylometry_pos_lsa_linear_svc_20261025_192936/confusion_matrix.csv
results/stylometry_pos_lsa_linear_svc_20261025_192936/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_linear_svc_20261025_192936/fold_metrics.csv
results/stylometry_pos_lsa_linear_svc_20261025_192936/fold_summary.csv
results/stylometry_pos_lsa_linear_svc_20261025_192936/oof_predictions.csv
results/stylometry_pos_lsa_linear_svc_20261025_192936/per_class_report.csv
results/stylometry_pos_lsa_linear_svc_20261025_192936/pooled_metrics.csv
results/stylometry_pos_lsa_linear_svc_20261025_192936/run_metadata.json
results/stylometry_pos_lsa_linear_svc_20261025_192936/summary.md
results/stylometry_pos_lsa_linear_svc_20261025_192936/top_confusions.csv
results/stylometry_pos_lsa_linear_svc_20261025_193051/bootstrap_ci.json
results/stylometry_pos_lsa_linear_svc_20261025_193051/confusion_matrix.csv
results/stylometry_pos_lsa_linear_svc_20261025_193051/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_linear_svc_20261025_193051/fold_metrics.csv
results/stylometry_pos_lsa_linear_svc_20261025_193051/fold_summary.csv
results/stylometry_pos_lsa_linear_svc_20261025_193051/oof_predictions.csv
results/stylometry_pos_lsa_linear_svc_20261025_193051/per_class_report.csv
results/stylometry_pos_lsa_linear_svc_20261025_193051/pooled_metrics.csv
results/stylometry_pos_lsa_linear_svc_20261025_193051/run_metadata.json
results/stylometry_pos_lsa_linear_svc_20261025_193051/summary.md
results/stylometry_pos_lsa_linear_svc_20261025_193051/top_confusions.csv
results/stylometry_pos_lsa_linear_svc_20261025_193207/bootstrap_ci.json
results/stylometry_pos_lsa_linear_svc_20261025_193207/confusion_matrix.csv
results/stylometry_pos_lsa_linear_svc_20261025_193207/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_linear_svc_20261025_193207/fold_metrics.csv
results/stylometry_pos_lsa_linear_svc_20261025_193207/fold_summary.csv
results/stylometry_pos_lsa_linear_svc_20261025_193207/oof_predictions.csv
results/stylometry_pos_lsa_linear_svc_20261025_193207/per_class_report.csv
results/stylometry_pos_lsa_linear_svc_20261025_193207/pooled_metrics.csv
results/stylometry_pos_lsa_linear_svc_20261025_193207/run_metadata.json
results/stylometry_pos_lsa_linear_svc_20261025_193207/summary.md
results/stylometry_pos_lsa_linear_svc_20261025_193207/top_confusions.csv
results/stylometry_pos_lsa_linear_svc_20261025_193328/bootstrap_ci.json
results/stylometry_pos_lsa_linear_svc_20261025_193328/confusion_matrix.csv
results/stylometry_pos_lsa_linear_svc_20261025_193328/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_linear_svc_20261025_193328/fold_metrics.csv
results/stylometry_pos_lsa_linear_svc_20261025_193328/fold_summary.csv
results/stylometry_pos_lsa_linear_svc_20261025_193328/oof_predictions.csv
results/stylometry_pos_lsa_linear_svc_20261025_193328/per_class_report.csv
results/stylometry_pos_lsa_linear_svc_20261025_193328/pooled_metrics.csv
results/stylometry_pos_lsa_linear_svc_20261025_193328/run_metadata.json
results/stylometry_pos_lsa_linear_svc_20261025_193328/summary.md
results/stylometry_pos_lsa_linear_svc_20261025_193328/top_confusions.csv
results/stylometry_pos_lsa_linear_svc_20261025_193455/bootstrap_ci.json
results/stylometry_pos_lsa_linear_svc_20261025_193455/confusion_matrix.csv
results/stylometry_pos_lsa_linear_svc_20261025_193455/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_linear_svc_20261025_193455/fold_metrics.csv
results/stylometry_pos_lsa_linear_svc_20261025_193455/fold_summary.csv
results/stylometry_pos_lsa_linear_svc_20261025_193455/oof_predictions.csv
results/stylometry_pos_lsa_linear_svc_20261025_193455/per_class_report.csv
results/stylometry_pos_lsa_linear_svc_20261025_193455/pooled_metrics.csv
results/stylometry_pos_lsa_linear_svc_20261025_193455/run_metadata.json
results/stylometry_pos_lsa_linear_svc_20261025_193455/summary.md
results/stylometry_pos_lsa_linear_svc_20261025_193455/top_confusions.csv
results/stylometry_pos_lsa_linear_svc_20261025_193625/bootstrap_ci.json
results/stylometry_pos_lsa_linear_svc_20261025_193625/confusion_matrix.csv
results/stylometry_pos_lsa_linear_svc_20261025_193625/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_linear_svc_20261025_193625/fold_metrics.csv
results/stylometry_pos_lsa_linear_svc_20261025_193625/fold_summary.csv
results/stylometry_pos_lsa_linear_svc_20261025_193625/oof_predictions.csv
results/stylometry_pos_lsa_linear_svc_20261025_193625/per_class_report.csv
results/stylometry_pos_lsa_linear_svc_20261025_193625/pooled_metrics.csv
results/stylometry_pos_lsa_linear_svc_20261025_193625/run_metadata.json
results/stylometry_pos_lsa_linear_svc_20261025_193625/summary.md
results/stylometry_pos_lsa_linear_svc_20261025_193625/top_confusions.csv
results/stylometry_pos_lsa_logreg_20261025_191805/bootstrap_ci.json
results/stylometry_pos_lsa_logreg_20261025_191805/confusion_matrix.csv
results/stylometry_pos_lsa_logreg_20261025_191805/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_logreg_20261025_191805/fold_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_191805/fold_summary.csv
results/stylometry_pos_lsa_logreg_20261025_191805/oof_predictions.csv
results/stylometry_pos_lsa_logreg_20261025_191805/per_class_report.csv
results/stylometry_pos_lsa_logreg_20261025_191805/pooled_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_191805/run_metadata.json
results/stylometry_pos_lsa_logreg_20261025_191805/summary.md
results/stylometry_pos_lsa_logreg_20261025_191805/top_confusions.csv
results/stylometry_pos_lsa_logreg_20261025_191938/bootstrap_ci.json
results/stylometry_pos_lsa_logreg_20261025_191938/confusion_matrix.csv
results/stylometry_pos_lsa_logreg_20261025_191938/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_logreg_20261025_191938/fold_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_191938/fold_summary.csv
results/stylometry_pos_lsa_logreg_20261025_191938/oof_predictions.csv
results/stylometry_pos_lsa_logreg_20261025_191938/per_class_report.csv
results/stylometry_pos_lsa_logreg_20261025_191938/pooled_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_191938/run_metadata.json
results/stylometry_pos_lsa_logreg_20261025_191938/summary.md
results/stylometry_pos_lsa_logreg_20261025_191938/top_confusions.csv
results/stylometry_pos_lsa_logreg_20261025_192116/bootstrap_ci.json
results/stylometry_pos_lsa_logreg_20261025_192116/confusion_matrix.csv
results/stylometry_pos_lsa_logreg_20261025_192116/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_logreg_20261025_192116/fold_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_192116/fold_summary.csv
results/stylometry_pos_lsa_logreg_20261025_192116/oof_predictions.csv
results/stylometry_pos_lsa_logreg_20261025_192116/per_class_report.csv
results/stylometry_pos_lsa_logreg_20261025_192116/pooled_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_192116/run_metadata.json
results/stylometry_pos_lsa_logreg_20261025_192116/summary.md
results/stylometry_pos_lsa_logreg_20261025_192116/top_confusions.csv
results/stylometry_pos_lsa_logreg_20261025_192311/bootstrap_ci.json
results/stylometry_pos_lsa_logreg_20261025_192311/confusion_matrix.csv
results/stylometry_pos_lsa_logreg_20261025_192311/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_logreg_20261025_192311/fold_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_192311/fold_summary.csv
results/stylometry_pos_lsa_logreg_20261025_192311/oof_predictions.csv
results/stylometry_pos_lsa_logreg_20261025_192311/per_class_report.csv
results/stylometry_pos_lsa_logreg_20261025_192311/pooled_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_192311/run_metadata.json
results/stylometry_pos_lsa_logreg_20261025_192311/summary.md
results/stylometry_pos_lsa_logreg_20261025_192311/top_confusions.csv
results/stylometry_pos_lsa_logreg_20261025_192535/bootstrap_ci.json
results/stylometry_pos_lsa_logreg_20261025_192535/confusion_matrix.csv
results/stylometry_pos_lsa_logreg_20261025_192535/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_logreg_20261025_192535/fold_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_192535/fold_summary.csv
results/stylometry_pos_lsa_logreg_20261025_192535/oof_predictions.csv
results/stylometry_pos_lsa_logreg_20261025_192535/per_class_report.csv
results/stylometry_pos_lsa_logreg_20261025_192535/pooled_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_192535/run_metadata.json
results/stylometry_pos_lsa_logreg_20261025_192535/summary.md
results/stylometry_pos_lsa_logreg_20261025_192535/top_confusions.csv
results/stylometry_pos_lsa_logreg_20261025_192822/bootstrap_ci.json
results/stylometry_pos_lsa_logreg_20261025_192822/confusion_matrix.csv
results/stylometry_pos_lsa_logreg_20261025_192822/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_logreg_20261025_192822/fold_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_192822/fold_summary.csv
results/stylometry_pos_lsa_logreg_20261025_192822/oof_predictions.csv
results/stylometry_pos_lsa_logreg_20261025_192822/per_class_report.csv
results/stylometry_pos_lsa_logreg_20261025_192822/pooled_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_192822/run_metadata.json
results/stylometry_pos_lsa_logreg_20261025_192822/summary.md
results/stylometry_pos_lsa_logreg_20261025_192822/top_confusions.csv
results/stylometry_pos_lsa_logreg_20261025_193844/bootstrap_ci.json
results/stylometry_pos_lsa_logreg_20261025_193844/confusion_matrix.csv
results/stylometry_pos_lsa_logreg_20261025_193844/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_logreg_20261025_193844/fold_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_193844/fold_summary.csv
results/stylometry_pos_lsa_logreg_20261025_193844/oof_predictions.csv
results/stylometry_pos_lsa_logreg_20261025_193844/per_class_report.csv
results/stylometry_pos_lsa_logreg_20261025_193844/pooled_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_193844/run_metadata.json
results/stylometry_pos_lsa_logreg_20261025_193844/summary.md
results/stylometry_pos_lsa_logreg_20261025_193844/top_confusions.csv
results/stylometry_pos_lsa_logreg_20261025_194037/bootstrap_ci.json
results/stylometry_pos_lsa_logreg_20261025_194037/confusion_matrix.csv
results/stylometry_pos_lsa_logreg_20261025_194037/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_logreg_20261025_194037/fold_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_194037/fold_summary.csv
results/stylometry_pos_lsa_logreg_20261025_194037/oof_predictions.csv
results/stylometry_pos_lsa_logreg_20261025_194037/per_class_report.csv
results/stylometry_pos_lsa_logreg_20261025_194037/pooled_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_194037/run_metadata.json
results/stylometry_pos_lsa_logreg_20261025_194037/summary.md
results/stylometry_pos_lsa_logreg_20261025_194037/top_confusions.csv
results/stylometry_pos_lsa_logreg_20261025_194222/bootstrap_ci.json
results/stylometry_pos_lsa_logreg_20261025_194222/confusion_matrix.csv
results/stylometry_pos_lsa_logreg_20261025_194222/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_logreg_20261025_194222/fold_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_194222/fold_summary.csv
results/stylometry_pos_lsa_logreg_20261025_194222/oof_predictions.csv
results/stylometry_pos_lsa_logreg_20261025_194222/per_class_report.csv
results/stylometry_pos_lsa_logreg_20261025_194222/pooled_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_194222/run_metadata.json
results/stylometry_pos_lsa_logreg_20261025_194222/summary.md
results/stylometry_pos_lsa_logreg_20261025_194222/top_confusions.csv
results/stylometry_pos_lsa_logreg_20261025_194415/bootstrap_ci.json
results/stylometry_pos_lsa_logreg_20261025_194415/confusion_matrix.csv
results/stylometry_pos_lsa_logreg_20261025_194415/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_logreg_20261025_194415/fold_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_194415/fold_summary.csv
results/stylometry_pos_lsa_logreg_20261025_194415/oof_predictions.csv
results/stylometry_pos_lsa_logreg_20261025_194415/per_class_report.csv
results/stylometry_pos_lsa_logreg_20261025_194415/pooled_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_194415/run_metadata.json
results/stylometry_pos_lsa_logreg_20261025_194415/summary.md
results/stylometry_pos_lsa_logreg_20261025_194415/top_confusions.csv
results/stylometry_pos_lsa_logreg_20261025_194613/bootstrap_ci.json
results/stylometry_pos_lsa_logreg_20261025_194613/confusion_matrix.csv
results/stylometry_pos_lsa_logreg_20261025_194613/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_logreg_20261025_194613/fold_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_194613/fold_summary.csv
results/stylometry_pos_lsa_logreg_20261025_194613/oof_predictions.csv
results/stylometry_pos_lsa_logreg_20261025_194613/per_class_report.csv
results/stylometry_pos_lsa_logreg_20261025_194613/pooled_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_194613/run_metadata.json
results/stylometry_pos_lsa_logreg_20261025_194613/summary.md
results/stylometry_pos_lsa_logreg_20261025_194613/top_confusions.csv
results/stylometry_pos_lsa_logreg_20261025_194802/bootstrap_ci.json
results/stylometry_pos_lsa_logreg_20261025_194802/confusion_matrix.csv
results/stylometry_pos_lsa_logreg_20261025_194802/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_logreg_20261025_194802/fold_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_194802/fold_summary.csv
results/stylometry_pos_lsa_logreg_20261025_194802/oof_predictions.csv
results/stylometry_pos_lsa_logreg_20261025_194802/per_class_report.csv
results/stylometry_pos_lsa_logreg_20261025_194802/pooled_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_194802/run_metadata.json
results/stylometry_pos_lsa_logreg_20261025_194802/summary.md
results/stylometry_pos_lsa_logreg_20261025_194802/top_confusions.csv
results/stylometry_pos_lsa_logreg_20261025_194959/bootstrap_ci.json
results/stylometry_pos_lsa_logreg_20261025_194959/confusion_matrix.csv
results/stylometry_pos_lsa_logreg_20261025_194959/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_logreg_20261025_194959/fold_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_194959/fold_summary.csv
results/stylometry_pos_lsa_logreg_20261025_194959/oof_predictions.csv
results/stylometry_pos_lsa_logreg_20261025_194959/per_class_report.csv
results/stylometry_pos_lsa_logreg_20261025_194959/pooled_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_194959/run_metadata.json
results/stylometry_pos_lsa_logreg_20261025_194959/summary.md
results/stylometry_pos_lsa_logreg_20261025_194959/top_confusions.csv
results/stylometry_pos_lsa_logreg_20261025_195203/bootstrap_ci.json
results/stylometry_pos_lsa_logreg_20261025_195203/confusion_matrix.csv
results/stylometry_pos_lsa_logreg_20261025_195203/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_logreg_20261025_195203/fold_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_195203/fold_summary.csv
results/stylometry_pos_lsa_logreg_20261025_195203/oof_predictions.csv
results/stylometry_pos_lsa_logreg_20261025_195203/per_class_report.csv
results/stylometry_pos_lsa_logreg_20261025_195203/pooled_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_195203/run_metadata.json
results/stylometry_pos_lsa_logreg_20261025_195203/summary.md
results/stylometry_pos_lsa_logreg_20261025_195203/top_confusions.csv
results/stylometry_pos_lsa_random_forest_20261025_195744/bootstrap_ci.json
results/stylometry_pos_lsa_random_forest_20261025_195744/confusion_matrix.csv
results/stylometry_pos_lsa_random_forest_20261025_195744/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_random_forest_20261025_195744/fold_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_195744/fold_summary.csv
results/stylometry_pos_lsa_random_forest_20261025_195744/oof_predictions.csv
results/stylometry_pos_lsa_random_forest_20261025_195744/per_class_report.csv
results/stylometry_pos_lsa_random_forest_20261025_195744/pooled_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_195744/run_metadata.json
results/stylometry_pos_lsa_random_forest_20261025_195744/summary.md
results/stylometry_pos_lsa_random_forest_20261025_195744/top_confusions.csv
results/stylometry_pos_lsa_random_forest_20261025_195906/bootstrap_ci.json
results/stylometry_pos_lsa_random_forest_20261025_195906/confusion_matrix.csv
results/stylometry_pos_lsa_random_forest_20261025_195906/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_random_forest_20261025_195906/fold_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_195906/fold_summary.csv
results/stylometry_pos_lsa_random_forest_20261025_195906/oof_predictions.csv
results/stylometry_pos_lsa_random_forest_20261025_195906/per_class_report.csv
results/stylometry_pos_lsa_random_forest_20261025_195906/pooled_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_195906/run_metadata.json
results/stylometry_pos_lsa_random_forest_20261025_195906/summary.md
results/stylometry_pos_lsa_random_forest_20261025_195906/top_confusions.csv
results/stylometry_pos_lsa_random_forest_20261025_200028/bootstrap_ci.json
results/stylometry_pos_lsa_random_forest_20261025_200028/confusion_matrix.csv
results/stylometry_pos_lsa_random_forest_20261025_200028/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_random_forest_20261025_200028/fold_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_200028/fold_summary.csv
results/stylometry_pos_lsa_random_forest_20261025_200028/oof_predictions.csv
results/stylometry_pos_lsa_random_forest_20261025_200028/per_class_report.csv
results/stylometry_pos_lsa_random_forest_20261025_200028/pooled_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_200028/run_metadata.json
results/stylometry_pos_lsa_random_forest_20261025_200028/summary.md
results/stylometry_pos_lsa_random_forest_20261025_200028/top_confusions.csv
results/stylometry_pos_lsa_random_forest_20261025_200155/bootstrap_ci.json
results/stylometry_pos_lsa_random_forest_20261025_200155/confusion_matrix.csv
results/stylometry_pos_lsa_random_forest_20261025_200155/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_random_forest_20261025_200155/fold_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_200155/fold_summary.csv
results/stylometry_pos_lsa_random_forest_20261025_200155/oof_predictions.csv
results/stylometry_pos_lsa_random_forest_20261025_200155/per_class_report.csv
results/stylometry_pos_lsa_random_forest_20261025_200155/pooled_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_200155/run_metadata.json
results/stylometry_pos_lsa_random_forest_20261025_200155/summary.md
results/stylometry_pos_lsa_random_forest_20261025_200155/top_confusions.csv
results/stylometry_pos_lsa_random_forest_20261025_200320/bootstrap_ci.json
results/stylometry_pos_lsa_random_forest_20261025_200320/confusion_matrix.csv
results/stylometry_pos_lsa_random_forest_20261025_200320/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_random_forest_20261025_200320/fold_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_200320/fold_summary.csv
results/stylometry_pos_lsa_random_forest_20261025_200320/oof_predictions.csv
results/stylometry_pos_lsa_random_forest_20261025_200320/per_class_report.csv
results/stylometry_pos_lsa_random_forest_20261025_200320/pooled_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_200320/run_metadata.json
results/stylometry_pos_lsa_random_forest_20261025_200320/summary.md
results/stylometry_pos_lsa_random_forest_20261025_200320/top_confusions.csv
results/stylometry_pos_lsa_random_forest_20261025_200445/bootstrap_ci.json
results/stylometry_pos_lsa_random_forest_20261025_200445/confusion_matrix.csv
results/stylometry_pos_lsa_random_forest_20261025_200445/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_random_forest_20261025_200445/fold_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_200445/fold_summary.csv
results/stylometry_pos_lsa_random_forest_20261025_200445/oof_predictions.csv
results/stylometry_pos_lsa_random_forest_20261025_200445/per_class_report.csv
results/stylometry_pos_lsa_random_forest_20261025_200445/pooled_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_200445/run_metadata.json
results/stylometry_pos_lsa_random_forest_20261025_200445/summary.md
results/stylometry_pos_lsa_random_forest_20261025_200445/top_confusions.csv
results/stylometry_pos_lsa_random_forest_20261025_200617/bootstrap_ci.json
results/stylometry_pos_lsa_random_forest_20261025_200617/confusion_matrix.csv
results/stylometry_pos_lsa_random_forest_20261025_200617/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_random_forest_20261025_200617/fold_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_200617/fold_summary.csv
results/stylometry_pos_lsa_random_forest_20261025_200617/oof_predictions.csv
results/stylometry_pos_lsa_random_forest_20261025_200617/per_class_report.csv
results/stylometry_pos_lsa_random_forest_20261025_200617/pooled_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_200617/run_metadata.json
results/stylometry_pos_lsa_random_forest_20261025_200617/summary.md
results/stylometry_pos_lsa_random_forest_20261025_200617/top_confusions.csv
results/stylometry_pos_lsa_random_forest_20261025_200748/bootstrap_ci.json
results/stylometry_pos_lsa_random_forest_20261025_200748/confusion_matrix.csv
results/stylometry_pos_lsa_random_forest_20261025_200748/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_random_forest_20261025_200748/fold_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_200748/fold_summary.csv
results/stylometry_pos_lsa_random_forest_20261025_200748/oof_predictions.csv
results/stylometry_pos_lsa_random_forest_20261025_200748/per_class_report.csv
results/stylometry_pos_lsa_random_forest_20261025_200748/pooled_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_200748/run_metadata.json
results/stylometry_pos_lsa_random_forest_20261025_200748/summary.md
results/stylometry_pos_lsa_random_forest_20261025_200748/top_confusions.csv
results/stylometry_pos_lsa_random_forest_20261025_200916/bootstrap_ci.json
results/stylometry_pos_lsa_random_forest_20261025_200916/confusion_matrix.csv
results/stylometry_pos_lsa_random_forest_20261025_200916/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_random_forest_20261025_200916/fold_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_200916/fold_summary.csv
results/stylometry_pos_lsa_random_forest_20261025_200916/oof_predictions.csv
results/stylometry_pos_lsa_random_forest_20261025_200916/per_class_report.csv
results/stylometry_pos_lsa_random_forest_20261025_200916/pooled_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_200916/run_metadata.json
results/stylometry_pos_lsa_random_forest_20261025_200916/summary.md
results/stylometry_pos_lsa_random_forest_20261025_200916/top_confusions.csv
results/stylometry_pos_lsa_random_forest_20261025_201210/bootstrap_ci.json
results/stylometry_pos_lsa_random_forest_20261025_201210/confusion_matrix.csv
results/stylometry_pos_lsa_random_forest_20261025_201210/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_random_forest_20261025_201210/fold_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_201210/fold_summary.csv
results/stylometry_pos_lsa_random_forest_20261025_201210/oof_predictions.csv
results/stylometry_pos_lsa_random_forest_20261025_201210/per_class_report.csv
results/stylometry_pos_lsa_random_forest_20261025_201210/pooled_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_201210/run_metadata.json
results/stylometry_pos_lsa_random_forest_20261025_201210/summary.md
results/stylometry_pos_lsa_random_forest_20261025_201210/top_confusions.csv

View File

@@ -1 +0,0 @@
f9566b75e0474a72271263bf203f892979e638750cddc980be9a69f690aaa884 /home/folen/Homework/NLP/lab1/exports/nlp_lab1_results_20261026_054117.tar.gz

View File

@@ -1,493 +0,0 @@
Дата: Пн 26 окт 2026 05:41:17 MSK
Каталог: /home/folen/Homework/NLP/lab1
Интерпретатор: python
=== Проверка окружения ===
Python: 3.11.2 (/home/folen/Homework/NLP/.venv/bin/python)
Каталог проекта: /home/folen/Homework/NLP/lab1
Версии библиотек:
numpy: 2.4.6
pandas: 3.1.0
scipy: 1.17.1
sklearn: 1.9.1
nltk: 3.10.4
matplotlib: 3.11.2
Корпус:
файл: /home/folen/Homework/NLP/lab1/data/dialogues.csv
SHA-256: 8946f08159daddb9d89c9d323205d4a7fb4bc617d16e300ff6c7606cb05030f7
реплик: 2005
персонажей: 34
словоупотреблений: 38207
NLTK:
токенизатор: NLTK
POS-теггер: доступен
пример токенов: ['Oh', ',', 'that', "'s", 'right', '...', 'I', 'heard']
пример POS: ['INTJ', 'PUNCT', 'DET', 'AUX', 'ADJ', 'PUNCT', 'PRON', 'VERB']
Окружение готово.
{"versions": {"numpy": "2.4.6", "pandas": "3.1.0", "scipy": "1.17.1", "sklearn": "1.9.1", "nltk": "3.10.4", "matplotlib": "3.11.2"}, "n_lines": 2005}
Git commit: 2c6bf4653adc6c356e4af205857dfdb0f144775b
Содержимое results:
results/ablation_stylometry_pos_lsa_20261025_170116/ablation.csv
results/benchmark_stylometry_pos_lsa_20261025_165348/summary.csv
results/char_multinomial_nb_20261025_155832/bootstrap_ci.json
results/char_multinomial_nb_20261025_155832/confusion_matrix.csv
results/char_multinomial_nb_20261025_155832/confusion_matrix_normalized.csv
results/char_multinomial_nb_20261025_155832/fold_metrics.csv
results/char_multinomial_nb_20261025_155832/fold_summary.csv
results/char_multinomial_nb_20261025_155832/oof_predictions.csv
results/char_multinomial_nb_20261025_155832/per_class_report.csv
results/char_multinomial_nb_20261025_155832/pooled_metrics.csv
results/char_multinomial_nb_20261025_155832/run_metadata.json
results/char_multinomial_nb_20261025_155832/summary.md
results/char_multinomial_nb_20261025_155832/top_confusions.csv
results/clustering_stylometry_pos_lsa_20261025_172241/character_centroids.csv
results/clustering_stylometry_pos_lsa_20261025_172241/clustering_metrics.csv
results/delta_20261025_172113/delta_sweep.csv
results/followup/comparisons.csv
results/followup/comparisons.md
results/followup/manifest.tsv
results/followup/summary.csv
results/followup/summary.md
results/fw_logreg_20261025_160058/bootstrap_ci.json
results/fw_logreg_20261025_160058/confusion_matrix.csv
results/fw_logreg_20261025_160058/confusion_matrix_normalized.csv
results/fw_logreg_20261025_160058/fold_metrics.csv
results/fw_logreg_20261025_160058/fold_summary.csv
results/fw_logreg_20261025_160058/oof_predictions.csv
results/fw_logreg_20261025_160058/per_class_report.csv
results/fw_logreg_20261025_160058/pooled_metrics.csv
results/fw_logreg_20261025_160058/run_metadata.json
results/fw_logreg_20261025_160058/summary.md
results/fw_logreg_20261025_160058/top_confusions.csv
results/length_dummy_20261025_155747/bootstrap_ci.json
results/length_dummy_20261025_155747/confusion_matrix.csv
results/length_dummy_20261025_155747/confusion_matrix_normalized.csv
results/length_dummy_20261025_155747/fold_metrics.csv
results/length_dummy_20261025_155747/fold_summary.csv
results/length_dummy_20261025_155747/oof_predictions.csv
results/length_dummy_20261025_155747/per_class_report.csv
results/length_dummy_20261025_155747/pooled_metrics.csv
results/length_dummy_20261025_155747/run_metadata.json
results/length_dummy_20261025_155747/summary.md
results/length_dummy_20261025_155747/top_confusions.csv
results/length_logreg_20261025_155719/bootstrap_ci.json
results/length_logreg_20261025_155719/confusion_matrix.csv
results/length_logreg_20261025_155719/confusion_matrix_normalized.csv
results/length_logreg_20261025_155719/fold_metrics.csv
results/length_logreg_20261025_155719/fold_summary.csv
results/length_logreg_20261025_155719/oof_predictions.csv
results/length_logreg_20261025_155719/per_class_report.csv
results/length_logreg_20261025_155719/pooled_metrics.csv
results/length_logreg_20261025_155719/run_metadata.json
results/length_logreg_20261025_155719/summary.md
results/length_logreg_20261025_155719/top_confusions.csv
results/length_logreg_20261025_155813/bootstrap_ci.json
results/length_logreg_20261025_155813/confusion_matrix.csv
results/length_logreg_20261025_155813/confusion_matrix_normalized.csv
results/length_logreg_20261025_155813/fold_metrics.csv
results/length_logreg_20261025_155813/fold_summary.csv
results/length_logreg_20261025_155813/oof_predictions.csv
results/length_logreg_20261025_155813/per_class_report.csv
results/length_logreg_20261025_155813/pooled_metrics.csv
results/length_logreg_20261025_155813/run_metadata.json
results/length_logreg_20261025_155813/summary.md
results/length_logreg_20261025_155813/top_confusions.csv
results/logs/run_all_20261025_150915.log
results/logs/run_all_20261025_151312.log
results/logs/run_all_20261025_152356.log
results/logs/run_all_20261025_155738.log
results/logs/run_followup_20261025_210825.log
results/logs/run_followup_smoke.log
results/lsa_char_logreg_20261025_164836/bootstrap_ci.json
results/lsa_char_logreg_20261025_164836/confusion_matrix.csv
results/lsa_char_logreg_20261025_164836/confusion_matrix_normalized.csv
results/lsa_char_logreg_20261025_164836/fold_metrics.csv
results/lsa_char_logreg_20261025_164836/fold_summary.csv
results/lsa_char_logreg_20261025_164836/oof_predictions.csv
results/lsa_char_logreg_20261025_164836/per_class_report.csv
results/lsa_char_logreg_20261025_164836/pooled_metrics.csv
results/lsa_char_logreg_20261025_164836/run_metadata.json
results/lsa_char_logreg_20261025_164836/summary.md
results/lsa_char_logreg_20261025_164836/top_confusions.csv
results/lsa_content_logreg_20261025_164717/bootstrap_ci.json
results/lsa_content_logreg_20261025_164717/confusion_matrix.csv
results/lsa_content_logreg_20261025_164717/confusion_matrix_normalized.csv
results/lsa_content_logreg_20261025_164717/fold_metrics.csv
results/lsa_content_logreg_20261025_164717/fold_summary.csv
results/lsa_content_logreg_20261025_164717/oof_predictions.csv
results/lsa_content_logreg_20261025_164717/per_class_report.csv
results/lsa_content_logreg_20261025_164717/pooled_metrics.csv
results/lsa_content_logreg_20261025_164717/run_metadata.json
results/lsa_content_logreg_20261025_164717/summary.md
results/lsa_content_logreg_20261025_164717/top_confusions.csv
results/lsa_fw_logreg_20261025_164750/bootstrap_ci.json
results/lsa_fw_logreg_20261025_164750/confusion_matrix.csv
results/lsa_fw_logreg_20261025_164750/confusion_matrix_normalized.csv
results/lsa_fw_logreg_20261025_164750/fold_metrics.csv
results/lsa_fw_logreg_20261025_164750/fold_summary.csv
results/lsa_fw_logreg_20261025_164750/oof_predictions.csv
results/lsa_fw_logreg_20261025_164750/per_class_report.csv
results/lsa_fw_logreg_20261025_164750/pooled_metrics.csv
results/lsa_fw_logreg_20261025_164750/run_metadata.json
results/lsa_fw_logreg_20261025_164750/summary.md
results/lsa_fw_logreg_20261025_164750/top_confusions.csv
results/lsa_pos_logreg_20261025_164912/bootstrap_ci.json
results/lsa_pos_logreg_20261025_164912/confusion_matrix.csv
results/lsa_pos_logreg_20261025_164912/confusion_matrix_normalized.csv
results/lsa_pos_logreg_20261025_164912/fold_metrics.csv
results/lsa_pos_logreg_20261025_164912/fold_summary.csv
results/lsa_pos_logreg_20261025_164912/oof_predictions.csv
results/lsa_pos_logreg_20261025_164912/per_class_report.csv
results/lsa_pos_logreg_20261025_164912/pooled_metrics.csv
results/lsa_pos_logreg_20261025_164912/run_metadata.json
results/lsa_pos_logreg_20261025_164912/summary.md
results/lsa_pos_logreg_20261025_164912/top_confusions.csv
results/lsa_sweep_20261025_164924/lsa_sweep.csv
results/lsa_sweep_20261025_185049/lsa_sweep.csv
results/lsa_word2_logreg_20261025_164641/bootstrap_ci.json
results/lsa_word2_logreg_20261025_164641/confusion_matrix.csv
results/lsa_word2_logreg_20261025_164641/confusion_matrix_normalized.csv
results/lsa_word2_logreg_20261025_164641/fold_metrics.csv
results/lsa_word2_logreg_20261025_164641/fold_summary.csv
results/lsa_word2_logreg_20261025_164641/oof_predictions.csv
results/lsa_word2_logreg_20261025_164641/per_class_report.csv
results/lsa_word2_logreg_20261025_164641/pooled_metrics.csv
results/lsa_word2_logreg_20261025_164641/run_metadata.json
results/lsa_word2_logreg_20261025_164641/summary.md
results/lsa_word2_logreg_20261025_164641/top_confusions.csv
results/lsa_word_logreg_20261025_164608/bootstrap_ci.json
results/lsa_word_logreg_20261025_164608/confusion_matrix.csv
results/lsa_word_logreg_20261025_164608/confusion_matrix_normalized.csv
results/lsa_word_logreg_20261025_164608/fold_metrics.csv
results/lsa_word_logreg_20261025_164608/fold_summary.csv
results/lsa_word_logreg_20261025_164608/oof_predictions.csv
results/lsa_word_logreg_20261025_164608/per_class_report.csv
results/lsa_word_logreg_20261025_164608/pooled_metrics.csv
results/lsa_word_logreg_20261025_164608/run_metadata.json
results/lsa_word_logreg_20261025_164608/summary.md
results/lsa_word_logreg_20261025_164608/top_confusions.csv
results/nested_stylometry_pos_lsa_linear_svc_20261025_172442/best_params.json
results/nested_stylometry_pos_lsa_linear_svc_20261025_172442/bootstrap_ci.json
results/nested_stylometry_pos_lsa_linear_svc_20261025_172442/confusion_matrix.csv
results/nested_stylometry_pos_lsa_linear_svc_20261025_172442/confusion_matrix_normalized.csv
results/nested_stylometry_pos_lsa_linear_svc_20261025_172442/fold_metrics.csv
results/nested_stylometry_pos_lsa_linear_svc_20261025_172442/fold_summary.csv
results/nested_stylometry_pos_lsa_linear_svc_20261025_172442/oof_predictions.csv
results/nested_stylometry_pos_lsa_linear_svc_20261025_172442/per_class_report.csv
results/nested_stylometry_pos_lsa_linear_svc_20261025_172442/pooled_metrics.csv
results/nested_stylometry_pos_lsa_linear_svc_20261025_172442/run_metadata.json
results/nested_stylometry_pos_lsa_linear_svc_20261025_172442/summary.md
results/nested_stylometry_pos_lsa_linear_svc_20261025_172442/top_confusions.csv
results/nested_stylometry_pos_lsa_logreg_20261025_172348/best_params.json
results/nested_stylometry_pos_lsa_logreg_20261025_172348/bootstrap_ci.json
results/nested_stylometry_pos_lsa_logreg_20261025_172348/confusion_matrix.csv
results/nested_stylometry_pos_lsa_logreg_20261025_172348/confusion_matrix_normalized.csv
results/nested_stylometry_pos_lsa_logreg_20261025_172348/fold_metrics.csv
results/nested_stylometry_pos_lsa_logreg_20261025_172348/fold_summary.csv
results/nested_stylometry_pos_lsa_logreg_20261025_172348/oof_predictions.csv
results/nested_stylometry_pos_lsa_logreg_20261025_172348/per_class_report.csv
results/nested_stylometry_pos_lsa_logreg_20261025_172348/pooled_metrics.csv
results/nested_stylometry_pos_lsa_logreg_20261025_172348/run_metadata.json
results/nested_stylometry_pos_lsa_logreg_20261025_172348/summary.md
results/nested_stylometry_pos_lsa_logreg_20261025_172348/top_confusions.csv
results/pos_multinomial_nb_20261025_155848/bootstrap_ci.json
results/pos_multinomial_nb_20261025_155848/confusion_matrix.csv
results/pos_multinomial_nb_20261025_155848/confusion_matrix_normalized.csv
results/pos_multinomial_nb_20261025_155848/fold_metrics.csv
results/pos_multinomial_nb_20261025_155848/fold_summary.csv
results/pos_multinomial_nb_20261025_155848/oof_predictions.csv
results/pos_multinomial_nb_20261025_155848/per_class_report.csv
results/pos_multinomial_nb_20261025_155848/pooled_metrics.csv
results/pos_multinomial_nb_20261025_155848/run_metadata.json
results/pos_multinomial_nb_20261025_155848/summary.md
results/pos_multinomial_nb_20261025_155848/top_confusions.csv
results/README.md
results/stylometry_linear_svc_20261025_164018/bootstrap_ci.json
results/stylometry_linear_svc_20261025_164018/confusion_matrix.csv
results/stylometry_linear_svc_20261025_164018/confusion_matrix_normalized.csv
results/stylometry_linear_svc_20261025_164018/fold_metrics.csv
results/stylometry_linear_svc_20261025_164018/fold_summary.csv
results/stylometry_linear_svc_20261025_164018/oof_predictions.csv
results/stylometry_linear_svc_20261025_164018/per_class_report.csv
results/stylometry_linear_svc_20261025_164018/pooled_metrics.csv
results/stylometry_linear_svc_20261025_164018/run_metadata.json
results/stylometry_linear_svc_20261025_164018/summary.md
results/stylometry_linear_svc_20261025_164018/top_confusions.csv
results/stylometry_logreg_20261025_160722/bootstrap_ci.json
results/stylometry_logreg_20261025_160722/confusion_matrix.csv
results/stylometry_logreg_20261025_160722/confusion_matrix_normalized.csv
results/stylometry_logreg_20261025_160722/fold_metrics.csv
results/stylometry_logreg_20261025_160722/fold_summary.csv
results/stylometry_logreg_20261025_160722/oof_predictions.csv
results/stylometry_logreg_20261025_160722/per_class_report.csv
results/stylometry_logreg_20261025_160722/pooled_metrics.csv
results/stylometry_logreg_20261025_160722/run_metadata.json
results/stylometry_logreg_20261025_160722/summary.md
results/stylometry_logreg_20261025_160722/top_confusions.csv
results/stylometry_pos_logreg_20261025_164246/bootstrap_ci.json
results/stylometry_pos_logreg_20261025_164246/confusion_matrix.csv
results/stylometry_pos_logreg_20261025_164246/confusion_matrix_normalized.csv
results/stylometry_pos_logreg_20261025_164246/fold_metrics.csv
results/stylometry_pos_logreg_20261025_164246/fold_summary.csv
results/stylometry_pos_logreg_20261025_164246/oof_predictions.csv
results/stylometry_pos_logreg_20261025_164246/per_class_report.csv
results/stylometry_pos_logreg_20261025_164246/pooled_metrics.csv
results/stylometry_pos_logreg_20261025_164246/run_metadata.json
results/stylometry_pos_logreg_20261025_164246/summary.md
results/stylometry_pos_logreg_20261025_164246/top_confusions.csv
results/stylometry_pos_lsa_linear_svc_20261025_184431/bootstrap_ci.json
results/stylometry_pos_lsa_linear_svc_20261025_184431/confusion_matrix.csv
results/stylometry_pos_lsa_linear_svc_20261025_184431/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_linear_svc_20261025_184431/fold_metrics.csv
results/stylometry_pos_lsa_linear_svc_20261025_184431/fold_summary.csv
results/stylometry_pos_lsa_linear_svc_20261025_184431/oof_predictions.csv
results/stylometry_pos_lsa_linear_svc_20261025_184431/per_class_report.csv
results/stylometry_pos_lsa_linear_svc_20261025_184431/pooled_metrics.csv
results/stylometry_pos_lsa_linear_svc_20261025_184431/run_metadata.json
results/stylometry_pos_lsa_linear_svc_20261025_184431/summary.md
results/stylometry_pos_lsa_linear_svc_20261025_184431/top_confusions.csv
results/stylometry_pos_lsa_linear_svc_20261025_184538/bootstrap_ci.json
results/stylometry_pos_lsa_linear_svc_20261025_184538/confusion_matrix.csv
results/stylometry_pos_lsa_linear_svc_20261025_184538/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_linear_svc_20261025_184538/fold_metrics.csv
results/stylometry_pos_lsa_linear_svc_20261025_184538/fold_summary.csv
results/stylometry_pos_lsa_linear_svc_20261025_184538/oof_predictions.csv
results/stylometry_pos_lsa_linear_svc_20261025_184538/per_class_report.csv
results/stylometry_pos_lsa_linear_svc_20261025_184538/pooled_metrics.csv
results/stylometry_pos_lsa_linear_svc_20261025_184538/run_metadata.json
results/stylometry_pos_lsa_linear_svc_20261025_184538/summary.md
results/stylometry_pos_lsa_linear_svc_20261025_184538/top_confusions.csv
results/stylometry_pos_lsa_linear_svc_20261025_184646/bootstrap_ci.json
results/stylometry_pos_lsa_linear_svc_20261025_184646/confusion_matrix.csv
results/stylometry_pos_lsa_linear_svc_20261025_184646/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_linear_svc_20261025_184646/fold_metrics.csv
results/stylometry_pos_lsa_linear_svc_20261025_184646/fold_summary.csv
results/stylometry_pos_lsa_linear_svc_20261025_184646/oof_predictions.csv
results/stylometry_pos_lsa_linear_svc_20261025_184646/per_class_report.csv
results/stylometry_pos_lsa_linear_svc_20261025_184646/pooled_metrics.csv
results/stylometry_pos_lsa_linear_svc_20261025_184646/run_metadata.json
results/stylometry_pos_lsa_linear_svc_20261025_184646/summary.md
results/stylometry_pos_lsa_linear_svc_20261025_184646/top_confusions.csv
results/stylometry_pos_lsa_linear_svc_20261025_184759/bootstrap_ci.json
results/stylometry_pos_lsa_linear_svc_20261025_184759/confusion_matrix.csv
results/stylometry_pos_lsa_linear_svc_20261025_184759/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_linear_svc_20261025_184759/fold_metrics.csv
results/stylometry_pos_lsa_linear_svc_20261025_184759/fold_summary.csv
results/stylometry_pos_lsa_linear_svc_20261025_184759/oof_predictions.csv
results/stylometry_pos_lsa_linear_svc_20261025_184759/per_class_report.csv
results/stylometry_pos_lsa_linear_svc_20261025_184759/pooled_metrics.csv
results/stylometry_pos_lsa_linear_svc_20261025_184759/run_metadata.json
results/stylometry_pos_lsa_linear_svc_20261025_184759/summary.md
results/stylometry_pos_lsa_linear_svc_20261025_184759/top_confusions.csv
results/stylometry_pos_lsa_linear_svc_20261025_184916/bootstrap_ci.json
results/stylometry_pos_lsa_linear_svc_20261025_184916/confusion_matrix.csv
results/stylometry_pos_lsa_linear_svc_20261025_184916/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_linear_svc_20261025_184916/fold_metrics.csv
results/stylometry_pos_lsa_linear_svc_20261025_184916/fold_summary.csv
results/stylometry_pos_lsa_linear_svc_20261025_184916/oof_predictions.csv
results/stylometry_pos_lsa_linear_svc_20261025_184916/per_class_report.csv
results/stylometry_pos_lsa_linear_svc_20261025_184916/pooled_metrics.csv
results/stylometry_pos_lsa_linear_svc_20261025_184916/run_metadata.json
results/stylometry_pos_lsa_linear_svc_20261025_184916/summary.md
results/stylometry_pos_lsa_linear_svc_20261025_184916/top_confusions.csv
results/stylometry_pos_lsa_linear_svc_20261025_185037/bootstrap_ci.json
results/stylometry_pos_lsa_linear_svc_20261025_185037/confusion_matrix.csv
results/stylometry_pos_lsa_linear_svc_20261025_185037/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_linear_svc_20261025_185037/fold_metrics.csv
results/stylometry_pos_lsa_linear_svc_20261025_185037/fold_summary.csv
results/stylometry_pos_lsa_linear_svc_20261025_185037/oof_predictions.csv
results/stylometry_pos_lsa_linear_svc_20261025_185037/per_class_report.csv
results/stylometry_pos_lsa_linear_svc_20261025_185037/pooled_metrics.csv
results/stylometry_pos_lsa_linear_svc_20261025_185037/run_metadata.json
results/stylometry_pos_lsa_linear_svc_20261025_185037/summary.md
results/stylometry_pos_lsa_linear_svc_20261025_185037/top_confusions.csv
results/stylometry_pos_lsa_logreg_20261025_164534/bootstrap_ci.json
results/stylometry_pos_lsa_logreg_20261025_164534/comparison_main_vs_pos.json
results/stylometry_pos_lsa_logreg_20261025_164534/confusion_matrix.csv
results/stylometry_pos_lsa_logreg_20261025_164534/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_logreg_20261025_164534/fold_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_164534/fold_summary.csv
results/stylometry_pos_lsa_logreg_20261025_164534/length_bins.csv
results/stylometry_pos_lsa_logreg_20261025_164534/oof_predictions.csv
results/stylometry_pos_lsa_logreg_20261025_164534/per_class_report.csv
results/stylometry_pos_lsa_logreg_20261025_164534/pooled_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_164534/run_metadata.json
results/stylometry_pos_lsa_logreg_20261025_164534/subgroup_age_group_confusions.csv
results/stylometry_pos_lsa_logreg_20261025_164534/subgroup_age_group_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_164534/subgroup_gender_confusions.csv
results/stylometry_pos_lsa_logreg_20261025_164534/subgroup_gender_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_164534/subgroup_species_confusions.csv
results/stylometry_pos_lsa_logreg_20261025_164534/subgroup_species_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_164534/summary.md
results/stylometry_pos_lsa_logreg_20261025_164534/top_confusions.csv
results/stylometry_pos_lsa_logreg_20261025_183404/bootstrap_ci.json
results/stylometry_pos_lsa_logreg_20261025_183404/confusion_matrix.csv
results/stylometry_pos_lsa_logreg_20261025_183404/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_logreg_20261025_183404/fold_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_183404/fold_summary.csv
results/stylometry_pos_lsa_logreg_20261025_183404/oof_predictions.csv
results/stylometry_pos_lsa_logreg_20261025_183404/per_class_report.csv
results/stylometry_pos_lsa_logreg_20261025_183404/pooled_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_183404/run_metadata.json
results/stylometry_pos_lsa_logreg_20261025_183404/summary.md
results/stylometry_pos_lsa_logreg_20261025_183404/top_confusions.csv
results/stylometry_pos_lsa_logreg_20261025_183524/bootstrap_ci.json
results/stylometry_pos_lsa_logreg_20261025_183524/confusion_matrix.csv
results/stylometry_pos_lsa_logreg_20261025_183524/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_logreg_20261025_183524/fold_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_183524/fold_summary.csv
results/stylometry_pos_lsa_logreg_20261025_183524/oof_predictions.csv
results/stylometry_pos_lsa_logreg_20261025_183524/per_class_report.csv
results/stylometry_pos_lsa_logreg_20261025_183524/pooled_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_183524/run_metadata.json
results/stylometry_pos_lsa_logreg_20261025_183524/summary.md
results/stylometry_pos_lsa_logreg_20261025_183524/top_confusions.csv
results/stylometry_pos_lsa_logreg_20261025_183653/bootstrap_ci.json
results/stylometry_pos_lsa_logreg_20261025_183653/confusion_matrix.csv
results/stylometry_pos_lsa_logreg_20261025_183653/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_logreg_20261025_183653/fold_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_183653/fold_summary.csv
results/stylometry_pos_lsa_logreg_20261025_183653/oof_predictions.csv
results/stylometry_pos_lsa_logreg_20261025_183653/per_class_report.csv
results/stylometry_pos_lsa_logreg_20261025_183653/pooled_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_183653/run_metadata.json
results/stylometry_pos_lsa_logreg_20261025_183653/summary.md
results/stylometry_pos_lsa_logreg_20261025_183653/top_confusions.csv
results/stylometry_pos_lsa_logreg_20261025_183839/bootstrap_ci.json
results/stylometry_pos_lsa_logreg_20261025_183839/confusion_matrix.csv
results/stylometry_pos_lsa_logreg_20261025_183839/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_logreg_20261025_183839/fold_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_183839/fold_summary.csv
results/stylometry_pos_lsa_logreg_20261025_183839/oof_predictions.csv
results/stylometry_pos_lsa_logreg_20261025_183839/per_class_report.csv
results/stylometry_pos_lsa_logreg_20261025_183839/pooled_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_183839/run_metadata.json
results/stylometry_pos_lsa_logreg_20261025_183839/summary.md
results/stylometry_pos_lsa_logreg_20261025_183839/top_confusions.csv
results/stylometry_pos_lsa_logreg_20261025_184052/bootstrap_ci.json
results/stylometry_pos_lsa_logreg_20261025_184052/confusion_matrix.csv
results/stylometry_pos_lsa_logreg_20261025_184052/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_logreg_20261025_184052/fold_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_184052/fold_summary.csv
results/stylometry_pos_lsa_logreg_20261025_184052/oof_predictions.csv
results/stylometry_pos_lsa_logreg_20261025_184052/per_class_report.csv
results/stylometry_pos_lsa_logreg_20261025_184052/pooled_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_184052/run_metadata.json
results/stylometry_pos_lsa_logreg_20261025_184052/summary.md
results/stylometry_pos_lsa_logreg_20261025_184052/top_confusions.csv
results/stylometry_pos_lsa_logreg_20261025_184325/bootstrap_ci.json
results/stylometry_pos_lsa_logreg_20261025_184325/confusion_matrix.csv
results/stylometry_pos_lsa_logreg_20261025_184325/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_logreg_20261025_184325/fold_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_184325/fold_summary.csv
results/stylometry_pos_lsa_logreg_20261025_184325/oof_predictions.csv
results/stylometry_pos_lsa_logreg_20261025_184325/per_class_report.csv
results/stylometry_pos_lsa_logreg_20261025_184325/pooled_metrics.csv
results/stylometry_pos_lsa_logreg_20261025_184325/run_metadata.json
results/stylometry_pos_lsa_logreg_20261025_184325/summary.md
results/stylometry_pos_lsa_logreg_20261025_184325/top_confusions.csv
results/stylometry_pos_lsa_random_forest_20261025_190013/bootstrap_ci.json
results/stylometry_pos_lsa_random_forest_20261025_190013/confusion_matrix.csv
results/stylometry_pos_lsa_random_forest_20261025_190013/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_random_forest_20261025_190013/fold_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_190013/fold_summary.csv
results/stylometry_pos_lsa_random_forest_20261025_190013/oof_predictions.csv
results/stylometry_pos_lsa_random_forest_20261025_190013/per_class_report.csv
results/stylometry_pos_lsa_random_forest_20261025_190013/pooled_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_190013/run_metadata.json
results/stylometry_pos_lsa_random_forest_20261025_190013/summary.md
results/stylometry_pos_lsa_random_forest_20261025_190013/top_confusions.csv
results/stylometry_pos_lsa_random_forest_20261025_190125/bootstrap_ci.json
results/stylometry_pos_lsa_random_forest_20261025_190125/confusion_matrix.csv
results/stylometry_pos_lsa_random_forest_20261025_190125/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_random_forest_20261025_190125/fold_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_190125/fold_summary.csv
results/stylometry_pos_lsa_random_forest_20261025_190125/oof_predictions.csv
results/stylometry_pos_lsa_random_forest_20261025_190125/per_class_report.csv
results/stylometry_pos_lsa_random_forest_20261025_190125/pooled_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_190125/run_metadata.json
results/stylometry_pos_lsa_random_forest_20261025_190125/summary.md
results/stylometry_pos_lsa_random_forest_20261025_190125/top_confusions.csv
results/stylometry_pos_lsa_random_forest_20261025_190237/bootstrap_ci.json
results/stylometry_pos_lsa_random_forest_20261025_190237/confusion_matrix.csv
results/stylometry_pos_lsa_random_forest_20261025_190237/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_random_forest_20261025_190237/fold_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_190237/fold_summary.csv
results/stylometry_pos_lsa_random_forest_20261025_190237/oof_predictions.csv
results/stylometry_pos_lsa_random_forest_20261025_190237/per_class_report.csv
results/stylometry_pos_lsa_random_forest_20261025_190237/pooled_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_190237/run_metadata.json
results/stylometry_pos_lsa_random_forest_20261025_190237/summary.md
results/stylometry_pos_lsa_random_forest_20261025_190237/top_confusions.csv
results/stylometry_pos_lsa_random_forest_20261025_190354/bootstrap_ci.json
results/stylometry_pos_lsa_random_forest_20261025_190354/confusion_matrix.csv
results/stylometry_pos_lsa_random_forest_20261025_190354/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_random_forest_20261025_190354/fold_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_190354/fold_summary.csv
results/stylometry_pos_lsa_random_forest_20261025_190354/oof_predictions.csv
results/stylometry_pos_lsa_random_forest_20261025_190354/per_class_report.csv
results/stylometry_pos_lsa_random_forest_20261025_190354/pooled_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_190354/run_metadata.json
results/stylometry_pos_lsa_random_forest_20261025_190354/summary.md
results/stylometry_pos_lsa_random_forest_20261025_190354/top_confusions.csv
results/stylometry_pos_lsa_random_forest_20261025_190510/bootstrap_ci.json
results/stylometry_pos_lsa_random_forest_20261025_190510/confusion_matrix.csv
results/stylometry_pos_lsa_random_forest_20261025_190510/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_random_forest_20261025_190510/fold_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_190510/fold_summary.csv
results/stylometry_pos_lsa_random_forest_20261025_190510/oof_predictions.csv
results/stylometry_pos_lsa_random_forest_20261025_190510/per_class_report.csv
results/stylometry_pos_lsa_random_forest_20261025_190510/pooled_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_190510/run_metadata.json
results/stylometry_pos_lsa_random_forest_20261025_190510/summary.md
results/stylometry_pos_lsa_random_forest_20261025_190510/top_confusions.csv
results/stylometry_pos_lsa_random_forest_20261025_190626/bootstrap_ci.json
results/stylometry_pos_lsa_random_forest_20261025_190626/confusion_matrix.csv
results/stylometry_pos_lsa_random_forest_20261025_190626/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_random_forest_20261025_190626/fold_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_190626/fold_summary.csv
results/stylometry_pos_lsa_random_forest_20261025_190626/oof_predictions.csv
results/stylometry_pos_lsa_random_forest_20261025_190626/per_class_report.csv
results/stylometry_pos_lsa_random_forest_20261025_190626/pooled_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_190626/run_metadata.json
results/stylometry_pos_lsa_random_forest_20261025_190626/summary.md
results/stylometry_pos_lsa_random_forest_20261025_190626/top_confusions.csv
results/stylometry_pos_lsa_random_forest_20261025_190749/bootstrap_ci.json
results/stylometry_pos_lsa_random_forest_20261025_190749/confusion_matrix.csv
results/stylometry_pos_lsa_random_forest_20261025_190749/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_random_forest_20261025_190749/fold_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_190749/fold_summary.csv
results/stylometry_pos_lsa_random_forest_20261025_190749/oof_predictions.csv
results/stylometry_pos_lsa_random_forest_20261025_190749/per_class_report.csv
results/stylometry_pos_lsa_random_forest_20261025_190749/pooled_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_190749/run_metadata.json
results/stylometry_pos_lsa_random_forest_20261025_190749/summary.md
results/stylometry_pos_lsa_random_forest_20261025_190749/top_confusions.csv
results/stylometry_pos_lsa_random_forest_20261025_190910/bootstrap_ci.json
results/stylometry_pos_lsa_random_forest_20261025_190910/confusion_matrix.csv
results/stylometry_pos_lsa_random_forest_20261025_190910/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_random_forest_20261025_190910/fold_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_190910/fold_summary.csv
results/stylometry_pos_lsa_random_forest_20261025_190910/oof_predictions.csv
results/stylometry_pos_lsa_random_forest_20261025_190910/per_class_report.csv
results/stylometry_pos_lsa_random_forest_20261025_190910/pooled_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_190910/run_metadata.json
results/stylometry_pos_lsa_random_forest_20261025_190910/summary.md
results/stylometry_pos_lsa_random_forest_20261025_190910/top_confusions.csv
results/stylometry_pos_lsa_random_forest_20261025_191031/bootstrap_ci.json
results/stylometry_pos_lsa_random_forest_20261025_191031/confusion_matrix.csv
results/stylometry_pos_lsa_random_forest_20261025_191031/confusion_matrix_normalized.csv
results/stylometry_pos_lsa_random_forest_20261025_191031/fold_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_191031/fold_summary.csv
results/stylometry_pos_lsa_random_forest_20261025_191031/oof_predictions.csv
results/stylometry_pos_lsa_random_forest_20261025_191031/per_class_report.csv
results/stylometry_pos_lsa_random_forest_20261025_191031/pooled_metrics.csv
results/stylometry_pos_lsa_random_forest_20261025_191031/run_metadata.json
results/stylometry_pos_lsa_random_forest_20261025_191031/summary.md
results/stylometry_pos_lsa_random_forest_20261025_191031/top_confusions.csv
results/verification_stylometry_pos_lsa_20261025_172237/fold_metrics.csv
results/verification_stylometry_pos_lsa_20261025_172237/pair_scores.csv
results/verification_stylometry_pos_lsa_20261025_172237/summary.json

View File

@@ -1,65 +0,0 @@
# Презентация
Готовая презентация: `stardew_stylometry.pptx` (16:9, 30 слайдов в финальной версии; исходная автоматическая сборка — 32 слайда).
PDF-версия для просмотра: `stardew_stylometry.pdf`.
## Структура
- **Часть 1. Корпус** — 2005 реплик, 34 персонажа, сбор, очистка, разметка,
распределение реплик и длин, ограничения.
- **Часть 2. Методология** — признаки, LSA, логистическая регрессия, SVM,
деревья, ансамбли, перекрёстная проверка и метрики.
- **Часть 3. Результаты** — сравнение моделей, вклад признаков, контроль
содержания, матрицы ошибок, классы-аттракторы, выводы.
На каждом слайде есть фон `images/background.jpg` и маленькая курица
`images/chicken.png` как пасхалка. На титульном слайде — `images/title.jpg`;
на слайде о персонажах — `images/characters.webp`; на последнем слайде — мем
`images/meme.webp`. Круговые диаграммы — `images/replics-count.png` и
`images/median.png`.
## Сборка
```bash
cd lab1
python -m pip install -r presentation/requirements.txt
python presentation/make_slide_figures.py
python presentation/build_deck.py
```
`make_slide_figures.py` строит матрицы ошибок и сравнительные диаграммы из
`results/final/finalists/voting_et_char_lean/oof_predictions.csv` и
`FINAL_RESULTS.csv`. `build_deck.py` собирает PPTX, добавляет заметки
докладчика, фон, курицу и изображения.
## Редактирование
Текст и порядок слайдов задаются в `build_deck.py`. После изменения текста
достаточно повторно запустить сборку. Готовый PPTX можно править и напрямую
в PowerPoint/LibreOffice; заметки докладчика находятся в заметках слайдов.
Изображения игры используются в учебных целях; права принадлежат их
правообладателям.
## Текущая версия и автоматическая сборка
Актуальная версия — `stardew_stylometry.pptx`: она правится вручную и содержит
имена докладчиков и сокращённый набор слайдов. Скрипт `build_deck.py` — это
первоначальная автоматическая сборка; повторный запуск перезапишет файл и
потеряет ручные правки. Для точечной переработки первого блока использован
`rework_block1.py`; перед повторным применением скрипта сделайте резервную
копию PPTX.
## LaTeX-версия
Академическая Beamer-презентация:
- `latex/stardew_stylometry.tex` — исходник;
- `latex/stardew_stylometry.pdf` — собранный PDF, 30 страниц;
- сборка: `cd latex && xelatex stardew_stylometry.tex` (два прохода).
В LaTeX-версии три части на трёх докладчиков, фон и курица на каждом слайде,
титульная заставка, изображение персонажей, две круговые диаграммы, матрицы
ошибок, F-меры по персонажам, приложение с полной матрицей и финальный мем.
Формул почти нет: одна компактная запись обозначений на слайде постановки
задачи.

View File

@@ -1,809 +0,0 @@
"""Сборка презентации по лабораторной работе.
Требования:
- 16:9, фон на каждом слайде;
- три содержательных блока на трёх рассказчиков;
- курица на каждом слайде;
- титульная картинка, персонажи, две круговые диаграммы, матрицы ошибок,
мем на последнем слайде;
- тёмный текст на светлых панелях.
"""
from __future__ import annotations
from pathlib import Path
from PIL import Image
from pptx import Presentation
from pptx.dml.color import RGBColor
from pptx.enum.shapes import MSO_SHAPE
from pptx.enum.text import MSO_ANCHOR, PP_ALIGN
from pptx.util import Inches, Pt
ROOT = Path(__file__).resolve().parents[1]
IMAGES = ROOT / "presentation" / "images"
GEN = ROOT / "presentation" / "generated"
OUT = ROOT / "presentation" / "stardew_stylometry.pptx"
SW, SH = 13.333, 7.5
FONT = "Arial"
CREAM = RGBColor(248, 241, 220)
CREAM_LIGHT = RGBColor(255, 252, 240)
DARK = RGBColor(30, 45, 33)
GREEN = RGBColor(49, 93, 58)
GREEN_DARK = RGBColor(28, 63, 47)
GOLD = RGBColor(204, 153, 51)
BROWN = RGBColor(126, 80, 42)
RED = RGBColor(166, 55, 43)
WHITE = RGBColor(255, 255, 255)
BORDER = RGBColor(181, 158, 111)
def add_background(slide) -> None:
slide.shapes.add_picture(
str(IMAGES / "background.jpg"), 0, 0, width=Inches(SW), height=Inches(SH)
)
def add_panel(slide, x, y, w, h, fill=CREAM, line=BORDER, radius: float = 0.06):
shape = slide.shapes.add_shape(
MSO_SHAPE.ROUNDED_RECTANGLE, Inches(x), Inches(y), Inches(w), Inches(h)
)
shape.fill.solid()
shape.fill.fore_color.rgb = fill
shape.line.color.rgb = line
shape.line.width = Pt(1.1)
shape.shadow.inherit = False
try:
shape.adjustments[0] = radius
except Exception:
pass
return shape
def add_text(
slide,
x,
y,
w,
h,
text: str,
size: int = 18,
color=DARK,
bold: bool = False,
align=PP_ALIGN.LEFT,
anchor=MSO_ANCHOR.TOP,
margin: float = 0.08,
):
box = slide.shapes.add_textbox(Inches(x), Inches(y), Inches(w), Inches(h))
frame = box.text_frame
frame.word_wrap = True
frame.vertical_anchor = anchor
frame.margin_left = Inches(margin)
frame.margin_right = Inches(margin)
frame.margin_top = Inches(0.04)
frame.margin_bottom = Inches(0.04)
paragraph = frame.paragraphs[0]
paragraph.alignment = align
run = paragraph.add_run()
run.text = text
run.font.name = FONT
run.font.size = Pt(size)
run.font.bold = bold
run.font.color.rgb = color
return box
def add_bullets(slide, x, y, w, h, items, size: int = 16, color=DARK, title: str | None = None):
box = slide.shapes.add_textbox(Inches(x), Inches(y), Inches(w), Inches(h))
frame = box.text_frame
frame.word_wrap = True
frame.margin_left = Inches(0.08)
frame.margin_right = Inches(0.08)
frame.margin_top = Inches(0.04)
first = True
if title:
paragraph = frame.paragraphs[0]
paragraph.space_after = Pt(7)
run = paragraph.add_run()
run.text = title
run.font.name = FONT
run.font.size = Pt(size + 3)
run.font.bold = True
run.font.color.rgb = GREEN_DARK
first = False
for item in items:
if isinstance(item, tuple):
text, level = item
else:
text, level = item, 0
paragraph = frame.paragraphs[0] if first else frame.add_paragraph()
first = False
prefix = "• " if level == 0 else "– "
paragraph.text = prefix + text
paragraph.level = level
paragraph.space_after = Pt(5)
paragraph.line_spacing = 1.0
for run in paragraph.runs:
run.font.name = FONT
run.font.size = Pt(size if level == 0 else size - 1)
run.font.color.rgb = color
return box
def add_table(slide, x, y, w, h, rows, col_widths=None, font_size: int = 11, header: bool = True):
shape = slide.shapes.add_table(len(rows), len(rows[0]), Inches(x), Inches(y), Inches(w), Inches(h))
table = shape.table
if col_widths:
total = sum(col_widths)
for index, value in enumerate(col_widths):
table.columns[index].width = Inches(w * value / total)
for row_index, row in enumerate(rows):
for col_index, value in enumerate(row):
cell = table.cell(row_index, col_index)
cell.margin_left = Inches(0.05)
cell.margin_right = Inches(0.05)
cell.margin_top = Inches(0.02)
cell.margin_bottom = Inches(0.02)
cell.fill.solid()
if header and row_index == 0:
cell.fill.fore_color.rgb = GREEN
else:
cell.fill.fore_color.rgb = CREAM_LIGHT if row_index % 2 else CREAM
paragraph = cell.text_frame.paragraphs[0]
paragraph.alignment = PP_ALIGN.LEFT if col_index == 0 else PP_ALIGN.CENTER
run = paragraph.add_run()
run.text = str(value)
run.font.name = FONT
run.font.size = Pt(font_size)
run.font.bold = header and row_index == 0
run.font.color.rgb = WHITE if header and row_index == 0 else DARK
return table
def add_image_fit(slide, path: Path, x, y, max_w, max_h, panel: bool = False):
with Image.open(path) as image:
width, height = image.size
aspect = width / height
if max_w / max_h > aspect:
draw_h = max_h
draw_w = draw_h * aspect
else:
draw_w = max_w
draw_h = draw_w / aspect
draw_x = x + (max_w - draw_w) / 2
draw_y = y + (max_h - draw_h) / 2
if panel:
add_panel(slide, draw_x - 0.07, draw_y - 0.07, draw_w + 0.14, draw_h + 0.14, fill=CREAM_LIGHT)
slide.shapes.add_picture(str(path), Inches(draw_x), Inches(draw_y), width=Inches(draw_w), height=Inches(draw_h))
def add_chicken(slide, x: float = 12.96, y: float = 7.12, size: float = 0.24):
slide.shapes.add_picture(
str(IMAGES / "chicken.png"), Inches(x), Inches(y), width=Inches(size), height=Inches(size)
)
def add_notes(slide, text: str) -> None:
slide.notes_slide.notes_text_frame.text = text
def new_slide():
slide = Presentation().slides.add_slide # placeholder, replaced below
return slide
def add_title_bar(slide, title: str, block: str = "", subtitle: str | None = None):
add_panel(slide, 0.42, 0.28, 12.49, 0.86, fill=CREAM)
add_text(slide, 0.58, 0.34, 10.2, 0.45, title, size=25, bold=True, color=GREEN_DARK)
if block:
add_text(slide, 10.0, 0.42, 2.65, 0.35, block, size=13, color=BROWN, align=PP_ALIGN.RIGHT)
if subtitle:
add_text(slide, 0.58, 0.76, 11.8, 0.3, subtitle, size=12, color=BROWN)
def add_footer(slide, block: str, number: int):
add_text(slide, 0.45, 7.12, 7.8, 0.24, block, size=10, color=DARK)
add_text(slide, 12.15, 7.12, 0.55, 0.24, str(number), size=10, color=DARK, align=PP_ALIGN.RIGHT)
def content_slide(prs, title: str, block: str, number: int, subtitle: str | None = None):
slide = prs.slides.add_slide(prs.slide_layouts[6])
add_background(slide)
add_title_bar(slide, title, block, subtitle)
add_footer(slide, block, number)
add_chicken(slide)
return slide
def section_slide(prs, number: int, block: str, title: str, subtitle: str, questions: list[str], notes: str, slide_number: int):
slide = prs.slides.add_slide(prs.slide_layouts[6])
add_background(slide)
add_panel(slide, 0.65, 0.9, 12.0, 5.6, fill=CREAM)
add_text(slide, 0.95, 1.15, 2.6, 1.0, block, size=22, bold=True, color=GOLD)
add_text(slide, 0.95, 2.05, 11.2, 0.9, title, size=38, bold=True, color=GREEN_DARK)
add_text(slide, 0.95, 3.05, 11.0, 0.6, subtitle, size=20, color=BROWN)
add_bullets(slide, 0.95, 3.9, 11.0, 1.9, questions, size=17, color=DARK)
add_text(slide, 0.95, 5.9, 7.0, 0.35, f"Рассказчик {number}", size=15, bold=True, color=RED)
add_footer(slide, block, slide_number)
add_chicken(slide)
add_notes(slide, notes)
return slide
def main() -> int:
prs = Presentation()
prs.slide_width = Inches(SW)
prs.slide_height = Inches(SH)
slide_no = 0
# 1. Титульный слайд
slide_no += 1
slide = prs.slides.add_slide(prs.slide_layouts[6])
add_background(slide)
add_panel(slide, 0.55, 1.15, 6.15, 4.25, fill=CREAM)
add_image_fit(slide, IMAGES / "title.jpg", 0.7, 1.3, 5.85, 3.9, panel=False)
add_panel(slide, 6.95, 1.15, 5.85, 5.45, fill=CREAM)
add_text(slide, 7.2, 1.45, 5.3, 0.35, "Лабораторная работа по анализу текстов", size=13, color=BROWN)
add_text(slide, 7.2, 1.9, 5.3, 1.55, "Можно ли различить голоса персонажей?", size=32, bold=True, color=GREEN_DARK)
add_text(slide, 7.2, 3.45, 5.25, 1.4, "Стилометрическая классификация 34 персонажей Stardew Valley по 2005 репликам", size=18, color=DARK)
add_text(slide, 7.2, 4.9, 5.3, 0.6, "Три блока · три рассказчика", size=16, bold=True, color=RED)
add_text(slide, 7.2, 5.28, 5.3, 0.45, "Корпус → методология → результаты", size=15, color=BROWN)
add_panel(slide, 0.55, 5.75, 12.25, 0.8, fill=CREAM_LIGHT)
add_text(slide, 0.75, 5.9, 11.9, 0.45, "2005 реплик · 34 персонажа · один автор · 5-блочная перекрёстная проверка", size=14, color=DARK, align=PP_ALIGN.CENTER)
add_chicken(slide, 12.95, 7.12, 0.26)
add_notes(slide, "Приветствие. Тема — стилометрия реплик Stardew Valley. Сразу уточнить: автор один, задача — различить речевые маски персонажей, а не установить авторство. Три блока: корпус, методология, результаты.")
# 2. Структура
slide_no += 1
slide = content_slide(prs, "Структура доклада", "Введение", slide_no)
blocks = [
("01", "Корпус", "Рассказчик 1", ["2005 реплик", "34 персонажа", "сбор, очистка, разметка", "ограничения данных"]),
("02", "Методология", "Рассказчик 2", ["признаки и LSA", "классификаторы", "перекрёстная проверка", "метрики"]),
("03", "Результаты", "Рассказчик 3", ["лучшие модели", "матрицы ошибок", "кого путает модель", "выводы"]),
]
for index, (num, title, narrator, lines) in enumerate(blocks):
x = 0.45 + index * 4.27
add_panel(slide, x, 1.5, 4.0, 5.15, fill=CREAM)
add_text(slide, x + 0.25, 1.7, 0.9, 0.5, num, size=30, bold=True, color=GOLD)
add_text(slide, x + 0.25, 2.3, 3.5, 0.5, title, size=24, bold=True, color=GREEN_DARK)
add_text(slide, x + 0.25, 2.9, 3.5, 0.35, narrator, size=13, color=RED)
add_bullets(slide, x + 0.25, 3.45, 3.5, 2.7, lines, size=14, color=DARK)
add_notes(slide, "Показать три блока и передать слово первому рассказчику. Подчеркнуть: сначала данные, затем методы, затем интерпретация.")
# 3. Блок 1
slide_no += 1
section_slide(
prs, 1, "Блок 1", "Корпус: как собрали и разметили",
"Что именно мы классифицируем и почему это сложная задача",
["Откуда данные и какова единица анализа", "Кто в корпусе и как распределены реплики", "Длина реплик и очистка", "Паспорт корпуса и ограничения"],
"Передать слово рассказчику 1. Блок отвечает на вопрос: что представляет собой выборка и можно ли ей доверять.",
slide_no,
)
# 4. Что анализируем
slide_no += 1
slide = content_slide(prs, "Что анализируем", "Блок 1 · Корпус", slide_no)
add_panel(slide, 0.45, 1.4, 5.95, 5.4, fill=CREAM)
add_bullets(slide, 0.7, 1.6, 5.45, 4.9, [
"Единица анализа — одна реплика персонажа.",
"Всего 2005 реплик; 34 персонажа; один автор.",
"Класс — имя персонажа; один объект относится ровно к одному классу.",
"Реплики короткие: в среднем 19 слов, медиана 17.",
"Корпус англоязычный; разметка — только текст и персонаж.",
], size=17, title="Постановка задачи")
add_panel(slide, 6.65, 1.4, 6.2, 5.4, fill=CREAM_LIGHT)
add_text(slide, 6.85, 1.55, 5.8, 0.35, "Примеры разных голосов", size=16, bold=True, color=GREEN_DARK)
quotes = [
("Abigail (139 реплик)", "Oh, that's right... I heard someone new was moving onto that old farm."),
("Jas (34 реплики)", "Wow, a birthday present! I love it!"),
("Krobus (28 реплик)", "I wanted to give you this as a 'thank you' for letting me live here with you."),
("Wizard (24 реплики)", "Ah, yes. I have predicted your arrival a long time ago, young PLAYER."),
]
y = 1.98
for name, quote in quotes:
add_text(slide, 6.85, y, 5.8, 0.3, name, size=13, bold=True, color=BROWN)
add_text(slide, 6.85, y + 0.28, 5.75, 0.55, f"«{quote}»", size=13, color=DARK)
y += 1.08
add_notes(slide, "Объяснить: одна реплика — один объект. Показать четыре голоса: длинная речь Abigail, короткая детская реплика Jas, нечеловеческая вежливость Krobus, архаичный Wizard. Это иллюстрирует, что сигнал может быть в лексике, длине, пунктуации и синтаксисе.")
# 5. Кто в корпусе
slide_no += 1
slide = content_slide(prs, "Кто в корпусе", "Блок 1 · Корпус", slide_no)
add_panel(slide, 0.4, 1.35, 8.8, 5.55, fill=CREAM_LIGHT)
add_image_fit(slide, GEN / "characters.png", 0.5, 1.42, 8.6, 5.4)
add_panel(slide, 9.3, 1.35, 3.6, 5.55, fill=CREAM)
add_bullets(slide, 9.48, 1.55, 3.25, 5.1, [
"34 персонажа: подростки, взрослые, пожилые, дети и нелюди.",
"Все реплики написаны одним разработчиком; многие — в одних и тех же обновлениях и сюжетных контекстах.",
"Классы сильно неравномерны: от 15 до 139 реплик.",
"Метаданные о поле, возрасте и виде используются только для анализа ошибок, не как признаки.",
], size=14, title="Состав")
add_notes(slide, "Показать портреты. Подчеркнуть: один автор и общие сюжетные контексты — поэтому сходство голосов ожидаемо. Метаданные не подаются классификатору; они нужны только для интерпретации ошибок.")
# 6. Количество реплик
slide_no += 1
slide = content_slide(prs, "Сколько реплик у каждого", "Блок 1 · Корпус", slide_no, "Радиальная диаграмма: число реплик по персонажам")
add_panel(slide, 0.4, 1.35, 7.2, 5.55, fill=CREAM_LIGHT)
add_image_fit(slide, IMAGES / "replics-count.png", 0.5, 1.42, 7.0, 5.4)
add_panel(slide, 7.85, 1.35, 5.05, 5.55, fill=CREAM)
add_bullets(slide, 8.02, 1.55, 4.7, 5.0, [
"Максимум — Abigail: 139 реплик; минимум — Mister Qi: 15.",
"Разница между классами почти десятикратная.",
"При 34 классах случайное угадывание даёт около 2,9%.",
"Поэтому доля правильных ответов обманчива: модель может выбирать частые классы.",
"Основная метрика — макро-усреднённая F-мера: каждый персонаж весит одинаково.",
], size=16, title="Дисбаланс классов")
add_notes(slide, "Диаграмма показывает дисбаланс. Сразу объяснить, почему accuracy не подходит: модель, всегда выбирающая Abigail, получит около 7% правильных ответов, но почти нулевую macro-F1 по остальным 33 классам.")
# 7. Длина реплик
slide_no += 1
slide = content_slide(prs, "Длина реплик", "Блок 1 · Корпус", slide_no, "Радиальная диаграмма: медианная длина предложения в символах")
add_panel(slide, 0.4, 1.35, 7.2, 5.55, fill=CREAM_LIGHT)
add_image_fit(slide, IMAGES / "median.png", 0.5, 1.42, 7.0, 5.4)
add_panel(slide, 7.85, 1.35, 5.05, 5.55, fill=CREAM)
add_bullets(slide, 8.02, 1.55, 4.7, 5.0, [
"Средняя длина — 19,1 слова; медиана — 17 слов.",
"Диапазон — от 3 до 78 слов.",
"Медианная длина предложения в символах: от 52 у Vincent до 114 у Sandy.",
"Короткие реплики дают мало свидетельств; стилометрические оценки на них нестабильны.",
"Длина сама по себе — сильный, но нежелательный признак: она может стать прокси персонажа.",
], size=15, title="Короткие тексты")
add_notes(slide, "Числа у подписей — медианная длина предложения в символах. Показать контраст Vincent/Jas и Sandy/Wizard. Сказать, что длина — законный признак, но её вклад нужно проверять отдельно, иначе модель будет различать не стиль, а «кто говорит длиннее».")
# 8. Очистка и разметка
slide_no += 1
slide = content_slide(prs, "Очистка и разметка", "Блок 1 · Корпус", slide_no)
add_panel(slide, 0.45, 1.35, 6.2, 5.55, fill=CREAM)
add_bullets(slide, 0.65, 1.55, 5.85, 5.1, [
"Источник — игровые реплики; CSV в cp1251, XLSX как резерв.",
"Нормализация Unicode: многоточия, кавычки, тире, неразрывные пробелы.",
"Плейсхолдеры <PLAYER>, <FARM> и другие заменены нейтральными словами; их количество сохранено отдельным признаком.",
"Токенизация и разбиение на предложения — NLTK; частеречная разметка — Penn Treebank → Universal POS.",
"Стоп-слова, пунктуация и регистр не удалялись: это стилометрический сигнал.",
], size=15, title="Что сделали")
add_panel(slide, 6.9, 1.35, 5.95, 5.55, fill=CREAM_LIGHT)
add_bullets(slide, 7.1, 1.55, 5.55, 5.1, [
"Шум: дубликаты, технические артефакты, общие короткие фразы.",
"Ручная фильтрация уже удалила короткие и общие реплики; правила не сохранились.",
"В текущем CSV 0 точных дублей; 13 пар с косинусной близостью ≥ 0,8 — все внутри одного персонажа.",
"Нет идентификаторов сцен и диалогов; это ограничение анализа.",
"Разметка — только имя персонажа; никакой ручной стилистической разметки нет.",
], size=15, title="Что считаем шумом")
add_notes(slide, "Объяснить различие между шумом и стилем. Удалять пунктуацию или стоп-слова нельзя: это носители стиля. Ручная фильтрация — ограничение: она могла удалить как раз короткие характерные реплики. Отметить, что почти-дубликаты не пересекают классы, поэтому утечки через них нет.")
# 9. Паспорт корпуса
slide_no += 1
slide = content_slide(prs, "Паспорт корпуса", "Блок 1 · Корпус", slide_no)
add_panel(slide, 0.45, 1.35, 7.1, 5.55, fill=CREAM_LIGHT)
rows = [
["Показатель", "Значение"],
["Реплик", "2005"],
["Персонажей", "34"],
["Словоупотреблений", "38 207"],
["Средняя длина", "19,1 слова"],
["Медиана длины", "17 слов"],
["Минимум / максимум", "3 / 78 слов"],
["Классы", "15–139 реплик"],
["Точные дубли", "0"],
["Почти-дубли (cos ≥ 0,8)", "13 пар, все внутри класса"],
]
add_table(slide, 0.7, 1.6, 6.6, 4.9, rows, col_widths=[3.4, 3.2], font_size=13)
add_panel(slide, 7.85, 1.35, 5.05, 5.55, fill=CREAM)
add_bullets(slide, 8.02, 1.6, 4.7, 5.0, [
"Корпус пригоден для классификации, но не для сильных выводов о «чистом» стиле.",
"Ограничения: короткие тексты, дисбаланс, один автор, отсутствие сцен.",
"Все дальнейшие результаты нужно сравнивать с случайным уровнем 2,9%, а не с нулём.",
], size=16, title="Что важно")
add_notes(slide, "Этот слайд — «паспорт» данных. Подчеркнуть: 2005 объектов и 34 класса — небольшая выборка; 15 реплик у Mister Qi дают очень широкий интервал. Поэтому дальше используем повторную перекрёстную проверку и доверительные интервалы.")
# 10. Блок 2
slide_no += 1
section_slide(
prs, 2, "Блок 2", "Методология: как измеряем стиль",
"Признаки, модели и проверка — на понятном языке",
["Какие свойства текста считаем", "Как работает LSA", "Как устроены классификаторы", "Как не обмануть себя при оценке"],
"Передать слово рассказчику 2. Блок объясняет, что именно подаётся модели и почему выбранным методам можно доверять.",
slide_no,
)
# 11. Общая схема
slide_no += 1
slide = content_slide(prs, "Общая схема", "Блок 2 · Методология", slide_no)
steps = [
("Реплика", "текст персонажа"),
("Очистка", "Unicode, плейсхолдеры, пробелы"),
("Токены и POS", "NLTK, Universal POS"),
("Признаки", "ручные, n-граммы, LSA"),
("Модель", "вероятности по 34 классам"),
("Проверка", "5 блоков, повторы, метрики"),
]
x = 0.55
for index, (title, subtitle) in enumerate(steps):
add_panel(slide, x, 2.0, 1.85, 2.35, fill=CREAM)
add_text(slide, x + 0.12, 2.18, 1.6, 0.45, title, size=15, bold=True, color=GREEN_DARK, align=PP_ALIGN.CENTER)
add_text(slide, x + 0.1, 2.7, 1.65, 1.1, subtitle, size=12, color=DARK, align=PP_ALIGN.CENTER)
if index < len(steps) - 1:
add_text(slide, x + 1.82, 2.7, 0.35, 0.4, "→", size=26, bold=True, color=RED, align=PP_ALIGN.CENTER)
x += 2.12
add_panel(slide, 0.55, 4.75, 12.15, 1.55, fill=CREAM_LIGHT)
add_bullets(slide, 0.75, 4.9, 11.75, 1.2, [
"Обучение и преобразования выполняются только внутри обучающих блоков; тест не участвует в настройке.",
"Вектор признаков — это не «понимание» текста, а числовое описание его статистических свойств.",
], size=15)
add_notes(slide, "Провести по схеме слева направо. Главная мысль: модель не «читает» реплику как человек, а считает частоты и комбинации признаков. Отдельно подчеркнуть, что все преобразования обучаются внутри блоков, иначе возникает утечка.")
# 12. Признаки
slide_no += 1
slide = content_slide(prs, "Признаки: что именно считаем", "Блок 2 · Методология", slide_no)
add_panel(slide, 0.45, 1.35, 6.1, 5.55, fill=CREAM)
add_bullets(slide, 0.65, 1.55, 5.7, 5.1, [
"454 ручных признака в 11 группах.",
"Длина: слова, символы, предложения, длина слова.",
"Лексика и орфография: распределение длин, апострофы, заглавные, цифры.",
"Словарное богатство: TTR, Guiraud, Yule's K, hapax; на коротких текстах нестабильно.",
"Пунктуация: запятые, многоточия, восклицания, вопросы, повторы.",
"Разговорные маркеры: междометия, смех, хеджи, вежливость, сокращения.",
], size=14, title="Ручные признаки")
add_panel(slide, 6.75, 1.35, 6.1, 5.55, fill=CREAM_LIGHT)
add_bullets(slide, 6.95, 1.55, 5.7, 5.1, [
"Функциональные слова: около 150 закрытых слов и 12 категорий.",
"Местоимения: 1-е, 2-е, 3-е лицо; отношение «я/ты».",
"Части речи: Universal POS и Penn Treebank, время, модальность, герундий.",
"Синтаксис-прокси: клаузы, подчинительные союзы, пассив, вопросы.",
"Символьные n-граммы 2–5 и POS n-граммы 1–3.",
"LSA: сжатые векторы по словам, содержанию, POS и символам.",
], size=14, title="Синтаксис и векторные блоки")
add_notes(slide, "Не перечислять все 454 признака. Объяснить логику: закрытые классы слов и пунктуация — классическая стилометрия; n-граммы — морфология и синтаксис; LSA — сжатое представление. Отметить, что словарное богатство и читаемость помечены как предварительные из-за коротких текстов.")
# 13. LSA
slide_no += 1
slide = content_slide(prs, "LSA простыми словами", "Блок 2 · Методология", slide_no)
add_panel(slide, 0.45, 1.35, 6.0, 5.55, fill=CREAM)
add_bullets(slide, 0.65, 1.55, 5.6, 5.1, [
"Строим таблицу «документ × термин» и взвешиваем частоты через TF-IDF.",
"Сжимаем таблицу сингулярным разложением до k скрытых осей.",
"Каждая реплика превращается в вектор из k чисел.",
"Оси не обязаны быть темами; их знак и порядок произвольны.",
"k выбирается по объяснённой дисперсии и внешней метрике.",
], size=16, title="Идея")
add_panel(slide, 6.65, 1.35, 6.2, 5.55, fill=CREAM_LIGHT)
add_bullets(slide, 6.85, 1.55, 5.8, 5.1, [
"word — лексика и тема.",
"content — содержание после удаления стоп-слов.",
"fw — функциональные слова и синтаксис.",
"char — морфология и пунктуация.",
"pos — частеречные последовательности.",
"В работе k = 50–400; для content-LSA качество росло до 400.",
], size=15, title="Варианты")
add_panel(slide, 0.65, 5.65, 11.9, 0.8, fill=CREAM_LIGHT)
add_text(slide, 0.8, 5.78, 11.6, 0.5, "Ключевой вопрос: LSA по содержательным словам ловит тему, а не только стиль.", size=15, bold=True, color=RED, align=PP_ALIGN.CENTER)
add_notes(slide, "Объяснить SVD без формул: было много столбцов, стало несколько скрытых осей; это сжатие, а не «понимание». Вариант content может кодировать тему, поэтому его нельзя автоматически называть стилем. В конце блока результатов покажем контроль содержания.")
# 14. Логистическая регрессия
slide_no += 1
slide = content_slide(prs, "Логистическая регрессия", "Блок 2 · Методология", slide_no)
add_panel(slide, 0.45, 1.35, 6.0, 5.55, fill=CREAM)
add_text(slide, 0.7, 1.55, 5.5, 0.35, "Как работает", size=18, bold=True, color=GREEN_DARK)
add_bullets(slide, 0.7, 1.95, 5.5, 4.7, [
"Для каждого класса считает линейную оценку: сумма «признак × вес» плюс сдвиг.",
"Преобразует оценки в вероятности через softmax: они неотрицательны и суммируются в 1.",
"Обучение минимизирует перекрёстную энтропию — штраф за низкую вероятность истинного класса.",
"L2-регуляризация удерживает веса малыми; параметр C задаёт силу регуляризации.",
"class_weight='balanced' повышает вес редких классов.",
], size=15)
add_panel(slide, 6.65, 1.35, 6.2, 5.55, fill=CREAM_LIGHT)
add_bullets(slide, 6.85, 1.55, 5.8, 5.1, [
"Плюсы: устойчива при большом числе признаков, даёт вероятности, коэффициенты интерпретируемы.",
"Минусы: только линейные границы; при сильной корреляции признаков веса нестабильны.",
"В работе — основная линейная базовая модель.",
"Сравнение C: 0,03–10; балансировка классов критична.",
], size=15, title="Что важно")
add_notes(slide, "Объяснить на пальцах: модель складывает голоса признаков с весами; softmax превращает их в вероятности. C — обратная сила штрафа: меньше C — проще модель. Отметить, что без балансировки редкие персонажи почти не находятся; это было видно в эксперименте.")
# 15. SVM и деревья
slide_no += 1
slide = content_slide(prs, "SVM, деревья и ансамбли", "Блок 2 · Методология", slide_no)
add_panel(slide, 0.45, 1.35, 6.05, 5.55, fill=CREAM)
add_bullets(slide, 0.65, 1.55, 5.65, 5.1, [
"Линейный SVM ищет границу с максимальным зазором; C штрафует ошибки.",
"SVM не даёт вероятностей: только решающую функцию; тройка лучших считается по её значениям.",
"Случайный лес: много деревьев, каждое на своей подвыборке; голосование.",
"Сверхслучайные деревья (ExtraTrees): пороги в узлах выбираются случайно; меньше дисперсии, быстрее.",
"Мягкое голосование усредняет вероятности нескольких моделей.",
"Стековое объединение обучает мета-классификатор на предсказаниях базовых моделей.",
], size=14, title="Идеи моделей")
add_panel(slide, 6.7, 1.35, 6.15, 5.55, fill=CREAM_LIGHT)
add_bullets(slide, 6.9, 1.55, 5.75, 5.1, [
"Деревья ловят нелинейные взаимодействия: «если многоточий и короткая реплика — это Jas».",
"Легко переобучаются на 2005 объектах; нужны ограничения глубины и листьев.",
"Дельта Барроуза: расстояние до центроида по частотным словам — классическая проверка.",
"Ансамбль полезен, если ошибки моделей не полностью совпадают.",
"Стековое объединение в наших данных переобучилось и оказалось хуже голосования.",
], size=14, title="Сильные и слабые стороны")
add_notes(slide, "Не углубляться в оптимизацию. Главное: SVM — линейная граница с зазором; деревья — нелинейные правила; ExtraTrees — случайные пороги; голосование усредняет вероятности. Дельта Барроуза — независимая классическая проверка, а не ещё одна нейросеть.")
# 16. Проверка
slide_no += 1
slide = content_slide(prs, "Как проверяем", "Блок 2 · Методология", slide_no)
add_panel(slide, 0.45, 1.35, 6.25, 5.55, fill=CREAM)
add_bullets(slide, 0.65, 1.55, 5.85, 5.1, [
"Стратифицированная перекрёстная проверка: 5 блоков, доли классов сохраняются.",
"Повторяем 3–5 раз с разными разбиениями — оцениваем разброс.",
"Вложенная проверка: внешние блоки — оценка, внутренние — настройка.",
"Отложенная выборка 80/20 — независимая проверка финальных кандидатов.",
], size=16, title="Схема")
add_panel(slide, 6.9, 1.35, 5.95, 5.55, fill=CREAM_LIGHT)
add_bullets(slide, 7.1, 1.55, 5.55, 5.1, [
"TF-IDF, SVD, масштабирование и отбор признаков обучаются только на обучающей выборке.",
"Тестовая выборка не участвует в выборе гиперпараметров или признаков.",
"Проверка перестановкой меток дала 0,027–0,028 — случайный уровень.",
"Почти-дубликаты внутри одного класса не пересекают обучение и тест.",
"Метрики: макро-F1, сбалансированная доля, тройка лучших, MCC, каппа, матрица ошибок.",
], size=15, title="Контроль утечек")
add_notes(slide, "Объяснить, почему нельзя настраивать на тесте. Привести аналогию: если подсматривать ответы, оценка будет завышена. Перестановка меток — контроль: если модель на случайных метках даёт 0,027, пайплайн не течёт.")
# 17. Метрики
slide_no += 1
slide = content_slide(prs, "Метрики: что и зачем", "Блок 2 · Методология", slide_no)
add_panel(slide, 0.45, 1.35, 6.15, 5.55, fill=CREAM_LIGHT)
rows = [
["Метрика", "Что показывает"],
["Макро-F1", "Средняя F-мера по 34 классам; все персонажи равны"],
["Доля правильных", "Доля совпавших ответов; обманчива при дисбалансе"],
["Сбалансированная доля", "Средняя полнота по классам"],
["Тройка лучших", "Истинный класс в тройке лучших"],
["MCC / каппа", "Согласие с поправкой на случай"],
["Матрица ошибок", "Кто с кем путается"],
]
add_table(slide, 0.65, 1.55, 5.75, 4.9, rows, col_widths=[2.2, 3.6], font_size=12)
add_panel(slide, 6.8, 1.35, 6.05, 5.55, fill=CREAM)
add_bullets(slide, 7.0, 1.55, 5.65, 5.1, [
"Случайный уровень при 34 классах: macro-F1 ≈ 0,029, top-3 ≈ 0,088.",
"Модель, всегда выбирающая Abigail: доля правильных ответов ≈ 6,9%, macro-F1 ≈ 0,004.",
"Поэтому основной вывод делаем по macro-F1 и доверительным интервалам.",
"Дополнительно смотрим F-меру по каждому персонажу и матрицу ошибок.",
], size=16, title="Почему не доля правильных ответов")
add_notes(slide, "Главный методический слайд: объяснить, что accuracy при 34 классах может вводить в заблуждение. Пример Dummy: 6,9% accuracy, но почти нулевая macro-F1. Поэтому все выводы — по macro-F1, а per-class F1 показывает, кого именно модель не находит.")
# 18. Блок 3
slide_no += 1
section_slide(
prs, 3, "Блок 3", "Результаты и интерпретация",
"Что получилось, кого модель путает и что это значит",
["Лучшие модели и масштаб качества", "Стиль или содержание", "Матрицы ошибок и пары персонажей", "Ограничения и выводы"],
"Передать слово рассказчику 3. Здесь важно не только назвать число, но и объяснить, что оно означает и чего оно не означает.",
slide_no,
)
# 19. Главный результат
slide_no += 1
slide = content_slide(prs, "Главный результат", "Блок 3 · Результаты", slide_no)
add_panel(slide, 0.4, 1.35, 9.0, 5.55, fill=CREAM_LIGHT)
add_image_fit(slide, GEN / "model_comparison.png", 0.5, 1.45, 8.8, 5.35)
add_panel(slide, 9.55, 1.35, 3.35, 5.55, fill=CREAM)
add_bullets(slide, 9.72, 1.55, 3.05, 5.1, [
"Мягкое голосование: 0,304.",
"ExtraTrees, char=10000: 0,276.",
"Вложенный выбор: 0,275.",
"Случайный уровень: 0,029.",
"На отложенной выборке: 0,333.",
], size=15, title="Макро-F1")
add_notes(slide, "Назвать числа и сразу масштаб: 0,30 при 34 классах — это примерно в 10 раз выше случайного. Не говорить «низкая точность» без сравнения с 0,029. Оговорить, что 0,333 на отложенной выборке — одно разбиение и широкий интервал.")
# 20. Что добавляет качество
slide_no += 1
slide = content_slide(prs, "Что добавляет качество", "Блок 3 · Результаты", slide_no)
add_panel(slide, 0.4, 1.35, 8.95, 5.55, fill=CREAM_LIGHT)
add_image_fit(slide, GEN / "feature_progression.png", 0.5, 1.45, 8.75, 5.35)
add_panel(slide, 9.5, 1.35, 3.4, 5.55, fill=CREAM)
add_bullets(slide, 9.67, 1.55, 3.1, 5.1, [
"Ручные признаки: 0,105.",
"+ char/POS n-граммы: 0,182.",
"+ content-LSA: 0,249.",
"ExtraTrees + char=10000: 0,276.",
"Мягкое голосование: 0,304.",
"Основной прирост — лексика и n-граммы, не «чистый синтаксис».",
], size=14, title="По шагам")
add_notes(slide, "Показать, что качество растёт не от «магии модели», а от добавления лексических и символьных признаков. Отметить: функциональные слова и POS- LSA сами по себе дают только 0,06–0,07.")
# 21. Стиль или содержание
slide_no += 1
slide = content_slide(prs, "Стиль или содержание?", "Блок 3 · Результаты", slide_no)
add_panel(slide, 0.4, 1.35, 8.4, 5.55, fill=CREAM_LIGHT)
add_image_fit(slide, GEN / "topic_control.png", 0.5, 1.45, 8.2, 5.35)
add_panel(slide, 8.95, 1.35, 3.95, 5.55, fill=CREAM)
add_bullets(slide, 9.12, 1.55, 3.65, 5.1, [
"Исходный текст: 0,248.",
"После замены содержательных слов: 0,113.",
"Падение более чем вдвое.",
"Остаток 0,113 всё ещё в 3,9 раза выше случайного.",
"Значит, стиль есть, но содержание доминирует.",
], size=15, title="Контроль")
add_notes(slide, "Это ключевой слайд для честной интерпретации. Содержательные слова заменили на CONTENT, оставив служебные слова, POS и пунктуацию. Качество упало с 0,248 до 0,113. Значит, нельзя утверждать, что модель выучила «чистый идиолект»: она во многом различает, о чём персонаж говорит.")
# 22. Как читать матрицу
slide_no += 1
slide = content_slide(prs, "Как читать матрицу ошибок", "Блок 3 · Результаты", slide_no)
add_panel(slide, 0.45, 1.35, 6.2, 5.55, fill=CREAM_LIGHT)
add_text(slide, 0.7, 1.55, 5.7, 0.4, "Маленький пример", size=17, bold=True, color=GREEN_DARK)
mini = [
["истина \\ прогноз", "A", "B", "C"],
["A", "70", "20", "10"],
["B", "15", "75", "10"],
["C", "5", "25", "70"],
]
add_table(slide, 0.7, 2.05, 5.6, 2.5, mini, col_widths=[2.2, 1.1, 1.1, 1.1], font_size=14)
add_text(slide, 0.7, 4.7, 5.7, 1.8, "Строка — истинный персонаж. Столбец — предсказание модели. Диагональ — правильные ответы. Вне диагонали — ошибки. Числа — проценты от строки.", size=14, color=DARK)
add_panel(slide, 6.85, 1.35, 6.0, 5.55, fill=CREAM)
add_bullets(slide, 7.05, 1.55, 5.6, 5.1, [
"Полная матрица 34×34 на доске нечитаема.",
"Поэтому на слайде — 12 самых частых персонажей и столбец «прочие».",
"Отдельно показываем 18 самых частых пар ошибок.",
"Полная матрица 34×34 — в раздатке/приложении.",
"На доске смотрим диагональ, крупные внедиагональные клетки и «прочие».",
], size=15, title="Как показываем")
add_notes(slide, "Объяснить матрицу на маленьком примере. Сказать: полная матрица 34×34 на доске превращается в кашу; поэтому используем три уровня: top-12 матрица, top-18 пар, полная матрица в раздатке. Это методически честно и читаемо.")
# 23. Матрица top12
slide_no += 1
slide = content_slide(prs, "Матрица ошибок: 12 персонажей", "Блок 3 · Результаты", slide_no)
add_panel(slide, 0.35, 1.35, 9.05, 5.55, fill=CREAM_LIGHT)
add_image_fit(slide, GEN / "confusion_top12.png", 0.45, 1.45, 8.85, 5.35)
add_panel(slide, 9.6, 1.35, 3.3, 5.55, fill=CREAM)
add_bullets(slide, 9.77, 1.55, 3.0, 5.1, [
"Диагональ: доля правильных ответов внутри класса.",
"Abigail: 21% верно, 18% уходит в «прочие».",
"Alex: 37% верно, 11% → Abigail.",
"Sebastian: 19% верно, 20% → Abigail.",
"У многих строк крупная доля «прочих»: модель не находит редкие классы.",
], size=13, title="Что видно")
add_notes(slide, "Провести по строкам: Abigail, Alex, Sebastian. Обратить внимание на столбец «прочие»: он большой, потому что 22 класса не поместились. Это не «мусор», а важная часть ошибок. Сказать, что диагональ далеко не 100%, но и не 0.")
# 24. Кого путает
slide_no += 1
slide = content_slide(prs, "Кого модель путает", "Блок 3 · Результаты", slide_no)
add_panel(slide, 0.35, 1.35, 9.0, 5.55, fill=CREAM_LIGHT)
add_image_fit(slide, GEN / "confusion_pairs.png", 0.45, 1.45, 8.8, 5.35)
add_panel(slide, 9.55, 1.35, 3.35, 5.55, fill=CREAM)
add_bullets(slide, 9.72, 1.55, 3.05, 5.1, [
"Sebastian → Abigail: 65.",
"Sebastian → Alex: 61.",
"Abigail → Sebastian: 59.",
"Abigail → Alex: 56.",
"Основной клубок — подростки/молодые взрослые: Abigail, Alex, Sebastian, Haley, Sam.",
], size=13, title="Частые пары")
add_notes(slide, "Показать, что ошибки не случайны: они концентрируются в одном круге персонажей. Это ожидаемо: один автор, общие темы, сходная эмоциональная окраска, короткие реплики. Это не доказательство «автор не старался», а наоборот — свидетельство близких речевых масок внутри группы.")
# 25. Классы-аттракторы
slide_no += 1
slide = content_slide(prs, "Классы-аттракторы", "Блок 3 · Результаты", slide_no)
add_panel(slide, 0.4, 1.35, 9.0, 5.55, fill=CREAM_LIGHT)
add_image_fit(slide, GEN / "hub_shares.png", 0.5, 1.45, 8.8, 5.35)
add_panel(slide, 9.55, 1.35, 3.35, 5.55, fill=CREAM)
add_bullets(slide, 9.72, 1.55, 3.05, 5.1, [
"Alex: 11,4% предсказаний при истинной доле 6,0%.",
"Abigail: 10,9% при 6,9%.",
"Редкие классы недопредставлены.",
"Это не «демографическая» путаница, а стягивание к популярным голосам.",
"Коррекция приоров не улучшила macro-F1.",
], size=13, title="Кто перетягивает")
add_notes(slide, "Объяснить «класс-аттрактор»: модель чаще выбирает Alex и Abigail, потому что их признаки часто встречаются и они похожи на многих. Редкие персонажи проигрывают. Простая коррекция приоров не помогла; нужны более тонкие правила или больше данных.")
# 26. Per-class F1
slide_no += 1
slide = content_slide(prs, "Персонажи: кто различается", "Блок 3 · Результаты", slide_no)
add_panel(slide, 0.3, 1.35, 9.25, 5.55, fill=CREAM_LIGHT)
add_image_fit(slide, GEN / "per_class_f1.png", 0.4, 1.45, 9.05, 5.35)
add_panel(slide, 9.75, 1.35, 3.15, 5.55, fill=CREAM)
add_bullets(slide, 9.9, 1.55, 2.85, 5.1, [
"Лучшие: Willy, Evelyn, Pierre, Leo, Pam.",
"Худшие: Kent, Mister Qi, Penny, Abigail, Leah.",
"Редкие классы — широкие интервалы.",
"Мягкое голосование улучшает Wizard, Dwarf, Vincent.",
], size=13, title="Факты")
add_notes(slide, "Показать, что качество по персонажам неоднородно. Evelyn/Willy/Pierre различаются лучше; Kent/Mister Qi/Penny — хуже. Это связано и с числом реплик, и с их речевой функцией. Не делать вывод, что персонаж «плохой»: у редких классов просто мало данных.")
# 27. Почему путаются
slide_no += 1
slide = content_slide(prs, "Почему персонажи путаются", "Блок 3 · Результаты", slide_no)
add_panel(slide, 0.45, 1.35, 6.15, 5.55, fill=CREAM)
add_bullets(slide, 0.65, 1.55, 5.75, 5.1, [
"Один автор и общие сюжетные контексты: персонажи говорят об одних и тех же событиях.",
"Сходные возраст и роль: подростки/молодые взрослые используют похожие темы и интонации.",
"Короткие реплики: мало слов, чтобы проявить индивидуальный синтаксис.",
"Дисбаланс: редкие классы реже выбираются моделью.",
"Общие формулы вежливости, шутки, междометия.",
], size=15, title="Причины")
add_panel(slide, 6.8, 1.35, 6.05, 5.55, fill=CREAM_LIGHT)
add_bullets(slide, 7.0, 1.55, 5.65, 5.1, [
"Клубок: Abigail, Alex, Sebastian, Haley, Sam.",
"Нелюди (Krobus, Dwarf, Wizard) различаются лучше, но Wizard и Dwarf редки.",
"Дети (Jas, Vincent) имеют короткие реплики; их легче узнать по длине.",
"Это не доказательство сходства «характеров», а свойство текстовых признаков.",
"Нужны сцены и больше данных, чтобы отделить стиль от контекста.",
], size=15, title="Что это значит")
add_notes(slide, "Ответить на вопрос «почему одни и те же персонажи путаются»: один автор, общие темы, сходный возраст/роль, короткие реплики, дисбаланс. Подчеркнуть: мы не делаем психологических выводов о персонажах; мы объясняем статистику ошибок.")
# 28. Что означают оценки
slide_no += 1
slide = content_slide(prs, "Что означают оценки", "Блок 3 · Результаты", slide_no)
add_panel(slide, 0.45, 1.35, 12.4, 5.55, fill=CREAM)
add_bullets(slide, 0.7, 1.55, 11.9, 5.1, [
"0,30 макро-F1 при 34 классах — это примерно в 10 раз выше случайного уровня 0,029.",
"Абсолютное значение умеренное: по одной короткой реплике персонажа действительно трудно определить.",
"Это не означает, что автор «не старался» или что стиль неразличим. Наоборот, устойчивое превышение случайного уровня говорит о намеренно различных голосах.",
"Умеренность объясняется короткими текстами, дисбалансом классов, общими темами и формулами, а не отсутствием стиля.",
"Мы не решаем задачу «он или не он писал»: все реплики одного автора. Мы решаем, какой персонаж говорит.",
], size=17, title="Уточнение интерпретации")
add_notes(slide, "Здесь важно поправить исходную формулировку. Нельзя говорить «низкие оценки хорошие, потому что автор не старался». Корректно: оценки выше случайного уровня, но умеренные; это свидетельствует о частичной различимости голосов. Один автор — значит, различия созданы намеренно, а не разными авторами. Низкие абсолютные значения — следствие трудности задачи, а не приговор стилю.")
# 29. Ограничения
slide_no += 1
slide = content_slide(prs, "Ограничения", "Блок 3 · Результаты", slide_no)
add_panel(slide, 0.45, 1.35, 6.15, 5.55, fill=CREAM)
add_bullets(slide, 0.65, 1.55, 5.75, 5.1, [
"Один корпус, одна игра, один автор.",
"Нет внешней валидации на другом корпусе.",
"Короткие реплики и дисбаланс классов.",
"Ручная фильтрация корпуса не воспроизводится.",
"Нет идентификаторов сцен и диалогов.",
], size=16, title="Данные")
add_panel(slide, 6.8, 1.35, 6.05, 5.55, fill=CREAM_LIGHT)
add_bullets(slide, 7.0, 1.55, 5.65, 5.1, [
"Выбор признаков и ансамбля опирался на предыдущие результаты.",
"Вложенная проверка снижает, но не устраняет оптимизм.",
"Содержательные слова и тема вносят большой вклад.",
"Простая калибровка приоров не помогла.",
"Стековое объединение переобучается на малом числе объектов.",
], size=16, title="Методология")
add_notes(slide, "Перечислить ограничения честно. Главное: нельзя переносить выводы на другие игры/авторов без внешней проверки. И нельзя называть результат «чистым стилем», пока не отделена тема.")
# 30. Выводы
slide_no += 1
slide = content_slide(prs, "Выводы", "Блок 3 · Результаты", slide_no)
add_panel(slide, 0.45, 1.35, 12.4, 5.55, fill=CREAM)
add_bullets(slide, 0.75, 1.55, 11.8, 5.1, [
"Персонажи статистически различимы: лучшая модель — 0,304 макро-F1, консервативная оценка — 0,275 при случайном уровне 0,029.",
"Основной вклад дают лексика, символьные n-граммы и LSA по содержательным словам; функциональные слова и синтаксис сами по себе слабы.",
"После замены содержательных слов качество падает с 0,248 до 0,113: содержание доминирует, но стилевой сигнал сохраняется.",
"Главная проблема — редкие классы и классы-аттракторы; мягкое голосование помогает, но не решает её полностью.",
"Корректный вывод: различимы речевые маски по лексико-тематическому профилю; для утверждения о «чистом стиле» нужны внешняя валидация и тематический контроль.",
], size=17, title="Пять тезисов")
add_notes(slide, "Сформулировать выводы без переубеждения аудитории. Главное: не «мы всё доказали», а «мы получили устойчивый выше случайного уровня сигнал; основной вклад — лексика/тема; для стиля нужны дальнейшие проверки».")
# 31. Приложение: полная матрица
slide_no += 1
slide = content_slide(prs, "Приложение: полная матрица 34×34", "Приложение", slide_no)
add_panel(slide, 0.35, 1.35, 7.4, 5.55, fill=CREAM_LIGHT)
add_image_fit(slide, GEN / "confusion_full34.png", 0.45, 1.45, 7.2, 5.35)
add_panel(slide, 7.95, 1.35, 4.95, 5.55, fill=CREAM)
add_bullets(slide, 8.12, 1.55, 4.6, 5.1, [
"Полная матрица 34×34 — для раздаточного материала.",
"На проекторе она слишком плотная; в докладе используем top-12 и top-18 пар.",
"Строки — истинные персонажи; столбцы — предсказания; цвет — доля строки.",
"Диагональ — правильные ответы; тёмные внедиагональные клетки — систематические ошибки.",
], size=15, title="Как использовать")
add_notes(slide, "Этот слайд можно не показывать, а раздать. Если показывать, не читать все клетки: показать диагональ и несколько тёмных пятен.")
# 32. Финал: мем
slide_no += 1
slide = prs.slides.add_slide(prs.slide_layouts[6])
add_background(slide)
add_panel(slide, 0.55, 1.2, 6.2, 5.45, fill=CREAM)
add_text(slide, 0.85, 1.5, 5.6, 1.0, "Спасибо за внимание!", size=34, bold=True, color=GREEN_DARK)
add_text(slide, 0.85, 2.55, 5.5, 1.5, "Вопросы, обсуждение и предложения по экспериментам", size=18, color=DARK)
add_text(slide, 0.85, 4.2, 5.5, 1.2, "«Один автор — разные голоса»", size=20, bold=True, color=RED)
add_panel(slide, 7.0, 0.9, 5.8, 6.0, fill=CREAM_LIGHT)
add_image_fit(slide, GEN / "meme.png", 7.15, 1.05, 5.5, 5.7)
add_chicken(slide, 0.25, 7.05, 0.28)
add_notes(slide, "Финальный слайд с мемом. Поблагодарить, перейти к вопросам. Курица спрятана слева внизу — пасхалка.")
prs.save(OUT)
print(f"Готово: {OUT}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 678 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 314 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 411 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 680 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 795 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -1,985 +0,0 @@
% !TeX program = xelatex
% Стилометрическая классификация реплик Stardew Valley — академическая Beamer-презентация.
% Сборка: xelatex stardew_stylometry.tex (два прохода) в каталоге latex/.
\documentclass[aspectratio=169,10pt]{beamer}
\usepackage{fontspec}
\usepackage[russian]{babel}
\babelfont{rm}{Liberation Sans}
\babelfont{sf}{Liberation Sans}
\usepackage{amsmath}
\usepackage{tikz}
\usetikzlibrary{calc}
\usepackage[most]{tcolorbox}
\usepackage{graphicx}
\usepackage{etoolbox}
\graphicspath{{../images/}{../generated/}}
% ---------- Цвета: сдержанная академическая гамма ----------
\definecolor{cream}{RGB}{253,246,231}
\definecolor{inkbrown}{RGB}{62,42,26}
\definecolor{accent}{RGB}{140,95,45}
% ---------- Общий вид ----------
\usefonttheme{professionalfonts}
\setbeamertemplate{navigation symbols}{}
\setbeamertemplate{caption}{\raggedright\insertcaption\par}
\setbeamercolor{normal text}{fg=inkbrown}
\setbeamercolor{frametitle}{fg=inkbrown}
\setbeamercolor{structure}{fg=accent}
\setbeamercolor{itemize item}{fg=accent}
\setbeamercolor{itemize subitem}{fg=accent}
\setbeamercolor{itemize/enumerate body}{fg=inkbrown}
\setbeamercolor{enumerate item}{fg=accent}
\setbeamersize{text margin left=0.55cm, text margin right=0.55cm}
\setbeamertemplate{itemize item}{\textbullet}
\setbeamertemplate{itemize subitem}{--}
% ---------- Нижний колонтитул: номер слайда и сколько осталось ----------
% Рисуется в конце каждой рамки поверх содержимого и не меняет вёрстку.
\newcommand{\slidefooter}{%
\begin{tikzpicture}[remember picture, overlay]
\node[anchor=north east, xshift=-0.1cm, yshift=-0.1cm]
at (current page.north east)
{\colorbox{cream}{\scriptsize\color{inkbrown}%
\insertframenumber\slash\inserttotalframenumber
\quad}};
\end{tikzpicture}%
}
% ---------- Фон: изображение + маленькая курица на каждом слайде ----------
\usebackgroundtemplate{%
\begin{tikzpicture}[remember picture, overlay]
\node at (current page.center)
{\includegraphics[width=\paperwidth,height=\paperheight]{background.jpg}};
\node[anchor=south east, xshift=-2.5mm, yshift=1.5mm]
at (current page.south east)
{\includegraphics[height=0.65cm]{chicken.png}};
\end{tikzpicture}}
% ---------- Заголовок слайда: кремовая полоса во всю ширину ----------
\newlength{\sidemargin}
\setlength{\sidemargin}{0.55cm}
\setbeamertemplate{frametitle}{%
\nointerlineskip
\vspace*{-0.2mm}%
\hspace*{-\sidemargin}%
\begin{tcolorbox}[enhanced, width=\paperwidth, colback=cream, colframe=cream,
opacityback=0.90, arc=0mm, boxrule=0pt,
left=\sidemargin, right=\sidemargin, top=1.2mm, bottom=1.2mm,
before skip=0pt, after skip=2mm]
{\usebeamerfont{frametitle}\usebeamercolor[fg]{frametitle}\large\bfseries\insertframetitle}
\end{tcolorbox}}
% ---------- Кремовые панели для содержимого ----------
\tcbset{panelstyle/.style={enhanced, colback=cream, opacityback=0.90,
colframe=accent, opacityframe=0.65, boxrule=0.6pt, arc=1.4mm,
boxsep=1.2mm, top=1.4mm, bottom=1.4mm, left=2.4mm, right=2.4mm,
before skip=1.6mm, after skip=1.6mm}}
\newtcolorbox{panel}[1][]{panelstyle, #1}
% ---------- Кремовый бокс с изо­бражением ----------
\newtcolorbox{imgbox}[1][]{enhanced, colback=cream, opacityback=0.90,
colframe=accent, opacityframe=0.65, boxrule=0.6pt, arc=1.4mm,
boxsep=0.8mm, top=0.8mm, bottom=0.8mm, left=0.8mm, right=0.8mm,
before skip=1.6mm, after skip=1.6mm, halign=center, #1}
% ---------- Титульный шаблон ----------
\title[Стилометрия реплик Stardew Valley]{Стилометрическая классификация реплик персонажей Stardew Valley}
\author{А.~А.~Аладьина \and В.~Н.~Копнин \and М.~В.~Савостиков}
\institute{ПИЭ-13МО}
\setbeamerfont{note page}{size=\footnotesize}
\begin{document}
% ==================== СЛАЙД: ТИТУЛ ====================
\begin{frame}[plain]
\begin{tikzpicture}[remember picture, overlay]
\node at (current page.center)
{\includegraphics[width=\paperwidth,height=\paperheight]{title.jpg}};
\node[anchor=south east, xshift=-2.5mm, yshift=1.5mm]
at (current page.south east)
{\includegraphics[height=0.65cm]{chicken.png}};
\end{tikzpicture}
\vspace*{-2.2cm}
\begin{center}
\begin{tcolorbox}[enhanced, width=0.92\paperwidth, colback=cream, opacityback=0.92,
colframe=cream, boxrule=0pt, arc=2mm, halign=center, boxsep=2mm]
{\large\bfseries Стилометрическая классификация реплик персонажей Stardew Valley}\\[2mm]
{\normalsize Можно ли различить голоса персонажей, \\ если все реплики написаны одним автором?}\\[2.4mm]
А.~А.~Аладьина \quad·\quad В.~Н.~Копнин \quad·\quad М.~В.~Савостиков\\[0.8mm]
\footnotesize ПИЭ-13МО
\end{tcolorbox}
\end{center}
\slidefooter
\end{frame}
% ==================== БЛОК 1 ====================
\begin{frame}{Часть 1\ \ ·\ \ Задача и корпус}
\begin{center}
\begin{tcolorbox}[enhanced, width=0.86\paperwidth, colback=cream, opacityback=0.9,
colframe=cream, boxrule=0pt, arc=2mm, halign=center, boxsep=3mm]
{\large Часть 1}\\[1.5mm]
{\Large\bfseries Решаемая задача и корпус}\\[2mm]
Постановка задачи · сбор и очистка данных · характеристики корпуса\\[2.5mm]
{\bfseries Докладчик: А.~А.~Аладьина}
\end{tcolorbox}
\end{center}
\note{Часть\,1. Говорит А. А. Аладьина. План части: что за задача мы решаем,
её формальная постановка, как собирали и чистили корпус, и какими
характеристиками он обладает.}
\slidefooter
\end{frame}
\begin{frame}{Задача и постановка}
\begin{columns}[T]
\begin{column}{0.57\textwidth}
\begin{panel}
\textbf{Решаемая задача}
\begin{itemize}
\item Дано: 2005 коротких реплик, 34 персонажа, один автор.
\item Требуется: по одной реплике определить персонажа.
\item Каждый объект относится ровно к одному из 34 классов.
\item Это не авторская атрибуция: мы различаем \emph{речевые маски персонажей}, а не авторов.
\item Проверяется, статистически различимы ли эти маски.
\end{itemize}
\end{panel}
\end{column}
\begin{column}{0.41\textwidth}
\begin{panel}
\textbf{Обозначения}\\[1mm]
$D=\{(x_i,\,y_i)\}_{i=1}^{n}$,\quad $n=2005$;\\
$x_i$ — реплика;\\
$y_i\in Y$, \ $|Y|=34$ — персонаж;\\
$f$ — классификатор.\\[1.5mm]
Критерий качества — макро-F1: средняя F-мера по 34 классам.
\end{panel}
\end{column}
\end{columns}
\note{Говорит А. А. Аладьина. Задача — многоклассовая классификация коротких
текстов. Формула одна: D — корпус, x — реплика, y — персонаж, f —
классификатор. Дальше все определения даются словами. Подчеркнуть: это не
авторская атрибуция, автор один; мы различаем речевые маски.}
\slidefooter
\end{frame}
\begin{frame}{Корпус: сбор, разметка, очистка}
\begin{columns}[T]
\begin{column}{0.33\textwidth}
\begin{panel}
\footnotesize\textbf{1. Сбор}
\begin{itemize}
\item Реплики из диалогов игры.
\item Единица анализа — одна реплика.
\item 34 персонажа, тексты на английском.
\end{itemize}
\end{panel}
\end{column}
\begin{column}{0.33\textwidth}
\begin{panel}
\footnotesize\textbf{2. Разметка}
\begin{itemize}
\item Метка — имя говорящего персонажа.
\item Ручная проверка реплики и метки.
\item Метаданные (пол, возраст, вид) хранятся отдельно и не подаются модели.
\end{itemize}
\end{panel}
\end{column}
\begin{column}{0.33\textwidth}
\begin{panel}
\footnotesize\textbf{3. Очистка}
\begin{itemize}
\item \textbf{Unicode-очистка}: разные варианты кавычек, тире,
многоточий и пробелов приводятся к единому виду.
\item Плейсхолдеры вида \texttt{<PLAYER>} заменены нейтральными словами.
\item Пунктуация и регистр сохранены: это стилометрический сигнал.
\end{itemize}
\end{panel}
\end{column}
\end{columns}
\begin{panel}
\footnotesize \textbf{Ограничения данных:} ручная фильтрация; нет идентификаторов сцен.
\end{panel}
\slidefooter
\end{frame}
\begin{frame}{Игровые персонажи}
\begin{center}
\begin{imgbox}
\includegraphics[height=0.65\textheight]{characters.png}
\end{imgbox}
\end{center}
\slidefooter
\end{frame}
\begin{frame}{Характеристики корпуса: классы}
\begin{columns}[T]
\begin{column}{0.42\textwidth}
\begin{center}
\begin{imgbox}
\includegraphics[width=\linewidth]{replics-count.png}
\end{imgbox}
\end{center}
\end{column}
\begin{column}{0.57\textwidth}
\begin{panel}
\begin{tabular}{@{}p{3.2cm}p{3.6cm}@{}}
\textbf{Реплик} & 2005\\[0.8mm]
\textbf{Персонажей} & 34\\[0.8mm]
\textbf{Максимум класса} & Abigail, 139\\[0.8mm]
\textbf{Минимум класса} & Mister Qi, 15\\[0.8mm]
\textbf{Точные дубли} & 0\\[0.8mm]
\textbf{Почти-дубли} & 13 пар, но только внутри класса\\
\end{tabular}
\end{panel}
\begin{panel}
Дисбаланс почти десятикратный: от 15 до 139 реплик. Поэтому
доля правильных ответов обманчива; основная метрика — макро усреднённая
F-мера по классам.
\end{panel}
\end{column}
\end{columns}
\slidefooter
\end{frame}
\begin{frame}{Характеристики корпуса: длина реплик}
\begin{columns}[T]
\begin{column}{0.42\textwidth}
\begin{imgbox}
\includegraphics[width=\linewidth]{median.png}
\end{imgbox}
\end{column}
\begin{column}{0.57\textwidth}
\begin{panel}
\begin{tabular}{@{}p{4.2cm}p{2.9cm}@{}}
\textbf{Средняя длина} & 19,1 слова\\[0.8mm]
\textbf{Медиана} & 17 слов\\[0.8mm]
\textbf{Диапазон} & 3–78 слов\\
\end{tabular}
\end{panel}
\begin{panel}
\begin{itemize}
\item Короткие реплики дают мало информации для стилометрических характеристик.
\item Длина сама по себе — сильный, но опасный признак: она может
подменять «стиль» просто узнаванием персонажа.
\item Поэтому её вклад проверялся отдельно: с исключением
признаков длины и по группам длины.
\end{itemize}
\end{panel}
\end{column}
\end{columns}
\note{Говорит А. А. Аладьина. Средняя реплика — всего 19 слов, медиана 17,
диапазон 3–78. На таких коротких текстах признаки словарного богатства и
читаемости нестабильны — это одна из причин низких абсолютных значений
качества, о которых расскажет третий докладчик.}
\slidefooter
\end{frame}
% ==================== БЛОК 2 ====================
\begin{frame}{Часть 2\ \ ·\ \ Методология}
\begin{center}
\begin{tcolorbox}[enhanced, width=0.86\paperwidth, colback=cream, opacityback=0.9,
colframe=cream, boxrule=0pt, arc=2mm, halign=center, boxsep=3mm]
{\large Часть 2}\\[1.5mm]
{\Large\bfseries Методология: как измеряем стиль}\\[2mm]
Признаки · LSA · классификаторы · схема проверки · метрики\\[2.5mm]
{\bfseries Докладчик: В.~Н.~Копнин}
\end{tcolorbox}
\end{center}
\note{Часть\,2. Говорит В. Н. Копнин. План части: общая схема конвейера,
какие признаки строим, как работает LSA, какие модели сравнивали и как устроена
проверка, чтобы не обмануть себя.}
\slidefooter
\end{frame}
\begin{frame}{Термины, которые встретятся дальше}
\begin{columns}[T]
\begin{column}{0.49\textwidth}
\begin{panel}
\footnotesize
\textbf{Фолд} — одна из частей разбиения выборки при перекрёстной
проверке. При 5 фолдах модель обучается на 4 и проверяется на 1,
затем разбиение меняется.\\[0.8mm]
\textbf{POS (part of speech)} — часть речи; POS-тег — её
обозначение; POS-грамма — последовательность тегов.\\[0.8mm]
\textbf{Universal POS} — универсальный набор тегов: NOUN, VERB,
ADJ, ADV, PRON, DET, ADP, CONJ, NUM, PRT, INTJ, PUNCT, X.
Единый набор для разных языков.\\[0.8mm]
\textbf{Клауза} — синтаксическая единица с подлежащим и
сказуемым; в репликах оценивается приблизительно.\\[0.8mm]
\textbf{Синтаксис-прокси} — приблизительная оценка синтаксиса
без полного разбора: число союзов, длина предложения, порядок слов.
\end{panel}
\end{column}
\begin{column}{0.49\textwidth}
\begin{panel}
\footnotesize
\textbf{n-грамм} — последовательность $n$ соседних единиц:
символов, слов или POS-тегов.\\[0.8mm]
\textbf{LSA} — сжатие таблицы «документ $\times$ признак» в
несколько скрытых компонент.\\[0.8mm]
\textbf{Макро-F1} — средняя F-мера по классам; каждый персонаж
весит одинаково.\\[0.8mm]
\textbf{Unicode-очистка} — приведение разных вариантов записи
символов к единому виду: кавычки, тире, многоточия, пробелы.\\[0.8mm]
\textbf{Класс-аттрактор} — класс, к которому модель ошибочно
относит чужие реплики.\\[0.8mm]
\textbf{Фолд и группа признаков} — разные вещи: фолд — часть
проверки, группа — набор признаков.
\end{panel}
\end{column}
\end{columns}
\note{Говорит В. Н. Копнин. Этот слайд — словарь терминов. Важно различать
фолд (часть кросс-валидации) и группу признаков (набор характеристик текста).
Universal POS — не «ещё одна модель», а универсальный набор частеречных
тегов; POS-граммы — последовательности этих тегов. Клауза и
синтаксис-прокси — приблизительные, но интерпретируемые оценки синтаксиса.}
\slidefooter
\end{frame}
\begin{frame}{Общая схема}
\begin{panel}
Реплика (текст персонажа) $\to$ очистка (Unicode, плейсхолдеры)
$\to$ токены и частеречная разметка (NLTK, Universal POS)
$\to$ признаки (ручные, n-граммы, LSA)
$\to$ модель (вероятности по 34 классам)
$\to$ проверка (5 фолдов, повторы, метрики).
\end{panel}
\begin{columns}[T]
\begin{column}{0.49\textwidth}
\begin{panel}
\textbf{Что важно про схему}
\begin{itemize}
\item Все обучаемые преобразования настраиваются только внутри
обучающих фолдах.
\item Тестовые фолды не участвуют ни в настройке признаков, ни в
выборе параметров модели.
\end{itemize}
\end{panel}
\end{column}
\begin{column}{0.49\textwidth}
\begin{panel}
\textbf{Что такое «вектор признаков»}
\begin{itemize}
\item Это не «понимание» текста, а числовое описание его
статистических свойств: длины, частоты, пунктуация, темы.
\end{itemize}
\end{panel}
\end{column}
\end{columns}
\note{Говорит В. Н. Копнин. Общий конвейер: текст превращается в цепочку
преобразований и в конце — в вектор чисел, по которому модель выбирает
класс. Ключевое правило: всё, что «учится» на данных, учится только на
обучающей части каждой проверки.}
\slidefooter
\end{frame}
\begin{frame}{454 ручных признака: 11 групп}
\begin{panel}
\begin{tabular}{@{}p{4cm}p{9.7cm}@{}}
\textbf{Группа} & \textbf{Примеры признаков} \\ \hline
Длина & число слов, символов, предложений; длина предложения \\
Лексика & длины слов, апострофы, заглавные буквы, цифры \\
Словарное богатство & TTR, Guiraud, Yule's K, hapax; на коротких текстах нестабильно \\
Символы & частоты букв, регистр, пунктуация \\
Пунктуация & запятые, многоточия, вопросы, восклицания, повторы \\
Функциональные слова & артикли, предлоги, союзы, местоимения \\
Местоимения & 1-е, 2-е, 3-е лицо; отношение «я/ты» \\
Части речи & Universal POS, Penn Treebank, время, модальность \\
Синтаксис & клаузы, подчинительные союзы, пассивные конструкции \\
Читаемость & Flesch, FK, SMOG; на коротких репликах приблизительна \\
Разговорные маркеры & междометия, смех, хеджи, вежливость, сокращения \\
\end{tabular}
\end{panel}
\note{Говорит В. Н. Копнин. Это расшифровка одиннадцати групп. Не нужно
запоминать все 454 признака: важнее логика — длина, лексика, пунктуация,
служебные слова, части речи, синтаксис, читаемость и разговорные маркеры.
Словарное богатство и читаемость на коротких репликах нестабильны.}
\slidefooter
\end{frame}
\begin{frame}{Автоматические признаки: n-граммы и LSA}
\begin{columns}[T]
\begin{column}{0.49\textwidth}
\begin{panel}
\textbf{n-граммы}
\begin{itemize}
\item \textbf{n-грамм} — последовательность $n$ соседних
единиц: символов, слов или POS-тегов.
\item Символьные n-граммы 2–5: морфология, пунктуация,
орфография.
\item n-граммы POS-тегов 1–3: последовательности частей речи,
синтаксические шаблоны.
\item TF-IDF: вес признака с учётом частоты в реплике и
редкости в корпусе.
\end{itemize}
\end{panel}
\end{column}
\begin{column}{0.49\textwidth}
\begin{panel}
\textbf{LSA}
\begin{itemize}
\item Сжатие таблицы «реплика $\times$ признак» в $k$ скрытых
компонент.
\item Варианты: \texttt{word}, \texttt{content}, \texttt{fw},
\texttt{char}, \texttt{pos}.
\item $k=50$–400; для \texttt{content}-LSA качество росло до 400.
\item LSA — сжатое представление, а не «понимание» текста.
\end{itemize}
\end{panel}
\end{column}
\end{columns}
\begin{panel}
Символьные n-граммы чувствительны к морфологии и пунктуации;
n-граммы POS-тегов — к порядку частей речи; LSA сжимает разреженные
признаки в несколько чисел. Это три разных взгляда на стиль.
\end{panel}
\note{Говорит В. Н. Копнин. n-грамма — это просто «окно» из n соседних
единиц. Символьные n-граммы ловят морфологию и пунктуацию, n-граммы POS-тегов —
синтаксические шаблоны. LSA не «понимает» текст: он сжимает таблицу
признаков в несколько скрытых компонент.}
\slidefooter
\end{frame}
\begin{frame}{LSA простыми словами}
\begin{columns}[T]
\begin{column}{0.49\textwidth}
\begin{panel}
\textbf{Идея}
\begin{itemize}
\item Строим таблицу «реплика $\times$ слово», взвешиваем
частоты через TF-IDF.
\item Сжимаем таблицу до $k$ скрытых осей: каждая реплика
превращается в короткий вектор.
\item Оси не обязаны быть «темами»: их порядок и знак произвольны.
\end{itemize}
\end{panel}
\end{column}
\begin{column}{0.49\textwidth}
\begin{panel}
\textbf{Варианты LSA}
\begin{itemize}
\item \texttt{word} — лексика и тема.
\item \texttt{content} — содержание без служебных слов.
\item \texttt{fw} — функциональные слова и синтаксис.
\item \texttt{char} — морфология и пунктуация.
\item \texttt{pos} — частеречные последовательности.
\end{itemize}
\end{panel}
\end{column}
\end{columns}
\begin{panel}
$k$ подбирался в диапазоне 50–400; для \texttt{content}-LSA качество
устойчиво росло до 400.
Ключевой вопрос: ловит ли LSA по содержательным словам \emph{тему}, а не
только стиль?
\end{panel}
\note{Говорит В. Н. Копнин. LSA объясняем без формул: таблица частот —
взвешивание — сжатие до k осей — короткий вектор для каждой реплики.
Важно ограничение метода: оси формально не интерпретируются, а LSA по
содержательным словам может кодировать тему. Для этого мы и делали
несколько вариантов LSA, чтобы сравнить тематический и стилистический
вклад.}
\slidefooter
\end{frame}
\begin{frame}{Классификаторы}
\begin{columns}[T]
\begin{column}{0.49\textwidth}
\begin{panel}
\textbf{Линейные модели}
\begin{itemize}
\item Логистическая регрессия: «признак $\times$ вес», затем
вероятности; редкие классы получают повышенные веса.
\item Линейный SVM: граница с максимальным зазором; часто силён
на тексте, но вероятностей не выдаёт.
\end{itemize}
\end{panel}
\end{column}
\begin{column}{0.49\textwidth}
\begin{panel}
\textbf{Деревья и ансамбли}
\begin{itemize}
\item Случайный лес и ExtraTrees: сотни деревьев; у ExtraTrees
пороги выбираются случайно — меньше разброс.
\item Мягкое голосование: усредняем вероятности моделей.
\item Объединение через мета-классификатор (стекинг) у нас переобучилось и оказалось хуже.
\end{itemize}
\end{panel}
\end{column}
\end{columns}
\begin{panel}
Сравнивали несколько семейств, чтобы вывод не зависел от одного
алгоритма: близкие результаты у разных моделей делают вывод надёжнее.
\end{panel}
\note{Говорит В. Н. Копнин. Логистическая регрессия — интерпретируемая
линейная база; SVM — сила на текстовых признаках, но без вероятностей;
деревья ловят нелинейные комбинации («многоточие + короткая реплика»), но
легко переобучаются на 2005 объектах. Лучший результат дало мягкое
голосование трёх моделей; объединение через мета-классификатор было хуже — переобучилось.}
\slidefooter
\end{frame}
\begin{frame}{Как проверяем}
\begin{columns}[T]
\begin{column}{0.49\textwidth}
\begin{panel}
\textbf{Схема оценки}
\begin{itemize}
\item Стратифицированная перекрёстная проверка: 5 фолдов
(частей разбиения); на 4 фолдах обучаемся, на 1 проверяем.
Доли классов внутри фолдов сохраняются.
\item Повторяем 3–5 раз с разными разбиениями — оцениваем разброс.
\item Вложенная схема: внешние фолды — оценка, внутренние —
настройка параметров модели.
\item Отдельная отложенная выборка 80/20 для финальных кандидатов.
\end{itemize}
\end{panel}
\end{column}
\begin{column}{0.49\textwidth}
\begin{panel}
\textbf{Контроль ошибок исследователя}
\begin{itemize}
\item TF-IDF, сжатие и масштабирование обучаются только на
обучающих фолдах — борьба с утечкой данных.
\item Контроль перестановкой меток дал 0,027–0,028 — воспроизведён
случайный уровень.
\item Почти-дубликаты находятся только внутри одного класса и
не пересекают обучение и тест.
\end{itemize}
\end{panel}
\end{column}
\end{columns}
\vspace{-1mm}
\note{Говорит В. Н. Копнин. Схема: 5-фолдовая стратифицированная
перекрёстная проверка с повторами; вложенная — для честной настройки
параметров; отложенная выборка — дополнительная проверка. Отдельно
переставляли метки: качество упало до случайного уровня, значит сигнал
не в «утечке».}
\slidefooter
\end{frame}
\begin{frame}{Метрики: что и зачем}
\begin{panel}
\begin{tabular}{@{}p{3.9cm}p{9.6cm}@{}}
\textbf{Метрика} & \textbf{Что показывает}\\[1mm]
\textbf{Macro-F1} & Средняя F-мера по 34 классам: каждый персонаж важен одинаково, включая редких. Основная метрика.\\[1mm]
\textbf{Доля правильных ответов} & Доля совпавших предсказаний; при дисбалансе обманчива: модель, всегда выбирающая частый класс, выглядит неплохо.\\[1mm]
\textbf{Сбалансированная доля} & Средняя полнота по классам: находит ли модель редких персонажей.\\[1mm]
\textbf{Тройка лучших (top-3)} & Истинный персонаж оказался в тройке наиболее вероятных ответов.\\[1mm]
\textbf{MCC / каппа} & Согласие с поправкой на случайное угадывание.\\[1mm]
\end{tabular}
\end{panel}
\begin{panel}
Ориентиры при 34 классах: случайный уровень macro-F1 $\approx$ 0,029,
top-3 $\approx$ 0,088.
\end{panel}
\note{Говорит В. Н. Копнин. Главная метрика — macro-F1: она равно
взвешивает все 34 класса, включая редких. Доля правильных ответов
обманчива при дисбалансе. Смысл всех остальных метрик — словесный, без
формул. Ориентиры случайного уровня уже озвучены — они пригодятся в
части 3.}
\slidefooter
\end{frame}
\begin{frame}{Гипотезы исследования}
\begin{columns}[T]
\begin{column}{0.57\textwidth}
\begin{panel}
\textbf{H1.} Функциональные слова, части речи и символьные
n-граммы (последовательности соседних символов) различают
персонажей выше случайного уровня.\\[0.7mm]
\textbf{H2.} На коротких репликах части речи и символьные
n-граммы устойчивее словарного богатства и читаемости.\\[0.7mm]
\textbf{H3.} LSA по всем словам отражает тему, а LSA по частям
речи и функциональным словам — стиль.\\[0.7mm]
\textbf{H4.} Качество зависит от длины реплики.\\[0.7mm]
\textbf{H5.} Ошибки группируются вокруг персонажей со сходным
возрастом, полом, ролью или видом.
\end{panel}
\end{column}
\begin{column}{0.41\textwidth}
\begin{panel}
\textbf{Как проверяем}
\begin{itemize}
\item H1 — сравнение моделей и поочерёдное исключение групп.
\item H2 — исключение групп и анализ по длине.
\item H3 — варианты LSA и контроль содержания.
\item H4 — метрики по группам длины.
\item H5 — анализ ошибок по метаданным.
\end{itemize}
\end{panel}
\end{column}
\end{columns}
\note{Говорит А. А. Аладьина. Гипотезы сформулированы до экспериментов.
H1 и H2 проверяют, есть ли стилометрический сигнал; H3 — стиль или тему;
H4 — влияние длины; H5 — социальные подгруппы. Термин «фолд» появится в
методологической части: это часть разбиения при перекрёстной проверке.}
\slidefooter
\end{frame}
% ==================== БЛОК 3 ====================
\begin{frame}{Часть 3\ \ ·\ \ Результаты}
\begin{center}
\begin{tcolorbox}[enhanced, width=0.86\paperwidth, colback=cream, opacityback=0.9,
colframe=cream, boxrule=0pt, arc=2mm, halign=center, boxsep=3mm]
{\large Часть 3}\\[1.5mm]
{\Large\bfseries Результаты и интерпретация}\\[2mm]
Оценки качества · вклад признаков · матрицы ошибок · ограничения · выводы\\[2.5mm]
{\bfseries Докладчик: М.~В.~Савостиков}
\end{tcolorbox}
\end{center}
\note{Часть\,3. Говорит М. В. Савостиков. План части: главные числа, что даёт
качество, стиль или содержание, кого модель путает, как корректно читать
оценки, ограничения и выводы.}
\slidefooter
\end{frame}
\begin{frame}{Главный результат}
\begin{columns}[T]
\begin{column}{0.47\textwidth}
\begin{panel}
\begin{tabular}{@{}p{5cm}r@{}}
\textbf{Мягкое голосование (5 фолдов)} & 0,304\\
\textbf{ExtraTrees, char = 10\,000} & 0,276\\
\textbf{Вложенный выбор} & 0,275\\
\textbf{Отложенная выборка} & 0,333\\
\textbf{Перестановка меток} & 0,029\\
\end{tabular}
\end{panel}
\begin{panel}
Числа — macro-F1. Случайный уровень для 34 классов — 0,029;
контроль перестановкой меток вышел на этот уровень, значит в извлеченных характеристиках есть информация.
\end{panel}
\end{column}
\begin{column}{0.52\textwidth}
\begin{imgbox}
\includegraphics[width=\linewidth]{model_comparison.png}
\end{imgbox}
\end{column}
\end{columns}
\note{Говорит М. В. Савостиков. Главная цифра — macro-F1 0,304 для мягкого
голосования при 5-фолдовой проверке. Более консервативная оценка — 0,275:
там конфигурация выбиралась честно, внутри обучающей части каждого фолда.
На отложенной выборке 0,333, но это одно разбиение. Суть: персонажи
действительно различимы.}
\slidefooter
\end{frame}
\begin{frame}{Что добавляет качество}
\begin{columns}[T]
\begin{column}{0.52\textwidth}
\begin{imgbox}
\includegraphics[width=\linewidth, height=0.55\textheight, keepaspectratio]{feature_progression.png}
\end{imgbox}
\end{column}
\begin{column}{0.47\textwidth}
\begin{panel}
\begin{itemize}
\item Только длина: 0,035.
\item Ручные стилометрические признаки: 0,105.
\item $+$ символьные и частеречные n-граммы: 0,182.
\item $+$ content-LSA: 0,249.
\item ExtraTrees, char = 10\,000: 0,276.
\item Мягкое голосование: 0,304.
\end{itemize}
Итого: основной прирост — лексика, n-граммы и content-LSA;
«чистый синтаксис» (функциональные слова и POS-LSA) сам по себе
слаб — важный отрицательный результат.
\end{panel}
\end{column}
\end{columns}
\note{Говорит М. В. Савостиков. Прогрессия: вручную посчитанные признаки
дают 0,105, добавляются символьные n-граммы — 0,182, LSA по содержанию —
0,249, лучшая одиночная модель — 0,276, ансамбль — 0,304. Обратный вывод
важен тоже: признаки «чистого синтаксиса» сами по себе почти ничего не
дают.}
\slidefooter
\end{frame}
\begin{frame}{Стиль или содержание?}
\begin{columns}[T]
\begin{column}{0.5\textwidth}
\begin{imgbox}
\includegraphics[width=\linewidth, height=0.55\textheight, keepaspectratio]{topic_control.png}
\end{imgbox}
\end{column}
\begin{column}{0.49\textwidth}
\begin{panel}
\begin{itemize}
\item Проверка: содержательные слова (существительные, глаголы,
прилагательные, наречия) заменены нейтральным маркером.
\item Исходный текст: 0,248; после маскирования: 0,113.
\item Падение более чем вдвое, но остаток 0,113 — в 3,9 раза
выше случайного уровня.
\end{itemize}
\textbf{Вывод контроля:} более половины качества даёт лексическое
\emph{содержание}; стилевой сигнал сохраняется, но слабее
тематического.
\end{panel}
\end{column}
\end{columns}
\note{Говорит М. В. Савостиков. Ключевой эксперимент: заменяем все
содержательные слова нейтральным маркером. Качество резко падает (0,248
становится 0,113). Тем не менее остаток — почти в четыре раза выше
случайного: какой-то стилевой компонент есть. Из этого следует главная
оговорка работы: мы не можем утверждать, что модель выучила «чистый
стиль» персонажей — основной вклад даёт тематический профиль.}
\slidefooter
\end{frame}
\begin{frame}{Матрица ошибок: как читать}
\begin{columns}[T]
\begin{column}{0.55\textwidth}
\begin{imgbox}
\includegraphics[width=\linewidth, height=0.66\textheight, keepaspectratio]{confusion_top12.png}
\end{imgbox}
\end{column}
\begin{column}{0.44\textwidth}
\begin{panel}
\begin{itemize}
\item \textbf{Строки} — истинный персонаж, \textbf{столбцы} —
предсказание; \textbf{диагональ} — верные ответы,
вне диагонали — ошибки.
\item Полная матрица 34$\times$34 нечитаема: показываем top-12.
\end{itemize}
\end{panel}
\begin{panel}
\textbf{Что видно:} крупная доля вне диагонали у многих строк —
редкие классы «утекают» в популярные.
\end{panel}
\end{column}
\end{columns}
\note{Говорит М. В. Савостиков. Обязательно проговорить, как читать матрицу:
строка — кто говорил на самом деле, столбец — кого предсказала модель,
диагональ — верные ответы, всё вне диагонали — ошибки. Полные 34 строки не
читаются на проекторе, поэтому top-12 + пары + полная матрица в приложении.}
\slidefooter
\end{frame}
\begin{frame}{Кого модель путает чаще всего}
\begin{columns}[T]
\begin{column}{0.52\textwidth}
\begin{imgbox}
\includegraphics[width=\linewidth, height=0.45\textheight, keepaspectratio]{confusion_pairs.png}
\end{imgbox}
\end{column}
\begin{column}{0.47\textwidth}
\begin{panel}
\textbf{Частые пары ошибок (число перепутанных реплик)}
\begin{itemize}
\item Sebastian $\to$ Abigail: 65
\item Sebastian $\to$ Alex: 61
\item Abigail $\to$ Sebastian: 59
\item Abigail $\to$ Alex: 56
\end{itemize}
Основной клубок — молодые персонажи: Abigail, Alex, Sebastian,
Haley, Sam. Один автор и общие сюжетные контексты делают сходство
голосов ожидаемым. Это коллапс редких классов
в классы-аттракторы.
\end{panel}
\vspace{-1mm}
\end{column}
\end{columns}
\note{Говорит М. В. Савостиков. Самые частые пары: Sebastian и Abigail
путаются в обе стороны (65 и 59), Sebastian и Alex (61), Abigail и Alex
(56). Ошибки сконцентрированы вокруг группы молодых персонажей. Важно: это
стягивание предсказаний к популярным классам, а не доказательство, что эти
персонажи «говорят одинаково» — хотя перекрёстные путаницы Sebastian —
Abigail — Alex согласуются с их сходными масками.}
\slidefooter
\end{frame}
\begin{frame}{Классы-аттракторы}
\begin{columns}[T]
\begin{column}{0.47\textwidth}
\begin{panel}
\textbf{Классы-аттракторы}
\begin{itemize}
\item Alex и Abigail собирают заметную долю \emph{чужих}
предсказаний: на разных повторах Abigail получает
порядка 10–18\,\% всех ответов при истинной доле
класса $\approx$ 6,9\,\%.
\item Коррекция априорных вероятностей классов
macro-F1 не улучшила и проблему не сняла.
\end{itemize}
\end{panel}
\end{column}
\begin{column}{0.52\textwidth}
\begin{imgbox}
\includegraphics[width=\linewidth, height=0.55\textheight, keepaspectratio]{hub_shares.png}
\end{imgbox}
\end{column}
\end{columns}
\note{Говорит М. В. Савостиков. Проблема редких классов: модель
предсказывает популярные классы слишком часто — Abigail получает десятки
процентов всех ответов при истинной доле около 7\,\%. Простая коррекция
приоров не помогла.}
\slidefooter
\end{frame}
\begin{frame}{Персонажи: кто различается}
\begin{columns}[T]
\begin{column}{0.47\textwidth}
\begin{panel}
\textbf{Лучшие и худшие по F-мере}
\begin{itemize}
\item Лучшие: Evelyn, Willy, Pam, Pierre, Leo.
\item Худшие: Kent, Wizard, Penny, Mister Qi, Leah, Abigail.
\item У редких классов — широкие интервалы оценок.
\item Мягкое голосование улучшает часть редких классов
(Wizard, Dwarf, Vincent), но Kent остаётся F1 = 0.
\end{itemize}
\end{panel}
\end{column}
\begin{column}{0.52\textwidth}
\begin{imgbox}
\includegraphics[width=\linewidth, height=0.6\textheight, keepaspectratio]{per_class_f1.png}
\end{imgbox}
\end{column}
\end{columns}
\note{Говорит М. В. Савостиков. Диаграмма: F-мера по каждому персонажу.
Лучшие — Evelyn, Willy, Pam, Pierre, Leo; худшие — Kent, Wizard, Penny,
Mister Qi, Leah, Abigail. Редкие классы имеют широкие доверительные
интервалы: для 15 реплик разброс оценок велик. Ансамбль прибавляет
отдельным редким классам, но Kent не находит никто.}
\slidefooter
\end{frame}
\begin{frame}{Что означают полученные оценки}
\begin{panel}
\begin{itemize}
\item 0,30 macro-F1 при 34 классах — примерно \textbf{в 10 раз выше
случайного уровня} 0,029: устойчивый сигнал различимости.
\item Значение умеренное и это ожидаемо: реплики по $\sim$19 слов,
дисбаланс классов и общие темы не позволяют «узнать» персонажа
по одной фразе.
\item Умеренность объясняется \emph{условиями задачи}, а не тем, что
автор «не старался»: превышение случайного уровня как раз
говорит о намеренно созданных речевых масках.
\end{itemize}
\end{panel}
\begin{panel}
\begin{itemize}
\item Мы решаем задачу «\emph{какой персонаж} говорит», а не
«\emph{он или не он} писал»: автор один.
\item И о «чистых» идиолектах говорить нельзя: основной вклад дают
лексико-тематический профиль и символьные n-граммы, а после
маскирования содержательных слов качество падает.
\end{itemize}
\end{panel}
\note{Говорит М. В. Савостиков — ключевой слайд по интерпретации. Проговорить
три пункта. (1) 0,304 при случайном 0,029 — это примерно десятикратное
превышение, то есть сигнал реальный. (2) Абсолютная величина умеренная из-за
коротких реплик, дисбаланса и общих тем — это свойства задачи, а не
«автор не пытался»; напротив, различимость масок подтверждает, что автор
сознательно делал разные голоса. (3) Мы определяем персонажа, а не
авторскую принадлежность: автор один. И мы не утверждаем, что выучили
«чистый стиль»: при маскировании содержания качество сильно падает.}
\slidefooter
\end{frame}
\begin{frame}{Ограничения}
\begin{columns}[T]
\begin{column}{0.49\textwidth}
\begin{panel}
\textbf{Данные}
\begin{itemize}
\item Один корпус, одна игра, один автор; внешняя валидация
не проводилась.
\item Короткие реплики и сильный дисбаланс классов.
\item Ручная фильтрация корпуса невоспроизводима.
\item Нет идентификаторов сцен и диалогов.
\end{itemize}
\end{panel}
\end{column}
\begin{column}{0.49\textwidth}
\begin{panel}
\textbf{Методология}
\begin{itemize}
\item Выбор признаков и ансамбля опирался на прошлые серии;
вложенная проверка снижает этот риск, но не устраняет.
\item Основной вклад — содержательные слова и тема.
\item Простая коррекция приоров не помогла редким классам.
\item Объединение через мета-классификатор переобучается на малой выборке.
\end{itemize}
\end{panel}
\end{column}
\end{columns}
\note{Говорит М. В. Савостиков. Ограничения делим на две группы: свойства
данных (один корпус и один автор, дисбаланс, короткие реплики,
невоспроизводимая ручная фильтрация, нет сцен) и свойства метода
(частичный выбор конфигураций по прошлым сериям, тематический сигнал в
признаках, неудачи простых калибровок). Честное перечисление — часть
верификации, а не признание провала.}
\slidefooter
\end{frame}
\begin{frame}{Выводы}
\begin{panel}
\begin{itemize}
\item \textbf{Персонажи статистически различимы:} лучшая модель — 0,304
macro-F1, консервативная оценка — 0,275; случайный уровень — 0,029.
\item \textbf{Основной сигнал — лексико-тематический:} символьные
n-граммы и content-LSA дают основной прирост; функциональные слова
и частеречная LSA сами по себе слабы.
\item \textbf{После замены содержательных слов} качество падает с 0,248
до 0,113: содержание доминирует, но стилевой сигнал сохраняется.
\item \textbf{Главная проблема — редкие классы и классы-аттракторы;}
мягкое голосование помогает, но не решает её полностью.
\item \textbf{Корректная формулировка результата:} различимы речевые
маски по лексико-тематическому профилю; утверждение о «чистом
стиле» требует внешней валидации и тематического контроля.
\end{itemize}
\end{panel}
\note{Говорит М. В. Савостиков. Пять итоговых тезисов. Финальную формулировку
оставить точной: мы показали статистическую различимость речевых масок по
лексико-тематическому профилю; для более сильных утверждений — разные идиолекты,
применимость как инструмент — нужны внешний корпус, тематический контроль
и разметка сцен.}
\slidefooter
\end{frame}
% ==================== ПРИЛОЖЕНИЕ ====================
\begin{frame}{Приложение: полная матрица ошибок 34$\times$34}
\begin{imgbox}
\includegraphics[width=\linewidth, height=0.8\textheight, keepaspectratio]{confusion_full34.png}
\end{imgbox}
\note{Приложение для вопросов и раздаточного материала. Если спросят про
конкретную пару персонажей — искать её в полной матрице: строки сортированы
по размеру класса, цвет кодирует долю строки. Кто говорит: отвечает
М. В. Савостиков (можно подключить В. Н. Копнина по вопросам валидации).}
\slidefooter
\end{frame}
\begin{frame}{Спасибо за внимание}
\begin{imgbox}
\includegraphics[width=\linewidth, height=0.8\textheight, keepaspectratio]{meme.png}
\end{imgbox}
\begin{tikzpicture}[remember picture, overlay]
\node[anchor=south east, xshift=-2.5mm, yshift=1.5mm]
at (current page.south east)
{\includegraphics[height=0.65cm]{chicken.png}};
\end{tikzpicture}
\note{Финальный слайд; мем как пасхалка. Говорят по очереди все:
А. А. Аладьина благодарит и передаёт слово для вопросов; по вопросам
корпуса отвечает Аладьина, по методологии — Копнин, по результатам —
Савостиков.}
\slidefooter
\end{frame}
\end{document}

View File

@@ -1,263 +0,0 @@
"""Графики для презентации: матрицы ошибок, сравнение моделей, ошибки по классам."""
from __future__ import annotations
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from matplotlib.colors import LinearSegmentedColormap
ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "presentation" / "generated"
OUT.mkdir(parents=True, exist_ok=True)
OOF = ROOT / "results" / "final" / "finalists" / "voting_et_char_lean" / "oof_predictions.csv"
METRICS = ROOT / "FINAL_RESULTS.csv"
plt.rcParams.update({
"font.family": "DejaVu Sans",
"font.size": 13,
"axes.titlesize": 18,
"axes.labelsize": 13,
"savefig.facecolor": "#F8F1DC",
"figure.facecolor": "#F8F1DC",
})
CREAM = "#F8F1DC"
DARK = "#203A2B"
GREEN = "#4D7C4A"
BROWN = "#8B5A2B"
RED = "#B23A2E"
GOLD = "#D8A23A"
CMAP = LinearSegmentedColormap.from_list("stardew_blue", ["#FFFDF4", "#BFD9C0", "#2E6B57", "#173A34"])
def load_oof() -> pd.DataFrame:
frame = pd.read_csv(OOF)
frame["y_true"] = frame["y_true"].astype(str)
frame["y_pred"] = frame["y_pred"].astype(str)
return frame
def top_confusions(frame: pd.DataFrame, n: int = 18) -> pd.Series:
errors = frame[frame["y_true"] != frame["y_pred"]]
pairs = errors.groupby(["y_true", "y_pred"]).size().sort_values(ascending=False).head(n)
pairs.index = [f"{a} → {b}" for a, b in pairs.index]
return pairs
def confusion_top12(frame: pd.DataFrame) -> None:
labels = frame["y_true"].value_counts().head(12).index.tolist()
matrix = pd.crosstab(frame["y_true"], frame["y_pred"]).reindex(index=labels, fill_value=0)
shown = matrix[[c for c in labels if c in matrix.columns]].copy()
shown["прочие"] = matrix.drop(columns=[c for c in labels if c in matrix.columns]).sum(axis=1)
shown = shown.div(shown.sum(axis=1), axis=0) * 100
fig, ax = plt.subplots(figsize=(12.5, 7.4))
image = ax.imshow(shown.to_numpy(), cmap=CMAP, vmin=0, vmax=60, aspect="auto")
ax.set_xticks(np.arange(shown.shape[1]))
ax.set_yticks(np.arange(shown.shape[0]))
ax.set_xticklabels(shown.columns, rotation=42, ha="right", fontsize=10)
ax.set_yticklabels(shown.index, fontsize=11)
ax.set_xlabel("Предсказанный персонаж", fontsize=14, color=DARK)
ax.set_ylabel("Истинный персонаж", fontsize=14, color=DARK)
ax.set_title("Матрица ошибок: 12 самых частых персонажей, % строки", fontsize=18, color=DARK, pad=12)
for row in range(shown.shape[0]):
for col in range(shown.shape[1]):
value = shown.iloc[row, col]
if value >= 0.5:
color = "white" if value > 32 else DARK
ax.text(col, row, f"{value:.0f}", ha="center", va="center", fontsize=9, color=color)
cbar = fig.colorbar(image, ax=ax, fraction=0.035, pad=0.02)
cbar.set_label("% строки", color=DARK)
fig.tight_layout()
fig.savefig(OUT / "confusion_top12.png", dpi=220, bbox_inches="tight")
plt.close(fig)
def confusion_full(frame: pd.DataFrame) -> None:
labels = sorted(frame["y_true"].unique())
matrix = pd.crosstab(frame["y_true"], frame["y_pred"]).reindex(index=labels, columns=labels, fill_value=0)
matrix = matrix.div(matrix.sum(axis=1), axis=0) * 100
fig, ax = plt.subplots(figsize=(13.5, 11.5))
image = ax.imshow(matrix.to_numpy(), cmap=CMAP, vmin=0, vmax=35, aspect="auto")
ax.set_xticks(np.arange(len(labels)))
ax.set_yticks(np.arange(len(labels)))
ax.set_xticklabels(labels, rotation=90, fontsize=5.5)
ax.set_yticklabels(labels, fontsize=5.5)
ax.set_xlabel("Предсказанный персонаж", fontsize=12, color=DARK)
ax.set_ylabel("Истинный персонаж", fontsize=12, color=DARK)
ax.set_title("Полная матрица ошибок, % строки (раздаточный материал)", fontsize=16, color=DARK)
fig.colorbar(image, ax=ax, fraction=0.03, pad=0.015).set_label("% строки")
fig.tight_layout()
fig.savefig(OUT / "confusion_full34.png", dpi=220, bbox_inches="tight")
plt.close(fig)
def pairs_chart(frame: pd.DataFrame) -> None:
pairs = top_confusions(frame, 18).sort_values()
fig, ax = plt.subplots(figsize=(12.6, 7.2))
colors = [RED if i >= len(pairs) - 5 else BROWN for i in range(len(pairs))]
ax.barh(pairs.index, pairs.values, color=colors, edgecolor="white", linewidth=0.6)
for index, value in enumerate(pairs.values):
ax.text(value + 0.6, index, str(value), va="center", fontsize=10, color=DARK)
ax.set_xlabel("Число ошибок за 3 повтора", fontsize=13, color=DARK)
ax.set_title("Самые частые пары «истинный → предсказанный»", fontsize=18, color=DARK)
ax.tick_params(axis="y", labelsize=10)
ax.grid(axis="x", alpha=0.2, color=DARK)
ax.set_xlim(0, pairs.max() * 1.16)
fig.tight_layout()
fig.savefig(OUT / "confusion_pairs.png", dpi=220, bbox_inches="tight")
plt.close(fig)
def per_class_chart(frame: pd.DataFrame) -> None:
first = frame[frame["repeat"] == frame["repeat"].min()]
labels = sorted(first["y_true"].unique())
rows = []
for label in labels:
mask = first["y_true"] == label
tp = int(((first["y_true"] == label) & (first["y_pred"] == label)).sum())
fp = int(((first["y_true"] != label) & (first["y_pred"] == label)).sum())
fn = int(((first["y_true"] == label) & (first["y_pred"] != label)).sum())
precision = tp / (tp + fp) if tp + fp else 0.0
recall = tp / (tp + fn) if tp + fn else 0.0
f1 = 2 * precision * recall / (precision + recall) if precision + recall else 0.0
rows.append((label, f1, int(mask.sum())))
table = pd.DataFrame(rows, columns=["class", "f1", "support"]).sort_values("f1")
worst = table.head(12).sort_values("f1", ascending=False)
best = table.tail(12).sort_values("f1")
fig, axes = plt.subplots(1, 2, figsize=(13.2, 7.0), gridspec_kw={"width_ratios": [1, 1]})
axes[0].barh(worst["class"], worst["f1"], color=RED, edgecolor="white")
axes[0].set_title("Худшие 12 персонажей", fontsize=15, color=DARK)
axes[0].set_xlim(0, 0.72)
axes[0].grid(axis="x", alpha=0.2)
for index, (value, support) in enumerate(zip(worst["f1"], worst["support"])):
axes[0].text(value + 0.008, index, f"{value:.2f} (n={support})", va="center", fontsize=9, color=DARK)
axes[1].barh(best["class"], best["f1"], color=GREEN, edgecolor="white")
axes[1].set_title("Лучшие 12 персонажей", fontsize=15, color=DARK)
axes[1].set_xlim(0, 0.72)
axes[1].grid(axis="x", alpha=0.2)
for index, (value, support) in enumerate(zip(best["f1"], best["support"])):
axes[1].text(value + 0.008, index, f"{value:.2f} (n={support})", va="center", fontsize=9, color=DARK)
fig.suptitle("F-мера по персонажам: мягкое голосование, первый повтор", fontsize=17, color=DARK, y=0.99)
fig.tight_layout()
fig.savefig(OUT / "per_class_f1.png", dpi=220, bbox_inches="tight")
plt.close(fig)
def model_comparison() -> None:
table = pd.read_csv(METRICS)
wanted = [
("voting_et_char_lean", "Мягкое голосование"),
("et_char10000", "ExtraTrees, char=10000"),
("nested_feature_selection", "Вложенный выбор"),
("et_full", "ExtraTrees, полный"),
("logreg_lean_content300", "Logreg, lean + content"),
("stacking", "Стековое объединение"),
]
rows = []
for key, label in wanted:
row = table[table["label"] == key]
if not row.empty:
rows.append((label, float(row.iloc[0]["macro_f1_mean"]), float(row.iloc[0]["macro_f1_std"])))
rows.append(("Случайный уровень", 0.029, 0.0))
frame = pd.DataFrame(rows, columns=["label", "mean", "std"]).sort_values("mean")
fig, ax = plt.subplots(figsize=(12.4, 6.8))
colors = [GOLD if label == "Мягкое голосование" else (RED if label == "Случайный уровень" else GREEN) for label in frame["label"]]
ax.barh(frame["label"], frame["mean"], xerr=frame["std"], color=colors, edgecolor="white", capsize=4)
for index, (value, std) in enumerate(zip(frame["mean"], frame["std"])):
offset = value + max(std, 0.0) + 0.008
ax.text(offset, index, f"{value:.3f}", va="center", fontsize=11, color=DARK)
ax.set_xlim(0, 0.38)
ax.set_xlabel("Макро-усреднённая F-мера", fontsize=13, color=DARK)
ax.set_title("Сравнение моделей: среднее и стандартное отклонение", fontsize=18, color=DARK)
ax.grid(axis="x", alpha=0.2)
fig.tight_layout()
fig.savefig(OUT / "model_comparison.png", dpi=220, bbox_inches="tight")
plt.close(fig)
def feature_progression() -> None:
labels = [
"Только длина",
"Ручные признаки",
"Ручные + char/POS",
"+ content-LSA",
"ExtraTrees, char=10000",
"Мягкое голосование",
]
values = [0.035, 0.105, 0.182, 0.249, 0.276, 0.304]
fig, ax = plt.subplots(figsize=(12.4, 6.6))
colors = [BROWN, BROWN, GREEN, GREEN, GOLD, RED]
ax.bar(labels, values, color=colors, edgecolor="white")
for index, value in enumerate(values):
ax.text(index, value + 0.008, f"{value:.3f}", ha="center", fontsize=12, color=DARK)
ax.set_ylim(0, 0.35)
ax.set_ylabel("Макро-усреднённая F-мера", fontsize=13, color=DARK)
ax.set_title("Что добавляет качество", fontsize=18, color=DARK)
ax.tick_params(axis="x", rotation=18, labelsize=11)
ax.grid(axis="y", alpha=0.2)
fig.tight_layout()
fig.savefig(OUT / "feature_progression.png", dpi=220, bbox_inches="tight")
plt.close(fig)
def topic_control() -> None:
labels = ["Исходный\nExtraTrees", "Маскированный\nExtraTrees", "Исходный\nLogreg", "Маскированный\nLogreg"]
values = [0.2483, 0.1129, 0.1467, 0.1067]
fig, ax = plt.subplots(figsize=(10.8, 6.2))
colors = [GREEN, RED, GREEN, RED]
ax.bar(labels, values, color=colors, edgecolor="white")
for index, value in enumerate(values):
ax.text(index, value + 0.007, f"{value:.3f}", ha="center", fontsize=12, color=DARK)
ax.set_ylim(0, 0.30)
ax.set_ylabel("Макро-усреднённая F-мера", fontsize=13, color=DARK)
ax.set_title("Контроль содержания: замена содержательных слов на CONTENT", fontsize=17, color=DARK)
ax.tick_params(axis="x", labelsize=11)
ax.grid(axis="y", alpha=0.2)
fig.tight_layout()
fig.savefig(OUT / "topic_control.png", dpi=220, bbox_inches="tight")
plt.close(fig)
def hub_shares(frame: pd.DataFrame) -> None:
true = frame["y_true"].value_counts(normalize=True)
pred = frame["y_pred"].value_counts(normalize=True)
labels = pred.head(10).index.tolist()
x = np.arange(len(labels))
fig, ax = plt.subplots(figsize=(12.4, 6.2))
ax.bar(x - 0.2, [true.get(label, 0) * 100 for label in labels], width=0.4, label="Истинная доля", color=GREEN)
ax.bar(x + 0.2, [pred.get(label, 0) * 100 for label in labels], width=0.4, label="Доля предсказаний", color=RED)
ax.set_xticks(x)
ax.set_xticklabels(labels, rotation=25, ha="right", fontsize=11)
ax.set_ylabel("% реплик", fontsize=13, color=DARK)
ax.set_title("Классы-аттракторы: кого модель выбирает слишком часто", fontsize=17, color=DARK)
ax.legend(frameon=False)
ax.grid(axis="y", alpha=0.2)
fig.tight_layout()
fig.savefig(OUT / "hub_shares.png", dpi=220, bbox_inches="tight")
plt.close(fig)
def main() -> int:
frame = load_oof()
confusion_top12(frame)
confusion_full(frame)
pairs_chart(frame)
per_class_chart(frame)
model_comparison()
feature_progression()
topic_control()
hub_shares(frame)
print("Готово:", OUT)
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -1,3 +0,0 @@
python-pptx>=1.0,<2
Pillow>=10
matplotlib>=3.8

View File

@@ -1,251 +0,0 @@
"""Переработка первого блока текущей презентации.
Скрипт не пересобирает весь файл: он читает уже отредактированный PPTX,
удаляет старые слайды первого блока и вставляет новую академическую
последовательность: задача -> строгая постановка -> корпус -> очистка ->
характеристики.
"""
from __future__ import annotations
import sys
from pathlib import Path
from pptx import Presentation
from pptx.enum.text import PP_ALIGN
from pptx.util import Inches
HERE = Path(__file__).resolve().parent
if str(HERE) not in sys.path:
sys.path.insert(0, str(HERE))
from build_deck import ( # noqa: E402
DARK,
GEN,
GREEN_DARK,
RED,
add_background,
add_bullets,
add_chicken,
add_footer,
add_image_fit,
add_notes,
add_panel,
add_table,
add_text,
add_title_bar,
)
PPTX = HERE / "stardew_stylometry.pptx"
BLOCK = "Блок 1 · Корпус"
NARRATOR = "А. А. Аладьина"
def delete_slide(prs, index: int) -> None:
slide_id = prs.slides._sldIdLst[index]
rel_id = slide_id.rId
prs.part.drop_rel(rel_id)
del prs.slides._sldIdLst[index]
def move_slide(prs, old_index: int, new_index: int) -> None:
slide_id = prs.slides._sldIdLst[old_index]
prs.slides._sldIdLst.remove(slide_id)
prs.slides._sldIdLst.insert(new_index, slide_id)
def base_slide(prs, title: str, subtitle: str | None = None):
slide = prs.slides.add_slide(prs.slide_layouts[6])
add_background(slide)
add_title_bar(slide, title, NARRATOR, subtitle)
add_footer(slide, BLOCK, 0)
add_chicken(slide)
return slide
def slide_task(prs):
slide = base_slide(prs, "Решаемая задача")
add_panel(slide, 0.45, 1.35, 6.15, 5.55)
add_bullets(slide, 0.65, 1.55, 5.75, 5.1, [
"Дано: 2005 коротких реплик, 34 персонажа, один автор.",
"Требуется: по одной реплике определить персонажа.",
"Класс — имя персонажа; один объект относится ровно к одному классу.",
"Реплики короткие: в среднем 19 слов, медиана 17.",
"Классы неравномерны: от 15 до 139 реплик.",
], size=16, title="Постановка")
add_panel(slide, 6.8, 1.35, 6.05, 5.55)
add_bullets(slide, 7.0, 1.55, 5.65, 5.1, [
"Проверяется, различимы ли речевые маски персонажей статистически.",
"Это не авторская атрибуция: все реплики написаны одним автором.",
"Ошибки между персонажами ожидаемы: общий автор, общие темы и контексты.",
"Практическая ценность — методика стилометрии для коротких текстов.",
"Ограничение: один корпус, одна игра, один автор.",
], size=16, title="Что именно проверяется")
add_notes(slide, "Рассказчик 1: Аладьина Анна Андреевна. Сформулировать задачу без рекламных формулировок: есть корпус коротких реплик, нужно предсказать персонажа. Сразу отделить это от авторской атрибуции: автор один, задача — различимость речевых масок.")
def slide_formal(prs):
slide = base_slide(prs, "Строгая постановка задачи", "Формальная запись и протокол оценки")
add_panel(slide, 0.45, 1.35, 7.55, 5.55)
add_bullets(slide, 0.65, 1.55, 7.15, 5.1, [
"D = {(x_i, y_i)}, i = 1..n; n = 2005.",
"x_i — одна реплика (короткий текст).",
"y_i ∈ Y, |Y| = 34 — имя персонажа.",
"φ(x_i) ∈ R^d — вектор признаков реплики.",
"f_θ: R^d → R^34 — классификатор с параметрами θ.",
"ŷ_i = argmax_{c ∈ Y} f_θ(φ(x_i))_c — предсказание.",
], size=16, title="Дано")
add_panel(slide, 8.2, 1.35, 4.65, 5.55)
add_bullets(slide, 8.38, 1.55, 4.3, 5.1, [
"Обучение: минимизация перекрёстной энтропии + L2-регуляризация.",
"Веса классов: balanced, чтобы редкие персонажи не игнорировались.",
"Оценка: macro-F1 = (1/|Y|) Σ_c 2P_cR_c / (P_c + R_c).",
"Схема: стратифицированная 5-блочная CV, повторы, вложенная настройка.",
"Случайный уровень: 0,029; top-3: 0,088.",
"Один объект — одна реплика; обучение и оценка разделены.",
], size=14, title="Критерий и проверка")
add_notes(slide, "Рассказчик 1. Это ключевой слайд строгой постановки. Проговорить обозначения: объект — реплика, метка — персонаж, признаковое описание — φ(x), классификатор — f. Отдельно сказать, что оцениваем не accuracy, а macro-F1, потому что классов 34 и они неравномерны.")
def slide_corpus(prs):
slide = base_slide(prs, "Корпус: сбор и разметка")
add_panel(slide, 0.45, 1.35, 7.05, 5.55)
add_text(slide, 0.68, 1.55, 6.6, 0.3, "1. Сбор", size=17, bold=True, color=GREEN_DARK)
add_bullets(slide, 0.68, 1.9, 6.55, 1.35, [
"Реплики извлечены из игровых диалогов Stardew Valley.",
"Единица анализа — одна реплика; всего 2005 объектов.",
"Корпус охватывает 34 персонажа; все тексты на английском языке.",
], size=14)
add_text(slide, 0.68, 3.2, 6.6, 0.3, "2. Разметка", size=17, bold=True, color=GREEN_DARK)
add_bullets(slide, 0.68, 3.55, 6.55, 1.35, [
"Метка — имя говорящего персонажа; один объект — один класс.",
"Ручная проверка соответствия реплики и персонажа.",
"Метаданные (пол, возраст, вид) хранятся отдельно и не подаются модели.",
], size=14)
add_text(slide, 0.68, 4.85, 6.6, 0.3, "3. Ограничения", size=17, bold=True, color=GREEN_DARK)
add_bullets(slide, 0.68, 5.18, 6.55, 1.4, [
"Нет идентификаторов сцен и диалогов.",
"Классы неравномерны: 15–139 реплик.",
"Часть коротких и общих фраз удалена вручную; правила не сохранились.",
], size=14)
add_panel(slide, 7.7, 1.35, 5.15, 5.55)
add_image_fit(slide, GEN / "characters.png", 7.85, 1.5, 4.85, 3.9, panel=False)
add_text(slide, 7.9, 5.55, 4.75, 1.1, "34 персонажа: подростки, взрослые, пожилые, дети и нелюди. Все реплики написаны одним разработчиком.", size=13, color=DARK, align=PP_ALIGN.CENTER)
add_notes(slide, "Рассказчик 1. Описать три шага: сбор, разметка, ограничения. Не утверждать, что корпус идеален: ручная фильтрация и отсутствие сцен — существенные ограничения. Метаданные не являются признаками.")
def slide_cleaning(prs):
slide = base_slide(prs, "Очистка и шум")
add_panel(slide, 0.45, 1.35, 6.1, 5.55)
add_bullets(slide, 0.65, 1.55, 5.7, 5.1, [
"Нормализация Unicode: многоточия, кавычки, тире, неразрывные пробелы.",
"Плейсхолдеры <PLAYER>, <FARM> и другие заменены нейтральными словами.",
"Количество плейсхолдеров сохранено отдельным признаком.",
"Токенизация, предложения и POS-разметка — NLTK; Universal POS.",
"Пунктуация, регистр и стоп-слова сохранены: это стилометрический сигнал.",
], size=15, title="Что сделали")
add_panel(slide, 6.75, 1.35, 6.1, 5.55)
add_bullets(slide, 6.95, 1.55, 5.7, 5.1, [
"Шум: дубликаты, технические артефакты, общие короткие формулы.",
"Ручная фильтрация удалила короткие и общие фразы; правила не сохранены.",
"В текущем CSV 0 точных дублей; 13 пар с косинусной близостью ≥ 0,8 — внутри класса.",
"Нет сцен и диалогов: нельзя учесть корреляцию реплик внутри сцены.",
"Разметка только по имени; ручной стилистической разметки нет.",
], size=15, title="Что считаем шумом")
add_notes(slide, "Рассказчик 1. Объяснить, что очистка не должна удалять стиль: пунктуация, регистр и служебные слова сохранены. Ручная фильтрация — ограничение; почти-дубликаты не пересекают классы, поэтому утечки через них нет. Отметить отсутствие сцен.")
def slide_class_stats(prs):
slide = base_slide(prs, "Характеристики корпуса: классы", "Распределение реплик по персонажам")
add_panel(slide, 0.4, 1.35, 7.2, 5.55)
add_image_fit(slide, GEN.parent / "images" / "replics-count.png", 0.5, 1.45, 7.0, 5.35)
add_panel(slide, 7.8, 1.35, 5.1, 5.55)
rows = [
["Показатель", "Значение"],
["Реплик", "2005"],
["Персонажей", "34"],
["Максимум", "Abigail, 139"],
["Минимум", "Mister Qi, 15"],
["Разброс классов", "15–139"],
["Случайный уровень", "2,9%"],
]
add_table(slide, 8.0, 1.55, 4.7, 3.2, rows, col_widths=[2.4, 2.3], font_size=12)
add_bullets(slide, 8.0, 4.9, 4.7, 1.8, [
"Дисбаланс почти десятикратный.",
"Поэтому основная метрика — macro-F1, а не доля правильных ответов.",
], size=13)
add_notes(slide, "Рассказчик 1. Показать дисбаланс: максимум Abigail 139, минимум Mister Qi 15. При 34 классах случайное угадывание — 2,9%. Отсюда macro-F1 и веса классов; accuracy может обманывать.")
def slide_length_stats(prs):
slide = base_slide(prs, "Характеристики корпуса: длина реплик", "Медианная длина предложения в символах")
add_panel(slide, 0.4, 1.35, 7.2, 5.55)
add_image_fit(slide, GEN.parent / "images" / "median.png", 0.5, 1.45, 7.0, 5.35)
add_panel(slide, 7.8, 1.35, 5.1, 5.55)
rows = [
["Показатель", "Значение"],
["Средняя длина", "19,1 слова"],
["Медиана", "17 слов"],
["Диапазон", "3–78 слов"],
["Медиана в символах", "52–114"],
]
add_table(slide, 8.0, 1.55, 4.7, 2.5, rows, col_widths=[2.4, 2.3], font_size=12)
add_bullets(slide, 8.0, 4.25, 4.7, 2.35, [
"Короткие реплики дают мало стилометрических свидетельств.",
"Длина — сильный признак, но может стать прокси персонажа.",
"Её вклад проверяется отдельно и по группам длины.",
], size=13)
add_notes(slide, "Рассказчик 1. Числа у подписей — медианная длина предложения в символах. Контраст Vincent/Jas и Sandy/Wizard. Длина — законный, но опасный признак: она может отражать не стиль, а привычную длину реплик персонажа.")
def renumber(prs) -> None:
for index, slide in enumerate(prs.slides, start=1):
for shape in slide.shapes:
if not shape.has_text_frame:
continue
text = shape.text_frame.text.strip()
if text.isdigit() and shape.top > Inches(6.9) and shape.width < Inches(0.8):
shape.text_frame.text = str(index)
def replace_narrators(prs) -> None:
mapping = {
"Рассказчик 2": "В. Н. Копнин",
"Рассказчик 3": "М. В. Савостиков",
}
for slide in prs.slides:
for shape in slide.shapes:
if shape.has_text_frame and shape.text_frame.text.strip() in mapping:
shape.text_frame.text = mapping[shape.text_frame.text.strip()]
def main() -> int:
prs = Presentation(PPTX)
# Сначала создаём новые слайды в конце, чтобы их части получили
# уникальные имена и не конфликтовали с удаляемыми слайдами.
base = len(prs.slides)
slide_task(prs)
slide_formal(prs)
slide_corpus(prs)
slide_cleaning(prs)
slide_class_stats(prs)
slide_length_stats(prs)
# Переносим новые слайды сразу после титульного.
for offset in range(6):
move_slide(prs, base + offset, 1 + offset)
# Теперь старые слайды первого блока находятся на позициях 7–12.
for _ in range(6):
delete_slide(prs, 7)
replace_narrators(prs)
renumber(prs)
prs.save(PPTX)
print(f"Готово: {PPTX}; слайдов: {len(prs.slides)}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

Binary file not shown.

View File

@@ -1,652 +0,0 @@
---
title: "Отчёт по лабораторной работе №1"
subtitle: "Классификация текстов на основе стилометрических характеристик"
author: "А. А. Аладьина, В. Н. Копнин, М. В. Савостиков"
date: "01.11.2026"
lang: ru-RU
---
# Аннотация
Работа посвящена стилометрической классификации реплик персонажей игры
Stardew Valley. Корпус содержит 2005 реплик, принадлежащих 34 персонажам;
все реплики написаны одним автором. Проверяется, различимы ли речевые маски
персонажей по отдельной реплике, если исключить авторскую атрибуцию как
таковую.
Построены 454 ручных стилометрических признака, символьные и частеречные
n-граммы, а также несколько вариантов латентно-семантического анализа.
Сравнивались логистическая регрессия, линейный метод опорных векторов,
наивный байесовский классификатор, случайный лес, сверхслучайные деревья,
метод ближайших соседей и дельта Барроуза. Использована повторная
стратифицированная перекрёстная проверка, вложенная настройка параметров и
отдельная отложенная выборка.
Лучший наблюдаемый результат — мягкое голосование трёх моделей: macro-F1
около **0,304** при перекрёстной проверке и **0,333** на отложенной выборке.
Консервативная оценка при вложенном выборе конфигурации — **0,275**.
Случайный уровень для 34 классов — **0,029**. Основной вклад дают
лексико-тематические признаки и символьные n-граммы; после замены
содержательных слов нейтральным маркером качество падает с 0,248 до 0,113.
Следовательно, результат корректно интерпретировать как различимость речевых
масок по лексико-тематическому профилю, а не как доказательство «чистого»
идиолекта.
# 1. Постановка задачи
## 1.1. Направление исследования
Направление работы — классификация текстов по стилометрическим
характеристикам. В отличие от классической авторской атрибуции, все тексты
корпуса принадлежат одному автору. Исследуется другой вопрос:
> различаются ли речевые маски 34 персонажей по стилометрическим признакам
> отдельных реплик, если автор у них общий?
Такая постановка позволяет проверить, насколько автор намеренно создаёт
разные голоса персонажей, и какие признаки несут этот сигнал.
## 1.2. Гипотезы
| Обозначение | Гипотеза |
|---|---|
| H1 | Функциональные слова, части речи и символьные n-граммы различают персонажей выше случайного уровня |
| H2 | На коротких репликах частеречные и символьные признаки устойчивее словарного богатства и читаемости |
| H3 | LSA по всем словам отражает тему, а LSA по частям речи и функциональным словам — стиль |
| H4 | Качество зависит от длины реплики |
| H5 | Ошибки группируются вокруг персонажей со сходным возрастом, полом, ролью или видом |
## 1.3. Формальная постановка
Пусть \(D=\{(x_i,y_i)\}_{i=1}^{n}\) — корпус, где \(n=2005\),
\(x_i\) — одна реплика, \(y_i\in Y\) — имя персонажа,
\(|Y|=34\). Каждый объект относится ровно к одному классу.
Требуется построить классификатор
\[
f\colon \Phi(x_i)\to Y,
\]
где \(\Phi(x_i)\) — вектор признаков реплики. Качество оценивается
макро-усреднённой F-мерой:
\[
\operatorname{macroF1}=\frac{1}{|Y|}\sum_{c\in Y}
\frac{2P_cR_c}{P_c+R_c}.
\]
Макро-усреднение выбрано потому, что все 34 персонажа важны одинаково,
а классы неравномерны.
# 2. Корпус
## 2.1. Источник и единица анализа
Корпус собран из диалогов игры Stardew Valley. Единица анализа — одна
реплика персонажа; тексты на английском языке. Метка — имя говорящего,
определённое по игровым диалогам и проверенное вручную.
Метаданные о поле, возрасте, виде и роли персонажа хранятся отдельно и
используются только для анализа ошибок. В признаки классификатора они не
подаются, чтобы не подменять стилометрическую задачу демографической.
## 2.2. Сбор и разметка
Правила формирования корпуса:
1. Из игровых диалогов извлекаются реплики персонажей.
2. Каждая реплика становится отдельным объектом.
3. Метка — имя говорящего; один объект относится ровно к одному классу.
4. Проводится ручная проверка соответствия реплики и метки.
5. Метаданные персонажей ведутся отдельной таблицей.
6. Служебные метки игрового движка (`<PLAYER>`, `<FARM>`, `<PET>`,
`<SPOUSE>`, `<TIME>`, `<NOUN>`, `<NAME>`) заменяются нейтральными словами;
их количество сохраняется отдельным признаком.
Корпус хранится в CSV с разделителем `;` в кодировке cp1251; обязательные
колонки — «Реплика» и «Персонаж». Метаданные персонажей ведутся отдельным
XLSX-файлом. Такое разделение позволяет менять признаки, не затрагивая
разметку, и проверять корпус независимо от моделей.
## 2.3. Очистка и шум
Выполнялась минимальная нормализация:
- Unicode-очистка: разные варианты кавычек, тире, многоточий и пробелов
приводятся к единому виду;
- замена плейсхолдеров нейтральными словами;
- токенизация и разбиение на предложения средствами NLTK;
- частеречная разметка Penn Treebank с переводом в Universal POS.
Не удалялись:
- пунктуация;
- регистр;
- стоп-слова;
- короткие реплики, оставшиеся после ручной фильтрации.
Пунктуация, регистр и служебные слова рассматриваются как стилометрический
сигнал, а не как шум.
Шумом считались:
- точные дубликаты;
- технические артефакты;
- общие короткие формулы;
- плейсхолдеры игрового движка.
Корпус прошёл ручную фильтрацию: короткие и общие фразы удалены. Точные
правила этой фильтрации не сохранились, поэтому исходное распределение
восстановить нельзя. Это ограничение зафиксировано в выводах.
## 2.4. Характеристики корпуса
| Показатель | Значение |
|---|---:|
| Реплик | 2005 |
| Персонажей | 34 |
| Словоупотреблений | 38 207 |
| Средняя длина реплики | 19,1 слова |
| Медиана длины | 17 слов |
| Минимум / максимум | 3 / 78 слов |
| Классы | 15–139 реплик |
| Точные дубли реплик | 0 |
| Почти-дубли (косинусная близость ≥ 0,8) | 13 пар, все внутри одного класса |
| Реплик с плейсхолдерами | 172 |
![Распределение числа реплик по персонажам](figures/replics-count.png){width=70%}
![Медианная длина предложения в символах по персонажам](figures/median.png){width=82%}
![Персонажи корпуса](figures/characters.png){width=92%}
Классы неравномерны: максимум — Abigail (139 реплик), минимум — Mister Qi
(15 реплик). Разница почти десятикратная. Поэтому доля правильных ответов
может вводить в заблуждение: модель, всегда выбирающая частый класс, получает
заметную accuracy, но низкую macro-F1.
## 2.5. Соответствие формальным требованиям
Задание предусматривает не менее 90 текстов объёмом не менее 30 предложений
и примерно равные классы. В данном корпусе единица анализа — реплика; она
короче формального «текста» и содержит в среднем 19 слов. По согласованию с
преподавателем эксперимент выполнен на уровне реплик: это позволяет
использовать все 2005 объектов и корректно поставить задачу для коротких
игровых реплик. Классы не равны по объёму; для компенсации использованы
веса классов и macro-F1.
Это ограничение важно учитывать при сравнении с работами, где документы
содержат десятки предложений.
# 3. Стилометрические признаки
## 3.1. Ручные признаки: 454 признака в 11 группах
| Группа | Примеры |
|---|---|
| Длина | число слов, символов, предложений; длина предложения |
| Лексика | длины слов, апострофы, заглавные буквы, цифры |
| Словарное богатство | TTR (доля уникальных слов), Guiraud, Herdan, Yule's K, Simpson's D, доля гапаксов и дислегомен |
| Символы | частоты букв, регистр, цифры, пунктуация |
| Пунктуация | запятые, точки, многоточия, вопросы, восклицания, повторы |
| Функциональные слова | артикли, предлоги, союзы, местоимения, вспомогательные глаголы |
| Местоимения | 1-е, 2-е, 3-е лицо; отношение «я/ты» |
| Части речи | Universal POS, Penn Treebank, время, модальность, герундий |
| Синтаксис | клаузы (элементарные предложения), подчинительные союзы, пассивные конструкции, вопросы |
| Читаемость | Flesch, Flesch—Kincaid, Gunning Fog, SMOG, Coleman—Liau, ARI |
| Разговорные маркеры | междометия, смех, осторожные формулировки (хеджи), интенсификаторы, вежливость, сокращения |
Словарное богатство и читаемость помечены как предварительные: на репликах
длиной 10–20 слов они нестабильны. Основные группы для коротких текстов —
функциональные слова, части речи, пунктуация, символьные n-граммы и
разговорные маркеры.
### Обоснование выбора
- Функциональные слова — классическое ядро стилометрии: закрытый класс мало
зависит от темы.
- Части речи и синтаксические прокси отражают структуру фразы.
- Символьные n-граммы устойчивы к теме и фиксируют морфологию, пунктуацию и
орфографию.
- Разговорные маркеры важны для игровых реплик: междометия, многоточия,
смех и вежливость различают персонажей.
- LSA сжимает разреженное пространство и позволяет сравнить тематический и
стилистический вклады.
## 3.2. Символьные и частеречные n-граммы
- Символьные n-граммы: TF-IDF по символьным n-граммам в границах слов
(`char_wb`), диапазон 2–5, `min_df=3`, до 5000–10 000 признаков,
L2-нормализация.
- Частеречные n-граммы: TF-IDF по последовательностям Universal POS,
диапазон 1–3, до 500 признаков.
TF-IDF — вес признака, который растёт с частотой внутри реплики и падает с
частотой в корпусе: частые общие n-граммы получают меньший вес, редкие
характерные — больший.
Символьные n-граммы фиксируют морфологию и оформление; n-граммы частей речи —
синтаксические шаблоны. Оба блока устойчивее к теме, чем словарные n-граммы.
## 3.3. Латентно-семантический анализ
LSA реализован как:
```text
TF-IDF -> усечённое сингулярное разложение (TruncatedSVD) -> L2-нормализация
```
Варианты:
| Вариант | Что отражает |
|---|---|
| `word` | лексику и тему |
| `word2` | лексику и устойчивые сочетания |
| `content` | содержание после удаления стоп-слов |
| `fw` | функциональные слова и синтаксис |
| `char` | морфологию и пунктуацию |
| `pos` | частеречные последовательности |
Число компонент подбиралось в диапазоне 50–400 по объяснённой дисперсии и
внешней метрике. Для `content`-LSA качество росло до 400 компонент.
Ключевое ограничение: LSA по содержательным словам может отражать тему, а не
только стиль. Поэтому варианты сравнивались между собой, а не выбирались
заранее.
# 4. Классификаторы
В работе использовано шесть семейств ML-классификаторов и классическая дельта
Барроуза — всего семь подходов; требование «не менее трёх» выполнено с
запасом.
## 4.1. Логистическая регрессия
Линейная модель: для каждого класса вычисляется оценка
\(s_c(x)=w_c^\top x+b_c\), затем вероятности через softmax. Обучение
минимизирует перекрёстную энтропию с L2-регуляризацией. Параметр \(C\)
управляет силой регуляризации; `class_weight="balanced"` повышает вес редких
классов.
Сильные стороны: устойчивость при большом числе признаков, вероятности,
интерпретируемые коэффициенты. Ограничение: только линейные границы.
## 4.2. Линейный метод опорных векторов
Ищет линейную границу с максимальным зазором; параметр \(C\) штрафует
нарушения зазора. Не выдаёт вероятностей: для тройки лучших используется
решающая функция. Силён на текстовых признаках, но требует настройки \(C\).
## 4.3. Случайный лес
Ансамбль решающих деревьев: каждое дерево обучается на подвыборке,
разбиения используют случайное подмножество признаков. Класс определяется
голосованием. Лес ловит нелинейные взаимодействия, но склонен к
переобучению на 2005 объектах; ограничивались глубина и размер листьев.
## 4.4. Сверхслучайные деревья (ExtraTrees)
Похожи на случайный лес, но пороги разбиения выбираются случайно. Это
уменьшает дисперсию и ускоряет обучение. В работе ExtraTrees с расширенным
символьным словарём показали лучший результат среди одиночных моделей.
## 4.5. Наивный байесовский классификатор
MultinomialNB использует теорему Байеса и предположение об условной
независимости признаков; сглаживание Лапласа задаётся параметром \(\alpha\).
Быстрый baseline для частотных признаков; требует неотрицательных значений,
поэтому LSA-компоненты для него исключались.
## 4.6. Метод ближайших соседей
Класс определяется по \(k\) ближайшим объектам; использовалось косинусное
расстояние и веса, обратные расстоянию. Простая локальная модель; чувствительна
к размерности и масштабу признаков.
## 4.7. Дельта Барроуза
Классический стилометрический метод: выбираются наиболее частотные слова,
частоты z-нормируются по обучающей выборке, документ относится к классу с
минимальным расстоянием до центроида. Использовались манхэттенское и
косинусное расстояния. Дельта — независимая проверка, не заменяющая
ML-классификаторы.
## 4.8. Ансамбли
- **Мягкое голосование** усредняет вероятности нескольких моделей.
- **Объединение через мета-классификатор (стекинг)** обучает второй уровень
на предсказаниях базовых моделей. В наших данных оно переобучилось и
оказалось хуже мягкого голосования.
# 5. Схема верификации
## 5.1. Перекрёстная проверка
Основная схема:
```text
RepeatedStratifiedKFold(n_splits=5, n_repeats=3–5)
```
При 5 фолдах модель обучается на 4 фолдах и проверяется на 1; доли классов
сохраняются внутри фолдов. Повторность позволяет оценить разброс.
## 5.2. Вложенная проверка
Внешние фолды используются для оценки качества, внутренние — для настройки
гиперпараметров. Дополнительно выполнен вложенный выбор конфигурации
признаков и модели: в каждом внешнем фолде конфигурация выбиралась только по
внутренним фолдам.
## 5.3. Контроль утечек
- TF-IDF, SVD, масштабирование и отбор признаков обучаются только на
обучающих фолдах.
- Тестовая выборка не участвует в выборе гиперпараметров или признаков.
- Контроль перестановкой меток дал macro-F1 0,0270–0,0282 — случайный
уровень.
- Все 13 почти-дубликатов находятся внутри одного класса; межклассовых
почти-дублей не обнаружено, поэтому прямой утечки метки нет. При более
строгой оценке такие пары всё равно следует держать в одном фолде.
## 5.4. Метрики
| Метрика | Что показывает |
|---|---|
| Macro-F1 | Основная метрика: средняя F-мера по 34 классам |
| Доля правильных ответов | Доля совпавших ответов; обманчива при дисбалансе |
| Сбалансированная доля | Средняя полнота по классам |
| Top-3 | Истинный класс попал в тройку лучших |
| MCC (коэффициент корреляции Мэтьюза) | Корреляция истинных и предсказанных меток |
| Каппа Коэна | Согласие с поправкой на случай |
| Log loss / Brier | Качество вероятностей |
| Матрица ошибок | Какие персонажи перепутываются |
Дополнительно использованы бутстрэп-интервалы, критерий Мак-Немара для
парных сравнений, критерий Уилкоксона и поправка Холма—Бонферрони.
# 6. Результаты
## 6.1. Сравнение моделей
| Конфигурация | Macro-F1 | Доля правильных ответов | Top-3 |
|---|---:|---:|---:|
| Мягкое голосование трёх моделей | **0,3036 ± 0,0189** | 0,2808 | 0,4823 |
| ExtraTrees, `char_max_features=10000` | 0,2757 ± 0,0219 | 0,2633 | 0,4554 |
| Вложенный выбор конфигурации | 0,2746 ± 0,0233 | 0,2603 | — |
| Случайный лес, `char_max_features=10000` | 0,2583 ± 0,0066 | 0,2391 | 0,4197 |
| ExtraTrees, полный набор | 0,2499 ± 0,0209 | 0,2374 | 0,4234 |
| Логистическая регрессия, lean + content-LSA | 0,2466 ± 0,0191 | 0,2349 | 0,4030 |
| Логистическая регрессия, `stylometry_pos_lsa` | 0,1785 ± 0,0060 | 0,1679 | 0,3322 |
| Только ручные признаки | 0,1051 ± 0,0038 | 0,1012 | 0,2338 |
| Случайный уровень | 0,029 | 0,029 | 0,088 |
На отложенной выборке 80/20 мягкое голосование дало **0,3333**; ExtraTrees
на полном наборе — 0,2793; случайный лес с `char_max_features=10000` —
0,2554; логистическая регрессия с content-LSA — 0,2459.
![Сравнение моделей](figures/model_comparison.png){width=82%}
## 6.2. Вклад признаков
| Условие | Macro-F1 |
|---|---:|
| Только длина | 0,035 |
| Только ручные стилометрические признаки | 0,105 |
| Ручные + символьные и частеречные n-граммы | 0,182 |
| То же + `content`-LSA | 0,249 |
| ExtraTrees с расширенным символьным словарём | 0,276 |
| Мягкое голосование | 0,304 |
![Вклад признаков](figures/feature_progression.png){width=72%}
Основной прирост дают символьные n-граммы, лексика и `content`-LSA.
Функциональные слова и POS-LSA сами по себе слабы (0,06–0,07): это важный
отрицательный результат.
## 6.3. Стиль или содержание
Содержательные слова (существительные, глаголы, прилагательные, наречия,
числительные) заменялись нейтральным маркером `CONTENT`; служебные слова,
пунктуация и частеречные теги сохранялись.
| Условие | Macro-F1 |
|---|---:|
| Исходный текст, стилевой набор, ExtraTrees | 0,2483 |
| Маскированный текст, стилевой набор, ExtraTrees | 0,1129 |
| Исходный текст, стилевой набор, LogReg | 0,1467 |
| Маскированный текст, стилевой набор, LogReg | 0,1067 |
![Контроль содержания](figures/topic_control.png){width=68%}
Падение более чем вдвое показывает, что значительная часть качества связана
с лексическим содержанием. Остаток 0,113 всё ещё в 3,9 раза выше случайного
уровня: некоторый стилевой сигнал сохраняется, но он слабее тематического.
## 6.4. Матрицы ошибок
Строки матрицы — истинный персонаж, столбцы — предсказание модели, диагональ
— правильные ответы, вне диагонали — ошибки. Полная матрица 34×34 на доске
нечитаема; поэтому в основном тексте показаны 12 самых частых персонажей и
столбец «прочие», а также 18 самых частых пар ошибок.
![Матрица ошибок: 12 персонажей](figures/confusion_top12.png){width=92%}
![Самые частые пары ошибок](figures/confusion_pairs.png){width=92%}
Основные пары ошибок (за три повтора): Sebastian → Abigail (65),
Sebastian → Alex (61), Abigail → Sebastian (59), Abigail → Alex (56).
Крупнейший клубок — молодые персонажи Abigail, Alex, Sebastian, Haley, Sam.
Многие из них писались в одних и тех же обновлениях и сюжетных контекстах,
поэтому сходство голосов ожидаемо; это не разные авторы.
## 6.5. Ошибки по персонажам
![F-мера по персонажам](figures/per_class_f1.png){width=92%}
Лучшие результаты по F-мере: Willy, Evelyn, Pierre, Pam, Leo, Vincent.
Худшие: Kent, Mister Qi, Penny, Abigail, Leah, Jodi. Редкие классы имеют
широкие интервалы; у Mister Qi всего 15 реплик.
Abigail и Alex являются классами-аттракторами: модель ошибочно относит к ним
чужие реплики. Abigail получает 10–18% всех предсказаний при истинной доле
около 6,9%. Простая коррекция априорных вероятностей классов не улучшила
macro-F1.
![Классы-аттракторы](figures/hub_shares.png){width=88%}
## 6.6. Длина реплики
| Длина | Macro-F1 |
|---|---:|
| 0–5 слов | 0,097 |
| 6–10 | 0,244 |
| 11–15 | 0,233 |
| 16–20 | 0,239 |
| 21–30 | 0,295 |
| 31+ | 0,271 |
Зависимость немонотонная: максимум приходится на 21–30 слов, после 30 слов
качество снижается. Гипотеза «чем длиннее, тем лучше» не подтверждается.
## 6.7. Верификация и кластеризация
- Верификация «свой/чужой»: ROC-AUC 0,630–0,633; EER (равная ошибка
ложного принятия и ложного отказа) 0,412–0,415; top-1 0,133–0,141.
Выше случайного, но практически слабо.
- Кластеризация: ARI (скорректированный индекс Рэнда) = 0 для k = 3/5/8;
NMI (нормированная взаимная информация) 0,14–0,53. Устойчивых кластеров,
соответствующих персонажам, нет.
## 6.8. Калибровка и ансамбли
Коррекция априорных вероятностей вида
\(p'_{ic}\propto p_{ic}\pi_c^{-\alpha}\) проверялась по схеме
«один повтор — отложенный, остальные — обучающие». Среднее изменение
macro-F1 составило **−0,0022**: простой пересчёт приоров не помог.
Мягкое голосование трёх моделей (ExtraTrees на полном наборе, ExtraTrees с
`char_max_features=10000`, логистическая регрессия с lean + content-LSA)
дало **0,3036 ± 0,0189**. Объединение через мета-классификатор оказалось хуже
(0,2259 ± 0,0163) из-за переобучения на малом числе объектов.
# 7. Обсуждение
1. **Персонажи статистически различимы, но сигнал умеренный.** 0,304
macro-F1 при 34 классах — примерно в 10 раз выше случайного уровня 0,029.
Абсолютное значение ограничено короткими репликами, дисбалансом и общими
темами.
2. **Основной вклад — лексико-тематический.** Символьные n-граммы и
`content`-LSA дают основной прирост; функциональные слова и POS-LSA сами
по себе слабы. После маскирования содержательных слов качество падает
более чем вдвое.
3. **Наиболее устойчивая одиночная модель — ExtraTrees с
`char_max_features=10000`.** Она выбрана в 14 из 15 внешних фолдов
вложенной процедуры и даёт 0,2757 ± 0,0219.
4. **Мягкое голосование — лучший наблюдаемый результат**, но состав
ансамбля выбирался по предыдущим сериям. Для строгого вывода нужна
внешняя валидация.
5. **Ошибки — это коллапс редких классов в классы-аттракторы**, а не
доказательство сходства характеров. Abigail, Alex и ещё несколько
популярных классов забирают чужие реплики.
6. **Один автор и общие контексты** делают путаницу между персонажами
ожидаемой. Это не означает, что автор «не старался»: превышение
случайного уровня как раз говорит о намеренно созданных речевых масках.
# 8. Ограничения
- Один корпус, одна игра, один автор; внешняя валидация не проводилась.
- Единица анализа — реплика, а не текст из 30 предложений; это согласовано,
но ограничивает сравнение с документными корпусами.
- Классы неравномерны; редкие классы имеют широкие интервалы.
- Ручная фильтрация корпуса невоспроизводима.
- Нет идентификаторов сцен и диалогов.
- Выбор признаков и ансамбля частично опирался на предыдущие серии;
вложенная проверка снижает, но не устраняет оптимизм.
- Содержательные слова и тема вносят большой вклад; «чистый стиль» не
доказан.
- Простая коррекция приоров не помогла; калибровка редких классов требует
дальнейшей работы.
# 9. Выводы
1. Речевые маски 34 персонажей статистически различимы по отдельным
репликам: лучшая модель — 0,304 macro-F1, консервативная оценка — 0,275
при случайном уровне 0,029.
2. Основной сигнал — лексико-тематический: символьные n-граммы и
`content`-LSA дают основной прирост; функциональные слова и частеречная
LSA сами по себе слабы.
3. После замены содержательных слов качество падает с 0,248 до 0,113:
содержание доминирует, но стилевой сигнал сохраняется.
4. Главная проблема — редкие классы и классы-аттракторы; мягкое голосование
помогает, но не решает её полностью.
5. Корректная формулировка результата: различимы речевые маски по
лексико-тематическому профилю; утверждение о «чистом стиле» требует
внешней валидации и тематического контроля.
# 10. Воспроизводимость
## 10.1. Структура проекта
```text
lab1/
├── stylometry/ # чтение, признаки, LSA, модели, метрики
├── scripts/ # проверка окружения, запуск серий, упаковка
├── tests/ # pytest
├── configs/ # конфигурация
├── data/ # корпус и метаданные
├── report/ # этот отчёт
├── presentation/ # PPTX/PDF и LaTeX-версия
├── README.md
├── RUNBOOK.md
├── EXPERIMENTS.md
├── FEATURES.md
├── METRICS_AND_CLASSIFIERS.md
├── TUNING.md
├── FINAL_REPORT.md
├── FINAL_RESULTS.csv
├── requirements.txt
└── requirements-report.txt
```
Краткая и подробная инструкции к архиву (`START_HERE.md`,
`ARCHIVE_README.md`) лежат в его корне, рядом с каталогом `lab1`.
## 10.2. Установка и запуск
```bash
cd lab1
python -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt
python scripts/download_nltk_data.py
python scripts/check_environment.py
python -m pytest -q
# аудит и признаки
python -m stylometry.cli audit
python -m stylometry.cli features
# основной эксперимент
python -m stylometry.cli run \
--preset stylometry_pos_lsa --model logreg --repeats 5
# полная серия
bash scripts/run_all_experiments.sh
# дополнительные эксперименты
python scripts/run_final_experiments.py \
--stages nested topic calibration ensemble extra \
--out results/final
# пересборка отчёта (pandoc + LibreOffice)
python scripts/build_report.py
```
## 10.3. Программное обеспечение
- Python 3.11+;
- NumPy, pandas, SciPy, scikit-learn, matplotlib;
- NLTK (`punkt`, `punkt_tab`, `averaged_perceptron_tagger_eng`, `stopwords`);
- python-pptx и Pillow — для презентации;
- pandoc, LibreOffice, python-docx и PyMuPDF — для сборки отчёта;
- XeLaTeX и Beamer — для LaTeX-версии презентации.
# Приложение A. Полная матрица ошибок
![Полная матрица ошибок 34×34](figures/confusion_full34.png){width=92%}
# Приложение B. Сводная таблица результатов
См. `FINAL_RESULTS.csv` в корне проекта.
# Приложение C. Состав архива
В архив включены код, тесты, конфигурации, корпус, метаданные, документация,
отчёт, презентация (PPTX и LaTeX), сводные результаты и инструкции запуска.
Виртуальное окружение, кэш NLTK и промежуточные матрицы признаков в архив не
включены: они восстанавливаются скриптами.
# Приложение D. Источники и программные средства
1. Burrows, J. F. «Delta»: a Measure of Stylistic Difference and a Guide to
Likely Authorship // Literary and Linguistic Computing. 2002. Vol. 17,
no. 3. P. 267–287.
2. Eder, M., Rybicki, J., Kestemont, M. Stylometry with R: A Package for
Computational Text Analysis // The R Journal. 2016. Vol. 8, no. 1.
P. 107–121.
3. Mosteller, F., Wallace, D. L. Inference and Disputed Authorship:
The Federalist. Reading, MA: Addison-Wesley, 1964.
4. Stamatatos, E. A Survey of Modern Authorship Attribution Methods // Journal of the American Society for Information Science and Technology. 2009. Vol. 60, no. 3. P. 538–556.
5. Pedregosa, F. et al. Scikit-learn: Machine Learning in Python //
Journal of Machine Learning Research. 2011. Vol. 12. P. 2825–2830.
6. Bird, S., Klein, E., Loper, E. Natural Language Processing with Python.
Sebastopol, CA: O'Reilly Media, 2009.
7. ConcernedApe. Stardew Valley. Video game. 2016.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 680 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 795 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

View File

@@ -1,5 +0,0 @@
# Дополнительные зависимости для пересборки отчёта.
# Основные зависимости проекта — в requirements.txt.
python-docx>=1.1,<2
PyMuPDF>=1.24,<2
Pillow>=10,<13

View File

@@ -8,4 +8,3 @@ nltk>=3.9,<4
joblib>=1.3,<2 joblib>=1.3,<2
tabulate>=0.9,<1 tabulate>=0.9,<1
pytest>=8.0,<10 pytest>=8.0,<10
python-pptx>=1.0,<2

View File

@@ -1,336 +0,0 @@
#!/usr/bin/env python
"""Сводка follow-up экспериментов и парные сравнения по повторам.
Скрипт читает каталоги results/followup/<label>/, созданные
scripts/run_followup_experiments.sh, и формирует:
- summary.csv / summary.md — среднее macro-F1 по повторам, а не только repeat 0;
- comparisons.csv / comparisons.md — парные сравнения с McNemar и bootstrap;
- comparisons/<A>__vs__<B>.json — детали по каждому повтору.
Пример:
python scripts/analyze_followup.py \
--followup-dir results/followup \
--pairs final_rf:ref_logreg final_rf:ref_no_lsa
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from typing import Any
import numpy as np
import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(PROJECT_ROOT))
from stylometry.evaluation import classification_metrics # noqa: E402
from stylometry.stats import bootstrap_difference, mcnemar_exact # noqa: E402
def _json_load(path: Path) -> dict[str, Any]:
return json.loads(path.read_text(encoding="utf-8"))
def _read_run(run_dir: Path) -> tuple[dict[str, Any], pd.DataFrame]:
metadata = _json_load(run_dir / "run_metadata.json")
pooled_path = run_dir / "pooled_metrics.csv"
if pooled_path.exists():
pooled = pd.read_csv(pooled_path)
else:
pooled = pd.DataFrame([metadata.get("metrics", {})])
return metadata, pooled
def _feature_summary(config: dict[str, Any]) -> str:
keys = (
"groups",
"lsa_variants",
"lsa_components",
"min_df",
"char_max_features",
"pos_max_features",
"char_ngram_range",
"pos_ngram_range",
"block_weights",
)
features = config.get("feature_config", {})
return json.dumps({key: features.get(key) for key in keys if key in features}, ensure_ascii=False)
def build_summary(followup_dir: Path) -> pd.DataFrame:
rows: list[dict[str, Any]] = []
for run_dir in sorted(followup_dir.iterdir()):
if not run_dir.is_dir() or not (run_dir / "run_metadata.json").exists():
continue
metadata, pooled = _read_run(run_dir)
config = metadata.get("config", {})
bootstrap = metadata.get("bootstrap", {}).get("macro_f1", {})
macro = pd.to_numeric(pooled.get("macro_f1"), errors="coerce")
accuracy = pd.to_numeric(pooled.get("accuracy"), errors="coerce")
balanced = pd.to_numeric(pooled.get("balanced_accuracy"), errors="coerce")
top3 = pd.to_numeric(pooled.get("top3_accuracy"), errors="coerce")
command_path = run_dir / "command.txt"
rows.append(
{
"label": run_dir.name,
"preset": config.get("preset"),
"model": config.get("model"),
"folds": config.get("folds"),
"n_repeats": int(len(pooled)),
"n_samples": config.get("n_samples"),
"mean_macro_f1": float(macro.mean()),
"std_macro_f1": float(macro.std(ddof=1)) if len(macro) > 1 else 0.0,
"min_macro_f1": float(macro.min()),
"max_macro_f1": float(macro.max()),
"repeat0_macro_f1": float(macro.iloc[0]),
"mean_accuracy": float(accuracy.mean()),
"mean_balanced_accuracy": float(balanced.mean()),
"mean_top3_accuracy": float(top3.mean()),
"ci_lower_repeat0": bootstrap.get("lower"),
"ci_upper_repeat0": bootstrap.get("upper"),
"model_params": json.dumps(config.get("model_params", {}), ensure_ascii=False),
"feature_config": _feature_summary(config),
"command": command_path.read_text(encoding="utf-8").strip()
if command_path.exists()
else "",
}
)
summary = pd.DataFrame(rows)
if summary.empty:
return summary
reference = summary.loc[summary["label"] == "ref_logreg", "mean_macro_f1"]
if not reference.empty:
summary["delta_vs_ref_logreg"] = summary["mean_macro_f1"] - float(reference.iloc[0])
else:
summary["delta_vs_ref_logreg"] = np.nan
summary = summary.sort_values("mean_macro_f1", ascending=False).reset_index(drop=True)
summary.insert(0, "rank", np.arange(1, len(summary) + 1))
return summary
def _compare_pair(
followup_dir: Path,
label_a: str,
label_b: str,
n_boot: int,
seed: int,
) -> tuple[dict[str, Any] | None, list[dict[str, Any]]]:
dir_a = followup_dir / label_a
dir_b = followup_dir / label_b
if not (dir_a / "oof_predictions.csv").exists() or not (dir_b / "oof_predictions.csv").exists():
print(f"[compare] пропуск {label_a} vs {label_b}: нет oof_predictions.csv")
return None, []
oof_a = pd.read_csv(dir_a / "oof_predictions.csv")
oof_b = pd.read_csv(dir_b / "oof_predictions.csv")
common_repeats = sorted(set(oof_a["repeat"].unique()) & set(oof_b["repeat"].unique()))
if not common_repeats:
print(f"[compare] пропуск {label_a} vs {label_b}: нет общих повторов")
return None, []
details: list[dict[str, Any]] = []
for repeat in common_repeats:
first = oof_a.loc[oof_a["repeat"] == repeat, ["line_id", "y_true", "y_pred"]]
second = oof_b.loc[oof_b["repeat"] == repeat, ["line_id", "y_true", "y_pred"]]
merged = first.merge(
second,
on=["line_id", "y_true"],
suffixes=("_a", "_b"),
how="inner",
)
if merged.empty:
continue
labels = sorted(merged["y_true"].unique())
metrics_a = classification_metrics(merged["y_true"], merged["y_pred_a"], labels=labels)
metrics_b = classification_metrics(merged["y_true"], merged["y_pred_b"], labels=labels)
mcnemar = mcnemar_exact(merged["y_true"], merged["y_pred_a"], merged["y_pred_b"])
difference = bootstrap_difference(
merged["y_true"],
merged["y_pred_a"],
merged["y_pred_b"],
metric="macro_f1",
labels=labels,
n_boot=n_boot,
seed=seed + int(repeat),
)
details.append(
{
"repeat": int(repeat),
"n_samples": int(len(merged)),
"macro_f1_a": metrics_a["macro_f1"],
"macro_f1_b": metrics_b["macro_f1"],
"macro_f1_diff": metrics_a["macro_f1"] - metrics_b["macro_f1"],
"accuracy_a": metrics_a["accuracy"],
"accuracy_b": metrics_b["accuracy"],
"accuracy_diff": metrics_a["accuracy"] - metrics_b["accuracy"],
"mcnemar_p": mcnemar["p_value"],
"mcnemar_b": mcnemar["b"],
"mcnemar_c": mcnemar["c"],
"bootstrap_mean_diff": difference["mean_difference"],
"bootstrap_lower": difference["lower"],
"bootstrap_upper": difference["upper"],
}
)
if not details:
print(f"[compare] пропуск {label_a} vs {label_b}: не удалось сопоставить строки")
return None, []
diffs = np.asarray([row["macro_f1_diff"] for row in details], dtype=float)
p_values = np.asarray([row["mcnemar_p"] for row in details], dtype=float)
ci_lower = np.asarray([row["bootstrap_lower"] for row in details], dtype=float)
ci_upper = np.asarray([row["bootstrap_upper"] for row in details], dtype=float)
summary = {
"pair": f"{label_a} vs {label_b}",
"label_a": label_a,
"label_b": label_b,
"n_repeats": len(details),
"mean_macro_f1_a": float(np.mean([row["macro_f1_a"] for row in details])),
"mean_macro_f1_b": float(np.mean([row["macro_f1_b"] for row in details])),
"mean_diff": float(diffs.mean()),
"std_diff": float(diffs.std(ddof=1)) if len(diffs) > 1 else 0.0,
"min_diff": float(diffs.min()),
"max_diff": float(diffs.max()),
"n_positive": int((diffs > 0).sum()),
"n_negative": int((diffs < 0).sum()),
"mean_mcnemar_p": float(p_values.mean()),
"min_mcnemar_p": float(p_values.min()),
"mean_ci_lower": float(ci_lower.mean()),
"mean_ci_upper": float(ci_upper.mean()),
"n_ci_excludes_zero": int(((ci_lower > 0) | (ci_upper < 0)).sum()),
}
return summary, details
def _format_markdown(summary: pd.DataFrame) -> str:
columns = [
"rank",
"label",
"model",
"preset",
"n_repeats",
"mean_macro_f1",
"std_macro_f1",
"mean_accuracy",
"mean_top3_accuracy",
"delta_vs_ref_logreg",
]
table = summary[columns].copy()
return table.to_markdown(index=False, floatfmt=".4f")
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--followup-dir",
default=str(PROJECT_ROOT / "results" / "followup"),
help="Каталог с подкаталогами запусков.",
)
parser.add_argument(
"--pairs",
nargs="*",
default=[],
help="Пары для сравнения в формате label_a:label_b.",
)
parser.add_argument("--n-boot", type=int, default=1000)
parser.add_argument("--seed", type=int, default=42)
args = parser.parse_args(argv)
followup_dir = Path(args.followup_dir)
if not followup_dir.exists():
raise FileNotFoundError(f"Нет каталога: {followup_dir}")
summary = build_summary(followup_dir)
if summary.empty:
print(f"[summary] в {followup_dir} нет запусков с run_metadata.json")
return 1
summary_path = followup_dir / "summary.csv"
summary.to_csv(summary_path, index=False, encoding="utf-8-sig")
markdown = [
"# Follow-up: сводка запусков",
"",
"Среднее и стандартное отклонение посчитаны по pooled-метрикам каждого",
"повтора (`pooled_metrics.csv`), а не только по repeat 0.",
"",
_format_markdown(summary),
"",
]
(followup_dir / "summary.md").write_text("\n".join(markdown), encoding="utf-8")
print(f"[summary] {summary_path}")
print(
summary[
["rank", "label", "model", "n_repeats", "mean_macro_f1", "std_macro_f1"]
].to_string(index=False)
)
comparison_rows: list[dict[str, Any]] = []
comparisons_dir = followup_dir / "comparisons"
comparisons_dir.mkdir(exist_ok=True)
for pair in args.pairs:
if ":" not in pair:
print(f"[compare] некорректная пара: {pair}")
continue
label_a, label_b = pair.split(":", 1)
summary_row, details = _compare_pair(
followup_dir,
label_a.strip(),
label_b.strip(),
n_boot=args.n_boot,
seed=args.seed,
)
if summary_row is None:
continue
comparison_rows.append(summary_row)
payload = {"summary": summary_row, "details": details}
output = comparisons_dir / f"{label_a.strip()}__vs__{label_b.strip()}.json"
output.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
print(
f"[compare] {summary_row['pair']}: "
f"diff={summary_row['mean_diff']:+.4f}, "
f"{summary_row['n_positive']}/{summary_row['n_repeats']} повторов в плюс, "
f"mean McNemar p={summary_row['mean_mcnemar_p']:.4f}"
)
if comparison_rows:
comparisons = pd.DataFrame(comparison_rows).sort_values("mean_diff", ascending=False)
comparisons.to_csv(followup_dir / "comparisons.csv", index=False, encoding="utf-8-sig")
columns = [
"pair",
"n_repeats",
"mean_macro_f1_a",
"mean_macro_f1_b",
"mean_diff",
"std_diff",
"n_positive",
"mean_mcnemar_p",
"mean_ci_lower",
"mean_ci_upper",
"n_ci_excludes_zero",
]
comparison_markdown = [
"# Follow-up: парные сравнения",
"",
"`mean_diff` — средняя разница macro-F1 (A − B) по общим повторам.",
"`n_positive` — сколько повторов дали положительную разницу.",
"`n_ci_excludes_zero` — сколько повторов имеют bootstrap-интервал,",
"не содержащий ноль (описательно, без поправки на множественность).",
"",
comparisons[columns].to_markdown(index=False, floatfmt=".4f"),
"",
]
(followup_dir / "comparisons.md").write_text(
"\n".join(comparison_markdown), encoding="utf-8"
)
print(f"[compare] {followup_dir / 'comparisons.csv'}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -1,570 +0,0 @@
#!/usr/bin/env python3
"""Сборка отчёта: Markdown -> DOCX -> PDF.
Скрипт рассчитан на запуск из каталога lab1:
python scripts/build_report.py
Требуются pandoc, python-docx и libreoffice. PDF собирается через
LibreOffice, чтобы не зависеть от LaTeX-шаблона и корректно передать
кириллицу, таблицы и рисунки.
"""
from __future__ import annotations
import argparse
import shutil
import subprocess
import sys
from pathlib import Path
import docx
from docx.enum.section import WD_SECTION
from docx.enum.table import WD_TABLE_ALIGNMENT
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_BREAK, WD_LINE_SPACING
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from docx.shared import Cm, Pt, RGBColor
ROOT = Path(__file__).resolve().parents[1]
REPORT_MD = ROOT / "report" / "REPORT.md"
BUILD = ROOT / "report" / "build"
DOCX_OUT = ROOT / "report" / "REPORT.docx"
PDF_OUT = ROOT / "report" / "REPORT.pdf"
BODY_FONT = "Times New Roman"
HEAD_FONT = "Times New Roman"
MONO_FONT = "Liberation Mono"
INK = RGBColor(0x1A, 0x1A, 0x1A)
MUTED = RGBColor(0x55, 0x55, 0x55)
ACCENT = RGBColor(0x5B, 0x3A, 0x1E)
TABLE_WIDTHS = [
(2.8, 13.8),
(11.6, 5.0),
(4.6, 12.0),
(3.4, 13.2),
(5.6, 11.0),
(7.6, 3.4, 3.4, 2.2),
(9.6, 7.0),
(10.0, 6.6),
(11.6, 5.0),
]
TABLE_CAPTIONS = [
"Гипотезы исследования и способ их проверки",
"Характеристики корпуса",
"Группы ручных стилометрических признаков",
"Варианты латентно-семантического анализа",
"Метрики качества и их назначение",
"Сравнение конфигураций классификаторов",
"Вклад групп признаков",
"Контроль содержания: исходный и маскированный текст",
"Качество классификации по длине реплики",
]
def run(cmd: list[str], cwd: Path | None = None) -> None:
print("+", " ".join(cmd))
subprocess.run(cmd, cwd=cwd, check=True)
def set_run_font(run, name: str, size: float | None = None, bold: bool | None = None,
italic: bool | None = None, color: RGBColor | None = None) -> None:
run.font.name = name
run._element.rPr.rFonts.set(qn("w:eastAsia"), name)
if size is not None:
run.font.size = Pt(size)
if bold is not None:
run.bold = bold
if italic is not None:
run.italic = italic
if color is not None:
run.font.color.rgb = color
def set_style_font(style, name: str, size: float, bold: bool = False,
color: RGBColor = INK, italic: bool | None = None) -> None:
style.font.name = name
style.font.size = Pt(size)
style.font.bold = bold
style.font.color.rgb = color
if italic is not None:
style.font.italic = italic
rpr = style.element.get_or_add_rPr()
rfonts = rpr.find(qn("w:rFonts"))
if rfonts is None:
rfonts = OxmlElement("w:rFonts")
rpr.append(rfonts)
for attr in ("w:ascii", "w:hAnsi", "w:eastAsia", "w:cs"):
rfonts.set(qn(attr), name)
def set_paragraph_spacing(style, before: float = 0, after: float = 0,
line: float = 1.5) -> None:
pf = style.paragraph_format
pf.space_before = Pt(before)
pf.space_after = Pt(after)
pf.line_spacing = line
def shade(cell, fill: str) -> None:
tc_pr = cell._tc.get_or_add_tcPr()
shd = OxmlElement("w:shd")
shd.set(qn("w:val"), "clear")
shd.set(qn("w:color"), "auto")
shd.set(qn("w:fill"), fill)
tc_pr.append(shd)
def set_cell_margins(table, top=60, bottom=60, left=90, right=90) -> None:
tbl_pr = table._tbl.tblPr
mar = OxmlElement("w:tblCellMar")
for edge, value in (("top", top), ("left", left), ("bottom", bottom), ("right", right)):
el = OxmlElement(f"w:{edge}")
el.set(qn("w:w"), str(value))
el.set(qn("w:type"), "dxa")
mar.append(el)
tbl_pr.append(mar)
def set_repeat_header(row) -> None:
tr_pr = row._tr.get_or_add_trPr()
el = OxmlElement("w:tblHeader")
el.set(qn("w:val"), "true")
tr_pr.append(el)
cant_split = OxmlElement("w:cantSplit")
cant_split.set(qn("w:val"), "true")
tr_pr.append(cant_split)
def set_table_borders(table, color: str = "B8A88F", size: int = 4) -> None:
tbl_pr = table._tbl.tblPr
borders = OxmlElement("w:tblBorders")
for edge in ("top", "left", "bottom", "right", "insideH", "insideV"):
el = OxmlElement(f"w:{edge}")
el.set(qn("w:val"), "single")
el.set(qn("w:sz"), str(size))
el.set(qn("w:space"), "0")
el.set(qn("w:color"), color)
borders.append(el)
tbl_pr.append(borders)
def add_page_field(paragraph, total: bool = False) -> None:
run = paragraph.add_run()
fld_char_begin = OxmlElement("w:fldChar")
fld_char_begin.set(qn("w:fldCharType"), "begin")
instr_text = OxmlElement("w:instrText")
instr_text.set(qn("xml:space"), "preserve")
instr_text.text = "NUMPAGES" if total else "PAGE"
fld_char_end = OxmlElement("w:fldChar")
fld_char_end.set(qn("w:fldCharType"), "end")
run._r.append(fld_char_begin)
run._r.append(instr_text)
run._r.append(fld_char_end)
def add_bottom_border(paragraph, color: str = "5B3A1E", size: int = 6) -> None:
p_pr = paragraph._p.get_or_add_pPr()
borders = OxmlElement("w:pBdr")
bottom = OxmlElement("w:bottom")
bottom.set(qn("w:val"), "single")
bottom.set(qn("w:sz"), str(size))
bottom.set(qn("w:space"), "4")
bottom.set(qn("w:color"), color)
borders.append(bottom)
p_pr.append(borders)
def add_left_border(paragraph, color: str = "B8A88F", size: int = 18) -> None:
p_pr = paragraph._p.get_or_add_pPr()
borders = OxmlElement("w:pBdr")
left = OxmlElement("w:left")
left.set(qn("w:val"), "single")
left.set(qn("w:sz"), str(size))
left.set(qn("w:space"), "8")
left.set(qn("w:color"), color)
borders.append(left)
p_pr.append(borders)
def add_page_break(paragraph) -> None:
run = paragraph.add_run()
run.add_break(WD_BREAK.PAGE)
def style_document(doc) -> None:
section = doc.sections[0]
section.page_width = Cm(21.0)
section.page_height = Cm(29.7)
section.left_margin = Cm(2.6)
section.right_margin = Cm(1.8)
section.top_margin = Cm(2.0)
section.bottom_margin = Cm(2.0)
section.header_distance = Cm(1.0)
section.footer_distance = Cm(1.0)
styles = doc.styles
normal = styles["Normal"]
set_style_font(normal, BODY_FONT, 12)
normal.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
normal.paragraph_format.first_line_indent = Cm(1.25)
normal.paragraph_format.line_spacing = 1.5
normal.paragraph_format.space_before = Pt(0)
normal.paragraph_format.space_after = Pt(0)
for name in ("Body Text", "First Paragraph"):
st = styles[name]
set_style_font(st, BODY_FONT, 12)
st.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
st.paragraph_format.first_line_indent = Cm(1.25)
st.paragraph_format.line_spacing = 1.5
st.paragraph_format.space_before = Pt(0)
st.paragraph_format.space_after = Pt(6)
styles["First Paragraph"].paragraph_format.space_before = Pt(0)
title = styles["Title"]
set_style_font(title, HEAD_FONT, 24, bold=True, color=ACCENT)
title.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
title.paragraph_format.space_before = Pt(150)
title.paragraph_format.space_after = Pt(12)
title.paragraph_format.line_spacing = 1.1
subtitle = styles["Subtitle"]
set_style_font(subtitle, HEAD_FONT, 15, bold=False, color=INK)
subtitle.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
subtitle.paragraph_format.space_before = Pt(0)
subtitle.paragraph_format.space_after = Pt(28)
subtitle.paragraph_format.line_spacing = 1.2
author = styles["Author"]
set_style_font(author, BODY_FONT, 13, color=INK)
author.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
author.paragraph_format.space_before = Pt(0)
author.paragraph_format.space_after = Pt(6)
date = styles["Date"]
set_style_font(date, BODY_FONT, 13, color=MUTED)
date.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
date.paragraph_format.space_before = Pt(0)
date.paragraph_format.space_after = Pt(0)
for name, size, before, after in (
("Heading 1", 17, 0, 10),
("Heading 2", 14, 14, 6),
("Heading 3", 12.5, 12, 4),
):
st = styles[name]
set_style_font(st, HEAD_FONT, size, bold=True, color=INK)
st.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.LEFT
st.paragraph_format.first_line_indent = Cm(0)
st.paragraph_format.space_before = Pt(before)
st.paragraph_format.space_after = Pt(after)
st.paragraph_format.line_spacing = 1.15
st.paragraph_format.keep_with_next = True
styles["Heading 1"].paragraph_format.page_break_before = False
styles["Heading 1"].paragraph_format.space_before = Pt(20)
block = styles["Block Text"]
set_style_font(block, BODY_FONT, 11.5, italic=False)
block.paragraph_format.left_indent = Cm(1.0)
block.paragraph_format.right_indent = Cm(0.5)
block.paragraph_format.first_line_indent = Cm(0)
block.paragraph_format.line_spacing = 1.3
block.paragraph_format.space_before = Pt(6)
block.paragraph_format.space_after = Pt(8)
block.paragraph_format.keep_together = True
source = styles["Source Code"]
set_style_font(source, MONO_FONT, 9)
source.paragraph_format.left_indent = Cm(0.6)
source.paragraph_format.first_line_indent = Cm(0)
source.paragraph_format.line_spacing = 1.0
source.paragraph_format.space_before = Pt(4)
source.paragraph_format.space_after = Pt(4)
source.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.LEFT
source.paragraph_format.keep_together = True
try:
verbatim = styles["Verbatim Char"]
set_style_font(verbatim, MONO_FONT, 9.5)
except KeyError:
pass
for name in ("Image Caption", "Caption"):
st = styles[name]
set_style_font(st, BODY_FONT, 10, italic=True, color=MUTED)
st.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
st.paragraph_format.first_line_indent = Cm(0)
st.paragraph_format.line_spacing = 1.15
st.paragraph_format.space_before = Pt(4)
st.paragraph_format.space_after = Pt(12)
for name in ("Table Caption",):
st = styles[name]
set_style_font(st, BODY_FONT, 10, italic=False, color=INK)
st.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
st.paragraph_format.first_line_indent = Cm(0)
st.paragraph_format.line_spacing = 1.15
st.paragraph_format.space_before = Pt(4)
st.paragraph_format.space_after = Pt(12)
st.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.LEFT
st.paragraph_format.first_line_indent = Cm(0)
st.paragraph_format.line_spacing = 1.15
st.paragraph_format.space_before = Pt(10)
st.paragraph_format.space_after = Pt(4)
st.paragraph_format.keep_with_next = True
def add_header_footer(doc) -> None:
section = doc.sections[0]
header = section.header
header.is_linked_to_previous = False
hp = header.paragraphs[0]
hp.alignment = WD_ALIGN_PARAGRAPH.RIGHT
hp.paragraph_format.space_after = Pt(0)
run = hp.add_run("Стилометрия реплик Stardew Valley · отчёт по лабораторной работе №1")
set_run_font(run, BODY_FONT, 8.5, color=MUTED)
add_bottom_border(hp, color="D8CBB8", size=4)
footer = section.footer
footer.is_linked_to_previous = False
fp = footer.paragraphs[0]
fp.alignment = WD_ALIGN_PARAGRAPH.CENTER
fp.paragraph_format.space_before = Pt(0)
fp.paragraph_format.space_after = Pt(0)
run = fp.add_run("Стр. ")
set_run_font(run, BODY_FONT, 9, color=MUTED)
add_page_field(fp)
for r in fp.runs:
set_run_font(r, BODY_FONT, 9, color=MUTED)
run = fp.add_run(" из ")
set_run_font(run, BODY_FONT, 9, color=MUTED)
add_page_field(fp, total=True)
for r in fp.runs:
set_run_font(r, BODY_FONT, 9, color=MUTED)
def add_title_page_notes(doc) -> None:
"""Добавляет подпись направления под титульным блоком и разрыв страницы."""
date_para = None
for p in doc.paragraphs:
if p.style.name == "Date":
date_para = p
break
if date_para is None:
return
anchor = date_para
note = anchor.insert_paragraph_before()
note.alignment = WD_ALIGN_PARAGRAPH.CENTER
note.paragraph_format.first_line_indent = Cm(0)
note.paragraph_format.space_before = Pt(18)
run = note.add_run("Направление: автоматический анализ текстов\n")
set_run_font(run, BODY_FONT, 11, color=MUTED)
run = note.add_run("Корпус: Stardew Valley · 2005 реплик · 34 персонажа")
set_run_font(run, BODY_FONT, 11, color=MUTED)
spacer = doc.add_paragraph()
spacer.paragraph_format.space_after = Pt(0)
add_page_break(spacer)
date_para._p.addnext(spacer._p)
def number_figures(doc) -> None:
n = 0
for p in doc.paragraphs:
if p.style.name == "Image Caption" and p.text.strip():
n += 1
p.text = f"Рисунок {n}. {p.text.strip()}"
for run in p.runs:
set_run_font(run, BODY_FONT, 10, italic=True, color=MUTED)
def add_table_captions(doc) -> None:
for idx, table in enumerate(doc.tables):
if idx >= len(TABLE_CAPTIONS):
break
para = doc.add_paragraph(style="Table Caption")
table._tbl.addprevious(para._p)
run = para.add_run(f"Таблица {idx + 1}. {TABLE_CAPTIONS[idx]}")
set_run_font(run, BODY_FONT, 10, bold=True, color=INK)
para.paragraph_format.first_line_indent = Cm(0)
para.paragraph_format.space_before = Pt(10)
para.paragraph_format.space_after = Pt(4)
para.paragraph_format.keep_with_next = True
def set_column_widths(table, widths_cm: tuple[float, ...]) -> None:
table.autofit = False
tbl_pr = table._tbl.tblPr
layout = OxmlElement("w:tblLayout")
layout.set(qn("w:type"), "fixed")
tbl_pr.append(layout)
grid = table._tbl.tblGrid
for idx, width in enumerate(widths_cm):
if idx < len(grid.gridCol_lst):
grid.gridCol_lst[idx].set(qn("w:w"), str(int(Cm(width).twips)))
for row in table.rows:
for idx, width in enumerate(widths_cm):
if idx < len(row.cells):
row.cells[idx].width = Cm(width)
def style_tables(doc) -> None:
for t_idx, table in enumerate(doc.tables):
table.alignment = WD_TABLE_ALIGNMENT.CENTER
table.autofit = True
set_table_borders(table)
set_cell_margins(table)
for r_idx, row in enumerate(table.rows):
if r_idx == 0:
set_repeat_header(row)
else:
tr_pr = row._tr.get_or_add_trPr()
cant_split = OxmlElement("w:cantSplit")
cant_split.set(qn("w:val"), "true")
tr_pr.append(cant_split)
for cell in row.cells:
for p in cell.paragraphs:
p.paragraph_format.first_line_indent = Cm(0)
p.paragraph_format.space_before = Pt(1)
p.paragraph_format.space_after = Pt(1)
p.paragraph_format.line_spacing = 1.0
if r_idx < len(table.rows) - 1:
p.paragraph_format.keep_with_next = True
for run in p.runs:
set_run_font(
run, BODY_FONT, 9.5 if len(table.columns) >= 5 else 10,
bold=(r_idx == 0),
)
if r_idx == 0:
shade(cell, "F1E8D8")
# Ширина таблицы по ширине полей.
tbl_pr = table._tbl.tblPr
tbl_w = OxmlElement("w:tblW")
tbl_w.set(qn("w:type"), "pct")
tbl_w.set(qn("w:w"), "100%")
tbl_pr.append(tbl_w)
if t_idx < len(TABLE_WIDTHS) and len(TABLE_WIDTHS[t_idx]) == len(table.columns):
set_column_widths(table, TABLE_WIDTHS[t_idx])
# Небольшой отступ между таблицей и следующим абзацем.
spacer = doc.add_paragraph()
spacer.paragraph_format.space_before = Pt(0)
spacer.paragraph_format.space_after = Pt(0)
spacer.paragraph_format.line_spacing = 1.0
sr = spacer.add_run("")
sr.font.size = Pt(4)
table._tbl.addnext(spacer._p)
def style_blockquotes(doc) -> None:
for p in doc.paragraphs:
if p.style.name == "Block Text":
add_left_border(p)
def add_math_font_note(doc) -> None:
"""OMML-формулы наследуют Cambria Math; явно задаём её для совместимости."""
for r in doc.element.body.iter(qn("m:r")):
rpr = r.find(qn("w:rPr"))
if rpr is None:
rpr = OxmlElement("w:rPr")
r.insert(0, rpr)
rfonts = rpr.find(qn("w:rFonts"))
if rfonts is None:
rfonts = OxmlElement("w:rFonts")
rpr.append(rfonts)
rfonts.set(qn("w:ascii"), "Cambria Math")
rfonts.set(qn("w:hAnsi"), "Cambria Math")
def build_docx() -> None:
BUILD.mkdir(parents=True, exist_ok=True)
raw = BUILD / "REPORT_raw.docx"
run([
"pandoc", str(REPORT_MD),
"-f", "markdown+tex_math_dollars+tex_math_single_backslash+raw_tex",
"-t", "docx",
"-o", str(raw),
f"--resource-path={REPORT_MD.parent}",
])
doc = docx.Document(raw)
style_document(doc)
section = doc.sections[0]
section.different_first_page_header_footer = True
add_header_footer(doc)
# Титульный лист без колонтитулов.
for part in (section.first_page_header, section.first_page_footer):
part.is_linked_to_previous = False
for paragraph in part.paragraphs:
paragraph.text = ""
add_title_page_notes(doc)
number_figures(doc)
add_table_captions(doc)
style_tables(doc)
style_blockquotes(doc)
add_math_font_note(doc)
doc.core_properties.title = "Отчёт по лабораторной работе №1"
doc.core_properties.subject = "Классификация текстов на основе стилометрических характеристик"
doc.core_properties.author = "А. А. Аладьина, В. Н. Копнин, М. В. Савостиков"
doc.core_properties.language = "ru-RU"
doc.save(DOCX_OUT)
print(f"DOCX: {DOCX_OUT}")
def build_pdf() -> None:
if shutil.which("libreoffice") is None:
raise SystemExit("libreoffice не найден: PDF не собран")
run([
"libreoffice", "--headless", "--convert-to", "pdf",
"--outdir", str(PDF_OUT.parent), str(DOCX_OUT),
])
produced = PDF_OUT.parent / (DOCX_OUT.stem + ".pdf")
if produced != PDF_OUT and produced.exists():
produced.replace(PDF_OUT)
print(f"PDF: {PDF_OUT}")
def check_pdf() -> None:
try:
import fitz # PyMuPDF
except ImportError:
print("PyMuPDF не установлен: пропускаю проверку PDF")
return
doc = fitz.open(PDF_OUT)
print(f"PDF страниц: {doc.page_count}")
text = "\n".join(page.get_text() for page in doc)
required = [
"Аннотация", "Постановка задачи", "Корпус", "Стилометрические признаки",
"Классификаторы", "Схема верификации", "Результаты", "Обсуждение",
"Ограничения", "Выводы", "Воспроизводимость",
]
missing = [item for item in required if item.lower() not in text.lower()]
if missing:
print("Не найдены разделы:", ", ".join(missing))
raise SystemExit(1)
print("Все основные разделы найдены в PDF")
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--skip-pdf", action="store_true", help="только DOCX")
args = parser.parse_args()
if not REPORT_MD.exists():
raise SystemExit(f"нет файла {REPORT_MD}")
build_docx()
if not args.skip_pdf:
build_pdf()
check_pdf()
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,208 +0,0 @@
#!/usr/bin/env python3
"""Собирает итоговый ZIP-архив лабораторной работы.
Запуск из каталога lab1:
python scripts/package_submission.py
Скрипт копирует только нужные файлы во временный каталог, формирует
MANIFEST.txt и CHECKSUMS.sha256, затем упаковывает всё в
dist/nlp_lab1_submission_YYYYMMDD.zip. Рядом кладётся .sha256 самого архива.
"""
from __future__ import annotations
import argparse
import hashlib
import os
import shutil
import zipfile
from datetime import date
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
DIST = ROOT / "dist"
DEFAULT_NAME = f"nlp_lab1_submission_{date.today():%Y%m%d}"
EXCLUDE_PREFIXES = (
"data/processed/",
"features/",
"results/",
"nltk_data/",
".venv/",
".git/",
"presentation/preview/",
"report/build/",
".pytest_cache/",
"dist/",
"exports/nlp_lab1_results_",
"exports/nlp_lab1_final_results_20261026_120000.tar.gz.sha256",
)
EXCLUDE_NAMES = {
"stardew_stylometry.bak",
"START_HERE.md",
"ARCHIVE_README.md",
}
LATEX_AUX_SUFFIXES = {".aux", ".log", ".nav", ".out", ".snm", ".toc"}
KEY_FILES = [
"lab1/report/REPORT.pdf",
"lab1/report/REPORT.docx",
"lab1/report/REPORT.md",
"lab1/presentation/stardew_stylometry.pdf",
"lab1/presentation/stardew_stylometry.pptx",
"lab1/presentation/latex/stardew_stylometry.pdf",
"lab1/presentation/latex/stardew_stylometry.tex",
"lab1/FINAL_RESULTS.csv",
"lab1/data/dialogues.csv",
"lab1/exports/nlp_lab1_final_results_20261026_120000.tar.gz",
]
def sha256(path: Path) -> str:
h = hashlib.sha256()
with path.open("rb") as fh:
for chunk in iter(lambda: fh.read(1024 * 1024), b""):
h.update(chunk)
return h.hexdigest()
def should_copy(rel: Path) -> bool:
posix = rel.as_posix()
if rel.name in EXCLUDE_NAMES:
return False
if "__pycache__" in rel.parts or rel.suffix == ".pyc":
return False
if rel.suffix in LATEX_AUX_SUFFIXES:
return False
if any(posix == prefix.rstrip("/") or posix.startswith(prefix)
for prefix in EXCLUDE_PREFIXES):
return False
return True
def copy_tree(stage: Path) -> int:
target_root = stage / "lab1"
count = 0
for path in sorted(ROOT.rglob("*")):
if not path.is_file():
continue
rel = path.relative_to(ROOT)
if not should_copy(rel):
continue
dst = target_root / rel
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(path, dst)
count += 1
return count
def write_manifest(stage: Path) -> int:
files = sorted(p for p in stage.rglob("*") if p.is_file())
lines = [
"Архив: " + stage.name,
f"Файлов в манифесте: {len(files)} (сам MANIFEST.txt не включён)",
"",
f"{'SHA-256':64} {'Размер':>12} Путь",
"-" * 100,
]
total = 0
for path in files:
size = path.stat().st_size
total += size
rel = path.relative_to(stage).as_posix()
lines.append(f"{sha256(path)} {size:>12} {rel}")
lines.append("-" * 100)
lines.append(f"Итого байт: {total}")
(stage / "MANIFEST.txt").write_text("\n".join(lines) + "\n", encoding="utf-8")
return len(files)
def write_checksums(stage: Path) -> None:
lines = []
for rel_name in KEY_FILES:
path = stage / rel_name
if path.exists():
lines.append(f"{sha256(path)} {rel_name}")
else:
print(f"ВНИМАНИЕ: нет ключевого файла {rel_name}")
(stage / "CHECKSUMS.sha256").write_text("\n".join(lines) + "\n", encoding="utf-8")
def zip_stage(stage: Path, archive: Path) -> None:
archive.parent.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(archive, "w", compression=zipfile.ZIP_DEFLATED,
compresslevel=6) as zf:
for path in sorted(stage.rglob("*")):
if not path.is_file():
continue
rel = path.relative_to(stage).as_posix()
arcname = f"{stage.name}/{rel}"
info = zipfile.ZipInfo.from_file(path, arcname)
info.compress_type = zipfile.ZIP_DEFLATED
with path.open("rb") as src, zf.open(info, "w") as dst:
shutil.copyfileobj(src, dst)
def human(n: int) -> str:
for unit in ("Б", "КБ", "МБ", "ГБ"):
if n < 1024 or unit == "ГБ":
return f"{n:.1f} {unit}" if unit != "Б" else f"{n} {unit}"
n /= 1024
return f"{n} Б"
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--name", default=DEFAULT_NAME, help="имя каталога и ZIP")
parser.add_argument("--keep-stage", action="store_true",
help="не удалять промежуточный каталог dist/stage")
args = parser.parse_args()
name = args.name
stage = DIST / "stage" / name
if stage.exists():
shutil.rmtree(stage)
stage.mkdir(parents=True, exist_ok=True)
print(f"1/4. Копирование файлов в {stage} ...")
copied = copy_tree(stage)
shutil.copy2(ROOT / "START_HERE.md", stage / "START_HERE.md")
shutil.copy2(ROOT / "ARCHIVE_README.md", stage / "ARCHIVE_README.md")
print("2/4. Формирование CHECKSUMS.sha256 ...")
write_checksums(stage)
print("3/4. Формирование MANIFEST.txt ...")
total = write_manifest(stage)
archive = DIST / f"{name}.zip"
print(f"4/4. Упаковка {archive} ...")
zip_stage(stage, archive)
digest = sha256(archive)
(DIST / f"{name}.zip.sha256").write_text(
f"{digest} {archive.name}\n", encoding="utf-8"
)
print()
print(f"Файлов в lab1: {copied}")
print(f"Всего файлов в архиве: {total + 1}")
print(f"Размер ZIP: {human(archive.stat().st_size)}")
print(f"SHA-256 ZIP: {digest}")
print(f"Архив: {archive}")
print(f"Контрольная сумма: {archive}.sha256")
if not args.keep_stage:
shutil.rmtree(stage, ignore_errors=True)
try:
(DIST / "stage").rmdir()
except OSError:
pass
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -1,664 +0,0 @@
"""Дополнительные эксперименты для итогового отчёта.
Стадии:
- nested: вложенный выбор конфигурации признаков и модели;
- topic: контроль содержания через маскирование содержательных слов;
- calibration: коррекция априорных вероятностей классов;
- ensemble: мягкое голосование и stacking;
- extra: дополнительные конфигурации и отложенная выборка.
Запуск:
python scripts/run_final_experiments.py --stages nested topic calibration ensemble extra
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from dataclasses import replace
from pathlib import Path
from typing import Any, Iterable, Sequence
import numpy as np
import pandas as pd
from sklearn.ensemble import VotingClassifier, StackingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, confusion_matrix, f1_score
from sklearn.model_selection import StratifiedKFold, train_test_split
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from stylometry.cv import run_repeated_cv
from stylometry.datasets import load_feature_frame
from stylometry.evaluation import (
bootstrap_ci,
classification_metrics,
confusion_matrix_frame,
per_class_report,
top_confusions,
)
from stylometry.pipelines import (
FeatureConfig,
build_pipeline,
preset_config,
)
DEFAULT_OUT = PROJECT_ROOT / "results" / "final"
def save_json(data: Any, path: Path) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8") as stream:
json.dump(data, stream, ensure_ascii=False, indent=2, default=str)
def save_cv(result, directory: Path, title: str, config: dict[str, Any]) -> None:
directory.mkdir(parents=True, exist_ok=True)
result.fold_metrics.to_csv(directory / "fold_metrics.csv", index=False, encoding="utf-8-sig")
result.oof.to_csv(directory / "oof_predictions.csv", index=False, encoding="utf-8-sig")
pooled, bootstrap = result.pooled_summary()
pooled.to_csv(directory / "pooled_metrics.csv", index=False, encoding="utf-8-sig")
save_json(bootstrap, directory / "bootstrap_ci.json")
first = result.oof[result.oof["repeat"] == result.oof["repeat"].min()]
per_class_report(first["y_true"], first["y_pred"], result.labels).to_csv(
directory / "per_class_report.csv", index=False, encoding="utf-8-sig"
)
matrix = confusion_matrix_frame(first["y_true"], first["y_pred"], result.labels)
matrix.to_csv(directory / "confusion_matrix.csv", encoding="utf-8-sig")
top_confusions(first["y_true"], first["y_pred"], result.labels).to_csv(
directory / "top_confusions.csv", index=False, encoding="utf-8-sig"
)
summary = {
"title": title,
"config": config,
"fold_mean_macro_f1": float(result.fold_metrics["macro_f1"].mean()),
"fold_std_macro_f1": float(result.fold_metrics["macro_f1"].std(ddof=1)),
"n_splits": result.n_splits,
"n_repeats": result.n_repeats,
"bootstrap": bootstrap,
}
save_json(summary, directory / "summary.json")
(directory / "summary.md").write_text(
f"# {title}\n\n"
f"Macro-F1 по фолдам: {summary['fold_mean_macro_f1']:.4f} "
f"± {summary['fold_std_macro_f1']:.4f}\n\n"
f"Pooled:\n\n{pooled.to_string(index=False)}\n",
encoding="utf-8",
)
def lean_config(base: FeatureConfig, lsa_components: int = 300) -> FeatureConfig:
groups = tuple(
group
for group in base.groups
if group not in {"function_words", "pos", "vocabulary", "readability"}
)
return replace(
base,
groups=groups,
lsa_variants=("content",),
lsa_components=lsa_components,
use_char_ngrams=True,
use_pos_ngrams=True,
include_lsa=True,
)
def masked_frame(frame: pd.DataFrame) -> pd.DataFrame:
"""Заменяет содержательные слова на CONTENT, сохраняя служебные слова."""
content_tags = {"NOUN", "VERB", "ADJ", "ADV", "NUM"}
def _mask_row(row: pd.Series) -> str:
tokens = list(row["tokens"])
tags = list(row["upos"])
result = []
for token, tag in zip(tokens, tags):
if tag in content_tags and re.search(r"[A-Za-z]", str(token)):
result.append("CONTENT")
else:
result.append(str(token))
return " ".join(result) if result else "__EMPTY__"
result = frame.copy()
result["masked_text"] = result.apply(_mask_row, axis=1)
result["text"] = result["masked_text"]
return result
# ---------------------------------------------------------------------------
# Nested selection
# ---------------------------------------------------------------------------
def nested_selection(
frame: pd.DataFrame,
y: np.ndarray,
out_dir: Path,
folds: int = 5,
inner_folds: int = 3,
repeats: int = 1,
seed: int = 42,
) -> None:
base = preset_config("stylometry_pos_lsa")
candidates: list[tuple[str, FeatureConfig, str]] = [
("et_full", base, "extra_trees"),
("et_no_lsa", preset_config("stylometry_pos"), "extra_trees"),
("et_lean_content300", lean_config(base, 300), "extra_trees"),
("logreg_lean_content300", lean_config(base, 300), "logreg"),
("rf_char10000", replace(base, char_max_features=10000), "random_forest"),
("rf_char15000", replace(base, char_max_features=15000), "random_forest"),
("et_char10000", replace(base, char_max_features=10000), "extra_trees"),
("et_lean_content400", lean_config(base, 400), "extra_trees"),
]
labels = np.unique(y)
rows: list[dict[str, Any]] = []
oof_rows: list[dict[str, Any]] = []
selection_rows: list[dict[str, Any]] = []
for repeat in range(repeats):
outer = StratifiedKFold(n_splits=folds, shuffle=True, random_state=seed + repeat)
for fold, (train_idx, test_idx) in enumerate(outer.split(frame, y), start=1):
print(f"[nested] repeat={repeat} fold={fold}: inner selection", flush=True)
inner = StratifiedKFold(
n_splits=inner_folds,
shuffle=True,
random_state=seed + repeat * 100 + fold,
)
inner_scores: dict[str, list[float]] = {name: [] for name, _, _ in candidates}
for inner_train, inner_test in inner.split(frame.iloc[train_idx], y[train_idx]):
for name, config, model_name in candidates:
pipeline = build_pipeline(config, model_name)
pipeline.fit(
frame.iloc[train_idx].iloc[inner_train],
y[train_idx][inner_train],
)
prediction = pipeline.predict(
frame.iloc[train_idx].iloc[inner_test]
)
inner_scores[name].append(
f1_score(
y[train_idx][inner_test],
prediction,
labels=labels,
average="macro",
zero_division=0,
)
)
means = {
name: float(np.mean(values)) for name, values in inner_scores.items()
}
best_name = max(means, key=means.get)
best_config, best_model = next(
(config, model) for name, config, model in candidates if name == best_name
)
selection_rows.append(
{
"repeat": repeat,
"fold": fold,
"selected": best_name,
"inner_mean": means[best_name],
**{f"inner_{name}": value for name, value in means.items()},
}
)
pipeline = build_pipeline(best_config, best_model)
pipeline.fit(frame.iloc[train_idx], y[train_idx])
prediction = pipeline.predict(frame.iloc[test_idx])
metrics = classification_metrics(
y[test_idx], prediction, labels=labels
)
metrics.update({"repeat": repeat, "fold": fold, "selected": best_name})
rows.append(metrics)
for sample_index, predicted in zip(test_idx, prediction):
oof_rows.append(
{
"repeat": repeat,
"fold": fold,
"line_id": int(frame.iloc[sample_index]["line_id"]),
"y_true": y[sample_index],
"y_pred": predicted,
"selected": best_name,
}
)
fold_metrics = pd.DataFrame(rows)
oof = pd.DataFrame(oof_rows).sort_values(["repeat", "line_id"]).reset_index(drop=True)
out_dir.mkdir(parents=True, exist_ok=True)
fold_metrics.to_csv(out_dir / "fold_metrics.csv", index=False, encoding="utf-8-sig")
oof.to_csv(out_dir / "oof_predictions.csv", index=False, encoding="utf-8-sig")
pd.DataFrame(selection_rows).to_csv(
out_dir / "selection.csv", index=False, encoding="utf-8-sig"
)
first = oof[oof["repeat"] == oof["repeat"].min()]
per_class_report(first["y_true"], first["y_pred"], labels).to_csv(
out_dir / "per_class_report.csv", index=False, encoding="utf-8-sig"
)
confusion_matrix_frame(first["y_true"], first["y_pred"], labels).to_csv(
out_dir / "confusion_matrix.csv", encoding="utf-8-sig"
)
pooled = []
for repeat, group in oof.groupby("repeat"):
metrics = classification_metrics(group["y_true"], group["y_pred"], labels=labels)
metrics["repeat"] = repeat
pooled.append(metrics)
pooled_frame = pd.DataFrame(pooled)
pooled_frame.to_csv(out_dir / "pooled_metrics.csv", index=False, encoding="utf-8-sig")
save_json(
{
"fold_mean_macro_f1": float(fold_metrics["macro_f1"].mean()),
"fold_std_macro_f1": float(fold_metrics["macro_f1"].std(ddof=1)),
"pooled": pooled_frame.to_dict(orient="records"),
"selection_counts": pd.DataFrame(selection_rows)["selected"].value_counts().to_dict(),
},
out_dir / "summary.json",
)
print(
f"[nested] macro-F1={fold_metrics['macro_f1'].mean():.4f} "
f"± {fold_metrics['macro_f1'].std(ddof=1):.4f}"
)
# ---------------------------------------------------------------------------
# Topic control
# ---------------------------------------------------------------------------
def topic_control(
frame: pd.DataFrame,
y: np.ndarray,
out_dir: Path,
folds: int = 5,
repeats: int = 3,
seed: int = 42,
) -> None:
style_groups = (
"length",
"punctuation",
"function_words",
"pronouns",
"pos",
"syntax",
"dialogue",
)
config = FeatureConfig(
groups=style_groups,
use_char_ngrams=True,
char_ngram_range=(2, 5),
char_max_features=5000,
use_pos_ngrams=True,
pos_ngram_range=(1, 3),
pos_max_features=500,
lsa_variants=("fw", "pos"),
lsa_components=100,
stylo_normalize=True,
include_stylo=True,
include_lsa=True,
)
masked = masked_frame(frame)
for name, data in (("original", frame), ("masked", masked)):
for model in ("extra_trees", "logreg"):
print(f"[topic] {name} / {model}", flush=True)
pipeline = build_pipeline(config, model)
result = run_repeated_cv(
pipeline,
data,
y,
n_splits=folds,
n_repeats=repeats,
seed=seed,
model_name=model,
verbose=False,
)
save_cv(
result,
out_dir / f"{name}_{model}",
f"Topic control: {name} / {model}",
{"condition": name, "model": model, "groups": style_groups},
)
# ---------------------------------------------------------------------------
# Calibration
# ---------------------------------------------------------------------------
def _prior_adjust(scores: np.ndarray, priors: np.ndarray, alpha: float) -> np.ndarray:
adjusted = scores / np.power(np.maximum(priors, 1e-12), alpha)[None, :]
row_sums = adjusted.sum(axis=1, keepdims=True)
return adjusted / np.maximum(row_sums, 1e-12)
def calibration(
frame: pd.DataFrame,
y: np.ndarray,
out_dir: Path,
folds: int = 5,
repeats: int = 5,
seed: int = 42,
) -> None:
config = preset_config("stylometry_pos_lsa")
pipeline = build_pipeline(config, "extra_trees")
print("[calibration] base CV", flush=True)
result = run_repeated_cv(
pipeline,
frame,
y,
n_splits=folds,
n_repeats=repeats,
seed=seed,
model_name="extra_trees",
verbose=False,
)
save_cv(
result,
out_dir / "base_extra_trees",
"Calibration base: extra_trees",
{"model": "extra_trees", "preset": "stylometry_pos_lsa"},
)
labels = result.labels
label_to_index = {label: index for index, label in enumerate(labels)}
priors = np.bincount(
np.array([label_to_index[value] for value in y]), minlength=len(labels)
).astype(float)
priors /= priors.sum()
alphas = [0.0, 0.25, 0.5, 0.75, 1.0]
matrix: dict[int, np.ndarray] = {}
truths: dict[int, np.ndarray] = {}
for repeat, group in result.oof.groupby("repeat"):
scores = result.scores_by_repeat.get(int(repeat))
if scores is None or scores.shape[0] != len(group):
continue
matrix[int(repeat)] = scores
truths[int(repeat)] = group["y_true"].to_numpy()
rows = []
for repeat in sorted(matrix):
other = [value for value in matrix if value != repeat]
table: dict[float, float] = {}
for alpha in alphas:
values = []
for other_repeat in other:
prediction_indices = np.argmax(
_prior_adjust(matrix[other_repeat], priors, alpha), axis=1
)
values.append(
f1_score(
truths[other_repeat],
labels[prediction_indices],
labels=labels,
average="macro",
zero_division=0,
)
)
table[alpha] = float(np.mean(values))
best_alpha = max(table, key=table.get)
baseline_pred = labels[np.argmax(matrix[repeat], axis=1)]
corrected_pred = labels[
np.argmax(_prior_adjust(matrix[repeat], priors, best_alpha), axis=1)
]
baseline = f1_score(
truths[repeat], baseline_pred, labels=labels, average="macro", zero_division=0
)
corrected = f1_score(
truths[repeat], corrected_pred, labels=labels, average="macro", zero_division=0
)
rows.append(
{
"repeat": repeat,
"selected_alpha": best_alpha,
"baseline_macro_f1": baseline,
"corrected_macro_f1": corrected,
"delta": corrected - baseline,
**{f"alpha_{alpha}": table[alpha] for alpha in alphas},
}
)
table = pd.DataFrame(rows)
out_dir.mkdir(parents=True, exist_ok=True)
table.to_csv(out_dir / "calibration.csv", index=False, encoding="utf-8-sig")
save_json(
{
"mean_baseline": float(table["baseline_macro_f1"].mean()),
"mean_corrected": float(table["corrected_macro_f1"].mean()),
"mean_delta": float(table["delta"].mean()),
"selected_alpha": table["selected_alpha"].tolist(),
},
out_dir / "summary.json",
)
print(table.to_string(index=False))
# ---------------------------------------------------------------------------
# Ensemble
# ---------------------------------------------------------------------------
def _base_estimators() -> list[tuple[str, Any]]:
base = preset_config("stylometry_pos_lsa")
return [
("et_full", build_pipeline(base, "extra_trees")),
(
"rf_char10000",
build_pipeline(replace(base, char_max_features=10000), "random_forest"),
),
(
"logreg_lean_content300",
build_pipeline(lean_config(base, 300), "logreg"),
),
]
def ensemble(
frame: pd.DataFrame,
y: np.ndarray,
out_dir: Path,
folds: int = 5,
repeats: int = 3,
seed: int = 42,
with_stacking: bool = True,
) -> None:
estimators = _base_estimators()
voting = VotingClassifier(estimators=estimators, voting="soft", n_jobs=1)
print("[ensemble] soft voting", flush=True)
result = run_repeated_cv(
voting,
frame,
y,
n_splits=folds,
n_repeats=repeats,
seed=seed,
model_name="soft_voting",
verbose=False,
)
save_cv(
result,
out_dir / "soft_voting",
"Ensemble: soft voting",
{"estimators": [name for name, _ in estimators], "voting": "soft"},
)
if with_stacking:
print("[ensemble] stacking", flush=True)
stacking = StackingClassifier(
estimators=estimators,
final_estimator=LogisticRegression(
max_iter=3000, class_weight="balanced"
),
stack_method="predict_proba",
cv=3,
n_jobs=1,
passthrough=False,
)
stacked = run_repeated_cv(
stacking,
frame,
y,
n_splits=folds,
n_repeats=1,
seed=seed,
model_name="stacking",
verbose=False,
)
save_cv(
stacked,
out_dir / "stacking",
"Ensemble: stacking",
{"estimators": [name for name, _ in estimators], "meta": "logreg"},
)
# ---------------------------------------------------------------------------
# Extra configurations
# ---------------------------------------------------------------------------
def extra(
frame: pd.DataFrame,
y: np.ndarray,
out_dir: Path,
folds: int = 5,
repeats: int = 3,
seed: int = 42,
) -> None:
base = preset_config("stylometry_pos_lsa")
configs: list[tuple[str, FeatureConfig, str]] = [
("rf_char15000", replace(base, char_max_features=15000), "random_forest"),
("logreg_lean_content400", lean_config(base, 400), "logreg"),
("logreg_lean_content500", lean_config(base, 500), "logreg"),
(
"et_block_weights",
replace(
base,
block_weights={
"stylo": 1.0,
"char": 0.7,
"pos": 0.7,
"lsa_word": 0.3,
"lsa_pos": 0.3,
},
),
"extra_trees",
),
]
for name, config, model in configs:
print(f"[extra] {name}", flush=True)
pipeline = build_pipeline(config, model)
result = run_repeated_cv(
pipeline,
frame,
y,
n_splits=folds,
n_repeats=repeats,
seed=seed,
model_name=model,
verbose=False,
)
save_cv(result, out_dir / name, f"Extra: {name}", {"model": model})
# Отложенная выборка: одно стратифицированное разбиение 80/20.
train_idx, test_idx = train_test_split(
np.arange(len(frame)), test_size=0.2, stratify=y, random_state=seed
)
holdout_rows = []
for name, config, model in (
("et_full_holdout", base, "extra_trees"),
(
"rf_char10000_holdout",
replace(base, char_max_features=10000),
"random_forest",
),
("logreg_lean_content300_holdout", lean_config(base, 300), "logreg"),
):
print(f"[extra] holdout {name}", flush=True)
pipeline = build_pipeline(config, model)
pipeline.fit(frame.iloc[train_idx], y[train_idx])
prediction = pipeline.predict(frame.iloc[test_idx])
metrics = classification_metrics(y[test_idx], prediction, labels=np.unique(y))
metrics["name"] = name
metrics["train_n"] = len(train_idx)
metrics["test_n"] = len(test_idx)
holdout_rows.append(metrics)
pd.DataFrame(holdout_rows).to_csv(
out_dir / "holdout_metrics.csv", index=False, encoding="utf-8-sig"
)
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument(
"--stages",
nargs="+",
default=["nested", "topic", "calibration", "ensemble", "extra"],
choices=["nested", "topic", "calibration", "ensemble", "extra"],
)
parser.add_argument("--out", default=str(DEFAULT_OUT))
parser.add_argument("--folds", type=int, default=5)
parser.add_argument("--repeats", type=int, default=3)
parser.add_argument("--seed", type=int, default=42)
parser.add_argument("--no-stacking", action="store_true")
args = parser.parse_args()
frame = load_feature_frame(verbose=True)
y = frame["character"].to_numpy()
output = Path(args.out)
output.mkdir(parents=True, exist_ok=True)
if "nested" in args.stages:
nested_selection(
frame,
y,
output / "nested_selection",
folds=args.folds,
inner_folds=3,
repeats=args.repeats,
seed=args.seed,
)
if "topic" in args.stages:
topic_control(
frame,
y,
output / "topic_control",
folds=args.folds,
repeats=args.repeats,
seed=args.seed,
)
if "calibration" in args.stages:
calibration(
frame,
y,
output / "calibration",
folds=args.folds,
repeats=5,
seed=args.seed,
)
if "ensemble" in args.stages:
ensemble(
frame,
y,
output / "ensemble",
folds=args.folds,
repeats=args.repeats,
seed=args.seed,
with_stacking=not args.no_stacking,
)
if "extra" in args.stages:
extra(
frame,
y,
output / "extra",
folds=args.folds,
repeats=args.repeats,
seed=args.seed,
)
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -1,130 +0,0 @@
"""Сравнение финалистов и вариантов мягкого голосования."""
from __future__ import annotations
import json
import sys
from dataclasses import replace
from pathlib import Path
import numpy as np
import pandas as pd
from sklearn.ensemble import VotingClassifier
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from stylometry.cv import run_repeated_cv
from stylometry.datasets import load_feature_frame
from stylometry.pipelines import FeatureConfig, build_pipeline, preset_config
OUT = PROJECT_ROOT / "results" / "final" / "finalists"
def lean(base: FeatureConfig, k: int) -> FeatureConfig:
groups = tuple(
group for group in base.groups
if group not in {"function_words", "pos", "vocabulary", "readability"}
)
return replace(base, groups=groups, lsa_variants=("content",), lsa_components=k)
def save(result, name: str, config: dict) -> None:
directory = OUT / name
directory.mkdir(parents=True, exist_ok=True)
result.fold_metrics.to_csv(directory / "fold_metrics.csv", index=False, encoding="utf-8-sig")
result.oof.to_csv(directory / "oof_predictions.csv", index=False, encoding="utf-8-sig")
pooled, bootstrap = result.pooled_summary()
pooled.to_csv(directory / "pooled_metrics.csv", index=False, encoding="utf-8-sig")
with (directory / "summary.json").open("w", encoding="utf-8") as stream:
json.dump(
{
"config": config,
"fold_mean_macro_f1": float(result.fold_metrics["macro_f1"].mean()),
"fold_std_macro_f1": float(result.fold_metrics["macro_f1"].std(ddof=1)),
"pooled": pooled.to_dict(orient="records"),
"bootstrap": bootstrap,
},
stream,
ensure_ascii=False,
indent=2,
default=str,
)
def main() -> int:
frame = load_feature_frame(verbose=True)
y = frame["character"].to_numpy()
base = preset_config("stylometry_pos_lsa")
configs = [
("et_full", base, "extra_trees"),
("et_char10000", replace(base, char_max_features=10000), "extra_trees"),
("et_lean_content300", lean(base, 300), "extra_trees"),
("et_lean_content400", lean(base, 400), "extra_trees"),
("logreg_lean_content300", lean(base, 300), "logreg"),
]
rows = []
for name, config, model in configs:
print(f"[finalists] {name}", flush=True)
result = run_repeated_cv(
build_pipeline(config, model), frame, y,
n_splits=5, n_repeats=5, seed=42, model_name=model, verbose=False,
)
save(result, name, {"name": name, "model": model})
pooled, bootstrap = result.pooled_summary()
first = pooled.iloc[0].to_dict()
rows.append({
"name": name,
"model": model,
"macro_f1": first.get("macro_f1"),
"accuracy": first.get("accuracy"),
"balanced_accuracy": first.get("balanced_accuracy"),
"top3_accuracy": first.get("top3_accuracy"),
"macro_f1_ci_lower": bootstrap["macro_f1"]["lower"],
"macro_f1_ci_upper": bootstrap["macro_f1"]["upper"],
})
voting_sets = {
"voting_et_char_lean": [
("et_full", build_pipeline(base, "extra_trees")),
("et_char10000", build_pipeline(replace(base, char_max_features=10000), "extra_trees")),
("logreg_lean_content300", build_pipeline(lean(base, 300), "logreg")),
],
"voting_four": [
("et_full", build_pipeline(base, "extra_trees")),
("rf_char10000", build_pipeline(replace(base, char_max_features=10000), "random_forest")),
("et_char10000", build_pipeline(replace(base, char_max_features=10000), "extra_trees")),
("logreg_lean_content300", build_pipeline(lean(base, 300), "logreg")),
],
}
for name, estimators in voting_sets.items():
print(f"[finalists] {name}", flush=True)
voting = VotingClassifier(estimators=estimators, voting="soft", n_jobs=1)
result = run_repeated_cv(
voting, frame, y, n_splits=5, n_repeats=3, seed=42,
model_name=name, verbose=False,
)
save(result, name, {"name": name, "estimators": [key for key, _ in estimators]})
pooled, bootstrap = result.pooled_summary()
first = pooled.iloc[0].to_dict()
rows.append({
"name": name,
"model": "soft_voting",
"macro_f1": first.get("macro_f1"),
"accuracy": first.get("accuracy"),
"balanced_accuracy": first.get("balanced_accuracy"),
"top3_accuracy": first.get("top3_accuracy"),
"macro_f1_ci_lower": bootstrap["macro_f1"]["lower"],
"macro_f1_ci_upper": bootstrap["macro_f1"]["upper"],
})
table = pd.DataFrame(rows).sort_values("macro_f1", ascending=False)
OUT.mkdir(parents=True, exist_ok=True)
table.to_csv(OUT / "finalists.csv", index=False, encoding="utf-8-sig")
print(table.to_string(index=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -1,824 +0,0 @@
#!/usr/bin/env bash
# Follow-up серия экспериментов по итогам предварительных прогонов.
#
# Запуск из каталога lab1:
# bash scripts/run_followup_experiments.sh
#
# С другим интерпретатором:
# PYTHON=../.venv/bin/python bash scripts/run_followup_experiments.sh
#
# Быстрая проверка (1 повтор, без сетки и seed-робастности):
# QUICK=1 bash scripts/run_followup_experiments.sh
#
# Только воспроизведение и финальная модель, без тяжёлых стадий:
# RUN_GRID=0 RUN_SEEDS=0 bash scripts/run_followup_experiments.sh
#
# Все результаты складываются в results/followup/<label>/, команды и
# сгенерированные конфиги сохраняются рядом. Общий лог:
# results/logs/run_followup_<timestamp>.log
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
cd "${PROJECT_ROOT}"
PYTHON="${PYTHON:-python}"
if [[ "${PYTHON}" == */* ]]; then
PYTHON_DIR="$(cd "$(dirname "${PYTHON}")" && pwd)"
PYTHON="${PYTHON_DIR}/$(basename "${PYTHON}")"
fi
if ! "${PYTHON}" -c "import sklearn, pandas, nltk" >/dev/null 2>&1; then
echo "ОШИБКА: в интерпретаторе ${PYTHON} не установлены зависимости."
echo "Выполните: ${PYTHON} -m pip install -r requirements.txt"
exit 1
fi
RESULTS_DIR="${PROJECT_ROOT}/results"
# FOLLOWUP_DIR можно переопределить, чтобы прогнать серию в отдельный каталог
# (например, для дымового теста) и не смешивать с основными результатами.
FOLLOWUP_DIR="${FOLLOWUP_DIR:-${RESULTS_DIR}/followup}"
CONFIG_DIR="${FOLLOWUP_DIR}/configs"
COMPARISON_DIR="${FOLLOWUP_DIR}/comparisons"
LOG_DIR="${RESULTS_DIR}/logs"
MANIFEST="${FOLLOWUP_DIR}/manifest.tsv"
REPEATS_MAIN="${REPEATS_MAIN:-5}"
REPEATS_GRID="${REPEATS_GRID:-3}"
REPEATS_SWEEP="${REPEATS_SWEEP:-3}"
REPEATS_NGRAM="${REPEATS_NGRAM:-3}"
REPEATS_FINAL="${REPEATS_FINAL:-10}"
RUN_GRID="${RUN_GRID:-1}"
RUN_SEEDS="${RUN_SEEDS:-1}"
RUN_FINAL="${RUN_FINAL:-1}"
RUN_CONTROLS="${RUN_CONTROLS:-1}"
SKIP_TESTS="${SKIP_TESTS:-0}"
FORCE="${FORCE:-0}"
SEED="${SEED:-42}"
if [[ "${QUICK:-0}" == "1" ]]; then
REPEATS_MAIN=1
REPEATS_GRID=1
REPEATS_SWEEP=1
REPEATS_NGRAM=1
REPEATS_FINAL=1
RUN_GRID=0
RUN_SEEDS=0
fi
mkdir -p "${FOLLOWUP_DIR}" "${CONFIG_DIR}" "${COMPARISON_DIR}" "${LOG_DIR}"
if [[ ! -f "${MANIFEST}" ]]; then
printf 'label\toriginal_dir\n' > "${MANIFEST}"
fi
RUN_ID="${RUN_ID:-$(date +%Y%m%d_%H%M%S)}"
LOG_FILE="${LOG_DIR}/run_followup_${RUN_ID}.log"
exec > >(tee -a "${LOG_FILE}") 2>&1
step() {
echo
echo "============================================================"
echo "== $*"
echo "============================================================"
}
run() {
echo
echo ">>> $*"
"$@"
}
write_config() {
local name="$1"
cat > "${CONFIG_DIR}/${name}.toml"
}
# Запускает команду, находит созданный ею каталог в results/ и переносит его
# в results/followup/<label>. Так у каждого эксперимента появляется
# стабильное имя, а command.txt фиксирует точную команду.
run_labeled() {
local label="$1"
shift
local before after dir dest
before="$(mktemp)"
after="$(mktemp)"
find "${RESULTS_DIR}" -maxdepth 1 -mindepth 1 -type d -printf '%f\n' | sort > "${before}"
run "$@"
find "${RESULTS_DIR}" -maxdepth 1 -mindepth 1 -type d -printf '%f\n' | sort > "${after}"
dir="$(comm -13 "${before}" "${after}" | head -n 1)"
rm -f "${before}" "${after}"
if [[ -z "${dir}" ]]; then
echo "ОШИБКА: команда не создала каталог результатов (${label})" >&2
return 1
fi
dest="${FOLLOWUP_DIR}/${label}"
if [[ -e "${dest}" ]]; then
if [[ "${FORCE}" == "1" ]]; then
rm -rf "${dest}"
else
echo "ОШИБКА: ${dest} уже существует. Задайте FORCE=1 или удалите каталог." >&2
return 1
fi
fi
mv "${RESULTS_DIR}/${dir}" "${dest}"
printf '%q ' "$@" > "${dest}/command.txt"
printf '\n' >> "${dest}/command.txt"
printf '%s\t%s\n' "${label}" "${dir}" >> "${MANIFEST}"
echo "[followup] ${label} -> ${dest}"
}
latest_dir() {
local prefix="$1"
ls -1dt "${RESULTS_DIR}/${prefix}"_* 2>/dev/null | head -n 1 || true
}
echo "Начало follow-up серии: $(date)"
echo "Интерпретатор: ${PYTHON}"
echo "Повторы: main=${REPEATS_MAIN}, grid=${REPEATS_GRID}, sweep=${REPEATS_SWEEP}, ngram=${REPEATS_NGRAM}, final=${REPEATS_FINAL}"
echo "Тяжёлые стадии: grid=${RUN_GRID}, seeds=${RUN_SEEDS}, final=${RUN_FINAL}, controls=${RUN_CONTROLS}"
echo "Каталог результатов: ${FOLLOWUP_DIR}"
echo "Лог: ${LOG_FILE}"
# ---------------------------------------------------------------------------
# Конфиги признакового пространства. CLI не умеет менять lsa_components,
# min_df, n-gram ranges и groups, поэтому параметры задаются через TOML.
# ---------------------------------------------------------------------------
step "0. Генерация конфигов"
write_config lsa_content_k300 <<'EOF'
[experiment]
preset = "lsa_content"
model = "logreg"
[features]
lsa_components = 300
EOF
write_config lsa_content_k400 <<'EOF'
[experiment]
preset = "lsa_content"
model = "logreg"
[features]
lsa_components = 400
EOF
write_config lsa_char_k300 <<'EOF'
[experiment]
preset = "lsa_char"
model = "logreg"
[features]
lsa_components = 300
EOF
write_config lsa_pos_k300 <<'EOF'
[experiment]
preset = "lsa_pos"
model = "logreg"
[features]
lsa_components = 300
EOF
write_config stylometry_pos_content300 <<'EOF'
[experiment]
preset = "stylometry_pos_lsa"
model = "logreg"
[features]
lsa_variants = ["content"]
lsa_components = 300
EOF
write_config stylometry_pos_pos300 <<'EOF'
[experiment]
preset = "stylometry_pos_lsa"
model = "logreg"
[features]
lsa_variants = ["pos"]
lsa_components = 300
EOF
write_config stylometry_pos_lsa_k300 <<'EOF'
[experiment]
preset = "stylometry_pos_lsa"
model = "logreg"
[features]
lsa_variants = ["word", "pos"]
lsa_components = 300
EOF
write_config groups_lean1 <<'EOF'
[experiment]
preset = "stylometry_pos_lsa"
model = "logreg"
[features]
groups = ["length", "lexical", "vocabulary", "characters", "punctuation", "pronouns", "syntax", "readability", "dialogue"]
EOF
write_config groups_lean2 <<'EOF'
[experiment]
preset = "stylometry_pos_lsa"
model = "logreg"
[features]
groups = ["length", "lexical", "characters", "punctuation", "pronouns", "syntax", "dialogue"]
EOF
write_config groups_lean2_content300 <<'EOF'
[experiment]
preset = "stylometry_pos_lsa"
model = "logreg"
[features]
groups = ["length", "lexical", "characters", "punctuation", "pronouns", "syntax", "dialogue"]
lsa_variants = ["content"]
lsa_components = 300
EOF
write_config min_df2 <<'EOF'
[experiment]
preset = "stylometry_pos_lsa"
model = "logreg"
[features]
min_df = 2
EOF
write_config min_df5 <<'EOF'
[experiment]
preset = "stylometry_pos_lsa"
model = "logreg"
[features]
min_df = 5
EOF
write_config char_max10000 <<'EOF'
[experiment]
preset = "stylometry_pos_lsa"
model = "logreg"
[features]
char_max_features = 10000
EOF
write_config char_ngram_2_4 <<'EOF'
[experiment]
preset = "stylometry_pos_lsa"
model = "logreg"
[features]
char_ngram_range = [2, 4]
EOF
write_config char_ngram_3_5 <<'EOF'
[experiment]
preset = "stylometry_pos_lsa"
model = "logreg"
[features]
char_ngram_range = [3, 5]
EOF
write_config pos_ngram_1_2 <<'EOF'
[experiment]
preset = "stylometry_pos_lsa"
model = "logreg"
[features]
pos_ngram_range = [1, 2]
EOF
write_config pos_ngram_1_4 <<'EOF'
[experiment]
preset = "stylometry_pos_lsa"
model = "logreg"
[features]
pos_ngram_range = [1, 4]
EOF
write_config pos_max1000 <<'EOF'
[experiment]
preset = "stylometry_pos_lsa"
model = "logreg"
[features]
pos_max_features = 1000
EOF
echo "Сгенерировано конфигов: $(find "${CONFIG_DIR}" -name '*.toml' | wc -l)"
# ---------------------------------------------------------------------------
# F0. Проверка окружения и воспроизведение опорных результатов.
# ---------------------------------------------------------------------------
step "F0. Проверка окружения и опорные запуски"
run "${PYTHON}" scripts/check_environment.py
if [[ "${SKIP_TESTS}" != "1" ]]; then
run "${PYTHON}" -m pytest -q
fi
run_labeled ref_logreg \
"${PYTHON}" -m stylometry.cli run --preset stylometry_pos_lsa \
--model logreg --folds 5 --repeats "${REPEATS_MAIN}" --seed "${SEED}"
run_labeled ref_nested_logreg \
"${PYTHON}" -m stylometry.cli run --preset stylometry_pos_lsa \
--model logreg --folds 5 --nested --seed "${SEED}"
run_labeled ref_no_lsa \
"${PYTHON}" -m stylometry.cli run --preset stylometry_pos \
--model logreg --folds 5 --repeats "${REPEATS_MAIN}" --seed "${SEED}"
run_labeled ref_stylometry \
"${PYTHON}" -m stylometry.cli run --preset stylometry \
--model logreg --folds 5 --repeats "${REPEATS_MAIN}" --seed "${SEED}"
# ---------------------------------------------------------------------------
# F1. Сравнение моделей на одном признаковом пространстве, 5 повторов.
# ---------------------------------------------------------------------------
step "F1. Сравнение моделей (5 повторов + nested)"
run_labeled model_rf_default \
"${PYTHON}" -m stylometry.cli run --preset stylometry_pos_lsa \
--model random_forest --folds 5 --repeats "${REPEATS_MAIN}" --seed "${SEED}"
run_labeled model_et_default \
"${PYTHON}" -m stylometry.cli run --preset stylometry_pos_lsa \
--model extra_trees --folds 5 --repeats "${REPEATS_MAIN}" --seed "${SEED}"
run_labeled model_svc_default \
"${PYTHON}" -m stylometry.cli run --preset stylometry_pos_lsa \
--model linear_svc --folds 5 --repeats "${REPEATS_MAIN}" --seed "${SEED}"
run_labeled model_knn_default \
"${PYTHON}" -m stylometry.cli run --preset stylometry_pos_lsa \
--model knn --folds 5 --repeats "${REPEATS_GRID}" --seed "${SEED}"
run_labeled model_rf_nested \
"${PYTHON}" -m stylometry.cli run --preset stylometry_pos_lsa \
--model random_forest --folds 5 --nested --seed "${SEED}"
run_labeled model_et_nested \
"${PYTHON}" -m stylometry.cli run --preset stylometry_pos_lsa \
--model extra_trees --folds 5 --nested --seed "${SEED}"
run_labeled model_svc_nested \
"${PYTHON}" -m stylometry.cli run --preset stylometry_pos_lsa \
--model linear_svc --folds 5 --nested --seed "${SEED}"
# ---------------------------------------------------------------------------
# F2. Сетка вокруг лучшей модели (random forest по умолчанию).
# ---------------------------------------------------------------------------
if [[ "${RUN_GRID}" == "1" ]]; then
step "F2. Сетка random forest / extra trees"
for depth in None 30 40; do
for leaf in 1 2 3; do
if [[ "${depth}" == "None" && "${leaf}" == "1" ]]; then
continue # это model_rf_default
fi
if [[ "${depth}" == "None" ]]; then
depth_json="null"
else
depth_json="${depth}"
fi
run_labeled "rf_d${depth}_l${leaf}" \
"${PYTHON}" -m stylometry.cli run --preset stylometry_pos_lsa \
--model random_forest --folds 5 --repeats "${REPEATS_GRID}" --seed "${SEED}" \
--model-params "{\"max_depth\": ${depth_json}, \"min_samples_leaf\": ${leaf}}"
done
done
for max_features in 0.05 0.1; do
for n_estimators in 400 800; do
run_labeled "rf_mf${max_features}_n${n_estimators}" \
"${PYTHON}" -m stylometry.cli run --preset stylometry_pos_lsa \
--model random_forest --folds 5 --repeats "${REPEATS_GRID}" --seed "${SEED}" \
--model-params "{\"max_features\": ${max_features}, \"n_estimators\": ${n_estimators}, \"max_depth\": null, \"min_samples_leaf\": 1}"
done
done
run_labeled rf_n800_sqrt \
"${PYTHON}" -m stylometry.cli run --preset stylometry_pos_lsa \
--model random_forest --folds 5 --repeats "${REPEATS_GRID}" --seed "${SEED}" \
--model-params '{"n_estimators": 800, "max_depth": null, "min_samples_leaf": 1}'
run_labeled rf_cw_balanced \
"${PYTHON}" -m stylometry.cli run --preset stylometry_pos_lsa \
--model random_forest --folds 5 --repeats "${REPEATS_GRID}" --seed "${SEED}" \
--model-params '{"class_weight": "balanced"}'
run_labeled rf_cw_none \
"${PYTHON}" -m stylometry.cli run --preset stylometry_pos_lsa \
--model random_forest --folds 5 --repeats "${REPEATS_GRID}" --seed "${SEED}" \
--model-params '{"class_weight": null}'
run_labeled et_dNone_l2 \
"${PYTHON}" -m stylometry.cli run --preset stylometry_pos_lsa \
--model extra_trees --folds 5 --repeats "${REPEATS_GRID}" --seed "${SEED}" \
--model-params '{"max_depth": null, "min_samples_leaf": 2}'
run_labeled et_d30_l1 \
"${PYTHON}" -m stylometry.cli run --preset stylometry_pos_lsa \
--model extra_trees --folds 5 --repeats "${REPEATS_GRID}" --seed "${SEED}" \
--model-params '{"max_depth": 30, "min_samples_leaf": 1}'
run_labeled et_d30_l2 \
"${PYTHON}" -m stylometry.cli run --preset stylometry_pos_lsa \
--model extra_trees --folds 5 --repeats "${REPEATS_GRID}" --seed "${SEED}" \
--model-params '{"max_depth": 30, "min_samples_leaf": 2}'
# Лучшая точка сетки проверяется на 5 повторах. Это разведочный выбор:
# для честной итоговой оценки нужен nested CV (см. final_rf_nested и
# обсуждение в FOLLOWUP_EXPERIMENTS.md).
step "F2.1. Лучшая точка сетки RF на 5 повторах"
BEST_JSON="$("${PYTHON}" - "${FOLLOWUP_DIR}" <<'PY'
import glob
import json
import os
import sys
import pandas as pd
root = sys.argv[1]
candidates = []
for path in glob.glob(os.path.join(root, "rf_*")):
metadata_path = os.path.join(path, "run_metadata.json")
pooled_path = os.path.join(path, "pooled_metrics.csv")
if not (os.path.exists(metadata_path) and os.path.exists(pooled_path)):
continue
with open(metadata_path, encoding="utf-8") as stream:
metadata = json.load(stream)
pooled = pd.read_csv(pooled_path)
params = metadata.get("config", {}).get("model_params") or {}
candidates.append(
(float(pooled["macro_f1"].mean()), os.path.basename(path), json.dumps(params, ensure_ascii=False))
)
if candidates:
candidates.sort(reverse=True)
print(f"{candidates[0][1]}\t{candidates[0][2]}")
PY
)"
if [[ -n "${BEST_JSON}" ]]; then
BEST_LABEL="${BEST_JSON%%$'\t'*}"
BEST_PARAMS="${BEST_JSON#*$'\t'}"
echo "[followup] лучшая точка сетки: ${BEST_LABEL}, параметры: ${BEST_PARAMS}"
run_labeled rf_grid_best_5 \
"${PYTHON}" -m stylometry.cli run --preset stylometry_pos_lsa \
--model random_forest --folds 5 --repeats "${REPEATS_MAIN}" --seed "${SEED}" \
--model-params "${BEST_PARAMS}"
else
echo "[followup] не удалось выбрать лучшую точку сетки RF"
fi
else
echo
echo "F2 пропущена (RUN_GRID=${RUN_GRID})."
fi
# ---------------------------------------------------------------------------
# F3. LSA: тема против стиля. Перебор k и подтверждение на 5 повторах.
# ---------------------------------------------------------------------------
step "F3. LSA: варианты, число компонент и объединение с признаками"
run_labeled lsa_sweep \
"${PYTHON}" -m stylometry.cli lsa-sweep \
--variants content,char,word,pos,fw --components 100,200,300,400 \
--model logreg --folds 5 --repeats "${REPEATS_SWEEP}" --seed "${SEED}"
run_labeled lsa_content_k300 \
"${PYTHON}" -m stylometry.cli run --config "${CONFIG_DIR}/lsa_content_k300.toml" \
--model logreg --folds 5 --repeats "${REPEATS_MAIN}" --seed "${SEED}"
run_labeled lsa_content_k400 \
"${PYTHON}" -m stylometry.cli run --config "${CONFIG_DIR}/lsa_content_k400.toml" \
--model logreg --folds 5 --repeats "${REPEATS_MAIN}" --seed "${SEED}"
run_labeled lsa_char_k300 \
"${PYTHON}" -m stylometry.cli run --config "${CONFIG_DIR}/lsa_char_k300.toml" \
--model logreg --folds 5 --repeats "${REPEATS_MAIN}" --seed "${SEED}"
run_labeled lsa_pos_k300 \
"${PYTHON}" -m stylometry.cli run --config "${CONFIG_DIR}/lsa_pos_k300.toml" \
--model logreg --folds 5 --repeats "${REPEATS_MAIN}" --seed "${SEED}"
run_labeled stylometry_pos_content300 \
"${PYTHON}" -m stylometry.cli run --config "${CONFIG_DIR}/stylometry_pos_content300.toml" \
--model logreg --folds 5 --repeats "${REPEATS_MAIN}" --seed "${SEED}"
run_labeled stylometry_pos_pos300 \
"${PYTHON}" -m stylometry.cli run --config "${CONFIG_DIR}/stylometry_pos_pos300.toml" \
--model logreg --folds 5 --repeats "${REPEATS_MAIN}" --seed "${SEED}"
run_labeled stylometry_pos_lsa_k300 \
"${PYTHON}" -m stylometry.cli run --config "${CONFIG_DIR}/stylometry_pos_lsa_k300.toml" \
--model logreg --folds 5 --repeats "${REPEATS_MAIN}" --seed "${SEED}"
# ---------------------------------------------------------------------------
# F4. Абляция групп признаков на 5 повторах и «худые» наборы.
# ---------------------------------------------------------------------------
step "F4. Абляция групп признаков и lean-наборы"
run_labeled ablate_full \
"${PYTHON}" -m stylometry.cli ablate --preset stylometry_pos_lsa \
--model logreg --folds 5 --repeats "${REPEATS_MAIN}" --seed "${SEED}"
for name in groups_lean1 groups_lean2 groups_lean2_content300; do
run_labeled "${name}" \
"${PYTHON}" -m stylometry.cli run --config "${CONFIG_DIR}/${name}.toml" \
--model logreg --folds 5 --repeats "${REPEATS_MAIN}" --seed "${SEED}"
done
run_labeled groups_lean1_rf \
"${PYTHON}" -m stylometry.cli run --config "${CONFIG_DIR}/groups_lean1.toml" \
--model random_forest --folds 5 --repeats "${REPEATS_MAIN}" --seed "${SEED}"
run_labeled groups_lean2_rf \
"${PYTHON}" -m stylometry.cli run --config "${CONFIG_DIR}/groups_lean2.toml" \
--model random_forest --folds 5 --repeats "${REPEATS_MAIN}" --seed "${SEED}"
# ---------------------------------------------------------------------------
# F5. Настройка символьных и POS n-грамм.
# ---------------------------------------------------------------------------
step "F5. Символьные и POS n-граммы"
for name in min_df2 min_df5 char_max10000 char_ngram_2_4 char_ngram_3_5; do
run_labeled "${name}" \
"${PYTHON}" -m stylometry.cli run --config "${CONFIG_DIR}/${name}.toml" \
--model logreg --folds 5 --repeats "${REPEATS_NGRAM}" --seed "${SEED}"
done
for name in pos_ngram_1_2 pos_ngram_1_4 pos_max1000; do
run_labeled "${name}" \
"${PYTHON}" -m stylometry.cli run --config "${CONFIG_DIR}/${name}.toml" \
--model logreg --folds 5 --repeats "${REPEATS_NGRAM}" --seed "${SEED}"
done
run_labeled min_df2_rf \
"${PYTHON}" -m stylometry.cli run --config "${CONFIG_DIR}/min_df2.toml" \
--model random_forest --folds 5 --repeats "${REPEATS_MAIN}" --seed "${SEED}"
run_labeled char_max10000_rf \
"${PYTHON}" -m stylometry.cli run --config "${CONFIG_DIR}/char_max10000.toml" \
--model random_forest --folds 5 --repeats "${REPEATS_MAIN}" --seed "${SEED}"
# ---------------------------------------------------------------------------
# F6. Балансировка классов.
# ---------------------------------------------------------------------------
step "F6. class_weight: balanced против none"
run_labeled classweight_logreg_none \
"${PYTHON}" -m stylometry.cli run --preset stylometry_pos_lsa \
--model logreg --folds 5 --repeats "${REPEATS_MAIN}" --seed "${SEED}" \
--model-params '{"class_weight": null}'
run_labeled classweight_svc_none \
"${PYTHON}" -m stylometry.cli run --preset stylometry_pos_lsa \
--model linear_svc --folds 5 --repeats "${REPEATS_MAIN}" --seed "${SEED}" \
--model-params '{"class_weight": null}'
run_labeled classweight_rf_none \
"${PYTHON}" -m stylometry.cli run --preset stylometry_pos_lsa \
--model random_forest --folds 5 --repeats "${REPEATS_MAIN}" --seed "${SEED}" \
--model-params '{"class_weight": null}'
run_labeled classweight_rf_balanced \
"${PYTHON}" -m stylometry.cli run --preset stylometry_pos_lsa \
--model random_forest --folds 5 --repeats "${REPEATS_MAIN}" --seed "${SEED}" \
--model-params '{"class_weight": "balanced"}'
# ---------------------------------------------------------------------------
# F7. Устойчивость к seed.
# ---------------------------------------------------------------------------
if [[ "${RUN_SEEDS}" == "1" ]]; then
step "F7. Устойчивость к seed (logreg и random forest)"
for seed in 7 2024 1337; do
run_labeled "seed_logreg_${seed}" \
"${PYTHON}" -m stylometry.cli run --preset stylometry_pos_lsa \
--model logreg --folds 5 --repeats "${REPEATS_MAIN}" --seed "${seed}"
run_labeled "seed_rf_${seed}" \
"${PYTHON}" -m stylometry.cli run --preset stylometry_pos_lsa \
--model random_forest --folds 5 --repeats "${REPEATS_MAIN}" --seed "${seed}" \
--model-params "{\"random_state\": ${seed}}"
done
else
echo
echo "F7 пропущена (RUN_SEEDS=${RUN_SEEDS})."
fi
# ---------------------------------------------------------------------------
# F8. Финальная модель, отчёты и независимые проверки.
# ---------------------------------------------------------------------------
if [[ "${RUN_FINAL}" == "1" ]]; then
step "F8. Финальная модель и отчёты"
run_labeled final_rf \
"${PYTHON}" -m stylometry.cli run --preset stylometry_pos_lsa \
--model random_forest --folds 5 --repeats "${REPEATS_FINAL}" --seed "${SEED}"
run_labeled final_rf_nested \
"${PYTHON}" -m stylometry.cli run --preset stylometry_pos_lsa \
--model random_forest --folds 5 --nested --seed "${SEED}"
FINAL_DIR="${FOLLOWUP_DIR}/final_rf"
if [[ -d "${FINAL_DIR}" ]]; then
run "${PYTHON}" -m stylometry.cli length-report \
--run-dir "${FINAL_DIR}" --seed "${SEED}"
for group in gender age_group species; do
run "${PYTHON}" -m stylometry.cli subgroup-report \
--run-dir "${FINAL_DIR}" --group "${group}"
done
run "${PYTHON}" -m stylometry.cli compare \
--run-a "${FINAL_DIR}" --run-b "${FOLLOWUP_DIR}/ref_logreg" \
--out "${COMPARISON_DIR}/final_rf_vs_ref_logreg.json" --seed "${SEED}"
run "${PYTHON}" -m stylometry.cli compare \
--run-a "${FINAL_DIR}" --run-b "${FOLLOWUP_DIR}/ref_no_lsa" \
--out "${COMPARISON_DIR}/final_rf_vs_ref_no_lsa.json" --seed "${SEED}"
fi
run_labeled verify_full_42 \
"${PYTHON}" -m stylometry.cli verify --preset stylometry_pos_lsa \
--folds 5 --seed 42
run_labeled verify_full_7 \
"${PYTHON}" -m stylometry.cli verify --preset stylometry_pos_lsa \
--folds 5 --seed 7
run_labeled verify_full_2024 \
"${PYTHON}" -m stylometry.cli verify --preset stylometry_pos_lsa \
--folds 5 --seed 2024
run_labeled delta_cosine \
"${PYTHON}" -m stylometry.cli delta --features 500,1000,2000 \
--metrics cosine --folds 5 --repeats "${REPEATS_GRID}" --seed "${SEED}"
for clusters in 3 5 8; do
run_labeled "cluster_k${clusters}" \
"${PYTHON}" -m stylometry.cli cluster --preset stylometry_pos_lsa \
--clusters "${clusters}" --seed "${SEED}"
done
else
echo
echo "F8 пропущена (RUN_FINAL=${RUN_FINAL})."
fi
# ---------------------------------------------------------------------------
# F9. Контроли: нижние границы, near-duplicates и перестановка меток.
# ---------------------------------------------------------------------------
if [[ "${RUN_CONTROLS}" == "1" ]]; then
step "F9. Контроли"
run_labeled control_length_logreg \
"${PYTHON}" -m stylometry.cli run --preset length \
--model logreg --folds 5 --repeats "${REPEATS_MAIN}" --seed "${SEED}"
run_labeled control_length_dummy \
"${PYTHON}" -m stylometry.cli run --preset length \
--model dummy --folds 5 --repeats 1 --seed "${SEED}"
step "F9.1. Поиск near-duplicates"
"${PYTHON}" - "${FOLLOWUP_DIR}" <<'PY'
from pathlib import Path
import sys
import numpy as np
import pandas as pd
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
from stylometry.datasets import load_feature_frame
frame = load_feature_frame(verbose=False)
matrix = TfidfVectorizer(analyzer="char_wb", ngram_range=(3, 5), min_df=1).fit_transform(
frame["text"]
)
similarity = cosine_similarity(matrix)
rows = np.triu_indices_from(similarity, k=1)
values = similarity[rows]
threshold = 0.8
mask = values >= threshold
pairs = pd.DataFrame(
{
"line_id_a": frame.iloc[rows[0][mask]]["line_id"].to_numpy(),
"character_a": frame.iloc[rows[0][mask]]["character"].to_numpy(),
"text_a": frame.iloc[rows[0][mask]]["text"].to_numpy(),
"line_id_b": frame.iloc[rows[1][mask]]["line_id"].to_numpy(),
"character_b": frame.iloc[rows[1][mask]]["character"].to_numpy(),
"text_b": frame.iloc[rows[1][mask]]["text"].to_numpy(),
"cosine": values[mask],
"same_character": frame.iloc[rows[0][mask]]["character"].to_numpy()
== frame.iloc[rows[1][mask]]["character"].to_numpy(),
}
).sort_values("cosine", ascending=False)
out_dir = Path(sys.argv[1]) / "negative_control"
out_dir.mkdir(parents=True, exist_ok=True)
pairs.to_csv(out_dir / "near_duplicates.csv", index=False, encoding="utf-8-sig")
print(f"[near-duplicates] пар с cosine >= {threshold}: {len(pairs)}")
if not pairs.empty:
print(pairs.head(10).to_string(index=False))
PY
step "F9.2. Контроль перестановкой меток"
"${PYTHON}" - "${FOLLOWUP_DIR}" <<'PY'
from pathlib import Path
import sys
import numpy as np
import pandas as pd
from stylometry.cv import run_repeated_cv
from stylometry.datasets import load_feature_frame
from stylometry.pipelines import build_pipeline, preset_config
frame = load_feature_frame(verbose=False)
y = frame["character"].to_numpy()
config = preset_config("stylometry_pos_lsa")
rows = []
for seed in (42, 7, 2024):
rng = np.random.default_rng(seed)
shuffled = rng.permutation(y)
pipeline = build_pipeline(config, "logreg")
result = run_repeated_cv(
pipeline,
frame,
shuffled,
n_splits=5,
n_repeats=1,
seed=seed,
model_name="permuted_logreg",
verbose=False,
)
pooled, _ = result.pooled_summary()
row = pooled.iloc[0].to_dict()
row["permutation_seed"] = seed
rows.append(row)
out_dir = Path(sys.argv[1]) / "negative_control"
out_dir.mkdir(parents=True, exist_ok=True)
table = pd.DataFrame(rows)
table.to_csv(out_dir / "permutation_logreg.csv", index=False, encoding="utf-8-sig")
print(table[["permutation_seed", "macro_f1", "accuracy", "top3_accuracy"]].to_string(index=False))
print(f"[permutation] средний macro-F1: {table['macro_f1'].mean():.4f}")
PY
else
echo
echo "F9 пропущена (RUN_CONTROLS=${RUN_CONTROLS})."
fi
# ---------------------------------------------------------------------------
# Сводка по всем запускам и парные сравнения по повторам.
# ---------------------------------------------------------------------------
step "Сводка follow-up"
PAIRS=(
"model_rf_default:ref_logreg"
"model_rf_default:ref_no_lsa"
"model_rf_default:model_svc_default"
"rf_grid_best_5:model_rf_default"
"model_svc_default:ref_logreg"
"ref_no_lsa:ref_logreg"
"stylometry_pos_content300:ref_no_lsa"
"stylometry_pos_pos300:ref_no_lsa"
"stylometry_pos_lsa_k300:ref_logreg"
"groups_lean1:ref_logreg"
"groups_lean2:ref_logreg"
"groups_lean2_content300:ref_logreg"
"groups_lean1_rf:model_rf_default"
"groups_lean2_rf:model_rf_default"
"min_df2:ref_logreg"
"char_max10000:ref_logreg"
"char_ngram_2_4:ref_logreg"
"char_ngram_3_5:ref_logreg"
"pos_ngram_1_2:ref_logreg"
"pos_ngram_1_4:ref_logreg"
"pos_max1000:ref_logreg"
"min_df2_rf:model_rf_default"
"char_max10000_rf:model_rf_default"
"classweight_logreg_none:ref_logreg"
"classweight_svc_none:model_svc_default"
"classweight_rf_none:model_rf_default"
"classweight_rf_balanced:model_rf_default"
"final_rf:ref_logreg"
"final_rf:ref_no_lsa"
"final_rf:model_rf_default"
"final_rf:final_rf_nested"
"control_length_logreg:ref_logreg"
)
run "${PYTHON}" scripts/analyze_followup.py \
--followup-dir "${FOLLOWUP_DIR}" \
--pairs "${PAIRS[@]}" \
--n-boot 1000 --seed "${SEED}"
step "Готово"
echo "Конец follow-up серии: $(date)"
echo
echo "Результаты: ${FOLLOWUP_DIR}"
echo "Сводка: ${FOLLOWUP_DIR}/summary.csv"
echo "Сравнения: ${FOLLOWUP_DIR}/comparisons.csv"
echo "Лог: ${LOG_FILE}"

View File

@@ -0,0 +1,140 @@
#!/usr/bin/env bash
# Тюнинг поверх полной серии scripts/run_all_experiments.sh.
# Не повторяет E0–E16: только гиперпараметры, состав признаков, веса, k LSA.
#
# Запуск из каталога lab1:
# bash scripts/run_tuning_experiments.sh
#
# Быстрый черновик:
# REPEATS=1 bash scripts/run_tuning_experiments.sh
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
cd "${PROJECT_ROOT}"
PYTHON="${PYTHON:-python}"
REPEATS="${REPEATS:-3}"
SEED="${SEED:-42}"
PRESET="${PRESET:-stylometry_pos_lsa}"
LOG_DIR="${PROJECT_ROOT}/results/logs"
mkdir -p "${LOG_DIR}"
RUN_ID="${RUN_ID:-$(date +%Y%m%d_%H%M%S)}"
LOG_FILE="${LOG_DIR}/tune_${RUN_ID}.log"
exec > >(tee -a "${LOG_FILE}") 2>&1
step() {
echo
echo "============================================================"
echo "== $*"
echo "============================================================"
}
run() {
echo
echo ">>> $*"
"$@"
}
echo "Тюнинг: $(date)"
echo "Интерпретатор: ${PYTHON}"
echo "Повторы: ${REPEATS}, seed: ${SEED}, пресет: ${PRESET}"
echo "Лог: ${LOG_FILE}"
step "T1. Logreg: C"
for c in 0.03 0.1 0.3 1 3 10; do
run "${PYTHON}" -m stylometry.cli run \
--preset "${PRESET}" --model logreg \
--repeats "${REPEATS}" --seed "${SEED}" \
--model-params "{\"C\": ${c}}"
done
step "T2. Linear SVM: C"
for c in 0.03 0.1 0.3 1 3 10; do
run "${PYTHON}" -m stylometry.cli run \
--preset "${PRESET}" --model linear_svc \
--repeats "${REPEATS}" --seed "${SEED}" \
--model-params "{\"C\": ${c}}"
done
step "T3. Logreg без балансировки классов"
run "${PYTHON}" -m stylometry.cli run \
--preset "${PRESET}" --model logreg \
--repeats "${REPEATS}" --seed "${SEED}" \
--model-params '{"class_weight": null}'
step "T4. Без vocabulary и readability"
run "${PYTHON}" -m stylometry.cli run \
--config configs/tune_no_vocab_readability.toml \
--preset "${PRESET}" --model logreg \
--repeats "${REPEATS}" --seed "${SEED}"
step "T5. Без vocabulary, readability и length"
run "${PYTHON}" -m stylometry.cli run \
--config configs/tune_no_vocab_readability_length.toml \
--preset "${PRESET}" --model logreg \
--repeats "${REPEATS}" --seed "${SEED}"
step "T6. Char/POS n-граммы"
run "${PYTHON}" -m stylometry.cli run \
--config configs/tune_char_narrow.toml \
--preset "${PRESET}" --model logreg \
--repeats "${REPEATS}" --seed "${SEED}"
run "${PYTHON}" -m stylometry.cli run \
--config configs/tune_char_wide.toml \
--preset "${PRESET}" --model logreg \
--repeats "${REPEATS}" --seed "${SEED}"
step "T7. Веса блоков"
run "${PYTHON}" -m stylometry.cli run \
--config configs/tune_block_weights.toml \
--preset "${PRESET}" --model logreg \
--repeats "${REPEATS}" --seed "${SEED}"
step "T8. LSA k=150 и k=300 (в полной серии были 50/100/200)"
run "${PYTHON}" -m stylometry.cli run \
--config configs/tune_lsa_k150.toml \
--preset "${PRESET}" --model logreg \
--repeats "${REPEATS}" --seed "${SEED}"
run "${PYTHON}" -m stylometry.cli run \
--config configs/tune_lsa_k300.toml \
--preset "${PRESET}" --model logreg \
--repeats "${REPEATS}" --seed "${SEED}"
run "${PYTHON}" -m stylometry.cli lsa-sweep \
--variants word,content,fw,char,pos \
--components 150,300 \
--model logreg --repeats 1 --seed "${SEED}"
step "T9. Случайный лес: глубина и лист"
for depth in 10 20 30; do
for leaf in 2 3 5; do
run "${PYTHON}" -m stylometry.cli run \
--preset "${PRESET}" --model random_forest \
--repeats "${REPEATS}" --seed "${SEED}" \
--model-params "{\"max_depth\": ${depth}, \"min_samples_leaf\": ${leaf}}"
done
done
run "${PYTHON}" -m stylometry.cli run \
--preset "${PRESET}" --model random_forest \
--repeats "${REPEATS}" --seed "${SEED}" \
--model-params '{"max_depth": 20, "min_samples_leaf": 3, "max_features": 0.1}'
step "T10. Extra trees (не было в benchmark полной серии)"
run "${PYTHON}" -m stylometry.cli run \
--preset "${PRESET}" --model extra_trees \
--repeats "${REPEATS}" --seed "${SEED}" \
--model-params '{"max_depth": 20, "min_samples_leaf": 3}'
step "T11. Дельта: 50 и 1000 MFW (100/200/500 уже в полной серии)"
run "${PYTHON}" -m stylometry.cli delta \
--features 50,1000 --metrics manhattan,cosine \
--repeats 2 --seed "${SEED}"
step "Готово"
echo "Конец тюнинга: $(date)"
echo "Лог: ${LOG_FILE}"
echo
echo "Сравнить два каталога:"
echo " python -m stylometry.cli compare --run-a results/<A> --run-b results/<B>"