Algorithmes fédérés personnalisés, partie 2 : Implémentation de la moyenne fédérée

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

png

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.