はじめに
新千円札を出すたびに思う。
北里柴三郎、1000円の顔してなくない?
威厳がすごい。貫禄がすごい。どう見ても5000円以上の雰囲気を纏っている。
この「なんか高そうな顔」という直感、定量的に検証できるんじゃないか。世界中の紙幣の肖像画を集めて、顔だけ見て金額を当てるモデルを作れば、「北里の顔は何円に見えるか」がわかるはず。
やってみた。地獄だった。
仮説
紙幣の肖像画には、金額に対応した視覚的パターンがある
高額紙幣の人物はそれなりの貫禄があり、低額紙幣の人物はカジュアルに見える、みたいな傾向が存在するなら、機械学習モデルは「顔だけで金額を予測」できるはずだ。
パイプライン全体像
1. Wikimedia Commons から世界38通貨の紙幣画像を収集
2. MTCNN / OpenCV DNN / Haar Cascade で肖像(顔)だけ切り抜く
3. EfficientNet-B0 で「この顔は高額?低額?」を学習
4. 北里・野口・渋沢の肖像を突っ込んで予測
シンプルに見えるでしょう。コードよりデータ収集が10倍つらかった。
設計上の最大の意思決定:「金額」の扱い
1000円と1ドルは比較できない。為替レートが違う。
だから通貨内での相対ランクにした。0が最低額面、1が最高額面。
# 通貨内でのランクに正規化 (0=最低額, 1=最高額)
df["denom_rank"] = df.groupby("currency")["denomination"].transform(
lambda x: (x.rank() - 1) / max(x.nunique() - 1, 1)
)
これで「日本円の1000円 = 0.0」「10000円 = 1.0」のように、通貨をまたいで統一的に扱える。
データ収集:地獄の始まり
Wikimedia Commons のカテゴリ構造、信用するな
最初はカテゴリAPI(Category:1000_yen_banknotes)で画像を集めた。787ファイルヒットして「余裕じゃん」と思った。
31枚しか使えなかった。
理由:ファイル名が多言語で書かれていて、金額をパースできない。日本銀行券千円_表.jpg みたいなファイル名から「1000」を取り出すのは至難の業。
対策として、カテゴリ名から金額を読む方式に切り替えた。1000_yen_banknotes なら denomination=1000。当たり前の解法だが、気づくまで時間がかかった。
検索APIへの切り替え
カテゴリ構造はWikimediaの編集者次第で変わる。ある日突然サブカテゴリが空になった。
結局、Search API(action=query&list=search&srnamespace=6)に全面移行。1額面あたり4つのクエリバリエーションで検索する:
queries = [
f"{denom_str} {currency_name} banknote",
f"{denom_str} {currency_name} note",
f"{denom_str} {currency_name} bill",
f"{currency_name} {denom_str} banknote",
]
429 Too Many Requests:本当の地獄
Wikimedia の upload.wikimedia.org は、短時間に大量ダウンロードすると429を返してIPをブロックする。
対策として色々やった:
# ブラウザのUser-Agentを使う(Bot UAだと403になる)
DOWNLOAD_HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ..."
}
# 連続5回429 → 10分待機
if _consecutive_429 >= 5:
print(f"IPブロック中とみなし 600s 待機...")
time.sleep(600)
それでもブロックされ続けた。スマホテザリングでIP変えるか、一晩放置するか、Kaggleに切り替えるか。データ収集は泥仕事。
最終的な方針:通貨単位で処理する
全通貨をまとめてダウンロード → 全部顔検出、ではなく、1通貨ずつ「ダウンロード → 顔検出 → 結果確認」を完結させる方式に変更した。
# JPYだけまず処理
python collect_and_extract.py --currency JPY
# 進捗確認
python collect_and_extract.py --status
# 完了済み通貨はスキップして続行
python collect_and_extract.py
これで429でブロックされても通貨単位で再開できるし、「この通貨、顔検出率が低いな」という品質チェックも即座にできる。
顔検出:3段構えの検出パイプライン
紙幣の肖像は正面向きとは限らない。横顔、斜め、絵画調…。1つの検出器では拾いきれない。
class FaceDetector:
def detect(self, img_bgr):
# 1) MTCNN(最も高精度、confidence > 0.7)
# 2) OpenCV DNN(SSD ベース、confidence > 0.5)
# 3) Haar Cascade(最終手段)
# すべて失敗 → その画像は使わない
重要な設計判断:顔が検出できなかった画像は捨てる。
以前は「中央切り抜き(center_crop)」でフォールバックしていたが、紙幣の模様や建物がデータセットに混入して品質が大幅に下がった。顔が見つからない=その画像は使わない。これだけでデータ品質が劇的に改善した。
モデル:EfficientNet-B0 + デュアルヘッド
class BanknoteModel(nn.Module):
def __init__(self):
# EfficientNet-B0(ImageNet事前学習済み)
self.features = efficientnet_b0.features
self.avgpool = efficientnet_b0.avgpool
# 回帰ヘッド:金額ランク (0.0〜1.0) を直接予測
self.regressor = nn.Sequential(
nn.Linear(1280, 256), nn.ReLU(), nn.Dropout(0.3),
nn.Linear(256, 1), nn.Sigmoid(),
)
# 分類ヘッド:low / mid / high の3値分類(補助タスク)
self.classifier = nn.Sequential(
nn.Linear(1280, 128), nn.ReLU(), nn.Dropout(0.3),
nn.Linear(128, 3),
)
損失関数は 0.7 * MSE + 0.3 * CrossEntropy。回帰だけだと学習が不安定になりがちなので、3値分類を補助タスクとして入れている。
学習戦略:
- 最初の10エポックはバックボーン凍結(ヘッドだけ学習)
- 11エポック目以降はバックボーンも解凍して全体をfine-tune(学習率を1/10に)
- CosineAnnealingLR で学習率スケジューリング
仮説検定の基準
ランダム予測(0〜1の一様分布)の MSE は理論値で 1/12 ≈ 0.0833。
判定基準:
- Pearson r > 0.2 かつ MSE < 0.0833 → 仮説支持
- それ以外 → 仮説棄却
結果
暫定結果(初回データセット、96肖像)
通貨別のPearson r:
| 通貨 | Pearson r | n |
|---|---|---|
| エジプト(EGP) | 0.959 | 12 |
| スイス(CHF) | 0.860 | 15 |
| 英国(GBP) | 0.775 | 8 |
| ユーロ(EUR) | 0.762 | 11 |
| 日本(JPY) | 0.533 | 10 |
北里の予測結果:
| 人物 | 本当の額面 | 予測ランク |
|---|---|---|
| 野口英世 | 1000円 (rank=0.0) | 0.419 |
| 北里柴三郎 | 1000円 (rank=0.0) | 0.440 |
| 渋沢栄一 | 10000円 (rank=1.0) | 0.486 |
北里は野口(同じ1000円)より高く予測された。 モデルも「こいつ1000円じゃないだろ」と思っている。
最終結果(データ品質改善後)
Test MSE: 0.1053
Baseline MSE: 0.0833(ランダム予測)
Test Pearson r: -0.176
3値分類精度: 0.423(chance=0.333)
仮説判定: [NG] 仮説棄却
…覆った。
データ品質を改善して非肖像画像を除外したら、むしろ性能が下がった。
考察:なぜ結果が覆ったのか
正直に書く。
-
データ量の問題:center_cropを廃止して「顔が見つからない画像は捨てる」方針にしたことで、使える画像が大幅に減った。学習データが足りない状態。
-
ノイズが「効いていた」可能性:center_cropで切り出した紙幣中央部には、額面の数字や装飾パターンが含まれていた。モデルはそれを手がかりに金額を当てていた可能性がある。つまり「顔で判別」ではなく「紙幣のデザインで判別」していたかもしれない。
-
データリーク:同じ紙幣の異なる画像(表と裏、異なるバージョン)が訓練とテストに分かれていた可能性。顔ではなく紙幣の背景パターンで判別していたなら、それはデータリーク。
教訓
1. データ収集は想像の3倍つらい
Wikimedia Commonsは「誰でもアクセスできるフリー画像データベース」だが、実際に大量ダウンロードしようとすると429で殴られる。API設計とダウンロード制限は別物。
2. center_cropは麻薬
顔検出に失敗したときの「とりあえず中央を切り出す」は、一見データ量を増やしてくれる。しかし、そのデータは肖像ではなく紙幣のデザインパターンを含んでおり、モデルに間違った手がかりを学習させる。精度は上がるが、仮説の検証にはならない。
3. 「精度が上がった」は「仮説が正しい」ではない
初回データセットで r=0.533 が出たとき、「やっぱり北里は5000円の顔してる!」と喜んだ。しかしデータを精査したらリークの可能性が見えて、品質改善後は仮説棄却せざるを得なくなった。
再現手順
git clone https://github.com/xxx/banknote-portrait-analysis
cd banknote-portrait-analysis
# 1. データ収集 + 顔検出(通貨単位で処理)
python collect_and_extract.py --currency JPY
python collect_and_extract.py # 全通貨
# 2. モデル学習
python train_model.py
# 3. 詳細評価
python evaluate.py
主要ライブラリ
torch, torchvision # EfficientNet-B0
mtcnn, opencv-python # 顔検出
scikit-learn # データ分割
pandas, numpy # データ処理
matplotlib, seaborn # 可視化
結論
北里柴三郎が5000円の顔をしているかどうか、機械学習では結論が出なかった。
初回データでは「はい」と言いかけたが、データを精査したら「わからない」に戻った。仮説検証で一番難しいのは、自分に都合のいい結果を疑うこと。
ただ、エジプトポンドやスイスフランでは通貨内で高い相関が出ている。データ量が十分にあれば、肖像の視覚的特徴と金額に何らかの関連がある可能性は残っている。
次のステップ:
- Kaggleの紙幣画像データセットを使って、データ量を10倍に増やして再検証
- 顔の特徴量(年齢推定、表情、顔の向き)を明示的に抽出してみる
- 「お札の肖像だけ見て金額を当てるゲーム」を作って、人間の精度と比較する
北里先生、私はまだ諦めてません。あなたは1000円に収まる顔じゃない。
本記事のコード・画像は全て公開しています。紙幣画像はWikimedia Commonsから取得(CC-BY-SA)。
本研究は特定の紙幣・人物を貶める意図はありません。純粋な知的好奇心です。