本页面提供的是上一版软件的文档。当前版本中已删除对应的英文页面。

使用深度学习进行语音命令识别

此示例说明如何训练一个简单的深度学习模型来检测音频中是否存在语音命令。此示例使用语音命令数据集 [1] 来训练卷积神经网络,以识别给定的一组命令。

要运行此示例,必须先下载该数据集。如果您不想下载数据集或训练网络,则可以在 MATLAB® 中打开此示例,并在命令行中键入 load('commandNet.mat') 来加载预训练的网络。加载网络后,直接转到此示例的最后一部分,Detect Commands Using Streaming Audio from Microphone

加载语音命令数据集

https://storage.googleapis.com/download.tensorflow.org/data/speech_commands_v0.01.tar.gz 下载数据集并解压缩下载的文件。将 datafolder 设置为数据的位置。使用 audioDatastore 创建包含文件名和对应标签的数据存储。使用文件夹名称作为标签源。指定读取整个音频文件的读取方法。创建数据存储副本以供以后使用。

datafolder = PathToDatabase;
ads = audioDatastore(datafolder, ...
    'IncludeSubfolders',true, ...
    'FileExtensions','.wav', ...
    'LabelSource','foldernames')
ads0 = copy(ads);
ads = 

  audioDatastore with properties:

                       Files: {
                              ' ...\datasets\google_speech\_background_noise_\doing_the_dishes.wav';
                              ' ...\datasets\google_speech\_background_noise_\dude_miaowing.wav';
                              ' ...\datasets\google_speech\_background_noise_\exercise_bike.wav'
                               ... and 64724 more
                              }
                      Labels: [_background_noise_; _background_noise_; _background_noise_ ... and 64724 more categorical]
    AlternateFileSystemRoots: {}
              OutputDataType: 'double'

选择要识别的单词

指定您希望模型识别为命令的单词。将所有非命令单词标记为 unknown。将非命令单词标注为 unknown 会创建一个单词组,用来逼近除命令之外的所有单词的分布。网络使用该组来学习命令与所有其他单词之间的差异。

为了减少已知单词和未知单词之间的类不平衡并加快处理速度,只在训练集中包括未知单词的一小部分 includeFraction。先不要在训练集中包含带有背景噪声的较长文件。背景噪声将在稍后的单独步骤中添加。

使用 subset(ads,indices) 创建仅包含按 indices 索引的文件和标签的数据存储。减小数据存储 ads,使其仅包含命令和未知单词子集。计算每一类的样本数。

commands = categorical(["yes","no","up","down","left","right","on","off","stop","go"]);

isCommand = ismember(ads.Labels,commands);
isUnknown = ~ismember(ads.Labels,[commands,"_background_noise_"]);

includeFraction = 0.2;
mask = rand(numel(ads.Labels),1) < includeFraction;
isUnknown = isUnknown & mask;
ads.Labels(isUnknown) = categorical("unknown");

ads = subset(ads,isCommand|isUnknown);
countEachLabel(ads)
ans =

  11×2 table

     Label     Count
    _______    _____

    down       2359 
    go         2372 
    left       2353 
    no         2375 
    off        2357 
    on         2367 
    right      2367 
    stop       2380 
    unknown    8294 
    up         2375 
    yes        2377 

将数据拆分成训练集、验证集和测试集

数据集文件夹中包含文本文件,其中列出了要用作验证集和测试集的音频文件。这些预定义的验证集和测试集不包含同一个人对同一个单词的发音,因此最好使用这些预定义的集合,而不是选择整个数据集的随机子集。根据数据集文件夹中验证文件和测试文件的列表,使用支持函数 splitData 将数据存储拆分为训练集、验证集和测试集。

由于此示例训练单个网络,它仅使用验证集而不是测试集来评估经过训练的模型。如果您训练许多网络并选择具有最高验证准确度的网络作为最终网络,则您可以使用测试集来评估最终网络。

[adsTrain,adsValidation,adsTest] = splitData(ads,datafolder);

计算语音频谱图

为了得到能够高效训练卷积神经网络的数据,请将语音波形转换为 log-mel 频谱图。

定义频谱图计算的参数。segmentDuration 是每个语音段的持续时间(以秒为单位)。frameDuration 是用于计算频谱图的每个帧的持续时间。hopDuration 是频谱图的每列之间的时间步。numBands 是 log-mel 过滤器的数量,等于每个频谱图的高度。

segmentDuration = 1;
frameDuration = 0.025;
hopDuration = 0.010;
numBands = 40;

使用支持函数 speechSpectrograms 计算训练集、验证集和测试集的频谱图。speechSpectrograms 函数使用 designAuditoryFilterBank 进行 log-mel 频谱图计算。要获得具有更平滑分布的数据,请使用小偏移 epsil 取频谱图的对数。

epsil = 1e-6;

XTrain = speechSpectrograms(adsTrain,segmentDuration,frameDuration,hopDuration,numBands);
XTrain = log10(XTrain + epsil);

XValidation = speechSpectrograms(adsValidation,segmentDuration,frameDuration,hopDuration,numBands);
XValidation = log10(XValidation + epsil);

XTest = speechSpectrograms(adsTest,segmentDuration,frameDuration,hopDuration,numBands);
XTest = log10(XTest + epsil);

YTrain = adsTrain.Labels;
YValidation = adsValidation.Labels;
YTest = adsTest.Labels;
Computing speech spectrograms...
Processed 1000 files out of 25128
Processed 2000 files out of 25128
Processed 3000 files out of 25128
Processed 4000 files out of 25128
Processed 5000 files out of 25128
Processed 6000 files out of 25128
Processed 7000 files out of 25128
Processed 8000 files out of 25128
Processed 9000 files out of 25128
Processed 10000 files out of 25128
Processed 11000 files out of 25128
Processed 12000 files out of 25128
Processed 13000 files out of 25128
Processed 14000 files out of 25128
Processed 15000 files out of 25128
Processed 16000 files out of 25128
Processed 17000 files out of 25128
Processed 18000 files out of 25128
Processed 19000 files out of 25128
Processed 20000 files out of 25128
Processed 21000 files out of 25128
Processed 22000 files out of 25128
Processed 23000 files out of 25128
Processed 24000 files out of 25128
Processed 25000 files out of 25128
...done
Computing speech spectrograms...
Processed 1000 files out of 3391
Processed 2000 files out of 3391
Processed 3000 files out of 3391
...done
Computing speech spectrograms...
Processed 1000 files out of 3457
Processed 2000 files out of 3457
Processed 3000 files out of 3457
...done

可视化数据

绘制几个训练样本的波形和频谱图。播放对应的音频片段。

specMin = min(XTrain(:));
specMax = max(XTrain(:));
idx = randperm(size(XTrain,4),3);
figure('Units','normalized','Position',[0.2 0.2 0.6 0.6]);
for i = 1:3
    [x,fs] = audioread(adsTrain.Files{idx(i)});
    subplot(2,3,i)
    plot(x)
    axis tight
    title(string(adsTrain.Labels(idx(i))))

    subplot(2,3,i+3)
    spect = XTrain(:,:,1,idx(i));
    pcolor(spect)
    caxis([specMin+2 specMax])
    shading flat

    sound(x,fs)
    pause(2)
end

当网络的输入经过归一化且具有适度平滑的分布时,最易于训练神经网络。要检查数据分布是否平滑,请绘制训练数据像素值的直方图。

figure
histogram(XTrain,'EdgeColor','none','Normalization','pdf')
axis tight
ax = gca;
ax.YScale = 'log';
xlabel("Input Pixel Value")
ylabel("Probability Density")

添加背景噪声数据

网络必须不仅能够识别不同发音的单词,还要能够检测输入是否包含静音或背景噪声。

使用 _background_noise_ 文件夹中的音频文件创建一秒背景噪声的片段样本。根据每个背景噪声文件创建相同数量的背景片段。您还可以创建自己的背景噪声录音,并将它们添加到 _background_noise_ 文件夹。要计算从 adsBkg 数据存储中的音频文件获取的背景片段的 numBkgClips 频谱图,请使用支持函数 backgroundSpectrograms。在计算频谱图之前,该函数将使用从 volumeRange 给出的范围内的对数均匀分布中采样的因子重新调整每个音频片段。

创建 4,000 个背景片段,并以一个介于 1e-41 之间的数字为因子重新调整每个片段。XBkg 包含背景噪声的频谱图,音量范围从几乎静音到很大声。

adsBkg = subset(ads0,ads0.Labels=="_background_noise_");
numBkgClips = 4000;
volumeRange = [1e-4,1];

XBkg = backgroundSpectrograms(adsBkg,numBkgClips,volumeRange,segmentDuration,frameDuration,hopDuration,numBands);
XBkg = log10(XBkg + epsil);
Computing background spectrograms...
Processed 1000 background clips out of 4000
Processed 2000 background clips out of 4000
Processed 3000 background clips out of 4000
Processed 4000 background clips out of 4000
...done

对背景噪声的频谱图进行拆分,以用于训练集、验证集和测试集。由于 _background_noise_ 文件夹仅包含大约五分半钟的背景噪声,因此不同数据集中的背景样本高度相关。要增加背景噪声的变化,您可以创建自己的背景文件并添加到该文件夹中。要增强网络的抗噪稳健性,您还可以尝试将背景噪声混合到语音文件中。

numTrainBkg = floor(0.8*numBkgClips);
numValidationBkg = floor(0.1*numBkgClips);
numTestBkg = floor(0.1*numBkgClips);

XTrain(:,:,:,end+1:end+numTrainBkg) = XBkg(:,:,:,1:numTrainBkg);
XBkg(:,:,:,1:numTrainBkg) = [];
YTrain(end+1:end+numTrainBkg) = "background";

XValidation(:,:,:,end+1:end+numValidationBkg) = XBkg(:,:,:,1:numValidationBkg);
XBkg(:,:,:,1:numValidationBkg) = [];
YValidation(end+1:end+numValidationBkg) = "background";

XTest(:,:,:,end+1:end+numTestBkg) = XBkg(:,:,:,1: numTestBkg);
clear XBkg;
YTest(end+1:end+numTestBkg) = "background";

YTrain = removecats(YTrain);
YValidation = removecats(YValidation);
YTest = removecats(YTest);

绘制训练集和验证集中不同类标签的分布。测试集与验证集的分布非常相似。

figure('Units','normalized','Position',[0.2 0.2 0.5 0.5]);
subplot(2,1,1)
histogram(YTrain)
title("Training Label Distribution")
subplot(2,1,2)
histogram(YValidation)
title("Validation Label Distribution")

添加数据增强

创建增强的图像数据存储,以实现频谱图的自动增强和大小调整。将频谱图在时间上随机向前或向后平移最多 10 帧(100 毫秒),然后将频谱图沿时间轴放大或缩小 20%。增强数据可以增加训练数据的有效大小,并有助于防止网络过拟合。增强的图像数据存储在训练过程中实时创建增强的图像,并将它们输入到网络中。内存中不保存任何增强的频谱图。

sz = size(XTrain);
specSize = sz(1:2);
imageSize = [specSize 1];
augmenter = imageDataAugmenter( ...
    'RandXTranslation',[-10 10], ...
    'RandXScale',[0.8 1.2], ...
    'FillValue',log10(epsil));
augimdsTrain = augmentedImageDatastore(imageSize,XTrain,YTrain, ...
    'DataAugmentation',augmenter);

定义神经网络架构

创建一个层数组形式的简单网络架构。使用卷积层和批量归一化层,并使用最大池化层“在空间上”(即,在时间和频率上)对特征图进行下采样。添加最终的最大池化层,它随时间对输入特征图进行全局池化。这会在输入频谱图中强制实施(近似的)时间平移不变性,从而使网络在对语音进行分类时不依赖于语音的准确时间位置,得到相同的分类结果。全局池化还可以显著减少最终全连接层中的参数数量。为了降低网络记住训练数据特定特征的可能性,可为最后一个全连接层的输入添加一个小的丢弃率。

该网络很小,因为它只有五个卷积层和几个过滤器。numF 控制卷积层中的过滤器数量。要提高网络的准确度,请尝试通过添加一些相同的块(由卷积层、批量归一化层和 ReLU 层组成)来增加网络深度。还可以尝试通过增大 numF 来增加卷积过滤器的数量。

使用加权交叉熵分类损失。weightedClassificationLayer(classWeights) 可创建一个自定义分类层,用于计算按 classWeights 加权的观测值的交叉熵损失。按照 categories(YTrain) 中类的显示顺序指定相同顺序的类权重。为了使每个类在损失中的总权重相等,使用的类权重应与每个类的训练样本数成反比。使用 Adam 优化器训练网络时,训练算法与类权重的整体归一化无关。

classWeights = 1./countcats(YTrain);
classWeights = classWeights'/mean(classWeights);
numClasses = numel(categories(YTrain));

timePoolSize = ceil(imageSize(2)/8);
dropoutProb = 0.2;
numF = 12;
layers = [
    imageInputLayer(imageSize)

    convolution2dLayer(3,numF,'Padding','same')
    batchNormalizationLayer
    reluLayer

    maxPooling2dLayer(3,'Stride',2,'Padding','same')

    convolution2dLayer(3,2*numF,'Padding','same')
    batchNormalizationLayer
    reluLayer

    maxPooling2dLayer(3,'Stride',2,'Padding','same')

    convolution2dLayer(3,4*numF,'Padding','same')
    batchNormalizationLayer
    reluLayer

    maxPooling2dLayer(3,'Stride',2,'Padding','same')

    convolution2dLayer(3,4*numF,'Padding','same')
    batchNormalizationLayer
    reluLayer
    convolution2dLayer(3,4*numF,'Padding','same')
    batchNormalizationLayer
    reluLayer

    maxPooling2dLayer([1 timePoolSize])

    dropoutLayer(dropoutProb)
    fullyConnectedLayer(numClasses)
    softmaxLayer
    weightedClassificationLayer(classWeights)];

训练网络

指定训练选项。使用小批量大小为 128 的 Adam 优化器。进行 25 轮训练,并在 20 轮后将学习率降低十分之一。

miniBatchSize = 128;
validationFrequency = floor(numel(YTrain)/miniBatchSize);
options = trainingOptions('adam', ...
    'InitialLearnRate',3e-4, ...
    'MaxEpochs',25, ...
    'MiniBatchSize',miniBatchSize, ...
    'Shuffle','every-epoch', ...
    'Plots','training-progress', ...
    'Verbose',false, ...
    'ValidationData',{XValidation,YValidation}, ...
    'ValidationFrequency',validationFrequency, ...
    'LearnRateSchedule','piecewise', ...
    'LearnRateDropFactor',0.1, ...
    'LearnRateDropPeriod',20);

训练网络。如果您没有 GPU,则训练网络可能需要较长的时间。要加载预训练网络而不是从头开始训练网络,请将 doTraining 设置为 false

doTraining = true;
if doTraining
    trainedNet = trainNetwork(augimdsTrain,layers,options);
else
    load('commandNet.mat','trainedNet');
end

评估经过训练的网络

基于训练集(无数据增强)和验证集计算网络的最终准确度。网络对于此数据集非常准确。但是,训练数据、验证数据和测试数据全都具有相似的分布,不一定能反映真实环境。尤其是对仅包含少量单词读音的 unknown 类别,更是如此。

YValPred = classify(trainedNet,XValidation);
validationError = mean(YValPred ~= YValidation);
YTrainPred = classify(trainedNet,XTrain);
trainError = mean(YTrainPred ~= YTrain);
disp("Training error: " + trainError*100 + "%")
disp("Validation error: " + validationError*100 + "%")
Training error: 4.0419%
Validation error: 6.4099%

绘制混淆矩阵。使用列汇总和行汇总显示每个类的准确率和召回率。对混淆矩阵的类进行排序。最大的混淆发生在未知单词与命令之间,以及 upoffdownno,以及 gono 这三对命令之间。

figure('Units','normalized','Position',[0.2 0.2 0.5 0.5]);
cm = confusionchart(YValidation,YValPred);
cm.Title = 'Confusion Matrix for Validation Data';
cm.ColumnSummary = 'column-normalized';
cm.RowSummary = 'row-normalized';
sortClasses(cm, [commands,"unknown","background"])

在处理硬件资源受限的应用(如移动应用)时,请考虑可用内存和计算资源的限制。当使用 CPU 时,以 KB 为单位计算网络总大小,并测试网络的预测速度。预测时间是指对单个输入图像进行分类的时间。如果向网络中输入多个图像,可以同时对它们进行分类,从而缩短每个图像的预测时间。然而,在对流音频进行分类时,单个图像预测时间是最相关的。

info = whos('trainedNet');
disp("Network size: " + info.bytes/1024 + " kB")

for i=1:100
    x = randn(imageSize);
    tic
    [YPredicted,probs] = classify(trainedNet,x,"ExecutionEnvironment",'cpu');
    time(i) = toc;
end
disp("Single-image prediction time on CPU: " + mean(time(11:end))*1000 + " ms")
Network size: 285.2109 kB
Single-image prediction time on CPU: 1.9703 ms

使用来自麦克风的流音频检测命令

基于来自麦克风的流音频测试新训练的命令检测网络。如果尚未训练网络,请在命令行中键入 load('commandNet.mat') 以加载预训练网络和对实时流音频进行分类所需的参数。尝试说出其中一个命令,例如 yesnostop。然后,尝试说一个未知的单词,如 MarvinSheilabedhousecatbird 或从 0 到 9 的任意数字。

指定音频采样率和分类率(以 Hz 为单位),并创建一个可以从麦克风读取音频的音频设备读取器。

fs = 16e3;
classificationRate = 20;
audioIn = audioDeviceReader('SampleRate',fs, ...
    'SamplesPerFrame',floor(fs/classificationRate));

指定音频流频谱图的计算参数并初始化音频缓冲区。提取网络的分类标签。分别为流音频标签和分类概率初始化时长半秒的缓冲区。通过这些缓冲区来比较较长时间内的分类结果,并就是否检测到了命令达成“一致”。

frameLength = floor(frameDuration*fs);
hopLength = floor(hopDuration*fs);
waveBuffer = zeros([fs,1]);

labels = trainedNet.Layers(end).Classes;
YBuffer(1:classificationRate/2) = categorical("background");
probBuffer = zeros([numel(labels),classificationRate/2]);

创建一个图窗,在图窗存在期间一直检测命令。要停止实时检测,只需关闭图窗。

h = figure('Units','normalized','Position',[0.2 0.1 0.6 0.8]);

filterBank = designAuditoryFilterBank(fs,'FrequencyScale','bark',...
    'FFTLength',512,...
    'NumBands',numBands,...
    'FrequencyRange',[50,7000]);

while ishandle(h)

    % Extract audio samples from the audio device and add the samples to
    % the buffer.
    x = audioIn();
    waveBuffer(1:end-numel(x)) = waveBuffer(numel(x)+1:end);
    waveBuffer(end-numel(x)+1:end) = x;

    % Compute the spectrogram of the latest audio samples.
    [~,~,~,spec] =  spectrogram(waveBuffer,hann(frameLength,'periodic'),frameLength - hopLength,512,'onesided');
    spec = filterBank * spec;
    spec = log10(spec + epsil);

    % Classify the current spectrogram, save the label to the label buffer,
    % and save the predicted probabilities to the probability buffer.
    [YPredicted,probs] = classify(trainedNet,spec,'ExecutionEnvironment','cpu');
    YBuffer(1:end-1)= YBuffer(2:end);
    YBuffer(end) = YPredicted;
    probBuffer(:,1:end-1) = probBuffer(:,2:end);
    probBuffer(:,end) = probs';

    % Plot the current waveform and spectrogram.
    subplot(2,1,1);
    plot(waveBuffer)
    axis tight
    ylim([-0.2,0.2])

    subplot(2,1,2)
    pcolor(spec)
    caxis([specMin+2 specMax])
    shading flat

    % Now do the actual command detection by performing a very simple
    % thresholding operation. Declare a detection and display it in the
    % figure title if all of the following hold:
    % 1) The most common label is not |background|.
    % 2) At least |countThreshold| of the latest frame labels agree.
    % 3) The maximum predicted probability of the predicted label is at
    % least |probThreshold|. Otherwise, do not declare a detection.
    [YMode,count] = mode(YBuffer);
    countThreshold = ceil(classificationRate*0.2);
    maxProb = max(probBuffer(labels == YMode,:));
    probThreshold = 0.7;
    subplot(2,1,1);
    if YMode == "background" || count<countThreshold || maxProb < probThreshold
        title(" ")
    else
        title(string(YMode),'FontSize',20)
    end

    drawnow

end

参考

[1] Warden P. "Speech Commands: A public dataset for single-word speech recognition", 2017. Available from http://download.tensorflow.org/data/speech_commands_v0.01.tar.gz. Copyright Google 2017. The Speech Commands Dataset is licensed under the Creative Commons Attribution 4.0 license, available here: https://creativecommons.org/licenses/by/4.0/legalcode.

另请参阅

| |

相关主题