PyTorch Geometric (PyG) ile çizgesel sinir ağları

Sinir ağları, metin, resim ve ses dosyaları gibi yapılandırılmış veriler için tasarlanmıştır. Ancak, klasik sinir ağlarının işleyemeyeceği çok sayıda düzensiz yapıda veri vardır. Bu tür veriler için PyTorch Geometric’i kullanabiliriz. PyTorch Geometric çizgeler ve örgüler gibi yapılandırılmamış verileri işleyebilen çizgesel sinir ağlarının çalıştırılması için geliştirilmiş bir kütüphanedir.

Kurulum

Not: PyTorch Geometric’i kurabilmeniz için önce PyTorch yüklenmelidir. PyTorch’un nasıl kurulacağına ilişkin talimatlar için PyTorch’u yükleme kılavuzuna başvurabilirsiniz. PyTorch Geometric’i yüklemek için lütfen şu adımları izleyin:

  1. Şu anda yüklü olan PyTorch sürümünü kontrol edin ve bir ortam değişkeninde saklayın. Bunu yapmak için aşağıdaki komutu çağırmanız yeterlidir:

    export TORCH=$(python -c "import torch; print(torch.__version__)")
    
  2. Şu anda yüklü olan cudatoolkit sürümünü kontrol edin ve bunu bir ortam değişkeninde saklayın.

    1. İlk önce kurduğunuz CUDA sürümünü kontrol edin:

      python -c "import torch; print(torch.version.cuda)"
      >> 11.1
      
    2. Yukarıda gösterilen sürüme bağlı olarak aşağıdaki ortam değişkenini ayarlayın

         export CUDA=cu111 # for cuda 11.1
      
      Bu öğreticinin yazıldığı sırada cuda sürümü için olası değerler cpu (eğer cudatoolkit kurulu değilse), cu92, cu101, cu102, cu110 veya cu111 şeklindedir.
      
  3. Gerekli kütüphaneleri aşağıdaki çağrılarla kurun:

    pip install torch-scatter -f https://pytorch-geometric.com/whl/torch-${TORCH}+${CUDA}.html
    pip install torch-sparse -f https://pytorch-geometric.com/whl/torch-${TORCH}+${CUDA}.html
    pip install torch-cluster -f https://pytorch-geometric.com/whl/torch-${TORCH}+${CUDA}.html
    pip install torch-spline-conv -f https://pytorch-geometric.com/whl/torch-${TORCH}+${CUDA}.html
    pip install torch-geometric
    

Veri - çizge veri yapısı

PyG tanımları, çizge veri kümelerini tanımlamak için torch_geometric.data.Data sınıfını kullanır. Bu türdeki her nesne bir çizgeyi temsil eder. Bir Data nesnesi birçok veri üyesi içerebilir. İşte içerebileceği bazı önemli veri üyeleri:

  • edge_index: COO formatında çige bağlantı bilgisini tutan Torch tensörü. Bu, [2, number_of_edges] boyutunda olacak. Her sütun bir bağlantıyı temsil eder.

  • x: çizgedeki tüm düğümlerin özellik vektörleri. [num_nodes, num_node_features] şeklinde bir tensör

  • edge_attr: [num_edges, num_edge_features] şeklinde bağlantı özelliklerinin saklandığı matris

  • y: eğitmek için etiketler (isteğe bağlı şekle sahip olabilir), ör. düğüm düzeyindeki şekil [num_nodes, *] hedefleri veya şekili [1, * ] olacak şekilde çizge düzeyindeki hedefler

Yukarıdaki niteliklerin tümü bir Data nesnesi oluşturmak için gerekli değildir. Ek olarak, gerekirse nesneyi, örneğin bağlantı ağırlıkları gibi kendi yarattığımız özelliklerimizle genişletebiliriz.

Aşağıdaki örnekte, dört düğümü ve üç bağlantısı olan yönsüz bir çizge tanımlıyoruz. Ayrıca çizgedeki tüm düğümler için özellik vektörleri ekliyoruz:

import torch
from torch_geometric import Data

edge_index = torch.tensor([[1, 2, 0, 1, 2, 0],
               [2, 1, 1, 0, 0, 0]])
graph = Data(edge_index = edge_index)
print(f"Çizge: {graph}")

graph.x = torch.randn((4,5))
print(f"Düğüm özellikleri ekledikten sonraki çizge: {graph}")
print(f"çizgenin {graph.num_nodes} düğümü ve {graph.num_edges} bağlantısı vardır")

Veri objelerinin birçok faydalı yardımcı fonksiyonu vardır:

print(f"`Data` nesnesinde hangi verilerin olduğunu kontrol edin: {data.keys}")

print(f"düğüm özelliği vektörleri\n {data['x']}")

print(f"edge_attr verilerde mi? {'edge_attr' in data}")

print(f"düğüm özellikleri sayısı {data.num_node_features}")

print(f"Çizge izole düğümler içeriyor mu? {data.contains_isolated_nodes()}")

print(f"Çizge kendi kendine döngüler içeriyor mu? {data.contains_self_loops()}")

print(f"Çizge yönlendirilmiş mi? {data.is_directed()}")

Mevcut GNN katmanlarını kullanarak bir model oluşturma

PyG, mevcut GNN katmanlarının kapsamlı bir koleksiyonuyla birlikte gelir. Bu katmanları kendi modellerimizi oluşturmak için kullanabiliriz. Aşağıdaki örnekte, düğüm sınıflandırma - çizgedeki düğümleri (köşeler) sınıflandırma görevini yerine getirmek için bazı bilinen GNN’leri kullanarak bir sinir ağı modeli oluşturuyoruz. Modelimiz, her düğüm için bir özellik vektörü ile birlikte bir çizge alacak ve bu düğümleri 7 olası sınıftan birine sınıflandıracaktır.

Veri kümesi

PyG’nin sağladığı veri kümelerinden birini kullanacağız. Sınıflandırma görevlerini değerlendirmek için bu tür eğitimlerde sıklıkla kullanılan Cora veri setini kullanıyoruz.

Bu veri kümesini yüklerken, root parametresinde veri kümesini indirmek istediğimiz konumu belirtiyoruz. Bu durumda da name parametresinde istediğimiz veri setinin adını belirtmemiz gerekiyor. Tüm veri kümeleri bu parametreyi gerektirmez. Veri kümelerinin gereksinimlerini PyG’nin belgelerinde kontrol edebilirsiniz.

dataset nesnesi, veri kümesi içindeki tüm çizgelerin bir listesini içerir. Bizim durumumuzda, Cora veri seti tek bir çizge içerir.

from torch_geometric.data import DataLoader
from torch_geometric.datasets import Planetoid

dataset = Planetoid(root='data', name='Cora') # dataset bir çizge listesi içerir

print(f"dataset'de {len(dataset)} çizge var")

print(dataset[0])
cora = dataset[0]

num_node_features =  cora.num_features
# Çizgedeki düğüm sınıflarının sayısı
num_classes = cora.y.max().item()+1

Model oluşturma

Şimdi hem çizge sinir ağı katmanlarını hem de normal bir sinir ağı katmanını içeren bir sinir ağı modeli oluşturuyoruz. Bu model, torch.nn.Module sınıfından miras alan bir sınıf olacak ve normal bir sinir ağı ile tamamen aynı şekilde çalışacak, yani, düğümlerin (x tensörü ile temsil edilen) özelliklerini alacak ve bu özellikleri kullanarak sınıflandırmalar yapacaktır. Bizim modelimiz ile normal bir sinir ağı modeli arasındaki tek fark, bizim modelimize çizge sinir ağı katmanları ekleyeceğiz. Bu katmanlar, eğitim sırasında düğümlerin özellik vektörlerinin yanı sıra çizgenin bağlantı bilgilerini de kullanacak.

__init__ fonksiyonunda iki GNN katmanı ve bir lineer katmanın yanı sıra iki aktivasyon fonksiyonu ekliyoruz. GNN katmanları, çizge bağlantı bilgilerinin yanı sıra düğümlerin özelliklerini alırken, doğrusal katman yalnızca düğümlerin özellik vektörlerini alacaktır. Başka bir deyişle, doğrusal katman, özellik vektörlerini çizge yapısı hakkında herhangi bir bilgi kullanmadan işleyecektir.

İleri fonksiyonuna bir Data nesnesi iletiriz ve ondan düğüm özelliklerini (data.x) ve çizgenin bağlantı bilgilerini (data.edge_index) çıkarırız. Unutulmamalıdır ki düğüm özelliklerini GNN katmanlarına geçirdiğimizde bağlantı bilgisini de iletmiş oluyoruz. Bunun nedeni, bu katmanların işlemleri sırasında bağlantı bilgilerini kullanmasıdır.

Grafiği GNN’lerden geçirdikten sonra, düğüm özelliklerini doğrusal bir katmandan geçiriyoruz. Grafiğin bağlantı bilgilerini geçmediğimize dikkat edin. Son olarak, bir log-softmax aktivasyonu kullanarak her düğüm için 7 elemanlık bir satır olacak olan sonuçları döndürüyoruz.

import torch_geometric.nn as pyg_nn
import torch.nn as nn

class GNN(nn.Module):
    def __init__(self, in_features, num_hidden_feats, num_classes):
        super(GNN, self).__init__()
        # ModuleList, sinir ağı katmanlarının bir listesini tutar
        self.gnn_layers = nn.ModuleList()
        # Bu "Graph Convolutional Network" katmanı, in_feature uzunluğundaki özellik vektörlerini alacak ve her düğüm için num_hidden_feats uzunluğunda özellik vektörleri üretecektir.
        self.gnn_layers.append(pyg_nn.GCNConv(in_features, num_hidden_feats))
        # Bu "Çizge Dikkat Ağı" katmanı, hidden_layer_features uzunluğundaki özellikleri alacak ve her düğüm için hidden_layer_features uzunluğunda vektörler üretecektir.
        self.gnn_layers.append(pyg_nn.GATConv(num_hidden_feats, num_hidden_feats))
        # Bu, sıradan bir doğrusal sinir ağı katmanıdır.
        self.lin = nn.Linear(num_hidden_feats, num_classes)
        self.relu = nn.ReLU()

    # Bir çizge yapısı içeren `Data` nesnesini ileri işlevine ileteceğiz.
    def forward(self, data):
        # Düğüm özelliklerini ve bağlantı bilgisi tensörlerini "data" nesnesinden çıkarıyoruz
        node_features, edge_index = data.x, data.edge_index
        # Özellik vektörlerini ve bağlantı bilgilerini GNN katmanına aktarıyoruz. GNN katmanı, işlem sırasında bağlantı bilgilerini kullanacaktır.
        out_node_features = self.gnn_layers[0](node_features, edge_index)
        # GNN katmanı, güncellenmiş düğüm özelliği vektörlerini döndürür
        out_node_features = self.relu(out_node_features)
        out_node_features = self.gnn_layers[1](out_node_features, edge_index)
        out_node_features = self.relu(out_node_features)
        # Düğüm özellik vektörlerini doğrusal katmana geçiriyoruz. `self.lin` bir GNN katmanı olmadığı için bağlantı bilgisini iletmemize gerek olmadığına dikkat edin.
        out_node_features = self.lin(out_node_features)
        return out_node_features

num_hidden_feats = 128

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

model = GNN(num_node_features, num_hidden_feats, num_classes).to(device)
print(model)

Optimize edici ve kayıp

Bir Adam optimize edici ve bir negatif log-olasılık kaybı fonksiyonu kullanıyoruz. Optimize edici, parametreleri Adam stratejisine göre güncellemeyi yönetecek ve kayıp fonksiyonu, modeldeki eğitilebilir parametrelerin kayıplarını ve gradyanlarını hesaplamak için kullanılacaktır.

optimizer = torch.optim.Adam(model.parameters(), lr = 0.01)
loss_function = nn.functional.nll_loss

Eğitim döngüsü

Eğitim döngüsü, PyTorch ile oluşturulmuş normal bir sinir ağının eğitim döngüsüne tam olarak benziyor. Her eğitim adımında, veri kümesini modelden geçiririz ve model her düğüm için bir puan vektörü döndürür. Ardından, bu puanların kaybını hesaplıyoruz ve kaybı, model parametrelerinin gradyanlarını hesaplamak için kullanıyoruz. Son olarak, hesaplanan gradyanları kullanarak modelin parametrelerini güncellemek için optimize ediciyi kullanıyoruz.

Eğitim sırasında, verilerimizin bir kısmını eğitim için, bir kısmını da test için kullanmak istiyoruz. Diğer bir deyişle, kalan düğümlerin sınıflarını gizli tutarken sadece bazı düğümlerin sınıflarını eğitim için kullanmak istiyoruz. Ancak eğitim örneklerinin çıktılarını hesaplamak için çizgenin tamamı gerektiğinden, çizgenin tamamını modele aktarmamız gerekiyor. Cora çizgesindeki train_mask özelliğini kullanarak verinin eğitim kısmını alıyoruz. PyG tarafından sağlanan tüm çizgelerin eğitim maskelerine sahip olmadığına dikkat edilmelidir.

epochs = 100
for epoch in range(epochs):
    # `Data` nesnesini modele geçiriyoruz. Model, güncelleme işleminden sonra düğümlerin özellik vektörlerini döndürür.
    y_score = model(cora)
    # Kaybı hesaplamak için eğitim kümesindeki düğümlerin yalnızca özellik vektörlerini seçmek için `train_mask` kullanıyoruz.
    y_score_train = y_score[cora.train_mask]
    # Ayrıca, yalnızca eğitim kümesindeki düğümlerin etiketlerini seçmek için `train_mask` kullanırız.
    y_train = cora.y[cora.train_mask]
    # Kaybı hesaplıyoruz, model parametrelerine göre kaybın gradyanlarını hesaplıyoruz ve bunları güncellemek için optimize ediciyi kullanıyoruz.
    loss = loss_function(y_score_train, y_train)
    loss.backward()
    optimizer.step()
    optimizer.zero_grad()
    if epoch % 20 == 0:
        print(f"Epoch {epoch}: loss {loss}")

Test döngüsü

Çizgeler üzerinde öğrenme prosedürünü test ediyoruz, bu düzenli veri kümelerini kullanmaya benzer. Test verilerini modelden geçireceğiz, tahminler yapacağız ve doğru tahminlerin sayısını sayacağız. Test verilerini elde etmek için Cora veri seti ile sağlanan test_mask özelliğini kullanıyoruz.

with torch.no_grad():
    y_score = model(cora)[cora.test_mask]
    prediction = y_score.argmax(dim=1)
    score =  prediction.eq(cora.y[cora.test_mask]).sum().item()
    print(f"Final accuracy = {100*score/cora.test_mask.sum()}")

GNN katmanı oluşturma - mesaj geçiş arayüzü (message passing interface)

Teori

Önceki örnekte, çizgeleri işleyebilen ve düğüm sınıflandırmasını gerçekleştirebilen bir makine öğrenimi modeli oluşturduk. Ancak, zaten var olan çizge sinir ağı katmanlarını kullandık. Aşağıdaki örnekte kendi GNN katmanımızı oluşturacağız ve bunu çizge sınıflandırması yapacak bir modelde kullanacağız.

Konvolüsyonları düzensiz verilere (örneğin çizgeler) genelleştirmeye mesaj geçişi (message passing) denir. Mesaj geçiş şeması, \(\mathbf{x} *i^{k}\) ifadesinin i düğümünün k katmanındaki özellik vektörü olduğu göz önüne alındığında aşağıdaki gibi ifade edilebilir. \(\mathbf{e}_{i,j}\), \((i,j)\) bağlantıyla ilişkili isteğe bağlı bir özellik vektörüdür.

\[\mathbf{x}_i^{(k)} = \gamma^{(k)} \left( \mathbf{x}\ *i^{(k-1)}, \square*\ {j \in \mathcal{N}(i)} \, \phi^{(k)}\left(\mathbf{x}_i^{(k-1)}, \mathbf{x}\ *j^{(k-1)},\mathbf{e}*\ {j,i}\right) \right)\]

\(\square\) permütasyon değişmez bir fonksiyon olduğunda (işlenenlerin sırası önemli değildir), toplam, maksimum veya ortalama gibi fonksiyonlar toplaşma fonksiyonu olarak adlandırılır. \(\gamma\) ve \(\phi\) türevlenebilir fonksiyonlardır. (örneğin doğrusal sinir ağı katmanları.)

Başka bir deyişle, \(k\) katmanından mesaj geçtikten sonra bir \(i\) düğümünün özellik vektörünü hesaplamak için aşağıdaki adımları yaparız:

  1. \(i\) düğümünün gelen her \(j\) komşusu için bir “mesaj” üretirken \(\phi\) fonksiyonunu uygularız. \(\phi\) fonksiyonu, \(i, j\)’nin özellik vektörlerini ve isteğe bağlı olarak \((i,j)\) bağlantısının özellik vektörünü kullanır.

  2. \(\square\) fonksiyonunu kullanarak \(i\) düğümüne gelen tüm mesajları tek bir vektörde topluyoruz. \(\square\) fonksiyonu, tüm mesajların toplamı, tüm mesajların ortalaması veya maksimum mesaj olabilir. Bu, \(i\) düğümüne gönderilen tüm mesajların tek bir temsilini oluşturacaktır.

  3. Son olarak, \(\gamma\) dönüşümünü mesajların toplu gösterimi ve düğümün kendisinin gömülmesi için uygularız. Nihai çıktı, düğümün yeni özellik vektörü olacaktır.

Torch_geometric.nn.MessagePassing, kendisini miras alan sınıfların yukarıda açıklanan prosedürü kolaylıkla uygulamasına izin veren bir arayüzdür. Aşağıdaki fonksiyonlar bu özelliği sağlar:

  • MessagePassing(aggr="add", flow="source_to_target", node_dim=-2): aggr parametresi, toplaşma şemasını(\(\square\)) ("add", "sum" veya "max") tanımlar ve flow, mesaj akışının bir uç kaynağın kaynağından hedefe mi yoksa tam tersi mi olduğunu belirler.

  • MessagePassing.propagate(edge_index, **kwargs): bu fonksiyon mesaj geçirme prosedürünü gerçekleştirecektir. İletileri oluşturmak ve yerleştirmeleri güncellemek için gerekli olan uç bağlantı bilgilerini (edge_index) ve diğer tüm verileri (ör. düğüm özellik vektörleri x, bağlantı özellik vektörleri edge_attr, vb.) alır ve her biri için bir vektör içeren bir matris döndürür. propogate() aşağıdaki üç işlevi çağırır:

  1. MessagePassing.message(...) : Bu fonksiyon, yukarıdaki formüldeki \(\phi\) fonksiyonunu temsil eder. propagate() fonksiyonuna iletilen tüm parametreleri alır ve isteğe bağlı olarak, grafiğin bağlantılarının kaynağına ve hedefine eşlenen özellik vektörlerinden de geçirelebilir. Detaylandırmak gerekirse, propagate() fonksiyonuna köşe özellikleri, çizgedeki her düğüm için bir satır, içeren bir matristen geçilmişse, örnek olarak node_feats => tensor([num_nodes, num_feats]) matrisi, ve message() fonksiyonuna yapılan çağrı node_feats_i parametresini içeriyorsa, o zaman node_feats_i, [sayı_edgeleri, sayı_feats] boyutunda bir matris olur ve node_feats_i[a] ve node_feats[edge_index[1][a] eşdeğer olur. Başka bir deyişle, bu a bağlantısının hedef düğümüne ait node_feats satırıdır. Öte yandan, yapılan çağrıya, bir node_feats_j parametresi iletilirse, o zaman node_feats matrisinin eşlemelerini içerecek, ancak bağlantıların kaynaklarına dayalı olacaktır. Programcı, mesajları oluşturmak için propagate() fonksiyonuna iletilen diğer parametrelerin yanı sıra bu fonksiyonları kullanabilir. Bu fonksiyon, her bağlantı için bir satır içeren bir matris, msj, döndürmelidir, burada msgs[a] satırı, bağlantı a’nın hedef düğümüne gönderilen bir mesaj, yani edge_index[1][a] düğümüne gönderilen bir mesaj olacaktır.

  2. MessagePassing.aggregate(msgs, ...): bu fonksiyon, message() fonksiyonu tarafından döndürülen tüm mesajları alacak ve yukarıdaki formüldeki \(\square\) fonksiyonunu uygulayacaktır. Yani, mesajları her köşe için tek bir vektörde toplar (toplar, maksimumlarını bulur veya ortalamalarını bulur) ve düğüm başına bir son vektör içeren matrisi döndürür.

  3. MessagePassing.update(aggr_out, ...): Bu fonksiyon, propagate() öğesine iletilen tüm parametrelerin yanı sıra her bir köşe için ileti toplaşmasının sonucunu içeren propagate() öğesinin döndürdüğü matrisi alır ve yukarıdaki formülasyondaki \(\gamma\) dönüşümü ve yayılma sürecinin son çıktısını döndürür.

Aşağıdaki şekil, parametre olarak grafiğin bağlantı bilgilerini (edge_index) ve ayrıca her düğüm için özellik vektörlerini içeren bir matrisi (node_features) alan propagate() fonksiyonuna yapılan bir çağrıyı gösterir.

/assets/pytorch-education/mp.png

Dataset - veri kümesi

Çoklu çizgelere sahip bir veri seti kullanacağız ve çizge sınıflandırması yapacağız.

from torch_geometric.datasets import TUDataset

dataset = TUDataset(root='data', name='ENZYMES')

print(f"Bu veri kümesinde {len(dataset)} çizge var ")

Veri yükleyiciler (Dataloaders)

Bu veri seti büyük olduğu için PyG tarafından sağlanan DataLoader mekanizmasını kullanacağız. PyTorch DataLoader sınıfına benzer şekilde davranır, ancak özellikle torch_geometric.data.Dataset sınıfı için modifiye edilmiştir ve veri kümelerini çoklu çizgelere bölümlemeyi işler. Eğitim verileri için bir veri yükleyici ve test verileri için bir tane oluşturacağız. batch_size parametresi, parti başına kaç numunenin yükleneceğini belirler.

from torch_geometric.data import DataLoader

# Train_loader eğitmek için çizgelerin %80'ini kullanacak ve test_loader kalan %20'yi test için kullanacak
# batch_size, puanları hesaplarken sınıfları kullanılacak düğüm sayısını belirler
train_loader = DataLoader(dataset[:int(data_size * 0.8)], batch_size=64, shuffle=True)
test_loader = DataLoader(dataset[int(data_size * 0.8):], batch_size=64, shuffle=True)

train_iter = iter(train_loader)
batch = train_iter.next()
print(batch)
print(f"Toplu iş {batch.x.shape[0]} düğümü içermesine rağmen, yalnızca {batch.y.shape[0]} etiketi vardır (çizge sayısı).")
Batch(batch=[2083], edge_index=[2, 7694], ptr=[65], x=[2083, 3], y=[64])
Toplu  2083 düğümü içermesine rağmen, yalnızca 64 etiketi vardır (çizge sayısı).

GNN katmanı tanımlama

Şimdi, önceki örnekte kullandığımız GCN katmanına matematiksel olarak eşdeğer bir GNN katmanı tanımlıyoruz. Katmanı tanımlamak için mesaj geçiş arayüzünü kullanacağız.

Yapıcıda, toplaşma fonksiyonunun “toplam” olarak istediğimizi ve mesajların bir bağlantının kaynağından hedefine akması gerektiğini belirtiriz. Ayrıca tek bir doğrusal katman ekliyoruz.

İleri fonksiyonunda , düğüm özelliklerini doğrusal katmandan geçiririz, sonra dönüştürülmüş düğüm özellikleri (node_feats: tensor([num_nodes, in_channels])) ve bağlantı bilgileri ile propagate() fonksiyonunu çağırırız. Yayma fonksiyonu önce message() fonksiyonunu çağırır ve mesaj fonksiyonu node_feats_j parametresine sahip olduğundan, node_feats matrisi, node_feats_j üretmek için çizgedeki tüm bağlantıların kaynaklarıyla eşleştirilir. Bu, node_feats_j[a] == node_feats[edge_index[0][a]] anlamına gelir.

i ve j düğümleri arasındaki a bağlantısına karşılık gelen her node_feats_j[a] öğesi için, message() fonksiyonu node_feats_j[a] * 1/( sqrt(degree(i) değerini döndürür. )) * sqrt(derece(j)))) olarak ifade edilir.

Daha sonra, aggregate() fonksiyonu otomatik olarak çağrılır ve message() döndürdüğü matris üzerinde toplaşma işlemi olarak add yapar. Son olarak, update() fonksiyonu çağrılacak ve aggregate() fonksiyonunun döndürdüğü tensörden geçirilecektir. update() döndürdüğü tensör, propagate() fonksiyonu tarafından döndürülecektir.

import torch_geometric.utils as pyg_utils
class GCN(pyg_nn.MessagePassing):
    def __init__(self, in_channels, out_channels):

         # Bu katmanın toplaşma fonksiyonu olarak toplama kullanacağını ve mesajların bir bağlantnın kaynağından ucun hedefine gideceğini belirtiyoruz.
        super(GCN, self).__init__(aggr='add', flow='source_to_target')
        # Katmanda kullanacağımız doğrusal bir sinir ağı ekliyoruz.
        self.lin = nn.Linear(in_channels, out_channels)

    def forward(self, x, edge_index):
        # Katmanımızdan bir girdi geçtiğinde "forward" fonksiyonunu çağrılır. "x" düğüm özelliklerini ve "edge_index" bağlantı bilgilerini alacağız

        # bitişiklik matrisine kendi kendine döngüler ekleyin.
        edge_index, _ = pyg_utils.add_self_loops(edge_index)

        # Düğüm özelliği matrisini dönüştür
        node_feats = self.lin(x)

        # Yayılma çağrısı mesaj geçişini yürütecek
        # Önce 'message()' çağrılır, ardından 'aggregate()', ardından 'update()' ve 'update()' çıktılanır
        # 'propagate()' öğesine iletilen tüm parametreler, çağırdığı diğer üç fonksiyonlara iletilecektir.
        return self.propagate(edge_index, node_feats=node_feats)

    def message(self, node_feats_j, edge_index, size):
        # Fonksiyon argümanlarına `node_feats_j` parametresini eklediğimizde, `node_feats`in çizgenin tüm bağlantılarının kaynakları üzerindeki eşlemesi hesaplanacak ve `node_feats_j` içine yerleştirilecektir.
        # node_feats_j şekili =>[num_edges, out_channels]
        row, col = edge_index

        # GCN belgesine göre normları hesaplayın
        deg = pyg_utils.degree(row, size[0], dtype=node_feats_j.dtype)
        deg_inv_sqrt = deg.pow(-0.5)
        norm = deg_inv_sqrt[row] * deg_inv_sqrt[col]
        # Döndürülen matris, çizgedeki her bağlantı için bir mesaj içerir.
        return norm.view(node_feats_j.shape[0], 1)*node_feats_j

    def update(self, aggr_out):
        # "message()" fonksiyonu tarafından döndürülen matristeki mesajlar toplanır ve her düğüm için tek bir kümelenmiş vektör oluşturmak üzere "aggr_out" içine yerleştirilir.
        # aggr_out şekili => [N, out_channels]
        return aggr_out

Model oluşturma

Oluşturduğumuz GNN katmanını eksiksiz bir modelde kullanacağız. Önceki örnekte kullandığımıza benzer bir model kullanacağız, ancak bir çizgideki düğümlerin tüm özellik vektörlerini tek bir özellik vektöründe toplayacak ek bir havuz fonksiyonu ekleyeceğiz. Bunun nedeni, bu modeli çizge sınıflandırması için kullanmak istememizdir.

Havuzlama fonksiyonunda , data nesnesinin içindeki batch üyesini kullanırız. Bu üye yalnızca, örnekleri almak için bir DataLoader kullandığımızda eklenir. batch tensörü, data nesnesindeki her düğümün çizge kimliğini içerir.

import torch
class GNN(nn.Module):
    def __init__(self, in_features, num_hidden_feats, num_classes):
        super(GNN, self).__init__()
        self.gnn_layers = nn.ModuleList()
        self.gnn_layers.append(GCN(in_features, num_hidden_feats))

        self.gnn_layers.append(pyg_nn.GATConv(num_hidden_feats, num_hidden_feats))
        self.lin = nn.Linear(num_hidden_feats, num_classes)
        self.relu = nn.ReLU()
        self.log_softmax = nn.LogSoftmax(dim=1)

    def forward(self, data):
        # Bağlantı bilgilerine ('edge_index') ve düğüm özelliklerine ('x') ek olarak, 'batch' tensörünü de çıkarıyoruz. Bu tensör, partideki her düğümü ait olduğu çizgeye eşler.
        node_features, edge_index, batch = data.x, data.edge_index, data.batch
        # Bu katmanı, GCNConv katmanını kullandığımız şekilde kullanıyoruz.
        out_node_features = self.gnn_layers[0](node_features, edge_index)
        out_node_features = self.relu(out_node_features)
        out_node_features = self.gnn_layers[1](out_node_features, edge_index)
        out_node_features = self.relu(out_node_features)
        # Bu bir çizge sınıflandırma problemi olduğundan, havuzlama fonksiyonunu kullanarak her bir çizgeye ait düğümlerin tüm özellik vektörlerini tek bir vektörde toplayacağız. Havuzlama fonksiyonu, her düğümün çizge kimliklerini içeren "batch" tensörünü kullanır.
        out_graph_features = pyg_nn.global_mean_pool(out_node_features, batch)
        out_graph_features = self.lin(out_graph_features)
        return self.log_softmax(out_graph_features)

num_hidden_feats = 128
num_node_features = dataset.num_node_features
num_classes = dataset.num_classes

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

model = GNN(num_node_features, num_hidden_feats, num_classes).to(device)
print(model)

Eğitim döngüsü

Kullanacağımız eğitim döngüsü, birkaç temel fark dışında son örnekte kullandığımıza benzer. İlk olarak, her eğitim döneminde, birden fazla grup arasında yineleme yapacağız ve bu gruplar üzerinde eğiteceğiz. Bu yinelemeyi yapmak için train_loaderı kullanacağız. İkincisi, eğitim verilerini seçmek için maske kullanmamıza gerek yok. Bunun nedeni, train_loaderın yalnızca eğitim verilerini içermesidir.

epochs = 10
for epoch in range(epochs):
    epoch_loss = 0
    for batch_num, batch in enumerate(train_loader):
        y_score = model(batch)
        loss = loss_function(y_score, batch.y)
        loss.backward()
        optimizer.step()
        optimizer.zero_grad()
        epoch_loss+=loss
    if epoch % 2 == 0:
        print(f"Loss {epoch_loss}")

Test etmek

Benzer bir şekilde, test için, test verilerini yüklemek için test_loaderı kullanacağız ve test_loaderdan partileri modele geçirecek ve bunları tahmin için kullanacağız.

with torch.no_grad():
    num_correct = 0
    total_samples = 0
    for batch in test_loader:
        y_score = model(batch)
        y_pred = y_score.argmax(dim=1)
        num_correct += y_pred.eq(batch.y).sum().item()
        total_samples +=len(batch.batch.unique())
    print(f"Accuracy {num_correct/total_samples*100}")