1. 程式人生 > 其它 >語音轉換 resnet_CCF音訊分類:語音指令識別練習

語音轉換 resnet_CCF音訊分類:語音指令識別練習

技術標籤:語音轉換 resnet

最近在關注Rainforest Connection Species Audio Detection,然後看到CCF的練習專案,藉此機會學習一下音訊相關的處理方案。

github倉庫連結:https://github.com/blueloveTH/speech_commands_recognition

任務簡介

訓練集有57886條,測試集有6835條,每條資料都是一段1秒左右的語音,其中包含一個單詞,一共有30種可能,因此是一個30分類任務。

還有一位同學對賽題進行了更詳細的介紹,對任務還不太理解的朋友,我強烈建議你看一下↓

2020CCFBDCI通用音訊分類CNN方案(0.90+方案)_wherewegogo的部落格-CSDN部落格

特徵工程

特徵部分採用1x32x32的對數梅爾頻譜圖(Log-Melspectrogram),關於它的介紹網上已有很多資料。這是一種很常規的特徵,幾乎所有音訊任務都有用到。

nn類方法通常不用MFCC,因為MFCC是頻譜圖經有損變換得到的,去除了相關性。而nn具有強大的端到端學習能力,直接使用原始頻譜圖能提取出更多資訊。(事實上,直接喂16000維的原始波形也能達到90+準確率)

librosa包提供了melspec的實現,下面是提取特徵的核心程式碼。先將所有音訊擷取到定長,即1秒16000次取樣,再用librosa提取頻譜圖,最後轉換到對數域,以放大低頻部分的資訊。

def crop_or_pad(y, length=16000):
    if len(y) < length:
        y = np.concatenate([y, np.zeros(length - len(y))])
    elif len(y) > length:
        y = y[: length]
    return y

def extract_features(wav):
    wav = crop_or_pad(wav)
    melspec = librosa.feature.melspectrogram(wav, sr=16000, n_mels=32)
    return librosa.power_to_db(melspec)

歸一化

由於缺乏對頻譜圖意義的理解,查閱文章 How to normalize spectrograms ,其探討了頻譜圖應該如何進行歸一化的問題。方法有下列幾種:

  • 不歸一化
  • 逐樣本歸一化
  • 逐通道歸一化
  • 全域性歸一化

本文測試了不歸一化、逐樣本和全域性歸一化的效果,後二者略好一點。github倉庫中用的是全域性歸一化,即預計算訓練集上的均值和方差,如下:

mean, std = -34.256863, 17.706715
def normalize(x):
    return (x - mean) / std

模型架構(resnet)

c6aaf55512c9ebeac83adc6cc1fa31ab.png

本文使用了pytorch配合keras4torch搭建模型和訓練管線。

keras4torch是我們正在開發的pypi包,它是keras介面的一個子集,後端基於pytorch,其程式碼對於torch使用者和keras使用者都較容易理解。詳細介紹↓

https://github.com/blueloveTH/keras4torch​github.com

殘差塊定義

首先是BN+ReLU+Conv2d的三連

import keras4torch as k4t
import torch.nn as nn

def bn_relu_conv(channels, stride):
    return nn.Sequential(
        k4t.layers.BatchNorm2d(),
        nn.ReLU(inplace=True),
        k4t.layers.Conv2d(channels, kernel_size=3, stride=stride, padding=1, bias=False)
    )

這裡使用了keras4torch提供的Conv2d,它是torch.nn.Conv2d的封裝,實現了自動尺寸推斷,無需設定輸入通道數。

下面是殘差連線。

class _ResidualBlock(nn.Module):
    def __init__(self, in_channels, out_channels, stride):
        super(_ResidualBlock, self).__init__()
        self.sequential = nn.Sequential(
            bn_relu_conv(out_channels, stride=stride),
            bn_relu_conv(out_channels, stride=1)
        )

        self.equalInOut = (in_channels == out_channels)

        if not self.equalInOut:
            self.conv_shortcut = k4t.layers.Conv2d(out_channels, kernel_size=1, stride=stride, padding=0, bias=False)

    def forward(self, x):
        if not self.equalInOut:
            return self.conv_shortcut(x) + self.sequential(x)
        else:
            return x + self.sequential(x)

如果輸入和輸出通道不一致,我們需要一個額外的線性卷積層使它們的通道數相同。

這一步操作判斷了輸入通道數,因此繼承KerasLayer以獲取in_shape,從而使殘差塊具有自動尺寸推斷的特性。

class ResidualBlock(k4t.layers.KerasLayer):
    def build(self, in_shape: torch.Size):
        return _ResidualBlock(in_shape[1], *self.args, **self.kwargs)

多個殘差塊

stack_blocks將n個殘差塊疊起來。

def stack_blocks(n, channels, stride):
    return nn.Sequential(
            *[ResidualBlock(channels, stride if i == 0 else 1) for i in range(n)]
        )

最後用Sequential構建整個模型。

def wideresnet(depth, num_classes, widen_factor=10):
    nChannels = [16, 16*widen_factor, 32*widen_factor, 64*widen_factor]
    n = (depth - 4) // 6

    model = nn.Sequential(
            k4t.layers.Conv2d(nChannels[0], kernel_size=3, stride=1, padding=1, bias=False),
            stack_blocks(n, nChannels[1], stride=1),
            stack_blocks(n, nChannels[2], stride=2),
            stack_blocks(n, nChannels[3], stride=2),

            k4t.layers.BatchNorm2d(), nn.ReLU(inplace=True),
            nn.AdaptiveAvgPool2d(1), nn.Flatten(),
            k4t.layers.Linear(num_classes)
        )

    return model

訓練設定

優化器使用SGD,動量為0.9,L2正則係數為0.01。

初始學習率為1e-2,然後按下述公式調整。

equation?tex=%5Cbegin%7Bequation%7D+learning%5C_rate%28epoch%29+%3D+%5Cbegin%7Bcases%7D+10%5E%7B-2%7D+%26+epoch%5Cle13+%5C%5C+3%5Ctimes10%5E%7B-3%7D+%26+13+%5Clt+epoch+%5Cle+20+%5C%5C+9%5Ctimes10%5E%7B-4%7D+%26+20+%5Clt+epoch+%5Cle+27+%5C%5C+2.7%5Ctimes10%5E%7B-4%7D+%26+27+%5Clt+epoch+%5Cle+34+%5C%5C+8.1%5Ctimes10%5E%7B-5%7D+%26+epoch+%5Cgt+34+%5C%5C+%5Cend%7Bcases%7D+%5Cend%7Bequation%7D

動態學習率非常重要,有利於優化器找到損失更低的點。如果使用固定學習率,效果會比較差。

接著使用keras4torch.Model 封裝torch模型以整合keras風格的訓練api,包括.compile(), .fit(), .evaluate().predict()

支援DataLoader的相應版本為 .fit_dl(), .evaluate_dl(), 和.predict_dl()

def build_model():
    model = wideresnet(depth=28, widen_factor=10, num_classes=NUM_CLASSES)

    model = k4t.Model(model).build([1, 32, 32])
    
    model.compile(optimizer=torch.optim.SGD(model.parameters(), lr=1e-2, momentum=0.9, weight_decay=1e-2), 
                    loss=k4t.losses.CELoss(label_smoothing=0.1),
                    metrics=['acc'], device='cuda')

    return model

資料增強

使用線上增強,對原始波形應用三種簡單變換:

  • 改變振幅
  • 平移波形
  • 改變速度和音調

每種變換都以50%概率應用於每個波形,即每個epoch有約12.5%的資料未經變換,其餘資料經歷不同程度的變換,這樣整個訓練過程模型仍以接近1的概率看到全部原始資料。

class StochasticTransform(object):
    def __call__(self, x):
        return self.transform(x) if random.random() < 0.5 else x

    @abstractclassmethod
    def transform(self, x):
        pass

class ChangeAmplitude(StochasticTransform):
    def __init__(self, min=0.7, max=1.2):
        self.amplitude_range = (min, max)

    def transform(self, x):
        x = x * random.uniform(*self.amplitude_range)
        return x
    
class ChangeSpeedAndPitch(StochasticTransform):
    def __init__(self, max_scale=0.2):
        self.max_scale = max_scale

    def transform(self, x):
        scale = random.uniform(-self.max_scale, self.max_scale)
        speed_fac = 1.0  / (1 + scale)
        x = np.interp(np.arange(0, len(x), speed_fac), np.arange(0,len(x)), x).astype(np.float32)
        return x

class TimeShift(StochasticTransform):
    def __init__(self, frac_0=8, frac_1=3):
        self.frac_0 = frac_0
        self.frac_1 = frac_1

    def transform(self, x):
        a = np.arange(len(x))
        a = np.roll(a, np.random.randint(len(x)//self.frac_0, len(x)//self.frac_1))
        return x[a]

此外,https://github.com/johnmartinsson/bird-species-classification/wiki/Data-Augmentation 也提供了很棒的參考。

線上增強能使模型看到數十倍於原始資料量的增強例項,從而使學習到的函式比較平滑。另一種策略是離線增強,即事先預計算增強例項,混合後與原始資料一塊餵給模型,優點是速度比較快。

最後是五折交叉驗證,ModelCheckpoint將儲存驗證集上表現最好的模型用於預測。

from sklearn.model_selection import StratifiedKFold
from torch.optim.lr_scheduler import MultiStepLR

kfold = StratifiedKFold(n_splits=5, shuffle=True, random_state=2020)

y_proba = np.zeros([len(x_test), NUM_CLASSES]).astype(np.float32)
model_name = 'wideresnet28'

for i, (trn, val) in enumerate(kfold.split(x_train, y_train)):
    print(f'Processing fold {i}:')

    model = build_model()
    
    lr_scheduler = MultiStepLR(model.trainer.optimizer, milestones=[13, 20, 27, 34], gamma=0.3)
    lr_scheduler = k4t.callbacks.LRScheduler(lr_scheduler)

    model_checkpoint = k4t.callbacks.ModelCheckpoint(f'best_{model_name}_{i}.pt', monitor='val_acc')

    trn_loader, val_loader = make_dataloader(x_train[trn], y_train[trn], x_train[val], y_train[val])

    history = model.fit_dl(trn_loader,
            epochs=40,
            val_loader=val_loader,
            callbacks=[model_checkpoint, lr_scheduler]
    )

    model.load_weights(f'best_{model_name}_{i}.pt')
    y_proba += model.predict(x_test, activation=nn.Softmax(-1))

y_proba /= kfold.n_splits

實驗結果

b089bace9583ddf3b7583326a4901233.png