Skip to main content


Using Custom Models

Welcome to the Bitfount federated learning tutorials! In this sequence of tutorials, you will learn how federated learning works on the Bitfount platform. This is the eighth notebook in the series.

In this tutorial you will learn how to train a model using a custom model by extending a base model in the Bitfount framework. We will use the Pod you set up in "Running a Pod", so double check that it is online in the Bitfount Hub prior to executing this notebook. If it is offline, you can bring it back online by repeating the Running a Pod tutorial.


In this tutorial we will first show you how to test your custom model using local training on your machine and then we will move on to training on a Pod. Note, to run a custom model on a Pod, you must have Super Modeller permissions or General Modeller with permission to run specified models on the Pod. For the purposes of this tutorial, you already have the correct permissions because Pod owners have Super Modeller permissions to their own Pods by default.

import logging  # isort: splitfrom pathlib import Pathimport nest_asyncioimport torchfrom torch import nn as nnfrom torch.nn import functional as Ffrom torchmetrics.functional import accuracyfrom bitfount import (    BitfountModelReference,    BitfountSchema,    CSVSource,    DataStructure,    ModelTrainingAndEvaluation,    PyTorchBitfountModel,    PyTorchClassifierMixIn,    ResultsOnly,    get_pod_schema,    setup_loggers,)nest_asyncio.apply()  # Needed because Jupyter also has an asyncio loop

Let's import the loggers, which allow you to monitor progress of your executed commands and raise errors in the event something goes wrong.

loggers = setup_loggers([logging.getLogger("bitfount")])

Creating a custom model

For this tutorial we will create a custom model, extending and overriding the built-in BitfountModel class (specifically, we will use the PyTorchBitfountModel class). Details on this can be found in the documentation in the bitfount.backends.pytorch.models.bitfount_model module. Note, Bitfount does not vet custom models, nor are custom models private. Custom models saved to the Hub are searchable by users who know the URL for the custom model.

The PyTorchBitfountModel uses the PyTorch Lightning library to provide high-level implementation options for a model in the PyTorch framework. This enables you to only implement the methods you need to dictate how the model training should be performed.

For our custom model we need to implement the following methods:

  • __init__(): how to setup the model
  • configure_optimizers(): how optimizers should be configured in the model
  • forward(): how to perform a forward pass in the model, how the loss is calculated
  • training_step(): what one training step in the model looks like
  • validation_step(): what one validation step in the model looks like
  • test_step(): what one test step in the model looks like

Now we implement the custom model, feel free to try out your own model here:

# Update the class name for your Custom modelclass MyCustomModel(PyTorchClassifierMixIn, PyTorchBitfountModel):    # A custom model built using PyTorch Lightning.    def __init__(self, **kwargs):        super().__init__(**kwargs)        self.learning_rate = 0.001        # Initializes the model and sets hyperparameters.        # We need to call the parent __init__ first to ensure base model is set up.        # Then we can set our custom model parameters.    def create_model(self):        self.input_size = self.datastructure.input_size        return nn.Sequential(            nn.Linear(self.input_size, 500),            nn.ReLU(),            nn.Dropout(0.1),            nn.Linear(500, self.n_classes),        )    def forward(self, x):        # Defines the operations we want to use for prediction.        x, sup = x        x = self._model(x.float())        return x    def training_step(self, batch, batch_idx):        # Computes and returns the training loss for a batch of data.        x, y = batch        y_hat = self(x)        loss = F.cross_entropy(y_hat, y)        return loss    def validation_step(self, batch, batch_idx):        # Operates on a single batch of data from the validation set.        x, y = batch        preds = self(x)        loss = F.cross_entropy(preds, y)        preds = F.softmax(preds, dim=1)        acc = accuracy(preds, y)        # We can log out some useful stats so we can see progress        self.log("val_loss", loss, prog_bar=True)        self.log("val_acc", acc, prog_bar=True)        return {            "val_loss": loss,            "val_acc": acc,        }    def test_step(self, batch, batch_idx):        x, y = batch        preds = self(x)        preds = F.softmax(preds, dim=1)        # Output targets and prediction for later        return {"predictions": preds, "targets": y}    def configure_optimizers(self):        # Configure the optimizer we wish to use whilst training.        optimizer = torch.optim.AdamW(self.parameters(), lr=self.learning_rate)        return optimizer

Training locally with a custom model

With the above model we can now change our config to use this custom model. The configuration is for the most part the same as before.

First, let's import and test the model on a local dataset.

datasource = CSVSource(    path="",    ignore_cols=["fnlwgt"],)schema = BitfountSchema(    datasource,    table_name="census-income-demo",    force_stypes={        "census-income-demo": {            "categorical": [                "TARGET",                "workclass",                "marital-status",                "occupation",                "relationship",                "race",                "native-country",                "gender",                "education",            ],        },    },)datastructure = DataStructure(target="TARGET", table="census-income-demo")model = MyCustomModel(datastructure=datastructure, schema=schema, epochs=2)

Training on a Pod with your own custom model

If you have your model defined locally, you can train on a remote Pod in the usual way - by providing the Pod identifiers as an argument to the .fit method.

NOTE: Your model will be uploaded to the Bitfount Hub during this process. You can view your uploaded models at:

pod_identifier = "census-income-demo"schema = get_pod_schema(pod_identifier)    pod_identifiers=[pod_identifier],    model_out=Path(""),    extra_imports=["from torchmetrics.functional import accuracy"],)

If you want to train on a Pod with a custom model you have developed previously and is available on the Hub (, you may reference it using BitfountModelReference. Within BitfountModelReference you can specify username and model_ref. In the previous case, the username is our own username, so we don't need to specify it. If you wanted to use a model uploaded by someone else you would need to specify their username and the name of their model.

NOTE: To run a custom model on a Pod for which you are not the owner, the Pod owner must have granted you the Super Modeller role. To learn about our Pod policies see :

model = BitfountModelReference(    # username=<INSERT USERNAME>, # NOTE: if the Custom Model was developed by another user you must provide their username    model_ref="MyCustomModel",    datastructure=datastructure,    schema=schema,    # model_version=4, # NOTE: use a specific version of the model (default is latest uploaded),    hyperparameters={"epochs": 2},)protocol = ResultsOnly(algorithm=ModelTrainingAndEvaluation(model=model))[pod_identifier])

model_ref is either the name of an existing custom model (one that has been uploaded to the Hub) or, if using a new custom model, the path to the model file. The code will handle the upload of the model to the Hub the first time it is used, after which you can refer to it by name.

You've now successfully learned how to run a custom model!