Benutzerdefinierte föderierte Algorithmen, Teil 2: Implementieren der föderierten Mittelwertbildung

Auf TensorFlow.org ansehen In Google Colab ausführen Quelle auf GitHub anzeigen Notizbuch herunterladen

Dieses Tutorial ist der zweite Teil einer zweiteiligen Serie , die zeigt , wie benutzerdefinierte Typen von föderierten Algorithmen in TFF mit dem implementieren Federated Core (FC) , die als Grundlage dient für den Federated Learning (FL) Schicht ( tff.learning ) .

Wir empfehlen Ihnen, zuerst das Lesen ersten Teil dieser Serie , die hier verwendet , um einige der wichtigsten Konzepte und Programmierung Abstraktionen einzuführen.

Dieser zweite Teil der Serie verwendet die im ersten Teil vorgestellten Mechanismen, um eine einfache Version föderierter Trainings- und Bewertungsalgorithmen zu implementieren.

Wir empfehlen Ihnen , die eine Bewertung der Bildklassifikation und Textgenerierung Tutorials für ein übergeordnetes und sanftere Einführung in TFF des Federated Learning - APIs, wie sie Ihnen die Konzepte helfen setzen wir hier in Zusammenhang beschreiben.

Bevor wir anfangen

Bevor wir beginnen, versuchen Sie, das folgende "Hello World"-Beispiel auszuführen, um sicherzustellen, dass Ihre Umgebung richtig eingerichtet ist. Wenn es nicht funktioniert, entnehmen Sie bitte der Installationsanleitung für Anweisungen.

!pip install --quiet --upgrade tensorflow-federated-nightly
!pip install --quiet --upgrade nest-asyncio

import nest_asyncio
nest_asyncio.apply()
import collections

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

# TODO(b/148678573,b/148685415): must use the reference context because it
# supports unbounded references and tff.sequence_* intrinsics.
tff.backends.reference.set_reference_context()
@tff.federated_computation
def hello_world():
  return 'Hello, World!'

hello_world()
'Hello, World!'

Implementieren der föderierten Mittelwertbildung

Wie in Federated Lernen für Bildklassifizierung , werden wir das MNIST Beispiel verwenden, aber da dies als ein Low-Level - Tutorial gedacht ist, werden wir in dem Bypass der Keras API und tff.simulation , schreiben Rohmodell Code und Konstrukt eines föderierten Datensatz von Grund auf neu.

Vorbereiten föderierter Datensätze

Zur Demonstration simulieren wir ein Szenario, in dem wir Daten von 10 Benutzern haben und jeder der Benutzer Kenntnisse darüber beisteuert, wie man eine andere Ziffer erkennt. Das ist etwa so nicht iid wie es geht.

Laden wir zunächst die Standard-MNIST-Daten:

mnist_train, mnist_test = tf.keras.datasets.mnist.load_data()
Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/mnist.npz
11493376/11490434 [==============================] - 0s 0us/step
11501568/11490434 [==============================] - 0s 0us/step
[(x.dtype, x.shape) for x in mnist_train]
[(dtype('uint8'), (60000, 28, 28)), (dtype('uint8'), (60000,))]

Die Daten werden als Numpy-Arrays geliefert, eines mit Bildern und eines mit Ziffernbeschriftungen, wobei die erste Dimension die einzelnen Beispiele durchläuft. Lassen Sie uns eine Hilfsfunktion schreiben, die sie so formatiert, wie wir föderierte Sequenzen in TFF-Berechnungen einspeisen, dh als Liste von Listen - die äußere Liste erstreckt sich über die Benutzer (Ziffern), die inneren erstreckt sich über Stapel von Daten in die Reihenfolge jedes Kunden. Wie üblich, werden wir jede Partie als ein Paar von Tensor genannt strukturieren x und y , die jeweils mit der führenden Batch Dimension. Während es an , wir glätten auch jedes Bild in einen 784-Element - Vektor und rescale die Pixel in es in den 0..1 Bereich, so dass wir nicht die Modell Logik mit Datenkonvertierungen Krempel müssen.

NUM_EXAMPLES_PER_USER = 1000
BATCH_SIZE = 100


def get_data_for_digit(source, digit):
  output_sequence = []
  all_samples = [i for i, d in enumerate(source[1]) if d == digit]
  for i in range(0, min(len(all_samples), NUM_EXAMPLES_PER_USER), BATCH_SIZE):
    batch_samples = all_samples[i:i + BATCH_SIZE]
    output_sequence.append({
        'x':
            np.array([source[0][i].flatten() / 255.0 for i in batch_samples],
                     dtype=np.float32),
        'y':
            np.array([source[1][i] for i in batch_samples], dtype=np.int32)
    })
  return output_sequence


federated_train_data = [get_data_for_digit(mnist_train, d) for d in range(10)]

federated_test_data = [get_data_for_digit(mnist_test, d) for d in range(10)]

Als schnelle Plausibilitätsprüfung, lassen Sie sich Blick auf dem Y - Tensor in der letzten Partie der Daten durch den fünften Client beigetragen (der einer zu der Ziffer entspricht 5 ).

federated_train_data[5][-1]['y']
array([5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
       5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
       5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
       5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
       5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5], dtype=int32)

Schauen wir uns zur Sicherheit auch das Bild an, das dem letzten Element dieses Stapels entspricht.

from matplotlib import pyplot as plt

plt.imshow(federated_train_data[5][-1]['x'][-1].reshape(28, 28), cmap='gray')
plt.grid(False)
plt.show()

png

Zur Kombination von TensorFlow und TFF

In diesem Tutorial für Kompaktheit dekorieren wir sofort Funktionen , die TensorFlow Logik einführen tff.tf_computation . Für komplexere Logik ist dies jedoch nicht das Muster, das wir empfehlen. Das Debuggen von TensorFlow kann bereits eine Herausforderung sein, und das Debuggen von TensorFlow, nachdem es vollständig serialisiert und dann erneut importiert wurde, verliert zwangsläufig einige Metadaten und schränkt die Interaktivität ein, was das Debuggen noch schwieriger macht.

Daher empfehlen wir dringend , komplexe TF Logik als Stand-alone - Python - Funktionen zu schreiben (das heißt, ohne tff.tf_computation Dekoration). Auf diese Weise die TensorFlow Logik kann mit TF Best Practices und Werkzeuge (wie eifrig Modus) entwickelt und getestet werden, bevor die Berechnung für TFF Serialisierung (zB durch den Aufruf tff.tf_computation mit einer Python - Funktion als Argument).

Definieren einer Verlustfunktion

Nachdem wir nun die Daten haben, definieren wir eine Verlustfunktion, die wir für das Training verwenden können. Zuerst definieren wir den Eingabetyp als TFF namens Tupel. Da die Größe der Daten Chargen variieren können, setzen wir die Batch - Dimension None , um anzuzeigen , dass die Größe dieser Dimension unbekannt ist.

BATCH_SPEC = collections.OrderedDict(
    x=tf.TensorSpec(shape=[None, 784], dtype=tf.float32),
    y=tf.TensorSpec(shape=[None], dtype=tf.int32))
BATCH_TYPE = tff.to_type(BATCH_SPEC)

str(BATCH_TYPE)
'<x=float32[?,784],y=int32[?]>'

Sie fragen sich vielleicht, warum wir nicht einfach einen gewöhnlichen Python-Typ definieren können. Denken Sie an die Diskussion in Teil 1 , wo wir erklären , dass , während wir die Logik der TFF Berechnungen ausdrücken können mit Python, unter der Motorhaube TFF Berechnungen sind nicht Python. Das Symbol BATCH_TYPE oben definiert repräsentiert eine abstrakte TFF - Typ - Spezifikation. Es ist wichtig , diesen abstrakten TFF - Typen aus Beton Python Darstellungsart zu unterscheiden, beispielsweise Behälter, wie dict oder collections.namedtuple , die verwendet werden , um den TFF - Typen in dem Körper eines Python Funktion darzustellen. Im Gegensatz zu Python, hat TFF einen einzigen abstrakten Typkonstruktor tff.StructType für Tupel artige Behälter, mit Elementen , die individuell benannt werden können oder links unbenannte. Dieser Typ wird auch verwendet, um formale Parameter von Berechnungen zu modellieren, da TFF-Berechnungen formal nur einen Parameter und ein Ergebnis deklarieren können - Beispiele dafür werden Sie in Kürze sehen.

Lassen Sie uns nun den TFF - Typ von Modellparametern definieren, wieder als TFF genannt Tupel von Gewichten und Voreingenommenheit.

MODEL_SPEC = collections.OrderedDict(
    weights=tf.TensorSpec(shape=[784, 10], dtype=tf.float32),
    bias=tf.TensorSpec(shape=[10], dtype=tf.float32))
MODEL_TYPE = tff.to_type(MODEL_SPEC)

print(MODEL_TYPE)
<weights=float32[784,10],bias=float32[10]>

Mit diesen Definitionen können wir jetzt den Verlust für ein bestimmtes Modell über eine einzelne Charge definieren. Beachten Sie die Verwendung von @tf.function Dekorateur innerhalb des @tff.tf_computation Dekorateur. Dies ermöglicht es uns , TF mit Python wie Semantik zu schreiben , obwohl in einem waren tf.Graph Kontext , der mit dem tff.tf_computation Dekorateur.

# NOTE: `forward_pass` is defined separately from `batch_loss` so that it can 
# be later called from within another tf.function. Necessary because a
# @tf.function  decorated method cannot invoke a @tff.tf_computation.

@tf.function
def forward_pass(model, batch):
  predicted_y = tf.nn.softmax(
      tf.matmul(batch['x'], model['weights']) + model['bias'])
  return -tf.reduce_mean(
      tf.reduce_sum(
          tf.one_hot(batch['y'], 10) * tf.math.log(predicted_y), axis=[1]))

@tff.tf_computation(MODEL_TYPE, BATCH_TYPE)
def batch_loss(model, batch):
  return forward_pass(model, batch)

Wie erwartet, Berechnung batch_loss kehrt float32 hat das Modell und einen einzelnen Daten Charge gegeben. Beachten Sie, wie die MODEL_TYPE und BATCH_TYPE haben zusammen in eine 2-Tupel von formalen Parametern in einen Topf geworfen worden; können Sie die Art von erkennen batch_loss als (<MODEL_TYPE,BATCH_TYPE> -> float32) .

str(batch_loss.type_signature)
'(<<weights=float32[784,10],bias=float32[10]>,<x=float32[?,784],y=int32[?]>> -> float32)'

Lassen Sie uns als Plausibilitätsprüfung ein mit Nullen gefülltes Anfangsmodell konstruieren und den Verlust über den oben visualisierten Datenstapel berechnen.

initial_model = collections.OrderedDict(
    weights=np.zeros([784, 10], dtype=np.float32),
    bias=np.zeros([10], dtype=np.float32))

sample_batch = federated_train_data[5][-1]

batch_loss(initial_model, sample_batch)
2.3025854

Man beachte , daß wir die TFF - Berechnung mit dem anfänglichen Modell als definierten Vorschub dict , obwohl der Körper der Python - Funktion , die definiert , es verbraucht Modellparameter als model['weight'] und model['bias'] . Die Argumente des Aufrufs batch_loss nicht einfach auf den Körper dieser Funktion übergeben.

Was passiert , wenn wir invoke batch_loss ? Der Python Körper von batch_loss wird bereits in der obigen Zelle verfolgt und serialisiert , wo er definiert wurde. TFF fungiert als Anrufer batch_loss an der Berechnungsdefinitionszeit, und als Ziel des Aufrufs zu dem Zeitpunkt batch_loss aufgerufen. In beiden Rollen dient TFF als Brücke zwischen dem abstrakten Typsystem von TFF und den Python-Darstellungstypen. Bei der Anrufung der Zeit wird TFF akzeptieren die meisten Standard - Python - Containertypen ( dict , list , tuple , collections.namedtuple , etc.) als konkrete Darstellungen von abstrakten TFF Tupel. Obwohl TFF-Berechnungen, wie oben erwähnt, formal nur einen einzelnen Parameter akzeptieren, können Sie die bekannte Python-Aufrufsyntax mit Positions- und/oder Schlüsselwortargumenten verwenden, falls der Parametertyp ein Tupel ist - es funktioniert wie erwartet.

Gefälleabstieg mit einer einzigen Charge

Lassen Sie uns nun eine Berechnung definieren, die diese Verlustfunktion verwendet, um einen einzelnen Schritt des Gradientenabstiegs durchzuführen. Beachten Sie, wie diese Funktion bei der Definition, die wir verwenden batch_loss als Subkomponente. Sie können eine Berechnung mit aufgebauten aufrufen tff.tf_computation im Innern des Körpers einer anderen Berechnung, doch in der Regel ist dies nicht notwendig - wie oben erwähnt, weil Serialisierung einiger Debug - Informationen verliert, ist es für komplexere Berechnungen zu schreiben oft vorzuziehen ist , und testen Sie alle TensorFlow ohne die tff.tf_computation Dekorateur.

@tff.tf_computation(MODEL_TYPE, BATCH_TYPE, tf.float32)
def batch_train(initial_model, batch, learning_rate):
  # Define a group of model variables and set them to `initial_model`. Must
  # be defined outside the @tf.function.
  model_vars = collections.OrderedDict([
      (name, tf.Variable(name=name, initial_value=value))
      for name, value in initial_model.items()
  ])
  optimizer = tf.keras.optimizers.SGD(learning_rate)

  @tf.function
  def _train_on_batch(model_vars, batch):
    # Perform one step of gradient descent using loss from `batch_loss`.
    with tf.GradientTape() as tape:
      loss = forward_pass(model_vars, batch)
    grads = tape.gradient(loss, model_vars)
    optimizer.apply_gradients(
        zip(tf.nest.flatten(grads), tf.nest.flatten(model_vars)))
    return model_vars

  return _train_on_batch(model_vars, batch)
str(batch_train.type_signature)
'(<<weights=float32[784,10],bias=float32[10]>,<x=float32[?,784],y=int32[?]>,float32> -> <weights=float32[784,10],bias=float32[10]>)'

Wenn Sie eine Python - Funktion mit dekoriert aufrufen tff.tf_computation innerhalb des Körpers einer anderen solchen Funktion wird die Logik der inneren TFF Berechnung eingebettet ( im wesentlichen, inlined) in der Logik des äußerte. Wie oben erwähnt, wenn Sie beide Berechnungen schreiben, ist es wahrscheinlich , bevorzugt , die innere Funktion (um batch_loss in diesem Fall) eine regelmäßige Python oder tf.function anstatt eine tff.tf_computation . Doch hier zeigen wir , dass die Berufung einer tff.tf_computation innerhalb eines anderen im Grunde wie erwartet funktioniert. Dies kann erforderlich sein , wenn zum Beispiel Sie nicht über den Python - Code zu definieren haben batch_loss , sondern nur seine serialisierten TFF Darstellung.

Wenden wir diese Funktion nun einige Male auf das Ausgangsmodell an, um zu sehen, ob der Verlust abnimmt.

model = initial_model
losses = []
for _ in range(5):
  model = batch_train(model, sample_batch, 0.1)
  losses.append(batch_loss(model, sample_batch))
losses
[0.19690022, 0.13176313, 0.10113226, 0.082738124, 0.0703014]

Gradientenabstieg auf einer Sequenz von lokalen Daten

Da nun batch_train zur Arbeit erscheint, lassen Sie uns eine ähnliche Trainingsfunktion schreiben local_train dass verbraucht die gesamte Sequenz aller Chargen von einem Benutzer statt nur einer einzigen Charge. Die neue Berechnung müssen jetzt verbrauchen tff.SequenceType(BATCH_TYPE) statt BATCH_TYPE .

LOCAL_DATA_TYPE = tff.SequenceType(BATCH_TYPE)

@tff.federated_computation(MODEL_TYPE, tf.float32, LOCAL_DATA_TYPE)
def local_train(initial_model, learning_rate, all_batches):

  # Mapping function to apply to each batch.
  @tff.federated_computation(MODEL_TYPE, BATCH_TYPE)
  def batch_fn(model, batch):
    return batch_train(model, batch, learning_rate)

  return tff.sequence_reduce(all_batches, initial_model, batch_fn)
str(local_train.type_signature)
'(<<weights=float32[784,10],bias=float32[10]>,float32,<x=float32[?,784],y=int32[?]>*> -> <weights=float32[784,10],bias=float32[10]>)'

In diesem kurzen Codeabschnitt sind einige Details vergraben, lassen Sie uns sie nacheinander durchgehen.

Erstens, während wir diese Logik vollständig in TensorFlow umgesetzt haben könnten, auf Berufung tf.data.Dataset.reduce die Sequenz zu verarbeiten ähnlich, wie wir es früher getan haben, haben wir diesmal dafür entschieden , die Logik in der Klebstoff Sprache auszudrücken als tff.federated_computation . Wir haben die föderierte Operator verwendet tff.sequence_reduce die Reduktion durchzuführen.

Der Betreiber tff.sequence_reduce wird ähnlich verwendet tf.data.Dataset.reduce . Sie können im Wesentlichen daran denken als die gleichen wie tf.data.Dataset.reduce , aber für den Einsatz in föderierten Berechnungen, die wie Sie sich erinnern können, können nicht TensorFlow Code enthalten. Es ist ein Template - Operator mit einem formalen Parameter 3-Tupel , die aus einer Folge von aus T , der Anfangszustand der Reduktion -typed Elemente (wir darauf verweisen abstrakt als Null) einen Typ U und die Reduktion Betreiber Typ (<U,T> -> U) , daß der Zustand ändert sich der Reduktion durch ein einziges Element verarbeitet. Das Ergebnis ist der Endzustand der Reduktion, nachdem alle Elemente in einer sequentiellen Reihenfolge verarbeitet wurden. In unserem Beispiel ist der Reduktionszustand das Modell, das auf einem Präfix der Daten trainiert wurde, und die Elemente sind Datenbatches.

Zweitens beachten Sie, dass wir wieder eine Berechnung verwendet haben ( batch_train ) als eine Komponente innerhalb eines anderen ( local_train ), aber nicht direkt. Wir können es nicht als Reduktionsoperator verwenden, da es einen zusätzlichen Parameter benötigt - die Lernrate. Um dies zu beheben, definieren wir eine eingebettete föderierten Berechnung batch_fn , dass bindet an das local_train ‚s Parameter learning_rate in seinem Körper. Es ist einer auf diese Weise definierten Kindberechnung erlaubt, einen formalen Parameter ihres Elternteils zu erfassen, solange die Kindberechnung nicht außerhalb des Hauptteils ihres Elternteils aufgerufen wird. Sie können als Äquivalent dieses Musters denken functools.partial in Python.

Die praktische Auswirkung der Erfassung learning_rate diese Art und Weise ist natürlich, dass der gleiche Lernrate Wert für alle Chargen verwendet wird.

Nun versuchen Sie , lassen Sie sich die neu lokale Trainingsfunktion definierte auf der gesamten Sequenz von Daten aus dem gleichen Benutzer, der die Probencharge (Ziffer beigetragen 5 ).

locally_trained_model = local_train(initial_model, 0.1, federated_train_data[5])

Hat es funktioniert? Um diese Frage zu beantworten, müssen wir Evaluation implementieren.

Lokale Auswertung

Hier ist eine Möglichkeit, eine lokale Auswertung zu implementieren, indem Sie die Verluste über alle Datenbatches addieren (wir hätten genauso gut den Durchschnitt berechnen können; wir überlassen es dem Leser).

@tff.federated_computation(MODEL_TYPE, LOCAL_DATA_TYPE)
def local_eval(model, all_batches):
  # TODO(b/120157713): Replace with `tff.sequence_average()` once implemented.
  return tff.sequence_sum(
      tff.sequence_map(
          tff.federated_computation(lambda b: batch_loss(model, b), BATCH_TYPE),
          all_batches))
str(local_eval.type_signature)
'(<<weights=float32[784,10],bias=float32[10]>,<x=float32[?,784],y=int32[?]>*> -> float32)'

Auch hier gibt es ein paar neue Elemente, die durch diesen Code veranschaulicht werden, lassen Sie uns sie nacheinander durchgehen.

Erstens haben wir zwei neue Federated Operatoren für die Verarbeitung von Sequenzen verwendet: tff.sequence_map , die eine Abbildungsfunktion nimmt T->U und eine Folge von T und gibt eine Folge von U , erhalten durch Anwenden der Abbildungsfunktion punktuellen und tff.sequence_sum dass fügt einfach alle Elemente hinzu. Hier ordnen wir jeden Datenbatch einem Verlustwert zu und addieren dann die resultierenden Verlustwerte, um den Gesamtverlust zu berechnen.

Beachten Sie, dass wir wieder genutzt haben könnte tff.sequence_reduce , aber dies wäre die beste Wahl nicht sein - das Reduktionsverfahren ist per definitionem, sequentiell, während die Abbildung und die Summe kann parallel berechnet werden. Wenn Sie die Wahl haben, ist es am besten, sich an Operatoren zu halten, die die Implementierungsoptionen nicht einschränken. Wenn unsere TFF-Berechnung in Zukunft kompiliert wird, um in einer bestimmten Umgebung eingesetzt zu werden, können Sie alle potenziellen Möglichkeiten für eine schnellere , skalierbarere, ressourceneffizientere Ausführung.

Zweitens beachten Sie, dass wie in local_train , die Komponente Funktion wir brauchen ( batch_loss ) mehr Parameter als das, was der föderierten Operator (nimmt tff.sequence_map ) erwartet, so dass wir wieder eine partielle, diesmal inline definieren , indem direkt eine Verpackung lambda als tff.federated_computation . Wrapper inline mit einer Funktion als Argument verwendet , ist der empfohlene Weg zu verwenden tff.tf_computation einbetten TensorFlow Logik in TFF.

Mal sehen, ob unser Training funktioniert hat.

print('initial_model loss =', local_eval(initial_model,
                                         federated_train_data[5]))
print('locally_trained_model loss =',
      local_eval(locally_trained_model, federated_train_data[5]))
initial_model loss = 23.025854
locally_trained_model loss = 0.4348469

Tatsächlich hat sich der Verlust verringert. Aber was passiert, wenn wir es anhand der Daten eines anderen Benutzers auswerten?

print('initial_model loss =', local_eval(initial_model,
                                         federated_train_data[0]))
print('locally_trained_model loss =',
      local_eval(locally_trained_model, federated_train_data[0]))
initial_model loss = 23.025854
locally_trained_model loss = 74.50075

Wie erwartet wurde es noch schlimmer. Das Modell wurde trainiert zu erkennen , 5 , und hat noch nie einen gesehen 0 . Damit stellt sich die Frage: Wie hat sich das lokale Training auf die Qualität des Modells aus globaler Sicht ausgewirkt?

Föderierte Auswertung

Dies ist der Punkt auf unserer Reise, an dem wir endlich zu föderierten Typen und föderierten Berechnungen zurückkehren - dem Thema, mit dem wir begonnen haben. Hier ist ein Paar von TFF-Typdefinitionen für das Modell, das vom Server stammt, und die Daten, die auf den Clients verbleiben.

SERVER_MODEL_TYPE = tff.type_at_server(MODEL_TYPE)
CLIENT_DATA_TYPE = tff.type_at_clients(LOCAL_DATA_TYPE)

Mit all den bisher eingeführten Definitionen ist die Darstellung der föderierten Auswertung in TFF ein Einzeiler - wir verteilen das Modell an die Clients, lassen jeden Client die lokale Auswertung für seinen lokalen Teil der Daten aufrufen und berechnen dann den Verlust. Hier ist eine Möglichkeit, dies zu schreiben.

@tff.federated_computation(SERVER_MODEL_TYPE, CLIENT_DATA_TYPE)
def federated_eval(model, data):
  return tff.federated_mean(
      tff.federated_map(local_eval, [tff.federated_broadcast(model), data]))

Wir haben bereits Beispiele gesehen tff.federated_mean und tff.federated_map in einfachen Szenarien, und auf der intuitiven Ebene, sie funktionieren wie erwartet, aber es gibt noch mehr in diesem Code - Abschnitt als das Auge, so lassen sie gehen über ihn sorgfältig.

Zuerst brechen die im Stich lassen die jeden Kunden lassen invoke lokale Auswertung auf seinem lokalen Teil des Datenteils. Wie Sie aus den vorhergehenden Abschnitten erinnern kann, local_eval hat eine Art Signatur der Form (<MODEL_TYPE, LOCAL_DATA_TYPE> -> float32) .

Der föderierte Operator tff.federated_map ist eine Vorlage , die eine 2-Tupel als Parameter übernimmt, der die Abbildungsfunktion eines bestimmten Typs besteht T->U und einen föderierten Wert vom Typ {T}@CLIENTS (dh mit dem Mitglied Bestandteilen der gleichen Typs wie der Parameter der Abbildungsfunktion), und gibt ein Ergebnis des Typs {U}@CLIENTS .

Da wir Fütterung local_eval als Abbildungsfunktion auf einer Pro-Client - Basis anzuwenden, das zweite Argument eines föderierten Typs sein sollte {<MODEL_TYPE, LOCAL_DATA_TYPE>}@CLIENTS , dh in der Nomenklatur der vorhergehenden Abschnitte, sollte es ein föderiertes Tupel sein. Jeder Kunde sollte eine ganze Reihe von Argumenten für halte local_eval als Mitglied consituent. Stattdessen sind wir es ein 2-Element Python Fütterung list . Was passiert hier?

Tatsächlich ist dies ein Beispiel für eine implizite Typumwandlung in TFF, ähnlich wie implizitem Typ Abgüsse Sie an anderer Stelle angetroffen haben, zum Beispiel , wenn Sie einen Feed int auf eine Funktion , die einen übernimmt float . Implizites Casting wird an dieser Stelle kaum verwendet, aber wir planen, es in TFF durchdringen zu lassen, um die Boilerplate zu minimieren.

Die implizite Besetzung , die in diesem Fall angewandt ist , ist die Äquivalenz zwischen Federated Tupel der Form {<X,Y>}@Z und Tupel von föderierten Werten <{X}@Z,{Y}@Z> . Formal, diese zwei verschiedene Arten Signaturen sind, betrachtet es aus der Sicht der Programmierer, jedes Gerät in Z hält zwei Einheiten von Daten X und Y . Was hier passiert , ist nicht anders als zip in Python, und in der Tat bieten wir einen Operator tff.federated_zip , dass Sie solche Konvertierungen explizit ausführen können. Wenn die tff.federated_map ein Tupel als zweites Argument begegnet, ruft es einfach tff.federated_zip für Sie.

In Anbetracht dessen sollte man nun in der Lage sein , den Ausdruck zu erkennen tff.federated_broadcast(model) als einen Wert von TFF - Typ darstellt {MODEL_TYPE}@CLIENTS und data als ein Wert von TFF - Typ {LOCAL_DATA_TYPE}@CLIENTS (oder einfach CLIENT_DATA_TYPE ) die beiden durch ein implizites gefilterten zusammenkommen tff.federated_zip zweites Argument zu bilden tff.federated_map .

Der Betreiber tff.federated_broadcast , wie man es erwarten würde, überträgt nur Daten von dem Server an die Clients.

Sehen wir uns nun an, wie sich unser lokales Training auf den durchschnittlichen Verlust im System ausgewirkt hat.

print('initial_model loss =', federated_eval(initial_model,
                                             federated_train_data))
print('locally_trained_model loss =',
      federated_eval(locally_trained_model, federated_train_data))
initial_model loss = 23.025852
locally_trained_model loss = 54.432625

Tatsächlich ist der Verlust erwartungsgemäß gestiegen. Um das Modell für alle Benutzer zu verbessern, müssen wir uns mit allen Daten vertraut machen.

Verbundtraining

Der einfachste Weg zur Implementierung eines föderierten Trainings besteht darin, lokal zu trainieren und dann die Modelle zu mitteln. Dies verwendet die gleichen Bausteine ​​und Muster, die wir bereits besprochen haben, wie Sie unten sehen können.

SERVER_FLOAT_TYPE = tff.type_at_server(tf.float32)


@tff.federated_computation(SERVER_MODEL_TYPE, SERVER_FLOAT_TYPE,
                           CLIENT_DATA_TYPE)
def federated_train(model, learning_rate, data):
  return tff.federated_mean(
      tff.federated_map(local_train, [
          tff.federated_broadcast(model),
          tff.federated_broadcast(learning_rate), data
      ]))

Beachten Sie, dass in der voll funktionsfähige Implementierung von Federated Averaging bereitgestellt durch tff.learning , anstatt die Modelle mit durchschnittlich bevorzugen wir auf mittlere Modell Deltas, für eine Reihe von Gründen, zum Beispiel die Fähigkeit , die Update - Normen zu befestigen, für die Kompression, usw. .

Mal sehen, ob das Training funktioniert, indem wir ein paar Trainingsrunden laufen und den durchschnittlichen Verlust vorher und nachher vergleichen.

model = initial_model
learning_rate = 0.1
for round_num in range(5):
  model = federated_train(model, learning_rate, federated_train_data)
  learning_rate = learning_rate * 0.9
  loss = federated_eval(model, federated_train_data)
  print('round {}, loss={}'.format(round_num, loss))
round 0, loss=21.60552406311035
round 1, loss=20.365678787231445
round 2, loss=19.27480125427246
round 3, loss=18.31110954284668
round 4, loss=17.45725440979004

Der Vollständigkeit halber lassen Sie uns nun auch die Testdaten ausführen, um zu bestätigen, dass unser Modell gut verallgemeinert.

print('initial_model test loss =',
      federated_eval(initial_model, federated_test_data))
print('trained_model test loss =', federated_eval(model, federated_test_data))
initial_model test loss = 22.795593
trained_model test loss = 17.278767

Damit ist unser Tutorial abgeschlossen.

Natürlich spiegelt unser vereinfachtes Beispiel eine Reihe von Dingen nicht wider, die Sie in einem realistischeren Szenario tun müssten – zum Beispiel haben wir keine anderen Metriken als den Verlust berechnet. Wir empfehlen Ihnen , zu studieren , die Implementierung von föderierten Lungs in tff.learning als ein vollständigeres Beispiel, und als eine Möglichkeit , einige der Programmierpraktiken zu zeigen , wir fördern möchten.