Losowe generowanie szumów w TFF

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

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

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