주요 콘텐츠

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

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

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

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

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

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

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

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

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

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

Flow diagram of the network architecture, showing the sequence input, the convolutional layers, the LSTM layers, and the output layers.

데이터 불러오기

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);
classNames = categories(labels);

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

idx = 1;
filename = files(idx);
video = readVideo(filename);
[height,width,numChannels,numFrames] = size(video);

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

labels(idx)
ans = categorical
     brush_hair 

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

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

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

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

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

netCNN = imagePretrainedNetwork("googlenet");

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

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

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

Flow diagram of the feature extractor network architecture, showing the image input, the convolutional layers, and the output layers. The feature vectors are taken from the network after the convolutional layers and before the output layers.

비디오 데이터를 읽어 들여서 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} = predict(netCNN,video,Outputs=layerName);
        sequences{i,1} = squeeze(sequences{i,1})';

    end
    
    save(tempFile,"sequences","-v7.3");
end

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

sequences(1:10)
ans=10×1 cell array
    {409×1024 single}
    {395×1024 single}
    {323×1024 single}
    {246×1024 single}
    {159×1024 single}
    {137×1024 single}
    {359×1024 single}
    {191×1024 single}
    {439×1024 single}
    {528×1024 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,1);
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 계층. BiLSTM 계층의 OutputMode 옵션을 "last"로 설정하여 각 시퀀스에 대해 1개의 레이블만 출력하십시오.

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

numFeatures = size(sequencesTrain{1},2);
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")];

훈련 옵션 지정하기

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

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

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

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

  • 검증 손실이 이전 Epoch 5회 동안의 최저값보다 크거나 같은 경우 훈련을 중지합니다.

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

miniBatchSize = 32;
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, ...
    ValidationPatience=5, ...
    Plots="training-progress", ...
    Metrics="accuracy", ...
    Verbose=false);

LSTM 신경망 훈련시키기

trainnet 함수를 사용하여 신경망을 훈련시킵니다. 실행하는 데 시간이 오래 걸릴 수 있습니다. 기본적으로 trainnet 함수는 GPU를 사용할 수 있으면 GPU를 사용합니다. GPU에서 훈련시키려면 Parallel Computing Toolbox™ 라이선스와 지원되는 GPU 장치가 필요합니다. 지원되는 장치에 대한 자세한 내용은 GPU 연산 요구 사항 (Parallel Computing Toolbox) 항목을 참조하십시오. GPU를 사용할 수 없는 경우, trainnet 함수는 CPU를 사용합니다. 실행 환경을 수동으로 선택하려면 ExecutionEnvironment 훈련 옵션을 사용하십시오.

[netLSTM,info] = trainnet(sequencesTrain,labelsTrain,layers,"crossentropy",options);

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

accuracy = testnet(netLSTM,sequencesValidation,labelsValidation,"accuracy",MiniBatchSize=miniBatchSize)
accuracy = 
65.7312

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

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

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

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

  • 특징을 추출하려면 컨벌루션 계층을 사용하십시오.

  • 결과로 생성되는 벡터 시퀀스를 분류하려면 LSTM 계층을 삽입하고 그 뒤에 출력 계층이 오도록 하십시오. 출력 계층(모델 헤드라고도 함)에는 마지막 완전 연결 계층과 소프트맥스 계층이 포함됩니다.

Flow diagram of the network architecture, showing the sequence input, the convolutional layers, the LSTM layers, and the output layers.

컨벌루션 계층 추가하기

입력 계층의 평균값 영상을 추출합니다. 이 평균값 영상은 시퀀스 입력 계층에서 영상을 정규화할 때 사용됩니다.

averageImage = netCNN.Layers(1).Mean;

입력 계층("data"), 그리고 풀링 계층 뒤에서 활성화에 사용되는 계층("pool5-drop_7x7_s1", "loss3-classifier", "prob")을 제거합니다.

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

시퀀스 입력 계층 추가하기

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

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

신경망에 시퀀스 입력 계층을 추가하고 그 출력을 첫 번째 컨벌루션 계층("conv1-7x7_s2")의 입력에 연결합니다.

net = addLayers(net,inputLayer);
net = connectLayers(net,"input/out","conv1-7x7_s2/in");

LSTM 계층 추가하기

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

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

신경망에 LSTM 계층을 추가합니다. 마지막 컨벌루션 계층("pool5-7x7_s1")을 BiLSTM 계층의 입력("bilstm/in")에 연결합니다.

net = addLayers(net,lstmLayers);
net = connectLayers(net,"pool5-7x7_s1/out","bilstm/in");

신경망 확인하기

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

analyzeNetwork(net)

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

앞에서와 같은 단계를 사용하여 비디오 "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

신경망을 초기화하고 이를 사용하여 비디오를 분류합니다.

net = initialize(net);
video = centerCrop(video,inputSize);
scoresPred = predict(net,video);
Y = scores2label(scoresPred,classNames)
Y = 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)

[height,width] = size(video,1:2);

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

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

end

참고 항목

| | | | |

도움말 항목