从一段吉他音频,
到一串 DSP 控制指令。
沿着数据流阅读这个训练工程:样本如何配对,立体声如何进入 Whisper,程序 token 如何成为学习目标,以及模型如何被评估和保存。每个环节先讲逻辑,再看实际代码。
训练流程图
按编号阅读 · 点击进入对应逻辑与代码14 评估按间隔穿插在 13 训练中,训练返回后再执行一次。PAUSE 保存状态后停止训练循环,但仍进入最终评估和导出。
一条样本的形状变化
音频 [T,C]→波形 [2,96000]→特征 [160,3000]→logits [B,L,3206]默认 Whisper Base 特征配置:波形先取 6 秒,再由特征提取器补到 30 秒;目标 labels 为 [B,L],L ≤ 768。B 为 batch size。
target_program_json.model_token_ids。数据渲染、程序序列化和实时效果器推理不在当前 V4 工程中。文件结构与职责
以“入口、训练实现、固定资源、运行产物、说明文档”组织阅读。训练代码保持在 Main,常用命令统一从 scripts 进入。
AI_Guitar_Pedal_V4/
├── README.md 项目导航
├── scripts/
│ ├── train.ps1 统一训练入口
│ └── README.md 入口说明
├── Main/
│ ├── train_program_seq2seq.py
│ ├── requirements.txt
│ ├── ai_guitar_pedal/
│ │ ├── __init__.py 包标记
│ │ ├── phase2_training_utils.py
│ │ ├── storage_redirect.py
│ │ ├── memory_guard.py
│ │ └── tokenizer/
│ │ ├── __init__.py 包标记
│ │ └── dsp_control_tokenizer.py
│ ├── resources/
│ │ └── dsp_control_vocab.json
│ ├── tools/
│ │ └── run_phase2_full_train_safe.ps1
│ │ 旧入口兼容层
│ └── storage/ 运行时产物(忽略)
│ ├── artifacts/
│ │ ├── checkpoints/<实验>/
│ │ ├── exports/<实验>/
│ │ ├── runs/<实验>/
│ │ └── logs/
│ ├── cache/whisper/features/
│ ├── data/
│ └── tmp/
└── docs/
├── README.md 文档导航
├── reference/ 原迁移记录
├── maintenance/ 整理与验证记录
└── site/
├── guide.json / guide.en.json 中英讲解与代码引用
├── template.html / template.en.html 网页骨架
├── style.css / app.js
├── build.py 从源码生成页面
├── verify.py 链接与源码一致性检查
├── dependency-excerpts.json
│ 依赖源码阅读证据
└── dist/ 可直接打开或托管
├── index.html / en.html
├── source/ 可定位行号的源码
└── source-manifest.json| 文件 / 目录 | 职责与训练关系 |
|---|---|
scripts/train.ps1 | 解析数据位置、设置缓存环境、选择 Python、记录日志并调用训练脚本。 |
train_program_seq2seq.py | 主控制流:读取标签、数据划分、Dataset/Collator、建模、Trainer、评估和导出。 |
phase2_training_utils.py | 立体声处理、特征缓存、首层卷积适配、暂停和恢复。 |
tokenizer/ + resources/ | 严格的 DSP token↔ID 映射;词表是输出协议。 |
storage_redirect.py | 统一产物和依赖缓存目录,检查输出是否位于指定根目录。 |
memory_guard.py | 检查 RAM/VRAM 使用率,按间隔输出告警。 |
Main/storage/ | 运行时生成;checkpoint、export、日志和特征缓存均放在这里。 |
docs/site/dist/ | 网页成品。包含训练代码快照,不包含数据集或模型权重。 |
requirements.txt | 训练依赖清单;当前未锁版本。 |
__init__.py | 标记 Python 包,使训练入口能够导入各模块。 |
.gitignore | 排除虚拟环境、Python 缓存和 Main/storage 运行产物。 |
外部只读依赖:同级 AI_Guitar_Pedal_V2/Audio 下的 Wet_Main manifest、音频和 Presets/programs。V4 自带词表;也可通过 -DataProjectRoot 改用完整的数据工程。
01启动入口与存储路径
先确定数据从哪里读、训练产物写到哪里,再进入 Python 主流程。
- 输入
- PowerShell 参数 + 数据工程路径
- 输出
- 训练命令、缓存环境、产物目录
- 核心文件
- scripts/train.ps1 · storage_redirect.py
逻辑 · 如何工作
推荐从项目根目录执行 scripts/train.ps1。启动器优先检查 V4 自己的 Audio/Samples/Wet_Main/manifest_full.jsonl,找不到时尝试同级 V2;也可以显式传入 -DataProjectRoot。音频和 program JSON 被读取,训练产物默认写入 V4 的 Main/storage。
启动器设置 Hugging Face、Torch、pip 和临时文件等目录,优先使用 V4 的 Main/.venv,其次使用数据工程的虚拟环境,最后使用系统 Python。训练脚本在导入重型依赖之前也根据 --e_root 引导存储环境。
-DryRun 用于查看解析后的命令;它仍启动 Python、导入依赖并建立目录,但在数据扫描和模型加载之前退出。-ValidateOnly 则继续完成数据检查与划分后退出,同样不提取特征或加载 Whisper 权重。
代码 · 核心实现
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$projectDir = (Resolve-Path (Join-Path $scriptDir "..")).Path
$mainDir = (Resolve-Path (Join-Path $projectDir "Main")).Path
$workspaceDir = Split-Path -Parent $projectDir
if (-not $DataProjectRoot) {
$localManifest = Join-Path $projectDir "Audio\Samples\Wet_Main\manifest_full.jsonl"
$v2ProjectRoot = Join-Path $workspaceDir "AI_Guitar_Pedal_V2"
$v2Manifest = Join-Path $v2ProjectRoot "Audio\Samples\Wet_Main\manifest_full.jsonl"
if (Test-Path $localManifest) {
$DataProjectRoot = $projectDir
} elseif (Test-Path $v2Manifest) {
$DataProjectRoot = $v2ProjectRoot
} else {
throw "No complete Phase 2 data tree found. Pass -DataProjectRoot explicitly."
}
}
$DataProjectRoot = Resolve-ExistingPath -PathValue $DataProjectRoot
scripts/train.ps1 · L67–L84 ↗def bootstrap_storage_env_from_argv(argv: Sequence[str]) -> None:
raw_e_root = early_arg_value(argv, "--e_root") or E_ROOT
try:
e_root = resolve_e_root(raw_e_root)
storage = ensure_storage_tree(e_root)
apply_env_defaults(storage, set_temp_tmp=True)
except Exception:
return
Main/train_program_seq2seq.py · L34–L41 ↗02确定模型要学习的输出语言
固定词表将效果器结构、参数名和离散值映射为 3,206 个 ID。
- 输入
- dsp_control_vocab.json
- 输出
- token ↔ ID;PAD/BOS/EOS = 0/1/2
- 核心类
- DSPControlTokenizer
逻辑 · 如何工作
V4 使用专门的 DSP 控制词表。每个 token 代表结构标记、效果器类型、参数名称或离散数值;模型在每个输出位置预测 3,206 类中的一类。它不是自然语言转写,也不直接预测音频波形。
构造 tokenizer 时要求 ID 唯一、从 0 连续,并必须包含 PAD、BOS、EOS。encode_tokens 与 decode_ids 做严格的字典映射,未知 token 或 ID 直接报错;训练主要读取已编码的 model_token_ids。
例如下方真实程序片段先声明一条 chain,再声明 gate 效果器,逐项提供参数和值,最后结束效果器与整条链。Q_0169 是量化符号;它的物理单位及反量化规则由外部 DSP 协议决定。
| 符号组 | 数量 | 意义 |
|---|---|---|
| 结构符号 | 6 | PAD、BOS、EOS、CHAIN、END_CHAIN、END_FX |
| FX / CAT / P | 141 / 32 / 1,740 | 效果器 / 类别 / 参数名 |
| B / I / E | 2 / 63 / 221 | 布尔 / 整数 / 枚举值 |
| Q | 1,001 | Q_0000 … Q_1000 量化值 |
<P_19000_A> <Q_0000> … <P_19000_T> <Q_0169>
<END_FX> … <END_CHAIN> <EOS>
示例摘自外部 V2 programs/0001.json;省略部分参数与效果器,不作为可直接执行的完整程序。
代码 · 核心实现
def __init__(self, token_to_id: dict[str, int]):
if not token_to_id:
raise ValueError("DSP control vocabulary is empty")
ids = list(token_to_id.values())
if len(ids) != len(set(ids)):
raise ValueError("DSP control vocabulary contains duplicate token IDs")
if sorted(ids) != list(range(len(ids))):
raise ValueError("DSP control token IDs must be contiguous from zero")
self.token_to_id = dict(token_to_id)
self.id_to_token = {token_id: token for token, token_id in token_to_id.items()}
self.vocab_size = len(token_to_id)
self.pad_token_id = self._required_id("<PAD>")
self.bos_token_id = self._required_id("<BOS>")
self.eos_token_id = self._required_id("<EOS>")
Main/ai_guitar_pedal/tokenizer/dsp_control_tokenizer.py · L11–L25 ↗ def decode_ids(self, token_ids: Iterable[int]) -> list[str]:
tokens = []
for token_id in token_ids:
token_id = int(token_id)
if token_id not in self.id_to_token:
raise ValueError(f"Unknown DSP control token ID: {token_id}")
tokens.append(self.id_to_token[token_id])
return tokens
Main/ai_guitar_pedal/tokenizer/dsp_control_tokenizer.py · L43–L50 ↗03样本如何连接到目标程序
manifest 的一行将音频路径与已经序列化好的 DSP 程序绑定。
- 输入
- manifest JSONL + program JSON
- 输出
- audio、labels、program_hash、target_length
- 核心函数
- load_program_rows → read_program_token_ids
逻辑 · 如何工作
训练 manifest 逐行读取 audio_filepath 和 target_program_json。相对路径按 manifest 所在目录解析;如果 program 的原始路径不存在,则尝试 program_root / 文件名。音频路径没有同样的文件名回退。
目标取自程序 JSON 的 model_token_ids。默认移除开头的 BOS,要求末尾为 EOS,检查 token ID 是否落在词表范围内。长于 max_target_length 的样本会导致校验失败,不会静默截断。
同一个 program JSON 只需解析一次;加载器缓存其 labels、长度及 SHA-256。文件缺失和字段错误会被汇总报告。数据渲染和将效果器参数转成 model_token_ids 的上游过程不在 V4 内。
代码 · 核心实现
def read_program_token_ids(
program_path: Path,
tokenizer: DSPControlTokenizer,
strip_bos: bool,
) -> List[int]:
payload = json.loads(program_path.read_text(encoding="utf-8"))
token_ids = payload.get("model_token_ids")
if not isinstance(token_ids, list) or not token_ids:
raise ValueError(f"program missing model_token_ids: {program_path}")
out = [int(x) for x in token_ids]
if strip_bos and out and out[0] == tokenizer.bos_token_id:
out = out[1:]
if not out or out[-1] != tokenizer.eos_token_id:
raise ValueError(f"program token sequence must end with EOS: {program_path}")
bad = [x for x in out if x < 0 or x >= tokenizer.vocab_size]
if bad:
raise ValueError(f"program token id out of vocabulary: {program_path}, examples={bad[:10]}")
return out
Main/train_program_seq2seq.py · L255–L272 ↗ cached = program_cache[program_path]
rows.append(
{
"audio": str(audio_path),
"labels": cached["labels"],
"program_hash": cached["program_hash"],
"target_length": cached["target_length"],
}
)
Main/train_program_seq2seq.py · L339–L347 ↗04按完整程序隔离训练与验证
同一 DSP 程序对应的不同音频被放在同一个集合里。
- 输入
- 包含 labels 与 program_hash 的所有 rows
- 输出
- train_rows / eval_rows + split 清单
- 默认配置
- seed = 42;eval_ratio = 0.02
逻辑 · 如何工作
把完整 labels 的整数以逗号连接后做 SHA-256,得到 program_hash。语义等价但 token 序列不同的程序仍会得到不同 hash;分组标准是序列完全一致。
先把相同 hash 的样本放在一组,对排序后的组键按 seed=42 打乱。默认约 2% 的程序组作为验证集,其余作为训练集。条件允许时至少留一个验证组,也至少保留一个训练组;eval_ratio=0 或只有一个组时没有验证集。
划分后分别打乱样本行,并在 checkpoint 实验目录写出 train_split_desensitized.jsonl 与 eval_split_desensitized.jsonl,记录音频路径、hash 和长度。ValidateOnly 在这里结束。
代码 · 核心实现
def token_hash(token_ids: Sequence[int]) -> str:
text = ",".join(str(int(x)) for x in token_ids)
return hashlib.sha256(text.encode("utf-8")).hexdigest()
Main/train_program_seq2seq.py · L250–L252 ↗def split_by_program_hash(
rows: Sequence[Dict[str, Any]],
eval_ratio: float,
seed: int,
) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
if not (0.0 <= eval_ratio < 1.0):
raise ValueError(f"eval_ratio must be in [0,1), got {eval_ratio}")
grouped: Dict[str, List[Dict[str, Any]]] = defaultdict(list)
for row in rows:
grouped[str(row["program_hash"])].append(row)
keys = sorted(grouped)
rng = random.Random(seed)
rng.shuffle(keys)
if eval_ratio <= 0.0 or len(keys) <= 1:
eval_keys = set()
else:
eval_count = int(round(len(keys) * eval_ratio))
eval_count = max(1, eval_count)
eval_count = min(eval_count, len(keys) - 1)
eval_keys = set(keys[:eval_count])
train_rows: List[Dict[str, Any]] = []
eval_rows: List[Dict[str, Any]] = []
for key in keys:
if key in eval_keys:
eval_rows.extend(grouped[key])
else:
train_rows.extend(grouped[key])
rng.shuffle(train_rows)
rng.shuffle(eval_rows)
if not train_rows:
raise ValueError("No train rows after program-hash split.")
return train_rows, eval_rows
Main/train_program_seq2seq.py · L367–L400 ↗05把音频统一为双声道 16 kHz
保留左右声道,取开头 6 秒,长度不足时在末尾补零。
- 输入
- manifest 指定的 wet WAV [T,C]
- 输出
- float32 波形 [2,96000]
- 核心函数
- load_audio_stereo_16k / fix_audio_length
逻辑 · 如何工作
soundfile.read(always_2d=True) 将音频读成 [采样点, 声道]。单声道被复制到左右两路;超过两个声道时只保留前两路。随后转置为 [2,T],分别重采样到 16 kHz。
max_len = max_seconds × 16000,默认是 96,000 个采样点。长音频直接取前 6 秒,短音频在末尾补零。两路使用相同的时间区间,不会先混成单声道。
这一步只读取 wet audio,manifest 中的干音溯源字段不会作为第二个模型输入。当前也没有随机裁剪、响度标准化或数据增强。
代码 · 核心实现
def load_audio_stereo_16k(path: Path, target_sr: int = TARGET_SR) -> np.ndarray:
audio, sr = sf.read(str(path), always_2d=True)
audio = np.asarray(audio, dtype=np.float32)
if audio.size == 0:
raise ValueError(f"Empty audio file: {path}")
if audio.shape[1] == 1:
audio = np.repeat(audio, STEREO_CHANNELS, axis=1)
elif audio.shape[1] < STEREO_CHANNELS:
raise ValueError(f"Expected at least one audio channel, got shape {audio.shape}: {path}")
elif audio.shape[1] > STEREO_CHANNELS:
audio = audio[:, :STEREO_CHANNELS]
stereo = audio.T
if int(sr) != int(target_sr):
stereo = np.stack(
[
librosa.resample(channel, orig_sr=int(sr), target_sr=int(target_sr))
for channel in stereo
],
axis=0,
)
return np.asarray(stereo, dtype=np.float32)
Main/ai_guitar_pedal/phase2_training_utils.py · L153–L175 ↗def fix_audio_length(waveform: np.ndarray, max_len: int) -> np.ndarray:
if waveform.ndim != 2 or waveform.shape[0] != STEREO_CHANNELS:
raise ValueError(f"Expected stereo waveform with shape [2, T], got {waveform.shape}")
if waveform.shape[1] > max_len:
waveform = waveform[:, :max_len]
elif waveform.shape[1] < max_len:
waveform = np.pad(
waveform,
((0, 0), (0, max_len - waveform.shape[1])),
mode="constant",
)
return waveform.astype(np.float32, copy=False)
Main/ai_guitar_pedal/phase2_training_utils.py · L178–L189 ↗06左右声道各提特征,再沿 Mel 维拼接
两个 80-bin Log-Mel 矩阵拼成一个 160-bin 输入。
- 输入
- [2,96000] 波形
- 输出
- [160,3000] float32 特征(默认配置)
- 排列
- 前 80 bins = L;后 80 bins = R
逻辑 · 如何工作
同一个 Whisper feature extractor 逐声道执行时频变换。默认 Base 配置为采样率 16 kHz、FFT 窗口 400 点、hop 160 点、80 个 Mel bins;Log-Mel 压缩频谱幅度动态范围,使模型读取时频结构。
V4 调用 feature extractor 时未设置 max_length 或 padding。核对本地 Base 的 preprocessor_config 与 Transformers 实现后,默认仍会补到 30 秒(480,000 点),每路得到 [80,3000]。因此“取 6 秒音频”并没有同时缩短 Whisper 的输入帧数。
torch.cat(channel_features, dim=0) 沿 Mel 维拼接左右两路,得到 [160,3000]。时间轴仍为 3000 帧;这一步没有混合左右波形,也没有把它们接成 12 秒。
[96000] → padding 480000 → [80,3000]Right[96000] → padding 480000 → [80,3000]Concat dim=0[160,3000]代码 · 核心实现
def stereo_to_features(
feature_extractor: AutoFeatureExtractor,
waveform: np.ndarray,
) -> torch.Tensor:
if waveform.ndim != 2 or waveform.shape[0] != STEREO_CHANNELS:
raise ValueError(f"Expected stereo waveform with shape [2, T], got {waveform.shape}")
channel_features: list[torch.Tensor] = []
for channel in waveform:
features = feature_extractor(
channel,
sampling_rate=TARGET_SR,
return_tensors="pt",
).input_features
channel_features.append(features[0].cpu().to(torch.float32))
if len(channel_features) != STEREO_CHANNELS:
raise ValueError(f"Expected {STEREO_CHANNELS} channel feature tensors")
if channel_features[0].shape != channel_features[1].shape:
raise ValueError(
"Stereo channel feature shapes differ: "
f"{channel_features[0].shape} vs {channel_features[1].shape}"
)
return torch.cat(channel_features, dim=0).to(torch.float32)
Main/ai_guitar_pedal/phase2_training_utils.py · L192–L215 ↗07避免每个 epoch 重复计算频谱
缓存命中时直接读取张量,缺失时计算并保存 .pt 文件。
- 输入
- 音频路径、model_name、max_seconds
- 输出
- CPU 特征张量 + 磁盘 .pt 缓存
- 默认开关
- use_feature_cache / preextract_features = true
逻辑 · 如何工作
缓存目录由模型标识、采样率、秒数和声道模式组成。audio_root 内的音频按相对路径生成缓存名,目录中的 renders 前缀会被规范化;外部音频则使用绝对路径的 SHA-1。audio_root 主要用于这个缓存定位过程。
默认在构建模型之前遍历所有 rows,预提取训练和验证特征。这是固定特征变换,并没有利用验证集去拟合归一化参数。之后 Dataset 能直接读取缓存,减少每轮训练中的 CPU 计算。
写缓存时先保存临时文件,再用 os.replace 替换目标。读取时检查对象为 tensor 且首维为 160,转为 float32。关闭缓存时会在 Dataset 访问样本时重新计算。
代码 · 核心实现
def feature_cache_base_dir(
feature_cache_root: Path,
model_name: str,
max_seconds: float,
) -> Path:
model_id = normalize_model_name_for_cache(model_name)
model_tag = re.sub(r"[^A-Za-z0-9._-]+", "_", model_id).strip("_")
max_tag = f"{max_seconds:.3f}".replace(".", "p")
return feature_cache_root / model_tag / f"sr{TARGET_SR}_max{max_tag}_{AUDIO_CHANNEL_MODE}"
Main/ai_guitar_pedal/phase2_training_utils.py · L298–L306 ↗def load_or_compute_input_features(
wav_path: Path,
feature_extractor: AutoFeatureExtractor,
max_len: int,
use_feature_cache: bool,
cache_path: Path,
save_on_miss: bool,
) -> torch.Tensor:
if use_feature_cache and cache_path.exists():
tensor = torch.load(cache_path, map_location="cpu")
if not isinstance(tensor, torch.Tensor):
raise ValueError(f"Invalid cached tensor format: {cache_path}")
if tensor.ndim < 2 or int(tensor.shape[0]) != STEREO_FEATURE_BINS:
raise ValueError(
f"Cached features are not stereo {STEREO_FEATURE_BINS}-bin tensors: "
f"{cache_path} shape={tuple(tensor.shape)}"
)
return tensor.to(torch.float32)
tensor = compute_input_features(
wav_path=wav_path,
feature_extractor=feature_extractor,
max_len=max_len,
)
if use_feature_cache and save_on_miss:
atomic_torch_save(tensor, cache_path)
return tensor
Main/ai_guitar_pedal/phase2_training_utils.py · L345–L371 ↗def atomic_torch_save(tensor: torch.Tensor, path: Path) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
tmp = path.with_suffix(path.suffix + f".tmp_{os.getpid()}")
torch.save(tensor.cpu(), tmp)
os.replace(tmp, path)
Main/ai_guitar_pedal/phase2_training_utils.py · L328–L332 ↗08把不同长度的目标拼成一个 batch
特征直接堆叠,labels 只补到当前 batch 最长长度。
- 输入
- 单样本 input_features + labels
- 输出
- [B,160,3000] 与 [B,L]
- 忽略标记
- labels padding = -100
逻辑 · 如何工作
ProgramSeq2SeqDataset.__getitem__ 根据索引找到音频,计算缓存路径并加载特征,同时将 program token ID 转为 torch.long。Dataset 不再解析程序语法。
Collator 用 torch.stack 合并固定形状的输入特征。目标程序长度可以不同,因此找到本 batch 的最大长度 L,创建 [B,L] 的 -100 矩阵,再把各条真实 labels 写入前部。
-100 表示监督位置应被忽略。它不属于 3,206 个 DSP token,不能与 PAD ID=0 混同。后续交叉熵和准确率都跳过这些位置。
代码 · 核心实现
def __getitem__(self, idx: int) -> Dict[str, torch.Tensor]:
row = self.rows[idx]
wav_path = Path(str(row["audio"])).expanduser().resolve()
cache_path = feature_cache_path_for_audio(
wav_path=wav_path,
audio_root=self.audio_root,
cache_base=self.cache_base,
)
input_features = load_or_compute_input_features(
wav_path=wav_path,
feature_extractor=self.feature_extractor,
max_len=self.max_len,
use_feature_cache=self.use_feature_cache,
cache_path=cache_path,
save_on_miss=self.use_feature_cache,
)
return {
"input_features": input_features,
"labels": torch.tensor(row["labels"], dtype=torch.long),
}
Main/train_program_seq2seq.py · L442–L461 ↗class Seq2SeqCollator:
def __init__(self, pad_token_id: int, label_pad_token_id: int = -100) -> None:
self.pad_token_id = int(pad_token_id)
self.label_pad_token_id = int(label_pad_token_id)
def __call__(self, items: List[Dict[str, torch.Tensor]]) -> Dict[str, torch.Tensor]:
input_features = torch.stack([item["input_features"] for item in items], dim=0)
labels = [item["labels"] for item in items]
max_len = max(int(label.shape[0]) for label in labels)
padded = torch.full(
(len(labels), max_len),
self.label_pad_token_id,
dtype=torch.long,
)
for idx, label in enumerate(labels):
padded[idx, : label.shape[0]] = label
return {"input_features": input_features, "labels": padded}
Main/train_program_seq2seq.py · L464–L480 ↗09把语音模型的输出改为 DSP 程序
复用兼容的 encoder-decoder 权重,替换目标词表并扩展输出位置容量。
- 输入
- Whisper Base 预训练权重 + DSP tokenizer
- 输出
- DSP 词表维度 3206;目标位置 ≥ 768
- 骨架
- Base:d_model 512;encoder/decoder 各 6 层
逻辑 · 如何工作
读取模型配置,将 vocab_size 设为 3206;PAD、BOS、EOS 和 decoder 起始 token 全部使用 DSP 协议。decoder 最大位置容量设为“原容量和请求上限中的较大者”,默认从原 Base 的 448 扩到 768。
清除语音转写任务的 forced decoder IDs 和 suppress token 列表,使 decoder 按 DSP token 学习。调用 from_pretrained(ignore_mismatched_sizes=True) 加载兼容参数;尺寸改变的 token embedding、输出投影或位置 embedding 不会原样继承。
模型随后进行立体声首层适配。训练时 use_cache=False,按配置开启 gradient checkpointing,以反向传播时重新计算部分激活换取内存。V4 没有额外加入 LoRA 或冻结 encoder 的训练策略。
[B,160,3000] → conv1 [B,512,3000] → conv2 [B,512,1500] → encoder [B,1500,512] → decoder [B,L,512] → [B,L,3206]维度基于 Whisper Base;encoder、decoder Transformer 和输出投影的具体运算由 Transformers 依赖实现。
代码 · 核心实现
def build_model(args: argparse.Namespace, tokenizer: DSPControlTokenizer) -> WhisperForConditionalGeneration:
config = WhisperConfig.from_pretrained(args.model_name)
requested_target_positions = int(args.max_target_length)
if requested_target_positions <= 0:
raise ValueError(f"max_target_length must be positive, got {requested_target_positions}")
config.max_target_positions = max(
requested_target_positions,
int(getattr(config, "max_target_positions", 0) or 0),
)
config.vocab_size = int(tokenizer.vocab_size)
config.pad_token_id = int(tokenizer.pad_token_id)
config.bos_token_id = int(tokenizer.bos_token_id)
config.eos_token_id = int(tokenizer.eos_token_id)
config.decoder_start_token_id = int(tokenizer.bos_token_id)
config.forced_decoder_ids = None
config.suppress_tokens = []
config.begin_suppress_tokens = []
model = WhisperForConditionalGeneration.from_pretrained(
args.model_name,
config=config,
ignore_mismatched_sizes=True,
)
adapt_whisper_model_for_stereo(model)
model.config.use_cache = False
if hasattr(model, "generation_config"):
model.generation_config.pad_token_id = int(tokenizer.pad_token_id)
model.generation_config.bos_token_id = int(tokenizer.bos_token_id)
model.generation_config.eos_token_id = int(tokenizer.eos_token_id)
model.generation_config.decoder_start_token_id = int(tokenizer.bos_token_id)
model.generation_config.forced_decoder_ids = None
model.generation_config.suppress_tokens = []
model.generation_config.begin_suppress_tokens = []
model.generation_config.max_length = int(args.max_target_length)
if args.gradient_checkpointing:
model.gradient_checkpointing_enable()
return model
Main/train_program_seq2seq.py · L507–L543 ↗10如何让第一层接受左右声道
扩展 conv1 的输入通道,左右两半分别继承原权重的一半。
- 输入
- 原 conv1:80 个输入通道
- 输出
- 新 conv1:160 个输入通道
- 初始化
- W_left = W_right = 0.5 × W_old
逻辑 · 如何工作
如果模型第一层已经是 160 通道,直接更新配置后返回。若是原始 80 通道,创建一个输入通道数为 160 的 Conv1d,保留输出通道、卷积核、stride、padding、设备与数据类型。
将原卷积权重复制到前 80 和后 80 个输入通道,各乘 0.5,bias 保持不变。在左右特征相同的情况下,新层输出与原单声道卷积一致,避免简单复制带来的响应翻倍。
最后替换 encoder.conv1,并将 config.num_mel_bins 设为 160。训练后左右两半的权重可以分别学习;初始化本身并不代表模型已经学会空间关系。
代码 · 核心实现
def adapt_whisper_model_for_stereo(model: torch.nn.Module) -> None:
conv1 = whisper_encoder_first_conv(model)
if conv1.in_channels == STEREO_FEATURE_BINS:
setattr(model.config, "num_mel_bins", STEREO_FEATURE_BINS)
return
if conv1.in_channels != MONO_FEATURE_BINS:
raise RuntimeError(
f"Unsupported Whisper conv1 input channels: {conv1.in_channels}; "
f"expected {MONO_FEATURE_BINS} or {STEREO_FEATURE_BINS}"
)
new_conv = torch.nn.Conv1d(
in_channels=STEREO_FEATURE_BINS,
out_channels=conv1.out_channels,
kernel_size=conv1.kernel_size,
stride=conv1.stride,
padding=conv1.padding,
dilation=conv1.dilation,
groups=conv1.groups,
bias=conv1.bias is not None,
padding_mode=conv1.padding_mode,
device=conv1.weight.device,
dtype=conv1.weight.dtype,
)
with torch.no_grad():
new_conv.weight[:, :MONO_FEATURE_BINS, :] = conv1.weight * 0.5
new_conv.weight[:, MONO_FEATURE_BINS:, :] = conv1.weight * 0.5
if conv1.bias is not None and new_conv.bias is not None:
new_conv.bias.copy_(conv1.bias)
candidates = [
getattr(model, "encoder", None),
getattr(getattr(model, "model", None), "encoder", None),
getattr(getattr(model, "whisper", None), "encoder", None),
]
for encoder in candidates:
if encoder is not None and getattr(encoder, "conv1", None) is conv1:
encoder.conv1 = new_conv
break
setattr(model.config, "num_mel_bins", STEREO_FEATURE_BINS)
Main/ai_guitar_pedal/phase2_training_utils.py · L231–L270 ↗11用真实前缀预测下一个程序 token
损失是 token 交叉熵,BOS 由依赖内部右移 labels 时补入。
- 输入
- 音频特征 + 真值 labels [B,L]
- 输出
- logits [B,L,3206] + 标量 loss
- 实现归属
- V4 配置 / Transformers 内部 forward
逻辑 · 如何工作
V4 将 input_features 和 labels 交给 Trainer。Whisper 的 forward 在没有显式 decoder 输入时将 labels 右移一位,并在起点加入 BOS。decoder 在每个位置接收真实的历史 token,这就是 teacher forcing。
默认 labels 已移除开头 BOS,保留结束 EOS,因此学习目标是 <CHAIN> … <EOS>,输入前缀是 <BOS> <CHAIN> …。模型既要学习程序内容,也要学习什么时候结束。
输出投影把每个位置的隐藏状态变为 3206 类分数。依赖中的 CrossEntropyLoss() 对真实 ID 计算负对数概率并忽略 -100;结构符号和参数 token 一同参与。V4 没有通过 DSP 渲染音频计算可微重建损失。
<CHAIN> <FX_19000> … <END_CHAIN> <EOS>decoder input<BOS> <CHAIN> … preceding_token <END_CHAIN>仅对 labels ≠ -100 的位置求平均
代码 · 核心实现
def shift_tokens_right(input_ids: torch.Tensor, pad_token_id: int, decoder_start_token_id: int):
"""
Shift input ids one token to the right.
"""
shifted_input_ids = input_ids.new_zeros(input_ids.shape)
shifted_input_ids[:, 1:] = input_ids[:, :-1].clone()
shifted_input_ids[:, 0] = decoder_start_token_id
if pad_token_id is None:
raise ValueError("self.model.config.pad_token_id has to be defined.")
# replace possible -100 values in labels by `pad_token_id`
shifted_input_ids.masked_fill_(shifted_input_ids == -100, pad_token_id)
return shifted_input_ids if labels is not None:
if labels.shape[1] > self.max_target_positions:
raise ValueError(
f"Labels' sequence length {labels.shape[1]} cannot exceed the maximum allowed length of {self.max_target_positions} tokens."
)
if decoder_input_ids is None and decoder_inputs_embeds is None:
decoder_input_ids = shift_tokens_right(
labels, self.config.pad_token_id, self.config.decoder_start_token_id
)
outputs: Seq2SeqModelOutput = self.model(
input_features,
attention_mask=attention_mask,
decoder_input_ids=decoder_input_ids,
encoder_outputs=encoder_outputs,
decoder_attention_mask=decoder_attention_mask,
past_key_values=past_key_values,
decoder_inputs_embeds=decoder_inputs_embeds,
decoder_position_ids=decoder_position_ids,
use_cache=use_cache,
**kwargs,
)
lm_logits = self.proj_out(outputs.last_hidden_state)
loss = None
if labels is not None:
loss_fct = CrossEntropyLoss()
# move labels to correct device to enable PP
labels = labels.to(lm_logits.device)
loss = loss_fct(lm_logits.view(-1, self.config.vocab_size), labels.reshape(-1))12配置恢复与训练中的监控回调
恢复路径在训练开始前确定;内存与暂停回调在训练步骤结束时运行。
- 输入
- 内存状态 / PAUSE / resume 参数
- 输出
- 告警、pause_ckpt 或恢复路径
- 优先级
- 显式 checkpoint → pause_ckpt → 最新常规 checkpoint
逻辑 · 如何工作
这一环节横跨训练循环:先注册回调、选择恢复来源,再进入下一步 Trainer.train;运行期间的内存检查与暂停保存由 Trainer 触发。流程图中的编号表示配置与阅读顺序。
MemoryWarningCallback 在第一个正 step 检查,之后按默认 25 步间隔采样 RAM 和 VRAM。达到 90% 阈值时输出提醒,同类提醒默认至少间隔 200 步;它不会自动降低 batch 或暂停。
在 checkpoint 实验目录创建空文件 PAUSE 后,回调会在一个优化步结束时把模型、训练参数、optimizer、scheduler、可选 AMP scaler、Trainer 状态和随机数状态写到 pause_ckpt,成功后删除 PAUSE 并请求停止训练循环。
下次启动优先使用显式指定的 checkpoint;没有指定且开启自动恢复时,先尝试 pause_ckpt,再尝试编号最大的常规 checkpoint。要求 config 的 num_mel_bins 为 160。Trainer.train(resume_from_checkpoint=...) 才会按依赖支持恢复训练状态。
代码 · 核心实现
def on_step_end(self, args, state, control, **kwargs):
step = int(getattr(state, "global_step", 0))
if not self._should_check(step):
return control
ram = get_ram_usage()
if ram is not None and float(ram["ratio"]) >= self.ram_threshold:
if (step - self._last_ram_warn_step) >= self.repeat_warning_steps:
self._last_ram_warn_step = step
print(
"[MemWarn][RAM] "
f"step={step}, usage={float(ram['ratio']) * 100:.1f}%, "
f"used={_format_bytes_gb(int(ram['used']))}, "
f"total={_format_bytes_gb(int(ram['total']))}"
)
for item in get_vram_usage():
device = int(item["device"])
ratio = float(item["ratio"])
if ratio < self.vram_threshold:
continue
last_step = self._last_vram_warn_step.get(device, -10**9)
if (step - last_step) < self.repeat_warning_steps:
continue
self._last_vram_warn_step[device] = step
print(
"[MemWarn][VRAM] "
f"step={step}, cuda:{device}, usage={ratio * 100:.1f}%, "
f"used={_format_bytes_gb(int(item['used']))}, "
f"total={_format_bytes_gb(int(item['total']))}"
)
return control
Main/ai_guitar_pedal/memory_guard.py · L116–L147 ↗ def on_step_end(self, args, state, control, **kwargs):
if self._pause_handled or not self.pause_flag_file.exists():
return control
print("[PAUSE] detected -> saving checkpoint then stopping.")
try:
if self._trainer is None:
raise RuntimeError("Pause callback has no trainer reference.")
if self._trainer.args.should_save:
self._save_pause_checkpoint()
print(f"[PAUSE] checkpoint saved: {self.pause_ckpt_dir}")
if self.remove_pause_flag and self.pause_flag_file.exists():
self.pause_flag_file.unlink()
print(f"[PAUSE] flag removed: {self.pause_flag_file}")
else:
print("[PAUSE] non-saving process received pause signal, stopping.")
except Exception as exc:
print(f"[PAUSE] failed to save pause checkpoint: {exc}")
self._pause_handled = True
control.should_training_stop = True
return control
Main/ai_guitar_pedal/phase2_training_utils.py · L476–L497 ↗def resolve_resume_checkpoint(
args: argparse.Namespace,
pause_ckpt_dir: Path,
) -> Tuple[Path | None, str]:
if str(args.resume_from_checkpoint).strip():
resume_path = Path(str(args.resume_from_checkpoint)).expanduser().resolve()
if not resume_path.exists():
raise FileNotFoundError(f"resume_from_checkpoint not found: {resume_path}")
if not is_stereo_checkpoint(resume_path):
raise ValueError(
f"Explicit resume checkpoint is not stereo-compatible: {resume_path} "
f"(num_mel_bins={checkpoint_input_feature_bins(resume_path)}, "
f"expected={STEREO_FEATURE_BINS})"
)
return resume_path, "explicit"
if not bool(args.auto_resume):
return None, "disabled"
if is_trainer_checkpoint(pause_ckpt_dir) and is_stereo_checkpoint(pause_ckpt_dir):
return pause_ckpt_dir, "pause_ckpt"
if is_trainer_checkpoint(pause_ckpt_dir):
print(
f"[Resume] skipping mono/incompatible pause checkpoint: {pause_ckpt_dir} "
f"(num_mel_bins={checkpoint_input_feature_bins(pause_ckpt_dir)})"
)
latest_ckpt = find_latest_trainer_checkpoint(Path(str(args.output_dir)))
if latest_ckpt is not None and is_stereo_checkpoint(latest_ckpt):
return latest_ckpt, "latest_checkpoint"
if latest_ckpt is not None:
print(
f"[Resume] skipping mono/incompatible latest checkpoint: {latest_ckpt} "
f"(num_mel_bins={checkpoint_input_feature_bins(latest_ckpt)})"
)
return None, "none"
Main/ai_guitar_pedal/phase2_training_utils.py · L115–L150 ↗13交给 Trainer 执行反向传播和更新
默认每次前向 1 个样本,累积 32 次梯度后完成一次优化步。
- 输入
- 模型、Dataset、Collator、TrainingArguments
- 输出
- 更新后的权重与常规 checkpoint
- 默认配置
- 3 epochs;learning_rate=3e-5;batch=1 × 32
逻辑 · 如何工作
Trainer 负责 batch 迭代、前向 loss、反向传播和优化器更新。单卡且完整累积时,batch size 1 × gradient accumulation 32 相当于每次更新使用 32 个样本的梯度;“step”统计使用优化步,不是单条样本次数。
默认训练 3 个 epochs;设置正数 max_steps 时改由步数控制。CUDA 可用才开启 fp16。开启 gradient checkpointing 能减少激活保存量,同时增加部分计算。优化器及 scheduler 等未在 V4 显式改写的设置采用所安装 TrainingArguments 版本的默认值。
默认每 250 步保存一次、评估一次,常规 checkpoint 最多保留 5 个。每 10 步记录日志;report_to=[] 禁用了集成上报,所以 runs 或 TensorBoard 目录存在并不等于已写出 TensorBoard 事件。
代码 · 核心实现
kwargs: Dict[str, Any] = {
"output_dir": str(Path(args.output_dir).expanduser().resolve()),
"logging_dir": str(Path(args.log_dir).expanduser().resolve()),
"per_device_train_batch_size": int(args.per_device_train_batch_size),
"per_device_eval_batch_size": int(args.per_device_eval_batch_size),
"gradient_accumulation_steps": int(args.gradient_accumulation_steps),
"learning_rate": float(args.learning_rate),
"num_train_epochs": float(args.num_train_epochs),
"max_steps": int(args.max_steps),
"fp16": bool(args.fp16 and torch.cuda.is_available()),
"save_strategy": strategy,
"logging_strategy": "steps",
"logging_steps": 10,
"save_total_limit": max(1, int(args.save_total_limit)),
"report_to": [],
"run_name": sanitize_experiment_name(args.experiment_name),
"remove_unused_columns": False,
"seed": int(args.seed),
"gradient_checkpointing": bool(args.gradient_checkpointing),
"dataloader_num_workers": int(args.num_workers),
"dataloader_pin_memory": True,
"eval_accumulation_steps": max(1, int(args.eval_accumulation_steps)),
}
if int(args.max_steps) <= 0:
kwargs.pop("max_steps")
params = inspect.signature(TrainingArguments.__init__).parameters
Main/train_program_seq2seq.py · L557–L583 ↗ model = build_model(args, tokenizer)
trainer = Trainer(
model=model,
args=build_training_args(args, has_eval=eval_dataset is not None),
train_dataset=train_dataset,
eval_dataset=eval_dataset,
data_collator=Seq2SeqCollator(pad_token_id=tokenizer.pad_token_id),
compute_metrics=compute_token_metrics if eval_dataset is not None else None,
preprocess_logits_for_metrics=preprocess_logits_for_metrics
if eval_dataset is not None
else None,
)
Main/train_program_seq2seq.py · L749–L760 ↗ resume_ckpt, resume_source = resolve_resume_checkpoint(args, pause_ckpt_dir)
if resume_ckpt is not None:
print(f"[Resume] source={resume_source}, resume_from_checkpoint={resume_ckpt}")
trainer.train(resume_from_checkpoint=str(resume_ckpt))
else:
print(f"[Resume] source={resume_source}, starting from scratch")
trainer.train()
Main/train_program_seq2seq.py · L779–L785 ↗14评价预测 token,并选择最佳权重
token_acc 与 seq_exact 衡量提供真实历史 token 时的预测质量。
- 输入
- 验证前向 logits 与 labels
- 输出
- eval_token_acc / eval_seq_exact / eval_loss
- 最佳模型标准
- eval_token_acc 越大越好
逻辑 · 如何工作
preprocess_logits_for_metrics 在每个位置对词表维取 argmax,只保留预测 ID,减少评估时缓存全部 logits 的内存开销。compute_token_metrics 对齐 predictions 与 labels,并忽略 -100 位置。
token_acc 统计所有有效 token 中预测正确的比例;seq_exact 要求一条样本的所有有效位置均正确,再计算这类样本的比例。EOS 和结构 token 都参与这些指标。
存在验证集时设置 load_best_model_at_end=True,按照 eval_token_acc 最大值选择最佳 checkpoint;训练返回后再次 evaluate。没有验证集时不做上述选择。
seq_exact = 所有有效位置均正确的样本数 / 样本数
代码 · 核心实现
def preprocess_logits_for_metrics(logits, labels):
if isinstance(logits, tuple):
logits = logits[0]
return torch.argmax(logits, dim=-1)
Main/train_program_seq2seq.py · L483–L486 ↗def compute_token_metrics(eval_pred) -> Dict[str, float]:
preds = np.asarray(eval_pred.predictions)
labels = np.asarray(eval_pred.label_ids)
if preds.shape != labels.shape:
preds = preds.reshape(labels.shape)
mask = labels != -100
if not np.any(mask):
return {"token_acc": 0.0, "seq_exact": 0.0}
token_acc = float(np.mean(preds[mask] == labels[mask]))
seq_ok = []
for pred_row, label_row, mask_row in zip(preds, labels, mask):
seq_ok.append(bool(np.all(pred_row[mask_row] == label_row[mask_row])))
return {
"token_acc": token_acc,
"seq_exact": float(np.mean(seq_ok)) if seq_ok else 0.0,
}
Main/train_program_seq2seq.py · L489–L504 ↗ kwargs["load_best_model_at_end"] = True
kwargs["metric_for_best_model"] = "eval_token_acc"
kwargs["greater_is_better"] = True
Main/train_program_seq2seq.py · L593–L595 ↗15保存能用于后续加载的模型资产
模型、特征配置、DSP 词表和运行摘要共同描述这次训练结果。
- 输入
- 训练返回后的模型 + extractor + vocab
- 输出
- Main/storage/artifacts/exports/实验名/
- 追踪记录
- run_summary.json
逻辑 · 如何工作
调用 trainer.save_model 写出模型配置和权重;保存 feature extractor 配置,并复制当前 DSP 词表。模型实际使用 160-bin 输入,但 feature extractor 仍按单路 80-bin 配置工作,加载使用时必须执行同样的左右声道处理。
run_summary.json 记录目标定义、数据位置、样本量、program hash 数量、目标长度、指标、超参数、恢复来源、特征缓存路径和有效环境变量。它是回看训练设定与结果的入口。
常规 checkpoint / pause_ckpt 用于恢复训练状态;export 用于后续模型加载。当前 V4 的交付止于导出,没有把预测 token 还原成可运行 DSP、渲染音频或集成实时踏板的后续执行代码。
代码 · 核心实现
if eval_dataset is not None:
metrics = trainer.evaluate()
print(
f"[Eval] eval_token_acc={metrics.get('eval_token_acc', 0.0):.6f}, "
f"eval_seq_exact={metrics.get('eval_seq_exact', 0.0):.6f}"
)
else:
metrics = {}
export_dir.mkdir(parents=True, exist_ok=True)
trainer.save_model(str(export_dir))
feature_extractor.save_pretrained(str(export_dir))
shutil.copy2(vocab_path, export_dir / "dsp_control_vocab.json")
Main/train_program_seq2seq.py · L787–L799 ↗ summary = {
"objective": "audio_to_dsp_control_program_tokens",
"desensitized_from_preset_number": True,
"uses_preset_number_as_label": False,
"target_source_field": "target_program_json.model_token_ids",
"manifest": str(manifest_path),
"vocab": str(vocab_path),
"program_root": str(program_root),
"vocab_size": int(tokenizer.vocab_size),
"audio_root": str(audio_root),
"total_rows": int(len(rows)),
"train_rows": int(len(train_rows)),
"eval_rows": int(len(eval_rows)),
"unique_program_hashes": int(program_hash_count),
"train_program_hashes": int(len({r["program_hash"] for r in train_rows})),
"eval_program_hashes": int(len({r["program_hash"] for r in eval_rows})),
"target_length": {
"min": int(min(lengths)),
"max": int(max(lengths)),
"mean": float(np.mean(lengths)),
},
"metrics": metrics,
"training_config": {
"model_name": str(args.model_name),
"audio_channel_mode": AUDIO_CHANNEL_MODE,
"input_feature_bins": int(STEREO_FEATURE_BINS),
"max_seconds": float(args.max_seconds),
"max_target_length": int(args.max_target_length),
"strip_bos_from_labels": bool(args.strip_bos_from_labels),
"eval_ratio": float(args.eval_ratio),
"num_train_epochs": float(args.num_train_epochs),
"max_steps": int(args.max_steps),
"checkpoint_strategy": str(args.checkpoint_strategy),
"save_steps": int(args.save_steps),
"eval_steps": int(args.eval_steps),
"save_total_limit": int(args.save_total_limit),
"per_device_train_batch_size": int(args.per_device_train_batch_size),
"per_device_eval_batch_size": int(args.per_device_eval_batch_size),
"gradient_accumulation_steps": int(args.gradient_accumulation_steps),
"learning_rate": float(args.learning_rate),
"gradient_checkpointing": bool(args.gradient_checkpointing),
"fp16": bool(args.fp16 and torch.cuda.is_available()),
"gpu_mem_frac": float(args.gpu_mem_frac),
"auto_resume": bool(args.auto_resume),
"resume_from_checkpoint": str(resume_ckpt) if resume_ckpt is not None else "",
"resume_source": str(resume_source),
},
"feature_cache_base": str(cache_base),
"e_root": str(e_root),
"output_dir": str(output_dir),
"log_dir": str(log_dir),
"export_dir": str(export_dir),
"pause_flag_file": str(pause_flag_file),
"pause_ckpt_dir": str(pause_ckpt_dir),
"env": env_effective,
}
Main/train_program_seq2seq.py · L801–L856 ↗ (export_dir / "run_summary.json").write_text(
json.dumps(summary, indent=2, ensure_ascii=False),
encoding="utf-8",
)
print(f"Parameter sequence model exported to: {export_dir}")
Main/train_program_seq2seq.py · L857–L861 ↗运行命令与阅读索引
以下命令均从 AI_Guitar_Pedal_V4 项目根目录执行。先核对路径,再校验数据,最后开始训练。
1. 环境与检查
python -m venv Main/.venv
Main/.venv/Scripts/python.exe -m pip install -r Main/requirements.txt
powershell -ExecutionPolicy Bypass -File .\scripts\train.ps1 -DryRun
powershell -ExecutionPolicy Bypass -File .\scripts\train.ps1 -ValidateOnly
未建立 V4 环境时,启动器会尝试数据工程的 Main/.venv,再回退到系统 Python。requirements.txt 当前未锁版本;迁移到新环境时需检查 PyTorch/CUDA 和 Transformers 的实际版本。
2. 新实验与指定数据
powershell -ExecutionPolicy Bypass -File .\scripts\train.ps1 `
-ExperimentName Phase2_V4_Experiment_0002 `
-DataProjectRoot ..\AI_Guitar_Pedal_V2
# 从已有 export 的权重开始新实验(不恢复旧 optimizer)
# 先解析绝对路径;启动器会切换到 Main 后启动 Python
$exportPath = (Resolve-Path .\Main\storage\artifacts\exports\Phase2_V4_Experiment_0002).Path
powershell -ExecutionPolicy Bypass -File .\scripts\train.ps1 `
-ExperimentName Phase2_V4_FromExport `
-ModelName $exportPath
上述 ModelName 为路径示例,须先存在相应 export。实验目录相同时默认尝试自动恢复;想单独开始一轮实验,应使用新的 ExperimentName。MaxSteps、MaxSeconds、MaxTargetLength 等可通过启动器覆盖。
3. 暂停与继续
# 仅当这一实验正在训练时使用;名称要与当前实验一致
New-Item -ItemType File -Force `
.\Main\storage\artifacts\checkpoints\Phase2_V4_Experiment_0002\PAUSE
# 之后重新运行原命令,默认自动恢复
powershell -ExecutionPolicy Bypass -File .\scripts\train.ps1 `
-ExperimentName Phase2_V4_Experiment_0002 `
-ResumeFromPauseCkpt
默认参数速查
| 设置 | 默认值 | 对应含义 |
|---|---|---|
| ModelName | openai/whisper-base | 初始化模型及 extractor |
| MaxSeconds / MaxTargetLength | 6 / 768 | 取前 6 秒;labels 超过 768 则拒绝 |
| EvalRatio / seed | 0.02 / 42 | 按程序组划分 |
| TrainBatch / GradientAccumulation | 1 / 32 | 单卡完整累计时有效 batch 32 |
| LearningRate / NumTrainEpochs | 3e-5 / 3 | 学习率与默认轮数 |
| SaveSteps / EvalSteps | 250 / 250 | 按优化步保存与评估 |
| SaveTotalLimit | 5 | 常规 checkpoint 保留上限 |
| GradientCheckpointing / fp16 | true / CUDA 可用时 true | 内存与计算精度策略 |
| 缓存 / 预提取 / 自动恢复 | true / true / true | 可在启动器中关闭 |
本地阅读与后续维护
直接打开 docs/site/dist/index.html 即可离线阅读。更新训练源码或讲解内容后,运行 python docs/site/build.py 刷新代码片段、行号和源码快照,再执行 python docs/site/verify.py 检查内部链接与快照一致性。
网页由 docs/site/dist/ 内的静态文件组成,支持离线阅读和 portfolio 子目录托管。所有资源使用相对链接,无后端、外部字体或 CDN。
实现范围
- V4 已实现:数据校验与划分、立体声特征、缓存、Whisper 程序 token 训练、teacher-forced 指标、暂停恢复与导出。
- 外部提供:音频渲染、DSP 程序序列化和训练数据;默认来自 V2。
- 当前缺少:自由生成评估、完整程序语法约束、DSP 反量化执行、渲染音频对比、实时推理 UI。
本次整理验证 · 2026-09-16
使用新入口执行完整 ValidateOnly,退出码 0;未启动 GPU 训练。数据加载与标签校验耗时 27.7 秒,结果如下:
| 项目 | 实际结果 |
|---|---|
| 总样本 / 唯一程序 hash | 60,600 / 1,510 |
| 训练样本 / 训练程序 | 59,400 / 1,480 |
| 验证样本 / 验证程序 | 1,200 / 30 |
| 目标 token 长度(移除 BOS 后) | 最短 14;最长 685;平均 169.11 |
| 新旧启动入口一致性 | 41 个参数一致;52 行有效路径与参数输出一致;DryRun 退出码均为 0 |