
敵対的サンプル(FGSM)を自作分類器に実装したら、εを2.5まで上げても反転率は45%で頭打ちだった
「ごくわずかな、人間の目にはほとんど気づけないノイズを画像に足すだけで、パンダを99%の自信でテナガザルだと誤分類させられる」——adversarial examples(敵対的サンプル)の話は、AIの安全性を語るときによく引き合いに出される。有名な例だとGoodfellowらのFGSM(Fast Gradient Sign Method)論文のパンダの画像がそれで、見た目はほぼ変わらないのに、モデルの確信度だけが完全にひっくり返る。
これを自分の手で確かめたことは一度もなかった。ただし今回やるのは、大きな画像分類モデルへの攻撃ではない。2次元の小さなtoyデータセットに、自分でスクラッチ実装したMLPを学習させ、そのMLPだけをFGSMで殴ってみる、という小規模でクリーンな再現実験だ。実運用の画像認識モデルがどうか、という主張はしていない。それでも「決定境界のすぐ近くにいる点は、本当にごくわずかな一押しで判定が反転するのか」という核心の部分は、この小さな実験でも十分に検証できる。
実験に使ったコードの全文はGitHubに置いています。
two-moonsに97パラメータのMLPを学習させる
データはdropoutの記事でも使ったtwo-moons(ノイズ0.15、標準化してstd=1.0)。1000点を700(学習)/300(テスト)に分割し、2→24(tanh)→1(sigmoid)のMLPを、順伝播・逆伝播・Adamまで全部numpyで自前実装して4000エポック学習した。
初見の方のために、そもそも何をやらせているのかを先に書いておく。two-moonsは、2つの三日月(クレセント)型の点群が互い違いに噛み合うように並んだ2次元の人工データセットで、各点には「どちらの三日月から生成されたか」のラベル(class 0/class 1)が付いている。モデルの仕事は、座標(x1, x2)だけを見てこのラベルを当てる、ごく普通の2クラス分類だ——下の図で言えば、青い点群と赤い点群のあいだに境界線を引くこと。そして今回の攻撃側の「成功」は、もともと正しく分類されていた点の座標をほんの少しだけ動かして、この判定を誤りに変える(以降「反転」と呼ぶ)ことと定義する。
def forward(params, X):
W1, b1, W2, b2 = params
z1 = X @ W1 + b1
h1 = np.tanh(z1)
z2 = h1 @ W2 + b2
out = sigmoid(z2).ravel()
return out, (X, z1, h1, z2)
パラメータ数はたったの97個。それでもテスト精度は0.9833まで上がった。

わずか97パラメータでも、境界は単純な直線ではなく、データの重なり具合に合わせて細かく波打っている。この「波打ち具合」が、あとでFGSMの挙動に効いてくる。
FGSM: 勾配の向きは「重み」ではなく「入力」について取る
通常の学習では、損失を重みについて微分して重みを更新する。FGSMはこれの逆で、損失を入力について微分し、その符号方向に入力そのものを動かす。
def grad_wrt_input(params, cache, y_true, out):
"""FGSMの核心: 重みではなく「入力X」についての dL/dX を計算する"""
W1, b1, W2, b2 = params
X, z1, h1, z2 = cache
d_z2 = (out - y_true).reshape(-1, 1) # 点ごと、平均しない
d_h1 = d_z2 @ W2.T
d_z1 = d_h1 * (1 - np.tanh(z1) ** 2)
d_X = d_z1 @ W1.T # (n, 2) dL/dX
return d_X
これで各点ごとに が求まる。あとは
の式どおり、符号だけを使って各点を摂動させる。符号だけを使うのは「勾配の向きが正しければ、大きさは気にせず損失が一番増える方向に等方的に動かす」という設計思想で、これがFGSMの”S”(Sign)の部分だ。
テスト300点のうちもともと正しく分類されていた295点を攻撃対象にし、εを0から2.5まで0.01刻みで細かくスイープして、「もともと正解していた点のうち何%が誤分類に反転したか」(反転率)を実測した。
結果1: 反転率はε=0.26(stdの26%)で10%、ε=0.41で25%——ただし45%前後で頭打ち

正直、予想していたのは「εを上げ続ければいずれ反転率はほぼ100%に近づく」という素直な右肩上がりのカーブだった。実際は違った。
- ε=0.05: 反転率 1.02%
- ε=0.10: 反転率 2.03%
- ε=0.20: 反転率 6.10%
- ε=0.26: 反転率が初めて10%を超える
- ε=0.30: 反転率 12.88%
- ε=0.41: 反転率が初めて25%を超える
- その先は単調ですらない。ε=0.9付近で42.4%まで上がったあと、ε=1.6にかけて36.3%まで一度下がり、ε=2.03でスイープ全体の最高値48.1%をつける
- ε=2.5(データの標準偏差2.5個分、もはや”微小”とはとても言えない大きさ)の終点では反転率45.08%。スイープ全体を通して、反転率は一度も50%に届かなかった
295点中111点(37.6%)は、εをどれだけ大きくしても一度も反転しなかった。 これは決定境界が直線ではなく波打っているせいで、「損失を一番増やす方向」として固定した符号ベクトルのまま点をどんどん遠くまで動かしても、境界を1回またいだ後にまた別の境界を(逆向きに)またいで正しい側へ戻ってしまう、といったことが起きているらしい。実際、ε=0.9→1.6の区間で反転率そのものが42%台から36%台へ下がっているのは、「小さいεでは反転していた点が、より大きく動かした結果、境界を越えて正しい側に戻ってしまった」ことの直接の証拠だ。1ステップだけの線形近似であるFGSMは、複雑に曲がった境界に対しては「大きく動かせば動かすほど効く」わけではない、ということが実測で分かった。
結果2: それでも境界のすぐ隣の点は、ε=0.01というごく小さな摂動で反転する
反転率という集計値だけを見ていると、「FGSMはこの分類器にそこまで効かない」という印象になりかねない。ただし個々の点を見ると話が違う。反転する184点(295点から一度も反転しなかった111点を除いた残り)それぞれについて「最小でどれだけのεで反転するか」を調べたところ、その最小値はε=0.010——データの標準偏差のわずか1%だった。中央値は0.490、最大は2.490で、この最小反転εのばらつき自体が「境界からの距離のばらつき」をそのまま映している。
- ε≦0.01で反転する点: 295点中1点(0.3%)
- ε≦0.05で反転する点: 295点中3点(1.0%)
- ε≦0.10で反転する点: 295点中6点(2.0%)
境界のすぐ隣に偶然立っていた点は、本当にわずかな一押しで反転する。一方で境界から離れた場所にいる大多数の点は、標準偏差の何倍もの大きさで押してもびくともしない。「微小な摂動で分類が反転する」という現象自体は確かに起きているが、それは全体に一律に起きるのではなく、境界との距離に応じて劇的に確率が変わる、局所的な現象だということが分かった。
εを10%反転率に達する0.26に固定し、実際にどの点がどう動いたのかを個別に見てみる。

よく見ると、どの矢印もぴったり斜め45度を向いていることに気づくと思う。これはバグではなくsignを取ることの帰結で、2次元では勾配の各成分の符号が±1のどちらかになるため、摂動の方向は4つの対角方向のいずれかしかあり得ない(今回の295点も実際にこの4方向のどれかだった)。矢印の長さも全点共通で ——同じεで動かしているのだから、赤(反転した点)も青(反転しなかった点)も同じ長さになる。違うのはもともと境界からどれだけ近かったかだけで、赤い矢印はもともと境界のすぐそばにいた点、青い矢印はある程度余裕を持って境界の内側にいた点だ。「同じ強さの一押し」が、立っている場所次第で運命を分ける。
εを連続的に上げていくアニメーション
εを0から1.0まで連続的に上げながら、正解していた295点全体がどう動いていくかをGIFにした。

序盤はほとんど何も起きていないように見えるのに、εが上がるにつれて境界に近い点から順番に色を変えていく様子が見える。全員が一斉に動くのに、実際に「反対側に落ちる」のは境界のすぐそばにいた点だけ、というのがこのアニメーションで一番伝わってほしいところだ。
手を動かして意外だったこと
一番意外だったのは、反転率が45%程度で頭打ちになり、εをいくら大きくしても100%に近づかなかったことだ。攻撃前の予想では、εを十分大きくすれば理論上ほぼ全ての点を反転させられるはずだと思っていた。実際には陰陽の記事で見た「表現力の階段」と同じで、この決定境界自体が単純な直線ではなく複雑に波打っているせいで、1ステップの線形近似であるFGSMの「決めた方向にずっと進み続ける」という前提そのものが、大きく動かすほど崩れていく。295点中111点は、標準偏差の2.5倍もの大きさで押しても一度も反転しなかった——「攻撃を強くすればするほど効く」という単純な話ではなかった。
もう1つは、境界のすぐそばの点がε=0.01という本当にわずかな摂動で反転していたこと。集計された反転率のカーブだけを見ていたら、この「ごく一部の点が信じられないほど脆い」という事実には気づけなかった。全体の反転率が10%を超えるにはε=0.26まで必要だったのに、最も脆い1点はその26分の1のεで既に落ちていた。平均的な頑健さと、最悪ケースの脆さは、まったく違う指標だということを実測して痛感した。
小さな、けれど正確に狙われた一押しが、大きな流れの中では何の変哲もない一票にしかならないのに、境界のまさに際に立っている一点にとっては判定を丸ごとひっくり返す。これは意思決定や議論、あるいは自己評価にも通じる話だと思う。多くの場面では、少しくらい条件が変わっても結論は揺るがない——大多数の点がε=2.5でも動じなかったように。けれど、自分でも気づかないうちに「ちょうど境界線の真上」に立ってしまっている判断や自己評価があるとしたら、それは外から見れば自信満々に見えても、実はごくわずかな、狙いすました一押しだけでひっくり返る。怖いのは、境界の内側にいるか際どい位置にいるかは、自分で見ている限りではなかなか区別がつかないということだ。
まとめ
- two-moonsデータ(1000点、標準化)に2→24(tanh)→1(sigmoid)、パラメータ97個のMLPを学習させ、テスト精度0.9833を達成した
- FGSM(損失の入力に対する勾配の符号方向に で摂動)を、正しく分類されていた295点に対して実施した
- 反転率(誤分類に変わった割合)はε=0.26で10%、ε=0.41で25%に達したが、ε=2.5(標準偏差2.5個分)まで上げても45%前後で頭打ちになり(最高でもε=2.03の48.1%で、50%には一度も届かなかった)、295点中111点(37.6%)は一度も反転しなかった
- 一方、反転する点の中には最小ε=0.010(データの標準偏差のわずか1%)という極めて小さな摂動で反転するものもあり、境界からの距離次第で「脆さ」は劇的に変わることが分かった
- 集計された反転率のカーブだけでは、この「ごく一部の点が信じられないほど脆い」という事実も、「εを上げ続けても100%には近づかない」という頭打ちの事実も、どちらも見えてこなかった
このブログでは、機械学習の仕組みを実際にコードで実装して確かめた実験をテーマ別に整理しています(拡散モデル・Transformer・強化学習・多様体学習・過学習など)。


