Esta página foi traduzida pela API Cloud Translation.
Switch to English

Algoritmos federados personalizados, parte 1: introdução ao núcleo federado

Ver no TensorFlow.org Executar no Google Colab Ver fonte no GitHub

Este tutorial é a primeira parte de uma série de duas partes que demonstra como implementar tipos personalizados de algoritmos federados no TensorFlow Federated (TFF) usando Federated Core (FC) - um conjunto de interfaces de nível inferior que servem como base sobre a qual implementamos a camada Federated Learning (FL) .

Esta primeira parte é mais conceitual; apresentamos alguns dos principais conceitos e abstrações de programação usados ​​no TFF e demonstramos seu uso em um exemplo muito simples com uma matriz distribuída de sensores de temperatura. Na segunda parte desta série , usamos os mecanismos que apresentamos aqui para implementar uma versão simples de treinamento federado e algoritmos de avaliação. Como acompanhamento, encorajamos você a estudar a implementação da média federada em tff.learning .

Ao final desta série, você deve ser capaz de reconhecer que os aplicativos do Federated Core não estão necessariamente limitados ao aprendizado. As abstrações de programação que oferecemos são bastante genéricas e podem ser usadas, por exemplo, para implementar análises e outros tipos personalizados de cálculos sobre dados distribuídos.

Embora este tutorial seja projetado para ser independente, recomendamos que você leia primeiro os tutoriais sobre classificação de imagens e geração de texto para uma introdução de nível superior e mais suave à estrutura Federated TensorFlow e às APIs Federated Learning ( tff.learning ), como vai ajudá-lo a contextualizar os conceitos que descrevemos aqui.

Usos pretendidos

Em poucas palavras, Federated Core (FC) é um ambiente de desenvolvimento que torna possível expressar de forma compacta a lógica do programa que combina o código TensorFlow com operadores de comunicação distribuída, como aqueles que são usados ​​em Federated Averaging - computando somas distribuídas, médias e outros tipos de agregações distribuídas sobre um conjunto de dispositivos cliente no sistema, modelos de transmissão e parâmetros para esses dispositivos, etc.

Você pode estar ciente de tf.contrib.distribute , e uma pergunta natural a se fazer neste momento pode ser: de que forma essa estrutura difere? Afinal, as duas estruturas tentam fazer com que os cálculos do TensorFlow sejam distribuídos.

Uma maneira de pensar sobre isso é que, enquanto o objetivo declarado de tf.contrib.distribute é permitir que os usuários usem os modelos existentes e o código de treinamento com mudanças mínimas para habilitar o treinamento distribuído , e muito foco está em como tirar proveito da infraestrutura distribuída Para tornar o código de treinamento existente mais eficiente, o objetivo do Federated Core da TFF é dar aos pesquisadores e profissionais controle explícito sobre os padrões específicos de comunicação distribuída que usarão em seus sistemas. O foco do FC é fornecer uma linguagem flexível e extensível para expressar algoritmos de fluxo de dados distribuídos, em vez de um conjunto concreto de recursos de treinamento distribuídos implementados.

Um dos principais públicos-alvo da API FC da TFF são pesquisadores e profissionais que podem querer experimentar novos algoritmos de aprendizagem federada e avaliar as consequências de escolhas de design sutis que afetam a maneira como o fluxo de dados no sistema distribuído é orquestrado, ainda sem se atrapalhar com os detalhes de implementação do sistema. O nível de abstração que o FC API visa corresponde aproximadamente ao pseudocódigo que alguém poderia usar para descrever a mecânica de um algoritmo de aprendizagem federado em uma publicação de pesquisa - quais dados existem no sistema e como eles são transformados, mas sem cair para o nível de trocas individuais de mensagens de rede ponto a ponto.

A TFF como um todo visa cenários nos quais os dados são distribuídos e devem permanecer assim, por exemplo, por motivos de privacidade, e onde a coleta de todos os dados em um local centralizado pode não ser uma opção viável. Isso tem implicações na implementação de algoritmos de aprendizado de máquina que requerem um maior grau de controle explícito, em comparação com cenários em que todos os dados podem ser acumulados em um local centralizado em um data center.

Antes de começarmos

Antes de mergulharmos no código, tente executar o seguinte exemplo "Hello World" para garantir que seu ambiente esteja configurado corretamente. Se não funcionar, consulte o guia de instalação para obter instruções.


!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!'

Dados federados

Um dos recursos distintivos do TFF é que ele permite que você expresse de forma compacta os cálculos baseados no TensorFlow em dados federados . Usaremos o termo dados federados neste tutorial para nos referir a uma coleção de itens de dados hospedados em um grupo de dispositivos em um sistema distribuído. Por exemplo, os aplicativos executados em dispositivos móveis podem coletar dados e armazená-los localmente, sem fazer upload para um local centralizado. Ou, uma matriz de sensores distribuídos pode coletar e armazenar leituras de temperatura em seus locais.

Dados federados como os dos exemplos acima são tratados na TFF como cidadãos de primeira classe , ou seja, eles podem aparecer como parâmetros e resultados de funções e têm tipos. Para reforçar essa noção, vamos nos referir aos conjuntos de dados federados como valores federados ou como valores de tipos federados .

O ponto importante a entender é que estamos modelando toda a coleção de itens de dados em todos os dispositivos (por exemplo, toda a coleção de leituras de temperatura de todos os sensores em uma matriz distribuída) como um único valor federado.

Por exemplo, aqui está como alguém definiria no TFF o tipo de um flutuador federado hospedado por um grupo de dispositivos clientes. Uma coleção de leituras de temperatura que se materializam em uma série de sensores distribuídos pode ser modelada como um valor desse tipo federado.

federated_float_on_clients = tff.FederatedType(tf.float32, tff.CLIENTS)

De forma mais geral, um tipo federado em TFF é definido especificando o tipo T de seus constituintes - os itens de dados que residem em dispositivos individuais e o grupo G de dispositivos nos quais os valores federados desse tipo são hospedados (mais um terceiro, informação opcional que mencionaremos em breve). Nos referimos ao grupo G de dispositivos que hospedam um valor federado como o posicionamento do valor. Portanto, tff.CLIENTS é um exemplo de tff.CLIENTS .

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

Um tipo federado com constituintes membros T e colocação G pode ser representado compactamente como {T}@G , conforme mostrado abaixo.

str(federated_float_on_clients)
'{float32}@CLIENTS'

As chaves {} neste concisa notação servir como um lembrete de que os constituintes membros (itens de dados em dispositivos diferentes) podem ser diferentes, como seria de esperar, por exemplo, de leituras de sensores de temperatura, de modo que os clientes como um grupo está hospedando conjuntamente um de multi -defina de T -typed itens que, juntos, constituem o valor federado.

É importante notar que os constituintes membros de um valor federado são geralmente opacos para o programador, ou seja, um valor federado não deve ser pensado como um dict simples digitado por um identificador de um dispositivo no sistema - esses valores se destinam a ser transformados coletivamente apenas por operadores federados que representam abstratamente vários tipos de protocolos de comunicação distribuídos (como agregação). Se isso soar muito abstrato, não se preocupe - retornaremos a isso em breve e o ilustraremos com exemplos concretos.

Os tipos federados na TFF vêm em dois sabores: aqueles em que os constituintes membros de um valor federado podem ser diferentes (conforme visto acima) e aqueles em que todos são iguais. Isso é controlado pelo terceiro parâmetro all_equal opcional no construtor tff.FederatedType (padronizado como False ).

federated_float_on_clients.all_equal
False

Um tipo federado com um posicionamento G no qual todos os constituintes do membro do tipo T são conhecidos como iguais pode ser representado compactamente como T@G (ao contrário de {T}@G , ou seja, com as chaves removidas para refletir o fato de que o conjunto múltiplo de constituintes dos membros consiste em um único item).

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

Um exemplo de um valor federado desse tipo que pode surgir em cenários práticos é um hiperparâmetro (como uma taxa de aprendizado, uma norma de recorte, etc.) que foi transmitido por um servidor a um grupo de dispositivos que participam de um treinamento federado.

Outro exemplo é um conjunto de parâmetros para um modelo de aprendizado de máquina pré-treinado no servidor, que depois são transmitidos para um grupo de dispositivos clientes, onde podem ser personalizados para cada usuário.

Por exemplo, suponha que temos um par de parâmetros float32 a e b para um modelo de regressão linear unidimensional simples. Podemos construir o tipo (não federado) de tais modelos para uso na TFF da seguinte maneira. As chaves angulares <> na string de tipo impresso são uma notação TFF compacta para tuplas nomeadas ou não nomeadas.

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

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

Observe que estamos especificando apenas os dtype acima. Tipos não escalares também são suportados. No código acima, tf.float32 é uma notação de atalho para o tff.TensorType(dtype=tf.float32, shape=[]) mais geral tff.TensorType(dtype=tf.float32, shape=[]) .

Quando este modelo é transmitido aos clientes, o tipo do valor federado resultante pode ser representado conforme mostrado abaixo.

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

Por simetria com flutuante federado acima, iremos nos referir a esse tipo como uma tupla federada . De maneira mais geral, usaremos frequentemente o termo XYZ federado para nos referir a um valor federado no qual os constituintes membros são semelhantes a XYZ . Portanto, falaremos sobre coisas como tuplas federadas , sequências federadas , modelos federados e assim por diante.

Agora, voltando ao float32@CLIENTS - embora pareça replicado em vários dispositivos, é na verdade um único float32 , já que todos os membros são iguais. Em geral, você pode pensar em qualquer tipo federado totalmente igual , ou seja, um da forma T@G , como isomórfico a um tipo T não federado, uma vez que em ambos os casos, há realmente apenas um único item (embora potencialmente replicado) do tipo T

Dado o isomorfismo entre T e T@G , você pode se perguntar a que propósito, se houver, os últimos tipos podem servir. Leia.

Posicionamentos

Visão geral do design

Na seção anterior, apresentamos o conceito de canais - grupos de participantes do sistema que podem hospedar conjuntamente um valor federado e demonstramos o uso de tff.CLIENTS como uma especificação de exemplo de um tff.CLIENTS .

Para explicar por que a noção de um posicionamento é tão fundamental que precisamos incorporá-lo ao sistema de tipo TFF, lembre-se do que mencionamos no início deste tutorial sobre alguns dos usos pretendidos do TFF.

Embora neste tutorial, você só verá o código TFF sendo executado localmente em um ambiente simulado, nosso objetivo é que o TFF habilite a escrita de código que você possa implantar para execução em grupos de dispositivos físicos em um sistema distribuído, potencialmente incluindo dispositivos móveis ou incorporados executando o Android. Cada um desses dispositivos receberia um conjunto separado de instruções para executar localmente, dependendo da função que desempenha no sistema (um dispositivo de usuário final, um coordenador centralizado, uma camada intermediária em uma arquitetura multicamadas, etc.). É importante ser capaz de raciocinar sobre quais subconjuntos de dispositivos executam qual código e onde diferentes partes dos dados podem se materializar fisicamente.

Isso é especialmente importante ao lidar com, por exemplo, dados de aplicativos em dispositivos móveis. Como os dados são privados e podem ser confidenciais, precisamos verificar estaticamente se esses dados nunca sairão do dispositivo (e provar fatos sobre como os dados estão sendo processados). As especificações de posicionamento são um dos mecanismos projetados para oferecer suporte a isso.

O TFF foi projetado como um ambiente de programação centrado em dados e, como tal, ao contrário de algumas das estruturas existentes que se concentram em operações e onde essas operações podem ser executadas , o TFF se concentra em dados , onde esses dados se materializam e como estão sendo transformados . Consequentemente, o posicionamento é modelado como uma propriedade de dados no TFF, ao invés de uma propriedade de operações em dados. De fato, como você verá na próxima seção, algumas das operações TFF abrangem vários locais e são executadas "na rede", por assim dizer, em vez de serem executadas por uma única máquina ou grupo de máquinas.

Representar o tipo de um determinado valor como T@G ou {T}@G (em oposição a apenas T ) torna as decisões de colocação de dados explícitas e, junto com uma análise estática de programas escritos em TFF, pode servir como uma base para fornecer garantias formais de privacidade para dados confidenciais no dispositivo.

Uma coisa importante a notar neste ponto, no entanto, é que embora encorajemos os usuários TFF a serem explícitos sobre os grupos de dispositivos participantes que hospedam os dados (as colocações), o programador nunca lidará com os dados brutos ou identidades dos participantes individuais .

(Observação: embora vá muito além do escopo deste tutorial, devemos mencionar que há uma exceção notável ao acima, um operador tff.federated_collect que se destina a ser uma primitiva de baixo nível, apenas para situações especializadas. Seu uso explícito em situações onde pode ser evitado não é recomendado, pois pode limitar as possíveis aplicações futuras. Por exemplo, se durante o curso da análise estática, determinarmos que um cálculo usa tais mecanismos de baixo nível, podemos proibir o seu acesso a determinados tipos de dados.)

Dentro do corpo do código TFF, por design, não há como enumerar os dispositivos que constituem o grupo representado por tff.CLIENTS , ou para sondar a existência de um dispositivo específico no grupo. Não há conceito de dispositivo ou identidade de cliente em qualquer lugar na API Federated Core, no conjunto subjacente de abstrações arquitetônicas ou na infraestrutura de tempo de execução central que fornecemos para suportar simulações. Toda a lógica de computação que você escrever será expressa como operações em todo o grupo de clientes.

Lembre-se aqui do que mencionamos anteriormente sobre os valores dos tipos federados serem diferentes do Python dict , pois não se pode simplesmente enumerar seus constituintes membros. Pense nos valores que a lógica do seu programa TFF manipula como sendo associados a colocações (grupos), ao invés de participantes individuais.

As colocações são projetados para ser um cidadão de primeira classe em TFF, bem como, e pode aparecer como parâmetros e resultados de uma placement tipo (a ser representados por tff.PlacementType na API). No futuro, planejamos fornecer uma variedade de operadores para transformar ou combinar canais, mas isso está fora do escopo deste tutorial. Por enquanto, é suficiente pensar em placement como um tipo integrado primitivo opaco em TFF, semelhante a como int e bool são tipos integrados opacos em Python, com tff.CLIENTS sendo um literal constante desse tipo, não diferente de 1 sendo um literal constante do tipo int .

Especificação de canais

A TFF fornece dois literais básicos de posicionamento, tff.CLIENTS e tff.SERVER , para facilitar a expressão da rica variedade de cenários práticos que são modelados naturalmente como arquiteturas cliente-servidor, com vários dispositivos clientes (telefones celulares, dispositivos incorporados, bancos de dados distribuídos , sensores, etc.) orquestrados por um único coordenador de servidor centralizado. A TFF também foi projetada para oferecer suporte a posicionamentos personalizados, vários grupos de clientes, várias camadas e outras arquiteturas distribuídas mais gerais, mas discuti-las está fora do escopo deste tutorial.

A TFF não prescreve o que tff.CLIENTS ou tff.SERVER realmente representam.

Em particular, tff.SERVER pode ser um único dispositivo físico (um membro de um grupo singleton), mas também pode ser um grupo de réplicas em um cluster tolerante a falhas executando replicação de máquina de estado - não fazemos nenhuma arquitetura especial premissas. Em vez disso, usamos o bit all_equal mencionado na seção anterior para expressar o fato de que geralmente estamos lidando com apenas um único item de dados no servidor.

Da mesma forma, tff.CLIENTS em alguns aplicativos pode representar todos os clientes no sistema - o que, no contexto de aprendizagem federada, às vezes chamamos de população , mas, por exemplo, em implementações de produção de Média Federada , pode representar um coorte - um subconjunto de os clientes selecionados para participação em uma determinada rodada de treinamento. Os posicionamentos definidos abstratamente recebem um significado concreto quando um cálculo no qual eles aparecem é implantado para execução (ou simplesmente invocado como uma função Python em um ambiente simulado, como demonstrado neste tutorial). Em nossas simulações locais, o grupo de clientes é determinado pelos dados federados fornecidos como entrada.

Cálculos federados

Declaração de cálculos federados

O TFF foi projetado como um ambiente de programação funcional fortemente tipado que oferece suporte ao desenvolvimento modular.

A unidade básica de composição em TFF é uma computação federada - uma seção de lógica que pode aceitar valores federados como entrada e retornar valores federados como saída. Veja como você pode definir um cálculo que calcula a média das temperaturas informadas pela matriz de sensores de nosso exemplo anterior.

@tff.federated_computation(tff.FederatedType(tf.float32, tff.CLIENTS))
def get_average_temperature(sensor_readings):
  return tff.federated_mean(sensor_readings)

Olhando para o código acima, neste ponto você pode estar se perguntando - já não existem construções de decorador para definir unidades tf.function como tf.function no TensorFlow e, em caso afirmativo, por que introduzir outro e como ele é diferente?

A resposta curta é que o código gerado pelo wrapper tff.federated_computation não é TensorFlow nem é Python - é uma especificação de um sistema distribuído em uma linguagem de cola independente de plataforma interna. Neste ponto, isso sem dúvida parecerá enigmático, mas tenha em mente essa interpretação intuitiva de uma computação federada como uma especificação abstrata de um sistema distribuído. Explicaremos em um minuto.

Primeiro, vamos brincar um pouco com a definição. Os cálculos TFF são geralmente modelados como funções - com ou sem parâmetros, mas com assinaturas de tipo bem definidas. Você pode imprimir a assinatura de tipo de um cálculo consultando sua propriedade type_signature , conforme mostrado abaixo.

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

A assinatura de tipo nos diz que o cálculo aceita uma coleção de diferentes leituras de sensores em dispositivos clientes e retorna uma única média no servidor.

Antes de prosseguirmos, vamos refletir sobre isso por um minuto - a entrada e a saída desse cálculo estão em lugares diferentes (em CLIENTS vs. no SERVER ). Lembre-se do que dissemos na seção anterior sobre posicionamentos sobre como as operações TFF podem se estender por locais e rodar na rede , e o que acabamos de dizer sobre os cálculos federados como representação de especificações abstratas de sistemas distribuídos. Acabamos de definir uma computação desse tipo - um sistema distribuído simples no qual os dados são consumidos nos dispositivos clientes e os resultados agregados surgem no servidor.

Em muitos cenários práticos, os cálculos que representam tarefas de nível superior tendem a aceitar suas entradas e relatar suas saídas no servidor - isso reflete a ideia de que os cálculos podem ser acionados por consultas que se originam e terminam no servidor.

No entanto, a API FC não impõe essa suposição, e muitos dos blocos de construção que usamos internamente (incluindo vários operadores tff.federated_... que você pode encontrar na API) têm entradas e saídas com posicionamentos distintos, portanto, em geral, você deve não pense em uma computação federada como algo que roda no servidor ou é executado por um servidor . O servidor é apenas um tipo de participante em uma computação federada. Ao pensar sobre a mecânica de tais cálculos, é melhor sempre usar como padrão a perspectiva global de toda a rede, em vez da perspectiva de um único coordenador centralizado.

Em geral, as assinaturas de tipo funcional são representadas compactamente como (T -> U) para os tipos T e U de entradas e saídas, respectivamente. O tipo do parâmetro formal (como sensor_readings , neste caso) é especificado como o argumento para o decorador. Você não precisa especificar o tipo de resultado - ele é determinado automaticamente.

Embora a TFF ofereça formas limitadas de polimorfismo, os programadores são fortemente encorajados a serem explícitos sobre os tipos de dados com os quais trabalham, pois isso torna mais fácil a compreensão, depuração e verificação formal das propriedades do código. Em alguns casos, especificar explicitamente os tipos é um requisito (por exemplo, cálculos polimórficos não são atualmente executáveis ​​diretamente).

Execução de cálculos federados

Para oferecer suporte ao desenvolvimento e à depuração, o TFF permite que você invoque diretamente cálculos definidos dessa forma como funções Python, conforme mostrado abaixo. Onde a computação espera um valor de um tipo federado com o bit all_equal definido como False , você pode alimentá-lo como uma list simples em Python, e para tipos federados com o bit all_equal definido como True , você pode apenas alimentar diretamente o (único) membro constituinte. É também assim que os resultados são informados a você.

get_average_temperature([68.5, 70.3, 69.8])
69.53334

Ao executar cálculos como este no modo de simulação, você atua como um observador externo com uma visão de todo o sistema, que tem a capacidade de fornecer entradas e consumir saídas em qualquer local da rede, como de fato é o caso aqui - você forneceu valores de cliente na entrada e consumiu o resultado do servidor.

Agora, vamos retornar a uma observação que fizemos anteriormente sobre o decorador tff.federated_computation emitindo código em uma linguagem cola . Embora a lógica dos cálculos TFF possa ser expressa como funções comuns em Python (você só precisa decorá-los com tff.federated_computation como fizemos acima), e você pode invocá-los diretamente com argumentos Python como qualquer outra função Python neste notebook, nos bastidores, como observamos anteriormente, os cálculos TFF não são, na verdade, Python.

O que queremos dizer com isso é que quando o interpretador Python encontra uma função decorada com tff.federated_computation , ele rastreia as instruções no corpo dessa função uma vez (no momento da definição) e, em seguida, constrói uma representação serializada da lógica do cálculo para uso futuro - seja para execução ou para ser incorporado como um subcomponente em outro cálculo.

Você pode verificar isso adicionando uma declaração de impressão, da seguinte maneira:

@tff.federated_computation(tff.FederatedType(tf.float32, tff.CLIENTS))
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".

Você pode pensar no código Python que define uma computação federada de forma semelhante a como você pensaria no código Python que constrói um gráfico do TensorFlow em um contexto não ansioso (se você não está familiarizado com os usos não ansiosos do TensorFlow, pense no seu O código Python que define um gráfico de operações a serem executadas posteriormente, mas não as executa imediatamente). O código de construção de gráfico não ansioso no TensorFlow é Python, mas o gráfico do TensorFlow construído por esse código é independente da plataforma e serializável.

Da mesma forma, os cálculos TFF são definidos em Python, mas as instruções Python em seus corpos, como tff.federated_mean no exemplo que acabamos de mostrar, são compiladas em uma representação serializável portátil e independente de plataforma sob o capô.

Como desenvolvedor, você não precisa se preocupar com os detalhes desta representação, pois você nunca precisará trabalhar diretamente com ela, mas deve estar ciente de sua existência, o fato de que os cálculos TFF são fundamentalmente não ansiosos, e não pode capturar o estado Python arbitrário. O código Python contido no corpo de uma computação TFF é executado no momento da definição, quando o corpo da função Python decorado com tff.federated_computation é rastreado antes de ser serializado. Ele não é rastreado novamente no momento da invocação (exceto quando a função é polimórfica; consulte as páginas de documentação para obter detalhes).

Você pode se perguntar por que optamos por introduzir uma representação interna dedicada não-Python. Um dos motivos é que, em última análise, os cálculos TFF devem ser implantados em ambientes físicos reais e hospedados em dispositivos móveis ou incorporados, onde o Python pode não estar disponível.

Outra razão é que os cálculos TFF expressam o comportamento global de sistemas distribuídos, ao contrário dos programas Python, que expressam o comportamento local de participantes individuais. Você pode ver isso no exemplo simples acima, com o operador especial tff.federated_mean que aceita dados em dispositivos clientes, mas deposita os resultados no servidor.

O operador tff.federated_mean não pode ser facilmente modelado como um operador comum em Python, uma vez que não é executado localmente - como observado anteriormente, ele representa um sistema distribuído que coordena o comportamento de vários participantes do sistema. Iremos nos referir a esses operadores como operadores federados , para distingui-los dos operadores comuns (locais) em Python.

O sistema de tipos TFF e o conjunto fundamental de operações suportadas na linguagem do TFF, portanto, divergem significativamente daqueles em Python, necessitando do uso de uma representação dedicada.

Compondo cálculos federados

Conforme observado acima, as computações federadas e seus constituintes são mais bem compreendidas como modelos de sistemas distribuídos, e você pode pensar em compor computações federadas como a composição de sistemas distribuídos mais complexos a partir de sistemas mais simples. Você pode pensar no operador tff.federated_mean como um tipo de cálculo federado de modelo integrado com uma assinatura de tipo ({T}@CLIENTS -> T@SERVER) (na verdade, assim como os cálculos que você escreve, este operador também tem um complexo estrutura - sob o capô, nós a dividimos em operadores mais simples).

O mesmo é verdadeiro para compor cálculos federados. O cálculo get_average_temperature pode ser invocado em um corpo de outra função Python decorada com tff.federated_computation - isso fará com que seja embutido no corpo do pai, da mesma forma que tff.federated_mean foi embutido em seu próprio corpo anteriormente.

Uma restrição importante a ser tff.federated_computation é que os corpos das funções Python decorados com tff.federated_computation devem consistir apenas em operadores federados, ou seja, eles não podem conter diretamente operações do TensorFlow. Por exemplo, você não pode usar interfaces tf.nest diretamente para adicionar um par de valores federados. O código do TensorFlow deve ser confinado a blocos de código decorados com um tff.tf_computation discutido na seção a seguir. Somente quando encapsulado dessa maneira, o código do TensorFlow encapsulado pode ser invocado no corpo de um tff.federated_computation .

Os motivos para essa separação são técnicos (é difícil enganar operadores como tf.add para trabalhar com não tensores), bem como arquitetônicos. A linguagem de cálculos federados (ou seja, a lógica construída a partir de corpos serializados de funções Python decoradas com tff.federated_computation ) é projetada para servir como uma linguagem de colagem independente de plataforma. Esta linguagem cola é usada atualmente para construir sistemas distribuídos a partir de seções incorporadas do código do TensorFlow (confinado a blocos tff.tf_computation ). Com o passar do tempo, prevemos a necessidade de incorporar seções de outras lógicas não TensorFlow, como consultas de banco de dados relacional que podem representar pipelines de entrada, todas conectadas usando a mesma linguagem de colagem (os blocos tff.federated_computation ).

Lógica TensorFlow

Declaração de cálculos do TensorFlow

O TFF foi projetado para uso com o TensorFlow. Como tal, a maior parte do código que você escreverá no TFF provavelmente será código do TensorFlow comum (ou seja, execução local). Para usar esse código com TFF, conforme observado acima, ele só precisa ser decorado com tff.tf_computation .

Por exemplo, aqui está como poderíamos implementar uma função que pega um número e adiciona 0.5 a ele.

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

Mais uma vez, olhando para isso, você pode estar se perguntando por que devemos definir outro decorador tff.tf_computation vez de simplesmente usar um mecanismo existente, como tf.function . Ao contrário da seção anterior, aqui estamos lidando com um bloco comum de código do TensorFlow.

Existem alguns motivos para isso, cujo tratamento completo vai além do escopo deste tutorial, mas vale a pena citar o principal:

  • Para incorporar blocos de construção reutilizáveis ​​implementados usando o código do TensorFlow nos corpos de cálculos federados, eles precisam satisfazer certas propriedades - como rastrear e serializar no tempo de definição, ter assinaturas de tipo etc. Isso geralmente requer alguma forma de decorador.

Em geral, recomendamos usar os mecanismos nativos do TensorFlow para composição, como tf.function , sempre que possível, pois a maneira exata em que o decorador do TFF interage com funções ansiosas pode evoluir.

Agora, voltando ao trecho de código de exemplo acima, o cálculo add_half que acabamos de definir pode ser tratado pela TFF como qualquer outro cálculo TFF. Em particular, ele tem uma assinatura do tipo TFF.

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

Observe que este tipo de assinatura não possui canais. Os cálculos do TensorFlow não podem consumir ou retornar tipos federados.

Agora você também pode usar add_half como um bloco de construção em outros cálculos. Por exemplo, tff.federated_map como você pode usar o operador tff.federated_map para aplicar add_half a todos os membros constituintes de um float federado em dispositivos clientes.

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

Executar cálculos do TensorFlow

A execução de cálculos definidos com tff.tf_computation segue as mesmas regras que descrevemos para tff.federated_computation . Eles podem ser chamados como callables comuns em Python, como segue.

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>]

Mais uma vez, é importante notar que invocar o cálculo add_half_on_clients dessa maneira simula um processo distribuído. Os dados são consumidos nos clientes e devolvidos nos clientes. Na verdade, esse cálculo faz com que cada cliente execute uma ação local. Não há tff.SERVER explicitamente mencionado neste sistema (mesmo que na prática, orquestrar tal processamento possa envolver um). Pense em uma computação definida dessa forma como conceitualmente análoga ao estágio de Map no MapReduce .

Além disso, tenha em mente que o que dissemos na seção anterior sobre os cálculos TFF sendo serializados no momento da definição também permanece verdadeiro para o código tff.tf_computation - o corpo do Python de add_half_on_clients é rastreado uma vez no momento da definição. Em invocações subsequentes, o TFF usa sua representação serializada.

A única diferença entre os métodos Python decorados com tff.federated_computation e aqueles decorados com tff.tf_computation é que os últimos são serializados como gráficos TensorFlow (enquanto os primeiros não podem conter código TensorFlow diretamente incorporado neles).

Por baixo do capô, cada método decorado com tff.tf_computation desabilita temporariamente a execução ansiosa para permitir que a estrutura do cálculo seja capturada. Embora a execução antecipada esteja desativada localmente, você pode usar as construções TensorFlow, AutoGraph, TensorFlow 2.0 etc., contanto que escreva a lógica de sua computação de maneira que ela possa ser serializada corretamente.

Por exemplo, o código a seguir falhará:

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.

O tff.tf_computation acima falha porque constant_10 já foi construído fora do gráfico que tff.tf_computation constrói internamente no corpo de add_ten durante o processo de serialização.

Por outro lado, invocar funções python que modificam o gráfico atual quando chamadas dentro de uma tff.tf_computation é bom:

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

Observe que os mecanismos de serialização no TensorFlow estão evoluindo, e esperamos que os detalhes de como o TFF serializa os cálculos evoluam também.

Working with tf.data.Dataset s

As noted earlier, a unique feature of tff.tf_computation s is that they allows you to work with tf.data.Dataset s defined abstractly as formal parameters by your code. 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 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.FederatedType(tff.SequenceType(tf.float32), tff.CLIENTS))
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 .