【深度学习】第五章:再次回看网络架构-激活函数-损失函数-梯度下降

五、再次回看:网络架构-激活函数-损失函数-梯度下降

本部分是对前面章节中的点对点的补充。

DNN网络的架构图、数据流(从输入数据到每层的参数再到最终输出)前面都给大家进行了详细的演示。其中还手写了一个神经元的数据传播过程,可以看到就是一个矩阵乘法的过程,而我们的网络动辄就是很多层、每层又是很多个神经元,手写神经网络显然不现实,但还给大家手写了一个就是为了看清背后的数学计算过程。

下面再强调一下几个概念:
(1)nn.Linear()层叫线性层,也叫全连接层。
(2)当有人问你的模型有几层的时候,一般是包括输入层的,只有隐藏层和输出层包括在内。因为隐藏层和输出层都是有要学习的参数的,所以一般说的模型层数都只说带参数的层。
(3)架构图上一般不会画出激活函数(也叫激活层),但你一定要直到每个线性层后面都一定要跟激活函数的,如果两个线性层之间没有激活层,那这两个线性层就可以合并成一个层了,就没必要设置两个层了。所以线性层后必有激活层,即使架构图没有标出,你也要知道一定是要有激活层的。
(4)输出层的神经元个数是根据任务设置的。输出层的激活函数也是根据任务设置的,而且还可以灵活设置,就是如果你的输出层没有激活函数也是可以的,也是可以放到损失函数的计算过程中,这个看你怎么方便怎么来。
(5)写架构的时候,一般都是写成一个类,并且这个类要继承nn.Module,并且必须写forward方法。

pytorch框架把这些比如线性层、激活函数、卷积层等都打包成一个个独立的元素封装在nn模块中,我们只要调用这些元素,就可以像搭积木一样搭建自己的网络架构了。前提是你非常熟悉这些构建,下面详细看看nn.Linear()这个线性层的类。

1、nn.Linear()

通过上面的步骤,你得掌握:
(1)nn.Linear()的参数,以及前后层之间的参数衔接关系。
(2)你得会用parameters()方法或者.weight、.bias属性查看你搭建的层的参数,并且非常清晰参数矩阵的行列都是表示什么。
(3)清楚数据传播过程中的数学逻辑。

2、写架构时的注意点
下图是我们前面章节搭建的架构,下面一个点一个点拆解一下:

注意点:
(1)一般都是把架构写成一个类,然后实例化这个类,就生成了我们的模型。
(2)A处就是表示我们这个my_model类继承了nn.Module父类。这个是必须的。nn.Module类是pytorch封装好的一个模型构造类,是所有神经网络模块的基类。我们要定义自己的网络,就必须要继承它,才能加载这个父类中的__init__函数和forward函数。这样我们后续很多步骤比如正向传播、计算图创建、反向传播、梯度计算、上GPU运行等都不需要自己再重新写代码实现。nn.Module类的定义代码就有一千多行,我们都继承了就会方便很多。
(3)B处是定义我们my_modle类的__init__函数,这个函数也是写类的时候必写的,而且第一个参数一定是self,后面两个参数是我的网络实例化时需要传入的两个参数:in_features、out_features,就是输入的数据的特征个数,和最后输出的神经元数量。
(4)C处是用来调用父类的__init__函数下定义的所有东西。所以C行代码基本就是固定写法,无脑写即可。也是必须要写的,否则你写出来的类连实例化都没法实现,更别谈后续的操作了。
(5)D处是定义自己的隐藏层和输出层,self一定不能少。这部分知识点是python的基本知识点,不知道为啥的得系统看看python基础。
(6)E处要注意的就是层和层之间的参数关系,一定是前一层的out_features等于这一层的in_features,不然网络就跑不通。
(7)F是定义网络的向前计算,而且参数永远都是self和x,就是固定写法。forward方法下面的代码就是数据流动的计算过程。
(8)G处是激活函数,就是数据经过线性层后要再经过激活函数。隐藏层的激活函数都不能省。
(9)H处是输出层后面跟的激活函数。输出层后面可以跟激活函数也可以不跟,如果不跟,在计算损失loss时,你如果是多分类,你还是的进行softmax转化,所以你架构处写与不写,就看你自己了。如果你是多分类,你最好是写成log_softmax,而不是单独一个softmax,而且后面一定还记得dim=1。后面我们还会单独展开讲激活函数和softmax函数,到时候你就串联起来了。
以上就是写架构要注意的点。

3、激活函数
一个DNN架构中,除了全连接的线性层外,最重要的就是激活层了,这里单独把激活函数拿出来说说。
这里还是得分门别类的说激活函数。

如果是隐藏层的激活函数,那激活函数的主要功能就是进行非线性变换的,不同类型的激活函数都有各种不同的特点,所以非线性变换的优缺点都不一样。下面针对隐藏层的激活函数,只展开说说sigmoid、tanh、relu这三种激活函数,后面学CNN、RNN遇到了其他激活函数再单独拎出来讲。

如果是输出层的激活函数,那激活函数的主要功能是你的任务决定的。
a、如果你是回归类任务,你的输出层最好是不要任何激活函数,最后的输出层只是对前面特征变换的一个整理即可。
b、如果你是二分类任务,你的输出层激活函数一般都是sigmoid函数,因为sigmoid可以把负无穷到正无穷的数都映射到0-1之间,就是一个类概率结果,就方便分类。
c、如果你的任务是多分类任务,那你的输出层可以不要任何激活函数,这也是非常常用的做法,然后此时你的损失函数就用交叉熵损失函数,调用nn.CrossEntropyLoss(reduction='mean')函数,就可以无缝衔接了。如果你非得在架构里面加激活函数,建议你用log_softmax激活函数,这样就可以调用nn.NLLLoss()函数进行无缝衔接。如果你用的是softmax函数,那你就只能先对结果取对数,然后再调用nn.NLLLoss()函数计算损失,就比较麻烦一些,但也不是不可用。
后面讲到损失函数的时候,还会演示这里的结论,所以本部分我只单独把softmax函数拿出来说说。

(1)隐藏层的sigmoid、tanh、relu激活函数
作为隐藏层的激活函数,一是在数据正向传播过程中,要承担非线性变换的作用,这也正是神经网络模型的强大之处;二是在反向传播求梯度的时候,由于激活函数也是链式求导中的一个环节,所以激活函数另一个责任就是:至少不能妨碍梯度的回传。

也所以我们在选择激活函数的时候,是从上面两个方面来考虑使用哪个激活函数比较好一些。那我们下面看看不同激活函数是怎样进行非线性变换的,以及反向传播求梯度时的反应:


可见:

  • 在正向传播过程中,不管线性变换传来的值是多少,经过sigmoid激活函数,都会被变成0-1之间的数值。也正是0-1这个区间非常有意义,所以sigmoid函数经常也被放在输出层后面,用于二分类任务中的输出一个类概率值的要求。我们的逻辑回归模型,如果用DNN来表示,那就是一个特征个数个神经元的隐藏层+一个2个神经元的输出+sigmoid激活函数。
  • 在反向传播过程中,导函数连续,不存在导数不存在的情况。而且求导也非常简单。
  • 在[-5, 5]之间我们成为sigmoid函数的饱和区间,之所以提出这个饱和区间是因为,从正向传播来看,如果线性层传过来的数据超出这个区间,sigmoid函数的输出基本不是0就是1,就是说它对[-5,5]外的数据基本就不敏感了,没有区别性了。从反向传播来看,如果数据超出饱和区间,导数基本就是0了,那在链式求导的过程中,它作为其中的一个乘项,它趋近0,岂不是导数就趋近于0。所以如果你的网络比较深,而且你用的还是sigmoid激活函数,那就意味着反向传播时很容易出现梯度为0的情况,这种情况又叫梯度消失 ,梯度消失就意味着参数不迭代了(因为w = w - lr*grad),参数不迭代了就意味着模型不学习了,也意味着你无法训练模型了。
  • sigmoid函数的输出不是0均值,也就表示后面的线性层的输入不是0均值的信号,如果网络非常深,经常会导致梯度不平稳,影响模型学习和训练。这方面我们后面讲模型优化时会展开详细说明。


tanh激活函数是诞生在sigmoid激活函数后面的,是人们面对sigmoid激活函数的非0均值输出,无法很好训练深度模型后的思考,才诞生了tanh激活函数。所以tanh激活函数的最大优点就是0均值输出,而且它的导数也不复杂。但是tanh激活函数也有很多问题,比如它同样是exp计算,而且比sigmoid函数的exp计算还多,所以它经常不是梯度消失就是梯度爆炸,同样难以驾驭。但是当你的数据比较平稳,不出现特别大或者特别小的输入输出时,tanh的效果是要好于sigmoid的。


relu激活函数是当前使用最多的激活函数,经验表明使用ReLU的SGD算法的收敛速度比sigmoid和tanh快。而且relu的计算非常简单。但是relu也有一个缺陷就是,非常容易出现Dead ReLU Problem,就是神经元坏死现象,也叫神经元失活。relu是可以直接把线性变换后的负值直接变成0的,那这就相当于让这个神经元毙掉了,因为,你想一个0值再往后一层传,即使后一层的参数不为0,后一层的神经元接受到的数值也没有这个神经元的什么事儿了,所以就相当于把这个神经元失活了,就相当于前一层有没有这个神经元都无所谓了。再看反向传播,当relu把这个神经元的输出变成0后,那反向传播时它的梯度就是0了,那就意味着这个神经元的参数得不到更新了,参数不更新,意味着下次正向传播时还是上一轮的参数,所以大概率还是被relu变成0,就表示这个神经元不再相应后面的数据了,参数也就一直更新不了,很难再活过来。

但是随着优化技术的发展,这个问题一定程度也被解决得差不多了,后面我们讲优化的时候,会针对这个问题专门演示。这里想说的是,虽然relu容易出现神经元失活,影响训练,但是从另外角度看,它还不一定是坏事。比如有人就说,正是relu的这种特性可以抑制网络过拟合,让一些神经元失活,从而减小网络的复杂性,从而减小过拟合;再比如这个特性还可以逼迫网络上的所有权值尽量均匀分布,不能把参数都集中在某些神经元,这就相当于L2正则化的效果。

(2)输出层的softmax函数
前面说了输出层是可以不要任何激活函数的,因为激活函数的数学变换过程可以写到损失函数里面。
如果你非想在输出层写激活函数而非在损失函数中写,那分两种情况:一是,如果你是二分类你就用sigmoid激活函数,这个激活函数在前面讲过了。二是,如果你是多分类你就得用softmax或者log_softmax函数,这要看你如何和损失函数搭配了。所以这里先重点讲一下softmax激活函数。

softmax函数的计算公式也是非常简单,原理也非常简单,不简单的是代码实现有点小难理解,所以为了后面讲CNN(CNN很多都是多分类,所以会经常用到softmax)方便一点,这里先把softmax聊清楚。下面是pytorch实现softmax的方法:

这里面最难理解的就是dim这个参数,这个参数可以参考下图理解:

从公式上看,softmax函数的本质就是:对所有元素取e为底的指数计算,然后加和,然后算各个分量在加和中的占比。这个函数是个单调函数。越小的数,负无穷,它经过softmax变换就趋近0;越大的数,正无穷,就趋近1。所以数据传递神经元后其相乘相加加截距后的值越大,概率就越趋近1,反之,概率就趋近0。
但是,因为要进行指数运算,所以softmax函数经常会出现溢出现象infinite。就是取e为底的指数,很容易出现无法计算,即出现内存不足或者python服务器直接断开连接的情况。如果溢出一般的做法就是再加个log,就不会太大了。所以当我们自己手动计算softmax的时候,当出现溢出报错的时候,我们用pytorch的softmax函数就不会报错,那是因为它进行了log处理所以可以正常计算出softmax值。所以当我们手动的计算结果和调用函数的计算结果有一些精度差异的时候,不要惊讶,这是正常的。

4、损失函数

建立一个深度学习模型的过程就是:定义架构->定义损失函数->定义优化算法->以最小化损失函数为目标,求解出模型的wb->新样本带入模型进行预测。
之前也一直说损失函数就是深度学习模型的灵魂,本部分详细聊聊损失函数。

在初学阶段我们一般是做个回归任务或者分类任务,也就是训练一个回归模型或者分类模型。这个阶段我们最常用使用的损失函数是:
当你的模型是回归类模型的时候,也就是标签是一组连续的数值的时候,损失函数一般都用SSE,sum of the squared errors, 总误差平方和,或者用均方误差 MSE=1/m SSE。这两个指标你选哪个都可以,都可以作为你的损失函数,也就是一直说的loss。
当你的模型是分类模型时,也就是分类任务时,我们一般都用交叉熵损失函数。其中二分类只是多分类的一个特殊情况,所以二分类和多分类都用的是交叉熵损失函数。

这些损失函数是最常见最基础的损失函数,除此之外还有很多其他损失函数,比如L1(Mean Absolute Error)、L2(Mean Square Error)、Huber Loss LogCosh Loss、Cross Entropy(Log Loss)、Focal Loss、Hinge Loss等等。而至于选择什么样的损失函数,是根据你的任务和你想达到的目的来选择的,就是要选择能恰当的量化你模型效果和你目标之间差距的函数。其次是要考虑是否可以收敛,就是是否有解、是否可导、要搭配什么优化算法来求解、以及求解过程中的时间效率和空间效率等因素。一个恰当合适的loss可以让模型收敛更快,预测更准确,比如MSE Loss的梯度正比于Loss值,因此用MSE做损失函数进行训练时,收敛速度一般比SSE快。

损失函数背后都是数学,你要想非常清楚各种损失函数的优缺点,就得深剖背后的数学,Paddle损失函数总结 - 飞桨AI Studio星河社区 这篇博文把上面几种损失函数背后的数学以及可能的偏差说的非常清楚。

当我们的损失函数确定后,就开始求这个损失函数的最小值,而求最小值的过程就是找模型参数wb的过程,而求最小值的方法一般是采用梯度下降法。

为什么要找损失函数的最小值?我们就以回归模型为例,回归模型的损失函数是mse,是不是mse越小就越说明每条样本的预测值和真实标签越接近,也就是说预测结果和真实结果相差越小。所以,就是损失函数越小,说明模型预测效果越好,损失函数越大模型越糟。也所以说损失函数是模型的灵魂,因为损失函数决定了模型的样子。

为什么说求损失函数最小值的过程就是找模型参数wb的过程?因为你的损失函数是一个关于wb的函数呀,所以求损失函数最小值的本质就是一个数学问题了,一般情况下数学层面的处理流程是:先是把损失函数转化为凸函数(但是神经网络就不需要这步),其中最常见的就是拉格朗日变换。然后再求这个凸函数的最小值,这步就是优化的过程,用到的算法就叫优化算法,最常见的就是梯度下降优化算法。

损失函数背后的数学可以到网上查到很多的相关资料,所以这里我们重点展示一下如何代码实现:
(1)MSE和SSE

(2)二分类交叉熵

二分类交叉熵损失,nn提供了2个类:
class BCEWithLogitsLoss里面内置了sigmoid函数和交叉熵函数,它会自动计算输入值的sigmoid值,因此只需要输入zhat与真实标签即可。
class BCELoss 只有交叉熵函数没有sigmoid层,因此需要输入sigma与真实标签。

BCE,表示binary cross entropy loss
真实标签在第二个参数。预测值和真实标签的数据类型及结构shape必须相同。
上面两个类还有第三个参数reduction,默认是mean, 所以也可以设置reduction='sum'就是求总体误差。如果设置reduction='none'就求出来的是一个1000行1列的矩阵,就是每个样本的误差。

为什么求一个误差就要提供两个类呢?因为sigmoid函数是存在精度问题的。所以torch提供BCEWithLogitsLoss()的类中计算的sigmoid结果的精度要比我们自己计算的sigmoid精度要高。所以当我们有很高的精度要求的时候要用BCEWithLogitsLoss()

(3)多分类交叉熵损失函数
对于多分类模型,我们要先把标签向量变成onehot矩阵,就是做哑变量。这个onehot矩阵中用1表示真实标签的位置,用0表示不是真实标签。如下图所示:

如果你的模型是多分类模型,那你架构的输出层的神经元数量就是标签类别个数个神经元,此时你架构中的输出层可以不放任何激活函数,也可以放softmax激活函数,注意,如果你放激活函数的话,就只能放softmax激活函数。此时数据流再经过softmax激活层,返回的就是标签类别个数的类概率值。

多分类交叉熵损失函数的公式是:-sum(yi*log(y_predi))。其中,y_predi就是网络架构返回的softmax值,yi就是onehot矩阵。也可以看出,二分类交叉熵损失函数就是多分类交叉熵损失函数的一种特殊情况。
所以,这个计算过程就是先把softmax值取对数,然后再和onehot矩阵相乘,最后所有样本加和。

这个过程pytorch把每步都打包成一个类,这样我们就可以灵活使用。如果你的架构中没有softmax层,那你就先调用nn.logsoftmax类,把你模型的预测值变成类概率值,然后再调用nn.NLLLoss类计算交叉熵损失。
如果你的架构中已经有了softmax层,那你模型的输出就是已经是y_pred这种类概率值了,此时你调用nn.CrossEntropyLoss类计算交叉熵即可。

以上就是我们常见的损失函数的代码实现过程,在初学阶段这三种损失函数就足够应付回归类和分类类任务了。以后继续讲到别的模型,遇到别的损失函数再继续补充。

小结:我们建模的目的就是要预测,就是要预测效果非常好。那预测的效果就体现在损失函数上,因为我们的损失函数就是衡量预测值和真实标签之间的差距的。所以如果你的损失函数值为0,那就说明你都预测对了。所以损失越小,预测效果越好。也所以这就是一个数学问题:求损失函数的最小值。也所以,你的损失函数一定要和你的预测效果挂钩,所以我们有了SSE/MSE/交叉熵损失这些损失函数,都是衡量预测值和真实标签之间的差距的函数。
同时,我们的损失函数也正是我们的参数的函数,所以求到损失函数的最小值对应的那组参数就是我们模型的参数。也就是在这组参数下,模型的损失函数最小,也就是模型的预测效果最好。

5、梯度下降

上面我们定义了损失函数,那下面就是要找使损失函数取得最小值的一组权重w和b,就是优化的过程。而优化方法也有很多,其中最常见的就是梯度下降优化方法。这里我们也是简单入门一下梯度下降优化算法,因为DNN还是深度学习中最简单的架构,没法用它来举例,以后学到更复杂模型,遇到更前沿的优化算法,我们再补充。
其实看到这里,大家基本都明白了,深度学习其实并不难,全部都是围绕着数据、架构、损失、优化这几个方面进行的。所以后面更高阶的内容也都是围绕这几个点进行的。也所以这里只讲最最基础的梯度下降算法,主要是让大家了解一下基本思想。

从最开始说起吧,一个神经网络的训练过程就是:一个样本,比如有n个特种,从第0层神经网络灌进去进入第1层,经过第1层网络和第2层网络之间的w调节,调节就是相乘相加,得到一个数传到中间层第2层的神经元,这个神经元再把这个数用relu或者sigmoid等函数非线性变换成另一个数,输出第2层隐藏层,然后再经过第2层和第3层神经元之间的权重w再调节传入第3层神经网络层,如此数据传递,最后传到最后一层输出层, 如果是回归模型,数据相乘相加传到输出层神经元后可以直接输出,也可以也加一个函数映射,这里要说明的是对于回归模型一般最后一层神经网络只有一个神经元。

如果是二分类模型,一般最后一层神经网络可以有1个神经元也可以有2个神经元,如果有1个神经元,数据相乘相加传到输出层的这1个神经元后就是sign函数输出0或者1, 或者是sigmoid函数输出1个类概率概率,但是展示给我们的时候输出的就是相加=1的两个类概率,我们也可以把这两个类概率再用列表推导式,阈值设为0.5,把这两个类概率转化为0,1。如果输出层是有2个神经元的话,这两个神经元的激活函数就不能是sigmoid函数,就必须是softmax函数,用softmax函数把这两个结果,也就是把这两个数转化为相加等于1的两个类概率,然后用列表推导式阈值设为0.5输出两个0-1预测结果。

如果是多分类,最后输出层的神经元数量就是多分类的类别个数,数据相乘相加后传到输出层后就是softmax函数将其转化成多个数相加等于1的多个类概率,最后模型输出的结果既可以是多个类概率也可以是列表推导式把其中概率最大的那个类别设为1,其他几个类别全部设为0。

上面的过程就是数据的正向传播,这上面传播的过程中,我们都是先随机生成每层数据在传输过程中都需要的w,所以每层数据都是一个相乘相加的一个结果,每个神经元都有一个相加相乘的这个结果的输入和激活函数的映射输出结果。所以数据正向传播后我们会得到很多结果。

现在开始反向传播,为什么要反向传播呢?因为我们正向传播的时候w都是随机生成的呀,随机生成的w能指望它的输出有多接近样本的真实标签呀,所以我们要先假设一组w,也就是先随机生成一组w,让数据根据这组w正向传播一次,然后计算我们的损失函数,也就是计算一下这组随机的w会产生和真实标签y之间有多大的一个损失,这个损失我们是回归模型的话,就是mse均方误差,就是要训练一组w使这个mse均方误差最小。如果是二分类,我们的损失函数就是二分类交叉熵损失,我们追求的也就是最后的输出的类概率的结果近可能的带入这个交叉熵函数后,这个函数能取得最小的值,越小就说明我们预测的准确,都和真实的标签y相同。如果是多分类,我们的损失就是多分类交叉熵损失。所以问题到这里就变成我们怎么取得损失函数的最小值,而损失函数的最小值与我们的每个权重w都相关,我们要从右往左一个个w去看,这个w到底为损失函数的增加贡献了多少力量,比如,在输出层第一个神经元和它相邻的隐藏层上第一个神经元之间的参数w,到底为损失贡献了多少?那么它贡献了多少怎么衡量?就是如果这个w变化一点点这个损失函数会减少多少,也就是说这个损失函数对w求偏导数,那这个偏导数怎么求?就是链式求导法,先损失函数对输出结果求导,损失函数一定是模型预测结果的某个f函数,所以先损失函数对模型输出结果求导,而这个导数是输出结果的某个函数,输出结果我们已经在正向传播中得出了结果,所以直接带入这个函数就得出了这部分导数值。然后就是输出结果对相加相乘结果求导,这里输出结果就相当于y,相加相乘就相当于x,y和x之间的关心就是激活函数,这里我们假设激活函数都是可导的,我们求出的导函数就是关于x的函数,这里的x就是激活函数的输出结果,我们直接将这个结果带进去就求出了这部分导数了。然后就是求相加相乘结果对w的导数,这部分的导数就是上一层的隐藏层的输出结果和w相乘得到的想家想乘,所以相加相乘对w的导数就是上一层的结果,而上一层的结果我们在正向传播的时候也是已经都求出了过了。这样我们这三个导数连乘,就是链式求导的结果,而这三部分的导数,我们在正向传播时计算的结果都可以直接带入求出,至此我们就算出了损失函数关于这个倒数后面的w的倒数。

这里我们求出来的倒数的意思就是这个w变化一点点损失函数要变化多少,我们也称为梯度,这个梯度是有方向和大小的,我们有了梯度就知道我要怎么迭代这个w了,怎么迭代这个w才能减小损失函数,于是我们就开始迭代w,令w t+1 = wt-λ*上面我们已经求出来的梯度,其中,λ称为步长,如此我们迭代很多次,就找到了使损失函数最小的w。

所以反向传播的意思就是反向一步步求导,求出损失函数对应的w的导数,也就是梯度,我们再让w沿着这个梯度的反方向移动,因为w这样移动后损失函数就减小了。如此循环迭代,就找到了使损失函数最小化的一组w,也就是我们的神经网络的学习过程,也就是我们要的模型,再用这个模型去灌入新样本去预测,就会得到比较好的预测结果。

上面是求损失函数的最小值的流程,这个流程中的难点求偏导,由于神经网络的损失函数的具体表达式根本就写不出来,就没法求导,所以神经网络算法沉寂了半个世纪都没有太多的进展,直到有人提出数学上的链式求导法,在计算机上如何反向传播实现求导,神经网络才有了质的飞跃。

下面代码展示反向传播求导,并且迭代5次:

说明:上面反向传播只能执行一次,再运行执行就会报错说没有存储计算图,要解决这个问题就要加一个参数: loss.backward(retain_graph=True), 但是我们必须再执行一次正向传播才能再执行这行代码的时候才能保存计算图。
可见,pytorch都给我们反向求导都打包好,我们只需要loss.backward()一行代码,但这行代码包含的内容就是上述的文字描述和对应的数学方法。 下面看一下上面的运行结果:

至此,我们“创建数据、构建神经网络架构、添加激活函数、正向传播进行预测、计算损失函数、反向传播求梯度、最简单的梯度下降法迭代参数”这些步骤都点对点地又走了一遍。神经网络的基础内容就这么多。
但是,神经网络的内容远不止这些。并且上面的步骤也不是每步都走的非常顺利的。比如上面的例子,我们只是迭代了5次,损失loss的绝对值还是非常大的,就是我们预测的效果还是非常差的。但是我们迭代5万次就能迭代到loss趋近0吗?未必!这个过程中还有非常多的坎,后面我们就是针对这些坎进行讲解。也就是下一个章节我们开讲神经网络的优化。

  • 12
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
LeNet-5是一种经典的卷积神经网络,它是深度学习的先驱之一。在这里,我将向您展示如何使用C语言来实现LeNet-5模型。 在C语言中实现卷积神经网络需要用到一些基本的线性代数运算,例如矩阵乘法、向量加法和卷积等。您可以使用像OpenBLAS或MKL这样的优化库来执行这些运算。然后,您需要定义LeNet-5模型的架构和参数。最后,您可以编写代码来实现前向传递和反向传播算法,以及使用随机梯度下降法进行训练。 以下是一份示例代码,它演示了如何在C语言中实现LeNet-5: ```c #include <stdio.h> #include <stdlib.h> #include <math.h> // 定义神经网络层的参数 typedef struct Layer { int in_size; // 输入数据的大小 int out_size; // 输出数据的大小 int filter_size; // 过滤器的大小 int stride; // 步长 int padding; // 填充 double *weights; // 权重矩阵 double *biases; // 偏差向量 double *out_data; // 输出数据 } Layer; // 定义神经网络模型 typedef struct Model { int input_size; // 输入数据的大小 int output_size; // 输出数据的大小 Layer conv1; // 第一层卷积层 Layer conv2; // 第二层卷积层 Layer fc1; // 第一层全连接层 Layer fc2; // 第二层全连接层 } Model; // 定义ReLU激活函数 double relu(double x) { return fmax(0, x); } // 定义softmax激活函数 void softmax(double *x, int n) { double sum = 0; for (int i = 0; i < n; i++) { x[i] = exp(x[i]); sum += x[i]; } for (int i = 0; i < n; i++) { x[i] /= sum; } } // 定义卷积操作 void conv(double *in_data, double *out_data, double *weights, double *biases, int in_size, int out_size, int filter_size, int stride, int padding) { for (int i = 0; i < out_size; i++) { out_data[i] = biases[i]; for (int j = 0; j < in_size; j++) { for (int k = 0; k < filter_size; k++) { int index = i * stride + k - padding; if (index >= 0 && index < in_size) { out_data[i] += weights[j * filter_size + k] * in_data[index]; } } } out_data[i] = relu(out_data[i]); } } // 定义全连接操作 void fc(double *in

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值