主要内容

本页翻译不是最新的。点击此处可查看最新英文版本。

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

此示例说明如何通过将预训练图像分类模型和 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:大型人体运动数据库下载 HMBD51 数据集,并将 RAR 文件提取到名为 "hmdb51_org" 的文件夹中。该数据集包含 51 个类的 7000 个片段、大约 2 GB 的视频数据,例如 "drink""run""shake_hands"

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

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

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

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",为每个序列仅输出一个标签。

  • 输出大小对应于类数量的全连接层,以及 softmax 层。

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(以防止梯度爆炸)。

  • 每轮训练都会打乱数据。

  • 每轮训练后对网络进行一次验证。

  • 如果验证损失大于或等于之前五轮的最小值,则停止训练。

  • 在绘图中显示训练进度(包括网络的准确度),并隐藏详尽输出。

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 上进行训练需要 Parallel Computing Toolbox™ 许可证和受支持的 GPU 设备。有关受支持设备的信息,请参阅GPU 计算要求 (Parallel Computing Toolbox)。否则,trainnet 函数使用 CPU。要手动选择执行环境,请使用 ExecutionEnvironment 训练选项。

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

基于验证集计算网络分类准确度。使用与训练选项相同的小批量大小。

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

组合视频分类网络

要创建直接对视频进行分类的网络,请用创建的两个网络中的层组合成一个网络。使用来自卷积网络的层将视频变换为向量序列,使用来自 LSTM 网络的层对向量序列进行分类。

下图说明网络架构。

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

  • 要提取特征,请使用卷积层。

  • 要对得到的向量序列进行分类,请包括 LSTM 层,并在其后添加输出层。输出层(有时称为模型头)包括最终的全连接层和 softmax 层。

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 数组,其中 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)

[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

另请参阅

| | | | |

主题