深度学习入门——用python从轮子开始造神经网络(1)

深度学习入门——用python从轮子开始造神经网络(1)

作者:时棋

前言

由于作者水平问题,对深度学习的原理也给不出什么独到精妙的理解,本文单从实践角度试图让深度学习看起来简单一点。

本文不涉及任何已有框架如pytorch、tensorflow,环境为anaconda的默认base环境。

本文使用数据集:MNIST数据集

需要提前指出的是,依据本文内容写出的神经网络是最最简单最最原始的那种,换言之,你肯定没法跑出结果,因为算法效率太低了,同时不支持借用GPU算力。如何提升效率?请期待后续更新……

我们从图像分类问题开始。众所周知,计算机以数组形式阅读图片,简而言之,我们的程序要求输入一个数组,最后输出对它的分类,也可以用数组表示。比如,输入数组 ( x 1 , x 2 , x 3 ) (x_1,x_2,x_3) (x1,x2,x3),要确定它是三类中的哪一类,若是第二类,则输出应为 ( 0 , 1 , 0 ) (0,1,0) (0,1,0)

MNIST数据集

这里介绍一下我们使用的数据集:MNIST

MNIST数据集(Mixed National Institute of Standards and Technology database)是美国国家标准与技术研究院收集整理的大型手写数字数据库,包含60,000个示例的训练集以及10,000个示例的测试集。

在官方网站上有4个gz压缩文件,包含训练集与测试集,其中又分为图像集与标签集。每个图像是一张28 * 28像素的灰度手写数字图片。简而言之,我们读取的图片为1 * 28 * 28的数组,整个训练集的图片则是60000 * 28 * 28的数组,标签集则是1 * 60000的数组,每个元素对应一张图片,比如第三张图片是5,那么标签数组中的第三个元素就是5。

读取训练集的代码如下:

import numpy as np
import os
import gzip
import struct
def load_mnist_train(path):
    #path为你存放gz文件的目录路径
    labels_path = os.path.join(path,'train-labels-idx1-ubyte.gz')#标签集路径
    images_path = os.path.join(path,'train-images-idx3-ubyte.gz')#图像集路径    
    with gzip.open(labels_path,'rb') as lbpath:
        magic , number = struct.unpack('>II',lbpath.read(8))#去掉开头我们不需要的数据
        label = np.fromstring(lbpath.read(),dtype = np.uint8)#读取为np数组
    with gzip.open(images_path,'rb') as impath:
        magic , num , rows , cols = struct.unpack('>IIII',impath.read(16))
        images = np.fromstring(impath.read(),dtype = np.uint8).reshape(len(label),784)
        #将原为60000*28*28的数组转化为60000*784的数组
                
    return images,labels

读取测试集同理。

神经网络

之后举例就就近拿MNIST数据集举例了,也方便实践。

现在我们的需求是,训练出一个能够正确对图像数据进行分类的神经网络。那么,什么是神经网络?

不涉及具体技术细节,我们也可以描述其大概的结构。首先神经网络要有输入和输出,输入的是图像数据,输出对其的分类预测值,中间经过一系列数据处理……没错就这三层,输入层、中间层、输出层。中间层也称为隐藏层,神经网络的深度就是指中间层的层数。本文目标是从头造一个最简单的神经网络,那就只造一层,同时忽略偏置。

以MNIST数据集为例,一张图片输入进神经网络,输入层就是1 * 784的数组,也就是说有784个参数。中间层可以设置任意个数的神经元,接收输入层数据后经过一定处理将数据传给输出层,由于我们这里造的是单层神经网络,所以中间层和输出层其实合并了,直观上看就是在中间层完成了所有处理。

宏观上大致扫一遍,下面从具体细节看,每个神经元都要接收所有输入的数据,然后依据一个函数对其进行处理再输出。显然我们不可能只是把所有输入值相加,这里就涉及到神经网络可以称得上智能的地方了。输入值是按一定权重被神经元接收的,比如,2个神经元接收3个数据,就可以表示成:
{ a 1 = w 11 x 1 + w 21 x 2 + w 31 x 3 a 2 = w 12 x 1 + w 22 x 2 + w 32 x 3 \begin{cases}a_1=w_{11}x_1+w_{21}x_2+w_{31}x_3\\a_2=w_{12}x_1+w_{22}x_2+w_{32}x_3\end{cases} {a1=w11x1+w21x2+w31x3a2=w12x1+w22x2+w32x3
其中 a 1 , a 2 a_1,a_2 a1,a2即为神经元接收到的值, x 1 , x 2 , x 3 x_1,x_2,x_3 x1,x2,x3即为输入值, w w w为各输入值的权重。理论上,每个式子后面都可以分别加上一个常数 b b b作为偏置,但为求简略这里就忽略了。用矩阵的写法来看,那就是:
A = ( x 1 x 2 x 3 ) ( w 11 w 12 w 21 w 22 w 31 w 32 ) = ( a 1 a 2 ) A=\begin{pmatrix}x_1&x_2&x_3\end{pmatrix}\begin{pmatrix}w_{11}&w_{12}\\w_{21}&w_{22}\\w_{31}&w_{32}\end{pmatrix}=\begin{pmatrix}a_1&a_2\end{pmatrix} A=(x1x2x3)w11w21w31w12w22w32=(a1a2)
这里的权重矩阵就是神经网络能够对图像进行分类的核心所在,在机器学习中,这个权重矩阵是由人工确定的,而在深度学习中,这个矩阵是可以通过训练得到的,不涉及人工。

初始化生成一个权重矩阵的代码如下:

w = np.random.randn(input_layel,output_layel)
#第一个参数是输入层的数据个数,在单层网络的情况下第二个参数为输出层的参数个数,亦即要分类的类别个数

numpy中的矩阵乘法函数:

A = np.dot(x,w)
#注意矩阵形状正确

激活函数

现在中间层接收到的矩阵 A A A相当于未经内部处理的“刺激”,还需要神经元内部的函数对其进行转化,这个函数被称为激活函数。在感知机中,这个激活函数一般为阶跃函数,即:
f ( x ) = { 1 x ≥ 0 − 1 x < 0 f(x)=\begin{cases}1& x\ge0\\-1& x<0\end{cases} f(x)={11x0x<0
这个函数既不连续又不可导,差评,换人。

sigmoid函数:
f ( x ) = 1 1 + e − x f(x)=\frac{1}{1+e^{-x}} f(x)=1+ex1
ReLU函数:
f ( x ) = { x x > 0 0 x ≤ 0 f(x)=\begin{cases}x&x>0\\0&x\le0\end{cases} f(x)={x0x>0x0
这两个是常用的激活函数,下面我们会看到为什么激活函数需要连续可导,这是梯度下降算法的关键。

实现这两个函数的代码如下:

def sigmoid_function(x):
    return 1/(1+np.exp(-x))
def ReLU_function(x):
    return np.maximum(x,0)
#参数均为np数组

输出层需要输出具有概率意义的数据,即在0,1之间的数据,作为分类的预测值,二元分类输出层的激活函数一般为sigmoid函数,多元分类为softmax函数。

softmax函数:
y k = e x k ∑ i = 1 n e x i y_k=\frac{e^{x_k}}{\sum\limits_{i=1}\limits^ne^{x_i}} yk=i=1nexiexk
这里的 y k y_k yk即为图像为第k类的概率的预测值。

但由于指数运算容易超出程序可处理的范围,所以要对函数做一些改进,原理如下:
y k = e x k ∑ i = 1 n e x i = C e x k C ∑ i = 1 n e x i = e x k + l o g C ∑ i = 1 n e x i + l o g C = e x k + C ′ ∑ i = 1 n e x i + C ′ y_k=\frac{e^{x_k}}{\sum\limits_{i=1}\limits^ne^{x_i}}=\frac{Ce^{x_k}}{C\sum\limits_{i=1}\limits^ne^{x_i}}=\frac{e^{x_k+logC}}{\sum\limits_{i=1}\limits^ne^{x_i+logC}}=\frac{e^{x_k+C'}}{\sum\limits_{i=1}\limits^ne^{x_i+C'}} yk=i=1nexiexk=Ci=1nexiCexk=i=1nexi+logCexk+logC=i=1nexi+Cexk+C

简而言之,可以对输入的数组每个元素都加一个常数。一般来说,我们减去数组中的最大值。

实现代码:

def softmax_function(x):
    c = np.max(x)
    exp_x = np.exp(x-c)
    y = exp_x/np.sum(exp_x)
    return y

最后输出的就是最终的预测值了,以MNIST数据集为例,输出的数组中含有10个元素,每个元素对应其属于对应类的概率,如输入图像为5,那么我们预期中神经网络的输出数组的第五个元素应该显著大于其余元素。

损失函数

那么如何描述这个显著呢?引入损失函数,即预测值与实际值的偏差。常见的损失函数有均方误差与交叉熵误差。

均方误差:
E = ∑ ( y k − t k ) 2 k E=\frac{\sum(y_k-t_k)^2}{k} E=k(yktk)2
k为数组中数据的个数, y k y_k yk表示有k个元素的预测值, t k t_k tk为实际值,其中核心是分子的式子,不改变核心的前提下可以对其进行放缩,不妨将其放大 k 2 \frac{k}{2} 2k倍,变成:
E = 1 2 ∑ ( y k − t k ) 2 E=\frac{1}{2}\sum(y_k-t_k)^2 E=21(yktk)2
实现代码:

def mean_squared_error(y,t):
    return 0.5*np.sum((y-t)**2)

交叉熵误差:
E = − ∑ t k log ⁡ y k E=-\sum t_k\log y_k E=tklogyk
由于实际值数组中除正确分类对应位置的元素为1,其余都为0,实际上交叉熵误差的值就是负的预测值对应位置元素的自然对数。

为了防止运算中 y k y_k yk内有元素为0导致的错误情况,对每个元素加上一个小值delta。

实现代码:

def cross_entropy_error(y,t):
    delta = 1e-7
    return -np.sum(t*np.log(y+delta))

以上损失函数的实现实际上对我们的实际值数组有要求,必须写成我最开始举例的那个形式,如三类中的第二类 ( 0 , 1 , 0 ) (0,1,0) (0,1,0)(称为one-hot表示),而我们先前读取的MNIST数据集很遗憾并非初始如此,必须经过手动调整。方式很多,此处放下不提。

总体来看,我们的神经网络本身具备的性质为:神经元个数、中间层激活函数、输出层激活函数、损失函数。针对不同问题,需要输入的参数是:输入层参数个数、输出层参数个数、权重矩阵、训练集。

神经网络的学习

以上按前向顺序描述了神经网络的运行流程,接下来是学习过程,也就是根据训练集找到最优的权重矩阵。我们可以数学化地将其描述为,求损失函数(因变量)值最小时的w(自变量)

学过高中数学的都知道,求最小值或极小值要求导。在多元函数的情况下,就是求偏导。现有的框架大都自带求导函数,但我们现在没有轮子……所以还得从头开始。

显然我们没法求出每个函数微分的解析解,我们采用数值法,也就是依据导数定义……最简单的一元函数求导数的代码如下:

def numerical_diff(f,x):
    h = 1e-4
    fx1 = f(x+h)
    fx2 = f(x-h)
    return (fx1+fx2)/2h

此为函数f在x点处的导数。若要求多元函数的导数并对每个变量求导的值放在一个数组里,函数会复杂一点,最终得到的数组即为梯度。

def numerical_gradient(f, x):
    #这里输入的x即为我们的权重矩阵,为二维数组
    h = 1e-4 # 0.0001
    grad = np.zeros_like(x) # 生成和x形状相同的数组
    for idx in range(x.shape[0]):
        for idy in range(x.shape[1]):
            tmp_val = x[idx][idy]
            # f(x+h)的计算
            x[idx][idy] = tmp_val + h
            fxh1 = f(x)
            # f(x-h)的计算
            x[idx][idy] = tmp_val - h
            fxh2 = f(x)
            grad[idx][idy] = (fxh1 - fxh2) / (2*h)
            x[idx][idy] = tmp_val # 还原值
    return grad

接下来就是求极小值的过程了,也即为梯度下降法。具体的流程简单来说就是依据梯度中对应的导数值令相应变量向对应方向移动一步,由数学原理可知应当离极小值点更近,再次计算梯度,重复以上流程。其中需要注意的参数是,每次移动的步幅以及重复次数。

步幅对应的参数为学习率(learning-rate),过小过大都不合适,这个需要多次尝试以找到最优解。

代码如下:

def gradient_descent(f, init_x, lr=0.01, step_num=10): 
    #参数依次为:目标函数、初始值、学习率、重复次数
    x = init_x 
    for i in range(step_num):
        grad = numerical_gradient(f, x)
        x -= lr * grad
    return x

到这儿,所有准备工作差不多都完成了。当然,训练过程中我们不可能直接拿有60000个数据的训练集整体来学习,而是从中抽取较小的一簇数据,这一步就略过不提。

下面来搭建神经网络:

class simpleNet:
    def __init__(self,input_layel,output_layel,batch,x,t):
        #输入参数个数、输出参数个数、学习的一簇数据个数、图像集、标签集
        self.input_layel = input_layel
        self.output_layel = output_layel
        self.batch = batch
        self.x = x
        self.t = t
        self.w = np.random.randn(input_layel,output_layel)
    def predict(self,w):
        #输入权重矩阵
        step_1 = np.dot(self.x,w)
        step_2 = ReLU_function(step_1) #采用ReLU函数作为激活函数
        for idx in range(len(step_2)):
            step_2[idx] = softmax(step_2[idx])
        return step_2     			   #假装自己还有一层输出层
    def loss(self,w):
        n = self.batch
        loss = 0
        for i in range(self.batch):
            loss += cross_entropy_error(self.predict(w)[i],self.t[i])
        loss = loss/n #计算损失函数的均值
        return loss

开始攻击MNIST数据集:

train_batch = get_batch_100(images,labels)
#写一个函数来随机抽取训练集,这里抽100个
im_batch = train_batch[0]
lb_batch = train_batch[1]
My_first_net = simpleNet(784,10,100,im_batch,lb_batch)
w = My_first_net.w
w = gradient_descent(My_first_net.loss,w)

这样应该就能获得一个对应的权重矩阵来分类了(理论上)

当然一开始就说过,算法过于原始,实际上是跑不出来的。。。

你已经掌握了神经网络最基本的原理了,接下来要做的就是提高效率。

To be continued……

参考书:《深度学习入门:基于Python的理论与实现》 by 斋藤康毅

码农小蓝开业大吉!承接数据分析、代爬数据、机器学习等业务,小本经营,遵纪守法,私聊议价
【闲鱼】https://m.tb.cn/h.fK1wSxY?tk=oPUg2TuPzTD
有需求可加Q群:1013644195

你可能感兴趣的:(python,深度学习,神经网络)