<jupyter_start><jupyter_text>Node Classification<jupyter_code>import numpy as np
import pandas as pd
#import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import OneHotEncoder, LabelEncoder
#import tensorflow as tf
import torch
from torch_geometric.data import Data, DataLoader
import dgmc
from urllib.parse import urlparse
from torch_geometric.utils import to_networkx
import networkx as nx
import matplotlib.pyplot as plt
import funcs

%reload_ext autoreload
%autoreload 2<jupyter_output><empty_output><jupyter_text>Load DataLoading a days sample of data that was manually extracted from Hive. In this notebook we will just use the Sizmek data for node classification.<jupyter_code>zync = pd.read_csv("data/zync_session_tracking_raw_20210825.csv")
print(f"Zync data shape: {zync.shape}")

sizmek = pd.read_csv("data/sizmek_bidstream_raw_20210825.csv")
print(f"Sizmek data shape: {sizmek.shape}")

# Fix column headers
zync.columns = [i.split(".")[1] for i in zync.columns]
sizmek.columns = [i.split(".")[1] for i in sizmek.columns]

def make_smaller(df, frac):
    new_df = df.copy()
    new_df = new_df.sample(frac=frac, replace=False)
    return new_df

#zync = make_smaller(zync, 0.1)
#sizmek = make_smaller(sizmek, 0.1)
def append_z(x):
    return "zync::"+x

new_z = zync["session_id"].apply(append_z)
new_z.to_csv("data/zync_ids.csv")
def check_len(x):
    return len(str(x))

sizmek['user_id'].apply(check_len).value_counts()
sizmek.head(1)
zync.head(1)<jupyter_output><empty_output><jupyter_text>Transform Data  Create EdgesParse the base url domain for increased matching and then apply the functions defined up top to create the list of edge connections within the dataset.<jupyter_code>sizmek["url_domain"] = sizmek['url'].apply(
    lambda x: urlparse(x).netloc if pd.notnull(x) else x
)
sizmek_edges = funcs.connect_edges(sizmek, "url")
sizmek_edges.head(2)
sizmek['zeta_user_id'].value_counts()[:3]<jupyter_output><empty_output><jupyter_text>Create FeaturesUse a standard one-hot encoding method of select columns and transform to a new DataFrame<jupyter_code>feature_cols = ["account_id", "referrer_url", "city_code", 
                "state_code", "dma_code", "country_code"]

feature_enc = OneHotEncoder(handle_unknown="ignore")
sizmek_features = pd.DataFrame(
    feature_enc.fit_transform(sizmek[feature_cols]).toarray(), 
    columns=feature_enc.get_feature_names(feature_cols)
)
print(f"Unique features: {sizmek_features.shape[1]}")

label_enc = LabelEncoder()
labels = label_enc.fit_transform(sizmek["zeta_user_id"])
labels = torch.tensor(labels, dtype=torch.long)
print(f"Unique labels: {max(labels)}")

missing_ratio = sizmek["zeta_user_id"].isna().sum() / len(sizmek)
print(f"Percent of labels missing: {missing_ratio:.2%}")<jupyter_output>Unique features: 596
Unique labels: 67
Percent of labels missing: 29.68%<jupyter_text>Create Custom Graph ClassWe will use PyTorch Geometric to create a graph by subclassing their standard graph class `torch_geometric.data.Data`. The following are the basics needed to train in this situation:- x: the matrix of features- edge_index: set of connecting edges for source/target- y: the labels for classifying the nodes- train_mask: boolean mask for specifying which nodes to train on<jupyter_code>class MyData(Data):
    def __init__(self, x, edge_index, y, train_mask):
        super(MyData, self).__init__()
        self.x = x
        self.edge_index = edge_index
        self.y = y
        self.train_mask = train_mask
        
    @property
    def num_features(self):
        return self.x.shape[1]
    
    @property
    def num_classes(self):
        return self.y.__len__()
        

sizmek_data = MyData(
    x=torch.tensor(sizmek_features.values, dtype=torch.float),
    edge_index=torch.tensor(sizmek_edges[['source','target']].T.values, dtype=torch.long),
    y=labels,
    train_mask=sizmek["zeta_user_id"].isna()
)<jupyter_output><empty_output><jupyter_text>ModelingThe model will be a combination of graph convolution layers and tanh activations. Define Model<jupyter_code>import pytorch_lightning as pl
from torch.nn import Linear
from torch_geometric.nn import GCNConv

class GCN(pl.LightningModule):
    def __init__(self, dataset):
        super(GCN, self).__init__()
        torch.manual_seed(0)
        self.conv1 = GCNConv(dataset.num_features, 4)
        self.conv2 = GCNConv(4, 4)
        self.conv3 = GCNConv(4, 2)
        self.classifier = Linear(2, dataset.num_classes)
        
        self.in_channels = dataset.num_features
        self.out_channels = dataset.num_classes

    def forward(self, x, edge_index, edge_attr=None):
        print("x: ", x.shape)
        print("edge_index: ", edge_index.shape)
        h = self.conv1(x, edge_index)
        h = h.tanh()
        h = self.conv2(h, edge_index)
        h = h.tanh()
        h = self.conv3(h, edge_index)
        h = h.tanh()  # Final GNN embedding space.
        
        # Apply a final (linear) classifier.
        out = self.classifier(h)

        return out, h
    
model = GCN(sizmek_data)<jupyter_output><empty_output><jupyter_text>Visualize EmbeddingsBefore training we can go ahead and do a single pass of the network and visualize the outputs with T-SNE. The ideal outcome is that the different colors will become linearly seperable but for now they will mostly be bunched together due to the random activations of the weights.<jupyter_code>_, h = model(sizmek_data.x, sizmek_data.edge_index)
print(f"Embedding shape: {list(h.shape)}")

funcs.visualize(h, color=sizmek_data.y)

#G = to_networkx(d, to_undirected=True)
#visualize(G, color=d.y)<jupyter_output>Embedding shape: [10000, 2]<jupyter_text>Train<jupyter_code>criterion = torch.nn.CrossEntropyLoss()  # Define loss criterion
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)  # Define optimizer
for epoch in range(5):
    #loss, h = train(sizmek_data)
    #model.train()
    optimizer.zero_grad()  # Clear gradients
    out, h = model.forward(sizmek_data.x, sizmek_data.edge_index)  # Perform a single forward pass
    loss = criterion(out[sizmek_data.train_mask], sizmek_data.y[sizmek_data.train_mask]) # calc loss on training nodes
    loss.backward()  # Derive gradients
    optimizer.step()  # Update parameters based on gradients
    print(f'Epoch: {epoch:03d}, Loss: {loss:.4f}')
    #if epoch % 5 == 0:
    #    visualize(h, color=d.y, epoch=epoch, loss=loss)
    #    time.sleep(0.3)<jupyter_output>x:  torch.Size([10000, 596])
edge_index:  torch.Size([2, 2212814])
Epoch: 000, Loss: 8.8542
x:  torch.Size([10000, 596])
edge_index:  torch.Size([2, 2212814])
Epoch: 001, Loss: 8.7516
x:  torch.Size([10000, 596])
edge_index:  torch.Size([2, 2212814])
Epoch: 002, Loss: 8.6586
x:  torch.Size([10000, 596])
edge_index:  torch.Size([2, 2212814])
Epoch: 003, Loss: 8.5728
x:  torch.Size([10000, 596])
edge_index:  torch.Size([2, 2212814])
Epoch: 004, Loss: 8.4925<jupyter_text>Evaluate<jupyter_code>test_acc = funcs.test(sizmek_data, model)
print(f'Test Accuracy: {test_acc:.4f}')<jupyter_output>Correct predictions: 0
Test Accuracy: -0.0000
{'fraction_non_alphanumeric': 0.11444600280504909, 'fraction_numerical': 0.027489481065918652, 'mean_word_length': 4.673031026252984, 'pattern_counts': {'":': 0, '<': 33, '<?xml version=': 0, '>': 33, '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': '9', 'dir': 'jupyter-structured-clean-dedup', 'id': '148443', 'license': 'MIT', 'n_tokens_mistral': 2678, 'n_tokens_neox': 2534, 'n_words': 603, 'path': 'gnn_v2.ipynb', 'repo_name': 'cipher982/GNN-playground', 'size': '7130'}