Questa pagina è stata tradotta dall'API Cloud Translation.
Switch to English

Algoritmi federati personalizzati, Parte 1: Introduzione al Federated Core

Visualizza su TensorFlow.org Esegui in Google Colab Visualizza sorgente su GitHub

Questo tutorial è la prima parte di una serie in due parti che dimostra come implementare tipi personalizzati di algoritmi federati in TensorFlow Federated (TFF) utilizzando Federated Core (FC) , un insieme di interfacce di livello inferiore che servono come base su cui abbiamo implementato il livello Federated Learning (FL) .

Questa prima parte è più concettuale; introduciamo alcuni dei concetti chiave e delle astrazioni di programmazione utilizzati in TFF e ne dimostriamo l'uso su un esempio molto semplice con un array distribuito di sensori di temperatura. Nella seconda parte di questa serie , utilizziamo i meccanismi che presentiamo qui per implementare una versione semplice di algoritmi di addestramento e valutazione federati. Come follow-up, ti incoraggiamo a studiare l'implementazione della media federata in tff.learning .

Entro la fine di questa serie, dovresti essere in grado di riconoscere che le applicazioni di Federated Core non sono necessariamente limitate all'apprendimento. Le astrazioni di programmazione che offriamo sono piuttosto generiche e potrebbero essere utilizzate, ad esempio, per implementare analisi e altri tipi di calcoli personalizzati su dati distribuiti.

Sebbene questo tutorial sia progettato per essere autonomo, ti invitiamo a leggere prima i tutorial sulla classificazione delle immagini e sulla generazione di testo per un'introduzione più delicata e di livello superiore al framework federato TensorFlow e alle API Federated Learning ( tff.learning ), come ti aiuterà a contestualizzare i concetti che descriviamo qui.

Usi previsti

In poche parole, Federated Core (FC) è un ambiente di sviluppo che consente di esprimere in modo compatto la logica del programma che combina il codice TensorFlow con operatori di comunicazione distribuita, come quelli utilizzati in Federated Averaging : calcolo di somme distribuite, medie e altri tipi di aggregazioni distribuite su un insieme di dispositivi client nel sistema, modelli di trasmissione e parametri a tali dispositivi, ecc.

Potresti essere a conoscenza di tf.contrib.distribute e una domanda naturale da porre a questo punto potrebbe essere: in che modo differisce questo framework? Dopo tutto, entrambi i framework tentano di distribuire i calcoli di TensorFlow.

Un modo per pensarci è che, mentre l'obiettivo dichiarato di tf.contrib.distribute è quello di consentire agli utenti di utilizzare i modelli esistenti e il codice di addestramento con modifiche minime per abilitare la formazione distribuita , e molta attenzione è su come sfruttare l'infrastruttura distribuita Per rendere il codice di formazione esistente più efficiente, l'obiettivo del Federated Core di TFF è quello di dare a ricercatori e professionisti un controllo esplicito sui modelli specifici di comunicazione distribuita che useranno nei loro sistemi. L'obiettivo di FC è fornire un linguaggio flessibile ed estensibile per esprimere algoritmi di flusso di dati distribuiti, piuttosto che un insieme concreto di capacità di addestramento distribuite implementate.

Uno dei destinatari principali dell'API FC di TFF è costituito da ricercatori e professionisti che potrebbero voler sperimentare nuovi algoritmi di apprendimento federati e valutare le conseguenze di scelte di progettazione sottili che influenzano il modo in cui il flusso di dati nel sistema distribuito è orchestrato, ma senza rimanere impantanati dai dettagli di implementazione del sistema. Il livello di astrazione a cui mira l'API FC corrisponde all'incirca allo pseudocodice che si potrebbe utilizzare per descrivere i meccanismi di un algoritmo di apprendimento federato in una pubblicazione di ricerca: quali dati esistono nel sistema e come vengono trasformati, ma senza scendere al livello di scambi di messaggi di rete punto a punto individuali.

Il TFF nel suo insieme si rivolge a scenari in cui i dati vengono distribuiti e devono rimanere tali, ad esempio, per motivi di privacy, e dove la raccolta di tutti i dati in una posizione centralizzata potrebbe non essere un'opzione praticabile. Ciò ha implicazioni sull'implementazione di algoritmi di apprendimento automatico che richiedono un maggiore grado di controllo esplicito, rispetto agli scenari in cui tutti i dati possono essere accumulati in una posizione centralizzata in un data center.

Prima di iniziare

Prima di immergerci nel codice, prova a eseguire il seguente esempio "Hello World" per assicurarti che il tuo ambiente sia configurato correttamente. Se non funziona, fare riferimento alla Guida all'installazione 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
@tff.federated_computation
def hello_world():
  return 'Hello, World!'

hello_world()
b'Hello, World!'

Dati federati

Una delle caratteristiche distintive di TFF è che consente di esprimere in modo compatto i calcoli basati su TensorFlow su dati federati . Useremo il termine dati federati in questo tutorial per fare riferimento a una raccolta di elementi di dati ospitati su un gruppo di dispositivi in ​​un sistema distribuito. Ad esempio, le applicazioni in esecuzione su dispositivi mobili possono raccogliere dati e archiviarli localmente, senza caricarli in una posizione centralizzata. Oppure, una serie di sensori distribuiti può raccogliere e memorizzare le letture della temperatura nelle loro posizioni.

I dati federati come quelli negli esempi precedenti sono trattati in TFF come cittadini di prima classe , cioè possono apparire come parametri e risultati di funzioni e hanno tipi. Per rafforzare questa nozione, faremo riferimento ai set di dati federati come valori federati o come valori di tipi federati .

Il punto importante da capire è che stiamo modellando l'intera raccolta di elementi di dati su tutti i dispositivi (ad esempio, l'intera raccolta di letture della temperatura da tutti i sensori in un array distribuito) come un singolo valore federato.

Ad esempio, ecco come definire in TFF il tipo di float federato ospitato da un gruppo di dispositivi client. Una raccolta di letture di temperatura che si materializzano su una serie di sensori distribuiti potrebbe essere modellata come un valore di questo tipo federato.

federated_float_on_clients = tff.FederatedType(tf.float32, tff.CLIENTS)

Più in generale, un tipo federato in TFF viene definito specificando il tipo T dei suoi componenti membri : gli elementi di dati che risiedono sui singoli dispositivi e il gruppo G di dispositivi su cui sono ospitati i valori federati di questo tipo (più un terzo, informazione facoltativa di cui parleremo a breve). Ci riferiamo al gruppo G di dispositivi che ospitano un valore federata come il posizionamento del valore. Pertanto, tff.CLIENTS è un esempio di posizionamento.

str(federated_float_on_clients.member)
'float32'
str(federated_float_on_clients.placement)
'CLIENTS'

Un tipo federato con componenti membri T e posizionamento G può essere rappresentato in modo compatto come {T}@G , come mostrato di seguito.

str(federated_float_on_clients)
'{float32}@CLIENTS'

Le parentesi graffe {} in questa notazione concisa servono a ricordare che i componenti dei membri (elementi di dati su dispositivi diversi) possono differire, come ci si aspetterebbe ad esempio, dalle letture del sensore di temperatura, quindi i clienti come gruppo ospitano insieme un multi -set di elementi di tipo T che insieme costituiscono il valore federato.

È importante notare che i componenti membri di un valore federato sono generalmente opachi per il programmatore, vale a dire, un valore federato non dovrebbe essere pensato come un semplice dict codificato da un identificatore di un dispositivo nel sistema - questi valori hanno lo scopo di essere trasformati collettivamente solo da operatori federati che rappresentano astrattamente vari tipi di protocolli di comunicazione distribuiti (come l'aggregazione). Se questo suona troppo astratto, non preoccuparti: torneremo su questo a breve e lo illustreremo con esempi concreti.

I tipi federati in TFF sono disponibili in due versioni: quelli in cui i componenti membri di un valore federato possono differire (come appena visto sopra) e quelli in cui sono noti per essere tutti uguali. Questo è controllato dal terzo parametro opzionale all_equal nel costruttore tff.FederatedType (predefinito su False ).

federated_float_on_clients.all_equal
False

Un tipo federato con un posizionamento G in cui tutti i componenti del membro di tipo T sono noti per essere uguali può essere rappresentato in modo compatto come T@G (al contrario di {T}@G , cioè con le parentesi graffe lasciate cadere per riflettere il fatto che il multi-set di componenti membri è costituito da un unico elemento).

str(tff.FederatedType(tf.float32, tff.CLIENTS, all_equal=True))
'float32@CLIENTS'

Un esempio di un valore federato di questo tipo che potrebbe sorgere in scenari pratici è un iperparametro (come un tasso di apprendimento, una norma di ritaglio, ecc.) Che è stato trasmesso da un server a un gruppo di dispositivi che partecipano alla formazione federata.

Un altro esempio è un insieme di parametri per un modello di apprendimento automatico pre-addestrato sul server, che sono stati poi trasmessi a un gruppo di dispositivi client, dove possono essere personalizzati per ogni utente.

Ad esempio, supponiamo di avere una coppia di float32 parametri a e b per un modello di regressione lineare unidimensionale. Possiamo costruire il tipo (non federato) di tali modelli da utilizzare in TFF come segue. Le parentesi graffe <> nella stringa del tipo stampato sono una notazione TFF compatta per tuple con nome o senza nome.

simple_regression_model_type = (
    tff.StructType([('a', tf.float32), ('b', tf.float32)]))

str(simple_regression_model_type)
'<a=float32,b=float32>'

Nota che stiamo solo specificando dtype s sopra. Sono supportati anche i tipi non scalari. Nel codice precedente, tf.float32 è una notazione di scorciatoia per il più generale tff.TensorType(dtype=tf.float32, shape=[]) .

Quando questo modello viene trasmesso ai client, il tipo del valore federato risultante può essere rappresentato come mostrato di seguito.

str(tff.FederatedType(
    simple_regression_model_type, tff.CLIENTS, all_equal=True))
'<a=float32,b=float32>@CLIENTS'

Per simmetria con il float federato sopra, faremo riferimento a un tipo di questo tipo come una tupla federata . Più in generale, useremo spesso il termine XYZ federato per fare riferimento a un valore federato in cui i componenti membri sono simili a XYZ . Pertanto, parleremo di cose come tuple federate , sequenze federate , modelli federati e così via.

Ora, tornando a float32@CLIENTS - anche se sembra replicato su più dispositivi, in realtà è un singolo float32 , poiché tutti i membri sono uguali. In generale, si può pensare a qualsiasi tipo federato tutto uguale , ovvero uno della forma T@G , come isomorfo a un tipo T non federato, poiché in entrambi i casi, in realtà c'è un solo elemento (sebbene potenzialmente replicato) di tipo T

Dato l'isomorfismo tra T e T@G , potresti chiederti a quale scopo, se del caso, potrebbero servire questi ultimi tipi. Continuare a leggere.

Posizionamenti

Panoramica del design

Nella sezione precedente, abbiamo introdotto il concetto di posizionamenti : gruppi di partecipanti al sistema che potrebbero ospitare insieme un valore federato e abbiamo dimostrato l'uso di tff.CLIENTS come specifica di esempio di un posizionamento.

Per spiegare perché la nozione di posizionamento è così fondamentale che abbiamo dovuto incorporarla nel sistema di tipi TFF, ricorda ciò che abbiamo menzionato all'inizio di questo tutorial su alcuni degli usi previsti di TFF.

Sebbene in questo tutorial vedrai solo il codice TFF eseguito localmente in un ambiente simulato, il nostro obiettivo è che TFF consenta la scrittura di codice che potresti distribuire per l'esecuzione su gruppi di dispositivi fisici in un sistema distribuito, potenzialmente inclusi dispositivi mobili o incorporati con Android. Ciascuno di questi dispositivi riceverebbe un set separato di istruzioni da eseguire localmente, a seconda del ruolo che svolge nel sistema (un dispositivo dell'utente finale, un coordinatore centralizzato, un livello intermedio in un'architettura multilivello, ecc.). È importante essere in grado di ragionare su quali sottoinsiemi di dispositivi eseguono quale codice e dove le diverse parti dei dati potrebbero materializzarsi fisicamente.

Ciò è particolarmente importante quando si tratta, ad esempio, di dati di applicazioni su dispositivi mobili. Poiché i dati sono privati ​​e possono essere sensibili, abbiamo bisogno della capacità di verificare staticamente che questi dati non lasceranno mai il dispositivo (e dimostrare i fatti su come i dati vengono elaborati). Le specifiche di posizionamento sono uno dei meccanismi progettati per supportare questo.

TFF è stato progettato come un ambiente di programmazione incentrato sui dati e, come tale, a differenza di alcuni dei framework esistenti che si concentrano sulle operazioni e su dove potrebbero essere eseguite tali operazioni, TFF si concentra sui dati , dove si materializzano i dati e come vengono trasformati . Di conseguenza, il posizionamento è modellato come una proprietà dei dati in TFF, piuttosto che come una proprietà delle operazioni sui dati. In effetti, come vedrete nella prossima sezione, alcune delle operazioni TFF si estendono su più posizioni e vengono eseguite "in rete", per così dire, anziché essere eseguite da una singola macchina o da un gruppo di macchine.

Rappresentare il tipo di un certo valore come T@G o {T}@G (in opposizione a solo T ) rende esplicite le decisioni sul posizionamento dei dati e, insieme a un'analisi statica dei programmi scritti in TFF, può servire come base per fornire garanzie formali sulla privacy per i dati sensibili sul dispositivo.

Una cosa importante da notare a questo punto, tuttavia, è che mentre incoraggiamo gli utenti TFF ad essere espliciti sui gruppi di dispositivi partecipanti che ospitano i dati (i posizionamenti), il programmatore non tratterà mai i dati grezzi o le identità dei singoli partecipanti .

(Nota: anche se va molto al di fuori dello scopo di questo tutorial, dovremmo menzionare che c'è una notevole eccezione a quanto sopra, un operatore tff.federated_collect che è inteso come una primitiva di basso livello, solo per situazioni specializzate. Il suo uso esplicito in situazioni in cui può essere evitato è sconsigliato, in quanto potrebbe limitare le possibili applicazioni future. Ad esempio, se durante il corso dell'analisi statica, determiniamo che un calcolo utilizza tali meccanismi di basso livello, potremmo non consentire il suo accesso a determinati tipi di dati.)

All'interno del corpo del codice TFF, in base alla progettazione, non c'è modo di enumerare i dispositivi che costituiscono il gruppo rappresentato da tff.CLIENTS , o di sondare l'esistenza di un dispositivo specifico nel gruppo. Non esiste alcun concetto di identità del dispositivo o del client da nessuna parte nell'API Federated Core, nel set sottostante di astrazioni architettoniche o nell'infrastruttura di runtime di base che forniamo per supportare le simulazioni. Tutta la logica di calcolo che scrivi sarà espressa come operazioni sull'intero gruppo di client.

Ricorda qui ciò che abbiamo menzionato in precedenza sui valori dei tipi federati che sono diversi da Python dict , in quanto non si può semplicemente enumerare i loro costituenti membri. Pensa ai valori che la logica del tuo programma TFF manipola come associati a posizionamenti (gruppi), piuttosto che a singoli partecipanti.

I posizionamenti sono progettati per essere un cittadino di prima classe anche in TFF e possono apparire come parametri e risultati di un tipo di placement (per essere rappresentati da tff.PlacementType nell'API). In futuro, prevediamo di fornire una varietà di operatori per trasformare o combinare i posizionamenti, ma questo esula dallo scopo di questo tutorial. Per ora, è sufficiente pensare al placement come un tipo incorporato primitivo opaco in TFF, simile a come int e bool sono tipi incorporati opachi in Python, con tff.CLIENTS è una costante letterale di questo tipo, non diversamente da 1 essendo una costante letterale di tipo int .

Specifica dei posizionamenti

TFF fornisce due letterali di posizionamento di base, tff.CLIENTS e tff.SERVER , per semplificare l'espressione della ricca varietà di scenari pratici che sono naturalmente modellati come architetture client-server, con più dispositivi client (telefoni cellulari, dispositivi embedded, database distribuiti , sensori, ecc.) orchestrato da un unico coordinatore di server centralizzato. TFF è progettato per supportare anche posizionamenti personalizzati, più gruppi di client, architetture distribuite a più livelli e altre più generali, ma discuterne non rientra nell'ambito di questo tutorial.

TFF non prescrive cosa rappresentano effettivamente tff.CLIENTS o tff.SERVER .

In particolare, tff.SERVER può essere un singolo dispositivo fisico (un membro di un gruppo singleton), ma potrebbe anche essere un gruppo di repliche in un cluster a tolleranza di errore che esegue la replica della macchina a stati: non creiamo alcuna architettura speciale ipotesi. Piuttosto, usiamo il bit all_equal menzionato nella sezione precedente per esprimere il fatto che generalmente abbiamo a che fare con un solo elemento di dati sul server.

Allo stesso modo, tff.CLIENTS in alcune applicazioni potrebbe rappresentare tutti i client nel sistema - ciò che nel contesto dell'apprendimento federato a volte chiamiamo popolazione , ma ad esempio, nelle implementazioni di produzione della media federata , può rappresentare una coorte - un sottoinsieme di i clienti selezionati per la partecipazione a un particolare ciclo di formazione. Ai posizionamenti definiti in modo astratto viene dato un significato concreto quando un calcolo in cui compaiono viene distribuito per l'esecuzione (o semplicemente invocato come una funzione Python in un ambiente simulato, come è dimostrato in questo tutorial). Nelle nostre simulazioni locali, il gruppo di client è determinato dai dati federati forniti come input.

Calcoli federati

Dichiarazione di calcoli federati

TFF è progettato come un ambiente di programmazione funzionale fortemente tipizzato che supporta lo sviluppo modulare.

L'unità di composizione di base in TFF è un calcolo federato , una sezione di logica che può accettare valori federati come input e restituire valori federati come output. Ecco come definire un calcolo che calcoli la media delle temperature riportate dall'array di sensori dal nostro esempio precedente.

@tff.federated_computation(tff.FederatedType(tf.float32, tff.CLIENTS))
def get_average_temperature(sensor_readings):
  return tff.federated_mean(sensor_readings)

Guardando il codice sopra, a questo punto potresti chiederti: non ci sono già costrutti di decoratore per definire unità componibili come tf.function in TensorFlow, e se è così, perché introdurne un altro, e in che modo è diverso?

La risposta breve è che il codice generato dal wrapper tff.federated_computation non è TensorFlow, Python: è una specifica di un sistema distribuito in un linguaggio glue interno indipendente dalla piattaforma. A questo punto, questo sembrerà indubbiamente criptico, ma tieni a mente questa interpretazione intuitiva di un calcolo federato come una specifica astratta di un sistema distribuito. Lo spiegheremo tra un minuto.

Per prima cosa, giochiamo un po 'con la definizione. I calcoli TFF sono generalmente modellati come funzioni, con o senza parametri, ma con firme di tipo ben definite. È possibile stampare la firma del tipo di un calcolo interrogando la sua proprietà type_signature , come mostrato di seguito.

str(get_average_temperature.type_signature)
'({float32}@CLIENTS -> float32@SERVER)'

La firma del tipo ci dice che il calcolo accetta una raccolta di diverse letture del sensore sui dispositivi client e restituisce un'unica media sul server.

Prima di andare oltre, riflettiamo su questo per un minuto: l'input e l'output di questo calcolo si trovano in luoghi diversi (su CLIENTS e su SERVER ). Ricorda ciò che abbiamo detto nella sezione precedente sui posizionamenti su come le operazioni TFF possono estendersi su più posizioni ed essere eseguite nella rete , e ciò che abbiamo appena detto sui calcoli federati come rappresentanti delle specifiche astratte dei sistemi distribuiti. Abbiamo solo definito uno di questi calcoli: un semplice sistema distribuito in cui i dati vengono consumati sui dispositivi client e i risultati aggregati emergono sul server.

In molti scenari pratici, i calcoli che rappresentano le attività di primo livello tenderanno ad accettare i loro input e riportare i loro output sul server: questo riflette l'idea che i calcoli potrebbero essere attivati ​​da query che hanno origine e terminano sul server.

Tuttavia, l'API FC non impone questo presupposto e molti degli elementi costitutivi che utilizziamo internamente (inclusi numerosi operatori tff.federated_... che potresti trovare nell'API) hanno input e output con posizionamenti distinti, quindi in generale dovresti non pensare a un calcolo federato come a qualcosa che viene eseguito sul server o viene eseguito da un server . Il server è solo un tipo di partecipante a un calcolo federato. Nel pensare alla meccanica di tali calcoli, è meglio impostare sempre per impostazione predefinita la prospettiva globale della rete, piuttosto che la prospettiva di un singolo coordinatore centralizzato.

In generale, le firme di tipo funzionale sono rappresentate in modo compatto come (T -> U) per i tipi T e U di input e output, rispettivamente. Il tipo di parametro formale (come sensor_readings in questo caso) è specificato come argomento per il decoratore. Non è necessario specificare il tipo di risultato: viene determinato automaticamente.

Sebbene TFF offra forme limitate di polimorfismo, i programmatori sono fortemente incoraggiati a essere espliciti sui tipi di dati con cui lavorano, in quanto ciò semplifica la comprensione, il debug e la verifica formale delle proprietà del codice. In alcuni casi, la specifica esplicita dei tipi è un requisito (ad esempio, i calcoli polimorfici attualmente non sono direttamente eseguibili).

Esecuzione di calcoli federati

Per supportare lo sviluppo e il debug, TFF consente di richiamare direttamente i calcoli definiti in questo modo come funzioni Python, come mostrato di seguito. Laddove il calcolo si aspetta un valore di un tipo federato con il bit all_equal impostato su False , è possibile alimentarlo come un list semplice in Python e per i tipi federati con il bit all_equal impostato su True , è possibile alimentare direttamente il (singolo) costituente membro. Questo è anche il modo in cui ti vengono riportati i risultati.

get_average_temperature([68.5, 70.3, 69.8])
69.53334

Quando si eseguono calcoli come questo in modalità di simulazione, si agisce come un osservatore esterno con una vista a livello di sistema, che ha la capacità di fornire input e consumare output in qualsiasi punto della rete, come in effetti è il caso qui: hai fornito i valori del cliente in input e ha consumato il risultato del server.

Ora, torniamo a una nota che abbiamo fatto in precedenza sul decoratore tff.federated_computation emette codice in un linguaggio glue . Sebbene la logica dei calcoli TFF possa essere espressa come funzioni ordinarie in Python (devi solo decorarle con tff.federated_computation come abbiamo fatto sopra), e puoi invocarle direttamente con argomenti Python proprio come qualsiasi altra funzione Python in questo notebook, dietro le quinte, come abbiamo notato in precedenza, i calcoli TFF in realtà non sono Python.

Ciò che intendiamo con questo è che quando l'interprete Python incontra una funzione decorata con tff.federated_computation , traccia le istruzioni nel corpo di questa funzione una volta (al momento della definizione) e quindi costruisce una rappresentazione serializzata della logica di calcolo per un uso futuro, indipendentemente dal fatto che per l'esecuzione o per essere incorporato come sottocomponente in un altro calcolo.

Puoi verificarlo aggiungendo un'istruzione print, come segue:

@tff.federated_computation(tff.FederatedType(tf.float32, tff.CLIENTS))
def get_average_temperature(sensor_readings):

  print ('Getting traced, the argument is "{}".'.format(
      type(sensor_readings).__name__))

  return tff.federated_mean(sensor_readings)
Getting traced, the argument is "ValueImpl".

Puoi pensare al codice Python che definisce un calcolo federato in modo simile a come penseresti al codice Python che costruisce un grafico TensorFlow in un contesto non desideroso (se non hai familiarità con gli usi non desiderosi di TensorFlow, pensa al tuo Codice Python che definisce un grafico di operazioni da eseguire successivamente, ma non le esegue effettivamente al volo). Il codice di creazione di grafici non desideroso in TensorFlow è Python, ma il grafico TensorFlow costruito da questo codice è indipendente dalla piattaforma e serializzabile.

Allo stesso modo, i calcoli TFF sono definiti in Python, ma le istruzioni Python nei loro corpi, come tff.federated_mean nell'esempio appena mostrato, sono compilate in una rappresentazione serializzabile portatile e indipendente dalla piattaforma sotto il cofano.

Come sviluppatore, non devi preoccuparti dei dettagli di questa rappresentazione, poiché non avrai mai bisogno di lavorarci direttamente, ma dovresti essere consapevole della sua esistenza, del fatto che i calcoli TFF sono fondamentalmente non desiderosi, e non può acquisire lo stato arbitrario di Python. Il codice Python contenuto nel corpo di un calcolo TFF viene eseguito al momento della definizione, quando il corpo della funzione Python decorato con tff.federated_computation viene tracciato prima di essere serializzato. Non viene rintracciato nuovamente al momento dell'invocazione (tranne quando la funzione è polimorfica; fare riferimento alle pagine della documentazione per i dettagli).

Potresti chiederti perché abbiamo scelto di introdurre una rappresentazione interna non Python dedicata. Uno dei motivi è che, in definitiva, i calcoli TFF sono destinati a essere distribuiti in ambienti fisici reali e ospitati su dispositivi mobili o incorporati, dove Python potrebbe non essere disponibile.

Un altro motivo è che i calcoli TFF esprimono il comportamento globale dei sistemi distribuiti, al contrario dei programmi Python che esprimono il comportamento locale dei singoli partecipanti. Puoi vederlo nel semplice esempio sopra, con l'operatore speciale tff.federated_mean che accetta i dati sui dispositivi client, ma deposita i risultati sul server.

L'operatore tff.federated_mean non può essere facilmente modellato come un operatore ordinario in Python, poiché non viene eseguito localmente - come notato in precedenza, rappresenta un sistema distribuito che coordina il comportamento di più partecipanti al sistema. Faremo riferimento a tali operatori come operatori federati , per distinguerli dagli operatori ordinari (locali) in Python.

Il sistema di tipo TFF, e l'insieme fondamentale di operazioni supportate nel linguaggio del TFF, si discosta quindi in modo significativo da quelli in Python, rendendo necessario l'uso di una rappresentazione dedicata.

Composizione di calcoli federati

Come notato sopra, i calcoli federati e i loro componenti sono meglio compresi come modelli di sistemi distribuiti e si può pensare di comporre calcoli federati come comporre sistemi distribuiti più complessi da quelli più semplici. Puoi pensare all'operatore tff.federated_mean come a una sorta di calcolo federato del modello incorporato con una firma del tipo ({T}@CLIENTS -> T@SERVER) (in effetti, proprio come i calcoli che scrivi, anche questo operatore ha un struttura - sotto il cofano lo scomponiamo in operatori più semplici).

Lo stesso vale per la composizione di calcoli federati. Il calcolo get_average_temperature può essere invocato in un corpo di un'altra funzione Python decorata con tff.federated_computation - così facendo sarà incorporato nel corpo del genitore, molto nello stesso modo in cui tff.federated_mean era incorporato nel suo stesso corpo in precedenza.

Un'importante restrizione di cui essere consapevoli è che i corpi delle funzioni Python decorate con tff.federated_computation devono consistere solo di operatori federati, cioè non possono contenere direttamente operazioni TensorFlow. Ad esempio, non è possibile utilizzare direttamente le interfacce tf.nest per aggiungere una coppia di valori federati. Il codice TensorFlow deve essere limitato a blocchi di codice decorati con un tff.tf_computation discusso nella sezione seguente. Solo se racchiuso in questo modo, il codice TensorFlow avvolto può essere richiamato nel corpo di un tff.federated_computation .

Le ragioni di questa separazione sono tecniche (è difficile ingannare operatori come tf.add per lavorare con non-tensori) oltre che architettoniche. Il linguaggio dei calcoli federati (cioè la logica costruita da corpi serializzati di funzioni Python decorate con tff.federated_computation ) è progettato per fungere da linguaggio collante indipendente dalla piattaforma. Questo linguaggio collante è attualmente utilizzato per creare sistemi distribuiti da sezioni incorporate di codice TensorFlow (limitato ai blocchi tff.tf_computation ). Nella pienezza dei tempi, prevediamo la necessità di incorporare sezioni di altra logica non TensorFlow, come query di database relazionali che potrebbero rappresentare pipeline di input, tutte collegate tra loro utilizzando lo stesso linguaggio glue (i blocchi tff.federated_computation ).

Logica TensorFlow

Dichiarazione di calcoli TensorFlow

TFF è progettato per essere utilizzato con TensorFlow. In quanto tale, la maggior parte del codice che scriverai in TFF sarà probabilmente codice TensorFlow ordinario (cioè eseguito localmente). Per poter utilizzare tale codice con TFF, come notato sopra, deve solo essere decorato con tff.tf_computation .

Ad esempio, ecco come implementare una funzione che accetta un numero e aggiunge 0.5 ad esso.

@tff.tf_computation(tf.float32)
def add_half(x):
  return tf.add(x, 0.5)

Ancora una volta, guardando questo, ti tff.tf_computation chiedendo perché dovremmo definire un altro decoratore tff.tf_computation invece di usare semplicemente un meccanismo esistente come tf.function . A differenza della sezione precedente, qui abbiamo a che fare con un normale blocco di codice TensorFlow.

Ci sono alcune ragioni per questo, il cui trattamento completo va oltre lo scopo di questo tutorial, ma vale la pena nominare quello principale:

  • Per incorporare blocchi predefiniti riutilizzabili implementati utilizzando il codice TensorFlow nei corpi dei calcoli federati, è necessario che soddisfino determinate proprietà, come essere tracciati e serializzati al momento della definizione, avere firme di tipo, ecc. Questo generalmente richiede una qualche forma di decoratore.

In generale, si consiglia di utilizzare i meccanismi nativi di TensorFlow per la composizione, come tf.function , laddove possibile, poiché ci si può aspettare che il modo esatto in cui il decoratore di TFF interagisce con le funzioni desiderose si evolva.

Ora, tornando allo snippet di codice di esempio sopra, il calcolo add_half abbiamo appena definito può essere trattato da TFF proprio come qualsiasi altro calcolo TFF. In particolare, ha una firma di tipo TFF.

str(add_half.type_signature)
'(float32 -> float32)'

Tieni presente che questo tipo di firma non ha posizionamenti. I calcoli di TensorFlow non possono utilizzare o restituire tipi federati.

È ora possibile anche utilizzare add_half come blocco add_half in altri calcoli. Ad esempio, ecco come utilizzare l'operatore tff.federated_map per applicare add_half pointwise a tutti i componenti membri di un float federato sui dispositivi client.

@tff.federated_computation(tff.FederatedType(tf.float32, tff.CLIENTS))
def add_half_on_clients(x):
  return tff.federated_map(add_half, x)
str(add_half_on_clients.type_signature)
'({float32}@CLIENTS -> {float32}@CLIENTS)'

Esecuzione di calcoli TensorFlow

L'esecuzione dei calcoli definiti con tff.tf_computation segue le stesse regole descritte per tff.federated_computation . Possono essere invocati come normali chiamabili in Python, come segue.

add_half_on_clients([1.0, 3.0, 2.0])
[<tf.Tensor: shape=(), dtype=float32, numpy=1.5>,
 <tf.Tensor: shape=(), dtype=float32, numpy=3.5>,
 <tf.Tensor: shape=(), dtype=float32, numpy=2.5>]

Ancora una volta, vale la pena notare che invocare il calcolo add_half_on_clients in questo modo simula un processo distribuito. I dati vengono consumati sui client e restituiti sui client. In effetti, questo calcolo prevede che ogni client esegua un'azione locale. Non c'è tff.SERVER esplicitamente menzionato in questo sistema (anche se in pratica, orchestrare tale elaborazione potrebbe coinvolgerne uno). Pensa a un calcolo definito in questo modo come concettualmente analogo alla fase Map in MapReduce .

Inoltre, tieni presente che ciò che abbiamo detto nella sezione precedente sui calcoli TFF che vengono serializzati al momento della definizione rimane vero anche per il codice tff.tf_computation : il corpo Python di add_half_on_clients viene tracciato una volta al momento della definizione. Nelle chiamate successive, TFF usa la sua rappresentazione serializzata.

L'unica differenza tra i metodi Python decorati con tff.federated_computation e quelli decorati con tff.tf_computation è che questi ultimi sono serializzati come grafici TensorFlow (mentre i primi non possono contenere codice TensorFlow direttamente incorporato in essi).

Sotto il cofano, ogni metodo decorato con tff.tf_computation disabilita temporaneamente l'esecuzione desiderosa per consentire la cattura della struttura del calcolo. Sebbene l'esecuzione desiderosa sia disabilitata localmente, puoi usare i costrutti TensorFlow, AutoGraph, TensorFlow 2.0 e così via, purché tu scriva la logica del tuo calcolo in modo tale che possa essere serializzata correttamente.

Ad esempio, il codice seguente avrà esito negativo:

try:

  # Eager mode
  constant_10 = tf.constant(10.)

  @tff.tf_computation(tf.float32)
  def add_ten(x):
    return x + constant_10

except Exception as err:
  print (err)
Attempting to capture an EagerTensor without building a function.

Quanto sopra fallisce perché constant_10 è già stato costruito al di fuori del grafico che tff.tf_computation costruisce internamente nel corpo di add_ten durante il processo di serializzazione.

D'altra parte, invocare funzioni python che modificano il grafico corrente quando vengono chiamate all'interno di un tff.tf_computation va bene:

def get_constant_10():
  return tf.constant(10.)

@tff.tf_computation(tf.float32)
def add_ten(x):
  return x + get_constant_10()

add_ten(5.0)
15.0

Si noti che i meccanismi di serializzazione in TensorFlow si stanno evolvendo e ci aspettiamo che anche i dettagli su come TFF serializza i calcoli si evolvano.

Working with tf.data.Dataset s

As noted earlier, a unique feature of tff.tf_computation s is that they allows you to work with tf.data.Dataset s defined abstractly as formal parameters by your code. Parameters to be represented in TensorFlow as data sets need to be declared using the tff.SequenceType constructor.

For example, the type specification tff.SequenceType(tf.float32) defines an abstract sequence of float elements in TFF. Sequences can contain either tensors, or complex nested structures (we'll see examples of those later). The concise representation of a sequence of T -typed items is T* .

float32_sequence = tff.SequenceType(tf.float32)

str(float32_sequence)
'float32*'

Suppose that in our temperature sensor example, each sensor holds not just one temperature reading, but multiple. Here's how you can define a TFF computation in TensorFlow that calculates the average of temperatures in a single local data set using the tf.data.Dataset.reduce operator.

@tff.tf_computation(tff.SequenceType(tf.float32))
def get_local_temperature_average(local_temperatures):
  sum_and_count = (
      local_temperatures.reduce((0.0, 0), lambda x, y: (x[0] + y, x[1] + 1)))
  return sum_and_count[0] / tf.cast(sum_and_count[1], tf.float32)
str(get_local_temperature_average.type_signature)
'(float32* -> float32)'

In the body of a method decorated with tff.tf_computation , formal parameters of a TFF sequence type are represented simply as objects that behave like tf.data.Dataset , ie, support the same properties and methods (they are currently not implemented as subclasses of that type - this may change as the support for data sets in TensorFlow evolves).

You can easily verify this as follows.

@tff.tf_computation(tff.SequenceType(tf.int32))
def foo(x):
  return x.reduce(np.int32(0), lambda x, y: x + y)

foo([1, 2, 3])
6

Keep in mind that unlike ordinary tf.data.Dataset s, these dataset-like objects are placeholders. They don't contain any elements, since they represent abstract sequence-typed parameters, to be bound to concrete data when used in a concrete context. Support for abstractly-defined placeholder data sets is still somewhat limited at this point, and in the early days of TFF, you may encounter certain restrictions, but we won't need to worry about them in this tutorial (please refer to the documentation pages for details).

When locally executing a computation that accepts a sequence in a simulation mode, such as in this tutorial, you can feed the sequence as Python list, as below (as well as in other ways, eg, as a tf.data.Dataset in eager mode, but for now, we'll keep it simple).

get_local_temperature_average([68.5, 70.3, 69.8])
69.53333

Like all other TFF types, sequences like those defined above can use the tff.StructType constructor to define nested structures. For example, here's how one could declare a computation that accepts a sequence of pairs A , B , and returns the sum of their products. We include the tracing statements in the body of the computation so that you can see how the TFF type signature translates into the dataset's output_types and output_shapes .

@tff.tf_computation(tff.SequenceType(collections.OrderedDict([('A', tf.int32), ('B', tf.int32)])))
def foo(ds):
  print('element_structure = {}'.format(ds.element_spec))
  return ds.reduce(np.int32(0), lambda total, x: total + x['A'] * x['B'])
element_structure = OrderedDict([('A', TensorSpec(shape=(), dtype=tf.int32, name=None)), ('B', TensorSpec(shape=(), dtype=tf.int32, name=None))])

str(foo.type_signature)
'(<A=int32,B=int32>* -> int32)'
foo([{'A': 2, 'B': 3}, {'A': 4, 'B': 5}])
26

The support for using tf.data.Datasets as formal parameters is still somewhat limited and evolving, although functional in simple scenarios such as those used in this tutorial.

Putting it all together

Now, let's try again to use our TensorFlow computation in a federated setting. Suppose we have a group of sensors that each have a local sequence of temperature readings. We can compute the global temperature average by averaging the sensors' local averages as follows.

@tff.federated_computation(
    tff.FederatedType(tff.SequenceType(tf.float32), tff.CLIENTS))
def get_global_temperature_average(sensor_readings):
  return tff.federated_mean(
      tff.federated_map(get_local_temperature_average, sensor_readings))

Note that this isn't a simple average across all local temperature readings from all clients, as that would require weighing contributions from different clients by the number of readings they locally maintain. We leave it as an exercise for the reader to update the above code; the tff.federated_mean operator accepts the weight as an optional second argument (expected to be a federated float).

Also note that the input to get_global_temperature_average now becomes a federated float sequence . Federated sequences is how we will typically represent on-device data in federated learning, with sequence elements typically representing data batches (you will see examples of this shortly).

str(get_global_temperature_average.type_signature)
'({float32*}@CLIENTS -> float32@SERVER)'

Here's how we can locally execute the computation on a sample of data in Python. Notice that the way we supply the input is now as a list of list s. The outer list iterates over the devices in the group represented by tff.CLIENTS , and the inner ones iterate over elements in each device's local sequence.

get_global_temperature_average([[68.0, 70.0], [71.0], [68.0, 72.0, 70.0]])
70.0

This concludes the first part of the tutorial... we encourage you to continue on to the second part .