Технології AI Написано практикуючими розробниками

Federated Learning: тренування AI без крадіжки даних — як навчити модель на мільярдах пристроїв

Оновлено: 16 хв читання 14 переглядів

Твій телефон знає, які слова ти часто друкуєш. Твоя лікарня має тисячі медичних знімків. Твій банк бачить всі транзакції. Ці дані могли б натренувати найкращий AI у світі. Але ти не хочеш, щоб Google читав твої повідомлення. Лікарня не може ділитися медичними даними згідно HIPAA. Банк зв'язаний регуляціями PCI DSS.


Твій телефон знає, які слова ти часто друкуєш. Твоя лікарня має тисячі медичних знімків. Твій банк бачить всі транзакції. Ці дані могли б натренувати найкращий AI у світі. Але ти не хочеш, щоб Google читав твої повідомлення. Лікарня не може ділитися медичними даними згідно HIPAA. Банк зв'язаний регуляціями PCI DSS.

Federated Learning — революційний підхід до машинного навчання, який дозволяє тренувати модель на всіх цих даних, не збираючи їх в одному місці. Дані залишаються у власників. Моделі — подорожують. Це не компроміс між privacy та якістю моделі. Це краще рішення для обох.


Проблема централізованого ML: чому традиційний підхід не працює

Традиційний підхід до machine learning:

Клієнт 1 → дані →
Клієнт 2 → дані →  Центральний сервер → Модель
Клієнт 3 → дані →

Здається простим, але створює каскад проблем, які стають все серйознішими з кожним роком.

1. Privacy Catastrophe

Всі дані видимі серверу. Один breach — і мільйони records публічні. Equifax (2017) — 147 мільйонів людей. Yahoo — 3 мільярди акаунтів. Це не теорія — це реальність.

2. Bandwidth Nightmare

Передача терабайтів даних по мережі. Медичні зображення, відео, аудіо — все це має фізично переміститися на сервер. Для мільярдів пристроїв це просто неможливо.

3. Regulatory Compliance Violations

GDPR в Європі забороняє передачу персональних даних без explicit consent. HIPAA в США криміналізує неавторизований доступ до медичних даних. CCPA в Каліфорнії дає користувачам право на видалення. Централізація даних порушує все це одночасно.

4. Trust Issues

Клієнти не довіряють серверу. І правильно роблять. Навіть добросовісні компанії можуть бути зламані, продані, або змінити політику privacy.

5. Single Point of Failure

Один витік — і все пропало. Немає можливості відкликати дані після breach.


Federated Learning: парадигма «модель до даних»

Центральний принцип: Не дані до моделі, а модель до даних.

Round 1:
Server: глобальна модель M₀
        ↓ broadcast (тільки weights ~100MB)
┌───────┼───────┐
↓       ↓       ↓
Client1 Client2 Client3
(local  (local  (local
train)  train)  train)
(data   (data   (data
stays!) stays!) stays!)
↓       ↓       ↓
└───────┼───────┘
        ↓ aggregate (тільки gradients)
Server: M₁ = aggregate(M₁¹, M₁², M₁³)

Round 2-N: repeat until convergence

Що передається по мережі:

  • Gradient updates (зміни ваг)
  • Model weights (параметри моделі)
  • НЕ raw data

Що залишається локально:

  • Всі training samples
  • Personal information
  • Sensitive content

FedAvg: базовий алгоритм з глибоким розбором

Federated Averaging (McMahan et al., 2017) — це фундаментальний алгоритм, з якого починається будь-яке вивчення FL.

import numpy as np
import copy
from typing import List, Dict, Tuple

class FederatedAveragingServer:
    """
    Сервер для Federated Averaging.
    Координує тренування без доступу до даних клієнтів.
    """

    def __init__(self, model, num_rounds: int = 100,
                 clients_per_round: int = 10,
                 local_epochs: int = 5,
                 learning_rate: float = 0.01):
        self.global_model = model
        self.num_rounds = num_rounds
        self.clients_per_round = clients_per_round
        self.local_epochs = local_epochs
        self.lr = learning_rate
        self.history = {'loss': [], 'accuracy': []}

    def aggregate_weights(self, client_weights: List[Dict],
                          client_sizes: List[int]) -> Dict:
        """
        Weighted average of client model weights.
        Більші datasets мають більший вплив.
        """
        total_samples = sum(client_sizes)

        # Ініціалізуємо aggregated weights нулями
        aggregated = {}
        for key in client_weights[0].keys():
            aggregated[key] = np.zeros_like(client_weights[0][key])

        # Weighted sum
        for weights, size in zip(client_weights, client_sizes):
            weight_factor = size / total_samples
            for key in weights.keys():
                aggregated[key] += weights[key] * weight_factor

        return aggregated

    def select_clients(self, clients: List, k: int) -> List:
        """
        Випадковий вибір клієнтів для раунду.
        В реальності враховується availability, battery level, network.
        """
        return np.random.choice(clients, size=min(k, len(clients)),
                                replace=False).tolist()

    def train_round(self, clients: List) -> Tuple[Dict, float]:
        """
        Один раунд федеративного навчання.
        """
        # 1. Вибираємо підмножину клієнтів
        selected = self.select_clients(clients, self.clients_per_round)

        # 2. Розсилаємо поточні глобальні ваги
        global_weights = self.global_model.get_weights()

        # 3. Клієнти тренуються локально
        client_weights = []
        client_sizes = []
        client_losses = []

        for client in selected:
            # Клієнт отримує копію глобальної моделі
            local_weights, local_size, local_loss = client.local_train(
                global_weights,
                epochs=self.local_epochs,
                lr=self.lr
            )
            client_weights.append(local_weights)
            client_sizes.append(local_size)
            client_losses.append(local_loss)

        # 4. Агрегуємо результати
        new_global_weights = self.aggregate_weights(client_weights, client_sizes)

        # 5. Оновлюємо глобальну модель
        self.global_model.set_weights(new_global_weights)

        avg_loss = np.average(client_losses, weights=client_sizes)
        return new_global_weights, avg_loss

    def train(self, clients: List) -> Dict:
        """
        Повний цикл федеративного навчання.
        """
        for round_num in range(self.num_rounds):
            weights, loss = self.train_round(clients)
            self.history['loss'].append(loss)

            if round_num % 10 == 0:
                print(f"Round {round_num}: Loss = {loss:.4f}")

        return self.history


class FederatedClient:
    """
    Клієнт федеративного навчання.
    Має локальні дані та виконує тренування на них.
    """

    def __init__(self, client_id: str, local_data, local_labels, model_fn):
        self.client_id = client_id
        self.local_data = local_data
        self.local_labels = local_labels
        self.model = model_fn()
        self.data_size = len(local_data)

    def local_train(self, global_weights: Dict,
                    epochs: int, lr: float) -> Tuple[Dict, int, float]:
        """
        Локальне тренування на даних клієнта.
        Дані НІКОЛИ не покидають цей метод.
        """
        # Завантажуємо глобальні ваги
        self.model.set_weights(copy.deepcopy(global_weights))

        # Тренуємо локально
        total_loss = 0
        for epoch in range(epochs):
            # Shuffle local data
            indices = np.random.permutation(self.data_size)
            shuffled_data = self.local_data[indices]
            shuffled_labels = self.local_labels[indices]

            # Mini-batch training
            batch_size = 32
            for i in range(0, self.data_size, batch_size):
                batch_x = shuffled_data[i:i+batch_size]
                batch_y = shuffled_labels[i:i+batch_size]

                loss = self.model.train_step(batch_x, batch_y, lr)
                total_loss += loss

        avg_loss = total_loss / (epochs * (self.data_size // batch_size))

        # Повертаємо ТІЛЬКИ ваги, не дані
        return self.model.get_weights(), self.data_size, avg_loss

Differential Privacy: коли градієнтів недостатньо

Проблема: Навіть gradients можуть витікати інформацію про training data.

Membership Inference Attack:

class MembershipInferenceAttack:
    """
    Атака для визначення, чи був конкретний sample
    в training data моделі.
    """

    def __init__(self, target_model, shadow_models):
        self.target = target_model
        self.shadows = shadow_models
        self.attack_model = self._train_attack_model()

    def _train_attack_model(self):
        """
        Тренуємо класифікатор на shadow models.
        Вхід: confidence scores моделі
        Вихід: member / non-member
        """
        features = []
        labels = []

        for shadow in self.shadows:
            # Отримуємо predictions для training data (members)
            member_preds = shadow.predict(shadow.train_data)
            features.extend(member_preds)
            labels.extend([1] * len(member_preds))

            # Отримуємо predictions для test data (non-members)
            non_member_preds = shadow.predict(shadow.test_data)
            features.extend(non_member_preds)
            labels.extend([0] * len(non_member_preds))

        return train_classifier(features, labels)

    def attack(self, sample) -> float:
        """
        Повертає ймовірність того, що sample був у training data.
        """
        prediction = self.target.predict([sample])
        return self.attack_model.predict_proba([prediction])[0][1]


class GradientLeakageAttack:
    """
    Deep Leakage from Gradients (Zhu et al., 2019)
    Відновлення training data з градієнтів.
    """

    def __init__(self, model, gradient):
        self.model = model
        self.target_gradient = gradient

    def reconstruct(self, num_iterations=1000):
        """
        Ітеративно відновлюємо вхідні дані.
        """
        # Починаємо з випадкових даних
        dummy_data = torch.randn_like(self.model.expected_input)
        dummy_data.requires_grad = True

        optimizer = torch.optim.LBFGS([dummy_data])

        for i in range(num_iterations):
            def closure():
                optimizer.zero_grad()

                # Обчислюємо градієнт для dummy даних
                dummy_gradient = self.model.compute_gradient(dummy_data)

                # Мінімізуємо різницю між градієнтами
                loss = sum([
                    ((dummy_g - target_g) ** 2).sum()
                    for dummy_g, target_g in zip(dummy_gradient, self.target_gradient)
                ])

                loss.backward()
                return loss

            optimizer.step(closure)

        return dummy_data.detach()

Рішення: Differential Privacy (DP)

import torch
import numpy as np
from typing import List, Tuple

class DifferentiallyPrivateSGD:
    """
    DP-SGD: Gradient clipping + Gaussian noise.
    Математична гарантія (ε, δ)-differential privacy.
    """

    def __init__(self, model,
                 max_grad_norm: float = 1.0,  # C
                 noise_multiplier: float = 1.0,  # σ
                 delta: float = 1e-5):
        self.model = model
        self.max_grad_norm = max_grad_norm
        self.noise_multiplier = noise_multiplier
        self.delta = delta

        # Privacy accountant для трекінгу витраченого бюджету
        self.privacy_spent = 0.0

    def clip_gradients(self, gradients: List[torch.Tensor]) -> List[torch.Tensor]:
        """
        Per-sample gradient clipping.
        Обмежує вплив одного sample на модель.
        """
        clipped = []
        for grad in gradients:
            grad_norm = torch.norm(grad)
            clip_factor = min(1.0, self.max_grad_norm / (grad_norm + 1e-8))
            clipped.append(grad * clip_factor)
        return clipped

    def add_noise(self, gradients: List[torch.Tensor],
                  batch_size: int) -> List[torch.Tensor]:
        """
        Додаємо калібрований Gaussian noise.
        Noise scale = σ * C / batch_size
        """
        noised = []
        noise_scale = self.noise_multiplier * self.max_grad_norm / batch_size

        for grad in gradients:
            noise = torch.randn_like(grad) * noise_scale
            noised.append(grad + noise)

        return noised

    def private_step(self, batch_data, batch_labels) -> float:
        """
        Один крок DP-SGD.
        """
        batch_size = len(batch_data)

        # 1. Обчислюємо per-sample gradients
        per_sample_grads = []
        for x, y in zip(batch_data, batch_labels):
            grad = self.model.compute_gradient(x.unsqueeze(0), y.unsqueeze(0))
            per_sample_grads.append(grad)

        # 2. Clip кожен gradient окремо
        clipped_grads = [self.clip_gradients(g) for g in per_sample_grads]

        # 3. Aggregate (mean)
        aggregated = []
        for i in range(len(clipped_grads[0])):
            layer_grads = [g[i] for g in clipped_grads]
            aggregated.append(torch.stack(layer_grads).mean(dim=0))

        # 4. Add noise
        noised_grads = self.add_noise(aggregated, batch_size)

        # 5. Apply to model
        self.model.apply_gradients(noised_grads)

        # Update privacy budget
        self._update_privacy_accountant(batch_size)

        return self.model.current_loss

    def _update_privacy_accountant(self, batch_size: int):
        """
        Moments accountant для tight privacy bounds.
        """
        # Simplified: в реальності використовують RDP composition
        q = batch_size / self.total_dataset_size
        sigma = self.noise_multiplier

        # Approximate privacy cost per step
        step_epsilon = q * np.sqrt(2 * np.log(1.25 / self.delta)) / sigma
        self.privacy_spent += step_epsilon

    def get_privacy_spent(self) -> Tuple[float, float]:
        """
        Повертає (epsilon, delta) — скільки privacy витрачено.
        """
        return (self.privacy_spent, self.delta)

Non-IID Data: головний виклик федеративного навчання

Проблема: У реальному світі дані на клієнтах не є IID (Independent and Identically Distributed).

class NonIIDDataDistribution:
    """
    Моделювання різних типів non-IID розподілів.
    """

    @staticmethod
    def label_skew(data, labels, num_clients: int,
                   classes_per_client: int = 2):
        """
        Кожен клієнт має тільки певні класи.
        Приклад: User A бачить тільки котів і собак,
                 User B — тільки машини і літаки.
        """
        num_classes = len(np.unique(labels))
        client_data = [[] for _ in range(num_clients)]
        client_labels = [[] for _ in range(num_clients)]

        # Розподіляємо класи по клієнтах
        class_assignments = {}
        for c in range(num_classes):
            # Кожен клас присвоюється кільком клієнтам
            assigned_clients = np.random.choice(
                num_clients,
                size=num_clients // num_classes * classes_per_client,
                replace=False
            )
            class_assignments[c] = assigned_clients

        # Розподіляємо дані
        for i, (x, y) in enumerate(zip(data, labels)):
            eligible_clients = class_assignments[y]
            chosen = np.random.choice(eligible_clients)
            client_data[chosen].append(x)
            client_labels[chosen].append(y)

        return client_data, client_labels

    @staticmethod
    def quantity_skew(data, labels, num_clients: int,
                      alpha: float = 0.5):
        """
        Dirichlet distribution — різна кількість даних у клієнтів.
        alpha → 0: екстремальний дисбаланс
        alpha → ∞: рівномірний розподіл
        """
        proportions = np.random.dirichlet([alpha] * num_clients)
        client_sizes = (proportions * len(data)).astype(int)

        # Забезпечуємо, що кожен клієнт має хоча б 1 sample
        client_sizes = np.maximum(client_sizes, 1)

        indices = np.random.permutation(len(data))
        client_data = []
        client_labels = []

        start = 0
        for size in client_sizes:
            end = min(start + size, len(data))
            client_indices = indices[start:end]
            client_data.append(data[client_indices])
            client_labels.append(labels[client_indices])
            start = end

        return client_data, client_labels

    @staticmethod
    def feature_skew(data, labels, num_clients: int):
        """
        Різні feature distributions на клієнтах.
        Приклад: різні камери, освітлення, кути.
        """
        client_data = []
        client_labels = []

        for i in range(num_clients):
            # Кожен клієнт має своє "викривлення" даних
            brightness = np.random.uniform(0.7, 1.3)
            contrast = np.random.uniform(0.8, 1.2)
            noise_level = np.random.uniform(0, 0.1)

            # Застосовуємо трансформації
            transformed = data * brightness * contrast
            transformed += np.random.randn(*data.shape) * noise_level

            # Випадкова підмножина
            indices = np.random.choice(len(data), size=len(data)//num_clients)
            client_data.append(transformed[indices])
            client_labels.append(labels[indices])

        return client_data, client_labels

FedProx: рішення для Non-IID

FedProx (Li et al., 2020) додає proximal term до локального loss:

class FedProxClient:
    """
    FedProx = FedAvg + proximal regularization.
    Зменшує drift від глобальної моделі при non-IID даних.
    """

    def __init__(self, model, mu: float = 0.01):
        self.model = model
        self.mu = mu  # Proximal coefficient

    def local_train(self, global_weights: Dict,
                    epochs: int, lr: float) -> Tuple[Dict, int, float]:
        """
        Локальне тренування з proximal term.
        """
        self.model.set_weights(copy.deepcopy(global_weights))
        global_weights_tensor = self._dict_to_tensor(global_weights)

        for epoch in range(epochs):
            for batch_x, batch_y in self.data_loader:
                # Standard loss
                predictions = self.model(batch_x)
                task_loss = self.criterion(predictions, batch_y)

                # Proximal term: ||w - w_global||²
                current_weights = self._dict_to_tensor(self.model.get_weights())
                proximal_loss = (self.mu / 2) * torch.sum(
                    (current_weights - global_weights_tensor) ** 2
                )

                # Total loss
                total_loss = task_loss + proximal_loss

                # Backprop
                self.optimizer.zero_grad()
                total_loss.backward()
                self.optimizer.step()

        return self.model.get_weights(), len(self.dataset), total_loss.item()


class ScaffoldClient:
    """
    SCAFFOLD: Stochastic Controlled Averaging for FL.
    Variance reduction через control variates.
    """

    def __init__(self, model):
        self.model = model
        self.control_variate = None  # c_i
        self.server_control = None   # c

    def local_train(self, global_weights: Dict, server_control: Dict,
                    epochs: int, lr: float) -> Tuple[Dict, Dict, int]:
        """
        SCAFFOLD local update з control variate correction.
        """
        self.server_control = server_control
        self.model.set_weights(copy.deepcopy(global_weights))

        if self.control_variate is None:
            self.control_variate = {k: torch.zeros_like(v)
                                   for k, v in global_weights.items()}

        initial_weights = copy.deepcopy(global_weights)

        for epoch in range(epochs):
            for batch_x, batch_y in self.data_loader:
                # Compute gradient
                predictions = self.model(batch_x)
                loss = self.criterion(predictions, batch_y)
                loss.backward()

                # SCAFFOLD correction
                for name, param in self.model.named_parameters():
                    if param.grad is not None:
                        # g_i - c_i + c
                        param.grad.data += (
                            self.server_control[name] -
                            self.control_variate[name]
                        )

                self.optimizer.step()

        # Update local control variate
        final_weights = self.model.get_weights()
        new_control = {}
        for name in initial_weights.keys():
            # c_i^+ = c_i - c + (w^t - w^{t+1}) / (K * η)
            new_control[name] = (
                self.control_variate[name] - self.server_control[name] +
                (initial_weights[name] - final_weights[name]) / (epochs * lr)
            )

        delta_control = {
            name: new_control[name] - self.control_variate[name]
            for name in new_control.keys()
        }
        self.control_variate = new_control

        return final_weights, delta_control, len(self.dataset)

Secure Aggregation: криптографічний захист

Проблема: Навіть із DP, сервер бачить індивідуальні client updates.

Secure Aggregation: Сервер бачить ТІЛЬКИ суму, не окремі значення.

import secrets
from typing import Dict, List
import numpy as np

class SecureAggregationProtocol:
    """
    Secure Aggregation через pairwise masking.
    Сервер отримує тільки aggregate, не individual updates.
    """

    def __init__(self, num_clients: int, key_size: int = 256):
        self.num_clients = num_clients
        self.key_size = key_size
        self.shared_keys = {}  # Pairwise shared secrets

    def setup_phase(self, clients: List[str]):
        """
        Клієнти обмінюються ключами (Diffie-Hellman).
        """
        # Генеруємо pairwise shared secrets
        for i, client_i in enumerate(clients):
            for j, client_j in enumerate(clients):
                if i < j:
                    # В реальності — DH key exchange
                    shared_secret = secrets.token_bytes(self.key_size // 8)
                    key = (client_i, client_j)
                    self.shared_keys[key] = shared_secret

    def _generate_mask(self, seed: bytes, shape: tuple) -> np.ndarray:
        """
        Детерміновано генеруємо маску з shared secret.
        """
        rng = np.random.default_rng(int.from_bytes(seed[:8], 'big'))
        return rng.standard_normal(shape)

    def client_mask(self, client_id: str, update: np.ndarray,
                    all_clients: List[str]) -> np.ndarray:
        """
        Клієнт маскує свій update.
        Сума масок = 0 (вони попарно скасовуються).
        """
        masked_update = update.copy()

        for other_client in all_clients:
            if other_client == client_id:
                continue

            # Знаходимо shared key
            if client_id < other_client:
                key = (client_id, other_client)
                sign = 1
            else:
                key = (other_client, client_id)
                sign = -1

            if key in self.shared_keys:
                mask = self._generate_mask(self.shared_keys[key], update.shape)
                masked_update += sign * mask

        return masked_update

    def server_aggregate(self, masked_updates: List[np.ndarray]) -> np.ndarray:
        """
        Сервер просто сумує — маски скасовуються.
        """
        # Маски попарно рівні з протилежними знаками
        # Σ masks = 0
        # Тому Σ masked_updates = Σ original_updates
        return np.sum(masked_updates, axis=0)


class HomomorphicAggregation:
    """
    Альтернатива: Homomorphic Encryption.
    Сервер обчислює на зашифрованих даних.
    """

    def __init__(self, key_size: int = 2048):
        # В реальності: Paillier, CKKS, або BFV схеми
        self.public_key = None
        self.private_key = None
        self._generate_keys(key_size)

    def _generate_keys(self, key_size: int):
        """Генеруємо ключі для гомоморфного шифрування."""
        # Simplified — в реальності використовуйте tenseal, seal-python
        pass

    def encrypt_update(self, update: np.ndarray) -> 'EncryptedTensor':
        """Клієнт шифрує свій update."""
        return self._encrypt(update, self.public_key)

    def aggregate_encrypted(self,
                           encrypted_updates: List['EncryptedTensor']
                           ) -> 'EncryptedTensor':
        """
        Сервер сумує ЗАШИФРОВАНІ updates.
        Гомоморфна властивість: E(a) + E(b) = E(a + b)
        """
        result = encrypted_updates[0]
        for enc_update in encrypted_updates[1:]:
            result = result + enc_update  # Homomorphic addition
        return result

    def decrypt_aggregate(self,
                         encrypted_sum: 'EncryptedTensor') -> np.ndarray:
        """Тільки власник private key може розшифрувати."""
        return self._decrypt(encrypted_sum, self.private_key)

Практична реалізація з Flower Framework

Flower — найпопулярніший production-ready FL framework.

import flwr as fl
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from collections import OrderedDict
from typing import Dict, List, Tuple

# Define model
class CNNModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 32, 3, padding=1)
        self.conv2 = nn.Conv2d(32, 64, 3, padding=1)
        self.pool = nn.MaxPool2d(2, 2)
        self.fc1 = nn.Linear(64 * 7 * 7, 128)
        self.fc2 = nn.Linear(128, 10)
        self.relu = nn.ReLU()
        self.dropout = nn.Dropout(0.5)

    def forward(self, x):
        x = self.pool(self.relu(self.conv1(x)))
        x = self.pool(self.relu(self.conv2(x)))
        x = x.view(-1, 64 * 7 * 7)
        x = self.relu(self.fc1(x))
        x = self.dropout(x)
        return self.fc2(x)


class FlowerClient(fl.client.NumPyClient):
    """
    Production-ready Flower client.
    """

    def __init__(self, model: nn.Module, trainloader: DataLoader,
                 testloader: DataLoader, device: str = "cpu"):
        self.model = model.to(device)
        self.trainloader = trainloader
        self.testloader = testloader
        self.device = device

    def get_parameters(self, config: Dict) -> List[np.ndarray]:
        """Повертаємо параметри моделі як numpy arrays."""
        return [val.cpu().numpy() for _, val in self.model.state_dict().items()]

    def set_parameters(self, parameters: List[np.ndarray]):
        """Завантажуємо параметри в модель."""
        params_dict = zip(self.model.state_dict().keys(), parameters)
        state_dict = OrderedDict({k: torch.tensor(v) for k, v in params_dict})
        self.model.load_state_dict(state_dict, strict=True)

    def fit(self, parameters: List[np.ndarray],
            config: Dict) -> Tuple[List[np.ndarray], int, Dict]:
        """
        Локальне тренування.
        Викликається сервером кожен раунд.
        """
        self.set_parameters(parameters)

        epochs = config.get("local_epochs", 1)
        lr = config.get("learning_rate", 0.01)

        # Train
        self.model.train()
        optimizer = torch.optim.SGD(self.model.parameters(), lr=lr)
        criterion = nn.CrossEntropyLoss()

        total_loss = 0
        for epoch in range(epochs):
            for batch in self.trainloader:
                images, labels = batch[0].to(self.device), batch[1].to(self.device)
                optimizer.zero_grad()
                loss = criterion(self.model(images), labels)
                loss.backward()
                optimizer.step()
                total_loss += loss.item()

        return (
            self.get_parameters(config={}),
            len(self.trainloader.dataset),
            {"loss": total_loss / len(self.trainloader)}
        )

    def evaluate(self, parameters: List[np.ndarray],
                 config: Dict) -> Tuple[float, int, Dict]:
        """Оцінка на тестових даних."""
        self.set_parameters(parameters)

        self.model.eval()
        criterion = nn.CrossEntropyLoss()

        total_loss = 0
        correct = 0
        total = 0

        with torch.no_grad():
            for batch in self.testloader:
                images, labels = batch[0].to(self.device), batch[1].to(self.device)
                outputs = self.model(images)
                loss = criterion(outputs, labels)
                total_loss += loss.item()

                _, predicted = torch.max(outputs.data, 1)
                total += labels.size(0)
                correct += (predicted == labels).sum().item()

        accuracy = correct / total
        avg_loss = total_loss / len(self.testloader)

        return avg_loss, len(self.testloader.dataset), {"accuracy": accuracy}


# Custom aggregation strategy
class DPFedAvg(fl.server.strategy.FedAvg):
    """
    FedAvg з Differential Privacy на сервері.
    """

    def __init__(self, noise_multiplier: float = 0.1,
                 max_grad_norm: float = 1.0, **kwargs):
        super().__init__(**kwargs)
        self.noise_multiplier = noise_multiplier
        self.max_grad_norm = max_grad_norm

    def aggregate_fit(self, server_round: int,
                      results: List[Tuple[fl.server.client_proxy.ClientProxy,
                                         fl.common.FitRes]],
                      failures: List) -> Tuple[fl.common.Parameters, Dict]:
        """Агрегація з DP noise."""

        # Standard FedAvg aggregation
        aggregated_parameters, metrics = super().aggregate_fit(
            server_round, results, failures
        )

        if aggregated_parameters is not None:
            # Add DP noise
            ndarrays = fl.common.parameters_to_ndarrays(aggregated_parameters)
            noised_ndarrays = []

            for arr in ndarrays:
                noise = np.random.normal(
                    0,
                    self.noise_multiplier * self.max_grad_norm,
                    arr.shape
                )
                noised_ndarrays.append(arr + noise)

            aggregated_parameters = fl.common.ndarrays_to_parameters(noised_ndarrays)

        return aggregated_parameters, metrics


# Server startup
def start_server():
    strategy = DPFedAvg(
        fraction_fit=0.3,  # 30% clients per round
        fraction_evaluate=0.2,
        min_fit_clients=2,
        min_evaluate_clients=2,
        min_available_clients=5,
        noise_multiplier=0.1,
        max_grad_norm=1.0
    )

    fl.server.start_server(
        server_address="0.0.0.0:8080",
        config=fl.server.ServerConfig(num_rounds=50),
        strategy=strategy
    )


# Client startup
def start_client(client_id: int):
    model = CNNModel()
    trainloader, testloader = load_data_partition(client_id)

    client = FlowerClient(model, trainloader, testloader)

    fl.client.start_numpy_client(
        server_address="localhost:8080",
        client=client
    )

Real-World Deployments: хто вже використовує FL

1. Google Gboard (2017+)

  • Next-word prediction на мільярдах devices
  • On-device training у background
  • Model updates завантажуються через WiFi вночі
  • Privacy: Google ніколи не бачить, що ви друкуєте

2. Apple (2019+)

  • Siri voice recognition improvement
  • QuickType keyboard suggestions
  • Differential privacy on device + secure aggregation
  • Повна приватність для користувачів

3. NVIDIA FLARE (Healthcare)

  • Mammography AI: 20 hospitals collaboration
  • Без sharing patient data (HIPAA compliant)
  • 30% improvement в rare cancer detection
  • Кожна лікарня зберігає свої дані локально

4. WeBank (Finance)

  • Fraud detection across institutions
  • Regulatory compliant cross-bank ML
  • Федеративна feature engineering
  • Конкуренти співпрацюють без sharing даних

Бенчмарки: FedAvg vs FedProx vs SCAFFOLD

| Метрика | FedAvg | FedProx (μ=0.01) | SCAFFOLD |

|---------|--------|------------------|----------|

| IID Accuracy | 97.2% | 97.1% | 97.3% |

| Non-IID Accuracy | 89.4% | 93.1% | 95.2% |

| Rounds to converge (IID) | 50 | 48 | 35 |

| Rounds to converge (Non-IID) | 150 | 85 | 60 |

| Communication overhead | 1x | 1x | 2x |

| Client computation | 1x | 1.1x | 1.2x |


Ідеї для дослідження

Для бакалавра:

  • Implement FedAvg на MNIST/CIFAR-10
  • Порівняння IID vs non-IID settings
  • Вимірювання communication cost
  • Візуалізація convergence curves

Для магістра:

  • Додавання Differential Privacy до FL
  • Personalized Federated Learning (local fine-tuning)
  • Gradient compression techniques (Top-K, Random-K)
  • Cross-device vs cross-silo FL порівняння

Для PhD:

  • Novel aggregation algorithms для extreme non-IID
  • Theoretical privacy guarantees з moments accountant
  • Byzantine-robust aggregation (захист від malicious clients)
  • Federated Learning для foundation models (LLM)

Висновок: чому FL — це майбутнє

GDPR в Європі. CCPA в Каліфорнії. HIPAA для медицини. LGPD в Бразилії. Регуляції стають жорсткішими з кожним роком. Централізувати дані — все складніше юридично, етично, технічно.

Federated Learning — це не компроміс. Це краще рішення:

  • Більше даних — бо люди погоджуються ділитися
  • Більше privacy — бо дані не витікають
  • Compliance by design — бо дані не покидають юрисдикцію
  • Менше ризику — бо немає central point of failure

Хто освоїть FL зараз — буде тренувати моделі на даних, до яких конкуренти просто не мають доступу. Якщо ви плануєте дослідження в цій галузі, команда SKP-Degree на skp-degree.com.ua готова допомогти з реалізацією федеративних систем будь-якої складності. Для консультацій пишіть у Telegram: @kursovi_diplomy.


Federated Learning, differential privacy, приватність даних, GDPR compliance, Flower framework, PySyft, розподілене машинне навчання, edge AI — це основні концепції для дипломної чи магістерської роботи з децентралізованого штучного інтелекту та privacy-preserving ML.

Про автора

Команда SKP-Degree

Верифікований автор

Розробники та дослідники AI · Python, TensorFlow, PyTorch · Досвід у промисловій розробці

Команда SKP-Degree — професійні розробники з досвідом 7+ років у промисловій розробці. Виконали 1000+ проєктів для студентів з України, Польщі та країн Балтії.

Python Django Java ML/AI React C# / .NET JavaScript

Потрібна допомога з роботою?

Замовте курсову чи дипломну роботу з програмування. Оплата після демонстрації!

Без передоплати Відеодемонстрація Автономна робота 24/7
Написати в Telegram