简介:这份PDF面向具备一定Python基础、希望进入机器学习与语音识别方向的开发者与学习者,围绕「构建一个可运行的语音识别器」这一目标展开。内容从读取与绘制音频数据入手,逐步讲解音频信号转频域、自定义参数生成音频、合成音乐、提取频域特征、创建隐马尔科夫模型,最终整合为完整识别流程,帮助读者把理论落到代码实践。资源为单个PDF文件,压缩包约1.06MB,篇幅紧凑、便于随身查阅与按章节练习。目前已有788人学习,说明其在同类教程中具备一定参考价值。读者可从中获得音频采样与标准化处理、傅里叶变换频谱分析、MFCC特征提取、HMM建模及音素到文本映射等关键环节的实现思路,并借助numpy、scipy、matplotlib等库完成从数据读取到模型搭建的闭环,适合作为语音识别入门与项目实战的案头参考。
1. 从一段 44100Hz 的波形说起:这份 PDF 到底能帮你搭出什么
很多人第一次做语音识别,卡住的地方不是模型,而是连一段 wav 文件怎么读进来、波形长什么样、采样率意味着什么,脑子里都是糊的。这份《Python机器学习项目开发实战_语音识别_编程案例解析实例详解课程教程.pdf》里的第 7 章,恰好就是从这个最底层的问题切入的:它不跟你空谈算法,而是从scipy.io.wavfile读文件开始,一步步带你走到用隐马尔科夫模型(HMM)识别出 7 个英文单词。整章覆盖了读取与绘制音频、时域转频域、自定义参数生成音频、合成音乐、提取 MFCC 频域特征、创建 HMM、搭建语音识别器这条完整链路。适合已经会写 Python、想找一个能跑通的语音识别入门项目练手的开发者,也适合做课程设计或机器学习项目作业的同学拿来当骨架。它不追求工业级精度,但胜在每一步都有可复现的代码和参数解释,能让你真正理解语音识别系统内部在发生什么。
2. 读取音频与频域转换:把声音变成能算的数组
2.1 音频信号的数字化本质与读取方式
音频文件本质上是连续声波的数字化快照。真实世界的声音是连续的模拟波形,要存进计算机,就得按固定时间间隔采样、再把每个采样点的幅度量化成数字。语音场景里常见的采样率是 44100 Hz,意思是每秒钟把信号切成 44100 份,每隔约 1/44100 秒记录一个幅度值。采样率越高,还原出来的声音越接近连续信号,但数据量也越大。理解这一点很关键,因为后面所有的时间轴计算、频率轴换算,都依赖采样率这个基准。
读取音频用scipy.io.wavfile就够了,它返回两个东西:采样率和音频数据数组。音频数据通常是 16 位有符号整型,取值范围在 -32768 到 32767 之间。直接拿这个整数去做傅里叶变换或者画图,数值范围太大,不利于后续处理,所以第一步标准化是必须的。
import numpy as np import matplotlib.pyplot as plt from scipy.io import wavfile # 读取音频文件,返回采样率和音频数据数组 sampling_freq, audio = wavfile.read('input_read.wav') # 打印信号的基本参数 print('Shape:', audio.shape) print('Datatype:', audio.dtype) print('Duration:', round(audio.shape[0] / float(sampling_freq), 3), 'seconds') # 16位有符号整型标准化到 [-1, 1] 区间 audio = audio / (2.**15) # 只取前30个采样点用于可视化 audio = audio[:30] # 建立时间轴,按采样频率缩放 x_values = np.arange(0, len(audio), 1) / float(sampling_freq) # 单位从秒转换为毫秒 x_values *= 1000 plt.plot(x_values, audio, color='black') plt.xlabel('Time (ms)') plt.ylabel('Amplitude') plt.title('Audio signal') plt.show()这段代码里有两个参数值得注意。2.**15是 32768,对应 16 位有符号整型的满量程,除以它就把整数映射到了 -1 到 1 之间。时间轴先除以采样率得到秒,再乘 1000 转成毫秒,这样横轴读起来更直观。只取前 30 个点是因为采样率太高,全部画出来横轴会压缩得看不清单个波形细节。
2.2 傅里叶变换与功率谱的工程实现
时域波形告诉你信号随时间怎么振动,但它不告诉你这段声音里有哪些频率成分。而语音的性质恰恰由频率内容决定,所以必须做时域到频域的转换。傅里叶变换就是干这个的:把复杂信号分解成不同频率正弦波的叠加。工程上用的是快速傅里叶变换(FFT),numpy.fft.fft直接可用。
有一个细节容易翻车:FFT 的结果是关于中心点对称的,后半部分是前半部分的镜像,所以只需要取前一半。另外,我们最终关心的是功率信号,所以要对幅度平方,再做对数变换转成 dB。
import numpy as np from scipy.io import wavfile import matplotlib.pyplot as plt # 读取音频文件 sampling_freq, audio = wavfile.read('input_freq.wav') # 标准化到 [-1, 1] audio = audio / (2.**15) # 提取数组长度 len_audio = len(audio) # 应用傅里叶变换 transformed_signal = np.fft.fft(audio) half_length = int(np.ceil((len_audio + 1) / 2.0)) transformed_signal = abs(transformed_signal[0:half_length]) transformed_signal /= float(len_audio) transformed_signal **= 2 # 提取转换后的信号长度 len_ts = len(transformed_signal) # 根据信号长度的奇偶性,对部分信号乘以2 if len_audio % 2: transformed_signal[1:len_ts] *= 2 else: transformed_signal[1:len_ts-1] *= 2 # 转换为分贝值 power = 10 * np.log10(transformed_signal) # 建立频率轴,单位转换为 kHz x_values = np.arange(0, half_length, 1) * (sampling_freq / len_audio) / 1000.0 plt.figure() plt.plot(x_values, power, color='black') plt.xlabel('Freq (in kHz)') plt.ylabel('Power (in dB)') plt.show()这里有几个参数逻辑要讲清楚。half_length取(len_audio + 1) / 2.0向上取整,是为了处理奇数长度信号时也能正确截取。除以len_audio是归一化,避免功率值随信号长度线性增长。乘 2 的操作是因为只取了前半段频谱,能量少算了一半,需要补偿回来——但直流分量(索引 0)和偶数长度时的奈奎斯特分量(最后一个点)不需要乘 2,所以代码里用奇偶判断做了区分。频率轴的换算公式是采样率 / 信号长度,得到每个频率 bin 的间隔,再除以 1000 转成 kHz。
3. 生成音频与合成音乐:从正弦波到能听的旋律
3.1 用 NumPy 构造可控音频信号
理解了音频是正弦波的混合之后,反过来用 NumPy 生成音频就是顺理成章的事。你可以指定时长、采样率、音调频率,构造出一个纯正弦波,再加上噪声模拟真实环境,最后写成 wav 文件。这个过程能帮你建立对音频参数(时长、采样率、频率、幅度)的直觉。
import numpy as np import matplotlib.pyplot as plt from scipy.io.wavfile import write # 定义输出文件 output_file = 'output_generated.wav' # 指定音频生成参数 duration = 3 # 时长,单位秒 sampling_freq = 44100 # 采样率,单位 Hz tone_freq = 587 # 音调频率,单位 Hz min_val = -2 * np.pi max_val = 2 * np.pi # 生成时间轴和音频信号 t = np.linspace(min_val, max_val, duration * sampling_freq) audio = np.sin(2 * np.pi * tone_freq * t) # 叠加噪声 noise = 0.4 * np.random.rand(duration * sampling_freq) audio += noise # 归一化并转换为16位整型 scaling_factor = pow(2, 15) - 1 audio_normalized = audio / np.max(np.abs(audio)) audio_scaled = np.int16(audio_normalized * scaling_factor) # 写入输出文件 write(output_file, sampling_freq, audio_scaled) # 取前100个采样点可视化 audio = audio[:100] x_values = np.arange(0, len(audio), 1) / float(sampling_freq) x_values *= 1000 plt.plot(x_values, audio, color='black') plt.xlabel('Time (ms)') plt.ylabel('Amplitude') plt.title('Audio signal') plt.show()参数上,tone_freq决定音高,587 Hz 接近 D5 音。np.linspace从 -2π 到 2π 生成时间点,配合sin(2π * f * t)就能得到指定频率的正弦波。噪声幅度 0.4 是经验值,太大会盖过信号,太小听不出效果。写文件前必须归一化到 [-1, 1] 再乘pow(2,15)-1转成 16 位整型,否则write函数会因为数据类型不对而报错或产生爆音。
3.2 基于频率映射表合成音阶序列
单个正弦波只能发出“嘀”一声,要合成音乐,需要一张音阶到频率的映射表,然后按序列拼接不同音高的音频片段。这份资源里用了一个 JSON 文件tone_freq_map.json来存音阶和频率的对应关系,常见做法是参考标准音高频率表(比如 A4 = 440 Hz)来构建。
import json import numpy as np from scipy.io.wavfile import write def synthesizer(freq, duration, amp=1.0, sampling_freq=44100): """基于频率和时长合成音调""" t = np.linspace(0, duration, int(duration * sampling_freq)) audio = amp * np.sin(2 * np.pi * freq * t) return audio.astype(np.int16) if __name__ == '__main__': tone_map_file = 'tone_freq_map.json' with open(tone_map_file, 'r') as f: tone_freq_map = json.loads(f.read()) # 定义音阶序列及每个音的持续时间 tone_seq = [('D', 0.3), ('G', 0.6), ('C', 0.5), ('A', 0.3), ('Asharp', 0.7)] amplitude = 10000 sampling_freq = 44100 output = np.array([]) for item in tone_seq: input_tone = item[0] duration = item[1] synthesized_tone = synthesizer(tone_freq_map[input_tone], duration, amplitude, sampling_freq) output = np.append(output, synthesized_tone, axis=0) write('output_tone_seq.wav', sampling_freq, output)synthesizer函数里amp控制音量,10000 这个值在 16 位整型范围内留了足够余量,不会削波。tone_seq列表里每个元组是音阶名和持续秒数,按顺序拼接就能得到一段简单旋律。np.append沿 axis=0 拼接一维数组,最终写成一个完整的 wav 文件。这套逻辑虽然简单,但它是理解更复杂音频合成(比如加包络、和弦)的基础。
4. 提取 MFCC 特征与构建 HMM 分类器
4.1 MFCC 与滤波器组特征的提取
原始音频数据维度太高,直接丢给分类器效果很差。语音识别里最常用的频域特征是梅尔频率倒谱系数(MFCC),它模拟人耳对频率的非线性感知,把功率谱通过梅尔滤波器组和离散余弦变换压缩成低维特征向量。这份资源用的是python_speech_features包里的mfcc和logfbank两个函数。
import numpy as np import matplotlib.pyplot as plt from scipy.io import wavfile from python_speech_features import mfcc, logfbank # 读取音频文件 sampling_freq, audio = wavfile.read("input_freq.wav") # 提取MFCC和滤波器组特征 mfcc_features = mfcc(audio, sampling_freq) filterbank_features = logfbank(audio, sampling_freq) # 打印特征维度信息 print('\nMFCC:\nNumber of windows =', mfcc_features.shape[0]) print('Length of each feature =', mfcc_features.shape[1]) print('\nFilter bank:\nNumber of windows =', filterbank_features.shape[0]) print('Length of each feature =', filterbank_features.shape[1]) # 可视化MFCC特征 mfcc_features = mfcc_features.T plt.matshow(mfcc_features) plt.title('MFCC') # 可视化滤波器组特征 filterbank_features = filterbank_features.T plt.matshow(filterbank_features) plt.title('Filter bank') plt.show()mfcc函数默认返回一个二维数组,行数是时间窗数量,列数是每个窗的特征维度(默认 13 维)。logfbank返回滤波器组能量,维度更高(默认 26 维),适合做更细粒度的分析。转置之后用plt.matshow画出来,横轴是时间,纵轴是特征维度,颜色深浅代表数值大小。这里有个常见坑:python_speech_features的导入名是python_speech_features,但有些老教程写的是from features import mfcc,那是旧版本的写法,新版本已经改了包名,照着老代码跑会直接 ImportError。
4.2 用 hmmlearn 封装高斯隐马尔科夫模型
隐马尔科夫模型擅长处理时间序列数据,音频信号恰好就是典型的时间序列。它的核心思想是:观测到的音频特征由一组隐藏状态生成,通过训练找到这些隐藏状态的转移概率和发射概率,就能对信号建模。这份资源用hmmlearn包里的GaussianHMM来封装一个训练器类。
import numpy as np from hmmlearn import hmm class HMMTrainer(object): def __init__(self, model_name='GaussianHMM', n_components=4, cov_type='diag', n_iter=1000): self.model_name = model_name self.n_components = n_components self.cov_type = cov_type self.n_iter = n_iter self.models = [] if self.model_name == 'GaussianHMM': self.model = hmm.GaussianHMM( n_components=self.n_components, covariance_type=self.cov_type, n_iter=self.n_iter ) else: raise TypeError('Invalid model type') def train(self, X): """X是二维数组,每一行是一个特征向量""" np.seterr(all='ignore') self.models.append(self.model.fit(X)) def get_score(self, input_data): """对输入数据运行模型,返回得分""" return self.model.score(input_data)参数选择上,n_components=4表示假设有 4 个隐藏状态,这个数字取决于你的数据复杂度,词内音素越多,需要的状态可能越多。cov_type='diag'表示协方差矩阵是对角矩阵,计算量小,适合特征维度不高的情况;如果特征维度之间相关性强的,可以改成'full'。n_iter=1000是训练迭代上限,实际训练时模型可能在更少迭代内收敛。np.seterr(all='ignore')是为了压制训练过程中可能出现的数值警告,不影响结果但能让终端干净些。
4.3 搭建完整的语音识别器
有了 HMM 训练器,接下来就是把它用到实际的单词识别任务上。这份资源用了一个包含 7 个单词、每个单词 15 个音频文件的小型数据库。思路很直接:为每个单词类别训练一个独立的 HMM,识别新音频时,把它分别喂给所有模型,得分最高的那个模型对应的标签就是预测结果。
import os import argparse import numpy as np from scipy.io import wavfile from hmmlearn import hmm from python_speech_features import mfcc def build_arg_parser(): parser = argparse.ArgumentParser(description='Trains the HMM classifier') parser.add_argument("--input-folder", dest="input_folder", required=True, help="Input folder containing the audio files in subfolders") return parser if __name__ == '__main__': args = build_arg_parser().parse_args() input_folder = args.input_folder hmm_models = [] # 遍历输入文件夹下的每个子文件夹 for dirname in os.listdir(input_folder): subfolder = os.path.join(input_folder, dirname) if not os.path.isdir(subfolder): continue # 子文件夹名即为类别标签 label = subfolder[subfolder.rfind('/') + 1:] X = np.array([]) y_words = [] # 遍历子文件夹中的音频文件,留最后一个用于测试 for filename in [x for x in os.listdir(subfolder) if x.endswith('.wav')][:-1]: filepath = os.path.join(subfolder, filename) sampling_freq, audio = wavfile.read(filepath) mfcc_features = mfcc(audio, sampling_freq) if len(X) == 0: X = mfcc_features else: X = np.append(X, mfcc_features, axis=0) y_words.append(label) # 为每个类别训练一个HMM模型 hmm_trainer = HMMTrainer() hmm_trainer.train(X) hmm_models.append((hmm_trainer, label)) hmm_trainer = None # 测试文件列表 input_files = [ 'data/pineapple/pineapple15.wav', 'data/orange/orange15.wav', 'data/apple/apple15.wav', 'data/kiwi/kiwi15.wav' ] for input_file in input_files: sampling_freq, audio = wavfile.read(input_file) mfcc_features = mfcc(audio, sampling_freq) max_score = None output_label = None # 遍历所有模型,选取得分最高的 for item in hmm_models: hmm_model, label = item score = hmm_model.get_score(mfcc_features) if max_score is None or score > max_score: max_score = score output_label = label print("\nTrue:", input_file[input_file.find('/')+1:input_file.rfind('/')]) print("Predicted:", output_label)这段代码有几个关键设计点。每个类别的所有训练样本的 MFCC 特征被纵向拼接成一个大的二维数组,一次性喂给 HMM 训练,这样模型能学到该类别的整体特征分布。测试时留出每个类别的最后一个文件不参与训练,保证评估的公正性。get_score返回的是模型对输入数据的对数似然,值越大说明该模型越匹配这段音频。max_score初始为None而不是 0,是因为对数似然可能是负数,用 0 初始化会导致第一个模型永远被跳过。
5. 避坑与排查:跑通这套代码必须跨过的五个坎
5.1 现象:ImportError: No module named 'features'
原因:这份资源的代码写于较早时期,当时python_speech_features包的导入方式是from features import mfcc。新版本已经改为from python_speech_features import mfcc,包名变了但很多老教程没更新。
解决:统一改成from python_speech_features import mfcc, logfbank,并用pip install python_speech_features安装。如果 pip 源里找不到,可以尝试从源码安装,但注意不要装成同名的其他包。
5.2 现象:wavfile.read读进来的数组是 int16,做 FFT 后数值巨大
原因:16 位整型的取值范围是 -32768 到 32767,直接做傅里叶变换得到的幅度值会非常大,后续平方和对数运算容易溢出或产生无效值。
解决:读取后立即执行audio = audio / (2.**15)标准化到 [-1, 1]。这一步在所有涉及数值计算的环节之前都要做,包括 FFT、MFCC 提取和写文件前的归一化。
5.3 现象:HMM 训练时报ValueError: Expected 2D array, got 1D array instead
原因:hmmlearn的fit方法要求输入是二维数组,每一行是一个特征向量。如果直接把一维的音频数组传进去,就会报这个错。
解决:确保传给train方法的是 MFCC 特征矩阵,形状为(时间窗数量, 特征维度)。如果只有一个样本,用mfcc_features.reshape(1, -1)显式转成二维。多个样本拼接时用np.append(X, mfcc_features, axis=0),不要用np.concatenate搞错轴。
5.4 现象:识别结果总是预测成同一个单词
原因:最常见的情况是某个类别的训练数据量远大于其他类别,或者 MFCC 特征没有做均值归一化,导致模型被某个类别主导。另外,n_components设置过大而训练数据太少,模型会过拟合到某个局部最优。
解决:先检查每个类别的样本数量是否均衡。然后对 MFCC 特征做均值归一化(减去均值除以标准差),这一步能显著提升不同模型之间的可比性。如果数据量确实少,把n_components降到 2 或 3,减少模型复杂度。
5.5 现象:生成的 wav 文件播放时有严重爆音或完全没声音
原因:写文件前没有做归一化,或者归一化后没有转成 int16。scipy.io.wavfile.write对数据类型敏感,传 float 数组进去可能被截断成 0 或者产生溢出。
解决:写文件前执行audio_normalized = audio / np.max(np.abs(audio)),再乘pow(2,15)-1转成np.int16。如果音频全零,检查np.max(np.abs(audio))是否为 0,是的话说明信号本身有问题,回溯生成环节。
6. 进阶技巧:让这套识别器更稳的几个实操习惯
跑通基础版本之后,有几个习惯能帮你把识别准确率往上提一截。第一个是给 MFCC 特征加滑动窗口均值归一化。原始 MFCC 对录音音量、信道差异很敏感,同一单词在不同录音条件下提取的特征分布会偏移。常见做法是在时间轴上用一个滑动窗计算均值和标准差,然后对每个窗内的特征做标准化。这个操作不需要额外依赖,用numpy的cumsum就能实现,代价是引入一个窗口长度参数,一般取 50 到 100 帧之间。
第二个习惯是训练 HMM 之前先做 PCA 降维。MFCC 默认 13 维,加上一阶差分和二阶差分能到 39 维,但小数据集上高维特征反而容易让协方差矩阵估计不准。用sklearn.decomposition.PCA降到 8 到 10 维,训练速度会快很多,识别率通常不会掉,有时还会升。注意 PCA 的fit只能在训练集上做,测试集用同样的变换矩阵投影,否则就是数据泄露。
第三个是给每个类别的 HMM 设置不同的n_components。单词长度不一样,短单词(比如 “apple” 和 “kiwi”)用 3 个状态可能就够了,长单词(比如 “pineapple”)用 5 到 6 个状态更合适。一个偷懒但有效的做法是按音频平均时长来分配状态数,时长越长状态越多,比例控制在每 0.3 秒一个状态左右。
最后一个习惯是保留一个验证集来调参。这份资源里只留了最后一个文件做测试,样本太少,单次结果波动大。我一般会做 3 折交叉验证:把每个类别的 15 个文件分成 3 份,轮流拿 2 份训练、1 份测试,取平均识别率。这样调出来的n_components和cov_type更靠谱,不会因为某一次运气好就以为参数选对了。
从那以后我每次做音频分类项目,都会先把采样率、位深、声道数这三个参数确认一遍再动手写特征提取,因为血泪经验告诉我,百分之八十的玄学问题都出在数据格式没对齐上。希望帮到你。
本文还有配套的精品资源,点击获取