Ta strona została przetłumaczona przez Cloud Translation API.
Switch to English

Federated Learning for Image Classification

Zobacz na TensorFlow.org Uruchom w Google Colab Wyświetl źródło na GitHub

W tym samouczku wykorzystamy klasyczny przykład szkoleniowy MNIST, aby wprowadzić warstwę Federated Learning (FL) API TFF, tff.learning - zestaw interfejsów wyższego poziomu, które mogą być używane do wykonywania typowych typów zadań uczenia federacyjnego, takich jak szkolenie federacyjne w porównaniu z modelami dostarczonymi przez użytkowników zaimplementowanymi w TensorFlow.

Ten samouczek i Federated Learning API są przeznaczone głównie dla użytkowników, którzy chcą podłączyć własne modele TensorFlow do TFF, traktując ten ostatni głównie jako czarną skrzynkę. Aby uzyskać bardziej szczegółowe informacje na temat TFF i sposobu implementacji własnych algorytmów uczenia federacyjnego, zobacz samouczki dotyczące interfejsu API FC Core - niestandardowe algorytmy federacyjne część 1 i część 2 .

Aby uzyskać więcej informacji na temat tff.learning , przejdź do samouczka Federated Learning for Text Generation , który oprócz tff.learning powtarzających się modeli, demonstruje również ładowanie wstępnie wytrenowanego zserializowanego modelu Keras w celu udoskonalenia za pomocą uczenia federacyjnego połączonego z oceną za pomocą Keras.

Zanim zaczniemy

Zanim zaczniemy, wykonaj poniższe czynności, aby upewnić się, że środowisko jest poprawnie skonfigurowane. Jeśli nie widzisz powitania, zapoznaj się z instrukcją instalacji .


!pip install --quiet --upgrade tensorflow_federated_nightly
!pip install --quiet --upgrade nest_asyncio

import nest_asyncio
nest_asyncio.apply()

%load_ext tensorboard
import collections

import numpy as np
import tensorflow as tf
import tensorflow_federated as tff

np.random.seed(0)

tff.federated_computation(lambda: 'Hello, World!')()
b'Hello, World!'

Przygotowanie danych wejściowych

Zacznijmy od danych. Uczenie stowarzyszone wymaga stowarzyszonego zestawu danych, tj. Zbioru danych od wielu użytkowników. Dane sfederowane są zazwyczaj nieidentyfikowane , co stwarza unikalny zestaw wyzwań.

Aby ułatwić eksperymentowanie, wyposażyliśmy repozytorium TFF kilkoma zestawami danych, w tym sfederowaną wersją MNIST, która zawiera wersję oryginalnego zbioru danych NIST , który został ponownie przetworzony przy użyciu Leaf, tak aby dane były kluczowane przez oryginalnego autora cyfry. Ponieważ każdy moduł zapisujący ma unikalny styl, ten zestaw danych wykazuje rodzaj nie-iid zachowania oczekiwany od sfederowanych zestawów danych.

Oto jak możemy to załadować.

emnist_train, emnist_test = tff.simulation.datasets.emnist.load_data()

Zestawy danych zwrócone przez load_data() są instancjami tff.simulation.ClientData , interfejsu, który umożliwia wyliczenie zbioru użytkowników, skonstruowanie tf.data.Dataset który reprezentuje dane konkretnego użytkownika, oraz zapytanie struktura poszczególnych elementów. Oto, jak możesz użyć tego interfejsu do eksploracji zawartości zestawu danych. Należy pamiętać, że chociaż ten interfejs umożliwia iterację po identyfikatorach klientów, jest to tylko funkcja danych symulacji. Jak zobaczysz wkrótce, tożsamości klientów nie są używane przez federacyjną platformę uczenia się - ich jedynym celem jest umożliwienie wyboru podzbiorów danych do symulacji.

len(emnist_train.client_ids)
3383
emnist_train.element_type_structure
OrderedDict([('pixels', TensorSpec(shape=(28, 28), dtype=tf.float32, name=None)), ('label', TensorSpec(shape=(), dtype=tf.int32, name=None))])
example_dataset = emnist_train.create_tf_dataset_for_client(
    emnist_train.client_ids[0])

example_element = next(iter(example_dataset))

example_element['label'].numpy()
1
from matplotlib import pyplot as plt
plt.imshow(example_element['pixels'].numpy(), cmap='gray', aspect='equal')
plt.grid(False)
_ = plt.show()

png

Badanie heterogeniczności danych stowarzyszonych

Dane sfederowane zwykle nie są iid , użytkownicy zazwyczaj mają różne dystrybucje danych w zależności od wzorców użytkowania. Niektórzy klienci mogą mieć mniej przykładów szkoleniowych na urządzeniu, cierpiąc z powodu braku danych lokalnie, podczas gdy niektórzy klienci będą mieli więcej niż wystarczającą liczbę przykładów szkoleniowych. Przyjrzyjmy się tej koncepcji heterogeniczności danych typowej dla systemu federacyjnego z dostępnymi danymi EMNIST. Należy zauważyć, że ta głęboka analiza danych klienta jest dostępna tylko dla nas, ponieważ jest to środowisko symulacyjne, w którym wszystkie dane są dostępne dla nas lokalnie. W prawdziwym, federacyjnym środowisku produkcyjnym nie byłby w stanie sprawdzić danych pojedynczego klienta.

Najpierw pobierzmy próbkę danych jednego klienta, aby zapoznać się z przykładami na jednym symulowanym urządzeniu. Ponieważ używany przez nas zestaw danych został wprowadzony przez unikalnego autora, dane jednego klienta reprezentują pismo odręczne jednej osoby dla próbki cyfr od 0 do 9, symulując unikalny „wzorzec użytkowania” jednego użytkownika.

## Example MNIST digits for one client
figure = plt.figure(figsize=(20, 4))
j = 0

for example in example_dataset.take(40):
  plt.subplot(4, 10, j+1)
  plt.imshow(example['pixels'].numpy(), cmap='gray', aspect='equal')
  plt.axis('off')
  j += 1

png

Teraz zwizualizujmy liczbę przykładów na każdym kliencie dla każdej etykiety cyfrowej MNIST. W środowisku stowarzyszonym liczba przykładów na każdym kliencie może się znacznie różnić w zależności od zachowania użytkownika.

# Number of examples per layer for a sample of clients
f = plt.figure(figsize=(12, 7))
f.suptitle('Label Counts for a Sample of Clients')
for i in range(6):
  client_dataset = emnist_train.create_tf_dataset_for_client(
      emnist_train.client_ids[i])
  plot_data = collections.defaultdict(list)
  for example in client_dataset:
    # Append counts individually per label to make plots
    # more colorful instead of one color per plot.
    label = example['label'].numpy()
    plot_data[label].append(label)
  plt.subplot(2, 3, i+1)
  plt.title('Client {}'.format(i))
  for j in range(10):
    plt.hist(
        plot_data[j],
        density=False,
        bins=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])

png

Teraz zwizualizujmy średni obraz na klienta dla każdej etykiety MNIST. Ten kod wygeneruje średnią wartości każdego piksela dla wszystkich przykładów użytkownika dla jednej etykiety. Zobaczymy, że przeciętny obraz jednego klienta dla cyfry będzie wyglądał inaczej niż przeciętny obraz innego klienta dla tej samej cyfry, ze względu na unikalny styl pisma każdej osoby. Możemy się zastanawiać, jak każda lokalna runda treningowa będzie popychać model w innym kierunku u każdego klienta, ponieważ uczymy się na podstawie własnych unikalnych danych tego użytkownika w tej lokalnej rundzie. W dalszej części samouczka zobaczymy, jak możemy pobrać każdą aktualizację modelu od wszystkich klientów i zagregować je razem w nasz nowy model globalny, który nauczył się na podstawie unikalnych danych każdego klienta.

# Each client has different mean images, meaning each client will be nudging
# the model in their own directions locally.

for i in range(5):
  client_dataset = emnist_train.create_tf_dataset_for_client(
      emnist_train.client_ids[i])
  plot_data = collections.defaultdict(list)
  for example in client_dataset:
    plot_data[example['label'].numpy()].append(example['pixels'].numpy())
  f = plt.figure(i, figsize=(12, 5))
  f.suptitle("Client #{}'s Mean Image Per Label".format(i))
  for j in range(10):
    mean_img = np.mean(plot_data[j], 0)
    plt.subplot(2, 5, j+1)
    plt.imshow(mean_img.reshape((28, 28)))
    plt.axis('off')

png

png

png

png

png

Dane użytkownika mogą być zaszumione i nierzetelne. Na przykład, patrząc na dane Klienta nr 2 powyżej, widzimy, że w przypadku etykiety 2 mogło istnieć kilka niewłaściwie oznaczonych przykładów, które tworzyły bardziej hałaśliwy, wredny obraz.

Wstępne przetwarzanie danych wejściowych

Ponieważ dane są już tf.data.Dataset , przetwarzanie tf.data.Dataset można przeprowadzić za pomocą przekształceń zestawu danych. Tutaj spłaszczamy obrazy 28x28 w tablice 784 -elementowe, 28x28 poszczególne przykłady, organizujemy je w partie i zmieniamy nazwy funkcji z pixels i label na x i y do użytku z Kerasem. repeat również zbiór danych, aby uruchomić kilka epok.

NUM_CLIENTS = 10
NUM_EPOCHS = 5
BATCH_SIZE = 20
SHUFFLE_BUFFER = 100
PREFETCH_BUFFER= 10

def preprocess(dataset):

  def batch_format_fn(element):
    """Flatten a batch `pixels` and return the features as an `OrderedDict`."""
    return collections.OrderedDict(
        x=tf.reshape(element['pixels'], [-1, 784]),
        y=tf.reshape(element['label'], [-1, 1]))

  return dataset.repeat(NUM_EPOCHS).shuffle(SHUFFLE_BUFFER).batch(
      BATCH_SIZE).map(batch_format_fn).prefetch(PREFETCH_BUFFER)

Sprawdźmy, czy to zadziałało.

preprocessed_example_dataset = preprocess(example_dataset)

sample_batch = tf.nest.map_structure(lambda x: x.numpy(),
                                     next(iter(preprocessed_example_dataset)))

sample_batch
OrderedDict([('x', array([[1., 1., 1., ..., 1., 1., 1.],
       [1., 1., 1., ..., 1., 1., 1.],
       [1., 1., 1., ..., 1., 1., 1.],
       ...,
       [1., 1., 1., ..., 1., 1., 1.],
       [1., 1., 1., ..., 1., 1., 1.],
       [1., 1., 1., ..., 1., 1., 1.]], dtype=float32)), ('y', array([[2],
       [1],
       [2],
       [3],
       [6],
       [0],
       [1],
       [4],
       [1],
       [0],
       [6],
       [9],
       [9],
       [3],
       [6],
       [1],
       [4],
       [8],
       [0],
       [2]], dtype=int32))])

Mamy prawie wszystkie elementy konstrukcyjne potrzebne do konstruowania federacyjnych zestawów danych.

Jednym ze sposobów dostarczania federacyjnych danych do TFF w symulacji jest po prostu lista Pythona, gdzie każdy element listy zawiera dane pojedynczego użytkownika, czy to jako lista, czy jako tf.data.Dataset . Ponieważ mamy już interfejs, który zapewnia to drugie, użyjmy go.

Oto prosta funkcja pomocnicza, która utworzy listę zestawów danych z podanego zestawu użytkowników jako dane wejściowe do rundy szkolenia lub oceny.

def make_federated_data(client_data, client_ids):
  return [
      preprocess(client_data.create_tf_dataset_for_client(x))
      for x in client_ids
  ]

Jak teraz wybieramy klientów?

W typowym scenariuszu szkolenia federacyjnego mamy do czynienia z potencjalnie bardzo dużą populacją urządzeń użytkowników, z których tylko ułamek może być dostępny do szkolenia w danym momencie. Dzieje się tak na przykład w przypadku, gdy urządzeniami klienckimi są telefony komórkowe, które uczestniczą w szkoleniu tylko wtedy, gdy są podłączone do źródła zasilania, wyłączone z sieci z pomiarem iw inny sposób bezczynne.

Oczywiście jesteśmy w środowisku symulacyjnym, a wszystkie dane są dostępne lokalnie. Zwykle wtedy podczas przeprowadzania symulacji pobieramy po prostu próbkę losowego podzbioru klientów, którzy mają być zaangażowani w każdą rundę szkolenia, na ogół różni się w każdej rundzie.

To powiedziawszy, jak możesz się dowiedzieć, studiując artykuł na temat algorytmu uśredniania federacyjnego , osiągnięcie zbieżności w systemie z losowo próbkowanymi podzbiorami klientów w każdej rundzie może zająć trochę czasu, a setki rund byłoby niepraktyczne. ten interaktywny samouczek.

Zamiast tego zrobimy próbkę zestawu klientów raz i ponownie wykorzystamy ten sam zestaw w różnych rundach, aby przyspieszyć konwergencję (celowo nadmierne dopasowanie do danych tych kilku użytkowników). Pozostawiamy to czytelnikowi jako ćwiczenie, aby zmodyfikować ten samouczek, aby symulować losowe próbkowanie - jest to dość łatwe (kiedy już to zrobisz, pamiętaj, że uzyskanie zbieżności modelu może trochę potrwać).

sample_clients = emnist_train.client_ids[0:NUM_CLIENTS]

federated_train_data = make_federated_data(emnist_train, sample_clients)

print('Number of client datasets: {l}'.format(l=len(federated_train_data)))
print('First dataset: {d}'.format(d=federated_train_data[0]))
Number of client datasets: 10
First dataset: <DatasetV1Adapter shapes: OrderedDict([(x, (None, 784)), (y, (None, 1))]), types: OrderedDict([(x, tf.float32), (y, tf.int32)])>

Tworzenie modelu za pomocą Keras

Jeśli używasz Keras, prawdopodobnie masz już kod, który tworzy model Keras. Oto przykład prostego modelu, który wystarczy na nasze potrzeby.

def create_keras_model():
  return tf.keras.models.Sequential([
      tf.keras.layers.Input(shape=(784,)),
      tf.keras.layers.Dense(10, kernel_initializer='zeros'),
      tf.keras.layers.Softmax(),
  ])

Aby użyć dowolnego modelu z TFF, należy go tff.learning.Model w tff.learning.Model interfejsu tff.learning.Model , który udostępnia metody stemplowania przebiegu w przód modelu, właściwości metadanych itp., Podobnie jak Keras, ale wprowadza również dodatkowe elementy, takie jak sposoby kontrolowania procesu obliczania metryk federacyjnych. Na razie nie martwmy się o to; jeśli masz model Keras, taki jak ten, który właśnie zdefiniowaliśmy powyżej, możesz owinąć go TFF, wywołując tff.learning.from_keras_model , przekazując model i przykładową partię danych jako argumenty, jak pokazano poniżej.

def model_fn():
  # We _must_ create a new model here, and _not_ capture it from an external
  # scope. TFF will call this within different graph contexts.
  keras_model = create_keras_model()
  return tff.learning.from_keras_model(
      keras_model,
      input_spec=preprocessed_example_dataset.element_spec,
      loss=tf.keras.losses.SparseCategoricalCrossentropy(),
      metrics=[tf.keras.metrics.SparseCategoricalAccuracy()])

Szkolenie modelu na danych federacyjnych

Teraz, gdy mamy model opakowany jako tff.learning.Model do użytku z TFF, możemy pozwolić TFF skonstruować algorytm uśredniania federacyjnego, wywołując funkcję pomocniczą tff.learning.build_federated_averaging_process , jak poniżej.

Pamiętaj, że argument musi być konstruktorem (takim jak model_fn powyżej), a nie już skonstruowaną instancją, aby konstrukcja twojego modelu mogła się odbyć w kontekście kontrolowanym przez TFF (jeśli jesteś ciekawy przyczyn dlatego zachęcamy do przeczytania uzupełniającego samouczka dotyczącego niestandardowych algorytmów ).

Jedna krytyczna uwaga na temat algorytmu uśredniania federacyjnego poniżej: istnieją 2 optymalizatory: optymalizator _client i optymalizator _server. Optymalizator _client jest używany tylko do obliczania aktualizacji modelu lokalnego na każdym kliencie. Optymalizator _server stosuje uśrednioną aktualizację do modelu globalnego na serwerze. W szczególności oznacza to, że wybór optymalizatora i zastosowanego współczynnika uczenia się może różnić się od tych, które były używane do trenowania modelu na standardowym zestawie danych iid. Zalecamy rozpoczęcie od zwykłego SGD, prawdopodobnie z mniejszą szybkością uczenia się niż zwykle. Współczynnik uczenia się, którego używamy, nie został dokładnie dostosowany, zachęcamy do eksperymentowania.

iterative_process = tff.learning.build_federated_averaging_process(
    model_fn,
    client_optimizer_fn=lambda: tf.keras.optimizers.SGD(learning_rate=0.02),
    server_optimizer_fn=lambda: tf.keras.optimizers.SGD(learning_rate=1.0))

Co się stało? TFF skonstruował parę obliczeń federacyjnych i tff.templates.IterativeProcess je w tff.templates.IterativeProcess w którym te obliczenia są dostępne jako para właściwości, które są initialize i next .

W skrócie, obliczenia federacyjne to programy w języku wewnętrznym TFF, które mogą wyrażać różne algorytmy federacyjne (więcej na ten temat można znaleźć w samouczku dotyczącym algorytmów niestandardowych ). W tym przypadku dwa obliczenia wygenerowane i spakowane do iterative_process implementują uśrednianie federacyjne .

Celem TFF jest zdefiniowanie obliczeń w taki sposób, aby mogły być wykonywane w rzeczywistych ustawieniach uczenia federacyjnego, ale obecnie zaimplementowano tylko lokalne środowisko wykonawcze symulacji wykonania. Aby wykonać obliczenia w symulatorze, po prostu wywołaj je jak funkcję Pythona. To domyślne środowisko interpretowane nie jest zaprojektowane z myślą o wysokiej wydajności, ale wystarczy do tego samouczka; spodziewamy się zapewnić wydajniejsze środowiska wykonawcze symulacji, aby ułatwić badania na większą skalę w przyszłych wersjach.

Zacznijmy od initialize obliczeń. Podobnie jak w przypadku wszystkich obliczeń federacyjnych, można o tym myśleć jako o funkcji. Obliczenie nie przyjmuje argumentów i zwraca jeden wynik - reprezentację stanu procesu uśredniania stowarzyszonego na serwerze. Chociaż nie chcemy zagłębiać się w szczegóły TFF, pouczające może być zobaczenie, jak wygląda ten stan. Możesz to sobie wyobrazić w następujący sposób.

str(iterative_process.initialize.type_signature)
'( -> <model=<trainable=<float32[784,10],float32[10]>,non_trainable=<>>,optimizer_state=<int64>,delta_aggregate_state=<>,model_broadcast_state=<>>@SERVER)'

Chociaż powyższa sygnatura typu może na pierwszy rzut oka wydawać się nieco tajemnicza, można rozpoznać, że stan serwera składa się z model (początkowe parametry modelu dla MNIST, które zostaną rozesłane do wszystkich urządzeń) i optimizer_state (dodatkowe informacje utrzymywane przez serwer, takich jak liczba rund do wykorzystania w harmonogramach hiperparametrów itp.).

Wywołajmy obliczenia initialize aby skonstruować stan serwera.

state = iterative_process.initialize()

Drugi z pary federacyjnych obliczeń, next , reprezentuje pojedynczą rundę Federalne uśredniania, która składa się z pchania stanu serwera (w tym parametrów modelu) do klientów, szkolenia na urządzeniu na swoich lokalnych danych, gromadzenie i modelowe uśredniania aktualizacje i tworzenie nowego zaktualizowanego modelu na serwerze.

Koncepcyjnie możesz myśleć o next jako o funkcjonalnej sygnaturze typu, która wygląda następująco.

SERVER_STATE, FEDERATED_DATA -> SERVER_STATE, TRAINING_METRICS

W szczególności należy pomyśleć o next() nie jako o funkcji działającej na serwerze, ale raczej o deklaratywnej reprezentacji funkcjonalnej całego zdecentralizowanego obliczenia - część danych wejściowych jest dostarczana przez serwer ( SERVER_STATE ), ale każdy z nich uczestniczy urządzenie dostarcza swój własny lokalny zbiór danych.

Przeprowadźmy jedną rundę szkolenia i zwizualizujmy wyniki. Możemy użyć danych federacyjnych, które już wygenerowaliśmy powyżej, dla próbki użytkowników.

state, metrics = iterative_process.next(state, federated_train_data)
print('round  1, metrics={}'.format(metrics))
round  1, metrics=<broadcast=<>,aggregation=<>,train=<sparse_categorical_accuracy=0.12037037312984467,loss=3.0108425617218018>>

Przebiegnijmy jeszcze kilka rund. Jak wspomniano wcześniej, zazwyczaj w tym momencie należy wybrać podzbiór danych symulacji z nowej, losowo wybranej próbki użytkowników dla każdej rundy, aby zasymulować realistyczne wdrożenie, w którym użytkownicy stale przychodzą i odchodzą, ale w tym interaktywnym notatniku dla ze względu na demonstrację po prostu ponownie wykorzystamy tych samych użytkowników, aby system szybko się zbiegał.

NUM_ROUNDS = 11
for round_num in range(2, NUM_ROUNDS):
  state, metrics = iterative_process.next(state, federated_train_data)
  print('round {:2d}, metrics={}'.format(round_num, metrics))
round  2, metrics=<broadcast=<>,aggregation=<>,train=<sparse_categorical_accuracy=0.14814814925193787,loss=2.8865506649017334>>
round  3, metrics=<broadcast=<>,aggregation=<>,train=<sparse_categorical_accuracy=0.148765429854393,loss=2.9079062938690186>>
round  4, metrics=<broadcast=<>,aggregation=<>,train=<sparse_categorical_accuracy=0.17633745074272156,loss=2.724686622619629>>
round  5, metrics=<broadcast=<>,aggregation=<>,train=<sparse_categorical_accuracy=0.20226337015628815,loss=2.6334855556488037>>
round  6, metrics=<broadcast=<>,aggregation=<>,train=<sparse_categorical_accuracy=0.22427983582019806,loss=2.5482592582702637>>
round  7, metrics=<broadcast=<>,aggregation=<>,train=<sparse_categorical_accuracy=0.24094650149345398,loss=2.4472343921661377>>
round  8, metrics=<broadcast=<>,aggregation=<>,train=<sparse_categorical_accuracy=0.259876549243927,loss=2.3809611797332764>>
round  9, metrics=<broadcast=<>,aggregation=<>,train=<sparse_categorical_accuracy=0.29814815521240234,loss=2.156442403793335>>
round 10, metrics=<broadcast=<>,aggregation=<>,train=<sparse_categorical_accuracy=0.31687241792678833,loss=2.122845411300659>>

Utrata treningu zmniejsza się po każdej rundzie treningu federacyjnego, co wskazuje na zbieżność modelu. Istnieją pewne ważne zastrzeżenia dotyczące tych wskaźników szkolenia, jednak zobacz sekcję dotyczącą oceny w dalszej części tego samouczka.

Wyświetlanie metryk modelu w TensorBoard

Następnie zwizualizujmy metryki z tych obliczeń federacyjnych przy użyciu Tensorboard.

Zacznijmy od utworzenia katalogu i odpowiedniego modułu zapisującego podsumowanie, w którym zostaną zapisane metryki.


logdir = "/tmp/logs/scalars/training/"
summary_writer = tf.summary.create_file_writer(logdir)
state = iterative_process.initialize()

Wykreśl odpowiednie metryki skalarne z tym samym autorem podsumowania.


with summary_writer.as_default():
  for round_num in range(1, NUM_ROUNDS):
    state, metrics = iterative_process.next(state, federated_train_data)
    for name, value in metrics.train._asdict().items():
      tf.summary.scalar(name, value, step=round_num)

Uruchom TensorBoard z głównym katalogiem dziennika określonym powyżej. Załadowanie danych może zająć kilka sekund.


%tensorboard --logdir /tmp/logs/scalars/ --port=0

# Run this this cell to clean your directory of old output for future graphs from this directory.
!rm -R /tmp/logs/scalars/*

Aby wyświetlić metryki oceny w ten sam sposób, możesz utworzyć oddzielny folder eval, taki jak „logs / scalars / eval”, do zapisu w TensorBoard.

Dostosowywanie implementacji modelu

Keras jest zalecanym interfejsem API wysokiego poziomu dla TensorFlow i zachęcamy do używania modeli Keras (przez tff.learning.from_keras_model ) w TFF, gdy tylko jest to możliwe.

Jednak tff.learning udostępnia interfejs modelu niższego poziomu, tff.learning.Model , który udostępnia minimalną funkcjonalność niezbędną do używania modelu do uczenia federacyjnego. Bezpośrednie wdrożenie tego interfejsu (prawdopodobnie nadal przy użyciu bloków konstrukcyjnych, takich jak tf.keras.layers ) pozwala na maksymalne dostosowanie bez modyfikowania wewnętrznych algorytmów uczenia federacyjnego.

Więc zróbmy to wszystko od nowa.

Definiowanie zmiennych modelu, przebiegu do przodu i metryk

Pierwszym krokiem jest zidentyfikowanie zmiennych TensorFlow, z którymi będziemy pracować. Aby uczynić poniższy kod bardziej czytelnym, zdefiniujmy strukturę danych, która będzie reprezentować cały zestaw. Będzie to obejmować takie zmienne jak weights i bias , że będziemy trenować, a także zmienne, które posiadają różne łączne statystyki i liczniki będziemy aktualizować w trakcie szkolenia, takie jak loss_sum , accuracy_sum i num_examples .

MnistVariables = collections.namedtuple(
    'MnistVariables', 'weights bias num_examples loss_sum accuracy_sum')

Oto metoda, która tworzy zmienne. Ze względu na prostotę wszystkie statystyki reprezentujemy jako tf.float32 , ponieważ wyeliminuje to potrzebę konwersji typów na późniejszym etapie. Zawijanie inicjatorów zmiennych jako lambd jest wymaganiem narzuconym przez zmienne zasobów .

def create_mnist_variables():
  return MnistVariables(
      weights=tf.Variable(
          lambda: tf.zeros(dtype=tf.float32, shape=(784, 10)),
          name='weights',
          trainable=True),
      bias=tf.Variable(
          lambda: tf.zeros(dtype=tf.float32, shape=(10)),
          name='bias',
          trainable=True),
      num_examples=tf.Variable(0.0, name='num_examples', trainable=False),
      loss_sum=tf.Variable(0.0, name='loss_sum', trainable=False),
      accuracy_sum=tf.Variable(0.0, name='accuracy_sum', trainable=False))

Mając na miejscu zmienne parametrów modelu i statystyki skumulowane, możemy teraz zdefiniować metodę przejścia do przodu, która oblicza straty, emituje prognozy i aktualizuje skumulowane statystyki dla pojedynczej partii danych wejściowych w następujący sposób.

def mnist_forward_pass(variables, batch):
  y = tf.nn.softmax(tf.matmul(batch['x'], variables.weights) + variables.bias)
  predictions = tf.cast(tf.argmax(y, 1), tf.int32)

  flat_labels = tf.reshape(batch['y'], [-1])
  loss = -tf.reduce_mean(
      tf.reduce_sum(tf.one_hot(flat_labels, 10) * tf.math.log(y), axis=[1]))
  accuracy = tf.reduce_mean(
      tf.cast(tf.equal(predictions, flat_labels), tf.float32))

  num_examples = tf.cast(tf.size(batch['y']), tf.float32)

  variables.num_examples.assign_add(num_examples)
  variables.loss_sum.assign_add(loss * num_examples)
  variables.accuracy_sum.assign_add(accuracy * num_examples)

  return loss, predictions

Następnie definiujemy funkcję, która zwraca zestaw lokalnych metryk, ponownie używając TensorFlow. Są to wartości (oprócz aktualizacji modelu, które są obsługiwane automatycznie), które można zagregować na serwerze w ramach stowarzyszonego procesu uczenia się lub oceny.

Tutaj po prostu zwracamy średnią loss i accuracy , a także num_examples , których będziemy potrzebować, aby poprawnie num_examples wkłady różnych użytkowników podczas obliczania agregacji federacyjnych.

def get_local_mnist_metrics(variables):
  return collections.OrderedDict(
      num_examples=variables.num_examples,
      loss=variables.loss_sum / variables.num_examples,
      accuracy=variables.accuracy_sum / variables.num_examples)

Na koniec musimy ustalić, w jaki sposób agregować lokalne metryki emitowane przez każde urządzenie za pośrednictwem get_local_mnist_metrics . To jedyna część kodu, która nie jest napisana w TensorFlow - jest to obliczenie federacyjne wyrażone w TFF. Jeśli chcesz zagłębić się w szczegóły, przejrzyj samouczek niestandardowych algorytmów , ale w większości aplikacji tak naprawdę nie musisz; warianty wzoru pokazanego poniżej powinny wystarczyć. Oto jak to wygląda:

@tff.federated_computation
def aggregate_mnist_metrics_across_clients(metrics):
  return collections.OrderedDict(
      num_examples=tff.federated_sum(metrics.num_examples),
      loss=tff.federated_mean(metrics.loss, metrics.num_examples),
      accuracy=tff.federated_mean(metrics.accuracy, metrics.num_examples))
  

Argument metrics wejściowe odpowiada parametrowi OrderedDict zwróconemu przez get_local_mnist_metrics powyżej, ale krytycznie rzecz biorąc, wartości nie są już tf.Tensors - są one „opakowane” jako tff.Value s, aby było jasne, że nie można już nimi manipulować za pomocą TensorFlow, a jedynie używając operatorów federacyjnych TFF, takich jak tff.federated_mean i tff.federated_sum . Zwrócony słownik globalnych agregatów definiuje zestaw metryk, które będą dostępne na serwerze.

Konstruowanie instancji tff.learning.Model

Mając wszystko powyższe na miejscu, jesteśmy gotowi do skonstruowania reprezentacji modelu do użytku z TFF podobnej do tej, która jest generowana dla Ciebie, gdy pozwolisz TFF przyjąć model Keras.

class MnistModel(tff.learning.Model):

  def __init__(self):
    self._variables = create_mnist_variables()

  @property
  def trainable_variables(self):
    return [self._variables.weights, self._variables.bias]

  @property
  def non_trainable_variables(self):
    return []

  @property
  def local_variables(self):
    return [
        self._variables.num_examples, self._variables.loss_sum,
        self._variables.accuracy_sum
    ]

  @property
  def input_spec(self):
    return collections.OrderedDict(
        x=tf.TensorSpec([None, 784], tf.float32),
        y=tf.TensorSpec([None, 1], tf.int32))

  @tf.function
  def forward_pass(self, batch, training=True):
    del training
    loss, predictions = mnist_forward_pass(self._variables, batch)
    num_exmaples = tf.shape(batch['x'])[0]
    return tff.learning.BatchOutput(
        loss=loss, predictions=predictions, num_examples=num_exmaples)

  @tf.function
  def report_local_outputs(self):
    return get_local_mnist_metrics(self._variables)

  @property
  def federated_output_computation(self):
    return aggregate_mnist_metrics_across_clients

Jak widać, abstrakcyjne metody i właściwości zdefiniowane przez tff.learning.Model odpowiadają fragmentom kodu w poprzedniej sekcji, które wprowadzały zmienne i definiowały straty oraz statystyki.

Oto kilka punktów, na które warto zwrócić uwagę:

  • Wszystkie stany, których będzie używał twój model, muszą zostać przechwycone jako zmienne TensorFlow, ponieważ TFF nie używa Pythona w czasie wykonywania (pamiętaj, że twój kod powinien być napisany w taki sposób, aby można go było wdrożyć na urządzeniach mobilnych; zobacz samouczek niestandardowych algorytmów, aby uzyskać bardziej szczegółowe informacje komentarz do przyczyn).
  • Twój model powinien opisywać, jaką formę danych akceptuje ( input_spec ), ponieważ ogólnie rzecz biorąc, TFF jest silnym typem środowiska i chce określić sygnatury typów dla wszystkich składników. Deklarowanie formatu danych wejściowych modelu jest istotną częścią tego.
  • Chociaż z technicznego punktu widzenia nie jest to wymagane, zalecamy zawijanie całej logiki TensorFlow (przebieg do przodu, obliczenia metryczne itp.) Jako tf.function , Ponieważ pomaga to zapewnić serializację TensorFlow i eliminuje potrzebę jawnych zależności sterowania.

Powyższe jest wystarczające do oceny i algorytmów, takich jak Federated SGD. Jednak w przypadku uśredniania federacyjnego musimy określić, w jaki sposób model powinien trenować lokalnie dla każdej partii. Podczas tworzenia algorytmu uśredniania federacyjnego określimy optymalizator lokalny.

Symulowanie treningu federacyjnego w nowym modelu

Mając wszystko powyższe na swoim miejscu, pozostała część procesu wygląda tak, jak już widzieliśmy - po prostu zastąp konstruktor modelu konstruktorem naszej nowej klasy modelu i użyj dwóch federacyjnych obliczeń w utworzonym iteracyjnym procesie, aby przejść przez cykl rundy szkoleniowe.

iterative_process = tff.learning.build_federated_averaging_process(
    MnistModel,
    client_optimizer_fn=lambda: tf.keras.optimizers.SGD(learning_rate=0.02))
state = iterative_process.initialize()
state, metrics = iterative_process.next(state, federated_train_data)
print('round  1, metrics={}'.format(metrics))
round  1, metrics=<broadcast=<>,aggregation=<>,train=<num_examples=4860.0,loss=2.9713594913482666,accuracy=0.13518518209457397>>

for round_num in range(2, 11):
  state, metrics = iterative_process.next(state, federated_train_data)
  print('round {:2d}, metrics={}'.format(round_num, metrics))
round  2, metrics=<broadcast=<>,aggregation=<>,train=<num_examples=4860.0,loss=2.975412607192993,accuracy=0.14032921195030212>>
round  3, metrics=<broadcast=<>,aggregation=<>,train=<num_examples=4860.0,loss=2.9395227432250977,accuracy=0.1594650149345398>>
round  4, metrics=<broadcast=<>,aggregation=<>,train=<num_examples=4860.0,loss=2.710164785385132,accuracy=0.17139917612075806>>
round  5, metrics=<broadcast=<>,aggregation=<>,train=<num_examples=4860.0,loss=2.5891618728637695,accuracy=0.20267489552497864>>
round  6, metrics=<broadcast=<>,aggregation=<>,train=<num_examples=4860.0,loss=2.5148487091064453,accuracy=0.21666666865348816>>
round  7, metrics=<broadcast=<>,aggregation=<>,train=<num_examples=4860.0,loss=2.2816808223724365,accuracy=0.2580246925354004>>
round  8, metrics=<broadcast=<>,aggregation=<>,train=<num_examples=4860.0,loss=2.3656885623931885,accuracy=0.25884774327278137>>
round  9, metrics=<broadcast=<>,aggregation=<>,train=<num_examples=4860.0,loss=2.23549222946167,accuracy=0.28477364778518677>>
round 10, metrics=<broadcast=<>,aggregation=<>,train=<num_examples=4860.0,loss=1.974222183227539,accuracy=0.35329216718673706>>

Aby zobaczyć te metryki w TensorBoard, wykonaj czynności wymienione powyżej w części „Wyświetlanie metryk modelu w TensorBoard”.

Ocena

Wszystkie nasze eksperymenty do tej pory przedstawiały tylko federacyjne wskaźniki treningowe - średnie metryki ze wszystkich partii danych trenowanych przez wszystkich klientów w rundzie. Wprowadza to normalne obawy dotyczące nadmiernego dopasowania, zwłaszcza, że ​​dla uproszczenia używaliśmy tego samego zestawu klientów w każdej rundzie, ale istnieje dodatkowe pojęcie nadmiernego dopasowania w metrykach treningowych specyficznych dla algorytmu uśredniania federacyjnego. Najłatwiej to zobaczyć, jeśli wyobrazimy sobie, że każdy klient miał jedną partię danych i na tej partii trenujemy przez wiele iteracji (epok). W takim przypadku model lokalny szybko dopasuje się dokładnie do tej jednej partii, więc lokalna miara dokładności, którą uśredniamy, zbliży się do 1,0. W związku z tym te wskaźniki treningu mogą być traktowane jako znak postępu w treningu, ale niewiele więcej.

Aby przeprowadzić ocenę danych stowarzyszonych, można skonstruować inne obliczenie stowarzyszone przeznaczone tylko do tego celu, używając funkcji tff.learning.build_federated_evaluation i przekazując konstruktor modelu jako argument. Zauważ, że w przeciwieństwie do Federated Averaging, gdzie używaliśmy MnistTrainableModel , wystarczy przekazać MnistModel . Ocena nie wykonuje gradientu i nie ma potrzeby konstruowania optymalizatorów.

W przypadku eksperymentów i badań, gdy dostępny jest scentralizowany zestaw danych testowych, Federated Learning for Text Generation demonstruje inną opcję oceny: pobranie wyuczonych wag z uczenia federacyjnego, zastosowanie ich do standardowego modelu Keras, a następnie wywołanie tf.keras.models.Model.evaluate() w scentralizowanym zbiorze danych.

evaluation = tff.learning.build_federated_evaluation(MnistModel)

Możesz sprawdzić podpis typu abstrakcyjnego funkcji oceny w następujący sposób.

str(evaluation.type_signature)
'(<<trainable=<float32[784,10],float32[10]>,non_trainable=<>>@SERVER,{<x=float32[?,784],y=int32[?,1]>*}@CLIENTS> -> <num_examples=float32@SERVER,loss=float32@SERVER,accuracy=float32@SERVER>)'

Na tym etapie nie musisz martwić się o szczegóły, pamiętaj tylko, że przyjmuje on następującą ogólną formę, podobną do tff.templates.IterativeProcess.next ale z dwiema istotnymi różnicami. Po pierwsze, nie zwracamy stanu serwera, ponieważ ocena nie modyfikuje modelu ani żadnego innego aspektu stanu - można go traktować jako bezstanowy. Po drugie, ocena wymaga tylko modelu i nie wymaga żadnej innej części stanu serwera, która może być powiązana z uczeniem, takich jak zmienne optymalizatora.

SERVER_MODEL, FEDERATED_DATA -> TRAINING_METRICS

Przywołajmy ocenę ostatniego stanu, do którego doszliśmy podczas treningu. Aby wyodrębnić najnowszy wytrenowany model ze stanu serwera, wystarczy uzyskać dostęp do elementu członkowskiego .model w następujący sposób.

train_metrics = evaluation(state.model, federated_train_data)

Oto, co otrzymujemy. Zauważ, że liczby wyglądają nieznacznie lepiej niż te, które zostały zgłoszone w ostatniej rundzie szkolenia powyżej. Zgodnie z konwencją, metryki szkolenia zgłaszane przez iteracyjny proces uczenia ogólnie odzwierciedlają wydajność modelu na początku rundy szkoleniowej, więc metryki oceny zawsze będą o jeden krok do przodu.

str(train_metrics)
'<num_examples=4860.0,loss=1.7142657041549683,accuracy=0.38683128356933594>'

Teraz skompilujmy próbkę testową danych stowarzyszonych i ponownie uruchommy ocenę na danych testowych. Dane będą pochodzić z tej samej próby rzeczywistych użytkowników, ale z odrębnego przetrzymywanego zestawu danych.

federated_test_data = make_federated_data(emnist_test, sample_clients)

len(federated_test_data), federated_test_data[0]
(10,
 <DatasetV1Adapter shapes: OrderedDict([(x, (None, 784)), (y, (None, 1))]), types: OrderedDict([(x, tf.float32), (y, tf.int32)])>)
test_metrics = evaluation(state.model, federated_test_data)
str(test_metrics)
'<num_examples=580.0,loss=1.861915111541748,accuracy=0.3362068831920624>'

To kończy samouczek. Zachęcamy do zabawy parametrami (np. Rozmiarami partii, liczbą użytkowników, epokami, współczynnikiem uczenia się itp.), Zmodyfikowaniem powyższego kodu w celu symulacji treningu na losowych próbkach użytkowników w każdej rundzie oraz do zbadania innych samouczków opracowaliśmy.