本教程将讨论在 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.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.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
但是,用户可能需要谨慎使用它
tf.random.Generator
使用tf.Variable
来维护 RNG 算法的状态。在 TFF 中,建议在tff.tensorflow.computation
内构建生成器;很难在tff.tensorflow.computation
函数之间传递生成器及其状态。- 前面的代码片段还依赖于在生成器中仔细设置种子。如果我们使用
tf.random.Generator.from_non_deterministic_state()
而不是,我们可能会得到预期的但令人惊讶的结果(确定性n1==n2
)。
一般来说,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)]
推荐使用:tf.random.stateless_*
与助手
在 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)]