Ми розробляємо AI-системи на базі графових нейронних мереж (GNN) для задач, де важливі зв'язки між об'єктами. Коли табличні дані втрачають контекст, граф його зберігає. Наприклад, у виявленні шахрайства GNN аналізує ланцюжки транзакцій, а не окремі операції. Наша команда має 5+ років досвіду в GNN, реалізувала понад 30 проєктів для FinTech, e-Commerce та біоінформатики. Замовте розробку GNN-системи під ключ: ми підберемо архітектуру під ваші дані та забезпечимо продакшн-готовність. Зв'яжіться з нами для консультації та оцінки графової структури. Бюджет типового проєкту — від $10 000 до $50 000 залежно від складності.
Чому GNN перевершують класичні ML-моделі на графових даних?
Традиційні моделі (GBDT, лінійна регресія) працюють з векторами ознак, ігноруючи топологію. GNN оперує не лише ознаками вузлів, але й структурою — які вузли зв'язані, яка сила зв'язків, які типи ребер. Завдяки message passing, після K ітерацій кожен вузол «бачить» своїх сусідів на відстані K. Це дає приріст AUC на 5–15% у задачах класифікації вузлів та передбачення зв'язків порівняно з MLP або CatBoost.
Практичний приклад: у задачі виявлення шахрайства стандартна градієнтна модель дивиться на ознаки однієї транзакції — суму, час, геолокацію. GNN додає до цього контекст: як даний акаунт пов'язаний з іншими, чи були серед сусідів раніше заблоковані об'єкти, наскільки щільна навколо нього мережа підозрілих транзакцій. Саме цей мережевий контекст дозволяє виявляти скоординовані схеми шахрайства, непомітні для точкового аналізу. У наших проєктах перехід з CatBoost на GraphSAGE на фінансових даних давав покращення recall@5% FPR на 12–18 процентних пунктів — це в 2-3 рази краще за логістичну регресію.
Теоретична база та ключові архітектури
Основна ідея GNN — message passing: кожен вузол агрегує інформацію від своїх сусідів. Після K ітерацій вузол «бачить» K-hop neighbourhood.
Формула агрегації (GraphSAGE):
h_v^(k) = σ(W · CONCAT(h_v^(k-1), AGG({h_u^(k-1), u ∈ N(v)})))
Ключові архітектури (з посиланнями на оригінальні роботи):
| Архітектура | Агрегація | Застосування | Особливості |
|---|---|---|---|
| GCN (Kipf & Welling, ICLR 2017) | Spectral conv | Класифікація вузлів | Transductive |
| GraphSAGE (Hamilton et al., NeurIPS 2017) | Mean/LSTM/Max | Великі графи | Inductive |
| GAT (Veličković et al., ICLR 2018) | Attention | Неоднорідні графи | Зважені ребра |
| GIN (Xu et al., ICLR 2019) | Sum (найпотужніший) | Ізоморфізм графів | Максимальна виразність |
| RGCN (Schlichtkrull et al., ESWC 2018) | Relation-specific | Knowledge graphs | Різні типи ребер |
Реалізація GCN з PyTorch Geometric
Показати код реалізації GCN, GraphSAGE, GAT
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch_geometric.nn import GCNConv, SAGEConv, GATConv, global_mean_pool
from torch_geometric.data import Data, DataLoader
import numpy as np
import pandas as pd
class GraphConvNet(nn.Module):
"""
GCN для класифікації/регресії на графі.
Підходить для: fraud detection, рекомендацій, молекул.
"""
def __init__(self, node_features: int,
hidden_channels: int = 64,
output_dim: int = 1,
num_layers: int = 3,
dropout: float = 0.3):
super().__init__()
self.convs = nn.ModuleList()
self.bns = nn.ModuleList()
# Вхідний шар
self.convs.append(GCNConv(node_features, hidden_channels))
self.bns.append(nn.BatchNorm1d(hidden_channels))
# Приховані шари
for _ in range(num_layers - 2):
self.convs.append(GCNConv(hidden_channels, hidden_channels))
self.bns.append(nn.BatchNorm1d(hidden_channels))
# Вихідний шар
self.convs.append(GCNConv(hidden_channels, hidden_channels))
self.bns.append(nn.BatchNorm1d(hidden_channels))
self.dropout = dropout
self.classifier = nn.Linear(hidden_channels, output_dim)
def forward(self, x: torch.Tensor,
edge_index: torch.Tensor,
batch: torch.Tensor = None) -> torch.Tensor:
"""
x: (N, node_features) — матриця ознак вузлів
edge_index: (2, E) — список ребер у COO форматі
batch: (N,) — приналежність вузлів до графів (для батчингу)
"""
for conv, bn in zip(self.convs, self.bns):
x = conv(x, edge_index)
x = bn(x)
x = F.relu(x)
x = F.dropout(x, p=self.dropout, training=self.training)
# Graph-level readout (для задач на рівні графа)
if batch is not None:
x = global_mean_pool(x, batch)
return self.classifier(x)
class GraphSAGEEncoder(nn.Module):
"""
GraphSAGE для inductive learning (працює на нових вузлах без перенавчання).
Використовується для великих графів: соціальні мережі, транзакції.
"""
def __init__(self, in_channels: int, hidden_channels: int, out_channels: int,
num_layers: int = 3, aggr: str = 'mean'):
super().__init__()
self.convs = nn.ModuleList()
self.convs.append(SAGEConv(in_channels, hidden_channels, aggr=aggr))
for _ in range(num_layers - 2):
self.convs.append(SAGEConv(hidden_channels, hidden_channels, aggr=aggr))
self.convs.append(SAGEConv(hidden_channels, out_channels, aggr=aggr))
def forward(self, x, edge_index):
for i, conv in enumerate(self.convs):
x = conv(x, edge_index)
if i < len(self.convs) - 1:
x = F.relu(x)
x = F.dropout(x, p=0.2, training=self.training)
return x
def encode(self, x, edge_index):
"""L2-нормалізовані ембеддінги для downstream задач"""
out = self.forward(x, edge_index)
return F.normalize(out, p=2, dim=-1)
class GATNetwork(nn.Module):
"""
Graph Attention Network: зважена агрегація сусідів.
Attention ваги показують «важливість» кожного сусіда.
"""
def __init__(self, in_channels: int, hidden_channels: int,
out_channels: int, num_heads: int = 8):
super().__init__()
self.conv1 = GATConv(in_channels, hidden_channels,
heads=num_heads, dropout=0.6)
self.conv2 = GATConv(hidden_channels * num_heads, out_channels,
heads=1, concat=False, dropout=0.6)
def forward(self, x, edge_index):
x = F.dropout(x, p=0.6, training=self.training)
x = F.elu(self.conv1(x, edge_index))
x = F.dropout(x, p=0.6, training=self.training)
return self.conv2(x, edge_index)
Побудова графа з табличних даних
Показати код побудови графа
class GraphBuilder:
"""Конвертація табличних даних у граф для GNN"""
def build_user_item_graph(self, interactions: pd.DataFrame,
user_features: pd.DataFrame,
item_features: pd.DataFrame) -> Data:
"""
Дводольний граф користувач-товар для рекомендацій.
interactions: user_id, item_id, rating/count
"""
# Маппінг ID в індекси вузлів
user_ids = interactions['user_id'].unique()
item_ids = interactions['item_id'].unique()
n_users = len(user_ids)
user_idx = {uid: i for i, uid in enumerate(user_ids)}
item_idx = {iid: i + n_users for i, iid in enumerate(item_ids)}
# Ребра: користувач → товар
src = interactions['user_id'].map(user_idx).values
dst = interactions['item_id'].map(item_idx).values
# Двонаправлений граф (типово для GNN)
edge_index = torch.tensor(
np.vstack([
np.concatenate([src, dst]),
np.concatenate([dst, src])
]),
dtype=torch.long
)
# Матриця ознак вузлів
# Користувачі: embedding + поведінкові ознаки
user_feat_matrix = user_features.set_index('user_id').reindex(user_ids).fillna(0).values
# Товари: embedding + характеристики
item_feat_matrix = item_features.set_index('item_id').reindex(item_ids).fillna(0).values
# Вирівнюємо розмірності
max_dim = max(user_feat_matrix.shape[1], item_feat_matrix.shape[1])
user_feat_padded = np.pad(user_feat_matrix, ((0, 0), (0, max_dim - user_feat_matrix.shape[1])))
item_feat_padded = np.pad(item_feat_matrix, ((0, 0), (0, max_dim - item_feat_matrix.shape[1])))
x = torch.tensor(
np.vstack([user_feat_padded, item_feat_padded]),
dtype=torch.float
)
# Ваги ребер (наприклад, рейтинг)
edge_attr = torch.tensor(
np.concatenate([
interactions['rating'].values,
interactions['rating'].values # Дзеркальні ребра
]),
dtype=torch.float
).unsqueeze(1)
return Data(
x=x,
edge_index=edge_index,
edge_attr=edge_attr,
n_users=n_users
)
def build_transaction_graph(self, transactions: pd.DataFrame) -> Data:
"""
Граф транзакцій для fraud detection.
Вузли: акаунти, карти, IP-адреси, мерчанти.
Ребра: транзакції між ними.
"""
# Унікальні сутності
accounts = transactions['account_id'].unique()
merchants = transactions['merchant_id'].unique()
n_accounts = len(accounts)
acc_idx = {a: i for i, a in enumerate(accounts)}
mer_idx = {m: i + n_accounts for i, m in enumerate(merchants)}
src = transactions['account_id'].map(acc_idx).values
dst = transactions['merchant_id'].map(mer_idx).values
edge_index = torch.tensor([
np.concatenate([src, dst]),
np.concatenate([dst, src])
], dtype=torch.long)
# Ознаки транзакцій як атрибути ребер
edge_attr = torch.tensor(
transactions[['amount', 'hour_of_day', 'is_international']].values,
dtype=torch.float
)
edge_attr = torch.cat([edge_attr, edge_attr], dim=0) # Дублюємо для дзеркальних ребер
# Мітки: fraud = 1
if 'is_fraud' in transactions.columns:
y = torch.tensor(transactions['is_fraud'].values, dtype=torch.long)
else:
y = None
return Data(
x=torch.zeros(n_accounts + len(merchants), 16), # placeholder features
edge_index=edge_index,
edge_attr=edge_attr,
y=y
)
Навчання та оцінка GNN
Показати код навчання
class GNNTrainer:
"""Pipeline навчання GNN"""
def __init__(self, model: nn.Module, device: str = 'cuda'):
self.model = model.to(device)
self.device = device
self.optimizer = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4)
def train_epoch(self, data: Data, mask: torch.Tensor = None) -> float:
"""Одна епоха для node classification"""
self.model.train()
self.optimizer.zero_grad()
data = data.to(self.device)
out = self.model(data.x, data.edge_index)
if mask is not None:
loss = F.cross_entropy(out[mask], data.y[mask])
else:
loss = F.cross_entropy(out, data.y)
loss.backward()
self.optimizer.step()
return float(loss)
def evaluate(self, data: Data, mask: torch.Tensor) -> dict:
"""Оцінка якості передбачень"""
self.model.eval()
with torch.no_grad():
out = self.model(data.x.to(self.device), data.edge_index.to(self.device))
pred = out[mask].argmax(dim=-1).cpu()
true = data.y[mask].cpu()
from sklearn.metrics import accuracy_score, f1_score, roc_auc_score
probs = torch.softmax(out[mask], dim=-1)[:, 1].cpu().numpy()
return {
'accuracy': accuracy_score(true, pred),
'f1_macro': f1_score(true, pred, average='macro'),
'auc': roc_auc_score(true, probs) if len(np.unique(true)) > 1 else 0.5
}
def train(self, data: Data,
n_epochs: int = 200,
train_mask: torch.Tensor = None,
val_mask: torch.Tensor = None) -> dict:
"""Повний цикл навчання з early stopping"""
best_val_auc = 0
patience, patience_counter = 20, 0
history = {'train_loss': [], 'val_auc': []}
for epoch in range(n_epochs):
loss = self.train_epoch(data, train_mask)
history['train_loss'].append(loss)
if val_mask is not None and epoch % 5 == 0:
metrics = self.evaluate(data, val_mask)
history['val_auc'].append(metrics['auc'])
if metrics['auc'] > best_val_auc:
best_val_auc = metrics['auc']
patience_counter = 0
torch.save(self.model.state_dict(), 'best_gnn_model.pt')
else:
patience_counter += 1
if patience_counter >= patience:
print(f"Early stopping at epoch {epoch}")
break
return {'best_val_auc': best_val_auc, 'history': history}
Як масштабувати GNN на великі графи?
Стандартний GNN не масштабується на графи з мільйонами вузлів — повна матриця суміжності не поміщається в пам'ять. Наприклад, граф соціальної мережі може мати 10⁷ вузлів та 10⁸ ребер. Рішення:
-
GraphSAGE з mini-batch: семплювання K сусідів замість всіх. PyG підтримує через
NeighborLoaderз параметромnum_neighbors=[25, 10]. Це дозволяє обробляти графи з 10⁶ вузлів на одному GPU. - Cluster-GCN: розбиття графа на кластери, навчання всередині кластерів.
- GraphSAINT: випадкове семплювання підграфів з importance sampling.
Показати код завантажувача
from torch_geometric.loader import NeighborLoader
def create_scalable_dataloader(data: Data, batch_size: int = 1024) -> NeighborLoader:
"""Mini-batch завантажувач для великих графів"""
return NeighborLoader(
data,
num_neighbors=[25, 10, 5], # Сусіди для 3 hop
batch_size=batch_size,
input_nodes=data.train_mask,
shuffle=True,
num_workers=4
)
Область застосування та бенчмарки
Показати таблицю бенчмарків
| Задача | Датасет | Архітектура | AUC/Accuracy |
|---|---|---|---|
| Fraud detection | фінанс. транзакції (10⁶ вузлів) | GraphSAGE | AUC 0.93-0.97 |
| Рекомендації | Amazon | LightGCN | NDCG@20 0.045 |
| Соціальний спам | GAT | F1 0.89 | |
| Молекулярні властивості | ZINC | GIN | MAE 0.163 |
| Дорожній трафік | METR-LA | Diffusion GCN | RMSE 2.37 |
GNN перевершують традиційні методи лише тоді, коли структура графа несе інформацію. Якщо відносини між об'єктами випадкові — звичайний GBDT або MLP покаже порівнянний результат з меншою складністю.
Перед стартом проєкту ми проводимо аудит: чи має ваша доменна область значущу структуру графа, чи достатньо даних для навчання, чи реалістичний цільовий AUC. Якщо GNN не дає переваги, чесно скажемо про це і запропонуємо простішу модель. Такий підхід зберігає бюджет клієнта та підвищує довгострокову довіру до рішення.
Що входить в роботу (покроковий план)
- Аналіз графової структури та вибір архітектури (GCN, GraphSAGE, GAT, GIN).
- Побудова пайплайну даних: конвертація таблиць у графи, нормалізація ознак.
- Навчання та підбір гіперпараметрів з валідацією (early stopping, крос-валідація).
- Розгортання моделі через Triton Inference Server або ONNX Runtime.
- Документація model card, API-специфікація та керівництво користувача.
- Підтримка після впровадження: моніторинг дрейфу даних, донавчання.
Ми гарантуємо якість результату — всі рішення проходять тестування на ваших даних до фіксації. Оцінимо ваш проєкт за 2 – 3 робочих дні: надішліть опис задачі та приблизні обсяги графа (число вузлів, ребер, задача). Зв'яжіться з нами, щоб обговорити задачу.







