画像分類 AI のしくみ:畳み込みニューラルネットワークから Vision Transformer まで
画像分類 AI は、写真を受け取って「猫 66%・犬 24%・鳥 10%」のように、あらかじめ決めた種類(クラス)ごとの確率を返すプログラムです。この記事では、その中で何が計算されているのかを、機械学習に初めて触れる人向けに説明します。
結論を先に書くと、画像分類のモデルは次の 2 つでできています。
- 特徴を取り出す部分: 小さなフィルターを画像の上で滑らせる「畳み込み」をくり返し、線 → 模様 → 部品 → 物体と、だんだん大きな特徴をつかむ
- 判定する部分: 取り出した特徴から各クラスの点数を出し、「ソフトマックス」で確率に変える
フィルターの中身は人が決めるのではなく、大量の「画像と正解ラベルの組」から学習して決まります。
説明の途中に出てくる計算例は、Python と NumPy で実際に動かした結果です。
環境
計算例を動かした環境です。
| 項目 | バージョン |
|---|---|
| Python | 3.13 |
| NumPy | 2.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.659 | 0.417 |
| 鳥 | 0.099 | 2.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 までの流れ
画像分類のモデルは、ここまで説明した部品を土台に発展してきました。代表的なモデルを年代順に並べます。
| 年 | モデル | ポイント |
|---|---|---|
| 1998 | LeNet-5 | 畳み込み・プーリング・全結合という CNN の基本の形。手書き数字の認識に使われた |
| 2012 | AlexNet | ReLU と GPU による学習で、大規模な画像分類のコンテスト ILSVRC で 2 位以下を大きく引き離して優勝 |
| 2014 | VGG | 3 × 3 の小さなフィルターだけを深く(16〜19 層)重ねる単純な構成 |
| 2015 | ResNet | 「残差接続」で 100 層を超える深いネットワークの学習を可能にした |
| 2020 | Vision 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 を画像に持ち込んだモデルです。
- 画像を 16 × 16 画素の小さなパッチに区切る(224 × 224 の画像なら 14 × 14 = 196 枚)
- 各パッチを 1 列に並べてベクトルにし、文章の「単語」のように並べる
- Transformer の自己注意(self-attention)で、すべてのパッチどうしの関係を計算する
- 先頭に置いた分類用の要素の出力から、クラスごとの点数を出す
CNN は小さなフィルターで近くの画素から順に見ていくのに対し、ViT は最初の層から画像全体のパッチどうしの関係を見られます。そのかわり「近くの画素ほど関係が深い」という画像の性質を最初から持っていないため、少ないデータでは CNN に及ばず、大量のデータで事前に学習したときに力を発揮します。
論文のタイトル「An Image is Worth 16x16 Words(画像は 16 × 16 の単語に値する)」は、この「パッチを単語として扱う」考え方を表しています。
ブラウザで学習の様子を見る
このサイトの ニューラルネットワークの学習の可視化 では、全結合のニューラルネットワークが誤差逆伝播法とミニバッチ学習で 2 種類の点を分けていく様子を、ブラウザで動かしながら見られます。
画像ではなく平面上の点を分ける小さな問題ですが、この記事で説明した次のことを手で確かめられます。
- 学習率を大きくしすぎると学習が進まなくなる
- 活性化関数を「線形」にすると、層を増やしても曲がった境界が作れない
- 学習用とテスト用の誤差の差から、過学習の様子が分かる
まとめ
- 画像は数値の配列で、画像分類のモデルはそこからクラスごとの確率を出す関数
- 畳み込みは小さなフィルターを画像の上で滑らせて模様を探す処理で、重みが少なく、位置がずれても同じように反応する
- 「畳み込み → ReLU → プーリング」を重ねると、線から物体まで段階的に大きな特徴がつかめる
- 最後は全結合層で点数を出し、ソフトマックスで確率に変える
- フィルターや重みは、交差エントロピー誤差を勾配降下法で小さくするように学習し、勾配は誤差逆伝播法でまとめて求める
- CNN は AlexNet・VGG・ResNet と深くなり、2020 年からは画像をパッチに区切って Transformer で処理する ViT も使われている
参考
- Y. LeCun et al., "Gradient-Based Learning Applied to Document Recognition", Proceedings of the IEEE, 1998
- A. Krizhevsky, I. Sutskever, G. E. Hinton, "ImageNet Classification with Deep Convolutional Neural Networks", NeurIPS 2012
- K. Simonyan, A. Zisserman, "Very Deep Convolutional Networks for Large-Scale Image Recognition", 2014
- K. He et al., "Deep Residual Learning for Image Recognition", 2015
- A. Dosovitskiy et al., "An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale", ICLR 2021