下载 PDF[PDF 888.45KB]
概述
过去三十年,从实验室研究到真正走向市场,语音识别技术取得了巨大的进展。 语音识别技术在人们生活中发挥着越来越重要的作用。语音识别技术应用到了工作、家庭、汽车,医疗等各个领域。 它是世界 10 大新兴技术之一。
经过今年的发展,语音识别技术的主要算法已经从 GMM(高斯混合模型)和 HMM-GMM(隐马尔科夫模型—高斯混合模型)发展到 DNN(深度神经网络)。 DNN 的运行方式与人类大脑相似,是一种基于海量数据、计算高度密集,且极为复杂的模型。 得益于互联网,我们只需要一部智能手机就可运行 DNN,完全不必担忧远程计算机房内大量的服务器。 没有互联网,移动设备的语音识别服务没有任何用处,几乎无法听到您的语音,也无法运行。
是否可以将 DNN 计算流程从服务器转移至移动终端设备? 手机? 平板电脑? 答案是:可以。
凭借英特尔 CPU 对 SSSE3 指令集的支持,我们可以在不联网的情况下轻松运行基于 DNN 的语音识别应用。 经过测试,其准确率超过了 80%,接近于在线测试结果。 添加直接 SSSE3 支持可为移动设备创建良好的用户体验。 本文我们将探讨什么是 DNN,以及英特尔® SSSE3 指令集如何有助于加速 DNN 计算过程。
简介
DNN 是深度神经网络的缩写,它具有一个多隐藏层前向馈送网络。 DNN 是近年来机器学习领域的研究热点,产生了广泛的应用。 DNN 具有一个深层结构,需要学习的参数多达数千万,因而训练非常耗时。
语音识别是典型的 DNN 应用案例。 简单来说,语音识别应用由声学模型、语言模型和解码流程三部分构成。 声学模型用于模拟发音的概率分布。 语言模型用于模拟词语之间的关联。 而解码流程利用上述两种模型,将声音转化为文本。 神经网络能够模拟所有词语分布。 深度神经网络的表达能力要强于浅层神经网络,它模拟大脑的深层结构,能够更准确地“理解”事物的特征。 因此相比于其他方法,深度神经网络的深度可以成为模拟得更加准确的声学模型和语言模型。
图 1. DNN 应用领域
常见 DNN 结构图
常用的 DNN 通常包含线性与非线性层的多次交替叠加,如图所示:
图 2.包含 4 个隐藏层的 DNN 声学模型
在图 2 中,线性层是一种完整连接关系,从输入到输出的关系可由下列公式来表达:
YT = XTWT + B
其中,XT是行向量,由神经网络输入。 在语音识别应用中,我们通常将 4 帧数据放在一起计算,以创建 4xM 输入矩阵。 WT和 B 是神经网络与偏移向量的线性变换矩阵,其维度通常非常宽广。
英特尔® SSSE3 指令集
英特尔命名的 SIMD 流指令扩展 3 补充版 (SSSE3) 是 SSSE3 指令集的扩展版。 SSSE3 是 SIMD 技术的一部分,已集成至英特尔 CPU,有助于提升多媒体处理、编/解码和计算的能力。 使用 SSSE3 指令集,我们可以通过单个时钟周期内的单个指令来处理多个数据输入,从而显著提高程序的处理效率。 这种方式尤其适用于矩阵计算。
为了使用 SSSE3 指令集,我们首先需要声明和包含 SIMD 标头文件:
#include //MMX #include //SSE(include mmintrin.h) #include //SSE2(include xmmintrin.h) #include //SSE3(include emmintrin.h) #include //SSSE3(include pmmintrin.h) #include //SSE4.1(include tmmintrin.h) #include //SSE4.2(include smmintrin.h) #include //AES(include nmmintrin.h) #include //AVX(include wmmintrin.h) #include //(include immintrin.h)
“tmmintrin.h” 是面向 SSSE3 的标头文件,其中所定义的函数包括:
/*Add horizonally packed [saturated] words, double words, {X,}MM2/m{128,64} (b) to {X,}MM1 (a).*/ //a=(a0, a1, a2, a3, a4, a5, a6, a7), b=(b0, b1, b2, b3, b4, b5, b6, b7) //then r0=a0+a1,r1=a2+a3,r2=a4+a5,r3=a6+a7,r4=b0+b1,r5=b2+b3,r6=b4+b5, r7=b6+b7 extern __m128i _mm_hadd_epi16 (__m128i a, __m128i b); //a=(a0, a1, a2, a3), b=(b0, b1, b2, b3) //then r0=a0+a1,r1=a2+a3,r2=b0+b1,r3=b2+b3 extern __m128i _mm_hadd_epi32 (__m128i a, __m128i b); //SATURATE_16(x) is ((x > 32767) ? 32767 : ((x < -32768) ? -32768 : x)) //a=(a0, a1, a2, a3, a4, a5, a6, a7), b=(b0, b1, b2, b3, b4, b5, b6, b7) //then r0=SATURATE_16(a0+a1), ..., r3=SATURATE_16(a6+a7), //r4=SATURATE_16(b0+b1), ..., r7=SATURATE_16(b6+b7) extern __m128i _mm_hadds_epi16 (__m128i a, __m128i b); //a=(a0, a1, a2, a3), b=(b0, b1, b2, b3) //then r0=a0+a1, r1=a2+a3, r2=b0+b1, r3=b2+b3 extern __m64 _mm_hadd_pi16 (__m64 a, __m64 b); //a=(a0, a1), b=(b0, b1), 则r0=a0+a1, r1=b0+b1 extern __m64 _mm_hadd_pi32 (__m64 a, __m64 b); //SATURATE_16(x) is ((x > 32767) ? 32767 : ((x < -32768) ? -32768 : x)) //a=(a0, a1, a2, a3), b=(b0, b1, b2, b3) //then r0=SATURATE_16(a0+a1), r1=SATURATE_16(a2+a3), //r2=SATURATE_16(b0+b1), r3=SATURATE_16(b2+b3) extern __m64 _mm_hadds_pi16 (__m64 a, __m64 b); /*Subtract horizonally packed [saturated] words, double words, {X,}MM2/m{128,64} (b) from {X,}MM1 (a).*/ //a=(a0, a1, a2, a3, a4, a5, a6, a7), b=(b0, b1, b2, b3, b4, b5, b6, b7) //then r0=a0-a1, r1=a2-a3, r2=a4-a5, r3=a6-a7, r4=b0-b1, r5=b2-b3, r6=b4-b5, r7=b6-b7 extern __m128i _mm_hsub_epi16 (__m128i a, __m128i b); //a=(a0, a1, a2, a3), b=(b0, b1, b2, b3) //then r0=a0-a1, r1=a2-a3, r2=b0-b1, r3=b2-b3 extern __m128i _mm_hsub_epi32 (__m128i a, __m128i b); //SATURATE_16(x) is ((x > 32767) ? 32767 : ((x < -32768) ? -32768 : x)) //a=(a0, a1, a2, a3, a4, a5, a6, a7), b=(b0, b1, b2, b3, b4, b5, b6, b7) //then r0=SATURATE_16(a0-a1), ..., r3=SATURATE_16(a6-a7), //r4=SATURATE_16(b0-b1), ..., r7=SATURATE_16(b6-b7) extern __m128i _mm_hsubs_epi16 (__m128i a, __m128i b); //a=(a0, a1, a2, a3), b=(b0, b1, b2, b3) //then r0=a0-a1, r1=a2-a3, r2=b0-b1, r3=b2-b3 extern __m64 _mm_hsub_pi16 (__m64 a, __m64 b); //a=(a0, a1), b=(b0, b1), 则r0=a0-a1, r1=b0-b1 extern __m64 _mm_hsub_pi32 (__m64 a, __m64 b); //SATURATE_16(x) is ((x > 32767) ? 32767 : ((x < -32768) ? -32768 : x)) //a=(a0, a1, a2, a3), b=(b0, b1, b2, b3) //then r0=SATURATE_16(a0-a1), r1=SATURATE_16(a2-a3), //r2=SATURATE_16(b0-b1), r3=SATURATE_16(b2-b3) extern __m64 _mm_hsubs_pi16 (__m64 a, __m64 b); /*Multiply and add packed words, {X,}MM2/m{128,64} (b) to {X,}MM1 (a).*/ //SATURATE_16(x) is ((x > 32767) ? 32767 : ((x < -32768) ? -32768 : x)) //a=(a0, a1, a2, ..., a13, a14, a15), b=(b0, b1, b2, ..., b13, b14, b15) //then r0=SATURATE_16((a0*b0)+(a1*b1)), ..., r7=SATURATE_16((a14*b14)+(a15*b15)) //Parameter a contains unsigned bytes. Parameter b contains signed bytes. extern __m128i _mm_maddubs_epi16 (__m128i a, __m128i b); //SATURATE_16(x) is ((x > 32767) ? 32767 : ((x < -32768) ? -32768 : x)) //a=(a0, a1, a2, a3, a4, a5, a6, a7), b=(b0, b1, b2, b3, b4, b5, b6, b7) //then r0=SATURATE_16((a0*b0)+(a1*b1)), ..., r3=SATURATE_16((a6*b6)+(a7*b7)) //Parameter a contains unsigned bytes. Parameter b contains signed bytes. extern __m64 _mm_maddubs_pi16 (__m64 a, __m64 b); /*Packed multiply high integers with round and scaling, {X,}MM2/m{128,64} (b) to {X,}MM1 (a).*/ //a=(a0, a1, a2, a3, a4, a5, a6, a7), b=(b0, b1, b2, b3, b4, b5, b6, b7) //then r0=INT16(((a0*b0)+0x4000) >> 15), ..., r7=INT16(((a7*b7)+0x4000) >> 15) extern __m128i _mm_mulhrs_epi16 (__m128i a, __m128i b); //a=(a0, a1, a2, a3), b=(b0, b1, b2, b3) //then r0=INT16(((a0*b0)+0x4000) >> 15), ..., r3=INT16(((a3*b3)+0x4000) >> 15) extern __m64 _mm_mulhrs_pi16 (__m64 a, __m64 b); /*Packed shuffle bytes {X,}MM2/m{128,64} (b) by {X,}MM1 (a).*/ //SELECT(a, n) extracts the nth 8-bit parameter from a. The 0th 8-bit parameter //is the least significant 8-bits, b=(b0, b1, b2, ..., b13, b14, b15), b is mask //then r0 = (b0 & 0x80) ? 0 : SELECT(a, b0 & 0x0f), ..., //r15 = (b15 & 0x80) ? 0 : SELECT(a, b15 & 0x0f) extern __m128i _mm_shuffle_epi8 (__m128i a, __m128i b); //SELECT(a, n) extracts the nth 8-bit parameter from a. The 0th 8-bit parameter //is the least significant 8-bits, b=(b0, b1, ..., b7), b is mask //then r0= (b0 & 0x80) ? 0 : SELECT(a, b0 & 0x07),..., //r7=(b7 & 0x80) ? 0 : SELECT(a, b7 & 0x07) extern __m64 _mm_shuffle_pi8 (__m64 a, __m64 b); /*Packed byte, word, double word sign, {X,}MM2/m{128,64} (b) to {X,}MM1 (a).*/ //a=(a0, a1, a2, ..., a13, a14, a15), b=(b0, b1, b2, ..., b13, b14, b15) //then r0=(b0 < 0) ? -a0 : ((b0 == 0) ? 0 : a0), ..., //r15= (b15 < 0) ? -a15 : ((b15 == 0) ? 0 : a15) extern __m128i _mm_sign_epi8 (__m128i a, __m128i b); //a=(a0, a1, a2, a3, a4, a5, a6, a7), b=(b0, b1, b2, b3, b4, b5, b6, b7) //r0=(b0 < 0) ? -a0 : ((b0 == 0) ? 0 : a0), ..., //r7= (b7 < 0) ? -a7 : ((b7 == 0) ? 0 : a7) extern __m128i _mm_sign_epi16 (__m128i a, __m128i b); //a=(a0, a1, a2, a3), b=(b0, b1, b2, b3) //then r0=(b0 < 0) ? -a0 : ((b0 == 0) ? 0 : a0), ..., //r3= (b3 < 0) ? -a3 : ((b3 == 0) ? 0 : a3) extern __m128i _mm_sign_epi32 (__m128i a, __m128i b); //a=(a0, a1, a2, a3, a4, a5, a6, a7), b=(b0, b1, b2, b3, b4, b5, b6, b7) //then r0=(b0 < 0) ? -a0 : ((b0 == 0) ? 0 : a0), ..., //r7= (b7 < 0) ? -a7 : ((b7 == 0) ? 0 : a7) extern __m64 _mm_sign_pi8 (__m64 a, __m64 b); //a=(a0, a1, a2, a3), b=(b0, b1, b2, b3) //则r0=(b0 < 0) ? -a0 : ((b0 == 0) ? 0 : a0), ..., //r3= (b3 < 0) ? -a3 : ((b3 == 0) ? 0 : a3) extern __m64 _mm_sign_pi16 (__m64 a, __m64 b); //a=(a0, a1), b=(b0, b1), 则r0=(b0 < 0) ? -a0 : ((b0 == 0) ? 0 : a0), //r1= (b1 < 0) ? -a1 : ((b1 == 0) ? 0 : a1) extern __m64 _mm_sign_pi32 (__m64 a, __m64 b); /*Packed align and shift right by n*8 bits, {X,}MM2/m{128,64} (b) to {X,}MM1 (a).*/ //n: A constant that specifies how many bytes the interim result will be //shifted to the right, If n > 32, the result value is zero //CONCAT(a, b) is the 256-bit unsigned intermediate value that is a //concatenation of parameters a and b. //The result is this intermediate value shifted right by n bytes. //then r= (CONCAT(a, b) >> (n * 8)) & 0xffffffffffffffff extern __m128i _mm_alignr_epi8 (__m128i a, __m128i b, int n); //n: An integer constant that specifies how many bytes to shift the interim //result to the right,If n > 16, the result value is zero //CONCAT(a, b) is the 128-bit unsigned intermediate value that is formed by //concatenating parameters a and b. //The result value is the rightmost 64 bits after shifting this intermediate //result right by n bytes //then r = (CONCAT(a, b) >> (n * 8)) & 0xffffffff extern __m64 _mm_alignr_pi8 (__m64 a, __m64 b, int n); /*Packed byte, word, double word absolute value, {X,}MM2/m{128,64} (b) to {X,}MM1 (a).*/ //a=(a0, a1, a2, ..., a13, a14, a15) //then r0 = (a0 < 0) ? -a0 : a0, ..., r15 = (a15 < 0) ? -a15 : a15 extern __m128i _mm_abs_epi8 (__m128i a); //a=(a0, a1, a2, a3, a4, a5, a6, a7) //then r0 = (a0 < 0) ? -a0 : a0, ..., r7 = (a7 < 0) ? -a7 : a7 extern __m128i _mm_abs_epi16 (__m128i a); //a=(a0, a1, a2, a3) //then r0 = (a0 < 0) ? -a0 : a0, ..., r3 = (a3 < 0) ? -a3 : a3 extern __m128i _mm_abs_epi32 (__m128i a); //a=(a0, a1, a2, a3, a4, a5, a6, a7) //then r0 = (a0 < 0) ? -a0 : a0, ..., r7 = (a7 < 0) ? -a7 : a7 extern __m64 _mm_abs_pi8 (__m64 a); //a=(a0, a1, a2, a3) //then r0 = (a0 < 0) ? -a0 : a0, ..., r3 = (a3 < 0) ? -a3 : a3 extern __m64 _mm_abs_pi16 (__m64 a); //a=(a0, a1), then r0 = (a0 < 0) ? -a0 : a0, r1 = (a1 < 0) ? -a1 : a1 extern __m64 _mm_abs_pi32 (__m64 a);
__m64 和 __m128 的数据结构定义位于 MMX 的标头文件 “mmintrin.h” 和 SSE 标头文件 “xmmintrin.h”。
__m64:
typedef union __declspec(intrin_type) _CRT_ALIGN(8) __m64 { unsigned __int64 m64_u64; float m64_f32[2]; __int8 m64_i8[8]; __int16 m64_i16[4]; __int32 m64_i32[2]; __int64 m64_i64; unsigned __int8 m64_u8[8]; unsigned __int16 m64_u16[4]; unsigned __int32 m64_u32[2]; } __m64;
__m128:
typedef union __declspec(intrin_type) _CRT_ALIGN(16) __m128 { float m128_f32[4]; unsigned __int64 m128_u64[2]; __int8 m128_i8[16]; __int16 m128_i16[8]; __int32 m128_i32[4]; __int64 m128_i64[2]; unsigned __int8 m128_u8[16]; unsigned __int16 m128_u16[8]; unsigned __int32 m128_u32[4]; } __m128;
案例研究:使用 SSSE3 函数加速 DNN 计算
在本部分,我们将列举两个函数介绍如何使用 SSSE3 加速 DNN 计算流程。
__m128i _mm_maddubs_epi16 (__m128i a, __m128i b)饱和累加运算
该函数对 DNN 中的矩阵运算至关重要。参数 a 是 128 位寄存器,用于保存 16 个无符号整数(8 位),参数 b 是 16 个 8 位的带符号整数;返回结果包含 8 个 16 位整数。 该函数是满足矩阵运算要求的理想选择。 比如:
r0 := SATURATE_16((a0*b0) + (a1*b1)) r1 := SATURATE_16((a2*b2) + (a3*b3)) … r7 := SATURATE_16((a14*b14) + (a15*b15))
__m128i _mm_hadd_epi32 (__m128i a, __m128i b)相邻元素加法运算
该函数也可称为成对相加。参数 a 和参数 b 都是保存 4 个 32 位带符号整数的 128 位寄存器。 根据两个向量中的正态相匹配元素加法运算,它采用输入向量对相邻元素进行加法运算。 比如:
r0 := a0 + a1 r1 := a2 + a3 r2 := b0 + b1 r3 := b2 + b3
那么,我们假设 DNN 流程中需要执行向量运算任务:
Q: 有五个向量: a1、b1、b2、b3 和 b4。 向量 a1 是 16 维数的无符号 char 整数,而 b1、b2、b3 和 b4 都是 16 维数的带符号 char 整数。 我们需要 a1*b1、a1*b2、a1*b3 和 a1*b4 的内积,以保存并得出一个 32 位的带符号整数。
如果我们使用正交设计和 C 程序语言执行这一任务,编码应如下所示:
unsigned char b1[16],b2[16],b3[16],b4[16]; signed char a1[16]; int c[4],i; // Initialize b1,b2,b3,b4 and a1, for c, initialize with zeros // for(i=0;i<16;i++){ c[0] += (short)a1[i]*(short)b1[i]; c[1] += (short)a1[i]*(short)b1[i]; c[2] += (short)a1[i]*(short)b1[i]; c[3] += (short)a1[i]*(short)b1[i]; }
假设每个时钟周期包含一个乘法和加法,那么代码包含了 64 个时钟周期:
然后,我们换做使用 SSSE3 指令集来执行这一任务:
register __m128i a1,b1,b2,b3,b4,c,d1,d2,d3,d4; // initialize a1 b1 b2 b3 b4 c here, where c is set to zeros// d1 = _mm_maddubs_epi16(a1,b1); d1 = _mm_add_epi32(_mm_srai_epi32(_mm_unpacklo_epi16(d1, d1), 16), _mm_srai_epi32(_mm_unpackhi_epi16(d1, d1), 16)); d2 = _mm_maddubs_epi16(a1,b2); d2 = _mm_add_epi32(_mm_srai_epi32(_mm_unpacklo_epi16(d2, d2), 16), _mm_srai_epi32(_mm_unpackhi_epi16(d2, d2), 16)); d3 = _mm_hadd_epi32(d1, d2); d1 = _mm_maddubs_epi16(a1,b3); d1 = _mm_add_epi32(_mm_srai_epi32(_mm_unpacklo_epi16(d1, d1), 16), _mm_srai_epi32(_mm_unpackhi_epi16(d1, d1), 16)); d2 = _mm_maddubs_epi16(a1,b4); d2 = _mm_add_epi32(_mm_srai_epi32(_mm_unpacklo_epi16(d2, d2), 16), _mm_srai_epi32(_mm_unpackhi_epi16(d2, d2), 16)); d4 = _mm_hadd_epi32(d1, d2); c = _mm_hadd_epi32(d3, d4);
我们将结果保存在 “c” 的 128 位整数中,通过 4 个整数来连接。 如果将管道考虑在内的话,该流程消耗了 12 或 13 个时钟周期。 因此,这项任务可能得出的结果是:
实施 | CPU 时钟周期 | 提升 |
---|---|---|
普通的 C 编码 | 64 | - |
使用 SSSE3 指令集 | 13 | ~ 500% |
众所周知,语音识别的 DNN 流程设计多项矩阵计算。如果我们像这样在大妈中优化每项计算,IA 平台的性能将会实现前所未有的提升。 我们一直与 ISV Unisound 保持合作,该公司为中国市场提供语音识别服务。 Unisound 所使用的 DNN 流程在 ARM 设备上的性能提升了超过 10%。
总结
DNN 正成为语音识别技术的主要算法。 Google Now、Baidu Voice、Tencent Wechat、iFlytek Speech Service、Unisound Speech Service 和许多其他服务均使用这种算法。 同时,我们的 SSSE3 指令集可帮助优化这一语音识别流程。如果将该指令运用到这些应用,我相信,语音服务的用户体验将会显著提高,使用 IA 平台的用户也会不断增加。
关于作者
Li Alven 于 2007 年毕业于华中科技大学,主修计算机科学与信息安全。 他于 2013 年加入英特尔,在开发商关系部门移动支持团队担任资深应用工程师。 他主要负责为 IA 平台、语音识别技术、性能调优等领域提供与众不同的创新型支持。