waxal2026-backup / phase2_corrected /code /train_whisper_lin.py
Pricile's picture
compactage apres suppression luganda
6eed659
Raw
History Blame Contribute Delete
4.77 kB
#!/usr/bin/env python3
"""PARI STEP-CHANGE : fine-tune Whisper-large-v3 (seq2seq 1.5B) sur le LINGALA WAXAL (70 h).
Motivation : notre champion est un CTC w2v-BERT 2.0 (580M) ; sur benchmark africain comparable
le seq2seq Whisper fait -16% relatif de WER vs w2v-BERT CTC. Le lingala (combine 0.346) est LE
goulot ; le shona (0.125) est déjà saturé. Whisper connaît nativement 'ln' et produit
casse+ponctuation (que le scoreur WAXAL compte).
Conforme : entraînement sur le SEUL texte/audio du train WAXAL, fine-tuning autorisé (fil 34204).
Gate : devhard-lin (439 clips, locuteurs held-out) — fiable pour les GROS effets.
"""
import json, os, sys
import numpy as np, soundfile as sf, torch
from dataclasses import dataclass
from torch.utils.data import Dataset
from transformers import (WhisperForConditionalGeneration, WhisperProcessor,
Seq2SeqTrainer, Seq2SeqTrainingArguments)
BASE = os.environ.get("BASE_MODEL", "openai/whisper-large-v3")
LANG = os.environ.get("LANG_ASR", "lin")
WLANG = {"lin": "ln", "sna": "sn"}[LANG]
OUT = os.environ.get("OUT", "/scratch/runs/whisper_%s" % LANG)
EPOCHS = float(os.environ.get("EPOCHS", "3"))
BS = int(os.environ.get("BS", "4"))
ACC = int(os.environ.get("ACC", "4"))
LR = float(os.environ.get("LR", "1e-5"))
MAXDUR = float(os.environ.get("MAXDUR", "30"))
SR = 16000
def load_manifest(path):
rows = []
for l in open(path, encoding="utf-8"):
r = json.loads(l)
if not r.get("text", "").strip():
continue
a = r["audio"]
if not os.path.exists(a): # remap si le chemin a bougé
for cand in ("/scratch/prep/audio/", "/scratch/restore/devhard_audio/",
"/root/devhard_audio/"):
p = os.path.join(cand, os.path.basename(a))
if os.path.exists(p):
r["audio"] = p; break
if os.path.exists(r["audio"]) and r.get("duration", 0) <= MAXDUR:
rows.append(r)
return rows
class DS(Dataset):
def __init__(self, rows, proc):
self.rows = rows; self.proc = proc
def __len__(self):
return len(self.rows)
def __getitem__(self, i):
r = self.rows[i]
au = sf.read(r["audio"], dtype="float32")[0]
if au.ndim > 1:
au = au.mean(1)
feats = self.proc.feature_extractor(au, sampling_rate=SR).input_features[0]
ids = self.proc.tokenizer(r["text"], max_length=200, truncation=True).input_ids
return {"input_features": feats, "labels": ids}
@dataclass
class Collate:
proc: object
def __call__(self, batch):
feats = torch.tensor(np.stack([b["input_features"] for b in batch]))
lab = self.proc.tokenizer.pad([{"input_ids": b["labels"]} for b in batch],
return_tensors="pt")
labels = lab["input_ids"].masked_fill(lab.attention_mask.ne(1), -100)
if (labels[:, 0] == self.proc.tokenizer.convert_tokens_to_ids("<|startoftranscript|>")).all():
labels = labels[:, 1:]
return {"input_features": feats, "labels": labels}
def main():
proc = WhisperProcessor.from_pretrained(BASE, language=WLANG, task="transcribe")
train = load_manifest("/root/devhard/train_%s_min.jsonl" % LANG)
dev = load_manifest("/root/devhard/devhard_%s.jsonl" % LANG)[:120] # eval rapide
print("train %d clips (%.1f h) | eval %d" % (
len(train), sum(r.get("duration", 0) for r in train) / 3600, len(dev)), flush=True)
if not train:
print("AUCUNE DONNEE — prep_data non termine ?", flush=True); sys.exit(1)
model = WhisperForConditionalGeneration.from_pretrained(BASE, dtype=torch.bfloat16)
model.config.forced_decoder_ids = None
model.config.suppress_tokens = []
model.generation_config.language = WLANG
model.generation_config.task = "transcribe"
model.generation_config.forced_decoder_ids = None
model.config.use_cache = False
args = Seq2SeqTrainingArguments(
output_dir=OUT, per_device_train_batch_size=BS, gradient_accumulation_steps=ACC,
learning_rate=LR, warmup_steps=300, num_train_epochs=EPOCHS,
gradient_checkpointing=True, bf16=True, optim="adamw_torch_fused",
logging_steps=50, save_strategy="epoch", save_total_limit=2,
eval_strategy="no", report_to=[], dataloader_num_workers=4,
remove_unused_columns=False, lr_scheduler_type="linear")
tr = Seq2SeqTrainer(model=model, args=args, train_dataset=DS(train, proc),
data_collator=Collate(proc))
tr.train()
tr.save_model(OUT + "/final")
proc.save_pretrained(OUT + "/final")
print("WHISPER_TRAIN_DONE %s/final" % OUT, flush=True)
if __name__ == "__main__":
main()