深入浅出神经网络-学习小结

开源项目地址

kimsmith/深入浅出机器学习 (gitee.com)

神经网络识别手写数字

基础知识

如何理解感知机 激活函数是阶跃函数的神经元

sigmoid作用 阶跃函数的升级,平滑了阶跃函数,阶跃函数不容易稳定,sigmoid克服了此缺点

多层感知机 可以理解为多层sigmoid激活函数神经元连接的网络

前馈神经网络 指上个神经元输出会作为下个神经元输入的网络。意味着神经网络中没有环路

循环神经网络 带环路的神经网络。rnn应用面和fnn应用面都很广,fnn更广一些

梯度下降 一种最小化损失函数的方法。对于复杂情况,难以通过导数运算计算损失函数最小值。此时想象一个山谷,找到最低谷底,通过多次采样本计算最终值,每次步进一点,按一定步进方法寻找最终结果

随机梯度下降 先选取少量样本估算梯度,估算的梯度可以加速梯度下降

代码

核心类为network类

import numpy as np


class Network:
    def __init__(self, sizes):
        # size is the nodes number im every layer, like [2, 3, 1]
        self.num_layers = len(sizes)
        self.sizes = sizes
        self.bias = [np.random.randn(y, 1) for y in sizes[1:]]
        self.weights = [np.random.randn(y, x) for x, y in zip(sizes[:-1], sizes[1:])]

注意,输入的数据是先列再行,比如一个一维数组,它在网络运算中应该是列向量存在而不是行向量,如果是行向量,不方便表示多维数组,所以应该是先列吧

其中,偏置和权重是均值为0标准差为1的标准正态分布随机数

用np写一个sigmoid,支持基本数值类型也支持矩阵sigmoid计算

def sigmoid(z):
    return 1.0 / (1.0 + np.exp(-z))

给网络添加前馈运算

    def feedforward(self, a):
        for b, w in zip(self.bias, self.weights):
            a = sigmoid(np.dot(w, a) + b)
        return a

添加随机梯度下降

    def SGD(self, training_data, epochs, batch_size, learn_rate, test_data):
        if test_data:
            n_test = len(test_data)
        train_data_len = len(training_data)
        for j in range(epochs):
            random.shuffle(training_data)
            batchs = [training_data[k: k + batch_size] for k in range(0, train_data_len, batch_size)]
            for one_batch in batchs:
                self.update_batch(one_batch, learn_rate)
            if test_data:
                print('epoch {0}:{1} {2}'.format(j, self.evaluate(test_data), n_test))
            else:
                print('epoch {0} completed'.format(j))

training_data是二维数组,第二个维度是手写数字图像转化为一维向量的长度,第一个维度是手写图像的个数

epoch是训练轮数。逻辑是每轮先将训练数据随机打乱,分成几个batch,batch长度为batch_size,每个batch会用本batch计算梯度并更新权重,在update_batch方法实现

单次更新权重函数update_batch

    def update_batch(self, one_batch, learn_rate):
        delta_b = [np.zeros(b.shape) for b in self.bias]
        delta_w = [np.zeros(w.shape) for w in self.weights]
        for x, y in one_batch:
            delta_b_sample, delta_w_sample = self.backprop(x, y)
            delta_b = [db + dnb for db, dnb in zip(delta_b, delta_b_sample)]
            delta_w = [dw + dnw for dw, dnw in zip(delta_w, delta_w_sample)]
        self.weights = [w - (learn_rate/len(one_batch)) * nw for w, nw in zip(self.weights, delta_w)]
        self.bias = [b - (learn_rate/len(one_batch)) * nb for b, nb in zip(self.bias, delta_b)]

self.backprop代码下章看,主要逻辑是self.backprop实现,它对每个单独sample计算梯度,然后在update_batch里对batch梯度加和

训练得当的话,神经网络会是最好的模型

一个训练公式:复杂算法<=简单算法+好的训练数据

拿数据的话,写如下函数进行处理

import pickle
import gzip

import numpy as np

# usage: train_data, validata_data, test_data = mnist_loader.load_data_wrapper()


def load_data():
    f = gzip.open('../data/mnist.pkl.gz', 'rb')
    training_data, validation_data, test_data = pickle.load(f)
    f.close()
    return training_data, validation_data, test_data


def load_data_wrapper():
    tr_d, va_d, te_d = load_data()
    training_inputs = [np.reshape(x, (784, 1)) for x in tr_d[0]]
    training_results = [vectorized_result(y) for y in tr_d[1]]
    training_data = zip(training_inputs, training_results)
    validation_inputs = [np.reshape(x, (784, 1)) for x in va_d[0]]
    validation_data = zip(validation_inputs, va_d[1])
    test_inputs = [np.reshape(x, (784, 1)) for x in te_d[0]]
    test_data = zip(test_inputs, te_d[1])
    return training_data, validation_data, test_data


def vectorized_result(j):
    e = np.zeros((10, 1))
    e[j] = 1.0
    return e

反向传播代码(完整代码见开源项目)

损失函数使用均方误差


    def backprop(self, x, y):
        nabla_b = [np.zeros(b.shape) for b in self.bias]
        nabla_w = [np.zeros(w.shape) for w in self.weights]

        # feedforward operation
        activation = x
        activations = [x]
        zs = []
        for b, w in zip(self.bias, self.weights):
            z = np.dot(w, activation) + b
            zs.append(z)
            activation = sigmoid(z)
            activations.append(activation)

        # back forward operation
        delta = self.cost_derivative(activations[-1], y) * sigmoid_prime(zs[-1])
        nabla_b[-1] = delta
        nabla_w[-1] = np.dot(delta, activations[-2].transpose())
        for layer_number in range(2, self.num_layers):
            z = zs[-layer_number]
            sp = sigmoid_prime(z)
            delta = np.dot(self.weights[-layer_number + 1].transpose(), delta) * sp
            nabla_b[-layer_number] = delta
            nabla_w[-layer_number] = np.dot(delta, activations[-layer_number - 1].transpose())
        return nabla_b, nabla_w

    def cost_derivative(self, output_activations, y):
        return output_activations - y

效果

准确率能达到95%左右

开始深度学习

上面的多层感知机即使最后取得了好的结果,但权重和偏置很难用人类可以理解的东西去解释,网络为什么可以达到很高的准确率

一种思路是将复杂的问题分解为多个,多层的简单问题,分解的结果是深度学习,可以理解为深度神经网络

反向传播算法工作原理

本章数学较多,可以跳过

为啥用反向传播?因为比传统算法更快

阿达马积

类似向量内积,计算结果不加和,结果仍是一个向量,只是向量逐元素相乘

反向传播算法四个基本方程

反向传播目标是计算出代价函数关于权重和偏置的导数。计算这个最终结果需要的其中一个中间结果是每个神经元上的误差,用这个可以计算最终的导数。可以吧每个神经元的微小误差定义为代价函数的微分

总结

理解:方程1和2求出各层误差,方程3和4算的是损失函数对权重和偏置的导数,算出了1和2,再算3和4,用复合函数求导法即可算出。方程1规定了每层误差是损失函数的倒数乘以

改进神经网络的学习方法

引入交叉熵代价函数

对于sigmoid激活函数,梯度下降开始时,损失函数对权重和偏置导数会比较小,这也是sigmoid函数刚开始梯度下降速度会比较慢的原因。

交叉熵可以作为代价函数 但是为什么 

1 根据交叉熵表达式,其值大于0

2 交叉熵可以避免sigmoid的训练速度两边缓慢的问题

优点:二次函数也有1和2特点,但交叉熵还避免了学习速度下降的问题,因为当损失函数是交叉熵时,损失函数对权重或偏执求导,最后会约掉sigmoid倒数项,剩余sigmoid函数本身,所以不存在学习速度下降现象

交叉熵可以理解为:当出现严重错误时,也往往是人是学习速度最快的时候,但sigmoid函数存在学习速度慢的现象,而交叉熵避免了这种现象

关于学习率:如果使用不同的代价函数,不能直接使用相同的学习率

对于多个输出神经元的网络,交叉熵形如

什么时候使用交叉熵:当代价函数是sigmoid或二次函数时,可以考虑用交叉熵

因为如果用交叉熵为损失函数,损失函数对权重和偏置求导的最终化简结果不包含激活值,也就避免了sigmoid两边学习慢现象

交叉熵含义和起源

为了避免梯度下降过慢,考虑假定希望找到一个满足下列公式的损失函数,这样的损失函数可以避免梯度下降缓慢,然后积分求导,可以得出损失函数的原函数是形如交叉熵的函数

softmax

避免学习缓慢另一个简单方法是softmax。softmax用在输出层,其公式为

公式可以看出,softmax对于多输出神经元来说,输出神经元值总和为1,可视为概率分布

softmax作为输出函数,对权重和偏置求导结果如下

从求导结果可以看出,a和y都不会收到学习速度下降的影响

小结,如果用sigmoid作为激活层,可以用交叉熵作为损失函数避免学习下降,也可以用softmax作为输出层避免学习缓慢

过拟合与正则化

思考 已经有了训练集和测试集 为什么还需要验证集?因为加入不用验证集,那么实际会用过测试集的预测结果来对超参数进行调整,这其实也是一种过拟合,因为用了测试集的结果反过来在调整超参数。所以如果加入验证集,就可以解决这个问题。所以总结一下,使用验证集可以避免过拟合。

另一种防止过拟合的方法是保证样本数量足够多。因为样本数量过少,会较快出现过拟合,且测试集和训练集准确率差距会很大。

正则化

一种避免过拟合的方法是缩小网络规模,但缩小网络,也缩小了网络的能力上限。

另一种方法是正则化,分l1和l2正则化。l2是权重衰减,l2正则化思想是对代价函数加一项正则化项。对于交叉熵代价函数来说,形如下

其中,新增项是所有权重的平方和,乘以正则化参数,除以2n,n是所有样本数量。注意到正则化项中不包含偏置

直观从公式理解其作用,先求代价函数对权重和偏置的导数,发现只有对权重导数发生了变化,最后反映到梯度下降,会将大权重变小,同时可以避免代价函数局部梯度下降缓慢,因为正则化梯度下降的权重公式,极大降低了大权重的下降速度

为何正则化可以减小过拟合

如何理解正则化减小过拟合原理?

假设存在一个x和y的关系,我们希望准确预测y关于x的表达式

给出两种方案,1是y=2x,用简单的线性模型,2是用多项式拟合

拟合结果:多项式拟合理论可以模拟任何曲线,多项式对现有样本拟合结果也比线性模型更准确,但多项式随着x增大,y的大小会收到x最高次幂项主导,从而忽略其他项,这可能导致繁华结果不够理想。而线性模型虽然对现有样本拟合不如多项式好,但线性模型足够简单,不像多项式会受到最高次幂项影响,从而泛化能力可能更好。

正则化对神经网络的影响 神经网络如果使用正则化,大权重会快速下降,最后大多权重差距不会太大,这提升了网络的抗噪能力,使网络适用于希望抗噪的情况。而没使用正则化的网络,则适合拟合带有噪声的数据。相比之下,抗噪的网络,泛化性能会更好,也就是说,正则化的网络,泛化性能比无正则化的网络可能更好

总结 正则化可以减少过拟合,但只是从经验得出,难以具体科学解释为什么正则化减少了过拟合

其他正则化技术

介绍三种:1 l1正则化 2 dropout 3 人为扩展数据

l1正则化

类似l2正则化,只是正则化项不一样

求导后,正则化项多了一个常数,所以梯度下降时,权重下降速度多了一个常数项,最终向0前进。所以l1会使一些权重容易变成0从而不起作用,而l2会避免大权重存在,提高抗噪能力,提高泛化能力

dropout

想必你应该听过,和正则化修改代价函数不同,dropout是随机丢弃神经元,可以控制丢弃神经元的数量,简化网络避免过拟合。

前馈时随机disable某个比例数量的隐单元,最后预测时会把所有神经元打开,同时也会按比例降低输出单元比例的权重。思想是平均

人为扩展数据

以识别mnist手写数字为例,通过对手写数字的像素图片进行平移,旋转,拉伸等操作丰富训练集,从而提升识别精度。虽然旋转看起来没啥区别,但对像素图片和网络还是有区别的

权重初始化

前述手写数字识别的权重初始化是随机高斯分布,有没有更好的初始权重?

考虑一个例子,激活函数为sigmoid,不使用正则化,输出函数使用交叉熵避免学习缓慢。交叉熵仅对输出层有作用,对隐藏层神经元如果饱和则学习缓慢

解决方法 缩小初始化权重的分布,默认是均值0标准差1的分布,将分布的标准差改小,改为sqrt(1/n),这样损失函数对权重求导则不会陷入饱和,从而不会发生学习缓慢的现象。而偏置可以不用管,因为偏置只有一项,影响不大,可以不用管

总结 权重初始化可以避免学习缓慢,加速学习速度,但最终准确率不会有太大影响

代码优化手写数字识别

将前述各种优化方案加入到第一版网络代码,继承第一版网络代码

使用交叉熵作为默认代价函数

import json

import numpy as np

from network import sigmoid, sigmoid_prime, Network
import mnist_loader


class CrossEntropyCost:
    @staticmethod
    def fn(a, y):
        return np.sum(np.nan_to_num(-y * np.log(a) - (1 - y) * np.log(1 - a)))

    @staticmethod
    def delta(z, a, y):
        return a - y


class QuadraticCost:
    @staticmethod
    def fn(a, y):
        return 0.5 * (np.linalg.norm(a - y) ** 2)

    @staticmethod
    def delta(z, a, y):
        return (a - y) * sigmoid_prime(z)


class Network2(Network):
    def __init__(self, sizes, cost=CrossEntropyCost):
        self.num_layers = len(sizes)
        self.sizes = sizes
        self.default_weight_initializer()
        self.cost = cost

    def default_weight_initializer(self):
        self.bias = [np.random.randn(y, 1) for y in self.sizes[1:]]
        self.weights = [np.random.randn(y, x) / np.sqrt(x) for x, y in zip(self.sizes[:-1], self.sizes[1:])]

    def SGD(self, tr, epochs, batch_size, learn_rate, lmbda=0,
            evaluation_data=None,
            monitor_evaluation_cost=False,
            monitor_evaluation_accuracy=False,
            monitor_training_cost=False,
            monitor_training_accuracy=False):
        n = len(tr[0])
        tr = [(tr[0][index], tr[1][index]) for index in range(len(tr[0]))]
        if evaluation_data:
            n_data = len(evaluation_data[0])
            evaluation_data = [(evaluation_data[0][index], evaluation_data[1][index]) for index in range(len(evaluation_data[0]))]

        evaluation_cost, evaluation_accuracy = [], []
        training_cost, training_accuracy = [], []
        for j in range(epochs):
            # mini_batches = [[(tr[0][i], tr[1][i]) for i in range(k, k + batch_size)] for k in range(0, n, batch_size)]
            mini_batches = [tr[k: k + batch_size] for k in range(0, n, batch_size)]
            for mini_batch in mini_batches:
                self.update_batch(mini_batch, learn_rate, lmbda, len(tr))
            print('epoch %s training complete' % j)
            if monitor_training_cost:
                cost = self.total_cost(tr, lmbda)
                training_cost.append(cost)
                print('cost on training data: {}'.format(cost))
            if monitor_training_accuracy:
                accuracy = self.accuracy(tr, convert=True)
                training_accuracy.append(accuracy)
                print('accuracy on training data: {}/ {}'.format(accuracy, n))
            if monitor_evaluation_cost:
                cost = self.total_cost(evaluation_data, lmbda, convert=True)
                evaluation_cost.append(cost)
                print('cost on evaluation data: {}'.format(cost))
            if monitor_evaluation_accuracy:
                accuracy = self.accuracy(evaluation_data)
                evaluation_accuracy.append(accuracy)
                print('accuracy on evaluation data: {} / {}'.format(self.accuracy(evaluation_data), n_data))
            print()
        return evaluation_cost, evaluation_accuracy, training_cost, training_accuracy

    def update_batch(self, one_batch, learn_rate, lmbda, n):
        delta_b = [np.zeros(b.shape) for b in self.bias]
        delta_w = [np.zeros(w.shape) for w in self.weights]
        for x, y in one_batch:
            # method backprop is to calculate derivative of loss for weight and bias
            delta_b_sample, delta_w_sample = self.backprop(x, y)
            delta_b = [db + dnb for db, dnb in zip(delta_b, delta_b_sample)]
            delta_w = [dw + dnw for dw, dnw in zip(delta_w, delta_w_sample)]
        # regularization param lmbda to adjust weight except bias
        self.weights = [(1 - learn_rate * (lmbda / n)) * w - (learn_rate/len(one_batch)) * nw for w, nw in zip(self.weights, delta_w)]
        self.bias = [b - (learn_rate/len(one_batch)) * nb for b, nb in zip(self.bias, delta_b)]

    def accuracy(self, data, convert=False):
        if convert:
            results = [(np.argmax(self.feedforward(x)), np.argmax(y)) for x, y in data]
        else:
            results = [(np.argmax(self.feedforward(x)), y) for x, y in data]
        return sum(int(x == y) for x, y in results)

    def total_cost(self, data, lmbda, convert=False):
        cost = 0
        data_len = len(data)
        for x, y in data:
            a = self.feedforward(x)
            if convert:
                y = vectorized_result(y)
            cost += self.cost.fn(a, y) / data_len
        cost += 0.5 * (lmbda / len(data)) * sum(np.linalg.norm(w) ** 2 for w in self.weights)
        return cost

    def save(self, filename):
        data = {'sizes': self.sizes,
                'weights': [w.tolist() for w in self.weights],
                'biases': [b.tolist() for b in self.bias],
                'cost': str(self.cost.__name__)}
        with open(filename, 'w') as f:
            json.dump(data, f)


def vectorized_result(j):
    e = np.zeros((10, 1))
    e[j] = 1
    return e


if __name__ == '__main__':
    tr, vd, te = mnist_loader.load_data_wrapper()
    net = Network2([784, 30, 10], cost=CrossEntropyCost)
    net.SGD(tr, 30, 10, .5,
            lmbda=.8,
            evaluation_data=vd,
            monitor_evaluation_accuracy=True,
            monitor_evaluation_cost=True,
            monitor_training_accuracy=True,
            monitor_training_cost=True)

上述代码和第一版相比,修改了初始化权重的标准差,按输入规模进行了压缩以减小学习缓慢;还默认使用了交叉熵代价函数。交叉熵的delta用来计算反向传播误差

如何选择神经网络超参数

策略1-宽泛策略:

简化网络,加快训练速度,快速发现问题。可以通过减少隐含层或减少神经元数量实现

减少训练结果集y的项数。比如mnist手写数字识别,可以只选择为0和1的样本,这样可以加快5倍速度学习

适度减少测试,验证,训练集大小,加快训练速度

注意 样本量减少时 同时需要视情况调小正则化参数值

优先对实验规模等进行修改,优先考虑从实验获取最直观的感受,即感受超参数对性能的影响

其他技术

随机梯度下降的变化形式

hessian技术

将代价函数泰勒展开,保留二阶导数

将代价函数对权重的二阶导数写为H矩阵

每次更新权重步长公式为

可以通过学习率改变每次步长变化大小

优点 hessian技术让代价函数收敛速度比一般梯度下降算法更快

缺点 由于保留了二阶导数,hessian矩阵太大,每次计算权重更新步长麻烦。

怎么优化缺点 针对hessian技术,在此基础上诞生了很多改进的方法

基于动量的梯度下降算法

在原有梯度下降算法和hessian算法基础上,舍弃运算二阶导数,从而避免引入的超大矩阵;引入 速度 的概念。公式如下

u表示摩擦力。当u为0,速度仅由梯度决定,权重运算公式退化为一般梯度下降。当u为1,表示没有摩擦力。摩擦力和速度的引入,使基于动量的梯度下降更快收敛,且不会跨过山谷谷底。

代价函数其他方法

共轭梯度下降

BFGS方法

nesterov

其他人工神经元模型

修正线性单元(RELU)

正切激活函数tanh

神经网络故事

为何深度网络很难训练

梯度消失问题

对于深度网络 不是隐藏层越多 越准确

一个现象 一个多层感知机,后面隐含层学习速度比前面隐含层学习速度快,这个学习速度是指每个层的损失函数梯度向量的模。这会导致在反向传播时,梯度到前面的隐含层时,出现梯度消失问题。这个问题可以被解决,但解决方法不完美

梯度消失的原因

看一个四层,每层只有一个神经元的模型。根据嵌套函数求导法,可得损失函数对第一个神经元偏置的导数为

为何出现梯度消失

根据损失函数对偏置导数公式可发现,sigmoid函数导数值域在(0, 0.25),而权重绝对值小于1(如果使用标准高斯分布给全局权重随机赋值),所以多个项都比1小,所以多个项相互乘法会越乘越小,这就是 梯度消失的原因

梯度爆炸问题

类似梯度消失问题,当权重和sigmoid函数的导数乘积比1大时,多项相乘的结果会指数增加,导致梯度爆炸

梯度不稳定问题

可以理解成梯度爆炸和梯度消失都存在,事实上这是可能发生的,条件就是权重大小和sigmoid函数导数乘积是否比1大

梯度消失问题普遍存在

当使用sigmoid激活函数时,由于函数的特点,梯度消失比梯度爆炸更为常见

复杂神经网络中的梯度不稳定

上一节只讨论了每层只有一个神经元的网络,当每层有多个神经元时,结论也是类似的,甚至梯度消失会更明显

深度网络

卷积神经网络入门

共享权重

与全连接层相比的优点 参数数量小 因为cnn每个卷积核的权重共享,而全连接层是笛卡积,这个层次看,参数数量会小一些

池化层

常用的池化层有最大值池化层

cnn应用

修正线性单元

将sigmoid改为RELU效果会更好些

扩展训练数据

即对已有图像进行平移,旋转,甚至是放缩等

全连接层+dropout

cnn最后加一层全连接再加dropout,会再提升一些性能。注意dropout只对全连接层使用

集成神经网络

训练多个相同结构神经网络,然后进行投票选举等选出一个最好的

如何解决梯度消失梯度不稳定等问题

1 使用卷积层减少参数数量

2 使用正则化技术,尤其是dropout和卷积层

3 使用ReLU而不是sigmoid

4 使用gpu

提高训练准确度

见前述章节

采用对图像随即裁剪减少过拟合(尤其是对大型深度网络)(随机裁剪可以扩展基础数据集)

l2正则化减少过拟合

dropout减少过拟合

增加训练速度

基于动量的梯度下降

其他网络

imagenet

googlenet

KSH网络

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值