adap / flower

Flower: A Friendly Federated Learning Framework
https://flower.ai
Apache License 2.0
4.44k stars 784 forks source link

Error when implemnting Flower with CatBoost model "TypeError: None has type NoneType, but expected one of: bytes" #2844

Open yaman-hub opened 5 months ago

yaman-hub commented 5 months ago

Describe the bug

I am trying to implement federated learning using Flower from the link: https://github.com/adap/flower/tree/main/examples/xgboost-quickstart However, I am using CatBoost model instead of XGBoost, and I am also using my own dataset. Knowing that the code is working fine without changing the model to catboost.

Steps/Code to Reproduce

Client Code

import argparse
import pandas as pd
import catboost
from catboost import CatBoostClassifier
from sklearn.model_selection import train_test_split
from logging import INFO

import flwr as fl

# Arguments parser
parser = argparse.ArgumentParser()
parser.add_argument(
    "--node-id",
    default=0,
    type=int,
    help="Node ID used for the current client.",
)
args = parser.parse_args()
#####********
import argparse
from typing import Union
from logging import INFO
from datasets import Dataset, DatasetDict
import xgboost as xgb
import pandas as pd
from sklearn.model_selection import train_test_split

import catboost
from catboost import CatBoostClassifier

import flwr as fl
from flwr_datasets import FederatedDataset
from flwr.common.logger import log
from flwr.common import (
    Code,
    EvaluateIns,
    EvaluateRes,
    FitIns,
    FitRes,
    GetParametersIns,
    GetParametersRes,
    Parameters,
    Status,
)
from flwr_datasets.partitioner import IidPartitioner

# We first define arguments parser for user to specify the client/node ID.
parser = argparse.ArgumentParser()
parser.add_argument(
    "--node-id",
    default=0,
    type=int,
    help="Node ID used for the current client.",
)
args = parser.parse_args()
####*********

# Load and partition the dataset
fds = pd.read_csv('Noisy_Data.csv')
partition = fds.to_numpy()

# Data partitioning related functions
def transform_dataset_to_pool(data):
    x = data[:, 0:-1]
    y = data[:, -1]
    new_data = catboost.Pool(x, label=y)
    return new_data

train_data, valid_data = train_test_split(partition, test_size=20, random_state=32)
num_train = len(train_data)
num_val = len(valid_data)

train_pool = transform_dataset_to_pool(train_data)
valid_pool = transform_dataset_to_pool(valid_data)

# Hyper-parameters for CatBoost training
num_local_round = 5
params = {
    "iterations": 100,
    "learning_rate": 0.1,
    "depth": 8,
    "loss_function": "Logloss",
    "eval_metric": "AUC",
}

# Flower client
class CatBoostClient(fl.client.Client):
    def __init__(self):
        self.model = None

    def get_parameters(self, ins: GetParametersIns) -> GetParametersRes:
        _ = (self, ins)
        return GetParametersRes(
            status=Status(
                code=Code.OK,
                message="OK",
            ),
            parameters=Parameters(tensor_type="", tensors=[]),
        )

    def fit(self, ins):
        if not self.model:
            log(INFO, "Start training at round 1")
            model = CatBoostClassifier(**params)
            model.fit(train_pool, eval_set=valid_pool, verbose=10)
            self.model = model
        else:
            for item in ins.parameters.tensors:
                global_model = bytearray(item)

            self.model.load_model(global_model)
            self.model.fit(train_pool, eval_set=valid_pool, verbose=10)
        local_model = model.save_model('model_name')
        #local_model_bytes = bytes(local_model)
        return FitRes(
            status=Status(
                code=Code.OK,
                message="OK",
            ),
            parameters=Parameters(tensor_type="", tensors=[local_model]),
            num_examples=num_train,
            metrics={},
        )

    def evaluate(self, ins):
        eval_results = self.model.eval_metrics(valid_pool, ["AUC"])
        auc = round(eval_results['AUC'][-1], 4)

        return fl.common.EvaluateRes(
            status=fl.common.Status.OK,
            loss=0.0,
            num_examples=num_val,
            metrics={"AUC": auc},
        )

# Start Flower client
fl.client.start_client(server_address="127.0.0.1:8080", client=CatBoostClient())

Server Code

import flwr as fl
from flwr.server.strategy import FedAvg

# FL experimental settings
pool_size = 2
num_rounds = 5
num_clients_per_round = 2
num_evaluate_clients = 2

# Define strategy
strategy = FedAvg(
    fraction_fit=(float(num_clients_per_round) / pool_size),
    min_fit_clients=num_clients_per_round,
    min_available_clients=pool_size,
    min_evaluate_clients=num_evaluate_clients,
    fraction_evaluate=1.0,
)

# Start Flower server
fl.server.start_server(
    server_address="0.0.0.0:8080",
    config=fl.server.ServerConfig(num_rounds=num_rounds),
    strategy=strategy,
)

Expected Results

The server aggregated the models from all clients and gave me results.

Actual Results

An error on the client side that said: " return Parameters(tensors=parameters.tensors, tensor_type=parameters.tensor_type) TypeError: None has type NoneType, but expected one of: bytes"

However, the server will continue working until I stop it.

What might be the cause of the problem?

adam-narozniak commented 4 months ago

Hi @yaman-hub, I think that the local_model that you pass to the Parameters is None. I'd guess model.save_model('model_name') returns None. You need to pass the serialized model parameters instead.

YujiaZhang commented 3 weeks ago

@yaman-hub has this issue been resolved? I am also doing the experiment on CatBoost and LightGBM with flower