W tym samouczku omówione zostaną zalecane najlepsze praktyki dotyczące generowania losowego szumu w TFF. Generowanie losowego szumu jest ważnym elementem wielu technik ochrony prywatności w algorytmach sfederowanego uczenia się, np. prywatności różnicowej.
Zobacz na TensorFlow.org | Uruchom w Google Colab | Wyświetl źródło na GitHub | Pobierz notatnik |
Zanim zaczniemy
Najpierw upewnijmy się, że notebook jest podłączony do zaplecza, na którym skompilowano odpowiednie komponenty.
!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
Uruchom następujący przykład „Hello World”, aby upewnić się, że środowisko TFF jest poprawnie skonfigurowane. Jeśli to nie działa, proszę odnieść się do montażu prowadnicy do instrukcji.
@tff.federated_computation
def hello_world():
return 'Hello, World!'
hello_world()
b'Hello, World!'
Przypadkowy hałas na klientach
Zapotrzebowanie na hałas na klientach generalnie dzieli się na dwa przypadki: identyczny hałas i iid hałas.
- Na identycznej hałasu, zalecany wzorzec jest utrzymanie nasienie na serwerze, nadawanie jej klientów i użyć
tf.random.stateless
funkcje do generowania hałasu. - W przypadku szumu iid użyj tf.random.Generator zainicjowanego na kliencie z parametrem from_non_deterministic_state, zgodnie z zaleceniem TF, aby uniknąć funkcji tf.random.<distribution>.
Zachowanie klienta różni się od serwera (nie cierpi z powodu omówionych później pułapek), ponieważ każdy klient zbuduje własny wykres obliczeniowy i zainicjuje własne domyślne ziarno.
Identyczny hałas u klientów
# 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.
Niezależny hałas na klientach
@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.
Losowy szum na serwerze
Zniechęcać użycia: bezpośrednio za pomocą tf.random.normal
TF1.x jak API tf.random.normal
do losowego generowania hałasu są zdecydowanie odradzane w TF2 według losowego generowania hałasu w tutorialu TF . Zaskakujące zachowanie może się zdarzyć, gdy te interfejsy API są stosowane razem z tf.function
i tf.random.set_seed
. Na przykład poniższy kod wygeneruje tę samą wartość przy każdym wywołaniu. Oczekuje To zaskakujące zachowanie dla TF i wyjaśnienie można znaleźć w dokumentacji 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
W TFF sprawy mają się nieco inaczej. Jeśli będziemy zawijać generowanie hałasu jak tff.tf_computation
zamiast tf.function
, zostanie wygenerowany niedeterministyczne szum losowy. Jeśli jednak uruchomić ten fragment kodu kilka razy, inny zestaw (n1, n2)
będą generowane za każdym razem. Nie ma łatwego sposobu na ustawienie globalnego losowego seedu dla 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
Co więcej, w TFF można generować szum deterministyczny bez wyraźnego ustawienia zalążka. Funkcja return_two_noise
w poniższym fragmencie kodu zwraca dwa identyczne wartości hałasu. Jest to oczekiwane zachowanie, ponieważ TFF utworzy wykres obliczeń z wyprzedzeniem przed wykonaniem. Jednak ta sugeruje, użytkownicy mają zwrócić uwagę na wykorzystanie tf.random.normal
w 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
Wykorzystanie ostrożnie: tf.random.Generator
Możemy użyć tf.random.Generator
jak zasugerowano w tutorialu 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
Jednak użytkownicy mogą być ostrożni przy jego użyciu
-
tf.random.Generator
wykorzystujetf.Variable
utrzymać stany algorytmów RNG. W TFF, zaleca się contruct generator wewnątrztff.tf_computation
; i trudno jest przejść generatora i jego stan pomiędzytff.tf_computation
funkcji. - poprzedni fragment kodu również opiera się na ostrożnym ustawieniu nasion w generatorach. Będziemy się spodziewać, ale zaskakujące wyniki (deterministyczny
n1==n2
) jeśli używamytf.random.Generator.from_non_deterministic_state()
zamiast.
W ogóle, TFF preferuje operacje funkcjonalne i będziemy prezentować wykorzystanie tf.random.stateless_*
funkcje w następujących sekcjach.
W TFF do nauki sfederowanej często pracujemy ze strukturami zagnieżdżonymi zamiast skalarami, a poprzedni fragment kodu można w naturalny sposób rozszerzyć na struktury zagnieżdżone.
@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)]
Zalecane użycie: tf.random.stateless_*
z pomocnikiem
Ogólne zalecenie w TFF jest wykorzystanie funkcjonalnych tf.random.stateless_*
funkcje do losowego generowania hałasu. Funkcje te mają seed
(tensora w kształcie [2]
lub tuple
dwóch skalarnych tensorów) jako wyraźną wejścia argumentu generuje losowy szum. Najpierw definiujemy klasę pomocniczą, która utrzymuje ziarno jako pseudo stan. Pomocnik RandomSeedGenerator
ma operatorów funkcjonalnych w sposób państwo-w-stanie-out. Uzasadnione jest stosowanie jako stan licznika pseudo dla tf.random.stateless_*
jak te funkcje wyścig ziarno przed użyciem, aby hałasy generowane przez skorelowane statystycznie nieskorelowanych nasion.
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)
Teraz nam użyć klasy pomocnika i tf.random.stateless_normal
wygenerować (zagnieżdżone struktury) szumy w TFF. Poniższy fragment kodu wygląda bardzo podobny proces iteracyjny TFF, patrz simple_fedavg jako przykład wyrażenia stowarzyszonego algorytm uczenia się TFF wieloetapowym procesem. Stan tu ziarno pseudo losowego generowania hałasu jest tf.Tensor
, które mogą być łatwo transportowane w funkcji TFF i 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)]