Graphique des réseaux d'attention sous le capot

Nœud source: 769288

graphe des réseaux d'attention

Les réseaux de neurones graphiques (GNN) sont devenus la boîte à outils standard pour apprendre à partir des données graphiques. Les GNN sont capables d'apporter des améliorations pour des problèmes à fort impact dans différents domaines, tels que la recommandation de contenu ou la découverte de médicaments. Contrairement à d'autres types de données tels que les images, l'apprentissage à partir de données graphiques nécessite des méthodes spécifiques. Tel que défini par Michel Bronstein:

[..] ces méthodes sont basées sur une certaine forme de message passant sur le graphe permettant à différents nœuds d'échanger des informations.

Pour accomplir des tâches spécifiques sur les graphes (classification des nœuds, prédiction de liens, etc.), une couche GNN calcule les représentations des nœuds et des arêtes via ce que l'on appelle diffusion récursive de quartier (ou message passant). Selon ce principe, chaque nœud de graphe reçoit et agrège les caractéristiques de ses voisins afin de représenter la structure locale du graphe: différents types de couches GNN exécutent diverses stratégies d'agrégation.

Les formulations les plus simples de la couche GNN, telles que Graph Convolutional Networks (GCNs) ou GraphSage, exécutent une agrégation isotrope, où chaque voisin contribue également à mettre à jour la représentation du nœud central. Ce billet de blog présente une mini-série (2 articles) dédiée à l'analyse des Graph Attention Networks (GAT), qui définissent une opération d'anisotropie dans la diffusion récursive de voisinage. En exploitant le paradigme de l'anisotropie, la capacité d'apprentissage est améliorée par le mécanisme d'attention, qui attribue une importance différente à la contribution de chaque voisin.

Si vous êtes complètement nouveau sur les GNN et les concepts associés, je vous invite à lire l'article d'introduction suivant: Comprendre les éléments constitutifs des réseaux de neurones graphiques (introduction).

Si ce contenu éducatif approfondi vous est utile, abonnez-vous à notre liste de diffusion de recherche sur l'IA d'être alerté lorsque nous publierons du nouveau matériel. 

GCN vs GAT - Réchauffement mathématique

Cet échauffement est basé sur les détails GAT rapportés par la bibliothèque Deep Graph site de NDN Collective.

Avant de comprendre le comportement de la couche GAT, récapitulons les calculs derrière l'agrégation effectuée par la couche GCN.

GCN Layer - Fonction d'agrégation
  • est l'ensemble des voisins à un saut du nœud i. Ce nœud pourrait également être inclus parmi les voisins en ajoutant une auto-boucle.
  • est une constante de normalisation basée sur la structure du graphe, qui définit un calcul de moyenne isotrope.
  • σ est une fonction d'activation, qui introduit la non-linéarité dans la transformation.
  • est la matrice de poids des paramètres apprenables adoptée pour la transformation d'entités.

La couche GAT étend la fonction d'agrégation de base de la couche GCN, en attribuant une importance différente à chaque bord via les coefficients d'attention.

Équations de couche GAT
  • Équation (1) est une transformation linéaire de l'incorporation de la couche inférieure salut, ainsi que  W est sa matrice de poids apprenable. Cette transformation permet d'obtenir une puissance expressive suffisante pour transformer les caractéristiques d'entrée en caractéristiques de haut niveau et denses.
  • Équation (2) calcule un score d'attention non normalisé par paire entre deux voisins. Ici, il concatène d'abord le z plongements des deux nœuds, où || désigne la concaténation. Ensuite, il faut un produit scalaire d'une telle concaténation et un vecteur de poids apprenable a. En fin de compte, un LeakyReLU est appliqué au résultat du produit scalaire. Le score d'attention indique l'importance d'un nœud voisin dans le cadre de transmission de messages.
  • Équation (3) applique un softmax pour normaliser les scores d'attention sur les bords entrants de chaque nœud. Le softmax encode la sortie de l'étape précédente dans une distribution de probabilité. En conséquence, les scores d'attention sont beaucoup plus comparables entre les différents nœuds.
  • Équation (4) est similaire à l'agrégation GCN (voir l'équation au début de la section). Les plongements des voisins sont agrégés ensemble, mis à l'échelle par les scores d'attention. La principale conséquence de ce processus de mise à l'échelle est d'apprendre une contribution différente de chaque nœud de voisinage.

Implémentation NumPy

La première étape consiste à préparer les ingrédients (matrices) pour représenter un graphique simple et effectuer la transformation linéaire.

Code NumPy

print('nn----- One-hot vector representation of nodes. Shape(n,n)n')
X = np.eye(5, 5)
n = X.shape[0]
np.random.shuffle(X)
print(X) print('nn----- Embedding dimensionn')
emb = 3
print(emb) print('nn----- Weight Matrix. Shape(emb, n)n')
W = np.random.uniform(-np.sqrt(1. / emb), np.sqrt(1. / emb), (emb, n))
print(W) print('nn----- Adjacency Matrix (undirected graph). Shape(n,n)n')
A = np.random.randint(2, size=(n, n))
np.fill_diagonal(A, 1) A = (A + A.T)
A[A > 1] = 1
print(A)

Sortie

----- One-hot vector representation of nodes. Shape(n,n)
[[0 0 1 0 0] # node 1 [0 1 0 0 0] # node 2 [0 0 0 0 1] [1 0 0 0 0] [0 0 0 1 0]]
----- Embedding dimension
3
----- Weight Matrix. Shape(emb, n)
[[-0.4294049 0.57624235 -0.3047382 -0.11941829 -0.12942953] [ 0.19600584 0.5029172 0.3998854 -0.21561317 0.02834577] [-0.06529497 -0.31225734 0.03973776 0.47800217 -0.04941563]]
----- Adjacency Matrix (undirected graph). Shape(n,n)
[[1 1 1 0 1] [1 1 1 1 1] [1 1 1 1 0] [0 1 1 1 1] [1 1 0 1 1]]

La première matrice définit une représentation codée à chaud des nœuds (le nœud 1 est indiqué en gras). Ensuite, nous définissons une matrice de poids, en exploitant la dimension d'intégration définie. J'ai mis en évidence le vecteur de la 3e colonne de W car, comme vous le verrez sous peu, ce vecteur définit la représentation mise à jour du nœud 1 (une valeur 1 est initialisée en 3ème position). Nous pouvons effectuer la transformation linéaire pour obtenir une puissance expressive suffisante pour les caractéristiques des nœuds à partir de ces ingrédients. Cette étape vise à transformer les entités d'entrée (encodées à chaud) en une représentation basse et dense.

Couche GAT (équation 1)

Code NumPy

# equation (1)
print('nn----- Linear Transformation. Shape(n, emb)n')
z1 = X.dot(W.T)
print(z1)

Sortie

----- Linear Transformation. Shape(n, emb) [[-0.3047382 0.3998854 0.03973776] [ 0.57624235 0.5029172 -0.31225734] [-0.12942953 0.02834577 -0.04941563] [-0.4294049 0.19600584 -0.06529497] [-0.11941829 -0.21561317 0.47800217]]

L'opération suivante consiste à introduire les coefficients d'auto-attention pour chaque arête. Nous concaténons la représentation du nœud source et la représentation du nœud de destination pour représenter les arêtes. Ce processus de concaténation est activé par la matrice de contiguïté A, qui définit les relations entre tous les nœuds du graphe.

Couche GAT (équation 2)

Code NumPy

# equation (2) - First part
print('nn----- Concat hidden features to represent edges. Shape(len(emb.concat(emb)), number of edges)n')
edge_coords = np.where(A==1)
h_src_nodes = z1[edge_coords[0]]
h_dst_nodes = z1[edge_coords[1]]
z2 = np.concatenate((h_src_nodes, h_dst_nodes), axis=1)

Sortie

----- Concat hidden features to represent edges. Shape(len(emb.concat(emb)), number of edges) [[-0.3047382 0.3998854 0.03973776 -0.3047382 0.3998854 0.03973776] [-0.3047382 0.3998854 0.03973776 0.57624235 0.5029172 -0.31225734] [-0.3047382 0.3998854 0.03973776 -0.12942953 0.02834577 -0.04941563] [-0.3047382 0.3998854 0.03973776 -0.11941829 -0.21561317 0.47800217] [ 0.57624235 0.5029172 -0.31225734 -0.3047382 0.3998854 0.03973776] [ 0.57624235 0.5029172 -0.31225734 0.57624235 0.5029172 -0.31225734] [ 0.57624235 0.5029172 -0.31225734 -0.12942953 0.02834577 -0.04941563] [ 0.57624235 0.5029172 -0.31225734 -0.4294049 0.19600584 -0.06529497] [ 0.57624235 0.5029172 -0.31225734 -0.11941829 -0.21561317 0.47800217] [-0.12942953 0.02834577 -0.04941563 -0.3047382 0.3998854 0.03973776] [-0.12942953 0.02834577 -0.04941563 0.57624235 0.5029172 -0.31225734] [-0.12942953 0.02834577 -0.04941563 -0.12942953 0.02834577 -0.04941563] [-0.12942953 0.02834577 -0.04941563 -0.4294049 0.19600584 -0.06529497] [-0.4294049 0.19600584 -0.06529497 0.57624235 0.5029172 -0.31225734] [-0.4294049 0.19600584 -0.06529497 -0.12942953 0.02834577 -0.04941563] [-0.4294049 0.19600584 -0.06529497 -0.4294049 0.19600584 -0.06529497] [-0.4294049 0.19600584 -0.06529497 -0.11941829 -0.21561317 0.47800217] [-0.11941829 -0.21561317 0.47800217 -0.3047382 0.3998854 0.03973776] [-0.11941829 -0.21561317 0.47800217 0.57624235 0.5029172 -0.31225734] [-0.11941829 -0.21561317 0.47800217 -0.4294049 0.19600584 -0.06529497] [-0.11941829 -0.21561317 0.47800217 -0.11941829 -0.21561317 0.47800217]]

Dans le bloc précédent, j'ai mis en évidence les 4 lignes représentant les 4 bords entrants connectés au nœud 1. Les 3 premiers éléments de chaque ligne définissent la représentation d'incorporation des voisins du nœud 1, tandis que les 3 autres éléments de chaque ligne définissent les plongements de le nœud 1 lui-même (comme vous pouvez le remarquer, la première ligne encode une auto-boucle).

Après cette opération, nous pouvons introduire les coefficients d'attention et les multiplier par la représentation d'arête, issue du processus de concaténation. Enfin, la fonction Leaky Relu est appliquée à la sortie de ce produit.

Code NumPy

# equation (2) - Second part
print('nn----- Attention coefficients. Shape(1, len(emb.concat(emb)))n')
att = np.random.rand(1, z2.shape[1])
print(att) print('nn----- Edge representations combined with the attention coefficients. Shape(1, number of edges)n')
z2_att = z2.dot(att.T)
print(z2_att) print('nn----- Leaky Relu. Shape(1, number of edges)')
e = leaky_relu(z2_att)
print(e)

Sortie

----- Attention coefficients. Shape(1, len(emb.concat(emb))) [[0.09834683 0.42110763 0.95788953 0.53316528 0.69187711 0.31551563]] ----- Edge representations combined with the attention coefficients. Shape(1, number of edges) [[ 0.30322275] [ 0.73315639] [ 0.11150219] [ 0.11445879] [ 0.09607946] [ 0.52601309] [-0.0956411 ] [-0.14458757] [-0.0926845 ] [ 0.07860653] [ 0.50854017] [-0.11311402] [-0.16206049] [ 0.53443082] [-0.08722337] [-0.13616985] [-0.08426678] [ 0.48206613] [ 0.91199976] [ 0.2413991 ] [ 0.29330217]] ----- Leaky Relu. Shape(1, number of edges)
[[ 3.03222751e-01] [ 7.33156386e-01] [ 1.11502195e-01] [ 1.14458791e-01] [ 9.60794571e-02] [ 5.26013092e-01] [-9.56410988e-04] [-1.44587571e-03] [-9.26845030e-04] [ 7.86065337e-02] [ 5.08540169e-01] [-1.13114022e-03] [-1.62060495e-03] [ 5.34430817e-01] [-8.72233739e-04] [-1.36169846e-03] [-8.42667781e-04] [ 4.82066128e-01] [ 9.11999763e-01] [ 2.41399100e-01] [ 2.93302168e-01]]

À la fin de ce processus, nous avons obtenu un score différent pour chaque bord du graphique. Dans le bloc supérieur, j'ai mis en évidence l'évolution du coefficient associé à la première arête. Ensuite, pour rendre le coefficient facilement comparable entre différents nœuds, une fonction softmax est appliquée aux contributions de tous les voisins pour chaque nœud de destination.

Couche GAT (équation 3)

Code NumPy

# equation (3)
print('nn----- Edge scores as matrix. Shape(n,n)n')
e_matr = np.zeros(A.shape)
e_matr[edge_coords[0], edge_coords[1]] = e.reshape(-1,)
print(e_matr) print('nn----- For each node, normalize the edge (or neighbor) contributions using softmaxn')
alpha0 = softmax(e_matr[:,0][e_matr[:,0] != 0]) alpha1 = softmax(e_matr[:,1][e_matr[:,1] != 0])
alpha2 = softmax(e_matr[:,2][e_matr[:,2] != 0])
alpha3 = softmax(e_matr[:,3][e_matr[:,3] != 0])
alpha4 = softmax(e_matr[:,4][e_matr[:,4] != 0])
alpha = np.concatenate((alpha0, alpha1, alpha2, alpha3, alpha4))
print(alpha) print('nn----- Normalized edge score matrix. Shape(n,n)n')
A_scaled = np.zeros(A.shape)
A_scaled[edge_coords[0], edge_coords[1]] = alpha.reshape(-1,)
print(A_scaled)

Sortie

----- Edge scores as matrix. Shape(n,n) [[ 3.03222751e-01 7.33156386e-01 1.11502195e-01 0.00000000e+00 1.14458791e-01] [ 9.60794571e-02 5.26013092e-01 -9.56410988e-04 -1.44587571e-03 -9.26845030e-04] [ 7.86065337e-02 5.08540169e-01 -1.13114022e-03 -1.62060495e-03 0.00000000e+00] [ 0.00000000e+00 5.34430817e-01 -8.72233739e-04 -1.36169846e-03 -8.42667781e-04] [ 4.82066128e-01 9.11999763e-01 0.00000000e+00 2.41399100e-01 2.93302168e-01]] ----- For each node, normalize the edge (or neighbor) contributions using softmax [0.26263543 0.21349717 0.20979916 0.31406823 0.21610715 0.17567419 0.1726313 0.1771592 0.25842816 0.27167844 0.24278118 0.24273876 0.24280162 0.23393014 0.23388927 0.23394984 0.29823075 0.25138555 0.22399017 0.22400903 0.30061525] ----- Normalized edge score matrix. Shape(n,n) [[0.26263543 0.21349717 0.20979916 0. 0.31406823] [0.21610715 0.17567419 0.1726313 0.1771592 0.25842816] [0.27167844 0.24278118 0.24273876 0.24280162 0. ] [0. 0.23393014 0.23388927 0.23394984 0.29823075] [0.25138555 0.22399017 0. 0.22400903 0.30061525]]

Pour interpréter la signification de la dernière matrice définissant les scores de bord normalisés, récapitulons le contenu de la matrice de contiguïté.

----- Adjacency Matrix (undirected graph). Shape(n,n) [[1 1 1 0 1] [1 1 1 1 1] [1 1 1 1 0] [0 1 1 1 1] [1 1 0 1 1]]

Comme vous pouvez le voir, au lieu d'avoir 1 valeurs pour définir les arêtes, nous avons redimensionné la contribution de chaque voisin. La dernière étape consiste à calculer l'agrégation de voisinage: les plongements des voisins sont incorporés dans le nœud de destination, mis à l'échelle par les scores d'attention.

Couche GAT (équation 4)

Code NumPy

# equation (4)
print('nnNeighborhood aggregation (GCN) scaled with attention scores (GAT). Shape(n, emb)n')
ND_GAT = A_scaled.dot(z1)
print(ND_GAT)

Sortie

Neighborhood aggregation (GCN) scaled with attention scores (GAT). Shape(n, emb) [[-0.02166863 0.15062515 0.08352843] [-0.09390287 0.15866476 0.05716299] [-0.07856777 0.28521023 -0.09286313] [-0.03154513 0.10583032 0.04267501] [-0.07962369 0.19226439 0.069115 ]]

Prochaines étapes

Dans le futur article, je décrirai les mécanismes derrière la couche GAT multi-têtes, et nous verrons quelques applications pour la tâche de prédiction de lien.

Bibliographie

  • La version en cours d'exécution du code est disponible dans les éléments suivants cahier. Vous trouverez également une implémentation DGL, utile pour vérifier l'exactitude de l'implémentation.
  • L'article original sur les réseaux d'attention graphique de Petar Veličković, Guillem Cucurull, Arantxa Casanova, Adriana Romero, Pietro Liò, Yoshua Bengiois disponible sur arXiv.
  • Pour une explication approfondie du sujet, je suggère également la vidéo de Aleksa Gordic.

Cet article a été publié initialement le Vers la science des données et republié sur TOPBOTS avec la permission de l'auteur.

Profitez de cet article? Inscrivez-vous pour plus de mises à jour de l'IA.

Nous vous informerons lorsque nous publierons plus de formation technique.

Source : https://www.topbots.com/graph-attention-networks-under-the-hood/

Horodatage:

Plus de TOPBOTS