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" という名前のフォルダーに解凍します。このデータ セットには、"drink""run""shake_hands" など、51 クラスの 7,000 個のクリップから成る約 2 GB のビデオ データが格納されています。

RAR ファイルを解凍した後、サポート関数 hmdb51Files を使用してビデオのファイル名とラベルを取得します。

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

補助関数 readVideo (この例の最後に定義) を使用して最初のビデオを読み取り、ビデオのサイズを表示します。ビデオは H x W x C x S の配列です。ここで、HWC、および 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 ネットワークの入力サイズに一致するようにサイズを変更するには、補助関数 readVideo および centerCrop (この例の最後に定義) を使用します。この手順の実行には時間がかかることがあります。ビデオをシーケンスに変換した後、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

最初のいくつかのシーケンスのサイズを表示します。各シーケンスは DS 列の配列です。ここで、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 ネットワーク アーキテクチャを定義します。次のネットワーク層を指定します。

  • 入力サイズが特徴ベクトルの特徴次元に対応するシーケンス入力層。

  • 後にドロップアウト層がある、隠れユニットが 2,000 個の 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 に設定します (勾配の発散を防ぐため)。

  • すべてのエポックでデータをシャッフルします。

  • エポックごとに 1 回ネットワークを検証します。

  • プロットに学習の進行状況を表示し、詳細出力を表示しないようにします。

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 は入力ビデオを含む cell 配列を想定しているため、ビデオを含む 1 行 1 列の cell 配列を入力しなければなりません。

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

補助関数

関数 readVideofilename のビデオを読み取り、H x W x C- x S の配列を返します。ここで、HWC、および 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

参考

| | | | |

関連するトピック