このページの内容は最新ではありません。最新版の英語を参照するには、ここをクリックします。
深層学習を使用したビデオの分類
この例では、事前学習済みのイメージ分類モデルと LSTM ネットワークを組み合わせることによって、ビデオの分類用のネットワークを作成する方法を説明します。
ビデオの分類用の深層学習ネットワークを作成するには、以下のようにします。
GoogLeNet などの事前学習済みの畳み込みニューラル ネットワークを使用してビデオを特徴ベクトルのシーケンスに変換し、各フレームから特徴を抽出します。
シーケンスで LSTM ネットワークに学習させて、ビデオのラベルを予測します。
両方のネットワークからの層を組み合わせることによってビデオを直接分類するネットワークを組み立てます。
次の図はネットワーク アーキテクチャを示しています。
イメージ シーケンスをネットワークに入力するには、シーケンス入力層を使用します。
畳み込み層を使用して特徴を抽出します。すなわち、畳み込み演算をビデオの各フレームに個別に適用します。
結果のベクトル シーケンスを分類するには、LSTM 層の後に出力層を含めます。
データの読み込み
HMDB (大規模人間動作データベース) から HMBD51 データ セットをダウンロードし、RAR ファイルを "hmdb51_org"
という名前のフォルダーに解凍します。このデータ セットには、"drink"
、"run"
、"shake_hands"
など、51 クラスの 7,000 個のクリップから成る約 2 GB のビデオ データが格納されています。
RAR ファイルを解凍した後、サポート関数 hmdb51Files
を使用してビデオのファイル名とラベルを取得します。
dataFolder = "hmdb51_org";
[files,labels] = hmdb51Files(dataFolder);
classNames = categories(labels);
補助関数 readVideo
(この例の最後に定義) を使用して最初のビデオを読み取り、ビデオのサイズを表示します。ビデオは H x W x C x 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"
) の最後のプーリング層の出力です。
次の図は、ネットワークでのデータ フローを示しています。
ビデオ データを読み取り、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} = 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 ネットワーク アーキテクチャを定義します。次のネットワーク層を指定します。
入力サイズが特徴ベクトルの特徴次元に対応するシーケンス入力層。
後にドロップアウト層がある、隠れユニットが 2,000 個の 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 に設定します (勾配の発散を防ぐため)。
すべてのエポックでデータをシャッフルします。
エポックごとに 1 回ネットワークを検証します。
検証損失が 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 での学習には、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 層の後に出力層を含めます。出力層 (モデル ヘッドと呼ばれることもあります) には、最後の全結合層とソフトマックス層を含めます。
畳み込み層の追加
入力層の平均イメージを抽出します。これは、シーケンス入力層でイメージを正規化するために使用されます。
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
x W
x C-
x 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
参考
trainnet
| trainingOptions
| dlnetwork
| lstmLayer
| sequenceInputLayer
| flattenLayer