ffd48e8dd41b1550017bc3fbef08cbe1.png
本文内容都包含在拙作《深入理解神经网络》中

一、符号与表示

本文介绍全连接人工神经网络的训练算法——反向传播算法(关于人工神经网络的简单介绍,可参考“卷积神经网络简介”第二节)。反向传播算法本质上是梯度下降法(参考“上篇”)。人工神经网络的参数多,梯度计算比较复杂。在人工神经网络模型提出几十年后才有研究者提出了反向传播算法来解决深层参数的训练问题。本文将详细讲解该算法的原理及实现。

首先把文中用来表示神经网络的各种符号描述清楚。文中向量用粗体小写字母表示,矩阵用粗体大写字母表示,非粗体都是标量。向量或矩阵的转置用上标 T 表示。上标括号中的数字表示神经网络的层序号。下标的含义结合上下文自明。请看图 1.1 。

d06188e6527f57c13f6faf00aa23a788.png
图 1.1 多层全连接神经网络

图 1.1 描绘了一个多层全连接神经网络。该网络共有 K(大写)层。第 k(小写)层包含

个神经元。最后一层——第 K 层是输出层。第 K 层的输出
是神经网络的输出向量。该神经网络接受
个输入
。输入向量视作网络的第 0 层。网络的第 k 层( 0 <
k < K )是隐藏层。网络第 k 层的第 j 个神经元以及它的前后连接如图 1.2 所示。

9d14f5b3a291961e6ff94decde357698.png
图 1.2 神经元

圆圈

是线性加和单元。它连接到第 k-1 层的
个神经元的输出
是连接的权重。线性加和单元的计算结果
称作该神经元的“激活水平”,由 [1.1] 计算得到。

从 [1.1] 可看出,神经元的“激活水平”是其权值向量与输入向量的内积。f 是神经元的激活函数。激活函数的输入是激活水平,输出是神经元的输出:

提供给下一层(
第 k+1 层)的
个神经元作为输入之一。例如
乘上权重
送给第 k+1 层第 q 个神经元的线性加和单元。

神经网络的计算过程就是将输入向量提供给网络第 1 层各神经元,经过加权求和得到激活水平,之后对激活水平施加激活函数得到结果。将这些结果输送给下一层神经元。依此类推,直到最后一层(输出层)计算出结果,就是神经网络的输出向量。


二、训练过程

训练集中的样本形如:

。输入包含
个值,目标值包含
个值,分别对应神经网络的输入/输出维度。训练这样进行:将训练集中的样本一个接一个提交给神经网络。神经网络对样本输入
计算输出
,然后计算样本目标值与输出的平方和误差:

视输入

为固定值,把 E 当作全体权值
的函数。求 E 的梯度
,然后用下式更新全体权值:

[2.2] 是梯度下降法的更新式。其中

是步长,s 是迭代次数。梯度矩阵
由 E 对每一个权重
的偏导数
构成。式 [2.2] 等价于对每一个权重进行更新:

对每一个提交给神经网络的样本用式 [2.3] 对全体权值进行一次更新,直到所有样本的误差值都小于一个预设的阈值,此时训练完成。看到这里或有疑问:不是应该用所有训练样本的误差的模平方的平均值(均方误差)来作 E 么?如果把样本误差的模平方看作一个随机变量,那么所有样本的平均误差模平方是该随机变量的一个无偏估计。而一个样本的误差模平方也是该随机变量的无偏估计,只不过估计得比较粗糙(大数定律)。但是用一次一个样本的误差模平方进行训练可节省计算量,且支持在线学习(样本随来随训练)。

训练算法还可以有很多变体。例如动态步长、冲量等(参考“上篇”)。也可以将一批样本在同样的权值

下计算
,然后根据这一批
的平均值更新
。这称为批量更新。训练的关键问题是如何计算
,即如何计算每一个

三、反向传播

回顾图 1.1 和图 1.2 。首先对第 k 层第 j 个神经元关注这样一个值

。定义:

定义为 E 对第 k 层第 j 个神经元的激活水平
的偏导数的相反数。根据求导链式法则,有:

将 [3.2] 等号右侧的第二项展开:

结合定义 [3.1] ,有:

可见有了

就能计算 E 对任一权重
的偏导数。接下来的问题就是如何计算
。采用一种类似数学归纳法的方法。首先计算第 K 层(
输出层)第 j 个神经元的

表示 f 的导函数。[3.5] 展示了推导过程,其结论是:对于输出层(
第 K 层)第 j 个神经元来说:

等于目标值
与输出
之差乘上 f 在
的导数。可以将
看成一个经过缩放的误差。这个观点在后面讨论反向传播的意义时有用。

现在推导某个隐藏层——第 k 层第 j 个神经元的

。将第 k+1 层的全体
值视作一个向量:

再次回顾图 1.1 和图 1.2 。

被施加激活函数 f 得到
乘上第 k+1 层各神经元对
的各个权值,再与第 k 层其他神经元的输出加权求和,得到第 k+1 层各神经元的激活水平
再经过后面的网络得到网络输出, 最终计算出 E 。将整个过程视作一个三个函数 f, g , h 的复合。图 1.3 将神经网络的一部分画出来,并标注出从
到 E 的三个函数:

c4b03d99c612835e2dd9fd42e04d7b77.png
图 1.3 神经网络各个值影响损失函数的方式

连续使用链式法则,有:

等号右侧第一项是一个

函数的导数。它是
元向量。它的第 i 个元素是:

第二项是一个

函数的导数。它是
元向量。它的第 i 个元素为:

最后一项是激活函数 f 在

的偏导数。结合 [3.8]、[3.9] 和 [3.10] 得到:

[3.11] 是推导过程,它的结论是:

注意 [3.8] 至 [3.10] 的推导过程运用了多元函数的求导链式法则。一个

函数的导数是一个
的矩阵。多元复合函数的求导链式法则是将导矩阵相乘。具体证明请可参考书目 [1] 附录部分或其他微积分教材。至此所有要素齐备。综合 [2.3]、[3.4]、[3.6] 和 [3.12] 可得反向传播算法如下:

------------------------------------

  • 反向传播阶段:

  • 权值更新阶段:

------------------------------------

可以用更紧凑的矩阵形式表示反向传播算法。由 [3.11] 可以得到:

如 [3.14] 所示,第 k 层( k<K )全体

值组成的向量
可由本层的激活函数导数对角阵
、第 k+1 层的
和权值矩阵
计算得到。输出层(
第 K 层)的
这么计算:

将矩阵形式的反向传播与权值更新算法总结如下:

------------------------------------

  • 反向传播阶段:

  • 权值更新阶段:

------------------------------------

[3.16 b] 中

是第 k-1 层输出向量。可以看到隐藏层
的计算利用了下一层的
。一个训练样本
“正向”通过网络计算输出
。之后“反向”逐层计算
更新权值,并将
向前一层传播。所谓“反向”传播就是
的传播。以上推导没有包括神经元的偏置。把偏置看成一个连接到常量 1 的连接上的权值即可。

从计算式来看

是 E 对第 k 层第 j 个神经元的激活水平
的偏导数的相反数。还存在另一个视角。上文已经谈到,输出层的
是经过缩放的误差。隐藏层的
以连接权值加权组合了下一层各神经元的
。可以把
定义为某种“局部误差”。于是反向传播算法就是反向传播局部误差——把总误差分摊到各个神经元头上,让它们调整自己。

回顾一下图 1.3 。 第 k 层第 j 个神经元的激活水平

通过 f 影响输出
。输出
通过加权求和影响第 k+1 层各神经元的激活水平。第 k+1 层的各激活水平通过网络其余部分最终影响误差 E 。于是第 k+1 层各神经元的局部误差按照权值分配到第 k 层第 j 个(以及其他)神经元的输出上。再经过 f 的导函数的调整,得到分配在第 k 层第 j 个神经元上的局部误差。反向传播本质是梯度下降,而局部误差视角为理解算法提供了一种洞见。

四、实现

插播广告。后来笔者写了一个机器学习库。那里的 ANN 实现对本文例程做了改进:

zhangjuefei/mentat​github.com
cc460d633e1bb68ba1f1ec0914762efa.png

6f75600e59465d1bb87f425e33ab0424.gif

本节以 Python 语言实现了一个 mini-batch 随机梯度下降反向传播神经网络,带冲量和学习率衰减功能。代码如下:

import numpy as np


class DNN:
    def __init__(self, input_shape, shape, activations, eta=0.1, threshold=1e-5, softmax=False, max_epochs=1000,
                 regularization=0.001, minibatch_size=5, momentum=0.9, decay_power=0.5, verbose=False):

        if not len(shape) == len(activations):
            raise Exception("activations must equal to number od layers.")

        self.depth = len(shape)
        self.activity_levels = [np.mat([0])] * self.depth
        self.outputs = [np.mat(np.mat([0]))] * (self.depth + 1)
        self.deltas = [np.mat(np.mat([0]))] * self.depth
        self.eta = float(eta)
        self.effective_eta = self.eta
        self.threshold = float(threshold)
        self.max_epochs = int(max_epochs)
        self.regularization = float(regularization)
        self.is_softmax = bool(softmax)
        self.verbose = bool(verbose)
        self.minibatch_size = int(minibatch_size)
        self.momentum = float(momentum)
        self.decay_power = float(decay_power)
        self.iterations = 0
        self.epochs = 0

        self.activations = activations
        self.activation_func = []
        self.activation_func_diff = []
        for f in activations:

            if f == "sigmoid":
                self.activation_func.append(np.vectorize(self.sigmoid))
                self.activation_func_diff.append(np.vectorize(self.sigmoid_diff))
            elif f == "identity":
                self.activation_func.append(np.vectorize(self.identity))
                self.activation_func_diff.append(np.vectorize(self.identity_diff))
            elif f == "relu":
                self.activation_func.append(np.vectorize(self.relu))
                self.activation_func_diff.append(np.vectorize(self.relu_diff))
            else:
                raise Exception("activation function {:s}".format(f))

        self.weights = [np.mat(np.mat([0]))] * self.depth
        self.biases = [np.mat(np.mat([0]))] * self.depth
        self.acc_weights_delta = [np.mat(np.mat([0]))] * self.depth
        self.acc_biases_delta = [np.mat(np.mat([0]))] * self.depth

        self.weights[0] = np.mat(np.random.random((shape[0], input_shape)) / 100)
        self.biases[0] = np.mat(np.random.random((shape[0], 1)) / 100)
        for idx in np.arange(1, len(shape)):
            self.weights[idx] = np.mat(np.random.random((shape[idx], shape[idx - 1])) / 100)
            self.biases[idx] = np.mat(np.random.random((shape[idx], 1)) / 100)

    def compute(self, x):
        result = x
        for idx in np.arange(0, self.depth):
            self.outputs[idx] = result
            al = self.weights[idx] * result + self.biases[idx]
            self.activity_levels[idx] = al
            result = self.activation_func[idx](al)

        self.outputs[self.depth] = result
        return self.softmax(result) if self.is_softmax else result

    def predict(self, x):
        return self.compute(np.mat(x).T).T.A

    def bp(self, d):
        tmp = d.T

        for idx in np.arange(0, self.depth)[::-1]:
            delta = np.multiply(tmp, self.activation_func_diff[idx](self.activity_levels[idx]).T)
            self.deltas[idx] = delta
            tmp = delta * self.weights[idx]

    def update(self):

        self.effective_eta = self.eta / np.power(self.iterations, self.decay_power)

        for idx in np.arange(0, self.depth):
            # current gradient
            weights_grad = -self.deltas[idx].T * self.outputs[idx].T / self.deltas[idx].shape[0] + 
                           self.regularization * self.weights[idx]
            biases_grad = -np.mean(self.deltas[idx].T, axis=1) + self.regularization * self.biases[idx]

            # accumulated delta
            self.acc_weights_delta[idx] = self.acc_weights_delta[
                                              idx] * self.momentum - self.effective_eta * weights_grad
            self.acc_biases_delta[idx] = self.acc_biases_delta[idx] * self.momentum - self.effective_eta * biases_grad

            self.weights[idx] = self.weights[idx] + self.acc_weights_delta[idx]
            self.biases[idx] = self.biases[idx] + self.acc_biases_delta[idx]

    def fit(self, x, y):
        x = np.mat(x)
        y = np.mat(y)
        loss = []
        self.iterations = 0
        self.epochs = 0
        start = 0
        train_set_size = x.shape[0]

        while True:

            end = start + self.minibatch_size
            minibatch_x = x[start:end].T
            minibatch_y = y[start:end].T

            yp = self.compute(minibatch_x)
            d = minibatch_y - yp

            if self.is_softmax:
                loss.append(np.mean(-np.sum(np.multiply(minibatch_y, np.log(yp + 1e-1000)), axis=0)))
            else:
                loss.append(np.mean(np.sqrt(np.sum(np.power(d, 2), axis=0))))

            self.iterations += 1
            start = (start + self.minibatch_size) % train_set_size

            if self.iterations % train_set_size == 0:
                self.epochs += 1
                mean_e = np.mean(loss)
                loss = []

                if self.verbose:
                    print("epoch: {:d}. mean loss: {:.6f}. learning rate: {:.8f}".format(self.epochs, mean_e,
                                                                                         self.effective_eta))

                if self.epochs >= self.max_epochs or mean_e < self.threshold:
                    break

            self.bp(d)
            self.update()

    @staticmethod
    def sigmoid(x):
        return 1.0 / (1.0 + np.power(np.e, min(-x, 1e2)))

    @staticmethod
    def sigmoid_diff(x):
        return np.power(np.e, min(-x, 1e2)) / (1.0 + np.power(np.e, min(-x, 1e2))) ** 2

    @staticmethod
    def relu(x):
        return x if x > 0 else 0.0

    @staticmethod
    def relu_diff(x):
        return 1.0 if x > 0 else 0.0

    @staticmethod
    def identity(x):
        return x

    @staticmethod
    def identity_diff(x):
        return 1.0

    @staticmethod
    def softmax(x):
        x[x > 1e2] = 1e2
        ep = np.power(np.e, x)
        return ep / np.sum(ep, axis=0)

测试一下神经网络的拟合能力如何。用网络拟合以下三个函数:

对每个函数生成 100 个随机选择的数据点。神经网络的输入为 2 维,输出为 1 维。为每个函数训练 3 个神经网络。这些网络有 1 个隐藏层 1 个输出层,隐藏层神经元数量分别为:3、5 和 8 。隐藏层激活函数是 sigmoid 。输出层的激活函数是恒等函数

。初始学习率 0.4 ,衰减指数 0.2 。冲量惯性 0.6 。迭代 200 个 epoch 。mini batch 样本数为 40 。L2 正则,正则强度 0.0001 。拟合效果见下图:

8af6d16b0cfea682087761aa8313ebcf.png
图 4.1 对三种函数进行拟合

测试代码如下:

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
from mentat.classification_model import DNN
from mpl_toolkits.mplot3d import Axes3D

np.random.seed(42)

hidden_layer_size = [3, 5, 8]  # 隐藏层神经元个数(所有隐藏层都取同样数量神经元)
hidden_layers = 1  # 隐藏层数量
hidden_layer_activation_func = "sigmoid"  # 隐藏层激活函数
learning_rate = 0.4  # 学习率
max_epochs = 200  # 训练 epoch 数量
regularization_strength = 0.0001  # 正则化强度
minibatch_size = 40  # mini batch 样本数
momentum = 0.6  # 冲量惯性
decay_power = 0.2  # 学习率衰减指数


def f1(x):
    return (x[:, 0] ** 2 + x[:, 1] ** 2).reshape((len(x), 1))


def f2(x):
    return (x[:, 0] ** 2 - x[:, 1] ** 2).reshape((len(x), 1))


def f3(x):
    return (np.cos(1.2 * x[:, 0]) * np.cos(1.2 * x[:, 1])).reshape((len(x), 1))


funcs = [f1, f2, f3]

X = np.random.uniform(low=-2.0, high=2.0, size=(100, 2))
x_min, x_max = X[:, 0].min() - .5, X[:, 0].max() + .5
y_min, y_max = X[:, 1].min() - .5, X[:, 1].max() + .5
xx, yy = np.meshgrid(np.arange(x_min, x_max, .02), np.arange(y_min, y_max, .02))

# 模型
names = ["{:d} neurons per layer".format(hs) for hs
         in hidden_layer_size]

classifiers = [
    DNN(input_shape=2, shape=[hs] * hidden_layers + [1],
        activations=[hidden_layer_activation_func] * hidden_layers + ["identity"], eta=learning_rate, threshold=0.001,
        softmax=False, max_epochs=max_epochs, regularization=regularization_strength, verbose=True,
        minibatch_size=minibatch_size, momentum=momentum, decay_power=decay_power) for hs in
    hidden_layer_size
]

figure = plt.figure(figsize=(5 * len(classifiers) + 2, 4 * len(funcs)))
cm = plt.cm.PuOr
cm_bright = ListedColormap(["#DB9019", "#00343F"])
i = 1

for cnt, f in enumerate(funcs):

    zz = f(np.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape)
    z = f(X)

    ax = figure.add_subplot(len(funcs), len(classifiers) + 1, i, projection="3d")

    if cnt == 0:
        ax.set_title("data")

    ax.plot_surface(xx, yy, zz, rstride=1, cstride=1, alpha=0.6, cmap=cm)
    ax.contourf(xx, yy, zz, zdir='z', offset=zz.min(), alpha=0.6, cmap=cm)
    ax.scatter(X[:, 0], X[:, 1], z.ravel(), cmap=cm_bright, edgecolors='k')
    ax.set_xlim(xx.min(), xx.max())
    ax.set_ylim(yy.min(), yy.max())
    ax.set_zlim(zz.min(), zz.max())

    i += 1

    for name, clf in zip(names, classifiers):

        print("model: {:s} training.".format(name))

        ax = plt.subplot(len(funcs), len(classifiers) + 1, i)
        clf.fit(X, z)
        predict = clf.predict(np.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape)

        ax = figure.add_subplot(len(funcs), len(classifiers) + 1, i, projection="3d")

        if cnt == 0:
            ax.set_title(name)

        ax.plot_surface(xx, yy, predict, rstride=1, cstride=1, alpha=0.6, cmap=cm)
        ax.contourf(xx, yy, predict, zdir='z', offset=zz.min(), alpha=0.6, cmap=cm)
        ax.scatter(X[:, 0], X[:, 1], z.ravel(), cmap=cm_bright, edgecolors='k')
        ax.set_xlim(xx.min(), xx.max())
        ax.set_ylim(yy.min(), yy.max())
        ax.set_zlim(zz.min(), zz.max())

        i += 1
        print("model: {:s} train finished.".format(name))

plt.tight_layout()
plt.savefig(
    "pic/dnn_fitting_{:d}_{:.6f}_{:d}_{:.6f}_{:.3f}_{:3f}.png".format(hidden_layers, learning_rate, max_epochs,
                                                                      regularization_strength, momentum, decay_power))

五、参考书目
最优化导论 (豆瓣)​book.douban.com
04f7165772a1fb809c87676fcc21f62d.png
神经网络设计 (豆瓣)​book.douban.com
8b5cc86584f6e1ae56f5a2e9d07eaf06.png
机器学习 (豆瓣)​book.douban.com
5e2b6bda06962b68e8532ec41e811419.png
神经计算原理 (豆瓣)​book.douban.com
fc885282927d00a4cbffc7510545eb7b.png
Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐