├── FUEL ├── synthetic │ ├── figures │ │ ├── satifying.pdf │ │ └── violating.pdf │ ├── README.md │ ├── hco_search.py │ ├── hco_lp.py │ ├── latex_utils.py │ ├── utils.py │ └── experiment.py ├── requirements.txt ├── data │ └── data.md ├── po_lp.py ├── hco_lp.py ├── utils.py ├── main.py ├── README.md ├── dataload.py ├── dataset_generate.py └── hco_model.py ├── LICENSE └── README.md /FUEL/synthetic/figures/satifying.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cuis15/FCFL/HEAD/FUEL/synthetic/figures/satifying.pdf -------------------------------------------------------------------------------- /FUEL/synthetic/figures/violating.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cuis15/FCFL/HEAD/FUEL/synthetic/figures/violating.pdf -------------------------------------------------------------------------------- /FUEL/requirements.txt: -------------------------------------------------------------------------------- 1 | autograd==1.3 2 | cvxopt==1.2.0 3 | cvxpy==1.1.7 4 | cycler==0.10.0 5 | olefile==0.46 6 | pandas==1.1.5 7 | pyparsing==2.4.7 8 | scikit-image==0.17.2 9 | scikit-learn 10 | scipy 11 | torch==1.6.0 12 | torchvision==0.7.0 13 | urllib3==1.26.3 14 | yarg==0.1.9 15 | -------------------------------------------------------------------------------- /FUEL/synthetic/README.md: -------------------------------------------------------------------------------- 1 | # synthetic experiment for FUEL 2 | 3 | ## introduction for all files 4 | 5 | * utils.py: the neeeded functions 6 | * hco_lp.py: the function about searching for a descent direction 7 | * hco_search.py: the function for searching the descent function for the two stage 8 | * latex_utils.py: the needed function for drawing plots 9 | * figures: the dir for saving the results 10 | -------------------------------------------------------------------------------- /FUEL/data/data.md: -------------------------------------------------------------------------------- 1 | ## The data is downloaded as follows: 2 | ```wget https://archive.ics.uci.edu/ml/machine-learning-databases/adult/adult.data``` 3 | ```wget https://archive.ics.uci.edu/ml/machine-learning-databases/adult/adult.test``` 4 | 5 | ## Then run dataset_generate.py to process the dataset and the data is split into two clients in 6 | data/train/mytrain.json 7 | data/test/mytest.json 8 | (We follow exactly the same data processing procedures described in [the paper](https://arxiv.org/abs/1902.00146 and ) we are comparing with. See ```dataset_generate.py``` for the details.) 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 fairfl 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /FUEL/synthetic/hco_search.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from hco_lp import HCO_LP 4 | 5 | def hco_search(multi_obj_fg, x=None, deltas = None, para_delta = 0.5, lr_delta = 0.1, relax=False, eps=[1e-4, 1e-4, 1e-4], max_iters=100, 6 | n_dim=20, step_size=.1, grad_tol=1e-4, store_xs=False): 7 | # r = [0.98, 0.15] 8 | if relax: 9 | print('relaxing') 10 | else: 11 | print('Restricted') 12 | # randomly generate one solution 13 | x = np.random.randn(n_dim) if x is None else x 14 | deltas = [0.5, 0.5] 15 | # number of objectives 16 | lp = HCO_LP( n_dim, eps) # eps [eps_disparity,] 17 | lss, gammas, d_nds = [], [], [] 18 | if store_xs: 19 | xs = [x] 20 | 21 | # find the Pareto optimal solution 22 | desc, asce = 0, 0 23 | for t in range(max_iters): 24 | x = x.reshape(-1) 25 | ls, d_ls = multi_obj_fg(x) 26 | alpha, deltas = lp.get_alpha(ls, d_ls, deltas, para_delta, lr_delta, relax=relax) 27 | if lp.last_move == "dom": 28 | desc += 1 29 | else: 30 | asce += 1 31 | lss.append(ls) 32 | gammas.append(lp.gamma) 33 | d_nd = alpha @ d_ls 34 | d_nds.append(np.linalg.norm(d_nd, ord=np.inf)) 35 | 36 | 37 | if np.linalg.norm(d_nd, ord=np.inf) < grad_tol: 38 | print('converged, ', end=',') 39 | break 40 | x = x - 10. * max(ls[1], 0.1) * step_size * d_nd 41 | if store_xs: 42 | xs.append(x) 43 | 44 | print(f'# iterations={asce+desc}; {100. * desc/(desc+asce)} % descent') 45 | res = {'ls': np.stack(lss), 46 | 'gammas': np.stack(gammas)} 47 | if store_xs: 48 | res['xs': xs] 49 | return x, res 50 | -------------------------------------------------------------------------------- /FUEL/po_lp.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import cvxpy as cp 3 | import cvxopt 4 | import pdb 5 | # from cvxpylayers.torch import CvxpyLayer 6 | 7 | class PO_LP(object): # hard-constrained optimization 8 | 9 | def __init__(self, n_theta, n_alpha, eps): 10 | cvxopt.glpk.options["msg_lev"] = "GLP_MSG_OFF" 11 | # self.objs = objs # the two objs [l, g]. 12 | self.n_theta = n_theta # the dimension of \theta 13 | self.n_alpha = n_alpha 14 | self.eps = eps # the error bar of the optimization process eps1 < g 15 | self.grad_d = cp.Parameter((n_alpha, n_theta)) # [d_l, d_g] * d_l or [d_l, d_g] * d_g. 16 | self.l = cp.Parameter(( n_theta, 1)) 17 | self.l_g = cp.Parameter(( n_theta, n_alpha)) 18 | self.alpha = cp.Variable((1,n_alpha)) # Variable to optimize 19 | # disparities has been satisfies, in this case we only maximize the performance 20 | 21 | obj_dom = cp.Maximize(cp.sum((self.alpha @ self.grad_d) @ self.l)) 22 | constraints_dom = [self.alpha >= 0, cp.sum(self.alpha) == 1, 23 | (self.alpha @ self.grad_d) @ self.l_g >=0] 24 | 25 | self.prob_dom = cp.Problem(obj_dom, constraints_dom) # LP balance 26 | 27 | self.gamma = 0 # Stores the latest Optimum value of the LP problem 28 | self.disparity = 0 # Stores the latest maximum of selected K disparities 29 | 30 | 31 | # pytorch version 32 | # def get_alpha(self, grads, grad_l, l_g): 33 | 34 | # self.cvxpy = CvxpyLayer(self.prob_dom, parameters = [self.grad_d, self.l, self.l_g], variables = [self.alpha], gp = True) 35 | 36 | # alpha, = self.cvxpy(grads, grad_l, l_g) 37 | # return alpha.clone().detach() 38 | 39 | # numpy version 40 | 41 | def get_alpha(self, grads, grad_l, l_g): 42 | 43 | self.grad_d.value = grads.cpu().numpy() 44 | self.l.value = grad_l.cpu().numpy() 45 | self.l_g.value = l_g.cpu().numpy() 46 | self.gamma = self.prob_dom.solve(solver=cp.GLPK, verbose=False) 47 | return self.alpha.value, self.gamma 48 | 49 | 50 | -------------------------------------------------------------------------------- /FUEL/synthetic/hco_lp.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import cvxpy as cp 3 | import cvxopt 4 | import pdb 5 | 6 | class HCO_LP(object): # hard-constrained optimization 7 | 8 | def __init__(self, n, eps): 9 | cvxopt.glpk.options["msg_lev"] = "GLP_MSG_OFF" 10 | # self.objs = objs # the two objs [l, g]. 11 | self.n = n # the dimension of \theta 12 | self.eps = eps # the error bar of the optimization process [eps1 < g, eps2 < delta1, eps3 < delta2] 13 | self.deltas = cp.Parameter(2) # the two deltas of the objectives [l1, l2] 14 | self.Ca1 = cp.Parameter((2,1)) # [d_l, d_g] * d_l or [d_l, d_g] * d_g. 15 | self.Ca2 = cp.Parameter((2,1)) 16 | 17 | self.alpha = cp.Variable((1,2)) # Variable to optimize 18 | # disparities has been satisfies, in this case we only maximize the performance 19 | obj_dom = cp.Maximize(self.alpha @ self.Ca1) 20 | obj_fair = cp.Maximize(self.alpha @ self.Ca2) 21 | 22 | 23 | constraints_dom = [self.alpha >= 0, cp.sum(self.alpha) == 1] 24 | constraints_fair = [self.alpha >= 0, cp.sum(self.alpha) == 1, 25 | self.alpha @ self.Ca1 >= 0] 26 | 27 | self.prob_dom = cp.Problem(obj_dom, constraints_dom) # LP balance 28 | self.prob_fair = cp.Problem(obj_fair, constraints_fair) 29 | 30 | self.gamma = 0 # Stores the latest Optimum value of the LP problem 31 | self.disparity = 0 # Stores the latest selected K max disparities 32 | 33 | def get_alpha(self, ls, d_ls, deltas, para_delta, lr_delta, relax=False): 34 | d_l1 = np.array([d_ls[0]]).T 35 | d_l2 = np.array([d_ls[1]]).T 36 | 37 | if ls[1]<= self.eps[0]: # disparities < eps0 38 | self.Ca1.value = d_ls @ d_l1 39 | self.gamma = self.prob_dom.solve(solver=cp.GLPK, verbose=False) 40 | self.last_move = "dom" 41 | return self.alpha.value, deltas 42 | 43 | else: 44 | 45 | self.Ca1.value = d_ls @ d_l1 46 | self.Ca2.value = d_ls @ d_l2 47 | if relax: 48 | self.last_move = "fair" 49 | else: 50 | self.gamma = self.prob_fair.solve(solver=cp.GLPK, verbose=False) 51 | 52 | if self.eps[1] < deltas[0] and np.linalg.norm(d_l1) * para_delta <= deltas[0]: 53 | deltas[0] = lr_delta * deltas[0] 54 | if self.eps[2] < deltas[1] and np.linalg.norm(d_l2) * para_delta <= deltas[1]: 55 | deltas[1] = lr_delta * deltas[1] 56 | self.last_move = "fair" 57 | 58 | return self.alpha.value, deltas 59 | 60 | -------------------------------------------------------------------------------- /FUEL/synthetic/latex_utils.py: -------------------------------------------------------------------------------- 1 | import matplotlib 2 | from math import sqrt 3 | 4 | 5 | SPINE_COLOR = 'black' 6 | 7 | 8 | def latexify(fig_width=None, fig_height=None, columns=1): 9 | """Set up matplotlib's RC params for LaTeX plotting. 10 | Call this before plotting a figure. 11 | 12 | Parameters 13 | ---------- 14 | fig_width : float, optional, inches 15 | fig_height : float, optional, inches 16 | columns : {1, 2} 17 | """ 18 | 19 | # code adapted from http://www.scipy.org/Cookbook/Matplotlib/LaTeX_Examples 20 | 21 | # Width and max height in inches for IEEE journals taken from 22 | # computer.org/cms/Computer.org/Journal%20templates/transactions_art_guide.pdf 23 | 24 | assert(columns in [1,2]) 25 | 26 | if fig_width is None: 27 | fig_width = 3.487 if columns==1 else 6.9 # width in inches 28 | 29 | if fig_height is None: 30 | golden_mean = (sqrt(5)-1.0)/2.0 # Aesthetic ratio 31 | fig_height = fig_width*golden_mean # height in inches 32 | 33 | MAX_HEIGHT_INCHES = 8.0 34 | if fig_height > MAX_HEIGHT_INCHES: 35 | print("WARNING: fig_height too large:" + fig_height + 36 | "so will reduce to" + MAX_HEIGHT_INCHES + "inches.") 37 | fig_height = MAX_HEIGHT_INCHES 38 | # , '\usepackage{amsmath, amsfonts}', 39 | params = {'backend': 'pdf', 40 | 'text.latex.preamble': [r'\usepackage{amsmath}', 41 | r'\usepackage{amssymb}', 42 | r'\usepackage{gensymb}', 43 | # r'\usepackage{mathabx}', 44 | r'\usepackage{amsfonts}', 45 | r'\usepackage{newtxmath}'], 46 | 'axes.labelsize': 8, # fontsize for x and y labels (was 10) 47 | 'axes.titlesize': 9, 48 | 'font.size': 10, # was 10 49 | 'legend.fontsize': 9, # was 10 50 | 'legend.shadow': False, 51 | 'legend.fancybox': True, 52 | 'xtick.labelsize': 6, 53 | 'ytick.labelsize': 6, 54 | 'text.usetex': True, 55 | 'figure.figsize': [fig_width, fig_height], 56 | 'font.family': 'serif', 57 | 'font.serif': 'times new roman', 58 | 'patch.linewidth': 0.5 59 | } 60 | 61 | matplotlib.rcParams.update(params) 62 | 63 | 64 | def format_axes(ax, title=None, xlabel=None, ylabel=None, leg_loc=None, grid=None): 65 | 66 | for spine in ['top', 'right']: 67 | # ax.spines[spine].set_visible(False) 68 | ax.spines[spine].set_color(SPINE_COLOR) 69 | ax.spines[spine].set_linewidth(0.7) 70 | 71 | for spine in ['left', 'bottom']: 72 | ax.spines[spine].set_color(SPINE_COLOR) 73 | ax.spines[spine].set_linewidth(0.7) 74 | 75 | ax.xaxis.set_ticks_position('bottom') 76 | ax.yaxis.set_ticks_position('left') 77 | 78 | for axis in [ax.xaxis, ax.yaxis]: 79 | axis.set_tick_params(direction='out', color=SPINE_COLOR) 80 | 81 | if title is not None: 82 | ax.set_title(title) 83 | if xlabel is not None: 84 | ax.set_xlabel(xlabel, labelpad=0.4) 85 | if ylabel is not None: 86 | ax.set_ylabel(ylabel, labelpad=0.3) 87 | if leg_loc is not None: 88 | ax.legend(loc=leg_loc) 89 | if grid is not None: 90 | ax.grid(grid, lw=0.3) 91 | return ax 92 | -------------------------------------------------------------------------------- /FUEL/hco_lp.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import cvxpy as cp 3 | import cvxopt 4 | import pdb 5 | #from cvxpylayers.torch import CvxpyLayer 6 | import torch 7 | class HCO_LP(object): # hard-constrained optimization 8 | 9 | def __init__(self, n, eps): 10 | cvxopt.glpk.options["msg_lev"] = "GLP_MSG_OFF" 11 | # self.objs = objs # the two objs [l, g]. 12 | self.n = n # the dimension of \theta 13 | self.eps = eps # the error bar of the optimization process [eps1 < g, eps2 < delta1, eps3 < delta2] 14 | self.deltas = cp.Parameter(2) # the two deltas of the objectives [l1, l2] 15 | self.Ca1 = cp.Parameter((2,1)) # [d_l, d_g] * d_l or [d_l, d_g] * d_g. 16 | self.Ca2 = cp.Parameter((2,1)) 17 | 18 | self.alpha = cp.Variable((1,2)) # Variable to optimize 19 | # disparities has been satisfies, in this case we only maximize the performance 20 | obj_dom = cp.Maximize(self.alpha @ self.Ca1) 21 | obj_fair = cp.Maximize(self.alpha @ self.Ca2) 22 | 23 | 24 | constraints_dom = [self.alpha >= 0, cp.sum(self.alpha) == 1] 25 | constraints_fair = [self.alpha >= 0, cp.sum(self.alpha) == 1, 26 | self.alpha @ self.Ca1 >= 0] 27 | 28 | self.prob_dom = cp.Problem(obj_dom, constraints_dom) # LP balance 29 | self.prob_fair = cp.Problem(obj_fair, constraints_fair) 30 | 31 | self.gamma = 0 # Stores the latest Optimum value of the LP problem 32 | self.disparity = 0 # Stores the latest maximum of selected K disparities 33 | 34 | 35 | 36 | # # pytorch version 37 | # def get_alpha(self, dis_max, d_l1, d_l2, deltas, factor_delta, lr_delta): 38 | 39 | # d_ls = torch.cat((d_l1, d_l2)) 40 | # if dis_max[1]<= self.eps[0]: # [l, g] disparities < eps0 41 | # # self.Ca1.value = d_ls @ d_l1 42 | # self.cvxpy = CvxpyLayer(self.prob_dom, parameters = [self.Ca1], variables = [self.alpha]) 43 | 44 | # Ca1_value = d_ls @ d_l1.t() 45 | # alpha, = self.cvxpy(Ca1_value) 46 | 47 | # self.last_move = "dom" 48 | # return alpha.clone().detach(), deltas 49 | 50 | # else: 51 | # self.cvxpy = CvxpyLayer(self.prob_fair, parameters = [self.Ca1, self.Ca2], variables = [self.alpha]) 52 | # Ca1_value = d_ls @ d_l1.t() 53 | # Ca2_value = d_ls @ d_l2.t() 54 | 55 | # alpha, = self.cvxpy(Ca1_value, Ca2_value) 56 | # if self.eps[1] < deltas[0] and np.linalg.norm(d_l1.cpu()) * factor_delta <= deltas[0]: 57 | # deltas[0] = lr_delta * deltas[0] 58 | # if self.eps[2] < deltas[1] and np.linalg.norm(d_l2.cpu()) * factor_delta <= deltas[1]: 59 | # deltas[1] = lr_delta * deltas[1] 60 | # self.last_move = "fair" 61 | # return alpha.clone().detach(), deltas 62 | 63 | 64 | 65 | def get_alpha(self, dis_max, d_l1, d_l2, deltas, factor_delta, lr_delta): 66 | 67 | 68 | d_ls = torch.cat((d_l1, d_l2)) 69 | if dis_max[1]<= self.eps[0]: # [l, g] disparities < eps0 70 | # self.Ca1.value = d_ls @ d_l1 71 | self.Ca1.value = (d_ls @ d_l1.t()).cpu().numpy() 72 | self.gamma = self.prob_dom.solve(solver=cp.GLPK, verbose=False) 73 | self.last_move = "dom" 74 | if self.eps[1] < deltas[0] and np.linalg.norm(d_l1.cpu()) * factor_delta <= deltas[0]: 75 | deltas[0] = lr_delta * deltas[0] 76 | if self.eps[2] < deltas[1] and np.linalg.norm(d_l2.cpu()) * factor_delta <= deltas[1]: 77 | deltas[1] = lr_delta * deltas[1] 78 | return self.alpha.value, deltas 79 | 80 | else: 81 | self.Ca1.value = (d_ls @ d_l1.t()).cpu().numpy() 82 | self.Ca2.value = (d_ls @ d_l2.t()).cpu().numpy() 83 | self.gamma = self.prob_fair.solve(solver=cp.GLPK, verbose=False) 84 | if self.eps[1] < deltas[0] and np.linalg.norm(d_l1.cpu()) * factor_delta <= deltas[0]: 85 | deltas[0] = lr_delta * deltas[0] 86 | if self.eps[2] < deltas[1] and np.linalg.norm(d_l2.cpu()) * factor_delta <= deltas[1]: 87 | deltas[1] = lr_delta * deltas[1] 88 | self.last_move = "fair" 89 | return self.alpha.value, deltas -------------------------------------------------------------------------------- /FUEL/synthetic/utils.py: -------------------------------------------------------------------------------- 1 | import autograd.numpy as np 2 | from autograd import grad 3 | 4 | import matplotlib.pyplot as plt 5 | import matplotlib as mpl 6 | from labellines import labelLines # , labelLine, 7 | from latex_utils import latexify 8 | 9 | 10 | def f1(x): 11 | n = len(x) 12 | dx = np.linalg.norm(x - 1. / np.sqrt(n)) 13 | return 1 - np.exp(-dx**2) 14 | 15 | 16 | def f2(x): 17 | n = len(x) 18 | dx = np.linalg.norm(x + 1. / np.sqrt(n)) 19 | return 1 - np.exp(-dx**2) 20 | 21 | 22 | # calculate the gradients using autograd 23 | f1_dx = grad(f1) 24 | f2_dx = grad(f2) 25 | 26 | 27 | def concave_fun_eval(x): 28 | """ 29 | return the function values and gradient values 30 | """ 31 | return np.stack([f1(x), f2(x)]), np.stack([f1_dx(x), f2_dx(x)]) 32 | 33 | 34 | # ### create the ground truth Pareto front ### 35 | def create_pf(side_nonpf=False): 36 | """ 37 | if `side_nonpf` is True, then the boundary of attainable objectives, 38 | which lie adjacent to the PF, is also returned. 39 | """ 40 | def map_to_objective_space(xs): 41 | fs = [] 42 | for x1 in xs: 43 | x = np.array([x1, x1]) 44 | f, f_dx = concave_fun_eval(x) 45 | fs.append(f) 46 | 47 | return np.array(fs) 48 | 49 | if side_nonpf: 50 | ps = np.linspace(-1. / np.sqrt(2), 1. / np.sqrt(2), 30, endpoint=True) 51 | else: 52 | ps = np.linspace(-1 / np.sqrt(2), 1 / np.sqrt(2)) 53 | 54 | pf = map_to_objective_space(ps) 55 | 56 | if side_nonpf: 57 | s_left = np.linspace(-1.5 / np.sqrt(2), -1. / np.sqrt(2), 10, 58 | endpoint=True) 59 | fs_left = map_to_objective_space(s_left) 60 | 61 | s_right = np.linspace(1. / np.sqrt(2), 1.5 / np.sqrt(2), 10, 62 | endpoint=True) 63 | fs_right = map_to_objective_space(s_right) 64 | 65 | return pf, fs_left, fs_right 66 | 67 | return pf 68 | 69 | 70 | def circle_points(K, min_angle=None, max_angle=None): 71 | # generate evenly distributed preference vector 72 | ang0 = np.pi / 20. if min_angle is None else min_angle 73 | ang1 = np.pi * 9 / 20. if max_angle is None else max_angle 74 | angles = np.linspace(ang0, ang1, K, endpoint=True) 75 | x = np.cos(angles) 76 | y = np.sin(angles) 77 | return np.c_[x, y] 78 | 79 | 80 | def add_interval(ax, xdata, ydata, 81 | color="k", caps=" ", label='', side="both", lw=2): 82 | line = ax.add_line(mpl.lines.Line2D(xdata, ydata)) 83 | line.set_label(label) 84 | line.set_color(color) 85 | line.set_linewidth(lw) 86 | anno_args = { 87 | 'ha': 'center', 88 | 'va': 'center', 89 | 'size': 12, 90 | 'color': line.get_color() 91 | } 92 | a = [] 93 | if side in ["left", "both"]: 94 | a0 = ax.annotate(caps[0], xy=(xdata[0], ydata[0]), zorder=2, **anno_args) 95 | a.append(a0) 96 | if side in ["right", "both"]: 97 | a1 = ax.annotate(caps[1], xy=(xdata[1], ydata[1]), zorder=2, **anno_args) 98 | a.append(a1) 99 | return (line, tuple(a)) 100 | 101 | 102 | if __name__ == '__main__': 103 | theta = np.linspace(-4, 4, 150) 104 | l1 = [f1(np.array([x])) for x in theta] 105 | l2 = [f2(np.array([x])) for x in theta] 106 | 107 | latexify(fig_width=3, fig_height=3.0) 108 | fig, ax = plt.subplots() 109 | fig.subplots_adjust(left=0.025, bottom=.15, right=.975, top=.975) 110 | ax.spines['left'].set_position('zero') 111 | ax.spines['right'].set_color('none') # turn off the right spine/ticks 112 | ax.spines['top'].set_color('none') # turn off the top spine/ticks 113 | ax.xaxis.tick_bottom() 114 | 115 | ax.plot(theta, l1, color = "black", label=r'$l_1(\theta)$', lw=2) 116 | ax.plot(theta, l2, color = "red", label=r'$l_2(\theta)$', lw=2) 117 | 118 | # Comment out the twin axis code below for ppt 119 | axt = ax.twinx() 120 | axt.axis("off") 121 | # add_interval(axt, (-3, -1.05), (0.02, 0.02), "r", "()", 122 | # r"$\theta^0 \preccurlyeq -\mathbf{1}/\sqrt{n}$ or " + 123 | # r"$\theta^0 \succcurlyeq \mathbf{1}/\sqrt{n}$", side="right") 124 | 125 | # add_interval(axt, (1.05, 3), (0.02, 0.02), "r", "()", side="left") 126 | 127 | add_interval(axt, (-.98, .98), (0.02, 0.02), "g", "()", "Pareto Solution Set: "+ 128 | r"$-\mathbf{1}/\sqrt{n} \preccurlyeq \theta^0" + 129 | r"\preccurlyeq \mathbf{1}/\sqrt{n}$", side="both") 130 | 131 | axt.legend(loc=(0.0, 0.2)) 132 | 133 | labelLines(ax.get_lines(), xvals=[2, -2], align=False) 134 | ax.set_xlabel(r'$\theta$') 135 | ax.xaxis.set_label_coords(0.99, -0.03) 136 | 137 | plt.savefig('../figures/moo_synthetic.pdf') # for paper 138 | # plt.savefig('../figures/moo_synthetic_ppt_just_losses.pdf') # for ppt 139 | plt.show() 140 | 141 | -------------------------------------------------------------------------------- /FUEL/utils.py: -------------------------------------------------------------------------------- 1 | # utils functions 2 | import numpy as np 3 | import random 4 | import os 5 | from time import time 6 | import pickle 7 | import pdb 8 | import json 9 | import torch 10 | import logging 11 | 12 | def setup_seed(seed): 13 | torch.manual_seed(seed) 14 | torch.cuda.manual_seed_all(seed) 15 | np.random.seed(seed) 16 | random.seed(seed) 17 | torch.backends.cudnn.deterministic = True 18 | 19 | 20 | def get_random_dir_name(): 21 | import string 22 | from datetime import datetime 23 | dirname = datetime.now().strftime('%Y-%m-%d_%H-%M-%S') 24 | vocab = string.ascii_uppercase + string.ascii_lowercase + string.digits 25 | dirname = dirname + '-' + ''.join(random.choice(vocab) for _ in range(8)) 26 | return dirname 27 | 28 | 29 | ### ready for rewright 30 | def concave_fun(x, delta_l, delta_g): 31 | 32 | def f1(x): 33 | n = len(x) 34 | dx = np.linalg.norm(x - 1. / np.sqrt(n)) 35 | return 1 - np.exp(-dx**2) 36 | 37 | def f2(x): 38 | n = len(x) 39 | dx = np.linalg.norm(x + 1. / np.sqrt(n)) 40 | return 1 - np.exp(-dx**2) 41 | 42 | f1_dx = grad(f1) 43 | f2_dx = grad(f2) 44 | 45 | """ 46 | return the function values and gradient values 47 | """ 48 | return np.stack([f1(x), f2(x)]), np.stack([f1_dx(x), f2_dx(x)]) 49 | 50 | 51 | 52 | def construct_log(args): 53 | logger = logging.getLogger(__name__) 54 | logger.setLevel(level = logging.INFO) 55 | os.makedirs(args.log_dir, exist_ok = True) 56 | handler = logging.FileHandler(os.path.join(args.log_dir ,args.log_name)) 57 | handler.setLevel(logging.INFO) 58 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 59 | handler.setFormatter(formatter) 60 | logger.addHandler(handler) 61 | 62 | # console = logging.StreamHandler() 63 | # console.setLevel(logging.ERROR) 64 | # logger.addHandler(console) 65 | return logger 66 | 67 | 68 | 69 | def read_data(train_data_dir, test_data_dir): 70 | 71 | clients = [] 72 | groups = [] 73 | train_data = {} 74 | test_data = {} 75 | 76 | if "eicu" in train_data_dir: 77 | train_files = os.listdir(train_data_dir) 78 | train_files = [f for f in train_files if f.endswith('.npy')] 79 | for f in train_files: 80 | file_path = os.path.join(train_data_dir,f) 81 | cdata = np.load(file_path, allow_pickle=True).tolist() 82 | train_data.update(cdata['user_data']) 83 | 84 | test_files = os.listdir(test_data_dir) 85 | test_files = [f for f in test_files if f.endswith('.npy')] 86 | for f in test_files: 87 | file_path = os.path.join(test_data_dir,f) 88 | cdata = np.load(file_path, allow_pickle=True).tolist() 89 | test_data.update(cdata['user_data']) 90 | 91 | 92 | elif "adult" in train_data_dir: 93 | train_files = os.listdir(train_data_dir) 94 | train_files = [f for f in train_files if f.endswith('.json')] 95 | for f in train_files: 96 | file_path = os.path.join(train_data_dir,f) 97 | with open(file_path, 'r') as inf: 98 | cdata = json.load(inf) 99 | if 'hierarchies' in cdata: 100 | groups.extend(cdata['hierarchies']) 101 | train_data.update(cdata['user_data']) 102 | 103 | test_files = os.listdir(test_data_dir) 104 | test_files = [f for f in test_files if f.endswith('.json')] 105 | for f in test_files: 106 | file_path = os.path.join(test_data_dir,f) 107 | with open(file_path, 'r') as inf: 108 | cdata = json.load(inf) 109 | test_data.update(cdata['user_data']) 110 | 111 | elif "health" in train_data_dir: 112 | train_files = os.listdir(train_data_dir) 113 | train_files = [f for f in train_files if f.endswith('.npy')] 114 | for f in train_files: 115 | file_path = os.path.join(train_data_dir, f) 116 | cdata = np.load(file_path, allow_pickle=True).tolist() 117 | train_data.update(cdata['user_data']) 118 | 119 | test_files = os.listdir(test_data_dir) 120 | test_files = [f for f in test_files if f.endswith('.npy')] 121 | for f in test_files: 122 | file_path = os.path.join(test_data_dir, f) 123 | cdata = np.load(file_path, allow_pickle=True).tolist() 124 | test_data.update(cdata['user_data']) 125 | 126 | clients = list(train_data.keys()) 127 | 128 | return clients, groups, train_data, test_data 129 | 130 | 131 | """ 132 | for adult data, the dim is 99 133 | clients = ["phd", "non-phd"] 134 | X = train_data["phd"]["x"] 135 | 136 | for eicu data, the dim is 53 137 | clients = ["hospital_1", "hospital_2", ... "hospital_11"] 138 | 139 | for health data, the dim is 132 140 | clients = ["152610.0", "240043.0", "791272.0", "140343.0", "251809.0", "164823.0", "122401.0"] 141 | # """ 142 | # clients, groups, train_data, test_data = read_data("/home/sen/workspace/git_code/ICML2021/EPO_copy/mcpo/data/adult/train", "/home/sen/workspace/git_code/ICML2021/EPO_copy/mcpo/data/adult/test") 143 | # pdb.set_trace() 144 | 145 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /FUEL/main.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import argparse 3 | import os 4 | import matplotlib.pyplot as plt 5 | from latex_utils import latexify 6 | from utils import setup_seed, construct_log, get_random_dir_name 7 | from hco_model import MODEL 8 | import time 9 | from tensorboardX import SummaryWriter 10 | 11 | parser = argparse.ArgumentParser() 12 | # parser.add_argument('--other_comment', type = str, default="the_non_phd", help="the dim of the solution") 13 | parser.add_argument('--use_saved_args', type = bool, default=False, help="the dim of the solution") 14 | parser.add_argument('--exp-dir', type = str, default="", help="the dim of the solution") 15 | parser.add_argument('--FedAve', type = bool, default=False, help="the dim of the solution") 16 | parser.add_argument('--target_dir_name', type = str, default="", help="the dim of the solution") 17 | parser.add_argument('--commandline_file', type = str, default="results/args.json", help="the dim of the solution") 18 | parser.add_argument('--eps_g', type = float, default=0.1, help="max_epoch for unbiased_moe") 19 | parser.add_argument('--weight_eps', type=float, default=0.5, 20 | help="eps weight for specific eps") 21 | parser.add_argument('--uniform_eps', action="store_true", help="max_epoch for unbiased_moe") 22 | parser.add_argument('--eps_delta_l', type = float, default=1e-4, help="max_epoch for predictor") 23 | parser.add_argument('--eps_delta_g', type = float, default=1e-4, help="iteras for printing the loss info") 24 | parser.add_argument('--factor_delta', type = float, default=0.1, help="max_epoch for unbiased_moe") 25 | parser.add_argument('--lr_delta', type = float, default=0.01, help="max_epoch for predictor") 26 | parser.add_argument('--delta_l', type = float, default=0.5, help="max_epoch for predictor") 27 | parser.add_argument('--delta_g', type = float, default=0.5, help="max_epoch for predictor") 28 | parser.add_argument('--step_size', type = float, default=0.01, help="iteras for printing the loss info") 29 | parser.add_argument('--max_epoch_stage1', type = int, default=800, help="iteras for printing the loss info") 30 | parser.add_argument('--max_epoch_stage2', type = int, default=800, help="iteras for printing the loss info") 31 | parser.add_argument('--per_epoches', type = int, default=50, help="iteras for printing the loss info") 32 | parser.add_argument('--eval_epoch', type = int, default=20, help="iteras for printing the loss info") 33 | parser.add_argument('--grad_tol', type = float, default=1e-4, help="iteras for printing the loss info") 34 | parser.add_argument('--ckpt_dir', type = str, default= "results/models", help="iteras for printing the loss info") 35 | parser.add_argument('--log_dir', type = str, default= "results", help="iteras for printing the loss info") 36 | parser.add_argument('--log_name', type = str, default= "log", help="iteras for printing the loss info") 37 | parser.add_argument('--board_dir', type = str, default= "results/board", help="iteras for printing the loss info") 38 | parser.add_argument('--store_xs', type = bool, default=False, help="iteras for printing the loss info") 39 | parser.add_argument('--seed', type = int, default=1, help="iteras for printing the loss info") 40 | parser.add_argument('--batch_size', type = list, default=[100, 100], help="iteras for printing the loss info") 41 | parser.add_argument('--shuffle', type = bool, default=True, help="iteras for printing the loss info") 42 | parser.add_argument('--drop_last', type = bool, default=False, help="iteras for printing the loss info") 43 | parser.add_argument('--data_dir', type = str, default="data", help="iteras for printing the loss info") 44 | parser.add_argument('--dataset', type = str, default="adult", help="[adult, eicu_d, eicu_los]") 45 | parser.add_argument('--load_epoch', type = str, default=0, help="iteras for printing the loss info") 46 | parser.add_argument('--global_epoch', type = int, default=0, help="iteras for printing the loss info") 47 | parser.add_argument('--num_workers', type = int, default=0, help="iteras for printing the loss info") 48 | parser.add_argument('--n_feats', type = int, default=10, help="iteras for printing the loss info") 49 | parser.add_argument('--n_hiddens', type = int, default=40, help="iteras for printing the loss info") 50 | parser.add_argument('--sensitive_attr', type = str, default="race", help="iteras for printing the loss info") 51 | parser.add_argument('--valid', type = bool, default=False, help="iteras for printing the loss info") 52 | parser.add_argument('--policy', type = str, default="two_stage", help="[alternating, two_stage]") 53 | parser.add_argument('--uniform', action="store_true", help="uniform mode, without any fairness contraints") 54 | parser.add_argument('--disparity_type', type= str, default= "DP", help="uniform mode, without any fairness contraints") 55 | parser.add_argument('--baseline_type', type= str, default= "none", help="fedave_fair, individual_fair") 56 | parser.add_argument('--weight_fair', type= float, default= 1.0, help="weight for disparity") 57 | args = parser.parse_args() 58 | 59 | 60 | args.eps = [args.eps_g, args.eps_delta_l, args.eps_delta_g] 61 | args.train_dir = os.path.join(args.data_dir, args.dataset, "train") 62 | args.test_dir = os.path.join(args.data_dir, args.dataset, "test") 63 | args.ckpt_dir = os.path.join(args.target_dir_name, args.ckpt_dir) 64 | args.log_dir = os.path.join(args.target_dir_name, args.log_dir) 65 | args.board_dir = os.path.join(args.target_dir_name, args.board_dir) 66 | args.done_dir = os.path.join(args.target_dir_name, "done") 67 | args.commandline_file = os.path.join(args.target_dir_name, args.commandline_file) 68 | 69 | 70 | 71 | 72 | 73 | if __name__ == '__main__': 74 | 75 | writer = SummaryWriter(log_dir = args.board_dir) 76 | if args.use_saved_args: 77 | with open(args.commandline_file, "r") as f: 78 | args.__dict__ = json.load(f) 79 | else: 80 | pass 81 | os.makedirs(args.log_dir, exist_ok = True) 82 | os.system("cp *.py " + args.target_dir_name) 83 | logger = construct_log(args) 84 | setup_seed(seed = args.seed) 85 | model = MODEL(args, logger, writer) 86 | if args.valid: 87 | losses, accs, diss, pred_diss = model.valid_stage1(False, args.max_epoch_stage1) 88 | else: 89 | model.train() 90 | model.save_log() 91 | 92 | -------------------------------------------------------------------------------- /FUEL/synthetic/experiment.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import argparse 3 | from utils import concave_fun_eval, create_pf, circle_points 4 | from hco_search import hco_search 5 | 6 | import matplotlib.pyplot as plt 7 | from latex_utils import latexify 8 | 9 | 10 | parser = argparse.ArgumentParser() 11 | parser.add_argument('--n', type = int, default=20, help="the batch size for the unbiased model") 12 | parser.add_argument('--m', type = int, default=20, help="the batch size for the predicted model") 13 | parser.add_argument('--eps0', type = float, default=0.6, help="max_epoch for unbiased_moe") 14 | parser.add_argument('--eps1', type = float, default=1e-4, help="max_epoch for predictor") 15 | parser.add_argument('--eps2', type = float, default=1e-4, help="iteras for printing the loss info") 16 | parser.add_argument('--para_delta', type = float, default=0.1, help="max_epoch for unbiased_moe") 17 | parser.add_argument('--lr_delta', type = float, default=0.01, help="max_epoch for predictor") 18 | parser.add_argument('--step_size', type = float, default=0.005, help="iteras for printing the loss info") 19 | parser.add_argument('--max_iters', type = int, default=700, help="iteras for printing the loss info") 20 | parser.add_argument('--grad_tol', type = float, default=1e-4, help="iteras for printing the loss info") 21 | parser.add_argument('--store_xs', type = bool, default=False, help="iteras for printing the loss info") 22 | 23 | args = parser.parse_args() 24 | 25 | 26 | 27 | def case1_satisfyingMCF(): 28 | n = args.n # dim of solution space 29 | m = args.m # dim of objective space 30 | ##construct x0 31 | x0 = np.zeros(n) 32 | x0[range(0, n, 2)] = -0.2 33 | x0[range(1, n, 2)] = -0.2 34 | eps_set = [0.8, 0.6, 0.4, 0.2] 35 | color_set = ["c", "g", "orange", "b"] 36 | latexify(fig_width=2.2, fig_height=1.8) 37 | l0, _ = concave_fun_eval(x0) 38 | max_iters = args.max_iters 39 | relax = True 40 | pf = create_pf() 41 | fig = plt.figure() 42 | fig.subplots_adjust(left=.12, bottom=.12, right=.9, top=.9) 43 | label = 'Pareto\nFront' if relax else '' 44 | plt.plot(pf[:, 0], pf[:, 1], lw=2.0, c='k', label=label) 45 | label = r'$l(\theta^0)$' 46 | plt.scatter([l0[0]], [l0[1]], c='r', s=40) 47 | plt.annotate(label, xy = (l0[0]+0.03, l0[1]), xytext = (l0[0]+0.03, l0[1])) 48 | for idx, eps0 in enumerate(eps_set): 49 | if eps0 == 0.2: 50 | eps_plot = np.array([[ i*0.1 * 0.903, 0.2] for i in range(11)]) 51 | plt.plot(eps_plot[:,0], eps_plot[:,1], color = "gray", label = r'$\epsilon$', lw=1, ls='--') 52 | elif eps0 == 0.4: 53 | eps_plot = np.array([[ i*0.1 * 0.807, 0.4] for i in range(11)]) 54 | plt.plot(eps_plot[:,0], eps_plot[:,1], color = "gray", lw=1, ls='--') 55 | elif eps0 == 0.6: 56 | eps_plot = np.array([[ i*0.1* 0.652, 0.6] for i in range(11)]) 57 | plt.plot(eps_plot[:,0], eps_plot[:,1], color = "gray", lw=1, ls='--') 58 | elif eps0 == 0.8: 59 | eps_plot = np.array([[ i*0.1 * 0.412, 0.8] for i in range(11)]) 60 | plt.plot(eps_plot[:,0], eps_plot[:,1], color = "gray", lw=1, ls='--') 61 | else: 62 | print("error eps0") 63 | exit() 64 | c = color_set[idx] 65 | _, res = hco_search(concave_fun_eval, x=x0, deltas = [0.5, 0.5], para_delta = 0.5, lr_delta = args.lr_delta, relax=False, eps=[eps0, args.eps1, args.eps2], max_iters=max_iters, 66 | n_dim=args.n, step_size=args.step_size, grad_tol=args.grad_tol, store_xs=args.store_xs) 67 | ls = res['ls'] 68 | alpha = 1.0 69 | zorder = 1 70 | # plt.plot(ls[:, 0], ls[:, 1], c=c, lw=2.0, alpha=alpha, zorder=zorder) 71 | plt.plot(ls[:, 0], ls[:, 1], c=c, lw=2.0) 72 | print(ls[-1]) 73 | plt.scatter(ls[[-1], 0], ls[[-1], 1], c=c, s=40) 74 | plt.xlabel(r'$l_1$') 75 | plt.ylabel(r'$l_2$', rotation = "horizontal") 76 | plt.legend(loc='lower left', handletextpad=0.3, framealpha=0.9) 77 | ax = plt.gca() 78 | ax.xaxis.set_label_coords(1.05, -0.02) 79 | ax.yaxis.set_label_coords(-0.02, 1.02) 80 | ax.spines['right'].set_color('none') 81 | ax.spines['top'].set_color('none') 82 | plt.savefig('figures/satifying' + '.pdf') 83 | plt.close() 84 | 85 | 86 | 87 | def case2_violatingMCF(): 88 | n = args.n # dim of solution space 89 | m = args.m # dim of objective space 90 | ##construct x0 91 | x0 = np.zeros(n) 92 | x0[range(0, n, 2)] = 0.3 93 | x0[range(1, n, 2)] = -0.3 94 | eps_set = [0.2, 0.4, 0.6, 0.8] 95 | color_set = ["c", "g", "orange", "b"] 96 | latexify(fig_width=2.2, fig_height=1.8) 97 | l0, _ = concave_fun_eval(x0) 98 | max_iters = args.max_iters 99 | relax = True 100 | pf = create_pf() 101 | fig = plt.figure() 102 | fig.subplots_adjust(left=.12, bottom=.12, right=.9, top=.9) 103 | label = 'Pareto\nFront' if relax else '' 104 | plt.plot(pf[:, 0], pf[:, 1], lw=2.0, c='k', label=label) 105 | label = r'$l(\theta^0)$' 106 | plt.scatter([l0[0]], [l0[1]], c='r', s=40) 107 | plt.annotate(label, xy = (l0[0]+0.03, l0[1]), xytext = (l0[0]+0.03, l0[1])) 108 | for idx, eps0 in enumerate(eps_set): 109 | if eps0 == 0.2: 110 | eps_plot = np.array([[ i*0.1 * 0.903, 0.2] for i in range(11)]) 111 | plt.plot(eps_plot[:,0], eps_plot[:,1], color = "gray", label = r'$\epsilon$', lw=1, ls='--') 112 | elif eps0 == 0.4: 113 | eps_plot = np.array([[ i*0.1 * 0.807, 0.4] for i in range(11)]) 114 | plt.plot(eps_plot[:,0], eps_plot[:,1], color = "gray", lw=1, ls='--') 115 | elif eps0 == 0.6: 116 | eps_plot = np.array([[ i*0.1* 0.652, 0.6] for i in range(11)]) 117 | plt.plot(eps_plot[:,0], eps_plot[:,1], color = "gray", lw=1, ls='--') 118 | elif eps0 == 0.8: 119 | eps_plot = np.array([[ i*0.1 * 0.412, 0.8] for i in range(11)]) 120 | plt.plot(eps_plot[:,0], eps_plot[:,1], color = "gray", lw=1, ls='--') 121 | else: 122 | print("error eps0") 123 | exit() 124 | c = color_set[idx] 125 | _, res = hco_search(concave_fun_eval, x=x0, deltas = [0.5, 0.5], para_delta = 0.5, lr_delta = args.lr_delta, relax=False, eps=[eps0, args.eps1, args.eps2], max_iters=max_iters, 126 | n_dim=args.n, step_size=args.step_size, grad_tol=args.grad_tol, store_xs=args.store_xs) 127 | ls = res['ls'] 128 | alpha = 1.0 129 | zorder = 1 130 | plt.plot(ls[:, 0], ls[:, 1], c=c, lw=2.0, alpha=alpha, zorder=zorder) 131 | print(ls[-1]) 132 | plt.scatter(ls[[-1], 0], ls[[-1], 1], c=c, s=40) 133 | plt.xlabel(r'$l_1$') 134 | plt.ylabel(r'$l_2$', rotation = "horizontal") 135 | plt.legend(loc='lower left', handletextpad=0.3, framealpha=0.9) 136 | ax = plt.gca() 137 | ax.xaxis.set_label_coords(1.05, -0.02) 138 | ax.yaxis.set_label_coords(-0.02, 1.02) 139 | ax.spines['right'].set_color('none') 140 | ax.spines['top'].set_color('none') 141 | plt.savefig('figures/violating' + '.pdf') 142 | plt.close() 143 | 144 | 145 | if __name__ == '__main__': 146 | case1_satisfyingMCF() 147 | case2_violatingMCF() -------------------------------------------------------------------------------- /FUEL/README.md: -------------------------------------------------------------------------------- 1 | # Fair and Consistent Federated Learning 2 | 3 | 4 | ## introduction for all files 5 | * dataset_generate.py: the preprocessing code for processing the Adult dataset following exactly the same data processing procedures described in (https://arxiv.org/abs/1902.00146 and https://openreview.net/forum?id=ByexElSYDr). 6 | * dataload.py: loading dataset for the model 7 | * hco_lp.py: the function about searching for a descent direction in stage 1 (constrained min-max optimization) 8 | * po_lp.py: the function about searching for a descent direction in stage 2 (constrained Pareto optimization) 9 | * hco_model.py: the source code of the model including training, testing and saving. 10 | * utils.py: needed function for the implementation 11 | * main.py: the main function for all real-world dataset experiments. 12 | 13 | ## requirements 14 | python 3.6, the needed libraries are in requirements.txt 15 | 16 | ## dataset generatation: 17 | * download the original adult dataset: 18 | `wget https://archive.ics.uci.edu/ml/machine-learning-databases/adult/adult.data` 19 | `wget https://archive.ics.uci.edu/ml/machine-learning-databases/adult/adult.test` 20 | 21 | * dataset split 22 | `python dataset_generate.py` 23 | the split dataset is in ./data/train and ./data/test 24 | 25 | ## synthetic experiment 26 | We conduct the experiments on two different setting 1) the initilization satisfies fairness constraints; 2) the initilization violates the fairness constraints. All results in the two setting can be reproducted by: 27 | `cd synthetic` 28 | `python experiment.py` 29 | and the generated results are in ./synthetic/figures/ 30 | 31 | ## real-world dataset experiment 32 | 33 | **part of the parameter meanning in main.py** 34 | * sensitive_attr: sensitive attribute ("race" or "sex") 35 | * step_size: learning rate 36 | * eps_g: uniform fairness budget (all clients are assigned the same fairness budget) 37 | * max_epoch_stage1: the max iteration num for stage 1 (constrained min-max optimization) 38 | * max_epoch_stage2: the max iteration num for stage 2 (constrained Pareto optimization) 39 | * uniform_eps: bool variable (if we adopt uniform fairness budget setting, uniform_eps == True) 40 | * weight_eps: the ratio for specific client budget ("w" in the main text, specific client budget setting) 41 | * target_dir_name: the dir for saving results (including models, logs and args) 42 | 43 | ### uniform fairness budget on both clients 44 | * sensitive atttribute as ***race***, fairness budget as ***0.05*** 45 | `python main.py --step_size 0.03 --eps_g 0.05 --sensitive_attr race --max_epoch_stage1 800 --max_epoch_stage2 1500 --seed 1 --target_dir_name race_DP_0-05 --uniform_eps` 46 | ***results:*** is_train: False, epoch: 2299, loss: [0.5601576566696167, 0.39812201261520386], accuracy: [0.7292817831039429, 0.8195652365684509], auc: [0.6859285714285713, 0.8504551633040838], disparity: [0.0005307793617248535, 0.04531855136156082], pred_disparity: [0.0260276198387146, 0.04738526791334152] 47 | * sensitive attribute as ***race***, fairnesss budget as ***0.01*** 48 | `python main.py --step_size 0.015 --eps_g 0.01 --sensitive_attr race --max_epoch_stage1 800 --max_epoch_stage2 1500 --seed 1 --target_dir_name race_DP_0-01 --uniform_eps` 49 | ***results:*** is_train: False, epoch: 2299, loss: [0.5744820237159729, 0.4304683208465576], accuracy: [0.7016574740409851, 0.7970807552337646], auc: [0.6947857142857142, 0.837447616279523], disparity: [-0.002919316291809082, 0.0034494660794734955], pred_disparity: [-0.0016486644744873047, 0.008047450333833694] 50 | 51 | ### specific fairness budget on both clients 52 | take an example, we set sensitive attribute as ***race***, the original disparity is $\Delta DP = [DP_1, DP_2]$. 53 | The original disparities of both clients is measured by running: 54 | `python main.py --step_size 0.13 --max_epoch_stage1 800 --max_epoch_stage2 1000 --eps_g 1.0 --sensitive_attr race --seed 1 --target_dir_name race_specific_1-0 --uniform --uniform_eps` 55 | ***results:*** is_train: False, epoch: 1799, loss: [0.5699087977409363, 0.3846117854118347], accuracy: [0.7348066568374634, 0.8285093307495117], auc: [0.6905, 0.8529878658361068], disparity: [0.15790873765945435, 0.07422562688589096], pred_disparity: [0.15371835231781006, 0.07583809643983841] 56 | where there is no fairness constraints, and the measured original disparity are [0.15790873765945435, 0.07422562688589096]. 57 | 58 | * w = 0.8 (the fairness budget of both clients are 0.8* $\Delta DP$) 59 | `python main.py --weight_eps 0.8 --max_epoch_stage1 800 --max_epoch_stage2 1000 --step_size 0.07 --sensitive_attr race --seed 1 --target_dir_name race_specific_0-8` 60 | ***results:*** is_train: False, epoch: 1799, loss: [0.5635322332382202, 0.38921427726745605], accuracy: [0.7513812184333801, 0.8263354301452637], auc: [0.6877857142857143, 0.8522060544186512], disparity: [0.11624205112457275, 0.06827643513679504], pred_disparity: [0.1026315689086914, 0.0648479089140892] 61 | 62 | * w = 0.5 (the fairness budget of both clients are 0.5* $\Delta DP$) 63 | `python main.py --weight_eps 0.5 --max_epoch_stage1 800 --max_epoch_stage2 1000 --step_size 0.05 --sensitive_attr race --seed 1 --target_dir_name race_specific_0-5` 64 | ***results:*** is_train: False, epoch: 1799, loss: [0.5625894069671631, 0.394794762134552], accuracy: [0.7403315305709839, 0.820621132850647], auc: [0.6882142857142858, 0.8510285611480758], disparity: [0.05838644504547119, 0.04976653307676315], pred_disparity: [0.036145806312561035, 0.05463084578514099] 65 | 66 | * w = 0.2 (the fairness budget of both clients are 0.5* $\Delta DP$) 67 | `python main.py --weight_eps 0.2 --max_epoch_stage1 800 --max_epoch_stage2 1000 --step_size 0.03 --sensitive_attr race --seed 1 --target_dir_name race_specific_0-2` 68 | ***results:*** is_train: False, epoch: 1799, loss: [0.5699670910835266, 0.41371434926986694], accuracy: [0.7182320952415466, 0.8140993714332581], auc: [0.6844999999999999, 0.8462134911794057], disparity: [-0.0092887282371521, 0.03283762186765671], pred_disparity: [-0.0035886168479919434, 0.024312380701303482] 69 | 70 | ### Equal opportunity measurement with uniform fairness budget on both clients 71 | EO is also a popular disparity metric. The experiment results can be easily obtained by assigning "Eoppo" to the parameter "disparity_type". 72 | 73 | For example, we select the sensitive attribute as ***race***, fairness budget is 0.05: 74 | 75 | `python main.py --sensitive_attr race --step_size 0.1 --disparity_type Eoppo --max_epoch_stage1 800 --max_epoch_stage2 220 --eps_g 0.05 --target_dir_name adult_race_Eoppo_0-1 --seed 1 --uniform_eps` 76 | 77 | ***results:*** is_train: False, epoch: 1019, loss: [0.5672342777252197, 0.4211762547492981], accuracy: [0.7237569093704224, 0.8137267231941223], auc: [0.6760714285714287, 0.8362089015217425], disparity: [-0.054545462131500244, 0.02255287766456604], pred_disparity: [-0.017567992210388184, 0.034288644790649414] 78 | 79 | if we set the sensitive attribute as ***sex***, fairness budget is 0.05: 80 | 81 | `python main.py --sensitive_attr sex --step_size 0.1 --disparity_type Eoppo --max_epoch_stage1 800 --max_epoch_stage2 220 --eps_g 0.05 --target_dir_name adult_sex_Eoppo_0-1 --seed 1 --uniform_eps` 82 | 83 | ***results:*** is_train: False, epoch: 1019, loss: [0.5760114789009094, 0.4152376055717468], accuracy: [0.6961326003074646, 0.8184472322463989], auc: [0.6787857142857142, 0.8374747968830623], disparity: [0.0006434917449951172, -0.04236716032028198], pred_disparity: [0.0183182954788208, -0.04114463925361633] 84 | 85 | -------------------------------------------------------------------------------- /FUEL/dataload.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | import numpy as np 4 | from utils import read_data 5 | import torch 6 | from torch.utils.data import Dataset, DataLoader 7 | import pdb 8 | 9 | class Federated_Dataset(Dataset): 10 | def __init__(self, X, Y, A): 11 | self.X = X 12 | self.Y = Y 13 | self.A = A 14 | 15 | def __getitem__(self, index): 16 | X = self.X[index] 17 | Y = self.Y[index] 18 | A = self.A[index] 19 | return X, Y, A 20 | 21 | def __len__(self): 22 | return self.X.shape[0] 23 | 24 | 25 | #### adult dataset x("51 White", "52 Asian-Pac-Islander", "53 Amer-Indian-Eskimo", "54 Other", "55 Black", "56 Female", "57 Male") 26 | def LoadDataset(args): 27 | clients_name, groups, train_data, test_data = read_data(args.train_dir, args.test_dir) 28 | 29 | # client_name [phd, non-phd] 30 | client_train_loads = [] 31 | client_test_loads = [] 32 | args.n_clients = len(clients_name) 33 | # clients_name = clients_name[:1] 34 | if args.dataset == "adult": 35 | for client in clients_name: 36 | X = np.array(train_data[client]["x"]).astype(np.float32) 37 | 38 | Y = np.array(train_data[client]["y"]).astype(np.float32) 39 | 40 | if args.sensitive_attr == "race": 41 | A = X[:,51] # [1: white, 0: other] 42 | X = np.delete(X, [51, 52, 53, 54, 55], axis = 1) 43 | args.n_feats = X.shape[1] 44 | elif args.sensitive_attr == "sex": 45 | A = X[:, 56] # [1: female, 0: male] 46 | X = np.delete(X, [56, 57], axis = 1) 47 | args.n_feats = X.shape[1] 48 | elif args.sensitive_attr == "none-race": 49 | A = X[:, 51] # [1: white, 0: other] 50 | args.n_feats = X.shape[1] 51 | elif args.sensitive_attr == "none-sex": 52 | A = X[:, 56] 53 | args.n_feats = X.shape[1] 54 | else: 55 | print("error sensitive attr") 56 | exit() 57 | dataset = Federated_Dataset(X, Y, A) 58 | client_train_loads.append(DataLoader(dataset, X.shape[0], 59 | shuffle = args.shuffle, 60 | num_workers = args.num_workers, 61 | pin_memory = True, 62 | drop_last = args.drop_last)) 63 | 64 | 65 | for client in clients_name: 66 | X = np.array(test_data[client]["x"]).astype(np.float32) 67 | Y = np.array(test_data[client]["y"]).astype(np.float32) 68 | if args.sensitive_attr =="race": 69 | A = X[:,51] # [1: white, 0: other] 70 | X = np.delete(X, [51, 52, 53, 54, 55],axis = 1) 71 | elif args.sensitive_attr == "sex": 72 | A = X[:, 56] # [1: female, 0: male] 73 | X = np.delete(X, [56, 57], axis = 1) 74 | elif args.sensitive_attr == "none-race": 75 | A = X[:, 51] # [1: white, 0: other] 76 | args.n_feats = X.shape[1] 77 | elif args.sensitive_attr == "none-sex": 78 | A = X[:, 56] 79 | args.n_feats = X.shape[1] 80 | else: 81 | print("error sensitive attr") 82 | exit() 83 | 84 | dataset = Federated_Dataset(X, Y, A) 85 | 86 | client_test_loads.append(DataLoader(dataset, X.shape[0], 87 | shuffle = args.shuffle, 88 | num_workers = args.num_workers, 89 | pin_memory = True, 90 | drop_last = args.drop_last)) 91 | 92 | elif "eicu" in args.dataset: 93 | # elif args.dataset == "eicu_d" or args.dataset == "eicu_los": 94 | for client in clients_name: 95 | X = np.array(train_data[client]["x"]).astype(np.float32) 96 | 97 | Y = np.array(train_data[client]["y"]).astype(np.float32) 98 | 99 | if args.sensitive_attr == "race": 100 | A = train_data[client]["race"] 101 | args.n_feats = X.shape[1] 102 | elif args.sensitive_attr == "sex": 103 | A = train_data[client]["gender"] 104 | args.n_feats = X.shape[1] 105 | else: 106 | A = train_data[client]["race"] 107 | args.n_feats = X.shape[1] 108 | dataset = Federated_Dataset(X, Y, A) 109 | client_train_loads.append(DataLoader(dataset, X.shape[0], 110 | shuffle = args.shuffle, 111 | num_workers = args.num_workers, 112 | pin_memory = True, 113 | drop_last = args.drop_last)) 114 | 115 | for client in clients_name: 116 | X = np.array(test_data[client]["x"]).astype(np.float32) 117 | Y = np.array(test_data[client]["y"]).astype(np.float32) 118 | if args.sensitive_attr =="race": 119 | A = test_data[client]["race"] 120 | elif args.sensitive_attr == "sex": 121 | A = test_data[client]["gender"] 122 | else: 123 | A = test_data[client]["race"] 124 | 125 | dataset = Federated_Dataset(X, Y, A) 126 | 127 | client_test_loads.append(DataLoader(dataset, X.shape[0], 128 | shuffle = args.shuffle, 129 | num_workers = args.num_workers, 130 | pin_memory = True, 131 | drop_last = args.drop_last)) 132 | 133 | elif args.dataset == "health": 134 | for client in clients_name: 135 | X = np.array(train_data[client]["x"]).astype(np.float32) 136 | 137 | Y = np.array(train_data[client]["y"]).astype(np.float32) 138 | 139 | if args.sensitive_attr == "race": 140 | A = train_data[client]["race"] 141 | args.n_feats = X.shape[1] 142 | elif args.sensitive_attr == "sex": 143 | A = train_data[client]["isfemale"] 144 | args.n_feats = X.shape[1] 145 | else: 146 | A = train_data[client]["isfemale"] 147 | args.n_feats = X.shape[1] 148 | dataset = Federated_Dataset(X, Y, A) 149 | client_train_loads.append(DataLoader(dataset, X.shape[0], 150 | shuffle=args.shuffle, 151 | num_workers=args.num_workers, 152 | pin_memory=True, 153 | drop_last=args.drop_last)) 154 | 155 | for client in clients_name: 156 | X = np.array(test_data[client]["x"]).astype(np.float32) 157 | Y = np.array(test_data[client]["y"]).astype(np.float32) 158 | if args.sensitive_attr == "race": 159 | A = test_data[client]["race"] 160 | elif args.sensitive_attr == "sex": 161 | A = test_data[client]["isfemale"] 162 | else: 163 | A = np.zeros(X.shape[0]) 164 | 165 | dataset = Federated_Dataset(X, Y, A) 166 | 167 | client_test_loads.append(DataLoader(dataset, X.shape[0], 168 | shuffle=args.shuffle, 169 | num_workers=args.num_workers, 170 | pin_memory=True, 171 | drop_last=args.drop_last)) 172 | 173 | return client_train_loads, client_test_loads 174 | 175 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fair and Consistent Federated Learning 2 | the implementation of FCFL (https://arxiv.org/abs/2108.08435), accepted by NeurIPS 2021. 3 | 4 | ## introduction for all files 5 | * dataset_generate.py: the preprocessing code for processing the Adult dataset following exactly the same data processing procedures described in (https://arxiv.org/abs/1902.00146 and https://openreview.net/forum?id=ByexElSYDr). 6 | * dataload.py: loading dataset for the model 7 | * hco_lp.py: the function about searching for a descent direction in stage 1 (constrained min-max optimization) 8 | * po_lp.py: the function about searching for a descent direction in stage 2 (constrained Pareto optimization) 9 | * hco_model.py: the source code of the model including training, testing and saving. 10 | * utils.py: needed function for the implementation 11 | * main.py: the main function for all real-world dataset experiments. 12 | 13 | ## requirements 14 | python 3.6, the needed libraries are in requirements.txt 15 | 16 | ## dataset generatation: 17 | * download the original adult dataset: 18 | `wget https://archive.ics.uci.edu/ml/machine-learning-databases/adult/adult.data` 19 | `wget https://archive.ics.uci.edu/ml/machine-learning-databases/adult/adult.test` 20 | 21 | * dataset split 22 | `python dataset_generate.py` 23 | the split dataset should be in ./data/adult/train and ./data/adult/test (eICU data set is not directly available and detailed description is in the paper https://arxiv.org/abs/2108.08435) 24 | 25 | ## synthetic experiment 26 | We conduct the experiments on two different setting 1) the initilization satisfies fairness constraints; 2) the initilization violates the fairness constraints. All results in the two setting can be reproducted by: 27 | `cd synthetic` 28 | `python experiment.py` 29 | and the generated results are in ./synthetic/figures/ 30 | 31 | ## real-world dataset experiment 32 | 33 | **part of the parameter meanning in main.py** 34 | * sensitive_attr: sensitive attribute ("race" or "sex") 35 | * step_size: learning rate 36 | * eps_g: uniform fairness budget (all clients are assigned the same fairness budget) 37 | * max_epoch_stage1: the max iteration num for stage 1 (constrained min-max optimization) 38 | * max_epoch_stage2: the max iteration num for stage 2 (constrained Pareto optimization) 39 | * uniform_eps: bool variable (if we adopt uniform fairness budget setting, uniform_eps == True) 40 | * weight_eps: the ratio for specific client budget ("w" in the main text, specific client budget setting) 41 | * target_dir_name: the dir for saving results (including models, logs and args) 42 | 43 | ### uniform fairness budget on both clients 44 | * sensitive atttribute as ***race***, fairness budget as ***0.05*** 45 | `python main.py --step_size 0.03 --eps_g 0.05 --sensitive_attr race --max_epoch_stage1 800 --max_epoch_stage2 1500 --seed 1 --target_dir_name race_DP_0-05 --uniform_eps` 46 | ***results:*** is_train: False, epoch: 2299, loss: [0.5601576566696167, 0.39812201261520386], accuracy: [0.7292817831039429, 0.8195652365684509], auc: [0.6859285714285713, 0.8504551633040838], disparity: [0.0005307793617248535, 0.04531855136156082], pred_disparity: [0.0260276198387146, 0.04738526791334152] 47 | * sensitive attribute as ***race***, fairnesss budget as ***0.01*** 48 | `python main.py --step_size 0.015 --eps_g 0.01 --sensitive_attr race --max_epoch_stage1 800 --max_epoch_stage2 1500 --seed 1 --target_dir_name race_DP_0-01 --uniform_eps` 49 | ***results:*** is_train: False, epoch: 2299, loss: [0.5744820237159729, 0.4304683208465576], accuracy: [0.7016574740409851, 0.7970807552337646], auc: [0.6947857142857142, 0.837447616279523], disparity: [-0.002919316291809082, 0.0034494660794734955], pred_disparity: [-0.0016486644744873047, 0.008047450333833694] 50 | 51 | ### specific fairness budget on both clients 52 | take an example, we set sensitive attribute as ***race***, the original disparity is $\Delta DP = [DP_1, DP_2]$. 53 | The original disparities of both clients is measured by running: 54 | `python main.py --step_size 0.13 --max_epoch_stage1 800 --max_epoch_stage2 1000 --eps_g 1.0 --sensitive_attr race --seed 1 --target_dir_name race_specific_1-0 --uniform --uniform_eps` 55 | ***results:*** is_train: False, epoch: 1799, loss: [0.5699087977409363, 0.3846117854118347], accuracy: [0.7348066568374634, 0.8285093307495117], auc: [0.6905, 0.8529878658361068], disparity: [0.15790873765945435, 0.07422562688589096], pred_disparity: [0.15371835231781006, 0.07583809643983841] 56 | where there is no fairness constraints, and the measured original disparity are [0.15790873765945435, 0.07422562688589096]. 57 | 58 | * w = 0.8 (the fairness budget of both clients are 0.8* $\Delta DP$) 59 | `python main.py --weight_eps 0.8 --max_epoch_stage1 800 --max_epoch_stage2 1000 --step_size 0.07 --sensitive_attr race --seed 1 --target_dir_name race_specific_0-8` 60 | ***results:*** is_train: False, epoch: 1799, loss: [0.5635322332382202, 0.38921427726745605], accuracy: [0.7513812184333801, 0.8263354301452637], auc: [0.6877857142857143, 0.8522060544186512], disparity: [0.11624205112457275, 0.06827643513679504], pred_disparity: [0.1026315689086914, 0.0648479089140892] 61 | 62 | * w = 0.5 (the fairness budget of both clients are 0.5* $\Delta DP$) 63 | `python main.py --weight_eps 0.5 --max_epoch_stage1 800 --max_epoch_stage2 1000 --step_size 0.05 --sensitive_attr race --seed 1 --target_dir_name race_specific_0-5` 64 | ***results:*** is_train: False, epoch: 1799, loss: [0.5625894069671631, 0.394794762134552], accuracy: [0.7403315305709839, 0.820621132850647], auc: [0.6882142857142858, 0.8510285611480758], disparity: [0.05838644504547119, 0.04976653307676315], pred_disparity: [0.036145806312561035, 0.05463084578514099] 65 | 66 | * w = 0.2 (the fairness budget of both clients are 0.5* $\Delta DP$) 67 | `python main.py --weight_eps 0.2 --max_epoch_stage1 800 --max_epoch_stage2 1000 --step_size 0.03 --sensitive_attr race --seed 1 --target_dir_name race_specific_0-2` 68 | ***results:*** is_train: False, epoch: 1799, loss: [0.5699670910835266, 0.41371434926986694], accuracy: [0.7182320952415466, 0.8140993714332581], auc: [0.6844999999999999, 0.8462134911794057], disparity: [-0.0092887282371521, 0.03283762186765671], pred_disparity: [-0.0035886168479919434, 0.024312380701303482] 69 | 70 | ### Equal opportunity measurement with uniform fairness budget on both clients 71 | EO is also a popular disparity metric. The experiment results can be easily obtained by assigning "Eoppo" to the parameter "disparity_type". 72 | 73 | For example, we select the sensitive attribute as ***race***, fairness budget is 0.05: 74 | 75 | `python main.py --sensitive_attr race --step_size 0.1 --disparity_type Eoppo --max_epoch_stage1 800 --max_epoch_stage2 220 --eps_g 0.05 --target_dir_name adult_race_Eoppo_0-1 --seed 1 --uniform_eps` 76 | 77 | ***results:*** is_train: False, epoch: 1019, loss: [0.5672342777252197, 0.4211762547492981], accuracy: [0.7237569093704224, 0.8137267231941223], auc: [0.6760714285714287, 0.8362089015217425], disparity: [-0.054545462131500244, 0.02255287766456604], pred_disparity: [-0.017567992210388184, 0.034288644790649414] 78 | 79 | if we set the sensitive attribute as ***sex***, fairness budget is 0.05: 80 | 81 | `python main.py --sensitive_attr sex --step_size 0.1 --disparity_type Eoppo --max_epoch_stage1 800 --max_epoch_stage2 220 --eps_g 0.05 --target_dir_name adult_sex_Eoppo_0-1 --seed 1 --uniform_eps` 82 | 83 | ***results:*** is_train: False, epoch: 1019, loss: [0.5760114789009094, 0.4152376055717468], accuracy: [0.6961326003074646, 0.8184472322463989], auc: [0.6787857142857142, 0.8374747968830623], disparity: [0.0006434917449951172, -0.04236716032028198], pred_disparity: [0.0183182954788208, -0.04114463925361633] 84 | 85 | -------------------------------------------------------------------------------- /FUEL/dataset_generate.py: -------------------------------------------------------------------------------- 1 | import json 2 | import math 3 | import numpy as np 4 | import os 5 | import sys 6 | import random 7 | import pdb 8 | import math 9 | 10 | from numpy import genfromtxt 11 | import numpy as np 12 | 13 | NUM_USER = 2 14 | 15 | training_dir = "data/adult.train" 16 | testing_dir = "data/adult.test" 17 | 18 | inputs = ( 19 | ("age", ("continuous",)), 20 | ("workclass", ("Private", "Self-emp-not-inc", "Self-emp-inc", "Federal-gov", "Local-gov", "State-gov", "Without-pay", "Never-worked")), 21 | ("fnlwgt", ("continuous",)), 22 | ("education", ("Bachelors", "Some-college", "11th", "HS-grad", "Prof-school", "Assoc-acdm", "Assoc-voc", "9th", "7th-8th", "12th", "Masters", "1st-4th", "10th", "Doctorate", "5th-6th", "Preschool")), 23 | ("education-num", ("continuous",)), 24 | ("marital-status", ("Married-civ-spouse", "Divorced", "Never-married", "Separated", "Widowed", "Married-spouse-absent", "Married-AF-spouse")), 25 | ("occupation", ("Tech-support", "Craft-repair", "Other-service", "Sales", "Exec-managerial", "Prof-specialty", "Handlers-cleaners", "Machine-op-inspct", "Adm-clerical", "Farming-fishing", "Transport-moving", "Priv-house-serv", "Protective-serv", "Armed-Forces")), 26 | ("relationship", ("Wife", "Own-child", "Husband", "Not-in-family", "Other-relative", "Unmarried")), 27 | ("race", ("White", "Asian-Pac-Islander", "Amer-Indian-Eskimo", "Other", "Black")), 28 | ("sex", ("Female", "Male")), 29 | ("capital-gain", ("continuous",)), 30 | ("capital-loss", ("continuous",)), 31 | ("hours-per-week", ("continuous",)), 32 | ("native-country", ("United-States", "Cambodia", "England", "Puerto-Rico", "Canada", "Germany", "Outlying-US(Guam-USVI-etc)", "India", "Japan", "Greece", "South", "China", "Cuba", "Iran", "Honduras", "Philippines", "Italy", "Poland", "Jamaica", "Vietnam", "Mexico", "Portugal", "Ireland", "France", "Dominican-Republic", "Laos", "Ecuador", "Taiwan", "Haiti", "Columbia", "Hungary", "Guatemala", "Nicaragua", "Scotland", "Thailand", "Yugoslavia", "El-Salvador", "Trinadad&Tobago", "Peru", "Hong", "Holand-Netherlands")) 33 | ) 34 | 35 | def isFloat(string): 36 | # credits: http://stackoverflow.com/questions/2356925/how-to-check-whether-string-might-be-type-cast-to-float-in-python 37 | try: 38 | float(string) 39 | return True 40 | except ValueError: 41 | return False 42 | 43 | def find_means_for_continuous_types(X): 44 | means = [] 45 | for col in range(len(X[0])): 46 | summ = 0 47 | count = 0.000000000000000000001 48 | for value in X[:, col]: 49 | if isFloat(value): 50 | summ += float(value) 51 | count +=1 52 | means.append(summ/count) 53 | return means 54 | 55 | def generate_dataset(file_path): 56 | 57 | input_shape = [] 58 | for i in inputs: 59 | count = len(i[1 ]) 60 | input_shape.append(count) 61 | input_dim = sum(input_shape) 62 | 63 | 64 | outputs = (0, 1) # (">50K", "<=50K") 65 | output_dim = 2 # len(outputs) 66 | 67 | #input_shape: [1, 8, 1, 16, 1, 7, 14, 6, 5, 2, 1, 1, 1, 41] 68 | #input_dim: 105 (99) 69 | 70 | #output_dim: 2 71 | 72 | def prepare_data(raw_data, means): 73 | 74 | X = raw_data[:, :-1] 75 | y = raw_data[:, -1:] 76 | print(y) 77 | 78 | # X: 79 | def flatten_persons_inputs_for_model(person_inputs, means): 80 | input_shape = [1, 8, 1, 16, 1, 7, 14, 6, 5, 2, 1, 1, 1, 41] 81 | float_inputs = [] 82 | 83 | for i in range(len(input_shape)): 84 | features_of_this_type = input_shape[i] 85 | is_feature_continuous = features_of_this_type == 1 86 | 87 | if is_feature_continuous: 88 | # in order to be consistant with the google paper -- only train with categorical features 89 | ''' 90 | mean = means[i] 91 | if isFloat(person_inputs[i]): 92 | scale_factor = 1/(2*mean) # we prefer inputs mainly scaled from -1 to 1. 93 | float_inputs.append(float(person_inputs[i])*scale_factor) 94 | else: 95 | float_inputs.append(mean) 96 | ''' 97 | pass 98 | else: 99 | for j in range(features_of_this_type): 100 | feature_name = inputs[i][1][j] 101 | 102 | if feature_name == person_inputs[i]: 103 | float_inputs.append(1.) 104 | # if inputs[i][0] == "race": 105 | # print("race:", len(float_inputs)) 106 | # if inputs[i][0] == "sex": 107 | # print("sex:", len(float_inputs)) 108 | 109 | else: 110 | float_inputs.append(0) 111 | # if inputs[i][0] == "race": 112 | # print("race:", len(float_inputs)) 113 | # if inputs[i][0] == "sex": 114 | # print("sex:", len(float_inputs)) 115 | 116 | 117 | return float_inputs 118 | 119 | new_X = [] 120 | for person in range(len(X)): 121 | formatted_X = flatten_persons_inputs_for_model(X[person], means) 122 | new_X.append(formatted_X) 123 | new_X = np.array(new_X) 124 | 125 | # y: 126 | new_y = [] 127 | for i in range(len(y)): 128 | if y[i] == ">50K" or y[i] == ">50K.": 129 | new_y.append(1) 130 | else: # y[i] == "<=50k": 131 | new_y.append(0) 132 | 133 | new_y = np.array(new_y) 134 | 135 | return (new_X, new_y) 136 | 137 | def generate_dataset(file_path): 138 | data = np.genfromtxt(file_path, delimiter=', ', dtype=str, autostrip=True) 139 | print("Data {} count: {}".format(file_path, len(data))) 140 | print(data[0]) 141 | print(len(data[0])) 142 | 143 | means = find_means_for_continuous_types(data) 144 | print("Mean values for data types (if continuous): {}".format(means)) 145 | 146 | X, y = prepare_data(data, means) 147 | print(X[0].shape) 148 | print(X[0]) 149 | percent = sum([i for i in y]) * 1.0 /len(y) 150 | print("Data percentage {} that is >50k: {}%".format(file_path, percent*100)) 151 | 152 | return X.tolist(), y.tolist() 153 | 154 | def main(): 155 | 156 | 157 | train_data = {'users': [], 'user_data':{}, 'num_samples':[]} 158 | test_data = {'users': [], 'user_data':{}, 'num_samples':[]} 159 | 160 | train_output = "data/train/mytrain.json" 161 | test_output = "data/test/mytest.json" 162 | 163 | 164 | X_train, y_train = generate_dataset(training_dir) 165 | X_test, y_test = generate_dataset(testing_dir) 166 | 167 | 168 | # Create data structure 169 | train_data = {'users': [], 'user_data':{}, 'num_samples':[]} 170 | test_data = {'users': [], 'user_data':{}, 'num_samples':[]} 171 | 172 | 173 | 174 | X_train_phd = [] 175 | y_train_phd = [] 176 | X_test_phd = [] 177 | y_test_phd = [] 178 | X_train_non_phd = [] 179 | y_train_non_phd = [] 180 | X_test_non_phd = [] 181 | y_test_non_phd = [] 182 | for idx, item in enumerate(X_train): 183 | if item[21] == 1: 184 | X_train_phd.append(X_train[idx]) 185 | y_train_phd.append(y_train[idx]) 186 | else: 187 | X_train_non_phd.append(X_train[idx]) 188 | y_train_non_phd.append(y_train[idx]) 189 | for idx, item in enumerate(X_test): 190 | if item[21] == 1: 191 | X_test_phd.append(X_test[idx]) 192 | y_test_phd.append(y_test[idx]) 193 | else: 194 | X_test_non_phd.append(X_test[idx]) 195 | y_test_non_phd.append(y_test[idx]) 196 | 197 | # for phd users 198 | train_len = len(X_train_phd) 199 | print("training set for phd users: {}".format(train_len)) 200 | test_len = len(X_test_phd) 201 | uname='phd' 202 | train_data['users'].append(uname) 203 | train_data['user_data'][uname] = {'x': X_train_phd, 'y': y_train_phd} 204 | train_data['num_samples'].append(train_len) 205 | test_data['users'].append(uname) 206 | test_data['user_data'][uname] = {'x': X_test_phd, 'y': y_test_phd} 207 | test_data['num_samples'].append(test_len) 208 | 209 | 210 | # for non-phd users 211 | 212 | train_len = len(X_train_non_phd) 213 | print("training set for non-phd users: {}".format(train_len)) 214 | test_len = len(X_test_non_phd) 215 | uname='non-phd' 216 | train_data['users'].append(uname) 217 | train_data['user_data'][uname] = {'x': X_train_non_phd, 'y': y_train_non_phd} 218 | train_data['num_samples'].append(train_len) 219 | test_data['users'].append(uname) 220 | test_data['user_data'][uname] = {'x': X_test_non_phd, 'y': y_test_non_phd} 221 | test_data['num_samples'].append(test_len) 222 | 223 | 224 | with open(train_output,'w') as outfile: 225 | json.dump(train_data, outfile) 226 | with open(test_output, 'w') as outfile: 227 | json.dump(test_data, outfile) 228 | 229 | 230 | if __name__ == "__main__": 231 | main() 232 | 233 | -------------------------------------------------------------------------------- /FUEL/hco_model.py: -------------------------------------------------------------------------------- 1 | # lenet base model for Pareto MTL 2 | import torch 3 | import torch.nn as nn 4 | from torch.autograd import Variable 5 | from dataload import LoadDataset 6 | import os 7 | import numpy as np 8 | import argparse 9 | import json 10 | from hco_lp import HCO_LP 11 | import torch.nn.functional as F 12 | import pdb 13 | from po_lp import PO_LP 14 | import pickle 15 | from sklearn.metrics import roc_auc_score, classification_report 16 | 17 | 18 | class RegressionTrain(torch.nn.Module): 19 | 20 | def __init__(self, model, disparity_type = "DP", dataset = "adult"): 21 | super(RegressionTrain, self).__init__() 22 | self.model = model 23 | self.loss = nn.BCELoss() 24 | self.disparity_type = disparity_type 25 | self.dataset = dataset 26 | 27 | 28 | def forward(self, x, y, A): 29 | ys_pre = self.model(x).flatten() 30 | ys = torch.sigmoid(ys_pre) 31 | hat_ys = (ys >=0.5).float() 32 | task_loss = self.loss(ys, y) 33 | accs = torch.mean((hat_ys == y).float()).item() 34 | aucs = roc_auc_score(y.cpu(), ys.clone().detach().cpu()) 35 | if True: 36 | 37 | # pred_dis = torch.abs(torch.sum(ys * A)/torch.sum(A) - torch.sum(ys * (1-A))/torch.sum(1-A)) 38 | # pred_dis = torch.sum(F.sigmoid(10 * (ys - 0.5) + 0.5 ) * A)/torch.sum(A) - torch.sum(F.sigmoid(10 * (ys - 0.5) + 0.5) * (1-A))/torch.sum(1-A) 39 | if self.disparity_type == "DP": 40 | pred_dis = torch.sum(torch.sigmoid(10 * ys_pre) * A)/torch.sum( 41 | A) - torch.sum(torch.sigmoid(10 * ys_pre) * (1-A))/torch.sum(1-A) 42 | disparitys = torch.sum(hat_ys * A)/torch.sum(A) - \ 43 | torch.sum(hat_ys * (1-A))/torch.sum(1-A) 44 | 45 | elif self.disparity_type == "Eoppo": 46 | if "eicu_d" in self.dataset: 47 | pred_dis = torch.sum(torch.sigmoid(10 * (1-ys_pre)) * A * (1-y))/torch.sum( 48 | A * (1-y)) - torch.sum(torch.sigmoid(10 * (1-ys_pre)) * (1-A) * (1-y))/torch.sum((1-A)*(1-y)) 49 | 50 | disparitys = torch.sum((1-hat_ys) * A * (1-y))/torch.sum(A * (1-y)) - \ 51 | torch.sum((1-hat_ys) * (1-A) * (1-y)) / \ 52 | torch.sum((1-A) * (1-y)) 53 | else: 54 | pred_dis = torch.sum(torch.sigmoid(10 * ys_pre) * A * y)/torch.sum( 55 | A * y) - torch.sum(torch.sigmoid(10 * ys_pre) * (1-A) * y)/torch.sum((1-A)*y) 56 | disparitys = torch.sum(hat_ys * A * y)/torch.sum(A * y) - \ 57 | torch.sum(hat_ys * (1-A) * y)/torch.sum((1-A) * y) 58 | disparitys = disparitys.item() 59 | return task_loss, accs, aucs, pred_dis, disparitys, ys 60 | 61 | else: 62 | print("error model in forward") 63 | exit() 64 | # if self.dataset == "adult" and self.disparity_type == "DP" and self.sensitive_attr == "sex": 65 | # specific_eps = self.weight_eps * np.array([-0.24607987 - 0.14961589]) 66 | # elif self.dataset == "adult" and self.disparity_type == "DP" and self.sensitive_attr == "race": 67 | # specific_eps = self.weight_eps * np.array([-0.24607987 - 0.14961589]) 68 | 69 | def randomize(self): 70 | self.model.apply(weights_init) 71 | 72 | 73 | 74 | 75 | 76 | def weights_init(m): 77 | if isinstance(m, nn.Conv2d) or isinstance(m, nn.Linear): 78 | torch.nn.init.xavier_uniform_(m.weight.data) 79 | m.weight.data *= 0.1 80 | 81 | 82 | class RegressionModel(torch.nn.Module): 83 | def __init__(self, n_feats, n_hidden): 84 | super(RegressionModel, self).__init__() 85 | self.layers = nn.ModuleList() 86 | 87 | self.layers.append(nn.Linear(n_feats, 1)) 88 | # self.layers.append(nn.Linear(n_hidden, 1)) 89 | 90 | def forward(self, x): 91 | y = x 92 | for i in range(len(self.layers)): 93 | y_temp = self.layers[i](y) 94 | if i < len(self.layers) - 1: 95 | y = torch.tanh(y_temp) 96 | else: 97 | y = y_temp 98 | # y = torch.sigmoid(y_temp) 99 | return y 100 | 101 | 102 | class MODEL(object): 103 | def __init__(self, args, logger, writer): 104 | super(MODEL, self).__init__() 105 | 106 | 107 | self.dataset = args.dataset 108 | self.uniform_eps = bool(args.uniform_eps) 109 | if self.uniform_eps: 110 | self.eps = args.eps 111 | else: 112 | args.eps[0] = 0.0 113 | self.eps = [0.0, args.eps[1], args.eps[2]] 114 | 115 | self.max_epoch1 = args.max_epoch_stage1 116 | self.max_epoch2 = args.max_epoch_stage2 117 | self.ckpt_dir = args.ckpt_dir 118 | self.global_epoch = args.global_epoch 119 | self.log_pickle_dir = args.log_dir 120 | self.per_epoches = args.per_epoches 121 | self.factor_delta = args.factor_delta 122 | self.lr_delta = args.lr_delta 123 | self.deltas = np.array([0.,0.]) 124 | self.deltas[0] = args.delta_l 125 | self.deltas[1] = args.delta_g 126 | self.eval_epoch = args.eval_epoch 127 | self.data_load(args) 128 | self.logger = logger 129 | self.logger.info(str(args)) 130 | self.n_linscalar_adjusts = 0 131 | self.done_dir = args.done_dir 132 | self.FedAve = args.FedAve 133 | self.writer = writer 134 | self.uniform = args.uniform 135 | self.performence_only = args.uniform 136 | self.policy = args.policy 137 | self.disparity_type = args.disparity_type 138 | self.model = RegressionTrain(RegressionModel(args.n_feats, args.n_hiddens), args.disparity_type, args.dataset) 139 | self.log_train = dict() 140 | self.log_test = dict() 141 | self.baseline_type = args.baseline_type 142 | self.weight_fair = args.weight_fair 143 | self.sensitive_attr = args.sensitive_attr 144 | self.weight_eps = args.weight_eps 145 | 146 | if torch.cuda.is_available(): 147 | self.model.cuda() 148 | self.optim = torch.optim.SGD(self.model.parameters(), lr=args.step_size, momentum=0., weight_decay=1e-4) 149 | 150 | _, n_params = self.getNumParams(self.model.parameters()) 151 | self.hco_lp = HCO_LP(n=n_params, eps = self.eps) 152 | self.po_lp = PO_LP(n_theta=n_params, n_alpha = 1+ self.n_clients, eps = self.eps[0]) 153 | if int(args.load_epoch) != 0: 154 | self.model_load(str(args.load_epoch)) 155 | 156 | self.commandline_save(args) 157 | 158 | def commandline_save(self, args): 159 | with open(args.commandline_file, "w") as f: 160 | json.dump(args.__dict__, f, indent =2) 161 | 162 | def getNumParams(self, params): 163 | numParams, numTrainable = 0, 0 164 | for param in params: 165 | npParamCount = np.prod(param.data.shape) 166 | numParams += npParamCount 167 | if param.requires_grad: 168 | numTrainable += npParamCount 169 | return numParams, numTrainable 170 | 171 | 172 | def model_load(self, ckptname='last'): 173 | 174 | if ckptname == 'last': 175 | ckpts = os.listdir(self.ckpt_dir) 176 | if not ckpts: 177 | self.logger.info("=> no checkpoint found") 178 | exit() 179 | ckpts = [int(ckpt) for ckpt in ckpts] 180 | ckpts.sort(reverse=True) 181 | ckptname = str(ckpts[0]) 182 | filepath = os.path.join(self.ckpt_dir, ckptname) 183 | if os.path.isfile(filepath): 184 | with open(filepath, 'rb') as f: 185 | checkpoint = torch.load(f) 186 | 187 | # self.global_epoch = checkpoint['epoch'] 188 | self.model.load_state_dict(checkpoint['model']) 189 | self.optim.load_state_dict(checkpoint['optim']) 190 | self.logger.info("=> loaded checkpoint '{} (epoch {})'".format(filepath, self.global_epoch)) 191 | 192 | else: 193 | self.logger.info("=> no checkpoint found at '{}'".format(filepath)) 194 | 195 | 196 | def model_save(self, ckptname = None): 197 | states = {'epoch':self.global_epoch, 198 | 'model':self.model.state_dict(), 199 | 'optim':self.optim.state_dict()} 200 | if ckptname == None: 201 | ckptname = str(self.global_epoch) 202 | filepath = os.path.join(self.ckpt_dir, str(ckptname)) 203 | os.makedirs(self.ckpt_dir, exist_ok = True) 204 | with open(filepath, 'wb+') as f: 205 | torch.save(states, f) 206 | self.logger.info("=> saved checkpoint '{}' (epoch {})".format(filepath, self.global_epoch)) 207 | 208 | 209 | def data_load(self, args): 210 | self.client_train_loaders, self.client_test_loaders = LoadDataset(args) 211 | self.n_clients = len(self.client_train_loaders) 212 | self.iter_train_clients = [enumerate(i) for i in self.client_train_loaders] 213 | self.iter_test_clients = [enumerate(i) for i in self.client_test_loaders] 214 | 215 | 216 | def valid_stage1(self, if_train = False, epoch = -1): 217 | with torch.no_grad(): 218 | losses = [] 219 | accs = [] 220 | diss = [] 221 | pred_diss = [] 222 | aucs = [] 223 | if if_train: 224 | loader = self.client_train_loaders 225 | else: 226 | loader = self.client_test_loaders 227 | for client_idx, client_test_loader in enumerate(loader): 228 | valid_loss = [] 229 | valid_accs = [] 230 | valid_diss = [] 231 | valid_pred_dis = [] 232 | valid_auc = [] 233 | for it, (X, Y, A) in enumerate(client_test_loader): 234 | X = X.float() 235 | Y = Y.float() 236 | A = A.float() 237 | if torch.cuda.is_available(): 238 | X = X.cuda() 239 | Y = Y.cuda() 240 | A = A.cuda() 241 | loss, acc, auc, pred_dis, disparity, pred_y = self.model(X, Y, A) 242 | valid_loss.append(loss.item()) 243 | valid_accs.append(acc) 244 | valid_diss.append(disparity) 245 | valid_pred_dis.append(pred_dis.item()) 246 | valid_auc.append(auc) 247 | assert len(valid_auc)==1 248 | losses.append(np.mean(valid_loss)) 249 | accs.append(np.mean(valid_accs)) 250 | diss.append(np.mean(valid_diss)) 251 | pred_diss.append(np.mean(valid_pred_dis)) 252 | aucs.append(np.mean(valid_auc)) 253 | self.logger.info("is_train: {}, epoch: {}, loss: {}, accuracy: {}, auc: {}, disparity: {}, pred_disparity: {}".format(if_train, self.global_epoch, losses, accs, aucs, diss, pred_diss)) 254 | self.log_test[str(epoch)] = { "client_losses": losses, "pred_client_disparities": pred_diss, "client_accs": accs, "client_aucs": aucs, "client_disparities": diss, "max_losses": [max(losses), max(diss)]} 255 | 256 | if if_train: 257 | for i, item in enumerate(losses): 258 | self.writer.add_scalar("valid_train/loss_:"+str(i), item , epoch) 259 | self.writer.add_scalar("valid_trains/acc_:"+str(i), accs[i], epoch) 260 | self.writer.add_scalar("valid_trains/auc_:"+str(i), aucs[i], epoch) 261 | self.writer.add_scalar("valid_trains/disparity_:"+str(i), diss[i], epoch) 262 | self.writer.add_scalar("valid_trains/pred_disparity_:"+str(i), pred_diss[i], epoch) 263 | 264 | else: 265 | for i, item in enumerate(losses): 266 | self.writer.add_scalar("valid_test/loss_:"+str(i), item , epoch) 267 | self.writer.add_scalar("valid_test/acc_:"+str(i), accs[i], epoch) 268 | self.writer.add_scalar("valid_test/auc_:"+str(i), aucs[i], epoch) 269 | self.writer.add_scalar("valid_test/disparity_:"+str(i), diss[i], epoch) 270 | self.writer.add_scalar("valid_test/pred_disparity_:"+str(i), pred_diss[i], epoch) 271 | return losses, accs, diss, pred_diss, aucs 272 | 273 | 274 | def soften_losses(self, losses, delta): 275 | 276 | 277 | losses_list = torch.stack(losses) 278 | loss = torch.max(losses_list) 279 | 280 | alphas = F.softmax((losses_list - loss)/delta) 281 | alpha_without_grad = (Variable(alphas.data.clone(), requires_grad=False)) 282 | return alpha_without_grad, loss 283 | 284 | 285 | def train(self): 286 | 287 | if self.baseline_type == "none": 288 | if self.policy == "alternating": 289 | start_epoch = self.global_epoch 290 | for epoch in range(start_epoch , self.max_epoch1 + self.max_epoch2): 291 | if int(epoch/self.per_epoches) %2 == 0: 292 | self.train_stage1(epoch) 293 | else: 294 | self.train_stage2(epoch) 295 | 296 | # if self.uniform: 297 | # pass 298 | # else: 299 | # self.performence_only = bool(1-self.performence_only) 300 | 301 | elif self.policy == "two_stage": 302 | if self.uniform: 303 | self.performence_only = True 304 | else: 305 | self.performence_only = False 306 | start_epoch = self.global_epoch 307 | for epoch in range(start_epoch, self.max_epoch1): 308 | self.train_stage1(epoch) 309 | 310 | for epoch in range(self.max_epoch1, self.max_epoch2 + self.max_epoch1): 311 | self.train_stage2(epoch) 312 | 313 | 314 | elif self.baseline_type == "fedave_fair": 315 | start_epoch = self.global_epoch 316 | for epoch in range(start_epoch, self.max_epoch2 + self.max_epoch1): 317 | self.train_fed(epoch) 318 | 319 | 320 | 321 | def save_log(self): 322 | with open(os.path.join(self.log_pickle_dir, "train_log.pkl"), "wb") as f: 323 | pickle.dump(self.log_train, f) 324 | with open(os.path.join(self.log_pickle_dir, "test_log.pkl"), "wb") as f: 325 | pickle.dump(self.log_test, f) 326 | os.makedirs(self.done_dir, exist_ok = True) 327 | self.logger.info("logs have been saved") 328 | 329 | 330 | 331 | def train_fed(self, epoch): 332 | # start_epoch = self.global_epoch 333 | # for epoch in range(start_epoch, self.max_epoch1): 334 | # # scheduler.step() 335 | 336 | self.model.train() 337 | self.optim.zero_grad() 338 | losses_data = [] 339 | disparities_data = [] 340 | pred_disparities_data = [] 341 | accs_data = [] 342 | aucs_data = [] 343 | client_losses = [] 344 | client_disparities = [] 345 | for client_idx in range(self.n_clients): 346 | try: 347 | _, (X, Y, A) = self.iter_train_clients[client_idx].__next__() 348 | except StopIteration: 349 | self.iter_train_clients[client_idx] = enumerate( 350 | self.client_train_loaders[client_idx]) 351 | _, (X, Y, A) = self.iter_train_clients[client_idx].__next__() 352 | X = X.float() 353 | Y = Y.float() 354 | A = A.float() 355 | if torch.cuda.is_available(): 356 | X = X.cuda() 357 | Y = Y.cuda() 358 | A = A.cuda() 359 | 360 | loss, acc, auc, pred_dis, dis, pred_y = self.model(X, Y, A) 361 | 362 | 363 | ############################################################## GPU version 364 | 365 | client_losses.append(loss) 366 | client_disparities.append(torch.abs(pred_dis)) 367 | losses_data.append(loss.item()) 368 | disparities_data.append(dis) 369 | pred_disparities_data.append(pred_dis.item()) 370 | accs_data.append(acc) 371 | aucs_data.append(auc) 372 | 373 | loss_max_performance = max(losses_data) 374 | loss_max_disparity = disparities_data[np.argmax(np.abs(disparities_data))] 375 | self.logger.info("fedave_fair, epoch: {}, all client loss: {}, all pred client disparities: {}, all client disparities: {}, all client accs: {}, all client aucs: {}, all max loss: {}".format( 376 | self.global_epoch, losses_data, pred_disparities_data, disparities_data, accs_data, aucs_data, [loss_max_performance, loss_max_disparity])) 377 | 378 | self.log_train[str(epoch)] = {"stage": 1, "client_losses": losses_data, "pred_client_disparities": pred_disparities_data, "client_disparities": disparities_data, 379 | "client_accs": accs_data, "client_aucs": aucs_data, "max_losses": [loss_max_performance, loss_max_disparity]} 380 | 381 | for i, loss in enumerate(losses_data): 382 | self.writer.add_scalar("train/1_loss_" + str(i), loss, epoch) 383 | self.writer.add_scalar( 384 | "train/disparity_" + str(i), disparities_data[i], epoch) 385 | self.writer.add_scalar( 386 | "train/pred_disparity_" + str(i), pred_disparities_data[i], epoch) 387 | self.writer.add_scalar( 388 | "train/acc_" + str(i), accs_data[i], epoch) 389 | self.writer.add_scalar( 390 | "train/auc_" + str(i), aucs_data[i], epoch) 391 | 392 | 393 | self.optim.zero_grad() 394 | weighted_loss1 = torch.sum(torch.stack(client_losses)) 395 | weighted_loss2 = torch.sum(torch.stack(client_disparities)) * self.weight_fair 396 | weighted_loss = weighted_loss1 + weighted_loss2 397 | weighted_loss.backward() 398 | self.optim.step() 399 | 400 | # 2. apply gradient dierctly 401 | ############################ 402 | # grads_and_vars = opt.compute_gradients(loss, parameter_list) 403 | # my_grads_and_vars = [(g*C, v) for g, v in grads_and_vars] 404 | # opt.apply_gradients(my_grads_and_vars) 405 | 406 | # Calculate and record performance 407 | if epoch == 0 or (epoch + 1) % self.eval_epoch == 0: 408 | self.model.eval() 409 | losses, accs, client_disparities, pred_dis, aucs = self.valid_stage1( 410 | if_train=False, epoch=epoch) 411 | if epoch != 0: 412 | self.model_save() 413 | self.global_epoch += 1 414 | 415 | 416 | 417 | def train_stage1(self, epoch): 418 | # start_epoch = self.global_epoch 419 | # for epoch in range(start_epoch, self.max_epoch1): 420 | # # scheduler.step() 421 | 422 | self.model.train() 423 | self.optim.zero_grad() 424 | grads_performance = [] 425 | grads_disparity = [] 426 | losses_data = [] 427 | disparities_data = [] 428 | pred_disparities_data = [] 429 | accs_data = [] 430 | aucs_data = [] 431 | client_losses = [] 432 | client_disparities = [] 433 | for client_idx in range(self.n_clients): 434 | try: 435 | _, (X, Y, A) = self.iter_train_clients[client_idx].__next__() 436 | except StopIteration: 437 | self.iter_train_clients[client_idx] = enumerate(self.client_train_loaders[client_idx]) 438 | _, (X, Y, A) = self.iter_train_clients[client_idx].__next__() 439 | X = X.float() 440 | Y = Y.float() 441 | A = A.float() 442 | if torch.cuda.is_available(): 443 | X = X.cuda() 444 | Y = Y.cuda() 445 | A = A.cuda() 446 | 447 | loss, acc, auc, pred_dis, dis, pred_y = self.model(X, Y, A) 448 | 449 | 450 | 451 | 452 | ############################################################## GPU version 453 | loss.backward(retain_graph=True) 454 | grad = [] 455 | for param in self.model.parameters(): 456 | if param.grad is not None: 457 | grad.extend(Variable(param.grad.data.clone().flatten(), requires_grad=False)) 458 | grad = torch.stack(grad) 459 | grads_performance.append(grad) 460 | self.optim.zero_grad() 461 | 462 | 463 | 464 | torch.abs(pred_dis).backward(retain_graph=True) 465 | if self.performence_only: 466 | self.optim.zero_grad() 467 | grad = [] 468 | for param in self.model.parameters(): 469 | if param.grad is not None: 470 | grad.extend(Variable(param.grad.data.clone().flatten(), requires_grad=False)) 471 | grad = torch.stack(grad) 472 | grads_disparity.append(grad) 473 | self.optim.zero_grad() 474 | 475 | if self.uniform_eps: 476 | client_disparities.append(torch.abs(pred_dis)) 477 | specific_eps = 0 478 | 479 | else: 480 | if self.dataset == "adult" and self.disparity_type == "DP" and self.sensitive_attr == "sex": 481 | specific_eps = self.weight_eps * \ 482 | np.array([0.12938917 , 0.14046744]) 483 | 484 | elif self.dataset == "adult" and self.disparity_type == "DP" and self.sensitive_attr == "race": 485 | specific_eps = self.weight_eps * \ 486 | np.array([0.15663486, 0.07555133]) 487 | 488 | elif self.dataset == "adult" and self.disparity_type == "Eoppo" and self.sensitive_attr == "race": 489 | specific_eps = self.weight_eps * \ 490 | np.array([0.13454545, 0.09585903]) 491 | 492 | elif self.dataset == "adult" and self.disparity_type == "Eoppo" and self.sensitive_attr == "sex": 493 | specific_eps = self.weight_eps * \ 494 | np.array([0.00064349, 0.11690249]) 495 | 496 | elif "eicu_los" in self.dataset and self.disparity_type == "DP" and self.sensitive_attr == "race": 497 | specific_eps = self.weight_eps * np.array([0.052, 0.22, 0.035, 0.070, 0.094, 0.008, 0.047, 0.089, 0.078, 0.008, 0.108]) 498 | 499 | elif "eicu_los" in self.dataset and self.disparity_type == "Eoppo" and self.sensitive_attr == "race": 500 | specific_eps = self.weight_eps * np.array([0.187, 0.297, 0.021, 0.020, 0.103, 0.170, 0.138, 0.029, 0.065, 0.027, 0.087]) 501 | 502 | 503 | client_disparities.append(torch.abs(pred_dis) - specific_eps[client_idx]) 504 | 505 | 506 | client_losses.append(loss) 507 | losses_data.append(loss.item()) 508 | disparities_data.append(dis) 509 | pred_disparities_data.append(pred_dis.item()) 510 | accs_data.append(acc) 511 | aucs_data.append(auc) 512 | 513 | 514 | alphas_l, loss_max_performance = self.soften_losses(client_losses, self.deltas[0]) 515 | loss_max_performance = loss_max_performance.item() 516 | alphas_g, loss_max_disparity = self.soften_losses(client_disparities, self.deltas[1]) 517 | loss_max_disparity = loss_max_disparity.item() 518 | 519 | losses = np.array(losses_data) 520 | # a batch of [loss_c1, loss_c2, ... loss_cn], [grad_c1, grad_c2, grad_cn] 521 | if self.FedAve: 522 | preference = np.array([1 for i in range(self.n_clients)]) 523 | alpha = preference / preference.sum() 524 | self.n_linscalar_adjusts += 1 525 | 526 | else: 527 | try: 528 | # Calculate the alphas from the LP solver 529 | alphas_l = alphas_l.view(1, -1) 530 | grad_l = alphas_l @ torch.stack(grads_performance) 531 | alphas_g = alphas_g.view(1, -1) 532 | grad_g = alphas_g @ torch.stack(grads_disparity) 533 | alpha, deltas = self.hco_lp.get_alpha([loss_max_performance, loss_max_disparity], grad_l, grad_g, self.deltas, self.factor_delta, self.lr_delta) 534 | if torch.cuda.is_available(): 535 | alpha = torch.from_numpy(alpha.reshape(-1)).cuda() 536 | else: 537 | alpha = torch.from_numpy(alpha.reshape(-1)) 538 | self.deltas = deltas 539 | 540 | alpha = alpha.view(-1) 541 | except Exception as e: 542 | print(e) 543 | exit() 544 | ############################################################## GPU version 545 | 546 | self.logger.info("1, epoch: {}, all client loss: {}, all pred client disparities: {}, all client disparities: {}, all client accs: {}, all client aucs: {}, all max loss: {}, specific eps: {}, all Alpha: {}, all Deltas: {}".format(self.global_epoch, losses_data, pred_disparities_data, disparities_data, accs_data, aucs_data, [loss_max_performance, loss_max_disparity] , specific_eps, alpha.cpu().numpy(), self.deltas)) 547 | self.log_train[str(epoch)] = { "stage": 1, "client_losses": losses_data, "pred_client_disparities": pred_disparities_data, "client_disparities": disparities_data, "client_accs": accs_data, "client_aucs": aucs_data, "max_losses": [loss_max_performance, loss_max_disparity], "alpha": alpha.cpu().numpy(), "deltas": self.deltas} 548 | 549 | for i, loss in enumerate(losses_data): 550 | self.writer.add_scalar("train/1_loss_" + str(i), loss, epoch) 551 | self.writer.add_scalar("train/1_disparity_" + str(i), disparities_data[i], epoch) 552 | self.writer.add_scalar("train/1_pred_disparity_" + str(i), pred_disparities_data[i], epoch) 553 | self.writer.add_scalar("train/1_acc_" + str(i), accs_data[i], epoch) 554 | self.writer.add_scalar("train/1_auc_" + str(i), aucs_data[i], epoch) 555 | 556 | for i, a in enumerate(alpha): 557 | self.writer.add_scalar("train/1_alpha_" +str(i), a.item(), epoch) 558 | for i, delta in enumerate(self.deltas): 559 | self.writer.add_scalar("train/1_delta_" + str(i), delta, epoch) 560 | 561 | # 1. Optimization step 562 | # self.optim.zero_grad() 563 | # weighted_loss = torch.sum(torch.stack(client_losses) * alpha) # * 5. * max(epo_lp.mu_rl, 0.2) 564 | # weighted_loss.backward() 565 | # self.optim.step() 566 | 567 | # 2. Optimization step 568 | self.optim.zero_grad() 569 | weighted_loss1 = torch.sum(torch.stack(client_losses)*alphas_l) 570 | weighted_loss2 = torch.sum(torch.stack(client_disparities)*alphas_g) 571 | weighted_loss = torch.sum(torch.stack([weighted_loss1, weighted_loss2]) * alpha) 572 | weighted_loss.backward() 573 | self.optim.step() 574 | 575 | # 2. apply gradient dierctly 576 | ############################ 577 | # grads_and_vars = opt.compute_gradients(loss, parameter_list) 578 | # my_grads_and_vars = [(g*C, v) for g, v in grads_and_vars] 579 | # opt.apply_gradients(my_grads_and_vars) 580 | 581 | # Calculate and record performance 582 | if epoch == 0 or (epoch + 1) % self.eval_epoch == 0: 583 | self.model.eval() 584 | losses, accs, client_disparities, pred_dis, aucs = self.valid_stage1(if_train = False, epoch = epoch) 585 | if epoch != 0: 586 | self.model_save() 587 | self.global_epoch+=1 588 | 589 | 590 | 591 | def train_stage2(self, epoch): 592 | self.model.train() 593 | grads_performance = [] 594 | grads_disparity = [] 595 | disparities_data = [] 596 | client_losses = [] 597 | client_disparities = [] 598 | losses_data = [] 599 | accs_data = [] 600 | pred_diss_data = [] 601 | aucs_data = [] 602 | 603 | for client_idx in range(self.n_clients): 604 | 605 | try: 606 | _, (X, Y, A) = self.iter_train_clients[client_idx].__next__() 607 | except StopIteration: 608 | self.iter_train_clients[client_idx] = enumerate(self.client_train_loaders[client_idx]) 609 | _, (X, Y, A) = self.iter_train_clients[client_idx].__next__() 610 | X = X.float() 611 | Y = Y.float() 612 | A = A.float() 613 | if torch.cuda.is_available(): 614 | X = X.cuda() 615 | Y = Y.cuda() 616 | A = A.cuda() 617 | 618 | loss, acc, auc, pred_dis, dis, pred_y = self.model(X, Y, A) 619 | 620 | loss.backward(retain_graph=True) 621 | grad = [] 622 | for param in self.model.parameters(): 623 | if param.grad is not None: 624 | grad.extend(Variable(param.grad.data.clone().flatten(), requires_grad=False)) 625 | grad = torch.stack(grad) 626 | grads_performance.append(grad) 627 | self.optim.zero_grad() 628 | 629 | 630 | torch.abs(pred_dis).backward(retain_graph=True) 631 | if self.performence_only: 632 | self.optim.zero_grad() 633 | grad = [] 634 | for param in self.model.parameters(): 635 | if param.grad is not None: 636 | grad.extend(Variable(param.grad.data.clone().flatten(), requires_grad=False)) 637 | grad = torch.stack(grad) 638 | grads_disparity.append(grad) 639 | self.optim.zero_grad() 640 | 641 | client_losses.append(loss) 642 | client_disparities.append(torch.abs(pred_dis)) 643 | disparities_data.append(dis) 644 | accs_data.append(acc) 645 | losses_data.append(loss.item()) 646 | pred_diss_data.append(pred_dis.item()) 647 | aucs_data.append(auc) 648 | 649 | 650 | alpha_disparity, max_disparity = self.soften_losses(client_disparities, self.deltas[1]) 651 | 652 | client_pred_disparity = torch.sum(alpha_disparity * torch.stack(client_disparities)) 653 | 654 | grad_disparity = alpha_disparity.view(1, -1) @ torch.stack(grads_disparity) 655 | grads_performance = torch.stack(grads_performance) 656 | 657 | 658 | if max_disparity.item() < self.eps[0]: 659 | grad_disparity = torch.zeros_like(grad_disparity, requires_grad= False) 660 | grad_performance = torch.mean(grads_performance, dim = 0, keepdim=True) 661 | 662 | grads = torch.cat((grads_performance, grad_disparity), dim = 0) 663 | 664 | ##########################################GPU() 665 | grad_performance = grad_performance.t() 666 | ### 667 | 668 | alpha, gamma = self.po_lp.get_alpha(grads, grad_performance, grads.t()) 669 | if torch.cuda.is_available(): 670 | alpha = torch.from_numpy(alpha.reshape(-1)).cuda() 671 | else: 672 | alpha = torch.from_numpy(alpha.reshape(-1)) 673 | ##########################################GPU() 674 | 675 | client_losses.append(client_pred_disparity) 676 | weighted_loss = torch.sum(torch.stack(client_losses) * alpha) 677 | weighted_loss.backward() 678 | self.optim.step() 679 | self.optim.zero_grad() 680 | 681 | self.logger.info("2, epoch: {}, all client loss: {}, all pred client disparities: {}, all client disparities: {}, all client accs: {}, all client aucs: {}, max disparity: {}, alpha: {}, deltas: {}".format(epoch, 682 | losses_data, pred_diss_data, disparities_data, accs_data, aucs_data, max_disparity.item(), alpha.cpu().numpy(), self.deltas)) 683 | 684 | self.log_train[str(epoch)] = { "stage": 2, "client_losses": losses_data, "pred_client_disparities": pred_diss_data, "client_disparities": disparities_data, "client_accs": accs_data, "client_aucs": aucs_data, "max_losses": [max(losses_data), max(disparities_data)], "alpha": alpha.cpu().numpy(), "deltas": self.deltas} 685 | 686 | for i, loss in enumerate(losses_data): 687 | self.writer.add_scalar("train/2_loss_" + str(i), loss, epoch) 688 | self.writer.add_scalar("train/2_disparity_" + str(i), disparities_data[i], epoch) 689 | self.writer.add_scalar("train/2_pred_disparity_" + str(i), pred_diss_data[i], epoch) 690 | self.writer.add_scalar("train/2_acc_" + str(i), accs_data[i], epoch) 691 | self.writer.add_scalar("train/2_auc_" + str(i), aucs_data[i], epoch) 692 | 693 | if epoch == 0 or (epoch + 1) % self.eval_epoch == 0: 694 | self.model.eval() 695 | losses, accs, client_disparities, pred_dis, aucs = self.valid_stage1(if_train = False, epoch = epoch) 696 | if epoch != 0: 697 | self.model_save() 698 | self.global_epoch+=1 699 | --------------------------------------------------------------------------------