# Highway-Training-Datasets-2318 **Repository Path**: Vision-Studios/Highway-Training-Datasets-2318 ## Basic Information - **Project Name**: Highway-Training-Datasets-2318 - **Description**: 高速公路场景检测系统源码和数据集:改进yolo11-SPPF-LSKA - **Primary Language**: Python - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 1 - **Created**: 2024-12-17 - **Last Updated**: 2025-08-30 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ### 1.背景意义 研究背景与意义 随着城市化进程的加快和交通流量的增加,高速公路的安全管理和交通监控变得愈发重要。高速公路场景的实时检测与分析不仅能够提高交通管理的效率,还能有效降低交通事故的发生率。因此,开发一种高效、准确的高速公路场景检测系统具有重要的现实意义和应用价值。近年来,深度学习技术的迅猛发展为计算机视觉领域带来了新的机遇,尤其是目标检测算法的不断演进,使得实时监控和场景理解成为可能。 YOLO(You Only Look Once)系列算法因其快速和高效的特性,广泛应用于各种目标检测任务。YOLOv11作为该系列的最新版本,结合了多种先进的技术,具备更强的特征提取能力和更高的检测精度。通过对YOLOv11的改进,可以针对高速公路场景的特定需求进行优化,从而提升检测系统在复杂环境下的表现。 本研究基于“Highway Training Datasets 2”数据集,该数据集包含1300张高速公路场景的图像,经过精确标注,能够为模型的训练提供丰富的样本。数据集中仅包含一个类别“Highway”,这使得模型在特定场景下的检测更加聚焦,有助于提高模型的准确性和鲁棒性。通过对该数据集的深入分析与处理,结合改进的YOLOv11模型,可以实现对高速公路场景的高效检测,进而为交通管理部门提供实时的监控和预警支持。 综上所述,基于改进YOLOv11的高速公路场景检测系统的研究,不仅有助于提升交通安全管理的智能化水平,也为未来的智能交通系统建设奠定了基础。通过这一研究,期望能够为相关领域的学术研究和实际应用提供有价值的参考与借鉴。 ### 2.视频效果 [2.1 视频效果](https://www.bilibili.com/video/BV1ZjkjYsESN/) ### 3.图片效果 ![1.png](1.png) ![2.png](2.png) ![3.png](3.png) ##### [项目涉及的源码数据来源链接](https://kdocs.cn/l/cszuIiCKVNis)** 注意:本项目提供训练的数据集和训练教程,由于版本持续更新,暂不提供权重文件(best.pt),请按照6.训练教程进行训练后实现上图演示的效果。 ### 4.数据集信息 ##### 4.1 本项目数据集类别数&类别名 nc: 1 names: ['Highway'] 该项目为【图像分割】数据集,请在【训练教程和Web端加载模型教程(第三步)】这一步的时候按照【图像分割】部分的教程来训练 ##### 4.2 本项目数据集信息介绍 本项目数据集信息介绍 本项目所使用的数据集名为“Highway Training Datasets 2”,专门为高速公路场景检测系统的训练而设计。该数据集的主要目标是提升YOLOv11模型在高速公路环境中的物体检测能力,确保其在复杂交通条件下的准确性和实时性。数据集中包含的类别数量为1,唯一的类别为“Highway”,这意味着所有的数据样本均围绕高速公路的特征进行标注和分类。 在数据集的构建过程中,特别注重了多样性和代表性,以确保模型能够适应不同的高速公路场景。这些场景包括但不限于城市高速公路、乡村公路、夜间行驶以及不同天气条件下的高速公路。数据集中的图像来源广泛,涵盖了各种时间段和光照条件,力求在训练过程中提供丰富的视觉信息,以增强模型的泛化能力。 此外,数据集还包含了多种角度和视角下的高速公路图像,确保模型能够学习到高速公路的不同特征,如车道线、交通标志、车辆行驶状态等。这些信息对于提高YOLOv11在实际应用中的表现至关重要。通过使用“Highway Training Datasets 2”,我们期望能够显著提升模型在高速公路场景中的检测精度,进而为智能交通系统的应用提供强有力的技术支持。总之,本数据集的构建和应用为高速公路场景检测的研究提供了坚实的基础,推动了相关技术的发展。 ![4.png](4.png) ![5.png](5.png) ![6.png](6.png) ![7.png](7.png) ![8.png](8.png) ### 5.全套项目环境部署视频教程(零基础手把手教学) [5.1 所需软件PyCharm和Anaconda安装教程(第一步)](https://www.bilibili.com/video/BV1BoC1YCEKi/?spm_id_from=333.999.0.0&vd_source=bc9aec86d164b67a7004b996143742dc) [5.2 安装Python虚拟环境创建和依赖库安装视频教程(第二步)](https://www.bilibili.com/video/BV1ZoC1YCEBw?spm_id_from=333.788.videopod.sections&vd_source=bc9aec86d164b67a7004b996143742dc) ### 6.改进YOLOv11训练教程和Web_UI前端加载模型教程(零基础手把手教学) [6.1 改进YOLOv11训练教程和Web_UI前端加载模型教程(第三步)](https://www.bilibili.com/video/BV1BoC1YCEhR?spm_id_from=333.788.videopod.sections&vd_source=bc9aec86d164b67a7004b996143742dc) 按照上面的训练视频教程链接加载项目提供的数据集,运行train.py即可开始训练  Epoch gpu_mem box obj cls labels img_size 1/200 20.8G 0.01576 0.01955 0.007536 22 1280: 100%|██████████| 849/849 [14:42<00:00, 1.04s/it] Class Images Labels P R mAP@.5 mAP@.5:.95: 100%|██████████| 213/213 [01:14<00:00, 2.87it/s] all 3395 17314 0.994 0.957 0.0957 0.0843 Epoch gpu_mem box obj cls labels img_size 2/200 20.8G 0.01578 0.01923 0.007006 22 1280: 100%|██████████| 849/849 [14:44<00:00, 1.04s/it] Class Images Labels P R mAP@.5 mAP@.5:.95: 100%|██████████| 213/213 [01:12<00:00, 2.95it/s] all 3395 17314 0.996 0.956 0.0957 0.0845 Epoch gpu_mem box obj cls labels img_size 3/200 20.8G 0.01561 0.0191 0.006895 27 1280: 100%|██████████| 849/849 [10:56<00:00, 1.29it/s] Class Images Labels P R mAP@.5 mAP@.5:.95: 100%|███████ | 187/213 [00:52<00:00, 4.04it/s] all 3395 17314 0.996 0.957 0.0957 0.0845 ###### [项目数据集下载链接](https://kdocs.cn/l/cszuIiCKVNis) ### 7.原始YOLOv11算法讲解 YOLO11采用改进的骨干和颈部架构,增强了特征提取能力,提高了物体检测的精确度和复杂任务的表现。YOLO11引入精炼的架构设计和优化的训练流程,实现更快的处理速度,同时保持精度和性能之间的最佳平衡。通过模型设计的进步,YOLO11m在COCO数据集上实现了更高的均值平均精度(mAP),同时使用比YOLOv8m少22%的参数,使其在不妥协准确性的情况下更加计算高效。YOLO11可以无缝部署在各种环境中,包括边缘设备、云平台以及支持NVIDIA GPU的系统,确保最大灵活性。无论是物体检测、实例分割、图像分类、姿态估计,还是定向物体检测(OBB),YOLO11都旨在应对多样的计算机视觉挑战。 ![921ce05452174acfb8c1cefc1b02cd7f.jpeg](https://i-blog.csdnimg.cn/direct/921ce05452174acfb8c1cefc1b02cd7f.jpeg) ##### **Ultralytics YOLO11相比于之前版本的主要改进有哪些?** Ultralytics YOLO11在其前身基础上引入了几项重要进步。主要改进包括: 1. **增强的特征提取** :YOLO11采用改进的骨干和颈部架构,增强了特征提取能力,提高了物体检测的精确度。 2. **优化的效率和速度** :精炼的架构设计和优化的训练流程实现了更快的处理速度,同时保持了准确性和性能之间的平衡。 3. **更高的准确性与更少的参数** :YOLO11m在COCO数据集上实现了更高的均值平均精度(mAP),同时使用比YOLOv8m少22%的参数,使其在不妥协准确性的情况下更加计算高效。 4. **环境适应性强** :YOLO11可以在多种环境中部署,包括边缘设备、云平台以及支持NVIDIA GPU的系统。 5. **支持广泛的任务** :YOLO11支持多种计算机视觉任务,如物体检测、实例分割、图像分类、姿态估计和定向物体检测(OBB)。 我们先来看一下其网络结构有什么变化,可以看出,相比较于YOLOv8模型,其将CF2模块改成C3K2,同时在SPPF模块后面添加了一个C2PSA模块,且将YOLOv10的head思想引入到YOLO11的head中,使用深度可分离的方法,减少冗余计算,提高效率。下面我们来详细看一下这两个模块的结构是怎么构成的,以及它们为什么要这样设计 ![](https://i-blog.csdnimg.cn/direct/4367d8673a2d4bfe970a4bd3c8641b00.png) ##### C3K2的网络结构 从下面图中我们可以看到,C3K2模块其实就是C2F模块转变出来的,它代码中有一个设置,就是当c3k这个参数为FALSE的时候,C3K2模块就是C2F模块,也就是说它的Bottleneck是普通的Bottleneck;反之当它为true的时候,将Bottleneck模块替换成C3模块。 ![a5fac4fcfe90452487fee5d02fecb75d.png](https://i-blog.csdnimg.cn/direct/a5fac4fcfe90452487fee5d02fecb75d.png) ##### C2PSA的网络结构 ` `C2PSA是对 `C2f` 模块的扩展,它结合了PSA(Pointwise Spatial Attention)块,用于增强特征提取和注意力机制。通过在标准 `C2f` 模块中引入 PSA 块,C2PSA实现了更强大的注意力机制,从而提高了模型对重要特征的捕捉能力。 ![](https://i-blog.csdnimg.cn/direct/d0833f0920584efa9b95cc2b32e43583.png) ##### **C2f 模块回顾:** **** C2f模块是一个更快的 CSP(Cross Stage Partial)瓶颈实现,它通过两个卷积层和多个 Bottleneck 块进行特征提取。相比传统的 CSPNet,C2f 优化了瓶颈层的结构,使得计算速度更快。在 C2f中,`cv1` 是第一个 1x1 卷积,用于减少通道数;`cv2` 是另一个 1x1 卷积,用于恢复输出通道数。而 `n` 是一个包含 Bottleneck 块的数量,用于提取特征。 ##### **C2PSA 模块的增强** : **C2PSA** 扩展了 C2f,通过引入PSA( **Position-Sensitive Attention)** ,旨在通过多头注意力机制和前馈神经网络来增强特征提取能力。它可以选择性地添加残差结构(shortcut)以优化梯度传播和网络训练效果。同时,使用FFN 可以将输入特征映射到更高维的空间,捕获输入特征的复杂非线性关系,允许模型学习更丰富的特征表示。 ##### head部分 YOLO11在head部分的cls分支上使用深度可分离卷积 ,具体代码如下,cv2边界框回归分支,cv3分类分支。 self.cv2 = nn.ModuleList( nn.Sequential(Conv(x, c2, 3), Conv(c2, c2, 3), nn.Conv2d(c2, 4 * self.reg_max, 1)) for x in ch ) self.cv3 = nn.ModuleList( nn.Sequential( nn.Sequential(DWConv(x, x, 3), Conv(x, c3, 1)), nn.Sequential(DWConv(c3, c3, 3), Conv(c3, c3, 1)), nn.Conv2d(c3, self.nc, 1), ) for x in ch ) ![](https://i-blog.csdnimg.cn/direct/f9b7be0602d34a0eb3c49be570e6c6c3.png) ![](https://i-blog.csdnimg.cn/direct/f0b582155da04f569410077ae78db576.png) ### 8.200+种全套改进YOLOV11创新点原理讲解 #### 8.1 200+种全套改进YOLOV11创新点原理讲解大全 由于篇幅限制,每个创新点的具体原理讲解就不全部展开,具体见下列网址中的改进模块对应项目的技术原理博客网址【Blog】(创新点均为模块化搭建,原理适配YOLOv5~YOLOv11等各种版本) [改进模块技术原理博客【Blog】网址链接](https://gitee.com/qunmasj/good) ![9.png](9.png) #### 8.2 精选部分改进YOLOV11创新点原理讲解 ###### 这里节选部分改进创新点展开原理讲解(完整的改进原理见上图和[改进模块技术原理博客链接](https://gitee.com/qunmasj/good)【如果此小节的图加载失败可以通过CSDN或者Github搜索该博客的标题访问原始博客,原始博客图片显示正常】  ### OREPA:在线卷积重参数化 卷积神经网络(CNNs)已经在许多计算机视觉任务的应用成功,包括图像分类、目标检测、语义分割等。精度和模型效率之间的权衡也已被广泛讨论。 一般来说,一个精度较高的模型通常需要一个更复杂的块,一个更宽或更深的结构。然而,这样的模型总是太重,无法部署,特别是在硬件性能有限、需要实时推理的场景下。考虑到效率,更小、更紧凑和更快的模型自然是首选。 为了获得一个部署友好且高精度的模型,有研究者提出了基于结构重参数化的方法来释放性能。在这些方法中,模型在训练阶段和推理阶段有不同的结构。具体来说,使用复杂的训练阶段拓扑,即重参数化的块,来提高性能。训练结束后,通过等效变换将一个复杂的块重参为成一个单一的线性层。重参后的模型通常具有一个整洁架构模型,例如,通常是一个类似VGG的或一个类似ResNet的结构。从这个角度来看,重参化策略可以在不引入额外的推理时间成本的情况下提高模型的性能。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/e365c93892dc4a0b9cd641778907349e.png) BN层是重构模型的关键组成部分。在一个重新解析块(图1(b))中,在每个卷积层之后立即添加一个BN层。可以观察到,去除这些BN层会导致的性能退化。然而,当考虑到效率时,这种BN层的使用出乎意料地在训练阶段带来了巨大的计算开销。在推理阶段,复杂的块可以被压缩成一个卷积层。但是,在训练过程中,BN层是非线性的,也就是说,它们将特征映射除以它的标准差,这就阻止了合并整个块。因此,存在大量的中间计算操作(large FLOPS)和缓冲特征映射(high memory usage)。更糟糕的是,这么高的训练预算使得很难探索更复杂和可能更强的重参块。很自然地,下面的问题就出现了: 为什么标准化在重参中这么重要? 通过分析和实验,作者认为BN层中的尺度因子最重要,因为它们能够使不同分支的优化方向多样化。 基于观察结果,作者提出了在线重参化(OREPA)(图1(c)),这是一个两阶段的pipeline,使之能够简化复杂的training-time re-param block。 在第一阶段,block linearization,去除所有的非线性BN层,并引入线性缩放层。这些层与BN层具有相似的性质,因此它们使不同分支的优化多样化。此外,这些层都是线性的,可以在训练过程中合并成卷积层。 第二阶段,block squeezing,将复杂的线性块简化为单一的卷积层。OREPA通过减少由中间计算层引起的计算和存储开销,显著降低了训练成本,对性能只有非常小的影响。 此外,高效化使得探索更复杂的重参化拓扑成为可能。为了验证这一点,作者进一步提出了几个重参化的组件,以获得更好的性能。 在ImageNet分类任务上评估了所提出的OREPA。与最先进的修复模型相比,OREPA将额外的训练时间GPU内存成本降低了65%到75%,并将训练过程加快了1.5-2.3倍。同时,OREPA-ResNet和OREPA-VGG的性能始终优于+0.2%∼+0.6%之前的DBB和RepVGG方法。同时作者还评估了在下游任务上的OREPA,即目标检测和语义分割。作者发现OREPA可以在这些任务上也可以带来性能的提高。 提出了在线卷积重参化(OREPA)策略,这极大地提高了重参化模型的训练效率,并使探索更强的重参化块成为可能; 通过对重参化模型工作机制的分析,用引入的线性尺度层代替BN层,这仍然提供了不同的优化方向,并保持了表示能力; 在各种视觉任务上的实验表明,OREPA在准确性和训练效率方面都优于以前的重参化模型(DBB/RepVGG)。 #### 结构重参化 结构重参化最近被重视并应用于许多计算机视觉任务,如紧凑模型设计、架构搜索和剪枝。重参化意味着不同的架构可以通过参数的等价转换来相互转换。例如,1×1卷积的一个分支和3×3卷积的一个分支,可以转移到3×3卷积的单个分支中。在训练阶段,设计了多分支和多层拓扑来取代普通的线性层(如conv或全连接层)来增强模型。Cao等讨论了如何在训练过程中合并深度可分离卷积核。然后在推理过程中,将训练时间的复杂模型转移到简单模型中,以便于更快的推理。 在受益于复杂的training-time拓扑,同时,当前的重参化方法训练使用不可忽略的额外计算成本。当块变得更复杂以变得更强的表示时,GPU内存利用率和训练时间将会越来越长,最终走向不可接受。与以往的重参化方法不同,本文更多地关注训练成本。提出了一种通用的在线卷积重参化策略,使training-time的结构重参化成为可能。 #### Normalization BN被提出来缓解训练非常深度神经网络时的梯度消失问题。人们认为BN层是非常重要的,因为它们平滑了损失。最近关于无BN神经网络的研究声称,BN层并不是不可或缺的。通过良好的初始化和适当的正则化,可以优雅地去除BN层。 对于重参化模型,作者认为重参化块中的BN层是关键的。无BN的变体将会出现性能下降。然而,BN层是非线性的,也就是说,它们将特征图除以它的标准差,这阻止了在线合并块。为了使在线重参化可行,作者去掉了重参块中的所有BN层,并引入了BN层的线性替代方法,即线性缩放层。 #### 卷积分解 标准卷积层计算比较密集,导致大的FLOPs和参数量。因此,卷积分解方法被提出,并广泛应用于移动设备的轻量化模型中。重参化方法也可以看作是卷积分解的某种形式,但它更倾向于更复杂的拓扑结构。本文的方法的不同之处在于,在kernel-level上分解卷积,而不是在structure level。 #### 在线重参化 在本节中,首先,分析了关键组件,即重参化模型中的BN层,在此基础上提出了在线重参化(OREPA),旨在大大减少再参数化模型的训练时间预算。OREPA能够将复杂的训练时间块简化为一个卷积层,并保持了较高的精度。 OREPA的整体pipeline如图所示,它包括一个Block Linearization阶段和一个Block Squeezing阶段。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/7b39a390dd264540b91062d7d237bfc7.png) 参考该博客通过分析多层和多分支结构的优化多样性,深入研究了重参化的有效性,并证明了所提出的线性缩放层和BN层具有相似的效果。 最后,随着训练预算的减少,进一步探索了更多的组件,以实现更强的重参化模型,成本略有增加。 #### 重参化中的Normalization 作者认为中间BN层是重参化过程中多层和多分支结构的关键组成部分。以SoTA模型DBB和RepVGG为例,去除这些层会导致严重的性能下降,如表1所示。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/e3da47c1a49846b59a11f8c379d0ba68.png) 这种观察结果也得到了Ding等人的实验支持。因此,作者认为中间的BN层对于重参化模型的性能是必不可少的。 然而,中间BN层的使用带来了更高的训练预算。作者注意到,在推理阶段,重参化块中的所有中间操作都是线性的,因此可以合并成一个卷积层,从而形成一个简单的结构。 但在训练过程中,BN层是非线性的,即它们将特征映射除以其标准差。因此,中间操作应该单独计算,这将导致更高的计算和内存成本。更糟糕的是,如此高的成本将阻止探索更强大的训练模块。 #### Block Linearization 如3.1中所述,中间的BN层阻止了在训练过程中合并单独的层。然而,由于性能问题,直接删除它们并不简单。为了解决这一困境,作者引入了channel级线性尺度操作作为BN的线性替代方法。 缩放层包含一个可学习的向量,它在通道维度中缩放特征映射。线性缩放层具有与BN层相似的效果,它们都促进多分支向不同的方向进行优化,这是重参化时性能提高的关键。除了对性能的影响外,线性缩放层还可以在训练过程中进行合并,使在线重参化成为可能。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/42f5755f8681476e9a4896871597a2dd.png) 基于线性缩放层,作者修改了重参化块,如图所示。具体来说,块的线性化阶段由以下3个步骤组成: 首先,删除了所有的非线性层,即重参化块中的BN层 其次,为了保持优化的多样性,在每个分支的末尾添加了一个缩放层,这是BN的线性替代方法 最后,为了稳定训练过程,在所有分支的添加后添加一个BN层。 一旦完成线性化阶段,在重参化块中只存在线性层,这意味着可以在训练阶段合并块中的所有组件。 #### Block Squeezing Block Squeezing步骤将计算和内存昂贵的中间特征映射上的操作转换为更经济的kernel上的操作。这意味着在计算和内存方面从减少到,其中、是特征图和卷积核的空间尺寸。 一般来说,无论线性重参化块是多么复杂,以下2个属性始终成立: Block中的所有线性层,例如深度卷积、平均池化和所提出的线性缩放,都可以用带有相应参数的退化卷积层来表示; Block可以由一系列并行分支表示,每个分支由一系列卷积层组成。 有了上述两个特性,如果可以将 多层(即顺序结构) 多分支(即并行结构) 简化为单一卷积,就可以压缩一个块。在下面的部分中,将展示如何简化顺序结构(图(a))和并行结构(图(b))。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/c386ba0296fc46e9b235c7ff1d64f26b.png) ### 9.系统功能展示 图9.1.系统支持检测结果表格显示 图9.2.系统支持置信度和IOU阈值手动调节 图9.3.系统支持自定义加载权重文件best.pt(需要你通过步骤5中训练获得) 图9.4.系统支持摄像头实时识别 图9.5.系统支持图片识别 图9.6.系统支持视频识别 图9.7.系统支持识别结果文件自动保存 图9.8.系统支持Excel导出检测结果数据 ![10.png](10.png) ![11.png](11.png) ![12.png](12.png) ![13.png](13.png) ![14.png](14.png) ![15.png](15.png) ![16.png](16.png) ![17.png](17.png) ### 10. YOLOv11核心改进源码讲解 #### 10.1 kaln_conv.py 以下是对代码的核心部分进行分析和详细注释的结果: ```python from functools import lru_cache import torch import torch.nn as nn from torch.nn.functional import conv3d, conv2d, conv1d class KALNConvNDLayer(nn.Module): def __init__(self, conv_class, norm_class, conv_w_fun, input_dim, output_dim, degree, kernel_size, groups=1, padding=0, stride=1, dilation=1, dropout: float = 0.0, ndim: int = 2): super(KALNConvNDLayer, self).__init__() # 初始化参数 self.inputdim = input_dim # 输入维度 self.outdim = output_dim # 输出维度 self.degree = degree # 多项式的阶数 self.kernel_size = kernel_size # 卷积核大小 self.padding = padding # 填充 self.stride = stride # 步幅 self.dilation = dilation # 膨胀 self.groups = groups # 分组卷积的组数 self.base_activation = nn.SiLU() # 基础激活函数 self.conv_w_fun = conv_w_fun # 卷积权重函数 self.ndim = ndim # 数据的维度(1D, 2D, 3D) self.dropout = None # Dropout层 # 初始化Dropout层 if dropout > 0: if ndim == 1: self.dropout = nn.Dropout1d(p=dropout) elif ndim == 2: self.dropout = nn.Dropout2d(p=dropout) elif ndim == 3: self.dropout = nn.Dropout3d(p=dropout) # 检查分组参数的有效性 if groups <= 0: raise ValueError('groups must be a positive integer') if input_dim % groups != 0: raise ValueError('input_dim must be divisible by groups') if output_dim % groups != 0: raise ValueError('output_dim must be divisible by groups') # 创建基础卷积层和归一化层 self.base_conv = nn.ModuleList([conv_class(input_dim // groups, output_dim // groups, kernel_size, stride, padding, dilation, groups=1, bias=False) for _ in range(groups)]) self.layer_norm = nn.ModuleList([norm_class(output_dim // groups) for _ in range(groups)]) # 多项式权重的形状 poly_shape = (groups, output_dim // groups, (input_dim // groups) * (degree + 1)) + tuple( kernel_size for _ in range(ndim)) # 初始化多项式权重 self.poly_weights = nn.Parameter(torch.randn(*poly_shape)) # 使用Kaiming均匀分布初始化卷积层权重 for conv_layer in self.base_conv: nn.init.kaiming_uniform_(conv_layer.weight, nonlinearity='linear') nn.init.kaiming_uniform_(self.poly_weights, nonlinearity='linear') @lru_cache(maxsize=128) # 使用缓存避免重复计算勒让德多项式 def compute_legendre_polynomials(self, x, order): # 计算勒让德多项式 P0 = x.new_ones(x.shape) # P0 = 1 if order == 0: return P0.unsqueeze(-1) P1 = x # P1 = x legendre_polys = [P0, P1] # 使用递推关系计算高阶多项式 for n in range(1, order): Pn = ((2.0 * n + 1.0) * x * legendre_polys[-1] - n * legendre_polys[-2]) / (n + 1.0) legendre_polys.append(Pn) return torch.concatenate(legendre_polys, dim=1) def forward_kal(self, x, group_index): # 前向传播 base_output = self.base_conv[group_index](x) # 基础卷积输出 # 归一化输入以稳定勒让德多项式计算 x_normalized = 2 * (x - x.min()) / (x.max() - x.min()) - 1 if x.shape[0] > 0 else x if self.dropout is not None: x_normalized = self.dropout(x_normalized) # 应用Dropout # 计算勒让德多项式 legendre_basis = self.compute_legendre_polynomials(x_normalized, self.degree) # 使用多项式权重进行卷积 poly_output = self.conv_w_fun(legendre_basis, self.poly_weights[group_index], stride=self.stride, dilation=self.dilation, padding=self.padding, groups=1) # 合并基础输出和多项式输出 x = base_output + poly_output if isinstance(self.layer_norm[group_index], nn.LayerNorm): orig_shape = x.shape x = self.layer_norm[group_index](x.view(orig_shape[0], -1)).view(orig_shape) else: x = self.layer_norm[group_index](x) x = self.base_activation(x) # 应用激活函数 return x def forward(self, x): # 前向传播,处理分组输入 split_x = torch.split(x, self.inputdim // self.groups, dim=1) output = [] for group_ind, _x in enumerate(split_x): y = self.forward_kal(_x.clone(), group_ind) # 对每个组进行前向传播 output.append(y.clone()) y = torch.cat(output, dim=1) # 合并输出 return y ``` ### 代码分析与注释 1. **KALNConvNDLayer类**: - 这是一个自定义的卷积层,支持任意维度的卷积(1D, 2D, 3D)。 - 在初始化中,定义了输入输出维度、卷积参数、激活函数等。 - 通过`nn.ModuleList`创建多个卷积层和归一化层,支持分组卷积。 2. **compute_legendre_polynomials方法**: - 计算勒让德多项式,使用递推关系生成多项式序列,利用缓存机制提高效率。 3. **forward_kal方法**: - 处理每个分组的前向传播,包括基础卷积、归一化、勒让德多项式计算和输出合并。 4. **forward方法**: - 处理输入数据,将其分成多个组,并对每个组调用`forward_kal`进行处理,最后合并所有组的输出。 该代码实现了一个灵活的卷积层,可以通过调整参数适应不同的输入数据类型和结构,具有良好的扩展性和可重用性。 这个程序文件定义了一个用于实现高阶卷积神经网络层的类`KALNConvNDLayer`,以及其针对不同维度(1D、2D、3D)的子类。主要功能是通过卷积操作和Legendre多项式的组合来增强网络的表达能力。 首先,`KALNConvNDLayer`类的构造函数接受多个参数,包括卷积层的类型、归一化层的类型、卷积权重的计算函数、输入和输出维度、卷积核大小、分组数、填充、步幅、扩张率、丢弃率和维度数。它通过这些参数初始化了一些重要的属性,并且创建了多个卷积层和归一化层的实例。特别地,使用了Kaiming均匀分布来初始化卷积层和多项式权重,以确保网络在训练开始时的稳定性。 在`KALNConvNDLayer`中,有一个缓存的计算Legendre多项式的方法`compute_legendre_polynomials`,这个方法通过递归关系计算给定阶数的Legendre多项式。它使用了`lru_cache`装饰器来缓存计算结果,以避免重复计算,从而提高效率。 `forward_kal`方法是这个类的核心部分,它接收输入数据和组索引,首先通过基础卷积层对输入进行卷积操作,然后对输入进行归一化处理,并计算Legendre多项式。接着,使用给定的卷积权重函数将多项式基与多项式权重进行卷积,得到多项式输出。最后,将基础输出和多项式输出相加,并通过归一化层和激活函数进行处理,返回最终的输出。 `forward`方法负责处理输入数据的分组,将输入数据分成多个组,并对每个组调用`forward_kal`方法进行处理,最后将所有组的输出拼接在一起。 接下来的三个类`KALNConv3DLayer`、`KALNConv2DLayer`和`KALNConv1DLayer`分别继承自`KALNConvNDLayer`,并针对3D、2D和1D卷积进行了特定的初始化。这些子类简化了创建不同维度卷积层的过程,用户只需提供必要的参数即可。 整体来看,这个文件实现了一个灵活且强大的卷积层结构,结合了多项式的计算和卷积操作,适用于需要高阶特征提取的深度学习任务。 #### 10.2 VanillaNet.py 以下是经过简化并添加详细中文注释的核心代码部分: ```python import torch import torch.nn as nn import torch.nn.functional as F from timm.layers import weight_init # 定义激活函数类 class Activation(nn.ReLU): def __init__(self, dim, act_num=3, deploy=False): super(Activation, self).__init__() self.deploy = deploy # 是否处于部署模式 # 初始化权重和偏置 self.weight = torch.nn.Parameter(torch.randn(dim, 1, act_num * 2 + 1, act_num * 2 + 1)) self.bias = None self.bn = nn.BatchNorm2d(dim, eps=1e-6) # 批归一化 self.dim = dim self.act_num = act_num weight_init.trunc_normal_(self.weight, std=.02) # 权重初始化 def forward(self, x): # 前向传播 if self.deploy: return F.conv2d( super(Activation, self).forward(x), self.weight, self.bias, padding=(self.act_num * 2 + 1) // 2, groups=self.dim) else: return self.bn(F.conv2d( super(Activation, self).forward(x), self.weight, padding=self.act_num, groups=self.dim)) def switch_to_deploy(self): # 切换到部署模式,融合批归一化 if not self.deploy: kernel, bias = self._fuse_bn_tensor(self.weight, self.bn) self.weight.data = kernel self.bias = torch.nn.Parameter(torch.zeros(self.dim)) self.bias.data = bias self.__delattr__('bn') # 删除bn属性 self.deploy = True def _fuse_bn_tensor(self, weight, bn): # 融合权重和批归一化参数 kernel = weight running_mean = bn.running_mean running_var = bn.running_var gamma = bn.weight beta = bn.bias eps = bn.eps std = (running_var + eps).sqrt() t = (gamma / std).reshape(-1, 1, 1, 1) return kernel * t, beta + (0 - running_mean) * gamma / std # 定义基本块 class Block(nn.Module): def __init__(self, dim, dim_out, act_num=3, stride=2, deploy=False): super().__init__() self.deploy = deploy # 根据是否部署选择不同的卷积结构 if self.deploy: self.conv = nn.Conv2d(dim, dim_out, kernel_size=1) else: self.conv1 = nn.Sequential( nn.Conv2d(dim, dim, kernel_size=1), nn.BatchNorm2d(dim, eps=1e-6), ) self.conv2 = nn.Sequential( nn.Conv2d(dim, dim_out, kernel_size=1), nn.BatchNorm2d(dim_out, eps=1e-6) ) # 池化层 self.pool = nn.MaxPool2d(stride) if stride != 1 else nn.Identity() self.act = Activation(dim_out, act_num) # 激活函数 def forward(self, x): # 前向传播 if self.deploy: x = self.conv(x) else: x = self.conv1(x) x = F.leaky_relu(x, negative_slope=1) # 使用Leaky ReLU激活 x = self.conv2(x) x = self.pool(x) # 池化 x = self.act(x) # 激活 return x # 定义VanillaNet模型 class VanillaNet(nn.Module): def __init__(self, in_chans=3, num_classes=1000, dims=[96, 192, 384, 768], drop_rate=0, act_num=3, strides=[2, 2, 2, 1], deploy=False): super().__init__() self.deploy = deploy # 初始化stem部分 if self.deploy: self.stem = nn.Sequential( nn.Conv2d(in_chans, dims[0], kernel_size=4, stride=4), Activation(dims[0], act_num) ) else: self.stem1 = nn.Sequential( nn.Conv2d(in_chans, dims[0], kernel_size=4, stride=4), nn.BatchNorm2d(dims[0], eps=1e-6), ) self.stem2 = nn.Sequential( nn.Conv2d(dims[0], dims[0], kernel_size=1, stride=1), nn.BatchNorm2d(dims[0], eps=1e-6), Activation(dims[0], act_num) ) self.stages = nn.ModuleList() # 存储各个Block for i in range(len(strides)): stage = Block(dim=dims[i], dim_out=dims[i + 1], act_num=act_num, stride=strides[i], deploy=deploy) self.stages.append(stage) def forward(self, x): # 前向传播 if self.deploy: x = self.stem(x) else: x = self.stem1(x) x = F.leaky_relu(x, negative_slope=1) x = self.stem2(x) for stage in self.stages: x = stage(x) # 依次通过各个Block return x # 测试模型 if __name__ == '__main__': inputs = torch.randn((1, 3, 640, 640)) # 随机输入 model = VanillaNet() # 创建模型 pred = model(inputs) # 前向传播 print(pred.size()) # 输出预测结果的尺寸 ``` ### 代码说明: 1. **Activation 类**:自定义的激活函数类,包含权重和偏置的初始化,以及前向传播和批归一化的融合。 2. **Block 类**:基本的卷积块,包含两个卷积层和一个激活函数。根据是否在部署模式下选择不同的结构。 3. **VanillaNet 类**:主模型类,初始化输入层和多个 Block 组成的网络结构。前向传播时依次通过各个层。 4. **测试部分**:在主程序中创建模型并进行一次前向传播,输出结果的尺寸。 该程序文件 `VanillaNet.py` 实现了一个名为 VanillaNet 的深度学习模型,主要用于图像处理任务。文件开头包含版权信息和许可协议,表明该程序是开源的,可以在 MIT 许可证下进行修改和分发。 程序中首先导入了必要的库,包括 PyTorch 及其神经网络模块、功能模块,以及一些其他工具库。接着,定义了一个名为 `activation` 的类,该类继承自 `nn.ReLU`,用于实现自定义的激活函数。该类的构造函数中初始化了权重和偏置,并定义了批量归一化层。其 `forward` 方法根据是否处于部署模式,选择不同的前向传播方式。 接下来,定义了一个 `Block` 类,表示 VanillaNet 中的基本构建块。该类的构造函数根据输入和输出的维度、步幅以及是否处于部署模式,初始化了卷积层、池化层和激活函数。`forward` 方法实现了数据的前向传播,并根据步幅选择合适的池化操作。 `VanillaNet` 类是整个模型的核心,构造函数中定义了输入通道、类别数、各层的维度、丢弃率、激活函数数量、步幅等参数。根据这些参数,构建了模型的各个阶段。模型的前向传播方法实现了输入数据的逐层处理,并根据输入尺寸记录特征图。 此外,程序中还实现了权重更新的功能,通过 `update_weight` 函数将预训练模型的权重加载到当前模型中。文件中还定义了多个函数(如 `vanillanet_5` 到 `vanillanet_13`)用于创建不同配置的 VanillaNet 模型,并支持加载预训练权重。 最后,在文件的主程序部分,创建了一个随机输入,并实例化了一个 VanillaNet 模型,进行前向推理并打印输出特征图的尺寸。这部分代码用于测试模型的构建和功能。 整体来看,该程序实现了一个灵活的深度学习模型结构,支持多种配置和预训练权重的加载,适用于图像分类等任务。 #### 10.3 test_selective_scan.py 以下是代码中最核心的部分,并附上详细的中文注释: ```python import torch import torch.nn.functional as F def build_selective_scan_fn(selective_scan_cuda: object = None, mode="mamba_ssm", tag=None): """ 构建选择性扫描函数,支持不同的模式(如 mamba_ssm, ssoflex 等)。 """ MODE = mode class SelectiveScanFn(torch.autograd.Function): @staticmethod def forward(ctx, u, delta, A, B, C, D=None, z=None, delta_bias=None, delta_softplus=False, return_last_state=False, nrows=1, backnrows=-1): """ 前向传播函数,计算选择性扫描的输出。 参数: - u: 输入张量 - delta: 变化率张量 - A, B, C: 权重张量 - D: 可选的偏置张量 - z: 可选的门控张量 - delta_bias: 可选的变化率偏置 - delta_softplus: 是否使用 softplus 激活 - return_last_state: 是否返回最后的状态 - nrows: 行数 - backnrows: 反向传播时的行数 """ # 确保输入张量是连续的 if u.stride(-1) != 1: u = u.contiguous() if delta.stride(-1) != 1: delta = delta.contiguous() if D is not None: D = D.contiguous() if B.stride(-1) != 1: B = B.contiguous() if C.stride(-1) != 1: C = C.contiguous() if z is not None and z.stride(-1) != 1: z = z.contiguous() # 处理 B 和 C 的维度 if B.dim() == 3: B = rearrange(B, "b dstate l -> b 1 dstate l") ctx.squeeze_B = True if C.dim() == 3: C = rearrange(C, "b dstate l -> b 1 dstate l") ctx.squeeze_C = True # 检查输入的形状和类型 assert u.shape[1] % (B.shape[1] * nrows) == 0 assert nrows in [1, 2, 3, 4] # 只支持 1 到 4 行 # 选择合适的 CUDA 函数进行前向计算 if MODE in ["mamba_ssm"]: out, x, *rest = selective_scan_cuda.fwd(u, delta, A, B, C, D, z, delta_bias, delta_softplus) else: raise NotImplementedError # 保存用于反向传播的变量 ctx.save_for_backward(u, delta, A, B, C, D, delta_bias, x) last_state = x[:, :, -1, 1::2] # 获取最后的状态 return out if not return_last_state else (out, last_state) @staticmethod def backward(ctx, dout): """ 反向传播函数,计算梯度。 参数: - dout: 输出的梯度 """ u, delta, A, B, C, D, delta_bias, x = ctx.saved_tensors # 调用 CUDA 后向函数计算梯度 du, ddelta, dA, dB, dC, dD, ddelta_bias, *rest = selective_scan_cuda.bwd( u, delta, A, B, C, D, delta_bias, dout, x, None, False ) return (du, ddelta, dA, dB, dC, dD if D is not None else None, ddelta_bias if delta_bias is not None else None) def selective_scan_fn(u, delta, A, B, C, D=None, z=None, delta_bias=None, delta_softplus=False, return_last_state=False, nrows=1, backnrows=-1): """ 封装选择性扫描函数的调用。 """ outs = SelectiveScanFn.apply(u, delta, A, B, C, D, z, delta_bias, delta_softplus, return_last_state, nrows, backnrows) return outs return selective_scan_fn # 示例使用 selective_scan_fn = build_selective_scan_fn(selective_scan_cuda, mode="mamba_ssm") ``` ### 代码注释说明: 1. **导入库**:导入 PyTorch 和其功能模块。 2. **构建选择性扫描函数**:`build_selective_scan_fn` 函数用于创建一个选择性扫描的自定义函数,支持不同的模式。 3. **前向传播**:`forward` 方法实现了选择性扫描的前向计算,处理输入张量的形状和类型,并调用相应的 CUDA 函数。 4. **反向传播**:`backward` 方法实现了反向传播,计算梯度并返回。 5. **封装函数**:`selective_scan_fn` 是对选择性扫描函数的封装,便于调用。 以上是代码的核心部分和详细注释,提供了对选择性扫描的基本理解和实现逻辑。 这个程序文件 `test_selective_scan.py` 是一个用于测试选择性扫描(Selective Scan)功能的 Python 脚本,主要使用 PyTorch 库来实现。文件中包含了多个函数和类,主要分为选择性扫描的前向和反向传播计算,以及相关的测试功能。 首先,程序导入了必要的库,包括 `torch`、`torch.nn.functional`、`pytest` 和 `einops` 等。接着定义了一个函数 `build_selective_scan_fn`,用于构建选择性扫描的前向和反向传播函数。这个函数接受一个 CUDA 实现的选择性扫描函数和一些其他参数,并返回一个可以在 PyTorch 中使用的函数。 在 `SelectiveScanFn` 类中,定义了静态方法 `forward` 和 `backward`。`forward` 方法实现了选择性扫描的前向计算,处理输入的张量 `u`、`delta`、`A`、`B`、`C` 等,并根据不同的模式调用相应的 CUDA 实现。它还负责保存计算中间结果以供反向传播使用。`backward` 方法则实现了反向传播计算,计算梯度并返回。 接下来,定义了 `selective_scan_ref` 和 `selective_scan_ref_v2` 函数,这两个函数实现了选择性扫描的参考实现,用于与 CUDA 实现的结果进行比较。这些函数处理输入数据并执行选择性扫描的计算,返回输出和最后的状态。 文件中还定义了一个 `selective_scan_fn` 函数,它调用 `selective_scan_ref_v2`,并在主程序中根据不同的模式选择相应的 CUDA 实现。 在文件的末尾,使用 `pytest` 框架定义了一个测试函数 `test_selective_scan`,该函数通过参数化的方式测试选择性扫描的功能。它生成各种输入数据,调用选择性扫描函数,并检查输出的正确性和梯度的匹配性。测试过程中还会打印输出的最大差异和均值差异,以便于调试和验证。 总体来说,这个文件的主要目的是实现和测试选择性扫描的功能,确保其在不同模式下的正确性和性能。通过使用 CUDA 加速,程序能够高效地处理大规模数据,同时提供了参考实现以便于验证结果的准确性。 #### 10.4 dynamic_snake_conv.py 以下是经过简化和注释的核心代码部分,保留了最重要的功能,并对每个部分进行了详细的中文注释: ```python import torch import torch.nn as nn class DySnakeConv(nn.Module): def __init__(self, inc, ouc, k=3) -> None: """ 动态蛇形卷积的初始化 :param inc: 输入通道数 :param ouc: 输出通道数 :param k: 卷积核大小 """ super().__init__() # 初始化三个卷积层 self.conv_0 = Conv(inc, ouc, k) # 标准卷积 self.conv_x = DSConv(inc, ouc, 0, k) # 沿x轴的动态蛇形卷积 self.conv_y = DSConv(inc, ouc, 1, k) # 沿y轴的动态蛇形卷积 def forward(self, x): """ 前向传播 :param x: 输入特征图 :return: 连接后的输出特征图 """ # 将三个卷积的输出在通道维度上拼接 return torch.cat([self.conv_0(x), self.conv_x(x), self.conv_y(x)], dim=1) class DSConv(nn.Module): def __init__(self, in_ch, out_ch, morph, kernel_size=3, if_offset=True, extend_scope=1): """ 动态蛇形卷积的初始化 :param in_ch: 输入通道数 :param out_ch: 输出通道数 :param morph: 卷积核的形态(0表示沿x轴,1表示沿y轴) :param kernel_size: 卷积核大小 :param if_offset: 是否需要偏移 :param extend_scope: 扩展范围 """ super(DSConv, self).__init__() # 用于学习可变形偏移的卷积层 self.offset_conv = nn.Conv2d(in_ch, 2 * kernel_size, 3, padding=1) self.bn = nn.BatchNorm2d(2 * kernel_size) # 批归一化 self.kernel_size = kernel_size # 定义沿x轴和y轴的动态蛇形卷积 self.dsc_conv_x = nn.Conv2d(in_ch, out_ch, kernel_size=(kernel_size, 1), stride=(kernel_size, 1), padding=0) self.dsc_conv_y = nn.Conv2d(in_ch, out_ch, kernel_size=(1, kernel_size), stride=(1, kernel_size), padding=0) self.gn = nn.GroupNorm(out_ch // 4, out_ch) # 组归一化 self.act = Conv.default_act # 默认激活函数 self.extend_scope = extend_scope self.morph = morph self.if_offset = if_offset def forward(self, f): """ 前向传播 :param f: 输入特征图 :return: 经过动态蛇形卷积后的特征图 """ # 计算偏移 offset = self.offset_conv(f) offset = self.bn(offset) offset = torch.tanh(offset) # 将偏移限制在[-1, 1]之间 # 进行可变形卷积 dsc = DSC(f.shape, self.kernel_size, self.extend_scope, self.morph) deformed_feature = dsc.deform_conv(f, offset, self.if_offset) # 根据形态选择相应的卷积 if self.morph == 0: x = self.dsc_conv_x(deformed_feature.type(f.dtype)) else: x = self.dsc_conv_y(deformed_feature.type(f.dtype)) x = self.gn(x) # 归一化 x = self.act(x) # 激活 return x class DSC(object): def __init__(self, input_shape, kernel_size, extend_scope, morph): """ 动态蛇形卷积的坐标映射和双线性插值 :param input_shape: 输入特征图的形状 :param kernel_size: 卷积核大小 :param extend_scope: 扩展范围 :param morph: 卷积核的形态 """ self.num_points = kernel_size self.width = input_shape[2] self.height = input_shape[3] self.morph = morph self.extend_scope = extend_scope # 定义特征图的形状 self.num_batch = input_shape[0] self.num_channels = input_shape[1] def deform_conv(self, input, offset, if_offset): """ 进行可变形卷积 :param input: 输入特征图 :param offset: 偏移 :param if_offset: 是否需要偏移 :return: 变形后的特征图 """ y, x = self._coordinate_map_3D(offset, if_offset) # 计算坐标映射 deformed_feature = self._bilinear_interpolate_3D(input, y, x) # 进行双线性插值 return deformed_feature # 其他方法如 _coordinate_map_3D 和 _bilinear_interpolate_3D 省略,保持代码简洁 ``` ### 代码说明: 1. **DySnakeConv**: 这是动态蛇形卷积的主要类,包含了三个卷积层(标准卷积和两个动态蛇形卷积),在前向传播中将它们的输出拼接在一起。 2. **DSConv**: 动态蛇形卷积的实现,包含了学习偏移的卷积层和两个不同方向的卷积(x和y),并在前向传播中计算变形后的特征图。 3. **DSC**: 负责处理坐标映射和双线性插值的类,包含了进行可变形卷积的逻辑。 通过这些注释,代码的核心功能和每个部分的作用变得更加清晰。 这个程序文件 `dynamic_snake_conv.py` 实现了一个动态蛇形卷积(Dynamic Snake Convolution)的神经网络模块,主要用于处理图像数据。文件中定义了两个主要的类:`DySnakeConv` 和 `DSConv`,以及一个辅助类 `DSC`。 首先,`DySnakeConv` 类是一个神经网络模块,继承自 `nn.Module`。在初始化方法中,它接受输入通道数 `inc`、输出通道数 `ouc` 和卷积核大小 `k` 作为参数。该类创建了三个卷积层:`conv_0` 是标准卷积,`conv_x` 和 `conv_y` 是动态蛇形卷积,分别沿着 x 轴和 y 轴进行处理。在前向传播方法 `forward` 中,输入数据 `x` 会通过这三个卷积层进行处理,并将结果在通道维度上拼接起来,形成最终的输出。 接下来,`DSConv` 类实现了动态蛇形卷积的具体逻辑。它同样继承自 `nn.Module`,在初始化时接收输入和输出通道数、卷积核大小、形态参数 `morph`、是否使用偏移 `if_offset` 以及扩展范围 `extend_scope`。该类的构造函数中定义了用于学习偏移的卷积层 `offset_conv`,以及两个不同方向的卷积层 `dsc_conv_x` 和 `dsc_conv_y`。此外,还包括批归一化层和激活函数。 在 `DSConv` 的前向传播方法中,首先通过 `offset_conv` 计算出偏移量,然后使用 `tanh` 函数将偏移量限制在 -1 到 1 的范围内。接着,创建一个 `DSC` 实例,并调用其 `deform_conv` 方法进行卷积操作。根据 `morph` 参数的值,选择使用 x 轴或 y 轴的卷积层进行处理,最后返回经过激活函数处理后的结果。 `DSC` 类是一个辅助类,负责处理坐标映射和双线性插值。它的构造函数接收输入特征的形状、卷积核大小、扩展范围和形态参数。该类的 `_coordinate_map_3D` 方法用于生成坐标映射,根据偏移量和形态参数计算出新的坐标位置。 `_bilinear_interpolate_3D` 方法则实现了对输入特征图的双线性插值,以生成变形后的特征图。 总的来说,这个程序文件实现了一个灵活的卷积模块,能够根据输入特征的形状和偏移量动态调整卷积核的位置,从而提高模型对图像特征的捕捉能力。通过使用动态蛇形卷积,可以更好地处理图像中的形状变化和复杂结构。 ### 11.完整训练+Web前端界面+200+种全套创新点源码、数据集获取 ![19.png](19.png) # [下载链接:https://mbd.pub/o/bread/Z5yamZ1r](https://mbd.pub/o/bread/Z5yamZ1r)