Распознавание эмоций речи

Этот пример иллюстрирует простую систему распознавания эмоций речи (SER), использующую сеть BiLSTM. Вы начинаете с загрузки набора данных и затем тестирования обученной сети на отдельных файлах. Сеть обучалась на небольшой базе данных на немецком языке [1].

В примере вы проходите через обучение сети, которое включает загрузку, увеличение и обучение набора данных. Наконец, вы выполняете 10-кратную перекрестную валидацию типа «left-one-speaker-out» (LOSO), чтобы оценить сетевую архитектуру.

Функции, используемые в этом примере, были выбраны с помощью последовательного выбора признаков, аналогичного методу, описанному в разделе Последовательный выбор признаков для аудио функций.

Загрузка набора данных

Загрузите Берлинскую базу данных эмоциональной речи [1]. База данных содержит 535 высказываний 10 актёров, призванных передать одну из следующих эмоций: гнев, скука, отвращение, беспокойство/страх, счастье, грусть или нейтраль. Эмоции являются независимыми от текста.

url = "http://emodb.bilderbar.info/download/download.zip";
downloadFolder = tempdir;
datasetFolder = fullfile(downloadFolder,"Emo-DB");

if ~exist(datasetFolder,'dir')
    disp('Downloading Emo-DB (40.5 MB) ...')
    unzip(url,datasetFolder)
end

Создайте audioDatastore это указывает на аудио файлов.

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

Имена файлов являются кодами, указывающими на идентификатор динамика, устный текст, эмоции и версию. Веб-сайт содержит ключ для интерпретации кода и дополнительную информацию о докладчиках, таких как пол и возраст. Составьте таблицу с переменными Speaker и Emotion. Декодируйте имена файлов в таблицу.

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. Установите Labels свойство audioDatastore на labelTable.

ads.Labels = labelTable;

Выполните распознавание эмоций речи

Загрузите и загрузите предварительно обученную сеть, audioFeatureExtractor объект, используемый для обучения сети, и коэффициенты нормализации для функций. Эта сеть была обучена с использованием всех динамиков в наборе данных, кроме динамика 03.

url = 'http://ssd.mathworks.com/supportfiles/audio/SpeechEmotionRecognition.zip';
    downloadNetFolder = tempdir;
    netFolder = fullfile(downloadNetFolder,'SpeechEmotionRecognition');
    if ~exist(netFolder,'dir')
        disp('Downloading pretrained network (1 file - 1.5 MB) ...')
        unzip(url,downloadNetFolder)
    end
load(fullfile(netFolder,'network_Audio_SER.mat'));

Частота дискретизации, установленная на audioFeatureExtractor соответствует скорости дискретизации набора данных.

fs = afe.SampleRate;

Выберите динамик и эмоции, затем подставьте datastore, чтобы включить только выбранный динамик и эмоции. Прочтите из datastore и прослушайте файл.

speaker = categorical("03");
эмоции = categorical("Disgust");

adsSubset = подмножество (объявления, ads.Labels.Speaker = = динамик и объявления. Labels.Emotion = = эмоции);

audio = read (adsSubset);
звук (аудио, fs)

Используйте audioFeatureExtractor объект, чтобы извлечь функции и затем транспонировать их так, чтобы время было вдоль строк. Нормализуйте функции и затем преобразуйте их в последовательности с 20 элементами с 10-элементным перекрытием, что соответствует окнам приблизительно 600 мс с 300 мс перекрытием. Используйте вспомогательную функцию, 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 среднее значение
    case 'mean'
        пробники = среднее (YPred, 1);
    case 'median'
        пробники = медиана (YPred, 1);
    case 'mode'
        пробники = режим (YPred, 1);
end

pie (probs ./sum (probs), строка (net.Layers (end) .Classes))

Остальная часть примера иллюстрирует, как сеть была обучена и проверена.

Обучите сеть

10-кратная точность перекрестной валидации первой попытки обучения составила около 60% из-за недостаточности обучающих данных. Модель, обученная на недостаточных данных, перегружает некоторые складки и подгоняет другие. Чтобы улучшить общую подгонку, увеличьте размер набора данных с помощью audioDataAugmenter. 50 увеличение на файл было выбрано эмпирически как хороший компромисс между временем вычислений и улучшением точности. Вы можете уменьшить количество увеличений, чтобы ускорить пример.

Создайте audioDataAugmenter объект. Установите вероятность применения смещения тангажа на 0.5 и используйте область значений по умолчанию. Установите вероятность применения временного переключения на 1 и использовать область значений [-0.3,0.3] секунд. Установите вероятность добавления шума в 1 и задайте область значений ОСШ следующим [-20,40] дБ.

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)

Для каждого файла в audio datastore:

  1. Создайте 50 дополнений.

  2. Нормализуйте аудио, чтобы иметь максимальное абсолютное значение 1.

  3. Запишите дополненный аудио данные как WAV- файла. Добавление _augK каждому из имен файлов, где K является номером увеличения. Чтобы ускорить обработку, используйте parfor и разделите datastore.

Этот метод увеличения базы данных занимает много времени (примерно 1 часа) и занимает пространство (примерно 26 ГБ). Однако при итерации при выборе сетевой архитектуры или конвейера редукции данных эта предварительная стоимость в целом выгодна.

N = numel(ads.Files)*numAugmentations;
myWaitBar = HelperPoolWaitbar(N,"Augmenting Dataset...");

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);
            increment(myWaitBar)
        end
    end
end
Starting parallel pool (parpool) using the 'local' profile ...
Connected to the parallel pool (number of workers: 6).
delete(myWaitBar)
fprintf('Augmentation complete (%0.2f minutes).\n',toc/60)
Augmentation complete (6.28 minutes).

Создайте audio datastore, который указывает на дополненный набор данных. Реплицируйте строки таблицы меток исходного datastore NumAugmentations время для определения меток дополненного datastore.

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

Создайте audioFeatureExtractor объект. Задайте Window к периодической 30 мс Окна Хэмминга, OverlapLength на 0, и SampleRate к частоте дискретизации базы данных. Задайте gtcc, gtccDelta, mfccDelta, и spectralCrest на true извлечь их. Задайте SpectralDescriptorInput на melSpectrum так что spectralCrest вычисляется для спектра mel.

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);

Train для развертывания

Когда вы обучаете для развертывания, используйте все доступные динамики в наборе данных. Установите training datastore в дополненный datastore.

adsTrain = adsAug;

Преобразуйте обучающий audio datastore в длинный массив array. Если у вас есть Parallel Computing Toolbox™, экстракция автоматически распараллеливается. Если у вас нет Parallel Computing Toolbox™, код продолжает выполняться.

tallTrain = tall(adsTrain);

Извлеките обучающие функции и переориентируйте их так, чтобы время было вдоль строк, чтобы быть совместимым с sequenceInputLayer (Deep Learning Toolbox).

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: 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);

Буферизуйте векторы функций в последовательности так, чтобы каждая последовательность состояла из 20 векторов функций с перекрытиями из 10 векторов функций.

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

Тиражируйте метки наборов обучения и валидации так, чтобы они находились в соответствии один к одному с последовательностями. Не у всех выступающих есть высказывания за все эмоции. Создайте пустую categorical массив, который содержит все эмоциональные категории и добавляет его к меткам валидации так, чтобы категориальный массив содержал все эмоции.

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

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

Определите сеть BiLSTM с помощью bilstmLayer (Deep Learning Toolbox). Поместите dropoutLayer (Deep Learning Toolbox) до и после bilstmLayer чтобы помочь предотвратить сверхподбор кривой.

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];

Определите опции обучения с помощью trainingOptions (Deep Learning Toolbox).

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 (Deep Learning Toolbox).

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

Чтобы сохранить сеть, настроено audioFeatureExtractor, и коэффициенты нормализации, установите saveSERSystem на true.

saveSERSystem = false;
if saveSERSystem
    нормализаторы. Mean = M;
    normalizers.StandardDeviation = S;
    сохранить'network_Audio_SER.mat','net','afe','normalizers')
end

Обучение для валидации системы

Чтобы предоставить точную оценку модели, которую вы создали в этом примере, train и проверьте, используя кросс- валидацию leave-one-speaker-out (LOSO) k-fold. В этом методе вы обучаете, используя k-1 а затем проверьте на левом динамике. Вы повторяете эту процедуру k раз. Конечная точность валидации является средней из k складок.

Создайте переменную, которая содержит идентификаторы динамиков. Определите количество складок: 1 для каждого динамика. База данных содержит высказывания 10 уникальных дикторов. Использование summary отображение идентификаторов динамиков (левый столбец) и количества высказываний, которые они вносят в базу данных (правый столбец).

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 складок и возвращает истинные и предсказанные метки для каждой складки. Функции HelperTrainAndValidateNetwork с audioDatastore, дополненный audioDatastore, и audioFeatureExtractor.

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

Напечатайте точность по складкам и постройте график 10-кратной путаницы.

for ii = 1:numel(labelsTrue)
    foldAcc = mean(labelsTrue{ii}==labelsPred{ii})*100;
    fprintf('Fold %1.0f, Accuracy = %0.1f\n',ii,foldAcc);
end
Fold 1, Accuracy = 73.5
Fold 2, Accuracy = 77.6
Fold 3, Accuracy = 74.4
Fold 4, Accuracy = 68.4
Fold 5, Accuracy = 76.4
Fold 6, Accuracy = 80.0
Fold 7, Accuracy = 73.8
Fold 8, Accuracy = 87.0
Fold 9, Accuracy = 69.6
Fold 10, Accuracy = 70.4
labelsTrueMat = cat(1,labelsTrue{:});
labelsPredMat = cat(1,labelsPred{:});
figure
cm = confusionchart(labelsTrueMat,labelsPredMat);
valAccuracy = mean(labelsTrueMat==labelsPredMat)*100;
cm.Title = sprintf('Confusion Matrix for 10-Fold Cross-Validation\nAverage Accuracy = %0.1f',valAccuracy);
sortClasses(cm,categories(emptyEmotions))
cm.ColumnSummary = 'column-normalized';
cm.RowSummary = 'row-normalized';

Вспомогательные функции

Преобразуйте массив векторов функций в последовательности

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 Немецкая эмоциональная речь». В Proceedings Interspeech 2005. Лиссабон, Португалия: Международная ассоциация речевых коммуникаций, 2005 год.