python中forward的作用_基于numpy的前馈神经网络(feedforward neural network)

***

这几天在上Andrew Ng教授开的Coursera系列课程Deep Learning,总觉得光是看视频和做作业还不够,还是得自己动手写写代码,亲自实现课程里提到的算法内容,于是便有了这篇博客,作为自己入门深度学习的里程碑吧。

前馈神经网络

机器学习有两个基本问题,一是回归,二是分类,神经网络大多用于解决分类问题,前馈神经网络(feedforward neural network)是整个神经网络家族中较为常见和较为基础的一种,如下图右上角的DFF所示。图片来源是Cheat Sheets for AI, Neural Networks, Machine Learning, Deep Learning & Big Data。

神经网络中的基本元素是神经元,每层都有一定数量的神经元,神经元组合的多样性决定了神经网络的丰富性。下面是一个简单的前馈神经网络,总共有三层,从左到右分别是输入层、隐层和输出层,输入层的x1和x2表示这个样本只有两个特征(自变量),因为输入层通常不计入内,所以这是一个两层的神经网络,第一层有4个神经元,第二层只有1个。注意,隐层可以不止一层,隐层设置得越多,整个神经网络越庞大。

这个神经网络的工作原理是,给定一个样本的数据,将数据传输到第一层,进行线性变换和激活变换,得到加工过后的数据,这份新数据传到第二层,作为第二层的输入,接着进行线性变换和激活变换,又得到一份新的数据,因为第二层是最后一层了(如果不止两层就一直进行这样的操作直到抵达最后一层为止),所以最终的输出作为我们对该样本的预测值y_hat。

每一个神经元如同工厂的流水车间的机器,它重复做着四件事情:【接受上一层数据作为输入>>线性变换>>激活变换>>输出数据到下一层】,每个神经元中有三个组成部分:权重(weight)矩阵W,偏置(bias)向量b,以及激活函数(activation function) g,用公式表达为下图,其中上标(i)表示这是第i个样本数据,上标[1]和[2]分别表示这是神经网络的第一层与第二层:

公式(1)中,通过简单的线性变换得到了z[1],z称为prev_activation,接着通过激活变换(这里用的激活函数是tanh函数,下面会讲到),得到了a[1],称为activation,公式(3)(4)表达的是第二层的线性变换和激活变换,和第一层大同小异,只不过第二层的激活函数用的不是tanh函数而是sigmoid函数。

激活函数有下面三种,都是执行了非线性变换,实现的效果都是将prev_activation转化为activation。

每一个神经元都可以从上面三种激活函数中选取一个作为自己的激活函数g,经验表明,使用tanh函数的效果总是碾压使用sigmoid函数,所以人们大多使用tanh作为激活函数,近年来人们发现了relu函数,发现它的性能比tanh更好,relu成为了广受欢迎的激活函数。既然sigmoid性能最差,为什么还要介绍它?在一开始的时候说到,神经网络通常用于分类,比方说,给定一张图片,去识别预测它是不是一只猫:

我们的返回值应该是范围在0~1之间的概率值,sigmoid的函数范围是(0, 1), tanh的范围是(-1, 1), relu的是[0, +∞),使用sigmoid显然更合适些。所以通常一个神经网络的配置是,中间的隐层的所有神经元使用tanh或者relu作为激活函数,输出层的神经元使用sigmoid。

之所以要在线性变换之后进行非线性变化,是因为,如果没有非线性变换,纯粹使用线性变化的话,不管使用了多少层的线性变换,最终的结果通过合并同类项之后仍然是线性变换,100层的神经网络和1层的没有任何差别。神经网络从本质上来说就是一系列的非线性变换。

讲完了基本概念,我们现在的问题是,如何训练这个神经网络。

和其他的监督学习方法一样,神经网络同样是需要定义一个损失函数(cost function),通过对它进行最小化,得到最优的参数W和b,即每个神经元的权重矩阵和偏置向量。神经网络用的损失函数是交叉熵:

其中m为样本总数,y_hat为最终预测值,是通过每一层的神经元进行层层加工处理得到的,L表示整个神经网络的层数,即[L]表示最后一层:

优化损失函数的方法是梯度下降法,梯度下降的原理这里就不说了,就贴一下参数的更新公式:

以前在看书的时候,看到神经网络部分,总是在说“神经网络训练使用的是反向传播(back propagation)算法”,一直搞不明白这四个字究竟是什么意思,现在总算是有些理解了。

既然有反向传播,就有正向传播,所谓正向,就是沿着神经网络从输入层到输出层,就是沿着每个神经元执行【接受上一层数据作为输入>>线性变换>>激活变换>>输出数据到下一层】的方向,就是下图中从左到右的方向,而反向传播,指的是从输出层到输入层,即从右到左。

所谓的传播,其实就是把整个神经网络运算的过程抽象成了流程图的形式。正向传播时,数据通过第一层神经元,被处理成了新的数据,传输到下一层,再加工成新数据,再传到下一层,这便是数据的传播,而在反向传播时,传播的是梯度,从最后一层(输出层)出发,首先根据正向传播得到的预测值,计算得出最后一个神经元处的A(activation)的梯度,进而得到Z(prev_activation)的梯度,根据公式再算出W, b以及倒数第二层的A的梯度,就像多米诺骨牌,不断进行下去,直到将梯度传播到第一层的神经网络处。

在正向传播时,每个神经元执行了两步操作,线性变化和激活变换,线性变换是由W[l], b[l], A[l-1]得到了z[l],激活变化是由z[l]得到了A[l];而在反向传播时,在每个神经元中,根据相应的激活函数由dA[l]得到dz[l],再由dz[l]得到dW[l], db[l], dA[l-1],这可以看做是线性变化和激活变换的反向操作。

由此得到了每一层的神经元的W和b的梯度,参数W和b也由此得以更新,反向传播时传播的是梯度,也可以理解为传播的是误差,是当前模型的不足,参数W和b借助这个信息来进行修正,从而优化模型。于是整个神经网络就按照下图的流程进行迭代,直到达到理想的模型效果:

所以整个神经网络的操作流程是这样的:

确定神经网络的层数和每层的神经元数

初始化参数W和b

进行循环迭代

前向传播,得到预测值

计算损失函数

反向传播,得到参数W和b的梯度

对参数W和b进行更新

对新数据做预测

代码实现

课程里有给出前馈神经网络的python代码,但是我觉得写得过于繁琐(简单的几步操作居然写了300多行?),太注重形式,太刻意地去遵循代码规范,反倒增加了阅读负担,不太合我心意。

于是我按照自己对算法的理解写了一份代码,进行了精简,只保留最重要的部分,并且加入了L2正则化的部分。为了测试代码的正确性,这里使用的神经网络的层数和神经元数和Coursera上的是一样的,共有四层,每层的神经元数量分别是20/ 7/ 5/ 1,用的数据也是课程的数据。

下面进入代码部分。

首先要调用numpy。为什么要使用numpy?因为不想使用一层又一层的for循环,使用向量化的计算能够大幅度地降低运算时间。

准备好激活函数,隐层统一使用relu,输出层使用sigmoid:

def sigmoid(z):

'''

z为prev_activation, size为 nl * m

'''

return 1 / (1 + np.exp(-z))

def relu(z):

'''

z为prev_activation, size为 nl * m

'''

return np.maximum(0, z)

设置好神经网络的结构:

n0, m = X.shape

n1 = 20

n2 = 7

n3 = 5

n4 = 1

layers_dims = [n0, n1, n2, n3, n4] # [12288, 20, 7, 5, 1]

L = len(layers_dims) - 1 # 4层神经网络,不计输入层

构建神经网络模型:

##### neural network model

def neural_network(X, Y, learning_rate=0.01, num_iterations=2000, lambd=0):

m = X.shape[1]

### initialize forward propagation

param_w = [i for i in range(L+1)]

param_b = [i for i in range(L+1)]

np.random.seed(10)

for l in range(1, L+1):

if l < L:

param_w[l] = np.random.randn(layers_dims[l], layers_dims[l - 1]) * np.sqrt(2 / layers_dims[l - 1])

if l == L:

param_w[l] = np.random.randn(layers_dims[l], layers_dims[l - 1]) * 0.01

param_b[l] = np.zeros((layers_dims[l], 1))

activations = [X, ] + [i for i in range(L)]

prev_activations = [i for i in range(L+1)]

dA = [i for i in range(L+1)]

dz = [i for i in range(L+1)]

dw = [i for i in range(L+1)]

db = [i for i in range(L+1)]

for i in range(num_iterations):

### forward propagation

for l in range(1, L+1):

prev_activations[l] = np.dot(param_w[l], activations[l-1]) + param_b[l]

if l < L:

activations[l] = relu(prev_activations[l])

else:

activations[l] = sigmoid(prev_activations[l])

cross_entropy_cost = -1/m * (np.dot(np.log(activations[L]), Y.T) \

+ np.dot(np.log(1-activations[L]), 1-Y.T))

regularization_cost = 0

for l in range(1, L+1):

regularization_cost += np.sum(np.square(param_w[l])) * lambd/(2*m)

cost = cross_entropy_cost + regularization_cost

### initialize backward propagation

dA[L] = np.divide(1-Y, 1-activations[L]) - np.divide(Y, activations[L])

assert dA[L].shape == (1, m)

### backward propagation

for l in reversed(range(1, L+1)):

if l == L:

dz[l] = dA[l] * activations[l] * (1-activations[l])

else:

dz[l] = dA[l].copy()

dz[l][prev_activations[l] <= 0] = 0

dw[l] = 1/m * np.dot(dz[l], activations[l-1].T) + param_w[l] * lambd/m

db[l] = 1/m * np.sum(dz[l], axis=1, keepdims=True)

dA[l-1] = np.dot(param_w[l].T, dz[l])

assert dz[l].shape == prev_activations[l].shape

assert dw[l].shape == param_w[l].shape

assert db[l].shape == param_b[l].shape

assert dA[l-1].shape == activations[l-1].shape

param_w[l] = param_w[l] - learning_rate * dw[l]

param_b[l] = param_b[l] - learning_rate * db[l]

if i % 100 == 0:

print("cost after iteration {}: {}".format(i, cost))

Andrew Ng教授是用一个dict来保存每层神经元的参数的,比如说,在调用第三层的参数W3和b3时,他的写法是:parameters['W' + str(3)]和parameters['b' + str(3)],这样写没有错误,虽然直观,但是很麻烦,我的做法是,分别使用list来保存W和b,根据位置读取,对应上面的就是param_w[3]和param_b[3]。

注意到,python(以及其他编程语言)是从0开始计数的,而不是从1开始,这意味着,当神经网络共有4层的时候,我的param_w和param_b的长度是5,我对activation、prev_activation以及各个梯度dA/dz/dw/db都是用长度为5的list来保存的,而Ng教授记录这些变量的长度都是4。

我认为我的写法是更容易理解的写法,因为整个神经网络包括输入层在内一共有5层,但是因为习惯上我们不计输入层,所以这是个4层网络,如果我们把输入层称为第0层,输出层称为第4层,没有什么问题,并且恰好符合了编程语言从0开始计数的习惯。所以在我的写法下,activations[3]就表示第三层的输出,param_b[2]表示第二层的偏置向量,不会有什么误解,很直观。

而对于Ng教授的做法,获取每个参数时是基于字符串的,比如说grads["db" + str(4)]表示第四层的b的梯度,而在用for循环遍历每一层神经元的时候,又是基于位置的,这么一来,你就会在+1 和 -1之后迷失自我,即便你确保了自己没有出错,你也已经花费了不少精力来判断这个地方究竟是该+1还是-1还是保持原样。

下面是我在做Ng教授布置的编程作业时,需要填写的反向传播的部分,要求我在里面填入5行代码来完成,这导致了我在 l 和 l+1和 l-1三者之间纠结犹豫了很久才终于填写正确。我个人认为不是一段user-friendly的代码。

for l in reversed(range(L - 1)):

# lth layer: (RELU -> LINEAR) gradients.

# Inputs: "grads["dA" + str(l + 1)], current_cache".

# Outputs: "grads["dA" + str(l)] , grads["dW" + str(l + 1)] , grads["db" + str(l + 1)]

### START CODE HERE ### (approx. 5 lines)

current_cache = caches[l]

dA_prev_temp, dW_temp, db_temp = linear_activation_backward(grads['dA' + str(l + 1)], current_cache, activation="relu")

grads["dA" + str(l)] = dA_prev_temp

grads["dW" + str(l + 1)] = dW_temp

grads["db" + str(l + 1)] = db_temp

### END CODE HERE ###

Ng教授的代码中,对于不同的子函数,层数L时而等于5,时而等于4,很是混乱,而我的层数L始终等于4,符合python从0开始计数的习惯,使得整个算法写起来很快,也很好理解,一看就明白当前迭代到了哪一层。

另外,在写算法的时候,要重点关注每个参数的size,注意看它是几乘几的矩阵,很多时候出bug都来自于参数的size弄错了,下面是各参数的size表,里面的12288和209表示我们使用的训练集的数据有209个样本(图片),每个样本有12288个特征(每张图片是64像素*64像素的,所以有64*64*3=12288)。

关于size的规律是,梯度和参数的size相同,同一层的W和dW的size相同,b和db的size相同,其实也很好理解,看参数更新的公式就知道了;另外,同一层的A/z/dA/dz的size都是相同的,这个要记住。可以这么理解,原数据的size是p×m,即m个p维的样本(注意在神经网络中,每一列代表一个样本,和其他机器学习方法中每一行代表一个样本是反过来的),经过每一层的神经元处理之后,得到了新的数据,可以理解为是降维的过程,由一开始的12288×209的数据,通过第一层后变为了20×209的数据,即把高维的12288个特征浓缩为20个新特征,再浓缩为7个特征,最后得到了1×209的预测值。

最后是预测函数,将训练好的神经网络用于一套新的数据上:

def predict(X_new, parameters, threshold=0.5):

param_w = parameters["param_w"]

param_b = parameters["param_b"]

activations = [X_new, ] + [i for i in range(L)]

prev_activations = [i for i in range(L + 1)]

m = X_new.shape[1]

for l in range(1, L + 1):

prev_activations[l] = np.dot(param_w[l], activations[l - 1]) + param_b[l]

if l < L:

activations[l] = relu(prev_activations[l])

else:

activations[l] = sigmoid(prev_activations[l])

prediction = (activations[L] > threshold).astype("int")

return prediction

经验与收获

这是我第一次用python写一个具体的算法,虽然一直在python做数据分析,但是一直没有写过一个逻辑完整的、能应用于实际场合的算法(自己动手写完整算法的经历,在今天之前,是用R语言写了逐步回归和随机森林),今天算是填补了我在python上的这块空白。

使用assert语句来确保每个参数的shape正确,能够减少出现bug的几率

numpy中有一些操作和函数是element-wise的,要留意

整个算法没一会儿就写完了,但是训练的时候cost一直没有收敛,检查了老半天,才发现是由两个问题导致的:

relu的导函数写错了,返回值是0与1没错,但是我用来判断的参数竟然是A而不是z,真是写得莫名其妙;

初始化不正确(我后来才发现初始化是神经网络的关键),对于权重W我一直使用的写法是param_w[l] = np.random.randn(layers_dims[l], layers_dims[l - 1]) * 0.01,导致cost没能实现收敛,应该是由vanishing gradients导致的,后来我使用了He初始化和Xavier初始化,即把上面的0.01改成np.sqrt(2 / layers_dims[l - 1])或者np.sqrt(1 / layers_dims[l - 1]),效果明显。

怎么知道自己的最终算法写对了?我和课程使用的是同一套数据,如果使用我的算法,在相同的神经网络结构下,和Ng教授使用的算法得到的精确度差不多,就说明没问题了。

比起写代码,理解代码背后的算法内容更重要。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/558170.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Charles抓取HTTPS Windows Android iOS 图文详细

文章目录背景操作原理windows 安装CharlesCharles配置第一步&#xff1a;配置HTTP代理&#xff0c;这步与抓取HTTP请求是一样第二步&#xff1a;配置SSL代理第三步 &#xff1a;为手机配置代理iPhone 代理配置Android 代理配置第四步&#xff1a;安装根证书iPhone 安装charles证…

css线条伸缩_CSS3弹性伸缩布局之box布局

CSS3弹性伸缩布局简介2009年&#xff0c;W3C提出了一种崭新的方案----Flex布局(即弹性伸缩布局)&#xff0c;它可以简便、完整、响应式地实现各种页面布局&#xff0c;包括一直让人很头疼的垂直水平居中也变得很简单地就迎刃而解了。但是这个布局方式还处于W3C的草案阶段&#…

Android代码安装apk程序

文章目录一、高低版本兼容代码及配置1.java代码2.AndroidManifest.xml 配置ContentProvider3.paths xml 配置二、老版本代码三、可能出的问题及处理一、高低版本兼容代码及配置 高版本提高了sdcard、 app文件空间的访问权限&#xff0c;高低版本的系统api有一定区别&#xff0…

Android adb 安装apk程序

文章目录ADB简单介绍安装命令安装失败情况adb 安装测试包adb 安装卸载的所有命令摘要前言多数情况下&#xff0c;用户都是从应用商店或某网页下载apk后从手机上直接安装apk文件&#xff0c;在调试或下载到电脑上时候&#xff0c;我们就可以直接通过adb安装到手机。在测试小伙伴…

XML十六进制无效的字符(0x0b 0x0c 0x02)

文章目录解决方法ASCII“字符”1、ASCII 打印字符2、ASCII 非打印控制字符3、扩展 ASCII 打印字符4、ASCII 之外ASCII 字符输入方法与常用地方1、在文档中插入 ASCII 字符2、输入方法前言xml 中包含了非打印字符的情况下&#xff0c;即使是放在cdata 中&#xff0c;在不同平台上…

tab栏圆点按钮_uniapp tabbar上的小圆点

需求在tabbar的图标右侧显示和隐藏小圆点。一般表示有新消息、新更新。基本代码序号从0开始计算。uni.showTabBarRedDot({index:1});uni.hideTabBarRedDot({index:1});修改代码App.vue或index页在App.vue的onLaunch周期或者是index页的onLoad加入代码&#xff0c;判定是否需要显…

Android OpenGLES2.0(一)OpenGLES2.0基础介绍

文章目录什么是OpenGL ES&#xff1f;OpenGL ES可以做什么&#xff1f;OpenGL ES版本及Android支持情况OpenGL ES 2.0的优点OpenGL ES 2.0中基本概念顶点着色器片元着色器着色器语言坐标系图形的绘制投影光照纹理映射其它OpenGL ES 2.0过程及理解什么是OpenGL ES&#xff1f; O…

如何 方法内指令重排 进制_谈谈指令重排

这个知识点也是很多人说不清道不明的地方&#xff0c;感觉都知道&#xff0c;说又说不出来。为什么会这样呢&#xff1f;因为这几个字&#xff0c;很容易被当成动词去理解&#xff0c;其实正确的理解是当成名词&#xff0c;即指令重排现象。那什么时候会产生指令重排现象呢&…

Windows ‘ls‘ 不是内部或外部命令,也不是可运行的程序 或批处理文件

文章目录前言原因解决办法前言 Windows和mac混用或Windows与linux混用的情况下&#xff0c;在windows dos窗口随手一个ls&#xff0c;出现了 ‘ls’ 不是内部或外部命令&#xff0c;也不是可运行的程序 或批处理文件。 原因 ls是linux的命令&#xff0c;不是windows的命令,win…

python问题解决了吗_Python 问题怎么解决?

展开全部主要问题和解决方法如下&#xff1a;中文路径的问题&#xff1a;在D盘下保32313133353236313431303231363533e58685e5aeb931333365633939存一个名字为‘中文.txt‘的文件。运行如下测试代码&#xff1a;# -*- coding: utf-8 -*-fopen(D:\\中文.txt, r)print f.read()&a…

Linux ls 命令使用介绍

文章目录命令格式命令功能常用参数常用示例示例1 列出文件夹下的所有文件和目录的详细资料示例2 列出当前目录中所有以“t”开头的目录的详细内容&#xff0c;示例3 只列出文件下的子目录命令&#xff1a;示例4 列出目前工作目录下所有名称是以s开头的档案&#xff0c;越新的排…

exls导入数据库 php_PHP读取excel文件并导入数据库

PHPExcel是一个PHP类库&#xff0c;用来帮助我们简单、高效实现从Excel读取Excel的数据和导出数据到Excel。下面是PHPExcel读取的使用教程&#xff1a;1.首先下载PHPExcel2.下载好文件&#xff0c;解压可以得到如下文件&#xff1a;为了使用方便&#xff0c;我们可以在根目录创…

Linux cd 命令使用介绍

文章目录1. 命令格式2. 命令功能3. 常用范例示例1&#xff1a;进入系统根目录cd /2&#xff1a;使用 cd 命令进入当前用户主目录3&#xff1a;跳转到指定目录4&#xff1a;返回进入此目录之前所在的目录5&#xff1a;把上个命令的参数作为cd参数Linux cd 命令是Linux中最基本的…

netty冲突 play sbt_《Netty官方文档》本地传输接口

原文链接 译者&#xff1a;gm777自4.016版本以来&#xff0c; Netty提供了本地的socket传输使Linux系统可以使用JNI(JAVA本地接口)。这个传输接口不仅有着高性能并且产生更少的垃圾&#xff0c;所以你也许会想尝试使用一下。使用本地传输接口由于这个本地传输接口是与NIO传输接…

Linux pwd 命令使用介绍

文章目录1&#xff0e;命令格式2&#xff0e;命令功能3&#xff0e;常用参数4&#xff0e;常用实例实例1&#xff1a;用 pwd 命令查看默认工作目录的完整路径实例2&#xff1a;使用 pwd 命令查看指定文件夹Linux中用 pwd 命令来查看”当前工作目录“的完整路径。每当我们在终端…

java参数传入泛型类型_Java泛型参数界定到任何一个类型的范围

是否有一种语法或解决方法来将泛型类型参数限制为任何一种类型的类型&#xff1f;我知道您可以将类型限制为所有类型的所有类型(即AND逻辑)&#xff1a;public class MyClass & Serializable> { } // legal syntax有OR逻辑版本,就是这样的&#xff1a;public class MyCl…

Linux mkdir 命令使用介绍

文章目录1&#xff0e;命令格式2&#xff0e;命令功能3&#xff0e;命令参数4&#xff0e;命令实例实例1&#xff1a;创建一个空目录实例2&#xff1a;递归创建多个目录或一次创建多级目录实例3&#xff1a;创建权限为777的目录实例4&#xff1a;创建新目录都显示信息实例5&…

jmeter修改redis_jmeter如何访问redis服务缓存

Redis(REmote DIctionary Server)是一个开源的内存数据结构存储&#xff0c;用作数据库&#xff0c;缓存和消息代理。作为内存数据库&#xff0c;它将所有数据保存在RAM中。Redis在读取/写入数据时实现高性能&#xff0c;并且在您需要确保在所有测试服务器上使用唯一数据时也很…

Linux rm/rmdir 命令使用介绍

文章目录1&#xff0e;命令格式2&#xff0e;命令功能3&#xff0e;命令参数4&#xff0e;命令实例1. 删除文件&#xff0c;系统会先询问是否删除。2. 强行删除文件&#xff0c;系统不再提示3. 删除任何.txt文件&#xff1b;删除前逐一询问确认4. 将目录及子目录中所有档案删除…

window服务器cpu过高的排查_线上服务器发生CPU占用率过高应该如何排查并定位问题?...

国外开发者平台 HankerRank 发布的 2018 年开发者技能调查报告中有一项关于"雇主最看重哪些核心能力"的调查&#xff0c;结果显示如下&#xff1a;排名前几的比较受重视的能力分别为&#xff1a;解决问题、编程语言熟练程度、Debug、系统设计和性能优化。解决问题的能…