|
|
"""Coletor de métricas para análise de treinamento federado.""" |
|
|
import numpy as np |
|
|
|
|
|
|
|
|
class MetricsCollector: |
|
|
"""Coleta e organiza métricas de treinamento e validação.""" |
|
|
|
|
|
def __init__(self, strategy_name): |
|
|
self.strategy_name = strategy_name |
|
|
self.train_metrics_by_round = [] |
|
|
self.eval_metrics_by_round = [] |
|
|
|
|
|
self.convergence_metrics = { |
|
|
"rounds": [], |
|
|
"loss_variance": [], |
|
|
"loss_std": [], |
|
|
"max_min_diff": [] |
|
|
} |
|
|
|
|
|
@property |
|
|
def active_client_ids(self): |
|
|
""" |
|
|
Inspeciona os dados coletados e retorna uma lista ordenada de |
|
|
IDs de clientes únicos que enviaram métricas. |
|
|
""" |
|
|
ids = set() |
|
|
all_metrics_by_round = self.train_metrics_by_round + self.eval_metrics_by_round |
|
|
|
|
|
for round_data in all_metrics_by_round: |
|
|
for key in round_data.keys(): |
|
|
if key.startswith("client_"): |
|
|
|
|
|
try: |
|
|
client_id = int(key.split("_")[1]) |
|
|
ids.add(client_id) |
|
|
except (ValueError, IndexError): |
|
|
|
|
|
continue |
|
|
|
|
|
return sorted(list(ids)) |
|
|
|
|
|
def add_train_round(self, round_num, metrics): |
|
|
"""Adiciona métricas de uma rodada de treinamento.""" |
|
|
round_data = { |
|
|
"round": round_num, |
|
|
"global_train_loss": metrics.get("global_train_loss", None), |
|
|
"client_0_train_loss": metrics.get("client_0_train_loss", None), |
|
|
"client_1_train_loss": metrics.get("client_1_train_loss", None), |
|
|
"client_2_train_loss": metrics.get("client_2_train_loss", None), |
|
|
} |
|
|
self.train_metrics_by_round.append(round_data) |
|
|
|
|
|
def add_eval_round(self, round_num, metrics): |
|
|
"""Adiciona métricas de uma rodada de avaliação.""" |
|
|
round_data = { |
|
|
"round": round_num, |
|
|
"global_eval_loss": metrics.get("global_eval_loss", None), |
|
|
"client_0_eval_loss": metrics.get("client_0_eval_loss", None), |
|
|
"client_1_eval_loss": metrics.get("client_1_eval_loss", None), |
|
|
"client_2_eval_loss": metrics.get("client_2_eval_loss", None), |
|
|
} |
|
|
self.eval_metrics_by_round.append(round_data) |
|
|
|
|
|
def calculate_convergence_metrics(self, client_losses): |
|
|
"""Calcula métricas de convergência entre clientes.""" |
|
|
if len(client_losses) > 0: |
|
|
variance = np.var(client_losses) |
|
|
std_dev = np.std(client_losses) |
|
|
max_min_diff = max(client_losses) - min(client_losses) |
|
|
return variance, std_dev, max_min_diff |
|
|
return 0, 0, 0 |
|
|
|
|
|
@property |
|
|
def train_metrics(self): |
|
|
"""Retorna métricas de treino no formato para os gráficos.""" |
|
|
result = { |
|
|
"rounds": [r["round"] for r in self.train_metrics_by_round], |
|
|
"global_train_loss": [r["global_train_loss"] for r in self.train_metrics_by_round if r["global_train_loss"] is not None], |
|
|
"client_0_train_loss": [r["client_0_train_loss"] for r in self.train_metrics_by_round if r["client_0_train_loss"] is not None], |
|
|
"client_1_train_loss": [r["client_1_train_loss"] for r in self.train_metrics_by_round if r["client_1_train_loss"] is not None], |
|
|
"client_2_train_loss": [r["client_2_train_loss"] for r in self.train_metrics_by_round if r["client_2_train_loss"] is not None], |
|
|
} |
|
|
return result |
|
|
|
|
|
@property |
|
|
def eval_metrics(self): |
|
|
"""Retorna métricas de avaliação no formato para os gráficos.""" |
|
|
result = { |
|
|
"rounds": [r["round"] for r in self.eval_metrics_by_round], |
|
|
"global_eval_loss": [r["global_eval_loss"] for r in self.eval_metrics_by_round if r["global_eval_loss"] is not None], |
|
|
"client_0_eval_loss": [r["client_0_eval_loss"] for r in self.eval_metrics_by_round if r["client_0_eval_loss"] is not None], |
|
|
"client_1_eval_loss": [r["client_1_eval_loss"] for r in self.eval_metrics_by_round if r["client_1_eval_loss"] is not None], |
|
|
"client_2_eval_loss": [r["client_2_eval_loss"] for r in self.eval_metrics_by_round if r["client_2_eval_loss"] is not None], |
|
|
} |
|
|
return result |