小編這一段時間研究端到端的實現中文語音的識別,項目主體代碼使用了https://github.com/SeanNaren/deepspeech.pytorch/
的方案,不同的是這個模型主要為英文設計,在中文識別上可能需要做出一些變化,不僅涉及到數據集語料庫,還涉及到部分業務邏輯的修改,下面根據數據結構的變化為時間線詳細說一下。
整理好了思路和模塊之間耦合的細節,按照這個思路自己去實現一套語音識別項目也是可以的。
這個項目在github上面版本更新比較頻繁,優化的地方也有很多,2020年初到現在配置方式從傳統的
argparse.ArgumentParser()
變成了獨立的配置模塊,不得不說相當優雅了,松耦合使得代碼的調試過程避免了很多bug。
說一下整體的思路吧:、
一般語音識別工作相當于是把“語音特征數據”和“文字、詞”關聯起來了,需要我們的模型認識每一個字,這些字組合起來還要像是一句話,第一種方案:聲學模型+語言模型,聲學模型做的關聯是“特征數據”---“音素”,語言模型做的是“音素”---“句子”,語言模型讓這些識別結果看起來更像人話;第二種方案:端到端,也就是“特征數據”---“一句話”,這一般需要編解碼工具的輔助,本項目中涉及到的有greedyDecoder和beamDecoder,beamDecoder中可以加入語言模型的輔助,項目支持kenlm類型的語言模型(實際上他并不算是一種語言模型,他把N-gram語言模型進行了包裝,只不過是添加了更強的搜索算法,可以提高項目的效率)
1、關于語音識別工作,數據始終是我們不能忽略的,這是我們設計模型結構和業務邏輯的重點。在語音識別的任務中,第一步就是提取特征,什么是提取特征呢?就是把原始的音頻文件比如wav轉換成矩陣數據,一般數據的size大小和音頻的長度近乎成正比。常用的特征提取方法是:MFCC、FBANK、LPC,這個項目中使用的方法類似于fbank,具體的實現代碼如下:
import librosa
import soundfile as sf
import numpy as np
def load_audio(path):
sound, sample_rate = sf.read(path, dtype='int16')
sound = sound.astype('float32') / 32767 # normalize audio
if len(sound.shape) > 1:
if sound.shape[1] == 1:
sound = sound.squeeze()
else:
sound = sound.mean(axis=1) # multiple channels, average
return sound
audio_path = 'wav音頻路徑'
y = load_audio(audio_path)
n_fft = int(16000*.02) # 16000是音頻的頻率,02是傅里葉變換的窗口
win_length = n_fft
hop_length = int(16000*.01)
# STFT
D = librosa.stft(y, n_fft=n_fft, hop_length=hop_length,
win_length=win_length, window='hamming')
spect, phase = librosa.magphase(D)
spect = np.log1p(spect)
spect 就是最后提取的數據特征了,可以直接作為一個item輸入到model中。
我用一個3s的wav文件得到的數據size是(161,844),這里161是不會變的,更長的音頻會使得844這個位置的維度變大,這和其他的特征提取方法可能不太一樣,我們這里就使用161*1000來舉栗子吧。
這里要提到的一點是一般訓練的數據都會采用短音頻控制在10s之內是最好,過長的音頻會挑戰機器的內存,有可能會爆掉。
2、看一下項目的配置參數,剛開始接觸深度學習的項目,參數有很多,這個項目里面把參數分成了兩種:訓練參數、推理參數,其實我們并不知道這些參數是干嘛的,下面分析一下吧
訓練參數:
(1)epochs,我們輸入的數據集里面有很多條數據,一條(item)這里就是一個短音頻對應的數據特征,一個epoch就相當于把所有的數據(按照一定的排列方式和采樣方式)從模型里面訓練過了一遍,有多少個epoch就過了多少遍,一般的可以設置成30,50,70,100,看數據量大小,也看模型自己收斂不收斂,這個可以在train的過程中判斷,如果發現模型已經長時間沒有準確率上的提高,就可以設置停止當前epoch的訓練;
(2)seed,在訓練的過程中數據的排列和生成涉及到很多隨機的過程,如果先設置了seed,就會讓每一次隨機到的內容都一樣,就會讓每次訓練的結果都一樣,可以根據自己的需要選擇,他真的影響的代碼是下面這一句:
# Set seeds for determinism
torch.manual_seed(cfg.training.seed)
torch.cuda.manual_seed_all(cfg.training.seed)
np.random.seed(cfg.training.seed)
random.seed(cfg.training.seed)
這些在訓練的最開始就要設置好
(3)batch_size模型的訓練是在一個循環中,代碼大概是下面這樣的
for epoch in range(epoch):
for batch_data in train_dataset:
output = model(batch_data)
看代碼的時候可以先抓住這幾句重要的邏輯,其他的代碼多數是在處理數據的格式和形狀,在模型應用(也可以叫推理)的過程中一般是一個item為單位進行預測的,但是訓練的過程中并不是(這么多數據要循環到什么時候去),這里輸入數據的形狀是batch_size(這里選16吧,比較常見,太大也會爆炸)×1×161×1000,這里的第二維度加了1(一般的彩色圖像是三維的,也就是三層二維矩陣,相當于一層圖像數據,因為后面要做CNN卷積,需要一個平面的多通道的數據)
(4)train_manifest val_manifest
這些叫做清單文件,也可以說成數據集的一種形式,是csv文件,每一行有兩項數據,第一個是wav文件的地址,第二項是對應語音內容的地址一般為txt或者trn格式
(5)window_size window_stride
這里說的是我們1、中提到的語音特征提取步驟的窗口大小,按照默認的.02就挺好,一般在數據處理的時候窗口尺寸和窗口重疊是成對出現的,因為兩個窗口關聯性也就是重疊的區域大小一般可以為窗口建立關聯性,可以提高數據分析的連續性,避免窗口邊緣的數據和下一個窗口的數據產生關聯,而我們設置的窗口割裂了他們的關系。
(6)no_cuda,不適用cuda也就是GPU啦,這一般是False,GPU又好又快,有條件的還是用一下了,不過在數據輸入到模型之前會需要把數據和定義好的model也轉化成gpu類型的,比如在torch里面這樣:
device = torch.device("cuda")
inputs = inputs.to(device)
model = model.to(device)
(7)hidden_layers,hidden_size,這兩個參數都是跟模型的形狀相關的,不過不影響,不直接影響我們的數據,設置得合理一點就可以了,先介紹一下模型的內部構造吧:
CNN(卷積層)--RNN(lstm循環神經網絡)--LINEAR(全連接層,也叫做線性層)--SOFTMAX
我們這個hidden_size出現在RNN和LINEAR之間,最后會被乘積的操作消掉,這里設置的1024,我覺得還可以。hideen_layer指的是RNN有幾層,一般層數多一點5,8,10會讓RNN的表現更好。
(8)learning_anneal在訓練的過程中,每一個epoch都會調整自己的參數以及學習率,預測結果和目標結果(也就是label)發生同樣的偏差的時候,學習率越高,網絡參數的調整幅度就越大。這個是在模型發現自己矯枉過正的時候,就會衰減一下自己的學習率
新的學習率=舊的學習率/learning_anneal
lr = lr/ learning_anneal
(9)optim優化器的類型,是可以自己選的,我這里選擇了Adam,優化器綜合學習率、網絡模型參數、反向傳播(修正網絡模型參數的方法)權重衰減的數據對網絡模型的參數進行優化
優化器等的使用過程一般是這樣的(pytorch中)
# 訓練前
optim = torch.optim.Adam(model.paramters(), lr=lr)
# 訓練中 epoch內
output = model(input)
loss = f(output, target)
optim.zero_grad()
loss.backward()
optim.step()
(10)損失函數的定義,CTCLoss
順便說一下損失函數,這里也涉及到數據尺寸的分析。損失函數使用了CTCLoss,在將訓練數據輸入到模型中時,僅把label數據(也就是文本內容的變體)處理到字符的index形式即可,同時,每一個句子文本的長度都不一樣,所以一個batch中的數據shape可能不一致,比如在deepspeech項目train.py中,下面這個具體的情況:
inputs = inputs.to(device)
out, output_sizes = model(inputs, input_sizes)
out = out.transpose(0, 1) # TxNxH
float_out = out.float() # ensure float32 for loss
loss = criterion(float_out, targets, output_sizes, target_sizes).to(device)
loss = loss / inputs.size(0) # average the loss by minibatch
float_out: torch.Size([426, 16, 8679]), '426'不定,每句話都不一樣
targets: torch.Size([528]), '528'不定,每句話都不一樣
訓練數據的targets并沒有轉化成[0,0,0,1,0,0,0...,0]這樣的形式,仍然是字符索引的int
targets是把一個batch的索引都拼在了一起,形成一個長的List
targets格式形如: [s1c1,s1c2,s1c3,...,s1c10,s2c1,s2c2,...,s16c13],s是句子c是句子中字符
在數據shape不同的情況下直接交給 CTCLoss() 損失函數處理
這篇先分析到這里,下一篇繼續吧?(^_-)