File size: 11,484 Bytes
1cac303
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# -*- 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