Main Content

深層学習を使用したノイズに含まれる音声区間の検出

この例では、深層学習を使用して S/N 比が低い環境内で音声領域を検出する方法を説明します。ここでは Speech Commands データセットで双方向長短期記憶 (BiLSTM) ネットワークに学習させて音声区間を検出します。

はじめに

音声区間の検出は、自動音声認識や話者認識といったさまざまなオーディオ システムに欠かせない要素です。音声区間の検出は、ノイズによって音声がかき消されるような S/N 比 (SNR) が低い状況では、特に困難を伴います。

この例では、シーケンスおよび時系列のデータの学習に適した再帰型ニューラル ネットワーク (RNN) の一種、長短期記憶 (LSTM) ネットワークを使用します。LSTM ネットワークは、シーケンスのタイム ステップ間の長期的な依存関係を学習できます。LSTM 層 (lstmLayer) は、順方向の時間系列を確認し、一方、双方向の LSTM 層 (bilstmLayer) は、順方向および逆方向両方の時間系列を確認できます。この例では、双方向の LSTM 層を使用します。

この例では、スペクトル特性の特徴シーケンスと、高調波比のメトリクスを使用して、音声区間検出用の双方向 LSTM ネットワークに学習させます。

SNR が高いシナリオでは、従来の音声検出アルゴリズムが適切に機能します。間を置きながら発声された一連の単語で構成されるオーディオ ファイルを読み取ります。このオーディオを 16 kHz でリサンプリングします。オーディオを再生します。

fs = 16e3;
[speech,fileFs] = audioread("Counting-16-44p1-mono-15secs.wav");
speech = resample(speech,fs,fileFs);
speech = speech/max(abs(speech));
sound(speech,fs)

関数 detectSpeech (Audio Toolbox) を使用して、音声領域の位置を特定します。関数 detectSpeech は、すべての音声領域を正しく特定します。

win = hamming(50e-3*fs,"periodic");
detectSpeech(speech,fs,Window=win)

SNR が -20 dB となるように、洗濯機のノイズを使用してオーディオ信号を破損させます。破損させたオーディオを再生します。

[noise,fileFs] = audioread("WashingMachine-16-8-mono-200secs.mp3");
noise = resample(noise,fs,fileFs);

SNR = -20;
noiseGain = 10^(-SNR/20) * norm(speech) / norm(noise);

noisySpeech = speech + noiseGain*noise(1:numel(speech));
noisySpeech = noisySpeech./max(abs(noisySpeech));

sound(noisySpeech,fs)

ノイズを含むオーディオ信号に対し、detectSpeech を呼び出します。SNR が非常に低いため、この関数は音声領域の検出に失敗します。

detectSpeech(noisySpeech,fs,Window=win)

事前学習済みのネットワークと構成済みのaudioFeatureExtractor (Audio Toolbox)オブジェクトをダウンロードして読み込みます。このネットワークは、audioFeatureExtractor オブジェクトから出力された特徴を使用して、SNR が低い環境内で音声を検出するよう学習されています。

downloadFolder = matlab.internal.examples.downloadSupportFile("audio","VoiceActivityDetection.zip");
dataFolder = tempdir;
unzip(downloadFolder,dataFolder)
netFolder = fullfile(dataFolder,"VoiceActivityDetection");
load(fullfile(netFolder,"voiceActivityDetectionExample.mat"));
speechDetectNet
speechDetectNet = 
  SeriesNetwork with properties:

         Layers: [6×1 nnet.cnn.layer.Layer]
     InputNames: {'sequenceinput'}
    OutputNames: {'classoutput'}

afe
afe = 
  audioFeatureExtractor with properties:

   Properties
                     Window: [256×1 double]
              OverlapLength: 128
                 SampleRate: 16000
                  FFTLength: []
    SpectralDescriptorInput: 'linearSpectrum'
        FeatureVectorLength: 9

   Enabled Features
     spectralCentroid, spectralCrest, spectralEntropy, spectralFlux, spectralKurtosis, spectralRolloffPoint
     spectralSkewness, spectralSlope, harmonicRatio

   Disabled Features
     linearSpectrum, melSpectrum, barkSpectrum, erbSpectrum, mfcc, mfccDelta
     mfccDeltaDelta, gtcc, gtccDelta, gtccDeltaDelta, spectralDecrease, spectralFlatness
     spectralSpread, pitch, zerocrossrate, shortTimeEnergy


   To extract a feature, set the corresponding property to true.
   For example, obj.mfcc = true, adds mfcc to the list of enabled features.

音声データから特徴を抽出して正規化します。時間が列に沿うように特徴の向きを変更します。

features = extract(afe,noisySpeech);
features = (features - mean(features,1))./std(features,[],1);
features = features';

音声検出ネットワークに特徴を渡し、各特徴ベクトルが音声フレームに属するかどうかを分類させます。

decisionsCategorical = classify(speechDetectNet,features);

各判定は、audioFeatureExtractor によって解析された解析ウィンドウに対応します。オーディオ サンプルと 1 対 1 で対応するように、判定を複製します。音声、ノイズを含む音声、および VAD の判定をプロットします。

decisionsWindow = 1.2*(double(decisionsCategorical)-1);
decisionsSample = [repelem(decisionsWindow(1),numel(afe.Window)), ...
                   repelem(decisionsWindow(2:end),numel(afe.Window)-afe.OverlapLength)];

t = (0:numel(decisionsSample)-1)/afe.SampleRate;
plot(t,noisySpeech(1:numel(t)), ...
     t,speech(1:numel(t)), ...
     t,decisionsSample);
xlabel("Time (s)")
ylabel("Amplitude")
legend("Noisy Speech","Speech","VAD",Location="southwest")

ストリーミングのコンテキストで、学習済みの VAD ネットワークを使用することもできます。ストリーミング環境のシミュレーションを行うには、最初に音声とノイズ信号を WAV ファイルとして保存します。ストリーミング入力のシミュレーションを行うには、ファイルからフレームを読み取り、目的とする SNR でそれらを混合します。

audiowrite("Speech.wav",speech,fs)
audiowrite("Noise.wav",noise,fs)

ストリーミング オーディオに VAD ネットワークを適用する場合、遅延と精度との間でトレード オフする必要があります。ノイズに含まれるストリーミング音声区間の検出のデモに使用するパラメーターを定義します。テストの持続時間、ネットワークに渡されるシーケンスの長さ、シーケンスのホップ長、およびテストする SNR を設定できます。一般に、シーケンスを長くすると精度は向上しますが、遅延も増加します。また、デバイスへの信号出力として元の信号とノイズを含む信号のいずれかを選択できます。

testDuration = 20;

sequenceLength = 400;
sequenceHop = 20;
SNR = -20;
noiseGain = 10^(-SNR/20) * norm(speech) / norm(noise);

signalToListenTo = "noisy";

ストリーミング デモ用の補助関数を呼び出し、ストリーミング オーディオに対する VAD ネットワークの性能を観察します。パラメーターをライブ コントロールで設定しても、ストリーミングの例は中断されません。ストリーミング デモが完了した後、デモのパラメーターに変更を加えてストリーミング デモを再度実行できます。ストリーミング デモ用のコードは、サポート関数にあります。

helperStreamingDemo(speechDetectNet,afe, ...
                    "Speech.wav","Noise.wav", ...
                    testDuration,sequenceLength,sequenceHop,signalToListenTo,noiseGain);

この例の残りの部分では、VAD ネットワークの学習と評価について順に説明します。

VAD ネットワークの学習と評価

学習:

  1. LSTM ネットワークの学習に使用したオーディオ音声ファイルを指す audioDatastore (Audio Toolbox) を作成します。

  2. さまざまな持続時間の無音のセグメントによって分離された音声セグメントを含む学習信号を作成します。

  3. 洗濯機のノイズを使用して音声と無音の信号を破損させます (SNR = -10 dB)。

  4. ノイズを含む信号から、スペクトル特性と高調波比から成る特徴シーケンスを抽出します。

  5. この特徴シーケンスを使用して、音声区間領域を特定するために LSTM ネットワークに学習させます。

予測:

  1. 学習済みネットワークのテストに使用される音声ファイルの audioDatastore を作成し、無音のセグメントによって分離された音声を含むテスト信号を作成します。

  2. 洗濯機のノイズを使用してテスト信号を破損させます (SNR = -10 dB)。

  3. ノイズを含むテスト信号から特徴シーケンスを抽出します。

  4. 学習済みネットワークにテスト特徴を渡し、音声区間領域を特定します。

  5. ネットワークの精度を、信号と無音から成るテスト信号から取得した音声区間のベースラインと比較します。

次は学習プロセスのスケッチです。

次は予測プロセスのスケッチです。学習済みのネットワークを使用して予測を行います。

音声コマンド データセットの読み込み

Google Speech Commands データセット ([1]) をダウンロードし、解凍します。

downloadFolder = matlab.internal.examples.downloadSupportFile("audio","google_speech.zip");
dataFolder = tempdir;
unzip(downloadFolder,dataFolder)
dataset = fullfile(dataFolder,"google_speech");

学習データ セットを指すaudioDatastore (Audio Toolbox)を作成します。

adsTrain = audioDatastore(fullfile(dataset,"train"),Includesubfolders=true);

検証データ セットを指すaudioDatastore (Audio Toolbox)を作成します。

adsValidation = audioDatastore(fullfile(dataset,"validation"),Includesubfolders=true);

音声と無音から成る学習信号の作成

read (Audio Toolbox) を使用して、オーディオ ファイルの内容を読み取ります。構造体 adsInfo からサンプル レートを取得します。

[data,adsInfo] = read(adsTrain);
fs = adsInfo.SampleRate;

sound コマンドを使用してオーディオ信号を再生します。

sound(data,fs)

オーディオ信号をプロットします。

timeVector = (1/fs) * (0:numel(data)-1);
plot(timeVector,data)
ylabel("Amplitude")
xlabel("Time (s)")
title("Sample Audio")
grid on

この信号には音声以外の部分 (サイレンス、バックグラウンド ノイズなど) が含まれており、その部分には有意な音声情報が含まれていません。この例では、関数 detectSpeech (Audio Toolbox) を使用してサイレンスを除去します。

データの有意な部分を抽出します。解析用に、周期的な 50 ms のハミング ウィンドウを定義します。出力引数なしで detectSpeech を呼び出し、検出された音声領域をプロットします。detectSpeech を再度呼び出し、検出された音声のインデックスを返します。検出された音声領域を分離してから、sound コマンドを使用してオーディオを再生します。

win = hamming(50e-3*fs,"periodic");

detectSpeech(data,fs,Window=win);

speechIndices = detectSpeech(data,fs,Window=win);

sound(data(speechIndices(1,1):speechIndices(1,2)),fs)

関数 detectSpeech は、検出された音声領域をぴったりと囲むインデックスを返します。この例では、検出された音声のインデックスを両側に 5 フレーム拡張することで、最終的なモデルの性能が向上することが経験的に明らかになっています。音声のインデックスに 5 つのフレームを追加してインデックスを拡張してから、音声を再生します。

speechIndices(1,1) = max(speechIndices(1,1) - 5*numel(win),1);
speechIndices(1,2) = min(speechIndices(1,2) + 5*numel(win),numel(data));

sound(data(speechIndices(1,1):speechIndices(1,2)),fs)

学習データストアをリセットし、データストア内のファイルの順序をシャッフルします。

reset(adsTrain)
adsTrain = shuffle(adsTrain);
adsValidation = shuffle(adsValidation);

関数 detectSpeech は、統計に基づくしきい値を計算して音声領域を判定します。しきい値を直接指定することで、しきい値の計算をスキップして関数 detectSpeech の速度を上げることができます。データセットのしきい値を決定するには、ファイルのサンプリングに対して detectSpeech を呼び出し、計算されたしきい値を取得します。しきい値の平均を取ります。

T = zeros(500,2);
for ii = 1:500
     data = read(adsTrain); 
     [~,T(ii,:)] = detectSpeech(data,fs,Window=win);
end

T = mean(T,1);

reset(adsTrain)

学習データセットから複数の音声ファイルを結合し、1000 秒間の学習信号を作成します。detectSpeech を使用して、各ファイルの不要な部分を削除します。音声セグメントの間に、ランダムな無音期間を挿入します。

学習信号を事前に割り当てます。

duration = 1000*fs;
audioTraining = zeros(duration,1);

音声区間の学習マスクを事前に割り当てます。マスク内にある 1 の値は、音声区間が存在する部分にあるサンプルに対応します。0 の値は、音声区間が存在しない部分に対応します。

maskTraining = zeros(duration,1);

無音セグメントの最大持続時間を 2 秒に指定します。

maxSilenceSegment = 2;

ループ内でデータストアに対して read を呼び出し、学習信号を構築します。

numSamples = 1;    

while numSamples < duration
    data = read(adsTrain);
    data = data ./ max(abs(data)); % Scale amplitude

    % Determine regions of speech
    idx = detectSpeech(data,fs,Window=win,Thresholds=T);

    % If a region of speech is detected
    if ~isempty(idx)
        
        % Extend the indices by five frames
        idx(1,1) = max(1,idx(1,1) - 5*numel(win));
        idx(1,2) = min(length(data),idx(1,2) + 5*numel(win));
        
        % Isolate the speech
        data = data(idx(1,1):idx(1,2));
        
        % Write speech segment to training signal
        audioTraining(numSamples:numSamples+numel(data)-1) = data;
        
        % Set VAD baseline
        maskTraining(numSamples:numSamples+numel(data)-1) = true;
        
        % Random silence period
        numSilenceSamples = randi(maxSilenceSegment*fs,1,1);
        numSamples = numSamples + numel(data) + numSilenceSamples;
    end
end
audioTraining = audioTraining(1:duration);
maskTraining = maskTraining(1:duration);

学習信号の 10 秒間の部分を可視化します。ベースラインの音声区間マスクをプロットします。

figure
range = 1:10*fs;
plot((1/fs)*(range-1),audioTraining(range));
hold on
plot((1/fs)*(range-1),maskTraining(range),LineWidth=2);
grid on
xlabel("Time (s)")
legend("Signal","Speech Region")
title("Training Signal (first 10 seconds)");

学習信号の最初の 10 秒を再生します。

sound(audioTraining(range),fs);

学習信号へのノイズの追加

洗濯機のノイズを使用して学習信号を破損させます。つまり、S/N 比が -10 dB となるように、音声信号に洗濯機のノイズを追加します。

8 kHz のノイズを読み取り、16 kHz に変換します。

noise = audioread("WashingMachine-16-8-mono-1000secs.mp3");
noise = resample(noise,2,1);

ノイズを使用して学習信号を破損させます。

noise = noise(1:numel(audioTraining));
SNR = -10;
noise = 10^(-SNR/20) * noise * norm(audioTraining) / norm(noise);
audioTrainingNoisy = audioTraining + noise; 
audioTrainingNoisy = audioTrainingNoisy / max(abs(audioTrainingNoisy));

ノイズを含む学習信号内の長さ 10 秒の部分を可視化します。ベースラインの音声区間マスクをプロットします。

figure
plot((1/fs)*(range-1),audioTrainingNoisy(range));
hold on
plot((1/fs)*(range-1),maskTraining(range),LineWidth=2);
grid on
xlabel("Time (s)")
legend("Noisy Signal","Speech Area")
title("Training Signal (first 10 seconds)");

ノイズを含む学習信号の最初の 10 秒を再生します。

sound(audioTrainingNoisy(range),fs)

ベースラインの音声区間マスクは、音声と無音から成るノイズを含まない信号を使用して取得されたことに注意してください。ノイズで破損した信号に対して detectSpeech を使用すると良好な結果が得られないことを確認します。

speechIndices = detectSpeech(audioTrainingNoisy,fs,Window=win);

speechIndices(:,1) = max(1,speechIndices(:,1) - 5*numel(win));
speechIndices(:,2) = min(numel(audioTrainingNoisy),speechIndices(:,2) + 5*numel(win));

noisyMask = zeros(size(audioTrainingNoisy));
for ii = 1:size(speechIndices)
    noisyMask(speechIndices(ii,1):speechIndices(ii,2)) = 1;
end

ノイズを含む学習信号内の長さ 10 秒の部分を可視化します。ノイズを含む信号を解析して得られた音声区間マスクをプロットします。

figure
plot((1/fs)*(range-1),audioTrainingNoisy(range));
hold on
plot((1/fs)*(range-1),noisyMask(range),LineWidth=2);
grid on
xlabel("Time (s)")
legend("Noisy Signal","Mask from Noisy Signal")
title("Training Signal (first 10 seconds)");

音声と無音から成る検証信号の作成

学習済みのネットワークを検証するため、ノイズを含む 200 秒の音声信号を作成します。検証データストアを使用します。検証データストアと学習データストアでは話者が異なることに注意してください。

検証信号と検証マスクを事前に割り当てます。このマスクを使用して、学習済みネットワークの精度を評価します。

duration = 200*fs;
audioValidation = zeros(duration,1);
maskValidation = zeros(duration,1);

ループ内でデータストアに対して read を呼び出し、検証信号を構築します。

numSamples = 1;    
while numSamples < duration
    data = read(adsValidation);
    data = data ./ max(abs(data)); % Normalize amplitude
    
    % Determine regions of speech
    idx = detectSpeech(data,fs,Window=win,Thresholds=T);
    
    % If a region of speech is detected
    if ~isempty(idx)
        
        % Extend the indices by five frames
        idx(1,1) = max(1,idx(1,1) - 5*numel(win));
        idx(1,2) = min(length(data),idx(1,2) + 5*numel(win));

        % Isolate the speech
        data = data(idx(1,1):idx(1,2));
        
        % Write speech segment to training signal
        audioValidation(numSamples:numSamples+numel(data)-1) = data;
        
        % Set VAD Baseline
        maskValidation(numSamples:numSamples+numel(data)-1) = true;
        
        % Random silence period
        numSilenceSamples = randi(maxSilenceSegment*fs,1,1);
        numSamples = numSamples + numel(data) + numSilenceSamples;
    end
end

洗濯機のノイズを使用して検証信号を破損させます。このとき、S/N 比が -10 dB となるように、音声信号に洗濯機のノイズを追加します。検証信号には、学習信号で使用したものとは異なるノイズ ファイルを使用します。

noise = audioread("WashingMachine-16-8-mono-200secs.mp3");
noise = resample(noise,2,1);
noise = noise(1:duration);
audioValidation = audioValidation(1:numel(noise));

noise = 10^(-SNR/20) * noise * norm(audioValidation) / norm(noise);
audioValidationNoisy = audioValidation + noise; 
audioValidationNoisy = audioValidationNoisy / max(abs(audioValidationNoisy));

学習特徴の抽出

この例では、次の特徴を使用して LSTM ネットワークに学習させます。

  1. spectralCentroid (Audio Toolbox)

  2. spectralCrest (Audio Toolbox)

  3. spectralEntropy (Audio Toolbox)

  4. spectralFlux (Audio Toolbox)

  5. spectralKurtosis (Audio Toolbox)

  6. spectralRolloffPoint (Audio Toolbox)

  7. spectralSkewness (Audio Toolbox)

  8. spectralSlope (Audio Toolbox)

  9. harmonicRatio (Audio Toolbox)

この例では、audioFeatureExtractor (Audio Toolbox) を使用して、特徴セットのための最適な特徴抽出パイプラインを作成します。特徴セットを抽出するための audioFeatureExtractor オブジェクトを作成します。50% オーバーラップする 256 ポイントのハン ウィンドウを使用します。

afe = audioFeatureExtractor(SampleRate=fs, ...
    Window=hann(256,"Periodic"), ...
    OverlapLength=128, ...
    ...
    spectralCentroid=true, ...
    spectralCrest=true, ...
    spectralEntropy=true, ...
    spectralFlux=true, ...
    spectralKurtosis=true, ...
    spectralRolloffPoint=true, ...
    spectralSkewness=true, ...
    spectralSlope=true, ...
    harmonicRatio=true);

featuresTraining = extract(afe,audioTrainingNoisy);

特徴行列の次元を表示します。1 番目の次元は、信号が分割されるウィンドウの数に対応します (これは、ウィンドウの長さとオーバーラップの長さによって決まります)。2 番目の次元は、この例で使用される特徴の数です。

[numWindows,numFeatures] = size(featuresTraining)
numWindows = 124999
numFeatures = 9

分類アプリケーションでは、ゼロ平均、標準偏差 1 となるようにすべての特徴を正規化することをお勧めします。

各係数の平均と標準偏差を計算し、それらを使用してデータを正規化します。

M = mean(featuresTraining,1);
S = std(featuresTraining,[],1);
featuresTraining = (featuresTraining - M) ./ S;

同じプロセスを使用して、検証信号から特徴を抽出します。

featuresValidation = extract(afe,audioValidationNoisy);
featuresValidation = (featuresValidation - mean(featuresValidation,1)) ./ std(featuresValidation,[],1);

各特徴は、128 サンプルのデータ (ホップ長) に対応します。各ホップについて、128 サンプルに対応するベースライン マスク値のモードとして、期待される音声/非音声の値を設定します。音声/非音声マスクを categorical に変換します。

windowLength = numel(afe.Window);
hopLength = windowLength - afe.OverlapLength;
range = hopLength*(1:size(featuresTraining,1)) + hopLength;
maskMode = zeros(size(range));
for index = 1:numel(range)
    maskMode(index) = mode(maskTraining( (index-1)*hopLength+1:(index-1)*hopLength+windowLength ));
end
maskTraining = maskMode.';

maskTrainingCat = categorical(maskTraining);

検証マスクについても同じことを行います。

range = hopLength*(1:size(featuresValidation,1)) + hopLength;
maskMode = zeros(size(range));
for index = 1:numel(range)
    maskMode(index) = mode(maskValidation( (index-1)*hopLength+1:(index-1)*hopLength+windowLength ));
end
maskValidation = maskMode.';

maskValidationCat = categorical(maskValidation);

学習特徴とマスクを長さ 800 のシーケンスに分割します。このとき、連続するシーケンスを 75% オーバーラップさせます。

sequenceLength = 800;
sequenceOverlap = round(0.75*sequenceLength);

trainFeatureCell = helperFeatureVector2Sequence(featuresTraining',sequenceLength,sequenceOverlap);
trainLabelCell = helperFeatureVector2Sequence(maskTrainingCat',sequenceLength,sequenceOverlap);

LSTM ネットワーク アーキテクチャの定義

LSTM ネットワークは、シーケンス データのタイム ステップ間の長期的な依存関係を学習できます。この例では、双方向の LSTM 層 bilstmLayer を使用し、シーケンスを順方向および逆方向の両方で確認します。

入力サイズを長さ 9 (特徴の数) のシーケンスとして指定します。出力サイズが 200 である隠れ双方向 LSTM 層を指定し、シーケンスを出力します。このコマンドは、双方向 LSTM 層に対し、入力時系列を 200 個の特徴にマッピングするよう指示します (これらの特徴は次の層に渡されます)。さらに、出力サイズが 200 の双方向 LSTM 層を指定し、シーケンスの最後の要素を出力します。このコマンドは、双方向の LSTM 層に対し、入力を 200 の特徴にマッピングするよう指示し、その後、全結合層への出力を準備します。最後に、サイズが 2 の全結合層を含めることによって 2 個のクラスを指定し、その後にソフトマックス層と分類層を配置します。

layers = [ ...    
    sequenceInputLayer(afe.FeatureVectorLength)
    bilstmLayer(200,OutputMode="sequence")
    bilstmLayer(200,OutputMode="sequence")
    fullyConnectedLayer(2)
    softmaxLayer
    classificationLayer
    ];

次に、分類器の学習オプションを指定します。ネットワークが学習データから 20 個のパスを作るよう、MaxEpochs20 に設定します。ネットワークが一度に 64 個の学習信号を確認するよう、MiniBatchSize64 に設定します。Plots"training-progress" に設定し、反復回数の増大に応じた学習の進行状況を示すプロットを生成します。Verbosefalse に設定し、プロットで示されるデータに対応する表出力の表示を無効にします。Shuffle"every-epoch" に設定し、各エポックの最初に学習シーケンスをシャッフルします。LearnRateSchedule"piecewise" に設定し、特定のエポック数 (10) が経過するたびに、指定された係数 (0.1) によって学習率を減らします。ValidationData を検証予測子とターゲットに設定します。

この例では、適応モーメント推定 (ADAM) ソルバーを使用します。ADAM は、LSTM などの再帰型ニューラル ネットワーク (RNN) では、既定のモーメンタム項付き確率的勾配降下法 (SGDM) ソルバーよりもパフォーマンスにおいて優れています。

maxEpochs = 20;
miniBatchSize = 64;
options = trainingOptions("adam", ...
    MaxEpochs=maxEpochs, ...
    MiniBatchSize=miniBatchSize, ...
    Shuffle="every-epoch", ...
    Verbose=0, ...
    SequenceLength=sequenceLength, ...
    ValidationFrequency=floor(numel(trainFeatureCell)/miniBatchSize), ...
    ValidationData={featuresValidation.',maskValidationCat.'}, ...
    Plots="training-progress", ...
    LearnRateSchedule="piecewise", ...
    LearnRateDropFactor=0.1, ...
    LearnRateDropPeriod=5);

LSTM ネットワークの学習

trainNetwork を使用し、指定した学習オプションと層のアーキテクチャで LSTM ネットワークに学習させます。学習セットが大きいため、学習プロセスには数分かかる場合があります。

speedupExample = true;
if speedupExample
   [speechDetectNet,netInfo] = trainNetwork(trainFeatureCell,trainLabelCell,layers,options);
    display("Validation accuracy: " + netInfo.FinalValidationAccuracy + "percent.");
else
    load speechDetectNet
end

    "Validation accuracy: 91percent."

学習済みネットワークを使用した音声区間の検出

学習済みネットワークを使用して、検証信号に含まれる音声区間を推定します。推定された VAD マスクを categorical から double に変換します。

EstimatedVADMask = classify(speechDetectNet,featuresValidation.');
EstimatedVADMask = double(EstimatedVADMask);
EstimatedVADMask = EstimatedVADMask.' - 1;

実際のラベルと推定されたラベルのベクトルから、検証用の混同行列を計算してプロットします。

figure
confusionchart(maskValidation,EstimatedVADMask, ...
    title="Validation Accuracy",ColumnSummary="column-normalized",RowSummary="row-normalized");

ネットワークまたは特徴抽出パイプラインのパラメーターを変更した場合、新しいネットワークと audioFeatureExtractor オブジェクトが格納された MAT ファイルを再保存することを検討してください。

resaveNetwork = false;
if resaveNetwork
     save("Audio_VoiceActivityDetectionExample.mat","speechDetectNet","afe");
end

サポート関数

特徴ベクトルからシーケンスへの変換

function [sequences,sequencePerFile] = helperFeatureVector2Sequence(features,featureVectorsPerSequence,featureVectorOverlap)
    if featureVectorsPerSequence <= featureVectorOverlap
        error("The number of overlapping feature vectors must be less than the number of feature vectors per sequence.")
    end

    if ~iscell(features)
        features = {features};
    end
    hopLength = featureVectorsPerSequence - featureVectorOverlap;
    idx1 = 1;
    sequences = {};
    sequencePerFile = cell(numel(features),1);
    for ii = 1:numel(features)
        sequencePerFile{ii} = floor((size(features{ii},2) - featureVectorsPerSequence)/hopLength) + 1;
        idx2 = 1;
        for j = 1:sequencePerFile{ii}
            sequences{idx1,1} = features{ii}(:,idx2:idx2 + featureVectorsPerSequence - 1); %#ok<AGROW>
            idx1 = idx1 + 1;
            idx2 = idx2 + hopLength;
        end
    end
end

ストリーミングのデモ

function helperStreamingDemo(speechDetectNet,afe,cleanSpeech,noise,testDuration,sequenceLength,sequenceHop,signalToListenTo,noiseGain)

音声ファイルとノイズ ファイルをフレームごとに読み取るための dsp.AudioFileReader (DSP System Toolbox) オブジェクトを作成します。

    speechReader = dsp.AudioFileReader(cleanSpeech,PlayCount=inf);
    noiseReader = dsp.AudioFileReader(noise,PlayCount=inf);
    fs = speechReader.SampleRate;

dsp.MovingStandardDeviation (DSP System Toolbox) オブジェクトと dsp.MovingAverage (DSP System Toolbox) オブジェクトを作成します。これらを使用して、正規化のためにオーディオ特徴の標準偏差と平均値を割り出します。この統計量は、徐々に改善されるはずです。

    movSTD = dsp.MovingStandardDeviation(Method="Exponential weighting",ForgettingFactor=1);
    movMean = dsp.MovingAverage(Method="Exponential weighting",ForgettingFactor=1);

3 つの dsp.AsyncBuffer (DSP System Toolbox) オブジェクトを作成します。1 つは入力オーディオのバッファー用、1 つは抽出した特徴のバッファー用、もう 1 つは出力バッファーのバッファー用です。出力バッファーは、判定をリアルタイムで可視化する場合のみ必要となります。

    audioInBuffer = dsp.AsyncBuffer;
    featureBuffer = dsp.AsyncBuffer;
    audioOutBuffer = dsp.AsyncBuffer;

オーディオ バッファーでは、元のクリーンな音声信号とノイズを含む信号の両方がバッファーされます。再生するのは指定した signalToListenTo のみです。変数 signalToListenTo を、再生したいチャネルに変換します。

    channelToListenTo = 1;
    if strcmp(signalToListenTo,"clean")
        channelToListenTo = 2;
    end

元の音声信号、ネットワーク適用対象のノイズを含む信号、およびネットワークからの判定出力を可視化するために、時間スコープを作成します。

    scope = timescope(SampleRate=fs, ...
        TimeSpanSource="property", ...
        TimeSpan=3, ...
        BufferLength=fs*3*3, ...
        YLimits=[-1 1], ...
        TimeSpanOverrunAction="Scroll", ...
        ShowGrid=true, ...
        NumInputPorts=3, ...
        LayoutDimensions=[3,1], ...
        Title="Noisy Speech");
    scope.ActiveDisplay = 2;
    scope.Title = "Clean Speech (Original)";
    scope.YLimits = [-1 1];
    scope.ActiveDisplay = 3;
    scope.Title = "Detected Speech";
    scope.YLimits = [-1 1];

元のオーディオまたはノイズを含むオーディオをスピーカーから再生するために、audioDeviceWriter (Audio Toolbox) オブジェクトを作成します。

    deviceWriter = audioDeviceWriter(SampleRate=fs);

ループで使用される変数を初期化します。

    windowLength = numel(afe.Window);
    hopLength = windowLength - afe.OverlapLength;
    myMax = 0;
    audioBufferInitialized = false;
    featureBufferInitialized = false;

ストリーミングのデモを実行します。

    tic
    while toc < testDuration
    
        % Read a frame of the speech signal and a frame of the noise signal
        speechIn = speechReader();
        noiseIn = noiseReader();
        
        % Mix the speech and noise at the specified SNR
        noisyAudio = speechIn + noiseGain*noiseIn;
        
        % Update a running max for normalization
        myMax = max(myMax,max(abs(noisyAudio)));
        
        % Write the noisy audio and speech to buffers
        write(audioInBuffer,[noisyAudio,speechIn]);
        
        % If enough samples are buffered,
        % mark the audio buffer as initialized and push the read pointer
        % for the audio buffer up a window length.
        if audioInBuffer.NumUnreadSamples >= windowLength && ~audioBufferInitialized
            audioBufferInitialized = true;
            read(audioInBuffer,windowLength);
        end
        
        % If enough samples are in the audio buffer to calculate a feature
        % vector, read the samples, normalize them, extract the feature vectors, and write
        % the latest feature vector to the features buffer.
        while (audioInBuffer.NumUnreadSamples >= hopLength) && audioBufferInitialized
            x = read(audioInBuffer,windowLength + hopLength,windowLength);
            write(audioOutBuffer,x(end-hopLength+1:end,:));
            noisyAudio = x(:,1);
            noisyAudio = noisyAudio/myMax;
            features = extract(afe,noisyAudio);
            write(featureBuffer,features(2,:));
        end
        
        % If enough feature vectors are buffered, mark the feature buffer
        % as initialized and push the read pointer for the feature buffer
        % and the audio output buffer (so that they are in sync).
        if featureBuffer.NumUnreadSamples >= (sequenceLength + sequenceHop) && ~featureBufferInitialized
            featureBufferInitialized = true;
            read(featureBuffer,sequenceLength - sequenceHop);
            read(audioOutBuffer,(sequenceLength - sequenceHop)*windowLength);
        end
        
       while featureBuffer.NumUnreadSamples >= sequenceHop && featureBufferInitialized
            features = read(featureBuffer,sequenceLength,sequenceLength - sequenceHop);
            features(isnan(features)) = 0;
            
            % Use only the new features to update the
            % standard deviation and mean. Normalize the features.
            localSTD = movSTD(features(end-sequenceHop+1:end,:));
            localMean = movMean(features(end-sequenceHop+1:end,:));
            features = (features - localMean(end,:)) ./ localSTD(end,:);
            
            decision = classify(speechDetectNet,features');
            decision = decision(end-sequenceHop+1:end);
            decision = double(decision)' - 1;
            decision = repelem(decision,hopLength);
            
            audioHop = read(audioOutBuffer,sequenceHop*hopLength);
            
            % Listen to the speech or speech+noise
            deviceWriter(audioHop(:,channelToListenTo));
            
            % Visualize the speech+noise, the original speech, and the
            % voice activity detection.
            scope(audioHop(:,1),audioHop(:,2),audioHop(:,1).*decision)
       end
    end
    release(deviceWriter)
    release(audioInBuffer)
    release(audioOutBuffer)
    release(featureBuffer)
    release(movSTD)
    release(movMean)
    release(scope)
end

参考文献

[1] Warden P. "Speech Commands: A public dataset for single-word speech recognition", 2017. Available from https://storage.googleapis.com/download.tensorflow.org/data/speech_commands_v0.01.tar.gz. Copyright Google 2017. Speech Commands Dataset は、Creative Commons Attribution 4.0 license に従ってライセンスされています。

参考

|

関連するトピック