使用互信息进行无监督学习

使用互信息进行无监督学习

    • [0. 前言](#0. 前言)
    • [1. 基于离散随机变量互信息最大化的无监督学习](#1. 基于离散随机变量互信息最大化的无监督学习)
    • [2. 无监督聚类的编码器网络](#2. 无监督聚类的编码器网络)
    • [3. 使用 Keras 实现无监督聚类](#3. 使用 Keras 实现无监督聚类)
    • [4. 验证实验](#4. 验证实验)

0. 前言

深度学习中的一个经典问题是监督分类,监督分类需要带标注的输入图像。我们已经学习了如何在 MNIST 和 CIFAR10 数据集上执行分类任务:对于 MNIST 数据集,三层 CNN 配合全连接层可实现高达 99.3% 的准确率;而对于 CIFAR10 数据集,使用 ResNetDenseNet 可获得约 94% 的准确率。这两个数据集均属于已标注数据集。

与监督学习不同,本节的目标是实现无监督学习。我们关注的是无标注条件下的分类问题。核心思路是:如果能够学会对所有训练数据的潜编码向量进行聚类,那么通过线性分割算法即可对每个测试输入数据的潜向量进行分类。

1. 基于离散随机变量互信息最大化的无监督学习

为实现无标注条件下潜在编码向量的聚类学习,我们的训练目标是最大化输入图像 X X X 与其潜编码 Y Y Y 之间的互信息。 X X X 和 Y Y Y 均为随机变量。其原理在于:相似图像对应的潜向量将聚集在相同区域,而相距较远的区域可以通过线性分配问题轻松分离。这样,分类问题就能以无监督方式完成。数学上,目标函数为最大化:
I ( X ; Y ) = H ( X ) − H ( X ∣ Y ) I(X;Y)= H(X)−H(X|Y) I(X;Y)=H(X)−H(X∣Y)

直观而言,当观测到 Y Y Y 时,我们能更确定 X X X 的信息。但上式的问题在于,我们缺乏估计密度 P ( X ∣ Y ) P(X|Y) P(X∣Y) 来度量 H ( X ∣ Y ) H(X|Y) H(X∣Y) 的有效方法。
Ji 等人提出的不变信息聚类 (Invariant Information Clustering, IIC) 建议直接通过联合分布与边缘分布来度量 I ( X ; Y ) I(X;Y) I(X;Y)。该方法度量指向同一输入的两个潜编码随机变量间的互信息。假设输入 X X X 被编码为 Z Z Z:
Z = E ( X ) Z =\mathcal E (X) Z=E(X)

对同一输入 X X X 进行变换得到 X ˉ = G ( 𝑋 ) \bar X = \mathcal G(𝑋) Xˉ=G(X),且变换后 X ˉ \bar X Xˉ 仍能明确保持与 X X X 相同的类别归属。在图像处理中, G \mathcal G G 可以是常规操作如小幅旋转、随机裁剪和错切变换。有时,对比度与亮度调整、边缘检测、添加微量噪声及归一化等操作也可接受,只要处理后图像语义保持不变。例如,若 X X X 是狗的图像,经 G \mathcal G G 变换后的 X ˉ \bar X Xˉ 仍明显是狗。使用相同编码器网络得到的潜编码向量为:
Z ˉ = E ( X ˉ ) \bar Z =\mathcal E (\bar X) Zˉ=E(Xˉ)

因此,我们可以将以上改写为关于两个随机变量 Z Z Z 和 Z ˉ \bar Z Zˉ 的表达式:
I ( Z ; Z ˉ ) = ∑ Z ∈ Z ∑ Z ˉ ∈ Z ˉ P ( Z , Z ˉ ) l o g P ( Z , Z ˉ ) P ( Z ) P ( Z ˉ ) I(Z;\bar Z)= \sum_{Z\in Z}\sum_{\bar Z\in \bar Z}P(Z,\bar Z)log \frac {P(Z,\bar Z)}{P(Z)P(\bar Z)} I(Z;Zˉ)=Z∈Z∑Zˉ∈Zˉ∑P(Z,Zˉ)logP(Z)P(Zˉ)P(Z,Zˉ)

其中 P ( Z ) P(Z) P(Z) 和 P ( Z ˉ ) P(\bar Z) P(Zˉ) 可分别解释为 Z Z Z 和 Z ˉ \bar Z Zˉ 的边缘分布。对于离散随机变量 Z Z Z 和 Z ˉ \bar Z Zˉ, P ( Z ) P(Z) P(Z) 和 P ( Z ˉ ) P(\bar Z) P(Zˉ) 均为类别分布。可以设想编码器输出是一个维度等于训练和测试数据分布中类别数 N N N 的 softmax 向量。例如对于 MNIST 数据集,编码器输出是 10one-hot 向量,对应训练集和测试集中的 10 个数字。

我们首先估计 P ( Z , Z ˉ ) P(Z,\bar Z) P(Z,Zˉ)。IIC 假设 Z Z Z 和 Z ˉ \bar Z Zˉ 相互独立,因此联合分布可估计为:
P ( Z , Z ˉ ) = P ( Z ) P ( Z ˉ ) T P(Z,\bar Z) = P(Z)P(\bar Z)^T P(Z,Zˉ)=P(Z)P(Zˉ)T

由此生成 N × N N×N N×N 矩阵 P ( Z , Z ˉ ) P(Z,\bar Z) P(Z,Zˉ),其中每个元素 Z i j Z_{ij} Zij 对应同时观测到两个随机变量 ( Z i , Z ˉ j ) (Z_i,\bar Z_j) (Zi,Zˉj) 的概率。若在大批量数据上进行此估计,大样本的均值可近似联合概率。

由于使用互信息来估计密度函数,IIC 将采样限制在 ( Z i , Z ˉ i ) (Z_i,\bar Z_i) (Zi,Zˉi)。本质上,对每个样本 X i X_i Xi,我们计算其潜编码 P ( Z i ) = E ( X i ) P(Z_i)=\mathcal E(X_i) P(Zi)=E(Xi),然后对 X i X_i Xi 进行变换并计算其潜编码 P ( Z ˉ i ) = E ( X ˉ i ) P(\bar Z_i)=\mathcal E(\bar X_i) P(Zˉi)=E(Xˉi)。联合分布计算如下:
P ( Z , Z ˉ ) = 1 M ∑ i = 1 M P ( Z i ) P ( Z ˉ i ) T P(Z,\bar Z) = \frac 1 M\sum_{i=1}^MP(Z_i)P(\bar Z_i)^T P(Z,Zˉ)=M1i=1∑MP(Zi)P(Zˉi)T

其中 M M M 为批大小。由于对 X i X_i Xi 和 X ˉ i \bar X_i Xˉi 使用相同编码器 E \mathcal E E,所得联合分布应具有对称性。我们通过以下操作强制对称:
P ( Z , Z ˉ ) = P ( Z , Z ˉ ) + P ( Z , Z ˉ ) T 2 P(Z,\bar Z) =\frac {P(Z,\bar Z)+ P(Z,\bar Z)^T}2 P(Z,Zˉ)=2P(Z,Zˉ)+P(Z,Zˉ)T

给定 P ( Z , Z ˉ ) P(Z,\bar Z) P(Z,Zˉ) 后,边缘分布可计算为:
P ( Z ) = ∑ j = 1 N P ( Z i , Z ˉ j ) P(Z) = \sum_{j=1}^NP(Z_i,\bar Z_j) P(Z)=j=1∑NP(Zi,Zˉj)

我们对矩阵行向求和。类似地:
P ( Z ˉ ) = ∑ i = 1 N P ( Z i , Z ˉ j ) P(\bar Z) =\sum_{i=1}^NP(Z_i,\bar Z_j) P(Zˉ)=i=1∑NP(Zi,Zˉj)

我们对矩阵列向求和。

我们可以训练神经网络编码器 E \mathcal E E 来最大化互信息,或使用以下损失函数最小化负互信息:
L ( Z , Z ˉ ) = − I ( Z ; Z ˉ ) = ∑ Z ∈ Z ∑ Z ˉ ∈ Z ˉ P ( Z , Z ˉ ) ( l o g P ( Z ) + l o g P ( Z ˉ ) − l o g P ( Z , Z ˉ ) ) \mathcal L(Z,\bar Z)= -I(Z;\bar Z) = \sum_{Z\in Z}\sum_{\bar Z\in \bar Z}P(Z,\bar Z)(logP(Z)+logP(\bar Z)-logP(Z,\bar Z)) L(Z,Zˉ)=−I(Z;Zˉ)=Z∈Z∑Zˉ∈Zˉ∑P(Z,Zˉ)(logP(Z)+logP(Zˉ)−logP(Z,Zˉ))

在实现无监督聚类前,我们重新审视目标------最大化 I ( Z ; Z ˉ ) I(Z;\bar Z) I(Z;Zˉ)。由于 X X X 和 X ˉ = G ( X ) \bar X=\mathcal G(X) Xˉ=G(X) 及其对应潜编码向量 Z Z Z 和 Z ˉ \bar Z Zˉ 共享相同信息,神经网络编码器 E \mathcal E E 应学会将 X X X 和 X ˉ \bar X Xˉ 映射到具有近似取值的潜向量 Z Z Z 和 Z ˉ \bar Z Zˉ,以实现互信息最大化。在 MNIST 场景中,外观相似的数字其潜在编码向量将在空间中间一区域聚集。

若潜编码向量是 softmax 输出,则意味着我们正在进行无监督聚类,可通过线性分配算法转换为分类器。

下一节将讨论可用于实现无监督聚类的编码器网络模型,特别是介绍能同时估计 P ( Z ) P(Z) P(Z) 和 P ( Z ˉ ) P(\bar Z) P(Zˉ) 的编码器网络。

2. 无监督聚类的编码器网络

无监督聚类的编码器网络编码器采用类 VGG 主干网络和带 softmax 输出的全连接层。为提升模型性能,IIC 采用了过聚类技术。通过使用两个或多个编码器来生成对应的边缘分布 P ( Z ) P(Z) P(Z) 和 P ( Z ˉ ) P(\bar Z) P(Zˉ),并据此生成相应的联合分布 P ( Z , Z ˉ ) P(Z,\bar Z) P(Z,Zˉ)。在网络模型实现上,这体现为具有双头或多头输出的编码器结构。

接下来,我们将深入探讨 IIC 网络模型的具体实现、训练流程与评估方法,同时研究如何通过线性分配算法为每个聚类分配对应标签。

3. 使用 Keras 实现无监督聚类

使用 Keras 实现无监督聚类,网络超参数存储于 args 变量中,VGG 主干网络对象在初始化时传入。如 build_model() 方法所示,在给定主干网络的基础上,模型实质上仅包含一个带 softmax 激活的全连接层,并支持多头结构配置。

python 复制代码
class IIC:
    def __init__(self,
                 args,
                 backbone):
        self.args = args
        self.backbone = backbone
        self._model = None
        self.train_gen = DataGenerator(args, siamese=True)
        self.n_labels = self.train_gen.n_labels
        self.build_model()
        self.load_eval_dataset()
        self.accuracy = 0
    def build_model(self):
        inputs = Input(shape=self.train_gen.input_shape, name='x')
        x = self.backbone(inputs)
        x = Flatten()(x)
        # number of output heads
        outputs = []
        for i in range(self.args.heads):
            name = "z_head%d" % i
            outputs.append(Dense(self.n_labels,
                                 activation='softmax',
                                 name=name)(x))
        self._model = Model(inputs, outputs, name='encoder')
        optimizer = Adam(lr=1e-3)
        self._model.compile(optimizer=optimizer, loss=self.mi_loss)

实现 DataGenerator 类以多线程方式高效输送输入数据。该生成器会产生所需的配对训练数据,包含原始图像 X X X 及其变换版本 X ˉ \bar X Xˉ。以下代码片段展示了 DataGenerator 类最核心的方法 __data_generation():原始输入图像经过中心裁剪( MNIST 数据集裁剪为 24×24 像素);变换图像𝑋̅则采用两种处理方式------在 ±20° 范围内随机旋转,或从图像任意位置随机裁剪 16×1618×1820×20 像素区域后重置为 24×24 尺寸(裁剪尺寸存储于 crop_sizes 列表)。

需注意的是,DataGenerator 生成的数据中仅原始图像与变换图像具有重要性。损失函数所需的配对数据沿批次维度拼接,这使得我们能在单批配对数据中高效计算损失函数。

python 复制代码
def __data_generation(self,
        start_index,
        end_index)
    d = self.crop_size // 2
    crop_sizes = [self.crop_size * 2 + i for i in range(0,5,2)]
    image_size = self.data.shape[1] - self.crop_size
    x = self.data[self.indexes[start_index:end_index]]
    y1 = self.label[self.indexes[start_index:end_index]]

    target_shape = (x.shape[0],*self.input_shape)
    x1 = np.zeros(target_shape)
    if self.siamese:
        y2 = y1
        x2 = np.zeros(target_shape)
    
    for i in range(x1.shape[0]):
        image = x[i]
        x1[i] = image[d:image_size+d,d:image_size+d]
        if self.siamese:
            rotate = np.random.randint(0,2)
            #50-50% to change of crop or rotate
            if rotate == 1:
                shape = target_shape[1:]
                x2[i] = self.random_rotate(image,
                        target_shape=shape)
            else:
                x2[i] = self.random_crop(iamge,
                        target_shape[1:],
                        crop_sizes)
    
    #for IIC, we are mostly interested in paired images
    #X and Xbar = G(X)
    if self.siamese:
        #if NIME algorithm is chosen, use this to generate
        #the training data
        if self.mine:
            y = np.concatenate([y1,y2],axis=0)
            m1 = np.copy(x1)
            m2 = np.copy(x2)
            np.random.shuffle(m2)

            x1 = np.concatenate([x1,m1],axis=0)
            x2 = np.concatenate([x2,m2],axis=0)
            x = (x1,x2)
            return x,y
        x_train = np.concatenate([x1,x2],axis=0)
        y_train = np.concatenate([y1,y2],axis=0)
        y = []
        for i in range(self.args.heads):
            y.append(y_train)
        return x_train, y
    
    return x1,y1

为实现 VGG 主干网络,使用 Keras 实现 VGG 类,该 VGG 类具有高度灵活性,支持通过不同配置方式(或不同 VGG 变体)进行构建。

python 复制代码
class VGG:
    def __init__(self,cfg,input_shape=(24,24,1)):
        self.cfg = cfg
        self.input_shape = input_shape
        self._model = None
        self.build_model()
    
    def build_model(self):
        inputs = keras.layers.Input(shape=self.input_shape,name='x')
        x = VGG.make_layers(self.cfg,inputs)
        self._model = keras.Model(inputs,x,name='VGG')
    
    @property
    def model(self):
        return self._model
    
    @staticmethod
    def make_layers(cfg,
            inputs,
            batch_norm=True,
            in_channels=1)
        x = inputs
        for layer in cfg:
            if layer == 'M':
                x = keras.layers.MaxPool2D()(x)
            elif layer == 'A':
                x = keras.layers.AveragePooling2D(pool_size=3)(x)
            else:
                x = keras.layers.Conv2D(layer,
                        kernel_size=3,
                        padding='same',
                        kernel_initializer='he_normal')(x)
                if batch_norm:
                    x = keras.layers.BatchNormalization()(x)
                x = keras.layers.Activation('relu')(x)
        return x

IIC 类的其核心算法在于实现最小化负互信息的损失函数。在单批次损失评估中,我们解析 y_pred 并将其划分为上下两部分,分别对应原始输入图像 X X X 及其变换图像 X ˉ \bar X Xˉ 的编码器输出。需要强调的是,配对数据是通过将原始图像批次与变换图像批次沿批次维度拼接而成的。y_pred 的下半部分对应潜编码 Z Z Z,上半部分对应变换后的潜编码 Z ˉ \bar Z Zˉ。我们首先计算联合分布 P ( Z , Z ˉ ) P(Z,\bar Z) P(Z,Zˉ) 与边缘分布,最终返回负互信息值。需要特别说明的是,每个输出头对总损失函数的贡献权重相同,因此最终损失值会按输出头数量进行缩放。

python 复制代码
def mi_loss(self,y_true,y_pred):
    size = self.args.batch_size
    n_labels = y_pred.shape[-1]
    #low half is Z
    Z = y_pred[0:size,:]
    Z = keras.backend.expand_dims(Z,axis=2)
    #upper half is Zbar
    Zbar = y_pred[size:y_pred.shape[0],:]
    Zbar = keras.backend.expand_dims(Zbar,axis=1)
    #compute joint distribution (Eq 10.3.2 &.3)
    P = keras.backend.batch_dot(Z,Zbar)
    P = keras.backend.sum(P,axis=0)
    #enforce symmetric joint distribution (Eq 10.3.4)
    P = (P + keras.backend.transpose(P)) / 2.0
    #normalization of total probability to 1.0
    P = P / keras.backend.sum(P)
    #marginal distributions (Eq 10.3.5 & .6)
    Pi = keras.backend.expand_dims(keras.backend.sum(P,axis=1),axis=1)
    Pj = keras.backend.expand_dims(keras.backend.sum(P,axis=0),axis=0)
    Pi = keras.backend.repeat_elements(Pi,rep=n_labels,axis=1)
    Pj = keras.backend.repeat_elements(Pj,rep=n_labels,axis=0)
    P = keras.backend.clip(P,keras.backend.epsilon(),np.finfo(float).max)
    Pi = keras.backend.clip(Pi,keras.backend.epsilon(),np.finfo(float).max)
    Pj = keras.backend.clip(Pj,keras.backend.epsilon(),np.finfo(float).max)
    #negative MI loss (Eq 10.3.7)
    neg_mi = keras.backend.sum((P * (keras.backend.log(Pi) + keras.backend.log(Pj)-keras.backend.log(P))))
    #each head contribute 1/n_heads to the total loss
    return neg_mi / self.args.heads

IIC 网络训练方法的实现如下。由于使用了继承自 Sequence 类的 DataGenerator 对象,因此可以采用 Kerasfit_generator() 方法来训练模型。系统会选择性保存性能最优模型的权重参数。在 eval() 方法中,使用线性分类器为每个聚类分配标签。该线性分类器 unsupervised_labels() 采用匈牙利算法,以最小成本原则为聚类分配标签。

python 复制代码
    def train(self):
        accuracy = AccuracyCallback(self)
        lr_scheduler = LearningRateScheduler(lr_schedule,
                                             verbose=1)
        callbacks = [accuracy, lr_scheduler]
        self._model.fit_generator(generator=self.train_gen,
                                  use_multiprocessing=True,
                                  epochs=self.args.epochs,
                                  callbacks=callbacks,
                                  workers=4,
                                  shuffle=True)

	def eval(self):
	    y_pred = self._model.predict(self.x_test)
	    print("")
	    #accuracy per head
	    for head in range(self.args.heads):
	        if self.args.heads == 1:
	            y_head = y_pred
	        else:
	            y_head = y_pred[head]
	        y_head = np.argmax(y_head,axis=1)
	
	        accuracy = unsupervised_labels(list(self.y_test),
	                list(y_head),
	                self.n_labels,
	                self.n_labels)
	        info = "Head %d accuracy: %0.2f%%"
	        if self.accuracy > 0:
	            info += ", Old best accuracy: %0.2%%"
	            data = (head,accuracy,self.accuracy)
	        else:
	            data = (head,accuracy)
	        print(info % data)
	        #if accuracy improves during training,
	        #save the model weights on a file
	        if accuracy > self.accuracy and self.args.save_weights is not None:
	            self.accuracy = accuracy
	            folder = self.args.save_dir
	            os.makedirs(folder,exist_ok=True)
	            path = os.path.join(folder,self.args.save_weights)
	            print("Saving weights...",path)
	            self._model.save_weights(path)

def unsupervised_labels(y,yp,n_classes,n_clusters):
    assert n_classes == n_clusters

    #initialize count matrix
    C = np.zeros([n_clusters,n_classes])

    #popalate count matrix
    for i in range(len(y)):
        C[int(yp[i]),int(y[i])] += 1

    #optimal permutation using Hungarian Algo
    #the higher the count, the lower the cost
    #so we use -C for linear assignment
    row, col = linear_sum_assignment(-C)

    #compute accuracy
    accuracy = C[row,col].sum() / C.sum()

    return accuracy * 100

线性分配问题旨在确定每个聚类对应的类别,即如何为聚类分配标签。成本矩阵 C C C 的构建过程如下:对于每个聚类-真实标签组合,对应矩阵单元格的值会递减 1,其行列索引分别对应聚类编号与真实标签索引。基于该成本矩阵,线性分配问题的目标是找到最优分配矩阵X,使得总成本最小:
c o s t = m i n ∑ i , j c i j x i j cost = min\sum_{i,j}c_{ij}x_{ij} cost=mini,j∑cijxij

其中 c i j c_{ij} cij 与 x i j x_{ij} xij 分别代表矩阵 C C C 与 X X X 的元素, i i i 和 j j j 为索引。矩阵 X X X 需满足以下约束条件:

  • x i j ∈ { 0 , 1 } x_{ij} \in \{0,1\} xij∈{0,1}
  • ∑ i x i j = 1 \sum_{i}x_{ij}=1 ∑ixij=1 ( j = 1 , 2 , . . . , N j=1,2,...,N j=1,2,...,N)
  • ∑ j x i j = 1 \sum_{j}x_{ij}=1 ∑jxij=1 ( i = 1 , 2 , . . . , N i=1,2,...,N i=1,2,...,N)

X X X 是一个二元矩阵,每行仅能分配至一列,因此线性分配问题属于组合优化问题。对于单输出头模型的训练,执行命令:

bash 复制代码
python iic.py --heads=1 --train --save-weights=head1.h5

若需调整输出头数量,相应修改 --heads--save-weights 参数即可。下一节我们将深入分析 IIC 模型在 MNIST 分类任务中的性能表现。

4. 验证实验

通过对测试集进行聚类预测,再经由线性分配算法为每个聚类分配标签,最终实现从聚类到分类的转化。由于优化过程可能陷入局部最优解,有时需要多次运行训练流程。此外,在多头 IIC 模型中,不同输出头的性能表现也存在差异。例如,对单头 IIC 模型进行验证的命令如下:

bash 复制代码
python iic.py --heads=1 --eval --restore-weights=head1-best.h5

综上所述,无监督分类方法具有可行性,其实际效果甚至优于《深度神经网络》中介绍的监督分类方法。

相关推荐
996终结者2 小时前
深度学习从入门到精通(一):深度学习的分类
人工智能·深度学习·分类
这儿有一堆花5 小时前
向工程神经网络对二进制加法的巧妙解决方案
人工智能·深度学习·神经网络
点云SLAM5 小时前
方差的迭代计算公式
大数据·深度学习·数据分析·概率论·数学原理·概论率
auutuumn8 小时前
PyTorch深度学习实战01:全流程体验深度学习
人工智能·pytorch·深度学习
B站_计算机毕业设计之家8 小时前
深度学习:Yolo水果检测识别系统 深度学习算法 pyqt界面 训练集测试集 深度学习 数据库 大数据 (建议收藏)✅
数据库·人工智能·python·深度学习·算法·yolo·pyqt
xier_ran10 小时前
深度学习:为什么不能将多层神经网络参数全部初始化为零以及如何进行随机初始化
人工智能·深度学习
夫唯不争,故无尤也10 小时前
PyTorch中张量和模型的核心属性解析
人工智能·pytorch·深度学习
哥布林学者11 小时前
吴恩达深度学习课程二: 改善深层神经网络 第二周:优化算法(三)Momentum梯度下降法
深度学习·ai
AI街潜水的八角12 小时前
深度学习十种食物分类系统1:数据集说明(含下载链接)
人工智能·深度学习·分类