Hayami Kaito Web
~/tech

画像分類 AI のしくみ:畳み込みニューラルネットワークから Vision Transformer まで

published

画像分類 AI は、写真を受け取って「猫 66%・犬 24%・鳥 10%」のように、あらかじめ決めた種類(クラス)ごとの確率を返すプログラムです。この記事では、その中で何が計算されているのかを、機械学習に初めて触れる人向けに説明します。

結論を先に書くと、画像分類のモデルは次の 2 つでできています。

  • 特徴を取り出す部分: 小さなフィルターを画像の上で滑らせる「畳み込み」をくり返し、線 → 模様 → 部品 → 物体と、だんだん大きな特徴をつかむ
  • 判定する部分: 取り出した特徴から各クラスの点数を出し、「ソフトマックス」で確率に変える

フィルターの中身は人が決めるのではなく、大量の「画像と正解ラベルの組」から学習して決まります。

説明の途中に出てくる計算例は、Python と NumPy で実際に動かした結果です。

環境

計算例を動かした環境です。

項目バージョン
Python3.13
NumPy2.5

画像分類とは:画像を入れると確率が出てくる関数

画像分類のモデルは、ひとことで言えば関数です。

入力: 画像(数値の配列)
出力: クラスごとの確率(合計 1)
例:   [猫 0.659, 犬 0.242, 鳥 0.099] → いちばん大きい「猫」を答えにする

コンピューターから見た画像は数値の配列

コンピューターにとって、画像は数値が並んだ配列です。

  • グレースケールの画像は「高さ × 幅」の配列で、1 マス(画素)が明るさを表す
  • カラー画像は赤・緑・青の 3 枚の配列を重ねた「高さ × 幅 × 3」の配列(この 3 枚をチャンネルと呼ぶ)
  • 224 × 224 画素のカラー画像なら、224 × 224 × 3 = 150,528 個の数値

モデルの仕事は、この 15 万個の数値から「猫らしさ」を読み取ることです。

全結合のネットワークだけでは難しい理由

いちばん素直な方法は、15 万個の数値をそのまま全結合のニューラルネットワーク(すべての入力がすべてのニューロンにつながる層を重ねたもの)に入れることです。しかし、これには 2 つの問題があります。

  • 重みが多すぎる: 最初の層に 1,000 個のニューロンを置くだけで、重みは 150,528 × 1,000 = 約 1 億 5 千万個になる
  • 位置がずれると別物になる: 猫が写真の左にいても右にいても猫だが、全結合の層では入力の位置ごとに別の重みを持つので、「左の猫」と「右の猫」を別々に覚えなければならない

この 2 つを解決するのが、次に説明する畳み込みです。

畳み込み層:小さなフィルターで特徴を探す

フィルターを滑らせて掛け算と足し算をする

畳み込み(convolution)では、3 × 3 のような小さな数値の表(フィルター、カーネルとも呼ぶ)を用意し、画像の左上から 1 マスずつ滑らせます。各位置で、フィルターと重なった 9 マスどうしを掛け算して合計し、それを出力の 1 マスにします。

例として、真ん中に明るい縦じまがある 6 × 6 の画像(0 が黒、9 が白)に、「左が暗く右が明るい境目」に反応するフィルターをかけてみます。

import numpy as np

# 6x6 の画像(真ん中に明るい縦じま)
image = np.array([[0, 0, 9, 9, 0, 0]] * 6)

# 左が暗く右が明るい縦の境目に反応する 3x3 のフィルター
kernel = np.array([
    [-1, 0, 1],
    [-1, 0, 1],
    [-1, 0, 1],
])

def conv2d(x, k):
    kh, kw = k.shape
    out = np.zeros((x.shape[0] - kh + 1, x.shape[1] - kw + 1), dtype=int)
    for i in range(out.shape[0]):
        for j in range(out.shape[1]):
            out[i, j] = np.sum(x[i:i + kh, j:j + kw] * k)
    return out

print(conv2d(image, kernel))

実行結果:

[[ 27  27 -27 -27]
 [ 27  27 -27 -27]
 [ 27  27 -27 -27]
 [ 27  27 -27 -27]]

しまの左の境目(暗 → 明)では大きな正の値、右の境目(明 → 暗)では負の値が出ました。フィルターは「この模様がここにあるか」を調べる検出器で、出力は特徴がどこにあるかを示す地図になります。この出力を特徴マップと呼びます。

畳み込みには、全結合の 2 つの問題に対する答えが含まれています。

  • 重みが少ない: 1 枚のフィルターの重みは 3 × 3 = 9 個だけで、画像のどこでも同じ重みを使い回す。カラー画像(3 チャンネル)に 3 × 3 のフィルターを 64 枚かけても、重みとバイアスは 3 × 3 × 3 × 64 + 64 = 1,792 個
  • 位置がずれても同じように反応する: 同じフィルターを全体に滑らせるので、模様が左にあっても右にあっても同じ値が出る(出る場所がずれるだけ)

実際のモデルでは 1 つの層に数十〜数百枚のフィルターを並べ、それぞれが別の模様(縦線・横線・斜め・色の変わり目など)を探します。

出力の大きさを決めるストライドとパディング

上の例では 6 × 6 の画像が 4 × 4 に縮みました。3 × 3 のフィルターは端の 1 マスにはみ出して置けないためです。出力の大きさは次の式で決まります。

出力の幅 = (入力の幅 + 2 × パディング − フィルターの幅) ÷ ストライド + 1
例: (6 + 2 × 0 − 3) ÷ 1 + 1 = 4
  • パディング: 画像の周りを 0 で囲んで、出力が縮まないようにする(3 × 3 のフィルターならパディング 1 で同じ大きさになる)
  • ストライド: フィルターを何マスずつ動かすか。2 にすると出力の幅はおよそ半分になる

活性化関数 ReLU:いらない反応を 0 にする

畳み込みの出力は、次に活性化関数を通します。画像分類でよく使われるのは ReLU で、負の値を 0 にし、正の値はそのまま通します。

def relu(x):
    return np.maximum(x, 0)

print(relu(conv2d(image, kernel)))

実行結果:

[[27 27  0  0]
 [27 27  0  0]
 [27 27  0  0]
 [27 27  0  0]]

「暗 → 明」の境目だけが残りました。もう 1 つ大事な役割として、活性化関数がないと層を何枚重ねても全体が 1 つの掛け算と足し算(線形の計算)にまとまってしまい、複雑な形を表せません。ReLU のような曲がった関数をはさむことで、層を重ねる意味が生まれます。

プーリング層:特徴マップを小さくまとめる

プーリングは、特徴マップを小さな区画に分けて、区画ごとに 1 つの値にまとめる処理です。よく使われる最大プーリングでは、2 × 2 の区画ごとに最大値を取ります。

def max_pool(x, size=2):
    h, w = x.shape[0] // size, x.shape[1] // size
    return x[:h * size, :w * size].reshape(h, size, w, size).max(axis=(1, 3))

print(max_pool(relu(conv2d(image, kernel))))

実行結果:

[[27  0]
 [27  0]]

4 × 4 が 2 × 2 になりました。プーリングには次の効果があります。

  • 縦横が半分になるので、後ろの層の計算が 4 分の 1 になる
  • 区画の中で特徴が 1 マスずれても同じ値になるので、小さな位置のずれに強くなる
  • 縮んだ特徴マップに次の 3 × 3 のフィルターをかけると、元の画像ではより広い範囲を見ていることになる

層を重ねると大きな特徴がつかめる

「畳み込み → ReLU → プーリング」を何段も重ねると、層ごとに見ている範囲が広がります。

層とらえる特徴の例
浅い層線・境目・色の変わり目
中間の層線の組み合わせ(角・円・しま模様・毛並み)
深い層物体の部品(目・耳・車輪)や物体そのもの

この「線 → 模様 → 部品 → 物体」という積み上げが、畳み込みニューラルネットワーク(CNN、Convolutional Neural Network)の考え方です。

全結合層とソフトマックス:特徴から確率を出す

全結合層でクラスごとの点数を出す

特徴を取り出したあとは、特徴マップを 1 列に並べ(または平均を取り)、全結合層に入れてクラスごとの点数を出します。この点数はロジット(logit)と呼ばれ、どんな実数にもなります。ここまで縮めた特徴なら数も少ないので、全結合層の重みも現実的な数で済みます。

ソフトマックスで点数を確率に変える

点数のままでは「どれくらい自信があるか」が分かりにくいので、ソフトマックス関数で確率に変えます。

クラス i の確率 = exp(点数 i) ÷ (すべてのクラスの exp(点数) の合計)

exp を通すので値はすべて正になり、合計で割るので足すと 1 になります。点数の差が大きいほど、確率の差も大きくなります。

def softmax(z):
    e = np.exp(z - np.max(z))  # 大きな数で exp があふれないよう、最大値を引いてから計算する
    return e / e.sum()

logits = np.array([2.0, 1.0, 0.1])  # 猫・犬・鳥の点数
print(np.round(softmax(logits), 3))

実行結果:

[0.659 0.242 0.099]

これが冒頭の「猫 66%・犬 24%・鳥 10%」です。

学習のしくみ:フィルターの値はどう決まるのか

ここまでの説明では、縦の境目を探すフィルターを人が作りました。実際のモデルでは、フィルターも全結合層の重みも、最初はでたらめな値から始めて、データから自動で調整します。これが学習です。

損失関数で「どれだけ間違えたか」を数値にする

学習には、画像と正解ラベルの組を大量に用意します。モデルの答えが正解からどれだけ外れているかを 1 つの数値にしたものを損失(loss)と呼び、画像分類では交差エントロピー誤差がよく使われます。

損失 = −log(正解クラスの確率)

正解のクラスに高い確率を付けていれば損失は 0 に近く、低い確率しか付けていなければ大きくなります。上の [0.659, 0.242, 0.099] で確かめると次のとおりです。

正解正解クラスの確率損失
猫0.6590.417
鳥0.0992.317

「鳥なのに 10% しか付けなかった」ほうが、ずっと大きな損失になります。

勾配降下法で損失が下がる向きに重みを動かす

学習の目標は、たくさんの画像での損失の平均をできるだけ小さくすることです。そのために、各重みについて「その重みを少し増やすと損失がどれだけ増えるか」(勾配)を求め、損失が下がる向きに少しだけ動かします。

新しい重み = 重み − 学習率 × 勾配

学習率は 1 回に動かす幅で、大きすぎると行き過ぎて損失が下がらず、小さすぎると学習が進みません。これを何万回もくり返すのが勾配降下法です。実際には画像を 1 枚ずつではなく、数十〜数百枚のまとまり(ミニバッチ)ごとに勾配の平均を取って更新します。

ソフトマックスと交差エントロピー誤差を組み合わせると、点数についての勾配は「確率 − 正解(正解クラスだけ 1、ほかは 0)」というシンプルな形になります。正解が猫の例で、点数そのものを学習率 1.0 で 1 回動かしてみます。

target = np.array([1.0, 0.0, 0.0])  # 正解は猫
p = softmax(logits)
grad = p - target
new_logits = logits - 1.0 * grad
q = softmax(new_logits)
print(np.round(grad, 3), np.round(q, 3), round(-np.log(q[0]), 3))

実行結果:

[-0.341  0.242  0.099] [0.768 0.158 0.074] 0.264

猫の点数が上がり、犬と鳥の点数が下がって、猫の確率は 0.659 → 0.768、損失は 0.417 → 0.264 に下がりました。

この勾配の式が正しいことは、点数をほんの少し増やしたときと減らしたときの損失の差(数値微分)と比べて確かめました。どちらも [-0.341, 0.242, 0.099] になります。

誤差逆伝播法で全部の重みの勾配を一度に求める

実際のモデルには数百万〜数億の重みがあり、1 つずつ動かして損失の変化を調べていては時間がかかりすぎます。そこで、出力側から入力側へ向かって、微分の連鎖律(合成関数の微分)を使って勾配を順に計算していきます。これが誤差逆伝播法(バックプロパゲーション)です。

順伝播:   画像 → 畳み込み → ReLU → プーリング → … → 全結合 → ソフトマックス → 損失
逆伝播:   画像 ← 畳み込み ← ReLU ← プーリング ← … ← 全結合 ← ソフトマックス ← 損失の勾配

順伝播 1 回とほぼ同じ手間で、すべての重みの勾配がまとめて求まります。PyTorch や TensorFlow などのライブラリは、この計算を自動で行ってくれます。

学習用と検証用のデータを分ける

学習用の画像だけで成績を測ると、モデルが画像を丸暗記しただけでも高い点が出てしまいます。これを過学習と呼びます。そのため、学習には使わない画像(検証用・テスト用)を別に取っておき、初めて見る画像でどれだけ当たるかで性能を測ります。

過学習を防ぐ方法として、学習中に画像をランダムに反転・切り抜き・色を変えてデータを水増しするデータ拡張などがよく使われます。

CNN から Vision Transformer までの流れ

画像分類のモデルは、ここまで説明した部品を土台に発展してきました。代表的なモデルを年代順に並べます。

年モデルポイント
1998LeNet-5畳み込み・プーリング・全結合という CNN の基本の形。手書き数字の認識に使われた
2012AlexNetReLU と GPU による学習で、大規模な画像分類のコンテスト ILSVRC で 2 位以下を大きく引き離して優勝
2014VGG3 × 3 の小さなフィルターだけを深く(16〜19 層)重ねる単純な構成
2015ResNet「残差接続」で 100 層を超える深いネットワークの学習を可能にした
2020Vision Transformer(ViT)畳み込みを使わず、画像を小さなパッチに区切って Transformer で処理する

AlexNet:深層学習のブームのきっかけ

ImageNet は約 1,000 クラス・100 万枚を超える画像を集めたデータセットで、これを使ったコンテスト ILSVRC が毎年開かれていました。2012 年、CNN を使った AlexNet は上位 5 つの候補に正解が入らなかった割合(top-5 エラー率)で 15.3% を記録し、2 位の 26.2% を大きく下回りました。ここから画像認識の主流は CNN に移ります。

ResNet:残差接続で層を深くする

層を深くすれば表現力は上がるはずですが、単純に重ねると逆伝播で勾配がうまく伝わらず、かえって学習が難しくなります。ResNet は、何層かの処理を飛び越えて入力をそのまま出力に足す残差接続(スキップ接続)を入れました。

出力 = 入力 + 何層かの処理(入力)

各ブロックは「入力からの差分」だけを学べばよく、勾配も足し算の道を通って入力側まで届きやすくなります。ResNet は 152 層のモデルまで学習し、複数のモデルを組み合わせて ImageNet で top-5 エラー率 3.57% を達成して 2015 年の ILSVRC で優勝しました。残差接続は、このあと紹介する ViT を含め、今の多くのモデルで使われています。

Vision Transformer:画像を単語のように扱う

Vision Transformer(ViT)は、文章を扱う Transformer を画像に持ち込んだモデルです。

  1. 画像を 16 × 16 画素の小さなパッチに区切る(224 × 224 の画像なら 14 × 14 = 196 枚)
  2. 各パッチを 1 列に並べてベクトルにし、文章の「単語」のように並べる
  3. Transformer の自己注意(self-attention)で、すべてのパッチどうしの関係を計算する
  4. 先頭に置いた分類用の要素の出力から、クラスごとの点数を出す

CNN は小さなフィルターで近くの画素から順に見ていくのに対し、ViT は最初の層から画像全体のパッチどうしの関係を見られます。そのかわり「近くの画素ほど関係が深い」という画像の性質を最初から持っていないため、少ないデータでは CNN に及ばず、大量のデータで事前に学習したときに力を発揮します。

論文のタイトル「An Image is Worth 16x16 Words(画像は 16 × 16 の単語に値する)」は、この「パッチを単語として扱う」考え方を表しています。

ブラウザで学習の様子を見る

このサイトの ニューラルネットワークの学習の可視化 では、全結合のニューラルネットワークが誤差逆伝播法とミニバッチ学習で 2 種類の点を分けていく様子を、ブラウザで動かしながら見られます。

画像ではなく平面上の点を分ける小さな問題ですが、この記事で説明した次のことを手で確かめられます。

  • 学習率を大きくしすぎると学習が進まなくなる
  • 活性化関数を「線形」にすると、層を増やしても曲がった境界が作れない
  • 学習用とテスト用の誤差の差から、過学習の様子が分かる

まとめ

  • 画像は数値の配列で、画像分類のモデルはそこからクラスごとの確率を出す関数
  • 畳み込みは小さなフィルターを画像の上で滑らせて模様を探す処理で、重みが少なく、位置がずれても同じように反応する
  • 「畳み込み → ReLU → プーリング」を重ねると、線から物体まで段階的に大きな特徴がつかめる
  • 最後は全結合層で点数を出し、ソフトマックスで確率に変える
  • フィルターや重みは、交差エントロピー誤差を勾配降下法で小さくするように学習し、勾配は誤差逆伝播法でまとめて求める
  • CNN は AlexNet・VGG・ResNet と深くなり、2020 年からは画像をパッチに区切って Transformer で処理する ViT も使われている

参考