(11)_基于deep learning的快速图像检索系统
最后更新于:2022-04-01 14:21:58
作者:[寒小阳](http://blog.csdn.net/han_xiaoyang?viewmode=contents)
时间:2016年3月。
出处:[http://blog.csdn.net/han_xiaoyang/article/details/50856583](http://blog.csdn.net/han_xiaoyang/article/details/50856583)
声明:版权所有,转载请联系作者并注明出处
### 1.引言
本系统是基于CVPR2015的论文[《Deep Learning of Binary Hash Codes for Fast Image Retrieval》](http://www.iis.sinica.edu.tw/~kevinlin311.tw/cvprw15.pdf)实现的海量数据下的基于内容图片检索系统,250w图片下,对于给定图片,检索top 1000相似时间约为1s,其基本背景和原理会在下文提到。
### 2.基本问题与技术
大家都知道,基于内容的图像检索系统是根据图像的内容,在已有图像集中找到最『相近』的图片。而这类系统的效果(精准度和速度)和两个东西直接相关:
**图片特征的表达能力**
**近似最近邻的查找**
根据我们这个简单系统里的情况粗浅地谈谈这两个点。
首先说图像特征的表达能力,这一直是基于内容的图像检索最核心却又困难的点之一,计算机所『看到』的图片像素层面表达的低层次信息与人所理解的图像多维度高层次信息内容之间有很大的差距,因此我们需要一个尽可能丰富地表达图像层次信息的特征。我们前面的博客也提到了,deep learning是一个对于图像这种层次信息非常丰富的数据,有更好表达能力的框架,其中每一层的中间数据都能表达图像某些维度的信息,相对于传统的Hist,Sift和Gist,表达的信息可能会丰富一下,因此这里我们用deep learning产出的特征来替代传统图像特征,希望能对图像有更精准的描绘程度。
再说『近似最近邻』,ANN(Approximate Nearest Neighbor)/近似最近邻一直是一个很热的研究领域。因为在海量样本的情况下,遍历所有样本,计算距离,精确地找出最接近的Top K个样本是一个非常耗时的过程,尤其有时候样本向量的维度也相当高,因此有时候我们会牺牲掉一小部分精度,来完成在很短的时间内找到近似的top K个最近邻,也就是ANN,最常见的ANN算法包括[局部敏感度哈希/locality-sensitive hashing](https://en.wikipedia.org/wiki/Locality-sensitive_hashing),[最优节点优先/best bin first](https://en.wikipedia.org/wiki/Best_bin_first)和[Balanced box-decomposition tree](https://en.wikipedia.org/wiki/Balanced_box-decomposition_tree)等,我们系统中将采用LSH/局部敏感度哈希来完成这个过程。有一些非常专业的ANN库,比如[FLANN](http://www.cs.ubc.ca/research/flann/),有兴趣的同学可以了解一下。
### 3\. 本检索系统原理
图像检索系统和关键环节如下图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad8d60b8.jpg)
图像检索过程简单说来就是对图片数据库的每张图片抽取特征(一般形式为特征向量),存储于数据库中,对于待检索图片,抽取同样的特征向量,然后并对该向量和数据库中向量的距离,找出最接近的一些特征向量,其对应的图片即为检索结果。
基于内容的图像检索系统最大的难点在上节已经说过了,其一为大部分神经网络产出的中间层特征维度非常高,比如Krizhevsky等的在2012的ImageNet比赛中用到的[AlexNet神经网](http://papers.nips.cc/paper/4824-imagenet-classification-with-deep-convolutional-neural-networks),第7层的输出包含丰富的图像信息,但是维度高达4096维。4096维的浮点数向量与4096维的浮点数向量之间求相似度,运算量较大,因此Babenko等人在论文[Neural codes for image retrieval](http://arxiv.org/pdf/1404.1777v2.pdf)中提出用PCA对4096维的特征进行PCA降维压缩,然后用于基于内容的图像检索,此场景中效果优于大部分传统图像特征。同时因为高维度的特征之间相似度运算会消耗一定的时间,因此线性地逐个比对数据库中特征向量是显然不可取的。大部分的ANN技术都是将高维特征向量压缩到低维度空间,并且以01二值的方式表达,因为在低维空间中计算两个二值向量的汉明距离速度非常快,因此可以在一定程度上缓解时效问题。ANN的这部分hash映射是在拿到特征之外做的,本系统框架试图让卷积神经网在训练过程中学习出对应的『二值检索向量』,或者我们可以理解成对全部图先做了一个分桶操作,每次检索的时候只取本桶和临近桶的图片作比对,而不是在全域做比对,以提高检索速度。
[论文](http://www.iis.sinica.edu.tw/~kevinlin311.tw/cvprw15.pdf)是这样实现『二值检索向量』的:在Krizhevsky等2012年用于ImageNet中的卷积神经网络结构基础上,在第7层(4096个神经元)和output层之间多加了一个隐层(全连接层)。隐层的神经元激励函数,可以选用sigmoid,这样输出值在0-1之间值,可以设定阈值(比如说0.5)之后,将这一层输出变换为01二值向量作为『二值检索向量』,这样在使用卷积神经网做图像分类训练的过程中,会『学到』和结果类别最接近的01二值串,也可以理解成,我们把第7层4096维的输出特征向量,通过神经元关联压缩成一个低维度的01向量,但不同于其他的降维和二值操作,这是在一个神经网络里完成的,每对图片做一次完整的前向运算拿到类别,就产出了表征图像丰富信息的第7层output(4096维)和代表图片分桶的第8层output(神经元个数自己指定,一般都不会很多,因此维度不会很高)。引用论文中的图例解释就是如下的结构:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad8f1baa.png)
上方图为ImageNet比赛中使用的卷积神经网络;中间图为调整后,在第7层和output层之间添加隐层(假设为128个神经元)后的卷积神经网络,我们将复用ImageNet中得到最终模型的前7层权重做fine-tuning,得到第7层、8层和output层之间的权重。下方图为实际检索过程,对于所有的图片做卷积神经网络前向运算得到第7层4096维特征向量和第8层128维输出(设定阈值0.5之后可以转成01二值检索向量),对于待检索的图片,同样得到4096维特征向量和128维01二值检索向量,在数据库中查找二值检索向量对应『桶』内图片,比对4096维特征向量之间距离,做重拍即得到最终结果。图上的检索例子比较直观,对于待检索的”鹰”图像,算得二值检索向量为101010,取出桶内图片(可以看到基本也都为鹰),比对4096维特征向量之间距离,重新排序拿得到最后的检索结果。
### 4\. 预训练好的模型
一般说来,在自己的图片训练集上,针对特定的场景进行图像类别训练,得到的神经网络,中间层特征的表达能力会更有针对性一些。具体训练的过程可以第3节中的说明。对于不想自己重新费时训练,或者想快速搭建一个基于内容的图片检索系统的同学,这里也提供了100w图片上训练得到的卷积神经网络模型供大家使用。
这里提供了2个预先训练好的模型,供大家提取『图像特征』和『二值检索串』用。2个模型训练的数据集一致,卷积神经网络搭建略有不同。对于几万到十几万级别的小量级图片建立检索系统,请使用模型Image_Retrieval_20_hash_code.caffemodel,对于百万以上的图片建立检索系统,请使用模型Image_Retrieval_128_hash_code.caffemodel。
对于同一张图片,两者产出的特征均为4096维度,但用作分桶的『二值检索向量』长度,前者为20,后者为128。
模型下载地址为[云盘地址](http://pan.baidu.com/s/1eQS8l6y)。
## 傻瓜式环境配置手册
### 1.关于系统
这个说明是关于linux系统的,最好是centOS 7.0以上,或者ubuntu 14.04 以上。低版本的系统可能会出现boost,opencv等库版本不兼容问题。
### 2\. centOS配置方法
#### 2.1 配置yum源
配置合适的yum源是一种『偷懒』的办法,可以简化很多后续操作。不进行这一步的话很多依赖库都需要自己手动编译和指定caffe编译路径,耗时且经常编译不成功。
在国内的话用sohu或者163的源
`rpm -Uvh http://mirrors.sohu.com/fedora-epel/7/x86_64/e/epel-release-7-2.noarch.rpm`
如果身处国外的话,可以查一下[fedora mirror list](https://admin.fedoraproject.org/mirrormanager/mirrors/EPEL/7/x86_64),找到合适的yum源添加。
接着我们让新的源生效:
`yum repolist`
#### 2.2 安装依赖的库
该图像检索系统依赖于caffe深度学习框架,因此需要安装caffe依赖的部分库:比如protobuf是caffe中定义layers的配置文件解析时需要的,leveldb是训练时存储图片数据的数据库,opencv是图像处理库,boost是通用C++库,等等…
我们用yum install一键安装:
`sudo yum install protobuf-devel leveldb-devel snappy-devel opencv-devel boost-devel hdf5-devel`
#### 2.3 安装科学计算库
这个部分大家都懂的,因为要训练和识别过程,涉及到大量的科学计算,因此必要的科学计算库也需要安装。同时python版本caffe中会依赖一些python科学计算库,pip和easy_install有时候安装起来会有一些问题,因此部分库这里也用yum install直接安装了。
`yum install openblas-devel.x86_64 gcc-c++.x86_64 numpy.x86_64 scipy.x86_64 python-matplotlib.x86_64 lapack-devel.x86_64 python-pillow.x86_64 libjpeg-turbo-devel.x86_64 freetype-devel.x86_64 libpng-devel.x86_64 openblas-devel.x86_64`
#### 2.4 其余依赖
包括lmdb等:
`sudo yum install gflags-devel glog-devel lmdb-devel`
若此处yum源中找不到这些拓展package,可是手动编译(要有root权限):
~~~
# glog
wget https://google-glog.googlecode.com/files/glog-0.3.3.tar.gz
tar zxvf glog-0.3.3.tar.gz
cd glog-0.3.3
./configure
make && make install
# gflags
wget https://github.com/schuhschuh/gflags/archive/master.zip
unzip master.zip
cd gflags-master
mkdir build && cd build
export CXXFLAGS="-fPIC" && cmake .. && make VERBOSE=1
make && make install
# lmdb
git clone https://github.com/LMDB/lmdb
cd lmdb/libraries/liblmdb
make && make install
~~~
#### 2.5 python版本依赖
编译pycaffe的时候,我们需要更多的一些python的依赖库。这时候我们可以用pip或者easy_install完成。
pip和easy_install的配置方法为:
~~~
wget --no-check-certificate https://bootstrap.pypa.io/ez_setup.py
python ez_setup.py --insecure
wget https://bootstrap.pypa.io/get-pip.py
python get-pip.py
~~~
在caffe/python/requirements.txt中有pycaffe的python依赖包,如下:
~~~
Cython>=0.19.2
numpy>=1.7.1
scipy>=0.13.2
scikit-image>=0.9.3
matplotlib>=1.3.1
ipython>=3.0.0
h5py>=2.2.0
leveldb>=0.191
networkx>=1.8.1
nose>=1.3.0
pandas>=0.12.0
python-dateutil>=1.4,<2
protobuf>=2.5.0
python-gflags>=2.0
pyyaml>=3.10
Pillow>=2.3.0
~~~
通过以下shell命令可以全部安装:
~~~
for req in $(cat requirements.txt); do pip install $req; done
~~~
### 3\. ubuntu配置方法
基本与centOS一致,这里简单列出需要执行的shell命令:
~~~
sudo apt-get install libprotobuf-dev libleveldb-dev libsnappy-dev libopencv-dev libhdf5-serial-dev protobuf-compiler
sudo apt-get install --no-install-recommends libboost-all-dev
sudo apt-get install libgflags-dev libgoogle-glog-dev liblmdb-dev
~~~
python部分的依赖包安装方式同上。
### 4\. caffe的编译与准备
保证caffe所需依赖都安装完成后,在caffe目录下执行:
`cp Makefile.config.example Makefile.config`
根据自己的实际情况,修改Makefile.config的内容,主要修改的几个如下:
* 如果**没有GPU**,只打算用CPU进行实验,将`# CPU_ONLY := 1`前的#号去掉。
* 如果使用GPU,且有**cuDNN加速**,将`# USE_CUDNN := 1`前的#号去掉。
* 如果使用openBLAS,将`BLAS := atlas`改成`BLAS := open`,并添加`BLAS_INCLUDE := /usr/include/openblas`(Caffe中默认的矩阵运算库为[ATLAS](http://math-atlas.sourceforge.net/),但是[OpenBLAS](http://www.openblas.net/)有一些性能优化,因此建议换做OpenBLAS)
未完待续…
(10)_细说卷积神经网络
最后更新于:2022-04-01 14:21:56
作者:[寒小阳](http://blog.csdn.net/han_xiaoyang?viewmode=contents)
时间:2016年1月。
出处:[http://blog.csdn.net/han_xiaoyang/article/details/50542880](http://blog.csdn.net/han_xiaoyang/article/details/50542880)
声明:版权所有,转载请联系作者并注明出处
### 1. 前言
前面九讲对神经网络的结构,组件,训练方法,原理等做了介绍。现在我们回到本系列的核心:计算机视觉,神经网络中的一种特殊版本在计算机视觉中使用最为广泛,这就是大家都知道的卷积神经网络。卷积神经网络和普通的神经网络一样,由『神经元』按层级结构组成,其间的权重和偏移量都是可训练得到的。同样是输入的数据和权重做运算,输出结果输入激励神经元,输出结果。从整体上看来,整个神经网络做的事情,依旧是对于像素级别输入的图像数据,用得分函数计算最后各个类别的得分,然后我们通过最小化损失函数来得到最优的权重。之前的博文中介绍的各种技巧和训练方法,以及注意事项,在这个特殊版本的神经网络上依旧好使。
咳咳,我们来说说它的特殊之处,首先卷积神经网络一般假定输入就是图片数据,也正是因为输入是图片数据,我们可以利用它的像素结构特性,去做一些假设来简化神经网络的训练复杂度(减少训练参数个数)。
### 2.卷积神经网总体结构一览
我们前面讲过的神经网络结构都比较一致,输入层和输出层中间夹着数层隐藏层,每一层都由多个神经元组成,层和层之间是全连接的结构,同一层的神经元之间没有连接。
卷积神经网络是上述结构的一种特殊化处理,因为对于图像这种数据而言,上面这种结构实际应用起来有较大的困难:就拿CIFAR-10举例吧,图片已经很小了,是32*32*3(长宽各32像素,3个颜色通道)的,那么在神经网络当中,我们只看隐藏层中的一个神经元,就应该有32*32*3=3072个权重,如果大家觉得这个权重个数的量还行的话,再设想一下,当这是一个包含多个神经元的多层神经网(假设n个),再比如图像的质量好一点(比如是200*200*3的),那将有200*200*3*n= 120000n个权重需要训练,结果是拉着这么多参数训练,基本跑不动,跑得起来也是『气喘吁吁』,当然,最关键的是这么多参数的情况下,分分钟模型就过拟合了。别急,别急,一会儿我们会提到卷积神经网络的想法和简化之处。
卷积神经网络结构比较固定的原因之一,是图片数据本身的合理结构,类图像结构(200 * 200 * 3),我们也把卷积神经网络的神经元排布成 width * height * depth的结构,也就是说这一层总共有width * height * depth个神经元,如下图所示。举个例子说,CIFAR-10的输出层就是1 * 1* 10维的。另外我们后面会说到,每一层的神经元,其实只和上一层里某些小区域进行连接,而不是和上一层每个神经元全连接。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad46ec0f.png)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad774438.png)
#### 3.卷积神经网络的组成层
在卷积神经网络中,有3种最主要的层:
- 卷积运算层
- pooling层
- 全连接层
一个完整的神经网络就是由这三种层叠加组成的。
**结构示例**
我们继续拿CIFAR-10数据集举例,一个典型的该数据集上的卷积神经网络分类器应该有[INPUT - CONV - RELU - POOL - FC]的结构,具体说来是这样的:
- INPUT[32*32*3]包含原始图片数据中的全部像素,长宽都是32,有RGB 3个颜色通道。
- CONV卷积层中,没个神经元会和上一层的若干小区域连接,计算权重和小区域像素的内积,举个例子可能产出的结果数据是[32*32*12]的。
- RELU层,就是神经元激励层,主要的计算就是max(0,x),结果数据依旧是[32*32*12]。
- POOLing层做的事情,可以理解成一个下采样,可能得到的结果维度就变为[16*16*12]了。
- 全连接层一般用于最后计算类别得分,得到的结果为[1*1*10]的,其中的10对应10个不同的类别。和名字一样,这一层的所有神经元会和上一层的所有神经元有连接。
这样,卷积神经网络作为一个中间的通道,就一步步把原始的图像数据转成最后的类别得分了。有一个点我们要提一下,刚才说到了有几种不同的神经网络层,其中有一些层是有待训练参数的,另外一些没有。详细一点说,卷积层和全连接层包含权重和偏移的;而RELU和POOLing层只是一个固定的函数运算,是不包含权重和偏移参数的。不过POOLing层包含了我们手动指定的超参数,这个我们之后会提到。
总结一下:
- 一个卷积神经网络由多种不同类型的层(卷几层/全连接层/RELU层/POOLing层等)叠加而成。
- 每一层的输入结构是3维的数据,计算完输出依旧是3维的数据。
- 卷积层和全连接层包含训练参数,RELU和POOLing层不包含。
- 卷积层,全连接层和POOLing层包含超参数,RELU层没有。
下图为CIFAR-10数据集构建的一个卷积神经网络结构示意图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad791ec0.png)
既然有这么多不同的层级结构,那我们就展开来讲讲:
#### 3.1 卷积层
说起来,这是卷积神经网络的核心层(从名字就可以看出来对吧-_-||)。
#### 3.1.1 卷积层综述
**直观看来**,卷积层的参数其实可以看做,一系列的可训练/学习的过滤器。在前向计算过程中,我们输入一定区域大小(width*height)的数据,和过滤器点乘后等到新的二维数据,然后滑过一个个滤波器,组成新的3维输出数据。而我们可以理解成每个过滤器都只关心过滤数据小平面内的部分特征,当出现它学习到的特征的时候,就会呈现激活/activate态。
**局部关联度**。这是卷积神经网络的独特之处其中之一,我们知道在高维数据(比如图片)中,用全连接的神经网络,实际工程中基本是不可行的。卷积神经网络中每一层的神经元只会和上一层的一些局部区域相连,这就是所谓的局部连接性。你可以想象成,上一层的数据区,有一个滑动的窗口,只有这个窗口内的数据会和下一层神经元有关联,当然,这个做法就要求我们手动敲定一个超参数:窗口大小。通常情况下,这个窗口的长和宽是相等的,我们把长x宽叫做receptive field。实际的计算中,这个窗口是会『滑动』的,会近似覆盖图片的所有小区域。
举个实例,CIFAR-10中的图片输入数据为[32*32*3]的,如果我们把receptive field设为5*5,那receptive field的data都会和下一层的神经元关联,所以共有5*5*3=75个权重,注意到最后的3依旧代表着RGB 3个颜色通道。
如果不是输入数据层,中间层的data格式可能是[16 * 16 * 20]的,假如我们取3 * 3的receptive field,那单个神经元的权重为3*3*20=180。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad7ca3d8.png)
**局部关联细节**。我们刚才说到卷积层的局部关联问题,这个地方有一个receptive field,也就是我们直观理解上的『滑动数据窗口』。从输入的数据到输出数据,有三个超参数会决定输出数据的维度,分别是深度/depth,步长/stride 和 填充值/zero-padding:
1. 所谓深度/depth,简单说来指的就是卷积层中和上一层同一个输入区域连接的神经元个数。这部分神经元会在遇到输入中的不同feature时呈现activate状态,举个例子,如果这是第一个卷积层,那输入到它的数据实际上是像素值,不同的神经元可能对图像的边缘。轮廓或者颜色会敏感。
1. 所谓步长/stride,是指的窗口从当前位置到下一个位置,『跳过』的中间数据个数。比如从图像数据层输入到卷积层的情况下,也许窗口初始位置在第1个像素,第二个位置在第5个像素,那么stride=5-1=4.
1. 所谓zero-padding是在原始数据的周边补上0值的圈数。(下面第2张图中的样子)
这么解释可能理解起来还是会有困难,我们找两张图来对应一下这三个量:
![卷积层](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad7e24e6.png "")
这是解决ImageNet分类问题用到的卷积神经网络的一部分,我们看到卷积层直接和最前面的图像层连接。图像层的维度为[227 * 227 * 3],而receptive field设为11 * 11,图上未标明,但是滑动窗口的步长stride设为4,深度depth为48+48=96(这是双GPU并行设置),边缘没有补0,因此zero-padding为0,因此窗口滑完一行,总共停留次数为(data_len-receptive_field_len+2 * zero-padding)/stride+1=(227-11+2 * 0)/4+1=55,因为图像的长宽相等,因此纵向窗口数也是55,最后得到的输出数据维度为55 * 55 * 96维。
![滑动窗口图](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad812e5f.gif "")
这是一张动态的卷积层计算图,图上的zero-padding为1,所以大家可以看到数据左右各补了一行0,窗口的长宽为3,滑动步长stride为2。
**关于zero-padding**,补0这个操作产生的根本原因是,为了保证窗口的滑动能从头刚好到尾。举个例子说,上2图中的上面一幅图,因为(data_len-receptive_field_len+2 * zero-padding)/stride刚好能够整除,所以窗口左侧贴着数据开始位置,滑到尾部刚好窗口右侧能够贴着数据尾部位置,因此是不需要补0的。而在下面那幅图中,如果滑动步长设为4,你会发现第一次计算之后,窗口就无法『滑动』了,而尾部的数据,是没有被窗口『看到过』的,因此补0能够解决这个问题。
**关于窗口滑动步长**。大家可以发现一点,窗口滑动步长设定越小,两次滑动取得的数据,重叠部分越多,但是窗口停留的次数也会越多,运算律大一些;窗口滑动步长设定越长,两次滑动取得的数据,重叠部分越少,窗口停留次数也越少,运算量小,但是从一定程度上说数据信息不如上面丰富了。
#### 3.1.2 卷积层的参数共享
首先得说卷积层的参数共享是一个非常赞的处理方式,它使得卷积神经网络的训练计算复杂度和参数个数降低非常非常多。就拿实际解决ImageNet分类问题的卷积神经网络结构来说,我们知道输出结果有55*55*96=290400个神经元,而每个神经元因为和窗口内数据的连接,有11*11*3=363个权重和1个偏移量。所以总共有290400*364=105705600个权重。。。然后。。。恩,训练要累挂了。。。
因此我们做了一个大胆的假设,我们刚才提到了,每一个神经元可以看做一个filter,对图片中的数据窗区域做『过滤』。那既然是filter,我们干脆就假设这个神经元用于连接数据窗的权重是固定的,这意味着,对同一个神经元而言,不论上一层数据窗口停留在哪个位置,连接两者之间的权重都是同一组数。那代表着,上面的例子中的卷积层,我们只需要 神经元个数*数据窗口维度=96*11*11*3=34848个权重。
如果对应每个神经元的权重是固定的,那么整个计算的过程就可以看做,一组固定的权重和不同的数据窗口数据做内积的过程,这在数学上刚好对应『卷积』操作,这也就是卷积神经网的名字来源。另外,因为每个神经元的权重固定,它可以看做一个恒定的filter,比如上面96个神经元作为filter可视化之后是如下的样子:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad86c9e4.png)
需要说明的一点是,参数共享这个策略并不是每个场景下都合适的。有一些特定的场合,我们不能把图片上的这些窗口数据都视作作用等同的。一个很典型的例子就是人脸识别,一般人的面部都集中在图像的中央,因此我们希望,数据窗口滑过这块区域的时候,权重和其他边缘区域是不同的。我们有一种特殊的层对应这种功能,叫做局部连接层/Locally-Connected Layer
#### 3.1.3 卷积层的简单numpy实现
我们假定输入到卷积层的数据为`X`,加入`X`的维度为`X.shape: (11,11,4)`。假定我们的zero-padding为0,也就是左右上下不补充0数据,数据窗口大小为5,窗口滑动步长为2。那输出数据的长宽应该为(11-5)/2+1=4。假定第一个神经元对应的权重和偏移量分别为W0和b0,那我们就能算得,在第一行数据窗口停留的4个位置,得到的结果值分别为:
- `V[0,0,0] = np.sum(X[:5,:5,:] * W0) + b0`
- `V[1,0,0] = np.sum(X[2:7,:5,:] * W0) + b0`
- `V[2,0,0] = np.sum(X[4:9,:5,:] * W0) + b0`
- `V[3,0,0] = np.sum(X[6:11,:5,:] * W0) + b0`
注意上述计算过程中,`*`运算符是对两个向量进行点乘的,因此W0应该维度为(5,5,4),同样你可以计算其他位置的计算输出值:
- `V[0,0,1] = np.sum(X[:5,:5,:] * W1) + b1`
- `V[1,0,1] = np.sum(X[2:7,:5,:] * W1) + b1`
- `V[2,0,1] = np.sum(X[4:9,:5,:] * W1) + b1`
- `V[3,0,1] = np.sum(X[6:11,:5,:] * W1) + b1`
- …
每一个神经元对应不同的一组`W`和`b`,在每个数据窗口停留的位置,得到一个输出值。
我们之前提到了卷积层在做的事情,是不断做权重和窗口数据的点乘和求和。因此我们也可以把这个过程整理成一个大的矩阵乘法。
1. 看看数据端,我们可以做一个操作**im2col**将数据转成一个可直接供神经元filter计算的大矩阵。举个例子说,输入是[227*227*3]的图片,而神经元权重为[11*11*3],同时窗口移动步长为4,那我们知道数据窗口滑动过程中总共产生[(227-11)/4+1]*[(227-11)/4+1]=55*55=3025个局部数据区域,又每个区域包含11*11*3=363个数据值,因此我们想办法把原始数据重复和扩充成一个[363*3025]的数据矩阵`X_col`,就可以直接和filter进行运算了。
1. 对于filter端(卷积层),假如厚度为96(有96个不同权重组的filter),每个filter的权重为[11*11*3],因此filter矩阵`W_row`维度为[96*363]
1. 在得到上述两个矩阵后,我们的输出结果即可以通过`np.dot(W_row, X_col)`计算得到,结果数据为[96*3025]维的。
这个实现的弊端是,因为数据窗口的滑动过程中有重叠,因此我们出现了很多重复数据,占用内存较大。好处是,实际计算过程非常简单,如果我们用类似BLAS这样的库,计算将非常迅速。
另外,在反向传播过程中,其实卷积对应的操作还是卷积,因此实现起来也很方便。
#### 3.2 Pooling层
简单说来,在卷积神经网络中,Pooling层是夹在连续的卷积层中间的层。它的作用也非常简单,就是逐步地压缩/减少数据和参数的量,也在一定程度上减小过拟合的现象。Pooling层做的操作也非常简单,就是将原数据上的区域压缩成一个值(区域最大值/MAX或者平均值/AVERAGE),最常见的Pooling设定是,将原数据切成2 * 2的小块,每块里面取最大值作为输出,这样我们就自然而然减少了75%的数据量。需要提到的是,除掉MAX和AVERAGE的Pooling方式,其实我们也可以设定别的pooling方式,比如L2范数pooling。说起来,历史上average pooling用的非常多,但是近些年热度降了不少,工程师们在实践中发现max pooling的效果相对好一些。
一个对Pooling层和它的操作直观理解的示意图为:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad894341.png)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad8b2ef0.png)
上图为Pooling层的一个直观示例,相当于对厚度为64的data,每一个切片做了一个下采样。下图为Pooling操作的实际max操作。
Pooling层(假定是MAX-Pooling)在反向传播中的计算也是很简单的,大家都知道如何去求max(x,y)函数的偏导。
#### 3.3 归一化层(Normalization Layer)
卷积神经网络里面有时候会用到各种各样的归一化层,尤其是早期的研究,经常能见到它们的身影,不过近些年来的研究表明,似乎这个层级对最后结果的帮助非常小,所以后来大多数时候就干脆拿掉了。
#### 3.4 全连接层
这是我们在介绍神经网络的时候,最标准的形式,任何神经元和上一层的任何神经元之间都有关联,然后矩阵运算也非常简单和直接。现在的很多卷积神经网络结构,末层会采用全连接去学习更多的信息。
### 4. 搭建卷积神经网结构
从上面的内容我们知道,卷积神经网络一般由3种层搭建而成:卷积层,POOLing层(我们直接指定用MAX-Pooling)和全连接层。然后我们一般选用最常见的神经元ReLU,我们来看看有这些『组件』之后,怎么『拼』出一个合理的卷积神经网。
#### 4.1 层和层怎么排
最常见的组合方式是,用ReLU神经元的卷积层组一个神经网络,同时在卷积层和卷积层之间插入Pooling层,经过多次的[卷积层]=>[Pooling层]叠加之后,数据的总体量级就不大了,这个时候我们可以放一层全连接层,然后最后一层和output层之间是一个全连接层。所以总结一下,最常见的卷积神经网结构为:
[输入层] => [[ReLU卷积层] * N => [Pooling层]?] * M => [ReLU全连接层]*K => [全连接层]
解释一下,其中`\*`操作代表可以叠加很多层,而`[Pooling层]?`表示Pooling层其实是可选的,可有可无。`N`和`M`是具体层数。比如说`[输入层] -> [[ReLU卷积层]=>[ReLU卷积层]=>[Pooling层]]*3 -> [ReLU全连接层]*2 -> [全连接层]`就是一个合理的深层的卷积神经网。
『在同样的视野范围内,选择多层叠加的卷积层,而不是一个大的卷积层』
这句话非常拗口,但这是实际设计卷积神经网络时候的经验,我们找个例子来解释一下这句话:如果你设计的卷积神经网在数据层有3层连续的卷积层,同时每一层滑动数据窗口为3*3,第一层每个神经元可以同时『看到』3*3的原始数据层,那第二层每个神经元可以『间接看到』(1+3+1)*(1+3+1)=5*5的数据层内容,第三层每个神经元可以『间接看到』(1+5+1)*(1+5+1)=7*7的数据层内容。那从最表层看,还不如直接设定滑动数据窗口为7*7的,为啥要这么设计呢,我们来分析一下优劣:
- 虽然第三层对数据层的『视野』范围是一致的。但是单层卷积层加7*7的上层滑动数据窗口,结果是这7个位置的数据,都是线性组合后得到最后结果的;而3层卷积层加3*3的滑动数据窗口,得到的结果是原数据上7*7的『视野』内数据多层非线性组合,因此这样的特征也会具备更高的表达能力。
- 如果我们假设所有层的`『厚度』/channel`数是一致的,为C,那7*7的卷积层,会得到C×(7×7×C)=49C2个参数,而3层叠加的3*3卷积层只有3×(C×(3×3×C))=27C2个参数。在计算量上后者显然是有优势的。
- 同上一点,我们知道为了反向传播方便,实际计算过程中,我们会在前向计算时保留很多中间梯度,3层叠加的3*3卷积层需要保持的中间梯度要小于前一种情况,这在工程实现上是很有好处的。
#### 4.2 层大小的设定
话说层级结构确定了,也得知道每一层大概什么规模啊。现在我们就来聊聊这个。说起来,每一层的大小(神经元个数和排布)并没有严格的数字规则,但是我们有一些通用的工程实践经验和系数:
- 对于输入层(图像层),我们一般把数据归一化成2的次方的长宽像素值。比如CIFAR-10是32*32*3,STL-10数据集是64*64*3,而ImageNet是224*224*3或者512*512*3。
- 卷积层通常会把每个[滤子/filter/神经元]对应的上层滑动数据窗口设为3*3或者5*5,滑动步长stride设为1(工程实践结果表明stride设为1虽然比较密集,但是效果比较好,步长拉太大容易损失太多信息),zero-padding就不用了。
- Pooling层一般采用max-pooling,同时设定采样窗口为2*2。偶尔会见到设定更大的采样窗口,但是那意味着损失掉比较多的信息了。
- 比较重要的是,我们得预估一下内存,然后根据内存的情况去设定合理的值。我们举个例子,在ImageNet分类问题中,图片是224*224*3的,我们跟在数据层后面3个3*3『视野窗』的卷积层,每一层64个filter/神经元,把padding设为1,那么最后每个卷积层的output都是[224*224*64],大概需要1000万次对output的激励计算(非线性activation),大概花费72MB内存。而工程实践里,一般训练都在GPU上进行,GPU的内存比CPU要吃紧的多,所以也许我们要稍微调动一下参数。比如AlexNet用的是11*11的的视野窗,滑动步长为4。
#### 4.3 典型的工业界在用卷积神经网络
几个有名的卷积神经网络如下:
- **LeNet**,这是最早用起来的卷积神经网络,Yann LeCun在论文[LeNet](http://yann.lecun.com/exdb/publis/pdf/lecun-98.pdf)提到。
- **AlexNet**,2012 ILSVRC比赛远超第2名的卷积神经网络,和LeNet的结构比较像,只是更深,同时用多层小卷积层叠加提到大卷积层。
- **ZF Net**,2013 ILSVRC比赛冠军,可以参考论文[ZF Net](http://arxiv.org/abs/1311.2901)
- **GoogLeNet**,2014 ILSVRC比赛冠军,Google发表的论文[Going Deeper with Convolutions](http://arxiv.org/pdf/1409.4842v1.pdf)有具体介绍。
- **VGGNet**,也是2014 ILSVRC比赛中的模型,有意思的是,即使这个模型当时在分类问题上的效果,略差于google的GoogLeNet,但是在很多图像转化学习问题(比如object detection)上效果奇好,它也证明卷积神经网的『深度』对于最后的效果有至关重要的作用。预训练好的模型在[pretrained model site](http://www.robots.ox.ac.uk/~vgg/research/very_deep/)可以下载。
具体一点说来,[VGGNet](http://www.robots.ox.ac.uk/~vgg/research/very_deep/)的层级结构和花费的内存如下:
~~~
INPUT: [224x224x3] memory: 224*224*3=150K weights: 0
CONV3-64: [224x224x64] memory: 224*224*64=3.2M weights: (3*3*3)*64 = 1,728
CONV3-64: [224x224x64] memory: 224*224*64=3.2M weights: (3*3*64)*64 = 36,864
POOL2: [112x112x64] memory: 112*112*64=800K weights: 0
CONV3-128: [112x112x128] memory: 112*112*128=1.6M weights: (3*3*64)*128 = 73,728
CONV3-128: [112x112x128] memory: 112*112*128=1.6M weights: (3*3*128)*128 = 147,456
POOL2: [56x56x128] memory: 56*56*128=400K weights: 0
CONV3-256: [56x56x256] memory: 56*56*256=800K weights: (3*3*128)*256 = 294,912
CONV3-256: [56x56x256] memory: 56*56*256=800K weights: (3*3*256)*256 = 589,824
CONV3-256: [56x56x256] memory: 56*56*256=800K weights: (3*3*256)*256 = 589,824
POOL2: [28x28x256] memory: 28*28*256=200K weights: 0
CONV3-512: [28x28x512] memory: 28*28*512=400K weights: (3*3*256)*512 = 1,179,648
CONV3-512: [28x28x512] memory: 28*28*512=400K weights: (3*3*512)*512 = 2,359,296
CONV3-512: [28x28x512] memory: 28*28*512=400K weights: (3*3*512)*512 = 2,359,296
POOL2: [14x14x512] memory: 14*14*512=100K weights: 0
CONV3-512: [14x14x512] memory: 14*14*512=100K weights: (3*3*512)*512 = 2,359,296
CONV3-512: [14x14x512] memory: 14*14*512=100K weights: (3*3*512)*512 = 2,359,296
CONV3-512: [14x14x512] memory: 14*14*512=100K weights: (3*3*512)*512 = 2,359,296
POOL2: [7x7x512] memory: 7*7*512=25K weights: 0
FC: [1x1x4096] memory: 4096 weights: 7*7*512*4096 = 102,760,448
FC: [1x1x4096] memory: 4096 weights: 4096*4096 = 16,777,216
FC: [1x1x1000] memory: 1000 weights: 4096*1000 = 4,096,000
TOTAL memory: 24M * 4 bytes ~= 93MB / image (only forward! ~*2 for bwd)
TOTAL params: 138M parameters
~~~
有意思的是,大家会注意到,在VGGNet这样一个神经网络里,大多数的内存消耗在前面的卷积层,而大多数需要训练的参数却集中在最后的全连接层,比如上上面的例子里,全连接层有1亿权重参数,总共神经网里也就1.4亿权重参数。
#### 4.4 考虑点
组一个实际可用的卷积神经网络最大的瓶颈是GPU的内存。毕竟现在很多GPU只有3/4/6GB的内存,最大的GPU也就12G内存,所以我们应该在设计卷积神经网的时候多加考虑:
- 很大的一部分内存开销来源于卷积层的激励函数个数和保存的梯度数量。
- 保存的权重参数也是内存的主要消耗处,包括反向传播要用到的梯度,以及你用momentum, Adagrad, or RMSProp这些算法时候的中间存储值。
- 数据batch以及其他的类似版本信息或者来源信息等也会消耗一部分内存。
### 5. 更多的卷积神经网络参考资料
- [DeepLearning.net tutorial](http://deeplearning.net/tutorial/lenet.html)是一个用Theano完整实现卷积神经网的教程。
- [cuda-convnet2](https://code.google.com/p/cuda-convnet2/)是多GPU并行化的实现。
- [ConvNetJS CIFAR-10 demo](http://cs.stanford.edu/people/karpathy/convnetjs/demo/cifar10.html)允许你手动设定参数,然后直接在浏览器看卷积神经网络的结果。
- [Caffe](http://caffe.berkeleyvision.org/),主流卷积神经网络开源库之一。
- [Example Torch 7 ConvNet](https://github.com/nagadomi/kaggle-cifar10-torch7),在CIFAR-10上错误率只有7%的卷积神经网络实现。
- [Ben Graham’s Sparse ConvNet](https://www.kaggle.com/c/cifar-10/forums/t/10493/train-you-very-own-deep-convolutional-network/56310),CIFAR-10上错误率只有4%的实现。
- [Face recognition for right whales using deep learning](http://deepsense.io/deep-learning-right-whale-recognition-kaggle/?from=singlemessage&isappinstalled=0#rd),Kaggle看图识别濒临灭绝右鲸比赛的冠军队伍卷积神经网络。
(9)_串一串神经网络之动手实现小例子
最后更新于:2022-04-01 14:21:54
作者:[寒小阳](http://blog.csdn.net/han_xiaoyang?viewmode=contents)
时间:2016年1月。
出处:[http://blog.csdn.net/han_xiaoyang/article/details/50521072](http://blog.csdn.net/han_xiaoyang/article/details/50521072)
声明:版权所有,转载请联系作者并注明出处
### 1.引言
前面8小节,算从神经网络的结构、简单原理、数据准备与处理、神经元选择、损失函数选择等方面把神经网络过了一遍。这个部分我们打算把知识点串一串,动手实现一个简单的2维平面神经网络分类器,去分割平面上的不同类别样本点。为了循序渐进,我们打算先实现一个简单的线性分类器,然后再拓展到非线性的2层神经网络。我们可以看到简单的浅层神经网络,在这个例子上就能够有分割程度远高于线性分类器的效果。
### 2.样本数据的产生
为了凸显一下神经网络强大的空间分割能力,我们打算产生出一部分对于线性分类器不那么容易分割的样本点,比如说我们生成一份螺旋状分布的样本点,如下:
~~~
N = 100 # 每个类中的样本点
D = 2 # 维度
K = 3 # 类别个数
X = np.zeros((N*K,D)) # 样本input
y = np.zeros(N*K, dtype='uint8') # 类别标签
for j in xrange(K):
ix = range(N*j,N*(j+1))
r = np.linspace(0.0,1,N) # radius
t = np.linspace(j*4,(j+1)*4,N) + np.random.randn(N)*0.2 # theta
X[ix] = np.c_[r*np.sin(t), r*np.cos(t)]
y[ix] = j
# 可视化一下我们的样本点
plt.scatter(X[:, 0], X[:, 1], c=y, s=40, cmap=plt.cm.Spectral)
~~~
得到如下的样本分布:
![螺旋样本点集](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad70b30e.png "")
紫色,红色和黄色分布代表不同的3种类别。
一般来说,**拿到数据都要做预处理**,包括之前提到的**去均值和方差归一化**。不过我们构造的数据幅度已经在-1到1之间了,所以这里不用做这个操作。
### 3.使用Softmax线性分类器
#### 3.1 初始化参数
我们先在训练集上用softmax线性分类器试试。如我们在[之前的章节](http://blog.csdn.net/han_xiaoyang/article/details/49999583)提到的,我们这里用的softmax分类器,使用的是一个线性的得分函数/score function,使用的损失函数是互熵损失/cross-entropy loss。包含的参数包括得分函数里面用到的权重矩阵`W`和偏移量`b`,我们先随机初始化这些参数。
~~~
#随机初始化参数
import numpy as np
#D=2表示维度,K=3表示类别数
W = 0.01 * np.random.randn(D,K)
b = np.zeros((1,K))
~~~
#### 3.2 计算得分
线性的得分函数,将原始的数据映射到得分域非常简单,只是一个直接的矩阵乘法。
~~~
#使用得分函数计算得分
scores = np.dot(X, W) + b
~~~
在我们给的这个例子中,我们有2个2维点集,所以做完乘法过后,矩阵得分`scores`其实是一个[300*3]的矩阵,每一行都给出对应3各类别(紫,红,黄)的得分。
#### 3.3 计算损失
然后我们就要开始使用我们的损失函数计算`损失`了,我们之前也提到过,损失函数计算出来的结果代表着预测结果和真实结果之间的吻合度,我们的目标是最小化这个结果。直观一点理解,我们希望对每个样本而言,对应正确类别的得分高于其他类别的得分,如果满足这个条件,那么损失函数计算的结果是一个比较低的值,如果判定的类别不是正确类别,则结果值会很高。我们[之前](http://blog.csdn.net/han_xiaoyang/article/details/49999583)提到了,softmax分类器里面,使用的损失函数是互熵损失。一起回忆一下,假设`f`是得分向量,那么我们的互熵损失是用如下的形式定义的:
Li=−log(efyi∑jefj)
直观地理解一下上述形式,就是Softmax分类器把类别得分向量`f`中每个值都看成对应三个类别的log似然概率。因此我们在求每个类别对应概率的时候,使用指数函数还原它,然后归一化。从上面形式里面大家也可以看得出来,得到的值总是在0到1之间的,因此从某种程度上说我们可以把它理解成概率。如果判定类别是错误类别,那么上述公式的结果就会趋于无穷,也就是说损失相当相当大,相反,如果判定类别正确,那么损失就接近log(1)=0。这和我们直观理解上要最小化`损失`是完全吻合的。
当然,当然,别忘了,完整的损失函数定义,一定会加上正则化项,也就是说,完整的损失`L`应该有如下的形式:
L=1N∑iLidata loss+12λ∑k∑lW2k,lregularization loss
好,我们实现以下,根据上面计算得到的得分`scores`,我们计算以下各个类别上的概率:
~~~
# 用指数函数还原
exp_scores = np.exp(scores)
# 归一化
probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True)
~~~
在我们的例子中,我们最后得到了一个[300*3]的概率矩阵`prob`,其中每一行都包含属于3个类别的概率。然后我们就可以计算完整的互熵损失了:
~~~
#计算log概率和互熵损失
corect_logprobs = -np.log(probs[range(num_examples),y])
data_loss = np.sum(corect_logprobs)/num_examples
#加上正则化项
reg_loss = 0.5*reg*np.sum(W*W)
loss = data_loss + reg_loss
~~~
正则化强度λ在上述代码中是reg,最开始的时候我们可能会得到`loss=1.1`,是通过`np.log(1.0/3)`得到的(假定初始的时候属于3个类别的概率一样),我们现在想最小化损失`loss`
#### 3.4 计算梯度与梯度回传
我们能够用损失函数评估预测值与真实值之间的差距,下一步要做的事情自然是最小化这个值。我们用传统的梯度下降来解决这个问题。多解释一句,梯度下降的过程是:我们先选取一组随机参数作为初始值,然后计算损失函数在这组参数上的梯度(负梯度的方向表明了损失函数减小的方向),接着我们朝着负梯度的方向迭代和更新参数,不断重复这个过程直至损失函数最小化。为了清楚一点说明这个问题,我们引入一个中间变量p,它是归一化后的概率向量,如下:
pk=efk∑jefjLi=−log(pyi)
我们现在希望知道朝着哪个方向调整权重能够减小损失,也就是说,我们需要计算梯度∂Li/∂fk。损失Li从p计算而来,再退一步,依赖于f。于是我们又要做高数题,使用链式求导法则了,不过梯度的结果倒是非常简单:
∂Li∂fk=pk−1(yi=k)
解释一下,公式的最后一个部分表示yi=k的时候,取值为1。整个公式其实非常的优雅和简单。假设我们计算的概率p=[0.2, 0.3, 0.5],而中间的类别才是真实的结果类别。根据梯度求解公式,我们得到梯度df=[0.2,−0.7,0.5]。我们想想梯度的含义,其实这个结果是可解释性非常高的:大家都知道,梯度是最快上升方向,我们减掉它乘以步长才会让损失函数值减小。第1项和第3项(其实就是不正确的类别项)梯度为正,表明增加它们只会让最后的损失/loss增大,而我们的目标是减小loss;中间的梯度项-0.7其实再告诉我们,增加这一项,能减小损失Li,达到我们最终的目的。
我们依旧记`probs`为所有样本属于各个类别的概率,记`dscores`为得分上的梯度,我们可以有以下的代码:
~~~
dscores = probs
dscores[range(num_examples),y] -= 1
dscores /= num_examples
~~~
我们计算的得分`scores = np.dot(X, W)+b`,因为上面已经算好了`scores`的梯度`dscores`,我们现在可以回传梯度计算W和b了:
~~~
dW = np.dot(X.T, dscores)
db = np.sum(dscores, axis=0, keepdims=True)
#得记着正则化梯度哈
dW += reg*W
~~~
我们通过矩阵的乘法得到梯度部分,权重W的部分加上了正则化项的梯度。因为我们在设定正则化项的时候用了系数`0.5`(ddw(12λw2)=λw),因此直接用`reg*W`就可以表示出正则化的梯度部分。
#### 3.5 参数迭代与更新
在得到所需的所有部分之后,我们就可以进行参数更新了:
~~~
#参数迭代更新
W += -step_size * dW
b += -step_size * db
~~~
#### 3.6 大杂合:训练SoftMax分类器
~~~
#代码部分组一起,训练线性分类器
#随机初始化参数
W = 0.01 * np.random.randn(D,K)
b = np.zeros((1,K))
#需要自己敲定的步长和正则化系数
step_size = 1e-0
reg = 1e-3 #正则化系数
#梯度下降迭代循环
num_examples = X.shape[0]
for i in xrange(200):
# 计算类别得分, 结果矩阵为[N x K]
scores = np.dot(X, W) + b
# 计算类别概率
exp_scores = np.exp(scores)
probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True) # [N x K]
# 计算损失loss(包括互熵损失和正则化部分)
corect_logprobs = -np.log(probs[range(num_examples),y])
data_loss = np.sum(corect_logprobs)/num_examples
reg_loss = 0.5*reg*np.sum(W*W)
loss = data_loss + reg_loss
if i % 10 == 0:
print "iteration %d: loss %f" % (i, loss)
# 计算得分上的梯度
dscores = probs
dscores[range(num_examples),y] -= 1
dscores /= num_examples
# 计算和回传梯度
dW = np.dot(X.T, dscores)
db = np.sum(dscores, axis=0, keepdims=True)
dW += reg*W # 正则化梯度
#参数更新
W += -step_size * dW
b += -step_size * db
~~~
得到结果:
~~~
iteration 0: loss 1.096956
iteration 10: loss 0.917265
iteration 20: loss 0.851503
iteration 30: loss 0.822336
iteration 40: loss 0.807586
iteration 50: loss 0.799448
iteration 60: loss 0.794681
iteration 70: loss 0.791764
iteration 80: loss 0.789920
iteration 90: loss 0.788726
iteration 100: loss 0.787938
iteration 110: loss 0.787409
iteration 120: loss 0.787049
iteration 130: loss 0.786803
iteration 140: loss 0.786633
iteration 150: loss 0.786514
iteration 160: loss 0.786431
iteration 170: loss 0.786373
iteration 180: loss 0.786331
iteration 190: loss 0.786302
~~~
190次循环之后,结果大致收敛了。我们评估一下准确度:
~~~
#评估准确度
scores = np.dot(X, W) + b
predicted_class = np.argmax(scores, axis=1)
print 'training accuracy: %.2f' % (np.mean(predicted_class == y))
~~~
输出结果为49%。不太好,对吧?实际上也是可理解的,你想想,一份螺旋形的数据,你偏执地要用一个线性分类器去分割,不管怎么调整这个线性分类器,都非常非常困难。我们可视化一下数据看看决策边界(decision boundaries):
![线性分类器与决策边界](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad727e80.png "")
### 4.使用神经网络分类
从刚才的例子里可以看出,一个线性分类器,在现在的数据集上效果并不好。我们知道神经网络可以做非线性的分割,那我们就试试神经网络,看看会不会有更好的效果。对于这样一个简单问题,我们用单隐藏层的神经网络就可以了,这样一个神经网络我们需要2层的权重和偏移量:
~~~
# 初始化参数
h = 100 # 隐层大小(神经元个数)
W = 0.01 * np.random.randn(D,h)
b = np.zeros((1,h))
W2 = 0.01 * np.random.randn(h,K)
b2 = np.zeros((1,K))
~~~
然后前向计算的过程也稍有一些变化:
~~~
#2层神经网络的前向计算
hidden_layer = np.maximum(0, np.dot(X, W) + b) # 用的 ReLU单元
scores = np.dot(hidden_layer, W2) + b2
~~~
注意到这里,和之前线性分类器中的得分计算相比,多了一行代码计算,我们首先计算第一层神经网络结果,然后作为第二层的输入,计算最后的结果。哦,对了,代码里大家也看的出来,我们这里使用的是ReLU神经单元。
其他的东西都没太大变化。我们依旧按照之前的方式去计算loss,然后计算梯度`dscores`。不过反向回传梯度的过程形式上也有一些小小的变化。我们看下面的代码,可能觉得和Softmax分类器里面看到的基本一样,但注意到我们用`hidden_layer`替换掉了之前的`X`:
~~~
# 梯度回传与反向传播
# 对W2和b2的第一次计算
dW2 = np.dot(hidden_layer.T, dscores)
db2 = np.sum(dscores, axis=0, keepdims=True)
~~~
恩,并没有完事啊,因为`hidden_layer`本身是一个包含其他参数和数据的函数,我们得计算一下它的梯度:
~~~
dhidden = np.dot(dscores, W2.T)
~~~
现在我们有隐层输出的梯度了,下一步我们要反向传播到ReLU神经元了。不过这个计算非常简单,因为r=max(0,x),同时我们又有drdx=1(x>0)。用链式法则串起来后,我们可以看到,回传的梯度大于0的时候,经过ReLU之后,保持原样;如果小于0,那本次回传就到此结束了。因此,我们这一部分非常简单:
~~~
#梯度回传经过ReLU
dhidden[hidden_layer <= 0] = 0
~~~
终于,翻山越岭,回到第一层,拿到总的权重和偏移量的梯度:
~~~
dW = np.dot(X.T, dhidden)
db = np.sum(dhidden, axis=0, keepdims=True)
~~~
来,来,来。组一组,我们把整个神经网络的过程串起来:
~~~
# 随机初始化参数
h = 100 # 隐层大小
W = 0.01 * np.random.randn(D,h)
b = np.zeros((1,h))
W2 = 0.01 * np.random.randn(h,K)
b2 = np.zeros((1,K))
# 手动敲定的几个参数
step_size = 1e-0
reg = 1e-3 # 正则化参数
# 梯度迭代与循环
num_examples = X.shape[0]
for i in xrange(10000):
hidden_layer = np.maximum(0, np.dot(X, W) + b) #使用的ReLU神经元
scores = np.dot(hidden_layer, W2) + b2
# 计算类别概率
exp_scores = np.exp(scores)
probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True) # [N x K]
# 计算互熵损失与正则化项
corect_logprobs = -np.log(probs[range(num_examples),y])
data_loss = np.sum(corect_logprobs)/num_examples
reg_loss = 0.5*reg*np.sum(W*W) + 0.5*reg*np.sum(W2*W2)
loss = data_loss + reg_loss
if i % 1000 == 0:
print "iteration %d: loss %f" % (i, loss)
# 计算梯度
dscores = probs
dscores[range(num_examples),y] -= 1
dscores /= num_examples
# 梯度回传
dW2 = np.dot(hidden_layer.T, dscores)
db2 = np.sum(dscores, axis=0, keepdims=True)
dhidden = np.dot(dscores, W2.T)
dhidden[hidden_layer <= 0] = 0
# 拿到最后W,b上的梯度
dW = np.dot(X.T, dhidden)
db = np.sum(dhidden, axis=0, keepdims=True)
# 加上正则化梯度部分
dW2 += reg * W2
dW += reg * W
# 参数迭代与更新
W += -step_size * dW
b += -step_size * db
W2 += -step_size * dW2
b2 += -step_size * db2
~~~
输出结果:
~~~
iteration 0: loss 1.098744
iteration 1000: loss 0.294946
iteration 2000: loss 0.259301
iteration 3000: loss 0.248310
iteration 4000: loss 0.246170
iteration 5000: loss 0.245649
iteration 6000: loss 0.245491
iteration 7000: loss 0.245400
iteration 8000: loss 0.245335
iteration 9000: loss 0.245292
~~~
现在的训练准确度为:
~~~
#计算分类准确度
hidden_layer = np.maximum(0, np.dot(X, W) + b)
scores = np.dot(hidden_layer, W2) + b2
predicted_class = np.argmax(scores, axis=1)
print 'training accuracy: %.2f' % (np.mean(predicted_class == y))
~~~
你猜怎么着,准确度为98%,我们可视化一下数据和现在的决策边界:
![神经网络决策边界](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad74dce1.png "")
看起来效果比之前好多了,这充分地印证了我们在[手把手入门神经网络系列(1)_从初等数学的角度初探神经网络](http://blog.csdn.net/han_xiaoyang/article/details/50100367)中提到的,神经网络对于空间强大的分割能力,对于非线性的不规则图形,能有很强的划分区域和区分类别能力。
(8)_神经网络训练与注意点
最后更新于:2022-04-01 14:21:51
作者:[寒小阳](http://blog.csdn.net/han_xiaoyang?viewmode=contents)
时间:2016年1月。
出处:[http://blog.csdn.net/han_xiaoyang/article/details/50521064](http://blog.csdn.net/han_xiaoyang/article/details/50521064)
声明:版权所有,转载请联系作者并注明出处
### 1.训练
在前一节当中我们讨论了神经网络静态的部分:包括神经网络结构、神经元类型、数据部分、损失函数部分等。这个部分我们集中讲讲动态的部分,主要是训练的事情,集中在实际工程实践训练过程中要注意的一些点,如何找到最合适的参数。
#### 1.1 关于梯度检验
[之前的博文](http://blog.csdn.net/han_xiaoyang/article/details/50178505)我们提到过,我们需要比对数值梯度和解析法求得的梯度,实际工程中这个过程非常容易出错,下面提一些小技巧和注意点:
使用中心化公式,这一点我们之前也说过,使用如下的数值梯度计算公式:
df(x)dx=f(x+h)−f(x−h)2h(好的形式)
而不是
df(x)dx=f(x+h)−f(x)h(非中心化形式,不要用)
即使看似上面的形式有着2倍的计算量,但是如果你有兴趣用把公式中的f(x+h)和f(x−h)展开的话,你会发现上面公式出错率大概是O(h2)级别的,而下面公式则是O(h),注意到`h`是很小的数,因此显然上面的公式要精准得多。
使用相对误差做比较,这是实际工程中需要提到的另外一点,在我们得到数值梯度f′n和解析梯度f′a之后,我们如何去比较两者?第一反应是作差|f′a−f′n|对吧,或者顶多求一个平方。但是用绝对值是不可靠的,假如两个梯度的绝对值都在1.0左右,那么我们可以认为1e-4这样一个差值是非常小的,但是如果两个梯度本身就是1e-4级别的,那这个差值就相当大了。所以我们考虑相对误差:
∣f′a−f′n∣max(∣f′a∣,∣f′n∣)
加max项的原因很简单:整体形式变得简单和对称。再提个小醒,别忘了避开分母中两项都为0的情况。OK,对于相对误差而言:
- 相对误差>1e-2意味着你的实现肯定是有问题的
- 1e-2>相对误差>1e-4,你会有点担心
- 1e-4>相对误差,基本是OK的,但是要注意极端情况(使用tanh或者softmax时候出现kinks)那还是太大
- 1e-7>相对误差,放心大胆使用
哦,对对,还有一点,随着神经网络层数增多,相对误差是会增大的。这意味着,对于10层的神经网络,其实相对误差也许在1e-2级别就已经是可以正常使用的了。
**使用双精度浮点数**。如果你使用单精度浮点数计算,那你的实现可能一点问题都没有,但是相对误差却很大。实际工程中出现过,从单精度切到双精度,相对误差立马从1e-2降到1e-8的情况。
**要留意浮点数的范围**。一篇很好的文章是[What Every Computer Scientist Should Know About Floating-Point Arithmetic](http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html)。我们得保证计算时,所有的数都在浮点数的可计算范围内,太小的值(比如h)会带来计算上的问题。
**Kinks**。它指的是一种会导致数值梯度和解析梯度不一致的情况。会出现在使用ReLU或者类似的神经单元上时,对于很小的负数,比如x=-1e-6,因为x<0,所以解析梯度是绝对为0的,但是对于数值梯度而言,加入你计算f(x+h),取的h>1e-6,那就跳到大于0的部分了,这样数值梯度就一定和解析梯度不一样了。而且这个并不是极端情况哦,对于一个像CIFAR-10这样级别的数据集,因为有50000个样本,会有450000个max(0,x),会出现很多的kinks。
不过我们可以监控max里的2项,比较大的那项如果存在跃过0的情况,那就要注意了。
**设定步长h要小心**。h肯定不能特别大,这个大家都知道对吧。但我并不是说h要设定的非常小,其实h设定的非常小也会有问题,因为h太小程序可能会有精度问题。很有意思的是,有时候在实际情况中h如果从非常小调为1e-4或者1e-6反倒会突然计算变得正常。
**不要让正则化项盖过数据项**。有时候会出现这个问题,因为损失函数是数据损失部分与正则化部分的求和。因此要特别注意正则化部分,你可以想象下,如果它盖过了数据部分,那么主要的梯度来源于正则化项,那这样根本就做不到正常的梯度回传和参数迭代更新。所以即使在检查数据部分的实现是否正确,也得先关闭正则化部分(系数λ设为0),再检查。
**注意dropout和其他参数**。在检查数值梯度和解析梯度的时候,如果不把dropout和其他参数都『关掉』的话,两者之间是一定会有很大差值的。不过『关掉』它们的负面影响是,没有办法检查这些部分的梯度是否正确。所以,一个合理的方式是,在计算f(x+h)和f(x−h)之前,随机初始化`x`,然后再计算解析梯度。
**关于只检查几个维度**。在实际情况中,梯度可能有上百万维参数。因此每个维度都检查一遍就不太现实了,一般都是只检查一些维度,然后假定其他的维度也都正确。要小心一点:要保证这些维度的每个参数都检查对比过了。
#### 1.2 训练前的检查工作
在开始训练之前,我们还得做一些检查,来确保不会运行了好一阵子,才发现计算代价这么大的训练其实并不正确。
**在初始化之后看一眼loss**。其实我们在用很小的随机数初始化神经网络后,第一遍计算loss可以做一次检查(当然要记得把正则化系数设为0)。以CIFAR-10为例,如果使用Softmax分类器,我们预测应该可以拿到值为2.302左右的初始loss(因为10个类别,初始概率应该都未0.1,Softmax损失是-log(正确类别的概率):-ln(0.1)=2.302)。
**加回正则项**,接着我们把正则化系数设为正常的小值,加回正则化项,这时候再算损失/loss,应该比刚才要大一些。
**试着去拟合一个小的数据集**。最后一步,也是很重要的一步,在对大数据集做训练之前,我们可以先训练一个小的数据集(比如20张图片),然后看看你的神经网络能够做到0损失/loss(当然,是指的正则化系数为0的情况下),因为如果神经网络实现是正确的,在无正则化项的情况下,完全能够过拟合这一小部分的数据。
#### 1.3 训练过程中的监控
开始训练之后,我们可以通过监控一些指标来了解训练的状态。我们还记得有一些参数是我们认为敲定的,比如学习率,比如正则化系数。
- **损失/loss随每轮完整迭代后的变化**
下面这幅图表明了不同的学习率下,我们每轮完整迭代(这里的一轮完整迭代指的是所有的样本都被过了一遍,因为随机梯度下降中batch size的大小设定可能不同,因此我们不选每次mini-batch迭代为周期)过后的loss应该呈现的变化状况:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad5d07f6.png)
合适的学习率可以保证每轮完整训练之后,loss都减小,且能在一段时间后降到一个较小的程度。太小的学习率下loss减小的速度很慢,如果太激进,设置太高的学习率,开始的loss减小速度非常可观,可是到了某个程度之后就不再下降了,在离最低点一段距离的地方反复,无法下降了。下图是实际训练CIFAR-10的时候,loss的变化情况:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad6024d5.png)
大家可能会注意到上图的曲线有一些上下跳动,不稳定,这和随机梯度下降时候设定的batch size有关系。batch size非常小的情况下,会出现很大程度的不稳定,如果batch size设定大一些,会相对稳定一点。
- **训练集/验证集上的准确度**
然后我们需要跟踪一下训练集和验证集上的准确度状况,以判断分类器所处的状态(过拟合程度如何):
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad62166b.png)
随着时间推进,训练集和验证集上的准确度都会上升,如果训练集上的准确度到达一定程度后,两者之间的差值比较大,那就要注意一下,可能是过拟合现象,如果差值不大,那说明模型状况良好。
- **权重:权重更新部分 的比例**
最后一个需要留意的量是权重更新幅度和当前权重幅度的壁纸。注意哦,是权重更新部分,不一定是计算出来的梯度哦(比如训练用的vanilla sgd,那这个值就是`梯度`和`学习率`的乘积)。最好对于每组参数都独立地检查这个比例。我们没法下定论,但是在之前的工程实践中,一个合适的比例大概是1e-3。如果你得到的比例比这个值小很多,那么说明学习率设定太低了,反之则是设定太高了。
- **每一层的 激励/梯度值 分布**
如果初始化不正确,那整个训练过程会越来越慢,甚至直接停掉。不过我们可以很容易发现这个问题。体现最明显的数据是每一层的激励和梯度的方差(波动状况)。举个例子说,如果初始化不正确,很有可能从前到后逐层的激励(激励函数的输入部分)方差变化是如下的状况:
~~~
# 我们用标准差为0.01均值为0的高斯分布值来初始化权重(这不合理)
Layer 0: Variance: 1.005315e+00
Layer 1: Variance: 3.123429e-04
Layer 2: Variance: 1.159213e-06
Layer 3: Variance: 5.467721e-10
Layer 4: Variance: 2.757210e-13
Layer 5: Variance: 3.316570e-16
Layer 6: Variance: 3.123025e-19
Layer 7: Variance: 6.199031e-22
Layer 8: Variance: 6.623673e-25
~~~
大家看一眼上述的数值,就会发现,从前往后,激励值波动逐层降得非常厉害,这也就意味着反向算法中,计算回传梯度的时候,梯度都要接近0了,因此参数的迭代更新几乎就要衰减没了,显然不太靠谱。我们按照[上一讲](http://blog.csdn.net/han_xiaoyang/article/details/50451460)中提到的方式正确初始化权重,再逐层看激励/梯度值的方差,会发现它们的方差衰减没那么厉害,近似在一个级别:
~~~
# 重新正确设定权重:
Layer 0: Variance: 1.002860e+00
Layer 1: Variance: 7.015103e-01
Layer 2: Variance: 6.048625e-01
Layer 3: Variance: 8.517882e-01
Layer 4: Variance: 6.362898e-01
Layer 5: Variance: 4.329555e-01
Layer 6: Variance: 3.539950e-01
Layer 7: Variance: 3.809120e-01
Layer 8: Variance: 2.497737e-01
~~~
再看逐层的激励波动情况,你会发现即使到最后一层,网络也还是『活跃』的,意味着反向传播中回传的梯度值也是够的,神经网络是一个积极learning的状态。
- **首层的可视化**
最后再提一句,如果神经网络是用在图像相关的问题上,那么把首层的特征和数据画出来(可视化)可以帮助我们了解训练是否正常:
![可视化首层](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad63bca7.png "")
上图的左右是一个正常和不正常情况下首层特征的可视化对比。左边的图中特征噪点较多,图像很『浑浊』,预示着可能训练处于『病态』过程:也许是学习率设定不正常,或者正则化系数设定太低了,或者是别的原因,可能神经网络不会收敛。右边的图中,特征很平滑和干净,同时相互间的区分度较大,这表明训练过程比较正常。
#### 1.4 关于参数更新部分的注意点
当我们确信解析梯度实现正确后,那就该在后向传播算法中使用它更新权重参数了。就单参数更新这个部分,也是有讲究的:
说起来,神经网络的最优化这个子话题在深度学习研究领域还真是很热。下面提一下大神们的论文中提到的方法,很多在实际应用中还真是很有效也很常用。
#### 1.4.1 随机梯度下降与参数更新
**vanilla update**
这是最简单的参数更新方式,拿到梯度之后,乘以设定的学习率,用现有的权重减去这个部分,得到新的权重参数(因为梯度表示变化率最大的增大方向,减去这个值之后,损失函数值才会下降)。记`x`为权重参数向量x,而梯度为`dx`,然后我们设定学习率为`learning_rate`,则最简单的参数更新大家都知道:
~~~
# Vanilla update
x += - learning_rate * dx
~~~
当然`learning_rate`是我们自己敲定的一个超变量值(在该更新方法中是全程不变的),而且数学上可以保证,当学习率足够低的时候,经这个过程迭代后,损失函数不会增加。
**Momentum update**
这是上面参数更新方法的一种小小的优化,通常说来,在深层次的神经网络中,收敛效率更高一些(速度更快)。这种参数更新方式源于物理学角度的优化。
~~~
# 物理动量角度启发的参数更新
v = mu * v - learning_rate * dx # 合入一部分附加速度
x += v # 更新参数
~~~
这里`v`是初始化为0的一个值,`mu`是我们敲定的另外一个超变量(最常见的设定值为0.9,物理含义和摩擦力系数相关),一个比较粗糙的理解是,(随机)梯度下降可以看做从山上下山到山底的过程,这种方式,相当于在下山的过程中,加上了一定的摩擦阻力,消耗掉一小部分动力系统的能量,这样会比较高效地在山底停住,而不是持续震荡。对了,其实我们也可以用交叉验证来选择最合适的`mu`值,一般我们会从[0.5, 0.9, 0.95, 0.99]里面选出最合适的。
**Nesterov Momentum**
这是momentum update的一个不同的版本,最近也用得很火。咳咳,据称,这种参数更新方法,有更好的凸函数和凸优化理论基础,而实际中的收敛效果也略优于momentum update。
此处的深层次原理,博主表示智商有点捉急…有兴趣的同学可以看看以下的2个材料:
- Yoshua Bengio大神的[Advances in optimizing Recurrent Networks](http://arxiv.org/pdf/1212.0901v2.pdf)3.5节
- [Ilya Sutskever’s thesis](http://www.cs.utoronto.ca/~ilya/pubs/ilya_sutskever_phd_thesis.pdf)7.2节
它的思想对应着如下的代码:
~~~
x_ahead = x + mu * v
# 考虑到这个时候的x已经有一些变化了
v = mu * v - learning_rate * dx_ahead
x += v
~~~
工程上更实用的一个版本是:
~~~
v_prev = v # 当前状态先存储起来
v = mu * v - learning_rate * dx # 依旧按照Momentum update的方式更新
x += -mu * v_prev + (1 + mu) * v # 新的更新方式
~~~
#### 1.4.2 衰减学习率
在实际训练过程中,随着训练过程推进,逐渐衰减学习率是很有必要的。我们继续回到下山的场景中,刚下山的时候,可能离最低点很远,那我步子迈大一点也没什么关系,可是快到山脚了,我还激进地大步飞奔,一不小心可能就迈过去了。所以还不如随着下山过程推进,逐步减缓一点点步伐。不过这个『火候』确实要好好把握,衰减太慢的话,最低段震荡的情况依旧;衰减太快的话,整个系统下降的『动力』衰减太快,很快就下降不动了。下面提一些常见的学习率衰减方式:
- **步伐衰减**:这是很常见的一个衰减模式,每过一轮完整的训练周期(所有的图片都过了一遍)之后,学习率下降一些。比如比较常见的一个衰减率可能是每20轮完整训练周期,下降10%。不过最合适的值还真是依问题不同有变化。如果你在训练过程中,发现交叉验证集上呈现很高的错误率,还一直不下降,你可能就可以考虑考虑调整一下(衰减)学习率了。
- **指数级别衰减**:数学形式为α=α0e−kt,其中α0,k是需要自己敲定的超参数,t是迭代轮数。
- **1/t衰减**:有着数学形式为α=α0/(1+kt)的衰减模式,其中α0,k是需要自己敲定的超参数,t是迭代轮数。
实际工程实践中,大家还是更倾向于使用**步伐衰减**,因为它包含的超参数少一些,计算简单一些,可解释性稍微高一点。
#### 1.4.3 二次迭代方法
最优化问题里还有一个非常有名的[牛顿法](http://en.wikipedia.org/wiki/Newton%27s_method_in_optimization),它按照如下的方式进行迭代更新参数:
x←x−[Hf(x)]−1∇f(x)
这里的Hf(x)是[Hessian矩阵](http://en.wikipedia.org/wiki/Hessian_matrix),是函数的二阶偏微分。而∇f(x)和梯度下降里看到的一样,是一个梯度向量。直观理解是Hessian矩阵描绘出了损失函数的曲度,因此能让我们更高效地迭代和靠近最低点:乘以Hessian矩阵进行参数迭代会让在曲度较缓的地方,会用更激进的步长更新参数,而在曲度很陡的地方,步伐会放缓一些。因此相对一阶的更新算法,在这点上它还是有很足的优势的。
比较尴尬的是,实际深度学习过程中,直接使用二次迭代的方法并不是很实用。原因是直接计算Hessian矩阵是一个非常耗时耗资源的过程。举个例子说,一个一百万参数的神经网络的Hessian矩阵维度为[1000000*1000000],算下来得占掉3725G的内存。当然,我们有[L-BFGS](http://en.wikipedia.org/wiki/Limited-memory_BFGS)这种近似Hessian矩阵的算法,可以解决内存问题。但是L-BFGS一般在全部数据集上计算,而不像我们用的mini-batch SGD一样在小batch小batch上迭代。现在有很多人在努力研究这个问题,试图让L-BFGS也能以mini-batch的方式稳定迭代更新。但就目前而言,大规模数据上的深度学习很少用到L-BFGS或者类似的二次迭代方法,倒是随机梯度下降这种简单的算法被广泛地使用着。
感兴趣的同学可以参考以下文献:
- [On Optimization Methods for Deep Learning](http://ai.stanford.edu/~ang/papers/icml11-OptimizationForDeepLearning.pdf):2011年的论文比较随机梯度下降和L-BFGS
- [Large Scale Distributed Deep Networks](http://research.google.com/archive/large_deep_networks_nips2012.html):google brain组的论文,比较随机梯度下降和L-BFGS在大规模分布式优化上的差别。
- [SFO](http://arxiv.org/abs/1311.2115)算法试图结合随机梯度下降和L-BFGS的优势。
#### 1.4.4 逐参更新学习率
到目前为止大家看到的学习率更新方式,都是全局使用同样的学习率。调整学习率是一件很费时同时也容易出错的事情,因此大家一直希望有一种学习率自更新的方式,甚至可以细化到逐参数更新。现在确实有一些这种方法,其中大多数还需要额外的超参数设定,优势是在大多数超参数设定下,效果都比使用写死的学习率要好。下面稍微提一下常见的自适应方法(原谅博主底子略弱,没办法深入数学细节讲解):
**Adagrad**是Duchi等在论文[Adaptive Subgradient Methods for Online Learning and Stochastic Optimization](http://www.jmlr.org/papers/volume12/duchi11a/duchi11a.pdf)中提出的自适应学习率算法。简单代码实现如下:
~~~
# 假定梯度为dx,参数向量为x
cache += dx**2
x += - learning_rate * dx / np.sqrt(cache + 1e-8)
~~~
其中变量`cache`有着和梯度一样的维度,然后我们用这个变量持续累加梯度平方。之后这个值被用作参数更新步骤中的归一化。这种方法的好处是,对于高梯度的权重,它们的有效学习率被降低了;而小梯度的权重迭代过程中学习率提升了。而分母开根号这一步非常重要,不开根号的效果远差于开根号的情况。平滑参数`1e-8`避免了除以0的情况。
**RMSprop**是一种非常有效,然后好像还没有被公开发布的自适应学习率更新方法。有意思的是,现在使用这个方法的人,都引用的大神Geoff Hinton的coursera课程第6节的slide第29页。RMSProp方法对Adagrad算法做了一个简单的优化,以减缓它的迭代强度,它开方的部分cache做了一个平滑处理,大致的示意代码如下:
~~~
cache = decay_rate * cache + (1 - decay_rate) * dx**2
x += - learning_rate * dx / np.sqrt(cache + 1e-8)
~~~
这里的`decay_rate`是一个手动敲定的超参数,我们通常会在[0.9, 0.99, 0.999]中取值。需要特别注意的是,`x+=`这个累加的部分和Adagrad是完全一样的,但是`cache`本身是迭代变化的。
另外的方法还有:
- Matthew Zeiler提出的[Adadelta](http://arxiv.org/abs/1212.5701)
- [Adam: A Method for Stochastic Optimization](http://arxiv.org/abs/1412.6980)
- [Unit Tests for Stochastic Optimization](http://arxiv.org/abs/1312.6055)
下图是上述提到的多种参数更新方法下,损失函数最优化的示意图:
![参数更新1](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad660eb3.gif "")
![参数更新2](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad6b50a9.gif "")
#### 1.5 超参数的设定与优化
神经网络的训练过程中,不可避免地要和很多超参数打交道,这是我们需要手动设定的,大致包括:
- 初始学习率
- 学习率衰减程度
- 正则化系数/强度(包括l2正则化强度,dropout比例)
对于大的深层次神经网络而言,我们需要很多的时间去训练。因此在此之前我们花一些时间去做超参数搜索,以确定最佳设定是非常有必要的。最直接的方式就是在框架实现的过程中,设计一个会持续变换超参数实施优化,并记录每个超参数下每一轮完整训练迭代下的验证集状态和效果。实际工程中,神经网络里确定这些超参数,我们一般很少使用n折交叉验证,一般使用一份固定的交叉验证集就可以了。
一般对超参数的尝试和搜索都是在log域进行的。例如,一个典型的学习率搜索序列就是`learning_rate = 10 ** uniform(-6, 1)`。我们先生成均匀分布的序列,再以10为底做指数运算,其实我们在正则化系数中也做了一样的策略。比如常见的搜索序列为[0.5, 0.9, 0.95, 0.99]。另外还得注意一点,如果交叉验证取得的最佳超参数结果在分布边缘,要特别注意,也许取的均匀分布范围本身就是不合理的,也许扩充一下这个搜索范围会有更好的参数。
#### 1.6 模型融合与优化
实际工程中,一个能有效提高最后神经网络效果的方式是,训练出多个独立的模型,在预测阶段选结果中的众数。模型融合能在一定程度上缓解过拟合的现象,对最后的结果有一定帮助,我们有一些方式可以得到同一个问题的不同独立模型:
- **使用不同的初始化参数**。先用交叉验证确定最佳的超参数,然后选取不同的初始值进行训练,结果模型能有一定程度的差别。
- **选取交叉验证排序靠前的模型**。在用交叉验证确定超参数的时候,选取top的部分超参数,分别进行训练和建模。
- **选取训练过程中不同时间点的模型**。神经网络训练确实是一件非常耗时的事情,因此有些人在模型训练到一定准确度之后,取不同的时间点的模型去做融合。不过比较明显的是,这样模型之间的差异性其实比较小,好处是一次训练也可以有模型融合的收益。
还有一种常用的有效改善模型效果的方式是,对于训练后期,保留几份中间模型权重和最后的模型权重,对它们求一个平均,再在交叉验证集上测试结果。通常都会比直接训练的模型结果高出一两个百分点。直观的理解是,对于碗状的结构,有很多时候我们的权重都是在最低点附近跳来跳去,而没法真正到达最低点,而两个最低点附近的位置求平均,会有更高的概率落在离最低点更近的位置。
### 2. 总结
- 用一部分的数据测试你梯度计算是否正确,注意提到的注意点。
- 检查你的初始权重是否合理,在关掉正则化项的系统里,是否可以取得100%的准确度。
- 在训练过程中,对损失函数结果做记录,以及训练集和交叉验证集上的准确度。
- 最常见的权重更新方式是SGD+Momentum,推荐试试RMSProp自适应学习率更新算法。
- 随着时间推进要用不同的方式去衰减学习率。
- 用交叉验证等去搜索和找到最合适的超参数。
- 记得也做做模型融合的工作,对结果有帮助。
(7)_神经网络数据预处理,正则化与损失函数
最后更新于:2022-04-01 14:21:49
作者:[寒小阳](http://blog.csdn.net/han_xiaoyang?viewmode=contents)
时间:2016年1月。
出处:[http://blog.csdn.net/han_xiaoyang/article/details/50451460](http://blog.csdn.net/han_xiaoyang/article/details/50451460)
声明:版权所有,转载请联系作者并注明出处
### 1. 引言
上一节我们讲完了各种激励函数的优缺点和选择,以及网络的大小以及正则化对神经网络的影响。这一节我们讲一讲输入数据预处理、正则化以及损失函数设定的一些事情。
### 2. 数据与网络的设定
前一节提到前向计算涉及到的组件(主要是神经元)设定。神经网络结构和参数设定完毕之后,我们就得到得分函数/score function(忘记的同学们可以翻看一下之前的[博文](http://blog.csdn.net/han_xiaoyang/article/details/49999583)),总体说来,一个完整的神经网络就是在不断地进行线性映射(权重和input的内积)和非线性映射(部分激励函数作用)的过程。这一节我们会展开来讲讲数据预处理,权重初始化和损失函数的事情。
#### 2.1 数据预处理
在卷积神经网处理图像问题的时候,图像数据有3种常见的预处理可能会用到,如下。我们假定数据表示成矩阵为`X`,其中我们假定`X`是[N*D]维矩阵(N是样本数据量,D为单张图片的数据向量长度)。
- **去均值**,这是最常见的图片数据预处理,简单说来,它做的事情就是,对待训练的每一张图片的特征,都减去全部训练集图片的特征均值,这么做的直观意义就是,我们把输入数据各个维度的数据都中心化到0了。使用python的numpy工具包,这一步可以用`X -= np.mean(X, axis = 0)`轻松实现。当然,其实这里也有不同的做法:简单一点,我们可以直接求出所有像素的均值,然后每个像素点都减掉这个相同的值;稍微优化一下,我们在RGB三个颜色通道分别做这件事。
- **归一化**,归一化的直观理解含义是,我们做一些工作去保证所有的维度上数据都在一个变化幅度上。通常我们有两种方法来实现归一化。一个是在数据都去均值之后,每个维度上的数据都除以这个维度上数据的标准差(`X /= np.std(X, axis = 0)`)。另外一种方式是我们除以数据绝对值最大值,以保证所有的数据归一化后都在-1到1之间。多说一句,其实在任何你觉得各维度幅度变化非常大的数据集上,你都可以考虑归一化处理。不过对于图像而言,其实这一步反倒可做可不做,因为大家都知道,像素的值变化区间都在[0,255]之间,所以其实图像输入数据天生幅度就是一致的。
上述两个操作对于数据的作用,画成示意图,如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad506f5f.png)
- **PCA和白化/whitening**,这是另外一种形式的数据预处理。在经过去均值操作之后,我们可以计算数据的协方差矩阵,从而可以知道数据各个维度之间的相关性,简单示例代码如下:
~~~
# 假定输入数据矩阵X是[N*D]维的
X -= np.mean(X, axis = 0) # 去均值
cov = np.dot(X.T, X) / X.shape[0] # 计算协方差
~~~
得到的结果矩阵中元素(i,j)表示原始数据中,第i维和第j维直接爱你的相关性。有意思的是,其实协方差矩阵的对角线包含了每个维度的变化幅度。另外,我们都知道协方差矩阵是对称的,我们可以在其上做矩阵奇异值分解(SVD factorization):
~~~
U,S,V = np.linalg.svd(cov)
~~~
其中U为特征向量,我们如果相对原始数据(去均值之后)做去相关操作,只需要进行如下运算:
~~~
Xrot = np.dot(X, U)
~~~
这么理解一下可能更好,U是一组正交基向量。所以我们可以看做把原始数据`X`投射到这组维度保持不变的正交基底上,从而也就完成了对原始数据的去相关。如果去相关之后你再求一下`Xrot`的协方差矩阵,你会发现这时候的协方差矩阵是一个对角矩阵了。而numpy中的`np.linalg.svd`更好的一个特性是,它返回的U是对特征值排序过的,这也就意味着,我们可以用它进行降维操作。我们可以只取top的一些特征向量,然后做和原始数据做矩阵乘法,这个时候既降维减少了计算量,同时又保存下了绝大多数的原始数据信息,这就是所谓的主成分分析/PCA:
~~~
Xrot_reduced = np.dot(X, U[:,:100])
~~~
这个操作之后,我们把原始数据集矩阵从[N * D]降维到[N*100],保存了前100个能包含绝大多数数据信息的维度。实际应用中,你在PCA降维之后的数据集上,做各种机器学习的训练,在节省空间和时间的前提下,依旧能有很好的训练准确度。
最后我们再提一下whitening操作。所谓whitening,就是把各个特征轴上的数据除以特征向量,从而达到在每个特征轴上都归一化幅度的结果。whitening变换的几何意义和理解是,如果输入的数据是多变量高斯,那whitening之后的 数据是一个均值为0而不同方差的高斯矩阵。这一步简单代码实现如下:
~~~
#白化数据
Xwhite = Xrot / np.sqrt(S + 1e-5)
~~~
提个醒:whitening操作会有严重化噪声的可能。注意到我们在上述代码中,分母的部分加入了一个很小的数1e-5,以防止出现除以0的情况。但是数据中的噪声部分可能会因whitening操作而变大,因为这个操作的本质是把输入的每个维度都拉到差不多的幅度,那么本不相关的有微弱幅度变化的噪声维度,也被拉到了和其他维度同样的幅度。当然,我们适当提高坟墓中的安全因子(1e-5)可以在一定程度上缓解这个问题。
下图为原始数据到去相关到白化之后的数据分布示意图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad52f824.png)
我们来看看真实数据集上的操作与得到的结果,也许能对这些过程有更清晰一些的认识。大家都还记得CIFAR-10图像数据集吧。训练集大小为50000 * 3072,也就是说,每张图片都被展成一个3072维度的列向量了。然后我们对原始50000*3072数据矩阵做SVD分解,进行上述一些操作,再可视化一下,得到的结果示意图如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad55224d.png)
我们稍加解释一下,最左边是49张原始图片;左起第2幅图是最3072个特征向量中最top的144个,这144个特征向量包含了绝大多数数据变量信息,而其实它们代表的是图片中低频的信息;左起第3幅图表示PCA降维操作之后的49张图片,使用上面求得的144个特征向量。我们可以观察到图片好像被蒙上了一层东西一样,模糊化了,这也就表明了我们的top144个特征向量捕捉到的都是图像的低频信息,不过我们发现图像的绝大多数信息确实被保留下来了;最右图是whitening的144个数通过乘以`U.transpose()[:144,:]`还原回图片的样子,有趣的是,我们发现,现在低频信息基本都被滤掉了,剩下一些高频信息被放大呈现。
**实际工程中**,因为这个部分讲到数据预处理,我们就把基本的几种数据预处理都讲了一遍,但实际卷积神经网中,我们并没有用到去相关和whitening操作。当然,去均值是非常非常重要的,而每个像素维度的归一化也是常用的操作。
**特别说明**,需要特别说明的一点是,上述的预处理操作,一定都是在训练集上先预算的,然后应用在交叉验证/测试集上的。举个例子,有些同学会先把所有的图片放一起,求均值,然后减掉均值,再把这份数据分作训练集和测试集,这是不对的亲!!!
#### 2.2 权重初始化
我们之前已经看过一个完整的神经网络,是怎么样通过神经元和连接搭建起来的,以及如何对数据做预处理。在训练神经网络之前,我们还有一个任务要做,那就是初始化参数。
**错误的想法:全部初始化为0**,有些同学说,那既然要训练和收敛嘛,初始值就随便设定,简单一点就全设为0好了。亲,这样是绝对不行的!!!为啥呢?我们在神经网络训练完成之前,是不可能预知神经网络最后的权重具体结果的,但是根据我们归一化后的数据,我们可以假定,大概有半数左右的权重是正数,而另外的半数是负数。但设定全部初始权重都为0的结果是,网络中每个神经元都计算出一样的结果,然后在反向传播中有一样的梯度结果,因此迭代之后的变化情况也都一样,这意味着这个神经网络的权重没有办法差异化,也就没有办法学习到东西。
**很小的随机数**,其实我们依旧希望初始的权重是较小的数,趋于0,但是就像我们刚刚讨论过的一样,不要真的是0。综合上述想法,在实际场景中,我们通常会把初始权重设定为非常小的数字,然后正负尽量一半一半。这样,初始的时候权重都是不一样的很小随机数,然后迭代过程中不会再出现迭代一致的情况。举个例子,我们可能可以这样初始化一个权重矩阵`W=0.0001*np.random.randn(D,H)`。这个初始化的过程,使得每个神经元的权重向量初始化为多维高斯中的随机采样向量,所以神经元的初始权重值指向空间中的随机方向。
**特别说明**:其实不一定更小的初始值会比大值有更好的效果。我们这么想,一个有着非常小的权重的神经网络在后向传播过程中,回传的梯度也是非常小的。这样回传的”信号”流会相对也较弱,对于层数非常多的深度神经网络,这也是一个问题,回传到最前的迭代梯度已经很小了。
**方差归一化**,上面提到的建议有一个小问题,对于随机初始化的神经元参数下的输出,其分布的方差随着输入的数量,会增长。我们实际上可以通过除以总输入数目的平方根,归一化每个神经元的输出方差到1。也就是说,我们倾向于初始化神经元的权重向量为`w = np.random.randn(n) / sqrt(n)`,其中n为输入数。
我们从数学的角度,简单解释一下,为什么上述操作可以归一化方差。考虑在激励函数之前的权重w与输入x的内积s=∑niwixi部分,我们计算一下s的方差:
Var(s)=Var(∑inwixi)=∑inVar(wixi)=∑in[E(wi)]2Var(xi)+E[(xi)]2Var(wi)+Var(xi)Var(wi)=∑inVar(xi)Var(wi)=(nVar(w))Var(x)
注意,这个推导的前2步用到了方差的性质。第3步我们假定输入均值为0,因此E[xi]=E[wi]=0。不过这是我们的一个假设,实际情况下并不一定是这样的,比如ReLU单元的均值就是正的。最后一步我们假定wi,xi是独立分布。我们想让s的方差和输入x的方差一致,因此我们想让w的方差取值为1/n,又因为我们有公式Var(aX)=a2Var(X),所以a应该取值为a=1/n‾‾‾√,numpy里的实现为`w = np.random.randn(n) / sqrt(n)`。
对于初始化权重还有一些类似的研究和建议,比如说Glorot在论文[Understanding the difficulty of training deep feedforward neural networks](http://jmlr.org/proceedings/papers/v9/glorot10a/glorot10a.pdf)就推荐使用能满足Var(w)=2/(nin+nout)的权重初始化。其中nin,nout是前一层和后一层的神经元个数。而另外一篇比较新的论文[Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification](http://arxiv.org/abs/1502.01852),则指出尤其对于ReLU神经元,我们初始化方差应该为2.0/n,也就是`w = np.random.randn(n) * sqrt(2.0/n)`,目前的神经网络中使用了很多ReLU单元,因此这个设定其实在实际应用中使用最多。
**偏移量/bias初始化**:相对而言,bias项初始化就简单一些。我们很多时候简单起见,直接就把它们都设为0.在ReLU单元中,有些同学会使用很小的数字(比如0.01)来代替0作为所有bias项的初始值,他们解释说这样也能保证ReLU单元一开始就是被激活的,因此反向传播过程中不会终止掉回传的梯度。不过似乎实际的实验过程中,这个优化并不是每次都能起到作用的,因此很多时候我们还是直接把bias项都初始化为0。
#### 2.3 正则化
在前一节里我们说了我们要通过正则化来控制神经网络,使得它不那么容易过拟合。有几种正则化的类型供选择:
-
**L2正则化**,这个我们之前就提到过,非常常见。实现起来也很简单,我们在损失函数里,加入对每个参数的惩罚度。也就是说,对于每个权重w,我们在损失函数里加入一项12λw2,其中λ是我们可调整的正则化强度。顺便说一句,这里在前面加上1/2的原因是,求导/梯度的时候,刚好变成λw而不是2λw。L2正则化理解起来也很简单,它对于特别大的权重有很高的惩罚度,以求让权重的分配均匀一些,而不是集中在某一小部分的维度上。我们再想想,加入L2正则化项,其实意味着,在梯度下降参数更新的时候,每个权重以W += -lambda*W的程度被拉向0。
-
**L1正则化**,这也是一种很常见的正则化形式。在L1正则化中,我们对于每个权重w的惩罚项为λ|w|。有时候,你甚至可以看到大神们混着L1和L2正则化用,也就是说加入惩罚项λ1∣w∣+λ2w2,L1正则化有其独特的特性,它会让模型训练过程中,权重特征向量逐渐地稀疏化,这意味着到最后,我们只留下了对结果影响最大的一部分权重,而其他不相关的输入(例如『噪声』)因为得不到权重被抑制。所以通常L2正则化后的特征向量是一组很分散的小值,而L1正则化只留下影响较大的权重。在实际应用中,如果你不是特别要求只保留部分特征,那么L2正则化通常能得到比L1正则化更好的效果
-
**最大范数约束**,另外一种正则化叫做最大范数约束,它直接限制了一个上行的权重边界,然后约束每个神经元上的权重都要满足这个约束。实际应用中是这样实现的,我们不添加任何的惩罚项,就按照正常的损失函数计算,只不过在得到每个神经元的权重向量w⃗ 之后约束它满足∥w⃗ ∥2<c。有些人提到这种正则化方式帮助他们提高最后的模型效果。另外,这种正则化方式倒是有一点很吸引人:在神经网络训练学习率设定很高的时候,它也能很好地约束住权重更新变化,不至于直接挂掉。
-
**Dropout**,亲,这个是我们实际神经网络训练中,用的非常多的一种正则化手段,同时也相当有效。Srivastava等人的论文[Dropout: A Simple Way to Prevent Neural Networks from Overfitting](http://www.cs.toronto.edu/~rsalakhu/papers/srivastava14a.pdf)最早提到用dropout这种方式作为正则化手段。一句话概括它,就是:在训练过程中,我们对每个神经元,都以概率p保持它是激活状态,1-p的概率直接关闭它。
下图是一个3层的神经网络的dropout示意图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad5b2378.png)
可以这么理解,在训练过程中呢,我们对全体神经元,以概率p做了一个采样,只有选出的神经元要进行参数更新。所以最后就从左图的全连接到右图的Dropout过后神经元连接图了。需要多说一句的是,在测试阶段,我们不用dropout,而是直接从概率的角度,对权重配以一个概率值。
简单的Dropout代码如下(这是简易实现版本,但是不建议使用,我们会分析为啥,并在之后给出优化版):
~~~
p = 0.5 # 设定dropout的概率,也就是保持一个神经元激活状态的概率
def train_step(X):
""" X contains the data """
# 3层神经网络前向计算
H1 = np.maximum(0, np.dot(W1, X) + b1)
U1 = np.random.rand(*H1.shape) < p # 第一次Dropout
H1 *= U1 # drop!
H2 = np.maximum(0, np.dot(W2, H1) + b2)
U2 = np.random.rand(*H2.shape) < p # 第二次Dropout
H2 *= U2 # drop!
out = np.dot(W3, H2) + b3
# 反向传播: 计算梯度... (这里省略)
# 参数更新... (这里省略)
def predict(X):
# 加上Dropout之后的前向计算
H1 = np.maximum(0, np.dot(W1, X) + b1) * p
H2 = np.maximum(0, np.dot(W2, H1) + b2) * p
out = np.dot(W3, H2) + b3
~~~
上述代码中,在`train_step`函数中,我们做了2次Dropout。我们甚至可以在输入层做一次dropout。反向传播过程保持不变,除了我们要考虑一下`U1,U2`
很重要的一点是,大家仔细看`predict`函数部分,我们不再dropout了,而是对于每个隐层的输出,都用概率p做了一个幅度变换。可以从数学期望的角度去理解这个做法,我们考虑一个神经元的输出为x(没有dropout的情况下),它的输出的数学期望为px+(1−p)0,那我们在测试阶段,如果直接把每个输出x都做变换x→px,其实是可以保持一样的数学期望的。
上述代码的写法有一些缺陷,我们必须在测试阶段对每个神经的输出都以p的概率输出。考虑到实际应用中,测试阶段对于时间的要求非常高,我们可以考虑反着来,代码实现的时候用`inverted dropout`,即在训练阶段就做相反的幅度变换/scaling(除以p),这样在测试阶段,我们可以直接把权重拿来使用,而不用附加很多步用p做scaling的过程。`inverted dropout`的示例代码如下:
~~~
"""
Inverted Dropout的版本,把本该花在测试阶段的时间,转移到训练阶段,从而提高testing部分的速度
"""
p = 0.5 # dropout的概率,也就是保持一个神经元激活状态的概率
def train_step(X):
# f3层神经网络前向计算
H1 = np.maximum(0, np.dot(W1, X) + b1)
U1 = (np.random.rand(*H1.shape) < p) / p # 注意到这个dropout中我们除以p,做了一个inverted dropout
H1 *= U1 # drop!
H2 = np.maximum(0, np.dot(W2, H1) + b2)
U2 = (np.random.rand(*H2.shape) < p) / p # 这个dropout中我们除以p,做了一个inverted dropout
H2 *= U2 # drop!
out = np.dot(W3, H2) + b3
# 反向传播: 计算梯度... (这里省略)
# 参数更新... (这里省略)
def predict(X):
# 直接前向计算,无需再乘以p
H1 = np.maximum(0, np.dot(W1, X) + b1)
H2 = np.maximum(0, np.dot(W2, H1) + b2)
out = np.dot(W3, H2) + b3
~~~
对于dropout这个部分如果你有更深的兴趣,欢迎阅读以下文献:
1) 2014 Srivastava 的论文[Dropout paper](http://www.cs.toronto.edu/~rsalakhu/papers/srivastava14a.pdf)
2) [Dropout Training as Adaptive Regularization](http://papers.nips.cc/paper/4882-dropout-training-as-adaptive-regularization.pdf)
- **bias项的正则化**,其实我们在[之前的博客](http://blog.csdn.net/han_xiaoyang/article/details/49999583)中提到过,我们大部分时候并不对偏移量项做正则化,因为它们也没有和数据直接有乘法等交互,也就自然不会影响到最后结果中某个数据维度的作用。不过如果你愿意对它做正则化,倒也不会影响最后结果,毕竟总共有那么多权重项,才那么些bias项,所以一般也不会影响结果。
**实际应用中**:我们最常见到的是,在全部的交叉验证集上使用L2正则化,同时我们在每一层之后用dropout,很常见的dropout概率为p=0.5,你也可以通过交叉验证去调整这个值。
#### 2.4 损失函数
刚才讨论了数据预处理、权重初始化与正则化相关的问题。现在我们回到训练需要的关键之一:损失函数。对于这么复杂的神经网络,我们也得有一个评估准则去评估预测值和真实结果之间的吻合度,也就是损失函数。神经网络里的损失函数,实际上是计算出了每个样本上的loss,再求平均之后的一个形式,即L=1N∑iLi,其中N是训练样本数。
#### 2.4.1 分类问题
- **分类问题**是到目前为止我们一直在讨论的。我们假定一个数据集中每个样本都有唯一一个正确的标签/类别。我们之前提到过有两种损失函数可以使用,其一是SVM的hinge loss:
Li=∑j≠yimax(0,fj−fyi+1)
另外一个是Softmax分类器中用到的互熵损失:
Li=−log(efyi∑jefj)
-
**问题:特别多的类别数**。当类别标签特别特别多的时候(比如ImageNet包含22000个类别),[层次化的Softmax](http://arxiv.org/pdf/1310.4546.pdf),它将类别标签建成了一棵树,这样任何一个类别,其实就对应tree的一条路径,然后我们在每个树的结点上都训练一个Softmax以区分是左分支还是右分支。
-
**属性分类**,上述的两种损失函数都假定,对于每个样本,我们只有一个正确的答案yi。但是在有些场景下,yi是一个二值的向量,每个元素都代表有没有某个属性,这时候我们怎么办呢?举个例子说,Instagram上的图片可以看作一大堆hashtag里的一个tag子集,所有一张图片可以有多个tag。对于这种情况,大家可能会想到一个最简单的处理方法,就是对每个属性值都建一个二分类的分类器。比如,对应某个类别的二分类器可能有如下形式的损失函数:
Li=∑jmax(0,1−yijfj)
其中的求和是针对有所的类别j,而yij是1或者-1(取决于第i个样本是否有第j个属性的标签),打分向量fj在类别/标签被预测到的情况下为正,其他情况为负。注意到如果正样本有比+1小的得分,或者负样本有比-1大的得分,那么损失/loss就一直在累积。
另外一个也许有效的解决办法是,我们可以对每个属性,都单独训练一个逻辑回归分类器,一个二分类的逻辑回归分类器只有0,1两个类别,属于1的概率为:
P(y=1∣x;w,b)=11+e−(wTx+b)=σ(wTx+b)
又因为0,1两类的概率和为1,所以归属于类别0的概率为P(y=0∣x;w,b)=1−P(y=1∣x;w,b)。一个样本在σ(wTx+b)>0.5的情况下被判定为1,对应sigmoid函数化简一下,对应的是得分wTx+b>0。这时候的损失函数可以定义为最大化似然概率的形式,也就是:
Li=∑jyijlog(σ(fj))+(1−yij)log(1−σ(fj))
其中标签yij为1(正样本)或者0(负样本),而δ是sigmoid函数。
#### 2.4.2 回归问题
回归是另外一类机器学习问题,主要用于预测连续值属性,比如房子的价格或者图像中某些东西的长度等。对于回归问题,我们一般计算预测值和实际值之间的差值,然后再求L2范数或者L1范数用于衡量。其中对一个样本(一张图片)计算的L2范数损失为:
Li=∥f−yi∥22
而L1范数损失函数是如下的形式:
Li=∥f−yi∥1=∑j∣fj−(yi)j∣
**注意**:
- 回归问题中用到的L2范数损失,比分类问题中的Softmax分类器用到的损失函数,更难优化。直观想一想这个问题,一个神经网络最后输出离散的判定类别,比训练它去输出一个个和样本结果对应的连续值,要简单多了。
- 我们前面的[博文](http://blog.csdn.net/han_xiaoyang/article/details/49999583)中提到过,其实Softmax这种分类器,对于输出的打分结果具体值是不怎么在乎的,它只在乎各个类别之间的打分幅度有没有差很多(比如二分类两个类别的得分是1和9,与0.1和0.9)。
- 再一个,L2范数损失健壮性更差一些,异常点和噪声都可能改变损失函数的幅度,而带来大的梯度偏差。
- 一般情况下,对于回归问题,我们都会首先考虑,这个问题能否转化成对应的分类问题,比如说我们把输出值划分成不同的区域(切成一些桶)。举个例子,如果我们要预测一部电影的豆瓣打分,我们可以考虑把得分结果分成1-5颗星,而转化成一个分类问题。
- 如果你觉得问题确实没办法转化成分类问题,那要小心使用L2范数损失:举个例子,在神经网络中,在L2损失函数之前使用dropout是不合适的。
> 如果我们遇到回归问题,首先要想想,是否完全没有可能把结果离散化之后,把这个问题转化成一个分类问题。
### 3. 总结
总结一下:
- 在很多神经网络的问题中,我们都建议对数据特征做预处理,去均值,然后归一化到[-1,1]之间。
- 从一个标准差为2/n‾‾‾√的高斯分布中初始化权重,其中n为输入的个数。
- 使用L2正则化(或者最大范数约束)和dropout来减少神经网络的过拟合。
- 对于分类问题,我们最常见的损失函数依旧是SVM hinge loss和Softmax互熵损失。
(6)_神经网络结构与神经元激励函数
最后更新于:2022-04-01 14:21:47
作者:[寒小阳](http://blog.csdn.net/han_xiaoyang?viewmode=contents)
时间:2016年1月。
出处:[http://blog.csdn.net/han_xiaoyang/article/details/50447834](http://blog.csdn.net/han_xiaoyang/article/details/50447834)
声明:版权所有,转载请联系作者并注明出处
### 1.神经元与含义
大家都知道最开始深度学习与神经网络,是受人脑的神经元启发设计出来的。所以我们按照惯例也交代一下背景,从生物学的角度开始介绍,当然也是对神经网络研究的先驱们致一下敬。
#### 1.1 神经元激励与连接
大家都知道,人脑的基本计算单元叫做神经元。现代生物学表明,人的神经系统中大概有860亿神经元,而这数量巨大的神经元之间大约是通过1014−1015个突触连接起来的。下面有一幅示意图,粗略地描绘了一下人体神经元与我们简化过后的数学模型。每个神经元都从树突接受信号,同时顺着某个轴突传递信号。而每个神经元都有很多轴突和其他的神经元树突连接。而我们可以看到右边简化的神经元计算模型中,`信号`也是顺着`轴突`(比如x0)传递,然后在轴突处受到激励(w0倍)然后变成w0x0。我们可以这么理解这个模型:在信号的传导过程中,突触可以控制传导到下一个神经元的信号强弱(数学模型中的权重w),而这种强弱是可以学习到的。在基本生物模型中,树突传导信号到神经元细胞,然后这些信号被加和在一块儿了,如果加和的结果被神经元感知超过了某种阈值,那么神经元就被激活,同时沿着轴突向下一个神经元传导信号。在我们简化的数学计算模型中,我们假定有一个『激励函数』来控制加和的结果对神经元的刺激程度,从而控制着是否激活神经元和向后传导信号。比如说,我们在逻辑回归中用到的sigmoid函数就是一种激励函数,因为对于求和的结果输入,sigmoid函数总会输出一个0-1之间的值,我们可以认为这个值表明信号的强度、或者神经元被激活和传导信号的概率。
![神经元生物学模型](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad3ba7c9.png "")
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad3da6eb.png)
下面是一个简单的程序例子,表明前向传播中单个神经元做的事情:
~~~
class Neuron:
# ...
def forward(inputs):
"""
假定输入和权重都是1维的numpy数组,同时bias是一个数
"""
cell_body_sum = np.sum(inputs * self.weights) + self.bias
firing_rate = 1.0 / (1.0 + math.exp(-cell_body_sum)) # sigmoid activation function
return firing_rate
~~~
稍加解释,每个神经元对于输入和权重做内积,加上偏移量bias,然后通过激励函数(比如说这里是sigmoid函数),然后输出结果。
**特别说明**:实际生物体内的神经元相当复杂,比如说,神经元的种类就灰常灰常多,它们分别有不同的功能。而加和信号之后的激励函数的非线性变换,也比数学上模拟出来的函数复杂得多。我们用数学建模的神经网络只是一个非常简化后的模型,有兴趣的话你可以阅读[材料1](https://physics.ucsd.edu/neurophysics/courses/physics_171/annurev.neuro.28.061604.135703.pdf)或者[材料2](http://www.sciencedirect.com/science/article/pii/S0959438814000130)。
#### 1.2 单个神经元的分类作用
以sigmoid函数作为神经元的激励函数为例,这个大家可能稍微熟悉一点,毕竟我们[逻辑回归部分](http://blog.csdn.net/han_xiaoyang/article/details/49123419)重点提到了这个非线性的函数,把输入值压缩成0-1之间的一个概率值。而通过这个非线性映射和设定的阈值,我们可以把空间切分开,分别对应正样本区域和负样本区域。而对应回现在的神经元场景,我们如果稍加拟人化,可以认为神经元具备了喜欢(概率接近1)和不喜欢(概率接近0)线性划分的某个空间区域的能力。这也就是说,只要调整好权重,单个神经元可以对空间做线性分割。
**二值Softmax分类器**
对于Softmax分类器详细的内容欢迎参见[前面的博文系列](http://blog.csdn.net/han_xiaoyang/article/details/49999583),我们标记σ为sigmoid映射函数,则σ(∑iwixi+b)可视作二分类问题中属于某个类的概率P(yi=1∣xi;w),当然,这样我们也可以得到相反的那个类别的概率为P(yi=0∣xi;w)=1−P(yi=1∣xi;w)。根据前面博文提到的知识,我们可以使用互熵损失作为这个二值线性分类器的损失函数(loss function),而最优化损失函数得到的一组参数W,b,就能帮助我们将空间线性分割,得到二值分类器。当然,和逻辑回归中看到的一样,最后神经元预测的结果y值如果大于0.5,那我们会判定它属于这个类别,反之则属于另外一个类别。
**二值SVM分类器**
同样的,我们可以设定max-margin hinge loss作为损失函数,从而将神经元训练成一个二值支持向量机分类器。详细的内容依旧欢迎大家查看[之前的博客](http://blog.csdn.net/han_xiaoyang/article/details/49999583)。
**对于正则化的解释**
对于正则化的损失函数(不管是SVM还是Softmax),其实我们在神经元的生物特性上都能找到对应的解释,我们可以将其(正则化项的作用)视作信号在神经元传递过程中的逐步淡化/衰减(gradual forgetting),因为正则化项的作用是在每次迭代过程中,控制住权重w的幅度,往0上靠拢。
> 单个神经元的作用,可视作完成一个二分类的分类器(比如Softmax或者SVM分类器)
#### 1.3 常用激励函数
每一次输入和权重w线性组合之后,都会通过一个激励函数(也可以叫做非线性激励函数),经非线性变换后输出。实际的神经网络中有一些可选的激励函数,我们一一说明一下最常见的几种:
#### 1.3.1 sigmoid
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad405d77.png)
sigmoid函数提到的次数太多,相信大家都知道了。数学形式很简单,是σ(x)=1/(1+e−x),图像如上图所示,功能是把一个实数压缩至0到1之间。输入的数字非常大的时候,结果会接近1,而非常大的负数作为输入,则会得到接近0的结果。不得不说,早期的神经网络中,sigmoid函数作为激励函数使用非常之多,因为大家觉得它很好地解释了神经元受到刺激后是否被激活和向后传递的场景(从几乎没有被激活,也就是0,到完全被激活,也就是1)。不过似乎近几年的实际应用场景中,比较少见到它的身影,它主要的缺点有2个:
- sigmoid函数在实际梯度下降中,容易饱和和终止梯度传递。我们来解释一下,大家知道反向传播过程,依赖于计算的梯度,在一元函数中,即斜率。而在sigmoid函数图像上,大家可以很明显看到,在纵坐标接近0和1的那些位置(也就是输入信号的幅度很大的时候),斜率都趋于0了。我们回想一下反向传播的过程,我们最后用于迭代的梯度,是由中间这些梯度值结果相乘得到的,因此如果中间的局部梯度值非常小,直接会把最终梯度结果拉近0,也就是说,残差回传的过程,因为sigmoid函数的饱和被杀死了。说个极端的情况,如果一开始初始化权重的时候,我们取值不是很恰当,而激励函数又全用的sigmoid函数,那么很有可能神经元一个不剩地饱和到无法学习,整个神经网络也根本没办法训练起来。
- sigmoid函数的输出没有`0中心化`,这是一个比较闹心的事情,因为每一层的输出都要作为下一层的输入,而未0中心化会直接影响梯度下降,我们这么举个例子吧,如果输出的结果均值不为0,举个极端的例子,全部为正的话(例如f=wTx+b中所有x>0),那么反向传播回传到w上的梯度将全部为负,这带来的后果是,梯度更新的时候,不是平缓地迭代变化,而是类似锯齿状的突变。当然,要多说一句的是,这个缺点相对于第一个缺点,还稍微好一点,第一个缺点的后果是,很多场景下,神经网络根本没办法学习。
#### 1.3.2 Tanh
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad41a5c1.png)
Tanh函数的图像如上图所示。它会将输入值压缩至-1到1之间,当然,它同样也有sigmoid函数里说到的第一个缺点,在很大或者很小的输入值下,神经元很容易饱和。但是它缓解了第二个缺点,它的输出是0中心化的。所以在实际应用中,tanh激励函数还是比sigmoid要用的多一些的。
#### 1.3.3 ReLU
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad42b4f6.png)
ReLU是修正线性单元(The Rectified Linear Unit)的简称,近些年使用的非常多,图像如上图所示。它对于输入x计算f(x)=max(0,x)。换言之,以0为分界线,左侧都为0,右侧是y=x这条直线。
它有它对应的优势,也有缺点:
- 优点1:实验表明,它的使用,相对于sigmoid和tanh,可以非常大程度地提升随机梯度下降的收敛速度。不过有意思的是,很多人说,这个结果的原因是它是线性的,而不像sigmoid和tanh一样是非线性的。具体的收敛速度结果对比如下图,收敛速度大概能快上6倍:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad43b579.png)
- 优点2:相对于tanh和sigmoid激励神经元,求梯度不要简单太多好么!!!毕竟,是线性的嘛。。。
- 缺点1:ReLU单元也有它的缺点,在训练过程中,它其实挺脆弱的,有时候甚至会挂掉。举个例子说吧,如果一个很大的梯度`流经`ReLU单元,那权重的更新结果可能是,在此之后任何的数据点都没有办法再激活它了。一旦这种情况发生,那本应经这个ReLU回传的梯度,将永远变为0。当然,这和参数设置有关系,所以我们要特别小心,再举个实际的例子哈,如果学习速率被设的太高,结果你会发现,训练的过程中可能有高达40%的ReLU单元都挂掉了。所以我们要小心设定初始的学习率等参数,在一定程度上控制这个问题。
#### 1.3.4 Leaky ReLU
上面不是提到ReLU单元的弱点了嘛,所以孜孜不倦的ML researcher们,就尝试修复这个问题咯,他们做了这么一件事,在x<0的部分,leaky ReLU不再让y的取值为0了,而是也设定为一个坡度很小(比如斜率0.01)的直线。f(x)因此是一个分段函数,x<0时,f(x)=αx(α是一个很小的常数),x>0时,f(x)=x。有一些researcher们说这样一个形式的激励函数帮助他们取得更好的效果,不过似乎并不是每次都比ReLU有优势。
#### 1.3.5 Maxout
也有一些其他的激励函数,它们并不是对WTX+b做非线性映射f(WTX+b)。一个近些年非常popular的激励函数是Maxout(详细内容请参见[Maxout](http://www-etud.iro.umontreal.ca/~goodfeli/maxout.html))。简单说来,它是ReLU和Leaky ReLU的一个泛化版本。对于输入x,Maxout神经元计算max(wT1x+b1,wT2x+b2)。有意思的是,如果你仔细观察,你会发现ReLU和Leaky ReLU都是它的一个特殊形式(比如ReLU,你只需要把w1,b1设为0)。因此Maxout神经元继承了ReLU单元的优点,同时又没有『一不小心就挂了』的担忧。如果要说缺点的话,你也看到了,相比之于ReLU,因为有2次线性映射运算,因此计算量也double了。
#### 1.4 激励函数/神经元小总结
以上就是我们总结的常用的神经元和激励函数类型。顺便说一句,即使从计算和训练的角度看来是可行的,实际应用中,其实我们很少会把多种激励函数混在一起使用。
那我们咋选用神经元/激励函数呢?一般说来,用的最多的依旧是ReLU,但是我们确实得小心设定学习率,同时在训练过程中,还得时不时看看神经元此时的状态(是否还『活着』)。当然,如果你非常担心神经元训练过程中挂掉,你可以试试Leaky ReLU和Maxout。额,少用sigmoid老古董吧,有兴趣倒是可以试试tanh,不过话说回来,通常状况下,它的效果不如ReLU/Maxout。
### 2. 神经网络结构
#### 2.1 层级连接结构
神经网络的结构其实之前也提过,是一种单向的层级连接结构,每一层可能有多个神经元。再形象一点说,就是每一层的输出将会作为下一层的输入数据,当然,这个图一定是没有循环的,不然数据流就有点混乱了。一般情况下,单层内的这些神经元之间是没有连接的。最常见的一种神经网络结构就是全连接层级神经网络,也就是相邻两层之间,每个神经元和每个神经元都是相连的,单层内的神经元之间是没有关联的。下面是两个全连接层级神经网的示意图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad44fb8c.png)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad46ec0f.png)
**命名习俗**
有一点需要注意,我们再说N层神经网络的时候,通常的习惯是不把输入层计算在内,因此输入层直接连接输出层的,叫做单层神经网络。从这个角度上说,其实我们的逻辑回归和SVM是单层神经网络的特例。上图中两个神经网络分别是2层和3层的神经网络。
**输出层**
输出层是神经网络中比较特殊的一层,由于输出的内容通常是各类别的打分/概率(在分类问题中),我们通常都不在输出层神经元中加激励函数。
**关于神经网络中的组件个数**
通常我们在确定一个神经网络的时候,有几个描述神经网络大小的参数会提及到。最常见的两个是神经元个数,以及细化一点说,我们可以认为是参数的个数。还是拿上面的图举例:
- 第一个神经网络有4+2=6个神经元(我们不算输入层),因此有[3 * 4]+[4*2]=20个权重和4+2=6个偏移量(bias项),总共26个参数。
- 第二个神经网络有4+4+1个神经元,有[3 * 4]+[4 * 4]+[4 * 1 ]=32个权重,再加上4+4+1=9个偏移量(bias项),一共有41个待学习的参数。
给大家个具体的概念哈,现在实用的卷积神经网,大概有亿级别的参数,甚至可能有10-20层(因此是深度学习嘛)。不过不用担心这么多参数的训练问题,因此我们在卷积神经网里会有一些有效的方法,来共享参数,从而减少需要训练的量。
#### 2.2 神经网络的前向计算示例
神经网络组织成以上的结构,一个重要的原因是,每一层到下一层的计算可以很方便地表示成矩阵之间的运算,就是一直重复权重和输入做内积后经过激励函数变换的过程。为了形象一点说明,我们还举上面的3层神经网络为例,输入是一个3*1的向量,而层和层之间的连接权重可以看做一个矩阵,比如第一个隐藏层的权重W1是一个[4*3]的矩阵,偏移量b1是[4*1]的向量,因此用python中的numpy做内积操作`np.dot(W1,x)`实际上就计算出输入下一层的激励函数之前的结果,经激励函数作用之后的结果又作为新的输出。用简单的代码表示如下:
~~~
# 3层神经网络的前向运算:
f = lambda x: 1.0/(1.0 + np.exp(-x)) # 简单起见,我们还是用sigmoid作为激励函数吧
x = np.random.randn(3, 1) # 随机化一个输入
h1 = f(np.dot(W1, x) + b1) # 计算第一层的输出
h2 = f(np.dot(W2, h1) + b2) # 计算第二层的输出
out = np.dot(W3, h2) + b3 # 最终结果 (1x1)
~~~
上述代码中,`W1,W2,W3,b1,b2,b3`都是**待学习**的神经网络参数。注意到我们这里所有的运算都是向量化/矩阵化之后的,x不再是一个数,而是包含训练集中一个batch的输入,这样并行运算会加快计算的速度,仔细看代码,最后一层是没有经过激励函数,直接输出的。
#### 2.3 神经网络的表达力与size
一个神经网络结构搭起来之后,它就包含了数以亿计的参数和函数。我们可以把它看做对输入的做了一个很复杂的函数映射,得到最后的结果用于完成空间的分割(分类问题中)。那我们的参数对于这个所谓的复杂映射有什么样的影响呢?
其实,包含一个隐藏层(2层神经网络)的神经网络已经具备大家期待的能力,即只要隐藏层的神经元个数足够,我们总能用它(2层神经网络)去逼近任何连续函数(即输入到输出的映射关系)。详细的内容可以参加[Approximation by Superpositions of Sigmoidal Function](http://www.dartmouth.edu/~gvc/Cybenko_MCSS.pdf)或者[Michael Nielsen的介绍](http://neuralnetworksanddeeplearning.com/chap4.html)。我们之前的博文[手把手入门神经网络系列(1)_从初等数学的角度初探神经网络](http://blog.csdn.net/han_xiaoyang/article/details/50100367)也有提到。
问题是,如果单隐藏层的神经网络已经可以近似逼近任意的连续值函数,那么为什么我们还要用那么多层呢?很可惜的是,即使数学上我们可以用2层神经网近似几乎所有函数,但在实际的工程实践中,却是没啥大作用的。多隐藏层的神经网络比单隐藏层的神经网络工程效果好很多,即使从数学上看,表达能力应该是一致的。
不过还得说一句的是,通常情况下,我们工程中发现,3层神经网络效果优于2层神经网络,但是如果把层数再不断增加(4,5,6层),对最后结果的帮助就没有那么大的跳变了。不过在卷积神经网上还是不一样的,深层的网络结构对于它的准确率有很大的帮助,直观理解的方式是,图像是一种深层的结构化数据,因此深层的卷积神经网络能够更准确地把这些层级信息表达出来。
#### 2.4 层数与参数设定的影响
一个很现实的问题是,我们拿到一个实际问题的时候,怎么知道应该如何去搭建一个网络结构,可以最好地解决这个问题?应该搭建几层?每一层又应该有多少个神经元?
我们直观理解一下这个问题,当我们加大层数以及每一层的神经元个数的时候,我们的神经网络`容量`变大了。更通俗一点说,神经网络的空间表达能力变得更丰富了。放到一个具体的例子里我们看看,加入我们现在要处理一个2分类问题,输入是2维的,我们训练3个不同神经元个数的单隐层神经网络,它们的平面表达能力对比画出来如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad4a1093.png)
在上图中,我们可以看出来,更多的神经元,让神经网络有更好的拟合复杂空间函数的能力。但是任何事物都有双面性,拟合越来越精确带来的另外一个问题是,太容易过拟合了!!!,如果你很任性地做一个实验,在隐藏层中放入20个神经元,那对于上图这个一个平面,你完全可以做到100%把两类点分隔开,但是这样一个分类器太努力地学习和记住我们现在图上的这些点的分布状况了,以至于连噪声和离群点都被它学习下来了,这对于我们在新数据上的泛化能力,也是一个噩梦。
经我们上面的讨论之后,也许你会觉得,好像对于不那么复杂的问题,我们用更少数目的层数和神经元,会更不容易过拟合,效果好一些。但是这个想法是错误的!!!。永远不要用减少层数和神经元的方法来缓解过拟合!!!这会极大影响神经网络的表达能力!!!我们有其他的方法,比如说之前一直提到的正则化来缓解这个问题。
不要使用少层少神经元的简单神经网络的另外一个原因是,其实我们用梯度下降等方法,在这种简单神经网上,更难训练得到合适的参数结果。对,你会和我说,简单神经网络的损失函数有更少的局部最低点,应该更好收敛。是的,确实是的,更好收敛,但是很快收敛到的这些个局部最低点,通常都是全局很差的。相反,大的神经网络,确实损失函数有更多的局部最低点,但是这些局部最低点,相对于上面的局部最低点,在实际中效果却更好一些。对于非凸的函数,我们很难从数学上给出100%精准的性质证明,大家要是感兴趣的话,可以参考论文[The Loss Surfaces of Multilayer Networks](http://arxiv.org/abs/1412.0233)。
如果你愿意做多次实验,会发现,训练小的神经网络,最后的损失函数收敛到的最小值变动非常大。这意味着,如果你运气够好,那你maybe能找到一组相对较为合适的参数,但大多数情况下,你得到的参数只是在一个不太好的局部最低点上的。相反,大的神经网络,依旧不能保证收敛到最小的全局最低点,但是众多的局部最低点,都有相差不太大的效果,这意味着你不需要借助”运气”也能找到一个近似较优的参数组。
最后,我们提一下正则化,我们说了要用正则化来控制过拟合问题。正则话的参数是λ,它的大小体现我们对参数搜索空间的限制,设置小的话,参数可变动范围大,同时更可能过拟合,设置太大的话,对参数的抑制作用太强,以至于不太能很好地表征类别分布了。下图是我们在上个问题中,使用不同大小的正则化参数λ得到的平面分割结果。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad4cdf98.png)
恩,总之一句话,我们在很多实际问题中,还是得使用多层多神经元的大神经网络,而使用正则化来减缓过拟合现象。
### 3. 其他参考资料
- [Theano的深度学习导读](http://www.deeplearning.net/tutorial/mlp.html)
- [Michael Nielsen的神经网络导论](http://neuralnetworksanddeeplearning.com/chap1.html)
- [ConvNetJS demo](http://cs.stanford.edu/people/karpathy/convnetjs/)
(5)_反向传播与它的直观理解
最后更新于:2022-04-01 14:21:44
作者:[寒小阳](http://blog.csdn.net/han_xiaoyang?viewmode=contents)
时间:2015年12月。
出处:[http://blog.csdn.net/han_xiaoyang/article/details/50321873](http://blog.csdn.net/han_xiaoyang/article/details/50321873)
声明:版权所有,转载请联系作者并注明出处
### 1. 引言
其实一开始要讲这部分内容,我是拒绝的,原因是我觉得有一种写高数课总结的感觉。而一般直观上理解反向传播算法就是求导的一个链式法则而已。但是偏偏理解这部分和其中的细节对于神经网络的设计和调整优化又是有用的,所以硬着头皮写写吧。
**问题描述与动机:**
- 大家都知道的,其实我们就是在给定的图像像素向量x和对应的函数f(x),然后我们希望能够计算f在x上的梯度(∇f(x))
- 我们之所以想解决这个问题,是因为在神经网络中,f对应损失函数L,而输入x则对应训练样本数据和神经网络的权重W。举一个特例,损失函数可以是SVM loss function,而输入则对应样本数据(xi,yi),i=1…N和权重以及bias W,b。需要注意的一点是,在我们的场景下,通常我们认为训练数据是给定的,而权重是我们可以控制的变量。因此我们为了更新权重的等参数,使得损失函数值最小,我们通常是计算f对参数W,b的梯度。不过我们计算其在xi上的梯度有时候也是有用的,比如如果我们想做可视化以及了解神经网络在『做什么』的时候。
### 2.高数梯度/偏导基础
好了,现在开始复习高数课了,从最简单的例子开始,假如f(x,y)=xy,那我们可以求这个函数对x和y的偏导,如下:
f(x,y)=xy→∂f∂x=y∂f∂y=x
#### 2.1 解释
我们知道偏导数实际表示的含义:一个函数在给定变量所在维度,当前点附近的一个变化率。也就是:
df(x)dx=limh →0f(x+h)−f(x)h
以上公式中的ddx作用在f上,表示对x求偏导数,表示的是x维度上当前点位置周边很小区域的变化率。举个例子,如果x=4,y=−3,而f(x,y)=−12,那么x上的偏导∂f∂x=−3,这告诉我们如果这个变量(x)增大一个很小的量,那么整个表达式会以3倍这个量减小。我们把上面的公式变变形,可以这么看:f(x+h)=f(x)+hdf(x)dx。同理,因为∂f∂y=4,我们将y的值增加一个很小的量h,则整个表达式变化4h。> 每个维度/变量上的偏导,表示整个函数表达式,在这个值上的『敏感度』
哦,对,我们说的梯度∇f其实是一个偏导组成的向量,比如我们有∇f=[∂f∂x,∂f∂y]=[y,x]。即使严格意义上来说梯度是一个向量,但是大多数情况下,我们还是习惯直呼『x上的梯度』,而不是『x上的偏导』
大家都知道加法操作上的偏导数是这样的:
f(x,y)=x+y→∂f∂x=1∂f∂y=1
而对于一些别的操作,比如max函数,偏导数是这样的(后面的括号表示在这个条件下):
f(x,y)=max(x,y)→∂f∂x=1(x>=y)∂f∂y=1(y>=x)
### 3. 复杂函数偏导的链式法则
考虑一个麻烦一点的函数,比如f(x,y,z)=(x+y)z。当然,这个表达式其实还没那么复杂,也可以直接求偏导。但是我们用一个非直接的思路去求解一下偏导,以帮助我们直观理解反向传播中。如果我们用换元法,把原函数拆成两个部分q=x+y和f=qz。对于这两个部分,我们知道怎么求解它们变量上的偏导:∂f∂q=z,∂f∂z=q∂q∂x=1,∂q∂y=1,当然q是我们自己设定的一个变量,我们对他的偏导完全不感兴趣。
那『链式法则』告诉我们一个对上述偏导公式『串联』的方式,得到我们感兴趣的偏导数:∂f∂x=∂f∂q∂q∂x
看个例子:
~~~
x = -2; y = 5; z = -4
# 前向计算
q = x + y # q becomes 3
f = q * z # f becomes -12
# 类反向传播:
# 先算到了 f = q * z
dfdz = q # df/dz = q
dfdq = z # df/dq = z
# 再算到了 q = x + y
dfdx = 1.0 * dfdq # dq/dx = 1 恩,链式法则
dfdy = 1.0 * dfdq # dq/dy = 1
~~~
链式法则的结果是,只剩下我们感兴趣的`[dfdx,dfdy,dfdz]`,也就是原函数在x,y,z上的偏导。这是一个简单的例子,之后的程序里面我们为了简洁,不会完整写出`dfdq`,而是用`dq`代替。
以下是这个计算的示意图:
![例1](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad20cbfe.png "")
### 4. 反向传播的直观理解
一句话概括:反向传播的过程,实际上是一个由局部到全部的精妙过程。比如上面的电路图中,其实每一个『门』在拿到输入之后,都能计算2个东西:
- 输出值
- 对应输入和输出的局部梯度
而且很明显,每个门在进行这个计算的时候是完全独立的,不需要对电路图中其他的结构有了解。然而,在整个前向传输过程结束之后,在反向传播过程中,每个门却能逐步累积计算出它在整个电路输出上的梯度。`『链式法则』`告诉我们每一个门接收到后向传来的梯度,同时用它乘以自己算出的对每个输入的局部梯度,接着往后传。
以上面的图为例,来解释一下这个过程。加法门接收到输入[-2, 5]同时输出结果3。因为加法操作对两个输入的偏导都应该是1。电路后续的部分算出最终结果-12。在反向传播过程中,链式法则是这样做的:加法操作的输出3,在最后的乘法操作中,获得的梯度为-4,如果把整个网络拟人化,我们可以认为这代表着网络『想要』加法操作的结果小一点,而且是以4*的强度来减小。加法操作的门获得这个梯度-4以后,把它分别乘以本地的两个梯度(加法的偏导都是1),1*-4=-4。如果输入x减小,那加法门的输出也会减小,这样乘法输出会相应的增加。
反向传播,可以看做网络中门与门之间的『关联对话』,它们『想要』自己的输出更大还是更小(以多大的幅度),从而让最后的输出结果更大。
### 5. Sigmoid例子
上面举的例子其实在实际应用中很少见,我们很多时候见到的网络和门函数更复杂,但是不论它是什么样的,反向传播都是可以使用的,唯一的区别就是可能网络拆解出来的门函数布局更复杂一些。我们以之前的逻辑回归为例:
f(w,x)=11+e−(w0x0+w1x1+w2)
这个看似复杂的函数,其实可以看做一些基础函数的组合,这些基础函数及他们的偏导如下:
f(x)=1x→dfdx=−1/x2fc(x)=c+x→dfdx=1f(x)=ex→dfdx=exfa(x)=ax→dfdx=a
上述每一个基础函数都可以看做一个门,如此简单的初等函数组合在一块儿却能够完成逻辑回归中映射函数的复杂功能。下面我们画出神经网络,并给出具体输入输出和参数的数值:
![例2](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad21f54f.png "")
这个图中,[x0, x1]是输入,[w0, w1,w2]为可调参数,所以它做的事情是对输入做了一个线性计算(x和w的内积),同时把结果放入sigmoid函数中,从而映射到(0,1)之间的数。
上面的例子中,w与x之间的内积分解为一长串的小函数连接完成,而后接的是sigmoid函数σ(x),有趣的是sigmoid函数看似复杂,求解倒是的时候却是有技巧的,如下:
σ(x)=11+e−x→dσ(x)dx=e−x(1+e−x)2=(1+e−x−11+e−x)(11+e−x)=(1−σ(x))σ(x)
你看,它的导数可以用自己很简单的重新表示出来。所以在计算导数的时候非常方便,比如sigmoid函数接收到的输入是1.0,输出结果是-0.73。那么我们可以非常方便地计算得到它的偏导为(1-0.73)*0.73~=0.2。我们看看在这个sigmoid函数部分反向传播的计算代码:
~~~
w = [2,-3,-3] # 我们随机给定一组权重
x = [-1, -2]
# 前向传播
dot = w[0]*x[0] + w[1]*x[1] + w[2]
f = 1.0 / (1 + math.exp(-dot)) # sigmoid函数
# 反向传播经过该sigmoid神经元
ddot = (1 - f) * f # sigmoid函数偏导
dx = [w[0] * ddot, w[1] * ddot] # 在x这条路径上的反向传播
dw = [x[0] * ddot, x[1] * ddot, 1.0 * ddot] # 在w这条路径上的反向传播
# yes!就酱紫算完了!是不是很简单?
~~~
#### 5.1 工程实现小提示
回过头看看上头的代码,你会发现,实际写代码实现的时候,有一个技巧能帮助我们很容易地实现反向传播,我们会把前向传播的过程分解成反向传播很容易追溯回来的部分。
### 6. 反向传播实战:复杂函数
我们看一个稍复杂一些的函数:
f(x,y)=x+σ(y)σ(x)+(x+y)2
额,插一句,这个函数没有任何实际的意义。我们提到它,仅仅是想举个例子来说明复杂函数的反向传播怎么使用。如果直接对这个函数求x或者y的偏导的话,你会得到一个很复杂的形式。但是如果你用反向传播去求解具体的梯度值的话,却完全没有这个烦恼。我们把这个函数分解成小部分,进行前向和反向传播计算,即可得到结果,前向传播计算的代码如下:
~~~
x = 3 # 例子
y = -4
# 前向传播
sigy = 1.0 / (1 + math.exp(-y)) # 单值上的sigmoid函数
num = x + sigy
sigx = 1.0 / (1 + math.exp(-x))
xpy = x + y
xpysqr = xpy**2
den = sigx + xpysqr
invden = 1.0 / den
f = num * invden # 完成!
~~~
注意到我们并没有一次性把前向传播最后结果算出来,而是刻意留出了很多中间变量,它们都是我们可以直接求解局部梯度的简单表达式。因此,计算反向传播就变得简单了:我们从最后结果往前看,前向运算中的每一个中间变量`sigy, num, sigx, xpy, xpysqr, den, invden`我们都会用到,只不过后向传回的偏导值乘以它们,得到反向传播的偏导值。反向传播计算的代码如下:
~~~
# 局部函数表达式为 f = num * invden
dnum = invden
dinvden = num
# 局部函数表达式为 invden = 1.0 / den
dden = (-1.0 / (den**2)) * dinvden
# 局部函数表达式为 den = sigx + xpysqr
dsigx = (1) * dden
dxpysqr = (1) * dden
# 局部函数表达式为 xpysqr = xpy**2
dxpy = (2 * xpy) * dxpysqr #(5)
# 局部函数表达式为 xpy = x + y
dx = (1) * dxpy
dy = (1) * dxpy
# 局部函数表达式为 sigx = 1.0 / (1 + math.exp(-x))
dx += ((1 - sigx) * sigx) * dsigx # 注意到这里用的是 += !!
# 局部函数表达式为 num = x + sigy
dx += (1) * dnum
dsigy = (1) * dnum
# 局部函数表达式为 sigy = 1.0 / (1 + math.exp(-y))
dy += ((1 - sigy) * sigy) * dsigy
# 完事!
~~~
实际编程实现的时候,需要注意一下:
- 前向传播计算的时候注意保留部分中间变量:在反向传播计算的时候,会再次用到前向传播计算中的部分结果。这在反向传播计算的回溯时可大大加速。
#### 6.1 反向传播计算中的常见模式
即使因为搭建的神经网络结构形式和使用的神经元都不同,但是大多数情况下,后向计算中的梯度计算可以归到几种常见的模式上。比如,最常见的三种简单运算门(加、乘、最大),他们在反向传播运算中的作用是非常简单和直接的。我们一起看看下面这个简单的神经网:
![例3](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad23abbf.png "")
上图里有我们提到的三种门add,max和multiply。
- **加运算门**在反向传播运算中,不管输入值是多少,取得它output传回的梯度(gradient)然后均匀地分给两条输入路径。因为加法运算的偏导都是+1.0。
- **max(取最大)门**不像加法门,在反向传播计算中,它只会把传回的梯度回传给一条输入路径。因为max(x,y)只对x和y中较大的那个数,偏导为+1.0,而另一个数上的偏导是0。
- **乘法门**就更好理解了,因为x*y对x的偏导为y,而对y的偏导为x,因此在上图中x的梯度是-8.0,即-4.0*2.0
因为梯度回传的原因,神经网络对输入非常敏感。我们拿乘法门来举例,如果输入的xi全都变成原来1000倍,而权重w不变,那么在反向传播计算的时候,x路径上获得的回传梯度不变,而w上的梯度则会变大1000倍,这使得你不得不降低学习速率(learning rate)成原来的1/1000以维持平衡。因此在很多神经网络的问题中,输入数据的预处理也是非常重要的。
### 6.2 向量化的梯度运算
上面所有的部分都是在单变量的函数上做的处理和运算,实际我们在处理很多数据(比如图像数据)的时候,维度都比较高,这时候我们就需要把单变量的函数反向传播扩展到向量化的梯度运算上,需要特别注意的是矩阵运算的每个矩阵维度,以及转置操作。
我们通过简单的矩阵运算来拓展前向和反向传播运算,示例代码如下:
~~~
# 前向传播运算
W = np.random.randn(5, 10)
X = np.random.randn(10, 3)
D = W.dot(X)
# 假如我们现在已经拿到了回传到D上的梯度dD
dD = np.random.randn(*D.shape) # 和D同维度
dW = dD.dot(X.T) #.T 操作计算转置, dW为W路径上的梯度
dX = W.T.dot(dD) #dX为X路径上的梯度
~~~
### 7. 总结
直观地理解,反向传播可以看做图解求导的链式法则。
最后我们用一组图来说明实际优化过程中的正向传播与反向残差传播:
![1](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad2503b6.gif "")
![2](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad262e79.gif "")
![3](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad2743b1.gif "")
![4](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad2870a1.gif "")
![5](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad298a08.gif "")
![6](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad2a8481.gif "")
![7](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad2b9d75.gif "")
![8](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad2cbf98.gif "")
![9](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad2e2ca3.gif "")
![10](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad2f402e.gif "")
![11](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad311532.gif "")
![12](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad3279db.gif "")
![13](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad33babf.gif "")
![14](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad34e7e1.gif "")
![15](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad361ab0.gif "")
![16](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad3767ca.gif "")
![17](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad387ce5.gif "")
![18](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad3a1564.gif "")
(4)_最优化与随机梯度下降
最后更新于:2022-04-01 14:21:42
作者:[寒小阳](http://blog.csdn.net/han_xiaoyang?viewmode=contents)
时间:2015年12月。
出处:[http://blog.csdn.net/han_xiaoyang/article/details/50178505](http://blog.csdn.net/han_xiaoyang/article/details/50178505)
声明:版权所有,转载请联系作者并注明出处
### 1. 引言
上一节[深度学习与计算机视觉系列(3)_线性SVM与SoftMax分类器](http://blog.csdn.net/han_xiaoyang/article/details/49999583)中提到两个对图像识别至关重要的概念:
1. 用于把原始像素信息映射到不同类别得分的得分函数/score function
1. 用于评估参数W效果(评估该参数下每类得分和实际得分的吻合度)的损失函数/loss function
其中对于线性SVM,我们有:
1. 得分函数f(xi,W)=Wxi
1. 损失函数L=1N∑i∑j≠yi[max(0,f(xi;W)j−f(xi;W)yi+1)]+αR(W)
在取到合适的参数W的情况下,我们根据原始像素计算得到的预测结果和实际结果吻合度非常高,这时候损失函数得到的值就很小。
这节我们就讲讲,怎么得到这个合适的参数W,使得损失函数取值最小化。也就是最优化的过程。
### 2. 损失函数可视化
我们在计算机视觉中看到的损失函数,通常都是定义在非常高维的空间里的(比如CIFAR-10的例子里一个线性分类器的权重矩阵W是10 x 3073维的,总共有30730个参数 -_-||),人要直接『看到』它的形状/变化是非常困难的。但是机智的同学们,总是能想出一些办法,把损失函数在某种程度上可视化的。比如说,我们可以把高维投射到一个向量/方向(1维)或者一个面(2维)上,从而能直观地『观察』到一些变化。
举个例子说,我们可以对一个权重矩阵W(例如CIFAR−10中是30730个参数),可以找到W维度空间中的一条直线,然后沿着这条线,计算一下损失函数值的变化情况。具体一点说,就是我们找到一个方向W1(维度要和W一样,才能表示W的维度空间的一个方向/一条直线),然后我们给不同的a值,计算L(W+aW1),这样,如果a取得足够密,其实我们就能够在一定程度上描绘出损失函数沿着这个方向的变化了。
同样,如果我们给两个方向W1和W2,那么我们可以确定一个平面,我们再取不同值的a和b,计算L(W+aW1+bW2)的值,那么我们就可以大致绘出在这个平面上,损失函数的变化情况了。
根据上面的方法,我们画出了下面3个图。最上面的图是调整a的不同取值,绘出的损失函数变化曲线(越高值越大);中间和最后一个图是调整a与b的取值,绘出的损失函数变化图(蓝色表示损失小,红色表示损失大),中间是在一个图片样本上计算的损失结果,最下图为100张图片上计算的损失结果的一个平均。显然沿着直线方向得到的曲线底端为最小的损失值点,而曲面呈现的碗状图形`碗底`为损失函数取值最小处。
![损失函数沿直线投影图](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad13af17.png "")
![损失函数沿平面投影图2](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad14b512.jpg "")
![损失函数沿平面投影图2](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad161c38.jpg "")
我们从数学的角度,来尝试解释一下,上面的凹曲线是怎么出来的。对于第i个样本,我们知道它的损失函数值为:
Li=∑j≠yi[max(0,wTjxi−wTyixi+1)]
在所有的样本上的损失函数值,是它们损失函数值(`max(0,-)`,因此最小值为0)的平均值。为了更好理解,我们假定训练集里面有3个样本,都是1维的,同时总共有3个类别。所以SVM损失(暂时不考虑正则化项)可以表示为如下的式子:
L0=L1=L2=L=max(0,wT1x0−wT0x0+1)+max(0,wT2x0−wT0x0+1)max(0,wT0x1−wT1x1+1)+max(0,wT2x1−wT1x1+1)max(0,wT0x2−wT2x2+1)+max(0,wT1x2−wT2x2+1)(L0+L1+L2)/3
因为这个例子里的样本都是1维的,因此其实xi和wj都是实数。拿w0举例,损失函数里,大于0的值其实都和w0是线性关系的,而最小值为0。因此,我们可以想象成,三条折线『合体』得到的最终曲线,如下图所示:
![曲线的形成](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad172249.png "")
插几句题外话,从之前碗状结构的示意图,你可能会猜到SVM损失函数是一个凸函数,而对于凸函数的最小值求解方法有很多种。但之后当我们把损失函数f扩充到神经网络之后,损失函数将变成一个非凸函数,而如果依旧可视化的话,我们看到的将不再是一个碗状结构,而是凹凸不平的。
### 3. 最优化
在我们现在这个问题中,所谓的『最优化』其实指的就是找到能让损失函数最小的参数W。如果大家看过或者了解`凸优化`的话,我们下面介绍的方法,对你而言可能太简单了,有点`原始`,但是大家别忘了,我们后期要处理的是神经网络的损失函数,那可不是一个凸函数哦,所以我们还是一步步来一起看看,如果去实现最优化问题。
#### 3.1 策略1:随机搜寻(不太实用)
以一个笨方法开始,我们知道,当我们手头上有参数W后,我们是可以计算损失函数,评估参数合适程度的。所以最直接粗暴的方法就是,我们尽量多地去试参数,然后从里面选那个让损失函数最小的,作为最后的W。代码当然很简单,如下:
~~~
# 假设 X_train 是训练集 (例如. 3073 x 50,000)
# 假设 Y_train 是类别结果 (例如. 1D array of 50,000)
bestloss = float("inf") # 初始化一个最大的float值
for num in xrange(1000):
W = np.random.randn(10, 3073) * 0.0001 # 随机生成一组参数
loss = L(X_train, Y_train, W) # 计算损失函数
if loss < bestloss: # 比对已搜寻中最好的结果
bestloss = loss
bestW = W
print 'in attempt %d the loss was %f, best %f' % (num, loss, bestloss)
# prints:
# in attempt 0 the loss was 9.401632, best 9.401632
# in attempt 1 the loss was 8.959668, best 8.959668
# in attempt 2 the loss was 9.044034, best 8.959668
# in attempt 3 the loss was 9.278948, best 8.959668
# in attempt 4 the loss was 8.857370, best 8.857370
# in attempt 5 the loss was 8.943151, best 8.857370
# in attempt 6 the loss was 8.605604, best 8.605604
# ... (trunctated: continues for 1000 lines)
~~~
一通随机试验和搜寻之后,我们会拿到试验结果中最好的参数W,然后在测试集上看看效果:
~~~
# 假定 X_test 为 [3073 x 10000], Y_test 为 [10000 x 1]
scores = Wbest.dot(Xte_cols) # 10 x 10000, 计算类别得分
# 找到最高得分作为结果
Yte_predict = np.argmax(scores, axis = 0)
# 计算准确度
np.mean(Yte_predict == Yte)
# 返回 0.1555
~~~
随机搜寻得到的参数W,在测试集上的准确率为**15.5%**,总共10各类别,我们不做任何预测只是随机猜的结果应该是10%,好像稍高一点,但是…大家也看到了…这个准确率…实在是没办法在实际应用中使用。
#### 3.2 策略2:随机局部搜索
上一个策略完全就是盲搜,要想找到全局最优的那个结果基本是不可能的。它最大的缺点,就在于下一次搜索完全是随机进行的,没有一个指引方向。那我们多想想,就能想出一个在上个策略的基础上,优化的版本,叫做『随机局部搜索』。
这个策略的意思是,我们不每次都随机产生一个参数矩阵W了,而是在现有的参数W基础上,搜寻一下周边临近的参数,有没有比现在参数更好的W,然后我们用新的W替换现在的W,接着在周围继续小范围搜寻。这个过程呢,可以想象成,我们在一座山上,现在要下山,然后我们每次都伸脚探一探周边,找一个比现在的位置下降一些的位置,然后迈一步,接着在新的位置上做同样的操作,一步步直至下山。
从代码实现的角度看,以上的过程,实际上就是对于一个当前W,我们每次实验和添加δW′,然后看看损失函数是否比当前要低,如果是,就替换掉当前的W,代码如下:
~~~
W = np.random.randn(10, 3073) * 0.001 # 初始化权重矩阵W
bestloss = float("inf")
for i in xrange(1000):
step_size = 0.0001
Wtry = W + np.random.randn(10, 3073) * step_size
loss = L(Xtr_cols, Ytr, Wtry)
if loss < bestloss:
W = Wtry
bestloss = loss
print 'iter %d loss is %f' % (i, bestloss)
~~~
我们做了这么个小小的修正之后,我们再拿刚才一样的测试集来测一下效果,结果发现准确率提升至21.4%,虽然离实际应用差很远,但只是比刚才要进步一点点了。
但是还是有个问题,我们每次测试周边点的损失函数,是一件非常耗时的事情。我们有没有办法能够直接找到我们应该迭代的方向呢?
#### 3.3 策略3:顺着梯度下滑
刚才的策略,我们说了,最大的缺点是非常耗时,且计算量也很大。我们一直在做的事情,就是在当前的位置基础上,想找到一个最合适的下降方向。我们依旧回到我们假设的那个情境,如果我们在山顶,要以最快的方式下山,我们会怎么做?我们可能会环顾四周,然后找到最陡的方向,迈一小步,然后再找当前位置最陡的下山方向,再迈一小步…
而这里提到的最陡的方向,其实对应的就是数学里『梯度』的概念,也就是说,其实我们无需『伸脚试探』周边的陡峭程度,而是可以通过计算损失函数的梯度,直接取得这个方向。
我们知道在1个变量的函数里,某点的斜率/导数代表其变化率最大的方向。而对于多元的情况,梯度是上面情况的一个扩展,只不过这时候的变量不再是一个,而是多个,同时我们计算得到的『梯度方向』也是一个多维的向量。大家都知道数学上计算1维/元函数『梯度/导数』的表达式如下:
df(x)dx=limh →0f(x+h)−f(x)h
对于多元的情况,这个时候我们需要求的东西扩展成每个方向的『偏导数』,然后把它们合在一块组成我们的梯度向量。
我们用几张图来说明这个过程:
![梯度下降1](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad1808eb.jpg "")
![梯度下降2](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad195594.jpg "")
![各种下降算法](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad1b383b.gif "")
### 4. 计算梯度
有两种计算梯度的方法:
1. 慢一些但是简单一些的`数值梯度/numerical gradient`
1. 速度快但是更容易出错的`解析梯度/analytic gradient`
#### 4.1 数值梯度
根据上面提到的导数求解公式,我们可以得到数值梯度计算法。下面是一段简单的代码,对于一个给定的函数`f`和一个向量`x`,求解这个点上的梯度:
~~~
def eval_numerical_gradient(f, x):
"""
一个最基本的计算x点上f的梯度的算法
- f 为参数为x的函数
- x 是一个numpy的vector
"""
fx = f(x) # 计算原始点上函数值
grad = np.zeros(x.shape)
h = 0.00001
# 对x的每个维度都计算一遍
it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
while not it.finished:
# 计算x+h处的函数值
ix = it.multi_index
old_value = x[ix]
x[ix] = old_value + h # 加h
fxh = f(x) # 计算f(x + h)
x[ix] = old_value # 存储之前的函数值
# 计算偏导数
grad[ix] = (fxh - fx) / h # 斜率
it.iternext() # 开始下一个维度上的偏导计算
return grad
~~~
代码的方法很简单,对每个维度,都在原始值上加上一个很小的`h`,然后计算这个维度/方向上的偏导,最后组在一起得到梯度`grad`。
#### 4.1.1 实际计算中的提示
我们仔细看看导数求解的公式,会发现数学定义上h是要趋于0的,但实际我们计算的时候我们只要取一个足够小的数(比如1e-5)作为h就行了,所以我们要精准计算偏导的话,要尽量取到不会带来数值计算问题,同时又能很小的h。另外,其实实际计算中,我们用另外一个公式用得更多[f(x+h)−f(x−h)]/2h
下面我们用上面的公式在CIFAR-10数据集上,试一试吧:
~~~
def CIFAR10_loss_fun(W):
return L(X_train, Y_train, W)
W = np.random.rand(10, 3073) * 0.001 # 随机权重向量
df = eval_numerical_gradient(CIFAR10_loss_fun, W) # 计算梯度
~~~
计算到的梯度(准确地说,梯度的方向是函数增大方向,负梯度才是下降方向)告诉我们,我们应该『下山』的方向是啥,接着我们就沿着它小步迈进:
~~~
loss_original = CIFAR10_loss_fun(W) # 原始点上的损失
print 'original loss: %f' % (loss_original, )
# 多大步伐迈进好呢?我们选一些步长试试
for step_size_log in [-10, -9, -8, -7, -6, -5,-4,-3,-2,-1]:
step_size = 10 ** step_size_log
W_new = W - step_size * df # 新的权重
loss_new = CIFAR10_loss_fun(W_new)
print 'for step size %f new loss: %f' % (step_size, loss_new)
# 输出:
# original loss: 2.200718
# for step size 1.000000e-10 new loss: 2.200652
# for step size 1.000000e-09 new loss: 2.200057
# for step size 1.000000e-08 new loss: 2.194116
# for step size 1.000000e-07 new loss: 2.135493
# for step size 1.000000e-06 new loss: 1.647802
# for step size 1.000000e-05 new loss: 2.844355
# for step size 1.000000e-04 new loss: 25.558142
# for step size 1.000000e-03 new loss: 254.086573
# for step size 1.000000e-02 new loss: 2539.370888
# for step size 1.000000e-01 new loss: 25392.214036
~~~
#### 4.1.2 关于迭代的细节
如果大家仔细看上述代码的话,会发现我们step_size设的都是负的,确实我们每次update权重W的时候,是用原来的`W`减掉梯度方向的一个较小的值,这样损失函数才能减小。
#### 4.1.3 关于迭代的步长
我们计算得到梯度之后,就确定了幅度变化最快(负梯度是下降方向)的方向,但是它并没有告诉我们,我朝着这个方向,应该迈进多远啊。之后的章节会提到,选择正确的迭代步长(有时候我们也把它叫做`学习速率`)是训练过程中最重要(也是最让人头疼)的一个待设定参数。就像我想以最快的速度下山,我们能感知到最陡的方向,却不知道应该迈多大的步子。如果我们小步迈进,那确实每一步都能比上一步下降一些,但是速度太慢了亲!!但是如果我们以非常非常大的步伐迈进(假如腿巨长 -_-||),那你猜怎么着,你一不小心可能就迈过山脚迈到另一座山山腰上了…
下图是对以上情况的一个描述和解释:
![梯度下降](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad1ec7fd.jpg "")
图上红色的值很大,蓝色的值很小,我们想逐步下降至蓝色中心。如果迈进的步伐太小,收敛和行进的速度就会很慢,如果迈进的步伐太大,可能直接越过去了。
#### 4.1.4 效率问题
如果你再回过头去看看上面计算数值梯度的程序,你会发现,这个计算方法的复杂度,基本是和我们的参数个数成线性关系的。这意味着什么呢?在我们的CIFAR-10例子中,我们总共有30730个参数,因此我们单次迭代总共就需要计算30731次损失函数。这个问题在之后会提到的神经网络中更为严重,很可能两层神经元之间就有百万级别的参数权重,所以,计算机算起来都很耗时…人也要等结果等到哭瞎…
#### 4.2 解析法计算梯度
数值梯度发非常容易实现,但是从公式里面我们就看得出来,梯度实际上是一个近似(毕竟你没办法把`h`取到非常小),同时这也是一个计算非常耗时的算法。第二种计算梯度的方法是解析法,它可以让我们直接得到梯度的一个公式(代入就可以计算,非常快),但是呢,不像数值梯度法,这种方法更容易出现错误。so,聪明的同学们,就想了一个办法,我们可以先计算解析梯度和数值梯度,然后比对结果和校正,在确定我们解析梯度实现正确之后,我们就可以大胆地进行解析法计算了(这个过程叫做`梯度检查/检测`)
我们拿一个样本点的SVM损失函数举例:
Li=∑j≠yi[max(0,wTjxi−wTyixi+Δ)]
我们可以求它对每个权重的偏导数,比如说,我们求它对wyi的偏导,我们得到:
∇wyiLi=−⎛⎝⎜⎜∑j≠yi1(wTjxi−wTyixi+Δ>0)⎞⎠⎟⎟xi
其中1是一个bool函数,在括号内的条件为真的时候取值为1,否则为0。看起来似乎很吓人,但实际上要写代码完成的话,你只需要计算不满足指定SVM最小距离的类(对损失函数有贡献的类)的个数,然后用这个值会对数据向量xi做缩放即可得到梯度。但是要注意只是W中对应正确的类别的列的梯度。对于其他的j≠yi的情况,梯度为:
∇wjLi=1(wTjxi−wTyixi+Δ>0)xi
一旦得到梯度的表达式,那计算梯度和调整权重就变得非常直接和简单。熟练掌握如何在loss expression下计算梯度是非常重要的一个技巧,贯穿整个神经网络的训练实现过程,关于这个内容,下次会详细讲到。
### 5. 梯度下降
在我们有办法计算得到梯度之后,使用梯度去更新已有权重参数的过程叫做『梯度下降』,伪代码其实就是如下的样子:
~~~
while True:
weights_grad = evaluate_gradient(loss_fun, data, weights)
weights += - step_size * weights_grad # 梯度下降更新参数
~~~
这个简单的循环实质上就是很多神经网络库的核心。当然,我们也有其他的方式去实现最优化(比如说L-BFGS),但是梯度下降确实是当前使用最广泛,也相对最稳定的神经网络损失函数最优化方法。
#### 5.1 Mini-batch gradient descent
在大型的应用当中(比如ILSVRC),训练数据可能是百万千万级别的。因此,对整个训练数据集的样本都算一遍损失函数,以完成参数迭代是一件非常耗时的事情,一个我们通常会用到的替代方法是,采样出一个子集在其上计算梯度。现在比较前沿的神经网络结构基本都是这么做的,例如ConvNets是每256张作为一个batch去完成参数的更新。参数更新的代码如下:
~~~
while True:
data_batch = sample_training_data(data, 256) # 抽样256个样本作为一个batch
weights_grad = evaluate_gradient(loss_fun, data_batch, weights)
weights += - step_size * weights_grad # 参数更新
~~~
之所以可以这么做,是因为训练数据之间其实是关联的。我们简化一下这个问题,你想想,如果ILSVRC中的120w图片,如果只是1000张不同的图片,一直复制1200次得到的。那么其实我们在这1000张图片上算得的损失函数和120w的平均其实是一致的。当然,当然,在实际场景中,我们肯定很少遇到这种多次重复的情况,但是原数据的一个子集(mini-batch)上的梯度,其实也是对整体数据上梯度的一个很好的近似。因此,只在mini-batch上计算和更新参数,会有快得多的收敛速度。
上述算法的一个极端的情况是,如果我们的一个mini-batch里面只有一张图片。那这个过程就变成『随机梯度下降/Stochastic Gradient Descent (SGD)』,说起来,这个其实在实际应用中倒也没那么常见,原因是向量化之后,一次计算100张图片,其实比计算一张图片100次,要快得多。所以即使从定义上来说,SGD表示我们用一张图片上的梯度近似全局梯度,但是很多时候人们提到SGD的时候,其实他们指的是mini-batch梯度下降,也就是说,我们把一个batch当做1份了。额,还要稍微提一句的是,有些同学可能会问,这个batch size本身不是一个需要实验的参数吗,取多大的batch size好啊?但实际应用中,我们倒很少会用cross-validation去选择这个参数。这么说吧,我们一般是基于我们内存限制去取这个值的,比如设成100左右。
### 6. 总结
- 把损失函数在各参数上的取值,想象成我们所在山峰的高度。那么我们要最小化损失函数,实际上就是『要想办法下山』。
- 我们采取的下山策略是,一次迈一小步,只要每次都往下走了,那么最后就会到山底。
- 梯度对应函数变化最快的方向,负梯度的方向就是我们下山,环顾四周之后,发现最陡的下山路方向。
- 我们的步长(也叫学习率),会影响我们的收敛速度(下山速度),如果步伐特别特别大,甚至可能跃过最低点,跑到另外一个高值位置了。
- 我们用mini-batch的方式,用一小部分的样本子集,计算和更新参数,减少计算量,加快收敛速度。
(3)_线性SVM与SoftMax分类器
最后更新于:2022-04-01 14:21:40
作者: [寒小阳](http://blog.csdn.net/han_xiaoyang?viewmode=contents)
时间:2015年11月。
出处:[http://blog.csdn.net/han_xiaoyang/article/details/49999299](http://blog.csdn.net/han_xiaoyang/article/details/49999299)
声明:版权所有,转载请注明出处,谢谢。
### 1. 线性分类器
在[深度学习与计算机视觉系列(2)](http://blog.csdn.net/han_xiaoyang/article/details/49999583)我们提到了图像识别的问题,同时提出了一种简单的解决方法——KNN。然后我们也看到了KNN在解决这个问题的时候,虽然实现起来非常简单,但是有很大的弊端:
- 分类器必须记住全部的训练数据(因为要遍历找近邻啊!!),而在任何实际的图像训练集上,数据量很可能非常大,那么一次性载入内存,不管是速度还是对硬件的要求,都是一个极大的挑战。
- 分类的时候要遍历所有的训练图片,这是一个相当相当相当耗时的过程。
这个部分我们介绍一类新的分类器方法,而对其的改进和启发也能帮助我们自然而然地过渡到深度学习中的卷积神经网。有两个重要的概念:
- 得分函数/score function:将原始数据映射到每个类的打分的函数
- 损失函数/loss function:用于量化模型预测结果和实际结果之间吻合度的函数
在我们得到损失函数之后,我们就将问题转化成为一个最优化的问题,目标是得到让我们的损失函数取值最小的一组参数。
### 2. 得分函数/score function
首先我们定义一个有原始的图片像素值映射到最后类目得分的函数,也就是这里提到的得分函数。先笼统解释一下,一会儿我们给个具体的实例来说明。假设我们的训练数据为xi∈RD,对应的标签yi,这里i=1…N表示N个样本,yi∈1…K表示K类图片。
比如CIFAR-10数据集中N=50000,而D=32x32x3=3072像素,K=10,因为这时候我们有10个不同的类别(狗/猫/车…),我们实际上要定义一个将原始像素映射到得分上函数 f:RD↦RK
#### 2.1 线性分类器
我们先丢出一个简单的线性映射:
f(xi,W,b)=Wxi+b
在这个公式里,我们假定图片的像素都平展为[D x 1]的向量。然后我们有两个参数:W是[K x D]的矩阵,而向量b为[K x 1]的。在CIFAR-10中,每张图片平展开得到一个[3072 x 1]的向量,那W就应该是一个[10 x 3072]的矩阵,b为[10 x 1]的向量。
这样,以我们的线性代数知识,我们知道这个函数,接受3072个数作为输入,同时输出10个数作为类目得分。我们把W叫做权重,b叫做偏移向量。
说明几个点:
- 我们知道一次矩阵运算,我们就可以借助W把原始数据映射为10个类别的得分。
- 其实我们的输入(xi,yi)其实是固定的,我们现在要丛的事情是,我们要调整W, b使得我们的得分结果和实际的类目结果最为吻合。
- 我们可以想象到,这样一种分类解决方案的优势是,一旦我们找到合适的参数,那么我们最后的模型可以简化到只有保留W, b即可,而所有原始的训练数据我们都可以不管了。
- 识别阶段,我们需要做的事情仅仅是一次矩阵乘法和一次加法,这个计算量相对之前…不要小太多好么…
> 提前剧透一下,其实卷积神经网做的事情也是类似的,将原始输入的像素映射成类目得分,只不过它的中间映射更加复杂,参数更多而已…
#### 2.2 理解线性分类器
我们想想,其实线性分类器在做的事情,是对每个像素点的三个颜色通道,做计算。咱们拟人化一下,帮助我们理解,可以认为设定的参数/权重不同会影响分类器的『性格』,从而使得分类器对特定位置的颜色会有自己的喜好。
举个例子,假如说我们的分类器要识别『船只』,那么它可能会喜欢图片的四周都是蓝色(额,通常船只是在水里海里吧…)。
我们用一个实际的例子来表示这个得分映射的过程,大概就是下图这个样子:
![得分函数](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad05ae69.jpg "")
原始像素点向量xi经过W和b映射为对应结果类别的得分f(xi,W,b)=Wxi+b。不过上面这组参数其实给的是不太恰当的,因为我们看到在这组参数下,图片属于狗狗的得分最高 -_-||
#### 2.2.1 划分的理解_1
图片被平展开之后,向量维度很高,高维空间比较难想象。我们简化一下,假如把图片像素输入,看做可以压缩到二维空间之中的点,那我们想想,分类器实际上在做的事情就如下图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad0782cb.png)
W中的每一列对应类别中的每一类,而当我们改变W中的值的时候,图上的线的方向会跟着改变,那么b呢?对,b是一个偏移量,它表示当我们的直线方向确定以后,我们可以适当平移直线到合适的位置。没有b会怎么样呢,如果直线没有偏移量,那意味着所有的直线都要通过原点,这种强限制条件下显然不能保证很好的平面类别分割。
#### 2.2.2 划分的理解_2
对W第二种理解方式是,W的每一行可以看做是其中一个类别的模板。而我们输入图片相对这个类别的得分,实际上是像素点和模板匹配度(通过**内积**运算获得),而类目识别实际上就是在匹配图像和所有类别的模板,找到匹配度最高的那个。
是不是感觉和KNN有点类似的意思?是有那么点相近,但是这里我们不再比对所有图片,而是比对类别的模板,这样比对次数只和类目数K有关系,所以自然计算量要小很多,同时比对的时候用的不再是l1或者l2距离,而是内积计算。
我们提前透露一下CIFAR-10上学习到的模板的样子:
![CIFAR-10模板](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad09f420.jpg "")
你看,和我们设想的很接近,ship类别的周边有大量的蓝色,而car的旁边是土地的颜色。
#### 2.2.3 关于偏移量的处理
我们先回到如下的公式:
f(xi,W,b)=Wxi+b
公式中有W和b两个参数,我们知道调节两个参数总归比调节一个参数要麻烦,所以我们用一点小技巧,来把他们组合在一起,放到一个参数中。
我们现在要做的运算是矩阵乘法再加偏移量,最常用的合并方法就是,想办法把b合并成W的一部分。我们仔细看看下面这张图片:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad0b6651.png)
我们给输入的像素矩阵加上一个1,从而把b拼接到W里变成一个变量。依旧拿CIFAR-10举例,原本是[3072 x 1]的像素向量,我们添上最后那个1变成[3073 x 1]的向量,而[W]变成[W b]。
#### 2.2.4 关于数据的预处理
插播一段,实际应用中,我们很多时候并不是把原始的像素矩阵作为输入,而是会预先做一些处理,比如说,有一个很重要的处理叫做『去均值』,他做的事情是对于训练集,我们求得所有图片像素矩阵的均值,作为中心,然后输入的图片先减掉这个均值,再做后续的操作。有时候我们甚至要对图片的幅度归一化/scaling。去均值是一个非常重要的步骤,原因我们在后续的梯度下降里会提到。
#### 2.3 损失函数
我们已经通过参数W,完成了由像素映射到类目得分的过程。同时,我们知道我们的训练数据(xi,yi)是给定的,我们可以调整的是参数/权重W,使得这个映射的结果和实际类别是吻合的。
我们回到最上面的图片中预测猫/狗/船得分的例子里,这个图片中给定的W显然不是一个合理的值,预测的结果和实际情况有很大的偏差。于是我们现在要想办法,去把这个偏差表示出来,拟人一点说,就是我们希望我们的模型在训练的过程中,能够对输出的结果计算并知道自己做的好坏。
而能帮助我们完成这件事情的工具叫做『损失函数/loss function』,额,其实它还有很多其他的名字,比如说,你说不定在其他的地方听人把它叫做『代价函数/cost function』或者『客观度/objective』,直观一点说,就是我们输出的结果和实际情况偏差很大的时候,损失/代价就会很大。
#### 2.3.1 多类别支持向量机损失/Multiclass Support Vector Machine loss
腻害的大神们定义出了好些损失函数,我们这里首先要介绍一种极其常用的,叫做多类别支持向量机损失(Multiclass SVM loss)。如果要用一句精简的话来描述它,就是它(SVM)希望正确的类别结果获得的得分比不正确的类别,至少要高上一个固定的大小Δ。
我们先解释一下这句话,一会儿再举个例子说明一下。对于训练集中的第i张图片数据xi,我们的得分函数,在参数W下会计算出一个所有类得分结果f(xi,W),其中第j类得分我们记作f(xi,W)j,该图片的实际类别为yi,则对于第i张样本图片,我们的损失函数是如下定义的:
Li=∑j≠yimax(0,f(xi,W)j−f(xi,W)yi+Δ)
看公式容易看瞎,博主也经常深深地为自己智商感到捉急,我们举个例子来解释一下这个公式。
假如我们现在有三个类别,而得分函数计算某张图片的得分为f(xi,W)=[13,−7,11],而实际的结果是第一类(yi=0)。假设Δ=10(这个参数一会儿会介绍)。上面的公式把错误类别(j≠yi)都遍历了一遍,求值加和:
Li=max(0,−7−13+10)+max(0,11−13+10)
仔细看看上述的两项,左边项-10和0中的最大值为0,因此取值是零。其实这里的含义是,实际的类别得分13要比第二类得分-7高出20,超过了我们设定的正确类目和错误类目之间的最小margin Δ=10,因此第二类的结果我们认为是满意的,并不带来loss,所以值为0。而第三类得分11,仅比13小2,没有大于Δ=10,因此我们认为他是有损失/loss的,而损失就是当前距离2距离设定的最小距离Δ的差距8。
注意到我们的得分函数是输入像素值的一个线性函数(f(xi;W)=Wxi),因此公式又可以简化为(其中wj是W的第j行):
Li=∑j≠yimax(0,wTjxi−wTyixi+Δ)
我们还需要提一下的是,关于损失函数中max(0,-)的这种形式,我们也把它叫做hinge loss/铰链型损失,有时候你会看到squared hinge loss SVM(也叫L2-SVM),它用到的是max(0,−)2,这个损失函数惩罚那些在设定Δ距离之内的错误类别的惩罚度更高。两种损失函数标准在特定的场景下效果各有优劣,要判定用哪个,还是得借助于交叉验证/cross-validation。
对于损失函数的理解,可以参照下图:
![损失函数的理解](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad0ddf30.jpg "")
#### 2.3.2 正则化
如果大家仔细想想,会发现,使用上述的loss function,会有一个bug。如果参数W能够正确地识别训练集中所有的图片(损失函数为0)。那么我们对M做一些变换,可以得到无数组也能满足loss function=0的参数W’(举个例子,对于λ>1的所有λW,原来的错误类别和正确类别之间的距离已经大于Δ,现在乘以λ,更大了,显然也能满足loss为0)。
于是…我们得想办法把W参数的这种不确定性去除掉啊…这就是我们要提到的正则化,我们需要在原来的损失函数上再加上一项正则化项(regularization penalty R(W)),最常见的正则化项是L2范数,它会对幅度很大的特征权重给很高的惩罚:
R(W)=∑k∑lW2k,l
根据公式可以看到,这个表达式R(W)把所有W的元素的平方项求和了。而且它和数据本身无关,只和特征权重有关系。
我们把两部分组(数据损失/data loss和正则化损失/regularization loss)在一起,得到完整的多类别SVM损失权重,如下:
L=1N∑iLidata loss+λR(W)⏟regularization loss
也可以展开,得到更具体的完整形式:
L=1N∑i∑j≠yi[max(0,f(xi;W)j−f(xi;W)yi+Δ)]+λ∑k∑lW2k,l
其中N是训练样本数,我们给正则化项一个参数λ,但是这个参数的设定只有通过实验确定,对…还是得交叉验证/cross-validation。
关于设定这样一个正则化惩罚项为什么能解决W的不确定性,我们在之后的系列里会提到,这里我们举个例子简单看看,这个项是怎么起到作用的。
假定我们的输入图片像素矩阵是x=[1,1,1,1],而现在我们有两组不同的W权重参数中对应的向量w1=[1,0,0,0],w2=[0.25,0.25,0.25,0.25]。那我们很容易知道wT1x=wT2x=1,所以不加正则项的时候,这俩得到的结果是完全一样的,也就意味着——它们是等价的。但是加了正则项之后,我们发现w2总体的损失函数结果更小(因为4*0.25^2<1),于是我们的系统会选择w2,这也就意味着系统更『喜欢』权重分布均匀的参数,而不是某些特征权重明显高于其他权重(占据绝对主导作用)的参数。
之后的系列里会提到,这样一个平滑的操作,实际上也会提高系统的泛化能力,让其具备更高的通用性,而不至于在训练集上过拟合。
另外,我们在讨论过拟合的这个部分的时候,并没有提到b这个参数,这是因为它并不具备像W一样的控制输入特征的某个维度影响力的能力。还需要说一下的是,因为正则项的引入,训练集上的准确度是会有一定程度的下降的,我们永远也不可能让损失达到零了(因为这意味着正则化项为0,也就是W=0)。
下面是简单的计算损失函数(没加上正则化项)的代码,有未向量化和向量化两种形式:
~~~
def L_i(x, y, W):
"""
未向量化版本.
对给定的单个样本(x,y)计算multiclass svm loss.
- x: 代表图片像素输入的向量 (例如CIFAR-10中是3073 x 1,因为添加了bias项对应的1到x中)
- y: 图片对应的类别编号(比如CIFAR-10中是0-9)
- W: 权重矩阵 (例如CIFAR-10中是10 x 3073)
"""
delta = 1.0 # 设定delta
scores = W.dot(x) # 内积计算得分
correct_class_score = scores[y]
D = W.shape[0] # 类别数:例如10
loss_i = 0.0
for j in xrange(D): # 遍历所有错误的类别
if j == y:
# 跳过正确类别
continue
# 对第i个样本累加loss
loss_i += max(0, scores[j] - correct_class_score + delta)
return loss_i
def L_i_vectorized(x, y, W):
"""
半向量化的版本,速度更快。
之所以说是半向量化,是因为这个函数外层要用for循环遍历整个训练集 -_-||
"""
delta = 1.0
scores = W.dot(x)
# 矩阵一次性计算
margins = np.maximum(0, scores - scores[y] + delta)
margins[y] = 0
loss_i = np.sum(margins)
return loss_i
def L(X, y, W):
"""
全向量化实现 :
- X: 包含所有训练样本中数据(例如CIFAR-10是3073 x 50000)
- y: 所有的类别结果 (例如50000 x 1的向量)
- W: 权重矩阵 (例如10 x 3073)
"""
#待完成...
~~~
说到这里,其实我们的损失函数,是提供给我们一个数值型的表示,来衡量我们的预测结果和实际结果的差别。而要提高预测的准确性,要做的事情是,想办法最小化这个loss。
#### 2.4 一些现实的考虑点
#### 2.4.1 设定Delta
我们在计算Multi SVM loss的时候,Δ是我们提前设定的一个参数。这个值咋设定?莫不是…也需要交叉验证…?其实基本上大部分的场合下我们设定Δ=1.0都是一个安全的设定。我们看公式中的参数Δ和λ似乎是两个截然不同的参数,实际上他俩做的事情比较类似,都是尽量让模型贴近标准预测结果的时候,在 数据损失/data loss和 正则化损失/regularization loss之间做一个交换和平衡。
在损失函数计算公式里,可以看出,权重W的幅度对类别得分有最直接的影响,我们减小W,最后的得分就会减少;我们增大W,最后的得分就增大。从这个角度看,Δ这个参数的设定(Δ=1或者Δ=100),其实无法限定W的伸缩。而真正可以做到这点的是正则化项,λ的大小,实际上控制着权重可以增长和膨胀的空间。
#### 2.4.2 关于二元/Binary支持向量机
如果大家之前接触过Binary SVM,我们知道它的公式如下:
Li=Cmax(0,1−yiwTxi)+R(W)
我们可以理解为类别yi∈−1,1,它是我们的多类别识别的一个特殊情况,而这里的C和λ是一样的作用,只不过他们的大小对结果的影响是相反的,也就是C∝1λ
#### 2.4.3 关于非线性的SVM
如果对机器学习有了解,你可能会了解很多其他关于SVM的术语:kernel,dual,SMO算法等等。在这个系列里面我们只讨论最基本的线性形式。当然,其实从本质上来说,这些方法都是类似的。
### 2.5 Softmax分类器
话说其实有两种特别常见的分类器,前面提的SVM是其中的一种,而另外一种就是Softmax分类器,它有着截然不同的损失函数。如果你听说过『逻辑回归二分类器』,那么Softmax分类器是它泛化到多分类的情形。不像SVM这种直接给类目打分f(xi,W)并作为输出,Softmax分类器从新的角度做了不一样的处理,我们依旧需要将输入的像素向量映射为得分f(x_i; W) = W x_i,只不过我们还需要将得分映射到概率域,我们也不再使用hinge loss了,而是使用互熵损失/cross-entropy loss,形式如下:
Li=−log(efyi∑jefj)或者Li=−fyi+log∑jefj
我们使用fj来代表得分向量f的第j个元素值。和前面提到的一样,总体的损失/loss也是Li遍历训练集之后的均值,再加上正则化项R(W),而函数fj(z)=ezj∑kezk被称之为softmax函数:它的输入值是一个实数向量z,然后在指数域做了一个归一化(以保证之和为1)映射为概率。
#### 2.5.1 信息论角度的理解
对于两个概率分布p(“真实的概率分布”)和估测的概率分布q(估测的属于每个类的概率),它们的互熵定义为如下形式:
H(p,q)=−∑xp(x)logq(x)
而Softmax分类器要做的事情,就是要最小化预测类别的概率分布(之前看到了,是q=efyi/∑jefj)与『实际类别概率分布』(p=[0,…1,…,0],只在结果类目上是1,其余都为0)两个概率分布的互熵。
另外,因为互熵可以用熵加上KL距离/Kullback-Leibler Divergence(也叫相对熵/Relative Entropy)来表示,即H(p,q)=H(p)+DKL(p||q),而p的熵为0(这是一个确定事件,无随机性),所以互熵最小化,等同于最小化两个分布之间的KL距离。换句话说,互熵想要从给定的分布q上预测结果分布p。
#### 2.5.2 概率角度的理解
我们再来看看以下表达式
P(yi∣xi;W)=efyi∑jefj
其实可以看做给定图片数据xi和类别yi以及参数W之后的归一化概率。在概率的角度理解,我们在做的事情,就是最小化错误类别的负log似然概率,也可以理解为进行最大似然估计/Maximum Likelihood Estimation (MLE)。这个理解角度还有一个好处,这个时候我们的正则化项R(W)有很好的解释性,可以理解为整个损失函数在权重矩阵W上的一个高斯先验,所以其实这时候是在做一个最大后验估计/Maximum a posteriori (MAP)。
#### 2.5.3 实际工程上的注意点:数据稳定性
在我们要写代码工程实现Softmax函数的时候,计算的中间项efyi和∑jefj因为指数运算可能变得非常大,除法的结果非常不稳定,所以这里需要一个小技巧。注意到,如果我们在分子分母前都乘以常数C,然后整理到指数上,我们会得到下面的公式:
efyi∑jefj=CefyiC∑jefj=efyi+logC∑jefj+logC
C的取值由我们而定,不影响最后的结果,但是对于实际计算过程中的稳定性有很大的帮助。一个最常见的C取值为logC=−maxjfj。这表明我们应该平移向量f中的值使得最大值为0,以下的代码是它的一个实现:
~~~
f = np.array([123, 456, 789]) # 3个类别的预测示例
p = np.exp(f) / np.sum(np.exp(f)) # 直接运算,数值稳定性不太好
# 我们先对数据做一个平移,所以输入的最大值为0:
f -= np.max(f) # f 变成 [-666, -333, 0]
p = np.exp(f) / np.sum(np.exp(f)) # 结果正确,同时解决数值不稳定问题
~~~
#### 2.5.4 关于softmax这个名字的一点说明
准确地说,SVM分类器使用hinge loss(有时候也叫max-margin loss)。而Softmax分类器使用互熵损失/cross-entropy loss。Softmax分类器从softmax函数(恩,其实做的事情就是把一列原始的类别得分归一化到一列和为1的正数表示概率)得到,softmax函数使得互熵损失可以用起来。而实际上,我们并没有softmax loss这个概念,因为softmax实质上就是一个函数,有时候我们图方便,就随口称呼softmax loss。
#### 2.6 SVM 与 Softmax
这个比较很有意思,就像在用到分类算法的时候,就会想SVM还是logistic regression呢一样。
我们先用一张图来表示从输入端到分类结果,SVM和Softmax都做了啥:
![SVM与SoftMax](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad0ec093.png "")
区别就是拿到原始像素数据映射得到的得分之后的处理,而正因为处理方式不同,我们定义不同的损失函数,有不同的优化方法。
#### 2.6.1 另外的差别
- SVM下,我们能完成类别的判定,但是实际上我们得到的类别得分,大小顺序表示着所属类别的排序,但是得分的绝对值大小并没有特别明显的物理含义。
- Softmax分类器中,结果的绝对值大小表征属于该类别的概率。
举个例子说,SVM可能拿到对应 猫/狗/船 的得分[12.5, 0.6, -23.0],同一个问题,Softmax分类器拿到[0.9, 0.09, 0.01]。这样在SVM结果下我们只知道『猫』是正确答案,而在Softmax分类器的结果中,我们可以知道属于每个类别的概率。
但是,Softmax中拿到的概率,其实和正则化参数λ有很大的关系,因为λ实际上在控制着W的伸缩程度,所以也控制着最后得分的scale,这会直接影响最后概率向量中概率的『分散度』,比如说某个λ下,我们得到的得分和概率可能如下:
[1,−2,0]→[e1,e−2,e0]=[2.71,0.14,1]→[0.7,0.04,0.26]
而我们加大λ,提高其约束能力后,很可能得分变为原来的一半大小,这时候如下:
[0.5,−1,0]→[e0.5,e−1,e0]=[1.65,0.37,1]→[0.55,0.12,0.33]
因为λ的不同,使得最后得到的结果概率分散度有很大的差别。在上面的结果中,猫有着统治性的概率大小,而在下面的结果中,它和船只的概率差距被缩小。
#### 2.6.2 际应用中的SVM与Softmax分类器
实际应用中,两类分类器的表现是相当的。当然,每个人都有自己的喜好和倾向性,习惯用某类分类器。
一定要对比一下的话:
SVM其实并不在乎每个类别得到的绝对得分大小,举个例子说,我们现在对三个类别,算得的得分是[10, -2, 3],实际第一类是正确结果,而设定Δ=1,那么10-3=7已经比1要大很多了,那对SVM而言,它觉得这已经是一个很标准的答案了,完全满足要求了,不需要再做其他事情了,结果是 [10, -100, -100] 或者 [10, 9, 9],它都是满意的。
然而对于Softmax而言,不是这样的, [10, -100, -100] 和 [10, 9, 9]映射到概率域,计算得到的互熵损失是有很大差别的。所以Softmax是一个永远不会满足的分类器,在每个得分计算到的概率基础上,它总是觉得可以让概率分布更接近标准结果一些,互熵损失更小一些。
有兴趣的话,[W与得分预测结果demo](http://vision.stanford.edu/teaching/cs231n/linear-classify-demo/)是一个可以手动调整和观察二维数据上的分类问题,随W变化结果变化的demo,可以动手调调看看。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad11505e.png)
(2)_图像分类与KNN
最后更新于:2022-04-01 14:21:37
作者: [寒小阳](http://blog.csdn.net/han_xiaoyang?viewmode=contents)
时间:2015年11月。
出处:[http://blog.csdn.net/han_xiaoyang/article/details/49949535](http://blog.csdn.net/han_xiaoyang/article/details/49949535)
声明:版权所有,转载请注明出处,谢谢
### 1. 图像分类问题
这是人每天自然而然会做的事情,普通到大部分时候,我们都感知不到我们在完成一个个这样的任务。早晨起床洗漱,你要看看洗漱台一堆东西中哪个是杯子,哪个是你的牙刷;吃早餐的时候你要分辨食物和碗碟…
抽象一下,对于一张输入的图片,要判定它属于给定的一些**标签/类别**中的哪一个。看似很简单的一个问题,这么多年却一直是计算机视觉的一个核心问题,应用场景也很多。它的重要性还体现在,其实其他的一些计算机视觉的问题(比如说物体定位和识别、图像内容分割等)都可以基于它去完成。
咱们举个例子从机器学习的角度描述一下这个问题^_^
计算机拿到一张图片(如下图所示),然后需要给出它对应{猫,狗,帽子,杯子}4类的概率。人类是灰常牛逼的生物,我们一瞥就知道这货是猫。然而对计算机而言,他们是没办法像人一样『看』到整张图片的。对它而言,这是一个3维的大矩阵,包含248*400个像素点,每个像素点又有红绿蓝(RGB)3个颜色通道的值,每个值在0(黑)-255(白)之间,计算机就需要根据这248 * 400 * 3=297600个数值去判定这货是『猫』
![猫图像=>矩阵](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90acf49fb7.png "")
#### 1.1 图像识别的难点
图像识别看似很直接。但实际上包含很多挑战,人类可是经过数亿年的进化才获得如此强大的大脑,对于各种物体有着精准的视觉理解力。总体而言,我们想『教』会计算机去认识一类图,会有下面这样一些困难:
- **视角不同**,每个事物旋转或者侧视最后的构图都完全不同
- **尺寸大小不统一**,相同内容的图片也可大可小
- **变形**,很多东西处于特殊的情形下,会有特殊的摆放和形状
- **光影等干扰/幻象**
- **背景干扰**
- **同类内的差异(比如椅子有靠椅/吧椅/餐椅/躺椅…)**
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90acf72d01.png)
#### 1.2 识别的途径
首先,大家想想就知道,这个算法并不像『对一个数组排序』或者『求有向图的最短路径』,我们没办法提前制定一个流程和规则去解决。定义『猫』这种动物本身就是一件很难的事情了,更不要说去定义一只猫在图像上的固定表现形式。所以我们寄希望于机器学习,使用`『Data-driven approach/数据驱动法』`来做做尝试。简单说来,就是对于每个类别,我们都找一定量的图片数据,『喂』给计算机,让它自己去『学习和总结』每一类的图片的特点。对了,这个过程和小盆友学习新鲜事物是一样一样的。『喂』给计算机学习的图片数据就和下图的猫/狗/杯子/帽子一样:
![Data-driven approach](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90acf8cc40.jpg "")
#### 1.3 机器学习解决图像分类的流程/Pipeline
整体的流程和普通机器学习完全一致,简单说来,也就下面三步:
- **输入**:我们的给定K个类别的N张图片,作为计算机学习的训练集
- **学习**:让计算机逐张图片地『观察』和『学习』
- **评估**:就像我们上学学了东西要考试检测一样,我们也得考考计算机学得如何,于是我们给定一些计算机不知道类别的图片让它判别,然后再比对我们已知的正确答案。
### 2. 最近邻分类器(Nearest Neighbor Classifier)
先从简单的方法开始说,先提一提**最近邻分类器/Nearest Neighbor Classifier**,不过事先申明,它和深度学习中的卷积神经网/Convolutional Neural Networks其实一点关系都没有,我们只是从基础到前沿一点一点推进,最近邻是图像识别一个相对简单和基础的实现方式。
#### 2.1 CIFAR-10
[CIFAR-10](http://www.cs.toronto.edu/~kriz/cifar.html)是一个非常常用的图像分类数据集。数据集包含60000张32*32像素的小图片,每张图片都有一个类别标注(总共有10类),分成了50000张的训练集和10000张的测试集。如下是一些图片示例:
![CIFAR-10例子](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90acfaf106.jpg "")
上图中左边是十个类别和对应的一些示例图片,右边是给定一张图片后,根据像素距离计算出来的,最近的10张图片。
#### 2.2 基于最近邻的简单图像类别判定
假如现在用CIFAR-10数据集做训练集,判断一张未知的图片属于CIFAR-10中的哪一类,应该怎么做呢。一个很直观的想法就是,既然我们现在有每个像素点的值,那我们就根据输入图片的这些值,计算和训练集中的图片距离,找最近的图片的类别,作为它的类别,不就行了吗。
恩,想法很直接哈,这就是『最近邻』的思想。偷偷说一句,这种直接的做法在图像识别中,其实效果并不是特别好。比如上图是按照这个思想找的最近邻,其实只有3个图片的最近邻是正确的类目。
即使这样,作为最基础的方法,我们还是来实现一下吧。我们需要一个图像距离评定准则,比如最简单的方式就是,比对两个图像像素向量之间的l1距离(也叫曼哈顿距离/cityblock距离),公式如下:
d1(I1,I2)=∑p∣∣Ip1−Ip2∣∣
其实就是计算了所有像素点之间的差值,然后做了加法,直观的理解如下图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90acfcdcca.png)
我们先把数据集读进内存:
~~~
#! /usr/bin/env python
#coding=utf-8
import os
import sys
import numpy as np
def load_CIFAR_batch(filename):
"""
cifar-10数据集是分batch存储的,这是载入单个batch
@参数 filename: cifar文件名
@r返回值: X, Y: cifar batch中的 data 和 labels
"""
with open(filename, 'r') as f:
datadict=pickle.load(f)
X=datadict['data']
Y=datadict['labels']
X=X.reshape(10000, 3, 32, 32).transpose(0,2,3,1).astype("float")
Y=np.array(Y)
return X, Y
def load_CIFAR10(ROOT):
"""
读取载入整个 CIFAR-10 数据集
@参数 ROOT: 根目录名
@return: X_train, Y_train: 训练集 data 和 labels
X_test, Y_test: 测试集 data 和 labels
"""
xs=[]
ys=[]
for b in range(1,6):
f=os.path.join(ROOT, "data_batch_%d" % (b, ))
X, Y=load_CIFAR_batch(f)
xs.append(X)
ys.append(Y)
X_train=np.concatenate(xs)
Y_train=np.concatenate(ys)
del X, Y
X_test, Y_test=load_CIFAR_batch(os.path.join(ROOT, "test_batch"))
return X_train, Y_train, X_test, Y_test
# 载入训练和测试数据集
X_train, Y_train, X_test, Y_test = load_CIFAR10('data/cifar10/')
# 把32*32*3的多维数组展平
Xtr_rows = X_train.reshape(X_train.shape[0], 32 * 32 * 3) # Xtr_rows : 50000 x 3072
Xte_rows = X_test.reshape(X_test.shape[0], 32 * 32 * 3) # Xte_rows : 10000 x 3072
~~~
下面我们实现最近邻的思路:
~~~
class NearestNeighbor:
def __init__(self):
pass
def train(self, X, y):
"""
这个地方的训练其实就是把所有的已有图片读取进来 -_-||
"""
# the nearest neighbor classifier simply remembers all the training data
self.Xtr = X
self.ytr = y
def predict(self, X):
"""
所谓的预测过程其实就是扫描所有训练集中的图片,计算距离,取最小的距离对应图片的类目
"""
num_test = X.shape[0]
# 要保证维度一致哦
Ypred = np.zeros(num_test, dtype = self.ytr.dtype)
# 把训练集扫一遍 -_-||
for i in xrange(num_test):
# 计算l1距离,并找到最近的图片
distances = np.sum(np.abs(self.Xtr - X[i,:]), axis = 1)
min_index = np.argmin(distances) # 取最近图片的下标
Ypred[i] = self.ytr[min_index] # 记录下label
return Ypred
nn = NearestNeighbor() # 初始化一个最近邻对象
nn.train(Xtr_rows, Y_train) # 训练...其实就是读取训练集
Yte_predict = nn.predict(Xte_rows) # 预测
# 比对标准答案,计算准确率
print 'accuracy: %f' % ( np.mean(Yte_predict == Y_test) )
~~~
最近邻的思想在CIFAR上得到的准确度为**38.6%**,我们知道10各类别,我们随机猜测的话准确率差不多是1/10=10%,所以说还是有识别效果的,但是显然这距离人的识别准确率(94%)实在是低太多了,不那么实用。
#### 2.3 关于最近邻的距离准则
我们这里用的距离准则是l1距离,实际上除掉l1距离,我们还有很多其他的距离准则。
- 比如说l2距离(也就是大家熟知的欧氏距离)的计算准则如下:
d2(I1,I2)=∑p(Ip1−Ip2)2‾‾‾‾‾‾‾‾‾‾‾‾‾‾√
- 比如余弦距离计算准则如下:
1−I1⋅I2||I1||⋅||I2||
更多的距离准则可以参见[scipy相关计算页面](http://docs.scipy.org/doc/scipy-0.16.0/reference/generated/scipy.spatial.distance.pdist.html).
### 3. K最近邻分类器(K Nearest Neighbor Classifier)
这是对最近邻的思想的一个调整。其实我们在使用最近邻分类器分类,扫描CIFAR训练集的时候,会发现,有时候不一定距离最近的和当前图片是同类,但是最近的一些里有很多和当前图片是同类。所以我们自然而然想到,把最近邻扩展为最近的N个临近点,然后统计一下这些点的类目分布,取最多的那个类目作为自己的类别。
恩,这就是KNN的思想。
KNN其实是一种特别常用的分类算法。但是有个问题,我们的K值应该取多少呢。换句话说,我们找多少邻居来投票,比较靠谱呢?
#### 3.1 交叉验证与参数选择
在现在的场景下,假如我们确定使用KNN来完成图片类别识别问题。我们发现有一些参数是肯定会影响最后的识别结果的,比如:
- 距离的选择(l1,l2,cos等等)
- 近邻个数K的取值。
每组参数下其实都能产生一个新的model,所以这可以视为一个模型选择/model selection问题。而对于模型选择问题,最常用的办法就是在[交叉验证](https://en.wikipedia.org/wiki/Cross-validation_%28statistics%29)集上实验。
数据总量就那么多,如果我们在test data上做模型参数选择,又用它做效果评估,显然不是那么合理(因为我们的模型参数很有可能是在test data上过拟合的,不能很公正地评估结果)。所以我们通常会把训练数据分为两个部分,一大部分作为训练用,另外一部分就是所谓的cross validation数据集,用来进行模型参数选择的。比如说我们有50000训练图片,我们可以把它分为49000的训练集和1000的交叉验证集。
~~~
# 假定已经有Xtr_rows, Ytr, Xte_rows, Yte了,其中Xtr_rows为50000*3072 矩阵
Xval_rows = Xtr_rows[:1000, :] # 构建1000的交叉验证集
Yval = Ytr[:1000]
Xtr_rows = Xtr_rows[1000:, :] # 保留49000的训练集
Ytr = Ytr[1000:]
# 设置一些k值,用于试验
validation_accuracies = []
for k in [1, 3, 5, 7, 10, 20, 50, 100]:
# 初始化对象
nn = NearestNeighbor()
nn.train(Xtr_rows, Ytr)
# 修改一下predict函数,接受 k 作为参数
Yval_predict = nn.predict(Xval_rows, k = k)
acc = np.mean(Yval_predict == Yval)
print 'accuracy: %f' % (acc,)
# 输出结果
validation_accuracies.append((k, acc))
~~~
这里提一个在很多地方会看到的概念,叫做k-fold cross-validation,意思其实就是把原始数据分成k份,轮流使用其中k-1份作为训练数据,而剩余的1份作为交叉验证数据(因此其实对于k-fold cross-validation我们会得到k个accuracy)。以下是5-fold cross-validation的一个示例:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad007854.png)
以下是我们使用5-fold cross-validation,取不同的k值时,得到的accuracy曲线(补充一下,因为是5-fold cross-validation,所以在每个k值上有5个取值,我们取其均值作为此时的准确度)
![5-fold 交叉验证](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad01d985.png "")
可以看出大概在k=7左右有最佳的准确度。
#### 3.2 最近邻方法的优缺点
K最近邻的优点大家都看出来了,思路非常简单清晰,而且完全不需要训练…不过也正因为如此,最后的predict过程非常耗时,因为要和全部训练集中的图片比对一遍。
实际应用中,我们其实更加关心实施predict所消耗的时间,如果有一个图像识别app返回结果要半小时一小时,你一定第一时间把它卸了。我们反倒不那么在乎训练时长,训练时间长一点没关系,只要最后应用的时候识别速度快效果好,就很赞。后面会提到的深度神经网络就是这样,深度神经网络解决图像问题时训练是一个很耗时间的过程,但是识别的过程非常快。
另外,不得不多说一句的是,优化计算K最近邻时间问题,实际上依旧到现在都是一个非常热门的问题。**Approximate Nearest Neighbor (ANN)**算法是牺牲掉一小部分的准确度,而提高很大程度的速度,能比较快地找到近似的K最近邻,现在已经有很多这样的库,比如说[FLANN](http://www.cs.ubc.ca/research/flann/).
最后,我们用一张图来说明一下,用图片像素级别的距离来实现图像类别识别,有其不足之处,我们用一个叫做[t-SNE](http://lvdmaaten.github.io/tsne/)的技术把CIFAR-10的所有图片按两个维度平铺出来,靠得越近的图片表示其像素级别的距离越接近。然而我们瞄一眼,发现,其实靠得最近的并不一定是同类别的。
![像素级别图像距离排列](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90ad02ee2b.jpg "")
其实观察一下,你就会发现,像素级别接近的图片,在整张图的颜色分布上,有很大的共性,然而在图像内容上,有时候也只能无奈地呵呵嗒,毕竟颜色分布相同的不同物体也是非常多的。
(1)_基础介绍
最后更新于:2022-04-01 14:21:35
作者: [寒小阳](http://blog.csdn.net/han_xiaoyang?viewmode=contents) &&[龙心尘](http://blog.csdn.net/longxinchen_ml?viewmode=contents)
时间:2015年11月。
出处:[http://blog.csdn.net/han_xiaoyang/article/details/49876119](http://blog.csdn.net/han_xiaoyang/article/details/49876119)
声明:版权所有,转载请注明出处,谢谢。
### 1.背景
[计算机视觉](http://library.kiwix.org/wikipedia_zh_all/A/html/%E8%AE%A1/%E7%AE%97/%E6%9C%BA/%E8%A7%86/%E8%AE%A1%E7%AE%97%E6%9C%BA%E8%A7%86%E8%A7%89.html)/[computer vision](https://en.wikipedia.org/wiki/Computer_vision)是一个火了N年的topic。持续化升温的原因也非常简单:在搜索/影像内容理解/医学应用/地图识别等等领域应用太多,大家都有一个愿景『让计算机能够像人一样去”看”一张图片,甚至”读懂”一张图片』。
有几个比较重要的计算机视觉任务,比如图片的分类,物体识别,物体定位于检测等等。而近年来的[神经网络/深度学习](https://en.wikipedia.org/wiki/Deep_learning)使得上述任务的准确度有了非常大的提升。加之最近做了几个不大不小的计算机视觉上的项目,爱凑热闹的博主自然不打算放过此领域,也边学边做点笔记总结,写点东西,写的不正确的地方,欢迎大家提出和指正。
### 2.基础知识
为了简单易读易懂,这个系列中绝大多数的代码都使用python完成。这里稍微介绍一下python和Numpy/Scipy(**python中的科学计算包**)的一些基础。
#### 2.1 python基础
python是一种长得像伪代码,具备高可读性的编程语言。
优点挺多:可读性相当好,写起来也简单,所想立马可以转为实现代码,且社区即为活跃,可用的package相当多;缺点:效率一般。
#### 2.1.1 基本数据类型
最常用的有数值型(Numbers),布尔型(Booleans)和字符串(String)三种。
- 数值型(Numbers)
可进行简单的运算,如下:
~~~
x = 5
print type(x) # Prints "<type 'int'>"
print x # Prints "5"
print x + 1 # 加; prints "6"
print x - 1 # 减; prints "4"
print x * 2 # 乘; prints "10"
print x ** 2 # 幂; prints "25"
x += 1 #自加
print x # Prints "6"
x *= 2 #自乘
print x # Prints "12"
y = 2.5
print type(y) # Prints "<type 'float'>"
print y, y + 1, y * 2, y ** 2 # Prints "2.5 3.5 5.0 6.25"
~~~
PS:python中没有x++ 和 x– 操作
- 布尔型(Booleans)
包含True False和常见的与或非操作
~~~
t = True
f = False
print type(t) # Prints "<type 'bool'>"
print t and f # 逻辑与; prints "False"
print t or f # 逻辑或; prints "True"
print not t # 逻辑非; prints "False"
print t != f # XOR; prints "True"
~~~
- 字符串型(String)
字符串可以用单引号/双引号/三引号声明
~~~
hello = 'hello'
world = "world"
print hello # Prints "hello"
print len(hello) # 字符串长度; prints "5"
hw = hello + ' ' + world # 字符串连接
print hw # prints "hello world"
hw2015 = '%s %s %d' % (hello, world, 2015) # 格式化字符串
print hw2015 # prints "hello world 2015"
~~~
字符串对象有很有有用的函数:
~~~
s = "hello"
print s.capitalize() # 首字母大写; prints "Hello"
print s.upper() # 全大写; prints "HELLO"
print s.rjust(7) # 以7为长度右对齐,左边补空格; prints " hello"
print s.center(7) # 居中补空格; prints " hello "
print s.replace('l', '(ell)') # 字串替换;prints "he(ell)(ell)o"
print ' world '.strip() # 去首位空格; prints "world"
~~~
#### 2.1.2 基本容器
**列表/List**
和数组类似的一个东东,不过可以包含不同类型的元素,同时大小也是可以调整的。
~~~
xs = [3, 1, 2] # 创建
print xs, xs[2] # Prints "[3, 1, 2] 2"
print xs[-1] # 第-1个元素,即最后一个
xs[2] = 'foo' # 下标从0开始,这是第3个元素
print xs # 可以有不同类型,Prints "[3, 1, 'foo']"
xs.append('bar') # 尾部添加一个元素
print xs # Prints
x = xs.pop() # 去掉尾部的元素
print x, xs # Prints "bar [3, 1, 'foo']"
~~~
列表最常用的操作有:
**切片/slicing**
即取子序列/一部分元素,如下:
~~~
nums = range(5) # 从1到5的序列
print nums # Prints "[0, 1, 2, 3, 4]"
print nums[2:4] # 下标从2到4-1的元素 prints "[2, 3]"
print nums[2:] # 下标从2到结尾的元素
print nums[:2] # 从开头到下标为2-1的元素 [0, 1]
print nums[:] # 恩,就是全取出来了
print nums[:-1] # 从开始到第-1个元素(最后的元素)
nums[2:4] = [8, 9] # 对子序列赋值
print nums # Prints "[0, 1, 8, 8, 4]"
~~~
**循环/loops**
即遍历整个list,做一些操作,如下:
~~~
animals = ['cat', 'dog', 'monkey']
for animal in animals:
print animal
# 依次输出 "cat", "dog", "monkey",每个一行.
~~~
可以用enumerate取出元素的同时带出下标
~~~
animals = ['cat', 'dog', 'monkey']
for idx, animal in enumerate(animals):
print '#%d: %s' % (idx + 1, animal)
# 输出 "#1: cat", "#2: dog", "#3: monkey",一个一行。
~~~
**List comprehension**
这个相当相当相当有用,在很长的list生成过程中,效率完胜for循环:
~~~
# for 循环
nums = [0, 1, 2, 3, 4]
squares = []
for x in nums:
squares.append(x ** 2)
print squares # Prints [0, 1, 4, 9, 16]
# list comprehension
nums = [0, 1, 2, 3, 4]
squares = [x ** 2 for x in nums]
print squares # Prints [0, 1, 4, 9, 16]
~~~
你猜怎么着,list comprehension也是可以加多重条件的:
~~~
nums = [0, 1, 2, 3, 4]
even_squares = [x ** 2 for x in nums if x % 2 == 0]
print even_squares # Prints "[0, 4, 16]"
~~~
**字典/Dict**
和Java中的Map一样的东东,用于存储key-value对:
~~~
d = {'cat': 'cute', 'dog': 'furry'} # 创建
print d['cat'] # 根据key取出value
print 'cat' in d # 判断是否有'cat'这个key
d['fish'] = 'wet' # 添加元素
print d['fish'] # Prints "wet"
# print d['monkey'] # KeyError: 'monkey'非本字典的key
print d.get('monkey', 'N/A') # 有key返回value,无key返回"N/A"
print d.get('fish', 'N/A') # prints "wet"
del d['fish'] # 删除某个key以及对应的value
print d.get('fish', 'N/A') # prints "N/A"
~~~
对应list的那些操作,你在dict里面也能找得到:
**循环/loops**
~~~
# for循环
d = {'person': 2, 'cat': 4, 'spider': 8}
for animal in d:
legs = d[animal]
print 'A %s has %d legs' % (animal, legs)
# Prints "A person has 2 legs", "A spider has 8 legs", "A cat has 4 legs"
# 通过iteritems
d = {'person': 2, 'cat': 4, 'spider': 8}
for animal, legs in d.iteritems():
print 'A %s has %d legs' % (animal, legs)
# Prints "A person has 2 legs", "A spider has 8 legs", "A cat has 4 legs"
~~~
~~~
# Dictionary comprehension
nums = [0, 1, 2, 3, 4]
even_num_to_square = {x: x ** 2 for x in nums if x % 2 == 0}
print even_num_to_square # Prints "{0: 0, 2: 4, 4: 16}"
~~~
**元组/turple**
本质上说,还是一个list,只不过里面的每个元素都是一个两元组对。
~~~
d = {(x, x + 1): x for x in range(10)} # 创建
t = (5, 6) # Create a tuple
print type(t) # Prints "<type 'tuple'>"
print d[t] # Prints "5"
print d[(1, 2)] # Prints "1"
~~~
#### 2.1.3 函数
用def可以定义一个函数:
~~~
def sign(x):
if x > 0:
return 'positive'
elif x < 0:
return 'negative'
else:
return 'zero'
for x in [-1, 0, 1]:
print sign(x)
# Prints "negative", "zero", "positive"
~~~
~~~
def hello(name, loud=False):
if loud:
print 'HELLO, %s' % name.upper()
else:
print 'Hello, %s!' % name
hello('Bob') # Prints "Hello, Bob"
hello('Fred', loud=True) # Prints "HELLO, FRED!"
~~~
**类/Class**
python里面的类定义非常的直接和简洁:
~~~
class Greeter:
# Constructor
def __init__(self, name):
self.name = name # Create an instance variable
# Instance method
def greet(self, loud=False):
if loud:
print 'HELLO, %s!' % self.name.upper()
else:
print 'Hello, %s' % self.name
g = Greeter('Fred') # Construct an instance of the Greeter class
g.greet() # Call an instance method; prints "Hello, Fred"
g.greet(loud=True) # Call an instance method; prints "HELLO, FRED!"
~~~
#### 2.2.NumPy基础
NumPy是Python的科学计算的一个核心库。它提供了一个高性能的多维数组(矩阵)对象,可以完成在其之上的很多操作。很多机器学习中的计算问题,把数据vectorize之后可以进行非常高效的运算。
#### 2.2.1 数组
一个NumPy数组是一些类型相同的元素组成的类矩阵数据。用list或者层叠的list可以初始化:
~~~
import numpy as np
a = np.array([1, 2, 3]) # 一维Numpy数组
print type(a) # Prints "<type 'numpy.ndarray'>"
print a.shape # Prints "(3,)"
print a[0], a[1], a[2] # Prints "1 2 3"
a[0] = 5 # 重赋值
print a # Prints "[5, 2, 3]"
b = np.array([[1,2,3],[4,5,6]]) # 二维Numpy数组
print b.shape # Prints "(2, 3)"
print b[0, 0], b[0, 1], b[1, 0] # Prints "1 2 4"
~~~
生成一些特殊的Numpy数组(矩阵)时,我们有特定的函数可以调用:
~~~
import numpy as np
a = np.zeros((2,2)) # 全0的2*2 Numpy数组
print a # Prints "[[ 0. 0.]
# [ 0. 0.]]"
b = np.ones((1,2)) # 全1 Numpy数组
print b # Prints "[[ 1. 1.]]"
c = np.full((2,2), 7) # 固定值Numpy数组
print c # Prints "[[ 7. 7.]
# [ 7. 7.]]"
d = np.eye(2) # 2*2 对角Numpy数组
print d # Prints "[[ 1. 0.]
# [ 0. 1.]]"
e = np.random.random((2,2)) # 2*2 的随机Numpy数组
print e # 随机输出
~~~
#### 2.2.2 Numpy数组索引与取值
可以通过像list一样的分片/slicing操作取出需要的数值部分。
~~~
import numpy as np
# 创建如下的3*4 Numpy数组
# [[ 1 2 3 4]
# [ 5 6 7 8]
# [ 9 10 11 12]]
a = np.array([[1,2,3,4], [5,6,7,8], [9,10,11,12]])
# 通过slicing取出前两行的2到3列:
# [[2 3]
# [6 7]]
b = a[:2, 1:3]
# 需要注意的是取出的b中的数据实际上和a的这部分数据是同一份数据.
print a[0, 1] # Prints "2"
b[0, 0] = 77 # b[0, 0] 和 a[0, 1] 是同一份数据
print a[0, 1] # a也被修改了,Prints "77"
~~~
~~~
import numpy as np
a = np.array([[1,2,3,4], [5,6,7,8], [9,10,11,12]])
row_r1 = a[1, :] # a 的第二行
row_r2 = a[1:2, :] # 同上
print row_r1, row_r1.shape # Prints "[5 6 7 8] (4,)"
print row_r2, row_r2.shape # Prints "[[5 6 7 8]] (1, 4)"
col_r1 = a[:, 1]
col_r2 = a[:, 1:2]
print col_r1, col_r1.shape # Prints "[ 2 6 10] (3,)"
print col_r2, col_r2.shape # Prints "[[ 2]
# [ 6]
# [10]] (3, 1)"
~~~
还可以这么着取:
~~~
import numpy as np
a = np.array([[1,2], [3, 4], [5, 6]])
# 取出(0,0) (1,1) (2,0)三个位置的值
print a[[0, 1, 2], [0, 1, 0]] # Prints "[1 4 5]"
# 和上面一样
print np.array([a[0, 0], a[1, 1], a[2, 0]]) # Prints "[1 4 5]"
# 取出(0,1) (0,1) 两个位置的值
print a[[0, 0], [1, 1]] # Prints "[2 2]"
# 同上
print np.array([a[0, 1], a[0, 1]]) # Prints "[2 2]"
~~~
我们还可以通过条件得到bool型的Numpy数组结果,再通过这个数组取出符合条件的值,如下:
~~~
import numpy as np
a = np.array([[1,2], [3, 4], [5, 6]])
bool_idx = (a > 2) # 判定a大于2的结果矩阵
print bool_idx # Prints "[[False False]
# [ True True]
# [ True True]]"
# 再通过bool_idx取出我们要的值
print a[bool_idx] # Prints "[3 4 5 6]"
# 放在一起我们可以这么写
print a[a > 2] # Prints "[3 4 5 6]"
~~~
#### Numpy数组的类型
~~~
import numpy as np
x = np.array([1, 2])
print x.dtype # Prints "int64"
x = np.array([1.0, 2.0])
print x.dtype # Prints "float64"
x = np.array([1, 2], dtype=np.int64) # 强制使用某个type
print x.dtype # Prints "int64"
~~~
#### 2.2.3 Numpy数组的运算
矩阵的加减开方和(元素对元素)乘除如下:
~~~
import numpy as np
x = np.array([[1,2],[3,4]], dtype=np.float64)
y = np.array([[5,6],[7,8]], dtype=np.float64)
# [[ 6.0 8.0]
# [10.0 12.0]]
print x + y
print np.add(x, y)
# [[-4.0 -4.0]
# [-4.0 -4.0]]
print x - y
print np.subtract(x, y)
# 元素对元素,点对点的乘积
# [[ 5.0 12.0]
# [21.0 32.0]]
print x * y
print np.multiply(x, y)
# 元素对元素,点对点的除法
# [[ 0.2 0.33333333]
# [ 0.42857143 0.5 ]]
print x / y
print np.divide(x, y)
# 开方
# [[ 1. 1.41421356]
# [ 1.73205081 2. ]]
print np.sqrt(x)
~~~
矩阵的内积是通过下列方法计算的:
~~~
import numpy as np
x = np.array([[1,2],[3,4]])
y = np.array([[5,6],[7,8]])
v = np.array([9,10])
w = np.array([11, 12])
# 向量内积,得到 219
print v.dot(w)
print np.dot(v, w)
# 矩阵乘法,得到 [29 67]
print x.dot(v)
print np.dot(x, v)
# 矩阵乘法
# [[19 22]
# [43 50]]
print x.dot(y)
print np.dot(x, y)
~~~
特别特别有用的一个操作是,sum/求和(对某个维度):
~~~
import numpy as np
x = np.array([[1,2],[3,4]])
print np.sum(x) # 整个矩阵的和,得到 "10"
print np.sum(x, axis=0) # 每一列的和 得到 "[4 6]"
print np.sum(x, axis=1) # 每一行的和 得到 "[3 7]"
~~~
还有一个经常会用到操作是矩阵的转置,在Numpy数组里用.T实现:
~~~
import numpy as np
x = np.array([[1,2], [3,4]])
print x # Prints "[[1 2]
# [3 4]]"
print x.T # Prints "[[1 3]
# [2 4]]"
# 1*n的Numpy数组,用.T之后其实啥也没做:
v = np.array([1,2,3])
print v # Prints "[1 2 3]"
print v.T # Prints "[1 2 3]"
~~~
#### 2.2.4 Broadcasting
Numpy还有一个非常牛逼的机制,你想想,如果你现在有一大一小俩矩阵,你想使用小矩阵在大矩阵上做多次操作。额,举个例子好了,假如你想将一个1 * n的矩阵,加到m * n的矩阵的每一行上:
~~~
#你如果要用for循环实现是酱紫的(下面用y的原因是,你不想改变原来的x)
import numpy as np
x = np.array([[1,2,3], [4,5,6], [7,8,9], [10, 11, 12]])
v = np.array([1, 0, 1])
y = np.empty_like(x) # 设置一个和x一样维度的Numpy数组y
# 逐行相加
for i in range(4):
y[i, :] = x[i, :] + v
# 恩,y就是你想要的了
# [[ 2 2 4]
# [ 5 5 7]
# [ 8 8 10]
# [11 11 13]]
print y
~~~
~~~
#上一种方法如果for的次数非常多,会很慢,于是我们改进了一下
import numpy as np
x = np.array([[1,2,3], [4,5,6], [7,8,9], [10, 11, 12]])
v = np.array([1, 0, 1])
vv = np.tile(v, (4, 1)) # 变形,重复然后叠起来
print vv # Prints "[[1 0 1]
# [1 0 1]
# [1 0 1]
# [1 0 1]]"
y = x + vv # 相加
print y # Prints "[[ 2 2 4
# [ 5 5 7]
# [ 8 8 10]
# [11 11 13]]"
~~~
~~~
#其实因为Numpy的Broadcasting,你可以直接酱紫操作
import numpy as np
x = np.array([[1,2,3], [4,5,6], [7,8,9], [10, 11, 12]])
v = np.array([1, 0, 1])
y = x + v # 直接加!!!
print y # Prints "[[ 2 2 4]
# [ 5 5 7]
# [ 8 8 10]
# [11 11 13]]"
~~~
更多Broadcasting的例子请看下面:
~~~
import numpy as np
v = np.array([1,2,3]) # v has shape (3,)
w = np.array([4,5]) # w has shape (2,)
# 首先把v变成一个列向量
# v现在的形状是(3, 1);
# 作用在w上得到的结果形状是(3, 2),如下
# [[ 4 5]
# [ 8 10]
# [12 15]]
print np.reshape(v, (3, 1)) * w
# 逐行相加
x = np.array([[1,2,3], [4,5,6]])
# 得到如下结果:
# [[2 4 6]
# [5 7 9]]
print x + v
# 先逐行相加再转置,得到以下结果:
# [[ 5 6 7]
# [ 9 10 11]]
print (x.T + w).T
# 恩,也可以这么做
print x + np.reshape(w, (2, 1))
~~~
### 2.3 SciPy
Numpy提供了一个非常方便操作和计算的高维向量对象,并提供基本的操作方法,而Scipy是在Numpy的基础上,提供很多很多的函数和方法去直接完成你需要的矩阵操作。有兴趣可以浏览[Scipy方法索引](http://docs.scipy.org/doc/scipy/reference/index.html)查看具体的方法,函数略多,要都记下来有点困难,随用随查吧。
#### 向量距离计算
需要特别拎出来说一下的是,向量之间的距离计算,这个Scipy提供了很好的接口[scipy.spatial.distance.pdist](http://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.distance.pdist.html#scipy.spatial.distance.pdist):
~~~
import numpy as np
from scipy.spatial.distance import pdist, squareform
# [[0 1]
# [1 0]
# [2 0]]
x = np.array([[0, 1], [1, 0], [2, 0]])
print x
# 计算矩阵每一行和每一行之间的欧氏距离
# d[i, j] 是 x[i, :] 和 x[j, :] 之间的距离,
# 结果如下:
# [[ 0. 1.41421356 2.23606798]
# [ 1.41421356 0. 1. ]
# [ 2.23606798 1. 0. ]]
d = squareform(pdist(x, 'euclidean'))
print d
~~~
### 2.4 Matplotlib
这是python中的一个作图工具包。如果你熟悉matlab的语法的话,应该会用得挺顺手。可以通过[matplotlib.pyplot.plot](http://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.plot)了解更多绘图相关的设置和参数。
~~~
import numpy as np
import matplotlib.pyplot as plt
# 计算x和对应的sin值作为y
x = np.arange(0, 3 * np.pi, 0.1)
y = np.sin(x)
# 用matplotlib绘出点的变化曲线
plt.plot(x, y)
plt.show() # 只有调用plt.show()之后才能显示
~~~
结果如下:
![sin图像](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90acedf8dc.png "")
~~~
# 在一个图中画出2条曲线
import numpy as np
import matplotlib.pyplot as plt
# 计算x对应的sin和cos值
x = np.arange(0, 3 * np.pi, 0.1)
y_sin = np.sin(x)
y_cos = np.cos(x)
# 用matplotlib作图
plt.plot(x, y_sin)
plt.plot(x, y_cos)
plt.xlabel('x axis label')
plt.ylabel('y axis label')
plt.title('Sine and Cosine')
plt.legend(['Sine', 'Cosine'])
plt.show()
~~~
![sin和cos](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90acef2b5e.png "")
~~~
# 用subplot分到子图里
import numpy as np
import matplotlib.pyplot as plt
# 得到x对应的sin和cos值
x = np.arange(0, 3 * np.pi, 0.1)
y_sin = np.sin(x)
y_cos = np.cos(x)
# 2*1个子图,第一个位置.
plt.subplot(2, 1, 1)
# 画第一个子图
plt.plot(x, y_sin)
plt.title('Sine')
# 画第2个子图
plt.subplot(2, 1, 2)
plt.plot(x, y_cos)
plt.title('Cosine')
plt.show()
~~~
![subplot](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90acf11c87.png "")
#### 2.5 简单图片读写
可以使用`imshow`来显示图片。
~~~
import numpy as np
from scipy.misc import imread, imresize
import matplotlib.pyplot as plt
img = imread('/Users/HanXiaoyang/Comuter_vision/computer_vision.jpg')
img_tinted = img * [1, 0.95, 0.9]
# 显示原始图片
plt.subplot(1, 2, 1)
plt.imshow(img)
# 显示调色后的图片
plt.subplot(1, 2, 2)
plt.imshow(np.uint8(img_tinted))
plt.show()
~~~
![computer_vision](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-16_56e90acf25547.png "")
前言
最后更新于:2022-04-01 14:21:33
> 原文出处:[深度学习与计算机视觉](http://blog.csdn.net/column/details/dl-computer-vision.html)
作者:[han_xiaoyang](http://blog.csdn.net/han_xiaoyang)
**本系列文章经作者授权在看云整理发布,未经作者允许,请勿转载!**
# 深度学习与计算机视觉
> 本专栏专注于深度学习在计算机视觉领域的技术,针对图像识别等问题,从传统的SVM与逻辑回归分类器,到卷积神经网络/深度学习的技术细节。欢迎大家关注和提意见。