# 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), 'мин')