神经网络
概念
深度学习的核心就是神经网络,其是一种通过简单函数的组合来表示复杂函数的数学实体,这些复杂函数的基本构件就是神经元
单层神经网络
上述介绍了神经网络的基本概念,引出了神经元,而神经元核心也就是输入的线性变换,最简单的便是正比例函数,输入乘以一个数字【权重】,然后再加上一个数字【偏置】,然后应用一个非线性函数,而这个非线性函数便是激活函数
由此而构成的一个函数其数学表达式为o=f(wx+b),其中x是输入,w是权重,b是偏置,f是激活函数
而o和x可以是简单的标量,也可以是复杂的向量,w和b也是如此,但是当b为向量时,其输入的维度必须与权重匹配,而上述这样的表示式便是单层的神经网络
多次神经网络
既然已经知道了单层神经网络,那多层神经网络便是将一层的神经网络的输入(x)通过上一层神经网络的输出(o)来表示,如下
x_1 =f(w_0 * x + b_0)
x_2=f(w_1 * x_1 + b_1)
····
y=f(w_n * x_n + b_n)
同时每一层的激活函数可以完全不同,每一层神经网络都可以发挥自己的特性
激活函数
通过上文我们已经知道了什么是激活函数,这里我们详细介绍一下激活函数
激活函数主要在torch.nn模块中,这是一个专门用于神经网络的模块,也是这次主要的介绍对象
类型
限制输出范围--Tanh()
class torch.nn.Hardtanh(
min_val: float = -1.0,
max_val: float = 1.0,
inplace: bool = False
)
公式:
minval:范围最小值
maxval:范围最大值
inplace:布尔型,是否进行原地操作,即直接修改输入的这里,而不创立新的张量
压缩输出范围--Sigmoid()
class torch.nn.Sigmoid()
公式:
整流线性单元--ReLU()
class torch.nn.ReLU(inplace: bool = False)
公式:
inplace:布尔型,是否进行原地操作,即直接修改输入的这里,而不创立新的张量
LeakyReLU()
class torch.nn.LeakyReLU(negative_slope: float = 1e-2, inplace: bool = False)
公式:
negative_slope:用以控制负斜率的角度,负斜率是处理负输入值,默认为0.01
inplace:布尔型,是否进行原地操作,即直接修改输入的这里,而不创立新的张量
PReLU()
class torch.nn.PReLU(num_parameters: int = 1, init: float = 0.25, device=None, dtype=None)
公式:
num_parameters:需要学习的参数的数量
init:表示变量的初始值
device:运行设备
dtype:数据类型
其与上面的LeakyReLU()的区别在于该激活函数的
可以学习
SELU()
class torch.nn.SELU(inplace: bool = False)
公式:
其中
inplace:布尔型,是否进行原地操作,即直接修改输入的这里,而不创立新的张量
GELU()
class torch.nn.GELU(approximate: str = "none")
approximate:选择使用合适的算法,默认为none,精确模式 (
approximate='none');近似模式 (approximate='tanh')
参数模式 计算方法 核心特点 典型应用场景 精确模式 x*Φ(x),其中Φ(x)为标准正态分布的累积分布函数 ,通常通过误差函数erf计算。计算精度高,但涉及特殊函数 erf,计算开销较大。对数值精度要求极高的实验或理论研究。 近似模式 计算速度快,仅由初等运算构成,易于优化,在多数硬件(尤其是GPU)上性能更好。精度与精确公式差异极小,通常不影响模型收敛和效果。 绝大多数实际训练和推理的默认选择 在实际模型训练中,通常将
approximate设为'tanh',这是兼顾效率与效果的最佳实践
SiLU()
class torch.nn.SiLU(inplace: bool = False)
公式:
其中是Sigmoid()函数
inplace:布尔型,是否进行原地操作,即直接修改输入的这里,而不创立新的张量
主要应用--以图像处理为主
| 代表函数 | 图像处理中的主要应用场景 | 核心特点与说明 |
|---|---|---|
| ReLU | 绝大多数CNN的隐藏层默认选择 | 计算高效,缓解梯度消失,但可能导致"神经元死亡"。 |
| LeakyReLU/PReLU | 尝试解决ReLU"神经元死亡"问题 | 为负输入引入一个小梯度,PReLU的斜率可学习。 |
| SELU/GELU/SiLU | 部分现代先进架构(如Vision Transformer) | 具有自归一化或更平滑的特性,性能可能更好但计算稍复杂。 |
| Sigmoid | 网络输出层(用于二分类、像素级分类) | 将输出压缩到(0,1),可直接解释为概率。 |
| Tanh | 现已较少用于CNN隐藏层,多见于RNN | 输出以0为中心,但仍有梯度消失问题。 |
| Softmax | 网络输出层(用于图像多分类) | 将输出归一化为概率分布,总和为1。 |
而对于ReLU()函数及其变种函数
| 激活函数 | 核心特性与设计意图 | 典型应用场景/备注 |
|---|---|---|
| LeakyReLU | 针对ReLU的"神经元死亡"问题,为负输入引入一个固定的小斜率α(如0.01),确保梯度始终非零。 | 通用CNN/MLP,作为ReLU的直接替代品,性能稳定。α通常设为0.01。 |
| PReLU | 将LeakyReLU的斜率α变为可学习的参数,让网络自适应决定负区的最佳斜率,灵活性更强。 | 希望模型自行决定负区特性时。需要留意,增加少量可学习参数。 |
| SELU | 具有自归一化特性:在特定权重初始化下,能保持数据在前向/反向传播中的均值和方差稳定。 | 深层全连接网络。需配合LeCun正态初始化等特定初始化,效果才显著。 |
| GELU | 通过概率视角设计:用输入相对于其他输入的"百分比"来对其进行门控。平滑、非单调。 | Transformer架构的默认选择(如BERT、GPT、Vision Transformer)。性能出色。 |
| SiLU | Google发现,是平滑、非单调的激活函数。可看作x与其"门控"sigmoid的乘积,有自门控特性。 | 替代ReLU的强力候选者,在某些任务上优于ReLU。Swish特指β=1的版本。 |
损失函数
类型
Pytorch nn的模块也包括损失函数类型
均方误差损失--nn.MSELoss
class torch.nn.MESLoss(reduction: str = "mean")
def forward(self, input: Tensor, target: Tensor)
公式:loss = mean(y_pred - y_true)^2
reduction:指定应用输出的缩减方式,"none"表示不进行缩减;"mean"表示求取平均值;"sum"表示求取输出的和
input:需要计算损失函数的张量
target:计算出损失函数的张量
L1损失--L1Loss
核心计算预测值与真实值之间绝对误差的平均值 / 总和,因对异常值鲁棒、计算简单,常应用于对误差 "线性惩罚" 的回归场景,也是SmoothL1Loss的基础。因此通常也使用SmoothL1Loss。
class torch.nn.L1Loss(reduction: str = "mean")
def forward(self, input: Tensor, target: Tensor)
公式:
其中N为样本总数(像素总数)
reduction:指定应用于输出的缩减方式,"none"表示不进行缩减;"mean"表示求取平均值;"sum"表示求取输出的和
input:需要计算损失函数的张量
target:计算出损失函数的张量
平滑L1损失--nn.SmoothL1Loss
class torch.nn.SmoothL1Loss(reduction: str = "mean", beta: float = 1.0)
def forward(self, input: Tensor, target: Tensor)
公式:其中
reduction:指定应用于输出的缩减方式,"none"表示不进行缩减;"mean"表示求取平均值;"sum"表示求取输出的和
beta:指定L1损失和L2损失之间切换的阈值
input:需要计算损失函数的张量
target:计算出损失函数的张量
感知损失--nn.PerceptualLoss
公式:假设:预训练CNN的特征提取函数
:图形x在第l层的特征图
:模型预测图像;
:真实图像
其中:第l层1特征图的通道/高度/宽度
由于PerceptualLoss特别复杂,需要通过CNN采集特征,因此这里对其进行轻量化,其与L1Loss使用相同的函数,但是L1Loss损失直接对源数据进行损失计算,而PerceptualLoss需要先采集特征,再使用L1Loss计算损失,这个损失是特征之间的损失,更加精确
import torch import torch.nn as nn import torchvision.models as models class PerceptualLoss(nn.Module): def __init__(self, device='cpu'): # 嵌入式端默认CPU/NPU super().__init__() # 1. 加载轻量预训练模型:MobileNetV2(替代VGG,减少参数) mobilenet = models.mobilenet_v2(pretrained=True).to(device) # 2. 提取浅层特征层(第2层,对应纹理/边缘,计算量小) self.feature_extractor = nn.Sequential(*list(mobilenet.features)[:2]).to(device) # 3. 冻结参数(仅提取特征,不训练) for param in self.feature_extractor.parameters(): param.requires_grad = False # 4. 损失函数:L1(比MSE更鲁棒,嵌入式端运算更快) self.l1_loss = nn.L1Loss(reduction='mean') def forward(self, pred, target): # pred/target: [B, 3, H, W](归一化到0~1,符合预训练模型输入) # 提取特征 pred_feat = self.feature_extractor(pred) target_feat = self.feature_extractor(target) # 计算特征层的L1损失(感知损失) return self.l1_loss(pred_feat, target_feat) # 嵌入式端使用示例(模拟图像超分场景) device = torch.device('cpu') # 嵌入式端无GPU,用CPU/NPU perceptual_loss = PerceptualLoss(device=device) # 模拟输入:批量1,3通道,64x64(嵌入式端小尺寸) pred_img = torch.randn(1, 3, 64, 64).to(device) target_img = torch.randn(1, 3, 64, 64).to(device) loss = perceptual_loss(pred_img, target_img) print(f"PerceptualLoss (MobileNetV2版): {loss.item():.4f}")
交叉熵损失--nn.CrossEntropyLoss
class torch.nn.CrossEntropyLoss(weight: Optional[Tensor] = None, ignore_index: int = -100, reduction: str = "mean", label_smoothing: float = 0.0)
def forward(self, input: Tensor, target: Tensor)
公式:对于二分类场景,真实标签,其中模型预测为正类的概率为p
对于多分类场景,假设类别数为C,真实标签是独热编码形式y(输入不需要转化为独热编码,函数(自动转化),模型预测的概率分布为p(由softmax得到,满足)
weight:张量,给每个类别手动设置的重新缩放权重。
ignore_index:指定一个被忽略且不计入输入梯度的目标值。
reduction:指定应用于输出的缩减方式,"none"表示不进行缩减;"mean"表示求取平均值;"sum"表示求取输出的和
label_smoothing:一个在 [0.0, 1.0] 范围内的浮点数。指定计算损失时的平滑程度,其中 0.0 表示不进行平滑。
input:需要计算损失函数的张量
target:计算出损失函数的张量
骰子损失--nn.DiceLoss
在pytorch中没有,但是其经常与交叉熵损失(CrossEntropyLoss)一起使用,或者直接使用monai库
二分类/单通道分割
公式:设预测概率图P(经过Sigmoid归一化到[0,1]),真实标签图为T(0表示背景,1表示目标),为平滑项,通常取1e-6,其中
:预测与真实标签的交集即预测与真实标签逐元素相乘后求和(
)
、
:预测结果和真实标签的元素和(
、
)
其中、
分别为第c类的预测概率图和真实标签图(真实标签转为独热编码)
import torch import torch.nn as nn class DiceLoss(nn.Module): def __init__(self, eps=1e-6): super(DiceLoss, self).__init__() self.eps = eps def forward(self, pred, target): # pred: [batch_size, 1, H, W](经过 Sigmoid 后) # target: [batch_size, 1, H, W](0/1 标签) intersection = (pred * target).sum() # 交集求和 union = pred.sum() + target.sum() # 并集求和 dice_coeff = (2 * intersection + self.eps) / (union + self.eps) return 1 - dice_coeff # 使用示例 dice_loss = DiceLoss() pred = torch.sigmoid(torch.randn(2, 1, 32, 32)) target = torch.randint(0, 2, (2, 1, 32, 32), dtype=torch.float32) loss = dice_loss(pred, target) print(f"DiceLoss: {loss.item():.4f}")
多通道分割
对于C个类别的分割任务,其中、
分别为第c类的预测概率图和真实标签图(真实标签转为独热编码--one_hot)
class MultiClassDiceLoss(nn.Module): def __init__(self, num_classes=3, eps=1e-6): super(MultiClassDiceLoss, self).__init__() self.num_classes = num_classes self.eps = eps def forward(self, pred, target): # pred: [batch_size, num_classes, H, W] # target: [batch_size, H, W](类别索引,如 0,1,2) total_loss = 0.0 # 真实标签转为独热编码 target_onehot = nn.functional.one_hot(target.long(), num_classes=self.num_classes) target_onehot = target_onehot.permute(0, 3, 1, 2).float() # [B, C, H, W] for c in range(self.num_classes): pred_c = pred[:, c:c+1, :, :] # 第 c 类的预测概率 target_c = target_onehot[:, c:c+1, :, :] # 第 c 类的真实标签 intersection = (pred_c * target_c).sum() union = pred_c.sum() + target_c.sum() dice_coeff = (2 * intersection + self.eps) / (union + self.eps) total_loss += (1 - dice_coeff) return total_loss / self.num_classes # 平均每个类的损失 # 使用示例 multi_dice_loss = MultiClassDiceLoss(num_classes=3) pred = torch.softmax(torch.randn(2, 3, 32, 32), dim=1) target = torch.randint(0, 3, (2, 32, 32)) loss = multi_dice_loss(pred, target) print(f"MultiClassDiceLoss: {loss.item():.4f}")
掩码损失--nn.MaskLoss
二值掩码损失--nn.BCEWithLogitsLoss
class torch.nn.SmoothL1Loss(weight: Optional[Tensor] = None, reduction: str = "mean", pos_weight: Optional[Tensor] = None)
def forward(self, input: Tensor, target: Tensor)
公式:其中:真实掩码在(i,j)像素的标签(0/1);
:模型预测该像素为目标的概率(Sigmoid输出)
weight:张量,给每个类别手动设置的重新缩放权重。
reduction:指定应用于输出的缩减方式,"none"表示不进行缩减;"mean"表示求取平均值;"sum"表示求取输出的和
pos_weight:要与目标进行广播的正样本权重,必须是一个在类别维度上与类别数量大小相等的张量。
input:需要计算损失函数的张量
target:计算出损失函数的张量
对于大小为 [N,C,H,W] 的目标(其中N是批量大小),大小为 [N,C,H,W] 的 pos_weight 将为每个批次元素应用不同的 pos_weight,或者大小为 [C,H,W] 的 pos_weight 将在批次中应用相同的 pos_weight。
Dice-BCE混合MaskLoss
其本质就是DiceLoss和BCEWithLogitsLoss的混合,这样的组合即解决了不平衡,也保证了稳定性,同时其计算量也较小,针对于一些嵌入式设备也可以很好的承受
公式:其中为权重系数(通常去0.5,平衡两者,但是也可以根据自己的侧重点另外取值)
class DiceBCEMaskLoss(nn.Module): def __init__(self, alpha=0.5, eps=1e-6): super().__init__() self.alpha = alpha self.eps = eps self.bce = nn.BCEWithLogitsLoss(reduction='mean') def dice_loss(self, pred, target): # 计算Dice系数(无额外参数,轻量化) intersection = (torch.sigmoid(pred) * target).sum() union = torch.sigmoid(pred).sum() + target.sum() return 1 - (2 * intersection + self.eps) / (union + self.eps) def forward(self, pred_mask, true_mask): bce_loss = self.bce(pred_mask, true_mask) dice_loss = self.dice_loss(pred_mask, true_mask) return self.alpha * bce_loss + (1 - self.alpha) * dice_loss ## 创建模型 mask_loss = DiceBCEMaskLoss(alpha=0.5) pred_mask = torch.randn(2, 1, 64, 64) true_mask = torch.randint(0, 2, (2, 1, 64, 64)).float() # rand_like创建一个与true_mask相同维度大小的张量,范围为[0,1) # 对于true_mask的元素,括号中的元素小于0.1则清零,反之则为保持(为1) # true_mask * (torch.rand_like(true_mask) < 0.1)主要是为了定义一个是由少部分为目标,大部分为背景的一个图像 true_mask = true_mask * (torch.rand_like(true_mask) < 0.1).float() ## 创建损失函数 loss = mask_loss(pred_mask, true_mask) print(f"Dice-BCE MaskLoss: {loss.item():.4f}")
焦点损失--nn.FocalLoss
算是上面的交叉熵损失CrossEntropyLoss的改进版,通过 "降低易分类样本的权重、聚焦难分类样本",解决分类任务中 "多数类主导损失、少数类被忽略" 的问题
公式:模型预测为正类的概率为(其中正类为p,负类为1-p),其主要是在CrossEntropyLoss的基础上添加了调制系数和平衡因子
其中:类别平衡因子
:聚焦参数(常取为2),该值越大,对易样本(模型预测为正类的概率高的样本)的压制越强
import torch import torch.nn as nn import torch.nn.functional as F class FocalLoss(nn.Module): def __init__(self, alpha=0.25, gamma=2.0, num_classes=2, reduction='mean'): super().__init__() self.alpha = alpha # 类别平衡因子 self.gamma = gamma # 聚焦参数 self.num_classes = num_classes self.reduction = reduction # 聚合方式 def forward(self, logits, targets): # logits: [B, C](模型输出,未过Softmax/Sigmoid) # targets: [B](类别索引,0~C-1) B, C = logits.shape # 1. 计算真实类别的预测概率p_t if self.num_classes == 2: # 二分类:用sigmoid计算p_t p = torch.sigmoid(logits).squeeze(1) p_t = torch.where(targets == 1, p, 1 - p) alpha_t = torch.where(targets == 1, self.alpha, 1 - self.alpha) else: # 多分类:用softmax计算p_t p = F.softmax(logits, dim=1) p_t = p[range(B), targets] # 多分类α:可自定义,这里简化为均匀α=1/C alpha_t = torch.ones_like(p_t) / self.num_classes # 2. 计算聚焦系数((1-p_t)^γ) focal_weight = torch.pow(1 - p_t, self.gamma) # 3. 计算FocalLoss核心 # 用clamp避免log(0)导致数值爆炸 loss = -alpha_t * focal_weight * torch.log(torch.clamp(p_t, 1e-6, 1.0)) # 4. 聚合损失 if self.reduction == 'mean': return loss.mean() elif self.reduction == 'sum': return loss.sum() else: return loss # 二分类 focal_loss = FocalLoss(alpha=0.75, gamma=2, num_classes=2) logits = torch.tensor([[1.2, -0.5], [-0.3, 2.1]], dtype=torch.float32) targets = torch.tensor([0, 1], dtype=torch.long) loss = focal_loss(logits, targets) print(f"FocalLoss(二分类): {loss.item():.4f}") # 3类缺陷 focal_loss_multi = FocalLoss(alpha=0.3, gamma=2, num_classes=3) logits_multi = torch.tensor([[1.0, 0.5, -0.2], [-0.1, 2.0, 0.8]], dtype=torch.float32) targets_multi = torch.tensor([0, 2], dtype=torch.long) loss_multi = focal_loss_multi(logits_multi, targets_multi) print(f"FocalLoss(多分类): {loss_multi.item():.4f}")
交并比损失--nn.IoULoss
核心直接优化预测框与真实框的交并比,这让损失函数与检测任务的核心评估指标完全对齐,能显著提升边界框的回归精度。其中IoU是衡量两个框(预测框、真实框)重叠程度的核心指标,取值范围 [0,1],越接近 1 表示重叠度越高
公式:
其中:平滑项,通常取1e-6,一般都需要取值,避免并集为0时的除零错误
框格式:一般都使用xyxy格式即左上x轴,左上y轴,右下x轴,右下y轴,需要统一预测框和真实框的格式
输入和输出的维度都统一为[N,4],N表示输入和输出的个数,4表示该框的格式为xyxy
import torch import torch.nn as nn class IoULoss(nn.Module): def __init__(self, eps=1e-6, reduction='mean'): super().__init__() self.eps = eps # 避免除零 self.reduction = reduction def forward(self, pred_boxes, target_boxes): # 1. 提取坐标 x1_p, y1_p, x2_p, y2_p = pred_boxes[:, 0], pred_boxes[:, 1], pred_boxes[:, 2], pred_boxes[:, 3] x1_t, y1_t, x2_t, y2_t = target_boxes[:, 0], target_boxes[:, 1], target_boxes[:, 2], target_boxes[:, 3] # 2. 计算交集坐标 x1_inter = torch.max(x1_p, x1_t) y1_inter = torch.max(y1_p, y1_t) x2_inter = torch.min(x2_p, x2_t) y2_inter = torch.min(y2_p, y2_t) # 3. 计算交集面积 inter_area = (x2_inter - x1_inter).clamp(min=0) * (y2_inter - y1_inter).clamp(min=0) # 4. 计算预测框/真实框面积 pred_area = (x2_p - x1_p).clamp(min=0) * (y2_p - y1_p).clamp(min=0) target_area = (x2_t - x1_t).clamp(min=0) * (y2_t - y1_t).clamp(min=0) # 5. 计算并集面积 + 平滑项 union_area = pred_area + target_area - inter_area + self.eps # 6. 计算IoU和IoULoss iou = inter_area / union_area iou_loss = 1 - iou # 7. 聚合损失 if self.reduction == 'mean': return iou_loss.mean() elif self.reduction == 'sum': return iou_loss.sum() else: return iou_loss # 工业缺陷检测:单框回归 iou_loss = IoULoss(eps=1e-6) # 模拟输入--两个框 pred_boxes = torch.tensor([[10, 20, 30, 40], [50, 60, 70, 80]], dtype=torch.float32) target_boxes = torch.tensor([[12, 22, 32, 42], [51, 61, 71, 81]], dtype=torch.float32) loss = iou_loss(pred_boxes, target_boxes) print(f"IoULoss: {loss.item():.4f}")
中心距交并比损失--nn.DIoULoss
DIoULoss在IoU基础上加入中心距离惩罚项,即使预测框与真实框无交集,也能通过 "中心距离" 产生梯度,迫使框快速对齐
公式:
其中IoU表示预测框与真实框的交并比为
表示预测框中心与真实框中心的欧式距离平方
表示预测框和真实框的最小包围框对角线长度的平方
import torch import torch.nn as nn class DIoULoss(nn.Module): def __init__(self, eps=1e-6, reduction='mean'): super().__init__() self.eps = eps # 避免除零,嵌入式必加 self.reduction = reduction # 嵌入式优先mean,减少运算 def forward(self, pred_boxes, target_boxes): # 1. 提取坐标并校验 x1_p, y1_p, x2_p, y2_p = pred_boxes[:, 0], pred_boxes[:, 1], pred_boxes[:, 2], pred_boxes[:, 3] x1_t, y1_t, x2_t, y2_t = target_boxes[:, 0], target_boxes[:, 1], target_boxes[:, 2], target_boxes[:, 3] # 坐标范围校验,假设图像宽度128 x1_p = torch.clamp(x1_p, min=0) y1_p = torch.clamp(y1_p, min=0) x2_p = torch.clamp(x2_p, max=127) y2_p = torch.clamp(y2_p, max=127) # 确保x1<=x2,y1<=y2 x1_p, x2_p = torch.min(x1_p, x2_p), torch.max(x1_p, x2_p) y1_p, y2_p = torch.min(y1_p, y2_p), torch.max(y1_p, y2_p) x1_t, x2_t = torch.min(x1_t, x2_t), torch.max(x1_t, x2_t) y1_t, y2_t = torch.min(y1_t, y2_t), torch.max(y1_t, y2_t) # 2. 计算IoU # 交集坐标 x1_inter = torch.max(x1_p, x1_t) y1_inter = torch.max(y1_p, y1_t) x2_inter = torch.min(x2_p, x2_t) y2_inter = torch.min(y2_p, y2_t) # 交集面积(无交集时为0) inter_area = (x2_inter - x1_inter).clamp(min=0) * (y2_inter - y1_inter).clamp(min=0) # 预测框/真实框面积 pred_area = (x2_p - x1_p) * (y2_p - y1_p) target_area = (x2_t - x1_t) * (y2_t - y1_t) # IoU计算 union_area = pred_area + target_area - inter_area + self.eps iou = inter_area / union_area # 3. 计算中心距离d²和包围框对角线D² # 中心坐标 cx_p, cy_p = (x1_p + x2_p) / 2, (y1_p + y2_p) / 2 cx_t, cy_t = (x1_t + x2_t) / 2, (y1_t + y2_t) / 2 # 中心距离平方 d2 = (cx_p - cx_t) ** 2 + (cy_p - cy_t) ** 2 # 最小包围框对角线平方 x1_c = torch.min(x1_p, x1_t) y1_c = torch.min(y1_p, y1_t) x2_c = torch.max(x2_p, x2_t) y2_c = torch.max(y2_p, y2_t) D2 = (x2_c - x1_c) ** 2 + (y2_c - y1_c) ** 2 + self.eps # 加eps避免除零 # 4. 计算DIoULoss diou = iou - (d2 / D2) diou_loss = 1 - diou # 5. 聚合损失 if self.reduction == 'mean': return diou_loss.mean() elif self.reduction == 'sum': return diou_loss.sum() else: return diou_loss # 工业裂纹检测,xyxy格式 diou_loss = DIoULoss(eps=1e-6) # 模拟预测框 pred_boxes = torch.tensor([[10, 20, 30, 40], [50, 60, 70, 80]], dtype=torch.float32) # 模拟真实框 target_boxes = torch.tensor([[12, 22, 32, 42], [51, 61, 71, 81]], dtype=torch.float32) loss = diou_loss(pred_boxes, target_boxes) print(f"DIoULoss(工业裂纹检测): {loss.item():.4f}")
边界损失--nn.BoundaryLoss
核心解决普通分割损失(如DiceLoss、CrossEntropyLoss)"只关注像素类别、忽略边界细节" 的问题 ------ 通过聚焦图像中目标与背景的边界像素,强制模型学习边界的精细特征,最终提升分割结果的边缘清晰度。
边界掩码加权
公式:
或者直接进行更简单的加权形式
其中BCE表示上述的二值掩码损失nn.BCEWithLogitsLoss
B表示边界像素集合
表示权重(边界像素
=k,k大于等于2;非边界
=1)
表示边界像素数量;N表示总像素数量
工业上一般混合DiceLoss损失使用
import torch import torch.nn as nn import torch.nn.functional as F class BoundaryLoss(nn.Module): def __init__(self, boundary_weight=5.0, eps=1e-6): super().__init__() self.boundary_weight = boundary_weight # 边界像素权重 self.eps = eps # 3x3膨胀核 self.dilate_kernel = torch.ones(1, 1, 3, 3, dtype=torch.float32) def _extract_boundary_mask(self, mask): # 膨胀操作 mask_dilated = F.conv2d(mask, self.dilate_kernel, padding=1) mask_dilated = (mask_dilated > 0).float() # 边界掩码 = 膨胀掩码 - 原始掩码 boundary_mask = (mask_dilated - mask).clamp(min=0) return boundary_mask def forward(self, pred, target): B, C, H, W = pred.shape # 1. 提取边界掩码 self.dilate_kernel = self.dilate_kernel.to(target.device) boundary_mask = self._extract_boundary_mask(target) non_boundary_mask = 1 - boundary_mask # 2. 计算像素级BCE损失 pred_sigmoid = torch.sigmoid(pred) bce = - (target * torch.log(pred_sigmoid + self.eps) + (1 - target) * torch.log(1 - pred_sigmoid + self.eps)) # 3. 加权计算边界损失:边界像素高权重,非边界低权重 boundary_loss = (bce * boundary_mask * self.boundary_weight).sum() non_boundary_loss = (bce * non_boundary_mask).sum() # 归一化:避免边界像素数量少导致损失占比过高 total_pixels = boundary_mask.sum() + non_boundary_mask.sum() + self.eps loss = (boundary_loss + non_boundary_loss) / total_pixels return loss # 混合损失:BoundaryLoss + DiceLoss class SegmentationLoss(nn.Module): def __init__(self, boundary_weight=5.0, dice_weight=0.5, boundary_loss_weight=0.5): super().__init__() self.boundary_loss = BoundaryLoss(boundary_weight=boundary_weight) self.dice_loss = DiceLoss() # 复用之前定义的DiceLoss self.dice_w = dice_weight self.boundary_w = boundary_loss_weight def forward(self, pred, target): dice_loss = self.dice_loss(pred, target) boundary_loss = self.boundary_loss(pred, target) return self.dice_w * dice_loss + self.boundary_w * boundary_loss class DiceLoss(nn.Module): def __init__(self, eps=1e-6): super().__init__() self.eps = eps def forward(self, pred, target): pred_sigmoid = torch.sigmoid(pred) intersection = (pred_sigmoid * target).sum() union = pred_sigmoid.sum() + target.sum() return 1 - (2 * intersection + self.eps) / (union + self.eps) # 模拟输入 seg_loss = SegmentationLoss(boundary_weight=3.0) pred = torch.randn(2, 1, 64, 64) target = torch.randint(0, 2, (2, 1, 64, 64)).float() loss = seg_loss(pred, target) print(f"SegmentationLoss(Dice+Boundary): {loss.item():.4f}")
使用场景
| 损失类型 | 具体损失函数 | 核心解决问题 | 适用任务(工业 CV) | 核心特点(嵌入式视角) |
|---|---|---|---|---|
| 像素级基础损失 | L1Loss | 简单数值回归,像素级误差衡量 | 简单图像回归(如低精度超分) | 计算量极低,嵌入式极易部署 |
| 分类专用损失 | CrossEntropyLoss | 普通类别平衡的分类 | 常规缺陷分类(背景 / 缺陷二分类) | 计算量低,嵌入式易部署 |
| FocalLoss | 类别不平衡 + 难样本挖掘(如小缺陷) | 工业缺陷检测(小目标 / 不平衡分类) | 略增计算量(幂运算),需轻量化,嵌入式核心选型 | |
| 分割专用损失 | DiceLoss | 分割任务类别不平衡(如缺陷掩码) | 缺陷分割(裂纹 / 划痕掩码) | 计算量低,嵌入式易部署 |
| BoundaryLoss | 分割边界精度优化(如缺陷边缘) | 缺陷分割(裂纹 / 零件轮廓) | 略增计算量(边界提取),需轻量化边界提取 | |
| 检测框回归损失 | SmoothL1Loss | 框坐标回归(鲁棒性) | 目标检测框回归(基础版) | 计算量低,嵌入式易部署 |
| IoULoss/DIoULoss | 直接优化 IoU(框重叠度) | 高精度缺陷检测框回归(核心) | 略增计算量(面积 / 距离计算),嵌入式优先 DIoULoss | |
| 生成 / 增强专用损失 | PerceptualLoss | 高层语义特征匹配(视觉保真) | 图像超分 / 修复(内容保真) | 中等计算量(CNN 特征提取),需轻量化模型 |
Pytorch nn模块
正如上文所说,pytorch有一个专门用于神经网络的子模块,即nn模块,而nn模块除了上述的激活函数外,还有一个非常重要且基础的基类,也就是Module,其实Pytorch中所有神经网络模块的基类
nn.Linear
class torch.nn.Linear(in_features: int,
out_features: int,
bias: bool = True,
device=None,
dtype=None)
in_features:输入样本特征维度的大小,也是上一层的输出样本特征维度的大小
out_features:输出样本特征维度的大小,也是下一层的输入样本特征维度的大小
bias:该层神经网络是否学习一个偏置即b,默认为True
device:运行设备
dtype:数据类型
当为True时,y=wx+b
x是输入,形状为
(..., in_features),...表示任意的批次维度。y是输出,形状为
(..., out_features)。
nn.Linear是nn.Module的一个子类,其主要是通过参数属性、权重和偏置对输入应用仿射变换,也就是我们上一篇对于温度计的表述
import torch import torch.optim as optim def model(t_u, w, b): return t_u * w + b def loss_fn(t_p, t_c): loss = (t_p - t_c) ** 2 return loss.mean() def training_loop(n_epochs, optimizer, params, train_t_c, val_t_c, train_t_u, val_t_u): """训练模型""" for epoch in range(1, n_epochs + 1): train_t_p = model(train_t_u, *params) train_loss = loss_fn(train_t_p, train_t_c) val_t_p = model(val_t_u, *params) val_loss = loss_fn(val_t_p, val_t_c) optimizer.zero_grad() train_loss.backward() optimizer.step() print('Epoch %d, Training Loss %f, Validation Loss %f' % (epoch, float(train_loss), float(val_loss))) return params # 实际值(期望值)和输入值 t_c = [0.5, 14.0, 15.0, 28.0, 11.0, 8.0, 3.0, -4.0, 6.0, 13.0, 21.0] t_u = [35.7, 55.9, 58.2, 81.9, 56.3, 48.9, 33.9, 21.8, 48.4, 60.4, 68.4] t_c = torch.tensor(t_c) t_u = torch.tensor(t_u) # 交叉验证 t_un = t_u * 0.01 train_t_c = t_c[0:8] val_t_c = t_c[8:12] train_t_u = t_un[0:8] val_t_u = t_un[8:12] params = torch.tensor([1.0, 0.0], requires_grad = True) learning_rate = 0.1 optimizer = optim.SGD([params], lr = learning_rate) training_loop(n_epochs = 5000, optimizer = optimizer, params = params, train_t_c = train_t_c, val_t_c = val_t_c, train_t_u = train_t_u, val_t_u = val_t_u)这里面我们是没有引入nn模块的,接下来便是引入nn模块修改它
import torch import torch.optim as optim import torch.nn as nn def training_loop(n_epochs, optimizer, model, loss_fn, train_t_c, val_t_c, train_t_u, val_t_u): """训练模型""" for epoch in range(1, n_epochs + 1): train_t_p = model(train_t_u) train_loss = loss_fn(train_t_p, train_t_c) val_t_p = model(val_t_u) val_loss = loss_fn(val_t_p, val_t_c) optimizer.zero_grad() train_loss.backward() optimizer.step() print('Epoch %d, Training Loss %f, Validation Loss %f' % (epoch, float(train_loss), float(val_loss))) # 实际值(期望值)和输入值 t_c = [0.5, 14.0, 15.0, 28.0, 11.0, 8.0, 3.0, -4.0, 6.0, 13.0, 21.0] t_u = [35.7, 55.9, 58.2, 81.9, 56.3, 48.9, 33.9, 21.8, 48.4, 60.4, 68.4] t_c = torch.tensor(t_c) t_u = torch.tensor(t_u) # 交叉验证 t_un = t_u.sum() / 11 t_un = t_u / t_un train_t_c = t_c[0:8].unsqueeze(1) val_t_c = t_c[8:12].unsqueeze(1) train_t_u = t_un[0:8].unsqueeze(1) val_t_u = t_un[8:12].unsqueeze(1) # 构造模型 linear_model = nn.Linear(1, 1) loss_fn = nn.MSELoss() learning_rate = 0.01 optimizer = optim.SGD(params = linear_model.parameters(), lr = learning_rate) training_loop(n_epochs = 5000, optimizer = optimizer, model = linear_model, loss_fn = loss_fn, train_t_c = train_t_c, val_t_c = val_t_c, train_t_u = train_t_u, val_t_u = val_t_u) print(linear_model.weight) print(linear_model.bias)
nn.Sequential
用于将多个网络层按顺序打包成一个整体的、可调用的模块。当输入数据"流经"这个模块时,它会严格按照你定义的顺序,依次通过每一层。同时其会自动生成forward方法,不需要独自去定义
model = nn.Sequential(nn.Conv2d(1,20,5),nn.ReLU(),nn.Conv2d(20,64,5),nn.ReLU())输入数据首先会传递给 Conv2d(1,20,5)。Conv2d(1,20,5)的输出将用作第一个ReLU的输入;第一个ReLU的输出将成为Conv2d(20,64,5)的输入。最后Conv2d(20,64,5)的输出将用作第二个ReLU的输入。
一个更好的对比
import torch.nn as nn # ========== 方法一:标准的 nn.Module 子类写法(手动定义forward) ========== class NetManual(nn.Module): def __init__(self, in_features=10, out_features=2): super().__init__() self.fc1 = nn.Linear(in_features, 50) # 第一层 self.relu1 = nn.ReLU() self.fc2 = nn.Linear(50, 20) # 第二层 self.relu2 = nn.ReLU() self.out = nn.Linear(20, out_features) # 输出层 def forward(self, x): # 必须手动写出每一层的调用顺序和连接方式 x = self.fc1(x) x = self.relu1(x) x = self.fc2(x) x = self.relu2(x) x = self.out(x) return x model_manual = NetManual() # ========== 方法二:使用 nn.Sequential 写法 ========== # 优点:代码极其简洁,自动定义forward model_seq = nn.Sequential( nn.Linear(10, 50), nn.ReLU(), nn.Linear(50, 20), nn.ReLU(), nn.Linear(20, 2) ) # model_manual.out == model_seq[-1]如果你的网络有跳跃、多分支或多输入多输出,仍然需要自定义
nn.Module并在forward中描述复杂逻辑。同时数据必须依次经过序列中的每一层,无法根据条件跳过某一层。
以下是两个完整的数据拟合代码
pythonfrom typing import OrderedDict import torch import torch.optim as optim import torch.nn as nn from matplotlib import pyplot as plt import time def training_loop(n_epochs, optimizer, model, loss_fn, train_t_c, val_t_c, train_t_u, val_t_u): """训练模型""" for epoch in range(1, n_epochs + 1): train_t_p = model(train_t_u) train_loss = loss_fn(train_t_p, train_t_c) val_t_p = model(val_t_u) val_loss = loss_fn(val_t_p, val_t_c) optimizer.zero_grad() train_loss.backward() optimizer.step() print('Epoch %d, Training Loss %f, Validation Loss %f' % (epoch, float(train_loss), float(val_loss))) # 实际值(期望值)和输入值 t_c = [0.5, 14.0, 15.0, 28.0, 11.0, 8.0, 3.0, -4.0, 6.0, 13.0, 21.0] t_u = [35.7, 55.9, 58.2, 81.9, 56.3, 48.9, 33.9, 21.8, 48.4, 60.4, 68.4] t_c = torch.tensor(t_c) t_u = torch.tensor(t_u) # 交叉验证 t_u_mean = t_u.mean() t_u_std = t_u.std() t_un = (t_u - t_u_mean)/t_u_std train_t_c = t_c[0:8].unsqueeze(1) val_t_c = t_c[8:12].unsqueeze(1) train_t_u = t_un[0:8].unsqueeze(1) val_t_u = t_un[8:12].unsqueeze(1) # 构造模型 loss_fn = nn.MSELoss() seq_model = nn.Sequential(OrderedDict([ ('hidden_linear', nn.Linear(1, 11)), ('hidden_activation', nn.Tanh()), ('output_linear', nn.Linear(11, 1)) ])) learning_rate = 0.01 optimizer = optim.SGD(params = seq_model.parameters(), lr = learning_rate) epoch_start = time.time() training_loop(n_epochs = 100, optimizer = optimizer, model = seq_model, loss_fn = loss_fn, train_t_c = train_t_c, val_t_c = val_t_c, train_t_u = train_t_u, val_t_u = val_t_u) epoch_end = time.time() total_time = epoch_end - epoch_start print(f"总训练时间: {total_time:.2f} 秒") print('output', seq_model(val_t_u)) print('answer', val_t_c) print('hidden', seq_model.hidden_linear.weight.grad) # 绘制结果 t_range = torch.arange(20., 90.).unsqueeze(1) # 形状: (70, 1) # 对t_range使用相同的标准化 t_range_normalized = (t_range - t_u_mean) / t_u_std fig = plt.figure(dpi=100) plt.xlabel('Fahrenheit') plt.ylabel('Celsius') plt.plot(t_u.numpy(), t_c.numpy(), 'o', label='Original data') plt.plot(t_range.numpy(), seq_model(t_range_normalized).detach().numpy(), 'c-', label='Model prediction') plt.plot(t_u.numpy(), seq_model(t_un.unsqueeze(1)).detach().numpy(), 'kx', label='Predicted points') plt.legend() plt.show()
下面的代码是一个拥有11个指标和1个输出的数据集,每个类型有接近5000个值
pythonfrom typing import OrderedDict import torch import torch.optim as optim import torch.nn as nn from matplotlib import pyplot as plt import time def training_loop(n_epochs, optimizer, model, loss_fn, train_t_c, val_t_c, train_t_u, val_t_u): """训练模型""" for epoch in range(1, n_epochs + 1): train_t_p = model(train_t_u) train_loss = loss_fn(train_t_p, train_t_c) val_t_p = model(val_t_u) val_loss = loss_fn(val_t_p, val_t_c) optimizer.zero_grad() train_loss.backward() optimizer.step() print('Epoch %d, Training Loss %f, Validation Loss %f' % (epoch, float(train_loss), float(val_loss))) # 实际值(期望值)和输入值 t_c = [0.5, 14.0, 15.0, 28.0, 11.0, 8.0, 3.0, -4.0, 6.0, 13.0, 21.0] t_u = [35.7, 55.9, 58.2, 81.9, 56.3, 48.9, 33.9, 21.8, 48.4, 60.4, 68.4] t_c = torch.tensor(t_c) t_u = torch.tensor(t_u) # 交叉验证 t_u_mean = t_u.mean() t_u_std = t_u.std() t_un = (t_u - t_u_mean)/t_u_std train_t_c = t_c[0:8].unsqueeze(1) val_t_c = t_c[8:12].unsqueeze(1) train_t_u = t_un[0:8].unsqueeze(1) val_t_u = t_un[8:12].unsqueeze(1) # 构造模型 loss_fn = nn.MSELoss() seq_model = nn.Sequential(OrderedDict([ ('hidden_linear', nn.Linear(1, 11)), ('hidden_activation', nn.Tanh()), ('output_linear', nn.Linear(11, 1)) ])) learning_rate = 0.01 optimizer = optim.SGD(params = seq_model.parameters(), lr = learning_rate) epoch_start = time.time() training_loop(n_epochs = 100, optimizer = optimizer, model = seq_model, loss_fn = loss_fn, train_t_c = train_t_c, val_t_c = val_t_c, train_t_u = train_t_u, val_t_u = val_t_u) epoch_end = time.time() total_time = epoch_end - epoch_start print(f"总训练时间: {total_time:.2f} 秒") print('output', seq_model(val_t_u)) print('answer', val_t_c) print('hidden', seq_model.hidden_linear.weight.grad) # 绘制结果 t_range = torch.arange(20., 90.).unsqueeze(1) # 形状: (70, 1) # 对t_range使用相同的标准化 t_range_normalized = (t_range - t_u_mean) / t_u_std fig = plt.figure(dpi=100) plt.xlabel('Fahrenheit') plt.ylabel('Celsius') plt.plot(t_u.numpy(), t_c.numpy(), 'o', label='Original data') plt.plot(t_range.numpy(), seq_model(t_range_normalized).detach().numpy(), 'c-', label='Model prediction') plt.plot(t_u.numpy(), seq_model(t_un.unsqueeze(1)).detach().numpy(), 'kx', label='Predicted points') plt.legend() plt.show()

