Добавлены параметры моделей, веса блоков и руководство по настройке
This commit is contained in:
@@ -52,5 +52,6 @@ nlp/
|
|||||||
- [`lab1/METRICS_AND_CLASSIFIERS.md`](lab1/METRICS_AND_CLASSIFIERS.md) —
|
- [`lab1/METRICS_AND_CLASSIFIERS.md`](lab1/METRICS_AND_CLASSIFIERS.md) —
|
||||||
методическое описание метрик и классификаторов.
|
методическое описание метрик и классификаторов.
|
||||||
- [`lab1/FEATURES.md`](lab1/FEATURES.md) — признаки и формулы.
|
- [`lab1/FEATURES.md`](lab1/FEATURES.md) — признаки и формулы.
|
||||||
|
- [`lab1/TUNING.md`](lab1/TUNING.md) — настройка параметров и перебор.
|
||||||
- [`lab1/PRESENTATION_OUTLINE.md`](lab1/PRESENTATION_OUTLINE.md) — структура
|
- [`lab1/PRESENTATION_OUTLINE.md`](lab1/PRESENTATION_OUTLINE.md) — структура
|
||||||
презентации.
|
презентации.
|
||||||
|
|||||||
@@ -162,6 +162,8 @@ python -m stylometry.cli run --config configs/default.toml \
|
|||||||
ограничения и чек-лист для следующего исследователя.
|
ограничения и чек-лист для следующего исследователя.
|
||||||
- **`FEATURES.md`** — состав стилометрических признаков, формулы и обоснование
|
- **`FEATURES.md`** — состав стилометрических признаков, формулы и обоснование
|
||||||
выбора; дополняет машинно-читаемый `features/feature_dictionary.csv`.
|
выбора; дополняет машинно-читаемый `features/feature_dictionary.csv`.
|
||||||
|
- **`TUNING.md`** — где менять параметры классификаторов, признаки, LSA и веса
|
||||||
|
блоков; рекомендуемый порядок улучшения результатов.
|
||||||
- **`METRICS_AND_CLASSIFIERS.md`** — методическое описание метрик и
|
- **`METRICS_AND_CLASSIFIERS.md`** — методическое описание метрик и
|
||||||
классификаторов: что показывает каждая метрика, как она считается, как
|
классификаторов: что показывает каждая метрика, как она считается, как
|
||||||
интерпретировать значения, чем отличаются модели и как объяснять это на
|
интерпретировать значения, чем отличаются модели и как объяснять это на
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
Полный протокол: `EXPERIMENTS.md`.
|
Полный протокол: `EXPERIMENTS.md`.
|
||||||
Описание признаков: `FEATURES.md`.
|
Описание признаков: `FEATURES.md`.
|
||||||
Описание метрик и классификаторов: `METRICS_AND_CLASSIFIERS.md`.
|
Описание метрик и классификаторов: `METRICS_AND_CLASSIFIERS.md`.
|
||||||
|
Настройка параметров и перебор: `TUNING.md`.
|
||||||
Структура презентации: `PRESENTATION_OUTLINE.md`.
|
Структура презентации: `PRESENTATION_OUTLINE.md`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
455
lab1/TUNING.md
Normal file
455
lab1/TUNING.md
Normal file
@@ -0,0 +1,455 @@
|
|||||||
|
# Настройка моделей и признаков
|
||||||
|
|
||||||
|
Документ отвечает на практический вопрос: **где и какие параметры менять,
|
||||||
|
чтобы пытаться улучшить macro-F1**. Он не заменяет `EXPERIMENTS.md`: там
|
||||||
|
описан протокол, здесь — точки настройки и рекомендуемый порядок перебора.
|
||||||
|
|
||||||
|
Главное правило: настраивать параметры можно только по обучающим блокам
|
||||||
|
перекрёстной проверки. Нельзя выбирать признаки, число компонент LSA или
|
||||||
|
гиперпараметры по тестовой выборке: это даёт оптимистическую оценку и
|
||||||
|
не воспроизводится.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Где что находится
|
||||||
|
|
||||||
|
| Что менять | Файл | Где именно |
|
||||||
|
|---|---|---|
|
||||||
|
| Параметры классификаторов | `stylometry/pipelines.py` | `make_classifier` |
|
||||||
|
| Сетки для вложенной CV | `stylometry/pipelines.py` | `PARAM_GRIDS` |
|
||||||
|
| Состав блоков признаков | `stylometry/pipelines.py` | `FeatureConfig`, `preset_config` |
|
||||||
|
| Веса блоков | `stylometry/pipelines.py` | `FeatureConfig.block_weights` |
|
||||||
|
| Параметры TF-IDF/LSA | `stylometry/lsa.py` | `_DEFAULT_CONFIGS`, `LSAVectorizer` |
|
||||||
|
| Ручные признаки | `stylometry/features/` | модули групп |
|
||||||
|
| Списки функциональных слов | `stylometry/lexicons.py` | тематические множества |
|
||||||
|
| Параметры дельты | `stylometry/delta.py` | `DeltaClassifier`, `MFWVectorizer` |
|
||||||
|
| Схема CV | `stylometry/cv.py` | `run_repeated_cv`, `run_nested_cv` |
|
||||||
|
| Конфигурация запуска | `configs/default.toml` | `[cv]`, `[experiment]`, `[model_params]`, `[features]` |
|
||||||
|
| CLI | `stylometry/cli.py` | `cmd_run`, `cmd_benchmark`, `cmd_ablate`, `cmd_lsa_sweep` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Параметры классификаторов
|
||||||
|
|
||||||
|
### 2.1. Через конфигурацию
|
||||||
|
|
||||||
|
В `configs/default.toml` есть секция `[model_params]`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[model_params]
|
||||||
|
C = 0.3
|
||||||
|
class_weight = "balanced"
|
||||||
|
```
|
||||||
|
|
||||||
|
Запуск:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m stylometry.cli run \
|
||||||
|
--config configs/default.toml \
|
||||||
|
--preset stylometry_pos_lsa \
|
||||||
|
--model logreg \
|
||||||
|
--repeats 3
|
||||||
|
```
|
||||||
|
|
||||||
|
Параметры из `[model_params]` передаются в `make_classifier`. Для каждой
|
||||||
|
модели используются только её собственные параметры; лишние игнорируются.
|
||||||
|
|
||||||
|
### 2.2. Через командную строку
|
||||||
|
|
||||||
|
Удобно для быстрых проверок:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m stylometry.cli run \
|
||||||
|
--preset stylometry_pos_lsa \
|
||||||
|
--model logreg \
|
||||||
|
--repeats 1 \
|
||||||
|
--model-params '{"C": 0.3, "class_weight": "balanced"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
JSON позволяет передать `null`, например отключить веса классов:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
--model-params '{"class_weight": null}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3. Что менять у конкретных моделей
|
||||||
|
|
||||||
|
#### Логистическая регрессия (`logreg`)
|
||||||
|
|
||||||
|
| Параметр | Смысл | Разумный диапазон |
|
||||||
|
|---|---|---|
|
||||||
|
| `C` | Обратная сила L2-регуляризации | 0,01; 0,03; 0,1; 0,3; 1; 3; 10; 30 |
|
||||||
|
| `class_weight` | Веса классов | `"balanced"` или `null` |
|
||||||
|
| `solver` | Алгоритм оптимизации | `lbfgs`, `liblinear`, `saga` |
|
||||||
|
| `max_iter` | Максимум итераций | 1000–10000 |
|
||||||
|
|
||||||
|
Что ожидать:
|
||||||
|
|
||||||
|
- меньше `C` — сильнее регуляризация, модель проще, меньше переобучения;
|
||||||
|
- больше `C` — модель лучше подстраивается под обучающие данные, но может
|
||||||
|
переобучиться;
|
||||||
|
- `class_weight="balanced"` почти всегда полезен при дисбалансе 15–139;
|
||||||
|
- `liblinear` иногда лучше на разреженных признаках, но медленнее при многих
|
||||||
|
классах.
|
||||||
|
|
||||||
|
#### Линейный метод опорных векторов (`linear_svc`)
|
||||||
|
|
||||||
|
| Параметр | Смысл | Разумный диапазон |
|
||||||
|
|---|---|---|
|
||||||
|
| `C` | Штраф за нарушения зазора | 0,01; 0,03; 0,1; 0,3; 1; 3; 10 |
|
||||||
|
| `loss` | Функция потерь | `squared_hinge`, `hinge` |
|
||||||
|
| `class_weight` | Веса классов | `"balanced"` или `null` |
|
||||||
|
| `tol` | Допуск сходимости | 1e-3–1e-5 |
|
||||||
|
| `max_iter` | Максимум итераций | 10000–50000 |
|
||||||
|
|
||||||
|
Помните: `linear_svc` не выдаёт вероятности. Для top-k используются значения
|
||||||
|
решающей функции; log loss для него не считается.
|
||||||
|
|
||||||
|
#### Случайный лес (`random_forest`)
|
||||||
|
|
||||||
|
| Параметр | Смысл | Разумный диапазон |
|
||||||
|
|---|---|---|
|
||||||
|
| `n_estimators` | Число деревьев | 300–1000 |
|
||||||
|
| `max_depth` | Глубина дерева | `null`, 10, 20, 30, 50 |
|
||||||
|
| `min_samples_leaf` | Минимум объектов в листе | 1, 2, 3, 5 |
|
||||||
|
| `min_samples_split` | Минимум объектов для разбиения | 2, 5, 10 |
|
||||||
|
| `max_features` | Число признаков на разбиение | `"sqrt"`, `"log2"`, 0.1, 0.3 |
|
||||||
|
| `class_weight` | Веса классов | `"balanced_subsample"`, `"balanced"`, `null` |
|
||||||
|
|
||||||
|
На 2005 объектах и 34 классах лес легко переобучается. Сначала уменьшайте
|
||||||
|
`max_depth` и увеличивайте `min_samples_leaf`, а не число деревьев.
|
||||||
|
|
||||||
|
#### Сверхслучайные деревья (`extra_trees`)
|
||||||
|
|
||||||
|
Параметры те же, что у случайного леса. `max_features` и `min_samples_leaf`
|
||||||
|
здесь особенно важны: случайные пороги уменьшают дисперсию, но при слишком
|
||||||
|
большой глубине модель всё равно переобучается.
|
||||||
|
|
||||||
|
#### Наивный байесовский классификатор (`multinomial_nb`)
|
||||||
|
|
||||||
|
| Параметр | Смысл | Разумный диапазон |
|
||||||
|
|---|---|---|
|
||||||
|
| `alpha` | Сглаживание Лапласа | 0,01; 0,1; 0,3; 1; 3; 10 |
|
||||||
|
| `fit_prior` | Учитывать априорные вероятности классов | `true`, `false` |
|
||||||
|
|
||||||
|
В текущем конвейере для `multinomial_nb` исключаются LSA-блоки, потому что
|
||||||
|
компоненты SVD могут быть отрицательными. Используются ручные неотрицательные
|
||||||
|
признаки, char- и POS- n-граммы. Весов классов у модели нет; при сильном
|
||||||
|
дисбалансе она смещается к частым классам.
|
||||||
|
|
||||||
|
#### k ближайших соседей (`knn`)
|
||||||
|
|
||||||
|
| Параметр | Смысл | Разумный диапазон |
|
||||||
|
|---|---|---|
|
||||||
|
| `n_neighbors` | Число соседей | 3, 5, 7, 9, 11, 15 |
|
||||||
|
| `weights` | Вес соседей | `"uniform"`, `"distance"` |
|
||||||
|
| `metric` | Метрика расстояния | `"cosine"`, `"euclidean"`, `"manhattan"` |
|
||||||
|
|
||||||
|
Для разреженных текстовых признаков косинусное расстояние обычно лучше
|
||||||
|
евклидова. kNN чувствителен к размерности; уменьшение числа признаков или
|
||||||
|
LSA может помочь.
|
||||||
|
|
||||||
|
#### Дельта Барроуза (`delta`)
|
||||||
|
|
||||||
|
В `stylometry/delta.py`:
|
||||||
|
|
||||||
|
- `n_features` — число наиболее частотных слов;
|
||||||
|
- `metric` — `manhattan`, `cosine`, `euclidean`;
|
||||||
|
- `zscore` — z-нормирование частот.
|
||||||
|
|
||||||
|
CLI перебирает `n_features` и `metric`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m stylometry.cli delta \
|
||||||
|
--features 50,100,200,500,1000 \
|
||||||
|
--metrics manhattan,cosine \
|
||||||
|
--repeats 2
|
||||||
|
```
|
||||||
|
|
||||||
|
Чтобы менять `zscore`, передайте параметр в `DeltaClassifier` из сценария или
|
||||||
|
добавьте его в CLI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Сетки для вложенной кросс-валидации
|
||||||
|
|
||||||
|
`PARAM_GRIDS` в `stylometry/pipelines.py` задаёт, что именно перебирает
|
||||||
|
`--nested`. Текущие сетки небольшие, чтобы серия завершалась за разумное время.
|
||||||
|
Их можно расширить:
|
||||||
|
|
||||||
|
```python
|
||||||
|
PARAM_GRIDS = {
|
||||||
|
"logreg": {"model__C": [0.03, 0.1, 0.3, 1.0, 3.0, 10.0]},
|
||||||
|
"linear_svc": {"model__C": [0.03, 0.1, 0.3, 1.0, 3.0, 10.0]},
|
||||||
|
"random_forest": {
|
||||||
|
"model__max_depth": [None, 10, 20, 30],
|
||||||
|
"model__min_samples_leaf": [1, 2, 3, 5],
|
||||||
|
"model__max_features": ["sqrt", 0.1, 0.3],
|
||||||
|
},
|
||||||
|
"multinomial_nb": {"model__alpha": [0.01, 0.1, 0.3, 1.0, 3.0]},
|
||||||
|
"knn": {"model__n_neighbors": [3, 5, 7, 9, 11, 15]},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
После изменения:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m stylometry.cli run \
|
||||||
|
--preset stylometry_pos_lsa \
|
||||||
|
--model logreg \
|
||||||
|
--nested
|
||||||
|
```
|
||||||
|
|
||||||
|
Вложенная CV обучает модель для каждой комбинации гиперпараметров во
|
||||||
|
внутренних блоках и оценивает выбранную модель на внешних. Это дороже, но
|
||||||
|
честнее, чем настраивать параметры на тех же данных, где измеряется качество.
|
||||||
|
|
||||||
|
Не расширяйте сетку одновременно по всем моделям: сначала найдите удачный
|
||||||
|
диапазон на быстрых запусках, затем проверьте его через `--nested`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Признаки: что и где менять
|
||||||
|
|
||||||
|
### 4.1. Состав групп
|
||||||
|
|
||||||
|
`FeatureConfig` в `stylometry/pipelines.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
groups: tuple[str, ...] = (
|
||||||
|
"length", "lexical", "vocabulary", "characters", "punctuation",
|
||||||
|
"function_words", "pronouns", "pos", "syntax", "readability", "dialogue",
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
В `configs/default.toml` можно задать список групп:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[features]
|
||||||
|
groups = ["length", "lexical", "characters", "punctuation",
|
||||||
|
"function_words", "pronouns", "pos", "syntax", "dialogue"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Это исключит `vocabulary` и `readability` — самые нестабильные группы на
|
||||||
|
коротких репликах. Сравните результат с полным набором через `ablate`.
|
||||||
|
|
||||||
|
### 4.2. Символьные n-граммы
|
||||||
|
|
||||||
|
В `[features]`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
char_ngram_range = [2, 6]
|
||||||
|
char_max_features = 8000
|
||||||
|
min_df = 2
|
||||||
|
```
|
||||||
|
|
||||||
|
Что пробовать:
|
||||||
|
|
||||||
|
- `[2, 5]` — текущий вариант;
|
||||||
|
- `[2, 4]` — меньше шума, но меньше морфологии;
|
||||||
|
- `[2, 6]` — больше деталей, выше риск переобучения;
|
||||||
|
- `char_max_features`: 3000, 5000, 8000, 12000;
|
||||||
|
- `min_df`: 1, 2, 3, 5.
|
||||||
|
|
||||||
|
### 4.3. POS n-граммы
|
||||||
|
|
||||||
|
```toml
|
||||||
|
pos_ngram_range = [1, 4]
|
||||||
|
pos_max_features = 800
|
||||||
|
min_df = 2
|
||||||
|
```
|
||||||
|
|
||||||
|
POS-признаки обычно устойчивее к теме, чем словарные. Если увеличивать
|
||||||
|
`pos_max_features`, следите за переобучением: редкие POS-шаблоны могут быть
|
||||||
|
шумом.
|
||||||
|
|
||||||
|
### 4.4. LSA
|
||||||
|
|
||||||
|
В `configs/default.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
lsa_components = 150
|
||||||
|
```
|
||||||
|
|
||||||
|
В `stylometry/lsa.py` в `_DEFAULT_CONFIGS` задаются параметры каждого
|
||||||
|
варианта: `ngram_range`, `min_df`, `max_df`, `max_features`, `token_pattern`.
|
||||||
|
Например, для `word` можно увеличить `max_features` с 5000 до 10000, а для
|
||||||
|
`char` — с 5000 до 8000.
|
||||||
|
|
||||||
|
Что пробовать:
|
||||||
|
|
||||||
|
- `lsa_components`: 50, 100, 150, 200, 300;
|
||||||
|
- `lsa_variants`: `word`, `word2`, `content`, `fw`, `char`, `pos`;
|
||||||
|
- `min_df`: 2, 3, 5;
|
||||||
|
- `max_features`: 3000–12000 в зависимости от варианта.
|
||||||
|
|
||||||
|
Команда для перебора:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m stylometry.cli lsa-sweep \
|
||||||
|
--variants word,content,fw,char,pos \
|
||||||
|
--components 50,100,150,200,300 \
|
||||||
|
--model logreg --repeats 1
|
||||||
|
```
|
||||||
|
|
||||||
|
После выбора лучшего `k` проверьте его с 5 повторами через `run`.
|
||||||
|
|
||||||
|
### 4.5. Веса блоков
|
||||||
|
|
||||||
|
После L2-нормировки каждый блок имеет единичную норму, поэтому блоки
|
||||||
|
конкурируют на равных. Если один блок шумит, его вес можно уменьшить:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[features.block_weights]
|
||||||
|
stylo = 1.0
|
||||||
|
char = 0.5
|
||||||
|
pos = 0.7
|
||||||
|
lsa_word = 0.3
|
||||||
|
lsa_pos = 0.3
|
||||||
|
```
|
||||||
|
|
||||||
|
Правило: сумма весов не важна, важны относительные значения. Начните с
|
||||||
|
единиц, затем уменьшайте шумные блоки. Веса — такой же гиперпараметр, поэтому
|
||||||
|
выбирать их нужно по внутренней CV, а не по тесту.
|
||||||
|
|
||||||
|
### 4.6. Добавление своих признаков
|
||||||
|
|
||||||
|
1. Создайте модуль в `stylometry/features/`, например `my_features.py`.
|
||||||
|
2. Реализуйте `extract(row: pd.Series) -> dict[str, float]`.
|
||||||
|
3. Зарегистрируйте группу в `FEATURE_GROUPS` и префикс в
|
||||||
|
`FEATURE_GROUP_PREFIXES` в `stylometry/features/stylometry.py`.
|
||||||
|
4. Добавьте группу в `FeatureConfig.groups` или в пресет.
|
||||||
|
5. Проверьте, что значения конечны и не зависят от меток.
|
||||||
|
6. Запустите `features` и `ablate`.
|
||||||
|
|
||||||
|
Не добавляйте признаки, которые используют целевую переменную или статистики
|
||||||
|
тестовой выборки.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Рекомендуемый порядок улучшения
|
||||||
|
|
||||||
|
1. **Зафиксируйте базовый уровень:** `stylometry`, `stylometry_pos`,
|
||||||
|
`stylometry_pos_lsa`, 5 повторов. Без этого нельзя понять, помогает ли
|
||||||
|
изменение.
|
||||||
|
2. **Уберите шумные группы.** Поочерёдно исключайте `vocabulary`,
|
||||||
|
`readability`, `length`; смотрите `delta_macro_f1` и доверительные
|
||||||
|
интервалы.
|
||||||
|
3. **Настройте char/POS n-граммы.** Диапазоны, `max_features`, `min_df`.
|
||||||
|
4. **Настройте LSA.** Варианты и число компонент; отдельно проверьте
|
||||||
|
«стиль или тема».
|
||||||
|
5. **Настройте веса блоков.**
|
||||||
|
6. **Настройте классификатор.** Начните с `logreg.C`, затем `linear_svc.C`,
|
||||||
|
затем деревья, NB и kNN.
|
||||||
|
7. **Проверьте устойчивость.** Лучшие 2–3 конфигурации — через `--nested`,
|
||||||
|
5 повторов, разные seed.
|
||||||
|
8. **Сравните статистически.** `compare` (Мак-Немар, бутстрэп-разница),
|
||||||
|
`subgroup-report`, `length-report`, `per_class_report`.
|
||||||
|
9. **Только после этого** делайте выводы.
|
||||||
|
|
||||||
|
Не пытайтесь улучшить всё одновременно: при 2005 объектах и 34 классах
|
||||||
|
разница в 1–2 процентных пункта часто находится в пределах шума.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Что обычно даёт прирост, а что — нет
|
||||||
|
|
||||||
|
### Обычно помогает
|
||||||
|
|
||||||
|
- исключение нестабильных групп (`vocabulary`, `readability`);
|
||||||
|
- char- и POS- n-граммы;
|
||||||
|
- `class_weight="balanced"` для линейных моделей;
|
||||||
|
- подбор `C` для логистической регрессии и SVM;
|
||||||
|
- уменьшение `max_features` или `min_df` при переобучении;
|
||||||
|
- веса блоков: уменьшение шумного LSA или char-блока;
|
||||||
|
- больше повторов и вложенная CV для честной оценки.
|
||||||
|
|
||||||
|
### Может помочь, но требует проверки
|
||||||
|
|
||||||
|
- увеличение `char_max_features` и диапазона n-грамм;
|
||||||
|
- увеличение числа компонент LSA;
|
||||||
|
- `max_features=0.1/0.3` у деревьев;
|
||||||
|
- `min_samples_leaf` > 1 у деревьев;
|
||||||
|
- kNN с косинусным расстоянием и `weights="distance"`;
|
||||||
|
- ансамбли (голосование logreg + SVM) — в текущем коде не реализованы, но
|
||||||
|
могут дать небольшой прирост.
|
||||||
|
|
||||||
|
### Обычно не помогает или вредит
|
||||||
|
|
||||||
|
- увеличение сложности модели без контроля переобучения;
|
||||||
|
- настройка гиперпараметров на тестовой выборке;
|
||||||
|
- удаление пунктуации и стоп-слов в стилометрии;
|
||||||
|
- лемматизация по умолчанию;
|
||||||
|
- SMOTE и синтез текста;
|
||||||
|
- подбор признаков по тестовым метрикам;
|
||||||
|
- сравнение моделей только по доле правильных ответов.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Как фиксировать результаты
|
||||||
|
|
||||||
|
Для каждого варианта сохраняйте:
|
||||||
|
|
||||||
|
- `run_metadata.json` (конфигурация, seed, версии);
|
||||||
|
- `pooled_metrics.csv` (macro-F1, balanced accuracy, top-3);
|
||||||
|
- `bootstrap_ci.json`;
|
||||||
|
- `per_class_report.csv`;
|
||||||
|
- `confusion_matrix.csv`;
|
||||||
|
- `run_id` и команду запуска.
|
||||||
|
|
||||||
|
Не сравнивайте запуски с разными `--folds` или `--seed` через `compare`.
|
||||||
|
Для корректного парного сравнения используйте одинаковые схему CV и seed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Быстрые команды для перебора
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Логистическая регрессия: C
|
||||||
|
for c in 0.03 0.1 0.3 1 3 10; do
|
||||||
|
python -m stylometry.cli run \
|
||||||
|
--preset stylometry_pos_lsa --model logreg \
|
||||||
|
--repeats 3 --model-params "{\"C\": $c}"
|
||||||
|
done
|
||||||
|
|
||||||
|
# 2. Линейный SVM: C
|
||||||
|
for c in 0.03 0.1 0.3 1 3 10; do
|
||||||
|
python -m stylometry.cli run \
|
||||||
|
--preset stylometry_pos_lsa --model linear_svc \
|
||||||
|
--repeats 3 --model-params "{\"C\": $c}"
|
||||||
|
done
|
||||||
|
|
||||||
|
# 3. LSA: число компонент
|
||||||
|
python -m stylometry.cli lsa-sweep \
|
||||||
|
--variants word,content,fw,char,pos \
|
||||||
|
--components 50,100,150,200,300 \
|
||||||
|
--model logreg --repeats 1
|
||||||
|
|
||||||
|
# 4. Деревья: глубина и лист
|
||||||
|
for depth in 10 20 30; do
|
||||||
|
for leaf in 1 2 3; do
|
||||||
|
python -m stylometry.cli run \
|
||||||
|
--preset stylometry_pos_lsa --model random_forest \
|
||||||
|
--repeats 3 \
|
||||||
|
--model-params "{\"max_depth\": $depth, \"min_samples_leaf\": $leaf}"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
Такие циклы создают много каталогов результатов. После серии удобно собрать
|
||||||
|
сводку по `pooled_metrics.csv` и `run_metadata.json`; для этого можно
|
||||||
|
использовать `pandas` или добавить отдельный сценарий.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Ограничения
|
||||||
|
|
||||||
|
- 2005 реплик и 34 класса — небольшая выборка; многие различия статистически
|
||||||
|
незначимы.
|
||||||
|
- Короткие тексты ограничивают пользу словарного богатства и читаемости.
|
||||||
|
- Один автор: задача измеряет различимость речевых масок, а не авторский
|
||||||
|
стиль.
|
||||||
|
- LSA может отражать тему; сравнение вариантов обязательно.
|
||||||
|
- Ручная фильтрация корпуса не воспроизводима; это ограничение нельзя
|
||||||
|
исправить настройкой модели.
|
||||||
|
- Любая настройка по тестовой выборке делает результат невалидным.
|
||||||
@@ -15,6 +15,24 @@ seed = 42
|
|||||||
preset = "stylometry_pos_lsa"
|
preset = "stylometry_pos_lsa"
|
||||||
model = "logreg"
|
model = "logreg"
|
||||||
|
|
||||||
|
[model_params]
|
||||||
|
# Параметры конкретного классификатора. Раскомментируйте нужные.
|
||||||
|
# Для logreg:
|
||||||
|
# C = 1.0
|
||||||
|
# class_weight = "balanced"
|
||||||
|
# Для linear_svc:
|
||||||
|
# C = 1.0
|
||||||
|
# Для random_forest / extra_trees:
|
||||||
|
# n_estimators = 400
|
||||||
|
# max_depth = 30
|
||||||
|
# min_samples_leaf = 1
|
||||||
|
# Для multinomial_nb:
|
||||||
|
# alpha = 1.0
|
||||||
|
# fit_prior = true
|
||||||
|
# Для knn:
|
||||||
|
# n_neighbors = 5
|
||||||
|
# weights = "distance"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# Эти параметры описаны в stylometry/pipelines.py и могут переопределяться
|
# Эти параметры описаны в stylometry/pipelines.py и могут переопределяться
|
||||||
# программно. В командной строке удобнее выбирать готовый preset.
|
# программно. В командной строке удобнее выбирать готовый preset.
|
||||||
@@ -25,3 +43,11 @@ pos_ngram_range = [1, 3]
|
|||||||
lsa_components = 100
|
lsa_components = 100
|
||||||
min_df = 3
|
min_df = 3
|
||||||
stylo_scaler = "standard"
|
stylo_scaler = "standard"
|
||||||
|
|
||||||
|
[features.block_weights]
|
||||||
|
# Веса блоков в FeatureUnion. Пусто — равные веса.
|
||||||
|
# stylo = 1.0
|
||||||
|
# char = 0.5
|
||||||
|
# pos = 0.5
|
||||||
|
# lsa_word = 0.5
|
||||||
|
# lsa_pos = 0.5
|
||||||
|
|||||||
@@ -373,13 +373,24 @@ def cmd_run(args: argparse.Namespace) -> int:
|
|||||||
)
|
)
|
||||||
|
|
||||||
overrides = dict(config_data.get("features", {}))
|
overrides = dict(config_data.get("features", {}))
|
||||||
for key in ("char_ngram_range", "pos_ngram_range"):
|
for key in ("char_ngram_range", "pos_ngram_range", "groups"):
|
||||||
if key in overrides and isinstance(overrides[key], list):
|
if key in overrides and isinstance(overrides[key], list):
|
||||||
overrides[key] = tuple(overrides[key])
|
overrides[key] = tuple(overrides[key])
|
||||||
if "lsa_variants" in overrides and isinstance(overrides["lsa_variants"], list):
|
if "lsa_variants" in overrides and isinstance(overrides["lsa_variants"], list):
|
||||||
overrides["lsa_variants"] = tuple(overrides["lsa_variants"])
|
overrides["lsa_variants"] = tuple(overrides["lsa_variants"])
|
||||||
feature_config = _feature_config(preset, overrides)
|
feature_config = _feature_config(preset, overrides)
|
||||||
pipeline = build_pipeline(feature_config, model)
|
|
||||||
|
model_params = dict(config_data.get("model_params", {}))
|
||||||
|
if args.model_params:
|
||||||
|
try:
|
||||||
|
extra_params = json.loads(args.model_params)
|
||||||
|
except json.JSONDecodeError as error:
|
||||||
|
raise ValueError(f"Некорректный JSON в --model-params: {error}") from error
|
||||||
|
if not isinstance(extra_params, dict):
|
||||||
|
raise ValueError("--model-params должен быть JSON-объектом.")
|
||||||
|
model_params.update(extra_params)
|
||||||
|
|
||||||
|
pipeline = build_pipeline(feature_config, model, **model_params)
|
||||||
run_config = {
|
run_config = {
|
||||||
"preset": preset,
|
"preset": preset,
|
||||||
"model": model,
|
"model": model,
|
||||||
@@ -390,6 +401,7 @@ def cmd_run(args: argparse.Namespace) -> int:
|
|||||||
"n_classes": int(len(np.unique(y))),
|
"n_classes": int(len(np.unique(y))),
|
||||||
"corpus_sha256": frame.attrs.get("corpus_sha256", ""),
|
"corpus_sha256": frame.attrs.get("corpus_sha256", ""),
|
||||||
"feature_config": dict(feature_config.__dict__),
|
"feature_config": dict(feature_config.__dict__),
|
||||||
|
"model_params": model_params,
|
||||||
"package_versions": _package_versions(),
|
"package_versions": _package_versions(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,6 +504,7 @@ def cmd_ablate(args: argparse.Namespace) -> int:
|
|||||||
lsa_components=base.lsa_components,
|
lsa_components=base.lsa_components,
|
||||||
stylo_scaler=base.stylo_scaler,
|
stylo_scaler=base.stylo_scaler,
|
||||||
stylo_normalize=base.stylo_normalize,
|
stylo_normalize=base.stylo_normalize,
|
||||||
|
block_weights=dict(base.block_weights),
|
||||||
include_stylo=True,
|
include_stylo=True,
|
||||||
include_lsa=base.include_lsa,
|
include_lsa=base.include_lsa,
|
||||||
char_max_features=base.char_max_features,
|
char_max_features=base.char_max_features,
|
||||||
@@ -772,6 +785,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
run.add_argument("--folds", type=int, default=None)
|
run.add_argument("--folds", type=int, default=None)
|
||||||
run.add_argument("--repeats", type=int, default=None)
|
run.add_argument("--repeats", type=int, default=None)
|
||||||
run.add_argument("--seed", type=int, default=None)
|
run.add_argument("--seed", type=int, default=None)
|
||||||
|
run.add_argument(
|
||||||
|
"--model-params",
|
||||||
|
default=None,
|
||||||
|
help='JSON с параметрами модели, например \'{"C": 0.3, "class_weight": "balanced"}\'',
|
||||||
|
)
|
||||||
run.add_argument("--nested", action="store_true", help="Вложенная кросс-валидация")
|
run.add_argument("--nested", action="store_true", help="Вложенная кросс-валидация")
|
||||||
run.set_defaults(func=cmd_run)
|
run.set_defaults(func=cmd_run)
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ class FeatureConfig:
|
|||||||
lsa_components: int = 100
|
lsa_components: int = 100
|
||||||
stylo_scaler: str = "standard" # standard | minmax | none
|
stylo_scaler: str = "standard" # standard | minmax | none
|
||||||
stylo_normalize: bool = True
|
stylo_normalize: bool = True
|
||||||
|
# Веса блоков в FeatureUnion: stylo, char, pos, lsa_word, lsa_pos и т.д.
|
||||||
|
# Пустой словарь означает равный вес блоков.
|
||||||
|
block_weights: dict[str, float] = field(default_factory=dict)
|
||||||
include_stylo: bool = True
|
include_stylo: bool = True
|
||||||
include_lsa: bool = True
|
include_lsa: bool = True
|
||||||
char_max_features: int = 5000
|
char_max_features: int = 5000
|
||||||
@@ -150,7 +153,18 @@ def build_feature_union(config: FeatureConfig) -> Any:
|
|||||||
|
|
||||||
if len(branches) == 1:
|
if len(branches) == 1:
|
||||||
return branches[0][1]
|
return branches[0][1]
|
||||||
return FeatureUnion(branches)
|
|
||||||
|
union_kwargs: dict[str, Any] = {}
|
||||||
|
if config.block_weights:
|
||||||
|
branch_names = {name for name, _ in branches}
|
||||||
|
unknown = set(config.block_weights) - branch_names
|
||||||
|
if unknown:
|
||||||
|
raise ValueError(
|
||||||
|
f"Неизвестные блоки в block_weights: {sorted(unknown)}. "
|
||||||
|
f"Доступны: {sorted(branch_names)}"
|
||||||
|
)
|
||||||
|
union_kwargs["transformer_weights"] = dict(config.block_weights)
|
||||||
|
return FeatureUnion(branches, **union_kwargs)
|
||||||
|
|
||||||
|
|
||||||
def make_classifier(name: str, **params: Any) -> Any:
|
def make_classifier(name: str, **params: Any) -> Any:
|
||||||
@@ -159,19 +173,24 @@ def make_classifier(name: str, **params: Any) -> Any:
|
|||||||
strategy = params.pop("strategy", "most_frequent")
|
strategy = params.pop("strategy", "most_frequent")
|
||||||
return DummyClassifier(strategy=strategy)
|
return DummyClassifier(strategy=strategy)
|
||||||
if name == "multinomial_nb":
|
if name == "multinomial_nb":
|
||||||
return MultinomialNB(alpha=params.pop("alpha", 1.0))
|
return MultinomialNB(
|
||||||
|
alpha=params.pop("alpha", 1.0),
|
||||||
|
fit_prior=params.pop("fit_prior", True),
|
||||||
|
)
|
||||||
if name == "logreg":
|
if name == "logreg":
|
||||||
return LogisticRegression(
|
return LogisticRegression(
|
||||||
C=params.pop("C", 1.0),
|
C=params.pop("C", 1.0),
|
||||||
solver="lbfgs",
|
solver=params.pop("solver", "lbfgs"),
|
||||||
max_iter=5000,
|
max_iter=params.pop("max_iter", 5000),
|
||||||
class_weight="balanced",
|
class_weight=params.pop("class_weight", "balanced"),
|
||||||
)
|
)
|
||||||
if name == "linear_svc":
|
if name == "linear_svc":
|
||||||
return LinearSVC(
|
return LinearSVC(
|
||||||
C=params.pop("C", 1.0),
|
C=params.pop("C", 1.0),
|
||||||
class_weight="balanced",
|
loss=params.pop("loss", "squared_hinge"),
|
||||||
max_iter=20_000,
|
tol=params.pop("tol", 1e-4),
|
||||||
|
class_weight=params.pop("class_weight", "balanced"),
|
||||||
|
max_iter=params.pop("max_iter", 20_000),
|
||||||
dual="auto",
|
dual="auto",
|
||||||
)
|
)
|
||||||
if name == "random_forest":
|
if name == "random_forest":
|
||||||
@@ -179,7 +198,9 @@ def make_classifier(name: str, **params: Any) -> Any:
|
|||||||
n_estimators=params.pop("n_estimators", 400),
|
n_estimators=params.pop("n_estimators", 400),
|
||||||
max_depth=params.pop("max_depth", None),
|
max_depth=params.pop("max_depth", None),
|
||||||
min_samples_leaf=params.pop("min_samples_leaf", 1),
|
min_samples_leaf=params.pop("min_samples_leaf", 1),
|
||||||
class_weight="balanced_subsample",
|
min_samples_split=params.pop("min_samples_split", 2),
|
||||||
|
max_features=params.pop("max_features", "sqrt"),
|
||||||
|
class_weight=params.pop("class_weight", "balanced_subsample"),
|
||||||
n_jobs=-1,
|
n_jobs=-1,
|
||||||
random_state=params.pop("random_state", 42),
|
random_state=params.pop("random_state", 42),
|
||||||
)
|
)
|
||||||
@@ -188,7 +209,9 @@ def make_classifier(name: str, **params: Any) -> Any:
|
|||||||
n_estimators=params.pop("n_estimators", 400),
|
n_estimators=params.pop("n_estimators", 400),
|
||||||
max_depth=params.pop("max_depth", None),
|
max_depth=params.pop("max_depth", None),
|
||||||
min_samples_leaf=params.pop("min_samples_leaf", 1),
|
min_samples_leaf=params.pop("min_samples_leaf", 1),
|
||||||
class_weight="balanced",
|
min_samples_split=params.pop("min_samples_split", 2),
|
||||||
|
max_features=params.pop("max_features", "sqrt"),
|
||||||
|
class_weight=params.pop("class_weight", "balanced"),
|
||||||
n_jobs=-1,
|
n_jobs=-1,
|
||||||
random_state=params.pop("random_state", 42),
|
random_state=params.pop("random_state", 42),
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user