Datawhale AI 夏令营(第五期)——向李宏毅学深度学习(进阶)——2

Task2:《深度学习详解》笔记

提示:此处为作者对学习内容的简析和总结



提示:以下是本篇文章正文内容

自适应学习率:深度学习优化的关键

自适应学习率是一种通过自动调整学习率来改善深度学习模型训练的技术。传统的梯度下降方法中,学习率是一个固定的值。然而,深度学习模型中的参数在不同的维度上可能有不同的灵敏度,固定的学习率难以满足所有参数的优化需求。为解决这一问题,自适应学习率算法应运而生,它能根据每个参数的梯度信息动态调整学习率,从而提高训练效率和模型性能。

传统梯度下降的局限性

在传统的梯度下降方法中,学习率 𝜂决定了参数更新的步伐大小。若学习率过大,可能会导致参数更新幅度过大,从而跳过全局最优解;若学习率过小,则会导致训练时间过长,甚至陷入局部最优解。具体来说,梯度下降的更新公式如下:
θ t + 1 = θ t − η ⋅ g t \theta_{t+1} = \theta_t - \eta \cdot g_t θt+1=θtηgt
其中,𝜃𝑡表示在第 𝑡次迭代时的参数值,𝑔𝑡为损失函数关于参数的梯度。

AdaGrad 的创新

AdaGrad 的提出是为了应对梯度下降中不同参数对学习率的不同需求。AdaGrad 会根据每个参数的历史梯度信息,动态调整学习率,使得学习率随着训练过程的进行而逐渐减小。其更新公式为:
θ t + 1 ← θ t − η G t + ϵ ⋅ g t \theta_{t+1} \leftarrow \theta_t - \frac{\eta}{\sqrt{G_t + \epsilon}} \cdot g_t θt+1θtGt+ϵ ηgt

其中,𝐺𝑡是到当前迭代为止,参数的梯度平方和,𝜖是一个小的常数,用于避免分母为零的情况。通过这种方式,AdaGrad 能够在训练早期以较大的步伐更新参数,而在训练后期,随着梯度的逐渐减小,学习率也随之减小,从而使得训练过程更为平稳。

然而,AdaGrad 也存在其局限性——由于累积的梯度平方和𝐺𝑡会持续增大,这可能导致学习率在训练后期过于减小,从而阻碍模型继续优化。

RMSProp 与 Adam 的改进

MSProp 是对 AdaGrad 的改进,它通过引入一个衰减系数𝛼来控制历史梯度的累积效果,使得算法能够适应长时间的训练。RMSProp 的更新公式为:
θ t + 1 ← θ t − η α ⋅ G t − 1 + ( 1 − α ) ⋅ g t 2 + ϵ ⋅ g t \theta_{t+1} \leftarrow \theta_t - \frac{\eta}{\sqrt{\alpha \cdot G_{t-1} + (1-\alpha) \cdot g_t^2 + \epsilon}} \cdot g_t θt+1θtαGt1+(1α)gt2+ϵ ηgt
相比于 AdaGrad,RMSProp 不会让学习率过快减小,因此在训练较长时间的情况下,能够保持较好的优化效果。

Adam 是一种结合了 RMSProp 和动量法的优化算法,其既考虑了梯度的大小,也考虑了梯度的方向。Adam 的更新过程包含两个部分:首先计算梯度的一阶矩估计(动量)和二阶矩估计(均方根),然后使用这两个估计值来动态调整学习率。具体公式为:
m t = β 1 ⋅ m t − 1 + ( 1 − β 1 ) ⋅ g t m_t = \beta_1 \cdot m_{t-1} + (1 - \beta_1) \cdot g_t mt=β1mt1+(1β1)gt
v t = β 2 ⋅ v t − 1 + ( 1 − β 2 ) ⋅ g t 2 v_t = \beta_2 \cdot v_{t-1} + (1 - \beta_2) \cdot g_t^2 vt=β2vt1+(1β2)gt2
m t ^ = m t 1 − β 1 t , v t ^ = v t 1 − β 2 t \hat{m_t} = \frac{m_t}{1 - \beta_1^t}, \quad \hat{v_t} = \frac{v_t}{1 - \beta_2^t} mt^=1β1tmt,vt^=1β2tvt
θ t + 1 ← θ t − η ⋅ m t ^ v t ^ + ϵ \theta_{t+1} \leftarrow \theta_t - \frac{\eta \cdot \hat{m_t}}{\sqrt{\hat{v_t}} + \epsilon} θt+1θtvt^ +ϵηmt^
Adam 的这种设计使其在面对不同类型的损失函数时,都能够表现出良好的收敛性和鲁棒性。

总结

自适应学习率是一种在深度学习模型训练中通过自动调整学习率以适应不同参数需求的优化方法。传统的固定学习率在所有参数上都使用同样的更新步伐,但在实际应用中,模型的不同参数可能表现出不同的灵敏度,因此需要不同的学习率来有效地优化。自适应学习率方法如 AdaGrad、RMSProp 和 Adam,通过跟踪每个参数的历史梯度信息,动态调整学习率,从而使训练过程更加高效。尤其是 Adam 算法,它结合了一阶动量和二阶矩估计,不仅考虑了当前梯度的大小,还综合考虑了历史梯度的影响,从而在复杂的损失函数表面上实现了更稳定和快速的收敛。

代码实现

下面的代码实现了一个简单的全连接神经网络,使用 Adam 优化器训练


import torch
import torch.nn as nn
import torch.optim as optim

# 定义一个简单的全连接神经网络
class SimpleNN(nn.Module):
    def __init__(self):
        super(SimpleNN, self).__init__()
        self.fc1 = nn.Linear(784, 128)  
        self.fc2 = nn.Linear(128, 10)   

    def forward(self, x):
        x = torch.relu(self.fc1(x))  # 使用 ReLU 激活函数
        x = self.fc2(x)              
        return x

# 初始化模型、损失函数和优化器
model = SimpleNN()
criterion = nn.CrossEntropyLoss()  # 使用交叉熵损失函数
optimizer = optim.Adam(model.parameters(), lr=0.001)  # 使用Adam优化器,学习率为0.001

# 训练循环
for epoch in range(10):  # 训练10个epoch
    optimizer.zero_grad()  # 清除梯度
    inputs = torch.randn(64, 784)  # 输入数据,大小为64x784
    labels = torch.randint(0, 10, (64,))  # 标签,范围在0到9

    outputs = model(inputs)  # 前向传播计算输出
    loss = criterion(outputs, labels)  # 计算损失值
    loss.backward()  # 反向传播计算梯度
    optimizer.step()  # 使用Adam优化器更新模型参数

    print(f'Epoch [{epoch+1}/10], Loss: {loss.item():.4f}')  # 输出当前epoch的损失值
    

分类:深度学习中的基本任务

分类任务是机器学习和深度学习中的核心问题之一。其目标是将输入数据正确地分配到预定义的类别中。分类问题广泛应用于图像识别、语音识别、自然语言处理等领域。

分类与回归的对比

虽然分类和回归都是监督学习任务,但它们的目标不同。回归是预测一个连续值,如房价预测;而分类则是预测一个离散的标签,如判断一张图片中是猫还是狗。尽管如此,分类任务也可以通过回归模型来解决,比如将类别编码为数字,然后使用回归模型进行预测。

然而,由于分类任务中的类别之间并不存在数值关系(如 1 和 2 并不意味着比 2 和 3 更接近),所以在处理分类任务时,通常使用独热编码(One-Hot Encoding)来表示类别。比如,对于一个三分类问题,可以用 [1,0,0]、[0,1,0] 和 [0,0,1] 分别表示三种类别。这种编码方式避免了类别之间的顺序性关系对结果的影响。

Softmax 在分类中的应用

在多分类问题中,网络的输出往往通过 Softmax 函数进行归一化,得到各类别的概率分布。Softmax 函数能够将任意实数的输入转化为 0 到 1 之间的概率值,并且所有输出的和为 1。其计算公式为:
y i ′ = exp ⁡ ( y i ) ∑ j exp ⁡ ( y j ) y'_i = \frac{\exp(y_i)}{\sum_j \exp(y_j)} yi=jexp(yj)exp(yi)
Softmax 的作用不仅在于将输出值归一化,还能放大概率之间的差距,使得最大的输出对应的类别更加突出。这在实际分类任务中非常重要,因为我们通常希望模型的输出具有明确的指向性。

分类损失函数的选择

在分类任务中,交叉熵损失(Cross-Entropy Loss)是最常用的损失函数。它直接衡量了预测分布与真实分布之间的差距,损失越小,说明模型的预测越接近真实标签。交叉熵的公式为:

e = − ∑ i y i ln ⁡ y i ′ e = -\sum_i y_i \ln y'_i e=iyilnyi
交叉熵的优化目标是最大化模型预测类别的概率,从而最小化分类误差。相比之下,均方误差(Mean Squared Error, MSE)在分类任务中的表现往往不如交叉熵,因为 MSE 在处理概率分布时,无法提供清晰的梯度方向,尤其是在输出值接近 0 或 1 时。

使用交叉熵的优势

交叉熵在梯度下降过程中具有明确的梯度方向,使得模型能够更快地收敛到全局最优解。尤其是在深度神经网络的训练中,交叉熵能够避免损失函数表面平坦区域带来的困境,从而有效地引导模型参数的更新。这也是为什么在大多数分类问题中,交叉熵损失被广泛使用的原因。

总结

分类任务在深度学习中是将输入数据分配到预定义的类别中的过程。与回归不同,分类问题处理的是离散的标签,而非连续的数值。为了有效处理多分类问题,通常采用独热编码将类别标签转换为二进制向量,这避免了类别之间的顺序关系可能带来的误导。在此基础上,Softmax 函数将模型的输出转化为概率分布,使得各类别的输出可以直接解释为样本属于各个类别的概率。交叉熵损失函数则通过衡量预测分布与真实分布之间的差异,提供了一个清晰的优化目标,使得模型能够更有效地学习到正确的分类边界。

代码实现

下面的代码实现一个多分类任务的神经网络,使用 Softmax 和交叉熵损失函数来进行模型优化


import torch
import torch.nn as nn
import torch.optim as optim

# 定义一个简单的全连接神经网络
class SimpleClassifier(nn.Module):
    def __init__(self):
        super(SimpleClassifier, self).__init__()
        self.fc1 = nn.Linear(784, 128)  
        self.fc2 = nn.Linear(128, 10)   

    def forward(self, x):
        x = torch.relu(self.fc1(x))  # 使用 ReLU 激活函数
        x = self.fc2(x)              
        return x

# 初始化模型、损失函数和优化器
model = SimpleClassifier()
criterion = nn.CrossEntropyLoss()  # 使用交叉熵损失函数,用于多分类任务
optimizer = optim.SGD(model.parameters(), lr=0.01)  # 使用随机梯度下降(SGD)优化器,学习率为 0.01

# 训练循环
for epoch in range(10):  # 训练10 个epoch
    optimizer.zero_grad()  # 清除梯度
    inputs = torch.randn(64, 784)  # 输入数据,大小为64x784
    labels = torch.randint(0, 10, (64,))  # 标签,范围在0到9

    outputs = model(inputs)  # 前向传播计算输出
    loss = criterion(outputs, labels)  # 计算损失值
    loss.backward()  # 反向传播计算梯度
    optimizer.step()  # 使用SGD优化器更新模型参数

    print(f'Epoch [{epoch+1}/10], Loss: {loss.item():.4f}')  # 输出当前epoch的损失值

实践作业

平台的实践代码依次完成了深度神经网络训练的各个步骤。从导入必要的库和工具包开始,对数据进行准备与预处理。接着,定义了卷积神经网络的模型结构,设置了损失函数和优化器。随后,代码通过训练过程优化模型参数,并在训练结束后评估模型性能。最后,使用训练好的模型进行预测,并通过可视化展示了模型的特征提取效果。

此处为部分代码,代码注释在平台通义千问的基础上,由个人和gpt进行了优化


# 导入必要的库和模块
import torch
import numpy as np
from sklearn.manifold import TSNE
import matplotlib.pyplot as plt
from tqdm import tqdm
import matplotlib.cm as cm
import torch.nn as nn

# 根据CUDA是否可用选择执行设备
device = 'cuda' if torch.cuda.is_available() else 'cpu'

# 加载训练好的模型
model = Classifier().to(device)  # 将模型加载到指定设备上
# 加载模型保存的参数
state_dict = torch.load(f"{_exp_name}_best.ckpt")  # 从保存的文件中加载模型参数
# 将参数加载到模型中
model.load_state_dict(state_dict)  # 将加载的参数应用到模型中
# 设置模型为评估模式
model.eval()  # 将模型设置为评估模式,禁用训练期间的功能如dropout

# 打印模型结构
print(model)  # 打印模型的结构,以便检查模型的配置

def forward_to_layer(model, input_tensor, layer_index):
    """
    前向传播到指定层并返回该层的输出特征。

    参数:
    - model: 要进行前向传播的模型
    - input_tensor: 输入数据张量
    - layer_index: 要提取输出的层的索引

    返回:
    - 指定层的输出特征
    """
    outputs = []
    for i, layer in enumerate(model.children()):  # 遍历模型的所有层
        input_tensor = layer(input_tensor)  # 将输入数据传递到当前层
        if i == layer_index:  # 如果当前层是目标层,则停止前向传播
            break
        outputs.append(input_tensor)  # 将每一层的输出保存到outputs列表中
    return outputs[-1]  # 返回目标层的输出特征

# 加载定义的验证集
valid_set = FoodDataset("./hw3_data/valid", tfm=test_tfm)  # 加载验证数据集
valid_loader = DataLoader(valid_set, batch_size=64, shuffle=False, num_workers=0, pin_memory=True)  # 创建验证数据加载器

# 提取模型特定层的表示
index = 19  # 提取第19层的特征
features = []  # 用于存储特征
labels = []  # 用于存储标签

# 确保数据处理在正确的设备上进行
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# 遍历验证数据集
for batch in tqdm(valid_loader):  # tqdm用于显示进度条
    imgs, lbls = batch  # 获取图像和对应的标签
    imgs, lbls = imgs.to(device), lbls.to(device)  # 确保数据在正确的设备上
    with torch.no_grad():  # 在验证阶段不需要梯度,禁用梯度计算以节省内存和计算资源
        logits = forward_to_layer(model.cnn, imgs, index)  # 获取特定层的特征
        logits = logits.view(logits.size(0), -1)  # 将特征展平成一维以便后续处理
    labels.extend(lbls.cpu().numpy())  # 将标签转移到CPU并保存
    features.extend(logits.cpu().numpy())  # 将特征转移到CPU并保存

# 将features和labels列表转换为numpy数组
features = np.array(features)  # 转换特征为numpy数组
labels = np.array(labels)  # 转换标签为numpy数组

# 应用t-SNE到特征上
features_tsne = TSNE(n_components=2, init='pca', random_state=42).fit_transform(features)  # 使用t-SNE将特征降维到二维

# 绘制t-SNE可视化图
plt.figure(figsize=(10, 8))
for label in np.unique(labels):  # 遍历所有唯一的标签
    mask = (labels == label)  # 选择当前标签的数据点
    plt.scatter(features_tsne[mask, 0], features_tsne[mask, 1], label=f'Class {label}', s=5)  # 绘制散点图
plt.legend()  # 显示图例
plt.title('All Classes t-SNE Visualization')  # 设置标题
plt.show()  # 显示图像

# 绘制特定类别的t-SNE可视化图
plt.figure(figsize=(10, 8))
selected_label = 5  # 选择一个特定的标签进行可视化
mask = (labels == selected_label)  # 选择该标签的数据点
if mask.any():  # 检查是否存在该标签的数据点
    plt.scatter(features_tsne[mask, 0], features_tsne[mask, 1], label=f'Class {selected_label}', s=5)  # 绘制散点图
plt.legend()  # 显示图例
plt.title(f'Class {selected_label} t-SNE Visualization')  # 设置标题
plt.show()  # 显示图像

输出结果:
在这里插入图片描述
这张“所有类别的t-SNE可视化”图展示了使用t-SNE算法对卷积神经网络提取的特征进行降维后的结果,不同颜色的点代表不同的类别。其中大部分类别之间的界限并不明显,存在比较多的重叠和混杂,表明模型在某些类别上的区分能力有限,需要进一步优化特征提取或分类的策略。
在这里插入图片描述
这张“Class 5的t-SNE可视化”图展示了Class 5类别样本在特征空间中的分布情况。可以看到,Class 5的样本在二维空间中呈现出多个分散的小簇,表明该类别内部存在一定的特征差异。此外,这些小簇之间的距离较大,且有一些样本点分布较为孤立,表明在这一类别上可能要进一步优化模型的特征提取,以提高该类别的内部一致性和整体分类效果。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值