Download src/04_tabular.py from kaktusik21/3d-mesh-quality-control: direct link, hf CLI and curl.
- Browser
- Download file 9.12 kB
-
https://huggingface.co/kaktusik21/3d-mesh-quality-control/resolve/main/src/04_tabular.py
- Command line
-
hf download hf://kaktusik21/3d-mesh-quality-control/src/04_tabular.py
-
curl -L -o 04_tabular.py https://huggingface.co/kaktusik21/3d-mesh-quality-control/resolve/main/src/04_tabular.py
9.12 kB
| # KAGGLE: {"gpu": false, "internet": true, "datasets": ["daniilantonov5/3d-mesh-quality-control"], "kernels": ["alekstop/00-eda", "alekstop/01-geom", "alekstop/02-imgstats"]} | |
| # %% [markdown] | |
| # # 04 — Табличный бейзлайн: LightGBM на геометрии + статистиках карт нормалей | |
| # | |
| # Полностью CPU, GPU-квота не тратится. Это одновременно: | |
| # | |
| # * первая честная цифра итоговой метрики на OOF; | |
| # * страховочный submission; | |
| # * фильтр гипотез: видно, какие метки уже «берутся» дёшево, а какие упираются | |
| # и потому являются кандидатами на дообучение визуального бэкбона (gate-правило). | |
| # | |
| # Метрика считает две части независимо, поэтому голову `quality` оптимизируем | |
| # отдельно и **не форсируем** согласованность с дефектами. | |
| # %% | |
| import os, sys, json, time, math, warnings | |
| from pathlib import Path | |
| import numpy as np | |
| import pandas as pd | |
| warnings.filterwarnings('ignore') | |
| SEED = 42 | |
| np.random.seed(SEED) | |
| T0 = time.time() | |
| WORK = Path(os.environ.get('MQC_WORK', '/kaggle/working')) # в Colab задаётся окружением | |
| IN = Path(os.environ.get('MQC_IN', '/kaggle/input')) | |
| find = lambda pat: next(p for p in IN.rglob(pat)) | |
| csvs = {p.name: p for p in IN.rglob('*.csv')} | |
| pick = lambda pref: sorted([v for k, v in csvs.items() if k.lower().startswith(pref)], key=lambda p: len(p.name))[0] | |
| train = pd.read_csv(pick('train')) | |
| test = pd.read_csv(pick('test')) | |
| folds = pd.read_csv(find('folds.csv')) | |
| GEOM = pd.read_parquet(find('geom_features.parquet')) | |
| IMG = pd.read_parquet(find('img_features.parquet')) | |
| LABELS = [c for c in train.columns if c not in ('item_id', 'quality')] | |
| TARGETS = LABELS + ['quality'] | |
| print('geom', GEOM.shape, '| img', IMG.shape, '| folds', folds.shape) | |
| # %% | |
| # --- сборка матрицы признаков --- | |
| FEATS = [c for c in GEOM.columns if c not in ('item_id', 'err')] + \ | |
| [c for c in IMG.columns if c != 'item_id'] | |
| print('признаков всего:', len(FEATS)) | |
| full = pd.concat([train.assign(split='train'), test.assign(split='test')], ignore_index=True) | |
| full = full.merge(GEOM, on='item_id', how='left').merge(IMG, on='item_id', how='left') | |
| full = full.merge(folds, on='item_id', how='left') | |
| X = full[FEATS].replace([np.inf, -np.inf], np.nan).astype(np.float32).values | |
| istr = (full.split == 'train').values | |
| ite = ~istr | |
| FOLD = full.fold.fillna(-1).astype(int).values | |
| Y = full[TARGETS].fillna(0).astype(int).values | |
| print('train', istr.sum(), 'test', ite.sum(), 'NaN в X:', int(np.isnan(X).sum())) | |
| # %% | |
| # --- обучение: LightGBM, по одной бинарной задаче на цель --- | |
| import lightgbm as lgb | |
| NFOLD = 5 | |
| PARAMS = dict(objective='binary', metric='auc', learning_rate=0.04, | |
| num_leaves=63, min_child_samples=25, feature_fraction=0.35, | |
| bagging_fraction=0.85, bagging_freq=1, lambda_l2=5.0, | |
| n_estimators=3000, verbosity=-1, n_jobs=os.cpu_count(), seed=SEED) | |
| oof = np.zeros((istr.sum(), len(TARGETS)), np.float32) | |
| pte = np.zeros((ite.sum(), len(TARGETS)), np.float32) | |
| imp = {} | |
| Xtr_all, Ytr_all, Ftr = X[istr], Y[istr], FOLD[istr] | |
| Xte = X[ite] | |
| for t, tgt in enumerate(TARGETS): | |
| y = Ytr_all[:, t] | |
| best_iters, gains = [], np.zeros(len(FEATS)) | |
| for k in range(NFOLD): | |
| tr, va = Ftr != k, Ftr == k | |
| m = lgb.LGBMClassifier(**PARAMS) | |
| m.fit(Xtr_all[tr], y[tr], eval_set=[(Xtr_all[va], y[va])], | |
| eval_metric='auc', callbacks=[lgb.early_stopping(120, verbose=False)]) | |
| oof[va, t] = m.predict_proba(Xtr_all[va])[:, 1] | |
| pte[:, t] += m.predict_proba(Xte)[:, 1] / NFOLD | |
| best_iters.append(m.best_iteration_ or PARAMS['n_estimators']) | |
| gains += m.booster_.feature_importance('gain') | |
| from sklearn.metrics import roc_auc_score | |
| imp[tgt] = pd.Series(gains, index=FEATS).sort_values(ascending=False) | |
| print('%-13s AUC=%.4f pos=%5d iters~%4d' % | |
| (tgt, roc_auc_score(y, oof[:, t]), y.sum(), int(np.mean(best_iters))), flush=True) | |
| np.save(WORK / 'oof_tabular.npy', oof) | |
| np.save(WORK / 'pte_tabular.npy', pte) | |
| print('обучение:', round((time.time() - T0) / 60, 1), 'мин') | |
| # %% | |
| # --- пороги: покоординатная оптимизация (weighted-F1 разложим в сумму по меткам) --- | |
| from sklearn.metrics import f1_score | |
| GRID = np.arange(0.02, 0.96, 0.005) | |
| def best_thr(y, p): | |
| """порог, максимизирующий F1 метки""" | |
| s = [f1_score(y, p > t, zero_division=0) for t in GRID] | |
| i = int(np.argmax(s)) | |
| return float(GRID[i]), float(s[i]) | |
| def fold_avg_thr(y, p, f): | |
| """усреднение порога по фолдам — устойчивее, чем один прогон по всему OOF""" | |
| return float(np.mean([best_thr(y[f == k], p[f == k])[0] for k in range(NFOLD)])) | |
| THR = {} | |
| print('%-13s %6s %6s | %6s %6s' % ('label', 'thr*', 'F1*', 'thr_cv', 'F1_cv')) | |
| for t, tgt in enumerate(TARGETS): | |
| y, p = Ytr_all[:, t], oof[:, t] | |
| t1, s1 = best_thr(y, p) | |
| t2 = fold_avg_thr(y, p, Ftr) | |
| s2 = f1_score(y, p > t2, zero_division=0) | |
| THR[tgt] = t2 | |
| print('%-13s %6.3f %6.3f | %6.3f %6.3f' % (tgt, t1, s1, t2, s2)) | |
| # %% | |
| # --- итоговая метрика на OOF + разбор вклада каждой метки --- | |
| def final_metric(yq, pq, ya, pa): | |
| a = f1_score(yq, pq, zero_division=0) | |
| b = f1_score(ya, pa, average='weighted', zero_division=0) | |
| return 10 * a + 10 * b, a, b | |
| PA = np.stack([(oof[:, i] > THR[l]).astype(int) for i, l in enumerate(LABELS)], 1) | |
| YA = Ytr_all[:, :len(LABELS)] | |
| yq = Ytr_all[:, -1] | |
| # quality: прямая голова vs производная (1 - max_j p_j) vs блендинг | |
| p_direct = oof[:, -1] | |
| p_derived = 1.0 - oof[:, :len(LABELS)].max(1) | |
| best = None | |
| for w in np.arange(0, 1.01, 0.05): | |
| p = w * p_derived + (1 - w) * p_direct | |
| thr, s = best_thr(yq, p) | |
| if best is None or s > best[2]: | |
| best = (w, thr, s) | |
| W_Q, THR_Q, F1_Q = best | |
| print('quality: прямая F1=%.4f | производная F1=%.4f | блендинг w=%.2f thr=%.3f F1=%.4f' % ( | |
| best_thr(yq, p_direct)[1], best_thr(yq, p_derived)[1], W_Q, THR_Q, F1_Q)) | |
| pq = ((W_Q * p_derived + (1 - W_Q) * p_direct) > THR_Q).astype(int) | |
| tot, fq, fa = final_metric(yq, pq, YA, PA) | |
| print() | |
| print('OOF f1_final = %.3f из 20 (quality %.4f | artefacts_weighted %.4f)' % (tot, fq, fa)) | |
| sup = YA.sum(0) | |
| print() | |
| print('%-13s %7s %7s %7s %7s %8s' % ('label', 'support', 'вес', 'F1', 'вклад', 'потолок')) | |
| for i, l in enumerate(LABELS): | |
| f1 = f1_score(YA[:, i], PA[:, i], zero_division=0) | |
| w = sup[i] / sup.sum() | |
| print('%-13s %7d %7.3f %7.4f %7.3f %8.2f' % (l, sup[i], w, f1, 10 * w * f1, 10 * w)) | |
| # %% | |
| # --- gate-правило: сколько баллов ещё лежит на столе у каждой метки --- | |
| print('НЕДОБРАННЫЕ БАЛЛЫ (потолок - текущий вклад), сортировка по убыванию:') | |
| gap = [] | |
| for i, l in enumerate(LABELS): | |
| f1 = f1_score(YA[:, i], PA[:, i], zero_division=0) | |
| w = sup[i] / sup.sum() | |
| gap.append((10 * w * (1 - f1), l, f1, 10 * w)) | |
| gap.sort(reverse=True) | |
| for g, l, f1, cap in gap: | |
| print(' %-13s недобор %.3f балла (F1=%.3f, потолок %.2f)' % (l, g, f1, cap)) | |
| print(' %-13s недобор %.3f балла (F1=%.3f, потолок 10.00)' % ('quality', 10 * (1 - F1_Q), F1_Q)) | |
| # %% | |
| # --- топ признаков по каждой метке (материал для презентации и интерпретации) --- | |
| for tgt in TARGETS: | |
| top = imp[tgt].head(10) | |
| print('%-13s' % tgt, ' | '.join('%s' % k for k in top.index)) | |
| pd.DataFrame({t: imp[t] for t in TARGETS}).to_csv(WORK / 'lgb_gain.csv') | |
| # %% | |
| # --- submission --- | |
| sub = pd.DataFrame({'item_id': full.loc[ite, 'item_id'].values}) | |
| for i, l in enumerate(LABELS): | |
| sub[l] = (pte[:, i] > THR[l]).astype(int) | |
| pq_te = W_Q * (1.0 - pte[:, :len(LABELS)].max(1)) + (1 - W_Q) * pte[:, -1] | |
| sub['quality'] = (pq_te > THR_Q).astype(int) | |
| sub = sub[['item_id'] + LABELS + ['quality']] | |
| sub.to_csv(WORK / 'submission.csv', index=False) | |
| json.dump({'thr': THR, 'w_quality': W_Q, 'thr_quality': THR_Q, | |
| 'oof_final': tot, 'oof_f1_quality': fq, 'oof_f1_artefacts': fa}, | |
| open(WORK / 'tabular_config.json', 'w'), indent=2) | |
| print(sub.shape) | |
| print('доля положительных в submission:') | |
| print(sub[LABELS + ['quality']].mean().round(4)) | |
| print('в train для сравнения:') | |
| print(train[LABELS + ['quality']].mean().round(4)) | |
| print('всего:', round((time.time() - T0) / 60, 1), 'мин') | |