简介:这是一份面向语音识别研究者与初学者的MATLAB实现资源,聚焦LPCC(线性预测编码系数)特征提取方法。LPCC在语音识别系统中能反映频谱与时域动态特性,常与SVM、HMM等分类模型配合,用于构建完整识别流程。压缩包仅含1个m文件,大小891B,即lpcc.m脚本。脚本覆盖信号预加重、加窗分帧、功率谱估计、线性预测系数求解、对数倒谱转换以及一阶二阶差分计算等LPCC提取关键环节,代码量精简,可直接在MATLAB中调用,也可嵌入现有语音识别框架作为前端特征模块。尽管包体极小,但脚本逻辑完整,便于逐行理解算法原理,在此基础上可调整LPC阶数、窗函数或归一化策略,进一步优化系统识别率。目前已有165人学习下载,对于希望快速掌握LPCC实现细节或开展语音特征对比实验的开发者而言,是一份小巧实用的参考代码。
1. 为什么 LPCC 在 MATLAB 语音识别里还是绕不开的特征
做语音识别的人多半会默认 MFCC 是起点,但只要你搜一次「LPCC MATLAB 语音识别」,就会看到课程设计、毕业设计和不少嵌入式老项目里,LPCC 依然被大批量地使用。LPCC(Linear Prediction Cepstral Coefficients,线性预测倒谱系数)是从 LPC 系数递推出来的倒谱特征,描述的是声道冲激响应的谱包络。它的计算量比 MFCC 小一个量级,参数含义直接对应发音生理模型,在干净语音、孤立词识别和资源受限的离线语音识别模块里,优势非常具体。这篇文章从原理讲起,给出能在 MATLAB 里直接复现的提取代码、一套可跑的孤立词识别系统,以及参数调试时真正会踩的坑。新手可以照着敲完,熟手可以直接跳到第 5 章看边界条件。
2. LPCC 的原理与参数:LPC、倒谱和声道模型怎么串起来
2.1 全极点模型与倒谱递推:LPCC 不依赖 FFT
很多人的误区是「LPCC 就是对 LPC 系数做 FFT 再取模」。实际上 LPC 的建模对象是声道传递函数,用全极点模型近似:
H(z) = G / (1 + Σ_{k=1}^{p} a_k z^{-k})
其中 a_k 是线性预测系数,G 是增益。对这个模型的幅度谱取对数再做逆变换,得到的系数 c_n 就是倒谱。关键性质在于:全极点模型的倒谱根本不需要做 FFT,可以直接从 a_k 递推出来:
% 递推关系(公式索引从 0 开始,MATLAB 实现时注意偏移) % c_0 = ln(E) % n 在 1..p 之间: c_n = a_n + sum_{k=1}^{n-1} (k/n)*c_k*a_{n-k} % n 大于 p 之后: c_n = sum_{k=n-p}^{n-1} (k/n)*c_k*a_{n-k}这里 E 是线性预测误差能量。这个递推式告诉你两件事:第一,LPCC 的计算量由 LPC 阶数 p 决定,和 FFT 长度无关;第二,倒谱系数随 n 增大衰减很快,取前 12 到 16 个就足够描述谱包络。还有一个容易被忽略的好性质:c_0 的取值只影响自身,递推从 c_1 开始就不依赖 c_0,所以不同实现里 c_0 是取 ln(E) 还是取 0.5*ln(E),完全不影响后面系数的数值,识别任务里把它丢掉是安全的。
2.2 LPC 阶数 p、倒谱个数 Q 和采样率怎么搭配
LPC 阶数不是越大越好。阶数过低,共振峰展不开,谱包络过度平滑;阶数过高,模型开始拟合基频的精细谐波结构,反而丢掉了「声道滤波特性」这个物理含义。经验上 p 大约等于采样率(kHz)加 2 到 4,8 kHz 语音取 10 到 12,16 kHz 宽带语音取 16 到 20。
| 参数 | 常见取值 | 依据 |
|---|---|---|
| LPC 阶数 p | 10~12(8 kHz),16~20(16 kHz) | 每个共振峰约需 2 阶,再加 2~4 阶裕量 |
| LPCC 个数 Q | 12~16 | 高阶倒谱系数衰减快,截断即可 |
| 帧长 | 20~25 ms | 短时平稳假设的工程折中 |
| 帧移 | 8~10 ms | 相邻帧重叠 50%~60%,保证轨迹平滑 |
倒谱个数 Q 和 p 的关系经常被搞混。递推式在 n 超过 p 之后只有前 p 个 a_k 参与,所以 Q 可以大于 p,但高阶系数幅度很小,取太多只会把噪声带进特征。我一般取 Q 等于 p 或者 p 加 2,超过 20 个基本没有正向收益。
2.3 LPCC 与 MFCC 的边界:什么时候该用 LPCC
MFCC 用 Mel 滤波器组模拟人耳感知,对噪声和频谱细节更鲁棒,是绝大多数现代识别系统的默认特征。LPCC 的优势在三个方面:一是计算量小,只有一次自相关和一次 Levinson-Durbin 递推,适合在 MATLAB 里跑实时原型,也适合移植到 ESP32 这类资源受限的板子上做离线命令词识别;二是系数和 LPC 模型直接对应,做共振峰分析、声道长度归一化时顺手;三是在干净语音、单说话人的孤立词任务里,LPCC 的识别率并不比 MFCC 差,而实现代码短得多。
短板也要说清楚:LPCC 对加性噪声敏感,因为噪声会污染自相关序列;它对声道模型假设的依赖强,女声和童声的共振峰位置偏高,同样阶数下拟合效果不如男声,这时需要适当抬高 p。我的选择标准是:资源受限或做课程验证用 LPCC,复杂环境识别用 MFCC,两边都不舍得丢就拼成混合特征。
3. 用 MATLAB 复现 LPCC 提取:从递推公式到可运行函数
3.1 先写 Levinson-Durbin 递推求解 LPC 系数
LPCC 的前置条件是 LPC 系数 a_k 和预测误差能量 E,它们从帧信号的自相关序列解出来。自相关法配合 Levinson-Durbin 递推是最稳的组合,保证求解出来的全极点滤波器稳定。下面的函数输入自相关序列 r 和阶数 p,输出 LPC 系数和误差能量:
function [a, E] = levinson_durbin(r, p) % r: 自相关序列,长度 p+1,r(1) 对应 r(0) % a: LPC 系数,长度 p,a(i) 对应 a_i % E: 预测误差能量,递推过程中单调不增 a = zeros(p, 1); E = r(1); for i = 1:p % 计算第 i 步反射系数 k_i s = 0; for j = 1:i-1 s = s + a(j) * r(i-j+1); end k_i = -(r(i+1) + s) / E; % 由前一步系数更新当前阶系数 a_prev = a; for j = 1:i-1 a(j) = a_prev(j) + k_i * a_prev(i-j); end a(i) = k_i; E = E * (1 - k_i^2); end end核心是反射系数 k_i 的求解,它等于当前阶预测误差与上一步误差能量的比值取负。E 每步乘以 (1 - k_i^2),只要 |k_i| 小于 1,E 就单调下降,滤波器稳定。信号里如果有直流分量,r(1) 偏大,最后得到的系数会整体偏移,所以分帧前最好先做预加重或者去直流。
3.2 用递归关系把 LPC 转成 LPCC
有了 a 和 E,LPCC 就是合并两个循环的事。注意 c_0 是 log(E),它携带的是帧能量信息,受录音距离和增益影响太大,识别任务里通常丢弃:
function ceps = lpc2lpcc(a, E, Q) % a: LPC 系数,长度 p % E: 预测误差能量 % Q: 需要提取的 LPCC 个数(不含 c_0) p = length(a); ceps = zeros(Q + 1, 1); % 索引 1 存放 c_0 ceps(1) = log(E); % n = 1..p:c_n = a_n + sum(k/n)*c_k*a_{n-k} for n = 1:min(p, Q) s = a(n); for k = 1:n-1 s = s + (k / n) * ceps(k+1) * a(n-k); end ceps(n+1) = s; end % n > p:只用前 p 个 LPC 系数参与递推 for n = p+1:Q s = 0; for k = n-p:n-1 s = s + (k / n) * ceps(k+1) * a(n-k); end ceps(n+1) = s; end ceps = ceps(2:end); % 丢弃 c_0,长度变为 Q end这里索引错一位是最常见的 bug:MATLAB 数组从 1 开始,公式里的 c_n 从 0 开始,所以 ceps(k+1) 存的是 c_k。第二个循环的下界 n-p 保证只用已经算出来的系数;如果 p 比 n 大,第二个循环根本不会进入,由第一个循环覆盖。返回值去掉 c_0 后长度正好是 Q。
3.3 封装成分帧特征提取函数 lpcc_extract
把整条链路串起来:预加重、分帧加窗、逐帧自相关、LPC、LPCC。帧长 25 ms、帧移 10 ms 是语音识别里最常用的组合:
function feats = lpcc_extract(x, fs, p, Q) % 提取整段语音的 LPCC 特征矩阵 % x: 单声道语音,double 类型 % fs: 采样率 % p: LPC 阶数 % Q: 每帧 LPCC 个数 if size(x, 2) > 1 x = mean(x, 2); % 立体声转单声道 end x = x / max(abs(x)); % 幅值归一化 x = filter([1, -0.97], 1, x); % 预加重,系数 0.97 frame_len = round(0.025 * fs); % 25 ms frame_shift = round(0.010 * fs); % 10 ms N = length(x); n_frames = floor((N - frame_len) / frame_shift) + 1; feats = zeros(n_frames, Q); hamm = 0.54 - 0.46 * cos(2 * pi * (0:frame_len-1)' / (frame_len - 1)); for i = 1:n_frames idx = (i-1) * frame_shift + 1 : (i-1) * frame_shift + frame_len; frame = x(idx) .* hamm; r = zeros(p + 1, 1); for k = 1:p+1 r(k) = sum(frame(1:end-k+1) .* frame(k:end)); % r(τ) end [a, E] = levinson_durbin(r, p); if E < 1e-12 E = 1e-12; % 防止 log(0) 出现 NaN end feats(i, :) = lpc2lpcc(a, E, Q); end end自相关那行frame(1:end-k+1) .* frame(k:end)对应 r(τ) = Σ x(n)x(n+τ),比双重循环快得多。预加重系数 0.97 提升高频段,抵消语音频谱随频率下降的趋势;如果你的录音设备高频已经很亮,降到 0.95 即可。汉明窗我用手写公式替代hamming,这样没有 Signal Processing Toolbox 也能跑,filter和audioread都是基础函数,不受工具箱限制。
3.4 最小验证:跑一段语音看特征
[x, fs] = audioread('your_wav.wav'); % 换成你自己的录音 feats = lpcc_extract(x, fs, 12, 12); plot(feats(:, 1:3), 'LineWidth', 1); % 只看前 3 阶,避免图太乱 xlabel('帧号'); ylabel('LPCC 值');如果 feats 里出现 NaN,说明存在能量接近 0 的帧,log(E) 溢出,原因通常是录音首尾的静音段太长,解决方案是给 E 加下限或者先做端点检测。正常语音的特征曲线应该在中段平稳起伏、首尾接近 0;如果整段都很平,先检查是不是整段都录成了噪声。手头没有现成 wav 文件时,Signal Processing Toolbox 自带的load mtlb可以直接用,采样率 7418 Hz,对应 p 取 12 没问题。
4. 在 MATLAB 里搭一个 LPCC 孤立词识别系统
4.1 端点检测与模板生成
孤立词识别是最适合 LPCC 的场景:每个词 0.3 到 1 秒,先用端点检测截出有效段,再提 LPCC,最后用 DTW 匹配。端点检测我用短时能量加连续段筛选:能量门限去掉静音,取最长的连续命中段作为语音。简化的双门限版本如下:
function [idx_s, idx_e] = simple_vad(x, fs) % 基于短时能量的端点检测,返回语音段起止样本点 framelen = round(0.020 * fs); shift = round(0.010 * fs); nf = floor((length(x) - framelen) / shift) + 1; energy = zeros(nf, 1); for i = 1:nf idx = (i-1)*shift + 1 : (i-1)*shift + framelen; energy(i) = sum(x(idx).^2) / framelen; end e_th = max(energy) * 0.1; % 门限取最大能量的 10% on = energy > e_th; % 找最长的连续命中段 max_len = 0; max_start = 1; max_end = 1; i = 1; while i <= nf if on(i) j = i; while j < nf && on(j+1) j = j + 1; end if j - i + 1 > max_len max_len = j - i + 1; max_start = i; max_end = j; end i = j + 1; else i = i + 1; end end idx_s = (max_start - 1) * shift + 1; idx_e = min((max_end - 1) * shift + framelen, length(x)); end能量门限取最大值的 10%,对单人、固定距离的录音够用。注意循环里用 while 而不是 for,因为要在找到一段连续区间后跳过整段,MATLAB 的 for 循环变量无法在循环体内修改。模板生成时每类词录 5 到 10 遍,每遍经过端点检测后提 LPCC,得到的特征矩阵行数随时长变化,所以模板是变长的,后面必须用 DTW。
4.2 DTW 距离:变长特征的匹配
DTW 的核心是构造帧间欧氏距离矩阵,再沿三条路径累积找最小代价。我做了一点归一化处理,用路径长度去除累积距离,避免读得慢的样本因为帧数多而系统性增大距离:
function d = dtw_dist(t, r) % t: 模板特征矩阵(帧数 x 维数) % r: 测试特征矩阵(帧数 x 维数) Mt = size(t, 1); Mr = size(r, 1); d = zeros(Mt, Mr); for i = 1:Mt for j = 1:Mr d(i, j) = sqrt(sum((t(i,:) - r(j,:)).^2)); end end D = zeros(Mt, Mr); D(1, 1) = d(1, 1); for i = 2:Mt D(i, 1) = D(i-1, 1) + d(i, 1); end for j = 2:Mr D(1, j) = D(1, j-1) + d(1, j); end for i = 2:Mt for j = 2:Mr D(i, j) = d(i, j) + min([D(i-1,j), D(i,j-1), D(i-1,j-1)]); end end d = D(Mt, Mr) / (Mt + Mr); % 路径长度归一化 end三条允许的移动分别对应「测试帧多读一帧」「模板帧多读一帧」「正常对齐」,相当于允许语速在一定范围内伸缩。归一化用 Mt+Mr 而不是真正的路径步数,是一个常用近似,好处是计算简单、对端点误差不敏感。如果你的词表里存在读音长度差异极大的类别,可以给 dtw_dist 加一个弯曲窗口限制,比如只允许路径在对角线附近 ±10 帧内移动,能防止错误对齐。
4.3 识别与留一法评估
小数据集上我一般用留一法:每条录音轮流当测试样本,其余全部当模板,统计识别率。这样 30 条数据也能得到稳定估计:
% data{c}{t} 是第 c 类第 t 条录音的 LPCC 特征矩阵(已提好) C = length(data); correct = 0; total = 0; for c = 1:C for t = 1:length(data{c}) templates = data; test_feat = templates{c}{t}; templates{c}(t) = []; % 留出当前样本 best_d = Inf; pred = 0; for cc = 1:C for tt = 1:length(templates{cc}) dist = dtw_dist(templates{cc}{tt}, test_feat); if dist < best_d best_d = dist; pred = cc; end end end if pred == c correct = correct + 1; end total = total + 1; end end fprintf('留一法识别率:%.1f%% (%d/%d)\n', 100*correct/total, correct, total);注意:MATLAB 里删除 cell 数组中的一项要写
templates{c}(t) = [],用小括号而不是花括号。写成templates{c}{t} = []只会把内容置空,cell 里留一个空矩阵,DTW 时会直接报维度错误。
干净环境下,10 类词、每类 5 遍训练,LPCC 加 DTW 做到 95% 以上是正常的。如果低于 90%,先查端点检测是否把环境噪声截了进来,再查每类内部是否有明显音量差异。
5. LPCC 参数微调、易混分析与一种进阶用法
5.1 三个必调参数:p、Q、预加重系数
p 是唯一需要和采样率联动的参数。8 kHz 电话语音取 10 到 12,16 kHz 宽带语音取 16 到 20。判断依据是共振峰拟合:画出 LPC 谱包络,如果低频段出现无意义的窄尖峰,p 偏高;如果第二、第三共振峰糊成一团,p 偏低。女声和童声的共振峰整体偏高,同一份数据换成女性说话人时,p 需要比男声高 2 到 4 阶,这也是「女声语音识别比男声更低」的一个直接原因。
Q 取 12 就够大部分命令词,超过 20 后高阶系数由噪声主导,DTW 距离被噪声项拉平,识别率反而下降。预加重系数 0.95 到 0.97 之间影响的是谱包络倾斜而不是共振峰位置,不值得反复试,固定 0.97 就行。
5.2 用混淆矩阵定位易混类别
识别率只是一个数,混淆矩阵才能告诉你哪两类在互相混。有 Statistics and Machine Learning Toolbox 的话,直接一行代码:
cm = confusionchart(true_labels, pred_labels);没有工具箱就手动统计,C 类建 C 乘 C 矩阵,行是真值、列是预测。如果发现「二」和「四」这类音节结构接近的词互相混,优先检查端点截取是否一致,其次才是调 p。易混对通常集中在音素结构相似的数字上,这时候加差分特征比加阶数更有效。
5.3 差分 LPCC 和向量量化:往深度学习过渡的两种做法
静态 LPCC 只描述当前帧的谱包络,丢失了语音的动态变化。一阶差分把相邻帧的变化趋势加进来,是提升识别率性价比最高的做法:
function dfeats = lpcc_delta(feats, win) % 一阶差分倒谱,win 常见取 2,即前后各 2 帧 dfeats = zeros(size(feats)); for n = 1:size(feats, 1) num = 0; den = 0; for k = -win:win idx = min(max(n + k, 1), size(feats, 1)); % 边界复制 num = num + k * feats(idx, :); den = den + k^2; end dfeats(n, :) = num / den; end end边界处理用复制而不是补零,语音首尾本就是静音帧,复制几帧对距离影响极小,补零反而会引入虚假的剧烈变化。把 dfeats 拼到静态特征后面再走 DTW,或者喂给 LSTM,就是从 LPCC 往 MATLAB 深度学习路线最短的升级路径。另一种做法是向量量化:把训练集所有帧的 LPCC 用kmeans(feats_all, K)聚成 K 个码本中心,每段语音用码本序号序列表示,再接 HMM 或直接做直方图匹配,这是早期说话人识别系统的经典结构,代码量不大,适合在 MATLAB 里快速验证特征区分度。
本文还有配套的精品资源,点击获取