スポンサーリンク

Per-Channel Energy Normalization(PCEN)の性能確認【PyTorch】

librosa

キーワードスポッティングや音イベント検出で、対数log-melや対数スペクトルに変わる特徴量として提案されたPer-Channel Energy Normalization(PCEN)の性能を、yes/noのspeech commands datasetで確認しました。

PCENに関しては以下記事を参照ください。

speech commands datasetsのダウンロード

MicrosoftのPyTorch を使用したオーディオ分類の概要に沿って、yes/noのspeech commands datasetsでPCENと対数スペクトルの特徴量による性能差を見ていきます。

必要なライブラリをインポートします。Google Colabの場合は、librosaをインストールしてください。

!pip install git+https://github.com/librosa/librosa

import os
import torch
import torchaudio
from torch.utils.data import Dataset, DataLoader
from torchvision import datasets, transforms
from torchvision import datasets, transforms
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, models, transforms
import pandas as pd
import numpy as np
from pathlib import Path
import IPython.display as ipd
import matplotlib.pyplot as plt
import librosa

torchaudioを使って、SPEECHCOMMANDS datasetをダウンロードします。

default_dir = os.getcwd()
folder = 'data'

os.mkdir(folder) 

trainset_speechcommands = torchaudio.datasets.SPEECHCOMMANDS(f'./{folder}/', download=True)

yes/noの音源を対数スペクトログラムとPCENに変換

ダウンロードしたSPEECHCOMMANDS datasetから、yes/noの音源のみ読み込みデータローダーにセットします。

def load_audio_files(path: str, label:str):

    dataset = []
    walker = sorted(str(p) for p in Path(path).glob(f'*.wav'))

    for i, file_path in enumerate(walker):
        path, filename = os.path.split(file_path)
        speaker, _ = os.path.splitext(filename)
        speaker_id, utterance_number = speaker.split("_nohash_")
        utterance_number = int(utterance_number)
    
        waveform, sample_rate = torchaudio.load(file_path)
        dataset.append([waveform, sample_rate, label, speaker_id, utterance_number])
        
    return dataset

trainset_speechcommands_yes = load_audio_files('./data/SpeechCommands/speech_commands_v0.02/yes', 'yes')
trainset_speechcommands_no = load_audio_files('./data/SpeechCommands/speech_commands_v0.02/no', 'no')

trainloader_yes = torch.utils.data.DataLoader(trainset_speechcommands_yes, batch_size=1,
                                            shuffle=True, num_workers=0)
trainloader_no = torch.utils.data.DataLoader(trainset_speechcommands_no, batch_size=1,
                                            shuffle=True, num_workers=0)

データローダーから音源を取り出して、対数スペクトログラムとPCENのpng画像をフォルダに保存します。

def create_spectrogram_images(trainloader, label_dir):
    directory = f'./data/spectrograms/{label_dir}/'
    directory_pce = f'./data/pce/{label_dir}/'

    os.makedirs(directory, mode=0o777, exist_ok=True)

    os.makedirs(directory_pce, mode=0o777, exist_ok=True)
             
    for i, data in enumerate(trainloader):

        waveform = data[0]
        sample_rate = data[1][0]
        label = data[2]
        ID = data[3]

        spectrogram_tensor = torchaudio.transforms.Spectrogram()(waveform)   
        pcen_S = librosa.pcen(spectrogram_tensor[0].detach()[0,:,:].numpy().copy()*(2**31), sr=sample_rate)     
            
        fig = plt.figure();
        plt.imsave(f'./data/spectrograms/{label_dir}/spec_img{i}.png', spectrogram_tensor[0].log2()[0,:,:].numpy(), cmap='viridis');
        fig = plt.figure();
        plt.imsave(f'./data/pce/{label_dir}/pce_img{i}.png', pcen_S, cmap='viridis');

create_spectrogram_images(trainloader_yes, 'yes')
create_spectrogram_images(trainloader_no, 'no')

対数スペクトログラムとPCENを、訓練データと検証データで8:2に分割して、DataLoaderにセットします。

data_path = './data/spectrograms' 
data_pce_path = './data/pce'

yes_no_dataset = datasets.ImageFolder(
    root=data_path,
    transform=transforms.Compose([transforms.Resize((201,81)),
                                  transforms.ToTensor(),
                                  transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)),
                                  ])
)

yes_no_dataset_pce = datasets.ImageFolder(
    root=data_pce_path,
    transform=transforms.Compose([transforms.Resize((201,81)),
                                  transforms.ToTensor()
                                  ])
)

train_size = int(0.8 * len(yes_no_dataset))
test_size = len(yes_no_dataset) - train_size
yes_no_train_dataset, yes_no_test_dataset = torch.utils.data.random_split(yes_no_dataset, [train_size, test_size],generator=torch.Generator().manual_seed(42))
yes_no_train_dataset_pce, yes_no_test_dataset_pce = torch.utils.data.random_split(yes_no_dataset_pce, [train_size, test_size],generator=torch.Generator().manual_seed(42))

train_dataloader = torch.utils.data.DataLoader(
    yes_no_train_dataset,
    batch_size=15,
    num_workers=2,
    shuffle=True
)

test_dataloader = torch.utils.data.DataLoader(
    yes_no_test_dataset,
    batch_size=15,
    num_workers=2,
    shuffle=True
)

train_dataloader_pce = torch.utils.data.DataLoader(
    yes_no_train_dataset_pce,
    batch_size=15,
    num_workers=2,
    shuffle=True
)

test_dataloader_pce = torch.utils.data.DataLoader(
    yes_no_test_dataset_pce,
    batch_size=15,
    num_workers=2,
    shuffle=True
)

モデルの定義と学習

モデル、学習とテストの関数を定義します。

class CNNet(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(3, 32, kernel_size=5)
        self.conv2 = nn.Conv2d(32, 64, kernel_size=5)
        self.conv2_drop = nn.Dropout2d()
        self.flatten = nn.Flatten()
        self.fc1 = nn.Linear(51136, 50)
        self.fc2 = nn.Linear(50, 2)

    def forward(self, x):
        x = F.relu(F.max_pool2d(self.conv1(x), 2))
        x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)), 2))
        x = self.flatten(x)
        x = F.relu(self.fc1(x))
        x = F.dropout(x, training=self.training)
        x = F.relu(self.fc2(x))
        return F.log_softmax(x,dim=1)  

def train(dataloader, model, loss, optimizer):
    model.train()
    size = len(dataloader.dataset)
    for batch, (X, Y) in enumerate(dataloader):
        X, Y = X.to(device), Y.to(device)
        optimizer.zero_grad()
        pred = model(X)
        loss = cost(pred, Y)
        loss.backward()
        optimizer.step()

        if batch % 100 == 0:
            loss, current = loss.item(), batch * len(X)
            print(f'loss: {loss:>7f}  [{current:>5d}/{size:>5d}]')

def test(dataloader, model):
    size = len(dataloader.dataset)
    model.eval()
    test_loss, correct = 0, 0

    with torch.no_grad():
        for batch, (X, Y) in enumerate(dataloader):
            X, Y = X.to(device), Y.to(device)
            pred = model(X)

            test_loss += cost(pred, Y).item()
            correct += (pred.argmax(1)==Y).type(torch.float).sum().item()

    test_loss /= size
    correct /= size

    print(f'\\nTest Error:\\nacc: {(100*correct):>0.1f}%, avg loss: {test_loss:>8f}\\n')

対数スペクトログラムを特徴量としモデルを学習させます。

device = 'cuda' if torch.cuda.is_available() else 'cpu'
print('Using {} device'.format(device))

model = CNNet().to(device)
cost = torch.nn.CrossEntropyLoss()

learning_rate = 0.0001
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)

epochs = 100

for t in range(epochs):
    print(f'Epoch {t+1}\\n-------------------------------')
    train(train_dataloader, model, cost, optimizer)
    test(test_dataloader, model)
print('Done!')

PCENを特徴量としモデルを学習させます。

model_pcen = CNNet().to(device)
optimizer = torch.optim.Adam(model_pcen.parameters(), lr=learning_rate)

for t in range(epochs):
    print(f'Epoch {t+1}\\n-------------------------------')
    train(train_dataloader_pce, model_pcen, cost, optimizer)
    test(test_dataloader_pce, model_pcen)
print('Done!')

対数スペクトログラムとPCENの性能比較

それぞれ100エポック終了時のモデルを用いて検証データの性能を比較します。

test(test_dataloader, model)
# Test Error:
# acc: 97.1%, avg loss: 0.006882

test(test_dataloader_pce, model_pcen)
# Test Error:
# acc: 98.8%, avg loss: 0.004739

対数スペクトログラムに対して、PCENの方が性能良いことを確認できました。

関連記事、参考資料

コメント