Last active 1 month ago

drholy revised this gist 1 month ago. Go to revision

1 file changed, 196 insertions, 204 deletions

changes.md

@@ -1,22 +1,37 @@
1 - Ниже план закрытия дыр **по порядку**. Сначала фиксируется, что едет в архив, потом CV, потом баги фич, потом уборка. Иначе легко «починить структуру» и отправить не тот zip.
1 + Ниже **единый план**: твой разбор репозитория + roadmap коллеги. Главное изменение приоритетов: **human-CV больше не гейт**. Коллега проверил тремя сабмитами: LB ≈ LLM-прокси − 0.19, а human 0.74 не ранжирует кандидатов. Поэтому fold 4 / hard-soup / категорийные веса на human-OOF уходят вниз.
2 2
3 - Каждый шаг: что сделать → как проверить → когда считать закрытым.
3 + Цифры коллеги, от которых пляшем: LB **0.43059** (E5) → **0.43169** (soup+V2+категории). Их бленд твоего E5 с их CatBoost — **0.43456**.
4 4
5 5 ---
6 6
7 - ## Фаза 0. Заморозить текущий кандидат
7 + ## Правило отбора экспериментов
8 8
9 - Пока это не сделано, ничего не переименовывать и не двигать файлы.
9 + Каждый кандидат меряется так, в этом порядке:
10 10
11 - 1. Записать на бумаге (или в один файл `ACTIVE.md`, если переключишься в Agent mode) четыре факта:
12 - - какой чекпоинт E5: путь к `model.safetensors`
13 - - какой tabular: путь к `*.joblib` и `sklearn` version
14 - - какой конфиг бленда: сейчас это `ensemble-candidate.json`
15 - - SHA текущего `submission.zip`: в README это `21767a7d…`, сверить `Get-FileHash`
11 + 1. **LLM-прокси** (фиксированный сэмпл, не в обучении) — основное.
12 + 2. Human disjoint folds — вторично, «не уехала ли модель с ручных меток».
13 + 3. Сабмит на LB — только если прокси вырос и архив/время влезают.
16 14
17 - 2. Прогнать эталон на тех же данных, что в README:
15 + Не принимать решение по human-OOF, если прокси не вырос. Коллега: прирост на прокси переносится в LB с коэффициентом ~0.4; калибровка LB ≈ прокси − 0.19.
16 +
17 + Не делать (уже проверено у них): bi-encoder косинусы, AutoGluon, hard-mine по human-OOF, категорийные веса по human-CV, масштабирование таблички 2M→8M LLM.
18 +
19 + ---
20 +
21 + ## Фаза 0. Заморозить якоря
22 +
23 + Пока это не сделано — не двигать файлы и не переобучать.
24 +
25 + 1. Зафиксировать текущий боевой набор:
26 + - E5 чекпоинт (`model.safetensors`)
27 + - compact `*.joblib` + версия sklearn
28 + - `ensemble-candidate.json`
29 + - SHA zip (`21767a7d…` в README) — сверить `Get-FileHash`
30 +
31 + 2. Снять **два** референса, не один:
18 32
19 33 ```bash
34 + # A. Human (регрессия runtime, не оценка качества)
20 35 python -u run_ensemble_candidate.py \
21 36 --items_path data/items_human.parquet \
22 37 --matches_path data/matches.parquet \
@@ -31,37 +46,48 @@ python -u verify_candidate_output.py \
31 46 --items-path data/items_human.parquet
32 47 ```
33 48
34 - **Готово, когда:** CSV валиден, число строк 365 654, refit macro совпадает с `0.785309497` (±1e-9). Этот CSV — регрессионный якорь. Любой следующий рефакторинг runtime обязан дать byte-identical или score-identical файл.
49 + 3. Зафиксировать LLM-прокси (действие №1 коллеги). У тебя уже есть зачатки: `prepare_proxy_data.py`, `new_ideas/X_proxy.npy`, `new_ideas/proxy_meta.parquet`. Канон:
35 50
36 - ---
51 + - один неизменяемый сэмпл (~300k LLM-пар), `random_state=42`
52 + - бинаризация `target >= 0.5`
53 + - macro PR-AUC по категориям
54 + - эти пары **исключены из любого дальнейшего обучения**
55 + - скрипт оценки: взять/адаптировать их `scripts/eval_llm_proxy.py`
37 56
38 - ## Фаза 1. Один runtime, один конфиг, один упаковщик
57 + Записать в таблицу три точки коллеги как sanity калибровки:
39 58
40 - Это закрывает самую опасную дыру: локальный `run.py` ≠ архив.
59 + | Модель | Прокси | LB | Прокси − LB |
60 + |---|---|---|---|
61 + | (их слабая) | 0.502 | 0.3097 | 0.192 |
62 + | твой E5 | 0.613 | 0.4306 | 0.182 |
63 + | E5 + их CatBoost | 0.624 | 0.4346 | 0.189 |
41 64
42 - ### Шаг 1.1. Сделать `run.py` боевым
65 + **Готово, когда:** есть `submit-reference.csv` и одно число прокси для текущего zip. Дальше любой эксперимент пишет строку в эту таблицу.
43 66
44 - - Перенести логику из `run_ensemble_candidate.py` в `run.py` (не alias в обратную сторону: организатор вызывает `python -u run.py`).
45 - - `run_ensemble_candidate.py` и `run_gpu_candidate.py` оставить на 3–5 строк: `from run import main`.
46 - - Дефолты оставить как в кандидате: `models/cross-encoder`, `models/compact.joblib`, `ensemble.json`.
67 + ---
47 68
48 - **Проверка:** повтор Фазы 0 через `run.py`. Diff с `submit-reference.csv` пустой.
69 + ## Фаза 1. Один runtime (закрывает дыру у обоих разборов)
49 70
50 - ### Шаг 1.2. Один `ensemble.json`
71 + Коллега, §5: в боевом `run.py` сейчас чистый CE, ансамбль только в `run_ensemble_candidate.py`.
51 72
52 - - Содержимое `ensemble-candidate.json` → `ensemble.json`.
53 - - `ensemble-candidate.json` удалить после того, как все скрипты/README ссылаются на `ensemble.json`.
54 - - В `run.py` падать явно, если нет `neural_weight` (сейчас молча получится transformer-only, если кто-то вызовет старый код).
73 + ### 1.1. `run.py` = ансамбль
55 74
56 - **Проверка:** `python -c "import json; c=json.load(open('ensemble.json')); assert 'neural_weight' in c and 'neural_weight_by_category' in c"`
75 + - Логику `run_ensemble_candidate.py` перенести в `run.py`.
76 + - Алиасы на 3 строки: `from run import main`.
77 + - Падать, если в конфиге нет `neural_weight` (иначе снова уедет чистый CE).
57 78
58 - ### Шаг 1.3. Убить сломанный CPU-упаковщик
79 + **Проверка:** `run.py` даёт byte-identical `submit-reference.csv`.
59 80
60 - - `make_submission.py` удалить или переименовать в `experiments/make_cpu_submission.py` и в первой строке docstring написать `DEAD: packs model.joblib with GPU run.py`.
61 - - Канон один: `make_ensemble_submission.py`.
62 - - `make_gpu_submission.py` оставить только как «E5-only fallback», в docstring явно: не текущий лидерборд-кандидат.
81 + ### 1.2. Один `ensemble.json`
63 82
64 - **Проверка:** в корне одна команда сборки архива. В zip есть `run.py`, `ensemble.json` с весами, `solution/features.py`, `solution/features_v2.py`, `models/compact.joblib`, `models/cross-encoder/model.safetensors`. Нет `model.joblib` в корне архива.
83 + - Содержимое `ensemble-candidate.json` → `ensemble.json`.
84 + - Старый файл удалить после смены ссылок.
85 +
86 + ### 1.3. Один упаковщик
87 +
88 + - Канон: `make_ensemble_submission.py`.
89 + - `make_submission.py` убрать или в `experiments/` с пометкой DEAD (пакует `model.joblib` + GPU `run.py`).
90 + - `make_gpu_submission.py` — только E5-only fallback.
65 91
66 92 ```bash
67 93 python make_ensemble_submission.py \
@@ -72,269 +98,235 @@ python make_ensemble_submission.py \
72 98 --output-path submission.zip
73 99 ```
74 100
75 - Размер < 4.7 GB, `zipfile.ZipFile.testzip()` чистый.
76 -
77 - ### Шаг 1.4. Синхронизировать README и `metadata.json`
78 -
79 - В README оставить **один** блок «Current submission»:
101 + ### 1.4. README
80 102
81 - - entry point: `python -u run.py`
82 - - состав: E5 soup + compact V2 + category weights
83 - - команда упаковки
84 - - SHA и размер
85 - - CV-оценка (disjoint folds), отдельно refit sanity
103 + Один блок Current submission. Human-CV и refit 0.785 — в Historical / sanity, не как оценка LB. Добавить строку «gate = LLM-прокси».
86 104
87 - Убрать противоречие «active = E5-only» в начале файла. Исторический CPU/AutoGluon — в секцию `Historical`, с пометкой «другой validation protocol».
88 -
89 - **Готово, когда:** несуществующий человек по README собирает тот же zip, что на лидерборде.
105 + **Готово, когда:** по README собирается тот же zip, что на LB 0.43169.
90 106
91 107 ---
92 108
93 - ## Фаза 2. Закрыть CV: fold 4
94 -
95 - Без этого mean 0.728 / 0.738 — оценка по 4/5 схемы.
96 -
97 - ### Шаг 2.1. Обучить и оценить E5 на fold 4
109 + ## Фаза 2. Перемерить всё, что уже лежит на диске (H100, без нового обучения)
98 110
99 - ```bash
100 - bash scripts/train_gpu_pipeline.sh 4
101 - ```
111 + Это пункт 1 недели коллеги: «до +0.01–0.02, если пик не в конце файнтюна». Делать **до** fold 4 и до нового претрейна.
102 112
103 - Ожидаемые артефакты:
113 + На **том же** прокси прогнать:
104 114
105 - - `models/multilingual-e5-fold4/`
106 - - `models/multilingual-e5-fold4/validation.npz`
107 - - рядом `.json` с macro AP
108 -
109 - ### Шаг 2.2. Табличный OOF fold 4
110 -
111 - Та же команда, которой считались фолды 0–3 (у тебя это `evaluate_tabular_fold.py` / `evaluate_v2_folds.py` + кэш V2). Нужны:
112 -
113 - - V2 (и base, если ансамбль `base_v2`) фичи на всех human pairs
114 - - OOF fold 4 в том же формате `indices/prediction/target/categories`
115 -
116 - ### Шаг 2.3. Пересчитать бленд на 5 фолдах
115 + 1. Чекпоинт этапа 1: `models/multilingual-e5-llm` (до human-finetune).
116 + 2. Финальный human-finetune.
117 + 3. Hard-soup 0.10 (текущий LB).
118 + 4. Промежуточные `checkpoint-*` этапа 2, если сохранились.
119 + 5. Супы этап1 × этап2: `tuned_weight ∈ {0.3, 0.5, 0.7, 1.0}` через уже существующий `merge_transformer_weights.py`.
117 120
118 121 ```bash
119 - python merge_oof_predictions.py ... # все 5 фолдов
120 - python tune_blend.py --neural-path ... --tabular-path ... --output-path ...
121 - python tune_category_blend.py --neural-path ... --tabular-path ... --output-path ...
122 + python merge_transformer_weights.py \
123 + --base-model models/multilingual-e5-llm \
124 + --tuned-model <HUMAN_OR_SOUP> \
125 + --tuned-weight 0.5 \
126 + --output-dir models/soup-stage1-2-w05
122 127 ```
123 128
124 - Правило: **новые category weights попадают в `ensemble.json` только если `accepted=true` и crossfit_gain > 0 на 5 фолдах**. Иначе оставить старые веса.
125 -
126 - ### Шаг 2.4. Обновить `gpu_cv.metrics.json`
129 + Потом тот же `eval_llm_proxy` на каждый чекпоинт.
127 130
128 - Поля, которые должны появиться:
131 + Решение:
129 132
130 - - `evaluated_folds: [0,1,2,3,4]`
131 - - mean/std E5 по 5 фолдам
132 - - mean/std E5+V2 по 5 фолдам
133 - - новый global / category blend, если принят
133 + - если этап 1 или ранний этап 2 **выше** на прокси, чем текущий soup — это новый runtime **без обучения**;
134 + - human-метрика при этом может упасть — это ожидаемо (у них CatBoost: прокси 0.513 → 0.484 после human-finetune);
135 + - в архив класть победителя по прокси, если влезает в 780 с.
134 136
135 - **Готово, когда:** нет дыры «fold 4 missing». Если fold 4 принципиально holdout — тогда в метриках `n_splits=4` и в `prepare_transformer_data.py` тоже 4, а не 5. Смешивать нельзя.
137 + **Готово, когда:** таблица чекпоинт → прокси. Выбран новый baseline. Human-fold 4 **не блокирует** этот выбор.
136 138
137 139 ---
138 140
139 - ## Фаза 3. Честные baselines
141 + ## Фаза 3. Освободить бюджет инференса (разблокирует e5-base)
140 142
141 - Закрывает ложное «+0.14 от модели».
143 + Коллега: из 469 с полного прогона **281 с — однопоточный V2**, ~190 с — CE. Лимит ~780 с.
142 144
143 - ### Шаг 3.1. Не трогать старый `train.py` как источник цифр
145 + ### 3.1. Распараллелить V2
144 146
145 - Либо пометить deprecated, либо перевести на `make_disjoint_folds` из `validation.py`. Новые цифры писать только с disjoint protocol.
147 + В `build_features_v2` (и при необходимости base `build_features`): чанки пар, `multiprocessing`, ~20 CPU. Ожидание: 281 с → 25–40 с, запас ~4 минуты.
146 148
147 - ### Шаг 3.2. Пересчитать CPU/HGB и AutoGluon на тех же 5 фолдах
149 + Не менять численные значения фич (битовая идентичность на 1k пар).
148 150
149 - - HGB base features: `evaluate_tabular_fold.py` на фолдах 0–4
150 - - V2 compact: уже будет из Фазы 2
151 - - AutoGluon — опционально, только если нужен абзац в README
151 + ### 3.2. Двойная буферизация CE
152 152
153 - В README таблица:
153 + В `predict_cross_encoder`: токенизировать батч N+1, пока GPU считает N. Ожидание +20–30% к CE-стадии, без смены скоров.
154 154
155 - | Модель | Protocol | Mean macro PR-AUC |
156 - |---|---|---|
157 - | HGB base | disjoint 5-fold | … |
158 - | E5-small | disjoint 5-fold | … |
159 - | E5 + V2 | disjoint 5-fold | … |
160 - | refit train (sanity) | не оценка | 0.785 |
155 + ### 3.3. Symmetric TTA — только после 3.1
161 156
162 - Старые 0.593 / 0.596 оставить только как «leaky 25% holdout, do not compare».
157 + Скоринг (A,B) и (B,A), усреднение. Для small это ещё ~190 с. Влезает только если V2 уже быстрый. Мерить на прокси, не на human.
163 158
164 - **Готово, когда:** в README нет рядом стоящих чисел с разным protocol без пометки.
159 + ### 3.4. `max_length`
160 +
161 + Доля пар, обрезанных при 160. Если заметная — 192/224 на small (бюджет уже есть). На прокси.
162 +
163 + **Готово, когда:** полный прогон в образе, V2 < 50 с, CE не медленнее, CSV совпадает с якорем (кроме TTA/max_len — те отдельные ветки).
165 164
166 165 ---
167 166
168 - ## Фаза 4. Баги фич (меняют матрицу → нужен ретрейн tabular)
167 + ## Фаза 4. Баги фич, которые коллега и разбор оба указали
169 168
170 - Делать **после** якоря Фазы 0 и **до** большого рефакторинга папок.
169 + Делать вместе с 3.1, пока трогаешь `features_v2.py`. Меняет схему → ретрейн таблички.
171 170
172 - ### Шаг 4.1. Battery в V2
171 + 1. **`battery` в `MEASUREMENT_KINDS`** — сейчас mah/мач парсятся и выкидываются из per-kind match/conflict.
172 + 2. **Числовой допуск ±5%** (их `num_val_close`): `990 г` vs `1 кг` у тебя conflict. Добавить в V2, не ломая нормализацию единиц.
173 + 3. Пустые атрибуты: оба пустые → similarity 1.0, как Jaccard (твой разбор).
174 + 4. Категория пары только по `id1`: на прокси померить альтернативу (id1, или id1 если равны иначе `unknown`). Менять runtime только если прокси вырос.
175 + 5. TF-IDF: один check fit-on-train vs transductive на прокси. В `run.py` оставить transductive.
173 176
174 - В `solution/features_v2.py`:
177 + Их 32 фичи (`src/features.py`) + твои слоты/измерения — отдельный дешёвый стек, не в этом же коммите.
175 178
176 - - добавить `"battery"` в `MEASUREMENT_KINDS`
177 - - проверить, что `FEATURE_NAMES_V2` удлинился на 2 колонки (`v2_battery_measurement_match/conflict`)
178 - - прогнать `test_features_v2.py`
179 + После изменения схемы: новый кэш, новый joblib, новый прокси-скор. Старый compact несовместим — это ок.
179 180
180 - Потом пересобрать кэш и compact-модель. Старый `compact.joblib` с новой схемой **несовместим** — `run.py` уже падает на `features.shape[1] != len(feature_names)`, это хорошо.
181 + ---
181 182
182 - ### Шаг 4.2. Пустые атрибуты
183 + ## Фаза 5. Переобучение под тестовое распределение (H100, ночи)
183 184
184 - Зафиксировать контракт и покрыть тестом:
185 + Только после Фазы 2 (знаешь, какой чекпоинт вообще лучший) и Фазы 3 (есть бюджет на base).
185 186
186 - - оба без атрибутов → `common_attribute_value_ratio = 1.0` (как Jaccard)
187 - - ровно один без атрибутов → 0.0
187 + Все варианты мерять на прокси. Human — в лог, не в решение.
188 188
189 - Сейчас пустое ∩ пустое даёт 0.0 при Jaccard 1.0.
189 + ### 5.1. Мягкий этап 2, не 4 эпохи human
190 190
191 - ### Шаг 4.3. TF-IDF leakage-check (не обязательно в runtime)
191 + Подозрение коллеги: 4 эпохи, lr 1e-5, batch 1024 утащили E5 с LLM-теста.
192 192
193 - Один скрипт: на fold 0 fit TF-IDF только на train items, сравнить macro AP с transductive fit.
194 - - если дельта < 0.001 — оставить transductive (для теста соревнования это правильный режим)
195 - - если больше — в CV всегда fit на train, в `run.py` оставить fit на переданных items
193 + Порядок:
196 194
197 - **Готово, когда:** новый V2 кэш + новый joblib + новый zip проходят verify; OOF fold 0 не просел. Если просел — revert 4.1/4.2 по отдельности.
195 + 1. Если Фаза 2 уже нашла лучший ранний чекпоинт — стартовать от него.
196 + 2. Смешанный этап 2: human + 0.5–1M LLM-пар (не из прокси-сэмпла), human с большим весом, **1–2 эпохи**, не 4.
197 + 3. Не делать hard-mine по human-OOF (у них перенос в LB ≈ 0).
198 198
199 - ---
199 + ### 5.2. Больше претрейна этапа 1
200 200
201 - ## Фаза 5. Свести дубли в одно место
201 + Сейчас 1 эпоха на 11.2M. На H100 эпоха ~1.5–2 ч (batch 2048, seq 160).
202 + 2–3 эпохи, cosine → 0. На мягких метках E5-small скорее недообучен.
202 203
203 - Не двигать каталоги, только импорты.
204 + ### 5.3. e5-base / USER-base
204 205
205 - ### Шаг 5.1. Метрика
206 + После ускорения V2:
206 207
207 - - Оставить `macro_average_precision` только в `validation.py`
208 - - Удалить копии из `train.py` и `train_autogluon.py`
208 + - `intfloat/multilingual-e5-base`
209 + - `deepvk/USER-base` (русский, попробовать оба)
209 210
210 - ### Шаг 5.2. Текст пары
211 + Тот же пайплайн: претрейн LLM → мягкий этап 2.
212 + Инференс base ~3× small → ~570 с CE + 40 с V2 ≈ 650 с < 780 с.
213 + Претрейн base: 4–6 ч/эпоха, ночь.
211 214
212 - - `product_text` только в `solution/transformer.py`
213 - - `prepare_transformer_data.py` импортирует его
214 - - Добавить тест: Python-строка == ожидаемый шаблон `Название:\nКатегория:\nАтрибуты:` (DuckDB-SQL сверить тем же тестом на 10 строках)
215 + Large не трогать, пока не режешь `max_len` или не дистиллируешь.
215 216
216 - ### Шаг 5.3. Сет-метрики
217 + Ожидание коллеги: +0.02–0.04 LB на base + быстрые фичи.
217 218
218 - - `_jaccard`, `_overlap`/`_overlap_min`, `_conflict`, `_ratio` — один модуль, например `solution/set_metrics.py`
219 - - `features.py` и `features_v2.py` импортируют оттуда
219 + ---
220 220
221 - ### Шаг 5.4. Blend helpers
221 + ## Фаза 6. Ансамбль на LLM-распределении
222 222
223 - - `category_ranks` только в `tune_blend.py`
224 - - `tune_category_blend.py` импортирует, свою копию удалить
225 - - `category_class_weights` — в `train.py` или `validation.py`, один раз
223 + Текущая V2-таблица учится только на human и тянет бленд к human. Их факт: CatBoost на 2M LLM (CE по soft labels) + human вес 5 → прокси 0.51 vs 0.41 human-only. Твой E5 + их CatBoost, ранг-бленд 0.8/0.2 → **+0.004 LB**.
226 224
227 - ### Шаг 5.5. Verify
225 + ### 6.1. Переобучить табличку на LLM-миксе
228 226
229 - - Оставить `verify_candidate_output.py` как канон (колонки, порядок, `[0,1]`, опциональный score)
230 - - `evaluate_predictions.py` сделать тонкой обёрткой или удалить
227 + Варианты, дешевле → дороже:
231 228
232 - **Проверка после каждого подшага:** `python test_features_v2.py` + повтор инференса на 1 000 пар, macro не меняется (кроме Фазы 4, где схема менялась специально).
229 + 1. Забрать их `artifacts/catboost_v5_llm8m.cbm` + `src/features.py` и смешать с твоим E5 (быстрый потолок 0.43456).
230 + 2. Обучить свой HGB/CatBoost на LLM-миксе (`scripts/train_gbdt_llm.py` у них) на **твоих** V2 (+ допуск ±5%, battery).
231 + 3. Стек: их 32 фичи ∪ твои слоты/измерения.
233 232
234 - ---
233 + Не гнать 2M→8M: у них +0.003 прокси / +0.001 LB, насыщение.
235 234
236 - ## Фаза 6. Минимальные тесты
235 + ### 6.2. Категорийные веса — только на прокси
237 236
238 - Пока нет pytest-конфига — добавить `requirements-dev.txt` (`pytest`) и эти файлы:
237 + Твой `tune_category_blend.py` оставить, **данные сменить** на прокси. Human cross-fit не использовать как accept-gate.
239 238
240 - | Тест | Что закрывает |
241 - |---|---|
242 - | `tests/test_validation.py` | `item_overlap_count == 0` на `make_disjoint_folds`; все пары имеют fold |
243 - | `tests/test_features.py` | swap-invariance; длина == `len(FEATURE_NAMES)`; finite; battery kind есть |
244 - | `tests/test_text.py` | `product_text` стабилен |
245 - | `tests/test_verify.py` | плохой CSV (NaN, другой порядок, лишняя колонка) падает |
246 - | `tests/test_packaging.py` | namelist zip содержит обязательные пути; `ensemble.json` содержит веса |
239 + ### 6.3. Второй CE
247 240
248 - `test_features_v2.py` перенести в `tests/` и убрать хак `experiments_next`.
241 + Когда будет base или USER-base: бленд small+base (+ LLM-табличка третьим). Веса по прокси, `tune_three_blend.py` уже есть. Два разных CE обычно дают больше, чем ещё одна табличка.
249 242
250 - **Готово, когда:** `pytest -q` зелёный без GPU и без `data/`.
243 + **Готово, когда:** прокси ансамбля > прокси лучшего одиночного CE, полный прогон < 780 с, zip < 4.7 GB.
251 244
252 245 ---
253 246
254 - ## Фаза 7. Гигиена обучения (не меняет текущий zip, пока не ретрейнишь E5)
247 + ## Фаза 7. Инженерный долг (не блокирует H100)
255 248
256 - В `train_cross_encoder.py`:
249 + Делать в паузах между ночными трейнами.
257 250
258 - 1. `drop_last=False` на последней эпохе human-finetune (на pretrain 11M можно оставить True). Либо логировать, сколько примеров отброшено.
259 - 2. Swap: `torch.Generator` / `np.random.default_rng(seed + worker_id)`, не голый `np.random.random()` в collator с `num_workers>0`.
260 - 3. Раз в N шагов — macro AP на 2–4k валидационных пар фолда (не полный eval). Сейчас качество видно только после отдельного скрипта.
261 - 4. `problem_type`: либо убрать, либо поставить комментарий «unused; loss is custom BCE».
251 + ### 7.1. Дубли
262 252
263 - `scripts/train_gpu_pipeline.sh`: дефолт `PYTHON_BIN=.venv-autogluon/bin/python` переименовать в `.venv-gpu` / `requirements-gpu.txt`. Имя сейчас врёт.
253 + - `macro_average_precision` только в `validation.py`
254 + - `product_text` только в `solution/transformer.py` (+ тест vs DuckDB)
255 + - set-helpers в одном модуле
256 + - `category_ranks` только в `tune_blend.py`
257 + - канон проверки CSV: `verify_candidate_output.py`
264 258
265 - **Готово, когда:** один fold переобучается воспроизводимо (тот же seed → тот же loss на step 50). Старый лидерборд-чекпоинт не перезаписывать, пока 5-fold mean не выше текущего.
259 + ### 7.2. Тесты без GPU и без `data/`
266 260
267 - ---
261 + - фолды: overlap == 0
262 + - фичи: swap-invariance, длина схемы, battery, finite
263 + - verify: NaN / чужой порядок падают
264 + - zip: namelist + есть `neural_weight`
268 265
269 - ## Фаза 8. Раскладка репозитория (последней)
266 + Убрать хак `experiments_next` из `test_features_v2.py`.
270 267
271 - Только когда Фазы 1, 5, 6 зелёные. Иначе сломаешь упаковку.
268 + ### 7.3. Гигиена `train_cross_encoder.py`
272 269
273 - ```text
274 - solution/ # runtime, попадает в zip
275 - training/ # train_cross_encoder, train_compact, hard-mine, soup
276 - eval/ # evaluate_*, tune_*, verify, analyze_*
277 - packaging/ # make_ensemble_submission, make_gpu_submission
278 - experiments/ # autogluon, qwen, proxy, distill, _audit_*
279 - scripts/
280 - tests/
281 - ```
270 + - `worker_init_fn` для swap (коллега и разбор: глобальный numpy RNG коррелирует воркеры)
271 + - `drop_last=False` на коротком human/микс этапе; на 11M pretrain можно оставить True
272 + - раз в N шагов — прокси на 2–4k пар, не ждать конца
273 + - в `train_gpu_pipeline.sh` дефолт `.venv-autogluon` → `.venv-gpu`
282 274
283 - После переноса:
275 + ### 7.4. Раскладка репо — последней
284 276
285 - - поправить импорты
286 - - поправить пути в `make_ensemble_submission.py` (`solution/...` остаются)
287 - - прогнать Фазу 0 и pytest
277 + Только когда 1.x и тесты зелёные:
288 278
289 - В zip по-прежнему только `run.py`, `metadata.json`, `ensemble.json`, `solution/*`, веса. Никакого `experiments/`.
290 -
291 - Git: `git init` + текущий `.gitignore` (уже игнорирует `data/`, `models/`). Не коммитить parquet/веса.
279 + `solution/` runtime, `training/`, `eval/`, `packaging/`, `experiments/` (AutoGluon, Qwen-as-runtime, proxy leftovers).
292 280
293 281 ---
294 282
295 - ## Фаза 9. Score — только после закрытия дефектов
283 + ## Фаза 8. Тяжёлое, если 5.x выдохлись
296 284
297 - Это уже не «починить дыры», а следующие эксперименты. Делать по одному, каждый с 5-fold (или хотя бы fold 0 + fold 4).
285 + Qwen как runtime не возвращать (0.53 на human-fold, медленный). H100 между экспериментами:
298 286
299 - 1. **Soup вес по OOF**, не фиксированный 0.10. Сетка `tuned_weight ∈ {0.05, 0.10, 0.20, 0.30}` через `merge_transformer_weights.py` + `evaluate_cross_encoder.py`.
300 - 2. **Hard negatives из каталога**, не только из размеченных пар: near-miss того же бренда / другого артикула. Текущий `build_oof_hard_training.py` этого не делает.
301 - 3. **Категории, где `neural_weight` < 0.7** (Автотовары, Обувь, Ювелирка, Мебель): отдельные conflict-фичи, не общий V2.
302 - 4. **Qwen не трогать**, пока teacher < 0.65 на том же fold. Сейчас 0.53, в zip не класть.
303 - 5. AutoGluon в runtime не возвращать.
287 + 1. Переразметить серую зону 11.2M (метка 0.3–0.7 или сильный разъезд E5 vs табличка) через Qwen3-8B/32B-Instruct (Apache 2.0). Код почти готов: `build_teacher_distillation_data.py`.
288 + 2. Дистилляция: teacher-ансамбль (E5-base + CatBoost + …) → 11M мягких меток → студент small/base. В zip одна быстрая модель.
304 289
305 - Критерий принятия любого эксперимента: 5-fold mean выше текущего **и** архив < 4.7 GB **и** полный прогон в образе < лимита времени.
290 + Ожидание неизвестно, 2–3 дня H100. Не начинать, пока не закрыты Фазы 2 и 5.1–5.2.
306 291
307 - ---
292 + Human fold 4: посчитать как вторичную метрику, когда будет простой. **Не ждать его**, чтобы выбрать чекпоинт или веса бленда.
308 293
309 - ## Порядок на календарь
294 + Честные baselines в README: leaky 0.59 не ставить рядом с disjoint 0.73 и не ставить их рядом с LB 0.43 без подписи protocol.
310 295
311 - | День | Что | Нужен GPU? |
312 - |---|---|---|
313 - | 1 | Фаза 0 + 1 (runtime/конфиг/упаковщик/README) | нет, кроме короткого verify |
314 - | 1–2 | Фаза 6 тесты + Фаза 5 дубли | нет |
315 - | 2 | Фаза 4.1–4.2 фичи, ретрейн compact | CPU достаточно |
316 - | 2–3 | Фаза 2 fold 4 E5 + tabular + blend | да, H100 |
317 - | 3 | Фаза 3 честные baselines в README | нет, если OOF уже есть |
318 - | 4 | Фаза 8 раскладка, если всё зелёное | нет |
319 - | дальше | Фаза 9 по одному эксперименту | да |
296 + ---
320 297
321 - Не параллелить Фазу 8 с Фазой 1–2. Не менять фичи и entry point в одном коммите.
298 + ## Календарь на неделю (сшивка двух планов)
299 +
300 + | Слот | Что | GPU | Зачем |
301 + |---|---|---|---|
302 + | День 1 утро | Фаза 0 + 1 (якоря, `run.py`, zip, README) | нет | не отправить не тот архив |
303 + | День 1 день | Фаза 2: прокси на этапе 1 / этапе 2 / супах 0.3–1.0 | мало | самый дешёвый LB |
304 + | День 1–2 | Фаза 3.1–3.2 + Фаза 4 (V2 parallel, battery, ±5%) | нет | бюджет на base |
305 + | Ночь 1–2 | Фаза 5.2: претрейн 2–3 эпохи small | H100 | недообучен на LLM |
306 + | День 2 | Фаза 5.1: мягкий этап 2 (микс), выбор по прокси | H100 | не уехать с теста |
307 + | День 2–3 | Фаза 6.1–6.2: LLM-табличка + веса на прокси | CPU | их +0.004 уже доказан |
308 + | Ночь 3 | Фаза 5.3: e5-base и/или USER-base | H100 | крупный прирост |
309 + | День 4 | Фаза 6.3: бленд small+base+табличка по прокси | мало | |
310 + | Паузы | Фаза 7 тесты/дубли | нет | |
311 + | Если плато | Фаза 8 переразметка | H100 | |
312 +
313 + Не параллелить перенос папок (7.4) с Фазой 1–2.
314 + Не менять фичи и entry point в одном коммите.
315 + Не запускать 4 эпохи human и hard-mine по human-OOF — это уже опровергнутый путь.
322 316
323 317 ---
324 318
325 - ## Definition of Done по всему анализу
326 -
327 - Репозиторий закрыт, когда одновременно верно:
319 + ## Definition of Done
328 320
329 - 1. `python run.py ...` локально = то, что в `submission.zip`.
321 + 1. `python run.py` локально = содержимое `submission.zip`.
330 322 2. Один `ensemble.json` с весами, один упаковщик.
331 - 3. `make_submission.py` не может собрать невалидный GPU+joblib архив.
332 - 4. Оценены все заявленные фолды (5 или честно 4).
333 - 5. В README нет сравнения leaky holdout с disjoint CV.
334 - 6. Battery kind есть в схеме; пустые атрибуты согласованы; есть тест.
335 - 7. Метрика, текст пары, set-helpers, verify — в одном месте.
336 - 8. `pytest -q` зелёный без данных соревнования.
337 - 9. Якорь `submit-reference.csv` либо byte-identical, либо заново зафиксирован после сознательного изменения фич/весов.
338 - 10. Следующий score-эксперимент не начат, пока пункты 1–9 не выполнены.
339 -
340 - Если нужно, в Agent mode могу идти по этому чеклисту с Фазы 1 и не трогать обучение, пока runtime и упаковка не сойдутся.
323 + 3. Есть неизменяемый LLM-прокси; все решения в одной таблице прокси / (опц.) LB.
324 + 4. Выбран чекпоинт по прокси, не по концу 4-й human-эпохи.
325 + 5. V2 в контейнере < 50 с; полный прогон < 780 с; zip < 4.7 GB.
326 + 6. Battery + ±5% в схеме; табличка, если есть, обучена на LLM-миксе.
327 + 7. Категорийные веса, если есть, подобраны на прокси.
328 + 8. Human-CV в README помечен как вторичный; leaky 0.59 не сравнивается с LB.
329 + 9. `pytest -q` зелёный без данных соревнования.
330 + 10. В бэклоге нет bi-encoder / AutoGluon / human hard-mine / Qwen-runtime.
331 +
332 + Если переключишься в Agent mode, логичный старт — Фаза 0+1 (не ломает обучение), сразу за ней скрипт прокси-оценки существующих чекпоинтов: это единственный пункт, который коллега оценивает как возможный +0.01–0.02 без новой ночи H100.

drholy revised this gist 1 month ago. Go to revision

1 file changed, 208 insertions, 346 deletions

changes.md

@@ -1,478 +1,340 @@
1 - # Полный отчёт по выполненной работе
1 + Ниже план закрытия дыр **по порядку**. Сначала фиксируется, что едет в архив, потом CV, потом баги фич, потом уборка. Иначе легко «починить структуру» и отправить не тот zip.
2 2
3 - ## 1. Инфраструктура
4 -
5 - Проект развёрнут в двух местах:
6 -
7 - - локальный репозиторий: `A:\projects\ecup-matching`;
8 - - GPU-сервер: `/home/ubuntu/ecup-matching`;
9 - - оборудование сервера: **1× NVIDIA H100 NVL, 94 ГБ VRAM**, 24 CPU-потока, около 128 ГБ RAM;
10 - - репозиторий: `https://git.byte-mate.ru/drholy/ecup-matching.git`;
11 - - ветка: `main`.
12 -
13 - Все итоговые изменения отправлены в Gitea. Текущий коммит:
14 -
15 - ```text
16 - 1c7447e31c88c8f94d5409e5b9d4d7c30e17d083
17 - ```
18 -
19 - Локальный `HEAD` совпадает с `origin/main`.
3 + Каждый шаг: что сделать → как проверить → когда считать закрытым.
20 4
21 5 ---
22 6
23 - ## 2. Анализ задачи и данных
24 -
25 - Были обработаны официальные файлы:
7 + ## Фаза 0. Заморозить текущий кандидат
26 8
27 - - `items.parquet` — полный каталог из **13 397 761 товаров**;
28 - - `matches.parquet` — **365 654 пары** с человеческой разметкой;
29 - - `matches_llm.parquet` — **11 187 780 пар** с LLM-разметкой.
9 + Пока это не сделано, ничего не переименовывать и не двигать файлы.
30 10
31 - Проверено, что человеческая и LLM-выборки не имеют общих:
11 + 1. Записать на бумаге (или в один файл `ACTIVE.md`, если переключишься в Agent mode) четыре факта:
12 + - какой чекпоинт E5: путь к `model.safetensors`
13 + - какой tabular: путь к `*.joblib` и `sklearn` version
14 + - какой конфиг бленда: сейчас это `ensemble-candidate.json`
15 + - SHA текущего `submission.zip`: в README это `21767a7d…`, сверить `Get-FileHash`
32 16
33 - - пар товаров;
34 - - идентификаторов товаров.
17 + 2. Прогнать эталон на тех же данных, что в README:
35 18
36 - Это позволило использовать LLM-данные для предварительного обучения без прямого пересечения с human-выборкой.
19 + ```bash
20 + python -u run_ensemble_candidate.py \
21 + --items_path data/items_human.parquet \
22 + --matches_path data/matches.parquet \
23 + --output_path submit-reference.csv \
24 + --model-path <E5_DIR> \
25 + --tabular-path <COMPACT.joblib> \
26 + --config-path ensemble-candidate.json
37 27
38 - Для корректной локальной оценки применено разбиение по связанным компонентам идентификаторов товаров. Один товар не может попасть одновременно в train и validation. Это защищает метрику от утечки через повторяющиеся товары.
28 + python -u verify_candidate_output.py \
29 + --matches-path data/matches.parquet \
30 + --prediction-path submit-reference.csv \
31 + --items-path data/items_human.parquet
32 + ```
39 33
40 - Основная метрика — средний PR-AUC по 20 категориям товаров.
34 + **Готово, когда:** CSV валиден, число строк 365 654, refit macro совпадает с `0.785309497` (±1e-9). Этот CSV — регрессионный якорь. Любой следующий рефакторинг runtime обязан дать byte-identical или score-identical файл.
41 35
42 36 ---
43 37
44 - ## 3. Исходная нейросетевая модель
38 + ## Фаза 1. Один runtime, один конфиг, один упаковщик
45 39
46 - Основной моделью выбрана:
40 + Это закрывает самую опасную дыру: локальный `run.py` ≠ архив.
47 41
48 - ```text
49 - intfloat/multilingual-e5-small
50 - ```
42 + ### Шаг 1.1. Сделать `run.py` боевым
51 43
52 - Она была преобразована в cross-encoder, получающий на вход два товара:
44 + - Перенести логику из `run_ensemble_candidate.py` в `run.py` (не alias в обратную сторону: организатор вызывает `python -u run.py`).
45 + - `run_ensemble_candidate.py` и `run_gpu_candidate.py` оставить на 3–5 строк: `from run import main`.
46 + - Дефолты оставить как в кандидате: `models/cross-encoder`, `models/compact.joblib`, `ensemble.json`.
53 47
54 - - название;
55 - - категорию;
56 - - атрибуты.
48 + **Проверка:** повтор Фазы 0 через `run.py`. Diff с `submit-reference.csv` пустой.
57 49
58 - Обучение состояло из двух этапов:
50 + ### Шаг 1.2. Один `ensemble.json`
59 51
60 - 1. Предварительное обучение на 11,18 млн LLM-пар.
61 - 2. Дообучение на 365 654 human-парах.
52 + - Содержимое `ensemble-candidate.json` → `ensemble.json`.
53 + - `ensemble-candidate.json` удалить после того, как все скрипты/README ссылаются на `ensemble.json`.
54 + - В `run.py` падать явно, если нет `neural_weight` (сейчас молча получится transformer-only, если кто-то вызовет старый код).
62 55
63 - Использовались:
56 + **Проверка:** `python -c "import json; c=json.load(open('ensemble.json')); assert 'neural_weight' in c and 'neural_weight_by_category' in c"`
64 57
65 - - BF16;
66 - - CUDA;
67 - - сортировка пар по длине;
68 - - симметричная перестановка товаров;
69 - - category-balanced loss;
70 - - длина последовательности 160 токенов.
58 + ### Шаг 1.3. Убить сломанный CPU-упаковщик
71 59
72 - Первоначальное предобучение проводилось на 3× RTX 4090, затем все новые эксперименты и финальная сборка выполнялись на одной H100.
60 + - `make_submission.py` удалить или переименовать в `experiments/make_cpu_submission.py` и в первой строке docstring написать `DEAD: packs model.joblib with GPU run.py`.
61 + - Канон один: `make_ensemble_submission.py`.
62 + - `make_gpu_submission.py` оставить только как «E5-only fallback», в docstring явно: не текущий лидерборд-кандидат.
73 63
74 - Результаты E5 на четырёх независимых фолдах:
64 + **Проверка:** в корне одна команда сборки архива. В zip есть `run.py`, `ensemble.json` с весами, `solution/features.py`, `solution/features_v2.py`, `models/compact.joblib`, `models/cross-encoder/model.safetensors`. Нет `model.joblib` в корне архива.
75 65
76 - | Фолд | Macro PR-AUC |
77 - |---:|---:|
78 - | 0 | 0,731129 |
79 - | 1 | 0,726350 |
80 - | 2 | 0,732702 |
81 - | 3 | 0,721693 |
82 - | **Среднее** | **0,727969** |
66 + ```bash
67 + python make_ensemble_submission.py \
68 + --model-dir <E5_DIR> \
69 + --tabular-model <COMPACT.joblib> \
70 + --config-path ensemble.json \
71 + --run-path run.py \
72 + --output-path submission.zip
73 + ```
83 74
84 - Стандартное отклонение: `0,004277`.
75 + Размер < 4.7 GB, `zipfile.ZipFile.testzip()` чистый.
85 76
86 - ---
77 + ### Шаг 1.4. Синхронизировать README и `metadata.json`
87 78
88 - ## 4. Новые структурные признаки
79 + В README оставить **один** блок «Current submission»:
89 80
90 - Разработано **68 симметричных V2-признаков**. Они одинаково работают для пар `(товар A, товар B)` и `(товар B, товар A)`.
81 + - entry point: `python -u run.py`
82 + - состав: E5 soup + compact V2 + category weights
83 + - команда упаковки
84 + - SHA и размер
85 + - CV-оценка (disjoint folds), отдельно refit sanity
91 86
92 - Добавлены признаки для:
87 + Убрать противоречие «active = E5-only» в начале файла. Исторический CPU/AutoGluon — в секцию `Historical`, с пометкой «другой validation protocol».
93 88
94 - - точного и нормализованного совпадения названий;
95 - - token Dice и символьного Jaccard;
96 - - совпадения префиксов и суффиксов;
97 - - кодов моделей и артикулов;
98 - - длинных числовых идентификаторов;
99 - - брендов;
100 - - цветов;
101 - - размеров;
102 - - материалов;
103 - - количества товаров в комплекте;
104 - - страны производства;
105 - - пола;
106 - - совместимости;
107 - - веса, объёма, длины, мощности и памяти;
108 - - совпадения ключей и значений атрибутов;
109 - - критических совпадений и противоречий.
89 + **Готово, когда:** несуществующий человек по README собирает тот же zip, что на лидерборде.
110 90
111 - Парсинг JSON-атрибутов оптимизирован: каждый товар разбирается один раз, а не повторно для каждой пары.
91 + ---
112 92
113 - Для признаков добавлены тесты:
93 + ## Фаза 2. Закрыть CV: fold 4
114 94
115 - - симметричность;
116 - - инвариантность;
117 - - совпадение кэшированного и прямого расчёта;
118 - - корректность схемы признаков.
95 + Без этого mean 0.728 / 0.738 — оценка по 4/5 схемы.
119 96
120 - Тест завершился успешно:
97 + ### Шаг 2.1. Обучить и оценить E5 на fold 4
121 98
122 - ```text
123 - feature invariance and cached parsing tests passed
99 + ```bash
100 + bash scripts/train_gpu_pipeline.sh 4
124 101 ```
125 102
126 - ---
127 -
128 - ## 5. Компактная табличная модель
103 + Ожидаемые артефакты:
129 104
130 - Поверх V2-признаков обучен компактный ансамбль `HistGradientBoostingClassifier`:
105 + - `models/multilingual-e5-fold4/`
106 + - `models/multilingual-e5-fold4/validation.npz`
107 + - рядом `.json` с macro AP
131 108
132 - - одна глобальная модель;
133 - - отдельные модели по каждой из 20 категорий;
134 - - взвешивание классов;
135 - - смешивание глобального и категорийного прогнозов.
109 + ### Шаг 2.2. Табличный OOF fold 4
136 110
137 - Артефакт был обучен и сериализован непосредственно в официальном Docker-образе:
111 + Та же команда, которой считались фолды 0–3 (у тебя это `evaluate_tabular_fold.py` / `evaluate_v2_folds.py` + кэш V2). Нужны:
138 112
139 - ```text
140 - odsai/ecup26-matching-baseline:1.0
141 - ```
113 + - V2 (и base, если ансамбль `base_v2`) фичи на всех human pairs
114 + - OOF fold 4 в том же формате `indices/prediction/target/categories`
142 115
143 - Использован `scikit-learn 1.9.0`, соответствующий окружению проверки. Это устранило предыдущую ошибку:
116 + ### Шаг 2.3. Пересчитать бленд на 5 фолдах
144 117
145 - ```text
146 - ModuleNotFoundError: No module named '_loss'
118 + ```bash
119 + python merge_oof_predictions.py ... # все 5 фолдов
120 + python tune_blend.py --neural-path ... --tabular-path ... --output-path ...
121 + python tune_category_blend.py --neural-path ... --tabular-path ... --output-path ...
147 122 ```
148 123
149 - Размер табличного артефакта — всего **5,1 МБ**.
124 + Правило: **новые category weights попадают в `ensemble.json` только если `accepted=true` и crossfit_gain > 0 на 5 фолдах**. Иначе оставить старые веса.
150 125
151 - Самостоятельный результат V2-модели на fold 0:
126 + ### Шаг 2.4. Обновить `gpu_cv.metrics.json`
152 127
153 - ```text
154 - Macro PR-AUC: 0,613059
155 - ```
128 + Поля, которые должны появиться:
129 +
130 + - `evaluated_folds: [0,1,2,3,4]`
131 + - mean/std E5 по 5 фолдам
132 + - mean/std E5+V2 по 5 фолдам
133 + - новый global / category blend, если принят
156 134
157 - Её основное назначение — дополнять нейросеть точными структурными сигналами, а не работать отдельно.
135 + **Готово, когда:** нет дыры «fold 4 missing». Если fold 4 принципиально holdout — тогда в метриках `n_splits=4` и в `prepare_transformer_data.py` тоже 4, а не 5. Смешивать нельзя.
158 136
159 137 ---
160 138
161 - ## 6. Ансамбль E5 + V2
139 + ## Фаза 3. Честные baselines
162 140
163 - Глобально оптимальный вес составил:
141 + Закрывает ложное «+0.14 от модели».
164 142
165 - ```text
166 - 80% E5 + 20% V2
167 - ```
143 + ### Шаг 3.1. Не трогать старый `train.py` как источник цифр
168 144
169 - Результаты на четырёх фолдах:
145 + Либо пометить deprecated, либо перевести на `make_disjoint_folds` из `validation.py`. Новые цифры писать только с disjoint protocol.
170 146
171 - | Фолд | E5 + V2 |
172 - |---:|---:|
173 - | 0 | 0,740693 |
174 - | 1 | 0,734238 |
175 - | 2 | 0,742041 |
176 - | 3 | 0,733032 |
177 - | **Среднее** | **0,737501** |
147 + ### Шаг 3.2. Пересчитать CPU/HGB и AutoGluon на тех же 5 фолдах
178 148
179 - Прирост относительно E5:
149 + - HGB base features: `evaluate_tabular_fold.py` на фолдах 0–4
150 + - V2 compact: уже будет из Фазы 2
151 + - AutoGluon — опционально, только если нужен абзац в README
180 152
181 - ```text
182 - +0,009532 Macro PR-AUC
183 - ```
153 + В README таблица:
184 154
185 - Дополнительно рассчитаны индивидуальные веса ансамбля для каждой категории. Для защиты от переобучения их качество проверялось cross-fit способом:
155 + | Модель | Protocol | Mean macro PR-AUC |
156 + |---|---|---|
157 + | HGB base | disjoint 5-fold | … |
158 + | E5-small | disjoint 5-fold | … |
159 + | E5 + V2 | disjoint 5-fold | … |
160 + | refit train (sanity) | не оценка | 0.785 |
186 161
187 - ```text
188 - Глобальный вес: 0,736345
189 - Cross-fit категорийные веса: 0,737900
190 - Прирост от категорийных весов: +0,001555
191 - ```
162 + Старые 0.593 / 0.596 оставить только как «leaky 25% holdout, do not compare».
192 163
193 - Именно категорийные веса включены в текущий `submission.zip`.
164 + **Готово, когда:** в README нет рядом стоящих чисел с разным protocol без пометки.
194 165
195 166 ---
196 167
197 - ## 7. Proxy-выборка и hard-negative mining
168 + ## Фаза 4. Баги фич (меняют матрицу → нужен ретрейн tabular)
198 169
199 - Созданы leakage-safe proxy-выборки:
170 + Делать **после** якоря Фазы 0 и **до** большого рефакторинга папок.
200 171
201 - - сложные положительные пары;
202 - - сложные отрицательные пары;
203 - - anchor-примеры;
204 - - пары с максимальной ошибкой OOF-моделей.
172 + ### Шаг 4.1. Battery в V2
205 173
206 - Для fold 0 было собрано 76 393 hard-примеров. Для финального обучения — 78 366 примеров.
174 + В `solution/features_v2.py`:
207 175
208 - На них выполнено дополнительное обучение E5. Сам hard-finetuned checkpoint оказался хуже базового:
176 + - добавить `"battery"` в `MEASUREMENT_KINDS`
177 + - проверить, что `FEATURE_NAMES_V2` удлинился на 2 колонки (`v2_battery_measurement_match/conflict`)
178 + - прогнать `test_features_v2.py`
209 179
210 - ```text
211 - Обычная E5, fold 0: 0,731129
212 - Hard E5, fold 0: 0,725666
213 - ```
180 + Потом пересобрать кэш и compact-модель. Старый `compact.joblib` с новой схемой **несовместим** — `run.py` уже падает на `features.shape[1] != len(feature_names)`, это хорошо.
214 181
215 - Поэтому hard-модель не была использована напрямую.
182 + ### Шаг 4.2. Пустые атрибуты
216 183
217 - Вместо этого построен model soup:
184 + Зафиксировать контракт и покрыть тестом:
218 185
219 - ```text
220 - 90% исходных весов + 10% hard-mined весов
221 - ```
186 + - оба без атрибутов → `common_attribute_value_ratio = 1.0` (как Jaccard)
187 + - ровно один без атрибутов → 0.0
222 188
223 - Model soup сохраняет исходный размер модели и дал небольшой положительный результат:
189 + Сейчас пустое ∩ пустое даёт 0.0 при Jaccard 1.0.
224 190
225 - ```text
226 - E5 baseline, fold 0: 0,731129
227 - E5 hard soup, fold 0: 0,731486
228 - ```
191 + ### Шаг 4.3. TF-IDF leakage-check (не обязательно в runtime)
229 192
230 - Вместе с V2:
193 + Один скрипт: на fold 0 fit TF-IDF только на train items, сравнить macro AP с transductive fit.
194 + - если дельта < 0.001 — оставить transductive (для теста соревнования это правильный режим)
195 + - если больше — в CV всегда fit на train, в `run.py` оставить fit на переданных items
231 196
232 - ```text
233 - E5 hard soup + V2, fold 0: 0,741258
234 - ```
235 -
236 - Этот model soup входит в финальный архив.
197 + **Готово, когда:** новый V2 кэш + новый joblib + новый zip проходят verify; OOF fold 0 не просел. Если просел — revert 4.1/4.2 по отдельности.
237 198
238 199 ---
239 200
240 - ## 8. Эксперимент с Qwen 4B
201 + ## Фаза 5. Свести дубли в одно место
241 202
242 - В качестве более крупной русскоязычной модели исследована:
203 + Не двигать каталоги, только импорты.
243 204
244 - ```text
245 - Qwen/Qwen3-Reranker-4B
246 - ```
205 + ### Шаг 5.1. Метрика
247 206
248 - Использовались:
207 + - Оставить `macro_average_precision` только в `validation.py`
208 + - Удалить копии из `train.py` и `train_autogluon.py`
249 209
250 - - H100 NVL;
251 - - NF4-квантизация;
252 - - QLoRA;
253 - - rank 16;
254 - - BF16-вычисления;
255 - - 1000 шагов дообучения.
210 + ### Шаг 5.2. Текст пары
256 211
257 - Результат Qwen отдельно:
212 + - `product_text` только в `solution/transformer.py`
213 + - `prepare_transformer_data.py` импортирует его
214 + - Добавить тест: Python-строка == ожидаемый шаблон `Название:\nКатегория:\nАтрибуты:` (DuckDB-SQL сверить тем же тестом на 10 строках)
258 215
259 - ```text
260 - Fold 0 Macro PR-AUC: 0,532836
261 - Инференс 73 080 пар: около 690 секунд
262 - Пиковая память: около 62,7 ГБ
263 - ```
216 + ### Шаг 5.3. Сет-метрики
264 217
265 - Модель оказалась слишком медленной и слабой для непосредственного включения в submission.
218 + - `_jaccard`, `_overlap`/`_overlap_min`, `_conflict`, `_ratio` — один модуль, например `solution/set_metrics.py`
219 + - `features.py` и `features_v2.py` импортируют оттуда
266 220
267 - Как дополнительный OOF-сигнал она давала небольшой прирост:
221 + ### Шаг 5.4. Blend helpers
268 222
269 - ```text
270 - E5 soup + base/V2: 0,744664
271 - E5 soup + base/V2 + Qwen: 0,745477
272 - ```
223 + - `category_ranks` только в `tune_blend.py`
224 + - `tune_category_blend.py` импортирует, свою копию удалить
225 + - `category_class_weights` — в `train.py` или `validation.py`, один раз
273 226
274 - Однако Qwen не включена в runtime, потому что:
227 + ### Шаг 5.5. Verify
275 228
276 - - одна только модель превышает допустимый практический бюджет;
277 - - даже INT4-инференс слишком медленный;
278 - - итоговый архив должен быть меньше 5 ГБ;
279 - - решение должно укладываться в лимит времени.
229 + - Оставить `verify_candidate_output.py` как канон (колонки, порядок, `[0,1]`, опциональный score)
230 + - `evaluate_predictions.py` сделать тонкой обёрткой или удалить
280 231
281 - Qwen сохранена как экспериментальный offline teacher.
232 + **Проверка после каждого подшага:** `python test_features_v2.py` + повтор инференса на 1 000 пар, macro не меняется (кроме Фазы 4, где схема менялась специально).
282 233
283 234 ---
284 235
285 - ## 9. Эксперименты с AutoGluon
286 -
287 - AutoGluon был протестирован на созданных признаках.
236 + ## Фаза 6. Минимальные тесты
288 237
289 - Полученный результат:
238 + Пока нет pytest-конфига — добавить `requirements-dev.txt` (`pytest`) и эти файлы:
290 239
291 - ```text
292 - Macro PR-AUC: около 0,6425
293 - ```
240 + | Тест | Что закрывает |
241 + |---|---|
242 + | `tests/test_validation.py` | `item_overlap_count == 0` на `make_disjoint_folds`; все пары имеют fold |
243 + | `tests/test_features.py` | swap-invariance; длина == `len(FEATURE_NAMES)`; finite; battery kind есть |
244 + | `tests/test_text.py` | `product_text` стабилен |
245 + | `tests/test_verify.py` | плохой CSV (NaN, другой порядок, лишняя колонка) падает |
246 + | `tests/test_packaging.py` | namelist zip содержит обязательные пути; `ensemble.json` содержит веса |
294 247
295 - Это хуже специализированной компактной модели и не улучшило ансамбль достаточно, чтобы оправдать:
248 + `test_features_v2.py` перенести в `tests/` и убрать хак `experiments_next`.
296 249
297 - - больший размер;
298 - - более сложную сериализацию;
299 - - риск несовместимости;
300 - - дополнительное время инференса.
301 -
302 - Поэтому AutoGluon не включён в финальное решение, но воспроизводимые скрипты оставлены в репозитории.
250 + **Готово, когда:** `pytest -q` зелёный без GPU и без `data/`.
303 251
304 252 ---
305 253
306 - ## 10. Финальная архитектура
307 -
308 - Текущий `submission.zip` содержит:
309 -
310 - 1. `multilingual-e5-small` model soup:
311 - - 90% исходного checkpoint;
312 - - 10% hard-mined checkpoint.
254 + ## Фаза 7. Гигиена обучения (не меняет текущий zip, пока не ретрейнишь E5)
313 255
314 - 2. Компактный HGB-ансамбль:
315 - - 68 V2-признаков;
316 - - глобальная модель;
317 - - 20 категорийных моделей.
256 + В `train_cross_encoder.py`:
318 257
319 - 3. Категорийные веса E5/V2.
258 + 1. `drop_last=False` на последней эпохе human-finetune (на pretrain 11M можно оставить True). Либо логировать, сколько примеров отброшено.
259 + 2. Swap: `torch.Generator` / `np.random.default_rng(seed + worker_id)`, не голый `np.random.random()` в collator с `num_workers>0`.
260 + 3. Раз в N шагов — macro AP на 2–4k валидационных пар фолда (не полный eval). Сейчас качество видно только после отдельного скрипта.
261 + 4. `problem_type`: либо убрать, либо поставить комментарий «unused; loss is custom BCE».
320 262
321 - 4. GPU-инференс:
322 - - CUDA;
323 - - BF16;
324 - - SDPA attention;
325 - - автоматический batch size 4096 для H100;
326 - - сортировка пар по длине.
263 + `scripts/train_gpu_pipeline.sh`: дефолт `PYTHON_BIN=.venv-autogluon/bin/python` переименовать в `.venv-gpu` / `requirements-gpu.txt`. Имя сейчас врёт.
327 264
328 - 5. Потоковое чтение `items.parquet`, чтобы не загружать полный каталог в pandas.
329 -
330 - Qwen и AutoGluon в архив не входят.
265 + **Готово, когда:** один fold переобучается воспроизводимо (тот же seed → тот же loss на step 50). Старый лидерборд-чекпоинт не перезаписывать, пока 5-fold mean не выше текущего.
331 266
332 267 ---
333 268
334 - ## 11. Размер итогового архива
335 -
336 - Итоговый файл:
337 -
338 - [submission.zip](A:/projects/ecup-matching/submission.zip)
269 + ## Фаза 8. Раскладка репозитория (последней)
339 270
340 - Параметры:
271 + Только когда Фазы 1, 5, 6 зелёные. Иначе сломаешь упаковку.
341 272
342 273 ```text
343 - Размер: 375 118 360 байт
344 - Размер в десятичных ГБ: около 0,375 ГБ
345 - Жёсткий лимит: 5 000 000 000 байт
346 - Запас до лимита: около 4,625 ГБ
274 + solution/ # runtime, попадает в zip
275 + training/ # train_cross_encoder, train_compact, hard-mine, soup
276 + eval/ # evaluate_*, tune_*, verify, analyze_*
277 + packaging/ # make_ensemble_submission, make_gpu_submission
278 + experiments/ # autogluon, qwen, proxy, distill, _audit_*
279 + scripts/
280 + tests/
347 281 ```
348 282
349 - SHA-256:
283 + После переноса:
350 284
351 - ```text
352 - 21767A7D41659097308EA9DD663C8387E222D25A8830A336AEED721303969173
353 - ```
354 -
355 - Архив содержит 12 файлов, включая:
285 + - поправить импорты
286 + - поправить пути в `make_ensemble_submission.py` (`solution/...` остаются)
287 + - прогнать Фазу 0 и pytest
356 288
357 - - `run.py`;
358 - - `metadata.json`;
359 - - `ensemble.json`;
360 - - `models/compact.joblib`;
361 - - E5 weights и tokenizer;
362 - - код V2-признаков.
289 + В zip по-прежнему только `run.py`, `metadata.json`, `ensemble.json`, `solution/*`, веса. Никакого `experiments/`.
363 290
364 - Целостность ZIP проверена локально и на сервере.
291 + Git: `git init` + текущий `.gitignore` (уже игнорирует `data/`, `models/`). Не коммитить parquet/веса.
365 292
366 293 ---
367 294
368 - ## 12. Проверка в официальном Docker
369 -
370 - Архив был распакован в отдельный каталог и протестирован в том же образе, который используется организаторами:
371 -
372 - ```text
373 - odsai/ecup26-matching-baseline:1.0
374 - CUDA 12.8
375 - NVIDIA H100 NVL
376 - 90 ГБ ограничения памяти
377 - ```
378 -
379 - ### Проверка на 1000 парах
380 -
381 - ```text
382 - Товаров просканировано: 13 397 761
383 - Товаров выбрано: 1 986
384 - Пар обработано: 1 000
385 - Время run.py: 45,9 секунды
386 - ```
387 -
388 - Результат укладывается в 60-секундный лимит выполнения решения.
389 -
390 - ### Полная проверка
391 -
392 - ```text
393 - Товаров выбрано: 711 304
394 - Пар обработано: 365 654
395 - Время run.py: 469,3 секунды
396 - Лимит: 13 минут / 780 секунд
397 - Запас: около 311 секунд
398 - ```
399 -
400 - Из 469,3 секунды расчёт 68 V2-признаков занял 281,2 секунды.
295 + ## Фаза 9. Score — только после закрытия дефектов
401 296
402 - Проверено:
297 + Это уже не «починить дыры», а следующие эксперименты. Делать по одному, каждый с 5-fold (или хотя бы fold 0 + fold 4).
403 298
404 - - ровно 365 654 результата;
405 - - исходный порядок пар сохранён;
406 - - колонки строго `id1,id2,predict`;
407 - - нет `NaN` и бесконечностей;
408 - - значения находятся в диапазоне `[0,1]`.
299 + 1. **Soup вес по OOF**, не фиксированный 0.10. Сетка `tuned_weight ∈ {0.05, 0.10, 0.20, 0.30}` через `merge_transformer_weights.py` + `evaluate_cross_encoder.py`.
300 + 2. **Hard negatives из каталога**, не только из размеченных пар: near-miss того же бренда / другого артикула. Текущий `build_oof_hard_training.py` этого не делает.
301 + 3. **Категории, где `neural_weight` < 0.7** (Автотовары, Обувь, Ювелирка, Мебель): отдельные conflict-фичи, не общий V2.
302 + 4. **Qwen не трогать**, пока teacher < 0.65 на том же fold. Сейчас 0.53, в zip не класть.
303 + 5. AutoGluon в runtime не возвращать.
409 304
410 - Диапазон прогнозов:
411 -
412 - ```text
413 - Минимум: 0,00553552
414 - Максимум: 0,99133800
415 - Среднее: 0,37741749
416 - ```
417 -
418 - Контрольная метрика на данных, на которых была обучена финальная модель:
419 -
420 - ```text
421 - Macro PR-AUC: 0,785309497
422 - ```
423 -
424 - Это только проверка сериализации и работоспособности. Для оценки ожидаемого качества используется leakage-safe CV около `0,7379`, а не training-метрика `0,7853`.
305 + Критерий принятия любого эксперимента: 5-fold mean выше текущего **и** архив < 4.7 GB **и** полный прогон в образе < лимита времени.
425 306
426 307 ---
427 308
428 - ## 13. Изменения в репозитории
429 -
430 - Основные коммиты:
431 -
432 - ```text
433 - 8150c28 Add H100 ensemble and QLoRA experiments
434 - a086ab3 Record H100 teacher and hard-soup results
435 - 1c7447e Promote validated H100 ensemble submission
436 - ```
309 + ## Порядок на календарь
437 310
438 - Добавлены или обновлены:
311 + | День | Что | Нужен GPU? |
312 + |---|---|---|
313 + | 1 | Фаза 0 + 1 (runtime/конфиг/упаковщик/README) | нет, кроме короткого verify |
314 + | 1–2 | Фаза 6 тесты + Фаза 5 дубли | нет |
315 + | 2 | Фаза 4.1–4.2 фичи, ретрейн compact | CPU достаточно |
316 + | 2–3 | Фаза 2 fold 4 E5 + tabular + blend | да, H100 |
317 + | 3 | Фаза 3 честные baselines в README | нет, если OOF уже есть |
318 + | 4 | Фаза 8 раскладка, если всё зелёное | нет |
319 + | дальше | Фаза 9 по одному эксперименту | да |
439 320
440 - - генератор V2-признаков;
441 - - тесты признаков;
442 - - compact model trainer;
443 - - hard-example builder;
444 - - model-soup builder;
445 - - Qwen QLoRA scripts;
446 - - AutoGluon experiments;
447 - - ансамблевый runtime;
448 - - упаковщик с жёсткой проверкой 5 ГБ;
449 - - конфигурация категорийных весов;
450 - - проверка выходного CSV;
451 - - метрики и документация;
452 - - финальный `submission.zip`.
453 -
454 - Push успешно выполнен:
455 -
456 - ```text
457 - d67c0b8..1c7447e main -> main
458 - ```
459 -
460 - Текущий локальный commit и `origin/main` совпадают.
321 + Не параллелить Фазу 8 с Фазой 1–2. Не менять фичи и entry point в одном коммите.
461 322
462 323 ---
463 324
464 - ## Итог
325 + ## Definition of Done по всему анализу
465 326
466 - Получен полностью переносимый GPU-ансамбль:
327 + Репозиторий закрыт, когда одновременно верно:
467 328
468 - ```text
469 - E5-small hard model soup + 68 V2-признаков + категорийные HGB-модели
470 - ```
471 -
472 - Ожидаемая leakage-safe CV-метрика:
473 -
474 - ```text
475 - около 0,7379 Macro PR-AUC
476 - ```
329 + 1. `python run.py ...` локально = то, что в `submission.zip`.
330 + 2. Один `ensemble.json` с весами, один упаковщик.
331 + 3. `make_submission.py` не может собрать невалидный GPU+joblib архив.
332 + 4. Оценены все заявленные фолды (5 или честно 4).
333 + 5. В README нет сравнения leaky holdout с disjoint CV.
334 + 6. Battery kind есть в схеме; пустые атрибуты согласованы; есть тест.
335 + 7. Метрика, текст пары, set-helpers, verify — в одном месте.
336 + 8. `pytest -q` зелёный без данных соревнования.
337 + 9. Якорь `submit-reference.csv` либо byte-identical, либо заново зафиксирован после сознательного изменения фич/весов.
338 + 10. Следующий score-эксперимент не начат, пока пункты 1–9 не выполнены.
477 339
478 - Это примерно на `0,0099` выше исходного E5-решения. Архив успешно протестирован в официальном Docker, укладывается в ограничения по времени и занимает только 0,375 ГБ из разрешённых 5 ГБ.
340 + Если нужно, в Agent mode могу идти по этому чеклисту с Фазы 1 и не трогать обучение, пока runtime и упаковка не сойдутся.

drholy revised this gist 1 month ago. Go to revision

1 file changed, 478 insertions

changes.md(file created)

@@ -0,0 +1,478 @@
1 + # Полный отчёт по выполненной работе
2 +
3 + ## 1. Инфраструктура
4 +
5 + Проект развёрнут в двух местах:
6 +
7 + - локальный репозиторий: `A:\projects\ecup-matching`;
8 + - GPU-сервер: `/home/ubuntu/ecup-matching`;
9 + - оборудование сервера: **1× NVIDIA H100 NVL, 94 ГБ VRAM**, 24 CPU-потока, около 128 ГБ RAM;
10 + - репозиторий: `https://git.byte-mate.ru/drholy/ecup-matching.git`;
11 + - ветка: `main`.
12 +
13 + Все итоговые изменения отправлены в Gitea. Текущий коммит:
14 +
15 + ```text
16 + 1c7447e31c88c8f94d5409e5b9d4d7c30e17d083
17 + ```
18 +
19 + Локальный `HEAD` совпадает с `origin/main`.
20 +
21 + ---
22 +
23 + ## 2. Анализ задачи и данных
24 +
25 + Были обработаны официальные файлы:
26 +
27 + - `items.parquet` — полный каталог из **13 397 761 товаров**;
28 + - `matches.parquet` — **365 654 пары** с человеческой разметкой;
29 + - `matches_llm.parquet` — **11 187 780 пар** с LLM-разметкой.
30 +
31 + Проверено, что человеческая и LLM-выборки не имеют общих:
32 +
33 + - пар товаров;
34 + - идентификаторов товаров.
35 +
36 + Это позволило использовать LLM-данные для предварительного обучения без прямого пересечения с human-выборкой.
37 +
38 + Для корректной локальной оценки применено разбиение по связанным компонентам идентификаторов товаров. Один товар не может попасть одновременно в train и validation. Это защищает метрику от утечки через повторяющиеся товары.
39 +
40 + Основная метрика — средний PR-AUC по 20 категориям товаров.
41 +
42 + ---
43 +
44 + ## 3. Исходная нейросетевая модель
45 +
46 + Основной моделью выбрана:
47 +
48 + ```text
49 + intfloat/multilingual-e5-small
50 + ```
51 +
52 + Она была преобразована в cross-encoder, получающий на вход два товара:
53 +
54 + - название;
55 + - категорию;
56 + - атрибуты.
57 +
58 + Обучение состояло из двух этапов:
59 +
60 + 1. Предварительное обучение на 11,18 млн LLM-пар.
61 + 2. Дообучение на 365 654 human-парах.
62 +
63 + Использовались:
64 +
65 + - BF16;
66 + - CUDA;
67 + - сортировка пар по длине;
68 + - симметричная перестановка товаров;
69 + - category-balanced loss;
70 + - длина последовательности 160 токенов.
71 +
72 + Первоначальное предобучение проводилось на 3× RTX 4090, затем все новые эксперименты и финальная сборка выполнялись на одной H100.
73 +
74 + Результаты E5 на четырёх независимых фолдах:
75 +
76 + | Фолд | Macro PR-AUC |
77 + |---:|---:|
78 + | 0 | 0,731129 |
79 + | 1 | 0,726350 |
80 + | 2 | 0,732702 |
81 + | 3 | 0,721693 |
82 + | **Среднее** | **0,727969** |
83 +
84 + Стандартное отклонение: `0,004277`.
85 +
86 + ---
87 +
88 + ## 4. Новые структурные признаки
89 +
90 + Разработано **68 симметричных V2-признаков**. Они одинаково работают для пар `(товар A, товар B)` и `(товар B, товар A)`.
91 +
92 + Добавлены признаки для:
93 +
94 + - точного и нормализованного совпадения названий;
95 + - token Dice и символьного Jaccard;
96 + - совпадения префиксов и суффиксов;
97 + - кодов моделей и артикулов;
98 + - длинных числовых идентификаторов;
99 + - брендов;
100 + - цветов;
101 + - размеров;
102 + - материалов;
103 + - количества товаров в комплекте;
104 + - страны производства;
105 + - пола;
106 + - совместимости;
107 + - веса, объёма, длины, мощности и памяти;
108 + - совпадения ключей и значений атрибутов;
109 + - критических совпадений и противоречий.
110 +
111 + Парсинг JSON-атрибутов оптимизирован: каждый товар разбирается один раз, а не повторно для каждой пары.
112 +
113 + Для признаков добавлены тесты:
114 +
115 + - симметричность;
116 + - инвариантность;
117 + - совпадение кэшированного и прямого расчёта;
118 + - корректность схемы признаков.
119 +
120 + Тест завершился успешно:
121 +
122 + ```text
123 + feature invariance and cached parsing tests passed
124 + ```
125 +
126 + ---
127 +
128 + ## 5. Компактная табличная модель
129 +
130 + Поверх V2-признаков обучен компактный ансамбль `HistGradientBoostingClassifier`:
131 +
132 + - одна глобальная модель;
133 + - отдельные модели по каждой из 20 категорий;
134 + - взвешивание классов;
135 + - смешивание глобального и категорийного прогнозов.
136 +
137 + Артефакт был обучен и сериализован непосредственно в официальном Docker-образе:
138 +
139 + ```text
140 + odsai/ecup26-matching-baseline:1.0
141 + ```
142 +
143 + Использован `scikit-learn 1.9.0`, соответствующий окружению проверки. Это устранило предыдущую ошибку:
144 +
145 + ```text
146 + ModuleNotFoundError: No module named '_loss'
147 + ```
148 +
149 + Размер табличного артефакта — всего **5,1 МБ**.
150 +
151 + Самостоятельный результат V2-модели на fold 0:
152 +
153 + ```text
154 + Macro PR-AUC: 0,613059
155 + ```
156 +
157 + Её основное назначение — дополнять нейросеть точными структурными сигналами, а не работать отдельно.
158 +
159 + ---
160 +
161 + ## 6. Ансамбль E5 + V2
162 +
163 + Глобально оптимальный вес составил:
164 +
165 + ```text
166 + 80% E5 + 20% V2
167 + ```
168 +
169 + Результаты на четырёх фолдах:
170 +
171 + | Фолд | E5 + V2 |
172 + |---:|---:|
173 + | 0 | 0,740693 |
174 + | 1 | 0,734238 |
175 + | 2 | 0,742041 |
176 + | 3 | 0,733032 |
177 + | **Среднее** | **0,737501** |
178 +
179 + Прирост относительно E5:
180 +
181 + ```text
182 + +0,009532 Macro PR-AUC
183 + ```
184 +
185 + Дополнительно рассчитаны индивидуальные веса ансамбля для каждой категории. Для защиты от переобучения их качество проверялось cross-fit способом:
186 +
187 + ```text
188 + Глобальный вес: 0,736345
189 + Cross-fit категорийные веса: 0,737900
190 + Прирост от категорийных весов: +0,001555
191 + ```
192 +
193 + Именно категорийные веса включены в текущий `submission.zip`.
194 +
195 + ---
196 +
197 + ## 7. Proxy-выборка и hard-negative mining
198 +
199 + Созданы leakage-safe proxy-выборки:
200 +
201 + - сложные положительные пары;
202 + - сложные отрицательные пары;
203 + - anchor-примеры;
204 + - пары с максимальной ошибкой OOF-моделей.
205 +
206 + Для fold 0 было собрано 76 393 hard-примеров. Для финального обучения — 78 366 примеров.
207 +
208 + На них выполнено дополнительное обучение E5. Сам hard-finetuned checkpoint оказался хуже базового:
209 +
210 + ```text
211 + Обычная E5, fold 0: 0,731129
212 + Hard E5, fold 0: 0,725666
213 + ```
214 +
215 + Поэтому hard-модель не была использована напрямую.
216 +
217 + Вместо этого построен model soup:
218 +
219 + ```text
220 + 90% исходных весов + 10% hard-mined весов
221 + ```
222 +
223 + Model soup сохраняет исходный размер модели и дал небольшой положительный результат:
224 +
225 + ```text
226 + E5 baseline, fold 0: 0,731129
227 + E5 hard soup, fold 0: 0,731486
228 + ```
229 +
230 + Вместе с V2:
231 +
232 + ```text
233 + E5 hard soup + V2, fold 0: 0,741258
234 + ```
235 +
236 + Этот model soup входит в финальный архив.
237 +
238 + ---
239 +
240 + ## 8. Эксперимент с Qwen 4B
241 +
242 + В качестве более крупной русскоязычной модели исследована:
243 +
244 + ```text
245 + Qwen/Qwen3-Reranker-4B
246 + ```
247 +
248 + Использовались:
249 +
250 + - H100 NVL;
251 + - NF4-квантизация;
252 + - QLoRA;
253 + - rank 16;
254 + - BF16-вычисления;
255 + - 1000 шагов дообучения.
256 +
257 + Результат Qwen отдельно:
258 +
259 + ```text
260 + Fold 0 Macro PR-AUC: 0,532836
261 + Инференс 73 080 пар: около 690 секунд
262 + Пиковая память: около 62,7 ГБ
263 + ```
264 +
265 + Модель оказалась слишком медленной и слабой для непосредственного включения в submission.
266 +
267 + Как дополнительный OOF-сигнал она давала небольшой прирост:
268 +
269 + ```text
270 + E5 soup + base/V2: 0,744664
271 + E5 soup + base/V2 + Qwen: 0,745477
272 + ```
273 +
274 + Однако Qwen не включена в runtime, потому что:
275 +
276 + - одна только модель превышает допустимый практический бюджет;
277 + - даже INT4-инференс слишком медленный;
278 + - итоговый архив должен быть меньше 5 ГБ;
279 + - решение должно укладываться в лимит времени.
280 +
281 + Qwen сохранена как экспериментальный offline teacher.
282 +
283 + ---
284 +
285 + ## 9. Эксперименты с AutoGluon
286 +
287 + AutoGluon был протестирован на созданных признаках.
288 +
289 + Полученный результат:
290 +
291 + ```text
292 + Macro PR-AUC: около 0,6425
293 + ```
294 +
295 + Это хуже специализированной компактной модели и не улучшило ансамбль достаточно, чтобы оправдать:
296 +
297 + - больший размер;
298 + - более сложную сериализацию;
299 + - риск несовместимости;
300 + - дополнительное время инференса.
301 +
302 + Поэтому AutoGluon не включён в финальное решение, но воспроизводимые скрипты оставлены в репозитории.
303 +
304 + ---
305 +
306 + ## 10. Финальная архитектура
307 +
308 + Текущий `submission.zip` содержит:
309 +
310 + 1. `multilingual-e5-small` model soup:
311 + - 90% исходного checkpoint;
312 + - 10% hard-mined checkpoint.
313 +
314 + 2. Компактный HGB-ансамбль:
315 + - 68 V2-признаков;
316 + - глобальная модель;
317 + - 20 категорийных моделей.
318 +
319 + 3. Категорийные веса E5/V2.
320 +
321 + 4. GPU-инференс:
322 + - CUDA;
323 + - BF16;
324 + - SDPA attention;
325 + - автоматический batch size 4096 для H100;
326 + - сортировка пар по длине.
327 +
328 + 5. Потоковое чтение `items.parquet`, чтобы не загружать полный каталог в pandas.
329 +
330 + Qwen и AutoGluon в архив не входят.
331 +
332 + ---
333 +
334 + ## 11. Размер итогового архива
335 +
336 + Итоговый файл:
337 +
338 + [submission.zip](A:/projects/ecup-matching/submission.zip)
339 +
340 + Параметры:
341 +
342 + ```text
343 + Размер: 375 118 360 байт
344 + Размер в десятичных ГБ: около 0,375 ГБ
345 + Жёсткий лимит: 5 000 000 000 байт
346 + Запас до лимита: около 4,625 ГБ
347 + ```
348 +
349 + SHA-256:
350 +
351 + ```text
352 + 21767A7D41659097308EA9DD663C8387E222D25A8830A336AEED721303969173
353 + ```
354 +
355 + Архив содержит 12 файлов, включая:
356 +
357 + - `run.py`;
358 + - `metadata.json`;
359 + - `ensemble.json`;
360 + - `models/compact.joblib`;
361 + - E5 weights и tokenizer;
362 + - код V2-признаков.
363 +
364 + Целостность ZIP проверена локально и на сервере.
365 +
366 + ---
367 +
368 + ## 12. Проверка в официальном Docker
369 +
370 + Архив был распакован в отдельный каталог и протестирован в том же образе, который используется организаторами:
371 +
372 + ```text
373 + odsai/ecup26-matching-baseline:1.0
374 + CUDA 12.8
375 + NVIDIA H100 NVL
376 + 90 ГБ ограничения памяти
377 + ```
378 +
379 + ### Проверка на 1000 парах
380 +
381 + ```text
382 + Товаров просканировано: 13 397 761
383 + Товаров выбрано: 1 986
384 + Пар обработано: 1 000
385 + Время run.py: 45,9 секунды
386 + ```
387 +
388 + Результат укладывается в 60-секундный лимит выполнения решения.
389 +
390 + ### Полная проверка
391 +
392 + ```text
393 + Товаров выбрано: 711 304
394 + Пар обработано: 365 654
395 + Время run.py: 469,3 секунды
396 + Лимит: 13 минут / 780 секунд
397 + Запас: около 311 секунд
398 + ```
399 +
400 + Из 469,3 секунды расчёт 68 V2-признаков занял 281,2 секунды.
401 +
402 + Проверено:
403 +
404 + - ровно 365 654 результата;
405 + - исходный порядок пар сохранён;
406 + - колонки строго `id1,id2,predict`;
407 + - нет `NaN` и бесконечностей;
408 + - значения находятся в диапазоне `[0,1]`.
409 +
410 + Диапазон прогнозов:
411 +
412 + ```text
413 + Минимум: 0,00553552
414 + Максимум: 0,99133800
415 + Среднее: 0,37741749
416 + ```
417 +
418 + Контрольная метрика на данных, на которых была обучена финальная модель:
419 +
420 + ```text
421 + Macro PR-AUC: 0,785309497
422 + ```
423 +
424 + Это только проверка сериализации и работоспособности. Для оценки ожидаемого качества используется leakage-safe CV около `0,7379`, а не training-метрика `0,7853`.
425 +
426 + ---
427 +
428 + ## 13. Изменения в репозитории
429 +
430 + Основные коммиты:
431 +
432 + ```text
433 + 8150c28 Add H100 ensemble and QLoRA experiments
434 + a086ab3 Record H100 teacher and hard-soup results
435 + 1c7447e Promote validated H100 ensemble submission
436 + ```
437 +
438 + Добавлены или обновлены:
439 +
440 + - генератор V2-признаков;
441 + - тесты признаков;
442 + - compact model trainer;
443 + - hard-example builder;
444 + - model-soup builder;
445 + - Qwen QLoRA scripts;
446 + - AutoGluon experiments;
447 + - ансамблевый runtime;
448 + - упаковщик с жёсткой проверкой 5 ГБ;
449 + - конфигурация категорийных весов;
450 + - проверка выходного CSV;
451 + - метрики и документация;
452 + - финальный `submission.zip`.
453 +
454 + Push успешно выполнен:
455 +
456 + ```text
457 + d67c0b8..1c7447e main -> main
458 + ```
459 +
460 + Текущий локальный commit и `origin/main` совпадают.
461 +
462 + ---
463 +
464 + ## Итог
465 +
466 + Получен полностью переносимый GPU-ансамбль:
467 +
468 + ```text
469 + E5-small hard model soup + 68 V2-признаков + категорийные HGB-модели
470 + ```
471 +
472 + Ожидаемая leakage-safe CV-метрика:
473 +
474 + ```text
475 + около 0,7379 Macro PR-AUC
476 + ```
477 +
478 + Это примерно на `0,0099` выше исходного E5-решения. Архив успешно протестирован в официальном Docker, укладывается в ограничения по времени и занимает только 0,375 ГБ из разрешённых 5 ГБ.
Newer Older