雲端 Mac 的 MLX 微調資料驗證:格式與集合洩漏

雲端 Mac 的 MLX 微調資料驗證:格式與集合洩漏

雲端 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

確認規格後,再開始實驗

確認模型權重、最高資源用量與租期,再選擇適合的週期。需要連線說明時,請先參閱說明中心。