【深度学习周报第八周】

文章目录

  • 摘要
  • 第13章 计算机视觉
    • 13.5 树叶分类竞赛总结
    • 13.6 目标检测算法
      • 13.6.1 区域卷积神经网络系列算法R-CNN(Region-based CNN or Regions with CNN features)
      • 13.6.2 单发多框检测(SSD)
      • 13.6.3 YOLO(You Only Look Once)
    • 13.7 多尺度目标检测
      • 13.7.1 多尺度锚框
      • 13.7.2 单发多框检测(SSD)代码实现
    • 13.8 语义分割和数据集
      • 13.8.1 语义分割
      • 13.8.2 语义分割数据集
    • 13.9 转置卷积 (transposed convolution)
      • 基础卷积
      • 转置卷积与卷积的异同
      • 与矩阵变换的联系
  • 总结

摘要

本周主要学习了计算机视觉中的语义分割和目标检测技术。在语义分割部分,重点分析了Pascal VOC2012数据集的处理方法,包括RGB标签到类别ID的转换、随机裁剪数据增强以及自定义数据集的构建。目标检测方面深入研究了R-CNN系列算法(R-CNN、Fast R-CNN、Faster R-CNN、Mask R-CNN)的演进过程,对比了两阶段检测与单阶段检测(SSD、YOLO)的特点,并实现了SSD模型的完整训练流程。此外还详细探讨了转置卷积的原理及其与常规卷积的异同,包括填充、步幅和多通道处理等核心概念,并通过矩阵变换的角度解释了卷积运算的数学本质。
由于上周锚框代码和这周的单发多框检测SSD代码实现比较复杂,花了很多时间去看,需要根据书上代码一行一行理解原理,

第13章 计算机视觉

13.5 树叶分类竞赛总结

比赛地址:树叶分类

技术分析
1、数据增强

  • 在测试的时候多次使用较弱的增强然后取平均(在测试的时候是在中心点 crop 裁剪出一张图片做预测,很多时候也有在四个角上 crop 一次,然后将五次所得的结果取平均)

2、使用多个模型预测,最后加权平均

  • 有使用多个模型加权取平均的,也有使用单一模型多次训练然后取平均的
  • 模型多为 ResNet 变种(对模型进行了更多的细节上的调整):DenseNet,ResNeXt,ResNeSt,EfficientNet

3、优化算法和学习率

  • 优化算法一般使用的是 SGD 或者 Adam(多为 Adam 或者其变种,SGD 通过仔细调参(如 momentum 等)通常能够获得比 Adam 更好的结果,但是 Adam 对学习率不太敏感,换句话说,Adam 在不需要过多调参的情况下也能够获得不错的加过,Adam 的结果更加平滑(可以理解为:如果两种算法的解集看成是同一坐标系中的两条二次函数曲线的话,SGD 所得的解集波峰比 Adam 更高,但是 SGD 对调参的要求也更高,Adam 的曲线更加平滑,虽然波峰比不上 SGD 但是相对来讲整体更加稳定,受参数的影响较小),所以在不那么关注优化算法而是对模型和数据进行改进的情况下,可以使用 Adam 来弱化调参队最终结果的影响)
  • 学习率基本上是固定不变的,也有可能经过一段时间之后下降一次:一般是基于 Cosine下降 (好处是不用调参)或者是训练不动的时候往下调(每隔一定的 epoch 之后学习率下降一次)

4、清理数据

  • 有重复的图片,可以手动去除
  • 图片背景较多,而且树叶没有方向性,可以做更多增强(随机旋转,更大的裁剪)
  • 跨图片增强:Mixup:随机取两张图片,然后取一个随机权重,再将两张图片进行叠加(标号也进行叠加)得到一个新的训练图片参加训练);CutMix:(在不同的图片中随机采样一些块,然后取一个随机的权重将这些块进行随机的组合,最终所得的图片的标号也取决于随机组合的权重,这样做的好处是能够尽可能地关注数据中的局部信息)

小结

  • 提升精度思路:根据数据挑选增强,使用新模型、新优化算法,多个模型融合,测试时使用增强
  • 在工业界应用:
  • -少使用模型融合和测试时增强,计算代价过高
  • 通过固定模型超参数,而将经理主要花在提升数据质量

13.6 目标检测算法

本节只简要介绍一下区域卷积神经网络(region-based CNN或regions with CNN features,R-CNN)及其一系列改进方法 Fast R-CNN 、Faster R-CNN、掩码Mask R-CNN 和单发多框检测SSD和YoLo (You Only Look Once)这些模型的设计思路

目标检测算法主要分为两个类型
(1)two-stage方法,如R-CNN系算法(region-based CNN),其主要思路是先通过启发式方法(selective search)或者CNN网络(RPN)产生一系列稀疏的候选框,然后对这些候选框进行分类与回归,two-stage方法的优势是准确度高
(2)one-stage方法,如Yolo和SSD,其主要思路是均匀地在图片的不同位置进行密集抽样,抽样时可以采用不同尺度和长宽比,然后利用CNN提取特征后直接进行分类与回归,整个过程只需要一步,所以其优势是速度快,但是均匀的密集采样的一个重要缺点是训练比较困难,这主要是因为正样本与负样本(背景)极其不均衡,导致模型准确度稍低

13.6.1 区域卷积神经网络系列算法R-CNN(Region-based CNN or Regions with CNN features)

R-CNN

R-CNN首先从输入图像中选取若干(例如2000个)提议区域(如锚框也是一种选取方法),并标注它们的类别和边界框(如偏移量)。然后,用卷积神经网络对每个提议区域进行前向传播以抽取其特征。 接下来,我们用每个提议区域的特征来预测类别和边界框。下图展示了R-CNN模型

在这里插入图片描述

R-CNN包括以下四个步骤:

1.对输入图像使用选择性搜索来选取多个高质量的提议区域 。这些提议区域通常是在多个尺度下选取的,并具有不同的形状和大小。每个提议区域都将被标注类别和真实边界框;

2.选择一个预训练的卷积神经网络,并将其在输出层之前截断。将每个提议区域变形为网络需要的输入尺寸,并通过前向传播输出抽取的提议区域特征;

3.将每个提议区域的特征连同其标注的类别作为一个样本。训练多个支持向量机对目标分类,其中每个支持向量机用来判断样本是否属于某一个类别;

4.将每个提议区域的特征连同其标注的边界框作为一个样本,训练线性回归模型来预测真实边界框。

每次选取的锚框大小是不一样的,怎么使这些大小不一的锚框变成一个batch?

Rol pooling(兴趣区域池化层)

  • R-CNN比较关键的一层,让大小不一的锚框变成统一的形状
  • 给定一个锚框,先均匀的分割成nm块,然后输出每一块里面的最大值,这样不论锚框有多大都只输出nm个值。不同大小的锚框就变成了同样的大小,作为一个小批量处理比较方便

在这里插入图片描述

  • 上图中对 3 * 3 的黑色方框中的区域进行 2 * 2 的兴趣区域池化,由于 3 * 3 的区域不能均匀地进行切割成 4 块,所以会进行取整(最终将其分割成为 2 * 2、1 * 2、2 * 1、1 * 1 四块),在做池化操作的时候分别对四块中每一块取最大值,然后分别填入 2 * 2 的矩阵中相应的位置

兴趣区域池化层(RoI Pooing)和最大池化层的区别?

  • 池化层是通过窗口大小、填充、步幅来间接控制输出形状

  • 在兴趣区域池化层,对每个区域的输出形状是可以直接指定的

小结

  • 尽管 R-CNN 模型通过预训练的卷积神经网络有效地抽取了图像特征,但是速度非常慢(如果从一张图片中选取了上千个提议区域,就需要上千次的卷积神经网络的前向传播来执行目标检测,计算量非常大)

Fast R-CNN

Fast R-CNN对R-CNN最主要的改进之一:仅在整张图像上执行卷积神经网络的前向传播

在这里插入图片描述

  • Fast R-CNN 的改进是:在拿到一张图片之后,首先使用 CNN 对图片进行特征提取(不是对图片中的锚框进行特征提取,而是对整张图片进行特征提取,仅在整张图像上执行卷积神经网络的前向传播),最终会得到一个 7 * 7 或者 14 * 14 的 feature map
    抽取完特征之后,再对图片进行锚框的选择(selective search),搜索到原始图片上的锚框之后将其(按照一定的比例)映射到 CNN 的输出上
  • 映射完锚框之后,再使用 RoI pooling 对 CNN 输出的 feature map 上的锚框进行特征抽取,生成固定长度的特征(将 n * m 的矩阵拉伸成为 nm 维的向量),之后再通过一个全连接层(这样就不需要使用SVM一个一个的操作,而是一次性操作了)对每个锚框进行预测:物体的类别和真实的边缘框的偏移

Fast R-CNN 相对于 R-CNN 更快的原因是:Fast R-CNN 中的 CNN 不再对每个锚框抽取特征,而是对整个图片进行特征的提取(这样做的好处是:不同的锚框之间可能会有重叠的部分,如果对每个锚框都进行特征提取的话,可能会对重叠的区域进行多次重复的特征提取操作),然后再在整张图片的feature中找出原图中锚框对应的特征,最后一起做预测

Faster R-CNN

  • 为了精确地检测目标结果,Fast R-CNN 模型通常需要在选择性搜索中生成大量的提议区域
  • 使用了一个区域提议网络来替代启发式搜索获得更好的锚框,从而减少区域的生成数量,并保证目标检测的精度
    在这里插入图片描述
  • Faster R-CNN 的改进:使用 RPN 神经网络来替代 selective search (选择性搜索)
  • RoI 的输入是CNN 输出的 feature map 和生成的锚框
  • RPN 的输入是 CNN 输出的 feature map,输出是一些比较高质量的锚框(可以理解为一个比较小而且比较粗糙的目标检测算法: CNN 的输出进入到 RPN 之后再做一次卷积,然后生成一些锚框(可以是 selective search 或者其他方法来生成初始的锚框),再训练一个二分类问题:预测锚框是否框住了真实的物体以及锚框到真实的边缘框的偏移,最后使用 NMS 进行去重,使得锚框的数量变少)
  • RPN 的作用是生成大量结果很差的锚框,然后进行预测,最终输出比较好的锚框供后面的网络使用(预测出来的比较好的锚框会进入 RoI pooling,后面的操作与 Fast R-CNN 类似)
  • 通常被称为两阶段的目标检测算法:RPN 做小的目标检测(粗糙),整个网络再做一次大的目标检测(精准)
  • ·Faster R-CNN 目前来说是用的比较多的算法,准确率比较高,但是速度比较慢

区域提议网络的步骤如下图所示:
在这里插入图片描述

  • 区域提议网络作为Faster R-CNN模型的一部分,是和整个模型一起训练得到的。 (Faster R-CNN的目标函数不仅包括目标检测中的类别和边界框预测,还包括区域提议网络中锚框的二元类别和边界框预测。
  • 作为端到端训练的结果,区域提议网络能够学习到如何生成高质量的提议区域,从而在减少了从数据中学习的提议区域的数量的情况下,仍保持目标检测的精度。

Mask R-CNN
在这里插入图片描述
Mask R-CNN 是基于 Faster R-CNN 修改而来的,改进在于

  • 假设有每个像素的标号的话,就可以对每个像素做预测(FCN)
  • 将兴趣区域汇聚层替换成了兴趣区域对齐层(RoI pooling -> RoI align),使用双线性插值(bilinear interpolation)保留特征图上的空间信息,进而更适于像素级预测:对于pooling来说,假如有一个3 * 3的区域,需要对它进行2 * 2的RoI pooling操作,那么会进行取整从而切割成为不均匀的四个部分,然后进行 pooling 操作,这样切割成为不均匀的四部分的做法对于目标检测来说没有太大的问题,因为目标检测不是像素级别的,偏移几个像素对结果没有太大的影响。但是对于像素级别的标号来说,会产生极大的误差;RoI align 不管能不能整除,如果不能整除的话,会直接将像素切开,切开后的每一部分是原像素的加权(它的值是原像素的一部分)
  • 兴趣区域对齐层的输出包含了所有与兴趣区域的形状相同的特征图,它们不仅被用于预测每个兴趣区域的类别和边界框,还通过额外的全卷积网络预测目标的像素级位置

小结

  • R-CNN 是最早、也是最有名的一类基于锚框和 CNN 的目标检测算法(R-CNN 可以认为是使用神经网络来做目标检测工作的奠基工作之一),它对图像选取若干提议区域,使用卷积神经网络对每个提议区域执行前向传播以抽取其特征,然后再用这些特征来预测提议区域的类别和边框
  • Fast/Faster R-CNN持续提升性能:Fast R-CNN 只对整个图像做卷积神经网络的前向传播,还引入了兴趣区域汇聚层(RoI pooling),从而为具有不同形状的兴趣区域抽取相同形状的特征;Faster R-CNN 将 Fast R-CNN 中使用的选择性搜索替换为参与训练的区域提议网络,这样可以在减少提议区域数量的情况下仍然保持目标检测的精度;Mask R-CNN 在 Faster R-CNN 的基础上引入了一个全卷积网络,从而借助目标的像素级位置进一步提升目标检测的精度
  • Faster R-CNN 和 Mask R-CNN 是在追求高精度场景下的常用算法(Mask R-CNN 需要有像素级别的标号,所以相对来讲局限性会大一点,在无人车领域使用的比较多)

13.6.2 单发多框检测(SSD)

生成锚框

在这里插入图片描述

下面是SSD的网络结构:
在这里插入图片描述

  • 输入图像之后,首先进入一个基础网络来提取特征,提取完特征之后对每个像素生成大量的锚框(每个锚框就是一个样本,然后预测锚框的类别以及到真实边界框的偏移),多个卷积块来减半宽高
  • SSD 在给定锚框之后直接对锚框进行预测,而不需要做两阶段(为什么 Faster RCNN 需要做两次,而 SSD 只需要做一次?SSD 通过做不同分辨率下的预测来提升最终的效果,越到底层的 feature map,就越大,越往上,feature map 越少,因此底层更加有利于小物体的检测,而上层更有利于大物体的检测)
  • SSD 不再使用 RPN 网络,而是直接在生成的大量样本(锚框)上做预测,看是否包含目标物体;如果包含目标物体,再预测该样本到真实边缘框的偏移

模型精度
在这里插入图片描述

  • SSD 相对于Faster RCNN 来讲速度快很多,但是精度不是太好
  • SSD 的实现相对来讲比较简单,R-CNN 系列代码的实现非常困难

13.6.3 YOLO(You Only Look Once)

  • yolo也是一个单阶段算法,只有一个单神经网络来预测
  • yolo 也需要锚框,这点和 SSD 相同,但是 SSD 是对每个像素点生成多个锚框,所以在绝大部分情况下两个相邻像素的所生成的锚框的重叠率是相当高的,这样就会导致很大的重复计算量。
  • yolo 的想法是尽量让锚框不重叠:首先将图片均匀地分成 S * S 块,每一块就是一个锚框,每一个锚框预测 B 个边缘框(考虑到一个锚框中可能包含多个物体),所以最终就会产生 S ^ 2 * B 个样本,因此速度会远远快于 SSD
  • yolo 在后续的版本(V2,V3,V4…)中在细节上有持续的改进,但是核心思想没有变,真实的边缘框不会随机的出现,真实的边缘框的比例、大小在每个数据集上的出现是有一定的规律的,在知道有一定的规律的时候就可以使用聚类算法将这个规律找出来(给定一个数据集,先分析数据集中的统计信息,然后找出边缘框出现的规律,这样之后在生成锚框的时候就会有先验知识,从而进一步做出优化)

13.7 多尺度目标检测

如果以输入图像的每个像素为中心生成锚框,会得到太多需要计算的锚框。例如,一个561像素*728像素的输入图像,以每个像素为中心生成五个形状不同的锚框,就需要标注和预测超过200万个锚框.( 561 ∗ 728 ∗ 5 = 2042040 561*728*5=2042040 5617285=2042040)

13.7.1 多尺度锚框

在不同尺度下,可以生成不同数量和不同大小的锚框。例如 1 像 素 × 1 像 素 1像素\times1像素 1×1 1 像 素 × 2 像 素 1像素\times2像素 1×2 2 像 素 × 2 像 素 2像素\times2像素 2×2的目标可以分别以4种、2种、1种的方式出现在 2 像 素 × 2 像 素 2像素\times2像素 2×2的图像上。

下面使用代码演示如何在多个尺度下生成锚框,先读取一张图片,他的宽度和高度分别是561像素和728像素。

%matplotlib inline   
# 在Jupyter Notebook中内嵌显示matplotlib图像
import torch 
from d2l import torch as d2l

img=d2l.plt.imread('../img/catdog.jpg')   #调用的matplotlib.pyplot.imread() 函数来读取图像文件
h,w=img.shape[:2]
h,w

在这里插入图片描述
通过定义特征图的形状,我们可以确定任何图像上均匀抽样锚框的中心。
给定特征图的宽度fmap_w和高度fmap_h,下面的函数将均匀第对任何输入图像中fmap_h行和fmap_w列中的像素进行抽样。然后以这些均匀抽样的像素为中心,将会生成尺度为s且宽高比ratios不同的锚框

def display_anchors(fmap_w,fmap_h,s):
"""在特征图 (feature map) 上生成锚框 (anchors),每个单位(像素)作为锚框的中心"""
  d2l.set_figsize()
  #前两个维度上的值不影响输出
  fmap=torch.zeros(1,10,fmap_h,fmap_w)  #创建10个假特征图
  anchors=d2l.multibox_prior(fmap,sizes=s,ratios=[1,2,0.5])  #生成锚框 (基于特征图尺寸和比例)
  bbox_scale=torch.tensor((w,h,w,h))  #将归一化锚框坐标缩放回原图尺寸 (w,h,w,h格式)
  d2l.show_bboxes(d2l.plt.imshow(img).axes,anchors[0]*bbox_scale) #在图像上绘制锚框

探测小目标

display_anchors(fmap_w=4, fmap_h=4, s=[0.15]) 
#在4x4特征图上生成缩放比例为0.15的锚框,占图片的15%

在这里插入图片描述

将特征图的宽高减半,使用较大锚框来检测较大的目标。当尺度被设置为0.4时,一些锚框将会重叠

display_anchors(fmap_w=2, fmap_h=2, s=[0.4])

在这里插入图片描述
再进一步减半宽高,增加锚框尺度到0.8。此时锚框的中心即图像的中心

display_anchors(fmap_w=1, fmap_h=1, s=[0.8])

在这里插入图片描述

13.7.2 单发多框检测(SSD)代码实现

模型

在这里插入图片描述

  • 通过多尺度特征块,单发多框检测生成不同大小的锚框,并通过预测边界框的类别和偏移量来检测大小不同的目标,因此这是一个多尺度目标检测模型。
  • SSD基础网络块由锚框来进行类别预测和边界框预测

类别预测层

类别预测层使用一个保持输入的宽度和高度不变的卷积层,输出通道数=锚框数*(类别数+1是背景类)

%matplotlib inline
import torch
import torchvision
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l


def cls_predictor(nums_inputs,nums_anchors,num_classes):
  #使用卷积层来输出类别的预测可以降低模型复杂度,不改变宽高(而不是全连接层)
  return nn.Conv2d(nums_inputs,
                   nums_anchors*(num_classes+1), #输出通道数
                   kernel_size=3, #卷积核大小
                   padding=1
                   )

边界框预测层
与类别预测框不同,需要为每个锚框预测4个偏移量

def box_predictor(nums_inputs,nums_anchors):#边界框预测
  return nn.Conv2d(nums_inputs,
                   nums_anchors*4, #预测4个偏移
                   kernel_size=3,
                   padding=1)

多尺度预测
在不同尺度下,特征图的形状和以同一单元为中心的锚框数量有所不同,预测输出的形状也有所不同

def forward(x, block):#执行前向计算
    return block(x)

Y1 = forward(torch.zeros((2, 8, 20, 20)), cls_predictor(8, 5, 10)) 
#批量大小,通道数,高度,宽度       输入通道,锚框数,类别数
Y2 = forward(torch.zeros((2, 16, 10, 10)), cls_predictor(16, 3, 10))
Y1.shape, Y2.shape   #输出通道数=5*(10+1)=55  3*(10+1)=33

在这里插入图片描述

为了将不同的预测输出连接起来提高计算效率,需要把张量转换成一致的格式。
不同尺度下批量大小不变,先将预测结果转换成二维格式(批量大小,高度宽度通道数),再在维度1上拼接

可以用下面的函数来是实现展平:

torch.flatten(input,start_dim,end_dim)
start_dim:0 开始展平的起始维度(默认从第 0 维开始)。
end_dim:-1 结束展平的终止维度(默认为最后一维)。

def flatten_pred(pred):  #展平预测标签,start_dim=1是把高度宽度通道数拉成一维
    return torch.flatten(pred.permute(0, 2, 3, 1), start_dim=1)  #这里最后得到二维张量[0,[2,3,1]]

def concat_preds(preds): #​​将多个预测张量(preds)拼接(concatenate)成一个更大的张量​​
    return torch.cat([flatten_pred(p) for p in preds], dim=1)   #拼接上面组合而成的宽高通道数,保留批次数

concat_preds([Y1, Y2]).shape   #[2,55*20*20+33*10*10]=[2,22000+3300]

在这里插入图片描述

宽高减半块
每个宽高减半块由两个填充为1的33卷积快以及步幅为2的22最大汇聚层组成,虽然填充为1的3*3卷积块不改变特征图的形状,但是可以增加每个单元在其输出特征图上的感受野。

def down_sample_blk(in_channels, out_channels): 
    blk=[]
    for _ in range(2):
        blk.append(nn.Conv2d(in_channels,out_channels,kernel_size=3,padding=1)) #卷积层
        blk.append(nn.BatchNorm2d(out_channels)) #批量标准化
        blk.append(nn.ReLU()) #relu激活函数
        in_channels=out_channels
    blk.append(nn.MaxPool2d(2))#最大池化层来减半宽高
    return nn.Sequential(*blk)

基础网络块
一个基础网络块串联了三个高宽减半块,并逐步将通道数翻倍

def base_net(): #基本网络块
    blk = []
    num_filters = [3, 16, 32, 64]  #[输入, 第1层输出, 第2层输出, 第3层输出]
    for i in range(len(num_filters) - 1):
        blk.append(down_sample_blk(num_filters[i], num_filters[i + 1]))
    return nn.Sequential(*blk)

forward(torch.zeros((2, 3, 256, 256)), base_net()).shape     
#输出从3——>16->32->64  宽高减半三次256-128-64-32

完整的模型
完整的SSD模型由五个模块组成,第一个模块是基础网络块,第二个到第四个是高宽减半块,最后一个模块使用全局最大汇聚层将宽高都降为1.

def get_blk(i):
    if i == 0:
        blk = base_net()
    elif i == 1:
        blk = down_sample_blk(64, 128)
    elif i == 4:
        blk = nn.AdaptiveMaxPool2d((1, 1))
    else:
        blk = down_sample_blk(128, 128)
    return blk

定义前向传播函数,与图像分类不同,输出不仅包含特征图Y还包含在当前尺度下根据Y生成的锚框、预测类别、偏移量

def blk_forward(X,blk,size,ratio,cls_predictor,bbox_predictor):
  Y=blk(X)   #特征图
  anchors=d2l.multibox_prior(Y,sizes=size,ratios=ratio) #锚框
  cls_pred=cls_predictor(Y)  #预测类别
  bbox_preds=bbox_predictor(Y) #边界框预测
  return(Y,anchors,cls_pred,bbox_preds)

设置超参数

sizes = [[0.2, 0.272], [0.37, 0.447], [0.54, 0.619], [0.71, 0.79],
         [0.88, 0.961]] #这里可以随便取,较小值是0.2-1.05之间
ratios = [[1, 2, 0.5]] * 5
num_anchors = len(sizes[0]) + len(ratios[0]) - 1  #锚框数

定义完整的TinySSD模型

setattr 和 getattr 是 Python 的内置函数,用于动态地设置和获取对象的属性。它们在 PyTorch 的模型定义中经常用于模块化地管理子模块(如 nn.Module 的子模块)

class TinySSD(nn.Module):
    def __init__(self, num_classes, **kwargs):
        super(TinySSD, self).__init__(**kwargs)
        self.num_classes = num_classes #类别数
        idx_to_in_channels = [64, 128, 128, 128, 128] #输出通道
        for i in range(5):
            setattr(self, f'blk_{i}', get_blk(i)) # 设置第i个特征提取块(基础网络+下采样)
            setattr(  # # 设置第i个分类预测器(预测每个锚框的类别)
                self, f'cls_{i}',
                cls_predictor(idx_to_in_channels[i], num_anchors,
                              num_classes))
            setattr(self, f'bbox_{i}',  # 设置第i个边界框预测器(预测每个锚框的偏移量)
                    bbox_predictor(idx_to_in_channels[i], num_anchors))

    def forward(self, X):  # 对每个特征提取块进行前向传播
        anchors, cls_preds, bbox_preds = [None] * 5, [None] * 5, [None] * 5
        for i in range(5):
            X, anchors[i], cls_preds[i], bbox_preds[i] = blk_forward(
                X, getattr(self, f'blk_{i}'), sizes[i], ratios[i],
                getattr(self, f'cls_{i}'), getattr(self, f'bbox_{i}'))
        anchors = torch.cat(anchors, dim=1) #将所有锚框沿维度1拼接(合并不同特征图生成的锚框)
        cls_preds = concat_preds(cls_preds) # 拼接所有分类预测结果并调整形状
        cls_preds = cls_preds.reshape(cls_preds.shape[0], -1,  # 重塑为(batch_size, 总锚框数, 类别数+1)
                                      self.num_classes + 1)    #加1是背景类  
        bbox_preds = concat_preds(bbox_preds)  #拼接边缘预测框的结果
        return anchors, cls_preds, bbox_preds

创建实例,执行前向计算

net = TinySSD(num_classes=1)
X = torch.zeros((32, 3, 256, 256))
anchors, cls_preds, bbox_preds = net(X)

print('output anchors:', anchors.shape) #锚框集合数,锚框个数,每个锚框四个坐标
print('output class preds:', cls_preds.shape)#输入图像,锚框数,类别数+1
print('output bbox preds:', bbox_preds.shape)#输入,锚框数*4=5444*4=21776

在这里插入图片描述

读取数据集和初始化

batch_size = 32
train_iter, _ = d2l.load_data_bananas(batch_size)

device, net = d2l.try_gpu(), TinySSD(num_classes=1)
trainer = torch.optim.SGD(net.parameters(), lr=0.2, weight_decay=5e-4)  #SGD优化

定义损失函数和评价函数

cls_loss = nn.CrossEntropyLoss(reduction='none') #锚框类别的损失,使用交叉熵损失用于计算锚框类别预测的损失(多分类问题)
# reduction='none'表示不自动求平均或求和,保留每个样本的损失值
bbox_loss = nn.L1Loss(reduction='none')  #L1范数损失,预测值与真实值之差的绝对值
#bbox_mask令负类锚框和填充锚框不参与损失的计算
def calc_loss(cls_preds, cls_labels, bbox_preds, bbox_labels, bbox_masks):
    """  计算总损失=分类损失+边界框回归损失
        cls_preds (Tensor): 分类预测结果,形状为(batch_size, 总锚框数, num_classes+1)
        cls_labels (Tensor): 分类真实标签,形状为(batch_size, 总锚框数)
        bbox_preds (Tensor): 边界框偏移量预测,形状为(batch_size, 总锚框数, 4)
        bbox_labels (Tensor): 边界框偏移量真实值,形状同bbox_preds
        bbox_masks (Tensor): 边界框掩码,用于过滤负类锚框和填充锚框,形状同bbox_preds """
    batch_size, num_classes = cls_preds.shape[0], cls_preds.shape[2]
    cls = cls_loss(cls_preds.reshape(-1, num_classes), #预测结果reshape为(-1, num_classes),即(批量*锚框数, 类别数)
                   cls_labels.reshape(-1)).reshape(batch_size, -1).mean(dim=1) #reshape回(batch_size, 总锚框数),再对每个样本的所有锚框损失求平均(dim=1)
    bbox = bbox_loss(bbox_preds * bbox_masks,
                     bbox_labels * bbox_masks).mean(dim=1)
    return cls + bbox



def cls_eval(cls_preds, cls_labels):
    return float( #类别预测结果在最后一维,argmax需要指定最后一维
        (cls_preds.argmax(dim=-1).type(cls_labels.dtype) == cls_labels).sum())
#因为偏移量使用了L1范数损失,这里使用平均绝对误差来评价边界框
def bbox_eval(bbox_preds, bbox_labels, bbox_masks):
    return float((torch.abs((bbox_labels - bbox_preds) * bbox_masks)).sum())

训练模型

# 设置训练轮次为20,并初始化计时器
num_epochs, timer = 20, d2l.Timer()

# 创建动画可视化器,用于绘制训练过程中的分类错误率和边界框MAE
animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],
                        legend=['class error', 'bbox mae'])

# 将模型移动到指定设备(GPU/CPU)
net = net.to(device)

# 开始训练循环
for epoch in range(num_epochs):
    # 初始化指标累加器(存储4个值:分类正确数、总样本数、边界框误差和、有效锚框数)
    metric = d2l.Accumulator(4)
    
    # 设置为训练模式(启用dropout/batch norm等训练专用层)
    net.train()
    
    # 遍历训练数据集
    for features, target in train_iter:
        # 开始计时(测量单次迭代时间)
        timer.start()   
        # 清空优化器的梯度
        trainer.zero_grad()  
        # 将数据移动到指定设备
        X, Y = features.to(device), target.to(device)  
        # 前向传播:获取锚框、分类预测和边界框预测
        anchors, cls_preds, bbox_preds = net(X)  
        # 生成训练目标:为每个锚框分配真实标签
        bbox_labels, bbox_masks, cls_labels = d2l.multibox_target(anchors, Y)   
        # 计算总损失(分类损失 + 边界框回归损失)
        l = calc_loss(cls_preds, cls_labels, bbox_preds, bbox_labels, bbox_masks)
        # 反向传播(计算梯度)
        l.mean().backward()
        # 更新模型参数
        trainer.step()
        
        # 累计评估指标:
        # [0]分类正确数 [1]总锚框数 [2]边界框误差和 [3]有效锚框数
        metric.add(cls_eval(cls_preds, cls_labels), cls_labels.numel(),
                   bbox_eval(bbox_preds, bbox_labels, bbox_masks),
                   bbox_labels.numel())
    
    # 计算本轮次指标:
    # 分类错误率 = 1 - 正确数/总数
    # 边界框MAE = 误差和/有效锚框数
    cls_err, bbox_mae = 1 - metric[0] / metric[1], metric[2] / metric[3]
    
    # 更新可视化曲线
    animator.add(epoch + 1, (cls_err, bbox_mae))

# 输出最终性能指标
print(f'class err {cls_err:.2e}, bbox mae {bbox_mae:.2e}')

# 计算并输出训练速度(样本/秒)
print(f'{len(train_iter.dataset) / timer.stop():.1f} examples/sec on '
      f'{str(device)}')

在这里插入图片描述
预测目标

x.squeeze()​​用于去除所有大小为1的维度​​(即“压缩”维度)。它的作用可以理解为“去掉不必要的单维度”,类似于数学中的降维操作。

X = torchvision.io.read_image('../img/banana.jpg').unsqueeze(0).float()
img = X.squeeze(0).permute(1, 2, 0).long()
## 1. 去除batch维度(从[1,C,H,W]变为[C,H,W])
# 2. 调整通道顺序从[C,H,W]变为[H,W,C](适合matplotlib显示)
# 3. 转换为long类型(整数像素值)

def predict(X):
    net.eval()#设置为评估模型
    # 前向传播获取模型输出:
    # anchors: 生成的锚框 [1, 5444, 4]
    # cls_preds: 类别预测 [1, 5444, 2]
    # bbox_preds: 边界框偏移量 [1, 5444, 4]
    anchors, cls_preds, bbox_preds = net(X.to(device))
    # 对类别预测进行softmax归一化,得到概率分布
    # 调整维度从[1,5444,2]变为[1,2,5444](符合multibox_detection输入要求)
    cls_probs = F.softmax(cls_preds, dim=2).permute(0, 2, 1)
    #执行多框检测
    output = d2l.multibox_detection(cls_probs, bbox_preds, anchors)
    #   # 过滤无效检测结果(类别ID=-1表示无效预测)
    idx = [i for i, row in enumerate(output[0]) if row[0] != -1]
    return output[0, idx]

output = predict(X)

筛选所有置信度不低于 0.9 的边界框,做为最终输出

def display(img, output, threshold):
    d2l.set_figsize((5, 5)) # 设置图像显示大小为5x5英寸
    fig = d2l.plt.imshow(img)
    for row in output:
        score = float(row[1])
        if score < threshold:
            continue
        h, w = img.shape[0:2]
        bbox = [row[2:6] * torch.tensor((w, h, w, h), device=row.device)]
        d2l.show_bboxes(fig.axes, bbox, '%.2f' % score, 'w')

display(img, output.cpu(), threshold=0.9)
#output.cpu(): 将检测结果移回CPU(如果之前在GPU上)
#threshold=0.9: 只显示置信度≥90%的检测框

在这里插入图片描述

13.8 语义分割和数据集

13.8.1 语义分割

在这里插入图片描述

  • 在图片分类中,其主要任务是给定一张图片,识别图片中主体物体
  • 目标检测,也叫物体检测,其主要任务是找出图片中多个感兴趣的物体,并且找到每个物体的具体位置(使用方形边界框来标注和预测图像中的目标),问题是这些框很多时候比较粗糙,只能标注出大致的位置,但是无法标注出物体各部分的具体位置以及物体与背景之间的分割线(物体的具体轮廓)
  • 语义分割可以识别并理解图像中每一个像素的内容(将图片中的每个像素分类到对应的类别),其语义区域的标注和预测是像素级的
  • 与图片分类、目标检测相比,语义分割标注的像素级边框更加精细

应用场景

背景虚化
在这里插入图片描述

路面分割
在这里插入图片描述

对比

  • 图像分割:将图像分割成若干区域,根据图像中像素之间的相关性来分割。不需要有关图像像素的标签信息,在预测时也不能保证分割的区域有我们希望得到的语义。
  • 实例分割(同时检测并分割),研究如何识别图像中各个目标实例的像素级区域。不仅需要区分语义,还要区分不同的目标实例。如下图所示,实例分割还要区分狗的不同标号。
    在这里插入图片描述

13.8.2 语义分割数据集

最重要的语义分割数据集是 Pascal VOC2012

下载数据集
和前面一样使用url和SHA-1验证码验证文件完整性下载数据集,再解压

%matplotlib inline
import os
import torch
import torchvision
from d2l import torch as d2l

#下载数据集,使用url和SHA-1验证码来验证文件完整性
d2l.DATA_HUB['voc2012'] = (d2l.DATA_URL + 'VOCtrainval_11-May-2012.tar',
                           '4e443f8a2eca6b1dac8a6c57641b67dd40621a49')

#解压文件
voc_dir = d2l.download_extract('voc2012', 'VOCdevkit/VOC2012')

将输入的特征图和标注后的标签图读入内存

def read_voc_images(voc_dir, is_train=True):
    """读取所有VOC图像并标注。"""
    #判断要读取的文件是训练集还是测试集
    txt_fname = os.path.join(voc_dir, 'ImageSets', 'Segmentation',
                             'train.txt' if is_train else 'val.txt')
    mode = torchvision.io.image.ImageReadMode.RGB ## 设置图像读取模式为RGB(三通道)
    with open(txt_fname, 'r') as f:
        images = f.read().split()  #按空格、换行来分割文件名
    features, labels = [], []
    for i, fname in enumerate(images):  #读取图片和标签
        features.append(#读取原始图片
            torchvision.io.read_image(
                os.path.join(voc_dir, 'JPEGImages', f'{fname}.jpg')))
        labels.append(## 读取对应的标签(分割标注图)
            torchvision.io.read_image(
                os.path.join(voc_dir, 'SegmentationClass', f'{fname}.png'),
                mode))
    return features, labels

train_features, train_labels = read_voc_images(voc_dir, True)
print(train_features[0])

绘制前五个图像和标签(标注后的图片)

n = 5
imgs = train_features[0:n] + train_labels[0:n]  #这里的lables是语义分割标注后的图片
imgs = [img.permute(1, 2, 0) for img in imgs] # 调整张量维度顺序,从PyTorch的(通道数,高,宽转为Matplotlib需要的(高,宽,通道数)
d2l.show_images(imgs, 2, n);   #show_images(图片列表,行数,列数,scale显示比例默认为2)

在这里插入图片描述
列举RGB颜色值和类名

VOC_COLORMAP = [[0, 0, 0], [128, 0, 0], [0, 128, 0], [128, 128, 0],
                [0, 0, 128], [128, 0, 128], [0, 128, 128], [128, 128, 128],
                [64, 0, 0], [192, 0, 0], [64, 128, 0], [192, 128, 0],
                [64, 0, 128], [192, 0, 128], [64, 128, 128], [192, 128, 128],
                [0, 64, 0], [128, 64, 0], [0, 192, 0], [128, 192, 0],
                [0, 64, 128]]

VOC_CLASSES = [  
    'background', 'aeroplane', 'bicycle', 'bird', 'boat', 'bottle', 'bus',
    'car', 'cat', 'chair', 'cow', 'diningtable', 'dog', 'horse', 'motorbike',
    'person', 'potted plant', 'sheep', 'sofa', 'train', 'tv/monitor']

查找标签中每个像素的类索引

这里使用(r*256+g)*256+b的结果来作为索引唯一标识一个类别是一个向量计算,比逐元素(r,g,b)快很多。这种方法是计算机视觉中,​​处理颜色标注数据的经典技巧​​。

def voc_colormap2label():
    """构建从RGB到VOC类别索引的映射。"""
    colormap2label = torch.zeros(256**3, dtype=torch.long)
    for i, colormap in enumerate(VOC_COLORMAP):
        colormap2label[(colormap[0] * 256 + colormap[1]) * 256 +
                       colormap[2]] = i   #从RGB列表转换为VOC类别
    return colormap2label

def voc_label_indices(colormap, colormap2label):
    """将VOC标签中的RGB值映射到它们的类别索引。"""
    colormap = colormap.permute(1, 2, 0).numpy().astype('int32')
    idx = ((colormap[:, :, 0] * 256 + colormap[:, :, 1]) * 256 +
           colormap[:, :, 2])
    return colormap2label[idx] #从类别转换为RGB对应的像素值

举例

y = voc_label_indices(train_labels[0], voc_colormap2label())
y[105:115, 130:140], VOC_CLASSES[1]  #y[105:115, 130:140]的像素区域

在这里插入图片描述

  • 输出张量中,飞机头部的类别索引为1,背景为0

预处理数据

在语义分割中,为了避免预测的像素类别映射到原始图像不够精细,我们将图像裁剪为固定尺寸,而不是在缩放。使用随机裁剪,裁剪输入图像和标签的相同区域。

def voc_rand_crop(feature, label, height, width):
    """随机裁剪特征和标签图像。"""
    rect = torchvision.transforms.RandomCrop.get_params(  # 获取随机裁剪参数(相同的随机种子保证特征和标签裁剪位置一致)
        feature, (height, width))
    feature = torchvision.transforms.functional.crop(feature, *rect) #特征图裁剪
    label = torchvision.transforms.functional.crop(label, *rect)  #标签图裁剪
    return feature, label

imgs = []
for _ in range(n):
    imgs += voc_rand_crop(train_features[0], train_labels[0], 200, 300)

imgs = [img.permute(1, 2, 0) for img in imgs]
d2l.show_images(imgs[::2] + imgs[1::2], 2, n);

在这里插入图片描述

自定义数据集

class VOCSegDataset(torch.utils.data.Dataset):
    """一个用于加载VOC数据集的自定义数据集。"""
    def __init__(self, is_train, crop_size, voc_dir):
        self.transform = torchvision.transforms.Normalize(  #标准化
            mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
        self.crop_size = crop_size #裁剪尺寸
        features, labels = read_voc_images(voc_dir, is_train=is_train)
        self.features = [  #过滤尺寸不足的特征图,并初始化
            self.normalize_image(feature)
            for feature in self.filter(features)]
        self.labels = self.filter(labels)  #过滤尺寸不足的标注图
        self.colormap2label = voc_colormap2label() #建立颜色矩阵和类别的映射
        print('read ' + str(len(self.features)) + ' examples')

    def normalize_image(self, img):
        return self.transform(img.float()) #转化为float后标准化

    def filter(self, imgs):
        return [
            img for img in imgs if (img.shape[1] >= self.crop_size[0] and  #高度检测
                                    img.shape[2] >= self.crop_size[1])] #宽度检测

    def __getitem__(self, idx):
        feature, label = voc_rand_crop(self.features[idx], self.labels[idx], #随机裁剪
                                       *self.crop_size)
        return (feature, voc_label_indices(label, self.colormap2label))  #把RGB标签图转换成类别ID图

    def __len__(self):
        return len(self.features)


crop_size = (320, 480)
voc_train = VOCSegDataset(True, crop_size, voc_dir) #加载训练集
voc_test = VOCSegDataset(False, crop_size, voc_dir) #加载测试集

在这里插入图片描述

读取数据集

batch_size = 64
train_iter = torch.utils.data.DataLoader(
    voc_train, batch_size, shuffle=True, drop_last=True, #batch_size = 64
train_iter = torch.utils.data.DataLoader(
    voc_train, batch_size, shuffle=True, drop_last=True, 
    # drop_last=True 丢弃最后不足batch_size的批次(保持批次形状一致)
    num_workers=d2l.get_dataloader_workers())
for X, Y in train_iter:
    print(X.shape)
    print(Y.shape)
    break
    num_workers=d2l.get_dataloader_workers())
for X, Y in train_iter:
    print(X.shape)
    print(Y.shape)
    break

在这里插入图片描述

整合所有组件

def load_data_voc(batch_size, crop_size):
    """加载VOC语义分割数据集。"""
    voc_dir = d2l.download_extract('voc2012',
                                   os.path.join('VOCdevkit', 'VOC2012'))
    num_workers = d2l.get_dataloader_workers()
    train_iter = torch.utils.data.DataLoader(
        VOCSegDataset(True, crop_size, voc_dir), batch_size, shuffle=True,
        drop_last=True, num_workers=num_workers)
    test_iter = torch.utils.data.DataLoader(
        VOCSegDataset(False, crop_size, voc_dir), batch_size, drop_last=True,
        num_workers=num_workers)
    return train_iter, test_iter

小结

  • 语义分割通过将图像划分为属于不同语义类别的区域,来识别并理解图像中像素级别的内容。
  • 语义分割的一个重要的数据集叫做Pascal VOC2012。
  • 由于语义分割的输入图像和标签在像素上一一对应,输入图像会被随机裁剪为固定尺寸而不是缩放

13.9 转置卷积 (transposed convolution)

  • 卷积不会增大输入的高宽,通常要么不变要不减半
  • 转置卷积可以增加输入的高宽
  • 如果输入图像和输出图像的空间维度相同(高宽),像素级的语义分割会很方便

基础卷积

在这里插入图片描述

  • 上图的步幅stride为1,填充为0
  • 这个核会在输入上以步幅为 1 进行滑动且没有填充,对于输入的每一个元素,它会跟核上的每一个元素按元素做乘法,然后逐次写回到对应的位置(写回到一个更大的矩阵中,除了写回的位置,其他元素初始化为 0
  • 生成的结果是(n+
import torch
from torch import nn
from d2l import torch as d2l

def trans_conv(X, K):
    """ 实现转置卷积 """
    h, w = K.shape #卷积核的高宽
    Y = np.zeros((X.shape[0] + h - 1, X.shape[1] + w - 1))
    for i in range(X.shape[0]):
        for j in range(X.shape[1]):
            Y[i: i + h, j: j + w] += X[i, j] * K
    return Y

#验证上图结果
X = torch.tensor([[0.0, 1.0], [2.0, 3.0]])
K = torch.tensor([[0.0, 1.0], [2.0, 3.0]])
trans_conv(X, K)

在这里插入图片描述

调包实现

nn.ConvTranspose2d 二维转置卷积
nn.ConvTranspose2d(
in_channels=1, # 输入通道数
out_channels=1, # 输出通道数
kernel_size=2, # 转置卷积核大小
stride=1, # 步长(默认1)
padding=0, # 输入填充(默认0)
output_padding=0, # 输出填充(默认0)
bias=False # 是否使用偏置项
)

X, K = X.reshape(1, 1, 2, 2), K.reshape(1, 1, 2, 2)
tconv = nn.ConvTranspose2d(1, 1, kernel_size=2, bias=False) # bias是否使用偏置项
tconv.weight.data = K
tconv(X)

在这里插入图片描述

转置卷积与卷积的异同

1、工作原理

  • 常规卷积是输入跟核进行按元素的乘法并相加,最后得到对应位置的值,通过核在输入上进行滑动从而得到输出
  • 转置卷积是输入的单个元素跟核按元素做乘法但不相加,保持核的大小,然后按元素写回到一个更大的矩阵的对应位置(输入的每个元素会生成与核大小相同的矩阵写入到一个更大的矩阵的对应位置,所以输出的高宽相对于输入来讲是变大的)

2、输入输出

  • 常规卷积通过卷积核 “减少” 输入元素
  • 转置卷积通过卷积核 “广播” 输入元素,从而产生大于输入的输出

3、填充

  • 常规卷积中将填充应用于输入(如果将高和宽两侧的填充数指定为 1 时,常规卷积的输入中将增加第一和最后的行和列)
  • 转置卷积中将填充应用于输出(如果将高和宽两侧的填充数指定为 1 时,转置卷积的输出中将删除第一和最后的行和列)
tconv = nn.ConvTranspose2d(1, 1, kernel_size=2, padding=1, bias=False)
tconv.weight.data = K
tconv(X)  #填充为1,输出删掉第一行第三行,第一列和第三列,只剩中间的4

在这里插入图片描述

4、步幅

  • 常规卷积中,步幅所指定的是卷积核在输入上的滑动距离
  • 转置卷积中,步幅所指定的是卷积核每次运算结果写回到中间结果(输出)矩阵中对应位置的滑动距离,上图两张图是卷积核为 2 × 2 ,步幅为1和2 的转置卷积运算

在这里插入图片描述

  • stride步幅为1

在这里插入图片描述

  • 步幅为2
tconv = nn.ConvTranspose2d(1, 1, kernel_size=2, stride=2, bias=False)
tconv.weight.data = K
tconv(X)

在这里插入图片描述

5、多通道

  • 对于多输入和输出通道,转置卷积与常规卷积以相同的方式运作

一个张量经过卷积和转置卷积得到的张量形状相同

X = torch.rand(size=(1, 10, 16, 16))
conv = nn.Conv2d(10, 20, kernel_size=5, padding=2, stride=3)
tconv = nn.ConvTranspose2d(20, 10, kernel_size=5, padding=2, stride=3)
tconv(conv(X)).shape == X.shape

在这里插入图片描述

与矩阵变换的联系

可以用矩阵乘法来实现卷积

X = torch.arange(9.0).reshape(3, 3)
print(X)
K = torch.tensor([[1.0, 2.0], [3.0, 4.0]])
Y = d2l.corr2d(X, K)
Y

在这里插入图片描述

  • 0* 1+1* 2+3* 3+4* 4=27

把卷积核K重写为包含大量0的稀疏矩阵W,非0元素来自卷积核

  • 添加多个零的目的是为了 ​​将 2D 卷积核转换为等效的矩阵乘法形式​​(即 ​​Toeplitz 矩阵​​)
def kernel2matrix(K):
    k, W = torch.zeros(5), torch.zeros((4, 9))
    k[:2], k[3:5] = K[0, :], K[1, :]
    W[0, :5], W[1, 1:6], W[2, 3:8], W[3, 4:] = k, k, k, k
    return W

W = kernel2matrix(K)
W

在这里插入图片描述

Y == torch.matmul(W, X.reshape(-1)).reshape(2, 2)

在这里插入图片描述

  • 逐行连接输入X,w和向量化的X相乘实现了上述的Y
Z = trans_conv(Y, K)
Z == torch.matmul(W.T, Y.reshape(-1)).reshape(3, 3)

在这里插入图片描述

  • 卷积的前向传播可以由输入向量X和权重矩阵W相乘来实现 y=Wx
  • 卷积的反向传播可以通过输入向量X和权重矩阵转置 W^T xy=W ^T
  • 转置卷积可以交换卷积层的前向传播函数和反向传播函数

小结

  • 与通过卷积核减少输入元素的常规卷积相反,转置卷积通过卷积核广播输入元素,从而产生形状大于输入的输出
  • 如果我们将 X 输入卷积层 f 来获得输出 Y = f(X) 并创造一个与 f 有相同的超参数、但输出通道数是 X 中通道数的转置卷积层 g ,那么 g(Y) 的形状将与 X 相同。
  • 可以使用矩阵乘法来实现卷积。转置卷积层能够交换卷积层的正向传播函数和反向传播函数

总结

本周学习了计算机视觉中的目标检测和语义分割技术,重点掌握了锚框生成、SSD模型实现、转置卷积原理以及Pascal VOC数据集的处理方法。通过代码实践,理解了多尺度特征融合、类别预测和边界框回归等核心概念。下周计划完成全卷积网络和风格迁移的学习,尽快结束计算机视觉章节,开始循环神经网络的学习。

源码链接: https://pan.quark.cn/s/a4b39357ea24 斐讯K2是一款广受用户青睐的无线路由器,其运行表现稳定且具备较高的可操作性,在DIY爱好者群体中拥有极高的声誉。本资料将系统性地阐述斐讯K2的固件刷机方法及其关联的技术要点。固件升级是路由器爱好者改善设备性能、扩展功能的一种普遍手段,经由替换出厂固件,能够达更加个性化的网络配置、增强安全防护等目标。斐讯K2固件资源库涵盖了多种知名的非官方固件,诸如Tomato Pheonix 不死鸟、高恪、PandoraBox 潘多拉等,这些固件均具备独特的优势,能够适配不同用户的需求。 1. Tomato Pheonix 不死鸟:Tomato是一款立足于Linux的开源固件,以其精巧、高效而备受推崇。不死鸟版本是专门为华硕及斐讯路由器优化的分支,提供了卓越的QoS(服务质量)配置、详尽的图表监控以及便捷的固件升级途径。对于那些需要精准调控带宽和监测网络状态的用户而言,这是一个理想的选项。 2. 高恪:高恪固件是OpenWrt的定制化版本,着重于操作的便捷性和运行的可靠性,特别适合对路由器操作不甚熟悉的用户群体。它提供了一些实用的功能,例如内置的广告屏蔽、快速测速工具等,同时保留了OpenWrt的适应性。 3. PandoraBox 潘多拉:潘多拉盒是另一款基于OpenWrt的固件,它以丰富的插件库和强大的自定义潜力而闻名。用户能够依据个人需求安装各类插件,实现更多功能,如远程接入、DDNS(动态域名解析服务)等。 4. 官方固件的纯净版本与定制版本:官方固件通常更侧重于稳定性,纯净版意味着未预置额外的应用或服务,适合注重稳定性的用户。定制版则可能包含了制造商的特色功能或优...
源码下载地址: https://pan.quark.cn/s/926926948560 AS3.0与XML结合的通用图片滚动功能,是一种基于ActionScript 3.0和XML技术的动态图像展示方案,非常适合初学者进行学习和实践应用。此项目的关键在于借助XML文件作为数据媒介,用来保存图像的相关参数,例如图像的链接地址、展示的次序等,接着在AS3.0环境中对XML进行解析,并动态地载入和展示这些图像,达图像的滚动或是循环播放的目的。 我们需要明确ActionScript 3.0(AS3.0)是Adobe Flash Professional以及Flex Builder等开发工具中采用的编程语言,用于构建交互式内容以及丰富的互联网应用。相较于先前的版本,AS3.0在性能上有了大幅度的提升,并且引入了更为规范的面向对象编程模式,涵盖了类、接口以及包等概念。 XML(可扩展标记语言)是一种简明且高效的数据传输格式,既便于人类阅读和编写,也易于机器进行解析和生。在该项目中,XML文件用于存储图像数据,例如图像的URL、延时的时长、动画的样式等,通过这种方式可以将数据与程序代码分离,从而增强代码的可维护性与可扩展程度。 实施这一图片滚动功能,主要涉及到以下AS3.0的核心知识点: 1. **XML解析**:运用`XML`类来载入并解析XML文件,从而获取图像的清单。AS3.0提供了简便的API来操作XML节点,例如`children()`、`attributes()`等,用以获取子节点和属性值。 2. **事件监听**:借助`EventDispatcher`类来监控载入和解析过程中的事件,比如`Event.OPEN`、`Event.PROGRESS`、`Event...
内容概要:本文介绍了软件许可管理的技术实现方式及相关工具资源,重点阐述了加密外壳(EMS)和API加密两种保护机制。加密外壳通过将程序(如.exe、.dll、.apk)封装在加密壳中,实现运行时内存解密,防止静态反编译和代码篡改,同时支持对数据文件、系统参数及部分代码的加密,并依赖硬件锁(HL)或软件锁(SL)进行授权控制。API加密则通过在代码中嵌入安全验证调用,确保授权合法后才执行核心逻辑。文章还说明了锁的类型(HL/SL)、模式(有驱/AdminMode与无驱/UserMode)、升级路径以及虚拟时钟功能,并描述了产品授权流程从功能定义到产品创建、授权生的全过程,支持通过C2V文件或锁ID复制已有授权状态。文中附带多个开源平台链接和技术博客参考资源。; 适合人群:从事软件版权保护、授权系统开发或安全技术研究的研发人员,尤其是具备一定逆向工程、软件安全基础的1-3年经验开发者。; 使用场景及目标:①构建安全的软件授权体系,防止盗版和非法使用;②实现灵活的功能授权管理(如时效、并发、硬件绑定);③选择合适的加密方案(硬件锁/软锁、有驱/无驱)并集到现有产品中;④学习加密外壳与API验证的实际应用方法; 阅读建议:此资源侧重于软件许可的技术架构与实施细节,建议结合提供的GitHub、Gitee项目链接及CSDN技术文章深入理解实现原理,并通过实际调试加密壳和模拟授权流程加强实践能力。
内容概要:本文聚焦于“风光制氢合氨系统优化研究”,系统阐述了基于Cplex求解器对该耦合系统进行数学建模与优化求解的全过程,并提供了完整的Matlab代码实现。研究整合风能、光伏等可再生能源发电与电解水制氢、合氨化工工艺,构建涵盖系统容量配置与运行调度的联合优化模型,旨在提升绿电就地消纳水平、降低碳排放强度并实现综合能源利用效率的最大化。文中详细解析了优化模型的核心,包括以综合本最小化或能源效率最大化为目标的目标函数设计,以及涵盖设备出力能力、系统能量动态平衡、设备启停特性等关键环节的约束条件建模方法,利用Cplex求解器进行高效精确求解,模型适用于并网与离网等多种运行场景。; 适合人群:具备一定能源系统建模与优化理论基础,熟练掌握Matlab编程语言及常用优化工具箱(如YALMIP)应用的科研人员与工程技术从业者,特别适用于从事综合能源系统规划、绿色氢能与绿氨生产、可再生能源高效集等前沿领域的硕士、博士研究生及高校科研人员。; 使用场景及目标:①复现高水平学术论文中关于风光制氢合氨系统的复杂优化模型;②深入掌握Cplex求解器在大规模、多约束能源系统优化问题中的高级建模与调用技巧;③开展面向“双碳”战略的绿氢、绿氨生产项目的可行性分析、规划设计与运行策略研究,为清洁能源项目的科学决策与工程落地提供量化依据和技术支撑。; 阅读建议:建议读者结合文中提供的Matlab代码与相关领域的权威文献进行对照学习,重点剖析模型构建的物理逻辑与数学推导过程,熟练掌握Cplex与Matlab的接口调用方法;鼓励读者通过调整系统参数、修改目标函数或扩展模型结构(如引入更多不确定性因素)等方式进行二次开发,以适应不同的实际应用场景,进一步深化对综合能源系统优化的理解与实践能力。
打开链接下载源码: https://pan.quark.cn/s/a4b39357ea24 本资源汇编了数据结构实验的上机任务解答,涵盖了代码实现以及详尽的注释说明。以下是对相关知识的梳理: 1. 数据结构实验:该文档呈现了数据结构实验的上机任务解答,包含代码实现与详尽的注释说明。此实验旨在评估学生对数据结构的掌握程度及编程能力。 2. 结构体数组:在C++语言中,结构体数组是一种常见的数据组织形式。结构体数组能够存储大量数据,并支持灵活的操作。在本资源中,结构体数组被用于存储赫夫曼树的节点信息。 3. 赫夫曼树:赫夫曼树是一种特殊的二叉树结构,其每个节点的权值等于其左右子树的权值之和。赫夫曼树在数据压缩、编码与解码等领域具有广泛的应用。在本资源中,赫夫曼树被用于实现数据的编码与解码功能。 4. 选择函数:选择函数是赫夫曼树的关键算法之一,负责选取赫夫曼树的根节点与叶节点。在本资源中,选择函数通过递归算法来选取赫夫曼树的根节点与叶节点。 5. 创建赫夫曼树:构建赫夫曼树是赫夫曼编码的核心步骤。在本资源中,采用递归算法来构建赫夫曼树,并将其存储在结构体数组中。 6. 赫夫曼编码:赫夫曼编码是一种可变长度的编码方式,利用赫夫曼树表示符号的频率信息。在本资源中,赫夫曼编码被用于对输入字符串进行编码,并存储在字符数组中。 7. 字符串操作:字符串操作是C++语言的基础功能之一。在本资源中,通过字符串操作实现字符串的连接与截取等操作。 8. 输入输出操作:输入输出操作是C++语言的基础功能之一。在本资源中,利用输入输出操作读取输入数据并输出结果。 9. 指针操作:指针操作是C++语言的基础功能之一。在本资源中,通过指针操作实现动态内存分配和...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值