✨ 本笔记整理自我研读国防科技大学吕欣老师团队编写的经典教材《数据挖掘》后的学习体会。吕老师的书不仅体系完整、逻辑清晰,而且兼顾了理论深度与实践案例,让人读来有一种“豁然开朗”的感觉。尤其是在图数据与图神经网络的章节里,语言简练而不失精准,案例翔实而又易于复现。
作为学习者,我深切感受到:一本好教材不仅是知识的载体,更是科研道路上的指路明灯。吕老师将抽象的理论与可操作的代码桥接起来,让人不仅“知其然”,更能“知其所以然”。以下内容就是我在阅读后结合代码实践与思考整理的笔记与感悟。


一、图数据结构的建立

学习图神经网络(Graph Neural Networks, GNN)的第一步,就是学会如何从现实问题中构建图数据结构。

在《数据挖掘》书中强调:

图数据本质上是对象与关系的有机结合。
节点代表实体,边则刻画它们之间的联系。只有当我们把对象的属性信息与结构信息结合起来,才能发挥图学习的优势。

在经典的 Cora 数据集 中,每个节点代表一篇论文,边表示引用关系,节点特征为 1433 维的词袋模型向量,标签共 7 类(如神经网络、遗传算法、强化学习等)。学习目标是:根据论文的内容和引用关系,为每篇论文打上正确的研究方向标签。

核心步骤:

  1. 读取节点特征与标签(cora.content);

特征向量是一个1433维的二进制词袋模型(word bag model),每一维都对应了一个关键词,1表示该词出现在论文中,0表示没有出现。 在Cora数据集中,有两个文件,分别是cora.cities和cora.content,其中cora.cities为节点ID之间的对应关系(即,引用关系),cora.content则为节点ID、节点特征向量(1433个特征)和节点标签(7类中的一类)。

2.读取引用关系,建立邻接矩阵(cora.cites);

输入cora.cities和cora.content,输出图的邻接矩阵:

#读取cora数据,函数传入数据目录data _path_
def load cora_data (data path) :
       print ( "load_cora_data ...")

​       #使用np.genfromtxt,读取cora.content,得到content数组,并将content数组拆分为节点ID,idx,节点特征向量features,节点标签labels
​       content = np.genfromtxt(data _path + '/cora.content ' , dtype=np.dtype(str))
​       idx = content[ : , 0].astype(np.int32)#节点的ID
​       features = content [ : , 1:-1].astype (np.float32)#节点的特征向量
​       labels = encode_labels(content[: , -1])#节点的标签,从字符串类型,转换为整型索引

​       #使用node_num保存节点的数量,打印会看到有2708个节点
​       node num = len (idx)
​       print(f"node_num: {node_num}")
#读取cora.cites,建立邻接矩阵
cites = np.genfromtxt (data path + ' /cora.cites ' , dtype=np.int32)
#将每个节点的ID映射到一个连续的整数索引上
idx_map = {j: i for i, j in enumerate (idx) }
print(f"idx_map: {idx_map } ")
edges = [ (idx_map[i], idx_map[j]) for i, j in cites]#保存边的列表
edges = np.array (edges, dtype=np .int32)
print(f"edge_num: { len (edges) } "')
#建立cora数据集对应的无向图adj
adj = build_symmetric_adj (edges, node_num)
print (f"adj : {adj.shape } ")

3.特征与邻接矩阵归一化,并转为张量。

这样,一个图数据结构就建立完成了。这里最让我印象深刻的是:在图数据中,信息的价值往往来自邻居。单看一篇论文的词向量,也许很难分类,但加入它与邻居的关系,模型就能学习到更丰富的语义信息。


二、图卷积神经网络(GCN)的原理

这本书里对图卷积(Graph Convolution)的解释极其简洁优美,可以概括为两个核心操作:

  1. 线性变换:对节点自身的特征做加权变换,提取高层特征;

  2. 邻居聚合:通过邻接矩阵传播和融合邻居信息。

在图卷积中,包含了一个线性层

我们会使用该线性层对节点特征x,进行线性变换,也就是对x进行特征提取。图卷积中的可训练参数,都在这个线性层中。

例如:

img

x是一个3×2的矩阵。如果线性变换后的特征数是4;那么就会使用32的矩阵x,乘以24大小的权重矩阵w,得到3*4大小的输出结果。

img

具体会将邻接矩阵adj和特征向量x进行矩阵相乘。

这个步骤会,通过图传导每个节点中的特征信息。

例如:

img

特征向量x,是31大小的,代表了3个节点;每个节点有1个特征,特征值分别是1、2、3;邻接矩阵adj是33大小的,对应了3个节点的无向图。

我们先来关注节点0的特征聚合:

img

节点0与节点0本身、节点1相连,和节点2不相连;所以adj的第0行是1、1、0。将它和x相乘,就会得到节点0的相邻特征聚合结果,为3。这里的11,表示聚合节点0本身的特征值1;12表示聚合节点1的特征值2;0*3表示不聚合节点2的特征值。因此最后的结果就是1+2=3。

同理,计算节点1的特征聚合:

img

因为节点1和0、1、2三个节点都相连,所以1+2+3=6。

计算节点2的特征聚合:

img

是2+3=5,表示将节点1和节点2的特征值,进行聚合。

直观感受就是:
每一层图卷积,都会让节点“看见”更远一跳的邻居,从而在局部邻域中不断融合信息。最终,节点的表征不再是孤立的向量,而是一个充分吸收了邻居上下文的高维表示。

这种“自然而然的扩散”机制,恰好契合了现实网络中的传播规律,比如学术引用、社交网络信息扩散,甚至是生物分子间的相互作用。


三、代码实践与实验结果

基于 PyTorch,我们实现了一个两层 GCN,并在 Cora 数据集上进行实验:

  • 训练集:15%

  • 测试集:85%

  • 优化器:Adam

  • 损失函数:交叉熵

import torch
from torch import nn
from torch.nn.parameter import Parameter83import math

#图卷积的代码实现
class GraphConvolution (nn. Module) :
def _init_(self, feature_num,hidden_size):88
super (GraphConvolution, self) .___init___()

#定义图卷积中的线性层权重w和偏置b
self.w = Parameter (torch.FloatTensor(feature_num,hidden_size))
self.b = Parameter(torch.FloatTensor(hidden_size) )
#对w和b进行初始化
stdv = 1. / math.sqrt(self.w.size (1))
self.w.data.uniform_(-stdv , stdv)
self.b.data.uniform_(-stdv , stdv)

#传入特征向量x和邻接矩阵adj
def forward(self, x, adj) :99
x = torch.mm(x, self.w)#计算x和w的矩阵相乘,进行线性变换
#计算稀疏矩阵adj和x的矩阵相乘
output = torch.spmm(adj, x)#进行相邻特征聚合
return output + self.b #返回output+偏置b

#实现图卷积神经网络105日class GCN (nn . Module) :
super(GCN, self) .__init__()
self.gc1 = GraphConvolution(input_size, hidden_size)#图卷积
self.relu = nn.ReLU ()#激活函数
self.drop = nn . Dropout (dropout) # dropout层
self.gc2 = GraphConvolution(hidden_size,output_size)#图卷积

#传入特征向量x和邻接矩阵adj,计算前向传播
def forward (self, x, adj) :
×=self.gc1(x, adj)
x= self.relu(×)
x=self.drop (×)
x= self.gc2(×, adj)
return x

最后实现图卷积神经网络的训练和测试:

from torch import optim
if_name_== '__main_':
#定义当前设备
device = torch.device("cuda" if torch.cuda.is_available()else "cpu")
print(f"device = {device}")
features, labels, adj = load cora_ data(' ./data/')
print (f" features: { features . shape}")
print(f"labels: { labels. shape}")
print(f"adj: {adj .shape}")
​
features = features. to (device)
labels = labels. to (device) 
adj = adj . to (device) 
assert len (features) == len (labels)
assert len (features) = = len (adj)
sample_ num = features. shape [0]
train_ num = int (sample_ num * 0.15)
#使用剩下的,2302个数据,进行测试
test_ num = sample_ num - train num .
print(f"train_ num: {train_ num}")
print(f"test_ num: {test_ num}")
#后面会看到,基于图卷积神经网络,可以通过少量的训练数据,得到较好的测试效果

首先,定义当前设备device。使用load_cora_data读取数据,得到:1)样本的特征向量features2)样本的标签labels3)邻接矩阵adj.这里打印调试信息

然后我们使用15%,406个数据,进行训练。使用剩下的,2302个数据,进行测试。

feature num = features. shape[1] #输入特征数
hiddensize=16#隐藏层特征数
class_ num = labels .max() .item() + 1 #样本标签数量
dropout = 0.5 # dropout率
print(f"feature num: { feature_ num}")
print(f"hidden_ size: {hidden size}")
print(f"class num: {class num} ")
#创建GCN模型
model = GCN (feature_ num,hidden_ size, class_ num, dropout) . to (device) 
model.train() #调整为训练模式
​
optimizer = optim. Adam (model .parameters()) # Adam优化器
criterion = nn.CrossEntropyLoss() #交叉熵损失函数

获取输入特征数feature_num;定义隐藏层特征数为16;样本标签数class_num;dropout率设置为0.5。创建GCN模型model,将模型调整为训练模式。然后定义Adam优化器optimizer和交叉熵损失函数criterion。

接着进入3000轮的循环迭代:

n_ epoch = 3000
for epoch in range(1, n_ epoch + 1) : # 3000轮的迭代循环
opt. imizer.zero_ grad() #将梯度清零
outputs = model (features, adj )
​
#在计算损失时,只计算训练集节点的损失
loss = criterion (outputs[:train_ num] ,labels[:train_ num])
loss. backward() #计算梯度
optimizer.step() #梯度下降
if epoch % 100 == 0:
print(f'Epoch {epoch}/ {n_ epoch}, Loss: {loss.item() : .3f}')

首先将梯度清零,然后进行前向传播。这里要说明的是,图卷积神经网络的前向传播有些特殊。在图卷积网络的前向传播阶段,需要计算图中所有节点的特征表示。也就是将features和adj,全部传入到model,计算出output。这是因为,每个节点的特征更新,不仅依赖于其自身的特征,还依赖于其邻居节点的特征。也就是,那些不在训练集中的节点的特征,会影响到训练集中的节点的特征表示。我们要将图中的节点和它的特征,看做是一个整体来理解和处理。这是分析和处理图数据的基本原则。接着,进行损失计算。在计算损失时,只计算训练集节点的损失,也就是只使用train_num个样本计算损失。最后使用backward计算梯度,使用optimizer.step计算梯度下降。

完成训练后,将模型调整为测试模式:

model.eval() #将模型调整为测试模式
outputs = model (features, adj) #基于全量特征计算结果
predicted = torch.argmax (outputs [train_ num:], dim=1) #测试样本的预测结果
correct = (predicted == labels[train_ num:]) .sum() . item()
accuracy = 100*correct / test_ num #计算正确率
print(f 'Accuracy: {correct}/ {test_ num}={accuracy: .1f}8')

基于全量特征计算结果outputs,然后将测试样本的预测结果保存到predict。最后计算正确率accuracy

经过 3000 轮训练,测试集准确率达到了 81.4%。这充分说明了 GCN 在图数据中的强大表现力。值得一提的是,哪怕只使用很少量的训练样本,模型也能在大规模节点上取得很好的泛化效果,这就是 图结构带来的“监督信号放大效应”


四、学习感悟

读完吕欣老师的《数据挖掘》后,我最大的感受是:

  • 理论与实践的统一:书中既有数理推导,又有工程实现,真正做到了“知其所以然”。

  • 逻辑的清晰性:每一个知识点都环环相扣,从数据结构、算法原理到实验复现,形成了完整的链条。

  • 科研的启发性:通过对比传统机器学习与图学习,深刻理解到图神经网络的独特优势。

如果说数据挖掘是“从数据中发现价值”,那么图数据挖掘则是“让关系赋予价值”。这是吕老师书中给我留下的最大启发。


五、扩展与思考

在学习过程中,我还思考了几个问题:

  1. 为什么要加自环?
    在邻接矩阵中给每个节点加自环,是为了让节点在更新时保留自己的特征信息,而不是完全被邻居淹没。

  2. 为什么少量标注就够?
    这是因为图卷积能够通过邻接关系,将少量监督信号传导到整个网络中。换句话说,标签的“影响力”会通过边不断扩散。

  3. 未来的改进方向
    除了经典的 GCN,还有 GAT(图注意力网络)、GraphSAGE、GIN 等更强大的模型,它们通过注意力机制、采样或更强的表达能力,进一步提升了对复杂网络的建模能力。


六、总结

通过研读这本《数据挖掘》,我不仅掌握了图数据和图神经网络的核心方法,还收获了宝贵的科研思维方式。可以说,这本书是我进入图挖掘与深度学习交叉领域的一扇大门。

作者:爱吃火锅同学、陈同学

Logo

惟楚有才,于斯为盛。欢迎来到长沙!!! 茶颜悦色、臭豆腐、CSDN和你一个都不能少~

更多推荐