Isa0 commited on
Commit
e498f5b
·
1 Parent(s): c17d163

add train code

Browse files
Files changed (3) hide show
  1. README.md +89 -0
  2. export_onnx.py +69 -0
  3. train.py +128 -0
README.md CHANGED
@@ -1,3 +1,92 @@
1
  ---
2
  license: mit
3
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  license: mit
3
  ---
4
+ # Speech to Text
5
+
6
+ Fine-tune a [Wav2Vec2](https://huggingface.co/facebook/wav2vec2-base) acoustic model on the [LJSpeech](https://keithito.com/LJ-Speech-Dataset/) dataset using CTC, then export it to ONNX for inference.
7
+
8
+ ## Requirements
9
+
10
+ - Python >= 3.10
11
+ - A CUDA-capable GPU is recommended for training
12
+
13
+ Install dependencies:
14
+
15
+ ```bash
16
+ pip install -e .
17
+ ```
18
+
19
+ ## Training
20
+
21
+ Fine-tune `facebook/wav2vec2-base` on LJSpeech (5% held out for eval). Training takes ~10 epochs by default and writes checkpoints to `wav2vec2-ljspeech/`.
22
+
23
+ ```bash
24
+ python train.py
25
+ ```
26
+
27
+ Key settings live at the top of `train.py`:
28
+
29
+ | Constant | Default | Purpose |
30
+ | --- | --- | --- |
31
+ | `MODEL_ID` | `facebook/wav2vec2-base` | Pre-trained wav2vec2 checkpoint |
32
+ | `DATASET_ID` | `lj_speech` | HuggingFace dataset id |
33
+
34
+ Training hyperparameters (batch size, epochs, learning rate, etc.) are configured through `TrainingArguments` inside `train.py`.
35
+
36
+ Monitor progress with TensorBoard:
37
+
38
+ ```bash
39
+ tensorboard --logdir wav2vec2-ljspeech
40
+ ```
41
+
42
+ ## ONNX Export
43
+
44
+ Export the trained checkpoint to ONNX and validate it with ONNX Runtime:
45
+
46
+ ```bash
47
+ python export_onnx.py
48
+ ```
49
+
50
+ Options:
51
+
52
+ ```
53
+ --model-dir Checkpoint directory (default: wav2vec2-ljspeech)
54
+ --output Output ONNX path (default: wav2vec2-ljspeech.onnx)
55
+ --opset ONNX opset version (default: 17)
56
+ ```
57
+
58
+ The exported model uses dynamic axes on batch and time, so it accepts audio of any length.
59
+
60
+ ## Inference
61
+
62
+ ```python
63
+ import numpy as np
64
+ import onnxruntime as ort
65
+ from transformers import Wav2Vec2Processor
66
+
67
+ processor = Wav2Vec2Processor.from_pretrained("wav2vec2-ljspeech")
68
+ session = ort.InferenceSession("wav2vec2-ljspeech.onnx")
69
+
70
+ # audio_array: 16 kHz mono float32 numpy array
71
+ inputs = processor(audio_array, sampling_rate=16000, return_tensors="np")
72
+ logits = session.run(None, {"input_values": inputs.input_values})[0]
73
+ text = processor.tokenizer.batch_decode(np.argmax(logits, axis=-1))[0]
74
+ print(text)
75
+ ```
76
+
77
+ Notes:
78
+
79
+ - Audio must be **16 kHz mono float32**.
80
+ - The `Wav2Vec2Processor` handles waveform normalization and tokenization — always pass audio through it before the ONNX session.
81
+ - This exports the **acoustic model only**. Add an external LM (e.g. KenLM) for language-model-rescored decoding if needed.
82
+
83
+ ## Project Layout
84
+
85
+ ```
86
+ speech-to-text/
87
+ ├── train.py # Wav2Vec2 + CTC fine-tuning on LJSpeech
88
+ ├── export_onnx.py # ONNX export and ONNX Runtime validation
89
+ ├── main.py # Placeholder entry point
90
+ ├── pyproject.toml # Project metadata and dependencies
91
+ └── README.md
92
+ ```
export_onnx.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ from pathlib import Path
3
+
4
+ import numpy as np
5
+ import onnxruntime as ort
6
+ import torch
7
+ from transformers import Wav2Vec2ForCTC, Wav2Vec2Processor
8
+
9
+ DEFAULT_MODEL_DIR = "wav2vec2-ljspeech"
10
+ DEFAULT_OUTPUT = "wav2vec2-ljspeech.onnx"
11
+ SAMPLE_RATE = 16000
12
+
13
+
14
+ def export(model_dir: str, output: str, opset: int) -> None:
15
+ model_path = Path(model_dir)
16
+ processor = Wav2Vec2Processor.from_pretrained(model_path)
17
+ model = Wav2Vec2ForCTC.from_pretrained(model_path)
18
+ model.eval()
19
+
20
+ seq_len = SAMPLE_RATE
21
+ dummy = torch.zeros(1, seq_len, dtype=torch.float32)
22
+
23
+ dynamic_axes = {
24
+ "input_values": {0: "batch", 1: "time"},
25
+ "logits": {0: "batch", 1: "time"},
26
+ }
27
+
28
+ output_path = Path(output)
29
+ output_path.parent.mkdir(parents=True, exist_ok=True)
30
+
31
+ with torch.no_grad():
32
+ torch.onnx.export(
33
+ model,
34
+ (dummy,),
35
+ str(output_path),
36
+ input_names=["input_values"],
37
+ output_names=["logits"],
38
+ dynamic_axes=dynamic_axes,
39
+ opset_version=opset,
40
+ do_constant_folding=True,
41
+ )
42
+
43
+ print(f"Exported ONNX model to {output_path}")
44
+ print(f" opset: {opset}, vocab_size: {len(processor.tokenizer)}")
45
+ print(f" size: {output_path.stat().st_size / (1024 * 1024):.1f} MB")
46
+
47
+ validate(output_path, processor, seq_len)
48
+
49
+
50
+ def validate(onnx_path: Path, processor, seq_len: int) -> None:
51
+ session = ort.InferenceSession(str(onnx_path), providers=["CPUExecutionProvider"])
52
+
53
+ audio = np.zeros((1, seq_len), dtype=np.float32)
54
+ outputs = session.run(None, {"input_values": audio})
55
+ logits = outputs[0]
56
+
57
+ print(f"Validated with ONNX Runtime: output shape = {logits.shape}")
58
+ pred_ids = np.argmax(logits, axis=-1)
59
+ text = processor.tokenizer.batch_decode(pred_ids)[0]
60
+ print(f"Decoded dummy input -> {text!r}")
61
+
62
+
63
+ if __name__ == "__main__":
64
+ parser = argparse.ArgumentParser(description="Export Wav2Vec2 CTC to ONNX")
65
+ parser.add_argument("--model-dir", default=DEFAULT_MODEL_DIR)
66
+ parser.add_argument("--output", default=DEFAULT_OUTPUT)
67
+ parser.add_argument("--opset", type=int, default=17)
68
+ args = parser.parse_args()
69
+ export(args.model_dir, args.output, args.opset)
train.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from dataclasses import dataclass
3
+ from typing import Any
4
+
5
+ from datasets import load_dataset
6
+ from transformers import (
7
+ AutoTokenizer,
8
+ Trainer,
9
+ TrainingArguments,
10
+ Wav2Vec2ForCTC,
11
+ Wav2Vec2Processor,
12
+ )
13
+
14
+ MODEL_ID = "facebook/wav2vec2-base"
15
+ DATASET_ID = "lj_speech"
16
+
17
+
18
+ @dataclass
19
+ class DataCollatorCTCWithPadding:
20
+ processor: Wav2Vec2Processor
21
+ padding: bool | str = True
22
+
23
+ def __call__(self, features: list[dict[str, Any]]) -> dict[str, torch.Tensor]:
24
+ input_features = [{"input_values": f["input_values"]} for f in features]
25
+ label_features = [{"input_ids": f["labels"]} for f in features]
26
+
27
+ batch = self.processor.feature_extractor.pad(
28
+ input_features, padding=self.padding, return_tensors="pt"
29
+ )
30
+ labels_batch = self.processor.tokenizer.pad(
31
+ label_features, padding=self.padding, return_tensors="pt"
32
+ )
33
+ labels = labels_batch["input_ids"].masked_fill(
34
+ labels_batch.attention_mask.ne(1), -100
35
+ )
36
+ batch["labels"] = labels
37
+ return batch
38
+
39
+
40
+ def prepare_dataset(batch, processor):
41
+ audio = batch["audio"]
42
+ batch["input_values"] = processor(
43
+ audio["array"], sampling_rate=audio["sampling_rate"]
44
+ ).input_values[0]
45
+ batch["input_length"] = len(batch["input_values"])
46
+ batch["labels"] = processor(text=batch["normalized_text"]).input_ids
47
+ return batch
48
+
49
+
50
+ def compute_metrics(pred):
51
+ from jiwer import wer
52
+
53
+ pred_logits = pred.predictions
54
+ pred_ids = torch.argmax(torch.tensor(pred_logits), dim=-1)
55
+
56
+ pred_str = [s if len(s) > 0 else "-" for s in tokenizer.batch_decode(pred_ids)]
57
+ label_ids = pred.label_ids.copy()
58
+ label_ids[label_ids == -100] = tokenizer.pad_token_id
59
+ label_str = tokenizer.batch_decode(label_ids, group_tokens=False)
60
+
61
+ error = wer(predictions=pred_str, references=label_str)
62
+ return {"wer": error}
63
+
64
+
65
+ if __name__ == "__main__":
66
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
67
+ processor = Wav2Vec2Processor.from_pretrained(MODEL_ID)
68
+
69
+ dataset = load_dataset(DATASET_ID, split="train")
70
+ dataset = dataset.train_test_split(test_size=0.05, seed=42)
71
+
72
+ dataset = dataset.map(
73
+ lambda batch: prepare_dataset(batch, processor),
74
+ remove_columns=dataset.column_names["train"],
75
+ num_proc=4,
76
+ )
77
+
78
+ dataset = dataset.filter(
79
+ lambda x: x < processor.tokenizer.model_max_length,
80
+ input_columns=["input_length"],
81
+ num_proc=4,
82
+ )
83
+
84
+ data_collator = DataCollatorCTCWithPadding(processor=processor)
85
+
86
+ model = Wav2Vec2ForCTC.from_pretrained(
87
+ MODEL_ID,
88
+ attention_dropout=0.0,
89
+ hidden_dropout=0.0,
90
+ feat_proj_dropout=0.0,
91
+ mask_time_prob=0.0,
92
+ layerdrop=0.0,
93
+ ctc_loss_reduction="mean",
94
+ pad_token_id=processor.tokenizer.pad_token_id,
95
+ vocab_size=len(processor.tokenizer),
96
+ )
97
+ model.freeze_feature_extractor()
98
+
99
+ training_args = TrainingArguments(
100
+ output_dir="wav2vec2-ljspeech",
101
+ per_device_train_batch_size=8,
102
+ gradient_accumulation_steps=2,
103
+ learning_rate=3e-4,
104
+ num_train_epochs=10,
105
+ warmup_steps=500,
106
+ fp16=True,
107
+ save_steps=500,
108
+ eval_strategy="steps",
109
+ eval_steps=500,
110
+ logging_steps=100,
111
+ save_total_limit=3,
112
+ report_to=["tensorboard"],
113
+ gradient_checkpointing=True,
114
+ )
115
+
116
+ trainer = Trainer(
117
+ model=model,
118
+ args=training_args,
119
+ train_dataset=dataset["train"],
120
+ eval_dataset=dataset["test"],
121
+ data_collator=data_collator,
122
+ compute_metrics=compute_metrics,
123
+ processing_class=processor.tokenizer,
124
+ )
125
+
126
+ trainer.train()
127
+ trainer.save_model(training_args.output_dir)
128
+ processor.save_pretrained(training_args.output_dir)