深度学习入门

前言:依据李沐视频整理

数据操作

按元素操作

各个矩阵的元素都是按照对应位置来操作

1
2
3
x = torch.tensor([1.0, 2, 4, 8])
y = torch.tensor([2, 2, 2, 2])
x + y, x - y, x * y, x / y, x ** y # **运算符是求幂运算
1
2
3
4
5
(tensor([ 3.,  4.,  6., 10.]),
tensor([-1., 0., 2., 6.]),
tensor([ 2., 4., 8., 16.]),
tensor([0.5000, 1.0000, 2.0000, 4.0000]),
tensor([ 1., 4., 16., 64.]))

逻辑判断

1
X == Y
1
2
3
tensor([[False,  True, False,  True],
[False, False, False, False],
[False, False, False, False]])

广播机制

在形状不同的张量上,会调用广播机制来执行按元素操作,也就是通过适当的复制元素来扩展一个或者两个数组,以便在转换之后,可以按照对应位置的元素进行操作

在大多数情况下,我们将沿着数组中长度为1的轴进行广播,如下例子:

1
2
3
a = torch.arange(3).reshape((3, 1))
b = torch.arange(2).reshape((1, 2))
a, b
1
2
3
4
(tensor([[0],
[1],
[2]]),
tensor([[0, 1]]))

由于ab分别是3×1和1×2矩阵,如果让它们相加,它们的形状不匹配。 我们将两个矩阵广播为一个更大的3×2矩阵,如下所示:矩阵a将复制列, 矩阵b将复制行,然后再按元素相加。

1
a + b
1
2
3
tensor([[0, 1],
[1, 2],
[2, 3]])

转换为其他python对象

将深度学习框架定义的张量转换为NumPy张量(ndarray)很容易,反之也同样容易。 torch张量和numpy数组将共享它们的底层内存,就地操作更改一个张量也会同时更改另一个张量。

1
2
3
A = X.numpy()
B = torch.tensor(A)
type(A), type(B)
1
(numpy.ndarray, torch.Tensor)

将张量转换为标量,可以调用item函数

1
2
a = torch.tensor([3.5])
a, a.item(), float(a), int(a)
1
(tensor([3.5000]), 3.5, 3.5, 3)

数据预处理

处理缺失值

典型的方法包括插值法和删除法。先使用pandas进行读取,然后进行插值操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 如果没有安装pandas,只需取消对以下行的注释来安装pandas
# !pip install pandas
import pandas as pd

data = pd.read_csv(data_file)
print(data)
# NumRooms Alley Price
# 0 NaN Pave 127500
# 1 2.0 NaN 106000
# 2 4.0 NaN 178100
# 3 NaN NaN 140000

inputs, outputs = data.iloc[:, 0:2], data.iloc[:, 2]
# 位置索引iloc,input选择所有行,0-2列;output是所有行,第二列
inputs = inputs.fillna(inputs.mean())
print(inputs)
# NumRooms Alley
# 0 3.0 Pave
# 1 2.0 NaN
# 2 4.0 NaN
# 3 3.0 NaN

转换为张量

将pandas里面的数值转换为张量

1
2
3
4
5
import torch

X = torch.tensor(inputs.to_numpy(dtype=float))
y = torch.tensor(outputs.to_numpy(dtype=float))
X, y
1
2
3
4
5
(tensor([[3., 1., 0.],
[2., 0., 1.],
[4., 0., 1.],
[3., 0., 1.]], dtype=torch.float64),
tensor([127500., 106000., 178100., 140000.], dtype=torch.float64))

线性代数

点积

通俗的讲就是是相同位置的按元素乘积的和:

1
2
y = torch.ones(4, dtype = torch.float32)
x, y, torch.dot(x, y)
1
(tensor([0., 1., 2., 3.]), tensor([1., 1., 1., 1.]), tensor(6.))

也可以这样写

1
2
torch.sum(x * y)
# 输出同样的是tensor(6.)

范数

简单讲范数即是向量的大小

  • L1范数

向量绝对值相加

X1=i=1nxi||X||_{1} = \sum_{i = 1}^{n}|x_{i}|

  • L2范数

像是以前高中学过的向量的模

X2=i=1nx2||X||_{2} = \sqrt{\sum_{i = 1}^{n}x^{2}}

  • F范数

针对矩阵的范数

XF=i=1mj=1nxij2||X||_{F} = \sqrt{\sum_{i = 1}^{m}\sum_{j = 1}^{n}x_{ij}^{2}}

调用以下函数将计算矩阵的Frobenius范数

1
2
torch.norm(torch.ones((4, 9)))
# tensor(6.)

矩阵计算

通常所指的梯度就是导数,即函数的斜率。梯度指向的是值变化最大的方向

自动求导的两种模式

  • 正向累积
  • 反向累积

链式法则的公式

image-20240725172612742

正向累积 示意

image-20240725172650144

反向累积、又称反向传递 示意

image-20240725172707248

一个例子

对函数进行求导

y=2x2y = 2x^2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import torch

x = torch.arange(4.0)
#tensor([0., 1., 2., 3.])

x.requires_grad_(True)
# 等价于x=torch.arange(4.0,requires_grad=True)

# requires_grad 表达的含义是,这一参数是否保留(或者说持有,即在前向传播完成后,是否在显存中记录这一参数的梯度,而非立即释放)梯度,等待优化器执行optim.step()更新参数

y = 2 * torch.dot(x, x)

y.backward()
print(x.grad)
# tensor([ 0., 4., 8., 12.])

x.grad == 4 * x
# 函数y = 2x^x关于x的梯度应为,快速验证这个梯度是否计算正确
# 输出tensor([True, True, True, True])

损失函数

损失函数(loss function)能够量化目标的实际值与预测值之间的差距。 通常我们会选择非负数作为损失,且数值越小表示损失越小,完美预测时的损失为0。

image-20240729171712070

回归问题中最常用的损失函数是平方误差函数。

分类问题中则多采用交叉熵损失函数

交叉熵损失函数

交叉熵常用来衡量两个概率的区别

H(p,q)=ipilog(qi)H(p,q) = \sum_{i}-p_{i}\log(q_{i})

将他作为损失

其中,对于任何标签y}和模型预测y^,损失函数为:

l(y,y^)=j=1qyjlogy^j.=logyy^l(\mathbf{y}, \hat{\mathbf{y}}) = - \sum_{j=1}^q y_j \log \hat{y}_j. = -\log\hat{\mathbf{y}_{y}}

其梯度是真实概率和预测概率的区别

ojl(y,y^)=softmax(o)jyj.\partial_{o_j} l(\mathbf{y}, \hat{\mathbf{y}}) = \mathrm{softmax}(\mathbf{o})_j - y_j.

损失函数:用来衡量模型预测值与真实值之间的差异。

  • 损失函数越小说明模型和参数越符合训练样本。
  • 任何能够衡量模型预测值与真实值之间差异的函数都可以叫做损失函数。
  • 为了避免过拟合,一般在损失函数的后面添加正则化项:(F = 损失函数 + 正则化项)

常用的损失函数

1、交叉熵(Cross Entropy),用于分类:反应两个概率分布的距离(不是欧式距离)。交叉熵又称为对数似然损失、对数损失;二分类时还可称为逻辑回归损失。

1
torch.nn.CrossEntropyLoss(weight=None, size_average=None, ignore_index=-100, reduce=None, reduction='mean')

C=1nx[ylna+(1y)ln(1a)]C = -\frac{1}{n}\sum_{x}[y\ln a + (1 - y)\ln (1 - a)]

其中:c表示损失值;
    n表示样本数量,也就是batchsize;
    x表示预测向量维度,主要是因为需要在输出的特征向量维度上一个个计算并求和;
    y表示真实值,对应x维度上的标签(1 或 0);
    a表示输出的预测概率值(0~1之间,总和为1)。

2、均方误差(Mean Squared error,MSE),用于回归:反应预测值与实际值之间的欧氏距离。

1
torch.nn.MSELoss(size_average=None, reduce=None, reduction='mean')

MSE=i=1n(y预测值y实际值)2nMSE = \frac{\sum_{i = 1}^n(y\text{预测值} - y\text{实际值}) ^ 2}{n}

梯度下降

那么怎么进行模型的优化呢?我们用到一种名为梯度下降(gradient descent)的方法, 这种方法几乎可以优化所有深度学习模型。 它通过不断地在损失函数递减的方向上更新参数来降低误差。

那么,为什么需要进行梯度下降呢

有个挺不错的视频:[5分钟深度学习] #01 梯度下降算法_哔哩哔哩_bilibili

以线性的时候为例,现在需要一条直线来回归拟合这三个点

拟合这三个点

直线的方程是y =wx + b,而通过上述的损失函数,也就是直线到点的距离量化,作为与实际相差的值,那么得到损失函数就是一个二次函数

二次函数

这个二次函数的最小值,也就是损失函数的最小值,就是这个直线方程与实际相差最小,最拟合。这里我们使用牛顿迭代法来求出这个最小值,也就是图中所示的b的更新方程。其中ε就是学习率

梯度下降最简单的用法是计算损失函数(数据集中所有样本的损失均值) 关于模型参数的导数(在这里也可以称为梯度)。 但实际中的执行可能会非常慢:因为在每一次更新参数之前,我们必须遍历整个数据集。 因此,我们通常会在每次需要计算更新的时候随机抽取一小批样本, 这种变体叫做小批量随机梯度下降(minibatch stochastic gradient descent)。

在每次迭代中,我们首先随机抽样一个小批量样本, 它是由固定数量的训练样本组成的。 然后,我们计算小批量的平均损失关于模型参数的导数(也可以称为梯度)。 最后,我们将梯度乘以一个预先确定的正数,并从当前参数的值中减掉。

计算梯度和损失值:

  • 挑选一个初始值w0
  • 重复迭代参数t = 1,2,3
  • 沿梯度方向将增加损失函数值
  • 学习率:步长的超参数

image-20240729143941676

选择学习率,不能太大也不能太小

太小会导致训练慢

太大则会导致震荡

1
2
optimizer.zero_grad() #梯度清零
optimizer.step() #梯度更新

经典的梯度下降法

先假设一个学习率,参数沿梯度的反方向移动。

  • 计算:每次迭代过程中,采用所有的训练数据的平均损失来近似目标函数。
  • 特点:(1)靠近极值点时收敛速度减慢;(2)可能会" 之 "字形的下降。(3)对学习率非常敏感,难以驾驭;(4)对参数空间的方向没有解决方法。

SGD、SGDM、NAG

SGD(随机梯度下降法)

  • 计算:每次只使用一个数据样本,去计算损失函数,求梯度,更新参数。
  • 特点:(1)计算速度快,但是梯度下降慢(2)可能会在最低处两边震荡,停留在局部最优。

SGDM(SGM with Momentum:动量梯度下降)

  • 计算:对已有的梯度进行指数加权平均,然后加上(1 - beta)。即在现有梯度信息的基础上,加入一个惯性。而梯度方向,由之前与现在的梯度方向共同决定。

  • 优缺点:

    • (1)与SGD相比,训练过程的震荡幅度会变小,速度变快。

    • (2)SGDM速度没Adam快,但泛化能力好。

NAG(Nesterov Accelerated Gradient)

  • 计算:先使用累积的动量计算一次,得到下一次的梯度方向,再把下一个点的梯度方向,与历史累积动量相结合,计算现在这个时刻的累计动量。

AdaGrad、RMSProp、Adam

  • AdaGrad(Adaptive Gradient,自适应步长):累积梯度平方

    • 计算:在现有的(梯度 * 步长)上,添加了一个系数:1 /(历史梯度的平方和,再开根号)。
    • 优缺点:(1)适合处理稀疏数据,可以对稀疏特征进行大幅更新。(2)学习率单调递减,最终会趋于0,学习提前终止。
  • RMSProp(root mean square prop,梯度平方根):累积梯度平方的滑动平均

    • 计算:该方法和Adagrad的区别就是分母不一样,使得系数不会因为前几步的梯度太大而导致分母太大,从而导致系数变得太小而走不动。
  • Adam(Adaptive Moment Estimation,自适应估计):利用梯度的一阶、二阶矩阵估计

    • 优缺点:Adam是一种在深度学习模型中用来替代随机梯度下降的优化算法。它是SGDM和RMSProp算法的结合,训练速度快,泛化能力不太行。

softmax

与线性回归一样,softmax回归也是一个单层神经网络。 由于计算每个输出o1、o2和o3取决于 所有输入x1、x2、x3和x4, 所以softmax回归的输出层也是全连接层。

image-20240729173000131

简单的来说:

  • Softmax 回归是一个多分类模型

  • 使用softmax操作子类得到每个类的预测置信度

  • 使用交叉熵来衡量预测和标号的区别

在实际的应用中,可以直接使用softmax函数对模型输出的pred_logits进行处理,得到预测概率,且各个种类概率加起来等于1

使用F.softmax函数计算概率分布

1
pred_softmax = F.softmax(pred_logits, dim=1)

感知机

XOR问题

当使用线性模型的时候,如果数据呈现下面的这种分布,很明显,我们无法通过一个线性模型来达到分类的目的

因此,聪明的人们想出了使用多层模型来达到分类的目的。

image-20240729161934894

使用两个线性模型(上图的两条直线)先进行分类,然后进行异或运算,即可得到最终正确的分类,这就是多层的感知机

多层感知机在输出层和输入层之间增加一个或多个全连接隐藏层,并通过激活函数转换隐藏层的输出

激活函数

为什么要一个非线性激活函数

如果没有—那么最后多层感知机依旧是一个线性的模型,等价于一个单层的感知机

ReLU函数

最受欢迎的激活函数是修正线性单元(Rectified linear unit,ReLU), 因为它实现简单,同时在各种预测任务中表现良好。 ReLU提供了一种非常简单的非线性变换。 给定元素x,ReLU函数被定义为该元素与0的最大值:

ReLU(x)=max(x,0).ReLU(x) = max(x,0).

说白了就是一个和0比较的max函数

1
2
3
x = torch.arange(-8.0, 8.0, 0.1, requires_grad=True)
y = torch.relu(x)
d2l.plot(x.detach(), y.detach(), 'x', 'relu(x)', figsize=(5, 2.5))

sigmoid函数

对于一个定义域在R中的输入, sigmoid函数将输入变换为区间(0, 1)上的输出。 因此,sigmoid通常称为挤压函数(squashing function): 它将范围(-inf, inf)中的任意输入压缩到区间(0, 1)中的某个值

1
2
y = torch.sigmoid(x)
d2l.plot(x.detach(), y.detach(), 'x', 'sigmoid(x)', figsize=(5, 2.5))

tanh函数

与sigmoid函数类似, tanh(双曲正切)函数也能将其输入压缩转换到区间(-1, 1)上。

1
2
y = torch.tanh(x)
d2l.plot(x.detach(), y.detach(), 'x', 'tanh(x)', figsize=(5, 2.5))

(1)用于神经网络的隐含层与输出层之间(如:卷积层+激活函数+池化层),为神经网络提供非线性建模能力。
(2)若没有激励函数,则模型只能处理线性可分问题。与最原始的感知机相当,拟合能力有限。

image-20240724154035611

深度学习的四种激活函数:

image-20240724154203729

image-20240724154240130

如何选择激活函数:

(1)任选其一:若网络层数不多
(2)ReLU:若网络层数较多

  • 不宜选择sigmoid、tanh,因为它们的导数都小于1,sigmoid的导数更是在[0, 1/4]之间。
  • 根据微积分链式法则,随着网络层数增加,导数或偏导将指数级变小。
  • 网络层数较多的激活函数其导数不宜小于1也不能大于1,大于1将导致梯度爆炸,导数为1最好,而relu正好满足这个要

(3)Softmax:用于多分类神经网络输出层。

正则化

提高模型的泛化能力,防止过拟合

L1 正则化

  • L1正则化(L1范数),通常表示为:||W||1:指权值向量 W 中各个元素的绝对值之和。
  • 特点:又叫特征选择,产生稀疏权值矩阵。即提取权重值最大的前N个值,并将冗余的权重值置0,直接删除。

L2 正则化

  • L2正则化(L2范数),通常表示为:||W||2:指权值向量 W 中各个元素的平方和,然后求平方根。
  • 特点:又叫权重衰减。即抑制模型中产生过拟合的参数,使其趋近于0(而不等于0),影响变小。
  • 特点:倾向于让所有参数的权值尽可能小。

若不关心显式特征(哪些特征重要或不重要),L2正则化的性能往往优于L1正则化。

Dropout 正则化

随机删除一定比例的神经元

1
torch.nn.Dropout(p=0.5) # 表示随机删除 50% 的神经元

Dropout正则化: 在训练过程中,随机删除一定比例的神经元(比例参数可设置)。
一般只在训练阶段使用,测试阶段不使用。
一般控制在20% ~ 50%。太低没有效果,太高则会导致模型欠学习。
应用场景:
(1)在大型网络模型上效果显著
(2)在输入层和隐藏层都使用Dropout
(3)当学习率和冲量值较大时:如学习率扩大10 ~ 100倍,冲量值调高到0.9 ~ 0.99。
(4)用于限制网络模型的权重时:学习率越大,权重值越大。

通常将其作用在隐藏全连接层的输出上

image-20240730124159950

丢弃法将一些输出项随机置0来控制模型复杂度

丢弃概率是控制模型复杂度的超参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
net = nn.Sequential(nn.Flatten(),
nn.Linear(784, 256),
nn.ReLU(),
# 在第一个全连接层之后添加一个dropout层
nn.Dropout(dropout1),
nn.Linear(256, 256),
nn.ReLU(),
# 在第二个全连接层之后添加一个dropout层
nn.Dropout(dropout2),
nn.Linear(256, 10))

def init_weights(m):
if type(m) == nn.Linear:
nn.init.normal_(m.weight, std=0.01)

net.apply(init_weights);

前向传播

实现信息的前向传导,即数据从输入层,依次经过多个隐藏层,最终到达输出层。数据每经过一个网络层,其节点输出的值所代表的信息层次就越高阶和概括

反向传播

再贴一个视频 介绍反向传播

迭代训练,降低损失(loss,与最优化方法(如:梯度下降法)结合使用的,用来训练神经网络的常见方法。

计算过程

通过损失函数,计算模型中所有权重参数的梯度,并反馈给优化算法进行梯度(权值)更新。迭代训练 N 次,获得最小损失。

从后向前计算

这种沿着黄色箭头,从后向前计算参数梯度值的方法就叫反向传播算法。

单次反向传播的过程:首先是离输出层最近的网络层E,然后是网络层D,并按照EDCBA的依次顺序进行反向传播,直到所有层都完成一次。


深度学习入门
http://example.com/2024/07/25/deeplearningPreliminaries/
作者
Mercury
发布于
2024年7月25日
许可协议