model_fatsusus / predict_bmi.py
Jjtumarai's picture
deploy: bodyfat estimation app
1cac303
Raw
History Blame Contribute Delete
11.5 kB
# -*- 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}{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