セマンティック セグメンテーション用の完全畳み込みネットワークの学習と展開
この例では、GPU Coder™ を使用した NVIDIA® GPU での完全畳み込みセマンティック セグメンテーション ネットワークの学習および展開について説明します。
セマンティック セグメンテーション ネットワークはイメージ内のすべてのピクセルを分類して、クラスごとにセグメント化されたイメージを作成します。セマンティック セグメンテーションの応用例としては、自動運転のための道路セグメンテーションや医療診断のための癌細胞セグメンテーションなどがあります。詳細については、深層学習を使用したセマンティック セグメンテーション入門 (Computer Vision Toolbox)を参照してください。
学習手順を示すために、この例では、セマンティック イメージ セグメンテーション用に設計された 1 つのタイプの畳み込みニューラル ネットワーク (CNN) である FCN-8s [1] に学習させます。他のタイプのセマンティック セグメンテーション ネットワークには、SegNet、U-Net などの完全畳み込みネットワークがあります。この学習手順は、これらのネットワークにも適用することができます。
この例では、学習用に Cambridge University の CamVid データセット [2] を使用します。このデータセットは、運転中に得られた路上レベルでのビューが含まれるイメージ コレクションです。データセットは、車、歩行者、道路を含む 32 個のセマンティック クラスについてピクセルレベルのラベルを提供します。
サードパーティの必要条件
必須
CUDA® 対応 NVIDIA GPU および互換性のあるドライバー。
オプション
NVIDIA CUDA Toolkit。
NVIDIA cuDNN ライブラリ。
コンパイラおよびライブラリの環境変数。サポートされているコンパイラおよびライブラリのバージョンの詳細は、サードパーティ ハードウェア (GPU Coder)を参照してください。環境変数の設定は、前提条件となる製品の設定 (GPU Coder)を参照してください。
GPU 環境の検証
関数coder.checkGpuInstall
(GPU Coder)を使用して、この例を実行するのに必要なコンパイラおよびライブラリが正しく設定されていることを検証します。
envCfg = coder.gpuEnvConfig('host'); envCfg.DeepLibTarget = 'cudnn'; envCfg.DeepCodegen = 1; envCfg.Quiet = 1; coder.checkGpuInstall(envCfg);
設定
この例では、VGG-16 ネットワークから初期化された重みを使用して完全畳み込みセマンティック セグメンテーション ネットワークを作成します。関数 vgg16 は、Deep Learning Toolbox Model for VGG-16 Network サポート パッケージの有無をチェックし、事前学習済みの VGG-16 モデルを返します。
vgg16();
事前学習済みのバージョンの FCN をダウンロードします。この事前学習済みのモデルを使用することで、学習の完了を待つことなく例全体を実行できます。doTraining フラグは、この例において、学習済みネットワークの例または事前学習済みの FCN ネットワークをコード生成に使用するかどうかを制御します。
doTraining = false; if ~doTraining pretrainedURL = 'https://www.mathworks.com/supportfiles/gpucoder/cnn_models/fcn/FCN8sCamVid.mat'; disp('Downloading pretrained FCN (448 MB)...'); websave('FCN8sCamVid.mat',pretrainedURL); end
Downloading pretrained FCN (448 MB)...
CamVid データセットのダウンロード
これらの URL から CamVid データセットをダウンロードします。
imageURL = 'http://web4.cs.ucl.ac.uk/staff/g.brostow/MotionSegRecData/files/701_StillsRaw_full.zip'; labelURL = 'http://web4.cs.ucl.ac.uk/staff/g.brostow/MotionSegRecData/data/LabeledApproved_full.zip'; outputFolder = fullfile(pwd,'CamVid'); if ~exist(outputFolder, 'dir') mkdir(outputFolder) labelsZip = fullfile(outputFolder,'labels.zip'); imagesZip = fullfile(outputFolder,'images.zip'); disp('Downloading 16 MB CamVid dataset labels...'); websave(labelsZip, labelURL); unzip(labelsZip, fullfile(outputFolder,'labels')); disp('Downloading 557 MB CamVid dataset images...'); websave(imagesZip, imageURL); unzip(imagesZip, fullfile(outputFolder,'images')); end
データのダウンロード時間はお使いのインターネット接続によって異なります。この例の実行は、ダウンロード操作が完了するまで進行しません。または、Web ブラウザーを使用して、このデータセットをローカル ディスクにまずダウンロードします。次に、変数 outputFolder を使用してダウンロード済みのファイルの場所を指します。
CamVid イメージの読み込み
imageDatastore
を使用して CamVid イメージを読み込みます。imageDatastore によって、大規模なイメージ コレクションを効率的にディスクに読み込むことができます。
imgDir = fullfile(outputFolder,'images','701_StillsRaw_full'); imds = imageDatastore(imgDir);
イメージのうちの 1 つを表示します。
I = readimage(imds,25); I = histeq(I); imshow(I)
CamVid のピクセル ラベル付きイメージの読み込み
pixelLabelDatastore
(Computer Vision Toolbox) を使用して、CamVid のピクセル ラベル イメージ データを読み込みます。pixelLabelDatastore は、ピクセル ラベル データとラベル ID をクラス名のマッピングにカプセル化します。
SegNet ペーパー [3] で説明されている学習方法に従って、CamVid の元の 32 個のクラスを 11 個のクラスにグループ化します。次のクラスを指定します。
classes = [ "Sky" "Building" "Pole" "Road" "Pavement" "Tree" "SignSymbol" "Fence" "Car" "Pedestrian" "Bicyclist" ];
32 個のクラスを 11 個のクラスに減らすには、元のデータセットの複数のクラスをグループとしてまとめます。たとえば、"Car"、"SUVPickupTruck"、"Truck_Bus"、"Train"、および "OtherMoving" を組み合わせたものを "Car" とします。サポート関数 camvidPixelLabelIDs
を使用することで、グループ化されたラベル ID が返されます。
labelIDs = camvidPixelLabelIDs();
クラスおよびラベル ID を使用して、pixelLabelDatastore を作成します。
labelDir = fullfile(outputFolder,'labels');
pxds = pixelLabelDatastore(labelDir,classes,labelIDs);
イメージの上に重ね合わせることで、ピクセル ラベル付きイメージのうちの 1 つを読み取って表示します。
C = readimage(pxds,25);
cmap = camvidColorMap;
B = labeloverlay(I,C,'ColorMap',cmap);
imshow(B)
pixelLabelColorbar(cmap,classes);
色の重ね合わせが存在しない領域にはピクセル ラベルはなく、学習中は使用されません。
データセット統計の解析
CamVid データセット内のクラス ラベルの分布を表示するには、countEachLabel
(Computer Vision Toolbox) を使用します。この関数は、クラス ラベル別にピクセルの数をカウントします。
tbl = countEachLabel(pxds)
tbl=11×3 table
Name PixelCount ImagePixelCount
______________ __________ _______________
{'Sky' } 7.6801e+07 4.8315e+08
{'Building' } 1.1737e+08 4.8315e+08
{'Pole' } 4.7987e+06 4.8315e+08
{'Road' } 1.4054e+08 4.8453e+08
{'Pavement' } 3.3614e+07 4.7209e+08
{'Tree' } 5.4259e+07 4.479e+08
{'SignSymbol'} 5.2242e+06 4.6863e+08
{'Fence' } 6.9211e+06 2.516e+08
{'Car' } 2.4437e+07 4.8315e+08
{'Pedestrian'} 3.4029e+06 4.4444e+08
{'Bicyclist' } 2.5912e+06 2.6196e+08
ピクセル数をクラス別に可視化します。
frequency = tbl.PixelCount/sum(tbl.PixelCount);
bar(1:numel(classes),frequency)
xticks(1:numel(classes))
xticklabels(tbl.Name)
xtickangle(45)
ylabel('Frequency')
観測値の数がすべてのクラスで等しいことが理想的です。CamVid 内のクラスは不均衡です。これは、路上シーンの自動車データセットに共通する問題です。こうしたシーンには、歩行者や自転車運転者のピクセルよりも多くの空、建物、および道路のピクセルが含まれます。これは、空、建物、および道路がイメージ内でより広い領域を占めているためです。学習では上位クラスを優先してバイアスがかけられるため、正しく処理されていない場合は、こうした不均衡が学習プロセスに悪影響を及ぼす可能性があります。この例の後半では、クラスの重み付けを使用してこの問題に対処します。
CamVid データのリサイズ
CamVid データセット内のイメージは 720 × 960 です。学習時間を短縮し、メモリ使用量を削減するには、サポート関数 resizeCamVidImages
および resizeCamVidPixelLabels
を使用して、イメージおよびピクセル ラベル イメージを 360×480 にリサイズします。
imageFolder = fullfile(outputFolder,'imagesResized',filesep); imds = resizeCamVidImages(imds,imageFolder); labelFolder = fullfile(outputFolder,'labelsResized',filesep); pxds = resizeCamVidPixelLabels(pxds,labelFolder);
学習セットとテスト セットの準備
SegNet は、データセットのイメージの 60% を使用して学習されます。残りのイメージはテスト用に使用されます。次のコードでは、イメージとピクセル ラベル データを学習セットとテスト セットに無作為に分割します。
[imdsTrain,imdsTest,pxdsTrain,pxdsTest] = partitionCamVidData(imds,pxds);
60 対 40 に分割すると、トレーニング イメージとテスト イメージの数が次のようになります。
numTrainingImages = numel(imdsTrain.Files)
numTrainingImages = 421
numTestingImages = numel(imdsTest.Files)
numTestingImages = 280
ネットワークの作成
fcnLayers
(Computer Vision Toolbox) を使用して、VGG-16 の重みを使用して初期化される完全な畳み込みネットワーク層を作成します。関数 fcnLayers
はネットワーク変換を実行して VGG-16 からの重みを転送し、セマンティック セグメンテーションに必要な層を新たに追加します。関数 fcnLayers
の出力は FCN を表す LayerGraph オブジェクトです。LayerGraph オブジェクトは、ネットワーク層および層間の接続をカプセル化します。
imageSize = [360 480]; numClasses = numel(classes); lgraph = fcnLayers(imageSize,numClasses);
イメージ サイズは、データセット内のイメージのサイズに基づいて選択されます。クラスの数は CamVid 内のクラスに基づいて選択されます。
クラスの重み付けを使用したクラスのバランス調整
CamVid 内のクラスはバランスがとれていません。学習を改善するために、あらかじめ関数countEachLabel
(Computer Vision Toolbox)によって計算したピクセル ラベルのカウントを使用して、中央頻度クラスの重みを計算できます [3]。
imageFreq = tbl.PixelCount ./ tbl.ImagePixelCount; classWeights = median(imageFreq) ./ imageFreq;
pixelClassificationLayer
(Computer Vision Toolbox) を使用してクラスの重みを指定します。
pxLayer = pixelClassificationLayer('Name','labels','Classes',tbl.Name,'ClassWeights',classWeights)
pxLayer = PixelClassificationLayer with properties: Name: 'labels' Classes: [11×1 categorical] ClassWeights: [11×1 double] OutputSize: 'auto' Hyperparameters LossFunction: 'crossentropyex'
現在の pixelClassificationLayer を削除して新しい層を追加することで、新しい pixelClassificationLayer をもつ SegNet ネットワークを更新します。現在の pixelClassificationLayer には 'pixelLabels' という名前が付けられています。関数removeLayers
を使用してこれを削除し、関数addLayers
を使用して新しい層を追加し、関数connectLayers
を使用して新しい層を残りのネットワークに接続します。
lgraph = removeLayers(lgraph,'pixelLabels'); lgraph = addLayers(lgraph, pxLayer); lgraph = connectLayers(lgraph,'softmax','labels');
学習オプションの選択
学習の最適化アルゴリズムは Adam です。これは "適応モーメント推定" (adaptive moment estimation) に由来します。関数trainingOptions
を使用して、Adam に使用されるハイパーパラメーターを指定します。
options = trainingOptions('adam', ... 'InitialLearnRate',1e-3, ... 'MaxEpochs',100, ... 'MiniBatchSize',4, ... 'Shuffle','every-epoch', ... 'CheckpointPath', tempdir, ... 'VerboseFrequency',2);
'MiniBatchSize' 4 により学習中のメモリ使用量を削減します。この値は、使用しているシステムの GPU メモリの量に応じて増減させることができます。
'CheckpointPath' は一時的な場所に設定されています。この名前と値のペアを設定すると、各学習エポックの終わりにネットワーク チェックポイントを保存できます。システム障害や停電で学習が中断された場合に、保存したチェックポイントから学習を再開できます。'CheckpointPath' で指定された場所に、ネットワーク チェックポイントを保存するのに十分なスペースがあることを確認します。
データ拡張
データ拡張は、学習中に元のデータをランダムに変換してネットワークの精度を高めるために使用されます。データ拡張を使用すると、ラベル付き学習サンプルの数を増やさずに、学習データをさらに多様化させることができます。イメージとピクセル ラベル データの両方に同じランダム変換を適用するには、データストアの統合と変換を使用します。まず、imdsTrain
と pxdsTrain
を統合します。
dsTrain = combine(imdsTrain, pxdsTrain);
次に、データストアの変換を使用して、サポート関数 augmentImageAndLabel
で定義されている目的のデータ拡張を適用します。ここでは、データ拡張に対して +/- 10 ピクセルのランダムな左/右反射とランダムな X/Y 平行移動が使用されます。
xTrans = [-10 10]; yTrans = [-10 10]; dsTrain = transform(dsTrain, @(data)augmentImageAndLabel(data,xTrans,yTrans));
データ拡張は、テスト データと検証データには適用されないことに注意してください。理想的には、テスト データと検証データは元のデータを代表するもので、バイアスのない評価を行うために変更なしで使用されなければなりません。
学習の開始
doTraining
フラグが true の場合は、trainNetwork
を使用して学習を開始します。そうでない場合は、事前学習済みのネットワークを読み込みます。
この学習は、12 GB の GPU メモリ搭載の NVIDIA™ Titan Xp で検証済みです。GPU のメモリがこれより少ない場合、メモリ不足が発生する可能性があります。システムに十分なメモリがない場合は、trainingOptions
の MiniBatchSize
プロパティを 1 に下げてみます。このネットワークの学習は、お使いの GPU ハードウェアによっては約 5 時間またはそれ以上かかります。
doTraining = false; if doTraining [net, info] = trainNetwork(dsTrain,lgraph,options); save('FCN8sCamVid.mat','net'); end
DAG ネットワーク オブジェクトを FCN8sCamVid.mat
という名前の MAT ファイルとして保存します。この MAT ファイルはコード生成時に使用されます。
MEX コード生成の実行
関数 fcn_predict
は、イメージ入力を受け取り、FCN8sCamVid.mat
ファイルに保存されている深層学習ネットワークを使用して、イメージについて予測を実行します。この関数は、ネットワーク オブジェクトを FCN8sCamVid.mat
から永続変数 mynet に読み込み、以降の予測呼び出しではその永続オブジェクトを再利用します。
type('fcn_predict.m')
function out = fcn_predict(in) %#codegen % Copyright 2018-2019 The MathWorks, Inc. persistent mynet; if isempty(mynet) mynet = coder.loadDeepLearningNetwork('FCN8sCamVid.mat'); end % pass in input out = predict(mynet,in);
ターゲット言語を C++ に設定して MEX ターゲットの GPU 構成オブジェクトを生成します。関数 coder.DeepLearningConfig
(GPU Coder) を使用して cuDNN
深層学習構成オブジェクトを作成し、それを GPU コード構成オブジェクトの DeepLearningConfig
プロパティに割り当てます。サイズが [360, 480, 3] の入力を指定して codegen
(MATLAB Coder) コマンドを実行します。このサイズは FCN の入力層に対応します。
cfg = coder.gpuConfig('mex'); cfg.TargetLang = 'C++'; cfg.DeepLearningConfig = coder.DeepLearningConfig('cudnn'); codegen -config cfg fcn_predict -args {ones(360,480,3,'uint8')} -report
Code generation successful: View report
生成された MEX の実行
入力イメージを読み込んで表示します。
im = imread('testImage.png');
imshow(im);
入力イメージに対して fcn_predict_mex
を呼び出して予測を実行します。
predict_scores = fcn_predict_mex(im);
変数 predict_scores
は、各クラスのピクセル単位の予測スコアに対応する 11 個のチャネルを持つ 3 次元行列です。最大予測スコアを使用してチャネルを計算し、ピクセル単位のラベルを取得します。
[~,argmax] = max(predict_scores,[],3);
セグメント化されたラベルを入力イメージに重ね合わせ、セグメント化された領域を表示します。
classes = [ "Sky" "Building" "Pole" "Road" "Pavement" "Tree" "SignSymbol" "Fence" "Car" "Pedestrian" "Bicyclist" ]; cmap = camvidColorMap(); SegmentedImage = labeloverlay(im,argmax,'ColorMap',cmap); figure imshow(SegmentedImage); pixelLabelColorbar(cmap,classes);
クリーンアップ
メモリに読み込まれた静的ネットワーク オブジェクトをクリアします。
clear mex;
サポート関数
function data = augmentImageAndLabel(data, xTrans, yTrans) % Augment images and pixel label images using random reflection and % translation. for i = 1:size(data,1) tform = randomAffine2d(... 'XReflection',true,... 'XTranslation', xTrans, ... 'YTranslation', yTrans); % Center the view at the center of image in the output space while % allowing translation to move the output image out of view. rout = affineOutputView(size(data{i,1}), tform, 'BoundsStyle', 'centerOutput'); % Warp the image and pixel labels using the same transform. data{i,1} = imwarp(data{i,1}, tform, 'OutputView', rout); data{i,2} = imwarp(data{i,2}, tform, 'OutputView', rout); end end
参考文献
[1] Long, J., E. Shelhamer, and T. Darrell."Fully Convolutional Networks for Semantic Segmentation."Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition, 2015, pp. 3431–3440.
[2] Brostow, G. J., J. Fauqueur, and R. Cipolla. "Semantic object classes in video: A high-definition ground truth database." Pattern Recognition Letters. Vol. 30, Issue 2, 2009, pp 88-97.
[3] Badrinarayanan, V., A. Kendall, and R. Cipolla. "SegNet: A Deep Convolutional Encoder-Decoder Architecture for Image Segmentation." arXiv preprint arXiv:1511.00561, 2015.