Predicting trust and distrust with a signed GCN (Bitcoin-OTC)
Author: K3-Node Team
Backend: Multi-Backend
Dataset: BitcoinOTC
Description: In Bitcoin-OTC, users rate how much they trust each other, from -10 to +10.
Predicting trust and distrust with a signed GCN (Bitcoin-OTC)
In Bitcoin-OTC, users rate how much they trust each other, from -10 to +10. The edges are therefore signed: positive (trust) or negative (distrust). A signed GCN (Derr et al., 2018) keeps separate "friend" and "enemy" representations for every user, following balance theory (the enemy of my enemy is my friend), and predicts the sign of held-out edges.
Same model as PyG's examples/signed_gcn.py.
Install K3-Node, then choose a backend: "tensorflow", "torch" or "jax"
Load the data
All ratings of the 138 time steps are combined; ratings above 0 are positive edges, below 0 negative ones.
import keras
from keras import ops
from k3_node.datasets import BitcoinOTC
from k3_node.models import SignedGCN
from k3_node.training import gradient_step
dataset = BitcoinOTC("data/BitcoinOTC-1", edge_window_size=1)
edge_index = ops.concatenate([data.edge_index for data in dataset], axis=1)
rating = ops.concatenate([data.edge_attr for data in dataset], axis=0)
pos_edge_index = ops.take(edge_index, ops.where(rating > 0)[0], axis=1)
neg_edge_index = ops.take(edge_index, ops.where(rating < 0)[0], axis=1)
print(f"{pos_edge_index.shape[1]} positive and {neg_edge_index.shape[1]} negative edges")
Define the model
20% of each kind of edge is held out for testing. The node features are the 64 leading singular vectors of the signed adjacency matrix of the training edges.
model = SignedGCN(64, 64, num_layers=2, lamb=5)
train_pos_edge_index, test_pos_edge_index = model.split_edges(pos_edge_index)
train_neg_edge_index, test_neg_edge_index = model.split_edges(neg_edge_index)
x = model.create_spectral_features(train_pos_edge_index, train_neg_edge_index, num_nodes=dataset.num_nodes)
Train
The loss combines classifying edges as positive, negative or absent with a term that keeps friends close and enemies far apart in the embedding space (weighted by lamb).
def loss_fn():
z = model(x, train_pos_edge_index, train_neg_edge_index)
return model.loss(z, train_pos_edge_index, train_neg_edge_index)
model(x, train_pos_edge_index, train_neg_edge_index) # create the weights
optimizer = keras.optimizers.Adam(learning_rate=0.01, weight_decay=5e-4)
epochs = 101
for epoch in range(epochs):
loss = gradient_step(loss_fn, model.trainable_variables, optimizer)
if epoch % 10 == 0:
z = model(x, train_pos_edge_index, train_neg_edge_index)
auc, f1 = model.test(z, test_pos_edge_index, test_neg_edge_index)
print(f"Epoch {epoch:03d}: loss {loss:.4f}, AUC {auc:.4f}, F1 {f1:.4f}")