From 5a4a19e0df4cae19348ac76d242afb451d8e6aa0 Mon Sep 17 00:00:00 2001 From: Vladislav Kopnin Date: Sun, 25 Oct 2026 16:08:00 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BF=D0=B0=D1=80=D0=B0=D0=BC=D0=B5=D1=82=D1=80?= =?UTF-8?q?=D1=8B=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB=D0=B5=D0=B9,=20=D0=B2?= =?UTF-8?q?=D0=B5=D1=81=D0=B0=20=D0=B1=D0=BB=D0=BE=D0=BA=D0=BE=D0=B2=20?= =?UTF-8?q?=D0=B8=20=D1=80=D1=83=D0=BA=D0=BE=D0=B2=D0=BE=D0=B4=D1=81=D1=82?= =?UTF-8?q?=D0=B2=D0=BE=20=D0=BF=D0=BE=20=D0=BD=D0=B0=D1=81=D1=82=D1=80?= =?UTF-8?q?=D0=BE=D0=B9=D0=BA=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + lab1/README.md | 2 + lab1/RUNBOOK.md | 1 + lab1/TUNING.md | 455 +++++++++++++++++++++++++++++++++++ lab1/configs/default.toml | 26 ++ lab1/stylometry/cli.py | 22 +- lab1/stylometry/pipelines.py | 41 +++- 7 files changed, 537 insertions(+), 11 deletions(-) create mode 100644 lab1/TUNING.md diff --git a/README.md b/README.md index 6089314..8efbe63 100644 --- a/README.md +++ b/README.md @@ -52,5 +52,6 @@ nlp/ - [`lab1/METRICS_AND_CLASSIFIERS.md`](lab1/METRICS_AND_CLASSIFIERS.md) — методическое описание метрик и классификаторов. - [`lab1/FEATURES.md`](lab1/FEATURES.md) — признаки и формулы. +- [`lab1/TUNING.md`](lab1/TUNING.md) — настройка параметров и перебор. - [`lab1/PRESENTATION_OUTLINE.md`](lab1/PRESENTATION_OUTLINE.md) — структура презентации. diff --git a/lab1/README.md b/lab1/README.md index 981fc6b..7fe8031 100644 --- a/lab1/README.md +++ b/lab1/README.md @@ -162,6 +162,8 @@ python -m stylometry.cli run --config configs/default.toml \ ограничения и чек-лист для следующего исследователя. - **`FEATURES.md`** — состав стилометрических признаков, формулы и обоснование выбора; дополняет машинно-читаемый `features/feature_dictionary.csv`. +- **`TUNING.md`** — где менять параметры классификаторов, признаки, LSA и веса + блоков; рекомендуемый порядок улучшения результатов. - **`METRICS_AND_CLASSIFIERS.md`** — методическое описание метрик и классификаторов: что показывает каждая метрика, как она считается, как интерпретировать значения, чем отличаются модели и как объяснять это на diff --git a/lab1/RUNBOOK.md b/lab1/RUNBOOK.md index 8a3a74c..f82930c 100644 --- a/lab1/RUNBOOK.md +++ b/lab1/RUNBOOK.md @@ -29,6 +29,7 @@ Полный протокол: `EXPERIMENTS.md`. Описание признаков: `FEATURES.md`. Описание метрик и классификаторов: `METRICS_AND_CLASSIFIERS.md`. +Настройка параметров и перебор: `TUNING.md`. Структура презентации: `PRESENTATION_OUTLINE.md`. --- diff --git a/lab1/TUNING.md b/lab1/TUNING.md new file mode 100644 index 0000000..c53c439 --- /dev/null +++ b/lab1/TUNING.md @@ -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 может отражать тему; сравнение вариантов обязательно. +- Ручная фильтрация корпуса не воспроизводима; это ограничение нельзя + исправить настройкой модели. +- Любая настройка по тестовой выборке делает результат невалидным. diff --git a/lab1/configs/default.toml b/lab1/configs/default.toml index 70b80a9..8c7fb89 100644 --- a/lab1/configs/default.toml +++ b/lab1/configs/default.toml @@ -15,6 +15,24 @@ seed = 42 preset = "stylometry_pos_lsa" 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] # Эти параметры описаны в stylometry/pipelines.py и могут переопределяться # программно. В командной строке удобнее выбирать готовый preset. @@ -25,3 +43,11 @@ pos_ngram_range = [1, 3] lsa_components = 100 min_df = 3 stylo_scaler = "standard" + +[features.block_weights] +# Веса блоков в FeatureUnion. Пусто — равные веса. +# stylo = 1.0 +# char = 0.5 +# pos = 0.5 +# lsa_word = 0.5 +# lsa_pos = 0.5 diff --git a/lab1/stylometry/cli.py b/lab1/stylometry/cli.py index 5f12e75..5c3d881 100644 --- a/lab1/stylometry/cli.py +++ b/lab1/stylometry/cli.py @@ -373,13 +373,24 @@ def cmd_run(args: argparse.Namespace) -> int: ) 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): overrides[key] = tuple(overrides[key]) if "lsa_variants" in overrides and isinstance(overrides["lsa_variants"], list): overrides["lsa_variants"] = tuple(overrides["lsa_variants"]) 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 = { "preset": preset, "model": model, @@ -390,6 +401,7 @@ def cmd_run(args: argparse.Namespace) -> int: "n_classes": int(len(np.unique(y))), "corpus_sha256": frame.attrs.get("corpus_sha256", ""), "feature_config": dict(feature_config.__dict__), + "model_params": model_params, "package_versions": _package_versions(), } @@ -492,6 +504,7 @@ def cmd_ablate(args: argparse.Namespace) -> int: lsa_components=base.lsa_components, stylo_scaler=base.stylo_scaler, stylo_normalize=base.stylo_normalize, + block_weights=dict(base.block_weights), include_stylo=True, include_lsa=base.include_lsa, 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("--repeats", 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.set_defaults(func=cmd_run) diff --git a/lab1/stylometry/pipelines.py b/lab1/stylometry/pipelines.py index 47bb604..90cc5d9 100644 --- a/lab1/stylometry/pipelines.py +++ b/lab1/stylometry/pipelines.py @@ -45,6 +45,9 @@ class FeatureConfig: lsa_components: int = 100 stylo_scaler: str = "standard" # standard | minmax | none 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_lsa: bool = True char_max_features: int = 5000 @@ -150,7 +153,18 @@ def build_feature_union(config: FeatureConfig) -> Any: if len(branches) == 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: @@ -159,19 +173,24 @@ def make_classifier(name: str, **params: Any) -> Any: strategy = params.pop("strategy", "most_frequent") return DummyClassifier(strategy=strategy) 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": return LogisticRegression( C=params.pop("C", 1.0), - solver="lbfgs", - max_iter=5000, - class_weight="balanced", + solver=params.pop("solver", "lbfgs"), + max_iter=params.pop("max_iter", 5000), + class_weight=params.pop("class_weight", "balanced"), ) if name == "linear_svc": return LinearSVC( C=params.pop("C", 1.0), - class_weight="balanced", - max_iter=20_000, + loss=params.pop("loss", "squared_hinge"), + tol=params.pop("tol", 1e-4), + class_weight=params.pop("class_weight", "balanced"), + max_iter=params.pop("max_iter", 20_000), dual="auto", ) if name == "random_forest": @@ -179,7 +198,9 @@ def make_classifier(name: str, **params: Any) -> Any: n_estimators=params.pop("n_estimators", 400), max_depth=params.pop("max_depth", None), 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, 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), max_depth=params.pop("max_depth", None), 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, random_state=params.pop("random_state", 42), )