Main Content

オーディオの特徴に関する逐次特徴選択

この例では、数字の音声認識タスクに適用される特徴選択の標準的なワークフローを説明します。

逐次特徴選択では、与えらえた特徴セットでネットワークに学習させた後、精度が最大になるまで特徴を段階的に追加または削除します [1]。この例では、Free Spoken Digit データセット [2] を使用して、逐次特徴選択を数字音声認識タスクに適用します。

ストリーミング数字音声の認識

この例を進めるために、まず、事前学習済みのネットワーク、ネットワークの学習に使用する audioFeatureExtractor (Audio Toolbox) オブジェクト、および特徴の正規化係数を読み込みます。

load("network_Audio_SequentialFeatureSelection.mat","bestNet","afe","normalizers");

マイクからオーディオを読み取るために audioDeviceReader (Audio Toolbox) を作成します。3 つの dsp.AsyncBuffer (DSP System Toolbox) オブジェクトを作成します。1 つは、マイクから読み取ったオーディオをバッファーするため、1 つは、音声検出用に入力オーディオの短時間エネルギーをバッファーするため、もう 1 つは予測をバッファーするために使用します。

fs = afe.SampleRate;

deviceReader = audioDeviceReader(SampleRate=fs,SamplesPerFrame=256);

audioBuffer = dsp.AsyncBuffer(fs*3);
steBuffer = dsp.AsyncBuffer(1000);
predictionBuffer = dsp.AsyncBuffer(5);

ストリーミング オーディオ、推論中のネットワーク出力の確率、および予測を表示するためのプロットを作成します。

fig = figure;

streamAxes = subplot(3,1,1);
streamPlot = plot(zeros(fs,1));
ylabel("Amplitude")
xlabel("Time (s)")
title("Audio Stream")
streamAxes.XTick = [0,fs];
streamAxes.XTickLabel = [0,1];
streamAxes.YLim = [-1,1];

analyzedAxes = subplot(3,1,2);
analyzedPlot = plot(zeros(fs/2,1));
title("Analyzed Segment")
ylabel("Amplitude")
xlabel("Time (s)")
set(gca,XTickLabel=[])
analyzedAxes.XTick = [0,fs/2];
analyzedAxes.XTickLabel = [0,0.5];
analyzedAxes.YLim = [-1,1];

probabilityAxes = subplot(3,1,3);
probabilityPlot = bar(0:9,0.1*ones(1,10));
axis([-1,10,0,1])
ylabel("Probability")
xlabel("Class")

ストリーミング数字音声 (0 ~ 9 の数字) の認識を 20 秒間実行します。ループを実行している間、数字をどれか読み上げて精度をテストします。

最初に、短時間エネルギーのしきい値を定義します。このしきい値より小さい信号には音声が含まれていないとみなします。

steThreshold = 0.015;
idxVec = 1:fs;
tic
while toc < 20
    
    % Read in a frame of audio from your device.
    audioIn = deviceReader();
    
    % Write the audio into a the buffer.
    write(audioBuffer,audioIn);
    
    % While 200 ms of data is unused, continue this loop.
    while audioBuffer.NumUnreadSamples > 0.2*fs
        
        % Read 1 second from the audio buffer. Of that 1 second, 800 ms
        % is rereading old data and 200 ms is new data.
        audioToAnalyze = read(audioBuffer,fs,0.8*fs);
        
        % Update the figure to plot the current audio data.
        streamPlot.YData = audioToAnalyze;

        ste = mean(abs(audioToAnalyze));
        write(steBuffer,ste);
        if steBuffer.NumUnreadSamples > 5
            abc = sort(peek(steBuffer));
            steThreshold = abc(round(0.4*numel(abc)));
        end
        if ste > steThreshold
            
            % Use the detectSpeeech function to determine if a region of speech
            % is present.
            idx = detectSpeech(audioToAnalyze,fs);
            
            % If a region of speech is present, perform the following.
            if ~isempty(idx)
                % Zero out all parts of the signal except the speech
                % region, and trim to 0.5 seconds.
                audioToAnalyze = trimOrPad(audioToAnalyze(idx(1,1):idx(1,2)),fs/2);
                
                % Normalize the audio.
                audioToAnalyze = audioToAnalyze/max(abs(audioToAnalyze));
                
                % Update the analyzed segment plot
                analyzedPlot.YData = audioToAnalyze;

                % Extract the features and transpose them so that time is
                % across columns.
                features = (extract(afe,audioToAnalyze))';

                % Normalize the features.
                features = (features - normalizers.Mean) ./ normalizers.StandardDeviation;
                
                % Call classify to determine the probabilities and the
                % winning label.
                features(isnan(features)) = 0;
                [label,probs] = classify(bestNet,features);
                
                % Update the plot with the probabilities and the winning
                % label.
                probabilityPlot.YData = probs;
                write(predictionBuffer,probs);

                if predictionBuffer.NumUnreadSamples == predictionBuffer.Capacity
                    lastTen = peek(predictionBuffer);
                    [~,decision] = max(mean(lastTen.*hann(size(lastTen,1)),1));
                    probabilityAxes.Title.String = num2str(decision-1);
                end
            end
        else
            % If the signal energy is below the threshold, assume no speech
            % detected.
             probabilityAxes.Title.String = "";
             probabilityPlot.YData = 0.1*ones(10,1);
             analyzedPlot.YData = zeros(fs/2,1);
             reset(predictionBuffer)
        end
        
        drawnow limitrate
    end
end

この例の残りでは、ストリーミング検出に使用したネットワークがどのように学習されたか、およびネットワークに渡される特徴がどのように選択されたかを説明します。

学習データセットと検証データセットの作成

Free Spoken Digit データセット (FSDD) をダウンロードします [2]。FSDD には、数字音声 (0 ~ 9) の短いオーディオ ファイルが含まれています。

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

録音を指す audioDatastore (Audio Toolbox) を作成します。データ セットのサンプル レートを指定します。

ads = audioDatastore(dataset,IncludeSubfolders=true);
[~,adsInfo] = read(ads);
fs = adsInfo.SampleRate;

ファイル名の最初の要素は、ファイルに含まれる数字音声です。ファイル名の最初の要素を取得して categorical に変換し、audioDatastoreLabels プロパティを設定します。

[~,filenames] = cellfun(@(x)fileparts(x),ads.Files,UniformOutput=false);
ads.Labels = categorical(string(cellfun(@(x)x(1),filenames)));

データストアを開発セットと検証セットに分割するには、splitEachLabel (Audio Toolbox) を使用します。データの 80% を開発に割り当て、残りの 20% を検証に割り当てます。

[adsTrain,adsValidation] = splitEachLabel(ads,0.8);

オーディオ特徴抽出器のセットアップ

ウィンドウ幅 30 ms、更新頻度 10 ms でオーディオ特徴を抽出する audioFeatureExtractor (Audio Toolbox) オブジェクトを作成します。この例でテストしたい特徴をすべて true に設定します。

win = hamming(round(0.03*fs),"periodic");
overlapLength = round(0.02*fs);

afe = audioFeatureExtractor( ...
    Window=win, ...
    OverlapLength=overlapLength, ...
    SampleRate=fs, ...
    ...
    linearSpectrum=false, ...
    melSpectrum=false, ...
    barkSpectrum=false, ...
    erbSpectrum=false, ...
    ...
    mfcc=true, ...
    mfccDelta=true, ...
    mfccDeltaDelta=true, ...
    gtcc=true, ...
    gtccDelta=true, ...
    gtccDeltaDelta=true, ...
    ...
    spectralCentroid=true, ...
    spectralCrest=true, ...
    spectralDecrease=true, ...
    spectralEntropy=true, ...
    spectralFlatness=true, ...
    spectralFlux=true, ...
    spectralKurtosis=true, ...
    spectralRolloffPoint=true, ...
    spectralSkewness=true, ...
    spectralSlope=true, ...
    spectralSpread=true, ...
    ...
    pitch=false, ...
    harmonicRatio=false, ...
    zerocrossrate=false, ...
    shortTimeEnergy=false);

層と学習オプションの定義

この例で使用する 深層学習層の一覧trainingOptions を定義します。最初の層である sequenceInputLayer は、単なるプレースホルダーです。最初の層は、逐次特徴選択でどの特徴をテストするかに応じて、適切なサイズの sequenceInputLayer に置き換えられます。

numUnits = 100;
layers = [ ...
    sequenceInputLayer(1)
    bilstmLayer(numUnits,OutputMode="last")
    fullyConnectedLayer(numel(categories(adsTrain.Labels)))
    softmaxLayer
    classificationLayer];

options = trainingOptions("adam", ...
    LearnRateSchedule="piecewise", ...
    Shuffle="every-epoch", ...
    Verbose=false, ...
    MaxEpochs=20);

逐次特徴選択

逐次特徴選択の基本形式では、与えらえた特徴セットでネットワークに学習させた後、精度が向上しなくなるまで特徴を段階的に追加または削除します [1]

前方選択

4 つの特徴のセットに前方選択を適用する単純なケースについて考えてみましょう。最初の前方選択ループでは、ネットワークを学習し検証精度を比較することによって、4 つの特徴がそれぞれ個別にテストされます。検証精度が最も高い特徴が記録されます。2 番目の前方選択ループでは、最初のループで得られた最適な特徴が残りの特徴とそれぞれ組み合わされます。次に、特徴の各ペアを使用して学習が実行されます。2 番目のループの精度が最初のループの精度より向上しなかった場合、選択プロセスは終了します。そうでない場合、新しい最適な特徴セットが選択されます。前方選択ループは、精度が向上しなくなるまで続けられます。

後方選択

後方特徴選択では、最初にすべての特徴が含まれる特徴セットで学習させた後、特徴を削除して精度が向上するかどうかをテストします。

逐次特徴選択の実行

補助関数 (sequentialFeatureSelectiontrainAndValidateNetwork、および trimOrPad) は、前方逐次特徴選択または後方逐次特徴選択を実装します。学習データストア、検証データストア、オーディオ特徴抽出器、ネットワーク層、ネットワーク オプション、および方向を指定します。原則として、小さい特徴セットが予想される場合は前方を選択し、大きい特徴セットが予想される場合は後方を選択します。

direction = 'forward';
[logbook,bestFeatures,bestNet,normalizers] = sequentialFeatureSelection(adsTrain,adsValidation,afe,layers,options,direction);

HelperFeatureExtractor からの出力 logbook は、テストされたすべての特徴の構成および対応する検証精度が格納されたテーブルです。

logbook
logbook=62×2 table
                                  Features                                   Accuracy
    _____________________________________________________________________    ________

    "mfccDelta, spectralKurtosis, spectralRolloffPoint"                       98.25  
    "mfccDelta, spectralRolloffPoint"                                         97.75  
    "mfccDelta, spectralEntropy, spectralRolloffPoint"                        97.75  
    "mfccDelta, spectralDecrease, spectralKurtosis, spectralRolloffPoint"     97.25  
    "mfccDelta, mfccDeltaDelta"                                                  97  
    "mfccDelta, gtccDeltaDelta, spectralRolloffPoint"                            97  
    "mfcc, mfccDelta, spectralKurtosis, spectralRolloffPoint"                    97  
    "mfcc, mfccDelta"                                                         96.75  
    "mfccDelta, gtccDeltaDelta, spectralKurtosis, spectralRolloffPoint"       96.75  
    "mfccDelta, spectralRolloffPoint, spectralSlope"                           96.5  
    "mfccDelta"                                                               96.25  
    "mfccDelta, spectralKurtosis"                                             96.25  
    "mfccDelta, spectralSpread"                                               96.25  
    "mfccDelta, spectralDecrease, spectralRolloffPoint"                       96.25  
    "mfccDelta, spectralFlatness, spectralKurtosis, spectralRolloffPoint"     96.25  
    "mfccDelta, gtccDeltaDelta"                                                  96  
      ⋮

sequentialFeatureSelection からの出力 bestFeatures には、最適な特徴が true に設定された構造体が含まれています。

bestFeatures
bestFeatures = struct with fields:
                    mfcc: 0
               mfccDelta: 1
          mfccDeltaDelta: 0
                    gtcc: 0
               gtccDelta: 0
          gtccDeltaDelta: 0
        spectralCentroid: 0
           spectralCrest: 0
        spectralDecrease: 0
         spectralEntropy: 0
        spectralFlatness: 0
            spectralFlux: 0
        spectralKurtosis: 1
    spectralRolloffPoint: 1
        spectralSkewness: 0
           spectralSlope: 0
          spectralSpread: 0

この構造体を使用して audioFeatureExtractor を設定できます。

set(afe,bestFeatures)
afe
afe = 
  audioFeatureExtractor with properties:

   Properties
                     Window: [240×1 double]
              OverlapLength: 160
                 SampleRate: 8000
                  FFTLength: []
    SpectralDescriptorInput: 'linearSpectrum'
        FeatureVectorLength: 15

   Enabled Features
     mfccDelta, spectralKurtosis, spectralRolloffPoint

   Disabled Features
     linearSpectrum, melSpectrum, barkSpectrum, erbSpectrum, mfcc, mfccDeltaDelta
     gtcc, gtccDelta, gtccDeltaDelta, spectralCentroid, spectralCrest, spectralDecrease
     spectralEntropy, spectralFlatness, spectralFlux, spectralSkewness, spectralSlope, spectralSpread
     pitch, harmonicRatio, 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.

sequentialFeatureSelection は、最も性能が良いネットワークと、選択された特徴に対応する正規化係数も出力します。ネットワーク、構成済みの audioFeatureExtractor、および正規化係数を保存するには、次の行のコメントを解除します。

% save('network_Audio_SequentialFeatureSelection.mat','bestNet','afe','normalizers')

まとめ

この例では、再帰型ニューラル ネットワーク (LSTM または BiLSTM) の逐次特徴選択のワークフローについて説明しました。これは、CNN および RNN-CNN のワークフローに簡単に適用することができます。

サポート関数

学習ネットワークと検証ネットワーク

function [trueLabels,predictedLabels,net,normalizers] = trainAndValidateNetwork(adsTrain,adsValidation,afe,layers,options)
% Train and validate a network.
%
%   INPUTS:
%   adsTrain      - audioDatastore object that points to training set
%   adsValidation - audioDatastore object that points to validation set
%   afe           - audioFeatureExtractor object.
%   layers        - Layers of LSTM or BiLSTM network
%   options       - trainingOptions object
%
%   OUTPUTS:
%   trueLabels      - true labels of validation set
%   predictedLabels - predicted labels of validation set
%   net             - trained network
%   normalizers     - normalization factors for features under test

% Copyright 2019 The MathWorks, Inc.

% Convert the data to tall arrays.
tallTrain = tall(adsTrain);
tallValidation = tall(adsValidation);

% Extract features from the training set. Reorient the features so that
% time is along rows to be compatible with sequenceInputLayer.
fs = afe.SampleRate;
tallTrain = cellfun(@(x)trimOrPad(x,fs/2),tallTrain,UniformOutput=false);
tallTrain = cellfun(@(x)x/max(abs(x),[],"all"),tallTrain,UniformOutput=false);
tallFeaturesTrain = cellfun(@(x)extract(afe,x),tallTrain,UniformOutput=false);
tallFeaturesTrain = cellfun(@(x)x',tallFeaturesTrain,UniformOutput=false);  %#ok<NASGU>
[~,featuresTrain] = evalc('gather(tallFeaturesTrain)'); % Use evalc to suppress command-line output.

tallValidation = cellfun(@(x)trimOrPad(x,fs/2),tallValidation,UniformOutput=false);
tallValidation = cellfun(@(x)x/max(abs(x),[],'all'),tallValidation,UniformOutput=false);
tallFeaturesValidation = cellfun(@(x)extract(afe,x),tallValidation,UniformOutput=false);
tallFeaturesValidation = cellfun(@(x)x',tallFeaturesValidation,UniformOutput=false); %#ok<NASGU>
[~,featuresValidation] = evalc('gather(tallFeaturesValidation)'); % Use evalc to suppress command-line output.

% Use the training set to determine the mean and standard deviation of each
% feature. Normalize the training and validation sets.
allFeatures = cat(2,featuresTrain{:});
M = mean(allFeatures,2,"omitnan");
S = std(allFeatures,0,2,"omitnan");
featuresTrain = cellfun(@(x)(x-M)./S,featuresTrain,UniformOutput=false);
for ii = 1:numel(featuresTrain)
    idx = find(isnan(featuresTrain{ii}));
    if ~isempty(idx)
        featuresTrain{ii}(idx) = 0;
    end
end
featuresValidation = cellfun(@(x)(x-M)./S,featuresValidation,UniformOutput=false);
for ii = 1:numel(featuresValidation)
    idx = find(isnan(featuresValidation{ii}));
    if ~isempty(idx)
        featuresValidation{ii}(idx) = 0;
    end
end

% Replicate the labels of the train and validation sets so that they are in
% one-to-one correspondence with the sequences.
labelsTrain = adsTrain.Labels;

% Update input layer for the number of features under test.
layers(1) = sequenceInputLayer(size(featuresTrain{1},1));

% Train the network.
net = trainNetwork(featuresTrain,labelsTrain,layers,options);

% Evaluate the network. Call classify to get the predicted labels for each
% sequence.
predictedLabels = classify(net,featuresValidation);
trueLabels = adsValidation.Labels;

% Save the normalization factors as a struct.
normalizers.Mean = M;
normalizers.StandardDeviation = S;
end

逐次特徴選択

function [logbook,bestFeatures,bestNet,bestNormalizers] = sequentialFeatureSelection(adsTrain,adsValidate,afeThis,layers,options,direction)
%
%   INPUTS:
%   adsTrain    - audioDatastore object that points to training set
%   adsValidate - audioDatastore object that points to validation set
%   afe         - audioFeatureExtractor object. Set all features to test to true
%   layers      - Layers of LSTM or BiLSTM network
%   options     - trainingOptions object
%   direction   - SFS direction, specify as 'forward' or 'backward'
%
%   OUTPUTS:
%   logbook         - table containing feature configurations tested and corresponding validation accuracies
%   bestFeatures    - struct containg best feature configuration
%   bestNet         - Trained network with highest validation accuracy
%   bestNormalizers - Feature normalization factors for best features

% Copyright 2019 The MathWorks, Inc.

afe = copy(afeThis);
featuresToTest = fieldnames(info(afe));
N = numel(featuresToTest);
bestValidationAccuracy = 0;

% Set the initial feature configuration: all on for backward selection
% or all off for forward selection.
featureConfig = info(afe);
for i = 1:N
    if strcmpi(direction,"backward")
        featureConfig.(featuresToTest{i}) = true;
    else
        featureConfig.(featuresToTest{i}) = false;
    end
end

% Initialize logbook to track feature configuration and accuracy.
logbook = table(featureConfig,0,VariableNames=["Feature Configuration","Accuracy"]);

% Perform sequential feature evaluation.
wrapperIdx = 1;
bestAccuracy = 0;
while wrapperIdx <= N
    % Create a cell array containing all feature configurations to test
    % in the current loop.
    featureConfigsToTest = cell(numel(featuresToTest),1);
    for ii = 1:numel(featuresToTest)
        if strcmpi(direction,"backward")
            featureConfig.(featuresToTest{ii}) = false;
        else
            featureConfig.(featuresToTest{ii}) = true;
        end
        featureConfigsToTest{ii} = featureConfig;
        if strcmpi(direction,"backward")
            featureConfig.(featuresToTest{ii}) = true;
        else
            featureConfig.(featuresToTest{ii}) = false;
        end
    end

    % Loop over every feature set.
    for ii = 1:numel(featureConfigsToTest)

        % Determine the current feature configuration to test. Update
        % the feature afe.
        currentConfig = featureConfigsToTest{ii};
        set(afe,currentConfig)

        % Train and get k-fold cross-validation accuracy for current
        % feature configuration.
        [trueLabels,predictedLabels,net,normalizers] = trainAndValidateNetwork(adsTrain,adsValidate,afe,layers,options);
        valAccuracy = mean(trueLabels==predictedLabels)*100;
        if valAccuracy > bestValidationAccuracy
            bestValidationAccuracy = valAccuracy;
            bestNet = net;
            bestNormalizers = normalizers;
        end

        % Update Logbook
        result = table(currentConfig,valAccuracy,VariableNames=["Feature Configuration","Accuracy"]);
        logbook = [logbook;result]; %#ok<AGROW> 

    end

    % Determine and print the setting with the best accuracy. If accuracy
    % did not improve, end the run.
    [a,b] = max(logbook{:,"Accuracy"});
    if a <= bestAccuracy
        wrapperIdx = inf;
    else
        wrapperIdx = wrapperIdx + 1;
    end
    bestAccuracy = a;

    % Update the features-to-test based on the most recent winner.
    winner = logbook{b,"Feature Configuration"};
    fn = fieldnames(winner);
    tf = structfun(@(x)(x),winner);
    if strcmpi(direction,"backward")
        featuresToRemove = fn(~tf);
    else
        featuresToRemove = fn(tf);
    end
    for ii = 1:numel(featuresToRemove)
        loc =  strcmp(featuresToTest,featuresToRemove{ii});
        featuresToTest(loc) = [];
        if strcmpi(direction,"backward")
            featureConfig.(featuresToRemove{ii}) = false;
        else
            featureConfig.(featuresToRemove{ii}) = true;
        end
    end

end

% Sort the logbook and make it more readable.
logbook(1,:) = []; % Delete placeholder first row.
logbook = sortrows(logbook,"Accuracy","descend");
bestFeatures = logbook{1,"Feature Configuration"};
m = logbook{:,"Feature Configuration"};
fn = fieldnames(m);
myString = strings(numel(m),1);
for wrapperIdx = 1:numel(m)
    tf = structfun(@(x)(x),logbook{wrapperIdx,"Feature Configuration"});
    myString(wrapperIdx) = strjoin(fn(tf),", ");
end
logbook = table(myString,logbook{:,"Accuracy"},VariableNames=["Features","Accuracy"]);
end

トリミングまたはパディング

function y = trimOrPad(x,n)
% y = trimOrPad(x,n) trims or pads the input x to n samples. If x is
% trimmed, it is trimmed equally on the front and back. If x is padded, it is
% padded equally on the front and back with zeros. For odd-length trimming or
% padding, the extra sample is trimmed or padded from the back.

% Copyright 2019 The MathWorks, Inc.
a = size(x,1);
if a < n
    frontPad = floor((n-a)/2);
    backPad = n - a - frontPad;
    y = [zeros(frontPad,1);x;zeros(backPad,1)];
elseif a > n
    frontTrim = floor((a-n)/2)+1;
    backTrim = a - n - frontTrim;
    y = x(frontTrim:end-backTrim);
else
    y = x;
end
end

参考文献

[1] Jain, A., and D. Zongker. "Feature Selection: Evaluation, Application, and Small Sample Performance." IEEE Transactions on Pattern Analysis and Machine Intelligence. Vol. 19, Issue 2, 1997, pp. 153-158.

[2] Jakobovski. “Jakobovski/Free-Spoken-Digit-Dataset.” GitHub, May 30, 2019. https://github.com/Jakobovski/free-spoken-digit-dataset.