JA EN
体系CNN・画像認識
·無料·10分で読めます

画像分類を1から理解する — 畳み込みという発明

写真をそのまま全結合層に流すと、パラメータが1億個を超え、しかも猫が数画素ずれただけで別物になります。畳み込みはこの2つを「小さな窓を滑らせる」という1つのアイデアで同時に解きました。カーネル・ストライド・パディング・プーリングを式と手触りで押さえ、最後はsoftmaxで確率にするところまで。

対象imageタスクclassification

写真を、そのまま全結合に流してみる

前の記事で組み立てたニューラルネットワークは、入力ベクトルに重み行列を掛けて非線形関数に通す装置でした。ならば写真もベクトルにして流せばよさそうです。224×224画素のカラー写真なら、画素値を一列に並べれば 224×224×3=150,528224 \times 224 \times 3 = 150{,}528 次元のベクトルになります。

ところがこの素直な発想は、実装の工夫では逃げられない理由で2つとも破綻します。

破綻1: パラメータが爆発する

隠れ層を1000ユニットだけ置いてみます。1層目の重み行列は 1000×150,528、つまり1億5千万個のパラメータです。1層目だけで、です。32ビット浮動小数点なら重みだけで約600メガバイト、学習中はこれと同じ大きさの勾配と最適化アルゴリズムの内部状態がさらに積み上がります。自由に動く数がこれだけあれば、訓練画像を丸暗記する余力も持ってしまいます。

破綻2: 数画素ずらすと、別の入力になる

もう1つのほうが本質的です。猫が左上に写った写真で学習したモデルに、同じ猫が右下に写った写真を見せてみます。全結合層にとって、この2枚は共通点がまったくない別のベクトルです。左上の画素を担当する重みと右下の画素を担当する重みは、互いに無関係な独立したパラメータだからです。「猫は画面のどこにいても猫」という当たり前を、データで教え込むしかありません。

畳み込みが解いた2つの問題

畳み込み(convolution)は、この2つを1つのアイデアで同時に解きました。小さな窓を1枚だけ用意して、画像の上を滑らせる。 それだけです。

窓の大きさは例えば3×3で、中には9個の重みが入っています。窓を画像の左上に当て、重なった9画素と9個の重みを掛けて足す——前の記事でやった wxw^\top x そのものです。1画素右へずらして、また掛けて足す。端まで繰り返すと、数値が2次元に並んだものができます。

この操作には2つの制約が埋め込まれています。

局所性(local connectivity) — 各出力は入力全体ではなく3×3の近傍だけを見ます。画像では隣り合う画素が強く関係し、遠く離れた画素はほとんど関係しない。その事実を構造そのものに書き込んだわけです。入力が224×224だろうが4000×3000だろうが重みは9個のままで、パラメータ爆発が消えます。

重み共有(weight sharing) — 同じ窓を全位置で使い回します。左上を見るときも右下を見るときも、まったく同じ9個の重みです。だから「左上で覚えた縦線の検出」はそのまま右下でも縦線を検出する。これを平行移動同変性と呼びます。

単なる計算量削減の工夫ではなく、「画像とはこういうものだ」という事前知識をアーキテクチャに埋め込んだ——そこが発明でした。

(fk)(i,j)=u=0K1v=0K1f(i+u, j+v)k(u,v)(f * k)(i,j) = \sum_{u=0}^{K-1}\sum_{v=0}^{K-1} f(i+u,\ j+v)\, k(u,v)
(1)

ff は入力画像、kkK×KK \times Kカーネル(窓の中身。フィルタとも呼びます)、(i,j)(i,j) は出力の位置です。この式が言っているのは要するに、窓の中の画素と重みの内積を、位置ごとに計算し直しているだけ。厳密にはこれは相互相関で、数学の畳み込みはカーネルを反転させますが、重みは学習で決まる以上、違いは呼び名だけです。

カーネル・ストライド・パディング

実装で決めるのは3つの数です。カーネルサイズ KK は窓の一辺で、3が定番。3×3を2枚重ねると5×5相当の範囲が見えるうえ、パラメータは 9+9=18 個で 25 個より少ないからです。ストライド SS は窓を何画素ずつずらすか。2にすると出力は縦横おおよそ半分に縮みます。パディング PP は入力の周囲を0で何画素ぶん囲むか。K=3K=3P=1P=1 なら出力が入力と同じ大きさに保たれます。

出力の一辺はこの式で決まります。

Hout=H+2PKS+1H_{\text{out}} = \left\lfloor \frac{H + 2P - K}{S} \right\rfloor + 1
(2)

HH は入力の一辺、外側の記号は切り捨て。つまりこの式は、パディングで広がった入力の上に、KK 画素幅の窓を SS 画素ずつずらしながら何回置けるかを数えているだけです。2P2P は周囲に足した幅、KK を引くのは窓が端からはみ出さないようにするため、SS で割るのは残りの距離を「何歩ぶん」に直す操作、最後の +1+1 は一歩も動かないうちに置ける最初の1回です。次元が合わないというエラーの大半は、ここの計算違いです。

チャネルの話も要ります。カラー画像は入力チャネルが3(RGB)なので、カーネルも奥行き3を持ち、27個の重みと1個のバイアスで出力を1枚作ります。この1枚が特徴マップ——「この層が探している1種類の模様が、どこにどれだけあるか」の地図です。1種類では足りないので独立したカーネルを64枚や256枚並べます。層のパラメータ数は K×K×Cin×Cout+CoutK \times K \times C_{\text{in}} \times C_{\text{out}} + C_{\text{out}}CinC_{\text{in}} は入力チャネル数、CoutC_{\text{out}} はカーネルの枚数)で、3×33\times3・入力3・出力64なら1,792個。全結合層の1億5千万と5桁近く違います。

ただしパラメータが減ることと計算が軽いことは別で、畳み込みは実装上大きな行列積1回に落とされます(行列積のコスト)。

プーリング: 少しのずれを吸収する

畳み込みが持つのは平行移動「同変」性でした。猫が右にずれれば特徴マップの反応も右にずれる。しかし最終的に欲しいのは「猫がいる」という位置に依らない判定、つまり平行移動不変性です。

その距離を詰めるのがプーリングです。2×2の窓ごとに最大値だけを取る(マックスプーリング)と縦横が半分になり、窓の中での小さな位置ずれは出力に現れなくなります。同時に、解像度が落ちることで次の層の3×3の窓が元画像の上ではより広い範囲を覆います。これが受容野の拡大です。近年はプーリングを置かず、ストライド2の畳み込みに縮小を兼ねさせる設計も多く見られます。

積み重ねると、エッジが物体になる

層を重ねると、各層が反応する対象が階層的に育ちます。入り口の層は明暗の境目、つまりエッジや特定方向の線に反応します。数画素しか見えていないので、それ以上のものは見ようがありません。次の層は前の層が作った特徴マップを入力にして同じことをします。「縦線の地図」と「横線の地図」を同時に見て、両方が同じ場所で強ければ、一定間隔で並んでいれば格子模様。さらに奥では目のような部品、顔や車体のような物体の一部へ。この階層は人が設計したものではなく、この構造で学習すると自然に現れます。

スコアを確率に変える

最後は、小さくなった特徴マップを一列に並べ、全結合層でクラス数ぶんの数値を出します。ここで全結合を使ってよいのは、入力がもう150,528次元ではなく、はるかに小さく要約された表現だからです。出てくるのはロジットと呼ばれる生のスコアで、大小に意味はあっても範囲も合計も決まっていません。これを確率に変えるのがsoftmaxです。

pi=exp(zi/T)jexp(zj/T)p_i = \frac{\exp(z_i / T)}{\sum_j \exp(z_j / T)}
(3)

ziz_i はクラス ii のロジット、pip_i がその確率、TT温度(既定は1)です。この式が言っているのは要するに、指数関数で全部を正の値にしてから、合計が1になるように割っているだけ。指数を通すので、ロジットの差がわずかでも確率の差は大きく開きます。

FIG 1温度を下げると分布が1クラスへ尖り、上げると平らになる。棒の高さ(確率)はロジットの差だけで決まっていて、モデルが実際に当たっているかどうかとは無関係だという点に注意

numpyで書く畳み込み

ここまでの話は、これだけです。

import numpy as np

def conv2d(x, k, stride=1, pad=0):
    x = np.pad(x, pad)                    # 周囲を0で囲む
    K = k.shape[0]
    H = (x.shape[0] - K) // stride + 1    # 出力サイズの式そのもの
    W = (x.shape[1] - K) // stride + 1
    out = np.zeros((H, W))
    for i in range(H):
        for j in range(W):
            win = x[i*stride:i*stride+K, j*stride:j*stride+K]
            out[i, j] = (win * k).sum()   # 窓と重みの内積
    return out

(win * k).sum() が窓1回ぶんの内積で、二重ループが「滑らせる」に対応します。

現場ではこう使う

画像分類が仕事になるのは、たいてい「写真を見て仕分ける」業務の自動化です。製造ラインの外観検査、書類の種類判定、投稿画像のフィルタ——入り口はどれも学習済みモデルを自分のデータで微調整するところから始まり、ゼロから畳み込みを積む場面はほとんどありません。

触るパラメータはだいたい決まっています。 入力解像度(224が既定だが、細かい傷を見るなら上げる)、正規化の平均と標準偏差(学習済み重みが期待する値をそのまま使う)、データ拡張(RandomResizedCrop、左右反転、色調のゆらぎ)、学習率(微調整では事前学習時より1〜2桁小さく)、どこまでの層を凍結するか。データが少ないほど深く凍結するのが定石です。

事故のほとんどは前処理の不一致から起きます。 学習時と推論時でリサイズの方法や正規化の値が違うと、精度だけが静かに落ちる。例外も警告も出ないので発見が遅れます。学習と推論で同じ前処理関数を共有する、という一点で防げます。

データ拡張は「本番で起きうる変形」だけを入れる。 左右反転は猫には有効ですが、文字や矢印、左右に意味のある医療画像では毒です。畳み込みが構造として持つのは平行移動への強さだけで、回転や拡大縮小には自動で強くなりません。

評価では正解率を信じすぎない。 不良品が1%しかないラインでは「全部良品」と答えるだけで99%です。クラスごとの適合率・再現率と混同行列を見るのが最低条件。加えてsoftmaxの確率は較正されているとは限らず自信過剰になりがちなので、「確信度0.9以上なら自動処理」といった閾値は本番相当のデータで測り直してから決めてください。さらに、学習時に見せていない種類の画像を入れても、モデルは既知クラスのどれかに高い確率を割り当てます。「知らない」と言う能力は標準の分類器には無い——これを運用設計の前提に。

設計レビューでよく問われるのは「なぜ全結合ではなく畳み込みか」です。パラメータ数と平行移動同変性の2点で答えられれば十分。もう1つ頻出なのが「1×1の畳み込みは何をしているのか」で、答えはチャネル方向だけの線形結合——各画素位置で独立に走る全結合層です。チャネル数の増減やボトルネック構造に使われます。

まとめ

次は、この積み重ねを深くしていくと学習が壊れる問題と、その回避策です。

コメント

コメントにはログインが必要です