В этом руководстве будут рассмотрены рекомендуемые передовые практики для генерации случайного шума в TFF. Генерация случайного шума является важным компонентом многих методов защиты конфиденциальности в алгоритмах федеративного обучения, например, дифференциальной конфиденциальности.
Посмотреть на TensorFlow.org | Запускаем в Google Colab | Посмотреть исходный код на GitHub | Скачать блокнот |
Прежде, чем мы начнем
Во-первых, давайте убедимся, что ноутбук подключен к бэкэнду, на котором скомпилированы соответствующие компоненты.
!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 как API , tf.random.normal
для генерации случайных шумов настоятельно не рекомендуется в TF2 по случайной обучающей генерации шума в TF . Удивляет поведение может произойти , если эти интерфейсы используются вместе с 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
Однако пользователям, возможно, придется соблюдать осторожность при его использовании.
-
tf.random.Generator
используетtf.Variable
для поддержания состояния для ГСЧ алгоритмов. В TFF, рекомендуется 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_*
с помощником
Общая рекомендация в 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 как пример выражения федеративного алгоритм обучения как 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)]