Main Content

딥러닝을 사용해 음성 명령 인식 모델 훈련시키기

이 예제에서는 오디오에서 음성 명령의 존재 여부를 감지하는 딥러닝 모델을 훈련시키는 방법을 보여줍니다. 이 예제에서는 Speech Commands Dataset[1]을 사용하여 컨벌루션 신경망이 명령 세트를 인식하도록 훈련시킵니다.

사전 훈련된 음성 명령 인식 시스템을 사용하려면 Speech Command Recognition Using Deep Learning (Audio Toolbox) 항목을 참조하십시오.

이 예제를 빠르게 실행하려면 speedupExampletrue로 설정하십시오. 퍼블리시된 대로 예제 전체를 실행하려면 speedupExamplefalse로 설정하십시오.

speedupExample = false;

재현이 가능하도록 난수 시드값을 설정합니다.

rng default

데이터 불러오기

이 예제에서는 Google Speech Commands Dataset[1]을 사용합니다. 데이터 세트를 다운로드하고 압축을 풉니다.

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

데이터 증대시키기

신경망은 발화된 다양한 단어를 인식할 수 있어야 할 뿐 아니라 오디오 입력값이 무음인지 배경 잡음인지를 감지할 수도 있어야 합니다.

지원 함수인 augmentDataset은 Google Speech Commands Dataset의 배경 폴더에 있는 긴 오디오 파일을 사용하여 1초 길이의 배경 잡음 세그먼트를 만듭니다. 이 함수는 각 배경 잡음 파일로부터 동일한 개수의 배경 세그먼트를 만든 다음 훈련 폴더와 검증 폴더로 분할합니다.

augmentDataset(dataset)

훈련 데이터저장소 만들기

훈련 데이터 세트를 가리키는 audioDatastore (Audio Toolbox) 항목을 만듭니다.

ads = audioDatastore(fullfile(dataset,"train"), ...
    IncludeSubfolders=true, ...
    FileExtensions=".wav", ...
    LabelSource="foldernames");

모델이 명령으로 인식해야 할 단어를 사용자가 지정해 줍니다. 명령이나 배경 잡음이 아닌 파일은 모두 unknown으로 지정합니다. 명령이 아닌 단어를 unknown으로 지정하면 명령을 제외한 모든 단어의 분포에 근접한 단어 그룹이 만들어집니다. 신경망은 이 그룹을 사용하여 명령과 명령이 아닌 단어의 차이를 학습합니다.

알려진 단어와 알려지지 않은 단어 사이의 클래스 불균형을 줄이고 처리 속도를 높이려면 알려지지 않은 단어들의 일부만 훈련 세트에 포함하십시오.

subset (Audio Toolbox)을 사용하여 명령, 배경 잡음, 알려지지 않은 단어의 서브셋만 포함하는 데이터저장소를 만듭니다. 각 범주에 속하는 표본의 개수를 셉니다.

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

isCommand = ismember(ads.Labels,commands);
isBackground = ismember(ads.Labels,background);
isUnknown = ~(isCommand|isBackground);

includeFraction = 0.2; % Fraction of unknowns to include.
idx = find(isUnknown);
idx = idx(randperm(numel(idx),round((1-includeFraction)*sum(isUnknown))));
isUnknown(idx) = false;

ads.Labels(isUnknown) = categorical("unknown");

adsTrain = subset(ads,isCommand|isUnknown|isBackground);
adsTrain.Labels = removecats(adsTrain.Labels);

검증 데이터저장소 만들기

검증 데이터 세트를 가리키는 audioDatastore (Audio Toolbox) 항목을 만듭니다. 훈련 데이터저장소를 만들 때와 동일한 단계를 따릅니다.

ads = audioDatastore(fullfile(dataset,"validation"), ...
    IncludeSubfolders=true, ...
    FileExtensions=".wav", ...
    LabelSource="foldernames");

isCommand = ismember(ads.Labels,commands);
isBackground = ismember(ads.Labels,background);
isUnknown = ~(isCommand|isBackground);

includeFraction = 0.2; % Fraction of unknowns to include.
idx = find(isUnknown);
idx = idx(randperm(numel(idx),round((1-includeFraction)*sum(isUnknown))));
isUnknown(idx) = false;

ads.Labels(isUnknown) = categorical("unknown");

adsValidation = subset(ads,isCommand|isUnknown|isBackground);
adsValidation.Labels = removecats(adsValidation.Labels);

훈련 및 검증 레이블 분포를 시각화합니다.

figure(Units="normalized",Position=[0.2,0.2,0.5,0.5])

tiledlayout(2,1)

nexttile
histogram(adsTrain.Labels)
title("Training Label Distribution")
ylabel("Number of Observations")
grid on

nexttile
histogram(adsValidation.Labels)
title("Validation Label Distribution")
ylabel("Number of Observations")
grid on

요청된 경우 데이터 세트를 줄여 예제 속도를 높입니다.

if speedupExample
    numUniqueLabels = numel(unique(adsTrain.Labels)); %#ok<UNRCH> 
    % Reduce the dataset by a factor of 20
    adsTrain = splitEachLabel(adsTrain,round(numel(adsTrain.Files) / numUniqueLabels / 20));
    adsValidation = splitEachLabel(adsValidation,round(numel(adsValidation.Files) / numUniqueLabels / 20));
end

훈련을 위해 데이터 준비하기

컨벌루션 신경망의 효율적인 훈련을 위해 데이터를 준비하려면 음성 파형을 청각 기반 스펙트로그램으로 변환하십시오.

처리 속도를 높이려면 여러 워커 간에 특징 추출을 분산할 수 있습니다. Parallel Computing Toolbox™에 액세스할 수 있는 경우 병렬 풀을 시작합니다.

if canUseParallelPool && ~speedupExample
    useParallel = true;
    gcp;
else
    useParallel = false;
end

특징 추출

오디오 입력값에서 청각 스펙트로그램을 추출하는 파라미터를 정의합니다. segmentDuration은 각 음성 클립의 지속 시간(단위: 초)입니다. frameDuration은 스펙트럼 계산을 위한 각 프레임의 지속 시간입니다. hopDuration은 각 스펙트럼 사이의 시간 스텝입니다. numBands는 청각 스펙트로그램의 필터 개수입니다.

fs = 16e3; % Known sample rate of the data set.

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

FFTLength = 512;
numBands = 50;

segmentSamples = round(segmentDuration*fs);
frameSamples = round(frameDuration*fs);
hopSamples = round(hopDuration*fs);
overlapSamples = frameSamples - hopSamples;

특징 추출을 수행할 audioFeatureExtractor (Audio Toolbox) 객체를 만듭니다.

afe = audioFeatureExtractor( ...
    SampleRate=fs, ...
    FFTLength=FFTLength, ...
    Window=hann(frameSamples,"periodic"), ...
    OverlapLength=overlapSamples, ...
    barkSpectrum=true);
setExtractorParameters(afe,"barkSpectrum",NumBands=numBands,WindowNormalization=false);

audioDatastore (Audio Toolbox)에 일련의 transform (Audio Toolbox)을 정의하여 오디오를 일관된 길이가 되도록 채우고 특징을 추출한 다음 로그를 적용합니다.

transform1 = transform(adsTrain,@(x)[zeros(floor((segmentSamples-size(x,1))/2),1);x;zeros(ceil((segmentSamples-size(x,1))/2),1)]);
transform2 = transform(transform1,@(x)extract(afe,x));
transform3 = transform(transform2,@(x){log10(x+1e-6)});

readall (Audio Toolbox) 함수를 사용하여 데이터저장소에서 모든 데이터를 읽어 들입니다. 각 파일을 읽고 나서 데이터를 반환하기 전에 해당 데이터는 변환을 거쳐서 전달됩니다.

XTrain = readall(transform3,UseParallel=useParallel);

출력값은 numFiles×1 셀형 배열입니다. 셀형 배열의 각 요소는 파일에서 추출한 청각 스펙트로그램에 대응됩니다.

numFiles = numel(XTrain)
numFiles = 28463
[numHops,numBands,numChannels] = size(XTrain{1})
numHops = 98
numBands = 50
numChannels = 1

셀형 배열을 4번째 차원을 따라 청각 스펙트로그램이 있는 4차원 배열로 변환합니다.

XTrain = cat(4,XTrain{:});

[numHops,numBands,numChannels,numFiles] = size(XTrain)
numHops = 98
numBands = 50
numChannels = 1
numFiles = 28463

검증 세트에 대해 위에서 설명한 특징 추출 단계를 수행합니다.

transform1 = transform(adsValidation,@(x)[zeros(floor((segmentSamples-size(x,1))/2),1);x;zeros(ceil((segmentSamples-size(x,1))/2),1)]);
transform2 = transform(transform1,@(x)extract(afe,x));
transform3 = transform(transform2,@(x){log10(x+1e-6)});
XValidation = readall(transform3,UseParallel=useParallel);
XValidation = cat(4,XValidation{:});

편의를 위해 훈련 목표 레이블과 검증 목표 레이블을 분리합니다.

TTrain = adsTrain.Labels;
TValidation = adsValidation.Labels;

데이터 시각화하기

몇몇 훈련 샘플의 파형과 청각 스펙트로그램을 플로팅합니다. 대응하는 오디오 클립을 재생합니다.

specMin = min(XTrain,[],"all");
specMax = max(XTrain,[],"all");
idx = randperm(numel(adsTrain.Files),3);
figure(Units="normalized",Position=[0.2,0.2,0.6,0.6]);

tlh = tiledlayout(2,3);
for ii = 1:3
    [x,fs] = audioread(adsTrain.Files{idx(ii)});

    nexttile(tlh,ii)
    plot(x)
    axis tight
    title(string(adsTrain.Labels(idx(ii))))
    
    nexttile(tlh,ii+3)
    spect = XTrain(:,:,1,idx(ii))';
    pcolor(spect)
    clim([specMin specMax])
    shading flat
    
    sound(x,fs)
    pause(2)
end

신경망 아키텍처 정의하기

간단한 신경망 아키텍처를 계층 배열로 만듭니다. 컨벌루션 계층과 배치 정규화 계층을 사용하고, 최댓값 풀링 계층을 사용하여 특징 맵을 "공간적으로"(즉, 시간과 주파수에서) 다운샘플링합니다. 시간의 흐름에 따라 입력 특징 맵을 전역적으로 풀링하는 마지막 최댓값 풀링 계층을 추가합니다. 이렇게 하면 입력 스펙트로그램에서 (근사적인) 시간 이동 불변성이 적용되어 음성의 정확한 시간적 위치에 상관없이 신경망이 동일한 분류를 수행할 수 있게 됩니다. 전역적 풀링은 마지막 완전 연결 계층에서 파라미터의 개수를 대폭 줄여 주기도 합니다. 신경망이 훈련 데이터의 구체적인 특징을 기억할 가능성을 줄이려면 입력값의 마지막 완전 연결 계층에 소량의 드롭아웃을 추가하십시오.

신경망은 몇 개의 필터만 있는 컨벌루션 계층 5개만 포함하므로 크기가 작습니다. numF는 컨벌루션 계층의 필터 개수를 제어합니다. 신경망의 정확도를 높이려면 컨벌루션 계층, 배치 정규화 계층, ReLU 계층으로 구성된 동일한 블록들을 추가하여 신경망의 심도를 높여보십시오. numF를 높여 컨벌루션 필터의 개수를 늘려볼 수도 있습니다.

각 클래스에 손실의 총 가중치를 동일하게 주려면 각 클래스의 훈련 표본의 개수에 반비례하는 클래스 가중치를 사용하십시오. Adam 최적화 함수를 사용하여 신경망을 훈련시키는 경우에는 훈련 알고리즘이 클래스 가중치의 전체 정규화에 대해 독립적입니다.

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

timePoolSize = ceil(numHops/8);

dropoutProb = 0.2;
numF = 12;
layers = [
    imageInputLayer([numHops,afe.FeatureVectorLength])
    
    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([timePoolSize,1])
    dropoutLayer(dropoutProb)

    fullyConnectedLayer(numClasses)
    softmaxLayer];

훈련 옵션 지정하기

훈련을 위한 파라미터를 정의하려면 trainingOptions를 사용하십시오. 미니 배치 크기가 128인 Adam 최적화 함수를 사용합니다.

miniBatchSize = 128;
validationFrequency = floor(numel(TTrain)/miniBatchSize);
options = trainingOptions("adam", ...
    InitialLearnRate=3e-4, ...
    MaxEpochs=15, ...
    MiniBatchSize=miniBatchSize, ...
    Shuffle="every-epoch", ...
    Plots="training-progress", ...
    Verbose=false, ...
    ValidationData={XValidation,TValidation}, ...
    ValidationFrequency=validationFrequency, ...
    Metrics="accuracy");

신경망 훈련시키기

신경망을 훈련시키려면 trainnet을 사용하십시오. GPU가 없으면, 신경망 훈련에 시간이 걸릴 수 있습니다.

trainedNet = trainnet(XTrain,TTrain,layers,@(Y,T)crossentropy(Y,T,classWeights(:),WeightsFormat="C"),options);

훈련된 신경망 평가하기

훈련 세트와 검증 세트에 대해 신경망의 최종 정확도를 계산하려면 minibatchpredict를 사용하십시오. 신경망은 이 데이터 세트에서 매우 정확한 결과를 나타냅니다. 그러나 훈련 데이터, 검증 데이터, 테스트 데이터는 모두 실제 환경을 그대로 반영한다고 보기 어려운 비슷한 분포를 갖고 있습니다. 이러한 한계는 특히 적은 개수의 발화된 단어를 포함하는 unknown 범주에서 두드러지게 나타납니다.

scores = minibatchpredict(trainedNet,XValidation);
YValidation = scores2label(scores,classes,"auto");
validationError = mean(YValidation ~= TValidation);
scores = minibatchpredict(trainedNet,XTrain);
YTrain = scores2label(scores,classes,"auto");
trainError = mean(YTrain ~= TTrain);

disp(["Training error: " + trainError*100 + " %";"Validation error: " + validationError*100 + " %"])
    "Training error: 3.2744 %"
    "Validation error: 6.6217 %"

검증 세트에 대해 혼동행렬을 플로팅하려면 confusionchart를 사용하십시오. 열 및 행 요약을 사용하여 각 클래스의 정밀도를 표시하고 다시 호출합니다.

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

모바일 애플리케이션과 같이 하드웨어 리소스 제약이 있는 애플리케이션에서 작업할 때는 가용 메모리와 연산 리소스에 대한 제한을 고려하는 것이 중요합니다. 신경망의 총 크기를 kB 단위로 계산하고, CPU를 사용할 때의 예측 속도를 테스트합니다. 예측 시간은 단일 입력 영상을 분류하는 시간입니다. 신경망에 여러 개의 영상을 입력하는 경우, 복수의 영상이 동시에 분류될 수 있고, 그 결과 영상당 예측 시간이 단축됩니다. 그러나 스트리밍 오디오를 분류할 때는 단일 영상 예측 시간이 가장 중요합니다.

for ii = 1:100
    x = randn([numHops,numBands]);
    predictionTimer = tic;
    y = predict(trainedNet,x);
    time(ii) = toc(predictionTimer);
end

disp(["Network size: " + whos("trainedNet").bytes/1024 + " kB"; ...
"Single-image prediction time on CPU: " + mean(time(11:end))*1000 + " ms"])
    "Network size: 310.5391 kB"
    "Single-image prediction time on CPU: 1.8936 ms"

지원 함수

배경 잡음이 있는 데이터셋 증대시키기

function augmentDataset(datasetloc)
adsBkg = audioDatastore(fullfile(datasetloc,"background"));
fs = 16e3; % Known sample rate of the data set
segmentDuration = 1;
segmentSamples = round(segmentDuration*fs);

volumeRange = log10([1e-4,1]);

numBkgSegments = 4000;
numBkgFiles = numel(adsBkg.Files);
numSegmentsPerFile = floor(numBkgSegments/numBkgFiles);

fpTrain = fullfile(datasetloc,"train","background");
fpValidation = fullfile(datasetloc,"validation","background");

if ~datasetExists(fpTrain)

    % Create directories
    mkdir(fpTrain)
    mkdir(fpValidation)

    for backgroundFileIndex = 1:numel(adsBkg.Files)
        [bkgFile,fileInfo] = read(adsBkg);
        [~,fn] = fileparts(fileInfo.FileName);

        % Determine starting index of each segment
        segmentStart = randi(size(bkgFile,1)-segmentSamples,numSegmentsPerFile,1);

        % Determine gain of each clip
        gain = 10.^((volumeRange(2)-volumeRange(1))*rand(numSegmentsPerFile,1) + volumeRange(1));

        for segmentIdx = 1:numSegmentsPerFile

            % Isolate the randomly chosen segment of data.
            bkgSegment = bkgFile(segmentStart(segmentIdx):segmentStart(segmentIdx)+segmentSamples-1);

            % Scale the segment by the specified gain.
            bkgSegment = bkgSegment*gain(segmentIdx);

            % Clip the audio between -1 and 1.
            bkgSegment = max(min(bkgSegment,1),-1);

            % Create a file name.
            afn = fn + "_segment" + segmentIdx + ".wav";

            % Randomly assign background segment to either the train or
            % validation set.
            if rand > 0.85 % Assign 15% to validation
                dirToWriteTo = fpValidation;
            else % Assign 85% to train set.
                dirToWriteTo = fpTrain;
            end

            % Write the audio to the file location.
            ffn = fullfile(dirToWriteTo,afn);
            audiowrite(ffn,bkgSegment,fs)

        end

        % Print progress
        fprintf('Progress = %d (%%)\n',round(100*progress(adsBkg)))

    end
end
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. The Speech Commands Dataset is licensed under the Creative Commons Attribution 4.0 license, available here: https://creativecommons.org/licenses/by/4.0/legalcode.

참고 문헌

[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.

참고 항목

| | | | |

관련 항목