DL_知识点随笔积累

1. 前言

  • 此篇博文用来记录平常见到的DL有关讲解很清楚的知识点
  • 目前文章包括如下内容:(会坚持更新来记录有关知识)
    • 全连接层流程及softmax loss
    • 过拟合欠拟合问题
    • 权重衰减的推导
    • 归一化
    • 网络参数的几种初始化方式
    • 机器学习模型的三种评估方法
    • 感受野
    • 全局平均池化
    • 张量维度的判断技巧
    • 最近邻插值Nearest和双线性插值bilinear
    • Focal Loss
    • 有关PyTorch中nn.CrossEntropyLoss()加入权重的理解
    • Pseudo Labeling 训练策略
    • CNN计算力FLOPs
    • 评价指标ROC和AUC
    • torch.cat方法简记

2. 全连接层和损失层

2.1 过程分析

  • 先理清下从全连接层到损失层之间的计算:

  • 这张图的等号左边部分就是全连接层做的事,W是全连接层的参数,我们也称为权值,X是全连接层的输入,也就是特征
  • 从图上可以看出特征X是N$\times$1的向量,这是怎么得到的呢?这个特征就是由全连接层前面多个卷积层和池化层处理后得到的,假设全连接层前面连接的是一个卷积层,这个卷积层的输出是100个特征(也就是我们常说的feature map的channel为100),每个特征的大小是4$\times$4,那么在将这些特征输入给全连接层之前会将这些特征flat成N$\times$1的向量(这个时候N就是100$\times$4$\times$4=1600)。

  • 再来看W,W是全连接层的参数,是个T$\times$N的矩阵,这个N和X的N对应,T表示类别数,比如你是7分类,那么T就是7。我们所说的训练一个网络,对于全连接层而言就是寻找最合适的W矩阵。因此全连接层就是执行WX得到一个T$\times$1的向量(也就是图中的logits[T$\times1$]),这个向量里面的每个数都没有大小限制的,也就是从负无穷大到正无穷大。

  • 然后如果你是多分类问题,一般会在全连接层后面接一个softmax层,这个softmax的输入是T$\times$1的向量,输出也是T$\times$1的向量(也就是图中的prob[T$\times$1],这个向量的每个值表示这个样本属于每个类的概率),只不过输出的向量的每个值的大小范围为0到1。

  • softmax的输出向量就是概率,该样本属于各个类的概率

2.2 softmax

  • softmax的公式:
  • 公式非常简单,前面说过softmax的输入是WX,假设模型的输入样本是L,讨论一个3分类问题(类别用1,2,3表示),样本L的真实类别是2,那么这个样本L经过网络所有层到达softmax层之前就得到了WX,也就是说WX是一个3$\times$1的向量,那么上面公式中的$a_j$就表示这个3$\times$1的向量中的第j个值(最后会得到S1,S2,S3);而分母中的$a_k$则表示3$\times$1的向量中的3个值,所以会有个求和符号(这里求和是k从1到T,T和上面图中的T是对应相等的,也就是类别数的意思,j的范围也是1到T)。
  • 因为$e^x$恒大于0,所以分子永远是正数,分母又是多个正数的和,所以分母也肯定是正数,因此$S_j$是正数,而且范围是(0,1)。如果现在不是在训练模型,而是在测试模型,那么当一个样本经过softmax层并输出一个T$\times$1的向量时,就会取这个向量中值最大的那个数的index作为这个样本的预测标签。

2.3 softmax loss

  • 公式如下:
  • 首先L是损失。$S_j$是softmax的输出向量S的第j个值,前面已经介绍过了,表示的是这个样本属于第j个类别的概率。$y_j$前面有个求和符号,j的范围也是1到类别数T,因此y是一个1$\times$T的向量,里面的T个值,而且只有1个值是1,其他T-1个值都是0。那么哪个位置的值是1呢?答案是真实标签对应的位置的那个值是1,其他都是0。所以这个公式其实有一个更简单的形式:
  • 当然此时要限定j是指向当前样本的真实标签。
  • 假设一个5分类问题,然后一个样本I的标签y=[0,0,0,1,0],也就是说样本I的真实标签是4,假设模型预测的结果概率(softmax的输出)p=[0.1,0.15,0.05,0.6,0.1],可以看出这个预测是对的,那么对应的损失L=-log(0.6),也就是当这个样本经过这样的网络参数产生这样的预测p时,它的损失是-log(0.6)。

  • 那么假设p=[0.15,0.2,0.4,0.1,0.15],这个预测结果就很离谱了,因为真实标签是4,而你觉得这个样本是4的概率只有0.1(远不如其他概率高,如果是在测试阶段,那么模型就会预测该样本属于类别3),对应损失L=-log(0.1)。那么假设p=[0.05,0.15,0.4,0.3,0.1],这个预测结果虽然也错了,但是没有前面那个那么离谱,对应的损失L=-log(0.3)。我们知道log函数在输入小于1的时候是个负数,而且log函数是递增函数,所以-log(0.6) < -log(0.3) < -log(0.1)。简单讲就是你预测错比预测对的损失要大,预测错得离谱比预测错得轻微的损失要大。

2.3 cross entropy 交叉熵

  • 公式如下:
  • 当cross entropy的输入P是softmax的输出时,cross entropy等于softmax loss。Pj是输入的概率向量P的第j个值,所以如果你的概率是通过softmax公式得到的,那么cross entropy就是softmax loss。

3. 过拟合与欠拟合

  • 对于有监督学习算法,例如分类和回归,通常有两种情况下生成的模型不能很好地拟合数据:欠拟合(underfitting)过拟合(overfitting)
  • 有监督学习算法的一个重要度量是泛化它衡量从训练数据导出的模型对不可见数据的期望属性的预测能力。当我们说一个模型是欠拟合或过拟合时,它意味着该模型没有很好地推广到不可见数据。
  • 一个与训练数据相拟合的模型并不一定意味着它能很好地概括不可见数据。有以下几点原因
    • 训练数据只是我们从现实世界中收集的样本,只代表了现实的一部分。这可能是因为训练数据根本不具有代表性,因此即使模型完全符合训练数据,也不能很好的拟合不可见数据。
    • 我们收集的数据不可避免地含有噪音和误差。即便该模型与数据完全吻合,也会错误地捕捉到不期望的噪音和误差,最终导致对不可见数据的预测存在偏差和误差。
  • 如下图所示的三种情况:

  • 欠拟合:
    • 欠拟合模型是指不能很好地拟合训练数据的模型,即显著偏离真实值的模型。
    • 欠拟合的原因之一可能是模型对数据而言过于简化,因此无法捕获数据中隐藏的关系。
    • 从上图(1)可以看出,在分离样本(即分类)的过程中,一个简单的线性模型(一条直线)不能清晰地画出不同类别样本之间的边界,从而导致严重的分类错误。
    • 为了避免上述欠拟合的原因,我们需要选择一种能够从训练数据集生成更复杂模型的替代算法。
  • 过拟合:
    • 过拟合模型是与训练数据拟合较好的模型,即误差很小或没有误差,但不能很好地推广到不可见数据。
    • 与欠拟合相反,过拟合往往是一个能够适应每一位数据的超复杂模型,但却可能会陷入噪音和误差的陷阱。从上面的图(3)可以看出,虽然模型在训练数据中的分类错误少了,但在不可见数据上更可能出错。
    • 类似地于欠拟合的情况,为了避免过拟合,可以尝试另一种从训练数据集生成更简单的模型的算法。
    • 或者更常见的情况是,使用生成过拟合模型的原始算法,但在算法中增加添加了正则项,即对过于复杂的模型进行附加处理,从而引导算法在拟合数据的同时生成一个不太复杂的模型。
  • 解决过拟合问题的几种方法:
    • 减小网络大小,防止过拟合的最简单的方法就是减小模型大小,即减少模型中可学习参数的个数(这由层 数和每层的单元个数决定)。
    • 添加权重正则化:强制让模型权重只能取较小的值, 从而限制模型的复杂度,这使得权重值的分布更加规则。其实现方法是向网络损失函数中添加与较大权重值相关的成本,成本有L1正则化、L2正则化
    • Dropout正则化:对某一层使用 dropout,在训练过程中随机将该层的一些输出特征舍弃(设置为 0)。

4. 权重衰减

  • 过拟合现象:即模型的训练误差远小于它在测试集上的误差。虽然增大训练数据集可能会减轻过拟合,但是获取额外的训练数据往往代价高昂。
  • 本部分学习应对过拟合问题的常用方法:权重衰减(weight decay)。

  • 权重衰减等价于$L_2$范数正则化(regularization)。正则化通过为模型损失函数添加惩罚项使学出的模型参数值较小,是应对过拟合的常用手段。

  • 我们先描述$L_2$范数正则化,再解释它为何又称权重衰减。
  • $L_2$范数正则化在模型原损失函数基础上添加$L_2$范数惩罚项,从而得到训练所需的最小化的函数。
  • $L_2$范数惩罚项指的是模型权重参数每个元素的平方和与一个正的常数的乘积。

  • 以下面的线性回归损失函数为例:

  • 其中$w_1,w_2$是权重参数,b是偏重参数,样本i的输入为$x_1^{(i)},x_2^{(i)}$,标签为$y^{(i)}$,样本数为n。
  • 将权重参数用向量$w = [w_1,w_2]$表示,带有$L_2$范数惩罚项的新损失函数为:
  • 其中超参数$\lambda > 0$。当权重参数为0时,惩罚项最小;当$\lambda$较大时,惩罚项在损失函数中的比重较大,这通常使得学到的权重参数的元素较接近0。
  • 在线性回归中,使用小批量梯度下降方法优化,选取一组模型参数的初始值,如随机选取;接下来对参数进行多层迭代,使每次迭代都可能降低损失函数的值。每次迭代时,先随机均匀采样一个由固定数目训练数据样本所组成的小批量$B$,求小批量数据样本的平均损失有关模型参数的导数(梯度),最后用此结果与预先设定的一个正数的乘积作为模型参数在本次迭代的减小量。迭代过程如下:
  • 在上式中,$B$代表每个小批量中的样本个数(批量大小,batch size),$\eta$称作学习率(learning rate)并取正数。需要强调的是,这里的批量大小和学习率的值是人为设定的,并不是通过模型训练学出的,因此叫作超参数(hyperparameter)。我们通常所说的“调参”指的正是调节超参数,例如通过反复试错来找到超参数合适的值。在少数情况下,超参数也可以通过模型训练学出。

  • 在有了$L_2$范数惩罚项之后,在小批量梯度下降中,将权重$w_1$和$w_2$的迭代方式求导之后如下

  • 上述迭代公式的转换如果看不懂,可以参考该博文的推导:链接

  • 经过上述推导及公式,可得知$L_2$范数正则化令权重$w_1,w_2$先乘以了小于1的数,再减去不含惩罚项的梯度。所以,$L_2$范数正则化又叫权重衰减。权重衰减通过惩罚绝对值较大的模型参数为需要学习的模型增加了限制,这可能会对过拟合有效。

5. 归一化

  • 在机器学习中,对输入特征做归一化预处理操作是常见的步骤。类似的,在图像处理中,同样的可以将图像的每个像素信息看作是一种特征。
  • 在实践中,对每个特征减去平均值来中心化数据是非常重要的,这种归一化处理方式被称作“中心式归一化”。
  • 卷积神经网络中的数据预处理操作:通常是计算训练集图像像素均值,之后在处理训练集、验证集和测试集图像时需要分别减去该均值。
  • 减均值的原理:我们默认自然图像是一类平稳的数据分布(即数据每一个维度的统计都服从相同分布),此时,从每个样本上减去数据的统计平均值(逐样本计算)可以移除共同部分,凸显个体差异。
  • 需要注意的是,在实际操作中应该首先划分好训练集、验证集和测试集。而均值只针对划分后的训练集计算。不可直接在未划分的所有图像上计算均值,如此会违背机器学习的基本原理:在模型训练中,能且仅能从训练数据中获取信息。
  • 下图展示了图像减均值(未重塑)的效果。通过肉眼观察可以发现,“天空”等背景部分被有效“移除”了,而“车”和“建筑”等显著区域被“凸显”出来。

6. 网络参数初始化

  • 神经网络模型一般依靠随机梯度下降法进行模型训练和参数更新,网络的最终性能与收敛得到的最优解直接相关,而收敛效果实际 上又很大程度取决于网络参数最开始的初始化。

6.1 全零初始化

  • 通过合理的数据预处理和规范化,网络收敛到稳定状态时,参数(权值)在理想情况下应基本保持正负各半的状态(此时的期望为0)。
  • 由上,一种简单且听起来合理的方法就是全零初始化,将所有的参数都初始化为零,因为这样,初始化为0得到的参数的期望与网络稳定时得到的期望是一致的,都为0。但是:参数全为0时,网络不同神经元的输出必然相同,相同输出则会导致梯度更新完全一样,这样就会使得更新后的参数保持一样的状态。换句话说,如果参数全零初始化,网络的神经元无法做出改变和进行训练。

  • 所以,不能进行全零初始化。

6.2 随机初始化

  • 可将参数值随机设定为接近 y 的一个 很小的随机数(有正有负)。在实际应用中,随机参数服从高斯分布或均匀分布都是较有效的初始化方式。
  • tf中见到的随机填充初始化:
1
2
3
4
def weight_variable(shape):        # 权值w初始化设置 stddev=0.1:给一些偏差0.1防止死亡节点
initial = tf.truncated_normal(shape,stddev=0.1)
# tf.truncated_normal函数返回指定形状的张量填充随机截断的正常值
return tf.Variable(initial) # 创建一个变量
1
2
3
4
5
6
7
8
9
10
11
# 定义所有的网络参数  random_normal:将返回一个指定形状的张量,通过随机的正常值填充
weights = {
'wc1': tf.Variable(tf.random_normal([11, 11, 1, 96])),
'wc2': tf.Variable(tf.random_normal([5, 5, 96, 256])),
'wc3': tf.Variable(tf.random_normal([3, 3, 256, 384])),
'wc4': tf.Variable(tf.random_normal([3, 3, 384, 384])),
'wc5': tf.Variable(tf.random_normal([3, 3, 384, 256])),
'wd1': tf.Variable(tf.random_normal([4*4*256, 4096])),
'wd2': tf.Variable(tf.random_normal([4096, 1024])),
'out': tf.Variable(tf.random_normal([1024, n_classes]))
}

6.3 Xavier初始化(针对tanh激活函数)

  • tf中实现的:
1
2
3
kernel = tf.get_variable(scope + "w",
shape=[kh,kw,n_in,n_out],dtype=tf.float32,
initializer=tf.contrib.layers.xavier_initializer_conv2d())

6.4 He初始化(针对ReLU激活函数)

7. 模型评估

  • 评估模型的重点是将数据划分为三个集合:训练集、验证集和测试集。在训练数据上训练模型,在验证数据上评估模型。一旦找到了最佳参数,就在测试数据上最后测试一次。
  • 在于开发模型时总是需要调节模型配置,比如选择层数或每层大小,这叫作模型的超参,以便与模型参数(即权重)区分开】。
  • 这个调节过程需要使用模型在验证数据上的性能作为反馈信号。这个调节过程本质上就是一种学习:在某个参数空间中寻找良好的模型配置。

  • 将数据划分为训练集、验证集和测试集可能看起来很简单,但如果可用数据很少,还有几 种高级方法可以派上用场。以下是三种经典的评估方法:简单的留出验证、K 折验证,以及带有打乱数据的重复 K 折验证

7.1 简单的留出验证

  • 留出一定比例的数据作为测试集。在剩余的数据上训练模型,然后在测试集上评估模型。为了防止信息泄露,不能基于测试集来调节模型,所以还应该保留一个验证集。
  • 代码流程如下所示:

  • 这是最简单的评估方法,但有一个缺点:如果可用的数据很少,那么可能验证集和测试集包含的样本就太少,从而无法在统计学上代表数据。这个问题很容易发现:如果在划分数据前进行不同的随机打乱,最终得到的模型性能差别很大,那么就存在这个问题。下面的 K 折验证与重复的K折验证,它们是解决这一问题的两种方法。

7.2 K折验证

  • K折验证 (K-fold validation) 将数据划分为大小相同的 K 个分区。对于每个分区 i,在剩余的 K-1个分区上训练模型,然后在分区 i 上评估模型。最终分数等于 K 个分数的平均值。
  • 对于不同的训练集 - 测试集划分,如果模型性能的变化很大,那么这种方法很有用。与留出验证一样,这种方法也需要独立的验证集进行模型校正。

  • 代码流程如下:

7.3 带有打乱数据的重复k折验证

  • 如果可用的数据相对较少,而你又需要尽可能精确地评估模型,那么可以选择带有打乱数据的重复K折验证。
  • 具体做法是多次使用 K 折验证,在每次将数据划分为 K 个分区之前都先将数据打乱。 最终分数是每次 K 折验证分数的平均值。注意,这种方法一共要训练和评估 P×K 个模型(P是重复次数),计算代价很大。

7.4 注意事项

  • 数据代表性:例如,你想要对数字图像进行分类,而图像样本是按类别排序的,如果你将前 80% 作为训练集,剩余 20% 作为测试集,那么会导致训练集中只包含类别 0~7,而测试集中只包含 类别 8~9。这个错误看起来很可笑,却很常见。因此,在将数据划分为训练集和测试集之前,通常应该随机打乱数据
  • 时间箭头:如果想要根据过去预测未来(比如明天的天气、股票走势等),那么在划分数据前你不应该随机打乱数据,因为这么做会造成时间泄露:你的模型将在未来数据上得到有效训练。在这种情况下,你应该始终确保测试集中所有数据的时间都晚于训练集数据。
  • 数据冗余:如果数据中的某些数据点出现了两次(这在现实中 的数据里十分常见),那么打乱数据并划分成训练集和验证集会导致训练集和验证集之 间的数据冗余。一 定要确保训练集和验证集之间没有交集。

8. 感受野

8.1 感受野定义

  • 卷积神经网络每一层输出的特征图(feature map)上的像素点在输入图片上映射的区域大小

    The receptive field is defined as the region in the input space that a particular CNN’s feature is looking at (i.e. be affected by).

  • 看一个简单例子:

  • 两层 3 x 3 的卷积核卷积操作之后的感受野是 5 x 5,其中卷积核(filter)的步长(stride)为1、padding为0,如图所示:

    • 上半部分图中,对于输出的特征图的一个像素点,在输入图像的映射的区域大小就是5 x 5
    • 下半部分图中,对于第三层的输出的像素点,在第二层的映射的区域范围为3 x 3,对于第二层而言,输出的3 x 3的特征图的每个像素点在第一层的映射范围为3 x 3,但是,第二层的特征图,即第一层卷积之后的输出,对于第一层来说,感受野为全部的5 x 5,所以,对于第三层在第一层的感受野也是5 x 5

  • 三层 3 x 3 卷积核操作之后的感受野是 7 x 7,其中卷积核的步长为1,padding为0,如图所示:

8.2 The fixed-sized CNN feature map visualization

  • 接下来的内容来自A guide to receptive field arithmetic for Convolutional Neural Networks

  • 感受野指的是一个特定的CNN特征(特征图上的某个点)在输入空间所受影响的区域

  • 一个感受野可以用中心位置(center location)和大小(size)来表征。
  • 然而,对于一个CNN特征来说,感受野中的每个像素值(pixel)并不是同等重要。一个像素点越接近感受野中心,它对输出特征的计算所起作用越大。

  • 现在,我们关注如何计算一个特定感受野的中心位置和大小。

  • 下图为给出了感受野实例。其中输入特征图大小为 5 X 5,采用的卷积参数如下:卷积核大小 k= 3 X 3,padding大小p = 1,步长s = 2 。经过一次卷积之后,得到大小为 3 X 3的输出特征图(绿色)
  • 在这个特征图上继续采用相同的卷积,即参数都一致,得到一个 2 X 2的特征图(橙色)。输出特征图的大小可以通过如下公式计算:

  • 对上述图片结果的解释:

    • 上半部分的输入为5 x 5,padding = 1,在该卷积下得到3 x 3的输出,可由计算公式得来。
    • 下半部分,因为后来的橙色是由绿色部分由相同的卷积得来,所以padding=1,所以此时的中间绿色应该补一圈0,即变为5 x 5。这时,因为绿色变为5 x 5,所以原来的输入也该增大,所以就如上图所示。
  • 在这个可视化中,我们可以看到每个特征图所包含的特征数,但是很难知道每个特征的感受野的中心位置和大小

  • 下图给出了固定大小的的CNN可视化,所有的特征图固定大小并保持与输入特征图大小一致

  • 每个特征被标记在其感受野所在的中心(从而定位出感受野中心位置)。由于一个特征图中所有的特征都有相同大小的感受野,我们可以简单地在每个特征周围画出一个边界框,从而获得感受野的大小。

9. 全局平均池化

  • global average pooling,GAP层,通过降低模型的参数数量来最小化过拟合效应。
  • 类似最大池化层,GAP层可以用来降低三维张量的空间维度,然而,GAP层的降维更加激进,一个h × w × d的张量会被降维至1 × 1 × d。GAP层通过取平均值映射每个h × w的特征映射至单个数字

  • 如下图所示:

  • 在pytorch中,没有全局平均池化的实现,使用平均池化,将核的大小设为输入的高和宽即可:
1
2
3
4
5
6
7
8
9
class GlobalAvgPool2d(nn.Module):
# 全局平均池化层可通过将池化窗口形状设置成输入的高和宽实现
def __init__(self):
super(GlobalAvgPool2d, self).__init__()
def forward(self, x):
return F.avg_pool2d(x, kernel_size=x.size()[2:])

net.add_module("global_avg_pool", GlobalAvgPool2d())
# GlobalAvgPool2d的输出: (Batch, 512, 1, 1)

10. 张量维度的判断技巧

  • 以PyTorch为例,我们可以使用tensor.shape来输出张量的具体维度,但是当输出一个张量时,我们如何快速得出shape,以下为例:
1
2
3
4
5
tensor(  [[[[ 0.4024,  0.8702],
[-0.0625, -0.3826]],

[[-1.1489, -0.5550],
[ 0.0176, 1.0890]]]])
  • 我们可以从最外层往里面看,最外层的大括号有几个,说明该张量的维度有几个。
  • 从最外层的大括号开始,最外层的内容有一项:
1
2
3
4
5
[[[ 0.4024,  0.8702],
[-0.0625, -0.3826]],

[[-1.1489, -0.5550],
[ 0.0176, 1.0890]]]
  • 第二层有两项:
1
2
3
4
5
[[ 0.4024,  0.8702],
[-0.0625, -0.3826]],

[[-1.1489, -0.5550],
[ 0.0176, 1.0890]]
  • 第三层有两项:
1
2
[ 0.4024,  0.8702],
[-0.0625, -0.3826]
  • 第四层有两项:
1
0.4024,  0.8702
  • 所以维度为[1,2,2,2]

  • 例2,如下张量,我们也可快速得出张量shape = [2,2,2]

1
2
3
4
5
tensor([[[0.9278, 0.2627],
[0.8803, 0.9595]],

[[0.6918, 0.6921],
[0.7309, 0.9530]]])

11. 上采样常见插值

11.1 最近邻插值算法

  • 最简单的一种插值方法,在待求像素的四邻像素中,将距离待求像素最近的邻像素灰度赋给待求像素。
  • 设i+u, j+v(i, j为正整数, u, v为大于零小于1的小数,下同)为待求象素坐标,则待求象素灰度的值 f(i+u, j+v) 如下图所示:

  • 如果(i+u, j+v)落在A区,即u<0.5, v<0.5,则将左上角象素的灰度值赋给待求象素,同理,落在B区则赋予右上角的象素灰度值,落在C区则赋予左下角象素的灰度值,落在D区则赋予右下角象素的灰度值。
  • 最邻近元法可能会造成插值生成的图像灰度上的不连续,在灰度变化的地方可能出现明显的锯齿状。

11.2 单线性插值

  • 双线性插值实际上是从2个方向一共进行了3次单线性插值,咱们先了解单线性插值的计算方式。
  • 已知中P1点和P2点,坐标分别为(x1, y1)、(x2, y2),要计算 [x1, x2] 区间内某一位置 x 在直线上的y值:

  • 2点求一条直线公式(这是双线性插值所需要的唯一的基础公式):
  • 整理成下面的格式:
  • 现在再把公式中的分式看成一个整体,原式可以理解成 $y_1$ 与 $y_2$ 是加权系数,如何理解这个加权,要返回来思考一下,咱们先要明确一下根本的目的:咱们现在不是在求一个公式,而是在图像中根据2个点的像素值求未知点的像素值。这样一个公式是不满足咱们写代码的要求的。 现在根据实际的目的理解,就很好理解这个加权了,$y_1$与$y_2$分别代表原图像中的像素值,上面的公式可以写成如下形式:

11.3 双线性插值

  • 已知Q11(x1,y1)、Q12(x1,y2)、Q21(x2,y1)、Q22(x2,y2),求其中点P(x,y)的值。

  • 双线性插值是分别在两个方向计算了共3次单线性插值

  • 如图所示,先在x方向求2次单线性插值,获得R1(x, y1)、R2(x, y2)两个临时点,再在y方向计算1次单线性插值得出P(x, y)(实际上调换2次轴的方向先y后x也是一样的结果)

  • x方向单线性插值

    • 直接带入前一步单线性插值最后的公式
  • y方向单线性插值

  • 最后,将第一步结果带入第二步;

12.Focal Loss 问题

  • Focal Loss的引入主要是为了解决难易样本数量不平衡(注意,有区别于正负样本数量不平衡)的问题;

12.1 Focal Loss 原理分析

  • 二分类的情况下,模型最后需要预测的结果只有两种情况,对于每个类别我们的预测得到的概率为p和1-p,交叉熵的公式如下:
  • 即可转型为:
  • 其中,y表示样本的label,正为1,负为0;p表示样本预测为正类的概率

  • 为了解决正负样本不平衡的问题,我们通常会在交叉熵损失的前面加上一个参数 $\alpha$,如下:

  • 但这并不能解决全部问题。根据正、负、难、易,样本一共可以分为以下四类:
正难正易
负难负易
  • 尽管 $\alpha$ 平衡了正负样本,但对难易样本的不平衡没有任何帮助。而实际上,目标检测中大量的候选目标都是像下图一样的易分样本。

  • 这些样本的损失很低,但是由于数量极不平衡,易分样本的数量相对来讲太多,最终主导了总的损失。而本文的作者认为,易分样本(即,置信度高的样本)对模型的提升效果非常小,模型应该主要关注那些难分样本这个假设是有问题的,是GHM损失的主要改进对象

  • 这个时候,就有了Focal Loss,一个简单的思想:把高置信度(p)样本的损失再降低一些!

  • 举个例:$\gamma = 2$ 时,p = 0.968,则 $(1-0.968)^2 = 0.001$,损失也随之衰减了1000倍

  • Focal Loss的最终形式结合了上面的公式(2)。这很好理解,公式(3)解决了难易样本的不平衡,公式(2)解决了正负样本的不平衡,将公式(2)与(3)结合使用,同时解决正负难易2个问题!如下:

  • 实验表明:$\gamma=2; \alpha=0.25$ 时效果最好。

12.2 代码实现

  • 代码实现正在学习ing,待更新。

13. PyTorch中nn.CrossEntropyLoss()设置权重

13.1 正负样本数量不平衡问题

  • 这里直接拿我遇到的问题举例:做多分类的分割实验,其中训练数据集中,大部分数据为背景类,建筑类其次,车辆和道路类较少,这里拿图像(未切割)举例:

  • 下图的最右边被当成了测试集;

  • 可以看出:背景样本数目过多;车辆(蓝色)、道路样本数目少
  • 推荐一篇博文:解释了该损失函数的计算

  • 加入权重的理解为:假设有4个类别,0-3;对应的weight为[0.1,1,1,1],就意味着减轻了类别0的权重,所以,权重小,得到的loss更小,模型就会反而去关注loss的较大的类的训练;

  • 对类别比例小的样本给更大的权重系数,对类别比例大的样本给更小的权重系数,通过这种方式可以在一定程度上解决正负样本不均衡的问题。

1
2
3
4
# 损失函数和优化器
weight=torch.from_numpy(np.array([0.1,1.0,1.0,1.0])).float()
criterion = nn.CrossEntropyLoss(weight = weight).to(device) # CrossEntropyLoss适用多分类
optimizer = optim.Adam(deeplabv3_model.parameters(), lr=1e-3)
  • 关于权重的计算:
  • 在PyTorch官方论坛上看到的方法,该用户说这样得到了较好的效果,如下代码:
1
2
3
nSamples = [887, 6130, 480, 317, 972, 101, 128]
normedWeights = [1 - (x / sum(nSamples)) for x in nSamples]
normedWeights = torch.FloatTensor(normedWeights).to(device)
  • 先计算个类别的样本数,在我理解就是计算训练集的label的各类别的像素点数,对于语义分割实验,label的每个像素点的数字C代表C类,遍历每张label,计算所有label的所有类的点的数量;

  • 自己写的遍历函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import cv2
import os
from PIL import Image

def pixel():
images = os.listdir('data/dataset3/anno_prepped_train')

num_0 = 0
num_1 = 0
num_2 = 0
num_3 = 0

print('开始遍历:')

for image_name in images:
image = cv2.imread('data/dataset3/anno_prepped_train/' + image_name, 0)
for h in range(320):
for w in range(320):
if image[h,w]== 0:
num_0 = num_0+1
elif image[h,w]== 1:
num_1 = num_1+1
elif image[h,w]== 2:
num_2 = num_2+1
elif image[h,w]== 3:
num_3 = num_3+1
print(image_name +':遍历结束' )
print('遍历结束:')
print("num_0:" + str(num_0))
print("num_1:" + str(num_1))
print("num_2:" + str(num_2))
print("num_3:" + str(num_3))

nSamples = []
nSamples.append(num_0)
nSamples.append(num_1)
nSamples.append(num_2)
nSamples.append(num_3)


normedWeights = [1 - (x / sum(nSamples)) for x in nSamples]

print(normedWeights)

if __name__ == '__main__':
pixel()
  • 运行结果:

遍历结束:
num_0:603274142
num_1:95118988
num_2:80328758
num_3:20407712
[0.2450859760419336, 0.8809717622773577, 0.8994796863988019, 0.9744625752819067]

  • 训练结果:加入权重进行训练,效果提升明显。

14. Pseudo Labeling 策略

  • 最近学习到了一个实用的深度学习训练策略:Pseudo Labeling,有人称之为伪标签训练
  • 思想大致如下:
    • 在原先A训练数据集的基础上训练得到模型A_1;
    • 使用模型A_1进行预测测试集,将置信度较高的预测结果保存;
    • 将上步骤所得的预测结果B加入到A训练集中:得到新的训练集合C,A+B = C;
    • 载入模型A_1,在C数据集上进行训练,所得模型C_1;
    • 经测试,模型C_1较之前A_1有所提升;
  • kaggle上的一个notebook链接:传送门

15. FLOPs学习总结

15.1 什么是FLOPs

  • 论文中通常会比较在差不多的flops上两个模型的差距。比如说DenseNet 中就放出了一张在flops差不多的情况下,其与Resnet的对比图来说明DenseNet所需计算力小而正确率高的优势。
  • FLOPS:注意全大写,是floating point operations per second的缩写,意指每秒浮点运算次数,理解为计算速度。是一个衡量硬件性能的指标。
  • FLOPs:注意s小写,是floating point operations的缩写(s表复数),意指浮点运算数,理解为计算量。可以用来衡量算法/模型的复杂度。

  • 通常我们去评价一个模型时,首先看的应该是它的精确度

  • 当你模型达到一定的精确度之后,就需要更进一步的评价指标来评价你模型
    • 1)前向传播时所需的计算力,它反应了对硬件如GPU性能要求的高低;
    • 2)参数个数,它反应所占内存大小。
  • 为什么要加上这两个指标呢?因为这事关你模型算法的落地。比如你要在手机和汽车上部署深度学习模型,对模型大小和计算力就有严格要求。模型参数想必大家都知道是什么怎么算了,而前向传播时所需的计算力可能还会带有一点点疑问。所以这里总计一下前向传播时所需的计算力。它正是由FLOPs体现,那么FLOPs该怎么计算呢?

15.2 如何计算FLOPs

  • 在一个模型进行前向传播的时候,会进行卷积、池化、BatchNorm、Relu、Upsample等操作。这些操作的进行都会有其对应的计算力消耗产生,其中,卷积所对应的计算力消耗是所占比重最高的。所以,我们这里主要讲一下卷积操作所对应的计算力。
  • 以下图为例进行讲解:

  • Image大小为 5x5,卷积核大小为 3x3,那么一次3x3的卷积(求右图矩阵一个元素的值)所需运算量:(3x3)个乘法+(3x3-1)个加法 = 17。要得到右图convolved feature (3x3的大小):17x9 = 153

  • 如果输出 feature map 的分辨率是 H x W ,且输出 o 个 feature map,则输出 feature map 包含 Unit的总数就是 H x W x o。

  • k 表示卷积核大小;

  • c 表示输入 feature map 的数量;

  • 卷积层 wx + b 需要计算两部分,首先考虑前半部分 wx 的计算量:
  • 输出 feature map 上的单个 Unit 有:k x k x c 次乘法,以及 k x k x c - 1 次加法
  • 于是在 H x W x o的数量下:

    • k x k x c x H x W x o 次乘法 —(1)
    • (k x k x c - 1) x H x W x o 次加法 —(2)
  • 由于 b 只存在加法运算,输出 feature map 上的每个 Unit 做一次偏置项加法。因此,该卷积层在计算偏置项时总共包含:

    • H x W x o 次加法 —(3)
  • 将该卷积层的 wx 和 b 两部分的计算次数累计起来就有:
    • 式(1) 次乘法:
    • 式(2) + 式(3) 次加法:
  • 对于带偏置项的卷积层,乘法运算和加法运算的次数相等,刚好配对。定义一次加法和乘法表示一个flop,该层的计算力消耗为:k x k x c x H x W x o

  • 计算FLOPs的开源库:https://github.com/sovrasov/flops-counter.pytorch

16. ROC和AUC

16.1 TPR和FPR基础概念

  • 混淆矩阵中有着Positive、Negative、True、False的概念,其意义如下:
    • 称预测类别为1的为Positive(阳性),预测类别为0的为Negative(阴性)。
    • 预测正确的为True(真),预测错误的为False(伪)。
  • 对上述概念进行组合,就产生了如下的混淆矩阵:

  • 然后,由此引出True Positive Rate(真阳率)、False Positive(伪阳率)两个概念:
  • 仔细看这两个公式,发现其实TPRate就是TP除以TP所在的列,FPRate就是FP除以FP所在的列,二者意义如下:
    • TPRate的意义是所有真实类别为1的样本中,预测类别为1的比例。
    • FPRate的意义是所有真实类别为0的样本中,预测类别为1的比例。

16.2 ROC曲线和AUC

  • ROC曲线的横轴为FPR,纵轴为TPR;而AUC为ROC曲线下的面积;示例图如下

  • ROC曲线有4个关键点:
    • 点(0,0):FPR=TPR=0;即TP=FP=0,表示分类器预测的都为负样本
    • 点(1,1):FPR=TPR=1;即TN=FN=0,表示分类器预测的都为正样本
    • 点(0,1):FPR=0,TPR=1;即FN=FP=0,表示最优分类器,所有样本正确分类
    • 点(1,0):FPR=1,TPR=0;即TN=TP=0,表示最差分类器,所有样本错误分类
  • ROC曲线越接近左上角,表示该分类器性能越好

  • 举例如下:

  • 首先对于硬分类器(例如SVM,NB),预测类别为离散标签,对于8个样本的预测情况如下:

  • 得到的混淆矩阵如下:

  • 进而算得TPRate=3/4,FPRate=2/4,得到ROC曲线:最终得到AUC为0.625

  • 对于LR等预测类别为概率的分类器,依然用上述例子,假设预测结果如下:

  • 这时,需要设置阈值来得到混淆矩阵,不同的阈值会影响得到的TPRate,FPRate,如果阈值取0.5,小于0.5的为0,否则为1,那么我们就得到了与之前一样的混淆矩阵。其他的阈值就不再啰嗦了。依次使用所有预测值作为阈值,得到一系列TPRate,FPRate,描点,求面积,即可得到AUC。
  • AUC的优势:AUC的计算方法同时考虑了分类器对于正例和负例的分类能力,在样本不平衡的情况下,依然能够对分类器作出合理的评价。
  • 例如在反欺诈场景,设欺诈类样本为正例,正例占比很少(假设0.1%),如果使用准确率评估,把所有的样本预测为负例,便可以获得99.9%的准确率。但是如果使用AUC,把所有样本预测为负例,TPRate和FPRate同时为0(没有Positive),与(0,0) (1,1)连接,得出AUC仅为0.5,成功规避了样本不均匀带来的问题。

17. torch.cat() 方法简记

  • torch.cat是将两个张量(tensor)拼接在一起

  • 例子理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
>>> import torch
>>> A=torch.ones(2,3) #2x3的张量(矩阵)
>>> A
tensor([[ 1., 1., 1.],
[ 1., 1., 1.]])
>>> B=2*torch.ones(4,3)#4x3的张量(矩阵)
>>> B
tensor([[ 2., 2., 2.],
[ 2., 2., 2.],
[ 2., 2., 2.],
[ 2., 2., 2.]])
>>> C=torch.cat((A,B),0)#按维数0(行)拼接
>>> C
tensor([[ 1., 1., 1.],
[ 1., 1., 1.],
[ 2., 2., 2.],
[ 2., 2., 2.],
[ 2., 2., 2.],
[ 2., 2., 2.]])
>>> C.size()
torch.Size([6, 3])
>>> D=2*torch.ones(2,4) #2x4的张量(矩阵)
>>> C=torch.cat((A,D),1)#按维数1(列)拼接
>>> C
tensor([[ 1., 1., 1., 2., 2., 2., 2.],
[ 1., 1., 1., 2., 2., 2., 2.]])
>>> C.size()
torch.Size([2, 7])
  • 上面给出了两个张量A和B,分别是2行3列,4行3列。即他们都是2维张量。因为只有两维,这样在用torch.cat拼接的时候就有两种拼接方式:按行拼接和按列拼接。即所谓的维数0和维数1。
  • C=torch.cat((A,B),0)就表示按维数0(行)拼接A和B,也就是竖着拼接,A上B下。此时需要注意:列数必须一致,即维数1数值要相同,这里都是3列,方能列对齐。拼接后的C的第0维是两个维数0数值和,即2+4=6.
  • C=torch.cat((A,B),1)就表示按维数1(列)拼接A和B,也就是横着拼接,A左B右。此时需要注意:行数必须一致,即维数0数值要相同,这里都是2行,方能行对齐。拼接后的C的第1维是两个维数1数值和,即3+4=7.
  • 从2维例子可以看出,使用torch.cat((A,B),dim)时,除拼接维数dim数值可不同外其余维数数值需相同,方能对齐。
-------------The End-------------
谢谢大锅请我喝杯阔乐~