Files
2026-07-13 13:35:51 +08:00

253 lines
7.6 KiB
Python

import argparse
import os
import time
import warnings
import torch
import torch.nn.functional as F
from config import CONFIG
from modules import GCNNet
from sampler import SAINTEdgeSampler, SAINTNodeSampler, SAINTRandomWalkSampler
from torch.utils.data import DataLoader
from utils import calc_f1, evaluate, load_data, Logger, save_log_dir
def main(args, task):
warnings.filterwarnings("ignore")
multilabel_data = {"ppi", "yelp", "amazon"}
multilabel = args.dataset in multilabel_data
# This flag is excluded for too large dataset, like amazon, the graph of which is too large to be directly
# shifted to one gpu. So we need to
# 1. put the whole graph on cpu, and put the subgraphs on gpu in training phase
# 2. put the model on gpu in training phase, and put the model on cpu in validation/testing phase
# We need to judge cpu_flag and cuda (below) simultaneously when shift model between cpu and gpu
if args.dataset in ["amazon"]:
cpu_flag = True
else:
cpu_flag = False
# load and preprocess dataset
data = load_data(args, multilabel)
g = data.g
train_mask = g.ndata["train_mask"]
val_mask = g.ndata["val_mask"]
test_mask = g.ndata["test_mask"]
labels = g.ndata["label"]
train_nid = data.train_nid
in_feats = g.ndata["feat"].shape[1]
n_classes = data.num_classes
n_nodes = g.num_nodes()
n_edges = g.num_edges()
n_train_samples = train_mask.int().sum().item()
n_val_samples = val_mask.int().sum().item()
n_test_samples = test_mask.int().sum().item()
print(
"""----Data statistics------'
#Nodes %d
#Edges %d
#Classes/Labels (multi binary labels) %d
#Train samples %d
#Val samples %d
#Test samples %d"""
% (
n_nodes,
n_edges,
n_classes,
n_train_samples,
n_val_samples,
n_test_samples,
)
)
# load sampler
kwargs = {
"dn": args.dataset,
"g": g,
"train_nid": train_nid,
"num_workers_sampler": args.num_workers_sampler,
"num_subg_sampler": args.num_subg_sampler,
"batch_size_sampler": args.batch_size_sampler,
"online": args.online,
"num_subg": args.num_subg,
"full": args.full,
}
if args.sampler == "node":
saint_sampler = SAINTNodeSampler(args.node_budget, **kwargs)
elif args.sampler == "edge":
saint_sampler = SAINTEdgeSampler(args.edge_budget, **kwargs)
elif args.sampler == "rw":
saint_sampler = SAINTRandomWalkSampler(
args.num_roots, args.length, **kwargs
)
else:
raise NotImplementedError
loader = DataLoader(
saint_sampler,
collate_fn=saint_sampler.__collate_fn__,
batch_size=1,
shuffle=True,
num_workers=args.num_workers,
drop_last=False,
)
# set device for dataset tensors
if args.gpu < 0:
cuda = False
else:
cuda = True
torch.cuda.set_device(args.gpu)
val_mask = val_mask.cuda()
test_mask = test_mask.cuda()
if not cpu_flag:
g = g.to("cuda:{}".format(args.gpu))
print("labels shape:", g.ndata["label"].shape)
print("features shape:", g.ndata["feat"].shape)
model = GCNNet(
in_dim=in_feats,
hid_dim=args.n_hidden,
out_dim=n_classes,
arch=args.arch,
dropout=args.dropout,
batch_norm=not args.no_batch_norm,
aggr=args.aggr,
)
if cuda:
model.cuda()
# logger and so on
log_dir = save_log_dir(args)
logger = Logger(os.path.join(log_dir, "loggings"))
logger.write(args)
# use optimizer
optimizer = torch.optim.Adam(model.parameters(), lr=args.lr)
# set train_nids to cuda tensor
if cuda:
train_nid = torch.from_numpy(train_nid).cuda()
print(
"GPU memory allocated before training(MB)",
torch.cuda.memory_allocated(device=train_nid.device) / 1024 / 1024,
)
start_time = time.time()
best_f1 = -1
for epoch in range(args.n_epochs):
for j, subg in enumerate(loader):
if cuda:
subg = subg.to(torch.cuda.current_device())
model.train()
# forward
pred = model(subg)
batch_labels = subg.ndata["label"]
if multilabel:
loss = F.binary_cross_entropy_with_logits(
pred,
batch_labels,
reduction="sum",
weight=subg.ndata["l_n"].unsqueeze(1),
)
else:
loss = F.cross_entropy(pred, batch_labels, reduction="none")
loss = (subg.ndata["l_n"] * loss).sum()
optimizer.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm(model.parameters(), 5)
optimizer.step()
if j == len(loader) - 1:
model.eval()
with torch.no_grad():
train_f1_mic, train_f1_mac = calc_f1(
batch_labels.cpu().numpy(),
pred.cpu().numpy(),
multilabel,
)
print(
f"epoch:{epoch + 1}/{args.n_epochs}, Iteration {j + 1}/"
f"{len(loader)}:training loss",
loss.item(),
)
print(
"Train F1-mic {:.4f}, Train F1-mac {:.4f}".format(
train_f1_mic, train_f1_mac
)
)
# evaluate
model.eval()
if epoch % args.val_every == 0:
if (
cpu_flag and cuda
): # Only when we have shifted model to gpu and we need to shift it back on cpu
model = model.to("cpu")
val_f1_mic, val_f1_mac = evaluate(
model, g, labels, val_mask, multilabel
)
print(
"Val F1-mic {:.4f}, Val F1-mac {:.4f}".format(
val_f1_mic, val_f1_mac
)
)
if val_f1_mic > best_f1:
best_f1 = val_f1_mic
print("new best val f1:", best_f1)
torch.save(
model.state_dict(),
os.path.join(log_dir, "best_model_{}.pkl".format(task)),
)
if cpu_flag and cuda:
model.cuda()
end_time = time.time()
print(f"training using time {end_time - start_time}")
# test
if args.use_val:
model.load_state_dict(
torch.load(
os.path.join(log_dir, "best_model_{}.pkl".format(task)),
weights_only=False,
)
)
if cpu_flag and cuda:
model = model.to("cpu")
test_f1_mic, test_f1_mac = evaluate(model, g, labels, test_mask, multilabel)
print(
"Test F1-mic {:.4f}, Test F1-mac {:.4f}".format(
test_f1_mic, test_f1_mac
)
)
if __name__ == "__main__":
warnings.filterwarnings("ignore")
parser = argparse.ArgumentParser(description="GraphSAINT")
parser.add_argument(
"--task", type=str, default="ppi_n", help="type of tasks"
)
parser.add_argument(
"--online",
dest="online",
action="store_true",
help="sampling method in training phase",
)
parser.add_argument("--gpu", type=int, default=0, help="the gpu index")
task = parser.parse_args().task
args = argparse.Namespace(**CONFIG[task])
args.online = parser.parse_args().online
args.gpu = parser.parse_args().gpu
print(args)
main(args, task=task)