Algoritmi federati personalizzati, parte 2: implementazione della media federata

Visualizza su TensorFlow.org Esegui in Google Colab Visualizza la fonte su GitHub Scarica taccuino

Questo tutorial è la seconda parte di una serie in due parti che viene illustrato come implementare i tipi personalizzati di algoritmi federati nel TFF utilizzando il Federated Nucleo (FC) , che funge da base per la Federated Learning (FL) strato ( tff.learning ) .

Vi invitiamo a leggere prima la prima parte di questa serie , che introducono alcuni dei concetti chiave e le astrazioni di programmazione utilizzato qui.

Questa seconda parte della serie utilizza i meccanismi introdotti nella prima parte per implementare una versione semplice di algoritmi di formazione e valutazione federati.

Vi invitiamo a rivedere la classificazione delle immagini e la generazione del testo tutorial per un livello più alto e più dolce introduzione alle API di apprendimento federati di TFF, come che vi aiuterà a mettere i concetti che descriviamo qui nel contesto.

Prima di iniziare

Prima di iniziare, prova a eseguire il seguente esempio "Hello World" per assicurarti che il tuo ambiente sia configurato correttamente. Se non funziona, si prega di fare riferimento alla installazione guida per le istruzioni.

!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!'

Implementazione della media federata

Come in federata apprendimento per l'immagine di classificazione , stiamo per utilizzare l'esempio MNIST, ma poiché questo è inteso come una guida di basso livello, stiamo per bypassare l'API Keras e tff.simulation , scrivere codice modello grezzo, e un costrutto set di dati federati da zero.

Preparazione di set di dati federati

A scopo dimostrativo, simuleremo uno scenario in cui abbiamo i dati di 10 utenti e ciascuno degli utenti contribuisce alla conoscenza di come riconoscere una cifra diversa. Questo è quanto di non IID come si arriva.

Per prima cosa, carichiamo i dati MNIST standard:

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,))]

I dati arrivano come array Numpy, uno con immagini e un altro con etichette di cifre, entrambi con la prima dimensione che va sui singoli esempi. Scriviamo una funzione di supporto che la formatti in un modo compatibile con il modo in cui inseriamo le sequenze federate nei calcoli TFF, ad esempio come un elenco di elenchi: l'elenco esterno va dagli utenti (cifre), quello interno va dai batch di dati in sequenza di ogni cliente. Come di consueto, ci strutturare ogni lotto come una coppia di tensori denominati x ed y , ciascuna con la dimensione principale batch. Mentre a lui, ci sarà anche appiattire ogni immagine in un vettore 784 elementi e ridimensionare i pixel in esso nella 0..1 gamma, in modo che non abbiamo ingombrare la logica del modello con le conversioni di dati.

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)]

Come un controllo di integrità rapido, Diamo un'occhiata alla Y tensore nel l'ultimo lotto di dati forniti dal quinto cliente (quello corrispondente alla cifra 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)

Per sicurezza, diamo un'occhiata anche all'immagine corrispondente all'ultimo elemento di quel batch.

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

Sulla combinazione di TensorFlow e TFF

In questo tutorial, per compattezza abbiamo immediatamente decorare funzioni che introducono logica tensorflow con tff.tf_computation . Tuttavia, per logiche più complesse, questo non è lo schema che consigliamo. Il debug di TensorFlow può già essere una sfida e il debug di TensorFlow dopo che è stato completamente serializzato e quindi reimportato perde necessariamente alcuni metadati e limita l'interattività, rendendo il debug ancora più difficile.

Pertanto, si consiglia vivamente di scrivere una logica complessa TF come funzioni Python stand-alone (cioè senza tff.tf_computation decorazione). In questo modo la logica tensorflow può essere sviluppato e testato utilizzando TF ottimali e strumenti (come la modalità ansioso), prima di serializzazione il calcolo per TFF (ad esempio, invocando tff.tf_computation con una funzione Python come argomento).

Definizione di una funzione di perdita

Ora che abbiamo i dati, definiamo una funzione di perdita che possiamo usare per l'addestramento. Innanzitutto, definiamo il tipo di input come un TFF denominato tupla. Dal momento che la dimensione dei lotti di dati può variare, abbiamo impostato la dimensione batch per None per indicare che le dimensioni di questa dimensione è sconosciuta.

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[?]>'

Forse ti starai chiedendo perché non possiamo semplicemente definire un normale tipo Python. Ricordiamo la discussione in parte 1 , in cui abbiamo spiegato che, mentre siamo in grado di esprimere la logica di calcoli TFF utilizzando Python, sotto i calcoli cofano TFF non sono Python. Il simbolo BATCH_TYPE sopra definito rappresenta una specifica astratta tipo TFF. È importante distinguere questo tipo TFF estratto calcestruzzo tipi di rappresentazione Python, ad esempio, contenitori come dict o collections.namedtuple che possono essere utilizzati per rappresentare il tipo TFF nel corpo di una funzione Python. Diversamente Python, TFF ha un unico tipo di costruzione astratta tff.StructType per tupla simili contenitori, elementi che possono essere denominati singolarmente o lasciati senza nome. Questo tipo viene anche utilizzato per modellare i parametri formali dei calcoli, poiché i calcoli TFF possono dichiarare formalmente solo un parametro e un risultato - ne vedrai esempi a breve.

Diamo ora definire il tipo TFF dei parametri del modello, ancora una volta come un TFF nome tupla di pesi e pregiudizi.

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]>

Con queste definizioni in atto, ora possiamo definire la perdita per un dato modello, su un singolo lotto. Si noti l'uso di @tf.function decoratore all'interno della @tff.tf_computation decoratore. Questo ci permette di scrivere TF utilizzando Python come la semantica, anche se eravamo in un tf.Graph contesto creato dalla tff.tf_computation decoratore.

# 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)

Come previsto, il calcolo batch_loss rendimenti float32 perdita dato il modello e un singolo lotto di dati. Si noti come il MODEL_TYPE e BATCH_TYPE sono stati ammassati insieme in un 2-tupla di parametri formali; si può riconoscere il tipo di batch_loss come (<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)'

Come controllo di integrità, costruiamo un modello iniziale pieno di zeri e calcoliamo la perdita sul batch di dati che abbiamo visualizzato sopra.

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

Nota che alimentano il calcolo TFF con il modello iniziale definita come dict , anche se il corpo della funzione Python che definisce consuma parametri del modello come model['weight'] e model['bias'] . Gli argomenti della chiamata a batch_loss non sono semplicemente trasmessi al corpo di tale funzione.

Che cosa succede quando invochiamo batch_loss ? Il corpo di Python batch_loss è già stato tracciato e serializzato nella cella sopra dove è stato definito. TFF funge chiamante batch_loss al momento definizione di calcolo, e come destinazione della chiamata al momento batch_loss viene richiamato. In entrambi i ruoli, TFF funge da ponte tra il sistema di tipi astratti di TFF e i tipi di rappresentazione Python. Al momento dell'invocazione, TFF accetterà tipi di contenitori Python più standard ( dict , list , tuple , collections.namedtuple , ecc) come rappresentazioni concrete di tuple TFF astratti. Inoltre, sebbene come notato sopra, i calcoli TFF accettino formalmente solo un singolo parametro, è possibile utilizzare la familiare sintassi della chiamata Python con argomenti posizionali e/o parole chiave nel caso in cui il tipo del parametro sia una tupla - funziona come previsto.

Discesa del gradiente su un singolo lotto

Ora definiamo un calcolo che utilizzi questa funzione di perdita per eseguire un singolo passaggio di discesa del gradiente. Si noti come nella definizione di questa funzione, usiamo batch_loss come un sottocomponente. È possibile richiamare un calcolo costruito con tff.tf_computation all'interno del corpo di un altro calcolo, anche se di solito non è necessario - come notato sopra, perché serializzazione perde alcune informazioni di debug, è spesso preferibile per calcoli più complessi per scrivere e testare tutto il tensorflow senza il tff.tf_computation decoratore.

@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]>)'

Quando si richiama una funzione Python decorato con tff.tf_computation all'interno del corpo di un altro tale funzione, la logica del calcolo TFF interno è incorporato (essenzialmente, inlined) nella logica di quella esterna. Come notato sopra, se si sta scrivendo due calcoli, è probabile preferibile fare la funzione interna ( batch_loss in questo caso) un Python regolare o tf.function piuttosto che un tff.tf_computation . Tuttavia, qui illustriamo che chiamando uno tff.tf_computation all'interno di un altro funziona sostanzialmente come previsto. Questo può essere necessario se, ad esempio, non è necessario il codice Python che definisce batch_loss , ma solo la sua rappresentazione TFF serializzato.

Ora applichiamo questa funzione alcune volte al modello iniziale per vedere se la perdita diminuisce.

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]

Discesa del gradiente su una sequenza di dati locali

Ora, dal momento che batch_train sembra funzionare, scriviamo una simile funzione di formazione local_train che consuma l'intera sequenza di tutti i lotti da un utente invece di un singolo lotto. Il nuovo calcolo dovrà ora consumare tff.SequenceType(BATCH_TYPE) al posto di 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]>)'

Ci sono alcuni dettagli nascosti in questa breve sezione di codice, esaminiamoli uno per uno.

In primo luogo, mentre avremmo potuto implementato questa logica interamente in tensorflow, basandosi su tf.data.Dataset.reduce per elaborare la sequenza in modo simile a come abbiamo fatto in precedenza, abbiamo scelto questa volta per esprimere la logica nella lingua colla , come tff.federated_computation . Abbiamo utilizzato l'operatore federata tff.sequence_reduce per eseguire la riduzione.

L'operatore tff.sequence_reduce viene usato in modo simile a tf.data.Dataset.reduce . Si può pensare ad esso come essenzialmente lo stesso di tf.data.Dataset.reduce , ma per l'utilizzo all'interno calcoli federati, che, come si può ricordare, non si può contenere codice tensorflow. Si tratta di un operatore modello con un parametro formale 3-tupla che consiste in una sequenza di T elementi -typed, lo stato iniziale della riduzione (faremo riferimento astrattamente come zero) di qualche tipo U , e l'operatore di riduzione digitare (<U,T> -> U) che altera lo stato della riduzione elaborando un unico elemento. Il risultato è lo stato finale della riduzione, dopo aver elaborato tutti gli elementi in ordine sequenziale. Nel nostro esempio, lo stato della riduzione è il modello addestrato su un prefisso dei dati e gli elementi sono batch di dati.

In secondo luogo, si noti che abbiamo usato una volta calcolo ( batch_train ) come componente all'interno di un altro ( local_train ), ma non direttamente. Non possiamo usarlo come operatore di riduzione perché richiede un parametro aggiuntivo: il tasso di apprendimento. Per risolvere questo, definiamo un incorporato calcolo federata batch_fn che si lega alla local_train 's parametro learning_rate nel suo corpo. È consentito a un calcolo figlio definito in questo modo catturare un parametro formale del suo genitore fintanto che il calcolo figlio non viene invocato al di fuori del corpo del suo genitore. Si può pensare a questo modello come un equivalente di functools.partial in Python.

L'implicazione pratica di catturare learning_rate in questo modo è, naturalmente, che lo stesso valore di tasso di apprendimento viene utilizzato in tutti i lotti.

Ora, proviamo la funzione di formazione locale appena definito su tutta la sequenza di dati dallo stesso utente che ha contribuito il lotto di campioni (cifra 5 ).

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

Ha funzionato? Per rispondere a questa domanda, dobbiamo implementare la valutazione.

Valutazione locale

Ecco un modo per implementare la valutazione locale sommando le perdite su tutti i batch di dati (avremmo potuto calcolare anche la media; lo lasceremo come esercizio per il lettore).

@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)'

Di nuovo, ci sono alcuni nuovi elementi illustrati da questo codice, esaminiamoli uno per uno.

In primo luogo, abbiamo utilizzato due nuovi operatori federata per elaborare sequenze: tff.sequence_map che richiede una funzione di mappatura T->U e una sequenza di T , ed emette una sequenza di U ottenuta applicando la funzione di mappatura puntuale e tff.sequence_sum che aggiunge solo tutti gli elementi. Qui, mappiamo ogni batch di dati su un valore di perdita, quindi aggiungiamo i valori di perdita risultanti per calcolare la perdita totale.

Si noti che avremmo potuto utilizzare di nuovo tff.sequence_reduce , ma questo non sarebbe la scelta migliore - il processo di riduzione è, per definizione, sequenziale, mentre la mappatura e la somma possono essere calcolate in parallelo. Quando viene data una scelta, è meglio attenersi a operatori che non vincolano le scelte di implementazione, in modo che quando il nostro calcolo TFF verrà compilato in futuro per essere distribuito in un ambiente specifico, si possa sfruttare appieno tutte le potenziali opportunità per un più veloce , esecuzione più scalabile e più efficiente in termini di risorse.

In secondo luogo, si noti che come in local_train , la funzione componente Dobbiamo ( batch_loss ) batte più parametri quali l'operatore federata ( tff.sequence_map ) si aspetta, quindi abbiamo ancora definiamo un parziale, questa volta in linea avvolgendo direttamente un lambda come tff.federated_computation . Uso involucri linea con una funzione come argomento è il metodo consigliato per utilizzare tff.tf_computation per incorporare tensorflow logica TFF.

Ora, vediamo se la nostra formazione ha funzionato.

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

In effetti, la perdita è diminuita. Ma cosa succede se lo valutiamo sui dati di un altro utente?

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

Come previsto, le cose sono peggiorate. Il modello è stato addestrato a riconoscere 5 , e non ha mai visto un 0 . Questo porta alla domanda: in che modo la formazione locale ha influito sulla qualità del modello dal punto di vista globale?

Valutazione federata

Questo è il punto del nostro viaggio in cui torniamo finalmente ai tipi federati e ai calcoli federati, l'argomento da cui siamo partiti. Ecco una coppia di definizioni dei tipi TFF per il modello che ha origine nel server e i dati che rimangono sui client.

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

Con tutte le definizioni introdotte finora, l'espressione della valutazione federata in TFF è una riga: distribuiamo il modello ai client, lasciamo che ogni client invochi la valutazione locale sulla sua porzione locale di dati e quindi calcoliamo la media della perdita. Ecco un modo per scrivere questo.

@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]))

Abbiamo già visto esempi di tff.federated_mean e tff.federated_map in scenari più semplici, e, a livello intuitivo, essi funzionano come previsto, ma c'è di più in questa sezione di codice che soddisfa l'occhio, quindi cerchiamo di andare su di esso con attenzione.

In primo luogo, la rottura di getterò le lasciare che ogni cliente invoke valutazione locale sulla sua porzione locale della parte di dati. Come ricorderete dalle sezioni precedenti, local_eval ha una firma tipo di modulo (<MODEL_TYPE, LOCAL_DATA_TYPE> -> float32) .

L'operatore federata tff.federated_map è un modello che accetta come parametro un 2-tupla che consiste nella funzione di mappatura di qualche tipo T->U e un valore federata di tipo {T}@CLIENTS (cioè, con costituenti membri della stesso tipo del parametro della funzione di mappatura), e restituisce un risultato di tipo {U}@CLIENTS .

Dal momento che siamo l'alimentazione local_eval come una funzione di mappatura da applicare su una base per-client, il secondo argomento dovrebbe essere di un tipo federato {<MODEL_TYPE, LOCAL_DATA_TYPE>}@CLIENTS , vale a dire, nella nomenclatura delle sezioni precedenti, si deve essere una tupla federata. Ogni cliente dovrebbe tenere una serie completa di argomenti per local_eval come consituent membro. Invece, stiamo alimentando un 2-elemento Python list . Cosa sta succedendo qui?

Infatti, questo è un esempio di un tipo colato implicita TFF, simile a calchi tipo implicite si può avere incontrato altrove, ad esempio, quando si invia un int ad una funzione che accetta un float . Il casting implicito è usato raramente a questo punto, ma abbiamo intenzione di renderlo più pervasivo in TFF come un modo per ridurre al minimo il boilerplate.

Il cast implicito che viene applicato in questo caso è l'equivalenza tra tuple federati della forma {<X,Y>}@Z , e tuple di federata valori <{X}@Z,{Y}@Z> . Mentre formalmente, questi due sono diverse firme di tipo, guardando dal punto di vista del programmatore, ogni dispositivo Z contiene due unità di dati X e Y . Quello che succede qui non è dissimile da zip in Python, e in effetti, offriamo un operatore tff.federated_zip che consente di effettuare tali conversioni esplicitamente. Quando il tff.federated_map incontra una tupla come secondo argomento, si invoca semplicemente tff.federated_zip per voi.

Ciò premesso, si dovrebbe ora essere in grado di riconoscere l'espressione tff.federated_broadcast(model) come per un valore di TFF tipo {MODEL_TYPE}@CLIENTS , ed data come un valore di tipo TFF {LOCAL_DATA_TYPE}@CLIENTS (o semplicemente CLIENT_DATA_TYPE ) , i due ottenendo filtrato insieme attraverso un implicito tff.federated_zip per formare il secondo argomento tff.federated_map .

L'operatore tff.federated_broadcast , come ci si aspetterebbe, semplicemente trasferisce i dati dal server ai client.

Ora, vediamo come la nostra formazione locale ha influito sulla perdita media nel sistema.

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

Infatti, come previsto, la perdita è aumentata. Per migliorare il modello per tutti gli utenti, dovremo addestrarci sui dati di tutti.

Formazione federata

Il modo più semplice per implementare l'addestramento federato consiste nell'addestrare localmente e quindi fare la media dei modelli. Questo utilizza gli stessi blocchi e schemi di cui abbiamo già discusso, come puoi vedere di seguito.

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
      ]))

Si noti che nella realizzazione full-optional di Federated della media fornita da tff.learning , piuttosto che la media dei modelli, preferiamo delta medio del modello, per una serie di motivi, ad esempio, la capacità di ritagliare le norme di aggiornamento, per la compressione, ecc .

Vediamo se l'allenamento funziona eseguendo alcuni cicli di allenamento e confrontando la perdita media prima e dopo.

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

Per completezza, eseguiamo ora anche i dati del test per confermare che il nostro modello generalizza bene.

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

Questo conclude il nostro tutorial.

Ovviamente, il nostro esempio semplificato non riflette una serie di cose che avresti bisogno di fare in uno scenario più realistico - ad esempio, non abbiamo calcolato metriche diverse dalla perdita. Vi incoraggiamo a studiare l'attuazione di una media federata in tff.learning come un esempio più completo, e come un modo per dimostrare alcune delle pratiche di codifica che vorremmo incoraggiare.