自编码器是什么?
自编码器本身就是一种BP神经网络。它是一种无监督学习算法。
我们都知道神经网络可以从任意精度逼近任意函数,这里我们让神经网络目标值等于输出值x,也就是模拟一个恒等函数:
太无聊了,是吗?输入等于输出,这网络有什么意义?但是,当我们把自编码神经网络加入某些限制,事情就发生了变化。如图1所示,这就是一个基本的自编码神经网络,可以看到隐含层节点数量要少于输入层节点数量。
图1
举个例子,如果我们输入一张10*10的图像,这样就有100个像素,所以输入层和输出层的节点数量就是100。而我们取隐藏层节点数量为25。注意,这样就会迫使隐藏层节点学习得到输入数据的压缩表示方法,逼得隐藏层要用25维数据重构出100维的数据。这样也就完成了学习过程。
这和我们学习的过程很像,假设一共有100个考点,但是只允许你用25个知识点概括所有这些考点,这就是学习的过程。
稀疏自编码器又是什么?
更一般的,如果隐藏层节点数量很大,甚至比输入层节点数量还要多时,我们仍然可以使用自编码算法,但是这时需要加入稀疏性限制。这就是稀疏自编码器。
什么是稀疏性限制?
简单说就是要保证隐藏神经元在大多数情况下是被抑制的状态。具体表现就是sigmoid函数的输出大多数状态是0,tanh函数的输出大多数状态是-1。这样有什么好处?这样能够迫使隐藏神经元发挥最大的潜力,在很不利的条件下学习到真正的特征。
怎么衡量某个隐藏神经元的激活度?
取平均就好了,假设表示在给定输入x的情况下,隐藏神经元j的激活度,那么自然就有平均激活度
稀疏性惩罚项——相对熵
为了保证这个稀疏度小到我们希望的那么多,比如说,即
我们需要在优化目标函数中加入一个额外的惩罚因子,这个罚因子基于相对熵(KLdivergence):
因此这个罚因子会有如下性质,当时(这里取稀疏性参数为0.2),等于0,而两者差异越来越大时,相对熵会快速趋近于无穷大,如图2所示:
图2
稀疏自编码器训练方式与BP神经网络相对比
稀疏自编码神经网络的代价函数是BP神经网络的代价函数加上一个稀疏性惩罚项:
相应地,残差迭代公式也要进行修正
实现一个稀疏自编码器
数据概览
采用的是Andrew著名的UFLDL给出的样例,其中数据文件叫做IMAGES,这是一个512*512*10的三维数组,里面存了10张图片,每张都是262144像素。执行
<span style="font-size:14px;">loadIMAGES;
imagesc(IMAGES(:,:,6))
colormapgray;</span>
可以看看第6张图的样子,如图3所示,是一张关于森林和雪山的图像。
图3
数据采样
由于数据的图片挺大,我们总不能一上来搞一个每层都2万多节点的神经网络训练吧。我们先采样,这里从10张图片里面随机采样10000个小patch,每个小patch是一个8*8像素小碎片,我们定义训练集是64*10000的矩阵。每一列就是把刚刚的小patch拉成列向量的结果。
这里代码如下
<span style="font-size:14px;">loadIMAGES; % load images from disk
patchsize= 8; % we'll use 8x8 patches
%numpatches = 10;
numpatches= 10000;
%Initialize patches with zeros. Your codewill fill in this matrix--one
%column per patch, 10000 columns.
patches= zeros(patchsize*patchsize, numpatches);
tic
image_size=size(IMAGES);
i=randi(image_size(1)-patchsize+1,1,numpatches);
j=randi(image_size(2)-patchsize+1,1,numpatches);
k=randi(image_size(3),1,numpatches);
fornum=1:numpatches
patches(:,num)=reshape(IMAGES(i(num):i(num)+patchsize-1,j(num):j(num)+patchsize-1,k(num)),1,[]);
end
toc</span>
这里randi函数,randi(iMax,m,n)在闭区间[1,iMax]生成m*n型随机矩阵。而reshape函数的作用是把每个小patch拉成一个列向量。
前200个patch的样子如图4所示:
图4
sparseAutoencoderCost.m
这部分代码是最核心也是最重要的,正如前面所说,现在优化目标函数有了3个部分:均方差项、权重衰减项、稀疏惩罚项。
这里程序需要一部分一部分地调试,否则非常容易得到看似正确但实际效果没有正确代码好的错误结果。这里附上我最新修改版本的代码,去掉了所有显式for循环,使用矢量化编程,简洁了代码,增强运行效率,但也减弱了可读性。
<span style="font-size:14px;">numpatches=size(patches,2);
a2=sigmoid(W1*patches+repmat(b1,1,numpatches));
a3=sigmoid(W2*a2+repmat(b2,1,numpatches));
Rho=sum(a2,2)/numpatches;
Penalty=-sparsityParam./Rho+(1-sparsityParam)./(1-Rho);
Delta3=(a3-patches).*a3.*(1-a3);
Delta2=(W2'*Delta3+beta*repmat(Penalty,1,numpatches)).*a2.*(1-a2);
cost1=sumsqr(a3-patches)/numpatches/2;
cost2=(sumsqr(W1)+sumsqr(W2))*lambda/2;
cost3=beta*sum(sparsityParam*log(sparsityParam./Rho)+(1-sparsityParam)*log((1-sparsityParam)./(1-Rho)));
cost=cost1+cost2+cost3;
W2grad=Delta3*a2'/numpatches+lambda*W2;
b2grad=sum(Delta3,2)/numpatches;
W1grad=Delta2*patches'/numpatches+lambda*W1;
b1grad=sum(Delta2,2)/numpatches;</span>
这里repmat的重复使用是用来复制矩阵的,一定要不遗余力去掉这部分代码中所有的显式for循环,否则执行起来时间会很长。
梯度校验
梯度校验是代码调试的大杀器,这一部分我用到了for循环,效率确实非常低了,所以调试的时候要把校验参数做些调整,令隐藏节点数量为2,采样数量为100,这样就能大大加快校验速度。否则要等相当久的时间。
<span style="font-size:14px;">EPSILON=0.0001;
thetaspslion=zeros(size(theta));
fori=1:size(theta)
thetaspslion(i)=EPSILON;
numgrad(i)=(J(theta+thetaspslion)-J(theta-thetaspslion))/2/EPSILON;
thetaspslion(i)=0;
end</span>
注意:最后运行时一定要关掉梯度校验,不然就要卡死机了。。。
这个梯度校验对于目标函数的3部分的调试一定要循序渐进,心急吃不了热豆腐。均方差项调试正确了以后,会得到如图5所示的图像
图5
而当,均方差项和权重衰减项都调试通过后,得到的图片是这样的
图6
而当所有的目标函数都调试通过以后,接下来就是见证奇迹的时刻!
图7
这就是图像的基,通过稀疏自编码器,我们学习得到了25个8*8的基,这些图像的基就相当于咱们每个视神经细胞所看到的东西,当这些细胞组成阵列,垒成层层叠叠,我们就能看到所有的东西了。
稀疏自编码器作为无监督学习的一层基本模块,这就是我们DeepLearning万里长城的第一步:搞基!