卷积主要有三大特点:
最常用的是大小的卷积核,两个卷积核和一个卷积核的感受野相同,但是减少了参数量和计算量,加快了模型训练。与此同时由于卷积核的增加,模型的非线性表达能力大大增强。
不过大卷积核(,)也有使用的空间,在GAN,图像超分辨率,图像融合等领域依然有较多的应用,大家可按需切入感兴趣的领域查看相关论文。
目标检测和目标跟踪很多模型都会用到RPN层,anchor是RPN层的基础,而感受野(receptive field,RF)是anchor的基础。
感受野的作用:
感受野计算:
增大感受野的方法:
常规的神经网络一般每层仅用一个尺寸的卷积核,但同一层的特征图可以分别使用多个不同尺寸的卷积核,以获得不同尺度的特征,再把这些特征结合起来,得到的特征往往比使用单一尺寸卷积核的要好,如GoogLeNet 、Inception系列的网络,均是每层使用了多个不同的卷积核结构。如下图所示,输入的特征图在同一层分别经过,和三种不同尺寸的卷积核,再将各自的特征图进行整合,得到的新特征可以看作不同感受野提取的特征组合,相比于单一尺寸卷积核会有更强的表达能力。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ebWyL0H5-1662211893298)(data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==)]
卷积的作用主要有以下几点:
卷积首发于NIN(Network in Network),后续也在GoogLeNet和ResNet等网络中使用。感兴趣的朋友可追踪这些论文研读细节。
转置卷积通过训练过程学习到最优的上采样方式,来代替传统的插值上采样方法,以提升图像分割,图像融合,GAN等特定任务的性能。
转置卷积并不是卷积的反向操作,从信息论的角度看,卷积运算是不可逆的。转置卷积可以将输出的特征图尺寸恢复卷积前的特征图尺寸,但不恢复原始数值。
转置卷积的计算公式:
我们设卷积核尺寸为,输入特征图为。
(1)当,时:
输入特征图在进行转置卷积操作时相当于进行了的填充,接着再进行正常卷积转置之后的标准卷积运算。
输出特征图的尺寸 =
(2)当,时:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F09zs2XL-1662211893307)(data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==)]
输入特征图在进行转置卷积操作时相当于进行了的填充,相邻元素间的空洞大小为,接着再进行正常卷积转置之后的标准卷积运算。
输出特征图的尺寸 =
空洞卷积的作用是在不进行池化操作损失信息的情况下,增大感受野,让每个卷积输出都包含较大范围的信息。
空洞卷积有一个参数可以设置dilation rate,其在卷积核中填充dilation rate个0,因此,当设置不同dilation rate时,感受野就会不一样,也获取了多尺度信息。
(a) 图对应3x3的1-dilated conv,和普通的卷积操作一样。(b)图对应的2-dilated conv,实际的卷积kernel size还是,但是空洞为,也就是对于一个的图像patch,只有个红色的点和的kernel发生卷积操作,其余的点的权重为。©图是4-dilated conv操作。
全连接层将卷积学习到的高维特征映射到label空间,可以作为整个网络的分类器模块。
虽然全连接层参数存在冗余的情况,但是在模型进行迁移学习时,其能保持较大的模型capacity。
目前很多模型使用全局平均池化(GAP)取代全连接层以减小模型参数,并且依然能达到SOTA的性能。
池化层的作用是对感受野内的特征进行选择,提取区域内最具代表性的特征,能够有效地减少输出特征数量,进而减少模型参数量。按操作类型通常分为最大池化(Max Pooling)、平均池化(Average Pooling)和求和池化(Sum Pooling),它们分别提取感受野内最大、平均与总和的特征值作为输出,最常用的是最大池化和平均池化。
BN层解决了什么问题?
统计机器学习中的一个经典假设是“源空间(source domain)和目标空间(target domain)的数据分布(distribution)是一致的”。如果不一致,那么就出现了新的机器学习问题,如transfer learning/domain adaptation等。而covariate shift就是分布不一致假设之下的一个分支问题,它是指源空间和目标空间的条件概率是一致的,但是其边缘概率不同。对于神经网络的各层输出,由于它们经过了层内卷积操作,其分布显然与各层对应的输入信号分布不同,而且差异会随着网络深度增大而增大,但是它们所能代表的label仍然是不变的,这便符合了covariate shift的定义。
因为神经网络在做非线性变换前的激活输入值随着网络深度加深,其分布逐渐发生偏移或者变动(即上述的covariate shift)。之所以训练收敛慢,一般是整体分布逐渐往非线性函数的取值区间的上下限两端靠近(比如sigmoid),所以这导致反向传播时低层神经网络的梯度消失,这是训练深层神经网络收敛越来越慢的本质原因。而BN就是通过一定的正则化手段,把每层神经网络任意神经元这个输入值的分布强行拉回到均值为0方差为1的标准正态分布,避免因为激活函数导致的梯度弥散问题。所以与其说BN的作用是缓解covariate shift,也可以说BN可缓解梯度弥散问题。
BN的公式
其中scale和shift是两个可学的参数,因为减去均值除方差未必是最好的分布。比如数据本身就很不对称,或者激活函数未必是对方差为1的数据有最好的效果。所以要加入缩放及平移变量来完善数据分布以达到比较好的效果。
BN层训练和测试的不同
在训练阶段,BN层是对每个batch的训练数据进行标准化,即用每一批数据的均值和方差。(每一批数据的方差和标准差不同)
而在测试阶段,我们一般只输入一个测试样本,并没有batch的概念。因此这个时候用的均值和方差是整个数据集训练后的均值和方差,可以通过滑动平均法求得:
上面式子简单理解就是:对于均值来说直接计算所有batch 值的平均值;然后对于标准偏差采用每个batch σ的无偏估计。
在测试时,BN使用的公式是:
BN训练时为什么不用整个训练集的均值和方差?
因为用整个训练集的均值和方差容易过拟合,对于BN,其实就是对每一batch数据标准化到一个相同的分布,而不同batch数据的均值和方差会有一定的差别,而不是固定的值,这个差别能够增加模型的鲁棒性,也会在一定程度上减少过拟合。
BN层用在哪里?
在CNN中,BN层应该用在非线性激活函数前面。由于神经网络隐藏层的输入是上一层非线性激活函数的输出,在训练初期其分布还在剧烈改变,此时约束其一阶矩和二阶矩无法很好地缓解 Covariate Shift;而BN的分布更接近正态分布,限制其一阶矩和二阶矩能使输入到激活函数的值分布更加稳定。
BN层的参数量
我们知道γ和β是需要学习的参数,而BN的本质就是利用优化学习改变方差和均值的大小。在CNN中,因为网络的特征是对应到一整张特征图上的,所以做BN的时候也是以特征图为单位而不是按照各个维度。比如在某一层,Batch大小为,那么做BN的参数量为。
BN的优缺点
优点:
缺点:
IoU(Intersection over Union)即交并比,是目标检测任务中一个重要的模块,其是GT bbox与pred bbox交集的面积 / 二者并集的面积。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mcKUlrmA-1662211893314)(data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==)]
下面我们用坐标(top,left,bottom,right),即左上角坐标,右下角坐标。从而可以在给定的两个矩形中计算IOU值。
def compute_iou(rect1,rect2):
# (y0,x0,y1,x1) = (top,left,bottom,right)
S_rect1 = (rect1[2] - rect1[0]) * (rect1[3] - rect1[1])
S_rect2 = (rect2[2] - rect2[0]) * (rect2[3] - rect1[1])
sum_all = S_rect1 + S_rect2
left_line = max(rect1[1],rect2[1])
right_line = min(rect1[3],rect2[3])
top_line = max(rect1[0],rect2[0])
bottom_line = min(rect1[2],rect2[2])
if left_line >= right_line or top_line >= bottom_line:
return 0
else:
intersect = (right_line - left_line) * (bottom_line - top_line)
return (intersect / (sum_area - intersect)) * 1.0
在目标检测中,我们可以利用非极大值抑制(NMS)对生成的大量候选框进行后处理,去除冗余的候选框,得到最具代表性的结果,以加快目标检测的效率。
如下图所示,消除多余的候选框,找到最佳的bbox:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZS9dcAdy-1662211893315)(data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==)]
非极大值抑制(NMS)流程:
import numpy as np
def py_cpu_nms(dets, thresh):
#x1、y1(左下角坐标)、x2、y2(右上角坐标)以及score的值
x1 = dets[:, 0]
y1 = dets[:, 1]
x2 = dets[:, 2]
y2 = dets[:, 3]
scores = dets[:, 4]
#每一个候选框的面积
areas = (x2 - x1 + 1) * (y2 - y1 + 1)
#按照score降序排序(保存的是索引)
order = scores.argsort()[::-1]
keep = []
while order.size > 0:
i = order[0]
keep.append(i)
#计算当前概率最大矩形框与其他矩形框的相交框的坐标,会用到numpy的broadcast机制,得到向量
xx1 = np.maximum(x1[i], x1[order[1:]])
yy1 = np.maximum(y1[i], y1[order[1:]])
xx2 = np.minimum(x2[i], x2[order[1:]])
yy2 = np.minimum(y2[i], y2[order[1:]])
#计算相交框的面积,注意矩形框不相交时w或h算出来会是负数,用0代替
w = np.maximum(0.0, xx2 - xx1 + 1)
h = np.maximum(0.0, yy2 - yy1 + 1)
inter = w * h
#计算重叠度IOU:重叠面积 / (面积1 + 面积2 - 重叠面积)
ovr = inter / (areas[i] + areas[order[1:]] - inter)
#找到重叠度不高于阈值的矩形框索引
inds = np.where(ovr <= thresh)[0]
# 将order序列更新,由于前面得到的矩形框索引要比矩形框在原order序列中的索引小1,所以要加1操作
order = order[inds + 1]
return keep
Two-stage目标检测算法:先进行区域生成(region proposal,RP)(一个有可能包含待检物体的预选框),再通过卷积神经网络进行样本分类。其精度较高,速度较慢。
主要逻辑:特征提取—>生成RP—>分类/定位回归。
常见的Two-stage目标检测算法有:Faster R-CNN系列和R-FCN等。
One-stage目标检测算法:不用RP,直接在网络中提取特征来预测物体分类和位置。其速度较快,精度比起Two-stage算法稍低。
主要逻辑:特征提取—>分类/定位回归。
常见的One-stage目标检测算法有:YOLO系列、SSD和RetinaNet等。
每次回答这个问题的时候,都会包含我的私心,我喜欢从电气自动化的角度去阐述,而非计算机角度,因为这会让我想起大学时代的青葱岁月。
ResNet就是一个差分放大器。ResNet的结构设计,思想逻辑,就是在机器学习中抽象出了一个差分放大器,其能让深层网络的梯度的相关性增强,在进行梯度反传时突出微小的变化。
模型的特点则是设计了残差结构,其对模型输出的微小变化非常敏感。
为什么加入残差模块会有效果呢?
假设:如果不使用残差模块,输出为,期望输出为,如果想要学习H函数,使得,这个变化率比较低,学习起来是比较困难的。
但是如果设计为 ,进行一种拆分,使得,那么学习目标就变为让,一个映射函数学习使得它输出由0.1变为0,这个是比较简单的。也就是说引入残差模块后的映射对输出变化更加敏感了。
进一步理解:如果,现在继续训练模型,使得映射函数。变化率为:,如果不用残差模块的话可能要把学习率从0.01设置为0.0000001。层数少还能对付,一旦层数加深的话可能就不太好使了。
这时如果使用残差模块,也就是变化为。这个变化率增加了100%。明显这样的话对参数权重的调整作用更大。
ResNeXt模型是在ResNet模型的基础上进行优化。其主要是在ResNeXt中引入Inception思想。如下图所示,左侧是ResNet经典结构,右侧是ResNeXt结构,其将单路卷积转化成多支路的多路卷积,进行分组卷积。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mAbdgjCa-1662211893321)(data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==)]ResNet与ResNeXt结构对比
作者进一步提出了ResNeXt的三种等价结构,其中c结构中的分组卷积思想就跃然纸上了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QetCF4b2-1662211893322)(data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==)]
最后我们看一下ResNeXt50和ResNet50结构上差异的对比图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UWjJdu7g-1662211893323)(data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==)]
ResNeXt论文:《Aggregated Residual Transformations for Deep Neural Networks》
MobileNet是一种轻量级的网络结构,主要针对手机等嵌入式设备而设计。MobileNetv1网络结构在VGG的基础上使用depthwise Separable卷积,在保证不损失太大精度的同时,大幅降低模型参数量。
Depthwise separable卷积是由Depthwise卷积和Pointwise卷积构成。Depthwise卷积(DW)能有效减少参数数量并提升运算速度。但是由于每个特征图只被一个卷积核卷积,因此经过DW输出的特征图只包含输入特征图的全部信息,而且特征之间的信息不能进行交流,导致“信息流通不畅”。Pointwise卷积(PW)实现通道特征信息交流,解决DW卷积导致“信息流通不畅”的问题。
Depthwise Separable卷积和标准卷积的计算量对比:
相比标准卷积,Depthwise Separable卷积可以大幅减小计算量。并且随着卷积通道数的增加,效果更加明显。
并且Mobilenetv1使用stride=2的卷积替换池化操作,直接在卷积时利用stride=2完成了下采样,从而节省了卷积后再去用池化操作去进行一次下采样的时间,可以提升运算速度。
MobileNetv1论文:《MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications》
MobileNetV2在MobileNetV1的基础上引入了Linear Bottleneck 和 Inverted Residuals。
MobileNetV2使用Linear Bottleneck(线性变换)来代替原本的非线性激活函数,来捕获感兴趣的流形。实验证明,使用Linear Bottleneck可以在小网络中较好地保留有用特征信息。
Inverted Residuals与经典ResNet残差模块的通道间操作正好相反。由于MobileNetV2使用了Linear Bottleneck结构,使其提取的特征维度整体偏低,如果只是使用低维的feature map效果并不会好。如果卷积层都是使用低维的feature map来提取特征的话,那么就没有办法提取到整体的足够多的信息。如果想要提取全面的特征信息的话,我们就需要有高维的feature map来进行补充,从而达到平衡。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pFZx4XGk-1662211893326)(data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==)]
MobileNetV2的论文:《MobileNetV2: Inverted Residuals and Linear Bottlenecks》
MobileNetV3在整体上有两大创新:
1.互补搜索技术组合:由资源受限的NAS执行模块级搜索;由NetAdapt执行局部搜索,对各个模块确定之后网络层的微调。
2.网络结构改进:进一步减少网络层数,并引入h-swish激活函数。
作者发现swish激活函数能够有效提高网络的精度。然而,swish的计算量太大了。作者提出h-swish(hard version of swish)如下所示:
这种非线性在保持精度的情况下带了了很多优势,首先ReLU6在众多软硬件框架中都可以实现,其次量化时避免了数值精度的损失,运行快。
MobileNetV3模型结构的优化:
MobileNetV3的论文:《Searching for MobileNetV3》
ViT模型特点:1.ViT直接将标准的Transformer结构直接用于图像分类,其模型结构中不含CNN。2.为了满足Transformer输入结构要求,输入端将整个图像拆分成小图像块,然后将这些小图像块的线性嵌入序列输入网络中。在最后的输出端,使用了Class Token形式进行分类预测。3.Transformer比CNN结构少了一定的平移不变性和局部感知性,在数据量较少的情况下,效果可能不如CNN模型,但是在大规模数据集上预训练过后,再进行迁移学习,可以在特定任务上达到SOTA性能。
ViT的整体模型结构:
其可以具体分成如下几个部分:
Efficientnet系列模型是通过grid search从深度(depth)、宽度(width)、输入图片分辨率(resolution)三个角度共同调节搜索得来的模型。其从EfficientNet-B0到EfficientNet-L2版本,模型的精度越来越高,同样,参数量和对内存的需求也会随之变大。
深度模型的规模主要是由宽度、深度、分辨率这三个维度的缩放参数决定的。这三个维度并不是相互独立的,对于输入的图片分辨率更高的情况,需要有更深的网络来获得更大的感受视野。同样的,对于更高分辨率的图片,需要有更多的通道来获取更精确的特征。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2XMcZwzm-1662211893330)(data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==)]EfficientNet模型搜索逻辑
EfficientNet模型的内部是通过多个MBConv卷积模块实现的,每个MBConv卷积模块的具体结构如下图所示。其用实验证明Depthwise Separable卷积在大模型中依旧非常有效;Depthwise Separable卷积较于标准卷积有更好的特征提取表达能力。
另外论文中使用了Drop_Connect方法来代替传统的Dropout方法来防止模型过拟合。DropConnect与Dropout不同的地方是在训练神经网络模型过程中,它不是对隐层节点的输出进行随机的丢弃,而是对隐层节点的输入进行随机的丢弃。
EfficientNet论文:《EfficientNet: Rethinking Model Scaling for Convolutional Neural Networks》
说点题外话,隔着paper都能看到作者那窒息的调参过程。。。
面试中经常会问一些关于模型方面的问题,这也是不太好量化定位的问题,因为模型繁杂多样,面试官问哪个都有可能,下面的逻辑图里我抛砖引玉列出了一些不管是在学术界还是工业界都是高价值的模型,供大家参考。
最好还是多用项目,竞赛,科研等工作润色简历,并在面试过程中将模型方面的问题引向这些工作中用到的熟悉模型里。
误差(Error)= 偏差(Bias) + 方差(Variance) + 噪声(Noise),一般地,我们把机器学习模型的预测输出与样本的真实label之间的差异称为误差,其反应的是整个模型的准确度。
噪声(Noise):描述了在当前任务上任何机器学习算法所能达到的期望泛化误差的下界,即刻画了当前任务本质的难度。
偏差(Bias):衡量了模型拟合训练数据的能力,偏差反应的是所有采样得到的大小相同的训练集训练出的所有模型的输出平均值和真实label之间的偏差,即模型本身的精确度。
偏差通常是由于我们对机器学习算法做了错误的假设所导致的,比如真实数据分布映射的是某个二次函数,但我们假设模型是一次函数。
偏差(Bias)越小,拟合能力却强(可能产生过拟合);反之,拟合能力越弱(可能产生欠拟合)。偏差越大,越偏离真实数据。
方差描述的是预测值的变化范围,离散程度,也就是离期望值的距离。方差越大,数据的分布越分散,模型的稳定程度越差。
方差也反应了模型每一次输出结果与模型输出期望之间的误差,即模型的稳定性。由方差带来的误差通常体现在测试误差相对于训练误差的增量上。
方差通常是由于模型的复杂度相对于训练样本数过高导致的。方差越小,模型的泛化能力越高;反之,模型的泛化能力越低。
如果模型在训练集上拟合效果比较优秀,但是在测试集上拟合效果比较差,则表示方差较大,说明模型的稳定程度较差,出现这种现象可能是由于模型对训练集过拟合造成的。
接下来我们用下面的射击的例子进一步解释这二者的区别。假设一次射击就是机器学习模型对一个样本进行预测。射中靶心位置代表预测准确,偏离靶心越远代表预测误差越大,其中左上角是最好的结果。
过拟合:模型在训练集上拟合的很好,但是模型连噪声数据的特征都学习了,丧失了对测试集的泛化能力。
解决过拟合的方法:
欠拟合:模型在训练集和测试集上效果均不好,其根本原因是模型没有学习好数据集的特征。
解决欠拟合的方法:
正则化是机器学习的核心主题之一。正则化本质是对某一问题加以先验的限制或约束以达到某种特定目的的一种操作。在机器学习中我们通过使用正则化方法,防止其过拟合,降低其泛化误差。
常用的正则化手段:
L范数主要起到了正则化(即用一些先验知识约束或者限制某一抽象问题)的作用,而正则化主要是防止模型过拟合。
范数主要用来表征高维空间中的距离,故在一些生成任务中也直接用L范数来度量生成图像与原图像之间的差别。
下面列出深度学习中的范数:
Dropout是在训练过程中以一定的概率使神经元失活,也就是输出等于0。从而提高模型的泛化能力,减少过拟合。
我们可以从两个方面去直观地理解Dropout的正则化效果:1)在Dropout每一轮训练过程中随机丢失神经元的操作相当于多个模型进行取平均,因此用于预测时具有vote的效果。2)减少神经元之间复杂的共适应性。当隐藏层神经元被随机删除之后,使得全连接网络具有了一定的稀疏化,从而有效地减轻了不同特征的协同效应。也就是说,有些特征可能会依赖于固定关系的隐含节点的共同作用,而通过Dropout的话,就有效地避免了某些特征在其他特征存在下才有效果的情况,增加了神经网络的鲁棒性。
Dropout在训练和测试时的区别:Dropout只在训练时产生作用,是为了减少神经元对部分上层神经元的依赖,类似将多个不同网络结构的模型集成起来,减少过拟合风险。而在测试时,应该用整个训练好的模型,因此不需要Dropout。
在二分类问题中,我们可以使用sigmoid函数将输出映射到【0,1】区间中,从而得到单个类别的概率。当我们将问题推广到多分类问题时,可以使用Softmax函数,对输出的值映射为概率值。
其定义为:
其中a代表了模型的输出。
交叉熵(cross entropy)常用于深度学习中的分类任务,其可以表示预测值与ground truth之间的差距。
交叉熵是信息论中的概念。其定义为:
代表的概率分布,代表预测值的概率分布。交叉熵从相对熵(KL散度)演变而来,代表了信息量,越大说明可能性越大,其信息量越少;反之则信息量越大。通过不断的训练优化,逐步减小交叉熵损失函数的值来达到缩小和距离的目的。
机器学习的直接目的是希望模型在真实场景的数据上有很好的预测效果,泛化误差越低越好。
如何去跟踪泛化误差呢?这时就需要验证集和测试集了。
我们可以使用训练集的数据来训练模型,然后用测试集上的误差推测最终模型在应对现实场景中的泛化误差。有了测试集,我们可以在本地验证模型的最终的近似效果。
与此同时,我们在模型训练过程中要实时监控模型的指标情况,从而进行模型参数优选操作。验证集就用于模型训练过程中的指标评估。
一般来说,如果当数据量不是很大的情况(万级别以下)可以将训练集、验证集和测试集划分为6:2:2;如果是万级别甚至十万级别的数据量,可以将训练集、验证集和测试集比例调整为98:1:1。
(注:在数据集划分时要主要类别的平衡)
首先,这个问题只存在于二分类问题中,对于多分类问题,只需要概率最高的那个预测标签作为输出结果即可。
F1值是综合了精准率和召回率两个指标对模型进行评价:
一般设0.5作为二分类的默认阈值,但一般不是最优阈值。想要精准率高,一般使用高阈值,而想要召回率高,一般使用低阈值。在这种情况下,我们通常可以通过P-R曲线去寻找最优的阈值点或者阈值范围。
我们在调用递归函数的时候,把递归函数当作普通函数(黑箱)来调用,即明白该函数的输入输出是什么,而不用管此函数的内部运行机制。
前序遍历:
def dfs(root):
if not root:
return
执行操作
dfs(root.left)
dfs(root.right)
中序遍历:
def dfs(root):
if not root:
return
dfs(root.left)
执行操作
dfs(root.right)
后序遍历:
def dfs(root):
if not root:
return
dfs(root.left)
dfs(root.right)
执行操作
树的遍历模式:
可迭代对象是迭代器、生成器和装饰器的基础。简单来说,可以使用for来循环遍历的对象就是可迭代对象。比如常见的list、set和dict。
我们来看一个:
from collections import Iterable
print(isinstance('abcddddd', Iterable)) # str是否可迭代
print(isinstance([1,2,3,4,5,6], Iterable)) # list是否可迭代
print(isinstance(12345678, Iterable)) # 整数是否可迭代
-------------结果如下----------------
True
True
False
当对所有的可迭代对象调用 dir() 方法时,会发现他们都实现了 iter 方法。这样就可以通过 iter(object) 来返回一个迭代器。
x = [1, 2, 3]
y = iter(x)
print(type(x))
print(type(y))
------------结果如下------------
可以看到调用iter()之后,变成了一个list_iterator的对象。可以发现增加了一个__next__方法。所有实现了__iter__和__next__两个方法的对象,都是迭代器。
迭代器是带状态的对象,它会记录当前迭代所在的位置,以方便下次迭代的时候获取正确的元素。__iter__返回迭代器自身,__next__返回容器中的下一个值,如果容器中没有更多元素了,则抛出Stoplteration异常。
x = [1, 2, 3]
y = iter(x)
print(next(y))
print(next(y))
print(next(y))
print(next(y))
----------结果如下----------
1
2
3
Traceback (most recent call last):
File "/Users/Desktop/test.py", line 6, in
print(next(y))
StopIteration
如何判断对象是否是迭代器,和判断是否是可迭代对象的方法差不多,只要把 Iterable 换成 Iterator。
Python的for循环本质上就是通过不断调用next()函数实现的,举个栗子,下面的代码先将可迭代对象转化为Iterator,再去迭代。这样可以节省对内存,因为迭代器只有在我们调用 next() 才会实际计算下一个值。
x = [1, 2, 3]
for elem in x:
...
itertools 库提供了很多常见迭代器的使用。
>>> from itertools import count # 计数器
>>> counter = count(start=13)
>>> next(counter)
13
>>> next(counter)
14
我们创建列表的时候,受到内存限制,容量肯定是有限的,而且不可能全部给他一次枚举出来。Python常用的列表生成式有一个致命的缺点就是定义即生成,非常的浪费空间和效率。
如果列表元素可以按照某种算法推算出来,那我们可以在循环的过程中不断推算出后续的元素,这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器:generator。
要创建一个generator,最简单的方法是改造列表生成式:
a = [x * x for x in range(10)]
print(a)
b = (x * x for x in range(10))
print(b)
--------结果如下--------------
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
at 0x10557da50>
还有一个方法是生成器函数,通过def定义,然后使用yield来支持迭代器协议,比迭代器写起来更简单。
def spam():
yield"first"
yield"second"
yield"third"
for x in spam():
print(x)
-------结果如下---------
first
second
third
进行函数调用的时候,返回一个生成器对象。在使用next()调用的时候,遇到yield就返回,记录此时的函数调用位置,下次调用next()时,从断点处开始。
我们完全可以像使用迭代器一样使用 generator ,当然除了定义。定义一个迭代器,需要分别实现 iter() 方法和 next() 方法,但 generator 只需要一个小小的yield。
generator还有 send() 和 close() 方法,都是只能在next()调用之后,生成器处于挂起状态时才能使用的。
python是支持协程的,也就是微线程,就是通过generator来实现的。配合generator我们可以自定义函数的调用层次关系从而自己来调度线程。
装饰器允许通过将现有函数传递给装饰器,从而向现有函数添加一些额外的功能,该装饰器将执行现有函数的功能和添加的额外功能。
装饰器本质上还是一个函数,它可以让已有的函数不做任何改动的情况下增加功能。
接下来我们使用一些例子来具体说明装饰器的作用:
如果我们不使用装饰器,我们通常会这样来实现在函数执行前插入日志:
def foo():
print('i am foo')
def foo():
print('foo is running')
print('i am foo')
虽然这样写是满足了需求,但是改动了原有的代码,如果有其他的函数也需要插入日志的话,就需要改写所有的函数,这样不能复用代码。
我们可以进行如下改写:
import logging
def use_log(func):
logging.warning("%s is running" % func.__name__)
func()
def bar():
print('i am bar')
use_log(bar) #将函数作为参数传入
-------------运行结果如下--------------
WARNING:root:bar is running
i am bar
这样写的确可以复用插入的日志,缺点就是显式的封装原来的函数,我们希望能隐式的做这件事。
我们可以用装饰器来写:
import logging
def use_log(func):
def wrapper(*args, **kwargs):
logging.warning('%s is running' % func.__name__)
return func(*args, **kwargs)
return wrapper
def bar():
print('I am bar')
bar = use_log(bar)
bar()
------------结果如下------------
WARNING:root:bar is running
I am bar
其中,use_log函数就是装饰器,它把我们真正想要执行的函数bar()封装在里面,返回一个封装了加入代码的新函数,看起来就像是bar()被装饰了一样。
但是这样写还是不够隐式,我们可以通过@语法糖来起到bar = use_log(bar)的作用。
import logging
def use_log(func):
def wrapper(*args, **kwargs):
logging.warning('%s is running' % func.__name__)
return func(*args, **kwargs)
return wrapper
@use_log
def bar():
print('I am bar')
@use_log
def haha():
print('I am haha')
bar()
haha()
------------结果如下------------
WARNING:root:bar is running
I am bar
WARNING:root:haha is running
I am haha
这样子看起来就非常简洁,而且代码很容易复用。可以看成是一种智能的高级封装。
在Python中,用一个变量给另一个变量赋值,其实就是给当前内存中的对象增加一个“标签”而已。
>>> a = [6, 6, 6, 6]
>>> b = a
>>> print(id(a), id(b), sep = '\n')
66668888
66668888
>>> a is b
True(可以看出,其实a和b指向内存中同一个对象。)
浅拷贝是指创建一个新的对象,其内容是原对象中元素的引用(新对象与原对象共享内存中的子对象)。
注:浅拷贝和深拷贝的不同仅仅是对组合对象来说,所谓的组合对象就是包含了其他对象的对象,如列表,类实例等等。而对于数字、字符串以及其他“原子”类型,没有拷贝一说,产生的都是原对象的引用。
常见的浅拷贝有:切片操作、工厂函数、对象的copy()方法,copy模块中的copy函数。
>>> a = [6, 8, 9]
>>> b = list(a)
>>> print(id(a), id(b))
4493469248 4493592128 #a和b的地址不同
>>> for x, y in zip(a, b):
... print(id(x), id(y))
...
4489786672 4489786672
4489786736 4489786736
4489786768 4489786768
# 但是他们的子对象地址相同
从上面的例子中可以看出,a浅拷贝得到b,a和b指向内存中不同的list对象,但是他们的元素指向相同的int对象,这就是浅拷贝。
深拷贝是指创建一个新的对象,然后递归的拷贝原对象所包含的子对象。深拷贝出来的对象与原对象没有任何关联。
深拷贝只有一种方式:copy模块中的deepcopy函数。
我们接下来用一个包含可变对象的列表来确切地展示浅拷贝和深拷贝的区别:
>>> a = [[6, 6], [8, 8], [9, 9]]
>>> b = copy.copy(a) # 浅拷贝
>>> c = copy.deepcopy(a) # 深拷贝
>>> print(id(a), id(b)) # a和b地址不同
4493780304 4494523680
>>> for x, y in zip(a, b): # a和b的子对象地址相同
... print(id(x), id(y))
...
4493592128 4493592128
4494528592 4494528592
4493779024 4493779024
>>> print(id(a), id(c)) # a和c不同
4493780304 4493469248
>>> for x, y in zip(a, c): # a和c的子对象地址也不同
... print(id(x), id(y))
...
4493592128 4493687696
4494528592 4493686336
4493779024 4493684896
Python是解释语言。
解释语言的优点是可移植性好,缺点是运行需要解释环境,运行起来比编译语言要慢,占用的资源也要多一些,代码效率低。
编译语言的优点是运行速度快,代码效率高,编译后程序不可以修改,保密性好。缺点是代码需要经过编译才能运行,可移植性较差,只能在兼容的操作系统上运行。
在Python中,使用引用计数进行垃圾回收;同时通过标记-清除算法解决容器对象可能产生的循环引用问题;最后通过分代回收算法提高垃圾回收效率。
Python里的多线程是假的多线程。
Python解释器由于设计时有GIL全局锁,导致了多线程无法利用多核,只有一个线程在解释器中运行。
对于I/O密集型任务,Python的多线程能起到作用,但对于CPU密集型任务,Python的多线程几乎占不到任何优势,还有可能因为争夺资源而变慢。
对所有面向I/O的(会调用内建的操作系统C代码的)程序来说,GIL会在这个I/O调用之前被释放,以允许其它的线程在这个线程等待I/O的时候运行。
如果是纯计算的程序,没有 I/O 操作,解释器会每隔 100 次操作就释放这把锁,让别的线程有机会执行(这个次数可以通过 sys.setcheckinterval 来调整)如果某线程并未使用很多I/O 操作,它会在自己的时间片内一直占用处理器和GIL。
缓解GIL锁的方法:多进程和协程(协程也只是单CPU,但是能减小切换代价提升性能)
首先,xrange函数和range函数的用法完全相同,不同的地方是xrange函数生成的不是一个list对象,而是一个生成器。
要生成很大的数字序列时,使用xrange会比range的性能优很多,因为其不需要一上来就开辟很大的内存空间。
Python 2.7.15 | packaged by conda-forge | (default, Jul 2 2019, 00:42:22)
[GCC 4.2.1 Compatible Clang 4.0.1 (tags/RELEASE_401/final)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> range(10)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> xrange(10)
xrange(10)
>>> list(xrange(10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
xrange函数和range函数一般都用在循环的时候。具体例子如下所示:
>>> for i in range(0,7):
... print(i)
...
0
1
2
3
4
5
6
>>> for i in xrange(0,7):
... print(i)
...
0
1
2
3
4
5
6
在Python3中,xrange函数被移除了,只保留了range函数的实现,但是此时range函数的功能结合了xrange和range。并且range函数的类型也发生了变化,在Python2中是list类型,但是在Python3中是range序列的对象。
Python的dict(字典)为了支持快速查找使用了哈希表作为底层结构,哈希表平均查找时间复杂度为O(1)。CPython 解释器使用二次探查解决哈希冲突问题。
PyTorch动态图:计算图的运算与搭建同时进行;其较灵活,易调节。
TensorFlow静态图:计算图先搭建图,后运算;其较高效,不灵活。
面向对象程序设计(Object-oriented programming,OOP)有三大特征 ——封装、继承、多态。
封装:把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。关键字:public, protected, private。不写默认为 private。
继承:基类(父类)——> 派生类(子类)
多态:即多种状态(形态)。简单来说,我们可以将多态定义为消息以多种形式显示的能力。多态是以封装和继承为基础的。
C++ 多态分类及实现:
什么是内存对齐?计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是有效对齐值的倍数。
什么是有效对齐值?计算机系统有默认对齐系数n,可以通过#pragma pack(n)来指定。有效对齐值就等与该对齐系数和结构体中最长的数据类型的长度两者最小的那一个值,比如对齐系数是8,而结构体中最长的是int,4个字节,那么有效对齐值为4。
为什么要内存对齐?假如没有内存对齐机制,数据可以任意存放,现在一个int变量存放在从地址1开始的连续四个字节地址中。当4字节存取粒度的处理器去取数据时,要先从0地址开始读取第一个4字节块,剔除不想要的字节(0地址),然后从地址4开始读取下一个4字节块,同样剔除不要的数据(5,6,7地址),最后留下的两块数据合并放入寄存器,这需要做很多工作,整体效率较低。
struct内存占用如何计算?结构体的内存计算方式遵循以下规则:
我们来举两个:
#include
#pragma pack(8)
int main()
{
struct Test
{
int a;
//long double大小为16bytes
long double b;
char c[10];
};
printf("%d", sizeof(Test));
return 0;
}
struct的内存占用为40bytes
#include
#pragma pack(16)
int main()
{
struct Test
{
int a;
//long double大小为16bytes
long double b;
char c[10];
}
printf("%d", sizeof(Test));
return 0;
}
struct的内存占用为48bytes
智能指针是一个类,这个类的构造函数中传入一个普通指针,析构函数中释放传入的指针。智能指针的类都是栈上的对象,所以当函数(或程序)结束时会自动被释放。
(注:不能将指针直接赋值给一个智能指针,一个是类,一个是指针。)
常用的智能指针:智能指针在C++11版本之后提供,包含在头文件中,主要是shared_ptr、unique_ptr、weak_ptr。unique_ptr不支持复制和赋值。当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果原来的unique_ptr 将存在一段时间,编译器将禁止这么做。shared_ptr是基于引用计数的智能指针。可随意赋值,直到内存的引用计数为0的时候这个内存会被释放。weak_ptr能进行弱引用。引用计数有一个问题就是互相引用形成环,这样两个指针指向的内存都无法释放。需要手动打破循环引用或使用weak_ptr。顾名思义,weak_ptr是一个弱引用,只引用,不计数。如果一块内存被shared_ptr和weak_ptr同时引用,当所有shared_ptr析构了之后,不管还有没有weak_ptr引用该内存,内存也会被释放。所以weak_ptr不保证它指向的内存一定是有效的,在使用之前需要检查weak_ptr是否为空指针。
智能指针的作用:C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,野指针,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。
开发一个C++程序的过程通常包括编辑、编译、链接、运行和调试等步骤。
编辑:编辑是C++程序开发过程的第一步,它主要包括程序文本的输入和修改。任何一种文本编辑器都可以完成这项工作。当用户完成了C++程序的编辑时,应将输入的程序文本保存为以.cpp为扩展名的文件(保存C++头文件时应以.h为扩展名)。
编译:C++是一种高级程序设计语言,它的语法规则与汇编语言和机器语言相比更接近人类自然语言的习惯。然而,计算机能够“看”懂的唯一语言是汇编语言。因此,当我们要让计算机“看”懂一个C++程序时,就必须使用编译器将这个C++程序“翻译”成汇编语言。编译器所做的工作实际上是一种由高级语言到汇编语言的等价变换。
汇编:将汇编语言翻译成机器语言指令。汇编器对汇编语言进行一系列处理后最终产生的输出结构称为目标代码,它是某种计算机的机器指令(二进制),并且在功能上与源代码完全等价。保存源代码和目标代码的文件分别称为源文件和目标文件( .obj)。
链接:要将汇编器产生的目标代码变成可执行程序还需要最后一个步骤——链接。链接工作是由“链接器”完成的,它将编译后产生的一个或多个目标文件与程序中用到的库文件链接起来,形成一个可以在操作系统中直接运行的可执行程序。(linux中的.o文件)
运行和调试:我们接下来就可以执行程序了。如果出现问题我们可以进行调试debug。
数组和链表是C/C++中两种基本的数据结构,也是两个最常用的数据结构。
数组的特点是在内存中,数组是一块连续的区域,并且数组需要预留空间。链表的特点是在内存中,元素的空间可以在任意地方,空间是分散的,不需要连续。链表中的元素都会两个属性,一个是元素的值,另一个是指针,此指针标记了下一个元素的地址。每一个数据都会保存下一个数据的内存的地址,通过此地址可以找到下一个数据。
数组的优缺点:
优点:查询效率高,时间复杂度可以达到O(1)。
缺点:新增和修改效率低,时间复杂度为O(N);内存分配是连续的内存,扩容需要重新分配内存。
链表的优缺点:
优点:新增和修改效率高,只需要修改指针指向即可,时间复杂度可以达到O(1);内存分配不需要连续的内存,占用连续内存少。
缺点:链表查询效率低,需要从链表头依次查找,时间复杂度为O(N)。
new和malloc主要有以下三方面的区别:
模型压缩是指对算法模型进行精简,进而得到一个轻量且性能相当的小模型,压缩后的模型具有更小的结构和更少的参数,可以有效降低计算和存储开销,便于部署在端侧设备中。
随着AI技术的飞速发展,不管是移动端产品还是线上产品,进行AI赋能都成为了趋势。这种情况下,AI算法的实时性与减少内存占用都显得极为重要。AI模型的参数在一定程度上能够表达其复杂性,但并不是所有的参数都在模型中发挥作用,部分参数作用有限,表达冗余,甚至会降低模型的性能。
AI服务器与PC端一般都是使用X86架构,因为其高性能;AI端侧设备(手机/端侧盒子等)一般使用ARM架构,因为需要低功耗。
X86指令集中的指令是复杂的,一条很长指令就可以很多功能;而ARM指令集的指令是很精简的,需要几条精简的短指令完成很多功能。
X86的方向是高性能方向,因为它追求一条指令完成很多功能;而ARM的方向是面向低功耗,要求指令尽可能精简。
常规精度一般使用FP32(32位浮点,单精度)占用4个字节,共32位;低精度则使用FP16(半精度浮点)占用2个字节,共16位,INT8(8位的定点整数)八位整型,占用1个字节等。
混合精度(Mixed precision)指使用FP32和FP16。使用FP16 可以减少模型一半内存,但有些参数必须采用FP32才能保持模型性能。
虽然INT8精度低,但是数据量小、能耗低,计算速度相对更快,更符合端侧运算的特点。
不同精度进行量化的归程中,量化误差不可避免。
在模型训练阶段,梯度的更新往往是很微小的,需要相对较高的精度,一般要用到FP32以上。在inference的阶段,精度要求没有那么高,一般F16或者INT8就足够了,精度影响不会很大。同时低精度的模型占用空间更小了,有利于部署在端侧设备中。
GPU在训练时有两个重要指标可以查看,即显存占用和GPU利用率。
显存指的是GPU的空间,即内存大小。显存可以用来放模型,数据等。
GPU 利用率主要的统计方式为:在采样周期内,GPU 上有 kernel 执行的时间百分比。可以简单理解为GPU计算单元的使用率。
Float32 是在深度学习中最常用的数值类型,称为单精度浮点数,每一个单精度浮点数占用4Byte的显存。
在整个神经网络训练周期中,在GPU上的显存占用主要包括:数据,模型参数,模型输出等。
数据侧:举个,一个323128128的四维矩阵,其占用的显存 = 323128128*4 /1000 / 1000 = 6.3M
模型侧:占用显存的层包括卷积层,全连接层,BN层,梯度,优化器的参数等。
输出侧:占用的显存包括网络每一层计算出来的feature map以及对应的梯度等。
我在之前专门沉淀了一篇关于算法模型部署逻辑的文章,大家可以直接进行阅读取用:
【CV算法上下游】系列之浅谈算法模型部署逻辑
AI端侧设备多聚焦于深度学习算法模型的加速与赋能,而传统图像算法在没有加速算子赋能的情况下,在AI端侧设备无法发挥最优的性能。
首先,假设卷积核的尺寸是,有个特征图作为输入,每个输出的特征图大小为,输出为个特征图。
由于模型参数量主要由卷积,全连接层,BatchNorm层等部分组成,我们以卷积的参数量为例进行参数量的计算分析:
卷积核参数量:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-485De99m-1662211893346)(data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==)]
偏置参数量:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-22FRWjHW-1662211893347)(data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==)]
总体参数量:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5c1dAvbR-1662211893348)(data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==)]
同样,我们假设卷积核的尺寸是,有个特征图作为输入,每个输出的特征图大小为,输出为个特征图。
由于在模型中卷积一般占计算量的比重是最高的,我们依旧以卷积的计算量为例进行分析:
FLOPS(全大写):是floating point operations per second的缩写,意指每秒浮点运算次数,理解为计算速度。是一个衡量硬件性能的指标。
FLOPs(s小写):是floating point operations的缩写(s表示复数),意指浮点运算数,理解为计算量。可以用来衡量算法/模型的复杂度。
针对模型的计算量应该指的是FLOPs。
在上述情况下,卷积神经网络一次前向传播需要的乘法运算次数为:
同时,所要进行的加法计算次数分为考虑偏置和不考虑偏置:
(1)考虑偏置的情况:
为了得到输出的特征图的一个未知的像素,我们需要进行
次加法操作,其中大小的卷积操作需要次加法,由于有C个通道,所以需要将结果乘以C,每个通道间的数要相加,所以需要C - 1次加法,最后再加上偏置的1次加法。
所以总的加法计算量如下:
所以总的卷积运算计算量(乘法+加法):
(2)不考虑偏置的情况:
总的卷积计算量:
图像二值化( Image Binarization)是将图像像素点的灰度值设置为0或255,也就是将整个图像呈现出明显的黑白效果的过程。二值图像每个像素只有两种取值:要么纯黑,要么纯白。
通过二值图像,将感兴趣目标和背景分离,能更好地分析物体的形状和轮廓。
图像二值化的方法有很多,其中最经典的就是采用阈值法(Thresholding)进行二值化。阈值法是指选取一个数字,大于它就设为白色,小于它就设为黑色。根据阈值选取方式的不同,又可以分为全局阈值和局部阈值。全局阈值(Global Method),指的是对整个图像中的每一个像素都选用相同的阈值。局部阈值(Local Method)又称自适应阈值(Adaptive Thresholding)。局部阈值法假定图像在一定区域内受到的光照比较接近。它用一个滑窗扫描图像,并取滑窗中心点亮度与滑窗内其他区域(称为邻域, neighborhood area)的亮度进行比较。如果中心点亮度高于邻域亮度,则将中心点设为白色,否则设为黑色。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KdOeCfye-1662211893350)(data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==)]
图像的膨胀(dilation)和腐蚀(erosion)是两种基本的形态学运算,主要用来寻找图像中的极大区域和极小区域。
膨胀类似于“领域扩张”,将图像的高亮区域或白色部分进行扩张,其运行结果图比原图的高亮区域更大。
腐蚀类似于“领域被蚕食”,将图像中的高亮区域或白色部分进行缩减细化,其运行结果图比原图的高亮区域更小。
图像为什么要滤波呢?一是为了消除图像在数字化过程中产生或者混入的噪声。二是为了提取图片对象的特征作为图像识别的特征模式。
什么是高斯噪声?首先,噪声在图像当中常表现为引起较强视觉效果的孤立像素点或像素块。简单来说,噪声的出现会给图像带来干扰,让图像变得不清楚。高斯噪声就是它的概率密度函数服从高斯分布(即正态分布)的一类噪声。如果一个噪声,它的幅度分布服从高斯分布,而它的功率谱密度又是均匀分布的,则称它为高斯白噪声。
高斯滤波是一种线性平滑滤波,可以用来消除高斯噪声。其公式如下所示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Gdjx45gL-1662211893350)(data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==)]
高斯滤波过程:假设高斯核:
那么高斯滤波计算过程就如下所示:
将这9个值加起来,就是中心点的高斯滤波的值。对所有点重复这个过程,就得到了高斯模糊后的图像。
二维高斯滤波能否分解为一维操作?可以进行分解,二维高斯滤波分解为两次一维高斯滤波,高斯二维公式可以推导为X轴与Y轴上的一维高斯公式。即使用一维高斯核先对图像逐行滤波,再对中间结果逐列滤波。
图像边缘是图像最基本的特征,指图像局部特征的不连续性。图像特征信息的突变处称之为边缘,例如灰度级的突变,颜色的突变,纹理结构的突变等。边缘是一个区域的结束,也是另一个区域的开始,利用该特征可以分割图像。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qmXRvMNq-1662211893353)(data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==)]
当我们看到一个有边缘的物体时,首先感受到的就是边缘。
上图(a)是一个理想的边缘所具备的特性。每个灰度级跃变到一个垂直的台阶上。而实际上,在图像采集系统的性能、采样率和获取图像的照明条件等因素的影响,得到的边缘往往是模糊的,边缘被模拟成具有“斜坡面”的剖面,如上图(b)所示,在这个模型中,模糊的边缘变得“宽”了,而清晰的边缘变得“窄”了。
图像的边缘有方向和幅度两种属性。边缘通常可以通过一阶导数或二阶导数检测得到。一阶导数是以最大值作为对应的边缘的位置,而二阶导数则以过零点作为对应边缘的位置。
常用的一阶导数边缘算子:Roberts算子、Sobel算子和Prewitt算子。
常用的二阶导数边缘算子:Laplacian 算子,此类算子对噪声敏感。
其他边缘算子:前面两类均是通过微分算子来检测图像边缘,还有一种就是Canny算子,其是在满足一定约束条件下推导出来的边缘检测最优化算子。
滤波操作是一种非常实用的图像数据预处理方法。滤波是一个信号处理领域的概念,而图像本身也可以看成是一个二维的信号,其中像素点数值的高低代表信号的强弱。
其中图像信息可以分为高频和低频两个维度:
高频:图像中灰度变化剧烈的点,一般是图像轮廓或者是噪声。
低频:图像中平坦的、变化不大的点,也就是图像中的大部分区域。
根据图像的高频与低频的特征,我们可以设计相应的高通与低通滤波器,高通滤波器可以检测保留图像中尖锐的、变化明显的地方。而低通滤波器可以让图像变得更光滑,去除图像中的噪声。
常见的低通滤波器有:线性的均值滤波器、高斯滤波器、非线性的双边滤波器、中值滤波器。
常见的高通滤波器有:Canny算子、Sobel算子、拉普拉斯算子等边缘滤波算子。
低频信息(低频分量):表示图像中灰度值变化缓慢的区域,对应着图像中大块平坦的区域。
高频信息(高频分量):表示图像中灰度值变化剧烈的区域,对应着图像的边缘(轮廓)、噪声(之所以说噪声也是高频分量,是因为图像噪声在大部分情况下都是高频的)以及细节部分。
低频分量主要对整幅图像强度的综合度量。高频分量主要对图像边缘和轮廓的度量(人眼对高频分量比较敏感)。
傅立叶变换角度理解:从傅立叶变换的角度,我们可以将图像从灰度分布转化为频率分布。图像进行傅立叶变换之后得到的频谱图,就是图像梯度的分布图。具体来说,傅立叶频谱图上我们能看到明暗不一的亮点,实际上就是图像上某一点与领域点差异的强弱,即梯度的大小。如果一幅图像的各个位置的强度大小相等,则图像只存在低频分量。从图像的频谱图上看,只有一个主峰,且位于频率为零的位置。如果一幅图像的各个位置的强度变化剧烈,则图像不仅存在低频分量,同时也存在多种高频分量。从图像的频谱上看,不仅有一个主峰,同时也存在多个旁峰。图像中的低频分量就是图像中梯度较小的部分,高频分量则相反。
色深(Color Depth)指的是色彩的深度,即精细度。在数字图像中,最小的单元是像素,在RGB三通道图像中,每个像素都由R,G,B三个通道组成,通常是24位的二进制位格式来表示。这表示颜色的2进制位数,就代表了色深。
空间平滑(模糊)技术是广泛应用于图像处理以降低图像噪声的技术。空间平滑技术可以分为两大类:局部平滑(Local Smoothing)和非局部平滑(Non-local Smoothing) 局部平滑方法利用附近的像素来平滑每个像素。通过设计不同的加权机制,产生了很多经典的局部平滑方法,例如高斯Smoothing,中值Smoothing,均值Smooyhing等。
而非局部平滑方法不限于附近的像素,而是使用图像全局中普遍存在的冗余信息进行去噪。具体来说,以较大的图像块为单位在图像中寻找相似区域,再对这些区域求平均,并对中心图像块进行替换,能够较好地去掉图像中的噪声。在平均操作中,可以使用高斯,中位数以及均值等对相似图像块进行加权。
RAW格式: 从相机传感器端获取的原始数字格式的数据, 又称为Bayer格式. 每个像素信息只有RGB中的某个颜色信息, 且每4个像素中有2个像素为G信息,1个R信息,1个B信息, 即GRBG格式。
RGB格式: RGB格式是由RAW数据插值计算后获取的、每个像素均包含了RGB三种颜色的信息。
深度学习中常用的色彩空间格式:RGB,RGBA,HSV,HLS,Lab,YCbCr,YUV等。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pG9kizkk-1662211893353)(data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==)]
RGB色彩空间以Red(红)、Green(绿)、Blue(蓝)三种基本色为基础,进行不同程度的叠加,产生丰富而广泛的颜色,所以俗称三基色模式。
RGBA是代表Red(红)、Green(绿)、Blue(蓝)和Alpha(透明度)的色彩空间。
HSV色彩空间(Hue-色调、Saturation-饱和度、Value-亮度)将亮度从色彩中分解出来,在图像增强算法中用途很广。
HLS色彩空间,三个分量分别是色相(H)、亮度(L)、饱和度(S)。
Lab色彩空间是由CIE(国际照明委员会)制定的一种色彩模式。自然界中任何一点色都可以在Lab空间中表达出来,它的色彩空间比RGB空间还要大。
YCbCr进行了图像子采样,是视频图像和数字图像中常用的色彩空间。在通用的图像压缩算法中(如JPEG算法),首要的步骤就是将图像的颜色空间转换为YCbCr空间。
YUV色彩空间与RGB编码方式(色域)不同。RGB使用红、绿、蓝三原色来表示颜色。而YUV使用亮度、色度来表示颜色。
模型训练时Resize图像常用的插值算法有:最近邻插值,双线性插值以及双三次插值等。
最近邻插值:没考虑其他相邻像素点的影响,因而重新采样后灰度值有明显的不连续性,图像质量损失较大,存在马赛克和锯齿现象。
双线性插值:也叫一阶插值,它是利用了待求像素点在源图像中4个最近邻像素之间的相关性,通过两次线性插值得到待求像素点的值。
双三次插值:也叫立方卷积插值,它是利用了待求像素点在源图像中相邻的16个像素点的值,即这16个像素点的加权平均。
一般先对数据进行归一化(Normalization)处理【0,1】,再进行标准化(Standardization)操作,用大数定理将数据转化为一个标准正态分布,最后再进行一些数据增强处理。
归一化后,可以提升模型精度。不同维度之间的特征在数值上有一定比较性,可以大大提高分类器的准确性。标准化后,可以加速模型收敛。最优解的寻优过程明显会变得平缓,更容易正确的收敛到最优解。
1.man:man command,可以查看某个命令的帮助文档,按q退出帮助文档
2.cd:用于切换目录,cd - 可以在最近两次目录之间来回切换
3.touch:touch file创建文件。
4.ls:ls -lh可以列出当前目录下文件的详细信息。
5.pwd:pwd命令以绝对路径的方式显示用户当前的工作目录
6.cat:cat file显示文件内容。
7.mkdir:mkdir dir可以创建一个目录;mkdir -p dir/xxx/xxx可以递归创建目录。
8.cat:cat file显示文件内容,按q退出。
9.more:more file显示文件内容,按q退出。
10.grep:筛选命令,比如我想查找当前目录下的py文件(ls -lh | grep .py)
11.whereis:可以查找含有制定关键字的文件,如whereis python3
重定向 > 和 >>:Linux 允许将命令执行结果重定向到一个文件,将本应显示在终端上的内容输出/追加到指定文件中。其中>表示输出,会覆盖原有文件;>>表示追加,会将内容追加到已有文件的末尾。
12.cp:cp dst1 dst2复制文件;cp -r dst1 dst2复制文件夹。
13.mv:mv dst1 dst2可以移动文件、目录,也可以给文件或目录重命名。
14.zip:zip file.zip file压缩文件;zip dir.zip -r dir压缩文件夹。
15.unzip:unzip file.zip解压由zip命令压缩的.zip文件。
16.tar:
tar -cvf file.tar dir打包文件夹
tar -xvf file.tar解包
tar -czvf file.tar.gz dir压缩文件夹
tar -zxvf file.tar.gz解压
17.chmod:chmod -R 777 data将整个data文件夹修改为任何人可读写。
18.ps:ps aux列出所有进程的详细信息。
19.kill:kill PID根据PID杀死进程。
20.df:df -h 查看磁盘空间。
21.du:du -h dir查看文件夹大小。
22.top:实时查看系统的运行状态,如 CPU、内存、进程的信息。
23wget:wget url从指定url下载文件。
24.ln:ln -s dst1 dst2建立文件的软链接,类似于windows的快捷方式;ln dst1 dst2建立文件的硬链接。无论哪种链接,dst1都最好使用绝对路径。
25.top:我们可以使用top命令实时的对系统处理器的状态进行监视。
26.apt-get:用于安装,升级和清理包。
27.vim:对文件内容进行编辑。
28.nvidia-smi:对GPU使用情况进行查看。
29.nohup sh test.sh &:程序后台运行且不挂断。
30.find:这个命令用于查找文件,功能强大。find . -name "*.c"表示查找当前目录及其子目录下所有扩展名是.c的文件。
进程和线程的基本概念:
进程:是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态的概念,竞争计算机系统资源的基本单位。
线程:是进程的一个执行单元,是进程内的调度实体。比进程更小的独立运行的基本单位。线程也被称为轻量级进程。
一个程序至少一个进程,一个进程至少一个线程。
线程的意义:
每个进程都有自己的地址空间,即进程空间,在网络环境下,一个服务器通常需要接收不确定数量用户的并发请求,为每一个请求都创建一个进程显然行不通(系统开销大响应用户请求效率低),因此操作系统中线程概念被引进。线程的执行过程是线性的,尽管中间会发生中断或者暂停,但是进程所拥有的资源只为该线状执行过程服务,一旦发生线程切换,这些资源需要被保护起来。进程分为单线程进程和多线程进程,单线程进程宏观来看也是线性执行过程,微观上只有单一的执行过程。多线程进程宏观是线性的,微观上多个执行操作。
进程和线程的区别:
地址空间:同一进程中的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
资源拥有:同一进程内的线程共享进程的资源如内存、I/O、CPU等,但是进程之间的资源是独立的。(一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃可能导致整个进程都死掉。所以多进程比多线程健壮。进程切换时,消耗的资源大、效率差。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程。)
执行过程:每个独立的线程都有一个程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。(线程是基于进程的)
线程是处理器调度的基本单元,但进程不是。
两者均可并发执行。
进程和线程的优缺点:
线程执行开销小,但是不利于资源的管理和保护。
进程执行开销大,但是能够很好的进行资源管理和保护。
何时使用多进程,何时使用多线程:
对资源的管理和保护要求高,不限制开销和效率时,使用多进程。(CPU密集型任务)
要求效率高,频繁切换时,资源的保护管理要求不是很高时,使用多线程。(I/O密集型任务)
TCP/IP四层模型:
四层模型逻辑:
发送端是由上至下,把上层来的数据在头部加上各层协议的数据(部首)再下发给下层。
接受端则由下而上,把从下层接收到的数据进行解密和去掉头部的部首后再发送给上层。
层层加密和解密后,应用层最终拿到了需要的数据。