iT邦幫忙

2025 iThome 鐵人賽

DAY 21
0
佛心分享-IT 人自學之術

學習 LLM系列 第 21

Day21 把不同資料測試一次

  • 分享至 

  • xImage
  •  

把其他類型文件拿來做 chunk → embeddings → 加入 index → 做檢索
chunk:把長文章切成多個片段(chunk),通常會設定最大長度 + overlap(重疊),以便檢索時能回傳相關段落而不是整篇文件
metadata:每個 chunk 要記 doc_id, chunk_id, start, end, source 等,方便回溯原文與顯示來源
embedding:把每個 chunk 用 sentence-transformers 轉成向量(numpy float32)
index:把向量加到 FAISS 或 Chroma檢索:輸入 query → encode → search top-k → 顯示 chunk 與來源

chunk 策略
句子/段落為單位(適合新聞、筆記):用標點或
換行切分(中文用「。!?;\n」等)
優點:語意完整、不易切斷句子
缺點:句子過長時需再細分
固定字元長度(char-based):每 chunk 固定 N 字元,並用 overlap
優點:簡單、可控長度
缺點:可能切斷句子
Token-based(最精準):以 tokenizer 的 token 數當長度單位(推薦用於要送給 LLM 的情況)
優點:能精準控制 token 長度(對 prompt token limit 有幫助)
缺點:需 tokenizer 支援(稍微複雜)
實作 :
建立 sample news 文本 → 示範三種 chunking → 建 chunks dataframe → 產生 embeddings → 建 FAISS index → 檢索 demo
句子法切出 1 個 chunk(示例)
CH 0 171 chars: 國際新聞 — 2025年9月28日,台北。 今天下午,市府宣布將在下個月啟動新的公共自行車專案, 此專案預計在市區增設 200 個停車樁,並改善既有的共享單車管理系統。 市長表示,此計畫 可望降低 物流與交通壅塞,並促進綠色出行。 居民代表

Token-based 切出 1 個 chunk(示例)
TK 0 168 chars: 國際新聞 — 2025年9月28日,台北。今天下午,市府宣布將在下個月啟動新的公共自行車專案, 此專案預計在市區增設 200 個停車樁,並改善既有的共享單車管理系統。市長表示,此計畫 可望降低 物流與交通壅塞,並促進綠色出行。居民代表則反映

# ========================
# 安裝必要套件(Colab)
# ========================
!pip install -q sentence-transformers faiss-cpu chromadb


# ========================
# 匯入套件
# ========================
import re, os, json
import numpy as np
import pandas as pd
from sentence_transformers import SentenceTransformer
from transformers import AutoTokenizer
import faiss
import chromadb
from chromadb import PersistentClient


# ========================
# 範例:一篇短新聞(你可以改成自己的文章或上傳 PDF 內容)
# ========================
news = """
國際新聞 — 2025年9月28日,台北。今天下午,市府宣布將在下個月啟動新的公共自行車專案,
此專案預計在市區增設 200 個停車樁,並改善既有的共享單車管理系統。市長表示,此計畫
可望降低 物流與交通壅塞,並促進綠色出行。居民代表則反映,希望能同步優化夜間照明與安全監控。
專案預算預估為三千萬元,若取得中央補助將能擴大到周邊區域。
"""


# 你也可以把一長段的「筆記」或 PDF 文字放在 `news` 變數裡(先把 PDF 轉文字)
print("文章長度(字元):", len(news))
print(news[:300])


# ========================
# Chunking 函式:句子/段落(中文標點)、char-based、token-based
# ========================
# 1) 句子/段落切分(以中文句號、問號、驚嘆號或換行)
def chunk_by_sentence(text, max_chars=300, overlap_chars=50):
    # 先用標點分句(簡單版)
    sents = re.split(r'(?<=。|!|\!|?|\?|\n)', text)
    sents = [s.strip() for s in sents if s.strip()]
    chunks = []
    cur = ""
    for s in sents:
        if len(cur) + len(s) <= max_chars:
            cur = (cur + " " + s).strip() if cur else s
        else:
            if cur:
                chunks.append(cur)
            # 如果這個句子本身超過 max_chars,則直接按 char 切
            if len(s) > max_chars:
                # 把長句用 char 切並可能帶 overlap
                pieces = chunk_by_chars(s, max_chars, overlap_chars)
                chunks.extend(pieces)
                cur = ""
            else:
                cur = s
    if cur:
        chunks.append(cur)
    # 加 overlap(用簡單方式:把前一 chunk 的 tail 再加入下一個 chunk 開頭)
    if overlap_chars > 0:
        new_chunks = []
        for i, c in enumerate(chunks):
            if i == 0:
                new_chunks.append(c)
            else:
                prev = new_chunks[-1]
                tail = prev[-overlap_chars:] if len(prev) > overlap_chars else prev
                new_chunks.append(tail + " " + c)
        chunks = new_chunks
    return chunks


# 2) char-based chunking(固定字數 + overlap)
def chunk_by_chars(text, max_chars=300, overlap_chars=50):
    chunks = []
    i = 0
    N = len(text)
    while i < N:
        start = max(0, i - overlap_chars) if i != 0 else 0
        chunk = text[start: i + max_chars]
        chunks.append(chunk.strip())
        i += max_chars
    return chunks


# 3) token-based chunking(使用模型 tokenizer 計 token 數)
def chunk_by_tokens(text, tokenizer, max_tokens=200, overlap_tokens=40):
    # tokenizer 是 transformers AutoTokenizer(use_fast=True 建議)
    tokens = tokenizer.encode(text, add_special_tokens=False)
    chunks = []
    i = 0
    N = len(tokens)
    while i < N:
        start = max(0, i - overlap_tokens) if i != 0 else 0
        sub = tokens[start: i + max_tokens]
        # decode back to string
        chunk_text = tokenizer.decode(sub, skip_special_tokens=True)
        chunks.append(chunk_text.strip())
        i += max_tokens
    return chunks


# ========================
# 把文章切成 chunks 並產生 metadata(示範使用 sentence-based 與 token-based)
# ========================
# 載入 tokenizer(跟你 later 會用的 embedding model 對齊)
tokenizer = AutoTokenizer.from_pretrained("sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2", use_fast=True)
embed_model = SentenceTransformer("sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")


# sentence-based chunks
sent_chunks = chunk_by_sentence(news, max_chars=220, overlap_chars=50)
print(f"句子法切出 {len(sent_chunks)} 個 chunk(示例)")
for i,c in enumerate(sent_chunks[:3]): print("CH", i, len(c), "chars:", c[:120].replace("\n"," "))


# token-based chunks
token_chunks = chunk_by_tokens(news, tokenizer, max_tokens=120, overlap_tokens=20)
print(f"\nToken-based 切出 {len(token_chunks)} 個 chunk(示例)")
for i,c in enumerate(token_chunks[:3]): print("TK", i, len(c), "chars:", c[:120].replace("\n"," "))


# 我們選擇使用 token-based(較穩定於 LLM token limit),同時保留 metadata
chunks = token_chunks  # 或 sent_chunks / chunk_by_chars(...)
doc_id = "news_20250928_01"


chunk_records = []
for i, text_chunk in enumerate(chunks):
    start = None
    end = None
    # 簡單儲存 start/end as char offsets(可選擇更精確的方法)
    # 這裡用 find 來標記(若文章有重複片段,find 會回傳第一個 index — 若要絕對準確可改用 token offsets)
    try:
        start = news.index(text_chunk)
        end = start + len(text_chunk)
    except ValueError:
        start = None
        end = None
    chunk_records.append({
        "doc_id": doc_id,
        "chunk_id": f"{doc_id}_c{i}",
        "text": text_chunk,
        "start_char": start,
        "end_char": end,
        "source": "sample_news"
    })


df_chunks = pd.DataFrame(chunk_records)
print("\nChunks dataframe 頂端:")
display(df_chunks.head())


# ========================
# 產生 embeddings(batch)
# ========================
texts = df_chunks["text"].tolist()
print("要 embed 的 chunk 數:", len(texts))
embeddings = embed_model.encode(texts, convert_to_numpy=True, show_progress_bar=True).astype("float32")
print("embeddings shape:", embeddings.shape)


# 儲存 chunks + embeddings(備份)
df_chunks.to_csv("news_chunks.csv", index=False, encoding="utf-8-sig")
np.save("news_chunks_embeddings.npy", embeddings)
print("已儲存 news_chunks.csv 與 news_chunks_embeddings.npy")


# ========================
# 建 FAISS index 並加入向量(cosine via normalize + IndexFlatIP)
# ========================
d = embeddings.shape[1]
faiss.normalize_L2(embeddings)  # normalize -> inner product 對應 cosine
index = faiss.IndexFlatIP(d)
index.add(embeddings)
print("FAISS index ntotal:", index.ntotal)


# ========================
# 檢索函式(FAISS)
# ========================
def faiss_search_chunks(query, k=3):
    q_emb = embed_model.encode([query], convert_to_numpy=True).astype("float32")
    faiss.normalize_L2(q_emb)
    D, I = index.search(q_emb, k)
    results = []
    for score, idx in zip(D[0], I[0]):
        rec = df_chunks.iloc[int(idx)].to_dict()
        rec["score"] = float(score)
        results.append(rec)
    return results


# ========================
# 測試檢索
# ========================
queries = [
    "市府要增設多少個停車樁?",
    "如果商品有瑕疵要怎麼辦?",
    "這個專案會改善什麼?"
]


for q in queries:
    print("\n>>> Query:", q)
    res = faiss_search_chunks(q, k=3)
    for r in res:
        print("score:", round(r['score'],3), " chunk_id:", r['chunk_id'], " text:", r['text'][:120].replace("\n"," "))

https://ithelp.ithome.com.tw/upload/images/20251005/20169173NlBOMGKFPz.jpg
Chunks dataframe 頂端:

doc_id
chunk_id
text
start_char
end_char
source
0
news_20250928_01
news_20250928_01_c0
國際新聞 — 2025年9月28日,台北。今天下午,市府宣布將在下個月啟動新的公共自行車專案...
None
None
sample_news

要 embed 的 chunk 數: 1

Batches: 100%
 1/1 [00:00<00:00,  3.97it/s]
embeddings shape: (1, 384)
已儲存 news_chunks.csv 與 news_chunks_embeddings.npy
FAISS index ntotal: 1

Query: 市府要增設多少個停車樁?
score: 0.579 chunk_id: news_20250928_01_c0 text: 國際新聞 — 2025年9月28日,台北。今天下午,市府宣布將在下個月啟動新的公共自行車專案, 此專案預計在市區增設 200 個停車樁,並改善既有的共享單車管理系統。市長表示,此計畫 可望降低 物流與交通壅塞,並促進綠色出行。居民代表則反映
score: -3.4028234663852886e+38 chunk_id: news_20250928_01_c0 text: 國際新聞 — 2025年9月28日,台北。今天下午,市府宣布將在下個月啟動新的公共自行車專案, 此專案預計在市區增設 200 個停車樁,並改善既有的共享單車管理系統。市長表示,此計畫 可望降低 物流與交通壅塞,並促進綠色出行。居民代表則反映
score: -3.4028234663852886e+38 chunk_id: news_20250928_01_c0 text: 國際新聞 — 2025年9月28日,台北。今天下午,市府宣布將在下個月啟動新的公共自行車專案, 此專案預計在市區增設 200 個停車樁,並改善既有的共享單車管理系統。市長表示,此計畫 可望降低 物流與交通壅塞,並促進綠色出行。居民代表則反映

Query: 如果商品有瑕疵要怎麼辦?
score: 0.061 chunk_id: news_20250928_01_c0 text: 國際新聞 — 2025年9月28日,台北。今天下午,市府宣布將在下個月啟動新的公共自行車專案, 此專案預計在市區增設 200 個停車樁,並改善既有的共享單車管理系統。市長表示,此計畫 可望降低 物流與交通壅塞,並促進綠色出行。居民代表則反映
score: -3.4028234663852886e+38 chunk_id: news_20250928_01_c0 text: 國際新聞 — 2025年9月28日,台北。今天下午,市府宣布將在下個月啟動新的公共自行車專案, 此專案預計在市區增設 200 個停車樁,並改善既有的共享單車管理系統。市長表示,此計畫 可望降低 物流與交通壅塞,並促進綠色出行。居民代表則反映
score: -3.4028234663852886e+38 chunk_id: news_20250928_01_c0 text: 國際新聞 — 2025年9月28日,台北。今天下午,市府宣布將在下個月啟動新的公共自行車專案, 此專案預計在市區增設 200 個停車樁,並改善既有的共享單車管理系統。市長表示,此計畫 可望降低 物流與交通壅塞,並促進綠色出行。居民代表則反映

Query: 這個專案會改善什麼?
score: 0.392 chunk_id: news_20250928_01_c0 text: 國際新聞 — 2025年9月28日,台北。今天下午,市府宣布將在下個月啟動新的公共自行車專案, 此專案預計在市區增設 200 個停車樁,並改善既有的共享單車管理系統。市長表示,此計畫 可望降低 物流與交通壅塞,並促進綠色出行。居民代表則反映
score: -3.4028234663852886e+38 chunk_id: news_20250928_01_c0 text: 國際新聞 — 2025年9月28日,台北。今天下午,市府宣布將在下個月啟動新的公共自行車專案, 此專案預計在市區增設 200 個停車樁,並改善既有的共享單車管理系統。市長表示,此計畫 可望降低 物流與交通壅塞,並促進綠色出行。居民代表則反映
score: -3.4028234663852886e+38 chunk_id: news_20250928_01_c0 text: 國際新聞 — 2025年9月28日,台北。今天下午,市府宣布將在下個月啟動新的公共自行車專案, 此專案預計在市區增設 200 個停車樁,並改善既有的共享單車管理系統。市長表示,此計畫 可望降低 物流與交通壅塞,並促進綠色出行。居民代表則反映


上一篇
Day20 做成 CLI 版本
系列文
學習 LLM21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言