CNN: ゼロから本番環境までの畳み込みネットワーク
コンピューターは写真の中の猫をどのようにして「見る」のでしょうか?交通標識と人間の顔はどうやって区別しますか? 答えは次のとおりです 畳み込みニューラル ネットワーク (CNN)、ディープラーニングアーキテクチャ それはコンピュータビジョンに革命をもたらしました。自動運転車から医療画像診断まで、 CNN は、私たちが毎日使用する何百万ものアプリケーションの背後にある目に見えないエンジンです。
このシリーズの最初の記事では 深層学習を使用したコンピューター ビジョン、私たちは構築します CNN をゼロから理解する: CNN とは何か、畳み込みフィルターがどのように機能するか、 歴史を作ったアーキテクチャと、PyTorch で完全な CNN を実装してトレーニングする方法。 完了すると、実際の画像を分類し、モデルを実稼働環境に導入するスキルが身に付きます。
シリーズ概要
| # | アイテム | 集中 |
|---|---|---|
| 1 | 現在位置 - CNN: 畳み込みネットワーク | アーキテクチャ、トレーニング、展開 |
| 2 | 転移学習と微調整 | 事前トレーニングされたモデル、ドメイン適応 |
| 3 | YOLO による物体検出 | リアルタイムの物体検出 |
| 4 | セマンティックセグメンテーション | ピクセルレベルの分類 |
| 5 | GAN による画像生成と拡散 | 合成画像生成 |
| 6 | エッジの導入と最適化 | 組み込みデバイス上のモデル |
何を学ぶか
- コンピューターが画像を表現する方法 (ピクセル、RGB チャネル、テンソル)
- 畳み込み演算: カーネル、機能マップ、スライディング ウィンドウ
- CNN の基本コンポーネント: 畳み込み、プーリング、アクティベーション
- アーキテクチャの進化: LeNet-5 から ConvNeXt まで
- ResNet とスキップ接続: 勾配消失を解決する方法
- PyTorch での完全な実装: CIFAR-10 分類用の CNN
- 転移学習: ImageNet で事前トレーニングされたモデルを再利用する
- 評価指標: 精度、精度、再現率、混同行列
- デプロイメント: トレーニングされたモデルから本番推論まで (ONNX、TorchScript)
1. ピクセルから機能まで: コンピューターが画像をどのように認識するか
私たち人間にとって、写真を見ることは自然な行為です。私たちの脳は瞬時に認識します 形、色、輪郭、物体。しかし、コンピュータにとって、画像は単なる数字の格子にすぎません。 各ピクセルは光の強さを示す数値で表されます。
1.1 数値の行列としての画像
グレースケール イメージは、各セルに 0 (黒) の間の値が含まれる 2D 配列です。
と255(白)。カラー画像は次のもので構成されます 3つのチャンネル (赤、緑、青)、
それぞれが別個の行列です。したがって、224x224 のカラー画像は次元テンソルです。
3 x 224 x 224、または 150,528 個の数値。
Immagine 4x4 (scala di grigi, valori 0-255):
+-----+-----+-----+-----+
| 10 | 20 | 30 | 40 |
+-----+-----+-----+-----+
| 50 | 120 | 180 | 60 |
+-----+-----+-----+-----+
| 70 | 200 | 220 | 80 |
+-----+-----+-----+-----+
| 90 | 100 | 110 | 130 |
+-----+-----+-----+-----+
Immagine a colori (3 canali RGB):
Canale R: [[10, 20, ...], ...] --> tonalita di rosso
Canale G: [[30, 40, ...], ...] --> tonalita di verde
Canale B: [[50, 60, ...], ...] --> tonalita di blu
Tensore finale: shape = (3, H, W) --> (canali, altezza, larghezza)
1.2 高密度ニューラルネットワークでは不十分な理由
最初に思い浮かぶアプローチは、画像を 1D ベクトルに平坦化し、それを通過させることです。 完全に接続された (高密度の) ニューラル ネットワークに接続します。 224x224x3 の画像の場合、これは次のことを意味します。 入力レイヤー 150,528 個のニューロン。 1,000 個のニューロンからなる隠れ層を使用すると、 最初の層だけですでに 1 億 5,000 万個のパラメータがあることになります。
画像の高密度ネットワークの問題
- パラメータ爆発: 最初の層にはすでに何百万もの重みがあり、計算量が法外です
- 空間的不変性がない: 猫が右に 10 ピクセル移動すると、ネットワークは猫を認識しなくなります
- 2D 構造の損失: 画像を平坦化すると、隣接するピクセル間の空間的関係が破壊されます。
- 過学習: パラメーターが多すぎてデータが少なすぎると、一般化ではなく暗記が行われます。
CNN は、次の 3 つの重要な洞察を活用して、これらすべての問題を解決します。 地域性 (視覚的なパターンはローカルです)、 負担の分担 (同じフィルターが画像内のどこでも機能します) e 翻訳の不変性 (エッジはどこにあるかに関係なくエッジです)。
2. 畳み込み演算
La 畳み込み これは CNN の中心となる数学的演算です。ちょっとしたフィルター (言った カーネル) 入力画像をスクロールし、各位置の合計を計算します カバーされたピクセルの重み付け。結果は、という名前の新しい配列になります。 機能マップ、 特定のパターン (垂直エッジ、水平エッジ、コーナー、テクスチャ) を強調表示します。
2.1 カーネルとスライディング ウィンドウ
カーネルは、スクロールされる重みの小さな行列 (通常は 3x3 または 5x5) です。 (スライド) 入力画像全体にわたって。各位置でのカーネル値は次のとおりです。 要素ごとに基礎となるピクセルを乗算し、加算して単一のピクセルを生成します。 出力特徴マップの値。
Input (5x5): Kernel (3x3):
+---+---+---+---+---+ +----+----+----+
| 1 | 2 | 3 | 0 | 1 | | -1 | 0 | 1 |
+---+---+---+---+---+ +----+----+----+
| 0 | 1 | 2 | 3 | 1 | | -1 | 0 | 1 |
+---+---+---+---+---+ +----+----+----+
| 1 | 0 | 1 | 2 | 0 | | -1 | 0 | 1 |
+---+---+---+---+---+ +----+----+----+
| 2 | 1 | 0 | 1 | 3 | (Filtro per bordi verticali)
+---+---+---+---+---+
| 0 | 1 | 2 | 1 | 0 |
+---+---+---+---+---+
Posizione (0,0): applica kernel ai pixel evidenziati [*]
[*1][*2][*3] 0 1
[*0][*1][*2] 3 1 Calcolo:
[*1][*0][*1] 2 0 (-1x1)+(0x2)+(1x3)+(-1x0)+(0x1)+(1x2)+(-1x1)+(0x0)+(1x1)
2 1 0 1 3 = -1 + 0 + 3 + 0 + 0 + 2 - 1 + 0 + 1 = 4
0 1 2 1 0
Output feature map (3x3):
+---+---+---+
| 4 | . | . | <-- Il 4 appena calcolato
+---+---+---+
| . | . | . | Il kernel scorre e calcola ogni cella
+---+---+---+
| . | . | . |
+---+---+---+
クラシックカーネルタイプ
| カーネル | 範囲 | 値(3x3) |
|---|---|---|
| 垂直境界線 | 垂直方向の変化を検出する | [-1、0、1]の繰り返し |
| 横枠 | 水平方向の遷移を検出する | [-1,-1,-1]、[0,0,0]、[1,1,1] |
| 研ぐ | 切れ味が増す | [0,-1.0]、[-1.5,-1]、[0,-1.0] |
| ガウスぼかし | ガウスぼかし | [1,2,1]、[2,4,2]、[1,2,1] / 16 |
CNN と従来の画像処理の主な違いは次のとおりです。 CNN で カーネル値は手動では設定されません。ネットワーク 自動的に学習します トレーニング中に最適なフィルターを提供します。 バックプロパゲーション。最初のレイヤーは単純なエッジとテクスチャの検出を学習します。 より深いものでは、これらの特徴がますます複雑なパターン (目、車輪、顔) に結合されます。
3. CNN のコンポーネント
CNN はさまざまなタイプのレイヤーで構成されており、それぞれに特定の役割があります。 効果的なアーキテクチャを設計するには、各コンポーネントの機能を理解することが重要です。
3.1 畳み込み層
畳み込み層は複数のフィルターを入力に適用し、次の特徴マップを生成します。 各フィルター。 32 個の 3x3 フィルターを RGB 画像に適用すると、32 個の特徴マップが得られます。 それぞれが異なるパターンを強調表示します。主要なパラメータは次のとおりです。
畳み込み層パラメータ
| パラメータ | 説明 | 代表的な値 |
|---|---|---|
| カーネルサイズ | フィルターサイズ(幅×高さ) | 3x3、5x5、7x7 |
| ストライド | フィルタースクロールステップ | 1、2 |
| パディング | 入力のエッジに追加されたピクセル | 0(有効)、1(3x3も同様) |
| フィルターの数 | 出力内の特徴マップの数 | 32、64、128、256、512 |
Formula dimensione output:
output_size = (input_size - kernel_size + 2 * padding) / stride + 1
Esempio: input 32x32, kernel 3x3
Stride=1, Padding=0: (32 - 3 + 0) / 1 + 1 = 30x30 (si riduce)
Stride=1, Padding=1: (32 - 3 + 2) / 1 + 1 = 32x32 (same padding)
Stride=2, Padding=1: (32 - 3 + 2) / 2 + 1 = 16x16 (dimezza)
Stride=1, Padding=1 ("same"):
Input: [A B C D E] Output: [A B C D E] --> stessa dimensione
(con zero-padding ai bordi)
Stride=2 (dimezza la risoluzione):
Input: [A B C D E F] Output: [A C E] --> meta dimensione
(salta un pixel ad ogni passo)
3.2 アクティベーション関数(ReLU)
各畳み込みの後に、非線形活性化関数が適用されます。最も一般的なのは
そこにあります ReLU (整流線形ユニット): f(x) = max(0, x)。
ReLU はすべての負の値をリセットし、正の値は変更しないままにします。非線形性がなければ、
一連の畳み込みは単一の線形変換に相当します。
ネットワークが複雑なパターンを学習できなくなります。
ReLU: f(x) = max(0, x) Semplice, veloce, standard
LeakyReLU: f(x) = x se x>0, 0.01x altrimenti Evita "dying ReLU"
GELU: f(x) = x * Phi(x) Usata nei Transformers, smooth
Swish: f(x) = x * sigmoid(x) Usata in EfficientNet
Input feature map: Dopo ReLU:
+----+-----+----+ +----+----+----+
| -3 | 5 | -1 | | 0 | 5 | 0 |
+----+-----+----+ +----+----+----+
| 2 | -7 | 4 | | 2 | 0 | 4 |
+----+-----+----+ +----+----+----+
| -2 | 1 | -5 | | 0 | 1 | 0 |
+----+-----+----+ +----+----+----+
3.3 プーリング層
プーリングにより特徴マップの空間次元が削減され、パラメーターの数が減少します。 特徴の位置の小さな変化に対してネットワークをより堅牢にします。 2種類 主なものは 最大プーリング (各ウィンドウの最大値を取る) e 平均プーリング (平均を計算します)。
Input (4x4): Output (2x2):
+----+----+----+----+ +----+----+
| 12 | 20| 30| 0 | | 20 | 30 | max(12,20,0,8)=20
+----+----+----+----+ --> +----+----+ max(30,0,2,14)=30
| 0 | 8 | 2 | 14 | | 15 | 16 |
+----+----+----+----+ +----+----+
| 15 | 2 | 3 | 16 |
+----+----+----+----+ Riduce 4x4 --> 2x2
| 1 | 6 | 7 | 8 | (dimezza altezza e larghezza)
+----+----+----+----+
Max Pooling: prende il massimo --> preserva le feature più forti
Avg Pooling: calcola la media --> effetto di smoothing
Global Average Pooling: media su tutta la feature map --> un singolo valore per canale
3.4 バッチ正規化
La バッチ正規化 (BatchNorm) 各レイヤーの出力を正規化します そのため、平均と単位分散はゼロになります。これによりトレーニングが安定し、 学習率が高く、軽い正則化機能として機能します。実際にやってみると、それは当てはまります 各畳み込み後のアクティブ化前の BatchNorm 層。
4. 典型的な CNN アーキテクチャ
標準的な CNN は、特徴抽出ブロック (畳み込み) という繰り返しパターンに従います。 + アクティベーション + プーリング)、その後に完全に接続されたレイヤーが最終的な分類に続きます。 深度が増すと、特徴マップの空間サイズは減少しますが、数は増加します ますます抽象化されたパターンをキャプチャするチャネル。
Input Immagine (3 x 32 x 32)
|
v
[Conv2d 3->32, 3x3, pad=1] --> [BatchNorm] --> [ReLU] --> [MaxPool 2x2]
| Feature maps: 32 x 16 x 16
v
[Conv2d 32->64, 3x3, pad=1] --> [BatchNorm] --> [ReLU] --> [MaxPool 2x2]
| Feature maps: 64 x 8 x 8
v
[Conv2d 64->128, 3x3, pad=1] --> [BatchNorm] --> [ReLU] --> [MaxPool 2x2]
| Feature maps: 128 x 4 x 4
v
[Flatten] --> Vettore di 128 * 4 * 4 = 2048 valori
|
v
[Linear 2048 -> 256] --> [ReLU] --> [Dropout 0.5]
|
v
[Linear 256 -> 10] --> Output: 10 classi (es. CIFAR-10)
|
v
[Softmax] --> Probabilità per ogni classe: [0.02, 0.01, 0.85, ...]
Flusso delle dimensioni:
(3, 32, 32) -> (32, 16, 16) -> (64, 8, 8) -> (128, 4, 4) -> (2048) -> (256) -> (10)
[immagine] [bordi, texture] [parti] [oggetti] [decisione] [classe]
学習された特徴の階層
- レイヤ 1 ~ 2 (低レベル): ボーダー、カラーグラデーション、シンプルなテクスチャ
- レイヤー 3 ~ 5 (中レベル): オブジェクトの角、輪郭、部分(目、車輪)
- レイヤ 6+ (高レベル): 完全なオブジェクト、シーン、抽象的な概念
この階層はトレーニング中に自動的に表示されます。必要はありません 何を探すべきかをネットワークに伝えるため、フィルターはデータに適応します。
5. CNN アーキテクチャの進化
CNN の歴史は革新的なアーキテクチャによって特徴づけられ、それぞれのアーキテクチャで 分野を変えるアイデア。この進化を知ることは理解の基礎です 現代建築の選択。
CNN アーキテクチャのタイムライン
| Anno | 建築 | 主要なイノベーション | トップ 1 イメージネット |
|---|---|---|---|
| 1998年 | ルネット-5 | 初の実用的なCNN(数字認識) | 該当なし |
| 2012年 | アレックスネット | GPUトレーニング、ReLU、ドロップアウト | 63.3% |
| 2014年 | VGGネット | 均一な 3x3 フィルターを備えたディープネットワーク | 74.5% |
| 2014年 | GoogLeNet/インセプション | インセプション モジュール、マルチスケール パラレル | 74.8% |
| 2015年 | レスネット | スキップ接続、152 層以上のネットワーク | 78.6% |
| 2019年 | EfficientNet | 複合スケーリング (深さ+幅+解像度) | 84.4% |
| 2022年 | 次への変換 | ビジョン トランスフォーマーにインスピレーションを得た最新の CNN | 87.8% |
5.1 LeNet-5 (1998) - パイオニア
Yann LeCun によって手書き数字認識 (MNIST)、LeNet-5 用に設計されました。 そして実質的に成功した最初の CNN です。わずか 5 つのレイヤーと 60,000 のパラメーターで、それが証明されました 畳み込みは画像から識別機能を学習できるということです。に使用されました 銀行小切手を自動的に読み取ります。
5.2 AlexNet (2012) - 革命
AlexNet は ImageNet 2012 コンペティションで大差で優勝し、エラーを削減しました 26%から16%へ。主な革新: GPU トレーニング (NVIDIA GTX 580 2 基)、機能 シグモイドの代わりに ReLU をアクティブ化し、正則化とデータ拡張のための Dropout。 この結果により、学界と産業界は深層学習が機能することを確信しました。
5.3 VGGNet (2014) - 深さが重要
VGG は、ネットワークが深いほど良い結果が得られることを示しました。彼の重要なアイデア 徹底的なシンプルさ: 3x3 のスタック フィルターのみを使用します。 2 つの連続した 3x3 レイヤー 単一の 5x5 レイヤーと同じ受容野を持っていますが、パラメーターはますます少なくなります。 非線形性。 VGG-16 には 16 レイヤーと 1 億 3,800 万のパラメーターがあります。
5.4 EfficientNet (2019) - インテリジェントなスケーリング
EfficientNet が導入したのは、 複合スケーリング: ただ増やすのではなく 深さ (VGG など) または幅、3 つの次元すべてを均一にスケールします (深さ、幅、入力解像度) のバランスのとれた係数を使用します。 EfficientNet-B0 ImageNet ではわずか 530 万のパラメータで 77.1% の精度を達成したとのレポート 前例のない精度パラメータ。
5.5 ConvNext (2022) - 現代化された CNN
ConvNeXt は、ビジョン トランスフォーマーに触発された技術で最新化された CNN が、 それらは変圧器アーキテクチャと競合する (そして超える) ことができます。イノベーションには次のようなものがあります。 7x7 深さ方向に分離可能なカーネル、BatchNorm の代わりに LayerNorm、GELU アクティベーション、 そして段階的にサイズが増加する「同型」デザイン。 ConvNeXt V2、バージョン内 E-ConvNeXt-Tiny、わずか 2.0 GFLOP で 80.6% のトップ 1 を達成、導入に優れています 効率的です。
6. ResNet とスキップ接続
ResNet (残留ネットワーク)、Heらによって提案されました。 2015年に解決しました 深層学習の基本的な問題の 1 つ: 劣化の問題。 ResNet が登場する前は、深いネットワークにレイヤーを追加すると、結果は良くなるどころか悪くなってしまいました。 トレーニングセットでも。このソリューションはシンプルであると同時にエレガントです。
6.1 勾配消失問題
バックプロパゲーション中、勾配は繰り返し乗算されます。 ネットワークの層。これらの乗算で 1 未満の値が生成される場合、勾配は 初期層に向かって伝播するにつれて、指数関数的に「消えていきます」。 50 層以上になると、勾配が非常に小さくなり、最初の数層で停止してしまいます。 学ぶこと。最近の研究では、スキップ接続がないと、L2 初期レイヤーではグラデーションが大幅に低下しますが、スキップ接続では ネットワーク全体で均一に保たれます。
Rete profonda SENZA skip connections (50 layer):
Layer 50 Layer 49 Layer 48 ... Layer 2 Layer 1
grad=1.0 * 0.8 * 0.8 ... * 0.8 * 0.8
Gradiente al layer 1: 0.8^49 = 0.00001 --> quasi zero!
I primi layer NON apprendono.
Rete profonda CON skip connections (ResNet):
Il gradiente ha un "percorso diretto" attraverso le skip connections.
Non viene moltiplicato ripetutamente per valori piccoli.
Gradiente al layer 1: ~0.5 --> i layer apprendono normalmente!
6.2 解決策: 残留学習
ResNet の素晴らしいアイデアはシンプルです。ブロックに変換を学習させる代わりに、
完了 H(x)、あなたは彼にだけを学ばせます 違い (残留物)
入力に関して: F(x) = H(x) - x。ブロックの出力は次のようになります。
y = F(x) + x、 どこ x ブロックを「ジャンプ」するのは入力です。
1つ 接続をスキップする (またはショートカット接続)。
Blocco Standard: Residual Block:
x x ------+
| | |
v v | (skip connection)
[Conv 3x3] [Conv 3x3] |
| | |
[BatchNorm] [BatchNorm] |
| | |
[ReLU] [ReLU] |
| | |
[Conv 3x3] [Conv 3x3] |
| | |
[BatchNorm] [BatchNorm] |
| | |
v v |
H(x) = output F(x) + x <--+
|
[ReLU]
|
v
output
Se F(x) = 0, l'output è semplicemente x (identità).
La rete può "saltare" un blocco se non serve.
Questo rende il training di reti profonde stabile.
それは機能するので
残差を学ぶ F(x) = 0 (つまり、「何もしない」)方がはるかに簡単です
完全なアイデンティティの変容を学ぶよりも。レイヤーが役に立たない場合、ネットワークは
ただ学ぶだけ F(x) = 0 そして入力を変更せずに渡します。これにより、
パフォーマンスを低下させることなく、数百のレイヤーを持つネットワークを構築します。
7. CNN のトレーニング
CNN のトレーニングとは、すべてのカーネル (フィルター) の最適な値を見つけることを意味します。 完全に接続された層の重み。これは反復プロセスを通じて行われます 順方向パス、損失計算、および勾配バックプロパゲーション。
7.1 損失関数
画像分類の場合、標準の損失関数は次のとおりです。 クロスエントロピー損失。 ネットワークによって予測された確率が実際のラベルからどの程度乖離しているかを測定します。 完璧な予測では損失 = 0 が生成されます。完全に間違った予測が生み出すもの 損失は無限大になる傾向があります。
Etichetta reale (one-hot): [0, 0, 1, 0, 0, 0, 0, 0, 0, 0] (classe "gatto" = indice 2)
Predizione buona: [0.02, 0.03, 0.85, 0.02, 0.01, 0.02, 0.01, 0.02, 0.01, 0.01]
Loss = -log(0.85) = 0.16 --> Loss bassa, predizione corretta
Predizione cattiva: [0.30, 0.25, 0.05, 0.10, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05]
Loss = -log(0.05) = 3.00 --> Loss alta, predizione sbagliata
7.2 オプティマイザー
オプティマイザは、損失を減らす方向にネットワークの重みを更新します。最もよく使用されるのは次のとおりです。
オプティマイザーの比較
| オプティマイザ | 特徴 | いつ使用するか |
|---|---|---|
| シンガポールドル + 勢い | シンプル、堅牢、安定したコンバージェンス | 長時間のトレーニング、最大の最終精度 |
| アダム | 適応学習率、高速収束 | プロトタイピング、中小規模のネットワーク |
| アダム・W | 正しい体重減少を持つアダム | 最新の標準、CNN に推奨 |
7.3 データの拡張
La データ拡張 過学習を防ぐための基本的なテクニックです そしてネットワークの汎用性を向上させます。ランダムな変換を適用することで構成されます 画像をトレーニング (回転、反転、クロップ、明るさの変更) してバリエーションを作成します 新しいデータを収集せずに合成します。
Immagine Originale: Trasformazioni:
+-------+ [Flip orizzontale] --> Immagine specchiata
| Gatto | [Rotazione +-15 gradi]--> Leggera rotazione
| --o-- | [Random Crop] --> Ritaglio casuale
| /|\ | [Color Jitter] --> Variazione colori
+-------+ [Gaussian Noise] --> Aggiunta rumore
[Cutout/Erasing] --> Maschera rettangolare casuale
[MixUp] --> Media pesata di 2 immagini
[CutMix] --> Porzione di un'immagine sovrapposta
Effetto: da 50.000 immagini di training, ogni epoca vede variazioni
diverse, come se avessi milioni di immagini uniche.
8. PyTorch での実装を完了する
理論から実践に移りましょう。分類のために完全な CNN を実装します データセットの画像 CIFAR-10 (10 クラスの 60,000 枚の 32x32 画像: 飛行機、車、鳥、猫、鹿、犬、カエル、馬、船、トラック)。
8.1 セットアップとデータセット
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
# Trasformazioni con data augmentation per il training
train_transform = transforms.Compose([
transforms.RandomHorizontalFlip(p=0.5),
transforms.RandomCrop(32, padding=4),
transforms.ColorJitter(brightness=0.2, contrast=0.2),
transforms.ToTensor(),
transforms.Normalize(
mean=[0.4914, 0.4822, 0.4465], # Media CIFAR-10
std=[0.2470, 0.2435, 0.2616] # Std CIFAR-10
),
])
# Trasformazioni per il test (nessuna augmentation)
test_transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize(
mean=[0.4914, 0.4822, 0.4465],
std=[0.2470, 0.2435, 0.2616]
),
])
# Caricamento dataset
train_dataset = torchvision.datasets.CIFAR10(
root='./data', train=True, download=True, transform=train_transform
)
test_dataset = torchvision.datasets.CIFAR10(
root='./data', train=False, download=True, transform=test_transform
)
# DataLoader con batching e shuffling
train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True, num_workers=4)
test_loader = DataLoader(test_dataset, batch_size=128, shuffle=False, num_workers=4)
CLASSES = ['aereo', 'auto', 'uccello', 'gatto', 'cervo',
'cane', 'rana', 'cavallo', 'nave', 'camion']
8.2 モデルの定義
class ResidualBlock(nn.Module):
"""Blocco residuale con skip connection."""
def __init__(self, in_channels: int, out_channels: int, stride: int = 1):
super().__init__()
self.conv1 = nn.Conv2d(
in_channels, out_channels, kernel_size=3,
stride=stride, padding=1, bias=False
)
self.bn1 = nn.BatchNorm2d(out_channels)
self.conv2 = nn.Conv2d(
out_channels, out_channels, kernel_size=3,
stride=1, padding=1, bias=False
)
self.bn2 = nn.BatchNorm2d(out_channels)
self.relu = nn.ReLU(inplace=True)
# Skip connection: se le dimensioni cambiano, usa conv 1x1
self.shortcut = nn.Sequential()
if stride != 1 or in_channels != out_channels:
self.shortcut = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=1,
stride=stride, bias=False),
nn.BatchNorm2d(out_channels)
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
identity = self.shortcut(x)
out = self.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
out = out + identity # Skip connection
out = self.relu(out)
return out
class CIFAR10CNN(nn.Module):
"""CNN con blocchi residuali per classificazione CIFAR-10."""
def __init__(self, num_classes: int = 10):
super().__init__()
# Primo layer convoluzionale
self.conv1 = nn.Conv2d(3, 32, kernel_size=3, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(32)
self.relu = nn.ReLU(inplace=True)
# Blocchi residuali con profondità crescente
self.layer1 = self._make_layer(32, 64, num_blocks=2, stride=1)
self.layer2 = self._make_layer(64, 128, num_blocks=2, stride=2)
self.layer3 = self._make_layer(128, 256, num_blocks=2, stride=2)
# Classificatore finale
self.global_avg_pool = nn.AdaptiveAvgPool2d((1, 1))
self.dropout = nn.Dropout(0.3)
self.fc = nn.Linear(256, num_classes)
def _make_layer(
self, in_ch: int, out_ch: int, num_blocks: int, stride: int
) -> nn.Sequential:
layers = [ResidualBlock(in_ch, out_ch, stride)]
for _ in range(1, num_blocks):
layers.append(ResidualBlock(out_ch, out_ch, stride=1))
return nn.Sequential(*layers)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.relu(self.bn1(self.conv1(x))) # (B, 32, 32, 32)
x = self.layer1(x) # (B, 64, 32, 32)
x = self.layer2(x) # (B, 128, 16, 16)
x = self.layer3(x) # (B, 256, 8, 8)
x = self.global_avg_pool(x) # (B, 256, 1, 1)
x = x.view(x.size(0), -1) # (B, 256)
x = self.dropout(x)
x = self.fc(x) # (B, 10)
return x
8.3 トレーニングループ
def train_model(
model: nn.Module,
train_loader: DataLoader,
test_loader: DataLoader,
epochs: int = 50,
lr: float = 0.01
) -> dict:
"""Addestra il modello e restituisce la storia del training."""
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = model.to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(
model.parameters(), lr=lr, momentum=0.9, weight_decay=1e-4
)
scheduler = optim.lr_scheduler.OneCycleLR(
optimizer, max_lr=lr, epochs=epochs,
steps_per_epoch=len(train_loader)
)
history = {'train_loss': [], 'test_loss': [], 'test_acc': []}
for epoch in range(epochs):
# --- Training ---
model.train()
running_loss = 0.0
for images, labels in train_loader:
images, labels = images.to(device), labels.to(device)
optimizer.zero_grad()
outputs = model(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
scheduler.step()
running_loss += loss.item()
avg_train_loss = running_loss / len(train_loader)
# --- Evaluation ---
model.eval()
test_loss = 0.0
correct = 0
total = 0
with torch.no_grad():
for images, labels in test_loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
test_loss += loss.item()
_, predicted = outputs.max(1)
total += labels.size(0)
correct += predicted.eq(labels).sum().item()
avg_test_loss = test_loss / len(test_loader)
accuracy = 100.0 * correct / total
history['train_loss'].append(avg_train_loss)
history['test_loss'].append(avg_test_loss)
history['test_acc'].append(accuracy)
print(
f"Epoch [{epoch+1}/{epochs}] "
f"Train Loss: {avg_train_loss:.4f} | "
f"Test Loss: {avg_test_loss:.4f} | "
f"Accuracy: {accuracy:.2f}%"
)
return history
# Esecuzione
model = CIFAR10CNN(num_classes=10)
history = train_model(model, train_loader, test_loader, epochs=50, lr=0.1)
# Output atteso dopo 50 epoche con OneCycleLR:
# Test Accuracy: ~92-93%
CIFAR-10 の推奨トレーニング パラメーター
| パラメータ | 価値 | モチベーション |
|---|---|---|
| バッチサイズ | 128 | 速度と勾配の安定性のバランス |
| 学習率 | 0.1 (OneCycleLR あり) | スケジューリングを備えた SGD は優れた精度を実現します |
| 体重の減少 | 1e-4 | 過学習を防ぐための L2 正則化 |
| 時代 | 50 | OneCycleLR では、収束するには 50 エポックで十分です |
| ドロップアウト | 0.3 | 最終層の前に追加の正則化 |
9. 転移学習
実際には、CNN をゼロからトレーニングすることはほとんどありません。の 転移学習 大規模なデータセット (120 万個の ImageNet など) で事前トレーニングされたモデルを再利用できます。 の画像と 1,000 のクラス) を特定の問題に適応させます。これにより、 トレーニング時間と必要なデータ量が大幅に削減され、パフォーマンスが向上します。
9.1 特徴抽出と微調整
2 つの転移学習戦略
| 戦略 | 仕組み | いつ使用するか |
|---|---|---|
| 特徴抽出 | 事前にトレーニングされたすべてのレイヤーをフリーズし、最後の分類子のみをトレーニングします | データが少ない (画像 1,000 枚未満)、ImageNet と同様のドメイン |
| 微調整 | 最後の N 層のフリーズを解除し、低い学習率で再トレーニングします | ImageNet 以外のドメインでより多くのデータが利用可能 |
import torchvision.models as models
def create_transfer_model(
num_classes: int,
freeze_backbone: bool = True
) -> nn.Module:
"""Crea un modello con transfer learning da ResNet-18."""
# Carica ResNet-18 pre-addestrato su ImageNet
model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)
# Strategia 1: Feature Extraction (congela il backbone)
if freeze_backbone:
for param in model.parameters():
param.requires_grad = False
# Sostituisci il classificatore finale
num_features = model.fc.in_features # 512 per ResNet-18
model.fc = nn.Sequential(
nn.Dropout(0.3),
nn.Linear(num_features, 256),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(256, num_classes)
)
return model
# Feature extraction: solo il classificatore viene addestrato
feature_model = create_transfer_model(num_classes=10, freeze_backbone=True)
# Fine-tuning: tutto il modello viene riaddestrato
finetune_model = create_transfer_model(num_classes=10, freeze_backbone=False)
# Per il fine-tuning, usa un learning rate più basso
optimizer = optim.AdamW([
{'params': finetune_model.layer4.parameters(), 'lr': 1e-4},
{'params': finetune_model.fc.parameters(), 'lr': 1e-3},
], weight_decay=1e-2)
Guida al Transfer Learning:
Dati simili a ImageNet Dati diversi da ImageNet
(oggetti, animali, scene) (medico, satellite, micro)
+---------------------------+---------------------------+
Pochi dati | Feature Extraction | Feature Extraction |
(<1.000 img) | Congela tutto, addestra FC | + Aumenta data augment. |
+---------------------------+---------------------------+
Molti dati | Fine-tuning ultimi layer | Fine-tuning completo |
(>5.000 img) | LR basso per il backbone | o training da zero |
+---------------------------+---------------------------+
10. 評価指標
CNN が適切に機能するかどうかを理解するには、精度だけでは十分ではありません。不均衡なデータセットの場合 (例: 95% クラス A、5% クラス B)、常に「クラス A」を予測するモデルの精度は 95% です。 しかしそれはまったく役に立たない。より詳細な指標が必要です。
画像分類のメトリクス
| メトリック | 測定内容 | Formula |
|---|---|---|
| 正確さ | 合計に対する正しい予測の割合 | TP + TN / 合計 |
| 精度 | 肯定的な予測のうち、正しいものはいくつありますか? | TP / (TP + FP) |
| 想起 | 本当の陽性者のうち、何人が見つかったのでしょうか? | TP / (TP + FN) |
| F1 スコア | 精度と再現率の調和平均 | 2 * (P * R) / (P + R) |
Confusion Matrix (predizioni vs realta):
Predetto: aereo auto uccel gatto cervo cane rana caval nave camion
Reale:
aereo [92] 1 2 0 0 0 1 0 3 1
auto 0 [95] 0 0 0 0 0 0 1 4
uccello 3 0 [85] 3 2 2 3 1 1 0
gatto 0 1 2 [78] 1 12 3 2 0 1
cervo 1 0 3 2 [88] 1 2 3 0 0
Interpretazione:
- Diagonale = predizioni corrette (più alto = meglio)
- Fuori diagonale = errori (confusioni tra classi)
- gatto vs cane: 12 gatti classificati come cani --> classi "confuse"
from sklearn.metrics import (
classification_report,
confusion_matrix
)
import numpy as np
def evaluate_model(
model: nn.Module,
test_loader: DataLoader,
device: torch.device,
class_names: list[str]
) -> dict:
"""Valuta il modello e restituisce metriche dettagliate."""
model.eval()
all_preds = []
all_labels = []
with torch.no_grad():
for images, labels in test_loader:
images = images.to(device)
outputs = model(images)
_, predicted = outputs.max(1)
all_preds.extend(predicted.cpu().numpy())
all_labels.extend(labels.numpy())
preds_array = np.array(all_preds)
labels_array = np.array(all_labels)
# Report dettagliato per classe
report = classification_report(
labels_array, preds_array,
target_names=class_names, output_dict=True
)
# Confusion matrix
cm = confusion_matrix(labels_array, preds_array)
# Accuracy globale
accuracy = np.mean(preds_array == labels_array) * 100
print(f"Accuracy globale: {accuracy:.2f}%")
print(classification_report(
labels_array, preds_array, target_names=class_names
))
return {'accuracy': accuracy, 'report': report, 'confusion_matrix': cm}
11. デプロイメント: トレーニングされたモデルから本番環境へ
モデルのトレーニングは仕事の半分にすぎません。本番環境に導入するには、エクスポートする必要があります 最適化された形式で推論 API を作成し、すべてをコンテナ化します スケーラブルな展開のために。
11.1 モデルのエクスポート
エクスポート形式
| 形式 | 使用 | 利点 |
|---|---|---|
| トーチスクリプト | Python を使用しない推論 PyTorch | Python 依存関係なし、完全なシリアル化 |
| ONNX | ユニバーサルフォーマット、マルチフレームワーク | TensorRT、OpenVINO、CoreML と互換性あり |
| TensorRT | NVIDIA GPU 向けに最適化された推論 | ネイティブ PyTorch より最大 5 倍高速 |
import torch.onnx
def export_model(model: nn.Module, export_path: str) -> None:
"""Esporta il modello in TorchScript e ONNX."""
model.eval()
dummy_input = torch.randn(1, 3, 32, 32)
# --- TorchScript ---
scripted_model = torch.jit.trace(model, dummy_input)
scripted_model.save(f"{export_path}/model_scripted.pt")
print("TorchScript salvato.")
# --- ONNX ---
torch.onnx.export(
model,
dummy_input,
f"{export_path}/model.onnx",
input_names=['image'],
output_names=['prediction'],
dynamic_axes={
'image': {0: 'batch_size'},
'prediction': {0: 'batch_size'}
},
opset_version=17
)
print("ONNX salvato.")
export_model(model, './exports')
11.2 FastAPI を使用した推論 API
# inference_api.py
from fastapi import FastAPI, UploadFile
from PIL import Image
import torch
import torchvision.transforms as transforms
import io
app = FastAPI(title="CNN Image Classifier")
# Carica il modello TorchScript
model = torch.jit.load("./exports/model_scripted.pt")
model.eval()
CLASSES = ['aereo', 'auto', 'uccello', 'gatto', 'cervo',
'cane', 'rana', 'cavallo', 'nave', 'camion']
preprocess = transforms.Compose([
transforms.Resize((32, 32)),
transforms.ToTensor(),
transforms.Normalize(
mean=[0.4914, 0.4822, 0.4465],
std=[0.2470, 0.2435, 0.2616]
),
])
@app.post("/predict")
async def predict(file: UploadFile):
"""Classifica un'immagine caricata."""
image_bytes = await file.read()
image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
tensor = preprocess(image).unsqueeze(0)
with torch.no_grad():
outputs = model(tensor)
probabilities = torch.softmax(outputs, dim=1)
confidence, predicted = probabilities.max(1)
return {
"class": CLASSES[predicted.item()],
"confidence": round(confidence.item() * 100, 2),
"all_probabilities": {
name: round(prob.item() * 100, 2)
for name, prob in zip(CLASSES, probabilities[0])
}
}
# Esecuzione: uvicorn inference_api:app --host 0.0.0.0 --port 8000
11.3 Docker によるコンテナ化
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY exports/ ./exports/
COPY inference_api.py .
EXPOSE 8000
CMD ["uvicorn", "inference_api:app", "--host", "0.0.0.0", "--port", "8000"]
# Build: docker build -t cnn-classifier .
# Run: docker run -p 8000:8000 cnn-classifier
# Test: curl -X POST -F "file=@cat.jpg" http://localhost:8000/predict
Pipeline: Training --> Export --> Container --> Deploy
[Training] [Export] [Container] [Deploy]
PyTorch + GPU --> TorchScript/ONNX --> Docker --> Kubernetes
50 epoche ~10 MB file FastAPI Auto-scaling
~92% accuracy Ottimizzato Health checks Load balancer
No dipendenza Python GPU opzionale Monitoring
Alternativa serverless:
Export ONNX --> AWS Lambda + ONNX Runtime --> API Gateway
Pro: Pay-per-use, zero infrastruttura da gestire
Contro: Cold start (~2s), limite 250MB package
結論と次のステップ
この記事では、畳み込みニューラル ネットワークについて包括的に理解しました。 基礎(コンピュータが画像をどのように見るか、畳み込み演算)から始めて 実際の実装 (PyTorch の残留ブロックを使用した CNN) と本番環境でのデプロイメントまで (TorchScript、ONNX、FastAPI、Docker)。
私たちは、1998 年の LeNet-5 から 2022 年の ConvNeXt まで、アーキテクチャの進化がどのように起こったかを見てきました。 スキップ接続 (ResNet) などのアイデアのおかげでパフォーマンスが徐々に向上しました。 複合スケーリング (EfficientNet) とトランスフォーマーからインスピレーションを得た設計 (ConvNeXt)。
覚えておくべき重要なポイント
- CNN のエクスプロイト 地域性, 負担の分担 e 空間的不変性 画像を効率的に処理するには
- 標準アーキテクチャは、Conv + BatchNorm + ReLU + Pooling のパターンに従い、深さを増やして繰り返します。
- Le 接続をスキップする (ResNet) は勾配を消失させずに深いネットワークをトレーニングするために不可欠です
- Il 転移学習 ほとんどの場合、特にデータが少ない場合には、最初からトレーニングするよりも適しています。
- La データ増強 一般化に不可欠であり、データ収集のコストはゼロです
- 製造の場合、輸出先 ONNX o トーチスクリプト そしてDockerでコンテナ化する
シリーズの次の記事では、さらに詳しく掘り下げていきます。 転移学習と微調整: 適切な事前トレーニング済みモデルの選択方法、段階的な微調整戦略、 ドメイン適応や知識の蒸留などの高度な技術。 3番目の記事では 私たちは直面します YOLO による物体検出、物体検出システム 業界で最も使用されているリアルタイム。
追加リソース
- 元の ResNet 論文: 「画像認識のための深層残差学習」 (He et al.、2015)
- 紙のコンベンションネクスト: 「2020 年代の ConvNet」 (Liu et al.、2022)
- Paper EfficientNet: 「EfficientNet: CNN のモデル スケーリングの再考」 (Tan & Le、2019)
- PyTorch ドキュメント: CNN と Torchvision のチュートリアル
- CS231n スタンフォード: 視覚認識のための畳み込みニューラル ネットワーク (オンライン コース)
- ONNX ランタイム: 最適化されたクロスプラットフォーム推論ドキュメント







