#はじめに
今年もAdvent Calendarネタということで、昨年の記事「モデルベース開発でMATLABからコントロールできるFPGAボード信号発生器の作成」に続き、ギターネタで作ってみることにしました。
(本当は別のネタで作成しかけていたんですが、〆切前になってもディープラーニングの学習精度がうまく上がらず挫折・・・)
本稿では、MATLABを用いて、予め録音したオーディオファイルからエレキギターの音色を推論・分類するディープラーニング・ネットワークを学習し、FPGAのパフォーマンス、回路リソースを見積もってからデプロイします。
作成したものはお遊びで作ったもので、何の役にも立たない代物ですが、こんな風にMATLABからディープラーニングのFPGA実装ができるんだなぁってことが理解してもらえれば幸いです。
なお、以降に記載するプログラムは全てMATLABのコードです。
エレキギターの音色
エレキギターの音色は歪んでいないクリーントーン、少し歪ませたクランチ、深く歪ませたディストーション/オーバードライブといったサウンドに大別されると思います。
ディストーションとオーバードライブも厳密には違うのですが、違いが微妙で人によっても判断が異なるし、そこまでは難しいと思い、とりあえず3つのカテゴリーに分類することにしました。
- クリーン
- クランチ
- ディストーション
#ネットワークモデル
サウンドの分類ではYAMNetというネットワークが有名らしい・・・MATLABでもネットワークモデルダウンロードできるようになっている。。。ということでこれを転移学習させてみました。
#データ取得
ギターアンプの音をマイク撮り、オーディオインターフェイスを介してPCで録音します。
フリーソフトで音色ごとに適当な長さで録音してオーディオファイル(mp3)に保存。
#オーディオデータ
オーディオデータはカテゴリーごとにフォルダ分けしておきます。
データ保存場所をaudioDatastoreで指定しておいて、後で学習する際に読み込めるようにしておきます。
ads = audioDatastore('K:\work\GuitarSound',...
'IncludeSubfolders',true,...
'LabelSource','foldernames')
>>ads
ads =
audioDatastore のプロパティ:
Files: { 'K:\work\GuitarSound\clean\clean1.mp3'; 'K:\work\GuitarSound\clean\clean2.mp3'; 'K:\work\GuitarSound\clean\clean3.mp3' ... and 15 more } Folders: { 'K:\work\GuitarSound' } Labels: [clean; clean; clean ... and 15 more categorical] AlternateFileSystemRoots: {} OutputDataType: 'double' SupportedOutputFormats: ["wav" "flac" "ogg" "mp4" "m4a"] DefaultOutputFormat: "wav"
#特徴量の抽出
人間の聴覚は音量、音程ともにLogスケールに近い特性になっています。人間の感覚に近い尺度で解析できるメルスペクトログラムを使って特徴量を抽出して、それをディープラーニングネットワークに入力するようにします。
クリーン、クランチ、ディストーションサウンドのそれぞれのメルスペクトログラムを比較してみましょう。フレーズはすべて同じで、Eのコードを弾いています。ちなみにEの基音周波数は82.41Hzです。
Cleanサウンドのメルスペクトログラム
[xData, fs] = audioread('E_clean.mp3');
figure
melSpectrogram(xData,fs, ...
'Window',hann(512,'periodic'), ...
'OverlapLength',512/2, ...
'FFTLength',512, ...
'NumBands',64, ...
'FilterBankNormalization','none', ...
'WindowNormalization',false, ...
'SpectrumType','magnitude', ...
'FilterBankDesignDomain','warped');
ギターサウンドを少しかじったことのある方だとご存じだと思いますが、歪みが増すにつれて整数倍高調波成分が増えて、サスティンが長くなるので、それがスペクトログラムにも現れています。
#ネットワークの学習
メルスペクトログラムをネットワークに入力するのにデータサイズを揃える必要があります。オーディオデータは1秒単位で細切れ(chunkSize)にしてからメルスペクトログラムを求めてそれを特徴量のデータとします。
fs = audioInfo.SampleRate;
soundDuration = 0.98; % sec
chunkSize = fs*soundDuration;
numBands = 64;
windowLength = round(0.025*fs);
k = 1;
for n = 1:numel(ads.Files)
xData = read(ads);
for m = 1:chunkSize:length(xData)-fs*soundDuration
trainAudio = xData(m:m+chunkSize);
dataFeatures(:,:,:,k) = melSpectrogram(trainAudio,fs, ...
'Window',hann(windowLength,'periodic'), ...
'NumBands',numBands, ...
'FilterBankNormalization','none', ...
'WindowNormalization',false, ...
'SpectrumType','magnitude', ...
'FilterBankDesignDomain','warped');
dataLabels(k,1) = ads.Labels(n);
k = k + 1;
end
end
メルスペクトログラムのデータは2次元配列となるので、それをYAMNetの画像入力層に入力します。画像入力層はメルスペクトログラムのデータサイズに合わせておきます。また、最後の全結合層と、最後の分類層を今回の3つのカテゴリに変更します。
load('YAMNetWeights.mat','YAMNetRaw')
net = YAMNetRaw;
uniqueLabels = unique(soundType); % 3分類
numLabels = numel(uniqueLabels);
lgraph = layerGraph(net.Layers);
lgraph = replaceLayer(lgraph,"input_1",imageInputLayer(size(dataFeatures(:,:,:,1))));
lgraph = replaceLayer(lgraph,"dense",fullyConnectedLayer(numLabels,"Name","dense",...
"WeightLearnRateFactor", 10, "BiasLearnRateFactor", 10));
lgraph = replaceLayer(lgraph,"Sound",...
classificationLayer("Name","GuitarSound","Classes",uniqueLabels));
ここでYAMNetのネットワークの構成を確認すると、全部で86層のシリーズネットワークになっています。
analyzeNetwork(net)
音声データを学習用、検証用、テスト用に分けます。
[trainInd,valInd,testInd] = dividerand(numel(dataLabels),0.5,0.4,0.1);
trainData = dataFeatures(:,:,1,trainInd);
validData = dataFeatures(:,:,1,valInd);
testData = dataFeatures(:,:,1,testInd);
trainLabels = categorical(dataLabels(trainInd,1));
validLabels = categorical(dataLabels(valInd,1));
testLabels = categorical(dataLabels(testInd,1));
そしてトレーニングを実行しました。
options = trainingOptions('adam','ValidationData',{single(validData),validLabels},...
'LearnRateSchedule', 'piecewise',...
'Plots','training-progress');
net = trainNetwork(single(trainData),trainLabels,lgraph,options);
#推論結果の確認
PC上でネットワークの推論結果を確認してみます。
testNum = 12;
result = classify(net,testData(:,:,:,testNum));
fprintf('推論結果: %s \n', result)
fprintf('正解: %s \n', testLabels(testNum))
推論結果: distortion
正解: distortion
推論結果とデータラベルが同一で正しい推論結果が得られていることが確認できました。
#FPGAのアーキテクチャ検討
MATLABのヘルプドキュメントによると、畳み込み層や全結合層は、マルチスレッド処理ができるように、スケジューラが制御する構成となっています。つまり、スレッド数を調整することで、並列度を上げた高速な回路にするか、並列度を下げて低速で回路リソースを抑えた回路にするかを選択できます。
例えば畳み込み層のスレッド数=9、全結合層のスレッド数=4で処理速度を推定すると
hPC = dlhdl.ProcessorConfig;
hPC.TargetPlatform = 'Altera Arria 10 SoC development kit'; % ターゲットボードの設定
hPC.setModuleProperty("conv","ConvThreadNumber",9); % 畳み込み層のスレッド数
hPC.setModuleProperty("fc","FCThreadNumber",4); % 全結合層のスレッド数
% hPC.optimizeConfigurationForNetwork(net) % ネットワーク構造によってメモリサイズなどを最適化
hPC.estimatePerformance(net) % パフォーマンスの推定
Deep Learning Processor Estimator Performance Results
LastFrameLatency(cycles) / LastFrameLatency(seconds) / FramesNum / TotalLatency / Frames/s
------------------------------------------------------------------------------------------------
Network 55278586 / 0.27639 / 1 / 55278586 / 3.6
- The clock frequency of the DL processor is: 200MHz
※各レイヤーでの処理時間も表示されますが、長いので省略。
1フレームあたりの処理時間は0.27639秒、フレームレートは3.6fpsとなりました。
これは元の1秒間の時系列データからメルスペクトログラムを求め、2次元配列の特徴量をネットワークに入力しているので、その推論にかかる時間です。
回路リソースの推定結果は以下となりました。
hPC.estimateResources
Deep Learning Processor Estimator Resource Results
DSPs Block RAM* LUTs(CLB/ALUT)
------------------------------------------------------------------------------------------------
DL_Processor 118 6069632 50980
- Block RAM represents Block RAM bits in Intel devices
次に畳み込み層のスレッド数=64、全結合層のスレッド数=64に設定、つまり並列度を大幅に上げて処理速度を推定すると
hPC.setModuleProperty("conv","ConvThreadNumber",64);
hPC.setModuleProperty("fc","FCThreadNumber",64);
hPC.estimatePerformance(net)
Deep Learning Processor Estimator Performance Results
LastFrameLatency(cycles) / LastFrameLatency(seconds) / FramesNum / TotalLatency / Frames/s
------------------------------------------------------------------------------------------------
Network 7914638 / 0.03957 / 1 / 7914638 / 25.3
- The clock frequency of the DL processor is: 200MHz
1フレームあたりの処理時間は0.03957秒、フレームレートは25.3fpsと大幅に高速になりました。
また、回路リソースの推定結果はスレッド数が増えた分増加しており、以下となりました。
hPC.estimateResources
Deep Learning Processor Estimator Resource Results
DSPs Block RAM* LUTs(CLB/ALUT)
------------------------------------------------------------------------------------------------
DL_Processor 742 20678144 220882
- Block RAM represents Block RAM bits in Intel devices
HDLコード生成をする前に、速度/回路リソースを考慮したうえで回路アーキテクチャを決定するのに役立ちそうですね。
#FPGA実装
回路アーキテクチャの方向性が決まったら、次のコマンドでネットワークのHDLコード生成&ビルドを行います。
dlhdl.buildProcessor(hPC)
HDLコード生成と、生成したHDLコードのQuartusでのコンパイルが数時間かけて行われ、sofファイルが生成されます。これは時間がかかるので、やり直しの無いよう、推定機能を利用して回路アーキテクチャを確定させておきましょう。
デフォルト設定では、こちらのヘルプドキュメントに説明してあるEthernetインターフェイスがディープラーニングIPと統合されてデプロイされるようになっています。これにより、データをEthernet経由でPC⇒ARM⇒FPGAに送り、推論結果をFPGA⇒ARM⇒PCで確認できます。PCとFPGAボードをEthernetケーブルで接続して、生成したIPの動作確認を行ってみましょう。
次に示すMATLABコードで、ターゲットボード、インターフェイス(Ethernet)、生成済みビットストリームファイルの設定を行い、deployコマンドを実行すると、ビットストリームがFPGAにダウンロードされ回路がコンフィギュレーションされ、続いてネットワークの係数がFPGAのメモリにロードされます。
hTarget = dlhdl.Target('Intel','Interface','Ethernet')
hW = dlhdl.Workflow('Network', net, 'Bitstream', '.\dlhdl_prj\dlprocessor.sof','Target',hTarget)
hW.deploy
ネットワークで使用するレイヤーが同一かつスレッド数が同一であれば、FPGAの回路構成は同じなので、同じビットストリームで、ネットワークの係数だけ変更して利用できるようになっています。
FPGAにロードしたネットワークで推論を行ってみましょう。
[prediction, speed] = hW.predict(single(testData(:,:,:,testNum)),'Profile','on');
[val, idx] = max(prediction);
fprintf('FPGA推論結果: %s \n', net.Layers(end).ClassNames{idx})
fprintf('正解: %s \n', testLabels(testNum))
FPGA推論結果: crunch
正解: crunch
FPGAにデプロイしたディープラーニングネットワークで推論結果を確認することが出来ました。
#最後に
お遊びで簡単に作れるギターサウンド分類をFPGAにデプロイしてみました。
あまり時間が取れなかったので、学習データもあまり沢山のバリエーションは用意できず、簡単な分類しかやっていない・・・というのは言い訳です。
今回挫折した別ネタは、もしちゃんと動くようになったら投稿しようと思います。(一生日の目を見ないかも(笑))
#参考文献
MATLAB Documentation
Transfer Learning with Pretrained Audio Networks
Deep Learning HDL Toolbox Documentation
MATLAB Example
Speech Command Recognition Using Deep Learning