TensorFlow实战之实现自编码器过程
关于本文说明,已同步本人另外一个博客地址位于http://blog.csdn.net/qq_37608890,详见http://blog.csdn.net/qq_37608890/article/details/79352212。
本文根据最近学习TensorFlow书籍网络文章的情况,特将一些学习心得做了总结,详情如下.如有不当之处,请各位大拿多多指点,在此谢过。
一、相关概念
1、稀疏性(Sparsity)及稀疏编码(Sparse Coding)
Sparsity 是当今机器学习领域中的一个重要话题。
Sparsity 的最重要的“客户”大概要属 high dimensional data
了吧。现在的机器学习问题中,具有非常高维度的数据随处可见。例如,在文档或图片分类中常用的 bag of words
模型里,如果词典的大小是一百万,那么每个文档将由一百万维的向量来表示。高维度带来的的一个问题就是计算量:在一百万维的空间中,即使计算向量的内积这样的基本操作也会是非常费力的。不过,如果向量是稀疏的的话(事实上在
bag of words 模型中文档向量通常都是非常稀疏的),例如两个向量分别只有L1 和 L2
个非零元素,那么计算内积可以只使用min(L1,L2) 次乘法完成。因此稀疏性对于解决高维度数据的计算量问题是非常有效的。
稀疏编码(Sparse Coding)算法是一种无监督学习方法,它用来寻找一组“超完备”基向量来更高效地表示样本数据。稀疏编码算法的目的就是找到一组基向量 ,使得我们能将输入向量 表示为这些基向量的线性组合:
虽然形如主成分分析技术(PCA)能使我们方便地找到一组“完备”基向量,但是这里我们想要做的是找到一组“超完备”基向量来表示输入向量(也就是说,k > n)。超完备基的好处是它们能更有效地找出隐含在输入数据内部的结构与模式。然而,对于超完备基来说,系数 ai 不再由输入向量
唯一确定。因此,在稀疏编码算法中,我们另加了一个评判标准“稀疏性”来解决因超完备而导致的退化(degeneracy)问题。要求系数 ai
是稀疏的意思就是说:对于一组输入向量,我们只想有尽可能少的几个系数远大于零。选择使用具有稀疏性的分量来表示我们的输入数据是有原因的,因为绝大多数的感官数据,比如自然图像,可以被表示成少量基本元素的叠加,在图像中这些基本元素可以是面或者线。
在早期,学者们研究稀疏编码(Sparse
Coding)时,搜集了大量黑白风景照片,且从中提取了16像素*16像素的图片碎片。结果发现:几乎所有的图像碎片都可以用64种正交的边组合得到,且组合出一张图像碎片需要的边的数量是很少的,即稀疏的。学者们同时也发现,声音其实也存在这种情况,他们从大量的未标注的音频中发现了20种基本结构,绝大多数声音可以由这些基本结构线性组合得到。显然,这就是特征的稀疏表达,使用少量的基本特征组合拼装得到更高层抽象的特征。一般情况想,我们也需要多层的神经网络,对每一层神经网络而言,前一层的输出都是未加工的像素,而这一层则是对像素进行加工组织成更高阶的特征。
2、自编码(AutoEncoder)
顾名思义,即可以使用自身的高阶特征编码自己。自编码器其实也是一种神经网络,它的输入和输出是一致的,它借助稀疏编码的思想,目标是使用稀疏的一些高阶特征重新组合来重构自己,即 :对所有的自编码器来讲,目标都是样本重构。
在机器学习中,自编码器的使用十分广泛。自编码器首先通过编码层,将高维空间的向量,压缩成低维的向量(潜在变量),然后通过解码层将低维向量解压重构出原始样本。
3、隐含层
指输入层和输出层以外,中间的那些层。输入层和输出层是可见的,且层的结构是相对固定的,而隐含层结构不固定,相当于不可见。只要隐含的节点足够多,即是只有一个隐含层的神经网络也可以拟合任意函数。隐含层层数越多,越容易拟合复杂的函数。拟合复杂函数需要的隐含节点数目随着层数的增多而呈指数下降。即层数越深,概念越抽象,这就是深度学习。
4、过拟合
指模型预测准确率在训练集上升高,但在测试集上反而下降。这是模型的泛化性不好,只记住了当前数据的特征。
5、Dropout
Dropout:防止过拟合的一种方法。将神经网络某一层的输出节点数据随机丢弃一部分。可以理解为是对特征的采样。
6、优化函数
优化调试网络中的参数。一般情况下,在调参时,学习率的设置会导致最后结果差异很大。神经网络通常不是凸优化,充满局部最优,但是神经网络可能有很多个局部最优都能达到良好效果,反而全局最优容易出现过拟合。
对于SGD,通常一开始学习率大一些,可以快速收敛,但是训练的后期,希望学习率可以小一些,可以比较稳定地落到一个局部最优解。
除SGD之外,还有自适应调节学习率的Adagrad、Adam、Adadelta等优化函数
7、激活函数
Sigmoid函数的输出在(0,1),最符合概率输出的定义,但局限性较大。
ReLU,当x<=0时,y=0;当x>0时,y=x。这非常类似于人类的阈值响应机制。ReLU的3个显著特点:单侧抑制;相对宽阔的兴奋边界;稀疏激活性。它是目前最符合实际神经元的模型。
二、自解码器算法原理
1、BacsicAuto-Encoder
Auto-Encoder(AE)是20世纪80年代晚期提出的,简单讲,AE可以被视为一个三层神经网络结构:一个输入层、一个隐藏层和一个输出层,从数据规模上讲,输入层与输出层具有相同的规模。
图1 Auto-Encoder网络结构示意图
其中,n表示输入层(同时也是作为输出层)的规模;m表示隐藏层的规模;,分别表示输出层、隐藏层和输出层上的向量,也是各层上对应的数据个数,这里隐藏层h的数据低于输入层和输出层的数据,即x>h<y且x=y。根据输入层x到隐藏层h的映射矩阵求出h,再根据隐藏层h到输出层的映射矩阵求出y。分别表示隐藏层和输出层上的偏置向量。表示输入层与隐藏层之间的权值矩阵,即x到h的映射矩阵,是n乘m阶矩阵;表示隐藏层与输出层之间的权值矩阵,即h到y的映射矩阵,是m乘n阶矩阵,也是W的逆矩阵。
针对AE而言,重要的是解决好矩阵W问题,一个好的矩阵W可以保证x完全等于y,可实际上很难,尤其当输入层x的数据量大成千上百个时,x和y的差别就可以说明矩阵W的优良程度。总之,我们的工作就是要求出矩阵W,并使得x和y之间的差异尽可能小。下面给出下图,进一步做出解释
图2 Auto-Encoder抽象结构
如图2所示,AE由编码器(Enconder)和解码器(Decoder)两部分构成。显然,从输入层到隐藏层属于编码过程,从隐藏层到输出层属于解码过程。我们设定f和g分别表示编码和解码函数,结合上面的内容,我们可以给出f和g的方程,如下(1.1)和(1.2)式所示
其中 为编码器的激活函数,通常情况下取Sigmoid函数,即 ;为解码器的激活函数,一般取Sigmoid函数或恒等函数,即。权值矩阵一般取,一些文献资料中提及称,这种情况下的AE具有tiedweights。本文指讨论二者相等的情况。
所以,截止目前,我们可知,AE的参数为 。
假设目前我们有一个训练集S=,那么我们要考虑的问题是如何利用S取训练,首先是建立一个训练目标。
结合前面的梳理,我们可知,AE可以被看成一个普通的三层神经网络,y也可以看成由x做的一个预测(prediction),且保证x和y尽可能接近,这种接近程度可以通过重构误差(reconstruction error)函数L(x,y)来进行表达。
根据解码器激活函数的不同,这里L(x,y)一般有两种取值方法:
第一种,当为恒等函数时,取平方误差(sequared error)函数。
第二种,当 为sigmoid函数(此种情况输入)时,取交叉熵(cross-entropy)函数
取得重构函数后,我们就可以对数据集S进行针对性的训练,去得到一个整体的损失函数(loss function)(1.5)
将上面这个函数(1.5)进行极小化处理,我们就可以得到所需的参数。
注意,一般文献中损失函数多数使用的是平均重构误差(average reconstruction error),即下面函数(1.6)
对于一个给定的训练集S而言,系数1/N并不对于上面这个损失函数最小化产生多大的影响,所以,为了简便,这里忽略这个系数。
最开始,AE只是作为一种降维技巧来使用的,视隐藏层h为x的一种降维表示,也要求m<n(h<x)。但是,目前来看,AE在m>=n的情况(有些文献中称为
over-complete
setting)下应用更多,以便获取更高维数更有意义的表达方式。当然,在次情况下,也会有一个问题不可忽略:若直接对损失函数(1.5)进行极小化的而没有加入任何其他限制条件的话,AE在这里学到的很可能是一个恒等函数(where
the auto-encoder could perfectly reconstruction the input without
needing to extract any useful feature),这种结果是不符合预期的。需要解决,例如 ,
在损失函数中加入稀疏性限制(Sparse Auto-Encoder),或在网络中引入随机性(如RBN,Denoising
Auto-Encoder)等。
下面先介绍 Regularized Auto-Encoer 和Sparse Auto-Encoder,其余的AE变种算法,后续有机会继续梳理。
2、 Regularized Auto-Encoder
我们在损失函数(1.5)中加入正则项,便可以得到所谓的Regularized Auto-Encoder。 常见的正则化有L1 正则和 L2正则。以L2正则为例进行介绍,损失 函数则变为如下(2.7):
其中 为权值矩阵W的元素。
公式(2.7)中的即为L2正则项,也叫做权重衰减(weight-decay)项(下标中的 就是weight-decay的意思),lambda为权重衰减参数,用来控制公式中两项的相对重要性。
3、Sparse Auto-Encoder
由公式(2.7)可以看出,权重衰减项实际上是对权重提出一些要求,也就是要求它们不能过大,否则会被惩罚。这里所讲的Sparse Auto-Encoder 是对隐藏层上神经元的激活度提出要求,使其满足一定的稀疏性。
关于隐藏层上神经元激活度如何刻画的问题,下面展开来看:
假设表示输入为x时,隐藏层上第j个神经元的激活度( 是(1.1)中向量h的第j个分量),则(3.8)
表示隐藏层上第j个神经元在训练集 上的平均激活度。为了保证隐藏层上的神经元大部分时间被抑制为零或者接近于零(即稀疏性限制),这里要求(3.9)
其中,为稀疏性参数,一般情况下取一个很小的数(如 )。对于那些与有显著不同的,会进行惩罚。这个限制方法的实现有多种,这里以一种基于KL divergence的方法,即引入(3.10)
其中,(3.11)
函数 具有如下性质:
随着和 之间的差异增大而单调递增,尤其当 时,存在达到最小值。所以,如果将(3.10)加入到损失函数里,则最小化损失函数即可达到使得尽可能靠近 的效果。这样来看,Sparse Auto-Encoder的损失函数就可以表达为(3.12)
这里beta为控制稀疏性惩罚项的权重系数。
我们也可以将L2正则和稀疏性限制结合起来使用,这时的损失函数也就变为(3.13)
三、实现去噪自编码器过程
1、代码实现过程如下
# 这里以最具代表性的去噪自编码器为例。 #导入MNIST数据集 import numpy as np import sklearn.preprocessing as prep import tensorflow as tf from tensorflow.examples.tutorials.mnist import input_data #这里使用一种参数初始化方法xavier initialization,需要对此做好定义工作。 #Xaiver初始化器的作用就是让权重大小正好合适。 #这里实现的是标准均匀分布的Xaiver初始化器。 def xavier_init(fan_in, fan_out, constant = 1): """ 目的是合理初始化权重。 参数: fan_in --行数; fan_out -- 列数; constant --常数权重,条件初始化范围的倍数。 return 初始化后的权重tensor. """ low = -constant * np.sqrt(6.0 / (fan_in + fan_out)) high = constant * np.sqrt(6.0 / (fan_in + fan_out)) return tf.random_uniform((fan_in, fan_out), minval = low, maxval = high, dtype = tf.float32) #定义一个去噪的自编码类 class AdditiveGaussianNoiseAutoencoder(object): """ __init__() :构建函数; n_input : 输入变量数; n_hidden : 隐含层节点数; transfer_function: 隐含层激活函数,默认是softplus; optimizer : 优化器,默认是Adam; scale : 高斯噪声系数,默认是0.1; """ def __init__(self, n_input, n_hidden, transfer_function =tf.nn.softplus, optimizer = tf.train.AdamOptimizer(), scale = 0.1): self.n_input = n_input self.n_hidden = n_hidden self.transfer = transfer_function self.scale = tf.placeholder(tf.float32) self.training_scale = scale network_weights = self._initialize_weights() self.weights = network_weights # 定义网络结构,为输入x创建一个维度为n_input的placeholder,然后 #建立一个能提取特征的隐含层。 self.x = tf.placeholder(tf.float32, [None, self.n_input]) self.hidden = self.transfer(tf.add(tf.matmul(self.x +scale * tf.random_normal((n_input,)), self.weights[\'w1\']), self.weights[\'b1\'])) self.reconstruction = tf.add(tf.matmul(self.hidden,self.weights[\'w2\']), self.weights[\'b2\']) #首先,定义自编码器的损失函数,在此直接使用平方误差(SquaredError)作为cost。 #然后,定义训练操作作为优化器self.optimizer对损失self.cost进行优化。 #最后,创建Session,并初始化自编码器全部模型参数。 self.cost = 0.5 *tf.reduce_sum(tf.pow(tf.subtract(self.reconstruction, self.x), 2.0)) self.optimizer = optimizer.minimize(self.cost) init = tf.global_variables_initializer() self.sess = tf.Session() self.sess.run(init) def _initialize_weights(self): all_weights = dict() all_weights[\'w1\'] = tf.Variable(xavier_init(self.n_input,self.n_hidden)) all_weights[\'b1\'] = tf.Variable(tf.zeros([self.n_hidden],dtype = tf.float32)) all_weights[\'w2\'] = tf.Variable(tf.zeros([self.n_hidden,self.n_input], dtype = tf.float32)) all_weights[\'b2\'] = tf.Variable(tf.zeros([self.n_input],dtype = tf.float32)) return all_weights def partial_fit(self, X): cost, opt = self.sess.run((self.cost, self.optimizer),feed_dict = {self.x: X, self.scale: self.training_scale}) return cost def calc_total_cost(self, X): return self.sess.run(self.cost, feed_dict = {self.x: X, self.scale:self.training_scale}) #定义一个transform函数,以便返回自编码器隐含层的输出结果,目的是提供一个接口来获取抽象后的特征。 def transform(self, X): return self.sess.run(self.hidden, feed_dict = {self.x: X, self.scale:self.training_scale}) def generate(self, hidden = None): if hidden is None: hidden = np.random.normal(size = self.weights["b1"]) return self.sess.run(self.reconstruction, feed_dict ={self.hidden: hidden}) def reconstruct(self, X): return self.sess.run(self.reconstruction, feed_dict ={self.x: X, self.scale: self.training_scale}) def getWeights(self): #获取隐含层的权重w1. return self.sess.run(self.weights[\'w1\']) def getBiases(self): #获取隐含层的偏执系数b1. return self.sess.run(self.weights[\'b1\']) #利用TensorFlow提供的读取示例数据的函数载入MNIST数据集。 mnist = input_data.read_data_sets(\'MNIST_data\', one_hot = True) #定义一个对训练、测试数据进行标准化处理的函数。 def standard_scale(X_train, X_test): preprocessor = prep.StandardScaler().fit(X_train) X_train = preprocessor.transform(X_train) X_test = preprocessor.transform(X_test) return X_train, X_test def get_random_block_from_data(data, batch_size): start_index = np.random.randint(0, len(data) - batch_size) return data[start_index:(start_index + batch_size)] X_train, X_test = standard_scale(mnist.train.images,mnist.test.images) n_samples = int(mnist.train.num_examples) training_epochs = 20 batch_size = 128 display_step = 1 autoencoder = AdditiveGaussianNoiseAutoencoder(n_input = 784, n_hidden = 200, transfer_function =tf.nn.softplus, optimizer =tf.train.AdamOptimizer(learning_rate = 0.001), scale = 0.01) for epoch in range(training_epochs): avg_cost = 0. total_batch = int(n_samples / batch_size) # Loop over all batches for i in range(total_batch): batch_xs = get_random_block_from_data(X_train, batch_size) # Fit training using batch data cost = autoencoder.partial_fit(batch_xs) # Compute average loss avg_cost += cost / n_samples * batch_size # Display logs per epoch step if epoch % display_step == 0: print("Epoch:", \'%04d\' % (epoch + 1), "cost=","{:.9f}".format(avg_cost)) #最后对训练完的模型进行性能测试。 print("Total cost: " +str(autoencoder.calc_total_cost(X_test)))
2、执行结果如下
Extracting MNIST_data/train-images-idx3-ubyte.gz Extracting MNIST_data/train-labels-idx1-ubyte.gz Extracting MNIST_data/t10k-images-idx3-ubyte.gz Extracting MNIST_data/t10k-labels-idx1-ubyte.gz Epoch: 0001 cost= 18871.253996591 Epoch: 0002 cost= 12308.673515909 Epoch: 0003 cost= 10227.495348864 Epoch: 0004 cost= 11243.596613636 Epoch: 0005 cost= 10782.029647727 Epoch: 0006 cost= 9165.328120455 Epoch: 0007 cost= 8487.490198295 Epoch: 0008 cost= 9195.667004545 Epoch: 0009 cost= 9026.087407955 Epoch: 0010 cost= 8301.502948295 Epoch: 0011 cost= 9921.268600568 Epoch: 0012 cost= 8789.966229545 Epoch: 0013 cost= 9115.636243182 Epoch: 0014 cost= 8993.681156818 Epoch: 0015 cost= 7670.030270455 Epoch: 0016 cost= 8108.834190341 Epoch: 0017 cost= 7897.135417045 Epoch: 0018 cost= 8332.914957955 Epoch: 0019 cost= 8091.132888068 Epoch: 0020 cost= 7822.976949432 Total cost: 725054.5
四、小结
至此,我们可以发现,从实现的角度而言,自编码器其实和一个单隐含层的神经网络差不多,只是自编码器在数据输入时做了标准化处理,且加上了一个高斯噪声,同时我们的输出结果不是数字分类结果,而是复原的数据,因此不需要用标注过的数据进行监督训练。自编码器作为一种无监督学习方法,它与其他无监督学习的区别主要在于:它不是对数据进行聚类,而是把数据中最有用、最频繁的高阶特征提取出来,然后根据这些高阶特征进行重构数据。在深度学习发展早期非常流行的DBN,也是依靠这种思想,先对数据进行无监督学习,提取到一些有用的特征,将神经网络权重初始化到一个较好的分布,然后再使用有标注的数据进行监督训练,即对权重进行fine-tune。
现实生活中,大部分数据是没有标准信息的,但人脑比较擅长处理这些数据,会提取出其中的高阶抽象特征,并使用在别的地方。自编码器作为深度学习在无监督领域的应用的确非常成功,同时无监督学习也将成为深度学习一个重要发展方向。
参考资料 主要参考资料《TensorFlow实战》(黄文坚 唐源 著)(电子工业出版社)。