图网络:从数据处理到DGL模型构建(GCN, GraphSAGE, RGCN)

本文介绍了图网络在数据处理上的特点,包括节点去重、训练集与测试集划分、边数据处理。详细讲解了如何从Networkx转换为DGL图,以及GCN、GraphSAGE和RGCN的模型构建、训练和预测过程。此外,还探讨了Transductive与Inductive学习的区别,并提供了相关学习资源。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.数据处理

1.1.原始数据节点去重

图网络学习普通机器学习/深度学习算法在数据处理上是存在差异的。

任务:根据用户的行为(Feature),预测用户在下一个时间段的某行为(Label)。
原始数据:不同用户在不同时间节点下的Feature及相应的Label。
100个样本,其中仅有20个不同的用户。

在传统算法中,上述数据可直接以100个样本及其对应Label进行模型的训练。
但是在图算法中,仅可保留20个user作为20个样本。因为在图网络中,用户和节点是一一对应的。
本次实验直接采用去旧留新法,即只保留某user的最新日期的样本

def obtain_node_data(file = 'train.txt'):

    node_data = pd.read_table(file).sample(frac = 1.0)
    node_data['vroleid'] = node_data['vroleid'].astype('str') # user id
    print('node_data: shape = {}, # user = {}'.format(node_data.shape, len(set(node_data['vroleid']))))
    
    node_data = node_data.dropna(axis = 1, how = 'all')
    print('node_data: shape = {}, # user = {}'.format(node_data.shape, len(set(node_data['vroleid']))))
    
    node_data = node_data.sort_values('stat_date', ascending = False).groupby('vroleid', as_index = False).first()
    print('node_data: shape = {}, # user = {}'.format(node_data.shape, len(set(node_data['vroleid']))))
    
    return node_data

1.2.训练集和测试集的节点划分

在实际应用中,会出现训练集和测试集的节点交叉的情形。

某user在训练集中是以20200101-20200201的时间段出现的。
该user在测试集是以20200301-20200401时间段出现的。

在本次实验中,对于此类交叉节点的处理是:只保留测试集中的最新时间的样本,用于测试。在训练集中去掉相关user的所有数据,以保证数据不重叠。

train_node_data = obtain_node_data('train.txt')
test_node_data = obtain_node_data('test.txt')

node_data = pd.concat([train_node_data, test_node_data])
node_data = node_data.sort_values('stat_date', ascending = False).groupby('vroleid', as_index = False).first()
print('node_data: shape = {}, # user = {}'.format(node_data.shape, len(set(node_data['vroleid']))))

train_node_data = node_data[~node_data['vroleid'].isin(list(test_node_data['vroleid']))]
test_node_data = node_data[node_data['vroleid'].isin(list(test_node_data['vroleid']))]
print('Shape: Train = {} | Test = {}'.format(train_node_data.shape, test_node_data.shape))

# 将训练集和测试集的节点数据合并(注意以train_mask和test_mask记录哪条节点数据属于哪个数据集。
train_mask_num = train_node_data.shape[0]
test_mask_num = test_node_data.shape[0]
print('Split Num: # Train = {}, # Test = {}'.format(train_mask_num, test_mask_num))

split_idx = [0] * train_mask_num + [1] * test_mask_num
node_data = pd.concat([train_node_data, test_node_data]).reset_index(drop = True)
node_data['split_idx'] = split_idx

1.3.边数据中删除节点

  • 去掉未在节点数据中出现的节点(因为没有相对应的Label)。
def process_edge_data(edge_data, node_data = test_node_data): 
   edge_data = edge_data[edge_data['vroleid'].isin(node_data['vroleid'])]
   edge_data = edge_data[edge_data['friend_roleid'].isin(node_data['vroleid'])]
   return edge_data
edge_data = process_edge_data(edge_data, node_data = node_data)
  • 根据图算法的需要:
    • Transductive learning(如GCN)
    • Inductive learning(如GraphSAGE)

在这里插入图片描述
Inductive learning:只根据现有的ABC来训练模型,在来一个新的数据时,直接加载5个ABC训练好的模型来预测。
Transductive learning:直接以某种算法观察出数据的分布,这里呈现三个cluster,就根据cluster判定,不会建立一个预测的模型。如果一个新的数据加进来,就必须重新算一遍整个算法,新加的数据也会导致旧的已预测?的结果改变。
参考:https://www.zhihu.com/question/68275921/answer/480709225

1.4.节点特征数据和Label

建图后才能处理。转Section 3。

2.建图

2.1.同质图 Networkx→DGL

  1. 先得到networkx的Graph

    networkx.convert_matrix.from_pandas_edgelist

    G = nx.from_pandas_edgelist(edge_data, 'vroleid', 'friend_roleid')
    print(nx.info(G))
    
    # 将node_data中没有好友关系/边的user节点加入到G中,即不存在于edge_data中的node
    no_edge_node = set(train_node_data.index).difference(set(G.nodes))
    G.add_nodes_from(no_edge_node)
    print(nx.info(G))
    

    注意: 原始的Edge数据可能并不能覆盖Node数据中的所有节点,因此对于未被覆盖的结点,要注意在建图的时候加进去,作为一个度为0的节点。

  2. 将networkx.graph转为DGL的Graph

    g = dgl.DGLGraph()
    g.from_networkx(G)
    g.add_edges(g.nodes(), g.nodes()) # self-loop
    # g边数 = G中的边数*2 + len(g.nodes())
    # 不建议使用这种方法,dgl.DGLGraph()将在后续版本中被淘汰。
    # https://docs.dgl.ai/en/latest/api/python/graph.html#adding-nodes-and-edges
    
    # https://docs.dgl.ai/en/latest/generated/dgl.graph.html
    # 建议使用这种方法建图,因为上个版本将被淘汰
    # 视算法情况决定是否加self-loop
    g = dgl.graph(G)
    print(g)
    # g边数 = G边数 * 2,可能是因为dgl.graph采用的是directed graph。
    

2.2.根据ID建图( 针对RGCN)

2.2.1.获得node和edge的ID

all_nodes = list(node_data['vroleid'])
entity2id = dict(zip(all_nodes, range(0, len(all_nodes))))

relation2id = dict(zip(range(1, 150), range(0, 149))) # 一共149个边类型(friend_level)

2.2.2.将原始边数据转换为ID数据

原始边数据:(vroleid, friend_level, friend_roleid)
转换为:(entity_id, relation_id, entity_id)

def read_triplets(edge_data, entity2id, relation2id):
    edge_data = np.array(edge_data[['vroleid', 'friend_level', 'friend_roleid']])
    
    triplets = []

    for line in edge_data:
        triplets.append((entity2id[line[0]], relation2id[line[1]], entity2id[line[-1]]))
    
    return np.array(triplets)
    
all_triplets = read_triplets(edge_data[['vroleid', 'friend_roleid', 'friend_level']], entity2id, relation2id)

2.2.3.建图

g = dgl.graph((all_triplets[:,0], all_triplets[:,2]), num_nodes = 299962)

3.节点特征, Label, 训练集/测试集idx

# 从node_data里提取和g.nodes()的顺序一致的Feature  https://www.jianshu.com/p/2d3dd3e30d51
user_order = list(G.nodes()) # 针对Section 2.1
user_order = all_nodes # 针对Section 2.2

feature = deepcopy(node_data).reset_index()
feature['vroleid'] = feature['vroleid'].astype('category')
feature['vroleid'].cat.reorder_categories(user_order, inplace = True)
feature.sort_values('vroleid', inplace = True)
feature.set_index('vroleid', inplace = True)
feature.drop(['stat_date', 'vopenid', 'not_lost', 'split_idx', 'label_log', 'label_reg'], axis = 1, inplace = True)

# 标准化 https://scikit-learn.org/stable/modules/preprocessing.html#preprocessing
scaler = preprocessing.StandardScaler().fit(feature)
feature = scaler.transform(feature)

for i,j in zip(*np.where(np.isnan(feature))):
    feature[i, j] = 0
labels = np.array(feature['not_lost'].astype(np.int))
label_log = np.array(feature['label_log'])
label_reg = np.array(feature['label_reg'])
split_mask_idx = np.array(feature['split_idx'].astype(np.int))

train_idx = np.where(split_mask_idx == 0)[0]
test_idx = np.where(split_mask_idx == 1)[0]

4.GCN→RF

4.1.模型结构

在这里插入图片描述

4.2.GCN函数定义


class GCNLayer(nn.Module):
    def __init__(self, in_feats, out_feats):
        super(GCNLayer, self).__init__()
        self.linear = nn.Linear(in_feats, out_feats)

    def forward(self, g, feature):
        # Creating a local scope so that all the stored ndata and edata
        # (such as the `'h'` ndata below) are automatically popped out
        # when the scope exits.
        with g.local_scope():
            g.ndata['h'] = feature
            g.update_all(gcn_msg, gcn_reduce)
            h = g.ndata['h']
            return self.linear(h)
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.layer1 = GCNLayer(180, 64)
        self.layer2 = GCNLayer(64, 180)

    def forward(self, g, features):
        output = F.relu(self.layer1(g, features))
        x = self.layer2(g, output)
        return x, output

def evaluate(model, g, features, labels, mask):
    model.eval() # 测试模式
    with th.no_grad(): # 关闭求导
        logits = model(g, features) # 所有数据作前向传播
        logits = logits[mask] # 取出相应数据集对应的部分
        labels = labels[mask]
        _, indices = th.max(logits, dim = 1) # 按行取argmax,得到预测的标签
        correct = th.sum(indices == labels)
        
        tn, fp, fn, tp = confusion_matrix(labels, indices).ravel() # y_true, y_pred
        assert correct.item() * 1.0 / len(labels) == (tn+tp)/len(labels)
        accuracy = (tn+tp)/len(labels)
        pos_acc = tp/sum(labels).item()
        neg_acc = tn/(len(indices)-sum(indices).item()) # [y_true=0 & y_pred=0] / y_pred=0
        neg_recall = tn / (tn+fp) # [y_true=0 & y_pred=0] / y_true=0
        roc_auc = roc_auc_score(labels, logits[:,1])
        prec, reca, _ = precision_recall_curve(labels, logits[:,1])
        aupr = auc(reca, prec)
        return neg_recall, neg_acc, pos_acc, accuracy, roc_auc, aupr
        

4.3.Step1:训练集训练GCN1

4.3.1.运行前获取数据


# step1中的所有图信息都是由train_node_data生成的。
g_step1 = dgl.DGLGraph()
g_step1.from_networkx(G_step1)
g_step1.add_edges(g_step1.nodes(), g_step1.nodes())

feature_step1 = th.FloatTensor(feature_step1)
labels_step1 = th.LongTensor(labels_step1)

train_idx_step1 = np.where(split_mask_idx_step1 == 0)[0]
val_idx_step1 = np.where(split_mask_idx_step1 == 1)[0]

train_mask_step1 = deepcopy(split_mask_idx_step1)
train_mask_step1[train_idx_step1] = 1
train_mask_step1[val_idx_step1] = 0
train_mask_step1 = th.BoolTensor(train_mask_step1)

val_mask_step1 = deepcopy(split_mask_idx_step1)
val_mask_step1[train_idx_step1] = 0
val_mask_step1[val_idx_step1] = 1
val_mask_step1 = th.BoolTensor(val_mask_step1)

4.3.2.训练模型


net = Net()
gcn_msg = fn.copy_src(src = 'h', out = 'm')
gcn_reduce = fn.sum(msg = 'm', out = 'h')

optimizer = th.optim.Adam(net.parameters(), lr=1e-3)
dur = []

for epoch in range(1, 51): # 完整遍历一遍训练集, 一个epoch做一次更新
    print(epoch, end = ',')
    t0 = time.time()

    net.train()
    logits, output_step1 = net(g_step1, feature_step1) # 所有数据前向传播
    logp = F.log_softmax(logits, 1)
    loss = F.nll_loss(logp[train_mask_step1], labels_step1[train_mask_step1]) # 只选择训练节点进行监督,计算loss

    optimizer.zero_grad() # 清空梯度
    loss.backward() # 反向传播计算参数的梯度
    optimizer.step() # 使用优化方法进行梯度更新

    dur.append(time.time() - t0)
    

4.4.Step2:获得测试集的graph embedding

4.4.1.运行前获取数据


# step2的所有数据都是由node_data得到的。
g_step2 = dgl.DGLGraph()
g_step2.from_networkx(G_step2)
g_step2.add_edges(g_step2.nodes(), g_step2.nodes())

feature_step2 = th.FloatTensor(feature_step2)
labels_step2 = th.LongTensor(labels_step2)

train_idx_step2 = np.where(split_mask_idx_step2 == 0)[0]
test_idx_step2 = np.where(split_mask_idx_step2 == 1)[0]

train_mask_step2 = deepcopy(split_mask_idx_step2)
train_mask_step2[train_idx_step2] = 1
train_mask_step2[test_idx_step2] = 0
train_mask_step2 = th.BoolTensor(train_mask_step2)

test_mask_step2 = deepcopy(split_mask_idx_step2)
test_mask_step2[train_idx_step2] = 0
test_mask_step2[test_idx_step2] = 1
test_mask_step2 = th.BoolTensor(test_mask_step2)

4.4.2.运行模型


optimizer = th.optim.Adam(net.parameters(), lr=1e-3)
dur = []

for epoch in range(1, 51): # 完整遍历一遍训练集, 一个epoch做一次更新
    print(epoch, end = ',')
    t0 = time.time()

    net.train()
    logits, output_step2 = net(g_step2, feature_step2) # 所有数据前向传播
    logp = F.log_softmax(logits, 1)
    loss = F.nll_loss(logp[train_mask_step2], labels_step2[train_mask_step2]) # 只选择训练节点进行监督,计算loss
    
    optimizer.zero_grad() # 清空梯度
    loss.backward() # 反向传播计算参数的梯度
    optimizer.step() # 使用优化方法进行梯度更新

    dur.append(time.time() - t0)

_, output_step2 = net(g_step2, feature_step2)
# 需要的是output_step2[test_mask_step2] 

4.5.random forest分类

4.5.1.定义函数


def performance_evaluation(y_true, y_pred):
    tn, fp, fn, tp = confusion_matrix(y_true, y_pred).ravel() # y_true, y_pred
        
    accuracy = (tn+tp)/len(y_true)
    pos_acc = tp/sum(y_true).item()
    neg_acc = tn/(len(y_pred)-sum(y_pred).item()) # [y_true=0 & y_pred=0] / y_pred=0
    neg_recall = tn / (tn+fp) # [y_true=0 & y_pred=0] / y_true=0

    return neg_recall, neg_acc, pos_acc, accuracy

4.5.2.数据准备


x_step1 = output_step1.data.numpy()
y_step1 = labels_step1.data.numpy()
x_test = output_step2[test_mask_step2].data.numpy()
y_test = labels_step2[test_mask_step2].data.numpy()

4.5.3.训练模型并预测


clf = RandomForestClassifier(random_state = 0)
clf.fit(x_step1[train_idx_step1], y_step1[train_idx_step1])

y_train_pred = clf.predict(x_step1[train_idx_step1])
y_val_pred = clf.predict(x_step1[val_idx_step1])
y_test_pred = clf.predict
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值