本章前面介绍了纯理论知识,目的是阐述语音识别的方法。接着搭建了开发环境,让读者可以动手编写代码。下面以识别特定词为例,使用深度学习方法和Python语言实现一个实战项目——基于特征词的语音唤醒。
说明:本例的目的是演示一个语音识别的Demo。如果读者已经安装开发环境,可以直接复制代码运行;如果没有,可学习完本章后再回头练习。我们会在本节中详细介绍每一步的操作过程和设计方法。
2.3.1 数据的准备
深度学习的第一步(也是重要的步骤)是数据的准备。数据的来源多种多样,既有不同类型的数据集,也有根据项目需求由项目组自行准备的数据集。由于本例的目的是识别特定词语而进行语音唤醒,因此采用一整套专门的语音命令数据集SpeechCommands,我们可以使用PyTorch专门的下载代码获取完整的数据集,代码如下:
#直接下载PyTorch数据库的语音文件
from torchaudio import datasets
datasets.SPEECHCOMMANDS(
root="./dataset", # 保存数据的路径
url='speech_commands_v0.02', # 下载数据版本URL
folder_in_archive='SpeechCommands',
download=True # 这个记得选True
)
SpeechCommands的数据量约为2GB,等待下载完毕后,可以在下载路径中查看下载的数据集,如图2-21所示。
打开数据集可以看到,根据不同的文件夹名称,其中内部被分成了40个类别,每个类别以名称命名,包含符合该文件名的语音发音,如图2-22和图2-23所示。
可以看到,根据文件名对每个发音进行归类,其中包含:
- 训练集包含51088个WAV语音文件。
- 验证集包含6798个WAV语音文件。
- 测试集包含6835个WAV语音文件。
读者可以使用计算机自带的语音播放程序试听部分语音。
2.3.2 数据的处理
下面开始进入这个语音识别Demo的代码实现部分。相信读者已经试听过部分语音内容,摆在读者面前的第一个难题是,如何将语音转换成计算机可以识别的信号。
梅尔频率是基于人耳听觉特性提出来的,它与Hz频率呈非线性对应关系。梅尔频率倒谱系数(Mel-Frequency Cepstral Coefficients,MFCC)则是利用它们之间的这种关系计算得到的Hz频谱特征,主要用于语音数据特征提取和降低运算维度。例如,对于一帧有512维(采样点)的数据,经过MFCC后可以提取出最重要的40维(一般而言),数据同时也达到了降维的目的。
这里,我们将MFCC理解成使用一个“数字矩阵”来替代一段语音即可。计算MFCC实际上是一个烦琐的任务,需要使用专门的类库来实现对语音MFCC的提取,代码处理如下:
【程序2-1】
import os
import numpy as np
#获取文件夹中所有的文件地址
def list_files(path):
files = []
for item in os.listdir(path):
file = os.path.join(path, item)
if os.path.isfile(file):
files.append(file)
return files
# 这里是一个对单独序列的cut或者pad的操作
# 这里输入的y是一个一维的序列,将输入的一维序列y拉伸或者裁剪到length长度
def crop_or_pad(y, length, is_train=True, start=None):
if len(y) < length: #对长度进行判断
y = np.concatenate([y, np.zeros(length - len(y))]) #若长度过短则进行补全操作
n_repeats = length // len(y)
epsilon = length % len(y)
y = np.concatenate([y] * n_repeats + [y[:epsilon]])
elif len(y) > length: #对长度进行判断,若长度过长则进行截断操作
if not is_train:
start = start or 0
else:
start = start or np.random.randint(len(y) - length)
y = y[start:start + length]
return y
import librosa as lb
# 计算梅尔频率图
def compute_melspec(y, sr, n_mels, fmin, fmax):
"""
:param y:传入的语音序列,每帧的采样
:param sr: 采样率
:param n_mels: 梅尔滤波器的频率倒谱系数
:param fmin: 短时傅里叶变换(STFT)的分析范围 min
:param fmax: 短时傅里叶变换(STFT)的分析范围 max
:return:
"""
# 计算Mel频谱图的函数
melspec = lb.feature.melspectrogram(y=y, sr=sr, n_mels=n_mels, fmin=fmin, fmax=fmax) # (128, 1024) 这个是输出一个声音的频谱矩阵
# Python中用于将语音信号的功率值转换为分贝(dB)值的函数
melspec = lb.power_to_db(melspec).astype(np.float32)
return melspec
# 对输入的频谱矩阵进行正则化处理
def mono_to_color(X, eps=1e-6, mean=None, std=None):
mean = mean or X.mean()
std = std or X.std()
X = (X - mean) / (std + eps)
_min, _max = X.min(), X.max()
if (_max - _min) > eps: #对越过阈值的内容进行处理
V = np.clip(X, _min, _max)
V = 255. * (V - _min) / (_max - _min)
V = V.astype(np.uint8)
else:
V = np.zeros_like(X, dtype=np.uint8)
return V
#创建语音特征矩阵
def audio_to_image(audio, sr, n_mels, fmin, fmax):
#获取梅尔频率图
melspec = compute_melspec(audio, sr, n_mels, fmin, fmax)
#进行正则化处理
image = mono_to_color(melspec)
return image
使用创建好的MFCC生产函数获取特定语音的MFCC矩阵也很容易,代码如下:
import numpy as np
from torchaudio import datasets
import sound_untils
import soundfile as sf
print("开始数据处理")
target_classes = ["bed","bird","cat","dog","four"]
counter = 0
sr = 16000
n_mels = 128
fmin = 0
fmax = sr//2
file_folder = "./dataset/SpeechCommands/speech_commands_v0.02/"
labels = []
sound_features = []
for classes in target_classes:
target_folder = file_folder + classes
_files = sound_untils.list_files(target_folder)
for _file in _files:
audio, orig_sr = sf.read(_file, dtype="float32") # 这里均值是 1308338, 0.8中位数是1730351,所以作者采用了中位数的部分
audio = sound_untils.crop_or_pad(audio, length=orig_sr) # 作者的想法是把audio做一个整体输入,在这里所有的都做了输入
image = sound_untils.audio_to_image(audio, sr, n_mels, fmin, fmax)
sound_features.append(image)
label = target_classes.index(classes)
labels.append(label)
sound_features = np.array(sound_features)
print(sound_features.shape) #(11965, 128, 32)
print(len(labels)) #(11965, 128, 32)
最终打印结果如下:
(11965,128,32)
11965
可以看到,根据作者设定的参数,特定路径指定的语音被转换成一个固定大小的矩阵,这也是根据前面超参数的设定而计算出的一个特定矩阵。有兴趣的读者可以将其打印出来并观察其内容。
2.3.3 模型的设计
对于深度学习而言,模型的设计是非常重要的步骤,由于本节的实战案例只是用于演示,因此采用了最简单的判别模型,实现代码如下(仅供读者演示,详细的内容在后续章节中介绍):
【程序2-2】
#这里使用ResNet作为特征提取模型,仅供读者演示,详细的内容在后续章节中介绍
import torch
class ResNet(torch.nn.Module):
def __init__(self,inchannels = 32):
super(ResNet, self).__init__()
#定义初始化神经网络层
self.cnn_1 = torch.nn.Conv1d(inchannels,inchannels*2,3,padding=1)
self.batch_norm = torch.nn.BatchNorm1d(inchannels*2)
self.cnn_2 = torch.nn.Conv1d(inchannels*2,inchannels,3,padding=1)
self.logits = torch.nn.Linear(128 * 32,5)
def forward(self,x):
#使用初始化定义的神经网络计算层进行计算
y = self.cnn_1((x.permute(0, 2, 1)))
y = self.batch_norm(y)
y = y.permute(0, 2, 1)
y = torch.nn.ReLU()(y)
y = self.cnn_2((y.permute(0, 2, 1))).permute(0, 2, 1)
output = x + y
output = torch.nn.Flatten()(output)
logits = self.logits(output)
return logits
if __name__ == '__main__':
image = torch.randn(size=(3,128,32))
ResNet()(image)
上面代码中的ResNet类继承自torch中的nn.Module类,目的是创建一个可以运行的深度学习模型,并在forward函数中通过神经网络进行计算,最终将计算结果作为返回值返回。
2.3.4 模型的数据输入方法
接下来设定模型的数据输入方法。深度学习模型的每一步都需要数据内容的输入,但是,一般情况下,由于计算硬件——显存的大小有限制,因此在输入数据时需要分步骤一块一块地将数据输入训练模型中。此处数据输入的实现代码如下:
【程序2-3】
#创建基于PyTorch的数据读取格式
import torch
class SoundDataset(torch.utils.data.Dataset):
#初始化数据读取地址
def __init__(self, sound_features = sound_features,labels = labels):
self.sound_features = sound_features
self.labels = labels
def __len__(self):
return len(self.labels) #获取完整的数据集长度
def __getitem__(self, idx):
#对数据进行读取,在模板中每次读取一个序号指向的数据内容
image = self.sound_features[idx]
image = torch.tensor(image).float() #对读取的数据进行类型转换
label = self.labels[idx]
label = torch.tensor(label).long() #对读取的数据进行类型转换
return image, label
在上面代码中,首先根据传入的数据在初始化时生成供训练使用的训练数据和对应的标签,之后的getitem函数建立了一个“传送带”,目的是源源不断地将待训练数据传递给训练模型,从而完成模型的训练。
2.3.5 模型的训练
对模型进行训练时,需要定义模型的一些训练参数,如优化器、损失函数、准确率以及训练的循环次数等。模型训练的实现代码如下:
import torch
from torch.utils.data import DataLoader, Dataset
from tqdm import tqdm
device = "cuda"
from sound_model import ResNet
sound_model = ResNet().to(device)
BATCH_SIZE = 32
LEARNING_RATE = 2e-5
import get_data
#导入数据集
train_dataset = get_data.SoundDataset()
#以PyTorch生成数据模板进行数据的读取和输出操作
train_loader = (DataLoader(train_dataset, batch_size=BATCH_SIZE,shuffle=True,num_workers=0))
#PyTorch中的优化器
optimizer = torch.optim.AdamW(sound_model.parameters(), lr = LEARNING_RATE)
#对学习率进行修正的函数
lr_scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer,T_max = 1600,eta_min=LEARNING_RATE/20,last_epoch=-1)
#定义损失函数
criterion = torch.nn.CrossEntropyLoss()
for epoch in range(9):
pbar = tqdm(train_loader,total=len(train_loader))
train_loss = 0.
for token_inp,token_tgt in pbar:
#将数据传入硬件中
token_inp = token_inp.to(device)
token_tgt = token_tgt.to(device)
#采用模型进行计算
logits = sound_model(token_inp)
#使用损失函数计算差值
loss = criterion(logits, token_tgt)
#计算梯度
optimizer.zero_grad()
loss.backward()
optimizer.step()
lr_scheduler.step() # 执行优化器
train_accuracy = ((torch.argmax(torch.nn.Softmax(dim=-1)(logits), dim=-1) == (token_tgt)).type(torch.float).sum().item() / len(token_tgt))
pbar.set_description(
f"epoch:{epoch + 1}, train_loss:{loss.item():.4f},, train_accuracy:{train_accuracy:.2f}, lr:{lr_scheduler.get_last_lr()[0] * 1000:.5f}")
上面代码完成了一个可以运行并持续对结果进行输出的训练模型,首先初始化模型的实例,之后建立数据的传递通道,而优化函数和损失函数也可以通过显式定义完成。
本文节选自《PyTorch语音识别实战》,获出版社和作者授权发布。