ในสายการผลิตจริง โมเดลที่ “accuracy สูง” ยังไม่พอ ต้องเลือกจุดทำงานที่สมดุลต้นทุนของการปล่อย ของเสียกับการทิ้งของดี ต้องตามให้ทันแสงที่เปลี่ยน และต้องเรียนรู้สิ่งใหม่โดยไม่ลืมของเก่า บทนี้คือชุดคำตอบ ระดับ production สำหรับสามคำถามนั้น
บริบท: QC ที่ต้นทุนไม่เท่ากัน
ของส่วนใหญ่เป็นของดี (class ไม่สมดุล) และต้นทุนของความผิดพลาดสองชนิดต่างกันมาก: การปล่อยของเสียถึงมือ ลูกค้า (false accept) มักแพงกว่าการทิ้งของดี (false reject) หลายเท่า ดังนั้น accuracy จึงหลอกตา เราต้อง ออกแบบโปรโตคอลทดสอบที่อิงต้นทุนจริง
ปรับ FAR vs FRR ด้วย cost-based ROC optimization
FAR (False Acceptance Rate) คือสัดส่วนของเสียที่ถูกปล่อยผ่าน FRR (False Rejection Rate) คือสัดส่วนของดีที่ถูกปฏิเสธ ทั้งสองเป็น trade-off ผ่าน threshold แทนที่จะเลือก จุดที่ accuracy สูงสุด เรากำหนดต้นทุนให้แต่ละชนิดความผิดพลาด แล้วเลือก threshold ที่ทำให้ต้นทุนรวมที่คาดหวังต่ำสุด
1import numpy as np2from sklearn.metrics import roc_curve345def optimal_threshold(6 y_true: np.ndarray, # 1 = defect (positive), 0 = ok7 y_score: np.ndarray, # คะแนนความน่าจะเป็น defect8 cost_false_accept: float = 50.0, # ปล่อยของเสีย (แพง)9 cost_false_reject: float = 1.0, # ทิ้งของดี10 defect_prior: float | None = None,11) -> dict:12 """เลือก threshold ที่ minimize expected cost บน ROC."""13 fpr, tpr, thresholds = roc_curve(y_true, y_score)14 # FRR = พลาด defect = 1 - tpr ; FAR ฝั่ง ok = fpr15 frr = 1.0 - tpr16 far = fpr1718 p_defect = defect_prior if defect_prior is not None else float(y_true.mean())19 p_ok = 1.0 - p_defect2021 # expected cost ต่อชิ้น = ต้นทุน_พลาดdefect*P(defect)*FRR + ต้นทุน_ปล่อยเสีย*P(ok)*FAR22 expected_cost = (23 cost_false_reject * p_defect * frr24 + cost_false_accept * p_ok * far25 )26 best = int(np.argmin(expected_cost))27 return {28 "threshold": float(thresholds[best]),29 "far": float(far[best]),30 "frr": float(frr[best]),31 "expected_cost": float(expected_cost[best]),32 }Active learning loop รับมือ lighting drift
แสงในไลน์ผลิตเปลี่ยนตามเวลา/กะ ทำให้ distribution เลื่อน (lighting drift) การ label ทุกภาพใหม่แพงเกินไป active learning เลือกเฉพาะภาพที่โมเดล “ไม่มั่นใจที่สุด” (uncertainty sampling) มาให้คน label เพราะภาพเหล่านั้นให้ข้อมูลใหม่ต่อการเรียนรู้สูงสุดต่อหนึ่ง label
1import numpy as np234def margin_uncertainty(probs: np.ndarray) -> np.ndarray:5 """ความไม่มั่นใจแบบ margin: 1 - (top1 - top2). สูง = ไม่มั่นใจ."""6 sorted_p = np.sort(probs, axis=1)[:, ::-1]7 margin = sorted_p[:, 0] - sorted_p[:, 1]8 return 1.0 - margin91011def select_for_labeling(12 probs: np.ndarray, # (N, num_classes)13 budget: int,14 near_threshold: tuple[float, float] | None = None,15 binary_score: np.ndarray | None = None,16) -> np.ndarray:17 """คืน index ของภาพที่ควรส่งให้คน label ภายใต้ budget ที่จำกัด."""18 uncertainty = margin_uncertainty(probs)1920 # เน้นภาพที่คะแนนใกล้เส้นตัดสินใจ ซึ่งมักเป็นผลของ drift21 if near_threshold is not None and binary_score is not None:22 lo, hi = near_threshold23 in_band = (binary_score >= lo) & (binary_score <= hi)24 uncertainty = np.where(in_band, uncertainty + 1.0, uncertainty)2526 return np.argsort(uncertainty)[::-1][:budget]กัน catastrophic forgetting: EWC + Experience Replay
เมื่อเทรนต่อด้วยข้อมูลใหม่ล้วน โมเดลมักลืม task เดิม (catastrophic forgetting) สองเทคนิคที่ใช้คู่กันได้ผลดี: EWC เพิ่ม regularization ลงโทษการขยับ weight ที่สำคัญต่อ task เดิม (ถ่วงด้วย Fisher information) ส่วน Experience Replay ผสมตัวอย่างเก่าเข้าไปใน batch เพื่อเสริมความจำโดยตรง
1import torch2import torch.nn as nn345class EWC:6 """Elastic Weight Consolidation: ปกป้อง weight สำคัญของ task เดิม."""78 def __init__(self, model: nn.Module, lambda_: float = 1000.0):9 self.model = model10 self.lambda_ = lambda_11 # ค่า weight อ้างอิงหลังเรียน task เดิม (theta*)12 self.star = {n: p.detach().clone() for n, p in model.named_parameters()}13 self.fisher: dict[str, torch.Tensor] = {}1415 def compute_fisher(self, loader, device: str) -> None:16 """ประมาณ Fisher information = ความสำคัญของแต่ละ weight ต่อ task เดิม."""17 fisher = {n: torch.zeros_like(p) for n, p in self.model.named_parameters()}18 self.model.eval()19 for x, y in loader:20 x, y = x.to(device), y.to(device)21 self.model.zero_grad()22 loss = nn.functional.cross_entropy(self.model(x), y)23 loss.backward()24 for n, p in self.model.named_parameters():25 if p.grad is not None:26 fisher[n] += p.grad.detach() ** 227 n_batches = max(len(loader), 1)28 self.fisher = {n: f / n_batches for n, f in fisher.items()}2930 def penalty(self) -> torch.Tensor:31 """loss term ที่ลงโทษการขยับ weight สำคัญออกจาก theta*."""32 loss = torch.tensor(0.0, device=next(self.model.parameters()).device)33 for n, p in self.model.named_parameters():34 if n in self.fisher:35 loss = loss + (self.fisher[n] * (p - self.star[n]) ** 2).sum()36 return self.lambda_ * loss1import random2import torch3import torch.nn as nn456def train_step_continual(7 model: nn.Module,8 new_batch, # (x, y) จากข้อมูลใหม่9 replay_buffer: list, # ตัวอย่างเก่าที่เก็บไว้10 ewc: "EWC",11 optimizer: torch.optim.Optimizer,12 device: str,13 replay_size: int = 16,14) -> float:15 """หนึ่งก้าวการเทรนต่อเนื่อง: ข้อมูลใหม่ + experience replay + EWC penalty."""16 model.train()17 x_new, y_new = (t.to(device) for t in new_batch)1819 # Experience Replay: ดึงตัวอย่างเก่ามาผสม กัน distribution เก่าหายไป20 if replay_buffer:21 sampled = random.sample(replay_buffer, min(replay_size, len(replay_buffer)))22 x_old = torch.stack([s[0] for s in sampled]).to(device)23 y_old = torch.tensor([s[1] for s in sampled]).to(device)24 x = torch.cat([x_new, x_old]); y = torch.cat([y_new, y_old])25 else:26 x, y = x_new, y_new2728 optimizer.zero_grad()29 loss = nn.functional.cross_entropy(model(x), y) + ewc.penalty()30 loss.backward()31 optimizer.step()32 return float(loss.item())เช็กลิสต์ก่อนสัมภาษณ์
- อธิบาย FAR/FRR เป็น trade-off และเลือกจุดทำงานด้วย expected cost ไม่ใช่ accuracy
- วาด ROC ได้ และชี้ว่าจุด optimal อยู่ที่ใดเมื่อต้นทุนสองด้านต่างกัน
- อธิบาย uncertainty sampling ว่าเลือกภาพ confidence ต่ำ/entropy สูงเพื่อใช้ label budget คุ้มสุด
- อธิบาย catastrophic forgetting และบอกได้ว่า EWC ใช้ Fisher information ถ่วงความสำคัญ weight
- รู้ว่า EWC (ระดับ parameter) กับ Experience Replay (ระดับข้อมูล) เสริมกันอย่างไร
สรุปสำคัญ
- ในงาน QC ต้นทุนของ false accept (ปล่อยของเสีย) กับ false reject (ทิ้งของดี) ไม่เท่ากัน เลือก threshold ที่ต้นทุนรวมต่ำสุดบน ROC ไม่ใช่ที่ accuracy สูงสุด
- lighting drift ทำให้ distribution เปลี่ยน ใช้ active learning ดึงเฉพาะภาพที่โมเดลไม่มั่นใจ (uncertainty sampling) มาให้คน label เพื่อใช้ budget คุ้มที่สุด
- การเทรนต่อด้วยข้อมูลใหม่ล้วน ทำให้ลืมของเก่า (catastrophic forgetting) EWC ลงโทษการเปลี่ยน weight สำคัญ ส่วน Experience Replay ผสมข้อมูลเก่าเข้าไปด้วย
ควิซท้ายบท
01ในการตรวจ QC เครื่องประดับ ทำไมจึงไม่ควรเลือก threshold ที่ให้ accuracy สูงสุด
02FAR (False Acceptance Rate) และ FRR (False Rejection Rate) สัมพันธ์กันอย่างไรเมื่อปรับ threshold
03กลยุทธ์ active learning แบบ uncertainty sampling รับมือ lighting drift อย่างไรให้คุ้ม label budget
04Elastic Weight Consolidation (EWC) กัน catastrophic forgetting ด้วยหลักการใด