图像相似度判断方法

前言

我原来希望将图像相似度方法做一个类似综述的总结,但是找了好久却发现好像并没有什么比较好参考资料,所以这里就仅仅介绍一些常用的方法。不过尽管如此,我还是希望在图像相似度判断方法上,谈谈自己的理解与认识。

图像相似度判断是个很广泛的说法,当比较的目标不一样,其使用的方法也不同。有时候我们需要比较的是整幅图像的相似度,而有些时候我们只是需要识别图像中相同或相似的物体,还有可能我们比较的是图像风格的相似性。也许正是由于这样,导致我在这方面没有找到比较系统的总结文章。物体的识别基本上是属于一个新的大类,风格的相似判断不甚了解,所以对于这些我们这里就不讨论了。在这里我们将目标缩小到两幅图像整体的相似性判断以及局部相同位置相似度大小比较上来。

简单统计量的比较

每个图像本身是一个数值分布,当我们比较它们时,也可以当作以一个数值分布来比较。这样最容易想到就是均值和方差了,当然整体图像计算均值和方差是没有位置信息的,当我希望有不同区域相似度的时候可以,选择一个合适矩形框,计算其局部的统计量再来比较。但是不管怎样,单一的统计量的比较都是比较局限的。

直方图比较

除了简单的均值和方差,使用直方图也是比较相似度的一个方法,比较两个图像直方图的差异,一般是使用巴氏距离或者归一化相关系数,度量巴氏距离的计算公式如下:

归一化相关系数的计算如下:

感知哈希方法

在以图搜图的应用中,感知哈希方法是一种非常常用的相似度判断方法。它的主要思想是使用图像生成能够代表图像整体特征的hash码,通过对比hash码来判断相似度。其中一种增强版的方法实现步骤如下:

  1. 缩小尺寸:典型值32x32
  2. 简化色彩:将彩色图灰度化
  3. 计算DCT:对图像作为离散余弦变换,得到32x32的DCT系数矩阵
  4. 缩小DCT:保留左上角的8x8的矩阵,这部分呈现了图片中的最低频率
  5. 计算平均值:计算8x8的DCT的均值
  6. 计算hash值:对8x8的DCT比较,大于均值设置1,小于均值设为0,由此得到一串64位的整数

上面的感知哈希方法是加入了DCT的版本,比没有普通的版本更加的稳定,图片的高宽、亮度甚至颜色的些许改变,都不会改变哈希值,更重要的是它的计算速度是非常快的。

在比较时可以用汉明距离来计算相似性。两个等长字符串s1与s2之间的汉明距离定义为将其中一个变为另外一个所需要作的最小替换次数,简单的说就是检查对应位置上的的字符,看有多少个位置值是不同的。计算汉明距离比较简单的方法就是使用异或运算,统计非零的个数。

SSIM

SSIM的全称为structural similarity index,即为结构相似性,是一种衡量两幅图像相似度的指标。它分别从亮度、对比度、结构三方面度量图像相似性,符合人类视觉系统(Human Visual System,HVS)的评价结果。在SSIM中亮度用均值表征,对比度用经过均值归一化之后的方差表征,结构用相关系数表示,三者的共同作用得到SSIM结果。

公式如下:

各项计算为(C1,C2和C3为常数,是为了避免分母为0而维持稳定):

选择C3=C2/2时,可以将公式整理为:

参考

  1. 相似性度量(Similarity Measurement)与“距离”(Distance)
  2. 基于感知哈希算法的视觉目标跟踪
  3. 图像质量评估算法SSIM(结构相似性)
  4. 图像质量评估指标 SSIM / PSNR / MSE
Compartir

图像算法基本算子总结

前言

承接上一篇求连通域的文章,本文继续复习整理关于图像处理和图像算法的一些基本知识点。

高斯模糊公式

高斯模糊,算是我们接触图像处理后最最基本的功能了,但是到现在我也没法写出高斯模糊的数学公式和标准高斯分布下高斯核模板取值情况,所以这里特地将其提取出来复习。

1. 一维高斯函数

在开始前,我先复习以下一维连续情况下的高斯函数的一般形式:

$$ f(x) = a e^{-\frac{(x-b)^2}{2c^2}} $$

而高斯分布的概率密度,则增加了函数在整个定义域内积分为1的约束,所以就成了我们习惯看到的形式(推导就不写了),mu为均值,sigma为标准差:

$$ f(x) = \frac{1}{\sqrt{2\pi}\sigma} e^{-\frac{(x-\mu)^2}{2\sigma^2}} $$

2. 二维高斯分布函数

由于图像是二维的数据,所以应用于图像的高斯分布函数也是二维的。一般的二维的高斯函数可以看成两个一维的高斯函数的乘积,形式如下:

$$ f(x,y) = a e^{-\frac{(x-\mu_x)^2}{2\sigma^2_x}-\frac{(y-\mu_y)^2}{2\sigma^2_y}} $$

通常在图像领域x、y属于统一分布,且x、y为坐标,其均值为其中心坐标,一般设置为0,因此就得到了图像中的二维高斯分布的连续的密度函数为:

$$ f(x,y) = \frac{1}{2\pi \sigma^2} e^{-\frac{x^2 + y^2}{2\sigma^2}} $$

3. 高斯核

首先数字图像是离散的,所以需要将上面的连续函数改为离散的形式,其次图像的尺寸是由大小,所以图像中的离散的高斯分布函数通常定义域只是一个小的区域。在实际使用中我们一般取一个n x n的矩形小区域,一般将n设为奇数保证它的中心坐标是一个整数,且坐标正负对称。

因此,我们可以将n x n矩形范围内的每一个坐标上(像素点)的高斯分布函数的值直接求出来,这就是高斯核。使用高斯核,可以减少重复的计算。在高斯模糊的实现中,就是使用高斯核作为每个坐标位置的权重来做平均。

假定标准差为1.5的一个3x3的高斯核模板如下:

sobel算子

sobel算子可用于计算图像的梯度在前面的梯度的文章就介绍过,但是并没有写出典型的模板值。为什么使用sobel算子来计算图像的梯度,

另外这里需要提示的是,由于sobel经常用于边沿检测,所以有时候我说检测边沿方向,而不是说梯度方向,这两者说法是不一样的,从广义上来将可以认为这两说法刚好是反的,所以刚刚接触到的时候后,常常会不注意看错或混淆这两个说法。计算水平方向(x)的梯度,作用等同检测垂直方向的边沿;而计算垂直方向上(y)的梯度,作用等同于检测垂直方向上的边沿。如下为典型的3x3的两个梯度方向的sobel模板:

拉普拉斯算子

1. 拉普拉斯算子的数学公式

在图像处理中,常将laplace(拉普拉斯)算子和sobel算子一起说明,主要是因为它们都是图像的微分算子,都可以用于边沿检测,两者的本质区别在于,sobel是一阶微分,而laplace则是二阶微分,对于连续二维函数其拉普拉斯算子定义为:

$$ Laplace(f) = \frac{\partial^2{f}}{\partial x^2} +
\frac{\partial^2{f}}{\partial y^2} $$

拉普拉斯算子定义的是梯度的散度,推导后是各个维度的二阶导数的相加,它和sobel算子不一样,它在不同方向上的值是一样,因此它具有旋转不变性。

对于离散的图像,使用基本的前后差分求梯度,有:

$$ \frac{\partial^2{f}}{\partial x^2} = [f(x+1,y) - f(x,y)] - [f(x,y) -
f(x-1,y)] = f(x+1,y)+f(x-1,y)-2f(x,y)$$

$$ \frac{\partial^2{f}}{\partial y^2} = [f(x,y+1) - f(x,y)] - [f(x,y) -
f(x,y-1)] = f(x,y+1)+f(x,y-1)-2f(x,y)$$

计算公式将变成:

$$ \nabla f(x, y) = f(x+1,y) + f(x-1,y) + f(x,y+1)+ f(x,y-1) -4f(x,y) $$

2. 拉普拉斯算子模板

一般我使用如下两种3x3的权重数据作为拉普拉斯算子的模板(前一种就是按照上面的离散定义,第二种是扩展后的):

拉普拉斯算子对孤立点或端点更为敏感,因此特别适用于以突出图像中的孤立点、孤立线或线端点为目的的场合,对于一阶梯度是看幅值,而拉普拉斯算子则看零点。

另外使用拉普拉斯算子可以对图像进行锐化处理,使灰度反差增强,从而使模糊图像变得更加清晰。基本的锐化操作为:对图像进行拉普拉斯算子处理,将生成的图像与原始图像叠加,即可得到更加锐化的图像(这种简单的办法缺点是对图像中的某些边缘产生双重响应)。

高斯拉普拉斯算子

1. LoG的数学公式

高斯拉普拉斯算子(Laplacian-of-Gaussian)简称LoG,是高斯和拉普拉斯的双结合,实际上就是由于拉普拉斯算子抗噪声能力太弱,所以就在前面加入高斯滤波后再做拉普拉斯计算。

公式上就是对二维高斯分布公式进行拉普拉斯算子处理,两次求导后结果如下:

$$ LoG = \frac{x^2 + y^2 - 2\sigma^2}{\sigma^4} e^{-\frac{x^2+y^2}{2\sigma^2}}$$

一个比较小的典型LoG模板如下:

2. 高斯差分算子

由于计算LoG比较复杂,所以在实际中为了减少计算量,通常使用高斯差分算子(DoG,Difference of Gaussian)来近似,DoG的是两个同均值不同方差的高斯分布相减的结果,在结果上与LoG非常的相似。

使用g表示一个高斯分布函数,则DoG公式为:

$$ DoG = \frac{g(x, y, k\sigma)-g(x,y,\sigma)}{k\sigma-\sigma}$$

DoG与LoG的对比图:

参考

  1. 高斯函数(Gaussian function)的详细分析
  2. 差分近似图像导数算子之Laplace算子
  3. 图像边缘检测——二阶微分算子(上)Laplace算子、LOG算子、DOG算子(Matlab实现)
  4. LOG高斯-拉普拉斯算子
Compartir

求连通域算法

前言

近些年来,图像领域的发展不管是学术界还是商业界基本上都被人工智能的方法统治了,我个人也是把工作和学习的重心也转移到深度学习上去了。 忽然回过头来看一些传统的图像处理方法,反而感觉有点生疏了。不过这也是很正常的事情,因为毕竟传统的图像处理算法发展了这么多年也算是一门大学科领域了,因此我希望通过整理那些我还不够熟悉但是又足够基础的图像处理算法,来加深我对图像处理算法的理解,本文算是这一系列的开始吧。

求连通域作用

连通域获取算是图像处理中,一步非常基础又十分有用的操作了,它能够使用在如:OCR识别中字符分割提取(车牌识别、文本识别、字幕识别等)、视觉跟踪中的运动前景目标分割与提取(行人入侵检测、遗留物体检测、基于视觉的车辆检测与跟踪等)、医学图像处理(感兴趣目标区域提取)等等各个方面。但是很奇怪我不知道为什么,我在实际的工作中就完全没有使用过连通域的方法,也许是由于在我的项目中并没有怎么涉及到识别与提取感兴趣区域的任务吧。

由于求连通区域算是一个比较基本简单的算法,所以本文会先使用openCV提供的对应接口函数检查效果,然后自己设计算法实现。

使用OpenCV的findContours函数

在OpenCV中提供了findContours来求图像中所有的连通区域,使用findContours能够得到不同区域包含点的集合和拓扑信息。由于求连通域是基于二值图,所以输入的图像要是一个二值图。

在找到区域后,可以使用drawContours来画区域的外轮廓,使用fillConvexPoly来标记连通区域。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import cv2
import numpy as np
import matplotlib.pyplot as plt

def pltshow(im):
plt.figure(figsize=(15,6))
if im.ndim == 3:
plt.imshow(cv2.cvtColor(im, cv2.COLOR_BGR2RGB))
else:
plt.imshow(im, 'gray')
plt.show()

img = cv2.imread('Image/bin.jpg', cv2.IMREAD_GRAYSCALE)
_, img = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY )
pltshow(img)
img.shape

找到连通曲,并对不同的连通区使用不同的颜色进行标记显示

1
2
3
4
5
6
image, contours, hierarchy = cv2.findContours(img, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
col_img = np.repeat(image, 3).reshape(image.shape[0], image.shape[1], 3)
for i in range(len(contours)):
col = i*35
cv2.fillConvexPoly(col_img, contours[i], (col,255-col,255-col))
pltshow(col_img)

运行结果如下:

下面代码是对连通区的外轮廓进行提取、标记

1
2
3
col_img = np.repeat(image, 3).reshape(image.shape[0], image.shape[1], 3)
cv2.drawContours(col_img, contours, -1, (255, 0, 255), 2)
pltshow(col_img)

运行结果如下:

连通域查找简单实现

连通域查找有两种方法:

  1. Two-Pass(两遍扫描法),就扫描图像两次,第一次扫描将为1的像素点设置一个label,label的取值由其相邻像素是否有label标记决定,如果有,使用相邻像素最小的label作为这个像素点的标记,如果没有使用一个新的标记值。第二遍的扫描将所有相邻的标记设置为相同的label(通常是这个区域label的最小值)。

  2. seed filling(种子填充法),选取前景中的一个像素点作为种子,根据连通的条件逐步的向外扩张,直到将区域内的像素点去不照完,同时以相同的方法搜寻其他的区域,直到图像上所有的区域都已经找完。

以下用python代码实现第二种方案,先不考虑连通域相互包含的层次关系:

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
47
48
49
50
def isConnected4Neb(img, r, c):
ret = []
i = r - 1
j = c
if i >= 0 and img[i, j] > 0: ret.append((i, j))
i = r + 1
j = c
if i < img.shape[0] and img[i, j] > 0: ret.append((i, j))
i = r
j = c - 1
if j > 0 and img[i, j] > 0: ret.append((i, j))
i = r
j = c + 1
if i < img.shape[1] and img[i, j] > 0: ret.append((i, j))
return ret


def findOneArea(img, img_pro, r, c):
area = []
use_stack = [(r, c)]
while len(use_stack) != 0:
i, j = use_stack.pop()
ret = isConnected4Neb(img_pro , i, j)
for item in ret:
img_pro[i, j] = 0
area.append(item)
use_stack.append(item)
return area


def findArea(img):
img_pro = img.copy()
areas = []
for i in range(img.shape[0]):
for j in range(img.shape[1]):
if img_pro[i,j] > 0:
area = findOneArea(img, img_pro, i, j)
if len(area) > 0:
areas.append(area)
return areas

def drowArea(im, areas, color):
for area in areas:
for i ,j in area:
im[i, j] = color

areas = findArea(img)
col_img = np.repeat(image, 3).reshape(image.shape[0], image.shape[1], 3)
drowArea(col_img, areas, (255,255, 0))
pltshow(col_img)

以上代码运行结果如下:

参考

  1. OpenCV-二值图像连通域分析
Compartir

牛顿法的推导

前言

前面我总结过梯度下降以及基于梯度下降的几种主要的优化方法,但是那是仅仅局限于深度学习领域的,而现在我希望将眼光扩大到整个数值优化领域。作为初学者,古老的牛顿法是我们很好的入门起点。

牛顿法的简介

牛顿法(Newton’s method)是由大名鼎鼎的牛顿于1671提出1736公开的的《流数法》一书中提出的方法,他是一种在实数和复数域近似求解方程的方法。这句话多数百科上的简介,基本上看不懂,结合应用场景,我觉得牛顿法可以描叙成:为了求函数的最值,使用函数的二阶泰勒展开,利用函数的一阶导数等于0推导的迭代方法。也许描叙得并不准确,但是我觉得这样的表述至少前面要具体和清晰不少。

假定有函数f(x),我们需要求其极值点,则牛顿法的迭代公式如下:

切线法解释

牛顿法常常又被称为切线法,这是因为求原始函数f(x)得极值点,等同于求其导数等于0的解,那么画出其导数曲线,从初始点画切线,将切线于x轴的交点值作为下一次x值,它将不断的接近曲线于x轴的交点,也就是f(x)的导数为0的地方。当我们从切线得角度取理解牛顿法就就现得非常非常的简单,其迭代动图如下所示:

Newton's method

当然除了直观的理解,还需要更严谨的数学推导,使用切线法得到上面牛顿法的迭代式也是很简单的。取初值点(x0,f(x0)),假定f(x)的导数为g(x),则上图中的曲线就不是f(x)了,而是g(x),以x0做g(x)的切线,则切线的直线方程为:

由于切线过(x0, g(x0))点,因此:

切线于x轴的交点x1作为下一个迭代点,根据上面的直线方程有:

写成通项的形式,并重新带入f(x)就成了:

二阶泰勒展开的推导

使用二阶泰勒展开在解释上没有上面切线法直观,但是从数学的看是更加严谨的。设xk为当前最小值点的估计值,首先对f(x)进行在xk附近的泰勒展开,对其做近似,保留常数项、一阶项和二阶项,舍弃后面的高阶项:

将近似项用h(x)来表示,则:

我们利用求h(x)的极值来近似得到f(x)的极值,h(x)的极值点,则其一阶导数为0,我对h(x)求导(注意是对x求导,x_k是常数),得到:

写成递推形式为:

优缺点

从本质上去看,牛顿法是二阶收敛,梯度下降是一阶收敛,所以牛顿法就更快,但是正是由于它要求二阶导数,在多维的情况下,就是需要求海森(Hessian)矩阵的逆矩阵,计算比较复杂。

参考

  1. 常见的几种优化算法
  2. 无约束优化算法——牛顿法与拟牛顿法(DFP,BFGS,LBFGS)
Compartir

RCNN网络的认识

前言——从图像分类到物体检测

在深度学习领域卷积神经网络可以说是占了其半壁江山,不过卷积神经网络诞生之初只适用于图像分类问题,而然面对实际工程的各种需求,单纯的图像分类显然是远远不够,因此如何将卷积神经网络强大的体征提取能力应用到更复杂的问题上去,是当时乃至现在热门的研究主题。在这其中,R-CNN的出现让物体检测也成为了卷积神经网络的拿手好戏。

图像分类到物体检测与何区别?图像分类只是基本整个图像判别其种类,而物体检测则是读取图像的内容,不仅要识别其中有无特定种类的物体,还要得到其在图像中所处的大概位置。相对于图像分类的笼统、粗略,物体检测就要显得细致、具体得多了。

正如Alexnet的出现让人们认识到了卷积神经网络的巨大威力一样,R-CNN的出现也让卷积神经网络在物体检测上的精度大幅提高了一个档次。虽然后续的一系列物体检测网络,不断刷新着检测精度与运行的速度的榜单,但是R-CNN毕竟是一个开创者,在其后诞生了一系列R-CNN的改进版本,这足以说明其模型的经典性。由于R-CNN的论文比较经典,所以网上已经有不少论文的翻译,因此我就不会详细的分析论文了。在这里我希望通过本文,能够比较针对性的记录一下自己对R-CNN的理解与认识。

R-CNN的本质

从图像分类到物体识别是一个很有挑战的过程,R-CNN的思路是:从原始图像中取出不同区域作为独立的小图像,导入到CNN网络中做图像分类,从而将物体识别问题转化为分类问题。在这其中,R-CNN使用region proposal的方法代替传统的滑动窗口来生成小区域图像,将像生成的region图像resize到分类网络要求的尺寸,使用CNN来提取特征,最后借助线性SVM来分类。由于R-CNN结合了Region proopsal和CNN,所以就被取名为R-CNN: Regions with CNN features。

除此之外为了让识别的区域更加准确,R-CNN还加入了一个回归模型,用来提高Bounding-
box(区域矩形框)的精度。所以从整体上来看,R-CNN可以看成由如下三部分组成:

  • Region proposal
  • CNN+SVM分类
  • Bouding Box线性回归

region proposal模块

在R-CNN中,组成其整体的三部分相对独立,region proposal更偏向传统方法,在R-CNN的中它更像是一步预处理步骤,它将生产的区域(Bouding Box)。原始论文中对region proposal的描叙相对较少,只是简单的提到了R-CNN中在众region proposal方法选取了selective search方法来使用。那么selective search是怎么工作的呢?这就只能查找关于selective search的相关论文了。在我个人看来,如果仅仅是关注深度学习这一块的话,确实是不必深究region proposal,因为这部分属于传统物体识别方法的大类,深入进去又是一个大坑,而且region proposal执行时间花销也不小,在后续的改进中版本的网络中,这部分都是主要被优化的对象,不过对于region proposal的大概原理还是有必要简单的了解以一下的。

selective search的目的就是生成在图像中可能有物体存在的区域位置的矩形框,由于它考虑了物体存在的可能性,从这个目标上来看,它比滑窗法(Sliding Window)生成的待选区域质量就要高得多。一般selective search会采样图像分割的技术,先将图像分成许多小区域,然后根据相似性将相似将相似的区域融合在一起,将子区域的矩形外界框作为bounding box输出。

在R-CNN测试阶段中region proposal模块会生成2000个区域(Bouding Box)输入到CNN网络中去,而在训练CNN时可以不用region proposal。

CNN+SVM分类器

R-CNN使用的CNN是AlexNet模型,输入227x227尺寸的RGB图像,最后的全连接层输出4096个特征的向量。而全连接层的存在也要求网络只能接受固定尺寸的图像,因此region proposal生成的区块需要全部缩放到227x227尺寸来。全连接层输出4096个特征的向量,导入到SVM分类器中,从而组合成一个完整的分类网络。

虽然在CNN与SVM联合组成一个分类器,但是在训练时却是分开训练的,先将CNN训练好,然后将CNN全连接层后面的分类部分换成SVM,然后再单独训练SVM。

1. CNN训练

对于R-CNN的CNN模块我们首先要知道的是,它是一个监督学习的网络,然而能够用于物体检测训练的数据却是比较少的,因此R-CNN在训练的时候采用了Supervised pre-training(预训练) + Domain-specific fine-tuning(特定数据集上微调)的方法来解决数据不足的问题。

在pre-training阶段,使用没有bounding box labels的ILSVRC2的数据集(1000个类别),而在fine-tuning的时候使用有bounding box labels的Pascal VOC数据集(21个类别),取Pascal VOC数据集图像中选取各类型的区域作为训练样本,正样本为与标注的bounding box重合区域(IoU)大于0.5的作为该类型的正样本,其余的为负样本。另外减小学习率,保证pre-training出来的参数不要变化太大。

2. SVM分类

这里不使用CNN直接分类,而还单独训练一个SVM分类器的原因:由于fine-tuning使用的数据量非常小,将IoU的标准卡严,很容易造成CNN过拟合,而降低IoU的标准,又导致CNN分类不准,所以使用一个适用于小样本分类的SVM分类器能够避免以上的问题。

R-CNN对每一种类型使用一个线性SVM分类器,因此又多少种类型,就又多少个SVM分类器,每一个SVM都会对的图像进行打分。

Bouding Box相关的处理

1. 挑选最好的Bouding Box

在测试时,region proposal可能会生成许多包含物体但是又过大或过小的Bouding Box,这些Bouding Box因为确实整体包含或者包含了大部分物体,所以会被分类器判别为真,但是我们肯定希望从中挑选出一个最好的Bouding Box,来作为我们检测到的唯一的结果输出。这里R-CNN选取SVM结果分数最高的最为输出结果。

2. 怎么处理多目标检测

仅仅选分数最高还无法处理多目标检测问题,如果一个图像上有可能存在多个检测的物体,那么仅仅取IoU最大的那个,一张图像将只能检测出一个物体。为解决这个问题,R-CNN使用的方法是,参考SVM输出的分数同时根据IoU的数值进行局部非极大值抑制,在一个图像中选出一个最好的Bouding Box,对于其周边的Bouding Box(IoU高的)去除,而对于其他IoU小的Bouding Box即使得分小也会保留,然后对剩下的Bouding Box重复上面的操作,直到选出所有目标。这样就即筛选出了局部最好的Bouding Box,同时不影响其他区域的检测。

3. Bouding Box线性回归

由region proposal生成的Bouding Box一般都不是完美包裹物体的矩形框,这是导致输出检测物体定位不精准的主要原因。因此在检测到物体之后,再对其输出的bounding box使用线性回归模型进行修正,其实质就是根据当前CNN输出的特征与当前的定位,来预测最好的bounding box(重心点与长宽)。当然回归模型也是需要单独训练的。

参考

  1. Rich feature hierarchies for accurate object detection and semantic segmentation(原始论文)
  2. R-CNN论文详解(论文翻译)
  3. 目标检测之Selective Search原理简述
  4. 图解CNN论文:尝试用最少的数学读懂深度学习论文
Compartir

深度学习主要优化算法总结

前言

深度学习的优化算法基本上都是基于梯度下降的,不过在复杂的网络训练中,仅仅使用梯度下降是远远不够的。一方面是由于深度学习的数据量巨大,不可能将数据全部带入求梯度下降;另一方面是深度的学习的待优化的参数太巨大,这么多参数组成了超高的维度必然在各个维度上的尺度非常的不一致,造成优化困难。所以在实际中,一般都是使用中的方法都是改进过的优化算法,本文主要介绍一下集中优化算法:

  1. SGD
  2. Momentum
  3. RMSprop
  4. Adam

各种优化算法的关系

其实除了以上几种外,还有很多其他的相关优化算法,这对于深度学习初学者来说,简直就是噩梦,感觉还没开始进入正题就陷入深渊。不过虽然优化算法本身是一个水很深的一大研究方向,但是针对深度学习的优化算法其实还是相对狭窄很多。正如上面所提到的那样,深度学习的优化算法本质上都是基于梯度下降的,所以一旦我们找倒了他们之间彼此的联系,再来理解就要简单许多。

首先我们先来看看基本的梯度下降的公式:

使用当前点的梯度与学习率来更新当前的参数大小,其中梯度主要指引优化的方向,学习率控制更新的步长。

再此基础上,SGD是为了解决数据量大,无法使用全部数据用来更新梯度;Momentum则是为了解决SGD由于数据批次的差异导致的梯度震荡优化速度慢的问题,RMSprop则是从学习率的角度出发提出自适应学习率的方法,而Adam则完全就是Momentum和RMSprop的方法的结合作用。RMSprop和Adam是根据实际情况自适应学习率,所以它们实际的学习率是动态调整的。

SGD

SGD是Stochastic Gradient Descent的缩写,SGD在基本的梯度下降方法做的改进就是,仅仅使用一个样本对梯度进行更新。这样做的好处就是每一个数据都是会独立作用到梯度更新上,比起使用全部数据才更新一次梯度的方法数据利用效率要高很多。但是由于数据的随机性、以及噪声的干扰,使得SGD每次更新的梯度对数据整体而言可能并不是下降的方向,虽然训练速度较快,但是震荡厉害、准确度下降。

为了减小SGD的震荡,使收敛更稳定,我可以使用去一个折中的方案:每次使用一小批数据(mini batch)来更新梯度,这样既提供了数据的利用率,又可以增加数据的稳定性,这种被称为Mini-Batch Gradient Descent(MBGD)。

不过在一些深度学习框架的实际使用中,这里有个问题我发现一般都只有SGD而没有MBGD,但是在训练时又分了mini batch。这就很奇怪了,明明只有MBGD才会使用到mini batch。我个人认为这是由于,神经网络计算数据的计算都是向量、矩阵化的,我们一般称为tensor,所以即使有多个数据,一般都是组成一个tensor作为一个数据输入到网络中的,而且网络也只会经过一次计算,所以形式上与SGD时一致的,但是效果却是MBGD一样。

指数滑动平均EMA

在说明后面的优化算法前,需要先了解指数滑动平均EMA,因为不管是Monentum还是RMSprop都是利用EMA对优化过程进行平滑的。EMA全称是Exponential Moving Average,是一种使用非常广泛的平滑方法,如股票、气温变化曲线,它能够用历史数据对当前数据进行平滑,以保持曲线整体的稳定性。

V表示平滑结果,theta 表示当前数据值,beta表示平滑系数,则EMA的计算方法如下:

如果使用上面的迭代关系,从V0开始算,则Vt的可以得到公式如下:

可以看到Vt使用到了所有历史数据,越靠近的数据影响越大,且越往后其影响力呈指数下降。

下图是使用EMA平滑气温变化曲线效果(蓝色点为原始数据,红、黄、绿色的曲线为使用了不同的平滑系数的EMA的效果):

EMA

EMA的偏差修正

在使用EMA平滑数据时候,需要注意在最开始的时候,由于历史数据很少或者没有,而我们设置V0初值却占了绝对主要的影响。一般V0会设置为0,这样会导致平滑曲线与实际数据相差甚远,不过随着历史数据越来越多,这个偏差就会变得越来越小。为了修正最开始的时候出现的偏差,我们一般可以使用如下公式对结果进行修正:

当V0 = 0时,当很小的时候将原始数据的影响放大,后面随着t的变大,修正的影响变小。

Momentum

前面说过SGD由于即使加入了mini batch后,仍然由于数据的随机性,所以当前所mini batch的数据与整个数据的分布可能不一致,导致SGD在收敛时震荡厉害。Monmentum为了减小震荡,使用了EMA来平滑梯度,从而达到当前参数优化不仅取决于当前步的梯度,还取决于历史梯度值。

Momentum方法将梯度下降比拟为一个滚动下山的小球,小球每一时刻的运动状态不仅取决与其当前所受合力的作用,还取决于其原来的运动状态。Momentun借用了物理学中动量的概念,用动量代替梯度作为参数更新的动力,动量的计算实际上就使用EMA的方法求得的:

可以理解为J的导数为当前合力作用与当前运动状态Vt共同决定下一刻的运动状态,一般超参数gramma设置为0.9,参数更新公式将变为:

Momentum方法通过使用EMA平滑了原始梯度数据,让训练时收敛更稳定。

RMSprop

相比Momentum使用历史数据来保持稳定,RMSprop则是直接考虑不同维度上的梯度变化的大小,在相同学习率下,某个维度梯度变化大,则在参数更新的时候步长方向都会较大,RMSprop算法不希望这些在不同维度上参数更新差异太大,因此它引入一个衰减因子来平衡不同维度上的差异。直观的理解,SGD的出现震荡,很大的原因就是由于其在某个维度上更新速度慢,而在另外的维度上更新速度快导致来回摆动。

另外使用梯度的平方作为抑制因子,平且通过EMA方法进行指数加权来平滑抑制因子,更新方程如下:

Adam

Adam是Adaptive Moment Estimation的缩写,前面提过Adam是Momentum与RMSprop的结合,它综合了上面两个算法的特点,既使用动量的方法平滑了更新的路径,又使用了衰减因子平衡了不同维度上的更新尺度。不过需要注意的一点是,Adam在用EMA做平滑的时候使用了偏差矫正,计算公式如下:

最后的参数更新规则为:

参考

  1. 一个框架看懂优化算法
  2. 深度学习——优化算法
  3. 深度学习——优化器算法Optimizer详解
Compartir

Tensorflow-Keras简单介绍

前言

本文是为初学者对Tensorflow和Keras的一个简单的介绍,主要基于Youtube上Tensorflow向导教程,国内B站搬运地址:https://www.bilibili.com/video/av23035841
github演示文档地址:https://github.com/Hvass-Labs/TensorFlow-Tutorials

在自己没有搭建好tensorflow环境的时候,如果可以翻墙,可以使用google的colab在线执行tensorflow的程序,还可以提供GPU服务。

另外一些知识来源其他参考资料:

  1. Deeplearning——动态图 vs. 静态图

感想

对于像我一样一开始接触Tensorflow的人来说,Tensorflow的一些自身定义的一些概念会然人很困惑,而且其运行方式也与通常与我们编写计算的程序不是很相同,会给人一种很不直观的感觉。为了解决这样的困惑,是需要稍微了解一些Tensorflow的框架设计的一些基本的思想的,这些特点对于理解Tensorflow中的那些概念非常有帮助,这些会在下面的记录说明。

Tenserflow理解

1. Tensorflow封装的层次

通常我们把Tenserflow都归类到深度学习神经网络库,但是Tensorflow更本质的是提供低级的数值计算的库,所以才会有Keras这样的专门针对深度神经网络更高级的封装,其后端可以基于Tensorflow。想使用Tensorflow底层的API就必须理解其定义的一大堆概念,但是实际使用时使用Keras这样高级的API会更加方便。

2. Tensorflow的计算图

Tensorflow把整个计算过程称为computational graph计算图,在整个计算过程都构建完成后再做计算。所以它不仅比原生python计算更块,也比numpy更快,因为numpy只能知道每一次操作的计算。Tensorflow的计算图属于静态的图,而像PyTorch的动态图则更加灵活(MxNet两者都支持)。不过最新版本的Tensorflow也可以支持动态图了。

动态计算意味着程序将按照我们编写命令的顺序进行执行。这种机制将使得调试更加容易,并且也使得我们将大脑中的想法转化为实际代码变得更加容易。而静态计算则意味着程序在编译执行时将先生成神经网络的结构,然后再执行相应操作。从理论上讲,静态计算这样的机制允许编译器进行更大程度的优化,但是这也意味着你所期望的程序与编译器实际执行之间存在着更多的代沟。这也意味着,代码中的错误将更加难以发现(比如,如果计算图的结构出现问题,你可能只有在代码执行到相应操作的时候才能发现它)。

Tenserflow重要数据类型

注意下面代码,是总jupyter notebook中转过来的,在jupyter可以直接运行,但是在这里除了重要的结果,我不会将运行结果复制过来,需要时可以直接到python环境运行,tensor版本为1.11.0。

1. Placeholder

placeholder就是作为给计算图数据输入的特殊的变量,其定义和numpy中的数据类似,比较简单:

1
2
3
4
5
6
import tensorflow as tf
import numpy as np
print(tf.__version__)

x = tf.placeholder(tf.float32)
y = tf.placeholder(tf.float32, [None, None])
2. Variable

一般的变量类型,但是相比placeholder必须初始化,指定维度和初值。Variable可以使用普通的运算符来计算,不过更丰富的运算需要使用tensorflow定义的运算函数。需要注意的是,Variable只是Tensorflow计算图中的一个结构,不等同与变量本身。如下判断两个Variable是否相等的操作,就不能使用“==”操作符了。

1
2
3
4
5
6
w = tf.Variable(tf.zeros([3, 4]),  name='w', dtype=tf.float32)
b = tf.Variable(tf.ones([3, 4]))
k = tf.Variable(tf.constant(0.05, shape=[3,4]))
r = w + b*10
c0 = (w == b)
c1 = tf.equal(w, b)

3. Session

tensorflow使用Session类型对象来执行计算,从上面的print信息可以看到,并没有得到我们需要的计算结果,实际上那些语句只是再构造计算图,要开始计算需要使用Session。使用Session的一般流程是:先创建,然后使用Session初始化变量,最后使用Session运行计算图。

1
2
3
4
session = tf.Session()
session.run(tf.global_variables_initializer())
# session.run(tf.initialize_all_variables()) 老的api函数
session.run(r)

更加常见的用法是使用feed_dict参数喂数据,当有计算图有placeholder时;需要run来计算结果的变量可以组成一个列表来传入。

1
2
3
4
ret = tf.matmul(x, w)
session.run(tf.global_variables_initializer())
input_data = np.arange(12*2).reshape((8,3))
session.run([ret, r], feed_dict={x:input_data})

Tensorflow神经网络基本接口

在tensorflow中nn模块时专门提供神经网络层设计的,以下为最基本的几个接口函数。

1. conv2d

conv2d是基本的卷积运算,注意strides参数一般就改变中间两项,表示在图像的水平和垂直方向的strides;padding参数设置为“same”的目的是保持输入和输入的尺寸一样在没有stride的情况下。

1
2
3
4
5
input_tensor = tf.Variable(tf.ones([10,100,100,3]))
weight_tensor = tf.Variable(tf.ones([3, 3, 3, 2]))
layer = tf.nn.conv2d(input=input_tensor, filter=weight_tensor, strides=[1,1,1,1], padding='SAME')
session.run(tf.global_variables_initializer())
dat = session.run(layer)
2. max_pool

使用pooling层时一般都是max pooling,来保持图像局部的最大特征,同时下采样。注意ksize是pooling的kernel size,其尺寸为2x2,但是要做运行需要时四维的,前后维度值都设1就可以了。

1
2
3
layer = tf.nn.max_pool(value=layer, ksize=[1,2,2,1], strides=[1,2,2,1], padding='SAME')
session.run(tf.global_variables_initializer())
dat = session.run(layer)
3. relu

使用tf.nn.relu即可。

6. 代价函数

代价函数也在tensorflow的nn模块中,如tf.nn.softmax_cross_entropy_with_logits。当然这个函数的输出是Tensor,需要将其变为一个数值来方便优化,可以使用tf.reduce_mean函数。

6. 优化器

tensorflow提供的优化其在train模块中,如:tf.train.AdamOptimizer。

使用layers来辅助构建网络

使用上面最基本计算操作来构建网络是比较麻烦的,所以在tensorflow中有提供layers模块来给我们辅助搭建网络,尽管它在易用性上仍然不如像prettytensor、tf.contrib等其他基于tensorflow底层API的封装,但是layers毕竟是tensorflow自带的。

1. 使用tf.layers.conv2d构建卷积层

相比nn中的conv2d只是单纯的卷积操作,layers中的conv2d是构建一层通用的卷积层网络,自动处理指定的卷积核数以及激活函数。

1
net = tf.layers.conv2d(inputs=input_tensor, name='conv_layer1', padding='SAME', filters=16, kernel_size=5, activation=tf.nn.relu, strides=1)

2. 使用tf.layers.max_pooling2d创建池化层

对比nn中的max_pool操作,Layers中的max_pooling2d函数特指2维数组的池化,提供的接口参数更简单。

1
net = tf.layers.max_pooling2d(inputs=net, pool_size=2, strides=2)

3. 使用tf.layers.flatten和tf.layers.dense创建全连接

有些版本在layers中没有添加flatten函数,不过我现在的1.11.0版本是有的,它将四维的tensor转为全连接输入的二维数组;然后用dense函数创建全连接层。

1
2
net = tf.layers.flatten(net)
net = tf.layers.dense(inputs=net, name='fc1', units=128, activation=tf.nn.relu)

使用Keras API

尽管有了tf.layers、prettytensor以及tf.contrib这些模块,但是现在人们更多的使用的是Keras,原因是前面两个模块或多或少被开发者所遗忘,或者使用仍然不方便。

1. 导入Keras模块

由于Keras是一个独立的库,所以在安装了tensorflow后还需要再安装Keras,当前可以如下导入Keras库。

1
2
from tensorflow.python.keras.models import Sequential
from tensorflow.python.keras.layers import InputLayer, Input, Reshape, MaxPooling2D, Conv2D, Dense, Flatten

2. 使用Sequential模块创建网络

Keras有两种方式构建网络:一种是使用Sequential模块;另一种是使用Fuction model。使用Sequential是非常简单的,它只需要添加各种已有的网络层即可,但是这样也让你不能对网络结构做细致的改变。Fuction model的使用会有些怪异,不过它能够做更发杂的网络。

如下代码为使用Sequential构建网络的实例,Inputlayer与tensorflow中的placeholder作用是类似的。

1
2
3
4
5
6
7
8
9
10
model = Sequential()
model.add(InputLayer(input_shape=(100,100,3)))
model.add(Conv2D(kernel_size=5, strides=1, filters=16, padding='same', activation='relu', name='kersa_conv_layer1'))
model.add(MaxPooling2D(pool_size=2, strides=2))
model.add(Conv2D(kernel_size=5, strides=1, filters=36, padding='same', activation='relu', name='kersa_conv_layer2'))
model.add(MaxPooling2D(pool_size=2, strides=2))
model.add(Flatten())
model.add(Dense(128, activation='relu'))
model.add(Dense(2, activation='softmax'))
model.summary()

执行如上代码,model.summary()会打印一个很清晰的网络简介结构表:

3. model compilation

在Keras中为模型设置优化器、和loss function被称为“compilation”

1
2
3
4
5
6
from tensorflow.python.keras.optimizers import Adam

optimizer = Adam(lr=1e-3)
model.compile(optimizer=optimizer,
loss='categorical_crossentropy',
metrics=['accuracy'])

Training、Evaluation,实例使用:model.fit(x=data.x_train, y=data.y_train, epochs=1, batch_size=128)和result = model.evaluate(x=data.x_test,y=data.y_test)

Keras的Save与Load Model

使用Keras的Save与Load要比Tensorflow原生的功能要方便很多,不过在使用前需要先安装好”h5py”库。

1
2
3
4
5
6
from tensorflow.python.keras.models import load_model

path_model = 'model.keras'
model.save(path_model)
del model
model = load_model(path_model)

原教程中为上面的保存方法,会出现类似:“ValueError: You are trying to load a weight file containing 4 layers into a model with 0 layers”的问题,使用下面代码可以运行。

1
2
model.save_weights('model.keras')
model.load_weights('model.keras',by_name=True)
Compartir

反向传播算法推导

介绍

反向传播,即Backpropagation(BP)是the backward propagation of errors的缩写,顾名思义就是误差向后传播(输入/激励前向传播)。反向传播算法对深度学习的发展具有极其重要的意义,可以说是当前深度学习领域中最为核心的一个算法了,它的存在让我们训练更深的深度神经网络变得简单易行。

反向传播算法使用在如梯度下降的优化算法中,作为其计算个网络层中参数的梯度的方法来使用,其主要解决的问题是网络中各层权重参数对最终的损失函数(真实值与预测值的偏差)的贡献量。本文假定我们都是知道像梯度下降这类优化方法的,而梯度下降需要使用梯度值,所以在本文中我们的目标是如何求权重梯度。

反向传播算法利用最基本的链式求导法则来推导计算公式,本身是比较基础的知识,但是一旦结合上多层结构的神经网络就变得繁琐了,特别是对于初学者,各种上下标和字母标记让人看得云里雾里。所以当我开始理解反向传播算法的时候不妨抛开网络本身,先理解其计算方法的本质,然后再结合网络结构推导。当然前提是对神经网络本身的各个基本网络连接与作用有相当的了解。

链式求导法则

梯度是各个自变量对因变量的偏导的集合,所以求梯度的问题等价于求各个分量的偏导。那么怎么求偏导呢?除了那些最基本函数的导数公式外,对复合函数求导的链式法则肯定是必须的,而这些如 f(g(t(x))) 的形式不正是深度学习中神经网络一层一层的形式么。

我将链式求导法则的基本的公式列出,以帮助回忆其内容,这些都是高等数学微积分课程的基础知识,我就不详细说明了。

对于F(g(x))有(直接贴wiki上的公式):

Chain rule

另一种写法可能更容易理解,对于z=f(y)、y=g(x)有:

Chain rule

如上所说,当前深度学习的神经网络是层级式的,每一层的结构都可以看成其输出对其输入的函数,而上一层的输入到一层输入的连接也可以同样看成一个函数,如此神经网络的整体结构就可以看作一层一层函数的嵌套。当我需要求得某一层参数得导数时,就可以直接使用链式法则即可,这就反向传播算法得数学基础,很简单吧。

用计算图表示网络

我们以两层网络为例,为了简化流程,我们先隐藏细节,使用计算图来表示神经网络的结构,如下图所示:

简化计算图

如上我们有两层神经网络的结构,输入X,经过两层网络连接和激活函数输出Yhat,通过损失函数j来衡量真实值与估计值之前的误差大小,误差的结果就是J。输入的数据X通过网络向前传播,而误差J则从输出端通过网络逐层反向传播,这就是反向传播算法的直观描叙。

另外上图中的数值结果表示我都用大写字母写,用以表示这是一个向量或者矩阵。(博客显示公式不是很好,只能截图了)

  • 数据正向传播:

  • 求导反向求(应用链式求导法则):

上面公式就是使用链式求导法则得到各层输入对最终损失函数的导数,不过在使用中我们真正需要的是:网络层中各层的权重W和偏置B对损失函数的偏导(注意激活函数是没
有W和B的)
,所以有如下等式:

  • 第二层网络:

  • 第一层网络:

从上面这些公式可以看到,W和B的偏导是从输出端的偏导开始求起,然后逐层求到输入端的。

全连接神经网络的形式

以上面的计算图为基础,我们开始分析特定的网络,以常见的全连接神经网络多分类器为例子,简单示意图如下(只是示意图,不是说明其中每层中神经元的个数就是图中所示):

mlp

那么我们可以写出有一个隐含层(即总共两层)多分类网络的各个表达式:

  • 交叉熵损失函数j为,假定有n个类别,注意这里的求和是对后面向量中的元素求和,结果是一个标量:

  • 第二层的激活函数都使用softmax函数,注意分母中的求和表示对向量Z2中的每一个元素求和(顺便写一下特殊化的二分类的sigmod函数):

  • 第一层激活函数使用relu函数:

  • 两层全连接层函数:

整合其前向传播的公式如下:

下面不写A1等于0的部分

求这个全连接网络特定函数的导数

  • 损失函数的导数:

  • sigmod函数的导数比较简单(先用通用型的x来作为自变量):

  • softmax就会稍微复杂一些,虽然它仍然是输入一个向量,输出一个相同尺寸的向量,但是由于softmax函数分母是各项的求和,使得向量中的各个元素不再独立。所以求偏导时我们需要将向量拆分后讨论,用小写字母x和o加下标来表示向量中不同元素(x输入元素,o输出元素,i表示输入端元素引索,j表示输出端元素的引索,k表示任意元素引索),注意与上面的那些大写字母加下标的意思不同,大写字母的下表标网络层次号。还有一个要注意的是就是,e^ok对oi的偏导只有当k=i时是非零值,其他时候都是0

(i == j)

(i != j)

  • relu函数的导数(先用通用型的x来作为自变量):

  • 全连接层函数的导数就不用写了,就是一个简单的线性模型

推导权重的偏导

上面我们已经分析了对于两层多分类网络中,几个特定函数的导数或偏导,下面我将结合上面所有的知识,推导出其每个网络层次的导数,这里小写字母z、w、b第一层和第二层写法可能会重复,为了区分我加入上标表示不同网络层次,不是平方号

对于分类问题,Y中只有一个yi为1,其余的都为0,所以一般整合softmax cross entropy在一起,化简后其导数将变为非常简单的形式为:

推导第二层的权重和偏置的偏导:

第一层的权重和和偏置的偏导计算类似,不过得先计算A1和Z1的偏导。

由于Z1的偏导是分段,为简化,为0的那段就不考虑了,因为它已经无法对前面的网络起作用了,所以下面只考虑不为0的那一部分,第一层的权重和和偏置的偏导:

总结

初始化W和B后,根据前向的公式,我们可以逐层求出Z1、A1、Z2、Yhat、J;然后根据反向传播公式逐层反向求出各层的偏导。实际中,我们并不像上面一样需要写出偏导计算的最终公式,因为它只需要知道其上一层的偏导结果即可,但是得到最终的结果对于我们分析会有特别的好处。例如从上面的权重偏导公式中可以看到,其结果只与两个因素有关,一个是误差项,另一个就是其后面所有层次的权重成绩。

从这一点,我们可以很直接的看出,梯度爆炸与梯度消失的原因,正是由于浅层网络的权重偏导取决于后面所有网络权重的乘积,后面的W过大就会导致乘积项指数增大、而W过小又会导致其乘积越来越接近于0。当然梯度爆炸与梯度消失的主要原因是由于选用的不好的激活函数,我们这里使用的是relu,它在输入值大于0的时候,导数稳定为1所以,所以我们在公式的乘积中没有激活函数导数项,这样的激活函数不会随层次增加而衰减或爆炸。

参考

  1. Backpropagation
  2. Chain rule
  3. 卷积神经网络系列之softmaxloss对输入的求导推导
  4. 简单易懂的softmax交叉熵损失函数求导
Compartir

numpy的高级操作

参考教程

系列视频:https://www.bilibili.com/video/av11263377

继续继承前文,本文讲介绍numpy使用中一些更高级的知识和操作,这里的高级只是相对前面的基础而言的并不是严格意义上的高级。本文主要所包括的内容有:

  1. numpy中更高级的数组
  2. 讲python的函数转为numpy的Universal funtion
  3. 使用outer函数生成二维数组

Structured Array and Record Array

1. Structured Array

对我自己的工作来说,大多数情况下还是只是使用numpy基本的多维数组,对于structured array就接触的非常的少了。structured array是个很灵活的结构,它很类似C语言中的结构体,能够实现自定的数组类型、其中能包含多种基本数值类型,structured array有如下特点:

  • numpy基本的多维数组只能使用一种类型,而structured array则可以让不同类型的结构混合起来定义
  • 相比纯python的更灵活的结构如list和dict,structured array经过numpy底层的优化让其计算相比前者要高效的多
  • 一行赋值时需要用tuple而不是list
  • 基本类型的所占字节数时固定的,在包含字符串元素时需要注意,字符长度超过定义的长度将会截断舍弃

如下为定义structured array:

1
2
3
4
import numpy as np

user_type = [('name', 'S6'), ('height', 'f8'), ('weight', 'f8'), ('age', 'i8')]
user_type

对其进行初始化和赋值:

1
2
3
4
stru_arr = np.zeros(4, dtype=user_type)
stru_arr[0] = ('kaka', 12, 82.5, 12)
stru_arr[1][0] = 'harry456'
stru_arr[2][0] = 'jack'

中structured array中,由于定义的时候,每一列元素类型都有名称,所以可以使用名称来引索整列:

1
stru_arr['name']

2. record array

record array与structured array类似,不过它特点是在引索某一列时将中括号变成成员引用号,如下将上面的structured array转化为record array,使用“.”访问:

1
2
3
4
5
rec_arr = np.rec.array(stru_arr)
rec_arr[0].name = 'lr'
# 下面两个结果是不一样的
stru_arr[0]['name']
rec_arr[0].name

将普通函数转化为numpy的Universal funtion

NumPy提供了两种基本的对象:ndarray(N-dimensional array object)和 ufunc(universal function object)。ndarray是存储单一数据类型的多维数组,而ufunc则是能够对数组进行处理的函数。ufunc的特点是它能够遍历numpy数组的中的每一个元素,并对其做相同的操作运算,而不需要使用for循环。Numpy内置的许多ufunc函数都是C语言实现的,计算速度非常快。

下面主要演示怎么将普通的python函数变为ufunc,不过这样转化的ufunc只是写法会方便很多,效率提高就不是很多,我自己测试就大概快1倍左右。

1
2
3
4
5
6
def normal_func(x):
return x**2-(x-1)**2

test_dat = np.arange(12).reshape((2,6))
my_ufunc = np.frompyfunc(normal_func, 1, 1)
my_ufunc(test_dat)

上面代码,可以看到使用frompyfunc函数,将普通的python函数转变为了nfunc,其中第一个参数是需要转化的函数名,第二个参数是转化函数的参数个数,第三个参数是返回值个数。

使用outer函数生成二维数组

1. numpy.outer

outer可以用两个向量,生成一个二维数组,直接使用outer函数得到的矩阵原始为向量各个元素的对应乘积,类似于混淆矩阵。如下为用outer函数生成一个记录9x9的乘法表矩阵:

1
2
3
v1 = np.arange(9)+1
v2 = np.arange(9)+1
np.outer(v1, v2)

2. numpy.ufunc.outer

处理直接使用outer外,还支持numpy的ufunc,在这种使用中outer函数的生成方法由ufunc决定。如下就是将操作从默认的乘法换成加法:

1
np.add.outer(v1,v2)
Compartir

numpy的基础操作2

参考教程

系列视频:https://www.bilibili.com/video/av11263377

继承前文,本文讲重点讲解numpy数组与数组,主要内容包括:

  1. 数组元素顺序的重新排列
  2. 数组的转置操作
  3. 数组的重复操作
  4. 数组的分解和拼接

修改元素排列顺序

1. fliplr

fliplr函数可以将数组左右元素的排列反序(l:left r:right 表示左右),需要注意的是不同维度数组的结果差异,且数组维度必须2维或以上的,其反序在都是在数组的第一纬度上。

2. flipud

fliplr函数可以将数组上下元素的排列反序(u:up,d:down 表示上下)。其反序在都是在数组的第零纬度上。

1
2
3
arr_d2 = np.arange(10).reshape((2,5))
np.fliplr(arr_d2)
np.flipud(arr_d2)

3. roll

循环移动,注意即使是多维数组roll时,仍然以一维的顺序移动

1
2
arr_d2 = np.arange(10).reshape((2,5))
np.roll(arr_d2, -2)

转置操作

1. transpose

transpose转置操作,当数组是二维时默认为矩阵的转置操作,其axis参数可以设置转置维度的排列顺序:

1
2
3
4
5
6
# 两者一样
np.transpose(arr_d2)
arr_d2.T
# 指定顺序
arr_d3 = np.arange(24).reshape((2,3,4))
tra = np.transpose(arr_d3, axes=(2,1,0))

2. swapaxes

交换两个维度:

1
np.swapaxes(arr_d2, 1, 0)

3. stack

stack函数它也是对数组指定维度维度上的交换,小于axis参数的维度都会被交换:

1
ret = np.stack(arr_d3, axis=2)

4. rollaxis

维度的循环交换:

1
np.rollaxis(arr_d3, 0, 2)

重复操作

1. tile

在生成数据时,很可能需要将数据重复多份,在numpy中最常用的重复函数就是tile。

1
2
3
4
arr_d1 = np.arange(10)
np.tile(arr_d1, 3)
np.tile(arr_d1, (3,2))
np.tile(arr_d2, (2,2,2))

2. repeat

repeat函数与tile函数功能类似,不过repeat函数可以指定沿某个轴复制,另外tile复制时是将原有数组得一个维度整体重复复制,而repeat则可以精确到每一个元素。

1
2
arr_d3 = np.arange(24).reshape((2,3,4))
np.repeat(arr_d3, 2, axis=2)

拼接与分解数组

1. concatenate

join操作的一种, 实际上和append的方法类似,但是它允许输入多个数组一起连接:

1
2
3
orig_arr = np.arange(12).reshape(2,2,3)
orig_arr1 = np.arange(112,124).reshape(2,2,3)
con_arr = np.concatenate((orig_arr, orig_arr1, orig_arr), axis=2)

2. split

split函数可以将一个数组划分成几个相等元素个数的子数组,注意设置划分个数时一定号被整除被划分的轴的长度整除

1
2
3
np.split(np.arange(12), 4)
orig_arr = np.arange(12).reshape(2,2,3)
np.split(orig_arr, 2, axis=1)
Compartir