Класифікація вузлів прогнозує мітки для сутностей у графі, використовуючи їхні зв'язки. На відміну від табличного ML, де кожен зразок незалежний, графовий ML експлуатує реляційну структуру - друзі користувача, цитування статті, зв'язки молекули. Коли відносини важливі, графи часто перевершують підходи тільки на фічах.
Де використовується
Класифікація вузлів забезпечує мережеві застосування:
- Соціальні мережі: Профілювання користувачів, виявлення ботів, виявлення спільнот
- E-commerce: Категоризація продуктів, виявлення фрод-кілець
- Біологія: Прогнозування функцій білків, drug-target взаємодія
- Академія: Класифікація статей, експертиза авторів
- Безпека: Виявлення malware, мережеві вторгнення
- Фінанси: Класифікація акаунтів, виявлення відмивання грошей
Продукти: Pinterest PinSage, LinkedIn graph ML, Google Knowledge Graph.
Типові дані
| Компонент | Опис | Приклади |
|---|---|---|
| Вузли | Сутності для класифікації | Користувачі, продукти, статті |
| Ребра | Відносини | Follows, покупки, цитує |
| Фічі вузлів | Атрибути сутностей | Профіль, текстові ембедінги |
| Фічі ребер | Атрибути відносин | Часові мітки, ваги, типи |
| Мітки | Категорії вузлів | Бот/людина, тема, роль |
Що ви робите на практиці
- Побудуйте граф - Визначте вузли, ребра, фічі
- Проаналізуйте структуру графа - Щільність, розподіл ступенів, компоненти
- Обробіть дисбаланс міток - Часто суворий у класифікації вузлів
- Оберіть transductive vs inductive - Фіксований граф чи нові вузли?
- Виберіть GNN архітектуру - GCN, GAT, GraphSAGE
- Запобігайте витоку міток - Обережно з message passing
- Оцінюйте правильно - Train/val/test розбивка вузлів
- Масштабуйте для продакшену - Mini-batching, sampling
Спочатку базова лінія
Почніть з простих підходів перед GNN:
import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
import networkx as nx
class NodeClassificationBaseline:
"""Прості базові лінії для класифікації вузлів."""
def __init__(self, graph, features, labels):
self.graph = graph
self.features = features
self.labels = labels
def feature_only_baseline(self, train_mask, test_mask):
"""Класифікація тільки за фічами вузлів (без графа)."""
scaler = StandardScaler()
X_train = scaler.fit_transform(self.features[train_mask])
X_test = scaler.transform(self.features[test_mask])
model = LogisticRegression(max_iter=1000)
model.fit(X_train, self.labels[train_mask])
predictions = model.predict(X_test)
return predictions
def label_propagation(self, train_mask, max_iter=100, alpha=0.8):
"""Напівкероване поширення міток."""
n_nodes = len(self.labels)
n_classes = len(np.unique(self.labels[self.labels >= 0]))
# Ініціалізація матриці міток
Y = np.zeros((n_nodes, n_classes))
for i in np.where(train_mask)[0]:
Y[i, self.labels[i]] = 1
# Нормалізована матриця суміжності
A = nx.adjacency_matrix(self.graph).toarray()
D = np.diag(A.sum(axis=1) + 1e-10)
D_inv_sqrt = np.diag(1.0 / np.sqrt(np.diag(D)))
A_norm = D_inv_sqrt @ A @ D_inv_sqrt
# Поширення
Y_init = Y.copy()
for _ in range(max_iter):
Y = alpha * A_norm @ Y + (1 - alpha) * Y_init
predictions = Y.argmax(axis=1)
return predictions
def node2vec_baseline(self, train_mask, test_mask, dimensions=64):
"""Node2Vec ембедінги + класифікатор."""
from node2vec import Node2Vec
# Генерація ембедінгів
node2vec = Node2Vec(self.graph, dimensions=dimensions,
walk_length=30, num_walks=200, workers=4)
model = node2vec.fit(window=10, min_count=1)
# Отримання ембедінгів для всіх вузлів
embeddings = np.array([model.wv[str(i)]
for i in range(len(self.labels))])
# Комбінуємо з фічами
X = np.hstack([self.features, embeddings])
# Навчаємо класифікатор
scaler = StandardScaler()
X_train = scaler.fit_transform(X[train_mask])
X_test = scaler.transform(X[test_mask])
clf = LogisticRegression(max_iter=1000)
clf.fit(X_train, self.labels[train_mask])
return clf.predict(X_test)
Чому базові лінії важливі: - Feature-only показує цінність структури графа - Label propagation - сильна напівкерована базова лінія - Node2Vec захоплює структурну інформацію - GNN повинні значимо перевершувати їх
GNN реалізація
Graph Neural Networks для класифікації вузлів:
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch_geometric.nn import GCNConv, GATConv, SAGEConv
class GCN(nn.Module):
"""Graph Convolutional Network."""
def __init__(self, in_channels, hidden_channels, out_channels,
num_layers=2, dropout=0.5):
super().__init__()
self.convs = nn.ModuleList()
self.convs.append(GCNConv(in_channels, hidden_channels))
for _ in range(num_layers - 2):
self.convs.append(GCNConv(hidden_channels, hidden_channels))
self.convs.append(GCNConv(hidden_channels, out_channels))
self.dropout = dropout
def forward(self, x, edge_index):
for i, conv in enumerate(self.convs[:-1]):
x = conv(x, edge_index)
x = F.relu(x)
x = F.dropout(x, p=self.dropout, training=self.training)
x = self.convs-1
return x
class GraphSAGE(nn.Module):
"""GraphSAGE для індуктивного навчання."""
def __init__(self, in_channels, hidden_channels, out_channels,
num_layers=2, dropout=0.5):
super().__init__()
self.convs = nn.ModuleList()
self.convs.append(SAGEConv(in_channels, hidden_channels))
for _ in range(num_layers - 2):
self.convs.append(SAGEConv(hidden_channels, hidden_channels))
self.convs.append(SAGEConv(hidden_channels, out_channels))
self.dropout = dropout
def forward(self, x, edge_index):
for i, conv in enumerate(self.convs[:-1]):
x = conv(x, edge_index)
x = F.relu(x)
x = F.dropout(x, p=self.dropout, training=self.training)
x = self.convs-1
return x
def train_gnn(model, data, optimizer, epochs=200):
"""Цикл навчання для класифікації вузлів."""
model.train()
for epoch in range(epochs):
optimizer.zero_grad()
out = model(data.x, data.edge_index)
loss = F.cross_entropy(out[data.train_mask],
data.y[data.train_mask])
loss.backward()
optimizer.step()
# Валідація
if epoch % 10 == 0:
model.eval()
with torch.no_grad():
pred = model(data.x, data.edge_index).argmax(dim=1)
val_acc = (pred[data.val_mask] == data.y[data.val_mask]).float().mean()
model.train()
print(f'Epoch {epoch}: Loss {loss:.4f}, Val Acc {val_acc:.4f}')
return model
Обробка великих графів
Mini-batching для масштабованості:
from torch_geometric.loader import NeighborLoader
class ScalableNodeClassification:
"""Обробка великих графів з семплінгом."""
def __init__(self, data, num_neighbors=[25, 10], batch_size=1024):
self.data = data
self.num_neighbors = num_neighbors
self.batch_size = batch_size
def get_loader(self, mask, shuffle=True):
"""Створення loader з семплінгом сусідів."""
return NeighborLoader(
self.data,
num_neighbors=self.num_neighbors,
batch_size=self.batch_size,
input_nodes=mask,
shuffle=shuffle
)
def train_epoch(self, model, loader, optimizer):
"""Навчання однієї епохи з mini-batching."""
model.train()
total_loss = 0
for batch in loader:
optimizer.zero_grad()
out = model(batch.x, batch.edge_index)
# Обчислюємо loss тільки на цільових вузлах
loss = F.cross_entropy(out[:batch.batch_size],
batch.y[:batch.batch_size])
loss.backward()
optimizer.step()
total_loss += loss.item() * batch.batch_size
return total_loss / self.data.train_mask.sum()
Transductive vs Inductive
class TransductiveVsInductive:
"""Правильна обробка обох налаштувань."""
def transductive_split(self, data, train_ratio=0.6, val_ratio=0.2):
"""
Transductive: Всі вузли в графі під час навчання,
але тільки деякі мають мітки.
"""
n = data.num_nodes
perm = torch.randperm(n)
train_size = int(n * train_ratio)
val_size = int(n * val_ratio)
train_mask = torch.zeros(n, dtype=torch.bool)
val_mask = torch.zeros(n, dtype=torch.bool)
test_mask = torch.zeros(n, dtype=torch.bool)
train_mask[perm[:train_size]] = True
val_mask[perm[train_size:train_size+val_size]] = True
test_mask[perm[train_size+val_size:]] = True
return train_mask, val_mask, test_mask
def inductive_split(self, data, train_ratio=0.6, val_ratio=0.2):
"""
Inductive: Тестові вузли повністю невидимі під час навчання.
Потрібно видалити тестові вузли з тренувального графа.
"""
n = data.num_nodes
perm = torch.randperm(n)
train_size = int(n * train_ratio)
val_size = int(n * val_ratio)
train_nodes = perm[:train_size]
val_nodes = perm[train_size:train_size+val_size]
test_nodes = perm[train_size+val_size:]
# Створюємо тренувальний підграф (без тестових вузлів)
train_val_nodes = torch.cat([train_nodes, val_nodes])
train_graph = self.subgraph(data, train_val_nodes)
return {
'train_graph': train_graph,
'train_nodes': train_nodes,
'val_nodes': val_nodes,
'test_nodes': test_nodes,
'full_graph': data
}
Метрики оцінки
| Метрика | Коли використовувати | Примітки |
|---|---|---|
| Accuracy | Збалансовані класи | Просто, але оманливо якщо дисбаланс |
| Macro F1 | Дисбаланс, важливі всі класи | Середній F1 по класах |
| Micro F1 | Дисбаланс, важливий загальний | Зважений за частотою класів |
| AUC-ROC | Бінарний або one-vs-rest | Незалежний від порогу |
Типові помилки та чек-лист
- [ ] Over-smoothing: Занадто багато шарів, представлення сходяться
- [ ] Витік міток: Тестові мітки видимі через message passing
- [ ] Відключені компоненти: Деякі вузли ізольовані від сигналу
- [ ] Дисбаланс класів: Меншинові класи недопредставлені
- [ ] Масштабування фіч: Фічі вузлів не нормалізовані
- [ ] Проблеми з пам'яттю: Граф занадто великий для GPU
- [ ] Гетерофілія: Припущення що сусіди схожі не виконується
Чек-лист для продакшену
Перед деплоєм: - [ ] Пайплайн побудови графа валідовано - [ ] Індуктивне навчання якщо очікуються нові вузли - [ ] Mini-batching для великих графів - [ ] Препроцесинг фіч стандартизовано - [ ] Порівняння з базовою лінією задокументовано
Після деплою: - [ ] Обробка нових вузлів протестована - [ ] Частота оновлення графа визначена - [ ] Латентність прогнозів моніториться - [ ] Обробка edge cases (ізольовані вузли) - [ ] Розклад перенавчання моделі
Інструменти та стек
| Категорія | Інструменти |
|---|---|
| Фреймворки | PyTorch Geometric, DGL, Spektral |
| Графові БД | Neo4j, Amazon Neptune, TigerGraph |
| Ембедінги | Node2Vec, DeepWalk, LINE |
| Візуалізація | NetworkX, Gephi, Graphistry |
| Масштабування | DistDGL, PyG distributed |
Приклад міні-кейсу
Сценарій: Виявлення ботів у соціальній мережі.
Побудова графа: - Вузли: Акаунти користувачів - Ребра: Follows, mentions, retweets - Фічі: Вік акаунту, частота постів, текстові ембедінги - Мітки: Бот/людина (маленький розмічений набір)
Підхід: 1. Побудовано граф follows з 10M вузлів 2. Напівкероване з 5% розміченими 3. GraphSAGE для індуктивного навчання (нові акаунти) 4. Neighbor sampling для масштабованості
Результати: - 94% F1 на виявленні ботів (vs 78% тільки фічі) - Виловлює координовані бот-мережі - 50ms інференс на новий акаунт - Перенавчається щотижня на нових мітках
Пов'язані сторінки
- Індекс: ML Field Guide
- Knowledge Graphs
- Виявлення фроду
- Семантичний пошук
Додаткові матеріали
Спробуйте наживо
Дослідіть пов'язану модель на практиці: відкрийте Graph Attention Network: Node Classification і змінюйте параметри під час роботи. Усе лишається у вашому браузері.
▶ Відкрити симуляцію Graph Attention Network: Node Classification