Sigmoid 函数

Sigmoid 函数是一类形状像 S 字母的函数,称为 S 型生长曲线。常见的 S 函数包括逻辑函数、双曲正弦函数、反正切函数、古德曼函数和平滑阶跃函数等。

我们先从机器学习中较为常见的 Logistic 函数入手,其定义为:

$$sigmoid(x)=\frac{1}{1+e^{-x}}$$

其函数图像如下:

函数图像

这个函数有一个特性,即其导数是原函数的函数:

$$\begin{aligned} &y=sigmoid(x)\\ &y'=y(1-y) \end{aligned}$$

因此其导数可以通过本身来进行计算。下文使用 Sigmoid 函数作为激活函数构建神经网络,并实现反向传播算法。

神经网络

感知器或者线性单元所使用的激活函数稍作拓展,当使用 Sigmoid 函数最为激活函数时,神经元结构如下图所示:

多个上图的神经元相互连接构成的网络结构叫做神经网络,如下图:

上图是一个全连接神经网络(Full Connent, FC),其组成包括:

  • 输入层(input layer)数据由此层输入,其输出值为下游节点的输入值。
  • 隐藏层(hidden layer)即外部不可见层,包含指定个节点个数,可以理解为神经网络的内部逻辑。
  • 输出层(output layer)结果由此层输出,其每一个节点连接着上一个隐藏层的所有节点。

对于隐藏层,每个神经元都会进行以下操作:

$$a_4=f(\vec{w}^T\cdot\vec{x})$$

即使用激活函数对加权输入进行激活判定。

假设选取隐藏层的节点 4 进行观察,那么其计算结果便为:

$$\begin{aligned} a_4&=sigmoid(\vec{w}^T\cdot\vec{x})\\ &=sigmoid(w_{41}x_1+w_{42}x_2+w_{43}x_3+w_{4b}) \end{aligned}$$

上式的 $w_{4b}$ 是节点 4 的偏置项,而 $w_{41},w_{42},w_{43}$ 分别为节点 1、2、3 到节点 4 连接的权重。进而我们可以得到隐藏层 4 个节点的计算结果:

$$a_4=sigmoid(w_{41}x_1+w_{42}x_2+w_{43}x_3+w_{4b})\\ a_5=sigmoid(w_{51}x_1+w_{52}x_2+w_{53}x_3+w_{5b})\\ a_6=sigmoid(w_{61}x_1+w_{62}x_2+w_{63}x_3+w_{6b})\\ a_7=sigmoid(w_{71}x_1+w_{72}x_2+w_{73}x_3+w_{7b})$$

令:

$$W= \begin{bmatrix} \vec{w}_4 \\ \vec{w}_5 \\ \vec{w}_6 \\ \vec{w}_7 \\ \end{bmatrix}= \begin{bmatrix} w_{41},w_{42},w_{43},w_{4b} \\ w_{51},w_{52},w_{53},w_{5b} \\ w_{61},w_{62},w_{63},w_{6b} \\ w_{71},w_{72},w_{73},w_{7b} \\ \end{bmatrix} $$

由此有:

$$\vec{a}=f(W\cdot\vec{x})\qquad $$

扩展到各层,如在开始的神经网络中,节点服从如下的计算:

$$\vec{a}_1=f(W_1\cdot\vec{x})$$

$$\vec{y}=f(W_2\cdot\vec{a}_1)$$

其中 $\vec{y}$ 是输出层的结果矩阵。

现在,我们需要知道一个神经网络的每个连接上的权值是如何得到的。我们可以说神经网络是一个模型,那么这些权值就是模型的参数,也就是模型要学习的东西。然而,一个神经网络的连接方式、网络的层数、每层的节点数这些参数,则不是学习出来的,而是人为事先设置的。对于这些人为设置的参数,我们称之为超参数 (Hyper-Parameters)。

反向传播算法 (Backpropagation, BP)

BP 算法是“误差反向传播”的简称,是一种与最优化方法(如梯度下降法)结合使用的,用来训练人工神经网络的常见方法。 该方法对网络中所有权重计算损失函数的梯度。 这个梯度会反馈给最优化方法,用来更新权值以最小化损失函数。 反向传播要求有对每个输入值期望得到的已知输出,来计算损失函数的梯度。仿照线性回归中的误差函数,我们使用误差平方和作为误差函数,然后使用随机梯度下降算法对权重进行计算:

$$E_d\equiv\frac{1}{2}\sum_{i}(t_i-y_i)^2$$

$$w_{ji}\gets w_{ji}-\eta\frac{\partial{E_d}}{\partial{w_{ji}}}$$

设 $net_j$ 是节点 $j$ 的加权输入,即:

$$net_j=\sum_{i}{w_{ji}}x_{ji}=\vec{w_j}\cdot\vec{x_j}$$

求导可得:

$$\begin{aligned} \frac{\partial{E_d}}{\partial{w_{ji}}}&=\frac{\partial{E_d}}{\partial{net_j}}\frac{\partial{net_j}}{\partial{w_{ji}}}\\ &=\frac{\partial{E_d}}{\partial{net_j}}\frac{\partial{\sum_{i}{w_{ji}}x_{ji}}}{\partial{w_{ji}}}\\ &=\frac{\partial{E_d}}{\partial{net_j}}x_{ji} \end{aligned}$$

上式中,是 $x_{ji}$ 节点 $i$ 传递给节点 $j$ 的输入值,也就是节点$i$的输出值。 对于 $\frac{\partial{E_d}}{\partial{net_j}}$,需要分为输出层和隐藏层来讨论。

对于输出层,由于 $y(j)=f(net_j)$,其中 $f$ 为激活函数,所以使用 $y_j$ 对 $\frac{\partial{E_d}}{\partial{net_j}}$ 进行变换:

$$\begin{aligned} \frac{\partial{E_d}}{\partial{net_j}}&=\frac{\partial{E_d}}{\partial{y_j}}\frac{\partial{y_j}}{\partial{net_j}}\\ &=\frac{\partial}{\partial{y_j}}\frac{1}{2}\sum_{i}(t_i-y_i)^2\cdot\frac{\partial f(net_j)}{\partial{net_j}}\\ &=\frac{\partial}{\partial{y_j}}\frac{1}{2}(t_j-y_j)^2\cdot y_j(1-y_j)\\ &=-(t_j-y_j)y_j(1-y_j)\\ \end{aligned}$$

令 $$\delta_j=-\frac{\partial{E_d}}{\partial{net_j}}$$ 则有:

$$\begin{aligned} &\delta_j=(t_j-y_j)y_j(1-y_j) \qquad (1)\\ &w_{ji}:=w_{ji}+\eta\cdot\delta_j\cdot x_{ji} \qquad (2) \end{aligned}$$

对于隐藏层,由于 $f(net_j)*w_k = net_k$,其中 $net_k$ 是下一层节点 $k \in NextLayer(j)$ 的加权输入,所以使用 $net_k$ 对 $\frac{\partial{E_d}}{\partial{net_j}}$ 进行变换:

$$\begin{aligned} \frac{\partial{E_d}}{\partial{net_j}}&=\sum_{k}\frac{\partial{E_d}}{\partial{net_k}}\frac{\partial{net_k}}{\partial{net_j}}\\ &=\sum_{k}-\delta_k\frac{\partial{net_k}}{\partial{a_j}}\frac{\partial{a_j}}{\partial{net_j}}\\ &=\sum_{k}-\delta_kw_{kj}\cdot \frac{\partial{f(net_j)}}{\partial{net_j}}\\ &=\sum_{k}-\delta_kw_{kj}\cdot a_j(1-a_j)\\ \end{aligned}$$

将 $\delta_j=-\frac{\partial{E_d}}{\partial{net_j}}$ 带入上式得到:

$$\delta_j=a_j(1-a_j)\sum_{k}\delta_kw_{kj} \qquad (3)$$

对于输出层节点应用式 $(1)$,对于隐藏层节点应用式 $(3)$,并将结果在式 $(2)$ 中更新权重,循环以上步骤即可实现基于反向传播算法的神经网络训练。

向量化及实现

将式 $(1)$、式 $(2)$ 和式 $(3)$ 改写为向量形式有:

$$\vec{\delta}=\vec{y}(1-\vec{y})(\vec{t}-\vec{y})\\ \vec{\delta^{(l)}}=\vec{a}^{(l)}(1-\vec{a}^{(l)})W^T\delta^{(l+1)}\\ W \gets W + \eta\cdot \vec{\delta}\cdot\vec{x}^T\\ \vec{a}=\sigma(W\cdot\vec{x})$$

其中,$\delta^{(l+1)}$ 表示第 $(l+1)$ 层的误差项。

代码实现如下(使用MINST 数据集, Python3.8):

import os
import time
import struct
import numpy as np


class Layer:
    def __init__(self, _in, _out):
        self.out = _out
        self.front = lambda x: 1 / (1 + np.exp(-x))
        self.back = lambda y: y * (1 - y)
        self.weight = np.random.uniform(-0.5, 0.5, (_out, _in))
        self.array = None

    def forward(self, array):
        self.array = array
        return self.front(np.dot(self.weight, array))

    def update(self, delta, rate):
        dp = self.back(self.array) * np.dot(np.transpose(self.weight), delta)
        self.weight += rate * np.dot(delta, np.transpose(self.array))
        return dp


class Network:
    def __init__(self, node):
        self.layers = []
        self.back = lambda y: y * (1 - y)
        for k in range(len(node) - 1):
            self.layers.append(Layer(node[k], node[k + 1]))

    def predict(self, array):
        result = array.copy()
        for layer in self.layers:
            result = layer.forward(result)

        return result

    def train(self, sample, label, epoch, rate):
        """
        sample: [m, n] (no bias layer)
        label: [n]
        """
        # Add Bias
        _input = np.column_stack((sample, np.ones([sample.shape[0], 1])))

        # Generate Layers
        count, p = _input.shape

        # Start Training
        for i in range(epoch):
            for j in range(count):
                result = self.predict(_input[j].reshape([p, 1]))
                delta = self.back(result) * self.delta(label[j], result)

                for layer in self.layers[::-1]:
                    delta = layer.update(delta, rate)

    @staticmethod
    def delta(label, predict):
        ans = np.zeros([10, 1])
        ans[label, 0] = 1
        return ans - predict


def load(path, kind):
    """Load MNIST data from `path`"""
    labels_path = os.path.join(path, '%s-labels-idx1-ubyte' % kind)
    images_path = os.path.join(path, '%s-images-idx3-ubyte' % kind)

    with open(labels_path, 'rb') as path:
        _, _ = struct.unpack('>II', path.read(8))
        labels = np.fromfile(path, dtype=np.uint8)

    with open(images_path, 'rb') as path:
        _, _, _, _ = struct.unpack('>IIII', path.read(16))
        images = np.fromfile(path, dtype=np.uint8).reshape(len(labels), 784)

    return (images / 2550), labels


def evaluate(network, test_data_set, test_labels):
    error = 0
    total = len(test_data_set)
    data = np.column_stack((test_data_set, np.ones([test_data_set.shape[0], 1])))
    for i in range(total):
        test = data[i].reshape([data[i].shape[0], 1])
        prt = network.predict(test)
        if test_labels[i] != int(np.where(prt == np.max(prt))[0]):
            error += 1
    return round(float(error) / float(total) * 100, 2)


def main():
    train_images, train_labels = load('.', 'train')
    test_images, test_labels = load('.', 't10k')
    bp = Network([785, 100, 10])
    for i in range(40):
        now = time.time()
        rate = 0.8 / (i + 1)
        bp.train(train_images, train_labels, 1, rate)
        debug = [
            'Rnd: {}'.format(i),
            'Err: {}%'.format(evaluate(bp, test_images, test_labels)),
            'Time: {}s'.format(round(time.time() - now, 2))
        ]
        print('  '.join(debug))
    pass


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        pass


运行结果:

kyaru# python3 c.py
Rnd: 0  Err: 9.13%  Time: 15.49s
Rnd: 1  Err: 6.72%  Time: 14.94s
Rnd: 2  Err: 6.07%  Time: 15.43s
Rnd: 3  Err: 5.45%  Time: 15.27s
Rnd: 4  Err: 5.16%  Time: 14.71s
Rnd: 5  Err: 4.97%  Time: 15.55s
Rnd: 6  Err: 4.76%  Time: 15.39s
Rnd: 7  Err: 4.61%  Time: 14.8s
Rnd: 8  Err: 4.46%  Time: 15.0s
Rnd: 9  Err: 4.44%  Time: 15.75s
Rnd: 10  Err: 4.3%  Time: 16.0s
Rnd: 11  Err: 4.23%  Time: 15.67s
Rnd: 12  Err: 4.19%  Time: 15.46s
Rnd: 13  Err: 4.14%  Time: 14.9s
Rnd: 14  Err: 4.06%  Time: 15.35s
Rnd: 15  Err: 4.03%  Time: 15.63s
Rnd: 16  Err: 3.99%  Time: 15.95s
Rnd: 17  Err: 3.95%  Time: 15.34s
Rnd: 18  Err: 3.96%  Time: 15.65s
Rnd: 19  Err: 3.93%  Time: 15.68s
Rnd: 20  Err: 3.86%  Time: 15.33s
Rnd: 21  Err: 3.82%  Time: 15.78s
Rnd: 22  Err: 3.81%  Time: 15.57s
Rnd: 23  Err: 3.76%  Time: 15.54s
Rnd: 24  Err: 3.74%  Time: 15.79s
Rnd: 24  Err: 3.74%  Time: 15.79s
Rnd: 25  Err: 3.74%  Time: 15.87s
Rnd: 26  Err: 3.73%  Time: 15.23s
Rnd: 27  Err: 3.71%  Time: 15.18s
Rnd: 28  Err: 3.7%  Time: 15.93s
Rnd: 29  Err: 3.7%  Time: 15.51s
Rnd: 30  Err: 3.7%  Time: 15.4s
Rnd: 31  Err: 3.7%  Time: 15.41s
Rnd: 32  Err: 3.68%  Time: 14.83s
Rnd: 33  Err: 3.66%  Time: 15.38s
Rnd: 34  Err: 3.65%  Time: 15.05s
Rnd: 35  Err: 3.66%  Time: 14.95s
Rnd: 36  Err: 3.66%  Time: 15.43s
Rnd: 37  Err: 3.64%  Time: 15.2s
Rnd: 38  Err: 3.63%  Time: 15.76s
Rnd: 39  Err: 3.63%  Time: 15.8s
...
Rnd: 128  Err: 3.18%  Time: 14.58s
Rnd: 129  Err: 3.17%  Time: 14.35s
Rnd: 130  Err: 3.17%  Time: 14.47s
Rnd: 131  Err: 3.17%  Time: 14.11s
Rnd: 132  Err: 3.17%  Time: 13.81s
Rnd: 133  Err: 3.17%  Time: 13.51s
Rnd: 134  Err: 3.17%  Time: 13.63s
Rnd: 135  Err: 3.16%  Time: 13.71s
Rnd: 136  Err: 3.16%  Time: 13.53s
Rnd: 137  Err: 3.16%  Time: 13.4s
Rnd: 138  Err: 3.16%  Time: 13.26s
Rnd: 139  Err: 3.16%  Time: 13.46s
Rnd: 140  Err: 3.15%  Time: 13.58s
Rnd: 141  Err: 3.17%  Time: 13.53s
Rnd: 142  Err: 3.17%  Time: 13.8s
Rnd: 143  Err: 3.16%  Time: 13.26s
Rnd: 144  Err: 3.16%  Time: 13.35s
Rnd: 145  Err: 3.16%  Time: 13.85s
Rnd: 146  Err: 3.16%  Time: 13.98s
Rnd: 147  Err: 3.16%  Time: 13.98s
Rnd: 148  Err: 3.16%  Time: 14.14s
Rnd: 149  Err: 3.16%  Time: 14.2s
kyaru#

参考