Niestandardowe algorytmy federacyjne, część 2: Implementacja federacyjnego uśredniania

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

Ten poradnik jest drugą częścią serii dwuczęściowym, który pokazuje jak zaimplementować niestandardowych rodzajów algorytmów federacyjnych w TFF pomocą Federalne rdzeń (FC) , który służy jako podstawa do (FL) Federalne Uczenie warstwa ( tff.learning ) .

Zachęcamy do zapoznania się z pierwszym pierwszą część tej serii , które wprowadzają niektóre z kluczowych pojęć i programowanie abstrakcje tutaj.

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

Zachęcamy do przeglądu klasyfikacji obrazu oraz generowanie tekstu tutoriale na wyższym poziomie i bardziej delikatne wprowadzenie do Federated API Learning TFF, tak jak oni pomogą Ci umieścić opisujemy tutaj w kontekście pojęć.

Zanim zaczniemy

Zanim zaczniemy, spróbuj uruchomić następujący przykład "Hello World", aby upewnić się, że twoje środowisko jest poprawnie skonfigurowane. Jeśli to nie działa, proszę odnieść się do montażu prowadnicy do instrukcji.

!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

# Must use the Python context because it
# supports tff.sequence_* intrinsics.
executor_factory = tff.framework.local_executor_factory(
    support_sequence_ops=True)
execution_context = tff.framework.ExecutionContext(
    executor_fn=executor_factory)
tff.framework.set_default_context(execution_context)
@tff.federated_computation
def hello_world():
  return 'Hello, World!'

hello_world()
b'Hello, World!'

Wdrażanie sfederowanego uśredniania

Podobnie jak w Federacji Learning dla wizerunku Klasyfikacji , mamy zamiar użyć przykładu MNIST, ale skoro to ma służyć jako poradnik niskiego poziomu, mamy zamiar obejścia API Keras i tff.simulation , pisać surowy kod modelu i skonstruować sfederowany zestaw danych od podstaw.

Przygotowywanie sfederowanych zbiorów danych

Na potrzeby demonstracji zasymulujemy scenariusz, w którym mamy dane od 10 użytkowników, a każdy z nich wnosi wiedzę, jak rozpoznać inną cyfrę. To jest tak nie- IID jak to się robi.

Najpierw załadujmy standardowe dane MNIST:

mnist_train, mnist_test = tf.keras.datasets.mnist.load_data()
[(x.dtype, x.shape) for x in mnist_train]
[(dtype('uint8'), (60000, 28, 28)), (dtype('uint8'), (60000,))]

Dane są dostarczane jako tablice Numpy, jedna z obrazami, a druga z etykietami cyfr, obie z pierwszym wymiarem przechodzącym przez poszczególne przykłady. Napiszmy funkcję pomocniczą, która sformatuje ją w sposób zgodny z tym, w jaki sposób wprowadzamy sekwencje sfederowane do obliczeń TFF, tj. jako listę list - zewnętrzną listę obejmującą użytkowników (cyfry), wewnętrzną obejmującą 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. Podczas gdy na niego, będziemy także spłaszczyć każdy obraz w 784-element wektora i przeskalować piksele w nim do 0..1 zakresu, tak że nie trzeba zaśmiecać logikę modelu z konwersja 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)]

Jako szybki kontrola poprawności, wygląd Chodźmy na Y tensora w ostatniej partii danych przekazanych przez piątego klienta (jeden 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 spójrzmy również na obrazek odpowiadający 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 dla zwartości natychmiast udekorować funkcje, które wprowadzają logikę TensorFlow z tff.tf_computation . Jednak w przypadku bardziej złożonej logiki nie jest to zalecany przez nas 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ść, czyniąc debugowanie jeszcze większym wyzwaniem.

Dlatego zalecamy pisanie złożonej logiki TF jako funkcje Pythona autonomicznych (czyli bez tff.tf_computation dekoracji). W ten sposób logika TensorFlow mogą być rozwijane i testowane przy użyciu TF najlepszych praktyk i narzędzi (takich jak tryb chętny), przed szeregowania obliczeń dla TFF (np powołując tff.tf_computation z funkcją Pythona jako argument).

Definiowanie funkcji straty

Teraz, gdy mamy dane, zdefiniujmy funkcję straty, której możemy użyć do treningu. Najpierw zdefiniujmy typ danych wejściowych jako krotkę o nazwie TFF. Ponieważ wielkość porcji danych może się różnić, ustalamy wymiar wsadowy do None , aby wskazać, że wielkość 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[?]>'

Być może zastanawiasz się, dlaczego nie możemy po prostu zdefiniować zwykłego typu Pythona. Przypomnijmy dyskusję w części 1 , gdzie wyjaśnił, że chociaż możemy wyrazić logikę obliczeń TFF przy użyciu Pythona pod obliczeń kaptur TFF nie są Python. Symbol BATCH_TYPE zdefiniowano powyżej, stanowi streszczenie specyfikację typu TFF. Ważne jest, aby odróżnić ten rodzaj TFF abstrakcyjne od konkretnych typów reprezentacji Python, np pojemnikach, takich jak dict lub collections.namedtuple , które mogą być wykorzystane do reprezentowania typu TFF w ciele funkcji Pythona. W przeciwieństwie do Pythona, TFF ma jeden abstrakcyjny typ konstruktora tff.StructType za krotka, jak pojemniki z elementami, które mogą być indywidualnie nazwanych lub pozostawione bezimienny. 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 tego.

Chodźmy teraz określić typ TFF parametrów modelu, ponownie jako TFF nazwana krotki wag i uprzedzeń.

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 jednej partii. Uwaga wykorzystanie @tf.function dekoratora wewnątrz @tff.tf_computation dekoratora. To pozwala nam napisać TF przy użyciu Python jak semantyki chociaż były wewnątrz tf.Graph kontekście stworzonej przez tff.tf_computation dekoratora.

# 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 float32 straty ze względu na model i pojedynczej partii danych. Uwaga jak MODEL_TYPE i BATCH_TYPE zostały skupione razem w 2-krotki parametrów formalnych; można rozpoznać typ batch_loss jako (<MODEL_TYPE,BATCH_TYPE> -> float32) .

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

Aby sprawdzić rozsądek, skonstruujmy początkowy model wypełniony zerami i obliczmy stratę na partii danych, którą zwizualizowaliś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.3025851

Zauważ, że obliczenia TFF paszy z początkowego modelu zdefiniowane jako dict , chociaż jednostka funkcji języka, których wyznacza zużywa modelu parametry model['weight'] i model['bias'] . Argumenty wywołania do batch_loss po prostu nie są przekazywane do organu tej funkcji.

Co się dzieje, gdy wzywamy batch_loss ? Pythonową ciało batch_loss już zostało odnalezione i szeregowane w powyższej komórki, w której została określona. TFF działa jako dzwoniącego do batch_loss w momencie definiowania obliczeń, a jako cel wywołania w czasie batch_loss jest wywoływany. W obu rolach TFF służy jako pomost między abstrakcyjnym systemem typów TFF a typami reprezentacji Pythona. W momencie wywołania, TFF akceptuje większość standardowych typów kontenerów (Python dict , list , tuple , collections.namedtuple , etc.) jak beton przedstawień abstrakcyjnych krotki 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 jest krotką — działa zgodnie z oczekiwaniami.

Zejście gradientowe na jednej partii

Teraz zdefiniujmy obliczenia, które wykorzystują tę funkcję straty do wykonania pojedynczego kroku opadania gradientu. Uwaga jak w definiowaniu tej funkcji, używamy batch_loss jako podskładnika. Można powołać się na obliczenia wykonane z tff.tf_computation wewnątrz ciała innego obliczeń, choć zwykle nie jest to konieczne - jak wspomniano powyżej, ponieważ serializacji traci pewne informacje debugowania, często jest to korzystne dla bardziej złożonych obliczeń pisać i testować wszystkie TensorFlow bez tff.tf_computation dekoratora.

@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)
'(<initial_model=<weights=float32[784,10],bias=float32[10]>,batch=<x=float32[?,784],y=int32[?]>,learning_rate=float32> -> <weights=float32[784,10],bias=float32[10]>)'

Po wywołaniu funkcji Pythona ozdobione tff.tf_computation w ciele innej takiej funkcji logiki wewnętrznej obliczeń TFF jest osadzony (zasadniczo inline), w logice warstwy zewnętrznej. Jak wspomniano powyżej, jeśli piszesz zarówno obliczenia, prawdopodobne jest korzystne, aby wewnętrzną funkcję ( batch_loss w tym przypadku) regularne Python lub tf.function raczej niż tff.tf_computation . Jednak tu zilustrować, że nazwanie jednej tff.tf_computation wewnątrz innej zasadzie działa zgodnie z oczekiwaniami. Może to być konieczne, jeśli, na przykład, nie masz kodu Pythona określającą batch_loss , lecz tylko jego odcinkach reprezentacji TFF.

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

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.19690023, 0.13176313, 0.10113225, 0.08273812, 0.070301384]

Zejście gradientowe w sekwencji danych lokalnych

Teraz, ponieważ batch_train wydaje się działać, niech napisać podobną funkcję szkolenie local_train który zużywa cała sekwencja wszystkich partii od jednego użytkownika, a nie tylko jednej partii. Nowe obliczenia trzeba będzie teraz konsumować 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):

  @tff.tf_computation(LOCAL_DATA_TYPE, tf.float32)
  def _insert_learning_rate_to_sequence(dataset, learning_rate):
    return dataset.map(lambda x: (x, learning_rate))

  batches_with_learning_rate = _insert_learning_rate_to_sequence(all_batches, learning_rate)

  # Mapping function to apply to each batch.
  @tff.federated_computation(MODEL_TYPE, batches_with_learning_rate.type_signature.element)
  def batch_fn(model, batch_with_lr):
    batch, lr = batch_with_lr
    return batch_train(model, batch, lr)

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

W tej krótkiej części kodu jest sporo szczegółów, przejrzyjmy je jeden po drugim.

Po pierwsze, podczas gdy moglibyśmy wdrożyły tę logikę w całości TensorFlow, powołując się na tf.data.Dataset.reduce do przetwarzania sekwencji podobnie do tego, jak zrobiliśmy to wcześniej, my zdecydowaliśmy się tym razem do wyrażania logiki w języku kleju jako tff.federated_computation . Użyliśmy stowarzyszonej operatora tff.sequence_reduce do przeprowadzenia redukcji.

Operator tff.sequence_reduce jest stosowany w sposób podobny do tf.data.Dataset.reduce . Można myśleć o nim jako w zasadzie takie same jak tf.data.Dataset.reduce , ale do stosowania wewnątrz federacyjnych obliczeń, które jak może pamiętacie, nie może zawierać kod TensorFlow. Jest to operator szablon z formalny parametr 3-krotki, które składa się z sekwencji T -typed elementy, początkowy stan redukcji (będziemy odnosić się do niego abstrakcyjnie jako zero) jakiegoś typu U , a operator redukcja typ (<U,T> -> U) , który zmienia stan redukcji za pomocą obróbki pojedynczego elementu. Rezultatem jest końcowy stan redukcji, po przetworzeniu wszystkich elementów w kolejności sekwencyjnej. W naszym przykładzie stanem redukcji jest model wyszkolony na prefiksie danych, a elementy to partie danych.

Po drugie, pamiętać, że mamy ponownie użyty jeden obliczeń ( batch_train ) jako składnik w innym ( local_train ), ale nie bezpośrednio. Nie możemy go używać jako operatora redukcji, ponieważ przyjmuje dodatkowy parametr - szybkość uczenia się. Aby rozwiązać ten problem, możemy zdefiniować wbudowany stowarzyszonego obliczeń batch_fn który wiąże się z local_train jako parametr learning_rate w jego organizmie. Dozwolone jest, aby obliczenie podrzędne zdefiniowane w ten sposób uchwyciło formalny parametr rodzica, o ile obliczenie podrzędne nie jest wywoływane poza ciałem rodzica. Można myśleć o tym wzorem jako ekwiwalent functools.partial w Pythonie.

Praktyczną implikacją przechwytywanie learning_rate ten sposób to, oczywiście, że ta sama wartość szybkość uczenia się jest stosowany we wszystkich partiach.

Teraz spróbujmy nowo zdefiniowanej funkcji lokalnego trening na całej sekwencji danych z tego samego użytkownika, który przyczynił się seria próbna (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 implementacji oceny lokalnej przez zsumowanie strat we wszystkich partiach danych (mogliśmy równie dobrze obliczyć średnią; zostawimy to jako ćwiczenie dla czytelnika).

@tff.federated_computation(MODEL_TYPE, LOCAL_DATA_TYPE)
def local_eval(model, all_batches):

  @tff.tf_computation(MODEL_TYPE, LOCAL_DATA_TYPE)
  def _insert_model_to_sequence(model, dataset):
    return dataset.map(lambda x: (model, x))

  model_plus_data = _insert_model_to_sequence(model, all_batches)

  @tff.tf_computation(tf.float32, batch_loss.type_signature.result)
  def tff_add(accumulator, arg):
    return accumulator + arg

  return tff.sequence_reduce(
      tff.sequence_map(
          batch_loss,
          model_plus_data), 0., tff_add)
str(local_eval.type_signature)
'(<model=<weights=float32[784,10],bias=float32[10]>,all_batches=<x=float32[?,784],y=int32[?]>*> -> float32)'

Znowu jest kilka nowych elementów zilustrowanych tym kodem, przejrzyjmy je jeden po drugim.

Po pierwsze, są używane dwie nowych operatorów stowarzyszone przetwarzania sekwencji: tff.sequence_map które ma funkcję mapowania T->U i sekwencję T i emituje sekwencja U otrzymuje się przez zastosowanie funkcji mapowania punktowo i tff.sequence_sum że po prostu dodaje wszystkie elementy. Tutaj mapujemy każdą partię danych na wartość strat, a następnie dodajemy wynikowe wartości strat, aby obliczyć całkowitą stratę.

Zauważ, że mogliśmy ponownie wykorzystane tff.sequence_reduce , ale to nie będzie najlepszym wyborem - proces redukcji jest, z definicji, sekwencyjnego, podczas gdy mapowanie i suma może być obliczane równolegle. Mając wybór, najlepiej trzymać się operatorów, które nie ograniczają możliwości wyboru implementacji, tak aby gdy nasze obliczenia TFF zostaną skompilowane w przyszłości do wdrożenia w określonym środowisku, można w pełni wykorzystać wszystkie potencjalne możliwości szybszego , bardziej skalowalne i wydajniejsze pod względem zasobów wykonanie.

Po drugie należy pamiętać, że podobnie jak w local_train funkcja składowa musimy ( batch_loss ) wykonuje więcej parametrów niż podmiot stowarzyszony ( tff.sequence_map ) oczekuje, więc ponownie zdefiniować częściowy, tym razem inline bezpośrednio owijania lambda jako tff.federated_computation . Korzystanie owijarki inline z funkcji jako argument jest zalecanym sposobem użycia tff.tf_computation osadzić TensorFlow logikę w TFF.

Zobaczmy teraz, czy nasz trening zadziałał.

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.43484688

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 do rozpoznawania 5 , i nigdy nie widział 0 . Nasuwa się pytanie – jak lokalne szkolenie wpłynęło na jakość modelu z perspektywy globalnej?

Ocena sfederowana

To jest punkt naszej podróży, w którym w końcu wracamy do typów sfederowanych i obliczeń sfederowanych — tematu, od którego zaczęliśmy. Oto para definicji typów TFF dla modelu pochodzącego z serwera oraz danych, 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 definicjach wprowadzonych do tej pory, wyrażanie sfederowanej oceny w TFF jest jednokierunkowe — dystrybuujemy model do klientów, pozwalamy każdemu klientowi wywoływać lokalną ocenę na swojej lokalnej części danych, a następnie uśredniamy straty. Oto jeden sposób na napisanie tego.

@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 scenariuszy, a na poziomie intuicyjnym, pracują zgodnie z oczekiwaniami, ale chodzi o coś więcej w tej sekcji kodu niż na pierwszy rzut oka, więc chodźmy nad nim ostrożnie.

Po pierwsze, przerwa Chodźmy na dół dać każdemu klientowi invoke oceny lokalnego na swojej lokalnej części część danych. Jak być może pamiętacie z poprzednich odcinków, local_eval ma podpis typu formularza (<MODEL_TYPE, LOCAL_DATA_TYPE> -> float32) .

Stowarzyszonego operatora tff.federated_map jest szablon, który przyjmuje się jako parametr 2-krotnego, który składa się z funkcji mapowania pewnego typu T->U a wartości stowarzyszonego typu {T}@CLIENTS (czyli związki z członkiem The tego samego typu co parametru funkcji mapowania) oraz zwraca wynik typu {U}@CLIENTS .

Ponieważ jesteśmy karmienia local_eval jako funkcję mapowania do stosowania na zasadzie per-klient, drugi argument powinien być typu stowarzyszonym {<MODEL_TYPE, LOCAL_DATA_TYPE>}@CLIENTS , czyli w nomenklaturze z poprzednich odcinków, to powinien być sfederowaną krotką. Każdy klient powinien posiadać pełny zestaw argumentów za local_eval jako consituent członkowskim. Zamiast tego, mamy do karmienia mu 2-elementowej Pythona list . Co tu się dzieje?

Rzeczywiście, jest przykładem niejawny typu oddanych w TFF, podobny do ukrytych odlewów typu możesz napotkać gdzie indziej, na przykład, gdy karmisz się int do funkcji, która przyjmuje float . Niejawne rzucanie jest obecnie rzadko używane, ale planujemy uczynić je bardziej powszechnym w TFF, aby zminimalizować schemat.

Domniemana odlew, który jest stosowany w tym przypadku, to równoważność stowarzyszonych krotki w postaci {<X,Y>}@Z i krotki SFEDEROWANYCH wartości <{X}@Z,{Y}@Z> . Choć formalnie, te dwa są różne podpisy typu, patrząc na to z punktu widzenia programistów, każde urządzenie w Z posiada dwie jednostki danych X i Y . Co tu się dzieje nie jest w przeciwieństwie do zip w Pythonie, i rzeczywiście, oferujemy operatora tff.federated_zip , który pozwala na wykonywanie takiej konwersji jawnie. Gdy tff.federated_map napotka krotki jako drugi argument, to po prostu wywołuje tff.federated_zip dla Ciebie.

Biorąc powyższe pod uwagę, należy teraz być w stanie rozpoznać wyrażenie tff.federated_broadcast(model) jako reprezentujące wartość TFF typu {MODEL_TYPE}@CLIENTS i data jako wartość typu TFF {LOCAL_DATA_TYPE}@CLIENTS (lub po prostu CLIENT_DATA_TYPE ) , dwa coraz przesączono wraz z domniemanym tff.federated_zip celu utworzenia drugiego argumentu tff.federated_map .

Operator tff.federated_broadcast , jak można się spodziewać, po prostu przenosi dane z serwera do klienta.

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 trenować na danych wszystkich.

Szkolenie sfederowane

Najprostszym sposobem zaimplementowania szkolenia federacyjnego jest szkolenie lokalne, a następnie uśrednianie modeli. Wykorzystuje to te same bloki konstrukcyjne i wzorce, 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
      ]))

Zauważ, że w pełni funkcjonalny wdrożenia Federalne uśredniania dostarczonych przez tff.learning zamiast uśrednienie modele, wolimy średnich modeli delt, dla wielu powodów, na przykład zdolność do klipu normy aktualizacji, na ściskanie, etc .

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.60552215576172
round 1, loss=20.365678787231445
round 2, loss=19.27480125427246
round 3, loss=18.311111450195312
round 4, loss=17.45725440979004

Dla kompletności przeanalizujmy 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 wskaźników innych niż strata. Zachęcamy do zbadania wdrażania w stowarzyszonym uśredniania w tff.learning jak Pełniejszy przykład i jako sposób wykazać niektóre praktyki kodowania Chcielibyśmy zachęcić.