
本期分享来自 MindSpore 社区的龙泳旭同学带来的项目经验:基于MindSpore,使用DFCNN和CTC损失函数的声学模型实现。
项目信息
项目名称
《基于MindSpore,使用DFCNN和CTC损失函数的声学模型实现》
方案描述
本项目的目标是使用MindSpore实现DFCNN+CTC的声学模型,将一句语音转化成一张特定模式的图像作为输入,然后通过DFCNN+CTC结构,对整句语音进行建模,实现输出单元直接与最终的识别结果(音节)相对应。
项目背景
自动语音识别(ASR)技术的目的是让机器能够"听懂"人类的语音,将人类语音信息转化为可读的文字信息,是实现人机交互的关键技术,也是长期以来的研究热点。最近几年,随着深度神经网络的应用,加上海量大数据的使用和云计算的普及,语音识别取得了突飞猛进的进展,在多个行业突破了实用化的门槛,越来越多的语音技术产品进入了人们的日常生活,包括苹果的Siri、亚马逊的Alexa、讯飞语音输入法、叮咚智能音箱等都是其中的典型代表。
......
20世纪80年代后期,深度神经网络(deep neural network,DNN)的前身— — 人工神经网络(artificial neural network,ANN)也成为了语音识别研究的一个方向。科大讯飞在2016年提出了一种全新的语音识别框架,称为全序列卷积神经网络(deep fully convolutional neural network,DFCNN)。实验证明,DFCNN 比BLSTM 语音识别系统这个学术界和工业界最好的系统识别率提升了15%以上。
——王海坤,潘嘉,刘聪.语音识别技术的研究进展与展望[J].电信科学,2018(2):1-11.
DFCNN的模型结构图大概如下所示:

DFCNN先对时域的语音信号进行傅里叶变换得到语音的语谱图,DFCNN直接将一句语音转化成一张图像作为输入,输出单元则直接与最终的识别结果(比如音节或者汉字)相对应。DFCNN的结构中把时间和频率作为图像的两个维度,通过结合较多的卷积层和池化(pooling)层,构成比较深的神经网络,实现对整句语音的建模。
正如上面所说,我们建立DFCNN模型,是把语谱图视作带有特定模式的图像,而有经验的语音学专家能够从中看出里面说的内容,于是我们便想将机器训练成为这么一个“专家”。
那么,为什么需要构建一个那么深的神经网络呢?
#
从输入端看
传统语音识别系统的提取特征方式是在傅里叶变换后用各种类型的人工设计的滤波器,比如Log Mel-Filter Bank,造成在语音信号频域信息损失比较明显。另外,传统语音特征采用非常大的帧移来降低运算量,导致时域上的信息会有损失,当说话人语速较快的时候, 这个问题表现得更为突出。而借鉴了计算机视觉领域加深网络的指导思想,DFCNN通过加深模型,保证了语音的长时相关性,能够看到足够长的历史与未来的信息,因此在顽健性上表现得更好。
#
从输出端看
DFCNN也能比较灵活地与其他模型进行接合,
例如:连接时序分类模型
(connectionist temporal classification,CTC),
以实现端到端的模型训练。
而本项目正是希望借用Mindspore进行实现。
Mindspore 简介
深度学习是近年来发展得比较快的一个领域,国外的公司诸如谷歌(Google)和脸书(Facebook)都分别推出了自己的开源深度学习框架,分别是采用静态计算图的Tensorflow与采用动态计算图的Pytorch,受到了广大的欢迎与应用。国内由华为公司推出的MindSpore框架则结合了动静计算图,发挥了二者的优势。由于Tensorflow与Pytorch出现的时间更早、社区更完善,MindSpore在基于DFCNN+CTC模型的实现任务上并没有具体的实现。但多亏于MindSpore框架底层API的完整、高效与简明,使得实现这一任务成为可能。
Mindspore 生态
MindSpore作为全球AI开源社区
(https://gitee.com/mindspore/mindspore),
致力于进一步开发和丰富AI软硬件应用生态。

Mindspore 技术特点
#
自动微分
当前主流深度学习框架中有三种自动微分技术:
基于静态计算图的转换:
编译时将网络转换为静态数据流图,将链式法则
应用于数据流图,实现自动微分。
基于动态计算图的转换:
记录算子过载正向执行时网络的运行轨迹,对动
态生成的数据流图应用链式法则,实现自动微分。
基于源码的转换:
该技术是从功能编程框架演进而来,以即时编译
(Just-in-time Compilation,JIT)的形式对中
间表达式(程序在编译过程中的表达式)进行自
动差分转换,支持复杂的控制流场景、高阶函数
和闭包。
TensorFlow早期采用的是静态计算图,PyTorch采
用的是动态计算图。静态映射可以利用静态编译技
术来优化网络性能,但是构建网络或调试网络非常
复杂。动态图的使用非常方便,但很难实现性能的
极限优化。
MindSpore找到了另一种方法,即基于源代码转换
的自动微分。一方面,它支持自动控制流的自动微
分,因此像PyTorch这样的模型构建非常方便。
另一方面,MindSpore可以对神经网络进行静态编
译优化,以获得更好的性能。

MindSpore自动微分的实现可以理解为程序本身的
符号微分。MindSpore IR是一个函数中间表达式,
它与基础代数中的复合函数具有直观的对应关系。
复合函数的公式由任意可推导的基础函数组成。
MindSpore IR中的每个原语操作都可以对应基础
代数中的基本功能,从而可以建立更复杂的流控制。
#
自动并行
MindSpore自动并行的目的是构建数据并行、模型
并行和混合并行相结合的训练方法。该方法能够自
动选择开销最小的模型切分策略,实现自动分布并
行训练。

目前MindSpore采用的是算子切分的细粒度并行策
略,即图中的每个算子被切分为一个集群,完成并
行操作。在此期间的切分策略可能非常复杂,但
是作为一名Python开发者,您无需关注底层实现,
只要顶层API计算是有效的即可。
快速开始
相信很多同学在进行机器学习时,用的比较多的是诸如Pytorch、Tensorflow和Keras等框架,对于Mindspore的使用,是比较陌生的。但是只要心里有机器学习的基本流程,我相信借助于Mindspore官方文档
(https://www.mindspore.cn/docs/programming_guide/zh-CN/r1.3/index.html)的帮助,与Model_Zoo
(https://gitee.com/mindspore/mindspore/tree/master/model_zoo)
参考案例的指导,从其他框架迁移快速适应过来是不成问题的。
我将该项目核心的实现大致分为如下两步走:搭建模型->训练流程。
搭建模型
Mindspore的API算子与Pytorch的API基本比较类似,而且有映射表(https://www.mindspore.cn/docs/note/zh-CN/r1.3/index.html#operator_api)
以进行参考,因此我可以比较快写出DFCNN的模型结构:
- class DFCNN(nn.Cell):
- """DFCNN model
- """
- def __init__(self, num_classes, input_nc = 1, padding=1, pad_mode='pad', has_bias = False, use_dropout = False):
- super(DFCNN,self).__init__()
-
- if pad_mode=='pad':
- assert padding>=0,"when the pad_mode is 'pad', the padding must be greater than or equal to 0!"
-
- if pad_mode=='same' or pad_mode=='valid':
- assert padding==0,"when the pad_mode is 'same' or 'valid', the padding must be equal to 0!"
-
- self.use_dropout = use_dropout
-
- # structure
-
- # seq 1
- self.conv11 = nn.Conv2d(
- in_channels=input_nc, out_channels=64,
- kernel_size=3, stride=1, padding=padding, has_bias=has_bias,pad_mode=pad_mode
- )
- self.bn11 = nn.BatchNorm2d(64)
- self.relu11 = nn.ReLU()
- self.conv12 = nn.Conv2d(in_channels=64, out_channels=64,
- kernel_size=3, stride=1, padding=padding, has_bias=has_bias, pad_mode=pad_mode
- )
- self.bn12 = nn.BatchNorm2d(64)
- self.relu12 = nn.ReLU()
- self.maxpool1 = nn.MaxPool2d(kernel_size=2, stride=2, pad_mode='valid')
-
- # seq 2
- self.conv21 = nn.Conv2d(
- in_channels=64,out_channels=128,
- kernel_size=3, stride=1,padding=padding, has_bias=has_bias,pad_mode=pad_mode
- )
- self.bn21 = nn.BatchNorm2d(128)
- self.relu21 = nn.ReLU()
- self.conv22 = nn.Conv2d(in_channels=128, out_channels=128,
- kernel_size=3, stride=1, padding=padding, has_bias=has_bias, pad_mode=pad_mode
- )
- self.bn22 = nn.BatchNorm2d(128)
- self.relu22 = nn.ReLU()
- self.maxpool2 = nn.MaxPool2d(kernel_size=2, stride=2, pad_mode='valid')
-
- # seq 3
- self.conv31 = nn.Conv2d(
- in_channels=128,out_channels=256,
- kernel_size=3, stride=1,padding=padding, has_bias=has_bias,pad_mode=pad_mode
- )
- self.bn31 = nn.BatchNorm2d(256)
- self.relu31 = nn.ReLU()
- self.conv32 = nn.Conv2d(in_channels=256, out_channels=256,
- kernel_size=3, stride=1, padding=padding, has_bias=has_bias, pad_mode=pad_mode
- )
- self.bn32 = nn.BatchNorm2d(256)
- self.relu32 = nn.ReLU()
- self.conv33 = nn.Conv2d(in_channels=256, out_channels=256,
- kernel_size=3, stride=1, padding=padding, has_bias=has_bias, pad_mode=pad_mode
- )
- self.bn33 = nn.BatchNorm2d(256)
- self.relu33 = nn.ReLU()
- self.maxpool3 = nn.MaxPool2d(kernel_size=2, stride=2, pad_mode='valid')
-
- # seq 4
- self.conv41 = nn.Conv2d(
- in_channels=256,out_channels=512,
- kernel_size=3, stride=1,padding=padding, has_bias=has_bias,pad_mode=pad_mode
- )
- self.bn41 = nn.BatchNorm2d(512)
- self.relu41 = nn.ReLU()
- self.conv42 = nn.Conv2d(in_channels=512, out_channels=512,
- kernel_size=3, stride=1, padding=padding, has_bias=has_bias, pad_mode=pad_mode
- )
- self.bn42 = nn.BatchNorm2d(512)
- self.relu42 = nn.ReLU()
- self.conv43 = nn.Conv2d(in_channels=512, out_channels=512,
- kernel_size=3, stride=1, padding=padding, has_bias=has_bias, pad_mode=pad_mode
- )
- self.bn43 = nn.BatchNorm2d(512)
- self.relu43 = nn.ReLU()
- self.maxpool4 = nn.MaxPool2d(kernel_size=1, stride=1, pad_mode='valid')
-
- # seq 5
- self.conv51 = nn.Conv2d(
- in_channels=512,out_channels=512,
- kernel_size=3, stride=1,padding=padding, has_bias=has_bias,pad_mode=pad_mode
- )
- self.bn51 = nn.BatchNorm2d(512)
- self.relu51 = nn.ReLU()
- self.conv52 = nn.Conv2d(in_channels=512, out_channels=512,
- kernel_size=3, stride=1, padding=padding, has_bias=has_bias, pad_mode=pad_mode
- )
- self.bn52 = nn.BatchNorm2d(512)
- self.relu52 = nn.ReLU()
- self.conv53 = nn.Conv2d(in_channels=512, out_channels=512,
- kernel_size=3, stride=1, padding=padding, has_bias=has_bias, pad_mode=pad_mode
- )
- self.bn53 = nn.BatchNorm2d(512)
- self.relu53 = nn.ReLU()
- self.maxpool5 = nn.MaxPool2d(kernel_size=1, stride=1, pad_mode='valid')
-
-
- self.bn = nn.BatchNorm2d(512)
- if self.use_dropout:
- self.drop1 = nn.Dropout(0.8)
- self.drop2 = nn.Dropout(0.8)
- self.drop3 = nn.Dropout(0.8)
- self.drop4 = nn.Dropout(0.8)
- self.drop5 = nn.Dropout(0.8)
- self.drop_fc1 = nn.Dropout(0.5)
- self.drop_fc2 = nn.Dropout(0.5)
- self.fc1 = nn.Dense(25 * 512, 4096, activation='relu')
- self.fc2 = nn.Dense(4096, 4096, activation='relu')
- self.fc3 = nn.Dense(4096, num_classes, activation='relu')
训练流程
熟悉Pytorch的朋友都知道,其训练流程基本上是参考这样一个套路来的:
(https://pytorch.org/tutorials/beginner/blitz/cifar10_tutorial.html#sphx-glr-beginner-blitz-cifar10-tutorial-py)
- for epoch in range(2): # loop over the dataset multiple times
-
- running_loss = 0.0
- for i, data in enumerate(trainloader, 0):
- # get the inputs; data is a list of [inputs, labels]
- inputs, labels = data
-
- # zero the parameter gradients
- optimizer.zero_grad()
-
- # forward + backward + optimize
- outputs = net(inputs)
- loss = criterion(outputs, labels)
- loss.backward()
- optimizer.step()
-
- # print statistics
- running_loss += loss.item()
- if i % 2000 == 1999: # print every 2000 mini-batches
- print('[%d, %5d] loss: %.3f' %
- (epoch + 1, i + 1, running_loss / 2000))
- running_loss = 0.0
-
- print('Finished Training')
而在使用Mindspore时,实则是需要通过mindspore.Model.train接口进行训练(参考)
(https://www.mindspore.cn/tutorials/zh-CN/master/quick_start.html):
- model.train(epochs,
- train_loader,
- callbacks=callbacks,
- dataset_sink_mode=False)
当我第一次使用时,是有很多疑问的:如果我需要使用自定义的损失函数呢?如果我需要更加灵活的数据加载类呢?
参考了官方教程和Model Zoo,我才明白Mindspore的思路,即是将训练流程也作一个Cell来处理,一切的安排都在construct中做。按照这个思路出发,我自定义实现了一个训练网络:
- class WithLossCell(nn.Cell):
-
- def __init__(self, backbone, loss_fn):
- super(WithLossCell, self).__init__(auto_prefix=False)
- self._backbone = backbone
- self._loss_fn = loss_fn
-
- def construct(self, img, label_indices, text, sequence_length):
- model_predict = self._backbone(img)
- return self._loss_fn(model_predict, label_indices, text, sequence_length)
-
- @property
- def backbone_network(self):
- return self._backbone
-
- def predict(self, img):
- return self._backbone(img)
那么,在我有自定义损失函数的需求时,只需将其传入到WithLossCell再封装到Model即可:
- # 模型
- net = DFCNN(num_classes=len(label2idx), padding=padding, pad_mode=pad_mode ,has_bias=has_bias, use_dropout=use_dropout)
- ......
- # 损失函数
- criterion = CTCLoss()
- ......
- # “打包”到一个Cell中
- net = WithLossCell(net, criterion)
- ......
- # 封装到Model中
- model = Model(net)
- ......
- # 进行训练
- model.train(epochs,
- train_loader,
- callbacks=callbacks,
- dataset_sink_mode=False)
事实上在项目的深入过程中,很多需求也是接踵而至的,这些细节若是读者有兴趣,可以参考此处
(https://gitlab.summer-ospp.ac.cn/summer2021/210610338)。
问题处理经验分享
问题出现的背景:
在搭建好模型、训练网络之后,在进行实际运行时候,出现了loss值一直震荡的问题,或许这也即是机器学习中经常让人头疼的问题。
问题解决的途径:
接触过机器学习的开发者或许很大一部分人都或多或少地碰到过相似的问题。正如Mindspore官方文档里所说:
(https://www.mindspore.cn/docs/programming_guide/zh-CN/r1.3/accuracy_optimization.html)
模型精度问题和一般的软件问题不同,定位周期一般也更长。在通常的程序中,程序输出和预期不符意味着存在bug(编码错误)。但是对一个深度学习模型来说,模型精度达不到预期,有着更复杂的原因和更多的可能性。由于模型精度要经过长时间的训练才能看到最终结果,定位精度问题通常会花费更长的时间。
那么我根据文档与以往的经验,我从以下几个点来排除问题:
检查超参数设置
检查输入数据
检查模型结构
为了能可视化地展示做出的调整是否有效果,我引入了Mindspore的配套工具:MindInsight(https://www.mindspore.cn/mindinsight/)。
这个工具十分强大,可以很好帮我定位问题、测验效果。通过这个工具,可以看到经过比较长时间的训练,我们网络确实存在着loss震荡的问题:

那么,接下来我便按照上述的几点问题进行修订,排除问题。
#
调整超参数
有时候一些bug往往是最简单的问题所导致的,
因此我打算从最简单的方向先进行试探。通过
实验不同的batch-size、学习率等等超参数,
loss值仍表现为震荡。因此可以暂时排除这个
问题。
#
检查数据输入
数据是模型训练的一个重要组成部分,如果数据
出现问题,模型也是必然出现问题的。因此,我
在网络前向传播前,利用matlibplot库输出进
入的张量的图形,进行检查:



基本可以判定进入网络的语谱图是正常处理的,因此也暂时排除了这个问题。
#
调整模型结构
问题出在模型上是对我而言,比较困难的一个方
向了。我试着从加深、加宽模型、调整参数初始
化等方式对模型进行修改,但loss值都表现出了
震荡。最终在一些机缘巧合上,我想从损失函数
上找问题。我认为可能是我对Mindspore官方文
档上对CTCLoss损失函数的介绍理解不够深刻,
导致我的模型没有办法很好的配合这个接口。于
是我开始阅读Mindspore的源码,试图从其中寻
找答案。终于在Mindspore底层的C++实现的
mindspore/ccsrc/backend/kernel_
compiler/cpu/ctcloss_cpu_
kernel.cc文件中找到了答案,在该文件的307
行,可以看到其对输入进行了一次Softmax处理。
- void CTCLossCPUKernel::LaunchKernel(const std::vector<AddressPtr> &inputs, const std::vector<AddressPtr> &outputs) {
- ......
- for (size_t b = 0; b < batch_size_; ++b) {
- std::vector<uint32_t> label_with_blank = labels_with_blank[b];
- // y_b [num_class, sequence_length]
- std::vector<std::vector<T>> y_b;
- std::vector<std::vector<T>> dy;
- std::vector<std::vector<T>> log_alpha_b;
- std::vector<std::vector<T>> log_beta_b;
- MatrixfromVector(num_class_, sequence_length_addr[b], &y_b, kLogZero_);
- MatrixfromVector(y_b.size(), y_b[0].size(), &dy, T(0));
- MatrixfromVector(label_with_blank.size(), sequence_length_addr[b], &log_alpha_b, kLogZero_);
- MatrixfromVector(label_with_blank.size(), sequence_length_addr[b], &log_beta_b, kLogZero_);
- //<<<<<<<<<<<<<<<<<<<<<<<<< 307 行 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
- InnerSoftMax(inputs_addr, &y_b, sequence_length_addr[b], num_class_, batch_size_, b);
- //>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
- CalculateFwdVar(label_with_blank, y_b, &log_alpha_b);
- CalculateBwdVar(label_with_blank, y_b, &log_beta_b);
-
- T log_pzx = kLogZero_;
- for (size_t u = 0; u < label_with_blank.size(); ++u) {
- log_pzx = LogSumExp(log_pzx, log_alpha_b[u][0] + log_beta_b[u][0]);
- }
-
- loss_addr[b] = -log_pzx;
-
- CalculateGrad(label_with_blank, y_b, log_alpha_b, log_beta_b, log_pzx, &dy);
-
- for (size_t t = 0; t < sequence_length_addr[b]; ++t) {
- for (size_t c = 0; c < num_class_; ++c) {
- gradient_addr[t * batch_size_ * num_class_ + b * num_class_ + c] = dy[c][t];
- }
- }
- }
- }
- .......
- class DFCNN(nn.Cell):
- """DFCNN model
- """
- ......
- def construct(self, x):
- x = self.feature(x) # [batch, 256, 200, 25] -> [batch, channels, max_time, fq]
- x = self.bn(x)
- x = x.transpose(0,2,1,3) # [batch, channels, max_time, fq] -> [batch, max_time, channels, fq]
- x = x.reshape(-1, x.shape[1], x.shape[2] * x.shape[3]) # [batch, max_time, channels*fq]
-
- x = self.fc1(x)
- if self.use_dropout:
- x = self.drop_fc1(x)
- x = self.fc2(x)
- if self.use_dropout:
- x = self.drop_fc2(x)
- x = self.fc3(x)
- #>>>>>>>>> 删除该层 >>>>>>>>>>>>
- x = self.softmax(x)
- #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
-
- return x
- ....
于是我不禁怀疑是否是我做了多余的操作,而使得损失函数计算不正常。于是我便将这一层删去了。最终,loss曲线表现出了正常的走势:

问题得以解决。
后续成果
项目代码已合入社区。
http://gitee.com/mindspore/course/tree/master/dfcnn

MindSpore官方资料
官方QQ群 : 486831414
官网:https://www.mindspore.cn/
Gitee : https : //gitee.com/mindspore/mindspore
GitHub : https://github.com/mindspore-ai/mindspore
论坛:https://bbs.huaweicloud.com/forum/forum-1076-1.html