การสร้างสัญญาณรบกวนแบบสุ่มใน TFF

บทช่วยสอนนี้จะกล่าวถึงแนวทางปฏิบัติที่ดีที่สุดสำหรับการสร้างสัญญาณรบกวนแบบสุ่มใน TFF การสร้างเสียงรบกวนแบบสุ่มเป็นองค์ประกอบที่สำคัญของเทคนิคการปกป้องความเป็นส่วนตัวจำนวนมากในอัลกอริธึมการเรียนรู้แบบรวมศูนย์ เช่น ความเป็นส่วนตัวที่แตกต่างกัน

ก่อนที่เราจะเริ่มต้น

อันดับแรก ให้เราตรวจสอบให้แน่ใจว่าโน้ตบุ๊กเชื่อมต่อกับแบ็กเอนด์ที่มีการรวบรวมส่วนประกอบที่เกี่ยวข้อง

!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!'

เสียงสุ่มบนไคลเอนต์

ความต้องการเสียงรบกวนจากลูกค้าโดยทั่วไปแบ่งออกเป็นสองกรณี: เสียงที่เหมือนกันและเสียงรบกวน

  • สำหรับเสียงเหมือนรูปแบบที่แนะนำคือการรักษาเมล็ดพันธุ์บนเซิร์ฟเวอร์ออกอากาศให้กับลูกค้าและใช้ 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 พฤติกรรมที่น่าแปลกใจที่อาจเกิดขึ้นเมื่อ API เหล่านี้ถูกนำมาใช้ร่วมกับ 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 ไม่กำหนดสุ่มเสียงจะถูกสร้างขึ้น แต่ถ้าเราเรียกใช้รหัสนี้ snippet หลายครั้งชุดที่แตกต่างกัน (n1, n2) จะถูกสร้างขึ้นในแต่ละครั้ง ไม่มีวิธีง่ายๆ ในการตั้งค่า Global Random Seed สำหรับ 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.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

อย่างไรก็ตาม ผู้ใช้อาจจะต้องระวังในการใช้งาน

  • tf.random.Generator ใช้ tf.Variable เพื่อรักษารัฐสำหรับกลไก RNG ในฉิบหายก็จะแนะนำให้ contruct กำเนิดภายใน tff.tf_computation ; และมันเป็นเรื่องยากที่จะผ่านเครื่องกำเนิดไฟฟ้าและรัฐระหว่าง tff.tf_computation ฟังก์ชั่น
  • ข้อมูลโค้ดก่อนหน้านี้ยังต้องอาศัยการตั้งค่าเมล็ดพันธุ์ในเครื่องกำเนิดไฟฟ้าอย่างระมัดระวัง เราคาดว่าจะได้รับ แต่ผลที่น่าแปลกใจ (กำหนด n1==n2 ) ถ้าเราใช้ tf.random.Generator.from_non_deterministic_state() แทน

โดยทั่วไปฉิบหายชอบการดำเนินงานการทำงานและเราจะแสดงการใช้งานของ 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)]

คำแนะนำทั่วไปในฉิบหายคือการใช้การทำงาน tf.random.stateless_* ฟังก์ชั่นสำหรับการสร้างสุ่มเสียง ฟังก์ชั่นเหล่านี้จะนำ seed (เมตริกซ์ที่มีรูปร่าง [2] หรือ tuple สองเทนเซอร์เกลา) เป็นอาร์กิวเมนต์การป้อนข้อมูลที่ชัดเจนในการสร้างสุ่มเสียง ก่อนอื่นเรากำหนดคลาสตัวช่วยเพื่อรักษาเมล็ดพันธุ์เป็นสถานะเทียม ผู้ช่วย RandomSeedGenerator มีผู้ประกอบการในการทำงานของรัฐในรัฐออกแฟชั่น มันมีเหตุผลที่จะใช้ตัวนับเป็นรัฐหลอกสำหรับ tf.random.stateless_* ฟังก์ชั่นเหล่านี้ แย่ง เมล็ดพันธุ์ก่อนที่จะใช้มันเพื่อให้เสียงที่สร้างขึ้นโดยการเพาะเมล็ดมีลักษณะร่วมกัน uncorrelated สถิติ

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 การสร้าง (โครงสร้างที่ซ้อนกัน) ความสุ่มเสียงในฉิบหาย โค้ดต่อไปนี้ลักษณะเป็นจำนวนมากเช่นกระบวนการซ้ำฉิบหายให้ดู simple_fedavg เป็นตัวอย่างของการแสดงขั้นตอนวิธีการเรียนรู้แบบ federated เป็นกระบวนการซ้ำฉิบหาย รัฐเมล็ดหลอกนี่สำหรับรุ่นที่สุ่มเสียงเป็น tf.Tensor ที่สามารถเคลื่อนย้ายได้อย่างง่ายดายในฉิบหายและฟังก์ชั่น 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)]