บทเรียน 5.4 — ข้างในการฝึก: Keras, Conv1D, gradient descent, int8 และ confusion matrix

Training II · ฝึกโมเดลใน TensorFlow (Docker) แล้วทดสอบบน PC

โมดูล 5 — ฝึกโมเดลและนำไปใช้หลายเป้าหมาย

ต่อจากบทเรียน 5.3 — ฝึกโมเดลใน Docker: หนึ่งชิ้นงาน สี่เป้าหมาย

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

train / val / test — สามชุด สามหน้าที่

dataset_tools.split() ของ บทเรียน 5.1–5.2 แบ่งข้อมูลเป็นสามกอง ไม่ใช่เพราะชอบ แต่เพราะแต่ละกองมีหน้าที่คนละอย่าง — เข้าใจตรงนี้ก่อน ค่อยฝึก

train (~70%) โมเดลเรียนรู้จากกองนี้ ปรับน้ำหนักตามกองนี้เท่านั้น val (~15%) เช็กทุก epoch ระหว่างฝึก จับ overfit / เลือกจุดหยุด test (~15%) แตะครั้งเดียวตอนจบ ค่าความแม่นที่เชื่อได้จริง
  • train — กองที่โมเดลเห็นและเรียนรู้ ปรับน้ำหนักตามกองนี้ (ห้ามเอา val/test มาปน)
  • val — ระหว่างฝึก เอามาเช็กทุก epoch ว่าโมเดล generalize ได้ไหม ใช้ตัดสินว่าฝึกพอหรือยัง
  • test — เก็บไว้ให้บริสุทธิ์ที่สุด แตะครั้งเดียวตอนจบเพื่อรายงานความแม่น "จริง" ที่ไม่หลอกตัวเอง
  • split() แบ่งแบบ stratified — แต่ละคลาส (idle/circle/shaking) กระจายครบทั้งสามกอง ไม่ใช่กองใดกองหนึ่งขาดคลาส

กฎเหล็ก: test set ต้องไม่เคยมีอิทธิพลต่อการตัดสินใจใดๆ ระหว่างฝึก ถ้าเราแอบดู test แล้วปรับโมเดล ตัวเลขสุดท้ายจะโกหกเรา — นี่คือวินัยพื้นฐานของ ML ที่ทำจริงทุกที่

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

ภาพเคลื่อนไหว — สัญญาณไหลผ่านโครงข่าย (forward pass)

ภาพเคลื่อนไหว: สัญญาณไหลจาก input ผ่าน hidden layers ไปยัง output สามคลาส idle circle shake

input → hidden → output: จุดแดงคือ activation ที่ไหลผ่านน้ำหนักจนได้คลาสที่ชนะ

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

แกะข้างใน — สี่จังหวะของการฝึก

ทั้งไฟล์ train.py (และ s12_train.py ของเรา) อ่านเป็นสี่จังหวะ ไล่เรียงกัน — จำโครงนี้ไว้ เดี๋ยวไล่ทีละอัน

1 · build สร้างโครงโมเดล Conv1D 2 · fit ฝึกด้วยชุด train+val 3 · convert int8 บีบ + calibrate 4 · eval PC วัด accuracy + confusion สี่จังหวะนี้คือทั้งเรื่องราวของชุดบทเรียน — และคือ 4 ช่องที่คุณจะเติมใน s12_train.py

เทียบกับบทเรียน 1.1–1.3: บทเรียน 1.1–1.3 มี 4 คำสั่ง (models/select/result/stop) วันนี้ก็ 4 จังหวะ (build/fit/convert/eval) — ต่างที่ตอนนี้เราไม่ได้ "เรียกโมเดล" แต่ "สร้างโมเดล"

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

รู้จัก Keras — เขียนโมเดลเป็นชั้นๆ

ก่อนดูโค้ดสร้างโมเดล มารู้จักเครื่องมือก่อน เราใช้ TensorFlow/Keras — วิธีเขียน neural network ที่อ่านเหมือน "ต่อบล็อกเลโก้" ทีละชั้น

Input (50, 6) Conv1D + Pool จับรูปแบบตามเวลา Pooling ยุบแกนเวลา Dense ตัดสินคลาส 3 คลาส
  • tf.keras.Sequential([...]) — วางชั้น (layer) เรียงจากบนลงล่าง ข้อมูลไหลผ่านทีละชั้น
  • แต่ละชั้นแปลงข้อมูล: Conv1D หา pattern, Pooling ย่อขนาด, Dense ตัดสินใจ — ต่อกันเป็นโมเดล
  • เราเลือกเฉพาะชั้นที่ Ethos-U55 เร่งได้ เพราะปลายทางคือลงชิป ไม่ใช่รันบน GPU ตัวใหญ่
  • ตอนฝึก Keras จะปรับ "น้ำหนัก" ในแต่ละชั้นให้ทายถูกขึ้นเรื่อยๆ อัตโนมัติ — เราแค่วางโครงกับป้อนข้อมูล

ไม่ต้องจำ API ของ Keras ทั้งหมดในชุดบทเรียนนี้ ขอแค่เห็นภาพว่า "โมเดล = ชั้นที่ต่อกัน" และข้อมูลไหลจาก input ผ่านชั้นต่างๆ ออกมาเป็นคลาส เดี๋ยวเราดูโค้ดจริงในสไลด์ถัดไป

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

จังหวะ 1 — สร้างโมเดล (Conv1D เล็กๆ)

โมเดลของเราคือ 1-D CNN เล็กๆ ตั้งใจให้เล็กพอลงชิป Conv1D กวาดไปตามแกนเวลา จับ "รูปร่างของการเคลื่อนไหว" ในหน้าต่าง 1 วินาที

def build_model(win, chans, n_classes):
    return tf.keras.Sequential([
        tf.keras.layers.Input(shape=(win, chans)),        # (50, 6): 1 วิ, 6 แกน
        tf.keras.layers.Conv1D(16, 5, padding="same", activation="relu"),
        tf.keras.layers.MaxPooling1D(2),
        tf.keras.layers.Conv1D(32, 3, padding="same", activation="relu"),
        tf.keras.layers.GlobalAveragePooling1D(),          # ยุบแกนเวลา
        tf.keras.layers.Dense(32, activation="relu"),
        tf.keras.layers.Dense(n_classes, activation="softmax"),  # 3 คลาส
    ])
  • input (win=50, chans=6) — หน้าต่าง 50 ตัวอย่าง (1 วิ ที่ 50 Hz) × 6 แกน IMU (ax..gz)
  • Conv1D → MaxPool → Conv1D — เรียนรู้รูปแบบตามเวลา ตัวเลข 16/32 คือจำนวน filter
  • GlobalAveragePooling1D — ยุบมิติเวลาให้เหลือเวกเตอร์เดียว แล้ว dense head ตัดสินคลาส
  • ทุก layer ที่เลือกเป็น op ที่ Ethos-U55 เร่งได้ นี่คือเหตุผลที่โมเดลต้องเล็กและเรียบ

ในฉบับเต็ม (examples/) เราเรียก model.summary() + model.count_params() เพื่อดูว่าโมเดลเล็กแค่ไหน — โมเดลลงบอร์ดได้ต้องมีพารามิเตอร์ระดับหลักพัน ไม่ใช่หลักล้าน

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

ภาพเคลื่อนไหว — Conv1D เลื่อน kernel ไปตามสัญญาณ

ภาพเคลื่อนไหว: kernel ของ Conv1D ไถลไปตามสัญญาณทีละตำแหน่ง แล้วคูณรวมเป็นผลลัพธ์ y[t]

kernel เดียวไถลทั้งเส้น = เรียน "รูปทรง" ที่เลื่อนไปมาได้ (translation-invariant)

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

คณิตหลังฉาก (1) — Conv1D ทำอะไรกับสัญญาณ

Conv1D ที่เราเพิ่งวางในโมเดลไม่ใช่เวทมนตร์ มันคือสมการเดียวสั้นๆ ที่เลื่อน "หน้าต่างเล็ก" (filter) ไปตามแกนเวลา แล้วคูณ-บวกทีละตำแหน่ง:

y[t]=∑k=0K−1w[k] x[t+k]+by[t] = \sum_{k=0}^{K-1} w[k]\,x[t+k] + b

อ่านทีละตัว (ภาษาคน):

  • xx = สัญญาณเข้า — หน้าต่าง IMU ของเรา (50 ตัวอย่างต่อหนึ่งแกน)
  • w[k]w[k] = ค่าน้ำหนักใน filter ยาว KK ค่า (โมเดลเราใช้ K=5K=5 แล้ว K=3K=3) — นี่คือ "รูปร่าง" ที่โมเดลเรียนรู้
  • bb = bias เลื่อนระดับผลลัพธ์
  • y[t]y[t] = ค่าออกที่ตำแหน่งเวลา tt — สูงเมื่อสัญญาณช่วงนั้น "หน้าตาเหมือน" filter
filter w (K=3) เลื่อนไปตามแกนเวลา แล้ว dot-product ทีละตำแหน่ง x[t] ... (หน้าต่าง IMU) w · x y[t] = w[0]·x[t] + w[1]·x[t+1] + w[2]·x[t+2] + b

ทำไมสำคัญตรงนี้: filter หนึ่งอันเรียน "ลายเซ็น" ของการเคลื่อนไหวหนึ่งแบบ — เช่น จังหวะสั่นถี่ๆ ของ shaking การมี 16/32 filter ก็เหมือนมีนักสังเกต 16/32 คนช่วยกันจับลายต่างๆ พร้อมกัน นี่คือเหตุผลที่ Conv1D เหมาะกับสัญญาณตามเวลาแบบ IMU

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

ภาพเคลื่อนไหว — Gradient Descent: โมเดลไถลลงหุบ Loss

ภาพเคลื่อนไหว: จุดน้ำหนัก θ ไถลลงตามความชันของกราฟ Loss จนถึงจุดต่ำสุด

▸ ลองเล่นสด (GeoGebra): เปิด Interactive Math Lab — ลากจุด/เลื่อนสไลเดอร์ดูสมการขยับตาม

แต่ละ epoch = ลูกบอลไถลลงหุบ loss เข้าใกล้จุดต่ำสุด (โมเดลทายแม่นขึ้นเรื่อย ๆ)

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

จังหวะ 2 — ฝึก (fit) และอ่านผล

เมื่อมีโครงโมเดลแล้ว เรา compile (บอกว่าใช้ optimizer/loss อะไร) แล้ว fit (ฝึกจริง):

model.compile(optimizer="adam",
              loss="sparse_categorical_crossentropy", metrics=["accuracy"])
model.fit(Xtr, ytr, validation_data=(Xva, yva),
          epochs=a.epochs, batch_size=32, verbose=2)
  • epoch = รอบที่โมเดลได้เห็นข้อมูลฝึกครบทั้งชุดหนึ่งครั้ง (เราตั้ง 25 รอบ)
  • batch_size=32 = อัปเดตน้ำหนักทุกๆ 32 หน้าต่าง ไม่ใช่ทีละอัน (เร็วและนิ่งกว่า)
  • validation_data = ทุก epoch เอา Xva (ชุดที่ไม่ได้ฝึก) มาทดสอบ ดูว่าโมเดลเก่งจริงหรือแค่จำ

อ่านตัวเลขระหว่างฝึก:

เห็นแบบนี้ แปลว่า
accuracy และ val_accuracy ขึ้นด้วยกัน ดี — โมเดลเข้าใจ generalize ได้
accuracy สูง แต่ val_accuracy ต่ำ/ตก overfit — จำข้อสอบ ไม่เข้าใจ
ทั้งคู่ต่ำค้าง underfit — โมเดลเล็กไป/ข้อมูลน้อยไป/ฝึกสั้นไป

fit คือช่องเติมที่ 1 ของคุณ — มันคือ "จังหวะที่โมเดลเรียนรู้จริง" ก่อนหน้านี้เราแค่วางโครง ยังไม่มีอะไรเกิดขึ้น

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

ภาพเคลื่อนไหว — Softmax เปลี่ยนคะแนนเป็นความน่าจะเป็น

ภาพเคลื่อนไหว: คะแนนดิบสามคลาสผ่าน softmax กลายเป็นความน่าจะเป็นที่รวมกันได้ 1

▸ ลองเล่นสด (GeoGebra): เปิด Interactive Math Lab — ลากจุด/เลื่อนสไลเดอร์ดูสมการขยับตาม

คะแนนดิบกลายเป็นความน่าจะเป็นที่รวมกันได้ 1 — ตัวสูงสุดคือคำตอบของโมเดล

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

คณิตหลังฉาก (2) — โมเดลเรียนรู้ยังไง (3 สมการ)

ตอน model.fit(...) ทำงาน มีสามสมการหมุนวนอยู่ข้างใน ไม่ต้องท่อง แค่เห็นภาพว่าแต่ละตัวทำอะไร:

1. softmax — ชั้นสุดท้าย (Dense(..., activation="softmax")) แปลงคะแนนดิบ ziz_i เป็นความน่าจะเป็นของ 3 คลาส ที่รวมกันได้ 1:

y^i=ezi∑j=1Cezj\hat{y}_i = \frac{e^{z_i}}{\sum_{j=1}^{C} e^{z_j}}

2. cross-entropy loss — คือ sparse_categorical_crossentropy ที่เราใส่ตอน compile วัดว่าคำทาย y^\hat{y} ห่างจากเฉลย yy แค่ไหน (ยิ่งทายถูกมั่นใจ ยิ่งเข้าใกล้ 0):

L=−∑i=1Cyilog⁡(y^i)L = -\sum_{i=1}^{C} y_i \log(\hat{y}_i)

3. gradient descent — คือหัวใจของ optimizer adam ค่อยๆ ขยับน้ำหนัก θ\theta ลง "เนินของ loss" ทีละก้าว:

θ←θ−η ∇θL\theta \leftarrow \theta - \eta\,\nabla_\theta L

อ่านสัญลักษณ์แบบภาษาคน:

  • ziz_i = คะแนนดิบของคลาส ii, C=3C=3 (idle/circle/shaking), y^i\hat{y}_i = ความน่าจะเป็นที่โมเดลให้คลาส ii
  • yiy_i = เฉลยจริง (คลาสถูก = 1 ที่เหลือ = 0) → LL ยิ่งต่ำ ยิ่งทายเก่ง
  • θ\theta = น้ำหนักทั้งหมดในโมเดล, ∇θL\nabla_\theta L = ทิศที่ loss ชันขึ้น, η\eta = learning rate (ก้าวสั้น/ยาว)
logits z คะแนนดิบ 3 คลาส softmax → ŷ ความน่าจะเป็น loss L ทายห่างเฉลยแค่ไหน ปรับ θ gradient descent วนซ้ำทุก batch/epoch จน loss ต่ำ

ทำไมสำคัญตรงนี้: วงจร softmax → loss → gradient descent วนซ้ำทุก batch นี่แหละคือสิ่งที่ซ่อนอยู่หลัง model.fit(...) บรรทัดเดียวที่คุณเติมในช่อง 1 พอเห็น accuracy ไต่ขึ้น = LL กำลังลดลง = θ\theta กำลังขยับถูกทาง

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

ทำไมต้อง int8 — และ quantization คืออะไร

โมเดลตอนฝึกใช้ตัวเลข float32 (ทศนิยม 32 บิต) แต่ NPU บนบอร์ดเร่งได้เฉพาะ int8 (จำนวนเต็ม 8 บิต) เราจึงต้อง "บีบ" ก่อนลงชิป

float32 (ตอนฝึก) แม่นเต็ม แต่ใหญ่ + กินไฟ int8 (ลงชิป) เล็กลง ~4 เท่า · NPU เร่งได้ Web/Cortex-A ก็รับได้ quantization แมพช่วง float -> เกล็ด int8
  • int8 มีค่าได้แค่ -128..127 เราจึงต้องหา "สเกล" ที่แมพช่วงค่าจริงของโมเดลลงไปในกรอบแคบๆ นี้
  • ได้ของแลกมา: ขนาดเล็กลง ~4 เท่า และ NPU เร่งได้ (float NPU เร่งไม่ได้) แต่เสี่ยงเสียความแม่นนิดหน่อย
  • int8 คือตัวหารร่วม ของทุกเป้าหมาย: MCU บังคับต้องใช้ ส่วน Web/Cortex-A รับได้หมด บีบครั้งเดียวใช้ได้ทั่ว

เราใช้ post-training quantization — ฝึกด้วย float ก่อน แล้วค่อยบีบทีหลัง ง่ายและพอสำหรับโมเดลเล็ก (มีวิธี quantization-aware training ที่แม่นกว่า แต่ซับซ้อนกว่า เก็บไว้เป็นการบ้านของนักวิจัย)

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

representative dataset — หัวใจของ int8

คำถามคือ converter จะรู้ได้ยังไงว่าควรเลือก "สเกล int8" เท่าไร? คำตอบ: เรา ป้อนตัวอย่างจริง ให้มันดู แล้วมันวัดช่วงค่าเอง — เรียกจังหวะนี้ว่า calibration

def representative():
    for i in range(min(200, len(X_repr))):
        yield [X_repr[i:i + 1].astype(np.float32)]   # ป้อนหน้าต่างจริง 200 อัน

conv.representative_dataset = representative           # ผูกเข้ากับ converter
  • converter รันตัวอย่าง 200 อันนี้ผ่านโมเดล ดูว่า activation แต่ละชั้นมีค่าจริงอยู่ในช่วงไหน
  • แล้วเลือกสเกล int8 ที่ครอบช่วงนั้นพอดี — ไม่ตัดหัวตัดหาง ไม่เผื่อกว้างจนเสียความละเอียด
  • ต้องเป็น ตัวอย่างจากชุดฝึกจริง ไม่ใช่ข้อมูลมั่ว ไม่งั้นสเกลจะเพี้ยน ความแม่นตก

ถ้าลืมผูก representative_dataset ทั้งที่ตั้ง TFLITE_BUILTINS_INT8 ไว้ conv.convert() จะหยุดทันทีด้วย ValueError: For full integer quantization, a representative_dataset must be specified. — นี่คือ ช่องเติมที่ 2 ของคุณ ส่วนความผิดพลาดที่เงียบกว่าและพบบ่อยในงานจริงคือป้อนตัวอย่างที่ไม่เหมือนข้อมูลจริง สเกลจะเพี้ยนแล้ว int8 แม่นตก

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

จังหวะ 3 — บีบเป็น int8 แบบ full-integer

รวมทุกอย่างเข้าด้วยกัน: บอก converter ให้บีบ ทั้ง input และ output เป็น int8 (ไม่ใช่แค่น้ำหนักข้างใน) นี่คือคอนฟิกที่ NPU บังคับ

conv = tf.lite.TFLiteConverter.from_keras_model(model)
conv.optimizations = [tf.lite.Optimize.DEFAULT]
conv.representative_dataset = representative                     # ช่องเติม 2
conv.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
conv.inference_input_type = tf.int8                             # ช่องเติม 3
conv.inference_output_type = tf.int8
tflite = conv.convert()
  • inference_input_type = tf.int8 + inference_output_type = tf.int8 — บังคับขาเข้า/ขาออกเป็น int8 ล้วน
  • นี่แหละที่แยก "โมเดลรันบนชิปได้จริง" ออกจาก "โมเดลที่รันได้แค่บน PC" — NPU อ่าน float ไม่ได้เลย
  • ผลลัพธ์คือ bytes ของไฟล์ .tflite เราเขียนลงดิสก์ตรงๆ ~11 KB

inference_input_type = tf.int8 คือ ช่องเติมที่ 3 — สั้นแค่สองบรรทัด แต่คือเส้นแบ่งระหว่าง "โมเดลลงบอร์ดได้" กับ "ไม่ได้"

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

อย่าลืม normalization — จุดพังเงียบ

ก่อนฝึก เรา normalize ข้อมูล (ลบ mean หารด้วย std) เพื่อให้ทุกแกนอยู่สเกลเดียวกัน แต่มีกับดัก: ฝั่งที่เอาโมเดลไปใช้ต้อง normalize ด้วยค่าเดียวกันเป๊ะ

(Xtr, Xva, Xte), (mean, std) = dt.normalize(Xtr, Xva, Xte)   # fit บนชุดฝึกเท่านั้น
...
np.savez(a.out + ".norm.npz", mean=mean, std=std)            # เซฟไว้คู่กับโมเดล
  • dt.normalize หา mean/std จาก ชุดฝึกเท่านั้น ห้ามให้สถิติของ val/test รั่วเข้ามา (ไม่งั้นตัวเลขสวยเกินจริง)
  • เราเซฟ mean/std ลงไฟล์ .norm.npz คู่กับโมเดล — บทเรียน 5.6–5.7 (เบราว์เซอร์) และ บทเรียน 5.8–5.9 (บอร์ด) ต้องโหลดไปใช้
  • ถ้า front-end บนบอร์ดใช้ mean/std คนละชุดกับตอนฝึก โมเดลจะทายมั่วทั้งๆ ที่ไฟล์ถูก

นี่คือ จุดพังเงียบ (silent failure) ที่หายากมาก — โมเดลไม่ error แต่ทายผิดหมด เพราะ "เห็นข้อมูลคนละสเกล" กับตอนฝึก จำไว้: normalization เป็นส่วนหนึ่งของโมเดล ไม่ใช่ของแถม

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

จังหวะ 4 — ทดสอบไฟล์ int8 บน PC

มีไฟล์ .tflite แล้ว เรารันมันบน PC ผ่าน ai-edge-litert (ตัวรัน .tflite รุ่นใหม่) นี่คือ ground truth ก่อนเอาลงบอร์ด:

from ai_edge_litert.interpreter import Interpreter
it = Interpreter(model_path=out_path); it.allocate_tensors()
inp, out = it.get_input_details()[0], it.get_output_details()[0]
in_scale, in_zero = inp["quantization"]         # สเกล/zero-point ของ input
for i in range(len(Xte)):
    q = np.clip(np.round(Xte[i:i+1] / in_scale + in_zero), -128, 127).astype(np.int8)
    it.set_tensor(inp["index"], q); it.invoke()
    o = it.get_tensor(out["index"])[0].astype(np.float32)
    o = (o - out_zero) * out_scale              # dequantize กลับเป็น float
    preds.append(int(o.argmax()))
acc = (preds == yte).mean()                      # ช่องเติม 4
  • โมเดลรับ int8 เราจึงต้อง quantize input (float → int8) ด้วยสเกลของมันเองก่อนป้อน
  • ขาออกได้ int8 กลับมา เราต้อง dequantize (int8 → float) ก่อนหา argmax (คลาสที่ชนะ)
  • acc = (preds == yte).mean() = สัดส่วนหน้าต่างที่ทายถูกในชุดทดสอบ — ช่องเติมที่ 4 ของคุณ

นี่คือ int8 math ที่เราเห็นครั้งแรกใน บทเรียน 1.1–1.3 (conf/scores) แต่รอบนี้เราลงมือแปลงเอง เข้าใจว่าตัวเลข int8 บนชิปเชื่อมกับความน่าจะเป็นยังไง

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

อ่าน confusion matrix ให้เป็น

accuracy บอกภาพรวม แต่ confusion matrix บอกว่าโมเดลสับสน "คู่ไหน" — สำคัญกว่าตัวเลขเดียวมากในงานจริง

confusion (rows=true, cols=pred) true \ pred idle circle shaking idle 7 0 0 circle 0 7 0 shaking 0 0 7 แนวทแยง = ทายถูก
  • แนวทแยง (idle→idle, circle→circle, shaking→shaking) = ทายถูก ยิ่งเลขสูงยิ่งดี
  • นอกแนวทแยง = ทายผิด เช่น ถ้าช่อง circle→shaking มีเลข แปลว่าโมเดลสับสนสองท่านี้ (คล้ายกัน)
  • ตารางนี้ perfect เพราะข้อมูลสังเคราะห์แยกคลาสง่าย — ข้อมูลจริงจากบอร์ดจะมีเลขนอกแนวทแยงบ้าง นั่นปกติ

คิดถึงบทเรียน false positive / false negative จาก โมดูล 4 (Analysis): confusion matrix คือที่ที่มันจับต้องได้ — "โมเดลชอบเข้าใจผิดว่า X เป็น Y" บอกเราว่าต้องเก็บข้อมูลเพิ่มตรงไหน

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0