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 ГБ. | |