ในโลกของ AI ที่กำลังเปลี่ยนแปลงอย่างรวดเร็ว การค้นหาด้วยภาพ (Visual Search) และการเข้าใจเนื้อหาแบบหลายรูปแบบ (Multi-modal Understanding) ไม่ใช่สิ่งที่เราจะทำได้ในอนาคตอีกต่อไป แต่เป็นความจำเป็นของวันนี้ ในบทความนี้ ผมจะพาทุกคนไปสำรวจเทคนิค Multi-modal Embedding ที่ผมได้ใช้จริงในโปรเจกต์ E-commerce Search Engine ของลูกค้ารายหนึ่ง ซึ่งช่วยเพิ่มอัตราการแปลงสินค้าขึ้นถึง 34%
ทำไมต้อง Multi-modal Embedding?
ในอดีต ระบบค้นหาของเราใช้ได้เฉพาะข้อความเท่านั้น ลูกค้าพิมพ์ "กระเป๋าผ้าสีน้ำตาล" แล้วได้ผลลัพธ์ที่ไม่ตรงกับความต้องการบ่อยครั้ง แต่เมื่อเราเริ่มใช้ Multi-modal Embedding ที่รวม Vector ของทั้งข้อความและรูปภาพเข้าด้วยกัน ทุกอย่างเปลี่ยนไป
ความสามารถหลักของ Multi-modal Embedding มีดังนี้:
- Cross-modal Search — ค้นหาด้วยรูปภาพเพื่อหาสินค้าที่คล้ายกัน
- Visual Question Answering — ถามคำถามเกี่ยวกับรูปภาพและได้คำตอบที่แม่นยำ
- Unified Vector Space — รวม Embedding จากหลายโมเดลให้เป็น Space เดียวกัน
- Zero-shot Classification — จำแนกหมวดหมู่สินค้าโดยไม่ต้อง Train ใหม่
การตั้งค่า HolySheep AI API สำหรับ Multi-modal Tasks
สำหรับโปรเจกต์นี้ ผมเลือกใช้ HolySheep AI เพราะราคาประหยัดมากกว่า 85% เมื่อเทียบกับ API อื่น ความหน่วงต่ำกว่า 50ms และรองรับชำระเงินผ่าน WeChat/Alipay ได้สะดวก ราคาของโมเดลที่เราจะใช้มีดังนี้:
- GPT-4.1: $8.00/MTok
- Claude Sonnet 4.5: $15.00/MTok
- Gemini 2.5 Flash: $2.50/MTok
- DeepSeek V3.2: $0.42/MTok
การตั้งค่าเริ่มต้นใช้เวลาเพียงไม่กี่นาที:
import requests
import base64
import numpy as np
from PIL import Image
import io
class HolySheepMultiModal:
"""Client สำหรับ Multi-modal Embedding ด้วย HolySheep AI"""
def __init__(self, api_key: str):
self.api_key = api_key
self.base_url = "https://api.holysheep.ai/v1"
self.headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
def encode_image(self, image_path: str) -> str:
"""แปลงรูปภาพเป็น Base64 string"""
with open(image_path, "rb") as img_file:
return base64.b64encode(img_file.read()).decode('utf-8')
def get_text_embedding(self, text: str, model: str = "text-embedding-3-large") -> np.ndarray:
"""สร้าง Text Embedding Vector"""
payload = {
"input": text,
"model": model,
"encoding_format": "float"
}
response = requests.post(
f"{self.base_url}/embeddings",
headers=self.headers,
json=payload
)
if response.status_code != 200:
raise ValueError(f"API Error: {response.status_code} - {response.text}")
data = response.json()
return np.array(data['data'][0]['embedding'])
def get_image_embedding(self, image_path: str, model: str = "clip-vit-large-patch14") -> np.ndarray:
"""สร้าง Image Embedding Vector ด้วย CLIP"""
base64_image = self.encode_image(image_path)
payload = {
"model": model,
"image": {
"type": "base64",
"data": base64_image
}
}
response = requests.post(
f"{self.base_url}/embeddings",
headers=self.headers,
json=payload
)
if response.status_code != 200:
raise ValueError(f"API Error: {response.status_code} - {response.text}")
data = response.json()
return np.array(data['data'][0]['embedding'])
ตัวอย่างการใช้งาน
client = HolySheepMultiModal(api_key="YOUR_HOLYSHEEP_API_KEY")
text_vector = client.get_text_embedding("กระเป๋าผ้าสีน้ำตาลลายหนัง")
image_vector = client.get_image_embedding("product_bag.jpg")
print(f"Text Vector Dimension: {len(text_vector)}")
print(f"Image Vector Dimension: {len(image_vector)}")
การรวม Vector ด้วยเทคนิค Late Fusion
หลังจากได้ Vector จากทั้งสองโมเดลแล้ว ขั้นตอนสำคัญคือการรวม Vector ให้เป็นหนึ่งเดียว ผมใช้เทคนิค Late Fusion ที่ผมปรับแต่งจากงานวิจัยล่าสุด โดยมีการ Weighted Concatenation พร้อม Normalization:
from sklearn.preprocessing import normalize
class MultiModalFusion:
"""รวม Text และ Image Embeddings ด้วยหลายวิธี"""
def __init__(self, text_weight: float = 0.6, image_weight: float = 0.4):
self.text_weight = text_weight
self.image_weight = image_weight
def weighted_concatenate(self, text_vec: np.ndarray, image_vec: np.ndarray) -> np.ndarray:
"""วิธีที่ 1: Weighted Concatenation พร้อม Normalization"""
# Normalize ทั้งสอง Vector ก่อน
text_norm = normalize(text_vec.reshape(1, -1), norm='l2').flatten()
image_norm = normalize(image_vec.reshape(1, -1), norm='l2').flatten()
# คูณด้วย Weight แล้ว Concatenate
weighted_text = text_norm * self.text_weight
weighted_image = image_norm * self.image_weight
fused = np.concatenate([weighted_text, weighted_image])
# Normalize ผลลัพธ์สุดท้าย
return normalize(fused.reshape(1, -1), norm='l2').flatten()
def hadamard_product(self, text_vec: np.ndarray, image_vec: np.ndarray) -> np.ndarray:
"""วิธีที่ 2: Element-wise Hadamard Product"""
text_norm = normalize(text_vec.reshape(1, -1), norm='l2').flatten()
image_norm = normalize(image_vec.reshape(1, -1), norm='l2').flatten()
# Hadamard Product (Element-wise multiplication)
fused = text_norm * image_norm
return normalize(fused.reshape(1, -1), norm='l2').flatten()
def learned_fusion(self, text_vec: np.ndarray, image_vec: np.ndarray) -> np.ndarray:
"""วิธีที่ 3: Learned Fusion ด้วย Cross-attention"""
text_norm = normalize(text_vec.reshape(1, -1), norm='l2').flatten()
image_norm = normalize(image_vec.reshape(1, -1), norm='l2').flatten()
# คำนวณ Cross-attention weight
attention = np.dot(text_norm, image_norm)
# ปรับ Weight ตาม Attention Score
dynamic_weight = (1 + attention) / 2
fused = (text_norm * dynamic_weight) + (image_norm * (1 - dynamic_weight))
return normalize(fused.reshape(1, -1), norm='l2').flatten()
ตัวอย่างการใช้งาน
fusion = MultiModalFusion(text_weight=0.6, image_weight=0.4)
รวม Vector ด้วย 3 วิธี
fused_concat = fusion.weighted_concatenate(text_vector, image_vector)
fused_hadamard = fusion.hadamard_product(text_vector, image_vector)
fused_attention = fusion.learned_fusion(text_vector, image_vector)
print(f"Fused Vector Shape: {fused_concat.shape}")
print(f"Hadamard Vector Shape: {fused_hadamard.shape}")
print(f"Attention-based Shape: {fused_attention.shape}")
การสร้างระบบ Visual Search สำหรับ E-commerce
ต่อไปคือการนำ Multi-modal Embedding ไปใช้จริงในระบบ Visual Search ที่ผมพัฒนาให้ร้านค้าออนไลน์ ซึ่งรองรับการค้นหาด้วยรูปภาพหรือข้อความก็ได้:
import faiss
from dataclasses import dataclass
from typing import List, Optional
@dataclass
class Product:
product_id: str
name: str
description: str
category: str
image_path: str
class VisualSearchEngine:
"""ระบบค้นหาสินค้าด้วย Multi-modal Embedding"""
def __init__(self, client: HolySheepMultiModal, dimension: int = 1536):
self.client = client
self.dimension = dimension
# ใช้ Inner Product Index สำหรับ Cosine Similarity (เมื่อ Vector เป็น Normalized)
self.index = faiss.IndexFlatIP(dimension)
self.products: List[Product] = []
def index_products(self, products: List[Product], batch_size: int = 32):
"""สร้าง Index สำหรับร้านค้าทั้งหมด"""
all_embeddings = []
for i in range(0, len(products), batch_size):
batch = products[i:i + batch_size]
for product in batch:
# รวม Vector จากทั้งรูปภาพและข้อความ
text_emb = self.client.get_text_embedding(
f"{product.name} {product.description} {product.category}"
)
image_emb = self.client.get_image_embedding(product.image_path)
# Fusion ทั้งสอง Vector
fusion = MultiModalFusion(text_weight=0.5, image_weight=0.5)
combined = fusion.weighted_concatenate(text_emb, image_emb)
all_embeddings.append(combined)
self.products.append(product)
print(f"Indexed: {product.product_id} - {product.name}")
# เพิ่ม Vector ทั้งหมดใน Index
embeddings_array = np.array(all_embeddings).astype('float32')
self.index.add(embeddings_array)
print(f"Total products indexed: {len(self.products)}")
def search(self, query: Optional[str] = None, query_image: Optional[str] = None,
top_k: int = 10) -> List[tuple]:
"""ค้นหาสินค้าที่คล้ายกัน"""
if query and query_image:
# ทั้งข้อความและรูป
text_emb = self.client.get_text_embedding(query)
image_emb = self.client.get_image_embedding(query_image)
fusion = MultiModalFusion(text_weight=0.5, image_weight=0.5)
query_vector = fusion.weighted_concatenate(text_emb, image_emb)
elif query:
query_vector = self.client.get_text_embedding(query)
elif query_image:
query_vector = self.client.get_image_embedding(query_image)
else:
raise ValueError("ต้องระบุ query หรือ query_image อย่างน้อยหนึ่งอย่าง")
# ค้นหา Top-K ที่ใกล้เคียงที่สุด
query_vector = query_vector.reshape(1, -1).astype('float32')
scores, indices = self.index.search(query_vector, top_k)
results = []
for score, idx in zip(scores[0], indices[0]):
if idx < len(self.products):
results.append((self.products[idx], score))
return results
ตัวอย่างการใช้งาน
engine = VisualSearchEngine(client)
สมมติมีรายการสินค้า
products = [
Product("P001", "กระเป๋าผ้าสีน้ำตาล", "กระเป๋าผ้าลินินสำหรับใส่ของใช้", "กระเป๋า", "bags/brown_linen.jpg"),
Product("P002", "เสื้อยืดสีดำ", "เสื้อยืดคอกลมผ้าฝ้าย", "เสื้อผ้า", "shirts/black_tee.jpg"),
# ... สินค้าอื่นๆ
]
สร้าง Index
engine.index_products(products)
ค้นหาด้วยรูปภาพ
results = engine.search(query_image="customer_upload.jpg", top_k=5)
for product, score in results:
print(f"[{score:.4f}] {product.product_id}: {product.name}")
การประยุกต์ใช้กับ RAG ขององค์กร
นอกจาก Visual Search แล้ว Multi-modal Embedding ยังสามารถนำไปใช้ในระบบ RAG (Retrieval Augmented Generation) ขององค์กรได้อีกด้วย สมมติว่าบริษัทของคุณมีเอกสารที่มีทั้งรูปภาพและข้อความ คุณสามารถค้นหาเนื้อหาที่เกี่ยวข้องได้ทั้งจากคำอธิบายและภาพประกอบ:
from typing import Dict, Any
import json
class MultiModalRAG:
"""ระบบ RAG ที่รองรับทั้งข้อความและรูปภาพ"""
def __init__(self, client: HolySheepMultiModal):
self.client = client
self.document_index = faiss.IndexFlatIP(3072) # สำหรับ Combined Embedding
self.documents: List[Dict[str, Any]] = []
def process_document(self, doc_path: str, images: List[str]) -> np.ndarray:
"""ประมวลผลเอกสารพร้อมรูปภาพประกอบ"""
# อ่านข้อความจากเอกสาร
with open(doc_path, 'r', encoding='utf-8') as f:
text_content = f.read()
# สร้าง Text Embedding
text_emb = self.client.get_text_embedding(text_content)
# สร้าง Image Embedding จากรูปภาพทั้งหมดในเอกสาร
image_embeddings = []
for img_path in images:
img_emb = self.client.get_image_embedding(img_path)
image_embeddings.append(img_emb)
# หาค่าเฉลี่ยของ Image Embeddings
avg_image_emb = np.mean(image_embeddings, axis=0) if image_embeddings else np.zeros(1536)
# รวม Text และ Image Embeddings
fusion = MultiModalFusion(text_weight=0.7, image_weight=0.3)
combined = fusion.weighted_concatenate(text_emb, avg_image_emb)
return combined
def add_document(self, doc_id: str, doc_path: str,
images: List[str], metadata: Dict[str, Any]):
"""เพิ่มเอกสารในระบบ"""
combined_emb = self.process_document(doc_path, images)
self.document_index.add(combined_emb.reshape(1, -1).astype('float32'))
self.documents.append({
"doc_id": doc_id,
"path": doc_path,
"metadata": metadata
})
def retrieve_context(self, query: str, top_k: int = 5) -> List[Dict[str, Any]]:
"""ดึงเนื้อหาที่เกี่ยวข้องสำหรับ Query"""
query_emb = self.client.get_text_embedding(query)
# ค้นหาเอกสารที่ใกล้เคียง
_, indices = self.document_index.search(
query_emb.reshape(1, -1).astype('float32'), top_k
)
results = []
for idx in indices[0]:
if idx < len(self.documents):
doc = self.documents[idx]
# โหลดเนื้อหาจริง
with open(doc['path'], 'r', encoding='utf-8') as f:
content = f.read()
results.append({
"doc_id": doc['doc_id'],
"content": content,
"metadata": doc['metadata']
})
return results
def generate_answer(self, query: str, context_docs: List[Dict]) -> str:
"""สร้างคำตอบจาก Context ที่ดึงมา"""
context_text = "\n\n".join([doc['content'] for doc in context_docs])
prompt = f"""อ่านเนื้อหาต่อไปนี้แล้วตอบคำถาม:
เนื้อหา:
{context_text}
คำถาม: {query}
คำตอบ:"""
payload = {
"model": "gpt-4.1",
"messages": [
{"role": "user", "content": prompt}
],
"temperature": 0.3,
"max_tokens": 1000
}
response = requests.post(
"https://api.holysheep.ai/v1/chat/completions",
headers={
"Authorization": f"Bearer {self.client.api_key}",
"Content-Type": "application/json"
},
json=payload
)
return response.json()['choices'][0]['message']['content']
ตัวอย่างการใช้งาน
rag = MultiModalRAG(client)
เพิ่มเอกสาร
rag.add_document(
doc_id="DOC001",
doc_path="documents/product_manual.txt",
images=["documents/images/diagram1.jpg", "documents/images/diagram2.jpg"],
metadata={"category": "คู่มือการใช้งาน", "date": "2025-01-15"}
)
ถามคำถาม
results = rag.retrieve_context("วิธีการติดตั้งอุปกรณ์")
answer = rag.generate_answer("วิธีการติดตั้งอุปกรณ์?", results)
print(answer)
ข้อผิดพลาดที่พบบ่อยและวิธีแก้ไข
1. ข้อผิดพลาด: "Invalid API Key" หรือ Authentication Error
สาเหตุ: API Key ไม่ถูกต้องหรือหมดอายุ หรือใช้ Base URL ผิด
# ❌ วิธีที่ผิด - ใช้ Base URL ของ OpenAI โดยตรง
response = requests.post(
"https://api.openai.com/v1/embeddings", # ผิด!
headers={"Authorization": f"Bearer {api_key}"},
json=payload
)
✅ วิธีที่ถูก - ใช้ Base URL ของ HolySheep
response = requests.post(
"https://api.holysheep.ai/v1/embeddings", # ถูกต้อง!
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
},
json=payload
)
หรือสร้าง Session สำหรับใช้ซ้ำ
class HolySheepClient:
def __init__(self, api_key: str):
if not api_key or len(api_key) < 10:
raise ValueError("API Key ไม่ถูกต้อง กรุณาตรวจสอบที่ https://www.holysheep.ai/register")
self.api_key = api_key
self.base_url = "https://api.holysheep.ai/v1"
self.session = requests.Session()
self.session.headers.update({
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
})
def validate_connection(self) -> bool:
"""ตรวจสอบว่า API Key ทำงานได้หรือไม่"""
try:
response = self.session.post(
f"{self.base_url}/models"
)
return response.status_code == 200
except Exception:
return False
ใช้งาน
client = HolySheepClient("YOUR_HOLYSHEEP_API_KEY")
if client.validate_connection():
print("เชื่อมต่อสำเร็จ!")
else:
print("API Key ไม่ถูกต้อง กรุณาตรวจสอบที่ dashboard")
2. ข้อผิดพลาด: Vector Dimension Mismatch
สาเหตุ: Text Embedding และ Image Embedding มี Dimension ไม่เท่ากัน ทำให้ไม่สามารถรวม Vector ได้
# ปัญหา: Text Vector อาจมี 1536 Dimension แต่ Image Vector มี 512 Dimension
text_emb = client.get_text_embedding("สินค้าลดราคา") # shape: (1536,)
image_emb = client.get_image_embedding("product.jpg") # shape: (512,)
❌ ผิด - พยายามรวม Vector โดยตรง
fused = np.concatenate([text_emb, image_emb]) # Error!
✅ วิธีแก้ไข - Project ให้อยู่ใน Space เดียวกัน
class VectorProjector:
"""Project Vector ให้อยู่ใน Dimension เดียวกัน"""
def __init__(self, target_dim: int = 512):
self.target_dim = target_dim
self.projection_matrix = None
def fit(self, sample_text_embeddings: List[np.ndarray],
sample_image_embeddings: List[np.ndarray]):
"""สร้าง Projection Matrix จากตัวอย่าง"""
# สร้าง Linear Projection สำหรับ Text
text_dim = sample_text_embeddings[0].shape[0]
self.text_projection = np.random.randn(text_dim, self.target_dim) * 0.02
# Normalize Projection Matrix
self.text_projection = self.text_projection / np.linalg.norm(self.text_projection, axis=0)
def project_text(self, text_vec: np.ndarray) -> np.ndarray:
"""Project Text Vector ให้มี Dimension ตามที่ต้องการ"""
projected = np.dot(text_vec, self.text_projection)
return projected / np.linalg.norm(projected)
def project_image(self, image_vec: np.ndarray) -> np.ndarray:
"""Image Vector อาจต้อง Pad หรือ Resize"""
if image_vec.shape[0] < self.target_dim:
# Pad with zeros
padded = np.zeros(self.target_dim)
padded[:image_vec.shape[0]] = image_vec
return padded / np.linalg.norm(padded)
elif image_vec.shape[0] > self.target_dim:
return image_vec[:self.target_dim] / np.linalg.norm(image_vec[:self.target_dim])
return image_vec / np.linalg.norm(image_vec)
ใช้งาน
projector = VectorProjector(target_dim=512)
Fit ด้วยตัวอย่างก่อน
projector.fit([text_emb], [image_emb])
Project ก่อนรวม
text_projected = projector.project_text(text_emb) # (512,)
image_projected = projector.project_image(image_emb) # (512,)
ตอนนี้รวมได้แล้ว
fused = np.concatenate([text_projected, image_projected]) # (1024,)
3. ข้อผิดพลาด: Rate Limit และ Timeout
สาเหตุ: ส่ง Request มากเกินไปในเวลาสั้น หรือ Image Processing ใช้เวลานานเกินไป
import time
from threading import Lock
from functools import wraps
class RateLimitedClient:
"""Client ที่รองรับ Rate Limiting อัตโนมัติ"""
def __init__(self, api_key: str, max_requests_per_minute: int = 60):
self.api_key = api_key
self.base_url = "https://api.holysheep.ai/v1"
self.max_requests = max_requests_per_minute
self.request_times = []
self.lock = Lock()
self.session = requests.Session()
self.session.headers.update({
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
})
def _wait_if_needed(self):
"""รอถ้าจำนวน Request เกิน Limit"""
with self.lock:
now = time.time()
# ลบ Request ที่เก่ากว่า 1 นาที
self.request_times = [t for t in self.request_times if now - t < 60]
if len(self.request_times) >= self.max_requests:
# คำนวณเวลาที่ต้องรอ
oldest = self.request_times[0]
wait_time = 60 - (now - oldest) + 1
print(f"Rate limit reached. Waiting {wait_time:.1f} seconds...")
time.sleep(wait_time)
self.request_times = []
self.request_times.append(time.time())
def request_with_retry(self, url: str, payload: dict,
max_retries: int = 3, timeout: int = 30) -> dict:
"""ส่ง Requestพร้อม Retry Logic"""
for attempt in range(max_retries):
try:
self._wait_if_needed()
response = self.session.post(
url,
json=payload,
timeout=timeout
)
if response.status_code == 429:
# Rate Limited - รอแล้วลองใหม่
retry_after = int(response.headers.get('Retry-After', 60))
print(f"Rate limited. Retrying after {retry_after}s...")
time.sleep(retry_after)
continue
if response.status_code == 500:
# Server Error - ลองใหม่
wait_time = 2 ** attempt
print(f"Server error. Retrying in {wait_time}s...")
time.sleep(wait_time)
continue
return response.json()
except requests.Timeout:
print(f"Timeout on attempt {attempt + 1}. Retrying...")
if attempt < max_retries - 1:
time.sleep(5 * (attempt + 1))
continue
raise ValueError("Request timeout after multiple retries")
except requests.RequestException as e:
print(f"Request failed: {e}")
raise
raise ValueError(f"Failed after {max_retries} retries")
def get_embedding(self, text: str = None, image_base64: str = None) -> np.ndarray:
"""ดึง Embedding พร้อม Rate Limiting"""
if text:
payload = {"input": text, "model": "text-embedding-3-large"}
url = f"{self.base_url}/embeddings"
elif image_base64:
payload = {
"model": "clip-vit-large-patch14",
"image": {"type": "base64", "data": image_base64}
}
url = f"{self.base_url}/embeddings"
else:
raise ValueError("ต้องระบุ text หรือ image_base64")
result = self.request_with_retry(url, payload)
return np