Path: blob/master/site/pt-br/federated/tutorials/custom_aggregators.ipynb
25118 views
Copyright 2021 The TensorFlow Federated Authors.
Implementando agregações personalizadas
Neste tutorial, explicamos os princípios de design por trás do módulo tff.aggregators
e práticas recomendadas para implementar a agregação personalizada de valores dos clientes para o servidor.
Pré-requisitos. Este tutorial presume que você já está familiarizado com os conceitos básicos do Federated Core, como colocações (tff.SERVER
, tff.CLIENTS
), a maneira como o TFF representa computações (tff.tf_computation
, tff.federated_computation
) e as assinaturas de tipo.
Resumo do design
No TFF, "agregação" se refere ao movimento de um conjunto de valores em tff.CLIENTS
para produzir um valor agregado do mesmo tipo em tff.SERVER
. Ou seja, cada valor de cliente individual não precisa estar disponível. Por exemplo, no aprendizado federado, é calculada a média das atualizações do modelo do cliente para obter uma atualização agregada e aplicar ao modelo global no servidor.
Além de operadores que atingem esse objetivo, como tff.federated_sum
, o TFF oferece o tff.templates.AggregationProcess
(um processo stateful) que formaliza a assinatura de tipo para a computação da agregação, de modo que possa generalizar para formas mais complexas do que uma simples soma.
Os principais componentes do módulo tff.aggregators
são fábricas para a criação do AggregationProcess
, que são projetados para serem blocos básicos de utilidade geral substituíveis do TFF em dois aspectos:
Computações parametrizadas. A agregação é um bloco básico independente que pode ser ligado a outros módulos do TFF criados para funcionar com
tff.aggregators
para parametrizar a agregação necessária.
Exemplo:
Composição da agregação. Um bloco básico de agregação pode ser composto com outros blocos básicos de agregação para criar agregações compostas mais complexas.
Exemplo:
O resto deste tutorial explica como esses dois objetivos são atingidos.
Processo de agregação
Primeira, resumimos o tff.templates.AggregationProcess
e seguimos com o padrão de fábrica para a criação.
O tff.templates.AggregationProcess
é um tff.templates.MeasuredProcess
com assinaturas de tipo especificadas para a agregação. Em especial, as funções initialize
e next
têm as seguintes assinaturas de tipo:
( -> state_type@SERVER)
(<state_type@SERVER, {value_type}@CLIENTS, *> -> <state_type@SERVER, value_type@SERVER, measurements_type@SERVER>)
O estado (do tipo state_type
) precisa ser colocado no servidor. A função next
recebe como argumento de entrada o estado e um valor a ser agregado (do tipo value_type
) colocado no cliente. O *
significa outros argumentos de entrada opcionais, por exemplo, pesos em uma média ponderada. Ela retorna um objeto de estado atualizado, o valor agregado do mesmo tipo colocado no servidor e algumas medidas.
Observe que ambos o estado que será passado entre as execuções da função next
e as medidas relatadas, que devem relatar qualquer informação dependendo de uma execução específica da função next
, podem estar vazios. Ainda assim, eles precisam ser especificados explicitamente para outras partes do TFF terem um contrato claro a seguir.
Outros módulos do TFF, por exemplo, as atualizações do modelo em tff.learning
, devem usar o tff.templates.AggregationProcess
para parametrizar a forma como os valores são agregados. No entanto, os valores agregados exatos e as assinaturas de tipo deles dependem do treinamento de outros detalhes do modelo e do algoritmo de aprendizado usado para isso.
Para tornar a agregação independente de outros aspectos computacionais, usamos o padrão de fábrica — criamos o tff.templates.AggregationProcess
apropriado depois que ficarem disponíveis as assinaturas de tipo relevantes dos objetos que serão agregados, invocando o método create
de fábrica. Por isso, a manipulação direta do processo de agregação só é necessária para autores de biblioteca, que são responsáveis por essa criação.
Fábricas de processos de agregação
Há duas classes de fábrica base abstratas para a agregação não ponderada e ponderada. O método create
recebe as assinaturas de tipo do valor que será agregado e retorna um tff.templates.AggregationProcess
para a agregação desses valores.
O processo criado por tff.aggregators.UnweightedAggregationFactory
aceita dois argumentos de entrada: (1) estado do servidor e (2) valor do tipo especificado value_type
.
Um exemplo de implementação é tff.aggregators.SumFactory
.
O processo criado por tff.aggregators.WeightedAggregationFactory
aceita três argumentos de entrada: (1) estado do servidor, (2) valor do tipo especificado value_type
e (3) peso do tipo weight_type
, conforme especificado pelo usuário da fábrica ao invocar o método create
.
Um exemplo de implementação é tff.aggregators.MeanFactory
, que calcula uma média ponderada.
O padrão de fábrica é como alcançamos o primeiro objetivo declarado acima. Essa agregação é um bloco básico independente. Por exemplo, ao mudar as variáveis do modelo que são treináveis, uma agregação complexa não precisa mudar necessariamente. A fábrica que a representa será invocada com uma assinatura de tipo diferente quando usada por um método como tff.learning.algorithms.build_weighted_fed_avg
.
Composições
Lembre-se de que um processo de agregação geral pode encapsular (a) parte do pré-processamento dos valores nos clientes, (b) movimento dos valores do cliente para o servidor e (c) parte do pós-processamento do valor agregado no servidor. O segundo objetivo declarado acima, composição da agregação, é realizado dentro do módulo tff.aggregators
ao estruturar a implementação das fábricas de agregação de modo que a parte (b) possa ser delegada para outra fábrica de agregação.
Em vez de implementar toda a lógica necessária em uma única classe de fábrica, as implementações são, por padrão, focadas em um único aspecto relevante para agregação. Quando necessário, esse padrão permite a substituição de um bloco básico de cada vez.
Um exemplo é a tff.aggregators.MeanFactory
ponderada. Sua implementação multiplica os valores e pesos fornecidos nos clientes, soma ambos os valores ponderados e pesos independentemente e divide a soma dos valores ponderados pela soma dos pesos no servidor. Em vez de implementar as somatórias usando o operador tff.federated_sum
diretamente, a somatória é delegada a duas instâncias de tff.aggregators.SumFactory
.
Essa estrutura possibilita a substituição de duas somatórias por fábricas diferentes, que realizam a soma de outra maneira. Por exemplo, uma tff.aggregators.SecureSumFactory
ou uma implementação personalizada da tff.aggregators.UnweightedAggregationFactory
. Por outro lado, tff.aggregators.MeanFactory
pode ser uma agregação interna de outra fábrica, como tff.aggregators.clipping_factory
, se os valores forem recortados antes de obter a média.
Veja o tutorial Tunagem de agregações recomendadas para aprendizado anterior para conferir usos recomendados do mecanismo de composição com as fábricas existentes no módulo tff.aggregators
.
Práticas recomendadas por exemplo
Vamos ilustrar os conceitos de tff.aggregators
em detalhes ao implementar uma única tarefa simples de exemplo e torná-la cada vez mais geral. Outra maneira de aprender é analisar a implementação de fábricas existentes.
Em vez de somar value
, a tarefa de exemplo é somar value * 2.0
e dividir a soma por 2.0
. O resultado da agregação equivale matematicamente à somatória direta de value
, e é possível pensar que consiste em três partes: (1) escalonamento nos clientes (2) somatório nos clientes (3) remoção do escalonamento no servidor.
OBSERVAÇÃO: essa tarefa não é necessariamente útil na prática. Ainda assim, é útil ao explicar os conceitos subjacentes.
Seguindo o design explicado acima, a lógica será implementada como uma subclasse de tff.aggregators.UnweightedAggregationFactory
, que cria o tff.templates.AggregationProcess
apropriado quando recebe um value_type
para agregar:
Implementação mínima
Para a tarefa de exemplo, as computações necessárias são sempre as mesmas, então não é necessário usar o estado. Por isso, ele está vazio e é representado como tff.federated_value((), tff.SERVER)
. O mesmo se aplica às medidas, por enquanto.
A implementação mínima da tarefa é a seguinte:
É possível verificar se tudo funciona como esperado com este código:
Statefulness e medidas
O statefulness é amplamente usado no TFF para representar computações que devem ser executadas iterativamente e mudar a cada iteração. Por exemplo, o estado de uma computação de aprendizado contém os pesos do modelo que está sendo treinado.
Para ilustrar como usar o estado na computação da agregação, modificamos a tarefa de exemplo. Em vez de multiplicar value
por 2.0
, vamos multiplicá-lo pelo índice de iteração — o número de vezes que a agregação foi executada.
Para fazer isso, precisamos de uma maneira de rastrear o índice de iteração, o que é obtido através do conceito de estado. Na initialize_fn
, em vez de criar um estado vazio, inicializamos o estado para que seja um escalar zero. Em seguida, o estado pode ser usado na next_fn
em três etapas: (1) incrementar em 1.0
, (2) usar para multiplicar value
e (3) retornar como o novo estado atualizado.
Depois disso, talvez você observe: Mas exatamente o mesmo código acima pode ser usado para verificar se tudo funciona como esperado. Como sei se algo realmente mudou?
Boa pergunta! É aqui que o conceito de medidas se torna útil. Em geral, as medidas podem relatar qualquer valor relevante em uma única execução da função next
, que pode ser usada para monitoramento. Nesse caso, pode ser o summed_value
do exemplo anterior, ou seja, o valor antes da etapa de "remoção de escalonamento", que deve depender do índice de iteração. Novamente, isso não é necessariamente útil na prática, mas ilustra o mecanismo relevante.
A resposta stateful da tarefa deve ficar assim:
Observe que o state
de entrada da next_fn
é colocado no servidor. Para usá-lo nos clientes, ele primeiro precisa ser comunicado, o que é possível usando o operador tff.federated_broadcast
.
Para verificar se tudo funciona como esperado, agora podemos observar as measurements
relatadas, que devem ser diferentes a cada rodada de execução, mesmo se executadas com os mesmos client_data
.
Tipos estruturados
Os pesos de um modelo treinado no aprendizado federado são geralmente representados como uma coleção de tensores, em vez de um único tensor. No TFF, isso é representado como tff.StructType
e, geralmente, fábricas de agregação úteis precisam aceitar tipos estruturados.
No entanto, nos exemplos acima, só trabalhamos com um objeto tff.TensorType
. Ao usar a fábrica anterior para criar o processo de agregação com um tff.StructType([(tf.float32, (2,)), (tf.float32, (3,))])
, obtemos um erro estranho, porque o TensorFlow tenta multiplicar um tf.Tensor
e uma list
.
O problema é que, em vez de multiplicar a estrutura dos tensores por uma constante, precisamos multiplicar cada tensor na estrutura por uma constante. A solução usual para esse problema é usar o módulo tf.nest
dentro das tff.tf_computation
s criadas.
A versão da ExampleTaskFactory
anterior compatível com os tipos estruturados deve ficar assim:
Esse exemplo destaca um padrão que pode ser útil seguir ao estruturar o código do TFF. Quando não está lidando com operações muito simples, o código fica mais legível se as tff.tf_computation
s que serão usadas como blocos básicos dentro de uma tff.federated_computation
forem criadas em um local separado. Dentro das tff.federated_computation
, esses blocos só são conectados usando operadores intrínsecos.
Para verificar se funciona como esperado:
Agregações internas
A etapa final é ativar opcionalmente a delegação da agregação real para outras fábricas, permitindo a fácil composição de diferentes técnicas de agregação.
Isso é realizado ao criar um argumento inner_factory
opcional no construtor da nossa ExampleTaskFactory
. Caso não esteja especificada, é usada a tff.aggregators.SumFactory
, que aplica o operador tff.federated_sum
usado diretamente na seção anterior.
Quando create
é chamado, podemos primeiro chamar create
da inner_factory
para criar o processo de agregação interna com o mesmo value_type
.
O estado do nosso processo retornado por initialize_fn
é uma composição de duas partes: o estado criado por "esse" processo e o estado do processo interno recém-criado.
A implementação da next_fn
é diferente, porque a agregação é delegada à função next
do processo interno e há diferença na composição da saída final. O estado é composto novamente pelo "interno" e "esse", e as medidas são compostas de maneira semelhante a um OrderedDict
.
Veja a seguir uma implantação desse padrão.
Ao delegar para a função inner_process.next
, a estrutura de retorno que obtemos é uma tff.templates.MeasuredProcessOutput
, com os mesmos três campos — state
, result
e measurements
. Ao criar a estrutura de retorno geral do processo de agregação composto, os campos state
e measurements
devem ser geralmente compostos e retornados juntos. Em contraste, o campo result
corresponde ao valor que está sendo agregado e, em vez disso, "flui" pela agregação composta.
O objeto state
deve ser visto como um detalhe de implementação da fábrica e, por isso, a composição pode ter qualquer estrutura. No entanto, measurements
corresponde aos valores que serão relatados ao usuário em algum momento. Portanto, recomendamos usar OrderedDict
, com nomenclatura composta, para que fique claro a origem da métrica relatada na composição.
Observe também o uso do operador tff.federated_zip
. O objeto state
controlado pelo processo criado deve ser um tff.FederatedType
. Se, em vez disso, tivéssemos retornado (this_state, inner_state)
na initialize_fn
, a assinatura de tipo do retorno seria um tff.StructType
com duas tuplas de tff.FederatedType
s. O uso de tff.federated_zip
"eleva" tff.FederatedType
ao nível superior. Isso é usado de maneira semelhante na next_fn
ao preparar o estado e as medidas que serão retornados.
Por fim, podemos ver como isso pode ser usado com a agregação interna padrão:
... e com uma agregação interna diferente. Por exemplo, uma ExampleTaskFactory
:
Resumo
Neste tutorial, explicamos as práticas recomendadas que devem ser seguidas para criar um bloco básico de agregação de uso geral, representado como uma fábrica de agregação. A generalidade transparece na intenção do design de duas maneiras:
Computações parametrizadas. A agregação é um bloco básico independente que pode ser ligado a outros módulos do TFF criados para funcionar com
tff.aggregators
para parametrizar a agregação necessária, comotff.learning.algorithms.build_weighted_fed_avg
.Composição da agregação. Um bloco básico de agregação pode ser composto com outros blocos básicos de agregação para criar agregações compostas mais complexas.