Эта страница была переведа с помощью Cloud Translation API.
Switch to English

Федеративное обучение для создания текста

Посмотреть на TensorFlow.org Запускаем в Google Colab Посмотреть исходный код на GitHub

Это руководство основано на концепциях руководства по федеративному обучению для классификации изображений и демонстрирует несколько других полезных подходов к федеративному обучению.

В частности, мы загружаем предварительно обученную модель Keras и уточняем ее с помощью федеративного обучения на (смоделированном) децентрализованном наборе данных. Это практически важно по нескольким причинам. Возможность использовать сериализованные модели позволяет легко сочетать федеративное обучение с другими подходами к машинному обучению. Кроме того, это позволяет использовать увеличивающийся диапазон предварительно обученных моделей - например, обучение языковых моделей с нуля редко требуется, поскольку в настоящее время широко доступны многочисленные предварительно обученные модели (см., Например, TF Hub ). Вместо этого имеет смысл начать с предварительно обученной модели и усовершенствовать ее с помощью федеративного обучения, адаптируясь к конкретным характеристикам децентрализованных данных для конкретного приложения.

В этом руководстве мы начинаем с RNN, которая генерирует символы ASCII, и уточняем ее с помощью федеративного обучения. Мы также показываем, как окончательные веса могут быть возвращены в исходную модель Keras, что позволяет легко оценивать и генерировать текст с помощью стандартных инструментов.


!pip install --quiet --upgrade tensorflow_federated_nightly
!pip install --quiet --upgrade nest_asyncio

import nest_asyncio
nest_asyncio.apply()
import collections
import functools
import os
import time

import numpy as np
import tensorflow as tf
import tensorflow_federated as tff

np.random.seed(0)

# Test the TFF is working:
tff.federated_computation(lambda: 'Hello, World!')()
b'Hello, World!'

Загрузить предварительно обученную модель

Мы загружаем модель, которая была предварительно обучена после учебника TensorFlow. Генерация текста с использованием RNN с активным выполнением . Однако, вместо того, чтобы изучать Полное собрание сочинений Шекспира , мы предварительно обучили модель тексту из « Повести о двух городах и рождественской гимне» Чарльза Диккенса.

Помимо расширения словарного запаса, мы не модифицировали исходный учебник, поэтому эта исходная модель не является современной, но она дает разумные прогнозы и достаточна для целей нашего учебника. Окончательная модель была сохранена с помощью tf.keras.models.save_model(include_optimizer=False) .

Мы будем использовать федеративное обучение, чтобы настроить эту модель для Шекспира в этом руководстве, используя федеративную версию данных, предоставленных TFF.

Создать таблицы поиска словаря

# A fixed vocabularly of ASCII chars that occur in the works of Shakespeare and Dickens:
vocab = list('dhlptx@DHLPTX $(,048cgkoswCGKOSW[_#\'/37;?bfjnrvzBFJNRVZ"&*.26:\naeimquyAEIMQUY]!%)-159\r')

# Creating a mapping from unique characters to indices
char2idx = {u:i for i, u in enumerate(vocab)}
idx2char = np.array(vocab)

Загрузите предварительно обученную модель и сгенерируйте текст

def load_model(batch_size):
  urls = {
      1: 'https://storage.googleapis.com/tff-models-public/dickens_rnn.batch1.kerasmodel',
      8: 'https://storage.googleapis.com/tff-models-public/dickens_rnn.batch8.kerasmodel'}
  assert batch_size in urls, 'batch_size must be in ' + str(urls.keys())
  url = urls[batch_size]
  local_file = tf.keras.utils.get_file(os.path.basename(url), origin=url)  
  return tf.keras.models.load_model(local_file, compile=False)
def generate_text(model, start_string):
  # From https://www.tensorflow.org/tutorials/sequences/text_generation
  num_generate = 200
  input_eval = [char2idx[s] for s in start_string]
  input_eval = tf.expand_dims(input_eval, 0)
  text_generated = []
  temperature = 1.0

  model.reset_states()
  for i in range(num_generate):
    predictions = model(input_eval)
    predictions = tf.squeeze(predictions, 0)
    predictions = predictions / temperature
    predicted_id = tf.random.categorical(
        predictions, num_samples=1)[-1, 0].numpy()
    input_eval = tf.expand_dims([predicted_id], 0)
    text_generated.append(idx2char[predicted_id])

  return (start_string + ''.join(text_generated))
# Text generation requires a batch_size=1 model.
keras_model_batch1 = load_model(batch_size=1)
print(generate_text(keras_model_batch1, 'What of TensorFlow Federated, you ask? '))
Downloading data from https://storage.googleapis.com/tff-models-public/dickens_rnn.batch1.kerasmodel
16195584/16193984 [==============================] - 0s 0us/step
16203776/16193984 [==============================] - 0s 0us/step
What of TensorFlow Federated, you ask? Sall
yesterday. Received the Bailey."

"Mr. Lorry, grimmering himself, or low varked thends the winter, and the eyes of Monsieur
Defarge. "Let his mind, hon in his
life and message; four declare 

Загрузить и предварительно обработать объединенные данные Шекспира

Пакет tff.simulation.datasets предоставляет множество наборов данных, которые разделены на «клиентов», где каждый клиент соответствует набору данных на определенном устройстве, которое может участвовать в федеративном обучении.

Эти наборы данных обеспечивают реалистичное распределение данных без IID, которое имитирует задачи обучения на реальных децентрализованных данных. Некоторая часть предварительной обработки этих данных была выполнена с использованием инструментов из проекта Leaf ( github ).

train_data, test_data = tff.simulation.datasets.shakespeare.load_data()

Наборы данных, предоставляемые shakespeare.load_data() состоят из последовательности Tensors строки, по одному для каждой строки, произнесенной определенным персонажем в пьесе Шекспира. Ключи клиента состоят из названия пьесы, соединенного с именем персонажа, так, например, MUCH_ADO_ABOUT_NOTHING_OTHELLO соответствует строкам для персонажа Отелло в пьесе « Много шума из ничего» . Обратите внимание, что в реальном сценарии федеративного обучения клиенты никогда не идентифицируются и не отслеживаются по идентификаторам, но для моделирования полезно работать с наборами данных с ключами.

Вот, например, некоторые данные Короля Лира:

# Here the play is "The Tragedy of King Lear" and the character is "King".
raw_example_dataset = train_data.create_tf_dataset_for_client(
    'THE_TRAGEDY_OF_KING_LEAR_KING')
# To allow for future extensions, each entry x
# is an OrderedDict with a single key 'snippets' which contains the text.
for x in raw_example_dataset.take(2):
  print(x['snippets'])
tf.Tensor(b'', shape=(), dtype=string)
tf.Tensor(b'What?', shape=(), dtype=string)

Теперь мы используем преобразования tf.data.Dataset чтобы подготовить эти данные для обучения char RNN, загруженного выше.

# Input pre-processing parameters
SEQ_LENGTH = 100
BATCH_SIZE = 8
BUFFER_SIZE = 100  # For dataset shuffling
# Construct a lookup table to map string chars to indexes,
# using the vocab loaded above:
table = tf.lookup.StaticHashTable(
    tf.lookup.KeyValueTensorInitializer(
        keys=vocab, values=tf.constant(list(range(len(vocab))),
                                       dtype=tf.int64)),
    default_value=0)


def to_ids(x):
  s = tf.reshape(x['snippets'], shape=[1])
  chars = tf.strings.bytes_split(s).values
  ids = table.lookup(chars)
  return ids


def split_input_target(chunk):
  input_text = tf.map_fn(lambda x: x[:-1], chunk)
  target_text = tf.map_fn(lambda x: x[1:], chunk)
  return (input_text, target_text)


def preprocess(dataset):
  return (
      # Map ASCII chars to int64 indexes using the vocab
      dataset.map(to_ids)
      # Split into individual chars
      .unbatch()
      # Form example sequences of SEQ_LENGTH +1
      .batch(SEQ_LENGTH + 1, drop_remainder=True)
      # Shuffle and form minibatches
      .shuffle(BUFFER_SIZE).batch(BATCH_SIZE, drop_remainder=True)
      # And finally split into (input, target) tuples,
      # each of length SEQ_LENGTH.
      .map(split_input_target))

Обратите внимание, что при формировании исходных последовательностей и при формировании пакетов выше мы используем drop_remainder=True для простоты. Это означает, что любые символы (клиенты), у которых нет хотя бы (SEQ_LENGTH + 1) * BATCH_SIZE символов текста, будут иметь пустые наборы данных. Типичный подход к решению этой проблемы - заполнить пакеты специальным токеном, а затем замаскировать потерю, чтобы не учитывать токены заполнения.

Это несколько усложнит пример, поэтому для этого урока мы используем только полные пакеты, как в стандартном руководстве . Однако в федеративной настройке эта проблема более значительна, поскольку у многих пользователей могут быть небольшие наборы данных.

Теперь мы можем предварительно обработать raw_example_dataset и проверить типы:

example_dataset = preprocess(raw_example_dataset)
print(example_dataset.element_spec)
(TensorSpec(shape=(8, 100), dtype=tf.int64, name=None), TensorSpec(shape=(8, 100), dtype=tf.int64, name=None))

Скомпилируйте модель и протестируйте на предварительно обработанных данных

Мы загрузили некомпилированную модель keras, но чтобы запустить keras_model.evaluate , нам нужно скомпилировать ее с потерями и метриками. Мы также скомпилируем оптимизатор, который будет использоваться в качестве оптимизатора на устройстве в федеративном обучении.

В исходном руководстве не было точности на уровне символов (доля предсказаний, в которых наибольшая вероятность была помещена на правильный следующий символ). Это полезный показатель, поэтому мы его добавляем. Однако нам необходимо определить новый класс метрики для этого, потому что наши прогнозы имеют ранг 3 (вектор логитов для каждого из BATCH_SIZE * SEQ_LENGTH ), а SparseCategoricalAccuracy ожидает только прогнозов ранга 2.

class FlattenedCategoricalAccuracy(tf.keras.metrics.SparseCategoricalAccuracy):

  def __init__(self, name='accuracy', dtype=tf.float32):
    super().__init__(name, dtype=dtype)

  def update_state(self, y_true, y_pred, sample_weight=None):
    y_true = tf.reshape(y_true, [-1, 1])
    y_pred = tf.reshape(y_pred, [-1, len(vocab), 1])
    return super().update_state(y_true, y_pred, sample_weight)

Теперь мы можем скомпилировать модель и оценить ее в нашем example_dataset .

BATCH_SIZE = 8  # The training and eval batch size for the rest of this tutorial.
keras_model = load_model(batch_size=BATCH_SIZE)
keras_model.compile(
    loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
    metrics=[FlattenedCategoricalAccuracy()])

# Confirm that loss is much lower on Shakespeare than on random data
loss, accuracy = keras_model.evaluate(example_dataset.take(5), verbose=0)
print(
    'Evaluating on an example Shakespeare character: {a:3f}'.format(a=accuracy))

# As a sanity check, we can construct some completely random data, where we expect
# the accuracy to be essentially random:
random_guessed_accuracy = 1.0 / len(vocab)
print('Expected accuracy for random guessing: {a:.3f}'.format(
    a=random_guessed_accuracy))
random_indexes = np.random.randint(
    low=0, high=len(vocab), size=1 * BATCH_SIZE * (SEQ_LENGTH + 1))
data = collections.OrderedDict(
    snippets=tf.constant(
        ''.join(np.array(vocab)[random_indexes]), shape=[1, 1]))
random_dataset = preprocess(tf.data.Dataset.from_tensor_slices(data))
loss, accuracy = keras_model.evaluate(random_dataset, steps=10, verbose=0)
print('Evaluating on completely random data: {a:.3f}'.format(a=accuracy))
Downloading data from https://storage.googleapis.com/tff-models-public/dickens_rnn.batch8.kerasmodel
16195584/16193984 [==============================] - 0s 0us/step
16203776/16193984 [==============================] - 0s 0us/step
Evaluating on an example Shakespeare character: 0.402000
Expected accuracy for random guessing: 0.012
Evaluating on completely random data: 0.011

Настройте модель с помощью федеративного обучения

TFF сериализует все вычисления TensorFlow, чтобы их можно было запускать в среде, отличной от Python (хотя на данный момент доступна только среда выполнения моделирования, реализованная на Python). Несмотря на то, что мы работаем в режиме ожидания (TF 2.0), в настоящее время TFF сериализует вычисления with tf.Graph.as_default() необходимые операции внутри контекста оператора with tf.Graph.as_default() . Таким образом, нам нужно предоставить функцию, которую TFF может использовать для представления нашей модели в граф, который она контролирует. Делаем это следующим образом:

# Clone the keras_model inside `create_tff_model()`, which TFF will
# call to produce a new copy of the model inside the graph that it will 
# serialize. Note: we want to construct all the necessary objects we'll need 
# _inside_ this method.
def create_tff_model():
  # TFF uses an `input_spec` so it knows the types and shapes
  # that your model expects.
  input_spec = example_dataset.element_spec
  keras_model_clone = tf.keras.models.clone_model(keras_model)
  return tff.learning.from_keras_model(
      keras_model_clone,
      input_spec=input_spec,
      loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
      metrics=[FlattenedCategoricalAccuracy()])

Теперь мы готовы построить итерационный процесс федеративного усреднения, который мы будем использовать для улучшения модели (подробности об алгоритме федеративного усреднения см. В статье « Коммуникационно-эффективное обучение глубоких сетей на основе децентрализованных данных» ).

Мы используем скомпилированную модель Кераса для выполнения стандартной (нефедеративной) оценки после каждого раунда федеративного обучения. Это полезно для исследовательских целей при моделировании федеративного обучения и наличии стандартного тестового набора данных.

В реалистичных производственных условиях этот же метод можно использовать для взятия моделей, обученных с помощью федеративного обучения, и их оценки на централизованном наборе эталонных данных для тестирования или обеспечения качества.

# This command builds all the TensorFlow graphs and serializes them: 
fed_avg = tff.learning.build_federated_averaging_process(
    model_fn=create_tff_model,
    client_optimizer_fn=lambda: tf.keras.optimizers.SGD(lr=0.5))

Вот самый простой из возможных циклов, в котором мы запускаем федеративное усреднение для одного раунда на одном клиенте в одном пакете:

state = fed_avg.initialize()
state, metrics = fed_avg.next(state, [example_dataset.take(5)])
train_metrics = metrics['train']
print('loss={l:.3f}, accuracy={a:.3f}'.format(
    l=train_metrics['loss'], a=train_metrics['accuracy']))
loss=4.403, accuracy=0.132

Теперь давайте напишем немного более интересный цикл обучения и оценки.

Чтобы это моделирование продолжалось относительно быстро, мы обучаем одних и тех же трех клиентов в каждом раунде, учитывая только два мини-батча для каждого.

def data(client, source=train_data):
  return preprocess(source.create_tf_dataset_for_client(client)).take(5)


clients = [
    'ALL_S_WELL_THAT_ENDS_WELL_CELIA', 'MUCH_ADO_ABOUT_NOTHING_OTHELLO',
]

train_datasets = [data(client) for client in clients]

# We concatenate the test datasets for evaluation with Keras by creating a 
# Dataset of Datasets, and then identity flat mapping across all the examples.
test_dataset = tf.data.Dataset.from_tensor_slices(
    [data(client, test_data) for client in clients]).flat_map(lambda x: x)

Начальное состояние модели, созданное с помощью fed_avg.initialize() , основано на случайных инициализаторах для модели clone_model() а не на загруженных весах, поскольку clone_model() не клонирует веса. Чтобы начать обучение с предварительно обученной модели, мы устанавливаем веса модели в состоянии сервера прямо из загруженной модели.

NUM_ROUNDS = 5

# The state of the FL server, containing the model and optimization state.
state = fed_avg.initialize()

# Load our pre-trained Keras model weights into the global model state.
state = tff.learning.state_with_new_model_weights(
    state,
    trainable_weights=[v.numpy() for v in keras_model.trainable_weights],
    non_trainable_weights=[
        v.numpy() for v in keras_model.non_trainable_weights
    ])


def keras_evaluate(state, round_num):
  # Take our global model weights and push them back into a Keras model to
  # use its standard `.evaluate()` method.
  keras_model = load_model(batch_size=BATCH_SIZE)
  keras_model.compile(
      loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
      metrics=[FlattenedCategoricalAccuracy()])
  state.model.assign_weights_to(keras_model)
  loss, accuracy = keras_model.evaluate(example_dataset, steps=2, verbose=0)
  print('\tEval: loss={l:.3f}, accuracy={a:.3f}'.format(l=loss, a=accuracy))


for round_num in range(NUM_ROUNDS):
  print('Round {r}'.format(r=round_num))
  keras_evaluate(state, round_num)
  state, metrics = fed_avg.next(state, train_datasets)
  train_metrics = metrics['train']
  print('\tTrain: loss={l:.3f}, accuracy={a:.3f}'.format(
      l=train_metrics['loss'], a=train_metrics['accuracy']))

print('Final evaluation')
keras_evaluate(state, NUM_ROUNDS + 1)
Round 0
    Eval: loss=3.324, accuracy=0.401
    Train: loss=4.360, accuracy=0.155
Round 1
    Eval: loss=4.361, accuracy=0.049
    Train: loss=4.235, accuracy=0.164
Round 2
    Eval: loss=4.219, accuracy=0.177
    Train: loss=4.081, accuracy=0.221
Round 3
    Eval: loss=4.080, accuracy=0.174
    Train: loss=3.940, accuracy=0.226
Round 4
    Eval: loss=3.991, accuracy=0.176
    Train: loss=3.840, accuracy=0.226
Final evaluation
    Eval: loss=3.909, accuracy=0.171

С изменениями по умолчанию мы не провели достаточного обучения, чтобы иметь большое значение, но если вы тренируетесь дольше на большем количестве данных Шекспира, вы должны увидеть разницу в стиле текста, созданного с помощью обновленной модели:

# Set our newly trained weights back in the originally created model.
keras_model_batch1.set_weights([v.numpy() for v in keras_model.weights])
# Text generation requires batch_size=1
print(generate_text(keras_model_batch1, 'What of TensorFlow Federated, you ask? '))
What of TensorFlow Federated, you ask? Shalways, I will call your
compet with any city brought their faces uncompany," besumed him. "When he
sticked Madame Defarge pushed the lamps.

"Have I often but no unison. She had probably come, 

Предлагаемые расширения

Этот урок - только первый шаг! Вот несколько идей, как можно попробовать расширить этот блокнот:

  • Напишите более реалистичный цикл обучения, в котором вы выбираете клиентов для обучения случайным образом.
  • Используйте " .repeat(NUM_EPOCHS) " в наборах данных клиента, чтобы попробовать несколько эпох локального обучения (например, как в McMahan et. Al. ). См. Также Федеративное обучение для классификации изображений, в котором это делается.
  • Измените команду compile() чтобы поэкспериментировать с использованием различных алгоритмов оптимизации на клиенте.
  • Попробуйте server_optimizer аргумент server_optimizer для build_federated_averaging_process чтобы попробовать различные алгоритмы применения обновлений модели на сервере.
  • Попробуйте аргумент client_weight_fn для build_federated_averaging_process чтобы попробовать разные веса клиентов. По умолчанию обновления клиента взвешиваются по количеству примеров на клиенте, но вы можете сделать, например, client_weight_fn=lambda _: tf.constant(1.0) .