1.手写nms,用C++写
NMS(Non-Maximum Suppression)是计算机视觉中常用的一种技术,用于处理目标检测任务中的重叠边界框问题。下面是一个简单的NMS算法实现,它使用了C++和OpenCV库:
#include <opencv2/opencv.hpp> #include <vector> #include <algorithm> void nms(std::vector<cv::Rect>& input_boxes, float threshold, std::vector<cv::Rect>& output_boxes) { std::sort(input_boxes.begin(), input_boxes.end(), [](const cv::Rect& a, const cv::Rect& b) { return a.area() > b.area(); }); while (input_boxes.size() > 0) { cv::Rect box1 = input_boxes[0]; output_boxes.push_back(box1); input_boxes.erase(input_boxes.begin()); for (auto it = input_boxes.begin(); it != input_boxes.end(); ) { cv::Rect box2 = *it; float iou = (box1 & box2).area() / (float)(box1.area() + box2.area() - (box1 & box2).area()); if (iou > threshold) it = input_boxes.erase(it); else ++it; } } }
在这个函数中,我们首先按照面积大小对输入的边界框进行排序。然后我们每次取出面积最大的边界框,并将其添加到输出的边界框列表中。接着我们将这个边界框从输入的边界框列表中移除,并删除与其重叠率超过阈值的所有其他边界框。这个过程将一直持续,直到输入的边界框列表为空。
注意的是,这个函数中的「iou」(Intersection over Union)计算的是两个边界框的重叠率,它是重叠区域的面积除以两个边界框的并集面积。我们使用这个值来判断两个边界框是否重叠:如果「iou」超过了阈值,我们就认为两个边界框重叠。
最后,你需要在实际的目标检测任务中,根据任务的具体需求来选择合适的阈值。
2.从模型和数据的角度分别说一下如何解决梯度爆炸的问题?
梯度爆炸是深度学习模型在训练过程中可能会遇到的问题,特别是在训练深度神经网络和递归神经网络时。梯度爆炸问题会导致网络的权重更新过大,使得模型无法收敛。以下是从模型和数据的角度分别来看如何解决梯度爆炸的问题:
从模型的角度解决梯度爆炸:
- 「权重初始化」:适当的权重初始化可以预防梯度爆炸。例如,Xavier初始化和He初始化可以保证权重在初始化时保持合适的尺度和方差。
- 「梯度裁剪」(Gradient Clipping):这是一种常见的解决梯度爆炸问题的技术。当梯度的范数超过某个阈值时,梯度会被裁剪或者缩放,以保证梯度更新不会过大。
- 「使用Batch Normalization」:Batch Normalization可以在训练过程中稳定神经网络的输出,防止值的范围变得过大或过小。
从数据的角度解决梯度爆炸:
- 「数据标准化」:通过将输入数据标准化到一个合适的尺度(例如,使其具有0均值和1标准差),可以防止数据的尺度过大导致的梯度爆炸。
- 「避免使用过大的学习率」:使用过大的学习率可能会导致梯度爆炸。使用学习率衰减策略,或者使用自适应学习率优化器(如Adam、Adagrad等)可以避免这个问题。
总的来说,解决梯度爆炸问题需要综合考虑模型的结构和参数设置,以及数据的处理方式。
3.fasterrcnn的流程,两阶段主要解决了什么问题?
Faster R-CNN是一种两阶段的目标检测方法,其由R-CNN和Fast R-CNN进一步演化而来,主要贡献在于引入了区域建议网络(Region Proposal Network, RPN)以更快速地生成高质量的候选框。
以下是Faster R-CNN的基本流程:
- 「区域建议网络」(RPN):这是Faster R-CNN的第一阶段。首先,模型通过一个卷积神经网络(通常是预训练的分类网络,如VGG16、ResNet等)将输入图像转化为一个特征图。然后,RPN在这个特征图上滑动,每个位置生成多个尺度和比例的候选框,并且对每个候选框进行二分类(是目标或者不是目标)。这样,RPN可以快速生成大量的候选框。
- 「RoI Pooling和分类」:这是Faster R-CNN的第二阶段。RPN生成的候选框被投影到特征图上,然后使用RoI Pooling将每个投影区域转化为固定大小的特征,这样就可以输入到全连接层进行进一步处理。然后,全连接层对每个RoI进行多分类(判断是哪一类目标)和边框回归(进一步调整候选框的位置和尺度)。
这种两阶段的流程解决了以下问题:
- 「提高检测精度」:由于RPN可以生成大量高质量的候选框,RoI Pooling可以充分利用卷积特征,全连接层可以进行精细的分类和回归,因此Faster R-CNN的检测精度很高。
- 「提高检测速度」:相比于R-CNN和Fast R-CNN,Faster R-CNN通过共享卷积特征和使用RPN生成候选框,大大提高了检测速度。
但是需要注意的是,Faster R-CNN虽然精度高,但是由于其两阶段的结构,相比于一阶段的方法(如YOLO、SSD等),其检测速度相对较慢。
4.yolo中是怎么解决正负样本不均衡问题的?yolo中的object分支有什么作用?
YOLO(You Only Look Once)是一种实时的目标检测方法,其主要特点是将目标检测任务转化为一个回归问题,从而实现端到端的训练和检测。
在YOLO中,正负样本不均衡问题主要体现在大量的背景区域(负样本)和少量的目标区域(正样本)之间。为了解决这个问题,YOLO采取了以下策略:
- 「设计损失函数」:YOLO的损失函数考虑了类别不均衡问题。YOLO的包含目标的边框(即正样本)的损失函数包括对于边框坐标的预测,置信度和分类的损失;不包含目标的边框(即负样本)的损失函数设计主要是是置信度损失
- 但是对于不包含目标的边框的损失,YOLO给予了较小的权重。
- 「选择正样本」:对于每一个真实目标,YOLO选择预测边框和真实边框的IoU(交并比)最大的边框作为正样本,这样可以保证每一个真实目标都有一个正样本进行学习。
YOLO中的object分支用于预测每一个预测边框的目标性(objectness),即这个边框中是否包含一个目标。object分支的输出是一个介于0和1之间的实数,值越大表示这个边框中包含目标的可能性越大。在预测阶段,YOLO通常会设定一个阈值(如0.5),只有object分支的输出大于这个阈值的边框才会被保留下来。object分支的引入可以帮助YOLO更准确地定位目标,减少虚假检测。
5.给定NCHW的输入,BN层的输出应该是什么?
批量归一化(Batch Normalization,BN)层是神经网络中常用的一种层,它可以缩放和平移每个输入通道的值,使得输出的均值接近0,方差接近1。这样可以稳定神经网络的训练,加速收敛,同时也有一定的正则化效果。
给定输入的维度是(N, C, H, W),其中N是批量大小(batch size),C是通道数(channels),H是高度(height),W是宽度(width)。
BN层的输出的维度应该与输入的维度相同,也是(N, C, H, W)。这是因为BN层对每个通道独立进行归一化,不改变通道数,也不改变每个通道的空间尺寸。
具体地,BN层首先计算每个通道的均值和方差,然后用每个通道的值减去均值并除以标准差,得到归一化的值。然后,BN层再用学习到的缩放参数和平移参数对归一化的值进行缩放和平移,得到最终的输出。这些操作都是逐元素进行的,所以不会改变输入的维度。
需要注意的是,以上是在训练阶段的操作。在推理阶段,BN层通常使用移动平均和移动方差代替每个批次的均值和方差。
6.BN层的作用是什么,有什么可学习参数?
Batch Normalization(BN)层的主要作用是通过正则化层的输入以减少"Internal Covariate Shift内部协变量偏移",从而使得深度网络的训练变得更稳定。Internal Covariate Shift是指训练神经网络时由于每一层参数的变化导致后一层输入分布的变化,这种变化会导致训练过程中每一层都需要不断地适应新的数据分布,使训练过程变得复杂并且可能导致训练过程陷入饱和区,从而影响训练速度。
BN层的引入,使得每一层的输入都近似标准正态分布,这样一来,后一层的输入分布就不会随着前一层参数的变化而变化,从而缓解了Internal Covariate Shift的问题。这样,BN层可以:
- 加速神经网络的收敛速度。
- 降低模型对初始化的敏感度。
- 具有一定的正则化效果,减少模型的过拟合。
在BN层中,每个特征通道有两个可学习的参数,一个是缩放参数γ(gamma),一个是平移参数β(beta)。这两个参数是在对输入数据做标准化处理之后引入的,目的是保持模型的表达能力。如果在BN操作后,模型需要原始的、未归一化的特征,那么这可以通过学习合适的γ和β参数来实现。
具体来说,对于每个通道,BN层的计算过程如下:
- 计算输入数据的均值和方差。
- 使用均值和方差对输入数据进行标准化处理。
- 使用缩放参数γ和平移参数β进行缩放和平移,这两个参数是通过反向传播和梯度下降等方式学习得到的。
7.在训模型的时候如果遇到显存占用了,但是后面的GPU利用率不高的情况,可能是因为什么?
在训练模型时,如果显存占用很高但GPU利用率不高,这可能由以下原因导致:
- 「CPU和GPU之间的数据传输瓶颈」:在训练过程中,数据需要从CPU传输到GPU。如果这个传输过程过慢,就可能导致GPU在等待数据时处于空闲状态,从而降低其利用率。
- 「训练批量(Batch size)过小」:如果训练批量过小,GPU可能无法充分利用其并行计算能力,导致利用率低。
- 「模型计算复杂度低」:如果模型的计算复杂度低(比如模型较小,或者模型的运算并不复杂),那么GPU的计算能力可能没有被充分利用。
- 「同步操作」:一些同步操作,如同步Batch Normalization或数据加载等,也可能导致GPU等待,降低其利用率。
- 「IO瓶颈」:如果数据从磁盘读取到内存的速度跟不上模型的训练速度,也会使GPU在等待新的数据时空闲,从而降低利用率。
- 「显存不足」:显存不足会限制网络的大小和批量大小,甚至可能导致无法运行网络,也会导致GPU利用率低。
解决上述问题的方法包括:优化数据传输过程、增大训练批量(前提是显存允许)、选择更复杂的模型、优化同步操作、提升IO速度、升级GPU或降低模型和批量的大小等。
8.在训模型的时候如果loss报Nan了,可能是因为什么?
模型训练过程中出现NaN(Not a Number)损失函数值,可能是由以下几个原因引起的:
- 「学习率过高」:学习率过高可能会使模型在训练过程中跳过最优解,导致损失函数的值快速增加到无穷大,然后变成NaN。降低学习率可能会帮助解决这个问题。
- 「参数初始化不恰当」:如果模型参数初始化的值过大或过小,可能会导致训练过程中的激活值或梯度过大或过小,从而引发NaN值。选择适合的参数初始化方法可能有助于解决这个问题。
- 「梯度爆炸/消失」:如果模型的梯度变得非常大或非常小,可能会导致NaN值。梯度裁剪或者使用更稳定的优化算法(如Adam)可以缓解这个问题。
- 「数值不稳定」:一些操作可能会在某些输入值上产生数值不稳定,例如,对一个非正数取对数,或者除以一个接近于零的数。需要确保所有的数值运算都是稳定的。
- 「数据问题」:如果输入数据包含NaN值或者无穷大的值,或者标签数据有问题,可能会导致NaN损失。需要检查输入数据和标签数据是否正确。
除了这些,如果问题仍然存在,建议使用调试工具或者逐步运行代码,查看哪一步开始产生NaN值,这将有助于识别和解决问题。
9.Detr是怎么做到不用nms的,如何保证在一个目标附近只生成一个bbox?
DETR(DEtection TRansformer)是一个基于Transformer结构的目标检测算法,它的特点是在检测过程中不需要使用NMS(Non-Maximum Suppression,非极大值抑制)。其基本思想是将目标检测任务转化为一个直接预测类别和位置的回归问题,它尝试为每个目标生成一个独特的bounding box。
在传统的目标检测方法中,模型会生成大量候选框,然后利用NMS等方法去除冗余的、重叠度高的候选框,从而确定最终的检测结果。但在DETR中,模型直接生成固定数量的预测,每个预测由一个类别和一个bounding box组成。这个固定数量的预测数量一般设置为图像中可能出现的目标数量的最大值。
为了实现这个目标,DETR引入了一个新的损失函数,叫做二分匹配损失(bipartite matching loss)。这个损失函数会在预测和真实目标之间建立一种一对一的匹配关系。具体来说,它会根据预测和真实目标之间的距离,为每个真实目标找到一个最匹配的预测,然后只计算这些匹配对的损失。通过这种方式,每个真实目标都只会有一个与之对应的预测,这就避免了在一个目标附近生成多个bounding box的问题。
因此,DETR的主要优点是其简单、端到端的结构,它避免了在预测过程中需要复杂的后处理步骤,如NMS。这种设计使得DETR的预测过程更加直接和清晰。
10.手撕回字形遍历:
回字形遍历(也称为螺旋遍历或螺旋式遍历)通常指的是在二维数组或矩阵中按照螺旋形状进行元素访问的方式。下面是一个用C++实现的回字形遍历的例子,假设我们的输入是一个m x n的二维数组或矩阵。
#include <vector> using namespace std; vector<int> spiralOrder(vector<vector<int>>& matrix) { vector<int> res; if (matrix.empty() || matrix[0].empty()) return res; int top = 0, bottom = matrix.size() - 1; int left = 0, right = matrix[0].size() - 1; while (true) { for (int i = left; i <= right; i++) res.push_back(matrix[top][i]); if (++top > bottom) break; for (int i = top; i <= bottom; i++) res.push_back(matrix[i][right]); if (--right < left) break; for (int i = right; i >= left; i--) res.push_back(matrix[bottom][i]); if (--bottom < top) break; for (int i = bottom; i >= top; i--) res.push_back(matrix[i][left]); if (++left > right) break; } return res; }
这段代码首先定义了上、下、左、右四个边界,然后在while循环中按照右移->下移->左移->上移的顺序访问元素,并适当地调整边界。当一个方向上的遍历完成后,就更新相应的边界,当边界不再有效时,即结束遍历。
这样,我们就可以按照螺旋的方式遍历完整个矩阵。最后返回的res向量中就包含了按照回字形遍历的所有元素。