Avant de lancer un fine-tuning MLX sur un Mac cloud, prenez le temps de valider les données. Une fois les jeux d’entraînement et de validation transférés, c’est la première vérification à effectuer : une ligne qui ne contient pas un objet JSON complet, une réponse vide ou une même conversation présente dans les deux jeux peut compromettre la comparaison des résultats d’une expérience. Voici comment mettre en place un contrôle reproductible avec la bibliothèque standard Python, sans télécharger de modèle ni dépendre de l’environnement d’entraînement.
Définir le format d’entrée sans laisser le script le deviner
Cet article traite uniquement de conversations en texte brut. Chaque ligne contient un objet dont le seul champ est messages ; chaque message ne comporte que role et content. Un unique message system est autorisé au début. Les messages doivent ensuite alterner entre user et assistant, et la conversation doit se terminer par une réponse. Les contenus multimodaux, les appels d’outils et les messages consécutifs de même rôle sortent du cadre de ce contrat.
Il s’agit de règles de validation propres au projet, et non d’un format imposé par tous les outils d’entraînement MLX. Avant l’exécution, vérifiez le format d’entrée attendu par votre outil. Si les données sources utilisent d’autres champs, prévoyez une étape de conversion explicite : le validateur ne doit pas supprimer des champs en silence.
{"messages":[{"role":"user","content":"解释训练集与验证集的区别。"},{"role":"assistant","content":"训练集用于更新参数,验证集用于观察未参与更新的数据表现。"}]}
Enregistrez les deux jeux dans train.jsonl et valid.jsonl, en UTF-8 sans BOM. Toute ligne vide constitue une erreur ; en revanche, le dernier caractère de fin de ligne du fichier ne compte pas comme une ligne vide supplémentaire. Conservez les fichiers sources en lecture seule et enregistrez les corrections dans une copie distincte pour pouvoir retracer les modifications.
Détecter les erreurs de format ligne par ligne avec la bibliothèque standard
Exécutez d’abord python3 --version pour vérifier que l’interpréteur est disponible, puis enregistrez le code ci-dessous dans check_dataset.py. La taille maximale d’une ligne est fixée à 256 KiB : c’est une limite de protection des entrées propre au projet, pas la capacité de contexte du modèle. Le script lit en mode binaire avec une limite de lecture, ce qui évite de charger intégralement une ligne trop longue en mémoire.
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]}))
Rechercher les doublons dans les deux jeux à la fois
Passez le jeu d’entraînement puis le jeu de validation en arguments : le script utilise un même index d’empreintes pour les deux. Les doublons internes à un jeu comme ceux présents dans les deux jeux provoquent donc un échec, avec indication de leur première occurrence. Le script s’arrête à la première erreur ; corrigez-la, puis relancez le contrôle. Il n’affiche pas le contenu intégral des conversations.
python3 check_dataset.py train.jsonl valid.jsonl
Avant de calculer l’empreinte, le script trie les clés des objets et élimine les différences de mise en forme JSON, tout en conservant l’ordre des messages et les caractères du texte. Modifier l’indentation ou l’ordre des clés ne permet donc pas de contourner la détection ; ajouter des espaces dans le texte peut en revanche produire une empreinte différente. Le script n’uniformise pas la casse et ne normalise pas Unicode de sa propre initiative, afin de ne pas altérer le code, les formules ou le sens du texte d’origine.
Éliminer les doublons exacts ne suffit pas à supprimer les fuites de données. Des questions-réponses distinctes issues d’un même document, ou des exemples dont la question est identique mais la réponse reformulée, peuvent encore créer une fuite entre les jeux.
Si vous disposez d’identifiants de documents sources, regroupez d’abord les données par document pour répartir ces groupes entre les jeux, puis générez les conversations. Sans identifiants de source, ajoutez au minimum une recherche de questions en double et une vérification par échantillonnage. Réussir le contrôle des empreintes ne prouve pas l’indépendance du jeu de validation. L’index d’empreintes grandit avec le nombre d’exemples ; pour les jeux de données très volumineux, utilisez plutôt un index sur disque.
Contrôler séparément la longueur en tokens
Le nombre d’octets sert uniquement à limiter les entrées anormales : il ne correspond pas à un nombre fixe de tokens. Le rapport entre encodage et tokenisation varie pour le chinois, le code et les émojis. Le gabarit de conversation ajoute également des marqueurs de rôle et d’autres tokens spéciaux.
Pour contrôler la longueur, utilisez le tokenizer fourni avec le modèle cible. Appliquez à la conversation entière le gabarit effectivement utilisé par l’outil d’entraînement, puis comptez les tokens. Consignez l’identifiant de révision du modèle, le gabarit, la longueur maximale de séquence et le nombre d’exemples qui dépassent cette limite. Ne vous contentez pas de compter séparément les tokens de chaque champ content puis de les additionner.
Pour les exemples trop longs, choisissez explicitement entre rejet, découpage et troncature. En cas de troncature, vérifiez que la réponse n’a pas été supprimée entièrement et que le masque de perte couvre toujours les parties utiles de la réponse. Tout exemple ne contenant plus aucun token utilisable pour l’entraînement doit être rejeté. Aucune commande de troncature universelle n’est proposée ici, car le comportement des gabarits et des masques varie selon le point d’entrée de l’entraînement.
Tester le validateur avec des cas invalides avant de l’intégrer à l’entraînement
Commencez par copier des exemples pour construire quatre cas invalides : une réponse vide, un ordre de rôles incorrect, une clé d’objet dupliquée et une conversation présente dans les deux fichiers. Exécutez chaque test et vérifiez qu’il renvoie le code de sortie 1 en permettant de repérer le fichier et le numéro de ligne. Testez aussi le cas de réussite avec deux conversations valides et distinctes, pour éviter qu’un validateur échouant systématiquement ne passe inaperçu.
Lors de l’intégration au script d’expérimentation, utilisez && ou une vérification explicite du code de sortie pour conditionner les étapes d’entraînement suivantes. L’exécution ne doit pas se poursuivre après un échec. Une fois les données figées, conservez les empreintes SHA-256 des deux fichiers, le nombre d’exemples, les résultats du contrôle de longueur et la version du validateur. Cet entraînement ne doit lire que cette copie validée.
La validation ne se résume pas à déclarer que « les données sont correctes ». Elle doit produire des éléments vérifiables par une nouvelle exécution : les mêmes entrées donnent la même conclusion, les erreurs sont localisables, la répartition entre les jeux est justifiée et le traitement des longueurs est documenté. Ce n’est qu’ensuite qu’il faut commencer à consommer du temps d’entraînement.
Questions fréquentes
Un fichier JSONL valide suffit-il pour entraîner le modèle ?
Non. Il faut aussi vérifier le format attendu par l’outil utilisé et mesurer la longueur avec le tokenizer et le modèle de conversation du modèle choisi.
Changer l’ordre des clés masque-t-il un doublon ?
Non. Le script trie les clés avant de calculer le SHA-256. Il détecte les conversations identiques, mais pas les reformulations ni les ressemblances sémantiques.
Louez un Mac pour la durée de votre expérience
Mac mini M4 dédié, avec 16 Go de mémoire unifiée et un SSD de 256 Go. Paiement à l’avance par jour, semaine, mois ou trimestre ; la machine s’arrête à la fin de la période si vous ne renouvelez pas. Préparez vous-même les modèles et l’environnement d’exécution. L’état réel des ressources est indiqué en temps réel dans la console.
Louer un Mac à la journée