Files
NLP/lab1/FOLLOWUP_EXPERIMENTS.md
2026-10-26 05:44:25 +03:00

637 lines
46 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 внутри группы
персонажей, а не качество предсказания самой группы.