100行Python代码,轻松搞定神经网络
off999 2024-10-14 12:14 27 浏览 0 评论
大数据文摘出品
来源:eisenjulian
编译:周家乐、钱天培
用tensorflow,pytorch这类深度学习库来写一个神经网络早就不稀奇了。
可是,你知道怎么用python和numpy来优雅地搭一个神经网络嘛?
现如今,有多种深度学习框架可供选择,他们带有自动微分、基于图的优化计算和硬件加速等各种重要特性。对人们而言,似乎享受这些重要特性带来的便利已经是理所当然的事儿了。但其实,瞧一瞧隐藏在这些特性下的东西,能更好的帮助你理解这些网络究竟是如何工作的。
所以今天,文摘菌就来手把手教大家搭一个神经网络。原料就是简单的python和numpy代码!
文章中的所有代码可以都在这儿获取。
https://colab.research.google.com/github/eisenjulian/slides/blob/master/NN_from_scratch/notebook.ipynb
符号说明
在计算反向传播时, 我们可以选择使用函数符号、变量符号去记录求导过程。它们分别对应了计算图中的边和节点来表示它们。
给定R^n→R和x∈R^n, 那么梯度是由偏导?f/?j(x)组成的n维行向量
如果f:R^n→R^m 和x∈R^n,那么 Jacobian矩阵是下列函数组成的一个m×n的矩阵。
对于给定的函数f和向量a和b如果a=f(b)那么我们用?a/?b 表示Jacobian矩阵,当a是实数时则表示梯度
链式法则
给定三个分属于不同向量空间的向量a∈A及c∈C和两个可微函数f:A→B及g:B→C使得f(a)=b和g(b)=c,我们能得到复合函数的Jacobian矩阵是函数f和g的jacobian矩阵的乘积:
这就是大名鼎鼎的链式法则。提出于上世纪60、70年代的反向传播算法就是应用了链式法则来计算一个实函数相对于其不同参数的梯度的。
要知道我们的最终目标是通过沿着梯度的相反方向来逐步找到函数的最小值 (当然最好是全局最小值), 因为至少在局部来说, 这样做将使得函数值逐步下降。当我们有两个参数需要优化时, 整个过程如图所示:
反向模式求导
假设函数fi(ai)=ai+1由多于两个函数复合而成,我们可以反复应用公式求导并得到:
可以有很多种方式计算这个乘积,最常见的是从左向右或从右向左。
如果an是一个标量,那么在计算整个梯度的时候我们可以通过先计算?an/?an-1并逐步右乘所有的Jacobian矩阵?ai/?ai-1来得到。这个操作有时被称作VJP或向量-Jacobian乘积(Vector-Jacobian Product)。
又因为整个过程中我们是从计算?an/?an-1开始逐步计算?an/?an-2,?an/?an-3等梯度到最后,并保存中间值,所以这个过程被称为反向模式求导。最终,我们可以计算出an相对于所有其他变量的梯度。
相对而言,前向模式的过程正相反。它从计算Jacobian矩阵如?a2/?a1开始,并左乘?a3/?a2来计算?a3/?a1。如果我们继续乘上?ai/?ai-1并保存中间值,最终我们可以得到所有变量相对于?a2/?a1的梯度。当?a2/?a1是标量时,所有乘积都是列向量,这被称为Jacobian向量乘积(或者JVP,Jacobian-Vector Product )。
你大概已经猜到了,对于反向传播来说,我们更偏向应用反向模式——因为我们想要逐步得到损失函数对于每层参数的梯度。正向模式虽然也可以计算需要的梯度, 但因为重复计算太多而效率很低。
计算梯度的过程看起来像是有很多高维矩阵相乘, 但实际上,Jacobian矩阵常常是稀疏、块或者对角矩阵,又因为我们只关心将其右乘行向量的结果,所以就不需要耗费太多计算和存储资源。
在本文中, 我们的方法主要用于按顺序逐层搭建的神经网络, 但同样的方法也适用于计算梯度的其他算法或计算图。
关于反向和正向模式的详尽描述可以参考这里?
http://colah.github.io/posts/2015-08-Backprop/
深度神经网络
在典型的监督机器学习算法中, 我们通常用到一个很复杂函数,它的输入是存有标签样本数值特征的张量。此外,还有很多用于描述模型的权重张量。
损失函数是关于样本和权重的标量函数, 它是衡量模型输出与预期标签的差距的指标。我们的目标是找到最合适的权重让损失最小。在深度学习中, 损失函数被表示为一串易于求导的简单函数的复合。所有这些简单函数(除了最后一个函数),都是我们指的层, 而每一层通常有两组参数: 输入 (可以是上一层的输出) 和权重。
而最后一个函数代表了损失度量, 它也有两组参数: 模型输出y和真实标签y^。例如, 如果损失度量l为平方误差, 则?l/?y为 2 avg(y-y^)。损失度量的梯度将是应用反向模式求导的起始行向量。
Autograd
自动求导背后的思想已是相当成熟了。它可以在运行时或编译过程中完成,但如何实现会对性能产生巨大影响。我建议你能认真阅读 HIPS autograd的 Python 实现,来真正了解autograd。
核心想法其实始终未变。从我们在学校学习如何求导时, 就应该知道这一点了。如果我们能够追踪最终求出标量输出的计算, 并且我们知道如何对简单操作求导 (例如加法、乘法、幂、指数、对数等等), 我们就可以算出输出的梯度。
假设我们有一个线性的中间层f,由矩阵乘法表示(暂时不考虑偏置):
为了用梯度下降法调整w值,我们需要计算梯度?l/?w。这里我们可以观察到,改变y从而影响l是一个关键。
每一层都必须满足下面这个条件: 如果给出了损失函数相对于这一层输出的梯度, 就可以得到损失函数相对于这一层输入(即上一层的输出)的梯度。
现在应用两次链式法则得到损失函数相对于w的梯度:
相对于x的是:
因此, 我们既可以后向传递一个梯度, 使上一层得到更新并更新层间权重, 以优化损失, 这就行啦!
动手实践
先来看看代码, 或者直接试试Colab Notebook
https://colab.research.google.com/github/eisenjulian/slides/blob/master/NN_from_scratch/notebook.ipynb
我们从封装了一个张量及其梯度的类(class)开始。
现在我们可以创建一个layer类,关键的想法是,在前向传播时,我们返回这一层的输出和可以接受输出梯度和输入梯度的函数,并在过程中更新权重梯度。
然后, 训练过程将有三个步骤, 计算前向传递, 然后后向传递, 最后更新权重。这里关键的一点是把更新权重放在最后, 因为权重可以在多个层中重用,我们更希望在需要的时候再更新它。
class Layer: def __init__(self): self.parameters = [] def forward(self, X): """ Override me! A simple no-op layer, it passes forward the inputs """ return X, lambda D: D def build_param(self, tensor): """ Creates a parameter from a tensor, and saves a reference for the update step """ param = Parameter(tensor) self.parameters.append(param) return param def update(self, optimizer): for param in self.parameters: optimizer.update(param)
标准的做法是将更新参数的工作交给优化器, 优化器在每一批(batch)后都会接收参数的实例。最简单和最广为人知的优化方法是mini-batch随机梯度下降。
class SGDOptimizer(): def __init__(self, lr=0.1): self.lr = lr def update(self, param): param.tensor -= self.lr * param.gradient param.gradient.fill(0)
在此框架下, 并使用前面计算的结果后, 线性层如下所示:
class Linear(Layer): def __init__(self, inputs, outputs): super().__init__() tensor = np.random.randn(inputs, outputs) * np.sqrt(1 / inputs) self.weights = self.build_param(tensor) self.bias = self.build_param(np.zeros(outputs)) def forward(self, X): def backward(D): self.weights.gradient += X.T @ D self.bias.gradient += D.sum(axis=0) return D @ self.weights.tensor.T return X @ self.weights.tensor + self.bias.tensor, backward
接下来看看另一个常用的层,激活层。它们属于点式(pointwise)非线性函数。点式函数的 Jacobian矩阵是对角矩阵, 这意味着当乘以梯度时, 它是逐点相乘的。
class ReLu(Layer): def forward(self, X): mask = X > 0 return X * mask, lambda D: D * mask
计算Sigmoid函数的梯度略微有一点难度,而它也是逐点计算的:
class Sigmoid(Layer): def forward(self, X): S = 1 / (1 + np.exp(-X)) def backward(D): return D * S * (1 - S) return S, backward
当我们按序构建很多层后,可以遍历它们并先后得到每一层的输出,我们可以把backward函数存在一个列表内,并在计算反向传播时使用,这样就可以直接得到相对于输入层的损失梯度。就是这么神奇:
class Sequential(Layer): def __init__(self, *layers): super().__init__() self.layers = layers for layer in layers: self.parameters.extend(layer.parameters) def forward(self, X): backprops = [] Y = X for layer in self.layers: Y, backprop = layer.forward(Y) backprops.append(backprop) def backward(D): for backprop in reversed(backprops): D = backprop(D) return D return Y, backward
正如我们前面提到的,我们将需要定义批样本的损失函数和梯度。一个典型的例子是MSE,它被常用在回归问题里,我们可以这样实现它:
def mse_loss(Yp, Yt): diff = Yp - Yt return np.square(diff).mean(), 2 * diff / len(diff)
就差一点了!现在,我们定义了两种层,以及合并它们的方法,下面如何训练呢?我们可以使用类似于scikit-learn或者Keras中的API。
class Learner(): def __init__(self, model, loss, optimizer): self.model = model self.loss = loss self.optimizer = optimizer def fit_batch(self, X, Y): Y_, backward = self.model.forward(X) L, D = self.loss(Y_, Y) backward(D) self.model.update(self.optimizer) return L def fit(self, X, Y, epochs, bs): losses = [] for epoch in range(epochs): p = np.random.permutation(len(X)) X, Y = X[p], Y[p] loss = 0.0 for i in range(0, len(X), bs): loss += self.fit_batch(X[i:i + bs], Y[i:i + bs]) losses.append(loss) return losses
这就行了!如果你跟随着我的思路,你可能就会发现其实有几行代码是可以被省掉的。
这代码能用不?
现在可以用一些数据测试下我们的代码了。
X = np.random.randn(100, 10) w = np.random.randn(10, 1) b = np.random.randn(1) Y = X @ W + B model = Linear(10, 1) learner = Learner(model, mse_loss, SGDOptimizer(lr=0.05)) learner.fit(X, Y, epochs=10, bs=10)
我一共训练了10轮。
我们还能检查学到的权重和真实的权重是否一致。
print(np.linalg.norm(m.weights.tensor - W), (m.bias.tensor - B)[0]) > 1.848553648022619e-05 5.69305886743976e-06
好了,就这么简单。让我们再试试非线性数据集,例如y=x1x2,并且再加上一个Sigmoid非线性层和另一个线性层让我们的模型更复杂些。像下面这样:
X = np.random.randn(1000, 2) Y = X[:, 0] * X[:, 1] losses1 = Learner( Sequential(Linear(2, 1)), mse_loss, SGDOptimizer(lr=0.01) ).fit(X, Y, epochs=50, bs=50) losses2 = Learner( Sequential( Linear(2, 10), Sigmoid(), Linear(10, 1) ), mse_loss, SGDOptimizer(lr=0.3) ).fit(X, Y, epochs=50, bs=50) plt.plot(losses1) plt.plot(losses2) plt.legend(['1 Layer', '2 Layers']) plt.show()
比较单一层vs两层模型在使用sigmoid激活函数的情况下的训练损失。
最后
希望通过搭建这个简单的神经网络,你已经掌握了用python和numpy实现神经网络的基本思路。
在这篇文章中,我们只定义了三种类型的层和一个损失函数, 所以还有很多事情可做,但基本原理都相似。感兴趣的同学可以试着实现更复杂的神经网络哦!
References
[1] Thinc Deep Learning Library
https://github.com/explosion/thinc
[2] PyTorch Tutorial
https://pytorch.org/tutorials/beginner/nn_tutorial.html
[3] Calculus on Computational Graphs
http://colah.github.io/posts/2015-08-Backprop/
[4] HIPS Autograd
https://github.com/HIPS/autograd
相关报道:
https://eisenjulian.github.io/deep-learning-in-100-lines/
相关推荐
- 戴尔官网保修查询入口(戴尔售后保质期查询)
-
可以按照以下步骤查询戴尔笔记本电脑的保修期:1.打开戴尔官网:https://www.戴尔.com/zh-cn/售后服务/保修政策.html2.点击页面上方的“服务与支持”按钮,进入戴尔的服务支持...
- 手机号邮箱登录入口(手机号邮箱官网)
-
手机163邮箱登录入口如下:163邮箱官网入口:https://smart.mail.163.com/login.htm点击进入登录或者注册邮箱即可。手机浏览器访问进入官网http://www.123...
- sd卡(sd卡无法读取怎么修复)
-
SD卡是大卡,相机用的;普通的手机内存卡,是小卡,正规的名称是macrosd卡,也就是微型SD卡。可以通过卡套转为普通的SD卡的大小。 其实就是大小不同。但手机上的内存卡,人们经常也俗称为SD...
- windows7蓝牙功能在哪里打开
-
点击搜索框在windows7系统主界面点击开始菜单,点击打开搜索框。输入命令输入services.msc后回车,在列表中找到并右击BluetoothSupportS...点击属性选择进入属性菜单,...
-
- 2010激活密钥(microsoft2010激活密钥)
-
步骤/方式1officeprofessionalplus2010:(office专业版)6QFdx-pYH2G-ppYFd-C7RJM-BBKQ8Bdd3G-xM7FB-Bd2HM-YK63V-VQFdKVYBBJ-TRJpB-QFQ...
-
2025-11-19 04:03 off999
- 联想官方刷新bios工具(联想电脑刷新bios)
-
刷新BIOS需要使用联想的官方网站或授权维修中心来进行操作。以下是一些基本步骤:1.访问联想的官方网站,找到BIOS更新程序并下载。在下载过程中,请确保选择与您计算机型号匹配的版本。2.将下载的B...
-
- 苹果ios14系统下载(苹果ios14.1下载)
-
1方法一步骤/方式一打开Appstore。步骤/方式二在搜索栏点击搜索框。步骤/方式三搜索并点击需要下载的软件。步骤/方式四点击获取。步骤/方式五最后验证ID密码即可。1.在应用商店搜索你要下载的应用名称。2.点击下载按钮,如果要求登...
-
2025-11-19 03:03 off999
- office2010怎么免费永久激活密钥
-
用这个试试,一个KMS激活工具可以激活2010到2019的Office自家的目前用的就是这个microsoft6477.moe/1716.html直接使用这个Microsoftoffice2010...
-
- 类似爱加速的国内ip(类似爱加速的app)
-
推荐“V8盒子”。这一款免费无广告的模拟器,不同于其它软件盒子,而是类似于X8沙箱,满足游戏多开,画中画,悬浮球操作,熄屏后台运行等多功能的沙箱盒子.支持一键root,一键安装xposed框架,能在安卓/苹果手机上运行多个安卓/ios虚拟系...
-
2025-11-19 02:03 off999
- 阿里旺旺手机客户端(阿里旺旺手机app)
-
手机淘宝的旺旺在打开商品后,会看到左下角有个旺旺的图标,点击就可以联系了。 阿里旺旺是将原先的淘宝旺旺与阿里巴巴贸易通整合在一起的一个新品牌。它是淘宝和阿里巴巴为商人量身定做的免费网上商务沟通软件,...
- 最纯净的pe装机工具(pe工具哪个纯净)
-
U盘装系统步骤:1.制作U盘启动盘。这里推荐大白菜U盘启动盘制作工具,在网上一搜便是。2.U盘启动盘做好了,我们还需要一个GHOST文件,可以从网上下载一个ghost版的XP/WIN7/WIN8系统,...
- 装一个erp系统多少钱(wms仓库管理软件)
-
现在主流有客户端ERP和云端ERP两种客户端通常一次买断,价格在万元左右,但是还有隐性费用,你需要支付服务器、数据管理员,此外如果系统需要更新维护,你还需要支付另外一笔不菲的费用。云端ERP:优势...
- cad2014序列号和密钥永久(autocad2014序列号和密钥)
-
1在cad2014中修改标注样式后,需要将其保存2单击“样式管理器”按钮,在弹出的窗口中选择修改后的标注样式,然后单击“设置为当前”按钮,再单击“保存当前样式”按钮,将其保存为新的样式名称3为了...
- qq修改密保手机号(qq修改密保手机号是什么意思)
-
QQ更改绑定的手机号码操作步骤如下:1、打开手机主界面,找到“QQ”软件点击打开。2、输入正确的QQ账户和密码登录到qq主界面。3、点击左上角的头像“图片”,进入到个人中心界面。4、进入到个人中心界面...
欢迎 你 发表评论:
- 一周热门
-
-
抖音上好看的小姐姐,Python给你都下载了
-
全网最简单易懂!495页Python漫画教程,高清PDF版免费下载
-
Python 3.14 的 UUIDv6/v7/v8 上新,别再用 uuid4 () 啦!
-
python入门到脱坑 输入与输出—str()函数
-
飞牛NAS部署TVGate Docker项目,实现内网一键转发、代理、jx
-
宝塔面板如何添加免费waf防火墙?(宝塔面板开启https)
-
Python三目运算基础与进阶_python三目运算符判断三个变量
-
(新版)Python 分布式爬虫与 JS 逆向进阶实战吾爱分享
-
慕ke 前端工程师2024「完整」
-
失业程序员复习python笔记——条件与循环
-
- 最近发表
- 标签列表
-
- python计时 (73)
- python安装路径 (56)
- python类型转换 (93)
- python进度条 (67)
- python吧 (67)
- python的for循环 (65)
- python格式化字符串 (61)
- python静态方法 (57)
- python列表切片 (59)
- python面向对象编程 (60)
- python 代码加密 (65)
- python串口编程 (77)
- python封装 (57)
- python写入txt (66)
- python读取文件夹下所有文件 (59)
- python操作mysql数据库 (66)
- python获取列表的长度 (64)
- python接口 (63)
- python调用函数 (57)
- python多态 (60)
- python匿名函数 (59)
- python打印九九乘法表 (65)
- python赋值 (62)
- python异常 (69)
- python元祖 (57)
