作者 | Comet
譯者 | 天道酬勤,責編 | Carol
出品 | AI 科技大本營 (ID:rgznai100)
這篇文章是由AssemblyAI的機器學習研究工程師Michael Nguyen撰寫的。AssemblyAI使用Comet記錄、可視化和了解模型開發流程。
Deep Speech使用連接時態分類(CTC)損失函數來預測語音記錄。LAS使用序列對網絡架構進行預測。
深度學習是一個快速發展的領域,而Deep Speech和LAS風格的體系結構已經過時。你可以在下面的「最新進展」部分中了解該行業的發展方向。
模型的輸出是字符的機率矩陣,我們使用該機率矩陣來解碼音頻中最有可能出現的字符。你可以找到完整的代碼,還可以在Google Colaboratory上的GPU支持下運行它。
準備數據管道
你可以在這篇優秀的文章中閱讀更多關於這種轉變的細節。對於本文,你可以將Mel頻譜圖視為聲音的圖片。
為了處理音頻數據,我們將使用一個非常有用的工具,被稱為torchaudio,它是PyTorch團隊專門為音頻數據創建的一個庫。我們將在LibriSpeech的一個子集上進行訓練,該子集是從有聲讀物中獲得的閱讀英語語音數據的語料庫,包括100個小時的轉錄音頻數據。你可以使用torchaudio輕鬆下載此數據集:
importtorchaudio train_dataset = torchaudio.datasets.LIBRISPEECH( "./", url= "train-clean-100", download= True)
test_dataset = torchaudio.datasets.LIBRISPEECH( "./", url= "test-clean", download= True)
數據集的每個樣本都包含波形、音頻採樣率、話語/標籤,以及樣本上更多的元數據。你可以在此處從原始碼中查看每個示例。
數據擴充– SpecAugment
在PyTorch中,你可以使用torchaudio函數FrequencyMasking來掩蓋頻率維度,並使用TimeMasking來度量時間維度。
torchaudio.transforms.FrequencyMasking
torchaudio.transforms.TimeMasking
有了數據後,我們需要將音頻轉換為Mel頻譜圖,並將每個音頻樣本的字符標籤映射為整數標籤:
classTextTransform:
"""Maps characters to integers and vice versa"""
def__init__(self):
char_map_str = """
' 0
a 2
b 3
c 4
d 5
e 6
f 7
g 8
h 9
i 10
j 11
k 12
l 13
m 14
n 15
o 16
p 17
q 18
r 19
s 20
t 21
u 22
v 23
w 24
x 25
y 26
z 27
"""
self.char_map = {}
self.index_map = {}
forline inchar_map_str.strip.split( 'n'):
ch, index = line.split
self.char_map[ch] = int(index)
self.index_map[int(index)] = ch
self.index_map[ 1] = ' '
deftext_to_int(self, text):
""" Use a character map and convert text to an integer sequence """
int_sequence = []
forc intext:
ifc == ' ':
ch = self.char_map[ '']
else:
ch = self.char_map[c]
int_sequence.append(ch)
returnint_sequence
defint_to_text(self, labels):
""" Use a character map and convert integer labels to an text sequence """
string = []
fori inlabels:
string.append(self.index_map[i])
return''.join(string).replace( '', ' ')
train_audio_transforms = nn.Sequential(
torchaudio.transforms.MelSpectrogram(sample_rate= 16000, n_mels= 128),
torchaudio.transforms.FrequencyMasking(freq_mask_param= 15),
torchaudio.transforms.TimeMasking(time_mask_param= 35)
)
valid_audio_transforms = torchaudio.transforms.MelSpectrogram
text_transform = TextTransform
defdata_processing(data, data_type= "train") :
spectrograms = []
labels = []
input_lengths = []
label_lengths = []
for(waveform, _, utterance, _, _, _) indata:
ifdata_type == 'train':
spec = train_audio_transforms(waveform).squeeze( 0).transpose( 0, 1)
else:
spec = valid_audio_transforms(waveform).squeeze( 0).transpose( 0, 1)
spectrograms.append(spec)
label = torch.Tensor(text_transform.text_to_int(utterance.lower))
labels.append(label)
input_lengths.append(spec.shape[ 0]// 2)
label_lengths.append(len(label))
spectrograms = nn.utils.rnn.pad_sequence(spectrograms, batch_first= True).unsqueeze( 1).transpose( 2, 3)
labels = nn.utils.rnn.pad_sequence(labels, batch_first= True)
returnspectrograms, labels, input_lengths, label_lengths
定義模型-DeepSpeech 2
我們的模型將類似於Deep Speech 2結構。該模型將具有兩個主要的神經網絡模塊——學習相關的音頻特徵的N層殘差卷積神經網絡(ResCNN),以及利用學習後的ResCNN音頻特徵的一組雙向遞歸神經網絡(BiRNN)。該模型的頂部是一個全連通層,用於按時間步長對字符進行分類。
添加這些殘差連接有助於模型更快地學習和更好地推廣。這篇可視化神經網絡的損失圖景的論文表明,具有殘留連接的網絡具有一個「平坦的」損失面,使模型更容易描繪損失狀況,並找到一個更低且更通用的最小值。
遞歸神經網絡(RNN)擅長處理序列建模問題。RNN會逐步處理音頻特徵,在使用前一幀的上下文的同時對每一幀進行預測。我們使用BiRNN是因為我們不僅需要每個步驟之前框架的上下文,還希望得到它之後框架的上下文。
這可以幫助模型做出更好的預測,因為音頻中的每一幀在進行預測之前都會有更多信息。我們使用RNN的門控遞歸單元(GRU)變種,因為它比LSTM需要的的計算資源更少,並且在某些情況下工作效果也一樣。
該模型為輸出字符的機率矩陣,我們將使用該矩陣將其輸入到解碼器中,提取模型認為是機率最高的字符。
classCNNLayerNorm(nn.Module):
"""Layer normalization built for cnns input"""
def__init__(self, n_feats):
super(CNNLayerNorm, self).__init__
self.layer_norm = nn.LayerNorm(n_feats)
defforward(self, x):
# x (batch, channel, feature, time)
x = x.transpose( 2, 3).contiguous # (batch, channel, time, feature)
x = self.layer_norm(x)
returnx.transpose( 2, 3).contiguous # (batch, channel, feature, time)
classResidualCNN(nn.Module):
"""Residual CNN inspired by https://arxiv.org/pdf/1603.05027.pdf
except with layer norm instead of batch norm
"""
def__init__(self, in_channels, out_channels, kernel, stride, dropout, n_feats):
super(ResidualCNN, self).__init__
self.cnn1 = nn.Conv2d(in_channels, out_channels, kernel, stride, padding=kernel// 2)
self.cnn2 = nn.Conv2d(out_channels, out_channels, kernel, stride, padding=kernel// 2)
self.dropout1 = nn.Dropout(dropout)
self.dropout2 = nn.Dropout(dropout)
self.layer_norm1 = CNNLayerNorm(n_feats)
self.layer_norm2 = CNNLayerNorm(n_feats)
defforward(self, x):
residual = x # (batch, channel, feature, time)
x = self.layer_norm1(x)
x = F.gelu(x)
x = self.dropout1(x)
x = self.cnn1(x)
x = self.layer_norm2(x)
x = F.gelu(x)
x = self.dropout2(x)
x = self.cnn2(x)
x += residual
returnx # (batch, channel, feature, time)
classBidirectionalGRU(nn.Module):
def__init__(self, rnn_dim, hidden_size, dropout, batch_first):
super(BidirectionalGRU, self).__init__
self.BiGRU = nn.GRU(
input_size=rnn_dim, hidden_size=hidden_size,
num_layers= 1, batch_first=batch_first, bidirectional= True)
self.layer_norm = nn.LayerNorm(rnn_dim)
self.dropout = nn.Dropout(dropout)
defforward(self, x):
x = self.layer_norm(x)
x = F.gelu(x)
x, _ = self.BiGRU(x)
x = self.dropout(x)
returnx
classSpeechRecognitionModel(nn.Module):
"""Speech Recognition Model Inspired by DeepSpeech 2"""
def__init__(self, n_cnn_layers, n_rnn_layers, rnn_dim, n_class, n_feats, stride= 2, dropout= 0.1) :
super(SpeechRecognitionModel, self).__init__
n_feats = n_feats// 2
self.cnn = nn.Conv2d( 1, 32, 3, stride=stride, padding= 3// 2) # cnn for extracting heirachal features
# n residual cnn layers with filter size of 32
self.rescnn_layers = nn.Sequential(*[
ResidualCNN( 32, 32, kernel= 3, stride= 1, dropout=dropout, n_feats=n_feats)
for_ inrange(n_cnn_layers)
])
self.fully_connected = nn.Linear(n_feats* 32, rnn_dim)
self.birnn_layers = nn.Sequential(*[
BidirectionalGRU(rnn_dim=rnn_dim ifi== 0elsernn_dim* 2,
hidden_size=rnn_dim, dropout=dropout, batch_first=i== 0)
fori inrange(n_rnn_layers)
])
self.classifier = nn.Sequential(
nn.Linear(rnn_dim* 2, rnn_dim), # birnn returns rnn_dim*2
nn.GELU,
nn.Dropout(dropout),
nn.Linear(rnn_dim, n_class)
)
defforward(self, x):
x = self.cnn(x)
x = self.rescnn_layers(x)
sizes = x.size
x = x.view(sizes[ 0], sizes[ 1] * sizes[ 2], sizes[ 3]) # (batch, feature, time)
x = x.transpose( 1, 2) # (batch, time, feature)
x = self.fully_connected(x)
x = self.birnn_layers(x)
x = self.classifier(x)
returnx
選擇合適的優化器和調度器–具有超融合的AdamW
優化器和學習率調度器在使模型收斂到最佳點方面起著非常重要的作用。選擇合適的的優化器和調度器還可以節省計算時間,並有助於你的模型更好應用到實際案例中。
對於我們的模型,我們將使用AdamW和一個周期學習率調度器。Adam是一種廣泛使用的優化器,可幫助你的模型更快地收斂,節省計算時間,但由於沒有推廣性,和隨機梯度下降(SGD)一樣臭名昭著。
AdamW最初是在「去耦權重衰減正則化」中引入的,被認為是對Adam的「修復」。該論文指出,原始的Adam算法權重衰減的實現上存在錯誤,AdamW試圖解決該問題。這個修復程序有助於解決Adam的推廣問題。
單周期學習率調度算法最早是在《超收斂:大學習率下神經網絡的快速訓練》一文中引入的。本文表明,你可以使用一個簡單的技巧,在保持其可推廣能力的同時,將神經網絡的訓練速度提高一個數量級。
開始時學習率很低,逐漸上升到一個很大的最大學習率,然後線性衰減到最初開始時的位置。
最大學習率比最低學習率要高很多,你可以獲得一些正則化好處,如果數據量較小,可以幫助你的模型更好地推廣。
使用PyTorch,這兩種方法已經成為軟體包的一部分。
optimizer = optim.AdamW(model.parameters, hparams[ 'learning_rate'])
scheduler = optim.lr_scheduler.OneCycleLR(optimizer,
max_lr=hparams[ 'learning_rate'],
steps_per_epoch= int( len(train_loader)),
epochs=hparams[ 'epochs'],
anneal_strategy= 'linear')
CTC損失功能–將音頻與文本對齊
我們的模型將接受訓練,預測輸入到模型中的聲譜圖中每一幀(即時間步長)字母表中所有字符的機率分布。
CTC損失功能的創新之處在於它允許我們可以跳過這一步。我們的模型將在訓練過程中學習對齊文本本身。關鍵在於CTC引入的「空白」標籤,該標籤使模型能夠表明某個音頻幀沒有產生字符。你可以在這篇出色的文章中看到有關CTC及其工作原理的更詳細說明。
PyTorch還內置了CTC損失功能。
criterion= nn.CTCLoss(blank= 28).to(device)
語音模型評估
你可以在此處查看它是如何實現。另一個有用的度量標準稱為字符錯誤率(CER)。CER測量模型輸出和真實標籤之間的字符誤差。這些指標有助於衡量模型的性能。
在本教程中,我們使用「貪婪」解碼方法將模型的輸出處理為字符,這些字符可組合創建文本。「貪婪」解碼器接收模型輸出,該輸出是字符的最大機率矩陣,對於每個時間步長(頻譜圖幀),它選擇機率最高的標籤。如果標籤是空白標籤,則將其從最終的文本中刪除。
defGreedyDecoder(output, labels, label_lengths, blank_label= 28, collapse_repeated=True) :
arg_maxes = torch.argmax(output, dim= 2)
decodes = []
targets = []
fori, args inenumerate(arg_maxes):
decode = []
targets.append(text_transform.int_to_text(labels[i][:label_lengths[i]].tolist))
forj, index inenumerate(args):
ifindex != blank_label:
ifcollapse_repeated andj != 0andindex == args[j -1]:
continue
decode.append(index.item)
decodes.append(text_transform.int_to_text(decode))
returndecodes, targets
使用Comet.ml訓練和監測實驗
Comet.ml提供了一個平台,允許深度學習研究人員跟蹤、比較、解釋和優化他們的實驗和模型。Comet.ml提高了AssemblyAI的工作效率,我們強烈建議團隊使用這個平台進行任何類型的數據科學實驗。
Comet.ml非常容易設置。僅需幾行代碼即可工作。
initialize experiment object
experiment = Experiment(api_key=comet_api_key, project_name=project_name)
experiment.set_name(exp_name)
track metrics
experiment.log_metric( 'loss', loss.item)
Comet.ml為你提供了一個非常高效的儀錶板,你可以查看和跟蹤模型的進度。
你可以使用Comet來跟蹤指標、代碼、超參數、模型圖等。Comet提供的一項非常方便的功能,能夠將你的實驗與許多其他實驗進行比較。
Comet具有豐富的功能集,我們在這裡不會全部介紹,但是我們強烈建議您使用它來提高生產率和健全性。
下面是我們訓練腳本的其餘部分。
classIterMeter(object):
"""keeps track of total iterations"""
def__init__(self):
self.val = 0
defstep(self):
self.val += 1
defget(self):
returnself.val
deftrain(model, device, train_loader, criterion, optimizer, scheduler, epoch, iter_meter, experiment):
model.train
data_len = len(train_loader.dataset)
withexperiment.train:
forbatch_idx, _data inenumerate(train_loader):
spectrograms, labels, input_lengths, label_lengths = _data
spectrograms, labels = spectrograms.to(device), labels.to(device)
optimizer.zero_grad
output = model(spectrograms) # (batch, time, n_class)
output = F.log_softmax(output, dim= 2)
output = output.transpose( 0, 1) # (time, batch, n_class)
loss = criterion(output, labels, input_lengths, label_lengths)
loss.backward
experiment.log_metric( 'loss', loss.item, step=iter_meter.get)
experiment.log_metric( 'learning_rate', scheduler.get_lr, step=iter_meter.get)
optimizer.step
scheduler.step
iter_meter.step
ifbatch_idx % 100== 0orbatch_idx == data_len:
print( 'Train Epoch: {} [{}/{} ({:.0f}%)]tLoss: {:.6f}'.format(
epoch, batch_idx * len(spectrograms), data_len,
100.* batch_idx / len(train_loader), loss.item))
deftest(model, device, test_loader, criterion, epoch, iter_meter, experiment):
print( 'nevaluating…')
model.eval
test_loss = 0
test_cer, test_wer = [], []
withexperiment.test:
withtorch.no_grad:
forI, _data inenumerate(test_loader):
spectrograms, labels, input_lengths, label_lengths = _data
spectrograms, labels = spectrograms.to(device), labels.to(device)
output = model(spectrograms) # (batch, time, n_class)
output = F.log_softmax(output, dim= 2)
output = output.transpose( 0, 1) # (time, batch, n_class)
loss = criterion(output, labels, input_lengths, label_lengths)
test_loss += loss.item / len(test_loader)
decoded_preds, decoded_targets = GreedyDecoder(output.transpose( 0, 1), labels, label_lengths)
forj inrange(len(decoded_preds)):
test_cer.append(cer(decoded_targets[j], decoded_preds[j]))
test_wer.append(wer(decoded_targets[j], decoded_preds[j]))
avg_cer = sum(test_cer)/len(test_cer)
avg_wer = sum(test_wer)/len(test_wer)
experiment.log_metric( 'test_loss', test_loss, step=iter_meter.get)
experiment.log_metric( 'cer', avg_cer, step=iter_meter.get)
experiment.log_metric( 'wer', avg_wer, step=iter_meter.get)
print( 'Test set: Average loss: {:.4f}, Average CER: {:4f} Average WER: {:.4f}n'.format(test_loss, avg_cer, avg_wer))
defmain(learning_rate= 5e-4, batch_size= 20, epochs= 10,
train_url= "train-clean-100", test_url= "test-clean",
experiment=Experiment (api_key= 'dummy_key', disabled=True) ) :
hparams = {
"n_cnn_layers": 3,
"n_rnn_layers": 5,
"rnn_dim": 512,
"n_class": 29,
"n_feats": 128,
"stride": 2,
"dropout": 0.1,
"learning_rate": learning_rate,
"batch_size": batch_size,
"epochs": epochs
}
experiment.log_parameters(hparams)
use_cuda = torch.cuda.is_available
torch.manual_seed( 7)
device = torch.device( "cuda"ifuse_cuda else"cpu")
ifnotos.path.isdir( "./data"):
os.makedirs( "./data")
train_dataset = torchaudio.datasets.LIBRISPEECH( "./data", url=train_url, download= True)
test_dataset = torchaudio.datasets.LIBRISPEECH( "./data", url=test_url, download= True)
kwargs = { 'num_workers': 1, 'pin_memory': True} ifuse_cuda else{}
train_loader = data.DataLoader(dataset=train_dataset,
batch_size=hparams[ 'batch_size'],
shuffle= True,
collate_fn= lambdax: data_processing(x, 'train'),
**kwargs)
test_loader = data.DataLoader(dataset=test_dataset,
batch_size=hparams[ 'batch_size'],
shuffle= False,
collate_fn= lambdax: data_processing(x, 'valid'),
**kwargs)
model = SpeechRecognitionModel(
hparams[ 'n_cnn_layers'], hparams[ 'n_rnn_layers'], hparams[ 'rnn_dim'],
hparams[ 'n_class'], hparams[ 'n_feats'], hparams[ 'stride'], hparams[ 'dropout']
).to(device)
print(model)
print( 'Num Model Parameters', sum([param.nelement forparam inmodel.parameters]))
optimizer = optim.AdamW(model.parameters, hparams[ 'learning_rate'])
criterion = nn.CTCLoss(blank= 28).to(device)
scheduler = optim.lr_scheduler.OneCycleLR(optimizer, max_lr=hparams[ 'learning_rate'],
steps_per_epoch=int(len(train_loader)),
epochs=hparams[ 'epochs'],
anneal_strategy= 'linear')
iter_meter = IterMeter
forepoch inrange( 1, epochs + 1):
train(model, device, train_loader, criterion, optimizer, scheduler, epoch, iter_meter, experiment)
test(model, device, test_loader, criterion, epoch, iter_meter, experiment)
訓練功能可在整個數據周期內訓練模型。在每個時期之後,測試功能都會根據測試數據評估模型。它獲取test_loss以及模型的cer和wer。你現在可以在Google合作實驗室的GPU支持下開始運行訓練腳本。
如何提高準確性
提高準確性的另一種方法是使用語言模型和CTC波束搜索算法對CTC機率矩陣進行解碼。CTC類型模型非常依賴此解碼過程來獲得良好的結果。這裡有一個方便的開源庫允許你這樣做。
本教程的使用範圍更廣,與BERT(3.4億個參數)相比,它是一個相對較小的模型(2300萬個參數)。儘管收益遞減,但似乎你的網絡規模越大,它的性能就越好。正如OpenAI的研究「 Deep Double Descent」證明的那樣,一個更大的模型並不總是等同於更好的性能。
該模型具有3個CNN殘差層和5個雙向GRU層,允許你在具有至少11GB內存的單個GPU上訓練合理的批處理大小。你可以調整main函數中的一些超級參數,減少或增加你的用例和計算可用性的模型大小。
轉換器
轉換器席捲了自然語言處理世界。首先在論文中介紹了「無可或缺的注意力」,轉換器已經出現和修改,幾乎擊敗所有現有的NLP任務,取代了RNN的類型體系結構。轉換器查看序列數據完整上下文的能力也可以轉轉移到語音中。
無人監督的預訓練
如果你密切關注深度學習,你可能聽說過BERT,GPT和GPT2。這些Transformer模型首先用於使用未標記文本數據的語言建模任務,並在各種NLP任務上進行了微調,獲得了最新的結果。在預訓練期間,該模型學習了一些語言統計方面的基礎知識,並利用該能力在其他任務上表現出色。我們相信這項技術在語音數據方面也具有廣闊的前景。
詞塊模型
我們的模型在上面定義了輸出字符。這樣做的一些好處是,在進行語音推理時,模型不必擔心詞彙量不足。對於單詞c h a t,每個字符都有自己的標籤。使用字符的缺點是效率低,由於你一次只能預測一個字符,該模型更容易出現錯誤。
使用整個單詞作為標籤已經探索了,在一定程度上取得了成功。使用這種方法,整個單詞chat將成為標籤。如果使用整個單詞,你就必須對所有可能的詞彙進行索引來才能進行預測,這會使內存效率低,在預測過程中可能會遇到詞彙量不足的情況。最有效的方法是使用單詞片段或子單詞單位作為標籤。
原文連結:https://hackernoon.com/building-an-end-to-end-speech-recognition-model-in-pytorch-with-assemblyai-5o8s3yry
本文由 AI 科技大本營翻譯,轉載請註明出處。
文章來源: https://twgreatdaily.com/zh-sg/AVgqr3IBd4Bm1__YFeru.html