《深度学习入门》读书笔记06

作于: 2024 年 2 月 28 日,预计阅读时间 25 分钟

CNN卷积神经网络

概述

卷积神经网络是一种神经网络,它主要用来处理图像和语音。

全连接神经网络在图像处理任务中存在缺陷,因其输入是一个向量,丢失了图像的空间特征,如相邻像素之间的关系。 在图像处理任务中,卷积层可以提取图像的空间特征,如相邻像素之间的关系。

典型的 CNN 卷积神经网络如图所示。

图7-2

卷积层

书中卷积定义为:图像处理中的滤波器运算。

图7-3

输入数据为三维图像信息(channel,height,width),也称特征图, 定义卷积核(或者卷积滤波器)为二维矩阵, 卷积核的大小为(channel,kernel_height,kernel_width), 卷积核的移动步长为stride, 卷积核的填充为padding。 矩阵元素的值就是卷积核的权重。

想象将卷积核从图像的左上角开始,向右移动stride个像素,然后从卷积核的左上角开始,向下移动stride个像素,重复这个过程,直到卷积核到达图像的右下角。

对卷积核覆盖的区域,与卷积核矩阵对位元素相乘,然后求和,得到卷积结果。

图7-4

对于多通道图像的卷积如下。

图7-8

卷积层同样也有偏置值。

图7-5

卷积核的填充,指的是对输入图像四周的填充,目的是为了保证卷积结果的大小不变。

无填充的情况下,应用卷积操作后,卷积结果的大小为:height-kernel_height+1, width-kernel_width+1。 多应用几次卷积操作,卷积结果的大小会越来越小。在某个时刻,卷积结果的大小会等于1,此时无法再继续应用卷积操作。

填充的作用是,保持卷积结果的大小和原输入图像大小一致。可以保持空间大小不变的情况下把数据传给下一层。

其次是步幅stride,步幅大小影响卷积结果的大小。 带padding时,步幅大小为1,卷积结果的大小不变。 步幅大小为2时,卷积结果的大小变为原来的1/2。 步幅大小为3时,卷积结果的大小变为原来的1/3。

书中提供了一条公式:

式7-1

以上。对于多通道图像卷积我们会发现,卷积结果的通道数固定是1,也就是输出空间对比输入空间还是缩小了。 如果我们需要输出多个通道的结果,则可以定义多个卷积核。卷积核数量等于输出通道数。

图7-11

注意多个卷积核的情况下,偏置是一个三维的张量 (FN,1,1)。

图7-12

卷积层批处理

卷积层的批处理就是给输入的特征图再增加一个维度,批样本数量 N。

之后的处理就是对这个 N 个特征图进行卷积操作,输出N个数据。

图7-13

卷积层的实现

im2col

首先定义卷积层的参数,即上面提到的四维数组 (卷积核数量FN,通道数量C,卷积核高度FH,卷积核宽度FW),使用 numpy.ndarray 实现。

输入同样是四维数组, 定义为 (批样本数N,通道数量C,特征图高度H,特征图宽度W)。

卷积运算的实现用的是 im2col。其基本思路是把每个卷积运算都转换为一个矩阵的运算。参考文章 im2col方法实现卷积算法

例如对下面的 44 的输入 X,应用 33 的卷积核 W,我们需要对输入X做四次卷积运算。我们先提取四次卷积运算的参数 $X^i$

输入 X 如下

/1234
1x(1,1)x(1,2)x(1,3)x(1,4)
2x(2,1)x(2,2)x(2,3)x(2,4)
3x(3,1)x(3,2)x(3,3)x(3,4)
4x(4,1)x(4,2)x(4,3)x(4,4)

第一次卷积(X的坐标1,1到3,3的卷积运算)的参数 $X^1$ 是输入 $X$ 的一部分。

/123
1x(1,1)x(1,2)x(1,3)
2x(2,1)x(2,2)x(2,3)
3x(3,1)x(3,2)x(3,3)

将这个矩阵平坦化,转换成一个行向量,得到下面的向量。

/123456789
1x(1,1)x(1,2)x(1,3)x(2,1)x(2,2)x(2,3)x(3,1)x(3,2)x(3,3)

卷积核 W 的定义如下。

/123
1w(1,1)w(1,2)w(1,3)
2w(2,1)w(2,2)w(2,3)
3w(3,1)w(3,2)w(3,3)

同样平坦化为行向量,然后转置成只有一个列的矩阵,得到下面的矩阵。

/1
1w(1,1)
2w(1,2)
3w(1,3)
4w(2,1)
5w(2,2)
6w(2,3)
7w(3,1)
8w(3,2)
9w(3,3)

这样一个卷积运算就可以转换为一个简单的矩阵乘法运算。

式-author-1

然后全局地看,4x4 特征图对 3x3 卷积核的卷积操作,可以视为四次上述矩阵运算的结果,定义为

$$ \begin{aligned} A &= \begin{bmatrix} A^1 \ A^2 \ A^3 \ A^4 \end{bmatrix} \ &= \begin{bmatrix} X^1 \ X^2 \ X^3 \ X^4 \end{bmatrix} W\ &= \begin{bmatrix} X^1 W \ X^2 W\ X^3 W\ X^4 W\end{bmatrix} \ \end{aligned} $$

也就是四次卷积,我们可以提取出输入特征图子集构成输入矩阵 X,合并成一个矩阵乘法运算 $A=XW$。

我们尝试用 python 验证下计算过程是否符合预期。

import numpy as np
from common.util import im2col

# 输入特征图 (N,C,H,W)=(1,1,4,4)
X = np.array([[
    [[1.0, 2.0, 3.0, 4.0],
     [5.0, 6.0, 7.0, 8.0],
     [9.0, 10., 11., 12.],
     [13., 14., 15., 16.]],
]])

# 卷积核 (FN,C,FH,FW)=(1,1,3,3)
W = np.array([[1., 2., 3.],
              [4., 5., 6.],
              [7., 8., 9.]])

# im2col 卷积参数为 3x3 步长1 无填充
cols = im2col(X, 3, 3, 1, 0)
# 得到矩阵 [X^1 X^2 X^3 X^4]
print('X =', cols)

# 平坦化W再转置
Wt = np.array([W.flatten()]).T
print('W =', Wt)

# 点乘!
A = np.dot(cols, Wt)

# 结果平坦化后再 reshape 成 2x2
At = A.flatten().reshape((2, 2))
print(f'A = {At}')

# 验算过程。ndarray 切片取矩阵子集,平坦化后和卷积核W对位相乘求和。
a1 = np.sum(X[0, 0, :3, :3].flatten() * W.flatten())
a2 = np.sum(X[0, 0, :3, 1:4].flatten() * W.flatten())
a3 = np.sum(X[0, 0, 1:4, :3].flatten() * W.flatten())
a4 = np.sum(X[0, 0, 1:4, 1:4].flatten() * W.flatten())
expected = np.array([[a1, a2],
                     [a3, a4]])
print(f'expected {expected}')
assert np.allclose(At, expected)
X = [[ 1.  2.  3.  5.  6.  7.  9. 10. 11.]
 [ 2.  3.  4.  6.  7.  8. 10. 11. 12.]
 [ 5.  6.  7.  9. 10. 11. 13. 14. 15.]
 [ 6.  7.  8. 10. 11. 12. 14. 15. 16.]]
W = [[1.]
 [2.]
 [3.]
 [4.]
 [5.]
 [6.]
 [7.]
 [8.]
 [9.]]
A = [[348. 393.]
 [528. 573.]]
expected [[348. 393.]
 [528. 573.]]

对于多通道的输入,卷积核也是多通道的。多通道特征图的卷积操作和单通道的卷积操作类似。只需要把各通道的输入上下拼接成矩阵得到 $X^1$ ,然后同样平坦化。

对于多通道卷积核 W 也是一样操作,将各通道的 W 上下拼接得到矩阵 W ,然后平坦化再转置得到新的单列矩阵 $W^T$。

如下:

输入特征图通道1

/1234
1x(1,1,1)x(1,1,2)x(1,1,3)x(1,1,4)
2x(1,2,1)x(1,2,2)x(1,2,3)x(1,2,4)
3x(1,3,1)x(1,3,2)x(1,3,3)x(1,3,4)
4x(1,4,1)x(1,4,2)x(1,4,3)x(1,4,4)

输入特征图通道2

/1234
1x(2,1,1)x(2,1,2)x(2,1,3)x(2,1,4)
2x(2,2,1)x(2,2,2)x(2,2,3)x(2,2,4)
3x(2,3,1)x(2,3,2)x(2,3,3)x(2,3,4)
4x(2,4,1)x(2,4,2)x(2,4,3)x(2,4,4)

则创建的 $X^1$ 矩阵为:

/123
1x(1,1,1)x(1,1,2)x(1,1,3)
2x(1,2,1)x(1,2,2)x(1,2,3)
3x(1,3,1)x(1,3,2)x(1,3,3)
4x(2,1,1)x(2,1,2)x(2,1,3)
5x(2,2,1)x(2,2,2)x(2,2,3)
6x(2,3,1)x(2,3,2)x(2,3,3)

类似的,创建的 $W^T$ 矩阵为:

/123
1w(1,1,1)w(1,1,2)w(1,1,3)
2w(1,2,1)w(1,2,2)w(1,2,3)
3w(1,3,1)w(1,3,2)w(1,3,3)
4w(2,1,1)w(2,1,2)w(2,1,3)
5w(2,2,1)w(2,2,2)w(2,2,3)
6w(2,3,1)w(2,3,2)w(2,3,3)

多通道卷积核输出是单通道的,计算和单通道卷积核差异不大。 我们接着考虑多个卷积核的情况,即完整的卷积层 Conv(FN,C,FH,FW) 使用 im2col 实现卷积运算的思路。

依然很简单,考虑 $W^T=\begin{bmatrix}W_1 & W_2\end{bmatrix}$,也就是第一个卷积核元素放在 $W^T$ 的第一列,第二个卷积核放在第二列...

最后依然是简单的矩阵乘法运算。

为什么可以这样?考虑矩阵点积的定义,X 矩阵的 行是单个卷积操作的输入特征图子集,$W^T$ 矩阵的列 是单个卷积操作的卷积核权重。 $XW$ 运算将 X 的行乘 W 的列然后求和,即完成一个卷积运算。$W^T$ 有多个列的情况下,X的行逐个和列运算,相当于多个卷积核在同一个输入特征图上进行卷积。 最终输出的每个列,对应一个卷积核的结果。

完整的卷积层看完,最后是批量化。

批量化依然是同样的思路,例如对输入(2,2,4,4),卷积参数(2,2,3,3),把样本1和样本2的 $X^1$ 纵向拼接得到 $\begin{bmatrix}X_{1,1} \ X_{1,2} \ X_{1,3} \ X_{1,4} \ X_{2,1} \ X_{2,2} \ X_{2,3} \ X_{2,4} \ \end{bmatrix}$ ,其中 $X_{1,2}$ 表示样本 1 的第 2 步卷积操作的输入特征图子集。

创建出矩阵 $X$ 之后和卷积核矩阵 $W$ 求点积即可。

我们实现一下完整的卷积层 Conv(FN,C,FH,FW) 正向传播运算。

import numpy as np
from common.util import im2col
from common.layers import Convolution


class Conv:
    def __init__(self, fn: int, c: int, fh: int, fw: int, stride: int, padding: int, bias: float):
        self.fn = fn
        self.c = c
        self.fh = fh
        self.fw = fw
        self.stride = stride
        self.padding = padding
        self.b = bias
        self._kernels = np.random.randn(fn, c, fh, fw)
        self._w_columns = np.reshape(self.kernels, (self.fn, -1)).T
        self.x_columns = None

    @property
    def kernels(self):
        return self._kernels

    @kernels.setter
    def kernels(self, k):
        self._kernels = k
        self._w_columns = np.reshape(self.kernels, (self.fn, -1)).T

    @property
    def w_columns(self):
        return self._w_columns

    def forward(self, x):
        n, c, h, w = x.shape
        self.x_columns = im2col(x, self.fh, self.fw, self.stride, self.padding)
        out = np.dot(self.x_columns, self.w_columns) + self.b
        out_h = 1 + int((h + 2 * self.padding - self.fh) / self.stride)
        out_w = 1 + int((w + 2 * self.padding - self.fw) / self.stride)
        out = out.reshape(n, out_h, out_w, -1).transpose(0, 3, 1, 2)
        return out


# 批量输入特征图,(2,2,4,4)
x = np.array([
    [
        [[1.0, 2.0, 3.0, 4.0],  # 样本 1 通道 1
         [5.0, 6.0, 7.0, 8.0],
         [9.0, 10., 11., 12.],
         [13., 14., 15., 16.]],
        [[1.0, 2.0, 3.0, 4.0],  # 样本 1 通道 2
         [5.0, 6.0, 7.0, 8.0],
         [9.0, 10., 11., 12.],
         [13., 14., 15., 16.]],
    ],
    [
        [[1.0, 2.0, 3.0, 4.0],  # 样本 2 通道 1
         [5.0, 6.0, 7.0, 8.0],
         [9.0, 10., 11., 12.],
         [13., 14., 15., 16.]],
        [[1.0, 2.0, 3.0, 4.0],  # 样本 2 通道 2
         [5.0, 6.0, 7.0, 8.0],
         [9.0, 10., 11., 12.],
         [13., 14., 15., 16.]],
    ],
])

c = Conv(2, 2, 3, 3, 1, 0, 1.0)
# 初始化一个简单的卷积核 (2,2,3,3)
c.kernels = np.array([
    [
        [[1., 1., 1.],  # 卷积核 1 通道 1
         [1., 1., 1.],
         [1., 1., 1.]],
        [[1., 1., 1.],  # 卷积核 1 通道 2
         [1., 1., 1.],
         [1., 1., 1.]],
    ],
    [
        [[1., 1., 1.],  # 卷积核 2 通道 1
         [1., 1., 1.],
         [1., 1., 1.]],
        [[1., 1., 1.],  # 卷积核 2 通道 2
         [1., 1., 1.],
         [1., 1., 1.]],
    ],
])
a = c.forward(x)
expected = Convolution(c.kernels, c.b).forward(x)
print(a)
assert np.allclose(a, expected), 'forward result not equals to expected'
[[[[109. 127.]
   [181. 199.]]

  [[109. 127.]
   [181. 199.]]]


 [[[109. 127.]
   [181. 199.]]

  [[109. 127.]
   [181. 199.]]]]

文中实现最为魔术的地方就是对 outreshapetranspose 了。

要理解 out = out.reshape(n, out_h, out_w, -1).transpose(0, 3, 1, 2) 这句代码需要从批量卷积,点积结果矩阵的构成说起。

我们知道单张输入特征图 (C,H,W) 在点积运算中体现为矩阵 X 的一行,单个卷积核 (C,FH,FW) 在点积运算中体现为矩阵 W 的一列。 而最终结果矩阵 A 的点是单张特征图对单个卷积核的卷积结果,矩阵 A 的一个行对应输入的一张特征图,矩阵 A 的一个列则对应一个卷积核。

而我们预期的卷积层输出结果是 (N,C,H,W) 这样一个四维数组,可以作为下一个卷积层的输入。结果矩阵 A 则是 (样本数N,样本多个卷积核的卷积结果C)。

注意 A 的行实际是多个卷积核卷积结果横向连接起来的,单个卷积核的结果又是 (H,W) 矩阵平坦化组成的。也就是:

/12345678
1a(1,1,1)a(1,1,2)a(1,2,1)a(1,2,2)a(2,1,1)a(2,1,2)a(2,2,1)a(2,2,2)

其中 a(1,2,1) 表示卷积核 1 输出的第 2 行第 1 列

所以我们先进行 reshape 操作,目的是把卷积结果 C 转换成矩阵形式 (H,W),因此 reshape 参数是 (n,out_h,out_w,-1), 即把矩阵 A 拆成一个四维数组 (N,OH,OW,C),此时 C 维度的大小为 2,2 个元素分别表示两个卷积核在 (N,OH,OW) 上的卷积结果。

然后进行 transpose 操作调整轴的顺序,这是个非常魔术的过程。这里直接看书中图示。

图7-20

transpose 后的多维数组,我们对各个维度的解读发生了改变。如图所示,transpose 后得到的是 (N,C,OH,OW) 的数组, 其中 N 表示样本数,C 对应卷积核个数(输出通道数),OH 表示输出特征图的高度,OW 表示输出特征图的宽度。

反向传播

卷积层的反向传播书中说法是和 Affine 层的反向传播实现类似。

我们知道 Affine 层的反向传播计算公式是

$$ \begin{aligned} \frac{\partial L}{\partial X}&=\frac{\partial L}{\partial Y} \times W^T \ \frac{\partial L}{\partial W}&=\frac{\partial L}{\partial Y} \times X^T \ \end{aligned} $$

Affine 层的正向传播中,$X$ 的每个行对应样本,$W$ 的每个列对应神经元。这与 Convolution 层很相似。Convolution 层的 $W$ 每个列对应卷积核。

Convolution 层和 Affine 层正向传播的差异在于 Convolution 层的卷积核和输入特征图需要先变换成矩阵形式再参与运算。 影响是,预期传入的 dout 会是一个四维数组 (N,C,H,W) 元素对应 $W$ 矩阵元素的偏导数。 在计算前,要先把 dout 转换为矩阵形式,同样的 $W$ 也要转为矩阵,然后用 Affine 的反向传播公式求关于 $W$ 和 $X$ 的梯度。 最后将求得的关于 $X$ 的梯度转为四维数组 (N,C,H,W) 形式返回,保存 $\frac{\partial L}{\partial W}$。

我实在懒得再写一遍了,以后精读的时候可能再针对每个层都进行详细分析。迫不及待想刷完这书再去看下一本进阶了。 这里就略过反向传播的实现细节了。

池化层

池化层是缩小空间的运算。

和卷积层一样有大小和步长参数,但没有卷积核(权重),只单纯对输入压缩。

书中例子如下:

图7-14

常见的池化层有最大池化和平均池化两种。最大池化是取池化窗口内的最大值,平均池化是取池化窗口内的平均值。

正向传播实现

池化层的正向传播和卷积层类似,区别在于不需要把一个样本的所有卷积参数 $X^i$ 平坦化到一个行,而是每个 $X^i$ 一个行。 把不同通道的输入特征图拆出来的 $X^i$ 上下连接起来。

书中图示比较清楚:

图7-21

处理完之后按行池化,得到结果,再 reshape 回 4 维数组 (N,C,H,W) 即可。注意池化层输入多少通道输出就是多少通道。

反向传播实现

池化层的反向传播和 ReLU 类似,例如最大池化,除最大元素外其他元素在池化后是置0的,也就是这些位置的系数为0,偏导数为0。

具体实现细节这里略。

使用 pytorch 实现 CNN

从这里就偏离原书了,直接上 pytorch 玩玩。

import torch
from torch.utils.data import DataLoader
from torchvision import datasets
from torchvision.transforms import ToTensor

# Download training data from open datasets.
training_data = datasets.MNIST(root="data", train=True, download=True, transform=ToTensor())
# Download test data from open datasets.
test_data = datasets.MNIST(root="data", train=False, download=True, transform=ToTensor())
# batch size
batch_size = 64

# Create data loaders.
train_dataloader = DataLoader(training_data, batch_size=batch_size)
test_dataloader = DataLoader(test_data, batch_size=batch_size)

device = 'cuda' if torch.cuda.is_available() else 'cpu'


class MyNetwork(torch.nn.Module):
    def __init__(self):
        super().__init__()
        self.flatten = torch.nn.Flatten()
        self.linear = torch.nn.Sequential(
            torch.nn.Conv2d(1, 30, 5, 1, 0),
            torch.nn.ReLU(),
            torch.nn.MaxPool2d(2, 1),
            torch.nn.Flatten(), # 为了把 Convolution/Pooling 层的输出和 Affine 层输入串起来,需要先把 Convolution/Pooling 层输出的 4D 数组平坦化
            torch.nn.Linear(15870, 100),
            torch.nn.ReLU(),
            torch.nn.Linear(100, 10),
            torch.nn.Softmax(),
        )

    def forward(self, x):
        return self.linear(x)


def train(dataloader, model, loss_fn, optimizer):
    size = len(dataloader.dataset)
    model.train()
    for batch, (X, y) in enumerate(dataloader):
        X, y = X.to(device), y.to(device)
        X = torch.reshape(X, (X.shape[0], 1, 28, 28))

        # Compute prediction error
        pred = model(X)
        loss = loss_fn(pred, y)

        # Backpropagation
        loss.backward()
        optimizer.step()
        optimizer.zero_grad()

        if batch % 100 == 0:
            loss, current = loss.item(), (batch + 1) * len(X)
            print(f"loss: {loss:>7f}  [{current:>5d}/{size:>5d}]")


def test(dataloader, model, loss_fn):
    size = len(dataloader.dataset)
    num_batches = len(dataloader)
    model.eval()
    test_loss, correct = 0, 0
    with torch.no_grad():
        for X, y in dataloader:
            X = torch.reshape(X, (X.shape[0], 1, 28, 28))
            X, y = X.to(device), y.to(device)
            pred = model(X)
            test_loss += loss_fn(pred, y).item()
            correct += (pred.argmax(1) == y).type(torch.float).sum().item()
    test_loss /= num_batches
    correct /= size
    print(f"Test Error: \n Accuracy: {(100 * correct):>0.1f}%, Avg loss: {test_loss:>8f} \n")


n = MyNetwork().to(device)
loss_fn = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(n.parameters(), lr=0.01)
epochs = 10

for t in range(epochs):
    print(f"Epoch {t + 1}\n-------------------------------")
    train(train_dataloader, n, loss_fn, optimizer)
    test(test_dataloader, n, loss_fn)
print("Done!")
Files already downloaded
Files already downloaded
Epoch 1
-------------------------------


F:\repos\deep-learning\venv\lib\site-packages\torch\nn\modules\module.py:1511: UserWarning: Implicit dimension choice for softmax has been deprecated. Change the call to include dim=X as an argument.
  return self._call_impl(*args, **kwargs)


loss: 2.301144  [   64/60000]
loss: 2.279937  [ 6464/60000]
loss: 2.266119  [12864/60000]
loss: 2.148455  [19264/60000]
loss: 2.036764  [25664/60000]
loss: 1.925048  [32064/60000]
loss: 1.805400  [38464/60000]
loss: 1.857848  [44864/60000]
loss: 1.821546  [51264/60000]
loss: 1.758208  [57664/60000]
Test Error: 
 Accuracy: 74.0%, Avg loss: 1.750762 

Epoch 2
-------------------------------
loss: 1.735336  [   64/60000]
loss: 1.741934  [ 6464/60000]
loss: 1.733680  [12864/60000]
loss: 1.639817  [19264/60000]
loss: 1.652467  [25664/60000]
loss: 1.673727  [32064/60000]
loss: 1.586213  [38464/60000]
loss: 1.640192  [44864/60000]
loss: 1.644729  [51264/60000]
loss: 1.602930  [57664/60000]
Test Error: 
 Accuracy: 89.7%, Avg loss: 1.588926 

Epoch 3
-------------------------------
loss: 1.579320  [   64/60000]
loss: 1.579364  [ 6464/60000]
loss: 1.563474  [12864/60000]
loss: 1.592638  [19264/60000]
loss: 1.576006  [25664/60000]
loss: 1.623149  [32064/60000]
loss: 1.539580  [38464/60000]
loss: 1.600444  [44864/60000]
loss: 1.608010  [51264/60000]
loss: 1.577690  [57664/60000]
Test Error: 
 Accuracy: 91.1%, Avg loss: 1.566124 

Epoch 4
-------------------------------
loss: 1.552503  [   64/60000]
loss: 1.560335  [ 6464/60000]
loss: 1.546871  [12864/60000]
loss: 1.581722  [19264/60000]
loss: 1.556692  [25664/60000]
loss: 1.605762  [32064/60000]
loss: 1.528894  [38464/60000]
loss: 1.589878  [44864/60000]
loss: 1.591401  [51264/60000]
loss: 1.566346  [57664/60000]
Test Error: 
 Accuracy: 91.6%, Avg loss: 1.555532 

Epoch 5
-------------------------------
loss: 1.538584  [   64/60000]
loss: 1.551226  [ 6464/60000]
loss: 1.536254  [12864/60000]
loss: 1.573621  [19264/60000]
loss: 1.545673  [25664/60000]
loss: 1.588099  [32064/60000]
loss: 1.522446  [38464/60000]
loss: 1.584719  [44864/60000]
loss: 1.583256  [51264/60000]
loss: 1.560601  [57664/60000]
Test Error: 
 Accuracy: 92.3%, Avg loss: 1.548232 

Epoch 6
-------------------------------
loss: 1.529052  [   64/60000]
loss: 1.545700  [ 6464/60000]
loss: 1.527557  [12864/60000]
loss: 1.567937  [19264/60000]
loss: 1.539456  [25664/60000]
loss: 1.574068  [32064/60000]
loss: 1.516613  [38464/60000]
loss: 1.580732  [44864/60000]
loss: 1.576618  [51264/60000]
loss: 1.558007  [57664/60000]
Test Error: 
 Accuracy: 92.7%, Avg loss: 1.542547 

Epoch 7
-------------------------------
loss: 1.524871  [   64/60000]
loss: 1.541902  [ 6464/60000]
loss: 1.519567  [12864/60000]
loss: 1.564264  [19264/60000]
loss: 1.534474  [25664/60000]
loss: 1.562087  [32064/60000]
loss: 1.507874  [38464/60000]
loss: 1.578054  [44864/60000]
loss: 1.566317  [51264/60000]
loss: 1.555856  [57664/60000]
Test Error: 
 Accuracy: 93.1%, Avg loss: 1.537830 

Epoch 8
-------------------------------
loss: 1.522017  [   64/60000]
loss: 1.538967  [ 6464/60000]
loss: 1.513827  [12864/60000]
loss: 1.560773  [19264/60000]
loss: 1.529202  [25664/60000]
loss: 1.553353  [32064/60000]
loss: 1.500312  [38464/60000]
loss: 1.574624  [44864/60000]
loss: 1.555169  [51264/60000]
loss: 1.554117  [57664/60000]
Test Error: 
 Accuracy: 93.5%, Avg loss: 1.533476 

Epoch 9
-------------------------------
loss: 1.519567  [   64/60000]
loss: 1.536195  [ 6464/60000]
loss: 1.510207  [12864/60000]
loss: 1.557672  [19264/60000]
loss: 1.524510  [25664/60000]
loss: 1.547272  [32064/60000]
loss: 1.495671  [38464/60000]
loss: 1.570410  [44864/60000]
loss: 1.543279  [51264/60000]
loss: 1.552659  [57664/60000]
Test Error: 
 Accuracy: 93.8%, Avg loss: 1.529591 

Epoch 10
-------------------------------
loss: 1.517417  [   64/60000]
loss: 1.534307  [ 6464/60000]
loss: 1.507608  [12864/60000]
loss: 1.554423  [19264/60000]
loss: 1.520418  [25664/60000]
loss: 1.542946  [32064/60000]
loss: 1.493244  [38464/60000]
loss: 1.565224  [44864/60000]
loss: 1.534792  [51264/60000]
loss: 1.551990  [57664/60000]
Test Error: 
 Accuracy: 94.0%, Avg loss: 1.526247 

Done!

/深度学习/ /python/ /numpy/ /matplotlib/