第8课:PyTorch|损失函数详解与深度学习损失选型【模型训练的“导航仪”】

文章目录
📖 课前导读
为什么损失函数是深度学习的“指挥棒”?
你搭建了一个神经网络,前向传播得到了预测值。但你如何知道这个预测“好”还是“坏”?损失函数(Loss Function)就是答案——它是一个数学函数,输入是模型预测值y_pred和真实标签y_true,输出一个标量数值,表示预测与真实之间的“差距”。训练的目标就是最小化这个损失。
损失函数的选择直接决定了模型的学习方向。选错了损失,即使网络再深、数据再多,模型也学不到你想要的规律。例如,用MSE做分类任务,模型会被迫去拟合概率值的精确位置,却忽略了类别间的相对顺序;用交叉熵做回归,模型会认为接近真实值的预测和远距离预测的惩罚力度相似,导致收敛变慢。
💡 类比:把训练比作蒙眼下山,模型是登山者,损失函数是脚下的坡度感知——告诉你哪个方向是下坡(梯度下降),而且损失的数值大小告诉你“这里有多陡”。一个好的损失函数就像精度极高的海拔仪,能引导登山者以最快的路径找到山谷最低点。
学完这一课,你将能够:
- ✅ 为回归任务选择MSE、L1或SmoothL1损失,并理解它们对异常值的敏感度差异
- ✅ 掌握分类任务中的交叉熵损失原理,了解它为什么比MSE更适合分类
- ✅ 处理多标签分类、二分类、多分类的损失选择
- ✅ 编写自定义损失函数(如加权损失、Focal Loss)
- ✅ 在多任务学习中合理组合多个损失,设置动态权重
- ✅ 调试训练中的损失异常:不下降、振荡、NaN等问题
一、知识原理:损失函数的数学基础
1.1 损失函数与风险最小化
从统计学习理论的角度,我们希望在数据分布P(x,y)上最小化期望风险(Expected Risk):R(f) = E[L(f(x), y)]。但由于真实分布未知,我们通过最小化经验风险(Empirical Risk)来近似:R_emp(f) = (1/N) * Σ L(f(x_i), y_i)。
因此,损失函数L决定了模型学习的归纳偏置——它会引导模型偏向某种特定的解。
1.2 损失函数的理想特性
| 特性 | 说明 |
|---|---|
| 非负性 | L(y_pred, y_true) ≥ 0,且仅当预测完全正确时为0 |
| 对称性(可选) | 对于某些任务,过高和过低估惩罚应对称 |
| 鲁棒性 | 对异常值不应过于敏感(如Huber损失) |
| 可微性 | 几乎处处可微,以便反向传播 |
| 与评价指标对齐 | 最小化损失应能最大化下游指标(如准确率、AUC) |
1.3 回归损失 vs 分类损失的核心区别
- 回归任务:输出连续值,预测值与真实值之间的距离有明确物理意义,损失常基于距离度量(绝对差、平方差)。
- 分类任务:输出离散类别,模型通常输出类别的概率,损失应惩罚“错误类别的概率过高”,常用交叉熵等基于信息论或概率距离的度量。
二、环境搭建与准备
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import matplotlib.pyplot as plt
import numpy as np
print(f"PyTorch版本: {torch.__version__}")
# 设置随机种子
torch.manual_seed(42)
np.random.seed(42)
# 用于可视化损失曲线的数据
x_plot = np.linspace(-3, 3, 100)
三、代码实战:回归损失函数
3.1 MSE Loss(均方误差)
MSE计算预测值与真实值之差的平方,再取平均(或求和)。
MSE = (1/N) * Σ (y_pred_i - y_true_i)^2
# PyTorch中的MSE损失
mse_loss = nn.MSELoss() # 默认 reduction='mean'
# 也可用 reduction='sum' 或 'none'(返回每个元素的损失)
y_true = torch.tensor([1.0, 2.0, 3.0])
y_pred = torch.tensor([1.5, 2.0, 2.5])
loss = mse_loss(y_pred, y_true)
print(f"MSE Loss: {loss.item():.4f}") # ((0.5^2 + 0^2 + (-0.5)^2)/3 = (0.25+0+0.25)/3=0.1667
# 手动验证
manual_mse = ((y_pred - y_true) ** 2).mean()
print(f"手动计算: {manual_mse.item():.4f}")
函数形式:F.mse_loss(y_pred, y_true, reduction='mean')
优缺点:
- ✅ 处处可导,优化平滑
- ✅ 对大误差惩罚力度大(平方放大),有助于快速减小大偏差
- ❌ 对异常值极其敏感(一个大误差会使损失飙升,模型会拼命拟合异常点)
- ❌ 梯度被误差值本身缩放,大误差时梯度大,小误差时梯度接近0——收敛后期更新缓慢
适用场景:当误差服从高斯分布时,MSE是最大似然估计下的最优选择,常用于普通回归任务且数据无显著异常值。
3.2 L1 Loss(平均绝对误差,MAE)
L1 = (1/N) * Σ |y_pred_i - y_true_i|
l1_loss = nn.L1Loss() # reduction='mean'
loss_l1 = l1_loss(y_pred, y_true)
print(f"L1 Loss: {loss_l1.item():.4f}") # (0.5+0+0.5)/3 = 0.3333
# 函数式: F.l1_loss
优缺点:
- ✅ 对异常值更鲁棒(梯度恒定,不会像MSE那样被异常值主导)
- ✅ 在0点处不可导(但次梯度可处理)
- ❌ 梯度恒定,当预测接近真实值时,更新步长不变,可能不易收敛到精确值
- ❌ 大误差时梯度仍是±1,不像MSE那样能快速拉回
适用场景:数据中存在较多异常值(如工业传感器噪声),或任务本身不在意极度精确(如目标检测中的边界框回归,常使用Smooth L1)。
3.3 Smooth L1 Loss(Huber损失的简化版)
结合MSE和L1的优点:在误差较小时为平方(平滑可导),误差较大时转为线性(鲁棒)。
SmoothL1(x) = 0.5 * x^2 if |x| < 1 else |x| - 0.5,其中 x = y_pred - y_true
smooth_l1 = nn.SmoothL1Loss() # 默认 beta=1.0(即阈值1)
loss_smooth = smooth_l1(y_pred, y_true)
print(f"Smooth L1 Loss: {loss_smooth.item():.4f}")
# 手动验证:误差为0.5时,0.5*0.5^2=0.125;0和-0.5类似,平均为0.0833?等一下,计算有误
# 正确计算:误差 e = [-0.5, 0, -0.5](预测-真值);SmoothL1(e): |e|<1 => 0.5*e^2,即0.125, 0, 0.125;平均值0.08333
优势:比MSE鲁棒,比L1在0点光滑。Faster R-CNN等目标检测模型中使用Smooth L1回归边界框。
3.4 回归损失可视化对比
errors = np.linspace(-3, 3, 200)
mse_vals = errors ** 2
l1_vals = np.abs(errors)
smooth_l1_vals = np.where(np.abs(errors) < 1, 0.5 * errors ** 2, np.abs(errors) - 0.5)
plt.figure(figsize=(10, 6))
plt.plot(errors, mse_vals, label='MSE', linewidth=2)
plt.plot(errors, l1_vals, label='L1 (MAE)', linewidth=2)
plt.plot(errors, smooth_l1_vals, label='Smooth L1 (beta=1)', linewidth=2)
plt.xlabel('Prediction Error (y_pred - y_true)')
plt.ylabel('Loss')
plt.title('回归损失函数对比')
plt.grid(True, alpha=0.3)
plt.legend()
plt.show()
从图中可见:MSE在误差大时急剧上升(过度惩罚异常值);L1线性增长;Smooth L1在误差小的时候像MSE(光滑),大的时候像L1(线性)。
3.5 回归损失的选择建议
| 情况 | 推荐损失 | 理由 |
|---|---|---|
| 数据干净,误差近似高斯 | MSE | 统计最优,收敛快 |
| 数据有异常值(长尾噪声) | L1 或 Smooth L1 | 不被离群点绑架 |
| 目标检测边界框回归 | Smooth L1 | 平衡精度与鲁棒性,且实现简单 |
| 深度学习任意回归任务(不确定) | Smooth L1 | 安全起点,结合两者优点 |
四、代码实战:分类损失函数
4.1 CrossEntropyLoss(交叉熵损失)
使用前提:多分类问题,模型最后一层输出未归一化的logits(形状 [batch_size, num_classes]),且标签为类别索引(形状 [batch_size])。
数学原理:CrossEntropy = -log(softmax(logits)[class])。实质上是LogSoftmax + NLLLoss的组合,比手动做softmax再负对数更数值稳定。
# 示例:3分类问题,batch_size=2
logits = torch.tensor([[2.0, 1.0, 0.1],
[0.5, 2.0, 0.3]]) # 形状[2,3]
labels = torch.tensor([0, 1]) # 第0个样本真实类别0,第1个真实类别1
ce_loss = nn.CrossEntropyLoss()
loss = ce_loss(logits, labels)
print(f"CrossEntropyLoss: {loss.item():.4f}")
# 手动模拟过程
softmax_probs = F.softmax(logits, dim=1) # 转为概率
log_softmax = torch.log(softmax_probs) # log概率
nll = -log_softmax[range(len(labels)), labels] # 取真实类别的负对数
manual_loss = nll.mean()
print(f"手动计算损失: {manual_loss.item():.4f}")
关键参数:
weight:类别权重(用于不平衡数据),例如weight=torch.tensor([0.2, 0.8, 0.5])reduction:'mean'(默认)、'sum'或'none'label_smoothing:标签平滑,让模型不极度自信,增强泛化(PyTorch 1.10+)
# 使用类别权重
class_weights = torch.tensor([1.0, 2.0, 1.5]) # 类别1权重更高,惩罚更多
weighted_ce = nn.CrossEntropyLoss(weight=class_weights)
loss_w = weighted_ce(logits, labels)
print(f"加权交叉熵: {loss_w.item():.4f}")
4.2 NLLLoss(负对数似然损失)
通常与LogSoftmax配合,相当于CrossEntropy拆成两步:LogSoftmax + NLLLoss。但直接使用CrossEntropyLoss更简单,除非你需要单独输出log-probabilities。
log_softmax = nn.LogSoftmax(dim=1)
nll_loss = nn.NLLLoss()
log_probs = log_softmax(logits)
loss_nll = nll_loss(log_probs, labels)
print(f"NLLLoss结果: {loss_nll.item():.4f}") # 应与CE相同
4.3 BCEWithLogitsLoss(二分类交叉熵)
用于二分类问题(标签为0/1),模型输出一个值(未经过Sigmoid)。数值上等价于 Sigmoid + BCELoss,但更稳定。
BCEWithLogitsLoss适用于标签形状为 [batch_size] 或者 [batch_size, 1],输出为 [batch_size] 或 [batch_size, 1]。
# 二分类示例
logits_binary = torch.tensor([2.0, -1.0, 0.5]) # 未sigmoid
labels_binary = torch.tensor([1.0, 0.0, 1.0]) # 真实标签
bce_loss = nn.BCEWithLogitsLoss()
loss_bce = bce_loss(logits_binary, labels_binary)
print(f"BCEWithLogitsLoss: {loss_bce.item():.4f}")
# 手动验证
probs = torch.sigmoid(logits_binary)
manual_bce = -(labels_binary * torch.log(probs) + (1-labels_binary) * torch.log(1-probs)).mean()
print(f"手动计算: {manual_bce.item():.4f}")
参数:同样支持 pos_weight(正样本权重,处理类别不平衡)和 weight。
4.4 BCELoss(不推荐单独使用)
需要手动对模型输出做Sigmoid,数值稳定性差(容易产生log(0))。所以一般使用BCEWithLogitsLoss。
# 不推荐
sigmoid = nn.Sigmoid()
bce = nn.BCELoss()
probs = sigmoid(logits_binary)
loss = bce(probs, labels_binary) # 和上面结果一致,但需要额外步骤
4.5 多标签分类损失
多标签分类:每个样本可能同时属于多个类别(如一张图同时包含猫、狗、鸟)。输出层为 num_classes个logits,每个类别独立使用Sigmoid,损失采用BCEWithLogitsLoss,标签形状与输出相同,值为0/1。
# 多标签示例:3个类别,2个样本
logits_multilabel = torch.tensor([[2.0, 0.5, -1.0],
[1.0, -0.5, 0.8]])
# 标签:样本1有类别0和2,样本2只有类别1
labels_multilabel = torch.tensor([[1.0, 0.0, 1.0],
[0.0, 1.0, 0.0]])
bce_loss_multilabel = nn.BCEWithLogitsLoss()
loss_ml = bce_loss_multilabel(logits_multilabel, labels_multilabel)
print(f"多标签损失: {loss_ml.item():.4f}")
4.6 分类损失API速查表
| 任务 | 推荐损失 | 模型最后一层 | 标签形式 | 说明 |
|---|---|---|---|---|
| 二分类 | BCEWithLogitsLoss | 1个神经元(无激活) | 0/1浮点或布尔 | 数值稳定 |
| 多分类(单标签) | CrossEntropyLoss | 类别数个神经元(无Softmax) | 类别索引整数 | 最常用 |
| 多分类(单标签),需要log-prob | LogSoftmax + NLLLoss | 类别数个神经元 | 类别索引 | 用于某些生成任务 |
| 多标签分类 | BCEWithLogitsLoss | 类别数个神经元 | 多热向量(0/1) | 每个类别独立二分类 |
| 多分类(标签平滑) | CrossEntropyLoss 设置 label_smoothing | 同上 | 同上 | 提升泛化 |
五、高级损失函数与自定义损失
5.1 自定义损失函数:两种方式
方式一:继承nn.Module(推荐用于带参数或复杂逻辑的损失)
class WeightedMSELoss(nn.Module):
"""加权MSE损失,对每个样本的loss乘以权重"""
def __init__(self, weight=None):
super().__init__()
self.weight = weight # 可以是标量或与目标同形状的tensor
def forward(self, pred, target):
loss = (pred - target) ** 2
if self.weight is not None:
loss = loss * self.weight.to(loss.device)
return loss.mean()
# 使用
w_mse = WeightedMSELoss(weight=torch.tensor([0.5, 1.0, 2.0]))
y_pred = torch.tensor([1.0, 2.0, 3.0])
y_true = torch.tensor([1.2, 1.8, 3.1])
loss = w_mse(y_pred, y_true)
print(loss)
方式二:简单的函数形式
def my_smooth_l1_loss(pred, target, beta=1.0):
diff = pred - target
abs_diff = torch.abs(diff)
loss = torch.where(abs_diff < beta, 0.5 * diff ** 2 / beta, abs_diff - 0.5 * beta)
return loss.mean()
# 在训练循环中直接使用
loss = my_smooth_l1_loss(output, target)
5.2 处理类别不平衡:Focal Loss
Focal Loss由RetinaNet提出,专门解决类别极不平衡的目标检测问题。它在交叉熵的基础上增加了调制因子(1 - p_t)^γ,使模型更关注难分类的样本。
公式:FL(p_t) = -α_t * (1 - p_t)^γ * log(p_t)
class FocalLoss(nn.Module):
def __init__(self, alpha=0.25, gamma=2.0, reduction='mean'):
super().__init__()
self.alpha = alpha
self.gamma = gamma
self.reduction = reduction
def forward(self, logits, targets):
# logits: [batch, num_classes], targets: [batch]
ce_loss = F.cross_entropy(logits, targets, reduction='none')
pt = torch.exp(-ce_loss) # 模型对正确类别的预测概率
focal_weight = (1 - pt) ** self.gamma
if self.alpha is not None:
alpha_t = self.alpha * (targets == 1).float() + (1 - self.alpha) * (targets != 1).float()
focal_weight = alpha_t * focal_weight
loss = focal_weight * ce_loss
if self.reduction == 'mean':
return loss.mean()
elif self.reduction == 'sum':
return loss.sum()
else:
return loss
# 使用示例
focal_loss = FocalLoss(alpha=0.25, gamma=2)
logits = torch.randn(4, 10)
labels = torch.randint(0, 10, (4,))
loss_focal = focal_loss(logits, labels)
5.3 标签平滑(Label Smoothing)
标签平滑是一种正则化技术,防止模型对训练标签过于自信(过拟合)。它将独热标签的1替换为1-ε,其他0替换为ε/(K-1)。
# PyTorch内置(1.10+)
ce_smooth = nn.CrossEntropyLoss(label_smoothing=0.1)
loss_smooth = ce_smooth(logits, labels)
print(f"标签平滑后损失: {loss_smooth.item():.4f}")
# 手动实现(用于老版本)
def label_smoothing_loss(logits, targets, epsilon=0.1):
K = logits.size(1)
log_probs = F.log_softmax(logits, dim=1)
with torch.no_grad():
smooth_targets = torch.zeros_like(log_probs).fill_(epsilon / (K - 1))
smooth_targets.scatter_(1, targets.unsqueeze(1), 1 - epsilon)
return -(smooth_targets * log_probs).sum(dim=1).mean()
5.4 多任务损失组合
当模型需要同时优化多个目标(例如同时做分类和回归),可以组合多个损失:total_loss = λ1 * loss1 + λ2 * loss2。
如何选择权重λ?常见方法:
- 手动设置:根据任务重要性固定权重。
- 动态加权:根据损失的大小自动调整(如
loss1 / (loss1 + loss2)归一化)。 - 不确定性加权:学习每个任务的不确定性(方差),详见论文
Multi-Task Learning Using Uncertainty to Weigh Losses for Scene Geometry and Semantics。
class MultiTaskLoss(nn.Module):
def __init__(self, num_tasks):
super().__init__()
self.log_vars = nn.Parameter(torch.zeros(num_tasks)) # 可学习方差的对数
def forward(self, losses):
# losses: list/tuple of task losses
weighted_loss = 0
for i, loss in enumerate(losses):
precision = torch.exp(-self.log_vars[i]) # 1/方差
weighted_loss += precision * loss + self.log_vars[i]
return weighted_loss
# 使用示例
mtl = MultiTaskLoss(num_tasks=2)
loss1 = torch.tensor(0.5, requires_grad=True)
loss2 = torch.tensor(1.2, requires_grad=True)
total = mtl([loss1, loss2])
total.backward() # 自动学习log_vars
六、训练中损失异常排查
6.1 损失不下降的可能原因
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 损失震荡不降 | 学习率过大 | 降低学习率或使用学习率衰减 |
| 损失先降后升 | 过拟合 | 增加正则化(Dropout, weight decay) |
| 损失下降极慢 | 学习率过小 / 梯度消失 | 提高学习率,检查激活函数 |
| 损失为NaN | 梯度爆炸 / 除以0 / log(0) | 梯度裁剪,检查数据归一化,使用稳定的损失函数(如BCEWithLogits) |
| 损失负值 | 某些损失定义允许负值(如余弦相似度)或代码错误 | 检查损失公式,确认是否合理 |
6.2 调试技巧
- 打印损失值:每个batch记录,观察趋势。
- 监控梯度范数:使用
torch.nn.utils.clip_grad_norm_前打印norm。 - 减小学习率进行测试:如果损失变为NaN,尝试极低学习率看是否正常,排除爆炸。
- 检查标签范围:分类标签是否超出类别数,回归标签是否数值过大。
- 检查数据预处理:是否归一化,是否存在缺失值。
# 梯度监控示例
def log_grad_norm(model):
total_norm = 0
for p in model.parameters():
if p.grad is not None:
param_norm = p.grad.data.norm(2)
total_norm += param_norm.item() ** 2
total_norm = total_norm ** 0.5
print(f"梯度总范数: {total_norm:.4f}")
七、难点解析:常见问题与陷阱
7.1 将未归一化的logits传入CrossEntropyLoss前手动Softmax
错误示例:
# 错误:CrossEntropyLoss内部已经包含Softmax
outputs = torch.softmax(model(x), dim=1)
loss = nn.CrossEntropyLoss()(outputs, labels) # 相当于做了两次softmax
后果:损失值不可解释,梯度扭曲,模型难以收敛。
7.2 回归任务使用了分类损失
例如用交叉熵做房价预测。交叉熵要求输出和为1,且期望的概率分布,而房价是连续值,两者完全不兼容。必须使用回归损失。
7.3 在二分类中使用CrossEntropyLoss
二分类也可以用CrossEntropyLoss,但需要输出2个神经元(分别对应类别0和1),标签为0/1。通常更高效的做法是BCEWithLogitsLoss,只需输出1个神经元。
7.4 损失函数的reduction参数导致batch统计错误
在分布式训练或需要梯度累加时,reduction='mean'在不同batch大小下会改变损失的尺度。最好统一使用reduction='sum'配合手工除以总样本数。
7.5 自定义损失中未正确处理设备和数据类型
自定义损失内部使用时,需要将权重等移动到与输入相同的设备。可使用self.weight.to(pred.device)。
八、课后总结
回归损失对比表
| 损失 | 公式 | 对异常值鲁棒 | 光滑性 | 适用场景 |
|---|---|---|---|---|
| MSE | 平方差 | ❌ | ✅ 处处光滑 | 高斯噪声,无异常值 |
| L1 | 绝对差 | ✅ | ❌ 0点不可导 | 有异常值 |
| Smooth L1 | 分段 | ✅ | ✅ | 目标检测,稳健回归 |
分类损失对比表
| 任务 | 推荐损失 | 内部操作 | 最后激活 | 标签形式 |
|---|---|---|---|---|
| 多分类(单标签) | CrossEntropyLoss | LogSoftmax + NLLLoss | 无 | 整数索引 |
| 二分类 | BCEWithLogitsLoss | Sigmoid + BCELoss | 无 | 0/1浮点 |
| 多标签分类 | BCEWithLogitsLoss | 每个类别Sigmoid | 无 | 多热向量 |
| 带标签平滑 | CrossEntropyLoss(label_smoothing) | 同上 | 无 | 整数索引 |
检查清单
- 知道为回归任务选择MSE或L1/SmoothL1的依据
- 能正确使用CrossEntropyLoss,不在之前加Softmax
- 理解BCEWithLogitsLoss与BCELoss的区别
- 会为多标签任务配置损失
- 能够编写简单的自定义损失函数
- 了解多任务损失组合的常用策略
- 掌握训练中损失异常的排查方法
九、课后作业
作业1:回归损失性能对比
构造一个包含异常值的回归数据集(例如在正态分布数据中加入10%的离群点)。用同一个简单线性模型,分别使用MSE、L1和SmoothL1损失训练,观察模型对异常值的拟合程度,并分析哪个损失效果最好。
作业2:交叉熵与MSE分类对比
在MNIST上训练一个简单网络,分别使用CrossEntropyLoss和MSELoss(需要将标签转为one-hot,输出层使用Softmax)。对比两者的收敛速度和最终准确率。解释为什么MSE不适合分类。
作业3:自定义损失实现
实现一个HuberLoss类(与SmoothL1Loss类似但可调节delta参数)。在回归任务中比较HuberLoss(delta=1)与SmoothL1Loss的数值差异。
作业4:类别不平衡实验
创建一个类别严重不平衡的二分类数据集(正负比1:100)。使用普通的BCEWithLogitsLoss,然后使用带有pos_weight参数的BCEWithLogitsLoss(例如pos_weight=100),对比模型在少数类上的召回率。
作业5:多任务损失组合
假设模型需要同时输出分类(3类)和回归(单个值)。定义总损失为loss_cls + λ * loss_reg。实验不同的λ值(0.1, 1.0, 10.0),观察两个任务的性能变化。尝试使用不确定性加权方法自动学习λ。
十、下一课预告
第9课我们将学习优化器原理与主流优化器实战调参,内容包括:
- 梯度下降的变体:SGD、Momentum、Nesterov
- 自适应学习率优化器:AdaGrad、RMSprop、Adam、AdamW
- 优化器参数详解(学习率、动量、权重衰减)
- 优化器选择策略和实战调参建议
- 学习率与优化器的配合
学完第9课,你将能根据任务特点选择合适的优化器,并调出更好更稳定的收敛效果。
附录:PyTorch损失函数API速查
| 损失类 | 函数式 | 用途 |
|---|---|---|
nn.MSELoss | F.mse_loss | 回归,平方差 |
nn.L1Loss | F.l1_loss | 回归,绝对差 |
nn.SmoothL1Loss | F.smooth_l1_loss | 稳健回归 |
nn.CrossEntropyLoss | F.cross_entropy | 多分类(单标签) |
nn.BCEWithLogitsLoss | F.binary_cross_entropy_with_logits | 二分类/多标签 |
nn.BCELoss | F.binary_cross_entropy | 二分类(需手动sigmoid) |
nn.NLLLoss | F.nll_loss | 配合LogSoftmax |
nn.KLDivLoss | F.kl_div | KL散度,用于蒸馏 |
nn.CosineEmbeddingLoss | - | 相似度学习 |
🔗《精讲25课|PyTorch 从入门到精通》系列课程导航
🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~
更多推荐


所有评论(0)