Main Content

使用深度学习对视频进行分类

此示例说明如何通过将预训练图像分类模型和 LSTM 网络相结合来创建视频分类网络。

要为视频分类创建深度学习网络,请执行以下操作:

  1. 使用预训练卷积神经网络(如 GoogLeNet)将视频转换为特征向量序列,以从每帧中提取特征。

  2. 基于序列训练 LSTM 网络来预测视频标签。

  3. 通过合并来自两个网络的层,组合一个直接对视频进行分类的网络。

下图说明网络架构。

  • 要将图像序列输入到网络,请使用序列输入层。

  • 要使用卷积层来提取特征,也就是说,要将卷积运算独立地应用于视频的每帧,请使用一个后跟卷积层的序列折叠层。

  • 要还原序列结构体并将输出重构为向量序列,请使用序列展开层和扁平化层。

  • 要对得到的向量序列进行分类,请包括 LSTM 层,并在其后添加输出层。

加载预训练卷积网络

要将视频帧转换为特征向量,请使用预训练网络的激活值。

使用 googlenet 函数加载预训练的 GoogLeNet 模型。此函数需要 Deep Learning Toolbox™ Model for GoogLeNet Network 支持包。如果未安装此支持包,则函数会提供下载链接。

netCNN = googlenet;

加载数据

HMDB:大型人体运动数据库下载 HMBD51 数据集,并将 RAR 文件提取到名为 "hmdb51_org" 的文件夹中。该数据集包含 51 个类的 7000 个片段、大约 2 GB 的视频数据,例如 "drink""run""shake_hands"

提取 RAR 文件后,使用支持函数 hmdb51Files 获取视频的文件名和标签。

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

使用在此示例末尾定义的 readVideo 辅助函数读取第一段视频,并查看该视频的大小。该视频是 H×W×C×S 数组,其中 HWCS 分别是视频的高度、宽度、通道数和帧数。

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 层,后跟一个丢弃层。通过将 BiLSTM 层的 'OutputMode' 选项设置为 '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).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 数组,其中 HWCS 分别是视频的高度、宽度、通道数和帧数。

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

另请参阅

| | | | |

相关主题