dro 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dro-0.0.1/LICENSE +0 -0
- dro-0.0.1/MANIFEST.in +1 -0
- dro-0.0.1/PKG-INFO +40 -0
- dro-0.0.1/dro/__init__.py +7 -0
- dro-0.0.1/dro/fetch_method.py +38 -0
- dro-0.0.1/dro/src/HR_DRO_LR.py +111 -0
- dro-0.0.1/dro/src/MMD_DRO.py +116 -0
- dro-0.0.1/dro/src/Sinkhorn_DRO.py +277 -0
- dro-0.0.1/dro/src/Unified_DRO.py +260 -0
- dro-0.0.1/dro/src/Wasserstein_DRO.py +305 -0
- dro-0.0.1/dro/src/__init__.py +6 -0
- dro-0.0.1/dro/src/base.py +90 -0
- dro-0.0.1/dro/src/f_DRO.py +233 -0
- dro-0.0.1/dro.egg-info/PKG-INFO +40 -0
- dro-0.0.1/dro.egg-info/SOURCES.txt +18 -0
- dro-0.0.1/dro.egg-info/dependency_links.txt +1 -0
- dro-0.0.1/dro.egg-info/requires.txt +6 -0
- dro-0.0.1/dro.egg-info/top_level.txt +1 -0
- dro-0.0.1/setup.cfg +4 -0
- dro-0.0.1/setup.py +29 -0
dro-0.0.1/LICENSE
ADDED
|
File without changes
|
dro-0.0.1/MANIFEST.in
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
include pydro/README.md
|
dro-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: dro
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A package of distributionally robust optimization (DRO) methods. Implemented via cvxpy and PyTorch
|
|
5
|
+
Home-page: https://github.com/namkoong-lab/dro
|
|
6
|
+
Author: Jiashuo Liu, Tianyu Wang, Peng Cui, Hongseok Namkoong
|
|
7
|
+
Author-email: liujiashuo77@gmail.com, tw2837@columbia.edu, cuip@tsinghua.edu.cn, namkoong@gsb.columbia.edu
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: MacOS
|
|
11
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
12
|
+
Requires-Python: >=3
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
|
|
16
|
+
### DRO Package
|
|
17
|
+
|
|
18
|
+
> <a href="https://ljsthu.github.io">Jiashuo Liu*</a>, <a href="https://wangtianyu61.github.io">Tianyu Wang*</a>, <a href="https://pengcui.thumedialab.com">Peng Cui</a>, <a href="https://hsnamkoong.github.io">Hongseok Namkoong</a>
|
|
19
|
+
|
|
20
|
+
> Tsinghua University, Columbia University
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
`DRO` is a python package that implements 12 typical DRO methods on linear models (SVM, logistic regression, and linear regression). It is built based on `cvxpy`. Implemented DRO methods include:
|
|
24
|
+
* $f$-DRO
|
|
25
|
+
* CVaR-DRO
|
|
26
|
+
* KL-DRO
|
|
27
|
+
* TV-DRO
|
|
28
|
+
* Marginal DRO (CVaR)
|
|
29
|
+
* Wasserstein DRO
|
|
30
|
+
* Wasserstein DRO
|
|
31
|
+
* Augmented Wasserstein DRO
|
|
32
|
+
* Regularized Wasserstein DRO
|
|
33
|
+
* MMD-DRO
|
|
34
|
+
* Sinkhorn-DRO
|
|
35
|
+
* Holistic DRO
|
|
36
|
+
* Unified-DRO
|
|
37
|
+
* $L_2$ cost
|
|
38
|
+
* $L_{inf}$ cost
|
|
39
|
+
|
|
40
|
+
Current version only contains linear models. And further version will incorporate neural network implementations via some approximations.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Built on Folktables and the codebase https://github.com/jpgard/subgroup-robustness-grows-on-trees
|
|
2
|
+
__version__ = "0.0.1"
|
|
3
|
+
__author__ = 'Jiashuo Liu, Tianyu Wang, Peng Cui, Hongseok Namkoong'
|
|
4
|
+
__credits__ = 'Peng Cui\'s Group and Hongseok Namkoong\' Group'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
from .fetch_method import fetch_method
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from .src import *
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def fetch_method(method, is_regression, input_dim=77):
|
|
5
|
+
if method == 'unified_dro_l2':
|
|
6
|
+
if is_regression == 1 or is_regression == 2:
|
|
7
|
+
raise NotImplementedError("Unified DRO does not support regression!")
|
|
8
|
+
else:
|
|
9
|
+
return MOT_Robust_CLF_L2()
|
|
10
|
+
if method == 'unified_dro_linf':
|
|
11
|
+
if is_regression == 1 or is_regression == 2:
|
|
12
|
+
raise NotImplementedError("Unified DRO does not support regression!")
|
|
13
|
+
else:
|
|
14
|
+
return MOT_Robust_CLF_Linf()
|
|
15
|
+
elif method == 'hr_dro_lr':
|
|
16
|
+
return HR_DRO_LR(is_regression=is_regression)
|
|
17
|
+
elif method == 'chi2_dro':
|
|
18
|
+
return chi2_DRO(input_dim=input_dim, is_regression=is_regression)
|
|
19
|
+
elif method == 'kl_dro':
|
|
20
|
+
return KL_DRO(input_dim=input_dim, is_regression=is_regression)
|
|
21
|
+
elif method == 'tv_dro':
|
|
22
|
+
return TV_DRO(input_dim=input_dim, is_regression=is_regression)
|
|
23
|
+
elif method == 'marginal_cvar_dro':
|
|
24
|
+
return Marginal_CVaR_DRO(input_dim=input_dim, is_regression=is_regression)
|
|
25
|
+
elif method == 'cvar_dro':
|
|
26
|
+
return CVaR_DRO(input_dim=input_dim, is_regression=is_regression)
|
|
27
|
+
elif method == 'wasserstein_dro':
|
|
28
|
+
return Wasserstein_DRO(input_dim=input_dim, is_regression=is_regression)
|
|
29
|
+
elif method == 'wasserstein_dro_satisficing':
|
|
30
|
+
return Wasserstein_DRO_satisficing(input_dim=input_dim, is_regression=is_regression)
|
|
31
|
+
elif method == 'wasserstein_dro_aug':
|
|
32
|
+
return Wasserstein_DRO_aug(input_dim=input_dim, is_regression=is_regression)
|
|
33
|
+
elif method == 'sinkhorn_dro':
|
|
34
|
+
return Sinkhorn_DRO_Linear(input_dim=input_dim, is_regression=is_regression)
|
|
35
|
+
elif method == 'mmd_dro':
|
|
36
|
+
return MMD_DRO(input_dim=input_dim, is_regression=is_regression)
|
|
37
|
+
else:
|
|
38
|
+
raise NotImplementedError(f"Algorithm {method} not implemented yet!")
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import cvxpy as cp
|
|
2
|
+
import numpy as np
|
|
3
|
+
from sklearn.metrics import f1_score
|
|
4
|
+
|
|
5
|
+
class HR_DRO_LR:
|
|
6
|
+
def __init__(self, r=1.0, alpha=1.0, epsilon=0.5, epsilon_prime=1.0, is_regression=0):
|
|
7
|
+
self.r = r
|
|
8
|
+
self.alpha = alpha
|
|
9
|
+
self.epsilon = epsilon
|
|
10
|
+
self.epsilon_prime = epsilon_prime
|
|
11
|
+
self.is_regression = is_regression
|
|
12
|
+
|
|
13
|
+
def update(self, config={}):
|
|
14
|
+
if 'r' in config.keys():
|
|
15
|
+
self.r = config["r"]
|
|
16
|
+
if 'alpha' in config.keys():
|
|
17
|
+
self.alpha = config["alpha"]
|
|
18
|
+
if 'epsilon' in config.keys():
|
|
19
|
+
self.epsilon = config["epsilon"]
|
|
20
|
+
if 'epsilon_prime' in config.keys():
|
|
21
|
+
self.epsilon_prime = config["epsilon_prime"]
|
|
22
|
+
|
|
23
|
+
def fit(self, X, Y):
|
|
24
|
+
T = X.shape[0]
|
|
25
|
+
theta = cp.Variable(X.shape[1]) # Define the size based on the problem
|
|
26
|
+
w = cp.Variable(T)
|
|
27
|
+
lambda_ = cp.Variable(nonneg=True)
|
|
28
|
+
beta = cp.Variable(nonneg=True)
|
|
29
|
+
eta = cp.Variable()
|
|
30
|
+
temp = cp.Variable()
|
|
31
|
+
|
|
32
|
+
# Objective
|
|
33
|
+
objective = cp.Minimize(1/T * cp.sum(w) + lambda_ * (self.r - 1) + beta * self.alpha + eta)
|
|
34
|
+
|
|
35
|
+
if self.is_regression == 1 or self.is_regression == 2:
|
|
36
|
+
# Constraints
|
|
37
|
+
constraints = []
|
|
38
|
+
# Add constraints based on the problem
|
|
39
|
+
for t in range(T):
|
|
40
|
+
constraints.append(temp >= cp.abs(theta.T @ X[t] - Y[t]))
|
|
41
|
+
constraints.append(w[t]>= cp.rel_entr(lambda_, (eta - cp.abs(theta.T @ X[t] - Y[t]) - self.epsilon * cp.norm(theta, 2)) ))
|
|
42
|
+
constraints.append(w[t]>= cp.rel_entr(lambda_, (eta - temp - self.epsilon_prime * cp.norm(theta, 2)))-beta)
|
|
43
|
+
constraints.append(eta >= cp.abs(theta.T @ X[t] - Y[t]) + self.epsilon_prime * cp.norm(theta, 2))
|
|
44
|
+
elif self.is_regression == 0:
|
|
45
|
+
Y = 2*Y - 1.0
|
|
46
|
+
# Constraints
|
|
47
|
+
constraints = []
|
|
48
|
+
# Add constraints based on the problem
|
|
49
|
+
constraints.append(eta>=1e-6)
|
|
50
|
+
for t in range(T):
|
|
51
|
+
constraints.append(temp <= Y[t]*(theta.T@X[t]))
|
|
52
|
+
constraints.append(w[t] >= cp.rel_entr(lambda_, eta))
|
|
53
|
+
constraints.append(w[t]>= cp.rel_entr(lambda_, (eta - 1 + Y[t]*(theta.T @ X[t])-self.epsilon*cp.norm(theta,2)) ))
|
|
54
|
+
constraints.append(w[t]>= cp.rel_entr(lambda_, (eta - 1 + temp - self.epsilon_prime * cp.norm(theta, 2)))-beta)
|
|
55
|
+
constraints.append(eta >= 1-Y[t]*(theta.T @ X[t])+self.epsilon*cp.norm(theta,2))
|
|
56
|
+
else:
|
|
57
|
+
raise NotImplementedError
|
|
58
|
+
|
|
59
|
+
# Problem
|
|
60
|
+
prob = cp.Problem(objective, constraints)
|
|
61
|
+
|
|
62
|
+
# Solve
|
|
63
|
+
prob.solve(solver=cp.MOSEK,
|
|
64
|
+
mosek_params={"MSK_DPAR_INTPNT_CO_TOL_REL_GAP": 1e-8},
|
|
65
|
+
verbose=True)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
self.theta = theta.value
|
|
69
|
+
self.w = w.value
|
|
70
|
+
self.lambda_ = lambda_.value
|
|
71
|
+
self.beta = beta.value
|
|
72
|
+
self.eta = eta.value
|
|
73
|
+
|
|
74
|
+
model_params = {}
|
|
75
|
+
model_params["theta"] = self.theta.reshape(-1).tolist()
|
|
76
|
+
return model_params
|
|
77
|
+
|
|
78
|
+
def predict(self, X):
|
|
79
|
+
scores = self.theta.T @ X.T
|
|
80
|
+
preds = scores.copy()
|
|
81
|
+
preds[scores >= 0] = 1
|
|
82
|
+
preds[scores < 0] = 0
|
|
83
|
+
return preds
|
|
84
|
+
|
|
85
|
+
def score(self, X, y):
|
|
86
|
+
# calculate accuracy of the given test data set
|
|
87
|
+
predictions = self.predict(X)
|
|
88
|
+
acc = np.mean([predictions.flatten() == y.flatten()])
|
|
89
|
+
f1 = f1_score(y, predictions, average='macro')
|
|
90
|
+
return acc, f1
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
if __name__=="__main__":
|
|
97
|
+
from sklearn.datasets import make_regression
|
|
98
|
+
from sklearn.linear_model import LinearRegression
|
|
99
|
+
import numpy as np
|
|
100
|
+
|
|
101
|
+
sample_size = 1000
|
|
102
|
+
feature_size = 10
|
|
103
|
+
X, y = make_regression(n_samples = sample_size, n_features = feature_size, noise = 1, random_state = 42)
|
|
104
|
+
|
|
105
|
+
method = HR_DRO_LR()
|
|
106
|
+
method.fit(X,y)
|
|
107
|
+
print(method.theta)
|
|
108
|
+
|
|
109
|
+
model = LinearRegression(fit_intercept=True)
|
|
110
|
+
model.fit(X, y)
|
|
111
|
+
print(model.coef_)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from .base import *
|
|
2
|
+
import numpy as np
|
|
3
|
+
import math
|
|
4
|
+
import cvxpy as cp
|
|
5
|
+
from sklearn.metrics.pairwise import rbf_kernel
|
|
6
|
+
from sklearn.metrics import euclidean_distances
|
|
7
|
+
|
|
8
|
+
"""
|
|
9
|
+
we set (X, Y) as the same scale but it may not be in practice.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MMD_DRO(base_DRO):
|
|
14
|
+
def __init__(self, input_dim, is_regression=2):
|
|
15
|
+
base_DRO.__init__(self, input_dim, is_regression)
|
|
16
|
+
self.eta = 0.1
|
|
17
|
+
self.sampling_method = 'bound'
|
|
18
|
+
self.n_certify_ratio = 1
|
|
19
|
+
def update(self, config = {}):
|
|
20
|
+
if 'eta' in config.keys():
|
|
21
|
+
self.eta = config['eta']
|
|
22
|
+
if 'sampling_method' in config.keys():
|
|
23
|
+
assert (config['sampling_method'] in ['bound', 'hull'])
|
|
24
|
+
self.sampling_method = config['sampling_method']
|
|
25
|
+
if 'n_certify_ratio' in config.keys():
|
|
26
|
+
self.n_certify_ratio = config['n_certify_ratio']
|
|
27
|
+
|
|
28
|
+
def matrix_decomp(self, K):
|
|
29
|
+
try:
|
|
30
|
+
L = np.linalg.cholesky(K)
|
|
31
|
+
except:
|
|
32
|
+
# print('warning, K is singular')
|
|
33
|
+
d, v = np.linalg.eigh(K) #L == U*diag(d)*U'. the scipy function forces real eigs
|
|
34
|
+
d[np.where(d < 0)] = 0 # get rid of small eigs
|
|
35
|
+
L = v @ np.diag(np.sqrt(d))
|
|
36
|
+
return L
|
|
37
|
+
|
|
38
|
+
def medium_heuristic(self, X, Y):
|
|
39
|
+
if self.is_regression == 1 or self.is_regression == 2:
|
|
40
|
+
distsqr = euclidean_distances(X, Y, squared = True)
|
|
41
|
+
else:
|
|
42
|
+
distsqr = euclidean_distances(X[:,0:-1], Y[:,0:-1], squared = True)
|
|
43
|
+
|
|
44
|
+
kernel_width = np.sqrt(0.5 * np.median(distsqr))
|
|
45
|
+
|
|
46
|
+
# in sklearn,
|
|
47
|
+
# kernel is done by K(x, y) = exp(-gamma ||x-y||^2)
|
|
48
|
+
kernel_gamma = 1.0 / (2 * kernel_width ** 2)
|
|
49
|
+
|
|
50
|
+
return kernel_width, kernel_gamma
|
|
51
|
+
|
|
52
|
+
def cvx_loss(self, theta, zeta):
|
|
53
|
+
if self.is_regression == 1 or self.is_regression == 2:
|
|
54
|
+
loss = (zeta[-1] - theta @ zeta[:-1]) ** 2
|
|
55
|
+
else:
|
|
56
|
+
loss = cp.pos(1 - cp.multiply(zeta[-1], theta @ zeta[:-1]))
|
|
57
|
+
return loss
|
|
58
|
+
|
|
59
|
+
def fit(self, X, y):
|
|
60
|
+
if self.is_regression == 0:
|
|
61
|
+
y = 2*y-1
|
|
62
|
+
sample_size, __ = X.shape
|
|
63
|
+
n_certify = int(self.n_certify_ratio * sample_size)
|
|
64
|
+
|
|
65
|
+
theta = cp.Variable(self.input_dim) # variable \theta
|
|
66
|
+
|
|
67
|
+
# constraint on the decision variable
|
|
68
|
+
|
|
69
|
+
# KDRO part
|
|
70
|
+
a = cp.Variable(sample_size + n_certify) # variable \alpha
|
|
71
|
+
f0 = cp.Variable() # variable f0
|
|
72
|
+
|
|
73
|
+
# --------------------------------------------------------------------------------
|
|
74
|
+
# Step 1: generate the sampled support
|
|
75
|
+
# --------------------------------------------------------------------------------
|
|
76
|
+
if self.sampling_method == 'bound': # sample within certain bound
|
|
77
|
+
# let the samples also live in the intervel I sampled uncertainty w
|
|
78
|
+
zeta = np.random.uniform(-1, 1, size=[n_certify, self.input_dim + 1])
|
|
79
|
+
elif self.sampling_method == 'hull': # sample using convex hull of empirical data
|
|
80
|
+
# let the samples also live in the intervel I sampled uncertainty w
|
|
81
|
+
# this is equiv. to really do it in multi dimensions, need to sample coeff. from a simplex
|
|
82
|
+
if self.is_regression == 1 or self.is_regression == 2:
|
|
83
|
+
zeta1 = np.random.uniform(np.min(X), np.max(X), size = [n_certify, self.input_dim])
|
|
84
|
+
zeta2 = np.random.uniform(np.min(y), np.max(y), size = [n_certify, 1])
|
|
85
|
+
else:
|
|
86
|
+
zeta1 = np.random.uniform(-1, 1, size = [n_certify, self.input_dim])
|
|
87
|
+
zeta2 = np.random.choice([-1, 1], size = (n_certify, 1))
|
|
88
|
+
zeta = np.concatenate([zeta1, zeta2], axis = 1)
|
|
89
|
+
else:
|
|
90
|
+
raise NotImplementedError
|
|
91
|
+
|
|
92
|
+
data = np.concatenate([X, y.reshape(-1, 1)], axis = 1)
|
|
93
|
+
# in practice, we always include the empirical data in the sampled support
|
|
94
|
+
zeta = np.concatenate([data, zeta])
|
|
95
|
+
|
|
96
|
+
kernel_width, kernel_gamma = self.medium_heuristic(zeta, zeta)
|
|
97
|
+
|
|
98
|
+
# --------------------------------------------------------------------------------
|
|
99
|
+
# Step 3: setup objective function and constraints
|
|
100
|
+
# --------------------------------------------------------------------------------
|
|
101
|
+
# evaluate the f, K at the value of zetas
|
|
102
|
+
K = rbf_kernel(zeta, zeta)
|
|
103
|
+
#gamma = kernel_gamma)
|
|
104
|
+
f = a @ K
|
|
105
|
+
constr = []
|
|
106
|
+
for i in range((len(zeta))):
|
|
107
|
+
constr += [self.cvx_loss(theta, zeta[i]) <= f0 + f[i]]
|
|
108
|
+
|
|
109
|
+
obj = f0 + cp.sum(f[0:sample_size]) / sample_size + self.eta * cp.norm(a.T @ self.matrix_decomp(K))
|
|
110
|
+
opt = cp.Problem(cp.Minimize(obj), constr)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
opt.solve(solver = cp.MOSEK)
|
|
114
|
+
self.theta = theta.value #, obj.value, a.value, f0.value, kernel_gamma, zeta
|
|
115
|
+
|
|
116
|
+
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import torch
|
|
3
|
+
import torch.nn as nn
|
|
4
|
+
import torch.nn.functional as F
|
|
5
|
+
import torch.optim as optim
|
|
6
|
+
from torch.utils.data import TensorDataset, DataLoader
|
|
7
|
+
from torch.autograd import Variable
|
|
8
|
+
import math
|
|
9
|
+
from sklearn.metrics import f1_score
|
|
10
|
+
|
|
11
|
+
class LinearModel(nn.Module):
|
|
12
|
+
def __init__(self, input_dim, output_dim):
|
|
13
|
+
super(LinearModel, self).__init__()
|
|
14
|
+
self.linear = nn.Linear(input_dim, output_dim, bias=True)
|
|
15
|
+
|
|
16
|
+
def forward(self, x):
|
|
17
|
+
return self.linear(x)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def to_tensor(x):
|
|
21
|
+
if type(x) == np.ndarray:
|
|
22
|
+
return torch.from_numpy(x).float()
|
|
23
|
+
elif type(x) == torch.Tensor:
|
|
24
|
+
return x
|
|
25
|
+
else:
|
|
26
|
+
print("Type error. Input should be either numpy array or torch tensor")
|
|
27
|
+
|
|
28
|
+
def SDRO_eval(theta, Lambda, Reg, X, Y):
|
|
29
|
+
n,d = X.shape
|
|
30
|
+
|
|
31
|
+
ratio_1 = Lambda / (Lambda - 2 * np.linalg.norm(theta)**2)
|
|
32
|
+
residual = np.mean((X@theta - Y)**2,0)
|
|
33
|
+
obj_1 = ratio_1 * residual
|
|
34
|
+
obj_2 = Lambda*Reg/2 * np.linalg.slogdet(np.eye(d) - theta@theta.T*2/Lambda)[1]
|
|
35
|
+
return obj_1[0] - obj_2, np.sqrt(residual[0])
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Sinkhorn_DRO_Linear:
|
|
41
|
+
def __init__(self, input_dim, reg_=1, lambda_=1, output_dim=1,
|
|
42
|
+
maxiter = 50, learning_rate = 1e-2, K_sample_max=5, is_regression=0):
|
|
43
|
+
print(input_dim)
|
|
44
|
+
self.model = LinearModel(input_dim, output_dim)
|
|
45
|
+
self.lambda_ = lambda_
|
|
46
|
+
self.reg_ = reg_
|
|
47
|
+
self.maxiter_ = maxiter
|
|
48
|
+
self.learning_rate = learning_rate
|
|
49
|
+
self.K_sample_max = K_sample_max
|
|
50
|
+
self.is_regression = is_regression
|
|
51
|
+
|
|
52
|
+
def update(self, config={}):
|
|
53
|
+
if "reg" in config.keys():
|
|
54
|
+
self.reg_ = config["reg"]
|
|
55
|
+
if "lambda" in config.keys():
|
|
56
|
+
self.lambda_ = config["lambda"]
|
|
57
|
+
if "k" in config.keys():
|
|
58
|
+
self.K_sample_max = config["k"]
|
|
59
|
+
|
|
60
|
+
def predict(self, X):
|
|
61
|
+
X = torch.tensor(X).float()
|
|
62
|
+
self.model.cpu()
|
|
63
|
+
if self.is_regression:
|
|
64
|
+
pred = self.model(X)
|
|
65
|
+
else:
|
|
66
|
+
pred = self.model(X)
|
|
67
|
+
pred[pred > 0.5] = 1
|
|
68
|
+
pred[pred <= 0.5] = 0
|
|
69
|
+
|
|
70
|
+
return pred.detach().numpy()
|
|
71
|
+
|
|
72
|
+
def score(self, X, y):
|
|
73
|
+
if self.is_regression:
|
|
74
|
+
return np.mean(self.predict(X).reshape(-1)-y.reshape(-1))
|
|
75
|
+
else:
|
|
76
|
+
acc = np.mean([self.predict(X).flatten() == y.flatten()])
|
|
77
|
+
f1 = f1_score(y, self.predict(X), average='macro')
|
|
78
|
+
return acc, f1
|
|
79
|
+
|
|
80
|
+
def fit(self, X, y, optimization_type="SG"):
|
|
81
|
+
X_tensor = torch.tensor(X, dtype=torch.float32)
|
|
82
|
+
Y_tensor = torch.tensor(y, dtype=torch.float32)
|
|
83
|
+
dataset = TensorDataset(X_tensor, Y_tensor)
|
|
84
|
+
dataloader = DataLoader(dataset, batch_size=64, shuffle=True)
|
|
85
|
+
|
|
86
|
+
if optimization_type == 'SG':
|
|
87
|
+
self.SDRO_SG_solver(dataloader)
|
|
88
|
+
elif optimization_type == 'MLMC':
|
|
89
|
+
self.SDRO_MLMC_solver(dataloader)
|
|
90
|
+
elif optimization_type == 'RTMLMC':
|
|
91
|
+
self.SDRO_RTMLMC_solver(dataloader)
|
|
92
|
+
else:
|
|
93
|
+
raise ImplementationError("This optimization method is not implemented! Please choose one in \{SG, MLMC, RTMLMC\}")
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
theta = self.model.linear.weight.cpu().detach().tolist()
|
|
97
|
+
bias = self.model.linear.bias.cpu().detach().tolist()
|
|
98
|
+
|
|
99
|
+
params = {}
|
|
100
|
+
params['theta'] = theta
|
|
101
|
+
params['bias'] = bias
|
|
102
|
+
return params
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def SDRO_SG_solver(self, dataloader, device=torch.device("cuda:7")):
|
|
106
|
+
"""
|
|
107
|
+
2-SDRO Approach with SG estimator for Regression Problem
|
|
108
|
+
# Input:
|
|
109
|
+
# Feature: N samples of R^d [dim: N*d]
|
|
110
|
+
# Target: labels of N samples [dim: N*1]
|
|
111
|
+
# theta: initial guess for optimization
|
|
112
|
+
# Lambda: Lagrangian multiplier
|
|
113
|
+
# Reg: bandwidth
|
|
114
|
+
# Output:
|
|
115
|
+
# theta: optimized decision
|
|
116
|
+
"""
|
|
117
|
+
iter = 0
|
|
118
|
+
Lambda_Reg = self.lambda_ * self.reg_
|
|
119
|
+
self.model.to(device)
|
|
120
|
+
optimizer_theta = torch.optim.Adam(self.model.parameters(), lr=self.learning_rate)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
for epoch in range(self.maxiter_):
|
|
124
|
+
for _, (data, target) in enumerate(dataloader):
|
|
125
|
+
iter = iter + 1
|
|
126
|
+
|
|
127
|
+
# generate stochastic samples
|
|
128
|
+
N, d = data.shape
|
|
129
|
+
data, target = Variable(data), Variable(target)
|
|
130
|
+
m = int(2**self.K_sample_max)
|
|
131
|
+
|
|
132
|
+
optimizer_theta.zero_grad()
|
|
133
|
+
data_noise = torch.randn([m, N, d]) * np.sqrt(self.reg_) + data.reshape([1,N,d])
|
|
134
|
+
data_noise_vec = data_noise.reshape([-1,d])
|
|
135
|
+
target_noise = target.repeat(m,1).reshape(-1,1).to(device)
|
|
136
|
+
|
|
137
|
+
haty = self.model(data_noise_vec.to(device))
|
|
138
|
+
|
|
139
|
+
obj_vec = (haty - target_noise) ** 2
|
|
140
|
+
obj_mat = obj_vec.reshape([m, N])
|
|
141
|
+
Residual = obj_mat / Lambda_Reg
|
|
142
|
+
|
|
143
|
+
Loss_SDRO = (torch.logsumexp(Residual, dim=0, keepdim=True)-math.log(m)) * Lambda_Reg
|
|
144
|
+
Loss_SDRO_avg = torch.mean(Loss_SDRO)
|
|
145
|
+
|
|
146
|
+
Loss_SDRO_avg.backward()
|
|
147
|
+
optimizer_theta.step()
|
|
148
|
+
|
|
149
|
+
if iter % 10000 == 0:
|
|
150
|
+
print(f"Iter {iter} {Loss_SDRO_avg.data}")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def SDRO_MLMC_solver(self, dataloader):
|
|
154
|
+
iter = 0
|
|
155
|
+
Lambda_Reg = self.lambda_ * self.reg_
|
|
156
|
+
|
|
157
|
+
optimizer_theta = torch.optim.SGD(self.model.parameters(), lr=self.learning_rate)
|
|
158
|
+
N_ell_hist = np.int_(2**(np.arange(self.K_sample_max) + 1))
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
for epoch in range(self.maxiter_):
|
|
162
|
+
for _, (data, target) in enumerate(dataloader):
|
|
163
|
+
iter = iter + 1
|
|
164
|
+
# generate stochastic samples
|
|
165
|
+
N, d = data.shape
|
|
166
|
+
data, target = Variable(data), Variable(target)
|
|
167
|
+
optimizer_theta.zero_grad()
|
|
168
|
+
|
|
169
|
+
m_total = 0
|
|
170
|
+
for K_sample in np.arange(self.K_sample_max):
|
|
171
|
+
m = int(2**K_sample)
|
|
172
|
+
N_ell = N_ell_hist[-K_sample-1]
|
|
173
|
+
data_ell = data[:N_ell, :]
|
|
174
|
+
N_ell, d = data_ell.shape
|
|
175
|
+
target_ell = target[:N_ell]
|
|
176
|
+
|
|
177
|
+
data_noise = torch.randn([m, N_ell, d]) * np.sqrt(self.reg_) + data_ell.reshape([1,N_ell,d])
|
|
178
|
+
m_total += m * N_ell
|
|
179
|
+
data_noise_vec = data_noise.reshape([-1,d])
|
|
180
|
+
target_noise = target_ell.repeat(m,1)
|
|
181
|
+
|
|
182
|
+
haty = self.model(data_noise_vec)
|
|
183
|
+
obj_vec = (haty - target_noise) ** 2
|
|
184
|
+
obj_mat = obj_vec.reshape([m, N_ell])
|
|
185
|
+
Residual = obj_mat / Lambda_Reg
|
|
186
|
+
|
|
187
|
+
if K_sample == 0:
|
|
188
|
+
Loss_SDRO = torch.log(torch.mean(torch.exp(Residual), dim=0)) * Lambda_Reg
|
|
189
|
+
Loss_SDRO_avg_K_sample = torch.mean(Loss_SDRO)
|
|
190
|
+
Loss_SDRO_avg_sum = Loss_SDRO_avg_K_sample
|
|
191
|
+
else:
|
|
192
|
+
m1 = int(m/2)
|
|
193
|
+
Residual_half = Residual[:m1,:]
|
|
194
|
+
Residual_remain = Residual[m1:,:]
|
|
195
|
+
Loss_SDRO_1 = torch.log(torch.mean(torch.exp(Residual), dim=0)) * Lambda_Reg
|
|
196
|
+
Loss_SDRO_avg_1 = torch.mean(Loss_SDRO_1)
|
|
197
|
+
Loss_SDRO_2 = (torch.log(torch.mean(torch.exp(Residual_half), dim=0)) + torch.log(torch.mean(torch.exp(Residual_remain), dim=0))) * Lambda_Reg
|
|
198
|
+
Loss_SDRO_avg_2 = torch.mean(Loss_SDRO_2)
|
|
199
|
+
Loss_SDRO_avg_K_sample = Loss_SDRO_avg_1 - 0.5 * Loss_SDRO_avg_2
|
|
200
|
+
Loss_SDRO_avg_sum = Loss_SDRO_avg_sum + Loss_SDRO_avg_K_sample
|
|
201
|
+
|
|
202
|
+
Loss_SDRO_avg_sum.backward()
|
|
203
|
+
optimizer_theta.step()
|
|
204
|
+
|
|
205
|
+
# if torch.linalg.norm(self.model.linear.weight.data) > 0.95*np.sqrt(Lambda/2):
|
|
206
|
+
# self.model.linear.weight.data = self.model.linear.weight.data / torch.linalg.norm(self.model.linear.weight.data) * 0.95*np.sqrt(Lambda/2)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def SDRO_RTMLMC_solver(self, dataloader):
|
|
211
|
+
iter = 0
|
|
212
|
+
Lambda_Reg = self.lambda_ * self.reg_
|
|
213
|
+
optimizer_theta = torch.optim.SGD(self.model.parameters(), lr=self.learning_rate)
|
|
214
|
+
|
|
215
|
+
# sampling from truncated gemoetric distribution
|
|
216
|
+
elements = np.arange(self.K_sample_max)
|
|
217
|
+
probabilities = (0.5) ** (elements)
|
|
218
|
+
probabilities = probabilities / np.sum(probabilities)
|
|
219
|
+
|
|
220
|
+
for epoch in range(self.maxiter_):
|
|
221
|
+
for _, (data, target) in enumerate(dataloader):
|
|
222
|
+
iter = iter + 1
|
|
223
|
+
# generate stochastic samples
|
|
224
|
+
N, d = data.shape
|
|
225
|
+
data, target = Variable(data), Variable(target)
|
|
226
|
+
|
|
227
|
+
K_sample = int(np.random.choice(list(elements), 1, list(probabilities)))
|
|
228
|
+
m = int(2**K_sample)
|
|
229
|
+
optimizer_theta.zero_grad()
|
|
230
|
+
data_noise = torch.randn([m, N, d]) * np.sqrt(Reg) + data.reshape([1,N,d])
|
|
231
|
+
data_noise_vec = data_noise.reshape([-1,d])
|
|
232
|
+
target_noise = target.repeat(m,1)
|
|
233
|
+
|
|
234
|
+
haty = data_noise_vec @ theta_torch.type(torch.float64)
|
|
235
|
+
obj_vec = (haty - target_noise) ** 2
|
|
236
|
+
obj_mat = obj_vec.reshape([m, N])
|
|
237
|
+
Residual = obj_mat / Lambda_Reg
|
|
238
|
+
|
|
239
|
+
if m == 1:
|
|
240
|
+
Loss_SDRO = torch.log(torch.mean(torch.exp(Residual), dim=0)) * Lambda_Reg
|
|
241
|
+
Loss_SDRO_avg = torch.mean(Loss_SDRO)
|
|
242
|
+
else:
|
|
243
|
+
m1 = int(2**(K_sample-1))
|
|
244
|
+
Residual_half = Residual[:m1,:]
|
|
245
|
+
Residual_remain = Residual[m1:,:]
|
|
246
|
+
Loss_SDRO_1 = torch.log(torch.mean(torch.exp(Residual), dim=0)) * Lambda_Reg
|
|
247
|
+
Loss_SDRO_avg_1 = torch.mean(Loss_SDRO_1)
|
|
248
|
+
Loss_SDRO_2 = (torch.log(torch.mean(torch.exp(Residual_half), dim=0)) + torch.log(torch.mean(torch.exp(Residual_remain), dim=0))) * Lambda_Reg
|
|
249
|
+
Loss_SDRO_avg_2 = torch.mean(Loss_SDRO_2)
|
|
250
|
+
Loss_SDRO_avg = Loss_SDRO_avg_1 - 0.5 * Loss_SDRO_avg_2
|
|
251
|
+
|
|
252
|
+
Loss_SDRO_avg = 1/probabilities[K_sample]* Loss_SDRO_avg
|
|
253
|
+
Loss_SDRO_avg.backward()
|
|
254
|
+
optimizer_theta.step()
|
|
255
|
+
|
|
256
|
+
# if torch.linalg.norm(self.model.linear.weight.data) > 0.95*np.sqrt(Lambda/2):
|
|
257
|
+
# self.model.linear.weight.data = self.model.linear.weight.data / torch.linalg.norm(self.model.linear.weight.data) * 0.95*np.sqrt(Lambda/2)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
if __name__ == "__main__":
|
|
263
|
+
from sklearn.datasets import make_regression
|
|
264
|
+
from sklearn.linear_model import LinearRegression
|
|
265
|
+
import numpy as np
|
|
266
|
+
|
|
267
|
+
sample_size = 1000
|
|
268
|
+
feature_size = 10
|
|
269
|
+
X, y = make_regression(n_samples = sample_size, n_features = feature_size, noise = 1, random_state = 42)
|
|
270
|
+
|
|
271
|
+
method = Sinkhorn_DRO_Linear(0.1, 1000.0, maxiter=2000, input_dim=feature_size)
|
|
272
|
+
method.fit(X,y)
|
|
273
|
+
print(method.model.linear.weight)
|
|
274
|
+
|
|
275
|
+
model = LinearRegression(fit_intercept=True)
|
|
276
|
+
model.fit(X, y)
|
|
277
|
+
print(model.coef_)
|