从头来写一个简单全连接网络(python 版)—全连接神经网络(1)

861 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第7天,点击查看活动详情

动机

做什么事都是有动机的,也就是我们需要找一个理由来做这件事。那么我们为什么要从头要写一个简单 python 版本的神经网络。当下这么多好用神经网络框架让我们可以随心所欲来定义网络。那么我写的一定没有他们好,虽然凡事没有绝对,但是基本我是没有可能超过他们,呵呵,其实没有可比性。但是为什么我还是想要写,这不是浪费时间吗? 其实就是为了安德烈卡帕斯一句建议学习深度学习最好自己写一个网络,这就是我要从无到有写一个网络动机。

设计模型

首先我们模型就是参数的集合,在我们函数集也就是我们网络结构确定了,也就是这个函数集确定,我们通过训练就是要在这个函数集中找到一个合适函数,每一个函数都对应着参数,也就是这些参数是用于区分函数集中函数之间差别。

前提

我们需要对神经网络了解,不过这里都是理论上,需要从头实现神经网络,使用函数式编程,

  • 为了方便解释说明各个步骤,例如构建模型、前向传播、反向传播,更新参数,我们没有基于面向对象思想进行编码,而是采用函数式方式进行编码
  • 分享中会重点详细介绍反向传播的过程,这个是神经网络实现的一个难点
  • 通过使用 numpy 来实现梯度下降对参数优化,这样大家对于梯度下降会有一个更深刻的认识

首先模型 model 以字典形式存储,以字典形式存储参数,

%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt

这里我们仅借助 numpy 这个操作数组的库,以及 matplotlib 做一些可视化工作。

初始化模型

首先要说明这里实现网络还是比较初级的,是一个简单的、串联结构网络,并不存在什么复杂结构。但是也希望自己这一系列分享不会止步于此。

def initialize_model(dimensions):
    '''
    输入 dimension 是一个定义网络各个层神经元个数的列表,返回是一个具有以下字段的字典结构的
    通过这个字典结果我们可以还原出一个网络大概结构或者说是形状
      model['nlayers'] : 神经网络的层数
      model['weights'] : 神经网络各层的权重(weight)矩阵的集合
      model['biases'] :  神经网络各层的偏置(bias)向量的集合
    '''
    weights, biases = [], []
    L = len(dimensions) - 1 # number of layers (i.e., excludes input layer)
    for l in range(L):
        W = np.random.randn(dimensions[l+1], dimensions[l])
        b = np.random.randn(dimensions[l+1], 1)
        weights.append(W)
        biases.append(b)
    return dict(weights=weights, biases=biases, nlayers=L)

根据输入,我们可以看到输入是一个 784 维度向量,隐含层是 15 神经网元的全连接层,输出层 10 维向量。看到这里做过神经网络一定露出笑容,这不是 hello MNIST 吗,神经网络中的 hello world,784 是数字尺寸 28 x 28 大小图像展平的维度,输出 10 是 0 到 9 10 个数字的类别。

dimensions = [784, 15, 10]
model = initialize_model(dimensions)
for k, (W, b) in enumerate(zip(model['weights'], model['biases'])):
    print(f'Layer {k+1}:\tShape of W{k+1}: {W.shape}\tShape of b{k+1}: {b.shape}')
Layer 1: Shape of W1: (15, 784) Shape of b1: (15, 1) 
Layer 2: Shape of W2: (10, 15) Shape of b2: (10, 1)

其实这里只有 2 层全连接,其中第一层权重参数 W15×784W_{15 \times 784} 而输入是一个 784 维向量

y15×1=W15×784x784×1+b15×1y_{15 \times 1} = W_{15 \times 784}x_{784 \times 1} + b_{15 \times 1}

而对于 2 层,也就是所谓输出层

我们可以同过下面代码来查看具体权重随机初始化的值

print(f'W2:\n\n{model["weights"][1]}')  
print(f'b2:\n\n{model["biases"][1]}')   

实现激活函数、loss 函数以及其求导

激活函数

这里激活函数采用 sigmoid

σ(x)=11+exp(x)=exp(x)1+exp(x)\sigma(x) = \frac{1}{1 + \exp(-x)} = \frac{\exp(x)}{1 + \exp(x)}
import math

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

sigmoid 函数求导

σ(x)=σ(x)(1σ(x))\sigma^{\prime}(x) = \sigma(x)(1 - \sigma(x))

为了增加 sigmoid 函数对于较大整数或者较大负数的 robust 修改如下

σ(x)={11+exp(x)x0111+exp(x)otherwise.\sigma(x) = \begin{cases} \frac{1}{1 + \exp(-x)} & x \ge 0\\ 1 -\frac{1}{1 + \exp(x)} & \text{otherwise.} \end{cases}
def sigma(x):
    '''这里输入是一个向量'''
    return np.where(x>=0, 1/(1+np.exp(-x)), 1 - 1/(1+np.exp(x))) 
def sigma_prime(x):
    return sigma(x)*(1-sigma(x)) # Derivative of logistic function