AI Engineer › Embeddings และ RAG

ประเมิน retrieval: hit@k, MRR และการวิเคราะห์ความผิดพลาด

ประมาณ 40 นาที

เป้าหมายของบท

  • คำนวณ hit@k, MRR และสถิติอันดับจากชุดคำถามที่มีเฉลย
  • แยกวิเคราะห์ตามกลุ่มและรายข้อที่พลาดเพื่อหาจุดที่ต้องแก้
  • ประเมินความไม่แน่นอนด้วย bootstrap และเปรียบเทียบการตั้งค่าอย่างระมัดระวัง

ไม่วัด = ไม่รู้ว่าดีขึ้นหรือแย่ลง

ระบบ RAG มีตัวแปรมากมาย (ขนาด chunk, วิธีทำเวกเตอร์, k, prompt) การแก้ทีละอย่างโดยดูจากตัวอย่างไม่กี่ข้อทำให้หลงทางได้ง่าย วิธีที่ใช้ได้จริง: มี ชุดคำถามที่มีเฉลย (golden set — ของเราคือ kb_questions.csv 30 ข้อ พร้อม gold_doc_id) แล้ววัดตัวเลขเดิมทุกครั้งที่แก้

ตัวชี้วัดของ retrieval

ให้ rank = อันดับ (เริ่มที่ 1) ของเอกสารเฉลยในรายการที่เรียงตามความคล้าย:

  • hit@k = สัดส่วนคำถามที่ rank ≤ k
  • MRR (mean reciprocal rank) = ค่าเฉลี่ยของ 1 ÷ rank (ให้รางวัลอันดับต้นๆ มากกว่า)
  • mean/median rank และ rank แย่สุด (ดูหางของการกระจาย)

ผล TF-IDF (stop words) บน 30 คำถาม: hit@1 0.6333, hit@3 0.8333, MRR 0.752; rank เฉลี่ย 2.1, median 1, แย่สุดอันดับ 9 (คำถามเรื่องเวลาทำการ) 5 ข้อ (16.7%) ที่เฉลยไม่อยู่ใน top-3

ranks = gold_ranks(TfidfVectorizer(stop_words="english"))
hit3 = (ranks <= 3).mean()
mrr = (1 / ranks).mean()

แยกดูตามกลุ่ม: ตัวเลขรวมซ่อนจุดอ่อน

หมวดของเอกสารเฉลย คำถาม hit@1 hit@3
returns 6 0.1667 0.5
shipping 5 0.4 0.8
account 4 1.0 1.0
promo 4 1.0 1.0
payment 4 0.75 1.0

หมวด returns แย่สุดเพราะเอกสาร d05–d08 (return policy / refund / exchange / damaged) ใช้คำคล้ายกันและคำถามใช้คำที่ต่างไป (broken, money, swap) — บอกได้ว่าควรแก้ตรงไหน (เช่น ปรับเอกสาร หรือใช้ embeddings) หมวดที่มีคำถามเดียว (privacy, support, warranty) ยังสรุปอะไรไม่ได้

ความไม่แน่นอน: 30 ข้อน้อยมาก

ต่างกันหนึ่งคำถาม = 3.3 จุดเปอร์เซ็นต์ ใช้ bootstrap สุ่มคำถามแบบใส่คืนหลายรอบ (1,000 รอบ, seed 42) เพื่อดูช่วงความไม่แน่นอน: hit@3 = 0.8333 มีช่วง 95% ประมาณ 0.70 – 0.9667, hit@1 = 0.6333 มีช่วง 0.4667 – 0.80 — กว้างมาก

เปรียบเทียบการตั้งค่า: ต่างกันจริงหรือแค่โชค

การตั้งค่า hit@1 hit@3 MRR
TF-IDF ปกติ 0.6000 0.7333 0.7054
+ stop words 0.6333 0.8333 0.7520
+ stop words + bigram 0.6333 0.8333 0.7543
+ stop words + ใส่ title 0.6000 0.8667 0.7465

ดูว่า กี่คำถามที่เปลี่ยน: เพิ่ม bigram เปลี่ยน 2 ข้อ (ดีขึ้น 2 แย่ลง 0) ส่วนเพิ่ม title เปลี่ยน 5 ข้อ (ดีขึ้น 2 แย่ลง 3) — hit@3 0.8667 ดูสูงกว่า แต่ MRR ไม่ได้ดีขึ้นและมีคำถามที่แย่ลงด้วย จึงสรุปไม่ได้ว่าดีกว่า หลักการ: ต่างกันไม่กี่ข้อบนชุดเล็ก = อยู่ในความไม่แน่นอน อย่าเลือกตามตัวเลขที่ดีที่สุดโดยไม่ดูรายข้อ และอย่าจูนจนแนบชุดคำถามชุดเดียว (overfit) — เก็บชุดคำถามอีกชุดไว้ตรวจสุดท้าย

วงจรปรับปรุง

วัด → ดูรายข้อที่พลาด → เดาสาเหตุ (คำต่างกัน? chunk ตัดผิด? เอกสารกำกวม?) → แก้ทีละอย่าง → วัดซ้ำด้วยชุดเดิม → เก็บผลไว้เทียบ

ตัวอย่างแก้โจทย์

กด "ขั้นต่อไป" ทีละขั้น ลองรันและแก้โค้ดในแต่ละขั้นได้

hit@k และ MRR

คำนวณจากอันดับของเอกสารเฉลย

  1. อันดับของเอกสารเฉลยของ 30 คำถาม (TF-IDF + stop words) — ส่วนใหญ่ 1, แย่สุด 9

    Python

จุดอ่อนตามหมวดและความไม่แน่นอน

แยกตามหมวดของเอกสารเฉลย แล้ว bootstrap

  1. hit@1 และ hit@3 แยกตามหมวด: returns แย่สุด (0.1667 / 0.5)

    Python

เติมช่องว่าง

แทน ___ ด้วยโค้ดที่ถูกต้อง แล้วกดตรวจคำตอบ

ยังไม่ได้ทำ

คำนวณ hit@3 จากอันดับที่ให้ไว้ เก็บไว้ในตัวแปร hit3 เติม ___ ให้ครบ

แบบฝึกท้ายบท

บทนี้จะนับว่าเสร็จเมื่อผ่านข้อพื้นฐานและประยุกต์ครบ ข้อท้าทายได้ดาวเพิ่ม

🟢 พื้นฐาน

🟢 พื้นฐานยังไม่ได้ทำ

จาก ranks (ให้ไว้): hit@1 และ hit@3 ปัด 4 ตำแหน่ง เก็บไว้ในตัวแปร hit1, hit3

🟢 พื้นฐานยังไม่ได้ทำ

MRR = ค่าเฉลี่ยของ 1 ÷ อันดับ (ปัด 4 ตำแหน่ง) เก็บไว้ในตัวแปร mrr

🟢 พื้นฐานยังไม่ได้ทำ

อันดับเฉลี่ย (ปัด 2 ตำแหน่ง) เก็บไว้ใน mean_rank, มัธยฐานไว้ใน median_rank (float) และอันดับที่แย่สุดไว้ใน worst_rank

🟢 พื้นฐานยังไม่ได้ทำ

มีกี่คำถามที่เอกสารเฉลยไม่อยู่ใน top-3 (อันดับ > 3) เก็บไว้ในตัวแปร n_miss_at3 และ question_id ของทั้งหมดเป็น list ไว้ใน miss_ids

🟡 ประยุกต์

🟡 ประยุกต์ยังไม่ได้ทำ

hit@3 ของคำถามที่เฉลยอยู่ในหมวด returns, shipping และ support (จากคอลัมน์ category ของ gold ที่ให้ไว้) ปัด 4 ตำแหน่ง เก็บไว้ในตัวแปร returns_hit3, shipping_hit3, support_hit3

🟡 ประยุกต์ยังไม่ได้ทำ

ในหมวดที่มีคำถามอย่างน้อย 4 ข้อ หมวดใดมี hit@1 ต่ำที่สุด เก็บชื่อหมวดไว้ใน weakest_category และค่า hit@1 (ปัด 4 ตำแหน่ง) ไว้ใน weakest_hit1

🟡 ประยุกต์ยังไม่ได้ทำ

question_id ของคำถามในหมวด returns ที่เฉลยไม่ได้อันดับ 1 (ranks > 1) เก็บเป็น list ไว้ใน returns_misses และจำนวนไว้ใน n_returns_misses

🔴 ท้าทาย

🔴 ท้าทายยังไม่ได้ทำ

bootstrap ช่วง 95% ของ hit@3: rng = np.random.RandomState(42), idx = rng.randint(0, len(ranks), (1000, len(ranks))), คำนวณ hit@3 ของแต่ละรอบจาก (ranks <= 3)[idx] แล้วหา percentile ที่ 2.5 และ 97.5 ปัด 4 ตำแหน่ง เก็บไว้ใน ci_low, ci_high

🔴 ท้าทายยังไม่ได้ทำ

เปรียบเทียบ TfidfVectorizer(stop_words="english") กับ TfidfVectorizer(stop_words="english", ngram_range=(1, 2)) ด้วย gold_ranks (ให้ไว้): เก็บจำนวนคำถามที่อันดับเปลี่ยน (n_changed), ที่ดีขึ้น (n_improved: อันดับเลขน้อยลง) และที่แย่ลง (n_worse)

Quiz

1. MRR ต่างจาก hit@k อย่างไร
2. ตัวเลข hit@3 รวม 0.8333 อาจซ่อนอะไร
3. บนชุดคำถาม 30 ข้อ การตั้งค่า A ได้ hit@3 สูงกว่า B หนึ่งข้อ ควรสรุปอย่างไร
4. ทำไมต้องเก็บชุดคำถามอีกชุดไว้ตรวจสุดท้าย

สรุป

  • ประเมิน retrieval ด้วยชุดคำถามที่มีเฉลย: hit@1 0.6333, hit@3 0.8333, MRR 0.752; rank เฉลี่ย 2.1, แย่สุดอันดับ 9
  • ตัวเลขรวมซ่อนจุดอ่อน: หมวด returns hit@1 0.1667 / hit@3 0.5; ชุด 30 ข้อกว้างมาก (bootstrap 95%: hit@3 0.70–0.9667)
  • การตั้งค่าที่ต่างกัน 1–2 ข้ออยู่ในความไม่แน่นอน (bigram เปลี่ยน 2 ข้อ ดีขึ้น 2 แย่ลง 0; title เปลี่ยน 5 ข้อ ดีขึ้น 2 แย่ลง 3) — ดูรายข้อและเก็บชุดตรวจสุดท้าย

คำถามสัมภาษณ์

คุณจะประเมินระบบ RAG อย่างไร

แยกประเมิน retrieval (hit@k, MRR, recall บนชุดคำถามที่มีเฉลยของเอกสาร) ออกจากคุณภาพคำตอบ (ความถูกต้อง การยึดบริบท/faithfulness การอ้างอิง การปฏิเสธเมื่อไม่มีข้อมูล) วัดแยกตามกลุ่มและรายข้อที่พลาด รายงานความไม่แน่นอน และเก็บชุดตรวจสุดท้ายแยกจากชุดที่ใช้จูน

ถ้า hit@k ต่ำ คุณจะวิเคราะห์หาสาเหตุอย่างไร

ดูรายข้อที่พลาดและอันดับของเฉลย แล้วแยกสาเหตุ: คำถามใช้คำต่างจากเอกสาร (ใช้ embeddings/query expansion/hybrid), chunk ตัดผิดหรือใหญ่เกินไป, เอกสารซ้ำ/กำกวมกันเอง (ปรับเอกสารหรือ metadata), หรือ k น้อยเกินไป (เพิ่ม k + reranker) แก้ทีละอย่างแล้ววัดซ้ำ