Compare commits
12 Commits
5a4a19e0df
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 32b61750bf | |||
| 09fc05c327 | |||
| 60b198f78e | |||
| 800da29fe7 | |||
| db843d33c5 | |||
| d8b6f07f26 | |||
| 8830008d0c | |||
| 9b17e37a38 | |||
| 73c32d10c4 | |||
| c3769b1d4a | |||
|
|
ac7c6f0b92 | ||
|
|
2c6bf4653a |
4
.gitignore
vendored
@@ -8,9 +8,5 @@ __pycache__/
|
|||||||
lab1/nltk_data/
|
lab1/nltk_data/
|
||||||
lab1/data/processed/
|
lab1/data/processed/
|
||||||
lab1/features/
|
lab1/features/
|
||||||
lab1/exports/
|
|
||||||
lab1/results/*
|
lab1/results/*
|
||||||
!lab1/results/README.md
|
!lab1/results/README.md
|
||||||
|
|
||||||
*.tar.gz
|
|
||||||
*.tar.gz.sha256
|
|
||||||
|
|||||||
10
README.md
@@ -53,5 +53,15 @@ 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) — готовая
|
||||||
|
презентация, сборка и исходные изображения.
|
||||||
|
|||||||
15
lab1/.gitignore
vendored
@@ -7,4 +7,17 @@ __pycache__/
|
|||||||
/features/
|
/features/
|
||||||
/results/*
|
/results/*
|
||||||
!/results/README.md
|
!/results/README.md
|
||||||
/exports/
|
|
||||||
|
/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/
|
||||||
|
|||||||
529
lab1/ARCHIVE_README.md
Normal file
@@ -0,0 +1,529 @@
|
|||||||
|
# Архив лабораторной работы №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
|
||||||
|
```
|
||||||
674
lab1/FINAL_REPORT.md
Normal file
@@ -0,0 +1,674 @@
|
|||||||
|
# Итоговый отчёт по лабораторной работе
|
||||||
|
|
||||||
|
## Стилометрическая классификация реплик персонажей 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`.
|
||||||
30
lab1/FINAL_RESULTS.csv
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
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
|
||||||
|
636
lab1/FOLLOWUP_EXPERIMENTS.md
Normal file
@@ -0,0 +1,636 @@
|
|||||||
|
# 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 внутри группы
|
||||||
|
персонажей, а не качество предсказания самой группы.
|
||||||
@@ -136,6 +136,51 @@ 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` задаёт параметры по умолчанию:
|
||||||
@@ -154,6 +199,10 @@ python -m stylometry.cli run --config configs/default.toml \
|
|||||||
|
|
||||||
## Основные документы
|
## Основные документы
|
||||||
|
|
||||||
|
- **`START_HERE.md`** — краткая инструкция: что открыть и как проверить
|
||||||
|
окружение за три минуты.
|
||||||
|
- **`ARCHIVE_README.md`** — подробное описание архива: состав, установка,
|
||||||
|
запуск, структура модулей, пересборка отчёта и презентации.
|
||||||
- **`RUNBOOK.md`** — пошаговая инструкция для запуска полной серии
|
- **`RUNBOOK.md`** — пошаговая инструкция для запуска полной серии
|
||||||
экспериментов: установка, проверка окружения, все команды, упаковка
|
экспериментов: установка, проверка окружения, все команды, упаковка
|
||||||
результатов и типичные ошибки.
|
результатов и типичные ошибки.
|
||||||
@@ -164,6 +213,14 @@ 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`** — методическое описание метрик и
|
||||||
классификаторов: что показывает каждая метрика, как она считается, как
|
классификаторов: что показывает каждая метрика, как она считается, как
|
||||||
интерпретировать значения, чем отличаются модели и как объяснять это на
|
интерпретировать значения, чем отличаются модели и как объяснять это на
|
||||||
|
|||||||
389
lab1/RESULTS_ANALYSIS.md
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
# Анализ результатов 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-граммы, а не
|
||||||
|
> функциональные слова и синтаксис. Поэтому результат корректно
|
||||||
|
> интерпретировать как различимость речевых масок по лексико-тематическому
|
||||||
|
> профилю; для утверждения о «чистом стиле» необходимы тематический контроль
|
||||||
|
> и внешняя валидация.
|
||||||
@@ -464,6 +464,34 @@ 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. Если серия прервалась
|
||||||
|
|
||||||
Скрипт останавливается при первой ошибке. Уже созданные каталоги результатов
|
Скрипт останавливается при первой ошибке. Уже созданные каталоги результатов
|
||||||
|
|||||||
81
lab1/START_HERE.md
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# С чего начать
|
||||||
|
|
||||||
|
Это архив лабораторной работы №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; корпус используется
|
||||||
|
исключительно в учебных и исследовательских целях.
|
||||||
BIN
lab1/dist/nlp_lab1_submission_20261101.zip
vendored
Normal file
1
lab1/dist/nlp_lab1_submission_20261101.zip.sha256
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
635ed9d56653259be30733fe8b80316e8ce1f3b32ac178170f5deef0f3e39d79 nlp_lab1_submission_20261101.zip
|
||||||
BIN
lab1/exports/nlp_lab1_final_results_20261026_120000.tar.gz
Normal file
@@ -0,0 +1 @@
|
|||||||
|
fd4aecdf5f7ecfe74b55324f0b4700e25adaa9f6bdf035edbd3017e58152838f exports/nlp_lab1_final_results_20261026_120000.tar.gz
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
Дата: 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.
|
||||||
BIN
lab1/exports/nlp_lab1_results_20261025_173103.tar.gz
Normal file
@@ -0,0 +1 @@
|
|||||||
|
fac6cd6286209a20afa732dd8da257fe39208e00cdc8542e73496800ca0aff5e /home/folen/Homework/NLP/lab1/exports/nlp_lab1_results_20261025_173103.tar.gz
|
||||||
254
lab1/exports/nlp_lab1_results_20261025_173103_manifest.txt
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
Дата: Вс 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
|
||||||
BIN
lab1/exports/nlp_lab1_results_20261025_192444.tar.gz
Normal file
@@ -0,0 +1 @@
|
|||||||
|
866cbec84ad3c79cdf7cdee14a534d736894037f68af88e4629262a5821cbefb /home/folen/Homework/NLP/lab1/exports/nlp_lab1_results_20261025_192444.tar.gz
|
||||||
486
lab1/exports/nlp_lab1_results_20261025_192444_manifest.txt
Normal file
@@ -0,0 +1,486 @@
|
|||||||
|
Дата: Вс 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
|
||||||
BIN
lab1/exports/nlp_lab1_results_20261026_054117.tar.gz
Normal file
@@ -0,0 +1 @@
|
|||||||
|
f9566b75e0474a72271263bf203f892979e638750cddc980be9a69f690aaa884 /home/folen/Homework/NLP/lab1/exports/nlp_lab1_results_20261026_054117.tar.gz
|
||||||
493
lab1/exports/nlp_lab1_results_20261026_054117_manifest.txt
Normal file
@@ -0,0 +1,493 @@
|
|||||||
|
Дата: Пн 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
|
||||||
65
lab1/presentation/README.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# Презентация
|
||||||
|
|
||||||
|
Готовая презентация: `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-меры по персонажам, приложение с полной матрицей и финальный мем.
|
||||||
|
Формул почти нет: одна компактная запись обозначений на слайде постановки
|
||||||
|
задачи.
|
||||||
809
lab1/presentation/build_deck.py
Normal file
@@ -0,0 +1,809 @@
|
|||||||
|
"""Сборка презентации по лабораторной работе.
|
||||||
|
|
||||||
|
Требования:
|
||||||
|
- 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())
|
||||||
BIN
lab1/presentation/generated/characters.png
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
lab1/presentation/generated/confusion_full34.png
Normal file
|
After Width: | Height: | Size: 178 KiB |
BIN
lab1/presentation/generated/confusion_pairs.png
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
lab1/presentation/generated/confusion_top12.png
Normal file
|
After Width: | Height: | Size: 238 KiB |
BIN
lab1/presentation/generated/feature_progression.png
Normal file
|
After Width: | Height: | Size: 141 KiB |
BIN
lab1/presentation/generated/hub_shares.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
lab1/presentation/generated/meme.png
Normal file
|
After Width: | Height: | Size: 678 KiB |
BIN
lab1/presentation/generated/model_comparison.png
Normal file
|
After Width: | Height: | Size: 132 KiB |
BIN
lab1/presentation/generated/per_class_f1.png
Normal file
|
After Width: | Height: | Size: 240 KiB |
BIN
lab1/presentation/generated/topic_control.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
lab1/presentation/images/background.jpg
Normal file
|
After Width: | Height: | Size: 238 KiB |
BIN
lab1/presentation/images/characters.webp
Normal file
|
After Width: | Height: | Size: 314 KiB |
BIN
lab1/presentation/images/chicken.png
Normal file
|
After Width: | Height: | Size: 411 B |
BIN
lab1/presentation/images/median.png
Normal file
|
After Width: | Height: | Size: 680 KiB |
BIN
lab1/presentation/images/meme.webp
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
lab1/presentation/images/replics-count.png
Normal file
|
After Width: | Height: | Size: 795 KiB |
BIN
lab1/presentation/images/title.jpg
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
lab1/presentation/latex/stardew_stylometry.pdf
Normal file
985
lab1/presentation/latex/stardew_stylometry.tex
Normal file
@@ -0,0 +1,985 @@
|
|||||||
|
% !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}
|
||||||
263
lab1/presentation/make_slide_figures.py
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
"""Графики для презентации: матрицы ошибок, сравнение моделей, ошибки по классам."""
|
||||||
|
|
||||||
|
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())
|
||||||
3
lab1/presentation/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
python-pptx>=1.0,<2
|
||||||
|
Pillow>=10
|
||||||
|
matplotlib>=3.8
|
||||||
251
lab1/presentation/rework_block1.py
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
"""Переработка первого блока текущей презентации.
|
||||||
|
|
||||||
|
Скрипт не пересобирает весь файл: он читает уже отредактированный 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())
|
||||||
BIN
lab1/presentation/stardew_stylometry.pdf
Normal file
BIN
lab1/presentation/stardew_stylometry.pptx
Normal file
BIN
lab1/report/REPORT.docx
Normal file
652
lab1/report/REPORT.md
Normal file
@@ -0,0 +1,652 @@
|
|||||||
|
---
|
||||||
|
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 |
|
||||||
|
|
||||||
|
{width=70%}
|
||||||
|
|
||||||
|
{width=82%}
|
||||||
|
|
||||||
|
{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.
|
||||||
|
|
||||||
|
{width=82%}
|
||||||
|
|
||||||
|
## 6.2. Вклад признаков
|
||||||
|
|
||||||
|
| Условие | Macro-F1 |
|
||||||
|
|---|---:|
|
||||||
|
| Только длина | 0,035 |
|
||||||
|
| Только ручные стилометрические признаки | 0,105 |
|
||||||
|
| Ручные + символьные и частеречные n-граммы | 0,182 |
|
||||||
|
| То же + `content`-LSA | 0,249 |
|
||||||
|
| ExtraTrees с расширенным символьным словарём | 0,276 |
|
||||||
|
| Мягкое голосование | 0,304 |
|
||||||
|
|
||||||
|
{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 |
|
||||||
|
|
||||||
|
{width=68%}
|
||||||
|
|
||||||
|
Падение более чем вдвое показывает, что значительная часть качества связана
|
||||||
|
с лексическим содержанием. Остаток 0,113 всё ещё в 3,9 раза выше случайного
|
||||||
|
уровня: некоторый стилевой сигнал сохраняется, но он слабее тематического.
|
||||||
|
|
||||||
|
## 6.4. Матрицы ошибок
|
||||||
|
|
||||||
|
Строки матрицы — истинный персонаж, столбцы — предсказание модели, диагональ
|
||||||
|
— правильные ответы, вне диагонали — ошибки. Полная матрица 34×34 на доске
|
||||||
|
нечитаема; поэтому в основном тексте показаны 12 самых частых персонажей и
|
||||||
|
столбец «прочие», а также 18 самых частых пар ошибок.
|
||||||
|
|
||||||
|
{width=92%}
|
||||||
|
|
||||||
|
{width=92%}
|
||||||
|
|
||||||
|
Основные пары ошибок (за три повтора): Sebastian → Abigail (65),
|
||||||
|
Sebastian → Alex (61), Abigail → Sebastian (59), Abigail → Alex (56).
|
||||||
|
Крупнейший клубок — молодые персонажи Abigail, Alex, Sebastian, Haley, Sam.
|
||||||
|
Многие из них писались в одних и тех же обновлениях и сюжетных контекстах,
|
||||||
|
поэтому сходство голосов ожидаемо; это не разные авторы.
|
||||||
|
|
||||||
|
## 6.5. Ошибки по персонажам
|
||||||
|
|
||||||
|
{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.
|
||||||
|
|
||||||
|
{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. Полная матрица ошибок
|
||||||
|
|
||||||
|
{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.
|
||||||
|
|
||||||
BIN
lab1/report/REPORT.pdf
Normal file
BIN
lab1/report/figures/characters.png
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
lab1/report/figures/confusion_full34.png
Normal file
|
After Width: | Height: | Size: 178 KiB |
BIN
lab1/report/figures/confusion_pairs.png
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
lab1/report/figures/confusion_top12.png
Normal file
|
After Width: | Height: | Size: 238 KiB |
BIN
lab1/report/figures/feature_progression.png
Normal file
|
After Width: | Height: | Size: 141 KiB |
BIN
lab1/report/figures/hub_shares.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
lab1/report/figures/median.png
Normal file
|
After Width: | Height: | Size: 680 KiB |
BIN
lab1/report/figures/model_comparison.png
Normal file
|
After Width: | Height: | Size: 132 KiB |
BIN
lab1/report/figures/per_class_f1.png
Normal file
|
After Width: | Height: | Size: 240 KiB |
BIN
lab1/report/figures/replics-count.png
Normal file
|
After Width: | Height: | Size: 795 KiB |
BIN
lab1/report/figures/topic_control.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
5
lab1/requirements-report.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Дополнительные зависимости для пересборки отчёта.
|
||||||
|
# Основные зависимости проекта — в requirements.txt.
|
||||||
|
python-docx>=1.1,<2
|
||||||
|
PyMuPDF>=1.24,<2
|
||||||
|
Pillow>=10,<13
|
||||||
@@ -8,3 +8,4 @@ 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
|
||||||
|
|||||||
336
lab1/scripts/analyze_followup.py
Executable file
@@ -0,0 +1,336 @@
|
|||||||
|
#!/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())
|
||||||
570
lab1/scripts/build_report.py
Normal file
@@ -0,0 +1,570 @@
|
|||||||
|
#!/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())
|
||||||
208
lab1/scripts/package_submission.py
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
#!/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())
|
||||||
664
lab1/scripts/run_final_experiments.py
Normal file
@@ -0,0 +1,664 @@
|
|||||||
|
"""Дополнительные эксперименты для итогового отчёта.
|
||||||
|
|
||||||
|
Стадии:
|
||||||
|
- 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())
|
||||||
130
lab1/scripts/run_finalists.py
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
"""Сравнение финалистов и вариантов мягкого голосования."""
|
||||||
|
|
||||||
|
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())
|
||||||
824
lab1/scripts/run_followup_experiments.sh
Executable file
@@ -0,0 +1,824 @@
|
|||||||
|
#!/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}"
|
||||||