Intel SSSE3命令セットを使用して、モバイルデバイスで実行される音声認識タスクでDNNアルゴリズムの実装を高速化する

過去30年にわたって、音声認識技術は深刻な進歩を遂げ、研究室での旅を開始し、幅広い消費者にリーチしました。 これらの技術は私たちの生活に重要な役割を果たし始めています。 彼らは職場で、自宅で、車の中で見つけることができます。 それらは、医療目的およびその他の活動分野で使用されます。 音声認識は、有望な世界トップ10のテクノロジーの1つです。





Vladstudioによる元の写真



復習



近年の研究の結果、基本的な音声認識アルゴリズムに変化がありました。 したがって、これらのアルゴリズムの前に、 GMM (ガウス混合モデル)およびHMM-GMM (隠れマルコフモデル-ガウス混合モデル)アルゴリズムがありました。 それらから、 DNN (Deep Neural Network)アルゴリズムへの移行がありました。 このアルゴリズムの動作は、人間の脳の活動に似ています。 複雑な計算と膨大なデータを使用します。



インターネットのおかげで、スマートフォン所有者は誰でも最新の音声認識技術を利用できます。 そのサービスには無数のサーバーがあります。 しかし、インターネットがなければ、モバイルデバイスでの音声認識サービスはほとんど役に立ちません。 彼らは彼らと「会話」しようとしている人々を正しく理解することはめったにありません。



DNNアルゴリズムの実装をサーバーからスマートフォンまたはタブレットに転送できますか? この質問に対する答えはイエスです。 SSSE3命令セットのIntelプロセッサによるサポートのおかげで、DNNベースの音声認識アプリケーションをモバイルデバイスで使用できます。 ただし、インターネット接続は必要ありません。 テストの結果、このようなアプリケーションによる音声認識の精度は80%以上でした。 これは、サーバーシステムを使用して達成可能なものに非常に近いものです。 この記事では、DNNアルゴリズムと、Intel SSSE3命令セットがこのアルゴリズムの実装に必要な計算を高速化する方法について説明します。



予備情報



DNN (GNS)は、Deep Neural Networkの略です。 これは、多くの隠れ層を含む直接配布ネットワークです。 DNNは最新の機械学習技術の最前線にいます。 このアルゴリズムには多くの実用的なアプリケーションがあります。



ディープニューラルネットワークには、多数の隠れ層があります。 それらをトレーニングする場合、数千万のパラメーターを変更する必要があります。 その結果、このようなネットワークのトレーニングには時間がかかります。



音声認識は典型的なDNNアプリケーションです。 簡略化された音声認識アプリケーションは、音響モデル、言語モデル、およびデコードサブシステムで構成されるものとして表すことができます。 音響モデルは、発音バリアントの確率分布をモデル化するために使用されます。 言語モデルは、単語間の関係をモデル化するために使用されます。 デコード段階では、上記の2つのモデルが使用され、音声はテキストに変換されます。 ニューラルネットワークは、あらゆる言語構造をシミュレートできます。 ディープニューラルネットワークは、浅いネットワークよりも重要なデータ属性を抽出する能力が高い一方で、人間の脳の構造をモデル化するため、物の特性をより正確に「理解」することができます。 その結果、他の方法と比較して、このようなニューラルネットワークでは音響モデルと言語モデルをより正確にモデル化できます。





DNNアルゴリズムの適用分野



典型的なディープニューラルネットワーク図



通常、典型的なディープニューラルネットワークには、互いにオーバーラップする多数の線形層と非線形層が含まれています。





DNNに基づく音響モデルの4つの隠れ層



図に示されているネットワークは、一連の線形層で構成されています。 前のレイヤーの各ニューロンは、次のレイヤーの各ニューロンに関連付けられています。 ネットワーク入力とその出力の接続は、次の式で説明できます。



Y T = X T W T + B



X Tは、ニューラルネットワークの入力である行ベクトルです。 音声認識に適用される場合、通常、同時作業のために4つのデータを配置し、4xM入力マトリックスを作成します。 W TとBは、それぞれニューラルネットワーク変換の線形行列と変位ベクトルです。 通常、このようなネットワークの次元は非常に大きく、すべての層に同じ数のニューロンがあります。つまり、ネットワークは正方形です。



Intel SSSE3命令セット



Intelは、Supplemental Streaming SIMD Extensions 3命令セット、または簡潔にするために、単にSSSE3(SSE3命令セット拡張)を呼び出します。 これは、Intelマイクロプロセッサに統合されたSIMDテクノロジーの一部です。 このテクノロジーは、マルチメディア処理機能を改善するように設計されています。 情報のエンコードおよびデコードのタスクを高速化し、さまざまな計算のパフォーマンスを高速化するように設計されています。 SSSE3命令セットを使用すると、1クロックサイクルで1つの命令で複数のデータストリームを処理できます。 これにより、アプリケーションのパフォーマンスが大幅に向上します。 特に、SSSE3コマンドはマトリックスコンピューティングに適用できます。



SSSE3命令セットを使用するには、対応するSIMDヘッダーファイルを接続する必要があります。



#include <mmintrin.h> //MMX #include <xmmintrin.h> //SSE #include <emmintrin.h> //SSE2 #include <pmmintrin.h> //SSE3 #include <tmmintrin.h> //SSSE3 #include <smmintrin.h> //SSSE4.1 #include <nmmintrin.h> //SSSE4.2 #include <wmmintrin.h> //AES #include <immintrin.h> //AVX
      
      





tmmintrin.hヘッダーファイルはSSSE3での作業を提供します。以下は、SSSE3で定義されている関数の説明です。



 /*  [ ]  ,  , {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); /*  [ ]  ,  , {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) // 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); /*    , {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)) // a    .  b    . 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)) // a    .  b    . extern __m64 _mm_maddubs_pi16 (__m64 a, __m64 b); /*         , {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); /*   {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); /*  , ,  , {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); /*      n*8 , {X,}MM2/m{128,64} (b) to {X,}MM1 (a).*/ //n: ,  ,    //    , // n > 32,    . //CONCAT(a, b)  256-   , //     a  b. // –   ,    n . //then r= (CONCAT(a, b) >> (n * 8)) & 0xffffffffffffffff extern __m128i _mm_alignr_epi8 (__m128i a, __m128i b, int n); //n:  ,  ,     //   . // n > 16,    . //CONCAT(a, b)  128-   , //     a  b. //  -  64 ,  //       n . //then r = (CONCAT(a, b) >> (n * 8)) & 0xffffffff extern __m64 _mm_alignr_pi8 (__m64 a, __m64 b, int n); /*   , ,  , {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アルゴリズムで試行する計算を高速化する



ここで、いくつかの関数を見てみましょう。 彼らの例では、DNNアルゴリズムを実装するときにSSSE3を使用して計算を高速化する方法を示します。



__m128i _mm_maddubs_epi16(__m128i a、__ m128i b)彩度の追加



この機能は、DNNアルゴリズムで行列計算を実行する場合に非常に重要です。 パラメーターは128ビットのレジスタで、16個の符号なし整数(8ビット)を格納するために使用されます。 パラメーターbは、同じく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ビットレジスタです。 2つのベクトルに対応する要素を追加する通常の操作に従って、チームは入力ベクトルの隣接要素の追加を実行します。



 r0 := a0 + a1 r1 := a2 + a3 r2 := b0 + b1 r3 := b2 + b3
      
      





DNN実装に典型的なベクトル計算タスクがあるとします。



5つのベクトルがあります:a1、b1、b2、b3、b4。 ベクトルa1は、signed char型の16個の整数の1次元配列です。 ベクトルb1、b2、b3、b4は、それぞれunsigned char型の16要素の整数の配列です。 スカラー積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; // // b1,b2,b3,b4  a1, c   // for(i=0;i<16;i++){ c[0] += (short)a1[i]*(short)b1[i]; c[1] += (short)a1[i]*(short)b2[i]; c[2] += (short)a1[i]*(short)b3[i]; c[3] += (short)a1[i]*(short)b4[i]; }
      
      





1つのクロックサイクルで、乗算の1つの操作と加算の1つの操作を実行できるとします。 計算を実行するための64クロックサイクルが得られます。



次に、SSSE3命令セットを使用して同じ問題を解決しましょう。



 register __m128i a1,b1,b2,b3,b4,c,d1,d2,d3,d4; // a1, b1, b2, b3  b4, c   // 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);
      
      





4つの整数を含む128ビットのレジスタに結果を保存します。 パイプラインデータ処理を考えると、計算には12または13クロックサイクルかかります。 このデータを比較すると、次の結果が得られます。

実装オプション

CPUサイクル

勝つ

一般的なCプログラミング

64

-
SSSE3を使用する

13

〜500%



ベンチマーク



上記のコードに基づいて実験を行いましょう。 異なる方法で同じ計算を実行する2つの関数を作成します。 そのうちの1つは、最後に、整数配列cの要素の合計を返します。2番目は、128ビットレジスタcの32ビット整数要素の合計です。 変数は、すべての関数呼び出しで初期化されます。 合計で、各関数への呼び出しは10,000,000回あり、テストはバックグラウンドスレッドで実行されます。





パフォーマンステストアプリケーションインターフェイス



これらは、Intel Atom Z3530 CPUを搭載したAsus Fonepad 8タブレットでアプリケーションのリリースバージョンをテストした結果です。 デバイスにAndroid 5.0がインストールされています。



SSSE3を使用した場合と使用しない場合で記述されたコードの実行速度の比較

いや

SSSE3を使用して、ms。

通常のC、msを使用します。

1

547

3781

2

507

3723

3

528

3762

4

517

3731

5

531

3755

6

517

3769

7

502

3752

8

529

3750

9

514

3745

10

510

3721

平均

520.2

3748.9

その結果、SSSE3命令を使用して計算を実装するコードは、通常よりも平均で7.2倍速く実行されることがわかりました。



Android Studioにインポートできるプロジェクトのソースコードは、 こちらにあります



まとめ



ご存知のように、ディープニューラルネットワークを使用して音声を認識すると、多くの行列計算が実行されます。 これらの計算を最適化すると、IAプラットフォームでこれまでにないパフォーマンスを実現できます。 中国で音声認識サービスを提供するISV Unisoundと協力しています。 Unisoundは、ARMデバイスでDNNベースのソフトウェアを使用すると、パフォーマンスを10%向上させることができました。



DNNは今日、音声認識の主要なアルゴリズムになりつつあります。 特に、Google Now、Baidu Voice、Tencent Wechat、iFlytek Speech Service、Unisound Speech Serviceなどのサービスで使用されています。 同時に、音声認識プロセスのベースとなる計算の最適化に役立つSSSE3命令のセットがあります。 DNNが使用されるすべての場所でこのような最適化を実装すると、音声認識の品質が向上し、IAプラットフォームの機能を完全に明らかにすることができます。



All Articles