Zapisz datę! Google I / O powraca w dniach 18-20 maja Zarejestruj się teraz
Ta strona została przetłumaczona przez Cloud Translation API.
Switch to English

Niestandardowe algorytmy federacyjne, część 2: wdrażanie uśredniania federacyjnego

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

Ten samouczek jest drugą częścią dwuczęściowej serii, która pokazuje, jak zaimplementować niestandardowe typy algorytmów federacyjnych w TFF przy użyciu Federated Core (FC) , który służy jako podstawa dla warstwy Federated Learning (FL) ( tff.learning ) .

Zachęcamy do zapoznania się najpierw z pierwszą częścią tej serii , w której przedstawiono niektóre kluczowe pojęcia i abstrakcje programistyczne użyte w tym artykule.

Ta druga część serii wykorzystuje mechanizmy wprowadzone w pierwszej części do implementacji prostej wersji federacyjnych algorytmów szkolenia i oceny.

Zachęcamy do zapoznania się z samouczkami dotyczącymi klasyfikacji obrazów i generowania tekstu, aby uzyskać wyższy poziom i delikatniejsze wprowadzenie do interfejsów API Federated Learning w TFF, ponieważ pomogą one umieścić opisane tutaj koncepcje w kontekście.

Zanim zaczniemy

Zanim zaczniemy, spróbuj uruchomić następujący przykład „Hello World”, aby upewnić się, że środowisko jest poprawnie skonfigurowane. Jeśli to nie zadziała, zapoznaj się z instrukcją instalacji, aby uzyskać instrukcje.

!pip install --quiet --upgrade tensorflow-federated-nightly
!pip install --quiet --upgrade nest-asyncio

import nest_asyncio
nest_asyncio.apply()
import collections

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

# TODO(b/148678573,b/148685415): must use the reference context because it
# supports unbounded references and tff.sequence_* intrinsics.
tff.backends.reference.set_reference_context()
@tff.federated_computation
def hello_world():
  return 'Hello, World!'

hello_world()
'Hello, World!'

Wdrażanie uśredniania federacyjnego

Podobnie jak w Federated Learning for Image Classification , będziemy używać przykładu MNIST, ale ponieważ jest to samouczek niskiego poziomu, zamierzamy ominąć Keras API i tff.simulation , napisać surowy kod modelu i skonstruować stowarzyszony zbiór danych od podstaw.

Przygotowywanie stowarzyszonych zestawów danych

Na potrzeby demonstracji zasymulujemy scenariusz, w którym mamy dane od 10 użytkowników, a każdy z użytkowników wnosi wiedzę, jak rozpoznać inną cyfrę. To jest tak niewiarygodne, jak to tylko możliwe.

Najpierw załadujmy standardowe dane MNIST:

mnist_train, mnist_test = tf.keras.datasets.mnist.load_data()
Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/mnist.npz
11493376/11490434 [==============================] - 0s 0us/step
11501568/11490434 [==============================] - 0s 0us/step
[(x.dtype, x.shape) for x in mnist_train]
[(dtype('uint8'), (60000, 28, 28)), (dtype('uint8'), (60000,))]

Dane są przedstawiane jako tablice Numpy, jedna z obrazami, a druga z etykietami cyfrowymi, przy czym pierwszy wymiar obejmuje poszczególne przykłady. Napiszmy funkcję pomocniczą, która formatuje ją w sposób zgodny ze sposobem, w jaki dostarczamy sekwencje federacyjne do obliczeń TFF, tj. Jako listę list - zewnętrzna lista obejmuje użytkowników (cyfry), wewnętrzna obejmuje partie danych w kolejność każdego klienta. Jak to zwykle będzie to struktura każdej partii w postaci pary tensorów nazwanych x i y , każdy z czołowych wymiaru wsadowego. W tym celu spłaszczymy również każdy obraz do postaci wektora o 784 elementach i przeskalujemy jego piksele do zakresu 0..1 , aby nie musieć zaśmiecać logiki modelu konwersjami danych.

NUM_EXAMPLES_PER_USER = 1000
BATCH_SIZE = 100


def get_data_for_digit(source, digit):
  output_sequence = []
  all_samples = [i for i, d in enumerate(source[1]) if d == digit]
  for i in range(0, min(len(all_samples), NUM_EXAMPLES_PER_USER), BATCH_SIZE):
    batch_samples = all_samples[i:i + BATCH_SIZE]
    output_sequence.append({
        'x':
            np.array([source[0][i].flatten() / 255.0 for i in batch_samples],
                     dtype=np.float32),
        'y':
            np.array([source[1][i] for i in batch_samples], dtype=np.int32)
    })
  return output_sequence


federated_train_data = [get_data_for_digit(mnist_train, d) for d in range(10)]

federated_test_data = [get_data_for_digit(mnist_test, d) for d in range(10)]

Aby szybko sprawdzić poprawność, przyjrzyjmy się tensorowi Y w ostatniej partii danych dostarczonych przez piątego klienta (ten odpowiadający cyfrze 5 ).

federated_train_data[5][-1]['y']
array([5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
       5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
       5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
       5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
       5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5], dtype=int32)

Dla pewności przyjrzyjmy się również obrazowi odpowiadającemu ostatniemu elementowi tej partii.

from matplotlib import pyplot as plt

plt.imshow(federated_train_data[5][-1]['x'][-1].reshape(28, 28), cmap='gray')
plt.grid(False)
plt.show()

png

O połączeniu TensorFlow i TFF

W tym samouczku, aby uzyskać zwartość, natychmiast dekorujemy funkcje wprowadzające logikę tff.tf_computation pomocą tff.tf_computation . Jednak w przypadku bardziej złożonej logiki nie jest to zalecany wzorzec. Debugowanie TensorFlow może już być wyzwaniem, a debugowanie TensorFlow po jego pełnej serializacji, a następnie ponownym zaimportowaniu z konieczności powoduje utratę niektórych metadanych i ogranicza interaktywność, co sprawia, że ​​debugowanie jest jeszcze większym wyzwaniem.

Dlatego zdecydowanie zalecamy pisanie złożonej logiki TF jako samodzielnych funkcji Pythona (to znaczy bez dekoracji tff.tf_computation ). W ten sposób logika TensorFlow może być rozwijana i testowana przy użyciu najlepszych praktyk i narzędzi TF (takich jak tryb przyspieszony), przed serializacją obliczeń dla TFF (np. Przez wywołanie tff.tf_computation z funkcją Pythona jako argumentem).

Definiowanie funkcji straty

Teraz, gdy mamy już dane, zdefiniujmy funkcję straty, której możemy użyć do treningu. Najpierw zdefiniujmy typ danych wejściowych jako TFF o nazwie krotka. Ponieważ rozmiar partii danych może się różnić, ustawiliśmy rozmiar partii na None aby wskazać, że rozmiar tego wymiaru jest nieznany.

BATCH_SPEC = collections.OrderedDict(
    x=tf.TensorSpec(shape=[None, 784], dtype=tf.float32),
    y=tf.TensorSpec(shape=[None], dtype=tf.int32))
BATCH_TYPE = tff.to_type(BATCH_SPEC)

str(BATCH_TYPE)
'<x=float32[?,784],y=int32[?]>'

Możesz się zastanawiać, dlaczego nie możemy po prostu zdefiniować zwykłego typu Pythona. Przypomnij sobie dyskusję w części 1 , w której wyjaśniliśmy, że chociaż możemy wyrazić logikę obliczeń TFF za pomocą Pythona, pod maską obliczenia TFF nie są Pythonem. Symbol BATCH_TYPE zdefiniowany powyżej reprezentuje abstrakcyjną specyfikację typu TFF. Ważne jest, aby odróżnić ten abstrakcyjny typ TFF od konkretnych typów reprezentacji Pythona, np. Kontenerów, takich jak dict lub collections.namedtuple które mogą być używane do reprezentowania typu TFF w treści funkcji Pythona. W przeciwieństwie do Pythona, TFF ma jeden konstruktor typu abstrakcyjnego tff.StructType dla kontenerów podobnych do krotek, z elementami, które mogą być indywidualnie nazwane lub pozostawione bez nazwy. Ten typ jest również używany do modelowania formalnych parametrów obliczeń, ponieważ obliczenia TFF mogą formalnie deklarować tylko jeden parametr i jeden wynik - wkrótce zobaczysz przykłady.

Zdefiniujmy teraz typ parametrów modelu TFF, ponownie jako TFF nazwaną krotką wag i odchylenia .

MODEL_SPEC = collections.OrderedDict(
    weights=tf.TensorSpec(shape=[784, 10], dtype=tf.float32),
    bias=tf.TensorSpec(shape=[10], dtype=tf.float32))
MODEL_TYPE = tff.to_type(MODEL_SPEC)

print(MODEL_TYPE)
<weights=float32[784,10],bias=float32[10]>

Mając te definicje, możemy teraz zdefiniować stratę dla danego modelu w pojedynczej partii. Zwróć uwagę na użycie dekoratora @tf.function wewnątrz dekoratora @tff.tf_computation . To pozwala nam pisać TF przy użyciu semantyki podobnej do Pythona, nawet jeśli były one wewnątrz kontekstu tf.Graph utworzonego przez dekorator tff.tf_computation .

# NOTE: `forward_pass` is defined separately from `batch_loss` so that it can 
# be later called from within another tf.function. Necessary because a
# @tf.function  decorated method cannot invoke a @tff.tf_computation.

@tf.function
def forward_pass(model, batch):
  predicted_y = tf.nn.softmax(
      tf.matmul(batch['x'], model['weights']) + model['bias'])
  return -tf.reduce_mean(
      tf.reduce_sum(
          tf.one_hot(batch['y'], 10) * tf.math.log(predicted_y), axis=[1]))

@tff.tf_computation(MODEL_TYPE, BATCH_TYPE)
def batch_loss(model, batch):
  return forward_pass(model, batch)

Zgodnie z oczekiwaniami, obliczenie batch_loss zwraca stratę typu float32 biorąc pod uwagę model i pojedynczą partię danych. Zwróć uwagę, jak MODEL_TYPE i BATCH_TYPE zostały zebrane razem w dwie krotki formalnych parametrów; możesz rozpoznać typ batch_loss jako (<MODEL_TYPE,BATCH_TYPE> -> float32) .

str(batch_loss.type_signature)
'(<<weights=float32[784,10],bias=float32[10]>,<x=float32[?,784],y=int32[?]>> -> float32)'

Aby sprawdzić poczytność, skonstruujmy początkowy model wypełniony zerami i obliczmy stratę na partii danych, które wizualizowaliśmy powyżej.

initial_model = collections.OrderedDict(
    weights=np.zeros([784, 10], dtype=np.float32),
    bias=np.zeros([10], dtype=np.float32))

sample_batch = federated_train_data[5][-1]

batch_loss(initial_model, sample_batch)
2.3025854

Zauważ, że zasilamy obliczenia TFF początkowym modelem zdefiniowanym jako dict , mimo że ciało funkcji Pythona, która ją definiuje, zużywa parametry model['weight'] jako model['weight'] i model['bias'] . Argumenty wywołania batch_loss nie są po prostu przekazywane do treści tej funkcji.

Co się stanie, gdy batch_loss ? Treść Pythona batch_loss została już prześledzona i zserializowana w powyższej komórce, w której została zdefiniowana. TFF działa jako obiekt wywołujący batch_loss w czasie definicji obliczeń i jako cel wywołania w momencie wywołania batch_loss . W obu rolach TFF służy jako pomost między abstrakcyjnym systemem typów TFF a typami reprezentacji Pythona. W czasie wywołania TFF zaakceptuje większość standardowych typów kontenerów Pythona ( dict , list , tuple , collections.namedtuple itp.) Jako konkretne reprezentacje abstrakcyjnych krotek TFF. Ponadto, chociaż jak wspomniano powyżej, obliczenia TFF formalnie akceptują tylko jeden parametr, możesz użyć znanej składni wywołania Pythona z argumentami pozycyjnymi i / lub słowami kluczowymi w przypadku, gdy typ parametru to krotka - działa zgodnie z oczekiwaniami.

Zejście gradientowe w pojedynczej partii

Teraz zdefiniujmy obliczenia, które wykorzystują tę funkcję utraty do wykonania pojedynczego kroku gradientu. Zwróć uwagę, jak definiując tę ​​funkcję, używamy batch_loss jako batch_loss . Możesz wywołać obliczenia skonstruowane za pomocą tff.tf_computation w treści innego obliczenia, chociaż zazwyczaj nie jest to konieczne - jak wspomniano powyżej, ponieważ serializacja powoduje utratę niektórych informacji debugowania, często lepiej jest w przypadku bardziej złożonych obliczeń napisać i przetestować wszystkie TensorFlow bez dekoratora tff.tf_computation .

@tff.tf_computation(MODEL_TYPE, BATCH_TYPE, tf.float32)
def batch_train(initial_model, batch, learning_rate):
  # Define a group of model variables and set them to `initial_model`. Must
  # be defined outside the @tf.function.
  model_vars = collections.OrderedDict([
      (name, tf.Variable(name=name, initial_value=value))
      for name, value in initial_model.items()
  ])
  optimizer = tf.keras.optimizers.SGD(learning_rate)

  @tf.function
  def _train_on_batch(model_vars, batch):
    # Perform one step of gradient descent using loss from `batch_loss`.
    with tf.GradientTape() as tape:
      loss = forward_pass(model_vars, batch)
    grads = tape.gradient(loss, model_vars)
    optimizer.apply_gradients(
        zip(tf.nest.flatten(grads), tf.nest.flatten(model_vars)))
    return model_vars

  return _train_on_batch(model_vars, batch)
str(batch_train.type_signature)
'(<<weights=float32[784,10],bias=float32[10]>,<x=float32[?,784],y=int32[?]>,float32> -> <weights=float32[784,10],bias=float32[10]>)'

Kiedy wywołujesz funkcję Pythona ozdobioną tff.tf_computation w treści innej takiej funkcji, logika wewnętrznego obliczenia TFF jest osadzona (zasadniczo, inline) w logice zewnętrznej. Jak wspomniano powyżej, jeśli piszesz oba obliczenia, prawdopodobnie lepiej będzie, aby funkcja wewnętrzna (w tym przypadku batch_loss ) była zwykłym Pythonem lub tf.function niż tff.tf_computation . Jednak tutaj ilustrujemy, że wywołanie jednego tff.tf_computation wewnątrz drugiego działa w zasadzie zgodnie z oczekiwaniami. Może to być konieczne, jeśli na przykład nie masz kodu Pythona definiującego batch_loss , a jedynie jego serializowaną reprezentację TFF.

Teraz zastosujmy tę funkcję kilka razy do początkowego modelu, aby zobaczyć, czy strata maleje.

model = initial_model
losses = []
for _ in range(5):
  model = batch_train(model, sample_batch, 0.1)
  losses.append(batch_loss(model, sample_batch))
losses
[0.19690022, 0.13176313, 0.10113226, 0.082738124, 0.0703014]

Zejście gradientowe na sekwencji danych lokalnych

Teraz, ponieważ wydaje się, że batch_train działa, batch_train podobną funkcję szkoleniową local_train która zużywa całą sekwencję wszystkich partii od jednego użytkownika zamiast tylko jednej partii. Nowe obliczenia będą musiały teraz zużywać tff.SequenceType(BATCH_TYPE) zamiast BATCH_TYPE .

LOCAL_DATA_TYPE = tff.SequenceType(BATCH_TYPE)

@tff.federated_computation(MODEL_TYPE, tf.float32, LOCAL_DATA_TYPE)
def local_train(initial_model, learning_rate, all_batches):

  # Mapping function to apply to each batch.
  @tff.federated_computation(MODEL_TYPE, BATCH_TYPE)
  def batch_fn(model, batch):
    return batch_train(model, batch, learning_rate)

  return tff.sequence_reduce(all_batches, initial_model, batch_fn)
str(local_train.type_signature)
'(<<weights=float32[784,10],bias=float32[10]>,float32,<x=float32[?,784],y=int32[?]>*> -> <weights=float32[784,10],bias=float32[10]>)'

W tej krótkiej sekcji kodu znajduje się sporo szczegółów, przejrzyjmy je jeden po drugim.

Po pierwsze, chociaż mogliśmy zaimplementować tę logikę całkowicie w TensorFlow, polegając na tf.data.Dataset.reduce celu przetworzenia sekwencji w podobny sposób, jak zrobiliśmy to wcześniej, tym razem zdecydowaliśmy się wyrazić logikę w języku kleju , jako tff.federated_computation . Do wykonania redukcji tff.sequence_reduce operatora federacyjnego tff.sequence_reduce .

Operator tff.sequence_reduce jest używany podobnie do tf.data.Dataset.reduce . Można go traktować tak, jak w zasadzie tf.data.Dataset.reduce , ale do użytku w obliczeniach federacyjnych, które, jak być może pamiętasz, nie mogą zawierać kodu TensorFlow. Jest to operator szablonu z formalnym parametrem 3-krotką, który składa się z sekwencji elementów typu T , początkowego stanu redukcji (będziemy odnosić się do niego abstrakcyjnie jako zero ) pewnego typu U oraz operatora redukcji type (<U,T> -> U) który zmienia stan redukcji poprzez przetworzenie pojedynczego elementu. Rezultatem jest końcowy stan redukcji, po przetworzeniu wszystkich elementów w kolejności sekwencyjnej. W naszym przykładzie stan redukcji to model wytrenowany na prefiksie danych, a elementy to paczki danych.

Po drugie, zwróć uwagę, że ponownie użyliśmy jednego obliczenia ( batch_train ) jako komponentu w innym ( local_train ), ale nie bezpośrednio. Nie możemy go używać jako operatora redukcji, ponieważ wymaga dodatkowego parametru - szybkości uczenia się. Aby rozwiązać ten problem, definiujemy osadzone obliczenie stowarzyszone batch_fn które wiąże się z parametrem learning_rate parametru local_train w jego treści. Dozwolone jest, aby obliczenia potomne zdefiniowane w ten sposób przechwytywały formalny parametr swojego rodzica, o ile obliczenia potomne nie są wywoływane poza treścią jego rodzica. Możesz myśleć o tym wzorcu jako o odpowiedniku functools.partial w Pythonie.

Praktyczna konsekwencja przechwytywania współczynnika learning_rate ten sposób polega oczywiście na tym, że we wszystkich partiach używana jest ta sama wartość współczynnika uczenia się.

Teraz wypróbujmy nowo zdefiniowaną lokalną funkcję szkoleniową na całej sekwencji danych od tego samego użytkownika, który dostarczył partię próbki (cyfra 5 ).

locally_trained_model = local_train(initial_model, 0.1, federated_train_data[5])

Zadziałało? Aby odpowiedzieć na to pytanie, musimy wdrożyć ewaluację.

Ocena lokalna

Oto jeden ze sposobów wdrożenia oceny lokalnej poprzez zsumowanie strat we wszystkich pakietach danych (równie dobrze mogliśmy obliczyć średnią; pozostawimy to jako ćwiczenie dla czytelnika).

@tff.federated_computation(MODEL_TYPE, LOCAL_DATA_TYPE)
def local_eval(model, all_batches):
  # TODO(b/120157713): Replace with `tff.sequence_average()` once implemented.
  return tff.sequence_sum(
      tff.sequence_map(
          tff.federated_computation(lambda b: batch_loss(model, b), BATCH_TYPE),
          all_batches))
str(local_eval.type_signature)
'(<<weights=float32[784,10],bias=float32[10]>,<x=float32[?,784],y=int32[?]>*> -> float32)'

Ponownie, jest kilka nowych elementów zilustrowanych przez ten kod, przejdźmy nad nimi jeden po drugim.

Po pierwsze, użyliśmy dwóch nowych operatorów stowarzyszonych do przetwarzania sekwencji: tff.sequence_map który pobiera funkcję odwzorowującą T->U i sekwencję T i emituje sekwencję U uzyskaną przez zastosowanie funkcji mapującej punktowo oraz tff.sequence_sum która po prostu dodaje wszystkie elementy. Tutaj mapujemy każdą partię danych na wartość straty, a następnie dodajemy wynikowe wartości strat, aby obliczyć całkowitą stratę.

Zauważ, że mogliśmy ponownie użyć tff.sequence_reduce , ale nie byłby to najlepszy wybór - proces redukcji jest z definicji sekwencyjny, podczas gdy mapowanie i suma mogą być obliczane równolegle. Mając wybór, najlepiej trzymać się operatorów, którzy nie ograniczają wyborów dotyczących implementacji, tak aby gdy nasze obliczenia TFF były kompilowane w przyszłości w celu wdrożenia w określonym środowisku, można w pełni wykorzystać wszystkie potencjalne możliwości szybszego , bardziej skalowalne, bardziej wydajne pod względem zasobów wykonanie.

Po drugie, zwróć uwagę, że tak jak w local_train , potrzebna nam funkcja komponentu ( batch_loss ) przyjmuje więcej parametrów niż oczekuje operator federacyjny ( tff.sequence_map ), więc ponownie definiujemy częściową, tym razem wbudowaną, bezpośrednio zawijając lambda jako tff.federated_computation . Używanie opakowań w tekście z funkcją jako argumentem jest zalecanym sposobem użycia tff.tf_computation do osadzenia logiki TensorFlow w TFF.

Zobaczmy teraz, czy nasze szkolenie zadziałało.

print('initial_model loss =', local_eval(initial_model,
                                         federated_train_data[5]))
print('locally_trained_model loss =',
      local_eval(locally_trained_model, federated_train_data[5]))
initial_model loss = 23.025854
locally_trained_model loss = 0.4348469

Rzeczywiście, strata się zmniejszyła. Ale co się stanie, jeśli ocenimy to na danych innego użytkownika?

print('initial_model loss =', local_eval(initial_model,
                                         federated_train_data[0]))
print('locally_trained_model loss =',
      local_eval(locally_trained_model, federated_train_data[0]))
initial_model loss = 23.025854
locally_trained_model loss = 74.50075

Zgodnie z oczekiwaniami sytuacja się pogorszyła. Model został przeszkolony, aby rozpoznawać 5 i nigdy nie widział 0 . Pojawia się pytanie - jak lokalne szkolenie wpłynęło na jakość modelu z perspektywy globalnej?

Ocena federacyjna

To jest punkt naszej podróży, w którym w końcu powracamy do typów federacyjnych i obliczeń federacyjnych - tematu, od którego zaczęliśmy. Oto para definicji typów TFF dla modelu, który pochodzi z serwera, oraz dane, które pozostają na klientach.

SERVER_MODEL_TYPE = tff.type_at_server(MODEL_TYPE)
CLIENT_DATA_TYPE = tff.type_at_clients(LOCAL_DATA_TYPE)

Przy wszystkich dotychczas wprowadzonych definicjach, wyrażanie oceny federacyjnej w TFF jest jednowierszowe - dystrybuujemy model do klientów, pozwalamy każdemu klientowi wywołać lokalną ocenę swojej lokalnej części danych, a następnie uśredniamy straty. Oto jeden sposób, aby to napisać.

@tff.federated_computation(SERVER_MODEL_TYPE, CLIENT_DATA_TYPE)
def federated_eval(model, data):
  return tff.federated_mean(
      tff.federated_map(local_eval, [tff.federated_broadcast(model), data]))

Widzieliśmy już przykłady tff.federated_mean i tff.federated_map w prostszych scenariuszach i na poziomie intuicyjnym działają one zgodnie z oczekiwaniami, ale w tej sekcji kodu jest więcej niż na pierwszy rzut oka, więc przyjrzyjmy się temu uważnie.

Najpierw podzielmy się, aby każdy klient mógł wywołać lokalną ocenę na swojej lokalnej części danych . Jak możesz sobie przypomnieć z poprzednich sekcji, local_eval ma podpis typu w postaci (<MODEL_TYPE, LOCAL_DATA_TYPE> -> float32) .

Operator stowarzyszony tff.federated_map to szablon, który akceptuje jako parametr 2-krotkę składającą się z funkcji mapowania pewnego typu T->U i wartości stowarzyszonej typu {T}@CLIENTS (tj. {T}@CLIENTS składnikami składowymi elementu taki sam typ jak parametr funkcji mapującej) i zwraca wynik typu {U}@CLIENTS .

Ponieważ local_eval jako funkcję mapującą, która ma być stosowana dla poszczególnych klientów, drugi argument powinien być typu federacyjnego {<MODEL_TYPE, LOCAL_DATA_TYPE>}@CLIENTS , tj. {<MODEL_TYPE, LOCAL_DATA_TYPE>}@CLIENTS z nomenklaturą z poprzednich sekcji powinien być sfederowaną krotką. Każdy klient powinien posiadać pełny zestaw argumentów dla local_eval jako członek-consituent. Zamiast tego dostarczamy mu 2-elementową list Pythona. Co tu się dzieje?

Rzeczywiście, jest to przykład niejawnego rzutowania typu w TFF, podobnego do niejawnych rzutów typu, które mogłeś napotkać gdzie indziej, np. Kiedy przekazujesz int do funkcji, która akceptuje float . W tym momencie rzadko używa się rzutowania niejawnego, ale planujemy uczynić go bardziej powszechnym w TFF jako sposób na zminimalizowanie schematu.

Niejawne rzutowanie zastosowane w tym przypadku to równoważność między sfederowanymi krotkami w postaci {<X,Y>}@Z i krotkami wartości federacyjnych <{X}@Z,{Y}@Z> . Chociaż formalnie te dwa są różnymi typami sygnatur, patrząc z perspektywy programisty, każde urządzenie w Z zawiera dwie jednostki danych X i Y To, co się tutaj dzieje, jest podobne do zip w Pythonie, i rzeczywiście, oferujemy operator tff.federated_zip który pozwala na bezpośrednie wykonywanie takich konwersji. Kiedy tff.federated_map napotka krotkę jako drugi argument, po prostu wywołuje za Ciebie tff.federated_zip .

Biorąc pod uwagę powyższe, powinieneś być w stanie rozpoznać wyrażenie tff.federated_broadcast(model) jako reprezentujące wartość typu TFF {MODEL_TYPE}@CLIENTS , a data jako wartość typu TFF {LOCAL_DATA_TYPE}@CLIENTS (lub po prostu CLIENT_DATA_TYPE ) , oba są filtrowane razem przez niejawny tff.federated_zip tworząc drugi argument tff.federated_map .

Operator tff.federated_broadcast , jak można się spodziewać, po prostu przesyła dane z serwera do klientów.

Zobaczmy teraz, jak nasze lokalne szkolenie wpłynęło na średnią stratę w systemie.

print('initial_model loss =', federated_eval(initial_model,
                                             federated_train_data))
print('locally_trained_model loss =',
      federated_eval(locally_trained_model, federated_train_data))
initial_model loss = 23.025852
locally_trained_model loss = 54.432625

Rzeczywiście, zgodnie z oczekiwaniami, strata wzrosła. Aby ulepszyć model dla wszystkich użytkowników, musimy poćwiczyć na danych wszystkich.

Szkolenie federacyjne

Najprostszym sposobem wdrożenia szkolenia federacyjnego jest szkolenie lokalne, a następnie uśrednianie modeli. Wykorzystuje te same bloki konstrukcyjne i wzory, które już omówiliśmy, jak widać poniżej.

SERVER_FLOAT_TYPE = tff.type_at_server(tf.float32)


@tff.federated_computation(SERVER_MODEL_TYPE, SERVER_FLOAT_TYPE,
                           CLIENT_DATA_TYPE)
def federated_train(model, learning_rate, data):
  return tff.federated_mean(
      tff.federated_map(local_train, [
          tff.federated_broadcast(model),
          tff.federated_broadcast(learning_rate), data
      ]))

Należy pamiętać, że w pełnej implementacji Uśredniania federacyjnego dostarczanego przez tff.learning , zamiast uśredniania modeli, wolimy uśredniać delty modeli z wielu powodów, np. Możliwość obcinania norm aktualizacji, kompresji itp. .

Zobaczmy, czy trening działa, przeprowadzając kilka rund treningu i porównując średnią stratę przed i po.

model = initial_model
learning_rate = 0.1
for round_num in range(5):
  model = federated_train(model, learning_rate, federated_train_data)
  learning_rate = learning_rate * 0.9
  loss = federated_eval(model, federated_train_data)
  print('round {}, loss={}'.format(round_num, loss))
round 0, loss=21.60552406311035
round 1, loss=20.365678787231445
round 2, loss=19.27480125427246
round 3, loss=18.31110954284668
round 4, loss=17.45725440979004

Aby uzyskać kompletność, uruchommy teraz również dane testowe, aby potwierdzić, że nasz model dobrze się uogólnia.

print('initial_model test loss =',
      federated_eval(initial_model, federated_test_data))
print('trained_model test loss =', federated_eval(model, federated_test_data))
initial_model test loss = 22.795593
trained_model test loss = 17.278767

To kończy nasz samouczek.

Oczywiście nasz uproszczony przykład nie odzwierciedla wielu rzeczy, które należałoby zrobić w bardziej realistycznym scenariuszu - na przykład nie obliczyliśmy innych metryk niż strata. Zachęcamy do przestudiowania implementacji uśredniania federacyjnego w tff.learning jako bardziej kompletnego przykładu i jako sposobu na zademonstrowanie niektórych praktyk kodowania, do których chcielibyśmy zachęcić.