from __future__ import print_function, division import argparse import random import numpy as np from sklearn.cluster import KMeans from sklearn.metrics.cluster import normalized_mutual_info_score as nmi_score from sklearn.metrics import adjusted_rand_score as ari_score import torch import torch.nn as nn import torch.nn.functional as F from torch.nn.parameter import Parameter from torch.optim import Adam from torch.utils.data import DataLoader from torch.nn import Linear from utils import load_data, load_graph from evaluation import eva from collections import Counter # torch.cuda.set_device(0) class AE(nn.Module): #定义了一个名为 AE 的自编码器类,继承自 nn.Module 类,以便可以使用 PyTorch 框架中的各种功能和特性 #三个编码器层 和 三个解码器层 每个层都由一个全连接线性层和rule激活函数组成 def __init__(self, n_enc_1, n_enc_2, n_enc_3, n_dec_1, n_dec_2, n_dec_3, n_input, n_z): super(AE, self).__init__() #encode 编码器 #全连接线性层: y=Wx+b x是输入向量 W是权值矩阵 b是偏置向量 y是输出向量 #“可学习”是指神经网络中的参数(如权重和偏置)可以通过反向传播算法自动调整,以最小化训练数据上的损失函数。 #每一个输入特征都和所有输出特征相连(即全连接) 全连接层:就是将输入的每个特征都与一组可学习的权值相乘,再加上一个偏置值。 得到一个新向量表示。 self.enc_1 = Linear(n_input, n_enc_1) #self.enc_1 2 等是一个Linear层对象 self.enc_2 = Linear(n_enc_1, n_enc_2) #这段代码中的参数是神经网络中的层参数,包括输入维度(n_enc_1)和输出维度(n_enc_2)。self.enc_2表示这是一个全连接层(Linear),它将输入向量的维度从n_enc_1转换为n_enc_2。 # 在神经网络训练过程中,这些参数会被反向传播算法自动学习,以优化模型的性能。 self.enc_3 = Linear(n_enc_2, n_enc_3) self.z_layer = Linear(n_enc_3, n_z) #self.enc_1 = Linear(n_input, n_enc_1) 定义了一个名为 self.enc_1 的对象,即第一层编码器 # 可以自动执行矩阵乘法和偏置相加操作 #self.z_layer = Linear(n_enc_3, n_z):定义一个输入维度为 n_enc_3,输出维度为 n_z 的线性层作为编码层。 #decode 解码器 self.dec_1 = Linear(n_z, n_dec_1) self.dec_2 = Linear(n_dec_1, n_dec_2) self.dec_3 = Linear(n_dec_2, n_dec_3) self.x_bar_layer = Linear(n_dec_3, n_input) #定义了前向传播函数 def forward(self, x): #输入一个X 输出相应的状态 #右边:编码器的输入 等号左边为编码器的输出 #relu激活函数: 输入值x >=0 则输出x 否则输出0 f(x)=max(0,x) 将负值归为0 正值结果不变 enc_h1 = F.relu(self.enc_1(x)) enc_h2 = F.relu(self.enc_2(enc_h1)) enc_h3 = F.relu(self.enc_3(enc_h2)) #在 forward 函数中,通过调用 self.enc_1(x) 方法,将输入数据 x 传递给第一层编码器进行线性变换。 其它相同 #self.enc_1 对象的创建与调用 self.enc_1(x) 方法密切相关,是调用该方法的前提条件 #最终输出 z = self.z_layer(enc_h3) #将第三层编码器的输出作为输入,通过编码层进行线性转换得到编码 z。 #把编码器的输出作为解码器的输入 ->变成解码器的状态 dec_h1 = F.relu(self.dec_1(z)) # 将编码 z 作为输入,通过第一层解码器进行线性转换,并经过ReLU激活函数得到第一层解码器的输出 dec_h1。 dec_h2 = F.relu(self.dec_2(dec_h1)) dec_h3 = F.relu(self.dec_3(dec_h2)) #最终输出 x_bar = self.x_bar_layer(dec_h3) #将第三层解码器的输出作为输入,通过重构层进行线性转换得到重构数据 x_bar。 return x_bar, enc_h1, enc_h2, enc_h3, z #SDCN聚类算法 class SDCN(nn.Module): #SDCN是一种基于自编码器的聚类算法 #SDCN 主要思想是利用自编码器学习数据点的低维表达,并通过双重自监督方法进行聚类。 # 具体地,SDCN 由两部分组成:自编码器和双重自监督模块 #AE部分如上 双重自监督块则通过将数据点与聚类中心之间的距离转化为相似度,以双重自监督方式训练网络,从而实现聚类任务 def __init__(self, n_enc_1, n_enc_2, n_enc_3, n_dec_1, n_dec_2, n_dec_3, n_input, n_z, n_clusters, v=1): super(SDCN, self).__init__() #AE部分 # autoencoder for intra information self.ae = AE( n_enc_1=n_enc_1, n_enc_2=n_enc_2, n_enc_3=n_enc_3, n_dec_1=n_dec_1, n_dec_2=n_dec_2, n_dec_3=n_dec_3, n_input=n_input, n_z=n_z) #self.ae 该属性是AE自编码器的对象 #其各层的维度和超参数由输入参数指定 # 从预训练模型文件中加载预训练权重并将其设置为自编码器 ae 的权重。 self.ae.load_state_dict(torch.load(args.pretrain_path, map_location='cpu')) #从指定路径 args.pretrain_path 中加载预训练模型的权重参数,并将其设置为自编码器 self.ae 的权重 #torch.load(args.pretrain_path, map_location='cpu') 函数用于从文件中加载对象,然后将其返回。 #加载了一个之前保存的预训练模型文件,该文件包含了自编码器的权重和偏置参数。加载时,使用 map_location='cpu' 指定运行设备为 CPU #加载完毕后,通过 self.ae.load_state_dict() 方法将预训练模型的权重设置为自编码器 self.ae 的权重 # cluster layer self.cluster_layer = Parameter(torch.Tensor(n_clusters, n_z)) #将一个张量定义为可学习Parameter变量 #定义一个名为 self.cluster_layer 的可学习变量(即是一个 Parameter 对象),其大小为 (n_clusters, n_z),即聚类层的权重矩阵。 #使用 Parameter 函数将一个张量封装成可训练的变量,并将其添加到模型中(self.)。 #由于它是一个可学习变量,所以在反向传播时会自动计算梯度并更新参数 #torch.Tensor(n_clusters, n_z) 用于创建一个大小为 (n_clusters, n_z) 的张量,它包含了聚类层的权重参数 n_z 是自编码器的输出维度 #张量(tensor)是一种多维数组 在模型训练时,该参数会根据输入数据和损失函数不断更新,最终用于表示每个聚类的中心向量。 # 这里使用 PyTorch 的 Parameter 函数将其封装成可训练的变量。 torch.nn.init.xavier_normal_(self.cluster_layer.data) #对聚类层的权重矩阵进行 Xavier 初始化,即从均匀分布中随机采样权重值并乘以一个初始化因子。 #Xavier 是一种常用的权重初始化方法,它根据输入和输出维度自适应地调整权重分布的方差, # 以避免梯度消失或梯度爆炸的问题,从而提高模型的性能和收敛速度 #使用 torch.nn.init.xavier_normal_() 函数来实现 Xavier 初始化。 # 具体地,self.cluster_layer.data 表示聚类层权重矩阵的数据,即一个形状为 (n_clusters, n_z) 的张量对象 #调用 torch.nn.init.xavier_normal_(self.cluster_layer.data) 函数会对该张量进行 Xavier 初始化,并将结果保存回张量中 # degree 设置自编码器的度数参数 v self.v = v #这段代码的作用是设置自编码器的度数参数 v。SDCN 算法中,双重自监督模块需要用到一个度数参数 v, # 用于计算数据点到聚类中心之间的距离。具体地,该度数参数 v 通常取值为 1 或 2,其越小则表示聚类中心之间的距离影响越大; # 反之则表示数据点之间的距离影响越大。在 SDCN 类的初始化函数中,通过 self.v = v 将输入参数 v 赋值给实例变量 self.v, # 即将度数参数 v 设置为输入参数 v,以便在后续的双重自监督模块中使用。 #在 SDCN 算法中,作者引入了参数 v 来控制度量距离时各个特征之间的权重,从而使距离度量更加鲁棒。具体来说,将原始数据 x 归一化为单位范数, # 然后在计算距离时使用带权重的欧几里得距离,其中每个维度上的权重由参数 v 控制,即对于两个数据点 x_i 和 x_j ,它们之间的距离可以表示为: #d(x_i, x_j) = sqrt(sum((v * (x_i - x_j))^2)) # 当 v=1 时,距离度量就是标准的欧几里得距离;当 v<1 时,表示某些特征比其他特征更重要;当 v>1 时,表示某些特征比其他特征更不重要。 def forward(self, x): #前向传播过程,输入原始数据 x ,通过自编码器 ae 进行编解码操作,得到重构结果 x_bar 和编码表达 z 。 #同时,通过双重自监督模块计算样本点和聚类中心之间的相似度 q ,并将其标准化为概率分布形式,作为输出结果返回 # DNN Module x_bar, tra1, tra2, tra3, z = self.ae(x) #将输入数据 x 通过自编码器 ae 进行编码和解码,并返回重构结果 x_bar 和编码器每一层的输出 tra1、tra2、tra3,以及最终的编码表达 z。 # Dual Self-supervised Module 双重自监督模块 #这一步是软分配 #将点到聚类中心的距离转换为相似度 q = 1.0 / (1.0 + torch.sum(torch.pow(z.unsqueeze(1) - self.cluster_layer, 2), 2) / self.v) #里面是计算欧式距离 #这段代码是 SDCN 算法中的双重自监督模块,用于计算数据点到聚类中心之间的相似度矩阵 q #该模块通过将数据点与聚类中心之间的距离转化为相似度来判断它们是否属于同一类别 #z 表示编码器的输出向量,即数据点经过编码器后得到的低维表达; # self.cluster_layer 表示聚类层的权重矩阵,其大小为 (n_clusters, n_z); # self.v 表示度数参数,通常取值为 1 或 2。 #z.unsqueeze(1):将编码器的输出向量 z 在第二个维度上进行扩展,变成一个形状为 (batch_size, 1, n_z) 的三维张量。 #z.unsqueeze(1) - self.cluster_layer:将聚类中心的权重矩阵 self.cluster_layer 广播成与 z 相同的形状, # 并与 z.unsqueeze(1) 相减,得到一个形状为 (batch_size, n_clusters, n_z) 的差值张量,表示每个数据点与所有聚类中心之间的距离。 #torch.pow(z.unsqueeze(1) - self.cluster_layer, 2):对差值张量中的每个元素进行平方操作,得到一个形状为 (batch_size, n_clusters, n_z) 的平方张量。 #首先通过torch.pow计算出数据点和每个聚类中心之间的欧式距离平方,并将其除以参数self.v(这里v的作用是调节聚类簇之间的相似度) #torch.sum(torch.pow(z.unsqueeze(1) - self.cluster_layer, 2), 2):对平方张量沿着第三个维度(即 n_z 维度)进行求和, # 得到一个形状为 (batch_size, n_clusters) 的距离矩阵,表示每个数据点到所有聚类中心的距离。 #1.0 / (1.0 + torch.sum(torch.pow(z.unsqueeze(1) - self.cluster_layer, 2), 2) / self.v): # 将距离矩阵除以度数参数 self.v 并加上常数 1,然后取倒数,得到一个形状为 (batch_size, n_clusters) 的相似度矩阵 q。 # 其中,q[i][j] 表示第 i 个数据点与第 j 个聚类中心之间的相似度。 #这一步计算的就是样本点和聚类中心之间的相似度,也称为“软分配”(soft assignment) #其中 z 是自编码器的输出 (encoder 的输出),表示每个样本的低维嵌入向量;self.cluster_layer 是聚类层参数,表示每个聚类的中心向量; # self.v 是一个超参数,表示高斯核函数的方差。 # 最终,计算得到的 q 是一个形状为 (batch_size, n_clusters) 的张量,其中每行代表一个样本对于每个聚类的软分配概率,即该样本属于每个聚类的概率。 q = q.pow((self.v + 1.0) / 2.0) #对相似度 q 进行幂运算,增加相似度间的差异性。 #这段代码用于对q张量进行幂次变换,其目的是增强概率分布的聚集性。该操作将q中每个元素取 (self.v + 1.0) / 2.0 次幂, #由于self.v参数通常设置为较小的值,因此该操作会增加数据点与最近聚类中心之间的相似度(即降低离群点的权重),从而更好地表达数据之间的聚合关系。最终得到一个形状 #仍为(batch_size, n_clusters)的张量q。 q = (q.t() / torch.sum(q, 1)).t() #归一化:数据点属于某个聚类的概率 (一行加和为1) #将幂运算后的相似度 q 标准化为概率分布形式。 #这段代码用于对q张量进行归一化操作,其目的是将每个数据点对应各个聚类的软分配概率转换为标准的概率分布。具体来说,该操作首先通过 #torch.sum(q, 1)沿着第二维度(也就是聚类数目)计算每个数据点对应的累加值,然后使用t() #将结果进行转置操作,使得它可以与q张量相除。最终再次使用t()将结果转回原始形状。这样做之后, #q张量中的每个元素都表示了对应数据点属于某个聚类的概率,而且每行的概率之和等于1,即成为了标准的概率分布。 return x_bar, q, z #返回重构结果 x_bar、聚类分布 q 和编码表达 z def target_distribution(q):#它的输入是一个二维数组q。该函数的作用是计算q的权重并返回对应的目标分布 # 这段代码的目的是计算q的平方值在不同列之间的比例,然后根据这些比例得出q的目标分布 weight = q ** 2 / q.sum(0) return (weight.t() / weight.sum(1)).t() #该函数首先计算每一列(轴0)中所有元素的平方并除以这一列中所有元素之和,得到一个新的一维数组weight。 # 然后,将这个数组转置后再次执行相同的操作,最终得到一个新的二维数组,表示q的目标分布。 def train_sdcn(dataset): #定义 SDCN 模型,并初始化模型参数 model = SDCN(500, 500, 2000, 2000, 500, 500, n_input=args.n_input, n_z=args.n_z, n_clusters=args.n_clusters, v=1.0).to(device) print(model) #.to(device) 是 PyTorch 中的一个方法,用于将模型或张量转移到指定的设备上运行。在这里,device 是一个表示正在使用的计算设备(如 CPU 或 GPU)的变量。 # 通过调用 .to(device) 方法,我们可以将 SDCN 模型移动到指定的设备上进行训练和推理。 #使用 Adam 优化器来优化模型的参数。 ??? optimizer = Adam(model.parameters(), lr=args.lr) #Adam 优化器是一种常用的梯度下降算法变体,它可以自适应地调整每个参数的学习率,从而加速模型的优化过程。在该代码中,使用 Adam 优化器来优化 SDCN 模型的参数。 #具体来说,程序将模型的参数 model.parameters() 和学习率 args.lr 传递给 Adam 优化器。然后,在每次迭代时,optimizer.step() 函数会根据 # 当前的梯度信息更新模型的权重和偏置等参数,以最小化损失函数。同时,optimizer.zero_grad() 函数会将模型的梯度清零,以避免梯度累积影响优化效果。 #因此,通过反向传播计算网络的误差梯度,并利用 Adam 优化器对模型参数进行更新,最终实现了对 SDCN 模型的训练和优化。cluster parameter initiate 聚类参数初始化 data = torch.Tensor(dataset.x).to(device) #将输入数据转换为 PyTorch 张量格式,并上传到 相关设备上(GPU)运行环境中。 y = dataset.y #获取输入数据的标签信息,并保存到变量 y 中。 with torch.no_grad(): #利用自编码器对输入数据进行无监督学习,将其映射到低维空间中,并提取嵌入向量 z。 _, _, _, _, z = model.ae(data) #在这一步操作中,由于没有进行反向传播计算梯度,因此使用了 torch.no_grad() 上下文管理器来告诉 PyTorch 不需要计算这部分计算图的梯度。 #with torch.no_grad(): 是 PyTorch 中的一个上下文管理器(context manager),用于控制梯度计算的开启和关闭。 # 在 with torch.no_grad(): 的上下文中,所有的张量操作都不会被跟踪,也就是说,不会在计算图中记录它们的操作过程和梯度信息,从而减少内存占用并提高计算效率。 # 通常情况下,with torch.no_grad(): 语句块中的代码都是一些不需要进行梯度计算的操作,例如模型的推理阶段或者预处理数据等。这样做可以避免占用大量的显存空间,同时加快计算速度。 kmeans = KMeans(n_clusters=args.n_clusters, n_init=20) #创建 KMeans 聚类对象,并设置聚类中心的个数和初始点的数量 #在进行聚类操作之前,KMeans 算法会产生若干组随机初始点来对聚类中心进行初始化,然后重复执行聚类操作,直到收敛并找到最优的聚类结果为止。 #n_init 参数控制了随机初始化的次数,每次都使用不同的随机初始点来运行聚类算法,从而克服算法陷入局部最优解的可能性。最终选择使损失函数最小化的那一组聚类结果作为最终结果返回。 y_pred = kmeans.fit_predict(z.data.cpu().numpy()) #使用 KMeans 算法对嵌入向量进行聚类,并获取聚类结果。 y_pred_last = y_pred #将最终聚类结果保存到变量 y_pred_last 中,用于后续判断停止条件。 model.cluster_layer.data = torch.tensor(kmeans.cluster_centers_).to(device) #将聚类中心作为聚类层参数进行初始化。 #将 KMeans 聚类算法得到的聚类中心更新到 SDCN 模型的聚类层参数上。 #具体来说,该操作将聚类中心转换为 PyTorch 张量格式,并上传到 GPU 上(如果模型在 GPU 上运行)。然后,将此参数赋值给 SDCN 模型的聚类层,从而更新聚类中心 #这里使用 model.cluster_layer.data 访问聚类层的参数(也就是聚类中心),并将其设置为新的聚类中心。 #由于聚类中心是优化器无法更新的变量,因此需要手动更新聚类中心 eva(y, y_pred, 'pae') #计算当前聚类结果与真实标签之间的评价指标。 for epoch in range(200): #迭代200次 if epoch % 1 == 0: #如果当前迭代次数可以被 1 整除,则执行以下操作 # update_interval _, tmp_q, _ = model(data) #对输入数据进行前向计算,得到模型的软分配概率 tmp_q tmp_q = tmp_q.data p = target_distribution(tmp_q) #调用函数 根据软分配概率 tmp_q 计算目标分布 p。 #软分配概率(n,k) (k为聚类中心个数) res1 = tmp_q.cpu().numpy().argmax(1) # Q 将软分配概率转换为 numpy 数组类型,并获取其中概率最大的索引作为聚类结果 res1 #代码中将模型输出的软分配概率 tmp_q 转换为 numpy 数组类型后, # 使用 .argmax(1) 方法获取每个样本所属的聚类类别(即概率最大的那一维的索引),并将这些聚类结果保存到变量 res1 中 #目标分布(n,k) res3 = p.data.cpu().numpy().argmax(1) # P 将目标分布转换为 numpy 数组类型,并获取其中概率最大的索引作为聚类结果 res3 #调用评价函数 eva(),并将真实标签 y、聚类结果 res1 或 res3,以及当前迭代次数的字符串表示(用于标识不同迭代次数的聚类结果)传递给该函数, # 计算并输出聚类结果的评价指标。 eva(y, res1, str(epoch) + 'Q') eva(y, res3, str(epoch) + 'P') x_bar, q, _ = model(data) #对输入数据进行前向计算,得到重构的输出数据 x_bar 和当前的软分配概率 q kl_loss = F.kl_div(q.log(), p, reduction='batchmean') #计算当前模型输出的软分配概率 q 和目标分布 p 之间的 KL 散度,并将其作为 KL 散度项 kl_loss。 #该语句中 F.kl_div() 是 PyTorch 中计算 KL 散度的函数,q.log() 表示取对数,p 表示目标分布,reduction='batchmean' 表示将所有样本的 KL 散度平均成一个标量。 #计算当前模型输出的软分配概率 q 和目标分布 p 之间的 KL 散度,并将其保存到变量 kl_loss 中。通过最小化 KL 散度损失,SDCN 模型可以训练出更好的聚类效果 re_loss = F.mse_loss(x_bar, data) #计算当前模型输出的重构数据 x_bar 和输入数据 data 之间的均方误差,并将其作为重构损失项 re_loss。 #该语句中 F.mse_loss() 是 PyTorch 中计算 MSE 的函数,x_bar 表示模型输出的重构数据,data 表示原始输入数据。通过计算二者之间的平均平方差,得到重构损失项 re_loss, loss = kl_loss + re_loss #具体来说,该语句将 KL 散度项 kl_loss 和重构损失项 re_loss 相加,得到模型的总损失值 #KL 散度项和重构损失项都是损失函数的一部分,它们分别反映了聚类损失和重构损失对模型训练的影响。KL 散度项可以使模型更好地进行聚类,提高聚类效果, # 而重构损失项可以保证模型输出的重构数据尽量接近输入数据,从而提高模型的重构能力。 #通过最小化总损失,SDCN 模型可以不断优化模型参数,使得模型能够更好地学习数据的特征,并完成聚类任务和重构任务,从而提高模型的性能和泛化能力。 #完成一次 SDCN 模型的反向传播和参数更新,使得模型能够不断地优化自身的参数 #清空优化器的梯度信息,执行反向传播计算参数的梯度,并使用优化器更新模型参数。 optimizer.zero_grad() #清空优化器 optimizer 中的梯度信息。 loss.backward() #对损失函数 loss 进行反向传播计算参数的梯度。 # 反向传播通过链式法则求得每个参数对于损失函数的梯度值,然后将其保存在各自的 Tensor 中。这些梯度值即为模型优化所需的方向信号。 optimizer.step()#使用优化器 optimizer 更新模型参数,以降低损失函数的值并提高模型性能。 #在反向传播之后,优化器使用预定义的优化算法(如 SGD、Adam 等)根据计算出来的梯度信息来更新模型的参数。 if __name__ == "__main__": parser = argparse.ArgumentParser( description='train', #设置该命令行程序的简短描述。 formatter_class=argparse.ArgumentDefaultsHelpFormatter) #设置命令行帮助信息的格式,默认为 argparse.HelpFormatter,可以使得输出的帮助信息中包含每个参数的默认值。 #使用了 Python 内置的 argparse 模块,创建了一个 ArgumentParser 对象 parser,用于解析命令行参数。 parser.add_argument('--name', type=str, default='cite') #cite parser.add_argument('--k', type=int, default=3) parser.add_argument('--lr', type=float, default=1e-3) parser.add_argument('--n_clusters', default=3, type=int) parser.add_argument('--n_z', default=10, type=int) parser.add_argument('--pretrain_path', type=str, default='pkl') #这段代码定义了六个命令行参数,分别是 name、k、lr、n_clusters、n_z 和 pretrain_path,并为每个参数指定了默认值和其他属性。在命令行中可以根据需要覆盖这些默认值。 args = parser.parse_args() #通过调用parser.parse_args()方法,从命令行获取这些参数的值,并将结果存储在args对象中。 args.cuda = torch.cuda.is_available() print("use cuda: {}".format(args.cuda)) device = torch.device("cuda" if args.cuda else "cpu") args.pretrain_path = 'data/{}.pkl'.format(args.name) dataset = load_data(args.name) #脚本根据获取到的args.name参数值加载相应的数据集,并根据数据集类型设置相应的其他超参数。最后,脚本调用train_sdcn()函数训练聚类模型。 if args.name == 'usps': args.n_clusters = 10 args.n_input = 256 if args.name == 'hhar': args.k = 5 args.n_clusters = 6 args.n_input = 561 if args.name == 'reut': args.lr = 1e-4 args.n_clusters = 4 args.n_input = 2000 if args.name == 'acm': args.k = None args.n_clusters = 3 args.n_input = 1870 if args.name == 'dblp': args.k = None args.n_clusters = 4 args.n_input = 334 if args.name == 'cite': args.lr = 1e-3 args.k = None args.n_clusters = 6 args.n_input = 3703 if args.name == 'abstract': args.lr = 1e-4 args.k = None args.n_clusters = 3 args.n_input = 10000 if args.name == 'bbc': args.lr = 1e-4 args.k = None args.n_clusters = 4 args.n_input = 9635 print(args) train_sdcn(dataset) #该代码定义了一个主函数,首先通过argparse库设置了命令行参数,包括name,k,lr,n_clusters,n_z等参数, # 然后加载数据集,然后加载数据集,并根据数据集的不同调整参数的值,最后调用train_sdcn()函数对数据集进行训练。 # 其中,train_sdcn()函数的作用是对数据集进行聚类,使用SDCN模型进行特征学习和聚类,并输出评估结果。