Zufällige Rauscherzeugung in TFF

In diesem Tutorial werden die empfohlenen Best Practices für die zufällige Rauschgenerierung in TFF erläutert. Die Erzeugung von zufälligem Rauschen ist eine wichtige Komponente vieler Techniken zum Schutz der Privatsphäre in föderierten Lernalgorithmen, z. B. Differential Privacy.

Auf TensorFlow.org ansehen In Google Colab ausführen Quelle auf GitHub anzeigen Notizbuch herunterladen

Bevor wir anfangen

Stellen wir zunächst sicher, dass das Notebook mit einem Backend verbunden ist, das die relevanten Komponenten kompiliert hat.

!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

Führen Sie das folgende "Hello World"-Beispiel aus, um sicherzustellen, dass die TFF-Umgebung richtig eingerichtet ist. Wenn es nicht funktioniert, entnehmen Sie bitte der Installationsanleitung für Anweisungen.

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

hello_world()
b'Hello, World!'

Zufälliges Rauschen bei Clients

Der Bedarf an Lärm bei Clients lässt sich im Allgemeinen in zwei Fälle unterteilen: identischer Lärm und iid-Lärm.

  • Für identischen Lärm, sind die empfohlene Muster einen Samen auf dem Server zu erhalten, ist es für den Kunden übertragen und die Verwendung tf.random.stateless Funktionen Rauschen zu erzeugen.
  • Verwenden Sie für iid-Rauschen einen tf.random.Generator, der auf dem Client mit from_non_deterministic_state initialisiert wurde, entsprechend der Empfehlung von TF, die tf.random.<distribution>-Funktionen zu vermeiden.

Das Client-Verhalten unterscheidet sich vom Server (er leidet nicht unter den später besprochenen Fallstricken), da jeder Client seinen eigenen Berechnungsgraphen erstellt und seinen eigenen Standard-Seed initialisiert.

Identisches Geräusch bei Kunden

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

Unabhängiger Lärm auf Kunden

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

Zufälliges Rauschen auf dem Server

Discouraged Nutzung: direkt mit tf.random.normal

TF1.x wie APIs tf.random.normal für Zufallsrauscherzeugung stark in TF2 entmutigt nach dem Zufallsrauscherzeugung tutorial in TF . Überraschende Verhalten kann auftreten , wenn diese APIs zusammen mit verwendet werden tf.function und tf.random.set_seed . Der folgende Code generiert beispielsweise bei jedem Aufruf denselben Wert. Dieses überraschende Verhalten ist für TF erwartet, und eine Erklärung in der finden Dokumentation von 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

Bei TFF sind die Dinge etwas anders. Wenn wir die Geräuschentwicklung als wickeln tff.tf_computation statt tf.function , nicht-deterministische Zufallsrauschen erzeugt. Wenn wir jedoch in diesem Code - Schnipsel mehrere Male ausgeführt, anderen Satz von (n1, n2) wird jedes Mal erzeugt werden. Es gibt keine einfache Möglichkeit, einen globalen zufälligen Seed für TFF festzulegen.

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

Darüber hinaus kann in TFF deterministisches Rauschen erzeugt werden, ohne dass explizit ein Seed gesetzt wird. Die Funktion return_two_noise in dem folgenden Codeausschnitt gibt zwei identische Rauschwerte. Dies ist das erwartete Verhalten, da TFF vor der Ausführung einen Berechnungsgraphen erstellt. Allerdings schlägt diese Benutzer zu achten auf die Nutzung haben tf.random.normal in 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

Verbrauch mit Vorsicht: tf.random.Generator

Wir können verwenden tf.random.Generator wie in den vorgeschlagenen 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

Benutzer müssen jedoch möglicherweise bei der Verwendung vorsichtig sein

Im Allgemeinen zieht TFF funktionale Operationen und wir werden die Nutzung von präsentieren tf.random.stateless_* Funktionen in den folgenden Abschnitten.

In TFF für föderiertes Lernen arbeiten wir oft mit verschachtelten Strukturen anstelle von Skalaren und der vorherige Codeausschnitt kann natürlich auf verschachtelte Strukturen erweitert werden.

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

Eine allgemeine Empfehlung in TFF ist die Funktions verwenden tf.random.stateless_* Funktionen für zufälliges Rauschen Generation. Diese Funktionen nehmen seed (ein Tensor mit Form [2] oder ein tuple von zwei skalaren Tensoren) als explizites Eingabeargument für zufälliges Rauschen zu erzeugen. Wir definieren zunächst eine Hilfsklasse, um den Seed als Pseudozustand zu verwalten. Der Helfer RandomSeedGenerator hat funktionelle Operatoren in einer state-in-state-out Mode. Es ist vernünftig , einen Zähler als Pseudo - Zustand für die Verwendung tf.random.stateless_* da diese Funktionen verwürfeln das Saatgut , bevor es unter Verwendung von Rauschen , das durch korrelierte Samen statistisch unkorreliert erzeugt zu machen.

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)

Nun wollen wir verwenden , um die Hilfsklasse und tf.random.stateless_normal zu erzeugen (verschachtelte Struktur) zufälliges Rauschen in TFF. Der folgende Codeausschnitt sieht viel wie ein TFF iterativer Prozess finden Sie simple_fedavg als Beispiel für föderierte Lernalgorithmus als TFF iterativen Prozess zum Ausdruck. Der Pseudosamen Zustand hier für Zufallsrauscherzeugung ist tf.Tensor , die leicht in TFF und TF Funktionen transportiert werden kann.

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