反向梯度——最快下降

目录

1986年的那篇神作

多层感知机网络遇到的大问题

神经网络结构的设计

What:

How:

再议损失函数

ChatGpt:

什么是梯度

什么是梯度递减

​编辑

梯度递减的线性回归实战

什么是随机梯度递减

GhatGpt:

利用SGD解决线性回归实践


1986年,杰弗里·辛顿和他的小伙伴们重新设计了BP算法,以人工神经网络模仿大脑工作机理。

2006年,辛顿教授提出了“深度信念网”(Deep BeliefNets,DBN)(实际上这就是多层神经网络的马甲)。后来,这个“深度信念网”被称为深度学习的开山之作。而辛顿被称为“深度学习教父”。

1986年的那篇神作

借助反向传播算法的学习表征(Learning Representations by Back-propagating errors)

这篇神作首次阐述了BP算法在神经网络模型上的应用。BP算法把调整网络权值的运算量,从原来的与神经元数目的平方成正比,下降到只和神经元数目本身成正比。运算量大幅度下降,从而使BP算法更有可操作性。

多层感知机网络遇到的大问题

多层前馈网络,有时也被称为多层感知机(Multi-Layer Perception,MLP)。但这个提法导致“感知机”的概念混乱。因为在多层前馈网络中,神经元的内部构造已悄然发生改变,即激活函数从简单粗暴的阶跃函数(sgn),演变成了比较平滑的挤压函数(Sigmoid)

为什么激活函数要改变呢?如果感知机的激活函数还是阶跃函数,对函数求导非常不利,进而难以使用优化算法来求得损失函数的极小值。

我们知道,当分类对象是线性可分且学习率 η 足够小时,感知机还能胜任,由其构建的网络还可以训练达到收敛。但当分类对象是线性不可分时,感知机就有点力不从心了。因此,通常感知机并不能推广到一般的前馈网络中。

按照前面章节的说法,机器学习就是找到一个好用的函数,较好地实现某个特定功能。简单地说,函数就是功能。

而对于某个特定的前馈神经网络,给定网络参数(连接权值和阈值),其实就是定义了一个具备数据采集(输入层)、加工处理(隐含层)、然后结果输出(输出层)的函数。

如果仅仅给定一个网络结构,其实它定义的是一个函数集合。因为网络参数(连接权值和阈值)的不同,实现的功能大相径庭。功能不同,自然函数也是不同的!

针对前馈神经网络,我们需要达到的目的其实很简单,就是让损失函数达到最小值。因为只有这样,实际输出和预期输出的差值才最小。利用最小化损失函数,能更好地提升分类的精度。

在确定了通过最小化损失函数来调整网络参数这一目标后,现在的问题就变成如何从众多网络参数(神经元之间的连接权值和偏置)中选择最佳的参数

简单粗暴的方法:枚举所有可能的权值,优中选优——对于语音设别神经网络,假设网络结构有7层,每一层有1000个神经元,仅一层之间的全连接值,就达到 1000×1000=10^6 个,多层的权值数量更多。既不优雅,也不高效!

神经网络结构的设计

神经:什么是神经元?

网络:连接权重和偏置是怎么连接的?

What:

什么是神经元?这世上本没有什么人工神经元,数值的逻辑意义是人赋予的。在输入层,每个像素都是一个数值(如果是彩色图,则是表示红绿蓝的3通道组),而把包容这个数值的容器视作一个神经元。

如果图片的维度是16×16,那么输入层神经元就可以设计为256个(也就是说,输入层是一个包括256个灰度值的向量),每个神经元接收的输入值就是归一化处理之后的灰度值。0代表白色像素,1代表黑色像素,灰色像素的值介于0到1间。也就是说,输入向量的维度(像素个数)要和输入层神经元的个数相同。

而对于输出层而言,它的神经元个数和输入神经元的个数是没有对应关系的。而是和待分事物类别有一定的相关性。比如,我们的任务是识别手写数字,而数字有 0 ~ 9 共10类。那么如果在输出层采用 Softmax 回归函数,它的输出神经元数量仅为 10 个,分别对应数字 0 ~ 9 的分类概率。

最终的分类结果,择其大者而判之。比如,如果判定为 2 的概率(比如说 80% )远远大于其他数字,那么整个神经网络的最终判定,就是数字 2 ,而非其他数字。

相比神经网络输入层与输出层设计的明了直观,前馈神经网络的隐含层设计可就没有那么简单了。说好听一点,它是一门艺术,依赖于工匠的打磨。说不好听的,它就是一个体力活,需要不断地试错。

我们可以把隐含层暂定为一个黑箱,他负责输入和输出之间的非线性映射变化,具体功能有点说不清道不明(这是神经网路理论的短板所在)。隐含层的层数不固定。每层的神经元个数也不固定,他们都属于超参数,是人么根据实际情况不断调整选取的。

How:

神经元之间是如何连接的。我们把神经元与神经元之间的影响程度叫权重,权重的大小就是连接的强弱。它告诉下一层相邻神经元更应该关注哪些像素图案。

除了连接权重,神经元内部还有一个施加于自身的特殊权值,叫偏置(bias)。偏置表示神经元是否更容易被激活。也就是说,他决定神经元的连接加权和有多大,才能让激发变得有意义。

神经网络结构设计的目的在于,让神经网络以更佳的性能来学习。而这里所谓的学习,就是找到合适的权重和偏置,让损失函数的值最小

再议损失函数

怎样才算做提升神经网络性能呢?需要损失函数。所谓损失函数,就是一个刻画实际输出值期望输出值之间落差的函数。

为了达到这个理想状态,我们当然期望这种落差最小,也就是说,我们希望快速调节好网络参数,从而让这个损失函数达到极小值。这时,神经网络的性能也就接近最优!

关于求损失函数极小值,示例:

在监督学习下,对于一个特定样本,它的特征记为x(如果是多个特征,x表示输入特征向量)以及预期目标 t (这里 t 是 target 的缩写)。根据模型 f(x)的实际输出 o (这里 o 是 output 的缩写),两者之间的误差(error)程度可用公式来表达:

e = 1/2 (t - o)^2

这里的 e 称之为单样本误差。前面的系数 1/2 主要是为了在求导找梯度时,消除差值的平方项 2。

假设在训练数据集合 D 中,有 n 个样本,我们可以借助标记 E 来表示训练数据中所有样本的误差总和。并用其大小来度量模型的误差程度,如下所示:

(9-2)

在这里,td 是第 d 个训练样本的目标输出,od 是第 d 个训练样本的实际输出。

在这里,对于第 d 个实例的输出 od 可记为:

(9-3)

在这里,wT 为各个特征取得的权值向量,xd 表示第 d 个训练样本的特征向量,于是我们可以用(xd,td)这样的元组对,表示训练集合中的第 d 个样本。

ChatGpt:

这是机器学习中的预测操作,它对每个样本进行独立的预测。

权值向量的转置: 权值向量的转置是一个向量,其中每个元素对应一个特征。这个向量包含了用于加权特征的权重参数。例如,如果有3个特征,权值向量的转置可能是 [w1, w2, w3]。权值向量通常是列向量

第d个训练样本的特征向量: 每个训练样本都有一个特征向量,其中包含了该样本的特征值。特征的数量就是特征向量的长度。在机器学习领域,列向特征向量是更常见和常用的表示方式。

所以,权值向量的转置乘以第d个训练样本的特征向量用于表示单个样本的预测值,而不是整个训练集合中的所有样本。

对于特定的训练数据集而言,(xd,td)的值都是已知的,可视为常量。所以,在本质上公式(9-2)是有关特征权值 w 的函数,其更为清晰的表达如公式(9-4)所示:

(9-4)

于是,对于神经网络学习的任务,在很大程度上就是求取到一些列合适的 w 值,以拟合或者说适配给定的训练数据,从而使得预测 od 尽可能靠近期望输出 td ,使得 E(w)取得最小值。这在数学上叫做优化问题,而公式(9-4)就是我们优化的目标,称之为目标函数

与其抽象地说,如何训练一个神经网络模型,不如更具体地说,如何设计一个好用的函数(即损失函数),用以解释这些训练样本随自变量的变化关系。如果网络能够在较大程度上正确分类,那么损失就越小,也就说明拟合的效果就越好。最后,我们再用损失最小化的模型去预测新数据。

那么,如何得到这个损失最小化的函数呢?下面我们介绍让这个函数值最小的算法,大致分为三步循环走。

(1)损失是否足够小?如果不是,计算损失函数的梯度。

(2)按梯度的方向反向走一小步,以减小损失。

(3)循环到(1)。

这种按照负梯度的若干倍数(通常小于1倍,这个倍数也称之为学习率 η ),不停地调整函数权值的过程就叫做“梯度下降法”。通过这样的方法,改变每个神经元与其他神经元的连接权重及自身的偏置,让损失函数的值下降得更快,进而将值收敛到损失函数的某个极小值。(损失函数是目标函数的一部分)

现在,我们已经明确目标:探寻让损失函数达到最小值的参数。那么,如何高效地找到这些能让损失函数达到极小值的参数呢?这就是我们即将讨论的下一个话题

首先我们要搞清楚的第一个概念是——什么是梯度?

什么是梯度

我们知道,求某个函数的极值,难免要用到“导数”等概念。对于某个连续函数 y = f(x),令其导数 y = f ‘(x)= 0,通过求解该微分方程,便可直接获得极值点。

然而,显而易见的方案并不见得能显而易见获得。一方面, y = f ‘(x)= 0 的显式解并不容易求得,当输入变量很多或者函数很复杂时,就更不容易求解微分方程。另一方面,求解微分方程并不是计算机所长。计算机擅长的是凭借强大的计算能力,通过插值等方法(如牛顿下山法、弦截法等),海陵尝试,一步一步把函数的极值点“试”出来。

为了快速找到这些极值点,人们还涉及了一种名为 Δ法则(Delta Rule)的启发式方法,该方法能让目标收敛到最佳解的近似值。

delta 法则的核心思想在于,使用梯度下降(Gradient Descent)的方法找极小值。使用梯度下降策略,同样离不开导数的辅助。在单变量的实值函数中,梯度可简单理解为只是导数;或者说对于一个线性函数而言,梯度就是曲线在某点的斜率。但对于多维变量的函数,梯度概念就不那么容易理解了,它要涉及标量场概念。

标量场的连续性在数学上通常表示为连续函数,其中每个点的值与其邻近点的值之间没有间断。这使得标量场成为描述自然界中许多连续性分布的有用工具,包括温度、压力、浓度等。

在向量微积分中,标量场的梯度,其实是一个向量场。假设一个标量函数 f 的梯度记为 ▽ f 或 grad f,这里 ▽ 表示向量微分算子。那么,在一个三维直角坐标系中,该函数的梯度  ▽ f就可以表示为公式(9-6)所示的样子:

为求得这个梯度值,难免要用的偏导的概念。它的英文是 partial derivatives (局部导数)。

什么是偏导呢?对于多维变量函数而言,当求某个变量的导数时,就是把其他变量视为常量,然后对整个函数求其偏导(相比于全部变量,这里只求一个变量,即为“局部”)。之后,这个过程对每个变量都求一遍导数,放在向量场中,就得到了这个函数的梯度。举例来说,对于 3 变量函数 f = x^2 + 3xy + y^2 + z^3,它的梯度可以这样求得:

对于函数的某个特定点,它的梯度表示从该店出发,函数值增长最为迅猛的方向。对于上述案例,梯度可以理解为,站在向量点 A(1,2,3),如果想让函数值 f 的值增长得最快,那么它的下一个前进的方向,就是朝着向量点 B(8,7,27)方向进发。

什么是梯度递减

显然,梯度最明显的应用就是快速找到多维变量函数的极大值。而梯度的反方向(即梯度递减),自然就是函数值下降最快的方向。如果函数每次都沿着梯度递减的方向迁建,就能走到函数的最小值附近。

山坡越陡峭(相当于斜率越大),顺着这样的山坡爬山就能快速抵达峰顶(对于函数而言,就是愈加快速收敛到极值点)(对于计算机而言不存在重力和阻力)。把爬到山峰变成找谷底(即求极小值),与找斜率最大的爬坡方法没有本质变化,只是方向相反。若把登山过程中求某点斜率最大的方向称为“梯度”,找谷底的方法就是“梯度递减”。

将“梯度递减”作为指导,走一步,算一步,一直沿“最陡峭”的方向探索者前进。如果直接让损失函数L的导数等于0来求得最小值,是一种相对宏观的做法。那么“梯度递减”是站在微观的观点,根据当前的情况,动态调整 w 的大小,通过多次迭代,来求得最低点。

“梯度递减”体现出来的指导意义,就是机器学习中“学习”的内涵,即使在大名鼎鼎的AlphaGo中,也是这么“学习”的!你是不是有点失望?这机器学习也太不“高大上”了!

但是别忘了,“学习”的本质在于性能的提升。利用梯度递减的方法,可以在很大程度上提升机器的性能。所以,从这个意义上讲,它就是“学习”!

当然,我们也很容易看到“梯度递减”的问题所在,那就是它很容易收敛到局部最小值。正如攀登高峰,我们总会感叹道“一山还比一山高”,探寻谷底时,我们也可能发现,“一谷还比一谷低”。但“只缘身在此山中”,当前的眼界让我们向“蚂蚁寻路”一样,很难让人有全局观,因为我们都没有“上帝视角”。尽管有这样的障碍,在工程实践中,还是衍生出很多出色的应用案例。这就好比,牛顿力学的解释在量子领域也很有限,但并不妨碍它在宏观世界处处指导我们的生活。

损失函数 E(x)是关于 w 的函数,为了收敛到 E(x)的最小值,需要计算 E(x)对每一个 w 参数的偏导数,这就提到了梯度递减方法。

利用随机梯度下降法(Stochastic Gradient Descent ,SGD)求解极小值(谷底)的做法是,我们先随机站在山谷上的某一点,然后发现哪边比较低(或较陡)就往哪边走,越靠近谷底,每次移动的距离就越小,最终通过多次迭代收敛到最低点。

η 就是学习率,它决定了梯度递减搜索的步长,这个步长过犹不及。如果值太小,则收敛慢;如果值太大,则容易越过极值,导致网络震荡,难以收敛。所以,通常要根据不同的状况来调整 η 的大小。

实际上,神经网络中的权值参数是非常多的,因此针对损失函数 E 的权值向量 w 的梯度公式如图所示。

梯度递减的线性回归实战

求解极小值的方法除了最小二乘法,还有梯度递减。下面我们来结合梯度递减策略,给出一个基于 Python 的简易线性回归求解方案。

线性模型通常被称为线性回归模型,是因为它们最初是用于解决回归问题的。

简易线性回归模型用公式表示就是y=w1 × x + w0 × 1。这里的 w1 和 w0 为回归系数,需要从训练数据中学习得到。为什么要把系数 1 单独写出来呢?其实是为了以统一的方式求解系数,如果说模型的第一个参数是可变参数,那么第二个输入就是固定值 1 ,也称之为 “哑元”(dummy)。

下列程序让读者对梯度下降具有一个感性的认识。程序本身的功能非常简单,就是给出有关面包重量(磅)和售价(美元)对应关系的 5 条数据。然后利用梯度下降策略,让程序在这 5 条数据中学习线性回归模型的参数 w0 和 w1。其中 w0 和 w1的起始值是任意给定的,然后反复迭代多次,逐步逼近最佳的 w0 和 w1,让损失函数达到最小值。在得到“最佳”的参数之后,我们再给出一个面包重量,让模型预测它的售价。这样就完成了一个完整的有监督学习的流程。

bread_price = [[0.5, 5], [0.6, 5.5], [0.8, 6], [1.1, 6.8], [1.4, 7]]

这里定义了一个名为 bread_price 的列表,其中包含了面包的价格和面包的重量。每个子列表包含两个值,第一个值是面包的重量(自变量),第二个值是面包的价格(因变量)。

def BGD_step_gradient(w0_current, w1_current, points, learningRate):
    w0_gradient = 0
    w1_gradient = 0
    for i in range(len(points)):
        x = points[i][0]
        y = points[i][1]
        w0_gradient += -1.0 * (y - ((w1_current * x) + w0_current))
        w1_gradient += -1.0 * x * (y - ((w1_current * x) + w0_current))
        new_w0 = w0_current - (learningRate * w0_gradient)
        new_w1 = w1_current - (learningRate * w1_gradient)
    return [new_w0, new_w1]

这是一个用于执行梯度下降法的函数 BGD_step_gradient。它接受当前权重 w0_currentw1_current,以及数据点集合 points 和学习率 learningRate 作为参数。

  • w0_gradientw1_gradient 初始化为零,它们将用于计算权重的梯度。
  • 然后,使用一个循环遍历数据点集合 points 中的每个数据点。
  • 在循环中,获取当前数据点的输入特征 x 和输出值 y
  • 使用这些数据点计算损失函数关于权重 w0w1 的梯度。
  • 最后,根据梯度和学习率来更新权重 w0w1,然后返回更新后的权重。
def gradient_descent_runner(points, start_w0, start_w1, rate_1, num_iterations):
    w0 = start_w0
    w1 = start_w1
    for i in range(num_iterations):
        w0, w1 = BGD_step_gradient(w0, w1, points, rate_1)
    return [w0, w1]

这是梯度下降的主要函数 gradient_descent_runner,它执行多次梯度下降的迭代。

  • 它接受数据点集合 points、初始权重 start_w0start_w1、学习率 rate_1,以及迭代次数 num_iterations 作为参数。
  • 在函数内部,它初始化权重 w0w1 为初始值。
  • 然后,使用一个循环执行指定次数的迭代,每次迭代都调用 BGD_step_gradient 函数来更新权重 w0w1
  • 最终,返回学习得到的权重。
def predict(w0, w1, wheat):
    price = w1 * wheat + w0
    return price

这是一个用于预测面包价格的函数 predict。它接受学习得到的权重 w0w1,以及新的面包重量 wheat 作为参数。它使用学习得到的权重来计算相应的面包价格,然后返回价格。

if __name__ == '__main__':
    learning_rate = 0.01
    num_iter = 100
    w0, w1 = gradient_descent_runner(bread_price, 5, 2.5, learning_rate, num_iter)
    price = predict(w0, w1, 0.9)
    print("price = ", price)

这部分是主程序。它首先设置学习率 learning_rate 和迭代次数 num_iter。然后,它调用 gradient_descent_runner 函数来训练模型,学习得到权重 w0w1。最后,它使用学习得到的权重来预测新的面包价格,并将结果打印出来。

结果:

第 08 行和第 09 行就是公式(9-12)的完美应用。第 10 行和第 11 行是公式(9-13)的代码版本。由于一次迭代,很难找到最佳的参数 w0 和 w1,需要迭代多次才能达到此目的。因此,我们专门设计了一个函数 gradient_descent_runner()来实施这个多次迭代操作。

程序中的迭代次数和学习率,其实也算做神经网络中的参数(称它们为超参数),但他们不是学习得来的,而是来自人为经验,因此模型性能的好坏,有一定的运气成分。

什么是随机梯度递减

前面我们讨论了标准的梯度递减训练模型,在工程实践中,标准梯度下降法主要存在两个问题:(1)当数据量太大时,收敛过程可能非常慢。(2)如果误差曲面存在多个局部最小值,那么标准梯度模型可能找不到全局最小值点

下面我们来解释第(1)个问题。如果根据公式(9-13)所示的模型来训练权值参数,每次更新迭代,都要遍历训练样本集合 D 中的所有成员,然后求误差和、分别求各个权值的梯度,迭代一次都会“大动干戈”。因此这种算法也叫做批量梯度下降法(Batch Gradient Descent,BGD)。可以想象,如果样本的数量非常庞大,如数百万到数亿,那么计算负载会异常巨大。

为了缓解这一问题,人们通常采用 BGD 的近似算法——随机梯度下降法(Stochastic Gradient Descent,SGD)

GhatGpt:

BGD 指的是将多个数据点的梯度合并或累加到一个单一的梯度值中。

当处理多个数据点时,每个数据点都会对梯度有一个贡献梯度累加意味着将每个数据点的梯度逐个相加从而得到所有数据点的总梯度。这通常用于批量梯度下降(Batch Gradient Descent)中,其中在每个迭代步骤中,使用整个数据集来计算梯度。

在梯度下降中,确实存在学习率的问题,如果学习率设置得太大,可能导致参数在更新时跳过最低点,而如果学习率设置得太小,收敛速度会变得非常慢。优化算法通常会采用一些改进技巧,如学习率衰减(learning rate decay)和自适应学习率(如Adam、RMSprop等),以在训练过程中动态地调整学习率,以平衡收敛速度和稳定性。

SGD每次迭代中选择一个随机的训练样本计算该样本对应的损失函数的梯度,然后使用这个梯度来更新模型参数。

SGD 的随机性使其在训练过程中引入了噪声,这有助于模型逃离局部最小值,并更广泛地探索参数空间。然而,由于随机性,SGD可能需要更多的迭代次数才能达到收敛,因为每次迭代都仅考虑一个样本的梯度。

在 SGD 中,遵循“一样本,一迭代”的策略。先随机挑选一个样本,然后根据单个样本的误差来调节权值,通过一系列的单样本权值调整,力图达到与BGD采取“全样本,一迭代”类似的权值效果。SGD的权值更新公式变为:

(9-14)

其中,t、o 和 xi 分别为目标值、实际输出值和第 i 个训练样本的输入。从表面上看来,公式(9-14)和公式(9-13)非常类似,但请注意,公式(9-14)少了一个误差求和的步骤。

这种简化带来了很多便利。比如,对于一个具有数百万样本的训练集合,完成一次样本遍历就能对权值更新数百万次,效率大大提升。反观 BGD,要遍历数百万样本后,才更新一次权值。

因此,随机梯度递减策略针对单个训练样本 d 的误差函数是:

(9-15)

图 9-12 所示为 BGD 与 SGD 的权值调整路线对比。由图可看出,BGD(实线曲线)一直是“稳健”地向着最低点前进的,而SGD(虚线曲线)明显“躁动”了许多,但总体上仍然向最低点逼近。

SGD 的随机性并非完全是坏事,特别是在探寻损失函数极小值时。如果损失函数的目标函数是一个“凸函数”,它的极小值存在且唯一,沿着梯度反方向,就能找到全局唯一的最小值。然而对于“非凸函数”来说,就没那么简单了,它可能存在许多局部最小值。

对于 BGD 而言,一旦陷入局部最小值,基于算法本身的策略,它很难“逃逸”出,如图 9-13(a)所示。对于局部最小值点而言,左退一步、右近一点,函数值都比自己大,所以就认为自己是最小值。殊不知,一谷更比一谷低。

而 SGD 先天带来的随机性,反而有助于逃逸出某些糟糕的局部最小值。SGD 虽然失去了权值调整的稳定性,但却带来了求全局极小值的可能性,如图 9-13(b)所示。

利用SGD解决线性回归实践

使用UCI大学的白酒质量数据库:

from csv import reader
class LinearUnit(object):
    def __init__(self, input_para_num, acti_func):
        self.activator = acti_func
    def predict(self, row_vec):
        act_values = self.weights[0]
        for i in range(len(row_vec) - 1):
            act_values += self.weights[ i + 1 ] * row_vec [i]
        return  self.activator(act_values)
    def train_sdg(self, dataset, rate, n_epoch):
        self.weights = [0.0 for i in range(len(dataset[0]))]
        for i in range(n_epoch):
            for input_vec_label in dataset:
                prediction = self.predict(input_vec_label)
                self._update_weights(input_vec_label, prediction, rate)
    def _update_weights(self, input_vec_lable, prediction, rate):
        delta = input_vec_lable[-1] - prediction
        #更新权值,第一个元素:哑元的权值
        self.weights[ 0 ] = self.weights[ 0 ] + rate * delta
        for i in range(len(self.weights) - 1):
            self.weights[ i + 1 ] = self.weights[ i + 1 ] + rate * delta *input_vec_lable[i]
def func_activator(input_value):
    return input_value
class Database():
    def __init__(self):
        self.dataset = list()
    #导入CSV文件
    def load_csv(self, filename):
        with open(filename, 'r') as file:
            csv_reader = reader(file, delimiter=';')
            #读取表头,实际上是跳过首行
            headings = next(csv_reader)
            #文件指针下移至第一条真正数据
            for row in csv_reader:
                if not row:
                    continue
                self.dataset.append(row)
    def dataset_str_to_float(self):
        col_len = len(self.dataset[0])
        for row in self.dataset:
            for column in range(col_len):
                row[column] = float(row[column].strip())
    #找到每一列(属性)的最小值和最大值
    def _dataset_minmax(self):
        self.minmax = list()
        for i in range (len(self.dataset[0])):
            col_values = [row[i] for row in self.dataset]
            value_min = min(col_values)
            value_max = max(col_values)
            self.minmax.append([value_min, value_max])

    #将数据集合中的每个(列)属性都规整化到0~1
    def normalize_dataset(self):
        self._dataset_minmax()
        for row in self.dataset:
            for i in range(len(row)):
               row[i] =(row[i] - self.minmax[i][0]) / (self.minmax[i][1] - self.minmax[i][0])
        return self.dataset
def get_training_dataset():
        db = Database()
        db.load_csv("winequality-white.csv")
        db.dataset_str_to_float()
        dataset = db.normalize_dataset()
        return dataset
def train_linear_unit():
    dataset = get_training_dataset()
    l_rate = 0.01
    n_epoch = 100
    #创建训练线性单元,输入参数的特征数
    linear_unit = LinearUnit(len(dataset[0]), func_activator)
    #训练,迭代100论,学习率为0.01
    linear_unit.train_sdg(dataset, l_rate,n_epoch)
    #返回训练好的线性单元
    return  linear_unit
if __name__ == '__main__':
    #获取数据并训练
    LU = train_linear_unit()
    #打印训练获得的权重
    print("weights = ", LU.weights)
    #测试
    test_data = [[0.23,0.08,0.20,0.01,0.14,0.07,0.17,0.07,0.55,0.28,0.47,0.67],
                 [0.25,0.10,0.21,0.01,0.11,0.13,0.23,0.08,0.54,0.15,0.47,0.50],
                 [0.28,0.15,0.19,0.02,0.11,0.10,0.20,0.11,0.55,0.49,0.44,0.83],
                 [0.35,0.16,0.17,0.15,0.12,0.07,0.22,0.18,0.37,0.15,0.24,0.33]]
    for i in range(len(test_data)):
        pred = LU.predict(test_data[i])
        print("expected = {0}, predicted = {1}". format(test_data[i][-1], pred))

代码讲解:

from csv import reader

这行代码导入了Python标准库中的CSV模块中的reader类,以便后面可以使用CSV文件进行数据操作。

class LinearUnit(object):
    def __init__(self, input_para_num, acti_func):
        self.activator = acti_func

这里定义了一个LinearUnit类,表示一个线性单元。构造函数__init__接受两个参数,input_para_num表示输入参数的数量,acti_func表示激活函数。构造函数将acti_func存储为对象的activator属性。

def predict(self, row_vec):
    act_values = self.weights[0]
    for i in range(len(row_vec) - 1):
        act_values += self.weights[i + 1] * row_vec[i]
    return self.activator(act_values)

predict方法用于根据输入的row_vec(行向量)进行预测。它首先初始化act_values为权重的第一个元素,然后通过循环遍历输入的特征值,将每个特征值与相应的权重相乘并累加到act_values中。最后,它应用激活函数self.activator并返回结果。

def train_sdg(self, dataset, rate, n_epoch):
    self.weights = [0.0 for i in range(len(dataset[0]))]
    for i in range(n_epoch):
        for input_vec_label in dataset:
            prediction = self.predict(input_vec_label)
            self._update_weights(input_vec_label, prediction, rate)

train_sdg方法用于使用随机梯度下降(Stochastic Gradient Descent,SGD)算法来训练线性单元。它接受三个参数:dataset表示训练数据集,rate表示学习率,n_epoch表示训练周期数。

在方法内部,首先初始化权重为零。然后,外部循环迭代n_epoch次,每次迭代遍历整个数据集。对于每个数据点,它使用self.predict方法得到预测值,然后调用self._update_weights方法来更新权重。

def _update_weights(self, input_vec_lable, prediction, rate):
    delta = input_vec_lable[-1] - prediction
    self.weights[0] = self.weights[0] + rate * delta
    for i in range(len(self.weights) - 1):
        self.weights[i + 1] = self.weights[i + 1] + rate * delta * input_vec_lable[i]

_update_weights方法是train_sdg的辅助方法,用于更新权重。它接受输入数据点input_vec_label、预测值prediction和学习率rate作为参数。首先计算误差delta,然后分别更新权重,首先是权重的第一个元素,然后是其余权重。

def func_activator(input_value):
    return input_value

func_activator是一个简单的激活函数,它接受一个输入值并直接返回该值,即线性激活函数。

class Database():
    def __init__(self):
        self.dataset = list()

这里定义了一个Database类,用于处理数据集。构造函数初始化了一个空的数据集列表。

def load_csv(self, filename):
    with open(filename, 'r') as file:
        csv_reader = reader(file, delimiter=';')
        headings = next(csv_reader)
        for row in csv_reader:
            if not row:
                continue
            self.dataset.append(row)

load_csv方法用于从CSV文件加载数据。它接受一个文件名filename作为参数,打开文件并创建一个CSV读取器。它首先读取CSV文件的标题行,然后遍历文件的每一行,将每行数据添加到self.dataset列表中。

def dataset_str_to_float(self):
    col_len = len(self.dataset[0])
    for row in self.dataset:
        for column in range(col_len):
            row[column] = float(row[column].strip())

dataset_str_to_float方法用于将数据集中的字符串值转换为浮点数。它首先确定数据集中的列数(特征数量),然后遍历数据集的每一行和每一列,将每个元素从字符串转换为浮点数。

def _dataset_minmax(self):
    self.minmax = list()
    for i in range(len(self.dataset[0])):
        col_values = [row[i] for row in self.dataset]
        value_min = min(col_values)
        value_max = max(col_values)
        self.minmax.append([value_min, value_max])

_dataset_minmax方法用于找到数据集中每个列(属性)的最小值和最大值。它首先初始化一个空的self.minmax列表,然后遍历数据集的每一列,找到每列的最小值和最大值,并将它们存储在self.minmax列表中。

def normalize_dataset(self):
    self._dataset_minmax()
    for row in self.dataset:
        for i in range(len(row)):
           row[i] = (row[i] - self.minmax[i][0]) / (self.minmax[i][1] - self.minmax[i][0])
    return self.dataset
def get_training_dataset():
    db = Database()
    db.load_csv("winequality-white.csv")
    db.dataset_str_to_float()
    dataset = db.normalize_dataset()
    return dataset

get_training_dataset函数是用于获取训练数据的函数。它首先创建一个Database类的实例,然后加载名为"winequality-white.csv"的CSV文件,将数据转换为浮点数,然后规范化数据,并最后返回数据集。

def train_linear_unit():
    dataset = get_training_dataset()
    l_rate = 0.01
    n_epoch = 100
    linear_unit = LinearUnit(len(dataset[0]), func_activator)
    linear_unit.train_sdg(dataset, l_rate, n_epoch)
    return linear_unit

train_linear_unit函数用于训练线性单元。它首先获取训练数据集,设置学习率(l_rate)和训练周期数(n_epoch),然后创建一个具有输入参数数量和激活函数的LinearUnit类的实例,最后使用train_sdg方法训练线性单元,并返回训练好的线性单元。

if __name__ == '__main__':
    LU = train_linear_unit()
    print("weights =", LU.weights)

这部分是主程序块。它首先检查是否当前脚本是主模块,以防止在被导入为模块时执行此代码。如果是主模块,它执行以下操作:

  • 调用train_linear_unit函数以获取训练好的线性单元,存储在LU变量中。
  • 打印训练获得的线性单元的权重
test_data = [
    [0.23, 0.08, 0.20, 0.01, 0.14, 0.07, 0.17, 0.07, 0.55, 0.28, 0.47, 0.67],
    [0.25, 0.10, 0.21, 0.01, 0.11, 0.13, 0.23, 0.08, 0.54, 0.15, 0.47, 0.50],
    [0.28, 0.15, 0.19, 0.02, 0.11, 0.10, 0.20, 0.11, 0.55, 0.49, 0.44, 0.83],
    [0.35, 0.16, 0.17, 0.15, 0.12, 0.07, 0.22, 0.18, 0.37, 0.15, 0.24, 0.33]
]

for i in range(len(test_data)):
    pred = LU.predict(test_data[i])
    print("expected = {0}, predicted = {1}".format(test_data[i][-1], pred))

这部分定义了一个测试数据集test_data,其中包含了四个数据点,每个数据点有12个特征值。然后,通过循环遍历每个测试数据点,使用已经训练好的线性单元LU来进行预测,然后打印每个测试数据点的期望值和预测值。

这段代码的主要功能是使用线性单元进行回归分析,训练模型并测试其性能。它演示了数据加载、预处理、模型训练和预测的基本流程。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值