什么是深度学习?
机器学习是实现人工智能的一种途径。
深度学习是机器学习的一个子集,也就是说深度学习是实现机器学习的一种方法。
深度学习是机器学习中一种基于对数据进行特征学习的算法。
深度学习是基于人工神经网络,深度是指网络中使用多层,每层都通过非线性变换处理数据,并逐渐提取出更复杂、更抽象的特征。
传统机器学习算法依赖人工设计特征,并进行特征提取;而深度学习方法不需要人工,而是依赖算法自动提取特征。深度学习通过模仿人脑的神经网络来处理和分析复杂的数据,从大量数据中自动提取复杂特征。这也是深度学习被看做黑盒子,可解释性差的原因。
深度学习尤其擅长处理高维数据,如图像、语音和文本。
① 多层非线性变换:深度学习模型由多个层次组成,每一层都应用非线性激活函数对输入数据进行变换。较低的层级通常捕捉到简单的特征(如边缘、颜色等),而更高的层级则可以识别更复杂的模式(如物体或面部识别)。
② 自动特征提取:与传统机器学习算法不同,深度学习能够自动从原始数据中学习到有用的特征,而不需要人工特征工程。这使得深度学习在许多领域中表现出色。
③ 大数据和计算能力:深度学习模型通常需要大量的标注数据和强大的计算资源(如GPU)来进行训练。大数据和高性能计算使得深度学习在图像识别、自然语言处理等领域取得了显著突破。
④ 可解释性差:深度学习模型内部的运作机制相对不透明,被称为“黑箱”可能会比较困难。这对某些应用场景来说是一个挑战。
常见的深度学习模型
- 卷积神经网络 (Convolutional Neural Networks, CNN)
- 循环神经网络 (Recurrent Neural Networks, RNN)
- 自编码器 (Autoencoders)
- 生成对抗网络 (Generative Adversarial Networks, GAN)
- Transformer
- 深度强化学习(Deep Reinforcement Learning, DRL)
- 图神经网络(Graph Neural Network, GNN)
- 人工神经网络(Artificial Neural Network, ANN)
PyTorch框架
什么是PyTorch
PyTorch 是一个由 Facebook(现在的 Meta)开发的深度学习框架,它提供了构建神经网络、自动微分和 GPU 加速的工具。
其中torch是PyTorch中的核心库,在代码中你可以这样引用PyTorch库
import torch
这个 torch 就是 PyTorch 框架的核心模块。也就是说:
PyTorch 是名字,torch 是用的库名。就像“TensorFlow” 是框架名,但你代码里写的是 import tensorflow 一样。
PyTorch的特点
1.类似于NumPy的张量计算
PyTorch中的基本数据结构是张量(Tensor),它与NumPy中的数组类似,但PyTorch的张量具有GPU加速的能力(通过CUDA),这使得深度学习模型能够高效地在GPU上运行。
2.自动微分系统
PyTorch提供了强大的自动微分功能(autograd模块),能够自动计算模型中每个参数的梯度。
自动微分使得梯度计算过程变得简洁和高效,并且支持复杂的模型和动态计算图。
3.深度学习库
PyTorch提供了一个名为torch.nn的子模块,用于构建神经网络。它包括了大量的预构建的层(如全连接层、卷积层、循环神经网络层等),损失函数(如交叉熵、均方误差等),以及优化算法(如SGD、Adam等)。
torch.nn.Module是PyTorch中构建神经网络的基础类,用户可以通过继承该类来定义自己的神经网络架构。
4.动态计算图
PyTorch使用动态计算图机制,允许在运行时构建和修改模型结构,具有更高的灵活性,适合于研究人员进行实验和模型调试。
5.GPU加速(CUDA支持)
PyTorch提供对GPU的良好支持,能够在NVIDIA的CUDA设备上高效地进行计算。用户只需要将数据和模型转移到GPU上,PyTorch会自动优化计算过程。
通过简单的tensor.to(device)方法,可以轻松地将模型和数据从CPU转移到GPU或从一个GPU转移到另一个GPU。
6.跨平台支持
PyTorch支持在多种硬件平台(如CPU、GPU、TPU等)上运行,并且支持不同操作系统(如Linux、Windows、macOS)以及分布式计算环境(如多GPU、分布式训练)。
张量
什么是张量
张量是PyTorch中的核心数据抽象,PyTorch中的张量就是元素为同一种数据类型的多维矩阵,与NumPy数组类似。
张量中默认的数据类型是float32(torch.FloatTensor),其他数据类型:
张量的创建方式
torch.tensor(data=, dtype=) 指定数据
import torch
import numpy as np
# 1. 创建张量标量
data = torch.tensor(10)
print(data)
# 2. numpy 数组, 由于data为float64, 张量元素类型也是float64
data = np.random.randn(2, 3)
data = torch.tensor(data)
print(data)
# 3. 列表, 浮点类型默认float32
data = [[10., 20., 30.], [40., 50., 60.]]
data = torch.tensor(data)
print(data)
torch.Tensor(size=) 指定数据或形状
# 1. 创建2行3列的张量, 默认 dtype 为 float32
data = torch.Tensor(2, 3)
print(data)
# 2. 注意: 如果传递列表, 则创建包含指定元素的张量
data = torch.Tensor([10])
print(data)
data = torch.Tensor([10, 20])
print(data)
torch.IntTensor()/FloatTensor() 创建指定类型的张量
# 1. 创建2行3列, dtype 为 int32 的张量
data = torch.IntTensor(2, 3)
print(data)
# 2. 注意: 如果传递的元素类型不正确, 则会进行类型转换
data = torch.IntTensor([2.5, 3.3])
print(data)
# 3. 其他的类型
data = torch.ShortTensor() # int16
data = torch.LongTensor() # int64
data = torch.FloatTensor() # float32
data = torch.DoubleTensor() # float64
torch.arange(start=, end=, step=):从start(包含)开始到end(不包含)每隔step生成一个点
torch.linspace(start=, end=, steps=):在[start,end]区间生成steps个点
# 1. 在指定区间按照步长生成元素 [start, end, step) 左闭右开
data = torch.arange(0, 10, 2)
print(data)
# 2. 在指定区间按照元素个数生成 [start, end, steps] 左闭右闭
# step = (end-start) / (steps-1)
# value_i = start + step * i
data = torch.linspace(0, 9, 10)
print(data)
torch.randn/rand(size=) 创建随机浮点类型张量
torch.randint(low=, high=, size=) 创建随机整数类型张量 左闭右开torch.initial_seed() 和 torch.manual_seed(seed=) 随机种子设置
torch.manual_seed(100)
data = torch.randn(2, 3)
print(data)
print('随机数种子:', torch.initial_seed())
torch.zeros(size=) 和 torch.zeros_like(input=) 创建全0张量
# 1. 创建指定形状全0张量
data = torch.zeros(2, 3)
print(data)
# 2. 根据张量形状创建全0张量
data = torch.zeros_like(data)
print(data)
torch.ones(size=) 和 torch.ones_like(input=) 创建全1张量
# 1. 创建指定形状全1张量
data = torch.ones(2, 3)
print(data)
# 2. 根据张量形状创建全1张量
data = torch.ones_like(data)
print(data)
torch.full(size=, fill_value=) 和 torch.full_like(input=, fill_value=) 创建全为指定值张量
# 1. 创建指定形状指定值的张量
data = torch.full([2, 3], 10)
print(data)
# 2. 根据张量形状创建指定值的张量
data = torch.full_like(data, 20)
print(data)
张量的数值计算
add(other=)、sub()、mul()、div()、neg()
add_(other=)、sub_()、mul_()、div_()、neg_()(其中带下划线的版本会修改原数据)
data = torch.randint(0, 10, [2, 3])
print(data)
# 1. 不修改原数据
new_data = data.add(10) # 等价 new_data = data + 10
print(new_data)
# 2. 直接修改原数据 注意: 带下划线的函数为修改原数据本身
data.add_(10) # 等价 data += 10
print(data)
# 3. 其他函数
print(data.sub(100))
print(data.mul(100))
print(data.div(100))
print(data.neg())
点乘运算
点乘(Hadamard)也称为元素级乘积,指的是相同形状的张量对应位置的元素相乘,使用mul和运算符 * 实现。
data1 = torch.tensor([[1, 2], [3, 4]])
data2 = torch.tensor([[5, 6], [7, 8]])
# 第一种方式
data = torch.mul(data1, data2)
print(data)
# 第二种方式
data = data1 * data2
print(data)
矩阵乘法运算
矩阵乘法运算要求第一个矩阵 shape: (n, m),第二个矩阵 shape: (m, p), 两个矩阵点积运算 shape 为: (n, p)。
运算符 @ 用于进行两个矩阵的乘积运算
torch.matmul(input=, other=) 对进行乘积运算的两矩阵形状没有限定。对于输入shape不同的张量, 对应的最后几个维度必须符合矩阵运算规则
# 点积运算
data1 = torch.tensor([[1, 2], [3, 4], [5, 6]])
data2 = torch.tensor([[5, 6], [7, 8]])
# 方式一:
data3 = data1 @ data2
print("data3-->", data3)
# 方式二:
data4 = torch.matmul(data1, data2)
print("data4-->", data4)
张量运算函数
tensor.mean(dim=):平均值
tensor.sum(dim=):求和
tensor.min/max(dim=):最小值/最大值
tensor.pow(exponent=):幂次方
tensor.sqrt(dim=):平方根
tensor.exp():指数
tensor.log(dim=):对数 以e为底
dim=0按列计算,dim=1按行计算
import torch
data = torch.randint(0, 10, [2, 3], dtype=torch.float64)
print(data)
# 1. 计算均值
# 注意: tensor 必须为 Float 或者 Double 类型
print(data.mean())
print(data.mean(dim=0)) # 按列计算均值
print(data.mean(dim=1)) # 按行计算均值
# 2. 计算总和
print(data.sum())
print(data.sum(dim=0))
print(data.sum(dim=1))
# 3. 计算平方
print(torch.pow(data,2))
# 4. 计算平方根
print(data.sqrt())
# 5. 指数计算, e^n 次方
print(data.exp())
# 6. 对数计算
print(data.log()) # 以 e 为底
print(data.log2())
print(data.log10())
张量索引操作
张量切片的基本格式是:
tensor[dim0_start:dim0_end:step0, dim1_start:dim1_end:step1, …, dimN_start:dimN_end:stepN]
其中:
dimX_start:第 X 维的起始索引(含)
dimX_end:第 X 维的结束索引(不含)
stepX:步长(可以省略,默认为 1)
如果省略某个维度的参数,如 start:end,则默认:
start 默认为 0
end 默认为该维度的长度
step 默认为 1
张量形状操作(重点)
reshape
用于改变张量(Tensor)的形状,而不改变其数据内容
torch_tensor.reshape(new_shape)
或
torch.reshape(torch_tensor, new_shape)
其中new_shape是一个整数序列(如元组或多个整数),表示你希望张量变成的新形状。可以使用 -1 来自动推断某一维的大小(只能用一次)。
import torch
x = torch.arange(12) # 创建一个一维张量,包含 0 到 11
print(x.shape) # 输出: torch.Size([12])
y = x.reshape(3, 4) # 转换为 3 行 4 列的二维张量
print(y)
torch.squeeze(input, dim=None):移除维度为 1 的维度。
• 如果不指定 dim,所有维度为 1 的轴都会被去掉;
• 如果指定 dim,只有在该维度为 1 的情况下才会被去掉。
x = torch.randn(1, 3, 1, 5)
print(x.shape) # torch.Size([1, 3, 1, 5])
y = x.squeeze()
print(y.shape) # torch.Size([3, 5])
z = x.squeeze(2)
print(z.shape) # torch.Size([1, 3, 5])
torch.unsqueeze(input, dim):在指定维度 dim 上插入一个大小为 1 的维度。
x = torch.tensor([1, 2, 3])
print(x.shape) # torch.Size([3])
y = x.unsqueeze(0)
print(y.shape) # torch.Size([1, 3])
z = x.unsqueeze(1)
print(z.shape) # torch.Size([3, 1])
tensor.transpose(dim0, dim1):实现交换张量形状的指定维度, 例如: 一个张量的形状为 (2, 3, 4) ,把 3 和 4 进行交换, 将张量的形状变为 (2, 4, 3)
tensor.permute(dims):一次交换更多的维度
import torch
x = torch.randn(2, 3)
# 原始形状: [2, 3]
x_t = x.transpose(0, 1)
# 新形状: [3, 2]
x = torch.randn(2, 3, 4)
# 原始形状: [2, 3, 4]
x_p = x.permute(2, 0, 1)
# 新形状: [4, 2, 3]
view函数也可以用于修改张量的形状,只能用于修改连续的张量。在PyTorch中,有些张量的底层数据在内存中的存储顺序与其在张量中的逻辑顺序不一致,view函数无法对这样的张量进行变形处理,例如: 一个张量经过了 transpose 或者 permute 函数的处理之后,就无法使用 view 函数进行形状操作。
contiguous:将不连续张量转为连续张量
is_contiguous:判断张量是否连续,返回True或False
# 1 一个张量经过了 transpose 或者 permute 函数的处理之后,就无法使用 view 函数进行形状操作
# 若要使用view函数, 需要使用contiguous() 变成连续以后再使用view函数
# 2 判断张量是否连续
data = torch.tensor([[10, 20, 30],[40, 50, 60]])
print('data--->', data, data.shape)
# 1 判断张量是否连续
print(data.is_contiguous()) # True
# 2 view
mydata2 = data.view(3, 2)
print('mydata2--->', mydata2, mydata2.shape)
# 3 判断张量是否连续
print('mydata2.is_contiguous()--->', mydata2.is_contiguous())
# 4 使用 transpose 函数修改形状
mydata3 = torch.transpose(data, 0, 1)
print('mydata3--->', mydata3, mydata3.shape)
print('mydata3.is_contiguous()--->', mydata3.is_contiguous())
# 5 需要先使用 contiguous 函数转换为连续的张量,再使用 view 函数
print (mydata3.contiguous().is_contiguous())
mydata4 = mydata3.contiguous().view(2, 3)
print('mydata4--->', mydata4.shape, mydata4)
张量的拼接操作
torch.cat(tensors, dim=0)将多个张量在指定维度上进行拼接,拼接的维度上需要保持一致的大小。
tensors:一个张量列表,如 [t1, t2, …]
dim:在哪个维度上拼接
import torch
a = torch.tensor([[1, 2], [3, 4]])
b = torch.tensor([[5, 6], [7, 8]])
# 在第0维拼接(行拼接)
torch.cat((a, b), dim=0)
# 输出:
# tensor([[1, 2],
# [3, 4],
# [5, 6],
# [7, 8]])
# 在第1维拼接(列拼接)
torch.cat((a, b), dim=1)
# 输出:
# tensor([[1, 2, 5, 6],
# [3, 4, 7, 8]])
torch.stack(tensors, dim=0)在一个新的维度上将多个张量堆叠起来,所有张量必须形状相同。
dim:在哪个维度上插入新维度进行堆叠
a = torch.tensor([[1, 2], [3, 4]])
b = torch.tensor([[5, 6], [7, 8]])
# 在第0维堆叠
torch.stack((a, b), dim=0)
# 输出:
# tensor([[[1, 2],
# [3, 4]],
#
# [[5, 6],
# [7, 8]]])
# 在第1维堆叠
torch.stack((a, b), dim=1)
# 输出:
# tensor([[[1, 2],
# [5, 6]],
#
# [[3, 4],
# [7, 8]]])
比较项 | torch.cat | torch.stack |
---|---|---|
拼接维度 | 不创建新维度 | 创建新维度 |
张量形状要求 | 除拼接维度外其他维度相同 | 所有张量形状必须完全相同 |
应用场景 | 合并批数据、扩展数据 | 生成高维输入、打包多张相同结构张量 |
自动微分模块
自动微分就是自动计算梯度值,也就是计算导数
什么是梯度:对函数求导的值就是梯度
什么是梯度下降法:是一种求最优梯度值的方法,使得损失函数的值最小
梯度经典语录
对函数求导得到的值就是梯度 (在数值上的理解):在某一个点上,对函数求导得到的值就是该点的梯度;没有点就无法求导,没有梯度
梯度就是上山下山最快的方向 (在方向上理解):在平面内,梯度就是某一点上的斜率,y = 2x^2 某一点x=1的梯度,就是这一点上的斜率
反向传播传播的是梯度:反向传播利用链式法则不断的从后向前求导,求出来的值就是梯度,所以大家都经常说反向传播传播的是梯度
链式法则中,梯度相乘,就是传说中的梯度传播
训练神经网络时,最常用的算法就是反向传播。在该算法中,参数(模型权重)会根据损失函数关于对应参数的梯度进行调整。为了计算这些梯度,PyTorch内置了名为 torch.autograd 的微分模块。它支持任意计算图的自动梯度计算:
PyTorch 中的张量 Tensor 有一个属性:requires_grad,如果设置为 True,就会开始追踪所有在这个张量上的操作,并自动构建计算图(computation graph)。调用 .backward() 后,PyTorch 会自动计算所有的梯度,并存储在 .grad 属性中
基本使用步骤
1.创建张量并启用自动求导
张量的数据类型必须是浮点数(如 torch.float32, torch.float64)或复数(如 torch.cfloat)
整数类型张量(如 torch.int, torch.long)是不支持自动求导的
import torch
x = torch.tensor([2.0], requires_grad=True)
现在PyTorch会追踪对x的所有操作
2.构建函数图
PyTorch 的自动微分是基于链式法则的,因此所有涉及的操作都必须是可导的(differentiable)。
• 常见的数学运算(加减乘除、exp、log、sin、relu等)都是可微的。
• 如果你用了一些不支持反向传播的操作(比如 .item()、.detach() 后进行运算),就会“断图”。
y = x**2 + 3*x + 1
这一步构建了一个计算图,PyTorch 会记录 y 如何由 x 计算得到
3.反向传播
y.backward()
调用 backward() 后,PyTorch 会自动计算 dy/dx,并将结果保存在 x.grad 中。
注意:当输出是一个向量或矩阵时,调用 .backward() 必须提供 gradient 参数(模拟链式法则的“上一层梯度”)。
# 错误:因为 y 是向量
x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
y = x ** 2
# y.backward() # 会报错
# 正确:提供一个同形状的梯度
y.backward(torch.tensor([1.0, 1.0, 1.0]))
或者
x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
y = x * x
z = y.sum() # 标量
z.backward()
print(x.grad) # 输出 tensor([2.0, 4.0, 6.0])
其实可以理解为:
y.backward(torch.ones_like(x)) # 与 y.sum().backward() 等价
4.查看梯度
print(x.grad) # 输出 tensor([7.])
说明当 x=2.0 时,函数 y = x^2 + 3x + 1 的导数是 7
梯度下降法求最优解
梯度下降法公式: w = w – r * grad (r是学习率, grad是梯度值)
清空上一次的梯度值: x.grad.zero_()
# 求 y = x**2 + 20 的极小值点 并打印y是最小值时 w的值(梯度)
# 1 定义点 x=10 requires_grad=True dtype=torch.float32
# 2 定义函数 y = x**2 + 20
# 3 利用梯度下降法 循环迭代1000 求最优解
# 3-1 正向计算(前向传播)
# 3-2 梯度清零 x.grad.zero_()
# 3-3 反向传播
# 3-4 梯度更新 x.data = x.data - 0.01 * x.grad
import torch
# 1 定义点x=10 requires_grad=True dtype=torch.float32
x = torch.tensor(10, requires_grad=True, dtype=torch.float32)
# 2 定义函数 y = x ** 2 + 20
y = x ** 2 + 20
print('开始 权重x初始值:%.6f (0.01 * x.grad):无 y:%.6f' % (x, y))
# 3 利用梯度下降法 循环迭代1000 求最优解
for i in range(1, 1001):
# 3-1 正向计算(前向传播)
y = x ** 2 + 20
# 3-2 梯度清零 x.grad.zero_()
# 默认张量的 grad 属性会累加历史梯度值 需手工清零上一次的提取
# 一开始梯度不存在, 需要做判断
if x.grad is not None:
x.grad.zero_()
# 3-3 反向传播
y.sum().backward()
# 3-4 梯度更新 x.data = x.data - 0.01 * x.grad
# x.data是修改原始x内存中的数据,前后x的内存空间一样;如果使用x,此时修改前后x的内存空间不同
x.data = x.data - 0.01 * x.grad # 注:不能 x = x - 0.01 * x.grad 这样写
print('次数:%d 权重x: %.6f, (0.01 * x.grad):%.6f y:%.6f' % (i, x, 0.01 * x.grad, y))
print('x:', x, x.grad, 'y最小值', y)
不能将自动微分的张量转换成numpy数组,会发生报错,可以通过detach()方法实现
# 定义一个张量
x1 = torch.tensor([10, 20], requires_grad=True, dtype=torch.float64)
# 将x张量转换成numpy数组
# 发生报错,RuntimeError: Can't call numpy() on Tensor that requires grad. Use tensor.detach().numpy() instead.
# 不能将自动微分的张量转换成numpy数组
# print(x1.numpy())
# 通过detach()方法产生一个新的张量,作为叶子结点
x2 = x1.detach()
# x1和x2张量共享数据,但是x2不会自动微分
print(x1.requires_grad)
print(x2.requires_grad)
# x1和x2张量的值一样,共用一份内存空间的数据
print(x1.data)
print(x2.data)
print(id(x1.data))
print(id(x2.data))
# 将x2张量转换成numpy数组
print(x2.numpy())
自动微分模块的应用
import torch
# 输入张量 2*5
x = torch.ones(2, 5)
# 目标值是 2*3
y = torch.zeros(2, 3)
# 设置要更新的权重和偏置的初始值
w = torch.randn(5, 3, requires_grad=True)
b = torch.randn(3, requires_grad=True)
# 设置网络的输出值
z = torch.matmul(x, w) + b # 矩阵乘法
# 设置损失函数,并进行损失的计算
loss = torch.nn.MSELoss()
loss = loss(z, y)
# 自动微分
loss.backward()
# 打印 w,b 变量的梯度
# backward 函数计算的梯度值会存储在张量的 grad 变量中
print("W的梯度:", w.grad)
print("b的梯度", b.grad)
PyTorch构建线性回归模型
我们使用 PyTorch 的各个组件来构建线性回归模型。在pytorch中进行模型构建的整个流程一般分为四个步骤:
准备训练集数据
构建要使用的模型
设置损失函数和优化器
模型训练
要使用的API:
使用 PyTorch 的 nn.MSELoss() 代替平方损失函数
使用 PyTorch 的 data.DataLoader 代替数据加载器
使用 PyTorch 的 optim.SGD 代替优化器
使用 PyTorch 的 nn.Linear 代替假设函数
import torch
from sklearn.datasets import make_regression
from torch.utils.data import TensorDataset, DataLoader
from torch import nn
from torch.optim import SGD
# 创建数据
def create_datas(b, num_samples):
x,y,coef = make_regression(
n_samples=num_samples, #样本数量
n_features=1, #特征数量
noise=10, #噪声
coef=True, #是否返回系数 w
bias=b, #偏置 b
)
x = torch.tensor(x,dtype=torch.float32)
y = torch.tensor(y,dtype=torch.float32)
return x,y,coef
def train(x,y):
datasets = TensorDataset(x,y)
# 创建数据加载器
# datasets:张量数据集对象
# batch_size:每个batch的样本数
# shuffle:是否打乱数据
dataloader = DataLoader(datasets,batch_size=16,shuffle=True)
model = nn.Linear(in_features=1,out_features=1)
# 损失函数
criterion = nn.MSELoss()
# 创建SGD优化器
optimizer = SGD(model.parameters(),lr=0.01)
# 训练
for epoch in range(1000):
total_loss = 0
train_samples = 0
for batch in dataloader:
x,y = batch
y_pred = model(x)
loss = criterion(y_pred.reshape(-1),y)
total_loss += loss.item()
train_samples += 1
# 梯度清零
optimizer.zero_grad()
# 反向传播
loss.backward()
# 更新参数
optimizer.step()
# 计算平均损失
print(f'epoch:{epoch},loss:{total_loss/train_samples}')
print('w:',model.weight.data)
print('b:',model.bias.data)
if __name__ == '__main__':
x,y,coef = create_datas(14.5,100)
train(x,y)
print('coef:',coef)
BGD、SGD和Mini-Batch
BGD(Batch Gradient Descent)、SGD(Stochastic Gradient Descent) 和 Mini-Batch Gradient Descent(小批量梯度下降) 是三种常见的优化方法,它们的主要区别在于每次更新参数时使用的数据量不同。以下是它们的详细介绍和区别:
方法 | 每次更新样本数 | 收敛速度 | 稳定性 | 资源占用 | 常用程度 |
---|---|---|---|---|---|
BGD | 全部样本 | 慢 | 高 | 高 | 较少 |
SGD | 1 个样本 | 快 | 低 | 低 | 少 |
Mini-Batch | 一部分样本 | 适中 | 中 | 中 | 最常用 |
人工神经网络
什么是神经网络
人工神经网络(Artificial Neural Network, 简写为ANN)也简称为神经网络(NN),是一种模仿生物神经网络结构和功能的计算模型。它由多个互相连接的人工神经元(也称为节点)构成,可以用于处理和学习复杂的数据模式,尤其适合解决非线性问题。人工神经网络是机器学习中的一个重要模型,尤其在深度学习领域中得到了广泛应用。
如何构建神经网络
神经网络是由多个神经元组成,构建神经网络就是在构建神经元。
在神经网络中,激活值(activation value)是指某个神经元在接收到输入并经过加权求和(通常记作 z)后,通过激活函数 f 变换所得到的输出值,常用记号为 a。它的数学形式可以表示为:
加权和 z:把上一层神经元的输出 xi 按照各自的权重 wi加权后,再加上偏置项 b;
激活函数 f:对 z 进行非线性映射,得到激活值 a。
激活值可以引入非线性,如果网络只做加权求和(线性变换),无论层数多深,整体还是线性的,无法拟合复杂的非线性关系。通过激活函数引入非线性,网络才能学习并逼近各种复杂函数。
以下是神经网络中神经元的构建说明
同一层的多个神经元可以看作是通过并行计算来处理相同的输入数据,学习输入数据的不同特征。每个神经元可能会关注输入数据中的不同部分,从而捕捉到数据的不同属性。
接下来,我们使用多个神经元来构建神经网络,相邻层之间的神经元相互连接,并给每一个连接分配一个强度,如下图所示:
神经网络中信息只向一个方向移动,即从输入节点向前移动,通过隐藏节点,再向输出节点移动。其中的基本部分是:
输入层(Input Layer): 即输入x的那一层(如图像、文本、声音等)。每个输入特征对应一个神经元。输入层将数据传递给下一层的神经元。
输出层(Output Layer): 即输出y的那一层。输出层的神经元根据网络的任务(回归、分类等)生成最终的预测结果。
隐藏层(Hidden Layers): 输入层和输出层之间都是隐藏层,神经网络的“深度”通常由隐藏层的数量决定。隐藏层的神经元通过加权和激活函数处理输入,并将结果传递到下一层。
其特点是:
- 同一层的神经元之间没有连接
- 第N层的每个神经元和第N-1层的所有神经元相连(这就是Fully Connected的含义),这就是全连接神经网络(FCNN)
- 全连接神经网络接收的样本数据是二维的,数据在每一层之间需要以二维的形式传递
- 第N-1层神经元的输出就是第N层神经元的输入
- 每个连接都有一个权重值(w系数和b系数)
激活函数
Sigmoid 激活函数
激活函数公式:
激活函数求导公式:
sigmoid 激活函数的函数图像如下:
从 sigmoid 函数的图像可以看出,它能把任何输入值压缩到 0 到 1 之间。但当输入的值太小(比如小于 -6)或者太大(比如大于 6)时,sigmoid 的输出几乎固定在接近 0 或接近 1,这时候不管你输入什么值,输出差别都非常小。举个例子,输入 100 和 10000,它们之间相差 100 倍,但经过 sigmoid 函数处理后,结果几乎都是 1。这就造成了一个问题:虽然原始输入差很多,但输出几乎一样,这会导致神经网络“感知不到”这种差距,从而丢失了重要的信息。
对于sigmoid函数而言,输入值在[-6, 6]之间输出值才会有明显差异,输入值在[-3, 3]之间才会有比较好的效果
通过上述导数图像,我们发现导数数值范围是 (0, 0.25),当输入的值<-6或者>6时,sigmoid激活函数图像的导数接近为 0,此时网络参数将更新极其缓慢,或者无法更新。
一般来说,sigmoid网络在5层之内就会产生梯度消失现象。而且,该激活函数的激活值并不是以0为中心的,激活值总是偏向正数,导致梯度更新时,只会对某些特征产生相同方向的影响,所以在实践中这种激活函数使用的很少。sigmoid函数一般只用于二分类的输出层。
Tanh激活函数
激活函数公式:
激活函数求导公式:
Tanh 激活函数的函数图像如下:
由上面的函数图像可以看到,Tanh函数将输入映射到(-1, 1)之间,图像以0为中心,激活值在0点对称,当输入在 −3到 3 之间时,Tanh 的输出值在 −1 到 1 之间平滑变化;而当输入小于约 −3或大于约 3 时,函数值便迅速趋近于 −1或 1,进入饱和区。此外,Tanh 的导数值始终介于 0 和 1 之间:在输入接近 0 附近时,导数接近最大值 1;但当输入进入饱和区(<−3 或 >3)时,其导数会趋近于 0,从而造成梯度消失的现象。
与Sigmoid相比,它是以0为中心的,使得其收敛速度要比Sigmoid快,减少迭代次数。然而,从图中可以看出,Tanh两侧的导数也为0,同样会造成梯度消失。
若使用时可在隐藏层使用tanh函数,在输出层使用sigmoid函数。
ReLU 激活函数
激活函数公式:
激活函数求导公式:
ReLU 的函数图像、导数图像如下:
ReLU 激活函数将小于0的值映射为0,而大于0的值则保持不变,它更加重视正信号,而忽略负信号,这种激活函数运算更为简单,能够提高模型的训练效率。
当x<0时,ReLU导数为0,而当x>0时,则不存在饱和问题。所以,ReLU 能够在x>0时保持梯度不衰减,从而缓解梯度消失问题。然而,随着训练的推进,部分输入会落入小于0区域,导致对应权重无法更新。这种现象被称为“神经元死亡”。
ReLU是目前最常用的激活函数。与sigmoid相比,RELU的优势是:
采用sigmoid函数,计算量大(指数运算),反向传播求误差梯度时,计算量相对大;而采用Relu激活函数,整个过程的计算量节省很多
sigmoid函数反向传播时,很容易就会出现梯度消失的情况,从而无法完成深层网络的训练;而采用relu激活函数,当输入的值>0时,梯度为1,不会出现梯度消失的情况
Relu会使一部分神经元的输出为0,这样就造成了网络的稀疏性,并且减少了参数的相互依存关系,缓解了过拟合问题的发生
SoftMax激活函数
激活函数公式:
softmax用于多分类过程中,它是二分类函数sigmoid在多分类上的推广,目的是将多分类的结果以概率的形式展现出来。
参数初始化
我们在构建网络之后,网络中的参数是需要初始化的。我们需要初始化的参数主要有权重和偏置,偏置一般初始化为0即可,而对权重的初始化则会更加重要。
数初始化的作用:
防止梯度消失或爆炸:初始权重值过大或过小会导致梯度在反向传播中指数级增大或缩小。
提高收敛速度:合理的初始化使得网络的激活值分布适中,有助于梯度高效更新。
保持对称性破除:权重的初始化需要打破对称性,否则网络的学习能力会受到限制。
初始化方法 | 打破对称性 | 梯度稳定性 | 适用激活函数 | 适用网络深度 | 实现难度 |
---|---|---|---|---|---|
均匀分布 | ✅ | 一般;区间选得不当易梯度爆炸或消失 | 通用 | 浅层(≤5层) | 简单 |
正态分布 | ✅ | 一般;σ选得不当易梯度问题 | 通用 | 浅层(≤5层) | 简单 |
全 0 | ❌ | 均一;无法学习 | — | 不推荐 | 简单 |
全 1 | ❌ | 易爆炸;无法学习 | — | 调试用 | 简单 |
固定值 | ❌ | 依固定值大小;易爆炸/消失 | — | 调试用 | 简单 |
Xavier(Glorot) | ✅ | 好;保持输入输出方差一致 | Sigmoid、Tanh 等对称激活 | 深层(≥10层) | 中等 |
He(Kaiming) | ✅ | 优;专为 ReLU 家族设计 | ReLU、Leaky ReLU 等非对称激活 | 深层(≥10层) | 中等 |
import torch
import torch.nn as nn
# 均匀分布
def dm01():
linear1 = nn.Linear(in_features=10, out_features=10)
nn.init.uniform_(linear1.weight)
nn.init.uniform_(linear1.bias)
print(linear1.weight)
print(linear1.bias)
# 正态分布
def dm02():
linear1 = nn.Linear(in_features=10, out_features=10)
nn.init.normal_(linear1.weight)
nn.init.normal_(linear1.bias)
print(linear1.weight)
print(linear1.bias)
# 均匀分布随机初始化
def dm03():
linear = nn.Linear(5, 3)
# 从0-1均匀分布产生参数
nn.init.uniform_(linear.weight)
nn.init.uniform_(linear.bias)
print(linear.weight.data)
# 固定初始化
def dm04():
linear = nn.Linear(5, 3)
nn.init.constant_(linear.weight, 5)
print(linear.weight.data)
# 全0初始化
def dm05():
linear = nn.Linear(5, 3)
nn.init.zeros_(linear.weight)
print(linear.weight.data)
# 全1初始化
def dm06():
linear = nn.Linear(5, 3)
nn.init.ones_(linear.weight)
print(linear.weight.data)
# 正态分布随机初始化
def dm07():
linear = nn.Linear(5, 3)
nn.init.normal_(linear.weight, mean=0, std=1)
print(linear.weight.data)
# kaiming 初始化
def dm08():
# kaiming 正态分布初始化
linear = nn.Linear(5, 3)
nn.init.kaiming_normal_(linear.weight, nonlinearity='relu')
print(linear.weight.data)
# kaiming 均匀分布初始化
linear = nn.Linear(5, 3)
nn.init.kaiming_uniform_(linear.weight, nonlinearity='relu')
print(linear.weight.data)
# xavier 初始化
def dm09():
# xavier 正态分布初始化
linear = nn.Linear(5, 3)
nn.init.xavier_normal_(linear.weight)
print(linear.weight.data)
# xavier 均匀分布初始
linear = nn.Linear(5, 3)
nn.init.xavier_uniform_(linear.weight)
print(linear.weight.data)
if __name__ == "__main__":
dm01()
dm02()
dm03()
dm04()
dm05()
dm06()
dm07()
dm08()
dm09()
搭建一个简单的神经网络
构建如图所示的神经网络,其中:
第1个隐藏层:权重初始化采用标准化的xavier初始化 激活函数使用sigmoid
第2个隐藏层:权重初始化采用标准化的He初始化 激活函数采用relu
out输出层线性层 假若多分类,采用softmax做数据归一化
在pytorch中定义深度神经网络其实就是层堆叠的过程,继承自nn.Module,实现两个方法:
init方法中定义网络中的层结构,主要是全连接层,并进行初始化
forward方法,在调用神经网络模型对象的时候,底层会自动调用该函数。该函数中为初始化定义的layer传入数据,进行前向传播等。
这是这个神经网络的代码:
import torch.nn as nn
import torch
from torchsummary import summary
class ModelDemo(nn.Module):
def __init__(self):
super().__init__()
self.linear1 = nn.Linear(in_features=3, out_features=3)
self.linear2 = nn.Linear(in_features=3, out_features=2)
self.output = nn.Linear(in_features=2, out_features=2)
nn.init.xavier_normal_(self.linear1.weight)
nn.init.zeros_(self.linear1.bias)
nn.init.kaiming_normal_(self.linear2.weight,nonlinearity='relu')
nn.init.zeros_(self.linear2.bias)
def forward(self, x):
x = torch.sigmoid(self.linear1(x))
x = torch.relu(self.linear2(x))
x = torch.softmax(self.output(x),dim=1)
return x
def train():
my_model = ModelDemo()
data = torch.randn(size=(5,3))
print('data->',data)
output = my_model(data)
print('output->',output)
summary(my_model,input_size=(3,),batch_size=5)
for name,param in my_model.named_parameters():
print('name->',name,'param->',param)
if __name__ == "__main__":
train()
下面这是一个带有可视化的双特征三分类问题的代码:
from sklearn.datasets import make_classification
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import TensorDataset, DataLoader
from torchsummary import summary
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA
from sklearn.manifold import TSNE
import numpy as np
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
# 生成2特征的三分类数据
X, y = make_classification(
n_samples=150, # 样本数量
n_features=2, # 特征数量
n_classes=3, # 类别数
n_informative=2, # 信息特征数量
n_redundant=0, # 冗余特征数量
n_repeated=0, # 重复特征数量
n_clusters_per_class=1, # 每个类别的簇数
random_state=42 # 随机种子,保证可重复性
)
# 转换为PyTorch张量
X_tensor = torch.FloatTensor(X)
y_tensor = torch.LongTensor(y)
# 打印形状
print("X形状:", X_tensor.shape) # [150, 2]
print("y形状:", y_tensor.shape) # [150]
# 可视化原始数据
def visualize_data(X, y, title="原始数据分布"):
"""直接可视化2D数据"""
plt.figure(figsize=(10, 8))
# 绘制散点图
for i in range(3): # 3个类别
plt.scatter(X[y == i, 0], X[y == i, 1],
label=f'类别 {i}', alpha=0.7, s=50)
plt.title(title)
plt.xlabel('特征1')
plt.ylabel('特征2')
plt.legend()
plt.grid(True, linestyle='--', alpha=0.7)
plt.savefig(f'{title}.png')
plt.show()
return X, None
# 可视化原始数据
X_pca, _ = visualize_data(X, y)
class ModelDemo(nn.Module):
def __init__(self):
super().__init__()
self.linear1 = nn.Linear(in_features=2, out_features=4)
self.linear2 = nn.Linear(in_features=4, out_features=3)
self.output = nn.Linear(in_features=3, out_features=3)
nn.init.xavier_normal_(self.linear1.weight)
nn.init.zeros_(self.linear1.bias)
nn.init.kaiming_normal_(self.linear2.weight, nonlinearity='relu')
nn.init.zeros_(self.linear2.bias)
def forward(self, x):
x = torch.sigmoid(self.linear1(x))
x = torch.relu(self.linear2(x))
x = torch.softmax(self.output(x), dim=1)
return x
def train_model():
# 创建模型实例
model = ModelDemo()
# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.01)
# 创建数据集和数据加载器
dataset = TensorDataset(X_tensor, y_tensor)
dataloader = DataLoader(dataset, batch_size=15, shuffle=True)
# 训练参数
epochs = 100
losses = []
# 训练循环
for epoch in range(epochs):
epoch_loss = 0
for batch_X, batch_y in dataloader:
# 前向传播
outputs = model(batch_X)
loss = criterion(outputs, batch_y)
# 反向传播和优化
optimizer.zero_grad()
loss.backward()
optimizer.step()
epoch_loss += loss.item()
# 记录每个epoch的平均损失
avg_loss = epoch_loss / len(dataloader)
losses.append(avg_loss)
# 每10个epoch打印一次损失
if (epoch+1) % 10 == 0:
print(f'Epoch [{epoch+1}/{epochs}], Loss: {avg_loss:.4f}')
# 绘制损失曲线
plt.figure(figsize=(10, 5))
plt.plot(losses)
plt.title('训练损失')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.grid(True)
plt.savefig('training_loss.png')
plt.show()
return model
def evaluate_model(model):
# 设置为评估模式
model.eval()
# 使用模型进行预测
with torch.no_grad():
outputs = model(X_tensor)
_, predicted = torch.max(outputs, 1)
# 计算准确率
correct = (predicted == y_tensor).sum().item()
total = y_tensor.size(0)
accuracy = correct / total
print(f'准确率: {accuracy * 100:.2f}%')
print(f'正确预测: {correct}/{total}')
# 打印混淆矩阵
from sklearn.metrics import confusion_matrix, classification_report
cm = confusion_matrix(y_tensor.numpy(), predicted.numpy())
print("混淆矩阵:")
print(cm)
print("\n分类报告:")
print(classification_report(y_tensor.numpy(), predicted.numpy()))
# 可视化预测结果
visualize_predictions(X, y_tensor.numpy(), predicted.numpy())
return predicted
def visualize_predictions(X, y_true, y_pred):
"""可视化模型预测与真实标签的对比"""
# 创建一个2x1的子图布局
fig, axes = plt.subplots(1, 2, figsize=(16, 7))
# 绘制真实标签
for i in range(3):
axes[0].scatter(X[y_true == i, 0], X[y_true == i, 1],
label=f'类别 {i}', alpha=0.7, s=50)
axes[0].set_title('真实标签')
axes[0].set_xlabel('特征1')
axes[0].set_ylabel('特征2')
axes[0].legend()
axes[0].grid(True, linestyle='--', alpha=0.7)
# 绘制预测标签
for i in range(3):
axes[1].scatter(X[y_pred == i, 0], X[y_pred == i, 1],
label=f'预测类别 {i}', alpha=0.7, s=50)
axes[1].set_title('模型预测')
axes[1].set_xlabel('特征1')
axes[1].set_ylabel('特征2')
axes[1].legend()
axes[1].grid(True, linestyle='--', alpha=0.7)
# 保存和显示图像
plt.tight_layout()
plt.savefig('predictions_comparison.png')
plt.show()
# 可视化分类错误的样本
plt.figure(figsize=(10, 8))
# 正确分类的样本
correct_mask = y_true == y_pred
plt.scatter(X[correct_mask, 0], X[correct_mask, 1],
c=y_true[correct_mask], marker='o', alpha=0.6, s=50, label='正确分类')
# 错误分类的样本
error_mask = y_true != y_pred
plt.scatter(X[error_mask, 0], X[error_mask, 1],
c=y_true[error_mask], marker='X', edgecolors='red', s=100, label='错误分类')
plt.title('分类结果分析')
plt.xlabel('特征1')
plt.ylabel('特征2')
plt.legend()
plt.grid(True, linestyle='--', alpha=0.7)
plt.savefig('classification_errors.png')
plt.show()
def predict_new_data(model, new_data):
# 确保模型处于评估模式
model.eval()
# 转换输入数据为张量
if not isinstance(new_data, torch.Tensor):
new_data = torch.FloatTensor(new_data)
# 使用模型进行预测
with torch.no_grad():
outputs = model(new_data)
probabilities = outputs.numpy()
_, predicted = torch.max(outputs, 1)
# 可视化新数据的预测
visualize_new_predictions(new_data.numpy(), predicted.numpy())
return predicted.numpy(), probabilities
def visualize_new_predictions(new_samples, predictions):
"""可视化新样本的预测结果"""
# 可视化原始数据点和新样本
plt.figure(figsize=(10, 8))
# 绘制原始数据
for i in range(3):
mask = y_tensor.numpy() == i
plt.scatter(X[mask, 0], X[mask, 1],
alpha=0.3, s=30, label=f'原始类别 {i}')
# 绘制新样本
for i in range(3):
mask = predictions == i
if np.any(mask):
plt.scatter(new_samples[mask, 0], new_samples[mask, 1],
marker='*', s=200, edgecolors='black', linewidth=1.5,
label=f'新样本 - 预测类别 {i}')
plt.title('新样本的预测结果')
plt.xlabel('特征1')
plt.ylabel('特征2')
plt.legend()
plt.grid(True, linestyle='--', alpha=0.7)
plt.savefig('new_samples_predictions.png')
plt.show()
if __name__ == "__main__":
# 训练模型
trained_model = train_model()
# 评估模型
predictions = evaluate_model(trained_model)
# 使用模型进行新数据预测的示例
# 生成5个随机样本作为新数据
new_samples = np.random.randn(5, 2)
print("\n预测新数据:")
print("新样本:", new_samples)
pred_classes, pred_probs = predict_new_data(trained_model, new_samples)
print("预测类别:", pred_classes)
print("预测概率:", pred_probs)
损失函数
在深度学习中, 损失函数是用来衡量模型参数质量的函数, 衡量的方式是比较网络输出(预测值)和真实输出(真实值)的差异。
模型通过最小化损失函数的值来调整参数,使其输出更接近真实值。
损失函数在不同的文献中名称是不一样的,主要有这几种叫法:损失函数、代价函数、目标函数、误差函数
损失函数作用:
评估性能:反映模型预测结果与目标值的匹配程度。
指导优化:通过梯度下降等算法最小化损失函数,优化模型参数。
多分类任务损失函数
Softmax 损失(又称 Softmax + 交叉熵损失)是多分类任务中最常用的目标函数。
Softmax 激活:将网络最后一层的实数输出转化为各类别的概率分布。
交叉熵(Cross‑Entropy)损失:衡量预测概率分布与真实分布之间的差异。
其中Softmax激活在前面已经写出,其主要公式为:
如果是one-hot标签,真实类别 c 对应 y_c=1,其余 y_j=0,则交叉熵损失公式为:
合起来的Softmax损失就是:
举个例子,在如下的神经网络结果中:
交叉熵损失为:
从概率角度理解,我们的目的是最小化正确类别所对应的预测概率的对数的负值(损失值最小),如下图所示:
在PyTorch中使用nn.CrossEntropyLoss()
实现,如下所示:
import torch
from torch import nn
# 分类损失函数:交叉熵损失使用nn.CrossEntropyLoss()实现。nn.CrossEntropyLoss()=softmax+损失计算
def test01():
# 设置真实值: 可以是热编码后的结果也可以不进行热编码
# y_true = torch.tensor([[0, 1, 0], [0, 0, 1]], dtype=torch.float32)
# 注意:类型必须是64位整型数据
y_true = torch.tensor([1, 2], dtype=torch.int64)
y_pred = torch.tensor([[0.2, 0.6, 0.2], [0.1, 0.8, 0.1]], requires_grad=True, dtype=torch.float32)
# 实例化交叉熵损失,默认求平均损失
# reduction='sum':总损失
loss = nn.CrossEntropyLoss()
# 计算损失结果
my_loss = loss(y_pred, y_true).detach().numpy()
print('loss:', my_loss)
二分类任务损失函数
在处理二分类任务时,我们不再使用softmax激活函数,而是使用sigmoid激活函数,那损失函数也相应的进行调整,使用二分类的交叉熵损失函数:
其图像如下图所示:
在PyTorch中实现时使用nn.BCELoss()
实现,如下所示:
import torch
from torch import nn
def test02():
# 1 设置真实值和预测值
y_true = torch.tensor([0, 1, 0], dtype=torch.float32)
# 预测值是sigmoid输出的结果
y_pred = torch.tensor([0.6901, 0.5459, 0.2469], requires_grad=True)
# 2 实例化二分类交叉熵损失
loss = nn.BCELoss()
# 3 计算损失
my_loss = loss(y_pred, y_true).detach().numpy()
print('loss:', my_loss)
回归任务损失函数
MAE损失函数
mean absolute loss(MAE)也被称为L1 Loss,是以绝对误差作为距离
损失函数公式:
曲线如下图所示:
MAE损失函数的特点是:
由于L1 loss具有稀疏性,为了惩罚较大的值,因此常常将其作为正则项添加到其他loss中作为约束。(0点不可导, 产生稀疏矩阵)
L1 loss的最大问题是梯度在零点不平滑,导致会跳过极小值
适用于回归问题中存在异常值或噪声数据时,可以减少对离群点的敏感性
MSE损失函数
Mean Squared Loss/ Quadratic Loss(MSE loss)也被称为L2 loss,或欧氏距离,它以误差的平方和的均值作为距离
损失函数公式:
曲线如下图所示:
特点是:
L2 loss也常常作为正则项,对于离群点(outliers)敏感,因为平方项会放大大误差
当预测值与目标值相差很大时, 梯度容易爆炸
梯度爆炸:网络层之间的梯度(值大于1.0)重复相乘导致的指数级增长会产生梯度爆炸
适用于大多数标准回归问题,如房价预测、温度预测等
Smooth L1损失函数
smooth L1说的是光滑之后的L1,是一种结合了均方误差(MSE)和平均绝对误差(MAE)优点的损失函数。它在误差较小时表现得像 MSE,在误差较大时则更像 MAE。
Smooth L1损失函数如下式所示:
其中:x=f(x)−y 为真实值和预测值的差值。
从上图中可以看出,该函数实际上就是一个分段函数
在[-1,1]之间实际上就是L2损失,这样解决了L1的不光滑问题
在[-1,1]区间外,实际上就是L1损失,这样就解决了离群点梯度爆炸的问题
特点是:
对离群点更加鲁棒:当误差较大时,损失函数会线性增加(而不是像MSE那样平方增加),因此它对离群点的惩罚更小,避免了MSE对离群点过度敏感的问题
计算梯度时更加平滑:与MAE相比,Smooth L1在小误差时表现得像MSE,避免了在训练过程中因使用绝对误差而导致的梯度不连续问题
神经网络优化方法
梯度下降算法回顾
梯度下降算法是一种寻找最优网络参数的策略,计算每次损失值对应的梯度,进行参数更新
- BGD,使用全部样本计算梯度,计算量大
- SGD,使用全部样本中随机一个样本计算梯度,梯度可能不合理
- Min-Batch,使用一批样本计算梯度,计算量相对小,梯度更加合理
反向传播(BP算法)
利用反向传播算法对神经网络进行训练。该方法与梯度下降算法相结合,对网络中所有权重计算损失函数的梯度,并利用梯度值来更新权值以最小化损失函数。
前向传播:指的是数据输入到神经网络中,逐层向前传输,一直运算到输出层为止。
反向传播(Back Propagation):利用损失函数ERROR值,从后往前,结合梯度下降算法,依次求各个参数的偏导,并进行参数更新。
反向传播算法利用链式法则对神经网络中的各个节点的权重进行更新。
【举个栗子🌰:】
如下图是一个简单的神经网络用来举例:激活函数为sigmoid
前向传播运算:
接下来是反向传播(求网络误差对各个权重参数的梯度):
我们先来求最简单的,求误差E对w5的导数。首先明确这是一个链式法则的求导过程,要求误差E对w5的导数,需要先求误差E对out_{o1}的导数,再求out_{o1}对net_{o1}的导数,最后再求net_{o1}对w_5的导数,经过这个链式法则,我们就可以求出误差E对w_5的导数(偏导),如下图所示:
导数(梯度)已经计算出来了,下面就是反向传播与参数更新过程:
如果要想求误差E对w1的导数,误差E对w1的求导路径不止一条,这会稍微复杂一点,但换汤不换药,计算过程如下所示:
梯度下降优化方法
梯度下降优化算法中,可能会碰到以下情况:
碰到平缓区域,梯度值较小,参数优化变慢
碰到 “鞍点” ,梯度为0,参数无法优化
碰到局部最小值,参数不是最优
对于这些问题, 出现了一些对梯度下降算法的优化方法,例如:Momentum、AdaGrad、RMSprop、Adam 等
指数加权平均
指数移动加权平均则是参考各数值,并且各数值的权重都不同,距离越远的数字对平均数计算的贡献就越小(权重较小),距离越近则对平均数的计算贡献就越大(权重越大)。
比如:明天气温怎么样,和昨天气温有很大关系,而和一个月前的气温关系就小一些。
计算公式可以用下面的式子来表示:
动量算法Momentum
当梯度下降碰到 “峡谷” 、”平缓”、”鞍点” 区域时, 参数更新速度变慢。 Momentum 通过指数加权平均法,累计历史梯度值,进行参数更新,越近的梯度值对当前参数更新的重要性越大。
optimizer = torch.optim.SGD([w], lr=0.01, momentum=0.9)代码只需要加一个参数momentum就能实现
AdaGrad
AdaGrad 通过对不同的参数分量使用不同的学习率,AdaGrad 的学习率总体会逐渐减小,这是因为 AdaGrad 认为:在起初时,我们距离最优目标仍较远,可以使用较大的学习率,加快训练速度,随着迭代次数的增加,学习率逐渐下降。
AdaGrad 缺点是可能会使得学习率过早、过量的降低,导致模型训练后期学习率太小,较难找到最优解。
optimizer = torch.optim.Adagrad([w], lr=0.01)代码使用adagrad优化方法
RMSProp
RMSProp 优化算法是对 AdaGrad 的优化。最主要的不同是,其使用指数加权平均梯度替换历史梯度的平方和。
RMSProp 与 AdaGrad 最大的区别是对梯度的累积方式不同,对于每个梯度分量仍然使用不同的学习率。
RMSProp 通过引入衰减系数β,控制历史梯度对历史梯度信息获取的多少. 被证明在神经网络非凸条件下的优化更好,学习率衰减更加合理一些。
需要注意的是:AdaGrad 和 RMSProp 都是对于不同的参数分量使用不同的学习率,如果某个参数分量的梯度值较大,则对应的学习率就会较小,如果某个参数分量的梯度较小,则对应的学习率就会较大一些。
optimizer = torch.optim.RMSprop([w], lr=0.01, alpha=0.9)其中控制衰减系数是alpha参数
Adam
Momentum 使用指数加权平均计算当前的梯度值
AdaGrad、RMSProp 使用自适应的学习率
Adam优化算法(Adaptive Moment Estimation,自适应矩估计)将 Momentum 和 RMSProp 算法结合在一起
修正梯度: 使⽤梯度的指数加权平均
修正学习率: 使⽤梯度平⽅的指数加权平均
原理:Adam 是结合了 Momentum 和 RMSProp 优化算法的优点的自适应学习率算法。它计算了梯度的一阶矩(平均值)和二阶矩(梯度的方差)的自适应估计,从而动态调整学习率。
梯度计算公式:
$m_t = \beta_1 m_{t-1} + (1 – \beta_1)\, g_t$
$s_t = \beta_2 s_{t-1} + (1 – \beta_2)\, g_t^2$
$\hat{m}_t = \frac{m_t}{1 – \beta_1^t}, \quad\hat{s}_t = \frac{s_t}{1 – \beta_2^t}$
权重参数更新公式:
$w_t = w_{t-1} – \frac{\eta}{\sqrt{\hat{s}_t} + \epsilon}\, \hat{m}_t$
其中,$m_t$ 是梯度的一阶矩估计,$s_t$ 是梯度的二阶矩估计,$ \hat{m_t}$和 $\hat{s_t}$ 是偏差校正后的估计。
小结
优化算法 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
SGD | 简单、容易实现。 | 收敛速度较慢,容易震荡,特别是在复杂问题中。 | 用于简单任务,或者当数据特征分布相对稳定时。 |
Momentum | 可以加速收敛,减少震荡,特别是在高曲率区域。 | 需要手动调整动量超参数,可能会在小步长训练中过度更新。 | 用于非平稳优化问题,尤其是深度学习中的应用。 |
AdaGrad | 自适应调整学习率,适用于稀疏数据。 | 学习率会在训练过程中逐渐衰减,可能导致早期停滞。 | 适合稀疏数据,如 NLP 或推荐系统中的特征。 |
RMSProp | 解决了 AdaGrad 学习率过早衰减的问题,适应性强。 | 需要选择合适的超参数,更新可能会过于激进。 | 适用于动态问题、非平稳目标函数,如深度学习训练。 |
Adam | 结合了 Momentum 和 RMSProp 的优点,适应性强且稳定。 | 需要调节更多的超参数,训练过程中可能会产生较大波动。 | 广泛适用于各种深度学习任务,特别是非平稳和复杂问题。 |
简单任务和较小的模型:SGD 或 Momentum
复杂任务或有大量数据:Adam 是最常用的选择,因其在大部分任务上都表现优秀
需要处理稀疏数据或文本数据:Adagrad 或 RMSProp
学习率衰减优化方法
为什么要进行学习率优化
在训练神经网络时,一般情况下学习率都会随着训练而变化。这主要是由于,在神经网络训练的后期,如果学习率过高,会造成loss的振荡,但是如果学习率减小的过慢,又会造成收敛变慢的情况。
运行下面代码,观察学习率设置不同对网络训练的影响:
等间隔学习率衰减
“等间隔学习率衰减”(也常被称为Step Decay)是一种在训练过程中按固定训练周期(epoch)将学习率降低的策略。其核心思想是,每隔预设的若干个 epoch,就将当前学习率按一定比例(比如 0.1 倍、0.5 倍等)衰减一次,以帮助模型在训练后期以更“小步伐”细致地搜索最优解。
# step_size:调整间隔数=50
# gamma:调整系数=0.5
# 调整方式:lr = lr * gamma
optim.lr_scheduler.StepLR(optimizer, step_size, gamma=0.1)
指定间隔学习率衰减
“指定隔学习率衰减”是一种在训练过程中按预先指定的若干时刻(或步数)来降低学习率的策略,也常被称为多步(Multi-Step)衰减或里程碑(Piecewise Constant)衰减。与“等间隔衰减”每隔固定周期衰减不同,指定隔衰减允许你灵活地在任意 Epoch(或迭代)点降低学习率,以配合验证集表现或经验先验。
# milestones:设定调整轮次:[50, 125, 160]
# gamma:调整系数
# 调整方式:lr = lr * gamma
optim.lr_scheduler.MultiStepLR(optimizer, milestones, gamma=0.1, last_epoch=-1)
按指数学习率衰减
按指数衰减(Exponential Decay)核心思想是让学习率随训练过程呈指数级地平滑下降,从而在训练初期保持较大学习率加速收敛,后期以更小步长精细调整参数。
# gamma:指数的底
# 调整方式
# lr= lr∗gamma^epoch
optim.lr_scheduler.ExponentialLR(optimizer, gamma)
小结
方法 | 等间隔学习率衰减 (Step Decay) | 指定间隔学习率衰减 (Exponential Decay) | 指数学习率衰减 (Exponential Moving Average Decay) |
---|---|---|---|
衰减方式 | 固定步长衰减 | 指定步长衰减 | 平滑指数衰减,历史平均考虑 |
实现难度 | 简单易实现 | 相对简单,容易调整 | 需要额外历史计算,较复杂 |
适用场景 | 大型数据集、较为简单的任务 | 对训练平稳性要求较高的任务 | 高精度训练,避免过快收敛 |
优点 | 直观,易于调试,适用于大批量数据 | 易于调试,稳定训练过程 | 平滑且考虑历史更新,收敛稳定性较强 |
缺点 | 学习率变化较大,可能跳过最优点 | 在某些情况下可能衰减过快,导致优化提前停滞 | 超参数调节较为复杂,可能需要更多的计算资源 |
正则化方法
什么是正则化
在设计机器学习算法时希望在新样本上的泛化能力强。许多机器学习算法都采用相关的策略来减小测试误差,这些策略被统称为正则化
神经网络强大的表示能力经常遇到过拟合,所以需要使用不同形式的正则化策略
目前在深度学习中使用较多的策略有范数惩罚,DropOut,特殊的网络层等,接下来我们对其进行详细的介绍
Dropout正则化
在训练深层神经网络时,由于模型参数较多,在数据量不足的情况下,很容易过拟合。Dropout(中文翻译成随机失活)是一个简单有效的正则化方法。
在训练过程中,Dropout的实现是让神经元以超参数p(丢弃概率)的概率停止工作或者激活被置为0,未被置为0的进行缩放,缩放比例为1/(1-p)。训练过程可以认为是对完整的神经网络的一些子集进行训练,每次基于输入数据只更新子网络的参数
在实际应用中,Dropout参数p的概率通常取值在0.2到0.5之间
– 对于较小的模型或较复杂的任务,丢弃率可以选择0.3或更小
– 对于非常深的网络,较大的丢弃率(如0.5或0.6)可能会有效防止过拟合
– 实际应用中,通常会在全连接层(激活函数后)之后添加Dropout层
在测试过程中,随机失活不起作用
– 在测试阶段,使用所有的神经元进行预测,以获得更稳定的结果
– 直接使用训练好的模型进行测试,由于所有的神经元都参与计算,输出的期望值会比训练阶段高。测试阶段的期望输出是 E[x_test] = x
– 测试/推理模式:model.eval()
在引入 Dropout 后,网络中神经元的“有效数量”在训练和测试阶段是不一样的——训练时每个神经元有概率 ppp 被“丢弃”,测试时则全部保留。为了让训练和测试阶段的激活(activation)尺度一致,我们必须对输出进行缩放。下面分两种方式来说明。
一、测试阶段缩放(Standard Dropout)
二、训练时缩放(Inverted Dropout)
总而言之,缩放就是为了保持训练和测试时神经元激活的期望一致,避免测试时激活值偏大或偏小,保证模型在推理阶段表现稳定。
两种实现方式:
在测试时统一乘以$q$
在训练时就把激活除以$q$(Inverted Dropout),测试时无需再缩放。
我们通过一段代码观察下dropout的效果:
import torch
import torch.nn as nn
# 初始化随机失活层
dropout = nn.Dropout(p=0.1)
# 初始化输入数据:表示某一层的weight信息
inputs = torch.randint(0, 10, size=[1, 4]).float()
layer = nn.Linear(4,5)
y = layer(inputs)
y = torch.relu(y)
print("未失活FC层的输出结果:\n", y)
y = dropout(y)
print("失活后FC层的输出结果:\n", y)
输出结果:
未失活FC层的输出结果:
tensor([[0.0000, 0.0000, 3.5438, 4.1741, 2.9888]], grad_fn=<ReluBackward0>)
失活后FC层的输出结果:
tensor([[0.0000, 0.0000, 3.9376, 4.6379, 0.0000]], grad_fn=<MulBackward0>)
批量归一正则化(Batch Normalization)
在神经网络的训练过程中,流经网络的数据都是一个batch,每个batch之间的数据分布变化非常剧烈,这就使得网络参数频繁的进行大的调整以适应流经网络的不同分布的数据,给模型训练带来非常大的不稳定性,使得模型难以收敛。如果我们对每一个batch的数据进行标准化之后,数据分布就变得稳定,参数的梯度变化也变得稳定,有助于加快模型的收敛。
通过标准化每一层的输入,使其均值接近0,方差接近1,从而加速训练并提高泛化能力。
先对数据标准化,再对数据重构(缩放+平移),写成公式如下所示:
λ和β是可学习的参数,它相当于对标准化后的值做了一个线性变换,λ为系数,β为偏置;eps 通常指为 1e-5,避免分母为 0;E(x) 表示变量的均值;Var(x) 表示变量的方差;
批量归一化的作用:
加速收敛:固定了每层输入的分布后,网络能够更稳定地更新参数,常常可将学习率调得更大,从而更快收敛。
减轻对初始化的依赖:不需要对参数做过于精细的初始化,批量归一化能在一定程度上缓冲不良初始化带来的影响。
一定的正则化效果:由于每个 mini‐batch 的统计量有噪声(batch 之间差异),在一定程度上对模型起到类似 Dropout 的正则化作用,能减少过拟合。
更深的网络结构:在没有批量归一化的情况下,网络层数过深容易训练失败;加入 BatchNorm 后,深度网络训练变得可行。