Algoritmi federati personalizzati, parte 1: Introduzione al nucleo federato

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

Questo tutorial è la prima parte di una serie in due parti che viene illustrato come implementare i tipi personalizzati di algoritmi federati in tensorflow federati (TFF) utilizzando la Federated Nucleo (FC) - un insieme di interfacce di livello inferiore che servono come base su cui abbiamo implementato la Federated Learning (FL) layer.

Questa prima parte è più concettuale; introduciamo alcuni dei concetti chiave e delle astrazioni di programmazione utilizzate in TFF e dimostriamo il loro utilizzo su un esempio molto semplice con una serie distribuita di sensori di temperatura. Nella seconda parte di questa serie , usiamo i meccanismi che introduciamo qui per implementare una semplice versione di algoritmi di addestramento e valutazione federati. Come follow-up, vi incoraggiamo a studiare l'attuazione di una media federata in tff.learning .

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

Anche se questo tutorial è stato progettato per essere autosufficiente, ti invitiamo a primi tutorial di lettura sulla classificazione di immagini e la generazione di testo per un livello più alto e più dolce introduzione al quadro tensorflow federati ed il Federated Learning API ( 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 permette di esprimere in modo compatto la logica del programma che combina tensorflow codice con operatori distribuiti di comunicazione, come ad esempio quelli che vengono utilizzati in Federati della media - calcolo somme distribuite, medie, e altri tipi di aggregazioni distribuite su un insieme di dispositivi client nel sistema, trasmissione di modelli e parametri a tali dispositivi, ecc.

Si può essere a conoscenza di tf.contrib.distribute , e una domanda naturale chiedersi a questo punto può essere: in che modo tale quadro differiscono? Dopotutto, entrambi i framework tentano di distribuire i calcoli di TensorFlow.

Un modo di pensare a questo proposito è che, mentre l'obiettivo dichiarato di tf.contrib.distribute è quello di permettere agli utenti di utilizzare i modelli esistenti e il codice di formazione con modifiche minime per consentire la formazione distribuita, e molto focus è su come trarre vantaggio da un'infrastruttura distribuita per rendere più efficiente il codice di formazione esistente, l'obiettivo del Federated Core di TFF è fornire a ricercatori e professionisti un controllo esplicito sui modelli specifici di comunicazione distribuita che utilizzeranno nei loro sistemi. L'obiettivo in 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 principali destinatari dell'API FC di TFF è costituito da ricercatori e professionisti che potrebbero voler sperimentare nuovi algoritmi di apprendimento federato e valutare le conseguenze di sottili scelte progettuali che influenzano il modo in cui è orchestrato il flusso di dati nel sistema distribuito, ma senza impantanarsi nei dettagli di implementazione del sistema. Il livello di astrazione a cui mira FC API corrisponde approssimativamente 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-punto individuali.

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 in cui 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, 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
@tff.federated_computation
def hello_world():
  return 'Hello, World!'

hello_world()
b'Hello, World!'

Dati federati

Una delle caratteristiche distintive della TFF è che permette di esprimere in modo compatto calcoli tensorflow basati su dati federati. Saremo utilizzando i dati federati termine in questo tutorial per fare riferimento a un insieme di elementi di dati ospitati in 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.

Dati federati come quelle negli esempi precedenti sono trattate TFF come cittadini di prima classe , cioè, possono apparire come parametri ei risultati delle funzioni, e hanno tipi. Per rafforzare questo concetto, si farà riferimento a insiemi di dati federati come valori federati, o come valori di tipi federate.

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 si potrebbe definire TFF il tipo di un galleggiante federata ospitato da un gruppo di dispositivi client. Una raccolta di letture di temperatura che si materializzano in una serie di sensori distribuiti potrebbe essere modellata come un valore di questo tipo federato.

federated_float_on_clients = tff.type_at_clients(tf.float32)

Più in generale, un tipo federata in TFF è definito specificando il tipo T dei suoi costituenti utente - gli elementi di dati che risiedono su singoli dispositivi, e il gruppo G di dispositivi su cui sono ospitati valori federati di questo tipo (più un terzo, informazione facoltativa di cui parleremo tra poco). Ci riferiamo al gruppo G di dispositivi che ospitano un valore federata come il posizionamento del valore. Così, tff.CLIENTS è un esempio di un collocamento.

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

Un tipo federata con costituenti utente T e posizionamento G può essere rappresentata in modo compatto come {T}@G , come illustrato di seguito.

str(federated_float_on_clients)
'{float32}@CLIENTS'

Le parentesi graffe {} in questa concisa notazione servono a ricordare che i costituenti membri (articoli di dati su dispositivi diversi) possono essere diversi, come ci si aspetterebbe per esempio, di letture del sensore di temperatura, in modo che i clienti come un gruppo stanno ospitando congiuntamente un più -creare di T elementi che costituiscono insieme il valore federata -typed.

È importante notare che i costituenti utente di un valore federata sono generalmente opachi al programmatore, ovvero un valore federata non dovrebbero essere considerati come un semplice dict calettato da un identificatore di un dispositivo nel sistema - questi valori sono destinati a trasformare collettivamente soltanto da operatori federata che rappresentano astrattamente vari tipi di protocolli di comunicazione distribuiti (come aggregazione). Se questo suona troppo astratto, non preoccuparti: torneremo su questo argomento tra poco e lo illustreremo con esempi concreti.

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

federated_float_on_clients.all_equal
False

Un tipo federata con un posizionamento G in cui tutto il T -typed costituenti utente sono noti per essere uguale può essere compattamente rappresentata come T@G (al contrario di {T}@G , cioè con le parentesi sceso a riflettere il fatto che l'insieme multiplo di elementi costitutivi è costituito da un unico elemento).

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

Un esempio di un valore federato di questo tipo che potrebbe presentarsi 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 machine learning pre-addestrato sul server, che sono stati poi trasmessi a un gruppo di dispositivi client, dove possono essere personalizzati per ciascun 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 angolari <> nella stringa tipo stampata sono una notazione TFF compatto per tuple denominati o anonimi.

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

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

Si noti che stiamo specificando solo dtype s sopra. Sono supportati anche i tipi non scalari. Nel codice sopra, tf.float32 è una notazione scorciatoia per la 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.type_at_clients(
    simple_regression_model_type, all_equal=True))
'<a=float32,b=float32>@CLIENTS'

Per simmetria con galleggiante federata sopra, si farà riferimento a tale tipo di come una tupla federata. Più in generale, noi spesso usiamo la XYZ federata termine per riferirsi a un valore federata in cui i costituenti membri sono XYZ -come. Così, parleremo di cose come tuple federati, sequenze federati, modelli federati, e così via.

Ora, tornando al float32@CLIENTS - Mentre sembra replicato su più dispositivi, in realtà è una sola float32 , dal momento che tutti gli Stati sono gli stessi. In generale, si può pensare di qualsiasi tipo federata tutto uguale, cioè, una delle forme T@G , come isomorfo ad un tipo non federata T , poiché in entrambi i casi, c'è in realtà una sola (anche se potenzialmente replicato) articolo di tipo T .

Data l'isomorfismo tra T e T@G , ci si potrebbe chiedere quale scopo, se del caso, gli ultimi tipi potrebbero servire. Continuare a leggere.

posizionamenti

Panoramica del design

Nella sezione precedente, abbiamo introdotto il concetto di collocamento - gruppi di partecipanti di sistema che potrebbero essere congiuntamente che ospitano un valore federata, e abbiamo dimostrato l'uso di tff.CLIENTS come specifica esempio di un collocamento.

Per spiegare il motivo per cui la nozione di posizionamento è così fondamentale che avevamo bisogno di incorporarlo nel sistema tipo TFF, ricordiamo quello che abbiamo accennato all'inizio di questo tutorial su alcuni usi previsti TFF.

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

Ciò è particolarmente importante quando si ha a che fare, ad esempio, con i dati delle applicazioni sui 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 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 data-centric, e come tale, a differenza di alcuni dei quadri esistenti che si concentrano sulla gestione e in cui tali operazioni possono funzionare, TFF si concentra su dati, in cui che si materializza di dati e come si sta trasformando. Di conseguenza, il posizionamento è modellato come una proprietà dei dati in TFF, piuttosto che come una proprietà delle operazioni sui dati. In effetti, come vedrai nella prossima sezione, alcune delle operazioni TFF si estendono su più posizioni e vengono eseguite "nella rete", per così dire, piuttosto che essere eseguite da una singola macchina o da un gruppo di macchine.

Che rappresenta il tipo di un certo valore come T@G o {T}@G (piuttosto che semplicemente T ) prende decisioni di collocamento dei dati esplicito, e insieme ad un'analisi statica di programmi scritti in TFF, può servire come base per la fornitura di garanzie formali sulla privacy per i dati sensibili sul dispositivo.

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

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

Ricordiamo qui ciò che abbiamo detto in precedenza sui valori di tipi federati che sono a differenza di Python dict , nel senso che non si può semplicemente elencare i loro elettori membri. Pensa ai valori che la logica del tuo programma TFF manipola come associati ai posizionamenti (gruppi), piuttosto che ai singoli partecipanti.

Posizionamenti sono progettati per essere cittadini di prima classe TFF pure, e possono apparire come parametri ei risultati di un placement tipo (da rappresentati dalla tff.PlacementType nella API). In futuro, prevediamo di fornire una varietà di operatori per trasformare o combinare i posizionamenti, ma questo non rientra nell'ambito di questo tutorial. Per ora, basti pensare placement come opaca primitivo tipo incorporato in TFF, simile a come int e bool sono opachi tipi predefiniti in Python, con tff.CLIENTS essendo una costante letterale di questo tipo, non dissimile 1 essendo un letterale costante di tipo int .

Specificare i posizionamenti

TFF offre due letterali di collocamento di base, tff.CLIENTS e tff.SERVER , per rendere più facile per esprimere la ricca varietà di scenari concreti che sono naturalmente modellata come architetture client-server, con più dispositivi client (telefoni cellulari, dispositivi embedded, database distribuiti , sensori, ecc) orchestrati da un unico coordinatore server centralizzato. TFF è progettato per supportare anche posizionamenti personalizzati, più gruppi di client, architetture distribuite a più livelli e altre più generali, ma discuterne esula dall'ambito di questo tutorial.

TFF non prescrive ciò che né le tff.CLIENTS o tff.SERVER in realtà rappresentano.

In particolare, tff.SERVER può essere un singolo dispositivo fisico (un membro di un gruppo singolo), ma potrebbe benissimo essere un gruppo di repliche in una replica di cluster fault-tolerant esecuzione macchina a stati - non facciamo alcuna speciale architettonico ipotesi. Piuttosto, usiamo il all_equal po 'citato nel paragrafo precedente per esprimere il fatto che siamo in genere si tratta di un unico elemento di dati sul server.

Allo stesso modo, tff.CLIENTS in alcune applicazioni potrebbero rappresentare tutti i clienti nel sistema - quello che nel contesto dell'apprendimento federata a volte ci riferiamo come la popolazione, ma ad esempio, in implementazioni di produzione di federati della media , può rappresentare una coorte - un sottoinsieme di i clienti selezionati per la partecipazione a un particolare ciclo di formazione. Ai posizionamenti definiti astrattamente 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 clienti è 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 base della composizione in TFF è un calcolo federata - una sezione di logica che può accettare valori federati come input e restituire valori federati come uscita. Ecco come è possibile definire un calcolo che calcoli la media delle temperature riportate dall'array di sensori del nostro esempio precedente.

@tff.federated_computation(tff.type_at_clients(tf.float32))
def get_average_temperature(sensor_readings):
  return tff.federated_mean(sensor_readings)

Guardando il codice di cui sopra, a questo punto si potrebbe chiedere - non ci sono già decoratore costrutti per definire le unità componibili come tf.function in tensorflow, e se sì, perché introduce un altro ancora, e come è diverso?

La risposta breve è che il codice generato dal tff.federated_computation involucro non è tensorflow, né è Python - è una specifica di un sistema distribuito in una lingua colla indipendente dalla piattaforma interna. 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 in 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 tipo di un calcolo interrogando la sua type_signature proprietà, come illustrato 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 una singola media sul server.

Prima di andare avanti, cerchiamo di riflettere su questo per un minuto - l'ingresso e l'uscita di questo calcolo sono in luoghi diversi (su CLIENTS contro al SERVER ). Ricordiamo quello che abbiamo detto nel paragrafo precedente sui posizionamenti su come le operazioni di TFF possono estendersi su località, ed eseguire nella rete, e ciò che abbiamo appena detto circa calcoli federati come rappresentare le specifiche astratte dei sistemi distribuiti. Ne abbiamo solo uno definito 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 alto livello tenderanno ad accettare i loro ingressi e riferire le loro uscite sul server - questo riflette l'idea che i calcoli potrebbero essere innescati da query che hanno origine e terminano sul server.

Tuttavia, FC API non impone tale assunzione, e molti dei blocchi usiamo internamente (tra cui numerosi tff.federated_... operatori si possono trovare nella API) di ingressi e uscite con posizionamenti distinti, quindi, in generale, si dovrebbe non pensare a un calcolo federata come qualcosa che viene eseguito sul server o viene eseguito da un server. Il server è solo un tipo di partecipante in un calcolo federato. Quando si pensa ai meccanismi di tali calcoli, è meglio utilizzare sempre la prospettiva dell'intera rete globale, piuttosto che la prospettiva di un singolo coordinatore centralizzato.

In generale, le firme di tipo compatto funzionali sono rappresentati come (T -> U) per i tipi T e U di ingressi e uscite, rispettivamente. Il tipo del parametro formale (tali sensor_readings in questo caso) è specificata come argomento 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, poiché ciò facilita la comprensione, il debug e la verifica formale delle proprietà del codice. In alcuni casi, è necessario specificare esplicitamente i tipi (ad esempio, i calcoli polimorfici non sono attualmente eseguibili direttamente).

Esecuzione di calcoli federati

Per supportare lo sviluppo e il debug, TFF consente di invocare direttamente i calcoli definiti in questo modo come funzioni Python, come mostrato di seguito. Qualora il calcolo si aspetta un valore di un tipo federata al all_equal bit impostato a False , è possibile alimentare come una pianura list in Python, e per i tipi federate con la all_equal bit impostato a True , si può solo alimentare direttamente il (singolo) membro costituente. Questo è anche il modo in cui i risultati ti vengono riportati.

get_average_temperature([68.5, 70.3, 69.8])
69.53334

Quando esegui calcoli come questo in modalità di simulazione, agisci come osservatore esterno con una vista a livello di sistema, che ha la capacità di fornire input e consumare output in qualsiasi posizione nella rete, come in effetti qui - hai fornito i valori del cliente in ingresso e consumato il risultato del server.

Ora, il ritorno di Let ad una nota abbiamo fatto in precedenza circa la tff.federated_computation decoratore che emette il codice in un linguaggio di colla. Anche se la logica dei calcoli TFF può essere espresso come funzioni ordinarie in Python (basta per decorarle con tff.federated_computation come abbiamo fatto in precedenza), e si può direttamente invocarli con argomenti Python, proprio come qualsiasi altra funzione Python in questo notebook, dietro le quinte, come abbiamo notato in precedenza, i calcoli TFF non sono in realtà Python.

Cosa intendiamo per questo è che quando l'interprete Python incontra una funzione decorato con tff.federated_computation , traccia il bilancio nel corpo di questa funzione una sola volta (in fase di definizione), e poi costruisce una rappresentazione serializzata della logica del calcolo per un utilizzo futuro - se per l'esecuzione o da incorporare come sottocomponente in un altro calcolo.

Puoi verificarlo aggiungendo una dichiarazione di stampa, come segue:

@tff.federated_computation(tff.type_at_clients(tf.float32))
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 ansioso (se non hai familiarità con gli usi non ansiosi di TensorFlow, pensa al tuo Codice Python che definisce un grafico di operazioni da eseguire in seguito, ma non le esegue effettivamente al volo). Il codice per la creazione di grafici non desideroso in TensorFlow è Python, ma il grafico TensorFlow costruito da questo codice è indipendente dalla piattaforma e serializzabile.

Analogamente, calcoli TFF sono definiti in Python, ma le istruzioni Python nei loro corpi, come tff.federated_mean nell'esempio Weve appena illustrati, vengono compilati 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 ansiosi, e non può catturare lo stato Python arbitrario. Codice Python contenuto nel corpo di un calcolo TFF viene eseguito al momento definizione, quando il corpo della funzione pitone decorato con tff.federated_computation è tracciata prima ottenere serializzato. Non viene ripercorsa di nuovo al momento dell'invocazione (tranne quando la funzione è polimorfica; per i dettagli fare riferimento alle pagine della documentazione).

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

Un'altra ragione è che i calcoli TFF esprimono il comportamento globale dei sistemi distribuiti, al contrario dei programmi Python che esprimono il comportamento locale dei singoli partecipanti. Si può vedere che nel semplice esempio di cui sopra, con lo speciale operatore tff.federated_mean che accetta i dati su dispositivi client, ma i depositi i risultati sul server.

L'operatore tff.federated_mean non può essere facilmente modellato come un operatore ordinario in Python, dal momento che non esegue localmente - come osservato in precedenza, rappresenta un sistema distribuito che coordina il comportamento di più partecipanti al sistema. Si farà riferimento a tali operatori come operatori federati, per distinguerli dai normali operatori (locali) in Python.

Il sistema di tipo TFF, e l'insieme fondamentale delle 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 costituenti sono meglio compresi come modelli di sistemi distribuiti e puoi pensare di comporre calcoli federati come comporre sistemi distribuiti più complessi da quelli più semplici. Si può pensare al tff.federated_mean dell'operatore come una sorta di built-in modello di calcolo federata con una firma di tipo ({T}@CLIENTS -> T@SERVER) (anzi, proprio come i calcoli si scrive, questo operatore ha anche un complesso struttura - sotto il cofano lo scomponiamo in operatori più semplici).

Lo stesso vale per la composizione di calcoli federati. Il calcolo get_average_temperature potrà essere invocato un corpo di un'altra funzione Python decorato con tff.federated_computation - così facendo farà sì che ad essere incorporato nel corpo del genitore, molto nello stesso modo tff.federated_mean è stato incorporato nel proprio corpo in precedenza.

Una restrizione importante considerare è che i corpi di funzioni Python decorati con tff.federated_computation devono essere formate da operatori federati, cioè, non possono contenere direttamente operazioni tensorflow. Ad esempio, non è possibile utilizzare direttamente tf.nest interfacce per aggiungere una coppia di valori federati. Codice tensorflow deve limitarsi ai blocchi di codice decorato con un tff.tf_computation discusso nella sezione seguente. Solo quando avvolto in questo modo può codice tensorflow avvolto essere richiamato nel corpo di un tff.federated_computation .

Le ragioni di questa separazione sono tecnica (è difficile ingannare operatori come tf.add di lavoro con i non tensori) nonché architettonico. Il linguaggio di calcoli federati (cioè, la logica costruita corpi serializzata funzioni Python decorati con tff.federated_computation ) è progettata per servire come linguaggio collante indipendente dalla piattaforma. Questo linguaggio colla è attualmente utilizzato per sistemi di compilazione distribuito da sezioni di codice incorporati tensorflow (confinato in tff.tf_computation blocchi). Nella pienezza del tempo, prevediamo la necessità di incorporare sezioni di altra logica, non tensorflow, quali query database relazionale che potrebbero rappresentare condutture di ingresso, tutti collegati insieme usando la stessa lingua colla (i tff.federated_computation blocchi).

Logica TensorFlow

Dichiarazione dei calcoli TensorFlow

TFF è progettato per l'uso con TensorFlow. Pertanto, 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, ha solo bisogno di essere decorato con tff.tf_computation .

Ad esempio, ecco come potremmo implementare una funzione che prende 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 a questo, ci si potrebbe chiedere perché dovremmo definire un altro decoratore tff.tf_computation anziché semplicemente utilizzando un meccanismo esistente, come tf.function . Diversamente dalla 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. Ciò richiede generalmente una forma di decoratore.

In generale, si consiglia di utilizzare meccanismi nativi tensorflow per la composizione, come tf.function , ove possibile, come il modo esatto in cui si può prevedere interagisce decoratore di TFF con funzioni ansiosi di evolvere.

Ora, tornando al codice di esempio frammento sopra, il calcolo add_half appena definita può essere trattata da TFF come qualsiasi altro calcolo TFF. In particolare, ha una firma di tipo TFF.

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

Nota che questo tipo di firma non ha posizionamenti. I calcoli TensorFlow non possono consumare o restituire tipi federati.

Ora è possibile anche utilizzare add_half come un blocco di costruzione in altri calcoli. Ad esempio, ecco come è possibile utilizzare il tff.federated_map all'operatore di applicare add_half puntuale a tutti i costituenti membri di un galleggiante federata sui dispositivi client.

@tff.federated_computation(tff.type_at_clients(tf.float32))
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

Esecuzione di calcoli definiti con tff.tf_computation segue le stesse regole abbiamo descritto per tff.federated_computation . Possono essere invocati come normali callable 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 invocando la computazione 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 fa eseguire a ciascun client un'azione locale. Non v'è alcun tff.SERVER esplicitamente menzionati in questo sistema (anche se, in pratica, orchestrare tale trattamento potrebbe comportare uno). Pensare ad un calcolo definito questo modo concettualmente analoga alla Map fase MapReduce .

Inoltre, tenete a mente che ciò che abbiamo detto nel paragrafo precedente circa calcoli TFF ottenere serializzato al momento definizione rimane vero per tff.tf_computation codice, come pure - il corpo di Python di add_half_on_clients viene tracciata una volta in fase di definizione. Nelle chiamate successive, TFF utilizza 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 vengano serializzati come grafici tensorflow (mentre la prima non è consentito contenere codice tensorflow direttamente incorporato in essi).

Sotto il cofano, ogni metodo decorato con tff.tf_computation disabilita temporaneamente l'esecuzione ansiosi per permettere la struttura del calcolo da catturare. Anche se l'esecuzione desiderosa è disabilitata localmente, puoi utilizzare i costrutti TensorFlow, AutoGraph, TensorFlow 2.0 e così via, purché tu scriva la logica del tuo calcolo in modo tale che possa essere serializzato 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 non riesce perché constant_10 è già stato costruito all'esterno del grafico che tff.tf_computation costruisce internamente al corpo del add_ten durante il processo di serializzazione.

D'altra parte, invocando funzioni pitone che modificano il grafico corrente quando viene chiamato all'interno di un tff.tf_computation è soddisfacente:

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 si evolvano anche i dettagli di come TFF serializza i calcoli.

Lavorare con tf.data.Dataset s

Come osservato in precedenza, una caratteristica unica di tff.tf_computation s è che ti permette di lavorare con tf.data.Dataset s definito astrattamente come parametri formali di codice. I parametri di essere rappresentati in tensorflow come insiemi di dati devono essere dichiarate utilizzando il tff.SequenceType costruttore.

Ad esempio, la specifica del tipo di tff.SequenceType(tf.float32) definisce una sequenza astratta di elementi galleggianti in TFF. Le sequenze possono contenere tensori o strutture nidificate complesse (ne vedremo esempi più avanti). La rappresentazione sintetica di una sequenza di T articoli -typed è 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.type_at_clients(tff.SequenceType(tf.float32)))
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 .