viceroy

git clone git://git.codymlewis.com/viceroy.git
Log | Files | Refs | README

commit 438986f5e80bb0d7c36857566b4c8e4e5d72178f
parent 4839a646357aa1ecd067ebe40e875c504b105f82
Author: Cody Lewis <cody@codymlewis.com>
Date:   Fri, 23 Oct 2020 16:59:13 +1100

Implemented foolsgold and added adversaries

Diffstat:
Mclient.py | 49++++++++++++++++++++++++++++++++++++++++++++-----
Mglobal_model.py | 51++++++++++++++++++++++++++++++++++-----------------
Moptions.json | 13++++++++++---
Mserver.py | 17++++++++++++-----
Mutils.py | 23+++++++++++++++++++++--
5 files changed, 121 insertions(+), 32 deletions(-)

diff --git a/client.py b/client.py @@ -12,6 +12,7 @@ from softmax_model import SoftMaxModel class Client: + """Federated learning client""" def __init__(self, data, options, classes): self.net = SoftMaxModel( data['x_dim'], @@ -39,11 +40,49 @@ class Client: ) def fit_async(self, verbose=False): + """Run the fit method in a suitable way for async running""" self.latest_loss, self.latest_grad = self.fit(verbose=verbose) -if __name__ == '__main__': - import utils - options = utils.load_options() - data = utils.load_data("mnist", train=False) - client = Client(data, options, [1, 2]) +class Flipper(Client): + """A simple label-flipping model poisoner""" + def __init__(self, data, options, classes): + super().__init__(data, options, classes) + ids = data['y'][0] == options.adversaries['from'] + self.x = data['x'][ids] + self.y = torch.tensor( + [options.adversaries['to'] for _ in ids] + ).unsqueeze(dim=0) + + +class OnOff(Client): + """ + Label flipping poisoner that switches its attack on and off every few + epochs + """ + def __init__(self, data, options, classes): + super().__init__(data, options, classes) + ids = data['y'][0] == options.adversaries['from'] + self.shadow_x = data['x'][ids] + self.shadow_y = torch.tensor( + [options.adversaries['to'] for _ in ids] + ).unsqueeze(dim=0) + self.epochs = 0 + + def fit(self, verbose=False): + self.epochs += 1 + if self.epochs % self.options.adversaries['toggle_time'] == 0: + temp = self.x + self.x = self.shadow_x + self.shadow_x = temp + temp = self.y + self.y = self.shadow_y + self.shadow_y = temp + return super().fit(verbose=verbose) + + +# Dictionary for factory stuctures of adversary construction +ADVERSARY_TYPES = { + "flip": Flipper, + "onoff": OnOff +} diff --git a/global_model.py b/global_model.py @@ -7,13 +7,14 @@ Author: Cody Lewis import torch from softmax_model import SoftMaxModel +import utils class GlobalModel: """The central global model for use within federated learning""" def __init__(self, num_in, num_out, fit_fun_name): self.net = SoftMaxModel(num_in, num_out) - self.histories = [] + self.histories = dict() self.fit_fun = { "fed_avg": fed_avg, "foolsgold": foolsgold @@ -21,16 +22,12 @@ class GlobalModel: def fit(self, grads, params): """Fit the model to some client gradients""" - self.fit_fun(self.net, grads, params) + self.fit_fun(self, grads, params) def predict(self, x): """Predict the classes of the data x""" return self.net(x) - def add_client(self): - """Add a client to the federated learning system""" - self.histories.append(torch.tensor([0])) - def get_params(self): """Get the tensor form parameters of this model""" return self.net.get_params() @@ -40,7 +37,7 @@ def fed_avg(net, grads, params): """Perform federated averaging across the client gradients""" num_clients = len(grads) total_dc = sum([grads[i]["data_count"] for i in range(num_clients)]) - for k, p in enumerate(net.parameters()): + for k, p in enumerate(net.net.parameters()): for i in range(num_clients): with torch.no_grad(): p.data.sub_( @@ -52,16 +49,24 @@ def fed_avg(net, grads, params): def foolsgold(net, grads, params): """Perform FoolsGold learning across the client gradients""" - # Maybe have a flat grads and list grads + flat_grads = utils.flatten_grads(grads) num_clients = len(grads) + total_dc = sum([grads[i]["data_count"] for i in range(num_clients)]) cs = torch.tensor( - [[0 for _ in num_clients] for _ in num_clients] + [[0 for _ in range(num_clients)] for _ in range(num_clients)], + dtype=torch.float32 ) - v = torch.tensor([0 for _ in num_clients]) - alpha = torch.tensor([0 for _ in num_clients]) + v = torch.tensor([0 for _ in range(num_clients)], dtype=torch.float32) + alpha = torch.tensor([0 for _ in range(num_clients)], dtype=torch.float32) + if len(net.histories) < num_clients: + while len(net.histories) < num_clients: + net.histories[len(net.histories)] = flat_grads[len(net.histories)] + else: + for i in range(num_clients): + net.histories[i] += flat_grads[i] for i in range(num_clients): - net.histories[i] += grads[i] - # TODO: feature importances S_t + # TODO: feature importances S_t (fi in other code: + # abs(w_t) / sum(abs(w_t))) for j in {x for x in range(num_clients)} - {i}: cs[i][j] = torch.cosine_similarity( net.histories[i], @@ -75,7 +80,19 @@ def foolsgold(net, grads, params): cs[i][j] *= v[i] / v[j] alpha[i] = 1 - max(cs[i]) alpha = alpha / max(alpha) - alpha = params['kappa'] * (torch.log(alpha / (1 - alpha)) + 0.5) - # for k, p in enumerate(net.parameters()): - # for i in range(num_clients): - # p.data.add_(alpha[i] * grads[i][k]) + for i, a in enumerate(alpha): + if a == 1: + alpha[i] = 1 + else: + alpha[i] = params['kappa'] * (torch.log(a / (1 - a)) + 0.5) + alpha[alpha > 1] = 1 + alpha[alpha < 0] = 0 + for k, p in enumerate(net.net.parameters()): + for i in range(num_clients): + with torch.no_grad(): + p.data.sub_( + alpha[i] * + params['lr'] * + (grads[i]["data_count"] / total_dc) * + grads[i]['params'][k] + ) diff --git a/options.json b/options.json @@ -1,14 +1,21 @@ { - "server_epochs": 30, + "server_epochs": 300, "user_epochs": 1, - "users": 10, + "users": 20, "batch_size": 512, "learning_rate": 0.01, - "fit_fun": "fed_avg", + "fit_fun": "foolsgold", "params": { "lr": 0.01, "kappa": 1 }, + "adversaries": { + "percent_adv": 0.5, + "type": "onoff", + "from": 1, + "to": 7, + "toggle_time": 30 + }, "verbosity": 1, "result_log_file": "./results.log" } diff --git a/server.py b/server.py @@ -10,7 +10,7 @@ import threading import torch.nn as nn from global_model import GlobalModel -from client import Client +from client import Client, ADVERSARY_TYPES import utils # TODO: Maybe verbosity for log file writing @@ -46,7 +46,8 @@ class Server: print( f"Epoch: {e + 1}/{epochs}, " + f"Loss: {criterion(self.net.predict(X), Y[0]):.6f}, " + - f"Accuracy: {stats['accuracy']}", + f"Accuracy: {stats['accuracy']}, " + + f"Attack Success Rate: {stats['attack_success']}", end="\r" if self.options.verbosity < 2 else "\n" ) if self.options.verbosity > 0: @@ -55,11 +56,11 @@ class Server: def add_clients(self, clients): """Add clients to the server""" self.num_clients += len(clients) - self.net.add_client() self.clients.extend(clients) def client_fit(client): + """Function that fits a client, for use within a thread""" client.fit_async(verbose=False) @@ -83,13 +84,18 @@ if __name__ == '__main__': [i for i in range(val_data['y_dim'])], options.users % val_data['y_dim'] ) + user_classes = [ + Client if i < options.users * (1 - options.adversaries['percent_adv']) + else ADVERSARY_TYPES[options.adversaries['type']] + for i in range(options.users) + ] server.add_clients( [ - Client( + u( train_data, options, class_shards[2*i:2*i + 2] - ) for i in range(options.users) + ) for i, u in enumerate(user_classes) ] ) if options.verbosity > 0: @@ -102,4 +108,5 @@ if __name__ == '__main__': print("-----[Results]-----") stats = utils.find_stats(server.net, val_data['x'], val_data['y'], options) print(f"Accuracy: {stats['accuracy'] * 100}%") + print(f"Attack Success Rate: {stats['attack_success'] * 100}%") print("-------------------") diff --git a/utils.py b/utils.py @@ -6,7 +6,6 @@ Author: Cody Lewis from typing import NamedTuple import json -import os import torch import torchvision @@ -42,12 +41,29 @@ def find_stats(model, X, Y, options): """Find statistics on the model based on validation data""" predictions = torch.argmax(model.predict(X), dim=1) accuracy = (predictions == Y[0]).sum().item() / len(Y[0]) + ids = Y[0] == options.adversaries['from'] + attack_success = ( + predictions[ids] == options.adversaries['to'] + ).sum().item() / len(ids) return { - "accuracy": accuracy + "accuracy": accuracy, + "attack_success": attack_success } +def flatten_grads(grads): + """Flatten gradients into vectors""" + flat_grads = [] + for g in grads.values(): + t = torch.tensor([]) + for p in g['params']: + t = torch.cat((t, p.flatten())) + flat_grads.append(t) + return flat_grads + + def create_log(log_fn, stats): + """Create the log file""" with open(log_fn, "w") as f: header = "" for k in stats.keys(): @@ -56,6 +72,7 @@ def create_log(log_fn, stats): def log_stats(log_fn, stats): + """Log the statistics into the file""" with open(log_fn, "a") as f: f.write(str(list(stats.values()))[1:-1].replace(' ', '') + "\n") @@ -69,6 +86,7 @@ class Options(NamedTuple): learning_rate: float fit_fun: str params: dict + adversaries: dict verbosity: int result_log_file: str @@ -85,6 +103,7 @@ def load_options(): options['learning_rate'], options['fit_fun'], options['params'], + options['adversaries'], options['verbosity'], options['result_log_file'] )