qbytearray初始化全0_全连接神经网络中的前向传播和反向传播过程+numpy实现

223444b63d90da8331b04f8320547a08.png

本文基于mnist手写数字识别任务,建立全连接前馈神经网络,详细分析其中的前向传播和反向传播过程,并使用numpy实现,用于解决手写数字识别任务。

注意: 本文中利用numpy实现全连接神经网络没有涉及太多封装(后续考虑封装成Pytorch的接口形式),只保留最简单的前向反向过程。


利用神经网络解决问题的训练过程如下:

  1. 建立网络结构,初始化网络模型参数;
  2. 前向传播得到模型的预测分布,并用损失函数计算预测分布和真实分布之间的损失值
  3. 反向传播求解损失函数对模型参数的梯度
  4. 更新模型参数
  5. 不断重复2-4的步骤,直到模型的性能达到任务要求

注意: 为了与输入数据(batch_size, input_size)中每个样本是行向量的形式保持一致, 本文推导过程中使用的单个样本的向量均为行向量,如果使用列向量进行推导计算需要进行相应的变换。

1 准备数据

def load_data(path):
    """
        Args:
            path: mnist数据集路径
        return:
            training_data: tuple
                training_data[0]: 输入数据, (num_samples, input_size) 即(50000, 784)
                training_data[1]: 标签, (num_samples, ) 即(50000, )
    """
    f = gzip.open(path)
    training_data, val_data, test_data = pickle.load(f, encoding='bytes')
    f.close()
    return training_data, val_data, test_data

数据集采用的是 mnist数据, 加载进来返回三部分: training_data, validation_data, test_data; 每部分是个tuple对象,包含两个numpy的ndarray:

  • [0] 输入数据 (num_samples, input_features)
  • [1] 标签 (num_samples, )

深度学习中训练和评估都是使用批处理数据,因此我们要对数据集进行batch化:

def load_batches(data, batch_size):
    """
        对数据洗牌,并分成一个个batch
        Args:
            data: tuple, 训练集 or 验证集
                data[0]: 输入数据, (num_samples, input_size)
                data[1]: 标签, (num_samples, )
            batch_size: 
        Return:
            batches_x: list
                batches_x[0]: (batch_size, input_size)
            batches_y: list
                batches_y[0]: (batch_size, )
    """
    n = len(data[0])
    # 对数据进行洗牌
    shuffle_idx = random.sample(range(n), n)
    X = data[0][shuffle_idx]
    Y = data[1][shuffle_idx]

    batches_x = [X[i: i+batch_size] for i in range(0, n, batch_size)]
    batches_y = [Y[i: i+batch_size] for i in range(0, n, batch_size)]

    return batches_x, batches_y

2 网络架构

我们首先要定义网络的结构,并初始化网络的模型参数(这里使用了标准正态分布进行初始化,未来将会介绍其他提高网络性能的初始化方法和其他提高网络性能的方法),在这里我们定义一个 Network类:

class Network(object):
    """
    fully-connected neural network

    Attributions:
        sizes: list, 每个元素是每层的神经元的个数, 包括输入输出层
        num_layers: 神经网络的层数
        weights: list, 每个元素是一层神经网络的权重
        bias: list, 每个元素是一层神经网络的偏置
    """
    def __init__(self, sizes):
        self.sizes = sizes
        self.num_layers = len(sizes)
        self.weights = [np.random.randn(i, j) for i, j in zip(self.sizes[:-1], self.sizes[1:])]
        self.bias = [np.random.randn(1, j) for j in self.sizes[1:]]

注意:

  1. 注意层与层之间网络权重的形状, 后面我们实现的是batch版本的训练过程,输入的数据形状是: (batch_size, input_size), 每个样本是行向量的形式,所以权重的形状是(权重输入神经元的个数, 权重输出神经元的个数), 例如部分网络如下图所示时:

484398b478414f810a7287ebdc56070a.png

那么权重 W1的形状是(64, 32), W2的形状是(32, 10)。

  1. 注意权重向量对应的位置:

第l层的权重

, 连接的输入是
, 输出是

定义好网络的结构后还有要考虑的就是激活函数的选择,在这里我们使用Relu作为隐藏层的激活函数,在输出层使用softmax+cross-entropy的组合.

3 前向传播

层与层之间的前向传播分为两部分: 线性变换+非线性激活。

在这里用

表示线性变换的输出结果,也就是非线性激活的输入;
表示非线性激活的输出结果,也就是下一层的输入:

所以在这里我们的网络前向传播过程如下:

3f1d66a95083d5af7fea9fe16e8711f4.png

首先实现激活函数部分:

def relu(z):
    """
    Args:
        z: (batch_size, hidden_size)
    """
    flag = (z <= 0) # 需要修改为0的部分
    z[flag] = 0
    return z

def softmax(logits):
    """
    Args:
        logits: (batch_size, output_size)
    Returns:
        (batch_size, output_size)
    """
    max_row = np.max(z, axis=-1, keepdims=True)  # 每一个样本的所有分数中的最大值
    tmp = z - max_row
    return np.exp(tmp) / np.sum(np.exp(tmp), axis=-1, keepdims=True)

这里说一下softmax函数的实现:

公式

分母要对指数幂求和,容易超出最大限制,得到 nan,因此采取了一个技巧:将向量z 中的每个值减去向量z的最大值,然后再进行softmax运算,这样得到的结果是不变的:

线性变换的实现很容易, 就是矩阵乘积运算,这里只截取部分代码说明:

for weight, bias in zip(self.weights[:-1], self.bias[:-1]):
    z = np.dot(a, weight) + bias
    a = relu(z)

交叉熵损失函数的实现没有单独写,而是将其与softmax组合在一起:

def softmax_cross_entropy(logits, y):
    """
    Args:
        logits: (batch_size, output_size), 网络的输出预测得分, 还没有进行 softmax概率化
        y: (batch_size, ) 每个样本的真实label
    return:
        a: (batch_size, output_size)
        loss: scalar
    """
    n = logits.shape[0]
    a = softmax(logits)
    scores = a[range(n), y]
    loss = -np.sum(np.log(scores)) / n 
    return a, loss

注意传入的 y, 只包含每个样本的label.

进行推理预测时的前向传播实现:

def forward(self, x):
        """
        x: (batch_size, input_size)
        """
        a = x
        for weight, bias in zip(self.weights[:-1], self.bias[:-1]):
            z = np.dot(a, weight) + bias
            a = relu(z) 

        # 处理输出层
        # 在前向传播时不需要进行softmax概率化,反向传播时才会用到
        logits = np.dot(a, self.weights[-1]) + self.bias[-1]
        return logits

注意:

前向传播过程有两种

  • 推理预测时的前向传播,此时不需要用到softmax, 只用logits就可以做出预测
  • 训练时的前向传播,此时需要记录中间变量z和a, 并且需要进行softmax计算

训练时的前向传播过程在backward()中进行实现。

4 反向传播

反向传播是神经网络中的重点,它是一种快速高效 求解损失函数对模型参数的梯度的方法,也就是求解

, 它的主要数学工具就是链式法则, 应用到了四个公式。

介绍公式之前,先在这里引入一个记号:

, 并把
称为第l层的误差,
称为第l层第j个神经元的误差, 为什么可以把它称为误差呢?

我们看前向传播过程中的一个片段:

06edf837dea5a9eb35e4d8b5e25a7d80.png

第l层的第j个神经元, 这个神经元的值为

, 经过激活函数激活
然后继续向前传播,直到最后的损失函数,考虑一下这个值发生一点变化
,这时候变化会沿着网络层层向下传递,这个变化会对最终的损失值影响有多大呢?根据导数的定义,

所以

越大,说明模型改进的空间就越大,真实分布与预测分布之间的误差就越大;而
的值由
决定,
越大,
就越大, 误差就越大; 反之亦然,因此就可以说
的一种反映,是误差的度量。

下面介绍反向传播过程的四个公式:

  1. 输出层的误差
  2. 误差反向传播(就是相邻两层误差之间的递推关系)
  3. 误差与模型权重的关系
  4. 误差与模型偏置的关系

下面我将详细介绍这四个公式,以及背后的直觉理解。

4.1 输出层误差

输出层的误差很容易计算,它取决于你选择的损失函数和输出层的激活函数, 关于softmax layer + cross-entropy的反向传播过程在这里不详细介绍了,可以参考之前写过的一篇文章(神经网络多分类中softmax+cross-entropy的前向传播和反向传播过程),过程有些麻烦,但结果很漂亮,也很容易实现:

def derivation_softmax_cross_entropy(logits, y):
    """
    Args:
        logits: (batch_size, output_size), 网络的输出预测得分, 还没有进行 softmax概率化
        y: (batch_size, ) 每个样本的真实label

    Return:
        frac {partial C}{partial z^L}
        (batch_size, output_size)
    """
    n = logits.shape[0]
    a = softmax(logits)
    a[range(n), y] -= 1
    return a

上面实现的一个batch的输出层误差.

4.2 误差反向传播

这个公式反映的是两层误差之间的关系,其中

计算过程如下图所示:

fd7367c6923ffb372851012a9f2abf9b.png

分为两个阶段:

  1. 将上一层的误差按照前向传播过程原路返回:
  2. 通过激活函数

具体实现(截取部分代码):

for i in range(2, self.num_layers):
    dl = np.dot(dl, self.weights[-i+1].T) * derivation_relu(zs[-i])

4.3 误差与模型权重的关系

看一个具体的例子:

76d24670c87cc30c3fc4064f49d8f770.png

损失函数对某个权重的导数就是:该权重的激活输入 和 该权重输出神经元的误差的乘积.

权重的激活输入是在前向传播时计算各层的a, 权重输出神经元的误差是从输出层误差反向传播计算的。

4.4 误差与模型偏置的关系

具体实现:

for i in range(2, self.num_layers):
    dl = np.dot(dl, self.weights[-i+1].T) * derivation_relu(zs[-i])
    dws[-i] = np.dot(_as[-i-1].T, dl) / n
    dbs[-i] = np.sum(dl, axis=0, keepdims=True) / n

这里将每个样本求得的梯度求和作平均。

从上面我们可以看到,还需要求解激活函数的导数:

def derivation_relu(z):
    flag = (z <= 0)
    z[flag] = 0
    z[~flag] = 1
    return z

softmax和corss-entropy的导数在输出层误差部分已经合起来一起求解了。

5 参数更新

采用mini-batch的随机梯度下降法进行参数更新:

loss, dws, dbs = self.backward(x, y)
self.weights = [weight - learning_rate * dw for weight, dw in zip(self.weights, dws)]
self.bias = [bias - learning_rate * db for bias, db in zip(self.bias, dbs)]

尽管实现很粗糙,但效果还是很不错的

def main():
    path = "data/mnist.pkl.gz"
    training_data, validation_data, test_data = load_data(path)
    model = Network([784, 30, 10])
    model.train(training_data, validation_data, 1, 50, 100)

按照以上设置,在验证集上的准确率可以达到 0.94 左右。

完整实现见 https://github.com/ccilery/nn-from-scratch

6 总结

本文介绍了全连接前馈神经网络中前向传播和反向传播过程,并用numpy实现,应用到mnist手写数字识别中。本文的实现比较粗暴,没有进行一定的抽象,未来计划实现CNN、RNN和LSTM后,参照Pytorch的接口进行一定的封装,变成一个小型的神经网络库。

参考:

  1. http://neuralnetworksanddeeplearning.com/chap1.html
  2. http://neuralnetworksanddeeplearning.com/chap2.html
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值