Main Content

音声感情認識

この例では、BiLSTM ネットワークを使用した単純な音声感情認識 (SER) システムについて説明します。まず、データセットをダウンロードし、各ファイルについて学習済みのネットワークをテストします。ネットワークは、小規模なドイツ語のデータベースで学習済みです [1]

この例では、ネットワークの学習について順を追って説明し、データセットのダウンロード、拡張、および学習を行います。最後に、leave-one-speaker-out (LOSO) 10 分割交差検証を行い、ネットワーク アーキテクチャを評価します。

この例で使用する特徴は、Sequential Feature Selection for Audio Features (Audio Toolbox)で説明されている手法と同様に、逐次特徴選択を使用して選択されたものです。

データセットのダウンロード

Berlin Database of Emotional Speech をダウンロードします [1]。このデータベースには、10 人の俳優による 535 個の発話が含まれています。発話の意図は、怒り、退屈、嫌悪、不安/恐怖、幸福、悲しみ、中立のうちいずれかの感情を伝えることです。感情はテキストと無関係です。

dataFolder = tempdir;
dataset = fullfile(dataFolder,"Emo-DB");
if ~datasetExists(dataset)
    url = "http://emodb.bilderbar.info/download/download.zip";
	disp("Downloading Emo-DB (40.5 MB) ...")
    unzip(url,dataset)
end
Downloading Emo-DB (40.5 MB) ...

オーディオ ファイルを指す audioDatastore (Audio Toolbox) を作成します。

ads = audioDatastore(fullfile(dataset,"wav"));

ファイル名は、話者 ID、発話されたテキスト、感情、およびバージョンを表すコードです。Web サイトには、コードを解釈するためのヒント、および話者に関する追加の情報 (性別や年齢など) が記載されています。変数 SpeakerEmotion を含む table を作成します。ファイル名を table に復号化します。

filepaths = ads.Files;
emotionCodes = cellfun(@(x)x(end-5),filepaths,UniformOutput=false);
emotions = replace(emotionCodes,["W","L","E","A","F","T","N"], ...
    ["Anger","Boredom","Disgust","Anxiety/Fear","Happiness","Sadness","Neutral"]);

speakerCodes = cellfun(@(x)x(end-10:end-9),filepaths,UniformOutput=false);
labelTable = cell2table([speakerCodes,emotions],VariableNames=["Speaker","Emotion"]);
labelTable.Emotion = categorical(labelTable.Emotion);
labelTable.Speaker = categorical(labelTable.Speaker);
summary(labelTable)
Variables:

    Speaker: 535×1 categorical

        Values:

            03       49   
            08       58   
            09       43   
            10       38   
            11       55   
            12       35   
            13       61   
            14       69   
            15       56   
            16       71   

    Emotion: 535×1 categorical

        Values:

            Anger             127   
            Anxiety/Fear       69   
            Boredom            81   
            Disgust            46   
            Happiness          71   
            Neutral            79   
            Sadness            62   

labelTable の順序は、audioDatastore に含まれるファイルの順序と同じです。audioDatastoreLabels プロパティを labelTable に設定します。

ads.Labels = labelTable;

音声感情認識の実行

事前学習済みのネットワーク、ネットワークの学習に使用するaudioFeatureExtractor (Audio Toolbox)オブジェクト、および特徴の正規化係数をダウンロードして読み込みます。このネットワークは、データセットに含まれる、話者 03 を除くすべての話者を使用して学習されています。

downloadFolder = matlab.internal.examples.downloadSupportFile("audio","SpeechEmotionRecognition.zip");
dataFolder = tempdir;
unzip(downloadFolder,dataFolder)
netFolder = fullfile(dataFolder,"SpeechEmotionRecognition");
load(fullfile(netFolder,"network_Audio_SER.mat"));

audioFeatureExtractor に設定されたサンプル レートは、データセットのサンプル レートに対応しています。

fs = afe.SampleRate;

話者と感情を選択します。次に、選択した話者と感情のみが含まれるようにデータストアをサブセット化します。データストアからファイルを読み取り、再生します。

speaker = categorical("03");
emotion = categorical("Disgust");

adsSubset = subset(ads,ads.Labels.Speaker==speaker & ads.Labels.Emotion==emotion);

audio = read(adsSubset);
sound(audio,fs)

audioFeatureExtractor オブジェクトを使用して特徴を抽出し、時間が行に沿うように、抽出した特徴を転置します。特徴を正規化してから、10 要素のオーバーラップをもつ 20 要素のシーケンスに変換します。これは、300 ms のオーバーラップをもつ 600 ms のウィンドウにほぼ対応します。サポート関数 HelperFeatureVector2Sequence を使用して、特徴ベクトルの配列をシーケンスに変換します。

features = (extract(afe,audio))';

featuresNormalized = (features - normalizers.Mean)./normalizers.StandardDeviation;

numOverlap = 10;
featureSequences = HelperFeatureVector2Sequence(featuresNormalized,20,numOverlap);

この特徴シーケンスをネットワークに入力して予測を実行します。予測の平均を計算し、選択した感情の確率分布を円グラフにプロットします。さまざまな話者、感情、シーケンスのオーバーラップ、および予測の平均を試すことで、ネットワークの性能をテストできます。ネットワークの性能の現実的な近似を得るには、ネットワークの学習に使用されなかった話者 03 を使用します。

YPred = double(predict(net,featureSequences));

average = "mode";
switch average
    case "mean"
        probs = mean(YPred,1);
    case "median"
        probs = median(YPred,1);
    case "mode"
        probs = mode(YPred,1);
end

pie(probs./sum(probs),string(net.Layers(end).Classes))

この例の残りでは、ネットワークの学習と検証がどのように行われたかを説明します。

ネットワークの学習

学習の最初の試行では、学習データが十分でなかったため、10 分割交差検証の精度は約 60% でした。不十分なデータで学習させたモデルは、一部の分割で過適合になり、その他の分割では過少適合になります。全体的な適合を改善するには、audioDataAugmenter (Audio Toolbox) を使用してデータセットのサイズを大きくします。処理時間と精度向上との良好なトレードオフを実現するために、ファイルごとに 50 個の拡張が経験的に選択されています。拡張の数を減らすと、例の実行速度を上げることができます。

audioDataAugmenter オブジェクトを作成します。ピッチ シフトを適用する確率を 0.5 に設定し、既定の範囲を使用します。時間シフトを適用する確率を 1 に設定し、[-0.3,0.3] 秒の範囲を使用します。ノイズを追加する確率を 1 に設定し、SNR の範囲を [-20,40] dB に指定します。

numAugmentations = 50;
augmenter = audioDataAugmenter(NumAugmentations=numAugmentations, ...
    TimeStretchProbability=0, ...
    VolumeControlProbability=0, ...
    ...
    PitchShiftProbability=0.5, ...
    ...
    TimeShiftProbability=1, ...
    TimeShiftRange=[-0.3,0.3], ...
    ...
    AddNoiseProbability=1, ...
    SNRRange=[-20,40]);

現在のフォルダーに、拡張されたデータセットを保持するための新しいフォルダーを作成します。

currentDir = pwd;
writeDirectory = fullfile(currentDir,"augmentedData");
mkdir(writeDirectory)

オーディオ データストア内の各ファイルについて、次を行います。

  1. 50 個の拡張を作成します。

  2. 最大絶対値が 1 となるようにオーディオを正規化します。

  3. 拡張されたオーディオ データを WAV ファイルとして書き込みます。各ファイル名に _augK を追加します。ここで、K は拡張の番号です。処理を高速化するために、parfor を使用してデータストアを分割します。

この方法でデータベースを拡張すると、時間がかかり、容量を消費します。ただし、ネットワーク アーキテクチャや特徴抽出パイプラインの選択を繰り返す場合は、一般に、この事前のコストは有益です。

N = numel(ads.Files)*numAugmentations;

reset(ads)

numPartitions = 18;

tic
parfor ii = 1:numPartitions
    adsPart = partition(ads,numPartitions,ii);
    while hasdata(adsPart)
        [x,adsInfo] = read(adsPart);
        data = augment(augmenter,x,fs);

        [~,fn] = fileparts(adsInfo.FileName);
        for i = 1:size(data,1)
            augmentedAudio = data.Audio{i};
            augmentedAudio = augmentedAudio/max(abs(augmentedAudio),[],"all");
            augNum = num2str(i);
            if numel(augNum)==1
                iString = ['0',augNum];
            else
                iString = augNum;
            end
            audiowrite(fullfile(writeDirectory,sprintf('%s_aug%s.wav',fn,iString)),augmentedAudio,fs);
        end
    end
end
disp("Augmentation complete in " + round(toc/60,2) + " minutes.")
Augmentation complete in 3.84 minutes.

拡張されたデータセットを指すオーディオ データストアを作成します。元のデータストアに含まれるラベル テーブルの行を NumAugmentations 回複製し、拡張されたデータストアのラベルを決定します。

adsAug = audioDatastore(writeDirectory);
adsAug.Labels = repelem(ads.Labels,augmenter.NumAugmentations,1);

audioFeatureExtractor (Audio Toolbox) オブジェクトを作成します。Window を周期的な 30 ms のハミング ウィンドウに設定し、OverlapLength0 に設定し、SampleRate をデータベースのサンプル レートに設定します。gtccgtccDeltamfccDelta、および spectralCrest を抽出するため、これらを true に設定します。SpectralDescriptorInputmelSpectrum に設定し、メル スペクトルのために spectralCrest が計算されるようにします。

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

afe = audioFeatureExtractor( ...
    Window=win, ...
    OverlapLength=overlapLength, ...
    SampleRate=fs, ...
    ...
    gtcc=true, ...
    gtccDelta=true, ...
    mfccDelta=true, ...
    ...
    SpectralDescriptorInput="melSpectrum", ...
    spectralCrest=true);

展開用の学習

展開用に学習させる場合、データセットで利用可能なすべての話者を使用します。学習データストアを拡張されたデータストアに設定します。

adsTrain = adsAug;

学習オーディオ データストアを tall 配列に変換します。Parallel Computing Toolbox™ がある場合、抽出は自動的に並列処理されます。Parallel Computing Toolbox™ がない場合でも、このコードは引き続き実行されます。

tallTrain = tall(adsTrain);

学習特徴を抽出し、その特徴の向きを時間が行に沿うように変更して、sequenceInputLayer と互換性をもたせます。

featuresTallTrain = cellfun(@(x)extract(afe,x),tallTrain,UniformOutput=false);
featuresTallTrain = cellfun(@(x)x',featuresTallTrain,UniformOutput=false);
featuresTrain = gather(featuresTallTrain);
Evaluating tall expression using the Parallel Pool 'local':
- Pass 1 of 1: 0% complete
Evaluation 0% complete

- Pass 1 of 1: Completed in 1 min 7 sec
Evaluation completed in 1 min 7 sec

学習セットを使用して、各特徴の平均と標準偏差を求めます。

allFeatures = cat(2,featuresTrain{:});
M = mean(allFeatures,2,"omitnan");
S = std(allFeatures,0,2,"omitnan");

featuresTrain = cellfun(@(x)(x-M)./S,featuresTrain,UniformOutput=false);

10 個の特徴ベクトルがオーバーラップする 20 個の特徴ベクトルによって各シーケンスが構成されるように、特徴ベクトルをシーケンスにバッファーします。

featureVectorsPerSequence = 20;
featureVectorOverlap = 10;
[sequencesTrain,sequencePerFileTrain] = HelperFeatureVector2Sequence(featuresTrain,featureVectorsPerSequence,featureVectorOverlap);

シーケンスと 1 対 1 で対応するように、学習セットと検証セットのラベルを複製します。すべての話者がすべての感情を表す発話をしているとは限りません。すべての感情カテゴリを含む空の categorical 配列を作成し、これを検証ラベルに追加して、categorical 配列にすべての感情が含まれるようにします。

labelsTrain = repelem(adsTrain.Labels.Emotion,[sequencePerFileTrain{:}]);

emptyEmotions = ads.Labels.Emotion;
emptyEmotions(:) = [];

bilstmLayer を使用して BiLSTM ネットワークを定義します。過適合を防止するために、bilstmLayer の前後に dropoutLayer を配置します。

dropoutProb1 = 0.3;
numUnits = 200;
dropoutProb2 = 0.6;
layers = [ ...
    sequenceInputLayer(afe.FeatureVectorLength)
    dropoutLayer(dropoutProb1)
    bilstmLayer(numUnits,OutputMode="last")
    dropoutLayer(dropoutProb2)
    fullyConnectedLayer(numel(categories(emptyEmotions)))
    softmaxLayer
    classificationLayer];

trainingOptions を使用して学習オプションを定義します。

miniBatchSize = 512;
initialLearnRate = 0.005;
learnRateDropPeriod = 2;
maxEpochs = 3;
options = trainingOptions("adam", ...
    MiniBatchSize=miniBatchSize, ...
    InitialLearnRate=initialLearnRate, ...
    LearnRateDropPeriod=learnRateDropPeriod, ...
    LearnRateSchedule="piecewise", ...
    MaxEpochs=maxEpochs, ...
    Shuffle="every-epoch", ...
    Verbose=false, ...
    Plots="Training-Progress");

trainNetwork を使用してネットワークに学習させます。

net = trainNetwork(sequencesTrain,labelsTrain,layers,options);

ネットワーク、構成済みの audioFeatureExtractor、および正規化係数を保存するには、saveSERSystemtrue に設定します。

saveSERSystem = false;
if saveSERSystem
    normalizers.Mean = M;
    normalizers.StandardDeviation = S;
    save("network_Audio_SER.mat","net","afe","normalizers")
end

システム検証用の学習

この例で作成したモデルを正確に評価するには、leave-one-speaker-out (LOSO) k 分割交差検証を使用して学習と検証を実行します。この手法では、k-1 人の話者を使用して学習させた後、除外した話者に対して検証を行います。この手順を k 回繰り返します。最終的な検証精度は、k 個の分割の平均になります。

話者 ID を格納する変数を作成します。分割の数を決定します。話者ごとに 1 つとします。このデータベースには、10 人の個別の話者による発話が含まれます。summary を使用して、話者 ID (左側の列) および話者がデータベースに寄与した発話の数 (右側の列) を表示します。

speaker = ads.Labels.Speaker;
numFolds = numel(speaker);
summary(speaker)
     03      49 
     08      58 
     09      43 
     10      38 
     11      55 
     12      35 
     13      61 
     14      69 
     15      56 
     16      71 

補助関数 HelperTrainAndValidateNetwork は、10 個のすべての分割に対して前述の手順を実行し、各分割について真のラベルと予測されたラベルを返します。audioDatastore、拡張された audioDatastore、および audioFeatureExtractor を渡して HelperTrainAndValidateNetwork を呼び出します。

[labelsTrue,labelsPred] = HelperTrainAndValidateNetwork(ads,adsAug,afe);

分割ごとの精度を出力し、10 分割の混同チャートをプロットします。

for ii = 1:numel(labelsTrue)
    foldAcc = mean(labelsTrue{ii}==labelsPred{ii})*100;
    disp("Fold " + ii + ", Accuracy = " + round(foldAcc,2))
end
Fold 1, Accuracy = 65.31
Fold 2, Accuracy = 68.97
Fold 3, Accuracy = 79.07
Fold 4, Accuracy = 71.05
Fold 5, Accuracy = 72.73
Fold 6, Accuracy = 74.29
Fold 7, Accuracy = 67.21
Fold 8, Accuracy = 85.51
Fold 9, Accuracy = 71.43
Fold 10, Accuracy = 67.61
labelsTrueMat = cat(1,labelsTrue{:});
labelsPredMat = cat(1,labelsPred{:});

figure
cm = confusionchart(labelsTrueMat,labelsPredMat, ...
    Title=["Confusion Matrix for 10-Fold Cross-Validation","Average Accuracy = " round(mean(labelsTrueMat==labelsPredMat)*100,1)], ...
    ColumnSummary="column-normalized",RowSummary="row-normalized");
sortClasses(cm,categories(emptyEmotions))

サポート関数

特徴ベクトルの配列をシーケンスに変換

function [sequences,sequencePerFile] = HelperFeatureVector2Sequence(features,featureVectorsPerSequence,featureVectorOverlap)
    % Copyright 2019 MathWorks, Inc.
    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 [trueLabelsCrossFold,predictedLabelsCrossFold] = HelperTrainAndValidateNetwork(varargin)
    % Copyright 2019 The MathWorks, Inc.
    if nargin == 3
        ads = varargin{1};
        augads = varargin{2};
        extractor = varargin{3};
    elseif nargin == 2
        ads = varargin{1};
        augads = varargin{1};
        extractor = varargin{2};
    end
    speaker = categories(ads.Labels.Speaker);
    numFolds = numel(speaker);
    emptyEmotions = (ads.Labels.Emotion);
    emptyEmotions(:) = [];

    % Loop over each fold.
    trueLabelsCrossFold = {};
    predictedLabelsCrossFold = {};
    
    for i = 1:numFolds
        
        % 1. Divide the audio datastore into training and validation sets.
        % Convert the data to tall arrays.
        idxTrain           = augads.Labels.Speaker~=speaker(i);
        augadsTrain        = subset(augads,idxTrain);
        augadsTrain.Labels = augadsTrain.Labels.Emotion;
        tallTrain          = tall(augadsTrain);
        idxValidation        = ads.Labels.Speaker==speaker(i);
        adsValidation        = subset(ads,idxValidation);
        adsValidation.Labels = adsValidation.Labels.Emotion;
        tallValidation       = tall(adsValidation);

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

        % 3. 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

        % 4. Buffer the sequences so that each sequence consists of twenty
        % feature vectors with overlaps of 10 feature vectors.
        featureVectorsPerSequence = 20;
        featureVectorOverlap = 10;
        [sequencesTrain,sequencePerFileTrain] = HelperFeatureVector2Sequence(featuresTrain,featureVectorsPerSequence,featureVectorOverlap);
        [sequencesValidation,sequencePerFileValidation] = HelperFeatureVector2Sequence(featuresValidation,featureVectorsPerSequence,featureVectorOverlap);

        % 5. Replicate the labels of the train and validation sets so that
        % they are in one-to-one correspondence with the sequences.
        labelsTrain = [emptyEmotions;augadsTrain.Labels];
        labelsTrain = labelsTrain(:);
        labelsTrain = repelem(labelsTrain,[sequencePerFileTrain{:}]);

        % 6. Define a BiLSTM network.
        dropoutProb1 = 0.3;
        numUnits     = 200;
        dropoutProb2 = 0.6;
        layers = [ ...
            sequenceInputLayer(size(sequencesTrain{1},1))
            dropoutLayer(dropoutProb1)
            bilstmLayer(numUnits,OutputMode="last")
            dropoutLayer(dropoutProb2)
            fullyConnectedLayer(numel(categories(emptyEmotions)))
            softmaxLayer
            classificationLayer];

        % 7. Define training options.
        miniBatchSize       = 512;
        initialLearnRate    = 0.005;
        learnRateDropPeriod = 2;
        maxEpochs           = 3;
        options = trainingOptions("adam", ...
            MiniBatchSize=miniBatchSize, ...
            InitialLearnRate=initialLearnRate, ...
            LearnRateDropPeriod=learnRateDropPeriod, ...
            LearnRateSchedule="piecewise", ...
            MaxEpochs=maxEpochs, ...
            Shuffle="every-epoch", ...
            Verbose=false);

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

        % 9. Evaluate the network. Call classify to get the predicted labels
        % for each sequence. Get the mode of the predicted labels of each
        % sequence to get the predicted labels of each file.
        predictedLabelsPerSequence = classify(net,sequencesValidation);
        trueLabels = categorical(adsValidation.Labels);
        predictedLabels = trueLabels;
        idx1 = 1;
        for ii = 1:numel(trueLabels)
            predictedLabels(ii,:) = mode(predictedLabelsPerSequence(idx1:idx1 + sequencePerFileValidation{ii} - 1,:),1);
            idx1 = idx1 + sequencePerFileValidation{ii};
        end
        trueLabelsCrossFold{i} = trueLabels; %#ok<AGROW>
        predictedLabelsCrossFold{i} = predictedLabels; %#ok<AGROW>
    end
end

参考文献

[1] Burkhardt, F., A. Paeschke, M. Rolfes, W.F. Sendlmeier, and B. Weiss, "A Database of German Emotional Speech." In Proceedings Interspeech 2005. Lisbon, Portugal: International Speech Communication Association, 2005.

参考

| | |

関連するトピック