深度学习定义:一般是指通过训练多层网络结构对未知数据进行分类或回归
深度学习分类:
有监督学习方法——深度前馈网络、卷积神经网络、循环神经网络等;
无监督学习方法——深度信念网、深度玻尔兹曼机,深度自编码器等。
深度学习的思想:
深度神经网络的基本思想是通过构建多层网络,对目标进行多层表示,以期通过多层的高层次特征来表示数据的抽象语义信息,获得更好的特征鲁棒性
--基础概念
张量 (Tensor)
维度 (Dimension)
数据类型 (Data Type)
--模型构建
神经元 (Neuron)
层 (Layer)
神经网络 (Neural Network)
激活函数 (Activation Function):赋予神经网络非线性特性的函数,作用于神经元的加权求和结果。常见的激活函数有 Sigmoid 函数、ReLU函数、tanh 函数等。
损失函数 (Loss Function):常见的损失函数有均方误差损失MSE,常用于回归任务;交叉熵损失,常用于分类任务。
--模型训练
优化器 (Optimizer) :调整模型参数以最小化损失函数的算法。常见的优化器有随机梯度下降(SGD)及其变种,如 Adagrad、Adadelta、Adam 等。
梯度 (Gradient) :损失函数关于模型参数的导数,指示了损失函数在参数空间中上升最快的方向,优化器通过反向传播计算梯度,并沿梯度的反方向更新参数,以降低损失函数值。
反向传播 (Backpropagation) :通过链式法则将损失函数对输出层的梯度反向传播到网络的每一层,从而计算出损失函数对每个参数的梯度,为参数更新提供依据。
学习率 (Learning Rate) :优化器在每次更新参数时步长的大小,它决定了模型在参数空间中搜索最优解的速度。
批量大小 (Batch Size)
迭代 (Epoch)
--模型评估与应用 过拟合 (Overfitting)
欠拟合 (Underfitting)
泛化能力 (Generalization Ability)
准确率 (Accuracy)
召回率 (Recall)
F1 值 (F1 - score)
神经元是神经网络的基本计算单元。每个神经元接收多个输入,每个输入都对应一个权重。
神经元首先对输入进行加权求和:z=∑_i=1_n w_i * x_i + b
其中b是偏置(bias)
然后,加权求和的结果z会通过一个激活函数f,得到神经元的输出y=f(z)
激活函数赋予了神经元非线性特性,使得神经网络能够学习复杂的函数关系。
多层感知机由感知机推广而来,最主要的特点是有多个神经元层,因此也叫深度神经网络。
结构
输入层:负责接收外部数据,它的神经元数量通常取决于输入数据的维度。例如,对于一张28×28像素的灰度图像,输入层神经元数量为28×28=784
隐藏层:位于输入层和输出层之间,不直接与外部数据交互。隐藏层可以有一层或多层,每一层都由多个神经元组成。
隐藏层的作用是对输入数据进行特征提取和转换,将原始数据映射到一个更高层次的特征空间。不同隐藏层学习到的特征逐渐从简单的局部特征过渡到复杂的全局特征 。
在回归任务中,输出层一般只有一个神经元,输出一个连续值 。
全连接层
在全连接层中,每一个神经元都与上一层的所有神经元相连。假设上一层有m个神经元,当前层有n个神经元,那么这两层之间就有m×n个权重参数。
全连接层可以对输入数据进行线性变换,公式为y=f(Wx+b),其中x是上一层的输出向量,W是权重矩阵,b是偏置向量,f是激活函数 。

其中圆形节点表示一个神经元,方形节点表示一组神经元
神经网络的计算主要有两种:
前向传播(foward propagation, FP)作用于每一层的输入,通过逐层计算得到输出结果;
反向传播(backward propagation, BP)作用于网络的输出,通过计算梯度由深到浅更新网络参数。
损失函数
为了衡量神经网络预测结果与真实值之间的差异,需要定义一个损失函数L
在回归任务中,常用均方误差损失函数,在分类任务中,常用交叉熵损失函数
反向传播
反向传播是一种高效计算损失函数关于神经网络参数梯度的算法。
它基于链式法则,从输出层开始,将损失函数对输出层的梯度反向传播到每一层,从而计算出损失函数对每一个权重和偏置的梯度。
例如,对于上述三层神经网络,先计算损失函数L对输出层输出 y^ 的梯度 ∂L/∂y^ 然后通过链式法则计算对隐藏层输出h的梯度 ∂L/∂h = ∂L/∂y^ * ∂y^/∂h
进而计算对隐藏层权重W 1和偏置b 1的梯度 。
参数更新
计算出梯度后,使用优化器来更新神经网络的参数。最常用的优化器是随机梯度下降(SGD)及其变种。
SGD 的基本思想是在每次迭代中,沿着损失函数梯度的反方向更新参数,更新公式为 w_t+1 = w_t − η * ∂L/∂wt 其中wt是当前参数值,η是学习率,
∂L/∂wt是损失函数关于参数w在当前时刻的梯度
通过不断迭代更新参数,使损失函数逐渐减小,从而让神经网络学习到数据中的模式和规律 。
超参数是在开始学习过程之前设置值的参数,而不是通过训练得到的参数数据。通常情况下,需要对超参数进行优化,给学习机选择一组最优超参数,以提高学习的性能和效果
比如学习率、梯度下降法迭代的数量、隐藏层数目、隐藏层单元数目、激活函数都需要根据实际情况来设置,这些数字实际上控制了最后的参数和的值,所以它们被称作超参数。
它使用BOBYQA算法,并有一个较优的起始点。由于BOBYQA只寻找最近的局部最优解,所以这个方法是否成功很大程度上取决于是否有一个好的起点。
在MITIE的情况下,我们知道一个好的起点,但这不是一个普遍的解决方案,因为通常你不会知道好的起点在哪里。这种方法非常适合寻找局部最优解。
激活函数将非线性特性引入到神经网络中。没有激活函数的每层都相当于矩阵相乘。输出仅是一个简单的线性函数。
激活函数可以把当前特征空间通过一定的线性映射转换到另一个空间,让数据能够更好的被分类。
为什么激活函数需要非线性函数
假若网络中全部是线性部件,那么线性的组合还是线性,输出就是一个简单的线性函数。这样就做不到用非线性来逼近任意函数。
使用非线性激活函数,使网络表示输入输出之间非线性的复杂的任意函数映射。
常见激活函数


引用资料
https://ai-wx.blog.csdn.net/article/details/104729911
https://ai-wx.blog.csdn.net/article/details/118914829
详解各激活函数(归一化层,“神经元死亡” 现象)
https://blog.csdn.net/JNingWei/article/details/79210904
总结
https://www.cnblogs.com/wj-1314/p/12015278.html
深度学习网络训练过程可以分成两大部分:前向计算过程与反向传播过程。
反向传播是为了将设定的网络中的众多参数一步步调整,使得预测结果能更加贴近真实值。
那么,在反向传播过程中,很重要的一点就是:参数如何更新?
显然,参数应该是朝着目标损失函数下降最快的方向更新,即最小化损失函数!
什么是梯度下降算法
一种常用的一阶迭代优化算法,用于求解无约束优化问题,在深度学习中被广泛应用于训练模型,以最小化损失函数。
什么是梯度
梯度是一个向量,它在函数中的每一点都指向函数增长最快的方向。
对于一个多元函数 J(θ1,θ2,⋯,θn),其梯度表示为∇J=(∂J/∂θ1, ∂J/∂θ2,⋯, ∂J/∂θn)。
例如,对于函数J(θ) = θ1^2 + θ2^2,∇J=(2θ1,2θ2)。
基本思想
为了找到损失函数的最小值,需要沿着与梯度相反的方向移动。因为梯度指向函数增长最快的方向,那么其反方向就是函数下降最快的方向。
每次移动的距离由学习率(learning rate)决定。
算法公式
单变量情况:
假设我们有一个单变量损失函数J(θ),梯度下降算法通过不断更新θ来最小化J(θ)。 更新公式为:θ = θ − α * ∂J(θ)/∂θ
例如,对于函数J(θ)=(θ−2)^2,其导数∂J(θ)/∂θ=2(θ−2)。若初始θ=5,学习率α=0.1,则第一次更新后θ=5−0.1×2×(5−2)=5−0.6=4.4。
多变量情况:
对于多元函数J(θ1,θ2,⋯,θn),更新公式为:θi = θi − α * J(θ1,θ2,⋯,θn)/∂θi。这里θi是函数的参数。
在深度学习中,损失函数L通常是关于模型参数W和b(例如权重和偏置)的多元函数,我们通过上述公式更新W和b来最小化损失函数。
引用资料
https://zhuanlan.zhihu.com/p/22252270
Pytorch中优化器与学习率衰减方法总结
https://blog.csdn.net/ys1305/article/details/94332643



计算示例


新的参数 θ0 =0.04,θ1 =0.0867,再次计算梯度并更新参数,直到满足收敛条件(如损失函数变化小于某个阈值或达到最大迭代次数)。
每次迭代(更新参数)只使用单个训练样本

优点:
SGD 一次迭代只需对一个样本进行计算,因此运行速度很快。
缺点:
一方面,SGD 的波动使它能够跳到新的可能更好的局部最小值。
另一方面,使得训练永远不会收敛,而是会一直在最小值附近波动。
2. 一次迭代只计算一张图片,没有发挥GPU并行运算的优势,使得整体计算的效率不高。
3. 由于梯度的不稳定性,SGD 通常难以收敛到精确的全局最优解,而是收敛到一个接近全局最优解的区域。
核心原理
小批量数据计算梯度:
MBGD 每次迭代使用一小部分训练样本(称为一个小批量,batch)来计算梯度。损失函数通常定义为小批量内样本损失的平均值,然后根据这个梯度来更新参数
参数更新:
利用计算得到的小批量梯度,结合学习率α,按照θ=θ−α∇J(θ)的公式更新参数。通过不断重复这个过程,遍历整个训练数据集多次(称为 epoch),逐步调整参数θ,使损失函数最小化
优势
计算效率与稳定性平衡:
相比于 BGD,MBGD 每次只处理小部分数据,大大减少了计算量,提高了训练速度,尤其适用于大规模数据集。
与 SGD 相比,由于使用多个样本计算梯度,MBGD 的梯度估计相对更稳定,减少了因单个样本随机性导致的参数更新波动,使得模型收敛过程更加平稳。
劣势
小批量大小选择:
小批量大小b的选择较为关键。在实际应用中,需要通过实验来确定合适的小批量大小。
收敛速度:
尽管 MBGD 在大多数情况下收敛速度较快,但在某些复杂的非凸问题中,可能仍然无法快速收敛到全局最优解或接近全局最优解的区域,
相较于一些自适应学习率的优化算法,如 Adam,可能需要更多的迭代次数。

优势
自适应学习率:
无需手动调整每个参数的学习率,算法能够自动根据参数的更新情况调整学习率。AdaGrad 可以为不同参数提供合适的学习率。
处理稀疏数据:
对于数据集中出现频率较低的特征(稀疏数据),其对应的参数更新频率也较低,AdaGrad 会为这些参数分配较大的学习率,使得模型能够更好地捕捉这些稀疏特征的信息。
劣势
学习率单调递减:
随着训练的进行,梯度累积矩阵的对角元素不断增大,导致学习率单调递减,最终可能变得极小。
这会使得参数更新步伐越来越小,导致模型训练后期收敛速度极慢,甚至可能无法收敛到最优解。
超参数依赖:
虽然 AdaGrad 减少了对学习率手动调整的需求,但对初始学习率α和防止除零的小常数ϵ的选择仍然较为敏感。
Adagrad的改进,解决了 Adagrad 学习率单调递减且过早衰减的问题,能够在训练过程中动态调整学习率。

优势
无需手动设置学习率:AdaDelta 通过自身机制动态调整学习率。
对非平稳目标函数适应性强:
在处理非平稳目标函数(如复杂的损失函数)时,AdaDelta 能够根据梯度的变化动态调整学习率,更好地适应目标函数的变化,从而提高模型的收敛速度和稳定性。
有效避免学习率过早衰减:
通过采用指数加权移动平均的方式更新梯度平方累积平均,AdaDelta 避免了 Adagrad 中学习率过早衰减的问题
劣势
对超参数敏感:虽然 AdaDelta 不需要手动设置学习率,但它对衰减系数ρ和防止除零的小常数ϵ比较敏感。
计算依赖历史信息:
由于 AdaDelta 的计算依赖于过去的梯度和参数更新量信息,在某些情况下对梯度变化的响应可能不够及时
特别是在梯度突然发生较大变化时,可能需要一定的时间来调整学习率以适应新的梯度情况。
AdaDelta的一个特例

优势
避免学习率过早衰减:
RMSProp 通过引入梯度平方的移动平均,克服了 Adagrad 中学习率随时间单调递减且过早衰减的问题。
它能够根据近期梯度的变化动态调整学习率,使得模型在训练后期仍能保持一定的学习能力,加快收敛速度。
对非平稳目标函数有效:
在深度学习中,损失函数往往是非平稳的,其表面可能包含许多局部起伏。
RMSProp 能够适应这种非平稳性,通过对梯度变化的及时响应,更好地在复杂的损失函数表面找到最优解。
相对简单易调参:
相比于一些复杂的自适应优化算法,RMSProp 的超参数相对较少,主要涉及初始学习率α、衰减系数ρ和防止除零的小常数ϵ。
劣势
对梯度一阶矩利用不足:
RMSProp 主要关注梯度的二阶矩(平方的平均)来调整学习率,对梯度的一阶矩(梯度的平均)利用不足。
可能导致在某些复杂问题上的收敛效果不如同时利用一阶矩和二阶矩的算法,如 Adam 算法。
可能陷入局部最优解:
尽管 RMSProp 在收敛速度和处理非平稳目标函数方面表现良好,但像其他基于梯度的优化算法一样,它仍然有可能陷入局部最优解,尤其是在处理非常复杂的非凸优化问题时。
结合了 Adagrad 和 RMSProp 的优点,通过计算梯度的一阶矩估计(均值)和二阶矩估计(方差),自适应地调整每个参数的学习率。

优势
收敛速度快:Adam 结合了梯度的一阶矩和二阶矩信息,自适应地调整学习率,收敛更快。
鲁棒性强:
对不同类型的问题和数据集都具有较好的适应性,无需过多地调整超参数就能取得不错的效果。
其自适应的学习率调整机制使得它在处理非平稳目标函数和具有噪声的梯度时表现出色,能够在各种复杂的情况下稳定地更新参数。
计算效率高:
Adam 的计算过程相对简单,主要涉及梯度计算、一阶矩和二阶矩估计的更新以及参数更新,这些计算操作在现代深度学习框架中都能高效实现。
同时,由于其快速收敛的特性,在达到相同的训练效果时,往往比其他算法需要更少的迭代次数,进一步提高了计算效率。
劣势
可能陷入局部最优解:
尽管 Adam 在收敛速度和鲁棒性方面表现优秀,但像大多数基于梯度的优化算法一样,它仍然有可能陷入局部最优解,特别是在处理非常复杂的非凸优化问题时。
对超参数的依赖:
虽然 Adam 对超参数的敏感性相对较低,但学习率α、一阶矩估计衰减系数β1和二阶矩估计衰减系数β2等超参数的选择仍然会影响模型的训练效果。
一种在梯度下降优化算法基础上引入的技术,用于加速模型收敛并减少振荡,尤其在处理复杂的损失函数地形时表现出色。
原理
Momentum 的核心思想源于物理学中的动量概念。在梯度下降过程中,每次参数更新不仅考虑当前梯度,还结合之前更新的 “方向惯性”。
想象一个小球在损失函数的 “地形” 上滚动,它不仅受当前坡度(梯度)影响,还保留了之前滚动方向的部分动量。
这有助于小球(参数更新)更直接地朝着最优解滚动,而不会在局部起伏中频繁改变方向。
参数更新公式:
设θ_t为第t次迭代时的参数向量,g_t = ∇J(θ_t)为第t次迭代的梯度,α为学习率,β(通常取值在0.9左右)为动量因子。引入一个速度变量v_t,它表示参数更新的方向和速度。
速度更新公式为v_t =β * v_(t−1) + α * g_t
参数更新公式为θ_(t+1) =θ_t − v_t
从公式可以看出,v_t不仅包含当前梯度g_t,还通过β * v_(t−1)保留了上一次速度的信息。
β的作用类似于摩擦力,它使得速度在每次迭代中逐渐衰减,但同时也保留了部分之前的更新方向,帮助参数更新在稳定方向上加速。
算法步骤
优势
加速收敛:
在损失函数的梯度方向较为一致的情况下,Momentum 能够加速参数更新。
因为它在更新过程中积累了之前的梯度信息,使得参数更新方向更稳定,从而更快地朝着最优解移动。
例如,在一个具有长而窄的 “山谷” 形状的损失函数中,传统梯度下降可能会在山谷两侧来回振荡,而 Momentum 可以借助之前的更新方向,沿着山谷快速下降,大大缩短收敛时间。
减少振荡:
在梯度方向频繁变化的区域,Momentum 可以平滑梯度的影响,减少参数更新的振荡。
由于速度变量综合了多个梯度信息,不会因单次梯度的剧烈变化而产生大幅度的方向改变,有助于模型更稳定地收敛。
劣势
超参数依赖:Momentum 对学习率α和动量因子β较为敏感。
可能错过最优解:
虽然 Momentum 有助于加速收敛,但在某些情况下,由于它具有一定的惯性,可能会在接近最优解时 “冲过” 最优解,导致无法准确收敛到全局最优解。
尤其是在最优解附近梯度变化较小的情况下,Momentum 可能无法及时调整更新方向,从而错过最优解。
在机器学习任务中,大部分监督学习算法都会有一个目标函数,算法对该目标函数进行优化,称为优化算法的过程。
例如在分类或者回归任务中,使用损失函数( Loss Function )作为其目标函数对算法模型进行优化 。
不同的损失函数在梯度下降过程中的收敛速度和性能都是不同的。
均方误差作为损失函数收敛速度慢,可能会陷入局部最优解;
而交叉熵作为损失函数的收敛速度比均方误差快,且较为容易找到函数最优解。
import numpy as np
def mean_squared_error(y_true, y_pred):
return np.mean(np.square(y_pred - y_true), axis=-1)
def mean_absolute_error(y_true, y_pred):
return np.mean(np.abs(y_pred - y_true), axis=-1)
def mean_squared_logarithmic_error(y_true, y_pred):
first_log = np.log(np.clip(y_pred, 10e-6, None) + 1.)
second_log = np.log(np.clip(y_true, 10e-6, None) + 1.)
return np.mean(np.square(first_log - second_log), axis=-1)
def mean_absolute_percentage_error(y_true, y_pred):
diff = np.abs((y_pred - y_true) / np.clip(np.abs(y_true), 10e-6, None))
return 100 * np.mean(diff, axis=-1)
均方误差损失函数 MSE 是使用最广泛的,且在大部分情况下有不错的性能。
平均绝对误差损失函数 MAE 会比较有效地惩罚异常值,如果数据异常值较多,考虑使用其作为损失函数。
一般情况下,为了不让数据出现太多异常值,可以对数据进行预处理操作。
均方误差对数损失与均方误差的计算过程类似,多了对每个输出数据进行对数计算,目的是缩小函数输出的范围值。
平均绝对百分比误差损失则计算预测值与真实值的相对误差。均方误差对数损失与平均绝对百分比误差损失实际上是用来处理大范围数据的
但是在神经网络中,我们常把输入数据归一化,然后再使用均方误差或者平均绝对误差损失来计算损失。
Logistic损失函数和负对数似然损失函数只能处理二分类问题,对于多分类,使用交叉熵损失函数(Cross Entropy Loss),其定义如下:

def cross_entropy(y_true, y_pred):
return -np.mean(y_true * np.log(y_pred + 10e-6))

运用 Hinge 损失的典型分类器是 SVM 算法,因为 Hinge 损失可以用来解决间隔最大化问题。
当分类模型需要硬分类结果的,例如分类结果是 0 或 1 、 -1或 1 的二分类数据, Hinge 损失是最方便的选择 。
Hinge 损失函数定义如下:

使用指数(Exponential)损失函数的典型分类器是 AdaBoost 算法,指数损失函数的定义如下:

def exponential(y_true, y_pred):
return np.sum(np.exp(-y_true * y_pred))
引用资料
https://blog.csdn.net/jiaoyangwm/article/details/80011656
局部连接和权值共享

在传统的全连接神经网络中,每一层的每个神经元都与下一层的所有神经元相连。而在 CNN 中,局部连接意味着卷积层的神经元仅与输入数据的一个局部区域相连。
一般认为图像的空间联系是局部的像素联系比较密切,而距离较远的像素相关性较弱,因此,每个神经元没必要对全局图像进行感知,只要对局部进行感知,然后在更高层将局部的信息综合起来得到全局信息。
每个神经元只与上一层的部分神经元相连,只感知局部,而不是整幅图像。
相比于全连接网络,局部连接大大减少了参数的数量。
卷积核共享有个问题:提取特征不充分,可以通过增加多个卷积核来弥补,可以学习多种特征。
如果我们使用10x10的卷积核,我们虽然需要计算多次,但我们需要的参数只有10x10=100个,加上一个偏向b,一共只需要101个参数。我们取得图像大小还是100x100。


参数计算
在局部连接且有权值共享的情况下,参数计算:

没有权值共享时:

卷积是一种有效提取图片特征的方法。
一般用一个正方形卷积核,遍历图片上的每一个像素点。
图片与卷积核重合区域内相对应的每一个像素值,乘卷积核内相对应点的权重,然后求和, 再加上偏置后,最后得到输出图片中的一个像素值。
图片分灰度图和彩色图,卷积核可以是单个也可以是多个。
因此卷积操作分以下三种情况:

卷积核通道数等于输入图片的通道数,每个通道都会随机生成 9 个待优化的参数,一共有 27 个待优化参数 w 和一个偏置 b。

注:这里还是单个卷积核的情况,但是一个卷积核可以有多个通道。默认情况下,卷积核的通道数等于输入图片的通道数。
以3通道输入,2个卷积核为例:

图中输入X:[1,h,w,3]指的是:输入1张高h宽w的3通道图片。
卷积核W:[k,k,3,2]指的是:卷积核尺寸为3*3,通道数为3,个数为2。
卷积操作后,输出的通道数=卷积核的个数
卷积核的个数和卷积核的通道数是不同的概念。每层卷积核的个数在设计网络时会给出,但是卷积核的通道数不一定会给出。默认情况下,卷积核的通道数=输入的通道数。
偏置数=卷积核数
填充
卷积操作中,经常会使用padding对输入进行填充操作。默认在图片周围填充0。
卷积层通过局部连接和权值共享的机制,显著减少连接的个数(需要训练的参数数量),但是每一个特征映射的神经元个数并没有显著减少。
例如,输入28×28,用5×5卷积核、步长1、无填充时,输出为24×24的特征图,神经元数量为24×24=576个。
池化层旨在通过降低特征面的分辨率来获得具有空间不变性的特征 。池化层起到二次提取特征的作用,它的每个神经元对局部接受域进行池化操作。
池化层同样基于局部相关性的思想,通过从局部相关的一组元素中进行采样或信息聚合,从而得到新的元素值。通常我们用到两种池化进行下采样:
(1)最大池化(Max Pooling),从局部相关元素集中选取最大的一个元素值。
(2)平均池化(Average Pooling),从局部相关元素集中计算平均值并返回。

权重W个数 + 偏置b的个数
LeNet - 5的典型结构包含7层(不包括输入层),由卷积层、池化层和全连接层组成,其结构如下表所示: |层类型|名称|输入大小|输出大小|说明| | ---- | ---- | ---- | ---- | ---- | |输入层|Input|$32\times32$|$32\times32$|原始图像大小| |卷积层|C1|$32\times32\times1$|$28\times28\times6$|6个$5\times5$卷积核,步长1,填充0| |池化层|S2|$28\times28\times6$|$14\times14\times6$|$2\times2$平均池化,步长2| |卷积层|C3|$14\times14\times6$|$10\times10\times16$|16个$5\times5$卷积核,步长1,填充0| |池化层|S4|$10\times10\times16$|$5\times5\times16$|$2\times2$平均池化,步长2| |全连接层|F5|$5\times5\times16$|$120\times1$|连接上一层所有神经元| |全连接层|F6|$120\times1$|$84\times1$| - | |全连接层|Output|$84\times1$|$10\times1$|10个输出对应10个数字类别|
例如,某个卷积核可能对水平方向的边缘敏感,另一个对垂直方向的边缘敏感。卷积层 C1 总参数量:
每个卷积核有5×5×1=25个参数(因为输入是单通道),共6个卷积核,所以卷积核参数总数为6×25=150。
每个卷积核输出一个特征图,会对应一个偏置参数,共6个偏置参数。150+6=156。
池化层 S2:0
...
全连接层 F5:
输入:S4 层输出的16个特征图,每个特征图大小为5×5,将其展平后得到16×5×5=400个神经元。
输出:该层有120个神经元。
参数数量为400×120=48000。
每个输出神经元有一个偏置参数,共120个偏置参数。
F5 层总参数量:48000+120=48120。
输入数据的大小 I,卷积和池化核大小 K,相应公式如下:
输出高度和宽度:O = (I + 2P - K) / S + 1
这里O表示输出数据大小,P为填充值,S为步长
输出通道数:仍为卷积核的输出通道数C_out,所以卷积层输出形状为O×O×C_out
例如,输入图像边长I = 224,卷积核边长K = 3,步长S = 1,填充P = 1,输出通道数C_out=64,则输出边长O=(224 + 2×1 - 3)/1 +1 = 224,输出形状为224×224×64
输出高度和宽度:同卷积层
输出通道数:保持与输入通道数C_in一致,所以池化层输出形状为O_O_C_in
例如,输入特征图边长I = 56,池化核边长K = 2,步长S = 2,无填充(P = 0),则输出边长O=(56 + 2×0 - 2)/2 +1 = 28,输出形状为28×28×C_in
全连接层展平操作不受输入数据高宽相等这一条件影响
假设上一层输出形状为I×I×C_in,展平后的向量长度为L = I×I×C_in
如果全连接层有N个神经元,输出大小就是N
例如,上一层输出边长I = 7,通道数C_in=512,展平后长度为7×7×512 = 25088,若全连接层有4096个神经元,则输出大小为4096
参数总量计算
卷积层:K * K * C_in * 卷积核个数 + 卷积核个数
卷积参数 = 卷积核长度x卷积核宽度x输入通道数x输出通道数+输出通道数(偏置)
卷积计算量 = 输出数据大小x卷积核长度x卷积核宽度x输入通道数
池化层:0
全连接层:输入神经元个数 * 输出神经元个数 + 输出神经元个数
为什么一层有多个卷积核
一个卷积核可能对水平边缘敏感,另一个卷积核可能更擅长检测垂直方向的边缘。通过使用多个卷积核,网络可以同时提取多种不同的局部特征
注意:在卷积神经网络中,尽管输入通道数为 6 且有 16 个卷积核,但输出通道数并不是简单的6×16,而是 16
每个卷积核实际上是三维的,其通道数与输入数据的通道数相同
以当前输入14×14×6为例,每个卷积核大小假设为K×K×6
在进行卷积运算时,对于每个卷积核,它会同时对输入数据的 6 个通道进行二维卷积操作
然后将这 6 个二维卷积的结果相加,得到一个二维特征图(无通道维度)
这就意味着,一个卷积核无论输入通道数是多少,最终只会产生一个二维特征图(不考虑通道)
AlexNet包含8层,其中有5层卷积层和3层全连接层,具体结构如下表所示: |层类型|名称|输入大小|输出大小|卷积核/池化核大小|步长|填充|说明| | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | |输入层|Input|$227×227×3$|$227×227×3$| - | - | - |彩色图像输入| |卷积层|Conv1|$227×227×3$|$55×55×96$|$11×11$|4|0|96个卷积核,ReLU激活| |池化层|Pool1|$55×55×96$|$27×27×96$|$3×3$|2|0|最大池化| |卷积层|Conv2|$27×27×96$|$27×27×256$|$5×5$|1|2|256个卷积核,ReLU激活| |池化层|Pool2|$27×27×256$|$13×13×256$|$3×3$|2|0|最大池化| |卷积层|Conv3|$13×13×256$|$13×13×384$|$3×3$|1|1|384个卷积核,ReLU激活| |卷积层|Conv4|$13×13×384$|$13×13×384$|$3×3$|1|1|384个卷积核,ReLU激活| |卷积层|Conv5|$13×13×384$|$13×13×256$|$3×3$|1|1|256个卷积核,ReLU激活| |池化层|Pool5|$13×13×256$|$6×6×256$|$3×3$|2|0|最大池化| |全连接层|FC6|$6×6×256$(展平为9216)|$4096×1$| - | - | - |全连接,ReLU激活| |全连接层|FC7|$4096×1$|$4096×1$| - | - | - |全连接,ReLU激活| |全连接层|FC8|$4096×1$|$1000×1$| - | - | - |全连接,Softmax激活用于1000类分类|
第一层卷积层使用了11 x 11 的卷积核,卷积核过大导致效果较差





Inception V1 参数少但效果好的原因除了模型层数更深、表达能力更强外,还有两点:
全连接层几乎占据了 AlexNet 或 VGGNet 中 90% 的参数量,而且会引起过拟合,去除全连接层后模型训练更快并且减轻了过拟合。
这一部分也借鉴了 Network In Network 的思想,形象的解释就是 Inception Module 本身如同大网络中的一个小网络,其结构可以反复堆叠在一起形成大网络。
为什么1 x 1卷积能降维
卷积以后通道数变为卷积核个数的数量
为什么卷积分解减少参数
从感受野的角度看,两个连续的3×3卷积核组合起来的感受野与一个7×7卷积核的感受野相同。

假设现有一个比较浅的网络已达到了饱和的准确率,这时在它后面再加上几个恒等映射层(即 y=x,输出等于输入),这样就增加了网络的深度,并且误差不会增加,也即更深的网络不应该带来训练集上误差的上升。
ResNet 引入了残差网络结构(residual network)

借鉴了高速网络的跨层连接的思想,但是对其进行了改进,残差项原本是带权值的,但 ResNet 用恒等映射代替了它。
当已经学习到较为饱和的准确率,或者发现下层的误差变大时,接下来的目标就转化为恒等映射的学习,也就是使得输入x近似于输出 H(x),以保持在后面的层次中不会造成精度下降。
上图的残差网络中,通过捷径连接的方式直接将输入x传到输出作为初始结果,输出结果为 H(x)=F(x)+x,当 F(x)=0 时,H(x)=x,也就是恒等映射。
于是,ResNet 相当于将学习目标改变了,不再是学习一个完整的输出,而是目标值H(X)和x的差值,也就是所谓的残差 F(x) := H(x)-x,因此,后面的训练目标就是要将残差结果逼近于 0,使到随着网络加深,准确率不下降。
学习的目标:目标值 H(x) 和输入 x 的差值,即 F(x):=H(x)-x,将残差逼近于 0,使得随着网络加深,准确率不下降。
残差网络

残差块(Residual Block):
一个标准的残差块包含两条路径:
卷积层负责提取特征,BN 用于对数据进行归一化处理,加速训练并减少梯度消失或爆炸的风险,ReLU 则增加模型的非线性表达能力。
这种连接方式使得网络可以直接学习残差函数,而不是学习完整的输入 - 输出映射,极大地简化了学习过程。
网络整体架构:
ResNet 由多个残差块堆叠而成,在网络的开头和结尾通常还包含一些常规的卷积层、池化层和全连接层。
例如,在常见的 ResNet - 50 模型中,包含了一个 7x7 的卷积层用于初始特征提取,接着是多个残差块组成的不同阶段,每个阶段包含多个残差块,最后通过全局平均池化层和全连接层得到分类结果。不同深度的 ResNet 模型(如 ResNet - 18、ResNet - 34、ResNet - 101 等)主要区别在于残差块的数量不同。
| 层名 | 输出尺寸 | 卷积核大小 | 步长 | 填充 | 输出通道数 | 残差块数量 |
|---|---|---|---|---|---|---|
| 卷积层1 | (112×112×64) | (7×7) | 2 | 3 | 64 | - |
| 最大池化层 | (56×56×64) | (3×3) | 2 | 1 | - | - |
| 残差块组1 | (56×56×64) | - | - | - | 64 | 2 |
| 残差块组2 | (28×28×128) | - | - | - | 128 | 2 |
| 残差块组3 | (14×14×256) | - | - | - | 256 | 2 |
| 残差块组4 | (7×7×512) | - | - | - | 512 | 2 |
| 全局平均池化层 | (1×1×512) | - | - | - | - | - |
| 全连接层 | (1×1×1000) | - | - | - | - | - |
注:
| 层名 | 输出尺寸 | 卷积核大小 | 步长 | 填充 | 输出通道数 | 残差块数量 |
|---|---|---|---|---|---|---|
| 卷积层1 | (112×112×64) | (7×7) | 2 | 3 | 64 | - |
| 最大池化层 | (56×56×64) | (3×3) | 2 | 1 | - | - |
| 残差块组1 | (56×56×256) | - | - | - | 256 | 3 |
| 残差块组2 | (28×28×512) | - | - | - | 512 | 4 |
| 残差块组3 | (14×14×1024) | - | - | - | 1024 | 6 |
| 残差块组4 | (7×7×2048) | - | - | - | 2048 | 3 |
| 全局平均池化层 | (1×1×2048) | - | - | - | - | - |
| 全连接层 | (1×1×1000) | - | - | - | - | - |
注:
残差学习:
传统神经网络学习的是y=F(x)。
而在 ResNet 中,网络学习的是残差函数F(x)=H(x)−x,其中H(x)是期望学习的映射,x是输入。
通过捷径连接,模型实际学习的是y=F(x)+x。
这种设计的好处在于,如果学习的残差函数F(x)为0,那么输出y就等于输入x,此时网络相当于恒等映射。
相比于直接学习复杂的映射H(x),学习残差函数F(x)要容易得多,尤其是当网络深度增加时,这种方式可以有效避免梯度消失问题,使得网络能够更容易收敛。
梯度传播:
在反向传播过程中,残差结构有助于梯度的顺利传播。
由于捷径连接的存在,梯度可以直接通过捷径连接回传到前面的层,而不会因为网络深度的增加而衰减过多。
优势
解决深度网络的退化问题:
通过引入残差结构,ResNet 成功解决了随着网络深度增加而导致的退化问题,使得模型可以在更深的层次上学习到更丰富的特征。
易于训练:
残差学习的方式使得网络更容易优化,即使网络深度非常深,也能够通过反向传播有效地更新参数。
泛化能力强:
ResNet 的结构设计使得模型具有较好的泛化能力,不仅在图像分类任务中表现出色,在其他计算机视觉任务如目标检测、语义分割等,以及一些非视觉领域的任务中也被广泛应用,并取得了良好的效果。

引用资料
https://my.oschina.net/u/876354/blog/1621839
RNN在处理长期依赖(时间序列上距离较远的节点)时会遇到巨大的困难,因为计算距离较远的节点之间的联系时会涉及雅可比矩阵的多次相乘,会造成梯度消失或者梯度膨胀的现象。
解决长期依赖问题
RNN
LSTM


LSTM 有通称作为“门”的结构来去除或者增加信息到细胞状态的能力。
门是一种让信息选择式通过的方法。他们包含一个 sigmoid 神经网络层和一个 pointwise 乘法操作。

LSTM 拥有三个门,分别是遗忘门,输入门和输出门,来保护和控制细胞状态。
读取ht−1和xt,通过 sigmoid 层输出一个在 0 到 1 之间的数值给每个在细胞状态Ct−1中的数字。1 表示“完全保留”,0 表示“完全舍弃”。

sigmoid 层称 “输入门” 决定什么值我们将要更新。
tanh 层创建一个新的候选值向量C~t加入到状态中。

将ct−1更新为ct:将旧状态与ft相乘,丢弃掉我们确定需要丢弃的信息。接着加上it∗C~t得到新的候选值,根据我们决定更新每个状态的程度进行变化。

通过sigmoid 层来确定细胞状态的哪个部分将输出。
把细胞状态通过 tanh 进行处理,并将它和 sigmoid 门的输出相乘,最终我们仅仅会输出我们确定输出的那部分。


门控循环单元(Gated Recurrent Unit,GRU)是一种循环神经网络(RNN)的变体,它是长短期记忆网络(LSTM)的简化版本,在保持处理序列长期依赖能力的同时,具有更简单的结构和更少的参数,计算效率更高。
例如,假设输入维度为 d_{in},隐藏维度为 d_{hidden},LSTM大约有 4 * d_{in} * d_{hidden} + 4 * d_{hidden}^2 个参数,GRU大约有 3 * d_{in} * d_{hidden} + 3 * d_{hidden}^2 个参数。 2. 计算效率:由于结构更简单,GRU在训练和推理时的计算量更小,计算速度更快。尤其在处理大规模数据或长序列数据时,这种计算效率的提升更为明显。 3. 性能表现:在许多实际应用中,GRU和LSTM的性能表现相近。例如在简单的时间序列预测任务或短文本分类任务中,GRU凭借其高效性可能更具优势;而在一些对长期依赖关系要求极高、数据序列非常复杂的任务中,LSTM可能会表现得更好,但差距并不总是十分显著。

见Paddle实践

在反向传播计算梯度时,梯度值变得非常大,以指数级增长,最终导致模型无法收敛,具体表现为模型参数更新幅度过大,权重变得极不稳定,甚至使得训练过程中断。
出现梯度爆炸的情况:
随着神经网络层数的增加,信号在网络中传递时会经过多次运算。
在反向传播中,梯度通过链式法则进行计算,每经过一层,梯度就会与该层的权重矩阵相乘。
如果网络层数过多,并且权重矩阵设置不合理(例如权重值普遍较大),在多次相乘后,梯度值可能会迅速增大,引发梯度爆炸。
例如,在一些深度超过几十层甚至上百层的神经网络中,如果没有适当的机制来控制梯度,梯度爆炸的风险就会显著增加。
某些激活函数的导数在特定区间内具有较大的值。例如,Sigmoid 函数的导数σ′(x)=σ(x)(1−σ(x)),其最大值为0.25,
但在某些情况下,如果网络中大量神经元都处于 Sigmoid 函数导数较大的区域,
在反向传播时,梯度经过这些层不断放大,也可能导致梯度爆炸。
相比之下,ReLU 函数在正数部分导数恒为1,相对不容易引发梯度爆炸,但如果网络结构设计不合理,依然可能出现问题。
如果权重初始值设置得过大,在正向传播时,经过多层的线性变换后,输出值会变得非常大。
当进行反向传播计算梯度时,这些较大的输出值会导致梯度也变得很大。
例如,在全连接层中,如果权重初始值普遍在10以上,且没有合适的初始化策略或正则化方法,梯度爆炸很可能在训练初期就出现。
学习率决定了模型参数在每次更新时的步长。如果学习率设置得过大,在根据梯度更新参数时,参数更新的幅度就会过大。
这可能使得模型在参数空间中 “跳跃” 过度,导致梯度不断增大,进而引发梯度爆炸。
例如,在使用随机梯度下降(SGD)算法时,如果初始学习率设置为0.1甚至更大,而没有采用学习率衰减策略,在训练过程中很容易出现梯度爆炸问题。
梯度消失与梯度爆炸相反。随着反向传播的进行,梯度在经过多层传递后逐渐趋近于 0,导致网络参数无法得到有效更新,模型难以收敛。
直观表现为训练过程中,损失函数长时间不下降或下降极为缓慢,模型的学习能力停滞,即使训练很长时间,性能也没有明显提升。
产生原因:
其导数σ′(x)=σ(x)(1−σ(x))取值范围在(0,0.25]。
在反向传播中,梯度计算需要乘以激活函数的导数。当神经元的输入值处于 Sigmoid 函数饱和区(即x绝对值较大的区域)时,导数接近 0。
例如,当x=4时,σ′(4)≈0.018。如果网络中多层神经元都处于这种状态,经过多次链式求导后,梯度会迅速趋近于 0。
其导数tanh′(x)=1−tanh^2(x)取值范围在(0,1]。
。类似 Sigmoid 函数,在输入值较大或较小时,导数接近 0,在深层网络中容易导致梯度消失。
神经网络是一个多层的链式结构,在反向传播计算梯度时,根据链式法则,梯度从输出层向输入层传递过程中,要经过多次乘法运算。
每一层的梯度计算都依赖上一层的梯度和当前层的权重与激活函数导数。例如,对于一个L层神经网络,假设第l层到第L层的权重矩阵为W_l,W_l+1 ,⋯,W_L
激活函数导数为δ_l ,δ_l+1 ,⋯,δ_L ,那么第l层的梯度∂Loss/∂W_l的计算涉及到∏_i=l_L W_i * ∏_i=l_L δ_i
如果权重矩阵的某些元素绝对值小于 1,或者激活函数导数较小,随着层数L的增加,这些小于 1 的值不断相乘,梯度就会以指数级减小,最终趋近于 0。
若权重初始值设置过小,在反向传播过程中,梯度在经过与权重矩阵相乘后,会变得更小。
例如,在全连接层中,如果权重初始值普遍在0.01以下,那么每次梯度更新时,参数的改变量就会非常小,导致梯度在传递过程中逐渐消失。
解决方法
如 ReLU函数,其表达式为f(x)=max(0,x),在x>0时,导数f′(x)=1
避免了因激活函数导数导致的梯度消失问题。
此外,还有 Leaky ReLU、PReLU 等改进的 ReLU 函数,在一定程度上缓解了 ReLU 函数在x<0时梯度为 0 的情况。
在反向传播计算完梯度后,对梯度进行检查和裁剪。
如果梯度的某个维度或整体范数超过设定的阈值,就按比例缩放梯度,使其保持在合理范围内,防止梯度消失(同时也能预防梯度爆炸)。
采用合适的权重初始化方法,如 Xavier 初始化、Kaiming 初始化等。
Xavier 初始化根据输入和输出神经元的数量来初始化权重,使得权重在正向和反向传播时,信号的方差保持一致,减少梯度消失的可能性。
Kaiming 初始化则针对 ReLU 激活函数进行了优化,能更好地初始化权重,提高网络的收敛速度。
在深层网络中,通过添加残差连接,允许梯度直接跳过某些层进行传播,减少了梯度在多层传递过程中的衰减。
例如 ResNet 网络通过引入残差块,成功训练了超过 100 层的深度神经网络,有效解决了梯度消失问题,提升了模型性能。
批归一化(Batch Normalization,BN)和层归一化(Layer Normalization,LN)

import torch
from torch import nn # 导入nn模块
class Sample(nn.Module):
def __init__(self):
super(Sample, self).__init__()
self.conv = nn.Sequential(
nn.Conv2d(1, 1, 5), # 卷积层
nn.Sigmoid(), # 激活函数
nn.MaxPool2d(2, 2), # 最大池化层
)
self.fc = nn.Sequential(
nn.Linear(14*14, 10), # 全连接层
nn.Sigmoid(), # 激活函数
)
def forward(self, img): # 定义前向计算
feature = self.conv(img) # 卷积层
output = self.fc(feature.view(img.shape[0], -1)) # 全连接层
return output

import torch
from torch import nn
class LeNet(nn.Module):
def __init__(self):
super(LeNet, self).__init__()
'''
这里搭建卷积层,需要按顺序定义卷积层、
激活函数、最大池化层、卷积层、激活函数、最大池化层,
具体形状见测试说明
'''
self.conv = nn.Sequential( # 输入(32*32*1)
########## Begin ##########
nn.Conv2d(1,6,5), # (batch_size, 6, 28, 28)
nn.Sigmoid(),
nn.MaxPool2d(2,2), # (batch_size, 6, 14, 14)
nn.Conv2d(6,16,5), # (batch_size, 6, 10, 10)
nn.Sigmoid(),
nn.MaxPool2d(2,2), # (batch_size, 16, 5, 5)
########## End ##########
)
'''
这里搭建全连接层,需要按顺序定义全连接层、
激活函数、全连接层、激活函数、全连接层,
具体形状见测试说明
'''
self.fc = nn.Sequential(
########## Begin ##########
nn.Linear(16*5*5,120), # 输入大小为16*5*5=400
nn.Sigmoid(),
nn.Linear(120,84),
nn.Sigmoid(),
nn.Linear(84,10),
########## End ##########
)
def forward(self, img):
'''
这里需要定义前向计算
'''
########## Begin ##########
feature=self.conv(img)
output=self.fc(feature.view(img.shape[0],-1))
# img.shape[0] 是 batch_size
# -1 表示自动计算该维度的大小,使得总元素数量保持不变。 例如 (batch_size, 16, 5, 5) --> (batch_size, 16*5*5)
# view(img.shape[0], -1) 会将其变形为 (batch_size, 16*5*5)
# img.shape[0] 改为 feature.shape[0] 可能有一定风险:卷积操作不会改变 batch size,但在某些特殊情况下(如自定义操作或错误的数据流),两者可能不一致。
return output
########## End ##########

import torch
from torch import nn
class AlexNet(nn.Module):
def __init__(self):
super(AlexNet, self).__init__()
'''
这里搭建卷积层,需要按顺序定义卷积层、
激活函数、最大池化层、卷积层、激活函数、
最大池化层、卷积层、激活函数、卷积层、
激活函数、卷积层、激活函数、最大池化层,
具体形状见测试说明
'''
self.conv = nn.Sequential(
########## Begin ##########
nn.Conv2d(3,96,kernel_size=(11,11),stride=(4,4)),
nn.ReLU(),
nn.MaxPool2d(kernel_size=3,stride=2,padding=0),
nn.Conv2d(96,256,kernel_size=(5,5),stride=(1,1),padding=(2,2)),
nn.ReLU(),
nn.MaxPool2d(kernel_size=3,stride=2,padding=0),
nn.Conv2d(256,384,kernel_size=(3,3),stride=(1,1),padding=(1,1)),
nn.ReLU(),
nn.Conv2d(384,384,kernel_size=(3,3),stride=(1,1),padding=(1,1)),
nn.ReLU(),
nn.Conv2d(384,256,kernel_size=(3,3),stride=(1,1),padding=(1,1)),
nn.ReLU(),
nn.MaxPool2d(kernel_size=3,stride=2,padding=0),
########## End ##########
)
'''
这里搭建全连接层,需要按顺序定义
全连接层、激活函数、丢弃法、
全连接层、激活函数、丢弃法、全连接层,
具体形状见测试说明
'''
self.fc = nn.Sequential(
########## Begin ##########
nn.Linear(in_features=6400,out_features=4096,bias=True),
nn.ReLU(),
nn.Dropout(p=0.5),
nn.Linear(in_features=4096,out_features=4096,bias=True),
nn.ReLU(),
nn.Dropout(p=0.5),
nn.Linear(in_features=4096,out_features=1000,bias=True),
########## End ##########
)
def forward(self, img):
'''
这里需要定义前向计算
'''
########## Begin ##########
feature=self.conv(img)
result=self.fc(feature.view(img.shape[0],-1))
########## End ##########
model = AlexNet()
print(model)
import matplotlib.pyplot as plt
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
# 设置种子和其他配置。
seed = 42
torch.manual_seed(seed)
torch.backends.cudnn.benchmark = False
torch.backends.cudnn.deterministic = True
# 设置批大小、学习周期和学习率。
batch_size = 512
epochs = 30
learning_rate = 1e-3
# 载入 MNIST 数据集中的图片进行训练。
transform = torchvision.transforms.Compose([torchvision.transforms.ToTensor()])
train_dataset = torchvision.datasets.MNIST(
root="~/torch_datasets", train=True, transform=transform, download=True
)
train_loader = torch.utils.data.DataLoader(
train_dataset, batch_size=batch_size, shuffle=True
)
# AE 自编码器
# 为了简化实现,我们在一个类中编写编码器和解码器层。我们为编码器和解码器层的组件都定义了全连接层。
class AE(nn.Module):
def __init__(self, **kwargs):
super().__init__()
self.encoder_hidden_layer = nn.Linear(
in_features=kwargs["input_shape"], out_features=128
)
self.encoder_output_layer = nn.Linear(
in_features=128, out_features=128
)
self.decoder_hidden_layer = nn.Linear(
in_features=128, out_features=128
)
self.decoder_output_layer = nn.Linear(
in_features=128, out_features=kwargs["input_shape"]
)
def forward(self, features):
activation = self.encoder_hidden_layer(features)
activation = torch.relu(activation)
code = self.encoder_output_layer(activation)
code = torch.sigmoid(code)
activation = self.decoder_hidden_layer(code)
activation = torch.relu(activation)
activation = self.decoder_output_layer(activation)
reconstructed = torch.sigmoid(activation)
return reconstructed
# 在使用我们定义的 AE 类之前,我们有以下事情要做:
# 配置要在哪个设备上运行;
# 实例化一个 AE 对象;
# 要定义优化器;
# 要定义重建损失。
device = torch.device("cpu")
# 建立 AE 模型并载入到 CPU 设备
model = AE(input_shape=784).to(device)
# Adam 优化器,学习率 10e-3
optimizer = optim.Adam(model.parameters(), lr=learning_rate)
# MSE 损失
criterion = nn.MSELoss()
# 我们在 CPU 设备上运行,实例化一个输入大小为 784 的 AE 自编码器,并用 Adam 作为我们的训练优化器。用 MSELoss 作为损失函数。接下来我们就可以训练了。
for epoch in range(epochs):
loss = 0
for batch_features, _ in train_loader:
# 将小批数据变形为 [N, 784] 矩阵,并加载到 CPU 设备
batch_features = batch_features.view(-1, 784).to(device)
# 梯度设置为 0,因为 torch 会累加梯度
optimizer.zero_grad()
# 计算重构
outputs = model(batch_features)
# 计算训练重建损失
train_loss = criterion(outputs, batch_features)
# 计算累积梯度
train_loss.backward()
# 根据当前梯度更新参数
optimizer.step()
# 将小批量训练损失加到周期损失中
loss += train_loss.item()
# 计算每个周期的训练损失
loss = loss / len(train_loader)
# 显示每个周期的训练损失
print("epoch : {}/{}, recon loss = {:.8f}".format(epoch + 1, epochs, loss))
# 我们用训练过的自编码器提取一些测试用例来重构。
test_dataset = torchvision.datasets.MNIST(
root="~/torch_datasets", train=False, transform=transform, download=True
)
test_loader = torch.utils.data.DataLoader(
test_dataset, batch_size=10, shuffle=False
)
test_examples = None
with torch.no_grad():
for batch_features in test_loader:
batch_features = batch_features[0]
test_examples = batch_features.view(-1, 784).to(device)
reconstruction = model(test_examples)
break
# 可视化
# 用我们训练过的自编码器重建一些测试图像。
with torch.no_grad():
number = 10
plt.figure(figsize=(20, 4))
for index in range(number):
# 显示原始图
ax = plt.subplot(2, number, index + 1)
plt.imshow(test_examples[index].cpu().numpy().reshape(28, 28))
plt.gray()
ax.get_xaxis().set_visible(False)
ax.get_yaxis().set_visible(False)
# 显示重构图
ax = plt.subplot(2, number, index + 1 + number)
plt.imshow(reconstruction[index].cpu().numpy().reshape(28, 28))
plt.gray()
ax.get_xaxis().set_visible(False)
ax.get_yaxis().set_visible(False)
plt.show()
# 可以看出,重构图片和原始图片差别不大,我们的训练还是比较成功的。
import torch
import torch.nn as nn
import torch.nn.functional as F
import matplotlib.pyplot as plt
class ConvAE(nn.Module):
def __init__(self, **kwargs):
super().__init__()
# 编码器 (Encoder):通过卷积层逐步压缩特征
self.encoder = nn.Sequential(
# 输入: (batch_size, 3, 64, 64) 假设输入为3通道64x64图像
nn.Conv2d(3, 16, kernel_size=3, stride=2, padding=1), # 输出: (16, 32, 32)
nn.ReLU(inplace=True),
nn.Conv2d(16, 32, kernel_size=3, stride=2, padding=1), # 输出: (32, 16, 16)
nn.ReLU(inplace=True),
nn.Conv2d(32, 64, kernel_size=3, stride=2, padding=1), # 输出: (64, 8, 8)
nn.ReLU(inplace=True),
nn.Conv2d(64, 128, kernel_size=3, stride=2, padding=1), # 输出: (128, 4, 4)
nn.ReLU(inplace=True)
)
# 解码器 (Decoder):通过转置卷积层恢复图像
self.decoder = nn.Sequential(
# 输入: (128, 4, 4)
nn.ConvTranspose2d(128, 64, kernel_size=3, stride=2, padding=1, output_padding=1), # 输出: (64, 8, 8)
nn.ReLU(inplace=True),
nn.ConvTranspose2d(64, 32, kernel_size=3, stride=2, padding=1, output_padding=1), # 输出: (32, 16, 16)
nn.ReLU(inplace=True),
nn.ConvTranspose2d(32, 16, kernel_size=3, stride=2, padding=1, output_padding=1), # 输出: (16, 32, 32)
nn.ReLU(inplace=True),
nn.ConvTranspose2d(16, 3, kernel_size=3, stride=2, padding=1, output_padding=1), # 输出: (3, 64, 64)
nn.Sigmoid() # 使用Sigmoid将输出归一化到[0,1]范围
)
def forward(self, x):
# 编码过程:得到潜在特征
x = self.encoder(x)
# 解码过程:重建输入图像
x = self.decoder(x)
return x
# 测试代码
if __name__ == "__main__":
# 实例化模型
model = ConvAE()
print(model)
# 创建随机测试数据 (batch_size=4, 3通道, 64x64图像)
test_input = torch.randn(4, 3, 64, 64)
# 前向传播
output = model(test_input)
# 打印输入输出形状(应保持一致)
print(f"输入形状: {test_input.shape}")
print(f"输出形状: {output.shape}")
import torch
from torch import nn
class ConvBNReLU(nn.Module):# 定义标准的卷积层:Conv+BN+ReLU
def __init__(self,nin,nout,ks,stride=1):
super().__init__()
pad=(ks-1)//2
self.layers=nn.Sequential(
nn.Conv1d(nin,nout,ks,stride,padding=pad),
# 输入:(batch_size, nin, sequence_length)
# 输出:(batch_size, nout, sequence_length)(当 stride=1 时长度不变)
nn.BatchNorm1d(nout), # 一维批标准化
nn.ReLU(),
)
def forward(self,x):
y=self.layers(x)
return y
class ConvUpBNReLU(nn.Module):# 上采样+卷积
def __init__(self,nin,nout,ks,stride=1):
super().__init__()
self.layers = nn.Sequential(
nn.Upsample(scale_factor=stride, mode="nearest"), # 上采样
ConvBNReLU(nin,nout,ks,stride)
)
def forward(self, x):
y=self.layers(x)
return y
# 生成器
class ImageGeneration(nn.Module):
def __init__(self,nclass=10):
super().__init__()
self.emb=nn.Embedding(nclass,64*4*4)
self.layers=nn.Sequential(
ConvBNReLU(64,64,1),
ConvUpBNReLU(64,32,2),
ConvBNReLU(32,32,1),
ConvUpBNReLU(32,16,2),
ConvBNReLU(16,16,1),
ConvUpBNReLU(16,8,2),
nn.Conv2d(8,3,1,1),
nn.Sigmoid() # 输出[0,1]
)
def forward(self,d):
h=self.emb(d)
h=h.reshape([-1,64,4,4])
x=self.layers(h)
return x
# 判别器
class ImageClassify(nn.Module):
def __init__(self):
super().__init__()
# DNN输出长度为2的表示向量
self.dnn=nn.Sequential(
ConvBNReLU(3,16,2),
ConvBNReLU(16,16,1),
ConvBNReLU(16,32,2),
ConvBNReLU(32,32,1),
ConvBNReLU(32,64,2),
ConvBNReLU(64,64,1),
nn.Flatten(),
nn.Linear(4*4*64,128),
)
self.classify=nn.Linear(128,10)
self.ganout=nn.Linear(128,1)
def forward(self,x):
h=self.dnn(x)
y1=self.classify(h)
y2=self.ganout(h)
return y1,y2
# 损失函数,交替迭代
# 定义生成器
gen=ImageGeneration()
gen.train()
# 定义判别器
dis=ImageClassify()
dis.train()
optim_gen=torch.optim.Adam(gen.parameters(),1e-3,weight_decay=0.0) # gen.parameters()获取需要训练的参数,无权重衰减
optim_dis=torch.optim.Adam(dis.parameters(),1e-3,weight_decay=0.0)
for epoch in range(10):
for x,d in dataloader: # x: 真实图像, d: 条件标签
# 训练判别器
dis.zero_grad()
# 噪声向量
z=torch.randn([len(x),100]),device=device,dtype=torch.float32
# 生成假图像
fake=gen(d,z)
# 生成图像输入到判别器
y1,y2=dis(fake.detach()) # 判别器对假图像的输出,阻止梯度传到生成器
# 真实图像输入到判别器
y3,y4=dis(x)
# 生成图像判别接近0
loss1=lossmse(y2,torch.zeros_like(y2)) # 判别器输出 y2 应接近 0
# 真实图像损失=类别损失+判别损失
loss2=lossce(y3,d)*0.2+lossmse(y4,torch.ones_like(y4)) # 判别器的分类输出 y3 应与真实标签 d 一致,判别器的真假输出 y4 应接近 1
# 优化
loss=loss1+loss2
loss.backward()
optim_dis.step()
optim_dis.zero_grad()
optim_gen.zero_grad()
# 训练生成器
gen.zero_grad()
z=torch.randn([len(x),100]),device=device,dtype=torch.float32
fake=gen(d,z)
y1,y2=dis(fake)
loss=lossce(y1,d)*0.2+lossmse(y2,torch.ones_like(y2))
loss.backward()
optim_gen.step()
optim_gen.zero_grad()
optim_dis.zero_grad()
# 解压数据集
import zipfile
zip_file = zipfile.ZipFile('/data/bigfiles/faces.zip')
zip_extract = zip_file.extractall('/data/faces')
batch_size = 72 # 批量大小
image_size = 64 # 训练图像空间大小
lr = 0.0002 # 学习率
beta1 = 0.5 # Adam优化器的beta1超参数
# 通过 paddle.vision.datasets 库读取数据:
import numpy as np
import paddle
import paddle.vision.datasets as ds
from paddle.vision.transforms import Compose, Resize, CenterCrop, ToTensor
def create_dataset_imagenet(dataset_path):
"""数据加载"""
transforms = Compose([Resize(image_size), CenterCrop(image_size), ToTensor()])
dataset = ds.DatasetFolder(root = dataset_path, transform=transforms)
return dataset
dataset = create_dataset_imagenet('/data/faces')
data_loader = paddle.io.DataLoader(dataset, batch_size=batch_size, shuffle=True)
# 可视化一部分卡通头像数据:
import matplotlib.pyplot as plt
def plot_data(data):
# 可视化部分训练数据
plt.figure(figsize=(10, 3), dpi=140)
for i, image in enumerate(data[:30], 1):
plt.subplot(3, 10, i)
plt.axis("off")
plt.imshow(image.transpose(1, 2, 0))
plt.show()
# 读取第一批数据
sample_data = iter(data_loader)
# 绘制第一批数据的部分图片
plot_data(next(sample_data)[0].numpy())
# 在加载好数据集以后,接着我们定义 DCGAN 模型,首先是生成器 Generator:
import paddle
import paddle.nn.functional as F
import paddle.nn as nn
##################################################################
#生成器定义
class Generator(nn.Layer):
def __init__(self, nz=100):
super().__init__()
#100*1*1
self.layer1 = nn.Sequential(
nn.Conv2DTranspose(nz, 1024, 4, 1, 0),
nn.BatchNorm2D(1024),
nn.ReLU()
)
#1024*4*4 (Paddle 默认的 NCHW 数据格式,N 为批量大小,C 为通道数,H 为高,W 为宽)
self.layer2 = nn.Sequential(
nn.Conv2DTranspose(1024, 512, 4, 2, 1),
nn.BatchNorm2D(512),
nn.ReLU()
)
#512*8*8
self.layer3 = nn.Sequential(
nn.Conv2DTranspose(512, 256, 4, 2, 1),
nn.BatchNorm2D(256),
nn.ReLU()
)
#256*16*16
self.layer4 = nn.Sequential(
nn.Conv2DTranspose(256, 128, 4, 2, 1),
nn.BatchNorm2D(128),
nn.ReLU()
)
#128*32*32
self.layer5 = nn.Sequential(
nn.Conv2DTranspose(128, 3, 4, 2, 1),
nn.Tanh()
)
self.seq = nn.Sequential(
self.layer1,
self.layer2,
self.layer3,
self.layer4,
self.layer5
)
def forward(self, x):
return self.seq(x)
# 接着是判别器 Discriminator:
#判别器定义
class Discriminator(nn.Layer):
def __init__(self):
super().__init__()
#3*64*64
self.layer1 = nn.Sequential(
nn.Conv2D(3, 64, 4, 2, 1),
nn.BatchNorm2D(64),
nn.LeakyReLU(0.2)
)
#64*32*32
self.layer2 = nn.Sequential(
nn.Conv2D(64, 128, 4, 2, 1),
nn.BatchNorm2D(128),
nn.LeakyReLU(0.2)
)
#128*16*16
self.layer3 = nn.Sequential(
nn.Conv2D(128, 256, 4, 2, 1),
nn.BatchNorm2D(256),
nn.LeakyReLU(0.2)
)
#256*8*8
self.layer4 = nn.Sequential(
nn.Conv2D(256, 512, 4, 2, 1),
nn.BatchNorm2D(512),
nn.LeakyReLU(0.2)
)
#512*4*4
self.layer5 = nn.Sequential(
nn.Conv2D(512, 1, 4, 1, 0),
nn.Sigmoid()
)
self.seq = nn.Sequential(
self.layer1,
self.layer2,
self.layer3,
self.layer4,
self.layer5
)
def forward(self, x):
out = self.seq(x)
return out
####################################################################
# 接着将生成器 Generator 与 Discriminator 组合起来构成 DCGAN:
import paddle.vision
from paddle.io import DataLoader
import paddle.optimizer
import paddle.vision.transforms as transforms
import matplotlib.pyplot as plt
noise_eval = paddle.randn((batch_size, 100, 1, 1)) # 随机噪声
class DCGAN:
def __init__(self):
self.generator = Generator() # 生成器
self.discriminator = Discriminator() # 判别器
self.optim_for_gen = paddle.optimizer.Adam(lr, parameters=self.generator.parameters(), beta1=0.5) # 生成器优器
self.optim_for_dis = paddle.optimizer.Adam(lr, parameters=self.discriminator.parameters(), beta1=0.5) # 判别器优化器
self.loss_for_gen = nn.BCELoss() # 生成器损失
self.loss_for_dis = nn.BCELoss() # 判别器损失
self.true_label = 1 # 真实数据标签
self.false_label = 0 # 生成假数据标签
self.label = paddle.to_tensor([1]*72)
def load_data(self, data):
# 导入数据
self.data_loader = data
def train(self, epochs):
## 导入之前的训练参数##
# self.generator.set_state_dict(paddle.load( os.path.join('model', 'generator.pdmodel')))
# self.discriminator.set_state_dict(paddle.load( os.path.join('model', 'discriminator.pdmodel')))
for epoch in range(1, epochs+1):
for i, img in enumerate(self.data_loader):
# 训练生成器
img = img[0]
self.optim_for_dis.clear_grad()
output = self.discriminator(img).squeeze()
self.label = paddle.to_tensor([self.true_label]*img.shape[0], dtype='float32')
err1 = self.loss_for_dis(output, self.label)
err1.backward()
self.label = paddle.to_tensor([self.false_label]*img.shape[0], dtype='float32')
noise = paddle.randn((img.shape[0], 100, 1, 1))
fake = self.generator(noise)
output = self.discriminator(fake.detach()).squeeze()
err2 = self.loss_for_dis(output, self.label)
err2.backward()
err = err1+err2
self.optim_for_dis.step()
# 训练判别器
self.optim_for_gen.clear_grad()
self.label = paddle.to_tensor([self.true_label]*img.shape[0], dtype='float32')
output = self.discriminator(fake).squeeze()
err3 = self.loss_for_gen(output, self.label)
err3.backward()
self.optim_for_gen.step()
# 打印损失
if i%5==0:
print('epoch/Epoch {}/{} iter/Iter {}/{} lossD {:.4f}, lossG {:.4f}'.format(epoch,
epochs,
i+1,
len(self.data_loader),
err.item(),
err3.item()))
# 模型训练
# 下面实例化 DCGAN,并导入上面加载好的数据 data_loader 进行模型训练。
dcgan = DCGAN() # 实例化 DCGAN
dcgan.load_data(data_loader) # 导入上面的数据集
dcgan.train()
# 模型测试
# 这里是输入随机噪声并测试训练了一个epoch的模型,生成卡通头像的代码和效果图:
t = transforms.Transpose(order=(1, 2, 0))
to_plot = dcgan.generator(noise_eval).numpy()
plt.figure(figsize=(8, 8))
for i, img in enumerate(to_plot):
if i >= 64:
break
img = t(img)
plt.subplot(8, 8, i+1)
plt.imshow(abs(img))
plt.show()
# 下面,我们导入预训练好的模型参数(80 个 epoch),再生成一批卡通头像:
# 载入模型参数
state_dict = paddle.load("/data/bigfiles/generator_net.pdparams")
# 将load后的参数与模型program关联起来
dcgan.generator.set_state_dict(state_dict)
t = transforms.Transpose(order=(1, 2, 0))
to_plot = dcgan.generator(noise_eval).numpy()
plt.figure(figsize=(8, 8))
for i, img in enumerate(to_plot):
if i >= 64:
break
img = t(img)
plt.subplot(8, 8, i+1)
plt.imshow(abs(img))
plt.show()
深度学习是一种基于多层神经网络的机器学习方法,强调通过构建和训练包含多个隐藏层的模型,自动从数据中提取特征,实现对复杂数据的高层次抽象和理解。