<jupyter_start><jupyter_text>Feature extraction from business relationship graphs by GCN We show the source codes used in the paper "Feature extraction from business relationship graphs by Graph Convolutional Networks" in this notebook.Although the paper uses actual bank data, we use fictitious data in this notebook for demonstration purposes. So, the execution results will differ from the paper.  1. Preparation  1.1. Generate dummy data<jupyter_code>!python generate_dummy_data.py<jupyter_output><empty_output><jupyter_text>1.2. Import packages and setup devices<jupyter_code>import os
import math
import random
import numpy as np
import pandas as pd
from sklearn import metrics
import networkx as nx
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import DataLoader
import dgl
import dgl.function as fn
from tqdm import tqdm_notebook as tqdm
def seed_everything(seed=1234):
    random.seed(seed)
    os.environ['PYTHONHASHSEED'] = str(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)
    torch.cuda.manual_seed(seed)
    torch.backends.cudnn.deterministic = True
seed = 123
seed_everything(seed)
torch.cuda.is_available()
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
device<jupyter_output><empty_output><jupyter_text>2. Train  2.1. Read target file<jupyter_code>df_target = pd.read_csv('data/train/target.csv', index_col='company_id')
df_target.head()<jupyter_output><empty_output><jupyter_text>2.2. Read business relationship graphs<jupyter_code>def make_node_feature(n_nodes, n_features):
    t1 = torch.eye(n_features)
    t2 = torch.zeros(n_nodes-n_features, n_features)
    t2[:, n_features-1] = 1
    return torch.cat([t1, t2])
graphs = []
for i in tqdm(df_target.index):
    try:
        Di_G = nx.read_weighted_edgelist(
            'data/train/{}.edgelist'.format(i), delimiter=',', nodetype=int, create_using=nx.DiGraph()
        )
        last_id = sorted(list(Di_G.nodes))[-1]
        for i in range(last_id):
            if not Di_G.has_node(i):
                Di_G.add_node(i)
        G = dgl.DGLGraph()
        G.from_networkx(Di_G, edge_attrs=['weight'])
        G.ndata['h'] = make_node_feature(G.number_of_nodes(), 8)
        G.edata['direction'] = torch.zeros(G.number_of_edges())
        src, dst = G.edges()
        weight = G.edata['weight']
        direction = torch.ones(G.number_of_edges())
        G.add_edges(dst, src, data={'weight': weight, 'direction': direction})
        G.to(device)
        graphs.append(G)
    except Exception as e:
        print(i)
        raise e
graphs = np.array(graphs)<jupyter_output><empty_output><jupyter_text>2.3. Define model<jupyter_code>class BiDirectionalGCN(nn.Module):
    def __init__(self, in_feats, out_feats, activation):
        super(BiDirectionalGCN, self).__init__()
        self.fc = nn.Linear(in_feats*3, out_feats)
        self.activation = activation

    def message_func(self, edges):
        weight = edges.data['weight'].view(-1, 1)
        messages = edges.src['h'] * weight
        return {'m': messages, 'direction': edges.data['direction']}
    
    def reduce_func(self, nodes):
        messages = nodes.mailbox['m']
        direction = nodes.mailbox['direction'].view(messages.size()[0], messages.size()[1], 1)
        h0 = torch.sum(messages * direction, dim=1)
        direction = (direction + 1) % 2  # 0=>1, 1=>0
        h1 = torch.sum(messages * direction, dim=1)
        return {'h0': h0, 'h1': h1}

    def node_apply_func(self, nodes):
        h = torch.cat([nodes.data['h'], nodes.data['h0'], nodes.data['h1']], dim=1)
        h = self.activation(self.fc(h))
        return {'h': h}
    
    def forward(self, g, h):
        g.ndata['h'] = h
        g.update_all(self.message_func, self.reduce_func)
        g.apply_nodes(func=self.node_apply_func)
        return g.ndata.pop('h')
class GATWithSelfLoop(nn.Module):
    def __init__(self, in_dim, out_dim, activation):
        super(GATWithSelfLoop, self).__init__()
        self.fc = nn.Linear(in_dim, out_dim, bias=False)
        self.attn_fc = nn.Linear(2 * out_dim, 1, bias=False)
        self.self_fc = nn.Linear(in_dim, out_dim, bias=False)
        self.self_attn_fc = nn.Linear(out_dim, 1, bias=False)
        self.activation = activation
        
    def node_apply_func(self, nodes):
        z_self = self.self_fc(nodes.data['h'])
        return {
            'z': self.fc(nodes.data['h']),
            'z_self': z_self,
            'e_self': F.leaky_relu(self.self_attn_fc(z_self))
        }
    
    def edge_attention(self, edges):
        z2 = torch.cat([edges.src['z'], edges.dst['z']], dim=1)
        e = F.leaky_relu(self.attn_fc(z2))
        self.attentions = e.clone().detach()
        return {'e': e}
    
    def message_func(self, edges):
        return {'z': edges.src['z'], 'e': edges.data['e']}
    
    def reduce_func(self, nodes):
        e_self = nodes.data['e_self'].view(-1, 1, 1)
        e = torch.cat([nodes.mailbox['e'], e_self], dim=1)
        alpha = F.softmax(e, dim=1)
        z_self = nodes.data['z_self']
        hidden_dim = z_self.size(1)
        z_self = z_self.view(-1, 1, hidden_dim)
        z = torch.cat([nodes.mailbox['z'], z_self], dim=1)
        h = self.activation(torch.sum(alpha * z, dim=1))
        return {'h': h}
    
    def forward(self, g, h):
        g.ndata['h'] = h
        g.apply_nodes(self.node_apply_func)
        g.apply_edges(self.edge_attention)
        g.update_all(self.message_func, self.reduce_func)
        return g.ndata.pop('h')
class Classifier(nn.Module):
    def __init__(self, in_dim, hidden_dim, n_classes):
        super(Classifier, self).__init__()
        self.layers = nn.ModuleList([
            BiDirectionalGCN(in_dim, hidden_dim, F.relu),
            GATWithSelfLoop(hidden_dim, hidden_dim, F.relu)
        ])
        self.fc1 = nn.Linear(hidden_dim*9, hidden_dim)
        self.activation = F.relu
        self.fc2 = nn.Linear(hidden_dim, n_classes)
        self.hidden_dim = hidden_dim
    
    def forward(self, g):
        h = g.ndata.pop('h')
        for conv in self.layers:
            h = conv(g, h)
        g.ndata['h'] = h
        features = []
        for graph in dgl.unbatch(g):
            node_features = graph.ndata['h'][0:7]
            mean = torch.mean(graph.ndata['h'][7:], 0).view(1, -1)
            max_values, _ = torch.max(graph.ndata['h'][7:], 0)
            max_values = max_values.view(1, -1)
            concat = torch.cat([node_features, mean, max_values], 0).view(1, -1)
            features.append(concat)
        tensor = torch.cat(features)
        return self.fc2(self.activation(self.fc1(tensor)))<jupyter_output><empty_output><jupyter_text>2.4. Fit<jupyter_code>labels = torch.tensor(df_target['target'].values).float()
epochs = 20
batch_size = 100
hidden_dim = 16
def collate(samples):
    graphs, labels = map(list, zip(*samples))
    batched_graph = dgl.batch(graphs)
    return batched_graph, torch.tensor(labels).to(device)
trainset = list(zip(graphs, labels))
data_loader = DataLoader(trainset, batch_size=batch_size, shuffle=True, collate_fn=collate)
model = Classifier(8, hidden_dim, 1).to(device)
loss_func = nn.BCEWithLogitsLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
for epoch in tqdm(range(epochs)):
    # Train
    model.train()
    epoch_loss = 0
    for iter, (bg, label) in enumerate(data_loader):
        prediction = model(bg).view(-1)
        loss = loss_func(prediction, label)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        epoch_loss += loss.detach().item()
    epoch_loss /= (iter + 1)
    print('Train loss:{}'.format(epoch_loss))<jupyter_output><empty_output><jupyter_text>3. Test  3.1. Read target file<jupyter_code>df_target = pd.read_csv('data/test/target.csv', index_col='company_id')
df_target.head()<jupyter_output><empty_output><jupyter_text>3.2. Read business relationship graph<jupyter_code>graphs = []
for i in tqdm(df_target.index):
    try:
        Di_G = nx.read_weighted_edgelist(
            'data/test/{}.edgelist'.format(i), delimiter=',', nodetype=int, create_using=nx.DiGraph()
        )
        last_id = sorted(list(Di_G.nodes))[-1]
        for i in range(last_id):
            if not Di_G.has_node(i):
                Di_G.add_node(i)
        G = dgl.DGLGraph()
        G.from_networkx(Di_G, edge_attrs=['weight'])
        G.ndata['h'] = make_node_feature(G.number_of_nodes(), 8)
        G.edata['direction'] = torch.zeros(G.number_of_edges())
        src, dst = G.edges()
        weight = G.edata['weight']
        direction = torch.ones(G.number_of_edges())
        G.add_edges(dst, src, data={'weight': weight, 'direction': direction})
        G.to(device)
        graphs.append(G)
    except Exception as e:
        print(i)
        raise e<jupyter_output><empty_output><jupyter_text>3.3. Predict<jupyter_code>total_count = len(df_target)
iteration_count = math.ceil(total_count / batch_size)
df_probs_list = []
for i in tqdm(range(iteration_count)):
    start = i * batch_size
    end = (i+1) * batch_size
    if end > total_count:
        end = total_count
    batch_graphs = dgl.batch(graphs[start:end])
    model.eval()
    prediction = model(batch_graphs).view(-1)
    probs = torch.sigmoid(prediction).to(torch.device('cpu')).view(-1).detach().numpy()
    df = df_target.iloc[start:end].copy()
    df['probability'] = probs
    df_probs_list.append(df)
df_probs = pd.concat(df_probs_list)
df_probs.head()<jupyter_output><empty_output><jupyter_text>3.4. Evaluation<jupyter_code>fpr, tpr, thresholds = metrics.roc_curve(df_probs['target'], df_probs['probability'])
print(metrics.auc(fpr, tpr))<jupyter_output>0.6317605276509386
{'fraction_non_alphanumeric': 0.1401378742668999, 'fraction_numerical': 0.013787426689988683, 'mean_word_length': 3.213263979193758, 'pattern_counts': {'":': 0, '<': 40, '<?xml version=': 0, '>': 43, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '10', 'dir': 'jupyter-structured-clean-dedup', 'id': '615632', 'license': 'MIT', 'n_tokens_mistral': 3507, 'n_tokens_neox': 3426, 'n_words': 648, 'path': 'train.ipynb', 'repo_name': 'marisakamozz/feature_extraction_from_bizgraph', 'size': '9719'}