-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsearch.xml
executable file
·183 lines (183 loc) · 75.8 KB
/
search.xml
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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title><![CDATA[博客搬家了~]]></title>
<url>%2F%E5%8D%9A%E5%AE%A2%E6%90%AC%E5%AE%B6%2Fblog-move%2F</url>
<content type="text"><![CDATA[本文原载于https://imlogm.github.io,转载请注明出处~ 摘要:新博客地址:https://segmentfault.com/u/logm/articles 关键字:博客搬家 新地址新博客地址:https://segmentfault.com/u/logm/articles 原因Github Page 很好玩,有非常大的灵活性。看到有小伙伴留言阅读后收获很大,我也很开心。 缺点也很明显,所见非所得,需要花费大量的时间在调整排版上。还要定期维护SEO,保证可以被搜索引擎收录。 开始实习后,空闲时间被压缩。手头积累了一堆笔记,却没时间发布到博客上。 之后的博客风格会更偏向笔记,谷歌可以检索到的内容不再做细致讲解,节约时间,保证产出。 博客内容依旧是非常精华的原创,拒绝无脑转载!]]></content>
<categories>
<category>博客搬家</category>
</categories>
<tags>
<tag>博客搬家</tag>
</tags>
</entry>
<entry>
<title><![CDATA[解决python的中文字符编码问题]]></title>
<url>%2F%E8%87%AA%E7%84%B6%E8%AF%AD%E8%A8%80%E5%A4%84%E7%90%86%2Fcharacter-encoding%2F</url>
<content type="text"><![CDATA[本文原载于https://imlogm.github.io,转载请注明出处~ 摘要:最近在做自然语言处理相关的项目,发现中文编码的问题实在需要好好学习下,我用python为例,简单介绍下python编程时如何处理好中文编码的问题。 关键字:自然语言处理, 字符编码, python 1. 从字符编码谈起讲真,字符编码是很大的一块内容,单用一篇博客是完全讲不完的。这里借用一下大佬的文章:字符编码笔记:ASCII,Unicode 和 UTF-8 - 阮一峰的日志 看完上面的那篇文章之后,相信你对字符编码有了一定的认识。在中文的自然语言处理中,最常遇到的是ASCII,Unicode,UTF-8,GB2312,GBK等。这几种编码,你都可以搜索相关的文章看下,我这里就不展开介绍了。直接用几个python的程序解释下如何在python中处理字符编码的问题。 2. 关于python的str类型和print过程比如一段程序:123456# -*- coding:utf-8 -*-s = "这是一段中文" # s是str类型的变量print(s)# 程序输出:“这是一段中文” 这段程序中的变量s就是str类型的。我们都知道计算机内部都是二进制的0和1,str类型就是这样的0和1组成的二进制字节流,也就是说这里的变量s在计算机内部是一段二进制字节,并不是字符串。 如果你是在python的交互式编程环境中,那么你可以做个实验:123>>> s = "这是一段中文">>> s\xe8\xbf\x99\xe6\x98\xaf\xe4\xb8\x80\xe6\xae\xb5\xe4\xb8\xad\xe6\x96\x87 \x表示这个数是十六进制数,\xe8表示这个数是十六进制数“E8”,转换为二进制为11101000,上面的“\xe8\xbf\x99\xe6\x98\xaf\xe4\xb8\x80\xe6\xae\xb5\xe4\xb8\xad\xe6\x96\x87”就是变量s所代表的字节流,换句话说,就是字符串“这是一段中文”在utf-8编码格式下的二进制表示。 这里就出现了两个问题: 变量s赋值时,一段中文字符串是怎么变成二进制字节流的? 打印变量s时,二进制字节流是怎么变成一段中文字符串的? 首先是问题1。变量s赋值时,字符串经过某种编码方式编码(encode)成为二进制字节,再赋值给变量s。这里的“某种编码方式”由代码显式指出,代码的第一行# -*- coding:utf-8 -*-就是用来显式地告诉计算机,你在str类型赋值时,用utf-8的编码方式。 然后是问题2。打印变量s时,二进制字节流通过某种编码方式解码(decode)为字符串。这里的“某种编码方式”由操作系统指出。我用的ubuntu系统使用的是utf-8的编码方式。 注意体会编码和解码这两个词的不同。编码方式和解码方式一样,才能正常print,否则显示的是乱码。 可能你还是不太明白,我们用上面的程序再做一组实验。因为每台电脑的命令行的编码方式不一样,我用的是ubuntu的系统,编码格式是utf-8,我以我的电脑为例来讲解。同时,注意实验要在命令行的状态下进行。有些ide比较智能,会自动更换输出环境的编码格式,达不到实验效果。 第一个程序和上面的一样,我们来看下效果:123456789# -*- coding:utf-8 -*-# 命令行的编码方式为utf-8s = "这是一段中文" # s是str类型的变量,计算机把字符串以utf-8格式编码成二进制字节,赋值给sprint(s) # s是str类型的变量,计算机读取s(也就是读取出二进制字节),然后以utf-8格式解码为字符串# 程序输出:“这是一段中文” 然后,我们改动第一行:123456789# -*- coding:GBK -*-# 命令行的编码方式为utf-8s = "这是一段中文" # s是str类型的变量,计算机把字符串以GBK格式编码成二进制字节,赋值给sprint(s) # s是str类型的变量,计算机读取s(也就是读取出二进制字节),然后以utf-8格式解码为字符串# 程序输出:一段乱码 我们再做第三个实验:123456789# -*- coding:utf-8 -*-# 命令行的编码方式为GBKs = "这是一段中文" # s是str类型的变量,计算机把字符串以utf-8格式编码成二进制字节,赋值给sprint(s) # s是str类型的变量,计算机读取s(也就是读取出二进制字节),然后以GBK格式解码为字符串# 程序输出:一段乱码 我想你应该能理解这三个程序之间的区别。 2. 关于unicode类型unicode类型是python中的一种字符串类型,在计算机内也是二进制字节。不过不同于str是单纯的二进制字节,unicode类型特指由ucs2或者ucs4编码格式编码的二进制字节。 如果你在python的交互式编程环境中,你可以做个实验:123>>> s = u"这是一段中文" # 这边多了个u,表示变量s为unicode变量>>> su'\u8fd9\u662f\u4e00\u6bb5\u4e2d\u6587' 可以看到,和上面str类型的实验结果的\x不一样了,这里出现的是\u。\u代表了在unicode编码表中的位置,比如\u8fd9就代表unicode编码表中8fd9这个位置的字符。 python中unicode类型的变量是作为一个中转站存在的。比如你要把一段字符串从utf-8编码转为GBK编码,你需要做的是: 123456789# -*- coding:utf-8 -*-s = "这是一段中文" # s是str类型的变量,计算机把字符串以utf-8格式编码成二进制字节,赋值给ss.decode("utf-8") # 二进制字节s以utf-8格式解码到unicode,解码后s从str类型变为unicode类型s.encode("GBK") # unicode类型的变量s被以GBK格式编码为二进制字符串,编码后变量s从unicode类型变为str类型# 程序输出:一段乱码 反过来,要把一个GBK编码的字符串转为utf-8也一样,要以unicode作为中转站。 3. sys.setdefaultencoding(‘utf-8’)网上一些教程会教你,在遇到中文编码问题的时候,在代码的开头加上这几句:1234# -*- coding:utf-8 -*-import sysreload(sys)sys.setdefaultencoding('utf-8') 你一试,还真的解决了问题,但你不知道这几句话有什么用。 第一句# -*- coding:utf-8 -*-上一段已经说了,是为了显式地说明代码是由utf-8格式编码的,如果你不加的话,一般来说是采用默认编码ascii。ascii不支持中文,你的代码中有任何中文就会出错。 后面三句,最重要的是sys.setdefaultencoding('utf-8'),它的目的是修改默认的解码方式为utf-8。 看下面的实验:123# -*- coding: utf-8 -*- s = '中文字符' # s是字符串经过utf-8编码格式编码后的二进制字节,str类型s.encode('GBK') # s是二进制字节,它不会直接encode。python会首先调用decode,将s从str类型变为unicode格式,再用GBK编码为str类型 你可以使用以下代码获取python默认的解码方式:12import sysprint(sys.getdefaultencoding()) 假如你获取到的默认解码方式为ascii。那么:123456# -*- coding: utf-8 -*- s = "这是一段中文" # s是字符串经过utf-8编码格式编码后的二进制字节,str类型s.encode("GBK") # s是二进制字节,它不会直接encode。python会首先调用decode,将s从str类型变为unicode格式,再用GBK编码为str类型# 如果你的默认解码方式为ascii,那么上面一句话在实际执行时,相当于下面这句话s.decode("ascii").encode("GBK") 显然,程序会报错:UnicodeDecodeError: ‘ascii’ codec can’t decode byte 0xe4 in position 。 解决方法1,显示地指明解码格式:1234# -*- coding: utf-8 -*- s = "这是一段中文" s.decode("utf-8").encode("GBK") 解决方法2,修改默认解码方式:12345678# -*- coding: utf-8 -*- import sysreload(sys)sys.setdefaultencoding('utf-8')s = "这是一段中文" s.encode("GBK") 你应该能从这几个实验中明白sys.setdefaultencoding('utf-8')的作用。 4. 检验学习成果python常见编码错误集合 - 妙音的博客 看看上面链接的博客里所列举的几个错误示例,现在你是否能够一眼就找出错误点,并给出解决方法呢?]]></content>
<categories>
<category>自然语言处理</category>
</categories>
<tags>
<tag>自然语言处理</tag>
<tag>字符编码</tag>
<tag>python</tag>
</tags>
</entry>
<entry>
<title><![CDATA[关于梯度下降法和牛顿法的数学推导]]></title>
<url>%2F%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0%2FgradientDescent%2F</url>
<content type="text"><![CDATA[本文原载于https://imlogm.github.io,转载请注明出处~ 摘要:梯度下降法是机器学习中用到比较多的一种求解方法,一般大家会通过形象化的图像来理解为什么梯度下降法有效。本文用数学推导的方式来证明梯度下降法有效,同时也介绍下牛顿法。 关键字:机器学习, 梯度下降, 牛顿法 1. 梯度下降法的推导梯度下降法在机器学习和深度学习里用的非常多,一般教程或者教材在解释梯度下降法的时候会用形象化的方式(二次曲线、下凸面等),想必大家都知道如何用形象化的方式来说明梯度下降法的有效性。这里,我就不再赘述这种形象化的解释了。我这里使用数学推导来证明梯度下降法的有效性。 一元函数的泰勒展开大家应该都知道,公式如下:$$ f(x)=f(x_0)+\frac{f’(x_0)}{1!}(x-x_0)+\frac{f’’(x_0)}{2!}(x-x_0)^2+\frac{f’’’(x_0)}{3!}(x-x_0)^3+… $$ 不妨只取右边式子的前两项,也就是一个“约等于”的结果:$$ f(x)=f(x_0)+\frac{f’(x_0)}{1!}(x-x_0) $$ 记:$ \Delta x=x-x_0 $,上式变为:$$ f(x)=f(x_0)+\frac{f’(x_0)}{1!}\Delta x $$ 我们的目标是在迭代过程中让下式恒成立,也就是说希望迭代过程中函数值会逐渐减小,用数学语言描述就是:$ f(x_{n+1})\leq f(x_{n}) $ 容易想到,应该构造:$$ \Delta x=-f’(x_0) $$ 此时:$$ f(x)=f(x_0)-f’(x_0)^2 $$ 写成迭代形式:$$ f(x_{n+1})=f(x_{n})-f’(x_{n})^2 $$ 由于$ f’(x)^2\geq 0 $,我们就完成了对于梯度下降有效性的证明。从上述步骤归纳出来的参数迭代更新的公式如下:$$ x_{n+1}=x_{n}-\alpha \Delta x_{n} $$ 以上步骤是在一元函数上证明了梯度下降的有效性。容易推广到多元函数。另外,在多元函数中,还可以补充证明梯度方向是下降最快的方向。详见:为什么梯度下降能找到最小值?-root的回答 2. 牛顿法说完了梯度下降法,顺便介绍下牛顿法的推导。因为牛顿法也是通过泰勒展开推导出来的。 继续看泰勒展开:$$ f(x)=f(x_0)+\frac{f’(x_0)}{1!}(x-x_0)+\frac{f’’(x_0)}{2!}(x-x_0)^2+\frac{f’’’(x_0)}{3!}(x-x_0)^3+… $$ 依旧,我们取右式的前2项:$$ f(x)=f(x_0)+\frac{f’(x_0)}{1!}(x-x_0) $$ 对等式两边取导数:$$ f’(x)=f’(x_0)+\frac{f’’(x_0)}{1!}(x-x_0) $$$$ f’(x)=f’(x_0)+\frac{f’’(x_0)}{1!}\Delta x $$ 根据微积分的性质,$ f(x) $取最小值时,有$ f’(x)=0 $,我们把这个性质代入上面的式子,有:$$ 0=f’(x_0)+\frac{f’’(x_0)}{1!}\Delta x $$$$ \Delta x=-\frac{f’(x_0)}{f’’(x_0)} $$ 这样我们就得到了牛顿法的参数迭代更新公式如下:$$ x_{n+1}=x_{n}-\frac{f’(x_n)}{f’’(x_n)} $$ 3. 梯度下降法和牛顿法的异同从上面的证明过程可以看出,梯度下降法和牛顿法虽然都可以用泰勒展开推导,但推导所依据的思想还是有一点不一样的。 在实际运用中,牛顿法和梯度下降法都是广泛应用于机器学习中的。两者的区别其实很多博客都有写,比如:梯度下降or拟牛顿法?-过拟合的回答 4. 拟牛顿法在上面牛顿法的参数迭代更新公式中,我们可以看到$ f’’(x_0) $是位于分母部分的。记住,上面的数学推导是用的一元函数,对于多元函数,这个分母存在相当于要计算Hessian矩阵的逆矩阵,这是非常困难且耗费时间的。因此,很多牛顿算法的变形出现了,这类变形统称拟牛顿算法。BFGS是用迭代法去近似计算海森矩阵。而BFGS需要额外储存近似的那个海森矩阵,所以有了改进版L-BFGS。]]></content>
<categories>
<category>机器学习</category>
</categories>
<tags>
<tag>机器学习</tag>
<tag>梯度下降</tag>
<tag>牛顿法</tag>
</tags>
</entry>
<entry>
<title><![CDATA[RCNN系列(R-CNN、Fast-RCNN、Faster-RCNN、Mask-RCNN)]]></title>
<url>%2F%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0%2Frcnn%2F</url>
<content type="text"><![CDATA[本文原载于https://imlogm.github.io,转载请注明出处~ 摘要:物体检测(object detection)是计算机视觉非常重要的一个领域。在深度学习出现之前,传统方法始终无法处理好物体检测问题,在深度学习方法引入之后,物体检测领域发生了翻天覆地的变化。最著名的是RCNN系列,另外还有YOLO、SSD。这篇文章首先介绍RCNN系列。 关键字:深度学习, 物体检测, RCNN 1. 前言物体检测(object detection)是计算机视觉非常重要的一个领域。在深度学习出现之前,传统方法始终无法处理好物体检测问题,在深度学习方法引入之后,物体检测领域发生了翻天覆地的变化。最著名的是RCNN系列,另外还有YOLO、SSD。这篇文章首先介绍RCNN系列。 网上关于RCNN系列介绍的文章非常多,比如基于深度学习的目标检测技术演进:R-CNN、Fast R-CNN、Faster R-CNN-冠军的试练的博客就用非常简洁明了的语言介绍了整个系列的诞生和演变过程。 如果你不在意具体的实现细节的话,上面链接中的文章就已经足够了。下面贴出的文章将更加深入实现细节,我会在每篇文章之后写上自己阅读时想到的问题,也许你们也会与我有同样的问题。 2. R-CNNRCNN-将CNN引入目标检测的开山之作-晓雷的文章 RCNN的过程分4个阶段: 候选区域提出阶段(Proposal):采用selective-search方法,从一幅图像生成1K~2K个候选区域; 特征提取:对每个候选区域,使用CNN进行特征提取; 分类:每个候选区域的特征放入分类器SVM,得到该候选区域的分类结果; 回归:候选区域的特征放入回归器,得到bbox的修正量。 下面是我在阅读时想到的一些问题。 2.1 候选区域提出阶段所产生的结果尺寸不同?由于RCNN特征提取阶段采用的是AlexNet,其最后两层是全连接层fc6和fc7,所以必须保证输入的图片尺寸相同。 而候选区域所产生的结果尺寸是不相同的。为此,论文中作者采用了多种方式对图片进行放缩(各向同性、各向异性、加padding),最后经过对比实验确定各向异性加padding的放缩方式效果最好。各向同性、各向异性、加padding的具体效果可以看上面链接中的文章。 2.2 分类器SVM使用的是二分类?论文中,单个SVM实现的是二分类,分类器阶段由多个SVM组合而成。比如总共有20种不同的物体(加1种背景),那么分类阶段必须要有21个SVM:第1个SVM的输出是该候选区域属于分类1的概率;第2个SVM的输出是该候选区域属于分类2的概率;……;第21个SVM的输出是该候选区域属于背景的概率。 对21个SVM的输出结果进行排序,哪个输出最大,候选区域就属于哪一类。比如,对于某个候选区域,第21个SVM的输出最大,那么就将该候选区域标为背景。 2.3 分类器的输入是?回归器的输入是?分类器的输入是特征提取器AlexNet的fc6的输出结果,回归器的输入是特征提取器AlexNet的pool5的输出结果。 之所以这样取输入,是因为,分类器不依赖坐标信息,所以取fc6全连接层的结果是没有问题的。但是回归器依赖坐标信息(要输出坐标的修正量),必须取坐标信息还没有丢失前的层。而fc6全连接层已经丢失了坐标信息。 2.4 正负样本的选择?正负样本是必须要考虑的问题。论文的做法是每个batch所采样的正负样本比为1:3。当然这个比例是可以变化的,这个系列的后续改进就把正负样本比变为了1:1。 如果之前没有接触过类似问题的话,是比较容易想当然地认为训练特征提取器、分类器、回归器时,就是把候选区域生成阶段的所有候选区域都放入训练。这样的思路是错的。一张图片中,背景占了绝大多数地方,这样就导致训练用的正样本远远少于负样本,对训练不利。 正确的做法是对所有候选区域进行随机采样,要求采样的结果中正样本有x张,负样本y张,且保证x与y在数值上相近。(对于一些问题,不大容易做到x:y = 1:1,但至少x与y应该在同一数量级下) 2.5 如何训练?RCNN的网络架构,注定了它不能像其他网络那样进行端到端(end-to-end)的训练。 前面提到RCNN分为4个阶段:Proposal阶段、特征提取阶段、分类阶段、回归阶段。这4个阶段都是相互独立训练的。 首先,特征提取器是AlexNet,将它的最后一层fc7进行改造,使得fc7能够输出分类结果。Proposal阶段对每张图片产生了1k~2k个候选区域,把这些图片依照正负样本比例喂给特征提取器,特征提取器fc7输出的分类结果与标签结果进行比对,完成特征提取器的训练。特征提取器的训练完成后,fc7层的使命也完成了,后面的分类器和回归器只会用到fc6、pool5的输出。 然后,Proposal和特征提取器已经训练完毕了。把它们的结果fc6,输入到分类器SVM中,SVM输出与标签结果比对,完成SVM的训练。 最后,回归器的训练也和SVM类似,只不过回归器取的是pool5的结果。 为什么不能同时进行上面3步的训练?因为特征提取器是CNN,分类器是SVM,回归器是脊回归器,不属于同一体系,无法共同训练。甚至在测试时,也需要把每一阶段的结果先保存到磁盘,再喂入下一阶段。这是非常麻烦的一件事。 聪明的你可能已经想到了:CNN不就能完成分类器和回归器的任务嘛?为什么不只用CNN?这就是RCNN系列后续做的改进之一,我们在下面会讲到。但由于某些原因,在RCNN这篇论文发表时,采用的是特征提取、分类器、回归器相互独立的结构。、 这里顺便提一下脊回归器,脊回归器又称岭回归器(Ridge Regression),表示该回归器加了L2正则化。 2.6 Proposal的每个候选区域单独提取特征,是不是很慢?是的,很慢。Proposal阶段会产生1k~2k个候选区域,每个候选区域都独立提取特征的话,那相当于每幅图片都要进行1k~2k次CNN。(当然由于有正负样本采样,实际并没有有这么多) 有没有什么好方法?聪明的你应该能想到:既然候选区域都是图片的一部分,那么先对整张图片进行特征提取,然后根据每个候选区域在原图上的位置选择相应的特征不就行了。 这种方式正是RCNN系列的后续改进之一,只不过在实现上要动点脑筋。(这种方式得到的每个区域的特征数目是不同的,如何把不同特征数目变为相同数目?) 3. Fast-RCNNFast R-CNN-晓雷的文章 理解了RCNN就能很快理解Fast-RCNN了。来讲讲Fast-RCNN相对于RCNN的改进之处。 首先,正如我们在2.5节提到的,Fast-RCNN将特征提取器、分类器、回归器合在了一起,都用CNN实现。 其次,正如我们在2.6节提到的,Fast-RCNN对整张图片进行特征提取,再根据候选区域在原图中的位置挑选特征。针对特征数目不同的问题,Fast-RCNN加入了ROI层,使得经过ROI层后,特征的数目相同。 ROI层的出现与SPPNet有密不可分的联系,请参见:SPPNet-引入空间金字塔池化改进RCNN-晓雷的文章 3.1 为什么叫Fast?将特征提取器、分类器、回归器合并,使得训练过程不需要再将每阶段结果保存磁盘单独训练,可以一次性完成训练,加快了训练速度。这是Fast之一。 对整张图片进行特征提取,用ROI层处理候选区域的特征,使得原本每一个候选区域都要做一次特征提取,变为了现在一整张图片做一次特征提取。训练速度(8.8倍)和测试速度(146倍)都大大加快,这是Fast之二。 3.2 分类器和回归器的实现细节?分类器应该都能想到,用的softmax代替SVM。 回归器求出(x,y,w,h)4个量,分别代表定位框左上角的坐标xy、宽度w、高度h,损失函数用的是Smooth-L1。 3.3 Proposal阶段看上去有点违和?发展到Fast-RCNN,后续3个阶段都是CNN完成的了,只剩下Proposal阶段还没有用CNN方式解决。Proposal阶段的结果还是需要先保存到磁盘,再喂入后续阶段,有点违和。 RCNN系列后续的改进,将把Proposal阶段也用CNN实现,真正做到端到端(end-to-end)。 4. Faster-RCNNFaster R-CNN-晓雷的文章 Faster-RCNN引入了RPN网络(region proposal network)来代替selective-search。这使得整个网络实现了端到端。 4.1 RPN网络是如何工作的?上面链接里的文章讲的挺详细的:整张图片经过特征提取,得到FeatureMap;将FeatureMap中的每一点按照视野域找到原图中对应的位置,称为Anchor;每个Anchor生成不同大小不同长宽比的多个候选区域。 回忆下selective-search的候选区域生成方式,它是按照颜色和纹理不断合并得到候选区域的,候选区域的产生没有规律,而RPN是每个Anchor都有对应的固定数量的候选区域,规律很明显。 理论上说,selective-search生成候选区域的方式更符合我们的直觉,而实验结果,在Faster-RCNN中RPN并不比selective-search差 4.2 为什么是Faster?容易想到,现在RPN网络可以与其他3个阶段共用同一个特征提取结果了,省掉了selective-search的时间。而事实上,selective-search是非常慢的,所以叫Faster。 5. Mask-RCNNMask-RCNN技术解析-linolzhang的专栏 到Faster-RCNN时,RCNN系列对物体检测问题已经非常拿手了。Mask-RCNN则是将RCNN扩展到语义分割领域。 5.1 为什么叫mask?Faster-RCNN网络的最后分别是分类网络和回归网络两条路并行,Mask-RCNN则是再加一条Mask网络与它们并行。 Mask网络的实现是FCN网络,这也是语义分割领域中非常经典的网络结构。 由于Mask网络的加入,Mask-RCNN不仅能处理物体检测问题,还能处理语义分割问题。 5.2 还有哪些细节上的变化?首先是ROI层变为了ROIAlign,目的是一样的。那为什么要加入ROIAlign呢?这是因为ROI层会有对齐问题,对齐问题在分类和框选时影响不大,但在语义分割需要严格依赖每个像素点的坐标时,影响会很大。ROIAlign能够解决对齐问题。 然后是特征提取网络改为了ResNet101+FPN,ResNet我在之前的博客中有细讲;FPN建议对语义分割或者关键点定位感兴趣的同学了解下,FPN是这两个领域中非常经典的结构。]]></content>
<categories>
<category>深度学习</category>
</categories>
<tags>
<tag>深度学习</tag>
<tag>物体检测</tag>
<tag>RCNN</tag>
</tags>
</entry>
<entry>
<title><![CDATA[图像分割之最小割与最大流算法]]></title>
<url>%2F%E5%9B%BE%E5%83%8F%E5%A4%84%E7%90%86%2Fmincut-maxflow%2F</url>
<content type="text"><![CDATA[本文原载于https://imlogm.github.io,转载请注明出处~ 摘要:图像分割中”Graph Cut”、”Grab Cut”等方法都有使用到最小割算法。网上资料介绍了Graph cut和Grab cut中图的构建方法,但对最小割的求解一笔带过。所以萌生了写一篇介绍图的最小割和最大流的博客的想法。 关键字:图像处理, 最小割, 最大流, 图像分割 1. 题外话图的最小割和最大流问题是图论中比较经典的问题,也是各大算法竞赛中经常出现的问题。图像分割中”Graph Cut”、”Grab Cut”等方法都有使用到最小割算法。网上资料介绍了Graph cut和Grab cut中图的构建方法,但对最小割的求解一笔带过。所以萌生了写一篇介绍图的最小割和最大流的博客。 2. 关于最小割(min-cut)假设大家对图论知识已经有一定的了解。如图1所示,是一个有向带权图,共有4个顶点和5条边。每条边上的箭头代表了边的方向,每条边上的数字代表了边的权重。 G = < V, E >是图论中对图的表示方式,其中V表示顶点(vertex)所构成的集合,E表示边(edge)所构成的集合。顶点V的集合和边的集合E构成了图G(graph)。 图1 那什么是最小割呢? 以图1为例,图1中顶点s表示源点(source),顶点t表示终点(terminal),从源点s到终点t共有3条路径: s -> a -> t s -> b -> t s -> a -> b-> t 现在要求剪短图中的某几条边,使得不存在从s到t的路径,并且保证所减的边的权重和最小。相信大家能很快想到解答:剪掉边”s -> a”和边”b -> t”。 剪完以后的图如图2所示。此时,图中已不存在从s到t的路径,且所修剪的边的权重和为:2 + 3 = 5,为所有修剪方式中权重和最小的。 我们把这样的修剪称为最小割。 图2 3. 关于最大流(max-flow)什么是最大流呢? 继续以图1为例,假如顶点s源源不断有水流出,边的权重代表该边允许通过的最大水流量,请问顶点t流入的水流量最大是多少? 从顶点s到顶点t的3条路径着手分析,从源点s到终点t共有3条路径: s -> a -> t:流量被边”s -> a”限制,最大流量为2 s -> b -> t:流量被边”b -> t”限制,最大流量为3 s -> a -> b-> t:边”s -> a”的流量已经被其他路径占满,没有流量 所以,顶点t能够流入的最大水流量为:2 + 3 = 5。 这就是最大流问题。所以,图1的最大流为:2 + 3 = 5。 细心的你可能已经发现:图1的最小割和最大流都为5。是的,经过数学证明可以知道,图的最小割问题可以转换为最大流问题。所以,算法上在处理最小割问题时,往往先转换为最大流问题。 那如何凭直觉解释最小割和最大流存在的这种关系呢?借用Jecvy博客的一句话:1.最大流不可能大于最小割,因为最大流所有的水流都一定经过最小割那些割边,流过的水流怎么可能比水管容量还大呢? 2.最大流不可能小于最小割,如果小,那么说明水管容量没有物尽其用,可以继续加大水流。 4. 最大流的求解现在我们已经知道了最小割和最大流之间的关系,也理解了最小割问题往往先转换为最大流问题进行求解。那么,如何有效求解最大流问题呢? 一种是Ford-Fulkerson算法,参见博客:装逼之二 最小割与最大流(mincut & maxflow)-carrotsss 另一种是Yuri Boykov的优化算法,参见博客:CV | Max Flow / Min Cut 最大流最小割算法学习-iLOVEJohnny的博客 事实上,我并不打算自己再把最大流算法讲解一遍,因为最大流算法很容易在搜索引擎上搜索到。我真正要讲的是下面的部分,关于如何把最大流的结果转换到最小割。 5. 如何把最大流的结果转换为最小割网上介绍最小割和最大流往往介绍完最大流的求解方法后就不继续讲解了,我上面贴出的两篇博客都存在这个问题。这样大家肯定会有个疑惑:如何把最大流的结果转换为最小割。 我以上面贴出的Ford-Fulkerson算法的博客的结果为例讲解下如何转换。如图3是最大流求解算法的最终结果。边上的数字“@/#”表示这条边最大流量为#,在最大流求解算法中该边所使用的流量为@。比如边“13/15”表示该边最大能容纳的流量为15,但在最大流求解算法中使用到的流量为13。 图3 我们把流量已经占满的所有边去掉,得到图4: 图4 此时,在图4中,以顶点s为起点进行图遍历(遍历的方法可以选择BFS广度优先遍历)。遍历结束后,可以得到遍历经过的所有点:S、B、C、D。 有些没学过图遍历的小伙伴可以这样理解:从顶点s出发,最多能经过哪些点。那么,很显然,最多能经过S、B、C、D这几个点。只不过人脑回答这个问题比较简单,但计算机需要特定的算法来解答,也就是上一段所说的”图遍历算法”。 这样,把S、B、C、D构成一个子图(图4中紫色部分),其他的点A、E、F、t构成另一个子图(图4中黄色部分)。连接两个子图的边有两种情况: 已被占满的前向边:s -> A, B -> E, D -> t 没有流量的反向边:A -> B, E -> D 其中“已被占满的前向边”就是我们要求的最小割。对于图4来说,就是”s -> A”、”B -> E”、”D -> t”这3条边。 6. 如何将最小割方法应用到图像分割中写这篇有关最小割的博客,其实是为了给下一篇博客做铺垫。下一篇博客将介绍Graph cut、Grab cut等算法是如何利用最小割来实现图像分割的。]]></content>
<categories>
<category>图像处理</category>
</categories>
<tags>
<tag>图像处理</tag>
<tag>最小割</tag>
<tag>最大流</tag>
<tag>图像分割</tag>
</tags>
</entry>
<entry>
<title><![CDATA[奥卡姆剃刀和没有免费的午餐定理]]></title>
<url>%2F%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0%2Foccam-razor-NFL%2F</url>
<content type="text"><![CDATA[本文原载于https://imlogm.github.io,转载请注明出处~ 摘要:“奥卡姆剃刀”和“没有免费的午餐”是机器学习中两个很基本的原则和定理。由于名字有点怪,所以初学者可能在理解上陷入误区。本文试图用简洁易懂的方式解释这两个原则和定理,并告诉大家它们的名字是怎么来的。 关键字:机器学习, 奥卡姆剃刀, 没有免费的午餐 “奥卡姆剃刀”和“没有免费的午餐”是机器学习中两个很基本的原则和定理,很多书都会提到它们来提升逼格。不过,烦就烦在它们的名字取的有些不好理解,初学者望文生义就容易错误理解。其实,了解了它们名字的由来,这两个原则和定理是很容易想明白的,也不用去纠结如何证明它们,因为它们更接近哲学思想,而不是你会在实际项目中用到的公式。 1. 奥卡姆剃刀(Occam’s razor)“奥卡姆剃刀”其实并不是机器学习领域产生的定理,事实上,它是哲学领域的一个思想。这个思想说起来也很简单,7个字,“简单的是最好的”。 相信大家不难理解这个哲学思想,比如在数学领域,大多数数学家认为“好的公式应当是简洁明了的”,就是“奥卡姆剃刀”的体现。 这个思想我能理解,但为什么叫这么奇怪的名字?容易想到,“奥卡姆”是提出这个思想的人的名字;至于为什么叫“剃刀”是因为这个思想的提出,对封建旧思想来说是把锋利的剃刀,狠狠地剃除教会的旧思想。(具体是如何剃除旧思想的就不展开了) 那这个思想是怎么应用在机器学习领域的呢?用下面的图1就可以概括: 图1 了解机器学习的同学不难看出,图1表示的是过拟合问题,不了解的同学也不必担心,可以把图1想象成用曲线拟合几个点。 那么问题来了,图1中,哪种拟合方式是比较好的呢?相信大多数人都会选择左小图的拟合方式。机器学习领域也通常认为左小图是比较好的,原因就是“奥卡姆剃刀”的思想,“简单的是最好的”。 有些敢于挑战权威的同学可能会反驳,“怎么证明图1左小图是更简单的呢?我可以认为右小图更简单”。是的,这个问题周志华的西瓜书中也有提到,其实是没有办法说明哪种更简单。这也是哲学问题的通病,难以联系到实际中,往往会有多种解读。 不过我们不用去纠结怎样才算“简单”,只要明白这个词是什么意思就可以了。 可能还会有同学反驳,“我同意左小图是简单的,但万一实际情况中右小图才是更符合结果的拟合方式呢?”。这个想法也是对的,我们无法证明实际情况一定是左小图的拟合方式最好。这也就是下面“没有免费的午餐”定理要说明的。 2. “没有免费的午餐”定理(no free lunch, NFL)这个定理的名字乍一看很唬人,也有很多初学者因为这个名字陷入了误区。我们可以先把名字放在一边,先看定理的内容。 这个定理证明起来很复杂,一长串的数学公式,但说明白其实只要一句话,“没有一种机器学习算法是适用于所有情况的”。 这也符合我们的直觉。举个例子吧,比如图1,假设图1的左小图是机器算法A给出的拟合曲线,图1的右小图是机器算法B给出的拟合曲线。我们就一定能说机器算法A比机器算法B更好吗?或者说左小图的拟合曲线一定比右小图更符合实际情况吗?都不能。“没有免费的午餐”定理证明了对于所有机器学习问题,机器算法A更好与机器算法B更好的概率是一样的。更一般地说,对于所有机器学习问题,任何一种算法(包括瞎猜)的期望效果都是一样的。 那我们还学个啥?既然任何算法的期望效果和瞎猜一样,我们为什么还要学? 注意,这个定理有个前提:“对于所有机器学习问题,且所有问题同等重要”。而我们实际情况不是这样,我们在实际中往往更关心的是一个特定的机器学习问题,对于特定的问题,特定的机器学习算法效果自然比瞎猜更好。还是图1的例子,虽然“没有免费的午餐”定理告诉我们:我们不能预计到底是左小图拟合更好还是右小图拟合更好,但聪明的你一定能想到:是好是坏,代入到具体问题中检验一下不就知道了。 这个定理本质上就是告诉我们不要奢望能找到一种算法对所有问题都适用。这么说来,这个定理其实有点废话,因为我们面对的总是一个特定的问题,而不是所有问题。 但是这个定理其实揭示了一个哲学思想,“有得必有失”,某一个机器学习算法在某个领域好用,在另外一个领域就有可能不好用,瞎猜在一些情况下不好用,但在某个特定的问题上会很好用。就像能量守恒定理,这里的能量增加,另外一边的能量就会减少。天上掉馅饼被你捡到了,这个时刻你很幸运,但是之后你就会倒霉。 理解了上面一段话,也就明白了这个定理为什么取这么奇怪的名字。]]></content>
<categories>
<category>机器学习</category>
</categories>
<tags>
<tag>机器学习</tag>
<tag>奥卡姆剃刀</tag>
<tag>没有免费的午餐</tag>
</tags>
</entry>
<entry>
<title><![CDATA[聊一聊ResNet系列(ResNet、ResNeXt)]]></title>
<url>%2F%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0%2Fresnet%2F</url>
<content type="text"><![CDATA[本文原载于https://imlogm.github.io,转载请注明出处~ 摘要:终于到了介绍ResNet系列的时候了。ResNet真的很好用,特别时shortcut的想法真的厉害。最近,又推出了改进版本ResNeXt,新版本结合了Inception多支路的思想。 关键字:深度学习, ResNet, ResNeXt 0. 前言说到残差网络ResNet,大家应该都不默认。近几年,深度学习领域的论文井喷,注重benchmark的文章不断强调自己把精度提高了多少多少点,注重算法思想的文章则宣称自己的算法多么多么有效。 但是这些文章中,鱼龙混杂,或者夸大的成分太多,我们该如何判断?工业界给了我们答案。如果一篇文章提到的思想能在工业被广泛使用的话,那一定是篇好文章。ResNet就是其中之一。 ResNet由微软亚研院MSRA的几位大佬所写,一发表,就引起巨大关注。就我个人的看法,ResNet真的是神作,在当时大家对一层一层堆叠的网络形成思维惯性的时候,shortcut的思想真的是跨越性的。 你能想到100+层的网络,运算量却和16层的VGG差不多,精度提高一个档次。而且模块性、可移植性很强。默默膜拜下大神。 1. ResNet网络上介绍ResNet的文章很多,比如:你必须要知道CNN模型:ResNet-小白将的文章 就我个人的看法,ResNet的核心就是shortcut的出现。图1是ResNet的基本模块。 图1 图1如果去掉旁路,那就是经典的堆叠式的网络。ResNet的贡献就是引入了旁路(shortcut)。旁路(shortcut)的引入一方面使得梯度的后向传播更加容易,使更深的网络得以有效训练;另一方面,也使得每个基本模块的学习任务从学习一个绝对量变为学习相对上一个基本模块的偏移量。 ResNet的网络结构很简单,就是把这些基本模块堆起来,形成不同层数的网络。基本模块的简单堆叠也使得ResNet可以很轻松地移植到其他网络中。图2是论文里提到的几种结构,它们都是不同数量的基本模块叠加得到的。 图2 还有一点令人惊讶的是,ResNet100+层的网络,运算量却和16层的VGG差不多,关于这点,可以看我之前的文章:以VGG为例,分析深度网络的计算量和参数量 默默放上一段用Keras写的ResNet,如果熟悉Keras的话,相信通过代码能很快理解ResNet的结构。当然,现在ResNet已经被Keras内置,只需要一句代码就能写出ResNet。 代码里说的conv_block和identity_block其实就是ResNet的基本模块,它们的区别是conv_block的旁路是直接一条线,identity_block的旁路有一个卷积层。之所以有的基本模块旁路一条线,有的基础模块旁路会有卷积层,是为了保证旁路出来的featuremap和主路的featuremap尺寸一致,这样它们才能相加。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107############################################################# Resnet Graph############################################################# Code adopted from:# https://github.com/fchollet/deep-learning-models/blob/master/resnet50.pydef resnet_graph(input_image, architecture, stage5=False): assert architecture in ["resnet50", "resnet101"] # Stage 1 x = KL.ZeroPadding2D((3, 3))(input_image) x = KL.Conv2D(64, (7, 7), strides=(2, 2), name='conv1', use_bias=True)(x) x = BatchNorm(axis=3, name='bn_conv1')(x) x = KL.Activation('relu')(x) C1 = x = KL.MaxPooling2D((3, 3), strides=(2, 2), padding="same")(x) # Stage 2 x = conv_block(x, 3, [64, 64, 256], stage=2, block='a', strides=(1, 1)) x = identity_block(x, 3, [64, 64, 256], stage=2, block='b') C2 = x = identity_block(x, 3, [64, 64, 256], stage=2, block='c') # Stage 3 x = conv_block(x, 3, [128, 128, 512], stage=3, block='a') x = identity_block(x, 3, [128, 128, 512], stage=3, block='b') x = identity_block(x, 3, [128, 128, 512], stage=3, block='c') C3 = x = identity_block(x, 3, [128, 128, 512], stage=3, block='d') # Stage 4 x = conv_block(x, 3, [256, 256, 1024], stage=4, block='a') block_count = {"resnet50": 5, "resnet101": 22}[architecture] for i in range(block_count): x = identity_block(x, 3, [256, 256, 1024], stage=4, block=chr(98 + i)) C4 = x # Stage 5 if stage5: x = conv_block(x, 3, [512, 512, 2048], stage=5, block='a') x = identity_block(x, 3, [512, 512, 2048], stage=5, block='b') C5 = x = identity_block(x, 3, [512, 512, 2048], stage=5, block='c') else: C5 = None return [C1, C2, C3, C4, C5]def identity_block(input_tensor, kernel_size, filters, stage, block, use_bias=True): """The identity_block is the block that has no conv layer at shortcut # Arguments input_tensor: input tensor kernel_size: defualt 3, the kernel size of middle conv layer at main path filters: list of integers, the nb_filters of 3 conv layer at main path stage: integer, current stage label, used for generating layer names block: 'a','b'..., current block label, used for generating layer names """ nb_filter1, nb_filter2, nb_filter3 = filters conv_name_base = 'res' + str(stage) + block + '_branch' bn_name_base = 'bn' + str(stage) + block + '_branch' x = KL.Conv2D(nb_filter1, (1, 1), name=conv_name_base + '2a', use_bias=use_bias)(input_tensor) x = BatchNorm(axis=3, name=bn_name_base + '2a')(x) x = KL.Activation('relu')(x) x = KL.Conv2D(nb_filter2, (kernel_size, kernel_size), padding='same', name=conv_name_base + '2b', use_bias=use_bias)(x) x = BatchNorm(axis=3, name=bn_name_base + '2b')(x) x = KL.Activation('relu')(x) x = KL.Conv2D(nb_filter3, (1, 1), name=conv_name_base + '2c', use_bias=use_bias)(x) x = BatchNorm(axis=3, name=bn_name_base + '2c')(x) x = KL.Add()([x, input_tensor]) x = KL.Activation('relu', name='res' + str(stage) + block + '_out')(x) return xdef conv_block(input_tensor, kernel_size, filters, stage, block, strides=(2, 2), use_bias=True): """conv_block is the block that has a conv layer at shortcut # Arguments input_tensor: input tensor kernel_size: defualt 3, the kernel size of middle conv layer at main path filters: list of integers, the nb_filters of 3 conv layer at main path stage: integer, current stage label, used for generating layer names block: 'a','b'..., current block label, used for generating layer names Note that from stage 3, the first conv layer at main path is with subsample=(2,2) And the shortcut should have subsample=(2,2) as well """ nb_filter1, nb_filter2, nb_filter3 = filters conv_name_base = 'res' + str(stage) + block + '_branch' bn_name_base = 'bn' + str(stage) + block + '_branch' x = KL.Conv2D(nb_filter1, (1, 1), strides=strides, name=conv_name_base + '2a', use_bias=use_bias)(input_tensor) x = BatchNorm(axis=3, name=bn_name_base + '2a')(x) x = KL.Activation('relu')(x) x = KL.Conv2D(nb_filter2, (kernel_size, kernel_size), padding='same', name=conv_name_base + '2b', use_bias=use_bias)(x) x = BatchNorm(axis=3, name=bn_name_base + '2b')(x) x = KL.Activation('relu')(x) x = KL.Conv2D(nb_filter3, (1, 1), name=conv_name_base + '2c', use_bias=use_bias)(x) x = BatchNorm(axis=3, name=bn_name_base + '2c')(x) shortcut = KL.Conv2D(nb_filter3, (1, 1), strides=strides, name=conv_name_base + '1', use_bias=use_bias)(input_tensor) shortcut = BatchNorm(axis=3, name=bn_name_base + '1')(shortcut) x = KL.Add()([x, shortcut]) x = KL.Activation('relu', name='res' + str(stage) + block + '_out')(x) return x 2. ResNeXtResNeXt是这个系列的新文章,是ResNet的升级版,升级内容为引入Inception的多支路的思想。 同样,网络上也有非常好的解读的文章:深度学习——分类之ResNeXt-范星.xfanplus的文章 如果你对ResNet和Inception都比较熟悉的话,那么其实只要看图3就能明白ResNeXt在做什么了。 图3 图3中左图是ResNet的基本模块,右图是ResNeXt的基本模块。容易发现,ResNeXt就是把ResNet的单个卷积改成了多支路的卷积。 作者说ResNeXt的目的是为了保持ResNet的高可移植性优点,同时继续提高精度。文章中还顺便吐槽了Inception的缺点。也就是我在上一篇介绍Inception的文章(聊一聊Inception系列)中说的:Inception系列的前两篇文章很惊艳,但是后两篇文章炼丹痕迹越来越重。直到Xception出现后,可移植性增强不少。]]></content>
<categories>
<category>深度学习</category>
</categories>
<tags>
<tag>深度学习</tag>
<tag>ResNet</tag>
<tag>ResNeXt</tag>
</tags>
</entry>
<entry>
<title><![CDATA[多种子的区域生长算法]]></title>
<url>%2F%E5%9B%BE%E5%83%8F%E5%A4%84%E7%90%86%2Fmulti-seed-region-grow%2F</url>
<content type="text"><![CDATA[本文原载于https://imlogm.github.io,转载请注明出处~ 摘要:多种子的区域生长算法,基于C++编写。 关键字:图像处理, 种子生长, 区域生长 1. 题外话最近需要找一种简单对图像进行分割的算法,作为后续算法的前处理阶段。最开始想到的是聚类,但是聚类会有分割后不保证连通性的问题。 区域生长法可以保证分割后各自区域的连通性。但网上大多数的代码都是单个种子的,用的多是matlab或旧版本的opencv。索性,我照着单种子的思路,写了一个多种子的区域生长算法分享出来。 2. 单种子的区域生长单种子的区域生长可以看这篇文章:图像处理算法1——区域生长法-夏天的风 事实上,我就是参照这篇文章的思路写的代码。这篇文章的代码思路清晰,可惜用的旧版本的opencv,而且是单种子。 3. 多种子的区域生长大体思路是这样的: 1、遍历全图,寻找是否还有undetermined的点,如有,作为种子 2、进行单种子的区域生长算法,生长出的区域记入矩阵mask 3、如果mask记录的区域面积足够大,把mask中的区域记录到矩阵dest中,状态为determined;如果mask记录的区域面积太小,把mask中的区域记录到矩阵dest中,状态为ignored(忽略分割出来面积过小的区域) 4、重复上述步骤,直到全图不存在undetermined的点 4. TODO我对现在的算法还不是很满意,其一,我没有做太多优化,代码运行卡卡的;其二,我用的灰度图,以及“差值/阈值”的方式来判断是否相似,阈值不是自适应的。 注:新版本添加了自动寻找阈值的demo,详见本文最后一节 对于图片尺寸越大越卡这个问题,可以在读图片的时候统一resize成256×256,大多数追求速度的图像处理算法都会有这步。 对于彩色图,用3个通道的距离来判断相似度会更好。 以上两个问题,我为了保持算法的原汁原味,就没有添加。(其实是我懒得写了~) 注:关于彩色图那点,最新版本已经添加了相关代码。思路是RGB三个通道的差值取平方和。Delta = R^2 + G^2 + B^2 。 5. 代码详见Github:https://github.com/imLogM/multi_seed_region_grow 6. 效果 原图 结果 7. 关于阈值自适应新版本的代码中,已经添加了阈值自适应的demo。想法来自于二值化领域的OTSU(大津法),不懂大津法的可以搜索下,虽然属于不同领域,但算法思想可以借鉴。我在demo中的具体做法如下: 1、threshold的取值在一定范围内,遍历所有可取的threshold 2、对于一个取到的threshold,区域生长算法可以得到分割后的N个区域。计算同一区域内的像素值的方差var(same_region)以及不同区域间像素平均值的差值delta(mean(different_region)) 3、用loss表征分割的好坏,公式中a和b是手动设置的参数,loss越小说明分割得越好:$$loss=a \div delta(mean(different-region)) + b \times Var(same-region)$$ 4、遍历所有可取的threshold,求出各自的loss,取loss最小值对应的threshold 原图 结果]]></content>
<categories>
<category>图像处理</category>
</categories>
<tags>
<tag>图像处理</tag>
<tag>种子生长</tag>
<tag>区域生长</tag>
</tags>
</entry>
<entry>
<title><![CDATA[C/C++常见问题(持续更新)]]></title>
<url>%2FC-C%2Fc-problem-1%2F</url>
<content type="text"><![CDATA[本文原载于https://imlogm.github.io,转载请注明出处~ 摘要:记录在使用C/C++时遇到的常见问题:友元,计时,cout与printf,结构体内存对齐,虚函数,派生类,auto关键字,指针的sizeof,短路求值,qsort与sort,类的拷贝构造&&等号赋值&&深拷贝&&浅拷贝,输入函数cin 关键字:C/C++ 1. 友元关于友元函数,可以看这篇文章:【C++基础之十】友元函数和友元类-偶尔e网事 文章里说的友元的缺点是破坏封装性比较好理解,友元的优点是提高程序运行效率这点我没有太想明白。(为什么我感觉友元的优点是让程序员能少写一点代码?) 文章最后提到友元不具有继承性和传递性,记一下就好。实际编程时,我还没有遇到使用友元的情况。 2. 计时在调试程序的时候,我们经常需要记录某一段代码运行的时间。我一般是使用clock()来计时的。示例代码如下:1234567891011#include <ctime> // 必须添加的头文件,C++是ctime,C是time.hint main() { clock_t start = clock(); // clock_t 其实是long int for (int i=0; i<1000000; i++); // 需要计时的程序段 clock_t end = clock(); double time_used = 1.0 * (end - start) / CLOCKS_PER_SEC; printf("程序用时: %.5f秒", time_used); return 0;} 如果有过硬件开发经验的同学应该能明白,函数clock()获取的是滴答计数器的值。可以形象地理解:计算机的底层(或硬件或软件模拟)能不断地发出滴答声,并且滴答声的频率是稳定的,每秒发出CLOCKS_PER_SEC次滴答声。程序一开始运行,就有一个计数器启动,不断记录它听到的滴答声的次数。clock()获取到的就是这个计数器的值。clock() / CLOCKS_PER_SEC得到的就是从程序开始运行到当前经过几秒。 在我的电脑上,CLOCKS_PER_SEC=1000000,类型clock_t是long int的别名,在不同的硬件上,这些值不一定相同。 注意:这种计时方式有最大可计时时间。我们可以算一下,long int型最大为$2^{31}$,$2^{31} \div 1000000=2147$,2147秒大约是35分钟。也就是说程序运行后,每过大约35分钟,clock()取到的值会再次回到0。网上也有教程说最大计时时间为72分钟,可能是当成了unsigned long在算了。 3. cout与printfcout比printf要方便一点,因为可以不用写格式控制字符串%d、%s等。重载运算符后也能让cout输出非基本类型的变量。 但是printf比cout在IO效率上要更高一些,当然也有方法可以弥补这点,可以看这篇文章:cin与scanf cout与printf效率问题 4. 结构体内存对齐这个涉及到比较底层的问题,用来解释sizeof(结构体)得到的结果为什么和直观想的不一样。可以看这篇文章:如何理解 struct 的内存对齐?-Courtier的回答 注意,形似下方代码的东西不是普通结构体,是位域,它的内存占用分析更复杂,这里就不展开了: 123456struct bs // 位域{ int a:8; // 8表示占用8个bit int b:2; int c:6;}; 5. 虚函数定义一个函数为虚函数,不代表函数为不被实现的函数。定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。 定义一个函数为纯虚函数,才代表函数没有被实现。纯虚函数定义如下:1virtual void fun()=0; 6. 关于派生类 公有继承的公有成员还是公有的,可以被访问 公有继承的私有成员不被继承,所以不能访问 公有继承的保护成员可以被类的方法访问,不能被对象访问 私有继承的公有成员会变成派生类的私有成员,也不能被访问 7. auto关键字auto关键字在C++11以后有了不同于以前的含义。以前,auto关键字用来表示该变量是具有自动存储期的局部变量。如下面代码所示: 1234// C++11标准以前,auto关键字的用法int a; // 平时,auto关键字可省略auto int a; // auto表示有自动存储期的局部变量,与上句效果相同static int a; // 静态变量没有自动存储器 C++11以后,auto表示该变量的类型由编译器推理。如下面代码所示: 123// C++11标准以后int a; auto b = a; // b没有类型,但编译器会自动推理出b的类型为int 新标准下的auto主要在定义STL的迭代器时用到,因为这些迭代器类型名字都太长了(比如std::vector<std::string>::iterator),使用auto关键字可以少打很多字。 8. 指针的sizeof这其实是一个值得注意的地方,因为老教材通常默认是32位机器。32位程序,sizeof(指针)=4;64位程序,sizeof(指针)=8 12345678#include <iostream>int main() { int a = 0; int* p = &a; std::cout << sizeof(p) << std::endl; return 0;} 9. 短路求值在用到&&和||时会有短路求值的情况出现。比如下面的代码: 1234567// && 的短路求值示例// 先判断isClothes(a),若为false,则不再判断isRed(a)if (isClothes(a) && isRed(a)) { printf("a为红色衣服");} else { printf("a不为红色衣服"); } 在代码执行的效率上说,短路求值能避免一些无意义的计算,但是短路求值也会有坑。 假如函数isRed(a)会对全局变量进行操作,或者有指针操作,就会出现问题,比如下面的代码: 12345678910111213141516171819202122232425int red_count = 0; // 全局变量,记录红色物品的数量int main() { // && 的短路求值示例 // 先判断isClothes(a),若为false,则不再判断isRed(a) if (isClothes(a) && isRed(a)) { printf("a为红色衣服"); } else { printf("a不为红色衣服"); } printf("\nred_count的值:%d", red_count); return 0;}// isRed函数的实现bool isRed(object a) { if (a是红色) { ++red_count; return true; } else { return false; }} 应该不难看出上述代码的问题出在red_count,由于短路求值,isRed(a)仅当isClothes(a)为true时执行,也就是说red_count只记录了红色衣服的数量,而我们本来是希望记录所有红色物体的数量的。 有可能会有同学说,上面的代码逻辑不规范,我自己写肯定不会这样统计红色物体数量。是的,一般都不会这么写代码,我这边这样写是为了说明短路求值会带来的问题。实际情况中,往往是涉及指针操作时会出现。 一些“简洁精妙”的代码,以“行数少”著称。它们&&和||两边的函数往往是精心设计,不能调换次序的。我们平时写代码的时候,没必要刻意追求“行数少”。如果“行数少”,但复杂度还和原来一样,那么这样的“行数少”有什么意义呢?反而造成阅读代码的困难。我们真正要追求的是复杂度的降低,和代码的易阅读性。 10. qsort与sortqsort与sort都是C/C++中的排序函数,其中qsort属于库stdlib.h,sort属于库algorithm,它们都只能对连续内存的数据进行排序(比如数组等),对不连续内存的数据不能排序(比如链表)。 有关这两个函数的用法,请看:qsort函数、sort函数-JokerSmithWang的博客 11. 类的拷贝构造&&等号赋值&&深拷贝&&浅拷贝这块内容需要一定的时间消化,参见:C++ 拷贝构造函数和赋值运算符-Brook的博客 12. 输入函数cin平时读取字符串一直用的是std::cin >> str读入输入的,但是这种方式如果字符串里有空格,那么会把空格视为分隔符,把原字符串分成多个字符串。如果要实现读取带空格的字符串,可以使用std::cin.getline(),具体使用方式可以参见:C++输入cin,cin.get(),cin.getline()详细比较及例子-Mac Jiang的博客]]></content>
<categories>
<category>C/C++</category>
</categories>
<tags>
<tag>C/C++</tag>
</tags>
</entry>
<entry>
<title><![CDATA[聊一聊Inception系列(GoogLeNet、Inception、Xception)]]></title>
<url>%2F%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0%2Finception%2F</url>
<content type="text"><![CDATA[本文原载于https://imlogm.github.io,转载请注明出处~ 摘要:最近使用Xception时发现效果很好,所以打算介绍下整个Inception系列。 关键字:深度学习, Inception, Xception, GoogLeNet 1. 从模型结构说起其实关于Inception的结构,以及各代的改进,大家可以看这篇文章:深入浅出——网络模型中Inception的作用与结构全解析-深度学习思考者 有同学可能要说,上面链接里的那篇文章这么简单,似乎没讲太多内容。其实因为Inception每代之间联系性比较强,所以看明白了其中一篇,其他的也都能很快懂。如果要我来讲的话,大概会是下面四行字: GoogLeNet(Inception-v1):相比AlexNet和VGG,出现了多支路,引入了1×1卷积帮助减少网络计算量 Inception-v2:引入Batch Normalization(BN);5×5卷积使用两个3×3卷积代替 Inception-v3:n×n卷积分割为1×n和n×1两个卷积 Inception-v4:进一步优化,引入ResNet的shortcut思想 个人体会:v1和v2中的改进对深度学习的发展具有非常大的意义;v3有一点创新,但开始出现“炼丹”的感觉;v4参考ResNet说明ResNet的思想确实牛逼,其他部分完全在“炼丹”,网络过于精细,不容易迁移到其他任务中 推荐大家在训练模型时尝试使用Inception-v2中出现的Batch Normalization(BN),关于BN的原理,可以看我上一篇文章:Batch Normalization(BN) 2. Xception之所以把Xception单拎出来说,一是因为Xception比较新,上面那篇文章没有讲到;二是因为Xception设计的目的与Inception不同:Inception的目标是针对分类任务追求最高的精度,以至于后面几代开始“炼丹”,模型过于精细;Xception的目标是设计出易迁移、计算量小、能适应不同任务,且精度较高的模型。 那么Xception与Inception-v3在结构上有什么差别呢? 如图1为Inception-v3的模块结构,依据化繁为简的思想,把模块结构改造为图2。 图1 图2 依据depthwise separable convolution的思想,可以进一步把图2改造到图3。 图3 什么是depthwise separable convolution呢?MobileNet等网络为了减少计算量都有用到这个方法,不过Xception在这里的用法和一般的depthwise separable convolution还有点不同,所以为了防止大家搞糊涂,我就不介绍一般性的用法了,直接介绍Xception中的用法。 对于112×112×64的输入做一次普通的3×3卷积,每个卷积核大小为3×3×64,也就是说,每一小步的卷积操作是同时在面上(3×3的面)和深度上(×64的深度)进行的。 那如果把面上和深度上的卷积分离开来呢?这就是图3所要表达的操作。依旧以112×112×64的输入来作例子,先进入1×1卷积,每个卷积核大小为1×1×64,有没有发现,这样每一小步卷积其实相当于只在深度上(×64的深度)进行。 然后,假设1×1卷积的输出为112×112×7,我们把它分为7份,即每份是112×112×1,每份后面单独接一个3×3的卷积(如图3所示,画了7个3×3的框),此时每个卷积核为3×3×1,有没有发现,这样每一小步卷积其实相当于只在面上(3×3的面)进行。 最后,把这7个3×3的卷积的输出叠在一起就可以了。根据Xception论文的实验结果,Xception在精度上略低于Inception-v3,但在计算量和迁移性上都好于Inception-v3。 3. 关于模型复杂度的计算其实这部分内容我本来打算重点写的,为此我还特地写了篇“以VGG为例,分析模型复杂度”的文章。(+﹏+)~ 可是,就在写那篇文章的时候,我发现了一篇对Inception的复杂度计算介绍得非常清晰的文章。那我就偷个懒,大家可以直接看这篇文章的后半部分:卷积神经网络的复杂度分析-Michael Yuan的文章]]></content>
<categories>
<category>深度学习</category>
</categories>
<tags>
<tag>深度学习</tag>
<tag>Inception</tag>
<tag>Xception</tag>
<tag>GoogLeNet</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Batch Normalization (BN)]]></title>
<url>%2F%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0%2Fbatch-normalization%2F</url>
<content type="text"><![CDATA[本文原载于https://imlogm.github.io,转载请注明出处~ 摘要:BN最早出现在Google的Inception(v2)网络中,它的效果让人瞠目结舌。那么,BN的原理是什么?为什么会有这么好的效果? 关键字:深度学习, BN, 优化 1. 从数据预处理谈起如果你有相关的机器学习经验的话,应该知道在进行模型训练前需要进行数据预处理。而数据预处理中非常重要的一步是“归一化”(Normalization)。 为什么需要归一化呢?其一,因为机器学习的训练本质上是让模型学习到数据的分布。如果训练集的分布与测试集的分布有很大不同,那么训练出来的模型泛化能力就差,人们自然希望先把训练集和测试集分布调整到相近再训练;其二,实践表明,一个好的数据分布,有利于加速模型的训练及减少不收敛出现的概率,人们自然希望把数据调整到“好的分布”再训练。 怎样的分布算是好的分布?如图1左小图,数据分布的中心不在原点,数据分布的x方向明显长于y方向,这不是一个好的分布;如图1右小图,数据分布的中心在原点,数据分布的各个方向长度都接近1,这是一个好的分布。当然,并不是说图1的分布不能用于训练,只要训练集和测试集的分布相同,无论怎样差的分布,理论上都能训练出具有优秀泛化能力的模型;只是分布越差,训练起来越困难。 图1 左中右分别代表:原图、PCA后、白化后的数据分布 在数据预处理中归一化常用的方法是主成分分析( PCA )与 白化( whitening )。它们的作用,就是把数据变到“好的分布”下。如果不了解这两项技术,可以看这篇文章:主成分分析( PCA )与 白化( whitening )-Pony_s 2. 预处理还不够也许数据预处理在机器学习领域还够用,但是到了深度学习领域,就显得力不从心了:因为深度网络很深,每一层过后,数据的分布都会产生一定的变化;浅层的微小变化经过一层层放大,到了深层就是很大的变化了。 论文把这样现象叫做“Internal Covariate Shift”。 如果能有一种方法,能够在每一层的输出上进行数据归一化就好了。Batch Normalization正是要解决这个问题。 3. Batch Normalization公式:$$\widehat{x}^{(k)}=\frac{x^{(k)}-E[x^{(k)}]}{\sqrt{Var[x^{(k)}]}}$$ 如何理解这个公式?结合图1,先看分子部分,$E[x^{(k)}]$表示的是$x^{(k)}$的期望,也就是平均值。$x^{(k)}-E[x^{(k)}]$,每个数据减去整体平均值,那么得到数据关于原点对称,这步的作用就是使数据分布关于原点对称。 再看分母部分,$Var[x^{(k)}]$表示求数据的方差,$\sqrt{Var[x^{(k)}]}$表示数据的标准差。分子除分母之后,数据的分布在各个方向上的标准差都为1,所以这步的作用是使数据分布在各个方向的标准差都为1。 4. 没有那么简单事实情况还没有那么简单。要知道并不是无脑在每一层的输出都加上上面的公式就能得到好的结果。万一某一些层就是要特殊的数据分布怎么办?我们不如把这个问题交给深度网络自身。 $$y^{(k)}=\gamma ^{(k)}\widehat{x}^{(k)}+\beta ^{(k)}$$ 公式中$\gamma ^{(k)}$和$\beta ^{(k)}$都是通过网络学习得到的。不难想到,当:$$\gamma ^{(k)}=\sqrt{Var[x^{(k)}]}\space ,\space \space \beta ^{(k)}=E[x^{(k)}]$$ 相当于没有加入归一化。 关于Batch Normalization的进一步展开,可以看这篇文章:Batch Normalization 学习笔记-hjimce的专栏 5. BN加在什么地方论文给出的位置是加在激活函数前,也就是“conv -> bn -> relu”,残差网络ResNet的代码也用的这种方式。当然,我也看到过加在激活函数后的,也就是“conv -> relu -> bn”。 你也可以看下这个回答:CNN中batch normalization应该放在什么位置?-lystdo的回答 6. BN是万能的?首先,提醒初学者一个容易掉进的坑: 如果BN的期望和标准差是每个mini-batch各自计算得出的,那么batch_size不要设太小,以免每个mini-batch得到的期望和标准差波动太大。 其次,不得不说,BN的效果确实显著:训练速度加快了,收敛精度也提高了。但是还是有一些情况不能使用BN:NTIRE2017夺冠的EDSR去掉了Batch Normalization层就获得了提高为什么?-pby5的回答]]></content>
<categories>
<category>深度学习</category>
</categories>
<tags>
<tag>深度学习</tag>
<tag>BN</tag>
<tag>优化</tag>
</tags>
</entry>
<entry>
<title><![CDATA[以VGG为例,分析深度网络的计算量和参数量]]></title>
<url>%2F%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0%2Fvgg-complexity%2F</url>
<content type="text"><![CDATA[本文原载于https://imlogm.github.io,转载请注明出处~ 摘要:我第一次读到ResNet时,完全不敢相信152层的残差网络,竟然在时间复杂度(计算量)上和16层的VGG是一样大的。当然,对于初学者而言,直接分析ResNet的时间复杂度是有点难度的。这篇文章我将以VGG为例,介绍深度网络的复杂度计算方法。掌握这些计算方法后,再去看Inception、ResNet、MobileNet、SqueezeNet等论文,你就能明白这些网络结构的精妙之处。 关键字:深度网络, VGG, 复杂度分析, 计算量, 参数量 1. VGG的结构VGG的结构如下图所示: 图1 不同VGG网络的结构 我们选取其中的VGG-16(上图中的D列)来进行计算量和参数量的分析。VGG-16每个卷积操作后,图像大小变化情况如下图所示: 图2 VGG-16的结构 2. 卷积操作的计算量和参数量对于卷积操作的计算量(时间复杂度)和参数量(空间复杂度)可以看这篇文章:卷积神经网络的复杂度分析-Michael Yuan的文章 注意,这些复杂度计算都是估算,并非精确值。 我们以VGG-16的第一层卷积为例:输入图像224×224×3,输出224×224×64,卷积核大小3×3。 计算量:$$ Times\approx 224\times 224\times 3\times 3\times 3\times 64=8.7\times 10^7$$ 参数量:$$ Space\approx 3\times 3\times 3\times 64=1728$$ 再举一个例子,VGG-16的最后一个卷积层:输入14×14×512,输出14×14×512,卷积核大小3×3。 计算量:$$ Times\approx 14\times 14\times 3\times 3\times 512\times 512=4.6\times 10^8$$ 参数量:$$ Space\approx 3\times 3\times 512\times 512=2.4\times 10^6$$ 3. 全连接层的计算量和参数量考虑VGG-16的最后一个全连接层:上层神经元数为4096,下层神经元数为1000。这样的全连接层复杂度应该如何计算? 其实全连接层可以视为一种特殊的卷积层:上层为1×1×4096,下层为1×1×1000,使用的1×1的卷积核进行卷积。 那么,计算量:$$ Times\approx 1\times 1\times 1\times 1\times 4096\times 1000=4\times 10^6$$ 参数量:$$ Space\approx 1\times 1\times 4096\times 1000=4\times 10^6$$ 4. VGG-16复杂度分析从上述计算中,相信大家对深度网络的复杂度已经有了一些体会,比如VGG-16中: 1、卷积层的时间复杂度大致是同一数量级的 2、随着网络深度加深,卷积层的空间复杂度快速上升(每层的空间复杂度是上层的两倍) 3、全连接层的空间复杂度比卷积层的最后一层还大 当然,深度网络的复杂度是和网络结构紧密相关的,上述3个结论仅对VGG这种网络结构有效。]]></content>
<categories>
<category>深度学习</category>
</categories>
<tags>
<tag>深度网络</tag>
<tag>VGG</tag>
<tag>复杂度分析</tag>
<tag>计算量</tag>
<tag>参数量</tag>
</tags>
</entry>
<entry>
<title><![CDATA[二维图像的傅立叶变换]]></title>
<url>%2F%E5%9B%BE%E5%83%8F%E5%A4%84%E7%90%86%2Fimage-fft%2F</url>
<content type="text"><![CDATA[本文原载于https://imlogm.github.io,转载请注明出处~ 摘要:二维图像的傅立叶变换,与一维傅立叶相比,在理解上要抽象很多。我在网上找了几篇相对较好的文章,并用matlab自己做了几个实验图像,希望能对大家理解二维图像的傅立叶变换有所帮助。 关键字:二维傅立叶变换,图像处理 1. 一维傅立叶变换如果是理工科的话,一维傅立叶变换应该在大学里都学过。如果有所遗忘的话,可以看这篇比较易懂又不失数学性的文章“如何理解傅里叶变换公式?-马同学的回答”。 一维傅立叶变换的公式为:$$ F(\omega)=\int_{-\infty}^{+\infty}f(t)e^{-i\omega t}dt $$ 2. 二维傅立叶变换二维傅立叶变换的公式为:$$ F(u,v)=\int_{-\infty}^{+\infty}\int_{-\infty}^{+\infty}f(x,y)e^{-i(ux+vy)}dxdy $$由一维傅立叶的公式,能比较容易类比得到二维傅立叶变换公式。但注意,二维傅立叶不是x方向与y方向正弦余弦的简单叠加,而是乘积的叠加。 比如:一维傅立叶变换的三角函数系是$sin(nx)$、$cos(nx)$以及常数1,二维傅立叶变换的三角函数系是$sin(ux+vy)$、$cos(ux+vy)$和常数1.而$sin(ux+vy)$和$cos(ux+vy)$可以继续分解为$sin(ux)sin(vy),\:\:sin(ux)cos(vy),\:\:cos(ux)sin(vy),\:\:cos(ux)cos(vy)$这四个乘积的形式。 如果对上面一段话的内容有兴趣的话,可以参看这篇文章“二维傅里叶变换是怎么进行的?-CharlyGordon的回答”。看不懂也没关系,只需要知道二维傅立叶不是x方向与y方向正弦余弦的简单叠加。 注意1:二维傅立叶变换后生成的图像与原图上的像素点不存在一一对应关系。原图中的像素值是x,y坐标轴下的(即空间域),而傅立叶变换后的像素值是u,v坐标轴下的(即频域)。 注意2:图像的像素点是离散且有限的,故实际进行图像傅立叶变换时,使用的是离散傅立叶变换(DFT),需要把上述公式中的积分号$\int_{-\infty}^{+\infty}$换成求和号$\sum$ 3. 二维傅立叶变换的直观理解原始图像经过二维傅立叶变换后得到的是u,v坐标系下的二维矩阵,由$(u_1,v_1)$、$(u_1,v_2)$、$(u_2,v_1)$等一系列点组成。每个位置$(u_n,v_n)$都有其对应的值$F(u_n,v_n)$。如果把这个二维矩阵归一化成傅立叶后的图像来显示,那么傅立叶后图像上像素点位置和像素点亮度就表征二维矩阵相应的点和该点的值。 如图1所示,有左中右三幅小图,我们先不管右小图,左小图是原始图像,中小图是傅立叶变换后的。可以看到傅立叶变换后的图片的两个斜对角出现了两个白点,这两点处的亮度值最大,其余点处亮度值为0。这表示原始图像可以由这两点所对应的三角波组成,三角波的幅值为其对应点的亮度。 图1 那么这些三角波长什么样子呢?图2是我从网上找到的一幅图,原始出处未知。原来这张图是频移后的三角波,由于频移是下面才讲到的知识点,我感觉这样会对理解造成一定影响,所以我处理成频移前的,便于大家理解。相信对比图1和图2,你们能很快理解其中的关系。 图2 在图2的右小图中,我用红框标出了其中的一个三角波,这个三角波的外形与图1的原始图像最相似。结果也和我们预料的一样,傅立叶变换后,这个三角波的幅值是最大的,所以我们看到了图1中小图左上角的白点。 注意:图2的三角波对应于图1中小图的左上角的区域,所以我们得到了左上角的白点。至于图1中小图的右上、左下、右下区域的三角波长什么样子,请看下面一段话。 看到这里可能大家还有个疑问:左上角的白点明白了,但是右下角的白点怎么来的?如果你自己动手做实验的话,会发现这两个白点是对称的(换而言之,右下角区域的三角波和左上角区域的三角波对称)。这其实是由两个原因共同作用造成的:其一,傅立叶双边频谱关于原点对称;其二,上面提到过,二维图像傅立叶变换是离散傅立叶变换,离散傅里叶变换本质是周期信号求傅里叶级数,所以其实会有周期延拓。 因为这两个性质涉及一些更深的知识,我不详细展开了。有兴趣的同学可以找信号处理方面的书来看,学过的同学应该能马上理解。 解决了图1中左小图、中小图的问题,那么图1的右小图是什么呢?右小图其实是中小图经过频移后的。为什么要频移,因为我们把傅立叶变换得到的二维矩阵用图像的方式显示时,默认的坐标原点(0, 0)位于图像的左上角。频移要做的就是把坐标原点移动到图像的中心。 可以想到,图1右小图的中间偏右下的点是由图1左小图左上角的点经过移动后得到的,而图1右小图的中间偏左上的点是由之前提到的“傅立叶双边频谱关于原点对称”这条性质得到的。 4. Matlab小实验看了上面的内容,相信大家都已经对二维傅立叶有了一定的直观印象。我又用matlab写了个小程序,生成了几幅图片,帮助大家理解。 图3 图4 图5 图6 图7 图8 5. Matlab代码最后附上我的matlab代码,便于大家自己做实验。 如果对傅立叶变换的代码有什么疑问的话,可以看这篇文章“使用matlab对图像进行傅里叶变换-三山音”。12345678910111213141516171819202122232425262728293031% 用sin(x+y)的图像来帮助理解二维图像的傅立叶变换img_size = 100; % 图片尺寸x_step = 1; y_step = 1;image = zeros(img_size, img_size);for x = x_step:x_step:x_step*img_size for y=y_step:y_step:y_step*img_size image(x/x_step, y/y_step)=sin(4*pi*x/img_size + 4*pi*y/img_size); endendsubplot(1,3,1)imshow(image) % 原图title('原图')subplot(1,3,2)image = im2double(image);F_unshift = fft2(image); F_unshift_abs = abs(F_unshift);T = log(F_unshift_abs+1);imshow(T); % 傅立叶变换后,未频移前title('傅立叶变换后,未频移')subplot(1,3,3)F = fftshift(F_unshift);F_abs = abs(F);T = log(F_abs+1);imshow(T) % 傅立叶变换后,频移后title('傅立叶变换后,频移后')]]></content>
<categories>
<category>图像处理</category>
</categories>
<tags>
<tag>图像处理</tag>
<tag>傅立叶变换</tag>
<tag>matlab</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Hello World]]></title>
<url>%2F%E6%B5%8B%E8%AF%95%E9%A1%B5%2Fhello-world%2F</url>
<content type="text"><![CDATA[Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub. Quick StartCreate a new post1$ hexo new "My New Post" More info: Writing Run server1$ hexo server More info: Server Generate static files1$ hexo generate More info: Generating Deploy to remote sites1$ hexo deploy More info: Deployment]]></content>
<categories>
<category>测试页</category>
</categories>
<tags>
<tag>测试页</tag>
</tags>
</entry>
</search>