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

語言: CN / TW / HK

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

語音識別原理

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

語音採樣

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

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

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

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

識別字符,組成文本

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

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

聲學模型

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

語言模型

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

詞彙模型

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

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

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

剛剛 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 濾波

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 小時的標記語言語音。數據增強對於提高計算機視覺和語音識別等深度學習的性能非常有效。現有的語音系統也可以用來引導新的數據收集。我們從以往的方法中得到啟示,引導更大的數據集和數據增加,以增加百度系統中的有效標記數據量。

現在的演示是識別音頻文件的內容。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/vosk-api

帶 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 君。

智能語音交互圖

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

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

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

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

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

四:各個語音比賽 SOTA 方案