2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【MATLAB】AI初心者が転移学習で標識認識に挑戦 ~進入禁止と天下一品の分類~

Last updated at Posted at 2026-01-25

背景

突然ですが、当方AI初心者です。大学でモデル予測制御に関する研究をやっていたので、最適化計算やそのアルゴリズムはすこーしだけわかるものの、画像認識やDNNといった部分は専門外です。
そんな私ですが、仕事の都合でDNNを使った画像認識について勉強する必要が出てきました。いろいろ記事や本を読み漁るもののイマイチどうもピンとこない…。そんなとき、こんな記事が目に留まりました。

さて、この天一のロゴを誤検知する問題は、数年前からSNSなどでたびたび騒がれている。なぜかホンダセンシングが多いが、日産(セレナ)でも同様の事案が報告されている。

と、記事内でも触れられているように、自動車メーカー各社で起こり得る事象であり、記事やSNSで取り上げられている企業を批判する意図ではありません。飽くまで事象の紹介です。

正直、これはしょうがないよねって思う一方で、イチ理系としてこのネタ 技術課題に挑戦してみようと思いました。

進め方

ゼロベースでAIを組む能力と時間はないので、転移学習を使ってライトにサクッと挑戦してみます。
転移学習ってなんぞやという方はこちらをご参照ください。

要するに、事前学習済ネットワークの最後の層のみ置き換えて再学習させる手法のようです。メリットとしては学習コストが圧倒的に低いことが挙げられます。マシンパワーという意味でも、再学習に必要な学習素材の規模という意味でも、一般人でもできるくらいのリソースで済むようで。

ネットワークの大部分を流用することで、分類ラベルに依らない汎用的な能力をそのまま使えるということですかね。ファインチューニングっていう言い方がより正確かもしれません。

環境

  • Surface Pro 8
  • Windows 11 Home
  • MATLAB R2024b (Home)
  • Deep Learning Toolbox
    ※MATLAB Onlineだったらツールボックスやアドオンなしでもできます

参考リンク

画像の用意

まずは、画像を用意します。TrafficSignというフォルダを作り、進入禁止, 天下一品というサブフォルダをそれぞれ作成します。あとは、インターネットの画像検索で画像を集めましょう。

↓ 40~50枚ずつ。なるべくいろんな角度や背景になるよう。
スクリーンショット 2026-01-25 152215.jpg

そしたら、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

Before_SqueezeNet.jpg

転移学習

では、本題の転移学習に着手します。まずは、事前準備です。
最初に、各種画像を拡張させます。ランダムに画像をシフトさせて、学習素材の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);

学習進捗_SqueezeNet.jpg

学習結果

では、さっそく学習結果を確認しましょう。学習前の実力確認で書いたコードに対して、netnetTransferに置き換えて再度実行します。どれも良い感じに分類できてますね。

% 学習後の効果確認
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

After_SqueezeNet.jpg

感度解析

良い感じに学習できたところで、AIがなにをもってこのように分類できているか気になりますよね?そこで、下記リンクを参考に可視化してみました。

どうやら、gradCAMという関数とocclusionSensitivityという関数でそれぞれ可視化できるようです。gradCAMが損失関数の勾配という解析的な指標で、occlusionSensitivityが摂動に対する応答という実測的な指標を見られるようです。

gradCAM
% 損失関数の勾配を用いた感度解析
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

gradCAMの結果
GradCAM_SqueezeNet.jpg

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) + ")")
    % 感度マップを算出
    channel = find(string(label) == classNamesNew);
    map = occlusionSensitivity(netTransfer, im, channel);
    hold on
    imagesc(map, 'AlphaData',0.5)
    colormap jet 
end

occlusionSensitivityの結果
OcclusionSensitivity_SqueezeNet.jpg

タフネス検証

敵対性生成ノイズを使って、タフネス検証ができるようです。
下記リンクを参考にやってみました。

意図的に誤分類するようなノイズを追加して、人の目には見えないノイズでも見事に騙されています。

TargetedNoise.jpg

SqueezeNetでの敵対性ノイズ
% 敵対性ノイズ:初期化
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でも試してみました。基本的な流れは同じなため詳細の説明は割愛させていただきます。

ウダウダ書いてもしょうがないので、結果とソースを記載して終わります。

↓ 学習前
Before_GoogleNet.jpg

↓ 学習状況
学習進捗_GoogleNet.jpg

↓ 学習結果
After_GoogleNet.jpg

↓ 感度解析結果(gradCAM
GradCAM_GoogleNet.jpg

↓ 感度解析結果(occlusionSensitivity
OcclusionSensitivity_GoogleNet.jpg

GoogleNetの場合

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
2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?