百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分类 > 正文

快速实现神经网络_快速实现神经网络的技术

ztj100 2025-09-06 16:15 11 浏览 0 评论

【编者按】和Piotr Skalski一起,基于NumPy手写神经网络,通过亲自动手实践,加深对神经网络内部机制的理解。

Keras、TensorFlow、PyTorch等高层框架让我们可以快速搭建复杂模型。然而,花一点时间了解下底层概念是值得的。前不久我发过一篇文章,以简单的方式解释了神经网络是如何工作的。但这是一篇高度理论性的文章,主要以数学为主(数学是神经网络超能力的来源)。我打算以实践的方式继续这一主题,在这篇文章中,我们将尝试仅仅基于NumPy创建一个可以工作的神经网络。最后,我们将测试一下创建的模型——用它来解决简单的分类问题,并和基于Keras搭建的神经网络比较一下。

显然,今天的文章会包含很多Python代码,我希望你不会因此觉得枯燥。我同时也把文中所有的代码发在GitHub上:
SkalskiP/ILearnDeepLearning.py

概览

在开始编程之前,先让我们准备一份基本的路线图。我们的目标是创建一个特定架构(层数、层大小、激活函数)的密集连接神经网络。然后训练这一神经网络并做出预测。

上面的示意图展示了训练网络时进行的操作,以及单次迭代不同阶段需要更新和读取的参数。

初始化神经网络层

让我们从初始化每一层的权重矩阵W和偏置向量b开始。下图展示了网络层l的权重矩阵和偏置向量,其中,上标[l]表示当前层的索引,n表示给定层中的神经元数量。

我们的程序也将以类似的列表形式描述神经网络架构。列表的每一项是一个字典,描述单个网络层的基本参数:input_dim是网络层输入的信号向量的大小,output_dim是网络层输出的激活向量的大小,activation是网络层所用的激活函数。

nn_architecture = [
    {"input_dim": 2, "output_dim": 4, "activation": "relu"},
    {"input_dim": 4, "output_dim": 6, "activation": "relu"},
    {"input_dim": 6, "output_dim": 6, "activation": "relu"},
    {"input_dim": 6, "output_dim": 4, "activation": "relu"},
    {"input_dim": 4, "output_dim": 1, "activation": "sigmoid"},
]

如果你熟悉这一主题,你的脑海中大概已经回荡起焦急的声音:“喂喂!搞错了!这里有些是不必要的……”这一次,你内心的声音是对的,一个网络层的输出向量同时也是下一层的输入,所以其实只用列出两者之一就够了。不过,我故意使用这样的表示法,使每一层的格式保持一致,也让初次接触这一主题的人更容易理解代码。

def init_layers(nn_architecture, seed = 99):
    np.random.seed(seed)
    number_of_layers = len(nn_architecture)
    params_values = {}

    for idx, layer in enumerate(nn_architecture):
        layer_idx = idx + 1
        layer_input_size = layer["input_dim"]
        layer_output_size = layer["output_dim"]

        params_values['W' + str(layer_idx)] = np.random.randn(
            layer_output_size, layer_input_size) * 0.1
        params_values['b' + str(layer_idx)] = np.random.randn(
            layer_output_size, 1) * 0.1

    return params_values

上面的代码初始化了网络层的参数。注意我们用随机的小数字填充矩阵W和向量b。这并不是偶然的。权重值无法使用相同的数字初始化,否则会造成破坏性的对称问题基本上,如果权重都一样,不管输入X是什么,隐藏层的所有单元也都一样。这样,我们就会陷入初始状态,不管训练多久,网络多深,都无望摆脱。线性代数不会原谅我们。

小数值增加了算法的效率。我们可以看看下面的sigmoid函数图像,大数值处的函数图像几乎是扁平的,这会对神经网络的学习速度造成显著影响。所有参数使用小随机数是一个简单的方法,但它保证了算法有一个足够好的开始。

激活函数

激活函数只需一行代码就可以定义,但它们给神经网络带来了非线性和所需的表达力。“没有它们,神经网络将变成线性函数的组合,也就是单个线性函数。”激活函数有很多种,但在这个项目中,我决定使用其中两种——sigmoid和ReLU。为了同时支持前向传播和反向传播,我们还需要准备好它们的导数。

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

def relu(Z):
    return np.maximum(0,Z)

def sigmoid_backward(dA, Z):
    sig = sigmoid(Z)
    return dA * sig * (1 - sig)

def relu_backward(dA, Z):
    dZ = np.array(dA, copy = True)
    dZ[Z <= 0] = 0;
    return dZ;

前向传播

我们设计的神经网络有一个简单的架构。输入矩阵X传入网络,沿着隐藏单元传播,最终得到预测向量Y_hat。为了让代码更易读,我将前向传播拆分成两个函数——单层前向传播,和整个神经网络前向传播。

def single_layer_forward_propagation(A_prev, W_curr, b_curr, activation="relu"):
    Z_curr = np.dot(W_curr, A_prev) + b_curr

    if activation is "relu":
        activation_func = relu
    elif activation is "sigmoid":
        activation_func = sigmoid
    else:
        raise Exception('Non-supported activation function')

    return activation_func(Z_curr), Z_curr

这部分代码大概是最直接,最容易理解的。给定来自上一层的输入信号,我们计算仿射变换Z,接着应用选中的激活函数。基于NumPy,我们可以对整个网络层和整批样本一下子进行矩阵操作,无需迭代,这大大加速了计算。除了计算结果外,函数还返回了一个反向传播时需要用到的中间值Z

基于单层前向传播函数,编写整个前向传播步骤很容易。这是一个略微复杂一点的函数,它的角色不仅是进行预测,还包括组织中间值。

def full_forward_propagation(X, params_values, nn_architecture):
    memory = {}
    A_curr = X

    for idx, layer in enumerate(nn_architecture):
        layer_idx = idx + 1
        A_prev = A_curr

        activ_function_curr = layer["activation"]
        W_curr = params_values["W" + str(layer_idx)]
        b_curr = params_values["b" + str(layer_idx)]
        A_curr, Z_curr = single_layer_forward_propagation(A_prev, W_curr, b_curr, activ_function_curr)

        memory["A" + str(idx)] = A_prev
        memory["Z" + str(layer_idx)] = Z_curr

    return A_curr, memory

损失函数

损失函数可以监测进展,确保我们向着正确的方向移动。“一般来说,损失函数是为了显示我们离‘理想’解答还有多远。”损失函数根据我们计划解决的问题而选用,Keras之类的框架提供了很多选项。因为我计划将神经网络用于二元分类问题,我决定使用交叉熵:

为了取得更多关于学习过程的信息,我决定另外实现一个计算精确度的函数。

def get_cost_value(Y_hat, Y):
    m = Y_hat.shape[1]
    cost = -1 / m * (np.dot(Y, np.log(Y_hat).T) + np.dot(1 - Y, np.log(1 - Y_hat).T))
    return np.squeeze(cost)

def get_accuracy_value(Y_hat, Y):
    Y_hat_ = convert_prob_into_class(Y_hat)
    return (Y_hat_ == Y).all(axis=0).mean()

反向传播

不幸的是,很多缺乏经验的深度学习爱好者都觉得反向传播很吓人,难以理解。微积分和线性代数的组合经常会吓退那些没有经过扎实的数学训练的人。所以不要过于担心你现在还不能理解这一切。相信我,我们都经历过这个过程。

def single_layer_backward_propagation(dA_curr, W_curr, b_curr, Z_curr, A_prev, activation="relu"):
    m = A_prev.shape[1]

    if activation is "relu":
        backward_activation_func = relu_backward
    elif activation is "sigmoid":
        backward_activation_func = sigmoid_backward
    else:
        raise Exception('Non-supported activation function')

    dZ_curr = backward_activation_func(dA_curr, Z_curr)
    dW_curr = np.dot(dZ_curr, A_prev.T) / m
    db_curr = np.sum(dZ_curr, axis=1, keepdims=True) / m
    dA_prev = np.dot(W_curr.T, dZ_curr)

    return dA_prev, dW_curr, db_curr

人们经常搞混反向传播和梯度下降,但事实上它们不一样。前者是为了高效地计算梯度,后者则是为了基于计算出的梯度进行优化。在神经网络中,我们计算损失函数在参数上的梯度,但反向传播可以用来计算任何函数的导数。反向传播算法的精髓在于递归地使用求导的链式法则,通过组合导数已知的函数,计算函数的导数。下面的公式描述了单个网络层上的反向传播过程。由于本文的重点在实际实现,所以我将省略求导过程。从公式上我们可以很明显地看到,为什么我们需要在前向传播时记住中间层的AZ矩阵的值。

和前向传播一样,我决定将计算拆分成两个函数。之前给出的是单个网络层的反向传播函数,基本上就是以NumPy方式重写上面的数学公式。而定义完整反向传播过程的函数,主要是读取、更新三个字典中的值。

def full_backward_propagation(Y_hat, Y, memory, params_values, nn_architecture):
    grads_values = {}
    m = Y.shape[1]
    Y = Y.reshape(Y_hat.shape)

    dA_prev = - (np.divide(Y, Y_hat) - np.divide(1 - Y, 1 - Y_hat));

    for layer_idx_prev, layer in reversed(list(enumerate(nn_architecture))):
        layer_idx_curr = layer_idx_prev + 1
        activ_function_curr = layer["activation"]

        dA_curr = dA_prev

        A_prev = memory["A" + str(layer_idx_prev)]
        Z_curr = memory["Z" + str(layer_idx_curr)]
        W_curr = params_values["W" + str(layer_idx_curr)]
        b_curr = params_values["b" + str(layer_idx_curr)]

        dA_prev, dW_curr, db_curr = single_layer_backward_propagation(
            dA_curr, W_curr, b_curr, Z_curr, A_prev, activ_function_curr)

        grads_values["dW" + str(layer_idx_curr)] = dW_curr
        grads_values["db" + str(layer_idx_curr)] = db_curr

    return grads_values

基于单个网络层的反向传播函数,我们从最后一层开始迭代计算所有参数上的导数,并最终返回包含所需梯度的python字典。

更新参数值

反向传播是为了计算梯度,以根据梯度进行优化,更新网络的参数值。为了完成这一任务,我们将使用两个字典作为函数参数:params_values,其中保存了当前参数值;grads_values,其中保存了用于更新参数值所需的梯度信息。现在我们只需在每个网络层上应用以下等式即可。这是一个非常简单的优化算法,但我决定使用它作为更高级的优化算法的起点(大概会是我下一篇文章的主题)。

def update(params_values, grads_values, nn_architecture, learning_rate):
    for idx, layer in enumerate(nn_architecture):
        layer_idx = idx + 1
        params_values["W" + str(layer_idx)] -= learning_rate * grads_values["dW" + str(layer_idx)]
        params_values["b" + str(layer_idx)] -= learning_rate * grads_values["db" + str(layer_idx)]

    return params_values;

整合一切

万事俱备只欠东风。最困难的部分已经完成了——我们已经准备好了所需的函数,现在只需以正确的顺序把它们放到一起。

def train(X, Y, nn_architecture, epochs, learning_rate):
    params_values = init_layers(nn_architecture, 2)
    cost_history = []
    accuracy_history = []

    for i in range(epochs):
        Y_hat, cashe = full_forward_propagation(X, params_values, nn_architecture)
        cost = get_cost_value(Y_hat, Y)
        cost_history.append(cost)
        accuracy = get_accuracy_value(Y_hat, Y)
        accuracy_history.append(accuracy)

        grads_values = full_backward_propagation(Y_hat, Y, cashe, params_values, nn_architecture)
        params_values = update(params_values, grads_values, nn_architecture, learning_rate)

    return params_values, cost_history, accuracy_history

大卫对战歌利亚

该是看看我们的模型能不能解决一个简单的分类问题的时候了。我生成了一个包含两个分类的数据点的数据集,如下图所示。

作为对比,我用高层框架Keras搭建了一个模型。两个模型采用相同的架构和学习率。不过这仍然是一场不公平的较量,因为我们的模型用的都是最简单的方案。最终,基于NumPy和Keras的模型在测试集上都取得了类似的精确度(95%)。不过,我们的模型收敛的速度要慢很多倍。在我看来,这主要是因为我们的模型缺乏恰当的优化算法。

#头条创作挑战赛##今日头条小助手##神经网络#

相关推荐

sharding-jdbc实现`分库分表`与`读写分离`

一、前言本文将基于以下环境整合...

三分钟了解mysql中主键、外键、非空、唯一、默认约束是什么

在数据库中,数据表是数据库中最重要、最基本的操作对象,是数据存储的基本单位。数据表被定义为列的集合,数据在表中是按照行和列的格式来存储的。每一行代表一条唯一的记录,每一列代表记录中的一个域。...

MySQL8行级锁_mysql如何加行级锁

MySQL8行级锁版本:8.0.34基本概念...

mysql使用小技巧_mysql使用入门

1、MySQL中有许多很实用的函数,好好利用它们可以省去很多时间:group_concat()将取到的值用逗号连接,可以这么用:selectgroup_concat(distinctid)fr...

MySQL/MariaDB中如何支持全部的Unicode?

永远不要在MySQL中使用utf8,并且始终使用utf8mb4。utf8mb4介绍MySQL/MariaDB中,utf8字符集并不是对Unicode的真正实现,即不是真正的UTF-8编码,因...

聊聊 MySQL Server 可执行注释,你懂了吗?

前言MySQLServer当前支持如下3种注释风格:...

MySQL系列-源码编译安装(v5.7.34)

一、系统环境要求...

MySQL的锁就锁住我啦!与腾讯大佬的技术交谈,是我小看它了

对酒当歌,人生几何!朝朝暮暮,唯有己脱。苦苦寻觅找工作之间,殊不知今日之事乃我心之痛,难道是我不配拥有工作嘛。自面试后他所谓的等待都过去一段时日,可惜在下京东上的小金库都要见低啦。每每想到不由心中一...

MySQL字符问题_mysql中字符串的位置

中文写入乱码问题:我输入的中文编码是urf8的,建的库是urf8的,但是插入mysql总是乱码,一堆"???????????????????????"我用的是ibatis,终于找到原因了,我是这么解决...

深圳尚学堂:mysql基本sql语句大全(三)

数据开发-经典1.按姓氏笔画排序:Select*FromTableNameOrderByCustomerNameCollateChinese_PRC_Stroke_ci_as//从少...

MySQL进行行级锁的?一会next-key锁,一会间隙锁,一会记录锁?

大家好,是不是很多人都对MySQL加行级锁的规则搞的迷迷糊糊,一会是next-key锁,一会是间隙锁,一会又是记录锁。坦白说,确实还挺复杂的,但是好在我找点了点规律,也知道如何如何用命令分析加...

一文讲清怎么利用Python Django实现Excel数据表的导入导出功能

摘要:Python作为一门简单易学且功能强大的编程语言,广受程序员、数据分析师和AI工程师的青睐。本文系统讲解了如何使用Python的Django框架结合openpyxl库实现Excel...

用DataX实现两个MySQL实例间的数据同步

DataXDataX使用Java实现。如果可以实现数据库实例之间准实时的...

MySQL数据库知识_mysql数据库基础知识

MySQL是一种关系型数据库管理系统;那废话不多说,直接上自己以前学习整理文档:查看数据库命令:(1).查看存储过程状态:showprocedurestatus;(2).显示系统变量:show...

如何为MySQL中的JSON字段设置索引

背景MySQL在2015年中发布的5.7.8版本中首次引入了JSON数据类型。自此,它成了一种逃离严格列定义的方式,可以存储各种形状和大小的JSON文档,例如审计日志、配置信息、第三方数据包、用户自定...

取消回复欢迎 发表评论: