Spaces:
Paused
Paused
| # -*- coding: utf-8 -*- | |
| import os as _o | |
| # chdir กลับ project root — แต่ "เฉพาะตอนที่อยู่ในโครงโปรเจกต์จริง" | |
| # เหตุผล: ตอน deploy ไฟล์นี้จะถูกก็อปไปวางแบนๆ ที่ /home/user/app/ ซึ่งขึ้น 3 ชั้นแล้วได้ "/" | |
| # ถ้า chdir ไปดื้อๆ จะหาไฟล์โมเดลไม่เจอ -> เช็คก่อนว่ามีโฟลเดอร์ results/ ไหม | |
| _root = _o.path.dirname(_o.path.dirname(_o.path.dirname(_o.path.abspath(__file__)))) | |
| if _o.path.isdir(_o.path.join(_root, 'results')): | |
| _o.chdir(_root) | |
| # ============================================================ | |
| # predict_bmi.py — เอา 22 ตัวเลขที่สกัดจากรูป มาแปลงเป็น BMI | |
| # | |
| # ===== ต้องการอะไร ===== | |
| # โมเดลที่เทรนบน Kaggle ถูกเซฟลง bmi_bundle.pkl แล้วโหลดกลับมาที่เครื่อง | |
| # คำถามคือ "ของที่โหลดมา ยังเป็นตัวเดิมอยู่ไหม หรือเพี้ยนระหว่างทาง?" | |
| # ไฟล์นี้ทั้งใช้ทำนายจริง และพิสูจน์ว่าโมเดลยังให้ผลเท่าเดิม (MAE 3.77) | |
| # | |
| # ===== ไฟล์นี้ทำอะไร ===== | |
| # 1) โหลด bmi_bundle.pkl (SVR + scaler ที่เทรนไว้แล้ว) | |
| # 2) รับ 7 ratio + 15 deep -> คืนค่า BMI | |
| # *** ใช้แค่ sklearn ไม่ต้อง torch/detectron2 -> รันบน Windows ได้ *** | |
| # | |
| # ===== วิธีคิด (ลำดับสำคัญมาก ห้ามสลับ) ===== | |
| # ตอนเทรนบน Kaggle ทำแบบนี้: | |
| # scaler.transform(7 ratio) แล้วค่อย hstack กับ 15 deep "ดิบ" -> 22 คอลัมน์ | |
| # | |
| # ทำไมสเกลแค่ 7 ratio: เพราะ Area เป็นเลขหลักหมื่น แต่ WSR เป็น 0.8 | |
| # ถ้าไม่ปรับสเกล SVR จะสนใจแต่ Area ตัวเดียว | |
| # ส่วน 15 deep ออกจาก CNN มาสเกลใกล้เคียงกันอยู่แล้ว -> ไม่ต้องแตะ | |
| # | |
| # *** ตอนใช้งานต้องทำ "เหมือนตอนเทรนเป๊ะ" *** | |
| # ถ้าสลับลำดับ หรือเผลอสเกล 15 deep ด้วย -> SVR เจอตัวเลขที่ไม่เคยเห็น -> BMI มั่ว | |
| # ============================================================ | |
| import numpy as np | |
| import joblib # ต้องใช้ joblib ไม่ใช่ pickle ธรรมดา (pickle.load อ่านไฟล์นี้ไม่ออก) | |
| # ตั้งผ่าน environment variable ได้ เพื่อให้ Docker ชี้ไปที่อื่นได้โดยไม่ต้องแก้โค้ด | |
| BUNDLE_PATH = _o.environ.get('BUNDLE_PATH', 'results/dense/bmi_bundle.pkl') | |
| # ============================================================ | |
| # โหลด bundle (ทำครั้งเดียวตอน import — จะได้ไม่โหลดซ้ำทุกครั้งที่ทำนาย) | |
| # ============================================================ | |
| def load_bundle(path=BUNDLE_PATH): | |
| """ | |
| คืน dict ที่มี 5 key: | |
| model : SVR ที่ fit แล้ว (ตัวทำนาย BMI) | |
| scaler : StandardScaler ที่ fit กับ 7 ratio ของชุด train | |
| ratio_cols : ชื่อคอลัมน์ 7 ratio (เรียงลำดับสำคัญ!) | |
| deep_cols : ชื่อคอลัมน์ 15 deep (d0..d14) | |
| name : ชื่อโมเดล ('SVR') | |
| """ | |
| b = joblib.load(path) | |
| # เช็คว่าไฟล์ครบจริง ไม่ใช่ไฟล์เสีย/ไฟล์ผิดตัว | |
| for k in ('model', 'scaler', 'ratio_cols', 'deep_cols'): | |
| if k not in b: | |
| raise KeyError(f'bundle ไม่มี key "{k}" — ไฟล์อาจผิดตัวหรือเสีย') | |
| return b | |
| _BUNDLE = None # ตัวแปรเก็บ bundle ไว้ใช้ซ้ำ (lazy load = โหลดตอนเรียกใช้ครั้งแรก) | |
| def _get_bundle(): | |
| """โหลด bundle ครั้งแรกครั้งเดียว ครั้งต่อไปใช้ของเดิม (ประหยัดเวลาตอนรันบนเว็บ)""" | |
| global _BUNDLE | |
| if _BUNDLE is None: | |
| _BUNDLE = load_bundle() | |
| return _BUNDLE | |
| # ============================================================ | |
| # หัวใจ: 22 ตัวเลข -> BMI | |
| # ============================================================ | |
| def predict_bmi(ratio7, deep15, bundle=None): | |
| """ | |
| รับ : ratio7 = 7 ค่า เรียงตาม ['WSR','WTR','WHpR','WHdR','HpHdR','Area','H2W'] | |
| deep15 = 15 ค่า จาก DenseNet (d0..d14) | |
| คืน : BMI (float) | |
| รับได้ทั้งคนเดียว (list ยาว 7) และหลายคนพร้อมกัน (array shape (n,7)) | |
| """ | |
| b = bundle if bundle is not None else _get_bundle() | |
| # แปลงเป็น array 2 มิติเสมอ (n คน, จำนวนฟีเจอร์) — เผื่อส่งมาเป็น list แบนๆ ของคนเดียว | |
| R = np.atleast_2d(np.asarray(ratio7, dtype=float)) | |
| D = np.atleast_2d(np.asarray(deep15, dtype=float)) | |
| # เช็คจำนวนฟีเจอร์ก่อน จะได้ error ที่อ่านรู้เรื่อง แทนที่จะไปพังลึกๆ ใน sklearn | |
| if R.shape[1] != len(b['ratio_cols']): | |
| raise ValueError(f"ratio ต้องมี {len(b['ratio_cols'])} ค่า ได้มา {R.shape[1]}") | |
| if D.shape[1] != len(b['deep_cols']): | |
| raise ValueError(f"deep ต้องมี {len(b['deep_cols'])} ค่า ได้มา {D.shape[1]}") | |
| # ===== หัวใจ: สเกล "แค่ 7 ratio" แล้วต่อกับ 15 deep ดิบ (ลำดับเดียวกับตอนเทรน) ===== | |
| X = np.hstack([b['scaler'].transform(R), D]) # -> (n, 22) | |
| pred = b['model'].predict(X) # SVR ทำนาย BMI | |
| return float(pred[0]) if pred.shape[0] == 1 else pred # คนเดียวคืน float / หลายคนคืน array | |
| # ============================================================ | |
| # เทสตัวเอง — พิสูจน์ว่าโมเดลที่โหลดมา "ยังเป็นตัวเดิม" | |
| # | |
| # วิธี verify: เอา feat_Image_test.csv (ชุดเทสที่ Kaggle ใช้วัดผล) | |
| # มาทำนายใหม่ในเครื่อง แล้วดูว่าได้ MAE/R2 เท่ากับที่รายงานไว้ไหม | |
| # เป้า: MAE 3.77 / R2 0.60 (plan03 ตารางผล) | |
| # ถ้าตรง = โหลดถูกตัว ลำดับ feature ถูก scaler ถูก -> เอาไปใช้ต่อได้ | |
| # ถ้าไม่ตรง = มีอะไรผิดตั้งแต่ต้น ห้ามไปต่อ | |
| # ============================================================ | |
| if __name__ == '__main__': | |
| import pandas as pd | |
| from sklearn.metrics import mean_absolute_error, r2_score | |
| print('=' * 62) | |
| print(' เทส predict_bmi.py — โมเดลที่โหลดมายังให้ผลเท่าเดิมไหม?') | |
| print('=' * 62) | |
| b = load_bundle() | |
| print(f" โมเดล : {b['name']} ({type(b['model']).__name__})") | |
| print(f" scaler : {type(b['scaler']).__name__} รับ {b['scaler'].n_features_in_} ฟีเจอร์ (= 7 ratio)") | |
| print(f" ratio_cols : {b['ratio_cols']}") | |
| print(f" deep_cols : {b['deep_cols'][0]} ... {b['deep_cols'][-1]} (รวม {len(b['deep_cols'])} ตัว)\n") | |
| # ---------- โหลดชุดเทส (ชุดเดียวกับที่ Kaggle ใช้วัด) ---------- | |
| te = pd.read_csv('results/dense/feat_Image_test.csv') | |
| te = te.replace([np.inf, -np.inf], np.nan).dropna() # ทิ้งแถวเสีย (เหมือนตอนเทรน) | |
| print(f' ชุดเทส: {len(te)} รูป') | |
| # ---------- ทำนายทั้งชุด ---------- | |
| pred = predict_bmi(te[b['ratio_cols']].values, te[b['deep_cols']].values, bundle=b) | |
| mae = mean_absolute_error(te.BMI.values, pred) | |
| r2 = r2_score(te.BMI.values, pred) | |
| print(f'\n ผลที่ได้ตอนนี้ : MAE {mae:.2f} R² {r2:+.3f}') | |
| print(f' ที่ Kaggle รายงาน: MAE 3.77 R² +0.600') | |
| # ---------- ตัดสิน ---------- | |
| # ยอมให้คลาดได้ 0.05 เพราะ sklearn คนละเวอร์ชันอาจปัดเศษต่างกันนิดหน่อย | |
| if abs(mae - 3.77) < 0.05: | |
| print('\n ✅ ตรงกัน — โมเดลที่โหลดมาเป็นตัวเดิม ใช้ต่อได้เลย') | |
| else: | |
| print(f'\n ❌ ไม่ตรง (ต่าง {abs(mae - 3.77):.2f}) — อย่าเพิ่งไปต่อ ให้ไล่เช็ค:') | |
| print(' - ลำดับคอลัมน์ ratio ถูกไหม') | |
| print(' - เผลอสเกล 15 deep ด้วยหรือเปล่า (ต้องสเกลแค่ 7 ratio)') | |
| print(' - เวอร์ชัน scikit-learn ต่างจาก 1.6.1 มากไปไหม') | |
| # ---------- ลองทำนายคนเดียว (แบบที่หน้าเว็บจะเรียกใช้จริง) ---------- | |
| row = te.iloc[0] | |
| one = predict_bmi(row[b['ratio_cols']].values, row[b['deep_cols']].values, bundle=b) | |
| print(f'\n ลองทำนายทีละคน (แบบที่เว็บจะใช้):') | |
| print(f' รูป {row.fname} -> ทำนาย BMI {one:.2f} ของจริง {row.BMI:.2f} คลาด {abs(one - row.BMI):.2f}') | |
| print('=' * 62) | |
| # วิธีอ่านผล: | |
| # - MAE 3.77 = ทำนาย BMI พลาดเฉลี่ย 3.77 หน่วย (เช่น จริง 25 ทายได้ 21-29) | |
| # - R² 0.60 = อธิบายความแปรปรวนของ BMI ได้ 60% | |
| # - "คลาด" ของคนเดียวจะแกว่งกว่าค่าเฉลี่ย เป็นเรื่องปกติ ดูภาพรวมที่ MAE | |