Using Opacus in federated learning

Zark: <p>Hi, I’m looking for how to combine federated learning with Opacus. The [tutoriasl ] <a href="https://opacus.ai/tutorials/" class="inline-onebox" rel="noopener nofollow ugc">Opacus · Train PyTorch models with Differential Privacy</a>) provide is no longer available. Can someone share some example?<br>
Also, I’m thinking, how to determine the privacy budget in FL, like, do we need to specify some amount of <strong>epsilon</strong> as the parameters of method <strong>privacy_engine.make_private_with_epsilon</strong> in for each client in each federated learning round？ Or we just use the  method <strong>privacy_engine</strong>  and calculate the epsilon as <strong>epsilon = privacy_engine.get_epsilon(delta=self.DELTA)</strong> instead?</p>
<p>Another question is when to use the method, like everytime a client is selected in current round, we built a new privacy_engine for that client?</p>
<p>Thanks for help</p>

alexandresablayrolle: <p>Hi Zark,</p>
<p>In FL it depends whether you want to do user-level privacy or sample-level privacy.</p>
<p>Regarding the budget, the epsilon of the privacy engine accounts for <em>all</em> training steps. So typically you should use the same privacy_engine throughout all rounds.</p>

Zark: <p>Hi Alex，</p>
<p>Thanks for your information. I tried to use the same privacy_engine throughout all rounds but somehow get an error after the first global round training in FL:  <strong>Trying to add hooks twice to the same model</strong>.  So currently, I’m building a new <strong>privacy_engine</strong> for clients participated in each global training round.</p>
<p>I’m not sure I understand the use of Opacus right, could you help me justify the following two statements:</p>
<p><strong>privacy_engine.make_private_with_epsilon</strong> with <strong>epoch = n</strong> as the argument will consume (approximately) the given <strong>target_epsilon</strong> within <strong>n</strong> epochs. Let’s say, if I set <strong>target_epsilon = 0.5</strong> and <strong>epoch = 10</strong>, after 10 epochs, <strong>privacy_engine.get_epsilon(delta=self.DELTA)</strong> returns value around 0.5.</p>
<p>While in the other way, <strong>privacy_engine.make_private</strong> with <strong>noise_multiplier</strong> as the argument will inject a fixed level of Gaussian noise (by default I guess?) into the model gradients to achieve DP.  In this way, <strong>privacy_engine.get_epsilon(delta=self.DELTA)</strong>  will calculate the privacy cost for each epoch. By recording this returned value, we can get a list of privacy costs: <strong>（ε_1,ε_2,…,ε_n)</strong> for <strong>n</strong> epochs.  And the total privacy cost is the sum of the list <strong>（ε_1,ε_2,…,ε_n)</strong></p>

alexandresablayrolle: <blockquote>
<p>privacy_engine.make_private_with_epsilon with epoch = n as the argument will consume (approximately) the given target_epsilon within n epochs. Let’s say, if I set target_epsilon = 0.5 and epoch = 10 , after 10 epochs, privacy_engine.get_epsilon(delta=self.DELTA) returns value around 0.5.</p>
</blockquote>
<p>That’s correct</p>
<blockquote>
<p>we can get a list of privacy costs: （ε_1,ε_2,…,ε_n) for n epochs. And the total privacy cost is the sum of the list（ε_1,ε_2,…,ε_n)</p>
</blockquote>
<p>It is possible to something along these lines but the formula for the global epsilon is <em>not</em> the sum of epsilons, you have to use the composition formula. But the goal of RDP accounting is to do exactly that for you.</p>
<p>If you can share a Minimal Reproducing Example, we can help you with the error " Trying to add hooks twice to the same model ." This error should not happen for your problem.</p>
<p>In the meantime, a way to solve your problem of accounting might be to use the <code>make_private()</code> function at every round, and choose the noise multiplier using the <code>get_noise_multiplier()</code> function from <a href="https://github.com/pytorch/opacus/blob/main/opacus/accountants/utils.py#L23" rel="noopener nofollow ugc">https://github.com/pytorch/opacus/blob/main/opacus/accountants/utils.py#L23</a>.<br>
This way, the noise multiplier will be computer to ensure that the <strong>total privacy epsilon over all epochs</strong> is set to your target_epsilon.</p>

Zark: <p>Hi  Alex,</p>
<p>I think I have figured out how to use Opacus in federated learning. Thanks for your helpful information.</p>
<p>I create a <strong>client</strong> class,  I attached a <strong>privacy_engine</strong> to each client, and at each training round of FL, I just load the model state from the global model, keeping the <strong>privacy_engine</strong> untouched</p>
<pre><code class="lang-auto">class client(object):
    def __init__(self, args, global_model, train_loader, .test_loader ):
        self.args = args
        self.eps =  {}
        self.delta =  1 / (1.1*len(train_set))
        # copy model parameters from global_model
        model = copy.deepcopy(global_model)
        # create privacy_engine 
        self.privacy_engine = PrivacyEngine()
        self.model, self.optimizer, self.train_loader = self.privacy_engine.make_private(
            module=model,
            optimizer=torch.optim.SGD(model.parameters(), self.args.lr, momentum=0.5),
            data_loader=self.train_loader,
            noise_multiplier =self.args.noise_multiplier, 
            max_grad_norm=self.args.max_grad_norm) 


    def update_model(self, global_round, model_weights):
        self.model.load_state_dict(model_weights)
        self.model.train()
        with BatchMemoryManager(data_loader=self.train_loader,
                                max_physical_batch_size=self.args.max_physical_batch_size,
                                optimizer=self.optimizer) as memory_safe_data_loader:            
            train_results = self.train(self.model, self.optimizer, memory_safe_data_loader)
        #record privacy cost spend
        self.eps[global_round] = self.privacy_engine.get_epsilon(delta=self.delta)
        return train_results

    def train(self, model, optimizer, dataloader):
        epoch_loss, epoch_acc = [], []
        for epoch in range(self.args.local_ep):
            batch_loss, batch_acc = [], []
            for batch_idx, (images, target) in enumerate(dataloader):
                optimizer.zero_grad()
                images, target = images.to(self.device), target.to(self.device)
                output = model(images)
                loss = nn.NLLLoss(output, target)
                loss.backward()
                optimizer.step()
        return model.state_dict()
</code></pre>
<p>In the main function, I first instantiate clients, so each client has their own privacy_engine</p>
<pre><code class="lang-auto">if __name__ == '__main__':  
    &lt;.... some other codes ...&gt;
    # instantiate clients
    client_lst = []                                                                         
    for cid in range(args.num_clients):                                                     
        client_lst.append(client(cid, args, global_model, train_loader,test_loader))           
             
    # the server selects clients to collaboratively train model  
    for epoch in range(args.epochs):                                              
        local_weights_lst = []                 
        m = max(int(args.frac * args.num_clients), 1)                                   
        selected_clients = np.random.choice(range(args.num_clients), m, replace=False)  
        
       for cid in selected_clients:                                                    
            local_weights = client_lst[cid].update_model(                                
                epoch, global_weights, args.noise_multiplier)              
            local_weights_lst.append(copy.deepcopy(local_weights))   
                                                                                    
       # aggregate local model weights to get global model weights                     
       global_weights = average_weights(local_weights_lst)                             
</code></pre>
<p>And the code runs well with no errors. In regards to your suggestion of  <code>get_noise_multiplier()</code>, I think it is worth trying.  The original question is solved and thanks again for your help.</p>
<p>Actually, there is another question that surfaces: I see from another question <a href="https://discuss.pytorch.org/t/how-to-store-the-state-and-resume-the-state-of-the-privacyengine/138538/2" class="inline-onebox">How to store the state and resume the state of the PrivacyEngine? - #2 by ffuuugor</a> which is about store and resume the state of <strong>privacy_engine</strong>, I’m wondering a such question:</p>
<p>If I first set <strong>noise_multipler = 1.1</strong>, run the training with DP-SGD and stored the state of  <strong>privacy_engine</strong>.  Then resumed the state of  <strong>privacy_engine</strong> and additionally, set <strong>noise_multipler = 1.2</strong> (another value different from the previous choice) and continue training with DP-SGD. Will Opacus be able to calculate the privacy cost spend so far ?</p>
<p>From the <code>Algorithm 1</code> of the original paper <strong>Deep Learning with Differential Privacy</strong>, I think they fixed the level of <strong>noise_multipler</strong>, so I assume the answer is no?</p>

Faisal_Ahmed: <p>Hi Alex,</p>
<p>Do you mean a separate privacy engine for each client across all communication rounds?</p>