YOLOv3 损失函数详解【论文笔记 + pytorch实现 附.pth权重文件】

本文深入解析YOLOv3目标检测模型,涵盖Darknet-53网络结构、图像预处理、损失函数计算等核心内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1 前言

     个人感觉YOLOv3论文写的真的很随意,首先大家可以感受下。作者在Introduction中是这样开头的:

"Sometimes you just kinda phone it in for a year, you know? I didn’t do a whole lot of research this year. Spent a lot of time on Twitter."

"其实有时候有些人一年的时间就这样蹉跎了,你懂的。所以去年我也没做什么研究,主要用来刷 Twitter 了。"

     作者是这样结尾的:“In closing, do not @ me. (because I finally quit Twitter)”.
     真是太随意了,不过毕竟是大牛,说的废话都可以被当做经典。不废话了,说说YOLOv3到底干了个啥。

2 Darknet-53网络结构

     首先回想一下 YOLOv2中提出的Darknet-19网络结构作为主干特征提取网络。考虑到对于小物体的检测,结合FPN(特征金字塔)的思想,YOLOv2简单添加一个 passthrough layer,把浅层特征图(分辨率为26 × 26,即提取特征图的倒数第二卷积层结果)连接到深层特征图。通过把高低分辨率的特征图做连结,叠加相邻特征到不同通道(而非空间位置),类似于ResNet中的identity mappings。

     在YOLOv3中,作者可能觉得Darknet-19网络还是不够深(因为更深的网络结构可以学习到更加丰富的特征),故再次借鉴ResNet网络和FPN(特征金字塔)的思想,提出了Darknet-53网络结构,如下图所示(图片来源)。
     Darknet53中的Residual Block进行一次3X3、步长为2的卷积,然后保存该卷积结果layer;再进行一次1X1的卷积和一次3X3的卷积,并把这个结果加上layer作为最后的结果。 残差网络的特点是容易优化,并且能够通过增加相当的深度来提高准确率。其内部的残差块使用了跳跃连接,缓解了在深度神经网络中增加深度带来的梯度消失问题。

     上图中左半部分虚线框内即为Darknet-53网络机构,可以看到该网络结构的输入为 416×416×3,之后通过一个3×3的卷积层来扩增通道数。接下来通过堆叠一系列Residual Block来构建网络,其具体个数为[1, 2, 8, 8, 4],最终主干网络输出大小为13×13、26×26、52×52三个大小的特征图,目的是可以检测到图像中更小的物体。特征图分割越密集,则每一个特征点相对于原图中的区域越小,从而可以监测到更小的物体。

     下图为9种先验框的尺寸,其中蓝色框为聚类得到的先验框。黄色框是ground truth,红框是检测对象中心点所在的网格。

     Darknet-53主干网络代码如下:

import torch
import torch.nn as nn
import math
from collections import OrderedDict

# 基本的darknet块

class BasicBlock(nn.Module):
    def __init__(self, inplanes, planes):  # resnet block中是 先进行一个1×1卷积 再进行一个3×3卷积
        super(BasicBlock, self).__init__()
        self.conv1 = nn.Conv2d(inplanes, planes[0], kernel_size=1,  # 1×1卷积目的是下降通道数
                               stride=1, padding=0, bias=False)
        self.bn1 = nn.BatchNorm2d(planes[0])
        self.relu1 = nn.LeakyReLU(0.1)
        
        self.conv2 = nn.Conv2d(planes[0], planes[1], kernel_size=3,  # 3×3卷积目的是扩张通道数,注意这里并不减少特征图的大小!!
                               stride=1, padding=1, bias=False)      # 这样做可以帮助减少参数量
        self.bn2 = nn.BatchNorm2d(planes[1])
        self.relu2 = nn.LeakyReLU(0.1)

    def forward(self, x):
        residual = x

        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu1(out)

        out = self.conv2(out)
        out = self.bn2(out)
        out = self.relu2(out)

        out += residual
        return out

class DarkNet(nn.Module):
    def __init__(self, layers):
        super(DarkNet, self).__init__()
        self.inplanes = 32
        self.conv1 = nn.Conv2d(3, self.inplanes, kernel_size=3, stride=1, padding=1, bias=False)  # 第一个卷积 3->32
        self.bn1 = nn.BatchNorm2d(self.inplanes)
        self.relu1 = nn.LeakyReLU(0.1)

        self.layer1 = self._make_layer([32, 64], layers[0])
        self.layer2 = self._make_layer([64, 128], layers[1])
        self.layer3 = self._make_layer([128, 256], layers[2])
        self.layer4 = self._make_layer([256, 512], layers[3])
        self.layer5 = self._make_layer([512, 1024], layers[4])

        self.layers_out_filters = [64, 128, 256, 512, 1024]

        # 进行权值初始化
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
                m.weight.data.normal_(0, math.sqrt(2. / n))
            elif isinstance(m, nn.BatchNorm2d):
                m.weight.data.fill_(1)
                m.bias.data.zero_()

    def _make_layer(self, planes, blocks):  # 进行下采样且不断堆叠残差块
        layers = []
        # 下采样,步长为2,卷积核大小为3,用于减少特征图尺寸
        layers.append(("ds_conv", nn.Conv2d(self.inplanes, planes[1], kernel_size=3,
                                stride=2, padding=1, bias=False)))
        layers.append(("ds_bn", nn.BatchNorm2d(planes[1])))
        layers.append(("ds_relu", nn.LeakyReLU(0.1)))
        # 加入darknet模块   
        self.inplanes = planes[1]
        for i in range(0, blocks):
            layers.append(("residual_{}".format(i), BasicBlock(self.inplanes, planes)))
        return nn.Sequential(OrderedDict(layers))

    def forward(self, x):
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu1(x)

        x = self.layer1(x)
        x = self.layer2(x)
        out3 = self.layer3(x)
        out4 = self.layer4(out3)
        out5 = self.layer5(out4)

        return out3, out4, out5

# pretrained为权重文件路径
def darknet53(pretrained, **kwargs):
    model = DarkNet([1, 2, 8, 8, 4])
    if pretrained:
        if isinstance(pretrained, str):
            model.load_state_dict(torch.load(pretrained))
        else:
            raise Exception("darknet request a pretrained path. got [{}]".format(pretrained))
    return model

     网络结构的右半部分包括将主干网络Darknet-53的输出进行生成特征金字塔。具体做法为首先将13×13×1024的特征层进行上采样变为26×26×256,再与26×26×512的特征层进行堆叠,其结果为26×26×768大小的特征;同理,大小为52×52×256的特征层与下一层网络堆叠后的结果为52×52×384
     对于这三个堆叠后的结果,分别在进行5次卷积操作,最后接上一个3×3的卷积和1×1的卷积,用于输出预测结果,三个层对应的输出结果大小分别为13×13×75、26×26×75、52×52×75。其中75 = (4+1+20)×3,4表示一个边界框对应的调整参数x, y, w, h,1表示置信度,20表示VOC数据集分为20个类,3表示特征图上的每一个点对应有3个anchor。

3 输入图片Resize

     输入该主干网络的图片为任意大小,故在进行特征提取之前需要进行Resize。这里将图片的相对较长的边缩小到416,在保持图片比例不变的情况下对短边进行缩减;最后用灰色条对空白部分进行填充。这样操作使得原图片不会失真,如下图若直接对原图进行resize为416×416,图片中的孩子就没那么可爱了。。。

     图片Resize函数代码:

def letterbox_image(image, size):
    iw, ih = image.size
    w, h = size
    scale = min(w/iw, h/ih)
    nw = int(iw*scale)
    nh = int(ih*scale)

    image = image.resize((nw, nh), Image.BICUBIC)
    new_image = Image.new('RGB', size, (128, 128, 128))
    # 将原图像paste到中心
    new_image.paste(image, ((w-nw)//2, (h-nh)//2))
    return new_image

4 YOLOv3的损失函数

     YOLOv3论文中也没有对训练过程中的损失函数进行详细介绍,通过查看源码、参考各种博客、观察运行过程中的数据弄明白了loss的计算过程。

Loss的计算共由四部分组成:

  1. 中心点 x、y 的调整参数;
  2. anchor的宽高w、h的调整参数;
  3. 置信度Conf;
  4. 类别预测Cls;

     下面将分别对真实框和预测框在以上四个方面的表示进行详细介绍。

4.1 真实框

     我们知道Loss是由图片的真实标签和网络对图片的预测结果两者通过一定的函数计算出来的。对于真实标签,仍利用以下这张图举例。我们知道 .xml文件是对每张图片的标注,这张图片对应的 .xml文件标注内容如下。

     这张图片的大小为 500×333。可以看到,对于该图像的标注,给定的是标注框的左上角坐标(168, 2)和右下角坐标(500, 331),经过Resize后图片尺寸成为416×416,并且将该图片的左上角和右下角标注进行相应变换。
     而在计算损失Loss时,首先将该图片对应的真实框转换为 [0.230, 0.200, 1, 0.795, 12](预估计),即首先需要将真实框的坐标转换为相对于原点的归一化坐标,12 表示person这个类。其次以下将全部基于特征图尺寸的大小进行。真正输入到Loss函数中进行计算的真实标签值是下图中表示的tx ,tx即为真实框中心点对应于该中心点落入特征图某点的左上角的尺寸(假定特征图每个点的长度为1),ty 同理。

     下图将粉色框放大以表示tx, ty的值:

     以上为真实框中心点用于计算Loss的tx, ty的表示,对于真实框用于计算Loss的宽高tw, th的表示如下:

     对于置信度Conf 损失项,真实框的置信度均为1;

     对于类别Cls损失项,真实框对应的类别为1,其余类别均为0。

4.2 预测框

     之前提到过,网络预测结果的输出大小为13×13×75、26×26×75、52×52×75,其中75 = (4+1+20)×3,4表示一个边界框对应的调整参数x, y, w, h,1表示置信度,20表示VOC数据集分为20个类,3表示特征图上的每一个点对应有3个anchor。对于每个anchor,都有以下的25个数字为一组的预测结果。

     0       	   1 		     2 			3 	        4 	    		  5:25
x的调整参数   y的调整参数   w的调整参数   h的调整参数   置信度 		20种类别的预测概率

     对以上预测结果拆分成四个部分对应Loss的四个部分,即可输入到损失函数中进行计算。

     那么预测结果中的这些调整参数到底有什么用呢?在下图中,黑色虚线框是原anchor尺寸,蓝色框是通过x, y, w, h的调整参数调整之后的框框,目的就是使anchor经过调整后更加毕竟真实框。

4.3 具体的损失函数

     YOLOv3的损失函数,不同部分的损失项使用了不同的损失函数。首先介绍使用到的两种损失函数,再具体对应到哪一损失项使用了那种损失函数。

     (1) BCELoss(Binary Cross Entropy)
     该损失函数用于二分类任务的交叉熵计算函数,其计算公式为:
      𝑙𝑛 = − 𝑦𝑛×𝑙𝑜𝑔(𝑥𝑛) - (1 − 𝑦𝑛)×𝑙𝑜𝑔(1 − 𝑥𝑛)
     函数代码块为:

def BCELoss(pred, target):
    epsilon = 1e-7
    pred = clip_by_tensor(pred, epsilon, 1.0 - epsilon)
    output = -target * torch.log(pred) - (1.0 - target) * torch.log(1.0 - pred)
    return output
    
def clip_by_tensor(t, t_min, t_max):
    t = t.float()
    result = (t >= t_min).float() * t + (t < t_min).float() * t_min
    result = (result <= t_max).float() * result + (result > t_max).float() * t_max
    return result

     (2)MSELoss(Mean Square Entropy),即为平方差损失函数,其代码块为:

def MSELoss(pred, target):
    return 0.5 * (pred-target)**2

损失函数Loss的四个部分具体使用的损失函数对应如下:

  1. 中心点 x、y 的调整参数 —— BCELoss
  2. anchor的宽高w、h的调整参数 —— MSELoss
  3. 置信度Conf —— BCELoss
  4. 类别预测Cls —— BCELoss

     可以看到,仅有w, h 的调整参数项使用了MSELoss,其余均为BCELoss。整个损失函数的部分代码块如下:

		# x为预测调整值 tx为真实调整值
loss_x = torch.sum(BCELoss(x, tx) / bs * box_loss_scale * mask)  
loss_y = torch.sum(BCELoss(y, ty) / bs * box_loss_scale * mask)

loss_w = torch.sum(MSELoss(w, tw) / bs * 0.5 * box_loss_scale * mask)
loss_h = torch.sum(MSELoss(h, th) / bs * 0.5 * box_loss_scale * mask)

loss_conf = torch.sum(BCELoss(conf, mask) * mask / bs) + torch.sum(BCELoss(conf, mask) * noobj_mask / bs)

loss_cls = torch.sum(BCELoss(pred_cls[mask == 1], tcls[mask == 1])/bs)
		 # loss 为对每一项分别乘对应的系数后求和,即为总loss
loss = loss_x * self.lambda_xy + loss_y * self.lambda_xy + \
loss_w * self.lambda_wh + loss_h * self.lambda_wh + \
loss_conf * self.lambda_conf + loss_cls * self.lambda_cls

5 把需要的论文、代码和权重文件带走

     论文地址:YOLOv3: An Incremental Improvement
     以上是对YOLOv3模型的具体阐释总结,包括Darknet-53特征提取网络、其图片预处理方式、训练过程的具体损失函数计算等等,具体学习还需要系统的阅读代码。

     作者源代码地址:https://github.com/bubbliiiing/yolo3-pytorch

     本人fork自学注释后github地址:https://github.com/Bryce-HJ/yolov3_pytorch

     该代码附有详细注释,结合本文应该可以很顺利的看懂。若有问题欢迎评论区留言。

     还是老样子,本文使用的网络 .pth权重文件 ,大小为236M,下载该权重文件放入对应文件夹model_data,即可直接对图片进行预测。

权重文件获取方式:关注【OAOA】回复【yolo3】即可获取。

<think>我们正在处理用户的问题:如何将pytorch_model.bin文件转换为.pth文件? 根据引用[1]和引用[2]的信息,我们知道: - .bin文件(如pytorch_model.bin)通常保存的是PyTorch模型的权重(state_dict),而不是完整的模型(包含结构和权重)。 - .pth文件通常也是保存模型权重(state_dict)的文件,在PyTorch中常用作保存和加载模型权重。 因此,实际上,pytorch_model.bin和.pth文件在内容上都是模型的权重字典(state_dict)。它们的区别主要在于文件扩展名,但PyTorch的`torch.load`和`torch.save`并不关心扩展名,只要文件内容正确即可。 所以,转换操作本质上就是重命名文件?但是,为了确保正确性,我们可以先加载.bin文件,然后将其保存为.pth格式。这样也可以确保文件格式的兼容性。 步骤: 1. 加载.bin文件(即加载state_dict) 2. 将state_dict保存为.pth文件 注意:如果用户希望保存的是完整的模型(包括结构),那么需要先有模型结构,然后加载权重,再保存。但用户的问题只是将.bin转成.pth,通常.pth也是只保存权重的,所以直接转换即可。 但是,引用[1]中提到,.bin文件可能是通过`model.save_pretrained("save_dir", save_format="pt")`生成的,这实际上生成的是pytorch_model.bin(即权重文件)。而.pth文件也是用同样的方式保存权重的。 因此,我们可以这样操作: 方法一(直接重命名): 由于.bin和.pth文件都是PyTorch可以识别的state_dict序列化文件,所以可以直接将pytorch_model.bin重命名为model.pth(或其他.pth文件)。但要注意,PyTorch在加载时并不关心扩展名,所以这种方法在大多数情况下有效,但可能在某些框架中约定俗成使用特定扩展名。 方法二(使用PyTorch重新保存): 为了确保文件格式正确,我们可以用PyTorch加载这个.bin文件,然后再保存为.pth格式。 代码示例: ```python import torch # 加载.bin文件(得到state_dict) state_dict = torch.load(&#39;pytorch_model.bin&#39;) # 保存为.pth文件 torch.save(state_dict, &#39;model.pth&#39;) ``` 但是,注意:如果.bin文件是来自Hugging Face等库的预训练模型,它可能包含的不仅仅是模型权重,还可能包含优化器状态等其他信息?但通常pytorch_model.bin只包含模型权重。所以上述方法是可行的。 另外,引用[4]中提到,如果缺少某些文件(如pytorch_model.bin)时,可以通过重命名.safetensors文件来解决。这也说明这些文件本质上都是模型权重的不同存储格式。 因此,我们推荐使用方法二,因为它可以确保文件被正确加载和保存,避免可能的格式问题。 总结步骤: 1. 确保已安装PyTorch。 2. 使用上述Python脚本加载.bin文件并保存为.pth文件。 注意事项: - 如果.bin文件非常大,确保有足够的内存。 - 转换后的.pth文件可以直接在PyTorch中使用`model.load_state_dict(torch.load(&#39;model.pth&#39;))`加载。 根据用户的问题,我们给出如下回答:</think>### 如何将 `pytorch_model.bin` 文件转换为 `.pth` 文件PyTorch 中,`pytorch_model.bin` 和 `.pth` 文件本质上都是模型权重文件(包含 `state_dict`),转换操作非常简单,只需以下两步: #### 🔧 转换步骤 1. **加载 `.bin` 文件** 使用 PyTorch 加载权重字典: ```python import torch # 加载 .bin 文件中的权重字典 state_dict = torch.load("pytorch_model.bin") ``` 2. **保存为 `.pth` 文件** 将权重字典保存为标准 PyTorch 格式: ```python # 保存为 .pth 文件(默认保存 state_dict) torch.save(state_dict, "model_weights.pth") ``` #### ⚠️ 注意事项 1. **文件内容一致性** `.bin` 和 `.pth` 文件PyTorch 中**本质相同**,都是序列化的 `state_dict` 字典。转换仅改变文件扩展名,不改变内容格式[^1][^2]。 2. **模型结构要求** 加载 `.pth` 文件时,需**提前定义匹配的模型结构**: ```python model = YourModelClass() # 需与原始模型结构一致 model.load_state_dict(torch.load("model_weights.pth")) ``` 3. **完整模型保存(可选)** 若需保存**完整模型**(含结构和权重),使用: ```python torch.save(model, "full_model.pth") # 保存整个模型对象 ``` #### 💡 常见问题解答 **Q:转换后加载失败?** - ✅ 检查模型结构是否与权重匹配 - ✅ 验证文件路径是否正确 - ✅ 确保 PyTorch 版本兼容(使用 `torch.__version__` 检查) **Q:`.bin` 和 `.pth` 有何区别?** - `.bin`:通用二进制格式,常见于 HuggingFace 等框架 - `.pth`:PyTorch 标准权重格式,更易被 PyTorch 生态工具识别[^2][^5] > 提示:若需转换 TensorFlow 或 ONNX 格式,需使用跨框架工具(如 `onnxruntime` 或 `tf2onnx`),但 `.bin` → `.pth` 本质是 PyTorch 内部的权重格式统一操作。 --- ### 相关问题 1. 如何解决加载 `.pth` 文件时的模型结构不匹配错误? 2. PyTorch 中 `.pth` 和 `.ckpt` 格式有何区别? 3. 如何将 `.bin` 权重直接加载到 HuggingFace 的 Transformers 模型? 4. 为什么转换后的文件大小会发生变化? 5. 如何在 C++ 中加载 PyTorch 的 `.pth` 模型文件? [^1]: 引用自《深度解析:如何在PyTorch实现.pt模型文件.bin二进制格式的高效转换》 [^2]: 引用自《pytorch模型里 safetensors 文件、bin文件pth文件区别、加载和保存方式》 [^5]: 引用自 PyTorch 官方文档和社区实践
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值