Geração de ruído aleatório em TFF

Este tutorial discutirá as melhores práticas recomendadas para geração de ruído aleatório em TFF. A geração de ruído aleatório é um componente importante de muitas técnicas de proteção de privacidade em algoritmos de aprendizado federado, por exemplo, privacidade diferencial.

Ver no TensorFlow.org Executar no Google Colab Ver fonte no GitHub Baixar caderno

Antes de começarmos

Primeiro, certifique-se de que o notebook esteja conectado a um backend que tenha os componentes relevantes compilados.

!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

Execute o seguinte exemplo "Hello World" para certificar-se de que o ambiente TFF está configurado corretamente. Se isso não funcionar, consulte a instalação guia para obter instruções.

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

hello_world()
b'Hello, World!'

Ruído aleatório nos clientes

A necessidade de ruído nos clientes geralmente se enquadra em dois casos: ruído idêntico e ruído iid.

  • Para o ruído idêntico, o padrão recomendado é manter uma semente no servidor, transmiti-lo aos clientes, e usar os tf.random.stateless funções para gerar ruído.
  • Para ruído de iid, use um tf.random.Generator inicializado no cliente com from_non_deterministic_state, de acordo com a recomendação do TF de evitar as funções tf.random.<distribution>.

O comportamento do cliente é diferente do servidor (não sofre das armadilhas discutidas posteriormente) porque cada cliente construirá seu próprio gráfico de computação e inicializará sua própria semente padrão.

Ruído idêntico em 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.

Ruído independente nos 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.

Ruído aleatório no servidor

Uso desencorajado: diretamente usando tf.random.normal

TF1.x como APIs tf.random.normal para a geração de ruído aleatório são fortemente desencorajados na TF2 acordo com o tutorial aleatório geração de ruído no TF . Comportamento surpreendente pode acontecer quando essas APIs são usados em conjunto com tf.function e tf.random.set_seed . Por exemplo, o código a seguir gerará o mesmo valor com cada chamada. Este comportamento surpreendente é esperado para TF, ea explicação pode ser encontrada na documentação 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

No TFF, as coisas são um pouco diferentes. Se enrole a geração de ruído como tff.tf_computation vez de tf.function , ruído aleatório não-determinístico será gerada. No entanto, se executar este trecho de código várias vezes, um conjunto diferente de (n1, n2) será gerado a cada vez. Não há uma maneira fácil de definir uma semente aleatória 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

Além disso, o ruído determinístico pode ser gerado no TFF sem definir explicitamente uma semente. A função return_two_noise no seguinte trecho de código retorna dois valores de ruído idênticos. Esse é um comportamento esperado porque o TFF construirá o gráfico de computação antecipadamente antes da execução. No entanto, isso sugere os usuários têm que prestar atenção sobre o uso de tf.random.normal em 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 com cautela: tf.random.Generator

Podemos usar tf.random.Generator como sugerido na TF tutorial .

@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

No entanto, os usuários podem ter que ter cuidado com seu uso

Em geral, TFF prefere operações funcionais e vamos mostrar o uso de tf.random.stateless_* funções nas seções seguintes.

No TFF para aprendizado federado, geralmente trabalhamos com estruturas aninhadas em vez de escalares e o trecho de código anterior pode ser estendido naturalmente para estruturas aninhadas.

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

A recomendação geral em TFF é usar os funcionais tf.random.stateless_* funções para geração de ruído aleatório. Estas funções tomar seed (um tensor com a forma [2] ou um tuple de dois tensores escalares) como um argumento de entrada explícita para gerar o ruído aleatório. Primeiro definimos uma classe auxiliar para manter a semente como pseudoestado. O auxiliar RandomSeedGenerator tem operadores funcionais de uma forma de estado-in-state-out. É razoável usar um contador de estado pseudo para tf.random.stateless_* como essas funções embaralhar a semente antes de usá-lo para fazer ruídos gerados por sementes correlacionados estatisticamente não correlacionadas.

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)

Agora vamos usar a classe auxiliar e tf.random.stateless_normal para gerar (estrutura aninhada de) ruído aleatório em TFF. O seguinte trecho de código se parece muito com um processo iterativo TFF, ver simple_fedavg como um exemplo de expressar algoritmo de aprendizagem federado como processo iterativo TFF. O estado semente pseudo aqui para geração de ruído aleatório é tf.Tensor que pode ser facilmente transportado em funções 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)]