Generazione di rumore casuale in TFF

Questo tutorial discuterà le migliori pratiche consigliate per la generazione di rumore casuale in TFF. La generazione di rumore casuale è una componente importante di molte tecniche di protezione della privacy negli algoritmi di apprendimento federati, ad esempio la privacy differenziale.

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

Prima di iniziare

Innanzitutto, assicuriamoci che il notebook sia connesso a un backend con i componenti pertinenti compilati.

!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

Eseguire il seguente esempio "Hello World" per assicurarsi che l'ambiente TFF sia configurato correttamente. Se non funziona, si prega di fare riferimento alla installazione guida per le istruzioni.

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

hello_world()
b'Hello, World!'

Rumore casuale sui clienti

La necessità di rumore sui client generalmente si divide in due casi: rumore identico e rumore iid.

  • Per il rumore identici, il modello consigliato è quello di mantenere un seme sul server, trasmetterlo ai clienti, e utilizzare le tf.random.stateless funzioni per generare rumore.
  • Per il rumore iid, utilizzare un tf.random.Generator inizializzato sul client con from_non_deterministic_state, in linea con la raccomandazione di TF di evitare le funzioni tf.random.<distribution>.

Il comportamento del client è diverso dal server (non soffre delle insidie ​​discusse più avanti) perché ogni client costruirà il proprio grafico di calcolo e inizializzerà il proprio seme predefinito.

Identico rumore sui clienti

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

Rumore indipendente sui clienti

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

Rumore casuale sul server

Utilizzo scoraggiato: direttamente usando tf.random.normal

TF1.x come API tf.random.normal per la generazione di rumore casuale sono fortemente sconsigliato in TF2 secondo l' esercitazione generazione di rumore casuale TF . Sorprendente comportamento può accadere quando queste API vengono utilizzati insieme con tf.function e tf.random.set_seed . Ad esempio, il codice seguente genererà lo stesso valore con ogni chiamata. Questo comportamento sorprendente è previsto per TF, e la spiegazione può essere trovata nella documentazione dei 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

In TFF le cose sono leggermente diverse. Se avvolgere la generazione di rumore come tff.tf_computation anziché tf.function , rumore casuale non deterministico verrà generato. Tuttavia, se si esegue questo frammento di codice più volte, insieme diverso di (n1, n2) verrà generato ogni volta. Non esiste un modo semplice per impostare un seed casuale globale per 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

Inoltre, il rumore deterministico può essere generato in TFF senza impostare esplicitamente un seme. La funzione return_two_noise nel seguente frammento di codice restituisce due valori misurati identici. Questo è un comportamento previsto perché TFF creerà il grafico di calcolo in anticipo prima dell'esecuzione. Tuttavia, questo suggerisce gli utenti devono prestare attenzione sull'uso del tf.random.normal in 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

Uso con attenzione: tf.random.Generator

Possiamo usare tf.random.Generator come suggerito nel tutorial di 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

Tuttavia, gli utenti potrebbero dover prestare attenzione al suo utilizzo

In generale, TFF preferisce operazioni funzionali e presenterà l'utilizzo di tf.random.stateless_* funzioni nelle seguenti sezioni.

In TFF per l'apprendimento federato, lavoriamo spesso con strutture nidificate anziché scalari e il frammento di codice precedente può essere naturalmente esteso a strutture nidificate.

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

Una raccomandazione generale TFF è quello di utilizzare i funzionali tf.random.stateless_* funzioni per la generazione di rumore casuale. Queste funzioni prendono seed (a Tensor di forma [2] o una tuple di due tensori scalari) come argomento ingresso esplicito per generare rumore casuale. Per prima cosa definiamo una classe di supporto per mantenere il seme come pseudo stato. L'aiutante RandomSeedGenerator ha operatori funzionali in modo stato-in-stato-out. È ragionevole utilizzare un contatore come pseudo stato di tf.random.stateless_* come queste funzioni scramble seme prima di utilizzarlo per fare rumori generati da semi correlati statisticamente non correlati.

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)

Ora cerchiamo di utilizzare la classe di supporto e tf.random.stateless_normal per generare (struttura annidata di) rumore casuale in TFF. Il seguente frammento di codice assomiglia molto a un processo iterativo TFF, vedere simple_fedavg come esempio di esprimere algoritmo di apprendimento federata come TFF processo iterativo. Lo stato pseudo seme qui per la generazione di rumore casuale è tf.Tensor che può essere facilmente trasportato in funzioni TFF e 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)]