【AI 全棧 SOTA 綜述 】這些你都不知道,怎麼敢說會 AI?【語音識別原理 + 實戰】

聲網Agora發表於2021-10-29

【AI全棧SOTA綜述 】這些你都不知道,怎麼敢說會AI?【語音識別原理+實戰】

章目錄

前言
語音識別原理
    訊號處理,聲學特徵提取
    識別字元,組成文字
    聲學模型
    語言模型
    詞彙模型
語音聲學特徵提取:MFCC和LogFBank演算法的原理
實戰一 ASR語音識別模型
        系統的流程
        基於HTTP協議的API介面
        客戶端
        未來
實戰二 調百度和科大訊飛API
實戰三 離線語音識別 Vosk

前言

img

語音識別原理

img

首先是語音任務,如語音識別和語音喚醒。聽到這些,你會想到科大訊飛、百度等中國的平臺。因為這兩家公司佔據了中國 80% 的語音市場,所以他們做得非常好。但是由於高精度的技術,他們不能開源,其他公司不得不花很多錢購買他們的 API,但是語音識別和其他應用很難學習(我培訓了一個語音識別專案,10 個圖形卡需要執行 20 天),這導致了民間語音識別的發展緩慢。陳軍收集了大量 SOTA 在當前領域的原理和實戰部分。今天讓我們大飽眼福吧!

語音取樣

在語音輸入後的數字化過程中,首先要確定語音的起始和結束,然後進行降噪和濾波(除人聲外還有許多噪聲),以保證計算機能夠識別濾波後的語音資訊。為了進一步處理,還需要對音訊訊號幀進行處理。同時,從微觀的角度來看,人們的語音訊號一般在一段時間內是相對穩定的,這就是所謂的短期平穩性,因此需要對語音訊號進行幀間處理,以便於處理。

通常一幀需要 20~50ms,幀間存在重疊冗餘,避免了幀兩端訊號的弱化,影響識別精度。接下來是關鍵特徵提取。由於對原始波形的識別不能達到很好的識別效果,需要通過頻域變換提取特徵引數。常用的變換方法是提取 MFCC 特徵,並根據人耳的生理特性將每幀波形變換為原始波形向量矩陣。

img

逐幀的向量不是很直觀。您也可以使用下圖中的頻譜圖來表示語音。每列從左到右是一個 25 毫秒的塊。與原始聲波相比,從這類資料中尋找規律要容易得多。

img

然而,頻譜圖主要用於語音研究,語音識別還需要逐幀使用特徵向量。

識別字元,組成文字

特徵提取完成後,進行特徵識別和字元生成。這一部分的工作是從每一幀中找出當前的音位,然後從多個音位中構詞,再從詞中構詞。當然,最困難的是從每一幀中找出當前的音素,因為每一幀都少於一個音素,而且只有多個幀才能形成一個音素。如果一開始是錯的,以後很難糾正。如何判斷每一幀屬於哪個音素?最簡單的方法是概率,哪個音素的概率最高。如果每幀中多個音素的概率是相同的呢?畢竟,這是可能的。每個人的口音、說話速度和語調都不一樣,人們很難理解你說的是你好還是霍爾。然而,語音識別的文字結果只有一個,人們不可能參與到糾錯的選擇中。此時,多個音素構成了單詞的統計決策,單詞構成了文字

img

這允許我們得到三個可能的轉錄-“你好”,“呼啦”和“奧洛”。最後,根據單詞的概率,我們會發現 hello 是最有可能的,所以我們輸出 hello 的文字。上面的例子清楚地描述了概率如何決定從幀到音素,然後從音素到單詞的一切。如何獲得這些概率?我們能數一數人類幾千年來所說的所有音素、單詞和句子,以便識別一種語言,然後計算概率嗎?這是不可能的。我們該怎麼辦?那我們需要模型:

聲學模型

cv 君相信大家一定知道是麼是聲學模型~ 根據語音的基本狀態和概率,嘗試獲取不同人群、年齡、性別、口音、說話速度的語音語料,同時嘗試採集各種安靜、嘈雜、遙遠的語音語料來生成聲學模型。為了達到更好的效果,不同的語言和方言會採用不同的聲學模型來提高精度,減少計算量。

語言模型

然後對基本的語言模型,單詞和句子的概率,進行大量的文字訓練。如果模型中只有“今天星期一”和“明天星期二”兩句話,我們只能識別這兩句話。如果我們想識別更多的句子,我們只需要覆蓋足夠的語料庫,但是模型會增加,計算量也會增加。所以我們實際應用中的模型通常侷限於應用領域,如智慧家居、導航、智慧音響、個人助理、醫療等,可以減少計算量,提高精度,

詞彙模型

最後,它還是一個比較常用的詞彙模型,是對語言模型的補充,是一個語言詞典和不同發音的註釋。例如,地名、人名、歌曲名、熱門詞彙、某些領域的特殊詞彙等都會定期更新。目前,已有許多簡化但有效的計算方法,如 HMM 隱馬爾可夫模型。隱馬爾可夫模型主要基於兩個假設:一是內部狀態轉移只與前一狀態相關,二是輸出值只與當前狀態(或當前狀態轉移)相關。簡化了問題,也就是說,一個句子中一個詞序列的概率只與前一個詞相關,因此計算量大大簡化。

img

最後,將語音識別為文字。語音聲學特徵提取:MFCC 和 logfbank 演算法原理

幾乎所有的自動語音識別系統,第一步都是提取語音訊號的特徵。通過提取語音訊號的相關特徵,有助於識別出相關的語音資訊,並將背景噪聲、情感等無關資訊剔除。

img

1 MFCC

剛剛 cv 君說到了 MFCC, 這個很經典哦~ MFCC 的全稱是“梅爾頻率倒譜系數”,這語音特徵提取演算法是這幾十年來,常用的演算法之一。這演算法通過在聲音訊率中,對非線性梅爾的對數能量頻譜,線性變換得到的。

1.1 分幀

由於儲存在計算機硬碟中的原始 wav 音訊檔案是可變長度的,我們首先需要將其切割成幾個固定長度的小塊,即幀。根據語音訊號變化快的特點,每幀的時長一般取 10-30ms,以保證一幀中有足夠的週期,且變化不會太劇烈。因此,這種傅立葉變換更適合於平穩訊號的分析。由於數字音訊的取樣率不同,每個幀向量的維數也不同。

1.2 預加重

由於人體聲門發出的聲音訊號有 12dB/倍頻程衰減,而嘴脣發出的聲音訊號有 6dB/倍頻程衰減,因此經過快速傅立葉變換後的高頻訊號中成分很少。因此,語音訊號預加重操作的主要目的是對每幀語音訊號的高頻部分進行增強,從而提高高頻訊號的解析度。

1.3 加窗

在之前的成幀過程中,一個連續的語音訊號被直接分割成若干段,由於截斷效應會導致頻譜洩漏。開窗操作的目的是消除每幀兩端邊緣的短時訊號不連續問題。在 MFCC 演算法中,窗函式通常是 Hamming 窗、矩形窗和 Hanning 窗。需要注意的是,在開窗之前必須進行預強調。

1.4 快速傅立葉變換

經過以上一系列的處理,我們仍然得到時域訊號,而在時域中可以直接獲得的語音資訊量較少。在語音訊號的進一步特徵提取中,需要將每一幀的時域訊號轉換為其頻域訊號。對於儲存在計算機中的語音訊號,我們需要使用離散傅立葉變換。由於普通離散傅立葉變換計算複雜度高,通常採用快速傅立葉變換來實現。由於 MFCC 演算法是分幀的,每一幀都是一個短時域訊號,所以這一步又稱為短時快速傅立葉變換。

1.5 計算幅度譜(對複數取模)

完成快速傅立葉變換後,語音特徵是一個復矩陣,它是一個能譜。由於能譜中的相位譜包含的資訊非常少,我們一般選擇丟棄相位譜,保留幅度譜。

丟棄相位譜保留幅度譜一般有兩種方法,分別是求每個複數的絕對值或平方值。

1.6 Mel 濾波

img

Mel 濾波的過程是 MFCC 的關鍵之一。Mel 濾波器是由 20 個三角形帶通濾波器組成的,將線性頻率轉換為非線性分佈的 Mel 頻率。

2 logfBank

對數銀行特徵提取演算法類似於 MFCC 演算法,是基於對數銀行的特徵提取結果進行處理。但是 logfBank 和 MFCC 演算法的主要區別在於是否進行離散餘弦變換。

隨著 DNN 和 CNN 的出現,特別是深度學習的發展,神經網路可以更好地利用 fBank 和 logfBank 特徵之間的相關性來提高最終語音識別的準確性,減少 WER,因此可以省略離散餘弦變換的步驟。

SOTA 原理+實戰 1 深度全卷積神經網路 語音識別

近年來,深度學習在人工智慧領域出現,對語音識別也產生了深遠的影響。深度神經網路已經逐漸取代了最初的 HMM 隱馬爾可夫模型。在人類的交流和知識傳播中,大約 70%的資訊來自語音。在未來,語音識別將不可避免地成為智慧生活的重要組成部分,它可以為語音輔助和語音輸入提供必要的基礎,這將成為一種新的人機互動方式。因此,我們需要讓機器理解人的聲音。

語音識別系統的聲學模型採用深度全卷積神經網路,直接以聲譜圖為輸入。在模型的結構上,借鑑了影像識別中的最佳網路配置 VGG。這種網路模型具有很強的表達能力,可以看到很長的歷史和未來的資訊,比 RNN 更健壯。在輸出端,模型可以通過 CTC 方案來完成

語音識別系統的聲學模型採用深度全卷積神經網路,直接以聲譜圖為輸入。在模型的結構上,借鑑了影像識別中的最佳網路配置 VGG。這種網路模型具有很強的表達能力,可以看到很長的歷史和未來的資訊,比 RNN 更健壯。在輸出端,該模型可以與 CTC 方案完美結合,實現整個模型的端到端訓練,直接將聲音波形訊號轉錄成漢語普通話拼音序列。在語言模型上,通過最大熵隱馬爾可夫模型將拼音序列轉換成中文文字。並且,為了通過網路向所有使用者提供服務。特徵提取通過成幀和加窗操作將普通的 wav 語音訊號轉換成神經網路所需的二維頻譜影像訊號

CTC 解碼 在語音進行識別資訊系統的聲學分析模型的輸出中,往往包含了企業大量使用連續不斷重複的符號,因此,我們需要將連續相同的符合合併為同一個符號,然後再通過去除靜音分隔標記符,得到發展最終解決實際的語音學習拼音符號序列。

該語言模型使用統計語言模型,將拼音轉換為最終識別的文字並輸出它。將拼音到文字的本質建模為隱馬爾可夫鏈,具有較高的準確率。下面深度解析程式碼,包會系列~

匯入 Keras 系列。

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

匯入聲學模型預設輸出的拼音的表示大小是 1428,即 1427 個拼音+1 個空白塊。

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

定義 CNN/LSTM/CTC 模型,使用函式式模型,設計輸入層,隱藏層和輸出層。

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='in
                       
  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)

定義 ctc 解碼

  # 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) # 獲取拼音列表

最終做語音識別用的函式,識別一個 wav 序列的語音

  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__'):

main 函式,啟動

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)

原理+實戰二 百度和科大訊飛語音識別

端到端的深度合作學習研究方法我們可以用來進行識別英語或漢語普通話,這是兩種截然不同的語言。因為使用神經網路手工設計整個過程的每個組成部分,端到端的學習使我們能夠處理各種各樣的聲音,包括嘈雜的環境,壓力和不同的語言。我們的方法關鍵是提高我們可以應用的 HPC 技術,以前發展需要數週才能進行完成的實驗,現在在幾天內就可以通過實現。這使得學生我們自己能夠更快地進行迭代,以鑑別出優越的架構和演算法。最後,在資料資訊中心進行使用稱為 Batch Dispatch with GPU 的技術,我們研究表明,我們的系統分析可以同時通過網路線上配置,以低成本部署,低延遲地為大量使用者管理提供一個服務。

端到端語音識別是一個活躍的研究領域,將其用於重新評估 DNN-HMM 的輸出,取得了令人信服的結果。Rnn 編解碼器使用編碼器 rnn 將輸入對映到固定長度向量,而解碼器網路將固定長度向量對映到輸出預測序列。有著自己注意力的 RNN 編碼器 – 解碼器在預測音素教學方面進行表現一個良好。結合 ctc 損失函式和 rnn 對時間資訊進行了模擬,並在字元輸出的端到端語音識別中取得了良好的效果。Ctc-rnn 模型也能很好地預測音素,儘管在這種情況下仍然需要一本詞典。

資料技術也是端到端語音進行識別系統成功的關鍵,Hannun 等人使用了中國超過 7000 小時的標記語言語音。資料增強對於提高計算機視覺和語音識別等深度學習的效能非常有效。現有的語音系統也可以用來引導新的資料收集。我們從以往的方法中得到啟示,引導更大的資料集和資料增加,以增加百度系統中的有效標記資料量。

img

現在的演示是識別音訊檔案的內容。token 獲取見官網,這邊調包沒什麼含金量 Python 技術篇-百度進行語音 API 鑑權認證資訊獲取 Access Token 注:下面的 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

cv 君今天由於篇幅問題,介紹了大量原理和 Sota 演算法,所以現在再最後分享一個,需要了解更多,歡迎持續關注本系列。

Vosk 支援 30 多種語言,並且現在做的不錯,在離線語音裡面不錯了,https://github.com/alphacep/v...

帶 Android python,c++ 的 pc 版本,等等 web 部署方案 Android 的話,就需要你安裝 Android 包,然後還要下載編譯工具,gradle,通過 Gradle 等方式編譯。

即可編譯,編譯成功後會生成 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 語言等部署,歡迎諮詢 cv 君。

img

智慧語音互動圖

總結

今天說了很多,歡迎各位看官賞臉觀看,這篇文章較多地在介紹 Tricks,互動性和趣味性在後面實戰部分~而且又是語音的演算法,今天沒法給大家演示很多有趣的。

後文,可以給大家由淺入深地進階語音部分的其他領域部分:

一:諸如 Siri ,小愛同學這樣的喚醒詞演算法和模型和 SOTA;

二: 說話人區別(鑑別思想)的 SOTA

三:多語種思路+少語種+困難語種思路和 SOTA

四:各個語音比賽 SOTA 方案

相關文章