<jupyter_start><jupyter_text>畳み込みニューラル・ネットワーク前のユニットでは、クラス定義を使って多層ニューラルネットワークを定義する方法を学びましたが、それらのネットワークは一般的なもので、コンピュータビジョンのタスクに特化したものではありませんでした。このユニットでは、コンピュータ・ビジョンに特化して設計された**畳み込みニューラルネットワーク（CNN）**について学びます。コンピュータビジョンは、一般的な分類とは異なります。なぜなら、写真の中の特定のオブジェクトを見つけようとするとき、私たちは画像をスキャンして、特定の**パターン**とその組み合わせを探しているからです。例えば、猫を探すときには、まず横線を探し、それがひげになり、ひげのある組み合わせによって猫の写真であることがわかるのです。あるパターンの相対的な位置と存在が重要であり、画像上の正確な位置ではない。パターンを抽出するために、**convolutional filters**という概念を使います。しかしその前に、前のユニットで定義したすべての依存関係と関数をロードしましょう。<jupyter_code>import torch
import torch.nn as nn
import torchvision
import matplotlib.pyplot as plt
from torchinfo import summary
import numpy as np

from pytorchcv import load_mnist, train, plot_results, plot_convolution, display_dataset
load_mnist(batch_size=128)<jupyter_output><empty_output><jupyter_text>Convolutional filters畳み込みフィルターとは、画像の各ピクセルの上を走り、隣接するピクセルの加重平均を計算する小さな窓のことです。畳み込みフィルタは、重み係数の行列で定義されます。それでは、MNISTの手書き数字に2種類の畳み込みフィルタを適用した例を見てみましょう。<jupyter_code>plot_convolution(torch.tensor([[-1.,0.,1.],[-1.,0.,1.],[-1.,0.,1.]]),'Vertical edge filter')
plot_convolution(torch.tensor([[-1.,-1.,-1.],[0.,0.,0.],[1.,1.,1.]]),'Horizontal edge filter')<jupyter_output><empty_output><jupyter_text>最初のフィルターは、**垂直エッジフィルター**と呼ばれ、以下の行列で定義されます。$$\left(    \begin{matrix}     -1 & 0 & 1 \cr     -1 & 0 & 1 \cr     -1 & 0 & 1 \cr    \end{matrix}\right)$$このフィルターが比較的均一なピクセルフィールドを通過するときには、すべての値の合計は0になりますが、画像の垂直方向のエッジに遭遇すると、高いスパイク値が生成されます。上の画像では、垂直方向のエッジは高い値と低い値で表され、水平方向のエッジは平均化されているのはこのためです。水平方向のエッジフィルターをかけると、逆に水平方向のラインが増幅され、垂直方向は平均化されます。従来のコンピュータビジョンでは、複数のフィルタを画像に適用して特徴量を生成し、それを機械学習アルゴリズムで分類器を構築していました。しかし、深層学習では、分類問題を解決するために、最適な畳み込みフィルターを **学習** するネットワークを構築します。そのために導入したのが**畳み込み層**です。  畳み込み層畳み込み層は、`nn.Conv2d`構文を使って定義します。以下のように指定する必要があります。* `in_channels` - 入力チャンネルの数．ここでは，グレースケール画像を扱うので，入力チャンネル数は 1 です．* `out_channels` - 使用するフィルタの数．ここでは9種類のフィルタを使用します。これにより、どのフィルタがシナリオに最適なのかをネットワークが探索する機会が多くなります。* `kernel_size` は，スライディングウィンドウのサイズである．通常，3x3 または 5x5 のフィルタが使用される．最も単純なCNNは1つの畳み込み層を持つ。入力サイズが28x28の場合、9個の5x5フィルタを適用すると、最終的に9x24x24のテンソルができあがります（長さ5のスライディング・インターバルが28ピクセルに収まる位置は24箇所しかないので、空間的なサイズは小さくなります）。畳み込みの後、9x24x24のテンソルを5184サイズの1つのベクトルに平坦化し、線形層を追加して、10のクラスを生成しています。また、層間には活性化関数「relu」を使用しています。<jupyter_code>class OneConv(nn.Module):
    def __init__(self):
        super(OneConv, self).__init__()
        self.conv = nn.Conv2d(in_channels=1,out_channels=9,kernel_size=(5,5))
        self.flatten = nn.Flatten()
        self.fc = nn.Linear(5184,10)

    def forward(self, x):
        x = nn.functional.relu(self.conv(x))
        x = self.flatten(x)
        x = nn.functional.log_softmax(self.fc(x),dim=1)
        return x

net = OneConv()

summary(net,input_size=(1,1,28,28))<jupyter_output><empty_output><jupyter_text>このネットワークの学習可能なパラメータは約50kであるのに対し，完全に連結された多層ネットワークでは約80kであることがわかります．畳み込みネットワークの方が一般化しやすいため、小さなデータセットでも良い結果を得ることができます。.<jupyter_code>hist = train(net, train_loader, test_loader, epochs=5)
plot_results(hist)<jupyter_output>Epoch  0, Train acc=0.943, Val acc=0.973, Train loss=0.001, Val loss=0.001
Epoch  1, Train acc=0.978, Val acc=0.977, Train loss=0.001, Val loss=0.001
Epoch  2, Train acc=0.984, Val acc=0.976, Train loss=0.000, Val loss=0.001
Epoch  3, Train acc=0.987, Val acc=0.978, Train loss=0.000, Val loss=0.001
Epoch  4, Train acc=0.989, Val acc=0.973, Train loss=0.000, Val loss=0.001<jupyter_text>ご覧のように、前のユニットの完全に接続されたネットワークに比べて、より高い精度を、より速く達成することができます。また、学習した畳み込み層の重みを可視化することで、何が起こっているのかをより理解することができます。<jupyter_code>fig,ax = plt.subplots(1,9)
with torch.no_grad():
    p = next(net.conv.parameters())
    for i,x in enumerate(p):
        ax[i].imshow(x.detach().cpu()[0,...])
        ax[i].axis('off')<jupyter_output><empty_output><jupyter_text>斜め方向のストロークを認識できそうなフィルターもあれば、かなり適当なフィルターもあることがわかります。 マルチレイヤーCNNとプーリングレイヤー最初の畳み込み層では、水平線や垂直線などの原始的なパターンを探しますが、その上にさらに畳み込み層を重ねることで、原始的な形状などのより高度なパターンを探すことができます。そして、さらに畳み込み層を重ねることで、それらの図形を画像の一部に合成し、最終的に分類しようとしている物体にまで仕上げることができます。その際には、画像の空間的なサイズを小さくするという方法もあります。3x3のスライドウィンドウ内に水平方向のストークがあることを検出したら、どのピクセルで発生したかはそれほど重要ではありません。そのため、画像のサイズを「縮小」することができます。これには、**pooling layer**を使用します。* **Average Pooling** スライディングウィンドウ（例えば、2x2ピクセル）を取り、ウィンドウ内の値の平均を計算します。* **Max Pooling** ウィンドウを最大値に置き換えます。マックスプーリングの考え方は，スライディングウィンドウの中に特定のパターンが存在することを検出することです。したがって、典型的なCNNでは、いくつかの畳み込み層を設け、その間にプーリング層を設けて画像のサイズを小さくします。また、フィルターの数も増やします。なぜなら、パターンが高度になればなるほど、探さなければならない興味深い組み合わせの可能性が増えるからです。![プーリング層を持つ複数の畳み込み層を示した画像](./images/cnn-pyramid.png)このアーキテクチャは、空間的な次元が減少し、機能/フィルタの次元が増加することから、**ピラミッド・アーキテクチャ**とも呼ばれます。<jupyter_code>class MultiLayerCNN(nn.Module):
    def __init__(self):
        super(MultiLayerCNN, self).__init__()
        self.conv1 = nn.Conv2d(1, 10, 5)
        self.pool = nn.MaxPool2d(2, 2)
        self.conv2 = nn.Conv2d(10, 20, 5)
        self.fc = nn.Linear(320,10)

    def forward(self, x):
        x = self.pool(nn.functional.relu(self.conv1(x)))
        x = self.pool(nn.functional.relu(self.conv2(x)))
        x = x.view(-1, 320)
        x = nn.functional.log_softmax(self.fc(x),dim=1)
        return x

net = MultiLayerCNN()
summary(net,input_size=(1,1,28,28))<jupyter_output><empty_output><jupyter_text>この定義について、いくつかの点に注意してください。* `Flatten` レイヤを使用する代わりに、`forward` 関数内のテンソルを `view` 関数を使用してフラット化しています。平坦化層は学習可能な重みを持たないため、クラス内に別の層のインスタンスを作成する必要はありません。* このモデルでは、プーリング層のインスタンスを1つだけ使用していますが、これは学習可能なパラメータが含まれていないためで、この1つのインスタンスは効果的に再利用することができます。* 学習可能なパラメータの数（～8.5K）は、これまでの例に比べて劇的に少なくなっています。これは、一般的に畳み込み層はパラメータが少なく、最終的に高密度の層を適用する前の画像の次元が大幅に減少するためです。パラメータの数が少ないと、データセットのサイズが小さくてもオーバーフィッティングを防ぐことができるため、モデルに良い影響を与えます。<jupyter_code>hist = train(net, train_loader, test_loader, epochs=5)<jupyter_output>Epoch  0, Train acc=0.948, Val acc=0.979, Train loss=0.001, Val loss=0.001
Epoch  1, Train acc=0.982, Val acc=0.984, Train loss=0.000, Val loss=0.000
Epoch  2, Train acc=0.985, Val acc=0.983, Train loss=0.000, Val loss=0.000
Epoch  3, Train acc=0.986, Val acc=0.983, Train loss=0.000, Val loss=0.000
Epoch  4, Train acc=0.986, Val acc=0.983, Train loss=0.000, Val loss=0.001<jupyter_text>ここで注目していただきたいのは、1層だけの場合よりも高い精度で、しかも1〜2回のエポックで、より高速に処理できることです。つまり、洗練されたネットワーク・アーキテクチャでは、何が起こっているのかを把握し、画像から一般的なパターンを抽出するために必要なデータ数が非常に少なくなるということです。 CIFAR-10データセットの実際の画像で試す手書きの数字を認識する問題は、一見おもちゃのように見えますが、ここからはもっと本格的な問題に取り組みます。[CIFAR-10](https://www.cs.toronto.edu/~kriz/cifar.html)と呼ばれる、さまざまな物体の写真を集めたより高度なデータセットを調べてみましょう。<jupyter_code>transform = torchvision.transforms.Compose(
    [torchvision.transforms.ToTensor(),
     torchvision.transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

trainset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=14, shuffle=True)
testset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=14, shuffle=False)
classes = ('plane', 'car', 'bird', 'cat',
           'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
display_dataset(trainset,classes=classes)<jupyter_output><empty_output><jupyter_text>CIFAR-10用の有名なアーキテクチャは[LeNet](https://en.wikipedia.org/wiki/LeNet)と呼ばれるもので、*Yann LeCun*氏によって提案されました。このアーキテクチャは、上述した原理と同じですが、主な違いは、入力カラーチャンネルが1ではなく3であることです。出力活性化関数として`log_softmax`を使わず、最後の完全連結層の出力を返すようにしています。この場合、モデルを最適化するために `CrossEntropyLoss` 損失関数を使用することができます。<jupyter_code>class LeNet(nn.Module):
    def __init__(self):
        super(LeNet, self).__init__()
        self.conv1 = nn.Conv2d(3, 6, 5)
        self.pool = nn.MaxPool2d(2)
        self.conv2 = nn.Conv2d(6, 16, 5)
        self.conv3 = nn.Conv2d(16,120,5)
        self.flat = nn.Flatten()
        self.fc1 = nn.Linear(120,64)
        self.fc2 = nn.Linear(64,10)

    def forward(self, x):
        x = self.pool(nn.functional.relu(self.conv1(x)))
        x = self.pool(nn.functional.relu(self.conv2(x)))
        x = nn.functional.relu(self.conv3(x))
        x = self.flat(x)
        x = nn.functional.relu(self.fc1(x))
        x = self.fc2(x)
        return x

net = LeNet()

summary(net,input_size=(1,3,32,32))<jupyter_output><empty_output><jupyter_text>Training this network properly will take significant amount of time, and should preferably be done on GPU-enabled compute.<jupyter_code>opt = torch.optim.SGD(net.parameters(),lr=0.001,momentum=0.9)
hist = train(net, trainloader, testloader, epochs=3, optimizer=opt, loss_fn=nn.CrossEntropyLoss())<jupyter_output>Epoch  0, Train acc=0.246, Val acc=0.390, Train loss=0.145, Val loss=0.118
{'fraction_non_alphanumeric': 0.13222844878979373, 'fraction_numerical': 0.04840825086443305, 'mean_word_length': 7.19941348973607, 'pattern_counts': {'":': 0, '<': 38, '<?xml version=': 0, '>': 38, 'https://': 2, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '10', 'dir': 'jupyter-structured-clean-dedup', 'id': '451182', 'license': 'CC-BY-4.0', 'n_tokens_mistral': 5538, 'n_tokens_neox': 5073, 'n_words': 376, 'path': 'Introduction-to-Computer-Vision-with-PyTorch/05.Use_a_convolutional_neural_network.ipynb', 'repo_name': 'SchwarzKatzeZwei/MSLearn-to-PyTorch', 'size': '8387'}