**NOTE**: This colab has been verified to work with the [latest released version](https://github.com/tensorflow/federated#compatibility) of the `tensorflow_federated` pip package, but the Tensorflow Federated project is still in pre-release development and may not work on `master`.
%% Cell type:markdown id: tags:
# Overview
In the [image classification](federated_learning_for_image_classification.ipynb) and
[text generation](federated_learning_for_text_generation.ipynb) tutorials, we learned how to set up model and data pipelines for Federated Learning (FL), and performed federated training via the `tff.learning` API layer of TFF.
This is only the tip of the iceberg when it comes to FL research. In this tutorial, we discuss how to implement federated learning algorithms *without* deferring to the `tff.learning` API. We aim to accomplish the following:
**Goals:**
* Understand the general structure of federated learning algorithms.
* Explore the *Federated Core* of TFF.
* Use the Federated Core to implement Federated Averaging directly.
While this tutorial is self-contained, we recommend first reading the [image classification](federated_learning_for_image_classification.ipynb) and
We first load and preprocess the EMNIST dataset included in TFF. For more details, see the [image classification](federated_learning_for_image_classification.ipynb) tutorial.
We use the same model as in the [image classification](federated_learning_for_image_classification.ipynb) tutorial. This model (implemented via `tf.keras`) has a single hidden layer, followed by a softmax layer.
In order to use this model in TFF, we wrap the Keras model as a [`tff.learning.Model`](https://www.tensorflow.org/federated/api_docs/python/tff/learning/Model). This allows us to perform the model's [forward pass](https://www.tensorflow.org/federated/api_docs/python/tff/learning/Model#forward_pass) within TFF, and [extract model outputs](https://www.tensorflow.org/federated/api_docs/python/tff/learning/Model#report_local_outputs). For more details, also see the [image classification](federated_learning_for_image_classification.ipynb) tutorial.
While we used `tf.keras` to create a `tff.learning.Model`, TFF supports much more general models. These models have the following relevant attributes capturing the model weights:
*`trainable_variables`: An iterable of the tensors corresponding to trainable layers.
*`non_trainable_variables`: An iterable of the tensors corresponding to non-trainable layers.
For our purposes, we will only use the `trainable_variables`. (as our model only has those!).
%% Cell type:markdown id: tags:
# Building your own Federated Learning algorithm
While the `tff.learning` API allows one to create many variants of Federated Averaging, there are other federated algorithms that do not fit neatly into this framework. For example, you may want to add regularization, clipping, or more complicated algorithms such as [federated GAN training](https://github.com/tensorflow/federated/tree/master/tensorflow_federated/python/research/gans). You may also be instead be interested in [federated analytics](https://ai.googleblog.com/2020/05/federated-analytics-collaborative-data.html).
%% Cell type:markdown id: tags:
For these more advanced algorithms, we'll have to write our own custom algorithm using TFF. In many cases, federated algorithms have 4 main components:
1. A server-to-client broadcast step.
2. A local client update step.
3. A client-to-server upload step.
4. A server update step.
%% Cell type:markdown id: tags:
In TFF, we generally represent federated algorithms as a [`tff.templates.IterativeProcess`](https://www.tensorflow.org/federated/api_docs/python/tff/templates/IterativeProcess)(which we refer to as just an `IterativeProcess` throughout). This is a class that contains `initialize` and `next` functions. Here, `initialize` is used to initialize the server, and `next` will perform one communication round of the federated algorithm. Let's write a skeleton of what our iterative process for FedAvg should look like.
First, we have an initialize function that simply creates a `tff.learning.Model`, and returns its trainable weights.
%% Cell type:code id: tags:
```
definitialize_fn():
model=model_fn()
returnmodel.trainable_variables
```
%% Cell type:markdown id: tags:
This function looks good, but as we will see later, we will need to make a small modification to make it a "TFF computation".
We'll focus on implementing these four components separately. We first focus on the parts that can be implemented in pure TensorFlow, namely the client and server update steps.
%% Cell type:markdown id: tags:
## TensorFlow Blocks
%% Cell type:markdown id: tags:
### Client update
We will use our `tff.learning.Model` to do client training in essentially the same way you would train a TensorFlow model. In particular, we will use `tf.GradientTape` to compute the gradient on batches of data, then apply these gradient using a `client_optimizer`. We focus only on the trainable weights.
The server update for FedAvg is simpler than the client update. We will implement "vanilla" federated averaging, in which we simply replace the server model weights by the average of the client model weights. Again, we only focus on the trainable weights.
%% Cell type:code id: tags:
```
@tf.function
def server_update(model, mean_client_weights):
"""Updates the server model weights as the average of the client model weights."""
model_weights = model.trainable_variables
# Assign the mean client weights to the server model.
tf.nest.map_structure(lambda x, y: x.assign(y),
model_weights, mean_client_weights)
return model_weights
```
%% Cell type:markdown id: tags:
The snippet could be simplified by simply returning the `mean_client_weights`. However, more advanced implementations of Federated Averaging use `mean_client_weights` with more sophisticated techniques, such as momentum or adaptivity.
**Challenge**: Implement a version of `server_update` that updates the server weights to be the midpoint of model_weights and mean_client_weights. (Note: This kind of "midpoint" approach is analogous to recent work on the [Lookahead optimizer](https://arxiv.org/abs/1907.08610)!).
%% Cell type:markdown id: tags:
So far, we've only written pure TensorFlow code. This is by design, as TFF allows you to use much of the TensorFlow code you're already familiar with. However, now we have to specify the **orchestration logic**, that is, the logic that dictates what the server broadcasts to the client, and what the client uploads to the server.
This will require the *Federated Core* of TFF.
%% Cell type:markdown id: tags:
# Introduction to the Federated Core
The Federated Core (FC) is a set of lower-level interfaces that serve as the foundation for the `tff.learning` API. However, these interfaces are not limited to learning. In fact, they can be used for analytics and many other computations over distributed data.
At a high-level, the federated core is a development environment that enables compactly expressed program logic to combine TensorFlow code with distributed communication operators (such as distributed sums and broadcasts). The goal is to give researchers and practitioners expliict control over the distributed communication in their systems, without requiring system implementation details (such as specifying point-to-point network message exchanges).
One key point is that TFF is designed for privacy-preservation. Therefore, it allows explicit control over where data resides, to prevent unwanted accumulation of data at the centralized server location.
%% Cell type:markdown id: tags:
## Federated data
A key concept in TFF is "federated data", which refers to a collection of data items hosted across a group of devices in a distributed system (eg. client datasets, or the server model weights). We model the entire collection of data items across all devices as a single *federated value*.
For example, suppose we have client devices that each have a float representing the temperature of a sensor. We could represent it as a *federated float* by
Federated types are specified by a type `T` of its member constituents (eg. `tf.float32`) and a group `G` of devices. We will focus on the cases where `G` is either `tff.CLIENTS` or `tff.SERVER`. Such a federated type is represented as `{T}@G`, as shown below.
%% Cell type:code id: tags:
```
str(federated_float_on_clients)
```
%% Output
'{float32}@CLIENTS'
%% Cell type:markdown id: tags:
Why do we care so much about placements? A key goal of TFF is to enable writing code that could be deployed on a real distributed system. This means that it is vital to reason about which subsets of devices execute which code, and where different pieces of data reside.
TFF focuses on three things: *data*, where the data is *placed*, and how the data is being *transformed*. The first two are encapsulated in federated types, while the last is encapsulated in *federated computations*.
%% Cell type:markdown id: tags:
## Federated computations
%% Cell type:markdown id: tags:
TFF is a strongly-typed functional programming environment whose basic units are *federated computations*. These are pieces of logic that accept federated values as input, and return federated values as output.
For example, suppose we wanted to average the temperatures on our client sensors. We could define the following (using our federated float):
You might ask, how is this different from the `tf.function` decorator in TensorFlow? The key answer is that the code generated by `tff.federated_computation` is neither TensorFlow nor Python code; It is a specification of a distributed system in an internal platform-independent *glue language*.
While this may sound complicated, you can think of TFF computations as functions with well-defined type signatures. These type signatures can be directly queried.
%% Cell type:code id: tags:
```
str(get_average_temperature.type_signature)
```
%% Output
'({float32}@CLIENTS -> float32@SERVER)'
%% Cell type:markdown id: tags:
This `tff.federated_computation` accepts arguments of federated type `{float32}@CLIENTS`, and returns values of federated type `{float32}@SERVER`. Federated computations may also go from server to client, from client to client, or from server to server. Federated computations can also be composed like normal functions, as long as their type signatures match up.
To support development, TFF allows you to invoke a `tff.federated_computation` as a Python function. For example, we can call
%% Cell type:code id: tags:
```
get_average_temperature([68.5, 70.3, 69.8])
```
%% Output
69.53334
%% Cell type:markdown id: tags:
## Non-eager computations and TensorFlow
%% Cell type:markdown id: tags:
There are two key restrictions to be aware of. First, when the Python interpreter encounters a `tff.federated_computation` decorator, the function is traced once and serialized for future use. Due to the decentralized nature of Federated Learning, this future usage may occur elsewhere, such as a remote execution environment. Therefore, TFF computations are fundamentally *non-eager*. This behavior is somewhat analogous to that of the [`tf.function`](https://www.tensorflow.org/api_docs/python/tf/function) decorator in TensorFlow.
Second, a federated computation can only consist of federated operators (such as `tff.federated_mean`), they cannot contain TensorFlow operations. TensorFlow code must be confined to blocks decorated with `tff.tf_computation`. Most ordinary TensorFlow code can be directly decorated, such as the following function that takes a number and adds `0.5` to it.
%% Cell type:code id: tags:
```
@tff.tf_computation(tf.float32)
def add_half(x):
return tf.add(x, 0.5)
```
%% Cell type:markdown id: tags:
These also have type signatures, but *without placements*. For example, we can call
%% Cell type:code id: tags:
```
str(add_half.type_signature)
```
%% Output
'(float32 -> float32)'
%% Cell type:markdown id: tags:
Here we see an important difference between `tff.federated_computation` and `tff.tf_computation`. The former has explicit placements, while the latter does not.
We can use `tff.tf_computation` blocks in federated computations by specifying placements. Let's create a function that adds half, but only to federated floats at the clients. We can do this by using `tff.federated_map`, which applies a given `tff.tf_computation`, while preserving the placement.
This function is almost identical to `add_half`, except that it only accepts values with placement at `tff.CLIENTS`, and returns values with the same placement. We can see this in its type signature:
%% Cell type:code id: tags:
```
str(add_half_on_clients.type_signature)
```
%% Output
'({float32}@CLIENTS -> {float32}@CLIENTS)'
%% Cell type:markdown id: tags:
In summary:
* TFF operates on federated values.
* Each federated value has a *federated type*, with a *type* (eg. `tf.float32`) and a *placement* (eg. `tff.CLIENTS`).
* Federated values can be transformed using *federated computations*, which must be decorated with `tff.federated_computation` and a federated type signature.
* TensorFlow code must be contained in blocks with `tff.tf_computation` decorators.
* These blocks can then be incorporated into federated computations.
%% Cell type:markdown id: tags:
# Building your own Federated Learning algorithm, revisited
Now that we've gotten a glimpse of the Federated Core, we can build our own federated learning algorithm. Remember that above, we defined an `initialize_fn` and `next_fn` for our algorithm. The `next_fn` will make use of the `client_update` and `server_update` we defined using pure TensorFlow code.
However, in order to make our algorithm a federated computation, we will need both the `next_fn` and `initialize_fn` to each be a `tff.federated_computation`.
%% Cell type:markdown id: tags:
## TensorFlow Federated blocks
%% Cell type:markdown id: tags:
### Creating the initialization computation
The initialize function will be quite simple: We will create a model using `model_fn`. However, remember that we must separate out our TensorFlow code using `tff.tf_computation`.
%% Cell type:code id: tags:
```
@tff.tf_computation
defserver_init():
model=model_fn()
returnmodel.trainable_variables
```
%% Cell type:markdown id: tags:
We can then pass this directly into a federated computation using `tff.federated_value`.
We now use our client and server update code to write the actual algorithm. We will first turn our `client_update` into a `tff.tf_computation` that accepts a client datasets and server weights, and outputs an updated client weights tensor.
We will need the corresponding types to properly decorate our function. Luckily, the type of the server weights can be extracted directly from our model.
The `tff.tf_computation` version of the server update can be defined in a similar way, using types we've already extracted.
%% Cell type:code id: tags:
```
@tff.tf_computation(model_weights_type)
defserver_update_fn(mean_client_weights):
model=model_fn()
returnserver_update(model,mean_client_weights)
```
%% Cell type:markdown id: tags:
Last, but not least, we need to create the `tff.federated_computation` that brings this all together. This function will accept two *federated values*, one corresponding to the server weights (with placement `tff.SERVER`), and the other corresponding to the client datasets (with placement `tff.CLIENTS`).
Note that both these types were defined above! We simply need to give them the proper placement using `tff.FederatedType`.
Now that we've built up the above, each part can be compactly represented as a single line of TFF code. This simplicity is why we had to take extra care to specify things such as federated types!
We now have a `tff.federated_computation` for both the algorithm initialization, and for running one step of the algorithm. To finish our algorithm, we pass these into `tff.templates.IterativeProcess`.
This reflects the fact that `federated_algorithm.initialize` is a no-arg function that returns a single-layer model (with a 784-by-10 weight matrix, and 10 bias units).
Here, we see that `federated_algorithm.next` accepts a server model and client data, and returns an updated server model.
%% Cell type:markdown id: tags:
## Evaluating the algorithm
%% Cell type:markdown id: tags:
Let's run a few rounds, and see how the loss changes. First, we will define an evaluation function using the *centralized* approach discussed in the second tutorial.
We first create a centralized evaluation dataset, and then apply the same preprocessing we used for the training data.
Note that we only `take` the first 1000 elements for reasons of computational efficiency, but typically we'd use the entire test dataset.
Next, we write a function that accepts a server state, and uses Keras to evaluate on the test dataset. If you're familiar with `tf.Keras`, this will all look familiar, though note the use of `set_weights`!
We see a slight decrease in the loss function. While the jump is small, we've only performed 10 training rounds, and on a small subset of clients. To see better results, we may have to do hundreds if not thousands of rounds.
%% Cell type:markdown id: tags:
## Modifying our algorithm
%% Cell type:markdown id: tags:
At this point, let's stop and think about what we've accomplished. We've implemented Federated Averaging directly by combining pure TensorFlow code (for the client and server updates) with federated computations from the Federated Core of TFF.
To perform more sophisticted learning, we can simply alter what we have above. In particular, by editing the pure TF code above, we can change how the client performs training, or how the server updates its model.
**Challenge:** Add [gradient clipping](https://towardsdatascience.com/what-is-gradient-clipping-b8e815cdfb48) to the `client_update` function.
%% Cell type:markdown id: tags:
If we wanted to make larger changes, we could also have the server store and broadcast more data. For example, the server could also store the client learning rate, and make it decay over time! Note that this will require changes to the type signatures used in the `tff.tf_computation` calls above.
**Harder Challenge:** Implement Federated Averaging with learning rate decay on the clients.
At this point, you may begin to realize how much flexibility there is in what you can implement in this framework. For ideas (including the answer to the harder challenge above) you can see the source-code for [`tff.learning.build_federated_averaging_process`](https://www.tensorflow.org/federated/api_docs/python/tff/learning/build_federated_averaging_process), or check out various [research projects](https://github.com/google-research/federated) using TFF.