Kiến trúc mạng học sâu: từ nền tảng đến LLM·Chương 8 — GNN: từ message passing đến Graph Transformer

Chế độ xem

Thực hành GNN với PyTorch Geometric: từ message passing đến đánh giá và debug

Một đồ thị gợi ý phim có nút là người dùng và phim, cạnh là lượt xem. Nút là một thực thể. Cạnh là một mối quan hệ giữa hai nút. Bạn muốn dự đoán cạnh nào sẽ xuất hiện tháng sau. Nếu chia ngẫu nhiên mọi lượt xem từ tháng 1 đến tháng 8 vào cả tập train và tập test, mô hình được học trên những lượt xem xảy ra sau thời điểm cần dự đoán. Đó là rò rỉ theo thời gian: thông tin từ tương lai lọt vào lúc huấn luyện. Câu hỏi của bài là cách đọc một đồ thị trong PyTorch Geometric, huấn luyện một MLP không dùng cạnh rồi một GCN, và phát hiện lỗi chia dữ liệu kiểu này. MLP là mạng chỉ biến đổi vector của từng nút. GCN là lớp trộn mỗi nút với láng giềng, có cân theo bậc. Bậc là số cạnh chạm vào nút. Bài không chứng minh lại công thức GCN và không giới thiệu Graph Transformer.

Đường đọc nhanh: mục 21 nếu rò rỉ đến từ thời gian, mục 19 nếu cạnh cần dự đoán vẫn nằm trong đồ thị lúc truyền tin, rồi mục 2, 4, 6, 7 và 10 để có một pipeline trên Cora. Mục 30 là checklist debug. Sampling, OGB và bài tập nằm ngoài đường này.

1. Công cụ: PyTorch Geometric hiện đại

PyTorch Geometric (PyG) là thư viện GNN trên PyTorch. Tài liệu hiện hành năm 2026 đang ở dòng 2.9.x, hỗ trợ nhiều operator, mini-batch loader, heterogeneous graph và các tiện ích cho graph learning.

Trên Colab/laptop có PyTorch phù hợp, bắt đầu bằng:

pip install -U torch-geometric

Nếu dùng extension tăng tốc hoặc neighbor sampling ở môi trường đặc biệt, cần kiểm tra hướng dẫn cài đặt đúng phiên bản PyTorch/CUDA trong tài liệu PyG thay vì copy lệnh cũ từ blog nhiều năm trước.

Kiểm tra:

import torch
import torch_geometric

print("PyTorch:", torch.__version__)
print("PyG:", torch_geometric.__version__)

2. Cora: dataset nhỏ nhưng tốt để học cơ chế

Cora là citation network:

  • node: paper;
  • edge: citation;
  • node feature: bag-of-words của paper;
  • label: chủ đề paper.

Load bằng Planetoid:

from torch_geometric.datasets import Planetoid


dataset = Planetoid(root="data/Planetoid", name="Cora")
data = dataset[0]

print(dataset)
print(data)

Kiểm tra các tensor quan trọng:

print("x:", data.x.shape)
print("edge_index:", data.edge_index.shape)
print("y:", data.y.shape)
print("train nodes:", int(data.train_mask.sum()))
print("val nodes:", int(data.val_mask.sum()))
print("test nodes:", int(data.test_mask.sum()))
print("classes:", dataset.num_classes)
print("features:", dataset.num_features)

2.1 Đọc shape trước khi đọc model

Một graph node-classification điển hình:

x          : [num_nodes, num_features]
edge_index : [2, num_edges]
y          : [num_nodes]
mask       : [num_nodes] boolean

Model cuối cùng trả:

logits      : [num_nodes, num_classes]

Loss chỉ lấy các hàng thuộc train_mask.

3. edge_index thực sự nghĩa là gì?

Trong PyG, với flow mặc định source_to_target:

src = data.edge_index[0]
dst = data.edge_index[1]

mỗi cột biểu diễn:

src[k]  --->  dst[k]

Lấy 10 edge đầu:

for s, t in data.edge_index[:, :10].t().tolist():
    print(f"{s} -> {t}")

Đây là sparse representation. Ta không cần adjacency matrix kích thước N×NN\times N.

4. Thực hành cơ chế: tự aggregate neighbor bằng PyTorch

Trước khi dùng GCNConv, hãy tự gộp láng giềng. Một thông điệp là gói số đi dọc một cạnh, ở đây chính là feature của nút nguồn. Gộp, aggregate, là cộng các gói đến cùng một nút rồi chia cho số gói, để ra trung bình.

import torch


def mean_aggregate(x, edge_index):
    src, dst = edge_index

    # Message của edge src -> dst là feature của source node.
    messages = x[src]

    # Cộng message vào destination.
    out = torch.zeros_like(x)
    out.index_add_(0, dst, messages)

    # Số incoming edges của mỗi destination node.
    deg = torch.bincount(dst, minlength=x.size(0)).to(x.dtype)
    deg = deg.clamp(min=1).unsqueeze(-1)

    return out / deg


neighbor_mean = mean_aggregate(data.x, data.edge_index)
print(neighbor_mean.shape)

4.1 Đoạn code này tương ứng với phần nào của message passing?

x[src]                  -> CREATE MESSAGE
index_add_(dst, ...)    -> AGGREGATE bằng SUM
/ degree                -> chuyển SUM thành MEAN

Chưa có learnable parameter và chưa update self-feature. Nhưng đây là lõi cơ học của neighborhood aggregation.

5. Tự viết một GCN propagation layer tối giản

Lớp dưới đây thực hiện đúng phép trộn đã chuẩn hóa: nhân feature với trọng số, thêm self-loop, rồi cân theo bậc. Self-loop là một cạnh từ nút tới chính nó. Công thức của phép đó là:

D~−1/2A~D~−1/2XW\tilde{D}^{-1/2}\tilde{A}\tilde{D}^{-1/2}XW

trong code.

import torch
from torch import nn
from torch_geometric.utils import add_self_loops, degree


class SimpleGCNLayer(nn.Module):
    def __init__(self, in_channels, out_channels):
        super().__init__()
        self.linear = nn.Linear(in_channels, out_channels, bias=False)

    def forward(self, x, edge_index):
        num_nodes = x.size(0)

        # 1) XW
        x = self.linear(x)

        # 2) A + I
        edge_index, _ = add_self_loops(
            edge_index,
            num_nodes=num_nodes,
        )

        src, dst = edge_index

        # 3) Degree sau khi có self-loop.
        deg = degree(dst, num_nodes=num_nodes, dtype=x.dtype)
        deg_inv_sqrt = deg.pow(-0.5)
        deg_inv_sqrt[torch.isinf(deg_inv_sqrt)] = 0

        # 4) 1 / sqrt(d_src * d_dst)
        norm = deg_inv_sqrt[src] * deg_inv_sqrt[dst]

        # 5) Message + aggregate
        messages = x[src] * norm.unsqueeze(-1)
        out = torch.zeros_like(x)
        out.index_add_(0, dst, messages)

        return out

Với graph có hướng, định nghĩa degree/normalization cần được xem xét theo semantics và flow. Đoạn trên dùng cho minh họa GCN trên graph kiểu Cora, thường được xử lý đối xứng.

So sánh shape với operator chuẩn:

layer = SimpleGCNLayer(dataset.num_features, 32)
out = layer(data.x, data.edge_index)
print(out.shape)

6. Baseline bắt buộc: MLP không dùng edge

Nếu không có MLP baseline, ta không biết GNN cải thiện nhờ đồ thị hay chỉ nhờ sức chứa của một mạng neural. MLP không nhận danh sách cạnh.

import torch.nn.functional as F


class MLP(torch.nn.Module):
    def __init__(self, in_channels, hidden_channels, out_channels):
        super().__init__()
        self.lin1 = torch.nn.Linear(in_channels, hidden_channels)
        self.lin2 = torch.nn.Linear(hidden_channels, out_channels)

    def forward(self, x):
        x = self.lin1(x)
        x = F.relu(x)
        x = F.dropout(x, p=0.5, training=self.training)
        return self.lin2(x)

MLP không nhận edge_index:

logits = model(data.x)

Trong khi GCN sẽ nhận:

logits = model(data.x, data.edge_index)

Đây là ablation quan trọng nhất của chương.

7. GCN chuẩn bằng GCNConv

from torch_geometric.nn import GCNConv


class GCN(torch.nn.Module):
    def __init__(self, in_channels, hidden_channels, out_channels):
        super().__init__()
        self.conv1 = GCNConv(in_channels, hidden_channels)
        self.conv2 = GCNConv(hidden_channels, out_channels)

    def forward(self, x, edge_index):
        x = self.conv1(x, edge_index)
        x = F.relu(x)
        x = F.dropout(x, p=0.5, training=self.training)
        x = self.conv2(x, edge_index)
        return x

7.1 Đếm tham số

def count_params(model):
    return sum(p.numel() for p in model.parameters() if p.requires_grad)

Parameter count hữu ích để so model tương đối công bằng. Đừng so GCN hidden 16 với GAT hidden 256 rồi kết luận attention tốt hơn.

8. Một training loop đủ sạch

import copy
import torch
from torch_geometric import seed_everything


def train_node_model(
    model,
    data,
    lr=0.01,
    weight_decay=5e-4,
    max_epochs=300,
    patience=50,
):
    optimizer = torch.optim.Adam(
        model.parameters(),
        lr=lr,
        weight_decay=weight_decay,
    )

    best_state = None
    best_val_loss = float("inf")
    bad_epochs = 0

    for epoch in range(1, max_epochs + 1):
        model.train()
        optimizer.zero_grad()

        if isinstance(model, MLP):
            logits = model(data.x)
        else:
            logits = model(data.x, data.edge_index)

        loss = F.cross_entropy(
            logits[data.train_mask],
            data.y[data.train_mask],
        )
        loss.backward()
        optimizer.step()

        model.eval()
        with torch.no_grad():
            if isinstance(model, MLP):
                logits = model(data.x)
            else:
                logits = model(data.x, data.edge_index)

            val_loss = F.cross_entropy(
                logits[data.val_mask],
                data.y[data.val_mask],
            ).item()

        if val_loss < best_val_loss:
            best_val_loss = val_loss
            best_state = copy.deepcopy(model.state_dict())
            bad_epochs = 0
        else:
            bad_epochs += 1

        if bad_epochs >= patience:
            break

    model.load_state_dict(best_state)
    return model

8.1 Vì sao early stopping dùng validation chứ không test?

Nếu nhìn test metric mỗi epoch để chọn epoch tốt nhất, test set đã tham gia model selection. Khi đó test không còn là estimate độc lập.

Test set chỉ nên dùng sau khi pipeline/hyperparameter đã chốt.

9. Đánh giá: Accuracy chưa đủ

from sklearn.metrics import accuracy_score, f1_score, classification_report


def predict(model, data):
    model.eval()
    with torch.no_grad():
        if isinstance(model, MLP):
            logits = model(data.x)
        else:
            logits = model(data.x, data.edge_index)
    return logits.argmax(dim=-1)


def evaluate(model, data, mask):
    pred = predict(model, data)
    y_true = data.y[mask].cpu().numpy()
    y_pred = pred[mask].cpu().numpy()

    return {
        "accuracy": accuracy_score(y_true, y_pred),
        "macro_f1": f1_score(y_true, y_pred, average="macro"),
        "micro_f1": f1_score(y_true, y_pred, average="micro"),
    }

9.1 Accuracy

Accuracy là tỷ lệ dự đoán đúng trên toàn bộ các nút được chấm:

Accuracy=correcttotal\mathrm{Accuracy}=\frac{\text{correct}}{\text{total}}

9.2 Macro-F1

Macro-F1 tính F1 từng lớp rồi lấy trung bình, nên lớp đông không át lớp ít:

F1macro=1C∑c=1CF1cF1_{macro}=\frac1C\sum_{c=1}^C F1_c

Macro-F1 giảm ảnh hưởng của class lớn áp đảo metric.

9.3 Với class imbalance mạnh

Nên xem thêm:

print(classification_report(y_true, y_pred, digits=4))

và confusion matrix.

10. Chạy MLP và GCN trên cùng protocol

seed_everything(42)

mlp = MLP(
    dataset.num_features,
    hidden_channels=32,
    out_channels=dataset.num_classes,
)
mlp = train_node_model(mlp, data)

seed_everything(42)

gcn = GCN(
    dataset.num_features,
    hidden_channels=32,
    out_channels=dataset.num_classes,
)
gcn = train_node_model(gcn, data)

print("MLP:", evaluate(mlp, data, data.test_mask))
print("GCN:", evaluate(gcn, data, data.test_mask))
print("MLP params:", count_params(mlp))
print("GCN params:", count_params(gcn))

Không nên quan tâm một con số cụ thể trong giáo trình vì version, split và seed có thể làm kết quả thay đổi. Điều cần quan sát:

GCN có tận dụng connectivity để cải thiện so với feature-only baseline không?

11. Nhiều seed: một run không phải kết luận

import numpy as np


def run_seed(seed, model_cls):
    seed_everything(seed)

    model = model_cls(
        dataset.num_features,
        32,
        dataset.num_classes,
    )
    model = train_node_model(model, data)
    return evaluate(model, data, data.test_mask)


seeds = [0, 1, 2, 3, 4]
results = [run_seed(s, GCN) for s in seeds]

macro_f1 = np.array([r["macro_f1"] for r in results])
print("Macro-F1 mean:", macro_f1.mean())
print("Macro-F1 std :", macro_f1.std(ddof=1))

Với benchmark nghiêm túc, nên theo protocol chính thức. OGB leaderboard, chẳng hạn, yêu cầu báo cáo mean và standard deviation qua nhiều random seeds theo quy định của benchmark.

12. Thay GCN bằng GraphSAGE

from torch_geometric.nn import SAGEConv


class GraphSAGE(torch.nn.Module):
    def __init__(self, in_channels, hidden_channels, out_channels):
        super().__init__()
        self.conv1 = SAGEConv(in_channels, hidden_channels)
        self.conv2 = SAGEConv(hidden_channels, out_channels)

    def forward(self, x, edge_index):
        x = self.conv1(x, edge_index)
        x = F.relu(x)
        x = F.dropout(x, p=0.5, training=self.training)
        return self.conv2(x, edge_index)

Trên Cora nhỏ, ta vẫn có thể full-batch. Giá trị thật của GraphSAGE rõ hơn khi dùng neighbor sampling trên graph lớn.

13. Thay bằng GAT

from torch_geometric.nn import GATConv


class GAT(torch.nn.Module):
    def __init__(
        self,
        in_channels,
        hidden_channels,
        out_channels,
        heads=4,
    ):
        super().__init__()

        self.conv1 = GATConv(
            in_channels,
            hidden_channels,
            heads=heads,
            dropout=0.5,
        )

        self.conv2 = GATConv(
            hidden_channels * heads,
            out_channels,
            heads=1,
            concat=False,
            dropout=0.5,
        )

    def forward(self, x, edge_index):
        x = F.dropout(x, p=0.5, training=self.training)
        x = self.conv1(x, edge_index)
        x = F.elu(x)
        x = F.dropout(x, p=0.5, training=self.training)
        return self.conv2(x, edge_index)

13.1 So sánh công bằng

GAT hidden_channels=32, heads=4 tạo hidden width hiệu dụng 128 nếu concat. Parameter count có thể lớn hơn GCN đáng kể.

Do đó báo cáo ít nhất:

ModelParamsTest AccuracyMacro-F1Mean ± std
MLP…………
GCN…………
SAGE…………
GAT…………

14. Ablation 1 — graph có thật sự hữu ích không?

Feature-only

MLP đã làm việc này.

Structure corruption

Một diagnostic đơn giản là phá quan hệ edge trong khi giữ feature/label.

Ví dụ ý tưởng:

edge_index_bad = data.edge_index.clone()
perm = torch.randperm(edge_index_bad.size(1))
edge_index_bad[1] = edge_index_bad[1, perm]

Cách này không phải degree-preserving rewiring hoàn hảo và có thể tạo self-loop/duplicate; nó chỉ minh họa tư duy ablation.

Nếu cần nghiên cứu nghiêm túc, dùng graph randomization/rewiring có kiểm soát.

Câu hỏi cần trả lời:

Nếu phá topology mà kết quả không giảm, model có đang thật sự dùng structure không?

15. Ablation 2 — feature hay topology là nguồn signal chính?

Thay node feature bằng constant:

x_constant = torch.ones(
    data.num_nodes,
    1,
    dtype=data.x.dtype,
)

Sau đó train GNN phù hợp input dimension mới.

Nếu model vẫn phân loại khá tốt, topology có signal mạnh.

Nếu performance sụp, node feature là thành phần chính.

16. Quan sát degree distribution

from torch_geometric.utils import degree

src, dst = data.edge_index
in_degree = degree(dst, num_nodes=data.num_nodes)

print("mean degree:", in_degree.mean().item())
print("median degree:", in_degree.median().item())
print("max degree:", in_degree.max().item())

Graph có heavy-tail degree distribution có thể gây:

  • hub domination;
  • neighborhood explosion;
  • mini-batch imbalance;
  • performance khác nhau giữa low/high-degree nodes.

Một phân tích tốt có thể chia test node theo degree bucket rồi báo cáo metric.

17. Neighbor sampling cho graph lớn

Một hop là một bước đi dọc đúng một cạnh. Đồ thị lớn không nhét nguyên vào bộ nhớ. NeighborLoader lấy mẫu láng giềng theo từng hop.

from torch_geometric.loader import NeighborLoader


train_loader = NeighborLoader(
    data,
    input_nodes=data.train_mask,
    num_neighbors=[15, 10],
    batch_size=128,
    shuffle=True,
)

batch = next(iter(train_loader))
print(batch)
print("target batch size:", batch.batch_size)
print("global ids:", batch.n_id[:10])

Ý nghĩa:

  • lấy 128 target nodes;
  • sample tối đa 15 neighbors ở hop đầu;
  • tiếp tục tối đa 10 ở hop sau.

17.1 Một lỗi rất phổ biến

Số num_neighbors phải phù hợp số layer/hop mà model cần. Nếu model có 3 message-passing layers nhưng loader chỉ sample 2 hop, layer ngoài cùng không có đầy đủ dependency mong muốn.

Tài liệu PyG cũng lưu ý directed/subgraph behavior khi số GNN layer khác số sampling hop.

18. Graph-level mini-batching khác node sampling

Nếu dataset gồm nhiều graph nhỏ như molecule, DataLoader thường ghép graph theo block-diagonal structure.

Ví dụ:

from torch_geometric.datasets import TUDataset
from torch_geometric.loader import DataLoader


dataset_g = TUDataset(root="data/TU", name="MUTAG")
loader = DataLoader(dataset_g, batch_size=32, shuffle=True)

batch = next(iter(loader))
print(batch)
print(batch.batch.shape)

batch.batch[i] cho biết node ii thuộc graph nào.

Graph readout:

from torch_geometric.nn import global_mean_pool


graph_embedding = global_mean_pool(node_embedding, batch.batch)

Đừng nhầm:

  • NeighborLoader: sample neighborhood trong một graph lớn;
  • DataLoader: batch nhiều graph.

Data leakage là thông tin của phần cần chấm điểm lọt vào lúc huấn luyện. Khác với rò rỉ thời gian ở đầu bài, lỗi dưới đây nằm ngay trên cạnh: nếu cạnh (u,v)(u,v) cần được dự đoán vẫn nằm trong đồ thị dùng để truyền tin, mô hình có thể thấy chính đáp án. Message passing là ba bước gửi gói, gộp gói, rồi viết lại vector nút. Gói đi trên cạnh nào thì tin của cạnh đó đi vào nút.

PyG có RandomLinkSplit:

from torch_geometric.transforms import RandomLinkSplit


split = RandomLinkSplit(
    num_val=0.1,
    num_test=0.2,
    is_undirected=True,
    neg_sampling_ratio=1.0,
)

train_data, val_data, test_data = split(data)

Đọc kỹ:

  • edge_index: edge dùng cho message passing;
  • edge_label_index: cặp node cần chấm điểm;
  • edge_label: positive/negative label.

19.1 Heterogeneous graph còn nguy hiểm hơn

Nếu có reverse relation:

user --buys--> item
item --rev_buys--> user

split một chiều nhưng quên split reverse edge có thể leak. RandomLinkSplit hỗ trợ edge_types và rev_edge_types để tránh lỗi này.

20. Negative sampling không phải chi tiết nhỏ

Link prediction thường có rất nhiều non-edge.

Nếu sample negative quá dễ, metric sẽ cao giả tạo.

Cần quyết định:

  • uniform negatives;
  • hard negatives;
  • time-respecting negatives;
  • filtered negatives trong knowledge graph.

Metric cũng tùy task:

  • ROC-AUC;
  • Average Precision;
  • Hits@K;
  • MRR.

Không dùng Accuracy mặc định cho mọi link-prediction task.

21. Split theo thời gian thay vì random khi deployment là tương lai

Ví dụ phim ở đầu bài viết thành lịch: train chỉ được thấy quá khứ của thời điểm cần dự đoán.

Train: Jan-Jun
Val  : Jul
Test : Aug

Nếu random split toàn bộ event từ Jan-Aug, train có thể thấy tương tác xảy ra sau test event.

Đây là temporal leakage.

Split phải mô phỏng cách model được dùng ngoài đời, không chỉ thuận tiện cho code.

22. Các lỗi graph preprocessing điển hình

22.1 Undirected graph nhưng chỉ lưu một chiều

Nếu quan hệ vô hướng mà edge_index chỉ có u -> v, message không tự đi v -> u.

Dùng:

from torch_geometric.utils import to_undirected

edge_index = to_undirected(edge_index)

khi semantics thật sự là undirected.

22.2 Thêm self-loop hai lần

Một số conv layer tự xử lý self-loop. Nếu preprocessing đã thêm và layer tiếp tục thêm, cần biết thư viện có coalesce/normalize thế nào.

22.3 Duplicate edges

Duplicate có thể khiến message bị tính nhiều lần.

from torch_geometric.utils import coalesce

edge_index = coalesce(edge_index)

nếu muốn loại duplicate theo semantics phù hợp.

22.4 Isolated nodes

Node degree 0 không nhận neighbor message. Cần self-loop, self-feature path hoặc xử lý riêng.

22.5 Sai chiều edge_index

PyG mặc định coi hàng 0 là source, hàng 1 là target. Đảo hai hàng trên directed graph có thể đổi hoàn toàn bài toán.

23. Debug bằng shape và gradient

In shape ở mỗi layer:

class DebugGCN(torch.nn.Module):
    def __init__(self, in_channels, hidden_channels, out_channels):
        super().__init__()
        self.conv1 = GCNConv(in_channels, hidden_channels)
        self.conv2 = GCNConv(hidden_channels, out_channels)

    def forward(self, x, edge_index):
        print("input:", x.shape)
        x = self.conv1(x, edge_index)
        print("after conv1:", x.shape)
        x = F.relu(x)
        x = self.conv2(x, edge_index)
        print("after conv2:", x.shape)
        return x

Kiểm tra gradient:

for name, p in model.named_parameters():
    if p.grad is not None:
        print(name, p.grad.norm().item())

Nếu gradient bằng 0/NaN, vấn đề có thể không liên quan graph mà nằm ở training pipeline.

24. Debug over-smoothing bằng embedding similarity

Over-smoothing là các vector nút trở nên quá giống nhau khi trộn láng giềng nhiều lần. Over-squashing là tin từ xa bị nén vào một vector có số chiều cố định trước khi tới nút đích. Homophily là láng giềng thường cùng nhãn hoặc cùng kiểu feature. Một chẩn đoán đơn giản cho over-smoothing: cosine similarity trung bình giữa các cặp nút ngẫu nhiên sau mỗi lớp.

Hàm dưới đây ước lượng độ giống trung bình đó trên các cặp lấy ngẫu nhiên.

import torch.nn.functional as F


def pair_cosine_mean(h, num_pairs=5000):
    n = h.size(0)
    i = torch.randint(0, n, (num_pairs,), device=h.device)
    j = torch.randint(0, n, (num_pairs,), device=h.device)
    return F.cosine_similarity(h[i], h[j], dim=-1).mean().item()

Đây chỉ là diagnostic, không phải metric chính thức duy nhất cho over-smoothing.

25. Đánh giá theo degree bucket

Model có thể tốt với hub nodes nhưng kém node degree thấp.

Ví dụ chia:

low    : degree <= 2
medium : 3..10
high   : > 10

rồi báo cáo Macro-F1 từng nhóm.

Điều này đặc biệt quan trọng khi deployment có nhiều cold-start node.

26. GNN experiment report tối thiểu nên lưu gì?

Mỗi run nên lưu:

seed
model
hidden_dim
num_layers
num_params
learning_rate
weight_decay
dropout
epoch_best
split_id
accuracy
macro_f1
runtime

Nếu dùng sampling:

batch_size
num_neighbors per hop
sampler settings

Nếu dùng graph construction:

edge construction rule
k / threshold
symmetrization
self-loop policy

Không ghi graph construction là thiếu một phần quan trọng của phương pháp.

27. OGB: bước tiếp theo sau Cora

Cora rất tốt để học, nhưng quá nhỏ để đại diện graph learning hiện đại.

Open Graph Benchmark (OGB) cung cấp:

  • realistic datasets;
  • official splits;
  • evaluator thống nhất;
  • node/link/graph tasks;
  • large-scale datasets.

Ví dụ hướng phát triển:

TaskDataset ví dụMetric thường dùng
node propertyogbn-arxivAccuracy
link propertyogbl-collabHits@K
graph propertyogbg-molhivROC-AUC

Khi chuyển sang OGB, ưu tiên official split + official evaluator thay vì tự random split để tiện.

28. Một pipeline nghiên cứu GNN có kiểm soát

Đây là quy trình đáng tin hơn:

GAT không đủ -> thử GATv2 -> thử 8 head -> thử Transformer -> ...

mà không biết bottleneck nằm đâu.

29. Bài tập thực hành theo mức

Mức A — bắt buộc

  1. Load Cora.
  2. In x, edge_index, y, masks.
  3. Train MLP và GCN với cùng hidden size.
  4. Báo cáo Accuracy và Macro-F1.
  5. Giải thích vì sao GCN khác MLP chỉ bằng 3 câu.

Mức B — khá

  1. Thêm GraphSAGE và GAT.
  2. Chạy 5 seeds.
  3. Báo cáo mean ± std.
  4. Đếm parameter.
  5. Chọn model dựa trên performance và complexity.

Mức C — giỏi

Làm 3 ablation:

  1. feature-only MLP;
  2. GCN graph thật;
  3. GCN graph đã rewired/shuffled có kiểm soát.

Viết kết luận: performance đến từ node feature, topology hay tương tác của cả hai?

Mức C+ — nghiên cứu

Chọn một OGB dataset và thiết kế protocol:

  • official split;
  • 10 seeds hoặc theo rule benchmark;
  • baseline ladder;
  • runtime/parameter count;
  • error analysis;
  • ablation.

30. Checklist debug nhanh

Triệu chứngKiểm tra đầu tiên
Loss không giảmlabel/mask, learning rate, gradient
GCN = MLPedge signal, homophily, graph construction
Train cao, val thấpoverfit, split, regularization
Deep GNN kémover-smoothing/squashing
Link prediction quá caoedge leakage, negative sampling
Directed task kémcó vô tình to_undirected không?
Hetero graph kémrelation type có bị bỏ mất không?
Node mới kémtransductive model/ID embedding?
OOMfull-batch, neighbor explosion, sampling
Kết quả dao độngseed, split, unstable optimization

Những ý mang đi

Nếu bạn không đọc bài khác, hãy nhớ lỗi trước khi nhớ tên lớp. Chia ngẫu nhiên một đồ thị có thời gian cho tương lai lọt vào train. Để cạnh cần dự đoán nằm trong đồ thị lúc truyền tin cũng là xem đáp án. Pipeline tối thiểu là cùng một split cho MLP không dùng cạnh và cho GCN, rồi báo Accuracy cùng Macro-F1 trên nhiều seed. Checklist ở mục 30 là nơi quay lại khi loss không giảm, khi GCN không hơn MLP, hoặc khi link prediction cao một cách đáng ngờ.

Bài sau nói vì sao GAT không phải Graph Transformer, và mã hóa nào đưa cấu trúc đồ thị vào attention.

31. Tài liệu tham khảo và API nên dùng

  1. PyTorch Geometric 2.9 documentation: https://pytorch-geometric.readthedocs.io/
  2. GCNConv, SAGEConv, GATConv, GINConv/GINEConv: https://pytorch-geometric.readthedocs.io/en/latest/notes/cheatsheet.html
  3. NeighborLoader: https://pytorch-geometric.readthedocs.io/en/latest/modules/loader.html
  4. RandomLinkSplit: https://pytorch-geometric.readthedocs.io/en/latest/generated/torch_geometric.transforms.RandomLinkSplit.html
  5. Open Graph Benchmark: https://ogb.stanford.edu/
  6. W. Hamilton et al., GraphSAGE: https://arxiv.org/abs/1706.02216
  7. T. Kipf, M. Welling, GCN: https://arxiv.org/abs/1609.02907