简介:本资源为基于Python与TensorFlow实现的声纹识别完整项目,面向计算机、人工智能相关专业的毕业设计、期末大作业与课程设计需求者,也适合希望入门语音识别与深度学习实战的开发者。项目围绕声纹特征提取、模型训练与说话人识别对比展开,包含训练、推理、数据生成与对比识别等核心脚本,并配有代码注释,新手也能理解整体流程。压缩包共22个文件,以11个py源码为主,辅以6个wav音频样本、2张jpg示意图、1个txt依赖说明、1个md文档及gitignore配置,整体约725KB,结构清晰、便于按模块查阅。已有324人学习下载,说明其具备一定参考价值。读者可获得可运行的声纹识别工程、音频数据样例、模型训练与推理思路,以及项目文档说明,适合直接用于课题答辩、作业提交或在此基础上二次开发与实验对比。
1. 声纹识别落地:从一段 3 秒录音到可复现的 Python+Tensorflow 工程
很多人第一次接触声纹识别,是被「一句话证明你是你」这个场景吸引的:打电话给银行客服,不用报身份证号,系统听你说几句就能确认身份。但真正动手做的时候,卡住新手的往往不是模型结构,而是「一段 wav 怎么变成模型能吃的张量」「两个人声音像怎么区分」「自己录的数据只有几十条够不够」。基于 Python+Tensorflow 实现声纹识别,本质是把音频信号处理、特征提取、深度网络训练和相似度度量串成一条可复现的流水线,而不是调一个库就完事。这篇笔记面向想拿到一份能跑通的源代码、并且愿意照着改参数做自己数据的从业者,从环境、特征、模型到避坑,把这条链路拆开讲清楚。适合有 Python 基础、做过一点深度学习、但没系统做过音频任务的人。
2. 声纹识别的技术底座:为什么是 MFCC 加 Tensorflow 这套组合
声纹识别(Speaker Recognition)分两个子任务:说话人辨认(identification,判断是库里哪个人)和说话人确认(verification,判断是不是声称的那个人)。工程上 90% 的落地是确认任务,因为它的输出是「接受/拒绝」,配合阈值就能做门禁、客服核身。要理解后面代码为什么那么写,得先搞清楚三件事:声音怎么表示、模型学什么、怎么判断两个人像不像。
2.1 从波形到 MFCC:一段音频到底被抽成了什么
原始音频是一维时序信号,采样率常见 16000Hz,也就是每秒 16000 个浮点数。直接把这串数喂给网络,效果差且难收敛,因为人耳对频率的感知不是线性的,而且说话内容(说了什么字)会干扰说话人身份。所以要先做特征提取,把「说了什么」尽量压掉,留下「谁在说」。
最经典的特征是 MFCC(梅尔频率倒谱系数)。它的处理链条是:预加重 → 分帧 → 加窗 → FFT → 梅尔滤波器组 → 取对数 → DCT。每一步都有物理意义,不是玄学。预加重(通常系数 0.97)是为了补偿高频在发声时被压抑;分帧一般 25ms 一帧、10ms 一帧移,因为语音在 10~30ms 内可认为平稳;加汉明窗减少频谱泄漏;梅尔滤波器组模拟人耳对低频更敏感的特性。
import tensorflow as tf import numpy as np def wav_to_mfcc(wav_path, sample_rate=16000, num_mfcc=40, frame_ms=25, hop_ms=10): # 读取 wav,统一单声道、指定采样率 audio_binary = tf.io.read_file(wav_path) waveform, _ = tf.audio.decode_wav(audio_binary, desired_channels=1) waveform = tf.squeeze(waveform, axis=-1) # 重采样到 16k,保证和训练集一致 if sample_rate != 16000: waveform = tf.signal.resample(waveform, int(len(waveform) * sample_rate / 16000)) # 分帧参数换算成采样点 frame_length = int(sample_rate * frame_ms / 1000) # 400 点 frame_step = int(sample_rate * hop_ms / 1000) # 160 点 # 短时傅里叶变换 stft = tf.signal.stft(waveform, frame_length=frame_length, frame_step=frame_step, fft_length=512) spectrogram = tf.abs(stft) # 梅尔滤波器组,默认 80 个梅尔频带 mel_weight = tf.signal.linear_to_mel_weight_matrix( num_mel_bins=80, num_spectrogram_bins=spectrogram.shape[-1], sample_rate=sample_rate, lower_edge_hertz=20, upper_edge_hertz=7600) mel_spec = tf.tensordot(spectrogram, mel_weight, axes=1) log_mel = tf.math.log(mel_spec + 1e-6) # DCT 取前 num_mfcc 维,丢掉第 0 维能量项 mfcc = tf.signal.mfccs_from_log_mel_spectrograms(log_mel)[..., 1:num_mfcc+1] return mfcc这段代码里几个参数直接决定后面模型能不能学出东西。num_mfcc=40是常见取值,太少丢信息,太多引入冗余;frame_ms=25、hop_ms=10是语音领域的经验值,改大改小都会影响时间分辨率;lower_edge_hertz=20是因为 20Hz 以下基本是噪声。注意mfccs_from_log_mel_spectrograms返回的第 0 维是能量,和音量强相关,说话人音量一变就漂,所以取[..., 1:num_mfcc+1]把它丢掉,这是很多人第一次做声纹时不知道的细节。
2.2 为什么选 Tensorflow 而不是别的框架
2024 年 Tensorflow 和 PyTorch 的流行趋势讨论很多,学术圈 PyTorch 占优,但声纹识别这类要上生产、要部署到移动端或服务端的场景,Tensorflow 的 TFLite、TF Serving 生态仍然省事。而且tf.signal里 STFT、梅尔滤波器、MFCC 都是现成的,不用自己手写 numpy 版本,少一堆数值 bug。选 Tensorflow 2.x 的另一个理由是 Keras 高层 API 让模型定义和训练循环足够短,新手能在一屏内看懂。
环境上,如果只有 CPU,装tensorflow即可;要用 GPU,得对齐 CUDA、cuDNN 和驱动版本。常见做法是先nvidia-smi看驱动支持的 CUDA 上限,再装对应版本的tensorflow。比如驱动较新时,pip install tensorflow==2.15这类带 GPU 支持的版本能直接用,不必手动装 CUDA 工具包,因为 pip 包自带运行时。装完用下面两行验证:
import tensorflow as tf print(tf.__version__, tf.config.list_physical_devices('GPU'))如果 GPU 列表为空但你有显卡,八成是版本没对齐,这时候别急着怀疑代码,先查版本矩阵。CPU 训练小数据集也能跑,只是慢,做原型够用。
2.3 数据怎么组织:说话人标签和三元组
声纹识别的数据集结构和图像分类不一样。图像分类是「一张图一个标签」,声纹是「一个人多段音频」。常见组织方式是每个说话人一个文件夹,里面放若干条 wav:
dataset/ speaker_001/ a.wav b.wav c.wav speaker_002/ ...训练时有两种主流损失:softmax 分类(把每个说话人当一类)和三元组损失(Triplet Loss,拉近同人、推远异人)。前者实现简单,适合说话人数量固定的闭集场景;后者泛化好,适合开集确认。源代码里如果用的是分类头,那推理时取倒数第二层作为 embedding,再算余弦相似度;如果用三元组,直接输出 embedding。理解这一点,才能看懂后面模型输出维度为什么那么设。
3. 用 Tensorflow 搭一个能跑的声纹模型:从数据管道到训练
这一章是核心,把数据加载、模型定义、训练循环、评估四步走完。每一步都给可抄的代码和参数说明,照着改就能跑自己的数据。
3.1 构建 tf.data 管道:批处理、缓存与打乱
音频长度不一,不能直接 stack 成 batch。常见做法是固定截取或填充到统一长度,比如 3 秒(16000*3=48000 点),对应 MFCC 约 300 帧。短于 3 秒的补零,长于 3 秒的随机截一段,随机截取本身也是一种数据增强。
import os, glob import tensorflow as tf SAMPLE_RATE = 16000 FIXED_LEN = 48000 # 3 秒 def load_and_fix(path, label): audio_binary = tf.io.read_file(path) waveform, _ = tf.audio.decode_wav(audio_binary, desired_channels=1) waveform = tf.squeeze(waveform, axis=-1) # 统一长度:短补长截 if tf.shape(waveform)[0] < FIXED_LEN: pad = FIXED_LEN - tf.shape(waveform)[0] waveform = tf.pad(waveform, [[0, pad]]) else: # 训练时随机截取,推理时取前 3 秒 offset = tf.random.uniform([], 0, tf.shape(waveform)[0]-FIXED_LEN, dtype=tf.int32) waveform = waveform[offset:offset+FIXED_LEN] return waveform, label def build_dataset(root, batch_size=32, shuffle=True): paths, labels = [], [] speakers = sorted(os.listdir(root)) spk2id = {s: i for i, s in enumerate(speakers)} for s in speakers: for w in glob.glob(os.path.join(root, s, '*.wav')): paths.append(w); labels.append(spk2id[s]) ds = tf.data.Dataset.from_tensor_slices((paths, labels)) if shuffle: ds = ds.shuffle(len(paths), seed=42) ds = ds.map(load_and_fix, num_parallel_calls=tf.data.AUTOTUNE) ds = ds.batch(batch_size).prefetch(tf.data.AUTOTUNE) return ds, len(speakers)batch_size=32是显存和收敛速度的折中,显存小就降到 16 或 8;shuffle的 seed 固定是为了可复现,做实验对比时别每次变;prefetch(AUTOTUNE)让数据加载和 GPU 计算重叠,不加的话 GPU 经常空等,训练速度差一截。num_parallel_calls同理,CPU 核多就让它自动调。
3.2 模型结构:CNN 加统计池化的最小可用版本
声纹模型不需要很复杂,一个 2D CNN 加统计池化(把时间维做均值和标准差拼接)就能到可用水平。输入是 MFCC 的 (时间帧, 特征维) 矩阵,当成单通道图像处理。
from tensorflow.keras import layers, Model def build_speaker_model(num_speakers, input_shape=(300, 40), embed_dim=128): inp = layers.Input(shape=input_shape) x = layers.Reshape((input_shape[0], input_shape[1], 1))(inp) # 4 个卷积块,逐步抽高层特征 for filters in [32, 64, 128, 256]: x = layers.Conv2D(filters, 3, padding='same', activation='relu')(x) x = layers.BatchNormalization()(x) x = layers.MaxPooling2D((2, 2))(x) # 统计池化:时间维均值+标准差,对时长变化更鲁棒 mean = layers.GlobalAveragePooling2D()(x) std = layers.Lambda(lambda t: tf.math.reduce_std(t, axis=[1, 2]))(x) x = layers.Concatenate()([mean, std]) x = layers.Dense(256, activation='relu')(x) x = layers.Dropout(0.3)(x) embed = layers.Dense(embed_dim, name='embedding')(x) # 推理时取这层 out = layers.Dense(num_speakers, activation='softmax')(embed) return Model(inp, out)embed_dim=128是 embedding 维度,太小区分度不够,太大容易过拟合,128 或 256 都常见。统计池化那一步是关键:如果只用GlobalAveragePooling,时长不同的音频均值会漂,加上标准差能保留更多分布信息,这是声纹模型比普通分类模型多出来的一步。Dropout(0.3)在小数据集上必加,否则训练集准确率飙到 99%、验证集不动。
3.3 训练循环与学习率调度
编译和训练用 Keras 标准流程,加两个回调:早停和降低学习率。
model = build_speaker_model(num_speakers=50) model.compile( optimizer=tf.keras.optimizers.Adam(1e-3), loss='sparse_categorical_crossentropy', metrics=['accuracy']) callbacks = [ tf.keras.callbacks.EarlyStopping(patience=8, restore_best_weights=True), tf.keras.callbacks.ReduceLROnPlateau(factor=0.5, patience=4, min_lr=1e-6), ] model.fit(train_ds, validation_data=val_ds, epochs=100, callbacks=callbacks)Adam(1e-3)是默认起点,如果 loss 震荡就降到 1e-4;patience=8表示验证集 8 轮不提升就停,防止过拟合;ReduceLROnPlateau在平台期把学习率砍半,比手动调省心。训练时盯两个数:训练 loss 和验证 loss 的差距。差距持续拉大就是过拟合,回去加数据或加 Dropout;两个都降不下去就是欠拟合,检查特征提取有没有写错。
3.4 推理:用 embedding 算余弦相似度做确认
训练完,把模型截到 embedding 层,注册时存每个人的 embedding 均值,验证时算余弦相似度,超过阈值就接受。
embed_model = Model(inputs=model.input, outputs=model.get_layer('embedding').output) def get_embedding(wav_path): waveform, _ = load_and_fix(wav_path, 0) mfcc = wav_to_mfcc_from_tensor(waveform) # 复用 2.1 的 MFCC 逻辑 mfcc = mfcc[tf.newaxis, ...] emb = embed_model(mfcc, training=False) return tf.math.l2_normalize(emb, axis=-1) def verify(wav_a, wav_b, threshold=0.75): ea, eb = get_embedding(wav_a), get_embedding(wav_b) sim = float(tf.reduce_sum(ea * eb)) # 已归一化,点积即余弦 return sim >= threshold, simthreshold=0.75不是拍脑袋,要在你的验证集上画 FAR/FRR 曲线找等错误率点。不同数据、不同模型阈值差很多,直接抄别人的阈值是常见翻车点。l2_normalize保证点积等于余弦相似度,省一次除法。
4. 避坑与排查:声纹识别最容易翻车的 5 个地方
这一章全是血泪经验,每条按「现象 → 原因 → 解决」写,遇到问题先对照这里。
现象:训练准确率很快到 99%,但换一段新录音就认错。原因:数据集里同一个人的多条音频来自同一次录音、同一设备,模型学到的是录音环境而不是声纹。解决:采集时让每个人在不同时间、不同设备、不同距离录,至少 3 种条件;训练时加 SpecAugment(对 MFCC 做时间掩蔽和频率掩蔽)做增强。
现象:验证时同一个人两条音频相似度只有 0.3,明显偏低。原因:MFCC 提取时采样率没统一,或者推理时用了训练时没见过的帧长。解决:把采样率、帧长、帧移、MFCC 维度写成全局常量,训练和推理共用同一个函数,别在两处各写一遍。
现象:GPU 显存够但训练极慢,GPU 利用率只有 20%。原因:数据管道没开 prefetch,或者 MFCC 在 Python 层逐条算,CPU 成瓶颈。解决:把 MFCC 计算放进tf.data.map里用 Tensorflow 算子做,别用 librosa 在 numpy 层算;加prefetch(AUTOTUNE)和num_parallel_calls。
现象:loss 变成 NaN。原因:MFCC 取 log 时遇到 0,log(0)是负无穷。解决:tf.math.log(mel_spec + 1e-6),这个 1e-6 不能省;另外检查输入音频有没有全是静音的片段。
现象:注册 50 个人后,新增第 51 个人要重训模型。原因:用了 softmax 分类头,类别数固定。解决:改用三元组损失或 ArcFace 这类度量学习损失,模型只输出 embedding,新增人只需注册 embedding,不用重训。这是闭集和开集的分水岭,做产品前先想清楚。
5. 进阶技巧:把等错误率压下来的三个实操手段
模型能跑通只是起点,真正决定能不能用的是等错误率(EER)。我一般按下面顺序调,每步都能看到 EER 下降。
第一,换更强的 embedding。CNN 加统计池化是基线,想再降 EER,常见做法是把 CNN 换成 ECAPA-TDNN 或 ResNet 结构,或者用预训练模型提取 embedding 再微调。但注意,结构越复杂,小数据集上越容易过拟合,数据不到几万条时,先把数据增强做足比换模型划算。
第二,做分数归一化。同一个阈值在不同人、不同信道下表现差很多,用 AS-Norm 或简单地对每个说话人的注册 embedding 做中心化,能显著降低跨信道误差。实操上,注册时存多条音频的 embedding 均值和标准差,验证时算 z-score,比裸余弦稳。
第三,调阈值要看业务。门禁场景宁可拒真也不能认假,阈值往高调;客服核身可以容忍一点误接受,阈值往低调。别追求单一 EER 数字,画一张 FAR/FRR 随阈值变化的表,让业务方选点。
| 手段 | 预期 EER 降幅 | 代价 |
|---|---|---|
| SpecAugment 增强 | 1~3 个百分点 | 训练变慢 |
| 统计池化替换平均池化 | 2~4 个百分点 | 几乎无 |
| 分数归一化 | 3~5 个百分点 | 需存注册统计量 |
| 换 ECAPA-TDNN | 5 个百分点以上 | 数据需求大 |
最后说个习惯:每次改完参数,固定随机种子跑三遍取平均,别信单次结果。声纹这行,随机性带来的波动经常比你想改的那个参数还大。我早期就是被一次「调参后 EER 降了 2 个点」骗过,复现三次发现是噪声。希望帮到你。
本文还有配套的精品资源,点击获取