AI Engineer › Embeddings และ RAG
Chunking: ตัดเอกสารให้ค้นหาได้
ประมาณ 35 นาที
เป้าหมายของบท
- ตัดเอกสารเป็น chunk แบบ sliding window (size/overlap) และคำนวณจำนวน chunk ได้
- ประเมินขนาด chunk ด้วย hit@k คู่กับปริมาณข้อความที่ส่งเข้า prompt
- ระวังตัวเลขที่หลอกตาเมื่อ chunk ใหญ่ครอบคลุมหลายเอกสาร
ทำไม RAG ต้องตัดเอกสาร
RAG (retrieval-augmented generation) = ดึงเฉพาะส่วนของเอกสารที่เกี่ยวกับคำถามมาใส่ใน prompt แล้วให้โมเดลตอบจากส่วนนั้น (จากบทแรก: ดึง 3 ส่วนแทนยัดทั้งฐาน ประหยัด token ราว 7 เท่า) เอกสารจริงยาวหลายหน้า จึงต้องตัดเป็น chunk ก่อน แล้วค้นหาระดับ chunk — chunk ที่ตัดไม่ดีทำให้คำตอบขาด (ตัดกลางประโยคสำคัญ) หรือรก (chunk ใหญ่จนมีเรื่องปน)
ชุดข้อมูลของเรา: 24 เอกสาร ~40 คำ รวม 973 คำ (ทั้งหมดเป็นข้อความสั้นอยู่แล้ว) — เพื่อฝึก chunking เราต่อทั้งหมดเป็น “คู่มือ” เล่มเดียวแล้วจดว่าแต่ละเอกสารอยู่ช่วงคำไหน (
spans)
Chunk แบบหน้าต่างเลื่อน (sliding window)
def chunk(words, size, overlap):
step = size - overlap
out = []
for start in range(0, len(words), step):
out.append((start, " ".join(words[start:start + size])))
if start + size >= len(words):
break
return out
size= ความยาว chunk (คำ),overlap= จำนวนคำที่ซ้อนกับ chunk ก่อนหน้า (กันตัดกลางข้อมูลสำคัญ)- 973 คำ ขนาด 100 overlap 20 → เลื่อนทีละ 80 คำ ได้ 12 chunk (chunk สุดท้ายสั้นกว่า: 93 คำ)
- ของจริงมักตัดตามโครงสร้าง (หัวข้อ ย่อหน้า ประโยค) ก่อน แล้วค่อยจำกัดความยาว — หลักการคือ 1 chunk ควรเป็น “หน่วยความหมาย” ที่อ่านเดี่ยวๆ รู้เรื่อง
วัดผลของขนาด chunk: ระวังตัวเลขที่หลอกตา
เราวัด hit@k = สัดส่วนคำถามที่ chunk อันดับ 1..k มีเอกสารเฉลยอยู่ด้วย (ค้นด้วย TF-IDF บน 30 คำถามของเรา):
| ขนาด (คำ) | overlap | จำนวน chunk | hit@1 | hit@3 |
|---|---|---|---|---|
| 20 | 0 | 49 | 0.5667 | 0.8000 |
| 40 | 0 | 25 | 0.6333 | 0.9000 |
| 60 | 0 | 17 | 0.5667 | 0.9333 |
| 100 | 20 | 12 | 0.6333 | 0.8667 |
| 200 | 40 | 6 | 0.7000 | 1.0000 |
| 400 | 80 | 3 | 0.8333 | 1.0000 |
| 973 (ทั้งเล่ม) | – | 1 | 1.0000 | 1.0000 |
hit@k ดูดีขึ้นเรื่อยๆ เมื่อ chunk ใหญ่ขึ้น — แต่นั่นเพราะ chunk ใหญ่ครอบคลุมหลายเอกสาร โอกาสทับเฉลยจึงสูง ทั้งเล่มได้ 1.0 โดยไม่ได้ “ค้นหา” อะไรเลย ต้องดูควบคู่กับ ปริมาณข้อความที่ส่งเข้า prompt (k × ขนาด chunk): ขนาด 40 ส่ง 3 × 40 = 120 คำ ได้ hit@3 0.90 เทียบกับขนาด 200 (overlap 40) ส่ง 600 คำ ได้ 1.00 — ต่อ 100 คำที่ส่ง ประสิทธิภาพ 0.75 เทียบ 0.1667 (ถ้า overlap 0 ทั้งคู่: ขนาด 200 ได้ hit@3 0.9667 หรือ 0.1611 ต่อ 100 คำ ข้อสรุปเหมือนเดิม)
สรุปหลักคิด: ไม่มีขนาดที่ดีที่สุดสากล — เลือกจากการวัด hit@k และ ต้นทุน/สิ่งรบกวนของ context บนชุดคำถามของงานจริงของคุณ, และใช้ overlap เมื่อกลัวข้อมูลสำคัญถูกตัดกลาง
ตัวอย่างแก้โจทย์
กด "ขั้นต่อไป" ทีละขั้น ลองรันและแก้โค้ดในแต่ละขั้นได้
ตัดคู่มือเป็น chunk
ต่อ 24 เอกสารเป็นคู่มือเล่มเดียวแล้วตัดด้วยหน้าต่างเลื่อน
คู่มือมี 973 คำ; เอกสาร d05 อยู่ที่ช่วงคำ (ค่า start, end) ใด
ขนาด chunk กับ hit@k และปริมาณที่ส่ง
วัดด้วย evaluate (ให้ไว้ ค้นด้วย TF-IDF)
ขนาด 40 ไม่ overlap: 25 chunk, hit@1 0.6333, hit@3 0.9
เติมช่องว่าง
แทน ___ ด้วยโค้ดที่ถูกต้อง แล้วกดตรวจคำตอบ
ตัดข้อความ 10 คำเป็น chunk ยาว 4 คำ ไม่ overlap แล้วนับจำนวน chunk ในตัวแปร n_chunks เติม ___ ให้ครบ
แบบฝึกท้ายบท
บทนี้จะนับว่าเสร็จเมื่อผ่านข้อพื้นฐานและประยุกต์ครบ ข้อท้าทายได้ดาวเพิ่ม
🟢 พื้นฐาน
คู่มือ words มีกี่คำ (n_words) และเอกสาร d05 อยู่ที่ช่วงคำ start ถึง end (spans) เก็บไว้ในตัวแปร d05_start, d05_end
ใช้ chunk (ให้ไว้) ขนาด 50 ไม่ overlap ได้กี่ chunk เก็บไว้ในตัวแปร n_chunks_50
ขนาด 100 overlap 20: chunk สุดท้ายมีกี่คำ เก็บไว้ในตัวแปร last_chunk_words
ขนาด 100 overlap 20: ตำแหน่งเริ่มของ 4 chunk แรก (list ของ int) เก็บไว้ในตัวแปร first_starts
🟡 ประยุกต์
ขนาด 100 overlap 20: chunk แต่ละอันทับกับกี่เอกสาร (ช่วงคำของ chunk คือ [start, start + จำนวนคำ) ทับกับ spans ถ้า s < g_end และ e > g_start) เก็บจำนวนเอกสารเฉลี่ยต่อ chunk (ปัด 2 ตำแหน่ง) ไว้ใน avg_docs_per_chunk
ใช้ evaluate (ให้ไว้) เก็บ hit@3 ของ chunk ขนาด 30, 60 และ 200 (overlap 0) ไว้ในตัวแปร hit3_30, hit3_60, hit3_200
ประสิทธิภาพต่อปริมาณที่ส่ง = hit@3 ÷ (3 × ขนาด ÷ 100) (ปัด 4 ตำแหน่ง, overlap 0) ของขนาด 40 และ 200 เก็บไว้ในตัวแปร eff_40, eff_200
🔴 ท้าทาย
เขียนการประเมินเอง (chunk ให้ไว้): ขนาด 60 ไม่ overlap ค้นด้วย TfidfVectorizer(stop_words="english") บนข้อความของ chunk ทุกอัน จัดอันดับด้วย cosine_similarity กับคำถามใน qs (np.argsort(-sims[i], kind="stable")) — chunk ถูกต้องถ้าช่วงคำของมันทับ spans ของเอกสารเฉลย เก็บ hit@1 และ hit@3 (ปัด 4 ตำแหน่ง) ไว้ใน hit1_60, hit3_60
เอกสารที่ถูกตัดข้ามหลาย chunk (ทับกับ chunk ตั้งแต่ 2 อันขึ้นไป) มีกี่เอกสาร เมื่อใช้ขนาด 60 ไม่ overlap (n_split_60) และขนาด 100 ไม่ overlap (n_split_100)
Quiz
สรุป
- RAG ค้นหาระดับ chunk: ตัดเอกสารด้วย size/overlap (973 คำ ขนาด 100 overlap 20 → 12 chunk) โดยให้ chunk เป็นหน่วยความหมายที่อ่านเดี่ยวๆ รู้เรื่อง
- hit@k เพิ่มตามขนาด chunk (ขนาด 200 hit@3 = 1.0, ทั้งเล่ม = 1.0) แต่เป็นเพราะครอบคลุมหลายเอกสาร — ต้องดูปริมาณที่ส่งควบคู่ (ขนาด 40: 0.75 ต่อ 100 คำ vs ขนาด 200 overlap 40: 0.1667)
- ไม่มีขนาดสากล เลือกจากการวัดบนคำถามจริง; overlap ต้องน้อยกว่า size
คำถามสัมภาษณ์
คุณจะเลือกขนาด chunk สำหรับระบบ RAG อย่างไร
เริ่มจากตัดตามโครงสร้างเอกสาร (หัวข้อ ย่อหน้า) แล้วจำกัดความยาว จากนั้นทดลองหลายขนาดและ overlap บนชุดคำถามที่มีเฉลย วัด hit@k/recall ของ retrieval และปริมาณ token ที่ส่งเข้า prompt พร้อมคุณภาพคำตอบ เลือกจุดที่คุ้ม ระวังตัวชี้วัดที่ดีขึ้นเพียงเพราะ chunk ใหญ่ครอบคลุมทุกอย่าง
chunk เล็กเกินไปและใหญ่เกินไปมีปัญหาอะไร
เล็กเกินไปตัดบริบทขาด ข้อมูลที่ต้องอ่านร่วมกันแยกไปอยู่คนละ chunk และมีจำนวน chunk มาก; ใหญ่เกินไปมีหลายเรื่องปน retrieval ไม่แม่นยำ สิ้นเปลือง token และสิ่งรบกวนทำให้โมเดลตอบแย่ลง