Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
tensorflow
GitHub Repository: tensorflow/docs-l10n
Path: blob/master/site/pt-br/federated/tutorials/custom_aggregators.ipynb
25118 views
Kernel: Python 3
#@title Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License.

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.

#@test {"skip": true} !pip install --quiet --upgrade tensorflow-federated

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:

  1. 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:

learning_process = tff.learning.algorithms.build_weighted_fed_avg( ..., model_aggregator=tff.aggregators.MeanFactory())
  1. 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:

secure_mean = tff.aggregators.MeanFactory( value_sum_factory=tff.aggregators.SecureSumFactory(...))

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.

import collections import tensorflow as tf import tensorflow_federated as tff

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:

class ExampleTaskFactory(tff.aggregators.UnweightedAggregationFactory): def create(self, value_type): @tff.federated_computation() def initialize_fn(): return tff.federated_value((), tff.SERVER) @tff.federated_computation(initialize_fn.type_signature.result, tff.type_at_clients(value_type)) def next_fn(state, value): scaled_value = tff.federated_map( tff.tf_computation(lambda x: x * 2.0), value) summed_value = tff.federated_sum(scaled_value) unscaled_value = tff.federated_map( tff.tf_computation(lambda x: x / 2.0), summed_value) measurements = tff.federated_value((), tff.SERVER) return tff.templates.MeasuredProcessOutput( state=state, result=unscaled_value, measurements=measurements) return tff.templates.AggregationProcess(initialize_fn, next_fn)

É possível verificar se tudo funciona como esperado com este código:

client_data = [1.0, 2.0, 5.0] factory = ExampleTaskFactory() aggregation_process = factory.create(tff.TensorType(tf.float32)) print(f'Type signatures of the created aggregation process:\n' f' - initialize: {aggregation_process.initialize.type_signature}\n' f' - next: {aggregation_process.next.type_signature}\n') state = aggregation_process.initialize() output = aggregation_process.next(state, client_data) print(f'Aggregation result: {output.result} (expected 8.0)')
Type signatures of the created aggregation process: - initialize: ( -> <>@SERVER) - next: (<state=<>@SERVER,value={float32}@CLIENTS> -> <state=<>@SERVER,result=float32@SERVER,measurements=<>@SERVER>) Aggregation result: 8.0 (expected 8.0)

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:

class ExampleTaskFactory(tff.aggregators.UnweightedAggregationFactory): def create(self, value_type): @tff.federated_computation() def initialize_fn(): return tff.federated_value(0.0, tff.SERVER) @tff.federated_computation(initialize_fn.type_signature.result, tff.type_at_clients(value_type)) def next_fn(state, value): new_state = tff.federated_map( tff.tf_computation(lambda x: x + 1.0), state) state_at_clients = tff.federated_broadcast(new_state) scaled_value = tff.federated_map( tff.tf_computation(lambda x, y: x * y), (value, state_at_clients)) summed_value = tff.federated_sum(scaled_value) unscaled_value = tff.federated_map( tff.tf_computation(lambda x, y: x / y), (summed_value, new_state)) return tff.templates.MeasuredProcessOutput( state=new_state, result=unscaled_value, measurements=summed_value) return tff.templates.AggregationProcess(initialize_fn, next_fn)

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.

client_data = [1.0, 2.0, 5.0] factory = ExampleTaskFactory() aggregation_process = factory.create(tff.TensorType(tf.float32)) print(f'Type signatures of the created aggregation process:\n' f' - initialize: {aggregation_process.initialize.type_signature}\n' f' - next: {aggregation_process.next.type_signature}\n') state = aggregation_process.initialize() output = aggregation_process.next(state, client_data) print('| Round #1') print(f'| Aggregation result: {output.result} (expected 8.0)') print(f'| Aggregation measurements: {output.measurements} (expected 8.0 * 1)') output = aggregation_process.next(output.state, client_data) print('\n| Round #2') print(f'| Aggregation result: {output.result} (expected 8.0)') print(f'| Aggregation measurements: {output.measurements} (expected 8.0 * 2)') output = aggregation_process.next(output.state, client_data) print('\n| Round #3') print(f'| Aggregation result: {output.result} (expected 8.0)') print(f'| Aggregation measurements: {output.measurements} (expected 8.0 * 3)')
Type signatures of the created aggregation process: - initialize: ( -> float32@SERVER) - next: (<state=float32@SERVER,value={float32}@CLIENTS> -> <state=float32@SERVER,result=float32@SERVER,measurements=float32@SERVER>) | Round #1 | Aggregation result: 8.0 (expected 8.0) | Aggregation measurements: 8.0 (expected 8.0 * 1) | Round #2 | Aggregation result: 8.0 (expected 8.0) | Aggregation measurements: 16.0 (expected 8.0 * 2) | Round #3 | Aggregation result: 8.0 (expected 8.0) | Aggregation measurements: 24.0 (expected 8.0 * 3)

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_computations criadas.

A versão da ExampleTaskFactory anterior compatível com os tipos estruturados deve ficar assim:

@tff.tf_computation() def scale(value, factor): return tf.nest.map_structure(lambda x: x * factor, value) @tff.tf_computation() def unscale(value, factor): return tf.nest.map_structure(lambda x: x / factor, value) @tff.tf_computation() def add_one(value): return value + 1.0 class ExampleTaskFactory(tff.aggregators.UnweightedAggregationFactory): def create(self, value_type): @tff.federated_computation() def initialize_fn(): return tff.federated_value(0.0, tff.SERVER) @tff.federated_computation(initialize_fn.type_signature.result, tff.type_at_clients(value_type)) def next_fn(state, value): new_state = tff.federated_map(add_one, state) state_at_clients = tff.federated_broadcast(new_state) scaled_value = tff.federated_map(scale, (value, state_at_clients)) summed_value = tff.federated_sum(scaled_value) unscaled_value = tff.federated_map(unscale, (summed_value, new_state)) return tff.templates.MeasuredProcessOutput( state=new_state, result=unscaled_value, measurements=summed_value) return tff.templates.AggregationProcess(initialize_fn, next_fn)

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_computations 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:

client_data = [[[1.0, 2.0], [3.0, 4.0, 5.0]], [[1.0, 1.0], [3.0, 0.0, -5.0]]] factory = ExampleTaskFactory() aggregation_process = factory.create( tff.to_type([(tf.float32, (2,)), (tf.float32, (3,))])) print(f'Type signatures of the created aggregation process:\n' f' - initialize: {aggregation_process.initialize.type_signature}\n' f' - next: {aggregation_process.next.type_signature}\n') state = aggregation_process.initialize() output = aggregation_process.next(state, client_data) print(f'Aggregation result: [{output.result[0]}, {output.result[1]}]\n' f' Expected: [[2. 3.], [6. 4. 0.]]')
Type signatures of the created aggregation process: - initialize: ( -> float32@SERVER) - next: (<state=float32@SERVER,value={<float32[2],float32[3]>}@CLIENTS> -> <state=float32@SERVER,result=<float32[2],float32[3]>@SERVER,measurements=<float32[2],float32[3]>@SERVER>) Aggregation result: [[2. 3.], [6. 4. 0.]] Expected: [[2. 3.], [6. 4. 0.]]

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.

@tff.tf_computation() def scale(value, factor): return tf.nest.map_structure(lambda x: x * factor, value) @tff.tf_computation() def unscale(value, factor): return tf.nest.map_structure(lambda x: x / factor, value) @tff.tf_computation() def add_one(value): return value + 1.0 class ExampleTaskFactory(tff.aggregators.UnweightedAggregationFactory): def __init__(self, inner_factory=None): if inner_factory is None: inner_factory = tff.aggregators.SumFactory() self._inner_factory = inner_factory def create(self, value_type): inner_process = self._inner_factory.create(value_type) @tff.federated_computation() def initialize_fn(): my_state = tff.federated_value(0.0, tff.SERVER) inner_state = inner_process.initialize() return tff.federated_zip((my_state, inner_state)) @tff.federated_computation(initialize_fn.type_signature.result, tff.type_at_clients(value_type)) def next_fn(state, value): my_state, inner_state = state my_new_state = tff.federated_map(add_one, my_state) my_state_at_clients = tff.federated_broadcast(my_new_state) scaled_value = tff.federated_map(scale, (value, my_state_at_clients)) # Delegation to an inner factory, returning values placed at SERVER. inner_output = inner_process.next(inner_state, scaled_value) unscaled_value = tff.federated_map(unscale, (inner_output.result, my_new_state)) new_state = tff.federated_zip((my_new_state, inner_output.state)) measurements = tff.federated_zip( collections.OrderedDict( scaled_value=inner_output.result, example_task=inner_output.measurements)) return tff.templates.MeasuredProcessOutput( state=new_state, result=unscaled_value, measurements=measurements) return tff.templates.AggregationProcess(initialize_fn, next_fn)

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.FederatedTypes. 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:

client_data = [1.0, 2.0, 5.0] factory = ExampleTaskFactory() aggregation_process = factory.create(tff.TensorType(tf.float32)) state = aggregation_process.initialize() output = aggregation_process.next(state, client_data) print('| Round #1') print(f'| Aggregation result: {output.result} (expected 8.0)') print(f'| measurements[\'scaled_value\']: {output.measurements["scaled_value"]}') print(f'| measurements[\'example_task\']: {output.measurements["example_task"]}') output = aggregation_process.next(output.state, client_data) print('\n| Round #2') print(f'| Aggregation result: {output.result} (expected 8.0)') print(f'| measurements[\'scaled_value\']: {output.measurements["scaled_value"]}') print(f'| measurements[\'example_task\']: {output.measurements["example_task"]}')
| Round #1 | Aggregation result: 8.0 (expected 8.0) | measurements['scaled_value']: 8.0 | measurements['example_task']: () | Round #2 | Aggregation result: 8.0 (expected 8.0) | measurements['scaled_value']: 16.0 | measurements['example_task']: ()

... e com uma agregação interna diferente. Por exemplo, uma ExampleTaskFactory:

client_data = [1.0, 2.0, 5.0] # Note the inner delegation can be to any UnweightedAggregaionFactory. # In this case, each factory creates process that multiplies by the iteration # index (1, 2, 3, ...), thus their combination multiplies by (1, 4, 9, ...). factory = ExampleTaskFactory(ExampleTaskFactory()) aggregation_process = factory.create(tff.TensorType(tf.float32)) state = aggregation_process.initialize() output = aggregation_process.next(state, client_data) print('| Round #1') print(f'| Aggregation result: {output.result} (expected 8.0)') print(f'| measurements[\'scaled_value\']: {output.measurements["scaled_value"]}') print(f'| measurements[\'example_task\']: {output.measurements["example_task"]}') output = aggregation_process.next(output.state, client_data) print('\n| Round #2') print(f'| Aggregation result: {output.result} (expected 8.0)') print(f'| measurements[\'scaled_value\']: {output.measurements["scaled_value"]}') print(f'| measurements[\'example_task\']: {output.measurements["example_task"]}')
| Round #1 | Aggregation result: 8.0 (expected 8.0) | measurements['scaled_value']: 8.0 | measurements['example_task']: OrderedDict([('scaled_value', 8.0), ('example_task', ())]) | Round #2 | Aggregation result: 8.0 (expected 8.0) | measurements['scaled_value']: 16.0 | measurements['example_task']: OrderedDict([('scaled_value', 32.0), ('example_task', ())])

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:

  1. 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, como tff.learning.algorithms.build_weighted_fed_avg.

  2. 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.