¿Tengo una pregunta? Conéctese con la comunidad en el Foro de visita del foro de TensorFlow

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 instructivo es la primera parte de una serie de dos partes que demuestra cómo implementar tipos personalizados de algoritmos federados en TensorFlow Federated (TFF) usando Federated Core (FC) , un conjunto de interfaces de nivel inferior que sirven como base sobre la cual hemos implementado la capa de aprendizaje federado (FL) .

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 , usamos los mecanismos que presentamos aquí para implementar una versión simple de algoritmos de capacitación y evaluación federados. Como seguimiento, lo alentamos a estudiar la implementación del promedio federado 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 instructivo está diseñado para ser autónomo, lo alentamos a leer primero los instructivos sobre clasificación de imágenes y generación de texto para una introducción de nivel superior y más suave al marco de trabajo federado de TensorFlow y las API de aprendizaje federado ( tff.learning ), como le ayudará a poner en contexto los conceptos que describimos aquí.

Usos previstos

En pocas palabras, Federated Core (FC) es un entorno de desarrollo que hace posible expresar de forma compacta la lógica del programa que combina el código de TensorFlow con operadores de comunicación distribuida, 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.

Es posible que conozca tf.contrib.distribute , y una pregunta natural para hacerse en este punto puede ser: ¿en qué se diferencia 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 que los usuarios utilicen los modelos existentes y el código de entrenamiento con cambios mínimos para habilitar el entrenamiento distribuido , se centra mucho en cómo 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 deseen 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 no funciona, consulte la guía de instalación 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 TFF es que le permite expresar de forma compacta cálculos basados ​​en TensorFlow en datos federados . Usaremos el término datos federados en este tutorial para referirnos a una colección de elementos de datos alojados en 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.

Los datos federados como los de los ejemplos anteriores se tratan en TFF como ciudadanos de primera clase , es decir, pueden aparecer como parámetros y resultados de funciones, y tienen tipos. Para reforzar esta noción, nos referiremos a los conjuntos de datos federados como valores federados o como valores de tipos federados .

El punto importante a entender 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, así es como se definiría en TFF el tipo de flotante federado alojado por un grupo de dispositivos cliente. Una colección de lecturas de temperatura que se materializan a través de una serie de sensores distribuidos podría modelarse como un valor de este tipo federado.

federated_float_on_clients = tff.type_at_clients(tf.float32)

De manera más general, un tipo federado en TFF se define especificando el tipo T de sus constituyentes miembros : los elementos de datos que residen en dispositivos individuales y el grupo G de dispositivos en los que se alojan valores federados de este tipo (más un tercio, información opcional que mencionaremos en breve). Nos referimos al grupo G de dispositivos que albergan un valor federado como la ubicación del valor. Por lo tanto, tff.CLIENTS es un ejemplo de ubicación.

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

Un tipo federado con constituyentes miembros T y ubicación G se puede representar 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 tener en cuenta que los componentes miembros de un valor federado son generalmente opacos para el programador, es decir, un valor federado no debe considerarse como un simple dict codificado por un identificador de un dispositivo en el sistema; estos valores están destinados a ser transformados colectivamente solo por operadores federados que representan de manera abstracta varios tipos de protocolos de comunicación distribuidos (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 anteriormente) y aquellos en los que se sabe que son todos iguales. Esto está controlado por el tercer parámetro opcional all_equal en el constructor tff.FederatedType (por defecto es False ).

federated_float_on_clients.all_equal
False

Un tipo federado con una ubicación G en la que se sabe que todos los constituyentes de miembros de tipo T son iguales se puede representar de forma compacta como T@G (en oposición a {T}@G , es decir, con las llaves eliminadas para reflejar el hecho de que el conjunto múltiple de componentes de los 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 preentrenado 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 tales modelos para usar en TFF de la siguiente manera. Las llaves angulares <> en la cadena de tipo impreso son una notación TFF compacta para tuplas con nombre 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 solo estamos especificando dtype s arriba. También se admiten los tipos no escalares. En el código anterior, tf.float32 es una notación de acceso directo para el 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 flotante federado anterior, nos referiremos a dicho tipo como tupla federada . De manera más general, usaremos el término XYZ federado para referirnos a un valor federado en el que los componentes de los miembros son similares a XYZ . Por lo tanto, hablaremos de cosas como tuplas federadas , secuencias federadas , modelos federados , etc.

Ahora, volviendo a float32@CLIENTS , aunque parece replicado en varios dispositivos, en realidad es un solo float32 , ya que todos los miembros son iguales. En general, puede pensar en cualquier tipo federado totalmente igual , es decir, uno de la forma T@G , como isomorfo a un tipo T no federado, ya que en ambos casos, en realidad solo hay un elemento único (aunque potencialmente replicado) de tipo T

Dado el isomorfismo entre T y T@G , es posible que se pregunte qué propósito, si es que tienen alguno, podrían servir los últimos tipos. Sigue leyendo.

Ubicaciones

Descripción general del diseño

En la sección anterior, presentamos el concepto de ubicaciones : grupos de participantes del sistema que podrían albergar conjuntamente un valor federado, y demostramos el uso de tff.CLIENTS como un ejemplo de especificación de una ubicación.

Para explicar por qué la noción de ubicación es tan fundamental que necesitábamos incorporarla en el sistema de tipos de TFF, recuerde lo que mencionamos al principio de este tutorial sobre algunos de los usos previstos de 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 usted 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 centrado en datos y, como tal, a diferencia de algunos de los marcos existentes que se centran en las operaciones y dónde se pueden ejecutar esas operaciones, TFF se centra en los datos , dónde se materializan esos datos y cómo se están 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.

Representar el tipo de cierto valor como T@G o {T}@G (en oposición a solo T ) hace que las decisiones de ubicación de datos sean explícitas y, junto con un análisis estático de programas escritos en TFF, puede servir como base para proporcionar garantías formales de privacidad para datos confidenciales en el dispositivo.

Sin embargo, una cosa importante a tener en cuenta en este punto es que, si bien alentamos a los usuarios de TFF a ser explícitos sobre los grupos de dispositivos participantes que albergan los datos (las ubicaciones), el programador nunca tratará los datos sin procesar o las identidades de los participantes individuales. .

(Nota: si bien va mucho más allá del alcance de este tutorial, debemos mencionar que hay una excepción notable a lo anterior, un operador tff.federated_collect que está diseñado como una primitiva de bajo nivel, solo para situaciones especializadas. Su uso explícito En situaciones en las que se puede evitar, no se recomienda, ya que puede limitar las posibles aplicaciones futuras. Por ejemplo, si durante el curso de un análisis estático, determinamos que un cálculo utiliza tales mecanismos de bajo nivel, es posible que deneguemos su acceso a ciertos tipos de datos.)

Dentro del cuerpo del código TFF, por diseño, no hay forma de enumerar los dispositivos que constituyen el grupo representado por tff.CLIENTS , o de sondear 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.

Recuerde aquí lo que mencionamos anteriormente acerca de que los valores de los tipos federados son diferentes a los dict Python, ya 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 también están diseñadas para ser un ciudadano de primera clase en TFF, y pueden aparecer como parámetros y resultados de un tipo de placement (para ser representado por tff.PlacementType en la 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, es suficiente pensar en la placement como un tipo incorporado primitivo opaco en TFF, similar a cómo int y bool son tipos incorporados opacos en Python, con tff.CLIENTS siendo un literal constante de este tipo, no muy diferente de 1 siendo un literal constante de tipo int .

Especificar ubicaciones

TFF proporciona dos literales de ubicación básicos, tff.CLIENTS y tff.SERVER , para facilitar la expresión de la rica variedad de escenarios prácticos que se modelan naturalmente como arquitecturas cliente-servidor, con múltiples dispositivos cliente (teléfonos móviles, dispositivos integrados, bases de datos distribuidas , sensores, etc.) orquestados por un único coordinador de 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 realmente representan tff.CLIENTS o tff.SERVER .

En particular, tff.SERVER puede ser un solo dispositivo físico (un miembro de un grupo singleton), pero también podría ser un grupo de réplicas en un clúster tolerante a fallas que ejecuta la replicación de la máquina de estado; no hacemos ninguna arquitectura especial. supuestos. Más bien, usamos el bit all_equal mencionado en la sección anterior para expresar el hecho de que generalmente estamos tratando con un solo elemento de datos en el servidor.

Asimismo, tff.CLIENTS en algunas aplicaciones puede representar a todos los clientes en el sistema - lo que en el contexto del aprendizaje federado a veces llamamos la población , pero por ejemplo, en implementaciones de producción de Federated Averaging , puede representar una cohorte - un subconjunto de los clientes seleccionados para participar en una ronda particular de capacitación. Las ubicaciones definidas de forma 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 composición en TFF es un cálculo federado , una sección de lógica que puede aceptar valores federados como entrada y devolver valores federados como salida. Así es como 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)

Al observar el código anterior, en este punto, es posible que se pregunte: ¿no existen ya construcciones de decorador para definir unidades componibles como tf.function en TensorFlow, y si es así, por qué introducir otro y en qué se diferencia?

La respuesta corta es que el código generado por el contenedor tff.federated_computation no es TensorFlow ni Python; es una especificación de un sistema distribuido en un lenguaje de cola interno independiente de la plataforma. 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 firma de tipo de un cálculo consultando su propiedad type_signature , 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 continuar, reflexionemos sobre esto por un minuto: la entrada y salida de este cálculo están en diferentes lugares (en CLIENTS vs. en el SERVER ). Recuerde lo que dijimos en la sección anterior sobre ubicaciones sobre cómo las operaciones TFF pueden extenderse a través de ubicaciones y ejecutarse en la red , y lo que acabamos de decir sobre los cálculos federados como representación de especificaciones abstractas de 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 muchos escenarios prácticos, los cálculos que representan las tareas de nivel superior tenderán a aceptar sus entradas e informar sus salidas en el servidor; esto refleja la idea de que los cálculos pueden desencadenarse por consultas que se originan y terminan en el servidor.

Sin embargo, la API de FC no impone esta suposición, y muchos de los componentes básicos que usamos internamente (incluidos numerosos operadores tff.federated_... que puede encontrar en la API) tienen entradas y salidas con ubicaciones distintas, por lo que, en general, debe No piense en un cómputo federado 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 por defecto la perspectiva global de toda la red, en lugar de la perspectiva de un único coordinador centralizado.

En general, las firmas de tipo funcional se representan de forma compacta como (T -> U) para los tipos T y U de entradas y salidas, respectivamente. El tipo de parámetro formal (como sensor_readings en este caso) se especifica como 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 (por ejemplo, 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álculo espera un valor de un tipo federado con el bit all_equal establecido en False , puede alimentarlo como una list simple en Python, y para los tipos federados con el bit all_equal establecido en True , puede simplemente alimentar directamente el (único) constituyente miembro. Así es también como se le informan los resultados.

get_average_temperature([68.5, 70.3, 69.8])
69.53334

Cuando ejecuta 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, tff.federated_computation a una nota que hicimos anteriormente sobre el decorador tff.federated_computation emite código en un lenguaje adhesivo . Aunque la lógica de los cálculos de TFF se puede expresar como funciones ordinarias en Python (solo necesita decorarlas con tff.federated_computation como hicimos anteriormente), y puede invocarlas directamente con argumentos de Python como cualquier otra función de Python en este notebook, detrás de escena, como mencionamos anteriormente, los cálculos de TFF en realidad no son Python.

Lo que queremos decir con esto es que cuando el intérprete de Python encuentra una función decorada con tff.federated_computation , rastrea las declaraciones en el cuerpo de esta función una vez (en el momento de la definición), y luego construye una representación serializada de la lógica del cálculo para uso futuro, ya sea 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 TFF se definen en Python, pero las declaraciones de Python en sus cuerpos, como tff.federated_mean en el ejemplo que acabamos de mostrar, se compilan en una representación serializable portátil e 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. El código de Python contenido en el cuerpo de un cálculo TFF se ejecuta en el momento de la definición, cuando el cuerpo de la función de Python decorado con tff.federated_computation se rastrea 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 optado por 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. Puede ver eso en el ejemplo simple anterior, con el operador especial tff.federated_mean que acepta datos en los dispositivos cliente, pero deposita los resultados en el servidor.

El operador tff.federated_mean no se puede modelar fácilmente como un operador ordinario en Python, ya que no se ejecuta localmente; como se señaló anteriormente, representa un sistema distribuido que coordina el comportamiento de múltiples participantes del sistema. Nos referiremos a estos operadores como operadores federados , para distinguirlos de los operadores ordinarios (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.

Composición de 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. Puede pensar en el operador tff.federated_mean como una especie de cálculo federado de plantilla incorporada con una firma de tipo ({T}@CLIENTS -> T@SERVER) (de hecho, al igual que los cálculos que 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 puede invocarse en un cuerpo de otra función de Python decorada con tff.federated_computation ; hacerlo hará que se tff.federated_computation en el cuerpo del padre, de la misma manera que tff.federated_mean se incrustó en su propio cuerpo anteriormente.

Una restricción importante a tener en cuenta es que los cuerpos de las funciones de Python decoradas con tff.federated_computation deben constar solo de operadores federados, es decir, no pueden contener directamente operaciones de TensorFlow. Por ejemplo, no puede usar directamente las interfaces tf.nest para agregar un par de valores federados. El código de TensorFlow debe limitarse a bloques de código decorados con un tff.tf_computation analiza en la siguiente sección. Solo cuando se ajusta de esta manera, se puede invocar el código ajustado de TensorFlow en el cuerpo de un tff.federated_computation .

Las razones de esta separación son técnicas (es difícil engañar a operadores como tf.add para que trabajen con no tensores) así como arquitectónicas. El lenguaje de cálculos federados (es decir, la lógica construida a partir de cuerpos serializados de funciones de Python decoradas con tff.federated_computation ) está diseñado para servir como un lenguaje adhesivo independiente de la plataforma. Este lenguaje de unión se usa actualmente para crear sistemas distribuidos a partir de secciones integradas del código de TensorFlow (limitado a bloques tff.tf_computation ). Con el tiempo, anticipamos la necesidad de incorporar secciones de otra lógica que no sea de TensorFlow, como consultas de bases de datos relacionales que podrían representar canalizaciones de entrada, todas conectadas entre sí usando el mismo lenguaje adhesivo (los bloques tff.federated_computation ).

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ás en TFF sea código de TensorFlow ordinario (es decir, de ejecución local). Para usar dicho código con TFF, como se señaló anteriormente, solo necesita decorarse con tff.tf_computation .

Por ejemplo, así es como podríamos implementar una función que toma un número y le agrega 0.5 .

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

Una vez más, al mirar esto, es posible que se pregunte por qué deberíamos definir otro decorador tff.tf_computation lugar de simplemente usar 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, recomendamos usar los mecanismos nativos de composición de TensorFlow, como tf.function , siempre que sea posible, ya que se puede esperar que evolucione la forma exacta en que el decorador de TFF interactúa con las funciones ansiosas.

Ahora, volviendo al fragmento de código de ejemplo anterior, el cálculo add_half que acabamos de definir puede ser tratado por TFF como cualquier otro cálculo TFF. 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 usar add_half como un componente básico en otros cálculos. Por ejemplo, así es como puede utilizar el operador tff.federated_map para aplicar add_half a todos los componentes miembros de un flotante federado 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

La ejecución de los cálculos definidos con tff.tf_computation sigue las mismas reglas que las que 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 invocar el cálculo 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 tff.SERVER mencionado explícitamente en este sistema (incluso si en la práctica, orquestar dicho procesamiento podría involucrar uno). Piense en un cálculo definido de esta manera como conceptualmente análogo a la etapa Map en MapReduce .

Además, tenga en cuenta que lo que dijimos en la sección anterior sobre los cálculos de TFF que se serializan en el momento de la definición también sigue siendo cierto para el código tff.tf_computation : el cuerpo de Python de add_half_on_clients se rastrea una vez en el momento de la definición. En invocaciones posteriores, TFF utiliza su representación serializada.

La única diferencia entre los métodos de Python decorados con tff.federated_computation y los decorados con tff.tf_computation es que los últimos se serializan como gráficos de TensorFlow (mientras que los primeros no pueden contener código de TensorFlow directamente incrustado en ellos).

Bajo el capó, cada método decorado con tff.tf_computation deshabilita temporalmente la ejecución ansiosa para permitir que se tff.tf_computation la estructura del cálculo. 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.

Lo anterior falla porque constant_10 ya se ha construido fuera del gráfico que tff.tf_computation construye internamente en el cuerpo de add_ten durante el proceso de serialización.

Por otro lado, invocar funciones de Python que modifican el gráfico actual cuando se 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 contf.data.Dataset s

Como se señaló anteriormente, una característica única de tff.tf_computation s es que le permite trabajar contf.data.Dataset s definidos de forma abstracta como parámetros formales por su código. Parameters to be represented in TensorFlow as data sets need to be declared using the tff.SequenceType constructor.

For example, the type specification tff.SequenceType(tf.float32) defines an abstract sequence of float elements in TFF. Sequences can contain either tensors, or complex nested structures (we'll see examples of those later). The concise representation of a sequence of T -typed items is 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 liketf.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 ordinarytf.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 atf.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 .