【三年面试五年模拟】算法工程师的求职面试秘籍(持续更新中)

【三年面试五年模拟】算法工程师的求职面试秘籍(持续更新中)

码字实在不易,希望大家能点赞支持!


大家好,我是Rocky。

当今AI行业的算法岗位日益饱和,不管是校招还是社招,获得算法岗offer的难度都大大增加,内卷严重。

虽然2022年AIGC浪潮到来,开启了AIGC时代, 但是AIGC时代所带来的岗位红利在海量的过剩人才海洋中,也是杯水车薪 。那么,我们该怎么办呢?

Don't worry, Rocky相信机会是留给有准备的人的,所以Rocky撰写梳理了“三年面试五年模拟”之算法工程师的求职面试“独孤九剑”秘籍,希望能给大家的算法岗求职路上带来更多助力 。只要我们能够有计划的准备,持之以恒,就一定能收获心仪的算法岗offer!

Rocky将字节,腾讯,阿里,百度,华为,网易,京东,美团,拼多多,快手,小米,微软,360,海康,大华,滴滴,商汤,旷视,依图等 50家大厂的AI算法岗相关的面试知识点进行梳理总结 ,筛选出高频核心面试考点,撰写成“三年面试五年模拟”之算法工程师的求职面试“独孤九剑”秘籍。

Rocky不仅从面试角度出发,同样兼顾了工业界实际业务(传统深度学习,自动驾驶,AIGC)的真实情况,筛选出来的高频面试知识点,就算不用于招聘面试准备,平时也可以当作巩固基础知识之用,让我们在实际工作中更能得心应手。

下面是Rocky梳理出来的算法工程师面试的 完整体系框架 ,包含了算法工程师需要具备的所有知识维度,方便大家针对性查漏补缺:

Rocky希望“独孤九剑”秘籍能让江湖中的英雄豪杰们获益。

So,enjoy:

面试资源分享

Rocky除了撰写“三年面试五年模拟”之算法工程师的求职面试“独孤九剑”秘籍,还收集整理了很多高价值面试资源,能帮助大家查漏补缺,提升能力:

大家可以关注Rocky的公众号 WeThinkIn, 后台回复: 面试资料 ,即可获得资源链接,包含 上述全套资源 ,帮助大家从0到1快速学习提升算法工程师的基本面能力。

深度学习基础

【一】卷积有什么特点?

卷积主要有 三大特点

  1. 局部连接。比起全连接,局部连接会大大减少网络的参数。在二维图像中,局部像素的关联性很强,设计局部连接保证了卷积网络对图像局部特征的强响应能力。
  2. 权值共享。参数共享也能减少整体参数量,增强了网络训练的效率。一个卷积核的参数权重被整张图片共享,不会因为图像内位置的不同而改变卷积核内的参数权重。
  3. 下采样。下采样能逐渐降低图像分辨率,实现了数据的降维,并使浅层的局部特征组合成为深层的特征。下采样还能使计算资源耗费变少,加速模型训练,也能有效控制过拟合。

【二】不同层次的卷积都提取什么类型的特征?

  1. 浅层卷积 \rightarrow 提取边缘特征
  2. 中层卷积 \rightarrow 提取局部特征
  3. 深层卷积 \rightarrow 提取全局特征

【三】卷积核大小如何选取?

最常用的是 3*3 大小的卷积核,两个 3 * 3 卷积核和一个 5 * 5 卷积核的感受野相同,但是减少了参数量和计算量,加快了模型训练。与此同时由于卷积核的增加,模型的非线性表达能力大大增强。

不过大卷积核( 7 * 7,9 * 9 )也有使用的空间,在GAN,图像超分辨率,图像融合等领域依然有较多的应用,大家可按需切入感兴趣的领域查看相关论文。

【四】卷积感受野的相关概念

目标检测和目标跟踪很多模型都会用到RPN层,anchor是RPN层的基础,而感受野(receptive field,RF)是anchor的基础。

感受野的作用:

  1. 一般来说感受野越大越好,比如分类任务中最后卷积层的感受野要大于输入图像。
  2. 感受野足够大时,被忽略的信息就较少。
  3. 目标检测任务中设置anchor要对齐感受野,anchor太大或者偏离感受野会对性能产生一定的影响。

感受野计算:

增大感受野的方法:

  1. 使用空洞卷积
  2. 使用池化层
  3. 增大卷积核

【五】网络每一层是否只能用一种尺寸的卷积核?

常规的神经网络一般每层仅用一个尺寸的卷积核,但同一层的特征图可以分别使用多个不同尺寸的卷积核,以获得不同尺度的特征,再把这些特征结合起来,得到的特征往往比使用单一尺寸卷积核的要好,如GoogLeNet 、Inception系列的网络,均是每层使用了多个不同的卷积核结构。如下图所示,输入的特征图在同一层分别经过 1*1,3*3和5*5 三种不同尺寸的卷积核,再将各自的特征图进行整合,得到的新特征可以看作不同感受野提取的特征组合,相比于单一尺寸卷积核会有更强的表达能力。

【六】1 * 1卷积的作用?

1 * 1 卷积的作用主要有以下几点:

  1. 实现特征信息的交互与整合。
  2. 对特征图通道数进行升维和降维,降维时可以减少参数量。
  3. 1*1 卷积+ 激活函数 \rightarrow 增加非线性,提升网络表达能力。
升维与降维
1 * 1卷积在GoogLeNet中的应用

1 * 1 卷积首发于NIN(Network in Network),后续也在GoogLeNet和ResNet等网络中使用。感兴趣的朋友可追踪这些论文研读细节。

【七】转置卷积的作用?

转置卷积通过训练过程学习到最优的上采样方式,来代替传统的插值上采样方法,以提升图像分割,图像融合,GAN等特定任务的性能。

转置卷积并不是卷积的反向操作,从信息论的角度看,卷积运算是不可逆的。转置卷积可以将输出的特征图尺寸恢复卷积前的特征图尺寸,但不恢复原始数值。

转置卷积的计算公式:

我们设卷积核尺寸为 K*K ,输入特征图为 i * i

(1)当 stride = 1,padding = 0 时:

输入特征图在进行转置卷积操作时相当于进行了 padding = K - 1 的填充,接着再进行正常卷积转置之后的标准卷积运算。

输出特征图的尺寸 = i + (K - 1)

(2)当 stride > 1,padding = 0 时:

输入特征图在进行转置卷积操作时相当于进行了 padding = K - 1 的填充,相邻元素间的空洞大小为 stride - 1 ,接着再进行正常卷积转置之后的标准卷积运算。

输出特征图的尺寸 = stride * (i - 1) + K

【八】空洞卷积的作用?

空洞卷积的作用是在不进行池化操作损失信息的情况下,增大感受野,让每个卷积输出都包含较大范围的信息。

空洞卷积有一个参数可以设置dilation rate,其在卷积核中填充dilation rate个0,因此,当设置不同dilation rate时,感受野就会不一样,也获取了多尺度信息。

(a) 图对应3x3的1-dilated conv,和普通的卷积操作一样。(b)图对应 3*3 的2-dilated conv,实际的卷积kernel size还是 3*3 ,但是空洞为 1 ,也就是对于一个 7*7 的图像patch,只有 9 个红色的点和 3*3 的kernel发生卷积操作,其余的点的权重为 0 。(c)图是4-dilated conv操作。

【九】全连接层的作用?

全连接层将卷积学习到的高维特征映射到label空间,可以作为整个网络的分类器模块。

虽然全连接层参数存在冗余的情况,但是在模型进行迁移学习时,其能保持较大的模型capacity。

目前很多模型使用全局平均池化(GAP)取代全连接层以减小模型参数,并且依然能达到SOTA的性能。

【十】CNN中池化的作用?

池化层的作用是对感受野内的特征进行选择,提取区域内最具代表性的特征,能够有效地减少输出特征数量,进而减少模型参数量。按操作类型通常分为最大池化(Max Pooling)、平均池化(Average Pooling)和求和池化(Sum Pooling),它们分别提取感受野内最大、平均与总和的特征值作为输出,最常用的是最大池化和平均池化。

【十一】有哪些方法能提升CNN模型的泛化能力?

  1. 采集更多数据:数据决定算法的上限。
  2. 优化数据分布:数据类别均衡。
  3. 选用合适的目标函数。
  4. 设计合适的网络结构。
  5. 数据增强。
  6. 权值正则化。
  7. 使用合适的优化器等。

【十二】BN层面试高频问题大汇总

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 u 值的平均值;然后对于标准偏差采用每个batch σ_B 的无偏估计。

在测试时,BN使用的公式是:

BN训练时为什么不用整个训练集的均值和方差?

因为用整个训练集的均值和方差容易过拟合,对于BN,其实就是对每一batch数据标准化到一个相同的分布,而不同batch数据的均值和方差会有一定的差别,而不是固定的值,这个差别能够增加模型的鲁棒性,也会在一定程度上减少过拟合。

BN层用在哪里?

在CNN中,BN层应该用在非线性激活函数前面。由于神经网络隐藏层的输入是上一层非线性激活函数的输出,在训练初期其分布还在剧烈改变,此时约束其一阶矩和二阶矩无法很好地缓解 Covariate Shift;而BN的分布更接近正态分布,限制其一阶矩和二阶矩能使输入到激活函数的值分布更加稳定。

BN层的参数量

我们知道 γ β 是需要学习的参数,而BN的本质就是利用优化学习改变方差和均值的大小。在CNN中,因为网络的特征是对应到一整张特征图上的,所以做BN的时候也是以特征图为单位而不是按照各个维度。比如在某一层,特征图的数量为 c ,那么做BN的参数量为 c * 2

BN的优缺点

优点:

  1. 可以选择较大的初始学习率。因为这个算法收敛很快。
  2. 可以不用dropout,L2正则化。
  3. 不需要使用局部响应归一化。
  4. 可以把数据集彻底打乱。
  5. 模型更加健壮。

缺点:

  1. Batch Normalization非常依赖Batch的大小,当Batch值很小时,计算的均值和方差不稳定。
  2. 所以BN不适用于以下几个场景:小Batch,RNN等。

【十三】什么是转置卷积的棋盘效应?

造成棋盘效应的原因是转置卷积的不均匀重叠(uneven overlap)。这种重叠会造成图像中某个部位的颜色比其他部位更深。

在下图展示了棋盘效应的形成过程,深色部分代表了不均匀重叠:

棋盘效应

接下来我们将卷积步长改为2,可以看到输出图像上的所有像素从输入图像中接收到同样多的信息,它们都从输入图像中接收到一个像素的信息,这样就不存在转置卷带来的重叠区域。

我们也可以直接进行插值Resize操作,然后再进行卷积操作来消除棋盘效应。这种方式在超分辨率重建场景中比较常见。例如使用双线性插值和近邻插值等方法来进行上采样。

【十四】Instance Normalization的作用?

Instance Normalization(IN)和Batch Normalization(BN)一样,也是Normalization的一种方法,只是IN是作用于单张图片,而BN作用于一个Batch。

BN对Batch中的每一张图片的同一个通道一起进行Normalization操作,而IN是指单张图片的单个通道单独进行Normalization操作。如下图所示,其中C代表通道数,N代表图片数量(Batch)。

IN适用于生成模型中,比如图片风格迁移。因为图片生成的结果主要依赖于某个图像实例,所以对整个Batch进行Normalization操作并不适合图像风格化的任务,在风格迁移中使用IN不仅可以加速模型收敛,并且可以保持每个图像实例之间的独立性。

下面是IN的公式:

其中t代表图片的index,i代表的是feature map的index。

【十五】什么是有效感受野?

感受野的相关知识在之前的文章 【三年面试五年模拟】算法工程师的独孤九剑秘籍(前六式汇总篇) 中介绍过。

我们接着再看看有效感受野(effective receptive field, ERF)的相关知识。

一般而言,feature map上有效感受野要小于实际感受野。其有效性,以中心点为基准,类似高斯分布向边缘递减。

总的来说,感受野主要描述feature map中的最大信息量,有效感受野则主要描述信息的有效性。

【十六】全局池化的作用?

全局池化主要包括全局平均池化和全局最大池化。

全局最大池化
全局平均池化

接下来,Rocky以全局平均池化为例,讲述其如何在深度学习网络中发挥作用。

刚才已经讲过,全局平均池化就是对最后一层卷积的特征图,每个通道求整个特征图的均值。如下图所示:

全局平均池化

一般网络的最后会再接几个全连接层,但全局池化后的feature map相当于一像素,所以最后的全连接其实就成了一个加权相加的操作。这种结构比起直接的全连接更加直观,参数量大大幅下降,并且泛化性能更好:

全局池化的作用:

  1. 代替全连接层,降低参数量。
  2. 减少过拟合,增加泛化能力。

【十七】深度学习中有哪些经典的优化器?

SGD(随机梯度下降)

随机梯度下降的优化算法在科研和工业界是很常用的。

很多理论和工程问题都能转化成对目标函数进行最小化的数学问题。

举个例子:梯度下降(Gradient Descent)就好比一个人想从高山上奔跑到山谷最低点,用最快的方式奔向最低的位置。

SGD的公式:

动量(Momentum)公式:

基本的mini-batch SGD优化算法在深度学习取得很多不错的成绩。然而也存在一些问题需解决:

  1. 选择恰当的初始学习率很困难。
  2. 学习率调整策略受限于预先指定的调整规则。
  3. 相同的学习率被应用于各个参数。
  4. 高度非凸的误差函数的优化过程,如何避免陷入大量的局部次优解或鞍点。

AdaGrad(自适应梯度)

AdaGrad优化算法(Adaptive Gradient,自适应梯度),它能够对每个不同的参数调整不同的学习率,对频繁变化的参数以更小的步长进行更新,而稀疏的参数以更大的步长进行更新。

AdaGrad公式:

g_{t,i} 表示t时刻的 \theta_{i} 梯度。

G_{t,ii} 表示t时刻参数 \theta_{i} 的梯度平方和。

与SGD的核心区别在于计算更新步长时,增加了分母:梯度平方累积和的平方根。此项能够累积各个参数 \theta_{i} 的历史梯度平方,频繁更新的梯度,则累积的分母逐渐偏大,那么更新的步长相对就会变小,而稀疏的梯度,则导致累积的分母项中对应值比较小,那么更新的步长则相对比较大。

AdaGrad能够自动为不同参数适应不同的学习率(平方根的分母项相当于对学习率α进进行了自动调整,然后再乘以本次梯度),大多数的框架实现采用默认学习率α=0.01即可完成比较好的收敛。

优势: 在数据分布稀疏的场景,能更好利用稀疏梯度的信息,比标准的SGD算法更有效地收敛。

缺点: 主要缺陷来自分母项的对梯度平方不断累积,随时间的增加,分母项越来越大,最终导致学习率收缩到太小无法进行有效更新。

RMSProp

RMSProp结合梯度平方的指数移动平均数来调节学习率的变化。能够在不稳定的目标函数情况下进行很好地收敛。

计算t时刻的梯度:

计算梯度平方的指数移动平均数(Exponential Moving Average), \gamma 是遗忘因子(或称为指数衰减率),依据经验,默认设置为0.9。

梯度更新的时候,与AdaGrad类似,只是更新的梯度平方的期望(指数移动均值),其中 \varepsilon = 10^{-8} ,避免除数为0。默认学习率 \alpha = 0.001

优势: 能够克服AdaGrad梯度急剧减小的问题,在很多应用中都展示出优秀的学习率自适应能力。尤其在不稳定(Non-Stationary)的目标函数下,比基本的SGD、Momentum、AdaGrad表现更良好。

Adam

Adam优化器结合了AdaGrad和RMSProp两种优化算法的优点。对梯度的一阶矩估计(First Moment Estimation,即梯度的均值)和二阶矩估计(Second Moment Estimation,即梯度的未中心化的方差)进行综合考虑,计算出更新步长。

Adam的优势:

  1. 实现简单,计算高效,对内存需求少。
  2. 参数的更新不受梯度的伸缩变换影响。
  3. 超参数具有很好的解释性,且通常无需调整或仅需很少的微调。
  4. 更新的步长能够被限制在大致的范围内(初始学习率)。
  5. 能自然地实现步长退火过程(自动调整学习率)。
  6. 很适合应用于大规模的数据及参数的场景。
  7. 适用于不稳定目标函数。
  8. 适用于梯度稀疏或梯度存在很大噪声的问题。

Adam的实现原理:

计算t时刻的梯度:

然后计算梯度的指数移动平均数, m_{0} 初始化为0。

类似于Momentum算法,综合考虑之前累积的梯度动量。

\beta_{1} 系数为指数衰减率,控制动量和当前梯度的权重分配,通常取接近于1的值。默认为0.9。

接着,计算梯度平方的指数移动平均数, v_{0} 初始化为0。

\beta_{2} 系数为指数衰减率,控制之前的梯度平方的影响情况。默认为0.999。

类似于RMSProp算法,对梯度平方进行加权均值。

由于 m_{0} 初始化为0,会导致 m_{t} 偏向于0,尤其在训练初期阶段。

所以,此处需要对梯度均值 m_{t} 进行偏差纠正,降低偏差对训练初期的影响。

同时 v_{0} 也要进行偏差纠正:

最后总的公式如下所示:

其中默认学习率 \alpha = 0.001 \varepsilon = 10^{-8} 避免除数变为0。

从表达式中可以看出,对更新的步长计算,能够从梯度均值和梯度平方两个角度进行自适应地调节,而不是直接由当前梯度决定。

Adam的不足:

虽然Adam算法目前成为主流的优化算法,不过在很多领域里(如计算机视觉的图像识别、NLP中的机器翻译)的最佳成果仍然是使用带动量(Momentum)的SGD来获取到的。

【十八】有哪些提高GAN训练稳定性的Tricks?

1.输入Normalize

  1. 将输入图片Normalize到 [-1,1] 之间。
  2. 生成器最后一层的输出使用Tanh激活函数。

Normalize非常重要,没有处理过的图片是没办法收敛的。图片Normalize一种简单的方法是(images-127.5)/127.5,然后送到判别器去训练。同理生成的图片也要经过判别器,即生成器的输出也是-1到1之间,所以使用Tanh激活函数更加合适。

2.替换原始的GAN损失函数和标签反转

  1. 原始GAN损失函数会出现训练早期梯度消失和Mode collapse(模型崩溃)问题。可以使用Earth Mover distance(推土机距离)来优化。
  2. 实际工程中用反转标签来训练生成器更加方便,即把生成的图片当成real的标签来训练,把真实的图片当成fake来训练。

3.使用具有球形结构的随机噪声 Z 作为输入

  1. 不要使用均匀分布进行采样
  1. 使用高斯分布进行采样


4.使用BatchNorm

  1. 一个mini-batch中必须只有real数据或者fake数据,不要把他们混在一起训练。
  2. 如果能用BatchNorm就用BatchNorm,如果不能用则用instance normalization。

5.避免使用ReLU,MaxPool等操作引入稀疏梯度

  1. GAN的稳定性会因为引入稀疏梯度受到很大影响。
  2. 最好使用类LeakyReLU的激活函数。(D和G中都使用)
  3. 对于下采样,最好使用:Average Pooling或者卷积+stride。
  4. 对于上采样,最好使用:PixelShuffle或者转置卷积+stride。

最好去掉整个Pooling逻辑,因为使用Pooling会损失信息,这对于GAN训练没有益处。

6.使用Soft和Noisy的标签

  1. Soft Label,即使用 [0.7-1.2] [0-0.3] 两个区间的随机值来代替正样本和负样本的Hard Label。
  2. 可以在训练时对标签加一些噪声,比如随机翻转部分样本的标签。

7.使用Adam优化器

  1. Adam优化器对于GAN来说非常有用。
  2. 在生成器中使用Adam,在判别器中使用SGD。

8.追踪训练失败的信号

  1. 判别器的损失=0说明模型训练失败。
  2. 如果生成器的损失稳步下降,说明判别器没有起作用。

9.在输入端适当添加噪声

  1. 在判别器的输入中加入一些人工噪声。
  2. 在生成器的每层中都加入高斯噪声。

10.生成器和判别器差异化训练

  1. 多训练判别器,尤其是加了噪声的时候。

11.Two Timescale Update Rule (TTUR)

对判别器和生成器使用不同的学习速度。使用较低的学习率更新生成器,判别器使用较高的学习率进行更新。

12. Gradient Penalty (梯度惩罚)

使用梯度惩罚机制可以极大增强 GAN 的稳定性,尽可能减少mode collapse问题的产生。

13. Spectral Normalization(谱归一化)

Spectral normalization可以用在判别器的weight normalization技术,可以确保判别器是K-Lipschitz连续的。

14. 使用多个GAN结构

可以使用多个GAN/多生成器/多判别器结构来让GAN训练更稳定,提升整体效果,解决更难的问题。

【十九】深度学习炼丹可以调节的一些超参数?

  1. 预处理(数据尺寸,数据Normalization)
  2. Batch-Size
  3. 学习率
  4. 优化器
  5. 损失函数
  6. 激活函数
  7. Epoch
  8. 权重初始化
  9. NAS网络架构搜索

【二十】滑动平均的相关概念

滑动平均(exponential moving average),或者叫做指数加权平均(exponentially weighted moving avergae),可以用来估计变量的局部均值,使得变量的更新与一段时间内的历史取值有关。

变量 v t 时刻记为 v_{t} \theta_{t} 为变量 v t 时刻训练后的取值,当不使用滑动平均模型时 v_{t} = \theta_{t} ,在使用滑动平均模型后, v_{t} 的更新公式如下:

上式中, \beta\epsilon[0,1) \beta = 0 相当于没有使用滑动平均。

t 时刻变量 v 的滑动平均值大致等于过去 1/(1-\beta) 个时刻 \theta 值的平均。并使用bias correction将 v_{t} 除以 (1 - \beta^{t}) 修正对均值的估计。

加入Bias correction后, v_{t} v_{biased_{t}} 的更新公式如下:

t 越大, 1 - \beta^{t} 越接近1,则公式(1)和(2)得到的结果( v_{t} v_{biased_{1}} )将越来越接近。

\beta 越大时,滑动平均得到的值越和 \theta 的历史值相关。如果 \beta = 0.9 ,则大致等于过去10个 \theta 值的平均;如果 \beta = 0.99 ,则大致等于过去100个 \theta 值的平均。

下图代表不同方式计算权重的结果:

如上图所示,滑动平均可以看作是变量的过去一段时间取值的均值,相比对变量直接赋值而言,滑动平均得到的值在图像上更加平缓光滑,抖动性更小,不会因为某种次的异常取值而使得滑动平均值波动很大。

滑动平均的优势: 占用内存少,不需要保存过去10个或者100个历史 \theta 值,就能够估计其均值。滑动平均虽然不如将历史值全保存下来计算均值准确,但后者占用更多内存,并且计算成本更高。

为什么滑动平均在测试过程中被使用?

滑动平均可以使模型在测试数据上更鲁棒(robust)。

采用随机梯度下降算法训练神经网络时,使用滑动平均在很多应用中都可以在一定程度上提高最终模型在测试数据上的表现。

训练中对神经网络的权重 weights 使用滑动平均,之后在测试过程中使用滑动平均后的 weights 作为测试时的权重,这样在测试数据上效果更好。因为滑动平均后的 weights 的更新更加平滑,对于随机梯度下降而言,更平滑的更新说明不会偏离最优点很远。比如假设decay=0.999,一个更直观的理解,在最后的1000次训练过程中,模型早已经训练完成,正处于抖动阶段,而滑动平均相当于将最后的1000次抖动进行了平均,这样得到的权重会更加鲁棒。

【二十一】Spectral Normalization的相关知识

Spectral Normalization是一种wegiht Normalization技术,和weight-clipping以及gradient penalty一样,也是让模型满足1-Lipschitz条件的方式之一。

Lipschitz(利普希茨)条件限制了函数变化的剧烈程度,即函数的梯度,来确保统计的有界性。因此函数更加平滑,在神经网络的优化过程中,参数变化也会更稳定,不容易出现梯度爆炸。

Lipschitz条件的约束如下所示:

\frac{||f(x) -f(x^\prime)||_{2}}{||x - x^\prime||_{2}} \leqslant K \\

其中 K 代表一个常数,即利普希茨常数。若 K=1 ,则是1-Lipschitz。

在GAN领域,Spectral Normalization有很多应用。在WGAN中,只有满足1-Lipschitz约束时,W距离才能转换成较好求解的对偶问题,使得WGAN更加从容的训练。

如果想让矩阵A映射: R^{n}\to R^{m} 满足K-Lipschitz连续,K的最小值为 \sqrt{\lambda_{1}} ( \lambda_{1} A_TA 的最大特征值),那么要想让矩阵A满足1-Lipschitz连续,只需要在A的所有元素上同时除以 \sqrt{\lambda_{1}} (Spectral norm)。

Spectral Normalization实际上在做的事,是将每层的参数矩阵除以自身的最大奇异值,本质上是一个逐层SVD的过程,但是真的去做SVD就太耗时了,所以采用幂迭代的方法求解。过程如下图所示:

幂迭代法流程

得到谱范数 \sigma_l(W) 后,每个参数矩阵上的参数皆除以它,以达到Normalization的目的。

【二十二】激活函数的作用,常用的激活函数有哪些?

激活函数的作用

激活函数可以引入非线性因素,提升网络的学习表达能力。

常用的激活函数

Sigmoid 激活函数

函数的定义为:

f(x) = \frac{1}{1 + e^{-x}} \\

如下图所示,其值域为 (0,1) 。也就是说,输入的每个神经元、节点都会被缩放到一个介于 0 1 之间的值。

x 大于零时输出结果会趋近于 1 ,而当 x 小于零时,输出结果趋向于 0 ,由于函数的特性,经常被用作二分类的输出端激活函数。

Sigmoid的导数:

f^{'}(x)=(\frac{1}{1+e^{-x}})^{'}=\frac{1}{1+e^{-x}}\left( 1- \frac{1}{1+e^{-x}} \right)=f(x)(1-f(x)) \\

x=0 时, f(x)'=0.25

Sigmoid的优点:

  1. 平滑
  2. 易于求导
  3. 可以作为概率,辅助解释模型的输出结果

Sigmoid的缺陷:

  1. 当输入数据很大或者很小时,函数的梯度几乎接近于0,这对神经网络在反向传播中的学习非常不利。
  2. Sigmoid函数的均值不是0,这使得神经网络的训练过程中只会产生全正或全负的反馈。
  3. 导数值恒小于1,反向传播易导致梯度消失。
Sigmoid导数示意图,两边梯度几乎为0

Tanh激活函数

Tanh函数的定义为:

f(x) = Tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}} \\

如下图所示,值域为 (-1,1)

Tanh的优势:

  1. Tanh函数把数据压缩到-1到1的范围,解决了Sigmoid函数均值不为0的问题,所以在实践中通常Tanh函数比Sigmoid函数更容易收敛。在数学形式上其实Tanh只是对Sigmoid的一个缩放形式,公式为 tanh(x) = 2f(2x) -1 f(x) 是Sigmoid的函数)。
  2. 平滑
  3. 易于求导

Tanh的导数:

f^{'}(x)=(\frac{e^x - e^{-x}}{e^x + e^{-x}})^{'}=1-(tanh(x))^2 \\

x=0 时, f(x)'=1

由Tanh和Sigmoid的导数也可以看出Tanh导数更陡,收敛速度比Sigmoid快。

Tanh导数示意图

Tanh的缺点:

导数值恒小于1,反向传播易导致梯度消失。

Relu激活函数

Relu激活函数的定义为:

f(x) = max(0, x) \\

如下图所示,值域为 [0,+∞)

ReLU的优势:

  1. 计算公式非常简单,不像上面介绍的两个激活函数那样涉及成本更高的指数运算,大量节约了计算时间。
  2. 在随机梯度下降中比Sigmoid和Tanh更加容易使得网络收敛。
  3. ReLU进入负半区的时候,梯度为0,神经元此时会训练形成单侧抑制,产生稀疏性,能更好更快地提取稀疏特征。
  4. Sigmoid和Tanh激活函数的导数在正负饱和区的梯度都会接近于0,这会造成梯度消失,而ReLU函数大于0部分都为常数保持梯度不衰减,不会产生梯度消失现象。

稀疏:在神经网络中,这意味着激活的矩阵含有许多0。这种稀疏性能让我们得到什么?这能提升时间和空间复杂度方面的效率,常数值所需空间更少,计算成本也更低。

ReLU的导数:

c(u)=\begin{cases} 0,x<0 \\ 1,x>0 \\ undefined,x=0\end{cases} \\

通常 x=0 时,给定其导数为 1 0

ReLU的导数

ReLU的不足:

  1. 训练中可能会导致出现某些神经元永远无法更新的情况。其中一种对ReLU函数的改进方式是LeakyReLU。
  2. ReLU不能避免梯度爆炸问题。

LeakyReLU激活函数

LeakyReLU激活函数定义为:

f(x) = \left\{ \begin{aligned} ax, \quad x<0 \\ x, \quad x\ge0 \end{aligned} \right.\\

如下图所示( a = 0.5 ),值域为 (-∞,+∞)

LeakyReLU的优势:

该方法与ReLU不同的是在 x 小于0的时候取 f(x) = ax ,其中 a 是一个非常小的斜率(比如0.01)。这样的改进可以使得当 x 小于0的时候也不会导致反向传播时的梯度消失现象。

LeakyReLU的不足:

  1. 无法避免梯度爆炸的问题。
  2. 神经网络不学习 \alpha 值。
  3. 在求导的时候,两部分都是线性的。

SoftPlus激活函数

SoftPlus激活函数的定义为:

f(x) = ln( 1 + e^x) \\

值域为 (0,+∞)

函数图像如下:

可以把SoftPlus看作是ReLU的平滑。

ELU激活函数

ELU激活函数解决了ReLU的一些问题,同时也保留了一些好的方面。这种激活函数要选取一个 \alpha 值,其常见的取值是在0.1到0.3之间。

函数定义如下所示:

f(x) = \left\{ \begin{aligned} a(e^x -1), \quad x<0 \\ x, \quad x\ge0 \end{aligned} \right.\\

如果我们输入的 x 值大于 0 ,则结果与ReLU一样,即 y 值等于 x 值;但如果输入的 x 值小于 0 ,则我们会得到一个稍微小于 0 的值,所得到的 y 值取决于输入的 x 值,但还要兼顾参数 \alpha ——可以根据需要来调整这个参数。公式进一步引入了指数运算 e^x ,因此ELU的计算成本比ReLU高。

下面给出了 \alpha 值为0.2时的ELU函数图:

ELU函数图

ELU的导数:

ELU的导数公式

导数图如下所示:

ELU的导数图

ELU的优势:

  1. 能避免ReLU中一些神经元无法更新的情况。
  2. 能得到负值输出。

ELU的不足:

  1. 包含指数运算,计算时间长。
  2. 无法避免梯度爆炸问题。
  3. 神经网络无法学习 \alpha 值。

【二十三】反向传播算法(BP)的概念及简单推导

反向传播(Backpropagation,BP)算法是一种与最优化方法(如梯度下降法)结合使用的,用来训练人工神经网络的常见算法。BP算法对网络中所有权重计算损失函数的梯度,并将梯度反馈给最优化方法,用来更新权值以最小化损失函数。该算法会先按前向传播方式计算(并缓存)每个节点的输出值,然后再按反向传播遍历图的方式计算损失函数值相对于每个参数的偏导数。

接下来我们以全连接层,使用sigmoid激活函数,Softmax+MSE作为损失函数的神经网络为例,推导BP算法逻辑。由于篇幅限制,这里只进行简单推导,后续Rocky将专门写一篇PB算法完整推导流程,大家敬请期待。

首先,我们看看sigmoid激活函数的表达式及其导数:

sigmoid表达式:\sigma(x) = \frac{1}{1+e^{-x}} \\ sigmoid导数:\frac{d}{dx}\sigma(x) = \sigma(x) - \sigma(x)^2 = \sigma(1- \sigma) \\

可以看到sigmoid激活函数的导数最终可以表达为输出值的简单运算。

我们再看MSE损失函数的表达式及其导数:

MSE损失函数的表达式:L = \frac{1}{2}\sum^{K}_{k=1}(y_k - o_k)^2 \\

其中 y_k 代表ground truth(gt)值, o_k 代表网络输出值。

MSE损失函数的偏导:\frac{\partial L}{\partial o_i} = (o_i - y_i) \\

由于偏导数中单且仅当 k = i 时才会起作用,故进行了简化。

接下来我们看看全连接层输出的梯度:

MSE损失函数的表达式:L = \frac{1}{2}\sum^{K}_{i=1}(o_i^1 - t_i)^2 \\ MSE损失函数的偏导:\frac{\partial L}{\partial w_{jk}} = (o_k - t_k)o_k(1-o_k)x_j \\

我们用 \delta_k = (o_k - t_k)o_k(1-o_k) ,则能再次简化:

MSE损失函数的偏导:\frac{dL}{dw_{jk}} = \delta_kx_j \\

最后,我们看看那PB算法中每一层的偏导数:

输出层:

\frac{\partial L}{\partial w_{jk}} = \delta_k^K o_j \\ \delta_k^K = (o_k - t_k)o_k(1-o_k) \\

倒数第二层:

\frac{\partial L}{\partial w_{ij}} = \delta_j^J o_i \\ \delta_j^J = o_j(1 - o_j) \sum_{k}\delta_k^Kw_{jk} \\

倒数第三层:

\frac{\partial L}{\partial w_{ni}} = \delta_i^I o_n \\ \delta_i^I = o_i(1 - o_i) \sum_{j}\delta_j^Jw_{ij} \\

像这样依次往回推导,再通过梯度下降算法迭代优化网络参数,即可走完PB算法逻辑。

【二十四】分组卷积的相关知识

分组卷积(Group Convolution)最早出现在AlexNet网络中,分组卷积被用来切分网络,使其能在多个GPU上并行运行。

分组卷积和普通卷积的区别

普通卷积进行运算的时候,如果输入feature map尺寸是 C\times H \times W ,卷积核有N个,那么输出的feature map与卷积核的数量相同也是N个,每个卷积核的尺寸为 C\times K \times K ,N个卷积核的总参数量为 N \times C \times K \times K

分组卷积的主要对输入的feature map进行分组,然后每组分别进行卷积。如果输入feature map尺寸是 C\times H \times W ,输出feature map的数量为 N 个,如果我们设定要分成G个group,则每组的输入feature map数量为 \frac{C}{G} ,则每组的输出feature map数量为 \frac{N}{G} ,每个卷积核的尺寸为 \frac{C}{G} \times K \times K ,卷积核的总数仍为N个,每组的卷积核数量为 \frac{N}{G} ,卷积核只与其同组的输入map进行卷积,卷积核的总参数量为 N \times \frac{C}{G} \times K \times K ,易得总的参数量减少为原来的 \frac{1}{G}

分组卷积的作用:

  1. 分组卷积可以减少参数量。
  2. 分组卷积可以看成是稀疏操作,有时可以在较少参数量的情况下获得更好的效果(相当于正则化操作)。
  3. 当分组数量等于输入feature map通道数量,输出feature map数量也等于输入feature map数量时,分组卷积就成了Depthwise卷积,可以使参数量进一步缩减。

经典模型&&热门模型

【一】目标检测中IOU的相关概念与计算

IoU(Intersection over Union)即交并比,是目标检测任务中一个重要的模块,其是GT bbox与pred bbox交集的面积 / 二者并集的面积。

下面我们用坐标(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的相关概念与计算

在目标检测中,我们可以利用非极大值抑制(NMS)对生成的大量候选框进行后处理,去除冗余的候选框,得到最具代表性的结果,以加快目标检测的效率。

如下图所示,消除多余的候选框,找到最佳的bbox:

非极大值抑制(NMS)流程:

  1. 首先我们需要设置两个值:一个Score的阈值,一个IOU的阈值。
  2. 对于每类对象,遍历该类的所有候选框,过滤掉Score值低于Score阈值的候选框,并根据候选框的类别分类概率进行排序: A < B < C < D < E < F
  3. 先标记最大概率矩形框F是我们要保留下来的候选框。
  4. 从最大概率矩形框F开始,分别判断A~E与F的交并比(IOU)是否大于IOU的阈值,假设B、D与F的重叠度超过IOU阈值,那么就去除B、D。
  5. 从剩下的矩形框A、C、E中,选择概率最大的E,标记为要保留下来的候选框,然后判断E与A、C的重叠度,去除重叠度超过设定阈值的矩形框。
  6. 就这样重复下去,直到剩下的矩形框没有了,并标记所有要保留下来的矩形框。
  7. 每一类处理完毕后,返回步骤二重新处理下一类对象。
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

【三】One-stage目标检测与Two-stage目标检测的区别?

Two-stage目标检测算法:先进行区域生成(region proposal,RP)(一个有可能包含待检物体的预选框),再通过卷积神经网络进行样本分类。其精度较高,速度较慢。

主要逻辑:特征提取—>生成RP—>分类/定位回归。

常见的Two-stage目标检测算法有:Faster R-CNN系列和R-FCN等。

One-stage目标检测算法:不用RP,直接在网络中提取特征来预测物体分类和位置。其速度较快,精度比起Two-stage算法稍低。

主要逻辑:特征提取—>分类/定位回归。

常见的One-stage目标检测算法有:YOLO系列、SSD和RetinaNet等。

【四】哪些方法可以提升小目标检测的效果?

  1. 提高图像分辨率。小目标在边界框中可能只包含几个像素,那么能通过提高图像的分辨率以增加小目标的特征的丰富度。
  2. 提高模型的输入分辨率。这是一个效果较好的通用方法,但是会带来模型inference速度变慢的问题。
  3. 平铺图像。
  1. 数据增强。小目标检测增强包括随机裁剪、随机旋转和镶嵌增强等。
  2. 自动学习anchor。
  3. 类别优化。

【五】ResNet模型的特点以及解决的问题?

每次回答这个问题的时候,都会包含我的私心,我喜欢从电气自动化的角度去阐述,而非计算机角度,因为这会让我想起大学时代的青葱岁月。

ResNet就是一个差分放大器。ResNet的结构设计,思想逻辑,就是在机器学习中抽象出了一个差分放大器,其能让深层网络的梯度的相关性增强,在进行梯度反传时突出微小的变化。

模型的特点则是设计了残差结构,其对模型输出的微小变化非常敏感。

为什么加入残差模块会有效果呢?

假设:如果不使用残差模块,输出为 F_{1} (x)= 5.1 ,期望输出为 H_{1} (x)= 5 ,如果想要学习H函数,使得 F_{1} (x) = H_{1} (x) = 5 ,这个变化率比较低,学习起来是比较困难的。

但是如果设计为 H_{1} (x) = F_{1} (x) + 5 = 5.1 ,进行一种拆分,使得 F_{1} (x)= 0.1 ,那么学习目标就变为让 F_{1} (x)= 0 ,一个映射函数学习使得它输出由0.1变为0,这个是比较简单的。也就是说引入残差模块后的映射对输出变化更加敏感了。

进一步理解:如果 F_{1} (x)= 5.1 ,现在继续训练模型,使得映射函数 F_{1} (x)= 5 。变化率为: (5.1 - 5) / 5.1 = 0.02 ,如果不用残差模块的话可能要把学习率从0.01设置为0.0000001。层数少还能对付,一旦层数加深的话可能就不太好使了。

这时如果使用残差模块,也就是 F_{1} (x)= 0.1 变化为 F_{1} (x)= 0 。这个变化率增加了100%。明显这样的话对参数权重的调整作用更大。

【六】ResNeXt模型的结构和特点?

ResNeXt模型是在ResNet模型的基础上进行优化。其主要是在ResNeXt中引入Inception思想。如下图所示,左侧是ResNet经典结构,右侧是ResNeXt结构,其将单路卷积转化成多支路的多路卷积,进行分组卷积。

ResNet与ResNeXt结构对比

作者进一步提出了ResNeXt的三种等价结构,其中c结构中的分组卷积思想就跃然纸上了。

最后我们看一下ResNeXt50和ResNet50结构上差异的对比图:

ResNeXt论文:《Aggregated Residual Transformations for Deep Neural Networks》

【七】MobileNet系列模型的结构和特点?

MobileNet是一种轻量级的网络结构,主要针对手机等嵌入式设备而设计。MobileNetv1网络结构在VGG的基础上使用depthwise Separable卷积,在保证不损失太大精度的同时,大幅降低模型参数量。

Depthwise separable卷积是由Depthwise卷积和Pointwise卷积构成。 Depthwise卷积(DW)能有效减少参数数量并提升运算速度。但是由于每个特征图只被一个卷积核卷积,因此经过DW输出的特征图只包含输入特征图的全部信息,而且特征之间的信息不能进行交流,导致“信息流通不畅”。Pointwise卷积(PW)实现通道特征信息交流,解决DW卷积导致“信息流通不畅”的问题。

Depthwise separable卷积

Depthwise Separable卷积和标准卷积的计算量对比:

Depthwise Separable卷积:标准卷积

相比标准卷积,Depthwise Separable卷积可以大幅减小计算量。并且随着卷积通道数的增加,效果更加明显。

并且Mobilenetv1使用stride=2的卷积替换池化操作,直接在卷积时利用stride=2完成了下采样,从而节省了卷积后再去用池化操作去进行一次下采样的时间,可以提升运算速度。

MobileNetv1论文:《MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications》

【八】MobileNet系列模型的结构和特点?(二)

MobileNetV2在MobileNetV1的基础上引入了Linear Bottleneck 和 Inverted Residuals。

MobileNetV2使用Linear Bottleneck(线性变换)来代替原本的非线性激活函数,来捕获感兴趣的流形。实验证明,使用Linear Bottleneck可以在小网络中较好地保留有用特征信息。

Inverted Residuals与经典ResNet残差模块的通道间操作正好相反。由于MobileNetV2使用了Linear Bottleneck结构,使其提取的特征维度整体偏低,如果只是使用低维的feature map效果并不会好。如果卷积层都是使用低维的feature map来提取特征的话,那么就没有办法提取到整体的足够多的信息。如果想要提取全面的特征信息的话,我们就需要有高维的feature map来进行补充,从而达到平衡。

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(Vision Transformer)模型的结构和特点?

ViT模型特点: 1.ViT直接将标准的Transformer结构直接用于图像分类,其模型结构中不含CNN。 2.为了满足Transformer输入结构要求,输入端将整个图像拆分成小图像块,然后将这些小图像块的线性嵌入序列输入网络中。在最后的输出端,使用了Class Token形式进行分类预测。 3.Transformer比CNN结构少了一定的平移不变性和局部感知性,在数据量较少的情况下,效果可能不如CNN模型,但是在大规模数据集上预训练过后,再进行迁移学习,可以在特定任务上达到SOTA性能。

ViT的整体模型结构:

其可以具体分成如下几个部分:

  1. 图像分块嵌入
  2. 多头注意力结构
  3. 多层感知机结构(MLP)
  4. 使用DropPath,Class Token,Positional Encoding等操作。

【十】EfficientNet系列模型的结构和特点?

Efficientnet系列模型是通过grid search从深度(depth)、宽度(width)、输入图片分辨率(resolution)三个角度共同调节搜索得来的模型。其从EfficientNet-B0到EfficientNet-L2版本,模型的精度越来越高,同样,参数量和对内存的需求也会随之变大。

深度模型的规模主要是由宽度、深度、分辨率这三个维度的缩放参数决定的。这三个维度并不是相互独立的,对于输入的图片分辨率更高的情况,需要有更深的网络来获得更大的感受视野。同样的,对于更高分辨率的图片,需要有更多的通道来获取更精确的特征。

EfficientNet模型搜索逻辑

EfficientNet模型的内部是通过多个MBConv卷积模块实现的,每个MBConv卷积模块的具体结构如下图所示。其用实验证明Depthwise Separable卷积在大模型中依旧非常有效;Depthwise Separable卷积较于标准卷积有更好的特征提取表达能力。

另外论文中使用了Drop_Connect方法来代替传统的Dropout方法来防止模型过拟合。DropConnect与Dropout不同的地方是在训练神经网络模型过程中,它不是对隐层节点的输出进行随机的丢弃,而是对隐层节点的输入进行随机的丢弃。

EfficientNet论文:《EfficientNet: Rethinking Model Scaling for Convolutional Neural Networks》

说点题外话,隔着paper都能看到作者那窒息的调参过程。。。

【十一】面试常问的经典模型?

面试中经常会问一些关于模型方面的问题,这也是不太好量化定位的问题,因为模型繁杂多样,面试官问哪个都有可能,下面的逻辑图里我抛砖引玉列出了一些不管是在学术界还是工业界都是高价值的模型,供大家参考。

最好还是多用项目,竞赛,科研等工作润色简历,并在面试过程中将模型方面的问题引向这些工作中用到的熟悉模型里。

【十二】Focal Loss的作用?

Focal Loss是解决了分类问题中类别不均衡、分类难度差异的一个损失函数,使得模型在训练过程中更加聚焦在困难样本上。

Focal Loss是从二分类问题出发,同样的思想可以迁移到多分类问题上。

我们知道二分类问题的标准loss是 交叉熵


对于二分类问题我们也几乎适用sigmoid激活函数 \hat{y} = \sigma(x) ,所以上面的式子可以转化成:


这里有 1 - \sigma(x) = \sigma(-x)

Focal Loss论文中给出的式子如下:

其中 y\in \{ 1,-1\} 是真实标签, p\in[0,1] 是预测概率。

我们再定义 p_{t}:

那么,上面的交叉熵的式子可以转换成:

有了上面的铺垫,最初Focal Loss论文中接着引入了 均衡交叉熵函数

针对类别不均衡问题,在Loss里加入一个控制权重,对于属于少数类别的样本,增大 \alpha_{t} 即可。但这样有一个问题,它仅仅解决了正负样本之间的平衡问题,并没有区分易分/难分样本。

为什么上述公式只解决正负样本不均衡问题呢?

因为增加了一个系数 \alpha_{t} ,跟 p_{t} 的定义类似,当 label=1 的时候 \alpha_{t}=\alpha ;当 label=-1 的时候, \alpha_{t}= 1 - \alpha \alpha 的范围也是 [0,1] 。因此可以通过设定 \alpha 的值(如果 1 这个类别的样本数比 -1 这个类别的样本数少很多,那么 \alpha 可以取 0.5 1 来增加 1 这个类的样本的权重)来控制正负样本对整体Loss的贡献。

Focal Loss

为了可以区分难/易样本,Focal Loss雏形就出现了:

(1 - p_{t})^{\gamma} 用于平衡难易样本的比例不均, \gamma >0 起到了对 (1 - p_{t}) 的放大作用。 \gamma >0 减少易分样本的损失,使模型更关注于困难易错分的样本。例如当 \gamma =2 时,模型对于某正样本预测置信度 p_{t} 0.9 ,这时 (1 - 0.9)^{\gamma} = 0.01 ,也就是FL值变得很小;而当模型对于某正样本预测置信度 p_{t} 为0.3时, (1 - 0.3)^{\gamma} = 0.49 ,此时它对Loss的贡献就变大了。当 \gamma = 0 时变成交叉熵损失。

为了应对正负样本不均衡的问题,在上面的式子中再加入平衡交叉熵的 \alpha_{t} 因子,用来平衡正负样本的比例不均,最终得到Focal Loss:

Focal Loss论文中给出的实验最佳取值为 a_{t}= 0.25 \gamma = 2

【十三】YOLO系列的面试问题

Rocky之前总结了YOLOv1-v7全系列的解析文章,帮助大家应对可能出现的与YOLO相关的面试问题,大家可按需取用:

【十四】有哪些经典的轻量型人脸检测模型?

人脸检测相对于通用目标检测来说,算是一个子任务。比起通用目标检测任务动辄检测1000个类别,人脸检测任务主要聚焦于人脸的单类目标检测,使用通用目标检测模型太过奢侈,有点“杀鸡用牛刀”的感觉,并且大量的参数冗余,会影响部署侧的实用性,故针对人脸检测任务,学术界提出了很多轻量型的人脸检测模型,Rocky在这里给大家介绍一些比较有代表性的:

  1. libfacedetection
  2. Ultra-Light-Fast-Generic-Face-Detector-1MB
  3. A-Light-and-Fast-Face-Detector-for-Edge-Devices
  4. CenterFace
  5. DBFace
  6. RetinaFace
  7. MTCNN

【十五】LFFD人脸检测模型的结构和特点?

Rocky在实习/校招面试中被多次问到LFFD模型以及面试官想套取LFFD相关算法方案的情况,说明LFFD模型在工业界还是比较有价值的,下面Rocky就带着大家学习一下LFFD模型的知识:

LFFD(A-Light-and-Fast-Face-Detector-for-Edge-Devices)适用于人脸、行人、车辆等单目标检测任务,具有速度快,模型小,效果好的特点。LFFD是Anchor-free的方法,使用感受野替代Anchors,并在主干结构上抽取8路特征图对从小到大的人脸进行检测,检测模块分为类别二分类与边界框回归。

LFFD模型结构

我们可以看到,LFFD模型主要由四部分组成:tiny part、small part、medium part、large part。

模型中并没有采用BN层,因为BN层会减慢17%的推理速度。其主要采用尽可能快的下采样来保持100%的人脸覆盖。

LFFD主要特点:

  1. 结构简单直接,易于在主流AI端侧设备中进行部署。
  2. 检测小目标能力突出,在极高分辨率(比如8K或更大)画面,可以检测其间10个像素大小的目标;

LFFD损失函数

LFFD损失函数是由regression loss和classification loss的加权和。

分类损失使用了交叉熵损失。

回归损失使用了L2损失函数。

LFFD论文地址: LFFD: A Light and Fast Face Detector for Edge Devices论文地址

【十六】U-Net模型的结构和特点?

U-Net网络结构如下所示:

U-Net网络结构

U-Net网络的特点:

  1. 全卷积神经网络:使用 1\times1 卷积完全取代了全连接层,使得模型的输入尺寸不受限制。
  2. 左半部分网络是收缩路径(contracting path):使用卷积和max pooling层,对feature map进行下采样。
  3. 右半部分网络是扩张路径(expansive path):使用转置卷积对feature map进行上采样,并将其与收缩路径对应层产生的特征图进行concat操作。上采样可以补充特征信息,加上与左半部分网络收缩路径的特征图进行concat(通过crop操作使得两个特征图尺寸一致),这就相当于在高分辨率和高维特征当中做一个融合折中。
  4. U-Net提出了让人耳目一新的编码器-解码器整体结构,让U-Net充满了生命力与强适应性。

U-Net在医疗图像,缺陷检测以及交通场景中有非常丰富的应用,可以说图像分割实际场景,U-Net是当仁不让的通用Baseline。

U-Net的论文地址: U-Net

【十七】RepVGG模型的结构和特点?

RepVGG模型的基本架构由20多层 3\times3 卷积组成,分成5个stage,每个stage的第一层是stride=2的降采样,每个卷积层用ReLU作为激活函数。

RepVGG的主要特点:

  1. 3\times3 卷积在GPU上的计算密度(理论运算量除以所用时间)可达1x1和5x5卷积的四倍.
  2. 直筒型单路结构的计算效率比多路结构高。
  3. 直筒型单路结构比起多路结构内存占用少。
  4. 单路架构灵活性更好,容易进一步进行模型压缩等操作。
  5. RepVGG中只含有一种算子,方便芯片厂商设计专用芯片来提高端侧AI效率。

那么是什么让RepVGG能在上述情形下达到SOTA效果呢?

答案就是结构重参数化(structural re-parameterization)。

结构重参数化逻辑

在训练阶段,训练一个多分支模型,并将多分支模型等价转换为单路模型。在部署阶段,部署单路模型即可。这样就可以同时利用多分支模型训练时的优势(性能高)和单路模型推理时的好处(速度快、省内存)。

更多结构重参数化细节知识将在后续的篇章中展开介绍,大家尽情期待!

【十八】GAN的核心思想?

2014年,Ian Goodfellow第一次提出了GAN的概念。Yann LeCun曾经说过:“生成对抗网络及其变种已经成为最近10年以来机器学习领域最为重要的思想之一”。GAN的提出让生成式模型重新站在了深度学习这个浪潮的璀璨舞台上,与判别式模型开始谈笑风生。

GAN由生成器 G 和判别器 D 组成。其中,生成器主要负责生成相应的样本数据,输入一般是由高斯分布随机采样得到的噪声 Z 。而判别器的主要职责是区分生成器生成的样本与 gt(GroundTruth) 样本,输入一般是 gt 样本与相应的生成样本,我们想要的是对 gt 样本输出的置信度越接近 1 越好,而对生成样本输出的置信度越接近 0 越好。与一般神经网络不同的是,GAN在训练时要同时训练生成器与判别器,所以其训练难度是比较大的。

在提出GAN的第一篇论文中,生成器被比喻为印假钞票的犯罪分子,判别器则被当作警察。犯罪分子努力让印出的假钞看起来逼真,警察则不断提升对于假钞的辨识能力。二者互相博弈,随着时间的进行,都会越来越强。在图像生成任务中也是如此,生成器不断生成尽可能逼真的假图像。判别器则判断图像是 gt 图像,还是生成的图像。二者不断博弈优化,最终生成器生成的图像使得判别器完全无法判别真假。

GAN的对抗思想主要由其目标函数实现。具体公式如下所示:

上面这个公式看似复杂,其实不然。跳出细节来看, 整个公式的核心逻辑其实就是一个min-max问题,深度学习数学应用的边界扩展到这里,GAN便开始发光了

接着我们再切入细节。我们可以分两部分开看这个公式,即判别器最小化角度与生成器最大化角度。在判别器角度,我们希望最大化这个目标函数,因为在公示第一部分,其表示 gt 样本 (x ~Pdata) 输入判别器后输出的置信度,当然是越接近 1 越好。而公式的第二部分表示生成器输出的生成样本 (G(z)) 再输入判别器中进行进行二分类判别,其输出的置信度当然是越接近 0 越好,所以 1 - D(G(z)) 越接近 1 越好。

在生成器角度, 我们想要最小化判别器目标函数的最大值 。判别器目标函数的最大值代表的是真实数据分布与生成数据分布的JS散度,JS散度可以度量分布的相似性,两个分布越接近,JS散度越小(JS散度是在初始GAN论文中被提出,实际应用中会发现有不足的地方,后来的论文陆续提出了很多的新损失函数来进行优化)

写到这里,大家应该就明白GAN的对抗思想了,下面是初始GAN论文中判别器与生成器损失函数的具体设置以及训练的具体流程:

在图中可以看出,将判别器损失函数离散化,其与交叉熵的形式一致,我们也可以说判别器的目标是最小化交叉熵损失。

【十九】面试常问的经典GAN模型?

  1. 原始GAN及其训练逻辑
  2. DCGAN
  3. CGAN
  4. WGAN
  5. LSGAN
  6. PixPix系列
  7. CysleGAN
  8. SRGAN系列

【二十】FPN(Feature Pyramid Network)的相关知识

FPN的创新点

  1. 设计特征金字塔的结构
  2. 提取多层特征(bottom-up,top-down)
  3. 多层特征融合(lateral connection)

设计特征金字塔的结构,用于解决目标检测中的多尺度问题,在基本不增加原有模型计算量的情况下,大幅度提升小物体(small object)的检测性能。

原来很多目标检测算法都是只采用高层特征进行预测,高层的特征语义信息比较丰富,但是分辨率较低,目标位置比较粗略。假设在深层网络中,最后的高层特征图中一个像素可能对应着输出图像 20 \times 20 的像素区域,那么小于 20 \times 20 像素的小物体的特征大概率已经丢失。与此同时,低层的特征语义信息比较少,但是目标位置准确,这是对小目标检测有帮助的。FPN将高层特征与底层特征进行融合,从而同时利用低层特征的高分辨率和高层特征的丰富语义信息,并进行了多尺度特征的独立预测,对小物体的检测效果有明显的提升。

FPN结构

传统解决这个问题的思路包括:

  1. 图像金字塔(image pyramid),即多尺度训练和测试。但该方法计算量大,耗时较久。
  2. 特征分层,即每层分别输出对应的scale分辨率的检测结果,如SSD算法。但实际上不同深度对应不同层次的语义特征,浅层网络分辨率高,学到更多是细节特征,深层网络分辨率低,学到更多是语义特征,单单只有不同的特征是不够的。

FPN的主要模块

  1. Bottom-up pathway(自底向上线路)
  2. Top-down path(自顶向下线路)
  3. Lareral connections(横向链路)

Bottom-up pathway(自底向上线路)

自底向上线路是卷积网络的前向传播过程。在前向传播过程中,feature map的大小可以在某些层发生改变。

Top-down path(自顶向下线路)和Lareral connections(横向链路)

自顶向下线路是上采样的过程,而横向链路是将自顶向下线路的结果和自底向上线路的结构进行融合。

上采样的feature map与相同大小的下采样的feature map进行逐像素相加融合(element-wise addition),其中自底向上的feature先要经过 1\times 1 卷积层,目的是为了减少通道维度。

FPN应用

论文中FPN直接在Faster R-CNN上进行改进,其backbone是ResNet101,FPN主要应用在Faster R-CNN中的RPN和Fast R-CNN两个模块中。

FPN+RPN:

将FPN和RPN结合起来,那RPN的输入就会变成多尺度的feature map,并且在RPN的输出侧接多个RPN head层用于满足对anchors的分类和回归。

FPN+Fast R-CNN:

Fast R-CNN的整体结构逻辑不变,在backbone部分引入FPN思想进行改造。

【二十一】SPP(Spatial Pyramid Pooling)的相关知识

在目标检测领域,很多检测算法最后使用了全连接层,导致输入尺寸固定。当遇到尺寸不匹配的图像输入时,就需要使用crop或者warp等操作进行图像尺寸和算法输入的匹配。这两种方式可能出现不同的问题:裁剪的区域可能没法包含物体的整体;变形操作造成目标无用的几何失真等。

而SPP的做法是在卷积层后增加一个SPP layer,将features map拉成固定长度的feature vector。然后将feature vector输入到全连接层中。以此来解决上述的尴尬问题。

crop/warp与SPP的区别

SPP的优点:

  1. SPP可以忽略输入尺寸并且产生固定长度的输出。
  2. SPP使用多种尺度的滑动核,而不是只用一个尺寸的滑动窗口进行pooling。
  3. SPP在不同尺寸feature map上提取特征,增大了提取特征的丰富度。
SPP逻辑图

在YOLOv4中,对SPP进行了创新使用,Rocky已在 【Make YOLO Great Again】YOLOv1-v7全系列大解析(Neck篇) 中详细讲解,大家可按需取用~

【二十二】目标检测中AP,AP50,AP75,mAP等指标的含义

AP:PR曲线下的面积。

PR曲线

AP50: 固定IoU为50%时的AP值。

AP75:固定IoU为75%时的AP值。

AP@[0.5:0.95]:把IoU的值从50%到95%每隔5%进行了一次划分,并对这10组AP值取平均。

mAP:对所有的类别进行AP的计算,然后取均值。

mAP@[.5:.95](即mAP@[.5,.95]):表示在不同IoU阈值(从0.5到0.95,步长0.05)(0.5、0.55、0.6、0.65、0.7、0.75、0.8、0.85、0.9、0.95)上的平均mAP。

【二十三】YOLOv2中的anchor如何生成?

YOLOv2中引入K-means算法进行anchor的生成,可以自动找到更好的anchor宽高的值用于模型训练的初始化。

但如果使用经典K-means中的欧氏距离作为度量,意味着较大的Anchor会比较小的Anchor产生更大的误差,聚类结果可能会偏离。

由于目标检测中主要关心anchor与ground true box(gt box)的IOU,不关心两者的大小。因此,使用IOU作为度量更加合适,即提高IOU值。因此YOLOv2采用IOU值为评判标准:

d(gt box,anchor) = 1 - IOU(gt box,anchor) \\

具体anchor生成步骤与经典K-means大致相同,在下一个章节中会详细介绍。主要的不同是使用的度量是 d(gt box,anchor) ,并将anchor作为簇的中心。

机器学习基础

【一】什么是模型的偏差和方差?

误差(Error)= 偏差(Bias) + 方差(Variance) + 噪声(Noise),一般地,我们把机器学习模型的预测输出与样本的真实label之间的差异称为误差,其反应的是整个模型的准确度。

噪声(Noise):描述了在当前任务上任何机器学习算法所能达到的期望泛化误差的下界,即刻画了当前任务本质的难度。

偏差(Bias):衡量了模型拟合训练数据的能力,偏差反应的是所有采样得到的大小相同的训练集训练出的所有模型的输出平均值和真实label之间的偏差,即模型本身的精确度。

偏差通常是由于我们对机器学习算法做了错误的假设所导致的,比如真实数据分布映射的是某个二次函数,但我们假设模型是一次函数。

偏差(Bias)越小,拟合能力却强(可能产生过拟合);反之,拟合能力越弱(可能产生欠拟合)。偏差越大,越偏离真实数据。

方差描述的是预测值的变化范围,离散程度,也就是离期望值的距离。方差越大,数据的分布越分散,模型的稳定程度越差。

方差也反应了模型每一次输出结果与模型输出期望之间的误差,即模型的稳定性。由方差带来的误差通常体现在测试误差相对于训练误差的增量上。

方差通常是由于模型的复杂度相对于训练样本数过高导致的。方差越小,模型的泛化能力越高;反之,模型的泛化能力越低。

如果模型在训练集上拟合效果比较优秀,但是在测试集上拟合效果比较差,则表示方差较大,说明模型的稳定程度较差,出现这种现象可能是由于模型对训练集过拟合造成的。

接下来我们用下面的射击的例子进一步解释这二者的区别。假设一次射击就是机器学习模型对一个样本进行预测。射中靶心位置代表预测准确,偏离靶心越远代表预测误差越大,其中左上角是最好的结果。

【二】数据类别不平衡怎么处理?

  1. 数据增强。
  2. 对少数类别数据做过采样,多数类别数据做欠采样。
  3. 损失函数的权重均衡。(不同类别的loss权重不一样,最佳参数需要手动调节)
  4. 采集更多少数类别的数据。
  5. 转化问题定义,将问题转化为异常点检测或变化趋势检测问题。 异常点检测即是对那些罕见事件进行识别,变化趋势检测区别于异常点检测,其通过检测不寻常的变化趋势来进行识别。
  6. 使用新的评价指标。
  7. 阈值调整,将原本默认为0.5的阈值调整到:较少类别/(较少类别+较多类别)。

【三】什么是过拟合,解决过拟合的方法有哪些?

过拟合:模型在训练集上拟合的很好,但是模型连噪声数据的特征都学习了,丧失了对测试集的泛化能力。

解决过拟合的方法:

  1. 重新清洗数据,数据不纯会导致过拟合,此类情况需要重新清洗数据或重新选择数据。
  2. 增加训练样本数量。使用更多的训练数据是解决过拟合最有效的手段。我们可以通过一定的规则来扩充训练数据,比如在图像分类问题上,可以通过图像的平移、旋转、缩放、加噪声等方式扩充数据;也可以用GAN网络来合成大量的新训练数据。
  3. 降低模型复杂程度。适当降低模型复杂度可以避免模型拟合过多的噪声数据。在神经网络中减少网络层数、神经元个数等。
  4. 加入正则化方法,增大正则项系数。给模型的参数加上一定的正则约束,比如将权值的大小加入到损失函数中。
  5. 采用dropout方法,dropout方法就是在训练的时候让神经元以一定的概率失活。
  6. 提前截断(early stopping),减少迭代次数。
  7. 增大学习率。
  8. 集成学习方法。集成学习是把多个模型集成在一起,来降低单一模型的过拟合风险,如Bagging方法。

【四】什么是欠拟合,解决欠拟合的方法有哪些?

欠拟合:模型在训练集和测试集上效果均不好,其根本原因是模型没有学习好数据集的特征。

解决欠拟合的方法:

  1. 可以增加模型复杂度。对于神经网络可以增加网络层数或者神经元数量。
  2. 减小正则化系数。正则化的目的是用来防止过拟合的,但是现在模型出现了欠拟合,则需要有针对性地减小正则化系数。
  3. Boosting。

【五】常用的距离度量方法

  1. 欧式距离
  2. 闵可夫斯基距离
  3. 马氏距离
  4. 互信息
  5. 余弦距离
  6. 皮尔逊相关系数
  7. Jaccard相关系数
  8. 曼哈顿距离

【六】正则化的本质以及常用正则化手段?

正则化是机器学习的核心主题之一。正则化本质是对某一问题加以先验的限制或约束以达到某种特定目的的一种操作。在机器学习中我们通过使用正则化方法,防止其过拟合,降低其泛化误差。

常用的正则化手段:

  1. 数据增强
  2. 使用L范数约束
  3. dropout
  4. early stopping
  5. 对抗训练

【七】L范数的作用?

L范数主要起到了正则化(即用一些先验知识约束或者限制某一抽象问题)的作用,而正则化主要是防止模型过拟合。

范数主要用来表征高维空间中的距离,故在一些生成任务中也直接用L范数来度量生成图像与原图像之间的差别。

下面列出深度学习中的范数:

【八】Dropout的作用?

Dropout是在训练过程中以一定的概率使神经元失活,也就是输出等于0。从而提高模型的泛化能力,减少过拟合。

使用Dropout

我们可以从两个方面去直观地理解Dropout的正则化效果:1)在Dropout每一轮训练过程中随机丢失神经元的操作相当于多个模型进行取平均,因此用于预测时具有vote的效果。2)减少神经元之间复杂的共适应性。当隐藏层神经元被随机删除之后,使得全连接网络具有了一定的稀疏化,从而有效地减轻了不同特征的协同效应。也就是说,有些特征可能会依赖于固定关系的隐含节点的共同作用,而通过Dropout的话,就有效地避免了某些特征在其他特征存在下才有效果的情况,增加了神经网络的鲁棒性。

Dropout在训练和测试时的区别:Dropout只在训练时产生作用,是为了减少神经元对部分上层神经元的依赖,类似将多个不同网络结构的模型集成起来,减少过拟合风险。而在测试时,应该用整个训练好的模型,因此不需要Dropout。

【九】Softmax的定义和作用

在二分类问题中,我们可以使用sigmoid函数将输出映射到【0,1】区间中,从而得到单个类别的概率。当我们将问题推广到多分类问题时,可以使用Softmax函数,对输出的值映射为概率值。

其定义为:

其中a代表了模型的输出。

【十】交叉熵定义和作用

交叉熵(cross entropy)常用于深度学习中的分类任务,其可以表示预测值与ground truth之间的差距。

交叉熵是信息论中的概念。其定义为:

P 代表 gt 的概率分布, q 代表预测值的概率分布。交叉熵从相对熵(KL散度)演变而来, log 代表了信息量, q 越大说明可能性越大,其信息量越少;反之则信息量越大。通过不断的训练优化,逐步减小交叉熵损失函数的值来达到缩小 p q 距离的目的。

【十一】训练集/验证集/测试集划分

机器学习的直接目的是希望模型在真实场景的数据上有很好的预测效果,泛化误差越低越好。

如何去跟踪泛化误差呢?这时就需要验证集和测试集了。

我们可以使用训练集的数据来训练模型,然后用测试集上的误差推测最终模型在应对现实场景中的泛化误差。有了测试集,我们可以在本地验证模型的最终的近似效果。

与此同时,我们在模型训练过程中要实时监控模型的指标情况,从而进行模型参数优选操作。验证集就用于模型训练过程中的指标评估。

一般来说,如果当数据量不是很大的情况(万级别以下)可以将训练集、验证集和测试集划分为6:2:2;如果是万级别甚至十万级别的数据量,可以将训练集、验证集和测试集比例调整为98:1:1。

(注:在数据集划分时要主要类别的平衡)

【十二】如何找到让F1最高的分类阈值?

首先,这个问题只存在于二分类问题中,对于多分类问题,只需要概率最高的那个预测标签作为输出结果即可。

F1值是综合了精准率和召回率两个指标对模型进行评价:

一般设0.5作为二分类的默认阈值,但一般不是最优阈值。想要精准率高,一般使用高阈值,而想要召回率高,一般使用低阈值。在这种情况下,我们通常可以通过P-R曲线去寻找最优的阈值点或者阈值范围。

【十三】机器学习有哪些种类?

机器学习中通常根据数据是否有标签可以分为监督学习(supervised learning)、非监督学习(unsupervised learning),半监督学习(semi-supervised learning)以及弱监督学习(weakly supervised learning)。

监督学习

机器学习模型在训练过程中的所有数据都有标签,就是监督学习的逻辑。

监督学习是最常见的学习种类,常见场景为分类和回归问题。

深度学习模型大都数都遵从监督学习的流程,并且支持向量机(Support Vector Machine, SVM),朴素贝叶斯(Naive Bayes),逻辑回归(Logistic Regression),K近邻(K-Nearest Neighborhood, KNN),决策树(Decision Tree),随机森林(Random Forest),AdaBoost以及线性判别分析(Linear Discriminant Analysis, LDA)等也属于监督学习算法的范畴。

非监督学习

非监督学习与监督学习完全相反,机器学习模型在训练过程中的所有数据都是没有标签的,主要学习数据本身的一些特性。

比如想象一个人从来没有见过猫和狗,如果给他看了大量的猫和狗,虽然他还是没有猫和狗的概念,但是他是能够观察出每个物种的共性和两个物种间的区别的,并对这个两种动物予以区分。

半监督学习

半监督学习的逻辑是机器学习模型在训练过程中,部分数据有标签,与此同时另外一部分数据没有标签,并把这两种数据都利用起来用于训练。

弱监督学习

弱监督学习的逻辑是机器学习模型在训练过程中使用的数据的标签存在不可靠的情况。这里的不可靠可以是标注不正确,多重标记,标记不充分,局部标记,包含噪声等情况。一个直观的例子是相对于分割的标签来说,分类的标签就是弱标签。

【十四】L1正则为什么比L2正则更容易产生稀疏解?

我们首先可以设目标函数为 L ,目标函数中的权值参数为 w ,那么目标函数和权值参数的关系如下所示:

如上图所示,最优的 w 在绿色的点处,而且 w 非零。

我们首先可以使用L2正则进行优化,新的目标函数: L + CW^{2} ,示意图如下蓝线所示:

我们可以看到,最优的 w 出现在黄点处, w 的绝对值减小了,更靠近横坐标轴,但是依然是非零的。

为什么是非零的呢?

我们可以对L2正则下的目标函数求导:

我们发现,权重 w 每次乘上的是小于1的倍数进行收敛,而且其导数在 w=0 时没有办法做到左右两边导数异号,所以L2正则使得整个训练过程稳定平滑,但是没有产生稀疏性。

接下来我们使用L1正则,新的目标函数: L + C|w| ,示意图如下粉线所示:

这里最优的 w 就变成了0。因为保证使用L1正则后 x=0 处左右两个导数异号,就能满足极小值点形成的条件。

我们来看看这次目标函数求导的式子:

可以看出L1正则的惩罚很大, w 每次都是减去一个常数的线性收敛,所以L1比L2更容易收敛到比较小的值,而如果 C > |f^{'}(0)| ,就能保证 w = 0 处取得极小值。

上面只是一个权值参数 w 。在深层网路中,L1会使得大量的 w 最优值变成0,从而使得整个模型有了稀疏性。

【十五】格拉姆矩阵的相关概念?

n维欧式空间中任意k个向量之间两两的内积所组成的矩阵,称为这k个向量的格拉姆矩阵(Gram matrix),这是一个对称矩阵。

其中对角线元素提供了k个不同特征图(a1,a2 ... ,ak)各自的信息,其余元素提供了不同特征图之间的相关信息。既能体现出有哪些特征,又能体现出不同特征间的紧密程度。图像风格迁移领域将其定义为风格特征。

格拉姆矩阵在风格迁移中有广泛的应用,深度学习中经典的风格迁移流程是:

  1. 准备基线图像和风格图像。
  2. 使用特征提取器分别提取基线图像和风格图像的feature map。
  3. 分别计算两个图像的feature map的格拉姆矩阵,以两个图像的格拉姆矩阵的差异最小化为优化目标,不断调整基线图像,使风格不断接近目标风格图像。

【十六】感知损失的相关概念?

感知损失在图像生成领域中比较常用。其核心是将gt图片卷积得到的高层feature与生成图片卷积得到的高层feature进行回归,从而约束生成图像的高层特征(内容和全局结构)。

经典感知损失结构

上面的公式中,划线部分代表了高层特征,一般使用VGG作为特征提取器。

【十七】Accuracy、Precision、Recall、F1 Scores的相关概念?

首先Rocky介绍一下相关名词:

  1. TP(True Positive): 预测为正,实际为正
  2. FP(False Positive): 预测为正,实际为负
  3. TN(True Negative):预测为负,实际为负
  4. FN(false negative): 预测为负,实际为正

Accuracy、Precision、Recall、F1 Scores的公式如下所示:

Accuracy(准确率):分类正确的样本数占样本总数的比例。

Precision(精准度/查准率):当前预测为正样本类别中被正确分类的样本比例。

Recall(召回率/查全率):预测出来的正样本占正样本总数的比例。

F1-score是Precision和Recall的综合。F1-score越高,说明分类模型越稳健。

【十八】梯度爆炸和梯度消失产生的原因及解决方法?

梯度爆炸和梯度消失问题

一般在深层神经网络中,我们需要预防梯度爆炸和梯度消失的情况。

梯度消失(gradient vanishing problem)和梯度爆炸(gradient exploding problem)一般随着网络层数的增加会变得越来越明显。

例如下面所示的含有三个隐藏层的神经网络,梯度消失问题发生时,接近输出层的hiden layer3的权重更新比较正常,但是前面的hidden layer1的权重更新会变得很慢,导致前面的权重几乎不变,仍然接近初始化的权重,这相当于hidden layer1没有学到任何东西,此时深层网络只有后面的几层网络在学习,而且网络在实际上也等价变成了浅层网络。

产生梯度爆炸和梯度消失问题的原因

我们来看看看反向传播的过程:

(假设网络每一层只有一个神经元,并且对于每一层 y_{i} = \sigma(z_{i}) = \sigma(w_{i}x_{i} + b_{i})

可以推导出:

而sigmoid的导数 \sigma^{'}(x) 如下图所示:

可以知道, \sigma^{'}(x) 的最大值是 \frac{1}{4} ,而我们初始化的权重 |w| 通常都小于1,因此 \sigma^{'}(x)|w| <= \frac{1}{4} ,而且链式求导层数非常多,不断相乘的话,最后的结果越来越小,趋向于0,就会出现梯度消失的情况。

梯度爆炸则相反, \sigma^{'}(x)|w| > 1 时,不断相乘结果变得很大。

梯度爆炸和梯度消失问题都是因为网络太深,网络权重更新不稳定造成的,本质上是梯度方向传播的连乘效应。

梯度爆炸和梯度消失的解决方法

  1. 使用预训练加微调策略。
  2. 进行梯度截断。
  3. 使用ReLU、LeakyReLU等激活函数。
  4. 引入BN层。
  5. 使用残差结构。
  6. 使用LSTM思想。

【十九】数据EDA逻辑(Exploratory Data Analysis)?

  1. 导入相应的Modules(numpy,pandas,matplotlib,PIL等)
  2. 阅读了解所有的数据文件(图片数据,类别文件,辅助文件等)
  3. 数据类别特征分析(数据类别总数,数据类别的平衡度,数据尺寸,噪声数据等)
  4. 数据可视化二次分析(直观了解不同类别的区别)

【二十】K折交叉验证逻辑?

K折交叉验证的作用

当有多个不同的模型(结构不同、超参数不同等)可以选择时,我们通过K折交叉验证来选取对于特定数据集最好的模型。

K折交叉验证的流程

  1. 将含有 N 个样本的数据集,分成 K 份,每份含有 \frac{N}{K} 个样本。选择其中一份作为验证集,另外 K-1 份作为训练集,验证集集就有 K 种情况。
  2. 在每种情况中,用训练集训练模型,用验证集测试模型,计算模型的泛化误差。
  3. 交叉验证重复 K 次,平均 K 次的结果作为模型最终的泛化误差。
  4. K 的取值一般在 [2,10] 之间。 K 折交叉验证的优势在于,同时重复运用随机产生的子样本进行训练和验证, 10 折交叉验证是最常用的。
  5. 训练集中样本数量要足够多,一般至少大于总样本数的50%。
  6. 训练集和验证集必须从完整的数据集中均匀采样。均匀采样的目的是希望减少训练集、验证集与原数据集之间的偏差。当样本数量足够多时,通过随机采样,便可以实现均匀采样的效果。

5折交叉验证举例

5折交叉验证(5-fold cross-validation)用来验证从不同的模型中选取最优的模型(最合适的模型)。将数据集分成5份,轮流将其中4份作为训练数据,1份作为验证数据,进行试验。每次试验都会得出相应的正确率。 5次的结果的正确率的平均值作为对算法精度的估计 。同时对不同的模型(如CNN、SVM、LR等)做上述相同的操作,得出每个模型在特定数据集上的平均能力,从中选优。

例子:

假设我们有一个特定数据集,我们想从YOLOv4、Mask R-CNN、SSD、Faster R-CNN、RetinaNet这五个模型中选取在这个特定数据集中有最好效果的一个模型作为baseline,我们可以进行交叉验证来进行判断:

步骤:

  1. 将数据集分成5份。
  2. 对于每一个模型,for i = 1, 2, 3, 4,5,每个for循环里将除了第i份的所有数据作为训练集用于训练,得到参数;再将参数在第i份数据上进行验证,得到评价结果。
  3. 最后我们可以得到5个模型的结果,每个模型有5个验证结果。将每个模型的结果取平均值,得到该模型的平均结果。
  4. 5个模型中平均结果最好的模型就是我们想要的最优模型。

【二十一】KL散度相关概念

KL散度(Kullback-Leibler divergence),可以以称作相对熵(relative entropy)或信息散度(information divergence)。KL散度的理论意义在于度量两个概率分布之间的差异程度,当KL散度越大的时候,说明两者的差异程度越大;而当KL散度小的时候,则说明两者的差异程度小。如果两者相同的话,则该KL散度应该为0。

接下来我们举一个具体的 :

我们设定两个概率分布分别为 P Q ,在设定为连续随机变量的前提下,他们对应的概率密度函数分别为 p(x) q(x) 。如果我们用 q(x) 去近似 p(x) ,则KL散度可以表示为:

KL(P||Q) = \int p(x)\log \frac{p(x)}{q(x)}dx \\

从上面的公式可以看出,当且仅当 P=Q 时, KL(P||Q) = 0 。此外我们可以知道KL散度具备非负性,即 KL(P||Q) >= 0 。并且从公式中我们也发现,KL散度不具备对称性,也就是说 P 对于 Q 的KL散度并不等于 Q 对于 P 的KL散度。因此, KL散度并不是一个度量(metric),即KL散度并非距离

我们再来看看离散的情况下用 q(x) 去近似 p(x) 的KL散度的公式:

KL(P||Q) = \sum p(x)\log \frac{p(x)}{q(x)} \\

接下来我们对上面的式子进行展开:

KL(P||Q) = \sum p(x)\log \frac{p(x)}{q(x)} = -\sum p(x)\log(q(x)) + \sum p(x)\log(p(x)) = H(P,Q) - H(P) \\

最后得到的第一项称作 P Q 的交叉熵(cross entropy),后面一项就是熵。

在信息论中,熵代表着信息量, H(P) 代表着基于 P 分布自身的编码长度,也就是最优的编码长度(最小字节数)。而 H(P,Q) 则代表着用 Q 的分布去近似 P 分布的信息,自然需要更多的编码长度。并且两个分布差异越大,需要的编码长度越大。所以两个值相减是大于等于0的一个值,代表冗余的编码长度,也就是两个分布差异的程度。所以KL散度在信息论中还可以称为相对熵(relative entropy)。

对深度学习中的生成模型来说,我们希望最小化真实数据分布与生成数据分布之间的KL散度,从而使得生成数据尽可能接近真实数据的分布。在实际场景中,我们是几乎不可能知道真实数据分布 P_{data}(x) 的,我们使用训练数据形成的生成分布在逼近 P_{data}(x)

【二十二】JS散度相关概念

JS散度全称Jensen-Shannon散度,简称JS散度。在概率统计中,JS散度也与KL散度一样具备了测量两个概率分布相似程度的能力,它的计算方法基于KL散度,继承了KL散度的非负性等,但有一点重要的不同,JS散度具备了对称性。

JS散度的公式如下所示,我们设定两个概率分布为 P Q ,另外我们还设定 M = 0.5 \times (P + Q) ,KL为KL散度公式。

JSD(P||Q) = \frac{1}{2}KL(P||M) + \frac{1}{2}KL(Q||M) \\

如果我们把KL散度公式写入展开的话,结果如下所示:

JSD(P||Q) = \int p(x)\log \frac{p(x)}{\frac{p(x) +q(x)}{2}} dx+ \int q(x)\log \frac{q(x)}{\frac{p(x) +q(x)}{2}}dx \\

深度学习中使用KL散度和JS散度进行度量的时候存在一个问题:

如果两个分布 P Q 离得很远,完全没有重叠的时候,那么KL散度值是没有意义的,而JS散度值是一个常数 \log2 。这对以梯度下降为基础的深度学习算法有很大影响,这意味梯度为0,即梯度消失。

【二十三】K-means算法逻辑?

K-means算法是一个实用的无监督聚类算法,其聚类逻辑依托欧式距离,当两个目标的距离越近,相似度越大。对于给定的样本集,按照样本之间的距离大小,将样本集划分为 K 个簇。让簇内的点尽量紧密的连在一起,而让簇间的距离尽量的大。

K-means的主要算法步骤

  1. 选择初始化的 k 个样本作为初始聚类中心 D = \{ D_{1}, D_{2}, D_{3}, ..., D_{k} \}
  2. 针对数据集中每个样本 x_{i} ,计算它到 k 个聚类中心的距离并将其分到距离最小的聚类中心所对应的类中.
  3. 针对每个类别 D_{j} ,重新计算它的聚类中心 D_{j} = \frac{1}{|c_{j}|}\sum_{x\in c_{j}}x 。(即属于该类的所有样本的质心);
  4. 重复上面2和3两步的操作,直到达到设定的中止条件(迭代次数、最小误差变化等)。

K-Means的主要优点

  1. 原理简单,实现容易,收敛速度快。
  2. 聚类效果较优。
  3. 算法的可解释度比较强。
  4. 主要需要调参的参数仅仅是簇数k。

K-Means的主要缺点

  1. K值需要人为设定,不好把握。
  2. 对初始的簇中心敏感,不同选取方式会得到不同结果。
  3. 对于不是凸的数据集比较难收敛。
  4. 如果各隐含类别的数据不平衡,比如各隐含类别的数据量严重失衡,或者各隐含类别的方差不同,则聚类效果不佳。
  5. 迭代结果只是局部最优。
  6. 对噪音和异常点比较的敏感。

【二十四】K近邻算法逻辑?

K近邻(K-NN)算法计算不同数据特征值之间的距离进行分类。存在一个样本数据集合,也称作训练数据集,并且数据集中每个数据都存在标签,即我们知道每一个数据与所属分类的映射关系。接着输入没有标签的新数据后,在训练数据集中找到与该新数据最邻近的K个数据,然后提取这K个数据中占多数的标签作为新数据的标签(少数服从多数逻辑)。

K近邻算法的主要步骤

  1. 计算新数据与各个训练数据之间的距离。
  2. 按照距离的递增关系进行排序。
  3. 选取距离最小的K个点。
  4. 确定前K个点所在类别的出现频率。
  5. 返回前K个点中出现频率最高的类别作为新数据的预测分类。

K近邻算法的结果很大程度取决于K的选择。其距离计算一般使用欧氏距离或曼哈顿距离等经典距离度量。

K近邻算法的主要优点

  1. 理论成熟,思想简单,既可以用来做分类又可以做回归。
  2. 可以用于非线性分类。
  3. 对数据没有假设,准确度高,对异常点不敏感。
  4. 比较适用于数据量比较大的场景,而那些数据量比较小的场景采用K近邻算法算法比较容易产生误分类情况。

K近邻算法的主要缺点

  1. 计算复杂性高;空间复杂性高。
  2. 样本不平衡的时候,对稀有类别的预测准确率低。
  3. 是慵懒散学习方法,基本上不学习,导致预测时速度比起逻辑回归之类的算法慢。
  4. 可解释性不强。

数据结构&&算法

【一】常用数据结构的相关知识

  1. 数组:最基本的数据结构,用连续内存存储数字。创建数组时,我们需要首先指定数组的容量大小,然后根据大小分配内存。即使我们只在数组中存储一个数字,也需要为所有的数据预先分配内存。因此数组的空间效率不是很好,经常会有空闲的区域没有得到充分的利用。(可动态分配内存来解决上述问题)由于数组中的内存是连续的,于是可以根据数组下标O(1)时间读/写任何元素,因此它的时间效率是很高的。
  2. 字符串:最基本的数据结构,用连续内存存储字符。C/C++中每个字符串都以字符’\0’作为结尾,这样我们就能很方便地找到字符串的最后尾部。
  3. 链表:链表的结构很简单,由指针把若干个节点连接成链状结构,并且链表是一种动态的数据结构,其需要对指针进行操作,因此链表很灵活。在创建链表时,我们无须知到链表的长度。当插入一个节点时,我们只需要为新节点分配内存,然后调整指针的指向来确保新节点被链接到链表中。内存分配不是在创建链表时一次性完成的,而是每添加一个节点分配一次内存。由于没有闲置的内存,链表的空间效率比数组高。
  4. 树:除根节点之外每个节点只有一个父节点,根节点没有父节点;除叶节点之外所有节点都有一个或多个子节点,叶节点没有子节点。父节点和子节点之间用指针链接。二叉树:是树的一种特殊结构,在二叉树中每个节点最多只能有两个子节点。二叉搜索树:左子节点总是小于或者等于根节点,而右子节点总是大于或者等于根节点。我么可以平均在O(logn)的时间内根据数值在二叉搜索树中找到一个结点。堆:分为最大堆和最小堆。在最大堆中根节点的值最大,在最小堆中根节点的值最小。
  5. 栈:栈是一个与递归紧密相关的数据结构,它在计算机领域被广泛应用,比如操作系统会给每个线程创建一个栈用来存储函数调用时各个函数的参数、返回地址及临时变量等。栈的特点是先进后出,即最后被压入(push)栈的元素会第一个被弹出(pop)。通常栈是一个不考虑排序的数据结构,我们需要O(n)时间才能找到栈中的最大值或最小值。
  6. 队列:队列与广度优先遍历算法紧密相关,队列的特点是先进先出。

【二】二叉树遍历(递归)模版

我们在调用递归函数的时候,把递归函数当作普通函数(黑箱)来调用,即明白该函数的输入输出是什么,而不用管此函数的内部运行机制。

前序遍历:

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)
    执行操作

【三】不同排序算法的异同?

【四】树有哪些遍历模式?

树的遍历模式:

  1. 前序遍历:先访问根节点,再访问左子节点,最后访问右子节点。
  2. 中序遍历:先访问左子节点,再访问根节点,最后访问右子节点。
  3. 后序遍历:先访问左子节点,再访问右子节点,最后访问根节点。
  4. 宽度优先遍历:先访问树的第一层节点,再访问树的第二层节点,一直到最后一层节点。

Python/C/C++知识

【一】Python中迭代器的概念?

可迭代对象是迭代器、生成器和装饰器的基础。简单来说,可以使用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))       # 整数是否可迭代
-------------结果如下----------------
False

当对所有的可迭代对象调用 dir() 方法时,会发现他们都实现了 iter 方法。这样就可以通过 iter(object) 来返回一个迭代器。

x = [1, 2, 3]
y = iter(x)
print(type(x))
print(type(y))
------------结果如下------------
<class 'list'>
<class 'list_iterator'>

可以看到调用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))
----------结果如下----------
Traceback (most recent call last):
  File "/Users/Desktop/test.py", line 6, in <module>
    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)
>>> next(counter)
14

【二】Python中生成器的相关知识

我们创建列表的时候,受到内存限制,容量肯定是有限的,而且不可能全部给他一次枚举出来。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]
<generator object <genexpr> 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我们可以自定义函数的调用层次关系从而自己来调度线程。

【三】Python中装饰器的相关知识

装饰器允许通过将现有函数传递给装饰器,从而向现有函数添加一些额外的功能,该装饰器将执行现有函数的功能和添加的额外功能。

装饰器本质上还是一个函数,它可以让已有的函数不做任何改动的情况下增加功能。

接下来我们使用一些例子来具体说明装饰器的作用:

如果我们不使用装饰器,我们通常会这样来实现在函数执行前插入日志:

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的深拷贝与浅拷贝?

在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中,使用引用计数进行垃圾回收;同时通过标记-清除算法解决容器对象可能产生的循环引用问题;最后通过分代回收算法提高垃圾回收效率。

【七】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,但是能减小切换代价提升性能)

【八】Python中range和xrange的区别?

首先,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)
>>> for i in xrange(0,7):
...     print(i)
6

在Python3中,xrange函数被移除了,只保留了range函数的实现,但是此时range函数的功能结合了xrange和range。并且range函数的类型也发生了变化,在Python2中是list类型,但是在Python3中是range序列的对象。

【九】Python中列表和元组的区别?

  1. 列表是可变的,在创建之后可以对其进行任意的修改。
  2. 元组是不可变的,元组一旦创建,便不能对其进行更改,可以元组当作一个只读版本的列表。
  3. 元组无法复制。
  4. Python将低开销的较大的块分配给元组,因为它们是不可变的。对于列表则分配小内存块。与列表相比,元组的内存更小。当你拥有大量元素时,元组比列表快。

【十】Python中dict(字典)的底层结构?

Python的dict(字典)为了支持快速查找使用了哈希表作为底层结构,哈希表平均查找时间复杂度为O(1)。CPython 解释器使用二次探查解决哈希冲突问题。

【十一】常用的深度学习框架有哪些,都是哪家公司开发的?

  1. PyTorch:Facebook
  2. TensorFlow:Google
  3. Keras:Google
  4. MxNet:Dmlc社区
  5. Caffe:UC Berkeley
  6. PaddlePaddle:百度

【十二】PyTorch动态图和TensorFlow静态图的区别?

PyTorch动态图:计算图的运算与搭建同时进行;其较灵活,易调节。

TensorFlow静态图:计算图先搭建图,后运算;其较高效,不灵活。

【十三】Python中assert的作用?

Python中assert(断言)用于判断一个表达式,在表达式条件为 false 的时候触发异常。

断言可以在条件不满足程序运行的情况下直接返回错误,而不必等待程序运行后出现崩溃的情况。

Rocky直接举一些例子:

>>> assert True 
>>> assert False
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError
>>> assert 1 == 1
>>> assert 1 == 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError
>>> assert 1 != 2

【十四】Python中互换变量有不用创建临时变量的方法吗?

在Python中,当我们想要互换两个变量的值或将列表中的两个值交换时,我们可以使用如下的格式进行,不需要创建临时变量:

x, y = y, x

这么做的原理是什么呢?

首先一般情况下Python是从左到右解析一个语句的,但在赋值操作的时候,因为是右值具有更高的计算优先级,所以需要从右向左解析。

对于上面的代码,它的执行顺序如下:

先计算右值 y , x (这里是简单的原值,但可能会有表达式或者函数调用的计算过程), 在内存中创建元组(tuple),存储 y, x 分别对应的值;计算左边的标识符,元组被分别分配给左值,通过解包(unpacking),元组中第一个标示符对应的值 (y) ,分配给左边第一个标示符 (x) ,元组中第二个标示符对应的值 (x) ,分配给左边第二个标示符 (y) ,完成了 x y 的值交换。

【十五】Python中的主要数据结构都有哪些?

  1. 列表(list)
  2. 元组(tuple)
  3. 字典(dict)
  4. 集合(set)

【十六】Python中的可变对象和不可变对象?

可变对象与不可变对象的区别在于对象本身是否可变。

可变对象:list(列表) dict(字典) set(集合)

不可变对象:tuple(元组) string(字符串) int(整型) float(浮点型) bool(布尔型)

【十七】Python中的None代表什么?

None是一个特殊的常量,表示空值,其和False,0以及空字符串不同,它是一个特殊Python对象, None的类型是NoneType。

None和任何其他的数据类型比较返回False。

>>> None == 0
False
>>> None == ' '
False
>>> None == None
>>> None == False
False

我们可以将None复制给任何变量,也可以给None赋值。

【十八】Python中 *args **kwargs 的区别?

*args **kwargs 主要用于函数定义。我们可以将不定数量的参数传递给一个函数。

这里的不定的意思是:预先并不知道函数使用者会传递多少个参数, 所以在这个场景下使用这两个关键字。

*args是用来发送一个非键值对的可变数量的参数列表给一个函数。

我们直接看一个例子:

def test_var_args(f_arg, *argv):
    print("first normal arg:", f_arg)
    for arg in argv:
        print("another arg through *argv:", arg)
test_var_args('hello', 'python', 'ddd', 'test')
-----------------结果如下-----------------------
first normal arg: hello
another arg through *argv: python
another arg through *argv: ddd
another arg through *argv: test

**kwargs允许我们将不定长度的键值对, 作为参数传递给一个函数。如果我们想要在一个函数里处理带名字的参数, 我们可以使用 **kwargs

我们同样举一个例子:

def greet_me(**kwargs):
    for key, value in kwargs.items():
        print("{0} == {1}".format(key, value))
greet_me(name="yasoob")
-----------结果如下-------------
name == yasoob

【十九】Python中Numpy的broadcasting机制?

Python的Numpy库是一个非常实用的数学计算库,其broadcasting机制给我们的矩阵运算带来了极大地方便。

我们先看下面的一个例子:

>>> import numpy as np
>>> a = np.array([1,2,3])
array([1, 2, 3])
>>> b = np.array([6,6,6])
array([6, 6, 6])
>>> c = a + b
array([7, 8, 9])

上面的代码其实就是把数组 a 和数组 b 中同样位置的每对元素相加。这里 a b 是相同长度的数组。

如果两个数组的长度不一致,这时候broadcasting就可以发挥作用了。

比如下面的代码:

>>> d = a + 5
array([6, 7, 8])

broadcasting会把 5 扩展成 [5,5,5] ,然后上面的代码就变成了对两个同样长度的数组相加。示意图如下(broadcasting不会分配额外的内存来存取被复制的数据,这里只是方面描述):

我们接下来看看多维数组的情况:

>>> e
array([[1., 1., 1.],
       [1., 1., 1.],
       [1., 1., 1.]])
>>> e + a
array([[2., 3., 4.],
       [2., 3., 4.],
       [2., 3., 4.]])

在这里一维数组被扩展成了二维数组,和 e 的尺寸相同。示意图如下所示:

我们再来看一个需要对两个数组都做broadcasting的例子:

>>> b = np.arange(3).reshape((3,1))
array([[0],
       [2]])
>>> b + a
array([[1, 2, 3],
       [2, 3, 4],
       [3, 4, 5]])

在这里 a b 都被扩展成相同的尺寸的二维数组。示意图如下所示:

总结broadcasting的一些规则:

  1. 如果两个数组维数不相等,维数较低的数组的shape进行填充,直到和高维数组的维数匹配。
  2. 如果两个数组维数相同,但某些维度的长度不同,那么长度为1的维度会被扩展,和另一数组的同维度的长度匹配。
  3. 如果两个数组维数相同,但有任一维度的长度不同且不为1,则报错。
>>> a = np.arange(3)
array([0, 1, 2])
>>> b = np.ones((2,3))
array([[1., 1., 1.],
       [1., 1., 1.]])
>>> a.shape
>>> a + b
array([[1., 2., 3.],
       [1., 2., 3.]])

接下来我们看看报错的例子:

>>> a = np.arange(3)
array([0, 1, 2])
>>> b = np.ones((3,2))
array([[1., 1.],
       [1., 1.],
       [1., 1.]])
>>> a + b
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: operands could not be broadcast together with shapes (3,) (3,2)

【二十】Python中的实例方法、静态方法和类方法三者区别?

不用@classmethod和@staticmethod修饰的方法为实例方法。在类中定义的方法默认都是实例方法。实例方法最大的特点是它至少要包含一个self参数,用于绑定调用此方法的实例对象,实例方法通常可以用类对象直接调用。

采用@classmethod修饰的方法为类方法。类方法和实例方法相似,它至少也要包含一个参数,只不过类方法中通常将其命名为cls,Python会自动将类本身绑定给cls参数。我们在调用类方法时,无需显式为cls参数传参。

采用@staticmethod修饰的方法为静态方法。静态方法没有类似self、cls这样的特殊参数,因此Python的解释器不会对它包含的参数做任何类或对象的绑定。也正因为如此,类的静态方法中无法调用任何类属性和类方法。

【二十一】Python中常见的切片操作

[:n]代表列表中的第一项到第n项。我们看一个例子:

example = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print(example[:6])
---------结果---------
[1, 2, 3, 4, 5, 6]

[n:]代表列表中第n+1项到最后一项:

example = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print(example[6:])
---------结果---------
[7, 8, 9, 10]

[-1]代表取列表的最后一个元素:

example = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print(example[-1])
---------结果---------

[:-1]代表取除了最后一个元素的所有元素:

example = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print(example[:-1])
---------结果---------
[1, 2, 3, 4, 5, 6, 7, 8, 9]

[::-1]代表取整个列表的相反列表:

example = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print(example[::-1])
---------结果---------
[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

[1:]代表从第二个元素意指读取到最后一个元素:

example = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print(example[1:])
---------结果---------
[2, 3, 4, 5, 6, 7, 8, 9, 10]

[4::-1]代表取下标为4(即第五个元素)的元素和之前的元素反转读取:

example = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print(example[4::-1])
---------结果---------
[5, 4, 3, 2, 1]

【二十二】Python中如何进行异常处理?

一般情况下,在Python无法正常处理程序时就会发生一个异常。异常在Python中是一个对象,表示一个错误。当Python脚本发生异常时我们需要捕获处理它,否则程序会终止执行。

捕捉异常可以使用try,except和finally语句。

try和except语句用来检测try语句块中的错误,从而让except语句捕获异常信息并处理。

try:
    6688 / 0
except:
    '''异常的父类,可以捕获所有的异常'''
    print "0不能被除"
else:
    '''保护不抛出异常的代码'''
    print "没有异常"
finally:
    print "最后总是要执行我"

【二十三】Python中remove,del以及pop之间的区别?

remove,del以及pop都可以用于删除列表、字符串等里面的元素,但是具体用法并不相同。

  1. remove是剔除第一个匹配的值。
  2. del是通过索引来删除当中的元素。
  3. pop是通过索引来删除当中的元素,并且返回该元素;若括号内不添加索引值,则默认删除最后一个元素。
>>> a = [0, 1, 2, 1, 3] 
>>> a.remove(1) 
[0, 2, 1, 3] 
>>> a = [0, 1, 2, 1, 3] 
>>> del a[1] 
[0, 2, 1, 3] 
>>> a = [0, 1, 2, 1, 3] 
>>> a.pop(1) 
[0, 2, 1, 3] 

【二十四】C/C++中野指针的概念?

野指针也叫空悬指针,不是指向null的指针,是未初始化或者未清零的指针。

产生原因:

  1. 指针变量未及时初始化。
  2. 指针free或delete之后没有及时置空。

解决办法:

  1. 定义指针变量及时初始化活着置空。
  2. 释放操作后立即置空。

【二十五】C/C++中内存泄漏以及解决方法?

内存泄漏是指己动态分配的堆内存由于某种原因导致程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

解决方法:

造成内存泄漏的主要原因是在使用new或malloc动态分配堆上的内存空间,而并未使用delete或free及时释放掉内存造成的。所以解决方法就是注意new/delete和malloc/free一定要配套使用。

【二十六】C/C++中面向对象和面向过程的区别?

面向对象(Object Oriented Programming,OOP)编程模型首先抽象出各种对象(各种类),并专注于对象与对象之间的交互,对象涉及的方法和属性都封装在对象内部。

面向对象的编程思想是一种依赖于类和对象概念的编程方式,一个形象的例子是将大象装进冰箱:

  1. 冰箱是一个对象,大象也是一个对象。
  2. 冰箱有自己的方法,打开、存储、关闭等;大象也有自己的方法,吃、走路等。
  3. 冰箱有自己的属性:长、宽、高等;大象也有自己的属性:体重、高度、体积等。

面向过程(Procedure Oriented Programming,POP)编程模型是将问题分解成若干步骤(动作),每个步骤(动作)用一个函数来实现,在使用的时候,将数据传递给这些函数。

面向过程的编程思想通常采用自上而下、顺序执行的方式进行,一个形象的例子依旧是将大象装进冰箱:

  1. 打开冰箱。
  2. 把大象装进冰箱。
  3. 关闭冰箱。

面向对象和面向过程的区别:

  1. 安全性角度。面向对象比面向过程安全性更高,面向对象将数据访问隐藏在了类的成员函数中,而且类的成员变量和成员函数都有不同的访问属性;而面向过程并没有办法来隐藏程序数据。
  2. 程序设计角度。面向过程通常将程序分为一个个的函数;而面向对象编程中通常使用一个个对象,函数通常是对象的一个方法。
  3. 逻辑过程角度。面向过程通常采用自上而下的方法;而面向对象通常采用自下而上的方法。
  4. 程序扩展性角度。面向对象编程更容易修改程序,更容易添加新功能。

【二十七】C/C++中常用容器功能汇总

vector(数组)

vector是封装动态数组的顺序容器。

成员函数:

  1. at():所需元素值的引用。
  2. front():访问第一个元素(返回引用)。
  3. back():访问最后一个元素(返回引用)。
  4. beign():返回指向容器第一个元素的迭代器。
  5. end():返回指向容器末尾段的迭代器。
  6. empty():检查容器是否为空。
  7. size():返回容器中的元素数。
  8. capacity():返回当前存储空间能够容纳的元素数。
  9. clear():清除内容。
  10. insert():插入元素。
  11. erase():擦除元素。
  12. push_back():将元素添加到容器末尾。
  13. pop_back():移除末尾元素。
  14. *max_element(v.begin(), v.end()):返回数组最大值。
  15. *min_element(v.begin(), v.end()):返回数组最小值。

queue(队列)

queue是容器适配器,他是FIFO(先进先出)的数据结构。

成员函数:

  1. front():访问第一个元素(返回引用)。
  2. back():访问最后一个元素(返回引用)。
  3. empty():检查容器是否为空。
  4. size():返回容器中的元素数。
  5. push():向队列尾部插入元素。
  6. pop():删除首个元素。

deque(双端队列)

deque是有下标顺序容器,它允许在其首尾两段快速插入和删除。

成员函数:

  1. front():访问第一个元素(返回引用)。
  2. back():访问最后一个元素(返回引用)。
  3. beign():返回指向容器第一个元素的迭代器。
  4. end():返回指向容器末尾段的迭代器。
  5. empty():检查容器是否为空。
  6. size():返回容器中的元素数。
  7. clear(): 清除内容。
  8. insert():插入元素。
  9. erase():擦除元素。
  10. push_back():将元素添加到容器末尾。
  11. pop_back():移除末尾元素。
  12. push_front():插入元素到容器起始位置。
  13. pop_front():移除首元素。
  14. at():所需元素值的引用。

set(集合)

集合基于红黑树实现,有自动排序的功能,并且不能存放重复的元素。

成员函数:

  1. begin()--返回指向第一个元素的迭代器。
  2. clear()--清除所有元素。
  3. count()--返回某个值元素的个数。
  4. empty()--如果集合为空,返回true。
  5. end()--返回指向最后一个元素的迭代器。
  6. erase()--删除集合中的元素。
  7. find()--返回一个指向被查找到元素的迭代器。
  8. insert()--在集合中插入元素。
  9. size()--集合中元素的数目。

unordered_set(无序集合)

无序集合基于哈希表实现,不能存放重复的元素。元素类型必须可以比较是否相等,因为这可以确定元素什么时候相等。

成员函数:

  1. empty():检查容器是否为空。
  2. size():返回容器中的元素数。
  3. insert():插入元素。
  4. clear():清除内容。
  5. count():返回匹配特定键的元素数量。
  6. find():寻找带有特定键的元素。
  7. erase()--删除集合中的元素。

unordered_map

unordered_map是关联容器,含有带唯一键的键-值对。

搜索、插入和元素移除拥有平均常数时间复杂度。

元素在内部不以任何特定顺序排序,而是组织进桶中。元素放进哪个桶完全依赖于其键的哈希。这允许对单独元素的快速访问,因为一旦计算哈希,则它准确指代元素所放进的桶。

成员函数:

  1. empty():检查容器是否为空。
  2. size():返回可容纳的元素数。
  3. insert():插入元素。
  4. clear():清除内容。
  5. count():返回匹配特定键的元素数量。
  6. find():寻找带有特定键的元素。
  7. erase()--删除集合中的元素。

【二十八】C/C++中指针和引用的区别

C语言的指针让我们拥有了直接操控内存的强大能力,而C++在指针基础上又给我们提供了另外一个强力武器 \to 引用。

首先我们来看一下C++中对象的定义:对象是指一块能存储数据并具有某种类型的内存空间。

一个对象a,它有值和地址&a。运行程序时,计算机会为该对象分配存储空间,来存储该对象的值,我们通过该对象的地址,来访问存储空间中的值。

指针p也是对象,它同样有地址&p和存储的值p,只不过,p存储的是其他对象的地址。如果我们要以p中存储的数据为地址,来访问对象的值,则要在p前加引用操作符 * ,即 *p

对象有常量(const)和变量之分,既然指针本身是对象,那么指针所存储的地址也有常量和变量之分,指针常量是指,指针这个对象所存储的地址是不可改变的,而常量指针的意思就是指向常量的指针。

我们可以把引用理解成变量的别名。定义一个引用的时候,程序把该引用和它的初始值绑定在一起,而不是拷贝它。计算机必须在声明引用r的同时就要对它初始化,并且r一经声明,就不可以再和其他对象绑定在一起了。

实际上,我们也可以把引用看作是通过一个指针常量来实现的,指向的地址不变,地址里的内容可以改变。

接下来我们来看看指针和引用的 具体区别:

  1. 指针是一个新的变量,要占用存储空间,存储了另一个变量的地址,我们可以通过访问这个地址来修改另一个变量。而引用只是一个别名,还是变量本身,不占用具体存储空间,只有声明没有定义。对引用的任何操作就是对变量本身进行操作,以达到修改变量的目的。
  2. 引用只有一级,而指针可以有多级。
  3. 指针传参的时候,还是值传递,指针本身的值不可以修改,需要通过解引用才能对指向的对象进行操作。引用传参的时候,传进来的就是变量本身,因此变量可以被修改。
  4. 引用它一定不为空,因此相对于指针,它不用检查它所指对象是否为空,这样就提高了效率。
  5. 引用必须初始化,而指针可以不初始化。

我们可以看下面的代码:

int a,b,*p,&r=a;//正确
r=3;//正确:等价于a=3
int &rr;//出错:引用必须初始化
p=&a;//正确:p中存储a的地址,即p指向a
*p=4;//正确:p中存的是a的地址,对a所对应的存储空间存入值4
p=&b//正确:p可以多次赋值,p存储b的地址

“&”不仅能表示引用,还可以表示成地址,还有可以作为按位与运算符。这个要根据具体情况而定。比如上面的例子,等号左边的,被解释为引用,右边的被解释成取地址。

引用的操作加了比指针更多的限制条件,保证了整体代码的安全性和便捷性。引用的合理使用可以一定程度避免“指针满天飞”的情况,可以一定程度上提升程序鲁棒性。并且指针与引用底层实现都是一样的,不用担心两者的性能差距。

【二十九】C/C++中宏定义的相关知识

宏定义可以把一个名称指定成任何一个文本。在完成宏定义后,无论宏名称出现在源代码的何处,预处理器都会将其替换成指定的文本。

//define 宏名 文本
#define WeThinkIn 666688889999
//define 宏名(参数) 文本
#define R(a,b) (a/b)
//注:带参数的宏替换最好在表达式整体上加括号,避免结果受其他运算影响。

宏定义的优点:

  1. 方便程序修改 ,如果一个常量在程序中大量使用,我们可以使用宏定义为其设置一个标识符。当我们想修改这个常量时,直接修改宏定义处即可,不必在程序中海量寻找所有相关位置。
  2. 提高程序的运行效率 ,使用带参数的宏定义可以完成函数的功能,但同时又比函数节省系统开销,提升程序运行效率。(无需调用函数这个流程)

宏定义和函数的区别:

  1. 宏在预处理阶段完成替换,之后替换的文本参与编译,相当于是恒等代换过程,运行时不存在函数调用,执行起来更快;而函数调用在运行时需要跳转到具体调用函数。
  2. 宏定义没有返回值;函数调用具有返回值。
  3. 宏定义参数没有类型,不进行类型检查;函数参数具有类型,需要检查类型。
  4. 宏定义不是说明或者语句,结尾不用加分号。
  5. 宏定义必须写在函数之外,其作用域为宏定义命令起到源程序结束。如要终止其作用域可使用# undef命令;而函数作用域在函数调用处。

【三十】C/C++中typedef关键字的相关知识

我们可以使用typedef关键字来定义自己习惯的数据类型名称,来替代系统默认的基本类型名称以及其他类型等名称。

在工业界中,我们一般在如下两个场景中会见到typedef的身影。

// 1.为基本数据类型定义新的类型名
typedef unsigned int WeThinkIn_int;
typedef char* WeThinkIn_point;
// 2.为自定义数据类型(结构体、共用体和枚举类型)定义简洁的类型名称
typedef struct target_Object
    int x;
    int y;
} WeThinkIn_Object;

typedef与宏定义的区别:

  1. 宏主要用于定义常量及书写复杂的内容;typedef主要用于定义类型别名。
  2. 宏替换发生在预处理阶段,属于文本恒等替换;typedef是编译中发挥作用。
  3. 宏定义参数没有类型,不进行类型检查;typedef参数具有类型,需要检查类型。
  4. 宏不是语句,不用在最后加分号;typedef是语句,要加分号标识结束。
  5. 注意对指针的操作,typedef char * p_char和#define p_char char *区别巨大。

【三十一】C/C++中面向对象的相关知识

面向对象程序设计(Object-oriented programming,OOP)有三大特征 ——封装、继承、多态。

封装:把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。 关键字:public, protected, private。不写默认为 private。

  1. public 成员:可以被任意实体访问。
  2. protected 成员:只允许被子类及本类的成员函数访问。
  3. private 成员:只允许被本类的成员函数、友元类或友元函数访问。

继承:基类(父类)——> 派生类(子类)

多态:即多种状态(形态)。简单来说,我们可以将多态定义为消息以多种形式显示的能力。多态是以封装和继承为基础的。

C++ 多态分类及实现:

  1. 重载多态(Ad-hoc Polymorphism,编译期):函数重载、运算符重载
  2. 子类型多态(Subtype Polymorphism,运行期):虚函数
  3. 参数多态性(Parametric Polymorphism,编译期):类模板、函数模板
  4. 强制多态(Coercion Polymorphism,编译期/运行期):基本类型转换、自定义类型转换

【三十二】C/C++中struct的内存对齐与内存占用计算?

什么是内存对齐?计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是有效对齐值的倍数。

什么是有效对齐值?计算机系统有默认对齐系数n,可以通过#pragma pack(n)来指定。有效对齐值就等与该对齐系数和结构体中最长的数据类型的长度两者最小的那一个值,比如对齐系数是8,而结构体中最长的是int,4个字节,那么有效对齐值为4。

为什么要内存对齐?假如没有内存对齐机制,数据可以任意存放,现在一个int变量存放在从地址1开始的连续四个字节地址中。当4字节存取粒度的处理器去取数据时,要先从0地址开始读取第一个4字节块,剔除不想要的字节(0地址),然后从地址4开始读取下一个4字节块,同样剔除不要的数据(5,6,7地址),最后留下的两块数据合并放入寄存器,这需要做很多工作,整体效率较低。

struct内存占用如何计算?结构体的内存计算方式遵循以下规则:

  1. 数据成员对齐规则:第一个数据成员放在offset为0的地方,以后的每一个成员的offset都必须是该成员的大小与有效对齐值相比较小的数值的整数倍,例如第一个数据成员是int型,第二个是double,有效对齐值为8,所以double的起始地址应该为8,那么第一个int加上内存补齐用了8个字节
  2. 结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部有效对齐值的整数倍地址开始存储。(比如struct a中存有struct b,b里有char, int, double,那b应该从8的整数倍开始存储)
  3. 结构体内存的总大小,必须是其有效对齐值的整数倍,不足的要补齐。

我们来举两个 :

#include <stdio.h>
#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 <stdio.h>
#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/C++中智能指针的定义与作用?

智能指针是一个类,这个类的构造函数中传入一个普通指针,析构函数中释放传入的指针。智能指针的类都是栈上的对象,所以当函数(或程序)结束时会自动被释放。

(注:不能将指针直接赋值给一个智能指针,一个是类,一个是指针。)

常用的智能指针:智能指针在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++程序的过程通常包括编辑、编译、链接、运行和调试等步骤。

编辑:编辑是C++程序开发过程的第一步,它主要包括程序文本的输入和修改。任何一种文本编辑器都可以完成这项工作。当用户完成了C++程序的编辑时,应将输入的程序文本保存为以.cpp为扩展名的文件(保存C++头文件时应以.h为扩展名)。

编译:C++是一种高级程序设计语言,它的语法规则与汇编语言和机器语言相比更接近人类自然语言的习惯。然而,计算机能够“看”懂的唯一语言是汇编语言。因此,当我们要让计算机“看”懂一个C++程序时,就必须使用编译器将这个C++程序“翻译”成汇编语言。编译器所做的工作实际上是一种由高级语言到汇编语言的等价变换。

汇编:将汇编语言翻译成机器语言指令。汇编器对汇编语言进行一系列处理后最终产生的输出结构称为目标代码,它是某种计算机的机器指令(二进制),并且在功能上与源代码完全等价。保存源代码和目标代码的文件分别称为源文件和目标文件( .obj)。

链接:要将汇编器产生的目标代码变成可执行程序还需要最后一个步骤——链接。链接工作是由“链接器”完成的,它将编译后产生的一个或多个目标文件与程序中用到的库文件链接起来,形成一个可以在操作系统中直接运行的可执行程序。(linux中的.o文件)

运行和调试:我们接下来就可以执行程序了。如果出现问题我们可以进行调试debug。

【三十五】C/C++中数组和链表的优缺点?

数组和链表是C/C++中两种基本的数据结构,也是两个最常用的数据结构。

数组的特点是在内存中,数组是一块连续的区域,并且数组需要预留空间。链表的特点是在内存中,元素的空间可以在任意地方,空间是分散的,不需要连续。链表中的元素都会两个属性,一个是元素的值,另一个是指针,此指针标记了下一个元素的地址。每一个数据都会保存下一个数据的内存的地址,通过此地址可以找到下一个数据。

数组的优缺点:

优点:查询效率高,时间复杂度可以达到O(1)。

缺点:新增和修改效率低,时间复杂度为O(N);内存分配是连续的内存,扩容需要重新分配内存。

链表的优缺点:

优点:新增和修改效率高,只需要修改指针指向即可,时间复杂度可以达到O(1);内存分配不需要连续的内存,占用连续内存少。

缺点:链表查询效率低,需要从链表头依次查找,时间复杂度为O(N)。

【三十六】C/C++中的new和malloc有什么区别?

new和malloc主要有以下三方面的区别:

  1. malloc和free是标准库函数,支持覆盖;new和delete是运算符,支持重载。
  2. malloc仅仅分配内存空间,free仅仅回收空间,不具备调用构造函数和析构函数功能,用malloc分配空间存储类的对象存在风险;new和delete除了分配回收功能外,还会调用构造函数和析构函数。
  3. malloc和free返回的是void类型指针(必须进行类型转换),new和delete返回的是具体类型指针。

模型部署

【一】模型压缩的必要性与可行性?

模型压缩是指对算法模型进行精简,进而得到一个轻量且性能相当的小模型,压缩后的模型具有更小的结构和更少的参数,可以有效降低计算和存储开销,便于部署在端侧设备中。

随着AI技术的飞速发展,不管是移动端产品还是线上产品,进行AI赋能都成为了趋势。这种情况下,AI算法的实时性与减少内存占用都显得极为重要。AI模型的参数在一定程度上能够表达其复杂性,但并不是所有的参数都在模型中发挥作用,部分参数作用有限,表达冗余,甚至会降低模型的性能。

【二】X86和ARM架构在深度学习侧的区别?

AI服务器与PC端一般都是使用X86架构,因为其高性能;AI端侧设备(手机/端侧盒子等)一般使用ARM架构,因为需要低功耗。

X86指令集中的指令是复杂的,一条很长指令就可以很多功能;而ARM指令集的指令是很精简的,需要几条精简的短指令完成很多功能。

X86的方向是高性能方向,因为它追求一条指令完成很多功能;而ARM的方向是面向低功耗,要求指令尽可能精简。

【三】FP32,FP16以及Int8的区别?

常规精度一般使用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的空间,即内存大小。显存可以用来放模型,数据等。

GPU 利用率主要的统计方式为:在采样周期内,GPU 上有 kernel 执行的时间百分比。可以简单理解为GPU计算单元的使用率。

【五】神经网络的显存占用分析

Float32 是在深度学习中最常用的数值类型,称为单精度浮点数,每一个单精度浮点数占用4Byte的显存。

在整个神经网络训练周期中,在GPU上的显存占用主要包括:数据,模型参数,模型输出等。

数据侧:举个 ,一个32 3 128 128的四维矩阵,其占用的显存 = 32 3 128 128*4 /1000 / 1000 = 6.3M

模型侧:占用显存的层包括卷积层,全连接层,BN层,梯度,优化器的参数等。

输出侧:占用的显存包括网络每一层计算出来的feature map以及对应的梯度等。

【六】算法模型部署逻辑?

我在之前专门沉淀了一篇关于算法模型部署逻辑的文章,大家可以直接进行阅读取用:

【CV算法上下游】系列之浅谈算法模型部署逻辑

【七】影响模型inference速度的因素?

  1. FLOPs(模型总的加乘运算)
  2. MAC(内存访问成本)
  3. 并行度(模型inference时操作的并行度越高,速度越快)
  4. 计算平台(GPU,AI协处理器,CPU等)

【八】为何在AI端侧设备一般不使用传统图像算法?

AI端侧设备多聚焦于深度学习算法模型的加速与赋能,而传统图像算法在没有加速算子赋能的情况下,在AI端侧设备无法发挥最优的性能。

【九】减小模型内存占用有哪些办法?

  1. 模型剪枝
  2. 模型蒸馏
  3. 模型量化
  4. 模型结构调整

【十】有哪些经典的轻量化网络?

  1. SqueezeNet
  2. MobileNet
  3. ShuffleNet
  4. Xception
  5. GhostNet

【十一】模型参数计算?

首先,假设卷积核的尺寸是 K \times K ,有 C 个特征图作为输入,每个输出的特征图大小为 H \times W ,输出为 M 个特征图。

由于模型参数量主要由卷积,全连接层,BatchNorm层等部分组成,我们以卷积的参数量为例进行参数量的计算分析:

卷积核参数量:

M\times C\times K\times K \\

偏置参数量:

M \\

总体参数量:

M\times C\times K\times K + M \\

【十二】模型FLOPs怎么算?

同样,我们假设卷积核的尺寸是 K\times K ,有 C 个特征图作为输入,每个输出的特征图大小为 H \times W ,输出为 M 个特征图。

由于在模型中卷积一般占计算量的比重是最高的,我们依旧以卷积的计算量为例进行分析:

FLOPS(全大写):是floating point operations per second的缩写,意指每秒浮点运算次数,理解为计算速度。是一个衡量硬件性能的指标。

FLOPs(s小写):是floating point operations的缩写(s表示复数),意指浮点运算数,理解为计算量。可以用来衡量算法/模型的复杂度。

针对模型的计算量应该指的是FLOPs。

在上述情况下,卷积神经网络一次前向传播需要的乘法运算次数为:

H\times W\times M\times C\times K\times K \\

同时,所要进行的加法计算次数分为考虑偏置和不考虑偏置:

(1)考虑偏置的情况:

为了得到输出的特征图的一个未知的像素,我们需要进行

(C\times K\times K - 1) + (C - 1) + 1 = C \times K \times K \\

次加法操作,其中 K\times K 大小的卷积操作需要 K\times K - 1 次加法,由于有C个通道,所以需要将结果乘以C,每个通道间的数要相加,所以需要C - 1次加法,最后再加上偏置的1次加法。

所以总的加法计算量如下:

H\times W\times M\times C\times K\times K \\

所以总的卷积运算计算量(乘法+加法):

2 \times H\times W\times M\times C\times K\times K \\

(2)不考虑偏置的情况:

总的卷积计算量:

H\times W\times M\times (2\times C\times K\times K - 1) \\

卷积运算过程

【十三】什么是异构计算?

首先,异构现象是指不同计算平台之间,由于硬件结构(包括计算核心和内存),指令集和底层软件实现等方面的不同而有着不同的特性。

异构计算是指联合使用两个或者多个不同的计算平台,并进行协同运算。比如CPU和GPU的异构计算,TPU和GPU的异构计算以及TPU/GPU/CPU的异构计算等等。

【十四】端侧部署时整个解决方案的核心指标?

  1. 精度
  2. 耗时
  3. 内存占用
  4. 功耗

【十五】什么是模型量化?

通常的深度学习模型参数是FP32浮点型的,而模型量化主要是使用FP16,INT8以及INT4等低精度类型来保存模型参数,从而有效的降低模型计算量和内存占用,并将精度损失限制在一个可接受的范围内。

模型量化主要分在线量化和离线量化。在线量化在模型训练阶段采用量化方法进行量化。离线量化主要在模型离线工具(模型转换阶段)中采用量化方法进行量化。

工业界中主要使用离线量化作为通用模型量化的解决方案。

【十六】什么是模型剪枝?

模型剪枝按照剪枝粒度可分为突触剪枝、神经元剪枝、权重矩阵剪枝等,主要是将权重矩阵中不重要的参数设置为0,结合稀疏矩阵来进行存储和计算。通常为了保证性能,需要逐步进行迭代剪枝,让精度损失限制在一个可接受的范围。

突触剪枝剪掉神经元之间的不重要的连接。对应到权重矩阵中,相当于将某个参数设置为0。

神经元剪枝则直接将某个节点直接裁剪。对应到权重矩阵中,相当于某一行和某一列置零。

除此之外,也可以将整个权重矩阵裁剪,每一层中只保留最重要的部分,这就是权重矩阵剪枝。相比突触剪枝和神经元剪枝,权重矩阵剪枝压缩率要大很多。

【十七】主流AI端侧硬件平台有哪些?

  1. 英伟达
  2. 海思
  3. 寒武纪
  4. 比特大陆
  5. 昇腾
  6. 登临
  7. 联咏
  8. 安霸
  9. 耐能
  10. 爱芯
  11. 瑞芯

【十八】主流AI端侧硬件平台一般包含哪些模块?

  1. 视频编解码模块
  2. CPU核心处理模块
  3. AI协处理器模块
  4. GPU模块
  5. DSP模块
  6. DDR内存模块
  7. 数字图像处理模块

【十九】算法工程师该如何看待硬件侧知识?

GPU乃至硬件侧的整体逻辑,是CV算法工作中必不可少的组成部分,也是算法模型所依赖的重要物理载体。

GPU的相关知识

现在AI行业有个共识,认为是数据的爆发和算力的突破开启了深度学习在计算机视觉领域的“乘风破浪”,而其中的算力,主要就是指以GPU为首的计算平台。

GPU(Graphical Processing Unit)从最初用来进行图形处理和渲染(玩游戏),到通过CUDA/OpenCL库以及相应的工程开发之后,成为深度学习模型在学术界和工业界的底层计算工具,其有以下的一些特征:

  1. 异构计算:GPU能作为CPU的协处理器与CPU协同运算。
  2. 单指令流多数据流(SIMD)架构:使得同一个指令(比如对图像数据的一些操作),可以同时在多个像素点上并行计算,从而得到比较大的吞吐量,深度学习中大量的矩阵操作,让GPU成为一个非常适合的计算平台。
  3. 多计算核心。比如Nvidia的GTX980GPU中,在和i7-5960CPU差不多的芯片面积上,有其128倍的运算速度。GTX980中有16个流处理单元,每个流处理单元中包含着128个CUDA计算核心,共有2048个GPU运算单元,与此同时i7-5960CPU只有16个类似的计算单元。
  4. CUDA模块。作为GPU架构中的最小单元,它的设计和CPU有着非常类似的结构,其中包括了一个浮点运算单元,整型运算单元以及控制单元。一个流处理单元中的CUDA模块将执行同一个指令,但是会作用在不同的数据上。多CUDA模块意味着GPU有更加高的计算性能,但更重要的是在算法侧有没有高效地调度和使用。
  5. 计算核心频率。即时钟频率,代表每一秒内能进行同步脉冲次数。就核心频率而言,CPU要高于GPU。由于GPU采用了多核逻辑,即使提高一些频率,其实对于总体性能影响不会特别大。
  6. 内存架构。GPU的多层内存架构包括全局内存,2级缓存,和芯片上的存储(包括寄存器,和1级缓存共用的共享内存,只读/纹理缓存和常量缓存)。

在使用GPU时,在命令行输入nvidia-smi命令时会打印出一张表格,其中包含了GPU当时状态的所有参数信息。

CUDA/cuDNN/OpenCL科普小知识:

  1. CUDA是NVIDIA推出的用于GPU的并行计算框架。
  2. cuDNN是NVIDIA打造的针对深度神经网络的加速库,是一个用于深层神经网络的GPU加速库。
  3. OpenCL是由苹果(Apple)公司发起,业界众多著名厂商共同制作的面向异构系统通用目的并行编程的开放式、免费标准,也是一个统一的编程环境。

深度学习的端侧设备

深度学习的端侧设备,又可以叫做边缘计算设备,深度学习特别是CV领域中,模型+端侧设备的组合能够加快业务的即时计算,决策和反馈能力,极大释放AI可能性。

深度学习的端侧设备主要由ARM架构的CPU+ GPU/TPU/NPU等协处理器 + 整体功耗 + 外围接口 + 工具链等部分组成,也是算法侧对端侧设备进行选型要考虑的维度。

在实际业务中,根据公司的不同,算法工程师涉及到的硬件侧范围也会不一样。如果公司里硬件和算法由两个部门分别负责,那么算法工程师最多接触到的硬件侧知识就是硬件性能评估,模型转换与模型硬件侧验证,一些硬件高层API接口的开发与使用;如果公司里没有这么细分的部门,那么算法工程师可能就会接触到端侧的视频编解码,模型推理加速,Opencv,FFmpeg,Tensor RT,工具链开发等角度的知识。

算法工程师该如何看待硬件侧

首先,整体上还是要将硬件侧工具化,把端侧设备当做算法模型的一个下游载体,会熟练的选型与性能评估更加重要。

端侧设备是算法产品整体解决方案中一个非常重要的模块,算法+硬件的范式将在未来的边缘计算与万物智能场景中持续发力。

在日常业务中,算法模型与端侧设备的适配性与兼容性是必须要考虑的问题,端侧设备是否兼容一些特殊的网络结构?算法模型转化并部署后,精度是否下降?功耗与耗时能否达标?等等都让算法工程师的模型设计逻辑有更多的抓手。

【二十】现有的一些移动端开源框架?

  1. NCNN,其GitHub地址: github.com/Tencent/ncnn
  2. Paddle Lite,其GitHub地址: github.com/PaddlePaddle
  3. MACE( Mobile AI Compute Engine),其GitHub地址: github.com/XiaoMi/mace
  4. TensorFlow Lite,其官网地址: tensorflow.org/lite?
  5. PocketFlow,其GitHub地址: github.com/Tencent/Pock
  6. 等等。。。

【二十一】端侧静态多Batch和动态多Batch的区别

当设置静态多Batch后,如Batch=6,那么之后不管是输入2Batch还是4Batch,都会按照6Batch的预设开始申请资源。

而动态多Batch不用预设Batch数,会根据实际场景中的真实输入Batch数来优化资源的申请,提高端侧实际效率。

由于动态多Batch的高性能,通常Inference耗时和内存占用会比静态多Batch时要大。

【二十二】优化模型端侧性能的一些方法

  1. 设计能最大限度挖掘AI协处理器性能的模型结构。
  2. 多模型共享计算内存。
  3. 减少模型分支结构,减少模型元素级操作。
  4. 卷积层的输入和输出特征通道数相等时MAC最小,以提升模型Inference速度。

【二十三】ONNX的相关知识

ONNX是一种神经网络模型的框架,其最经典的作用是作为不同框架之间的中间件,成为模型表达的一个通用架构,来增加不同框架之间的交互性。

ONNX的优势:

  1. ONNX的模型格式有极佳的细粒度。
  2. ONNX是模型表达的一个通用架构,主流框架都可以兼容。
  3. ONNX可以实现不同框架之间的互相转化。

【二十四】TensorRT的相关知识

TensorRT是一个高性能的深度学习前向Inference的优化器和运行的引擎。

TensorRT的核心:将现有的模型编译成一个engine,类似于C++的编译过程。在编译engine过程中,会为每一层的计算操作找寻最优的算子方法,将模型结构和参数以及相应kernel计算方法都编译成一个二进制engine,因此在部署之后大大加快了推理速度。

我们需要给TensorRT填充模型结构和参数,也就是解析我们自己的模型结构和参数文件,获取数据放到其中。官方给了三种主流框架模型格式的解析器(parser),分别是:ONNX,Caffe以及TensorFlow。

TensorRT的优势:

  1. 把一些网络层进行了合并。具体 如下图所示。
  2. 取消一些不必要的操作。比如不用专门做concat的操作等。
  3. TensorRT会针对不同的硬件都相应的优化,得到优化后的engine。
  4. TensorRT支持INT8和FP16的计算,通过在减少计算量和保持精度之间达到一个理想的trade-off。
TensorRT对网络结构进行重构

图像处理基础

【一】图像二值化的相关概念

图像二值化( Image Binarization)是将图像像素点的灰度值设置为0或255,也就是将整个图像呈现出明显的黑白效果的过程。二值图像每个像素只有两种取值:要么纯黑,要么纯白。

通过二值图像,将感兴趣目标和背景分离,能更好地分析物体的形状和轮廓。

图像二值化的方法有很多,其中最经典的就是采用阈值法(Thresholding)进行二值化。阈值法是指选取一个数字,大于它就设为白色,小于它就设为黑色。根据阈值选取方式的不同,又可以分为全局阈值和局部阈值。全局阈值(Global Method),指的是对整个图像中的每一个像素都选用相同的阈值。局部阈值(Local Method)又称自适应阈值(Adaptive Thresholding)。局部阈值法假定图像在一定区域内受到的光照比较接近。它用一个滑窗扫描图像,并取滑窗中心点亮度与滑窗内其他区域(称为邻域, neighborhood area)的亮度进行比较。如果中心点亮度高于邻域亮度,则将中心点设为白色,否则设为黑色。

【二】图像膨胀腐蚀的相关概念

图像的膨胀(dilation)和腐蚀(erosion)是两种基本的形态学运算,主要用来寻找图像中的极大区域和极小区域。

膨胀类似于“领域扩张”,将图像的高亮区域或白色部分进行扩张,其运行结果图比原图的高亮区域更大。

腐蚀类似于“领域被蚕食”,将图像中的高亮区域或白色部分进行缩减细化,其运行结果图比原图的高亮区域更小。

【三】高斯滤波的相关概念

图像为什么要滤波呢?一是为了消除图像在数字化过程中产生或者混入的噪声。二是为了提取图片对象的特征作为图像识别的特征模式。

什么是高斯噪声?首先,噪声在图像当中常表现为引起较强视觉效果的孤立像素点或像素块。简单来说,噪声的出现会给图像带来干扰,让图像变得不清楚。 高斯噪声就是它的概率密度函数服从高斯分布(即正态分布)的一类噪声。如果一个噪声,它的幅度分布服从高斯分布,而它的功率谱密度又是均匀分布的,则称它为高斯白噪声。

高斯滤波是一种线性平滑滤波,可以用来消除高斯噪声。其公式如下所示

高斯滤波过程: 假设高斯核:

那么高斯滤波计算过程就如下所示:

将这9个值加起来,就是中心点的高斯滤波的值。对所有点重复这个过程,就得到了高斯模糊后的图像。

二维高斯滤波能否分解为一维操作?可以进行分解,二维高斯滤波分解为两次一维高斯滤波,高斯二维公式可以推导为X轴与Y轴上的一维高斯公式。即使用一维高斯核先对图像逐行滤波,再对中间结果逐列滤波。

【四】边缘检测的相关概念

图像边缘是图像最基本的特征,指图像局部特征的不连续性。图像特征信息的突变处称之为边缘,例如灰度级的突变,颜色的突变,纹理结构的突变等。边缘是一个区域的结束,也是另一个区域的开始,利用该特征可以分割图像。

当我们看到一个有边缘的物体时,首先感受到的就是边缘。

上图(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图像和RGB图像的区别?

RAW格式: 从相机传感器端获取的原始数字格式的数据, 又称为Bayer格式. 每个像素信息只有RGB中的某个颜色信息, 且每4个像素中有2个像素为G信息,1个R信息,1个B信息, 即GRBG格式。

RGB格式: RGB格式是由RAW数据插值计算后获取的、每个像素均包含了RGB三种颜色的信息。

【十】常用的色彩空间格式

深度学习中常用的色彩空间格式:RGB,RGBA,HSV,HLS,Lab,YCbCr,YUV等。

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. 峰值信噪比(Peak-Signal to Noise Ratio,PSNR)
  2. 均方误差(Mean Square Error,MSE)
  3. MAE(Mean Absolute Error,MSE)
  4. 信噪比SNR(Signal to Noise Ratio,SNR)
  5. 信息保真度准则(Information Fidelity Criterion,IFC)
  6. 视觉信息保真度(Visual Information Fidelity,VIF)
  7. 结构相似度(Structure Similaruty,SSIM)

【十四】什么是图像畸变?

使用摄像头时,可能会出现图像边缘线条弯曲的情况,尤其是边缘部分是直线时,这种现象更为明显。比如摄像头显示画面中的门框、电线杆、墙面棱角、吊顶线等出现在边缘时,可能会有比较明显的弯曲现象,这种现象就叫做畸变。

畸变是指光学系统对物体所成的像相对于物体本身而言的失真程度,是光学透镜的固有特性,其直接原因是因为镜头的边缘部分和中心部分的放大倍率不一样导致。畸变并不影响像的清晰程度,只改变物体的成像形状,畸变是一种普遍存在的光学现象。

【十五】RGB图像转为灰度图的方法?

  1. RGB任选一通道作为灰度图
  2. RGB中最大值最为灰度图
  3. RGB的均值作为灰度图
  4. RGB的加权均值作为灰度图

【十六】仿射变换和透视变换的概念?

仿射变换是对图片进行平移,缩放,倾斜和旋转等操作,是一种二维坐标到二维坐标之间的线性变换。它保持了二维图形的“平直性”(直线经过变换之后依然是直线)和“平行性”(二维图形之间的相对位置关系保持不变,平行线依然是平行线,且直线上点的位置顺序不变)。

透视变换是将图片投影到一个新的视平面,也称作投影映射。它是将二维图片投影到三维空间,再投回另一个二维空间的映射操作。

仿射变换和透视变换的最大区别:一个平行四边形,经过仿射变换后依然是平行四边形;而经过透视变换后只是一个四边形(不再平行了)。

【十七】图像噪声的种类?

常规噪声

  1. 高斯噪声
  2. 脉冲噪声
  3. 泊松噪声
  4. 乘性噪声
  5. 瑞利噪声
  6. 伽马噪声
  7. 指数噪声
  8. 均匀噪声
  9. 椒盐噪声
  10. 散粒噪声
  11. 泊松噪声

对抗噪声

  1. 白盒对抗噪声
  2. 黑盒查询对抗噪声
  3. 黑盒迁移噪声
  4. 物理对抗噪声

【十八】Python中OpenCV和PIL的区别?

  1. 在读取图片时,OpenCV按照BGR的色彩模式渲染通道,而PIL按照RGB的色彩模式渲染通道。
  2. OpenCV性能较优,可以作为算法与工程的必备模块。

【十九】有哪些常用的图像去噪算法?

  • 空间域去燥:均值滤波器,中值滤波器,低通滤波器,高斯滤波,双边滤波,引导滤波,NLM(Non-Local means)算法等。
  • 频域去燥:小波变换,傅里叶变换,离散余弦变换,形态学滤波等。

【二十】有哪些常用的图像频域信息分离方法?

可以使用频域滤波器如小波变换,傅里叶变换,余弦变换,形态学滤波等方法将图像高低频信息分离。

【二十一】有哪些常用一阶微分梯度算子?

梯度算子

想要得到一张图像的梯度,要在图像的每个像素点处计算偏导数。因此一张图像 f (x,y) 位置处的 x y 方向上的梯度大小 g_{x} g_{y} 分别计算为:

上述两个公式对所有的 x y 的计算值可用下面的一维模版对 f(x,y) 的滤波得到。

用于计算梯度偏导数的滤波器模版,通常称之为梯度算子、边缘算子和边缘检测子等。

经典一阶梯度算子

Roberts算子

Roberts算子又称为交叉微分算法,它是基于交叉差分的梯度算法,通过局部差分计算检测边缘线条。常用来处理具有陡峭的低噪声图像,当图像边缘接近于正45度或负45度时,该算法处理效果更理想。其缺点是对边缘的定位不太准确,提取的边缘线条较粗。

Roberts算子的模板分为水平方向和垂直方向,如下式所示,从其模板可以看出,Roberts算子能较好的增强正负45度的图像边缘。

例如,下面给出Roberts算子的模板,在像素点 P5 x y 方向上的梯度大小 g_{x} g_{y} 分别计算为:

下图是Roberts算子的运行结果:

Prewitt算子

Prewitt算子是一种图像边缘检测的微分算子,其原理是利用特定区域内像素灰度值产生的差分实现边缘检测。由于Prewitt算子采用 3\times3 卷积模板对区域内的像素值进行计算,而Robert算子的模板为 2\times2 ,故Prewitt算子的边缘检测结果在水平方向和垂直方向均比Robert算子更加明显。Prewitt算子适合用来识别噪声较多、灰度渐变的图像,其计算公式如下所示:

例如,下面给出Prewitt算子的模板,在像素点 P5 x y 方向上的梯度大小 g_{x} g_{y} 分别计算为:

Prewitt算子运行结果如下:

Sobel算子

Sobel算子是一种用于边缘检测的离散微分算子,它结合了高斯平滑和微分求导。该算子用于计算图像明暗程度近似值,根据图像边缘旁边明暗程度把该区域内超过某个数的特定点记为边缘。Sobel算子在Prewitt算子的基础上增加了权重的概念,认为相邻点的距离远近对当前像素点的影响是不同的,距离越近的像素点对应当前像素的影响越大,从而实现图像锐化并突出边缘轮廓。

Sobel算子根据像素点上下、左右邻点灰度加权差,在边缘处达到极值这一现象检测边缘。对噪声具有平滑作用,提供较为精确的边缘方向信息。因为Sobel算子结合了高斯平滑和微分求导(分化),因此结果会具有更多的抗噪性,当对精度要求不是很高时,Sobel算子是一种较为常用的边缘检测方法。

Sobel算子的边缘定位更准确,常用于噪声较多、灰度渐变的图像。其算法模板如下面的公式所示,其中 d_{x} 表示水平方向, d_{y} 表示垂直方向。

例如,下面给出Sobel算子的模板,在像素点 P5 x y 方向上的梯度大小 g_{x} g_{y} 分别计算为:

sobel算子的效果如下:

各类算子的优缺点

Roberts算子

Roberts算子利用局部差分算子寻找边缘,边缘定位精度较高,但容易丢失一部分边缘,不具备抑制噪声的能力。该算子对具有陡峭边缘且含噪声少的图像效果较好,尤其是边缘正负45度较多的图像,但定位准确率较差。

Sobel算子

Sobel算子考虑了综合因素,对噪声较多的图像处理效果更好,Sobel 算子边缘定位效果不错,但检测出的边缘容易出现多像素宽度。

Prewitt算子

Prewitt算子对灰度渐变的图像边缘提取效果较好,而没有考虑相邻点的距离远近对当前像素点的影响,与Sobel 算子类似,不同的是在平滑部分的权重大小有些差异。

【二十二】拉普拉斯算子的相关概念

拉普拉斯算子是一个二阶算子,比起一阶微分算子,二阶微分算子的边缘定位能力更强,锐化效果更好。

使用二阶微分算子的基本方法是定义一种二阶微分的离散形式,然后根据这个形式生成一个滤波模版,与图像进行卷积。

滤波器分各向同性滤波器和各向异性滤波器。各向同性滤波器与图像进行卷积时,图像旋转后响应不变,说明滤波器模版自身是对称的。如果是各向异性滤波器,当原图旋转90度时,原图某一点能检测出细节(突变)的,但是现在却检测不出来了,这说明滤波器不是对称的。由于拉普拉斯算子是最简单的各向同性微分算子,它具有旋转不变形。

对于二维图像 f(x,y) ,二阶微分最简单的定义(拉普拉斯算子定义):

对于任意阶微分算子都是线性算子,所以二阶微分算子和后面的一阶微分算子都可以用生成模版然后卷积的方式得出结果。

根据前面对二阶微分的定义有:

根据上面的定义,与拉普拉斯算子的定义相结合,我们可以得到:

也就是一个点的拉普拉斯的算子计算结果是上下左右的灰度和减去本身灰度的四倍。同样,可以根据二阶微分的不同定义,所有符号相反,也就是上式所有灰度值全加上负号,就是-1,-1,-1,-1,4。但是我们要注意,符号改变,锐化的时候与原图的加或减应当相对变化。上面是四临接的拉普拉斯算子,将这个算子旋转45度后与原算子相架,就变成了八邻域的算子了,也就是一个像素周围一圈8个像素的和与中间像素8倍的差,作为拉普拉斯计算结果。

因为要强调图像中突变(细节),所以平滑灰度的区域,无响应,即模版系数的和为0,也是二阶微分的必备条件。

最后的锐化公式:

其中, g 是输出, f 为原始图像, c 是系数,用来对细节添加的多少进行调节。

我们接下来用更加形象的图像来解释拉普拉斯算子的有效性。

在边缘部分,像素值出现”跳跃“或者较大的变化。下图(a)中灰度值的”跃升”表示边缘的存在。如果使用一阶微分求导我们可以更加清晰的看到边缘”跃升”的存在(这里显示为高峰值)图(b)。

如果在边缘部分求二阶导数会出现什么情况呢,图(c)所示。

图(a)
图(b)
图(c)

我们会发现在一阶导数的极值位置,二阶导数为0。所以我们也可以用这个特点来作为检测图像边缘的方法。但是,二阶导数的0值不仅仅出现在边缘(它们也可能出现在无意义的位置),但是我们可以过滤掉这些点。

为了更适合于数字图像处理,我们如上面的式子所示,将其表示成了离散形式。为了更好的进行变成,我们也可以将其表示成模版的形式:

上图(a)表示离散拉普阿拉斯算子的模版,(b)表示其扩展模版,(c)则分别表示其他两种拉普拉斯的实现模版。

从模版形式中容易看出,如果在图像中一个较暗的区域中出现了一个亮点,那么用拉普拉斯运算就会使这个亮点变得更亮。因为图像中的边缘就是那些灰度发生跳变的区域,所以拉普拉斯锐化模板在边缘检测中很有用。

一般增强技术对于陡峭的边缘和缓慢变化的边缘很难确定其边缘线的位置。但此算子却可用二次微分正峰和负峰之间的过零点来确定,对孤立点或端点更为敏感,因此特别适用于以突出图像中的孤立点、孤立线或线端点为目的的场合。同梯度算子一样,拉普拉斯算子也会增强图像中的噪声,有时用拉普拉斯算子进行边缘检测时,可将图像先进行平滑处理。

图像锐化处理的作用是使灰度反差增强,从而使模糊图像变得更加清晰。图像模糊的实质就是图像受到平均运算或积分运算,因此可以对图像进行逆运算,如微分运算能够突出图像细节,使图像变得更为清晰。由于拉普拉斯是一种微分算子,它的应用可增强图像中灰度突变的区域,减弱灰度的缓慢变化区域。因此,锐化处理可选择拉普拉斯算子对原图像进行处理,产生描述灰度突变的图像,再将拉普拉斯图像与原始图像叠加而产生锐化图像。

这种简单的锐化方法既可以产生拉普拉斯锐化处理的效果,同时又能保留背景信息,将原始图像叠加到拉普拉斯变换的处理结果中去,可以使图像中的各灰度值得到保留,使灰度突变处的对比度得到增强,最终结果是在保留图像背景的前提下,突现出图像中小的细节信息。但其缺点是对图像中的某些边缘产生双重响应。

最后我们来看看拉普拉斯算子的效果:

【二十三】OpenCV读取图像的格式?

通常其他图像读取函数读取图片的时候是按RGB格式读取,但在OpenCV在读取图片时,是按BGR读取的。

【二十四】中值滤波与均值滤波的相关概念

均值滤波也称为线性滤波,其采用的主要方法为邻域平均法。线性滤波的基本原理是用均值代替原图像中的各个像素值,即对待处理的当前像素点 (x,y) ,选择一个模板,该模板由其近邻的若干像素组成,求模板中所有像素的均值,再把该均值赋予当前像素点 (x,y) ,作为处理后图像在该点上的灰度值 g(x,y) ,即 g(x,y)=\frac{1}{m} \Sigma f(x,y) m 为该模板中包含当前像素在内的像素总个数。这样的方法可以平滑图像,速度快,算法简单。但是无法去掉噪声,但能微弱的减弱它。

中值滤波是一种非线性平滑技术,它将每一像素点的灰度值设置为该点某邻域窗口内的所有像素点灰度值的中值。具体实现过程如下:

  1. 通过从图像中的某个采样窗口取出奇数个数据进行排序。
  2. 用排序后的中值作为当前像素点的灰度值。
  3. 在图像处理中,中值滤波常用来保护边缘信息,是经典的平滑噪声的方法,该方法法对消除椒盐噪音非常有效,在光学测量条纹图象的相位分析处理方法中有特殊作用,但在条纹中心分析方法中作用不大。
左侧使用了均值滤波,右侧使用了中值滤波

计算机基础

【一】深度学习中常用的Linux命令汇总

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四层模型的相关概念

TCP/IP四层模型:

  1. 应用层:负责各种不同应用之间的协议,如文件传输协议(FTP),远程登陆协议(Telnet),电子邮件协议(SMTP),网络文件服务协议(NFS),网络管理协议(SNMP)等。
  2. 传输层:负责可靠传输的TCP协议、高效传输的UDP协议。
  3. 网络层:负责寻址(准确找到对方设备)的IP,ICMP,ARP,RARP等协议。
  4. 数据链路层:负责将数字信号在物理通道(网线)中准确传输。

四层模型逻辑:

发送端是由上至下,把上层来的数据在头部加上各层协议的数据(部首)再下发给下层。

接受端则由下而上,把从下层接收到的数据进行解密和去掉头部的部首后再发送给上层。

层层加密和解密后,应用层最终拿到了需要的数据。

【四】OSI七层模型的相关概念

【五】Linux中的进程状态种类

  1. 运行(正在运行或在运行队列中等待)
  2. 中断(休眠中,受阻,在等待某个条件的形成或等待接受到信号)
  3. 不可中断(收到信号不唤醒和不可运行,进程必须等待直到有中断发生)
  4. 僵死(进程已终止,但进程描述符存在,直到父进程调用wait4()系统调用后释放)
  5. 停止(进程收到SIGSTOP, SIGSTP, SIGTIN, SIGTOU信号后停止运行运行)

【六】Linux中ps aux指令与grep指令配合管理进程

ps相关指令

ps命令(Process Status)是最基本同时也是非常强大的进程查看命令。

  • ps a 显示现行终端机下的所有程序,包括其他用户的程序。
  • ps -A 显示所有程序。
  • ps c 列出程序时,显示每个程序真正的指令名称,而不包含路径,参数或常驻服务的标示。
  • ps -e 此参数的效果和指定"A"参数相同。
  • ps e 列出程序时,显示每个程序所使用的环境变量。
  • ps f 用ASCII字符显示树状结构,表达程序间的相互关系。
  • ps -H 显示树状结构,表示程序间的相互关系。
  • ps -N 显示所有的程序,除了执行ps指令终端机下的程序之外。
  • ps s 采用程序信号的格式显示程序状况。
  • ps S 列出程序时,包括已中断的子程序资料。
  • ps -t <终端机编号>  指定终端机编号,并列出属于该终端机的程序的状况。
  • ps u   以用户为主的格式来显示程序状况。
  • ps x   显示所有程序,不以终端机来区分。

ps aux | more 指令

这个指令可以显示进程详细的状态。

参数解释:

  • USER:进程的所有者。
  • PID:进程的ID。
  • PPID:父进程。
  • %CPU:进程占用的CPU百分比。
  • %MEM:进程占用的内存百分比。
  • NI:进程的NICE值,数值越大,表示占用的CPU时间越少。
  • VSZ:该进程使用的虚拟内存量(KB)。
  • RSS:该进程占用的固定内存量(KB)。
  • TTY:该进程在哪个终端上运行,若与终端无关,则显示?。若为pts/0等,则表示由网络连接主机进程。
  • WCHAN:查看当前进程是否在运行,若为-表示正在运行。
  • START:该进程被触发启动时间。
  • TIME:该进程实际使用CPU运行的时间。
  • COMMAND:命令的名称和参数。
  • STAT状态位常见的状态字符: D 无法中断的休眠状态(通常 IO 的进程); R 正在运行可中在队列中可过行的; S 处于休眠状态; T 停止或被追踪; W 进入内存交换 (从内核2.6开始无效); X 死掉的进程 (基本很少見); Z 僵尸进程; < 优先级高的进程 N 优先级较低的进程 L 有些页被锁进内存; s 进程的领导者(在它之下有子进程); l 多进程的(使用 CLONE_THREAD, 类似 NPTL pthreads);+ 位于后台的进程组;

ps aux | grep xxx命令

如果直接用ps命令,会显示所有进程的状态,通常结合grep命令查看某进程的状态。

grep (global search regular expression(RE) and print out the line,全面搜索正则表达式并把行打印出来)是一种强大的文本搜索工具,它能使用正则表达式搜索文本,并把匹配的行打印出来。

例如我想要查看Python 的所有进程,可以在终端输入如下命令:

ps aux | grep python

便可以把Python相关的进程全部都打印到终端供我们查看。相关参数和之前的ps aux | more一致。

进程结束命令

我们可以使用kill命令来结束进程。

如下面的指令所示:

kill   PID  //杀掉进程
kill  -9 PID //强制杀死进程

【七】Git,GitLab,SVN的相关知识

Git

Git是当前主流的一种开源分布式版本控制系统,可以有效、快速的进行项目版本管理。

Git没有中央服务器,不同于SVN这种需要中央服务器的集中式版本控制系统。

Git的功能:版本控制(版本管理,远程仓库,分支协作)

Git的工作流程:

Git工作流程

Git的常用命令:

git init 创建仓库
git clone 克隆github上的项目到本地
git add  添加文件到缓存区
git commit 将缓存区内容添加到仓库中

GitLab

GitLab是一个基于Git实现的在线代码仓库软件,可以基于GitLab搭建一个类似于GitHub的仓库,但是GitLab有完善的管理界面和权限控制,有较高的安全性,可用于企业和学校等场景。

SVN

SVN全名Subversion,是一个开源的版本控制系统。不同于Git,SVN是集中式版本控制系统。

SVN只有一个集中管理的服务器,保存所有文件的修订版本,而协同工作的人们都通过客户端连到这台服务器,取出最新的文件或者提交更新。

SVN的特点是安全,效率,资源共享。

SVN的常用操作:

Checkout 检出代码
Update 更新代码
Commit 提交代码
Add 提交新增文件
Revert to this version + commit 撤销已经提交的代码

【八】协程的相关概念

协程(Coroutine,又称微线程)运行在线程之上,更加轻量级,协程并没有增加线程总数,只是在线程的基础之上通过分时复用的方式运行多个协程,大大提高工程效率。

协程的特点:

  1. 协程类似于子程序,但执行过程中,协程内部可中断,然后转而执行其他的协程,在适当的时候再返回来接着执行。协程之间的切换不需要涉及任何系统调用或任何阻塞调用。
  2. 协程只在一个线程中执行,发生在用户态上的一个逻辑。并且是协程之间的切换并不是线程切换,而是由程序自身控制,协程相比线程节省线程创建和切换的开销。
  3. 协程中不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

协程适用于有大量I/O操作业务的场景,可以到达很好的效果,一是降低了系统内存,二是减少了系统切换开销,因此系统的性能也会提升。

在协程中尽量不要调用阻塞I/O的方法,比如打印,读取文件等,除非改为异步调用的方式,并且协程只有在I/O密集型的任务中才会发挥作用。

【九】Linux系统的相关概念

Linux系统是一种操作系统(Operating System简称OS),它是软件的一部分,是硬件基础上的第一层软件,即硬件和应用软件沟通的桥梁。

Linux系统系统会控制其他程序运行,管理系统资源,提供最基本的计算功能,如管理及配置内存、决定系统资源供需的优先次序等,同时还提供一些基本的服务程序。Linux系统内核指的是提供硬件抽象层、硬盘及文件系统控制及多任务功能的系统核心程序。Linux发行套件系统是由 Linux系统内核与各种常用应用软件的集合产品。

在Linux系统中一切都是文件。在linux系统中,目录、字符设备、块设备、套接字、打印机等都被抽象成了文件,Linux系统中的一切文件都是从“根(/)”目录开始的,并按照树形结构来存放文件,且定义了常见目录的用途,文件和目录名称严格区分大小写。

Linux系统的文件目录结构

  • /usr:这是一个非常重要的目录,包含绝大多数的(多)用户工具和应用程序,用户的很多应用程序和文件都放在这个目录下,类似于windows下的program files目录。
  • /lib:存放着系统开机时会用到的函数库,以及在/bin和/sbin下命令会调用的函数库,几乎所有的应用程序都需要用到这些共享库。
  • /var:存放不断扩充的内容,如经常被修改的目录、文件(包括各种日志文件)等。
  • /boot:存放启动Linux时所需的一些核心文件(linux内核文件),包括一些引导程序文件、链接文件、镜像文件等。
  • /home:用户的主目录,在Linux中,每个用户都有一个自己的目录,该目录名一般以用户账号命名,包含保存的文件、个人设置等。
  • /sbin:s就是Super User的意思,这里存放的是系统管理员使用的系统管理命令。
  • /bin:这个存放的是当前用户的系统管理命令(cat、cp、ps等)。
  • /etc:存放所有的系统管理所需的配置文件和子目录(例如人员的帐号密码文件,各种服务的起始文件等)。
  • /tmp:存放一些临时文件,在系统重启时临时文件将被删除。
  • /snap:Ubuntu 16.04及之后版本引入了snap包管理器,与之相关的目录、文件(包括安装文件)位于/snap中。
  • /lost+found:该目录一般情况下是空的,当系统非法关机后会在该目录生成一些遗失的片段。
  • /media:linux系统会自动识别一些设备,例如U盘、光驱等等,当识别后,linux会把识别的设备挂载到该目录下。
  • /srv:该目录存放一些服务启动之后需要提取的数据。
  • /root:该目录为系统管理员用户主目录。
  • /opt:该目录存放安装的第三方软件,如Oracle数据库就可以安装到该目录下。
  • /mnt:挂载其他的文件系统(含硬盘分区)的目录。
  • /lib64:类似lib目录,存放64位库文件。
  • /srv:可以视作service的缩写,是一些网络服务启动后,这些服务需要取用的数据目录,常见的服务例如www,ftp等。
  • /proc:这个目录本身是一个虚拟文件系统,它放置的数据都是在内存当中,不占用硬盘的容量。
  • /sys:这个目录其实跟/proc非常的相似,也是一个虚拟的文件系统主要也是记录与内核相关的信息,不占用硬盘容量。
  • /dev:在linux中任何的设备和接口设备都是以文件的形式存在于这个目录当中。你只要到通过访问这个目录下的某个文件就相当于访问某个设备。

Linux系统种类

  • 红帽企业版Linux:RedHat是全世界内使用最广泛的Linux系统。它具有极强的性能与稳定性,是众多生成环境中使用的(收费的)系统。
  • Fedora:由红帽公司发布的桌面版系统套件,用户可以免费体验到最新的技术或工具,这些技术或工具在成熟后会被加入到RedHat系统中,因此Fedora也成为RedHat系统的试验版本。
  • CentOS:通过把RedHat系统重新编译并发布给用户免费使用的Linux系统,具有广泛的使用人群。
  • Deepin:在中国发行,对优秀的开源成品进行集成和配置。
  • Debian:稳定性、安全性强,提供了免费的基础支持,在国外拥有很高的认可度和使用率。
  • Ubuntu:是一款派生自Debian的操作系统,对新款硬件具有极强的兼容能力。Ubuntu与Fedora都是极其出色的Linux桌面系统,而且Ubuntu也可用于服务器领域。

【十】Linux系统和Windows系统的区别?

  • Linux系统更稳定且有效率。
  • Linux系统是免费(或少许费用),而Windows系统是商业化主导。
  • Linux系统漏洞少且快速修补。
  • Linux系统支持多用户同时使用计算机。
  • Linux系统有更加安全的用户与文件权限策略。
  • Linux系统可以访问源代码并根据用户的需要修改代码,而Windows系统不能访问源代码。
  • Linux系统更能支持多种深度学习配套软件,但是windows系统能支持大量的视频游戏软件。

【十一】POC验证测试的概念

POC(Proof of Concept) ,即概念验证。通常是企业进行产品选型时或开展外部实施项目前,进行的一种产品或供应商能力验证工作。主要验证内容:

  1. 产品的功能。产品功能由企业提供,企业可以根据自己的需求提供功能清单,也可以通过与多家供应商交流后,列出自己所需要的功能。
  2. 产品的性能。性能指标也是由企业提供,并建议提供具体性能指标所应用的环境及硬件设备等测试环境要求。
  3. 产品的API适用性。
  4. 产品相关技术文档的规范性、完整性。
  5. 涉及到自定义功能研发的,还需要验证API开放性,供应商实施能力。
  6. 企业资质规模及企业实施案例等。

验证内容归根结底,就是证明企业选择的产品或供应商能够满足企业提出的需求,并且提供的信息准确可靠。

POC测试工作的前提:

  1. 前期调研充分,并已经对产品或供应商有了比较深入的沟通了解。
  2. 企业对自己的产品需求比较清晰。

POC测试工作参与者:

使用用户代表、业务负责人、项目负责人、技术架构师、测试工程师、商务经理等。

POC测试工作准备文档:

  1. POC测试工作说明文档。内容包括测试内容、测试要求(如私有化部署)、测试标准、时间安排等。
  2. 功能测试用例。主要确认功能可靠性,准确性。内容包括功能名称、功能描述等。
  3. 场景测试用例。主要测试企业团队实施响应速度、实施能力、集成能力。这部分通常按照企业需求而定,不建议太复杂,毕竟需要供应商实施,拖的太久企业耐性受到影响,时间也会被拉长。
  4. 技术测评方案。主要验证产品的性能、功能覆盖情况、集成效率、技术文档的质量。
  5. 商务测评方案。主要包括企业实力、企业技术人才能力、版权验证、市场背景、产品报价等。

POC测试工作的主要流程:

第一阶段:工作启动

由商务或者对外代表对供应商发布正式邀请并附POC测试工作说明。

建立POC协同群。以满足快速沟通,应答。

涉及到私有化部署的,需要收集供应商部署环境要求,并与供应商一起进行部署工作,同时企业参与人员对部署工作情况做好记录。

第二阶段:产品宣讲及现场集中测试

供应商根据企业提供的POC测试工作说明及相应测试模块的用例或方案进行产品现场测试论证。

企业参与人员参与功能测试,并填写记录和意见。此阶段供应商往往需进行现场操作指导。

第三阶段:技术测评

供应商根据企业提供的技术要求给出相关支持技术文档,企业进行现场对比,根据实际情况进行统计记录。并保留供应商提供的资料和对比记录。

涉及到场景demo设计的,建议企业对实施人员能力、实施时长、实施准确性进行对比。

第四阶段:间歇性测试工作

该阶段是在第一阶段启动时,就可以开始了。测试功能外,还包括关键用户使用的体验心得、易用性评价。该部分允许企业用户主观评价,建议可以扩大范围组织间歇性测试,并做好测试用户记录。间歇时间1天或者多天根据实际情况安排。

第五阶段:商务验证

供应商根据企业提供的商务测评方案,积极配合工作。涉及到客户核实的,还需要企业进行考证。该部分工作也是从第一阶段启动时,就可以开始了。

第六阶段:背书归档、分析总结

每个阶段的工作都需要记录好参与人、时间、工作时间,并将测试过程中企业的、供应商的文档分类归档。对每个阶段进行分析对比,总结评价。最后进行整体工作分析总结。

POC工作按照不同企业和程度,测试的方式和投入力度不一样。但是目的都是相同的——验证产品或供应商能力是否满足企业需求。

【十二】Docker的相关概念及常用命令

Docker简介

Docker是一个开源的应用容器引擎,基于Go语言并遵从Apache2.0协议开源。

Docker可以打包代码以及相关的依赖到一个轻量级、可移植的容器中,然后发布到任何流行的Linux机器上,也可以实现虚拟化。

容器完全使用沙箱机制,相互之间不会有任何接口(类似iPhone的app),更重要的是容器性能开销极低。

Docker漫画

Docker的应用场景:

  1. Web应用的自动化打包和发布。
  2. 自动化测试和持续集成、发布。
  3. 在服务器环境中部署/调整数据库或其它的后台应用。

Docker架构

Docker包括三个基本单元:

  1. 镜像(Image):Docker镜像(Image),就相当于是一个root文件系统。比如官方镜像ubuntu:16.04就包含了完整的一套Ubuntu16.04最简系统的root文件系统。
  2. 容器(Container):镜像(Image)和容器(Container)的关系,就像是面向对象程序设计中的类和实例一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。
  3. 仓库(Repository):仓库可看成一个代码控制中心,用来保存镜像。

Docker容器使用

Docker客户端

Docker客户端非常简单,我们可以直接输入docker命令来查看到Docker客户端的所有命令选项。也可以通过命令docker command --help更深入的了解指定的Docker命令使用方法。

docker

容器使用

获取本地没有的镜像。如果我们本地没有我们想要的镜像,我们可以使用 docker pull 命令来载入镜像:

docker pull 镜像

启动容器。以下命令使用ubuntu镜像启动一个容器,参数为以命令行模式进入该容器:

docker run -it 镜像 /bin/bash

参数解释:

  • -i:允许你对容器内的标准输入 (STDIN) 进行交互。
  • -t:在新容器内指定一个伪终端或终端。
  • /bin/bash:放在镜像名后的是命令,这里我们希望有个交互式Shell。

我们可以通过 <仓库名>:<标签> 的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签,将以latest作为默认标签。

要退出终端,直接输入exit或者CTRL+D。

启动已经停止运行的容器。查看所有的容器的命令如下:

docker ps -a

我们也可以用docker ps命令查看正在运行的容器。

docker ps

我们可以使用 docker start 启动一个已停止的容器:

docker start 容器

想要后台运行容器,我们可以过 -d 指定容器的运行模式:

docker run -itd --name 指定创建的容器名 容器 /bin/bash

加了 -d 参数默认不会进入容器,想要进入容器需要使用下面的指令进入容器:

  • docker attach
  • docker exec:推荐大家使用 docker exec 命令,因为使用此命令退出容器终端,不会导致容器的停止。
docker attach 容器  //如果从这个容器退出,会导致容器的停止。
docker exec -it 容器 /bin/bash   //如果从这个容器退出,不会导致容器的停止。

想要停止容器,其命令如下:

docker stop 容器ID

停止的容器重启命令:

docker restart 容器ID

删除容器:

docker rm -f 容器ID

Docker镜像使用

列出镜像列表。我们可以使用 docker images 来列出本地主机上的镜像。

docker images

各个参数解释:

  • REPOSITORY:表示镜像的仓库源
  • TAG:镜像的标签
  • IMAGE ID:镜像ID
  • CREATED:镜像创建时间
  • SIZE:镜像大小

查找镜像:

docker search 镜像

各个参数解释:

  • NAME: 镜像仓库源的名称
  • DESCRIPTION: 镜像的描述
  • OFFICIAL: 是否 docker 官方发布
  • stars: 类似 Github 里面的 star,表示点赞、喜欢的意思。
  • AUTOMATED: 自动构建。

删除镜像:

docker rmi 镜像

Docker镜像的修改和自定义

docker镜像的更新

在启动docker镜像后,写入一些文件、代码、更新软件等等操作后,退出docker镜像,之后在终端输入如下命令:

docker commit -m="..." -a= "..." 容器ID 指定要创建的目标镜像名称

参数解释:

  • commit:固定格式
  • -m:提交的描述信息
  • -a:指定镜像作者

接着可以用docker images查看镜像是否更新成功。(注意:不要创建名称已存在的镜像,这样会使存在的镜像名称为none,从而无法使用)

镜像名称修改和添加新标签

更改镜像名称(REPOSITORY):

docker tag 容器ID 新名称

更改镜像tag,不修改名称:

docker tag IMAGEID(镜像id) REPOSITORY:TAG(仓库:标签)

Docker容器和本机之间的文件传输

主机和容器之间传输文件的话需要用到容器的ID全称。

从本地传输到容器中:

docker cp 本地文件路径 容器name:/root/(容器路径)

从容器传输到本地上:

docker cp 容器name:/root/(容器路径) 本地文件路径