【图表示学习】pytorch实现图注意力网络GAT

本文的实现只是对论文的原始代码进行了简单的修改、简化和说明,原始代码点这里

一、实现前的一些说明

在实现前,先对代码中比较难理解的部分进行说明。

1. 任意两节点的 W h ⃗ \textbf{W}\vec{h} Wh 的拼接

计算注意力系数的公式如下:
α i j = e x p ( L e a k y R e L U ( a ⃗ T [ W h ⃗ i ∣ ∣ W h ⃗ j ] ) ) ∑ k ∈ N i e x p ( L e a k y R e L U ( a ⃗ T [ W h ⃗ i ∣ ∣ W h ⃗ k ] ) ) \alpha_{ij}=\frac{exp(LeakyReLU(\vec{\textbf{a}}^T[\textbf{W}\vec{h}_i||\textbf{W}\vec{h}_j]))}{\sum_{k\in\mathcal{N}_i}exp(LeakyReLU(\vec{\textbf{a}}^T[\textbf{W}\vec{h}_i||\textbf{W}\vec{h}_k]))} αij=kNiexp(LeakyReLU(a T[Wh iWh k]))exp(LeakyReLU(a T[Wh iWh j]))
可以看到在计算时,需要对任意两个节点进行拼接操作, W h ⃗ i ∣ ∣ W h ⃗ j \textbf{W}\vec{h}_i||\textbf{W}\vec{h}_j Wh iWh j

这里假设有3个节点,每个节点的特征向量维度为5,即假设 h ⃗ i ∈ R 5 , 1 ≤ i ≤ 3 \vec{h}_i\in\mathbb{R}^5,1\leq i\leq3 h iR5,1i3,且令 H = [ h ⃗ 1 , … , h ⃗ 3 ] ∈ R 5 × 3 \textbf{H}=[\vec{h}_1,\dots,\vec{h}_{3}]\in\mathbb{R}^{5\times3} H=[h 1,,h 3]R5×3, W ∈ R 5 × 5 \textbf{W}\in\mathbb{R}^{5\times 5} WR5×5

则任意两个节点进行拼接操作相当于对矩阵 ( WH ) T ∈ R 3 × 5 (\textbf{WH})^T\in\mathbb{R}^{3\times 5} (WH)TR3×5的行向量进行两两拼接,这里先定义 ( WH ) T (\textbf{WH})^T (WH)T并展示如何进行两两拼接。

WH = torch.arange(0,3).repeat(5,1).T # 3个节点
# 两种不同的repeat方式
Wh_repeated_in_chunks = WH.repeat_interleave(3, dim=0)
Wh_repeated_alternating = WH.repeat(3,1)
# Wh两两拼接
all_combinations_matrix = torch.cat([Wh_repeated_in_chunks, Wh_repeated_alternating], dim=1)
result = all_combinations_matrix.view(3, 3, 2 * 5)
print(result)

输出:

tensor([[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
         [0, 0, 0, 0, 0, 1, 1, 1, 1, 1],
         [0, 0, 0, 0, 0, 2, 2, 2, 2, 2]],

        [[1, 1, 1, 1, 1, 0, 0, 0, 0, 0],
         [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
         [1, 1, 1, 1, 1, 2, 2, 2, 2, 2]],

        [[2, 2, 2, 2, 2, 0, 0, 0, 0, 0],
         [2, 2, 2, 2, 2, 1, 1, 1, 1, 1],
         [2, 2, 2, 2, 2, 2, 2, 2, 2, 2]]])

2.如何聚合相邻节点

h i ′ ⃗ = σ ( ∑ j ∈ N i α i j W h j ⃗ ) \vec{h_i'}=\sigma\Big(\sum_{j\in\mathcal{N}_i}\alpha_{ij}\textbf{W}\vec{h_j}\Big) hi =σ(jNiαijWhj )

这里假设是3个节点,其两两对应的注意力系数组成的矩阵为 A ∈ R 3 × 3 A\in\mathbb{R}^{3\times 3} AR3×3,其中 A i , j A_{i,j} Ai,j表示节点 i i i j j j的注意力系数。邻居矩阵则同样是一个 3 × 3 3\times 3 3×3的矩阵。

Wh = torch.randn(3,5) # 3个节点,每个节点5个特征
A = torch.randn(3,3) # 注意力系数矩阵
# 邻接矩阵
adj = torch.tensor([[0,1,1],
                    [1,0,0],
                    [1,0,0]])
zero_vec = -9e15*torch.ones_like(A)
# 使用adj作为掩码,将没有边连接的点对的注意力系数置为0
attention = torch.where(adj>0, A, zero_vec)
attention = F.softmax(attention, dim=1)
# h_prime.shape=(3,5),得到了每个节点的聚合新特征
h_prime = torch.matmul(attention, Wh)
print(h_prime)

输出:

tensor([[-1.0747, -0.0669,  0.9770,  0.0211, -1.6639],
        [-0.0577, -0.1590, -0.0546,  2.2421, -2.0541],
        [-0.0577, -0.1590, -0.0546,  2.2421, -2.0541]])

3.节点特征向量的标准化

假设有3个节点,每个节点的特征向量维度为5,即假设 h ⃗ i ∈ R 5 , 1 ≤ i ≤ 3 \vec{h}_i\in\mathbb{R}^5,1\leq i\leq3 h iR5,1i3,且令 H = [ h ⃗ 1 , … , h ⃗ 3 ] T ∈ R 3 × 5 \textbf{H}=[\vec{h}_1,\dots,\vec{h}_{3}]^T\in\mathbb{R}^{3\times5} H=[h 1,,h 3]TR3×5。对于单个节点的标准化就是 h ⃗ 1 s u m ( h ⃗ 1 ) \frac{\vec{h}_1}{sum(\vec{h}_1)} sum(h 1)h 1,那么以矩阵运算的方法进行标准化。

H = torch.ones(3,5)
# 特征求和
rowsum = np.array(H.sum(1))
# 倒数
r_inv = np.power(rowsum,-1).flatten()
# 解决除0问题
r_inv[np.isinf(r_inv)] = 0.
# 转换为对角阵
r_mat_inv = np.diag(r_inv)
# 对角阵乘以H,得到标准化矩阵
H = r_mat_inv.dot(H)
print(H)

输出:

[[0.2 0.2 0.2 0.2 0.2]
 [0.2 0.2 0.2 0.2 0.2]
 [0.2 0.2 0.2 0.2 0.2]]

4.邻接矩阵的标准化

假设有4个节点,邻居矩阵 A ∈ R 4 × 4 A\in\mathbb{R}^{4\times 4} AR4×4,那么邻接矩阵标准化

A = np.ones((4,4))
rowsum = A.sum(1)
r_inv_sqrt = np.power(rowsum, -0.5).flatten()
r_inv_sqrt[np.isinf(r_inv_sqrt)] = 0.
r_mat_inv_sqrt = np.diag(r_inv_sqrt)
A = A.dot(r_mat_inv_sqrt).transpose().dot(r_mat_inv_sqrt)
print(A)

输出:

[[0.25 0.25 0.25 0.25]
 [0.25 0.25 0.25 0.25]
 [0.25 0.25 0.25 0.25]
 [0.25 0.25 0.25 0.25]]

二、完整实现

1.数据加载和预处理

def load_data(path="./data/cora/", dataset="cora"):
    """Load citation network dataset (cora only for now)"""
    print('Loading {} dataset...'.format(dataset))
    
    # idx_features_labels.shape = (2708,1435);
    # 第一列是节点编号,最后一列是节点类别,中间列是节点的特征
    idx_features_labels = np.genfromtxt("{}{}.content".format(path, dataset), dtype=np.dtype(str))
    # 提取特征并按行压缩为稀疏矩阵
    features = sp.csr_matrix(idx_features_labels[:, 1:-1], dtype=np.float32)
    # 将标签转换为one-hot编码
    labels = pd.get_dummies(idx_features_labels[:,-1]).values

    # build graph
    # 节点编号
    idx = np.array(idx_features_labels[:, 0], dtype=np.int32)
    # (节点编号:出现顺序)
    idx_map = {j: i for i, j in enumerate(idx)}
    # 边表,shape = (5429,2)
    edges_unordered = np.genfromtxt("{}{}.cites".format(path, dataset), dtype=np.int32)
    # 使用idx_map映射edges_unordered中节点的编号
    edges = np.array(list(map(idx_map.get, edges_unordered.flatten())), dtype=np.int32).reshape(edges_unordered.shape)
    # 将edges转换为邻接矩阵
    adj = sp.coo_matrix((np.ones(edges.shape[0]), (edges[:, 0], edges[:, 1])), shape=(labels.shape[0], labels.shape[0]), dtype=np.float32)

    # 转换为对称矩阵
    adj = adj + adj.T.multiply(adj.T > adj) - adj.multiply(adj.T > adj)

    features = normalize_features(features)
    adj = normalize_adj(adj + sp.eye(adj.shape[0]))

    idx_train = range(140)
    idx_val = range(200, 500)
    idx_test = range(500, 1500)

    adj = torch.FloatTensor(np.array(adj.todense()))
    features = torch.FloatTensor(np.array(features.todense()))
    labels = torch.LongTensor(np.where(labels)[1])

    idx_train = torch.LongTensor(idx_train)
    idx_val = torch.LongTensor(idx_val)
    idx_test = torch.LongTensor(idx_test)

    return adj, features, labels, idx_train, idx_val, idx_test


def normalize_adj(mx):
    """Row-normalize sparse matrix"""
    rowsum = np.array(mx.sum(1))
    r_inv_sqrt = np.power(rowsum, -0.5).flatten()
    r_inv_sqrt[np.isinf(r_inv_sqrt)] = 0.
    r_mat_inv_sqrt = sp.diags(r_inv_sqrt)
    return mx.dot(r_mat_inv_sqrt).transpose().dot(r_mat_inv_sqrt)


def normalize_features(mx):
    """Row-normalize sparse matrix"""
    rowsum = np.array(mx.sum(1))
    r_inv = np.power(rowsum, -1).flatten()
    r_inv[np.isinf(r_inv)] = 0.
    r_mat_inv = sp.diags(r_inv)
    mx = r_mat_inv.dot(mx)
    return mx

adj, features, labels, idx_train, idx_val, idx_test = load_data()
print(adj.shape)
print(features.shape)
print(labels.shape)

输出:

torch.Size([2708, 2708])
torch.Size([2708, 1433])
torch.Size([2708])

2.定义模型结构

2.1 定义图注意力层(Graph Attention Layer)

class GraphAttentionLayer(nn.Module):
    def __init__(self, in_features, out_features, dropout, alpha, concat=True):
        super(GraphAttentionLayer, self).__init__()
        self.dropout = dropout
        self.in_features = in_features
        self.out_features = out_features
        self.alpha = alpha
        self.concat = concat
        
        self.W = nn.Parameter(torch.empty(size=(in_features, out_features)))
        nn.init.xavier_uniform_(self.W.data, gain=1.414)
        self.a = nn.Parameter(torch.empty(size=(2*out_features, 1)))
        nn.init.xavier_uniform_(self.a.data, gain=1.414)
        self.leakyrelu = nn.LeakyReLU(self.alpha)
        
    def forward(self, h, adj):
        Wh = torch.mm(h, self.W) # h.shape: (节点数N, 输入节点的特征维度in_features), Wh.shape: (N, out_features)
        a_input = self._prepare_attentional_mechanism_input(Wh) # (N, N, 2 * out_features)
        e = self.leakyrelu(torch.matmul(a_input, self.a).squeeze(2))
        zero_vec = -9e15*torch.ones_like(e)
        # mask注意力系数
        attention = torch.where(adj > 0, e, zero_vec)
        attention = F.softmax(attention, dim=1)
        attention = F.dropout(attention, self.dropout, training=self.training)
        # 注意力系数加权求和
        h_prime = torch.matmul(attention, Wh)

        if self.concat:
            return F.elu(h_prime)
        else:
            return h_prime
        
    def _prepare_attentional_mechanism_input(self, Wh):
        N = Wh.size()[0] # 节点数N
        Wh_repeated_in_chunks = Wh.repeat_interleave(N, dim=0) #(N*N, out_features)
        Wh_repeated_alternating = Wh.repeat(N, 1) #(N*N, out_features)
        all_combinations_matrix = torch.cat([Wh_repeated_in_chunks, Wh_repeated_alternating], dim=1)
        # all_combinations_matrix.shape == (N * N, 2 * out_features)
        return all_combinations_matrix.view(N, N, 2 * self.out_features)
    
    def __repr__(self):
        return self.__class__.__name__ + ' (' + str(self.in_features) + ' -> ' + str(self.out_features) + ')'

2.2 定义图注意力网络(GAT)

class GAT(nn.Module):
    def __init__(self, nfeat, nhid, nclass, dropout, alpha, nheads):
        """Dense version of GAT."""
        super(GAT, self).__init__()
        self.dropout = dropout
        # 多个图注意力层
        self.attentions = [GraphAttentionLayer(nfeat, nhid, dropout=dropout, alpha=alpha, concat=True) for _ in range(nheads)]
        for i, attention in enumerate(self.attentions):
            self.add_module('attention_{}'.format(i), attention)
        # 输出层
        self.out_att = GraphAttentionLayer(nhid * nheads, nclass, dropout=dropout, alpha=alpha, concat=False)

    def forward(self, x, adj):
        x = F.dropout(x, self.dropout, training=self.training)
        x = torch.cat([att(x, adj) for att in self.attentions], dim=1)
        x = F.dropout(x, self.dropout, training=self.training)
        x = F.elu(self.out_att(x, adj))
        return F.log_softmax(x, dim=1)

3.模型训练

3.1参数

hidden = 8 
dropout = 0.6
nb_heads = 8 
alpha = 0.2
lr = 0.005
weight_decay = 5e-4
epochs = 10000
patience = 100
cuda = torch.cuda.is_available()

3.2 实例化模型和优化器

# 实例化模型 
model = GAT(nfeat=features.shape[1], 
            nhid=hidden, 
            nclass=int(labels.max()) + 1, 
            dropout=dropout, 
            nheads=nb_heads, 
            alpha=alpha)
# 优化器
optimizer = optim.Adam(model.parameters(), 
                       lr=lr, 
                       weight_decay=weight_decay)
if cuda:
    model.cuda()
    features = features.cuda()
    adj = adj.cuda()
    labels = labels.cuda()
    idx_train = idx_train.cuda()
    idx_val = idx_val.cuda()
    idx_test = idx_test.cuda()

features, adj, labels = Variable(features), Variable(adj), Variable(labels)

3.3 训练

def train(epoch):
    t = time.time()
    # trian
    model.train()
    optimizer.zero_grad()
    output = model(features, adj)
    loss_train = F.nll_loss(output[idx_train], labels[idx_train])
    acc_train = accuracy(output[idx_train], labels[idx_train])
    loss_train.backward()
    optimizer.step()
        
    # eval
    model.eval()
    output = model(features, adj)
    loss_val = F.nll_loss(output[idx_val], labels[idx_val])
    acc_val = accuracy(output[idx_val], labels[idx_val])
    print('Epoch: {:04d}'.format(epoch+1),
          'loss_train: {:.4f}'.format(loss_train.data.item()),
          'acc_train: {:.4f}'.format(acc_train.data.item()),
          'loss_val: {:.4f}'.format(loss_val.data.item()),
          'acc_val: {:.4f}'.format(acc_val.data.item()),
          'time: {:.4f}s'.format(time.time() - t))

    return loss_val.data.item()


def compute_test():
    model.eval()
    output = model(features, adj)
    loss_test = F.nll_loss(output[idx_test], labels[idx_test])
    acc_test = accuracy(output[idx_test], labels[idx_test])
    print("Test set results:",
          "loss= {:.4f}".format(loss_test.item()),
          "accuracy= {:.4f}".format(acc_test.item()))
    
def accuracy(output, labels):
    preds = output.max(1)[1].type_as(labels)
    correct = preds.eq(labels).double()
    correct = correct.sum()
    return correct / len(labels)
t_total = time.time()
loss_values = []
bad_counter = 0
best = epochs + 1
best_epoch = 0
for epoch in range(epochs):
    # 训练模型并保存loss
    loss_values.append(train(epoch))
    # 保存模型
    torch.save(model.state_dict(), '{}.pkl'.format(epoch))
    # 记录loss最小的epoch
    if loss_values[-1] < best:
        best = loss_values[-1]
        best_epoch = epoch
        bad_counter = 0
    else:
        bad_counter += 1
    
    # 如果连续patience个epoch,最小Loss都没有变则终止模型训练
    if bad_counter == patience:
        break
        
    # 删除不是最优的模型
    files = glob.glob('*.pkl')
    for file in files:
        epoch_nb = int(file.split('.')[0])
        if epoch_nb < best_epoch:
            os.remove(file)

files = glob.glob('*.pkl')
for file in files:
    epoch_nb = int(file.split('.')[0])
    if epoch_nb > best_epoch:
        os.remove(file)
        
print("Optimization Finished!")
print("Total time elapsed: {:.4f}s".format(time.time() - t_total))

# 加载最优模型
print('Loading {}th epoch'.format(best_epoch))
model.load_state_dict(torch.load('{}.pkl'.format(best_epoch)))

compute_test()

输出:

Epoch: 0001 loss_train: 1.9447 acc_train: 0.1643 loss_val: 1.9360 acc_val: 0.3867 time: 1.8232s
Epoch: 0002 loss_train: 1.9351 acc_train: 0.2500 loss_val: 1.9255 acc_val: 0.5100 time: 1.2726s
Epoch: 0003 loss_train: 1.9181 acc_train: 0.3286 loss_val: 1.9152 acc_val: 0.5200 time: 1.2716s
Epoch: 0004 loss_train: 1.9063 acc_train: 0.3857 loss_val: 1.9049 acc_val: 0.5033 time: 1.3065s
Epoch: 0005 loss_train: 1.8864 acc_train: 0.4929 loss_val: 1.8943 acc_val: 0.5000 time: 1.2736s
...
Epoch: 0755 loss_train: 0.5768 acc_train: 0.8357 loss_val: 0.6613 acc_val: 0.8133 time: 1.3564s
Epoch: 0756 loss_train: 0.5965 acc_train: 0.8143 loss_val: 0.6615 acc_val: 0.8133 time: 1.4003s
Epoch: 0757 loss_train: 0.5388 acc_train: 0.8357 loss_val: 0.6619 acc_val: 0.8133 time: 1.4023s
Optimization Finished!
Total time elapsed: 1054.6370s
Loading 656th epoch
Test set results: loss= 0.6534 accuracy= 0.8460
  • 16
    点赞
  • 72
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BQW_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值