TFF 中的随机噪声生成

本教程将讨论在 TFF 中生成随机噪声的推荐最佳实践。随机噪声生成是联合学习算法中许多隐私保护技术的关键组成部分,例如差分隐私。

在 TensorFlow.org 上查看 在 Google Colab 中运行 在 GitHub 上查看源代码 下载笔记本

开始之前

首先,让我们确保笔记本连接到已编译相关组件的后端。

pip install --quiet --upgrade tensorflow-federated
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函数生成噪声。
  • 对于独立同分布噪声,请使用在客户端上使用 from_non_deterministic_state 初始化的 tf.random.Generator,这与 TF 的建议一致,即避免使用 tf.random.<distribution> 函数。

客户端行为与服务器不同(不会受到后面讨论的陷阱的影响),因为每个客户端将构建自己的计算图并初始化自己的默认种子。

客户端上的相同噪声

# Set to use 10 clients.
tff.backends.native.set_sync_local_cpp_execution_context(default_num_clients=10)

@tff.tensorflow.computation
def noise_from_seed(seed):
  return tf.random.stateless_normal((), seed=seed)

seed_type_at_server = tff.FederatedType(tff.to_type((np.int64, [2])), tff.SERVER)

@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.federated_min(noise_on_clients)
  max = tff.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.tensorflow.computation
def nondeterministic_noise():
  gen = tf.random.Generator.from_non_deterministic_state()
  return gen.normal(())

@tff.federated_computation
def get_random_min_and_max_nondeterministic():
  noise_on_clients = tff.federated_eval(nondeterministic_noise, tff.CLIENTS)
  min = tff.federated_min(noise_on_clients)
  max = tff.federated_max(noise_on_clients)
  return min, max

min, max = get_random_min_and_max_nondeterministic()
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()
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.490,   1.172.
Values differ across rounds.    -1.358,   1.208.

客户端上的模型初始化器

def _keras_model():
  inputs = tf.keras.Input(shape=(1,))
  outputs = tf.keras.layers.Dense(1)(inputs)
  return tf.keras.Model(inputs=inputs, outputs=outputs)

@tff.tensorflow.computation
def tff_return_model_init():
  model = _keras_model()
  # return the initialized single weight value of the dense layer
  return tf.reshape(
      tff.learning.models.ModelWeights.from_model(model).trainable[0], [-1])[0]

@tff.federated_computation
def get_random_min_and_max_nondeterministic():
  noise_on_clients = tff.federated_eval(tff_return_model_init, tff.CLIENTS)
  min = tff.federated_min(noise_on_clients)
  max = tff.federated_max(noise_on_clients)
  return min, max

min, max = get_random_min_and_max_nondeterministic()
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()
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.022,   1.567.
Values differ across rounds.    -1.675,   1.550.

服务器上的随机噪声

不推荐使用:直接使用tf.random.normal

根据TF 中的随机噪声生成教程,TF2 强烈不建议使用 TF1.x 类似的 API tf.random.normal 来生成随机噪声。当这些 API 与tf.functiontf.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.tensorflow.computation 而不是tf.function,则将生成非确定性随机噪声。但是,如果我们多次运行此代码片段,每次都会生成不同的(n1, n2) 集。没有简单的方法可以为 TFF 设置全局随机种子。

tf.random.set_seed(1)

@tff.tensorflow.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)
0.11990704 1.9185987

此外,可以在 TFF 中生成确定性噪声,而无需显式设置种子。以下代码片段中的函数return_two_noise 返回两个相同的噪声值。这是预期的行为,因为 TFF 将在执行之前预先构建计算图。但是,这表明用户必须注意在 TFF 中使用tf.random.normal

谨慎使用:tf.random.Generator

我们可以使用tf.random.Generator,如TF 教程中所建议的那样。

@tff.tensorflow.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.38260335

但是,用户可能需要谨慎使用它

一般来说,TFF 偏好函数操作,我们将在以下部分展示tf.random.stateless_* 函数的使用。

在用于联合学习的 TFF 中,我们经常使用嵌套结构而不是标量,前面的代码片段可以自然地扩展到嵌套结构。

@tff.tensorflow.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.38260335, -0.4780486 ],
       [-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.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 作为将联邦学习算法表示为 TFF 迭代过程的示例。此处用于生成随机噪声的伪种子状态是 tf.Tensor,它可以轻松地在 TFF 和 TF 函数中传输。

@tff.tensorflow.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.tensorflow.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.86828816,  0.8535084 ],
       [ 1.0053564 , -0.42096713]], dtype=float32), array([0.18048067], dtype=float32)]
n2 [array([[-1.1973879 , -0.2974589 ],
       [ 1.8309833 ,  0.17024393]], dtype=float32), array([0.68991095], dtype=float32)]