Классифицируйте видео Используя глубокое обучение

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

Создать нейронную сеть для глубокого обучения для видео классификации:

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

  2. Обучите сеть LSTM на последовательностях, чтобы предсказать видео метки.

  3. Соберите сеть, которая классифицирует видео непосредственно путем объединения слоев от обеих сетей.

Следующая схема иллюстрирует сетевую архитектуру.

  • Чтобы ввести последовательности изображений к сети, используйте входной слой последовательности.

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

  • Чтобы восстановить структуру последовательности и изменить вывод к векторным последовательностям, используйте слой разворачивания последовательности и сглаживать слой.

  • Чтобы классифицировать последовательности итогового вектора, включайте слои LSTM, сопровождаемые выходными слоями.

Загрузите предварительно обученную сверточную сеть

Чтобы преобразовать кадры видео к характеристическим векторам, используйте активации предварительно обученной сети.

Загрузите предварительно обученную модель GoogLeNet с помощью функции googlenet. Эта функция требует Модели Deep Learning Toolbox™ для пакета Сетевой поддержки GoogLeNet. Если этот пакет поддержки не установлен, то функция обеспечивает ссылку на загрузку.

netCNN = googlenet;

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

Загрузите набор данных HBMD51 с HMDB: большая человеческая база данных движения и извлечение файл RAR в папку под названием "hmdb51_org". Набор данных содержит приблизительно 2 Гбайт видеоданных для 7 000 клипов более чем 51 класс, такие как "drink", "run" и "shake_hands".

После извлечения файлов 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

Преобразуйте кадры в характеристические векторы

Используйте сверточную сеть в качестве экстрактора функции путем получения активаций при введении кадров видео к сети. Преобразуйте видео в последовательности характеристических векторов, где характеристическими векторами является вывод функции activations на последнем слое объединения сети GoogLeNet ("pool5-7x7_s1").

Эта схема иллюстрирует поток данных через сеть.

Чтобы считать видеоданные и изменить размер их, чтобы совпадать с входным размером сети GoogLeNet, используйте readVideo и функции помощника centerCrop, заданные в конце этого примера. Этот шаг может занять много времени, чтобы запуститься. После преобразования видео к последовательностям сохраните последовательности в MAT-файле в папке tempdir. Если файл 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-by-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. Задайте следующие сетевые слои.

  • Последовательность ввела слой с входным размером, соответствующим размерности признаков характеристических векторов

  • Слой BiLSTM с 2 000 скрытых модулей со слоем уволенного впоследствии. Выводить только одну метку для каждой последовательности путем установки опции 'OutputMode' слоя BiLSTM к 'last'

  • Полносвязный слой с выходным размером, соответствующим количеству классов, softmax слоя и слоя классификации.

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 (чтобы препятствовать тому, чтобы градиенты взорвались).

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

  • Переставьте данные каждая эпоха.

  • Подтвердите сеть однажды в эпоху.

  • Отобразите учебный прогресс графика и подавите многословный вывод.

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

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-by-W-by-C-by-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