3Dモデルから自動的に生成した画像とアノテーションのデータセットで学習するセマンティックセグメンテーション

# はじめに

セマンティックセグメンテーションを行う時に普段はアノテーションを作成するのは大変な作業でしょう。もしそれが自動的に作れるのならどれくらい楽になるでしょうね。

私は「自動的に生成された画像データセットで学習して本物に適用する」ということはよくやっています。普通の分類モデルでも教師データを準備することは大変なことだから、自動生成のデータが代わりに使えたら楽ですね。

そしてその生成データはセマンティックセグメンテーションにも使えるようにすることもできます。自動的に生成したデータなので、アノテーションも当然同時に作成することができます。しかもこれは手作業より正確で完璧なアノテーションになるでしょう。

「学習データがないので自分で生成する」という話はよくあることで新しいことではないのですが、これをセマンティックセグメンテーションに使う例はあまり聞いたことないの意外でした。だから私は自分で試してみました。

この記事ではランダムで3Dモデルを作成してレンダリングしてそれを使ってセマンティックセグメンテーションのニューラルネットワークに学習させる実装の例を説明します。

今回のコードはMATLABで書いています。理由はMATLABの3Dレンダリング機能で簡単に綺麗な3D画像が作れるからです。Pythonにはmatplotlibやmayaviがあるけど、MATLABの方がよくできています。PythonはMayaやBlenderなど3D専用のソフトウェアと連携できますが、そうしない限りPythonだけでMATLABほど綺麗な3D画像を作るのは難しいでしょう。

というわけで、今回の実装は全部MATLABを使うことにしましたが、自動的に学習データとアノテーションを作成するという考え方としてPythonや他の言語も通用するでしょう。

MATLABで深層学習の実装方法及びpytorchの書き方の比較は前回の記事に書いてあります。まずその記事を読んだら今回の記事がわかりやすくなると思います。

https://qiita.com/phyblas/items/19874643d91be99818fd

なお、セマンティックセグメンテーションの基本については沢山記事があるので割愛します。この記事の最後に纏めておいたリンクを参考に。

セマンティックセグメンテーションがかなり人気な手法で、このqiitaにも実装する例が沢山ありますが、全部Python（pytorch, keras, tensorflow, chainer）で、MATLABによる実装の例はなくて恐らくこの記事で初めてでしょうね。だから自分で書き方をアレンジするしかないです。


# 3Dモデルの作成

今回使うのは自分で作成する<ruby>海星<rt>ヒトデ</rt></ruby>のデータセットです。

まずこのコードを実行したら綺麗な<ruby>海星<rt>ヒトデ</rt></ruby>が出てきます。
```matlab
sx = 360; % 円周におけるポリゴンの分割
sy = 100; % 暑さにおけるポリゴンの分割
atsusa = 1; % 暑さ
hankei1 = 1.5; % 中の半径
hankei2 = 4; % 外の半径

% 暑さと円周のメッシュ
[ar_z,ar_theta] = meshgrid(linspace(0,atsusa-(atsusa/sy),sy), ...
                           linspace(0,360-(360/sx),sx));
ar_a = ((sin((1-ar_z/atsusa).^0.5*pi/2)).^2)*hankei2;
ar_r = ar_a - (hankei2-hankei1)./hankei2.*ar_a.*abs(cos(deg2rad((ar_theta)*5/2)));
ar_r = ar_r + randn(sx,sy)*0.04; % ランダムで表面を凸凹にする

% 少しねじる
ar_theta = ar_theta+15.*sin(deg2rad(ar_theta*5/2)).^2;
aaa = cumsum(randn(1,sx));
aaa = (aaa - linspace(aaa(1),aaa(end),sx))'/range(aaa)*10;
ar_theta = ar_theta+aaa;

% 直交座標系にする
ar_x = ar_r.*cos(deg2rad(ar_theta));
ar_y = ar_r.*sin(deg2rad(ar_theta));

% 頂点
ar_v1 = [ar_x(:) ar_y(:) ar_z(:)];
ar_v2 = [0 0 atsusa]; % 一番上の頂点
ar_v = [ar_v1
        ar_v2];

% 面
fv1 = (1:sx) + (0:sx:sx*(sy-1)-1)';
fv2 = [2:sx 1] + (0:sx:sx*(sy-1)-1)';
fv3 = [2:sx 1] + (sx:sx:sx*sy-1)';
fv4 = (1:sx) + (sx:sx:sx*sy-1)';
ar_f1 = cat(3,fv1.',fv2.',fv3.',fv4.');
ar_f1 = reshape(ar_f1,[],4);
ar_f2 = [(sx*(sy-1)+1:sx*sy)' ...
         [sx*(sy-1)+2:sx*sy sx*(sy-1)+1]' ...
         repmat(sx*sy+1,sx,1) ...
         NaN(sx,1)];
ar_f = [ar_f1;ar_f2];

% 海星の表面の色。HSV空間でランダムしてRGBに変換する
ar_hsv = [mod(unifrnd(0.02,0.08,[sx*sy+1 1]),1),unifrnd(0.3,1,[sx*sy+1 1]),unifrnd(0.7,1,[sx*sy+1 1])];
ar_c = hsv2rgb(ar_hsv);

close all
figure(Position=[100 100 600 600])

% 地面を入れる
[ymesh,xmesh] = meshgrid( ...
    linspace(-8,8,501), ...
    linspace(-8,8,501));
zmesh = -0.1.*rand(501,501);
mesh_jimen = mesh(xmesh,ymesh,zmesh,FaceColor=[0.79 0.63 0.16],LineStyle="none");

% 海星のポリゴンを入れる
patch( ...
    Faces=ar_f, ...
    Vertices=ar_v, ...
    FaceVertexCData=ar_c, ...
    FaceColor="interp", ...
    LineStyle="none");
material shiny % 明るい表面にする
lighting gouraud % グーローシェーディングを使う

set(gcf,color="k");
set(gca,Position=[0 0 1 1])
light(Position=[2 5 10],color=[0.9,0.9,0.9]); % 照明を入れる
camproj("perspective") % 透視図モード
view(0,50) % 眺める角度
axis equal off % 各軸の単位を揃える
```
![hitode.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/6a4e4c3b-c48b-43dc-7c52-fde5ac5a962b.jpeg)

今回はこのような<ruby>海星<rt>ヒトデ</rt></ruby>を検出するためのセマンティックセグメンテーションです。

3Dモデルを作ることは今回の記事の主題ではないので、説明は割愛しますが、MATLABによる3Dモデリングに関する基本は次の記事で説明しています。

https://qiita.com/phyblas/items/150a34293df36b8da313

今の簡単な<ruby>海星<rt>ヒトデ</rt></ruby>の3Dモデルで本物の<ruby>海星<rt>ヒトデ</rt></ruby>を代表できるとは言い難いのですが、もっと色々工夫したらできるでしょう。でもコードは複雑になるので、今回はただ簡単な例を作るためだからこれくらいにしましょう。


# ランダムで作った3Dモデルから学習用の画像を量産する

以上のコートで一枚の<ruby>海星<rt>ヒトデ</rt></ruby>の画像ができますが、もし色な大きさや地面など色々ランダムしたら、沢山違う画像ができるでしょう。

今回は学習に使うためにランダムで1000枚を作ります。同時に<ruby>海星<rt>ヒトデ</rt></ruby>がない地面だけの空っぽ画像も作って同じように学習に使います。そして<ruby>海星<rt>ヒトデ</rt></ruby>のピクセルを示すアノテーションも作成します。だから生成されるのは全部3000×3枚となります。

<ruby>海星<rt>ヒトデ</rt></ruby>なしの画像も学習に使うのは、<ruby>海星<rt>ヒトデ</rt></ruby>がない画像にも適用できるようにするためです。<ruby>海星<rt>ヒトデ</rt></ruby>あり画像ばっかり学習すると全ての画像はどこかで<ruby>海星<rt>ヒトデ</rt></ruby>があるという基準になる恐れがあるから。

それに<ruby>海星<rt>ヒトデ</rt></ruby>を入れるのと入れない同じ画像のペアで学習すると<ruby>海星<rt>ヒトデ</rt></ruby>の特徴を把握しやすいと思うから。このようなことができるのは人工的に作るデータだからこそですね。

生成コードは以下です。
```matlab
sx = 360;
sy = 100;
n_img = 2000; % 生成する海星の画像の枚数
px = 256; % 保存する画像の解像度
fol_nashi = "hitodenashi"; % 海星なし画像を保存するフォルダ
fol_ari = "hitodeari"; % 海星あり画像を保存するフォルダ
fol_anno = "hitodeanno"; % アノテーション画像を保存するフォルダ

% 保存するフォルダまだ存在していなければまず作成する
if(~exist(fol_nashi,"dir"))
    mkdir(fol_nashi)
end
if(~exist(fol_ari,"dir"))
    mkdir(fol_ari)
end
if(~exist(fol_anno,"dir"))
    mkdir(fol_anno)
end

for i = 1524:n_img % ループして一枚ずつ生成する
    % 毎回色々ランダムする
    atsusa = unifrnd(0.8,3);
    hankei1 = unifrnd(0.8,2);
    hankei2 = unifrnd(3,5);
    theta0 = unifrnd(0,360);
    idou_x = unifrnd(-3,3);
    idou_y = unifrnd(-3,3);
    h = unifrnd(0.04,0.16);
    xyz_shoumei = [unifrnd(-10,10) unifrnd(-10,10) 10];
    
    [ar_z,ar_theta] = meshgrid(linspace(0,atsusa-(atsusa/sy),sy), ...
                               linspace(0,360-(360/sx),sx));
    ar_a = ((sin((1-ar_z/atsusa).^0.5*pi/2)).^2)*hankei2;
    ar_r = ar_a - (hankei2-hankei1)./hankei2.*ar_a.*abs(cos(deg2rad((ar_theta-theta0-unifrnd(0,72))*5/2)));
    ar_r = ar_r + randn(sx,sy)*0.04;
    
    ar_theta = ar_theta+unifrnd(-15,15).*sin(deg2rad((ar_theta-theta0)*5/2)).^2;
    aaa = cumsum(randn(1,sx));
    aaa = (aaa - linspace(aaa(1),aaa(end),sx))'/range(aaa)*10;
    ar_theta = ar_theta+aaa;

    ar_x = ar_r.*cos(deg2rad(ar_theta))+idou_x;
    ar_y = ar_r.*sin(deg2rad(ar_theta))+idou_y;
    
    ar_v1 = [ar_x(:) ar_y(:) ar_z(:)];
    ar_v2 = [idou_x idou_y atsusa];
    ar_v = [ar_v1;ar_v2];
    
    fv1 = (1:sx) + (0:sx:sx*(sy-1)-1)';
    fv2 = [2:sx 1] + (0:sx:sx*(sy-1)-1)';
    fv3 = [2:sx 1] + (sx:sx:sx*sy-1)';
    fv4 = (1:sx) + (sx:sx:sx*sy-1)';
    ar_f1 = cat(3,fv1.',fv2.',fv3.',fv4.');
    ar_f1 = reshape(ar_f1,[],4);
    ar_f2 = [(sx*(sy-1)+1:sx*sy)' ...
             [sx*(sy-1)+2:sx*sy sx*(sy-1)+1]' ...
             repmat(sx*sy+1,sx,1) ...
             NaN(sx,1)];
    ar_f = [ar_f1;ar_f2];
    
    ar_hsv = [mod(unifrnd(h-0.03,h+0.03,[sx*sy+1 1]),1),unifrnd(0.3,1,[sx*sy+1 1]),unifrnd(unifrnd(0.2,0.9),1,[sx*sy+1 1])];
    ar_c = hsv2rgb(ar_hsv);
    
    
    close all
    figure(visible="off",Position=[100 100 600 600])
    set(gcf,color="k");
    set(gca,Position=[0 0 1 1])

    % 地面のメッシュを入れる
    kaizoudo_jimen = randi([100,600]);
    [ymesh,xmesh] = meshgrid( ...
        linspace(-8,8,kaizoudo_jimen+1), ...
        linspace(-8,8,kaizoudo_jimen+1));
    zmesh = -0.1.*rand(kaizoudo_jimen+1,kaizoudo_jimen+1);
    mesh_jimen = mesh(xmesh,ymesh,zmesh,...
             FaceColor=hsv2rgb([unifrnd(0.06,0.32),unifrnd(0.1,0.6),unifrnd(0.3,0.9)]),LineStyle="none");
    material shiny
    lighting gouraud

    camproj("perspective")
    light(Position=xyz_shoumei,color=[unifrnd(0.8,1),unifrnd(0.8,1),0.9]);
    axis equal off
    view(0,90)
    set(gca,CameraPosition=[0 0 20])
    set(gca,CameraTarget=[0 0 0])
    set(gca,CameraViewAngle=42)
    
    % 人手なしの画像を保存する
    img1 = imresize(getframe(gcf).cdata,[px px]);
    imwrite(img1,sprintf(fullfile(fol_nashi,"%04d.jpg"),i));
    
    % 海星を入れる
    patch( ...
        Faces=ar_f, ...
        Vertices=ar_v, ...
        FaceVertexCData=ar_c, ...
        FaceColor="interp", ...
        LineStyle="none");
    material shiny
    lighting gouraud
    
    % 海星ありの画像を保存する
    img2 = imresize(getframe(gcf).cdata,[px px]);
    imwrite(img2,sprintf(fullfile(fol_ari,"%04d.jpg"),i));

    delete(mesh_jimen) % 地面のメッシュを消して、海星だけ残す
    % アノテーションの画像を作る
    img3 = all(imresize(getframe(gcf).cdata,[px px])>0,3);
    imwrite(img3,sprintf(fullfile(fol_anno,"%04d.png"),i),BitDepth=1);
end
```

これを実行したら生成が始まります。生成速度はかなり速いです。パソコンの性能にもよりますが、私のパソコンでは1000個で10分くらいかかります。

このようにいっぱい<ruby>海星<rt>ヒトデ</rt></ruby>ができました。
![截屏2024-05-26-03.38.50.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/f95f8290-0255-6593-66e2-ce231e8a2a4e.jpeg)


ここで<ruby>海星<rt>ヒトデ</rt></ruby>なしともアノテーションとも並べて一部表示してみます。
```matlab
px = 256;
n = 6;
imds_nashi = imageDatastore("hitodenashi");
imds_ari = imageDatastore("hitodeari");
imds_anno = imageDatastore("hitodeanno");

arimg = zeros(px,px*3+2,3,n,"uint8")+255;
for i = 1:n
    arimg(:,1:px,:,i) = imds_nashi.read;
    arimg(:,2+px:1+px*2,:,i) = imds_ari.read;
    arimg(:,3+px*2:2+px*3,:,i) = repmat(imds_anno.read*255,1,1,3,1);
end

imwrite(imtile(arimg,GridSize=[n 1],BorderSize=1,BackgroundColor="w"),"hitode3x6.jpg");
```

![hitode3x6.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/f3905b75-45f1-1a61-2855-a735c7553556.jpeg)

このようにアノテーションもパッケージで作られました。ちゃんと<ruby>海星<rt>ヒトデ</rt></ruby>の輪郭になっています。これで学習用のデータが整いました。


# 使うニューラルネットワークのモデル

セマンティックセグメンテーションに使うニューラルネットワークのモデルは色々あって、今一番評判がいいのはDeepLab v3+でしょう。MATLABでもDeepLab v3+が準備されておいて簡単に使えるのですが、今回は小さい画像でも軽く実装できるU-netを使います。MATLABではU-netが準備してあるので簡単に使えます。

U-netは私が以前から使っていて馴染んでいるモデルです。記事にも書きました。

https://qiita.com/phyblas/items/2ad3d70841ca4a888ee4

https://qiita.com/phyblas/items/d4884a58041eaa01ef5e

以前使ったのはノイズ除去のためですが、本来U-netはセマンティックセグメンテーションのために作られたモデルですね。

実際にノイズ除去に使われたU-netはセマンティックセグメンテーションに使われるモデルとは少し違うが、構造は殆ど同じなのでどっちもU-netと呼びますね。

違いは例えば最後の層はセマンティックセグメンテーションの場合はソフトマックスですが、ノイズ除去では[LReLU](https://qiita.com/nishiha/items/c8c3eab87cbf289a69e0)に入れ替えられます。

adobe illustratorのextendscriptで今回使うU-netモデルの構造を描いてここに載せておきます。

![unet.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/fecd89cd-29db-f146-07d2-a37df5008bd8.png)

ここでmは画像のサイズで、nは入力チャネル数です。今回は色画像なのでn=3となりますが、n=1のグレースケールも使えます。convは畳み込み層でconvTは逆畳み込み層であり、後ろにある数字は出力チャネル数×カーネルサイズ、s=ストライド、p=パディング。最後の層でのkは分割する種類の数です。今回は<ruby>海星<rt>ヒトデ</rt></ruby>と地面でk=2だけとなります。

基本的にmaxpool層で半分ずつ小さくして、その後逆畳み込み層（convT）で2倍ずつ大きくする、というオートエンコーダーの構造ですが、スキップ接続があるのは特徴ですね。特徴マップのサイズは一番小さないところ（最後のmaxpoolの後）では入力サイズの1/16まで縮められます。


# セマンティックセグメンテーションの実装

データが準備できて、使うニューラルネットワークのモデルも決まったら、次はこれを使ってセマンティックセグメンテーションの学習をします。

画像は256ピクセルで生成されたのですが、このサイズの画像を扱うとなるとGPUがないと無理です。今回はCPUでも気軽に動ける程度の例をしたいので、64ピクセルに縮小した画像を使います。

セマンティックセグメンテーションでは評価の基準は正確度よりもダイス**係数**（F1値）や**IoU**や**Tversky**損失が使われることが多いのです。今回は一番わかりやすいダイス係数を使います。**再現率**（recall）と**適合率**（precision）も一緒に学習曲線グラフを描きます。10エポック経ってもダイス係数が上がらなければ学習はそれで終了となって、ダイス係数が一番高い時のモデルを採用します。

実装のコードは以下です。
```matlab
px = 64; % 入力画像のピクセル
n_epoch = 200; % 最大のエポック数
mouii = 10; % 正確度が何エポック上がらなければ学習が止まる
batch_size = 32; % 各ミニバッチの数
test_size = 0.2; % テストに使うデータの割合
fol_nashi = "hitodenashi"; % 海星なしデータが保存されてあるフォルダ
fol_ari = "hitodeari"; % 海星ありデータが保存されてあるフォルダ
fol_anno = "hitodeanno"; % アノテーションデータが保存されてあるフォルダ

% モデルはU-netを使う
dlnet = unet([px px 3],2);

% 学習ループを進める関数
function [dlnet,param_ag,param_asg] = fcn_update(dlnet,X,T,param_ag,param_asg,i_iter)
    if(canUseGPU) % GPUが使える場合
        X = gpuArray(X);
        T = gpuArray(T);
    end
    [Y,state] = dlnet.forward(X);
    loss = crossentropy(Y,T);
    param_grad = dlgradient(loss,dlnet.Learnables);
    dlnet.State = state;
    [dlnet,param_ag,param_asg] = adamupdate(dlnet,param_grad,param_ag,param_asg,i_iter);
end

% 海星ありと海星なしのデータを連結して準備する関数
function [X,T] = fcn_xt(ar_nashi,ar_ari,ar_anno)
    X = dlarray(cat(4,ar_nashi,ar_ari),"SSCB");
    T = dlarray( ...
        cat(4, ...
            cat(3,zeros(size(ar_anno)),ones(size(ar_anno))), ...
            cat(3,ar_anno,1-ar_anno)), ...
        "SSCB");
end

imds_nashi = imageDatastore(fol_nashi, ...
    ReadFcn=@(fn)(single(imresize(imread(fn),[px px])))/255);
imds_ari = imageDatastore(fol_ari, ...
    ReadFcn=@(fn)(single(imresize(imread(fn),[px px])))/255);
imds_anno = imageDatastore(fol_anno, ...
    ReadFcn=@(fn)single(imresize(imread(fn),[px px])>0));

% 全部の画像データを読み込んでおく
ar_nashi = cat(4,imds_nashi.readall{:});
ar_ari = cat(4,imds_ari.readall{:});
ar_anno = cat(4,imds_anno.readall{:});

n_data = numel(imds_ari.Files); % 全部のデータの数
n_kenshou = round(n_data*test_size); % 検証データの数
n_kunren = n_data-n_kenshou; % 訓練データの数
n_batch = ceil(n_kunren/batch_size); % 学習の時にミニバッチに分ける回数

% ランダムで訓練と検証データに分ける
idx_rand = randperm(n_data);
idx_kunren = idx_rand(1:n_kunren);
idx_kenshou = idx_rand(n_kunren+1:end);

param_ag = [];
param_asg = [];
lis_dice_kunren = []; % 各エポックの訓練データのダイス係数を収める配列
lis_dice_kenshou = []; % 各エポックの検証データの分割間違い率を収める配列
lis_saigen_kunren = []; % 各エポックの訓練データの再現率を収める配列
lis_saigen_kenshou = []; % 各エポックの検証データの分再現率を収める配列
lis_tekigou_kunren = []; % 各エポックの訓練データの適合率を収める配列
lis_tekigou_kenshou = []; % 各エポックの検証データの分適合率を収める配列
lis_sonshitsu_kunren = []; % 各エポックの訓練データの損失を収める配列
lis_sonshitsu_kenshou = []; % 各エポックの検証データの損失を収める配列
mouiika = 0; % 上がらない回数を数える
i_iter = 1; % 繰り返しの回数を数える

figure(Position=[100 100 400 600]); % 学習曲線のグラフを描く準備
fprintf("訓練データ数：%d\n検証データ数：%d\n学習開始\n",n_kunren,n_kenshou)
t0 = datetime("now");
% 学習ループ開始
for i_epoch = 1:n_epoch
    idx_perm = randperm(n_kunren);
    for i_batch = 1:n_batch
        idx_batch = idx_kunren(idx_perm((i_batch-1)*batch_size+1:min(i_batch*batch_size,n_kunren)));
        [X,T] = fcn_xt(...
            ar_nashi(:,:,:,idx_batch), ...
            ar_ari(:,:,:,idx_batch), ...
            ar_anno(:,:,:,idx_batch));
        % パラメータの更新
        [dlnet,param_ag,param_asg] = dlfeval(@fcn_update,dlnet,X,T,param_ag,param_asg,i_iter);
        i_iter = i_iter+1;
    end

    % 各エポックの学習が終わった後、学習データと検証データで検証を行う
    [X,T] = fcn_xt(ar_nashi(:,:,:,idx_kunren(1:n_kenshou)),ar_ari(:,:,:,idx_kunren(1:n_kenshou)),ar_anno(:,:,:,idx_kunren(1:n_kenshou)));
    Y = minibatchpredict(dlnet,X,MiniBatchSize=batch_size);
    lis_sonshitsu_kunren = cat(1,lis_sonshitsu_kunren,crossentropy(Y,T));

    seikaika = (Y(:,:,1,:)>0.5)==T(:,:,1,:);
    saigen = mean(seikaika(T(:,:,1,:)==1),"all");
    tekigou = mean(seikaika(Y(:,:,1,:)>0.5),"all");
    lis_dice_kunren = cat(1,lis_dice_kunren,2*(saigen*tekigou)/(saigen+tekigou));
    lis_saigen_kunren = cat(1,lis_saigen_kunren,saigen*100);
    lis_tekigou_kunren = cat(1,lis_tekigou_kunren,tekigou*100);

    [X,T] = fcn_xt(ar_nashi(:,:,:,idx_kenshou),ar_ari(:,:,:,idx_kenshou),ar_anno(:,:,:,idx_kenshou));
    Y = minibatchpredict(dlnet,X,MiniBatchSize=batch_size);
    lis_sonshitsu_kenshou = cat(1,lis_sonshitsu_kenshou,crossentropy(Y,T));

    seikaika = (Y(:,:,1,:)>0.5)==T(:,:,1,:);
    saigen = mean(seikaika(T(:,:,1,:)==1),"all");
    tekigou = mean(seikaika(Y(:,:,1,:)>0.5),"all");
    lis_dice_kenshou = cat(1,lis_dice_kenshou,2*(saigen*tekigou)/(saigen+tekigou));
    lis_saigen_kenshou = cat(1,lis_saigen_kenshou,saigen*100);
    lis_tekigou_kenshou = cat(1,lis_tekigou_kenshou,tekigou*100);
    
    % 学習曲線を描く
    tiledlayout(4,1,Padding="none",TileSpacing="tight");
    nexttile
    hold on
    [max_kunren,idxmax_kunren] = max(lis_dice_kunren);
    [max_kenshou,idxmax_kenshou] = max(lis_dice_kenshou);
    p1 = plot(lis_dice_kunren,"r",LineWidth=2,DisplayName=sprintf("訓練データ（最大 %.3g）",max_kunren));
    p2 = plot(lis_dice_kenshou,"g",LineWidth=2,DisplayName=sprintf("検証データ（最大 %.3g）",max_kenshou));
    plot(idxmax_kunren,max_kunren," or",MarkerFaceColor="r")
    plot(idxmax_kenshou,max_kenshou," og",MarkerFaceColor="g")
    legend([p1,p2],Location="best",FontSize=12)
    xlim([0.9 numel(lis_dice_kenshou)+0.1])
    xticklabels([])
    ylabel("ダイス係数",FontSize=12)
    grid

    nexttile
    hold on
    plot(clip(lis_saigen_kunren,1e-6,100),"r",LineWidth=2)
    plot(clip(lis_saigen_kenshou,1e-6,100),"g",LineWidth=2)
    xlim([0.9 numel(lis_saigen_kenshou)+0.1])
    xticklabels([])
    ylabel("再現率（%）",FontSize=12)
    grid

    nexttile
    hold on
    plot(clip(lis_tekigou_kunren,1e-6,100),"r",LineWidth=2)
    plot(clip(lis_tekigou_kenshou,1e-6,100),"g",LineWidth=2)
    xlim([0.9 numel(lis_tekigou_kenshou)+0.1])
    xticklabels([])
    ylabel("適合率（%）",FontSize=12)
    grid

    nexttile
    hold on
    plot(lis_sonshitsu_kunren,"r",LineWidth=2)
    plot(lis_sonshitsu_kenshou,"g",LineWidth=2)
    set(gca,YScale="log")
    xlim([0.9 numel(lis_sonshitsu_kunren)+0.1])
    xlabel("エポック",FontSize=12)
    ylabel("交差エントロピー",FontSize=12)
    grid
    saveas(gca,"lc_hitode.png")

    fprintf("%.2f分経って、%dエポックが終わった。最大ダイス係数は%.3f\n",minutes(datetime("now")-t0),i_epoch,max_kenshou);

    % 学習の各段階での予測結果の例も描いておく
    X = ar_ari(:,:,:,idx_kenshou(1:36));
    Y = minibatchpredict(dlnet,dlarray(X,"SSCB"),MiniBatchSize=batch_size).extractdata;
    imwrite(imtile([X;repmat(Y(:,:,1,:),1,1,3,1)]),sprintf("imtile_hitode%02d.jpg",i_epoch))

    % 検証データのダイス係数が最大より更に上がったかどうか
    if(lis_dice_kenshou(end) <= max(lis_dice_kenshou(1:end-1)))
        % 上がらなかった場合
        mouiika = mouiika+1;
        if(mouiika >= mouii)
            break % 数回も上がっていない場合すぐ学習を終了させる
        end
    else
        % 上がったらもう一度最初から数える
        mouiika = 0;
        % 今後使うために、一番いい結果を出す時の学習済みモデルを保存しておく
        save("hitodenet.mat","dlnet")
    end
end
```

GPUのない環境でこれを実行したら数時間がかかるでしょう。


# 結果

学習が終了した後、保存した学習曲線グラフはこうなりました。

![lc_hitode.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/7e93b4ce-a345-2c2a-0fca-1629cb0cb09f.png)

訓練データも検証データもあまり違いがなくて、ダイス係数が1の近くまで達したので、ちゃんと学習できたとわかります。

今回は80エポックまで行ったら検証データのダイス係数が10エポック上がっていないという条件が満たされてここで終了して、一番いい結果を出した70エポック目のモデルを採用することになりました。訓練データの方は学習が続いたらもっといい結果になりそうですが、それは過学習になるだけですね。

そして各学習ループが終わった後予測の例を描いたのでここで一部載せておきます。

まずは1エポック目。学習し始めたばかりでまだあまり形になっていない状態ですね。上述のグラフでもわかる通り再現率は低くて10%くらいしかない。
![imtile_hitode01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/2d8d7428-aae1-e59f-a27f-67253e162830.jpeg)

2エポック目になると大体いい形になってきましたね。再現率も70%になっているから。ただし白っぽいところがいっぱいあるってのは<ruby>海星<rt>ヒトデ</rt></ruby>ではない部分まで<ruby>海星<rt>ヒトデ</rt></ruby>と判断しすぎて、だから適合率が低いです。再現率が高くても適合率が低いというのも、違う意味で駄目です。ダイス係数は再現率も適合率も同時に考慮するバランスのいいスコアだからこれを基準とするのに意味がありますね。
![imtile_hitode02.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/17345485-8e14-3f43-980a-de857701a87d.jpeg)

3エポック目になると再現率が更に高くなって、余計な白い部分も大分なくなって適合率も回復しています。
![imtile_hitode03.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/53b8eb71-0dd2-0e3b-0426-6fb11997b065.jpeg)

それが続いて<ruby>海星<rt>ヒトデ</rt></ruby>の形が整っていきます。
5エポック目。
![imtile_hitode05.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/07230650-decc-5e0a-59e5-5da6ca404a87.jpeg)

17エポック目。大分きれいになってきました。
![imtile_hitode17.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/49b5933e-4dc9-35f4-836d-db7a8f5b9ff4.jpeg)

19エポック目では突然崩れました。グラフでもはっきり見えるほど大きな急変。これはやばいですね。
![imtile_hitode19.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/4b2e5a31-c577-a625-f34d-0fdaf24938e7.jpeg)

その後すぐ回復してよかったです。学習の時こういう突然崩壊することもよくあるのですね。回復しないままずっと壊れていく場合もあるから、いつもいい結果が出せる時のパラメータを保存しておくのは大事です。せっかく時間をかけて学習したのにいきなり台無しになるのは嫌ですよね。

次の段階で形は既に安定して特に変化がなくて、学習は一番いい70エポック目に辿り着きました。まあ、大体30エポックから数字も予測の形も殆ど変わらないので30エポックで終了してもいい気がしますけどね。

![imtile_hitode70.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/1a56bdcf-9692-ae09-8393-f1d12cf28512.jpeg)

学習が終了の後大部分はちゃんと<ruby>海星<rt>ヒトデ</rt></ruby>の形になっていますが、まだ境界線がおかしくて上手くいかないものがありますね。特に<ruby>海星<rt>ヒトデ</rt></ruby>と地面が似ている例。これはやはり難易度高くなりますね。

実際にもっと学習データを沢山生成したり、毎エポックで水増ししたりすることで画像のバリエーションを増やしたら更に結果はよくなりますけど、データが増えると学習にかかる時間も長くなるので今回はこれくらいにします。これでも1エポックに2~3分かかって、80エポックは3時間以上かかりました。因みに使ったパソコンはMacBook Air M3。

また、今回は<ruby>海星<rt>ヒトデ</rt></ruby>みたいな簡単な形をしているものだからこんな64ピクセルの小さい画像でも簡単に区別できるが、もっと複雑なものだとこんな小さな画像では難しいはずです。十分大きい入力サイズも大事ですが、その分計算の負担は莫大になってGPUが欠かせないものになります。


# 終わりに

以上生成データの中でセマンティックセグメンテーションが上手くいったとわかりました。でもこのモデルをこのまま実の写真に使うことは難しいでしょう。ただ簡単に数十行のコードで作成したものだから。まだ本物の<ruby>海星<rt>ヒトデ</rt></ruby>とはあまり似ていないから。実際の自然で撮った写真はもっと複雑だから学習データもそれなりに頑張らないといけないのですね。

もし3Dモデルからレンダリングした画像で学習したセマンティックセグメンテーションが実際に上手く本物の写真に使えるようになったらそれは凄いことになると思います。アノテーションの手間は全然かからないからです。

でもどうやって本物と同じ3Dモデルを作るのか、これは課題となりますね。AI技術とは別として、3D関連の知識は勿論必要です。
私が目指しているのもそのような、3DモデリングもできるAIプログラマーです。


# 参考&もっと読む

この記事を書く前に私いままで沢山セマンティックセグメンテーション関連の記事を読んできました。これら参考になった記事のリンクをここに貼っておきます。

#### 基本
- [画像セグメンテーションについて調査中](https://qiita.com/nonbiri15/items/3d10c52690024df82415)
- [深層学習を用いたセグメンテーションの紹介　セグメンテーションシリーズ①](https://qiita.com/skillup_ai/items/04e4f752a38eda5292fc)
- [セマンティックセグメンテーションをざっくり学ぶ](https://qiita.com/fujiya228/items/ea30dac6ef827d608a56)
- [セグメンテーションとマルチスケールアテンション](https://qiita.com/chiba1sonny/items/2f678643cd40b13c5d55)
- [国土地理院の空中写真データセットでセマンティック・セグメンテーションしてみた](https://qiita.com/naoto-github/items/e9e0022f4c46f84bcfc6)

#### アノテーションの話
- [画像の差分からセグメンテーションのアノテーションを作りたい！](https://qiita.com/milukyna/items/b5a88346bc97525f6117)
- [アノテーション仕様の曖昧さ (アスパラ収穫環境のSemantic Segmentationを例に)](https://qiita.com/yuki-inaho/items/c7dc76573f342ce72182)
- [[論文紹介]アノテーションを自動作成してくれるアルゴリズムが凄い！](https://qiita.com/Ryu0907/items/ee5efedbcfc1702ba022)
- [LabelMEの使い方（セマンティックセグメンテーション用アノテーションツール）](https://qiita.com/kotaxtech/items/f4afe24c6c033f77e10a)
- [たった数クリックでセグメンテーションのマスクを作る！？　Edge Flowの紹介](https://qiita.com/koshian2/items/8beab322ff20228658d8)

#### 各モデル
- [2022年時点リアルタイムセマンティックセグメンテーションモデルのまとめ](https://qiita.com/kenmaro/items/059a0bfc272a0c4e9465)
- [segmentation_models_pytorchの使い方と実装例](https://qiita.com/tchih11/items/6e143dc639e3454cf577)
- [セマンティックセグメンテーションについてザックリ解説【E資格対策】](https://qiita.com/yamo1116/items/614be88067afb85ea43c)

#### SegNet
- [MathWorks機械学習・深層学習セミナーの感想](https://qiita.com/kazuki1012/items/e4defb1bd557f9a0dd75)
- [ChainerでSegNetとU-Net](https://qiita.com/physics303/items/3fcb0af825a32f48c42e)
- [実装で学ぶ深層学習(segmentation編) ~SegNet の実装~](https://qiita.com/milky9712/items/dd4fae64751938c038e3)
- [【semantic segmentation】SegNet : Pooling Indicesでメモリの効率化](https://qiita.com/minh33/items/d800394f480b2d6eef33)
- [SegNet: 画像セグメンテーションニューラルネットワーク](https://qiita.com/cyberailab/items/d11862852eccc17585e8)
- [CaDIS: a Cataract Datasetで画像セグメンテーション](https://qiita.com/burokoron/items/c730e607607c925c6fd1)

#### U-Net
- [U-Net紹介](https://qiita.com/chiba1sonny/items/1dc0ab8edff02c210352)
- [U-Netとは　セグメンテーションシリーズ②](https://qiita.com/skillup_ai/items/94d5dbccee20aaf3c54f)
- [【Pytorch】UNetを実装する](https://qiita.com/gensal/items/03e9a6d0f7081e77ba37)
- [ボルダリングジムのホールドセグメンテーション](https://qiita.com/nagato0614/items/573f9397cb869139e5a5)
- [畳み込みオートエンコーダによる画像の再現、ノイズ除去、セグメンテーション](https://qiita.com/cvusk/items/019c254db883957b3050)
- [セマンティックセグメンテーションをやってみた](https://qiita.com/yakisobamilk/items/2470354c8d01aaf1e510)
- [U-net構造で、画像セグメンテーションしてみた。（１）](https://qiita.com/taashi/items/03728edc0bf4e8332f39)
- [U-net構造で、画像セグメンテーションしてみた。（２）](https://qiita.com/taashi/items/161f828472c6196633f1)
- [UNet(系)で、マルチクラスセグメンテーション(1)](https://qiita.com/taashi/items/1214f7aaf704e5507482)
- [U-NetでPascal VOC 2012の画像をSemantic Segmentationする (TensorFlow)](https://qiita.com/tktktks10/items/0f551aea27d2f62ef708)
- [UNetで脳腫瘍を検出してみた(colab環境)](https://qiita.com/1998922akito/items/c5d178f8108b629c6a12)

#### PSPNet
- [自作データを使ったPyTorchによるPSPNetの実装](https://qiita.com/rio53/items/90b757c2c0a3b0f8da77)
- [PSPNetをpytorchで実装しようとしたけどできなかった](https://qiita.com/nessie_sesein/items/fd4997f3d46b7ac2e189)
- [セマンティックセグメンテーション PSPNet](https://qiita.com/toshi_machine/items/35471d079064ce20aa71)
- [1日目　目標は衛星画像のSemantic Segmentation。](https://qiita.com/nigo1973/items/b33d63b976761d8c602e)
- [【semantic segmentation】PSPNet : Pyramid Pooling最強説](https://qiita.com/minh33/items/00af29d8984877a9051d)
- [PSPNetで脳腫瘍を検出してみた(colab環境)](https://qiita.com/1998922akito/items/0bd1174bf0c5fd149b7f)

#### DeepLab
- [【Semantic Segmentation】DeepLab (V1) : Fully Connected CRFでセグメンテーションを向上](https://qiita.com/minh33/items/8eb31d16a975d2a87de5)
- [【Semantic Segmentation】DeepLab(v2) : DeepLab(v1)との違いは？](https://qiita.com/minh33/items/23030207de240edecc15)
- [【Semantic Segmentation】DeepLab(v3) : DeepLab(v2)との違いは？](https://qiita.com/minh33/items/deb7765ca064f1b0477b)
- [【Semantic Segmentation】DeepLab(v3+) : DeepLab(v3)との違いは？](https://qiita.com/minh33/items/8f05c0a76d14f0977898)
- [DeepLab で Sematic Segmentation する（デモを動かす）](https://qiita.com/matchyy/items/09ce0881c5214d12dc5e)
- [DeepLabのSemantic Segmentationやーる（Windows10、Python3.6）](https://qiita.com/SatoshiGachiFujimoto/items/0f177f3bd0e2c4dc445c)
- [DeepLab v3+（意訳）](https://qiita.com/mine820/items/14e7c556b358dbc4ee9a)
- [DeepLab v3+でオリジナルデータを学習してセグメンテーションできるようにする](https://qiita.com/mine820/items/7a08fad3847f2981cb01)
- [DeepLab v3+でオリジナルデータを学習してセマンティックセグメンテーションする](https://qiita.com/otakoma/items/2f40e583980013acb2f7)
- [DeepLabv3＋を自前のデータセットで学習させる](https://qiita.com/john-rocky/items/b362931286739162628d)
- [誰でも出来る！DeepLab v3+でGPUを使って自作データセットで学習・推論する](https://qiita.com/mucchyo/items/d21993abee5e6e44efad)
- [DeepLab学習用画像の要件](https://qiita.com/j_catfish/items/0b1075c5cd5367406dd1)
- [最強のSemantic Segmentation「Deep lab v3 plus」を用いて自前データセットを学習させる](https://qiita.com/harmegiddo/items/b11decca0769fc108ec9)

#### 各分野の応用
- [農業領域でのSemantic Segmentationの難しさ（作物と環境の多様性）と対応方法の例](https://qiita.com/yuki-inaho/items/fc5fc8033b89a9b3a65c)
- [Pytorchによる航空画像の建物セグメンテーションの作成方法.](https://qiita.com/nigo1973/items/1d7495a963c23c97189c)
- [セマンティックセグメンテーションによる建造物識別](https://qiita.com/tammy2/items/6bb23a6b8fabdae67f7d)
- [アニメイラストのセグメンテーションがやりたかった話](https://qiita.com/artemis5656/items/81e30c9e37411bc771c9)
- [肺のX線画像のセグメンテーション（U-Net）をやってみた](https://qiita.com/insilicomab/items/423a362b7697c76707ed)
- [U-NetからDeepLab V3に変更したときの肺X線画像セグメンテーションの精度改善](https://qiita.com/insilicomab/items/70d5fd829403273d417f)
- [PSPNet vs UNet 脳腫瘍データセット](https://qiita.com/1998922akito/items/34fc89ccd36342270c6b)

#### その他
- [[PyTorch]セグメンテーションのためのDataAugmentation](https://qiita.com/switch23/items/bb3de2b2d1230a477821)
- [kerasを用いたセマンティックセグメンテーション](https://qiita.com/Ryosuke11/items/9ce0e44b1a03dfd885b8)
- [セマンティックセグメンテーションを試してみる(Pytorch)](https://qiita.com/shushin/items/125abecceb2dbb4ae5bf)
- [猫画像から猫部分のみを抽出する(matting/semantig segmentation)](https://qiita.com/knok/items/6ad09cc870739dbd921b)
- [U2Netを独自データでトレーニングする](https://qiita.com/john-rocky/items/1cbaa9a2fefb08624859)
- [Semantic Segmentationの実装](https://qiita.com/MMsk0914/items/2f64a741e04b36cd1c76)
- [segmentation_models_pytorchの使い方と実装例](https://qiita.com/tchih11/items/6e143dc639e3454cf577)
- [Image Pyramid(画像ピラミッド)と深層学習中の応用](https://qiita.com/chiba1sonny/items/be5a6e1d1d539bf21a11)
- [学習最適化のための損失関数とOptimizer & MRI画像を使った比較](https://qiita.com/ppza53893/items/8090322792e1c7f81e57)
- [纏を習得する](https://qiita.com/shun-kusano/items/eb545112caedff8d2b64)