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
-
先得到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的节点。
-
将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