图机器学习(6)——图自编码器
图机器学习(6)——图自编码器
- 0. 自编码器
- 1. 构建自编码器
- 2. 去噪自编码器
- 3. 图自编码器
0. 自编码器
自编码器是一个处理高维数据集的强大工具,近年来,自编码器随着神经网络算法的发展而获得广泛应用。这种工具不仅能压缩稀疏表示,还可作为生成模型的基础架构。
自编码器是一种输入输出相同的特殊神经网络,但在隐藏层中有较少的单元。简言之,它是一个使用显著较少的变量或自由度来重建其输入的神经网络。
由于自编码器不需要标签数据集,因此可以看作是一种无监督学习和降维技术。然而,与主成分分析和矩阵分解等其他技术不同,自编码器可以通过神经元的非线性激活函数学习非线性变换。
上图展示了一个简单的自编码器示例。可以看到,自编码器通常由两个核心组件构成:
- 编码器网络:通过一个或多个隐藏单元处理输入数据,将其映射为维度缩减的编码表示(欠完备自编码器)或施加稀疏约束的表示(过完备正则化自编码器)
- 解码器网络:从中间层的编码表示重构原始输入信号
整个编码器-解码器结构的训练目标是最小化网络重构输入的误差。完整定义自编码器需要指定损失函数,输入与输出间的误差可通过不同指标衡量——事实上,如何选择恰当的"重构误差"形式是构建自编码器的关键环节。常用的重构误差损失函数包括:均方误差、平均绝对误差、交叉熵和KL散度。
接下来,我们将从基础概念入手,从零开始构建自编码器,并将这些概念应用于图结构数据。
1. 构建自编码器
我们将从基础的自编码器实现开始——即构建一个简单的前馈神经网络来重构其输入。我们将把这个模型应用于 Fashion-MNIST
数据集,Fashion-MNIST
数据集类似于 MNIST
数据集,但包含的是黑白服装图像。
Fashion-MNIST
包含 10
个类别,由 60000
张训练图像和 10000
张测试图像组成,每张都是 28×28
像素的灰度图,分别对应以下服装品类:T恤、裤子、套头衫、连衣裙、外套、凉鞋、衬衫、运动鞋、包包和踝靴。相比原始 MNIST
数据集,Fashion-MNIST
任务更具挑战性,常被用作算法基准测试。
(1) 使用 Keras
导入 Fashion-MNIST
数据集:
(x_train, y_train), (x_test, y_test) = fashion_mnist.load_data()
(2) 最佳实践是将输入数据缩放到 1
左右的数量级(此时激活函数效率最高),并确保数值数据为单精度( 32
位)而非双精度( 64
位)。这是因为神经网络训练过程计算成本高昂,通常更注重速度而非精度。在某些情况下,精度甚至可以降低到半精度( 16
位)。对输入进行转换:
x_train = x_train.astype('float32') / 255.
x_test = x_test.astype('float32') / 255.print(x_train.shape)
print(x_test.shape)
(3) 绘制训练集中的样本,以直观了解我们所处理的输入类型:
classes = {0:"T-shirt/top",1: "Trouser",2: "Pullover",3: "Dress",4: "Coat",5: "Sandal",6: "Shirt",7: "Sneaker",8: "Bag",9: "Ankle boot",
}
n = 6
plt.figure(figsize=(20, 4))
for i in range(n):# display originalax = plt.subplot(1, n, i + 1)plt.imshow(x_test[i])plt.title(classes[y_test[i]])plt.gray()ax.get_xaxis().set_visible(False)ax.get_yaxis().set_visible(False)plt.show()
在以上代码中,classes
变量定义了整数标签与服装类别的映射关系(例如:0
对应T恤,1
对应裤子,2
对应套头衫等):
(4) 完成数据导入后,使用 Keras
函数式 API
构建自编码器网络,该 API
相比顺序式 API
具有更强的灵活性和扩展性。首先定义编码器网络结构:
input_img = Input(shape=(28, 28, 1))x = Conv2D(16, (3, 3), activation='relu', padding='same')(input_img)
x = MaxPooling2D((2, 2), padding='same')(x)
x = Conv2D(8, (3, 3), activation='relu', padding='same')(x)
x = MaxPooling2D((2, 2), padding='same')(x)
x = Conv2D(8, (3, 3), activation='relu', padding='same')(x)
encoded = MaxPooling2D((2, 2), padding='same')(x)
网络由三层相同模式的堆叠组成,每层包含两个核心组件::
Conv2D
,二维卷积层,通过权值共享机制处理输入特征。卷积运算后使用ReLU
激活函数进行非线性变换。第一层设置特征图数为16
,第二层和第三层为8
MaxPooling2D
,通过在指定窗口(在本节中为2x2
)上取最大值来对输入进行下采样
通过 Keras API
,我们还可以使用 Model
类查看各层如何变换输入数据:
Model(input_img, encoded).summary()
模型摘要信息如下所示,可以看到,编码阶段最终输出 (4,4,8)
的三维张量,较原始输入的 (28,28)
尺寸实现了六倍以上的压缩率:
接下来,构建解码器网络。需要特别说明的是,解码器网络的结构设计与编码器并无强制对称性要求,二者的权重参数也相互独立:
x = Conv2D(8, (3, 3), activation='relu', padding='same')(encoded)
x = UpSampling2D((2, 2))(x)
x = Conv2D(8, (3, 3), activation='relu', padding='same')(x)
x = UpSampling2D((2, 2))(x)
x = Conv2D(16, (3, 3), activation='relu')(x)
x = UpSampling2D((2, 2))(x)
decoded = Conv2D(1, (3, 3), activation='sigmoid', padding='same')(x)
在本节中,解码器网络与编码器结构相似,唯一的不同是,将 MaxPooling2D
的下采样操作替换为 UpSampling2D
上采样层——该层通过在指定窗口(本节为 2×2
)内重复输入值,使张量在每个维度上扩大两倍。
(5) 完成编码器-解码器的网络结构定义后,为了完整配置自编码器,我们还需要指定损失函数。此外,为了构建计算图,Keras
还需要知道应该使用的网络权重优化算法。通常,Keras
需要这两部分信息(损失函数和优化器)编译模型:
autoencoder = Model(input_img, decoded)
autoencoder.compile(optimizer='adam', loss='binary_crossentropy')
(6) 接下来,训练自编码器。Keras
的 Model
类提供的 API
类似于 scikit-learn
,使用 fit
方法训练神经网络。需要注意的是,由于自编码器的特性,自编码器的输入输出均为相同数据:
autoencoder.fit(x_train, x_train,epochs=50,batch_size=128,shuffle=True,validation_data=(x_test, x_test),callbacks=[TensorBoard(log_dir='/tmp/autoencoder')])
(7) 一旦训练完成,我们可以通过比较输入图像与其重建版本,来检验网络重建输入的能力,通过 Keras
的 Model
类的 predict
方法计算模型输出:
decoded_imgs = autoencoder.predict(x_test)
下图展示了重建后的图像。可以看到,网络在重建未见过的图像方面表现得相当好,网络能较好地重构测试图像(尤其是宏观特征),虽然压缩过程会丢失细节(如T恤图案),但关键特征已被有效捕捉。
(8) 将图像经过编码后的嵌入通过 T-SNE
降维至二维平面进行可视化:
from sklearn.manifold import TSNE
import numpy as np
from matplotlib.cm import tab10tsne = TSNE(n_components=2)
emb2d = tsne.fit_transform(embeddings)
x,y = np.squeeze(emb2d[:, 0]), np.squeeze(emb2d[:, 1])
summary = pd.DataFrame({"x": x, "y": y, "target": y_test, "size": 10})plt.figure(figsize=(10,8))for key, sel in summary.groupby("target"):plt.scatter(sel["x"], sel["y"], s=10, color=tab10.colors[key], label=classes[key])plt.legend()
plt.axis("off")
plt.show()
运行结果如下所示,可以看到 T-SNE
降维坐标(按样本类别着色)清晰呈现了不同服装的聚类效果,部分类别表现出显著分离性
但需要注意的是,自动编码器容易过拟合,因为它们往往会完全重建训练图像,而不具备良好的泛化能力。接下来,我们将学习如何防止过拟合,从而构建更加鲁棒的密集表示。
2. 去噪自编码器
自编码器不仅能将稀疏表示压缩为稠密向量,还广泛用于处理信号,去除噪声并提取相关(特征)信号。这在许多应用中非常有用,特别是在识别异常值和离群值时。
(1) 去噪自编码器是基本自编码器的一个变体。如前一节所述,基本自编码器使用相同图像作为输入和输出进行训练,而去噪自编码器则会在输入中注入不同强度的噪声,同时保持目标输出为纯净信号。实现方式之一是为输入添加高斯噪声:
noise_factor = 0.1
x_train_noisy = x_train + noise_factor * np.random.normal(loc=0.0, scale=1.0, size=x_train.shape)
x_test_noisy = x_test + noise_factor * np.random.normal(loc=0.0, scale=1.0, size=x_test.shape) x_train_noisy = np.clip(x_train_noisy, 0., 1.)
x_test_noisy = np.clip(x_test_noisy, 0., 1.)
(2) 训练时使用带噪输入数据,但目标输出仍为原始干净图像:
noisy_autoencoder.fit(x_train_noisy, x_train,epochs=50,batch_size=128,shuffle=True,validation_data=(x_test_noisy, x_test))
该方法通常适用于大规模数据集,此时过拟合噪声的风险较低。对于小规模数据集,为避免网络"记忆"静态噪声模式(即学习带噪图像到干净图像的固定映射),可以使用 GaussianNoise
层动态注入随机噪声。通过这种方式,每个训练 epoch
注入的噪声会动态变化,从而防止网络学习训练集上的固定噪声模式。为此,需要调整网络结构:
input_img = Input(shape=(28, 28, 1))
noisy_input = GaussianNoise(0.1)(input_img)
x = Conv2D(16, (3, 3), activation='relu', padding='same')(noisy_input)
不同之处在于,噪声输入从静态噪声(训练过程中不变)转变为动态变化(每个 epoch
重新生成),从而有效阻止网络记忆噪声模式。
GaussianNoise
层是正则化层的一种,这类层通过引入随机扰动来抑制神经网络过拟合。GaussianNoise
层使得模型更具鲁棒性,能够更好地进行泛化,避免自编码器学习恒等函数。
Dropout
层是另一种常见的正则化层,它以概率 p
随机置零部分输入,并将剩余输入缩放 1/(1-p)
倍,从而保持所有单元的总激活强度(统计上)不变。
Dropout
层相当于随机“关闭”层之间的某些连接,以减少输出对特定神经元的依赖。需要注意的是,正则化层只在训练过程中起作用,而在测试时,会自动退化为恒等映射。
在下图中,我们比较了噪声输入(输入)在未正则化训练的网络和带有 GaussianNoise
层的网络中的重建效果。可以看到,(例如比较裤子的图像)带有正则化的模型通常能够更有效地进行重建,并且能够重建无噪声的输出。
正则化层通常用于处理容易过拟合的深度神经网络,并能够学习自编码器的恒等函数。通常会引入 Dropout
层或高斯噪声层,重复由正则化层和可学习层组成的类似模式,这种结构通常称为堆叠去噪层。
3. 图自编码器
理解了自编码器的基本概念后,我们就可以将这一框架应用于图结构。虽然网络结构仍然可以分解为“编码器-解码器”架构,并在中间嵌入低维表示,但在处理图数据时,损失函数的定义需要格外谨慎。首先,我们需要调整重构误差,使其能够适应图结构的特性。为此,我们首先引入一阶邻接和高阶邻接的概念。
将自编码器应用于图结构时,网络的输入和输出应当是图的某种表示,例如邻接矩阵。此时,重构损失可以定义为输入矩阵与输出矩阵之差的 Frobenius
范数。然而,将自编码器应用于图结构和邻接矩阵时,会出现两个关键问题:
- 边的存在表示两个顶点之间存在关系或相似性,但边的缺失通常并不代表顶点之间的不相似性
- 邻接矩阵极为稀疏,因此模型自然会倾向于预测
0
,而不是正值。
为了应对图结构的这些特殊性,在定义重构损失时,我们需要对非零元素的预测误差施加更大的惩罚,而对零元素的误差惩罚较小。这可以通过以下损失函数实现:
L2nd=∑i=1n∥(X~i−Xi)⊙bi∥L_{2nd}=∑_{i=1}^n∥(\tilde X_i−X_i)⊙b_i∥ L2nd=i=1∑n∥(X~i−Xi)⊙bi∥
其中,⊙⊙⊙ 表示 Hadamard
逐元素乘积,如果节点 iii 和 jjj 之间存在边,则 bijb_{ij}bij 为 1
,否则为 0
。该损失函数确保共享邻域(即邻接向量相似)的顶点在嵌入空间中也彼此接近。因此,上述公式能够自然地在重构图中保持二阶邻接。
另一方面,我们还可以在重构图中增强一阶邻接,使得相连的节点在嵌入空间中彼此靠近。这一目标可以通过以下损失函数实现:
L1th=∑i,j=1nSij∣∣yj−yi∣∣22L_{1th}=\sum_{i,j=1}^nS_{ij}||y_j-y_i||_2^2 L1th=i,j=1∑nSij∣∣yj−yi∣∣22
其中,yiy_iyi 和 yjy_jyj 分别是节点 iii 和 jjj 在嵌入空间中的表示。该损失函数迫使相邻节点在嵌入空间中彼此接近。具体来说,如果两个节点紧密相连(即 SijS_{ij}Sij 较大),那么它们在嵌入空间中的距离 ∥yj−yi∥∥y_j−y_i∥∥yj−yi∥ 应当较小,以保持损失函数的值较低。
这两个损失函数可以结合成一个统一的损失函数。此外,为了防止过拟合,可以添加一个与权重系数范数成正比的正则化损失:
Ltotal=L2nd+α⋅L1st+v⋅Lreg=L2nd+α⋅L1st+v⋅∥W∥F2L_{total}=L_{2nd}+α\cdot L_{1st}+v\cdot L_{reg}=L_{2nd}+α\cdot L_{1st}+v\cdot ∥W∥_F^2 Ltotal=L2nd+α⋅L1st+v⋅Lreg=L2nd+α⋅L1st+v⋅∥W∥F2
其中,WWW 表示网络中所有的权重。上述公式由 Wang
等人于 2016 年提出,现称为结构深度网络嵌入 (Structural Deep Network Embedding
, SDNE
)。
尽管上述损失函数可以用 TensorFlow
和 Keras
实现,但我们可以直接在 GEM
包中使用该网络的实现。提取节点嵌入:
G=nx.karate_club_graph()
sdne=SDNE(d=2, beta=5, alpha=1e-5, nu1=1e-6, nu2=1e-6,K=3,n_units=[50, 15,], rho=0.3, n_iter=10,xeta=0.01,n_batch=100,modelfile=['enc_model.json','dec_model.json'],weightfile=['enc_weights.hdf5','dec_weights.hdf5'])
sdne.learn_embedding(G)
embeddings = m1.get_embedding()
虽然这类图自编码器功能强大,但在处理大型图数据时会面临挑战。因为在大型网络中,自编码器的输入是邻接矩阵,其每行元素数量与图中节点数相同(可能达到数百万甚至数千万量级)。