ייצור רעש אקראי ב- TFF

מדריך זה ידון בשיטות העבודה המומלצות ליצירת רעש אקראי ב-TFF. יצירת רעש אקראי הוא מרכיב חשוב בטכניקות רבות להגנת הפרטיות באלגוריתמי למידה מאוחדים, למשל, פרטיות דיפרנציאלית.

הצג באתר TensorFlow.org הפעל בגוגל קולאב צפה במקור ב-GitHub הורד מחברת

לפני שנתחיל

ראשית, הבה נוודא שהמחברת מחוברת ל-backend שבו הרכיבים הרלוונטיים מורכבים.

!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

הפעל את הדוגמה הבאה של "Hello World" כדי לוודא שסביבת TFF מוגדרת כהלכה. אם זה לא עובד, אנא פנה אל התקנת המדריך לקבלת הוראות.

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

hello_world()
b'Hello, World!'

רעש אקראי על לקוחות

הצורך ברעש על לקוחות מתחלק בדרך כלל לשני מקרים: רעש זהה ורעש iid.

  • עבור רעש זהה, הדפוס המומלץ הוא לשמור זרע בשרת, לשדר אותה ללקוחות, ולהשתמש tf.random.stateless הפונקציות לייצר רעש.
  • עבור רעש iid, השתמש ב-tf.random.Generator המאוחל בלקוח עם from_non_deterministic_state, בהתאם להמלצה של TF להימנע מפונקציות tf.random.<distribution>.

התנהגות הלקוח שונה מהשרת (לא סובל מהמלכודות שנדונו בהמשך) מכיוון שכל לקוח יבנה גרף חישוב משלו ויאתחל את ברירת המחדל שלו.

רעש זהה על לקוחות

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

רעש עצמאי על לקוחות

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

רעש אקראי בשרת

שימוש מחיפוש: באמצעות ישירות tf.random.normal

TF1.x כמו APIs tf.random.normal עבור דור רעש אקראי מעודדים מאוד ב TF2 פי ההדרכה ההדורה רעש האקראי TF . התנהגות מפתיעה עלולה לקרות כאשר APIs אלה משמשים יחד עם tf.function ו tf.random.set_seed . לדוגמה, הקוד הבא יפיק את אותו הערך בכל קריאה. התנהגות מפתיעה זו צפויה עבור TF, והסבר ניתן למצוא תיעוד של 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

ב-TFF, הדברים מעט שונים. אם אנו עוטפים את הדור רעש כמו tff.tf_computation במקום tf.function , רעש אקראי לא דטרמיניסטי יופק. עם זאת, אם אנחנו רצים מספר פעמים קטע הקוד הזה, קבוצה שונה של (n1, n2) יופק בכל פעם. אין דרך קלה להגדיר סיד אקראי גלובלי עבור 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

יתר על כן, ניתן ליצור רעש דטרמיניסטי ב-TFF מבלי להגדיר במפורש זרע. פונקצית return_two_noise בקוד קטע הקוד הבא מחזיר שני ערכי רעש זהים. זוהי התנהגות צפויה מכיוון ש- TFF יבנה גרף חישוב מראש לפני הביצוע. עם זאת, זה מרמז משתמשים צריכים לשים לב על שימוש tf.random.normal ב 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

שימוש בזהירות: tf.random.Generator

אנו יכולים להשתמש tf.random.Generator כפי שהוצעו הדרכת 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

עם זאת, ייתכן שמשתמשים יצטרכו להיות זהירים בשימוש בו

באופן כללי, TFF מעדיף פעולות פונקציונליות ואנחנו נציג את השימוש tf.random.stateless_* פונקציות בסעיפים הבאים.

ב-TFF ללמידה מאוחדת, אנו עובדים לעתים קרובות עם מבנים מקוננים במקום סקלרים וניתן להרחיב באופן טבעי את קטע הקוד הקודם למבנים מקוננים.

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

המלצה כללית TFF היא להשתמש פונקציונאלי tf.random.stateless_* פונקציות עבור דור רעש אקראי. פונקציות אלה לקחת seed (טנזור עם צורה [2] או tuple של שני טנזורים סקלר) כטיעון קלט מפורש לייצר רעש אקראי. תחילה אנו מגדירים מחלקה עוזרת כדי לשמור על הזרע כמצב פסאודו. העוזר RandomSeedGenerator יש מפעילים תפקודיים באופן המדינה ב-המדינה-אאוט. סביר להשתמש נגדי כמו מדינה פסאודו עבור tf.random.stateless_* כמו פונקציות אלה לטרוף את הזרע לפני השימוש בו כדי להשמיע קולות שנוצרו על ידי זרעים בקורלציה סטטיסטית מתואמים.

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)

עכשיו הבה להשתמש בכיתה עוזר ו tf.random.stateless_normal לייצר (מבנה מקונן של) רעש אקראי TFF. קטע הקוד הבא נראה הרבה כמו תהליך איטרטיבי TFF, ראה simple_fedavg כדוגמה להביע אלגוריתם למידה Federated כמו תהליך איטרטיבי TFF. המדינה זרע פסאודו כאן דור רעש אקראי הוא tf.Tensor שניתן להעביר בקלות פונקציות TFF ו 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)]