キーワードスポッティングや音イベント検出で、対数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の方が性能良いことを確認できました。
コメント