小编这一段时间研究端到端的实现中文语音的识别,项目主体代码使用了https://github.com/SeanNaren/deepspeech.pytorch/
的方案,不同的是这个模型主要为英文设计,在中文识别上可能需要做出一些变化,不仅涉及到数据集语料库,还涉及到部分业务逻辑的修改,下面根据数据结构的变化为时间线详细说一下。
整理好了思路和模块之间耦合的细节,按照这个思路自己去实现一套语音识别项目也是可以的。
这个项目在github上面版本更新比较频繁,优化的地方也有很多,2020年初到现在配置方式从传统的
argparse.ArgumentParser()
变成了独立的配置模块,不得不说相当优雅了,松耦合使得代码的调试过程避免了很多bug。
说一下整体的思路吧:、
一般语音识别工作相当于是把“语音特征数据”和“文字、词”关联起来了,需要我们的模型认识每一个字,这些字组合起来还要像是一句话,第一种方案:声学模型+语言模型,声学模型做的关联是“特征数据”---“音素”,语言模型做的是“音素”---“句子”,语言模型让这些识别结果看起来更像人话;第二种方案:端到端,也就是“特征数据”---“一句话”,这一般需要编解码工具的辅助,本项目中涉及到的有greedyDecoder和beamDecoder,beamDecoder中可以加入语言模型的辅助,项目支持kenlm类型的语言模型(实际上他并不算是一种语言模型,他把N-gram语言模型进行了包装,只不过是添加了更强的搜索算法,可以提高项目的效率)
1、关于语音识别工作,数据始终是我们不能忽略的,这是我们设计模型结构和业务逻辑的重点。在语音识别的任务中,第一步就是提取特征,什么是提取特征呢?就是把原始的音频文件比如wav转换成矩阵数据,一般数据的size大小和音频的长度近乎成正比。常用的特征提取方法是:MFCC、FBANK、LPC,这个项目中使用的方法类似于fbank,具体的实现代码如下:
import librosa
import soundfile as sf
import numpy as np
def load_audio(path):
sound, sample_rate = sf.read(path, dtype='int16')
sound = sound.astype('float32') / 32767 # normalize audio
if len(sound.shape) > 1:
if sound.shape[1] == 1:
sound = sound.squeeze()
else:
sound = sound.mean(axis=1) # multiple channels, average
return sound
audio_path = 'wav音频路径'
y = load_audio(audio_path)
n_fft = int(16000*.02) # 16000是音频的频率,02是傅里叶变换的窗口
win_length = n_fft
hop_length = int(16000*.01)
# STFT
D = librosa.stft(y, n_fft=n_fft, hop_length=hop_length,
win_length=win_length, window='hamming')
spect, phase = librosa.magphase(D)
spect = np.log1p(spect)
spect 就是最后提取的数据特征了,可以直接作为一个item输入到model中。
我用一个3s的wav文件得到的数据size是(161,844),这里161是不会变的,更长的音频会使得844这个位置的维度变大,这和其他的特征提取方法可能不太一样,我们这里就使用161*1000来举栗子吧。
这里要提到的一点是一般训练的数据都会采用短音频控制在10s之内是最好,过长的音频会挑战机器的内存,有可能会爆掉。
2、看一下项目的配置参数,刚开始接触深度学习的项目,参数有很多,这个项目里面把参数分成了两种:训练参数、推理参数,其实我们并不知道这些参数是干嘛的,下面分析一下吧
训练参数:
(1)epochs,我们输入的数据集里面有很多条数据,一条(item)这里就是一个短音频对应的数据特征,一个epoch就相当于把所有的数据(按照一定的排列方式和采样方式)从模型里面训练过了一遍,有多少个epoch就过了多少遍,一般的可以设置成30,50,70,100,看数据量大小,也看模型自己收敛不收敛,这个可以在train的过程中判断,如果发现模型已经长时间没有准确率上的提高,就可以设置停止当前epoch的训练;
(2)seed,在训练的过程中数据的排列和生成涉及到很多随机的过程,如果先设置了seed,就会让每一次随机到的内容都一样,就会让每次训练的结果都一样,可以根据自己的需要选择,他真的影响的代码是下面这一句:
# Set seeds for determinism
torch.manual_seed(cfg.training.seed)
torch.cuda.manual_seed_all(cfg.training.seed)
np.random.seed(cfg.training.seed)
random.seed(cfg.training.seed)
这些在训练的最开始就要设置好
(3)batch_size模型的训练是在一个循环中,代码大概是下面这样的
for epoch in range(epoch):
for batch_data in train_dataset:
output = model(batch_data)
看代码的时候可以先抓住这几句重要的逻辑,其他的代码多数是在处理数据的格式和形状,在模型应用(也可以叫推理)的过程中一般是一个item为单位进行预测的,但是训练的过程中并不是(这么多数据要循环到什么时候去),这里输入数据的形状是batch_size(这里选16吧,比较常见,太大也会爆炸)×1×161×1000,这里的第二维度加了1(一般的彩色图像是三维的,也就是三层二维矩阵,相当于一层图像数据,因为后面要做CNN卷积,需要一个平面的多通道的数据)
(4)train_manifest val_manifest
这些叫做清单文件,也可以说成数据集的一种形式,是csv文件,每一行有两项数据,第一个是wav文件的地址,第二项是对应语音内容的地址一般为txt或者trn格式
(5)window_size window_stride
这里说的是我们1、中提到的语音特征提取步骤的窗口大小,按照默认的.02就挺好,一般在数据处理的时候窗口尺寸和窗口重叠是成对出现的,因为两个窗口关联性也就是重叠的区域大小一般可以为窗口建立关联性,可以提高数据分析的连续性,避免窗口边缘的数据和下一个窗口的数据产生关联,而我们设置的窗口割裂了他们的关系。
(6)no_cuda,不适用cuda也就是GPU啦,这一般是False,GPU又好又快,有条件的还是用一下了,不过在数据输入到模型之前会需要把数据和定义好的model也转化成gpu类型的,比如在torch里面这样:
device = torch.device("cuda")
inputs = inputs.to(device)
model = model.to(device)
(7)hidden_layers,hidden_size,这两个参数都是跟模型的形状相关的,不过不影响,不直接影响我们的数据,设置得合理一点就可以了,先介绍一下模型的内部构造吧:
CNN(卷积层)--RNN(lstm循环神经网络)--LINEAR(全连接层,也叫做线性层)--SOFTMAX
我们这个hidden_size出现在RNN和LINEAR之间,最后会被乘积的操作消掉,这里设置的1024,我觉得还可以。hideen_layer指的是RNN有几层,一般层数多一点5,8,10会让RNN的表现更好。
(8)learning_anneal在训练的过程中,每一个epoch都会调整自己的参数以及学习率,预测结果和目标结果(也就是label)发生同样的偏差的时候,学习率越高,网络参数的调整幅度就越大。这个是在模型发现自己矫枉过正的时候,就会衰减一下自己的学习率
新的学习率=旧的学习率/learning_anneal
lr = lr/ learning_anneal
(9)optim优化器的类型,是可以自己选的,我这里选择了Adam,优化器综合学习率、网络模型参数、反向传播(修正网络模型参数的方法)权重衰减的数据对网络模型的参数进行优化
优化器等的使用过程一般是这样的(pytorch中)
# 训练前
optim = torch.optim.Adam(model.paramters(), lr=lr)
# 训练中 epoch内
output = model(input)
loss = f(output, target)
optim.zero_grad()
loss.backward()
optim.step()
(10)损失函数的定义,CTCLoss
顺便说一下损失函数,这里也涉及到数据尺寸的分析。损失函数使用了CTCLoss,在将训练数据输入到模型中时,仅把label数据(也就是文本内容的变体)处理到字符的index形式即可,同时,每一个句子文本的长度都不一样,所以一个batch中的数据shape可能不一致,比如在deepspeech项目train.py中,下面这个具体的情况:
inputs = inputs.to(device)
out, output_sizes = model(inputs, input_sizes)
out = out.transpose(0, 1) # TxNxH
float_out = out.float() # ensure float32 for loss
loss = criterion(float_out, targets, output_sizes, target_sizes).to(device)
loss = loss / inputs.size(0) # average the loss by minibatch
float_out: torch.Size([426, 16, 8679]), '426'不定,每句话都不一样
targets: torch.Size([528]), '528'不定,每句话都不一样
训练数据的targets并没有转化成[0,0,0,1,0,0,0...,0]这样的形式,仍然是字符索引的int
targets是把一个batch的索引都拼在了一起,形成一个长的List
targets格式形如: [s1c1,s1c2,s1c3,...,s1c10,s2c1,s2c2,...,s16c13],s是句子c是句子中字符
在数据shape不同的情况下直接交给 CTCLoss() 损失函数处理
这篇先分析到这里,下一篇继续吧❥(^_-)