基于MATLAB的0-9数字语音识别系统设计与实现
我们从零开始,用MATLAB实现了一个完整的数字语音识别系统,涵盖了:✅ 语音采集与数字化✅ 前端增强(预加重、分帧、加窗)✅ 端点检测(双门限法)✅ MFCC特征提取(含Δ/ΔΔ)✅ HMM建模与训练✅ 最大似然识别虽然这只是语音识别的“Hello World”,但它所蕴含的思想——信号处理 + 特征工程 + 概率建模——正是现代ASR系统的基石。当然,今天的方案也有局限:HMM假设太强、手工特
简介:本项目是一个基于MATLAB平台的简单语音识别系统,旨在实现对数字0到9的准确识别。系统以隐马尔科夫模型(HMM)为核心,结合维特比算法和鲍姆-韦尔奇算法,完成语音信号的预处理、特征提取、模型训练与解码全过程。用户仅需运行主文件”main.m”即可体验完整的识别流程,适合初学者深入理解HMM在语音识别中的应用。项目包含cdhmm模块,支持连续语音信号建模,是语音识别领域良好的实践入门案例。
语音识别系统的MATLAB实现:从零构建数字0-9识别系统
你有没有试过对着手机说“打电话给妈妈”,结果它却拨给了“张三”?😂 这种尴尬的背后,其实是语音识别系统在“听错”。而今天我们要做的,不是让AI理解整段对话,而是让它老老实实、准确无误地听懂你说的 一个数字 ——比如“7”。
别小看这个任务!电话语音菜单、银行验证码输入、智能家居控制……这些看似简单的场景,背后都依赖着一套精密的语音处理流程。本文将带你用 MATLAB 亲手搭建一个完整的数字语音识别系统,从录音开始,到特征提取,再到模型训练与识别,一步步揭开语音识别的神秘面纱。
准备好了吗?🎧 让我们从最基础的问题出发:计算机是怎么“听见”声音的?
语音信号是如何被数字化的?
想象一下,当你发出“5”的时候,空气中的压力波传到麦克风,变成微弱的电信号。但计算机可不懂模拟电压,它只认0和1。所以第一步,就是把这段连续的声波变成离散的数据点——也就是所谓的 数字化 。
这过程分两步走: 采样 + 量化 。
📏 采样率选多少才够用?
人耳能听到的声音频率范围大约是20Hz~20kHz。根据奈奎斯特采样定理,只要采样率超过最高频率的两倍,就能完整还原原始信号。也就是说,理论上32kHz以上就够了。
但在实际应用中,我们常用的是 16kHz ,为什么呢?
因为人类语音的主要能量集中在300Hz~3400Hz之间,电话系统就是基于这个频段设计的(8kHz采样)。对于数字识别这种任务,16kHz已经绰绰有余,既能保留足够的高频细节(比如“7”里的/qi/音),又不会带来太大的计算负担。
下面这张表可以帮你快速决策:
| 采样率 (kHz) | 最大可表示频率 (kHz) | 是否满足语音需求 | 存储开销 | 适用场景 |
|---|---|---|---|---|
| 8 | 4 | 基本满足 | 低 | 电话语音识别 |
| 16 | 8 | 完全满足 ✅ | 中 | 通用语音识别 |
| 44.1 | 22.05 | 超出需求 | 高 | 高保真音频 |
⚠️ 小贴士:如果你用的是笔记本自带麦克风,默认可能是44.1kHz或48kHz。建议先降采样到16kHz,避免后续处理浪费资源。
🔊 MATLAB中如何录音并保存?
MATLAB提供了非常方便的音频接口函数,我们可以轻松录制一段“数字语音”:
% 设置参数
fs = 16000; % 采样率
nBits = 16; % 量化位数
nChannels = 1; % 单声道
duration = 2; % 录2秒
recObj = audiorecorder(fs, nBits, nChannels);
disp('请说一个数字(如"3"),开始录音...');
recordblocking(recObj, duration);
disp('录音结束!');
audioData = getaudiodata(recObj); % 获取数据(double类型,[-1,1])
audiowrite('digit_3.wav', audioData, fs); % 保存为wav文件
录完之后,别忘了可视化看看效果:
[audio, fs] = audioread('digit_3.wav');
t = (0:length(audio)-1)/fs;
plot(t, audio); grid on;
xlabel('时间 (s)'); ylabel('归一化振幅');
title('数字"3"的语音波形');
你会看到类似这样的图形👇:
注意观察中间那一块“密集波动”的区域——那就是你的发音主体部分,前后可能是静音或者呼吸声。接下来我们要做的,就是自动找到这一段有效语音。
如何提升语音质量?前端增强三大法宝
原始录音往往夹杂着环境噪声、低频嗡嗡声,甚至你自己说话时离麦克风忽远忽近导致音量起伏。这些问题如果不处理,后续特征提取就会“失真”。
别急,工程师们早就准备了三件套: 预加重、分帧、加窗 。它们就像美颜滤镜一样,让你的声音更适合被机器“欣赏”。
🔧 预加重:拯救衰弱的高频信息
你知道吗?人在发声时,嘴唇辐射会让高频成分天然衰减。这就意味着像“s”、“sh”、“7(qi)”这类清辅音,在频谱上看起来特别弱,容易被忽略。
解决办法很简单:提前把高频“提亮”一点。这就是 预加重滤波器 的作用。
它的数学表达式是:
$$ y[n] = x[n] - \alpha x[n-1] $$
其中 $\alpha$ 通常取 0.95 左右。
来看个对比图:
alpha = 0.95;
pre_emphasized = filter([1, -alpha], 1, audio);
% 功率谱密度对比
[pxx, f] = pwelch([audio, pre_emphasized], hann(512), 256, 512, fs);
semilogy(f, pxx(:,1), 'b', f, pxx(:,2), 'r');
legend('原始', '预加重后'); xlabel('频率(Hz)'); ylabel('功率谱密度');
title('预加重前后频谱对比'); grid on;
你会发现红色曲线在高频段明显抬升了!📈 这样做不仅能增强清音辨识度,还能平衡整个频谱的能量分布,有利于后面的MFCC提取。
✂️ 分帧:把长语音切成“短句”
语音是非平稳信号——意思是它的统计特性随时间变化。但如果我们假设每 25ms 内它是“基本稳定”的,那就方便多了!
于是我们把一整段语音切成很多小片段,每个叫一“帧”。常见的设置是:
- 帧长:25ms → 对应 16kHz 下 400 个采样点
- 帧移:10ms → 每隔160个点取下一帧(重叠60%)
为什么要重叠?是为了防止两个帧之间的信息断裂。
MATLAB代码如下:
frameSize = round(0.025 * fs); % 25ms帧长
frameStep = round(0.010 * fs); % 10ms步长
frames = buffer(pre_emphasized, frameSize, frameSize - frameStep, 'nodelay');
buffer 函数会自动帮你切好所有帧,并排列成矩阵形式(每一列是一帧)。
🪟 加汉明窗:减少频谱泄露
直接对一帧数据做FFT会有问题——因为相当于乘了一个矩形窗,会导致严重的 频谱泄露 (spectral leakage),也就是能量扩散到邻近频率上去。
解决方法是加上一个平滑过渡的窗函数,比如 汉明窗(Hamming Window) :
$$ w[n] = 0.54 - 0.46 \cos\left(\frac{2\pi n}{N-1}\right) $$
它能让帧两端逐渐趋于零,避免突变。
hammingWindow = hamming(frameSize);
windowedFrames = frames .* repmat(hammingWindow, 1, size(frames,2));
现在每帧数据都被温柔地“包裹”起来,准备进入下一步分析啦!
怎么判断哪里是真正的“语音”?端点检测来帮忙
你总不能让模型去分析你咳嗽、翻书、打喷嚏的声音吧?所以我们需要一种机制,自动找出哪些帧属于“有效语音段”。
这就是 语音活动检测 (Voice Activity Detection, VAD),也叫 端点检测 。
最经典的方法之一是 双门限法 ,结合两个指标:
- 短时能量(Short-term Energy)
- 零交叉率(Zero-Crossing Rate, ZCR)
💡 短时能量:谁的声音更大?
能量高的地方大概率是有声音的。计算方式很简单:
$$ E_n = \sum_{m=0}^{N-1} x_n^2[m] $$
energy = sum(windowedFrames.^2, 1)';
画出来大概是这样👇:
可以看到中间有一段明显的能量峰值,那就是你说“3”的时候。
🔁 零交叉率:清音 vs 浊音的判别器
ZCR衡量的是信号穿过零轴的次数。一般来说:
- 浊音 (如元音 a/e/i):周期性强,ZCR低
- 清音 (如 s/sh/f):随机噪声感强,ZCR高
zcr = sum(abs(diff(sign(windowedFrames))), 1)' / (2 * frameSize);
结合这两个特征,我们就可以设定双重阈值:
highEnergyTh = 0.1 * max(energy);
lowEnergyTh = 0.05 * max(energy);
zcrTh = median(zcr);
speechIndices = (energy > lowEnergyTh) & (zcr < zcrTh);
然后通过简单的状态机逻辑确定起止点,剔除首尾的静音段。这样我们就得到了干净的“语音片段”!
特征提取的核心:MFCC到底是什么?
终于到了最关键的一步—— 特征提取 !
原始波形维度太高(比如2秒语音就有32000个点),而且冗余严重。我们需要一种方法,把语音的关键信息压缩成几十维的小向量,同时保留足够区分不同发音的能力。
这时候, 梅尔频率倒谱系数(MFCC) 登场了!
🧠 为什么MFCC这么牛?
因为它模仿了人耳的听觉特性!
人耳对低频更敏感(比如100Hz到200Hz的变化很容易察觉),而对高频不那么敏感(3000Hz到3100Hz差别不大)。MFCC正是基于这种非线性感知设计的。
它的核心思想是:先把频谱映射到 梅尔刻度(Mel Scale) 上,再提取倒谱系数。
转换公式是:
$$ \text{mel}(f) = 2595 \log_{10}\left(1 + \frac{f}{700}\right) $$
比如:
- 1000 Hz ≈ 1349 mel
- 2000 Hz ≈ 2034 mel
- 4000 Hz ≈ 2603 mel
你会发现,随着频率升高,相同Hz差对应的mel差越来越小——完美匹配人类感知!
📐 构建梅尔滤波器组
为了实现这种非线性加权,我们需要一组三角形带通滤波器,覆盖整个频率范围(0~8000Hz @ 16kHz)。这些滤波器在低频区密集,在高频区稀疏。
下面是MATLAB实现代码:
function filter_bank = create_mel_filterbank(fs, nfft, nfilts)
low_freq = 0;
high_freq = fs / 2;
mel_low = 2595 * log10(1 + low_freq / 700);
mel_high = 2595 * log10(1 + high_freq / 700);
mel_points = linspace(mel_low, mel_high, nfilts + 2);
hz_points = 700 * (10 .^ (mel_points / 2595) - 1);
bin = floor((nfft + 1) * hz_points / fs);
filter_bank = zeros(nfilts, nfft);
for i = 1:nfilts
start_bin = bin(i);
mid_bin = bin(i+1);
end_bin = bin(i+2);
for j = start_bin:mid_bin
if mid_bin ~= start_bin
filter_bank(i,j) = (j - start_bin) / (mid_bin - start_bin);
end
end
for j = mid_bin:end_bin
if end_bin ~= mid_bin
filter_bank(i,j) = (end_bin - j) / (end_bin - mid_bin);
end
end
end
end
调用方式:
fbank = create_mel_filterbank(16000, 512, 24); % 24个滤波器
imagesc(fbank); colorbar; title('梅尔滤波器组响应');
你会看到一组漂亮的三角形👇:
相邻滤波器约有50%重叠,确保没有频率“漏网之鱼”。
🔄 MFCC全流程流水线
完整的MFCC提取流程如下:
预加重 → 分帧 → 加窗 → FFT → 功率谱 → 梅尔滤波 → 对数压缩 → DCT → MFCC
逐个击破:
1. FFT & 功率谱估计
nfft = 512;
fft_spectrum = fft(windowedFrames, nfft, 1);
power_spectrum = abs(fft_spectrum(1:nfft/2+1, :)).^2;
2. 梅尔滤波积分
filter_banks = create_mel_filterbank(fs, nfft, 24);
mel_energy = filter_banks * power_spectrum;
3. 取对数
log_mel_energy = log(mel_energy + 1e-6); % 防止log(0)
4. 离散余弦变换(DCT)
mfcc = dct(log_mel_energy')'; % 转置后DCT
mfcc = mfcc(:, 1:13); % 只取前13维
为啥只取前13维?因为更高维主要反映细微信号波动或噪声,反而影响稳定性。
此外,第0维代表整体能量,容易受音量影响,一般也不用。
动态特征加持:Δ 和 ΔΔ 让模型“看到”变化趋势
静态的MFCC只能描述某一时刻的频谱状态,但语音的本质是 动态演变的过程 。比如“1”发音短促,“9”拖长,“7”有爆发感……
为了让模型捕捉这些时间上的“运动趋势”,我们引入 差分特征 :
- Δ(Delta) :一阶差分,表示“速度”
- ΔΔ(Delta-Delta) :二阶差分,表示“加速度”
计算公式(T=2时):
$$ \Delta c_t = \frac{(c_{t+1} - c_{t-1}) + 2(c_{t+2} - c_{t-2})}{10} $$
MATLAB实现:
function delta = compute_delta(cepstra, window)
[T, D] = size(cepstra);
delta = zeros(T, D);
denominator = 2 * sum((1:window).^2);
for t = 1:T
numerator = 0;
for tau = -window:window
if tau == 0 || t+tau < 1 || t+tau > T
continue;
end
numerator = numerator + tau * cepstra(t+tau, :);
end
delta(t, :) = numerator / denominator;
end
end
最终拼接成39维特征向量:
delta = compute_delta(mfcc, 2);
delta2 = compute_delta(delta, 2);
features = [mfcc, delta, delta2]; % 13 + 13 + 13 = 39维
这个操作几乎成了现代语音系统的标配,哪怕你现在用的Siri或小爱同学,背后也在悄悄计算ΔΔ 😏
数据集怎么组织?命名规则+划分策略
工欲善其事,必先利其器。再好的算法也需要高质量的数据支撑。
建议采集策略:
- 每位说话人对每个数字(0~9)说10遍
- 至少5位不同性别、年龄的人参与录制
- 在安静环境中使用同一麦克风,保持口距一致
文件命名规范推荐:
spk01_dig5_rep3.wav
└────┴────┴────┘
│ │ └── 第3次重复
│ └──────── 数字"5"
└────────────── 第1位说话人
目录结构清晰明了:
dataset/
├── train/
│ ├── 0/
│ ├── 1/
│ └── ...
└── test/
├── 0/
└── ...
关键原则: 按说话人划分训练集和测试集 !
千万不能让同一个说话人的样本同时出现在训练和测试中,否则会严重高估性能。推荐使用留一说话人法(Leave-One-Speaker-Out)或5折交叉验证:
cv = cvpartition(100, 'KFold', 5);
for k = 1:5
trainIdx = training(cv, k);
valIdx = test(cv, k);
% 训练HMM...
end
这样才能真正检验模型的泛化能力!
模型来了!用HMM建模数字发音的时间结构
现在我们有了39维特征序列,接下来要用一个强大的工具来建模它的 时间演化规律 ——那就是 隐马尔可夫模型(HMM) 。
🤔 HMM到底是个啥?
你可以把它想象成一个“看不见的状态机”。
举个例子:“7”这个音可能包含几个阶段:
1. 准备发音(无声)
2. 清擦音/q/
3. 元音/i/
4. 收尾
这些阶段就是“隐藏状态”,你听不到它们,但每个状态会“发射”出特定的MFCC特征。
HMM由五部分组成(五元组):
- 状态集合 S
- 观测序列 O
- 初始概率 π
- 转移矩阵 A
- 发射概率 B
在MATLAB中可以用结构体表示:
hmm_model = struct(...
'N', 5, ...
'pi', [1 0 0 0 0], ...
'A', [0.8,0.2,0,0,0; ...
0,0.7,0.3,0,0; ...
0,0,0.6,0.4,0; ...
0,0,0,0.5,0.5; ...
0,0,0,0,1.0], ...
'B_mean', randn(5,39), ...
'B_cov', cell(5,1));
注意这个A矩阵是 左至右结构 ,不允许回退,符合语音自然流向。
🔢 每个数字一个HMM,共10个模型
我们的策略很简单粗暴:为每个数字(0~9)单独训练一个HMM。
识别时,把新来的语音分别送入这10个模型,看哪个模型给出的概率最大,就认为是哪个数字。
这就是所谓的 最大似然分类 。
📊 状态数怎么选?3~5个刚刚好
太少了拟合不了复杂发音,太多了容易过拟合。
经验表明:
| 数字 | 推荐状态数 | 原因 |
|---|---|---|
| 0 (“ling”) | 5 | 零 + 元音 + 鼻音结尾 |
| 1 (“yi”) | 3 | 单音节,短促 |
| 2 (“er”) | 4 | 卷舌过渡明显 |
| 7 (“qi”) | 5 | 擦音+塞音组合 |
| 9 (“jiu”) | 5 | 多音节复合 |
你可以先统一设为4,后期再调优。
观测概率怎么建模?GMM还是单高斯?
每个HMM状态都需要一个“观测生成器”——即给定某个状态,它产生当前MFCC的概率是多少。
最常见的两种方式:
1️⃣ 单高斯模型(SGM)
假设每个状态的MFCC服从多元高斯分布:
$$ b_j(o_t) = \mathcal{N}(o_t | \mu_j, \Sigma_j) $$
优点:简单高效,适合小样本
缺点:无法刻画多峰分布(比如同一个人说“7”有时轻有时重)
MATLAB实现:
function prob = gaussian_pdf(x, mu, Sigma)
d = length(mu);
invSigma = inv(Sigma);
delta = x - mu;
exponent = -0.5 * delta' * invSigma * delta;
norm_const = 1 / sqrt((2*pi)^d * det(Sigma));
prob = norm_const * exp(exponent);
end
2️⃣ 高斯混合模型(GMM)
用多个高斯加权求和,表达能力更强:
$$ b_j(o_t) = \sum_{m=1}^{M} w_{jm} \cdot \mathcal{N}(o_t | \mu_{jm}, \Sigma_{jm}) $$
适合数据量较大时使用(>100条/类),可用 gmdistribution.fit 快速拟合。
HMM三大问题:评估、解码、训练
HMM的应用围绕三个核心问题展开:
| 问题 | 目标 | 算法 |
|---|---|---|
| 1. 评估 | 给定模型λ和观测O,求P(O|λ) | 前向算法 |
| 2. 解码 | 找出最可能的状态路径Q* | 维特比算法 |
| 3. 训练 | 根据数据优化模型参数 | 鲍姆-韦尔奇(EM) |
✅ 前向算法:算概率
用于计算某个HMM生成当前语音的可能性。
function alpha = forward_alg(obs, pi, A, B)
T = size(obs,1); N = length(pi);
alpha = zeros(T, N);
% 初始化
for i = 1:N
alpha(1,i) = pi(i) * gaussian_pdf(obs(1,:), B.mean(i,:), B.cov{i});
end
% 递推
for t = 2:T
for j = 1:N
alpha(t,j) = sum(alpha(t-1,:) .* A(:,j)) * ...
gaussian_pdf(obs(t,:), B.mean(j,:), B.cov{j});
end
end
end
🔍 维特比算法:找最佳路径
不仅关心总概率,还想看看状态是怎么跳转的。
适合调试和可视化。
🔁 鲍姆-韦尔奇算法:让模型自己学习
这是最难也是最关键的一步——利用大量样本自动调整HMM参数(A、B、π),使得模型能最好地解释所有训练数据。
MATLAB没有内置函数,需自行实现EM迭代过程。不过好消息是,只要你用了合理的初始化(比如用K-means初分状态),收敛通常很快。
最终拼图:完整识别流程跑通!
万事俱备,只欠东风。下面我们串起所有模块,完成一次真实识别:
% 1. 读取待识别语音
[x, fs] = audioread('test_digit.wav');
x = resample(x, 16000, fs); % 统一采样率
% 2. 预处理
x_pre = filter([1,-0.95],1,x);
frames = buffer(x_pre, 400, 320, 'nodelay');
windowed = frames .* hamming(400);
% 3. 端点检测(略)
% 4. 提取MFCC+Δ+ΔΔ → 得到39维特征序列O
% 5. 对10个HMM模型分别计算P(O|λ_i)
scores = zeros(10,1);
for digit = 0:9
model = load(['hmm_',num2str(digit),'.mat']);
scores(digit+1) = forward_prob(O, model);
end
% 6. 找最大得分
[~, predicted] = max(scores);
fprintf('识别结果:%d\n', predicted-1);
运行结果可能是:
识别结果:7
🎉 恭喜!你的语音识别系统上线了!
性能优化与可视化技巧
最后分享几个实用技巧,让你的系统更专业:
🎯 特征归一化:消除说话人间差异
不同人音量、语速、设备不同,导致MFCC分布偏移。建议做 均值方差归一化(MVN) :
mu = mean(all_train_features, 1);
sigma = std(all_train_features, 0, 1);
normalized_feats = (feats - mu) ./ sigma;
这对神经网络尤其重要,也能提升GMM/HMM稳定性。
📊 可视化:t-SNE看看类别是否可分
用降维技术把39维特征投影到2D平面,直观检验聚类效果:
Y = tsne(features, 'Perplexity', 30, 'Exaggeration', 2);
gscatter(Y(:,1), Y(:,2), labels);
title('t-SNE: MFCC特征空间分布');
理想情况下,每个数字应该形成独立簇👇:
如果混在一起,说明特征或数据有问题。
结语:这不是终点,而是起点 🚀
我们从零开始,用MATLAB实现了一个完整的数字语音识别系统,涵盖了:
✅ 语音采集与数字化
✅ 前端增强(预加重、分帧、加窗)
✅ 端点检测(双门限法)
✅ MFCC特征提取(含Δ/ΔΔ)
✅ HMM建模与训练
✅ 最大似然识别
虽然这只是语音识别的“Hello World”,但它所蕴含的思想—— 信号处理 + 特征工程 + 概率建模 ——正是现代ASR系统的基石。
当然,今天的方案也有局限:HMM假设太强、手工特征受限、难以处理连续语音。如果你想进一步升级,可以尝试:
- 换成深度学习模型(DNN-HMM、End-to-End)
- 使用Librosa或PyTorch替代MATLAB
- 加入语言模型提升鲁棒性
但无论如何,请记住: 每一个复杂的智能系统,都是从一行简单的代码开始的 。
现在,轮到你动手了!拿起麦克风,录一段“8”,然后让程序告诉你——它听懂了吗?🎙️💻
简介:本项目是一个基于MATLAB平台的简单语音识别系统,旨在实现对数字0到9的准确识别。系统以隐马尔科夫模型(HMM)为核心,结合维特比算法和鲍姆-韦尔奇算法,完成语音信号的预处理、特征提取、模型训练与解码全过程。用户仅需运行主文件”main.m”即可体验完整的识别流程,适合初学者深入理解HMM在语音识别中的应用。项目包含cdhmm模块,支持连续语音信号建模,是语音识别领域良好的实践入门案例。
更多推荐


所有评论(0)