本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本项目是一个基于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),也叫 端点检测

最经典的方法之一是 双门限法 ,结合两个指标:

  1. 短时能量(Short-term Energy)
  2. 零交叉率(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”,然后让程序告诉你——它听懂了吗?🎙️💻

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本项目是一个基于MATLAB平台的简单语音识别系统,旨在实现对数字0到9的准确识别。系统以隐马尔科夫模型(HMM)为核心,结合维特比算法和鲍姆-韦尔奇算法,完成语音信号的预处理、特征提取、模型训练与解码全过程。用户仅需运行主文件”main.m”即可体验完整的识别流程,适合初学者深入理解HMM在语音识别中的应用。项目包含cdhmm模块,支持连续语音信号建模,是语音识别领域良好的实践入门案例。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐