###概要
Chainerの開発終了が発表されたうえに顔認識という、いまさら感で溢れてますが、
備忘録を兼ねてここに綴ります。
このシリーズは2回に分けて発信します。
今回は訓練フェーズ(顔画像の学習)の実装方法を説明します。
次回は推論フェーズ(カメラを使った顔認識)の実装を説明する予定です。
なお、ML初学者で当時高校生というのもあって、情報の一部に誤りがあったり、プログラムにバグがあるかもしれません。もしそういったものがありましたら、コメントにて指摘していただけると嬉しいです。
(記事作成日: 2020/2/9)
###環境
-Software-
Windows 10 Home
Anaconda3 64-bit(Python3.7)
Spyder
-Library-
Chainer 7.0.0
-Hardware-
CPU: Intel core i9 9900K
GPU: NVIDIA GeForce GTX1080ti
RAM: 16GB 3200MHz
###参考
書籍
CQ出版 算数&ラズパイから始める ディープ・ラーニング
(Amazonページ)
サイト
Chainer APIリファレンス
###プログラム
一応、Githubに上げておきます。
https://github.com/himazin331/Face-Recognition-Chainer-
リポジトリには訓練フェーズ、推論フェーズ、データ加工プログラム、Haar-Cascadeが含まれています。
###前提
プログラム動作にはAnaconda3のインストールが必要です。
Anaconda3のダウンロード及びインストール方法は下記を参考にしてください。
Anaconda3 ダウンロードサイト
Anaconda3 インストール方法(Windows)
また、私の友人が投稿した記事もよかったら参考にしてみてください。
Anaconda3インストール後、Anaconda3 Power Promptにて、
pip install chainer
と、入力してChainerをインストールしてください。
###学習データについて
今回のプログラムでは学習データがグレースケール画像で32×32pxのJPEGファイルであることを
前提に実装されています。
データ加工には、こちらをご活用ください。
###ソースコード
コードが汚いのはご了承ください...
import argparse as arg
import os
import sys
import chainer
import chainer.functions as F
import chainer.links as L
from chainer import training
from chainer.training import extensions
# CNNの定義
class CNN(chainer.Chain):
# 各層定義
def __init__(self, n_out):
super(CNN, self).__init__(
# 畳み込み層の定義
conv1=L.Convolution2D(1, 16, 5, 1, 0), # 1st 畳み込み層
conv2=L.Convolution2D(16, 32, 5, 1, 0), # 2nd 畳み込み層
conv3=L.Convolution2D(32, 64, 5, 1, 0), # 3rd 畳み込み層
# 全ニューロンの線形結合
link=L.Linear(None, 1024), # 全結合層
link_class=L.Linear(None, n_out), # クラス分類用全結合層(n_out:クラス数)
)
# フォワード処理
def __call__(self, x):
# 畳み込み層->ReLU関数->最大プーリング層The Python path in your debug configuration is invalid.
h1 = F.max_pooling_2d(F.relu(self.conv1(x)), ksize=2) # 1st
h2 = F.max_pooling_2d(F.relu(self.conv2(h1)), ksize=2) # 2nd
h3 = F.relu(self.conv3(h2)) # 3rd
# 全結合層->ReLU関数
h4 = F.relu(self.link(h3))
# 予測値返却
return self.link_class(h4) # クラス分類用全結合層
# Trainer
class trainer(object):
# モデル構築,最適化手法セットアップ
def __init__(self):
# モデル構築
self.model = L.Classifier(CNN(2))
# 最適化手法のセットアップ
self.optimizer = chainer.optimizers.Adam() # Adamアルゴリズム
self.optimizer.setup(self.model) # optimizerにモデルをセット
# 学習
def train(self, train_set, batch_size, epoch, gpu, out_path):
# GPU処理に対応付け
if gpu >= 0:
chainer.cuda.get_device(gpu).use() # デバイスオブジェクト取得
self.model.to_gpu() # 入力データを指定のデバイスにコピー
# データセットイテレータの作成(学習データの繰り返し処理の定義,ループ毎でシャッフル)
train_iter = chainer.iterators.SerialIterator(train_set, batch_size)
# updater作成
updater = training.StandardUpdater(train_iter, self.optimizer, device=gpu)
# trainer作成
trainer = training.Trainer(updater, (epoch, 'epoch'), out=out_path)
# extensionの設定
# 処理の流れを図式化
trainer.extend(extensions.dump_graph('main/loss'))
# 学習毎snapshot(JSON形式)書込み
trainer.extend(extensions.snapshot(), trigger=(epoch, 'epoch'))
# log(JSON形式)書込み
trainer.extend(extensions.LogReport())
# 損失値をグラフにプロット
trainer.extend(extensions.PlotReport('main/loss', 'epoch', file_name='loss.png'))
# 実値をグラフにプロット
trainer.extend(extensions.PlotReport('main/accuracy', 'epoch', file_name='accuracy.png'))
# 学習毎「学習回数, 損失値, 実値, 経過時間」を出力
trainer.extend(extensions.PrintReport(['epoch', 'main/loss', 'main/accuracy', 'elapsed_time']))
# プログレスバー表示
trainer.extend(extensions.ProgressBar())
# 学習開始
trainer.run()
print("___Training finished\n\n")
# モデルをCPU対応へ
self.model.to_cpu()
# パラメータ保存
print("___Saving parameter...")
param_name = os.path.join(out_path, "face_recog.model") # 学習済みパラメータ保存先
chainer.serializers.save_npz(param_name, self.model) # NPZ形式で学習済みパラメータ書込み
print("___Successfully completed\n\n")
# データセット作成
def create_dataset(data_dir):
print("\n___Creating a dataset...")
cnt = 0
prc = ['/', '-', '\\', '|']
# 画像セットの個数
print("Number of Rough-Dataset: {}".format(len(os.listdir(data_dir))))
# 画像データの個数
for c in os.listdir(data_dir):
d = os.path.join(data_dir, c)
print("Number of image in a directory \"{}\": {}".format(c, len(os.listdir(d))))
train = [] # 仮データセット[フォルダ名, ラベル]
label = 0
# 仮データセット作成
for c in os.listdir(data_dir):
print('\nclass: {}, class id: {}'.format(c, label)) # 画像フォルダ名とクラスIDの出力
d = os.path.join(data_dir, c) # フォルダ名と画像フォルダ名の結合
imgs = os.listdir(d)
# JPEG形式の生データだけを読込
for i in [f for f in imgs if ('jpg' or 'JPG' in f)]:
# キャッシュファイルをスルー
if i == 'Thumbs.db':
continue
train.append([os.path.join(d, i), label]) # 画像フォルダパスと画像パスを結合後、リストに格納->仮データセット
cnt += 1
print("\r Loading a images and labels...{} ({} / {})".format(prc[cnt % 4], cnt, len(os.listdir(d))), end='')
print("\r Loading a images and labels...Done ({} / {})".format(cnt, len(os.listdir(d))), end='')
label += 1
cnt = 0
train_set = chainer.datasets.LabeledImageDataset(train, '.') # データセット化
print("\n___Successfully completed\n")
return train_set
def main():
# コマンドラインオプション作成
parser = arg.ArgumentParser(description='Face Recognition train Program(Chainer)')
parser.add_argument('--data_dir', '-d', type=str, default=None,
help='学習フォルダパスの指定(未指定ならエラー)')
parser.add_argument('--out', '-o', type=str,
default=os.path.dirname(os.path.abspath(__file__)) + '/result'.replace('/', os.sep),
help='パラメータの保存先指定(デフォルト値=./result)')
parser.add_argument('--batch_size', '-b', type=int, default=32,
help='ミニバッチサイズの指定(デフォルト値=32)')
parser.add_argument('--epoch', '-e', type=int, default=15,
help='学習回数の指定(デフォルト値=15)')
parser.add_argument('--gpu', '-g', type=int, default=-1,
help='GPU IDの指定(負の値はCPU処理を示す, デフォルト値=-1)')
args = parser.parse_args()
# 学習フォルダパス未指定->例外
if args.data_dir is None:
print("\nException: Folder not specified.\n")
sys.exit()
# 存在しない学習フォルダ指定時->例外
if os.path.exists(args.data_dir) is False:
print("\nException: Folder {} is not found.\n".format(args.data_dir))
sys.exit()
# 設定情報出力
print("=== Setting information ===")
print("# Images folder: {}".format(os.path.abspath(args.data_dir)))
print("# Output folder: {}".format(args.out))
print("# Minibatch-size: {}".format(args.batch_size))
print("# Epoch: {}".format(args.epoch))
print("===========================")
# データセット作成
train_set = create_dataset(args.data_dir)
# 学習開始
print("___Start training...")
Trainer = trainer()
Trainer.train(train_set, args.batch_size, args.epoch, args.gpu, args.out)
if __name__ == '__main__':
main()
コマンド
python face_recog_train_CH.py -d <フォルダ> -e <学習回数> -b <バッチサイズ>
(-o <保存先> -g <GPU ID>)
ファイルの保存先はデフォルトで./result
になっています。
###説明
コードの説明をしていきます。残念ながら説明能力は乏しいです。
####ネットワークモデル
ネットワークモデルは畳み込みニューラルネットワーク(CNN)となっています。
CNNクラスでネットワークモデルを定義しています。
# CNNの定義
class CNN(chainer.Chain):
# 各層定義
def __init__(self, n_out):
super(CNN, self).__init__(
# 畳み込み層の定義
conv1=L.Convolution2D(1, 16, 5, 1, 0), # 1st 畳み込み層
conv2=L.Convolution2D(16, 32, 5, 1, 0), # 2nd 畳み込み層
conv3=L.Convolution2D(32, 64, 5, 1, 0), # 3rd 畳み込み層
# 全ニューロンの線形結合
link=L.Linear(None, 1024), # 全結合層
link_class=L.Linear(None, n_out), # クラス分類用全結合層(n_out:クラス数)
)
# フォワード処理
def __call__(self, x):
# 畳み込み層->ReLU関数->最大プーリング層The Python path in your debug configuration is invalid.
h1 = F.max_pooling_2d(F.relu(self.conv1(x)), ksize=2) # 1st
h2 = F.max_pooling_2d(F.relu(self.conv2(h1)), ksize=2) # 2nd
h3 = F.relu(self.conv3(h2)) # 3rd
# 全結合層->ReLU関数
h4 = F.relu(self.link(h3))
# 予測値返却
return self.link_class(h4) # クラス分類用全結合層
CNNクラスの引数にはchainer.Chain
を渡しています。
chainer.Chain
はChainer特有のクラスで、ネットワークの核となります。
インスタンス生成時にインスタンスメソッド__init__
をコールして、スーパークラスであるchainer.Chain
のインスタンスメソッドを呼び出し、畳み込み層と全結合層を定義します。
本プログラムでの畳み込み層のハイパーパラメータは以下の表のとおりです。
入力チャンネル | 出力チャンネル | フィルタサイズ | ストライド幅 | パディング幅 | |
---|---|---|---|---|---|
1st | 1 | 16 | 5 | 1 | 0 |
2nd | 16 | 32 | 5 | 1 | 0 |
3rd | 32 | 64 | 5 | 1 | 0 |
学習データがグレースケール画像であることを前提としているため、1つめの畳み込み層の入力チャンネル数を1としています。RGB画像であれば3になります。 | |||||
"パディング幅 0"はパディング処理を行わないことを意味します。 |
全結合層のハイパーパラメータは以下の表のとおりです。
入力次元数 | 出力次元数 | |
---|---|---|
全結合層 | None | 1024 |
クラス分類用 | None | 2 |
入力次元数でNone を指定すると自動で入力データの次元数を適用してくれます。 |
今回は2クラス分類を行おうと思うので、クラス分類用の全結合層の出力次元数を2としました。
クラスCNNのインスタンス生成時、引数に数値を入れることで、その数値に対応したクラス分類になります。
(コード上ではn_out
が何クラスに分類するかを意味します。)
もう一つのメソッド__call__
で順伝播を行います。
全体の構造は下の図のとおりです。
プーリング層はプーリング領域を2×2とした最大プーリングです。
####データセット作成
まず、データセットについて注意点がありますので、先にデータセットを作成する関数の説明をします。
データセットの作成はcrate_dataset関数で行います。
# データセット作成
def create_dataset(data_dir):
print("\n___Creating a dataset...")
cnt = 0
prc = ['/', '-', '\\', '|']
# 画像セットの個数
print("Number of Rough-Dataset: {}".format(len(os.listdir(data_dir))))
# 画像データの個数
for c in os.listdir(data_dir):
d = os.path.join(data_dir, c)
print("Number of image in a directory \"{}\": {}".format(c, len(os.listdir(d))))
train = [] # 仮データセット[フォルダ名, ラベル]
label = 0
# 仮データセット作成
for c in os.listdir(data_dir):
print('\nclass: {}, class id: {}'.format(c, label)) # 画像フォルダ名とクラスIDの出力
d = os.path.join(data_dir, c) # フォルダ名と画像フォルダ名の結合
imgs = os.listdir(d)
# JPEG形式の生データだけを読込
for i in [f for f in imgs if ('jpg' or 'JPG' in f)]:
# キャッシュファイルをスルー
if i == 'Thumbs.db':
continue
train.append([os.path.join(d, i), label]) # 画像フォルダパスと画像パスを結合後、リストに格納->仮データセット
cnt += 1
print("\r Loading a images and labels...{} ({} / {})".format(prc[cnt % 4], cnt, len(os.listdir(d))), end='')
print("\r Loading a images and labels...Done ({} / {})".format(cnt, len(os.listdir(d))), end='')
label += 1
cnt = 0
train_set = chainer.datasets.LabeledImageDataset(train, '.') # データセット化
print("\n___Successfully completed\n")
return train_set
分類問題におけるデータセットには、学習データと正解ラベルが必要です。
今回の場合、学習データは顔画像で、正解ラベルはその顔に対応した数値となります。
例えば、不正解クラスと正解クラスがあるとき、
不正解クラスにある学習データのラベルをまとめて「0」
正解クラスにある学習データのラベルをまとめて「1」とします。
その特性上、フォルダの構造に注意しなければなりません。
上のように、1つのフォルダ(train_data)の中に、
各クラスのフォルダ(false, true)を作り、画像データを入れてください。
こうすることで、falseに含まれる学習データの正解ラベルが0に、trueに含まれる正解ラベルが1となります。
コマンドオプション -d で指定するのはこの例でいうと、train_dataになります。
コード中の注釈に書かれているような処理を行った後、
最後に下にあるコードで学習データとラベルがセットになっているリストを、
正式にデータセットとして作成します。
train_set = chainer.datasets.LabeledImageDataset(train, '.') # データセット化
####学習
trainerクラスで機械学習を行う前のセットアップや学習を行います。
# Trainer
class trainer(object):
# モデル構築,最適化手法セットアップ
def __init__(self):
# モデル構築
self.model = L.Classifier(CNN(2))
# 最適化手法のセットアップ
self.optimizer = chainer.optimizers.Adam() # Adamアルゴリズム
self.optimizer.setup(self.model) # optimizerにモデルをセット
インスタンス生成時にインスタンスメソッド__init__
をコールして、ネットワークモデルの構築と最適化アルゴリズムを決定します。
self.model = L.Classifier(CNN(2))
でCNN(2)のカッコ内に任意の値を入れることで、任意のクラス数に分類します。
構築した後、L.Classifier()
というChainer.linksのメソッドにより活性化関数及び損失関数が付与されます。ここでの活性化関数はソフトマックス関数といった出力時に用いる活性化関数です。活性化関数はソフトマックス関数、損失関数は交差エントロピー誤差がデフォルトで設定されているため、分類問題の場合はネットワークモデルをラップするだけで問題ありません。
次に、self.optimizer = chainer.optimizers.Adam()
で最適化アルゴリズムAdamのインスタンスを生成後、
self.optimizer.setup(self.model)
でネットワークモデルを適用します。
trainer
クラス内のtrain
メソッドでは、
データセットイテレータ(Iterator)、updater、trainerの作成を行い、学習を行っていきます。
# 学習
def train(self, train_set, batch_size, epoch, gpu, out_path):
# GPU処理に対応付け
if gpu >= 0:
chainer.cuda.get_device(gpu).use() # デバイスオブジェクト取得
self.model.to_gpu() # 入力データを指定のデバイスにコピー
# データセットイテレータの作成(学習データの繰り返し処理の定義,ループ毎でシャッフル)
train_iter = chainer.iterators.SerialIterator(train_set, batch_size)
# updater作成
updater = training.StandardUpdater(train_iter, self.optimizer, device=gpu)
# trainer作成
trainer = training.Trainer(updater, (epoch, 'epoch'), out=out_path)
# extensionの設定
# 処理の流れを図式化
trainer.extend(extensions.dump_graph('main/loss'))
# 学習毎snapshot(JSON形式)書込み
trainer.extend(extensions.snapshot(), trigger=(epoch, 'epoch'))
# log(JSON形式)書込み
trainer.extend(extensions.LogReport())
# 損失値をグラフにプロット
trainer.extend(extensions.PlotReport('main/loss', 'epoch', file_name='loss.png'))
# 実値をグラフにプロット
trainer.extend(extensions.PlotReport('main/accuracy', 'epoch', file_name='accuracy.png'))
# 学習毎「学習回数, 損失値, 実値, 経過時間」を出力
trainer.extend(extensions.PrintReport(['epoch', 'main/loss', 'main/accuracy', 'elapsed_time']))
# プログレスバー表示
trainer.extend(extensions.ProgressBar())
# 学習開始
trainer.run()
print("___Training finished\n\n")
# モデルをCPU対応へ
self.model.to_cpu()
# パラメータ保存
print("___Saving parameter...")
param_name = os.path.join(out_path, "face_recog.model") # 学習済みパラメータ保存先
chainer.serializers.save_npz(param_name, self.model) # NPZ形式で学習済みパラメータ書込み
print("___Successfully completed\n\n")
下のコードでは、データセットイテレータ(Iterator)の作成を行っています。
# データセットイテレータの作成(学習データの繰り返し処理の定義,ループ毎でシャッフル)
train_iter = chainer.iterators.SerialIterator(train_set, batch_size)
こいつは、データ順序のシャッフルやミニバッチを作成してくれます。
引数として対象となるデータセット(train_set
)とミニバッチサイズ(batch_size
)を指定します。
次に、updaterの作成を行います。
# updater作成
updater = training.StandardUpdater(train_iter, self.optimizer, device=gpu)
updater
は、パラメータの更新を行います。
引数として、データセットイテレータ(train_iter
)と最適化アルゴリズム(self.optimizer
)と必要であればGPU IDを指定してやります。
最適化アルゴリズムはself.optimizer.setup()
でネットワークモデルに最適化アルゴリズムを適用したものです。
直にchainer.optimizers.Adam()
を指定しても動作しません。
次に、trainerの作成を行います。
# trainer作成
trainer = training.Trainer(updater, (epoch, 'epoch'), out=out_path)
trainer
は、学習ループを実装します。
なにをトリガー(条件)に学習を終了するかを定義してやります。
通常は学習回数 epoch かiterationをトリガーにします。
今回の場合は学習回数epoch
をトリガーにします。
引数として、updater(updater
)とストップトリガー((epoch, 'epoch')
)と
その他、拡張機能により作成されるファイルの保存先を指定します。
次はいよいよ、学習! の前に便利な拡張機能を付与してあげましょう。
chainerにはTrainer Extensionという拡張機能があります。
# extensionの設定
# 処理の流れを図式化
trainer.extend(extensions.dump_graph('main/loss'))
# 学習毎snapshot(JSON形式)書込み
trainer.extend(extensions.snapshot(), trigger=(epoch, 'epoch'))
# log(JSON形式)書込み
trainer.extend(extensions.LogReport())
# 損失値をグラフにプロット
trainer.extend(extensions.PlotReport('main/loss', 'epoch', file_name='loss.png'))
# 実値をグラフにプロット
trainer.extend(extensions.PlotReport('main/accuracy', 'epoch', file_name='accuracy.png'))
# 学習毎「学習回数, 損失値, 実値, 経過時間」を出力
trainer.extend(extensions.PrintReport(['epoch', 'main/loss', 'main/accuracy', 'elapsed_time']))
# プログレスバー表示
trainer.extend(extensions.ProgressBar())
ここでは、
・入力データやパラメータの流れなどを以下のようなDOTファイルで書き出してくれる機能
・学習終了時パラメータなどの情報をスナップショットしてくれる機能
(snapshotを用いることで途中から学習を再開することができる)
・学習時の損失値や予測精度の履歴をJSON形式で書きだしてくれる機能
・損失値及び予測精度をグラフにプロットしPNG形式で書き出してくれる機能
・毎学習ごとに学習回数、損失値、予測精度、経過時間を出力する機能
・プログレスバーを表示する機能
を付与しています。
その他にもいくつか拡張機能があります。
Trainer Extensionリファレンス
また、生成されるDOTファイルやPNGファイルといったものはtraining.Trainer()
で指定した保存先に生成されます。
拡張機能を付与したら、ようやく学習開始です。
# 学習開始
trainer.run()
この一行で、すべてが始まります(?)
学習が終了するまで待ちましょう。
学習が終わったら、パラメータの保存を行います。
# パラメータ保存
print("___Saving parameter...")
param_name = os.path.join(out_path, "face_recog.model") # 学習済みパラメータ保存先
chainer.serializers.save_npz(param_name, self.model) # NPZ形式で学習済みパラメータ書込み
print("___Successfully completed\n\n")
chainer.serializers.save_npz()
に、パラメータの保存先(param_name
)とネットワークモデル(self.model
)を
指定してあげれば、NPZ形式でパラメータが保存されます。このパラメータを用いて、実際に顔を認識していきます。
####main関数
main関数はこれといって説明するところがないので割愛。
def main():
# コマンドラインオプション作成
parser = arg.ArgumentParser(description='Face Recognition train Program(Chainer)')
parser.add_argument('--data_dir', '-d', type=str, default=None,
help='学習フォルダパスの指定(未指定ならエラー)')
parser.add_argument('--out', '-o', type=str,
default=os.path.dirname(os.path.abspath(__file__)) + '/result'.replace('/', os.sep),
help='パラメータの保存先指定(デフォルト値=./result)')
parser.add_argument('--batch_size', '-b', type=int, default=32,
help='ミニバッチサイズの指定(デフォルト値=32)')
parser.add_argument('--epoch', '-e', type=int, default=15,
help='学習回数の指定(デフォルト値=15)')
parser.add_argument('--gpu', '-g', type=int, default=-1,
help='GPU IDの指定(負の値はCPU処理を示す, デフォルト値=-1)')
args = parser.parse_args()
# 学習フォルダパス未指定->例外
if args.data_dir is None:
print("\nException: Folder not specified.\n")
sys.exit()
# 存在しない学習フォルダ指定時->例外
if os.path.exists(args.data_dir) is False:
print("\nException: Folder {} is not found.\n".format(args.data_dir))
sys.exit()
# 設定情報出力
print("=== Setting information ===")
print("# Images folder: {}".format(os.path.abspath(args.data_dir)))
print("# Output folder: {}".format(args.out))
print("# Minibatch-size: {}".format(args.batch_size))
print("# Epoch: {}".format(args.epoch))
print("===========================")
# データセット作成
train_set = create_dataset(args.data_dir)
# 学習開始
print("___Start training...")
Trainer = trainer()
Trainer.train(train_set, args.batch_size, args.epoch, args.gpu, args.out)
if __name__ == '__main__':
main()
GPU処理について
私はGPUによる処理ができるように環境を構築してあるので、
以下のような処理を記述してあります。あってもなくとも問題ありませんし、GPUによる処理でなくとも構いません。
ただ、ChainerはTensorflowと違って学習時間が長いので、できればGPUによる処理をお勧めします。。(環境や解く問題によります)
GPU処理環境構築については割愛させてもらいます。
# GPU処理に対応付け
if gpu >= 0:
chainer.cuda.get_device(gpu).use() # デバイスオブジェクト取得
self.model.to_gpu() # 入力データを指定のデバイスにコピー
####注意
下の処理で学習データがいくつあるのかを計上しますが、
1枚多く計上されることがあります。これは、Thumbs.dbというサムネイルキャッシュも含めて計上しているからです。
~~めんどくさいので、~~こいつを考慮して計上するという処理を行ってません。
ですが、データセット作成時にはこいつはスルーするように処理してますので問題ないです。
for c in os.listdir(data_dir):
d = os.path.join(data_dir, c)
print("Number of image in a directory \"{}\": {}".format(c, len(os.listdir(d))))
###おわりに
今回初めて、Qiitaに投稿したのですが不安なところが多々あり、心配です...
概要でも述べましたが、なにか不味いところがあればコメントください。修正します。
次回は、推論フェーズということで、カメラを使って顔の認識を行うんですが...
私の顔を出すことはできないので、公人の顔画像で代用させて頂く予定です。
Chainer以外にも、Tensorflow(tf.keras)で顔認識プログラムを実装したり、フィルタや特徴マップの可視化に挑戦したり、
ハイパーパラメータ最適化フレームワーク「Optuna」を使ってみたりといろいろ挑戦してみたので、今後も投稿できればなと思います。(あくまでも備忘録という形にはなりますが)