工程文档 / Phase 2音频 → DSP 控制程序
AI GUITAR PEDAL / V4 / WHISPER BASE

从一段吉他音频,
到一串 DSP 控制指令。

沿着数据流阅读这个训练工程:样本如何配对,立体声如何进入 Whisper,程序 token 如何成为学习目标,以及模型如何被评估和保存。每个环节先讲逻辑,再看实际代码。

任务 Audio-to-program Seq2Seq说明范围 V4 Phase 2 训练流程 15 个环节
3,206DSP 词表大小
160左右声道 Mel bins 合并
6 s默认波形裁剪长度
768最大目标 token 长度

训练流程图

按编号阅读 · 点击进入对应逻辑与代码
DryRun → Python 解析路径后、数据扫描前结束ValidateOnly → 完成数据划分后结束12 恢复与回调 ↔ 13 训练循环:内存 / PAUSE

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 工程中。
PROJECT MAP

文件结构与职责

以“入口、训练实现、固定资源、运行产物、说明文档”组织阅读。训练代码保持在 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 改用完整的数据工程。

STEP 01 / 准备

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 ↗
STEP 02 / 数据

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 协议决定。

符号组数量意义
结构符号6PAD、BOS、EOS、CHAIN、END_CHAIN、END_FX
FX / CAT / P141 / 32 / 1,740效果器 / 类别 / 参数名
B / I / E2 / 63 / 221布尔 / 整数 / 枚举值
Q1,001Q_0000 … Q_1000 量化值
<BOS> <CHAIN> <FX_19000> <CAT_GATE>
<P_19000_A> <Q_0000> … <P_19000_T> <Q_0169>
<END_FX> … <END_CHAIN> <EOS>

示例摘自外部 V2 programs/0001.json;省略部分参数与效果器,不作为可直接执行的完整程序。

代码 · 核心实现

DSPControlTokenizer.__init__
    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 ↗
DSPControlTokenizer.decode_ids
    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 ↗
STEP 03 / 数据

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 内。

代码 · 核心实现

read_program_token_ids
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 ↗
STEP 04 / 数据

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 在这里结束。

代码 · 核心实现

token_hash
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 ↗
split_by_program_hash
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 ↗
STEP 05 / 特征

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 中的干音溯源字段不会作为第二个模型输入。当前也没有随机裁剪、响度标准化或数据增强。

代码 · 核心实现

load_audio_stereo_16k
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 ↗
fix_audio_length
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 ↗
STEP 06 / 特征

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 秒。

Left[96000] → padding 480000 → [80,3000]Right[96000] → padding 480000 → [80,3000]Concat dim=0[160,3000]

代码 · 核心实现

stereo_to_features
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 ↗
STEP 07 / 特征

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 访问样本时重新计算。

代码 · 核心实现

feature_cache_base_dir
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 ↗
load_or_compute_input_features
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 ↗
atomic_torch_save
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 ↗
STEP 08 / 数据装配

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 混同。后续交叉熵和准确率都跳过这些位置。

代码 · 核心实现

ProgramSeq2SeqDataset.__getitem__
    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 ↗
Seq2SeqCollator
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 ↗
STEP 09 / 模型

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 依赖实现。

代码 · 核心实现

build_model
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 ↗
STEP 10 / 模型

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。训练后左右两半的权重可以分别学习;初始化本身并不代表模型已经学会空间关系。

Conv([L; R]) = 0.5 · W ∗ L + 0.5 · W ∗ R + b

代码 · 核心实现

adapt_whisper_model_for_stereo
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 ↗
STEP 11 / 学习目标

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 渲染音频计算可微重建损失。

labels<CHAIN> <FX_19000> … <END_CHAIN> <EOS>decoder input<BOS> <CHAIN> … preceding_token <END_CHAIN>
Loss = mean[-log p(yₜ | audio, y<ₜ)]
仅对 labels ≠ -100 的位置求平均

代码 · 核心实现

Transformers 5.12.1 · shift_tokens_right
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
Transformers 5.12.1 · WhisperForConditionalGeneration.forward
        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))
STEP 12 / 训练控制(贯穿训练)

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=...) 才会按依赖支持恢复训练状态。

代码 · 核心实现

MemoryWarningCallback.on_step_end
    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 ↗
PauseSaveAndStopCallback.on_step_end
    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 ↗
resolve_resume_checkpoint
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 ↗
STEP 13 / 训练

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 事件。

代码 · 核心实现

Trainer 超参数
    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 ↗
STEP 14 / 验证

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。没有验证集时不做上述选择。

token_acc = 正确的有效 token 数 / 有效 token 总数
seq_exact = 所有有效位置均正确的样本数 / 样本数

代码 · 核心实现

preprocess_logits_for_metrics
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 ↗
compute_token_metrics
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 ↗
STEP 15 / 产物

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 ↗
PRACTICAL REFERENCE

运行命令与阅读索引

以下命令均从 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

默认参数速查

设置默认值对应含义
ModelNameopenai/whisper-base初始化模型及 extractor
MaxSeconds / MaxTargetLength6 / 768取前 6 秒;labels 超过 768 则拒绝
EvalRatio / seed0.02 / 42按程序组划分
TrainBatch / GradientAccumulation1 / 32单卡完整累计时有效 batch 32
LearningRate / NumTrainEpochs3e-5 / 3学习率与默认轮数
SaveSteps / EvalSteps250 / 250按优化步保存与评估
SaveTotalLimit5常规 checkpoint 保留上限
GradientCheckpointing / fp16true / 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 秒,结果如下:

项目实际结果
总样本 / 唯一程序 hash60,600 / 1,510
训练样本 / 训练程序59,400 / 1,480
验证样本 / 验证程序1,200 / 30
目标 token 长度(移除 BOS 后)最短 14;最长 685;平均 169.11
新旧启动入口一致性41 个参数一致;52 行有效路径与参数输出一致;DryRun 退出码均为 0