51从 0 到 1 实现卷积神经网络--从线性分类到深度学习

从线性分类到深度学习

机器学习、人工智能火了之后,大批新人涌入这个行业,尤其以深度学习为主的方向更为火爆。深度学习应用在人脸识别、姿态估计等领域取得了比传统视觉方法优秀得多的性能表现。现如今也涌现出许多优秀的深度学习和机器学习框架,例如 TensorFlow、PyTorch、PaddlePaddle 等。
但是这些框架都是直接将算法封装成 API 提供给人使用,虽然这样做使开发非常便利,但是大部分人其实对底层算法的实现并不了解、基础薄弱,也就是被很多人诟病的 “调包侠”。
本课程将致力于了解深度学习的基本算法介绍,并使用 NumPy 实现所介绍的算法,现学现用,真正了解框架底层的算法是如何运作的。

线性拟合

机器学习基本概念有很多种定义,最著名也是使用最广泛的是 Mitchell 于 1997 年提出的: 对于某类任务 T 和性能度量 P,如果计算机程序在 T 上以 P 衡量的性能随着经验E而自我完善,那么就称这个计算机程序从经验 E 学习。
接下来举一个简单的例子帮助理解这个概念。
线性拟合是初中的知识,原理非常、非常简单,就是用一条直线拟合一些点,并使得所有点到直线的距离之和最短。以下是一个示意图,使用蓝线拟合红点:


image.png

假设有N个点,每个点的坐标为(xi,yi),直线的公式是y=ax+b,所以线性拟合的目的是找到a和b使得所有点到直线的距离之和最短。
复习一下点到直线的距离公式:


image.png

再对所有点到直线的距离求和可以表示为:
image.png

那么最终的目的,则是求得a和b使得L最小。

回顾机器学习的概念,可以将整个线性拟合的过程对应到这些概念之中:
经验 E: 线性拟合中给出的这些点(红点)。
某类任务 T: 使用蓝线拟合红点,求得最优的a和b。
衡量的性能 P: 所有点到直线的距离之和最短,即使得L最小。

再次更抽象地对应到深度学习,称这些点为数据集,拟合过程为训练、构建模型,L为损失函数(Loss Function)或者误差函数(Error Function)、目标函数(Target Function)。

线性分类器

介绍完线性拟合之后,大致对深度学习的一些概念有了初步的了解。接下来根据线性拟合,可以引申出一个非常简单的分类器,线性分类器。
线性分类器的公式和线性拟合类似,只是线性分类器是一个多元函数,而线性拟合只是一元函数,是线性分类中最简单的一种。它的基本公式:

image.png

当输入和输出有大于等于 2 个时,W和b都为矩阵。在接下来的课程学习中,你会发现非常神奇的一件事,神经网络的基础之一就是这个最简单的公式。
为了内容更加简洁和方便叙述,接下来的线性分类器将会舍去偏置项b,而使用更简单的形式:
image.png

既然W是矩阵,那么如何确定W的维度呢?
首先假设数据集中有N个数据,每个数据有具体的 mm 项,即X=x1 ,x2 ,…,xm ,那么训练数据X则为一个N行m列的矩阵。
假设输出的类别有C个,期望输出的y为每一个类别的概率,即Y=y1,y2,…,yC,那么输出Y则为一个N行C列的矩阵,每一行对应一个样本的C个类别的概率。
image.png

image.png

接下来使用 MNIST 数据集数据集作为一个具体的实例进行讲解。
mnist 又叫手写体数据集,里面包含了从 0 到 9 的28∗28 的灰度图片,是深度学习中常用的一个数据集。其中训练集有 60000 个,测试集有 10000 个,0 到 9 的数字均匀分布。
对应到前面的叙述,则训练集C=10, N=60000,测试集C=10, N=10000。

# 下载手写体数据集
!wget -nc "http://labfile.oss.aliyuncs.com/courses/1213/mnist.zip"
!unzip -o mnist.zip

接下来,加载数据集:

import struct
import numpy as np


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)


mnist_test_data = read_mnist('mnist/t10k-images.idx3-ubyte')
mnist_train_data = read_mnist('mnist/train-images.idx3-ubyte')
mnist_train_labels = read_mnist('mnist/train-labels.idx1-ubyte')
mnist_test_labels = read_mnist('mnist/t10k-labels.idx1-ubyte')

我们可以通过绘图来看一下 MNIST 数据集数据集具体的样子。

import matplotlib.pyplot as plt
%matplotlib inline

plt.imshow(mnist_test_data[0, :, :])

读取完成之后,我们需要对数据预处理,主要完成 2 个操作:
对数据集的标签0,1,…,9 独热编码。
将数据展平并归一化。

one-hot(独热)编码指的是使用一个只有一位为 1 的二进制向量编码标签,下面独热编码一些数字,更为直观:

from sklearn.preprocessing import OneHotEncoder


labels = [[1], [2], [3]]  # 待编码的标签
encoder = OneHotEncoder()
encoder.fit_transform(labels).toarray()

由上可见,1 被 [1, 0, 0] 替换,2 被 [0, 1, 0] 替换,3 被 [0, 0, 1] 替换。深度学习中使用独热编码来表示真实概率,因为独热编码的结果只有一位为 1,这代表对应的标签真实概率为 100%。
展平操作如下图所示,我们将 MNIST 数据集数据集每个28∗28 的灰度图展平成一个一维向量,以输入线性分类器中。


image.png
encoder.fit(np.arange(10).reshape((-1, 1)))  # 生成标签并独热编码

mnist_test_labels = encoder.transform(np.reshape(
    mnist_test_labels, (-1, 1))).toarray()  # 生成标签并独热编码

mnist_train_labels = encoder.transform(np.reshape(
    mnist_train_labels, (-1, 1))).toarray()  # 生成标签并独热编码

# 将数据展平并归一化,并归一化到 -0.5~0.5 之间
mnist_train_data = (np.reshape(
    mnist_train_data, (mnist_train_data.shape[0], -1))-127.0)/255.0

mnist_test_data = (np.reshape(
    mnist_test_data, (mnist_test_data.shape[0], -1))-127.0)/255.0

mnist_train_data.shape, mnist_test_data.shape

至此,数据预处理完毕。我们得到了训练数据 mnist_train_data,测试数据 mnist_test_data 和对应的独热标签。
由上面的定义可知,完成手写体的分类是本次的任务 T,所以接下来需要定义一个性能度量 P,即损失函数,和一种 算法 T 使得损失最小。

因此,接下来会分别讲到这两部分内容:
损失函数: Softmax 和 Cross Entropy Loss。
算法 T: 如何求解最优的W。

Softmax 和交叉熵损失函数

Softmax 函数
Softmax 函数函数是机器学习和深度学习中相当常用到的函数,它的公式如下:

image.png

其中sk表示的是输入到 Softmax 函数的数据。
Softmax 函数具体的作用是将输入标准化到和为 1 的输出,经过 Softmax 函数的的数据可以被认为是概率。
为了更好理解,假设最后的输出y为(3.2,5.1,−1.7),对应的是每一个类别的概率,但是显然这个数字不能直接使用,通常来说概率都是和为 1 的,所以才需要经过 Softmax 进行标准化。

流程如图:


image.png

y=xW 最后输出的是一个N×C 的矩阵,每行对应一个样本的结果,所以需要按行进行标准化,使用 numpy 实现 Softmax 函数如下:

# 根据公式 5 实现
def softmax(input):
    assert len(input.shape) == 2 # 输入 softmax 函数的数据必须是一个二维矩阵
    exp_value = np.exp(input)  # 首先计算指数
    output = exp_value/np.sum(exp_value, axis=1)[:, np.newaxis]  # 然后按行标准化
    return output


test_data = np.array([[3.2, 5.1, -1.7]])
softmax(test_data)

交叉熵损失函数(Cross Entropy Loss)
既然经过 Softmax 函数之后的是概率,那么最根本的想法当然是使正确类别的概率最大。交叉熵损失函数出现的目的就是使正确类别概率最大。

image.png

如上图所示,交叉熵是用来衡量两个概率之间的差别。深度学习中使用 softmax 输出的概率拟合真实概率,并使得这两个两个概率之间的交叉熵最小。

假设交叉熵损失函数为L,那么单个样本的损失定义为:


image.png

指的是在样本是xi的情况下,使用概率分布Pi拟合真实概率分布Yi的误差。
通常真实概率是由人类在数据集上打的标签,例如上图的[1.00,0.00,0.00] 是人为打的标签。通常将标签 one-hot(独热)编码为真实概率。
为了更容易理解,代入 Softmax 函数的公式作为概率,交叉熵损失函数可以写成:


image.png

多个样本时,只需要将各个样本的损失相加即可:
image.png

其中Y=(y1,…,yi,…) 是真实概率,因为是独热编码,所以yi要么为 0要么为 1;X=(x1,…,xi,…) 是神经网络的输出。
接下来用代码来实现交叉熵损失函数,生成随机数据数据计算 Loss:

# 最后的输出是 N*C
N, C = 100, 3
test_data = np.random.randn(N, C)  # 生成随机数据

# 生成随机标签,不包括 C,并独热编码
test_labels = encoder.fit_transform(np.random.randint(0, C, (N, 1))).toarray()
prob = softmax(test_data)  # 每一行对应一个样本,按行计算概率

# 根据公式 7,8 实现
# loss = np.sum(-np.log(prob))  # 根据公式计算 loss
loss = np.sum(-np.log(prob) * test_labels) / N
loss

交叉熵损失函数是分类任务中常用的损失函数,而且取得的效果很好。

如何选取最优权重

介绍完交叉熵损失函数之后,最终的目的是优化W使得L最小(这里选取的L为交叉熵损失函数),那么采用什么方法才能找到最优的W取得很好的性能呢?
接下来介绍两种方法,一种非常 Naive 的随机查找法,另一种是深度学习的基础算法之一:梯度下降法。

随机查找法(Random search)

为什么说随机查找法非常 Naive?因为随机查找是随机生成W以找到最优的W。接下来用代码讲话,看看随机查找法的效果怎么样:

best_acc = -float('inf')  # 最高的准确度

# 迭代 100 次
for epoch in range(100):
    W = np.random.randn(784, 10)*0.01  # 随机生成 w,前面讲过 w 为 784*10 的矩阵
    # 根据公式 4 实现
    prob = softmax(np.dot(mnist_test_data, W))  # 只计算测试集 xW 和概率
    y_predict = np.argmax(prob, axis=1)  # 计算输出的标签,概率最大的
    y_real = np.argmax(mnist_test_labels, axis=1)
    acc = np.mean(y_predict == y_real)  # 计算测试集准确度
    if acc > best_acc:
        best_acc = acc  # 保存最好的准确度
best_acc

迭代 100 次之后,随机猜能达到 ~20% 左右的准确度,相对于 10% 的随机概率要提高了一倍。
但是不是随机查找真的能取得非常好的效果,答案是不可能的。实际的深度学习任务的网络结构、参数比这里多得多,随机查找几乎不可能取得好的效果,除非有人相信天上能掉馅饼。

梯度下降法(Gradient Descent)

梯度下降法是深度学习的基础算法之一,它的基本思想是参数沿着梯度的相反方向更新。如果你未曾听过的话,首先需要知道什么是梯度(Gradient)。
梯度的本意是一个向量(矢量),表示某一函数在该点处的方向导数沿着该方向取得最大值,即函数在该点处沿着该方向(此梯度的方向)变化最快,变化率最大。

image.png

以这张图为例,现在你站在山顶,山的构造其实就是对应损失函数,因此你下山其实是在寻找损失函数的最小值点。现在你不知道自己在那里,也不知道东南西北。但是你只需要知道按照最陡的方向一直往下走就一定能到山脚了。
image.png

下图可以更清晰表述梯度下降的过程:
image.png

因此梯度下降法有两个重点:计算梯度,以及沿着梯度的反方向更新参数。
其中,计算梯度只需要简单的高数知识,在计算梯度时需要应用求导的链式法则,对于更深的网络,链式法则和反向传播算法非常重要,反向传播算法会在接下来的课程中介绍。
多元函数计算梯度会涉及到偏导数的求解,你可以参考 维基百科-梯度。下面举一个简单的例子:
image.png

由于最终损失函数可以换算成一个关于权重W的函数,即L(x,W),梯度下降沿梯度的方向更新参数的基本公式为:
image.png

lr 为一个提前设定的值,控制W更新的幅度,对应到深度学习则称之为 学习率。
在实际使用时当然不会这么简单,不可能直接化简成关于W的函数,这时候就需要链式法则了。
为了更好的理解梯度下降,让代码来说话。举一个最简单的例子,一个简单的一元四次函数,使用梯度下降法求解其极小值点。首先,我们生成一个拥有两个极小值点的四次函数,并将函数图像绘制处理。

def f(x): return (x+2)*(x+1)*(x-2)*(x-1)


X = np.linspace(-3, 3, 10000)
Y = np.array([f(x) for x in X])
plt.plot(X, Y)

离散点的最小的 Y 和对应的 X 为:

Y[np.argmin(Y)], X[np.argmin(Y)]

上图同样可以看出有两个极小值点,并且最小值是左边的极小值点。这里为了求导方便这个四次函数关于 Y 轴对称,两个极值点都为最小值。


image.png
import math

def df(x): return 4*x**3-10*x  # 导数

math.sqrt(5/2)

根据上文介绍的梯度下降的基本准则,沿着梯度的方向更新参数,该一元函数更新x的公式为:


image.png

下面就是利用梯度下降法求函数的极值点。

lr = 0.001
x = -3
# 迭代 1000 次
for i in range(1000):
    x -= lr*df(x) # 根据公式 11 实现
x
image.png

学习率衰减策略

上面讲到了使用梯度下降法求解一个一元函数的极值,接下来为了更好的理解梯度下降法,绘制一个动画显示整个梯度下降的过程。如何绘制动画并不要求掌握,但是关于如何使用 Matplotlib 绘图,可以参考蓝桥云课课程 使用 Matplotlib 绘制 2D 和 3D 图形。

from matplotlib import animation
from IPython.display import HTML

# 这是一个基本函数,用来演示梯度下降过程,该函数不要求掌握
def show_anim(X, Y, f, df, max_iterations, lr, initial_x):
    fig, ax = plt.subplots()
    # 绘制曲线
    ax.plot(X, Y)
    line, = ax.plot(0, 0, 'ro')
    # 绘制箭头
    annotation = ax.annotate("", xy=(0, 0), xytext=(
        0, 0), arrowprops=dict(arrowstyle="->"))
    # 返回梯度下降的点,生成动画的箭头
    # 梯度下降法的函数,传入代求解函数、导数、Y 值、最大迭代次数、学习率、初始起点
    def gradient_descent(f, df, Y, max_iterations=30, lr=0.001, x=-3):
        # 初始点
        points = [(x, f(x))]
        for _ in range(max_iterations):
            # 梯度下降,并保留中间的点
            x -= lr*df(x)
            points.append((x, f(x)))
        arrows = []
        for i in range(len(points)-1):
            # 箭头起始点和终点
            arrows.append((points[i], points[i+1]))
        return arrows

    arrows = gradient_descent(f, df, Y, max_iterations, lr, initial_x)

    def init():
        return line,

    # 更新画布
    def update(index):
        start, end = arrows[index]
        line.set_data(end[0], end[1])

        annotation.set_position(start)
        annotation.xy = end

        return line, annotation
    # 绘制动画,不需要掌握
    anim = animation.FuncAnimation(
        fig, update, interval=200, blit=False, frames=max_iterations, init_func=init)
    return anim

接下来,我们就可以通过动画演示上文的梯度下降法,这里设置学习率初始为 0.001,起始点x=−3,最大迭代次数 100 次。

lr = 0.001
initial_x = -3
max_iterations = 100
anim = show_anim(X, Y, f, df, max_iterations, lr, initial_x)
from IPython.display import HTML

HTML(anim.to_html5_video())  # 运行查看动画

一般情况下,我们会先设置一个较大的学习率以尽快到达极小点。但固定学习率往往会带来另一个问题,那就是无法准确收敛,并在极小值附近反复波动。所以,为了兼顾梯度下降法后期的收敛速度和准确度,必须要应用学习率衰减策略。

一般情况下,学习率衰减策略有三种,分别是:


image.png

在实际应用中,按步长衰减是最常用的,因此这里将只使用和介绍按步长衰减。其基本公式如下:


image.png

其中,α为设定的衰减因子,k为是迭代次数/提前设定的步长,通俗点讲其实是每迭代一个设定的步长数目,则学习率变为上一个步长的α。
步长需要根据实际应用调整,但是衰减因子常为 0.1,即学习率是上一个步长的1/10。

接下来,我们实现按步长衰减策略,这段代码将在本次课程的后续内容中多次使用。

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):
        # 根据公式 12 实现
        self.lr = self.base_lr * \
            (self.deacy_factor**(self.step_count//self.step_size))  # 实现上面的公式
        return self.lr
lr = 0.001
x = -3
max_iter = 10000
scheduler = lr_scheduler(lr, 1000)
for i in range(max_iter):
    x -= scheduler.get_lr()*df(x)  # 根据公式 11 实现
    scheduler.step()
x

应用学习率衰减策略时需要注意最大迭代次数足够,否则可能收敛不完全。
通过学习率衰减最后迭代的结果是1.5811388301861444,而如果不应用结果是 −1.581138831026283,真实结果−1.5811388300841898,只看后面几位,分别是 0186144401861444,10262831026283,0084189800841898。事实证明应用学习率衰减获得了更好的结果。
由上文知道了学习率非常小的时候,会导致收敛速度很慢,那么当学习率很大时,会有什么结果呢?
答案是:Loss 震荡或超出数值范围。
我们还是以上面的一元函数为例,当设置学习率非常大时,看看演示动画有什么效果。

lr = 0.075
initial_x = -3
max_iterations = 100
anim = show_anim(X, Y, f, df, max_iterations, lr, initial_x)
HTML(anim.to_html5_video())

可以看出,当学习率为 0.075 时,最开始的阶段点变化的幅度非常大。虽然这里能够收敛,是因为如果再把学习率调大,更新x时,会直接超出计算机可表示的范围。这就是深度学习中常讨论的数值的上溢和下溢。你可以尝试自己把学习率调大之后,程序会报 OverflowError。
所以初始学习率不应过大也不应过小,通常在 0.0001~0.001 之间。

权重初始化方法

对深度学习有过一些了解的肯定知道,梯度下降法最大的问题是什么:迭代终止于局部最优解,很难找到全局最优解。
在本例中,当x初始化为 3 呢?

lr = 0.001
x = 3
for i in range(1000):
    x -= lr*df(x) # 根据公式 11 实现
x

除了初始值外,同样的参数取得了不同的结果。这就是一个好的权重初始化的重要性的体现了,一个好的权重可以获得可能更好的结果。这就是为什么迁移学习通常拿在 IMGAENET 训练好的模型,并可以获得高准确度的原因了,因为已经给定了一个非常好的初始化权重。
常用的初始化权重包括 Gaussian(高斯)初始化和 Xavier 初始化。而偏置项一般是常数为 0 的初始化。以经典的 Caffe 为例:

layer {
  name: "conv1"
  type: "Convolution"
  bottom: "data"
  top: "conv1"
  param {
    lr_mult: 1
  }
  param {
    lr_mult: 2
  }
  convolution_param {
    num_output: 20
    kernel_size: 5
    stride: 1
    weight_filler {
      type: "xavier"
    }
    bias_filler {
      type: "constant"
    }
  }
}

这是 Caffe 的配置文件设置一个卷积层的各项参数,后续课程会介绍卷积神经网络。weight_filler 采用的是 Xavier 初始化,而 bias_filler 采用 constant 初始化,默认为 0。
大部分深度学习框架会自动调用某个初始化方法,如果需要按照自己的需求更改权重初始化方法,则需要手动设置,例如 PyTorch 的初始化方法都在 torch.nn.init 中实现。
接下来,我们尝试实现各种初始化方法。

# 示例权重矩阵的形状
D, H = 10, 10

下面,使用高斯初始化生成一个零均值,标准差为 0.01 的权重:

W = 0.01*np.random.randn(D, H)
W

高斯初始化一般用于小网络,而且也能取得很好的效果,但是对于更深的网络,例如残差网络等几十层上百层的网络一般使用 Xavier 初始化。因为神经网络的基本算法是线性的y=Wx+b,如果使用高斯初始化,不停的做线性运算,网络越深,输出的数据会呈现极端分布,要么是 1 要么是 -1。
下面,我们使用 Xavier 初始化:

W = np.random.randn(D, H)/np.sqrt(D)
W

本次课程前半部分不会使用很深的网络,所以只会用到高斯初始化,后续将会使用 PyTorch 的初始化方法。

线性分类器实现

image.png

本次试验选择前面介绍的交叉熵损失函数,交叉熵损失函数如何计算 Loss 已经介绍了,接下来介绍如何对交叉熵损失函数计算梯度。首先对于单个样本X=(x1,…,xi,…),真实概率Y=(y1,…,yi,…) 的交叉熵损失函数表达如下:
image.png

最初的公式是很复杂的,所以利用链式法则先计算 Softmax 函数的梯度,再计算交叉熵损失函数的梯度,两者相乘即为交叉熵损失函数关于输入x的梯度。
在此为了表述简洁,仅先讨论单个样本的情况。
Softmax 函数求导
假设 Softmax 函数的输出P=Softmax(x)为P=(p1,…,pi,…),如下图表述:
image.png

首先求 Softmax 函数关于x的梯度。
image.png

因此求导过程分这两种情况进行:
当i=j 时:
image.png

当i!=j 时:
image.png

因此对 Softmax 求导完毕后发现导数值只与概率有关了,再对交叉熵损失函数关于输入 xx 求导,会发现一件更神奇的事。
交叉熵损失函数求导
image.png

image.png

代码实现
为了代码可复用,可以在后续实验中使用。在此实现一个类,包含两个函数,forward 函数定义交叉熵损失函数的前向传播过程,计算损失;backward 函数定义交叉熵损失函数求解梯度的反向传播过程。
在下一次实验中将会介绍对于多层网络神经反向传播的概念,在此只实现一层的线性分类器。

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
        # 根据公式 13 实现
        loss = -np.sum(np.multiply(self.labels, np.log(self.prob))
                       )/self.labels.shape[0]
        return loss

    def backward(self):
        # 根据公式 22 实现
        # 公式 22 只讨论了单个样本的情况,所以这里需要除以样本数
        self.grad = (self.prob - self.labels)/self.labels.shape[0]

接下来生成随机数据测试。

N, C = 10, 3

pred_prob = np.random.randn(N, C)  # 随机数据,代表线性分类器输出
labels = encoder.fit_transform(
    np.random.randint(0, C, (N, 1))).toarray()  # 生成标签并独热编码

loss_layer = CrossEntropyLossLayer()
# 前向传播,传入数据和独热编码的标签
loss = loss_layer.forward(pred_prob, labels)
# 后向传播计算梯度
loss_layer.backward()
loss, loss_layer.grad

上文已经介绍了线性分类器W的形状为784×10,在此不多做介绍。这里需要设置完各项参数,然后就开始训练。

from tqdm.notebook import tqdm

D_in, D_out = 784, 10
# 设置基础学习率
base_lr = 0.1
# 设置最大迭代次数
max_iter = 900
# 设置学习率衰减步长
step_size = 400
# 初始化一个学习率调度器
scheduler = lr_scheduler(base_lr, step_size)
W = np.random.randn(D_in, D_out)*0.01  # 高斯初始化权重

best_acc = -float('inf')
best_weight = None  # 保存最好的结果和准确度
loss_list = []
for _ in tqdm(range(max_iter)):
    # 测试阶段,输入测试集,然后计算准确度
    test_pred = np.dot(mnist_test_data, W)
    # 预测和真实标签
    pred_labels = np.argmax(test_pred, axis=1)
    real_labels = np.argmax(mnist_test_labels, axis=1)
    # 计算准确度
    acc = np.mean(pred_labels == real_labels)
    if acc > best_acc:
        best_acc = acc
        best_weight = W  # 保留结果最好的
    # 训练并更新参数
    train_pred = np.dot(mnist_train_data, W)
    # 前向传播输出损失
    loss = loss_layer.forward(train_pred, mnist_train_labels)
    loss_list.append(loss/mnist_train_data.shape[0])  # 数据的平均 Loss
    # 后向传播计算梯度
    loss_layer.backward()
    # 损失关于权重的梯度,根据公式 25 实现
    grad = np.dot(mnist_train_data.T, loss_layer.grad)
    # 更新参数
    W -= scheduler.get_lr()*grad
    # 学习率衰减
    scheduler.step()
best_acc

通过调节学习率和迭代次数最后获得了 ~90% 的准确度!而这只是一个简单的线性分类器,甚至连偏置项都省去了,深度学习的性能表现可见一斑。
我们可以绘制出 Loss 的变化曲线。

plt.plot(list(range(max_iter)), loss_list)

接下来,使用训练好的权重测试一张图片。

# 从测试机中随机抽取一张图片
import random
test_index = random.randint(0, mnist_test_data.shape[0]-1)
test_img = mnist_test_data[test_index, :]
# 展示图片
plt.imshow(test_img.reshape((28, 28)))

预测得到结果:

prob = softmax(np.dot(test_img[np.newaxis, :], W))  # 归一化为概率
np.argmax(prob, axis=1)  # 取概率最大的坐标

输出应当很大可能是正确的。

随机梯度下降法求解函数极值

使用梯度下降法求解多元函数极值

image.png

下面利用梯度下降法求该函数的极值点。

lr = 0.001
x = -3

def df(x): return 4*x**3-10*x  # 导数

for i in range(1000):
    # 根据公式 11 实现
    x -= lr*df(x)
x
image.png

你可能感兴趣的:(51从 0 到 1 实现卷积神经网络--从线性分类到深度学习)