Algoritmos federados personalizados, parte 1: Introducción al núcleo federado

Ver en TensorFlow.org Ejecutar en Google Colab Ver fuente en GitHub Descargar cuaderno

Este tutorial es la primera parte de una serie de dos partes que muestra cómo implementar tipos personalizados de algoritmos federados en TensorFlow Federados (TFF) utilizando el Federados Core (FC) - un conjunto de interfaces de nivel inferior que sirven de base sobre la cual hemos puesto en marcha el Federados de aprendizaje (FL) capa.

Esta primera parte es más conceptual; presentamos algunos de los conceptos clave y abstracciones de programación utilizados en TFF, y demostramos su uso en un ejemplo muy simple con una matriz distribuida de sensores de temperatura. En la segunda parte de esta serie , se utilizan los mecanismos que introducimos aquí para poner en práctica una versión simple de algoritmos de entrenamiento y evaluación federados. Como seguimiento, le recomendamos que para estudiar la aplicación de un promedio de federados en tff.learning .

Al final de esta serie, debería poder reconocer que las aplicaciones de Federated Core no se limitan necesariamente al aprendizaje. Las abstracciones de programación que ofrecemos son bastante genéricas y podrían usarse, por ejemplo, para implementar análisis y otros tipos de cálculos personalizados sobre datos distribuidos.

Aunque este tutorial está diseñado para ser autónomo, le animamos a primeros tutoriales de lectura en la clasificación de imágenes y la generación de texto de un nivel más alto y una introducción más suave para el marco TensorFlow federados y el Federados de aprendizaje API ( tff.learning ), como le ayudará a poner los conceptos que describimos aquí en contexto.

Usos previstos

En pocas palabras, Federated Core (FC) es un entorno de desarrollo que hace que sea posible expresar de forma compacta la lógica del programa que el código cosechadoras TensorFlow con los operadores distribuidos de comunicación, tales como los que se utilizan en Federated Averaging - calcular sumas distribuidas, promedios y otros tipos de agregaciones distribuidas sobre un conjunto de dispositivos cliente en el sistema, modelos y parámetros de transmisión a esos dispositivos, etc.

Usted puede ser consciente de tf.contrib.distribute , y una pregunta natural en este punto puede ser: ¿de qué manera se diferencia de este marco? Ambos marcos intentan distribuir los cálculos de TensorFlow, después de todo.

Una forma de pensar en ello es que, mientras que el objetivo declarado de tf.contrib.distribute es permitir a los usuarios utilizar los modelos existentes y el código de entrenamiento con cambios mínimos para permitir la formación distribuida, y mucho foco está en la forma de aprovechar la infraestructura distribuida Para hacer que el código de capacitación existente sea más eficiente, el objetivo del núcleo federado de TFF es brindar a los investigadores y profesionales un control explícito sobre los patrones específicos de comunicación distribuida que usarán en sus sistemas. El enfoque en FC es proporcionar un lenguaje flexible y extensible para expresar algoritmos de flujo de datos distribuidos, en lugar de un conjunto concreto de capacidades de capacitación distribuidas implementadas.

Uno de los principales destinatarios de la API de FC de TFF son los investigadores y profesionales que quieran experimentar con nuevos algoritmos de aprendizaje federado y evaluar las consecuencias de las opciones de diseño sutiles que afectan la forma en que se orquesta el flujo de datos en el sistema distribuido, pero sin atascarse con los detalles de implementación del sistema. El nivel de abstracción al que apunta FC API corresponde aproximadamente al pseudocódigo que se podría usar para describir la mecánica de un algoritmo de aprendizaje federado en una publicación de investigación: qué datos existen en el sistema y cómo se transforman, pero sin caer al nivel de intercambios individuales de mensajes de red punto a punto.

La TFF en su conjunto tiene como objetivo escenarios en los que los datos se distribuyen y deben seguir siéndolo, por ejemplo, por razones de privacidad, y donde la recopilación de todos los datos en una ubicación centralizada puede no ser una opción viable. Esto tiene implicaciones en la implementación de algoritmos de aprendizaje automático que requieren un mayor grado de control explícito, en comparación con escenarios en los que todos los datos se pueden acumular en una ubicación centralizada en un centro de datos.

Antes que empecemos

Antes de sumergirnos en el código, intente ejecutar el siguiente ejemplo de "Hola mundo" para asegurarse de que su entorno esté configurado correctamente. Si esto no funciona, por favor refiérase a la instalación de guía para obtener instrucciones.

!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
@tff.federated_computation
def hello_world():
  return 'Hello, World!'

hello_world()
b'Hello, World!'

Datos federados

Una de las características distintivas de la TFF es que permite que usted se exprese de forma compacta cálculos basados en los datos federados TensorFlow. Nosotros vamos a usar los datos federados plazo en este tutorial para hacer referencia a una colección de elementos de datos recibidos a través de un grupo de dispositivos en un sistema distribuido. Por ejemplo, las aplicaciones que se ejecutan en dispositivos móviles pueden recopilar datos y almacenarlos localmente, sin cargarlos en una ubicación centralizada. O una serie de sensores distribuidos puede recopilar y almacenar lecturas de temperatura en sus ubicaciones.

Datos federados como los de los ejemplos anteriores son tratados en TFF como ciudadanos de primera clase , es decir, que pueden aparecer como parámetros y resultados de las funciones, y tienen tipos. Para reforzar esta idea, nos referiremos a los conjuntos de datos federados como valores federados, o como valores de tipos federados.

El punto importante a comprender es que estamos modelando la colección completa de elementos de datos en todos los dispositivos (por ejemplo, las lecturas de temperatura de la colección completa de todos los sensores en una matriz distribuida) como un solo valor federado.

Por ejemplo, aquí es cómo se podría definir de TFF del tipo de un flotador federada organizada por un grupo de dispositivos de cliente. Una colección de lecturas de temperatura que se materializan a través de una matriz de sensores distribuidos podría modelarse como un valor de este tipo federado.

federated_float_on_clients = tff.type_at_clients(tf.float32)

Más en general, un tipo federados en TFF se define especificando el tipo T de sus constituyentes miembro - los elementos de datos que residen en dispositivos individuales, y el grupo G de dispositivos en los que se alojan los valores federados de este tipo (más una tercera, información opcional que mencionaremos en breve). Nos referimos al grupo G de los dispositivos de alojamiento un valor federados como la colocación del valor. Por lo tanto, tff.CLIENTS es un ejemplo de una colocación.

str(federated_float_on_clients.member)
'float32'
str(federated_float_on_clients.placement)
'CLIENTS'

Un tipo federada con constituyentes miembro T y la colocación G puede ser representado de forma compacta como {T}@G , como se muestra a continuación.

str(federated_float_on_clients)
'{float32}@CLIENTS'

Las llaves {} en esta concisa notación sirven como un recordatorio de que los constituyentes miembros (artículos de datos en diferentes dispositivos) pueden diferir, como era de esperar, por ejemplo, de las lecturas de los sensores de temperatura, por lo que los clientes, como grupo, están organizando conjuntamente un múltiple -accione de T elementos que en conjunto constituyen el valor federados -typed.

Es importante señalar que los constituyentes miembros de un valor federados son generalmente opaca para el programador, es decir, un valor federados no deben ser considerados como un simple dict tecleado por un identificador de un dispositivo en el sistema - estos valores están destinadas a transformarse colectivamente únicamente por los operadores federadas que representan abstractamente diversos tipos de protocolos de comunicación distribuidos (tales como la agregación). Si esto suena demasiado abstracto, no se preocupe, volveremos a esto en breve y lo ilustraremos con ejemplos concretos.

Los tipos federados en TFF vienen en dos sabores: aquellos en los que los componentes miembros de un valor federado pueden diferir (como se acaba de ver arriba) y aquellos en los que se sabe que son todos iguales. Esto es controlado por la tercera, opcional all_equal parámetro en el tff.FederatedType constructor (por defecto a False ).

federated_float_on_clients.all_equal
False

Un tipo federada con una colocación G en la que la totalidad de la T -typed constituyentes miembro son conocidos por ser igual puede ser representado de forma compacta como T@G (en contraposición a {T}@G , es decir, con las llaves se redujo a reflejar el hecho de que el conjunto múltiple de componentes de miembros consta de un solo elemento).

str(tff.type_at_clients(tf.float32, all_equal=True))
'float32@CLIENTS'

Un ejemplo de un valor federado de este tipo que podría surgir en escenarios prácticos es un hiperparámetro (como una tasa de aprendizaje, una norma de recorte, etc.) que ha sido transmitido por un servidor a un grupo de dispositivos que participan en un entrenamiento federado.

Otro ejemplo es un conjunto de parámetros para un modelo de aprendizaje automático previamente entrenado en el servidor, que luego se transmitieron a un grupo de dispositivos cliente, donde se pueden personalizar para cada usuario.

Por ejemplo, supongamos que tenemos un par de float32 parámetros a y b para un modelo de regresión lineal simple de una sola dimensión. Podemos construir el tipo (no federado) de dichos modelos para usar en TFF de la siguiente manera. Los soportes en ángulo <> en la cadena de tipo impreso son una notación compacta para TFF tuplas con o sin nombre.

simple_regression_model_type = (
    tff.StructType([('a', tf.float32), ('b', tf.float32)]))

str(simple_regression_model_type)
'<a=float32,b=float32>'

Tenga en cuenta que estamos especificando únicamente dtype s arriba. También se admiten los tipos no escalares. En el código anterior, tf.float32 es una notación acceso directo para el más general tff.TensorType(dtype=tf.float32, shape=[]) .

Cuando este modelo se transmite a los clientes, el tipo de valor federado resultante se puede representar como se muestra a continuación.

str(tff.type_at_clients(
    simple_regression_model_type, all_equal=True))
'<a=float32,b=float32>@CLIENTS'

Por simetría con el flotador federados anteriormente, nos referiremos a este tipo una como una tupla federado. De manera más general, vamos a menudo utilizamos el XYZ federados término para referirse a un valor federados en los que los constituyentes son miembros -como XYZ. Por lo tanto, vamos a hablar de cosas como tuplas federados, secuencias federados, modelos federados, y así sucesivamente.

Ahora, de vuelta a venir float32@CLIENTS - Aunque parece replicarse a través de múltiples dispositivos, en realidad es una sola float32 , ya que todos los miembros son iguales. En general, se puede pensar en cualquier tipo federados todo-iguales, es decir, una de la forma T@G , como isomorfo a un tipo no federados T , ya que en ambos casos, en realidad hay solamente un solo elemento (aunque potencialmente replicado) de tipo T .

Dado el isomorfismo entre T y T@G , usted puede preguntarse qué propósito, en su caso, los últimos tipos podrían servir. Sigue leyendo.

Ubicaciones

Descripción general del diseño

En la sección anterior, hemos introducido el concepto de colocaciones - grupos de participantes en el sistema que pueden ser conjuntamente un valor de alojamiento federados, y hemos demostrado el uso de tff.CLIENTS como una especificación de ejemplo de una colocación.

Para explicar por qué la noción de una colocación es tan fundamental que era necesario incorporar en el sistema de tipos de TFF, recordamos lo que hemos mencionado al principio de este tutorial sobre algunos de los usos previstos de la TFF.

Aunque en este tutorial, solo verá el código TFF que se ejecuta localmente en un entorno simulado, nuestro objetivo es que TFF habilite la escritura de código que podría implementar para su ejecución en grupos de dispositivos físicos en un sistema distribuido, incluyendo potencialmente dispositivos móviles o integrados. ejecutando Android. Cada uno de esos dispositivos recibiría un conjunto separado de instrucciones para ejecutar localmente, según el papel que desempeña en el sistema (un dispositivo de usuario final, un coordinador centralizado, una capa intermedia en una arquitectura de varios niveles, etc.). Es importante poder razonar sobre qué subconjuntos de dispositivos ejecutan qué código y dónde pueden materializarse físicamente diferentes partes de los datos.

Esto es especialmente importante cuando se trata, por ejemplo, de datos de aplicaciones en dispositivos móviles. Dado que los datos son privados y pueden ser confidenciales, necesitamos la capacidad de verificar estáticamente que estos datos nunca saldrán del dispositivo (y probar hechos sobre cómo se procesan los datos). Las especificaciones de ubicación son uno de los mecanismos diseñados para respaldar esto.

TFF ha sido diseñado como un entorno de programación centrada en los datos, y como tal, a diferencia de algunos de los marcos existentes que se centran en las operaciones y donde esas operaciones podrían funcionar, TFF se centra en los datos, en caso que se materializa de datos, y cómo se está transformando. En consecuencia, la ubicación se modela como una propiedad de los datos en TFF, más que como una propiedad de las operaciones sobre los datos. De hecho, como verá en la siguiente sección, algunas de las operaciones de TFF se extienden a través de ubicaciones y se ejecutan "en la red", por así decirlo, en lugar de ser ejecutadas por una sola máquina o un grupo de máquinas.

Que representa el tipo de un cierto valor como T@G o {T}@G (en lugar de sólo T ) hace que las decisiones de colocación de datos explícita, y junto con un análisis estático de programas escritos en TFF, que puede servir como una base para proporcionar garantías formales de privacidad para datos confidenciales en el dispositivo.

Una cosa importante a destacar en este punto, sin embargo, es que mientras que animamos a los usuarios TFF ser explícito acerca de los grupos de dispositivos participantes que albergan los datos (las ubicaciones), el programador no hacer frente a los datos en bruto o identidades de los participantes individuales .

Dentro del cuerpo del código de TFF, por diseño, no hay manera de enumerar los dispositivos que constituyen el grupo representado por tff.CLIENTS , o sonda para la existencia de un dispositivo específico en el grupo. No existe el concepto de un dispositivo o identidad de cliente en ninguna parte de la API central federada, el conjunto subyacente de abstracciones arquitectónicas o la infraestructura de tiempo de ejecución central que proporcionamos para respaldar las simulaciones. Toda la lógica de cálculo que escriba se expresará como operaciones en todo el grupo de clientes.

Recordemos aquí lo que hemos mencionado anteriormente acerca de los valores de tipos federados son a diferencia de Python dict , en la que uno no puede simplemente enumerar sus constituyentes miembros. Piense en los valores que manipula la lógica de su programa TFF como asociados con ubicaciones (grupos), en lugar de con participantes individuales.

Las ubicaciones están diseñados para ser un ciudadano de primera clase en TFF también, y pueden aparecer como parámetros y resultados de una placement de tipo (a ser representados por tff.PlacementType en el API). En el futuro, planeamos proporcionar una variedad de operadores para transformar o combinar ubicaciones, pero esto está fuera del alcance de este tutorial. Por ahora, basta con pensar en placement como una opaca primitiva tipo incorporado en TFF, similar a cómo int y bool se opaca tipos incorporados en Python, con tff.CLIENTS ser un literal constante de este tipo, no muy diferente de 1 ser un literal constante de tipo int .

Especificar ubicaciones

TFF ofrece dos literales de colocación básicos, tff.CLIENTS y tff.SERVER , para que sea fácil de expresar la rica variedad de escenarios prácticos que se modelan de forma natural como las arquitecturas cliente-servidor, con múltiples dispositivos de cliente (teléfonos móviles, dispositivos integrados, bases de datos distribuidas , sensores, etc.) orquestada por un solo coordinador servidor centralizado. TFF también está diseñado para admitir ubicaciones personalizadas, múltiples grupos de clientes, arquitecturas de múltiples niveles y otras arquitecturas distribuidas más generales, pero discutirlas está fuera del alcance de este tutorial.

TFF no prescribe lo que cualquiera de los tff.CLIENTS o la tff.SERVER representan en realidad.

En particular, tff.SERVER puede ser un dispositivo físico único (un miembro de un grupo Singleton), pero podría muy bien ser un grupo de réplicas en una réplica de la máquina de estado del clúster con tolerancia a fallos corriendo - no hacemos ninguna arquitectónico especial supuestos. Por el contrario, se utiliza el all_equal poco mencionado en la sección anterior para expresar el hecho de que en general estamos frente a un solo elemento de datos en el servidor.

Del mismo modo, tff.CLIENTS en algunas aplicaciones podrían representar a todos los clientes en el sistema - lo que en el contexto del aprendizaje federada a veces nos referimos como la población, pero por ejemplo, en las implementaciones de producción de Federados de promedio , puede representar una cohorte - un subconjunto de los clientes seleccionados para participar en una ronda particular de capacitación. Las ubicaciones definidas de manera abstracta reciben un significado concreto cuando un cálculo en el que aparecen se implementa para su ejecución (o simplemente se invoca como una función de Python en un entorno simulado, como se demuestra en este tutorial). En nuestras simulaciones locales, el grupo de clientes está determinado por los datos federados proporcionados como entrada.

Cálculos federados

Declarar cálculos federados

TFF está diseñado como un entorno de programación funcional fuertemente tipado que admite el desarrollo modular.

La unidad básica de la composición en TFF es un cálculo federados - una sección de lógica que puede aceptar los valores federados como entrada y devolver valores federados como salida. A continuación, le mostramos cómo puede definir un cálculo que calcule el promedio de las temperaturas informadas por la matriz de sensores de nuestro ejemplo anterior.

@tff.federated_computation(tff.type_at_clients(tf.float32))
def get_average_temperature(sensor_readings):
  return tff.federated_mean(sensor_readings)

Mirando el código anterior, en este punto se puede preguntar - No hay ya decorador de construcciones para definir unidades componibles como tf.function en TensorFlow, y si es así, ¿por qué introducir otro más, y en qué se diferencia?

La respuesta corta es que el código generado por el tff.federated_computation envoltorio no es ni TensorFlow, ni es Python - es una especificación de un sistema distribuido en un lenguaje independiente de la plataforma de cola interna. En este punto, esto sin duda parecerá críptico, pero tenga en cuenta esta interpretación intuitiva de una computación federada como una especificación abstracta de un sistema distribuido. Te lo explicaremos en un minuto.

Primero, juguemos un poco con la definición. Los cálculos de TFF generalmente se modelan como funciones, con o sin parámetros, pero con firmas de tipo bien definidas. Puede imprimir la declaración de tipo de un cálculo mediante la consulta de su type_signature propiedad, como se muestra a continuación.

str(get_average_temperature.type_signature)
'({float32}@CLIENTS -> float32@SERVER)'

La firma de tipo nos dice que el cálculo acepta una colección de diferentes lecturas de sensores en los dispositivos del cliente y devuelve un solo promedio en el servidor.

Antes de seguir adelante, vamos a reflexionar sobre esto por un minuto - la entrada y salida de este cálculo están en diferentes lugares (sobre CLIENTS frente al SERVER ). Recordemos lo que hemos dicho en el apartado anterior en las colocaciones sobre cómo las operaciones de TFF pueden extenderse a través de lugares, y se ejecutan en la red, y lo que acabamos de decir sobre los cálculos federados como la representación de las especificaciones abstractas de los sistemas distribuidos. Acabamos de definir uno de esos cálculos: un sistema distribuido simple en el que los datos se consumen en los dispositivos del cliente y los resultados agregados emergen en el servidor.

En muchas situaciones prácticas, los cálculos que representan las tareas de nivel superior tenderán a aceptar sus entradas e informar de sus resultados en el servidor - esto refleja la idea de que los cálculos podrían ser provocados por solicitudes que se originan y terminan en el servidor.

Sin embargo, el FC API no impone este supuesto, y muchos de los bloques de construcción que utiliza internamente (incluyendo numerosos tff.federated_... operadores que puede encontrar en la API) tienen entradas y salidas con ubicaciones distintas, por lo que en general, se debe no piense en un cálculo federados como algo que se ejecuta en el servidor o es ejecutado por un servidor. El servidor es solo un tipo de participante en un cálculo federado. Al pensar en la mecánica de tales cálculos, es mejor utilizar siempre de forma predeterminada la perspectiva global de la red, en lugar de la perspectiva de un único coordinador centralizado.

En general, las firmas de tipos funcionales se compacta representados como (T -> U) para los tipos T y U de entradas y salidas, respectivamente. El tipo del parámetro formal (tales sensor_readings en este caso) se especifica como el argumento para el decorador. No es necesario especificar el tipo de resultado, se determina automáticamente.

Aunque TFF ofrece formas limitadas de polimorfismo, se recomienda encarecidamente a los programadores que sean explícitos sobre los tipos de datos con los que trabajan, ya que eso facilita la comprensión, la depuración y la verificación formal de las propiedades de su código. En algunos casos, la especificación explícita de tipos es un requisito (p. Ej., Los cálculos polimórficos actualmente no se pueden ejecutar directamente).

Ejecución de cálculos federados

Para respaldar el desarrollo y la depuración, TFF le permite invocar directamente los cálculos definidos de esta manera como funciones de Python, como se muestra a continuación. Cuando el cómputo espera un valor de un tipo federada con la all_equal establecer el bit en False , se puede alimentar como una llanura list en Python, y para este tipo de federados con el all_equal bit igual a True , sólo puede alimentar directamente a la (única) constituyente miembro. Así es también como se le informan los resultados.

get_average_temperature([68.5, 70.3, 69.8])
69.53334

Al ejecutar cálculos como este en modo de simulación, actúa como un observador externo con una vista de todo el sistema, que tiene la capacidad de suministrar entradas y consumir salidas en cualquier ubicación de la red, como de hecho es el caso aquí: proporcionó valores de cliente en la entrada y consumió el resultado del servidor.

Ahora, Volvamos a una nota que hizo anteriormente acerca de la tff.federated_computation decorador de emisión de código en un lenguaje de pegamento. A pesar de que la lógica de los cálculos de FFT se puede expresar como funciones ordinarias en Python (sólo tiene que decorar con tff.federated_computation como lo hemos hecho anteriormente), y se puede invocar directamente con argumentos de Python al igual que cualquier otra función de Python en este cuaderno, detrás de las escenas, como hemos señalado anteriormente, los cálculos de FFT no son en realidad Python.

Lo que queremos decir con esto es que cuando el intérprete de Python se encuentra con una función decorado con tff.federated_computation , traza las declaraciones en el cuerpo de esta función una vez (en tiempo de definición), y luego construye una representación serializada de la lógica del cálculo para uso futuro - si para su ejecución, o para ser incorporado como un subcomponente en otro cálculo.

Puede verificar esto agregando una declaración impresa, de la siguiente manera:

@tff.federated_computation(tff.type_at_clients(tf.float32))
def get_average_temperature(sensor_readings):

  print ('Getting traced, the argument is "{}".'.format(
      type(sensor_readings).__name__))

  return tff.federated_mean(sensor_readings)
Getting traced, the argument is "ValueImpl".

Puede pensar en el código de Python que define un cálculo federado de manera similar a cómo pensaría en el código de Python que crea un gráfico de TensorFlow en un contexto no ansioso (si no está familiarizado con los usos no ansiosos de TensorFlow, piense en su Código Python que define un gráfico de operaciones que se ejecutarán más tarde, pero que en realidad no se ejecutarán sobre la marcha). El código de creación de gráficos no ansioso en TensorFlow es Python, pero el gráfico de TensorFlow construido por este código es independiente de la plataforma y serializable.

Del mismo modo, los cálculos de FFT se definen en Python, pero las sentencias de Python en sus cuerpos, tales como tff.federated_mean en el ejemplo WEVE simplemente muestran, se compilan en una representación serializable portátil y independiente de la plataforma bajo el capó.

Como desarrollador, no necesita preocuparse por los detalles de esta representación, ya que nunca necesitará trabajar directamente con ella, pero debe ser consciente de su existencia, el hecho de que los cálculos de TFF son fundamentalmente no ansiosos, y no puede capturar un estado de Python arbitrario. Código Python contenida en el cuerpo de un cálculo FFT se ejecuta en el momento de definición, cuando el cuerpo de la función Python decorado con tff.federated_computation se traza antes de ser serializado. No se vuelve a rastrear en el momento de la invocación (excepto cuando la función es polimórfica; consulte las páginas de documentación para obtener más detalles).

Quizás se pregunte por qué hemos elegido introducir una representación interna dedicada que no sea Python. Una razón es que, en última instancia, los cálculos de TFF están destinados a ser implementados en entornos físicos reales y alojados en dispositivos móviles o integrados, donde Python puede no estar disponible.

Otra razón es que los cálculos de TFF expresan el comportamiento global de los sistemas distribuidos, a diferencia de los programas de Python que expresan el comportamiento local de los participantes individuales. Se puede ver que en el sencillo ejemplo anterior, con el operador especial tff.federated_mean que acepta datos en los dispositivos cliente, pero los depósitos de los resultados en el servidor.

El operador tff.federated_mean no puede ser modelada fácilmente como un operador ordinaria en Python, ya que no se ejecuta de forma local - como se señaló anteriormente, representa un sistema distribuido que coordina el comportamiento de varios participantes en el sistema. Nos referiremos a estos operadores como operadores federados, para distinguirlos de los operadores normales (locales) en Python.

El sistema de tipo TFF, y el conjunto fundamental de operaciones admitidas en el lenguaje de TFF, se desvía significativamente de los de Python, lo que requiere el uso de una representación dedicada.

Componer cálculos federados

Como se señaló anteriormente, los cálculos federados y sus componentes se entienden mejor como modelos de sistemas distribuidos, y puede pensar en componer cálculos federados como componer sistemas distribuidos más complejos a partir de sistemas más simples. Se puede pensar en el tff.federated_mean operador como una especie de plantilla incorporados en el cálculo federada con una firma de tipo ({T}@CLIENTS -> T@SERVER) (de hecho, al igual que los cálculos se escribe, este operador también tiene un complejo estructura: debajo del capó lo dividimos en operadores más simples).

Lo mismo ocurre con la composición de cálculos federados. El cálculo get_average_temperature podrá ser invocado contra un cuerpo de otra función de Python decorado con tff.federated_computation - hacerlo hará que se puede incrustar en el cuerpo del padre, gran parte de la misma manera tff.federated_mean se incrustó en su propio cuerpo antes.

Una restricción importante a tener en cuenta es que los cuerpos de las funciones de Python decoradas con tff.federated_computation deben consistir únicamente de los operadores federados, es decir, que no pueden contener directamente las operaciones TensorFlow. Por ejemplo, no se puede utilizar directamente tf.nest interfaces de añadir un par de valores federados. TensorFlow código debe limitarse a bloques de código decoradas con un tff.tf_computation discute en la siguiente sección. Sólo cuando envuelto de esta manera puede el código TensorFlow envuelto ser invocada en el cuerpo de un tff.federated_computation .

Las razones de esta separación son de carácter técnico (que es difícil engañar a operadores como tf.add al trabajo con los no-tensores), así como arquitectónico. El lenguaje de cálculos federados (es decir, la lógica construida a partir de cuerpos serializada de las funciones de Python decoradas con tff.federated_computation ) está diseñado para servir como un lenguaje de pegamento independiente de la plataforma. Este lenguaje de pegamento se utiliza actualmente para los sistemas de generación distribuida a partir de secciones embebidas de código TensorFlow (confinado a tff.tf_computation bloques). En la plenitud del tiempo, anticipamos la necesidad de integrar las secciones de otra lógica, no TensorFlow, como las consultas de bases de datos relacionales que podrían representar las tuberías de entrada, todos conectados entre sí utilizando el mismo lenguaje de pegamento (los tff.federated_computation bloques).

Lógica de TensorFlow

Declarar cálculos de TensorFlow

TFF está diseñado para usarse con TensorFlow. Como tal, es probable que la mayor parte del código que escribirá en TFF sea código de TensorFlow ordinario (es decir, de ejecución local). Con el fin de utilizar dicho código con TFF, como se señaló anteriormente, sólo tiene que ser decorado con tff.tf_computation .

Por ejemplo, así es como podríamos implementar una función que toma un número y añade 0.5 a la misma.

@tff.tf_computation(tf.float32)
def add_half(x):
  return tf.add(x, 0.5)

Una vez más, mirando a esto, usted puede preguntarse por qué debemos definir otro decorador tff.tf_computation en lugar de simplemente utilizar un mecanismo existente, como tf.function . A diferencia de la sección anterior, aquí estamos tratando con un bloque ordinario de código de TensorFlow.

Hay algunas razones para esto, cuyo tratamiento completo va más allá del alcance de este tutorial, pero vale la pena nombrar la principal:

  • Para insertar bloques de creación reutilizables implementados con código TensorFlow en los cuerpos de los cálculos federados, deben satisfacer ciertas propiedades, como ser rastreados y serializados en el momento de la definición, tener firmas de tipos, etc. Esto generalmente requiere algún tipo de decorador.

En general, se recomienda utilizar los mecanismos nativos de TensorFlow para la composición, tales como tf.function , siempre que sea posible, ya que la manera exacta en que se puede esperar que interactúa decorador de TFF con funciones deseosos de evolucionar.

Ahora, volviendo al ejemplo de código fragmento anterior, el cálculo add_half que acabamos de definir puede ser tratada mediante FFT al igual que cualquier otro tipo de cálculo de FFT. En particular, tiene una firma de tipo TFF.

str(add_half.type_signature)
'(float32 -> float32)'

Tenga en cuenta que este tipo de firma no tiene ubicaciones. Los cálculos de TensorFlow no pueden consumir ni devolver tipos federados.

Ahora también puede utilizar add_half como un bloque de construcción en otros cálculos. Por ejemplo, aquí es cómo se puede utilizar el tff.federated_map operador para aplicar add_half puntual a todos los constituyentes miembros de un flotador federados en los dispositivos cliente.

@tff.federated_computation(tff.type_at_clients(tf.float32))
def add_half_on_clients(x):
  return tff.federated_map(add_half, x)
str(add_half_on_clients.type_signature)
'({float32}@CLIENTS -> {float32}@CLIENTS)'

Ejecutar cálculos de TensorFlow

Ejecución de cálculos definidos con tff.tf_computation sigue las mismas reglas que las describimos para tff.federated_computation . Se pueden invocar como invocables ordinarios en Python, de la siguiente manera.

add_half_on_clients([1.0, 3.0, 2.0])
[<tf.Tensor: shape=(), dtype=float32, numpy=1.5>,
 <tf.Tensor: shape=(), dtype=float32, numpy=3.5>,
 <tf.Tensor: shape=(), dtype=float32, numpy=2.5>]

Una vez más, vale la pena señalar que la invocación de la computación add_half_on_clients de esta manera simula un proceso distribuido. Los datos se consumen en los clientes y se devuelven a los clientes. De hecho, este cálculo hace que cada cliente realice una acción local. No hay ninguna tff.SERVER mencionado explícitamente en este sistema (aunque en la práctica, la orquestación de dicho tratamiento podría involucrar a uno). Piense en un cálculo definido de esta manera como conceptualmente análogo al Map etapa en MapReduce .

Además, tenga en cuenta que lo que hemos dicho en el apartado anterior sobre los cálculos de FFT conseguir serializados en el momento definición sigue siendo cierto para tff.tf_computation código, así - el cuerpo del pitón de add_half_on_clients se trazó una vez en el momento de definición. En invocaciones posteriores, TFF utiliza su representación serializada.

La única diferencia entre los métodos de Python decoradas con tff.federated_computation y aquellos decorado con tff.tf_computation es que este último se serializan como gráficos TensorFlow (mientras que el primero no se les permite contener código TensorFlow directamente incorporado en ellos).

Bajo el capó, cada método decorado con tff.tf_computation desactiva temporalmente la ejecución ansiosos con el fin de permitir que la estructura de la computación para ser capturado. Si bien la ejecución ansiosa está deshabilitada localmente, puede usar construcciones ansiosas de TensorFlow, AutoGraph, TensorFlow 2.0, etc., siempre que escriba la lógica de su cálculo de manera que pueda serializarse correctamente.

Por ejemplo, el siguiente código fallará:

try:

  # Eager mode
  constant_10 = tf.constant(10.)

  @tff.tf_computation(tf.float32)
  def add_ten(x):
    return x + constant_10

except Exception as err:
  print (err)
Attempting to capture an EagerTensor without building a function.

El anterior falla porque constant_10 ya ha sido construida fuera de la gráfica que tff.tf_computation construye internamente en el cuerpo de add_ten durante el proceso de serialización.

Por otra parte, invocando las funciones de Python que modifican el gráfico actual cuando llama dentro de un tff.tf_computation está bien:

def get_constant_10():
  return tf.constant(10.)

@tff.tf_computation(tf.float32)
def add_ten(x):
  return x + get_constant_10()

add_ten(5.0)
15.0

Tenga en cuenta que los mecanismos de serialización en TensorFlow están evolucionando, y esperamos que también evolucionen los detalles de cómo TFF serializa los cálculos.

Trabajar con tf.data.Dataset s

Como se señaló anteriormente, una característica única de tff.tf_computation s es que te permite trabajar con tf.data.Dataset s se define de manera abstracta como parámetros formales por su código. Parámetros que se representan en TensorFlow como conjuntos de datos necesitan ser declaradas mediante el tff.SequenceType constructor.

Por ejemplo, la especificación del tipo tff.SequenceType(tf.float32) define una secuencia abstracta de elementos de flotación en TFF. Las secuencias pueden contener tensores o estructuras anidadas complejas (veremos ejemplos de ellas más adelante). La representación concisa de una secuencia de T artículos -typed es T* .

float32_sequence = tff.SequenceType(tf.float32)

str(float32_sequence)
'float32*'

Suppose that in our temperature sensor example, each sensor holds not just one temperature reading, but multiple. Here's how you can define a TFF computation in TensorFlow that calculates the average of temperatures in a single local data set using the tf.data.Dataset.reduce operator.

@tff.tf_computation(tff.SequenceType(tf.float32))
def get_local_temperature_average(local_temperatures):
  sum_and_count = (
      local_temperatures.reduce((0.0, 0), lambda x, y: (x[0] + y, x[1] + 1)))
  return sum_and_count[0] / tf.cast(sum_and_count[1], tf.float32)
str(get_local_temperature_average.type_signature)
'(float32* -> float32)'

In the body of a method decorated with tff.tf_computation , formal parameters of a TFF sequence type are represented simply as objects that behave like tf.data.Dataset , ie, support the same properties and methods (they are currently not implemented as subclasses of that type - this may change as the support for data sets in TensorFlow evolves).

You can easily verify this as follows.

@tff.tf_computation(tff.SequenceType(tf.int32))
def foo(x):
  return x.reduce(np.int32(0), lambda x, y: x + y)

foo([1, 2, 3])
6

Keep in mind that unlike ordinary tf.data.Dataset s, these dataset-like objects are placeholders. They don't contain any elements, since they represent abstract sequence-typed parameters, to be bound to concrete data when used in a concrete context. Support for abstractly-defined placeholder data sets is still somewhat limited at this point, and in the early days of TFF, you may encounter certain restrictions, but we won't need to worry about them in this tutorial (please refer to the documentation pages for details).

When locally executing a computation that accepts a sequence in a simulation mode, such as in this tutorial, you can feed the sequence as Python list, as below (as well as in other ways, eg, as a tf.data.Dataset in eager mode, but for now, we'll keep it simple).

get_local_temperature_average([68.5, 70.3, 69.8])
69.53333

Like all other TFF types, sequences like those defined above can use the tff.StructType constructor to define nested structures. For example, here's how one could declare a computation that accepts a sequence of pairs A , B , and returns the sum of their products. We include the tracing statements in the body of the computation so that you can see how the TFF type signature translates into the dataset's output_types and output_shapes .

@tff.tf_computation(tff.SequenceType(collections.OrderedDict([('A', tf.int32), ('B', tf.int32)])))
def foo(ds):
  print('element_structure = {}'.format(ds.element_spec))
  return ds.reduce(np.int32(0), lambda total, x: total + x['A'] * x['B'])
element_structure = OrderedDict([('A', TensorSpec(shape=(), dtype=tf.int32, name=None)), ('B', TensorSpec(shape=(), dtype=tf.int32, name=None))])
str(foo.type_signature)
'(<A=int32,B=int32>* -> int32)'
foo([{'A': 2, 'B': 3}, {'A': 4, 'B': 5}])
26

The support for using tf.data.Datasets as formal parameters is still somewhat limited and evolving, although functional in simple scenarios such as those used in this tutorial.

Putting it all together

Now, let's try again to use our TensorFlow computation in a federated setting. Suppose we have a group of sensors that each have a local sequence of temperature readings. We can compute the global temperature average by averaging the sensors' local averages as follows.

@tff.federated_computation(
    tff.type_at_clients(tff.SequenceType(tf.float32)))
def get_global_temperature_average(sensor_readings):
  return tff.federated_mean(
      tff.federated_map(get_local_temperature_average, sensor_readings))

Note that this isn't a simple average across all local temperature readings from all clients, as that would require weighing contributions from different clients by the number of readings they locally maintain. We leave it as an exercise for the reader to update the above code; the tff.federated_mean operator accepts the weight as an optional second argument (expected to be a federated float).

Also note that the input to get_global_temperature_average now becomes a federated float sequence . Federated sequences is how we will typically represent on-device data in federated learning, with sequence elements typically representing data batches (you will see examples of this shortly).

str(get_global_temperature_average.type_signature)
'({float32*}@CLIENTS -> float32@SERVER)'

Here's how we can locally execute the computation on a sample of data in Python. Notice that the way we supply the input is now as a list of list s. The outer list iterates over the devices in the group represented by tff.CLIENTS , and the inner ones iterate over elements in each device's local sequence.

get_global_temperature_average([[68.0, 70.0], [71.0], [68.0, 72.0, 70.0]])
70.0

This concludes the first part of the tutorial... we encourage you to continue on to the second part .