🫥

Google Colab + PyTorch + ResNet で擬態の本気度を測定する

2024/11/21に公開

こんにちは!アルダグラムでエンジニアをしている内倉です。

今年も気がつけば11月、もう1年が終わろうとしていますね。
2024年も様々なニュースがありましたが、その中でも個人的に特に印象深かったのが、ウミウシに擬態するゴカイ「ケショウシリス」の発見です。
ゴカイはミミズの仲間、ウミウシは貝の仲間と、全然親戚感ないのに、研究者ですら最初はウミウシだと思っていたというのですから、見事というほかありません。

今回は、そんなケショウシリスの擬態の本気度を測ってみたいと思います。

前提

私自身は、生物学的な知識も、機械学習的な知識も全然なく、可能な限り手軽に実行したいので、以下のような組み合わせを利用することにしました。

  • Google Colab :環境設定の手間なく高性能なGPUを利用できる。
  • ResNet:ゼロから学習しなくてもそのまま利用できる、学習済みの高性能な画像認識モデル。さらに進化した ResNet-RS というモデルがあるみたいですが、より手軽に使えそうだったので、今回はこちらで。
  • PyTorch:直感的で、初心者でも扱いやすい機械学習ライブラリ。 ResNet もサポートしていて簡単に使える。

では、やっていきます💪

1. 画像の準備

なるべくシンプルな背景に単体で写っている、ノイズの少ない画像を、20〜30 枚くらい用意します。

今回は、適当な画像がそんなに見つからなかったので、背景を消したりトリミングしたりしたものも含めました。

ケショウシリスの画像は、Webニュースに掲載されていたもの(3枚)しか用意できませんでしたが、その場合も特徴の平均を出して比較するほうが良いようです。

!!!注意!!!
ケショウシリスはポップな見た目なので大丈夫ですが、ふつうのゴカイを検索したら
とんでもない画像が出てきてしまいます🙅🙅🙅 念のため。

用意した画像を Google ドライブ に格納します。

以下のような、フォルダ構成にしました。

test_images/
└── coryphellina_exoptata/ # アデヤカミノウミウシの画像フォルダ
|   └── 0/  # 1つのクラスとして設定
|       ├── image1.jpg
|       ├── image2.jpg
|       └── ...
├── syllis/ # ケショウシリスの画像フォルダ
    └── 0/
        ├── image1.jpg
        ├── image2.jpg
        └── ...

2. Google Colab の準備

つづいて、Google Colab を準備していきます。

  1. Google Colabにアクセス(1. で画像を格納したのと同じ Googleアカウントでログイン)し、右上の「New Notebook」から新しいノートブックを作成します。

  2. ランタイムのタイプを変更 ※ 任意

    デフォルトのランタイムはCPUなので、そのままだと実行時間がかかってしまう場合は、GPUに変更します。が、今回はデータが少ないので、変更しなくてもOKです。

    GPUを使いたい場合は、メニューから「ランタイム」>「ランタイムのタイプを変更」> 「ハードウェアアクセラレータ」の項目で「T4 GPU」を選択し、保存します。

  3. Google ドライブをマウントする

    さきほど Google ドライブ上に用意した画像を参照できるよう、マウントします。

    ノートブックのセルに以下のコードを書いて、実行します。

    from google.colab import drive
    drive.mount('/content/drive')
    

    初回実行時、以下のようなモーダルが表示されるので、「Google ドライブに接続」を押下し、アカウントの選択→アクセスを許可する情報の選択と進んでいきます。

    「Google ドライブのすべてのファイルの表示〜」のみ選択すればよいのかな?と思いましたが、 Error: credential propagation was unsuccessful とエラーが出てうまくいきませんでした。表示された、すべての情報を選択するとうまくいきました。

準備はこれだけです!!

3. アデヤカミノウミウシの特徴を抽出する

コードは以下のようになりました。

import os
from torchvision import datasets, transforms, models
from torch.utils.data import DataLoader
import torch
from PIL import Image # 後で使います

# カレントディレクトリを変更
os.chdir('/content/drive/My Drive/test_images')

# 画像をモデルに入力するためにリサイズ・正規化する ※1
transform = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406],
                         std=[0.229, 0.224, 0.225]),
])

# アデヤカミノウミウシ画像データセットを準備する ※2
adelocera_dataset = datasets.ImageFolder(
    'coryphellina_exoptata',
    transform=transform
)
dataloader = DataLoader(adelocera_dataset, batch_size=5, shuffle=False)

# ResNetモデルの準備 ※3
model = models.resnet50(pretrained=True)
model.fc = torch.nn.Identity()
model.eval()

# ResNetモデルで、アデヤカミノウミウシの特徴を抽出する ※4
features_list = []
with torch.no_grad():
    for images, _ in dataloader:
        features = model(images)
        features_list.append(features)

# 平均特徴ベクトルを計算
adelocera_features_avg = torch.mean(torch.cat(features_list), dim=0)

順番に見ていきます。

画像をモデルに入力するためにリサイズ・正規化する ※1

# 画像をモデルに入力するためにリサイズ・正規化する ※1
transform = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406],
                         std=[0.229, 0.224, 0.225]),
])

モデルに画像を入力するには、まず画像を適切な形式に整える必要があります。ResNetは、学習時に ImageNet というデータセットを使っており、入力画像が以下のようになっていることを期待しています。

  • 画像サイズ: 224×224ピクセル

    transforms.Resize((224, 224))

  • ピクセル値のスケール: ImageNetのデータ分布に基づいた正規化

    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])

平たく言うと、今回の入力データを ResNet が事前学習中に慣れ親しんだ入力に合わせることで、100% の力を発揮できるようにしています。

アデヤカミノウミウシ画像データセットを準備する ※2

# アデヤカミノウミウシ画像データセットを準備する ※2
adelocera_dataset = datasets.ImageFolder(
    'coryphellina_exoptata',
    transform=transform
)
dataloader = DataLoader(adelocera_dataset, batch_size=5, shuffle=False)

画像データを ResNet に投入するには、まず画像をデータセットとして準備し、そのデータセットをモデルに渡せる形式に変換する必要があります。

datasets.ImageFolderを使うと、フォルダ構造に基づいて画像を自動で分類し、データセットを簡単に作成できます。

本来は、第一引数で指定したフォルダ下のフォルダ名が「クラス(ラベル)」として扱われ、それぞれの画像を分類するために使われます。今回は分類が目的ではないため、以下のように適当なフォルダ( 0 )を1つ作り、画像をまとめて格納しています。

test_images/ # current dir
└── coryphellina_exoptata/ # アデヤカミノウミウシの画像フォルダ
|   └── 0/  # 1つのクラスとして設定
|       ├── image1.jpg
|       ├── image2.jpg
|       └── ...

dataset のままでは、まだ効率的にデータを処理することができません。

そこで、 DataLoader を使って、並列処理を行ったり、ランダムに順序を変更したりします。今回は、画像が25枚で、順序のランダム化は不要なので DataLoader(adelocera_dataset, batch_size=5, shuffle=False) としました。 なお、batch_size は画像数を割り切れる数を指定する必要があります。

ResNet モデルの準備 ※3

# ResNetモデルで、アデヤカミノウミウシの特徴を抽出する ※3
model = models.resnet50(pretrained=True)
model.fc = torch.nn.Identity()
model.eval()
  • pretrained=True : ImageNet で学習済みの重みを利用する指定。
  • model.fc = torch.nn.Identity() : ResNet の最後の分類層を無効にして、特徴ベクトルだけを出力します。
  • model.eval() : 推論モードに切り替え。推論モードは「新しいデータに対して予測を行うモード」です。今回は、特徴を抽出したいだけなので、こちら。

ResNet モデルで、アデヤカミノウミウシの特徴を抽出する ※4

# ResNetモデルで、アデヤカミノウミウシの特徴を抽出する ※4
features_list = []
with torch.no_grad():
    for images, _ in dataloader:
        features = model(images)
        features_list.append(features)

# 平均特徴ベクトルを計算
adelocera_features_avg = torch.mean(torch.stack(features_list), dim=0)

この部分では、いよいよ ResNet に、先ほど準備したアデヤカミノウミウシのデータを投入し、特徴ベクトルを抽出します。

特徴ベクトルは、画像の形状・模様・色合いなどの情報を数値化したもので、モデルが画像を理解するための「抽象的な特徴」です。

最後に、複数の画像から得られた特徴ベクトルを平均化し、「アデヤカミノウミウシ全体を代表する特徴」を作成します。これを元に、後でケショウシリスとの類似性を評価します。

4. ケショウシリス(とそのライバルたち)の特徴を抽出する

ケショウシリスのみのデータだと、結果の検証がしにくいと思い、ライバルの画像も追加で用意することにしました。

ライバル1

近所のスーパーで、ミノウミウシみのあるブドウを見つけたので、こちらをライバル1とします。

こちらを、さらに雰囲気が出るようにカットした画像を使用します。

ライバル2

そして、こちらが本命。我が家の巨匠(小3)による、制作時間2時間超えの大作、アデタロウです。

本家と比べると若干不気味ですが、色味はかなりいい線なのでは?

特徴ベクトルの抽出

ライバル2種の画像と、 3. で特徴ベクトルを出すのに利用したのとは別のアデヤカミノウミウシの画像を test_images フォルダに格納しておきます。

それぞれ、特徴ベクトルを抽出します。

とりあえず、1枚の画像から特徴ベクトル抽出するメソッドを作って

def extract_features(image_path, model, transform):
		# 画像を読み込んで前処理を適用
    image = Image.open(image_path)  # 画像を開く
    image_tensor = transform(image).unsqueeze(0)  # 前処理を適用してバッチ次元を追加

    # 特徴ベクトルを抽出
    with torch.no_grad():  # 勾配計算を無効化
        features = model(image_tensor).squeeze(0)  # 特徴ベクトルを取得

    return features

あとは、実行していくだけ。ケショウシリスの画像は複数枚あるので、アデヤカミノウミウシのときと同じように抽出します。

# ケショウシリスの平均特徴ベクトルを抽出
syllis_dataset = datasets.ImageFolder(
    'syllis',
    transform=transform
)

syllis_dataloader = DataLoader(syllis_dataset, batch_size=3, shuffle=False)

syllis_features_list = []
with torch.no_grad():
    for images, _ in syllis_dataloader:
        features = model(images)
        syllis_features_list.append(features)

syllis_features_avg = torch.mean(torch.cat(syllis_features_list, dim=0), dim=0)

# ほんものの平均特徴ベクトル抽出
honmono_features = extract_features('honmono.jpg', model, transform)
# ライバルも同様に抽出
...

5. 類似度の比較

すべてのデータが出たので、比較していきます。

今まで出してきた syllis_features_avghonmono_features は、画像の形状や色合いなどを表した、2048個の数値の集まりです。これを効率よく比較するために、コサイン類似度を計算します。

コサイン類似度の特徴は以下のようなかんじなので、今回の比較にはぴったりです。

  • ベクトルの長さの違いを無視して、重要な「方向性」のみを評価できる
  • 高次元データ(多くの特徴を持つデータ)でも安定して測定ができる
  • 結果が、-1 から 1 の範囲に収まるので、直感的に「どのくらい似てるか」わかりやすい(1 に近いほど似ている)
# 2つのベクトル間のコサイン類似度を計算
def calculate_cosine_similarity(vec1, vec2):
    # ドット積
    dot_product = torch.dot(vec1, vec2)
    # ノルム(ベクトルの大きさ)
    norm_a = torch.norm(vec1)
    norm_b = torch.norm(vec2)
    # コサイン類似度の計算
    cosine_sim = dot_product / (norm_a * norm_b)
    return cosine_sim

このメソッドを使用して、それぞれ結果を出力していきます。

similarity = calculate_cosine_similarity(adelocera_features_avg, syllis_features_avg)

# 結果を出力
print(f"アデヤカミノウミウシとケショウシリスの類似度: {similarity.item():.4f}")

すべてのデータの計測結果が、こちらです。

本物 ケショウシリス ライバル1(ブドウ) ライバル2(アデタロウ)
0.9421 0.8150 0.5402 0.6947

アデタロウ、惜しかったね!

アデタロウはとても繊細な作りのため、動かすことができずにそのまま撮影したのですが、背景等をもう少し海っぽくしたら更にいい数値が出たかもしれません。

でもまぁ、だいたい見た感じの印象そのままの数値かな、と思います!

やはり、ニセモノの中では、ケショウシリスが圧倒的に似ていますね。さすがです😳

6. おわりに

今回は、種の全体的な特徴を比較しましたが、形状、色等の項目ごとに比較をしたり

擬態に関係なく、被捕食者の特徴を調べて、捕食者が好む被捕食者の特徴が何なのか考察したりしても面白そうですね。

(実際には、味とか捕まえやすさとか、画像で測れない要素があるから、むずかしいかもしれませんが!)

もっとアルダグラムエンジニア組織を知りたい人、ぜひ下記の情報をチェックしてみてください!

アルダグラム Tech Blog

Discussion