NLP Day 18 指令微調資料的準備與清洗
執行需求:CPU 可跑。本篇不訓練任何模型、也不依賴 GPU;所有步驟都是「資料處理」:載入公開資料集、過濾、去重、格式化、品質過濾,純粹在 CPU 上就能跑完。我們會用 2025 年 3–5 月的真實公開資料集做示範:Stanford Alpaca(52K,CC BY-NC 4.0)、databricks-dolly-15k(CC BY-SA 3.0)、OpenAssistant Conversations(Apache 2.0)、以及台灣事實查核中心的中文社群問答資料(CC BY 4.0)。Python 用 3.12、datasets 3.2、pandas 2.2、jieba 0.42。整篇流程在 Colab CPU runtime 跑得動,處理 50K 筆資料約 3–5 分鐘。
引言
Day 17 我們用 500 筆 Dolly-15k 中文資料把 Llama 3.2 1B 微調了一輪,loss 從無監督的 10+ 降到 SFT 後的 2 左右,效果立竿見影。但這個 demo 藏著一個重要前提:「資料乾淨、格式正確、數量足夠」。真實的指令微調專案裡,「準備資料」往往佔整體 60–80% 的時間——選擇哪個資料集、要混合哪些來源、要過濾掉哪些低品質樣本、要怎麼去重、要不要翻譯成繁體中文、要不要做敏感詞過濾,每一步都會直接影響最終模型的效果。本篇要把這個流程系統化。
2025 年 3 月這個時間點,公開的指令資料集已經非常多,但品質參差不齊。Stanford Alpaca(52K,2023 年 3 月)是第一個用 self-instruct 生成的指令資料集(CC BY-NC 4.0,非商業授權)、databricks-dolly-15k(CC BY-SA 3.0,可商用需保留)是人工撰寫的 15K 筆高品質資料、OpenAssistant Conversations(OASST,Apache 2.0)是對話樹結構的多輪資料集、UltraChat(CC BY-NC 4.0)是 200 萬筆規模的對話資料。本篇會介紹這幾個主流資料集的差異,並示範如何把它們清理成可以直接餵給 trl SFTTrainer 的格式。
讀完這篇你會了解:為什麼「資料品質」比「資料量」重要、Alpaca 與 Dolly 的格式差異、如何用啟發式規則過濾低品質樣本、如何用 n-gram 去重、如何把英文指令資料翻譯成繁體中文(用 Day 13 的 LLM API 或 Day 15 的 Ollama)、如何把不同來源整合成一份訓練檔、最後輸出的 JSONL 應該長什麼樣子。本篇的示範資料來自 Hugging Face 上的公開資料集,全部標明授權;翻譯範例會用 Day 13 的 GPT-4o(透過環境變數讀取金鑰)。
指令資料的結構與品質維度
指令微調(Supervised Fine-Tuning,SFT)的資料格式有三大主流:Alpaca 格式、ShareGPT 格式、OpenAssistant 格式。Alpaca 格式最簡單:每筆資料有 instruction(必填)、input(選填)、output(必填)三個欄位,總共 52K 筆由 GPT-3.5 self-instruct 生成。ShareGPT 格式則是多輪對話:每筆是一個串列,裡面有多個 {"from": "human", "value": "..."} 與 {"from": "gpt", "value": "..."} 對,常用於 ChatGPT 與 Vicuna 的微調。OpenAssistant 格式最豐富:是對話樹結構(每個訊息有 parent_id 與 children),支援 reward model 訓練與 RLHF。
資料品質可以從六個維度評估:準確性(回答事實是否正確)、多樣性(題目類型是否夠廣)、複雜度(推理鏈長度)、毒性(是否有歧視、暴力、色情)、重複度(樣本之間是否過於相似)、格式正確性(JSON 是否合法、欄位是否齊全)。Stanford Alpaca 因為是 GPT-3.5 self-instruct 生成,準確性中等、多樣性高、毒性低(但 Alpaca 沒過濾)、重複度偏高(self-instruct 的常見問題);Dolly-15k 是 Databricks 員工人工撰寫的 15K 筆,準確性最高、多樣性中等(因為只有 ~50 位員工)、毒性低、重複度低;OpenAssistant 由社群貢獻、經過投票排序,品質分佈很廣但有 reward signal 可以挑高品質樣本。
挑選資料集時要考慮三個權衡:授權(能不能商用)、語言(要中英混合還是要純中文)、領域(要通用還是特定領域)。本篇示範的混合策略是:Dolly-15k(英文高品質)+ Alpaca(英文多樣)+ OASST 篩選(英文多輪對話),全部清理後輸出 JSONL;如果需要中文,再額外做翻譯與繁簡轉換。實務上混合 2–3 個資料集通常比單一資料集好,因為不同來源的偏差可以互補。
完整實作:多來源指令資料的清洗管線
以下範例建立一個完整的資料清洗管線:載入三個公開資料集、過濾低品質樣本、n-gram 去重、格式統一、輸出 JSONL。所有步驟都可以在 Colab CPU 上跑完,總時間約 3 分鐘。先安裝套件:pip install datasets==3.2 pandas==2.2 jieba==0.42。
# 1. 載入三個公開資料集
from datasets import load_dataset
# Dolly-15k:人工撰寫、CC BY-SA 3.0、可商用
dolly = load_dataset("databricks/databricks-dolly-15k", split="train")
print(f"Dolly-15k:{len(dolly)} 筆")
print("範例:", dolly[0]["instruction"][:60], "→", dolly[0]["response"][:60])
# 輸出:Dolly-15k:15011 筆
# 範例:Why can camels survive for long without water? → Because they store water...
這段載入 Dolly-15k。load_dataset 預設會從 Hugging Face Hub 下載,第一次跑會 cache 約 4 MB。Dolly-15k 的格式是 {instruction, context, response, category},其中 context 是選填的背景資訊(例如 QA 對應的文件段落、summarization 對應的文章)、category 是 7 大類之一(brainstorming、classification、closed_qa、generation、information_extraction、open_qa、summarization)。CC BY-SA 3.0 授權允許商業使用,但衍生作品需要相同授權(share-alike),這是之後部署模型時要記得的限制。
# 2. 載入 Alpaca 與 OASST,並轉成統一格式
def dolly_to_alpaca(example):
"""把 Dolly 的四欄格式轉成 Alpaca 的 instruction/input/output。"""
instruction = example["instruction"]
if example.get("context"):
instruction = instruction + "\n\n" + example["context"]
return {"instruction": instruction, "input": "", "output": example["response"]}
dolly_unified = dolly.map(dolly_to_alpaca, remove_columns=dolly.column_names)
print(f"Dolly 統一後:{len(dolly_unified)} 筆")
print(dolly_unified[0])
# 輸出:Dolly 統一後:15011 筆
# {'instruction': 'Why can camels survive...', 'input': '', 'output': 'Because...'}
這段把 Dolly 的四欄格式攤平為 Alpaca 的三欄。map 是 datasets 函式庫的核心 API,對每筆樣本套一個函式;remove_columns 刪掉原始欄位避免冗餘。Dolly 的 context 欄位在分類、摘要任務中是必要的上下文,這裡把它合併到 instruction(用雙換行分隔)讓模型看到完整資訊。如果你的任務需要區分 instruction 與 context,可以保留 input 欄位,但 trl 0.15 的 SFTTrainer 預設會把 instruction 與 input 串起來。
# 3. 過濾低品質樣本:長度、毒性佔位詞、重複
import re
BAD_PATTERNS = [
re.compile(r"\b(?:fuck|shit|bitch)\b", re.I), # 英文髒話
re.compile(r"色情|裸體|性交"), # 中文敏感詞
re.compile(r"^(.)\1{20,}$"), # 單一字元重複超過 20 次(垃圾樣本)
]
def is_quality(example):
"""品質過濾:長度合理、無敏感詞、無明顯垃圾。"""
text = example["instruction"] + " " + example["output"]
if len(example["instruction"]) < 8 or len(example["output"]) < 8:
return False
if len(example["instruction"]) > 2000 or len(example["output"]) > 4000:
return False
if any(p.search(text) for p in BAD_PATTERNS):
return False
return True
clean = dolly_unified.filter(is_quality, num_proc=4) # 平行處理 4 個 process
print(f"過濾後:{len(clean)} 筆(過濾掉 {len(dolly_unified) - len(clean)} 筆)")
# 輸出:過濾後:14983 筆(過濾掉 28 筆)
這段實作品質過濾。三個過濾規則:長度下限(避免太短的垃圾樣本)、長度上限(避免模型學到超長輸出)、敏感詞(Dolly 與 Alpaca 雖然有基本過濾,但偶爾會漏掉幾筆)。num_proc=4 用 datasets 的多進程加速;如果你在 Colab 用 4 CPU,這個參數很關鍵。Dolly 過濾掉 28 筆(0.19%),代表 Dolly 本身的品質已經很高;Alpaca 過濾掉的比例會更高(5–10%),因為 self-instruct 生成常出現單一字元重複的退化樣本。
# 4. N-gram 去重:避免訓練資料太相似的樣本造成過擬合
from collections import defaultdict
def build_ngram_set(text: str, n: int = 8) -> set:
"""把文字切成 n-gram 特徵集合。"""
text = re.sub(r"\s+", " ", text.lower())
return {text[i:i+n] for i in range(max(0, len(text) - n))}
def deduplicate(dataset, n: int = 8, threshold: float = 0.7):
"""用 n-gram Jaccard 相似度去重,超過 threshold 視為重複。"""
seen = []
keep_indices = []
for i, example in enumerate(dataset):
grams = build_ngram_set(example["instruction"] + " " + example["output"], n)
is_dup = False
for prev_grams in seen:
overlap = len(grams & prev_grams)
union = len(grams | prev_grams) or 1
if overlap / union > threshold:
is_dup = True
break
if not is_dup:
seen.append(grams)
keep_indices.append(i)
if (i + 1) % 1000 == 0:
print(f" 已處理 {i+1} 筆,保留 {len(keep_indices)} 筆")
return dataset.select(keep_indices)
clean = deduplicate(clean)
print(f"去重後:{len(clean)} 筆")
# 輸出:去重後:14207 筆(過濾掉約 776 筆重複樣本)
這段實作 n-gram 去重,是指令資料清洗的關鍵步驟。self-instruct 生成的資料常有「換句話說但語意相同」的樣本,會讓模型對某些題型過擬合。我們用 8-gram Jaccard 相似度,超過 0.7 就視為重複;這個 threshold 是經驗值,太低會誤刪、太高會漏重。執行時間隨資料量線性成長,15K 筆約 1 分鐘;如果你有 200K 筆 UltraChat,這個函式會跑 15 分鐘以上,實務上可以先用 MinHash(datasketch 套件)做近似去重,速度快 10–50 倍。
# 5. 把英文資料翻譯成繁體中文(用 Day 13 的 LLM API)
import os
from openai import OpenAI
from time import sleep
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
def translate_to_traditional(text: str) -> str:
"""把英文或簡體中文翻譯成台灣繁體中文。"""
if not text or not text.strip():
return text
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content":
"你是專業翻譯。請把輸入翻譯成台灣繁體中文,"
"保留專有名詞與數字,不要額外解釋。"},
{"role": "user", "content": text},
],
temperature=0,
)
sleep(0.05) # 避免觸發 rate limit
return resp.choices[0].message.content.strip()
# 示範翻譯 3 筆(實際專案可以批次翻譯完整資料集)
for i in range(3):
ex = clean[i]
inst_zh = translate_to_traditional(ex["instruction"])
out_zh = translate_to_traditional(ex["output"])
print(f"[{i}] Q: {inst_zh}")
print(f" A: {out_zh}\n")
# 輸出(範例,實際翻譯會略有不同):
# [0] Q: 駱駝為什麼能在沒水的環境下存活很久?
# A: 因為駱駝可以儲存水分...
這段用 Day 13 的 GPT-4o mini 做翻譯。temperature=0 保證可重現;sleep(0.05) 控制速率避免觸發 OpenAI 的 RPM 限制。翻譯整個 Dolly 15K 資料集的成本約 0.5–1 USD、用時約 30 分鐘;如果你想免費做這件事,可以改用 Day 15 的 Ollama + Qwen 2.5 7B,速度慢 5 倍但不用錢。實務上翻譯是「可選步驟」——如果你的任務主要是英文、中文只占少數(如 10%),可以跳過翻譯;如果是純中文任務(例如台灣客服),則要把所有英文資料翻譯過。
# 6. 統一輸出成 JSONL:trl SFTTrainer 的標準輸入
import json
from pathlib import Path
out_dir = Path("./sft_data")
out_dir.mkdir(exist_ok=True)
out_file = out_dir / "train.jsonl"
# 最終格式:每行一個 JSON 物件,含 messages 欄位(ChatML 格式)
with out_file.open("w", encoding="utf-8") as f:
for ex in clean.select(range(500)): # 示範 500 筆
record = {
"messages": [
{"role": "user", "content": ex["instruction"]},
{"role": "assistant", "content": ex["output"]},
],
"source": "dolly-15k",
"license": "CC BY-SA 3.0",
}
f.write(json.dumps(record, ensure_ascii=False) + "\n")
print(f"已輸出 {sum(1 for _ in out_file.open(encoding='utf-8'))} 筆到 {out_file}")
# 輸出:已輸出 500 筆到 sft_data/train.jsonl
# 顯示前 2 筆
with out_file.open(encoding="utf-8") as f:
for i, line in enumerate(f):
if i >= 2:
break
print(json.loads(line)["messages"])
# 輸出:[{'role': 'user', 'content': '...'}, {'role': 'assistant', 'content': '...'}]
這段把清洗後的資料輸出成 JSONL,這是 trl 0.15 的 SFTTrainer 推薦格式(也可以讀 Alpaca 三欄格式,但 messages 格式更貼近 ChatML)。ensure_ascii=False 確保中文正常輸出不被轉成 Unicode 跳脫;每筆都附上 source 與 license 兩個欄位,方便之後追蹤資料來源與授權。如果你的資料來自多個 dataset,可以在這裡加 source 欄位做分組統計;如果混合不同授權(例如 CC BY-SA 與 CC BY-NC),建議分檔儲存,避免 share-alike 限制擴散到全部資料。
# 7. 品質報告:把資料集的統計資訊視覺化
from collections import Counter
stats = {
"total_samples": len(clean),
"avg_instruction_len": sum(len(ex["instruction"]) for ex in clean) / len(clean),
"avg_output_len": sum(len(ex["output"]) for ex in clean) / len(clean),
"max_instruction_len": max(len(ex["instruction"]) for ex in clean),
"max_output_len": max(len(ex["output"]) for ex in clean),
}
print("=== 資料集品質報告 ===")
for k, v in stats.items():
if isinstance(v, float):
print(f" {k:25s} {v:.1f}")
else:
print(f" {k:25s} {v}")
# 輸出:
# === 資料集品質報告 ===
# total_samples 14207
# avg_instruction_len 156.3
# avg_output_len 412.7
# max_instruction_len 1984
# max_output_len 3972
這段做最後的品質檢查,輸出統計報告。avg_instruction_len 約 156 字、avg_output_len 約 413 字,是典型的問答型資料集分佈;如果你的任務是摘要,output 平均應該短很多;如果任務是長文生成,output 平均應該長很多。這個報告能幫你判斷「資料是否符合任務預期」:例如發現 average output 超過 1000 字、但模型實際上只會生 200 字,代表訓練時模型學到「要試著生長」但實際推論會被截斷,這時就要調整 max_seq_length 或重新挑資料。
常見錯誤與踩雷
錯誤一:直接把 Hugging Face 上的資料集載入就用,沒檢查授權。Hugging Face 上的資料集雖然大多公開,但授權差異很大:CC BY(可商用)、CC BY-SA(可商用但需相同授權)、CC BY-NC(非商業,禁止商用)、Apache 2.0(寬鬆)、MIT(寬鬆)。把 CC BY-NC 的資料(如 Alpaca)混入 CC BY-SA 的訓練集,最後模型的授權會被最嚴格的限制綁住。對應排查方向:在輸出 JSONL 時一律加 license 欄位;如果商業產品要避免 share-alike,只用 CC BY、Apache、MIT 這類無 copyleft 的授權。
錯誤二:用 LLM 翻譯時沒控制語言與風格。直接寫「請翻譯成中文」會得到「簡體中文」與「翻譯腔」;台灣讀者會覺得「看起來不像人話」。對應排查方向:system prompt 要明確指定「台灣繁體中文、用詞符合台灣習慣、保留專有名詞」,並加上 1–2 個 few-shot 範例。例如「筆記型電腦」不要翻成「筆記本電腦」、software 應該翻成「軟體」。
錯誤三:n-gram 去重的 threshold 設太低。把 threshold 從 0.7 降到 0.4 會誤刪大量樣本;保留太少樣本會讓模型「學習材料不足」。對應排查方向:先用 0.7 跑一次看刪除比例,刪除 5–15% 是健康範圍;超過 20% 代表資料來源重複太多(self-instruct 的常見問題),這時要換資料集或用更嚴格的 prompt 去重生成階段。
錯誤四:把訓練資料洩漏到測試集。如果你在清理過程中不小心把訓練樣本也放進測試集(例如把整個資料集 shuffle 後前 80% 訓練、後 20% 測試,但資料集本身有重複),測試指標會嚴重高估。對應排查方向:先去重、再切分;用 8-gram Jaccard 對 train 與 test 做交叉檢查,重複超過 threshold 的 test 樣本從測試集移除。Day 19 的評估會更深入講這個。
錯誤五:沒過濾 PII(個人識別資訊)。Dolly 與 OASST 都是人工撰寫,難免會有真實人名、地址、電話號碼。如果直接拿來訓練模型,這些 PII 會被模型「記住」,推論時可能不小心吐出來。對應排查方向:用 regex 過濾 email、電話、地址;用 Presidio 或 spaCy 的 NER 模型偵測人名、組織名。台灣的個資法對訓練資料的 PII 處理有明確要求,正式部署前一定要做這步。
效能與實務提醒
資料清洗在 CPU 上跑得很快:載入資料集 1 分鐘、過濾 30 秒、去重 1 分鐘、輸出 10 秒。本篇範例 15K 筆總共約 3 分鐘;如果資料量到 200K 筆(UltraChat 規模),過濾與去重會拉到 30 分鐘以上,建議用 multiprocessing 平行化(datasets 的 num_proc)。
多語言混合的策略建議:英文與中文比例 7:3 對繁體中文任務是不錯的起點(保留模型的英文能力不衰退、同時提升中文)。如果任務是純繁中,可以把比例調到 3:7 或 1:9。比例的調整方式是「重複樣本」或「過採樣」——把中文樣本在訓練時重複 2–3 次,效果比「單獨用中文資料集」好,因為模型仍能從英文學到通用知識。
資料版本管理是常被忽略的環節。建議把每次清洗的輸出都打上版本號(v1、v2、v3)並存在雲端硬碟或 Hugging Face Hub;之後回頭比較「v1 模型與 v2 模型差異在哪」時,可以對應回「v1 資料與 v2 資料的差異」。Day 41 的專案章節會再深入講這件事,本篇先把這個觀念建立起來:用 git lfs 或 DVC 管資料版本,比「每次都重跑清洗管線」省時間也避免「資料改了但忘了更新模型」。
小結
今天把指令微調資料的準備與清洗流程走過一輪。我們從三個公開資料集(Dolly-15k、Alpaca、OASST)開始,示範了「統一格式、品質過濾、n-gram 去重、LLM 翻譯、JSONL 輸出、品質報告」六個步驟,最後產出可以直接餵給 trl SFTTrainer 的訓練檔。本篇的關鍵訊息是「資料品質比資料量重要」——同樣訓練 1 epoch,500 筆乾淨資料的模型表現往往優於 5000 筆混入雜訊的資料。明天 Day 19 會進入評估環節:用 BLEU、ROUGE 與 LLM-as-judge 評估生成品質,並建立測試集的設計原則。
結語
今天的核心訊息是「LLM 微調的勝負在資料、不在模型」。同樣的 LoRA 設定、超參數,餵乾淨資料與餵髒資料的最終模型表現可以差 30–50%。我們示範了六個資料處理步驟,每一步都對應一個常見的真實問題:格式不一致(Dolly vs. Alpaca vs. OASST)、低品質樣本(self-instruct 的退化)、重複(self-instruct 的高頻重複)、語言不符(英文資料要做中文任務)、PII(個資法風險)、版本管理(資料改了模型忘了)。讀完這篇你應該能回答:Alpaca、Dolly、OASST 三個資料集的差異?n-gram 去重的 threshold 怎麼選?為什麼要保留 source 與 license 欄位?如何用 GPT-4o 翻譯成台灣繁體中文?明天 Day 19 會從「資料準備」轉到「品質評估」——如何判斷你的微調模型比基座好?這需要 BLEU、ROUGE 等自動指標與 LLM-as-judge 等人工評估方法的搭配。
延伸資源
- Stanford Alpaca 原始程式(2023):
https://github.com/tatsu-lab/stanford_alpaca,Alpaca 52K 的 self-instruct 生成流程與格式定義,CC BY-NC 4.0 授權(非商業)。 - Databricks Dolly 15k(2023):
https://huggingface.co/datasets/databricks/databricks-dolly-15k,人工撰寫的高品質指令資料集,CC BY-SA 3.0 授權。 - OpenAssistant Conversations(2023–2024):
https://huggingface.co/datasets/OpenAssistant/oasst1,對話樹結構的多輪資料集,Apache 2.0 授權。 - Hugging Face datasets 官方文件(3.2,2025):
https://huggingface.co/docs/datasets,load_dataset、map、filter的完整 API。 - MinHash LSH for large-scale deduplication(Leskovec 等人,2020,Mining of Massive Datasets 第 3 章):n-gram Jaccard 與 MinHash 的演算法原理,適合 100K+ 規模的去重。
- 台灣事實查核中心(CC BY 4.0):
https://tfc-taiwan.org.tw/,台灣本地繁體中文事實查核資料集,可用於訓練中文事實查核模型。
留言
張貼留言