文章目录
传统图像处理
介绍canny边缘检测算法
边缘图像,是对原始图像进行边缘提取后得到的图像。边缘是图像性区域和另一个属性区域的交接处,是区域属性发生突变的地方,是图像中不确定性最大的地方,也是图像信息最集中的地方,图像的边缘包含着丰富的信息。
一种流行的边缘检测算法,多阶段算法。具体包括以下四个步骤
- 图像降噪
- 计算图像梯度
- 非极大值抑制
- 阈值筛选
1. 图像降噪
图像中的噪声是灰度变化很大的地方,容易被识别为伪边缘,因此第一步要先去除噪声。Canny使用高斯滤波使图像变得平滑(模糊),同时也有可能增大边缘的宽度。高斯滤波简单来说就是用一个高斯矩阵乘以每一个像素点及其邻域,取带权重的平均值作为最后的灰度值
简单易懂的高斯滤波
高斯滤波:
先引入两个问题。
- 图像为什么要滤波?
a.消除图像在数字化过程中产生或者混入的噪声。
b.提取图片对象的特征作为图像识别的特征模式。 - 滤波器该如何去理解?
答:滤波器可以想象成一个包含加权系数的窗口或者说一个镜片,当使用滤波器去平滑处理图像的时候,就是把通过这个窗口或者镜片去看这个图像。
滤波器分为很多种,有方框滤波、均值滤波、高斯滤波等。
高斯滤波是一种线性平滑滤波,适用于消除高斯噪声。 所以在讲高斯滤波之前,先解释一下什么是高斯噪声?
1 高斯噪声
首先,噪声在图像当中常表现为一引起较强视觉效果的孤立像素点或像素块。简单来说,噪声的出现会给图像带来干扰,让图像变得不清楚。 高斯噪声就是它的概率密度函数服从高斯分布(即正态分布)的一类噪声。如果一个噪声,它的幅度分布服从高斯分布,而它的功率谱密度又是均匀分布的,则称它为高斯白噪声。高斯白噪声的二阶矩不相关,一阶矩为常数,是指先后信号在时间上的相关性。
高斯滤波器是根据高斯函数的形状来选择权值的线性平滑滤波器 所以接下来再讲解一下高斯函数和高斯核。
2 高斯函数
注:σ的大小决定了高斯函数的宽度。
3 高斯核
理论上,高斯分布在所有定义域上都有非负值,这就需要一个无限大的卷积核。实际上,仅需要取均值周围3倍标准差内的值,以外部份直接去掉即可。 高斯滤波的重要两步就是先找到高斯模板然后再进行卷积,模板(mask在查阅中有的地方也称作掩膜或者是高斯核)。所以这个时候需要知道它怎么来?又怎么用? 举个栗子: 假定中心点的坐标是(0,0),那么取距离它最近的8个点坐标,为了计算,需要设定σ的值。假定σ=1.5,则模糊半径为1的高斯模板就算如下
这个时候我们我们还要确保这九个点加起来为1(这个是高斯模板的特性),这9个点的权重总和等于0.4787147,因此上面9个值还要分别除以0.4787147,得到最终的高斯模板。
4 高斯滤波计算
有了高斯模板,那么高斯滤波的计算便顺风顺水了。 举个栗子:假设现有9个像素点,灰度值(0-255)的高斯滤波计算如下:
将这9个值加起来,就是中心点的高斯滤波的值。 对所有点重复这个过程,就得到了高斯模糊后的图像。
5 高斯滤波步骤
综上可以总结一下步骤:
(1)移动相关核的中心元素,使它位于输入图像待处理像素的正上方 (2)将输入图像的像素值作为权重,乘以相关核 (3)将上面各步得到的结果相加做为输出 简单来说就是根据高斯分布得到高斯模板然后做卷积相加的一个过程。
2. 计算图像梯度
图像梯度的基本原理
sobel算子
3.非极大值抑制NMS
沿着边缘,通常观察到很少有点使边缘的可见性更清晰。所以我们可以忽略那些对特征可见性贡献不大的边缘点。为了达到同样的目的,我们使用非最大抑制方法。这里我们标记边缘曲线上幅度最大的点。这可以通过寻找最大值以及与曲线垂直的切片来获得。
4. 阈值筛选(滞后阈值)
图像模糊有什么用
我们可以看到,相对于原始图像,一些较小的物体已经融入背景,看不到了,有些物体即使能看到,亮度也明显降低。这样,我们用图像模糊将图像中较大的较亮的物体保留了下来,而其它的物体则消除了。我们进一步通过阈值处理对模糊后的图像进行操作,将最高亮度的25%作为阈值,低于此阈值的赋为0,高于此阈值的赋为255。
C++底层代码实现图像3*3均值滤波/中值滤波
#include<iostream>
#include<vector>
using namespace std;
int main() {
//定义被卷积的矩阵(其实是一个数组,数组元素的个数为8*8)
const int img_sz = 8;
float img[img_sz][img_sz];
for (int i = 0; i < img_sz; i++) {
for (int j = 0; j < img_sz; j++) {
img[i][j] = i +j;
}
}
//定义卷积核矩阵(其实也是一个数组)
const int kernel_sz = 3;
const float kernel[kernel_sz][kernel_sz] =
{
1,1,1,
1,1,1,
1,1,1,
};
const int out_img_sz = img_sz - kernel_sz + 1;
float out_img[out_img_sz][out_img_sz] = { 0 };
for (int i = 0; i < out_img_sz; i++) {
for (int j = 0; j < out_img_sz; j++) {
float sum = 0;
for (int k = 0; k < kernel_sz; k++) {
for (int m = 0; m < kernel_sz; m++) {
int row = i + k;
int col = j + m;
sum += img[row][col] * kernel[k][m];
}
}
out_img[i][j] = sum / (kernel_sz * kernel_sz);
}
}
for (int i = 0; i < out_img_sz; i++) {
for (int j = 0; j < out_img_sz; j++) {
cout << out_img[i][j] << " ";
}
cout << endl;
}
cout << endl;
}
中值滤波:
#include<iostream>
#include<vector>
using namespace std;
#include<algorithm>
int main() {
const int img_sz = 8;
float img[img_sz][img_sz];
for (int i = 0; i < img_sz; i++) {
for (int j = 0; j < img_sz; j++) {
img[i][j] = i +j;
}
}
const int kernel_sz = 3;
vector<float> kernel(kernel_sz*kernel_sz,0);
const int out_img_sz = img_sz - kernel_sz + 1;
float out_img[out_img_sz][out_img_sz] = { 0 };
for (int i = 0; i < out_img_sz; i++) {
for (int j = 0; j < out_img_sz; j++) {
for (int k = 0; k < kernel_sz; k++) {
for (int m = 0; m < kernel_sz; m++) {
int r = i + k;
int c = j + m;
kernel[k*kernel_sz+m] = img[r][c];
}
}
sort(kernel.begin(), kernel.end());
out_img[i][j] = kernel[4];
}
}
for (int i = 0; i < out_img_sz; i++) {
for (int j = 0; j < out_img_sz; j++) {
cout << out_img[i][j] << " ";
}
cout << endl;
}
cout << endl;
}
python手写卷积伪代码
import numpy as np
img_array = np.array([[1,2,3,4,5,6],[1,2,3,4,5,6],[1,2,3,4,5,6]])
kernel = np.array([[1,1],[1,1]])
padding = 1
step = 1
def pad(feature, padding):
m, n = feature.shape
new_m = m + 2*padding
new_n = n + 2*padding
new_feature = np.zeros((new_m,new_n))
new_feature[padding:padding+m,padding:padding+n] = feature[:,:]
return new_feature
def conv(feature_map, kernel, padding, step):
m, n = feature_map.shape
k_m, k_n = kernel.shape
target_x = (m+2*padding-k_m)/step + 1
target_y = (n+2*padding-k_n)/step + 1
target = (int(target_x), int(target_y))
res_map = np.zeros(target)
if padding != 0:
feature_map = pad(feature_map,padding)
m, n = feature_map.shape
for i in range(0,m-k_m+1):
for j in range(0,n-k_n+1):
new_array = feature_map[i:i+k_m,j:j+k_n]
new_data = new_array * kernel
res_map[i][j] = new_data.sum()
return res_map
print(img_array)
ok = conv(img_array,kernel,1,step)
print(ok)
import numpy as np
dfs conv(feature_map, mask_conv, step):
m, n = feature_map.shape
conv_x, conv_y = mask_conv.shape
target_x = (m - conv_x) / step + 1
target_y = (n - conv_y) / step + 1
out_feature_map = np.zeros((int(target_x),int(target_y)))
for i in range(0,m-conv_x+1):
for j in range(0,n-conv_y+1):
new_array = feature_map[i:i+conv_x,j:j+conv_y]
new_data = new_array * mask_conv
out_feature_map[i][j] = new_data.sum()
return out_feature_map
池化
# 池化(最大值)
import numpy as np
# 创建图像(大小为原图的1/4)
new_image2 = np.zeros((int(width/2), int(height/2)))
# 遍历池化(步长为2)
for x in range(0, width, 2):
for y in range(0, height, 2):
# 选取最大像素
pixels = []
pixels.append(new_image[x][y])
pixels.append(new_image[x+1][y])
pixels.append(new_image[x][y+1])
pixels.append(new_image[x+1][y+1])
new_image2[int(x/2)][int(y/2)] = max(pixels)
# 显示图像
plt.gray()
plt.imshow(new_image2)
plt.show()
SIFT特征
SIFT特征也叫做尺度不变特征,SIFT特征最后是把输入的图像表现成为128维的特征向量集合,SIFT特征具有旋转,缩放,平移,光照不变形
1. 尺度空间的极值检测:高斯差分金字塔->极值检测
目的:我们的目的是要找到一些特征,这些特征具有尺度不变形,也就是说一张图在不同的尺寸下面,我们仍然可以找到这些特征
- SIFT特征要有远近不变性,无论摄像机拍的远近都能识别同一个物体,高斯核模拟近处清晰远处模糊。另外,要找到图片在不同尺度空间里的极值,通常在边缘或者灰度突变的地方,所以要对高斯模糊后的图像进行差分。
- 二维高斯函数定义如下,其中,σ表示标准差,
由信号处理相关知识可知,将图像函数与高斯函数卷积等同于将图像的频谱与高斯函数的傅里叶变换相乘,因为高斯函数的傅立叶变换仍然是高斯函数,因此这等同于对源图像进行了低通滤波,即平滑效果,而且,σ值越大,滤波后的图像越模糊,得到高斯差分金字塔。
- 在得到DOG图像后,对于每一个octave的一组图像,查找所有的极值点,以下图为例,该极值点像素值大于局部8领域点和上下两个Scale的像素点,也就是说,定义一个极值点时,需要将当前像素点同9*2+8=26个像素值做比较。
2. 关键点的准确定位
+边缘点过滤
- 上一部找到的极值点是在离散空间找到的,离散空间的极值点并不一定是真真的连续空间的极值点,通过插值找到真正的极值点的位置。
利用已知的离散空间点插值得到的连续空间极值点的方法叫做子像素插值,利用尺度空间DOG函数进行曲线拟合,然后对其函数利用泰勒展开式,求得一个偏移量,从而实现对行(x),列(y)以及尺度(sigma)进行修正,一般认为,偏移量小于0.5则调整完毕.
3. 关键点主方向确定
上面找到的关键点保证了尺度不变形,为了实现旋转不变形,我们需要给特征点的方向进行赋值,利用特征点周围的像素点的梯度分布,统计生成一个梯度直方图从而方便来确定特征点的主方向。
(1)对于每一个特征点,选取其16×16的局部邻域;
(2)将16×16的局部邻域进行校准。具体操作为:绘制梯度分布直方图,选择出主方向,然后将该局部邻域做旋转使其同主方向对齐;
(3)将坐标轴旋转为主方向,以确保旋转不变形
4. 生成特征向量
- 将该局部邻域划分成4×4=16 个sub-blocks,对于每一个sub-block,绘制其方向直方图(横坐标为梯度方向索引,纵坐标为当前梯度方向的像素点个数),这里选取了8个梯度方向,所以每一个sub-block有8个特征值,当前特征点对应的总的特征值维度为8∗16=128,也即,该128维特征向量作为当前特征点的描述。
归一化
最后我们对生成的特征向量进行归一化处理,该处理可以去除光照的影响
SIFT特征的优缺点
优点:
通过SIFT提取的特征有着旋转平移不变形,尺度不变形,光照不变形,其中:
-
旋转不变形是因为在生成特征向量之前,我们需要将坐标轴做一个映射使其旋转到主方向,因此有了一定的旋转不变形
-
平移不变形是因为SIFT在计算特征向量的时候,提取关键点周围的区域的样本点,所以如果该特征点移动到任何处于该区域内的位置都可以被提取出来,这就有点像CNN中的pooling
-
尺度不变形是首先因为我们通过DOG拟合LOG,而LOG又经前人的证明可以在不同尺度下检测到图像的特征,其次通过DOG我们可以拟合出来不同的尺度的情况,在这种情况下求出来的关键点,自然是具有尺度不变形的
4.光照不变形是因为我们对最后的特征向量进行了归一化
缺点:
-
SIFT高度依赖局部区域像素的梯度,有可能这个区域取得不合适,导致我们找的主方向不准确,从而导致计算出来的特征向量误差很大,从而不能成功匹配
-
另外我们在进行SIFT特征选取之前,可以看一下图像的像素值分布,如果像素值分布过于集中,那么SIFT的表现也不会很好,对此我们可以做一些图像均衡化的处理
特征点匹配
对于两幅图像A和B,为了使用特征点匹配验证是否为相同图,需要经过如下步骤,
(1)分别得到图像A和B对应的特征点集合S1 和S2
(2)对于集合S1中的每一个特征点,分别找到S2中对应的最近邻特征点,可以用欧式距离作为度量标准,若小于阈值,则表明当前特征点匹配成功,否则匹配失败;
(3)若集合S1中匹配成功个数大于指定阈值,则表明图像A近似为图像B的一部分,判定为相同图片;
HOG特征
方向梯度直方图(Histogram of OrientedGradient,HOG)特征是一种在计算机视觉和图像处理中用来进行物体检测的特征描述子。它通过计算和统计图像局部区域的梯度方向直方图来构成特征。Hog特征结合SVM分类器已经被广泛应用于图像识别中,尤其在行人检测中获得了极大的成功。
计算每一个像素点的梯度值,得到梯度图(规模和原图大小一样)
计算梯度直方图
对16*16大小的block归一化
得到HOG特征向量
机器/深度学习
有哪些loss函数
python手动实现onehot编码
from numpy import argmax
data = 'hello world'##定义输入字符串
print(data)
alphabet = 'abcdefjhigklmnopqrstuvwxyz '//定义所有可能的输入值
char_to_int = dict((c,i) for i,c in enumerate(alphabet))//定义字符到int的映射
int_to_char = dict((i,c) for i,c in enumerate(alphabet))
integer_encoded = [char_to_int[char] for char in data]
onehot_encoded = list()
for value in integer_encoded ://独热编码
letter = [0 for _ in range(len(alphabet))]
letter[value] = 1
onehot_encoded.append(letter)
print(onehot_encoded)
inverted = int_to_char[argmax(onehot_encoded[0])];
print(inverted)
生成式模型和判别式模型的区别并举一些例子
生成模型:学习得到联合概率分布P(x,y),即特征x,y共同出现的概率。
常见的生成模型:朴素贝叶斯模型,混合高斯模型,HMM模型。
判别模型:学习得到条件概率分布P(y|x),即在特征x出现的情况下标记y出现的概率。
常见的判别模型:感知机,决策树,逻辑回归,SVM,CRF等。
参考链接
判别式模型:要确定一个羊是山羊还是绵羊,用判别式模型的方法是从历史数据中学习到模型,然后通过提取这只羊的特征来预测出这只羊是山羊的概率,是绵羊的概率。
生成式模型:是根据山羊的特征首先学习出一个山羊的模型,然后根据绵羊的特征学习出一个绵羊的模型,然后从这只羊中提取特征,放到山羊模型中看概率是多少,再放到绵羊模型中看概率是多少,哪个大就是哪个。
双线性插值及其python实现
解释1:
双线性插值(超级易懂的)
解释2:
源图像和目标图像几何中心的对齐
在计算源图像的虚拟浮点坐标的时候,一般情况:
srcX=dstX (srcWidth/dstWidth) ,
srcY = dstY (srcHeight/dstHeight)
中心对齐(OpenCV也是如此):
SrcX=(dstX+0.5) (srcWidth/dstWidth) -0.5
SrcY=(dstY+0.5) (srcHeight/dstHeight)-0.5
原理:
将公式变形:srcX=dstX (srcWidth/dstWidth)+0.5(srcWidth/dstWidth-1)相当于我们在原始的浮点坐标上加上了0.5*(srcWidth/dstWidth-1)这样一个控制因子,这项的符号可正可负,与srcWidth/dstWidth的比值也就是当前插值是扩大还是缩小图像有关,有什么作用呢?
看一个例子:假设源图像是33,中心点坐标(1,1)目标图像是99,中心点坐标(4,4),我们在进行插值映射的时候,尽可能希望均匀的用到源图像的像素信息,最直观的就是(4,4)映射到(1,1)现在直接计算srcX=43/9=1.3333!=1,也就是我们在插值的时候所利用的像素集中在图像的右下方,而不是均匀分布整个图像。现在考虑中心点对齐,srcX=(4+0.5)3/9-0.5=1,刚好满足我们的要求。
python实现
import numpy as np
def bilinear_interpolation(img, out_dim):
src_h, src_w, channel = img.shape # 原图片的高、宽、通道数
dst_h, dst_w = out_dim[1], out_dim[0] # 输出图片的高、宽
print('src_h,src_w=', src_h, src_w)
print('dst_h,dst_w=', dst_h, dst_w)
if src_h == dst_h and src_w == dst_w:
return img.copy()
dst_img = np.zeros((dst_h, dst_w, 3), dtype=np.uint8)
# dst_img = [[ 0 for col in range(dst_w)] for row in range(dst_h)]
scale_x, scale_y = float(src_w) / dst_w, float(src_h) / dst_h
for i in range(3): # 指定 通道数,对channel循环
for dst_y in range(dst_h): # 指定 高,对height循环
for dst_x in range(dst_w): # 指定 宽,对width循环
# 源图像和目标图像几何中心的对齐
# src_x = (dst_x + 0.5) * srcWidth/dstWidth - 0.5
# src_y = (dst_y + 0.5) * srcHeight/dstHeight - 0.5
src_x = (dst_x + 0.5) * scale_x - 0.5
src_y = (dst_y + 0.5) * scale_y - 0.5
# 计算在源图上四个近邻点的位置
src_x0 = int(np.floor(src_x))
src_y0 = int(np.floor(src_y))#np.floor()返回不大于输入参数的最大整数。(向下取整)
src_x1 = min(src_x0 + 1, src_w - 1)
src_y1 = min(src_y0 + 1, src_h - 1)
# 双线性插值
# 存储横/纵向一次插值的结果
temp0 = (src_x1 - src_x) * img[src_y0, src_x0, i] + (src_x - src_x0) * img[src_y0, src_x1, i]
temp1 = (src_x1 - src_x) * img[src_y1, src_x0, i] + (src_x - src_x0) * img[src_y1, src_x1, i]
dst_img[dst_y, dst_x, i] = int((src_y1 - src_y) * temp0 + (src_y - src_y0) * temp1)
return dst_img
卷积和插值的关系
所有使用电脑的人都会用到图片放大的操作,而把图片放大超过其分辨率后,图片就会模糊。传统放大图片都是采用插值的方法,最常用的有最近邻插值(Nearest-neighbor)、双线性插值(Bilinear)、双立方插值(bicubic)等,这些方法的速度非常快,因此被广泛采用。
基于卷积神经网络的方案很容易理解:传统插值方法可以看做把像素复制放大倍数后,用某种固定的卷积核去卷积;而基于卷积神经网络的超分方法无非是去学习这个卷积核,根据构建出的超分图像与真实的高分辨率图像(Ground Truth)的差距去更新网络参数。
信息量,信息熵,相对熵(KL散度),交叉熵
信息量
- 信息奠基人香农(Shannon)认为“信息是用来消除随机不确定性的东西”。也就是说衡量信息量大小就看这个信息消除不确定性的程度。“太阳从东方升起了”这条信息没有减少不确定性。因为太阳肯定从东面升起。这是句废话,信息量为0。
- 信息量用一个信息所需要的编码长度来定义,而一个信息的编码长度之所以跟其出现的概率呈负相关,是因为一个短编码的代价也是巨大的,因为会放弃所有以其为前缀的编码方式,比如字母”a”用单一个0作为编码的话,那么为了避免歧义,就不能有其他任何0开头的编码词了.所以一个词出现的越频繁,则其编码方式也就越短,同时付出的代价也大.
- “太阳从东方升起了”这条信息没有减少不确定性。因为太阳肯定从东面升起。这是句废话,信息量为0。
“吐鲁番下中雨了”(吐鲁番年平均降水量日仅6天)这条信息比较有价值,为什么呢,因为按统计来看吐鲁番明天不下雨的概率为98%(1-6/300),对于吐鲁番下不下雨这件事,首先它是随机不去确定的,这条信息直接否定了发生概率为98%的事件------不下雨,把非常大概率的事情(不下雨)否定了,即消除不确定性的程度很大,所以这条信息的信息量比较大。这条信息的情形发生概率仅为2%但是它的信息量去很大,上面太阳从东方升起的发生概率很大为1,但信息量确很小。
从上面两个例子可以看出:信息量的大小和事件发生的概率成反比。
信息量的表示如下:
熵
熵可以衡量一个系统的混乱程度, 从信息的角度来说,是从一种定量的角度来衡量信息多少的指标。简单来说,就是信息所包含的不确定性的大小,一个信息所包含的事件的不确定性越大,它所含的信息就越多。熵的本质是香农信息量的期望值。
熵的定义:如果一个随机变量X的可能取值为X = {x1, x2,…, xk},其概率分布为P(X = xi) = pi(i = 1,2, …, n),则随机变量X的熵定义为:
信息熵
理解1: 信息熵是用来衡量任意随机变量或整个系统(事物)不确定性的。变量或系统的不确定性越大,熵也就越大,事物越具不确定性(事物越复杂)把它搞清楚所需要的信息量也就越大。
理解2: 信息熵是信息论中用于度量信息量的一个概念。一个系统越是有序,信息熵就越低;反之,一个系统越是混乱,信息熵就越高。所以,信息熵也可以说是系统有序化程度的一个度量。
理解3: 信息量度量的是一个具体事件发生所带来的信息,而信息熵则是在结果出来之前对可能产生的信息量的期望——考虑该随机变量的所有可能取值,即所有可能发生事件所带来的信息量的期望。
理解4: 信息熵则代表一个分布的信息量,或者编码的平均长度(即信息量的均值)
信息熵的表示如下:
相对熵/K-L散度
-
1951年,Kullback和Leibler提出了K-L散度(Kullback-Leibler divergence,也被称作相对熵,relative entropy)用来度量两个分布的偏离程度。
-
相对熵,又称KL散度( Kullback–Leibler divergence),是描述两个概率分布P和Q差异的一种方法。它是非对称的,这意味着D(P||Q) ≠ D(Q||P)。特别的,在信息论中,D(P||Q)表示当用概率分布Q来拟合真实分布P时,产生的信息损耗,其中P表示真实分布,Q表示P的拟合分布。
-
有人将KL散度称为KL距离,但事实上,KL散度并不满足距离的概念,因为:(1)KL散度不是对称的;(2)KL散度不满足三角不等式。
-
比如有两个系统Q和P,事件在两个系统中发生的概率是不一样的。如果对于这一个事件,用它在系统Q中的信息量-它对应到P中的信息量,这个差值最后求整体的期望,就是它的相对熵。(注意,pi在前面代表以P为基准)
在机器学习中, p 往往用来表示样本的真实分布, q 用来表示模型所预测的分布,那么KL散度就可以计算两个分布的差异,也就是Loss损失值。
相对熵的物理定义:
我们已经知道信息熵的物理含义是最短的信息编码的长度,但是假如说我们估计的不准会怎么样?
比如:
稍微变形一下KL散度的公式:
l公式后半部分就是这个事件真实的信息熵即传输信息所需的最少编码,而前半部分指的的若使用了一个错误编码会导致传输使用多少比特。所以相对熵就是指,相对于最优的编码,使用错误的编码会浪费多少比特?
交叉熵
我们从上面KL散度的式子中可以看出,后一部分其实就是P的信息熵,那么前一部分其实就是交换熵。P和Q的交换熵=P和Q的KL散度-P的熵
KL散度 = 交叉熵 - 熵
如果我们默认了用KL散度来计算两个分布间的不同,那还要交叉熵做什么?
对比一下这是KL散度的公式:
机器如何学习?
拓展阅读:
我们知道在神经网络训练开始前,都要对输入数据做一个归一化处理,那么具体为什么需要归一化呢?归一化后有什么好处呢?原因在于神经网络学习过程本质就是为了学习数据分布,一旦训练数据与测试数据的分布不同,那么网络的泛化能力也大大降低;另外一方面,一旦每批训练数据的分布各不相同(batch 梯度下降),那么网络就要在每次迭代都去学习适应不同的分布,这样将会大大降低网络的训练速度,这也正是为什么我们需要对数据都要做一个归一化预处理的原因。
对于深度网络的训练是一个复杂的过程,只要网络的前面几层发生微小的改变,那么后面几层就会被累积放大下去。一旦网络某一层的输入数据的分布发生改变,那么这一层网络就需要去适应学习这个新的数据分布,所以如果训练过程中,训练数据的分布一直在发生变化,那么将会影响网络的训练速度。
我们知道网络一旦train起来,那么参数就要发生更新,除了输入层的数据外(因为输入层数据,我们已经人为的为每个样本归一化),后面网络每一层的输入数据分布是一直在发生变化的,因为在训练的时候,前面层训练参数的更新将导致后面层输入数据分布的变化。以网络第二层为例:网络的第二层输入,是由第一层的参数和input计算得到的,而第一层的参数在整个训练过程中一直在变化,因此必然会引起后面每一层输入数据分布的改变。我们把网络中间层在训练过程中,数据分布的改变称之为:“Internal Covariate Shift”。Paper所提出的算法,就是要解决在训练过程中,中间层数据分布发生改变的情况,于是就有了Batch Normalization,这个牛逼算法的诞生。
为什么可以用交叉熵作为代价函数?
机器学习的过程就是希望在训练数据上学到的分布P(model)和真实分布P(real)越接近越好,最小化两个分布之间的关系就是使其KL散度最小。但我我们没有真实分布,只能退而求其次,希望学到的模型分布P(model)与训练数据的分布P(training)一致。
对抗损失
Softmax函数求导
单个输出节点的二分类问题一般在输出节点上使用Sigmoid函数,拥有两个及其以上的输出节点的二分类或者多分类问题一般在输出节点上使用Softmax函数。其他层建议使用的激活函数可以参考下面的文章。
现在可以构建比较复杂的神经网络模型,最重要的原因之一得益于反向传播算法。反向传播算法从输出端也就是损失函数开始向输入端基于链式法则计算梯度,然后通过计算得到的梯度,应用梯度下降算法迭代更新待优化参数。
由于反向传播计算梯度基于链式法则,因此下面为了更加清晰,首先推导一下Softmax函数的导数。作为最后一层的激活函数,求导本身并不复杂,但是需要注意需要分成两种情况来考虑。
为了方便说明,先来简单看一个小例子。
介绍常见的轻量化网络
对于轻量化的网络设计,目前较为流行的有SqueezeNet、 MobileNet、ShuffleNet等结构。其中,SqueezeNet采用压缩再扩展的结构,MobileNet使用了效率更高的深度可分离卷积,而ShuffleNet提出了通道混洗的操作,从而进一步降低了模 型的计算量。
SqueezeNet
Fire Module: Squeeze and Expand
SqueezeNet 的主要模块为 Fire Module,它主要从网络结构优化的角度出发,使用了如下 3 点策略来减少网络参数,提升网络性能:
(1) 使用 1 × 1 卷积来替代部分的 3 × 3 卷积,可以将参数减少为原来的 1 / 9 ,同时减少输入通道的数量
(2) 利用 1 × 1 卷积来减少输入通道的数量
(3) 在减少通道数之后,使用多个尺寸的卷积核进行计算,以保留更多的信息,提升分类的准确率
其中,
SqueezeNet层:首先使用1×1卷积进行降维,特征图的尺寸不变,这里的S1小于M,达到了压缩的目的。(S1可控制channel的个数)。
Expand层:并行地使用1×1卷积与3×3卷积获得不同感受野的特征图,有点类似Inception模块,达到扩展的目的。
Concat层:对得到的两个特征图进行通道拼接,作为最终输出。
import torch
from torch import nn
class Fire(nn.Module):
def __init__(self, inplanes, squeeze_planes, expand_planes):
# 不改变输入特征图的宽高,只改变通道数
# 输入通道数为 inplanes,压缩通道数为 squeeze_planes,输出通道数为 2 * expand_planes
super(Fire, self).__init__()
# Squeeze 层
self.conv1 = nn.Conv2d(inplanes, squeeze_planes, kernel_size=1, stride=1)
self.bn1 = nn.BatchNorm2d(squeeze_planes)
self.relu1 = nn.ReLU(inplace=True)
# Expand 层
self.conv2 = nn.Conv2d(squeeze_planes, expand_planes, kernel_size=1, stride=1)
self.bn2 = nn.BatchNorm2d(expand_planes)
self.conv3 = nn.Conv2d(squeeze_planes, expand_planes, kernel_size=3, stride=1, padding=1)
self.bn3 = nn.BatchNorm2d(expand_planes)
self.relu2 = nn.ReLU(inplace=True)
def forward(self, x):
x = self.conv1(x)
x = self.bn1(x)
x = self.relu1(x)
out1 = self.conv2(x)
out1 = self.bn2(out1)
out2 = self.conv3(x)
out2 = self.bn3(out2)
out = torch.cat([out1, out2], 1)
out = self.relu2(out)
return out
SqueezeNet
SqueezeNet 总结
- SqueezeNet 是一个精心设计的轻量化网络,其性能与 AlexNet 相近,而模型参数仅有AlexNet的 1 / 50 。使用常见的模型压缩技术,如 SVD、剪枝和量化等,可以进一步压缩该模型的大小。例如,使用 Deep Compresion 技术对其进行压缩时,在几乎不损失性能的前提下,模型大小可以压缩到 0.5MB
- 基于其轻量化的特性,SqueezeNet 可以广泛地应用到移动端,促进了物体检测技术在移动端的部署与应用
MobileNet
SqueezeNet虽在一定程度上减少了卷积计算量,但仍然使用传统 的卷积计算方式,而在其后的MobileNet利用了更为高效的深度可分离卷积的方式,进一步加速了卷积网络在移动端的应用。
深度可分离卷积 (Depthwise Separable Convolution)
SqueezeNet 虽在一定程度上减少了卷积计算量,但仍然使用传统的卷积计算方式,而在其后的 MobileNet 利用了更为高效的深度可分离卷积的方式,进一步加速了卷积网络在移动端的应用
标准卷积:
输出特征图上的每一个点都同时融合了空间信息和通道信息
深度可分离卷积:
MobileNet v1
深度可分离卷积模块:
值得注意的是,在此使用了 ReLU6 来替代原始的 ReLU 激活函数,将 ReLU 的最大输出限制在 6 以下。这主要是为了满足移动端部署的需求。移动端通常使用 Float16 或者 Int8 等较低精度的模型,如果不对激活函数的输出进行限制的话,激活值的分布范围会很大,而低精度的模型很难精确地覆盖如此大范围的输出,这样会带来精度损失
MobileNet v1 整体结构:
**α和β分别为调整宽度超参数(控制卷积核的个数)和分辨率超参数,对准确率和参数量有影响。**如图所示,针对α超参数而言,α=1时,准确率最高,但参数量也最大,随着α值变小,虽然准确率降低了一些,但参数量也随之较为明显地减小。同样,β超参数设置越大,准确率越高,同时模型计算量也越大。
但是,Mobile Net_v1存在一个问题:depthwise部分卷积核容易训练废掉,即卷积核参数大部分为0,不能起到有效作用。
MobileNet v1 总结
MobileNet v2
网络的亮点:
- Inverted Residuals (倒残差结构)
- Linear Bottlenecks
倒残差结构:
对于传统的残差块(如左图所示)而言,一般由以下三个部分组成:首先经过1*1的卷积将图片的维度降低,然后通过33的卷积,最后在通过11的卷积升维,激活函数采用Relu激活函数。
倒残差块使用1*1的卷积的效果刚好与之相反,先通过11的卷积将维度提升,然后通过33的DW卷积,最后再用1*1的卷积降维,激活函数选用Relu6激活函数(函数图像如下图)。
v1和v2:
线性激活函数的使用:
论文中在倒残差结构的最后一层1*1的卷积层使用了线性激活函数,而不是relu激活函数,因为Relu激活函数对于低维的feature map造成的损失较大。
如图,假设input是二维矩阵,channel为1,分别采用不同维度的矩阵T进行变换到更高的维度,使用relu得到输出值。再使用T的逆矩阵还原为2D的矩阵,当T的维度为2和3的时候,丢失了很多信息。但随维度加深,维度越少。不难看出:relu激活函数对低维信息造成损失较大,对高维造成损失很小。而倒残差两边细,中间粗,输出时是低维的特征向量,需要线性的激活函数替代relu激活函数避免信息损失。
网络结构:
注意:只有当stride=1且输入输出特征矩阵shape相同时才有shotcut链接
倒残差结构细节:首先经过1*1卷积升维到channel为tk,然后经过dw卷积, 由于stride=s,所以宽高除以s。最后再降维到k’维
import torch.nn as nn
import math
# 标准 3 x 3 卷积
def conv_bn(inp, oup, stride):
return nn.Sequential(
nn.Conv2d(inp, oup, 3, stride, 1, bias=False),
nn.BatchNorm2d(oup),
nn.ReLU6(inplace=True)
)
# 标准 1 x 1 卷积
def conv_1x1_bn(inp, oup):
return nn.Sequential(
nn.Conv2d(inp, oup, 1, 1, 0, bias=False),
nn.BatchNorm2d(oup),
nn.ReLU6(inplace=True)
)
class InvertedResidual(nn.Module):
def __init__(self, inp, oup, stride, expand_ratio):
super(InvertedResidual, self).__init__()
self.stride = stride
assert stride in [1, 2]
hidden_dim = round(inp * expand_ratio) # 中间扩展层的通道数
self.use_res_connect = self.stride == 1 and inp == oup
if expand_ratio == 1:
# 不进行升维
self.conv = nn.Sequential(
# dw (逐通道卷积)
nn.Conv2d(hidden_dim, hidden_dim, 3, stride, 1, groups=hidden_dim, bias=False),
nn.BatchNorm2d(hidden_dim),
nn.ReLU6(inplace=True),
# pw-linear (降维)
nn.Conv2d(hidden_dim, oup, 1, 1, 0, bias=False),
nn.BatchNorm2d(oup),
)
else:
self.conv = nn.Sequential(
# pw (升维)
nn.Conv2d(inp, hidden_dim, 1, 1, 0, bias=False),
nn.BatchNorm2d(hidden_dim),
nn.ReLU6(inplace=True),
# dw (逐通道卷积)
nn.Conv2d(hidden_dim, hidden_dim, 3, stride, 1, groups=hidden_dim, bias=False),
nn.BatchNorm2d(hidden_dim),
nn.ReLU6(inplace=True),
# pw-linear (降维)
nn.Conv2d(hidden_dim, oup, 1, 1, 0, bias=False),
nn.BatchNorm2d(oup),
)
def forward(self, x):
if self.use_res_connect:
return x + self.conv(x)
else:
return self.conv(x)
class MobileNetV2(nn.Module):
def __init__(self, n_class=1000, input_size=224, width_mult=1.):
super(MobileNetV2, self).__init__()
block = InvertedResidual
input_channel = 32
last_channel = 1280
interverted_residual_setting = [
# t, c, n, s
[1, 16, 1, 1],
[6, 24, 2, 2],
[6, 32, 3, 2],
[6, 64, 4, 2],
[6, 96, 3, 1],
[6, 160, 3, 2],
[6, 320, 1, 1],
]
# building first layer
assert input_size % 32 == 0
input_channel = int(input_channel * width_mult)
self.last_channel = int(last_channel * width_mult) if width_mult > 1.0 else last_channel
self.features = [conv_bn(3, input_channel, 2)]
# building inverted residual blocks
for t, c, n, s in interverted_residual_setting:
output_channel = int(c * width_mult)
for i in range(n):
if i == 0:
self.features.append(block(input_channel, output_channel, s, expand_ratio=t))
else:
self.features.append(block(input_channel, output_channel, 1, expand_ratio=t))
input_channel = output_channel
# building last several layers
self.features.append(conv_1x1_bn(input_channel, self.last_channel))
# make it nn.Sequential
self.features = nn.Sequential(*self.features)
# building classifier
self.classifier = nn.Sequential(
nn.Dropout(0.2),
nn.Linear(self.last_channel, n_class),
)
self._initialize_weights()
def forward(self, x):
x = self.features(x)
x = x.mean(3).mean(2)
x = self.classifier(x)
return x
def _initialize_weights(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
m.weight.data.normal_(0, math.sqrt(2. / n))
if m.bias is not None:
m.bias.data.zero_()
elif isinstance(m, nn.BatchNorm2d):
m.weight.data.fill_(1)
m.bias.data.zero_()
elif isinstance(m, nn.Linear):
n = m.weight.size(1)
m.weight.data.normal_(0, 0.01)
m.bias.data.zero_()
原理介绍:
MobileNet v3
改进:
- 更新的block(bneck),在倒残差结构上改动 -> 加入了SE模块,更新了激活函数
- 使用NAS搜索参数
- 重新设计耗时层结构
通过上图可以看出,相比于v2版本,v3版本更准确更高效。尤其是V3-small版本,在参数量比V2小的前提下,准确率提升了6.6%
bneck结构
bneck结构可以看做**在MobileNet_v2的基础上增加了SEblock(注意力模块),**也就是上图中链接第三个和第四个网络层的部分。
在这里介绍一下SE Block的机制:如上图,有黄蓝两个特征矩阵,先对其进行平均池化得到0.25和0.3。然后经过两个全连接层,第一个全连接层采用Relu激活函数,第二个全连接层采用Hard-sigmoid激活函数,然后将两个值变为0.5和0.6。将得到的两个值与原矩阵的每个元素相乘,得到新的特征矩阵(如黄色矩阵第一格0.2与0.5相乘,得到0.1;蓝色矩阵第一格0.2*0.6=0.12,以此类推)。
重新设置耗时层结构:
- 减少第一个卷积层的卷积核的个数(32 -> 16)
- 精简Last Stage
上图是原来的Last Stage,下图是精简后的Last Stage :在卷积层以后直接接上平均池化层,结构简化,但是最后的准确率基本不变。最后节省了7ms的执行时间。
重新设计激活函数:
原本的激活函数是sigmoid激活函数和swish激活函数。
将swish和sigmoid激活函数替换成hard-sigmoid和hard-swish激活函数
可以看到swish和h-swish的函数图像是基本重合的,更换激活函数既能简化计算,又不会降低准确率。
ShuffleNet
通道混洗
C++实现random_shuffle()
class Solution {
public:
Solution(vector<int>& nums) {
vec = nums;
}
vector<int> reset() {
return vec;
}
vector<int> shuffle() {
vector<int> vecShuffle = vec;
int n = vec.size();
for(int i = 0;i<n;i++){
int r = rand() % (n-i);
swap(vecShuffle[i],vecShuffle[i+r]);
}
return vecShuffle;
}
private:
vector<int> vec;
};
/**
* Your Solution object will be instantiated and called as such:
* Solution* obj = new Solution(nums);
* vector<int> param_1 = obj->reset();
* vector<int> param_2 = obj->shuffle();
*/
#include<iostream>
#include<vector>
using namespace std;
template <typename T>
vector<T> my_random_shuffle(vector<T> input) {
srand((unsigned int)time(NULL));
for (int i = 1; i < input.size(); i++) {
swap(input[i], input[rand() % i]);
}
return input;
}
int main() {
int n = 10;
vector<int> input(n, 0);
vector<int> res(n, 0);
for (int i = 0; i < n; i++) {
input[i] = i + 1;
}
//for (int i = 0; i < 1e5; i++) {
int j = 0;
for (auto x : my_random_shuffle(input)) {
res[j] = x;
j++;
}
//}
for (int i = 0; i < n; i++) {
cout << res[i] << " ";
}
}
计算机并不能产生真正的随机数,而是已经编写好的一些无规则排列的数字存储在电脑里,把这些数字划分为若干相等的N份,并为每份加上一个编号用srand()函数获取这个编号,然后rand()就按顺序获取这些数字,当srand()的参数值固定的时候,rand()获得的数也是固定的,所以一般srand的参数用time(NULL),因为系统的时间一直在变,所以rand()获得的数,也就一直在变,相当于是随机数了。只要用户或第三方不设置随机种子,那么在默认情况下随机种子来自系统时钟。
如果想在一个程序中生成随机数序列,需要至多在生成随机数之前设置一次随机种子。 即:只需在主程序开始处调用srand((unsigned)time(NULL));后面直接用rand就可以了。不要在for等循环放置srand((unsigned)time(NULL))。
pytorch
pytorch中model.eval()会对哪些函数有影响?
model的eval方法主要是针对某些在train和predict两个阶段会有不同参数的层。比如Dropout层和BN层
Dropout在train时随机选择神经元而predict要使用全部神经元并且要乘一个补偿系数
BN在train时每个batch做了不同的归一化因此也对应了不同的参数,相应predict时实际用的参数是每个batch下参数的移动平均。
torch为了方便大家,设计这个eval方法就是让我们可以不用手动去针对这些层做predict阶段的处理(也可以叫evaluation阶段,所以这个方法名才是eval)
这也就是说,如果模型中用了dropout或bn,那么predict时必须使用eval 否则结果是没有参考价值的,不存在选择的余地。
语义分割
手写mIoU的计算及其python实现
原理介绍:
参考
一.IOU理解
在语义分割的问题中,交并比就是该类的真实标签和预测值的交和并的比值
单类的交并比可以理解为下图:
TP: 预测正确,真正例,模型预测为正例,实际是正例
FP: 预测错误,假正例,模型预测为正例,实际是反例
FN: 预测错误,假反例,模型预测为反例,实际是正例
TN: 预测正确,真反例,模型预测为反例,实际是反例
IoU = TP / (TP + FN + FP)
二.mIoU
MIOU就是该数据集中的每一个类的交并比的平均,计算公式如下:
Pij表示将i类别预测为j类别。
三.评价指标之间的关系
- IoU、Precision、Recall是针对所有图片内的某一类来说的;
- mIoU、mPA、Accuracy是针对所有类别来计算的;
- F1-Score 也是用来衡量分割精度的一个指标,同时考虑了准确率和召回率。
先回顾下一些基础知识:
python语义分割iou计算
import numpy as np
size = (20, 30)
classes = 2
pred = np.random.randint(0, classes, size)
target = np.random.randint(0, classes, size)
pred.flatten()
# 计算iou
def IOU(pred, target, nclass):
ious = [] # 记录每一类的iou
for i in range(classes):
pred_ins = pred == i # pred_ins为true/false矩阵
target_ins = target == i
inser = pred_ins[target_ins].sum()#使用True和False来选择输出的矩阵
union = pred_ins.sum() + target_ins.sum() - inser
iou = inser / union
ious.append(iou)
return ious
if __name__ == "__main__":
ious = IOU(pred, target, classes)
print(ious)