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
-
tf.random.Generator
usatf.Variable
para manter os estados para algoritmos de RNG. Em TFF, recomenda-se contruct o gerador dentro de umtff.tf_computation
; e é difícil de passar o gerador e seu estado entretff.tf_computation
funções. - o trecho de código anterior também depende da configuração cuidadosa de sementes em geradores. Vamos começar o esperado, mas resultados surpreendentes (determinista
n1==n2
) se usarmostf.random.Generator.from_non_deterministic_state()
em vez disso.
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)]
Uso recomendado: tf.random.stateless_*
com um ajudante
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)]