Voir sur TensorFlow.org | Exécuter dans Google Colab | Voir la source sur GitHub | Télécharger le cahier |
Ce tutoriel est la deuxième partie d'une série de deux parties qui montre comment implémenter des types personnalisés d'algorithmes fédérés dans TFF en utilisant le fédéré de tff.learning
base (FC) , qui sert de base pour l' apprentissage fédéré (FL) couche ( tff.learning
) .
Nous vous encourageons à lire d' abord la première partie de cette série , qui introduisent quelques - uns des concepts clés et des abstractions de programmation utilisés ici.
Cette deuxième partie de la série utilise les mécanismes introduits dans la première partie pour implémenter une version simple d'algorithmes de formation et d'évaluation fédérés.
Nous vous invitons à consulter la classification d'image et génération de texte tutoriels pour un niveau plus élevé et plus doux pour l' introduction Federated API d' apprentissage de TFF, car ils vous aideront à mettre les concepts que nous décrivons ici dans son contexte.
Avant de commencer
Avant de commencer, 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
# Must use the Python context because it
# supports tff.sequence_* intrinsics.
executor_factory = tff.framework.local_executor_factory(
support_sequence_ops=True)
execution_context = tff.framework.ExecutionContext(
executor_fn=executor_factory)
tff.framework.set_default_context(execution_context)
@tff.federated_computation
def hello_world():
return 'Hello, World!'
hello_world()
b'Hello, World!'
Implémentation de la moyenne fédérée
Comme dans Federated l' tff.simulation
apprentissage de classification d'images , nous allons utiliser l'exemple de MNIST, mais étant donné que cela est conçu comme un tutoriel bas niveau, nous allons contourner l'API Keras et tff.simulation
, écrire du code modèle brut, et la construction d' une ensemble de données fédéré à partir de zéro.
Préparation des ensembles de données fédérées
Dans un souci de démonstration, nous allons simuler un scénario dans lequel nous avons les données de 10 utilisateurs, et chacun des utilisateurs contribue à savoir comment reconnaître un chiffre différent. Ceci est à peu près aussi non IID qu'il obtient.
Tout d'abord, chargeons les données MNIST standard :
mnist_train, mnist_test = tf.keras.datasets.mnist.load_data()
[(x.dtype, x.shape) for x in mnist_train]
[(dtype('uint8'), (60000, 28, 28)), (dtype('uint8'), (60000,))]
Les données se présentent sous forme de tableaux Numpy, un avec des images et un autre avec des étiquettes numériques, les deux avec la première dimension passant par les exemples individuels. Écrivons une fonction d'assistance qui la formate d'une manière compatible avec la façon dont nous alimentons les séquences fédérées dans les calculs TFF, c'est-à-dire sous la forme d'une liste de listes - la liste externe couvrant les utilisateurs (chiffres), les internes couvrant des lots de données dans la séquence de chaque client. Comme d'habitude, nous structurons chaque lot comme une paire de tenseurs nommés x
et y
, chacun avec la dimension des lots principaux. Dans la foulée, nous allons également aplatir chaque image dans un vecteur 784-élément et redimensionnez les pixels dans la 0..1
gamme, afin que nous ne devons pas encombrer la logique du modèle avec des conversions de données.
NUM_EXAMPLES_PER_USER = 1000
BATCH_SIZE = 100
def get_data_for_digit(source, digit):
output_sequence = []
all_samples = [i for i, d in enumerate(source[1]) if d == digit]
for i in range(0, min(len(all_samples), NUM_EXAMPLES_PER_USER), BATCH_SIZE):
batch_samples = all_samples[i:i + BATCH_SIZE]
output_sequence.append({
'x':
np.array([source[0][i].flatten() / 255.0 for i in batch_samples],
dtype=np.float32),
'y':
np.array([source[1][i] for i in batch_samples], dtype=np.int32)
})
return output_sequence
federated_train_data = [get_data_for_digit(mnist_train, d) for d in range(10)]
federated_test_data = [get_data_for_digit(mnist_test, d) for d in range(10)]
Comme un contrôle de santé mentale rapide, le regard let au Y
tenseur dans le dernier lot de données fournies par le cinquième client (celui correspondant au chiffre 5
).
federated_train_data[5][-1]['y']
array([5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5], dtype=int32)
Pour être sûr, regardons également l'image correspondant au dernier élément de ce lot.
from matplotlib import pyplot as plt
plt.imshow(federated_train_data[5][-1]['x'][-1].reshape(28, 28), cmap='gray')
plt.grid(False)
plt.show()
Sur la combinaison de TensorFlow et TFF
Dans ce tutoriel, pour la compacité nous décorons immédiatement les fonctions qui introduisent une logique tensorflow avec tff.tf_computation
. Cependant, pour une logique plus complexe, ce n'est pas le modèle que nous recommandons. Le débogage de TensorFlow peut déjà être un défi, et le débogage de TensorFlow après qu'il a été entièrement sérialisé puis réimporté perd nécessairement certaines métadonnées et limite l'interactivité, ce qui rend le débogage encore plus difficile.
Par conséquent, il est fortement recommandé d' écrire une logique complexe TF en tant que fonctions Python autonome (qui est, sans tff.tf_computation
décoration). De cette façon , la logique tensorflow peut être développé et testé en utilisant les meilleures pratiques et les outils TF (comme le mode avide), avant sérialisation le calcul de TFF (par exemple, en invoquant tff.tf_computation
avec une fonction Python comme argument).
Définir une fonction de perte
Maintenant que nous avons les données, définissons une fonction de perte que nous pouvons utiliser pour l'entraînement. Tout d'abord, définissons le type d'entrée comme un tuple nommé TFF. Étant donné que la taille des lots de données peut varier, nous avons mis la dimension de lot None
pour indiquer que la taille de cette dimension est inconnue.
BATCH_SPEC = collections.OrderedDict(
x=tf.TensorSpec(shape=[None, 784], dtype=tf.float32),
y=tf.TensorSpec(shape=[None], dtype=tf.int32))
BATCH_TYPE = tff.to_type(BATCH_SPEC)
str(BATCH_TYPE)
'<x=float32[?,784],y=int32[?]>'
Vous vous demandez peut-être pourquoi nous ne pouvons pas simplement définir un type Python ordinaire. Rappelons la discussion dans la partie 1 , où nous avons expliqué que si nous pouvons exprimer la logique des calculs TFF en utilisant Python, sous les calculs de TFF hotte ne sont pas Python. Le symbole BATCH_TYPE
défini ci - dessus représente une spécification de type TFF abstraite. Il est important de distinguer ce type de TFF abstraite de béton types de représentation Python, par exemple, des conteneurs tels que dict
ou collections.namedtuple
qui peuvent être utilisés pour représenter le type de TFF dans le corps d'une fonction Python. Contrairement à Python, TFF a un seul constructeur de type abstrait tff.StructType
pour tuple comme des conteneurs, avec des éléments qui peuvent être individuellement nommés ou rester sans nom. Ce type est également utilisé pour modéliser les paramètres formels des calculs, car les calculs TFF ne peuvent formellement déclarer qu'un paramètre et un résultat - vous en verrez des exemples sous peu.
Définissons maintenant le type de TFF des paramètres du modèle, encore une fois comme TFF nommé tuple de poids et parti pris.
MODEL_SPEC = collections.OrderedDict(
weights=tf.TensorSpec(shape=[784, 10], dtype=tf.float32),
bias=tf.TensorSpec(shape=[10], dtype=tf.float32))
MODEL_TYPE = tff.to_type(MODEL_SPEC)
print(MODEL_TYPE)
<weights=float32[784,10],bias=float32[10]>
Avec ces définitions en place, nous pouvons maintenant définir la perte pour un modèle donné, sur un seul lot. Notez l'utilisation de @tf.function
décorateur intérieur du @tff.tf_computation
décorateur. Cela nous permet d'écrire TF en utilisant Python comme la sémantique , même si l' intérieur d' un été tf.Graph
contexte créé par le tff.tf_computation
décorateur.
# NOTE: `forward_pass` is defined separately from `batch_loss` so that it can
# be later called from within another tf.function. Necessary because a
# @tf.function decorated method cannot invoke a @tff.tf_computation.
@tf.function
def forward_pass(model, batch):
predicted_y = tf.nn.softmax(
tf.matmul(batch['x'], model['weights']) + model['bias'])
return -tf.reduce_mean(
tf.reduce_sum(
tf.one_hot(batch['y'], 10) * tf.math.log(predicted_y), axis=[1]))
@tff.tf_computation(MODEL_TYPE, BATCH_TYPE)
def batch_loss(model, batch):
return forward_pass(model, batch)
Comme prévu, le calcul batch_loss
rendement float32
la perte étant donné le modèle et un seul lot de données. Notez comment le MODEL_TYPE
et BATCH_TYPE
ont été réunies en une un 2-tuple de paramètres formels; vous pouvez reconnaître le type de batch_loss
comme (<MODEL_TYPE,BATCH_TYPE> -> float32)
.
str(batch_loss.type_signature)
'(<model=<weights=float32[784,10],bias=float32[10]>,batch=<x=float32[?,784],y=int32[?]>> -> float32)'
Pour vérifier l'intégrité, construisons un modèle initial rempli de zéros et calculons la perte sur le lot de données que nous avons visualisé ci-dessus.
initial_model = collections.OrderedDict(
weights=np.zeros([784, 10], dtype=np.float32),
bias=np.zeros([10], dtype=np.float32))
sample_batch = federated_train_data[5][-1]
batch_loss(initial_model, sample_batch)
2.3025851
Notez que nous alimentons le calcul de TFF avec le modèle initial défini comme un dict
, même si le corps de la fonction Python qui définit consomme les paramètres du modèle comme model['weight']
et le model['bias']
. Les arguments de l'appel à batch_loss
ne sont pas simplement transmis au corps de cette fonction.
Qu'est - ce qui se passe quand nous invoquons batch_loss
? Le corps Python de batch_loss
a déjà été tracé et publié en feuilleton dans la cellule ci - dessus où elle a été définie. TFF agit comme l'appelant à batch_loss
au moment de la définition de calcul, et comme la cible d'invocation au moment batch_loss
est invoquée. Dans les deux rôles, TFF sert de pont entre le système de types abstraits de TFF et les types de représentation Python. Au moment de l' invocation, TFF acceptera les types de conteneurs Python plus standard ( dict
, list
, tuple
, collections.namedtuple
, etc.) comme des représentations concrètes de tuples TFF abstraites. De plus, bien que, comme indiqué ci-dessus, les calculs TFF n'acceptent formellement qu'un seul paramètre, vous pouvez utiliser la syntaxe d'appel Python familière avec des arguments de position et/ou de mot-clé dans le cas où le type du paramètre est un tuple - cela fonctionne comme prévu.
Descente de gradient sur un seul lot
Maintenant, définissons un calcul qui utilise cette fonction de perte pour effectuer une seule étape de descente de gradient. Notez comment dans la définition de cette fonction, nous utilisons batch_loss
comme sous - composant. Vous pouvez appeler un calcul construit avec tff.tf_computation
l' intérieur du corps d'un autre calcul, mais en général ce n'est pas nécessaire - comme il est indiqué ci - dessus, parce que la sérialisation desserre des informations de débogage, il est souvent préférable pour les calculs plus complexes à écrire et tester toutes les tensorflow sans tff.tf_computation
décorateur.
@tff.tf_computation(MODEL_TYPE, BATCH_TYPE, tf.float32)
def batch_train(initial_model, batch, learning_rate):
# Define a group of model variables and set them to `initial_model`. Must
# be defined outside the @tf.function.
model_vars = collections.OrderedDict([
(name, tf.Variable(name=name, initial_value=value))
for name, value in initial_model.items()
])
optimizer = tf.keras.optimizers.SGD(learning_rate)
@tf.function
def _train_on_batch(model_vars, batch):
# Perform one step of gradient descent using loss from `batch_loss`.
with tf.GradientTape() as tape:
loss = forward_pass(model_vars, batch)
grads = tape.gradient(loss, model_vars)
optimizer.apply_gradients(
zip(tf.nest.flatten(grads), tf.nest.flatten(model_vars)))
return model_vars
return _train_on_batch(model_vars, batch)
str(batch_train.type_signature)
'(<initial_model=<weights=float32[784,10],bias=float32[10]>,batch=<x=float32[?,784],y=int32[?]>,learning_rate=float32> -> <weights=float32[784,10],bias=float32[10]>)'
Lors de l' appel d' une fonction python décoré avec tff.tf_computation
dans le corps d' une autre de ces fonctions, la logique du calcul de la FFT interne est intégré (essentiellement, inline) dans la logique de l'une extérieure. Comme indiqué plus haut, si vous écrivez les calculs, il est probable préférable de la fonction intérieure ( batch_loss
dans ce cas) un Python régulier ou tf.function
plutôt qu'un tff.tf_computation
. Cependant, ici nous illustrons qu'appeler un tff.tf_computation
dans un autre fonctionne essentiellement comme prévu. Cela peut être nécessaire si, par exemple, vous ne disposez pas du code Python définissant batch_loss
, mais seulement sa représentation TFF sérialisé.
Maintenant, appliquons cette fonction plusieurs fois au modèle initial pour voir si la perte diminue.
model = initial_model
losses = []
for _ in range(5):
model = batch_train(model, sample_batch, 0.1)
losses.append(batch_loss(model, sample_batch))
losses
[0.19690023, 0.13176313, 0.10113225, 0.08273812, 0.070301384]
Descente de gradient sur une séquence de données locales
Maintenant, puisque batch_train
semble fonctionner, Écrivons une fonction de formation similaire local_train
qui consume toute la séquence de tous les lots d'un utilisateur au lieu d'un seul lot. Le nouveau calcul devra consommer maintenant tff.SequenceType(BATCH_TYPE)
au lieu de BATCH_TYPE
.
LOCAL_DATA_TYPE = tff.SequenceType(BATCH_TYPE)
@tff.federated_computation(MODEL_TYPE, tf.float32, LOCAL_DATA_TYPE)
def local_train(initial_model, learning_rate, all_batches):
@tff.tf_computation(LOCAL_DATA_TYPE, tf.float32)
def _insert_learning_rate_to_sequence(dataset, learning_rate):
return dataset.map(lambda x: (x, learning_rate))
batches_with_learning_rate = _insert_learning_rate_to_sequence(all_batches, learning_rate)
# Mapping function to apply to each batch.
@tff.federated_computation(MODEL_TYPE, batches_with_learning_rate.type_signature.element)
def batch_fn(model, batch_with_lr):
batch, lr = batch_with_lr
return batch_train(model, batch, lr)
return tff.sequence_reduce(batches_with_learning_rate, initial_model, batch_fn)
str(local_train.type_signature)
'(<initial_model=<weights=float32[784,10],bias=float32[10]>,learning_rate=float32,all_batches=<x=float32[?,784],y=int32[?]>*> -> <weights=float32[784,10],bias=float32[10]>)'
Il y a pas mal de détails enfouis dans cette courte section de code, examinons-les un par un.
Tout d' abord, alors que nous aurions pu mis en œuvre cette logique entièrement en tensorflow, en se fondant sur tf.data.Dataset.reduce
pour traiter la séquence de la même façon dont nous l' avons fait précédemment, nous avons opté cette fois pour exprimer la logique dans la langue de la colle , comme tff.federated_computation
. Nous avons utilisé l'opérateur fédéré tff.sequence_reduce
pour effectuer la réduction.
L'opérateur tff.sequence_reduce
est utilisé de façon similaire à tf.data.Dataset.reduce
. Vous pouvez penser comme essentiellement les mêmes que tf.data.Dataset.reduce
, mais pour une utilisation à l' intérieur des calculs fédérés, qui , comme vous souvenez peut - être, ne peut pas contenir du code tensorflow. Il est un opérateur de matrice avec un paramètre formel 3-tuple qui consiste en une séquence de T
-typed éléments, l'état initial de la réduction (nous l' appelons abstraite à zéro) d' un certain type U
, et l'opérateur de réduction de de type (<U,T> -> U)
qui modifie l'état de la réduction par le traitement d' un seul élément. Le résultat est l'état final de la réduction, après traitement de tous les éléments dans un ordre séquentiel. Dans notre exemple, l'état de la réduction est le modèle entraîné sur un préfixe des données, et les éléments sont des lots de données.
Deuxièmement, notez que nous avons à nouveau utilisé un calcul ( batch_train
) en tant que composant dans un autre ( local_train
), mais pas directement. Nous ne pouvons pas l'utiliser comme opérateur de réduction car il prend un paramètre supplémentaire - le taux d'apprentissage. Pour résoudre ce problème, nous définissons un calcul fédéré intégré batch_fn
qui se lie au local_train
paramètre de learning_rate
dans son corps. Il est permis à un calcul enfant défini de cette manière de capturer un paramètre formel de son parent tant que le calcul enfant n'est pas invoqué en dehors du corps de son parent. Vous pouvez penser à ce modèle comme un équivalent de functools.partial
en Python.
L'implication pratique de la capture learning_rate
de cette façon est, bien sûr, que la même valeur de taux d'apprentissage est utilisé dans tous les lots.
Maintenant, nous allons essayer la fonction de formation locale nouvellement définie sur la totalité de la séquence des données du même utilisateur qui a contribué le lot d'échantillons (chiffre 5
).
locally_trained_model = local_train(initial_model, 0.1, federated_train_data[5])
Cela a-t-il fonctionné ? Pour répondre à cette question, nous devons mettre en œuvre l'évaluation.
Évaluation locale
Voici une façon de mettre en œuvre l'évaluation locale en additionnant les pertes sur tous les lots de données (nous aurions tout aussi bien pu calculer la moyenne ; nous laisserons cela en exercice au lecteur).
@tff.federated_computation(MODEL_TYPE, LOCAL_DATA_TYPE)
def local_eval(model, all_batches):
@tff.tf_computation(MODEL_TYPE, LOCAL_DATA_TYPE)
def _insert_model_to_sequence(model, dataset):
return dataset.map(lambda x: (model, x))
model_plus_data = _insert_model_to_sequence(model, all_batches)
@tff.tf_computation(tf.float32, batch_loss.type_signature.result)
def tff_add(accumulator, arg):
return accumulator + arg
return tff.sequence_reduce(
tff.sequence_map(
batch_loss,
model_plus_data), 0., tff_add)
str(local_eval.type_signature)
'(<model=<weights=float32[784,10],bias=float32[10]>,all_batches=<x=float32[?,784],y=int32[?]>*> -> float32)'
Encore une fois, il y a quelques nouveaux éléments illustrés par ce code, passons en revue un par un.
Premièrement, nous avons utilisé deux nouveaux opérateurs fédérés pour des séquences de traitement: tff.sequence_map
qui prend une fonction de mappage T->U
et une séquence de T
, et émet une suite de U
obtenu en appliquant la fonction de cartographie ponctuelle, et tff.sequence_sum
que ajoute simplement tous les éléments. Ici, nous mappons chaque lot de données à une valeur de perte, puis ajoutons les valeurs de perte résultantes pour calculer la perte totale.
Notez que nous aurions pu à nouveau utilisé tff.sequence_reduce
, mais ce ne serait pas le meilleur choix - le processus de réduction est, par définition, séquentielle, alors que la mise en correspondance et la somme peuvent être calculées en parallèle. Lorsqu'on vous donne le choix, il est préférable de s'en tenir à des opérateurs qui ne contraignent pas les choix d'implémentation, de sorte que lorsque notre calcul TFF est compilé à l'avenir pour être déployé dans un environnement spécifique, on peut tirer pleinement parti de toutes les opportunités potentielles pour un plus rapide , une exécution plus évolutive et plus économe en ressources.
D' autre part, note que , tout comme dans local_train
, la fonction composante nous avons besoin ( batch_loss
) prend plus de paramètres que ce que l'opérateur fédéré ( tff.sequence_map
) attend, donc on doit définir à nouveau une ligne partielle, cette fois en enveloppant directement un lambda
comme tff.federated_computation
. L' utilisation des enveloppes en ligne avec une fonction comme argument est la méthode recommandée pour utiliser tff.tf_computation
pour intégrer tensorflow logique dans TFF.
Voyons maintenant si notre entraînement a fonctionné.
print('initial_model loss =', local_eval(initial_model,
federated_train_data[5]))
print('locally_trained_model loss =',
local_eval(locally_trained_model, federated_train_data[5]))
initial_model loss = 23.025854 locally_trained_model loss = 0.43484688
En effet, la perte a diminué. Mais que se passe-t-il si nous l'évaluons sur les données d'un autre utilisateur ?
print('initial_model loss =', local_eval(initial_model,
federated_train_data[0]))
print('locally_trained_model loss =',
local_eval(locally_trained_model, federated_train_data[0]))
initial_model loss = 23.025854 locally_trained_model loss = 74.50075
Comme prévu, les choses ont empiré. Le modèle a été formé pour reconnaître 5
, et n'a jamais vu 0
. Cela amène la question suivante : comment la formation locale a-t-elle eu un impact sur la qualité du modèle d'un point de vue global ?
Évaluation fédérée
C'est le point de notre voyage où nous revenons enfin aux types fédérés et aux calculs fédérés - le sujet avec lequel nous avons commencé. Voici une paire de définitions de types TFF pour le modèle qui provient du serveur et les données qui restent sur les clients.
SERVER_MODEL_TYPE = tff.type_at_server(MODEL_TYPE)
CLIENT_DATA_TYPE = tff.type_at_clients(LOCAL_DATA_TYPE)
Avec toutes les définitions introduites jusqu'à présent, l'expression de l'évaluation fédérée dans TFF est simple : nous distribuons le modèle aux clients, laissons chaque client invoquer une évaluation locale sur sa partie locale de données, puis nous calculons la perte. Voici une façon d'écrire cela.
@tff.federated_computation(SERVER_MODEL_TYPE, CLIENT_DATA_TYPE)
def federated_eval(model, data):
return tff.federated_mean(
tff.federated_map(local_eval, [tff.federated_broadcast(model), data]))
Nous avons déjà vu des exemples de tff.federated_mean
et tff.federated_map
dans des scénarios plus simples, et au niveau intuitif, ils fonctionnent comme prévu, mais il y a plus dans cette section de code que rencontre l'oeil, donc nous allons passer soigneusement.
La pause de la première, Descendue le laisser chaque client Invoke évaluation locale sur sa partie locale d' une partie des données. Comme vous pouvez rappeler des sections précédentes, local_eval
a une signature de type de la forme (<MODEL_TYPE, LOCAL_DATA_TYPE> -> float32)
.
L'opérateur fédéré tff.federated_map
est un modèle qui accepte en tant que paramètre un 2-tuple qui se compose de la fonction de mappage d'un type T->U
et une valeur de type fédéré de {T}@CLIENTS
( par exemple, avec des constituants membres du même type que le paramètre de la fonction de mappage), et retourne un résultat de type {U}@CLIENTS
.
Puisque nous nourrissez local_eval
en fonction de mappage à appliquer sur une base par client, le second argument doit être d'un type fédéré {<MODEL_TYPE, LOCAL_DATA_TYPE>}@CLIENTS
, soit, dans la nomenclature des sections précédentes, il devrait être un tuple fédéré. Chaque client doit tenir un ensemble complet d'arguments pour local_eval
en tant que consituent membre. , Nous place il nourrir un Python 2 éléments list
. Qu'est-ce qu'il se passe ici?
En effet, ceci est un exemple d'une distribution de type implicite dans TFF, semblable à des moulages de type implicites que vous avez pu rencontrer ailleurs, par exemple, lorsque vous nourrissez un int
à une fonction qui accepte un float
. Le casting implicite est peu utilisé à ce stade, mais nous prévoyons de le rendre plus répandu dans TFF afin de minimiser le passe-partout.
La distribution implicite qui est appliqué dans ce cas est l'équivalence entre les tuples fédérés de la forme {<X,Y>}@Z
et n - uplets de valeurs fédérée <{X}@Z,{Y}@Z>
. Bien que formellement, ces deux sont différentes signatures de type, regardant du point de vue des programmeurs, chaque appareil en Z
possède deux unités de données X
et Y
. Ce qui se passe ici est un peu comme zip
en Python, et en effet, nous offrons un opérateur tff.federated_zip
qui vous permet d'effectuer des conversions telles explicity. Lorsque le tff.federated_map
rencontre un tuple comme second argument, il invoque simplement tff.federated_zip
pour vous.
Compte tenu de ce qui précède, vous devriez maintenant être en mesure de reconnaître l'expression tff.federated_broadcast(model)
comme représentant une valeur de type TFF {MODEL_TYPE}@CLIENTS
et data
comme une valeur de type TFF {LOCAL_DATA_TYPE}@CLIENTS
(ou tout simplement CLIENT_DATA_TYPE
) , les deux se filtré à travers un ensemble implicite tff.federated_zip
pour former le second argument de tff.federated_map
.
L'opérateur tff.federated_broadcast
, comme on pouvait s'y attendre, transfère simplement les données du serveur aux clients.
Voyons maintenant comment notre formation locale a affecté la perte moyenne dans le système.
print('initial_model loss =', federated_eval(initial_model,
federated_train_data))
print('locally_trained_model loss =',
federated_eval(locally_trained_model, federated_train_data))
initial_model loss = 23.025852 locally_trained_model loss = 54.432625
En effet, comme prévu, la perte a augmenté. Afin d'améliorer le modèle pour tous les utilisateurs, nous devrons nous former sur les données de chacun.
Formation fédérée
Le moyen le plus simple de mettre en œuvre une formation fédérée consiste à former localement, puis à faire la moyenne des modèles. Cela utilise les mêmes blocs de construction et modèles que nous avons déjà discutés, comme vous pouvez le voir ci-dessous.
SERVER_FLOAT_TYPE = tff.type_at_server(tf.float32)
@tff.federated_computation(SERVER_MODEL_TYPE, SERVER_FLOAT_TYPE,
CLIENT_DATA_TYPE)
def federated_train(model, learning_rate, data):
return tff.federated_mean(
tff.federated_map(local_train, [
tff.federated_broadcast(model),
tff.federated_broadcast(learning_rate), data
]))
Notez que dans la mise en œuvre complète en vedette de calcul de la moyenne fédérée fourni par tff.learning
, plutôt que la moyenne des modèles, nous préférons deltas modèle moyenne, pour un certain nombre de raisons, par exemple, la possibilité de couper les normes de mise à jour, pour la compression, etc. .
Voyons si l'entraînement fonctionne en exécutant quelques cycles d'entraînement et en comparant la perte moyenne avant et après.
model = initial_model
learning_rate = 0.1
for round_num in range(5):
model = federated_train(model, learning_rate, federated_train_data)
learning_rate = learning_rate * 0.9
loss = federated_eval(model, federated_train_data)
print('round {}, loss={}'.format(round_num, loss))
round 0, loss=21.60552215576172 round 1, loss=20.365678787231445 round 2, loss=19.27480125427246 round 3, loss=18.311111450195312 round 4, loss=17.45725440979004
Pour être complet, courons maintenant également sur les données de test pour confirmer que notre modèle se généralise bien.
print('initial_model test loss =',
federated_eval(initial_model, federated_test_data))
print('trained_model test loss =', federated_eval(model, federated_test_data))
initial_model test loss = 22.795593 trained_model test loss = 17.278767
Ceci conclut notre tutoriel.
Bien sûr, notre exemple simplifié ne reflète pas un certain nombre de choses que vous auriez besoin de faire dans un scénario plus réaliste - par exemple, nous n'avons pas calculé d'autres métriques que la perte. Nous vous encourageons à étudier la mise en tff.learning
œuvre de la moyenne fédérée dans tff.learning
comme un exemple plus complet, et comme un moyen de démontrer certaines des pratiques de codage que nous aimerions encourager.