语音识别
-
- 1、概述
- 2、与传统语音识别的对比
- 3、下载并分析数据集
- 4、读取样本
- 5、梅尔频率倒谱系数(MFCC)
- 6、倒谱分析(Cepstrum Analysis)
- 7、 梅尔频率分析(Mel-Frequency Analysis)
- 8、梅尔频率倒谱系数(MFCC)
- 9、 提取音频数据的MFCC特征
- 10、文字样本转化成向量
- 11、将音频数据转为MFCC,将译文转为向量
- 12、批次音频数据对齐
- 13、创建序列的稀疏表示
- 14、将字向量转成文字
- 15、next_batch函数
- 16、Bi-RNN网络
- 17、CTC网络
- 18、稀疏矩阵
- 19、levenshtein距离
- 20、CTC decoder
- 21、定义占位符
- 22、构建网络模型
- 23、定义损失函数和优化器
- 24、使用CTC decoder和计算编辑距离
- 25、建立session
- 26、完整代码
1、概述
本人从事语音方面的开发工作,通过音频和代码实战tensorflow是最直接有效的学习方式,先从简单的语音识别和tensorflow代码开始来了解这个体系
2、与传统语音识别的对比
传统的语音识别是基于语音学的方法,通常包含拼写、声学和语音模型等单独组件。训练模型的语料除了标注具体的文字外。还要标注按时间对应的音素,这就需要大量的人工成本。(标记因素是个很大的坑)而使用神经网络的语音识别就变得简单多了,通过能进行时序分类的连续时间分类目标函数(CTC),计算多个标签序列的概率,而序列是语音样本中所有可能的对应文字的集合。然后把预测结果跟实际比较,计算误差,不断更新网络权重。这样就丢弃音素的概念,就省了大量人工标注的成本,也不需要语言模型,只要有足够的样本,就可以训练各种语言的语音识别了。
3、下载并分析数据集
数据下载地址
下载并解压数据文件后如图所示:
data_thchs30文件夹包含的是语音数据和其翻译,我们来看看文件夹里的内容:
data文件夹下,“.wav”文件保存的是音频文件,”.wav.trn”保存的是翻译文件,然后,train/dev/test文件夹下的文件是将data文件夹下的文件分割过来的,这3个文件夹具体哪个文件夹分了多少文件,下面有个表显示了。这里有个词“symlinks”,链接?难道都是链接文件?不可能吧?去看看再说,打开data文件夹,内容如下图所示,
打开README.TXT
.wav文件是一个音频文件就不做过多描述了,这里我们打开一个.trn文件
文件的内容一共有三行
- 第一行是音频读取的文字
- 第二行是拼音+音调(中文抑扬顿挫的四个声调,用1234表示)
- 第三行是音素+音调(就是把拼音给分开了)
train文件夹跟data文件夹下的文件名一样,只不过这里总共只有20000个文件,而data文件夹下有26776个文件,可以猜测另外的6776个文件应该是放到dev和test文件夹下了。
4、读取样本
训练的话,我们就用train文件夹下的数据来训练,音频文件可以直接使用train文件夹下的,翻译的话,就得用data文件夹下的了,音频文件是**.wav,对应的翻译文件则是**.wav.trn
所以我们先找出所有的train文件夹下的音频文件,再找data文件夹下音频文件名+”.trn”后缀的文件就是翻译文件,取翻译文件的第一行,就是翻译内容了,将音频文件和翻译的内容一一对应,加载到内存中。
先来实现获取指定文件夹下所有WAV文件的函数
#encoding:utf-8
import os
#获取文件夹下所有的WAV文件
def get_wav_files(wav_path):
wav_files = []
for (dirpath, dirnames, filenames) in os.walk(wav_path):
for filename in filenames:
if filename.endswith('.wav') or filename.endswith('.WAV'):
# print(filename)
filename_path = os.path.join(dirpath, filename)
# print(filename_path)
wav_files.append(filename_path)
return wav_files
根据上面获取的WAV文件,获取其指定文件夹下对应的翻译文件里的第一行,即翻译文字
#获取wav文件对应的翻译文字
def get_tran_texts(wav_files, tran_path):
tran_texts = []
for wav_file in wav_files:
(wav_path, wav_filename) = os.path.split(wav_file)
tran_file = os.path.join(tran_path, wav_filename + '.trn')
# print(tran_file)
if os.path.exists(tran_file) is False:
return None
fd = open(tran_file,encoding='gb18030', errors='ignore')
text = fd.readline()
tran_texts.append(text.split('\n')[0])
fd.close()
return tran_texts
将上面两个函数整合成一个函数
#获取wav和对应的翻译文字
def get_wav_files_and_tran_texts(wav_path, tran_path):
wav_files = get_wav_files(wav_path)
tran_texts = get_tran_texts(wav_files, tran_path)
return wav_files, tran_texts
测试
wav_files, tran_texts = get_wav_files_and_tran_texts('data_thchs30/train', 'data_thchs30/data')
print(wav_files[0], tran_texts[0])
print(len(wav_files), len(tran_texts))
测试通过
5、梅尔频率倒谱系数(MFCC)
之前写过的MFCC
MFCC
这里重新复习一遍:
声谱图(Spectrogram)
如上图所示,一段语音被分成很多帧,每帧经过一个快速傅里叶变换(FFT)得到一个频谱,频谱反映的是信号频率与能量的关系。在实际应用中,一般有三种频谱图:线性振幅谱、对数振幅谱、自功率谱。对数振幅谱对各谱线的振幅都做了对数计算,其目的是使振幅较低的成份相对振幅较高的成份得以拉高,以便观察掩盖在低振幅噪声中的周期信号,所以其纵坐标的单位是分贝(dB)。
如上图所示,我们先将语音信号的某一帧频谱用坐标表示,注意:此时横轴已经是频率了,纵轴是振幅,然后将坐标旋转90度,得到如下图所示,
接着,将振幅映射到一个灰度水平线,其值为0-255,0表示黑,255表示白,振幅越大,对应的区域越黑,如下图所示,
这样就增加了时间的维度,就可以显示一段语音而不是一帧语音的频谱
我们就会得到一个随时间变化的频谱图,这个就是描述语音信号的声谱图,如下图所示。
如上图所示,很黑的地方就是频谱图中的峰值(共振峰)。为什么要这样搞呢?因为在声谱图中能更好的观察音素和它的特征。
另外,通过观察共振峰和它们的跃迁可以更好地识别声音。
隐马尔科夫模型(Hidden Markov Models)就是隐含地对声谱图进行建模以达到好的识别性能。还有一个作用就是它可以直观的评估TTS系统(text to speech)的好坏,直接对比合成的语音和自然的语音声谱图的匹配度即可。
6、倒谱分析(Cepstrum Analysis)
上图是一个语音的频谱图,峰值表示语音的主要频率成分,称为共振峰,共振峰携带了声音的辨识属性(相当于人的身份证),用它就可以识别不同的声音,这个属性特别重要,所以我们要把它提取出来。
我们不仅要提取出共振峰的位置,还得提取它们的转变过程,也就是频谱的包络(Spectral Envelope)。这个包络就是一条连接这些共振峰的平滑曲线,如下图所示
我们可以理解为,原始频谱由包络和频谱的细节组成,如果我们将这两部分分离,就可以得到包络了,如下图所示
因为我们用的是对数频谱,所以都加上了log,单位是dB。如上图所示,我们要在已知的logX[k]的基础上求logH(k)和logE(k),使得logX[k]=logH(k)+logE(k)。
为了将它们分离,我们得使用一个数学技巧,这个技巧就是对频谱做FFT,在频谱上做傅里叶变换,就相当于逆傅里叶变换(IFFT)。因为我们是在频谱的对数上处理的,在对数频谱上做IFFT就相当于在一个伪频率坐标上描述信号。
首先,画出伪频率坐标,如下图所示
伪频率坐标上分为低频率区域和高频率区域,通过IFFT将包络和频谱细节转换到伪频率坐标上
首先,将包络当成是一个每秒4个周期的正弦波,这样在伪频率坐标轴上给出一个4Hz的峰值。
同理,将频谱细节看成一个每秒100个周期的正弦波,这样在伪频率坐标轴上给出一个100Hz的峰值。
把它俩叠加在一起,就是原始频谱信号了
由上述可知,h[k]是x[k]的低频部分,而logX[k]是已知的,所以x[k]也是已知的,所以将x[k]通过一个低通滤波器就可以得到h[k]了,也就是频谱的包络。
x[k]称为倒谱,h[k]就是倒谱的低频部分,h[k]描述了频谱的包络,包络在语音识别中被广泛用于描述特征。
总结一下上述过程就是:
- 先将原始语音信号经过傅里叶变换得到频谱:X[k]=H[k]E[k] 只考虑幅度则是:||X[k]||=||H[k]|| ||E[k]||
- 对上式两边取对数得:log||X[k]||=log||H[k]|| + log||E[k]||
- 再对上式两边取逆傅里叶变换得到倒谱:x[k]=h[k]+e[k]
7、 梅尔频率分析(Mel-Frequency Analysis)
通过上面的步骤,我们可以得到一段语音的频谱包络,但是,对于人类听觉感知的实验表明,人类听觉的感知只聚焦在某些特定的区域,而不是整个频谱包络。
梅尔频率分析就是基于人类听觉感知实验的,实验观测发现人耳就像一个滤波器组,它只关注某些特定的频率分量。但是这些滤波器在频率坐标轴上却不是统一分布的,在低频区域有很多的滤波器,它们分布比较密集,在高频区域,分布的比较稀疏,如下图所示
8、梅尔频率倒谱系数(MFCC)
MFCC考虑了人类听觉特征,先将线性频谱映射到基于听觉感知的梅尔非线性频谱中,然后再转到倒谱上。
将普通频率转换到梅尔频率的公式如下:
在梅尔频域内,人对音调的感知度为线性关系。比如,两端语音信号的梅尔频率相差两倍,人耳听起来两者的音调也是相差两倍。
我们将频谱通过一组梅尔滤波器得到梅尔频谱,公式表达为:logX[k]=log(Mel-Spectrum)。然后,再在logX[k]上进行倒谱分析,
logX[k]=logH[k] + logE[k]
然后,进行IFFT变换,得,
x[k]=h[k]+e[k]
在梅尔频谱上得到的倒谱系数h[k]就是我们要说的梅尔频谱倒谱系数,简称MFCC。
提取MFCC的大致过程如上图所示。
- 先对语音进行预减轻、分帧和加窗;
- 对每个短时分析窗,通过FFT失掉对应的频谱;
- 将上面的频谱通过Mel滤波器组失掉Mel频谱;
- 在Mel频谱上面进行倒谱分析(取对数,做逆变换,现实逆变换一般是通过DCT离散余弦变换来实现,取DCT后的第2个到第13个系数作为MFCC系数),取得Mel频率倒谱系数MFCC,这个MFCC就是这帧语音的特征;
到这里,语音信号就能通过一系列倒谱向量来描述了,每个向量就是每帧的MFCC特征向量。
注:上述MFCC知识点参考自博客:https://blog.csdn.net/zouxy09/article/details/9156785/
文档:http://www.speech.cs.cmu.edu/15-492/slides/03_mfcc.pdf
9、 提取音频数据的MFCC特征
首先来安装python_speech_features工具,执行以下命令行即可
pip install python_speech_features
我们将语音数据转换为需要计算的13位或26位不同的倒谱特征的MFCC,将它作为模型的输入。经过转换,数据将会被存储在一个频率特征系数(行)和时间(列)的矩阵中。
因为声音不会孤立的产生,并且没有一对一映射到字符,所以,我们可以通过在当前时间索引之前和之后捕获声音的重叠窗口上训练网络,从而捕获共同作用的影响(即通过影响一个声音影响另一个发音)。
这里先插讲一下语音中的“分帧”和“加窗”的概念
分帧
如上图所示,傅里叶变换要求输入的信号是平稳的,但是语音信号在宏观上是不平稳的,在微观上却有短时平稳性(10-30ms内可以认为语音信号近似不变)。所以要把语音信号分为一些小段处理,每一个小段称为一帧。
加窗
取出一帧信号以后,在进行傅里叶变换前,还有先进行“加窗”操作,“加窗”其实就是乘以一个“窗函数”,如下图所示
加窗的目的是让一帧信号的幅度在两端渐变到0,这样就可以提供变换结果的分辨率。但是加窗也是有代价的,一帧信号的两端被削弱了,弥补的办法就是,邻近的帧直接要有重叠,而不是直接截取,如下图所示,
如上图所示,两帧之间有重叠部分,帧长为25ms,两帧起点位置的时间差叫帧移,一般取10ms或者帧长的一半
对于RNN,我们使用之前的9个时间片段和后面的9个时间片段,加上当前时间片段,每个加载窗口总共包括19个时间片段。当梅尔倒谱系数为26时,每个时间片段总共就有494个MFCC特征数。下图是以倒谱系数为13为例的加载窗口实例图
而当当前序列前或后不够9个序列时,比如第2个序列,这时就需要进行补0操作,将它凑够9个。最后,再进行标准化处理,减去均值,然后除以方差。
#将音频信息转成MFCC特征
#参数说明---audio_filename:音频文件 numcep:梅尔倒谱系数个数
# numcontext:对于每个时间段,要包含的上下文样本个数
def audiofile_to_input_vector(audio_filename, numcep, numcontext):
# 加载音频文件
fs, audio = wav.read(audio_filename)
# 获取MFCC系数
orig_inputs = mfcc(audio, samplerate=fs, numcep=numcep)
#打印MFCC系数的形状,得到比如(980, 26)的形状
#955表示时间序列,26表示每个序列的MFCC的特征值为26个
#这个形状因文件而异,不同文件可能有不同长度的时间序列,但是,每个序列的特征值数量都是一样的
print(np.shape(orig_inputs))
# 因为我们使用双向循环神经网络来训练,它的输出包含正、反向的结
# 果,相当于每一个时间序列都扩大了一倍,所以
# 为了保证总时序不变,使用orig_inputs =
# orig_inputs[::2]对orig_inputs每隔一行进行一次
# 取样。这样被忽略的那个序列可以用后文中反向
# RNN生成的输出来代替,维持了总的序列长度。
orig_inputs = orig_inputs[::2]#(490, 26)
print(np.shape(orig_inputs))
#因为我们讲解和实际使用的numcontext=9,所以下面的备注我都以numcontext=9来讲解
#这里装的就是我们要返回的数据,因为同时要考虑前9个和后9个时间序列,
#所以每个时间序列组合了19*26=494个MFCC特征数
train_inputs = np.array([], np.float32)
train_inputs.resize((orig_inputs.shape[0], numcep + 2 * numcep * numcontext))
print(np.shape(train_inputs))#)(490, 494)
# Prepare pre-fix post fix context
empty_mfcc = np.array([])
empty_mfcc.resize((numcep))
# Prepare train_inputs with past and future contexts
#time_slices保存的是时间切片,也就是有多少个时间序列
time_slices = range(train_inputs.shape[0])
#context_past_min和context_future_max用来计算哪些序列需要补零
context_past_min = time_slices[0] + numcontext
context_future_max = time_slices[-1] - numcontext
#开始遍历所有序列
for time_slice in time_slices:
#对前9个时间序列的MFCC特征补0,不需要补零的,则直接获取前9个时间序列的特征
need_empty_past = max(0, (context_past_min - time_slice))
empty_source_past = list(empty_mfcc for empty_slots in range(need_empty_past))
data_source_past = orig_inputs[max(0, time_slice - numcontext):time_slice]
assert(len(empty_source_past) + len(data_source_past) == numcontext)
#对后9个时间序列的MFCC特征补0,不需要补零的,则直接获取后9个时间序列的特征
need_empty_future = max(0, (time_slice - context_future_max))
empty_source_future = list(empty_mfcc for empty_slots in range(need_empty_future))
data_source_future = orig_inputs[time_slice + 1:time_slice + numcontext + 1]
assert(len(empty_source_future) + len(data_source_future) == numcontext)
#前9个时间序列的特征
if need_empty_past:
past = np.concatenate((empty_source_past, data_source_past))
else:
past = data_source_past
#后9个时间序列的特征
if need_empty_future:
future = np.concatenate((data_source_future, empty_source_future))
else:
future = data_source_future
#将前9个时间序列和当前时间序列以及后9个时间序列组合
past = np.reshape(past, numcontext * numcep)
now = orig_inputs[time_slice]
future = np.reshape(future, numcontext * numcep)
train_inputs[time_slice] = np.concatenate((past, now, future))
assert(len(train_inputs[time_slice]) == numcep + 2 * numcep * numcontext)
# 将数据使用正太分布标准化,减去均值然后再除以方差
train_inputs = (train_inputs - np.mean(train_inputs)) / np.std(train_inputs)
return train_inputs
10、文字样本转化成向量
对于文字样本,则需要将文字转换成具体的向量,代码如下
#将字符转成向量,其实就是根据字找到字在word_num_map中所应对的下标
def get_ch_lable_v(txt_file,word_num_map,txt_label=None):
words_size = len(word_num_map)
to_num = lambda word: word_num_map.get(word, words_size)
if txt_file!= None:
txt_label = get_ch_lable(txt_file)
print(txt_label)
labels_vector = list(map(to_num, txt_label))
print(labels_vector)
return labels_vector
我们调用get_wav_files_and_tran_texts函数获取了所有的WAV文件和其对应的翻译文字。现在,我们先来处理一下翻译的文字,先将所有文字提出来,然后,调用collections和Counter方法,统计一下每个字符出现的次数,然后,把它们放到字典里面去
# 字表
all_words = []
for label in labels:
#print(label)
all_words += [word for word in label]
#Counter,返回一个Counter对象集合,以元素为key,元素出现的个数为value
counter = Counter(all_words)
#排序
words = sorted(counter)
words_size= len(words)
word_num_map = dict(zip(words, range(words_size)))
print(word_num_map)
11、将音频数据转为MFCC,将译文转为向量
现在,整合上面两个函数,将音频数据转为时间序列(列)和MFCC(行)的矩阵,将对应的译文转成字向量,代码如下
#将音频数据转为时间序列(列)和MFCC(行)的矩阵,将对应的译文转成字向量
def get_audio_and_transcriptch(txt_files, wav_files, n_input, n_context,word_num_map,txt_labels=None):
audio = []
audio_len = []
transcript = []
transcript_len = []
if txt_files!=None:
txt_labels = txt_files
for txt_obj, wav_file in zip(txt_labels, wav_files):
# load audio and convert to features
audio_data = audiofile_to_input_vector(wav_file, n_input, n_context)
audio_data = audio_data.astype('float32')
# print(word_num_map)
audio.append(audio_data)
audio_len