本文已参与「新人创作礼」活动,一起开启掘金创作之路。
本文首发于CSDN。
诸神缄默不语-个人CSDN博文目录
cs224w(图机器学习)2021冬季课程学习笔记集合
@[toc]
我将写完的colab 2文件发到了GitHub上,有一些个人做笔记的内容。地址:cs224w-2021-winter-colab/CS224W_Colab_2.ipynb at master · PolarisRisingWar/cs224w-2021-winter-colab
本colab主要实现:
- 使用 PyGPyTorch Geometric (PyG) 入门教程 。
2. ogb包介绍
因为没有写过专门的ogb包教程,所以将对ogb包的概览和理解都写在这里。以后如有需要可能会整合为专门的相关教程。
obg包很多函数没有文档,所以只能靠查源码……这对我来说还是挺难的,所以这些函数我就只管用,先不做理解了。- 官网:Get Started | Open Graph Benchmark
- 示例代码:baseline experiments/example scripts
- OGB包中包含了一系列真实、大规模、常用于实现图机器学习任务benchmark的数据集。ogb包提供了数据集的dataloader和evaluator。
- Data Loaders官网示例代码:
from ogb.graphproppred import PygGraphPropPredDataset from torch_geometric.data import DataLoader # Download and process data at './dataset/ogbg_molhiv/' dataset = PygGraphPropPredDataset(name = "ogbg-molhiv", root = 'dataset/') split_idx = dataset.get_idx_split() train_loader = DataLoader(dataset[split_idx["train"]], batch_size=32, shuffle=True) valid_loader = DataLoader(dataset[split_idx["valid"]], batch_size=32, shuffle=False) test_loader = DataLoader(dataset[split_idx["test"]], batch_size=32, shuffle=False)
在这里的dataset与PyG中的dataset类似,都可以执行用索引提取Data,切片,应用DataLoader等操作。
DataLoader的shuffle:训练时置True,测试时置Falsesplit_idx是类似这样的字典:
{‘train’: tensor([ 0, 1, 2, …, 169145, 169148, 169251]),
‘valid’: tensor([ 349, 357, 366, …, 169185, 169261, 169296]),
‘test’: tensor([ 346, 398, 451, …, 169340, 169341, 169342])}ogb中的数据集都映射自现实世界实体,具体的映射信息可以见根目录中的mapping目录。
- Evaluator官方示例代码
from ogb.graphproppred import Evaluator evaluator = Evaluator(name = "ogbg-molhiv") input_dict = {"y_true": y_true, "y_pred": y_pred} result_dict = evaluator.eval(input_dict) # E.g., {"rocauc": 0.7321}
input_dict和result_dict的格式可以通过
evaluator.expected_input_format
和evaluator.expected_output_format
打印。
3. 节点预测任务
这一部分colab应该是参考了 ogb/gnn.py at master · snap-stanford/ogb 的GCN部分代码。
3.0 导包
import torch import torch.nn.functional as F # The PyG built-in GCNConv from torch_geometric.nn import GCNConv import torch_geometric.transforms as T from ogb.nodeproppred import PygNodePropPredDataset, Evaluator import copy
3.1 加载ogbn-arxiv数据集
ogb节点分类任务数据集官方文档
ogbn-arxiv数据集官方文档dataset_name = 'ogbn-arxiv' # Load the dataset and transform it to sparse tensor dataset = PygNodePropPredDataset(name=dataset_name, transform=T.ToSparseTensor()) print(dataset.task_type) print(dataset.num_classes) print(dataset.num_tasks) print(dataset.eval_metric)
multiclass classification
40
1
acc对代码中转换到稀疏矩阵的部分还不了解。总之简单来说,
toSparseTensor
方法是将edge_index转换为torch_sparse.SparseTensor
格式(torch_sparse的GitHub项目是rusty1s/pytorch_sparse: PyTorch Extension Library of Optimized Autograd Sparse Matrix Operations,还没有了解过详情)。(总之如果不加transform,这一项属性就是edge_index)ogbn-arxiv数据集是一个较小的数据集,用于节点多分类任务。来自MAGpytorch_sparse/tensor.py at master · rusty1s/pytorch_sparse 没看懂,算了)
将data转移到cuda上(如果有GPU的话),并进行数据划分
device = 'cuda' if torch.cuda.is_available() else 'cpu' # If you use GPU, the device should be cuda print('Device: {}'.format(device)) data = data.to(device) split_idx = dataset.get_idx_split() #将数据集分成了train,valid,test三部分 train_idx = split_idx['train'].to(device)
3.3 搭建神经网络模型
在这里用作分类模型。
如果将最后一层分类层(Softmax)摘掉,就直接输出这一层的隐藏节点嵌入。相当于将模型作为一个嵌入维度为output_dim的节点嵌入模型。(在后文图分类部分使用)网络模型示意图:
class GCN(torch.nn.Module): def __init__(self, input_dim, hidden_dim, output_dim, num_layers, dropout, return_embeds=False): """ dropout是dropout的概率 return_embeds如果置True的话就跳过分类层,输出节点嵌入(原话:Skip classification layer and return node embeddings) """ super(GCN, self).__init__() self.convs = torch.nn.ModuleList() for i in range(num_layers - 1): self.convs.append(GCNConv(input_dim, hidden_dim)) input_dim = hidden_dim self.convs.append(GCNConv(hidden_dim, output_dim)) self.bns=torch.nn.ModuleList([torch.nn.BatchNorm1d(hidden_dim) for i in range(num_layers-1)]) self.softmax=torch.nn.LogSoftmax() self.dropout = dropout self.return_embeds = return_embeds def reset_parameters(self): for conv in self.convs: conv.reset_parameters() for bn in self.bns: bn.reset_parameters() def forward(self, x, adj_t): out = None #前 num_layers-1 层 for layer in range(len(self.convs)-1): x=self.convs[layer](x,adj_t) #forward(x: torch.Tensor, edge_index: Union[torch.Tensor, torch_sparse.tensor.SparseTensor], #edge_weight: Optional[torch.Tensor] = None) x=self.bns[layer](x) x=F.relu(x) x=F.dropout(x,self.dropout,self.training) #最后一层 out=self.convs[-1](x,adj_t) if not self.return_embeds: out=self.softmax(out) return out
那个
reset_parameters()
方法重置了它的网络层的参数,这些网络层应该是一开始就自动调用reset_parameters()
了,之所以要再重新写一遍,我在GitHub上问了 ogb/gnn.py at master · snap-stanford/ogb 原作者,他说是因为在他的代码中可能需要多次训练模型,每次都期待有不同的初始化参数,所以专门写了这个函数来实现重置所有子Module的参数。(见:Why network module in example/. define reset_parameters manually? · Discussion #227 · snap-stanford/ogb)
……那么现在问题来了,colab2里面就跑了一次这个模型为啥还非要再写一遍这个方法?我个人倾向于是猜测是因为老师抄作业的时候抄拉了。关于
F.dropout()
方法第三个参数self.training,可参考我写的博文:PyTorch的F.dropout为什么要加self.training?3.4 构建train()函数
注意这里,我们在训练时是拿所有数据(整张图)喂进模型训练的,但是计算loss时只用训练集的loss来计算梯度。
def train(model, data, train_idx, optimizer, loss_fn): model.train() loss = 0 optimizer.zero_grad() out=model(data.x,data.adj_t) train_output=out[train_idx] train_label=data.y[train_idx,0] #这里注意data.y是个二维矩阵,但是我们希望输出一维向量 #所以也可以用squeeze, view, reshape 反正性质是一样的 loss=loss_fn(train_output,train_label) loss.backward() optimizer.step() return loss.item()
3.5 构建test()函数
返回在训练集、验证集、测试集上的评估指标结果
@torch.no_grad() def test(model, data, split_idx, evaluator): model.eval() out=model(data.x,data.adj_t) y_pred = out.argmax(dim=-1, keepdim=True) #ogbn-arxiv的评估指标是Accuracy #print(evaluator.expected_output_format)输出是:{'acc': acc} train_acc = evaluator.eval({ 'y_true': data.y[split_idx['train']], 'y_pred': y_pred[split_idx['train']], })['acc'] valid_acc = evaluator.eval({ 'y_true': data.y[split_idx['valid']], 'y_pred': y_pred[split_idx['valid']], })['acc'] test_acc = evaluator.eval({ 'y_true': data.y[split_idx['test']], 'y_pred': y_pred[split_idx['test']], })['acc'] return train_acc, valid_acc, test_acc
3.6 设置超参
args = { 'device': device, 'num_layers': 3, 'hidden_dim': 256, 'dropout': 0.5, 'lr': 0.01, 'epochs': 100, }
3.7 初始化模型和评估器
model = GCN(data.num_features, args['hidden_dim'], dataset.num_classes, args['num_layers'], args['dropout']).to(device) evaluator = Evaluator(name='ogbn-arxiv')
3.8 训练
跑 args[“epochs”] 轮epoch,将在验证集上表现最好的模型保存下来。
# reset the parameters to initial random value model.reset_parameters() optimizer = torch.optim.Adam(model.parameters(), lr=args['lr']) loss_fn = F.nll_loss best_model = None best_valid_acc = 0 for epoch in range(1, 1 + args["epochs"]): loss = train(model, data, train_idx, optimizer, loss_fn) result = test(model, data, split_idx, evaluator) train_acc, valid_acc, test_acc = result if valid_acc > best_valid_acc: best_valid_acc = valid_acc best_model = copy.deepcopy(model) print(f'Epoch: {epoch:02d}, ' f'Loss: {loss:.4f}, ' f'Train: {100 * train_acc:.2f}%, ' f'Valid: {100 * valid_acc:.2f}% ' f'Test: {100 * test_acc:.2f}%')
3.9 输出最好模型的表现结果
best_result = test(best_model, data, split_idx, evaluator) train_acc, valid_acc, test_acc = best_result print(f'Best model: ' f'Train: {100 * train_acc:.2f}%, ' f'Valid: {100 * valid_acc:.2f}% ' f'Test: {100 * test_acc:.2f}%')
Best model: Train: 74.44%, Valid: 71.94% Test: 71.15%
4. 图分类任务
这一部分的代码应该有参考自:
ogb/main_pyg.py at master · snap-stanford/ogb。但是这部分感觉参考得不太多,所以我就没仔细看这部分ogb官方的代码。4.0 导包
from ogb.graphproppred import PygGraphPropPredDataset, Evaluator from ogb.graphproppred.mol_encoder import AtomEncoder from torch_geometric.data import DataLoader from torch_geometric.nn import global_add_pool, global_mean_pool from tqdm.notebook import tqdm import copy
4.1 加载ogbg-molhiv数据集
ogb图分类任务数据集官方文档
ogbg-molhiv数据集官方文档dataset = PygGraphPropPredDataset(name='ogbg-molhiv') split_idx = dataset.get_idx_split() print(dataset.task_type) print(dataset.num_classes) print(dataset.num_tasks) print(dataset.eval_metric)
binary classification
2
1
rocaucogbg-molhiv是个较小的分子属性预测数据集,用于图分类任务(二元分类)。有41,127个无向图,平均每个图有25.5个节点、13.75个边。任务目标是二元分类。评估指标是ROC-AUC。
数据集改自 MoleculeNetnumpy官方文档索引部分对布尔或mask索引的讲解,batch.y是和is_labeled是同shape的布尔矩阵(这很显然嘛),同shape的布尔矩阵作为索引,返回的就是一维矩阵。4.6 构建eval()函数
def eval(model, device, loader, evaluator): model.eval() y_true = [] y_pred = [] for step, batch in enumerate(tqdm(loader, desc="Iteration")): batch = batch.to(device) if batch.x.shape[0] == 1: pass else: with torch.no_grad(): pred = model(batch) y_true.append(batch.y.view(pred.shape).detach().cpu()) y_pred.append(pred.detach().cpu()) y_true = torch.cat(y_true, dim = 0).numpy() y_pred = torch.cat(y_pred, dim = 0).numpy() input_dict = {"y_true": y_true, "y_pred": y_pred} return evaluator.eval(input_dict)
4.7 初始化模型和评估器
model = GCN_Graph(args['hidden_dim'], dataset.num_tasks, args['num_layers'], args['dropout']).to(device) evaluator = Evaluator(name='ogbg-molhiv')
4.8 训练
跑 args[“epochs”] 轮epoch,将在验证集上表现最好的模型保存下来。
(注意虽然这里评估指标写的是acc,但是其实是AUC……)model.reset_parameters() optimizer = torch.optim.Adam(model.parameters(), lr=args['lr']) loss_fn = torch.nn.BCEWithLogitsLoss() best_model = None best_valid_acc = 0 for epoch in range(1, 1 + args["epochs"]): print('Training...') loss = train(model, device, train_loader, optimizer, loss_fn) print('Evaluating...') train_result = eval(model, device, train_loader, evaluator) val_result = eval(model, device, valid_loader, evaluator) test_result = eval(model, device, test_loader, evaluator) train_acc, valid_acc, test_acc = train_result[dataset.eval_metric], val_result[dataset.eval_metric], test_result[dataset.eval_metric] if valid_acc > best_valid_acc: best_valid_acc = valid_acc best_model = copy.deepcopy(model) print(f'Epoch: {epoch:02d}, ' f'Loss: {loss:.4f}, ' f'Train: {100 * train_acc:.2f}%, ' f'Valid: {100 * valid_acc:.2f}% ' f'Test: {100 * test_acc:.2f}%')
4.9 输出最好模型的表现结果
train_acc = eval(best_model, device, train_loader, evaluator)[dataset.eval_metric] valid_acc = eval(best_model, device, valid_loader, evaluator)[dataset.eval_metric] test_acc = eval(best_model, device, test_loader, evaluator)[dataset.eval_metric] print(f'Best model: ' f'Train: {100 * train_acc:.2f}%, ' f'Valid: {100 * valid_acc:.2f}% ' f'Test: {100 * test_acc:.2f}%')
进度条不赘
打印输出:Best model: Train: 87.07%, Valid: 81.10% Test: 76.75%
5. 参考资料
- 写代码的时候有参考过这篇:cs224w-winter-2021/CS224W_Colab_2.ipynb at main · XckCodeDD/cs224w-winter-2021 但是,照例,我不想再重复多看别人的代码了,所以我也不知道这个答案有没有问题了。
Footnotes
-
注意有趣的一点(其实并不有趣因为我没搞懂):模型的实现使用的是GCN,这个算法到底是transductive还是inductive的我至今还没搞懂。
在节点分类任务中是用全图进行训练,仅通过训练集索引的节点数据进行优化,最后测试时也是用得到的模型对所有数据进行运算。
但是图分类任务中就可以直接很inductive地划分数据集,在训练集上训练,验证集上验证,测试集上测试……就这个问题我想了一下,我觉得在图分类任务上面应该是说GCN就单纯作为一个embedding的方法,所以大概它的参数是可以直接用在没见过的新图的数据上的……所以它应该inductive吧,要不然这咋整。
感觉我好像get到了什么,但是又隐约感到一点只能意会不可言传的微妙的懵逼感,所以大概我没搞懂。 ↩ -
Kuansan Wang, Zhihong Shen, Chiyuan Huang, Chieh-Han Wu, Yuxiao Dong, and Anshul Kanakia. Microsoft academic graph: When experts are not enough. Quantitative Science Studies, 1(1):396–413, 2020. ↩
-
Tomas Mikolov, Ilya Sutskever, Kai Chen, Greg S Corrado, and Jeff Dean. Distributed representationsof words and phrases and their compositionality. In Advances in Neural Information Processing Systems (NeurIPS), pp. 3111–3119, 2013. ↩
-
注意,这个Data没法直接调用
is_directed()
方法,因为is_undirected()
方法必须要用edge_index这个attribute。
所以我是用没加transform参数获取的数据集的Data调用的is_directed()
方法确认它是有向图的。
在PyG文档中也介绍toSparseTensor
方法最好晚点调用,因为有很多方法可能会依赖edge_index属性。原话:In case of composing multiple transforms, it is best to convert thedata
object to aSparseTensor
as late as possible, since there exist some transforms that are only able to operate ondata.edge_index
for now.
(当然在文档里本来就有写它是个有向图就是了……) ↩ -
Zhenqin Wu, Bharath Ramsundar, Evan N Feinberg, Joseph Gomes, Caleb Geniesse, Aneesh SPappu, Karl Leswing, and Vijay Pande. Moleculenet: a benchmark for molecular machine learning. Chemical Science, 9(2):513–530, 2018. ↩
-
Greg Landrum et al. RDKit: Open-source cheminformatics, 2006. ↩