chendren commited on
Commit
ae0f809
·
verified ·
1 Parent(s): fe1178e

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. train_v3.py +288 -0
train_v3.py ADDED
@@ -0,0 +1,288 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Voxtral 3B — Haitian Creole LoRA v3.
3
+ Synthesizes domain-specific audio on-the-fly, combines with CMU dataset, trains.
4
+ All in one job — no separate upload step.
5
+ """
6
+
7
+ import torch
8
+ import json
9
+ import os
10
+ import time
11
+ import numpy as np
12
+ import soundfile as sf
13
+ from datasets import load_dataset, Dataset, Audio, concatenate_datasets
14
+ from transformers import (
15
+ VoxtralForConditionalGeneration,
16
+ VoxtralProcessor,
17
+ VitsModel,
18
+ AutoTokenizer,
19
+ Trainer,
20
+ TrainingArguments,
21
+ )
22
+ from peft import LoraConfig, get_peft_model, PeftModel
23
+ from jiwer import wer, cer
24
+
25
+ MODEL_ID = "mistralai/Voxtral-Mini-3B-2507"
26
+ OUTPUT = os.environ.get("HF_OUTPUT_DIR", "./output")
27
+ NUM_TEST = 200
28
+ MAX_STEPS = 5000
29
+ LR = 3e-5
30
+ LORA_R = 32
31
+ LORA_ALPHA = 64
32
+ SAMPLE_RATE = 16000
33
+
34
+
35
+ class VoxtralDataCollator:
36
+ def __init__(self, processor, model_id):
37
+ self.processor = processor
38
+ self.model_id = model_id
39
+
40
+ def __call__(self, features):
41
+ texts = [f["text"].lower() for f in features]
42
+ audios = [f["audio"]["array"] for f in features]
43
+
44
+ prompt = self.processor.apply_transcription_request(
45
+ language="en", model_id=self.model_id,
46
+ audio=audios, format=["WAV"] * len(audios),
47
+ return_tensors="pt",
48
+ )
49
+ passthrough = {k: v for k, v in prompt.items()
50
+ if k not in ("input_ids", "attention_mask")}
51
+ prompt_ids = prompt["input_ids"]
52
+ prompt_attn = prompt["attention_mask"]
53
+ B = prompt_ids.size(0)
54
+ tok = self.processor.tokenizer
55
+
56
+ text_tok = tok(texts, add_special_tokens=False, padding=False,
57
+ truncation=True, max_length=256, return_tensors=None)
58
+ text_ids_list = text_tok["input_ids"]
59
+
60
+ input_ids, attention_mask, labels = [], [], []
61
+ for i in range(B):
62
+ p_ids = prompt_ids[i].tolist()
63
+ p_att = prompt_attn[i].tolist()
64
+ t_ids = text_ids_list[i]
65
+ ids = p_ids + t_ids + [tok.eos_token_id]
66
+ attn = p_att + [1] * (len(t_ids) + 1)
67
+ lab = [-100] * len(p_ids) + t_ids + [tok.eos_token_id]
68
+ input_ids.append(ids)
69
+ attention_mask.append(attn)
70
+ labels.append(lab)
71
+
72
+ pad_id = tok.pad_token_id if tok.pad_token_id is not None else tok.eos_token_id
73
+ max_len = max(len(x) for x in input_ids)
74
+ def pad_to(seq, fill, L): return seq + [fill] * (L - len(seq))
75
+
76
+ input_ids = [pad_to(x, pad_id, max_len) for x in input_ids]
77
+ attention_mask = [pad_to(x, 0, max_len) for x in attention_mask]
78
+ labels = [pad_to(x, -100, max_len) for x in labels]
79
+
80
+ batch = {
81
+ "input_ids": torch.tensor(input_ids, dtype=torch.long),
82
+ "attention_mask": torch.tensor(attention_mask, dtype=torch.long),
83
+ "labels": torch.tensor(labels, dtype=torch.long),
84
+ }
85
+ for k, v in passthrough.items():
86
+ batch[k] = v
87
+ return batch
88
+
89
+
90
+ def synthesize_domain_data():
91
+ """Synthesize Creole audio for domain text datasets using MMS-TTS."""
92
+ print("\n Loading MMS-TTS (facebook/mms-tts-hat)...")
93
+ tts_model = VitsModel.from_pretrained("facebook/mms-tts-hat").to("cuda")
94
+ tts_tokenizer = AutoTokenizer.from_pretrained("facebook/mms-tts-hat")
95
+
96
+ domains = [
97
+ ("jsbeaudry/med-eng-cre", "medical", "text_target", "train"),
98
+ ("jsbeaudry/data-therapy-creole", "therapy", "output", "train"),
99
+ ("jsbeaudry/creole-psychology", "psychology", "response_j", "train"),
100
+ ("jsbeaudry/creole-economy-with-words", "economics", "assistant", "train"),
101
+ ("jsbeaudry/creole-sport", "sports", "answer", "train"),
102
+ ]
103
+
104
+ audio_dir = "/tmp/synth_audio"
105
+ os.makedirs(audio_dir, exist_ok=True)
106
+
107
+ all_audio_paths = []
108
+ all_texts = []
109
+
110
+ for ds_name, domain, text_col, split in domains:
111
+ print(f" Synthesizing {domain}...")
112
+ ds = load_dataset(ds_name, split=split)
113
+ count = 0
114
+
115
+ for i, row in enumerate(ds):
116
+ text = row.get(text_col, "")
117
+ if not text or not isinstance(text, str) or len(text.strip()) < 10:
118
+ continue
119
+ text = text.strip()[:300]
120
+ try:
121
+ inputs = tts_tokenizer(text, return_tensors="pt").to("cuda")
122
+ with torch.no_grad():
123
+ audio = tts_model(**inputs).waveform.squeeze().cpu().numpy()
124
+ if len(audio) > SAMPLE_RATE: # >1 second
125
+ path = os.path.join(audio_dir, f"{domain}_{i:05d}.wav")
126
+ sf.write(path, audio, SAMPLE_RATE)
127
+ all_audio_paths.append(path)
128
+ all_texts.append(text.lower())
129
+ count += 1
130
+ except Exception:
131
+ continue
132
+
133
+ print(f" {domain}: {count} samples")
134
+
135
+ # Free TTS model
136
+ del tts_model, tts_tokenizer
137
+ torch.cuda.empty_cache()
138
+
139
+ # Build HF dataset
140
+ synth_ds = Dataset.from_dict({"audio": all_audio_paths, "text": all_texts})
141
+ synth_ds = synth_ds.cast_column("audio", Audio(sampling_rate=SAMPLE_RATE))
142
+ print(f" Total synthesized: {len(synth_ds)}")
143
+ return synth_ds
144
+
145
+
146
+ def main():
147
+ gpu = torch.cuda.get_device_name(0) if torch.cuda.is_available() else "CPU"
148
+ vram = torch.cuda.get_device_properties(0).total_memory / 1e9 if torch.cuda.is_available() else 0
149
+
150
+ print("=" * 70)
151
+ print(" Voxtral 3B — Haitian Creole LoRA v3 (CMU + Domain Synthesis)")
152
+ print(f" GPU: {gpu} ({vram:.0f} GB)")
153
+ print("=" * 70)
154
+
155
+ # Step 1: Synthesize domain audio
156
+ print("\n[1/6] Synthesizing domain-specific audio...")
157
+ t_synth = time.time()
158
+ synth_ds = synthesize_domain_data()
159
+ synth_time = time.time() - t_synth
160
+ print(f" Synthesis done: {synth_time/60:.1f} min")
161
+
162
+ # Step 2: Load CMU dataset
163
+ print("\n[2/6] Loading CMU Haitian Creole dataset...")
164
+ cmu_ds = load_dataset("jsbeaudry/cmu_haitian_creole_speech", split="train")
165
+ cmu_ds = cmu_ds.cast_column("audio", Audio(sampling_rate=SAMPLE_RATE))
166
+ cmu_ds = cmu_ds.remove_columns(["text"]).rename_column("normalized_text", "text")
167
+ cmu_ds = cmu_ds.select_columns(["audio", "text"])
168
+ print(f" CMU: {len(cmu_ds)} samples")
169
+
170
+ # Step 3: Merge and split
171
+ merged = concatenate_datasets([cmu_ds, synth_ds])
172
+ merged = merged.shuffle(seed=42)
173
+ test_ds = merged.select(range(len(merged) - NUM_TEST, len(merged)))
174
+ train_ds = merged.select(range(len(merged) - NUM_TEST))
175
+ print(f" Merged: {len(merged)} total, Train: {len(train_ds)}, Test: {len(test_ds)}")
176
+
177
+ # Step 4: Load model + LoRA
178
+ print("\n[3/6] Loading Voxtral 3B + LoRA...")
179
+ processor = VoxtralProcessor.from_pretrained(MODEL_ID)
180
+ model = VoxtralForConditionalGeneration.from_pretrained(
181
+ MODEL_ID, torch_dtype=torch.bfloat16, device_map="auto"
182
+ )
183
+ config = LoraConfig(
184
+ r=LORA_R, lora_alpha=LORA_ALPHA, lora_dropout=0.05, bias="none",
185
+ target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
186
+ task_type="SEQ_2_SEQ_LM",
187
+ )
188
+ model = get_peft_model(model, config)
189
+ model.print_trainable_parameters()
190
+
191
+ collator = VoxtralDataCollator(processor, MODEL_ID)
192
+
193
+ # Step 5: Train
194
+ print(f"\n[4/6] Training {MAX_STEPS} steps...")
195
+ t_train = time.time()
196
+
197
+ args = TrainingArguments(
198
+ output_dir=OUTPUT,
199
+ max_steps=MAX_STEPS,
200
+ per_device_train_batch_size=2,
201
+ gradient_accumulation_steps=4,
202
+ learning_rate=LR,
203
+ weight_decay=0.01,
204
+ warmup_steps=100,
205
+ logging_steps=100,
206
+ save_steps=1000,
207
+ bf16=True,
208
+ max_grad_norm=1.0,
209
+ dataloader_num_workers=1,
210
+ remove_unused_columns=False,
211
+ report_to="none",
212
+ lr_scheduler_type="cosine",
213
+ )
214
+
215
+ trainer = Trainer(
216
+ model=model, args=args,
217
+ train_dataset=train_ds,
218
+ data_collator=collator,
219
+ )
220
+ trainer.train()
221
+ train_time = time.time() - t_train
222
+ print(f" Training done: {train_time/60:.1f} min")
223
+
224
+ trainer.save_model(OUTPUT)
225
+ processor.save_pretrained(OUTPUT)
226
+
227
+ # Step 6: Evaluate
228
+ print(f"\n[5/6] Evaluating on {NUM_TEST} held-out samples...")
229
+ model.config = model.base_model.model.config
230
+ refs, preds = [], []
231
+
232
+ for i, sample in enumerate(test_ds):
233
+ audio = sample["audio"]["array"]
234
+ ref = sample["text"].lower()
235
+ inputs = processor.apply_transcription_request(
236
+ language="en", model_id=MODEL_ID,
237
+ audio=[audio], format=["WAV"], return_tensors="pt",
238
+ )
239
+ inputs = {k: v.to(model.device) if isinstance(v, torch.Tensor) else v for k, v in inputs.items()}
240
+ with torch.no_grad():
241
+ out = model.generate(**inputs, max_new_tokens=256)
242
+ pred = processor.batch_decode(out, skip_special_tokens=True)[0].strip()
243
+ refs.append(ref)
244
+ preds.append(pred)
245
+ if i < 10 or (i + 1) % 50 == 0:
246
+ print(f" [{i+1}/{NUM_TEST}]")
247
+ if i < 5:
248
+ print(f" Ref: {ref[:70]}")
249
+ print(f" Pred: {pred[:70]}")
250
+
251
+ ft_wer = wer(refs, preds)
252
+ ft_cer = cer(refs, preds)
253
+ exact = sum(1 for r, p in zip(refs, preds) if r.strip() == p.strip())
254
+
255
+ print(f"\n{'=' * 70}")
256
+ print(" RESULTS v3 (CMU + Medical + Therapy + Psychology + Economics + Sports)")
257
+ print(f"{'=' * 70}")
258
+ print(f" v1 WER: 11.8% (CMU only, r=16, 2000 steps)")
259
+ print(f" v2 WER: 9.7% (CMU only, r=32, 5000 steps)")
260
+ print(f" v3 WER: {ft_wer:.1%} (CMU + domains, r=32, {MAX_STEPS} steps)")
261
+ print(f" v3 CER: {ft_cer:.1%}")
262
+ print(f" v3 Exact: {exact}/{NUM_TEST} ({exact/NUM_TEST:.1%})")
263
+ print(f" Training data: {len(train_ds)} samples ({len(cmu_ds)} CMU + {len(synth_ds)} synthetic)")
264
+ print(f" Synthesis: {synth_time/60:.1f} min, Training: {train_time/60:.1f} min")
265
+ print(f"{'=' * 70}")
266
+
267
+ results = {
268
+ "version": "v3",
269
+ "finetuned_wer": ft_wer, "finetuned_cer": ft_cer,
270
+ "exact_matches": exact, "exact_match_rate": exact / NUM_TEST,
271
+ "v1_wer": 0.118, "v2_wer": 0.097, "baseline_wer": 1.014,
272
+ "train_samples": len(train_ds), "cmu_samples": len(cmu_ds),
273
+ "synth_samples": len(synth_ds), "test_samples": NUM_TEST,
274
+ "training_min": round(train_time / 60, 1),
275
+ "synthesis_min": round(synth_time / 60, 1),
276
+ "steps": MAX_STEPS, "lora_r": LORA_R, "lr": LR,
277
+ "gpu": gpu,
278
+ "domains": ["medical", "therapy", "psychology", "economics", "sports"],
279
+ "samples": [{"ref": r, "pred": p} for r, p in zip(refs, preds)],
280
+ }
281
+ os.makedirs(OUTPUT, exist_ok=True)
282
+ with open(os.path.join(OUTPUT, "results_v3.json"), "w") as f:
283
+ json.dump(results, f, indent=2, ensure_ascii=False)
284
+ print(f"Saved to {OUTPUT}/results_v3.json")
285
+
286
+
287
+ if __name__ == "__main__":
288
+ main()