用Python和PyTorch一步步实现SDCN:一个融合自编码器与图卷积的文本聚类实战

张开发
2026/4/13 15:15:39 15 分钟阅读

分享文章

用Python和PyTorch一步步实现SDCN:一个融合自编码器与图卷积的文本聚类实战
用Python和PyTorch一步步实现SDCN一个融合自编码器与图卷积的文本聚类实战在自然语言处理领域文本聚类一直是个既基础又充满挑战的任务。传统方法往往难以捕捉文本间复杂的语义关系而深度学习的出现为这一领域带来了新的可能性。今天我们要实现的SDCNStructural Deep Clustering Network模型巧妙地将自编码器AE与图卷积网络GCN的优势结合起来在文本聚类任务上展现出了令人惊艳的效果。不同于大多数教程停留在理论层面本文将带您从零开始用PyTorch完整实现SDCN模型。我们会从环境配置开始一步步完成数据预处理、模型构建、训练循环编写直到最终评估指标的计算与可视化。无论您是希望复现论文结果的研究者还是想深入理解图神经网络应用的开发者这个实战指南都将为您提供清晰的实现路径。1. 环境准备与数据加载在开始编码之前我们需要确保所有必要的工具和库都已就位。PyTorch作为我们的主要框架需要特别注意版本兼容性问题。# 基础环境配置 import torch import torch.nn as nn import torch.nn.functional as F import numpy as np from sklearn import metrics from sklearn.preprocessing import normalize import matplotlib.pyplot as plt print(fPyTorch版本: {torch.__version__})对于文本数据我们需要先将其转化为适合模型处理的格式。这里我们使用20 Newsgroups数据集作为示例这是一个经典的文本分类数据集也适合用于聚类任务。from sklearn.datasets import fetch_20newsgroups from sklearn.feature_extraction.text import TfidfVectorizer # 加载数据集 newsgroups fetch_20newsgroups(subsetall, remove(headers, footers, quotes)) texts newsgroups.data labels newsgroups.target # 使用TF-IDF向量化文本 vectorizer TfidfVectorizer(max_features2000) X vectorizer.fit_transform(texts).toarray() X normalize(X) # 归一化处理 print(f数据集形状: {X.shape}, 类别数: {len(set(labels))})关键点说明TF-IDF向量化时max_features参数控制了特征维度这对后续模型性能有重要影响数据归一化是必要的预处理步骤可以防止某些特征主导整个模型在实际应用中可能需要尝试不同的文本表示方法如Word2Vec、BERT等2. 构建文本图结构SDCN的核心创新之一就是将图结构引入文本聚类任务。我们需要为文本数据构建合适的图结构这是GCN能够发挥作用的基础。from sklearn.neighbors import NearestNeighbors def build_graph_adjacency(data, k10): 构建k近邻图邻接矩阵 nbrs NearestNeighbors(n_neighborsk).fit(data) distances, indices nbrs.kneighbors(data) # 构建对称邻接矩阵 n_samples data.shape[0] adj np.zeros((n_samples, n_samples)) for i in range(n_samples): for j_idx, j in enumerate(indices[i]): if i ! j: # 排除自连接 adj[i][j] 1 adj[j][i] 1 # 确保对称 # 添加自连接 adj adj np.eye(n_samples) # 归一化邻接矩阵 D np.diag(np.power(np.sum(adj, axis1), -0.5)) adj_normalized D.dot(adj).dot(D) return adj_normalized # 构建图邻接矩阵 adj build_graph_adjacency(X) adj torch.FloatTensor(adj) X torch.FloatTensor(X)邻接矩阵构建技巧k值的选择会影响图的结构通常需要根据数据规模和特性进行调整归一化邻接矩阵有助于稳定GCN的训练过程在实际应用中可能需要尝试不同的图构建方法如全连接、阈值连接等3. 自编码器AE模块实现自编码器是SDCN的第一个关键组件负责学习文本的低维表示。我们先实现一个多层感知机MLP作为编码器和解码器的基础结构。class MLPLayer(nn.Module): 基础MLP层 def __init__(self, input_dim, output_dim, activationNone): super(MLPLayer, self).__init__() self.linear nn.Linear(input_dim, output_dim) self.activation activation def forward(self, x): x self.linear(x) if self.activation is not None: x self.activation(x) return x class Autoencoder(nn.Module): 自编码器实现 def __init__(self, input_dim, hidden_dims[500, 500, 2000, 10]): super(Autoencoder, self).__init__() # 编码器层 self.encoder_layers nn.ModuleList() prev_dim input_dim for dim in hidden_dims: self.encoder_layers.append(MLPLayer(prev_dim, dim, activationnn.ReLU())) prev_dim dim # 解码器层对称结构 self.decoder_layers nn.ModuleList() hidden_dims_reversed hidden_dims[:-1][::-1] [input_dim] prev_dim hidden_dims[-1] for dim in hidden_dims_reversed: self.decoder_layers.append(MLPLayer(prev_dim, dim, activationnn.ReLU() if dim ! input_dim else None)) prev_dim dim def forward(self, x): # 编码过程 encoder_features [] for layer in self.encoder_layers: x layer(x) encoder_features.append(x) # 解码过程 for layer in self.decoder_layers: x layer(x) # 返回重建结果和各层编码特征 return x, encoder_features[1], encoder_features[2], encoder_features[3], encoder_features[-1]预训练自编码器def pretrain_autoencoder(model, X, epochs300, batch_size256, lr0.001): optimizer torch.optim.Adam(model.parameters(), lrlr) criterion nn.MSELoss() for epoch in range(epochs): total_loss 0 permutation torch.randperm(X.size()[0]) for i in range(0, X.size()[0], batch_size): batch_indices permutation[i:ibatch_size] batch_x X[batch_indices] optimizer.zero_grad() reconstructed, _, _, _, _ model(batch_x) loss criterion(reconstructed, batch_x) loss.backward() optimizer.step() total_loss loss.item() if (epoch1) % 50 0: print(fEpoch {epoch1}, Loss: {total_loss/X.size()[0]:.4f}) return model # 实例化并预训练自编码器 input_dim X.shape[1] ae Autoencoder(input_dim) ae pretrain_autoencoder(ae, X)4. 图卷积网络GCN模块实现GCN模块是SDCN的另一核心组件负责利用图结构信息增强聚类性能。我们实现一个基本的图卷积层然后构建完整的GCN模块。class GraphConvolution(nn.Module): 图卷积层实现 def __init__(self, input_dim, output_dim, activationNone): super(GraphConvolution, self).__init__() self.linear nn.Linear(input_dim, output_dim) self.activation activation def forward(self, x, adj, activeTrue): x torch.matmul(adj, x) # 邻接矩阵传播 x self.linear(x) if active and self.activation is not None: x self.activation(x) return x class GCNModule(nn.Module): GCN模块实现 def __init__(self, input_dim, hidden_dims[500, 500, 2000, 10]): super(GCNModule, self).__init__() self.gnn_layers nn.ModuleList() prev_dim input_dim for dim in hidden_dims: self.gnn_layers.append(GraphConvolution(prev_dim, dim, activationnn.ReLU())) prev_dim dim def forward(self, x, adj): for layer in self.gnn_layers[:-1]: x layer(x, adj) # 最后一层不使用激活函数 x self.gnn_layers[-1](x, adj, activeFalse) return x5. 完整SDCN模型集成现在我们将AE和GCN模块整合到一起构建完整的SDCN模型并实现其特有的双重自监督机制。class SDCN(nn.Module): 完整的SDCN模型 def __init__(self, ae, gcn, n_clusters, alpha1.0): super(SDCN, self).__init__() self.ae ae self.gcn gcn self.n_clusters n_clusters self.alpha alpha self.v 1.0 # 学生t分布的自由度 # 聚类层 hidden_dim self.ae.encoder_layers[-1].linear.out_features self.cluster_layer nn.Parameter(torch.Tensor(n_clusters, hidden_dim)) nn.init.xavier_normal_(self.cluster_layer) def forward(self, x, adj): # AE模块 x_bar, tra1, tra2, tra3, z self.ae(x) # GCN模块 sigma 0.5 # 融合系数 h self.gcn.gnn_layers[0](x, adj) h self.gcn.gnn_layers[1]((1-sigma)*h sigma*tra1, adj) h self.gcn.gnn_layers[2]((1-sigma)*h sigma*tra2, adj) h self.gcn.gnn_layers[3]((1-sigma)*h sigma*tra3, adj) h self.gcn.gnn_layers[4]((1-sigma)*h sigma*z, adj, activeFalse) # 预测分布 predict F.softmax(h, dim1) # 双重自监督 q 1.0 / (1.0 torch.sum(torch.pow(z.unsqueeze(1) - self.cluster_layer, 2), 2) / self.v) q q.pow((self.v 1.0) / 2.0) q (q.t() / torch.sum(q, 1)).t() return x_bar, q, predict, z6. 模型训练与评估SDCN的训练过程相对复杂需要协调自编码器的重建损失、聚类损失和图结构损失。我们实现一个完整的训练循环并包含评估指标计算。def train_sdcn(model, X, adj, labels, epochs100, lr0.001, gamma0.1, update_interval5): optimizer torch.optim.Adam(model.parameters(), lrlr) criterion_mse nn.MSELoss() # 评估函数 def evaluate(true_labels, pred_labels): acc metrics.accuracy_score(true_labels, pred_labels) nmi metrics.normalized_mutual_info_score(true_labels, pred_labels) ari metrics.adjusted_rand_score(true_labels, pred_labels) return acc, nmi, ari # 初始化目标分布 with torch.no_grad(): _, q, _, _ model(X, adj) best_acc 0 best_nmi 0 history {acc: [], nmi: [], ari: [], loss: []} for epoch in range(epochs): # 训练步骤 model.train() optimizer.zero_grad() x_bar, q_pred, predict, z model(X, adj) # 计算损失 loss_mse criterion_mse(x_bar, X) loss_kl F.kl_div(q_pred.log(), q, reductionbatchmean) loss loss_mse gamma * loss_kl loss.backward() optimizer.step() # 更新目标分布 if epoch % update_interval 0: with torch.no_grad(): _, q_new, _, _ model(X, adj) q q_new # 评估步骤 model.eval() with torch.no_grad(): _, _, predict, _ model(X, adj) pred_labels torch.argmax(predict, dim1).cpu().numpy() acc, nmi, ari evaluate(labels, pred_labels) history[acc].append(acc) history[nmi].append(nmi) history[ari].append(ari) history[loss].append(loss.item()) if acc best_acc: best_acc acc if nmi best_nmi: best_nmi nmi print(fEpoch {epoch1}: Loss{loss.item():.4f}, ACC{acc:.4f}, NMI{nmi:.4f}, ARI{ari:.4f}) return model, history, best_acc, best_nmi # 实例化并训练SDCN模型 n_clusters len(set(labels)) gcn GCNModule(input_dim) sdcn SDCN(ae, gcn, n_clusters) sdcn, history, best_acc, best_nmi train_sdcn(sdcn, X, adj, labels)训练技巧update_interval控制目标分布更新的频率影响训练稳定性gamma参数平衡重建损失和聚类损失的重要性训练初期可以设置较大的学习率后期逐渐减小7. 结果可视化与分析训练完成后我们需要对结果进行可视化分析这有助于理解模型的表现和潜在问题。def plot_results(history): plt.figure(figsize(15, 5)) # 绘制损失曲线 plt.subplot(1, 3, 1) plt.plot(history[loss]) plt.title(Training Loss) plt.xlabel(Epoch) plt.ylabel(Loss) # 绘制ACC和NMI曲线 plt.subplot(1, 3, 2) plt.plot(history[acc], labelACC) plt.plot(history[nmi], labelNMI) plt.title(Clustering Metrics) plt.xlabel(Epoch) plt.legend() # 绘制ARI曲线 plt.subplot(1, 3, 3) plt.plot(history[ari]) plt.title(ARI Score) plt.xlabel(Epoch) plt.ylabel(ARI) plt.tight_layout() plt.show() # 可视化训练过程 plot_results(history) # 获取最终聚类结果 with torch.no_grad(): _, _, predict, z sdcn(X, adj) pred_labels torch.argmax(predict, dim1).cpu().numpy() # 可视化潜在空间 def plot_latent_space(z, labels, pred_labels): z z.cpu().numpy() true_labels labels pred_labels pred_labels plt.figure(figsize(12, 6)) # 真实标签分布 plt.subplot(1, 2, 1) plt.scatter(z[:, 0], z[:, 1], ctrue_labels, cmaptab20, s5) plt.title(True Labels in Latent Space) plt.colorbar() # 预测聚类分布 plt.subplot(1, 2, 2) plt.scatter(z[:, 0], z[:, 1], cpred_labels, cmaptab20, s5) plt.title(Predicted Clusters in Latent Space) plt.colorbar() plt.tight_layout() plt.show() plot_latent_space(z, labels, pred_labels)常见问题排查如果ACC/NMI指标波动很大可能是学习率过高或目标分布更新太频繁潜在空间可视化中如果类别分离不明显可能需要调整AE的预训练过程指标持续较低可能表明需要重新设计图结构或调整模型架构8. 模型优化与调参建议在实际应用中SDCN模型有几个关键参数需要仔细调整以获得最佳性能。以下是经过实践验证的调参建议关键参数优化表参数推荐范围影响调整策略自编码器隐藏层[500,500,2000,10]影响特征提取能力根据数据复杂度调整最后一层通常设为聚类数图邻接矩阵k值5-20控制图连接的稀疏程度大数据集用较小k小数据集可适当增大融合系数sigma0.3-0.7平衡AE和GCN的贡献从0.5开始根据任务调整学习率1e-4到1e-3影响训练稳定性配合学习率衰减策略使用gamma0.1-1.0平衡重建和聚类损失从0.1开始逐步增加性能优化技巧使用学习率预热策略可以改善训练初期的稳定性尝试不同的图构建方法如余弦相似度代替欧氏距离在大型数据集上可以先用K-means初始化聚类中心考虑加入层归一化或残差连接来稳定深层网络训练# 学习率预热示例 def warmup_lr_scheduler(optimizer, warmup_iters, base_lr, warmup_factor0.1): def f(x): if x warmup_iters: return 1 alpha float(x) / warmup_iters return warmup_factor * (1 - alpha) alpha return torch.optim.lr_scheduler.LambdaLR(optimizer, f) # 使用示例 optimizer torch.optim.Adam(model.parameters(), lr0.001) scheduler warmup_lr_scheduler(optimizer, warmup_iters10, base_lr0.001)9. 扩展应用与进阶方向虽然我们在本文中主要关注文本聚类任务但SDCN的架构思想可以扩展到许多其他领域。以下是几个值得探索的进阶方向多模态数据聚类将文本与图像、音频等其他模态数据结合为不同模态设计特定的自编码器在潜在空间进行跨模态图构建class MultimodalSDCN(nn.Module): 多模态SDCN扩展 def __init__(self, text_ae, image_ae, gcn, n_clusters): super().__init__() self.text_ae text_ae self.image_ae image_ae self.gcn gcn self.n_clusters n_clusters # 融合层 text_dim text_ae.encoder_layers[-1].linear.out_features image_dim image_ae.encoder_layers[-1].linear.out_features self.fusion_layer nn.Linear(text_dim image_dim, text_dim) # 聚类层 self.cluster_layer nn.Parameter(torch.Tensor(n_clusters, text_dim)) nn.init.xavier_normal_(self.cluster_layer)动态图学习不依赖预定义的静态图结构在训练过程中联合学习最优图连接自适应调整邻接矩阵权重class DynamicGraphLearner(nn.Module): 动态图学习模块 def __init__(self, input_dim): super().__init__() self.projection nn.Linear(input_dim, input_dim) self.temperature nn.Parameter(torch.tensor(1.0)) def forward(self, z): # 计算节点间相似度 z_proj self.projection(z) sim_matrix torch.matmul(z_proj, z_proj.t()) # 应用温度调节的softmax adj F.softmax(sim_matrix / self.temperature, dim1) return adj实际部署建议使用ONNX格式导出模型以便跨平台部署对大型数据集实现批处理图卷积开发基于Flask或FastAPI的简易API服务# 模型导出示例 dummy_input torch.randn(1, X.shape[1]) torch.onnx.export(model, (dummy_input, adj[:1, :1]), sdcn_model.onnx, input_names[input, adj], output_names[output])

更多文章