背景
突然ですが、当方AI初心者です。大学でモデル予測制御に関する研究をやっていたので、最適化計算やそのアルゴリズムはすこーしだけわかるものの、画像認識やDNNといった部分は専門外です。
そんな私ですが、仕事の都合でDNNを使った画像認識について勉強する必要が出てきました。いろいろ記事や本を読み漁るもののイマイチどうもピンとこない…。そんなとき、こんな記事が目に留まりました。
さて、この天一のロゴを誤検知する問題は、数年前からSNSなどでたびたび騒がれている。なぜかホンダセンシングが多いが、日産(セレナ)でも同様の事案が報告されている。
と、記事内でも触れられているように、自動車メーカー各社で起こり得る事象であり、記事やSNSで取り上げられている企業を批判する意図ではありません。飽くまで事象の紹介です。
正直、これはしょうがないよねって思う一方で、イチ理系としてこのネタ 技術課題に挑戦してみようと思いました。
進め方
ゼロベースでAIを組む能力と時間はないので、転移学習を使ってライトにサクッと挑戦してみます。
転移学習ってなんぞやという方はこちらをご参照ください。
要するに、事前学習済ネットワークの最後の層のみ置き換えて再学習させる手法のようです。メリットとしては学習コストが圧倒的に低いことが挙げられます。マシンパワーという意味でも、再学習に必要な学習素材の規模という意味でも、一般人でもできるくらいのリソースで済むようで。
ネットワークの大部分を流用することで、分類ラベルに依らない汎用的な能力をそのまま使えるということですかね。ファインチューニングっていう言い方がより正確かもしれません。
環境
- Surface Pro 8
- Windows 11 Home
- MATLAB R2024b (Home)
- Deep Learning Toolbox
※MATLAB Onlineだったらツールボックスやアドオンなしでもできます
参考リンク
画像の用意
まずは、画像を用意します。TrafficSignというフォルダを作り、進入禁止, 天下一品というサブフォルダをそれぞれ作成します。あとは、インターネットの画像検索で画像を集めましょう。
↓ 40~50枚ずつ。なるべくいろんな角度や背景になるよう。

そしたら、MATLAB側でそれらを使ってImageDataStoreを作成します。どうやら大量の画像を扱うのに便利なようです(初めて知った)。
clear
close all
clc
% 使用する画像データの読み込み
imds = imageDatastore('TrafficSign', 'IncludeSubfolders',true, 'LabelSource','foldernames');
% 画像データを、学習用:検証用:テスト用 = 70% : 15% : 15% に分類
[imdsTrain, imdsValidation, imdsTest] = splitEachLabel(imds, 0.7,0.15,0.15, "randomized");
学習済みモデルのインポート
学習済モデルとしては、SqueezeNetを使用します。例題でも使用しており、特別なアドオンなく使えるので、まずはこちらで。
また、合わせて学習前の実力確認をやってみましょう。当たり前ですが、何も学習させていないので間違えてます。出たなりでよく頑張ってくれていますね。
% 事前学習済みネットワークの読み込み
[net, classNames] = imagePretrainedNetwork("squeezenet");
inputSize = net.Layers(1).InputSize; % 画像をリサイズするために、入力層のサイズを取得
% 転移学習前の実力確認
numTest = 8;
idx = randperm(numel(imdsTest.Files), numTest);
figure
for i = 1:numTest
subplot(numTest/2,2,i)
% 画像の読み込みと前処理
im = readimage(imdsTest, idx(i));
im = imresize(im, inputSize(1:2));
X = single(im);
% イメージを分類
scores = predict(net, X);
[label,score] = scores2label(scores, classNames);
% 予測されたラベルと対応するスコアを含むイメージを表示
imshow(im)
title(string(label) + " (Score: " + gather(score) + ")")
end
転移学習
では、本題の転移学習に着手します。まずは、事前準備です。
最初に、各種画像を拡張させます。ランダムに画像をシフトさせて、学習素材のN増しとそれに伴うタフネス向上を狙ってます。
% ±30 pixの範囲で画像をずらし、拡張データを作成
pixelRange = [-30 30];
imageAugmenter = imageDataAugmenter('RandXReflection',true, ...
'RandXTranslation',pixelRange, ...
'RandYTranslation',pixelRange);
augimdsTrain = augmentedImageDatastore(inputSize(1:2), imdsTrain, 'DataAugmentation',imageAugmenter);
augimdsValidation = augmentedImageDatastore(inputSize(1:2),imdsValidation);
augimdsTest = augmentedImageDatastore(inputSize(1:2),imdsTest);
そしたら次は、最後の層の置き換えです。ネットワーク構成上、厳密には最後の層ではないのですが、分類に効く事実上の最終層として最後の畳み込み層を置き換えます。事前に確認したところ、当該する層はconv10という名前のようです。
% クラス名と数を取得
classNamesNew = categories(imds.Labels);
numClasses = numel(classNamesNew);
% 既存構造を取得
lgraph = layerGraph(net);
idxReplacedLayer = {lgraph.Layers.Name} == "conv10"; % 名前がconv10のLayerを示すindex
filterSize = lgraph.Layers(idxReplacedLayer).FilterSize; % 元のFilterSizeをバッファ
% 新しい畳み込み層を作成 (FilterSizeは規定値を使用)
newLearnableLayer = convolution2dLayer(filterSize, numClasses, ...
'Name','new_conv10', ...
'WeightLearnRateFactor',10, ...
'BiasLearnRateFactor',10);
% 新しく作成した畳み込み層へ置換
lgraph = replaceLayer(lgraph,'conv10',newLearnableLayer);
myNet = dlnetwork(lgraph);
そして、ようやく学習にかかります。各種オプションを見よう見まねで設定し、trainnetを実行します。
% 学習オプションの設定
options = trainingOptions("adam", ...
ValidationData=augimdsValidation, ...
ValidationFrequency=5, ...
Plots="training-progress", ...
Metrics="accuracy", ...
Verbose=false);
% 学習
netTransfer = trainnet(augimdsTrain, myNet, "crossentropy", options);
学習結果
では、さっそく学習結果を確認しましょう。学習前の実力確認で書いたコードに対して、netをnetTransferに置き換えて再度実行します。どれも良い感じに分類できてますね。
% 学習後の効果確認
figure
for i = 1:numTest
subplot(numTest/2,2,i)
% 画像の読み込みと前処理
im = readimage(imdsTest, idx(i));
im = imresize(im, inputSize(1:2));
X = single(im);
% イメージを分類
scores = predict(netTransfer, X);
[label,score] = scores2label(scores, classNamesNew);
% 予測されたラベルと対応するスコアを含むイメージを表示
imshow(im)
title(string(label) + " (Score: " + gather(score) + ")")
end
感度解析
良い感じに学習できたところで、AIがなにをもってこのように分類できているか気になりますよね?そこで、下記リンクを参考に可視化してみました。
どうやら、gradCAMという関数とocclusionSensitivityという関数でそれぞれ可視化できるようです。gradCAMが損失関数の勾配という解析的な指標で、occlusionSensitivityが摂動に対する応答という実測的な指標を見られるようです。
% 損失関数の勾配を用いた感度解析
figure
for i = 1:numTest
subplot(numTest/2,2,i)
% 画像の読み込みと前処理
im = readimage(imdsTest, idx(i));
im = imresize(im, inputSize(1:2));
X = single(im);
% イメージを分類
scores = predict(netTransfer, X);
[label,score] = scores2label(scores, classNamesNew);
% 予測されたラベルと対応するスコアを含むイメージを表示
imshow(im)
title(string(label) + " (Score: " + gather(score) + ")")
% 感度マップを算出
map = gradCAM(netTransfer, X, label, ReductionLayer="prob");
hold on
imagesc(map,AlphaData=0.5)
colormap jet
end
% オクルージョンを利用した感度解析
figure
for i = 1:numTest
subplot(numTest/2,2,i)
% 画像の読み込みと前処理
im = readimage(imdsTest, idx(i));
im = imresize(im, inputSize(1:2));
X = single(im);
% イメージを分類
scores = predict(netTransfer, X);
[label,score] = scores2label(scores, classNamesNew);
% 予測されたラベルと対応するスコアを含むイメージを表示
imshow(im)
title(string(label) + " (Score: " + gather(score) + ")")
% 感度マップを算出
channel = find(string(label) == classNamesNew);
map = occlusionSensitivity(netTransfer, im, channel);
hold on
imagesc(map, 'AlphaData',0.5)
colormap jet
end
タフネス検証
敵対性生成ノイズを使って、タフネス検証ができるようです。
下記リンクを参考にやってみました。
意図的に誤分類するようなノイズを追加して、人の目には見えないノイズでも見事に騙されています。
% 敵対性ノイズ:初期化
lgraph = layerGraph(netTransfer);
lgraph = removeLayers(lgraph, lgraph.Layers(end).Name);
myNet = dlnetwork(lgraph);
inputSize = myNet.Layers(1).InputSize;
i = 1;
im = readimage(imdsTest,idx(i));
im = imresize(im, inputSize(1:2));
%T = "進入禁止";
T = "天下一品";
targetClass = "進入禁止";
%targetClass = "天下一品";
% 敵対性ノイズ① Untargeted
X = dlarray(single(im), "SSCB");
T = onehotencode(T, 1, 'ClassNames',classNamesNew);
T = dlarray(single(T), "CB");
gradient = dlfeval(@untargetedGradients, myNet, X, T);
epsilon = 1;
XAdv = X + epsilon*sign(gradient);
YPred = predict(myNet, X);
YPred = onehotdecode(squeeze(YPred), classNamesNew, 1);
YPredAdv = predict(myNet, XAdv);
YPredAdv = onehotdecode(squeeze(YPredAdv), classNamesNew, 1);
showAdversarialImage(X, YPred, XAdv, YPredAdv, epsilon);
% 敵対性ノイズ② Targeted
targetClass = onehotencode(targetClass, 1, 'ClassNames',classNamesNew);
epsilon = 5;
alpha = 0.2;
numIterations = 25;
delta = zeros(size(X), 'like',X);
for i = 1:numIterations
gradient = dlfeval(@targetedGradients, myNet, X+delta, targetClass);
delta = delta - alpha*sign(gradient);
delta(delta > epsilon) = epsilon;
delta(delta < -epsilon) = -epsilon;
end
XAdvTarget = X + delta;
YPredAdvTarget = predict(myNet, XAdvTarget);
YPredAdvTarget = onehotdecode(squeeze(YPredAdvTarget), classNamesNew, 1);
showAdversarialImage(X, YPred, XAdvTarget, YPredAdvTarget, epsilon);
%% Local Function
function gradient = untargetedGradients(dlnet,X,target)
Y = predict(dlnet,X);
Y = stripdims(squeeze(Y));
loss = crossentropy(Y,target,'DataFormat','CB');
gradient = dlgradient(loss,X);
end
function gradient = targetedGradients(dlnet,X,target)
Y = predict(dlnet,X);
Y = stripdims(squeeze(Y));
loss = mse(Y,target,'DataFormat','CB');
gradient = dlgradient(loss,X);
end
function showAdversarialImage(image,label,imageAdv,labelAdv,epsilon)
figure
subplot(1,3,1)
imgTrue = uint8(extractdata(image));
imshow(imgTrue)
title("Original Image" + newline + "Class: " + string(label))
subplot(1,3,2)
perturbation = uint8(extractdata(imageAdv-image+127.5));
imshow(perturbation)
title("Perturbation")
subplot(1,3,3)
advImg = uint8(extractdata(imageAdv));
imshow(advImg)
title("Adversarial Image (Epsilon = " + string(epsilon) + ")" + newline + ...
"Class: " + string(labelAdv))
end
余談 GoogleNetの場合
アドオンを入れれば他の学習済みネットワークも使えるようです(冒頭にも書きましたが、MATLAB Onlineであればアドオン不要!)。ということで、他の例としてGoogleNetでも試してみました。基本的な流れは同じなため詳細の説明は割愛させていただきます。
ウダウダ書いてもしょうがないので、結果とソースを記載して終わります。
↓ 感度解析結果(occlusionSensitivity)

clear
close all
clc
% 使用する画像データの読み込み
imds = imageDatastore('TrafficSign', 'IncludeSubfolders',true, 'LabelSource','foldernames');
% 画像データを、学習用:検証用:テスト用 = 70% : 15% : 15% に分類
[imdsTrain, imdsValidation, imdsTest] = splitEachLabel(imds, 0.7,0.15,0.15, "randomized");
% 事前学習済みネットワークの読み込み
[net, classNames] = imagePretrainedNetwork("googlenet");
inputSize = net.Layers(1).InputSize; % 画像をリサイズするために、入力層のサイズを取得
% 転移学習前の実力確認
numTest = 8;
idx = randperm(numel(imdsTest.Files), numTest);
figure
for i = 1:numTest
subplot(numTest/2,2,i)
% 画像の読み込みと前処理
im = readimage(imdsTest, idx(i));
im = imresize(im, inputSize(1:2));
X = single(im);
% イメージを分類
scores = predict(net, X);
[label,score] = scores2label(scores, classNames);
% 予測されたラベルと対応するスコアを含むイメージを表示
imshow(im)
title(string(label) + " (Score: " + gather(score) + ")")
end
% 既存構造を取得
lgraph = layerGraph(net);
% クラス名と数を取得
classNamesNew = categories(imds.Labels);
numClasses = numel(classNamesNew);
% 接続数のみ変えて、新しい全結合層を作成
newLearnableLayer = fullyConnectedLayer(numClasses, ...
'Name','new_fc', ...
'WeightLearnRateFactor',10, ...
'BiasLearnRateFactor',10);
% 新しく作成した全結合層へ置換
lgraph = replaceLayer(lgraph,'loss3-classifier',newLearnableLayer);
% ±30 pixの範囲で画像をずらし、拡張データを作成
pixelRange = [-30 30];
imageAugmenter = imageDataAugmenter('RandXReflection',true, ...
'RandXTranslation',pixelRange, ...
'RandYTranslation',pixelRange);
augimdsTrain = augmentedImageDatastore(inputSize(1:2), imdsTrain, 'DataAugmentation',imageAugmenter);
augimdsValidation = augmentedImageDatastore(inputSize(1:2), imdsValidation);
augimdsTest = augmentedImageDatastore(inputSize(1:2), imdsTest);
% 学習オプションの設定
options = trainingOptions('sgdm', ...
'MiniBatchSize',10, ...
'MaxEpochs',6, ...
'InitialLearnRate',1e-4, ...
'Shuffle','every-epoch', ...
'ValidationData',augimdsValidation, ...
'ValidationFrequency',3, ...
'Verbose',false, ...
'Plots','training-progress');
% 学習
myNet = dlnetwork(lgraph);
netTransfer = trainnet(augimdsTrain, myNet, "crossentropy", options);
% 学習後の効果確認
figure
for i = 1:numTest
subplot(numTest/2,2,i)
% 画像の読み込みと前処理
im = readimage(imdsTest, idx(i));
im = imresize(im, inputSize(1:2));
X = single(im);
% イメージを分類
scores = predict(netTransfer, X);
[label,score] = scores2label(scores, classNamesNew);
% 予測されたラベルと対応するスコアを含むイメージを表示
imshow(im)
title(string(label) + " (Score: " + gather(score) + ")")
end
% 損失関数の勾配を用いた感度解析
figure
for i = 1:numTest
subplot(numTest/2,2,i)
% 画像の読み込みと前処理
im = readimage(imdsTest, idx(i));
im = imresize(im, inputSize(1:2));
X = single(im);
% イメージを分類
scores = predict(netTransfer, X);
[label,score] = scores2label(scores, classNamesNew);
% 予測されたラベルと対応するスコアを含むイメージを表示
imshow(im)
title(string(label) + " (Score: " + gather(score) + ")")
% 感度マップを算出
map = gradCAM(netTransfer, X, label, ReductionLayer="prob");
hold on
imagesc(map,AlphaData=0.5)
colormap jet
end
% オクルージョンを利用した感度解析
figure
for i = 1:numTest
subplot(numTest/2,2,i)
% 画像の読み込みと前処理
im = readimage(imdsTest, idx(i));
im = imresize(im, inputSize(1:2));
X = single(im);
% イメージを分類
scores = predict(netTransfer, X);
[label,score] = scores2label(scores, classNamesNew);
% 予測されたラベルと対応するスコアを含むイメージを表示
imshow(im)
title(string(label) + " (Score: " + gather(score) + ")")
% 感度マップを算出
channel = find(string(label) == classNamesNew);
map = occlusionSensitivity(netTransfer, im, channel);
hold on
imagesc(map, 'AlphaData',0.5)
colormap jet
end









