📗 บทที่ 2 — ทำไมค้นไทยเพี้ยน (และแก้ใน 30 บรรทัด)¶
คู่กับ: หนังสือบทที่ 2 · สไลด์ 4/4 "ก่อน/หลัง bge-m3"
บทเรียนสำคัญที่สุดของเล่ม: vector DB ไม่ได้เข้าใจภาษา — embedding model เข้าใจ
ติดตั้ง (bge-m3 โหลด ~2GB ครั้งแรก — Colab ฟรีรันได้)¶
In [1]:
import sys
IN_COLAB = 'google.colab' in sys.modules
if IN_COLAB:
%pip -q install chromadb sentence-transformers
import chromadb
print('chromadb', chromadb.__version__, '· พร้อม ✓ (เครื่องเรา: ใช้ kernel vector-book)')
chromadb 1.5.9 · พร้อม ✓ (เครื่องเรา: ใช้ kernel vector-book)
1) โจทย์ชุดเดียวกับบทที่ 1 — คราวนี้ 2 collection เทียบกัน¶
brain_default: embedder ที่แถมมา (all-MiniLM-L6-v2 — เทรนอังกฤษ)brain_bge: bge-m3 (multilingual 100+ ภาษา — ตัวเดียวกับ ARRA production)
In [2]:
# ---- ตัว embed อัจฉริยะ: เครื่องเรา→Ollama (เร็ว+privacy) · Colab→sentence-transformers ----
import urllib.request, json
import numpy as np
def _ollama_ok():
try:
urllib.request.urlopen('http://localhost:11434/api/tags', timeout=2)
return True
except Exception:
return False
if _ollama_ok():
def embed_texts(texts):
req = urllib.request.Request('http://localhost:11434/api/embed',
data=json.dumps({'model': 'bge-m3', 'input': list(texts)}).encode(),
headers={'Content-Type': 'application/json'})
with urllib.request.urlopen(req, timeout=180) as r:
V = np.array(json.load(r)['embeddings'])
return V / np.linalg.norm(V, axis=1, keepdims=True)
print('embed ด้วย bge-m3 ผ่าน Ollama (local, ข้อมูลไม่ออกเครื่อง) ✓')
else:
from sentence_transformers import SentenceTransformer
_model = SentenceTransformer('BAAI/bge-m3')
def embed_texts(texts):
return _model.encode(list(texts), normalize_embeddings=True)
print('embed ด้วย bge-m3 ผ่าน sentence-transformers ✓')
embed ด้วย bge-m3 ผ่าน Ollama (local, ข้อมูลไม่ออกเครื่อง) ✓
In [3]:
from chromadb.utils.embedding_functions import EmbeddingFunction
class BgeM3(EmbeddingFunction):
def __init__(self):
pass
def __call__(self, texts):
return embed_texts(texts).tolist()
print('BgeM3 embedding function พร้อม ✓')
NOTES = [
'วิธีสอนนักศึกษาให้เข้าใจ vector search: เริ่มจาก cosine similarity ก่อน',
'สูตรกาแฟ cold brew: กาแฟ 100g น้ำ 1L แช่ 18 ชั่วโมง',
'ประชุมกับอาจารย์ฝน เรื่อง workshop วันที่ 26 กรกฎาคม ที่มหาวิทยาลัย',
'Embedding คือการแปลงข้อความเป็นตัวเลขหลายมิติที่เก็บความหมาย',
'รายการซื้อของ: นม ไข่ ขนมปัง กาแฟดริป',
]
IDS = [f'n{i}' for i in range(1, 6)]
client = chromadb.PersistentClient(path='./chroma_db')
col_default = client.get_or_create_collection('brain_default')
col_bge = client.get_or_create_collection('brain_bge', embedding_function=BgeM3())
col_default.upsert(ids=IDS, documents=NOTES)
col_bge.upsert(ids=IDS, documents=NOTES)
print('2 สมอง พร้อมเทียบ ✓')
BgeM3 embedding function พร้อม ✓
2 สมอง พร้อมเทียบ ✓
2) ยิง query ไทยชุดเดียวกัน ใส่ทั้งสองสมอง¶
In [4]:
QUERIES = {
'อยากสอนเรื่อง AI ค้นหา': 'สอน', # คำสำคัญที่ top-1 ควรมี
'เครื่องดื่ม': 'กาแฟ',
'นัดหมายกับใครบ้าง': 'ประชุม',
}
def top1(col, q):
r = col.query(query_texts=[q], n_results=1)
return r['documents'][0][0], 1 - r['distances'][0][0]
for q, keyword in QUERIES.items():
d1, s1 = top1(col_default, q)
d2, s2 = top1(col_bge, q)
print(f'Q: {q}')
print(f" MiniLM {'✓' if keyword in d1 else '✗'} ({s1:+.3f}) {d1[:45]}")
print(f" bge-m3 {'✓' if keyword in d2 else '✗'} ({s2:+.3f}) {d2[:45]}")
print()
Q: อยากสอนเรื่อง AI ค้นหา MiniLM ✗ (+0.531) ประชุมกับอาจารย์ฝน เรื่อง workshop วันที่ 26 bge-m3 ✓ (+0.266) วิธีสอนนักศึกษาให้เข้าใจ vector search: เริ่ม
Q: เครื่องดื่ม MiniLM ✓ (-0.130) รายการซื้อของ: นม ไข่ ขนมปัง กาแฟดริป bge-m3 ✓ (+0.043) รายการซื้อของ: นม ไข่ ขนมปัง กาแฟดริป
Q: นัดหมายกับใครบ้าง MiniLM ✗ (-0.130) รายการซื้อของ: นม ไข่ ขนมปัง กาแฟดริป bge-m3 ✓ (+0.028) ประชุมกับอาจารย์ฝน เรื่อง workshop วันที่ 26
✅ วัดผลตัวเอง #2 — bge-m3 ต้องถูกทั้ง 3 ข้อ¶
In [5]:
wins = sum(1 for q, kw in QUERIES.items() if kw in top1(col_bge, q)[0])
print(f'bge-m3 ถูก {wins}/3')
assert wins == 3, 'bge-m3 ควรถูกทั้ง 3 — ลองรันเซลล์บนใหม่'
print('✅ ผ่าน! เปลี่ยนแค่ embedding model — database ตัวเดิม โน้ตชุดเดิม query ชุดเดิม')
bge-m3 ถูก 3/3 ✅ ผ่าน! เปลี่ยนแค่ embedding model — database ตัวเดิม โน้ตชุดเดิม query ชุดเดิม
📌 สิ่งที่พิสูจน์แล้ว¶
| ตัวที่แถม (MiniLM) | bge-m3 | |
|---|---|---|
| เทรนด้วย | อังกฤษเป็นหลัก | 100+ ภาษา (contrastive ข้ามภาษา) |
| มิติ | 384 | 1024 |
| ผลไทย | ผิดเป็นส่วนใหญ่ + คะแนนติดลบ | ถูก 3/3 |
กฎ: เลือก embedding model ก่อนเลือก database · ทดสอบด้วยภาษาจริงของเราเสมอ
📊 เห็นภาพ — MiniLM vs bge-m3 (query ไทยชุดเดียวกัน)¶
In [6]:
# @@VIZ@@
import matplotlib, matplotlib.pyplot as plt
from matplotlib import font_manager as _fm
_av={f.name for f in _fm.fontManager.ttflist}
for _f in ['Thonburi','Loma','Sarabun','Noto Sans Thai','TH Sarabun New']:
if _f in _av: matplotlib.rcParams['font.family']=_f; break
matplotlib.rcParams['axes.unicode_minus']=False
import numpy as np
qs=list(QUERIES)
def _t1(c,q):
r=c.query(query_texts=[q],n_results=1); return 1-r['distances'][0][0]
mini=[_t1(col_default,q) for q in qs]; bge=[_t1(col_bge,q) for q in qs]
x=np.arange(len(qs)); w=0.35; plt.figure(figsize=(8,4))
plt.bar(x-w/2,mini,w,label='MiniLM (แถม)',color='#e34948')
plt.bar(x+w/2,bge,w,label='bge-m3',color='#1baf7a')
plt.axhline(0,color='#888',lw=.8); plt.xticks(x,qs,rotation=12,ha='right')
plt.ylabel('cosine top-1'); plt.legend(); plt.title('บทที่ 2 — bge-m3 ยกคะแนนขึ้นบวกทุก query')
plt.tight_layout(); plt.show()
🏋️ แบบฝึก¶
- ลองภาษาอังกฤษ: query
'appointment with someone'ใส่ทั้ง 2 สมอง — MiniLM ทำงานดีขึ้นไหม? ทำไม? - ลองค้นข้ามภาษา: ใส่โน้ตไทย ค้นด้วยอังกฤษบน bge-m3 (
'meeting with professor') — เจอไหม?
บทต่อไป: ch03_filter_metadata.ipynb