# -*- 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