Main Content

このページの翻訳は最新ではありません。ここをクリックして、英語の最新版を参照してください。

SSD 深層学習を使用したオブジェクト検出

この例では、シングル ショット検出器 (SSD) に学習させる方法を説明します。

概要

深層学習は、検出タスクに必要なイメージの特徴を自動的に学習する強力な機械学習手法です。深層学習を使用したオブジェクト検出には、Faster R-CNN、You Only Look Once (YOLO v2)、SSD など、いくつかの方法があります。この例では、関数 trainSSDObjectDetector を使用して SSD 車両検出器に学習させます。詳細については、オブジェクトの検出を参照してください。

事前学習済みの検出器のダウンロード

学習の完了を待たなくて済むように、事前学習済みの検出器をダウンロードします。検出器に学習させる場合は、変数 doTraining を true に設定します。

doTraining = false;
if ~doTraining && ~exist('ssdResNet50VehicleExample_22b.mat','file')
    disp('Downloading pretrained detector (44 MB)...');
    pretrainedURL = 'https://www.mathworks.com/supportfiles/vision/data/ssdResNet50VehicleExample_22b.mat';
    websave('ssdResNet50VehicleExample_22b.mat',pretrainedURL);
end

データセットの読み込み

この例では、295 個のイメージを含んだ小さな車両データセットを使用します。これらのイメージの多くは、Caltech の Cars 1999 データ セットおよび Cars 2001 データ セットからのものです。Pietro Perona 氏によって作成されたもので、許可を得て使用しています。各イメージには、1 または 2 個のラベル付けされた車両インスタンスが含まれています。小さなデータセットは SSD の学習手順を調べるうえで役立ちますが、実際にロバストな検出器に学習させるにはラベル付けされたイメージがより多く必要になります。

unzip vehicleDatasetImages.zip
data = load('vehicleDatasetGroundTruth.mat');
vehicleDataset = data.vehicleDataset;

学習データは table に保存されています。最初の列には、イメージ ファイルへのパスが含まれています。残りの列には、車両の ROI ラベルが含まれています。データの最初の数行を表示します。

vehicleDataset(1:4,:)
ans=4×2 table
              imageFilename                   vehicle     
    _________________________________    _________________

    {'vehicleImages/image_00001.jpg'}    {[220 136 35 28]}
    {'vehicleImages/image_00002.jpg'}    {[175 126 61 45]}
    {'vehicleImages/image_00003.jpg'}    {[108 120 45 33]}
    {'vehicleImages/image_00004.jpg'}    {[124 112 38 36]}

データ セットを、検出器に学習させるための学習セットと検出器を評価するためのテスト セットに分割します。データの 60% を学習用に選択します。残りを評価用に使用します。

rng(0);
shuffledIndices = randperm(height(vehicleDataset));
idx = floor(0.6 * length(shuffledIndices) );
trainingData = vehicleDataset(shuffledIndices(1:idx),:);
testData = vehicleDataset(shuffledIndices(idx+1:end),:);

imageDatastore および boxLabelDatastore を使用して、学習および評価中にイメージとラベル データを読み込みます。

imdsTrain = imageDatastore(trainingData{:,'imageFilename'});
bldsTrain = boxLabelDatastore(trainingData(:,'vehicle'));

imdsTest = imageDatastore(testData{:,'imageFilename'});
bldsTest = boxLabelDatastore(testData(:,'vehicle'));

イメージ データストアとボックス ラベル データストアを組み合わせます。

trainingData = combine(imdsTrain,bldsTrain);
testData = combine(imdsTest, bldsTest);

学習イメージとボックス ラベルのうちの 1 つを表示します。

data = read(trainingData);
I = data{1};
bbox = data{2};
annotatedImage = insertShape(I,'rectangle',bbox);
annotatedImage = imresize(annotatedImage,2);
figure
imshow(annotatedImage)

Figure contains an axes object. The axes object contains an object of type image.

SSD オブジェクト検出ネットワークの作成

関数 ssdObjectDetector を使用して、SSD オブジェクト検出器を自動的に作成します。ssdObjectDetector では、SSD オブジェクト検出器をパラメーター化するいくつかの入力 (特徴抽出ネットワークとも呼ばれるベース ネットワーク、入力サイズ、クラス名、アンカー ボックス、検出ネットワーク ソースなど) を指定する必要があります。入力ベース ネットワークの特定の層を使用して、検出ネットワーク ソースを指定します。検出ネットワークは、関数 ssdObjectDetector によって入力ベース ネットワークに自動的に接続されます。

通常、特徴抽出ネットワークは事前学習済みの CNN です (詳細については事前学習済みの深層ニューラル ネットワーク (Deep Learning Toolbox)を参照)。この例では特徴抽出に ResNet-50 を使用します。用途の要件によって、MobileNet v2 や ResNet-18 など、その他の事前学習済みのネットワークも使用できます。検出サブネットワークは特徴抽出ネットワークと比べて小さい CNN であり、少数の畳み込み層と SSD に固有の層で構成されます。

net = resnet50();
lgraph = layerGraph(net);

ネットワーク入力サイズを選択する際には、学習イメージのサイズ、および選択したサイズでのデータの処理によって発生する計算コストを考慮します。可能な場合、学習イメージのサイズに近いネットワーク入力サイズを選択します。ただし、この例を実行する計算コストを削減するため、ネットワーク入力サイズを [300 300 3] に指定します。学習中、trainSSDObjectDetector によって、ネットワーク入力サイズに合わせて学習イメージのサイズが自動的に変更されます。

inputSize = [300 300 3];

検出するオブジェクト クラスを定義します。

classNames = {'vehicle'};

検出の学習に関してベース ネットワークをよりロバストにするために、ResNet-50 ネットワークから余分な層を削除し、新しい層を追加します。

補助関数 iRemoveLayers() は、layerGraph オブジェクトに含まれる上記の層の後ろにある層を削除します。ResNet-50 などの事前学習済みのネットワークをバックボーン ネットワークで使用できるようにするため、この関数を使用して、その事前学習済みのネットワークから分類層と全結合層を削除する必要があります。バックボーン特徴抽出ネットワークをよりロバストにするため、7 つの畳み込み層を新たに追加します。

ssdLayerGraph = iRemoveLayers(lgraph,'activation_40_relu');
weightsInitializerValue = 'glorot';
biasInitializerValue = 'zeros';

% Append Extra layers on top of a base network.
extraLayers = [];

% Add conv6_1 and corresponding reLU
filterSize = 1;
numFilters = 256;
numChannels = 1024;
conv6_1 = convolution2dLayer(filterSize, numFilters, NumChannels = numChannels, ...
    Name = 'conv6_1', ...
    WeightsInitializer = weightsInitializerValue, ...
    BiasInitializer = biasInitializerValue);
relu6_1 = reluLayer(Name = 'relu6_1');
extraLayers = [extraLayers; conv6_1; relu6_1];

% Add conv6_2 and corresponding reLU
filterSize = 3;
numFilters = 512;
numChannels = 256;
conv6_2 = convolution2dLayer(filterSize, numFilters, NumChannels = numChannels, ...
    Padding = iSamePadding(filterSize), ...
    Stride = [2, 2], ...
    Name = 'conv6_2', ...
    WeightsInitializer = weightsInitializerValue, ...
    BiasInitializer = biasInitializerValue);
relu6_2 = reluLayer(Name = 'relu6_2');
extraLayers = [extraLayers; conv6_2; relu6_2];

% Add conv7_1 and corresponding reLU
filterSize = 1;
numFilters = 128;
numChannels = 512;
conv7_1 = convolution2dLayer(filterSize, numFilters, NumChannels = numChannels, ...
    Name = 'conv7_1', ...
    WeightsInitializer = weightsInitializerValue, ...
    BiasInitializer = biasInitializerValue);
relu7_1 = reluLayer(Name = 'relu7_1');
extraLayers = [extraLayers; conv7_1; relu7_1];

% Add conv7_2 and corresponding reLU
filterSize = 3;
numFilters = 256;
numChannels = 128;
conv7_2 = convolution2dLayer(filterSize, numFilters, NumChannels = numChannels, ...
    Padding = iSamePadding(filterSize), ...
    Stride = [2, 2], ...
    Name = 'conv7_2', ...
    WeightsInitializer = weightsInitializerValue, ...
    BiasInitializer = biasInitializerValue);
relu7_2 = reluLayer(Name = 'relu7_2');
extraLayers = [extraLayers; conv7_2; relu7_2];

% Add conv8_1 and corresponding reLU
filterSize = 1;
numFilters = 128;
numChannels = 256;
conv8_1 = convolution2dLayer(filterSize, numFilters, NumChannels = numChannels, ...
    Name = 'conv8_1', ...
    WeightsInitializer = weightsInitializerValue, ...
    BiasInitializer = biasInitializerValue);
relu8_1 = reluLayer(Name = 'relu8_1');
extraLayers = [extraLayers; conv8_1; relu8_1];

% Add conv8_2 and corresponding reLU
filterSize = 3;
numFilters = 256;
numChannels = 128;
conv8_2 = convolution2dLayer(filterSize, numFilters, NumChannels = numChannels, ...
    Name = 'conv8_2', ...
    WeightsInitializer = weightsInitializerValue, ...
    BiasInitializer = biasInitializerValue);
relu8_2 = reluLayer(Name ='relu8_2');
extraLayers = [extraLayers; conv8_2; relu8_2];

% Add conv9_1 and corresponding reLU
filterSize = 1;
numFilters = 128;
numChannels = 256;
conv9_1 = convolution2dLayer(filterSize, numFilters, NumChannels = numChannels, ...
    Padding = iSamePadding(filterSize), ...
    Name = 'conv9_1', ...
    WeightsInitializer = weightsInitializerValue, ...
    BiasInitializer = biasInitializerValue);
relu9_1 = reluLayer('Name', 'relu9_1');
extraLayers = [extraLayers; conv9_1; relu9_1];

if ~isempty(extraLayers)
    lastLayerName = ssdLayerGraph.Layers(end).Name;
    ssdLayerGraph = addLayers(ssdLayerGraph, extraLayers);
    ssdLayerGraph = connectLayers(ssdLayerGraph, lastLayerName, extraLayers(1).Name);
end

検出ネットワーク ソースを追加するネットワーク層の名前を指定します。

detNetworkSource = ["activation_22_relu", "activation_40_relu", "relu6_2", "relu7_2", "relu8_2"];

アンカー ボックスを指定します。アンカー ボックス (M 行 1 列の cell 配列) のカウント (M) は、検出ネットワーク ソースのカウントと同じでなければなりません。

anchorBoxes = {[60,30;30,60;60,21;42,30];...
               [111,60;60,111;111,35;64,60;111,42;78,60];...
               [162,111;111,162;162,64;94,111;162,78;115,111];...
               [213,162;162,213;213,94;123,162;213,115;151,162];...
               [264,213;213,264;264,151;187,213]};

SSD オブジェクト検出器オブジェクトを作成します。

detector = ssdObjectDetector(ssdLayerGraph,classNames,anchorBoxes,DetectionNetworkSource=detNetworkSource,InputSize=inputSize,ModelName='ssdVehicle'); 

データ拡張

データ拡張は、学習中に元のデータをランダムに変換してネットワークの精度を高めるために使用されます。データ拡張を使用すると、ラベル付き学習サンプルの数を実際に増やさずに、学習データをさらに多様化させることができます。transform を使用して、以下のように学習データを拡張します。

  • イメージおよび関連するボックス ラベルを水平方向にランダムに反転。

  • イメージおよび関連するボックス ラベルをランダムにスケーリング。

  • イメージの色にジッターを付加。

データ拡張はテスト データには適用されないことに注意してください。理想的には、テスト データは元のデータを代表するもので、バイアスのない評価を行うために変更なしで使用されなければなりません。

augmentedTrainingData = transform(trainingData,@augmentData);

同じイメージを繰り返し読み取り、拡張された学習データを可視化します。

augmentedData = cell(4,1);
for k = 1:4
    data = read(augmentedTrainingData);
    augmentedData{k} = insertShape(data{1},rectangle = data{2});
    reset(augmentedTrainingData);
end

figure
montage(augmentedData,BorderSize = 10)

Figure contains an axes object. The axes object contains an object of type image.

学習データの前処理

拡張された学習データを前処理して学習用に準備します。

preprocessedTrainingData = transform(augmentedTrainingData,@(data)preprocessData(data,inputSize));

前処理された学習データを読み取ります。

data = read(preprocessedTrainingData);

イメージと境界ボックスを表示します。

I = data{1};
bbox = data{2};
annotatedImage = insertShape(I,'rectangle',bbox);
annotatedImage = imresize(annotatedImage,2);
figure
imshow(annotatedImage)

Figure contains an axes object. The axes object contains an object of type image.

SSD オブジェクト検出器の学習

trainingOptions を使用してネットワーク学習オプションを指定します。'CheckpointPath' を一時的な場所に設定します。これにより、学習プロセス中に部分的に学習させた検出器を保存できます。停電やシステム障害などで学習が中断された場合に、保存したチェックポイントから学習を再開できます。

options = trainingOptions('sgdm', ...
        MiniBatchSize = 16, ....
        InitialLearnRate = 1e-3, ...
        LearnRateSchedule = 'piecewise', ...
        LearnRateDropPeriod = 30, ...
        LearnRateDropFactor =  0.8, ...
        MaxEpochs = 20, ...
        VerboseFrequency = 50, ...        
        CheckpointPath = tempdir, ...
        Shuffle = 'every-epoch');

doTraining が true の場合、関数trainSSDObjectDetectorを使用して、SSD オブジェクト検出器に学習させます。そうでない場合は、事前学習済みのネットワークを読み込みます。

if doTraining
    % Train the SSD detector.
    [detector, info] = trainSSDObjectDetector(preprocessedTrainingData,detector,options);
else
    % Load pretrained detector for the example.
    pretrained = load('ssdResNet50VehicleExample_22b.mat');
    detector = pretrained.detector;
end

この例は、12 GB メモリ搭載の NVIDIA™ Titan X GPU で検証済みです。GPU のメモリがこれより少ない場合、メモリ不足が発生する可能性があります。これが発生した場合は、関数 trainingOptions を使用して 'MiniBatchSize' を減らします。この設定を使用してこのネットワークに学習させるのに約 2 時間かかりました。学習所要時間は使用するハードウェアによって異なります。

迅速なテストとして、1 つのテスト イメージ上で検出器を実行します。

data = read(testData);
I = data{1,1};
I = imresize(I,inputSize(1:2));
[bboxes,scores] = detect(detector,I);

結果を表示します。

I = insertObjectAnnotation(I,'rectangle',bboxes,scores);
figure
imshow(I)

Figure contains an axes object. The axes object contains an object of type image.

テスト セットを使用した検出器の評価

大規模なイメージ セットで学習済みのオブジェクト検出器を評価し、パフォーマンスを測定します。Computer Vision Toolbox™ には、平均適合率 (evaluateDetectionPrecision) や対数平均ミス率 (evaluateDetectionMissRate) などの一般的なメトリクスを測定するオブジェクト検出器の評価関数が用意されています。この例では、平均適合率メトリクスを使用してパフォーマンスを評価します。平均適合率は、検出器が正しい分類を実行できること (precision) と検出器がすべての関連オブジェクトを検出できること (recall) を示す単一の数値です。

学習データと同じ前処理変換をテスト データに適用します。データ拡張はテスト データには適用されないことに注意してください。テスト データは元のデータを代表するもので、バイアスのない評価を行うために変更なしで使用されなければなりません。

preprocessedTestData = transform(testData,@(data)preprocessData(data,inputSize));

すべてのテスト イメージに対して検出器を実行します。

detectionResults = detect(detector, preprocessedTestData, MiniBatchSize = 32);

平均適合率メトリクスを使用してオブジェクト検出器を評価します。

[ap,recall,precision] = evaluateDetectionPrecision(detectionResults, preprocessedTestData);

適合率/再現率 (PR) の曲線は、さまざまなレベルの再現率における検出器の適合率を示しています。すべてのレベルの再現率で適合率が 1 になるのが理想的です。より多くのデータを使用すると平均適合率を向上できますが、学習に必要な時間が長くなる場合があります。PR 曲線をプロットします。

figure
plot(recall,precision)
xlabel('Recall')
ylabel('Precision')
grid on
title(sprintf('Average Precision = %.2f',ap))

Figure contains an axes object. The axes object with title Average Precision = 0.88 contains an object of type line.

コード生成

検出器に学習させて評価したら、GPU Coder™ を使用して ssdObjectDetector のコードを生成できます。詳細については、シングル ショット マルチボックス検出器を使用したオブジェクト検出のコードの生成の例を参照してください。

サポート関数

function B = augmentData(A)
% Apply random horizontal flipping, and random X/Y scaling. Boxes that get
% scaled outside the bounds are clipped if the overlap is above 0.25. Also,
% jitter image color.
B = cell(size(A));

I = A{1};
sz = size(I);
if numel(sz)==3 && sz(3) == 3
    I = jitterColorHSV(I,...
        Contrast = 0.2,...
        Hue = 0,...
        Saturation = 0.1,...
        Brightness = 0.2);
end

% Randomly flip and scale image.
tform = randomAffine2d(XReflection = true, Scale = [1 1.1]);  
rout = affineOutputView(sz,tform, BoundsStyle = 'CenterOutput');    
B{1} = imwarp(I,tform,OutputView = rout);

% Sanitize boxes, if needed.
A{2} = helperSanitizeBoxes(A{2}, sz);
    
% Apply same transform to boxes.
[B{2},indices] = bboxwarp(A{2},tform,rout,OverlapThreshold = 0.25);    
B{3} = A{3}(indices);
    
% Return original data only when all boxes are removed by warping.
if isempty(indices)
    B = A;
end
end

function data = preprocessData(data,targetSize)
% Resize image and bounding boxes to the targetSize.
sz = size(data{1},[1 2]);
scale = targetSize(1:2)./sz;
data{1} = imresize(data{1},targetSize(1:2));

% Sanitize boxes, if needed.
data{2} = helperSanitizeBoxes(data{2}, sz);

% Resize boxes.
data{2} = bboxresize(data{2},scale);
end

function lgraph = iRemoveLayers(lgraph, lastLayer)

    % Remove all the layers after lastLayer.
    dg = vision.internal.cnn.RCNNLayers.digraph(lgraph);

    % Find the last layer.
    id = findnode(dg,char(lastLayer));

    % Search for all nodes starting from the feature extraction
    % layer.
    if ~(sum(id)==0)
        ids = dfsearch(dg,id);
        names = dg.Nodes.Name(ids,:);
        lgraph = removeLayers(lgraph, names(2:end));
    end
end

function p = iSamePadding(FilterSize)
    p = floor(FilterSize / 2);
end

参考文献

[1] Liu, Wei, Dragomir Anguelov, Dumitru Erhan, Christian Szegedy, Scott Reed, Cheng Yang Fu, and Alexander C. Berg. "SSD: Single shot multibox detector." In 14th European Conference on Computer Vision, ECCV 2016. Springer Verlag, 2016.

参考

アプリ

関数

オブジェクト

関連するトピック