自编码器介绍:
深度学习可以解决一些人工难以提取有效特征的问题。在深度学习的早期,它一直被认为是一种无监督特征学习的方法。对于深度学习用于特征学习有两个关键点:
1)无监督学习,即不需要标注的数据就可以对数据进行一定程度的学习,提取频繁出现的特征;
2)逐层抽象,即从简单的微观的特征开始,不断抽象特征的层级,逐渐往复杂的宏观特征转变。例如以汽车图片为例,深度学习的特征学习过程为:将一张图片的原始像素慢慢抽象,从像素组成点、线,再将点线组成小零件,再将小零件组成车轮、车窗、车身等高阶特征,这便是深度学习在训练过程中所做的特征学习。
特征是可以进行不断抽象转化为高一级的特征,那我们如何找到一些基本的结构,然后如何抽象呢?主要可以分为两种情况:
1)在有很多标注的数据时,可以使用训练一个深层的神经网络来获得;
2)在没有标注数据时,我们可以使用无监督的自编码器来提取特征。
自编码器,即可以使用自身的高阶特征编码自己。自编码器也是一种神经网络,它的输入和输出是一致的,它借助稀疏编码器的思想,目标是使用稀疏的一些高阶特征重新组合来重构自己。
它的特点:
1)期望输入和输出一致,自编码器的输入节点和输出节点的数量是一致的;
2)希望使用高阶特征来重构自己,而不是复制像素点。
自编码器通常希望使用少量稀疏的高阶特征来重构输入,因此我们可以加入几种限制:
1)如果限制训练中间隐含层的节点数量,比如使中间隐含层节点的数量小于输入/输出节点的数量,就相当于一个降维的过程。只能够学习到数据中最重要的特征复原,将可能不相关的内容去除。如果再给中间隐含层的权重加入L1正则,则可以根据惩罚系数控制隐含节点的稀疏程度,系数越大,学到的特征组合越稀疏,实际使用的特征数量越少。
2)如果给数据加入噪声,那么就是去噪自编码器。我们将从噪声中学习出数据的特征,这时我们只有学习数据中频繁出现的模式和结构,将无规律的噪声略去,才可以复原数据。去噪自编码器中最长使用的噪声是加性高斯噪声。
如果自编码器的隐含层只有一层,那么其原理类似于主成分分析(PCA)。
自编码器的结构图如下图所示:
在训练很深的神经网络时,直接训练会变得非常困难,Hinton提出了一种可行的方案:
1)可以使用无监督的逐层训练提取特征,将网络权重的初始化到一个比较好的位置,辅助后面的监督训练。
2)利用监督方法训练整个网络。
其中无监督的逐层训练,其思想和自编码器非常相似。
同时,Hinton提出的DBN模型,它包括多个隐含层,每个隐含层都是限制玻尔兹曼机。DBN在训练时,需要对每两层间进行无监督的预训练,这个过程其实就相当与一个多层的自编码器,可以将整个网络的权重初始化到一个理想的分布,最后通过反向传播算法调整模型的权重,这个过程会使用经过标注信息来做监督性的分类训练。上述两个过程,能够很好的解决由于神经网络过深导致的梯度弥散的问题。
下面我们以去噪自编码器为例,简单介绍一下如何使用TensorFlow实现自编码器。
import numpy as np
import sklearn.preprocessing as prep
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
def xavier_init(fan_in, fan_out, constant=1):
# 权重初始化,使得权重满足(low, high)的均匀分布
# fan_in: 输入节点的数量; fan_out: 输出节点的数量
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):
def __init__(self, n_input, n_hidden, transfer_function = tf.nn.softplus,
optimizer = tf.train.AdamOptimizer(), scale = 0.1):
# 神经网络的构建函数;
# n_input:输入变量数; n_hidden:隐含层节点数; transfer_function: 隐含层激活函数
# optimizer: 训练优化器; scale: 高斯噪声系数
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_weight = self._initialize_weights()
self.weights = network_weight
# 网络结构
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']) # 不需要使用激活函数
# 自编码器的损失函数
self.cost = 0.5 * tf.reduce_sum(tf.pow(tf.subtract(self.x, self.reconstruction), 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})
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):
return self.sess.run(self.weights['b1'])
mnist = input_data.read_data_sets('MNIST_data', one_hot = True)
def standard_scale(X_train, X_test):
# 对训练和测试数据进行标准化,需要注意的是必须保证训练集和测试集都使用完全相同的Scale
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)
train_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(train_epochs):
avg_cost = 0
total_batch = int(n_samples / batch_size) #总共能够获取的块数
for i in range(total_batch):
batch_xs = get_random_block_from_data(X_train, batch_size) #获得的每一块的数据
cost = autoencoder.partial_fit(batch_xs)
avg_cost += cost / n_samples * batch_size #计算获得平均损失
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)))
1. import sklearn.preprocessing as prep
这是一个对数据进行预处理的常用模块,在后面的程序中我们会使用它来进行数据的标准化。
2. def xavier_init(fan_in, fan_out, constant=1):
该函数定义了一种参数初始化的方法,它会根据某一层网络的输入、输出节点数量自动调整最合适的分布。对于深度学习而言,如果权重初始化得太小,那信号将在每层间传递时逐渐缩小而难以产生作用,但如果权重初始化得太大,那信号将在每层间传递时逐渐放大并导致发散和失效。上述函数初始化就是实现权重初始化得大小正好合适。
3.self.hidden=self.transfer(tf.add(tf.matmul(self.x+scale*tf.random_normal((n_input,)), self.weights[‘w1’]), self.weights[‘b1’]))
加入高斯噪声的信号,从而实现去噪自编码器,可以将噪声去除,实现标准的自编码器。
4. self.cost=0.5*tf.reduce_sum(tf.pow(tf.subtract(self.x, self.reconstruction), 2.0))
使用平方误差作为损失函数,计算重构的输出与输入之间的差异。
5. def _initialize_weights(self):
初始化权重的函数,其中w1使用前面定义的xavier_init函数初始化,即可返回一个比较适合于softplus等激活函数的权重初始分布,而偏置b1将其全部置为0,对于输出层,因为没有使用激活函数,这里将w2,b2全部初始化为0即可。
6. def partial_fit(self, X):
定义计算损失以及执行一步训练的函数。
7. def standard_scale(X_train, X_test):
对训练数据和测试数据进行标准化处理的函数。标准化也就是让数据变成0均值且标准差为1的分布。方法是先减去均值,再除以标准差(使用sklearn.preprossingd的StandardScaler这个类实现)。值得注意的是,必须保证训练数据和测试数据使用完全相同的Scaler,这样才能保证后面模型处理数据时的一致性,这也就是为什么先在训练数据上fit出一个共用的Scaler的原因。
8. def get_random_block_from_data(data, batch_size):
定义一个随机获取block数据的函数,值得注意的是,这属于不放回抽样,可以提高数据的利用效率。
9.autoencoder=AdditiveGaussianNoiseAutoencoder(n_input=784,n_hidden=200,transfer_function=tf.nn.softplus,optimizer=tf.train.AdamOptimizer(learning_rate = 0.001),scale=0.01)
创建一个autoencoder的实例,输入节点数量为784,引层节点数为200,激活函数为softplus,优化器optimizer为Adam并且学习率为0.001,噪声系数为0.01。
以上就是实现自编码器的过程,从上面的过程中我们可以看出,实现自编码器和实现一个单引层的神经网络差不多,只不过是在数据输入时做了标准化,并且加上了高斯噪声,同时我们的输出结果不是数字分类结果,而是复原的数据,因此不需要用标注过的数据(损失函数的构建是通过重建数据与实际输入的均方误差得到的)。因此自编码器作为一种无监督学习的方法,它不是对数据进行聚类,而是提取其中最有用、最频繁出现的高阶特征,根据这些高阶特征重构数据。
现在无监督预训练的使用场景比以前少了许多,训练全连接的MLP或者CNN、RNN时,我们都不需要先使用无监督训练提取特征。但是无监督学习乃至自编码器依然是非常有用的。由于现实生活中,有标注的信息还是少数,我们可以提取无监督数据的高阶特征,并使用在其他地方。