52从 0 到 1 实现卷积神经网络--反向传播和多层神经网络实现

反向传播和多层神经网络实现

在实验开始之前,为了方便阅读,并复用之前的部分代码,我们首先将上一次试验完成的内容粘贴至此。

import numpy as np
import struct

# 读取 MNIST 数据集数据集
def read_mnist(filename):
    with open(filename, 'rb') as f:
        zero, data_type, dims = struct.unpack('>HBB', f.read(4))
        shape = tuple(struct.unpack('>I', f.read(4))[0] for d in range(dims))
        return np.frombuffer(f.read(), dtype=np.uint8).reshape(shape)

# softmax 函数
def softmax(input):
    exp_value = np.exp(input)  # 首先计算指数
    output = exp_value/np.sum(exp_value, axis=1)[:, np.newaxis]  # 然后按行标准化
    return output

# 交叉熵损失函数
class CrossEntropyLossLayer():
    def __init__(self):
        pass

    def forward(self, input, labels):
        # 做一些防止误用的措施,输入数据必须是二维的,且标签和数据必须维度一致
        assert len(input.shape) == 2, '输入的数据必须是一个二维矩阵'
        assert len(labels.shape) == 2, '输入的标签必须是独热编码'
        assert labels.shape == input.shape, '数据和标签数量必须一致'
        self.data = input
        self.labels = labels
        self.prob = np.clip(softmax(input), 1e-9, 1.0)  # 在取对数时不能为 0,所以用极小数代替 0
        loss = -np.sum(np.multiply(self.labels, np.log(self.prob))
                       )/self.labels.shape[0]
        return loss

    def backward(self):
        self.grad = (self.prob - self.labels)/self.labels.shape[0]  # 根据公式计算梯度

# 学习率衰减
class lr_scheduler(object):
    def __init__(self, base_lr, step_size, deacy_factor=0.1):
        self.base_lr = base_lr  # 最初的学习率
        self.deacy_factor = deacy_factor  # 学习率衰减因子
        self.step_count = 0  # 当前的迭代次数
        self.lr = base_lr  # 当前学习率
        self.step_size = step_size  # 步长

    def step(self, step_count=1):  # 默认 1 次
        self.step_count += step_count

    def get_lr(self):
        self.lr = self.base_lr * \
            (self.deacy_factor**(self.step_count//self.step_size))
        return self.lr

计算图 Computational Graph

在介绍深度学习最重要的反向传播算法之前,先引入一个概念: 计算图。
计算图在 TensorFlow、PyTorch 等新框架中提的比较多,很多算法都是基于计算图实现。但是像 Caffe 这种老牌框架没有计算图这个概念,而是以 Layer(层)为基本单位计算梯度、更新参数。
为什么要引入计算图?在接下来介绍完反向传播算法之后就会发现计算图这个概念有多方便理解。
首先来看一下一个简单的计算图,其中,A、x、b作为输入,y是中间变量,z是计算图最后的输出。


image.png

这其实就是一个简单的线性分类器y=Wx+b 的计算图拆分。因此计算图定义了一系列数据和在数据上的操作。

反向传播 Backpropagation

反向传播算法是深度学习的基本算法之一,多层神经网络更新参数都是基于反向传播的。而反向传播又是利用最基本的求导数的链式法则和梯度下降实现。下面,我们详细来说明反向传播过程,其中的一部分内容在上个实验已有提及。
接下来,我们使用梯度下降法更新参数,损失函数可以换算成一个关于权重W和偏置b的函数,即L(x,W,b)。


image.png

image.png

神经网络训练分为两个阶段:第一阶段为前向传播(Forward),数据在神经网络中计算,最后输出损失函数计算损失;第二阶段为反向传播(Backward),也就是本节所介绍的内容。反向传播阶段需要计算梯度,然后根据梯度更新参数。图中为了简洁省略了偏置项b的更新,但是偏置项更新和W一样。
在上图中,有三层网络,反向传播如何更新每一层的参数呢?


image.png

反向传播实现

了解反向传播的基本思想之之后,为了更好理解反向传播,本节举两个例子,分别是学术界应用较多的老牌框架 Caffe 和工业届新兴框架 PyTorch 是如何实现反向传播的。
Caffe
Caffe 是计算机视觉(Computer Vision)方向一个常用的框架,底层是以 C++ 实现,提供 C++、Python、Matlab 接口。对于反向传播,Caffe 是以层为单位的,例如卷积层、全连接层等都对应了一个 Layer 类,在这各类中保存数据和梯度等。每个 Layer 类都有 CPU 版本和 GPU 版本,分别对应一对 Forward 和 Backward 函数,其中 GPU 版本需要实现 Cuda 核函数来调用 GPU 加速计算。

image.png

下面是 Caffe 官方实现的 Sigmoid 激活函数的 CPU 代码和相应注释,是由 C++ 实现。

template 
// sigmoid 的定义
inline Dtype sigmoid(Dtype x) {
  return 0.5 * tanh(0.5 * x) + 0.5; // tanh 函数可以转化为 sigmoid 函数,这里的计算是等价的
}

// 前向传播
template 
void SigmoidLayer::Forward_cpu(const vector*>& bottom,
    const vector*>& top) {
  // 上一层的数据,使该层的输入
  const Dtype* bottom_data = bottom[0]->cpu_data();
  // 该层的数据
  Dtype* top_data = top[0]->mutable_cpu_data();
  // 输入数据的数目
  const int count = bottom[0]->count();
  // 对数据使用激活函数
  for (int i = 0; i < count; ++i) {
    // 保存到该层的数据中
    top_data[i] = sigmoid(bottom_data[i]);
  }
}

// 反向传播
template 
void SigmoidLayer::Backward_cpu(const vector*>& top,
    const vector& propagate_down,
    const vector*>& bottom) {
  // 如果需要传播,才计算梯度
  if (propagate_down[0]) {
      // 上一层的数据,和前向传播相反
    const Dtype* top_data = top[0]->cpu_data();
      // 上一层的梯度,和前向传播相反
    const Dtype* top_diff = top[0]->cpu_diff();
      // 该层的梯度
    Dtype* bottom_diff = bottom[0]->mutable_cpu_diff();
      // 该层的数量
    const int count = bottom[0]->count();
      // 计算该层的梯度
    for (int i = 0; i < count; ++i) {
        // 上一层的数据
      const Dtype sigmoid_x = top_data[i];
        // 按公式更新梯度
      bottom_diff[i] = top_diff[i] * sigmoid_x * (1. - sigmoid_x);
    }
  }
}

PyTorch
PyTorch 底层也是 C++ 甚至是 C 实现,相对于 Caffe 的优势之处在于基于计算图的自动求导机制(前面已经介绍了计算图)。PyTorch 相对于 TensorFlow 的优势则是动态图(Dynamic Graph),每次迭代都创建图,相对于 TensorFlow 更加灵活。下面是 PyTorch 动态图和根据计算图反向传播的示意图:
下面,我们利用 PyTorch 的自动求导机制解决一个简单函数的求解梯度的问题。

image.png

import torch

np_x_data = np.random.randn(2, 2)
np_y_data = np.random.randn(2, 2)
np_z_data = np.random.randn(2, 2)

# 使用 numpy 实现公式计算梯度,验证是否正确
np_gradient = np_y_data

# 使用 Pytorch 自动求导机制
tensor_x_data = torch.from_numpy(np_x_data)
tensor_y_data = torch.from_numpy(np_y_data)
tensor_z_data = torch.from_numpy(np_z_data)

# 需要计算梯度
tensor_x_data.requires_grad = True
tensor_y_data.requires_grad = True
tensor_z_data.requires_grad = True

# 实现
output = torch.sum(tensor_x_data*tensor_y_data+tensor_z_data)
output.backward()

print("公式计算结果:\n", np_gradient)
print("PyTorch 自动求导:\n", tensor_x_data.grad.numpy())

可以看出,公式求解和 PyTorch 是一致的,所以 PyTorch 的自动求导机制非常方便。实际上整个运算在 PyTorch 中是以如下形式存在的,以计算图为表现形式:


image.png

随机梯度下降 Stochastic Gradient Descent

在前面的实验训练一个简单的线性分类器时,采取的策略是一次读入全部的数据。这种方法在上一次实验是完全足够的,因为 MNIST 数据集手写体数据集不大,每张图片只有28X28个像素,一共才 60000 张图片,按每个像素8字节(双精度浮点数)计算。
但是如果数据集是彩色图片,图片大小几百甚至上千呢?这些情况在深度学习中都是非常常见的,所以不同于 NLP 和传统机器学习等方向,深度学习对计算资源消耗很大。
再来看一下损失函数的公式:


image.png

按照前面的方法,叫做全批量梯度下降。当N非常大,或者网络很深(这时候训练的参数非常多)时,直接一次性读入内存是不现实的,这时候就需要引入随机梯度下降的方法。
随机梯度下降还是采用的梯度下降更新参数,但是和梯度下降不同的是使用一个 minibatch 来估计全部数据集,minibatch 通常为 32/64/128,也就是说每次加载一个 minibatch 来更新参数。

对应到深度学习框架之中:
Caffe: 在 Caffe 中需要将数据集存储为 LMDB 或者 HDF5 格式的数据,并在训练时加载。
PyTorch: 重写 torch.utils.data.Dataset 类,并使用 torch.utils.data.DataLoader 进行加载。更多信息参考 torchvision.datasets。

接下来实现一个 Dataloader 类按 minibatch 加载数据,初始化传入数据集(为了简便不每次读图片)、batch 大小、是否打乱(一般都只针对训练集打乱数据)。
分别重写 __getitem__: 根据下标获取数据;__iter__: for 循环迭代数据;__len__: 获取数据集长度。

class Dataloader(object):
    def __init__(self, data, labels, batch_size, shuffle=True):
        # 初始数据和标签
        self.data = data
        self.labels = labels
        # 批量大小
        self.batch_size = batch_size
        # 是否打乱,默认打乱数据集,只针对训练集
        self.shuffle = shuffle

    def __getitem__(self, index):
        # 根据下标返回数据
        return self.data[index], self.labels[index]

    def __iter__(self):
        datasize = self.data.shape[0]
        # 生成迭代序列
        data_seq = np.arange(datasize)
        if self.shuffle:
            # 打乱迭代序列
            np.random.shuffle(data_seq)
        # 生成的是 Batch 序列
        interval_list = np.append(
            np.arange(0, datasize, self.batch_size), datasize)
        for index in range(interval_list.shape[0]-1):
            s = data_seq[interval_list[index]:interval_list[index+1]]
            # 返回 batch 的数据
            yield self.data[s], self.labels[s]

    def __len__(self):
        # 返回数据集长度
        return self.data.shape[0]

接下来演示如何使用该类,首先下载 MNIST 数据集手写体数据集:

!wget -nc "http://labfile.oss.aliyuncs.com/courses/1213/mnist.zip"
!unzip -o mnist.zip

按前面前面试验介绍,我们执行同样的读取数据、归一化、对标签独热编码等操作。

from sklearn.preprocessing import OneHotEncoder

# 读取并归一化数据,不归一化会导致 nan
test_data = ((read_mnist(
    'mnist/t10k-images.idx3-ubyte').reshape((-1, 784))-127.0)/255.0).astype(np.float32)
train_data = ((read_mnist(
    'mnist/train-images.idx3-ubyte').reshape((-1, 784))-127.0)/255.0).astype(np.float32)

# 独热编码标签
encoder = OneHotEncoder()
encoder.fit(np.arange(10).reshape((-1, 1)))
train_labels = encoder.transform(read_mnist(
    'mnist/train-labels.idx1-ubyte').reshape((-1, 1))).toarray().astype(np.float32)
test_labels = encoder.transform(read_mnist(
    'mnist/t10k-labels.idx1-ubyte').reshape((-1, 1))).toarray().astype(np.float32)

然后定义 Dataloader 加载数据集,batch 大小设为 120,只对训练数据打乱:

batch_size = 120  # 为了下面绘图方便,一般 batch 为 32/64/128

train_dataloader = Dataloader(
    train_data, train_labels, batch_size, shuffle=True)
test_dataloader = Dataloader(
    test_data, test_labels, batch_size, shuffle=False)

然后使用 Matplotlib 绘制一部分可视化数据:

import matplotlib.pyplot as plt
%matplotlib inline

for i in range(100):
    img, label = train_dataloader[i]
    plt.subplot(10, 10, i+1)
    plt.axis('off')
    plt.imshow(img.reshape((28, 28)))

线性层实现

前面的实验只是实现了单层的线性分类,对于深度学习中的多层神经网络,其实也只是简单地堆积多层线性分类器实现。我们已知,一个线性分类器权重的维度是 期望输入期望输出 的二维矩阵,偏置项是 1期望输出 的行向量。那么如何确定多层神经网络的权重维度呢?以此类推,其实和一层是一样的,来看下图可以更好理解。

image.png

每一个线性层所进行的操作都是:
image.png

为了可以做矩阵乘法,结合上图的多层神经网络的输入和输出,可以得出:线性层的权重的维度是 期望输入*期望输出 的二维矩阵,偏置项是 1*期望输出 的行向量的结论。
接下来使用代码实现线性层:

class Linear(object):
    def __init__(self, D_in, D_out):
        # 初始化权重和偏置的维度,高斯初始化权重,零初始化偏置
        self.weight = np.random.randn(D_in, D_out).astype(np.float32)*0.01
        self.bias = np.zeros((1, D_out), dtype=np.float32)

    def forward(self, input):
        # 前行传播保存输入数据,并做线性分类
        self.data = input
        return np.dot(self.data, self.weight)+self.bias

    def backward(self, top_grad, lr):
        # 反向传播计算梯度,前面已经介绍了如何关于输入计算梯度
        self.grad = np.dot(top_grad, self.weight.T).astype(np.float32)
        # 更新参数,最后一项为损失关于当前权重的梯度
        self.weight -= lr*np.dot(self.data.T, top_grad)
        # y=xW+b 关于 b 的偏导为 1,已经介绍过了
        self.bias -= lr*np.mean(top_grad, axis=0)

然后,根据前面介绍的 SGD 和线性层实习结合起来一起实现一个新的线性分类器,并初始化各项参数:

lr = 0.12  # 学习率
D, C = 784, 10  # 输入输出维度
np.random.seed(1)  # 固定随机生成的权重
max_iter = 20  # 最大迭代次数
step_size = 20  # 最大迭代步长

loss_layer = CrossEntropyLossLayer()  # 损失层
scheduler = lr_scheduler(lr, step_size)  # 学习率衰减

因为在下面的多层神经网络实现中也会使用到这部分代码,为了代码可重用,这里封装成一个函数。然后,分别传入各项参数,返回最后的权重、准确度和损失数值。

from tqdm.notebook import tqdm
import copy

# Net 为网络结构,需要定义 backward 和 forward 操作
def train_and_test(loss_layer, net, scheduler, max_iter, train_dataloader, test_dataloader, batch_size):
    test_loss_list, train_loss_list, train_acc_list, test_acc_list = [], [], [], []
    best_net = None
    # 最高准确度,和对应权重
    best_acc = -float('inf')
    for epoch in range(max_iter):
        # 训练
        correct = 0
        total_loss = 0
        with tqdm(total=len(train_dataloader)//batch_size+1) as pbar:
            for data, labels in train_dataloader:
                # 前向输出概率
                train_pred = net.forward(data)

                # 计算准确度
                pred_labels = np.argmax(train_pred, axis=1)
                real_labels = np.argmax(labels, axis=1)
                correct += np.sum(pred_labels==real_labels)

                # 前向输出损失
                loss = loss_layer.forward(train_pred, labels)
                total_loss += loss*data.shape[0]

                # 反向更新参数
                loss_layer.backward()
                # print(epoch, loss, correct)
                net.backward(loss_layer.grad, scheduler.get_lr())
                pbar.update(1)
            
        acc = correct/len(train_dataloader)
        print('Epoch {}/{}: train accuracy, {},  train loss: {}'.format(epoch+1, max_iter, acc, total_loss/len(train_dataloader)))
        train_acc_list.append(acc)
        train_loss_list.append(total_loss/len(train_dataloader))
        scheduler.step()
        
        # 测试
        correct = 0
        total_loss = 0
        for data, labels in test_dataloader:
            # 前向输出概率
            test_pred = net.forward(data)

            # 前向输出损失
            loss = loss_layer.forward(test_pred, labels)
            total_loss += loss*data.shape[0]

            # 计算准确度
            pred_labels = np.argmax(test_pred, axis=1)
            real_labels = np.argmax(labels, axis=1)
            correct += np.sum(pred_labels==real_labels)
        acc = correct/len(test_dataloader)
        test_acc_list.append(acc)
        test_loss_list.append(total_loss/len(test_dataloader))
        print('Epoch {}/{}: test accuracy, {},  test loss: {}'.format(epoch+1, max_iter, acc, total_loss/len(test_dataloader)))

        if acc > best_acc: 
            best_acc = acc
            best_net = copy.deepcopy(net)
    return test_loss_list, train_loss_list, train_acc_list, test_acc_list, best_net
# 线性分类器
linear_classifer = Linear(D, C)

test_loss_list, train_loss_list, train_acc_list, test_acc_list, best_net = train_and_test(
    loss_layer, linear_classifer, scheduler, max_iter, train_dataloader, test_dataloader, batch_size)
np.max(test_acc_list)  # 准确度

测试完毕后将会获得大约 92% 的测试准确度,相对于前面的全部一次性训练有了几个百分点的提高。
接下来,分别绘制训练和测试的准确度和损失曲线。

def show(max_iter, train_loss_list, test_loss_list, train_acc_list, test_acc_list):
    plt.subplot(2, 1, 1)
    plt.title('Loss')
    plt.plot(range(max_iter), train_loss_list, label='Train loss')
    plt.plot(range(max_iter), test_loss_list, label='Test loss')
    plt.legend()
    plt.subplot(2, 1, 2)
    plt.title('Accuracy')
    plt.plot(range(max_iter), train_acc_list, label='Train accuracy')
    plt.plot(range(max_iter), test_acc_list, label='Test accuracy')
    plt.legend()
    plt.subplots_adjust(hspace=0.5)

show(max_iter, train_loss_list, test_loss_list, train_acc_list, test_acc_list)

可以看到,Test loss 和 Test accuracy 很快就收敛了,这是因为只有一层神经网络的原因,需要训练的参数很少。

激活函数 Activation Function

激活函数是深度学习中一个非常重要的概念,在前面介绍反向传播时,使用了 Caffe 的激活函数实现作为例子。那么激活函数有什么用呢?
根据前面介绍的内容,可以了解到神经网络的线性层所进行的操作只是:

image.png

如果只是简单的堆叠多层神经网络,也不外乎是多个矩阵的乘法和加法。
image.png

如上图所示,两层神经网络堆叠之后,代入上述公式,最后得到的结果仍然是一个线性的。但在现实里很多实际问题其实不是简单的线性分类。因此如何解决线性不能解决的例如 异或 问题呢?
答案就是在这里介绍的激活函数,目的是为神经网络引入非线性因素。通过在神经网络输出之后接一个激活函数,将线性输出变为非线性的,这就为神经网络提供了更强的泛化能力。
在下图中,如果σ 是非线性的,重新计算之后所得到函数变得更加复杂了。
image.png

下面将介绍两个常见的激活函数: Sigmoid 和 ReLU,并实现其中的 ReLU 激活函数。
Sigmoid
Sigmoid 是一个比较早的激活函数,其公式为:
image.png

绘制出 Sigmoid 曲线图为:
image.png

image.png

但是 Sigmoid 有什么问题呢?
由前面的知识,知道权重是根据梯度进行更新的,而如果输入激活函数的x接近两端,梯度几乎为 0,再利用链式法则计算梯度时,整个网络梯度都接近于 0 了,这就是 Sigmoid 面临的最大问题,也是深度学习中常说的 梯度消失,因此 Sigmoid 函数只在 0 附近非常敏感,而在两端时几乎不更新参数。另外还有一个缺点是 Sigmoid 需要计算指数,消耗比较大。
ReLU 线性整流函数
ReLU 激活函数是深度学习中最常用的激活函数,其公式为:
image.png

绘制出 ReLU 曲线图为:
image.png

可以看出小于0的部分都被置为了0。ReLU 解决了 Sigmoid 的两个问题:
ReLU 计算比 Sigmoid 的指数计算要高效。
只有在小于0的部分才会出现梯度消失。

接下来对 ReLU 求导的过程其实很简单,小于零部分的导数为0,大于等于零部分的导数为1。
在用链式法则乘以上一层的梯度,因此在激活层的梯度则为(假设上一层梯度为topgrad,激活层输入为x):


image.png

按照前面的思想,根据该公式实现一个为 ReLU 的类,封装激活函数为一层。

class ReLU(object):
    def forward(self, input):
        self.data = input
        # 按照公式 16 实现
        return np.maximum(0, input)

    def backward(self, top_grad):
        # 根据公式 17 实现
        self.grad = (self.data > 0)*top_grad
        # relu 没有需要更新的参数

更多的激活函数,例如 tanh、Leaky ReLU、PReLU 等不再介绍。

多层神经网络实现

前面介绍并实现了线性层、激活层、交叉损失函数层,利用这三个部分,可以实现一个简单的多层神经网络。接下来,实现一个不包含输入层的三层神经网络,前两层后都接一个激活层,最后一层则作为分类器。如下图:


image.png

按上图定义一个三层网络结构,在 __init__ 中初始化网络层,然后分别定义前向传播和反向传播的顺序,即 forward 和 backward 函数,且反向传播是按照前向的相反顺序进行的。

class Net(object):

    def __init__(self):
        # 定义上图的网络结构的每层
        self.layer1 = Linear(784, 1024)
        self.relu1 = ReLU()
        self.layer2 = Linear(1024, 1024)
        self.relu2 = ReLU()
        self.classifier = Linear(1024, 10)

    def forward(self, input):
        # 定义前向传播的过程
        input = self.layer1.forward(input)
        input = self.relu1.forward(input)
        input = self.layer2.forward(input)
        input = self.relu2.forward(input)
        input = self.classifier.forward(input)
        return input

    def backward(self, top_grad, lr):
        # 反向传播和前向传播顺序相反
        self.classifier.backward(top_grad, lr)
        self.relu2.backward(self.classifier.grad)
        self.layer2.backward(self.relu2.grad, lr)
        self.relu1.backward(self.layer2.grad)
        self.layer1.backward(self.relu1.grad, lr)

完成之后进行训练,使用交叉熵损失函数。这时候由于参数较多,训练速度比一层网络要慢得多,可以稍微等一会。这就是为什么几乎所有的深度学习框架为了追求性能底层都是使用 C/C++ 实现的,而常常提供 Python 接口进行开发的原因,因为 Python 性能太差了。

lr = 0.35  # 学习率
max_iter = 7 # 最大迭代次数和步长
step_size = 5

loss_layer = CrossEntropyLossLayer()  # 损失层
scheduler = lr_scheduler(lr, step_size)  # 学习率衰减

# 训练时间较长,请耐心等等
net = Net()
test_loss_list, train_loss_list, train_acc_list, test_acc_list, best_net = train_and_test(
    loss_layer, net, scheduler, max_iter, train_dataloader, test_dataloader, batch_size)
np.max(test_acc_list)  # 准确度

上述运行完成之后将会获得接近 98% 的准确度,增大迭代次数可以提高准确度。相比于单层神经网络有提高非常多了,这是一个很棒的结果。像上面一样绘制训练和测试的准确度、损失曲线。

show(max_iter, train_loss_list, test_loss_list, train_acc_list, test_acc_list)

PyTorch 实现

PyTorch 实现了一些常用的数据集,都在 torchvision.datasets 之中说明,在此直接使用 PyTorch 提供的 API,然后使用 torch.utils.data.DataLoader。
当本地未下载数据集时,DataLoader 会自动进行下载。然后,torchvision.transforms.Compose 将一系列数据预处理方式打包。数据会被自动转为 PyTorch 的 Tensor 格式,ToTensor 会将数据归一化到[0,1]。

import os
import torchvision
import torch

if os.path.exists('processed/'):
    download = False
else:
    download = True
    
transform = torchvision.transforms.Compose([
    torchvision.transforms.ToTensor(),
])

train_dataset = torchvision.datasets.MNIST(
    root='.', download=download, transform=transform)
test_dataset = torchvision.datasets.MNIST(
    root='.', download=download, train=False, transform=transform)
torch_train_dataloader = torch.utils.data.DataLoader(
    train_dataset, batch_size=120, shuffle=True, num_workers=4)
torch_test_dataloader = torch.utils.data.DataLoader(
    test_dataset, batch_size=120, shuffle=False, num_workers=4)

train_dataset, test_dataset

然后,像前面介绍的一样定义前向传播过程,反向传播的过程 PyTorch 会自动进行,就不需要再写反向传播的代码了。这里保持和前面同样的网络,三层线性层,前两层接一个 ReLU 激活函数。

# pytorch 网络需继承 torch.nn.Module
class TorchNet(torch.nn.Module):
    def __init__(self):
        super(TorchNet, self).__init__()
        self.layer1 = torch.nn.Linear(784, 1024)
        self.relu1 = torch.nn.ReLU(inplace=True)
        self.layer2 = torch.nn.Linear(1024, 1024)
        self.relu2 = torch.nn.ReLU(inplace=True)
        self.classifier = torch.nn.Linear(1024, 10)

    def forward(self, x):
        # 前向传播过程
        x = self.layer1(x)
        x = self.relu1(x)
        x = self.layer2(x)
        x = self.relu2(x)
        x = self.classifier(x)
        return x

定义完网络结构之后,开始训练。采用 PyTorch 实现的 SGD 优化器(随机梯度下降),和 torch.optim.lr_scheduler.StepLR 是按步长衰减,衰减因子设为 0.1。

import torch.nn.functional as F
from tqdm.notebook import tqdm

torch_net = TorchNet()

# 最大迭代次数
EPOCHS = 7
# 基础学习率
base_lr = 0.02
# 步长
step_size = 5

# SGD 优化器
# 为了缩短实验时间,为 SGD 优化器加上 momentum 提高收敛速度。不需要掌握。
optimizer = torch.optim.SGD(torch_net.parameters(), lr=base_lr, momentum=0.9)
# 按步长衰减,步长为 10,衰减因子 0.1
exp_lr_scheduler = torch.optim.lr_scheduler.StepLR(
    optimizer, step_size=step_size, gamma=0.1)

test_loss_list, train_loss_list, train_acc_list, test_acc_list = [], [], [], []

for epoch in range(EPOCHS):
    # 训练模式
    torch_net.train()
    correct = 0
    train_loss = 0
    for data, labels in tqdm(torch_train_dataloader):
        # 将数据展平,前面的实验已经介绍了,一张图片展成一维的向量
        data = data.view(data.size(0), -1)
        # 前向传播
        output = torch_net(data)
        # 计算准确的和损失
        loss = F.cross_entropy(output, labels)

        # 计算准确度和损失
        pred = output.max(1, keepdim=True)[1]
        correct += pred.eq(labels.view_as(pred)).sum().item()
        train_loss += loss.item()*len(data)
        # 清空梯度,PyTorch 会保存梯度记录
        optimizer.zero_grad()
        # 反向传播
        loss.backward()
        # 更新参数
        optimizer.step()

    # 学习率衰减
    exp_lr_scheduler.step()
    train_loss /= len(torch_train_dataloader.dataset)
    acc = correct/len(torch_train_dataloader.dataset)
    
    print('Epoch {}/{}:'.format(epoch+1, EPOCHS))
    print('Train set: Average loss: {:.4f}, Accuracy: {}/{} ({:.2f}%)'.format(
        train_loss, correct, len(torch_train_dataloader.dataset),
        100. * correct / len(torch_train_dataloader.dataset)))
    
    train_loss_list.append(train_loss)
    train_acc_list.append(acc)

    # 测试模式
    torch_net.eval()
    correct = 0
    test_loss = 0
    # 测试不需要创建图
    with torch.no_grad():
        for data, labels in torch_test_dataloader:
            # 前向传播,计算准确度、损失
            data = data.view(data.size(0), -1)
            output = torch_net(data)
            test_loss += F.cross_entropy(output,
                                         labels, reduction='sum').item()
            # 计算准确度和损失
            pred = output.max(1, keepdim=True)[1]
            correct += pred.eq(labels.view_as(pred)).sum().item()
            
    test_loss /= len(torch_test_dataloader.dataset)
    acc = correct/len(torch_test_dataloader.dataset)
    print('Test set: Average loss: {:.4f}, Accuracy: {}/{} ({:.2f}%)'.format(
        test_loss, correct, len(torch_test_dataloader.dataset),
        100. * acc))
    test_loss_list.append(test_loss)
    test_acc_list.append(acc)
show(EPOCHS, train_loss_list, test_loss_list, train_acc_list, test_acc_list)
np.max(test_acc_list)

训练完成之后,同样会获得接近 98% 的准确度。
到此,本次试验的内容就全部结束了。在这次实验中,我们实现了一个简单的多层神经网络,并大大提高了模型在手写体数据集上的准确度。

PyTorch 求导和 Sigmoid 激活层实现

使用 PyTorch 自动求导求二元函数极值点

前面的课程中,我们已经知道了 PyTorch 的自动求导机制,并和 NumPy 实现的进行了对比,结果一致。接下来承接第一次挑战所完成的二元函数极小值点。
请使用 PyTorch 的自动求导机制求得以下函数的极小值:


image.png

image.png

完成该挑战时,请注意以下几点:
PyTorch 的 Tensor 如果需要计算梯度,使用 x.requires_grad = True 进行标记。
如果需要计算梯度,Tensor 分为两部分:data 和 grad,分别通过 x.data 和 x.grad 获得。
PyTorch 会记录梯度,所以每次迭代之后,都需要对梯度进行重置,例如调用 x.grad.fill_(0) 将梯度置为 0。
PyTorch 对需要计算梯度的 Tensor 不可更改,请使用 x.data 进行更改,例如需要 +1,会改变 Tensor 的数据。

你可能感兴趣的:(52从 0 到 1 实现卷积神经网络--反向传播和多层神经网络实现)