查看原文
其他

【综述专栏】自编码器的最佳特征:最大化互信息

在科学研究中,从方法论上来讲,都应“先见森林,再见树木”。当前,人工智能学术研究方兴未艾,技术迅猛发展,可谓万木争荣,日新月异。对于AI从业者来说,在广袤的知识森林中,系统梳理脉络,才能更好地把握趋势。为此,我们精选国内外优秀的综述文章,开辟“综述专栏”,敬请关注。

来源:知乎—科技猛兽
地址:https://zhuanlan.zhihu.com/p/245121413
特征提取是无监督学习中很重要且很基本的一项任务,常见形式是训练一个编码器将原始数据集编码为一个固定长度的向量。自然地,我们对这个编码器的基本要求是:保留原始数据的(尽可能多的)重要信息。
我们怎么知道编码向量保留了重要信息呢?
一个很自然的想法是这个编码向量应该也要能还原出原始图片出来,所以我们还训练一个解码器,试图重构原图片,最后的loss就是原始图片和重构图片的mse。这便有了标准的自编码器的设计。后来,我们还希望编码向量的分布尽量能接近高斯分布,这就有了变分自编码器。
上面这种想法我们默认了:如果能够重构出原图,那么提取的特征就是好的特征。可事实真的是这样吗?“重构”这个要求是否合理?
首先,使用mse loss这个损失函数过于苛刻,得到的重构结果通常也很模糊,而且对于图像重构事实上我们并没有非常适合的loss可以选用。最理想的方法是用对抗网络训练一个判别器出来,但是这会进一步增加任务难度。
其次,一个很有趣的事实是:我们大多数人能分辨出很多真假币,但如果要我们画一张百元大钞出来,我相信基本上画得一点都不像。这表明,对于真假币识别这个任务,可以设想我们有了一堆真假币供学习,我们能从中提取很丰富的特征,但是这些特征并不足以重构原图,它只能让我们分辨出这堆纸币的差异。
也就是说,对于数据集和任务来说,合理的、充分的特征并不一定能完成图像重构。

01

重构得到的特征不是最好的
上面的讨论表明,重构得到的特征不一定是最好的。我们试想,把你放在人群中,如何从人群中精准地把你识别出来?那这个对应的特征才是好的特征。所以,好特征的基本原则应当是“能够从整个数据集中辨别出该样本出来”,也就是说,提取出该样本最独特的信息。如何衡量提取出来的信息是该样本独特的呢?我们用“互信息”来衡量。
定义一些符号:
   :原图片的集合,    为某一个原始图像。
   :编码向量的集合,    为某一个编码向量。
   表示    所产生的编码向量的分布,它就是我们想要寻找的编码器。那么可以用互信息来表示    的相关性:

里    是原始数据的分布,一个好的特征编码器    ,应该使得互信息尽可能地大。

互信息越大意味着(大部分的)    应当尽量大,这意味着    应当远大于    ,即对于每个    ,编码器能找出专属于    的那个    ,使得    的概率远大于随机的概率    。这样一来,我们就有能力只通过    就从中分辨出原始样本    来。
前面提到,相对于自编码器,变分自编码器VAE同时还希望隐变量服从标准正态分布的先验分布,这有利于使得编码空间更加规整,甚至有利于解耦特征,便于后续学习。因此,在这里我们同样希望加上这个约束。
设    为标准正态分布,我们去最小化    与先验分布    的KL散度:

加权混合之后可以得到最小化的总目标:

意一下这里面各个变量的含义:    是我们要求的编码器,    是原始数据的分布,    是标准正态分布,    是编码向量的先验分布,未知,没法继续往下算了。

02

简化先验
对含有位置分布的表达式的解决办法是继续进行化简,所以将上式变为:

方法是把右边的一重积分人为变成二重积分,加上的是对    的积分。
上式有2项,第1项可以视为互信息,第2项可以视为    与    的KL散度:

第2项刚好是VAE中的散度一项,而且在已经假设了    的情况下,这一项是可以计算出来的,计算方法在VAE的文章中有提到为:
对于    维变量    来讲:

式中:    为    的每一维变量的均值和方差。
也就是说,我们经过了一系列的变换,巧妙地避开了   这个未知的编码向量的先验分布。

03

互信息
现在我们只剩下最大化    这一项了,我们把定义稍作变换:

这个形式揭示了互信息的本质含义:    描述了两个变量    的联合分布,    则是随机抽取一个    和一个    时的分布(假设它们两个不相关时),而互信息则是这两个分布的KL散度。而所谓最大化互信息,就是要拉大   与    之间的距离。
我们已经把互信息等效为了KL散度,注意KL散度理论上是没有最大值的,最大化它很可能得到一个无穷大的结果。所以,为了更有效地优化,我们抓住“最大化互信息就是拉大   与    之间的距离”这个特点,我们不用   散度,而换一个有上界的度量:   散度。

JS散度同样衡量了两个分布的距离,但是它有上界    ,我们最大化它的时候,同样能起到类似最大化互信息的效果,但是又不用担心无穷大问题。
所以我们把目标函数改写为:


04

  散度与互信息

在上篇文章中:
https://zhuanlan.zhihu.com/p/245566551
我们表明了  散度竟然也可以通过神经网络    去表示:

对于    散度,给出的结果是:

其中  用  激活,这就是最初的GAN网络。
那就可以写成:

但这次的概率分布    是二维概率分布:
所以    为    ,    为    ,代入之后得:

其实上式的含义非常简单,它就是“负采样估计”:引入一个判别网络    ,    及其对应的    视为一个正样本对,    及随机抽取的    则视为负样本,然后最大化似然函数,等价于最小化交叉熵。
可视化的流程图如下:

最大化互信息:估计KL转化为:估计JS再转化为:训练网络的流程图

样一来,通过负采样的方式,我们就给出了估计    散度的一种方案,从而也就给出了估计    版互信息的一种方案,从而成功攻克了互信息。现在,对应上,具体的loss为:

仔细观察不难发现上式可以进一步化简为:

具体的操作方法是:
首先,我们随机选一张图片    ,通过编码器就可以得到    的均值和方差,然后重参数就可以得到    ,这样的一个    对构成一个正样本。
为了减少计算量,我们直接在batch内对图片进行随机打乱,然后按照随机打乱的顺序作为选择负样本的依据,也就是说,如果    是原来batch内的第4张图片,将图片打乱后第4张图片是    ,那么    就是正样本,    就是负样本。
一步,若把局部互信息也考虑进来,则设中间特征为    ,则这个中间特征可以视为是    个向量的集合,即:   
我们想计算这   个向量与    之间的互信息,称之为“局部互信息”。
估算方法跟全局是一样的,将每一个    与    拼接起来得到    ,相当于得到了一个更大的feature map,负样本的选取方法也是用在batch内随机打算的方案。
现在,加入局部互信息的总loss为:

求期望的过程在代码中使用K.means来实现。

05

代码解读

代码流程

导入必要的库和定义超参数:
import numpy as npimport globimport imageiofrom keras.models import Modelfrom keras.layers import *from keras import backend as Kfrom keras.optimizers import Adamfrom keras.datasets import cifar10import tensorflow as tf

(x_train, y_train), (x_test, y_test) = cifar10.load_data()x_train = x_train.astype('float32') / 255 - 0.5x_test = x_test.astype('float32') / 255 - 0.5y_train = y_train.reshape(-1)y_test = y_test.reshape(-1)img_dim = x_train.shape[1]

z_dim = 256 # 隐变量维度alpha = 0.5 # 全局互信息的loss比重beta = 1.5 # 局部互信息的loss比重gamma = 0.01 # 先验分布的loss比重
编码器和reparameterization trick:
x_in = Input(shape=(img_dim, img_dim, 3))x = x_in
for i in range(3): x = Conv2D(z_dim // 2**(2-i), kernel_size=(3,3), padding='SAME')(x) x = BatchNormalization()(x) x = LeakyReLU(0.2)(x) x = MaxPooling2D((2, 2))(x)
feature_map = x # 截断到这里,认为到这里是feature_map(局部特征)feature_map_encoder = Model(x_in, x)

for i in range(2): x = Conv2D(z_dim, kernel_size=(3,3), padding='SAME')(x) x = BatchNormalization()(x) x = LeakyReLU(0.2)(x)
x = GlobalMaxPooling2D()(x) # 全局特征
z_mean = Dense(z_dim)(x) # 均值,也就是最终输出的编码z_log_var = Dense(z_dim)(x) # 方差,这里都是模仿VAE的

encoder = Model(x_in, z_mean) # 总的编码器就是输出z_mean

# 重参数技巧def sampling(args): z_mean, z_log_var = args u = K.random_normal(shape=K.shape(z_mean)) return z_mean + K.exp(z_log_var / 2) * u

# 重参数层,相当于给输入加入噪声z_samples = Lambda(sampling)([z_mean, z_log_var])prior_kl_loss = - 0.5 * K.mean(1 + z_log_var - K.square(z_mean) - K.exp(z_log_var))
打乱数据以采样负样本:
def shuffling(x): idxs = K.arange(0, K.shape(x)[0]) idxs = tf.random_shuffle(idxs)    return K.gather(x, idxs)
全局判别器和局部判别器:
# 与随机采样的特征拼接(全局)z_shuffle = Lambda(shuffling)(z_samples)z_z_1 = Concatenate()([z_samples, z_samples])z_z_2 = Concatenate()([z_samples, z_shuffle])
# 与随机采样的特征拼接(局部)feature_map_shuffle = Lambda(shuffling)(feature_map)z_samples_repeat = RepeatVector(4 * 4)(z_samples)z_samples_map = Reshape((4, 4, z_dim))(z_samples_repeat)z_f_1 = Concatenate()([z_samples_map, feature_map])z_f_2 = Concatenate()([z_samples_map, feature_map_shuffle])

# 全局判别器z_in = Input(shape=(z_dim*2,))z = z_inz = Dense(z_dim, activation='relu')(z)z = Dense(z_dim, activation='relu')(z)z = Dense(z_dim, activation='relu')(z)z = Dense(1, activation='sigmoid')(z)
GlobalDiscriminator = Model(z_in, z)
z_z_1_scores = GlobalDiscriminator(z_z_1)z_z_2_scores = GlobalDiscriminator(z_z_2)global_info_loss = - K.mean(K.log(z_z_1_scores + 1e-6) + K.log(1 - z_z_2_scores + 1e-6))

# 局部判别器z_in = Input(shape=(None, None, z_dim*2))z = z_inz = Dense(z_dim, activation='relu')(z)z = Dense(z_dim, activation='relu')(z)z = Dense(z_dim, activation='relu')(z)z = Dense(1, activation='sigmoid')(z)
LocalDiscriminator = Model(z_in, z)
z_f_1_scores = LocalDiscriminator(z_f_1)z_f_2_scores = LocalDiscriminator(z_f_2)local_info_loss = - K.mean(K.log(z_f_1_scores + 1e-6) + K.log(1 - z_f_2_scores + 1e-6))
模型训练并输出编码器的特征:
model_train = Model(x_in, [z_z_1_scores, z_z_2_scores, z_f_1_scores, z_f_2_scores])model_train.add_loss(alpha * global_info_loss + beta * local_info_loss + gamma * prior_kl_loss)model_train.compile(optimizer=Adam(1e-3))
model_train.fit(x_train, epochs=50, batch_size=64)model_train.save_weights('total_model.cifar10.weights')

# 输出编码器的特征zs = encoder.predict(x_train, verbose=True)zs.mean() # 查看均值(简单观察先验分布有没有达到效果)zs.std() # 查看方差(简单观察先验分布有没有达到效果)
随机选一张图片,输出最相近的图片。可以选用欧氏距离或者cos值:
def sample_knn(path): n = 10 topn = 10 figure1 = np.zeros((img_dim*n, img_dim*topn, 3)) figure2 = np.zeros((img_dim*n, img_dim*topn, 3)) zs_ = zs / (zs**2).sum(1, keepdims=True)**0.5 for i in range(n): one = np.random.choice(len(x_train)) idxs = ((zs**2).sum(1) + (zs[one]**2).sum() - 2 * np.dot(zs, zs[one])).argsort()[:topn] for j,k in enumerate(idxs): digit = x_train[k] figure1[i*img_dim: (i+1)*img_dim, j*img_dim: (j+1)*img_dim] = digit idxs = np.dot(zs_, zs_[one]).argsort()[-n:][::-1] for j,k in enumerate(idxs): digit = x_train[k] figure2[i*img_dim: (i+1)*img_dim, j*img_dim: (j+1)*img_dim] = digit figure1 = (figure1 + 1) / 2 * 255 figure1 = np.clip(figure1, 0, 255) figure2 = (figure2 + 1) / 2 * 255 figure2 = np.clip(figure2, 0, 255) imageio.imwrite(path+'_l2.png', figure1) imageio.imwrite(path+'_cos.png', figure2)

sample_knn('test')

参考:

https://kexue.fm/archives/6024

本文目的在于学术交流,并不代表本公众号赞同其观点或对其内容真实性负责,版权归原作者所有,如有侵权请告知删除。


“综述专栏”历史文章


更多综述专栏文章,

请点击文章底部“阅读原文”查看



分享、点赞、在看,给个三连击呗!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存