🗺️ แผนที่ความหมาย — จาก Dataset → Embed → Visualization¶
Notebook นี้คือ "บทความที่รันได้": อ่านจากบนลงล่าง กด Run ทีละเซลล์ แล้วคุณจะได้ แผนที่ความหมายของคำ แบบเดียวกับในหนังสือ Second Brain ด้วย Vector Search — ด้วยข้อมูลของคุณเอง
สิ่งที่จะได้เรียน:
- สร้าง dataset คำ/ประโยค (แก้เป็นของคุณเองได้)
- แปลงเป็นเวกเตอร์ด้วย bge-m3 (โมเดล multilingual ที่เข้าใจไทย)
- วัดความใกล้ด้วย cosine similarity (สมการเดียว)
- ฉายลง 2 มิติด้วย PCA แล้ววาดแผนที่
- อ่านผลอย่างถูกต้อง — ทำไมบางคำ "ดูใกล้" ทั้งที่ไม่เกี่ยว
ใช้ได้ทั้ง Google Colab (แนะนำ — ฟรี, ไม่ต้องติดตั้งอะไรในเครื่อง) และ Jupyter ในเครื่อง
ขั้นที่ 0 — ติดตั้ง (ครั้งเดียว ~2 นาที)¶
ใน Colab: กด Run เซลล์ล่างได้เลย · ในเครื่อง: ใช้ venv ก็ได้
import sys, platform
IN_COLAB = 'google.colab' in sys.modules
if IN_COLAB:
%pip -q install chromadb sentence-transformers scikit-learn matplotlib
import subprocess, matplotlib, matplotlib.font_manager as fm
subprocess.run(['apt-get', '-qq', 'install', '-y', 'fonts-thai-tlwg'], capture_output=True)
for f in fm.findSystemFonts(fontpaths=['/usr/share/fonts/truetype/tlwg']):
fm.fontManager.addfont(f)
matplotlib.rcParams['font.family'] = 'Loma'
else:
# เครื่องเรา: เลือก kernel "Python (vector-book)" (มี dependency ครบแล้ว)
import matplotlib
matplotlib.rcParams['font.family'] = 'Thonburi' if platform.system() == 'Darwin' else 'Loma'
matplotlib.rcParams['axes.unicode_minus'] = False
print('สภาพแวดล้อม:', 'Google Colab' if IN_COLAB else 'เครื่องเรา (kernel vector-book)', '· พร้อม ✓')
สภาพแวดล้อม: เครื่องเรา (kernel vector-book) · พร้อม ✓
ขั้นที่ 1 — Dataset: คำของเรา¶
จัดคำเป็นกลุ่มไว้ เพื่อระบายสีตอนวาดเท่านั้น — โมเดลไม่เห็นชื่อกลุ่มเลย ถ้าแผนที่ออกมากองตรงกลุ่มสี แปลว่าโมเดลจัดกลุ่มเองได้จริง
💡 ลองแก้เป็นคำของคุณเอง — หัวข้องานวิจัย, ชื่อวิชา, เมนูโปรด แล้วรันใหม่ทั้งหมด
WORDS = {
'สัตว์เลี้ยง': ['แมว', 'ลูกแมว', 'หมา', 'ลูกหมา', 'กระต่าย', 'cat', 'puppy'],
'อาหาร': ['ต้มยำกุ้ง', 'ผัดไทย', 'ส้มตำ', 'กาแฟลาเต้', 'ชาเขียว'],
'การเรียนการสอน': ['ประชุมกับอาจารย์', 'นัดหมายสัมมนา', 'workshop', 'สอนนักศึกษา', 'งานวิจัย'],
'การเงิน': ['บิตคอยน์', 'ราคาหุ้น', 'อัตราดอกเบี้ย', 'เงินเฟ้อ'],
'เทคโนโลยี': ['vector search', 'embedding', 'ปัญญาประดิษฐ์', 'ฐานข้อมูล'],
}
labels = [w for ws in WORDS.values() for w in ws]
groups = [g for g, ws in WORDS.items() for _ in ws]
print(f'ทั้งหมด {len(labels)} คำ ใน {len(WORDS)} กลุ่ม')
ทั้งหมด 25 คำ ใน 5 กลุ่ม
ขั้นที่ 2 — Embed: ข้อความ → เวกเตอร์ 1024 มิติ¶
ใช้ BAAI/bge-m3 — โมเดล multilingual (100+ ภาษา รวมไทย) ตัวเดียวกับที่ ARRA Oracle ใช้ใน production · ครั้งแรกจะโหลดโมเดล ~2GB (Colab ฟรีรันได้)
# ---- ตัว 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 ✓')
import numpy as np
V = embed_texts(labels) # normalize → cosine = dot เฉยๆ
print('รูปร่างข้อมูล:', V.shape) # (25, 1024) — 25 คำ คำละ 1024 ตัวเลข
print('ตัวอย่าง 6 มิติแรกของ "แมว":', np.round(V[0][:6], 3))
embed ด้วย bge-m3 ผ่าน Ollama (local, ข้อมูลไม่ออกเครื่อง) ✓
รูปร่างข้อมูล: (25, 1024) ตัวอย่าง 6 มิติแรกของ "แมว": [-0.035 0.025 -0.057 0.012 -0.002 -0.028]
🏠 ทางเลือกสำหรับคนรันในเครื่อง + มี Ollama: แทนเซลล์บนด้วย
import urllib.request, json, numpy as np def embed(texts): req = urllib.request.Request('http://localhost:11434/api/embed', data=json.dumps({'model': 'bge-m3', 'input': texts}).encode(), headers={'Content-Type': 'application/json'}) with urllib.request.urlopen(req) as r: return np.array(json.load(r)['embeddings']) V = embed(labels); V = V / np.linalg.norm(V, axis=1, keepdims=True)ได้ผลเหมือนกัน (โมเดลเดียวกัน) — และข้อมูลไม่ออกจากเครื่องเลย
ขั้นที่ 3 — Cosine Similarity: สมการเดียวที่ต้องรู้¶
$$\cos(A,B) = \frac{A \cdot B}{\|A\|\,\|B\|}$$
เรา normalize เวกเตอร์ไปแล้ว (‖v‖=1) → cosine เหลือแค่ dot product → คูณ matrix ทีเดียวได้คะแนน ทุกคู่พร้อมกัน
S = V @ V.T # (25×25) — คะแนนความใกล้ทุกคู่
def sim(a, b):
return S[labels.index(a), labels.index(b)]
print(f"cos(แมว, ลูกแมว) = {sim('แมว', 'ลูกแมว'):.3f} ← ใกล้จริง")
print(f"cos(แมว, cat) = {sim('แมว', 'cat'):.3f} ← ข้ามภาษา!")
print(f"cos(ประชุมกับอาจารย์, นัดหมายสัมมนา) = {sim('ประชุมกับอาจารย์', 'นัดหมายสัมมนา'):.3f}")
print(f"cos(ปัญญาประดิษฐ์, เงินเฟ้อ) = {sim('ปัญญาประดิษฐ์', 'เงินเฟ้อ'):.3f} ← แทบไม่เกี่ยว")
cos(แมว, ลูกแมว) = 0.888 ← ใกล้จริง cos(แมว, cat) = 0.781 ← ข้ามภาษา! cos(ประชุมกับอาจารย์, นัดหมายสัมมนา) = 0.757 cos(ปัญญาประดิษฐ์, เงินเฟ้อ) = 0.543 ← แทบไม่เกี่ยว
📏 สเกลอ่านคะแนน (สำคัญมาก)¶
| คะแนน | ความหมาย | ตัวอย่างจากข้อมูลจริง |
|---|---|---|
| ≥ 0.70 | 🟢 ใกล้จริง | แมว↔ลูกแมว 0.888 · cat↔แมว 0.781 |
| 0.55–0.70 | 🟡 เกี่ยวอ่อนๆ | งานวิจัย↔ปัญญาประดิษฐ์ 0.611 |
| < 0.55 | 🔴 แทบไม่เกี่ยว | ปัญญาประดิษฐ์↔เงินเฟ้อ 0.543 |
⚠️ กับดักที่เจอกันบ่อย: ถ้าถามหา "เพื่อนใกล้สุด 3 อันดับ" ระบบจะตอบเสมอ แม้คำนั้นจะโดดเดี่ยว (เพื่อนใกล้สุดอาจได้แค่ 0.54 = โซนแดง) → "ใกล้สุดในบรรดาที่มี" ≠ "เกี่ยวข้องจริง" → ระบบจริงต้องตั้ง threshold ตัด ไม่งั้นจะตอบมั่วจากผลคะแนนต่ำ (หนังสือบทที่ 11)
ขั้นที่ 4 — PCA: ฉาย 1024 มิติ ลงกระดาษ 2 มิติ¶
ตามองได้แค่ 2-3 มิติ — PCA เลือก "มุมมอง" ที่เก็บความแตกต่างไว้มากที่สุด
from sklearn.decomposition import PCA
pca = PCA(n_components=2)
P = pca.fit_transform(V)
kept = pca.explained_variance_ratio_.sum()
print(f'2 มิตินี้เก็บข้อมูลได้ {kept:.0%} ของทั้งหมด — ที่เหลือ {1-kept:.0%} หายไปตอนฉาย')
print('→ ตำแหน่งบนภาพ "เพี้ยนได้" — ตัวเลข cosine (ขั้นที่ 3) คือความจริง ภาพคือภาพประกอบ')
2 มิตินี้เก็บข้อมูลได้ 24% ของทั้งหมด — ที่เหลือ 76% หายไปตอนฉาย → ตำแหน่งบนภาพ "เพี้ยนได้" — ตัวเลข cosine (ขั้นที่ 3) คือความจริง ภาพคือภาพประกอบ
ขั้นที่ 5 — วาดแผนที่ความหมาย 🗺️¶
import matplotlib.pyplot as plt
COLORS = {'สัตว์เลี้ยง': '#3987e5', 'อาหาร': '#c98500', 'การเรียนการสอน': '#199e70',
'การเงิน': '#e66767', 'เทคโนโลยี': '#9085e9'}
fig, ax = plt.subplots(figsize=(13, 9))
for g in WORDS:
idx = [i for i, gg in enumerate(groups) if gg == g]
ax.scatter(P[idx, 0], P[idx, 1], s=140, c=COLORS[g], label=g, zorder=3)
for i, w in enumerate(labels):
ax.annotate(w, (P[i, 0], P[i, 1]), textcoords='offset points',
xytext=(0, 12), ha='center', fontsize=12)
ax.legend(fontsize=12, loc='best')
ax.set_title('แผนที่ความหมาย — โมเดลจัดกลุ่มเอง (ไม่มีใครบอกว่าคำไหนกลุ่มไหน)', fontsize=15)
ax.set_xticks([]); ax.set_yticks([])
plt.tight_layout(); plt.show()
ขั้นที่ 6 — เส้นเพื่อนบ้าน: ใครใกล้ใคร (พร้อมกันความเข้าใจผิด)¶
วาดเส้นจากทุกคำไปเพื่อนใกล้สุด — เฉพาะคู่ที่ cosine ≥ 0.7 (โซนเขียว) คำที่ไม่มีเส้น = คำโดดเดี่ยว ไม่ใช่ระบบพัง
fig, ax = plt.subplots(figsize=(13, 9))
THRESHOLD = 0.7
for i in range(len(labels)):
for j in range(i + 1, len(labels)):
if S[i, j] >= THRESHOLD:
ax.plot([P[i, 0], P[j, 0]], [P[i, 1], P[j, 1]], '-',
color='#199e70', alpha=0.55, lw=2.2, zorder=1)
mx, my = (P[i, 0]+P[j, 0])/2, (P[i, 1]+P[j, 1])/2
ax.annotate(f'{S[i, j]:.2f}', (mx, my), fontsize=9, color='#199e70',
ha='center', bbox=dict(boxstyle='round,pad=0.15', fc='white', ec='none', alpha=.8))
for g in WORDS:
idx = [i for i, gg in enumerate(groups) if gg == g]
ax.scatter(P[idx, 0], P[idx, 1], s=140, c=COLORS[g], label=g, zorder=3)
for i, w in enumerate(labels):
ax.annotate(w, (P[i, 0], P[i, 1]), textcoords='offset points', xytext=(0, 12), ha='center', fontsize=12)
ax.legend(fontsize=12); ax.set_xticks([]); ax.set_yticks([])
ax.set_title(f'เส้นเชื่อมเฉพาะคู่ที่ cosine ≥ {THRESHOLD} — "เพื่อนแท้" เท่านั้น', fontsize=15)
plt.tight_layout(); plt.show()
lonely = [labels[i] for i in range(len(labels)) if max(S[i, j] for j in range(len(labels)) if j != i) < THRESHOLD]
print('คำโดดเดี่ยว (ไม่มีเพื่อนถึง 0.7):', ', '.join(lonely))
คำโดดเดี่ยว (ไม่มีเพื่อนถึง 0.7): ต้มยำกุ้ง, ผัดไทย, ส้มตำ, กาแฟลาเต้, ชาเขียว, workshop, สอนนักศึกษา, งานวิจัย, บิตคอยน์, ราคาหุ้น, อัตราดอกเบี้ย, เงินเฟ้อ, vector search, embedding, ปัญญาประดิษฐ์, ฐานข้อมูล
สรุป — สิ่งที่ notebook นี้พิสูจน์¶
- โมเดลจัดกลุ่มเอง — เราไม่เคยบอกชื่อกลุ่ม แต่คำกองตามความหมาย
- ข้ามภาษาได้จริง — cat อยู่กับ แมว (0.781) เพราะ bge-m3 เทรนข้ามภาษา
- คะแนนต้องอ่านเป็น — ต่ำกว่า 0.55 = แทบไม่เกี่ยว แม้จะเป็น "อันดับ 1" ก็ตาม
- ภาพ 2D คือภาพประกอบ — PCA ทิ้งข้อมูลไปมาก ตัวเลข cosine คือความจริง
ไปต่อ¶
- แก้
WORDSเป็นข้อมูลของคุณ (หัวข้อวิจัย, โน้ต, อะไรก็ได้) แล้วรันใหม่ - ต่อยอดเป็น second brain เต็มรูป: หนังสือบทที่ 1-3 (ChromaDB + bge-m3 + filter)
- ทฤษฎีเบื้องหลังทุกขั้น:
deep-technical/86 บท
ประกอบหนังสือ "Second Brain ด้วย Vector Search" · ARRA Oracle Workshop 26 ก.ค. 2026