【深度学习】YOLO源码中的mAP计算代码的理解笔记(大部分代码逐行+基础解释)

提示:本篇博客是在阅读了YOLO源码中的mAP计算方法的代码后加上官方解释以及自己的debug调试理解每一步是怎么操作的。由于是大部分代码进行了逐行解释,所以篇幅过长。

文章目录

  • 前言
  • 一、输入格式处理
  • 1.1 转换公式
  • 二、init:初始化
    • 2.1 iouv
    • 2.2 stats
  • 三、process_batch:实现预测结果和真实结果的匹配(TP/FP统计)
    • 3.1 输入参数的格式
    • 3.2 代码注释(逐行)
  • 四、calculate_ap_per_class: 计算每一类别的AP值
    • 4.1 代码注释(逐行)
  • 五、compute_ap:计算PR曲线的面积
  • 六、源码
  • 结束语


前言

首先,在理解YOLO源码中的mAP计算过程大部分参考了这篇文章:【目标检测】评价指标:mAP概念及其代码实现(yolo源码/pycocotools),这篇文章提到了mAP计算的一些基本知识,也提供了代码,这里也是参考的这篇文章里的yolo源码的mAP计算代码(这篇文章是根据YOLO源码中的整理过后的代码)。想要源码可以点链接进去或者本篇最后部分贴了源码。

一、输入格式处理

输入的格式要求如下图:
在这里插入图片描述
在进行mAP计算之前需要将YOLO模型预测文件的数据格式转换为绝对位置,并且需要进行相应位置的转换:
YOLO预测文件中:[class, x c e n t e r x_{center} xcenter, y c e n t e r y_{center} ycenter, width, height, conf] → \to [ x m i n x_{min} xmin, y m i n y_{min} ymin, x m a x x_{max} xmax, y m a x y_{max} ymax, conf, class]
其中的 x c e n t e r x_{center} xcenter, y c e n t e r y_{center} ycenter, width, height为相对位置,都是归一化处理后的百分比位置,( x m i n x_{min} xmin, y m i n y_{min} ymin),( x m a x x_{max} xmax, y m a x y_{max} ymax)则是经过还原后在原图上标注框的左上角和右下角在原图中的坐标
真实结果同理进行转换:我们在标注label的时候保存格式也是经过归一化处理后的数据,在计算mAP的时候也需要还原到原图的坐标上去。

1.1 转换公式

此处的转换原理和公式参考这篇文章所提到的标签格式转换:利用mAP计算yolo精确度,同时这篇文章也有格式转换的代码,大家可以参考,此处就不再贴格式转换代码,只简述原理以及公式。
格式转换的原理如下图:
在这里插入图片描述
根据原理可以得到原始公式:
x c e n t e r = x m i n + x m a x 2 , y c e n t e r = y m i n + y m a x 2 , w i d t h = x m a x − x m i n , h e i g h t = y m a x − y m i n x_{center}= \frac{x_{min}+x_{max}}{2}, \ y_{center}= \frac{y_{min}+y_{max}}{2} ,\\ width = x_{max} - x_{min},\ height = y_{max} - y_{min} xcenter=2xmin+xmax, ycenter=2ymin+ymax,width=xmaxxmin, height=ymaxymin
联立上面四个公式可以得到:
x m i n = x c e n t e r − w i d t h 2 , x m a x = x c e n t e r + w i d t h 2 y m i n = y c e n t e r − h e i g h t 2 , y m a x = y c e n t e r + h e i g h t 2 x_{min}= x_{center} - \frac{width}{2}, \ x_{max}= x_{center} + \frac{width}{2} \\ y_{min}= y_{center}-\frac{height}{2}, \ y_{max}= y_{center} + \frac{height}{2} xmin=xcenter2width, xmax=xcenter+2widthymin=ycenter2height, ymax=ycenter+2height
此时得到的( x m i n x_{min} xmin, y m i n y_{min} ymin),( x m a x x_{max} xmax, y m a x y_{max} ymax)是归一化后的坐标,还需要进行原图的还原:其中W、H为原图的宽度和高度。ps:此处的W、H和width、height不是同一个东西,width和height是经过归一化处理后的值,而W和H是原图的宽高
x m i n = x m i n ∗ W , x m a x = x m a x ∗ W y m i n = y m i n ∗ H , y m a x = y m a x ∗ H x_{min}= x_{min} \ast W , \ x_{max}= x_{max} \ast W \\ y_{min}= y_{min} \ast H,\ y_{max}= y_{max} \ast H xmin=xminW, xmax=xmaxWymin=yminH, ymax=ymaxH


二、init:初始化

在这里插入图片描述

2.1 iouv

iouv就是从0.5到0.95分为10个值的数组

2.2 stats

stats是一个列表,包含4个numpy数组
stats[0] → \to shape:[ n p r e d n_{pred} npred,10] → \to 所有预测框载10个IOU阈值上是TP还是FP,其中 n p r e d n_{pred} npred表述所有预测框的总数量
stats[1] → \to shape:[ n p r e d n_{pred} npred] → \to 所有预测框的置信度
stats[2] → \to shape:[ n p r e d n_{pred} npred] → \to 所有预测框的预测类别
stats[3] → \to shape:[ n l a b e l n_{label} nlabel] → \to 所有真实框(即label标签的)的预测类别,其中 n l a b e l n_{label} nlabel表述所有真实框的总数

三、process_batch:实现预测结果和真实结果的匹配(TP/FP统计)

3.1 输入参数的格式

在一部分已经阐述了格式的转换,此处就不再进行赘述。
其中,N是预测框的总数,M是真实框的总数
在这里插入图片描述

3.2 代码注释(逐行)

# 每一个预测结果在不同IoU下的预测结果匹配
correct = np.zeros((detections.shape[0], self.niou)).astype(bool)

初始化correct(bool形式) → \to shape:[N, 10] → \to 在10个IOU阈值上每个预测框的TP、FP情况,True表示TP,False表示FP。
zeros即初始化全为0 → \to False

此处穿插python知识:使用shape可以快速读取矩阵的形状

# 二维矩阵
shape[0] # 读取矩阵第一维度的长度 即数组的行数
shape[1] # 读取矩阵第二维度的长度 即数组的列数# 图像
image.shape[0] # 图片的高
image.shape[1] # 图片的宽
image.shape[2] # 图片的通道数# 一般来说,在二维张量里,shape[-1]表示列数,
#注意,即使是一维行向量,shape[-1]表示行向量的元素总数,换言之也是列数:
shape[-1] # 表示最后一个维度

if detections is None:self.stats.append((correct, *torch.zeros((2, 0), device=self.device), labels[:, 0]))

注意一下:本地调试的时候,这里直接写None会报错
后面本人使用以下解决:当没有产生预测文件的时候:使用tensor生成0行6列的向量。

 else: # 没有预测框生成detections = torch.zeros((0, 6))

然后将上面从头开始的两句(correct的初始化以及判空的语句)修改为下面这个即可。

nl, npr = labels.shape[0], detections.shape[0]
correct = torch.zeros(npr, self.niou, dtype=torch.bool, device="cpu")if npr == 0:if nl:self.stats.append((correct, *torch.zeros((2, 0), device=self.device), labels[:, 0]))

else:# 计算标签与所有预测结果之间的IoUiou = box_iou(labels[:, 1:], detections[:, :4])

iou → \to shape:[M,N],以labels真实框为行,detections预测框为列 → \to 计算每个真实框与每个预测框之间的交并比IOU


   # 计算每一个预测结果可能对应的实际标签correct_class = labels[:, 0:1] == detections[:, 5]

correct_class → \to shape:[M,N],以labels真实框为行,detections预测框为列 → \to 保存每个真实框与每个预测框之间的类别是否相等,True即为类别一致,False即为类别不一致。
其中,labels[:, 0:1] == detections[:, 5]返回值为bool类型,True or False

例子:比如预测框预测的三个框的类别依次为2,0,1(括号里面表示类别,括号前面表示下标),真实框只有两个框,并且类别依次为0,1。得到的correct_class矩阵如下所示

label\detection0(2)1(0)2(1)
0(1)FFT
1(2)TFF

 for i in range(self.niou):  # 在不同IoU置信度下的预测结果匹配结果# 根据IoU置信度和类别对应得到预测结果与实际标签的对应关系x = torch.where((iou >= self.iouv[i]) & correct_class)

外层的循环是指不同的IOU阈值下的计算
使用torch.where(此处采用的是后面所提的用法2:取Tensor中符合条件的坐标)获得真实框和预测框之间的iou大于此时的IOU阈值并且类别一致的结果 → \to shape:[2, N s a m e N_{same} Nsame] → \to N s a m e N_{same} Nsame指的是一致的组总数量,第一行表示行坐标,第二行表示列坐标。

例子:假设前面获得的iou和correct_class如下,此时的iou阈值为0.5,(0,2)(1,0)这两组符合条件,成为返回的结果。故torch.where返回值为[ [0,1] [2,0] ]。(第一行表示真实框索引,第二行表示预测框索引)

iou:

label\detection0(2)1(0)2(1)
0(1)0.60.70.8
1(2)0.90.20.1

correct_class:

label\detection0(2)1(0)2(1)
0(1)FFT
1(2)TFF

此处穿插python知识:torch.where用法
参考这篇文章:torch.where()的两种用法

用法1:按照指定条件合并两个同维度Tensor:
函数原型:torch.where(condition, x, y) → \to Tensor
在这里插入图片描述

用法2:取Tensor中符合条件的坐标:
函数原型:torch.where(condition) → \to Tensor
在这里插入图片描述


# 若存在和实际标签相匹配的预测结果
# x[0]:存在为True的索引(实际结果索引), x[1]当前所有True的索引(预测结果索引)
if x[0].shape[0]:  # [label, detect, iou]matches = torch.cat((torch.stack(x, 1), iou[x[0], x[1]][:, None]), 1).cpu().numpy()

通俗讲,x[0]指的是x的第一行,即对应真实框的索引号。x[1]指的是x的第二行,即对应的预测框的索引号。如果x[0]存在,就进行一个[label, detect, iou]的拼接。

torch.stack(x, 1) → \to x在列上进行拼接,从[ [0,1] [2,0] ] → \to [ [0,2] [1,0] ]。相当于就是变回一组坐标的形式。
iou[x[0], x[1]][:, None] → \to 将iou矩阵中的对应于x中的索引的iou取出来,以[[0.8],[0.9]]的形式。
torch.cat((torch.stack(x, 1), iou[x[0], x[1]][:, None]), 1) → \to 将前面所得到的两个东西在列上进行拼接 → \to 得到[ [0,2,0.8], [1,0,0.9] ] (形成[label, detect, iou])

此处穿插python知识:torch.stack用法
沿一个新维度对输入一系列张量进行连接,序列中所有张量应为相同形状,stack 函数返回的结果会新增一个维度。

# dim = 0 : 在第0维进行连接,相当于在行上进行组合(输入张量为一维,输出张量为两维)
a = torch.tensor([1, 2, 3])
b = torch.tensor([11, 22, 33])
c = torch.stack([a, b],dim=0)
#c => tensor([[ 1,  2,  3], [11, 22, 33]])# dim=1:在第1维进行连接,相当于在对应行上面对列元素进行组合(输入张量为一维,输出张量为两维)
a = torch.tensor([1, 2, 3])
b = torch.tensor([11, 22, 33])
c = torch.stack([a, b],dim=1)
#c => tensor([[ 1, 11], [ 2, 22], [ 3, 33]])

此处穿插python知识:torch.cat用法
用于在指定的维度上拼接张量。这个函数接收一个张量列表,并在指定的维度上将它们连接起来。它通常用于连接两个或多个张量,以创建一个更大的张量。

tensor1 = torch.tensor([[1, 2], [3, 4]])
tensor2 = torch.tensor([[5, 6], [7, 8]])
# 在第0维(行)上连接这两个张量
result = torch.cat((tensor1, tensor2), dim=0)
# result => tensor([[1, 2], [3, 4], [5, 6], [7, 8]])
# 新的张量,行数=tensor1和tensor2 的行数之和,列数与 tensor1 和 tensor2 的列数相同。

   if x[0].shape[0] > 1:  # 存在多个与目标对应的预测结果# 根据IoU从高到低排序 [实际结果索引,预测结果索引,结果IoU]matches = matches[matches[:, 2].argsort()[::-1]] 

matches[:, 2].argsort()[::-1] → \to -1指的是逆序排序,即从IOU高到低排序,2是指matches[2]即iou值,整体返回的是逆序排位后的索引,如[1,0] (因为0.9>0.8,所以第二行应该和第一行交换位置,所以得到的排序后的索引为[1,0])

再使用matches = matches[排序后的索引] 进行替换重置。


 # 每一个预测结果保留一个和实际结果的对应
matches = matches[np.unique(matches[:, 1], return_index=True)[1]] 
# 每一个实际结果和一个预测结果对应
matches = matches[np.unique(matches[:, 0], return_index=True)[1]]

np.unique(matches[:, 0], return_index=True)[0] => 去除重复后的值 [1] => 去除重复后的每个值对应的索引 [2] => dtype
然后matches再根据获得去除重复后的索引进行排列。
ps:因为前面已经按照IOU从高到低进行排序了,故当去除的时候自动保留最高的IOU的那一组

此处穿插python知识:numpy.unique用法
此处参考这篇文章:【Python】np.unique() 介绍与使用
去除其中重复的元素 ,并按元素由小到大返回一个新的无元素重复的元组或者列表。

# 格式:numpy.unique(arr, return_index, return_inverse, return_counts)
# arr:输入数组,如果不是一维数组则会展开
# return_index:如果为 true,返回新列表元素在旧列表中的位置(下标),并以列表形式存储。
# return_inverse:如果为true,返回旧列表元素在新列表中的位置(下标),并以列表形式存储。
# return_counts:如果为 true,返回去重数组中的元素在原数组中的出现次数。
A = [1, 2, 2, 5, 3, 4, 3]
a = np.unique(A) # [1 2 3 4 5]
a, indices = np.unique(A, return_index=True)# 返回新列表元素在旧列表中的位置(下标)
# a => [1 2 3 4 5] indices => [0 1 4 5 3]
a, indices = np.unique(A, return_inverse=True)# 旧列表的元素在新列表的位置
# a => [1 2 3 4 5] indices => [0 1 1 4 2 3 2]
a, indices = np.unique(A, return_counts=True)# 每个元素在旧列表里各自出现了几次
# a => [1 2 3 4 5] indices => [1 2 2 1 1]

 # 表明当前预测结果在当前IoU下实现了目标的预测correct[matches[:, 1].astype(int), i] = True  

matches[:, 1].astype(int) → \to 将matches的1列(第二列,即预测框的索引)作为int类型
matches表明经过筛选后留下的预测框的索引,然后在correct矩阵中将其置为True,即表明在该阈值(i)下,此预测框为正确的预测(TP)。


   # 预测结果在不同IoU是否预测正确, 预测置信度, 预测类别, 实际类别self.stats.append((correct, detections[:, 4], detections[:, 5], labels[:, 0]))

当所有iou阈值循环完成后,stats添加筛选过后的四个Numpy数组:correct、conf、class、label_class。


一个小总结:

数组名shape意义
correct[ N p r e d N_{pred} Npred, 10] N p r e d N_{pred} Npred表示所有预测框的数量,表示所有预测框在该IOU阈值下为TP还是FP
correct_class[ N l a b e l N_{label} Nlabel, N p r e d N_{pred} Npred]表明每组真实框与预测框之间的类别是否一致
x[2,X]经过类别和阈值筛选后剩下的X组数据的索引号,第一行表示行坐标,第二行表示列坐标
matches[Y,3]去除重复后剩下的Y组数据,[label, detection, iou]形式,表明预测框和真实框匹配的框的数据
stats每一维都有四个数组分别为correct、conf、class、label_class

四、calculate_ap_per_class: 计算每一类别的AP值

4.1 代码注释(逐行)

stats = [torch.cat(x, 0).cpu().numpy() for x in zip(*self.stats)]  # to numpy
# tp:所有预测结果在不同IoU下的预测结果 [n, 10]
# conf: 所有预测结果的置信度
# pred_cls: 所有预测结果得到的类别
# target_cls: 所有图片上的实际类别
tp, conf, pred_cls, target_cls = stats[0], stats[1], stats[2], stats[3]
# 根据类别置信度从大到小排序
i = np.argsort(-conf)  # 根据置信度从大到小排序
tp, conf, pred_cls = tp[i], conf[i], pred_cls[i]

第一行代码是将所有图片的四个数组进行汇总,在YOLO源码中,未拼接前 → \to stats[0] 指第一张图的四个数组,拼接后 → \to stats[0]指所有图片的所有预测框的TP/FP情况。

然后再根据置信度逆序排序,即按照置信度从高到低进行排序。


# 得到所有类别及其对应数量(目标类别数)
unique_classes, nt = np.unique(target_cls, return_counts=True)
nc = unique_classes.shape[0]  # number of classes
# ap: 每一个类别在不同IoU置信度下的AP,shape[nc类别数, 10], 
# p:每一个类别的P曲线(不同类别置信度), r:每一个类别的R(不同类别置信度)
ap, p, r = np.zeros((nc, tp.shape[1])), np.zeros((nc, 1000)), np.zeros((nc, 1000))

np.unique用法见前面,此处不再赘述。


for ci, c in enumerate(unique_classes):  # 对每一个类别进行P,R计算,ci为c的index,c为值i = pred_cls == cn_l = nt[ci]  # number of labels 该类别的实际数量(正样本数量)n_p = i.sum()  # number of predictions 预测结果数量if n_p == 0 or n_l == 0:continue

i = pred_cls == c返回的是bool类型的一维数组。
nt指的是每个类别真实框的总数量,一维数组。


# cumsum:轴向的累加和, 计算当前类别在不同的类别置信度下的P,R
fpc = (1 - tp[i]).cumsum(0)  # FP累加和(预测为负样本且实际为负样本)
tpc = tp[i].cumsum(0)  # TP累加和(预测为正样本且实际为正样本)
# 召回率计算(不同的类别置信度下)
recall = tpc / (n_l + eps)
# 精确率计算(不同的类别置信度下)
precision = tpc / (tpc + fpc)

TP和FP的计算方式开头所提到的参考文章有非常清楚的解释,此处不再赘述。
此处说一下tp[i]的用法见下图,前面获得了i这个数组,记录了在tp数组中哪些预测框是当前类别的预测框。
tp[i]会保留对应i为True的数据。
在这里插入图片描述

cumsum()的用法见图

import numpy as np
a = np.asarray([[1, 2, 3],[4, 5, 6],[7, 8, 9]])
b = a.cumsum(axis=0) # 按行累加
# b=>[[1 2 3] [5 7 9] [12 15 18]]
c = a.cumsum(axis=1) # 按列累加
# c=>[[1 3 6] [4 9 15] [7 15 24]]

            # 计算不同类别置信度下的AP(根据P-R曲线计算)for j in range(tp.shape[1]):ap[ci, j], mpre, mrec = self.compute_ap(recall[:, j], precision[:, j])# 所有类别的ap值 @0.5:0.95return ap

最后得到recall和precision值后就是计算ap值。

五、compute_ap:计算PR曲线的面积

此处就是计算每一组PR值形成的曲线的面积,代码本来的注解就很清晰明了,此处就不逐行解释了。
此处贴一个插值积分求面积的博客,里面比较详细介绍了插值积分:numpy.interp()用法

六、源码

在这里插入图片描述

class MeanAveragePrecison:def __init__(self, device="cpu"):'''计算mAP: mAP@0.5; mAP @0.5:0.95; mAP @0.75'''self.iouv = torch.linspace(0.5, 0.95, 10, device=device)  # 不同的IoU置信度 @0.5:0.95self.niou = self.iouv.numel()  # IoU置信度数量self.stats = []  # 存储预测结果self.device = devicedef process_batch(self, detections, labels):'''预测结果匹配(TP/FP统计):param detections:(array[N,6]) x1,y1,x1,y1,conf,class (原图绝对坐标):param labels:(array[M,5]) class,x1,y1,x2,y2 (原图绝对坐标)'''# 每一个预测结果在不同IoU下的预测结果匹配correct = np.zeros((detections.shape[0], self.niou)).astype(bool)if detections is None:self.stats.append((correct, *torch.zeros((2, 0), device=self.device), labels[:, 0]))else:# 计算标签与所有预测结果之间的IoUiou = box_iou(labels[:, 1:], detections[:, :4])# 计算每一个预测结果可能对应的实际标签correct_class = labels[:, 0:1] == detections[:, 5]for i in range(self.niou):  # 在不同IoU置信度下的预测结果匹配结果# 根据IoU置信度和类别对应得到预测结果与实际标签的对应关系x = torch.where((iou >= self.iouv[i]) & correct_class)# 若存在和实际标签相匹配的预测结果if x[0].shape[0]:  # x[0]:存在为True的索引(实际结果索引), x[1]当前所有True的索引(预测结果索引)# [label, detect, iou]matches = torch.cat((torch.stack(x, 1), iou[x[0], x[1]][:, None]), 1).cpu().numpy()if x[0].shape[0] > 1:  # 存在多个与目标对应的预测结果matches = matches[matches[:, 2].argsort()[::-1]]  # 根据IoU从高到低排序 [实际结果索引,预测结果索引,结果IoU]matches = matches[np.unique(matches[:, 1], return_index=True)[1]]  # 每一个预测结果保留一个和实际结果的对应matches = matches[np.unique(matches[:, 0], return_index=True)[1]]  # 每一个实际结果和一个预测结果对应correct[matches[:, 1].astype(int), i] = True  # 表面当前预测结果在当前IoU下实现了目标的预测# 预测结果在不同IoU是否预测正确, 预测置信度, 预测类别, 实际类别self.stats.append((correct, detections[:, 4], detections[:, 5], labels[:, 0]))def calculate_ap_per_class(self, save_dir='.', names=(), eps=1e-16):stats = [torch.cat(x, 0).cpu().numpy() for x in zip(*self.stats)]  # to numpy# tp:所有预测结果在不同IoU下的预测结果 [n, 10]# conf: 所有预测结果的置信度# pred_cls: 所有预测结果得到的类别# target_cls: 所有图片上的实际类别tp, conf, pred_cls, target_cls = stats[0], stats[1], stats[2], stats[3]# 根据类别置信度从大到小排序i = np.argsort(-conf)  # 根据置信度从大到小排序tp, conf, pred_cls = tp[i], conf[i], pred_cls[i]# 得到所有类别及其对应数量(目标类别数)unique_classes, nt = np.unique(target_cls, return_counts=True)nc = unique_classes.shape[0]  # number of classes# ap: 每一个类别在不同IoU置信度下的AP, p:每一个类别的P曲线(不同类别置信度), r:每一个类别的R(不同类别置信度)ap, p, r = np.zeros((nc, tp.shape[1])), np.zeros((nc, 1000)), np.zeros((nc, 1000))for ci, c in enumerate(unique_classes):  # 对每一个类别进行P,R计算i = pred_cls == cn_l = nt[ci]  # number of labels 该类别的实际数量(正样本数量)n_p = i.sum()  # number of predictions 预测结果数量if n_p == 0 or n_l == 0:continue# cumsum:轴向的累加和, 计算当前类别在不同的类别置信度下的P,Rfpc = (1 - tp[i]).cumsum(0)  # FP累加和(预测为负样本且实际为负样本)tpc = tp[i].cumsum(0)  # TP累加和(预测为正样本且实际为正样本)# 召回率计算(不同的类别置信度下)recall = tpc / (n_l + eps)# 精确率计算(不同的类别置信度下)precision = tpc / (tpc + fpc)# 计算不同类别置信度下的AP(根据P-R曲线计算)for j in range(tp.shape[1]):ap[ci, j], mpre, mrec = self.compute_ap(recall[:, j], precision[:, j])# 所有类别的ap值 @0.5:0.95return apdef compute_ap(self, recall, precision):# 增加初始值(P=1.0 R=0.0) 和 末尾值(P=0.0, R=1.0)mrec = np.concatenate(([0.0], recall, [1.0]))mpre = np.concatenate(([1.0], precision, [0.0]))# Compute the precision envelope np.maximun.accumulate# (返回一个数组,该数组中每个元素都是该位置及之前的元素的最大值)mpre = np.flip(np.maximum.accumulate(np.flip(mpre)))# 计算P-R曲线面积method = 'interp'  # methods: 'continuous', 'interp'if method == 'interp':  # 插值积分求面积x = np.linspace(0, 1, 101)  # 101-point interp (COCO))# 积分(求曲线面积)ap = np.trapz(np.interp(x, mrec, mpre), x)elif method == 'continuous':  # 不插值直接求矩阵面积i = np.where(mrec[1:] != mrec[:-1])[0]  # points where x axis (recall) changesap = np.sum((mrec[i + 1] - mrec[i]) * mpre[i + 1])  # area under curvereturn ap, mpre, mrec

结束语

浅浅记录这几天阅读YOLO源码中的mAP计算过程,自身python基础不太ok,也算一边查阅python语法知识一边debug看懂每一步中数值的变化。可能以上自身的理解存在一点偏差,如果存在问题欢迎大家在评论区指出~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/10427.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Leetcode每日一题】 综合练习 - 电话号码的字母组合(难度⭐⭐)(75)

1. 题目解析 题目链接:电话号码的字母组合 这个问题的理解其实相当简单,只需看一下示例,基本就能明白其含义了。 2.算法原理 算法设计思路 在解决这类问题时,我们需要认识到每个位置上的数字对应的字符集合是相互独立的&#…

什么是翘尾因素

在有关CPI 的分析文章和新闻稿件中,经常会出现“翘尾因素”或“翘尾影响” 等词汇,这是分析同比价格指数变动幅度时所特有的概念。那么什么是“翘尾因素” 或“翘尾影响”呢? 一、什么是翘尾因素 “翘尾因素”是指上年价格上涨(…

使用scrollIntoView滚动元素到可视区域

1. 实现效果 点击顶部标签栏,让对应的内容出现在可视区域: 2. scrollIntoView () scrollIntoView 是一个内置的 JavaScript 方法,用于将元素滚动到视口可见的位置。它通常用于用户界面中,以便用户能轻松看到特定的元素。此方…

perf 中的 cpu-cycles event 介绍

perf 中的 cpu-cycles event 介绍 cycles简介 cycles事件记录处理器核心执行的时钟周期数。每个时钟周期代表处理器内部时钟振荡器的一个周期。这个事件通常用于衡量处理器的执行速度,因为它直接反映了指令执行所需的时间。一个较高的cycles计数可能意味着代码执行…

2024版本idea集成SpringBoot + Ai 手写一个chatgpt 【推荐】

题目:SpringBoot OpenAi 在这里获取key和url:获取免费key base-url为这两个: 话不多说直接来! 一、简介 Spring AI 是 AI 工程的应用框架。其目标是将 Spring 生态系统设计原则(如可移植性和模块化设计&#xff…

暗区突围pc资格 暗区突围pc端测试资格获取

《暗区突围》的诞生,仿佛在游戏界投下了一枚深水炸弹,它不仅仅是射击游戏的新标杆,更是对玩家策略思维、生存直觉与团队协作能力的一次全面考验。在这个精心构建的虚拟战场中,每一次踏入暗区,都是对未知的探索&#xf…

【练习4】

1.两数之和 暴力&#xff1a; class Solution { public:vector<int> twoSum(vector<int>& nums, int target) {int n nums.size();vector<int> res(2, -1); // 初始化结果为-1for (int i 0; i < n; i) {int temp nums[i];for (int j i 1; j <…

做视频号小店,怎么找达人合作?这里有详细讲解

大家好&#xff0c;我是电商笨笨熊 做视频号小店是没有自然流量的&#xff0c;这点刚入驻的新玩家还不清楚&#xff1b; 因此很多老电商玩家们还想着继续拿其他平台动销自然流的玩法去做视频号&#xff1b; 只能说这种方式在视频号是完全行不通的&#xff0c;当下想要推广售…

设计模式2——原则篇:依赖倒转原则、单一职责原则、合成|聚合复用原则、开放-封闭原则、迪米特法则、里氏代换原则

设计模式2——设计原则篇 目录 一、依赖倒转原则 二、单一职责原则&#xff08;SRP&#xff09; 三、合成|聚合复用原则&#xff08;CARP&#xff09; 四、开放-封闭原则 五、迪米特法则&#xff08;LoD&#xff09; 六、里氏代换原则 七、接口隔离原则 八、总结 一、依赖…

Python-VBA函数之旅-setattr函数

目录 一、setattr函数的常见应用场景 二、setattr函数使用注意事项 三、如何用好setattr函数&#xff1f; 1、setattr函数&#xff1a; 1-1、Python&#xff1a; 1-2、VBA&#xff1a; 2、推荐阅读&#xff1a; 个人主页&#xff1a; https://blog.csdn.net/ygb_1024?…

宏集Panorama SCADA软件获BACnet BTL认证

Panorama 获得BACnet BTL认证 建筑物的组件&#xff08;空调系统、照明传感器等&#xff09;能否使用共同通讯协议&#xff1f;这正是标准化 BACnet协议&#xff08;Building Automation and Control Networks&#xff09;所提供的功能。该协议旨在实现建筑物中各种设备和系统…

【TS】入门

创建项目 vscode自动编译ts 生成配置文件 tsc --init 然后发现终端也改变了&#xff1a;

SOCKET编程(3):相关结构体与函数

相关结构体与函数 sockaddr、sockaddr_in结构体 sockaddr和sockaddr_in详解 struct sockaddr共16字节&#xff0c;协议族(family)占2字节&#xff0c;IP地址和端口号在sa_data字符数组中 /* Structure describing a generic socket address. */ struct sockaddr {__SOCKADDR…

抓大鹅教程电脑端秒通关……

大家好&#xff0c;我是小黄。 最近抓大鹅小程序游戏很火&#xff0c;抓大鹅小游戏是由青岛蓝飞互娱科技股份有限公司开发并推出的一款休闲益智类三消游戏。在游戏中&#xff0c;玩家需要在特定的“购物篮子”背景下&#xff0c;找到三个相同的物品并将其消除。游戏的玩法简单…

社工库信息查询

此网站需要注册账号&#xff0c;新用户注册送3点券&#xff0c;每日签到可获得1.5点券。也可通过充值来查 我这里有方法可以利用缺陷来无限获取点券查人

程序员的归宿。。

大家好&#xff0c;我是瑶琴呀。 相信每个进入职场的人都考虑过自己的职业生涯规划&#xff0c;在不同的年龄段可能面临不同挑战&#xff0c;这点对于 35 的人应该更为感同身受。 对于程序员来说&#xff0c;大部分人的职业道路主要是下面三种&#xff1a;第一条&#xff0c;…

【Delphi 爬虫库 6】使用正则表达式提取猫眼电影排行榜top100

正则表达式库的简单介绍 正则表达式易于使用&#xff0c;功能强大&#xff0c;可用于复杂的搜索和替换以及基于模板的文本检查。这对于输入形式的用户输入验证特别有用-验证电子邮件地址等。您还可以从网页或文档中提取电话号码&#xff0c;邮政编码等&#xff0c;在日志文件中…

人生是旷野,不是轨道

最近看到一句话&#xff0c;很喜欢&#xff0c;分享一下。"人生是旷野&#xff0c;不是轨道"。人生不是固定的方程式&#xff0c;也没有唯一答案&#xff0c;没有谁生来就应该是什么样。别太被太多世俗观念束缚住手脚&#xff0c;每个人都有权利自由生长&#xff0c;…

用友畅捷通T+ keyEdit sql注入漏洞

产品介绍 畅捷通 T 是一款灵动&#xff0c;智慧&#xff0c;时尚的基于互联网时代开发的管理软件&#xff0c;主要针对中小型工贸与商贸企业&#xff0c;尤其适合有异地多组织机构&#xff08;多工厂&#xff0c;多仓库&#xff0c;多办事处&#xff0c;多经销商&#xff09;的…

朋友圈刷屏的粘土风格照片,你体验过了吗?

Remini 的粘土风格真的丑萌丑萌的&#xff01; 从去年“妙鸭相机”的走红&#xff0c;到今年Remini的刷屏&#xff0c;其实可以看出大众对于图片趣玩的兴趣非常大&#xff01; 一张普通的照片经过工具的处理&#xff0c;一下子变成新风格&#xff0c;让人眼前一亮。如果你也对…