Генерация случайного шума в TFF

В этом руководстве будут рассмотрены рекомендуемые передовые практики для генерации случайного шума в 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)]

Общая рекомендация в 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)]