どうもエンジニアのirohasです。
普段からAI開発をメインに活動をしていますが、
今回はその人工知能のタスクの中で、画像認識分野で遊んでみたのでそれを記事にしていこうと思います。
目次
1.はじめに
2.環境
3.転移学習とは
4.ResNet18の紹介
5.データセットの前処理
6.転移学習の設定
7.学習
8.学習結果
9.テスト
10.まとめ
11.参考文献
1. はじめに
画像認識技術は、デジタル画像やビデオから情報を抽出し理解するコンピュータ科学の分野で、この技術は、さまざまなアプリケーションで活用されており、日常生活にも広く浸透しています。
この技術について、ここでは初心者の方にもわかりやすく説明していきます。
画像認識とは?
画像認識は、コンピュータが画像の中の物体、人物、場所、行動などを識別し理解する技術です。例えば、スマートフォンのカメラが顔を認識してピントを合わせる機能や、セキュリティシステムが不審者の顔を識別する機能などがあります。
どのように機能するのか?
画像認識システムは大きく分けて以下のステップで機能します。
・画像の取得: 最初に、デジタルカメラやスキャナーなどを使用して画像を取得します。
・前処理: 画像の品質を向上させるために、ノイズ除去やコントラスト調整などの前処理が行われます。
・特徴抽出: 画像から特徴を抽出します。これには、色、形、テクスチャなど、識別に有用な情報が含まれます。
・分類: 抽出した特徴をもとに、画像の内容が何であるかを識別します。これには機械学習のアルゴリズムがよく用いられます。
画像認識の応用例
最近至る所でこの技術は使用されていますが、以下に代表例をまとめておきます。
・顔認証:顔決済や勤怠管理、体温測定などで使用されている技術
・画像生成AI:イラスト生成や動画生成などで使用されている技術。モデルとしては、Stable-diffusionなどが有名。Deep Fakeなど、悪用もされている技術であり、早急な法整備などが急がれている。
・自動運転: 車両が周囲の環境を理解し、他の車両、歩行者、障害物を識別する。
・医療画像分析: MRIやCTスキャンなどの画像から疾患の診断を支援したりする。
・セキュリティ: 監視カメラの映像から不審な行動を検出し、セキュリティを強化する。
・小売業: 商品認識を通じて在庫管理を自動化する。
2. 環境
PC: MacOS(MacBook Pro M2, 1TB)
言語: Python v3.10
使用ライブラリ:Pytorch,matplotlib,numpy,os
3. 転移学習とは
転移学習(Transfer Learning)は、機械学習の一分野で、あるタスクで学習した知識を別のタスクに応用する技術。このアプローチは、新しいタスクでゼロから学習を始めるのではなく、既にトレーニングされたモデルを基にして、新しい問題に対応させることで、学習時間の短縮やデータの少ない状況でも高い性能を実現することができます。
転移学習の主なメリット
・効率的な学習: 既存のモデルを再利用することで、トレーニングに必要な時間とリソースを大幅に削減できる。
・データの少ない問題に対応: 新しいタスクで利用可能なデータが少ない場合でも、既に豊富なデータで学習されたモデルを使用することで、性能の向上が期待できる。
・汎用性の向上: 一つのタスクで得られた知識が他の多くのタスクにも有用であることが多いため、より汎用的なモデルの構築が可能になる。
ファインチューニンングとの違い
転移学習は、あるタスク(ソースタスク)で訓練されたモデルの知識を、異なるが関連する別のタスク(ターゲットタスク)に適用するプロセスを指すのに対して、
ファインチューニングは、転移学習の一部と見なすことができる技術であるが、事前に訓練されたモデル(通常は転移学習のソースモデル)を新しいタスクに適応させる目的で再訓練するプロセスのことを指す。
数式から読み解く違い
転移学習では、下記の計算で学習を行っていきます。
1.ソースモデルのパラメータ:
\theta_S (事前学習済み)
2.ターゲットモデルのパラメータ初期値:
\theta_T = \theta_S
3.ターゲットタスク用のデータ:
\mathcal{D}_T = \{(x_i ,y_i)\}
4.学習プロセス:
データ
\mathcal{D}_Tを使用して、\theta_Tを更新する
ファインチューニングでは、下記の計算で学習を行っていきます。
1.事前学習済みのパラメータ:
\theta_S
2.ファインチューニングされるパラメータの初期値:
\theta'_T = \theta_S
3.ターゲットタスクのデータ:
\mathcal{D}_T = \{(x_i ,y_i)\}
4.パラメータの再学習:
\theta'_T = \theta'_T - \alpha \nabla_{\theta'_T}L(\theta'_T;x_i,y_i)
ここで、Lは損失関数、αは学習率、∇θ'Tは、損失関数の勾配です。
上記の式の違いから読み解ける主な違いは、ターゲットタスクにどのように適応するかという点です。
転移学習はソースモデルのパラメータを直接新しいタスクの初期パラメータとして使用し、これを全体的または部分的に調整することがありますが、ファインチューニングでは、特定の層のパラメータを積極的に再訓練し、新しいタスクに特化させることを目的としています。
これにより、ターゲットタスクに対するモデルの適応性と性能が向上します。
4.ResNet18の紹介
ResNet18は、ResNet(Residual Network)ファミリーの一員で、18層の深さを持つ畳み込みニューラルネットワークです。
ResNetは、画像認識や画像分類タスクに広く利用されているアーキテクチャで、特にディープラーニングの分野で注目されています。
2015年にMicrosoft ResearchのKaiming Heらによって提案されたこのネットワークは、その効果的な「残差学習」のアプローチにより、非常に深いネットワークを効率的に学習することが可能になりました。
ResNetの主要な特徴
1.残差ブロック(Residual Blocks):
ResNetの核心的な構造は、残差ブロックです。これは、入力をブロックの出力にショートカット接続(スキップ接続)して加える設計をしています。このアプローチにより、ネットワークは入力の「残差」を学習することになります。これにより、勾配消失問題が緩和され、非常に深いネットワークでも効率的に学習することが可能になります。
2.ショートカット接続:
ResNetでは、目指す写像を F(x) とし、入力との残差を F(x)−x で定義します。従って、目的の写像は F(x)+x と表されます。
ショートカット接続は、いくつかの層をスキップする単純な恒等写像で、追加のパラメータが不要であり、計算の複雑さを増さず、逆伝播も実施可能です。
数式によるResNetブロックの定義は以下になります:
y = \mathcal{F}(x, { W_i }) + x
ここで、xとyはそれぞれブロックの入力ベクトルと出力ベクトルです。
F(x,Wi)
は学習すべき残差写像を示し、この場合、残差関数は、
F=W_2σ(W_1x)
として表されます。(σはReLU関数です。)
また、入力とFの次元が異なる場合、次のように線形射影Wsを使用して次元を合わせることが可能です:
y = \mathcal{F}(x, \{W_i\}) + W_s x
残差関数Fは、通常、2つまたは3つの層で構成されることが多く、各層は畳み込み層や全結合層として実装され、1層のみでは、ブロックが線形になり、残差の効果が低減されます。
3.バックボーンアーキテクチャとしての利用:
ResNet18は比較的浅いモデルでありながら、その性能は十分に高く、多くの画像処理タスクでバックボーンとして利用されます。また、転移学習の基盤としても広く用いられており、様々なドメインの問題に適応させることができます。
4.アーキテクチャの詳細
ResNet18は以下の構成から成り立っています:
・初期の畳み込み層とプール層
・8つの残差ブロック(各ブロックは2つの畳み込み層から構成)
・平均プーリング層
・全結合層
合計で18層(畳み込み層と全結合層の合計)から成り立っており、このシンプルで効果的な構造は、多くの標準的なデータセットで高い精度を実現できます。
コード上で構成を見ると下記のようになっています。
ResNet(
(conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
(layer1): Sequential(
(0): BasicBlock(
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
(1): BasicBlock(
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(layer2): Sequential(
(0): BasicBlock(
(conv1): Conv2d(64, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(64, 128, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(layer3): Sequential(
(0): BasicBlock(
(conv1): Conv2d(128, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(128, 256, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(layer4): Sequential(
(0): BasicBlock(
(conv1): Conv2d(256, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(avgpool): AdaptiveAvgPool2d(output_size=(1, 1))
(fc): Linear(in_features=512, out_features=1000, bias=True)
)
5.データセットの前処理
まず、今回はデータセットとして、「小倉唯」さんと「日高里菜」さんのデータをスクレイピングで300枚程度入手し、その中からノイズデータを取り除き、100枚に調整します。
その後、学習精度の向上を図るために、Data Augumentation(データ拡張)を行います。
transform = transforms.Compose([
transforms.Grayscale(),
])
今回は、上記のコードで、グレースケール変換を行っています。
その後、学習を行うために、下記コードでデータの調整を行います。
data_transform = transforms.Compose([
transforms.RandomResizedCrop(224),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
transforms.RandomResizedCrop(224):
ここでは、画像をランダムにクロップし、その後で224x224ピクセルにリサイズしています。
224×224は多くの畳み込みニューラルネットワーク(CNN)モデルで標準的な入力サイズになります。
transforms.ToTensor():
ここでは画像をPyTorchのテンソルに変換しており、画像のピクセル値を0~255の整数から0~1の範囲の浮動小数点数にスケーリングしています。
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]):
ここでは、テンソル化された画像データを正規化しています。
正規化は、データセット全体の平均(mean)と標準偏差(std)を用いて各チャネル(R, G, B)の画像データを標準化します。
具体的には、各チャネルから平均を引き、標準偏差で割ります。
今回指定している平均と標準偏差は、ImageNetデータセットに基づく値で、このように処理することでモデルの学習が安定し、収束のスピード上昇が期待できます。
6.転移学習の設定
preModel = models.resnet18(pretrained=True).to(device)
models.resnet18(pretrained=True):
PyTorch の torchvision パッケージから ResNet18 モデルをロードします。
ここで、pretrained=True と指定することで、ImageNet データセットで事前に訓練された重みを持つモデルがロードできます。
事前訓練されたモデルを使用することで、様々な画像認識タスクに対する性能が向上し、ゼロからトレーニングするよりもはるかに少ないデータと時間で優れた結果を得ることができます。
to(device):
このメソッドは、モデルを特定のデバイス(CPUまたはGPU)に移動させます。
device を torch.device("cuda") と設定することでモデルが GPU に配置され、計算が高速化され、逆に、GPUが利用できない場合は torch.device("cpu") と設定します。
7.学習
epoch = 20
model_path = "./model/ogura_hidaka_{:02}.pth"
criterion = torch.nn.CrossEntropyLoss()
optimizer= torch.optim.SGD(preModel.parameters(), lr=0.001, momentum=0.9)
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=2000, gamma=0.1)
# Execute Training
training(preModel, epoch, model_path, optimizer)
今回、学習エポックは20、損失関数はクロスエントロピー誤差、最適化アルゴリズムにSGD(確率的勾配降下法)、スケジューラーで2000ステップごとに学習率が更新、各ステップで学習率を元の値の10%まで減少させる設定を行い、作成したtraining関数で学習を実行します。
8.学習結果
Start Training!
Epoch: 0
Train loss: 0.046526549152425815 acc: 0.5324324324324324 lr: 0.001
Test loss: 0.039304574809613683 acc: 0.638095238095238
Epoch: 1
Train loss: 0.03507144257828996 acc: 0.7216216216216216 lr: 0.001
Test loss: 0.030784142815640996 acc: 0.7523809523809524
Epoch: 2
Train loss: 0.03966745991964598 acc: 0.6891891891891891 lr: 0.001
Test loss: 0.0267647492566279 acc: 0.8297619047619048
Epoch: 3
Train loss: 0.026544940954930072 acc: 0.8108108108108109 lr: 0.001
Test loss: 0.0277804646641016 acc: 0.7797619047619048
Epoch: 4
Train loss: 0.03352582817142074 acc: 0.7567567567567568 lr: 0.001
Test loss: 0.026005355756552447 acc: 0.8107142857142857
Epoch: 5
Train loss: 0.027272234011340785 acc: 0.7972972972972973 lr: 0.001
Test loss: 0.021138081256122817 acc: 0.8571428571428571
Epoch: 6
Train loss: 0.024479682743549346 acc: 0.8540540540540541 lr: 0.001
Test loss: 0.020734517143240996 acc: 0.8630952380952381
Epoch: 7
Train loss: 0.027446120655214463 acc: 0.8108108108108109 lr: 0.001
Test loss: 0.018797582600797925 acc: 0.8726190476190476
Epoch: 8
Train loss: 0.02854586054344435 acc: 0.8054054054054054 lr: 0.001
Test loss: 0.020105837950749057 acc: 0.8738095238095238
Epoch: 9
Train loss: 0.024294248263578157 acc: 0.8405405405405405 lr: 0.001
Test loss: 0.018324440566911585 acc: 0.8678571428571429
Epoch: 10
Train loss: 0.029528083591847807 acc: 0.8 lr: 0.001
Test loss: 0.02433850665000223 acc: 0.8095238095238095
Epoch: 11
Train loss: 0.02546348144879212 acc: 0.8162162162162162 lr: 0.001
Test loss: 0.025875801726111345 acc: 0.7880952380952381
Epoch: 12
Train loss: 0.026212830317986976 acc: 0.8081081081081081 lr: 0.001
Test loss: 0.019320117358473086 acc: 0.8642857142857143
Epoch: 13
Train loss: 0.02252257643519221 acc: 0.8729729729729729 lr: 0.001
Test loss: 0.019582729143578382 acc: 0.8559523809523809
Epoch: 14
Train loss: 0.02093083097322567 acc: 0.8594594594594595 lr: 0.001
Test loss: 0.017621773889377003 acc: 0.8904761904761904
Epoch: 15
Train loss: 0.01695048450618177 acc: 0.8918918918918919 lr: 0.001
Test loss: 0.0167833261324891 acc: 0.8869047619047619
Epoch: 16
Train loss: 0.018449862180529415 acc: 0.8864864864864865 lr: 0.001
Test loss: 0.016269141595278466 acc: 0.8988095238095238
Epoch: 17
Train loss: 0.024224071587259706 acc: 0.8216216216216217 lr: 0.001
Test loss: 0.02018669196182773 acc: 0.8630952380952381
Epoch: 18
Train loss: 0.023289755490180607 acc: 0.8243243243243243 lr: 0.001
Test loss: 0.018441100276651835 acc: 0.8666666666666667
Epoch: 19
Train loss: 0.01834669048721726 acc: 0.8756756756756757 lr: 0.001
Test loss: 0.014308378951890128 acc: 0.9095238095238095
一枚目が、損失値(Loss)、二枚目が学習精度のグラフになっています。
Loss値は基本的に値が小さければ小さいほど、学習精度は当然高ければ高いほど良いものと判断します。
今回はLoss値は、ベスト値がTrain:0.01695048450618177, test:0.014308378951890128で、学習精度はベスト値がTrain:0.8864864864864865, test:0.9095238095238095になっており、どちらも非常に良い学習ができたことが見て取れます。
今回、事前学習モデルとして、VGG16とResNet18のどちらを採用するか迷いましたが、ResNet18を選んだ理由は、今回は二値分類タスクであったのと、VGG16に比べて少ない計算リソースで高速に学習でき、一般に高い精度を達成するためで、また、深いネットワークでの学習が容易なため、効率的に使用できると考えたからです。
9.テスト
今回は事前に下記5枚のデータをテストデータとして用意しました。
いやぁ、お二人ともやっぱりとんでもなく可愛いですね、、(オタクのぼやき)
この画像がうまく分類できるのか、学習モデルを利用して確かめてみましょう!
def main():
model_path = './model/ogura_hidaka_19.pth'
image_folder = './exam/'
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# def classes
classes = ['ogura', 'hidaka']
# Load Model
model = load_model(model_path, device)
# 結果の表示
display_prediction(image_folder, model, device, classes)
上記コードで、精度が一番良かったモデルを読み込ませ、display_prediction関数でテストデータが格納されているパスから画像を取り出し、その画像たちに対してAIにどちらの声優さんなのかを判断させます。
全問正解!!!
しかも、全てのデータに対して90%越えの確信度を持っての正解なので、学習は大成功と言えるでしょう!
10.まとめ
画像認識を用いたプロダクトは、現代社会では非常にホットなものであり、日に日にその社会的地位は高まっています。
そのため、このようなスキルを手にすることは、プログラマーとしても一つ非常に強いスキルとなると考えます。
特に、AI開発エンジニアの場合、自然言語処理と同じくらいにメインテーマの一つでもあるので、その開発に携わりたいと考えている方は、必須のスキルとなってくると考えます。
私自身、実際に仕事で画像認識・自然言語処理どちらの分野でもAIモデルの作成からプロダクトリリースまで経験したことが数多くあるため、その時の知見も今回の検証に大きく活かせたなと感じました。
今回、私がVGG16でなくResNetを使用したように、自分のやるタスクや、データセットの状況、学習環境に応じて使用する事前学習モデルなどの選定が必要になってきます。
逆に、以前記事にもしましたが、MNISTのような手書き数字データの解析などでは、28x28ピクセルのグレースケール画像として提供されており解像度は低く、情報の量も限られているのもあるため、事前学習モデルは使わずに、比較的簡単なニューラルネットワークの学習でも、98%以上の精度が出せたりすることもあるので、作成者側が状況に応じて判断できる能力は必要不可欠だとも考えます。
Kaggleの「digit-recognizerコンペ」にシンプルなNNモデルで挑み高精度を出してみた。
11.参考文献
https://arxiv.org/pdf/1512.03385.pdf
https://arxiv.org/pdf/1512.03385.pdf