【原理+实战】AI所有领域SOTA综述 (一)语音识别

文章目录

  • 前言
  • 语音识别原理
    • 信号处理,声学特征提取
    • 识别字符,组成文本
    • 声学模型
    • 语言模型
    • 词汇模型
  • 语音声学特征提取:MFCC和LogFBank算法的原理
  • 实战一 ASR语音识别模型
      • 系统的流程
      • 基于HTTP协议的API接口
      • 客户端
      • 未来
  • 实战二 调百度和科大讯飞API
  • 实战三 离线语音识别 Vosk

前言

首先,cv君下血本费时整理了AI在音视频领域的大量的方向,形成本文综述,从原理到底层算法,到上层应用,统统透析~本系列由于综述文章过长的原因,所以分开写了。文章附带大量的算法原理+代码实现教学,欢迎关注,一起AI。
【原理+实战】AI所有领域SOTA综述 (一)语音识别_第1张图片

语音识别原理

【原理+实战】AI所有领域SOTA综述 (一)语音识别_第2张图片

首先是语音识别和语音唤醒等任务。一听到你就会想起科大讯飞,中国百度等平台,由于
这两家企业在中国语音领域占用80+市场,所以他们做得很优秀,不过由于高精技术无法开源,其他企业只得花费大量的金钱去购买其API,而无法研究语音识别等应用,导致民间语音识别发展较慢,今天我们来一饱眼福吧!

信号处理,声学特征提取

我们都知道声音信号是连续的模拟信号,要让计算机处理首先要转换成离散的数字信号,进行采样处理。正常人听觉的频率范围大约在20Hz~20KHz之间,为了保证音频不失真影响识别,同时数据又不会太大,通常的采样率为16KHz。
语音采样

在数字化的过程中,我们首先要判断端头,确定语音的开始和结束,然后要进行降噪和过滤处理(除了人声之外,存在很多的噪音),保证让计算机识别的是过滤后的语音信息。获得了离散的数字信号之后,为了进一步的处理我们还需要对音频信号 分帧。因为离散的信号单独计算数据量太大了,按点去处理容易出现毛刺,同时从微观上来看一段时间内人的语音信号一般是比较平稳的,称为 短时平稳性,所以会需要将语音信号分帧,便于处理。

我们的每一个发音,称为一个 音素,是语音中的最小单位,
比如普通话发音中的元音,辅音。不同的发音变化是由于人口腔肌肉的变化导致的,
这种口腔肌肉运动相对于语音频率来说是非常缓慢的,所以我们为了保证信号的短时平稳性,
分帧的长度应当小于一个音素的长度,当然也不能太小否则分帧没有意义。

通常一帧为20~50毫秒,同时帧与帧之间有交叠冗余,避免一帧的信号在两个端头被削弱了影响识别精度。常见的比如 帧长为25毫秒,两帧之间交叠15毫秒,也就是说每隔25-15=10毫秒取一帧,帧移为10毫秒,分帧完成之后,信号处理部分算是完结了。

随后进行的就是整个过程中极为关键的特征提取。将原始波形进行识别并不能取得很好的识别效果,而需要进行频域变换后提取的特征参数用于识别。常见的一种变换方法是提取MFCC特征,根据人耳的生理特性,把每一帧波形变成一个多维向量,可以简单地理解为这个向量包含了这帧语音的内容信息。

实际应用中,这一步有很多细节,声学特征也不止有MFCC这一种,具体这里不讲,但是各种特征提取方法的核心目的都是统一的:尽量描述语音的根本特征,尽量对数据进行压缩。

比如下图示例中,每一帧f1,f2,f3…转换为了14维的特征向量,然后整个语音转换为了14*N(N为帧数)的向量矩阵。
【原理+实战】AI所有领域SOTA综述 (一)语音识别_第3张图片

一帧一帧的向量如果不太直观,还可以用下图的频谱图表示语音,每一列从左到右都是一个25毫秒的块,相比于原始声波,从这种数据中寻找规律要容易得多。
【原理+实战】AI所有领域SOTA综述 (一)语音识别_第4张图片

不过频谱图主要用作语音研究,语音识别还是需要用一帧一帧的特征向量。

识别字符,组成文本

特征提取完成之后,就进入了特征识别,字符生成环节。这部分的核心工作就是从 每一帧当中找出当前说的音素,再由多个音素组成单词,再由单词组成文本句子。 其中最难的当然是从每一帧中找出当前说的音素,因为我们每一帧是小于一个音素的,多个帧才能构成一个音素,如果最开始就错了则后续很难纠正。

怎么判断每一个帧属于哪个音素了?最容易实现的办法就是概率,看哪个音素的概率最大,则这个帧就属于哪个音素。那如果每一帧有多个音素的概率相同怎么办,毕竟这是可能的,每个人口音、语速、语气都不同,人也很难听清楚你说的到底是Hello还是Hallo。而我们语音识别的文本结果只有一个,不可能还让人参与选择进行纠正。

这时候多个音素组成单词的统计决策,单词组成文本的统计决策就发挥了作用,它们也是同样的基于概率:音素概率相同的情况下,再比较组成单词的概率,单词组成之后再比较句子的概率。
【原理+实战】AI所有领域SOTA综述 (一)语音识别_第5张图片

比如以上那个词很有可能是「HHHEE_LL_LLLOOO」。但它同时认为我说的也可能是「HHHUU_LL_LLLOOO」,或者甚至是「AAAUU_LL_LLLOOO」。我们可以遵循一些步骤来整理这个输出。首先,我们将用单个字符替换任何重复的字符:

· HHHEE_LL_LLLOOO 变为 HE_L_LO
· HHHUU_LL_LLLOOO 变为 HU_L_LO
· AAAUU_LL_LLLOOO 变为 AU_L_LO

然后,我们将删除所有空白:

· HE_L_LO 变为 HELLO
· HU_L_LO 变为 HULLO
· AU_L_LO 变为 AULLO

这让我们得到三种可能的转写——「Hello」、「Hullo」和「Aullo」,最终根据单词概率我们会发现Hello是最可能的,所以输出Hello的文本。上面的例子很明确的描述怎么从帧到音素,再从音素到单词,概率决定一切,那这些概率是怎么获得的了?难道为了识别一种语言我们把人类几千上百年说过的所有音素,单词,句子都统计出来,然后再计算概率?傻子都知道这是不可能的,那怎么办,这时我们就需要模型:

声学模型

发声的基本音素状态和概率,尽量获得不同人、不同年纪、性别、口音、语速的发声语料,同时尽量采集多种场景安静的,嘈杂的,远距离的发声语料生成声学模型。为了达到更好的效果,针对不同的语言,不同的方言会用不同的声学模型,在提高精度的同时降低计算量。

语言模型

单词和语句的概率,使用大量的文本训练出来。如果模型中只有两句话“今天星期一”和“明天星期二”,那我们就只能识别出这两句,而我们想要识别更多,只需要涵盖足够的语料就行,不过随之而来的就是模型增大,计算量增大。所以我们实际应用中的模型通常是限定应用域的,同比如智能家居的,导航的,智能音箱的,个人助理的,医疗的等等,降低计算量的同时还能提高精度,

词汇模型

针对语言模型的补充,语言词典和不同的发音标注。比如定期更新的地名,人名,歌曲名称,热词,某些领域的特殊词汇等等。

语言模型和声学模型可以说是语音识别中最重要的两个部分,语音识别中一个很重要的工作就是训练模型,有不识别的句子我们就加进去重新训练。不过我们在训练和计算概率时会发现一个问题,假设某条句子S出现的概率为P(S),其中单词序列为W1,W2,W3 …, Wn

P(S) = P(W1,W2,W3 …, Wn) 展开为每个词出现的条件概率相乘
= P(W1)·P(W2|W1)·P(W3|W1,W2)···P(Wn|W1,W2,W3 …, Wn-1)

从计算上看第一个词的条件概率P(W1)很好计算,第二个词P(W2|W1)在已知第一个词的情况下,还不太麻烦,第三个词开始变得很难了,因为涉及到三个变量W1,W2,W3,每一个词都可能是一种语言字典的大小,到了Wn基本无法估计了,计算量太大了。

这时我们有很多简化但是有效的方法进行计算,比如说HMM隐马尔科夫模型Hidden Markov Model。

隐马尔科夫模型基于了两个最大的假设:一是内部状态的转移只与
上一状态有关,另一是输出值只与当前状态(或当前的状态转移)有关。就把问题简化了,

也就是说一个句子中某个单词序列出现的概率只和前面的一个单词有关,这样计算量就被大大简化了。

P(S) = P(W1)·P(W2|W1)·P(W3|W2)···P(Wn|Wn-1)

如上图示例,基于隐马尔科夫算法生成语言模型。我们只要按照实际要求构造出对应的模型,模型中涵盖足够的语料,就能解决各种语音识别问题。

语音识别过程其实就是在模型的状态网络中搜索一条最佳路径,语音对应这条路径的概率最大,这称之为“解码”。路径搜索的算法是一种动态规划剪枝的算法,称之为Viterbi算法,用于寻找全局最优路径。

如此一来整个语音识别的流程就很清晰了,再来回顾以下整个步骤:

信号处理:模数转换,识别端头,降噪等等。信号表征:信号分帧,特征提取,向量化等等。
模式识别:寻找最优概率路径,声学模型识别音素,词汇模型和语言模型识别单词和句子。

【原理+实战】AI所有领域SOTA综述 (一)语音识别_第6张图片最后将语音识别成文本。

语音声学特征提取:MFCC和LogFBank算法的原理

几乎任何做自动语音识别的系统,第一步就是对语音信号,进行特征的提取。通过提取语音信号的相关特征,有利于识别相关的语音信息,并丢弃携带的其他不相关的所有信息,如背景噪声、情绪等。
【原理+实战】AI所有领域SOTA综述 (一)语音识别_第7张图片

   我们都知道,人类说话是通过体内的发声器产生的初始声音,
   被包括舌头和牙齿在内的其他物体形成的声道的形状进行滤波,
   从而产生出各种各样的语音的。传统的语音特征提取算法正是基于这一点,
   通过一些数字信号处理算法,能够更准确地包含相关的特征,
   从而有助于后续的语音识别过程。常见的语音特征提取算法有MFCC、FBank、LogFBank等。

1 MFCC

MFCC的中文全称是“梅尔频率倒谱系数”,这种语音特征提取算法是这几十年来,最常用的算法之一。这种算法是通过在声音频率中,对非线性梅尔刻度的对数能量频谱,进行线性变换得到的[1]。

MFCC特征提取算法的主体流程如下:
MFCC算法流程图1 MFCC算法流程
1.1 分帧

由于存储在计算机硬盘中的原始wav音频文件是不定长的,
我们首先需要将其按一定方法切分为固定长度的多个小片段,也就是分帧。根据语音信号变化迅速的特性,每一帧的时间长度一般取10-30毫秒,以保证一帧内有足够多的周期,且变化不会过于剧烈,因此,更适合这种适用于分析平稳信号的傅里叶变换。由于数字音频的采样率不同,分帧所得的每一帧向量的维度也不同。

为了避免时间窗的边界导致信息遗漏的问题,因此,在对从信号中取每一帧的时间窗进行偏移的时候,帧和帧之间需要有一部分的重叠区域。这个时间窗的偏移量,我们一般取为帧长的一半,即每一步都偏移一帧的大约二分之一之后的位置,作为时间窗取下一帧的最终位置。这样做的好处是,避免了帧与帧之间的特性变化过大。

   通常来说,我们选取时间窗长度为25毫秒,时间窗的偏移量为10毫秒。

1.2 预加重

   由于声音信号从人的声门发出后,存在12dB/倍频程的衰减,在通过口唇辐射后,
   还存在6dB/倍频程的衰减,因而在进行快速傅里叶变换之后,
   高频信号部分中的成分较少。所以,对语音信号进行预加重操作,
   其主要目的是加强语音信号的每一帧中,那些高频部分的信号
   ,以提高其高频信号的分辨率。我们需要通过采用如下公式的一阶高通滤波器进行预加重操作:

H(z)=1−α×z−1(1)
S(n)=S(n)−α×S(n−1)∀n∈N(2)

在上式中,α是预加重的系数,其一般的取值范围是0.9 < α < 1.0,通常取0.97。n表示当前处理的是第n帧,其中,第一个n=0的帧需要特别处理。
1.3 加窗

   在之前的分帧过程中,直接将一个连续的语音信号切分为若干个片段,
   会造成截断效应产生的频谱泄漏,加窗操作的目的是消除每个帧的短时信号在其两
   端边缘处出现的信号不连续性问题。MFCC算法中,选取的窗函数通常是汉
   明窗,也可以使用矩形窗和汉宁窗。需要注意的是,预加重必须在加窗之前进行。

汉明窗的窗函数为:
W(n)=0.53836−0.46164×cos(2πnN−1)(0≤n≤N,n=0,1,2,…,N)(3)
加窗过程为:
S′(n)=W(n)×S(n)(4)
1.4 快速傅里叶变换

在经过上述的一系列的处理过程之后,我们得到的仍然是时域的信号,而时域中可直接获取的语音信息量较少。在进行进一步的语音信号特征提取时,还需要将每一帧的时域信号对应转换到其频域信号。对于存储在计算机上的语音信号,我们需要使用离散的傅里叶变换,由于普通的离散傅里叶变换的计算复杂度较高,通常使用快速傅里叶变换来实现。由于MFCC算法经过分帧之后,每一帧都是短时间内的时域信号,所以这一步也成为短时快速傅里叶变换。
P(n)=∑N−1k=0S(n)×e−j⋅2πknN(0

   根据奈奎斯特定理,如果要再次从离散的数字信号无损地转换到模拟信号上,
   在对模拟信号进行采样时,我们需要采用模拟信号最高频率值的2倍以上的采样率,
   对模拟信号进行模数转换的采样。对于语音识别常用的16kHz采样率音频,
   傅里叶变换之后的频率范围为0到8kHz之间。

1.5 计算幅度谱(对复数取模)

   在完成了快速傅里叶变换之后,得到的语音特征是一个复数矩阵,
   它是一个能量谱,由于能量谱中的相位谱包含的信息量极少,
   所以我们一般选择丢弃相位谱,而保留幅度谱。

   丢弃相位谱保留幅度谱的方法一般是两种,对每一个复数求绝对值或者求平方值。

P′(n)=P2(n)−−−√(6)
P′(n)=P2(n)(7)
1.6 Mel滤波
【原理+实战】AI所有领域SOTA综述 (一)语音识别_第8张图片

   Mel滤波的过程是MFCC和fBank特征的关键之一。
   Mel滤波器是由20个三角形带通滤波器组成的,将线性频率转换为非线性分布的Mel频率。

Mel滤波器原理图图2 Mel滤波器原理图

Mel倒谱公式:
Mel(f)=2595×log10(1+f700)=1125×ln(1+f700)(8)
梅尔滤波器:
Bm[k]=⎧⎩⎨⎪⎪⎪⎪⎪⎪0k−fm−1fm−fm−1fm+1−kfm+1−fmkfm+1fm−1≤k≤fmfm≤k≤fm+1(9)

Mel滤波公式:
Em=ln(∑N−1k=0P(k)×Hm(k))(10)

经过Mel滤波之后,Em即为得到的fBank特征。
1.7 取对数

   在得到上一步的fBank特征之后,由于人耳对声音的感受是成对数值增长的,
   所以需要将数值再进行一次对数运算,以模拟人耳的感受。
   我们需要对纵轴通过取对数进行缩放,可以放大低能量处的能量差异。

1.8 离散余弦变换

离散余弦变换是MFCC相对于fBank所特有的一步特征提取运算。在上一步取了对数之后,我们还需要对得到的N维特征向量值,再进行一次离散余弦变换(DCT)。做DCT的根本原因是,不同阶数信号值之间具有一定的相关性,而我们需要去掉这种相关性,将信号再映射到低维的空间中。由于最有效的特征聚集在前12个特征里,所以在实际中,一般仅保留前12-20个结果值,通常取13个,这样一来,就进一步压缩了数据。 离散余弦变换公式如下:
Ci=2N−−√∑Nj=1Ej×cos(π⋅iN⋅(j−0.5)),∀i∈1,M
1.9 计算动态特征

上述MFCC算法仅仅体现了MFCC的静态特征,而其动态特征还需要使用静态特征的差分来表示。通过将得到的动态的特征,和前一步得到的静态特征相结合,可以有效地提高这种语音识别系统的识别性能。 差分参数的计算公式:
dt=⎧⎩⎨⎪⎪⎪⎪⎪⎪Ct+1−Ct∑Kk=1k(Ct+k−Ct−k)2∑Kk=1k2√Ct−Ct−1t

式中,dt是第t个一阶差分值,Ct是第t个倒谱系数值,Q是倒谱系数的最大阶数,K是一阶差分的时间差,一般可取1或取2。二阶差分则将上式的结果再代入进行计算即可。

   最后,再将静态特征和动态特征的一阶、二阶差分值合并起来,
   当静态特征是13维的特征向量时,合并动态特征后,总共有39维特征。

2 logfBank

logfBank特征提取算法类似于MFCC算法,都是基于fBank特征提取结果的基础上,再进行一些处理的。不过logfBank跟MFCC算法的主要区别在于,是否再进行离散余弦变换。logfBank特征提取算法在跟上述步骤一样得到fBank特征之后,直接做对数变换作为最终的结果,计算量相对MFCC较小,且特征的相关性较高,所以传统的语音识别技术常常使用MFCC算法。

   随着DNN和CNN的出现,尤其是深度学习的发展
   ,由于fBank以及logfBank特征之间的相关性可以更好地被神经网络利用,
   以提高最终语音识别的准确率,降低WER,因此,可以省略掉离散余弦变换这一步骤。

3 总结

本文主要介绍了MFCC和LogFBank语音特征提取算法的数学原理及计算过程方法,之后AI柠檬博客还将更新另一种语音识别特征提取算法:语谱图特征,敬请期待!

实战一 ASR语音识别模型

ASRT是一套基于深度学习实现的语音识别系统,全称为Auto Speech Recognition Tool,由AI柠檬博主开发并在GitHub上开源(GPL 3.0协议)。本项目声学模型通过采用卷积神经网络(CNN)和连接性时序分类(CTC)方法,使用大量中文语音数据集进行训练,将声音转录为中文拼音,并通过语言模型,将拼音序列转换为中文文本。算法模型在测试集上已经获得了80%的正确率。基于该模型,在Windows平台上实现了一个基于ASRT的语音识别应用软件,取得了较好应用效果。这个应用软件包含Windows 10 UWP商店应用和Windows 版.Net平台桌面应用,也一起开源在GitHub上了。

ASRT项目主页:

https://asrt.ailemon.me

ASRT项目文档:

https://asrt.ailemon.me/docs/

GitHub项目地址:

语音识别核心系统

https://github.com/nl8590687/ASRT_SpeechRecognition

语音识别客户端应用

Windows桌面版 https://github.com/nl8590687/ASRT_SpeechClient_WPF

Windows 10 UWP版 https://github.com/nl8590687/ASRT_SpeechClient_UWP

Java Web版 https://github.com/nl8590687/ASRT_SpeechClient_JavaWeb

Python SDK
https://github.com/nl8590687/ASRT_SDK_Python3

近年来,深度学习在人工智能领域兴起,其对语音识别也产生了深远影响,深层的神经网络逐步替代了原来的GMM-HMM模型。在人类的交流和知识传播中,大约 70% 的信息是来自于语音。未来,语音识别将必然成为智能生活里重要的一部分,它可以为语音助手、语音输入等提供必不可少的基础,这将会成为一种新的人机交互方式。因此,我们需要让机器听懂人的声音。

我们的语音识别系统的声学模型采用了深度全卷积神经网络,直接将语谱图作为输入。模型结构上,借鉴了图像识别中效果最好的网络配置VGG,这种网络模型有着很强的表达能力,可以看到非常长的历史和未来信息,相比RNN在鲁棒性上更出色。在输出端,这种模型可以和CTC方案可以完美结合,以实现整个模型的端到端训练,将声音波形信号直接转录为中文普通话拼音序列。在语言模型上,通过最大熵隐含马尔可夫模型,将拼音序列转换为中文文本。并且,为了通过网络提供服务给所有的用户,本项目还使用了Python的HTTP协议基础服务器包,提供基于网络HTTP协议的语音识别API,客户端软件通过网络,调用该API实现语音识别功能。

目前,该语音识别系统在考虑朝着语音识别框架方向发展,以方便研究人员随时上手研究新模型,使用新数据集等。

系统的流程

特征提取 将普通的wav语音信号通过分帧加窗等操作转换为神经网络需要的二维频谱图像信号,即语谱图。

声学模型 基于Keras和TensorFlow框架,使用这种参考了VGG的深层的卷积神经网络作为网络模型,并训练。

CTC解码 在语音识别系统的声学模型的输出中,往往包含了大量连续重复的符号,因此,我们需要将连续相同的符合合并为同一个符号,然后再去除静音分隔标记符,得到最终实际的语音拼音符号序列。

语言模型 使用统计语言模型,将拼音转换为最终的识别文本并输出。拼音转文本的本质被建模为一条隐含马尔可夫链,这种模型有着很高的准确率。(其原理请看:https://blog.ailemon.me/2017/04/27/statistical-language-model-chinese-pinyin-to-words/)

基于HTTP协议的API接口

本项目使用了Python内置的http.server包来实现了一个基础的基于http协议的API服务器。通过将声学模型和语言模型连接起来,使用该服务器程序,可以直接实现一个简单的API服务器,通过POST方式进行数据交互。

这是POST参数列表:
参数名 说明
token 服务器对连接的客户端进行认证用的口令,避免其被非法调用
fs 指示传送的wav波形信号的频率是多少,单位:Hz
wavs 一个包含了全部语音波形信号的列表

客户端

本项目的客户端分为两种,均为Windows客户端,一个是UWP客户端,另一个是WPF客户端,源码均需要使用VS2017来开发和编译,使用C#和XAML编写。项目包含有界面逻辑和录音模块、语音识别API调用模块,并包含对wav文件的raw格式进行的解析。

客户端通过自动控制录音的中断时间、两个录音模块连续交替录音,以及异步发送请求操作,最终按照先后顺序将返回结果显示在界面的文本框中,实现了长时间连续语音识别的功能。

未来

未来的ASRT,还要加入针对说话人进行识别的功能,也就是做一个说话人识别系统,用来实现AI的“认主”行为,让AI知晓现在是谁在说话,这将是AI实际应用时很多场景下会面临的一个问题。不过这个项目截至发稿前,暂时还没有动工,有感兴趣的小伙伴欢迎提前关注一波~

ASRT项目主页:

https://asrt.ailemon.me

ASRT项目文档:

https://asrt.ailemon.me/docs/

GitHub项目地址:

语音识别核心系统

https://github.com/nl8590687/ASRT_SpeechRecognition

语音识别客户端应用

Windows桌面版 https://github.com/nl8590687/ASRT_SpeechClient_WPF

Windows 10 UWP版 https://github.com/nl8590687/ASRT_SpeechClient_UWP

Java Web版 https://github.com/nl8590687/ASRT_SpeechClient_JavaWeb

Python SDK
https://github.com/nl8590687/ASRT_SDK_Python3

说话人识别系统

https://github.com/nl8590687/ASRT_SpeakerRecognition

import platform as plat
import os
import time

from general_function.file_wav import *
from general_function.file_dict import *
from general_function.gen_func import *
from general_function.muti_gpu import *

import keras as kr
import numpy as np
import random

from keras.models import Sequential, Model
from keras.layers import Dense, Dropout, Input, Reshape, BatchNormalization # , Flatten
from keras.layers import Lambda, TimeDistributed, Activation,Conv2D, MaxPooling2D,GRU #, Merge
from keras.layers.merge import add, concatenate
from keras import backend as K
from keras.optimizers import SGD, Adadelta, Adam

from readdata24 import DataSpeech

abspath = ''
ModelName='261'
NUM_GPU = 2

class ModelSpeech(): # 语音模型类
	def __init__(self, datapath):
		'''
		初始化
		默认输出的拼音的表示大小是1428,即1427个拼音+1个空白块
		'''
		MS_OUTPUT_SIZE = 1428
		self.MS_OUTPUT_SIZE = MS_OUTPUT_SIZE # 神经网络最终输出的每一个字符向量维度的大小
		#self.BATCH_SIZE = BATCH_SIZE # 一次训练的batch
		self.label_max_string_length = 64
		self.AUDIO_LENGTH = 1600
		self.AUDIO_FEATURE_LENGTH = 200
		self._model, self.base_model = self.CreateModel() 
		
		self.datapath = datapath
		self.slash = ''
		system_type = plat.system() # 由于不同的系统的文件路径表示不一样,需要进行判断
		if(system_type == 'Windows'):
			self.slash='\\' # 反斜杠
		elif(system_type == 'Linux'):
			self.slash='/' # 正斜杠
		else:
			print('*[Message] Unknown System\n')
			self.slash='/' # 正斜杠
		if(self.slash != self.datapath[-1]): # 在目录路径末尾增加斜杠
			self.datapath = self.datapath + self.slash
	
		
	def CreateModel(self):
		'''
		定义CNN/LSTM/CTC模型,使用函数式模型
		输入层:200维的特征值序列,一条语音数据的最大长度设为1600(大约16s)
		隐藏层:卷积池化层,卷积核大小为3x3,池化窗口大小为2
		隐藏层:全连接层
		输出层:全连接层,神经元数量为self.MS_OUTPUT_SIZE,使用softmax作为激活函数,
		CTC层:使用CTC的loss作为损失函数,实现连接性时序多输出
		
		'''
		
		input_data = Input(name='the_input', shape=(self.AUDIO_LENGTH, self.AUDIO_FEATURE_LENGTH, 1))
		
		layer_h1 = Conv2D(32, (3,3), use_bias=False, activation='relu', padding='same', kernel_initializer='he_normal')(input_data) # 卷积层
		#layer_h1 = Dropout(0.05)(layer_h1)
		layer_h2 = Conv2D(32, (3,3), use_bias=True, activation='relu', padding='same', kernel_initializer='he_normal')(layer_h1) # 卷积层
		layer_h3 = MaxPooling2D(pool_size=2, strides=None, padding="valid")(layer_h2) # 池化层
		
		#layer_h3 = Dropout(0.05)(layer_h3) # 随机中断部分神经网络连接,防止过拟合
		layer_h4 = Conv2D(64, (3,3), use_bias=True, activation='relu', padding='same', kernel_initializer='he_normal')(layer_h3) # 卷积层
		#layer_h4 = Dropout(0.1)(layer_h4)
		layer_h5 = Conv2D(64, (3,3), use_bias=True, activation='relu', padding='same', kernel_initializer='he_normal')(layer_h4) # 卷积层
		layer_h6 = MaxPooling2D(pool_size=2, strides=None, padding="valid")(layer_h5) # 池化层
		
		#layer_h6 = Dropout(0.1)(layer_h6)
		layer_h7 = Conv2D(128, (3,3), use_bias=True, activation='relu', padding='same', kernel_initializer='he_normal')(layer_h6) # 卷积层
		#layer_h7 = Dropout(0.15)(layer_h7)
		layer_h8 = Conv2D(128, (3,3), use_bias=True, activation='relu', padding='same', kernel_initializer='he_normal')(layer_h7) # 卷积层
		layer_h9 = MaxPooling2D(pool_size=2, strides=None, padding="valid")(layer_h8) # 池化层
		
		#layer_h9 = Dropout(0.15)(layer_h9)
		layer_h10 = Conv2D(128, (3,3), use_bias=True, activation='relu', padding='same', kernel_initializer='he_normal')(layer_h9) # 卷积层
		#layer_h10 = Dropout(0.2)(layer_h10)
		layer_h11 = Conv2D(128, (3,3), use_bias=True, activation='relu', padding='same', kernel_initializer='he_normal')(layer_h10) # 卷积层
		layer_h12 = MaxPooling2D(pool_size=1, strides=None, padding="valid")(layer_h11) # 池化层
		
		#layer_h12 = Dropout(0.2)(layer_h12)
		layer_h13 = Conv2D(128, (3,3), use_bias=True, activation='relu', padding='same', kernel_initializer='he_normal')(layer_h12) # 卷积层
		#layer_h13 = Dropout(0.3)(layer_h13)
		layer_h14 = Conv2D(128, (3,3), use_bias=True, activation='relu', padding='same', kernel_initializer='he_normal')(layer_h13) # 卷积层
		layer_h15 = MaxPooling2D(pool_size=1, strides=None, padding="valid")(layer_h14) # 池化层
		
		#test=Model(inputs = input_data, outputs = layer_h12)
		#test.summary()
		
		layer_h16 = Reshape((200, 3200))(layer_h15) #Reshape层
		
		#layer_h16 = Dropout(0.3)(layer_h16) # 随机中断部分神经网络连接,防止过拟合
		layer_h17 = Dense(128, activation="relu", use_bias=True, kernel_initializer='he_normal')(layer_h16) # 全连接层
		
		inner = layer_h17
		#layer_h5 = LSTM(256, activation='relu', use_bias=True, return_sequences=True)(layer_h4) # LSTM层
		
		rnn_size=128
		gru_1 = GRU(rnn_size, return_sequences=True, kernel_initializer='he_normal', name='gru1')(inner)
		gru_1b = GRU(rnn_size, return_sequences=True, go_backwards=True, kernel_initializer='he_normal', name='gru1_b')(inner)
		gru1_merged = add([gru_1, gru_1b])
		gru_2 = GRU(rnn_size, return_sequences=True, kernel_initializer='he_normal', name='gru2')(gru1_merged)
		gru_2b = GRU(rnn_size, return_sequences=True, go_backwards=True, kernel_initializer='he_normal', name='gru2_b')(gru1_merged)
		
		gru2 = concatenate([gru_2, gru_2b])
		
		layer_h20 = gru2
		#layer_h20 = Dropout(0.4)(gru2)
		layer_h21 = Dense(128, activation="relu", use_bias=True, kernel_initializer='he_normal')(layer_h20) # 全连接层
		
		#layer_h17 = Dropout(0.3)(layer_h17)
		layer_h22 = Dense(self.MS_OUTPUT_SIZE, use_bias=True, kernel_initializer='he_normal')(layer_h21) # 全连接层
		
		y_pred = Activation('softmax', name='Activation0')(layer_h22)
		model_data = Model(inputs = input_data, outputs = y_pred)
		#model_data.summary()
		
		labels = Input(name='the_labels', shape=[self.label_max_string_length], dtype='float32')
		input_length = Input(name='input_length', shape=[1], dtype='int64')
		label_length = Input(name='label_length', shape=[1], dtype='int64')
		# Keras doesn't currently support loss funcs with extra parameters
		# so CTC loss is implemented in a lambda layer
		
		#layer_out = Lambda(ctc_lambda_func,output_shape=(self.MS_OUTPUT_SIZE, ), name='ctc')([y_pred, labels, input_length, label_length])#(layer_h6) # CTC
		loss_out = Lambda(self.ctc_lambda_func, output_shape=(1,), name='ctc')([y_pred, labels, input_length, label_length])
		
		
		
		model = Model(inputs=[input_data, labels, input_length, label_length], outputs=loss_out)
		
		model.summary()
		
		# clipnorm seems to speeds up convergence
		#sgd = SGD(lr=0.0001, decay=1e-6, momentum=0.9, nesterov=True, clipnorm=5)
		#ada_d = Adadelta(lr = 0.01, rho = 0.95, epsilon = 1e-06)
		opt = Adam(lr = 0.001, beta_1 = 0.9, beta_2 = 0.999, decay = 0.0, epsilon = 10e-8)
		#model.compile(loss={'ctc': lambda y_true, y_pred: y_pred}, optimizer=sgd)
		
		model.build((self.AUDIO_LENGTH, self.AUDIO_FEATURE_LENGTH, 1))
		model = ParallelModel(model, NUM_GPU)
		
		model.compile(loss={
     'ctc': lambda y_true, y_pred: y_pred}, optimizer = opt)
		
		
		# captures output of softmax so we can decode the output during visualization
		test_func = K.function([input_data], [y_pred])
		
		#print('[*提示] 创建模型成功,模型编译成功')
		print('[*Info] Create Model Successful, Compiles Model Successful. ')
		return model, model_data
		
	def ctc_lambda_func(self, args):
		y_pred, labels, input_length, label_length = args
		
		y_pred = y_pred[:, :, :]
		#y_pred = y_pred[:, 2:, :]
		return K.ctc_batch_cost(labels, y_pred, input_length, label_length)
	
	
	
	def TrainModel(self, datapath, epoch = 2, save_step = 1000, batch_size = 32, filename = abspath + 'model_speech/m' + ModelName + '/speech_model'+ModelName):
		'''
		训练模型
		参数:
			datapath: 数据保存的路径
			epoch: 迭代轮数
			save_step: 每多少步保存一次模型
			filename: 默认保存文件名,不含文件后缀名
		'''
		data=DataSpeech(datapath, 'train')
		
		num_data = data.GetDataNum() # 获取数据的数量
		
		yielddatas = data.data_genetator(batch_size, self.AUDIO_LENGTH)
		
		for epoch in range(epoch): # 迭代轮数
			print('[running] train epoch %d .' % epoch)
			n_step = 0 # 迭代数据数
			while True:
				try:
					print('[message] epoch %d . Have train datas %d+'%(epoch, n_step*save_step))
					# data_genetator是一个生成器函数
					
					#self._model.fit_generator(yielddatas, save_step, nb_worker=2)
					self._model.fit_generator(yielddatas, save_step)
					n_step += 1
				except StopIteration:
					print('[error] generator error. please check data format.')
					break
				
				self.SaveModel(comment='_e_'+str(epoch)+'_step_'+str(n_step * save_step))
				self.TestModel(self.datapath, str_dataset='train', data_count = 4)
				self.TestModel(self.datapath, str_dataset='dev', data_count = 4)
				
	def LoadModel(self,filename = abspath + 'model_speech/m'+ModelName+'/speech_model'+ModelName+'.model'):
		'''
		加载模型参数
		'''
		self._model.load_weights(filename)
		self.base_model.load_weights(filename + '.base')

	def SaveModel(self,filename = abspath + 'model_speech/m'+ModelName+'/speech_model'+ModelName,comment=''):
		'''
		保存模型参数
		'''
		self._model.save_weights(filename+comment+'.model')
		self.base_model.save_weights(filename + comment + '.model.base')
		f = open('step'+ModelName+'.txt','w')
		f.write(filename+comment)
		f.close()

	def TestModel(self, datapath='', str_dataset='dev', data_count = 32, out_report = False, show_ratio = True):
		'''
		测试检验模型效果
		'''
		data=DataSpeech(self.datapath, str_dataset)
		#data.LoadDataList(str_dataset) 
		num_data = data.GetDataNum() # 获取数据的数量
		if(data_count <= 0 or data_count > num_data): # 当data_count为小于等于0或者大于测试数据量的值时,则使用全部数据来测试
			data_count = num_data
		
		try:
			ran_num = random.randint(0,num_data - 1) # 获取一个随机数
			
			words_num = 0
			word_error_num = 0
			
			nowtime = time.strftime('%Y%m%d_%H%M%S',time.localtime(time.time()))
			if(out_report == True):
				txt_obj = open('Test_Report_' + str_dataset + '_' + nowtime + '.txt', 'w', encoding='UTF-8') # 打开文件并读入
			
			txt = ''
			for i in range(data_count):
				data_input, data_labels = data.GetData((ran_num + i) % num_data)  # 从随机数开始连续向后取一定数量数据
				
				# 数据格式出错处理 开始
				# 当输入的wav文件长度过长时自动跳过该文件,转而使用下一个wav文件来运行
				num_bias = 0
				while(data_input.shape[0] > self.AUDIO_LENGTH):
					print('*[Error]','wave data lenghth of num',(ran_num + i) % num_data, 'is too long.','\n A Exception raise when test Speech Model.')
					num_bias += 1
					data_input, data_labels = data.GetData((ran_num + i + num_bias) % num_data)  # 从随机数开始连续向后取一定数量数据
				# 数据格式出错处理 结束
				
				pre = self.Predict(data_input, data_input.shape[0] // 8)
				
				words_n = data_labels.shape[0] # 获取每个句子的字数
				words_num += words_n # 把句子的总字数加上
				edit_distance = GetEditDistance(data_labels, pre) # 获取编辑距离
				if(edit_distance <= words_n): # 当编辑距离小于等于句子字数时
					word_error_num += edit_distance # 使用编辑距离作为错误字数
				else: # 否则肯定是增加了一堆乱七八糟的奇奇怪怪的字
					word_error_num += words_n # 就直接加句子本来的总字数就好了
				
				if(i % 10 == 0 and show_ratio == True):
					print('Test Count: ',i,'/',data_count)
				
				txt = ''
				if(out_report == True):
					txt += str(i) + '\n'
					txt += 'True:\t' + str(data_labels) + '\n'
					txt += 'Pred:\t' + str(pre) + '\n'
					txt += '\n'
					txt_obj.write(txt)
				
			
			#print('*[测试结果] 语音识别 ' + str_dataset + ' 集语音单字错误率:', word_error_num / words_num * 100, '%')
			print('*[Test Result] Speech Recognition ' + str_dataset + ' set word error ratio: ', word_error_num / words_num * 100, '%')
			if(out_report == True):
				txt = '*[测试结果] 语音识别 ' + str_dataset + ' 集语音单字错误率: ' + str(word_error_num / words_num * 100) + ' %'
				txt_obj.write(txt)
				txt_obj.close()
			
		except StopIteration:
			print('[Error] Model Test Error. please check data format.')
	
	def Predict(self, data_input, input_len):
		'''
		预测结果
		返回语音识别后的拼音符号列表
		'''
		
		batch_size = 1 
		in_len = np.zeros((batch_size),dtype = np.int32)
		
		in_len[0] = input_len
		
		x_in = np.zeros((batch_size, 1600, self.AUDIO_FEATURE_LENGTH, 1), dtype=np.float)
		
		for i in range(batch_size):
			x_in[i,0:len(data_input)] = data_input
		
		
		base_pred = self.base_model.predict(x = x_in)
		
		#print('base_pred:\n', base_pred)
		
		#y_p = base_pred
		#for j in range(200):
		#	mean = np.sum(y_p[0][j]) / y_p[0][j].shape[0]
		#	print('max y_p:',np.max(y_p[0][j]),'min y_p:',np.min(y_p[0][j]),'mean y_p:',mean,'mid y_p:',y_p[0][j][100])
		#	print('argmin:',np.argmin(y_p[0][j]),'argmax:',np.argmax(y_p[0][j]))
		#	count=0
		#	for i in range(y_p[0][j].shape[0]):
		#		if(y_p[0][j][i] < mean):
		#			count += 1
		#	print('count:',count)
		
		base_pred =base_pred[:, :, :]
		#base_pred =base_pred[:, 2:, :]
		
		r = K.ctc_decode(base_pred, in_len, greedy = True, beam_width=100, top_paths=1)
		
		#print('r', r)
		
		
		r1 = K.get_value(r[0][0])
		#print('r1', r1)
		
		
		#r2 = K.get_value(r[1])
		#print(r2)
		
		r1=r1[0]
		
		return r1
		pass
	
	def RecognizeSpeech(self, wavsignal, fs):
		'''
		最终做语音识别用的函数,识别一个wav序列的语音
		'''
		
		#data = self.data
		#data = DataSpeech('E:\\语音数据集')
		#data.LoadDataList('dev')
		# 获取输入特征
		#data_input = GetMfccFeature(wavsignal, fs)
		#t0=time.time()
		data_input = GetFrequencyFeature3(wavsignal, fs)
		#t1=time.time()
		#print('time cost:',t1-t0)
		
		input_length = len(data_input)
		input_length = input_length // 8
		
		data_input = np.array(data_input, dtype = np.float)
		#print(data_input,data_input.shape)
		data_input = data_input.reshape(data_input.shape[0],data_input.shape[1],1)
		#t2=time.time()
		r1 = self.Predict(data_input, input_length)
		#t3=time.time()
		#print('time cost:',t3-t2)
		list_symbol_dic = GetSymbolList(self.datapath) # 获取拼音列表
		
		
		r_str=[]
		for i in r1:
			r_str.append(list_symbol_dic[i])
		
		return r_str
		pass
		
	def RecognizeSpeech_FromFile(self, filename):
		'''
		最终做语音识别用的函数,识别指定文件名的语音
		'''
		
		wavsignal,fs = read_wav_data(filename)
		
		r = self.RecognizeSpeech(wavsignal, fs)
		
		return r
		
		pass
		
	
		
	@property
	def model(self):
		'''
		返回keras model
		'''
		return self._model


if(__name__=='__main__'):
	
	#import tensorflow as tf
	#from keras.backend.tensorflow_backend import set_session
	#os.environ["CUDA_VISIBLE_DEVICES"] = "1"
	#进行配置,使用70%的GPU
	#config = tf.ConfigProto()
	#config.gpu_options.per_process_gpu_memory_fraction = 0.95
	#config.gpu_options.allow_growth=True   #不全部占满显存, 按需分配
	#set_session(tf.Session(config=config))
	
	
	datapath =  abspath + ''
	modelpath =  abspath + 'model_speech'
	
	
	if(not os.path.exists(modelpath)): # 判断保存模型的目录是否存在
		os.makedirs(modelpath) # 如果不存在,就新建一个,避免之后保存模型的时候炸掉
	
	system_type = plat.system() # 由于不同的系统的文件路径表示不一样,需要进行判断
	if(system_type == 'Windows'):
		datapath = 'E:\\语音数据集'
		modelpath = modelpath + '\\'
	elif(system_type == 'Linux'):
		datapath =  abspath + 'dataset'
		modelpath = modelpath + '/'
	else:
		print('*[Message] Unknown System\n')
		datapath = 'dataset'
		modelpath = modelpath + '/'
	
	ms = ModelSpeech(datapath)
	
	
	#ms.LoadModel(modelpath + 'm261/speech_model261_e_0_step_98000.model')
	ms.TrainModel(datapath, epoch = 50, batch_size = 16, save_step = 500)
	#ms.TestModel(datapath, str_dataset='test', data_count = 128, out_report = True)
	#r = ms.RecognizeSpeech_FromFile('E:\\语音数据集\\ST-CMDS-20170001_1-OS\\20170001P00241I0053.wav')
	#r = ms.RecognizeSpeech_FromFile('E:\\语音数据集\\ST-CMDS-20170001_1-OS\\20170001P00020I0087.wav')
	#r = ms.RecognizeSpeech_FromFile('E:\\语音数据集\\wav\\train\\A11\\A11_167.WAV')
	#r = ms.RecognizeSpeech_FromFile('E:\\语音数据集\\wav\\test\\D4\\D4_750.wav')
	#print('*[提示] 语音识别结果:\n',r)

部分代码介绍

实战二 调百度和科大讯飞API

【原理+实战】AI所有领域SOTA综述 (一)语音识别_第9张图片
现在演示的是识别音频文件的内容。
token获取见官网,这边调包没什么含金量。
Python 技术篇-百度语音API鉴权认证获取Access Token
注:下面的 token 是我自己申请的,建议按照我的文章自己来申请专属的。

import requests
import os
import base64
import json

apiUrl='http://vop.baidu.com/server_api'
filename = "16k.pcm"   # 这是我下载到本地的音频样例文件名
size = os.path.getsize(filename)   # 获取本地语音文件尺寸
file1 = open(filename, "rb").read()   # 读取本地语音文件   
text = base64.b64encode(file1).decode("utf-8")   # 对读取的文件进行base64编码
data = {
     
    "format":"pcm",   # 音频格式
    "rate":16000,   # 采样率,固定值16000
    "dev_pid":1536,   # 普通话
    "channel":1,   # 频道,固定值1
    "token":"24.0c828682d414bf79b08f89c4c7dcd83a.2592000.1562739150.282335-16470175",   # 重要,鉴权认证Access Token,需要自己来申请
    "cuid":"DC-85-DE-F9-08-59",   # 随便一个值就好了,官网推荐是个人电脑的MAC地址
    "len":size,   # 语音文件的尺寸
    "speech":text,   # base64编码的语音文件
}
try:
    r = requests.post(apiUrl, data = json.dumps(data)).json()
    print(r)
    print(r.get("result")[0])
except Exception as e:
    print(e)

科大讯飞同样的方式,参见官网教程。

实战三 离线语音识别 Vosk

Vosk 支持30多种语言,并且现在做的不错,在离线语音里面不错了,https://github.com/alphacep/vosk-api

带Android python,c++ 的pc版本,等等web部署方案
Android 的话,就需要你安装Android 包,然后还要下载编译工具,gradle
cd android
gradle build
即可编译,编译成功后会生成apk安装包,手机就能安装,离线使用了。
部分代码:

  /**
     * Adds listener.
     */
    public void addListener(RecognitionListener listener) {
     
        synchronized (listeners) {
     
            listeners.add(listener);
        }
    }

    /**
     * Removes listener.
     */
    public void removeListener(RecognitionListener listener) {
     
        synchronized (listeners) {
     
            listeners.remove(listener);
        }
    }

    /**
     * Starts recognition. Does nothing if recognition is active.
     * 
     * @return true if recognition was actually started
     */
    public boolean startListening() {
     
        if (null != recognizerThread)
            return false;

        recognizerThread = new RecognizerThread();
        recognizerThread.start();
        return true;
    }

这边实战的比较简单,后续我做了很多优化,支持Android,python ,c++,java语言等部署,欢迎咨询我。

你可能感兴趣的:(精选目标检测,算法,人工智能,深度学习,pytorch)