3d-mesh-quality-control / src /04_tabular.py
kaktusik21's picture
Upload folder using huggingface_hub
728f0ef verified
Raw History Blame Contribute Delete
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), 'мин')