Main Content

딥러닝을 사용하여 비디오 분류하기

이 예제에서는 사전 훈련된 영상 분류 모델과 LSTM 신경망을 결합하여 비디오를 분류하는 신경망을 만드는 방법을 보여줍니다.

비디오를 분류하는 딥러닝 신경망을 만들려면 다음을 수행하십시오.

  1. GoogLeNet과 같은 사전 훈련된 컨벌루션 신경망을 사용하여 비디오를 특징 벡터로 구성된 시퀀스로 변환하여 각 프레임에서 특징을 추출합니다.

  2. 시퀀스를 대상으로 LSTM 신경망을 훈련시켜서 비디오 레이블을 예측합니다.

  3. 두 신경망의 계층을 결합하여 비디오를 직접 분류하는 신경망을 조합합니다.

다음 도식은 신경망 아키텍처를 보여줍니다.

  • 신경망에 영상 시퀀스를 입력하려면 시퀀스 입력 계층을 사용하십시오.

  • 특징 추출을 위해 컨벌루션 계층을 사용하려면, 즉 비디오의 각 프레임에 독립적으로 컨벌루션 연산을 적용하려면, 시퀀스 접기 계층 뒤에 컨벌루션 계층을 사용하십시오.

  • 시퀀스 구조체를 복원하고 출력값의 형태를 벡터 시퀀스로 변경하려면 시퀀스 펼치기 계층과 평탄화 계층을 사용하십시오.

  • 결과로 생성되는 벡터 시퀀스를 분류하려면 LSTM 계층을 삽입하고 그 뒤에 출력 계층이 오도록 하십시오.

사전 훈련된 컨벌루션 신경망 불러오기

비디오 프레임을 특징 벡터로 변환하려면 사전 훈련된 신경망의 활성화 값을 사용하십시오.

googlenet 함수를 사용하여 사전 훈련된 GoogLeNet 모델을 불러옵니다. 이 함수를 사용하려면 Deep Learning Toolbox™ Model for GoogLeNet Network 지원 패키지가 필요합니다. 이 지원 패키지가 설치되어 있지 않으면 함수에서 다운로드 링크를 제공합니다.

netCNN = googlenet;

데이터 불러오기

HMDB: a large human motion database에서 HMBD51 데이터 세트를 다운로드하고 "hmdb51_org"라는 이름의 폴더에 RAR 파일의 압축을 풉니다. 이 데이터 세트에는 "drink", "run", "shake_hands" 등 51개의 클래스로 구성된 7,000개의 클립으로 이루어진 약 2GB 분량의 비디오 데이터가 있습니다.

RAR 파일의 압축을 푼 후에는 지원 함수 hmdb51Files를 사용하여 비디오의 파일 이름과 레이블을 가져옵니다.

dataFolder = "hmdb51_org";
[files,labels] = hmdb51Files(dataFolder);

이 예제의 끝부분에 정의된 readVideo 헬퍼 함수를 사용하여 첫 번째 비디오를 읽어 들인 후 비디오의 크기를 확인합니다. 비디오는 H×W×C×S 배열입니다. 여기서 H, W, C, S는 비디오의 높이, 너비, 채널 개수, 프레임 개수입니다.

idx = 1;
filename = files(idx);
video = readVideo(filename);
size(video)
ans = 1×4

   240   320     3   409

대응되는 레이블을 확인합니다.

labels(idx)
ans = categorical
     brush_hair 

비디오를 보려면 implay 함수를 사용하십시오(Image Processing Toolbox™ 필요). 이 함수는 [0,1] 범위 내에 있는 데이터가 필요하므로 먼저 데이터를 255로 나누어야 합니다. 또는 루프를 사용해 개별 프레임을 순회하고 imshow 함수를 사용할 수도 있습니다.

numFrames = size(video,4);
figure
for i = 1:numFrames
    frame = video(:,:,:,i);
    imshow(frame/255);
    drawnow
end

프레임을 특징 벡터로 변환하기

비디오 프레임을 신경망에 입력할 때의 활성화 값을 가져와서 컨벌루션 신경망을 특징 추출기로 사용하십시오. 비디오를 특징 벡터로 구성된 시퀀스로 변환합니다. 여기서 특징 벡터는 GoogLeNet 신경망의 마지막 풀링 계층("pool5-7x7_s1")에 대해 activations 함수를 적용한 결과의 출력값입니다.

다음 도식에서는 신경망 내의 데이터 흐름을 보여줍니다.

비디오 데이터를 읽어 들여서 GoogLeNet 신경망의 입력 크기와 일치하도록 크기 조정하려면 이 예제의 끝부분에 정의된 readVideocenterCrop 헬퍼 함수를 사용하십시오. 이 단계는 실행하는 데 시간이 오래 걸릴 수 있습니다. 비디오를 시퀀스로 변환한 후에는 시퀀스를 tempdir 폴더에 있는 MAT 파일에 저장합니다. MAT 파일이 이미 존재하는 경우, 시퀀스를 다시 변환하지 않고 MAT 파일에서 불러옵니다.

inputSize = netCNN.Layers(1).InputSize(1:2);
layerName = "pool5-7x7_s1";

tempFile = fullfile(tempdir,"hmdb51_org.mat");

if exist(tempFile,'file')
    load(tempFile,"sequences")
else
    numFiles = numel(files);
    sequences = cell(numFiles,1);
    
    for i = 1:numFiles
        fprintf("Reading file %d of %d...\n", i, numFiles)
        
        video = readVideo(files(i));
        video = centerCrop(video,inputSize);
        
        sequences{i,1} = activations(netCNN,video,layerName,'OutputAs','columns');
    end
    
    save(tempFile,"sequences","-v7.3");
end

처음 몇 개 시퀀스의 크기를 확인합니다. 각 시퀀스는 D×S 배열입니다. 여기서 D는 특징의 개수(풀링 계층의 출력 크기)이고 S는 비디오 프레임의 개수입니다.

sequences(1:10)
ans = 10×1 cell array
    {1024×409 single}
    {1024×395 single}
    {1024×323 single}
    {1024×246 single}
    {1024×159 single}
    {1024×137 single}
    {1024×359 single}
    {1024×191 single}
    {1024×439 single}
    {1024×528 single}

훈련 데이터 준비하기

데이터를 훈련 파티션과 검증 파티션으로 분할하고 긴 시퀀스는 모두 제거하여 훈련에 사용할 수 있도록 데이터를 준비합니다.

훈련 파티션과 검증 파티션 만들기

데이터를 분할합니다. 데이터의 90%를 훈련 파티션에 할당하고 10%를 검증 파티션에 할당합니다.

numObservations = numel(sequences);
idx = randperm(numObservations);
N = floor(0.9 * numObservations);

idxTrain = idx(1:N);
sequencesTrain = sequences(idxTrain);
labelsTrain = labels(idxTrain);

idxValidation = idx(N+1:end);
sequencesValidation = sequences(idxValidation);
labelsValidation = labels(idxValidation);

긴 시퀀스 제거하기

신경망에 있는 일반적인 시퀀스보다 훨씬 긴 시퀀스는 훈련 과정에서 다량의 채우기가 적용되는 원인이 될 수 있습니다. 너무 많은 채우기는 분류 정확도를 저하시킬 수 있습니다.

훈련 데이터의 시퀀스 길이를 가져와서 훈련 데이터 히스토그램으로 시각화합니다.

numObservationsTrain = numel(sequencesTrain);
sequenceLengths = zeros(1,numObservationsTrain);

for i = 1:numObservationsTrain
    sequence = sequencesTrain{i};
    sequenceLengths(i) = size(sequence,2);
end

figure
histogram(sequenceLengths)
title("Sequence Lengths")
xlabel("Sequence Length")
ylabel("Frequency")

400개 이상의 시간 스텝이 있는 시퀀스는 몇 개밖에 없습니다. 분류 정확도를 높이려면 400개 이상의 시간 스텝이 있는 훈련 시퀀스와 그에 대응되는 레이블을 제거하십시오.

maxLength = 400;
idx = sequenceLengths > maxLength;
sequencesTrain(idx) = [];
labelsTrain(idx) = [];

LSTM 신경망 만들기

다음으로, 비디오를 나타내는 특징 벡터로 구성된 시퀀스를 분류할 수 있는 LSTM 신경망을 만듭니다.

LSTM 신경망 아키텍처를 정의합니다. 다음 신경망 계층을 지정합니다.

  • 입력 크기가 특징 벡터의 특징 차원과 같은 시퀀스 입력 계층

  • 2000개의 은닉 유닛이 있고 뒤에 드롭아웃 계층이 오는 BiLSTM 계층. 각 시퀀스에 대해 1개의 레이블만 출력하려면 BiLSTM 계층의 'OutputMode' 옵션을 'last'로 설정하십시오.

  • 출력 크기가 클래스 개수와 같은 완전 연결 계층, 소프트맥스 계층 및 분류 계층.

numFeatures = size(sequencesTrain{1},1);
numClasses = numel(categories(labelsTrain));

layers = [
    sequenceInputLayer(numFeatures,'Name','sequence')
    bilstmLayer(2000,'OutputMode','last','Name','bilstm')
    dropoutLayer(0.5,'Name','drop')
    fullyConnectedLayer(numClasses,'Name','fc')
    softmaxLayer('Name','softmax')
    classificationLayer('Name','classification')];

훈련 옵션 지정하기

trainingOptions 함수를 사용하여 훈련 옵션을 지정합니다.

  • 미니 배치 크기를 16으로, 초기 학습률을 0.0001로, (기울기가 한없이 증가하지 않도록) 기울기 임계값을 2로 설정합니다.

  • 매 Epoch마다 데이터를 섞습니다.

  • Epoch당 한 번씩 신경망을 검증합니다.

  • 훈련 진행 상황을 플롯에 표시하고 세부 정보가 출력되지 않도록 합니다.

miniBatchSize = 16;
numObservations = numel(sequencesTrain);
numIterationsPerEpoch = floor(numObservations / miniBatchSize);

options = trainingOptions('adam', ...
    'MiniBatchSize',miniBatchSize, ...
    'InitialLearnRate',1e-4, ...
    'GradientThreshold',2, ...
    'Shuffle','every-epoch', ...
    'ValidationData',{sequencesValidation,labelsValidation}, ...
    'ValidationFrequency',numIterationsPerEpoch, ...
    'Plots','training-progress', ...
    'Verbose',false);

LSTM 신경망 훈련시키기

trainNetwork 함수를 사용하여 신경망을 훈련시킵니다. 실행하는 데 시간이 오래 걸릴 수 있습니다.

[netLSTM,info] = trainNetwork(sequencesTrain,labelsTrain,layers,options);

검증 세트에 대한 신경망의 분류 정확도를 계산합니다. 훈련 옵션과 동일한 미니 배치 크기를 사용합니다.

YPred = classify(netLSTM,sequencesValidation,'MiniBatchSize',miniBatchSize);
YValidation = labelsValidation;
accuracy = mean(YPred == YValidation)
accuracy = 0.6647

비디오 분류 신경망 조합하기

비디오를 직접 분류하는 신경망을 만들려면, 생성한 신경망 양쪽의 계층을 사용하여 신경망을 조합합니다. 컨벌루션 신경망의 계층을 사용하여 비디오를 벡터 시퀀스로 변환하고, LSTM 신경망의 계층을 사용하여 벡터 시퀀스를 분류합니다.

다음 도식은 신경망 아키텍처를 보여줍니다.

  • 신경망에 영상 시퀀스를 입력하려면 시퀀스 입력 계층을 사용하십시오.

  • 특징 추출을 위해 컨벌루션 계층을 사용하려면, 즉 비디오의 각 프레임에 독립적으로 컨벌루션 연산을 적용하려면, 시퀀스 접기 계층 뒤에 컨벌루션 계층을 사용하십시오.

  • 시퀀스 구조체를 복원하고 출력값의 형태를 벡터 시퀀스로 변경하려면 시퀀스 펼치기 계층과 평탄화 계층을 사용하십시오.

  • 결과로 생성되는 벡터 시퀀스를 분류하려면 LSTM 계층을 삽입하고 그 뒤에 출력 계층이 오도록 하십시오.

컨벌루션 계층 추가하기

먼저 GoogLeNet 신경망의 계층 그래프를 만듭니다.

cnnLayers = layerGraph(netCNN);

입력 계층("data")과 활성화를 위해 사용되는 풀링 계층 뒤의 계층들("pool5-drop_7x7_s1", "loss3-classifier", "prob", "output")을 제거합니다.

layerNames = ["data" "pool5-drop_7x7_s1" "loss3-classifier" "prob" "output"];
cnnLayers = removeLayers(cnnLayers,layerNames);

시퀀스 입력 계층 추가하기

GoogLeNet 신경망과 같은 입력 크기의 영상을 포함하는 영상 시퀀스를 받는 시퀀스 입력 계층을 만듭니다. GoogLeNet 신경망과 동일한 평균값 영상을 사용하여 영상을 정규화하려면 시퀀스 입력 계층의 'Normalization' 옵션을 'zerocenter'로 설정하고 'Mean' 옵션을 GoogLeNet의 입력 계층의 평균값 영상으로 설정합니다.

inputSize = netCNN.Layers(1).InputSize(1:2);
averageImage = netCNN.Layers(1).Mean;

inputLayer = sequenceInputLayer([inputSize 3], ...
    'Normalization','zerocenter', ...
    'Mean',averageImage, ...
    'Name','input');

계층 그래프에 시퀀스 입력 계층을 추가합니다. 시퀀스로 구성된 영상에 컨벌루션 계층을 독립적으로 적용하려면 시퀀스 입력 계층과 컨벌루션 계층 사이에 시퀀스 접기 계층을 삽입하여 영상 시퀀스의 시퀀스 구조체를 제거하십시오. 시퀀스 접기 계층의 출력값을 첫 번째 컨벌루션 계층("conv1-7x7_s2")의 입력값에 연결합니다.

layers = [
    inputLayer
    sequenceFoldingLayer('Name','fold')];

lgraph = addLayers(cnnLayers,layers);
lgraph = connectLayers(lgraph,"fold/out","conv1-7x7_s2");

LSTM 계층 추가하기

LSTM 신경망의 시퀀스 입력 계층을 제거하여 계층 그래프에 LSTM 계층을 추가합니다. 시퀀스 접기 계층에 의해 제거된 시퀀스 구조체를 복원하려면 컨벌루션 계층 뒤에 시퀀스 펼치기 계층을 삽입하십시오. LSTM 계층에는 벡터로 구성된 시퀀스가 필요합니다. 시퀀스 펼치기 계층의 출력값 형태를 벡터 시퀀스로 변경하려면 시퀀스 펼치기 계층 뒤에 평탄화 계층을 삽입하십시오.

LSTM 신경망에서 계층을 취하고 시퀀스 입력 계층을 제거합니다.

lstmLayers = netLSTM.Layers;
lstmLayers(1) = [];

계층 그래프에 시퀀스 펼치기 계층, 평탄화 계층 및 LSTM 계층을 추가합니다. 마지막 컨벌루션 계층("pool5-7x7_s1")을 시퀀스 펼치기 계층의 입력값("unfold/in")에 연결합니다.

layers = [
    sequenceUnfoldingLayer('Name','unfold')
    flattenLayer('Name','flatten')
    lstmLayers];

lgraph = addLayers(lgraph,layers);
lgraph = connectLayers(lgraph,"pool5-7x7_s1","unfold/in");

펼치기 계층이 시퀀스 구조체를 복원할 수 있도록 하려면 시퀀스 접기 계층의 "miniBatchSize" 출력값을 시퀀스 펼치기 계층의 대응되는 입력값에 연결합니다.

lgraph = connectLayers(lgraph,"fold/miniBatchSize","unfold/miniBatchSize");

신경망 조합하기

analyzeNetwork 함수를 사용하여 신경망이 유효한지 확인합니다.

analyzeNetwork(lgraph)

assembleNetwork 함수를 사용하여 신경망이 예측을 수행할 준비가 되도록 조합합니다.

net = assembleNetwork(lgraph)
net = 
  DAGNetwork with properties:

         Layers: [148×1 nnet.cnn.layer.Layer]
    Connections: [175×2 table]

새 데이터를 사용하여 분류하기

앞에서와 같은 단계를 사용하여 비디오 "pushup.mp4"를 읽어 들이고 가운데에 맞게 자릅니다.

filename = "pushup.mp4";
video = readVideo(filename);

비디오를 보려면 implay 함수를 사용하십시오(Image Processing Toolbox 필요). 이 함수는 [0,1] 범위 내에 있는 데이터가 필요하므로 먼저 데이터를 255로 나누어야 합니다. 또는 루프를 사용해 개별 프레임을 순회하고 imshow 함수를 사용할 수도 있습니다.

numFrames = size(video,4);
figure
for i = 1:numFrames
    frame = video(:,:,:,i);
    imshow(frame/255);
    drawnow
end

조합된 신경망을 사용하여 비디오를 분류합니다. classify 함수에는 입력 비디오를 포함하는 셀형 배열이 필요하므로 비디오를 포함하는 1×1 셀형 배열을 입력해야 합니다.

video = centerCrop(video,inputSize);
YPred = classify(net,{video})
YPred = categorical
     pushup 

헬퍼 함수

readVideo 함수는 filename에 있는 비디오를 읽어 들여서 H×W×C-×S 배열을 반환합니다. 여기서 H, W, C, S는 비디오의 높이, 너비, 채널 개수, 프레임 개수입니다.

function video = readVideo(filename)

vr = VideoReader(filename);
H = vr.Height;
W = vr.Width;
C = 3;

% Preallocate video array
numFrames = floor(vr.Duration * vr.FrameRate);
video = zeros(H,W,C,numFrames);

% Read frames
i = 0;
while hasFrame(vr)
    i = i + 1;
    video(:,:,:,i) = readFrame(vr);
end

% Remove unallocated frames
if size(video,4) > i
    video(:,:,:,i+1:end) = [];
end

end

centerCrop 함수는 비디오의 가장 긴 모서리를 자르고 크기가 inputSize가 되도록 조정합니다.

function videoResized = centerCrop(video,inputSize)

sz = size(video);

if sz(1) < sz(2)
    % Video is landscape
    idx = floor((sz(2) - sz(1))/2);
    video(:,1:(idx-1),:,:) = [];
    video(:,(sz(1)+1):end,:,:) = [];
    
elseif sz(2) < sz(1)
    % Video is portrait
    idx = floor((sz(1) - sz(2))/2);
    video(1:(idx-1),:,:,:) = [];
    video((sz(2)+1):end,:,:,:) = [];
end

videoResized = imresize(video,inputSize(1:2));

end

참고 항목

| | | | |

관련 항목