多分类svm的hinge loss公式推导_如何搭建一个简单的多分类深度神经网络?

去年今日此门中,人面桃花相映红。人面不知何处去,桃花依旧笑春风。
--- 崔护《题都城南庄》

深度学习(Deep Learning)是机器学习的分支,是一种以人工神经网络为架构,对数据进行表征学习的算法。

本篇我们试着通过对神经网络的作用机制进行逐步拆解来从头搭建一个简单的神经网络的框架。虽然现在已经有足够多好用且高效的第三方包,例如PyTorch, Tensorflow, Keras供我们直接调用,但自己动手去搭建这么一个简单的神经网络,对我们加深理解其作用机制大有裨益。

以一个经典的两层神经网络为例,其主体结构主要由以下三部分构成:

  1. 输入层 (input layer)
  2. 隐藏层 (hidden layer)
  3. 输出层 (output layer)

a88173b44e75be67efed041c229a7635.png
图片来源: Usman Malik

三个部分(输出层、隐藏层、输出层)之间通过两种过程(前馈、反向传播)来进行相互作用,构成了一个正反馈系统。在前馈过程中,为了对线性不可分的样本数据进行分类,会通过激活函数对中间值进行非线性转换。在反向传播过程中,又会应用到链式法则和梯度下降算法来求解权重和偏置的更新幅度。概括来说,前馈是对权重和偏置项进行处理得到中间值,再对中间值进行非线性处理返回预测值的过程;反向传播则根据预测值与真实值的误差重新更新权重和偏置项以使得误差最小化的过程。

  • 前馈(feed forward)
    • 激活函数 (activation function)
  • 反向传播 (back propagation)
    • 链式法则 (chain rule)
    • 梯度下降 (gradient descent)

激活函数

常用激活函数表现形式及其特点如下:

  • sigmoid

|
  • tanh

|
  • ReLU

|

Sigmoid函数将原始输入值映射到

之间,可以理解为原始值对应某个输出的概率。Sigmoid函数由于存在梯度消失、非关于原点对称以及训练速度相对较慢的问题,现在已经较少使用。

tanh(Hyperbolic Tangent)函数亦称双曲正切函数,可以通过对Sigmoid函数进行线性变换得到。tanh函数由于映射的数值是以零为中心分布的,避免了梯度更新过程中出现的锯齿状,但其和Sigmoid类似,依然无法消除在极端值时发生梯度消失的现象。

ReLU(Rectified Linear Unit)即修正线性单元,其表现形式简单,仅在输入超过一定阈值时才会起作用,加快了训练速度。但由于其在负值条件时,梯度为0,造成某些参数无法被更新,即Dying ReLU Problem。为了解决这个问题,ReLU的一些衍生激活函数被相应提出,如LeakyReLU,当然也可以通过调节学习率来尽量减少该问题的发生。

损失函数

  • 平方根损失函数

  • 交叉熵损失函数

链式法则

根据Wiki的介绍,链式法则是求复合函数导数的一个法则。设

为关于
的可导函数,则复合函数
关于
的导数的推导过程为:

,则

从而有:

可得:

故:

梯度下降

关于梯度下降的详细介绍,可参见上一篇文章:不同梯度下降算法的介绍及Python实现,此处不再赘述。

回到主线,这里我们以仅含一层隐含层的前馈神经网络为例进行说明。

前馈

一个两层的前馈神经网络的前馈过程可以用如下四个等式进行概括:

其中,

分别表示输入层到隐藏层和隐藏层到输出层的经权重和偏置调整后的计算值,
分别表示隐藏层和输出层经激活函数非线性调整后的值,
分别表示隐藏层和输出层的激活函数,下标
分别表示隐藏层和输出层

反向传播

反向传播的目的是为了通过对权重和偏差进行不断修正从而达到预测值和真实值误差最小,而这个修正过程的本质便是梯度下降算法,而梯度下降算法中的权重和偏置更新幅度究竟应该取多少,又会涉及到使用链式法则来决定。其计算流程可以通过以下四个等式来表示:

其中,

表示损失函数,
表示学习率

偏置的更新类似于权重,但更好的处理方式是将偏置项向量作为第一列纳入到权重矩阵中去,最后通过矩阵运算来进行参数更新,快捷且高效。

根据损失函数和激活函数的选取不同,以上通用形式返回的结果会略有差异。这里举个具体的例子来说明一下,方便大家理解。

假设有一个两层的神经网络其隐藏层激活函数为

,输出层激活函数为
,损失函数为交叉熵损失函数(cross entropy loss function),则反向传播过程中该计算得到的
是多少呢?
(以下推导无法保证完全正确,请谨慎采纳并欢迎反馈指正!)

我们对等式

进行拆解,分别求出各个成分的值,得到:

是one-hot encoder形式

表示Kronecker delta

组合得到:

虽然最终的形式看起来很简单,但由于在代码实现层面是通过矩阵运算来处理,所以会有一些矩阵知识的小坑需要注意,如:

  1. 矩阵,向量,标量之间求导的原理
  2. 矩阵运算时各种积的差异及使用环境,如Inner Product vs. Outer Product, Hadamard Product vs. Kronecker Product

废话了这么多,我们看一下具体的代码实现并用其训练经典的Iris数据集,为了保持连贯性以及减少重复劳动,这里构建的多分类深度学习的框架基于上一篇梯度下降的框架结构来增补。

import numpy as np
import pandas as pd
from sklearn.datasets import load_iris


def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def sigmoid_derivative(y):
    return y * (1 - y)

def tanh(x):
    return np.tanh(x)

def tanh_derivative(y):
    return 1 - y**2

def softmax(x):
    exp_x = np.exp(x)
    return exp_x / exp_x.sum(axis=1).reshape(-1, 1)

class MultiClassNeuralNetwork:
    def __init__(self, eta=0.01, batch_size=5, shuffle=True, random_state=None,
                 n_iter=1e5, tolerance=1e-5):
        self.eta = eta
        self.batch_size = batch_size
        self.shuffle = shuffle
        self.n_iter = n_iter
        self.tolerance = tolerance
        if random_state:
            np.random.seed(random_state)

    def fit(self, X, y):
        _X = np.c_[np.ones(len(X)), X]  # 增加偏置项
        n_samples, n_features = _X.shape
        n_labels = len(np.unique(y))
        self.w_h = np.random.randn(n_features, 6)  # 纵轴代表的隐藏节点数
        self.w_o = np.random.randn(6, n_labels)  # 横轴与上面纵轴相等

        self.loss_ = [0]
        self.cnt = 0
        while self.cnt < self.n_iter:
            self.cnt += 1
            if self.shuffle:
                _X, y = self._shuffle(_X, y)
            _y = pd.get_dummies(y).values

            errors = []
            for i in range(0, n_samples, self.batch_size):
                xi, yi = _X[i: i + self.batch_size], _y[i: i + self.batch_size]
                # feed forward (前馈)
                zh = xi.dot(self.w_h)
                # ah = sigmoid(zh)  # hidden layer
                ah = tanh(zh)
                zo = ah.dot(self.w_o)
                ao = softmax(zo)  # output layer

                # back propagation (反向传播)
                # 矩阵求导术
                deriv_w_o = ah.T.dot(ao - yi)
                deriv_w_h = xi.T.dot((tanh_derivative(ah) * ((ao - yi).dot(self.w_o.T))))
                # deriv_w_h = xi.T.dot((sigmoid_derivative(ah) * ((ao - yi).dot(self.w_o.T))))

                self.w_o -= self.eta * deriv_w_o
                self.w_h -= self.eta * deriv_w_h

                error_i = - (yi * np.log(ao))
                errors.append(error_i.sum())
            loss = 1 / (2 * self.batch_size) * np.mean(errors)  # 一轮完整迭代后的误差
            if self.cnt % 20 == 0:
                print('epoch: {}, loss: {}'.format(self.cnt, loss))
            delta_loss = loss - self.loss_[-1]
            self.loss_.append(loss)
            if np.abs(delta_loss) < self.tolerance:
                break
        return self

    def predict(self, X):
        _X = np.c_[np.ones(len(X)), X]
        return np.argmax(softmax(tanh(_X.dot(self.w_h)).dot(self.w_o)), axis=1)
        # return np.argmax(softmax(sigmoid(_X.dot(self.w_h)).dot(self.w_o)), axis=1)

    def accuracy(self, X, y):
        pred_y = self.predict(X)
        accuracy = sum(np.equal(pred_y, y)) / len(y)
        return accuracy

    @staticmethod
    def _shuffle(X, y):
        location = np.random.permutation(len(y))
        return X[location], y[location]


if __name__ == '__main__':
    X, y = load_iris(return_X_y=True)
    nn = MultiClassNeuralNetwork(eta=0.005, batch_size=30, random_state=1024, tolerance=1e-5)
    nn.fit(X, y)
    y_pred = nn.predict(X)
    accuracy = nn.accuracy(X, y)
    print(y_pred)
    print(accuracy)

输出结果及准确率为:

7521d2cd4455968f4df603734f2e807e.png
预测值与真实值对照

可以看到,准确率在98.7%,对150个样本的预测结果只错了两个,表现应该还是不错的。细心的同学可能会发现,代码中还有sigmoid作为激活函数的实现,大家感兴趣的可以拓展一下该模块,改为通过参数来控制激活函数、损失函数的选取。

最后,有几个tips简单说一下,在构建神经网络时,输入层的节点数与特征的维度相等,输出层的节点数要与目标的维度匹配。中间层节点数一般是输入层节点数的1.2~1.5倍,一般而言,节点越多,拟合的效果越好。

最后的最后,由于自己也是边学边记录,所以错误在所难免,欢迎大家指正!

写代码花了半天,写成文章前后拖了一个月,所以看到这里的童鞋是不是点个赞再走

参考资料

[1] https://stats.stackexchange.com/questions/235528/backpropagation-with-softmax-cross-entropy

[2] https://stackabuse.com/creating-a-neural-network-from-scratch-in-python-multi-class-classification/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值