诸神缄默不语-个人CSDN博文目录
cs224w(图机器学习)2021冬季课程学习笔记集合
VX号“PolarisRisingWar”可直接搜索添加作者好友讨论。
更新日志:
2021.11.16 优化排版
colab 2 文件原始下载地址
我将写完的colab 2文件发到了GitHub上,有一些个人做笔记的内容。地址:cs224w-2021-winter-colab/CS224W_Colab_2.ipynb at master · PolarisRisingWar/cs224w-2021-winter-colab
本colab主要实现:
这部分的详细解释可以参考我写的另一篇博文:PyTorch Geometric (PyG) 入门教程 。
因为没有写过专门的ogb包教程,所以将对ogb包的概览和理解都写在这里。以后如有需要可能会整合为专门的相关教程。
obg包很多函数没有文档,所以只能靠查源码……这对我来说还是挺难的,所以这些函数我就只管用,先不做理解了。
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,测试时置False
split_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目录。
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
打印。
这一部分colab应该是参考了 ogb/gnn.py at master · snap-stanford/ogb 的GCN部分代码。
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
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数据集是一个较小的数据集,用于节点多分类任务。来自MAG3语料集,表示arxiv论文互相引用的状态,节点是论文,链接是引用。每个节点有128维的特征,是其标题与摘要词嵌入的平均值。词嵌入通过skip-gram模型4获取。
有169,343个节点,是有向图5,1,166,243条边,多分类任务(40个论文主题)。
数据集切分的依据是论文发表时间(2017年及其之前的数据作为训练集,2018年的数据作为验证集,2019年及后的作为测试集)(90491个训练集数据,29799个验证集数据,48603个测试集数据)。
ogb官方提供的评估指标是accuracy。
有一张图,打印出来Data是:Data(adj_t=[169343, 169343, nnz=1166243], node_year=[169343, 1], x=[169343, 128], y=[169343, 1])
# Make the adjacency matrix to symmetric
data.adj_t = data.adj_t.to_symmetric()
(to_symmetric()
函数没找到文档,只找到了源代码:pytorch_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)
在这里用作分类模型。
如果将最后一层分类层(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?
注意这里,我们在训练时是拿所有数据(整张图)喂进模型训练的,但是计算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()
返回在训练集、验证集、测试集上的评估指标结果
@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
args = {
'device': device,
'num_layers': 3,
'hidden_dim': 256,
'dropout': 0.5,
'lr': 0.01,
'epochs': 100,
}
model = GCN(data.num_features, args['hidden_dim'],
dataset.num_classes, args['num_layers'],
args['dropout']).to(device)
evaluator = Evaluator(name='ogbn-arxiv')
跑 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}%')
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%
这一部分的代码应该有参考自:
ogb/main_pyg.py at master · snap-stanford/ogb。但是这部分感觉参考得不太多,所以我就没仔细看这部分ogb官方的代码。
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
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
rocauc
ogbg-molhiv是个较小的分子属性预测数据集,用于图分类任务(二元分类)。有41,127个无向图,平均每个图有25.5个节点、13.75个边。任务目标是二元分类。评估指标是ROC-AUC。
数据集改自 MoleculeNet6,每个分子都已通过 RDKit7 进行了预处理。每个图代表一个分子,节点代表原子,边代表化学键。节点有9维特征,包含了其原子数、手征、形式电荷、该原子是否在环中等信息。
官方网站上提供了对原始特征的预处理示例代码:
from ogb.graphproppred.mol_encoder import AtomEncoder, BondEncoder
atom_encoder = AtomEncoder(emb_dim = 100)
bond_encoder = BondEncoder(emb_dim = 100)
atom_emb = atom_encoder(x) # x is input atom feature
edge_emb = bond_encoder(edge_attr) # edge_attr is input edge feature
作为示例,打印数据集的第一个图,输出如下:
Data(edge_attr=[40, 3], edge_index=[2, 40], x=[19, 9], y=[1, 1])
train_loader = DataLoader(dataset[split_idx["train"]], batch_size=32, shuffle=True, num_workers=0)
valid_loader = DataLoader(dataset[split_idx["valid"]], batch_size=32, shuffle=False, num_workers=0)
test_loader = DataLoader(dataset[split_idx["test"]], batch_size=32, shuffle=False, num_workers=0)
device = 'cuda' if torch.cuda.is_available() else 'cpu'
args = {
'device': device,
'num_layers': 5,
'hidden_dim': 256,
'dropout': 0.5,
'lr': 0.001,
'epochs': 30,
}
class GCN_Graph(torch.nn.Module):
def __init__(self, hidden_dim, output_dim, num_layers, dropout):
super(GCN_Graph, self).__init__()
# Load encoders for Atoms in molecule graphs
self.node_encoder = AtomEncoder(hidden_dim)
# Node embedding model
self.gnn_node = GCN(hidden_dim, hidden_dim,
hidden_dim, num_layers, dropout, return_embeds=True)
self.pool=global_mean_pool
self.linear = torch.nn.Linear(hidden_dim, output_dim)
def reset_parameters(self):
self.gnn_node.reset_parameters()
self.linear.reset_parameters()
def forward(self, batched_data):
x, edge_index, batch = batched_data.x, batched_data.edge_index, batched_data.batch
embed = self.node_encoder(x)
out=self.gnn_node(embed,edge_index)
out=self.pool(out,batch)
out=self.linear(out)
return out
def train(model, device, data_loader, optimizer, loss_fn):
"""
optimizer是给定优化器(torch.optim)
loss_fn是给定损失函数
"""
model.train()
loss = 0
for step, batch in enumerate(tqdm(data_loader, desc="Iteration")):
batch = batch.to(device)
if batch.x.shape[0] == 1 or batch.batch[-1] == 0:
pass
else:
## ignore nan targets (unlabeled) when computing training loss.
is_labeled = batch.y == batch.y
optimizer.zero_grad()
op=model(batch)
train_op=op[is_labeled]
train_labels=batch.y[is_labeled]
#loss=loss_fn(train_op,train_labels)
#RuntimeError: result type Float can't be cast to the desired output type Long
#train_op的dtype是torch.float32
#train_labels的dtype是torch.int64
loss=loss_fn(train_op,train_labels.float())
loss.backward()
optimizer.step()
return loss.item()
代码里面有一句判断x第一维长度为1或者batch最后一个数据为0(这是只有一个图的情况的意思吗?),还有忽略无标签数据的……其实我没搞懂这是在干啥,我还专门跑了一下如下两个代码:
for batch in train_loader:
if batch.x.shape[0] == 1 or batch.batch[-1] == 0:
print(batch)
break
for batch in train_loader:
if batch.x.shape[0] == 1 or batch.batch[-1] == 0:
pass
else:
is_labeled = batch.y == batch.y
if False in is_labeled:
print(batch)
break
发现都没有输出,也就是两种情况都不存在。
……所以我也没搞懂在什么情况下这个功能会起作用。
但是is_labeled在这里同时作为将二维矩阵变形到一维向量使用(跟3.4用的第二维度取索引为0的一维,或者squeeze view reshape之类的函数在此方面功能相似)。
这个机制的原理是这样的:见 numpy官方文档索引部分对布尔或mask索引的讲解,batch.y是和is_labeled是同shape的布尔矩阵(这很显然嘛),同shape的布尔矩阵作为索引,返回的就是一维矩阵。
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)
model = GCN_Graph(args['hidden_dim'],
dataset.num_tasks, args['num_layers'],
args['dropout']).to(device)
evaluator = Evaluator(name='ogbg-molhiv')
跑 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}%')
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%
可参考:PyTorch Geometric (PyG) 入门教程 ↩︎
注意有趣的一点(其实并不有趣因为我没搞懂):模型的实现使用的是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 the data
object to a SparseTensor
as late as possible, since there exist some transforms that are only able to operate on data.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. ↩︎