概要
個人的な備忘録を兼ねたPyTorchの基本的な解説とまとめです。GTZANデータセットを利用しての音楽ジャンル分類の演習を行ってみたいと思います。GTZANデータセットは2025年8月現在リンク切れ?みたいでtorchaudio.datasets.GTZANを利用してダウンロードできなくなっているようです。Kaggleから直接ダウンロードするのが良いと思います。
Kaggle: GTZAN Dataset - Music Genre Classification
ちょっとだけ注意で、jazzジャンルのjazz.00054.wavがなぜか壊れています。Kaggleのdiscussionでも指摘されていました![]()
今回は1次元畳み込みを利用した音楽データのジャンル分類問題となります。ネットワーク構造の書き方に注力するため、ジャンルを4分類にして演習を行っていきます。最終的に次のような表になります。
図:個別の分類精度
方針
- できるだけ同じコード進行
- できるだけ簡潔(細かい内容は割愛)
- 特徴量などの部分,あえて数値で記入(どのように変わるかがわかりやすい)
演習用のファイル
- データ:GTZANの音楽ジャンルデータ
- Kaggle: GTZAN Dataset - Music Genre Classificationからダウンロード
- コード:sample_13.ipynb
注意点
- 今回の演習では計算速度とデータサイズの肥大化を避けるたデータの数を少なくしています
- 4ジャンル(classical、jazz、pop、reggae)4個のファイルの合計16個のファイルを利用します
- 音楽データの入手とデータセット作成後でないとsample_14.ipynbは動作しないぞ〜
〜
今回新しく加わる内容は、ネットワークの書き方です。
変更点
- ネットワーク構造の書き方に
nn.Sequentialを導入
音楽データから波形データを抽出する内容は第12.5回を参照してください。
1. 演習用のデータセット作成(準備)
GTZANのデータをダウンロードしてZIPを解凍すると、「blues, classical, country,...」と音楽ジャンルのディレクトリが10種類できるはずです。
事前に4ジャンルから4個ファイルを選んでおきましょう
下記のコードではaudioフォルダにジャンル名のフォルダを作成してその中に4個のファイルをコピーしました。コードの中核はファイルをlibrosaで読み込み、5秒ずつの長さに重複なしで分割するだけです。音楽ジャンルの分類になるのでラベルはジャンル名をID化しておきます。
import librosa
import numpy as np
import glob
from pathlib import Path
file_list = glob.glob("./audio/*/*.wav") # wavファイルを読み込むディレクトリ適宜変更してください
# ['./audio/classical/classical.00000.wav', ...,'./audio/jazz/jazz.00003.wav']
# 音楽ジャンルの辞書
genre_dic = {"classical":0, "jazz":1,"pop":2, "reggae":3}
labels = [genre_dic[Path(path).parent.name] for path in file_list]
#------------------------------------------------------------------------------
# サンプリングレート: 22050
# 分割する時間:5秒
target_sr = 22050
sequence_sec = 5 # 列の長さ(秒)
sequence_length = target_sr * sequence_sec # 実際の系列長
data_list = [] # 等長音声データのリスト
label_list = [] # 対応するラベルのリスト
for num, filename in enumerate(file_list):
audio, sr = librosa.load(filename, sr=target_sr) # ファイル読み込み
divided_number = len(audio)//sequence_length # 何個に分割できるか?分割数
segments = [audio[i*sequence_length:(i+1)*sequence_length] for i in range(0,divided_number)]
data_list.extend(segments)
label_list.extend([labels[num] for _ in range(len(segments))])
x = np.array(data_list)
t = np.array(label_list)
# x.shape, (96, 110250)
# t.shape, (96,)
np.savez_compressed("train_data.npz", x=x, t=t)
コードのポイント
- audioディレクトリに4ジャンルの音源をそれぞれ4個ずつ用意しておきます
- 実際の系列長はサンプリングレート×秒数なので 22,050x5=110,250となります
- librosaで読み込んだ波形データを先頭から110,250個でスライスする部分が、
segments = [audio[i*sequence_length:(i+1)*sequence_length] for i in range(0,divided_number)]となります - 入力データと教師データ(ラベル)のnumpy配列を
train_data.npzとして保存しました
2.
音楽ジャンル分類のコードと解説♪
PyTorchによるプログラムの流れを確認します。基本的に下記の5つの流れとなります。Juypyter Labなどで実際に入力しながら進めるのがオススメ
- データの読み込みとtorchテンソルへの変換 (2.1)
- ネットワークモデルの定義と作成 (2.2)
- 誤差関数と誤差最小化の手法の選択 (2.3)
- 変数更新のループ (2.4)
- 検証 (2.5)
2.1 データの読み込みとtorchテンソルへの変換
import numpy as np
import torch
import torch.nn as nn
from sklearn.model_selection import train_test_split
data = np.load("./train_data.npz")
x = data["x"]
t = data["t"]
# x.shape (96, 110250)
# t.shape (96,)
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"device: {device}")
# 入力データは FloatTensor
# 入力データの形状を(バッチサイズ、チャンネル、系列長)
# 教師データはラベルなので LongTensor
X = torch.FloatTensor(x).to(device).view(x.shape[0], 1, x.shape[1])
T = torch.LongTensor(t).to(device)
x_train, x_test, t_train, t_test = train_test_split(X, T, test_size=0.2, stratify=t, random_state=55)
コードのポイント
- conv1dを利用するので、入力データの形状を(バッチサイズ、1チャンネル、系列長)に変換します
- train_test_splitで分割するオプションの
stratify=tを利用して「データセットにおけるラベルの割合を、分割後の学習データとテストデータでも反映させる」ようにします
2.2 ネットワークモデルの定義と作成
ネットワークを定義する部分です。これまでとは若干異なる書き方になっています。これまで利用していたパターンは、
- __init__の部分にネットワークの定義を並べる
- forwardの部分に順番に層を記述
するという形でした。この書き方で全く問題ないのですが、利用するネットワーク層の数が多くなると、やや見通しが悪くなる可能性があります。
PyTorchのネットワークの記述方法の一つnn.Sequentialを使います。使い方も簡単で nn.Sequential( ) のカッコ内に利用するネットワーク層を順番に並べるだけです。
図:ネットワーク構造の基本形
音声データから特徴量を抽出するネットワーク構造に features、特徴量からジャンル分類するネットワーク構造に classifierという名前をつけておきます。系列長が110,250次元あるので、conv1dとプーリング層をそれぞれ3回利用して特徴量を抽出していきます1。1次元畳み込み層やプーリング層については、第12回のconv1d解説を参考にしてください。
class DNN(nn.Module):
def __init__(self):
super().__init__()
self.features = nn.Sequential(
# 第1ブロック
nn.Conv1d(in_channels=1, out_channels=64, kernel_size=500, stride=16),
nn.BatchNorm1d(num_features=64),
nn.ReLU(),
nn.MaxPool1d(kernel_size=4, stride=2),
# 第2ブロック
nn.Conv1d(in_channels=64, out_channels=128, kernel_size=100, stride=16),
nn.BatchNorm1d(num_features=128),
nn.ReLU(),
nn.MaxPool1d(kernel_size=4, stride=2),
# 第3ブロック
nn.Conv1d(in_channels=128, out_channels=256, kernel_size=10, stride=4),
nn.BatchNorm1d(num_features=256),
nn.LeakyReLU(),
nn.AdaptiveAvgPool1d(output_size=8) # 固定サイズの出力
)
self.classifier = nn.Sequential(
nn.Flatten(),
nn.Linear(in_features=256 * 8, out_features=512),
nn.BatchNorm1d(num_features=512),
nn.ReLU(),
nn.Dropout(p=0.3),
nn.Linear(in_features=512, out_features=128),
nn.BatchNorm1d(num_features=128),
nn.ReLU(),
nn.Dropout(p=0.3),
nn.Linear(in_features=128, out_features=4)
)
def forward(self, x):
h = self.features(x)
y = self.classifier(h)
return y
model = DNN()
model.to(device)
実際にprint(model)すると、順番に並べているだけなのですが、ちょっとプロっぽい表現に見えませんか?![]()
DNN(
(features): Sequential(
(0): Conv1d(1, 64, kernel_size=(500,), stride=(16,))
(1): BatchNorm1d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(2): ReLU()
(3): MaxPool1d(kernel_size=4, stride=2, padding=0, dilation=1, ceil_mode=False)
(4): Conv1d(64, 128, kernel_size=(100,), stride=(16,))
(5): BatchNorm1d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(6): ReLU()
(7): MaxPool1d(kernel_size=4, stride=2, padding=0, dilation=1, ceil_mode=False)
(8): Conv1d(128, 256, kernel_size=(10,), stride=(4,))
(9): BatchNorm1d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(10): LeakyReLU(negative_slope=0.01)
(11): AdaptiveAvgPool1d(output_size=8)
)
(classifier): Sequential(
(0): Flatten(start_dim=1, end_dim=-1)
(1): Linear(in_features=2048, out_features=512, bias=True)
(2): BatchNorm1d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(3): ReLU()
(4): Dropout(p=0.3, inplace=False)
(5): Linear(in_features=512, out_features=128, bias=True)
(6): BatchNorm1d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(7): ReLU()
(8): Dropout(p=0.3, inplace=False)
(9): Linear(in_features=128, out_features=4, bias=True)
)
)
コードのポイント
-
nn.Sequentialを利用してネットワーク層を波形データから特徴量を抽出するfeaturesと、特徴量から分類に落とし込むclassifierの2種類に分けて記述します - forward部分は、まとめたネットワークをそのまま記述します
- 第1ブロックConv1dの入力チャンネルは、波形データそのものなのでin_channels=1となります
- カーネルサイズは、500、100、10と徐々に小さくしています。この数値を変更すると精度や学習速度も変わってきます
- 第3ブロックのAdaptiveAvgPool1dは、出力サイズを固定できる便利なプーリング層です。
nn.AdaptiveAvgPool1d(output_size=8)で出力サイズは、(バッチサイズ、チャンネル数、8)に固定されます - classifierブロックは、Linearと活性化関数を利用して最終的に4分類にすればOKです
- 随所にあるBatchNorm1dは、バッチ正規化と呼ばれるもので、データを平均0、分散1になるように調整するものです。これで学習が安定します。詳細はPyTorchのBatchNorm1dを参照してください
2.3 誤差関数と誤差最小化の手法の選択
分類問題なので損失関数はクロスエントロピー損失となります。最適化の手法もAdamやAdamWあたりを好みに応じて選択します。
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.AdamW(model.parameters(), lr=0.0005)
2.4 変数更新のループ
学習ループです。学習用に分割したデータx_trainを利用してモデルを学習させていきます。ネットワーク構造から BatchNorm1d() を除くと学習がやや不安定になり、損失が下がるまで少し時間がかかります。
# 精度計算の関数
def accuracy(y, t):
_,argmax_list = torch.max(y, dim=1)
accuracy = sum(argmax_list == t).item()/len(t)
return accuracy
# 学習ループ
LOOP = 100 # LOOP回数の適宜選択
model.train() # 学習時はmodel.train()、検証時はmodel.eval()
for epoch in range(LOOP):
optimizer.zero_grad()
y = model(x_train)
loss = criterion(y, t_train)
acc = accuracy(y, t_train)
loss.backward()
optimizer.step()
if (epoch+1)%20 == 0:
print(f"{epoch}: loss: {loss.item()},\tacc:{acc}")
2.5 検証
テスト用データを使って検証してみます。学習結果ごとに検証の精度は、集めてくるデータや学習回数などで毎回異なりますが、概ね0.9〜1.0になります。今回はあえて、95%精度のもので紹介しておきます。
model.eval()
with torch.inference_mode():
y_test = model(x_test)
test_acc = accuracy(y_test, t_test)
# test_acc 0.9〜1.0
正解はpopですが、reggaeと予測したのが1個あるようです。5秒間の音楽データなので切り抜かれた部分について、両者が似ていると感じることは十分ありえるかな。
図:個別の分類精度
3. 次回
次回はせっかく10ジャンルあるので、全ジャンルでの分類を行いたいと思います。ただ、データ数が多いのでデータをいくつかのグループに分けて学習するというミニバッチ学習をする必要がありそうです。
目次ページ
注
-
正規化については、参考になる記事がいくつかあります。Batch, Layer...気持ちやPyTorchで、BatchNorm...確認する、深層学習 Day4-BatchNorm...などを参考にしてください。 ↩