Algorithmes fédérés personnalisés, Partie 1 : Introduction au noyau fédéré

Voir sur TensorFlow.org Exécuter dans Google Colab Voir la source sur GitHub Télécharger le cahier

Ce tutoriel est la première partie d'une série de deux parties qui montre comment implémenter des types personnalisés d'algorithmes fédérés en tensorflow fédérés (TFF) en utilisant le noyau fédéré (FC) - un ensemble d'interfaces de niveau inférieur qui servent de fondement sur lequel nous avons mis en place l' apprentissage fédéré (FL) couche.

Cette première partie est plus conceptuelle ; nous présentons certains des concepts clés et des abstractions de programmation utilisés dans TFF, et nous démontrons leur utilisation sur un exemple très simple avec un réseau distribué de capteurs de température. Dans la deuxième partie de cette série , nous utilisons les mécanismes que nous introduisons ici pour mettre en œuvre une version simple des algorithmes de formation et d' évaluation fédérées. En tant que suivi, nous vous encourageons à étudier la mise en tff.learning œuvre de la moyenne fédérée dans tff.learning .

À la fin de cette série, vous devriez être en mesure de reconnaître que les applications de Federated Core ne se limitent pas nécessairement à l'apprentissage. Les abstractions de programmation que nous proposons sont assez génériques et pourraient être utilisées, par exemple, pour implémenter des analyses et d'autres types de calculs personnalisés sur des données distribuées.

Bien que ce tutoriel est conçu pour être autonome, nous vous encourageons à les premiers tutoriels de lecture sur la classification d'images et génération de texte pour un niveau plus élevé et l' introduction plus douce au cadre fédéré tensorflow et l' apprentissage fédéré API ( tff.learning ), comme cela vous aidera à mettre les concepts que nous décrivons ici dans leur contexte.

Utilisations prévues

En un mot, fédéré de base (FC) est un environnement de développement qui permet à la logique de programme exprimer de manière compacte que le code de moissonneuses - batteuses avec les opérateurs de communication distribués, tels que ceux qui sont utilisés dans la moyenne fédérée - calcul des sommes distribuées, moyennes, et d' autres types d'agrégations distribuées sur un ensemble d'appareils clients dans le système, en diffusant des modèles et des paramètres vers ces appareils, etc.

Vous connaissez peut - être tf.contrib.distribute , et une question naturelle à poser à ce moment peut - être: de quelles façons ce cadre différent? Les deux frameworks tentent de répartir les calculs TensorFlow, après tout.

Une façon de penser est que, alors que l'objectif déclaré de tf.contrib.distribute est de permettre aux utilisateurs d'utiliser les modèles existants et le code de formation avec un minimum de changements pour permettre la formation distribuée, et beaucoup l' accent est sur la façon de tirer parti des infrastructures distribuées pour rendre le code de formation existant plus efficace, l'objectif du noyau fédéré de TFF est de donner aux chercheurs et aux praticiens un contrôle explicite sur les modèles spécifiques de communication distribuée qu'ils utiliseront dans leurs systèmes. L'objectif de FC est de fournir un langage flexible et extensible pour exprimer des algorithmes de flux de données distribués, plutôt qu'un ensemble concret de capacités de formation distribuées implémentées.

L'un des principaux publics cibles de l'API FC de TFF est constitué des chercheurs et des praticiens qui pourraient vouloir expérimenter de nouveaux algorithmes d'apprentissage fédéré et évaluer les conséquences de choix de conception subtils qui affectent la manière dont le flux de données dans le système distribué est orchestré, mais sans se perdre dans les détails de mise en œuvre du système. Le niveau d'abstraction visé par l'API FC correspond à peu près au pseudocode que l'on pourrait utiliser pour décrire la mécanique d'un algorithme d'apprentissage fédéré dans une publication de recherche - quelles données existent dans le système et comment elles sont transformées, mais sans tomber au niveau de échanges individuels de messages de réseau point à point.

La TFF dans son ensemble cible des scénarios dans lesquels les données sont distribuées et doivent le rester, par exemple pour des raisons de confidentialité, et où la collecte de toutes les données à un emplacement centralisé peut ne pas être une option viable. Cela a des implications sur la mise en œuvre d'algorithmes d'apprentissage automatique qui nécessitent un degré accru de contrôle explicite, par rapport aux scénarios dans lesquels toutes les données peuvent être accumulées dans un emplacement centralisé dans un centre de données.

Avant de commencer

Avant de plonger dans le code, essayez d'exécuter l'exemple "Hello World" suivant pour vous assurer que votre environnement est correctement configuré. Si cela ne fonctionne pas, s'il vous plaît se référer à l' installation guide pour les instructions.

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

Données fédérées

L' une des caractéristiques distinctives de TFF est qu'il vous permet d'exprimer des calculs basés sur compacte tensorflow sur les données fédérées. Nous utiliserons le terme de données fédérées dans ce tutoriel pour faire référence à une collection d'éléments de données hébergées sur un groupe de dispositifs dans un système distribué. Par exemple, les applications exécutées sur des appareils mobiles peuvent collecter des données et les stocker localement, sans téléchargement vers un emplacement centralisé. Ou, un réseau de capteurs distribués peut collecter et stocker des lectures de température à leurs emplacements.

Données fédérées comme celles dans les exemples ci - dessus sont traités TFF comme des citoyens de première classe , à savoir, ils peuvent apparaître comme les paramètres et les résultats des fonctions, et ils ont des types. Pour renforcer cette notion, nous appellerons à des ensembles de données fédérées en tant que valeurs fédérées, ou comme valeurs de types fédérées.

Le point important à comprendre est que nous modélisons l'ensemble de la collection d'éléments de données sur tous les appareils (par exemple, l'ensemble des lectures de température de collection de tous les capteurs d'un réseau distribué) en tant que valeur fédérée unique.

Par exemple, voici comment on pourrait définir le type TFF d'un flotteur fédéré hébergé par un groupe de périphériques clients. Une collection de lectures de température qui se matérialisent sur un réseau de capteurs distribués pourrait être modélisée comme une valeur de ce type fédéré.

federated_float_on_clients = tff.type_at_clients(tf.float32)

Plus généralement, un type fédéré dans la FFT est définie en spécifiant le type T de ses constituants membres - les éléments de données qui résident sur des dispositifs individuels, et le groupe G de dispositifs sur lesquels les valeurs fédérés de ce type sont organisées (plus d' un tiers, information facultative que nous mentionnerons sous peu). Nous appelons le groupe G de dispositifs d' hébergement d' une valeur fédérée comme le placement de la valeur. Ainsi, tff.CLIENTS est un exemple d'un placement.

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

Un type fédéré avec les électeurs membres T et le placement G peut être représenté comme compact {T}@G , comme indiqué ci - dessous.

str(federated_float_on_clients)
'{float32}@CLIENTS'

Les accolades {} dans cette concise notation servent de rappel que les constituants membres (éléments de données sur différents appareils) peuvent varier, comme on peut s'y attendre , par exemple, des lectures de capteurs de température, de sorte que les clients en tant que groupe organisent conjointement un à plusieurs -mettre de T -typed éléments qui constituent ensemble la valeur fédérée.

Il est important de noter que les constituants membres d'une valeur fédérée sont généralement opaques au programmeur, à savoir, une valeur fédérée ne doivent pas être considérés comme une simple dict calée par un identifiant d'un dispositif dans le système - ces valeurs sont destinées à être collectivement transformé uniquement par des opérateurs fédérés qui représentent de façon abstraite différents types de protocoles de communication distribués (tels que l' agrégation). Si cela vous semble trop abstrait, ne vous inquiétez pas, nous y reviendrons sous peu et nous l'illustrons par des exemples concrets.

Les types fédérés dans TFF se présentent sous deux formes : ceux où les constituants membres d'une valeur fédérée peuvent différer (comme on vient de le voir ci-dessus), et ceux où ils sont connus pour être tous égaux. Ceci est contrôlé par le troisième, en option all_equal paramètre dans le tff.FederatedType constructeur (par défaut à False ).

federated_float_on_clients.all_equal
False

Un type fédéré avec un placement G dans lequel tous les T constituants membres -typed sont connus pour être égale peut être compacte représentée comme T@G (par opposition à {T}@G , qui est, avec les accolades chuté à refléter le fait que le multi-ensemble des constituants des membres se compose d'un seul élément).

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

Un exemple d'une valeur fédérée de ce type qui pourrait survenir dans des scénarios pratiques est un hyperparamètre (tel qu'un taux d'apprentissage, une norme d'écrêtage, etc.) qui a été diffusé par un serveur à un groupe d'appareils qui participent à une formation fédérée.

Un autre exemple est un ensemble de paramètres pour un modèle d'apprentissage automatique pré-entraîné sur le serveur, qui ont ensuite été diffusés à un groupe d'appareils clients, où ils peuvent être personnalisés pour chaque utilisateur.

Par exemple, supposons que nous avons une paire de float32 paramètres a et b pour un simple unidimensionnel modèle de régression linéaire. Nous pouvons construire le type (non fédéré) de tels modèles à utiliser dans TFF comme suit. Les équerres <> dans la chaîne de type imprimé est une notation TFF compact pour tuples nommée ou non.

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

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

Notez que nous ne spécifions dtype s ci - dessus. Les types non scalaires sont également pris en charge. Dans le code ci - dessus, tf.float32 est une notation de raccourci pour le plus général tff.TensorType(dtype=tf.float32, shape=[]) .

Lorsque ce modèle est diffusé aux clients, le type de la valeur fédérée résultante peut être représenté comme indiqué ci-dessous.

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

Par symétrie avec flotteur fédéré ci - dessus, nous appellerons à un tel type comme tuple fédérée. De manière plus générale, nous allons souvent utiliser le terme XYZ fédérée pour faire référence à une valeur fédérée dans laquelle les constituants sont membres XYZ -comme. Ainsi, nous allons parler de choses comme tuples fédérées, des séquences fédérées, des modèles fédérés, et ainsi de suite.

Maintenant, pour revenir à float32@CLIENTS - alors qu'il apparaît répliqué sur plusieurs périphériques, il est en fait un seul float32 , puisque tous les membres sont les mêmes. En général, vous pouvez penser à un tout égal de type fédéré, soit un de la forme T@G , comme isomorphe à un type non fédérée T , puisque dans les deux cas, il n'y a en fait qu'un seul (quoique potentiellement répliquées) article Type de T .

Compte tenu de l'isomorphisme entre T et T@G , vous pouvez vous demander dans quel but, le cas échéant, ces derniers types pourraient servir. Continuer à lire.

Emplacements

Aperçu de la conception

Dans la section précédente, nous avons introduit le concept des placements - groupes de participants au système qui pourraient être une valeur d' hébergement conjointement fédérée, et nous avons démontré l'utilisation de tff.CLIENTS comme par exemple la spécification d'un placement.

Pour expliquer pourquoi la notion d'un placement est si fondamental que nous devions intégrer dans le système de type TFF, rappelons ce que nous avons mentionné au début de ce tutoriel sur certaines des utilisations prévues de TFF.

Bien que dans ce didacticiel, vous ne verrez que le code TFF exécuté localement dans un environnement simulé, notre objectif est que TFF permette d'écrire du code que vous pourriez déployer pour l'exécution sur des groupes de périphériques physiques dans un système distribué, incluant potentiellement des périphériques mobiles ou embarqués sous Android. Chacun de ces appareils recevrait un ensemble distinct d'instructions à exécuter localement, en fonction du rôle qu'il joue dans le système (un appareil d'utilisateur final, un coordinateur centralisé, une couche intermédiaire dans une architecture à plusieurs niveaux, etc.). Il est important de savoir quels sous-ensembles d'appareils exécutent quel code et où différentes parties des données peuvent se matérialiser physiquement.

Ceci est particulièrement important lorsqu'il s'agit, par exemple, de données d'application sur des appareils mobiles. Étant donné que les données sont privées et peuvent être sensibles, nous devons pouvoir vérifier de manière statique que ces données ne quitteront jamais l'appareil (et prouver les faits sur la façon dont les données sont traitées). Les spécifications de placement sont l'un des mécanismes conçus pour prendre en charge cela.

TFF a été conçu comme un environnement de programmation centrée sur les données, et en tant que telle, contrairement à certains des cadres existants qui mettent l' accent sur les opérations et où ces opérations pourraient fonctionner, TFF met l' accent sur les données, où que se matérialise de données, et comment il est transformé. Par conséquent, le placement est modélisé comme une propriété des données dans TFF, plutôt que comme une propriété des opérations sur les données. En effet, comme vous êtes sur le point de le voir dans la section suivante, certaines des opérations TFF s'étendent sur plusieurs emplacements et s'exécutent "sur le réseau", pour ainsi dire, plutôt que d'être exécutées par une seule machine ou un groupe de machines.

Représentant le type d'une certaine valeur T@G ou {T}@G (par opposition à juste T ) prend des décisions de placement de données explicites, et avec une analyse statique des programmes écrits en TFF, il peut servir de base à fournir des garanties formelles de confidentialité pour les données sensibles sur l'appareil.

Une chose importante à noter à ce stade, cependant, est que même si nous encourageons les utilisateurs TFF être explicite sur les groupes de périphériques participants qui hébergent les données (les stages), le programmeur ne réglera pas les données brutes ou identités des participants individuels .

(Note: Bien qu'il va bien au- delà de la portée de ce tutoriel, il convient de mentionner qu'il ya une exception notable à ce qui précède, un tff.federated_collect opérateur qui est conçu comme une primitive de bas niveau, que pour les situations particulières Son utilisation explicite. dans les situations où il peut être évité n'est pas recommandé, car cela peut limiter les applications futures possibles. Par exemple, si au cours d'une analyse statique, nous déterminons qu'un calcul utilise des mécanismes de bas niveau, nous pouvons interdire son accès à certains types de données.)

Dans le corps de la FFT du code, par sa conception, il n'y a aucun moyen d'énumérer les dispositifs qui constituent le groupe représenté par tff.CLIENTS , ou à la sonde de l'existence d'un dispositif spécifique dans le groupe. Il n'y a aucun concept d'identité d'appareil ou de client dans l'API Federated Core, l'ensemble sous-jacent d'abstractions architecturales ou l'infrastructure d'exécution principale que nous fournissons pour prendre en charge les simulations. Toute la logique de calcul que vous écrivez sera exprimée sous forme d'opérations sur l'ensemble du groupe client.

Rappelons ici ce que nous l' avons mentionné plus haut sur les valeurs de types fédérées étant la différence Python dict dans la mesure où on ne peut pas simplement énumérer leurs électeurs membres. Pensez aux valeurs que la logique de votre programme TFF manipule comme étant associées à des stages (groupes), plutôt qu'à des participants individuels.

Les placements sont conçus pour être un citoyen de première classe dans TFF ainsi, et peuvent apparaître sous forme de paramètres et les résultats d'un placement de type (d'être représenté par tff.PlacementType dans l'API). À l'avenir, nous prévoyons de fournir une variété d'opérateurs pour transformer ou combiner des emplacements, mais cela sort du cadre de ce didacticiel. Pour l' instant, il suffit de penser le placement comme un type opaque intégré primitif TFF, semblable à la façon int et bool sont opaques types intégrés dans Python, avec tff.CLIENTS étant un littéral constante de ce type, un peu comme 1 étant un littéral constante de type int .

Spécification des emplacements

TFF prévoit deux littéraux de placement de base, tff.CLIENTS et tff.SERVER , pour le rendre facile d'exprimer la grande variété de scénarios pratiques qui sont naturellement modélisés comme des architectures client-serveur, avec plusieurs périphériques clients (téléphones mobiles, systèmes embarqués, bases de données distribuées , capteurs, etc.) orchestrées par un seul coordinateur du serveur centralisé. TFF est conçu pour prendre également en charge les emplacements personnalisés, plusieurs groupes de clients, plusieurs niveaux et d'autres architectures distribuées plus générales, mais leur discussion dépasse le cadre de ce didacticiel.

TFF ne prescrit pas ce que soit les tff.CLIENTS ou tff.SERVER représentent en fait.

En particulier, tff.SERVER peut être un dispositif physique unique (un membre d'un groupe singleton), mais il pourrait tout aussi bien être un groupe de répliques dans une réplication de la machine d' état en cours d' exécution cluster de tolérance aux pannes - nous ne faisons pas d' architecture spéciale hypothèses. Au contraire, nous utilisons le all_equal bit mentionné dans la section précédente pour exprimer le fait que nous sommes généralement face à un seul élément de données au niveau du serveur.

De même, tff.CLIENTS dans certaines applications peuvent représenter tous les clients dans le système - ce qui , dans le contexte de l' apprentissage fédérée nous appelons parfois la population, mais par exemple, dans les implémentations de production de la moyenne fédérée , il peut représenter une cohorte - un sous - ensemble les clients sélectionnés pour participer à un cycle de formation particulier. Les placements définis de manière abstraite reçoivent une signification concrète lorsqu'un calcul dans lequel ils apparaissent est déployé pour exécution (ou simplement invoqué comme une fonction Python dans un environnement simulé, comme le montre ce tutoriel). Dans nos simulations locales, le groupe de clients est déterminé par les données fédérées fournies en entrée.

Calculs fédérés

Déclaration de calculs fédérés

TFF est conçu comme un environnement de programmation fonctionnel fortement typé qui prend en charge le développement modulaire.

L'unité de base de la composition dans la TFF est un calcul fédéré - une section de logique qui peut accepter des valeurs fédérés en entrée et les valeurs de retour fédérés en sortie. Voici comment vous pouvez définir un calcul qui calcule la moyenne des températures signalées par le réseau de capteurs de notre exemple précédent.

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

En regardant le code ci - dessus, à ce stade , vous pourriez demander - ne sont pas déjà là décorateur des constructions pour définir des unités composables telles que tf.function dans tensorflow, et si oui, pourquoi introduire encore un autre, et comment est - il différent?

La réponse courte est que le code généré par le tff.federated_computation wrapper est ni tensorflow, ni Python - c'est une spécification d'un système distribué dans un langage de colle plate-forme indépendante interne. À ce stade, cela semblera sans aucun doute cryptique, mais veuillez garder à l'esprit cette interprétation intuitive d'un calcul fédéré en tant que spécification abstraite d'un système distribué. Nous l'expliquerons dans une minute.

Tout d'abord, jouons un peu avec la définition. Les calculs de TFF sont généralement modélisés sous forme de fonctions - avec ou sans paramètres, mais avec des signatures de type bien définies. Vous pouvez imprimer la signature de type d'un calcul en interrogeant sa type_signature propriété, comme indiqué ci - dessous.

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

La signature de type nous indique que le calcul accepte une collection de différentes lectures de capteurs sur les appareils clients et renvoie une seule moyenne sur le serveur.

Avant d' aller plus loin, nous allons réfléchir à cela pendant une minute - l'entrée et la sortie de ce calcul sont dans des endroits différents (sur CLIENTS par rapport au SERVER ). Rappelons ce que nous avons dit dans la section précédente sur les placements sur la façon dont les opérations de TFF peuvent étendre sur des endroits, et exécuter dans le réseau, et ce que nous venons de dire des calculs fédérés comme représentant les spécifications abstraites des systèmes distribués. Nous avons juste défini un tel calcul - un système distribué simple dans lequel les données sont consommées sur les appareils clients et les résultats agrégés émergent sur le serveur.

Dans de nombreux scénarios pratiques, les calculs qui représentent les tâches de haut niveau auront tendance à accepter leurs entrées et leurs sorties rapport au serveur - cela reflète l'idée que les calculs pourraient être déclenchées par des requêtes en provenance et sur le serveur.

Cependant, l' API FC n'impose pas cette hypothèse, et la plupart des blocs de construction , nous utilisons en interne (y compris de nombreux tff.federated_... opérateurs vous pouvez trouver dans l'API) ont des entrées et des sorties avec des stages distincts, donc en général, vous devriez ne pas penser à un calcul fédéré comme quelque chose qui fonctionne sur le serveur ou est exécuté par un serveur. Le serveur n'est qu'un type de participant dans un calcul fédéré. En pensant à la mécanique de ces calculs, il est préférable de toujours utiliser par défaut la perspective globale du réseau, plutôt que la perspective d'un seul coordinateur centralisé.

En général, les signatures de types fonctionnels sont représentés sous forme compacte (T -> U) pour les types T et U des entrées et des sorties, respectivement. Le type du paramètre formel (tels sensor_readings dans ce cas) est spécifié comme argument pour le décorateur. Vous n'avez pas besoin de spécifier le type de résultat - il est déterminé automatiquement.

Bien que TFF offre des formes limitées de polymorphisme, les programmeurs sont fortement encouragés à être explicites sur les types de données avec lesquels ils travaillent, car cela facilite la compréhension, le débogage et la vérification formelle des propriétés de votre code. Dans certains cas, la spécification explicite des types est une exigence (par exemple, les calculs polymorphes ne sont actuellement pas directement exécutables).

Exécuter des calculs fédérés

Afin de prendre en charge le développement et le débogage, TFF vous permet d'invoquer directement des calculs définis de cette manière en tant que fonctions Python, comme indiqué ci-dessous. Lorsque le calcul prévoit une valeur d'un type fédéré avec le all_equal bit à False , vous pouvez le nourrir comme une plaine list en Python, et pour les types fédérés avec le all_equal bit à True , vous pouvez simplement alimenter directement le (seul) membre constituant. C'est également ainsi que les résultats vous sont rapportés.

get_average_temperature([68.5, 70.3, 69.8])
69.53334

Lorsque vous exécutez des calculs comme celui-ci en mode simulation, vous agissez en tant qu'observateur externe avec une vue à l'échelle du système, qui a la capacité de fournir des entrées et de consommer des sorties à n'importe quel endroit du réseau, comme c'est effectivement le cas ici - vous avez fourni des valeurs client en entrée, et consommé le résultat du serveur.

Maintenant, le retour de let à une note , nous avons fait plus tôt sur le tff.federated_computation décorateur de code émettant dans une langue de colle. Bien que la logique des calculs TFF peut être exprimée comme des fonctions en Python ( il vous suffit de les décorer avec tff.federated_computation comme nous l' avons fait ci - dessus), et vous pouvez appeler directement les arguments avec Python , tout comme toutes les autres fonctions Python dans ce bloc - notes, dans les coulisses, comme nous l' avons noté plus haut, les calculs de la FFT ne sont en réalité pas Python.

Ce que nous entendons par là que lorsque l'interpréteur Python rencontre une fonction décorée avec tff.federated_computation , il trace les déclarations contenues dans ce corps de fonction une fois (au moment de la définition), puis construit une représentation sérialisée de la logique du calcul pour une utilisation future - que ce soit pour exécution, ou à incorporer en tant que sous-composant dans un autre calcul.

Vous pouvez le vérifier en ajoutant une instruction print, comme suit :

@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".

Vous pouvez penser au code Python qui définit un calcul fédéré de la même manière que vous penseriez au code Python qui construit un graphique TensorFlow dans un contexte non enthousiaste (si vous n'êtes pas familiarisé avec les utilisations non enthousiastes de TensorFlow, pensez à votre Code Python définissant un graphique d'opérations à exécuter ultérieurement, mais ne les exécutant pas réellement à la volée). Le code de création de graphe non enthousiaste dans TensorFlow est Python, mais le graphe TensorFlow construit par ce code est indépendant de la plate-forme et sérialisable.

De même, les calculs TFF sont définis en Python, mais les déclarations Python dans leurs corps, tels que tff.federated_mean dans l'exemple WEVE vient de montrer, sont compilés dans une représentation sérialisable portable et plate-forme indépendante sous le capot.

En tant que développeur, vous n'avez pas besoin de vous préoccuper des détails de cette représentation, car vous n'aurez jamais besoin de travailler directement avec elle, mais vous devez être conscient de son existence, du fait que les calculs TFF sont fondamentalement peu enthousiastes, et ne peut pas capturer un état Python arbitraire. Code Python contenu dans le corps d'un calcul de TFF est exécuté au moment de la définition, lorsque le corps de la fonction Python décorée avec tff.federated_computation est tracée avant d' être publié en feuilleton. Elle n'est pas retracée au moment de l'invocation (sauf lorsque la fonction est polymorphe ; veuillez vous référer aux pages de documentation pour plus de détails).

Vous vous demandez peut-être pourquoi nous avons choisi d'introduire une représentation interne non Python dédiée. L'une des raisons est qu'en fin de compte, les calculs TFF sont destinés à être déployables dans des environnements physiques réels et hébergés sur des appareils mobiles ou embarqués, où Python peut ne pas être disponible.

Une autre raison est que les calculs TFF expriment le comportement global des systèmes distribués, par opposition aux programmes Python qui expriment le comportement local des participants individuels. Vous pouvez voir que dans l'exemple simple ci - dessus, avec l'opérateur spécial tff.federated_mean qui accepte les données sur les périphériques clients, mais les dépôts résultats sur le serveur.

L'opérateur tff.federated_mean ne peut pas être facilement modélisé comme un opérateur ordinaire en Python, car il n'exécute pas localement - comme indiqué plus haut, il représente un système distribué qui coordonne le comportement des participants au système multiples. Nous appellerons ces opérateurs que les opérateurs fédérés, pour les distinguer des opérateurs ordinaires (locaux) en Python.

Le système de type TFF, et l'ensemble fondamental d'opérations supportées dans le langage de TFF, s'écarte donc considérablement de ceux de Python, nécessitant l'utilisation d'une représentation dédiée.

Composer des calculs fédérés

Comme indiqué ci-dessus, les calculs fédérés et leurs constituants sont mieux compris comme des modèles de systèmes distribués, et vous pouvez considérer la composition de calculs fédérés comme la composition de systèmes distribués plus complexes à partir de systèmes plus simples. Vous pouvez penser à l' tff.federated_mean opérateur comme une sorte de modèle incorporé calcul fédéré avec une signature de type ({T}@CLIENTS -> T@SERVER) ( en effet, comme les calculs que vous écrivez, cet opérateur a aussi un complexe structure - sous le capot, nous le décomposons en opérateurs plus simples).

Il en est de même pour la composition de calculs fédérés. Le calcul get_average_temperature peut être invoqué dans un corps d' une autre fonction Python décorée avec tff.federated_computation - faisant le fera à incorporer dans le corps du parent, beaucoup de la même manière tff.federated_mean a été intégré dans son propre corps plus tôt.

Une restriction importante à prendre en compte est que les corps de fonctions Python décorées avec tff.federated_computation doivent contenir que des opérateurs fédérés, par exemple, ils ne peuvent pas contenir directement des opérations tensorflow. Par exemple, vous ne pouvez pas utiliser directement tf.nest interfaces pour ajouter une paire de valeurs fédérées. Le code tensorflow doit se limiter à des blocs de code décorés avec un tff.tf_computation discuté dans la section suivante. Seulement lorsqu'il est enroulé de cette manière peut le code tensorflow enveloppé être invoqué dans le corps d'un tff.federated_computation .

Les raisons de cette séparation sont techniques (il est difficile de tromper les opérateurs tels que tf.add à travailler avec les non-tenseurs), ainsi que l' architecture. La langue des calculs fédérés ( par exemple, la logique construite à partir d' organes sérialisée de fonctions python décorées avec tff.federated_computation ) est conçu pour servir comme un langage de colle indépendant de la plateforme. Ce langage de la colle est actuellement utilisé pour construire des systèmes distribués à partir de sections intégrées de code tensorflow (limité à tff.tf_computation blocs). Dans la plénitude des temps, nous nous attendons à la nécessité de sections intégrer d'autres, la logique non-tensorflow, telles que les requêtes de bases de données relationnelles qui pourraient représenter des pipelines d'entrée, tous reliés entre eux en utilisant le même langage de colle (les tff.federated_computation blocs).

Logique TensorFlow

Déclaration des calculs TensorFlow

TFF est conçu pour être utilisé avec TensorFlow. En tant que tel, la majeure partie du code que vous écrirez dans TFF sera probablement du code TensorFlow ordinaire (c'est-à-dire exécuté localement). Pour utiliser ce code avec TFF, comme il est indiqué ci - dessus, il a juste besoin d'être décoré avec tff.tf_computation .

Par exemple, voici comment nous pourrions mettre en œuvre une fonction qui prend un nombre et ajoute 0.5 à elle.

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

Encore une fois, en regardant cela, vous demandez peut - être pourquoi nous devons définir un autre décorateur tff.tf_computation au lieu d'utiliser simplement un mécanisme existant tel que tf.function . Contrairement à la section précédente, nous avons affaire ici à un bloc ordinaire de code TensorFlow.

Il y a plusieurs raisons à cela, dont le traitement complet dépasse le cadre de ce tutoriel, mais il vaut la peine de nommer la principale :

  • Afin d'intégrer des blocs de construction réutilisables implémentés à l'aide du code TensorFlow dans les corps des calculs fédérés, ils doivent satisfaire certaines propriétés, telles que la traçabilité et la sérialisation au moment de la définition, la possession de signatures de type, etc. Cela nécessite généralement une forme de décorateur.

En général, nous vous recommandons d' utiliser des mécanismes natifs de tensorflow pour la composition, comme tf.function , autant que possible, que la manière exacte dont les interagit décorateur de TFF avec des fonctions avides peuvent être appelées à évoluer.

Maintenant, pour revenir à l'exemple de code extrait ci - dessus, le calcul add_half que nous venons de définir peut être traité par TFF comme tout autre calcul de TFF. En particulier, il possède une signature de type TFF.

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

Notez que ce type de signature n'a pas d'emplacements. Les calculs TensorFlow ne peuvent pas consommer ou renvoyer des types fédérés.

Vous pouvez maintenant utiliser add_half comme bloc de construction dans d' autres calculs. Par exemple, voici comment vous pouvez utiliser le tff.federated_map opérateur pour appliquer add_half pointwise à tous les membres constituants d'un flotteur fédéré sur les machines clientes.

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

Exécuter des calculs TensorFlow

Exécution des calculs définis avec tff.tf_computation suit les mêmes règles que celles que nous avons décrites pour tff.federated_computation . Ils peuvent être invoqués comme des callables ordinaires en Python, comme suit.

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

Encore une fois, il convient de noter que l' invocation du calcul add_half_on_clients de cette manière simule un processus distribué. Les données sont consommées sur les clients et renvoyées sur les clients. En effet, ce calcul fait effectuer à chaque client une action locale. Il n'y a pas tff.SERVER mentionné explicitement dans ce système (même si , dans la pratique, orchestrer un tel traitement peut impliquer un). Pensez à un calcul défini ainsi que sur le plan conceptuel analogue à la Map étape MapReduce .

De plus, gardez à l' esprit que ce que nous avons dit dans la section précédente sur les calculs TFF se sérialisés au moment de la définition reste vrai pour tff.tf_computation le code aussi bien - le corps Python de add_half_on_clients se trace une fois au moment de la définition. Lors des appels suivants, TFF utilise sa représentation sérialisée.

La seule différence entre les méthodes Python décorées avec tff.federated_computation et ceux décorés tff.tf_computation est que ces derniers sont sérialisés sous forme de graphiques tensorflow (alors que les premiers ne sont pas autorisés à contenir du code tensorflow directement intégré dans les).

Sous le capot, chaque méthode décorée avec tff.tf_computation désactive temporairement l' exécution désireux afin de permettre à capturer la structure du calcul. Bien que l'exécution rapide soit localement désactivée, vous pouvez utiliser les constructions TensorFlow, AutoGraph, TensorFlow 2.0, etc., tant que vous écrivez la logique de votre calcul de manière à ce qu'elle puisse être correctement sérialisée.

Par exemple, le code suivant échouera :

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.

L'échec ci - dessus parce que constant_10 a déjà été construit à l'extérieur du graphique que tff.tf_computation construit en interne dans le corps du add_ten pendant le processus de sérialisation.

D'autre part, en invoquant les fonctions de python qui modifient le graphique actuel lorsqu'il est appelé dans un tff.tf_computation est très bien:

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

Notez que les mécanismes de sérialisation dans TensorFlow évoluent, et nous nous attendons à ce que les détails de la façon dont TFF sérialise les calculs évoluent également.

Travailler avec 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.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 .