雲端 Mac 的 MLX 微調資料驗證:格式與集合洩漏
準備在雲端 Mac 上執行 MLX 微調時,先別急著啟動訓練。上傳訓練集與驗證集後,最值得先做的是資料驗收:某一行不是完整的 JSON、回答為空,或同一段對話同時出現在兩個集合,都可能讓一次實驗失去比較價值。以下使用 Python 標準函式庫建立可重複執行的檢查流程,不必下載模型,也不依賴訓練環境。
先訂好輸入規格,別讓腳本猜格式
本文處理純文字對話,每行一個物件,且只包含 messages 欄位;每則訊息也只能包含 role、content。開頭允許有一則 system,之後必須依 user、assistant 交替排列,最後以回答結束。多模態內容、工具呼叫,以及連續出現相同角色的訊息,都不在這次的規格範圍內。
這是一套專案驗收規則,不代表所有 MLX 訓練工具都只接受這種結構。執行前應核對目前工具的資料輸入規格;若來源資料使用其他欄位,請先寫好明確的轉換步驟,不要讓檢查器默默丟棄欄位。
{"messages":[{"role":"user","content":"解释训练集与验证集的区别。"},{"role":"assistant","content":"训练集用于更新参数,验证集用于观察未参与更新的数据表现。"}]}
將兩個集合儲存為 train.jsonl、valid.jsonl,使用不含 BOM 的 UTF-8 編碼。空白行視為錯誤;檔案末尾的最後一個換行符號不算額外空白行。原始檔案以唯讀方式保存,修訂版本另存新檔,方便追查資料變更。
用標準函式庫逐行攔截格式問題
先執行 python3 --version,確認直譯器可用,再將以下程式碼儲存為 check_dataset.py。單行上限設為 256 KiB,這是本專案防範異常輸入的門檻,不是模型的上下文容量。腳本以二進位模式限制每次讀取的長度,避免先將超長行完整載入記憶體。
import hashlib
import json
import sys
LIMIT = 256 * 1024
seen = {}
def unique_object(pairs):
result = {}
for key, value in pairs:
if key in result:
raise ValueError("duplicate object key")
result[key] = value
return result
def reject_constant(value):
raise ValueError("nonstandard JSON constant")
def validate(obj):
if not isinstance(obj, dict) or set(obj) != {"messages"}:
raise ValueError("expected messages-only object")
messages = obj["messages"]
if not isinstance(messages, list):
raise ValueError("messages must be an array")
roles = []
for message in messages:
if not isinstance(message, dict):
raise ValueError("message must be an object")
if set(message) != {"role", "content"}:
raise ValueError("unexpected message fields")
role = message["role"]
content = message["content"]
if not isinstance(role, str):
raise ValueError("role must be text")
if not isinstance(content, str) or not content.strip():
raise ValueError("content must be nonempty text")
roles.append(role)
body = roles[1:] if roles[:1] == ["system"] else roles
if len(body) < 2 or len(body) % 2:
raise ValueError("expected complete conversation pairs")
if body != ["user", "assistant"] * (len(body) // 2):
raise ValueError("invalid role order")
return messages
def inspect(path):
count = 0
with open(path, "rb") as stream:
while True:
raw = stream.readline(LIMIT + 1)
if not raw:
break
count += 1
try:
if len(raw) > LIMIT:
raise ValueError("line exceeds byte limit")
obj = json.loads(
raw.decode("utf-8"),
object_pairs_hook=unique_object,
parse_constant=reject_constant,
)
messages = validate(obj)
canonical = json.dumps(
messages, ensure_ascii=False,
sort_keys=True, separators=(",", ":"),
).encode("utf-8")
digest = hashlib.sha256(canonical).hexdigest()
if digest in seen:
raise ValueError(
"duplicate conversation; first at " + seen[digest]
)
seen[digest] = f"{path}:{count}"
except (ValueError, RecursionError) as error:
raise ValueError(f"{path}:{count}: {error}") from error
if count == 0:
raise ValueError(f"{path}: empty dataset")
return count
if __name__ == "__main__":
if len(sys.argv) != 3:
raise SystemExit("usage: check_dataset.py TRAIN VALID")
try:
counts = [inspect(path) for path in sys.argv[1:]]
except (OSError, ValueError) as error:
print(str(error), file=sys.stderr)
raise SystemExit(1)
print(json.dumps({"train": counts[0], "valid": counts[1]}))
讓重複檢查涵蓋兩個集合
依序傳入訓練集與驗證集,腳本會共用同一份雜湊摘要索引。因此,不論是集合內部重複,還是跨集合重複,都會判定為失敗,並顯示第一次出現的位置。腳本遇到第一個錯誤就會停止,修正後再重新執行,不會輸出整段對話內容。
python3 check_dataset.py train.jsonl valid.jsonl
計算雜湊摘要前,會先排序物件的鍵,消除 JSON 排版造成的差異,但保留訊息順序與內文的所有字元。調整縮排或鍵的順序無法避開檢查;在內文增加空白,則可能產生不同的摘要。腳本不會擅自統一大小寫或改寫 Unicode,以免改變程式碼、公式及原文含義。
精確去重不等於消除資料洩漏。從同一份文件拆出的不同問答,或問題相同但回答經過改寫的樣本,仍可能造成跨集合洩漏。
若有來源文件編號,應先依文件分組切分,再產生對話。若沒有來源編號,至少也要補做問題重複檢查與抽樣複核,不要把通過摘要檢查當成驗證集具備獨立性的證明。摘要索引會隨樣本數增加而成長;超大型資料集應改用磁碟索引。
分詞長度必須另外驗收
位元組數只用來限制異常輸入,不能換算成固定的 token 數。中文、程式碼、表情符號的編碼與分詞比例不同,聊天範本也會加入角色標記及其他特殊 token。
長度檢查應使用目標模型隨附的分詞器,依照訓練工具實際採用的聊天範本處理整段對話,再統計 token 數。記錄模型修訂識別碼、範本、最大序列長度,以及超限筆數;不要只分別計算每則 content 的 token 數後相加。
超限樣本要明確選擇拒絕、拆分或截斷。若採用截斷,應檢查是否把回答全部截掉,以及損失遮罩是否仍涵蓋有效回答;有效訓練 token 為零的樣本必須拒絕。這裡不提供通用的截斷指令,因為不同訓練入口的範本與遮罩行為並不相同。
用反例驗收檢查器,再串接訓練
先複製樣本,建立四種反例:空回答、角色順序錯誤、重複的物件鍵,以及跨檔案重複的對話。逐項執行,確認都回傳結束代碼 1,且能指出檔案與行號。也要用兩筆不同的合法對話驗證成功路徑,避免檢查器一直報錯卻沒有人發現。
整合進實驗腳本時,使用 && 或明確判斷結束代碼,再銜接後續訓練步驟,不可在檢查失敗後繼續執行。資料定稿後,保存兩份檔案的 SHA-256、樣本數、長度檢查結果與檢查器版本;本次訓練只讀取這份已通過驗收的副本。
驗收的終點不是一句「資料正常」,而是可重新執行的證據:相同輸入得到相同結論、錯誤能定位、集合切分有依據、長度處理有紀錄。完成這些,再開始投入訓練時間。
常見問題
JSONL 驗證通過,就代表資料適合 MLX 微調嗎?
不代表。腳本只檢查約定的訊息結構與完全重複的對話;仍須確認訓練工具的輸入規格,並使用目標模型的分詞器與聊天範本檢查長度。
改變 JSON 物件的鍵順序能避開重複檢查嗎?
不能。腳本排序物件鍵後才計算 SHA-256,因此鍵順序不影響結果;改寫措辭與語意近似則需要額外檢查。
依實驗時間租用 Mac
獨享 Mac mini M4,16GB 統一記憶體 / 256GB SSD。可預付日租、週租、月租或季租;租期結束後若未續租,機器將停止運作。模型與執行環境請自行準備,實際可用狀態以控制台即時回報為準。
按日租用 Mac