• 欢迎访问web前端中文站,JavaScript,CSS3,HTML5,web前端demo
  • 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏web前端中文站吧

AI应用开发基础傻瓜书系列4-用线性回归来理解神经网络的训练过程

JavaScript web前端中文站 11个月前 (11-21) 1161次浏览 已收录 0个评论

全套教程请点击:微软 AI 开发教程

更多精彩内容请看 web 前端中文站
http://www.lisa33xiaoq.net 可按 Ctrl + D 进行收藏

第四篇:用线性回归来理解神经网络的训练过程

下面我们举一个简单的线性回归的例子来说明实际的反向传播和梯度下降的过程。完全看懂此文后,会对理解后续的文章有很大的帮助。

为什么要用线性回归举例呢?因为/(y = wx+b/) (其中,y,w,x,b 都是标量)这个函数的形式和神经网络中的/(Y = WX + B/)(其中,Y,W,X,B 等都是矩阵)非常近似,可以起到用简单的原理理解复杂的事情的作用。

创造训练数据

让我们先自力更生创造一些模拟数据:

import numpy as np import matplotlib.pyplot as plt # create mock up data # count of samples m = 100 X = np.random.random(m) # create some offset as noise to simulate real data noise = np.random.normal(0,0.1,x.shape) W = 2 B = 3 Y = W * X + B + noise plt.plot(X, Y, "b.") plt.show()

得到 100 个数据点如下:

AI 应用开发基础傻瓜书系列 4-用线性回归来理解神经网络的训练过程

好了,模拟数据制作好了,目前 X 是一个 100 个元素的集合,里面有 0~100 之间的随机 x 点,Y 是一个 100 个元素的集合,里面有对应到每个 x 上的/(y=2x+3/)的值,然后再加一个或正或负的上下偏移作为噪音,来满足对实际数据的模拟效果(因为大部分真实世界的生产数据从来都不是精确的,精确只存在于数学领域)。

现在我们要忘记这些模拟数据(样本值)是如何制作出来的,也就是要忘记 W,B 的值。我们就假设这是实际应用中收集到的模拟数据,但是我们并不知道它的原始函数是什么参数,只知道是公式/(y = wx + b/),我们的任务就是要根据这些样本值,通过神经网络训练的方式,得到 w 和 b 的值。注意这里 x 和 y 是样本的输入和输出,不是目标变量,这一点和常见的初等数学题不一样,要及时转变概念。

训练方式的选择

接下来,我们会用两种方式来训练神经网络(神经元):

  1. 把所有样本逐个地输入网络训练
  2. 把所有样本整批的输入网络训练

Pseudo code 伪代码如下:

第一种方式:逐个样本训练

for 每个样本 x,y:     标量前向计算得到 z 值 = wx+b     计算损失(optional)     计算 w 的梯度(输入 Z,Y,X 的值)     计算 b 的梯度(输入 Z,Y,X 的值)     更新 w,b 的值

第一种方式的好处是每次计算都是标量计算,不涉及到矢量或者矩阵,便于大家理解。但是有个问题就是,如果最后几个样本的误差较大的话,会把前面已经训练得差不多的 w,b 的值变坏。

第二种方式:批量样本训练

while 停止条件不满足     矩阵前向计算得到 Z 值 = wX+b(其中 X 是所有样本的一个数组/集合)     计算损失(optional)     计算 w 的梯度     计算 b 的梯度     更新 w,b 的值

第二种方式我们用了矩阵和标量的运算,以及矩阵和矩阵的运算。由于是批量样本做为输入,所以某些个样本的误差不会对整体造成影响。

使用第一种方式训练

定义神经网络结构

对于简单的线性回归问题,我们使用单层网络单个神经元就足够了。而且由于是线性的,我们不需要定义激活函数,这就大大简化了程序,而且便于大家循序渐进地理解。

def forward_calculation(w,b,X):     z = w * x + b     return z

其中,由于 X 是一组数据(100 个),所以它是一个矢量,或者理解为一维数组。w 和 b 都是一个标量,Z 的计算结果也是一个矢量,尺寸和 X 一样。

上面的写法,实际上是每次迭代都用所有的样本做训练,因为输入是 X,是所有样本的集合。还有另外一种做法,就是每次训练,只用一个训练样本,那么就需要在主循环中进行调度,一次使用一个样本。

定义损失函数

我们用传统的均方差函数: /(loss = /frac{1}{2}(z-y)^2/),其中,z 是每一次迭代的预测输出,y 是样本标签数据。这个损失函数的直观理解如下图:

AI 应用开发基础傻瓜书系列 4-用线性回归来理解神经网络的训练过程

假设我们计算出初步的结果是红色虚线所示,这条直线是否合适呢?我们来计算一下图中每个点到这条直线的距离(黄色线),把这些距离的值都加起来(都是正数,不存在互相抵消的问题)成为 loss,然后想办法不断改变红色直线的角度和位置,让 loss 最小,就意味着整体偏差最小,那么最终的那条红色直线就是我们要的结果。

下面是 Python 的 code,用于计算损失:

# w:weight, y:sample data, m:count of sample def loss_calculation(z,y):     loss = (z-y)**2    # cannot use (Z-Y)^2     cost = loss/2     return cost

其实,这个 loss 值可以不用计算的,因为我们使用这个损失函数的目的是要反向传播,而不是真的用这个 loss 值去做什么具体的运算。具体的计算是体现在求导梯度的函数中。

搞明白为何用均方差 MSE 函数后,我们再看看 MSE 如何应用到反向传播中。

定义针对 w 的梯度函数

因为:

/[z = wx+b/]

/[loss = /frac{1}{2}(z-y)^2/]

所以我们用 loss 的值作为基准,去求 w 对它的影响,也就是 loss 对 w 的偏导数:

/[ /frac{/partial{loss}}{/partial{w}} = /frac{/partial{loss}}{/partial{z}}*/frac{/partial{z}}{/partial{w}} /]

其中:

/[ /frac{/partial{loss}}{/partial{z}} = /frac{/partial{}}{/partial{z}}[/frac{1}{2}(z-y)^2] = z-y /]

而:

/[ /frac{/partial{z}}{/partial{w}} = /frac{/partial{}}{/partial{w}}(wx+b) = x /]

所以:

/[ /frac{/partial{loss}}{/partial{w}} = /frac{/partial{loss}}{/partial{z}}*/frac{/partial{z}}{/partial{w}} = (z-y)x /]

写成 code:

# w:weight, X,Y:sample data, m:count of sample def dJw(z,y,x):     dw = (z-y)*x     return dw

定义针对 b 的梯度函数

因为:

/[Z = wX+b/]

/[loss = /frac{1}{2}(Z-Y)^2/]

所以我们用 loss 的值作为基准,去求 w 对它的影响,也就是 loss 对 w 的偏导数:

/[ /frac{/partial{loss}}{/partial{b}} = /frac{/partial{loss}}{/partial{Z}}*/frac{/partial{Z}}{/partial{b}} /]

其中:

/[ /frac{/partial{loss}}{/partial{Z}} = /frac{/partial{}}{/partial{Z}}[(z-y)^2] = z-y /]

而:

/[ /frac{/partial{z}}{/partial{b}} = /frac{/partial{(wx+b)}}{/partial{b}} = 1 /]

所以:

/[ /frac{/partial{loss}}{/partial{b}} = /frac{/partial{loss}}{/partial{Z}}*/frac{/partial{Z}}{/partial{b}} = z-y /]

# Z:predication value, Y:sample data, m:count of sample def dJb(z,y):     db = z - y     return db

每次迭代后更新 w,b 的值

def update_weights(w, b, dw, db, eta):     w = w - eta*dw     b = b - eta*db     return w,b

eta 在本程序中恒等于 0.1,这是随机梯度下降法。也可以在迭代到一定次数后,把 eta 的值逐步减小,变成 0.01,这样会形成开始时大步前进,到后面时小步快跑的局面,利于训练准确度提高。

初始化变量及参数

# initialize_data # step for each iteration eta = 0.1 # set w,b=0, you can set to others values to have a try w = 0 b = 0

程序主循环

j 是外循环的次数,先只训练一次看看效果。

for j in range(1):     for i in range(m):         # get x and y value for one sample         x = X[i]         y = Y[i]         # get z from x,y         z = forward_calculation(w, b, x)         # calculate lost (optional)         #loss = loss_calculation(z, y)         # calculate gradient of w and b         dw = dJw(z, y, x)         db = dJb(z, y)         # update w,b         w, b = update_weights(w, b, dw, db, eta)         print(w,b)

程序运行结果如下:

0.11278289694938642 0.34606738038593576 0.4048467024747609 0.7556962791118582 0.46002740765953076 1.0072739217390403 0.6629665909817659 1.3133134892057239 ...... 1.8800223947881818 3.054203494854314 1.8798053702497237 3.0511601599536635 1.8722710744969668 3.0374415142130298 1.8761825822484357 3.0437127218759885

目标是 w=2,b=3,看上去误差还比较大。我们设置外循环次数为 3,再看看效果。

#for j in range(1): for j in range(3):
...... 1.950551908871318 3.01593409919309 1.9512023724525054 3.0181474143771783 1.9512839743332555 3.0189251294358055 1.950145626519112 3.0148986986722246 1.9387958775488612 3.0023925945934153

貌似距离理想值更进了一步。但其实这两次的结果不可比,因为我们每次都用新的随机数做为样本,而不是同一批随机数。所以大家可以自己试着把随机数保存到文件里,每次训练时读出来,这样就可以比较效果了。

使用第二种方式训练

# use all the samples as a batch to train, then iteration on batch import numpy as np import matplotlib.pyplot as plt # create mock up data # count of samples m = 100 X = np.random.random(m) # create some offset as noise to simulate real data noise = np.random.normal(0,0.1,X.shape) W = 2 B = 3 Y = X * W + B + noise plt.plot(X, Y, "b.") plt.show()   # 由于 X 是一组数据(100 个),所以它是一个矢量,或者理解为一维数组。w 和 b 都是一个标量,Z 的计算结果也是一个矢量,尺寸和 X 一样。 def forward_calculation(w,b,X):     Z = w * X + b     return Z  # 由于是 m 个训练样本批量训练,所以结果要除以 m,下同 # 注意 X,Y,Z 都是数组 def loss_calculation(Z,Y,m):     loss = (Z-Y)**2    # cannot use (Z-Y)^2     cost = loss.sum()/m/2     return cost  def dJw(Z,Y,X,m):     q = (Z-Y)*X     dw = sum(q)/m     return dw  def dJb(Z,Y,m):     q = Z - Y     db = sum(q)/m     return db  # w,b 是标量,所以代码和第一种方式相同  def update_weights(w, b, dw, db, eta):     w = w - eta*dw     b = b - eta*db     return w,b  # initialize_data # step for each iteration eta = 0.1 # set w,b=0, you can set to others values to have a try w = 0 b = 0 # condition 1 to stop iteration: when Q - prevQ < error error = 1e-10 prevQ = 10 # condition 2 to stop iteration max_iteration = 10000 # counter of iteration iteration = 0  # condition 2 to stop while iteration < max_iteration:     # using current w,b to calculate Z     Z = forward_calculation(w,b,X)     # compare Z and Y     Q = loss_calculation(Z, Y, m)     # get gradient value     dW = dJw(Z, Y, X, m)     dB = dJb(Z, Y, m)     # update w and b     w, b = update_weights(w, b, dW, dB, eta)     print(iteration,w,b)     iteration += 1     # condition 1 to stop  #    if abs(Q - prevQ) < error: #        break     prevQ = Q  print(Q,prevQ) print(w,b)

损失函数的微小变化

我们用传统的均方差函数: /(loss = /frac{1}{2}(Z-Y)^2/),其中,Z 是每一次迭代的预测输出,Y 是样本标签数据。我们使用所有样本参与训练,因此损失函数实际为:

/[loss = /frac{1}{2m}/sum_{i=1}^{m}(Z_i – Y_i) ^ 2/]

其中的分母中有个 2,实际上是想在求导数时把这个 2 约掉,没有什么原则上的区别。

由于 loss 是所有样本的集合,我们先对其中的所有值求总和,样本数量是 m,然后除以 m 来求一个平均值。

其实,这个 loss 值可以不用计算的,因为我们使用这个损失函数的目的是要反向传播,而不是真的用这个 loss 值去做什么具体的运算。具体的计算是体现在求导梯度的函数中。

定义针对 w 的梯度函数

因为:

/[Z = wX+b/]

/[loss = /frac{1}{2m}(Z-Y)^2/]

所以我们用 loss 的值作为基准,去求 w 对它的影响,也就是 loss 对 w 的偏导数:

/[ /frac{/partial{loss}}{/partial{w}} = /frac{/partial{loss}}{/partial{Z}}*/frac{/partial{Z}}{/partial{w}} /]

其中:

/[ /frac{/partial{loss}}{/partial{Z}} = /frac{/partial{}}{/partial{Z}}[/frac{1}{2m}(Z-Y)^2] = /frac{1}{m}(Z-Y) /]

而:

/[ /frac{/partial{z}}{/partial{w}} = /frac{/partial{}}{/partial{w}}(wX+b) = X /]

所以:

/[ /frac{/partial{loss}}{/partial{w}} = /frac{/partial{loss}}{/partial{Z}}*/frac{/partial{Z}}{/partial{w}} = /frac{1}{m}(Z-Y)X /]

写成 code:

# w:weight, X,Y:sample data, m:count of sample def dJw(Z,Y,X,m):     q = (Z-Y)*X     # because w is a scalar, so dw should be a scalar too     dw = sum(q)/m     return dw

定义针对 b 的梯度函数

因为:

/[Z = wX+b/]

/[loss = /frac{1}{2m}(Z-Y)^2/]

所以我们用 loss 的值作为基准,去求 w 对它的影响,也就是 loss 对 w 的偏导数:

/[ /frac{/partial{loss}}{/partial{b}} = /frac{/partial{loss}}{/partial{Z}}*/frac{/partial{Z}}{/partial{b}} /]

其中:

/[ /frac{/partial{loss}}{/partial{Z}} = /frac{/partial{}}{/partial{Z}}[/frac{1}{2m}(Z-Y)^2] = /frac{1}{m}(Z-Y) /]

而:

/[ /frac{/partial{Z}}{/partial{b}} = /frac{/partial{(wX+b)}}{/partial{b}} = 1 /]

所以:

/[ /frac{/partial{loss}}{/partial{b}} = /frac{/partial{loss}}{/partial{Z}}*/frac{/partial{Z}}{/partial{b}} = /frac{1}{m}(Z-Y) /]

# Z:predication value, Y:sample data, m:count of sample def dJb(Z,Y,m):     q = Z - Y     db = sum(q)/m     return db

程序运行结果如下:

0 0.204633398307696 0.3943112518285292 1 0.3842082815875446 0.7395242688455653 2 0.5418315944143854 1.0417326666889068 3 0.6802247732855068 1.3062739239107688 ...... 935 2.014844080911897 2.9912924671594148 936 2.014846970318595 2.9912909940330077 937 2.014849838514886 2.991289531720451 938 2.014852685656469 2.991288080142363 939 2.0148555118979017 2.9912866392199446 940 2.014858317392607 2.9912852088749755 941 2.0148611022928815 2.9912837890298087 0.004078652569361402 0.004078652668164296 2.0148611022928815 2.9912837890298087

训练过程迭代了 941 次,loss 的前后差值小于 1e-10 了,达到了停止条件。可以看到最后 w = 2.0148, b = 2.9912, 非常接近 W=2, B=3 的真实值。

也可以注释掉 condition 1,让迭代达到 10000 次,但其实结果并不会好到哪里去。

孔子说:点赞是人类的美德!如果觉得有用,关闭网页前,麻烦您给点个赞!然后准备学习下一周的内容。

本系列博客链接:

【注:本文源自网络文章资源,由站长整理发布】

  • 神经网络的基本工作原理
  • 神经网络中反向传播与梯度下降的基本概念
  • 损失函数
  • 激活函数
  • 用线性回归来理解神经网络训练过程
  • 徒手搭建神经网络
  • 徒手搭建 CNN 网络
  • 徒手搭建 RNN 网络
  • 模型内部
  • 附录:基本数学导数公式

web 前端中文站 , 版权所有丨如未注明 , 均为原创丨本网站采用BY-NC-SA协议进行授权
转载请注明原文链接:AI 应用开发基础傻瓜书系列 4-用线性回归来理解神经网络的训练过程
喜欢 (0)
发表我的评论
取消评论
表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址