Generación de ruido aleatorio en TFF

Este tutorial discutirá las mejores prácticas recomendadas para la generación de ruido aleatorio en TFF. La generación de ruido aleatorio es un componente importante de muchas técnicas de protección de la privacidad en los algoritmos de aprendizaje federados, por ejemplo, la privacidad diferencial.

Ver en TensorFlow.org Ejecutar en Google Colab Ver fuente en GitHub Descargar cuaderno

Antes de que comencemos

Primero, asegurémonos de que el portátil esté conectado a un backend que tenga compilados los componentes relevantes.

!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

Ejecute el siguiente ejemplo de "Hello World" para asegurarse de que el entorno TFF esté configurado correctamente. Si esto no funciona, por favor refiérase a la instalación de guía para obtener instrucciones.

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

hello_world()
b'Hello, World!'

Ruido aleatorio en los clientes

La necesidad de ruido en los clientes generalmente se divide en dos casos: ruido idéntico y ruido iid.

  • Para el ruido idéntico, el patrón recomendado es mantener una semilla en el servidor, transmitido a los clientes, y utilizar los tf.random.stateless funciones para generar ruido.
  • Para ruido iid, use un tf.random.Generator inicializado en el cliente con from_non_deterministic_state, de acuerdo con la recomendación de TF de evitar las funciones tf.random. <distribution>.

El comportamiento del cliente es diferente al del servidor (no sufre los inconvenientes que se comentan más adelante) porque cada cliente construirá su propio gráfico de cálculo e inicializará su propia semilla predeterminada.

Ruido idéntico en los clientes

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

Ruido independiente en los clientes

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

Ruido aleatorio en el servidor

Uso desanimado: directamente utilizando tf.random.normal

TF1.x como API tf.random.normal para la generación de ruido aleatorio se desaconseja en TF2 de acuerdo con el tutorial al azar generación de ruido en TF . Sorprendente comportamiento puede ocurrir cuando estas API se utilizan junto con tf.function y tf.random.set_seed . Por ejemplo, el siguiente código generará el mismo valor con cada llamada. Se espera que este comportamiento sorprendente para TF, y la explicación puede encontrarse en la documentación 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

En TFF, las cosas son ligeramente diferentes. Si nos envuelva la generación de ruido como tff.tf_computation en lugar de tf.function , se generará ruido aleatorio no determinista. Sin embargo, si corremos este fragmento de código en múltiples ocasiones, un conjunto diferente de (n1, n2) se generará cada vez. No hay una manera fácil de establecer una semilla aleatoria global para 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

Además, se puede generar ruido determinista en TFF sin establecer explícitamente una semilla. La función return_two_noise en el siguiente fragmento de código devuelve dos valores de ruido idénticas. Este es el comportamiento esperado porque TFF construirá un gráfico de cálculo por adelantado antes de la ejecución. Sin embargo, esto sugiere los usuarios tienen que prestar atención en el uso de tf.random.normal en 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 cuidado: tf.random.Generator

Podemos utilizar tf.random.Generator como se sugiere en el tutorial de 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

Sin embargo, es posible que los usuarios deban tener cuidado con su uso.

En general, TFF prefiere operaciones funcionales y vamos a mostrar el uso de tf.random.stateless_* funciones en las siguientes secciones.

En TFF para el aprendizaje federado, a menudo trabajamos con estructuras anidadas en lugar de escalares y el fragmento de código anterior se puede extender naturalmente a estructuras anidadas.

@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 recomendación general en TFF es utilizar los funcionales tf.random.stateless_* funciones para la generación de ruido aleatorio. Estas funciones toman seed (un tensor con forma de [2] o una tuple de dos tensores escalares) como un argumento de entrada explícita para generar ruido aleatorio. Primero definimos una clase auxiliar para mantener la semilla como pseudoestado. El ayudante RandomSeedGenerator tiene operadores funcionales de una forma de estado en estado de espera. Es razonable utilizar un contador como pseudo estado de tf.random.stateless_* ya que estas funciones revuelven la semilla antes de usarlo para hacer ruidos generados por las semillas correlacionados estadísticamente correlacionados.

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)

Ahora vamos a utilizar la clase de ayuda y tf.random.stateless_normal para generar (estructura anidada de) el ruido aleatorio en la TFF. El siguiente fragmento de código se parece mucho a un proceso iterativo TFF, consulte simple_fedavg como un ejemplo de expresar algoritmo de aprendizaje federados como proceso iterativo TFF. El estado de pseudo semilla aquí para la generación de ruido aleatorio es tf.Tensor que puede ser fácilmente transportado en funciones TFF y 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)]