YOLO-学习笔记
文章目录
- 划分区域
- 筛选需要的目标
- 聚类
- NMS(非极大值抑制)
- YOLOV1代码解析
- 特征提取层
- He 初始化(He Initialization)
- 问题
- He 初始化的原理
- 解释:
- 检测头
- train()
- 输入处理函数
- target_process
- YOLO-V2基于Anchor的偏移量
- Ground Truth (GT):
- Anchor Boxes:
- **GT 和 Anchor 的关系**:
- YOLO v3-检测头分叉
- YOLO v4
- Mish 激活函数
- 使用了 YOLOv4 的 "Self-ensembling" 技术
- CIoU (Complete Intersection over Union) 损失函数
- 加权交叉熵损失(Weighted Cross-Entropy Loss)
- 使用了 Soft-NMS(Soft Non-Maximum Suppression)
- YOLO v5
- 自适应anchor
- Transformer
- Transformer 的主要结构
- 1. 编码器(Encoder)
- 解码器(Decoder)
- Transformer 的关键机制
- 自注意力(Self-Attention)
- 多头注意力(Multi-Head Attention)
- 编码器
- Transformer 优势
划分区域
YOLO本质上用一个(c,x,y,w,h)去负责image某个区域的目标。c表示置信度,x,y,为中心位置,w,h为高宽。可以把图片划分成4*4的区域,先找中心点是否有目标位置,有目标就c置概率
筛选需要的目标
如果一个区域有多个检测目标,那就采取两个方法
聚类
将候选框目标分成几个类别,但是会有问题,不知道聚成几个类别。
NMS(非极大值抑制)
2个框重合度很高,大概率是一个目标,那就只取一个框。一直抑制下去,直到找到
YOLOV1代码解析
特征提取层
class VGG(nn.Module): # 定义VGG类,继承自torch.nn.Module
def __init__(self): # 初始化方法,定义网络的层结构和初始化方式
super(VGG, self).__init__() # 调用父类的构造函数,super() 是 Python 的一个内置函数,用来调用父类(基类)的方法
# VGG网络的层配置,'M'表示MaxPool层,数字表示卷积层的输出通道数
cfg = [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 512, 'M']
layers = [] # 用于存储网络的层
batch_norm = False # 是否使用批归一化,默认为False
in_channels = 3 # 输入图像的通道数,RGB图像通常是3个通道
# 遍历cfg配置,构建每一层
for v in cfg:
if v == 'M': # 如果是池化层,添加一个MaxPool2d
layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
else: # 如果是卷积层
conv2d = nn.Conv2d(in_channels, v, kernel_size=3, padding=1) # 3x3卷积,padding=1保证输出与输入的大小一致
if batch_norm: # 如果使用批归一化,batch_norm 是一个布尔值变量,见前面,
layers += [conv2d, nn.BatchNorm2d(v), nn.ReLU(inplace=True)] # 卷积、批归一化、ReLU激活
else: # 不使用批归一化
layers += [conv2d, nn.ReLU(inplace=True)] # 只有卷积和ReLU激活
in_channels = v # 更新输入通道数为当前卷积层的输出通道数
# 将所有的卷积和池化层组合成一个Sequential容器
self.features = nn.Sequential(*layers)#nn.Sequential 是一个容器类,用于将多个神经网络层(如卷积层、池化层、激活函数等)按顺序组合起来,形成一个新的模块。
#*layers 是对列表 layers 的解包操作。它将 layers 列表中的每个元素依次作为参数传递给 nn.Sequential,从而将这些层按顺序组合在一起。
#例如,如果 layers 中包含三个层,nn.Sequential(*layers) 就相当于:
#nn.Sequential(layer1, layer2, layer3)
# 全局池化层,输出大小为7x7
self.avgpool = nn.AdaptiveAvgPool2d((7, 7))
# 分类层(全连接层)这个模块由多个层组成
self.classifier = nn.Sequential(
nn.Linear(512 * 7 * 7, 4096), # 输入维度是512x7x7,输出维度是4096
nn.ReLU(True), # ReLU激活
nn.Dropout(), # Dropout层,防止过拟合
nn.Linear(4096, 4096), # 第二个全连接层
nn.ReLU(True), # ReLU激活
nn.Dropout(), # Dropout层
nn.Linear(4096, 1000), # 输出层,1000类分类
)
# 权重初始化部分,采用He初始化方法(适用于ReLU激活),为 ReLU 激活函数设计的权重初始化方法。它的主要目的是帮助训练深度神经网络,避免因梯度消失或梯度爆炸问题导致的训练困难。详细见下面
for m in self.modules():
if isinstance(m, nn.Conv2d): # 如果是卷积层
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu') # 使用He初始化
if m.bias is not None: # 如果卷积层有偏置项
nn.init.constant_(m.bias, 0) # 偏置项初始化为0
elif isinstance(m, nn.BatchNorm2d): # 如果是BatchNorm层
nn.init.constant_(m.weight, 1) # BatchNorm的权重初始化为1
nn.init.constant_(m.bias, 0) # BatchNorm的偏置初始化为0
elif isinstance(m, nn.Linear): # 如果是全连接层
nn.init.normal_(m.weight, 0, 0.01) # 使用正态分布初始化全连接层的权重
nn.init.constant_(m.bias, 0) # 全连接层的偏置初始化为0
# 前向传播方法
def forward(self, x):
x = self.features(x) # 通过卷积层和池化层提取特征
x_fea = x # 保存特征图,用于后续的返回
x = self.avgpool(x) # 通过全局平均池化层,将特征图缩放为7x7
x_avg = x # 保存经过全局池化后的特征图
x = x.view(x.size(0), -1) # 将特征图展平为一维,准备输入到全连接层
x = self.classifier(x) # 通过全连接层进行分类
return x, x_fea, x_avg # 返回分类结果、特征图和经过池化后的特征图
# 提取特征的方法,不经过全连接层
def extractor(self, x):
x = self.features(x) # 只通过卷积层和池化层提取特征
return x
He 初始化(He Initialization)
He 初始化(有时称为 He 均匀初始化 或 He 正态初始化)是一种专门为 ReLU 激活函数设计的权重初始化方法。它的主要目的是帮助训练深度神经网络,避免因梯度消失或梯度爆炸问题导致的训练困难。
问题
在深度神经网络中,权重初始化对于训练的稳定性和速度至关重要。使用不恰当的初始化方式会导致训练过程中的梯度消失或梯度爆炸问题,从而影响网络的性能。
- ReLU 激活函数:ReLU 激活函数的输出为非负数,这使得网络在训练过程中可能存在梯度消失问题,特别是当权重初始化得不合适时。
- 传统初始化方法(如 Xavier 初始化):Xavier 初始化方法是根据激活函数的输出分布来初始化权重的,但它更多地针对 sigmoid 或 tanh 等激活函数。对于 ReLU 激活函数,Xavier 初始化并不总是能提供好的性能。
He 初始化的原理
He 初始化的核心思想是,保证每一层的输入和输出具有相同的方差,以避免激活值变得过大或过小,从而导致梯度消失或梯度爆炸。
-
He 初始化的数学公式:
- 权重矩阵的初始化方法如下:
- 对于 正态分布(高斯分布):
W ∼ N ( 0 , 2 n in ) W \sim \mathcal{N}(0, \frac{2}{n_{\text{in}}}) W∼N(0,nin2) - 对于 均匀分布:
W ∼ U ( − 6 n in , 6 n in ) W \sim U\left(-\sqrt{\frac{6}{n_{\text{in}}}}, \sqrt{\frac{6}{n_{\text{in}}}}\right) W∼U(−nin6,nin6)
- 对于 正态分布(高斯分布):
其中:
- n in n_{\text{in}} nin 是每一层的输入单元数(即该层前一层的神经元个数)。
- 权重矩阵的初始化方法如下:
解释:
- 方差的调整:He 初始化的权重分布的方差为 $ \frac{2}{n_{\text{in}}} $,它考虑了 ReLU 激活函数的特点,能够更好地避免在训练开始时出现梯度消失或梯度爆炸的情况。
- 通过这种初始化,网络的每一层的输出在训练开始时都能保持一个较为合适的范围,从而让网络在训练时更加稳定。
检测头
self.detector = nn.Sequential(
nn.Linear(512*7*7, 4096), # 输入维度:512*7*7,输出维度:4096
nn.ReLU(True), # ReLU 激活函数,
nn.Dropout(), # Dropout 层,用于防止过拟合,用于在训练过程中随机丢弃一部分神经元的输出,默认50%
nn.Linear(4096, 1470), # 输入维度:4096,输出维度:1470
)
train()
def train():
for epoch in range(epochs): # 遍历所有的训练周期(epoch)
ts = time.time() # 记录当前时间,后面用来计算每个epoch的训练时间
for iter, batch in enumerate(train_loader): # 遍历训练集的每一个批次(batch)
optimizer.zero_grad() # 清除之前的梯度,避免累积,每次进行反向传播时,PyTorch 默认会在每次进行反向传播时将计算得到的梯度累加到当前已存在的梯度上。
# 取图片,进行预处理
inputs = input_process(batch) # 对当前批次的图像数据进行处理(例如:归一化、裁剪等操作)
# 取标注,进行处理
labels = target_process(batch) # 对当前批次的标注数据进行处理(例如:边界框坐标等)
# 获取模型的输出
outputs = yolov1_model(inputs) # 将处理后的图像数据输入YOLOv1模型,获取预测结果
# 注释掉的调试代码,用于调试时查看某些输出
# import pdb
# pdb.set_trace()
# 计算损失(损失计算可以根据具体任务不同而不同)
# loss = criterion(outputs, labels) # 使用某种损失函数(如交叉熵)计算损失
loss, lm, glm, clm = lossfunc_details(outputs, labels) # 使用自定义的损失函数,计算总损失以及其他损失的分量(例如:定位损失、类别损失等)
loss.backward() # 反向传播计算梯度
optimizer.step() # 根据梯度更新模型参数
# 每10次迭代打印一次输出,查看当前损失和学习率
if iter % 10 == 0:
# 输出当前迭代的损失值和学习率
print("epoch{}, iter{}, loss: {}, lr: {}".format(
epoch, iter, loss.data.item(), optimizer.state_dict()['param_groups'][0]['lr']
))
# 每完成一个epoch,输出训练时间
print("Finish epoch {}, time elapsed {}".format(epoch, time.time() - ts))
print("*"*30)
# 调用学习率调度器,根据需要更新学习率
scheduler.step() # 更新学习率(如果有的话)
输入处理函数
def input_process(batch):
# import pdb
# pdb.set_trace()
# 计算批次大小,假设batch[0]是一个包含多张图片的列表
batch_size = len(batch[0])
# 创建一个大小为 (batch_size, 3, 448, 448) 的零张量
# 3 是RGB通道,448x448是目标尺寸
input_batch = torch.zeros(batch_size, 3, 448, 448)
# 对每一张图像进行处理
for i in range(batch_size):
# 获取当前批次的第 i 张图片
inputs_tmp = Variable(batch[0][i])
# 使用 OpenCV 的 resize 函数将图像大小调整为 448x448
# permute([1,2,0]) 将原始的 (C, H, W) 维度转为 (H, W, C) 以符合 OpenCV 的格式,1是第二个:H,2是第三个:W,0是第一个:H,
inputs_tmp1 = cv2.resize(inputs_tmp.permute([1,2,0]).numpy(), (448, 448))
# 将调整大小后的 NumPy 数组转换回 PyTorch 张量,并调整维度为 (C, H, W)
inputs_tmp2 = torch.tensor(inputs_tmp1).permute([2,0,1])
# 将处理后的图像放入 input_batch 中对应位置
input_batch[i:i+1,:,:,:] = torch.unsqueeze(inputs_tmp2, 0)
# 返回处理好的输入批次
return input_batch
target_process
将目标框(bounding boxes)数据进行处理,以适应 YOLO 模型的训练格式。具体地,它将目标框信息与网格(grid)坐标结合起来,并填充到一个目标批次 target_batch 中。每个网格单元包含一个关于物体的目标框信息(如是否包含物体、目标框的相对坐标等)。
def target_process(batch, grid_number=7):
# batch[1]表示label
# batch[0]表示image
batch_size = len(batch[0]) # 获取当前批次的大小
target_batch = torch.zeros(batch_size, grid_number, grid_number, 30) # 创建一个全零的目标批次,形状为 (batch_size, 7, 7, 30),7*7个候选区域,30 代表每个网格单元的输出信息的维度:5 个信息表示框[是否有物体, x_center, y_center, width, height],20 个类别信息[类别1的置信度, 类别2的置信度, ..., 类别20的置信度]
for i in range(batch_size):
labels = batch[1] # 获取当前批次的标签(目标框)
batch_labels = labels[i] # 获取当前样本的标签
number_box = len(batch_labels['boxes']) # 获取当前样本中的目标框数量
# 遍历每个网格(grid_number x grid_number),进行坐标分配
for wi in range(grid_number): # 水平网格遍历
for hi in range(grid_number): # 垂直网格遍历
# 遍历每个目标框(bounding box)
for bi in range(number_box):
bbox = batch_labels['boxes'][bi] # 获取当前目标框(bbox)的坐标
_, himg, wimg = batch[0][i].numpy().shape # 获取当前图像的高、宽(himg, wimg)
# 归一化目标框坐标,将其转换为相对坐标,范围在 [0, 1] 之间
bbox = bbox / torch.tensor([wimg, himg, wimg, himg])
# 计算目标框的中心点坐标
center_x = (bbox[0] + bbox[2]) * 0.5
center_y = (bbox[1] + bbox[3]) * 0.5
# 判断目标框的中心点是否落在当前网格单元内
if center_x <= (wi + 1) / grid_number and center_x >= wi / grid_number and center_y <= (hi + 1) / grid_number and center_y >= hi / grid_number:
# 如果目标框的中心点在网格内,则将该目标框的信息放入目标批次中
cbbox = torch.cat([torch.ones(1), bbox]) # 将目标框的坐标与标签(1)一起存储,表示该网格包含一个物体
target_batch[i:i + 1, wi:wi + 1, hi:hi + 1, :] = torch.unsqueeze(cbbox, 0) # 将目标框信息放入对应的网格位置
#else:
# cbbox = torch.cat([torch.zeros(1), bbox]) # 如果不在网格内,可以添加未使用的目标框(注释掉)
return target_batch # 返回处理后的目标批次
YOLO-V2基于Anchor的偏移量
Ground Truth (GT):
Ground Truth 是指图像中真实的物体边界框。
Anchor Boxes:
Anchor boxes 是在目标检测模型中预定义的一组不同尺寸和宽高比的边界框,用于在图像的不同区域上进行候选区域生成。在YOLO等检测模型中,Anchor boxes 是通过聚类方法等技巧从训练集中的物体框获取的。
第一步,把图像分成多个格子,比如77。
第二步,根据已知的真实物体框,使用K-means聚类分成K部分(K是提前设置好的)。得到K个Anchor Boxes边界框的大小。
第三步,在77个格子,每个格子放置Anchor Boxes,坐标也是提前设定的。初始坐标(中心点位置)是通过图像网格的划分来确定的,那么在每个网格单元内,模型会预测K个不同尺寸的 Anchor boxes。对于每个网格单元,模型会回归出 Anchor boxes 的中心点偏移量和宽高偏移量。
第四步,开始偏移。
GT 和 Anchor 的关系:
- 在训练过程中,目标检测模型通常会根据目标物体的
GT
边界框来计算与各个Anchor
框的匹配度。 - 通常使用 IoU (Intersection over Union)(交并比)来度量
Anchor
和GT
的重叠程度。 - IoU 越大,代表该
Anchor
框与真实的GT
边界框越接近。 - 当
Anchor
与GT
的 IoU 超过一定的阈值时,我们认为这个Anchor
框有效,可以用来回归真实的边界框坐标。
YOLO v3-检测头分叉
3个分支分别为32倍下采样,16倍下采样,8倍下采样,分别取预测大,中,小目标。确实,提升检测效果。
YOLO v4
Mish 激活函数
YOLOv4 引入了 Mish 激活函数,替代了传统的 ReLU 和 Leaky ReLU。Mish 函数是一种平滑且非单调的激活函数,表现出比 ReLU 更好的梯度传播能力,能够提高深度神经网络的学习能力和泛化性能。
Mish ( x ) = x ⋅ tanh ( ln ( 1 + e x ) ) \text{Mish}(x) = x \cdot \tanh(\ln(1 + e^x)) Mish(x)=x⋅tanh(ln(1+ex))
- 通过这种激活函数,YOLOv4 能够进一步改善训练时的收敛速度和精度。
使用了 YOLOv4 的 “Self-ensembling” 技术
YOLOv4 采用了自我集成(self-ensembling)技术,这是一种集成学习方法。它通过在训练过程中增加 DropBlock 和数据增强等技巧,类似于集成多个模型来获得更好的泛化能力。
- DropBlock 是一种改进版的 Dropout,用于提高网络的鲁棒性。它在训练时随机丢弃区域块而不是单个神经元,迫使网络学习更加广泛的特征。
- 数据增强 技术进一步提升了 YOLOv4 在各种场景中的鲁棒性。YOLOv4 使用了 Mosaic 数据增强,该方法将四张图像拼接成一张图像,以更好地适应各种规模的目标。
CIoU (Complete Intersection over Union) 损失函数
YOLOv4 引入了 CIoU 损失函数,作为边界框回归的优化目标。CIoU 是一种对传统的 IoU(Intersection over Union)进行改进的度量方法,考虑了边界框的相对位置、长宽比以及角度的偏差。
- CIoU 损失函数能够加速训练收敛,并提高模型的定位精度。
- 相比于标准的 IoU 损失,CIoU 考虑了更多的几何信息,使得网络能够更好地优化边界框的预测。
加权交叉熵损失(Weighted Cross-Entropy Loss)
YOLOv4 在目标分类损失计算中采用了 加权交叉熵损失,用以处理类别不均衡的问题。在许多检测任务中,正负样本之间的比例通常非常失衡,YOLOv4 通过加权交叉熵来缓解这一问题,从而提高了小物体和难以检测的物体的检测精度。
使用了 Soft-NMS(Soft Non-Maximum Suppression)
YOLOv4 改进了 Non-Maximum Suppression (NMS) 算法,使用了 Soft-NMS,这对于提高多目标检测中的性能非常有帮助。
- Soft-NMS 在筛选候选框时不仅仅是选择最大得分的框,还考虑了相似度得分,从而降低了过多的目标框被丢弃的概率。
- 通过 Soft-NMS,YOLOv4 能够更好地处理多物体重叠的情况,尤其是在拥挤场景下,检测的效果更为显著。
YOLO v5
自适应anchor
自适应anchor利用网络的学习功能,让坐标也是可以学习的。
Transformer
是一种广泛应用于自然语言处理(NLP)和其他序列数据任务的深度学习模型架构,由 Vaswani 等人在 2017 年提出。Transformer 的关键创新点是完全依赖 自注意力机制(Self-Attention)来建模序列中的长期依赖关系,避免了传统循环神经网络(RNN)和卷积神经网络(CNN)在处理长序列时的效率问题。Transformer 在机器翻译、文本生成、语言建模等任务中取得了显著的成果,并为后续的许多模型(如 BERT、GPT)提供了基础。
Transformer 的主要结构
Transformer 的整体架构由 编码器(Encoder) 和 解码器(Decoder) 两部分组成,其中编码器用于处理输入序列,解码器用于生成输出序列。每个编码器和解码器的内部结构由若干相同的层(layer)组成。
1. 编码器(Encoder)
编码器接收输入序列并生成表示该序列的上下文信息。每个编码器层包括以下几个组件:
- 多头自注意力(Multi-Head Self Attention):对输入的每个位置进行自注意力计算,捕捉词之间的依赖关系。
- 前馈神经网络(Feed-Forward Neural Network):对每个位置的表示进行进一步的处理。
- 层归一化(Layer Normalization):对每一层的输出进行归一化处理,增强训练稳定性。
- 残差连接(Residual Connections):避免梯度消失问题,使得网络更易训练。
解码器(Decoder)
解码器用于根据编码器的输出生成目标序列。每个解码器层包括:
- 多头自注意力(Masked Multi-Head Self Attention):和编码器中的自注意力类似,但在计算时会对未来的词进行遮蔽,确保每个位置只能依赖于当前及之前的位置。
- 编码器-解码器注意力(Encoder-Decoder Attention):解码器层中的注意力计算不仅依赖于解码器的输入,还会考虑编码器的输出,从而确保目标序列和源序列的关系被捕捉。
- 前馈神经网络(Feed-Forward Neural Network):和编码器中的相似,用于进一步处理信息。
Transformer 的关键机制
自注意力(Self-Attention)
自注意力是 Transformer 的核心机制,它通过对每个输入位置与其他所有位置之间的关系进行建模,计算出每个位置的加权表示。自注意力的过程如下:
- 将输入序列的每个词(或位置)映射到三个向量:查询(Query)、键(Key)、值(Value)。
- 计算查询与所有键的相似度(通常是点积),得到一个权重分布。
- 使用这些权重加权求和值向量,得到每个位置的输出表示。
自注意力公式:
Attention
(
Q
,
K
,
V
)
=
softmax
(
Q
K
T
d
k
)
V
\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right) V
Attention(Q,K,V)=softmax(dkQKT)V
其中:
- Q Q Q、 K K K、 V V V 分别是查询、键和值矩阵。
- d k d_k dk 是键向量的维度。开根号用来防止梯度爆炸和梯度消失。
多头注意力(Multi-Head Attention)
为了让模型能够在多个子空间中捕捉不同的关系,Transformer 使用了多头注意力机制。它将查询、键、值矩阵分别拆分成多个头(head),每个头独立进行注意力计算,然后将各个头的输出拼接起来,最后经过线性变换得到最终结果。
位置编码(Positional Encoding)
由于 Transformer 没有像 RNN 那样的序列顺序处理机制,因此需要通过位置编码来注入输入序列的位置信息。位置编码通常是通过正弦和余弦函数生成的,能够让模型感知到词语在序列中的相对位置。
Transformer 优势
- 并行化:传统的 RNN 和 LSTM 在处理长序列时需要逐步计算,难以并行化。而 Transformer 的自注意力机制允许对所有位置的计算并行进行,显著提高了训练效率。
- 长距离依赖建模:自注意力机制能够捕捉序列中任意两位置之间的关系,使得模型能处理长距离依赖问题,这在传统的 RNN 中是很困难的。
- 灵活性:Transformer 通过堆叠多个编码器和解码器层,能够处理复杂的任务和模型。