Przejdź do treści

Co to jest uczenie federacyjne (Federated Learning)?

Dzisiejszy artykuł jest inny niż wszystkie do tej pory…

Jakiś czas temu napisał do mnie Adam Narożniak, Data Scientist z Flower z pytaniem, czy mógłby napisać artykuł na moim blogu o uczeniu federacyjnym. Przyznam, że wcześniej nie miałem bladego pojęcia o tej działce uczenia maszynowego i stwierdziłem, że pewnie nie jestem jedynym data scientistem, który niewiele wie na ten temat. Odpisałem Adamowi, że chętnie zamieszczę jego wpis, aby z jednej strony samemu nauczyć się czegoś nowego, ale z drugiej aby dać moim czytelnikom taką samą możliwość.

Adam tak pisze o sobie:

Jestem entuzjastą uczenia maszynowego, szczególnie ciekawi mnie audio i sektor medyczny. Wierzę, że przyszłość przyniesie nam wiele uczenia federacyjnego i zamierzam jej w tym pomóc poprzez implementację wielu praktycznych zastosowań i rozwój potrzebnej teorii.

Chciałem jeszcze uściślić, że poniższy artykuł nie jest w żaden sposób sponsorowany. Służy jedynie szerzeniu wiedzy o nowym (przynajmniej dla mnie nowym) zagadnieniu związanym z uczeniem maszynowym.

Zatem zapraszam do lektury!

Wstęp

Aby zrozumieć uczenie federacyjne przypomnijmy sobie, jak wygląda uczenie klasyczne i pomyślmy, jakie potencjalne problemy możemy napotkać.

Na nasze potrzeby wyróżnimy dwa niezbędne faktory potrzebne do zastosowania sztucznej inteligencji:

  • dane,
  • model.

Uczymy model przy użyciu danych, aby wykonać przydatne zadanie. Zadaniem może być wykrywanie obiektów na obrazach, transkrypcja nagrania dźwiękowego lub granie w grę taką jak Go.

Wiele wyborów dotyczących modelu takich jak architektura, optymalizator, hiperparametry zależy od nas. 

Dane najczęściej wymagają dodatkowego przetwarzania tak, aby stworzyć wartościowe dane wejściowe do modelu. Dodatkowo wypełniamy brakujące wartości, usuwamy niepotrzebne cechy i na przykład całe dane normalizujemy przed wpuszczeniem do modelu. 

Najprawdopodobniej znasz ten schemat. Zastanówmy się jednak, jak dane stały się dla nas dostępne. Często po prostu mamy je zgromadzone i nie musimy martwić się, czy ktoś miał problemy np. z wrzuceniem ich na dysk. Nie interesuje nas także, czy ktoś przesyłał je do chmury, czy za pomocą płyty DVD lub pendrive, ani czy zostały stworzone przez jedną osobę, czy wiele. Dane mamy po prostu zgromadzone i scentralizowane w jednym miejscu. 

Gdzie leży problem?

W praktyce dane, z którymi pracujemy, nie pochodzą z jednego urządzenia/użytkownika, szczególnie z maszyny, na której trenujemy model. Dane są tworzone lub gromadzone w innym miejscu, na przykład:

  • w telefonie podczas interakcji z aplikacją,
  • w samochodzie, który zbiera dane z czujników,
  • w laptopie, który odbiera dane z klawiatury,
  • w inteligentnym asystencie głosowym z mikrofonu, który słucha wydawanych poleceń,
  • w oddziale szpitala. 

Jak sam widzisz, źródeł danych może być wiele, tzn. może to być każda osoba z zainstalowaną na smartfonie aplikacją (kilkaset tysięcy) lub kilka oddziałów banków, natomiast nie jest to jedna jednostka. 

Uczenie federacyjne vs klasyczne

Kiedy dane dostępne są w wielu miejscach, możemy zastosować uczenie federacyjne.

Główną różnicę pomiędzy uczeniem klasycznym a federacyjnym dobrze obrazują dwie następujące instrukcje:

  • klasyczne uczenie maszynowe: przenieś dane w jedno miejsce (gdzie nastąpi trening),
  • federacyjne uczenie maszynowe: przenieś obliczenia do “(miejsca) danych” (urządzeń, na których dane się znajdują).

Miejsce ze zgromadzonymi danymi nazywane jest często serwerem, natomiast poszczególne urządzenia z małymi fragmentami danych (np. wyprodukowanymi i/lub zgromadzonymi tylko przez urządzenia samodzielnie) klientami, węzłami klienckimi lub po prostu węzłami.

Możesz się zastanawiać, dlaczego po prostu nie prześlemy danych w jedno miejsce. Niestety nie zawsze jest to możliwe.

Dlaczego klasyczne scentralizowane podejście do uczenia maszynowego nie działa?

Istnieje wiele powodów, natomiast najważniejsze to:

  • przepisy – regulacje takie jak m.in. GDPR (Europa), CCPA (Kalifornia), PIPEDA (Kanada), LGPD (Brazylia), PDPL (Argentyna), KVKK (Turcja), POPI (RPA), FSS (Rosja), CDPR (Chiny), PDPB (Indie), PIPA (Korea), APPI (Japonia), PDP (Indonezja), PDPA (Singapur), APP (Australia) chronią dane przed ich przenoszeniem. W rzeczywistości przepisy te czasami nawet uniemożliwiają pojedynczym organizacjom łączenie danych własnych użytkowników w celu trenowania modeli sztucznej inteligencji. Wynika to z faktu, że użytkownicy mieszkają w różnych częściach świata, więc ich dane podlegają różnym przepisom dotyczącym ochrony danych,
  • prywatność danych / preferencje użytkowników – np. w finansach ze względu na konieczność zachowania prywatności klientów nie ma możliwości udostępniania sobie nawzajem przez instytucje finansowe wrażliwych danych, które mogłyby posłużyć do budowy wspólnych modeli (np. identyfikujących nieuczciwe transakcje, przewidujących ceny akcji lub zarządzania ryzykiem),
  • ilość danych – niektóre czujniki, takie jak kamery, wytwarzają tak dużą ilość danych, że zebranie ich wszystkich nie jest ani wykonalne, ani opłacalne. Pomyślmy o krajowej sieci kolejowej z setkami stacji kolejowych w całym kraju. Jeśli każdy z dworców jest wyposażony w pewną liczbę kamer bezpieczeństwa, to ilość danych, które one wytwarzają, wymaga niezwykle wydajnej i kosztownej infrastruktury do przetwarzania i przechowywania. A większość z tych danych nie jest nawet przydatna.

Jak sam widzisz uczenie federacyjne może być przydatne oraz ma wiele zastosowań i potencjalnie świetlaną przyszłość!

Uczenie federacyjne vs klasyczne (graficznie)

Zobrazujmy to, co wiemy, za pomocą grafik.

Zaczniemy od legendy, aby wszystkie ilustracje były jasne.

Uczenie klasyczne możemy przedstawić za pomocą poniższej grafiki.

Uczenie federacyjne pozostawia parę pytań. Na ten moment wiemy, że mamy do czynienia z wieloma klientami, którzy mają dostęp do danych lokalnych oraz wiemy, że trening musi odbywać się lokalnie. Możemy się domyślać, że muszą istnieć jakieś relacje pomiędzy lokalnymi modelami. Moglibyśmy pomyśleć o połączeniu każdego urządzenia z każdym, natomiast nie jest to praktyczne rozwiązanie. Wprowadzimy zatem pojęcie serwera, który w niewiadomy (na razie) dla nas sposób współpracuje z klientami. Spójrzmy na grafikę poniżej.

Przejdźmy teraz do detali pokazujących, jak to jest możliwe.

Jak działa uczenie federacyjne?

Krok 0: Inicjalizacja modelu globalnego

Zaczynamy od inicjalizacji modelu na serwerze. Jest to dokładnie to samo, co w klasycznym uczeniu scentralizowanym: inicjalizujemy parametry modelu losowo lub z wcześniej zapisanego punktu kontrolnego.

Krok 1: Wysyłamy model do pewnej liczby połączonych organizacji/urządzeń (klientów)

Następnie wysyłamy parametry modelu globalnego do podłączonych klientów. Ma to na celu zapewnienie, że każdy uczestniczący węzeł rozpoczyna swoje lokalne szkolenie przy użyciu tych samych parametrów modelu (oczywiście architektura modelu też jest taka sama, natomiast teoretycznie optymalizator mógłby być inny, nie jest to jednak spotykane zastosowanie). Często używamy tylko jakiejś frakcji (np. kilku/kilkunastu) połączonych węzłów zamiast wszystkich. Powodem tego jest to, że wybieranie coraz większej liczby węzłów klienckich przynosi malejące zyski.

Krok 2: Trenujemy modele lokalnie na danych każdej organizacji/urządzenia (węzła klienckiego)

Teraz gdy wybrane do treningu węzły klienckie (całość lub część – ta, o której wspomniałem wyżej) mają najnowszą wersję parametrów modelu globalnego, rozpoczynają trening lokalny. Używają własnego lokalnego zbioru danych do trenowania własnych modeli lokalnych. Nie trenują modelu aż do pełnego zbiegnięcia. Trenują go tylko przez chwilę. To może być tak mało, jak jedna epoka lub nawet tylko kilka kroków (mini-batch’ów) na lokalnych danych.

Krok 3: Zwracanie aktualizacji modeli (lokalnych) do serwera

Po lokalnym treningu każdy węzeł kliencki ma nieco inną wersję parametrów modelu, które pierwotnie otrzymał. Parametry są różne, ponieważ każdy węzeł kliencki ma inne przykłady w swoim lokalnym zbiorze danych. Węzły klienckie wysyłają następnie te aktualizacje modelu z powrotem do serwera. Wysyłane aktualizacje modelu mogą być albo pełnymi parametrami modelu, albo tylko gradientami, które zostały zgromadzone podczas lokalnego treningu.

Krok 4: Agregacja aktualizacji modeli lokalnych w nowy model globalny

Serwer otrzymuje aktualizację modelu (jego wagi lub gradienty) od wybranych węzłów klienckich. Jeśli wybrał 100 węzłów klienckich, ma teraz 100 nieco różnych wersji oryginalnego modelu globalnego, z których każda została wytrenowana na lokalnych danych jednego klienta. Ale czy nie chcieliśmy mieć jednego modelu, który uczyłby się na danych ze wszystkich 100 węzłów klienckich?

Aby uzyskać jeden model, musimy połączyć wszystkie aktualizacje modelu, które otrzymaliśmy od węzłów klienckich. Proces ten nazywany jest agregacją i istnieje wiele różnych sposobów, aby to zrobić. Najbardziej podstawowy to Federated Averaging (można to przetłumaczyć jako sfederalizowane uśrednianie) (McMahan et al., 2016), często skracany jako FedAvg.

FedAvg bierze 100 aktualizacji modelu i, jak sama nazwa wskazuje, uśrednia je. Aby być bardziej precyzyjnym, bierze średnią ważoną aktualizacji modelu, ważoną przez liczbę przykładów, które każdy klient użył do szkolenia. Na przedstawionej grafice dla ułatwienia  i, nie jest brana pod uwagę wielkość danych każdego klienta. W praktyce jest to ważny krok i powinien być zastosowany. Waga jest ważna, aby upewnić się, że każdy przykład danych ma taki sam wpływ na wynikowy model globalny. Jeśli jeden klient ma 10 przykładów, a inny klient ma 100 przykładów, to bez ważenia każdy z 10 przykładów wpłynąłby na model globalny dziesięć razy bardziej niż te ze 100 przykładów.

Krok 5: Powtarzaj kroki od 1 do 4, aż do zbieżności

Kroki od 1 do 4 są tym, co nazywamy pojedynczą rundą uczenia federacyjnego. Parametry modelu globalnego są wysyłane do uczestniczących węzłów klienckich (krok 1), węzły klienckie trenują na swoich lokalnych danych (krok 2), wysyłają swoje zaktualizowane modele do serwera (krok 3), a serwer następnie agreguje aktualizacje modelu, aby uzyskać nową wersję modelu globalnego (krok 4).

Podczas pojedynczej rundy każdy węzeł kliencki, który w niej uczestniczy, trenuje tylko przez chwilę. Oznacza to, że po etapie agregacji (krok 4) mamy model, który został wytrenowany na danych ze wszystkich węzłów klienckich, ale tylko przez chwilę. Następnie musimy powtarzać ten proces w kółko, aby w końcu uzyskać w pełni wytrenowany model, który działa dobrze na danych wszystkich węzłów.

Kod w Python

Do implementacji użyjemy biblioteki Flower. Pozwala ona na tworzenie zarówno lokalnych symulacji (my użyjemy tej funkcjonalności), jak i implementacji gotowych do produkcji (firmy, które używają Flower to m.in. Brave, Nokia, czy Banking Circle). Trenując model, możesz używać ulubionego frameworku do uczenia maszynowego, takiego jak Tensorflow, PyTorch, Hugging Face czy fastai. 

Jeśli chciałbyś sam sprawdzić, jak działa kod bez instalacji bibliotek lokalnie, to możesz użyć identycznej kopii kodu dostępnej w Google Colab (KLIKNIJ TUTAJ).

Mirek: Na wszelki wypadek gdyby kiedyś link od Adama wygasł to przygotowałem dla Was też notebook do pobrania bezpośrednio z bloga wraz z informacją o wersjach bibliotek itp. Możecie go bezpośrednio załadować sobie na Colaba, by działał.

Zależności

Przygotujmy potrzebne biblioteki.

# Pobierz biblioteki
!pip install -q flwr[simulation] tensorflow
# Importuj biblioteki
import os
from functools import partial

import flwr as fl
import numpy as np
import tensorflow as tf
from tensorflow.keras.datasets import mnist
from tensorflow.keras.layers import Conv2D, Dense, Dropout, Flatten, \
                                    Input, MaxPooling2D
from tensorflow.keras.models import Model
from platform import python_version
print(f'python: {python_version()}')
print(f"\nflwr: {fl.__version__}")
print(f"numpy: {np.__version__}")
print(f"tensorflow: {tf.__version__}")

Skonfigurujmy jeszcze TensorFlow tak, aby używać tylko CPU.

# Użyjmy tylko CPU
tf.config.set_visible_devices([], 'GPU')

Dane

Do tego eksperymentu pobierzmy jeszcze standardowy dobrze wszystkim znany zbiór cyfr pisanych ręcznie MNIST.

# Załadujmy zbiór dancyh MNIST
(x_train, y_train), (x_test, y_test) = mnist.load_data()

# Przeskalujmy dane do [0, 1]
x_train = x_train / 255.0
x_test = x_test / 255.0

# Dodajmy dodatkowy wymiar to danych (wymagane przez Conv2D)
x_train = x_train.reshape(x_train.shape[0], 28, 28, 1)
x_test = x_test.reshape(x_test.shape[0], 28, 28, 1)

Po pobraniu danych należy podzielić je tak, aby każdy klient miał unikalną część danych.

Uwaga! Symulujemy uczenie federacyjne, w normalnym scenariuszu dane będą znajdowały się u różnych klientów.

Każdy klient będzie miał dane treningowe i testowe, co nie jest wymogiem uczenia federacyjnego, ale powszechnym sposobem ewaluacji. 

Istnieją dwa powszechne sposoby ewaluacji:

  •  scentralizowana (inaczej mówiąc po stronie serwera) – co oznacza, że mamy jakąś część danych na serwerze i oceniamy globalny model na tej części danych,
  • sfederalizowana (inaczej mówiąc po stronie klienta) – w tym przypadku potrzebujemy, aby każdy klient miał podzielone dane;liczba ewaluacji na rundę jest równa liczbie klientów, których wybieramy do ewaluacji. 

Wybierzemy drugi sposób do oceny w analizowanym przykładzie.

# Podzielmy dane na 5 klientów
NUM_CLIENTS = 5
x_split = np.split(x_train, NUM_CLIENTS)
y_split = np.split(y_train, NUM_CLIENTS)

# Stwórzmy mapę cid (id klient) do danych
# Dla danych treningowych i testowych
num_data_in_split = x_split[0].shape[0]
train_split = 0.8
x_trains, y_trains, x_tests, y_tests = {}, {}, {}, {}
for idx, (client_x, client_y) in enumerate(zip(x_split, y_split)):
   train_end_idx = int(0.8 * num_data_in_split)
   x_trains[str(idx)] = client_x[:train_end_idx]
   y_trains[str(idx)] = client_y[:train_end_idx]
   x_tests[str(idx)] = client_x[train_end_idx:]
   y_tests[str(idx)] = client_y[train_end_idx:]

Potwierdźmy sobie jeszcze rozmiar danych dla każdej próbki:

for i in ['0','1','2','3','4']:
  print(f"x_trains['{i}'].shape = {x_trains[i].shape}; \
  x_tests['{i}'].shape = {x_tests[i].shape};")

Model

Zdefiniujemy model (dokładnie w ten sam sposób, jak pod klasyczne uczenie maszynowe).

class CNN(Model):
   def __init__(self):
       super(CNN, self).__init__()
       self.conv1 = Conv2D(32, (3, 3), activation='relu')
       self.pool1 = MaxPooling2D((2, 2))
       self.conv2 = Conv2D(64, (3, 3), activation='relu')
       self.pool2 = MaxPooling2D((2, 2))
       self.flatten = Flatten()
       self.dense1 = Dense(128, activation='relu')
       self.dropout = Dropout(0.5)
       self.dense2 = Dense(10, activation='softmax')

   def call(self, inputs):
       x = self.conv1(inputs)
       x = self.pool1(x)
       x = self.conv2(x)
       x = self.pool2(x)
       x = self.flatten(x)
       x = self.dense1(x)
       x = self.dropout(x)
       return self.dense2(x)

Uczenie federacyjne

Teraz stworzymy kilka klas i funkcji, aby wykorzystać framework Flower.

Do uruchomienia symulacji potrzebne będą następujące elementy:

  • klasa Klient Flower z trzema metodami do zaimplementowania,
  • funkcja, która tworzy Klienta Flower,
  • strategia, która definiuje algorytm federacji.

Klient Flower

Nadpiszemy (eng. overwrite) klasę NumpyClient. Musimy zaimplementować trzy metody klasy FlowerClient: fit, evaluate i get_parameters.

# Zdefiniujmy Klienta Flower
class FlowerClient(fl.client.NumPyClient):
   def __init__(self, model, X_train, y_train, X_test, y_test):
       self.model = model
       self.model.build((32, 28, 28, 1))
       self.X_train = X_train
       self.y_train = y_train
       self.X_test = X_test
       self.y_test = y_test

   def get_parameters(self, config):
       return self.model.get_weights()

   def fit(self, parameters, config):
       self.model.compile("adam", "sparse_categorical_crossentropy", 
                          metrics=["accuracy"])
       self.model.set_weights(parameters)
       self.model.fit(self.X_train, self.y_train, 
                      epochs=1, batch_size=32, verbose=0)
       return self.model.get_weights(), len(x_train), {}

   def evaluate(self, parameters, config):
       self.model.compile("adam", "sparse_categorical_crossentropy", 
                          metrics=["accuracy"])
       self.model.set_weights(parameters)
       loss, accuracy = self.model.evaluate(self.X_test, self.y_test, 
                                            batch_size=32, verbose=0)
       return loss, len(self.X_test), {"accuracy": accuracy}

Funkcja tworząca klienta

Symulacja Flower wymaga funkcji, która tworzy Klienta Flower. Wymaga ona tylko jednego argumentu: id klienta (w skrócie cid). W tej funkcji używamy mapy, której kluczami są cid, a wartościami są partycje zbioru danych. Tę mapę i inne parametry można przekazać za pomocą funkcji partial z biblioteki functools Pythona.

# Pełna funcja tworząca jednego clienta Flower
def create_client(
   cid, model_class, x_trains, y_trains, x_tests, y_tests
) -> FlowerClient:
   """Create a Flower client representing a single organization."""
   model = model_class()
   # Create a  single Flower client representing a single organization
   return FlowerClient(model, x_trains[cid], y_trains[cid], x_tests[cid], y_tests[cid])

# Funkcja tworząca jednego klienta Flower z tylko jednym argumentem - cid
# Reszta argumentów jest stała
client_fnc = partial(
   create_client,
   model_class=CNN,
   x_trains=x_trains,
   y_trains=y_trains,
   x_tests=x_tests,
   y_tests=y_tests,
)

Strategia

Po każdej rundzie uczenia federacyjnego, potrzebujemy sposobu, aby zagregować wagi. Istnieje kilka powszechnie stosowanych metod. My użyjemy domyślnej i najbardziej powszechnej strategii zwanej Federated Averaging (w skrócie FedAvg).

Potrzebujemy też funkcji, która opisuje jak obliczane będą uśrednione metryki.

# Uśrednianie metryk
def weighted_average(metrics):
   print(metrics)
   accuracies = [num_examples * m["accuracy"] for num_examples, m in metrics]
   examples = [num_examples for num_examples, _ in metrics]
   return {"accuracy": int(sum(accuracies)) / int(sum(examples))}

# Stwórzmy strategię FedAvg
strategy = fl.server.strategy.FedAvg(
   fraction_fit=1.0,  # Samplujmy 100% dostępnych klientów na trening
   fraction_evaluate=1.0,  # Samplujmy 100% dostępnych klientów na evaluację
   min_fit_clients=5,  # Nie samplujmy mniej niż 5 klientów na trening
   min_evaluate_clients=5,  #Nie samplujmy mniej niż 5 klientów na evaluację
   min_available_clients=5,  # Poczekaj aż 5 klientów jest dostępnych
   evaluate_metrics_aggregation_fn=weighted_average, # Uśrednianie metryk
)

Trening

W start_simulation określmy szczegóły treningu. Będziemy trenować przez dziesięć rund i będziemy trenować klientów sekwencyjnie (jeden za drugim). Ostatni argument w poniższym kodzie konfiguruje Ray (bibliotekę, której używamy do uruchomienia symulacji) tak, aby działała wydajniej. Podawanie go nie będzie potrzebne w przyszłych wersjach Flower (≥1.4.0).

fl.simulation.start_simulation(
   client_fn=client_fnc,
   num_clients=NUM_CLIENTS,
   config=fl.server.ServerConfig(num_rounds=10),
   strategy=strategy,
   client_resources={"num_cpus": 1, "num_gpus": 0},
   ray_init_args={
       "num_cpus": 1,
       "num_gpus": 0,
       "_system_config": {"automatic_object_spilling_enabled": False},
   },
)

Gratulacje, wytrenowałeś swój pierwszy model przy użyciu uczenia federacyjnego.

Terminologia

Uczenie federacyjne jest na razie stosunkowo mało popularne w Polsce. Z tego powodu uniwersalne nazwy nie są jeszcze przyjęte. Możecie spotkać następujące tłumaczenia: “sfederowana nauka, “uczenie się federacyjne” lub uczenie sfederalizowane. Wszystkie dotyczą tego samego konceptu. Terminologia użyta w tym artykule, to moja propozycja nomenklatury.

Podsumowanie

Właśnie dowiedziałeś się czym jest uczenie federacyjne oraz jak wytrenować model przy użyciu Flower i TensorFlow. Jeśli chciałbyś dowiedzieć się więcej, to zapraszam na oryginalną stronę i dokumentację Flower (strona jest w języku angielskim) https://flower.dev/. Jeśli masz jakieś pytania, możesz je zadać na slack’u Flower https://friendly-flower.slack.com/join/shared_invite/zt-q1t73p0h-fcGXz6lRDPdSrwYhBvaDFA#/shared-invite/email

Kilka słów ode mnie

Bardzo dziękuję Adamowi za podzielenie się swoją wiedzą i za czas poświęcony na napisanie artykułu. Mam nadzieję, że nie będzie to nasza ostatnia współpraca.

Jestem ciekawy Waszych opinii na temat gościnnych wpisów na blogu. Dajcie znać, co o tym myślicie.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Spodobało Ci się? Udostępnij ten post w social mediach! ❤️❤️❤️