Génération de bruit aléatoire dans TFF

Ce didacticiel discutera des meilleures pratiques recommandées pour la génération de bruit aléatoire dans TFF. La génération de bruit aléatoire est une composante importante de nombreuses techniques de protection de la vie privée dans les algorithmes d'apprentissage fédéré, par exemple la confidentialité différentielle.

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

Avant que nous commencions

Tout d'abord, assurons-nous que le notebook est connecté à un backend qui a compilé les composants pertinents.

!pip install --quiet --upgrade tensorflow_federated_nightly
!pip install --quiet --upgrade nest_asyncio

import nest_asyncio
nest_asyncio.apply()
import numpy as np
import tensorflow as tf
import tensorflow_federated as tff

Exécutez l'exemple "Hello World" suivant pour vous assurer que l'environnement TFF est correctement configuré. Si cela ne fonctionne pas, s'il vous plaît se référer à l' installation guide pour les instructions.

@tff.federated_computation
def hello_world():
  return 'Hello, World!'

hello_world()
b'Hello, World!'

Bruit aléatoire sur les clients

Le besoin de bruit sur les clients se divise généralement en deux cas : un bruit identique et un bruit iid.

  • Pour le bruit identique, le modèle recommandé est de maintenir une graine sur le serveur, la diffuser aux clients et utiliser les tf.random.stateless fonctions pour générer du bruit.
  • Pour le bruit iid, utilisez un tf.random.Generator initialisé sur le client avec from_non_deterministic_state, conformément à la recommandation de TF d'éviter les fonctions tf.random.<distribution>.

Le comportement du client est différent de celui du serveur (ne souffre pas des pièges évoqués plus loin) car chaque client construira son propre graphe de calcul et initialisera sa propre graine par défaut.

Bruit identique sur les clients

# Set to use 10 clients.
tff.backends.native.set_local_python_execution_context(num_clients=10)

@tff.tf_computation
def noise_from_seed(seed):
  return tf.random.stateless_normal((), seed=seed)

seed_type_at_server = tff.type_at_server(tff.to_type((tf.int64, [2])))

@tff.federated_computation(seed_type_at_server)
def get_random_min_and_max_deterministic(seed):
  # Broadcast seed to all clients.
  seed_on_clients = tff.federated_broadcast(seed)

  # Clients generate noise from seed deterministicly.
  noise_on_clients = tff.federated_map(noise_from_seed, seed_on_clients)

  # Aggregate and return the min and max of the values generated on clients.
  min = tff.aggregators.federated_min(noise_on_clients)
  max = tff.aggregators.federated_max(noise_on_clients)
  return min, max

seed = tf.constant([1, 1], dtype=tf.int64)
min, max = get_random_min_and_max_deterministic(seed)
assert min == max
print(f'Seed: {seed.numpy()}. All clients sampled value {min:8.3f}.')

seed += 1
min, max = get_random_min_and_max_deterministic(seed)
assert min == max
print(f'Seed: {seed.numpy()}. All clients sampled value {min:8.3f}.')
Seed: [1 1]. All clients sampled value    1.665.
Seed: [2 2]. All clients sampled value   -0.219.

Bruit indépendant sur les clients

@tff.tf_computation
def nondeterministic_noise():
  gen = tf.random.Generator.from_non_deterministic_state()
  return gen.normal(())

@tff.federated_computation(seed_type_at_server)
def get_random_min_and_max_nondeterministic(seed):
  noise_on_clients = tff.federated_eval(nondeterministic_noise, tff.CLIENTS)
  min = tff.aggregators.federated_min(noise_on_clients)
  max = tff.aggregators.federated_max(noise_on_clients)
  return min, max

min, max = get_random_min_and_max_nondeterministic(seed)
assert min != max
print(f'Values differ across clients. {min:8.3f},{max:8.3f}.')

new_min, new_max = get_random_min_and_max_nondeterministic(seed)
assert new_min != new_max
assert new_min != min and new_max != max
print(f'Values differ across rounds.  {new_min:8.3f},{new_max:8.3f}.')
Values differ across clients.   -1.810,   1.079.
Values differ across rounds.    -1.205,   0.851.

Bruit aléatoire sur le serveur

Utilisation Découragé: directement à l' aide tf.random.normal

TF1.x comme API tf.random.normal pour la génération de bruit aléatoire sont fortement déconseillé dans TF2 selon le tutoriel aléatoire de génération de bruit dans TF . Surprenant comportement peut se produire lorsque ces API sont utilisés conjointement avec tf.function et tf.random.set_seed . Par exemple, le code suivant générera la même valeur à chaque appel. Ce comportement surprenant est attendu pour TF, et l' explication se trouve dans la documentation de tf.random.set_seed .

tf.random.set_seed(1)

@tf.function
def return_one_noise(_):
  return tf.random.normal([])

n1=return_one_noise(1)
n2=return_one_noise(2) 
assert n1 == n2
print(n1.numpy(), n2.numpy())
0.3052047 0.3052047

Dans TFF, les choses sont légèrement différentes. Si nous terminerons la génération de bruit tff.tf_computation au lieu de tf.function , le bruit aléatoire non-déterministe sera généré. Cependant,, ensemble différent de si nous courons à plusieurs reprises de ce Code (n1, n2) est généré à chaque fois. Il n'y a pas de moyen simple de définir une graine aléatoire globale pour TFF.

tf.random.set_seed(1)

@tff.tf_computation
def return_one_noise(_):
  return tf.random.normal([])

n1=return_one_noise(1)
n2=return_one_noise(2) 
assert n1 != n2
print(n1, n2)
1.3283143 0.45740178

De plus, le bruit déterministe peut être généré dans TFF sans définir explicitement une graine. La fonction return_two_noise dans le code suivant rendement snippet deux valeurs de bruit identiques. Il s'agit d'un comportement attendu car TFF construira un graphique de calcul à l'avance avant l'exécution. Cependant, cela suggère les utilisateurs doivent faire attention à l'utilisation de tf.random.normal dans TFF.

@tff.tf_computation
def tff_return_one_noise():
  return tf.random.normal([])

@tff.federated_computation
def return_two_noise():
  return (tff_return_one_noise(), tff_return_one_noise())

n1, n2=return_two_noise() 
assert n1 == n2
print(n1, n2)
-0.15665223 -0.15665223

Utilisation avec précaution: tf.random.Generator

Nous pouvons utiliser tf.random.Generator comme suggéré dans le tutoriel TF .

@tff.tf_computation
def tff_return_one_noise(i):
  g=tf.random.Generator.from_seed(i)
  @tf.function
  def tf_return_one_noise():
    return g.normal([])
  return tf_return_one_noise()

@tff.federated_computation
def return_two_noise():
  return (tff_return_one_noise(1), tff_return_one_noise(2))

n1, n2 = return_two_noise() 
assert n1 != n2
print(n1, n2)
0.3052047 -0.38260338

Cependant, les utilisateurs peuvent devoir faire attention à son utilisation

En général, TFF préfère les opérations fonctionnelles et nous allons mettre en valeur l'utilisation des tf.random.stateless_* fonctions dans les sections suivantes.

Dans TFF pour l'apprentissage fédéré, nous travaillons souvent avec des structures imbriquées au lieu de scalaires et l'extrait de code précédent peut être naturellement étendu aux structures imbriquées.

@tff.tf_computation
def tff_return_one_noise(i):
  g=tf.random.Generator.from_seed(i)
  weights = [
         tf.ones([2, 2], dtype=tf.float32),
         tf.constant([2], dtype=tf.float32)
     ]
  @tf.function
  def tf_return_one_noise():
    return tf.nest.map_structure(lambda x: g.normal(tf.shape(x)), weights)
  return tf_return_one_noise()

@tff.federated_computation
def return_two_noise():
  return (tff_return_one_noise(1), tff_return_one_noise(2))

n1, n2 = return_two_noise() 
assert n1[1] != n2[1]
print('n1', n1)
print('n2', n2)
n1 [array([[0.3052047 , 0.5671378 ],
       [0.41852272, 0.2326421 ]], dtype=float32), array([1.1675092], dtype=float32)]
n2 [array([[-0.38260338, -0.47804865],
       [-0.5187485 , -1.8471988 ]], dtype=float32), array([-0.77835274], dtype=float32)]

Une recommandation générale dans la FFT est d'utiliser les fonctions tf.random.stateless_* fonctions de génération de bruit aléatoire. Ces fonctions prennent seed (un tenseur de forme [2] ou un tuple de deux tenseurs scalaires) en tant que paramètre d'entrée explicite pour générer un bruit aléatoire. Nous définissons d'abord une classe d'assistance pour maintenir la graine en tant que pseudo-état. L'aide RandomSeedGenerator a des opérateurs fonctionnels dans un mode état en état-out. Il est raisonnable d'utiliser un compteur comme état de pseudo pour tf.random.stateless_* car ces fonctions se bousculent la graine avant de l' utiliser pour faire des bruits générés par les graines statistiquement non corrélées corrélées.

def timestamp_seed():
  # tf.timestamp returns microseconds as decimal places, thus scaling by 1e6.
  return tf.math.cast(tf.timestamp() * 1e6, tf.int64)

class RandomSeedGenerator():

  def initialize(self, seed=None):
    if seed is None:
      return tf.stack([timestamp_seed(), 0])
    else:
      return tf.constant(self.seed, dtype=tf.int64, shape=(2,))

  def next(self, state):
    return state + tf.constant([0, 1], tf.int64)

  def structure_next(self, state, nest_structure):
    "Returns seed in nested structure and the next state seed."
    flat_structure = tf.nest.flatten(nest_structure)
    flat_seeds = [state + tf.constant([0, i], tf.int64) for
                  i in range(len(flat_structure))]
    nest_seeds = tf.nest.pack_sequence_as(nest_structure, flat_seeds)
    return nest_seeds, flat_seeds[-1] + tf.constant([0, 1], tf.int64)

Maintenant , laissez - nous utiliser la classe d'aide et tf.random.stateless_normal pour générer (structure imbriquée de) de bruit aléatoire dans TFF. L'extrait de code suivant ressemble beaucoup à un processus itératif TFF, voir simple_fedavg comme un exemple d'expression algorithme d'apprentissage fédéré comme processus itératif TFF. L'état de graines de pseudo ici pour la génération de bruit aléatoire est tf.Tensor qui peut être facilement transporté dans les fonctions TFF et TF.

@tff.tf_computation
def tff_return_one_noise(seed_state):
  g=RandomSeedGenerator()
  weights = [
         tf.ones([2, 2], dtype=tf.float32),
         tf.constant([2], dtype=tf.float32)
     ]
  @tf.function
  def tf_return_one_noise():
    nest_seeds, updated_state = g.structure_next(seed_state, weights)
    nest_noise = tf.nest.map_structure(lambda x,s: tf.random.stateless_normal(
        shape=tf.shape(x), seed=s), weights, nest_seeds)
    return nest_noise, updated_state
  return tf_return_one_noise()

@tff.tf_computation
def tff_init_state():
  g=RandomSeedGenerator()
  return g.initialize()

@tff.federated_computation
def return_two_noise():
  seed_state = tff_init_state()
  n1, seed_state = tff_return_one_noise(seed_state)
  n2, seed_state = tff_return_one_noise(seed_state)
  return (n1, n2)

n1, n2 = return_two_noise() 
assert n1[1] != n2[1]
print('n1', n1)
print('n2', n2)
n1 [array([[-0.21598858, -0.30700883],
       [ 0.7562299 , -0.21218438]], dtype=float32), array([-1.0359321], dtype=float32)]
n2 [array([[ 1.0722181 ,  0.81287116],
       [-0.7140338 ,  0.5896157 ]], dtype=float32), array([0.44190162], dtype=float32)]