Dice与IOU
Dice loss与IOU loss哪个用于网络模型的训练比较好?
答: 都不太好。都是一种metric learning的衡量方式,两者都存在训练过程不稳定的问题,在和很小的情况下会得到较大的梯度,会影响正常的反向传播。一般情况下,使用两者对应的损失函数的原因是分割的真实目的是最大化这两个度量指标,而交叉熵是一种代理形式,利用了其在反向传播中易于最大化优化的特点。
所以,正常情况下是使用交叉熵损失函数来训练网络模型,用Dice或IOU系数来衡量模型的性能。因为,交叉熵损失函数得到的交叉熵值关于logits的梯度计算形式类似:p-g(p是softmax的输出结果,g是ground truth),这样的关系式自然在求梯度的时候容易的多。而Dice系数的可微形式,loss值为2pg/(p^2 + g^2)或2pg/(p+g),其关于p的梯度形式显然是比较复杂的,且在极端情况下(p,g的值都非常小时)计算得到的梯度值可能会非常大,进而会导致训练不稳定。
def dice_coef(output, target):#output为预测结果 target为真实结果
smooth = 1e-5 #防止0除
if torch.is_tensor(output):
output = torch.sigmoid(output).data.cpu().numpy()
if torch.is_tensor(target):
target = target.data.cpu().numpy()
intersection = (output * target).sum()
return (2. * intersection + smooth) / (output.sum() + target.sum() + smooth)
语义分割中的IOU
def iou_score(output, target):
smooth = 1e-5
if torch.is_tensor(output):
output = torch.sigmoid(output).data.cpu().numpy()
if torch.is_tensor(target):
target = target.data.cpu().numpy()
output_ = output > 0.5
target_ = target > 0.5
intersection = (output_ & target_).sum()
union = (output_ | target_).sum()
return (intersection + smooth) / (union + smooth)
def compute_ious(pred, label, classes):
'''computes iou for one ground truth mask and predicted mask'''
ious = [] # 记录每一类的iou
for c in classes:
label_c = (label == c) # label_c为true/false矩阵
pred_c = (pred == c)
intersection = np.logical_and(pred_c, label_c).sum()
union = np.logical_or(pred_c, label_c).sum()
if union == 0:
ious.append(float('nan'))
else
ious.append(intersection / union)
return np.nanmean(ious) #返回当前图片里所有类的mean iou
目标检测中的IOU
1 def bb_intersection_over_union(boxA, boxB):
#定义一个函数来计算IOU的值
2 boxA = [int(x) for x in boxA] #从boxA中提取每一个元素x,并且每一个元素均为整数
3 boxB = [int(x) for x in boxB] #..
4 xA = max(boxA[0], boxB[0]) #BoxA、BoxB两个宽度之间的交集的左边值,即阴影的宽度w的最左边值
5 yA = max(boxA[1], boxB[1]) #BoxA、BoxB两个高度之间的交集的上面边值,即阴影的高度h的最上边值
6 xB = min(boxA[2], boxB[2]) #BoxA、BoxB两个宽度之间的交集的左边值,即阴影的宽度w的最右边值
7 yB = min(boxA[3], boxB[3]) #BoxA、BoxB两个高度之间的交集的上面边值,即阴影的高度h的最下边值
#阴影部分的面积(xB - xA) *(yB - yA)
#个人认为此处+1 ,是保证两张图相同时,两者最大值都为0,使算的阴影面积不为0
8 interArea = max(0, xB - xA + 1) * max(0, yB - yA + 1)
9 boxAArea = (boxA[2] - boxA[0] + 1) * (boxA[3] - boxA[1] + 1) #boxA的面积
10 boxBArea = (boxB[2] - boxB[0] + 1) * (boxB[3] - boxB[1] + 1) #boxB的面积
11 iou = interArea / float(boxAArea + boxBArea - interArea)
12 return iou
GIOU、DIOU、CIOU
import cv2
import numpy as np
def GIOU_score(box1,box2):
"""
计算两个区域的iou的值
para: box1 区域1的两个角的坐标值 x1,y1,x2,y2
para: box2 区域2的两个角的坐标值 x1,y1,x2,y2
"""
# 两个框的交
iou_x1 = max(box1[0], box2[0])
iou_y1 = max(box1[1], box2[1])
iou_x2 = min(box1[2], box2[2])
iou_y2 = min(box1[3], box2[3])
g_iou_x1 = min(box1[0], box2[0])
g_iou_y1 = min(box1[1], box2[1])
g_iou_x2 = max(box1[2], box2[2])
g_iou_y2 = max(box1[3], box2[3])
# 上面求出来的为交集的两个角的坐标
area_inter = max(0,(iou_x2 - iou_x1)) * max(0 , (iou_y2 - iou_y1))
# 计算两个区域的并集
area_union = max(0,((box1[2] - box1[0]) * (box1[3] - box1[1])) + ((box2[2] - box2[0]) * (box2[3] - box2[1])) - area_inter)
# 计算最小外接矩形
area_all = max(0,(g_iou_x2 - g_iou_x1) * (g_iou_y2 - g_iou_y1))
g_iou = max(0,area_inter/area_union) - max(0,area_all - area_union) / area_all
return float(g_iou) , (iou_x1,iou_y1,iou_x2,iou_y2) , (g_iou_x1,g_iou_y1,g_iou_x2,g_iou_y2)
GIoU最主要的作用: (1)对于相交的框,IOU可以被反向传播,即它可以直接用作优化的目标函数。但是非相交的,梯度将会为0,无法优化。此时使用GIoU可以完全避免此问题。所以可以作为目标函数;(2)可以分辨框的对齐方式
def DIOU_score(box1,box2):
"""
计算两个区域的iou的值
para: box1 区域1的两个角的坐标值 x1,y1,x2,y2
para: box2 区域2的两个角的坐标值 x1,y1,x2,y2
"""
# 两个框的交
iou_x1 = max(box1[0], box2[0])
iou_y1 = max(box1[1], box2[1])
iou_x2 = min(box1[2], box2[2])
iou_y2 = min(box1[3], box2[3])
d_x1 = max(0, (box1[2] + box1[0])/2)
d_y1 = max(0, (box1[3] + box1[1])/2)
d_x2 = max(0, (box2[2] + box2[0])/2)
d_y2 = max(0, (box2[3] + box2[1])/2)
c_x1 = min(box1[0], box2[0])
c_y1 = min(box1[1], box2[1])
c_x2 = max(box1[2], box2[2])
c_y2 = max(box1[3], box2[3])
# 上面求出来的为交集的两个角的坐标
area_inter = max(0,(iou_x2 - iou_x1)) * max(0 , (iou_y2 - iou_y1))
# 计算两个区域的并集
area_union = max(0,((box1[2] - box1[0]) * (box1[3] - box1[1])) + ((box2[2] - box2[0]) * (box2[3] - box2[1])) - area_inter)
# 计算最小外接矩形
c_2 = max(0,(c_x2 - c_x1))**2 + max(0,(c_y2 - c_y1))**2
d_2 = max(0,(d_x2 - d_x1))**2 + max(0,(d_y2 - d_y1))**2
g_iou = max(0,area_inter/area_union) - d_2/c_2
return float(g_iou) , (iou_x1,iou_y1,iou_x2,iou_y2) , (c_x1,c_y1,c_x2,c_y2), (int(d_x1),int(d_y1),int(d_x2),int(d_y2))
DIoU的优点如下:
1.与GIoU loss类似,DIoU loss在与目标框不重叠时,仍然可以为边界框提供移动方向。
2.DIoU loss可以直接最小化两个目标框的距离,而GIOU loss优化的是两个目标框之间的面积,因此比GIoU loss收敛快得多。
3.对于包含两个框在水平方向和垂直方向上这种情况,DIoU损失可以使回归非常快,而GIoU损失几乎退化为IoU损失
def CIOU_score(box1,box2):
"""
计算两个区域的iou的值
para: box1 区域1的两个角的坐标值 x1,y1,x2,y2
para: box2 区域2的两个角的坐标值 x1,y1,x2,y2
"""
# 两个框的交
iou_x1 = max(box1[0], box2[0])
iou_y1 = max(box1[1], box2[1])
iou_x2 = min(box1[2], box2[2])
iou_y2 = min(box1[3], box2[3])
d_x1 = max(0, (box1[2] + box1[0])/2)
d_y1 = max(0, (box1[3] + box1[1])/2)
d_x2 = max(0, (box2[2] + box2[0])/2)
d_y2 = max(0, (box2[3] + box2[1])/2)
c_x1 = min(box1[0], box2[0])
c_y1 = min(box1[1], box2[1])
c_x2 = max(box1[2], box2[2])
c_y2 = max(box1[3], box2[3])
w_gt = max(0,box2[2] - box2[0])
h_gt = max(0,box2[3] - box2[1])
w = max(0,box1[2] - box1[0])
h = max(0,box1[3] - box1[1])
# 上面求出来的为交集的两个角的坐标
area_inter = max(0,(iou_x2 - iou_x1)) * max(0 , (iou_y2 - iou_y1))
# 计算两个区域的并集
area_union = max(0,((box1[2] - box1[0]) * (box1[3] - box1[1])) + ((box2[2] - box2[0]) * (box2[3] - box2[1])) - area_inter)
iou = max(0,area_inter/area_union)
c_2 = max(0,(c_x2 - c_x1))**2 + max(0,(c_y2 - c_y1))**2
d_2 = max(0,(d_x2 - d_x1))**2 + max(0,(d_y2 - d_y1))**2
v = 4/pi**2 * (atan(w_gt/h_gt) - atan(w/h))**2
alpha = v / (1-iou + v)
c_iou = iou - d_2/c_2 - alpha * v
return float(c_iou) , (iou_x1,iou_y1,iou_x2,iou_y2) , (c_x1,c_y1,c_x2,c_y2), (int(d_x1),int(d_y1),int(d_x2),int(d_y2))
CIoU比DIoU多出了α和v这两个参数。其中α是用于平衡比例的参数。v用来衡量anchor框和目标框之间的比例一致性。从α参数的定义可以看出,损失函数会更加倾向于往重叠区域增多方向优化,尤其是IoU为零的时候。
ROC与AUC
ROC曲线:接收者操作特征(receiveroperating characteristic), roc曲线上每个点反映着对同一信号刺激的感受性。
横轴:假正类率 (false postive rate, FPR),特异度,划分实例中所有负例占所有负例的比例;TNR=1-FPR。
纵轴:真正类率 ==Recall(true postive rate, TPR),灵敏度,Sensitivity(正类覆盖率)。
截断点取不同的值,TPR和FPR的计算结果也不同。将截断点不同取值下对应的TPR和FPR结果画于二维坐标系中得到的曲线,就是ROC曲线。从高到低,依次将“Score”值作为阈值threshold,当测试样本属于正样本的概率大于这个threshold时认为是正样本,否则为负样本。从第一个样本开始,设该样本的Score值为阈值,则该样本及之后的样本(均比该样本概率值小)判为负样本,即所有样本判为全负,计算得TPR=FPR=0,即ROC曲线(0,0)点;再选择第二个样本点的Score作为阈值,大于等于该阈值的样本(在该样本之前)判为正样本,小于该阈值的判为负样本,那么此时TP = 1, TP+FN = all gt,计算TPR = recall。以此类推。
PR (Precision Recall) 曲线
PR曲线展示的是Precision vs Recall的曲线,PR曲线与ROC曲线的相同点是都采用了TPR (Recall),都可以用AUC来衡量分类器的效果。不同点是ROC曲线使用了FPR,而PR曲线使用了Precision,因此PR曲线的两个指标都聚焦于正例。类别不平衡问题中由于主要关心正例,所以在此情况下PR曲线被广泛认为优于ROC曲线。类别不平衡问题中ROC曲线确实会作出一个比较乐观的估计,而PR曲线则因为Precision的存在会不断显现FP的影响。
- ROC曲线由于兼顾正例与负例,所以适用于评估分类器的整体性能,相比而言PR曲线完全聚焦于正例。
- 如果有多份数据且存在不同的类别分布,比如信用卡欺诈问题中每个月正例和负例的比例可能都不相同,这时候如果只想单纯地比较分类器的性能且剔除类别分布改变的影响,则ROC曲线比较适合,因为类别分布改变可能使得PR曲线发生变化时好时坏,这种时候难以进行模型比较;反之,如果想测试不同类别分布下对分类器的性能的影响,则PR曲线比较适合。
- 如果想要评估在相同的类别分布下正例的预测情况,则宜选PR曲线。
- 类别不平衡问题中,ROC曲线通常会给出一个乐观的效果估计,所以大部分时候还是PR曲线更好。
- 最后可以根据具体的应用,在曲线上找到最优的点,得到相对应的precision,recall,f1 score等指标,去调整模型的阈值,从而得到一个符合具体应用的模型。
AUC(Area under Curve):ROC曲线下的面积,介于0.1和1之间。AUC作为数值可以直观的评价分类器的好坏,值越大越好。
首先AUC值是一个概率值,当你随机挑选一个正样本以及负样本,当前的分类算法根据计算得到的Score值将这个正样本排在负样本前面的概率就是AUC值,AUC值越大,当前分类算法越有可能将正样本排在负样本前面,从而能够更好地分类。
最直观的,根据AUC这个名称,我们知道,计算出ROC曲线下面的面积,就是AUC的值。事实上,这也是在早期 Machine Learning文献中常见的AUC计算方法。由于测试样本是有限的。得到的AUC曲线必然是一个阶梯状的。因此,计算的AUC也就是这些阶梯下面的面积之和。这样,我们先把score排序(假设score越大,此样本属于正类的概率越大),然后一边扫描就可以得到想要的AUC。但是,这做有个缺点,就是当多个测试样本的score相等的时候,我们调整一下阈值,得到的不是曲线一个阶梯往上或者往右的延展,而是斜着向上形成一个梯形。此 时,我们就需要计算这个梯形的面积。由此,我们可以看到,用这种方法计算AUC实际上是比较麻烦的。
测试任意给一个正类样本和一个负类样本,正类样本的score有多大的概率大于负类样本的score。具体来说就是统计一下所有的 M×N(M为正类样本的数目,N为负类样本的数目)个正负样本对中,有多少个组中的正样本的score大于负样本的score。当二元组中正负样本的 score相等的时候,按照0.5计算。然后除以MN。实现这个方法的复杂度为O(n^2)。n为样本数(即n=M+N)
公式法:它也是首先对score从大到小排序,然后令最大score对应的sample 的rank为n,第二大score对应sample的rank为n-1,以此类推。然后把所有的正类样本的rank相加,再减去M-1种两个正样本组合的情况。得到的就是所有的样本中有多少对正类样本的score大于负类样本的score。然后再除以M×N。即
def calc_auc(y_labels, y_scores):
f = list(zip(y_scores, y_labels))
rank = [values2 for values1, values2 in sorted(f, key=lambda x: x[0])]
rankList = [i + 1 for i in range(len(rank)) if rank[i] == 1]
pos_cnt = np.sum(y_labels == 1)
neg_cnt = np.sum(y_labels == 0)
auc = (np.sum(rankList) - pos_cnt * (pos_cnt + 1) / 2) / (pos_cnt * neg_cnt)
return auc
#传统的方法
def auc_calculate(labels,preds,n_bins=100):
postive_len = sum(labels)
negative_len = len(labels) - postive_len
total_case = postive_len * negative_len
pos_histogram = [0 for _ in range(n_bins)]
neg_histogram = [0 for _ in range(n_bins)]
bin_width = 1.0 / n_bins
for i in range(len(labels)):
nth_bin = int(preds[i]/bin_width)
if labels[i]==1:
pos_histogram[nth_bin] += 1
else:
neg_histogram[nth_bin] += 1
accumulated_neg = 0
satisfied_pair = 0
for i in range(n_bins):
satisfied_pair += (pos_histogram[i]*accumulated_neg + pos_histogram[i]*neg_histogram[i]*0.5)
accumulated_neg += neg_histogram[i]
return satisfied_pair / float(total_case)
AUC意义的理解:AUC是评估分类器效果的,它解决的阈值不同设定导致的分类效果问题,显然当设定不同阈值时,对产生不同的分类结果,但是AUC却可以避免由于阈值的不同设定导致模型评估的不便利,因为AUC评估的是正负例判定概率的错开度,AUC越是接近1代表正负例判断概率越是错的开,那么越可以选择一个合适的阈值,将分类结果达到最好的效果。
深度学习常见代码
全连接层前向和反向
import numpy as np
# 定义线性层网络
class Linear():
"""
线性全连接层
"""
def __init__(self, dim_in, dim_out):
"""
参数:
dim_in: 输入维度
dim_out: 输出维度
"""
# 初始化参数
scale = np.sqrt(dim_in / 2)
self.weight = np.random.standard_normal((dim_in, dim_out)) / scale
self.bias = np.random.standard_normal(dim_out) / scale
# self.weight = np.random.randn(dim_in, dim_out)
# self.bias = np.zeros(dim_out)
self.params = [self.weight, self.bias]
def __call__(self, X):
"""
参数:
X:这一层的输入,shape=(batch_size, dim_in)
return:
xw + b
"""
self.X = X
return self.forward()
def forward(self):
return np.dot(self.X, self.weight) + self.bias
def backward(self, d_out):
"""
参数:
d_out:输出的梯度, shape=(batch_size, dim_out)
return:
返回loss对输入 X 的梯度(前一层(l-1)的激活值的梯度)
"""
# 计算梯度
# 对input的梯度有batch维度,对参数的梯度对batch维度取平均
d_x = np.dot(d_out, self.weight.T) # 输入也即上一层激活值的梯度
d_w = np.dot(self.X.T, d_out) # weight的梯度
d_b = np.mean(d_out, axis=0) # bias的梯度
return d_x, [d_w, d_b]
Dropout前向与反向
class Dropout():
"""
在训练时随机将部分feature置为0
"""
def __init__(self, p):
"""
parameters:
p: 保留比例
"""
self.p = p
def __call__(self, X, mode):
"""
mode: 是在训练阶段还是测试阶段. train 或者 test
"""
return self.forward(X, mode)
def forward(self, X, mode):
if mode == 'train':
self.mask = np.random.binomial(1, self.p, X.shape) / self.p
out = self.mask * X
else:
out = X
return out
def backward(self, d_out):
"""
d_out: loss对dropout输出的梯度
"""
return d_out * self.mask
激活函数
常用激活函数介绍
# 定义Relu层
class Relu(object):
def __init__(self):
self.X = None
def __call__(self, X):
self.X = X
return self.forward(self.X)
def forward(self, X):
return np.maximum(0, X)
def backward(self, grad_output):
"""
grad_output: loss对relu激活输出的梯度
return: relu对输入input_z的梯度
"""
grad_relu = self.X > 0 # input_z大于0的提放梯度为1,其它为0
return grad_relu * grad_output # numpy中*为点乘
class Tanh():
def __init__(self):
self.X = None
def __call__(self, X):
self.X = X
return self.forward(self.X)
def forward(self, X):
return np.tanh(X)
def backward(self, grad_output):
grad_tanh = 1 - (np.tanh(self.X)) ** 2
return grad_output * grad_tanh
class Sigmoid():
def __init__(self):
self.X = None
def __call__(self, X):
self.X = X
return self.forward(self.X)
def forward(self, X):
return self._sigmoid(X)
def backward(self, grad_output):
sigmoid_grad = self._sigmoid(self.X) * (1 - self._sigmoid(self.X))
return grad_output * sigmoid_grad
def _sigmoid(self, X):
return 1.0 / (1 + np.exp(-X))
Softmax
import numpy as np
def softmax(x):
exp_x = np.exp(x) # ps 下面结果中的e+01 是科学计数法 e+01 = 10
#print(exp_x) # [2.20264658e+04 7.38905610e+00 2.35385267e+17 5.45981500e+01]
sum_exp_x = np.sum(exp_x)
sm_x = exp_x/sum_exp_x
return sm_x
可能存在一个数值不稳定的情况
def softmax(x):
max_x = np.max(x) # 最大值
exp_x = np.exp(x - max_x)
sum_exp_x = np.sum(exp_x)
sm_x = exp_x/sum_exp_x
return sm_x
print(softmax(x))
激活函数的非零均值问题
Softmax 和 SVM 比较
- 计算上有差异:
SVM 和 Softmax 分类器对于数据有不同的处理方式。两个分类器都计算了同样的分值向量 (本节中是通过矩阵乘来实现)。不同之处在于对s中分值的解释:
SVM 分类器将它们看做是类别评分,它的损失函数鼓励正确的类别(本例中是蓝色的类别2)的分值比其他类别的分值高出至少一个安全边界值。
Softmax 分类器将这些数值看做是每个类别没有归一化的对数概率,鼓励正确分类的归一化的对数概率变高,其余的变低。 - 损失的绝对数值不可以直接解释:
SVM 的计算是无标定的,而且难以针对所有分类的评分值给出直观解释。Softmax 分类器则不同,它允许我们计算出对于所有分类标签的 『概率』。
但这里要注意,『不同类别概率』 分布的集中或离散程度是由正则化参数直接决定的。随着正则化参数 不断增强,权重数值会越来越小,最后输出的概率会接近于均匀分布。
也就是说,Softmax 分类器算出来的概率可以某种程度上视作一种对于分类正确性的自信。和 SVM 一样,数字间相互比较得出的大小顺序是可以解释的,但其绝对值则难以直观解释。 - 实际应用时,SVM 和 Softmax 是相似的
两种分类器的表现差别很小。相对于 Softmax 分类器,SVM 更加 『局部目标化(local objective)』,只要看到正确分类相较于不正确分类,已经得到了比边界值还要高的分数,它就会认为损失值是0,对于数字个体的细节是不关心的。
Softmax 分类器对于分数是永不满足的:正确分类总能得到更高的概率,错误分类总能得到更低的概率,损失值总是能够更小。
卷积层前向与反向传播
Im2Col
im2col是将一个[C,H,W]矩阵变成一个[H,W]矩阵的一个方法,其原理是利用了行列式进行等价转换。
一个[1, 6, 6]的输入,卷积核是[1, 3, 3],stride等于1,padding等于0。那么卷积的过程可视化如下图,一共需要做16次卷积计算,每次卷积计算有9次乘法和8次加法。
class Img2colIndices():
"""
卷积网络的滑动计算实际上是将feature map转换成为矩阵乘法的方式。
卷积计算forward前需要将feature map转换成为cols格式,每一次滑动的窗口作为cols的一列
卷积计算backward时需要将cols态的梯度转换成为与输入map shape一致的格式
该辅助类完成feature map --> cols 以及 cols --> feature map
设计卷积、maxpool、average pool都有可能用到该类进行转换操作
"""
def __init__(self, filter_size, padding, stride):
"""
parameters:
filter_shape: 卷积核的尺寸(h_filter, w_filter)
padding: feature边缘填充0的个数
stride: filter滑动步幅
"""
self.h_filter, self.w_filter = filter_size
self.padding = padding
self.stride = stride
def get_img2col_indices(self, h_out, w_out):
"""
获得需要由image转换为col的索引, 返回的索引是在feature map填充后对于尺寸的索引
获得每次卷积时,在feature map上卷积的元素的坐标索引。以后img2col时根据索引获得
i 的每一行,如第r行是filter第r个元素(左右上下的顺序)在不同位置卷积时点乘的元素的位置的row坐标索引
j 的每一行,如第r行是filter第r个元素(左右上下的顺序)在不同位置卷积时点乘的元素的位置的column坐标索引
结果i、j每一列,如第c列是filter第c次卷积的位置卷积的k×k个元素(左右上下的顺序)。
每一列长filter_height*filter_width*C,由于C个通道,每C个都是重复的,表示在第几个通道上做的卷积。
parameters:
h_out: 卷积层输出feature的height
w_out: 卷积层输出feature的width。每次调用imgcol时计算得到
return:
k: shape=(filter_height*filter_width*C, 1), 每挨着的filter_height*filter_width元素值都一样,表示从第几个通道取点
i: shape=(filter_height*filter_width*C, out_height*out_width), 依次待取元素的横坐标索引
j: shape=(filter_height*filter_width*C, out_height*out_width), 依次待取元素的纵坐标索引
"""
i0 = np.repeat(np.arange(self.h_filter), self.w_filter)
i1 = np.repeat(np.arange(h_out), w_out) * self.stride
i = i0.reshape(-1, 1) + i1
i = np.tile(i, [self.c_x, 1])
j0 = np.tile(np.arange(self.w_filter), self.h_filter)
j1 = np.tile(np.arange(w_out), h_out) * self.stride
j = j0.reshape(-1, 1) + j1
j = np.tile(j, [self.c_x, 1])
k = np.repeat(np.arange(self.c_x), self.h_filter * self.w_filter).reshape(-1, 1)
return k, i, j
def img2col(self, X):
"""
基于索引取元素的方法实现img2col
parameters:
x: 输入feature map,shape=(batch_size, channels, height, width)
return:
转换img2col,shape=(h_filter * w_filter*chanels, batch_size * h_out * w_out)
"""
self.n_x, self.c_x, self.h_x, self.w_x = X.shape
# 首先计算出输出特征的尺寸
# 计算输出feature的尺寸,并且保证是整数
h_out = (self.h_x + 2 * self.padding - self.h_filter) / self.stride + 1
w_out = (self.w_x + 2 * self.padding - self.w_filter) / self.stride + 1
if not h_out.is_integer() or not w_out.is_integer():
raise Exception("Invalid dimention")
else:
h_out, w_out = int(h_out), int(w_out) # 上一步在进行除法后类型会是float
# 0填充输入feature map
x_padded = None
if self.padding > 0:
x_padded = np.pad(X, ((0, 0), (0, 0), (self.padding, self.padding), (self.padding, self.padding)), mode='constant')
else:
x_padded = X
# 在计算出输出feature尺寸后,并且0填充X后,获得img2col_indices
# img2col_indices设为实例的属性,col2img时用,避免重复计算
self.img2col_indices = self.get_img2col_indices(h_out, w_out)
k, i, j = self.img2col_indices
# 获得参与卷积计算的col形式
cols = x_padded[:, k, i, j] # shape=(batch_size, h_filter*w_filter*n_channel, h_out*w_out)
cols = cols.transpose(1, 2, 0).reshape(self.h_filter * self.w_filter * self.c_x, -1) # reshape
return cols
def col2img(self, cols):
"""
img2col的逆过程
卷积网络,在求出x的梯度时,dx是col矩阵的形式(filter_height*filter_width*chanels, batch_size*out_height*out_width)
将dx有col格式转换成feature map的原尺寸格式。由get_img2col_indices获得该尺寸下的索引,使用numpt.add.at方法还原成img格式
parameters:
cols: dx的col形式, shape=(h_filter*w_filter*n_chanels, batch_size*h_out*w_out)
"""
# 将col还原成img2col的输出shape
cols = cols.reshape(self.h_filter * self.w_filter * self.c_x, -1, self.n_x)
cols = cols.transpose(2, 0, 1)
h_padded, w_padded = self.h_x + 2 * self.padding, self.w_x + 2 * self.padding
x_padded = np.zeros((self.n_x, self.c_x, h_padded, w_padded))
k, i, j = self.img2col_indices
np.add.at(x_padded, (slice(None), k, i, j), cols)
if self.padding == 0:
return x_padded
else:
return x_padded[:, :, self.padding : -self.padding, self.padding : -self.padding]
Conv2d前向与反向
卷积的过程,会调用im2col的函数。
class Conv2d():
def __init__(self, in_channels, n_filter, filter_size, padding, stride):
"""
parameters:
in_channel: 输入feature的通道数
n_filter: 卷积核数目
filter_size: 卷积核的尺寸(h_filter, w_filter)
padding: 0填充数目
stride: 卷积核滑动步幅
"""
self.in_channels = in_channels
self.n_filter = n_filter
self.h_filter, self.w_filter = filter_size
self.padding = padding
self.stride = stride
# 初始化参数,卷积网络的参数size与输入的size无关
self.W = np.random.randn(n_filter, self.in_channels, self.h_filter, self.w_filter) / np.sqrt(n_filter / 2.)
self.b = np.zeros((n_filter, 1))
self.params = [self.W, self.b]
def __call__(self, X):
# 计算输出feature的尺寸
self.n_x, _, self.h_x, self.w_x = X.shape
self.h_out = (self.h_x + 2 * self.padding - self.h_filter) / self.stride + 1
self.w_out = (self.w_x + 2 * self.padding - self.w_filter) / self.stride + 1
if not self.h_out.is_integer() or not self.w_out.is_integer():
raise Exception("Invalid dimensions!")
self.h_out, self.w_out = int(self.h_out), int(self.w_out)
# 声明Img2colIndices实例
self.img2col_indices = Img2colIndices((self.h_filter, self.w_filter), self.padding, self.stride)
return self.forward(X)
def forward(self, X):
# 将X转换成col
self.x_col = self.img2col_indices.img2col(X)
# 转换参数W的形状,使它适合与col形态的x做计算
self.w_row = self.W.reshape(self.n_filter, -1)
# 计算前向传播
out = self.w_row @ self.x_col + self.b # @在numpy中相当于矩阵乘法,等价于numpy.matmul()
out = out.reshape(self.n_filter, self.h_out, self.w_out, self.n_x)
out = out.transpose(3, 0, 1, 2)
return out
def backward(self, d_out):
"""
parameters:
d_out: loss对卷积输出的梯度
"""
# 转换d_out的形状
d_out_col = d_out.transpose(1, 2, 3, 0)
d_out_col = d_out_col.reshape(self.n_filter, -1)
d_w = d_out_col @ self.x_col.T
d_w = d_w.reshape(self.W.shape) # shape=(n_filter, d_x, h_filter, w_filter)
d_b = d_out_col.sum(axis=1).reshape(self.n_filter, 1)
d_x = self.w_row.T @ d_out_col
# 将col态的d_x转换成image格式
d_x = self.img2col_indices.col2img(d_x)
return d_x, [d_w, d_b]
BatchNorm2d前向反向
- 加快网络的训练与收敛的速度
在深度神经网络中,如果每层的数据分布都不一样的话,将会导致网络非常难收敛和训练。如果把每层的数据都在转换在均值为零,方差为1 的状态下,这样每层数据的分布都是一样的训练会比较容易收敛。 - 控制梯度爆炸防止梯度消失
以sigmoid函数为例,sigmoid函数使得输出[0,1]在之间,实际上当 输入过大或者过小,经过sigmoid函数后输出范围就会变得很小,而且反向传播时的梯度也会非常小,从而导致梯度消失,同时也会导致网络学习速率过慢;同时由于网络的前端比后端求梯度需要进行更多次的求导运算,最终会出现网络后端一直学习,而前端几乎不学习的情况。Batch Normalization (BN) 通常被添加在每一个全连接和激励函数之间,使数据在进入激活函数之前集中分布在0值附近,大部分激活函数输入在0周围时输出会有加大变化。
同样,使用了BN之后,可以使得权值不会很大,不会有梯度爆炸的问题。 - 防止过拟合
在网络的训练中,BN的使用使得一个batch中所有样本都被关联在了一起,因此网络不会从某一个训练样本中生成确定的结果,即同样一个样本的输出不再仅仅取决于样本的本身,也取决于跟这个样本同属一个batch的其他样本,而每次网络都是随机取batch,比较多样,可以在一定程度上避免了过拟合。
class BatchNorm2d():
"""
对卷积层来说,批量归一化发生在卷积计算之后、应用激活函数之前。
如果卷积计算输出多个通道,我们需要对这些通道的输出分别做批量归一化,且每个通道都拥有独立的拉伸和偏移参数,并均为标量。
设小批量中有 m 个样本。在单个通道上,假设卷积计算输出的高和宽分别为 p 和 q 。我们需要对该通道中 m×p×q 个元素同时做批量归一化。
对这些元素做标准化计算时,我们使用相同的均值和方差,即该通道中 m×p×q 个元素的均值和方差。
将训练好的模型用于预测时,我们希望模型对于任意输入都有确定的输出。
因此,单个样本的输出不应取决于批量归一化所需要的随机小批量中的均值和方差。
一种常用的方法是通过移动平均估算整个训练数据集的样本均值和方差,并在预测时使用它们得到确定的输出。
"""
def __init__(self, n_channel, momentum):
"""
parameters:
n_channel: 输入feature的通道数
momentum: moving_mean/moving_var迭代调整系数
"""
self.n_channel = n_channel
self.momentum = momentum
# 参与求梯度和迭代的拉伸和偏移参数,分别初始化成1和0
self.gamma = np.ones((1, n_channel, 1, 1))
self.beta = np.zeros((1, n_channel, 1, 1))
# 测试时使用的参数,初始化为0,需在训练时动态调整
self.moving_mean = np.zeros((1, n_channel, 1, 1))
self.moving_var = np.zeros((1, n_channel, 1, 1))
self.params = [self.gamma, self.beta]
def __call__(self, X, mode):
"""
X: shape = (N, C, H, W)
mode: 训练阶段还是测试阶段,train或test, 需要在调用时传参
"""
self.X = X # 求gamma的梯度时用
return self.forward(X, mode)
def forward(self, X, mode):
"""
X: shape = (N, C, H, W)
mode: 训练阶段还是测试阶段,train或test, 需要在调用时传参
"""
if mode != 'train':
# 如果是在预测模式下,直接使用传入的移动平均所得的均值和方差
self.x_norm = (X - self.moving_mean) / np.sqrt(self.moving_var + 1e-5)
else:
# 使用二维卷积层的情况,计算通道维上(axis=1)的均值和方差。
# 这里我们需要保持X的形状以便后面可以做广播运算
mean = X.mean(axis=(0, 2, 3), keepdims=True)
self.var = X.var(axis=(0, 2, 3), keepdims=True) # 设为self,是因为backward时会用到
# 训练模式下用当前的均值和方差做标准化。设为类实例的属性,backward时用
self.x_norm = (X - mean) / (np.sqrt(self.var + 1e-5))
# 更新移动平均的均值和方差
self.moving_mean = self.momentum * self.moving_mean + (1 - self.momentum) * mean
self.moving_var = self.momentum * self.moving_var + (1 - self.momentum) * self.var
# 拉伸和偏移
out = self.x_norm * self.gamma + self.beta
return out
def backward(self, d_out):
"""
d_out的形状与输入的形状一样
"""
d_gamma = (d_out * self.x_norm).sum(axis=(0, 2, 3), keepdims=True)
d_beta = d_out.sum(axis=(0, 2, 3), keepdims=True)
d_x = (d_out * self.gamma) / np.sqrt(self.var + 1e-5)
return d_x, [d_gamma, d_beta]
损失函数
Focal loss
class FocalLoss(nn.Module):
def __init__(self, alpha=1, gamma=2, logits=False, reduce=True):
super(FocalLoss, self).__init__()
self.alpha = alpha
self.gamma = gamma
self.logits = logits
self.reduce = reduce
def forward(self, inputs, targets):
if self.logits:
BCE_loss = F.binary_cross_entropy_with_logits(inputs, targets, reduce=False)
else:
BCE_loss = F.binary_cross_entropy(inputs, targets, reduce=False)
pt = torch.exp(-BCE_loss)
F_loss = self.alpha * (1-pt)**self.gamma * BCE_loss
if self.reduce:
return torch.mean(F_loss)
else:
return F_loss
Dice loss
### From https://www.kaggle.com/bigironsphere/loss-function-library-keras-pytorch
class DiceLoss(nn.Module):
def __init__(self, weight=None, size_average=True):
super(DiceLoss, self).__init__()
def forward(self, inputs, targets, smooth=1):
#comment out if your model contains a sigmoid or equivalent activation layer
inputs = F.sigmoid(inputs)
#flatten label and prediction tensors
inputs = inputs.view(-1)
targets = targets.view(-1)
intersection = (inputs * targets).sum()
dice = (2.*intersection + smooth)/(inputs.sum() + targets.sum() + smooth)
return 1 - dice
class DiceBCELoss(nn.Module):
def __init__(self, weight=None, size_average=True):
super(DiceBCELoss, self).__init__()
def forward(self, inputs, targets, smooth=1):
#comment out if your model contains a sigmoid or equivalent activation layer
inputs = F.sigmoid(inputs)
#flatten label and prediction tensors
inputs = inputs.view(-1)
targets = targets.view(-1)
intersection = (inputs * targets).sum()
dice_loss = 1 - (2.*intersection + smooth)/(inputs.sum() + targets.sum() + smooth)
BCE = F.binary_cross_entropy(inputs, targets, reduction='mean')
Dice_BCE = BCE + dice_loss
return Dice_BCE
IOU loss
### From https://www.kaggle.com/bigironsphere/loss-function-library-keras-pytorch
class IoULoss(nn.Module):
def __init__(self, weight=None, size_average=True):
super(IoULoss, self).__init__()
def forward(self, inputs, targets, smooth=1):
#comment out if your model contains a sigmoid or equivalent activation layer
inputs = F.sigmoid(inputs)
#flatten label and prediction tensors
inputs = inputs.view(-1)
targets = targets.view(-1)
#intersection is equivalent to True Positive count
#union is the mutually inclusive area of all labels & predictions
intersection = (inputs * targets).sum()
total = (inputs + targets).sum()
union = total - intersection
IoU = (intersection + smooth)/(union + smooth)
return 1 - IoU
其他
Kmeans聚类
- 从样本中选择 K 个点作为初始质心(完全随机)
- 计算每个样本到各个质心的距离,将样本划分到距离最近的质心所对应的簇中
- 计算每个簇内所有样本的均值,并使用该均值更新簇的质心
- 重复步骤 2 与 3 ,直到达到以下条件之一:
质心的位置变化小于指定的阈值(默认为 0.0001)
达到最大迭代次数
# 计算中心点和其他点直接的距离
def calc_distance(obs, guess_central_points):
"""
:param obs: 所有的观测点
:param guess_central_points: 中心点
:return:每个点对应中心点的距离
"""
distances = []
for x, y in obs:
distance = []
for xc, yc in guess_central_points:
distance.append(math.dist((x, y), (xc, yc)))
distances.append(distance)
return distances
def k_means(obs, k, dist=np.median):
"""
:param obs: 待观测点
:param k: 聚类数k
:param dist: 表征聚类中心函数
:return: guess_central_points中心点
current_cluster 分类结果
"""
obs_num = obs.shape[0]
if k < 1:
raise ValueError("Asked for %d clusters." % k)
# 随机取中心点
guess_central_points = obs[np.random.choice(obs_num, size=k, replace=False)] # 初始化最大距离
last_cluster = np.zeros((obs_num, ))
# 当小于一定值时聚类完成
while True:
# 关键是下面的calc_distance,来计算需要的距离
distances = calc_distance(obs, guess_central_points)
# 获得对应距离最小值的索引
current_cluster = np.argmin(distances, axis=1)
# 如果聚类类别没有改变, 则直接退出
if (last_cluster == current_cluster).all():
break
# 计算新的中心
for i in range(k):
guess_central_points[i] = dist(obs[current_cluster == i], axis=0)
last_cluster = current_cluster
NMS
--(非极大值抑制): 当两个空间位置非常接近,就以更高的那个作为基准,看即重合度如何,如果与其重合度超过阈值,就抑制更小的,只保留大的就,其它的就都应该过滤掉。对于而言,适合于水平框,针对各种不同形状的框,会有不同的来进行处理。
具体的步骤如下:
- 如图所示,我们有个带置信率的 ,我们先预设一个的阈值如。
- 按置信率大小对个框排序,举例为 。
- 设定置信率为的 为一个物体框;
- 在剩下个 中进行循环遍历,去掉与物体框大于的。
- 重复~的步骤,直到没有 为止。
- 每次获取到的最大置信率的 就是我们筛选出来的目标。
import numpy as np
def NMS(dets, thresh):
"""Pure Python NMS baseline."""
# tl_x,tl_y,br_x,br_y及score
x1 = dets[:, 0]
y1 = dets[:, 1]
x2 = dets[:, 2]
y2 = dets[:, 3]
scores = dets[:, 4]
#计算每个检测框的面积,并对目标检测得分进行降序排序
areas = (x2 - x1 + 1) * (y2 - y1 + 1)
order = scores.argsort()[::-1]
keep = [] #保留框的结果集合
while order.size > 0:
i = order[0]
keep.append(i) #保留该类剩余box中得分最高的一个
# 计算最高得分矩形框与剩余矩形框的相交区域
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:]])
#计算相交的面积,不重叠时面积为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)
#保留IoU小于阈值的box
inds = np.where(ovr <= thresh)[0]
order = order[inds + 1] #注意这里索引加了1,因为ovr数组的长度比order数组的长度少一个
return keep
Soft NMS
说到Soft NMS,首先需要了解传统NMS有哪些缺点。其主要缺点包括如下:
- 物体重叠:如下面第一张图,会有一个最高分数的框,如果使用的话就会把其他置信度稍低,但是表示另一个物体的预测框删掉(由于和最高置信度的框overlap过大)
- 所有的bbox都预测不准:不是所有的框都那么精准,有时甚至会出现某个物体周围的所有框都标出来了,但是都不准的情况。
- 传统的NMS方法是基于分类分数的,只有最高分数的预测框能留下来,但是大多数情况下IoU和分类分数不是强相关,很多分类标签置信度高的框都位置都不是很准。
Soft NMS主要是针对NMS过度删除框的问题。Soft-NMS吸取了NMS的教训,在算法执行过程中不是简单的对IoU大于阈值的检测框删除,而是降低得分。算法流程同NMS相同,但是对原置信度得分使用函数运算,目标是降低置信度得分。其算法步骤如下:
红色的部分表示原始NMS算法,绿色部分表示Soft-NMS算法,区别在于,绿色的框只是把降低了,而不是把直接去掉,极端情况下,如果只返回,那么等同于普通的NMS。为待处理框,为待处理框集合,是框更新得分,是NMS的阈值,集合用来放最终的,是置信度得分的重置函数。和的越大,的得分就下降的越厉害。函数是为了降低目标框的置信度,满足条件,如果和的越大,就应该越小,Soft-NMS提出了两种函数:
经典的NMS算法将大于阈值的窗口的得分全部置为,可表述如下:
论文置信度重置函数有两种形式改进,一种是线性加权的:
一种是高斯加权形式:
Soft NMS算法的优点如下: - 该方案可以很方便地引入到object detection算法中,不需要重新训练原有的模型;
- soft-NMS在训练中采用传统的NMS方法,可以仅在推断代码中实现soft-NMS。
- NMS是Soft-NMS特殊形式,当得分重置函数采用二值化函数时,Soft-NMS和NMS是相同的。soft-NMS算法是一种更加通用的非最大抑制算法。
而,在一些场景的实验中,可以看到Soft NMS的效果也是优于NMS的。[图片上传失败...(image-89e089-1662626669836)]
def cpu_soft_nms(np.ndarray[float, ndim=2] boxes, float sigma=0.5, float Nt=0.3, float threshold=0.001, unsigned int method=0):
cdef unsigned int N = boxes.shape[0]
cdef float iw, ih, box_area
cdef float ua
cdef int pos = 0
cdef float maxscore = 0
cdef int maxpos = 0
cdef float x1,x2,y1,y2,tx1,tx2,ty1,ty2,ts,area,weight,ov
for i in range(N):
# 在i之后找到confidence最高的框,标记为max_pos
maxscore = boxes[i, 4]
maxpos = i
tx1 = boxes[i,0]
ty1 = boxes[i,1]
tx2 = boxes[i,2]
ty2 = boxes[i,3]
ts = boxes[i,4]
pos = i + 1
# 找到max的框
while pos < N:
if maxscore < boxes[pos, 4]:
maxscore = boxes[pos, 4]
maxpos = pos
pos = pos + 1
# 交换max_pos位置和i位置的数据
# add max box as a detection
boxes[i,0] = boxes[maxpos,0]
boxes[i,1] = boxes[maxpos,1]
boxes[i,2] = boxes[maxpos,2]
boxes[i,3] = boxes[maxpos,3]
boxes[i,4] = boxes[maxpos,4]
# swap ith box with position of max box
boxes[maxpos,0] = tx1
boxes[maxpos,1] = ty1
boxes[maxpos,2] = tx2
boxes[maxpos,3] = ty2
boxes[maxpos,4] = ts
tx1 = boxes[i,0]
ty1 = boxes[i,1]
tx2 = boxes[i,2]
ty2 = boxes[i,3]
ts = boxes[i,4]
# 交换完毕
# 开始循环
pos = i + 1
while pos < N:
# 先记录内层循环的数据bi
x1 = boxes[pos, 0]
y1 = boxes[pos, 1]
x2 = boxes[pos, 2]
y2 = boxes[pos, 3]
s = boxes[pos, 4]
# 计算iou
area = (x2 - x1 + 1) * (y2 - y1 + 1)
iw = (min(tx2, x2) - max(tx1, x1) + 1) # 计算两个框交叉矩形的宽度,如果宽度小于等于0,即没有相交,因此不需要判断
if iw > 0:
ih = (min(ty2, y2) - max(ty1, y1) + 1) # 同理
if ih > 0:
ua = float((tx2 - tx1 + 1) * (ty2 - ty1 + 1) + area - iw * ih) #计算union面积
ov = iw * ih / ua #iou between max box and detection box
if method == 1: # linear
if ov > Nt:
weight = 1 - ov
else:
weight = 1
elif method == 2: # gaussian
weight = np.exp(-(ov * ov)/sigma)
else: # original NMS
if ov > Nt:
weight = 0
else:
weight = 1
boxes[pos, 4] = weight*boxes[pos, 4]
# if box score falls below threshold, discard the box by swapping with last box
# update N
if boxes[pos, 4] < threshold:
boxes[pos,0] = boxes[N-1, 0]
boxes[pos,1] = boxes[N-1, 1]
boxes[pos,2] = boxes[N-1, 2]
boxes[pos,3] = boxes[N-1, 3]
boxes[pos,4] = boxes[N-1, 4]
N = N - 1
pos = pos - 1
pos = pos + 1
keep = [i for i in range(N)]
return keep