2026-09-14 【音色评估】项目拆解(一点五)前置基础知识全解

27800 字
139 分钟
2026-09-14 【音色评估】项目拆解(一点五)前置基础知识全解

系列定位:本系列共 5 篇文章(第 1 篇前置调研 → 第 2 篇环境跑通 → 第 3 篇代码拆解 → 第 4 篇调试改参 → 第 5 篇复盘沉淀)。
本篇是第 1 篇与第 2 篇之间的”知识补丁”:第 1 篇告诉你”项目是什么”,但里面出现的采样定理、STFT、梅尔刻度、Fisher 判别、ViT、MAE、BCE、mAP、ffbank、patch、AMP、DDP 等概念,如果基础不牢,第 2 篇一开始就会卡住。
本篇目标:把这些底层知识一次性讲透,看完不需要再查其他资料。每个概念都按「定义 → 直觉 → 公式 → 在本项目中哪里用到 → 常见误解」五段式展开,并配 15 个可直接运行的最小实验。
约定:f 表示频率(Hz),sr 表示采样率,N 表示帧长/样本数,T 表示时长(秒)。所有代码默认在项目自带的 .venv_html 环境下运行。

目录#

第一部分 数学地基

  1. 三角函数、正弦波与相位
  2. 复数与欧拉公式
  3. 向量、矩阵与形状规则
  4. 均值、方差、协方差(含 unbiased 的坑)
  5. 导数、偏导、梯度与链式法则
  6. 概率与分布(正态、Beta、sigmoid)
  7. 对数与分贝
  8. 线性代数进阶:投影、特征值与广义特征值

第二部分 数字信号处理地基

  1. 连续信号到离散信号:采样定理与混叠
  2. 量化、位深与量化噪声
  3. 时域与频域:傅里叶思想的直觉
  4. DFT:公式、频率 bin 与单边谱
  5. FFT:为什么快,快在哪
  6. 时间分辨率 vs 频率分辨率的权衡
  7. 窗函数、频谱泄漏与分帧
  8. 幅度谱 / 功率谱 / 能量谱 / 对数谱
  9. 常用谱特征:谱质心、谱带宽、谱滚降、过零率、谱平坦度
  10. 滤波、预加重与卷积定理
  11. 采样率转换(重采样)与抗混叠

第三部分 音频工程地基

  1. 声压、dBFS 与响度
  2. PCM、位序与声道交错
  3. WAV/RIFF 文件结构逐字节
  4. 容器与编码:MP3 / AAC / Opus 原理
  5. 为什么浏览器录音是 WebM+Opus,后端为什么读不了
  6. 录音链路与常见音质问题

第四部分 语音声学地基

  1. 语音产生机理:声源—滤波器理论
  2. 基频 F0、音高与 F0 估计方法
  3. 谐波结构与谐噪比 HNR
  4. 共振峰、LPC 与 Burg 法
  5. 声门闭合商与”虚/实”的声学机制
  6. 语音的准平稳假设与分帧长度惯例

第五部分 听觉与特征工程地基

  1. 梅尔刻度的来历与公式
  2. 梅尔滤波器组的构造(5 步详解)
  3. log-mel / fbank / MFCC 的关系
  4. Kaldi fbank 与 librosa mel 的差异
  5. 特征归一化:AudioSet 全局均值方差与 ×2 的秘密
  6. 本项目核心特征:12 频段能量占比

第六部分 机器学习地基

  1. 监督学习范式与数据划分
  2. 过拟合、欠拟合与正则化
  3. 损失函数总览:MSE / CE / BCE
  4. 优化算法:GD → SGD → Momentum → Adam → AdamW
  5. 学习率调度:warmup、cosine、线性缩放规则
  6. 特征缩放与标准化
  7. 判别分析:Fisher / LDA 完整推导与项目实现对照
  8. 逻辑回归、线性分类边界与 sigmoid
  9. 评价指标:混淆矩阵到 mAP 与 d-prime
  10. 阈值、边界与分位数过滤

第七部分 深度学习地基

  1. 从感知机到 MLP
  2. 反向传播与计算图(手推)
  3. 激活函数与梯度问题
  4. 卷积与感受野
  5. 归一化层:BatchNorm vs LayerNorm
  6. 残差连接与初始化
  7. 正则化与数据增强
  8. 注意力机制与位置编码
  9. Transformer 与 ViT
  10. Swin 与窗口注意力
  11. 自监督学习与 MAE
  12. 微调策略与逐层学习率衰减
  13. 混合精度 AMP
  14. 分布式训练 DDP

第八部分 音频深度学习地基

  1. 音频表示的选择
  2. AST 与 AudioMAE
  3. 2D 时频掩码
  4. 片段切分与聚合、多标签与多任务
  5. 评估口径:mAP vs 组内准确率

第九部分 Web 与前端地基

  1. HTTP 协议与 multipart/form-data
  2. 同源策略、CORS 与安全上下文
  3. Flask、WSGI 与请求生命周期
  4. gunicorn 与反向代理
  5. 前端录音:getUserMedia / MediaRecorder
  6. Web Audio API 与音频解码
  7. Blob / ArrayBuffer / TypedArray
  8. JS 异步与 fetch
  9. Canvas 与 Chart.js

第十部分 工程与运维地基

  1. 进程、线程、GIL 与并发
  2. 虚拟环境、wheel 与依赖管理
  3. 依赖版本坑的完整回溯
  4. systemd、日志与排错方法论
  5. 资源估算

第十一部分 概念辨析速查表(24 组)

第十二部分 15 个最小实验(可直接跑)

第十三部分 自测题库(40 题带答案要点)

第十四部分 延伸资料清单


第一部分 数学地基#

1. 三角函数、正弦波与相位#

定义:正弦波是最基本的周期信号

x(t)=Asin⁡(2πft+φ)x(t) = A\sin(2\pi f t + \varphi)
  • A 振幅(决定响度);
  • f 频率(Hz,决定音高);
  • φ 相位(弧度,决定”从波形的哪个位置开始”);
  • 周期 T = 1/f。

为什么重要:傅里叶变换的本质就是”用一堆不同频率、振幅、相位的正弦波去拼出任意信号”。所以”频域”就是”每个正弦分量的振幅与相位表”。

在本项目:所有的频谱、能量占比、共振峰,都是”不同频率正弦分量强度”的度量。

常见误解:以为频率只有”一个值”。真实人声是复合音:基频 + 大量谐波 + 噪声成分叠加,频域上是一整片分布。


2. 复数与欧拉公式#

定义:虚数单位 j(工程上用 j,数学上用 i)满足 j² = −1。欧拉公式:

ejθ=cos⁡θ+jsin⁡θe^{j\theta} = \cos\theta + j\sin\theta

几何直觉:e^{jθ} 是复平面单位圆上的一个点,角度为 θ。让 θ 随时间线性增长 θ = 2πft,这个点就在圆上匀速旋转,它在实轴上的投影就是一个余弦波:

ℜ{ej2πft}=cos⁡(2πft)\Re\{e^{j2\pi f t}\} = \cos(2\pi f t)

为什么 DFT 用复数:用一个旋转向量 e^{-j2πkn/N} 去”套”信号的每个频率,通过内积测出该频率分量的振幅和相位——复数一部到位,比分别算 sin/cos 更优雅。因此 DFT 输出 X[k] 是复数:

  • 模 |X[k]| = 该频率的幅度(这是我们最关心的);
  • 幅角 ∠X[k] = 该频率的相位(音频任务里通常丢弃)。

常见误解:以为”复数”意味着物理上有虚数能量。复数只是数学记账工具,物理量始终是实数。


3. 向量、矩阵与形状规则#

点积(内积):a·b = Σᵢ aᵢbᵢ,衡量两个向量的”方向一致程度”。本项目物理量化的核心运算就是点积:

np.sum(weights * energy) # 等价于 weights @ energy

矩阵乘法:(m, n) @ (n, p) = (m, p) —— 内维必须相等,结果取外维。这条规则是读懂深度学习报错的第一钥匙(本项目那两个 bug 都是形状不匹配)。

广播(broadcasting):numpy/torch 允许形状不同的数组自动扩展运算,例如 (3,1) * (1,4) → (3,4)。规则是”从右往左对齐,维度为 1 或缺失可广播”。

本项目实战:报错 operands could not be broadcast together with shapes (12,) (13,) 就是”权重长度 12 与特征长度 13 无法广播”——两个维度都不为 1 且不相等,直接失败。


4. 均值、方差、协方差(含 unbiased 的坑)#

均值:μ = (1/n)Σxᵢ,数据的”重心”。

方差:σ² = (1/n)Σ(xᵢ−μ)²,数据的”分散程度”。标准差 σ = √σ²,与原数据同量纲。

关键坑:总体方差 vs 样本方差

版本分母用途
总体方差(biased)n已知全部数据时;深度学习里对 batch 内统计量常用这个
样本方差(unbiased)n−1用样本估计总体时(贝塞尔校正)

PyTorch 的 Tensor.var() 默认 unbiased=True(分母 n−1),而本项目 FisherLoss 显式写了 unbiased=False:

var0 = outputs[mask0].var(unbiased=False) # 分母用 n,batch 内统计更一致

为什么必须显式指定:batch 很小(比如每类只有 4 个样本)时,n 与 n−1 的差别高达 25%,会让损失值随 batch 大小漂移,训练不稳定。

协方差:cov(x,y) = E[(x−μx)(y−μy)],衡量两个变量的线性相关方向。协方差矩阵 Σ(或记 S)描述多维数据的”形状”(各维方差 + 两两相关)。Fisher/LDA 的核心就是用协方差矩阵的逆去”白化”数据再找判别方向。


5. 导数、偏导、梯度与链式法则#

导数:f'(x) = lim_{h→0}(f(x+h)−f(x))/h,即”变化率”,几何上是切线斜率。

偏导:多元函数对某一个变量求导(其他视为常数),记 ∂f/∂xᵢ。

梯度:把所有偏导拼成向量 ∇f = (∂f/∂x₁, …, ∂f/∂xₙ)。梯度的方向是函数上升最快的方向,所以梯度下降要”减去”梯度:

θ←θ−η∇L(θ)\theta \leftarrow \theta - \eta\nabla L(\theta)

链式法则:复合函数求导要逐层相乘

dzdx=dzdy⋅dydx\frac{dz}{dx} = \frac{dz}{dy}\cdot\frac{dy}{dx}

这就是反向传播的全部数学本质:从损失开始,逐层向前乘局部导数。

在本项目:frenquence_check/model.py 里作者手推了 Fisher 损失的梯度(见第 44 节),PyTorch 版本则由 autograd 自动完成。


6. 概率与分布#

  • 随机变量 / 期望 E[·]:期望就是”加权平均”。
  • 正态分布 N(μ, σ²):钟形曲线,自然界最普遍。为什么深度学习假设它:中心极限定理(大量独立小因素之和趋于正态)。
  • Beta 分布 Beta(a, b):取值在 [0,1],常用来采”混合比例 λ”。本项目 mixup 用 np.random.beta(10, 10) → 集中在 0.5 附近(a=b 越大越集中于 0.5),意思是”混合时两条音频权重差不多”;roll-mag 增强也用 np.random.beta(10,10)+0.5 生成 0.5~1.5 的幅度缩放。
  • 条件概率与贝叶斯:P(A|B) = P(B|A)P(A)/P(B)。分类器输出的”概率”实质是对后验的估计。
  • sigmoid(logistic)函数:
σ(z)=11+e−z∈(0,1),σ′(z)=σ(z)(1−σ(z))\sigma(z) = \frac{1}{1+e^{-z}}\in(0,1),\qquad \sigma'(z)=\sigma(z)(1-\sigma(z))

它把任意实数压到 (0,1),当概率用;导数形式极简,是反向传播友好的原因。

  • softmax:
softmax(z)i=ezi∑jezj\text{softmax}(z)_i = \frac{e^{z_i}}{\sum_j e^{z_j}}

把一组实数变成”和为 1”的分布(多分类用)。

常见误解:sigmoid 输出不是”真正的概率”,只是校准前的分数;模型置信度可能偏高,需要温度缩放等后处理,但本项目未做校准。


7. 对数与分贝#

  • 对数的作用:把乘除变加减、把大动态范围压缩成小范围。
  • 分贝(dB):对功率/能量用 10·log10(P/P₀),对幅度用 20·log10(A/A₀)(因为功率 ∝ 幅度²)。
  • 为什么音频常用 dB:人耳感知近似对数(韦伯-费希纳定律),且音频动态范围极大(从耳语到摇滚可达 10⁶ 倍),用 dB 只需几十个数。
  • 本项目:fbank 就是对数梅尔能量(log-mel);librosa.amplitude_to_db 用于画频谱图;数据集的 mean=-4.2677, std=4.5690 也是对数值的统计。
  • dBFS:数字音频里以满量程为 0 dB,dBFS = 20log10(A/A_max),0 dBFS 是最大不失真电平,超过即削波(clipping)。

常见误解:把”dB 是线性单位”搞混——dB 是相对量,必须说明参考基准(dB SPL、dBFS、dBu…)。


8. 线性代数进阶:投影、特征值与广义特征值#

投影:向量 x 在方向 w 上的投影长度是 (wᵀx)/‖w‖。判别分析就是”把高维数据投影到一条直线上,让两类尽量分开”。

特征值/特征向量:若 Av = λv,则 v 是 A 的特征向量(方向在变换下不变),λ 是特征值(缩放倍数)。

广义特征值问题:

Sbw=λSwwS_b w = \lambda S_w w

其中 S_b 是类间散度矩阵、S_w 是类内散度矩阵。解出的 w 就是 Fisher/LDA 的最优判别方向。两类情形有闭式解:

w∗∝Sw−1(μ1−μ0)w^* \propto S_w^{-1}(\mu_1-\mu_0)

几何直觉:先用 S_w^{-1} 把数据”拉成球形”(消除各方向方差不均),再取两类中心的连线方向。

矩阵求逆的直觉:A^{-1} 是”撤销 A 的变换”。若 S_w 奇异(某方向方差为 0,常见于特征冗余或样本太少),需要加正则项 S_w + εI——本项目 FisherLoss 分母加 1e-6、sklearn 的 LDA 用 shrinkage 都是这个道理。



第二部分 数字信号处理地基#

9. 连续信号到离散信号:采样定理与混叠#

采样:以间隔 T = 1/sr 抽取连续信号的值:

xs[n]=x(nT)x_s[n] = x(nT)

数学上等价于”乘以冲激串”:

xs(t)=x(t)⋅∑n=−∞∞δ(t−nT)x_s(t) = x(t)\cdot\sum_{n=-\infty}^{\infty}\delta(t-nT)

频域后果:时域相乘 = 频域卷积。冲激串的频谱仍是冲激串(间隔 sr),与 X(f) 卷积的结果就是把原始频谱以 sr 为周期复制:

原始频谱: [======X(f)======] 最高频率 f_max
采样后: ...X(f-sr) | X(f) | X(f+sr)... 每 sr 复制一份

奈奎斯特定理:只有 f_max < sr/2,复制出来的频谱才不会互相重叠,才有可能无失真还原。 sr/2 称为奈奎斯特频率。16 kHz 采样 → 上限 8 kHz。

混叠(aliasing):若 f > sr/2,该分量会”折叠”到 sr − f 的位置,伪装成一个低频。例:10 kHz 的信号用 16 kHz 采样,会听成 16−10 = 6 kHz。

抗混叠滤波:实际降采样前必须先低通滤波到 sr/2 以下,否则高频会折叠成假低频。librosa.load(..., sr=16000) 内部就是”低通 + 重采样”,不是简单抽点。

为什么本项目统一 16 kHz:人声中决定音色的基频、前几阶共振峰、以及区分”虚实”的噪声成分主要在 8 kHz 内;16 kHz 既够用又能省一半算力。更重要的是模型是在 16 kHz 数据上训练的,推理必须一致。


10. 量化、位深与量化噪声#

量化:把连续幅度映射到有限个离散电平。位深 B 比特 → 2^B 个电平。

量化噪声模型:把舍入误差近似为均匀分布的白噪声,其功率为 Δ²/12(Δ 为量化步长)。对满量程正弦,可推出经典结论:

SNRdB≈6.02B+1.76\text{SNR}_{\text{dB}} \approx 6.02B + 1.76
  • 16 bit → 约 98 dB(语音标准);
  • 8 bit → 约 50 dB(明显噪声);
  • 每个 bit 换 6 dB 动态范围。

本项目:前端转码固定写 16 bit(WAV 头偏移 34 处写 16)——够用且文件不太大(16 kHz 单声道 16 bit = 32 KB/s,10 秒 ≈ 320 KB)。

削波:若幅度超过量程被截断,会产生大量高次谐波(听感”破音”),这是录音质量问题的第一大来源,会直接污染高频特征(影响”明暗”判断)。


11. 时域与频域:傅里叶思想的直觉#

核心命题(傅里叶):任何”足够规矩”的信号,都能写成不同频率正弦/余弦的加权和:

x(t)=∑kAkcos⁡(2πfkt+φk)x(t) = \sum_{k} A_k\cos(2\pi f_k t + \varphi_k)
  • 时域看波形(横轴时间),频域看成分(横轴频率);
  • 两者是同一信息的两面,可以互相转换(傅里叶变换对),信息量不变(丢相位除外)。

直觉类比:一段音乐在时域是”一条抖动的线”,在频域是”一份乐器清单 + 每个乐器的音量”。音色分析关心的是”清单比例”,这正是频域更适合的原因。


12. DFT:公式、频率 bin 与单边谱#

离散傅里叶变换(DFT) 对 N 个采样点:

X[k]=∑n=0N−1x[n] e−j2πkn/N,k=0,1,…,N−1X[k]=\sum_{n=0}^{N-1}x[n]\,e^{-j2\pi kn/N},\qquad k=0,1,\dots,N-1

逆变换:

x[n]=1N∑k=0N−1X[k] e+j2πkn/Nx[n]=\frac{1}{N}\sum_{k=0}^{N-1}X[k]\,e^{+j2\pi kn/N}

每个 k 对应什么频率:

fk=k⋅srNf_k = \frac{k\cdot sr}{N}

N = n_fft = 2048、sr = 16000 时,f_k = 7.8125·k Hz;k = 128 → 1000 Hz。

对称性与单边谱:实信号的 DFT 满足共轭对称 X[N−k] = X*[k],负频率没有新信息。所以实信号只需看前 N/2+1 个点 → 输出长度 1025(这正是本项目 librosa.stft 输出形状 (1025, 帧数) 的来源)。

单边幅度谱的正确缩放:除 DC(k=0)与奈奎斯特点外,要把幅度乘 2 才能对应真实振幅——librosa 的 stft 默认不缩放,但做”能量占比”时缩放是全局的,归一化后自动抵消,所以项目直接用 |X|² 求和再除以总和完全没问题(这是个值得理解的小细节)。


13. FFT:为什么快,快在哪#

DFT 的代价:直接算需要 O(N²) 次复数乘法。N = 2048 时约 420 万次。

FFT(快速傅里叶变换) 利用旋转因子的周期性/对称性做分治:把长度 N 的 DFT 拆成两个长度 N/2 的 DFT(偶数点、奇数点),递归下去:

X[k]=E[k]+WNkO[k],X[k+N/2]=E[k]−WNkO[k]X[k] = E[k] + W_N^k O[k],\qquad X[k+N/2] = E[k] - W_N^k O[k]

其中 W_N^k = e^{-j2πk/N}。复杂度降到 O(N log N)——2048 点只需约 2.2 万次,快约 190 倍。这就是”实时频谱分析”可行的原因。

工程细节:

  • n_fft 取 2 的幂(512/1024/2048)最省事;
  • 补零(zero-padding) 能让频率”点更密”(插值),但不提高真实分辨率(真实分辨率由窗长决定)——很多人在这里误解;
  • 实数 FFT(rFFT)只算一半,省一半算力(librosa/torch 内部都用 rFFT)。

14. 时间分辨率 vs 频率分辨率的权衡#

频率分辨率(能区分多近的两个频率):

Δf≈srn_fft\Delta f \approx \frac{sr}{n\_fft}

时间分辨率(能分辨多短的变化):

Δt≈n_fftsr (窗长), 帧移 hop 决定时间采样密度\Delta t \approx \frac{n\_fft}{sr}\ (\text{窗长}),\ \text{帧移}\ hop\ \text{决定时间采样密度}

两者反向变化:窗越长 → 频率越精细、时间越模糊。这本质上是时频不确定性原理(Δt·Δf ≥ 常数),与海森堡原理同源(数学上都是傅里叶对偶性)。

本项目的选择:n_fft=2048(≈128 ms 窗,Δf≈7.8 Hz)、hop=512(32 ms)。语音处理惯例是”25 ms 窗、10 ms 帧移”,项目的窗更长,因为音色分析关心稳定的频谱形状而不是快速的音素变化。

同类对比:深度学习侧 fbank 用 25 ms 窗、10 ms 帧移(Kaldi 默认),因为模型需要更细的时间结构。


15. 窗函数、频谱泄漏与分帧#

为什么必须加窗:DFT 隐含假设”这 N 个点是一个周期信号”,但实际取的是信号的任意一段 → 截断处不连续,相当于乘了一个矩形窗。矩形窗的频谱是 sinc 函数,会把一个纯音的能量”涂抹”到邻近频点,这叫频谱泄漏(leakage)。

窗函数的作用:让帧两端平滑衰减到 0,减小截断突变,从而压低旁瓣(泄漏)。

窗主瓣宽度旁瓣特点
矩形窗最窄最高(−13 dB)频率分辨好但泄漏严重
Hann 窗中等低(−31 dB)通用首选(本项目 STFT 默认、Kaldi fbank 也用 hanning)
Hamming中等更低(−43 dB)旁瓣更低但衰减慢
Blackman宽极低需要极低泄漏时用

Hann 窗公式:

w[n]=0.5(1−cos⁡2πnN−1)w[n]=0.5\left(1-\cos\frac{2\pi n}{N-1}\right)

代价:加窗损失了帧边缘的信息 → 用重叠(overlap) 补偿,重叠率通常 50%~75%。

分帧与帧数公式:

  • center=False(不补零):帧数 = 1 + ⌊(L − n_fft)/hop⌋
  • center=True(librosa.stft 的默认值,两端各补 n_fft/2):帧数 = 1 + ⌊L/hop⌋

以 10 秒 16 kHz(L = 160 000)、n_fft = 2048、hop = 512 为例:

  • center=False → 1 + (160000−2048)/512 = 309 帧;
  • center=True(librosa 默认)→ 1 + 160000/512 = 313 帧。

Kaldi fbank 的帧数近似为 时长/帧移(16 kHz、10 ms 帧移、10 秒 → 约 1000 帧)。


16. 幅度谱 / 功率谱 / 能量谱 / 对数谱#

设 STFT 输出复数矩阵 X:

名称定义用途
幅度谱abs(X)(复数取模)可视化、谱特征
功率谱abs(X)²(或归一化到单位带宽)谱能量密度
能量谱abs(X)²(本项目用法)频段能量占比
对数谱20·log10(abs(X))压缩动态范围、便于观察
log-mel梅尔滤波后再取对数深度学习输入(fbank)

能量谱与功率谱的区别:严格来说功率谱要对时长/带宽做归一化(Pa²/Hz),但在”占比”这类比值计算里,是否归一化不影响结果(同一常数约掉)。项目代码注释里两者混用,实际计算的是 |X|² 的求和——理解这一点就不会被术语绊住。

为什么常丢掉相位:人耳对相位不敏感(对短时相位尤其如此),而幅度谱已足够支撑音色/内容判别;相位重建是另一条研究线(Griffin-Lim、神经声码器)。


17. 常用谱特征#

特征公式/定义声学含义本项目是否用
谱质心C = Σ f_k p_k / Σ p_k频谱”重心”,越高越亮✅ 用(占比加权,×0.001)
谱带宽sqrt(Σ (f_k − C)² p_k)频谱”胖瘦”❌
谱滚降累积能量到 85%/95% 对应的频率高频延伸程度❌
过零率 ZCR波形穿零次数 / 时长粗糙度、清浊音近似判据❌
谱平坦度几何均值 / 算术均值越接近 1 越像噪声❌
LF/HF 比低频能量和 / 高频能量和厚薄/明暗的粗判据✅ 用(1000 Hz 分界,×0.1)
总能量Σ abs(X)²响度✅ 用(×1e-7)

为什么项目只用其中几个:作者做过特征筛选实验(filter_np() 里注释保留了多套列组合),最终固化的 12 段占比 + 3 个综合量是”够用且稳定”的最小集合。复现时不要盲目加特征,加了反而可能过拟合。


18. 滤波、预加重与卷积定理#

滤波器:按频率选择性地放大/衰减。低通、高通、带通、带阻。

卷积定理(理解滤波的关键):

x(t)∗h(t) ⟺ X(f)⋅H(f)x(t)*h(t)\ \Longleftrightarrow\ X(f)\cdot H(f)

即时域卷积 = 频域相乘。所以”滤波”既可以理解为时域的加权滑动平均,也可以理解为频域上乘一条曲线。

预加重(pre-emphasis):语音频谱天然呈 −6 dB/oct 下降(高频能量低),预加重用一阶高通

y[n]=x[n]−αx[n−1],α≈0.97y[n]=x[n]-\alpha x[n-1],\quad \alpha\approx 0.97

把高频抬起来让频谱更平坦,便于建模;对应地,解码端要”去加重”还原。本项目 parselmouth.to_formant_burg(pre_emphasis_from=50) 里的参数就是预加重起点。

差分与倒谱:语音里常用”一阶/二阶差分”(Δ、ΔΔ)描述特征随时间的变化率;倒谱(cepstrum)是”对幅度谱再取对数、再做逆变换”,能把”声源(基频)“与”滤波器(共振峰)“分离——MFCC 就是这个思路的产物。


19. 采样率转换(重采样)与抗混叠#

降采样(downsampling):sr 44100 → 16000。必须先低通到新奈奎斯特频率(8 kHz)以下,否则折叠。 升采样(upsampling):插值 + 低通,填补中间样本。

librosa 的实现:librosa.load(path, sr=16000) 内部优先用 soxr(高质量、快)、其次 resampy/scipy.signal.resample_poly。不同实现会有微小数值差异,但对”能量占比”这类统计特征影响极小。

工程要点:

  1. 只在最前端做一次重采样,之后全链路保持 16 kHz;
  2. 不要用”每隔 k 个点取一个”这种朴素抽取(会严重混叠);
  3. 采样率是模型输入约定的一部分(fbank 的 sample_frequency 参数),改了会让梅尔滤波器对应错频率。


第三部分 音频工程地基#

20. 声压、dBFS 与响度#

  • 声压级(SPL):20log10(p/p₀),p₀ = 20 μPa;正常对话约 60 dB SPL。
  • dBFS:数字域相对满量程的分贝,0 dBFS 为上限。
  • 峰值电平(Peak):瞬时最大值,防削波看它。
  • RMS:均方根,反映平均能量。
  • LUFS:带频率加权(K-weighting)与门限的响度标准(流媒体统一到 −14 或 −16 LUFS)。

本项目为什么绕开响度问题:特征用能量占比(归一化),等于”把响度变量除掉”,因此同一人小声/大声说同一句话,音色分数应当接近——这是设计上的抗增益优势,也是可以在汇报里强调的亮点。(但它并非完全免疫:削波、底噪、麦克风频响仍会干扰。)


21. PCM、位序与声道交错#

  • PCM 数据布局:16 bit 有符号整数,小端序(低位字节在前)。写 WAV 时用 DataView.setInt16(offset, value, true),最后的 true 就是小端。
  • 声道交错(interleaved):立体声按 L1 R1 L2 R2 … 排列。本项目转单声道(左右取平均),因为音色评估针对单一说话人,且能减少一半数据量。
  • float vs int:librosa.load 默认返回 float32 且归一化到 [−1, 1];写盘时 soundfile 会按目标格式转成 int16。

22. WAV/RIFF 文件结构逐字节#

WAV 是 RIFF 容器的一种,结构是”块(chunk)链”:

RIFF chunk:
"RIFF" | size(4B) | "WAVE"
├─ fmt chunk: "fmt " | size(4B)=16 | format(2B) | channels(2B) | sampleRate(4B)
│ | byteRate(4B) | blockAlign(2B) | bitsPerSample(2B)
└─ data chunk: "data" | size(4B) | PCM 采样数据…

规模:byteRate = sr × channels × bits/8;blockAlign = channels × bits/8(每帧字节数);dataSize = 帧数 × blockAlign。 16 kHz 单声道 16 bit → byteRate = 32000 B/s,10 秒 = 320 000 B。

为什么常说是”44 字节头”:当 fmt 块为最简 16 字节、且后面紧跟 data 时,头部总长 = 12(RIFF)+ 8+16(fmt)+ 8(data 头)= 44。若含 LIST/fact 等扩展块,头会更长——健壮的解析器不应硬编码 44(本项目自己生成的 WAV 恰好是标准 44 字节)。

本项目代码对照(main.js::audioBufferToWavBlob):逐字段写入的就是上表,其中 view.setUint32(4, 36 + dataSize, true) 对应”RIFF 块大小 = 文件总长 − 8”。


23. 容器与编码:MP3 / AAC / Opus 原理#

有损压缩的共同原理(心理声学):

  1. 频域掩蔽:强音附近的弱音听不见 → 可以少分配比特;
  2. 同时掩蔽 / 时域掩蔽:前后短时间内的弱信号也可丢弃;
  3. 听觉阈值:绝对听阈以下的成分直接丢。

各自特点:

编码容器常见关键点典型码率
MP3.mp3较老,MDCT + 心理声学模型,兼容性最好128–320 kbps
AAC.m4a/.mp4/.aacMP3 的继任者,同码率音质更好96–256 kbps
Opus.webm/.ogg/.opus现代低延迟编码,语音/音乐通吃,浏览器 WebRTC 首选24–128 kbps

关键工程属性:

  • 有损:解码后波形 ≠ 原始波形。对”能量占比”这类统计特征影响不大,但高频截止(如 16 kbps Opus 可能只保留到 8 kHz 以下)会截断高频 → 直接影响”明暗”判断。所以评估系统最好要求上传无损 WAV,或至少中等以上码率。
  • 编码延迟/前导:MP3 有约 26 ms 编码延迟,AAC/Opus 也有 framing 延迟;精确对齐分析时要注意。
  • VBR/CBR:变码率音质更好但文件时长/大小关系不固定。

24. 为什么浏览器录音是 WebM+Opus,后端为什么读不了#

链路:getUserMedia 拿到麦克风流 → MediaRecorder 按浏览器默认编码录制 → Chrome/Edge/Firefox 通常选 WebM 容器 + Opus 编码(Safari 可能给 MP4/AAC)。

后端为什么失败:

  • soundfile(封装 libsndfile)支持的容器有限:WAV、FLAC、OGG、AIFF、MP3(较新版本),不包括 WebM;
  • WebM 是 Matroska 家族,需要解封装器 + Opus 解码器,libsndfile 都不具备 → 报 Format not recognised;
  • librosa 捕获该异常后回退 audioread,而 audioread 需要外部 ffmpeg / GStreamer 可执行文件 → 服务器没装就 NoBackendError。

三种解决思路对比(本项目选了第 1 种):

方案做法优缺点
① 前端转码(项目采用)decodeAudioData 解码 → 手写 WAV 上传后端零依赖、最稳;前端多几行代码,文件变大
② 服务器装 ffmpegapt install ffmpeg,让 audioread 工作改动小;增加依赖与运维成本
③ 输入限定格式只允许上传 WAV/MP3用户体验差(不能直接录音)

25. 录音链路与常见音质问题#

一条完整录音链路:声源 → 空气传播 → 麦克风(换能 + 频响)→ 前置放大(增益)→ ADC(采样 + 量化)→ 编码/传输 → 解码 → 分析。

问题成因对音色特征的影响
削波增益过大,超过 0 dBFS产生高次谐波 → “明暗”偏高、“虚实”偏实
底噪大麦克风/前置噪声高频噪声 → 判”虚”、“暗”
直流偏置ADC 或电路问题0 Hz 附近能量异常(代码里 waveform - waveform.mean() 就是去直流)
频率响应不平手机麦/廉价 USB 麦低频衰减 → 判”薄”(因此跨设备比较需谨慎)
采样率不匹配前端未重采样梅尔滤波器频率错位,特征整体偏移
房间混响反射声叠加频谱被”涂抹”,共振峰变宽

这段是实验设计要写进方法论的部分:评估前应统一录音设备与距离,否则测的是”设备 + 音色”的混合量。这也是本项目在汇报/论文里必须交代的局限性。



第四部分 语音声学地基#

26. 语音产生机理:声源—滤波器理论#

Source-Filter Theory(语音学最核心的模型):

气流(肺) → 声带振动(声源) → 声道形状(滤波器) → 唇鼻辐射 → 语音
产生基频与谐波 决定共振峰 高频提升
  • 声源决定基频 F0 与谐波结构(音高、以及”嗓音是否沙哑”);
  • 滤波器(声道形状,由舌、唇、软腭等控制)决定共振峰(决定元音,也影响音色);
  • 辐射(唇端)近似一阶高通,让高频略微提升。

为什么这个模型重要:

  1. 它解释了”音高(F0)“与”音色(共振峰包络 + 谐波幅度)“是可分离的两件事——同一个人唱不同音高,音色仍可辨识;
  2. 它指导了特征设计:要描述音色,就看频谱包络与谐波幅度分布,而不是只看基频;
  3. 倒谱/MFCC 就是”把声源与滤波器解耦”的数学手段。

清音 vs 浊音:

  • 浊音(元音、鼻音、部分辅音):声带振动,频谱有清晰谐波结构(能量集中在 F0 的整数倍);
  • 清音(s、sh、f、t、k 等):声带不振动,气流摩擦/爆破,频谱更像宽带噪声。 本项目分析的一般是朗读/播报的长段语音,以浊音为主,因此”谐波结构强弱”是有效判别依据。

27. 基频 F0、音高与 F0 估计方法#

  • 基频 F0:声带振动的基本频率,决定听感音高。男声约 80–180 Hz,女声约 160–300 Hz,儿童更高。
  • **音高(pitch)**是主观感知量,F0 是物理量,两者近似成正比但不完全等同(受响度、谐波结构影响)。

F0 估计的三大类方法:

方法原理特点
自相关(ACF)找波形与自身延迟后的最大相似度简单,易受倍频/半频错误影响
YIN / 倒谱(cepstrum)在倒谱域找峰(对应 F0 的”节奏”)更稳健,Praat 常用
深度学习方法直接回归 F0(CREPE 等)最准,需要模型

倒谱求 F0 的思路(理解 MFCC 的前置):

  1. 取对数幅度谱 log|X(f)|;
  2. 对它做逆 FFT → 得到”倒谱” c[τ];
  3. 谐波在倒谱上会形成”τ = 1/F0”处的尖峰 → 找到它即得 F0。

为什么倒谱能分离声源与滤波器:对数把”相乘”变”相加”(log(声源 × 滤波) = log声源 + log滤波),声源(谐波梳状)与滤波(平滑包络)在对数谱上各占不同”节奏”,逆变换后落在不同时间/倒频率位置。

本项目:F0 没有被显式计算,但”厚薄”维度看 0–1350 Hz 的能量分布,实际上间接反映基频与低次谐波的强弱(F0 高且低频能量少 → 偏”薄”)。


28. 谐波结构与谐噪比 HNR#

谐波结构:浊音频谱的能量集中在 F0 的整数倍处,形成”梳状”谱。

谐噪比(HNR, Harmonics-to-Noise Ratio):

HNRdB=10log⁡10PharmonicPnoise\text{HNR}_{\text{dB}} = 10\log_{10}\frac{P_{\text{harmonic}}}{P_{\text{noise}}}

估算方法(Praat 的做法,理解即可):

  1. 自相关函数 r(τ),周期处(τ = 1/F0)出现峰;
  2. HNR = 10log10(r_max/(1−r_max))。

声学含义:

状态HNR听感
声带闭合充分、气息控制好高(>20 dB)“实”、“结实”、“有芯”
气声多、声带闭合不足低(<10 dB)“虚”、“飘”、“漏气”
沙哑/病理嗓音低“粗糙”、“嘶”

本项目的实现差异(务必注意):代码里没有真的计算 HNR,“虚实”维度用的是中高频段(50–4280 Hz)12 段能量占比的线性组合作为 HNR 的代理特征(proxy)。逻辑是:气声/噪声成分多 → 中高频(尤其非谐波位置)能量占比上升 → 加权分偏移。写论文/汇报时应当表述为”以频段能量分布作为 HNR 与声门闭合程度的代理特征”,不能声称计算了 HNR。


29. 共振峰、LPC 与 Burg 法#

共振峰(Formant):声道作为共鸣管的谐振频率,在频谱包络上表现为峰,记作 F1、F2、F3…

共振峰典型范围主要与什么相关
F1200–900 Hz舌位高低(舌高则 F1 低)
F2700–2500 Hz舌位前后(舌前则 F2 高)
F32000–3500 Hz唇形圆展、卷舌
带宽(BW)50–300 Hz声道阻尼/损耗,越窄越”共鸣感强”

元音三角:F1–F2 平面上不同元音占据不同区域(如 /i/ 高 F2 低 F1,/a/ 高 F1 低 F2),这是语音识别与病理嗓音分析的基础。

LPC(线性预测编码)推导思路:

把声道建模为全极点滤波器:

H(z)=11−∑p=1Papz−pH(z)=\frac{1}{1-\sum_{p=1}^{P}a_p z^{-p}}

当前样本可由前 P 个样本线性预测:

x^[n]=∑p=1Papx[n−p]\hat{x}[n]=\sum_{p=1}^{P}a_p x[n-p]

最小化预测误差 e[n] = x[n] − x̂[n] 的能量,即可解出系数 a_p(一组线性方程,用 Levinson-Durbin 递推求解),再由 H(z) 的极点得到共振峰频率与带宽。

Burg 法:LPC 的一种求解方式,不显式求自相关,而是最小化前向与后向预测误差之和。优点是短数据稳定、频率估计精确,Praat 的 to_formant_burg() 就是它。

本项目参数的含义(get_fre.py::get_format):

sound.to_formant_burg(time_step=0.01, # 每 10 ms 分析一帧
max_number_of_formants=5,
maximum_formant=6000, # 最高搜索到 6 kHz(女声/儿童需要更高)
window_length=0.025, # 25 ms 分析窗(语音惯例)
pre_emphasis_from=50) # 50 Hz 起做预加重

取 F1–F3 的中位数作为代表值(抗离群),并把频率与带宽各 ×1e-4 缩放后拼入特征。注意:部署版没有使用共振峰特征,它只出现在科研脚本里(return 被注释切换)——复现时别多算。


30. 声门闭合商与”虚/实”的声学机制#

  • 声门:喉部两片声带之间的开口;说话时周期性开合。
  • 声门闭合商(CQ, Closed Quotient):一个振动周期中声门闭合时间所占比例。CQ 高 → 声带闭合充分、激励脉冲短而强 → 谐波丰富、听感”实”;CQ 低 → 大量漏气 → 噪声多、听感”虚”。
  • 开商(OQ)= 1 − CQ;**速度商(SQ)**描述开合不对称性。
  • 气流与气压:气声(breathy voice)是”声带振动 + 持续漏气”,频谱特征是中高频噪声底抬高、H1-H2 差值增大。

测量困难:CQ 需要高速喉镜或 EGG(电声门图)等专用设备,从普通录音里只能估计代理量(HNR、谐波幅度差 H1−H2、谱倾斜 spectral tilt 等)。本项目的做法(频段能量占比)就是一种代理。

可写进实验设计的建议:若条件允许,用 EGG 或至少用同一套录音设备建立基线,能把”虚实”这一维的效度(validity)说得更硬。


31. 语音的准平稳假设与分帧长度惯例#

语音是时变信号(音素切换很快),但在短时间窗内(10–30 ms)可近似认为是平稳的——这叫准平稳假设(quasi-stationarity),是所有短时分析(STFT、LPC、fbank)的前提。

惯例参数常用值原因
分析窗长25–30 ms覆盖 2–3 个基频周期(F0 ≥ 80 Hz → 周期 ≤ 12.5 ms),既能捕捉谐波又不跨越音素
帧移10 ms足够密以捕捉快速变化,且 50%–75% 重叠补偿加窗损失
本项目 STFTn_fft 2048(≈128 ms)、hop 512(32 ms)音色分析看”稳定频谱形状”,长窗换来更细的频率分辨率(Δf≈7.8 Hz),便于观察低频谐波结构
本项目 fbank25 ms 窗 / 10 ms 帧移(Kaldi 默认)深度学习需要更细的时间结构,且必须与训练时一致

为什么两套参数不同却不冲突:它们服务于不同目的——STFT 侧给”物理量化”(看重频谱形状与频段占比),fbank 侧给”神经网络”(看重时间序列模式)。理解这一点能解释”为什么同一个项目里有两套音频处理管线”,也是第 3 篇拆代码时的关键分界线。



第五部分 听觉与特征工程地基#

32. 梅尔刻度的来历与公式#

问题:人耳对频率的感知不是线性的——1 kHz 以下很敏感(能分辨几 Hz 差别),高频则迟钝(几千 Hz 的差别都听不出)。用等 Hz 间隔切频段,会造成”低频分辨率不足、高频浪费”。

梅尔刻度(Mel scale):一种模拟人耳感知的”主观音高”刻度。常用近似公式(Stevens & Volkmann,1937 / O’Shaughnessy):

m=2595log⁡10 ⁣(1+f700)m = 2595\log_{10}\!\left(1+\frac{f}{700}\right)

反变换(Mel → Hz,构造滤波器组时要用):

f=700(10m/2595−1)f = 700\left(10^{m/2595}-1\right)

推导思路(理解就够):该式源于”主观音高 ≈ 临界带数量的对数”这一经验规律;700 与 2595 是拟合常数:2595 ≈ 1000/ln(2),用来把”倍频程”换算成 mel 的尺度;在 1 kHz 处,mel 值约为 1000。

校验点:

  • f = 1000 Hz → m = 2595·log10(2.4286) ≈ 999.99(果然约 1000);
  • f = 4280 Hz → m ≈ 2211;
  • f = 124 Hz → m ≈ 183.7。

其它听觉刻度:Bark(临界带)、ERB(等效矩形带宽)。Mel 最常用,因为它简单;ERB 更贴近听觉生理。


33. 梅尔滤波器组的构造(5 步详解)#

深度学习侧的 fbank 特征,是把频谱通过一组三角形带通滤波器聚合而来。构造步骤:

第 1 步:确定频率范围

  • 下限 f_low(通常 0 或 20 Hz;本项目 Kaldi 默认 20 Hz);
  • 上限 f_high(通常 sr/2 = 8000 Hz,或直接取 8000)。

第 2 步:转到 Mel 域并均匀取点

滤波器个数 M = 128(本项目)。需要 M + 2 个边界点:

mi=m(flow)+i⋅m(fhigh)−m(flow)M+1,i=0,1,…,M+1m_i = m(f_{low}) + i\cdot\frac{m(f_{high})-m(f_{low})}{M+1},\quad i=0,1,\dots,M+1

第 3 步:转回 Hz

fi=700(10mi/2595−1)f_i = 700\left(10^{m_i/2595}-1\right)

得到 130 个频点,相邻三个构成一个三角形:(f_{i-1}, f_i, f_{i+1})。

第 4 步:在 FFT 频点上做三角加权

第 i 个滤波器的响应(在 FFT 的频率 bin k 上):

Hi(k)={fk−fi−1fi−fi−1,fi−1≤fk≤fifi+1−fkfi+1−fi,fi≤fk≤fi+10,其他H_i(k)= \begin{cases} \dfrac{f_k-f_{i-1}}{f_i-f_{i-1}}, & f_{i-1}\le f_k\le f_i\\[2mm] \dfrac{f_{i+1}-f_k}{f_{i+1}-f_i}, & f_i\le f_k\le f_{i+1}\\[2mm] 0, & \text{其他} \end{cases}

得到一个 (M, N/2+1) = (128, 1025)(16 kHz、n_fft 2048、25 ms 窗时为 512 点时是 257)的稀疏矩阵。

第 5 步:归一化与聚合

  • 面积归一化(norm='slaney'):让每个滤波器的面积相等,低频滤波器更窄但更高,使得各频带”总能量”可比;
  • 峰值归一化:三角形峰值为 1(Kaldi 的一种选择,便于用固定比例混合);
  • 聚合:mel_energy = H @ power_spectrum;取对数(加小常数防 log(0))→ log-mel / fbank。

为什么低频滤波器更窄:等 mel 间隔在低频对应更小的 Hz 间隔 → 低频分辨率高(符合人耳)。

在本项目:这里要特别注意区分两件事——

  1. 深度学习输入用的是”128 个梅尔滤波器的 fbank”;
  2. 物理量化特征用的是”12 个等梅尔间隔的频段能量占比”(直接用频段求和,不经过三角窗)。 两者都用梅尔刻度,但一个是”滤波器组特征”,一个是”分频段占比统计”,目的不同。

34. log-mel / fbank / MFCC 的关系#

名称计算链特点本项目
幅度谱FFT → 取模原始,维度高(1025)间接用到
mel 谱FFT 功率谱 → 梅尔滤波器组128 维,压缩频率轴❌(未直接用)
log-mel / fbankmel 谱 → 取对数动态范围压缩,神经网络友好✅ 模型输入
MFCClog-mel → DCT → 取前 13 维(+Δ+ΔΔ)去相关、维度低,传统 ASR 标配❌

DCT(离散余弦变换):对 log-mel 做 DCT 相当于”把相邻滤波器的相关性去掉”,得到近似独立的倒谱系数。代价是丢弃了高维细节。

为什么现代音频深度学习更爱 fbank 而不是 MFCC:

  1. DCT 是线性变换,会损失信息且不可逆(截断后);
  2. 神经网络自己能从 log-mel 里学出更好的表示(甚至端到端从波形学);
  3. fbank 保留了完整的梅尔频带信息,128 维比 13 维信息量大得多。

本项目 128 维的选择:AudioMAE/AST 惯例(128 mel × 1024 帧 ≈ 10 秒),高频率分辨率对区分音色细节(谐波密度、高频噪声)有利。


35. Kaldi fbank 与 librosa mel 的差异#

虽然都算 mel 特征,但细节不同,不能混用(训练用 Kaldi,推理也必须用 Kaldi):

维度Kaldi torchaudio.compliance.kaldi.fbank(本项目用)librosa.feature.melspectrogram
默认窗长/帧移25 ms / 10 ms由 n_fft/hop_length 指定(不按时间)
帧对齐snip_edges=True:不使用补零的帧(帧数 ≈ 时长/帧移)center=True:两端补零(帧数 = 1 + L/hop)
能量项可选 use_energy(本项目 False)无
窗函数povey(默认)/hanning(本项目指定 hanning)Hann
mel 归一化默认面积归一化,可调默认 norm='slaney'
对数有(log_mel)需手动 power_to_db
取值范围对数域,均值约 −4(对数能量)通常 dB(相对最大值)

这解释了为什么归一化常数是 mean=-4.2677, std=4.5690:Kaldi fbank 的对数能量在 AudioSet 上统计出的均值约 −4.3,量级与”DB 相对值”完全不同。如果换成 librosa 的 mel 特征还沿用这两个常数,输入分布会完全错位,模型输出变成噪声。

实践结论:模型训练/推理的特征提取器必须逐参数一致(库、窗函数、帧移、mel 数、归一化方式、甚至库版本),这是深度学习工程里最容易踩、也最难查的一类 bug。


36. 特征归一化:AudioSet 全局均值方差与 ×2 的秘密#

代码:

fbank = (fbank - audio_conf['mean']) / (audio_conf['std'] * 2)
# audioset: mean = -4.2677393, std = 4.5689974

为什么需要归一化:

  1. 让输入分布零均值、单位方差 → 优化更稳定、收敛更快;
  2. 不同录音条件(设备/增益)带来的整体偏移被部分消除。

三种归一化方式对比:

方式统计量来源优点缺点
全局 CMVN(本项目)训练集全局均值/方差(AudioSet)简单、确定、推理可复现对单个样本的偏移不敏感(对增益变化鲁棒性一般)
per-utterance每条音频自己的均值/方差消除个体/设备差异会削弱”绝对音色”信息(把所有人的谱形都拉到同一中心)
per-channel(类似 BN)每个特征维独立统计更细致需要统计量存储

为什么 ×2:AudioMAE/AST 的惯例,等价于把标准差乘以 2(即对输入做”轻度压缩”,降低异常值影响)。这在论文代码中常见,改动它会让输入分布与训练不一致。

归一化与后处理的关系:本项目物理量化侧的”能量占比”是另一种归一化(除以总能量),它带来的是”抗音量”特性;而 fbank 侧的 CMVN 带来的是”数值稳定”。两者目的不同,不要混淆。


37. 本项目核心特征:12 频段能量占比#

把前面知识串起来,完整定义本项目的物理特征:

输入:16 kHz 单声道波形 y(任意时长)

计算:

1) D = STFT(y, n_fft=2048, hop_length=512) # (1025, T) 复数
2) E = |D|² # 能量谱 (1025, T)
3) total = Σ_k Σ_t E[k,t] # 标量总能量
4) p[k] = Σ_t E[k,t] / total # 每个频点的能量占比 (1025,)
5) 对每个维度 d(厚薄/虚实/明暗):
对 12 个频段 [lo_i, hi_i]:
band_i = Σ_{k: lo_i ≤ f_k ≤ hi_i} p[k] # 12 个数
6) 综合量:total_energy×1e-7, LF/HF×0.1, spectral_centroid×1e-3
7) 拼接 → 该维度 15 维特征向量

性质分析(这些是可以写进论文的点):

性质说明
抗增益与总能量归一化,音量变化不影响
低维12 个数即可概括一个维度的频谱形状(相比 1025 个频点降维 98 倍)
等听觉间隔频段按等梅尔切分(第 32–33 节),与人耳分辨率匹配
可解释每个数对应一个可命名的频段(如 “610-733Hz”),前端柱状图可直接展示
与深度特征互补深度模型给判别力,物理特征给可解释性与抗噪基线

局限性(同样要写进论文):

  • 只用了幅度信息,丢弃相位;
  • 只用 12 段,丢失段内细节(如共振峰位置);
  • 对削波/强混响敏感(频谱形状被破坏);
  • 依赖固定的频段划分(换语言/换音域可能不适配)。


第六部分 机器学习地基#

38. 监督学习范式与数据划分#

标准范式:

数据 D = {(x_i, y_i)}
│
├─ 选择假设空间 f_θ(线性模型 / 神经网络 / 树 …)
├─ 定义损失 L(f_θ(x), y) 衡量"错得有多离谱"
├─ 优化 找 θ* = argmin L (梯度下降等)
└─ 评估 在未见过的数据上衡量泛化能力

三份数据的分工:

集合用途本项目例子
训练集学参数audio_finetune/train.json
验证集调超参、选 checkpoint、早停audio_finetune/eval.json(--first_eval_ep 15 后才开始评估)
测试集最终报告性能(只用一次)科研脚本里按 {1,2,3} 文件夹留出的样本

常见错误:用验证集反复调参后再把它当测试集报告 → 结果虚高(信息泄漏)。本项目可复现的评测脚本 evaluate_thickness_accuracy.py 就是在”厚薄”目录上做整体准确率统计,若要与论文对齐,必须确认它没参与训练。

交叉验证(k-fold):数据少时的替代方案——轮流留出 1/k 做验证,取平均。本项目数据量小(每级几百条),做 5 折交叉验证比单次划分更可信,这是复现时可以做的改进点。


39. 过拟合、欠拟合与正则化#

  • 欠拟合:模型太简单/训练不足 → 训练集与验证集都差;
  • 过拟合:模型记住训练样本噪声 → 训练集很好、验证集变差。

判断方法:画出训练/验证损失曲线,若验证损失先降后升,拐点即最佳停止点(早停)。

正则化手段全景(★ = 本项目用到):

手段机制本项目
L2 权重衰减惩罚 ‖θ‖²,让权重更小更平滑★ --weight_decay 1e-4(预训练)/ AdamW
L1惩罚 ‖θ‖₁,产生稀疏解✗
Dropout训练时随机丢神经元,防共适应✗(ViT 常用 drop_path 代替)
DropPath(随机深度)随机跳过整个残差分支★ --drop_path 0.1
数据增强扩充样本多样性★ mixup、SpecAugment、roll-mag
早停验证指标不再提升就停★ --first_eval_ep + 人工选 checkpoint
减小模型容量用更小模型✗
标签平滑软标签,防过度自信✗
掩码重建预训练用自监督任务学到通用表示★ MAE(最强正则)

为什么 MAE 预训练算”正则”:它强迫模型学”频谱的通用结构”,微调时需要的标注数据更少,过拟合风险显著下降。


40. 损失函数总览:MSE / CE / BCE#

回归(MSE):

LMSE=1n∑i(y^i−yi)2L_{\text{MSE}}=\frac{1}{n}\sum_i(\hat y_i-y_i)^2

梯度 ∂L/∂ŷ = 2(ŷ−y)/n,对异常值敏感。MAE(L1)更抗离群但对 0 点不可导。

多分类交叉熵(CE):设 p = softmax(z)

LCE=−∑kyklog⁡pkL_{\text{CE}}=-\sum_k y_k\log p_k

关键梯度:∂L/∂z = p − y(形式极简,是 softmax+CE 的”黄金搭档”)。

二元/多标签交叉熵(BCE):对每个标签独立做二分类,σ 是 sigmoid

LBCE=−1n∑i[yilog⁡σ(zi)+(1−yi)log⁡(1−σ(zi))]L_{\text{BCE}}=-\frac1n\sum_i\Big[y_i\log\sigma(z_i)+(1-y_i)\log\big(1-\sigma(z_i)\big)\Big]

梯度:∂L/∂z_i = σ(z_i) − y_i(与 CE 同款简洁形式)。

“WithLogits” 版本为什么数值更稳:log σ(z) 当 z 很负时会溢出。BCEWithLogitsLoss 内部用 log-sum-exp 恒等式合并计算:

log⁡(1+e−∣z∣)+max⁡(−z,0)\log(1+e^{-|z|}) + \max(-z, 0)

避免先 sigmoid 再 log 的两步溢出。本项目用的正是 torch.nn.BCEWithLogitsLoss(),所以模型输出 logits(未过 sigmoid),推理时再手动 torch.sigmoid(output)。

分类 vs 回归损失的选择:本项目等级本质是”有序离散类别”,用 BCE 多标签处理(而非回归 MSE),好处是:① 每个维度独立、互不干扰;② 输出概率可用于取最大值判级与阈值分析。若改成回归直接预测 1–3 分,会丢失”概率”这一信息,也无法自然地处理”模型犹豫”的情况。


41. 优化算法:GD → SGD → Momentum → Adam → AdamW#

批量梯度下降(BGD):用全量数据算梯度 → 准但慢。

随机梯度下降(SGD):每次用一个样本/小批量:

θ←θ−η gt,gt=∇L(θ;batcht)\theta \leftarrow \theta - \eta\,g_t,\qquad g_t=\nabla L(\theta;\text{batch}_t)

Momentum(动量):累加历史梯度方向,抑制震荡、加速收敛:

vt=μvt−1+gt,θ←θ−ηvtv_t = \mu v_{t-1} + g_t,\qquad \theta \leftarrow \theta - \eta v_t

Adam(自适应学习率 + 动量):

mt=β1mt−1+(1−β1)gt,vt=β2vt−1+(1−β2)gt2m_t=\beta_1 m_{t-1}+(1-\beta_1)g_t,\quad v_t=\beta_2 v_{t-1}+(1-\beta_2)g_t^2m^t=mt1−β1t,v^t=vt1−β2t,θ←θ−ηm^tv^t+ϵ\hat m_t=\frac{m_t}{1-\beta_1^t},\quad \hat v_t=\frac{v_t}{1-\beta_2^t},\quad \theta\leftarrow\theta-\eta\frac{\hat m_t}{\sqrt{\hat v_t}+\epsilon}
  • m 是一阶矩(梯度均值,类似动量);
  • v 是二阶矩(梯度平方的均值,用来”每个参数自适应”);
  • 偏差校正是因为初期 m、v 被初始化为 0 而偏小;
  • 默认 β₁=0.9, β₂=0.999, ε=1e-8;
  • 直觉:梯度大的参数步子自动变小,梯度小的参数步子变大 → 对稀疏/尺度差异大的参数友好。

AdamW(本项目微调用):把权重衰减从”加进梯度”改为”直接衰减参数”(decoupled weight decay):

θ←θ−η(m^v^+ϵ)−ηλθ\theta \leftarrow \theta - \eta\Big(\frac{\hat m}{\sqrt{\hat v}+\epsilon}\Big) - \eta\lambda\theta

原版 Adam+L2 会让自适应学习率与正则耦合(大梯度参数反而被正则得更少),AdamW 解耦后正则效果与训练更稳,是现代 Transformer 训练标配。

为什么项目用 Adam/AdamW 而不是纯 SGD:Transformer 类模型参数尺度差异大、对学习率敏感,AdamW 更容易收敛;而且微调数据量小、轮数少,需要快速稳定收敛。


42. 学习率调度:warmup、cosine 与线性缩放规则#

学习率 η 是最重要的超参:太大会发散/震荡,太小收敛慢或卡在次优。

Warmup(预热):前几个 epoch 让学习率从 0 线性升到目标值。原因:训练初期参数随机、梯度方向不可靠(尤其 Transformer 的 LayerNorm/注意力统计不稳定),直接大学习率容易”炸”。

Cosine 退火(余弦半周期):util/lr_sched.py 的实现(本项目实测源码):

lr(e)={lr⋅ew,e<wlrmin⁡+(lr−lrmin⁡)⋅12(1+cos⁡(πe−wE−w)),e≥w\text{lr}(e)= \begin{cases} \text{lr}\cdot\dfrac{e}{w}, & e<w\\[2mm] \text{lr}_{\min}+(\text{lr}-\text{lr}_{\min})\cdot\dfrac12\Big(1+\cos\big(\pi\dfrac{e-w}{E-w}\big)\Big), & e\ge w \end{cases}

其中 w = warmup_epochs、E = epochs。特点:前期降得快、后期平缓,训练末期”精修”效果好。

线性缩放规则(Linear Scaling Rule):批大小变大 → 梯度噪声变小 → 可以把学习率按比例放大。本项目微调源码:

args.lr = args.blr * eff_batch_size / 256 # eff_batch = batch × accum × GPU数

即”基础学习率 blr 对应 256 的有效批大小”,其他批大小按比例缩放。这解释了为什么脚本里写 --blr 1e-3 而不是直接给 --lr。

逐层学习率衰减(Layer-wise LR Decay):util/lr_decay.py

lri=lr×layer_decay (L−i)\text{lr}_i=\text{lr}\times\text{layer\_decay}^{\,(L-i)}

layer_decay=0.75、L=12 时,第 0 层学习率是浅层的 0.75¹² ≈ 0.032 倍——浅层(通用频谱特征)几乎不动,深层(任务相关)大步学习。这是从 MAE 权重微调时的标准技巧。


43. 特征缩放与标准化#

为什么必须缩放:若两个特征量级差 1000 倍(如”总能量 1e6”与”能量占比 0.01”),梯度下降会变成”狭长峡谷”里的锯齿下降,收敛极慢;神经网络里还会导致激活饱和。

方式对比:

方式公式适用
标准化(z-score)(x−μ)/σ最常用(本项目 fbank 归一化)
最小-最大归一化(x−min)/(max−min)有确定边界时
单位化x/‖x‖方向敏感任务
百分比占比x/Σx本项目物理特征(天然抗总量变化)
分位数归一化用分位数替代均值方差抗离群

项目里的两处缩放细节(都为了数值量级对齐,不含信息):

array.append(total_energy*0.0000001) # 1e-7
array.append(features['lfhf_ratio_proportion']*0.1)
array.append(features['spectral_centroid_proportion']*0.001)

这些常数会被后续线性权重与 a,b 映射吸收,但必须在训练与推理时保持一致(改了就要重训权重)。


44. 判别分析:Fisher / LDA 完整推导与项目实现对照#

这是本项目物理量化的理论核心,值得完整走一遍。

44.1 问题设定#

给定两组样本(等级 1、等级 2),特征维度 d = 12~15,样本数各 n₀, n₁。要找投影向量 w,把样本投到一维:

y=w⊤xy = w^\top x

44.2 两个散度#

类内散度(within-class scatter):投影后两类的方差之和

Swproj=w⊤Sww,Sw=∑c∈{0,1}∑x∈c(x−μc)(x−μc)⊤S_w^{\text{proj}} = w^\top S_w w,\qquad S_w=\sum_{c\in\{0,1\}}\sum_{x\in c}(x-\mu_c)(x-\mu_c)^\top

类间散度(between-class scatter):投影后两类均值差的平方

Sbproj=w⊤Sbw,Sb=(μ0−μ1)(μ0−μ1)⊤S_b^{\text{proj}} = w^\top S_b w,\qquad S_b=(\mu_0-\mu_1)(\mu_0-\mu_1)^\top

44.3 Fisher 准则与最优解#

J(w)=w⊤Sbww⊤SwwJ(w)=\frac{w^\top S_b w}{w^\top S_w w}

J 与 w 的尺度无关(分子分母同阶),所以只关心方向。对 w 求导并令零:

∂J∂w=0 ⟹ Sbw=λSww\frac{\partial J}{\partial w}=0\ \Longrightarrow\ S_b w=\lambda S_w w

这是广义特征值问题。因为 S_b 秩为 1(由两均值差的外积构成),可得唯一的非零特征值对应方向:

 w∗∝Sw−1(μ1−μ0) \boxed{\,w^*\propto S_w^{-1}(\mu_1-\mu_0)\,}

几何三步理解:

  1. μ1−μ0:两类中心的连线方向(想去的方向);
  2. S_w:数据自身的”形状”(各方向方差、相关性);
  3. S_w^{-1}:把数据”白化”成球形——若某方向方差很大,就压低该方向的权重,避免”大方差方向”主导。

44.4 与”倒数的损失”的等价性#

两类时,投影后的情形简化为标量:μ̃_c = wᵀμ_c,σ̃_c² = wᵀS_cw(S_c 为每类协方差)。Fisher 比:

J(w)=(μ~1−μ~0)2σ~02+σ~12J(w)=\frac{(\tilde\mu_1-\tilde\mu_0)^2}{\tilde\sigma_0^2+\tilde\sigma_1^2}

最大化 J = 最小化 1/J,于是损失:

L(w)=σ~02+σ~12(μ~1−μ~0)2L(w)=\frac{\tilde\sigma_0^2+\tilde\sigma_1^2}{(\tilde\mu_1-\tilde\mu_0)^2}

这正是 torchmodel.py::FisherLoss:

return (var0 + var1) / ((mu1 - mu0).pow(2) + 1e-6)

注意:这里 var0/var1 是投影后(即网络输出 outputs)的方差,因为网络本身就是那个线性投影 y = wᵀx + b(ThicknessModel 只有一个 nn.Linear(input_dim, 1))。所以”训练一个线性层 + Fisher 损失”= 直接解 Fisher 判别方向,只是用迭代法而非闭式解。

44.5 为什么不用闭式解 S_w^{-1}(μ1−μ0)?#

维度闭式解迭代训练(项目做法)
优点一步到位、确定可加正则、可处理奇异矩阵、可扩展到多类/非线性、能顺带打印”可用于部署的固定长度权重”
缺点S_w 奇异时无解;特征列可任意选择时不便需调学习率与轮数

项目里 t_index_select 可以任意选择特征列组合,S_w 在样本少、列多时容易奇异(d > n 时必然奇异),所以迭代法更实用。这也是工程上很常见的”用优化代替解析”的取舍。

44.6 项目里的手推梯度版本(model.py)#

对 L = (μ̃3 − μ̃2)^{−2},令 Δ = θᵀ(x̄3 − x̄2):

∂L∂θ=−2Δ−3⋅(xˉ3−xˉ2)\frac{\partial L}{\partial\theta}=-2\Delta^{-3}\cdot(\bar x_3-\bar x_2)

代码对照:

gra1 = np.power(np.mean(y_pred3)-np.mean(y_pred2), -3) # Δ^{-3}
a = x3_s/x3.shape[0] - x2_s/x2.shape[0] # x̄3 - x̄2
return -2*gra1*a # Direction == 2

完全吻合。作者没有用 autograd,而是自己推了梯度——这也可以理解为”科研脚本追求可控/可解释”。

44.7 从二类到多类与”两两判别”#

项目把三维度各分 3 级,但每次只学相邻两级(1-2 或 2-3),这是因为:

  • 相邻等级差异细微,单独学判别方向更灵敏;
  • 3 级可直接由”两级阈值序列”刻画(1-2 边界 + 2-3 边界),无需多类 LDA(多类需解 S_w^{-1}S_b 的多个特征向量,且本项目样本量不支持)。

这就是线上”每维度两套模板”(偶数行 = 1-2 级判别,奇数行 = 2-3 级判别)的由来。


45. 逻辑回归、线性分类边界与 sigmoid#

逻辑回归:p(y=1|x) = σ(wᵀx + b),决策边界 wᵀx + b = 0 是一条超平面。

与 Fisher 的关系:

  • 若两类协方差相同(S_0 = S_1),逻辑回归的最大似然解方向与 Fisher 判别方向一致(都正比于 S_w^{-1}(μ1−μ0));
  • 差别在于:Fisher 只关心”分开”,逻辑回归关心”概率校准”;Fisher 有闭式解,逻辑回归需要迭代。

多标签里的 sigmoid:11 个输出各自过 sigmoid → 11 个独立概率。注意:这些概率之和不为 1(多标签的正常现象),所以不能”取最大概率当唯一答案”来解释全部输出——但本项目在单个维度组内(如 [0<3>])确实取最大值判级,因为组内三级是互斥的(一条语音的厚薄只能是 C/B/A 之一)。这是多标签框架里嵌”组内单标签”语义的典型用法,也是 group_acc 指标的依据。


46. 评价指标:从混淆矩阵到 mAP 与 d-prime#

46.1 二分类混淆矩阵#

预测正预测负
实际正TPFN
实际负FPTN
  • 准确率 Accuracy = (TP+TN)/总数 —— 类别不平衡时会骗人;
  • 精确率 Precision = TP/(TP+FP):判为正的里面有多少真对;
  • 召回率 Recall = TP/(TP+FN):真为正的里面抓出来多少;
  • F1 = 2PR/(P+R):调和平均。

46.2 PR 曲线与 AP / mAP#

做法:把阈值从 1 扫到 0,每取一个阈值得一组 (Precision, Recall),连成曲线。

AP(Average Precision) = PR 曲线下面积,等价于:

AP=∑n(Rn−Rn−1) Pn\text{AP}=\sum_n (R_n-R_{n-1})\,P_n

mAP = 各类 AP 的平均。stat.py 里:

avg_precision = metrics.average_precision_score(target[:, k], output[:, k], average=None)
mAP = np.mean([stat['AP'] for stat in stats])

多标签任务为什么用 mAP 而不用准确率:每个标签独立、正负样本极不平衡(本项目 11 类中每类约 1/3 为正,但若换成 AudioSet 那种 527 类,正样本比例 <1%),准确率会被”全判负”刷得很高,AP 则对排序质量敏感。

46.3 ROC / AUC 与 d-prime#

  • ROC:横轴假正率 FPR = FP/(FP+TN),纵轴真正率 TPR = Recall;
  • AUC = ROC 下面积,等价于”随机取一正一负,正样本得分更高的概率”;
  • d-prime(辨别力指数,信号检测论):stat.py 用 AUC 反推
def d_prime(auc):
standard_normal = stats.norm()
return standard_normal.ppf(auc) * np.sqrt(2.0)

其中 Φ⁻¹ 是标准正态分位数函数。直觉:AUC=0.5 → d’=0(完全分不开);AUC=0.93 → d’≈2.1(分得不错);AUC→1 → d’→∞。

46.4 本项目自加的 group_acc(重要)#

label_groups = {'thick':[0,1,2], 'real':[3,4,5], 'bright':[6,7,8], 'sex':[9,10]}
group_acc[g] = accuracy_score(target[:, g], one_hot(argmax(output[:, g])))

含义:把每组的 3 个(或 2 个)输出单独拿出来,组内取最大当预测等级,与真值比。为什么必须这样:

  1. 产品指标:用户只关心”厚薄判成几级对不对”,不关心 11 类整体排序;
  2. 多标签框架下 mAP 无法表达”组内互斥”:取 argmax 后组内准确率才对应真实使用方式;
  3. 便于按维度诊断:可以看出模型在哪个维度最弱(比如”虚实”准确率明显低 → 说明该维标注难或特征不足)。

复现时的建议:报告结果时同时给 mAP(学术界可比)与 group_acc(业务可解释),并做每维度的混淆矩阵(3×3),这比单个数字有说服力得多。


47. 阈值、边界与分位数过滤#

为什么机器学习最后总要有”阈值”:

  • 概率输出 → 需要阈值才变”决策”(本项目取组内 argmax,等价于相对阈值);
  • 连续分数 → 需要边界才映射成等级(本项目 bands_change 的中心分);
  • 两套系统融合 → 需要”偏差多大才允许改判”(本项目 error_threshold = 20)。

阈值/边界的三种确定方式:

方式做法本项目
网格搜索在候选区间逐点扫描,取指标最优★ scatterplot.calbaseline/cal_sum 扫描分数轴,最大化分类正确率
分位数法取分布的某个分位点(如 70%)★ 结合使用:data_filter 先滤掉 25%–75%(明暗用 35%–70%)以外的离群样本,再搜边界
最优化(如 Youden’s J)最大化 TPR−FPR✗(可作改进)

为什么必须过滤分位数:极端样本(录音异常、标注错误)会把分数轴拉得很长,导致网格搜索出的”最优阈值”落在无效区域;先裁掉两端 → 边界更稳。代价是:真实使用中遇到极端样本会落到边界之外,行为需定义(本项目里就是”物理分极大/极小” → 触发跨级修正或保持原级)。

error_threshold=20 的定位:它不是分类阈值,而是融合修正的容差(0–100 尺度上)。逻辑上类似”避免频繁抖动”的滞回(hysteresis)设计:

  • 偏差 ≤ 20 → 认为”模型与物理一致,不改判”,仅打标记(C+/B−);
  • 偏差 > 20 → 认为”分歧显著”,采纳物理分改判一级。

调参方向(第 4 篇要做的实验):把 20 改成 10/30,观察改判率与(若有标注)准确率变化,就能画出”容差–准确率”曲线,说明这个值是经验折中。



第七部分 深度学习地基#

48. 从感知机到 MLP#

感知机:y = step(wᵀx + b),只能线性分类(连 XOR 都解不了)。 MLP(多层感知机):叠加”线性层 + 非线性激活”

h(1)=ϕ(W1x+b1),h(2)=ϕ(W2h(1)+b2), …h^{(1)}=\phi(W_1x+b_1),\quad h^{(2)}=\phi(W_2h^{(1)}+b_2),\ \dots

万能近似定理:足够宽的单隐层网络可逼近任意连续函数(存在性,不代表可训练/可泛化)。这给了”神经网络什么都能拟合”的底气,但实践中真正的关键是归纳偏置(卷积的局部性、注意力的全局交互、掩码预训练的结构先验)。

本项目:主干是 Transformer(注意力替代了卷积的先验),最终接一个线性头,与”MLP + sigmoid”在分类那一层是相通的。


49. 反向传播与计算图(手推一遍)#

计算图:把复合运算拆成节点,前向算值、反向用链式法则传梯度。

两层网络示例:z₁ = W₁x,a₁ = φ(z₁),z₂ = W₂a₁,L = ℓ(z₂, y)

∂L∂W2=∂L∂z2a1⊤,∂L∂W1=(W2⊤∂L∂z2)⊙ϕ′(z1) x⊤\frac{\partial L}{\partial W_2}=\frac{\partial L}{\partial z_2}a_1^\top,\qquad \frac{\partial L}{\partial W_1}=\Big(W_2^\top\frac{\partial L}{\partial z_2}\Big)\odot\phi'(z_1)\,x^\top

工程要点:

  1. 梯度累加:loss.backward() 会累加而非覆盖 → 必须每步 optimizer.zero_grad()(写错会导致梯度爆炸);
  2. 梯度消失/爆炸:深层网络连乘导致 → 用残差、LayerNorm、合理初始化、梯度裁剪(--clip_grad);
  3. torch.no_grad():推理时关掉计算图,省显存与时间(本项目推理全程 @torch.no_grad())。

50. 激活函数与梯度问题#

激活公式特点
Sigmoid1/(1+e^{−x})输出 (0,1);两端饱和 → 梯度消失;输出非零中心
Tanh(e^x−e^{−x})/(e^x+e^{−x})零中心,仍会饱和
ReLUmax(0,x)计算快、缓解梯度消失;有”死亡神经元”问题
LeakyReLUmax(αx,x)负半轴不零
GELUx·Φ(x)平滑版 ReLU,Transformer 标配
SwiGLU门控变体现代 LLM 常用

在 ViT 里:MLP 中间层用 GELU(timm 默认),注意力里没有激活(softmax 除外)。激活函数的非线性是网络表达能力的来源——如果全是线性层,多层也等价于一层。


51. 卷积与感受野#

二维卷积:用卷积核在输入上滑动做加权求和(严格说是互相关)。

输出尺寸公式:

Hout=⌊Hin+2P−KS⌋+1H_{out}=\left\lfloor\frac{H_{in}+2P-K}{S}\right\rfloor+1

K 核大小、S 步长、P 填充。

本项目核心用例:PatchEmbed_new 用 Conv2d(1, 768, kernel=16, stride=10) 把 (1,1024,128) 变成 patch 序列:

  • 时间轴:(1024 + 0 − 16)/10 + 1 = 101
  • 频率轴:(128 + 0 − 16)/10 + 1 = 12
  • 共 101 × 12 = 1212 个 patch,每个 768 维

关键概念:

概念含义本项目体现
感受野一个输出点”看到”的输入范围patch 覆盖 16×16(160 ms × 16 mel)
权值共享同一核在所有位置复用 → 平移不变、参数少patch embedding 只有一个卷积核集合
stride滑动步长,>1 即降采样stride=10 < kernel=16 → patch 重叠(时间上更密)
1×1 卷积逐点线性变换(跨通道)注意力中的 QKV 投影等价于 1×1 卷积
空洞卷积扩大感受野不增参数✗

为什么 patch embedding 用卷积实现:等价于”切块 + 展平 + 线性投影”,但实现更简洁、可复用高效卷积实现。这是 ViT 的通用做法。

与 CNN 的区别:CNN 的局部性先验强、数据需求小;ViT 的注意力是全局的、先验弱,需要更多数据或自监督预训练(这正是本项目用 MAE 预训练的原因)。


52. 归一化层:BatchNorm vs LayerNorm#

BatchNorm(BN):对一个 batch 内、每个通道做归一化

x^=x−μbatchσbatch2+ϵ,y=γx^+β\hat x=\frac{x-\mu_{\text{batch}}}{\sqrt{\sigma^2_{\text{batch}}+\epsilon}},\qquad y=\gamma\hat x+\beta
  • 训练时用 batch 统计量,推理时用滑动平均;
  • 缺点:依赖 batch 大小(小 batch 不稳)、对变长序列/RCNN 不友好、分布式训练需同步统计量。

LayerNorm(LN):对单个样本、整个特征维做归一化

x^i=xi−μsampleσsample2+ϵ\hat x_i=\frac{x_i-\mu_{\text{sample}}}{\sqrt{\sigma^2_{\text{sample}}+\epsilon}}
  • 与 batch 无关 → 适合变长序列与 Transformer;
  • 与 batch size 解耦 → 小批量也稳定;
  • ViT/MAE 的每个 block 里都用 LN(norm_layer=partial(nn.LayerNorm, eps=1e-6))。

其它:GroupNorm(按通道分组)、RMSNorm(省去均值,LLM 常用)、FrozenBatchNorm(冻结统计量,检测模型常用;timm 会从 torchvision 引入它,这也是本项目”timm 依赖 torchvision”的根源)。

Pre-LN vs Post-LN:原版 Transformer 是 Post-LN(x = LN(x + Attn(x))),ViT 普遍用 Pre-LN(x = x + Attn(LN(x))),训练更稳、可不用 warmup 太多。


53. 残差连接与初始化#

残差(Residual):

xl+1=xl+F(xl)x_{l+1}=x_l+\mathcal F(x_l)
  • 反向传播时梯度里含恒等项 1,梯度不会消失;
  • 网络可理解为”学习相对于恒等的增量”,训练更容易;
  • 使深层网络(ViT 12 层、Transformer 数十层)可训。

DropPath(随机深度):训练时以概率 p 整个跳过残差分支:

xl+1=xl+11−p⋅1[keep]⋅F(xl)x_{l+1}=x_l+\frac{1}{1-p}\cdot\mathbb 1[\text{keep}]\cdot\mathcal F(x_l)

相当于”随机用不同深度的子网络”,是 ViT 的重要正则(本项目 --drop_path 0.1)。

初始化:

  • 全零 → 对称性破缺失败,学不动;
  • 过大 → 激活爆炸;过小 → 信号衰减;
  • Xavier/Glorot:Var(W) = 1/fan_in(适合 tanh/sigmoid);
  • He/Kaiming:Var(W) = 2/fan_in(适合 ReLU);
  • trunc_normal_(截断正态,std=0.02):Transformer 标配,本项目 models_vit 导入它用于 ViT 权重初始化;
  • pos_embed 特殊处理:先用正弦余弦函数初始化,再按尺寸插值(model_utils/pos_embed.py)。

54. 正则化与数据增强#

Dropout:训练时随机置零一部分激活,推理时关闭(等价于集成)。Transformer 常用 drop_rate/attn_drop。

Mixup(本项目用了,且在波形层):

x~=λx1+(1−λ)x2,y~=λy1+(1−λ)y2\tilde x=\lambda x_1+(1-\lambda)x_2,\qquad \tilde y=\lambda y_1+(1-\lambda)y_2
  • 标签线性混合——这要求损失支持软标签,所以必须用 BCEWithLogitsLoss(多标签)而不是需要整数标签的 CrossEntropyLoss;
  • 在波形层做(_wav2fbank 里先混波形再算 fbank)比在频谱层更自然,能保留相位一致性(虽然相位最终被丢弃,但混合方式更接近物理叠加);
  • Beta(10,10) 使 λ 集中在 0.5,混合后的样本”像两个都像、又都不完全像”。

SpecAugment(本项目 freqm/timem):在频谱上随机遮挡若干频带/时间段。作用:让模型不依赖某单一频段(提升鲁棒性),同时等价于数据扩充。注意:freqm/timem 只在训练时开启,推理必须置 0(本项目 audio_conf 里显式写 freqm=0, timem=0)。

Roll-mag 增强(本项目 _roll_mag_aug):

idx = np.random.randint(len(waveform))
rolled = np.roll(waveform, idx) # 循环移位:等价"从任意时刻开始"
mag = np.random.beta(10,10) + 0.5 # 0.5~1.5 幅度缩放

作用:模拟”裁切起点不同”和”音量不同”,成本几乎为零。

其它可加但项目未用的增强:加性噪声(提升抗噪)、房间冲激响应卷积(模拟混响)、速度/音高扰动(sox/librosa.effects)、SpecMix。注意:音高扰动会改变 F0,对”厚薄”可能有副作用,需实验验证。


55. 注意力机制与位置编码#

自注意力(Self-Attention):把每个 token 映射成 Query、Key、Value 三组向量

Q=XWQ, K=XWK, V=XWVQ=XW_Q,\ K=XW_K,\ V=XW_VAttn(Q,K,V)=softmax(QK⊤dk)V\text{Attn}(Q,K,V)=\text{softmax}\Big(\frac{QK^\top}{\sqrt{d_k}}\Big)V

逐步理解:

  1. QKᵀ:每个 token 与所有 token 的相似度(点积越大越相关);
  2. /√d_k:缩放。若不缩放,点积随维度增大而方差变大 → softmax 饱和 → 梯度消失;
  3. softmax:相似度变成”权重分布”(每行和为 1);
  4. ×V:按权重加权求和 → 每个 token 得到”全局上下文聚合”的结果。

多头(Multi-Head):把 d=768 拆成 h=12 个 64 维子空间并行做注意力,再拼接:

MHA(X)=Concat(head1,…,headh)WO\text{MHA}(X)=\text{Concat}(\text{head}_1,\dots,\text{head}_h)W_O

不同头可以关注不同模式(如某些头关注低频谐波、某些关注高频噪声)。头数、维度、层数是 ViT-B 的固定配置:12 层、12 头、768 维。

复杂度:O(n²d)(n 为 token 数)。本项目 n = 1212,1212² ≈ 1.47M 的注意力矩阵——可接受;若 token 数上万(长音频不切片)就会爆显存,这也是”切片处理”的原因之一。

位置编码(ViT 用可学习 + 正弦余弦初始化):正弦余弦形式

PE(pos,2i)=sin⁡ ⁣(pos100002i/d),PE(pos,2i+1)=cos⁡ ⁣(pos100002i/d)PE_{(pos,2i)}=\sin\!\Big(\frac{pos}{10000^{2i/d}}\Big),\qquad PE_{(pos,2i+1)}=\cos\!\Big(\frac{pos}{10000^{2i/d}}\Big)

性质:不同频率的正弦组合使”相对位移”可以被线性表示(PE(pos+k) 可由 PE(pos) 线性变换得到),因此有利于表达相对位置。注意本项目 models_vit 里 pos_embed 尺寸固定为 1×1213×768,输入长度变了必须插值——这就是”10 秒切片”的硬约束来源。

2D 位置信息:音频 patch 是 101(时间)×12(频率) 的网格,AudioMAE 用 2D 正弦余弦位置编码(get_2d_sincos_pos_embed),让模型感知”第几帧、第几个频带”。


56. Transformer 与 ViT#

Transformer Encoder Block(Pre-LN 版):

x = x + MHA(LN(x)) # 自注意力子层 + 残差
x = x + MLP(LN(x)) # 前馈子层 + 残差 (768 → 3072 → 768, GELU)

ViT 完整前向(对照 models_vit.py::forward_features):

1) x = patch_embed(输入 (B,1,1024,128)) → (B,1212,768)
2) x = x + pos_embed[:,1:,:] # 加位置编码(丢掉第 0 个,那是给 cls 的)
3) cls = cls_token + pos_embed[:,:1,:] → 拼在最前 → (B,1213,768)
4) 12 × Block(Pre-LN + MHA + MLP) → (B,1213,768)
5) global_pool=True:
x = x[:,1:,:].mean(dim=1) # 丢掉 cls,对 1212 个 patch 平均
out = fc_norm(x) # LayerNorm
global_pool=False:用 x[:,0] 即 cls token
6) head: Linear(768 → 11) → logits

ViT-B 参数量手算(能算说明真懂):

组件参数量
patch_embed 卷积16×16×1×768 + 768 ≈ 197K
cls_token768
pos_embed1213×768 ≈ 932K
单 Block:QKV 768×2304+23041.77M
单 Block:Attn 输出投影 768×768+7680.59M
单 Block:MLP 768×3072+3072 与 3072×768+7684.72M
单 Block:2 个 LN≈3K
单 Block 合计≈7.09M
12 个 Block≈85.1M
fc_norm + head(11)≈10K
总计≈86.2M 参数(fp32 ≈ 345 MB)

这解释了 checkpoint 的 1.02 GB:微调时保存的通常包含 AdamW 状态(每个参数 exp_avg、exp_avg_sq 两份):

86.2M×3×4 B≈1.03 GB86.2\text{M}\times 3\times 4\,\text{B}\approx 1.03\ \text{GB}

与实测 1,024,918,304 字节吻合 → 文件里约 2/3 是优化器状态,推理只用到 1/3,这也是加载要 9–20 秒的原因。(工程优化建议:发布时只保存 model 权重,可把文件缩到 ~345 MB。)


57. Swin 与窗口注意力#

Swin Transformer:把注意力限制在局部窗口内(如 7×7),并在相邻层之间做窗口偏移(shifted window),使信息能跨窗口传播。

优势:复杂度从 O(n²) 降到 O(n)(与图像大小线性),且局部性先验更适合图像/频谱。

本项目用途:MAE 的解码器(decoder_mode=1 → SwinTransformerBlock)。原因:重建任务需要处理全部 1212 个 patch(含 80% mask token),用全局注意力代价太高;Swin 的局部窗口既省算力又擅长恢复局部结构(频谱的局部纹理)。

decoder_mode 取值:0 = 全局注意力(原版 MAE),1 = Swin 局部(本项目用),>3 为其它扩展(源码里有条件分支)。


58. 自监督学习与 MAE#

自监督的核心思想:不依赖人工标签,从数据本身构造监督信号(“预测被遮住的部分”、“判断是否同一段”等)。

主要流派:

流派代表监督信号特点
生成式/掩码BERT、MAE、AudioMAE重建被遮住的内容直接学数据分布,微调友好
对比式SimCLR、MoCo、wav2vec2同类(增广)靠近、异类远离表示判别性强,但需负样本/大 batch
预测式CPC、BYOL预测未来/另一视角无需负样本

BERT 与 MAE 的类比:BERT 遮 15% 的词让模型填空;MAE 遮 80% 的 patch 让模型重建。为什么 MAE 敢遮这么多:图像/频谱的空间冗余远高于语言(邻近像素/频点高度相关),遮 80% 仍有足够信息;且高掩码率让任务变难,模型被迫学”语义级”表示而非局部插值。

MAE 的数学细节(对照 models_mae.py):

  1. 分块:patchify 把 (B,1,1024,128) → (B,1212,256)(每 patch 16×16×1=256 个像素值);

  2. 随机掩码:

    len_keep = int(L * (1 - mask_ratio)) # L=1212, ratio=0.8 → 保留 242
    ids_shuffle = argsort(noise) # 随机排列
    x_visible = gather(x, ids_shuffle[:, :len_keep])
    mask = 1 表示被遮(按 ids_restore 还原顺序)
  3. 编码:只把可见的 242 个 patch 送进 encoder(省 ~80% 算力);

  4. 解码:把 242 个编码结果与 970 个 mask_token 按 ids_restore 拼回完整序列 → 加 decoder_pos_embed → 8 层 Swin 解码器;

  5. 重建:decoder_pred: Linear(512 → 256),输出每个 patch 的 256 个像素值;

  6. 损失(只在被遮 patch 上算):

L=1∣M∣∑p∈M∥x^p−xp∥2L=\frac{1}{|\mathcal M|}\sum_{p\in\mathcal M}\big\|\hat x_p-x_p\big\|^2

若 norm_pix_loss=True(本项目开启),先对每个 patch 做归一化:

x~p=xp−μpσp2+ϵ\tilde x_p=\frac{x_p-\mu_p}{\sqrt{\sigma_p^2+\epsilon}}

再算 MSE。为什么归一化:不同 patch 的能量差异很大(低频强、高频弱),不归一化会让损失被高能量 patch 主导;归一化后每个 patch 权重均衡,重建质量更均匀。

非对称设计的三个好处:① 编码器只算 20% → 训练快;② 解码器可以很浅(8 层 / 512 维 vs 编码器 12 层 / 768 维);③ mask token 只在解码器出现 → 编码器学的表示更”干净”(不含占位符信息)。

微调时怎么用预训练权重:把 encoder 权重载入 vit_base_patch16,丢弃 decoder(它的任务已完成)。本项目 main_finetune_as.py --finetune 就是这个流程。


59. 微调策略与逐层学习率衰减#

策略做法数据需求本项目
全量微调更新全部参数中–高★(微调全部 ViT-B)
线性探测冻结主干,只训分类头低✗
适配器/LoRA只训少量插入模块低✗(可作改进)
特征提取用中间层特征 + 传统分类器低✗

微调的学习率通常比预训练小 1–2 个数量级,且逐层衰减(第 42 节):浅层学的是通用频谱结构,改动越小越不容易灾难性遗忘。

原型(prototype)思路与本项目的对照:本项目的物理量化权重本质是一个”线性原型方向”,可与深度特征做后期融合(late fusion)——这与”早期融合(把物理特征拼进模型输入)“是两种可选设计;本项目选了后期融合,好处是模块解耦、可独立迭代(这也是为什么”物理常量”可以手工更新而不必重训模型)。


60. 混合精度训练(AMP)#

动机:fp32 占 4 字节、算力需求大;fp16/bf16 占 2 字节,在支持 Tensor Core 的 GPU 上可提速 2–3 倍、省一半显存。

格式对比:

格式位宽指数位数值范围精度问题
fp32328±3.4e38基准
fp16165±65504范围小 → 易下溢为 0
bf16168±3.4e38范围与 fp32 同,精度略低,更稳

Loss Scaling 原理(fp16 必需):梯度可能小到 1e-8 → fp16 下溢成 0 → 参数不更新。解决:先把 loss 放大 S 倍,反传得到放大 S 倍的梯度,更新前再除以 S:

gscaled=S∇L,θ←θ−ηgscaledSg_{\text{scaled}}=S\nabla L,\qquad \theta\leftarrow\theta-\eta\frac{g_{\text{scaled}}}{S}

NativeScaler(本项目 loss_scaler = NativeScaler())还会动态调整 S:若出现 inf/nan 就减小 S 并跳过该步。

本项目代码:

with torch.cuda.amp.autocast(): # 前向用 fp16(部分算子保持 fp32)
output = model(images)
loss = criterion(output, target)
loss_scaler(loss, optimizer, ...) # 缩放后的反向与更新

CPU 上的行为:torch.cuda.amp.autocast() 在无 CUDA 时会打印警告并自动禁用(本项目日志里那句 User provided device_type of 'cuda', but CUDA is not available. Disabling 就是它)——不影响数值正确性,只是没有加速。


61. 分布式训练 DDP#

为什么需要:ViT-B 在 ImageNet/AudioSet 规模上单卡训不完;且大 batch 通常带来更稳的梯度估计。

DDP(DistributedDataParallel)原理:

每张卡一份模型副本(参数相同)
├─ 各自读不同的数据子集(DistributedSampler 按 rank 切分)
├─ 各自前向 + 反向,得到本地梯度
├─ all-reduce 求平均梯度 ← 关键通信
└─ 各自用同一梯度更新参数(因此参数始终保持一致)

相关 API(本项目用到):

API作用
torch.distributed.launch --nproc_per_node=4启动 4 个进程(每卡一个)
DistributedSampler数据按 rank 切分,保证不重叠
init_process_group建立通信组
all_reduce / all_gather梯度平均 / 收集所有卡的结果
concat_all_gather(stat.py)评估时把各卡输出拼起来算整体 mAP
--dist_eval是否分布式评估(单卡时关闭)

有效批大小(effective batch size):

eff_batch=batch_size×accum_iter×ngpus\text{eff\_batch}=\text{batch\_size}\times\text{accum\_iter}\times\text{ngpus}

梯度累积(accum_iter)是”显存不够时用小步数凑大 batch”的手段:多次前向累积梯度、到 accum_iter 次再更新一次。学习率按 eff_batch 缩放(第 42 节)。

单卡如何跑:去掉 torch.distributed.launch,加 --dist_eval 关闭,并把 --batch_size 调小即可(本项目 inf.sh 里就是 --nproc_per_node=1)。

注意:DDP 下 BatchNorm 需要 SyncBatchNorm 才能跨卡统计(本项目用 LayerNorm,无此问题——这也是 Transformer 对分布式更友好的一个原因)。



第八部分 音频深度学习地基#

62. 音频表示的选择#

音频进神经网络有四条路,各有取舍:

表示做法优点缺点代表模型
原始波形直接吃 16 kHz 样本无信息损失、端到端序列极长(10 秒 = 160000 点)、学习难WaveNet、wav2vec2、SincNet
频谱图(STFT)abs(STFT) 当图像保留时频结构、可用视觉骨干维度大、相位丢失AST、AudioMAE
log-mel / fbank梅尔滤波 + 取对数维度可控(128×1024)、符合听觉、成熟丢失细粒度频率信息本项目、大部分音频分类
可学习前端用可训练卷积/Learnable filterbank 代替固定梅尔数据足够时更优数据少时易过拟合LEAF、SincNet

本项目的选择逻辑:

  • 主干是视觉 Transformer(ViT),天然吃”图像” → 需要二维表示 → 选 fbank(128 mel × 1024 帧);
  • 数据规模不足以支撑从波形端到端训 ViT → 用 log-mel 降低输入复杂度;
  • 与上游 AudioMAE/AST 代码兼容(同款 fbank 管线)→ 可直接复用预训练权重。

取舍核心:fbank 是”人为设计的归纳偏置”(梅尔刻度 + 对数压缩),在数据有限时比”让网络自己学”更稳。


63. AST 与 AudioMAE#

AST(Audio Spectrogram Transformer):

  • 首个纯注意力(无卷积)的音频分类模型;
  • 把 128×1024 的 log-mel 频谱切成 16×16 patch(stride 10,重叠),送 ViT;
  • 输出多标签(AudioSet 527 类),用 BCE + mAP 评估;
  • 在 AudioSet 上超过 CNN 模型,证明”频谱图 = 图像”这条路可行。

AudioMAE:

  • 在 AST 的思路上引入 MAE 自监督预训练:先用海量无标注音频学通用频谱表示,再微调;
  • 保留 AST 的输入格式与 ViT 主干,因此预训练权重可以迁移到 AST 风格的分类任务;
  • 关键改进:2D 时频掩码(同时遮时间段与频带段)、Swin 局部注意力解码器、norm_pix_loss。

本项目的继承关系(读代码时的定位依据):

AudioMAE (facebookresearch)
├─ models_mae.py ──► TimbreMask/models_mae.py(几乎原样,加 decoder_mode / roll_mag_aug)
├─ models_vit.py ──► TimbreMask/models_vit.py 与 html/model/models_vit.py(分类用)
├─ dataset.py ──► 同款 AudiosetDataset(AST 格式 JSON + class_labels_indices.csv)
└─ 数据格式 ──► {"wav":..., "labels":"/mid/1,/mid/2"}(AST 风格)

项目在 AudioMAE 之上做的三处定制(这是”复用而不是照抄”的证据):

  1. 自定义标签集:把 AudioSet 的 527 类换成 11 类自定义音色标签(/t/…、/r/…、/b/…、/s/…);
  2. 评估指标:stat.py 里加上 group_acc(按厚薄/虚实/明暗/性别分组算组内准确率);
  3. 工程简化:html/model/inference.py 把训练框架里的推理逻辑抽成 run_inference(),供 Flask 调用。

64. 2D 时频掩码#

一维掩码(原版 MAE):随机遮住序列中的若干 patch。 2D 时频掩码(AudioMAE,本项目微调也开):

# models_vit.py::random_masking_2d(use_custom_patch=True 时)
T, F = 101, 12 # 时间 × 频率 的 patch 网格
len_keep_T = int(T * (1 - mask_t_prob)) # 按时间轴遮 20%
len_keep_F = int(F * (1 - mask_f_prob)) # 再按频率轴遮 20%

为什么要在两个方向分别遮:

  1. 时间方向遮挡 → 迫使模型用上下文推断缺失时间段(学时间连续性);
  2. 频率方向遮挡 → 迫使用其它频带推断被遮频带(学频带间相关性,例如”低频谐波缺失时如何从高频推断音色”);
  3. 二者分别施加,比”随机散点遮挡”更接近真实损坏(真实的音频缺失常是”整段静音”或”某频段被滤波掉”)。

参数含义:mask_t_prob=0.2 表示遮 20% 的时间 patch(约 20 帧 ≈ 200 ms 的段落),mask_f_prob=0.2 表示遮 20% 的频带(约 2.4 个 16-wide 频带)。

注意:掩码是训练技巧。本项目推理时 audio_conf 里 freqm=0, timem=0,且 mask_t_prob=mask_f_prob=0.0,即”不遮”——保证推理确定性。


65. 片段切分与聚合、多标签与多任务#

片段(chunk)切分:因为 pos_embed 固定(1213),模型只能吃 1024 帧(≈10.24 秒):

num_chunks = n_frames // target_length # 完整片段数
# 不足一个片段 → ZeroPad2d 补零成一个片段

聚合(aggregation):

方式含义适用
mean(本项目默认)各片段概率取平均长音频内容均匀(如播报)
max取最大值只关心”是否出现”(事件检测)

注意:聚合是推理策略,不影响模型本身;但它会改变结果分布(max 更激进、mean 更保守),第 4 篇可以把它当作实验变量。

多标签(multi-label):一条语音同时拥有多个标签(厚薄 2 且 虚实 3 且 明暗 1 且 女声)→ 11 维输出用 sigmoid + BCE。

多任务(multi-task)与辅助标签:11 类中的性别(male/female)不是评估目标,而是辅助任务。它带来三个好处:

  1. 共享层得到额外监督:模型为了学好性别,必须提取与声学相关的特征(如基频范围),这些特征对音色判断也有用;
  2. 缓解过拟合:更多监督信号 = 更强的约束;
  3. 实用副产品:系统可顺带报告性别。

多任务损失:本项目其实是把多任务当成”多标签”统一用一个 BCE 处理(无任务权重);更精细的做法是给不同任务加权(如 L = L_thick + L_real + L_bright + 0.3·L_sex),是可选改进点。


66. 评估口径:mAP vs 组内准确率#

指标计算回答的问题本项目用途
mAP11 类 AP 的平均“排序质量好不好”与其他论文可比
组内准确率 group_acc组内 argmax 与真值比对“厚薄判成几级对不对”业务可解释、可诊断
混淆矩阵(3×3/2×2)每维度的预测 vs 真值“错在哪一级、偏上还是偏下”建议复现时补做
与物理分的一致性模型等级 vs 物理量化等级的一致率“两条路线有多一致”建议补做,能直接支撑融合设计

为什么要补做”两路线一致率”:融合逻辑假设”模型与物理分在多数样本上一致,仅在少数疑难样本上分歧”。若实测一致率很低,说明融合规则(阈值 20)不成立——这是复现中最有价值的验证性实验之一。



第九部分 Web 与前端地基#

67. HTTP 协议与 multipart/form-data#

HTTP 请求结构:

POST /api/evaluate HTTP/1.1 ← 请求行:方法 + 路径 + 版本
Host: example.com ← 头部
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryXXXX
Content-Length: 12345
<二进制/文本请求体> ← 实体

常用方法:GET(取)、POST(创建/提交)、PUT/PATCH(更新)、DELETE(删)。 状态码:2xx 成功(200)、3xx 重定向、4xx 客户端错(400 参数错、413 太大、404 不存在)、5xx 服务端错(500 内部错误)。本项目录音报错时前端收到 500。

multipart/form-data 详解(文件上传的标准格式):请求体被 boundary 分隔成多个”部件”,每个部件有自己的头部:

------WebKitFormBoundaryXXXX
Content-Disposition: form-data; name="audio"; filename="recording.wav"
Content-Type: audio/wav
<文件二进制数据>
------WebKitFormBoundaryXXXX--
  • boundary 是分隔字符串(浏览器自动生成);
  • 前端用 FormData.append('audio', blob, 'recording.wav') 构造;
  • 后端 Flask 用 request.files.get('audio') 取出(name 必须一致);
  • 注意:Content-Type 边界与 Content-Length 必须由客户端正确设置(用了 fetch + FormData 就自动处理)。

其它常见 Content-Type:application/json(JSON 体)、application/x-www-form-urlencoded(表单)、audio/wav(裸音频)。


68. 同源策略、CORS 与安全上下文#

同源:协议 + 域名 + 端口三者完全相同。

同源策略:浏览器默认禁止页面用 JS 读取”跨源”响应(防 CSRF/信息泄露)。本项目前后端同源(都用 http://127.0.0.1:5001 提供),所以不需要 CORS。

CORS(跨域资源共享):如果前端部署在 a.com、后端在 b.com,后端要返回:

Access-Control-Allow-Origin: https://a.com

并在预检请求(OPTIONS)中允许相应方法与头部。本项目若拆分部署(前端静态托管 + 后端 API)就需要加 CORS。

安全上下文(Secure Context):浏览器规定只有 HTTPS 或 localhost 才允许 getUserMedia(麦克风)。这是为什么部署到域名时必须配 HTTPS——否则页面上”录音”按钮会直接失败(连权限弹窗都不会出现)。

HTTPS 要点:TLS 握手 → 非对称加密协商会话密钥 → 对称加密传输;证书由 CA 签发,验证域名归属。部署时 Nginx 终结 TLS(证书装在 Nginx),后端仍走 HTTP 内网通信。


69. Flask、WSGI 与请求生命周期#

WSGI:Python Web 服务器与应用的接口标准(app(environ, start_response))。Flask 应用是 WSGI 应用,gunicorn/uwsgi 是 WSGI 服务器。

Flask 请求生命周期:

请求 → WSGI 服务器 → Flask App
├─ URL 路由匹配(@app.route)
├─ 进入请求上下文(request / session 可见)
├─ 执行视图函数 → 返回响应对象(字符串/JSON/模板渲染结果)
└─ 响应返回 → 中间件/after_request → 客户端

本项目用到的 Flask 概念:

概念代码说明
路由@app.route("/api/evaluate", methods=["POST"])URL → 函数
请求数据request.files.get("audio")multipart 文件
JSON 响应jsonify(result)自动设置 Content-Type: application/json
状态码return jsonify({...}), 400/500元组第二项
配置app.config["MAX_CONTENT_LENGTH"] = 50*1024*1024上传上限
模板render_template("index.html")Jinja2
静态文件/static/main.js由 Flask 直接提供
调试app.run(debug=True)仅开发用(自动重启 + 调试器)

为什么生产不能用 debug=True:调试器允许从浏览器执行任意 Python 代码(Werkzeug 调试页的经典漏洞),app.run 单线程也不够用。


70. gunicorn 与反向代理#

gunicorn 工作模型:

gunicorn(master 进程)
├─ worker 1(同步/gevent,独立进程)
├─ worker 2
└─ …
  • 同步 worker:一次处理一个请求,模型推理是 CPU 密集的,够用;
  • worker 数建议:2×CPU + 1(但本项目必须为 1:每个 worker 会各自加载约 1 GB 模型 → 内存 N × 1 GB,且推理本身 CPU 占满);
  • --timeout 900:请求超时(首次加载模型 + 长音频推理很慢);
  • 预加载(--preload)可让所有 worker 共享一份模型内存(复制写时技术),但本项目 -w 1 已无必要。

Nginx 反向代理的作用:

作用说明
端口/进程隔离外部只暴露 80/443,Python 服务只监听 127.0.0.1
TLS 终结证书、加密开销交给 Nginx
静态文件直接由 Nginx 提供(比 Python 快)
缓冲与超时proxy_read_timeout、client_max_body_size
限流/压缩/日志limit_req、gzip、access_log

本项目关键配置(否则会 502/413):

client_max_body_size 60m; # ≥ 后端 50MB
proxy_read_timeout 900s; # 推理慢,不能沿用默认 60s

SELinux:RHEL 系默认禁止 Nginx 主动连后端端口,需要 setsebool -P httpd_can_network_connect 1。


71. 前端录音:getUserMedia / MediaRecorder#

流程:

const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); // 请求麦克风
const rec = new MediaRecorder(stream, { mimeType: 'audio/webm;codecs=opus' }); // 选择编码容器
const chunks = [];
rec.ondataavailable = e => { if (e.data.size > 0) chunks.push(e.data); }; // 分片回调
rec.onstop = () => { const blob = new Blob(chunks, { type: rec.mimeType }); }; // 合成 Blob
rec.start(250); // 每 250ms 抛一次数据(timeslice)
rec.stop();

关键概念:

概念说明
getUserMedia请求设备权限;必须 HTTPS/localhost;返回 MediaStream
MediaRecorder把媒体流编码成容器;编码格式由浏览器决定(Chrome=WebM/Opus,Safari 可能 MP4/AAC)
isTypeSupported检测可用 MIME,本项目优先 audio/webm;codecs=opus
timeslice分片间隔;分片能减少内存峰值、支持长录音
Blob二进制大对象(不可直接运算,需 arrayBuffer() 读成 ArrayBuffer)

常见坑:

  • 忘写 stream.getTracks().forEach(t => t.stop()) → 麦克风一直占用(指示灯不灭);
  • audioChunks 未清空 → 第二次录音混入第一次数据;
  • 用 ondataavailable 的 e.data.size === 0 检查空片(本项目就做了);
  • 移动端与 Safari 的 MIME 兼容性(要 fallback)。

72. Web Audio API 与音频解码#

核心对象:

对象用途
AudioContext音频处理图的环境(有 sampleRate)
decodeAudioData(arrayBuffer)把任意浏览器支持的容器解码为 PCM 的 AudioBuffer(异步)
AudioBuffer解码结果:numberOfChannels / sampleRate / length / getChannelData(i)
AnalyserNode实时频谱/波形分析(本项目画波形用 fftSize=256 + getByteTimeDomainData)
OfflineAudioContext离线渲染(可在非实时下重采样/混音)
MediaStreamAudioSourceNode把麦克风流接入处理图

本项目用它解决了什么:浏览器能解 Opus/AAC/MP3,于是”解码 → 重编码 WAV”在前端完成,后端彻底不需要 ffmpeg(第 24 节)。

注意:decodeAudioData 需要 ArrayBuffer(不是 Blob),所以要 await blob.arrayBuffer();解码是异步的,必须 await。


73. Blob / ArrayBuffer / TypedArray#

类型是什么典型用法
Blob不可变的二进制”文件块”(含 MIME 类型)录音结果、上传对象
File带文件名的 Blob(继承自 Blob)<input type=file> 结果
ArrayBuffer一段固定长度的原始内存解码输入
TypedArray(Int16Array 等)对 ArrayBuffer 的类型化视图逐样本读写
DataView带字节序控制的读写视图写 WAV 头(setInt16(off, val, true) 的小端)

互转:

const buf = await blob.arrayBuffer(); // Blob → ArrayBuffer
const view = new DataView(buf); // 读写视图
const blob2 = new Blob([buf], {type:'audio/wav'}); // ArrayBuffer → Blob

本项目:audioBufferToWavBlob 用 ArrayBuffer 建 44 + dataSize 字节 → DataView 写头 → 循环写 int16 样本 → new Blob([buffer], {type:'audio/wav'})。


74. JS 异步与 fetch#

事件循环(Event Loop):JS 单线程,通过”任务队列 + 微任务队列”实现并发。关键影响:任何耗时操作(解码、网络、大循环)都会冻结 UI;因此解码用异步 API,requestAnimationFrame 用于动画。

Promise 与 async/await:

async function evaluateAudio() {
const res = await fetch('/api/evaluate', { method: 'POST', body: formData });
if (!res.ok) throw new Error(`Server error: ${res.status}`);
const data = await res.json();
showResults(data);
}
  • fetch 返回 Promise<Response>,只有网络错误才 reject;HTTP 4xx/5xx 需要手动检查 res.ok(本项目就得处理 500);
  • async 函数返回 Promise,await 暂停该函数直到 Promise 完成;
  • 异常必须 try/catch,否则未捕获的 Promise 拒绝会导致静默失败(本项目 catch 里 alert 提示并复位)。

阻塞陷阱(复现时可能遇到):前端把几 MB 的 WAV 用 JS 循环逐样本编码时,若是 10 秒以上录音可能造成几百毫秒卡顿(本项目 12 秒 ≈ 384K 样本,可接受;若要录几分钟,应改 Web Worker)。


75. Canvas 与 Chart.js#

Canvas:位图绘制上下文(getContext('2d')),适合逐帧绘制(本项目画录音波形)。常用 API:clearRect / beginPath / moveTo / lineTo / stroke / fillRect。

Chart.js(本项目用 4.4.4,CDN 引入):

new Chart(ctx, {
type: 'radar',
data: {
labels: ['厚薄', '明暗', '虚实'],
datasets: [{ data: [scores.thickness, scores.brightness, scores.solidity] }]
},
options: { scales: { r: { min: 0, max: 3, ticks: { stepSize: 1 } } } }
});
  • type:radar(雷达图)、bar(柱状图)、line 等;
  • data.labels / data.datasets[].data 是核心数据结构;
  • options 控制轴范围、刻度、动画、tooltip;
  • 仪表盘常用套路:固定 min/max 才能让不同样本可直接对比(本项目雷达图 0–3 级)。

离线部署注意:CDN 引入在纯内网会白屏 → 需把 chart.umd.min.js 下到 static/ 并改 <script src>。

响应式细节:canvas 需要容器有明确高度,否则图表高度为 0(常见”图表不显示”原因)。



第十部分 工程与运维地基#

76. 进程、线程、GIL 与并发#

  • 进程:独立内存空间;线程:共享内存、切换开销小;
  • GIL(全局解释器锁):CPython 同一时刻只允许一个线程执行 Python 字节码 → CPU 密集型任务多线程无加速(但 numpy/PyTorch 的底层 C 运算会释放 GIL,所以矩阵运算仍能并行);
  • 因此 CPU 密集的 Python 服务常多进程扩展(gunicorn 多 worker);
  • 本项目的内存约束:Torch 模型常驻内存约 1 GB/进程,所以 -w 1;若要并发更多请求,可以:① 队列排队(当前行为);② 用 GPU 提升单请求速度;③ 模型量化/ONNX 减小内存后适度加 worker。

并发与队列的直觉:CPU 推理 1030 秒/条 → 单 worker 吞吐约 26 条/分钟。10 人同时提交 → 后面的人等待时间线性增长。生产环境应设置请求队列上限 + 前端提示预计等待时间(这是可以写进改进清单的工程点)。


77. 虚拟环境、wheel 与依赖管理#

概念说明
venv隔离的 Python 环境(独立 site-packages)
pip包管理器;pip install -r requirements.txt
wheel(.whl)预编译二进制分发格式(装得快,无需编译)
sdist(.tar.gz)源码分发,安装时需编译(易失败,尤其 Windows)
requirements.txt依赖清单;建议 pip freeze > requirements.lock.txt 固化版本
--index-url / --extra-index-url切换包源(PyTorch 有独立源)
--no-deps跳过依赖解析(本项目离线装 wheel 时用过,代价是需自己补齐依赖)
--no-compile不生成 __pycache__(慢速磁盘上可显著提速)

为什么”锁版本”重要:深度学习生态版本耦合极强(torch↔torchaudio↔torchvision↔timm↔numpy↔numba),不锁版本 → 半年后重装就跑不起来。本项目实测可用组合已记录在文档里(torch 2.8.0+cpu 等),建议直接固化为 requirements.lock.txt。


78. 依赖版本坑的完整回溯(本项目实测)#

现象根因解决
Windows 装 torch 从官方源极慢/中断网络与镜像差异换清华镜像(PyPI)或分块断点续传下载 wheel 后离线安装
Linux pip install torch 拉了 2.4 GBPyPI 的 Linux torch 是 CUDA 构建加 --index-url https://download.pytorch.org/whl/cpu
torchaudio.load 报 TorchCodec 缺失torchaudio ≥2.9 改用 TorchCodec 后端降到 2.8(sox 后端)或安装 torchcodec 及其 FFmpeg 动态库
import timm 报 torchvision 扩展错误timm 顶层导入 torchvision,且其 _C_stable 与 torch CPU 构建不兼容装同源配套 torchvision;或用最小垫片满足导入(本项目采用后者,因为项目运行时并不真的调用 torchvision)
libsndfile not foundsoundfile 的”纯 Python wheel”不含 DLLWindows 用带 _soundfile_data/libsndfile_x64.dll 的 win_amd64 wheel;Linux dnf/apt install libsndfile1
scipy 警告 numpy 版本超上界scipy 1.9 元数据限定 numpy<1.26仅警告可忽略;严格做法是 numpy 1.25.x 或升级 scipy
numba 报 colorama 版本解析错误colorama 的 rc 版本号 0.4.6rc1 无法被 numba 解析安装稳定版 colorama 0.4.6
ModuleNotFoundError: decoratorlibrosa 的隐式依赖未装(--no-deps 所致)补装 decorator
PyTorch 2.6+ 加载旧 checkpoint 报错默认 weights_only=Truetorch.load(..., weights_only=False)

方法论:遇到依赖问题按”库→版本→源→二进制依赖”四层排查:① 报错说缺哪个库;② 版本是否与配套库匹配;③ 包源是否给对了构建(CPU/CUDA);④ 该库是否还需要系统级动态库(DLL/so)。


79. systemd、日志与排错方法论#

systemd 服务最小模板:

[Unit]
Description=Voice Timbre Evaluation
After=network.target
[Service]
WorkingDirectory=/opt/timbre/html
ExecStart=/opt/timbre/venv/bin/gunicorn -w 1 -b 127.0.0.1:5001 --timeout 900 app:app
Restart=always
RestartSec=5
Environment=OMP_NUM_THREADS=4
[Install]
WantedBy=multi-user.target
  • systemctl daemon-reload && systemctl enable --now timbre;
  • systemctl status timbre、journalctl -u timbre -f 看日志;
  • Restart=always 保证崩溃自动拉起;
  • Environment= 可注入环境变量(如线程数、API Key)。

排错五步法(本项目实战总结):

  1. 读 traceback 的最后几行:File "x.py", line N, in func → 精确定位;
  2. 区分环境错与逻辑错:
    • 环境错:ModuleNotFoundError、NoBackendError、cannot load library、Format not recognised;
    • 逻辑错:ValueError: shapes ... not aligned、IndexError、KeyError;
  3. 最小复现:把可疑函数单独拿出来喂一条已知音频(本项目两个 bug 都靠这招定位);
  4. 形状即真相:数值/深度学习报错先 print(x.shape),90% 是形状问题;
  5. 改完必回归:跑”通过用例 + 曾经失败用例”(本项目修完 speechprocess 后专测”强制三围 level=1”以覆盖原盲区)。

日志分级建议:INFO 记请求与结果摘要、WARNING 记可疑输入(短音频/削波)、ERROR 记异常与堆栈、DEBUG 记形状与中间值(生产关掉)。


80. 资源估算(会算才敢上线)#

资源估算公式本项目实测
模型权重内存参数量 × 4 B(fp32)86.2M × 4 ≈ 345 MB
推理峰值内存权重 + 激活 + 框架开销约 1.5–2.5 GB(含 torch 运行时)
checkpoint 磁盘权重 + 优化器状态(×3)1.02 GB(发布可精简到 ~345 MB)
单条推理时延(CPU)与 patch 数、层数、CPU 主频相关12 秒音频约 6–30 s(含预处理)
单条 GPU 时延Tensor Core 加速1–3 s(估计,需实测)
吞吐1 / 单条时延 × worker 数单 worker ≈ 2–6 条/分钟
磁盘增长每条评估存 1 个 WAV16 kHz 单声道 ≈ 32 KB/s → 每天 1000 条 × 10 s ≈ 320 MB

部署建议汇总:CPU 4 核 / 8 GB 内存 / ≥5 GB 磁盘可满足小规模演示;并发需求高时上 GPU + 队列。



第十一部分 概念辨析速查表(24 组)#

#容易混的一对区别要点
1幅度谱 vs 能量谱前者是复数取模 abs(X),后者是模的平方 abs(X)²;能量谱对强弱更敏感
2功率谱 vs 能量谱严格意义上前者需按带宽/时长归一化;占比计算中差别自动抵消
3采样率 vs 位深前者决定频率上限(sr/2),后者决定动态范围(≈6 dB/bit)
4容器 vs 编码盒子 vs 内容(WebM 容器 + Opus 编码)
5有损 vs 无损有损丢弃听不见的成分(MP3/AAC/Opus),无损保留全部(WAV/FLAC)
6频谱 vs 倒谱频谱横轴是频率;倒谱横轴是”倒频率”(时间的倒数),用于分离声源与滤波器
7fbank vs MFCC前者是 log-mel(128 维);后者再做 DCT 并截断(13 维)
8Mel 刻度 vs Hertz前者是人耳感知刻度(对数式),后者是物理频率线性刻度
9STFT vs FFTSTFT = 分帧 + 逐帧 FFT,得到时频二维;FFT 只是单帧变换
10n_fft vs hop_length前者是每帧长度(决定频率分辨率),后者是帧间步长(决定时间密度)
11补零 vs 提高分辨率补零只是频率轴上插值,不提高真实分辨率
12多分类 vs 多标签前者各类互斥(softmax+CE),后者可同时成立(sigmoid+BCE)
13sigmoid vs softmax前者逐维独立 (0,1),后者全维竞争求和为 1
14BCE vs CEBCE 处理每个标签的二分类;CE 处理互斥多分类
15mAP vs 准确率前者看排序质量、抗不平衡;后者看硬判决正确率、易被不平衡欺骗
16精确率 vs 召回率前者”判对的占比”(宁缺毋滥),后者”抓到的占比”(宁滥勿缺)
17参数 vs 超参数前者训练学到(权重),后者人工设定(学习率、层数)
18batch / iteration / epoch一批样本 / 一次参数更新 / 遍历全部数据一次
19L1 vs L2 正则L1 促稀疏(特征选择),L2 促平滑(防过拟合)
20BatchNorm vs LayerNormBN 跨样本按通道统计(依赖 batch),LN 单样本跨特征统计(Transformer 用)
21预训练 vs 微调前者自监督学通用表示,后者用标注数据适配任务
22自编码器 vs MAEMAE 是”高掩码率 + 非对称 + 只算遮住部分损失”的特化自编码器
23归一化 vs 标准化中文语境常混用;严格说归一化→[0,1],标准化→零均值单位方差
24物理特征 vs 深度特征前者人造可解释(12 段占比),后者学习得到不可解释(768 维嵌入)


第十二部分 15 个最小实验(可直接跑)#

环境:D:\音色评估项目总结\.venv_html\Scripts\python.exe,脚本放在项目根目录运行。 全部为”几分钟内能跑完、能亲眼看到结论”的实验,做完这套基本等于把本篇知识过了一遍手。

实验 1:观察混叠(10 kHz 用 16 kHz 采样会变成 6 kHz)#

import numpy as np
sr_high, sr_low, dur = 100000, 16000, 0.02
t = np.arange(int(sr_high*dur))/sr_high
x = np.sin(2*np.pi*10000*t) # 真实 10 kHz
xs = x[::int(sr_high/sr_low)] # 朴素抽取(无抗混叠)
f = np.fft.rfftfreq(len(xs), 1/sr_low)
spec = np.abs(np.fft.rfft(xs))
print('峰值频率 ≈', f[np.argmax(spec)], 'Hz ← 应为 6000 (16000-10000)')

实验 2:验证量化 SNR 公式(每个 bit 约 6 dB)#

import numpy as np
for bits in [8, 12, 16]:
q = 2.0/(2**bits) # 满量程 2.0 的量化步长
x = np.sin(2*np.pi*440*np.arange(16000)/16000)
xq = np.round(x/q)*q
noise = x-xq
snr = 10*np.log10(np.mean(x**2)/np.mean(noise**2))
print(f'{bits} bit: SNR={snr:5.1f} dB (理论 {6.02*bits+1.76:5.1f} dB)')

实验 3:FFT 分辨率——n_fft 决定能否分开两个邻近频率#

import numpy as np, librosa
sr = 16000
y = np.sin(2*np.pi*1000*np.arange(sr)/sr) + np.sin(2*np.pi*1020*np.arange(sr)/sr)
for n in [512, 2048, 8192]:
S = np.abs(librosa.stft(y, n_fft=n, hop_length=n//4))
spec = S.mean(axis=1); f = librosa.fft_frequencies(sr=sr, n_fft=n)
m = (f>900)&(f<1120)
print(f'n_fft={n:5d} Δf={sr/n:5.2f}Hz 该区间峰数≈{len(np.where(np.diff(np.sign(np.diff(spec[m])))<0)[0])}')

实验 4:窗函数与频谱泄漏#

import numpy as np, librosa, matplotlib.pyplot as plt
sr = 16000; n = 2048
x = np.sin(2*np.pi*1000.5*np.arange(n)/sr) # 非整数周期 → 泄漏
for name, win in [('矩形', np.ones(n)), ('Hann', np.hanning(n))]:
S = np.abs(np.fft.rfft(x*win)); f = np.fft.rfftfreq(n, 1/sr)
m = (f>700)&(f<1300)
side = np.mean(S[m])/S.max() # 旁瓣相对能量(泄漏程度)
print(f'{name:4s} 旁瓣/主瓣 ≈ {side:.2e}')

实验 5:STFT 参数的时频权衡(可视化)#

import numpy as np, librosa, librosa.display, matplotlib.pyplot as plt
y, sr = librosa.load('test_input.wav', sr=16000)
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
for ax, (n, h) in zip(axes, [(512, 128), (2048, 512), (8192, 2048)]):
librosa.display.specshow(librosa.amplitude_to_db(np.abs(librosa.stft(y, n_fft=n, hop_length=h))),
sr=sr, ax=ax, y_axis='log', x_axis='time')
ax.set_title(f'n_fft={n}, hop={h}')
plt.tight_layout(); plt.savefig('exp5_stft.png', dpi=90)
print('已保存 exp5_stft.png:注意窗越长,频率越细但时间越糊')

实验 6:验证”能量占比”抗音量(项目设计的关键性质)#

import numpy as np, librosa
y, sr = librosa.load('test_input.wav', sr=16000)
def band_props(y, sr, n_fft=2048, hop=512):
E = np.abs(librosa.stft(y, n_fft=n_fft, hop_length=hop))**2
p = E.sum(axis=1)/E.sum()
f = librosa.fft_frequencies(sr=sr, n_fft=n_fft)
bands = [(0,66),(66,137),(137,216),(216,301),(301,395),(395,498),
(498,610),(610,733),(733,867),(867,1014),(1014,1174),(1174,1350)]
return np.array([p[(f>=lo)&(f<=hi)].sum() for lo,hi in bands])
a = band_props(y, sr); b = band_props(y*0.1, sr) # 音量降到 10%
print('最大差异 =', np.abs(a-b).max(), ' ← 应接近 0(占比与音量无关)')

实验 7:验证三套频段确实是”等梅尔切分”#

import numpy as np
mel = lambda f: 2595*np.log10(1+f/700)
hz = lambda m: 700*(10**(m/2595)-1)
for name, (lo, hi) in [('厚薄', (0,1350)), ('虚实', (50,4280)), ('明暗', (0,4280))]:
edges = hz(np.linspace(mel(lo), mel(hi), 13))
print(name, '理论边界:', np.round(edges).astype(int).tolist())

实验 8:亲手构造梅尔滤波器组并与 librosa 对比#

import numpy as np, librosa, matplotlib.pyplot as plt
sr, n_fft, M = 16000, 2048, 128
mel = lambda f: 2595*np.log10(1+f/700); hz = lambda m: 700*(10**(m/2595)-1)
pts = hz(np.linspace(mel(20), mel(sr/2), M+2))
f = librosa.fft_frequencies(sr=sr, n_fft=n_fft)
H = np.zeros((M, len(f)))
for i in range(M):
lo, mid, hi = pts[i], pts[i+1], pts[i+2]
H[i] = np.where((f>=lo)&(f<=mid), (f-lo)/(mid-lo),
np.where((f>mid)&(f<=hi), (hi-f)/(hi-mid), 0))
print('自建滤波器组形状:', H.shape, ' 与 librosa 的相关系数:',
np.corrcoef(H.ravel(), librosa.filters.mel(sr=sr, n_fft=n_fft, n_mels=M).ravel())[0,1])

实验 9:用 Fisher 损失学权重,并与 sklearn LDA 对比方向#

import numpy as np, torch, torch.nn as nn
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis as LDA
rng = np.random.default_rng(0)
X0 = rng.normal(0, 1, (200, 12)) + np.linspace(1, 0, 12)
X1 = rng.normal(0, 1, (200, 12)) + np.linspace(0, 1, 12)
X = torch.tensor(np.vstack([X0, X1]), dtype=torch.float32)
y = torch.cat([torch.zeros(200), torch.ones(200)])
class FisherLoss(nn.Module):
def forward(self, o, y):
m0, m1 = o[y==0], o[y==1]
return (m0.var(unbiased=False)+m1.var(unbiased=False)) / ((m1.mean()-m0.mean())**2 + 1e-6)
lin = nn.Linear(12, 1)
opt = torch.optim.Adam(lin.parameters(), lr=0.05)
for _ in range(500):
loss = FisherLoss()(lin(X).squeeze(1), y); opt.zero_grad(); loss.backward(); opt.step()
w_torch = lin.weight.detach().numpy().ravel()
w_lda = LDA().fit(np.vstack([X0, X1]), [0]*200+[1]*200).coef_.ravel()
cos = float(w_torch @ w_lda / (np.linalg.norm(w_torch)*np.linalg.norm(w_lda)))
print('与 sklearn LDA 方向的余弦相似度 =', round(cos, 4), '(接近 ±1 说明方向一致,符号可反)')

实验 10:手算 [a, b, 中心] 映射(对应 calbound.py)#

def linear_params(mn, mx): a = 100/(mx-mn); return a, -mn*a
for rng_, center in [((-55,431), 37.07), ((-244,95.25), 13.9), ((-806.8,-368.56), -702.8)]:
a, b = linear_params(*rng_)
print(f'区间{rng_} → a={a:.5f}, b={b:.3f}, 中心映射到 {a*center+b:.2f} 分')

实验 11:验证 fbank 帧数 ≈ 时长 / 10 ms#

import torch, torchaudio, math
w, sr = torchaudio.load('test_input.wav') # 12 秒
fbank = torchaudio.compliance.kaldi.fbank(w - w.mean(), htk_compat=True, sample_frequency=sr,
use_energy=False, window_type='hanning', num_mel_bins=128, dither=0.0, frame_shift=10)
print('时长 %.2fs 帧数 %d ≈ 时长/0.01 = %.0f (target_length=1024 对应 %.2fs)'
% (w.shape[1]/sr, fbank.shape[0], w.shape[1]/sr/0.01, 1024*0.01))

实验 12:ViT 前向形状追踪(不需要权重)#

import torch, sys
sys.path.insert(0, 'html')
from model.models_vit import vit_base_patch16
import torch.nn as nn
m = vit_base_patch16(num_classes=11, global_pool=True, drop_path_rate=0.0, mask_2d=True, use_custom_patch=False)
from inference import PatchEmbed_new
m.patch_embed = PatchEmbed_new(img_size=(1024,128), patch_size=16, in_chans=1, embed_dim=768, stride=10)
m.pos_embed = nn.Parameter(torch.zeros(1, 1212+1, 768), requires_grad=False)
x = torch.randn(1, 1, 1024, 128)
print('patch 数 =', m.patch_embed.num_patches, '| 输出形状 =', tuple(m.eval()(x).shape))
print('参数量 ≈ %.1f M' % (sum(p.numel() for p in m.parameters())/1e6))

实验 13:可视化 MAE 的 2D 时频掩码#

import numpy as np, matplotlib.pyplot as plt
rng = np.random.default_rng(1); T, F, mt, mf = 101, 12, 0.2, 0.2
grid = np.zeros((T, F))
keep_t = rng.permutation(T)[:int(T*(1-mt))]; keep_f = rng.permutation(F)[:int(F*(1-mf))]
mask = np.ones((T, F)); mask[keep_t][:, keep_f] = 0
plt.figure(figsize=(6,4)); plt.imshow(mask, aspect='auto', cmap='gray_r')
plt.xlabel('频率 patch (12)'); plt.ylabel('时间 patch (101)'); plt.title('黑色=保留(20%×80%=16%)')
plt.savefig('exp13_mask.png', dpi=90); print('保留比例 =', round(1-mask.mean(), 3), '已保存 exp13_mask.png')

实验 14:BCE 与 CE 的数值对比#

import torch, torch.nn.functional as F
logits = torch.tensor([[2.0, 0.5, -1.0]]) # 三个类别
target_multi = torch.tensor([[1., 1., 0.]]) # 多标签:可以同时为真
target_single = torch.tensor([0]) # 多分类:只有一个答案
print('BCE (多标签, sigmoid):', round(F.binary_cross_entropy_with_logits(logits, target_multi).item(), 4))
print('CE (多分类, softmax):', round(F.cross_entropy(logits, target_single).item(), 4))
print('sigmoid 概率:', [round(v,3) for v in torch.sigmoid(logits).tolist()[0]],
'| softmax 概率:', [round(v,3) for v in torch.softmax(logits,1).tolist()[0]])

实验 15:端到端最小推理(脱离 Web,直接拿 11 维结果)#

import sys, torch
sys.path.insert(0, 'html')
from inference import run_inference
r = run_inference('test_input.wav') # 注意:在项目根目录运行会找不到 html 内相对路径,需 cd html
print('success:', r['success'])
print('labels :', r['labels'])
print('probs :', [round(x, 4) for x in r['raw_outputs']])
print('等级 : thickness=%d solidity=%d brightness=%d' % (
int(torch.tensor(r['raw_outputs'][0:3]).argmax())+1,
int(torch.tensor(r['raw_outputs'][3:6]).argmax())+1,
int(torch.tensor(r['raw_outputs'][6:9]).argmax())+1))

运行提示:实验 5/12/15 涉及 test_input.wav 与 html/ 内模块,请在项目根目录运行;实验 15 需要 cd html 后运行(因为 checkpoint 与 labels 的相对路径基准在 html 目录)。



第十三部分 自测题库(40 题,含答案要点)#

A. 数字音频(1–6)

  1. 16 kHz 采样为什么上限是 8 kHz?超过会怎样? → 奈奎斯特定理 f_max = sr/2;超过的部分会折叠(混叠)成假低频,必须在重采样前低通。
  2. 位深与动态范围的关系?16 bit 的理论 SNR 约多少? → 每个 bit 约 6 dB;6.02×16+1.76 ≈ 98 dB。
  3. 容器与编码的区别?给出本项目的一组例子。 → 容器=盒子(WebM),编码=内容压缩方式(Opus);WebM+Opus 后端读不了。
  4. WAV 头 44 字节中,byteRate、blockAlign、dataSize 怎么算? → sr×声道×位深/8;声道×位深/8;帧数×blockAlign。
  5. 为什么”能量占比”能抗音量变化? → 除以总能量,整体增益被约掉,只保留频谱形状。
  6. 有损编码对音色特征有什么影响? → 高频可能被截断(影响”明暗”)、引入编码噪声(影响”虚实”);建议无损 WAV。

B. 时频分析(7–14)

  1. STFT 的 n_fft 与 hop_length 分别决定什么? → 频率分辨率(sr/n_fft)与时间采样密度;二者权衡(不确定性原理)。
  2. n_fft=2048, sr=16000 时 Δf 与输出频率点数? → Δf≈7.8 Hz;N/2+1 = 1025。
  3. 补零能否提高频率分辨率? → 不能,只是在频率轴插值;真实分辨率由窗长决定。
  4. 为什么加窗?Hann 窗相比矩形窗的代价? → 抑制频谱泄漏;代价是主瓣变宽(频率分辨率下降)。
  5. center=True(librosa 默认)与 center=False 的帧数公式? → 1+⌊L/hop⌋ vs 1+⌊(L−n_fft)/hop⌋。
  6. 幅度谱、能量谱、对数谱各用在什么地方? → 可视化/谱特征、频段能量占比、压缩动态范围(fbank)。
  7. 为什么常用特征都忽略相位? → 人耳对短时相位不敏感,幅度信息足以支撑音色/内容判别。
  8. 谱质心与”明暗”的关系? → 谱质心越高,能量越偏高频,听感越明亮。

C. 语音声学(15–19)

  1. 声源-滤波器理论说了什么?为什么它能指导音色特征设计? → 声源(声带)给基频/谐波,声道给共振峰;音色信息主要在谱包络与谐波分布。
  2. F1、F2 分别与什么发音动作相关? → F1 舌位高低,F2 舌位前后。
  3. HNR 高/低分别对应什么听感?本项目真的算了吗? → 高=实,低=虚;本项目没算 HNR,用频段能量占比作代理。
  4. LPC 为什么能求共振峰?Burg 法的优点? → 全极点模型极点为声道谐振;Burg 短数据稳定、精度高。
  5. 为什么分析窗常取 25 ms? → 覆盖 2–3 个基频周期,既能捕捉谐波又不跨音素(准平稳假设)。

D. 特征工程(20–24)

  1. 梅尔公式与反变换?为什么低频滤波器更窄? → m=2595log10(1+f/700),反变换 f=700(10^(m/2595)−1);等梅尔间隔在低频对应更小 Hz 间隔。
  2. fbank 与 MFCC 的关系?为什么深度学习更爱 fbank? → fbank 是 log-mel,MFCC 再 DCT 截断;fbank 保留更多信息且可被网络自行利用。
  3. Kaldi fbank 与 librosa mel 有哪些具体差异?为什么不能混用? → 窗长/帧移定义、帧对齐(snip_edges vs center)、能量项、归一化与对数;混用导致分布错位、精度崩塌。
  4. 归一化常数 −4.2677/4.5690 从哪来?×2 是什么? → AudioSet 上 Kaldi fbank 的全局均值/标准差;×2 是 AudioMAE/AST 惯例(放大标准差)。
  5. 三套 12 频段怎么定的?举一个边界验证。 → 各维度范围内等梅尔切 12 份;明暗 0–4280 Hz 的 mel 总量≈2211,2211/12≈184.3,对应首段边界约 124 Hz。

E. 机器学习(25–32)

  1. Fisher 准则与最优方向的闭式解? → J=(μ1−μ0)²/(σ0²+σ1²);w* ∝ S_w^{−1}(μ1−μ0)。
  2. 为什么项目用”Fisher 准则的倒数”当损失? → 最小化 1/J 等价于最大化 J,且可用梯度下降迭代求解、便于输出固定长度权重。
  3. 两类情形下为什么”训练一个线性层+该损失”等于 LDA? → 线性层输出即投影 wᵀx+b,损失正是投影后的 Fisher 比倒数。
  4. AdamW 与 Adam 的区别? → 前者把权重衰减从梯度中解耦,直接作用于参数,正则与自适应学习率不再耦合。
  5. 学习率线性缩放规则?本项目怎么写的? → lr = blr × eff_batch/256。
  6. 逐层学习率衰减 0.75 的意义? → 浅层学习率更小,保护通用特征、防灾难性遗忘。
  7. AP、mAP、AUC、d-prime 各衡量什么? → 单类排序质量、多类平均、正负样本可分性、信号检测论下的辨别力(√2·Φ⁻¹(AUC))。
  8. group_acc 为什么比 mAP 更贴业务? → 组内 argmax 对应”该维度判成几级”的真实用法,可直接定位某维度的弱项。

F. 深度学习(33–38)

  1. ViT 为什么需要位置编码?本项目长度为何是 1213? → 注意力本身无序;1212 个 patch + 1 个 cls token。
  2. patch embedding 用 stride=10 而非 16 的意义? → patch 在时间轴重叠,提升时间分辨率(101 个时间块而非 64)。
  3. global_pool=True 与 cls token 两种方式有何区别? → 前者对所有 patch 平均(对长度变化鲁棒),后者用专用 token(原版 ViT)。
  4. MAE 为什么”编码器只看 20%、解码器看全部”? → 省算力(80% 不算)+ 解码器需要完整序列(含 mask token)来重建。
  5. 高掩码率 0.8 为什么有效? → 图像/频谱冗余高,遮 80% 后局部插值不可行,迫使模型学语义级结构。
  6. 混合精度里 Loss Scaling 解决什么问题? → fp16 下溢(小梯度变 0),先放大 loss 再等比例缩小梯度。

G. Web 与工程(39–40)

  1. 浏览器录音为什么必须 HTTPS?后端读不了 WebM 的三种解法及本项目选择? → 安全上下文要求;解法:前端转码(本项目)、服务器装 ffmpeg、限制上传格式。
  2. 为什么 gunicorn 只能开 1 个 worker?如何提升吞吐? → 每个 worker 独立加载约 1 GB 模型;提升吞吐应上 GPU、模型量化/ONNX、请求队列或横向扩容。


第十四部分 延伸资料清单#

音频与信号处理

  1. 教科书级:Oppenheim《离散时间信号处理》(STFT/DFT/滤波器章节);Rabiner & Schafer《语音信号处理理论》
  2. 在线课程:Coursera “Audio Signal Processing for Music Applications”(UPF,动手性强)
  3. 工具文档:librosa(core.audio、feature、display)、torchaudio(compliance.kaldi)、Praat/parselmouth
  4. 概念速查:Mel 刻度与滤波器组( librosa filters.mel 源码值得读一遍)

语音声学

  1. Jurafsky & Martin《Speech and Language Processing》第 25 章(语音学与特征)
  2. Praat 官方手册(共振峰/HNR/基频算法细节)

机器学习与深度学习

  1. 《动手学深度学习》d2l.ai(中文,含线性分类、注意力、自监督)
  2. 李沐论文精读(B 站):ViT、MAE、Swin、Transformer
  3. Goodfellow《深度学习》(优化器与正则化章节)
  4. PyTorch 官方教程(Autograd、AMP、DDP、torchaudio)

本项目上游

  1. facebookresearch/AudioMAE(本项目的直接源头,重点读 models_mae.py、dataset.py、stat.py)
  2. YuanGongND/ast(AST 数据格式与训练脚本)
  3. huggingface/pytorch-image-models(timm;ViT/Swin 实现)
  4. YuanGongND 的论文与仓库文档(音频分类实践细节)

Web 与工程

  1. Flask 官方 Quickstart;gunicorn 部署文档;Nginx 反向代理与 TLS 配置
  2. MDN:MediaRecorder、Web Audio API、FormData、fetch
  3. Chart.js 文档(radar/bar 配置)

工程实践

  1. 《SRE:Google 运维解密》前几章(可观测性、容量规划思路)
  2. 数据标注方法论:Inter-Annotator Agreement(Cohen’s κ)——音色分级这种主观标注必须报告一致性

结语:本篇与后续文章的衔接#

  • 第 2 篇(环境跑通)会用到:第 77–78 节(依赖与版本坑)、第 67–70 节(Flask/gunicorn)、第 71–73 节(前端录音与转码)。
  • 第 3 篇(代码拆解)会用到:第 32–37 节(物理特征)、第 44 节(Fisher 完全推导)、第 55–58 节(ViT 与 MAE)、第 72 节(Web Audio)。
  • 第 4 篇(调试改参)会用到:第 42 节(学习率与调度)、第 46 节(指标与混淆矩阵)、第 47 节(阈值与边界实验)、第 65 节(聚合策略实验)。
  • 第 5 篇(复盘沉淀)会用到:第 79 节(排错方法论)、第 80 节(资源估算)、第 37 节末尾(局限性清单)。

本篇的目标是”补齐地基”:读完之后,第 3 篇拆代码时你应该不再需要为任何一个名词停下来查资料。如果还有卡住的概念,说明它属于上面某个章节——回到对应小节,并跑一遍配套的最小实验。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

2026-09-14 【音色评估】项目拆解(一点五)前置基础知识全解

本文链接

https://study.aokana.cn/posts/2026-09-14-音色评估-项目拆解-一点五-前置基础知识全解-154/
作者
乌冬面
发布于
2026-09-14
许可协议
CC BY-NC-SA 4.0
相关文章智能推荐
1
2026-09-11 【音色评估】项目拆解(一):前置调研与全局认知
项目拆解系列定位:本系列共5篇文章,从零完整复现并深度吃透「声音色客观评估系统」,覆盖「调研摸底→环境跑通→代码拆解→调试改参→复盘沉淀」全流程。 目录 1. 项目定位:业务背景与核心价值 2. 前置基础:音色三维度的声学本质 2.1 数字音频基础概念 2.2 语音声学核心特征 2.3 「厚薄/明暗/虚实」的声学对应 3. 系统核心架构:双路线融合设计全解析 3.1 整体流程总览 3.2 路线一:物理声学
2
2026-10-17 【泛知碎藏】图像格式科普:一文看懂各类图片格式的起源、原理、优缺点与适用场景
泛知碎藏我们在手机拍照、网页存图、截图设计时,总会遇见各式各样后缀的图片文件:JPG、PNG、GIF、WebP、HEIC、AVIF……很多人只知道后缀不同,却不清楚它们诞生的背景、设计初衷,选错格式会造成画质损耗、文件体积爆炸、透明背景丢失、上传平台不识别等一系列麻烦。本文从起源讲起,完整梳理主流图像格式。 JPG / JPEG 起源 JPEG 是联合图像专家组(Joint Photographic E
3
2026-09-24 【音色评估】项目拆解(四):调试改参与变式测试
项目拆解系列定位:5 篇走完「调研摸底 → 环境跑通 → 代码拆解 → 调试改参 → 复盘沉淀」。 本篇目标:从"看懂代码"进阶到"能改、敢改、知道改了会怎样"。所有实验不需要新数据,全部用项目自带音频即可复跑。 与前面的衔接:第三篇把两条管线的数据契约讲透了(15 维特征、六套模板、1213 位置编码)。本篇就沿着这些契约故意去改,看哪一类参数会把系统改坏、哪一类安全。 配套可运行试验台:tuner_l
4
2026-09-23 【音色评估】项目拆解(三):逐行拆解代码
项目拆解系列定位:5 篇走完「调研摸底 → 环境跑通 → 代码拆解 → 调试改参 → 复盘沉淀」。 本篇目标:把"能跑通的代码"变成"能讲清楚的代码"——不看源码也能说出每一步在做什么、数据形状怎么变、每个常量从哪来。 与前面的衔接:第一篇给了全景(双路线融合、四模块);第 1.5 篇给了底层知识(STFT/梅尔/fbank/Fisher/ViT);第二篇把环境跑通了,并留下两个"跑通即暴露"的 bug。
5
2026-09-22 【音色评估】项目拆解(二):环境搭建与跑通
项目拆解系列定位:5 篇走完「调研摸底 → 环境跑通 → 代码拆解 → 调试改参 → 复盘沉淀」。 本篇目标:把项目从"一堆压缩包"变成「一条命令能跑、一个接口能验、一份环境手册能复刻」的可用状态。 与前面的衔接:第一篇定了"项目是什么"(双路线融合、四模块分工);第 1.5 篇补了底层知识(STFT / 梅尔刻度 / Fisher 判别 / fbank / ViT)。本篇不再重复概念,只讲实操与排错。
随机文章随机推荐

一起讨论,一起进步

交流想法,也欢迎提出建议。

评论
游客发言

讨论区

评论加载中…

Profile Image of the Author
乌冬面
形而上学,不行退学。
bilibiliSteamMyGameBlog
公告
很难说这是个有用的网站。 或许会存放大量不明所以的东西。
分类
标签
最新动态
站点统计
文章
41
分类
9
标签
45
总字数
218,155
运行时长
0 天
最后活动
0 天前
站点信息
构建平台
Local
博客版本
Firefly v6.14.5
文章许可
CC BY-NC-SA 4.0
目录
1
第一部分 数学地基
1. 三角函数、正弦波与相位
2. 复数与欧拉公式
3. 向量、矩阵与形状规则
4. 均值、方差、协方差(含 unbiased 的坑)
5. 导数、偏导、梯度与链式法则
6. 概率与分布
7. 对数与分贝
8. 线性代数进阶:投影、特征值与广义特征值
2
第二部分 数字信号处理地基
9. 连续信号到离散信号:采样定理与混叠
10. 量化、位深与量化噪声
11. 时域与频域:傅里叶思想的直觉
12. DFT:公式、频率 bin 与单边谱
13. FFT:为什么快,快在哪
14. 时间分辨率 vs 频率分辨率的权衡
15. 窗函数、频谱泄漏与分帧
16. 幅度谱 / 功率谱 / 能量谱 / 对数谱
17. 常用谱特征
18. 滤波、预加重与卷积定理
19. 采样率转换(重采样)与抗混叠
3
第三部分 音频工程地基
20. 声压、dBFS 与响度
21. PCM、位序与声道交错
22. WAV/RIFF 文件结构逐字节
23. 容器与编码:MP3 / AAC / Opus 原理
24. 为什么浏览器录音是 WebM+Opus,后端为什么读不了
25. 录音链路与常见音质问题
4
第四部分 语音声学地基
26. 语音产生机理:声源—滤波器理论
27. 基频 F0、音高与 F0 估计方法
28. 谐波结构与谐噪比 HNR
29. 共振峰、LPC 与 Burg 法
30. 声门闭合商与”虚/实”的声学机制
31. 语音的准平稳假设与分帧长度惯例
5
第五部分 听觉与特征工程地基
32. 梅尔刻度的来历与公式
33. 梅尔滤波器组的构造(5 步详解)
34. log-mel / fbank / MFCC 的关系
35. Kaldi fbank 与 librosa mel 的差异
36. 特征归一化:AudioSet 全局均值方差与 ×2 的秘密
37. 本项目核心特征:12 频段能量占比
6
第六部分 机器学习地基
38. 监督学习范式与数据划分
39. 过拟合、欠拟合与正则化
40. 损失函数总览:MSE / CE / BCE
41. 优化算法:GD → SGD → Momentum → Adam → AdamW
42. 学习率调度:warmup、cosine 与线性缩放规则
43. 特征缩放与标准化
44. 判别分析:Fisher / LDA 完整推导与项目实现对照
44.1 问题设定
44.2 两个散度
44.3 Fisher 准则与最优解
44.4 与”倒数的损失”的等价性
44.5 为什么不用闭式解 S_w^{-1}(μ1−μ0)?
44.6 项目里的手推梯度版本(model.py)
44.7 从二类到多类与”两两判别”
45. 逻辑回归、线性分类边界与 sigmoid
46. 评价指标:从混淆矩阵到 mAP 与 d-prime
46.1 二分类混淆矩阵
46.2 PR 曲线与 AP / mAP
46.3 ROC / AUC 与 d-prime
46.4 本项目自加的 group_acc(重要)
47. 阈值、边界与分位数过滤
7
第七部分 深度学习地基
48. 从感知机到 MLP
49. 反向传播与计算图(手推一遍)
50. 激活函数与梯度问题
51. 卷积与感受野
52. 归一化层:BatchNorm vs LayerNorm
53. 残差连接与初始化
54. 正则化与数据增强
55. 注意力机制与位置编码
56. Transformer 与 ViT
57. Swin 与窗口注意力
58. 自监督学习与 MAE
59. 微调策略与逐层学习率衰减
60. 混合精度训练(AMP)
61. 分布式训练 DDP
8
第八部分 音频深度学习地基
62. 音频表示的选择
63. AST 与 AudioMAE
64. 2D 时频掩码
65. 片段切分与聚合、多标签与多任务
66. 评估口径:mAP vs 组内准确率
9
第九部分 Web 与前端地基
67. HTTP 协议与 multipart/form-data
68. 同源策略、CORS 与安全上下文
69. Flask、WSGI 与请求生命周期
70. gunicorn 与反向代理
71. 前端录音:getUserMedia / MediaRecorder
72. Web Audio API 与音频解码
73. Blob / ArrayBuffer / TypedArray
74. JS 异步与 fetch
75. Canvas 与 Chart.js
10
第十部分 工程与运维地基
76. 进程、线程、GIL 与并发
77. 虚拟环境、wheel 与依赖管理
78. 依赖版本坑的完整回溯(本项目实测)
79. systemd、日志与排错方法论
80. 资源估算(会算才敢上线)
11
第十一部分 概念辨析速查表(24 组)
12
第十二部分 15 个最小实验(可直接跑)
实验 1:观察混叠(10 kHz 用 16 kHz 采样会变成 6 kHz)
实验 2:验证量化 SNR 公式(每个 bit 约 6 dB)
实验 3:FFT 分辨率——n_fft 决定能否分开两个邻近频率
实验 4:窗函数与频谱泄漏
实验 5:STFT 参数的时频权衡(可视化)
实验 6:验证”能量占比”抗音量(项目设计的关键性质)
实验 7:验证三套频段确实是”等梅尔切分”
实验 8:亲手构造梅尔滤波器组并与 librosa 对比
实验 9:用 Fisher 损失学权重,并与 sklearn LDA 对比方向
实验 10:手算 [a, b, 中心] 映射(对应 calbound.py)
实验 11:验证 fbank 帧数 ≈ 时长 / 10 ms
实验 12:ViT 前向形状追踪(不需要权重)
实验 13:可视化 MAE 的 2D 时频掩码
实验 14:BCE 与 CE 的数值对比
实验 15:端到端最小推理(脱离 Web,直接拿 11 维结果)
13
第十三部分 自测题库(40 题,含答案要点)
14
第十四部分 延伸资料清单
结语:本篇与后续文章的衔接