genDWD 0.1.0__py3-none-any.whl

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.
gendwd/__init__.py ADDED
@@ -0,0 +1 @@
1
+ from .gendwd import genDWD
gendwd/gendwd.py ADDED
@@ -0,0 +1,361 @@
1
+ import numpy as np
2
+ from scipy.linalg import cho_factor, cho_solve
3
+ from scipy.spatial.distance import cdist
4
+
5
+ class genDWD:
6
+ def __init__(self, C='auto', q=1, maxIter=2000):
7
+
8
+ self.C = C
9
+ self.q = q
10
+ self.maxIter = maxIter
11
+
12
+
13
+
14
+ self.is_multiclass = False
15
+ self.classes_ = None
16
+ self.w_ = None
17
+ self.beta_ = None
18
+ self.history_ = None
19
+ self.models_ = None
20
+
21
+ def _Newton_Method(self, a, q, sigma, s_old, max_iter=20, tol=1e-8):
22
+ s = np.maximum(s_old, 1e-10)
23
+ inv_sigma = 1.0 / sigma
24
+ c1 = q * (q + 2) * inv_sigma
25
+ c2 = q * (q + 1) * inv_sigma
26
+
27
+ for i in range(max_iter):
28
+ s_safe = np.minimum(s, 1e10)
29
+ s_q1 = s_safe**(q + 1)
30
+ s_q2 = s_safe**(q + 2)
31
+
32
+ numerator = c1 + a * s_q1
33
+ denominator = c2 + s_q2
34
+
35
+ s_next = s * (numerator / (denominator + 1e-15))
36
+ s_next = np.maximum(1e-10, s_next)
37
+
38
+ if np.max(np.abs(s_next - s)) < tol:
39
+ return s_next
40
+
41
+ s = s_next
42
+ return s
43
+
44
+ def _penalty_parameter(self, X, y, expon=1, rmzeroFea=True, scaleFea=True):
45
+ dim, n = X.shape
46
+ if rmzeroFea:
47
+ normX = np.sqrt(np.sum(X**2, axis=1))
48
+ nzrow = np.where(normX > 0)[0]
49
+ if len(nzrow) < len(normX):
50
+ X = X[nzrow, :]
51
+ dim = X.shape[0]
52
+
53
+ if scaleFea:
54
+ if dim > 0.5 * n:
55
+ normX = np.sqrt(np.sum(X**2, axis=1))
56
+ if np.max(normX) > 2 * np.min(normX):
57
+ if dim > 3 * n:
58
+ scaling_vec = 1.0 / np.maximum(1, np.sqrt(normX))
59
+ else:
60
+ scaling_vec = 1.0 / np.maximum(1, normX)
61
+ X = X * scaling_vec[:, np.newaxis]
62
+
63
+ positive = np.where(y == 1)[0]
64
+ negative = np.where(y == -1)[0]
65
+
66
+ limit = 100 if (dim > 1e4 and n > 1e4) else 200
67
+
68
+ if len(positive) > limit:
69
+ positive = np.random.choice(positive, limit, replace=False)
70
+ if len(negative) > limit:
71
+ negative = np.random.choice(negative, limit, replace=False)
72
+
73
+ posX = X[:, positive].T
74
+ negX = X[:, negative].T
75
+
76
+ ddist = cdist(negX, posX, metric='euclidean')
77
+ dd = np.median(ddist)
78
+
79
+ if expon == 1:
80
+ const = np.log(n) * (max(1000, dim)**(1/3))
81
+ else:
82
+ const = 10 * np.log(n) * (max(1000, dim)**(1/3))
83
+
84
+ C = (10**(expon + 1)) * max(1, const / (dd**(expon + 1)))
85
+ return C
86
+
87
+ def _matvec_A(self, x, Z, y, const):
88
+ dim = Z.shape[0]
89
+ n = Z.shape[1]
90
+
91
+ x = x.reshape(-1, 1)
92
+ w_part = x[:dim, :]
93
+ beta_part = x[dim:, :]
94
+
95
+ Ztw = Z.T @ w_part
96
+ ZZtw = Z @ Ztw
97
+ y_vec = y.reshape(-1, 1)
98
+ Zy_beta = Z @ (y_vec * beta_part)
99
+
100
+ upper = ZZtw + (const * w_part) + Zy_beta
101
+ lower_left = y_vec.T @ Ztw
102
+ lower_right = (y_vec.T @ y_vec) * beta_part
103
+ lower = lower_left + lower_right
104
+
105
+ return np.vstack((upper, lower))
106
+
107
+ def _psqmr_solver(self, Z, y, rhs, x0, const, tol=1e-6, max_iter=100):
108
+ r = rhs - self._matvec_A(x0, Z, y, const)
109
+ q = r.copy()
110
+ tau = np.linalg.norm(q)
111
+ theta = 0.0
112
+ d = np.zeros_like(x0)
113
+ x = x0.copy()
114
+
115
+ for k in range(1, max_iter + 1):
116
+ Aq = self._matvec_A(q, Z, y, const)
117
+ num = r.T @ r
118
+ den = q.T @ Aq
119
+ alpha_k = num / (den + 1e-12)
120
+
121
+ r_next = r - alpha_k * Aq
122
+ theta_next = np.linalg.norm(r_next) / (tau + 1e-12)
123
+ c_k = 1.0 / np.sqrt(1 + theta_next**2)
124
+
125
+ tau_next = tau * theta_next * c_k
126
+ d = (c_k**2 * theta**2) * d + (c_k**2 * alpha_k) * q
127
+ x = x + d
128
+
129
+ if tau_next < tol:
130
+ break
131
+
132
+ beta_k = (r_next.T @ r_next) / (num + 1e-12)
133
+ q = r_next + beta_k * q
134
+ r, theta, tau = r_next, theta_next, tau_next
135
+
136
+ return x
137
+
138
+ def _binary_fit(self, X, y, C_val):
139
+
140
+ X = X.T
141
+ dim, n = X.shape
142
+
143
+ if C_val == 'auto':
144
+ C_val = self._penalty_parameter(X, y)
145
+
146
+
147
+ Zscale = np.linalg.norm(X, 'fro')
148
+ Z = X * y.reshape(1, -1) / Zscale
149
+
150
+ sigma = min(10 * C_val, n)**self.q
151
+ normZ = 1 + np.sqrt(np.max(np.sum(Z * Z, axis=0)))
152
+
153
+ r = np.ones((n, 1))
154
+ wbeta = np.zeros((dim+1, 1))
155
+ u = np.zeros((dim, 1))
156
+ xi = np.zeros((n, 1))
157
+ alpha = np.zeros((n, 1))
158
+ p = np.zeros((dim, 1))
159
+ tau = 1.618
160
+ const = 1.0
161
+ tol = 1e-5
162
+
163
+
164
+
165
+ use_psqmr = (dim > 3000 and n > 3000)
166
+ use_smw = (dim > 3000 and n <= 3000)
167
+ use_cholesky = not use_psqmr and not use_smw
168
+
169
+
170
+
171
+ if use_cholesky:
172
+ A1 = Z @ Z.T + const * np.eye(dim)
173
+ A2 = Z @ y.reshape(-1, 1)
174
+ A3 = A2.T
175
+ A4 = y.reshape(1, -1) @ y.reshape(-1, 1)
176
+ A = np.block([[A1, A2], [A3, A4]])
177
+ A += 1e-9 * np.eye(A.shape[0])
178
+ A_factor = cho_factor(A)
179
+ elif use_smw:
180
+ D_inv = 1.0 / const
181
+ norm_y_sq = np.sum(y**2)
182
+ J_utama = np.eye(n) + D_inv * (Z.T @ Z)
183
+ J_factor = cho_factor(J_utama)
184
+ y_vec = y.flatten()
185
+ J_inv_y = cho_solve(J_factor, y_vec)
186
+ denom_smw = 1.0 + (1.0 / norm_y_sq) * (y_vec @ J_inv_y) - 1.0
187
+ inv_D_hat = np.concatenate([np.full(dim, D_inv), [1.0 / norm_y_sq]])
188
+
189
+ def solver_system(r_val, xi_val, alpha_val, u_val, p_val, sigma, wbeta):
190
+ tmp = r_val - xi_val + alpha_val / sigma
191
+ rhs1 = Z @ tmp + const * u_val + p_val / sigma
192
+ rhs2 = y.T @ tmp
193
+ rhs = np.vstack((rhs1, rhs2))
194
+
195
+ if use_cholesky:
196
+ sol = cho_solve(A_factor, rhs)
197
+ elif use_smw:
198
+ h_hat = inv_D_hat.reshape(-1, 1) * rhs
199
+ norm_y = np.sqrt(norm_y_sq)
200
+ Ut_h_hat = np.zeros((n + 1, 1))
201
+ Ut_h_hat[:n] = Z.T @ h_hat[:dim] + y.reshape(-1, 1) * h_hat[dim:]
202
+ Ut_h_hat[n] = norm_y * h_hat[dim:]
203
+ v = Ut_h_hat.flatten()
204
+ J_inv_v = np.zeros(n + 1)
205
+ J_inv_v[:n] = cho_solve(J_factor, v[:n])
206
+ J_inv_v[n] = -v[n]
207
+ y_bar = np.append(y_vec / norm_y, 1.0)
208
+ y_bar_t_J_inv_v = y_bar @ J_inv_v
209
+ y_bar_t_J_inv_y_bar = y_bar @ np.append(J_inv_y / norm_y, -1.0)
210
+ H_inv_Ut_h_hat = J_inv_v - (y_bar_t_J_inv_v / (1.0 + y_bar_t_J_inv_y_bar)) * np.append(J_inv_y / norm_y, -1.0)
211
+ U_res = np.zeros((dim + 1, 1))
212
+ U_res[:dim] = Z @ H_inv_Ut_h_hat[:n].reshape(-1, 1)
213
+ U_res[dim:] = y.reshape(1, -1) @ H_inv_Ut_h_hat[:n].reshape(-1, 1) + norm_y * H_inv_Ut_h_hat[n]
214
+ sol = h_hat - inv_D_hat.reshape(-1, 1) * U_res
215
+ elif use_psqmr:
216
+ sol = self._psqmr_solver(Z, y, rhs, wbeta, const, tol=1e-4, max_iter=50)
217
+ return sol[:dim], sol[dim:]
218
+
219
+ for iter in range(1, self.maxIter + 1):
220
+ rold, wbetaold, uold = r.copy(), wbeta.copy(), u.copy()
221
+ xiold, alphaold, pold = xi.copy(), alpha.copy(), p.copy()
222
+
223
+ w, beta = solver_system(rold, xiold, alphaold, uold, pold, sigma, wbetaold)
224
+
225
+ ZTwpbetay = Z.T @ w + beta * y.reshape(-1, 1)
226
+ cc = ZTwpbetay + xiold - alphaold / sigma
227
+ r = self._Newton_Method(cc, self.q, sigma, rold)
228
+ r = np.maximum(r, 0)
229
+
230
+ doublecompute_measure = normZ * np.linalg.norm(r - rold) * (iter**1.5)
231
+ if doublecompute_measure > 10 or iter < 50:
232
+ w, beta = solver_system(rold, xiold, alphaold, uold, pold, sigma, wbetaold)
233
+ ZTwpbetay = Z.T @ w + beta * y.reshape(-1, 1)
234
+
235
+ uinput = w - pold / (const * sigma)
236
+ u_norm = np.linalg.norm(uinput)
237
+ u = Zscale * uinput / max(Zscale, u_norm)
238
+
239
+ xiinput = r - ZTwpbetay + (alphaold - C_val) / sigma
240
+ xi = np.maximum(xiinput, 0)
241
+
242
+ Rp = ZTwpbetay + xi - r
243
+ alpha = alphaold - tau * sigma * Rp
244
+ p = pold - (tau * sigma * const) * (w - u)
245
+
246
+ alpha_f = alpha.flatten()
247
+ xi_f = xi.flatten()
248
+ r_f = r.flatten()
249
+ y_f = y.flatten()
250
+ rexpon1 = np.maximum(r_f, 1e-10)**(self.q + 1)
251
+
252
+ eta_c1 = np.abs(np.dot(y_f, alpha_f))
253
+ eta_c2 = np.abs(np.dot(xi_f, (C_val - alpha_f)))
254
+ term_comp3_a = np.linalg.norm(alpha_f * rexpon1 - self.q)
255
+ term_comp3_b = np.linalg.norm(alpha_f - self.q / rexpon1)**2
256
+ eta_c3 = min(term_comp3_a, term_comp3_b)
257
+ eta_c = max(eta_c1, eta_c2, eta_c3) / (1 + C_val)
258
+
259
+ eta_p1 = np.linalg.norm(Rp)
260
+ eta_p2 = np.linalg.norm(w - u)
261
+ eta_p3 = max(np.linalg.norm(w) - Zscale, 0)
262
+ eta_p = max(eta_p1, eta_p2, eta_p3) / (1 + C_val)
263
+
264
+ eta_d = max(np.linalg.norm(np.minimum(0, alpha_f)),
265
+ np.linalg.norm(np.maximum(alpha_f - C_val, 0))) / (1 + C_val)
266
+
267
+ primobj = np.sum(r_f / rexpon1) + np.sum(C_val * xi_f) + 1e-8
268
+ kappa = ((self.q + 1) / self.q) * (self.q**(1 / (self.q + 1)))
269
+ term_dual1 = kappa * np.sum(np.maximum(0, alpha_f)**(self.q / (self.q + 1)))
270
+ term_dual2 = np.linalg.norm(Z @ alpha.reshape(-1, 1))
271
+
272
+ dualobj = term_dual1 - term_dual2
273
+ eta_gap = abs(primobj - dualobj) / (1 + abs(primobj) + abs(dualobj))
274
+
275
+ kondisi1 = max(eta_p, eta_d)
276
+ kondisi2 = min(eta_gap, eta_c)
277
+ kondisi3 = max(eta_gap, eta_c)
278
+
279
+
280
+
281
+ if kondisi1 < tol and kondisi2 < np.sqrt(tol) and kondisi3 < 0.05:
282
+ break
283
+
284
+ if iter % 20 == 0:
285
+ ratio = max(eta_p, 0.2 * tol) / max(eta_d, 0.2 * tol)
286
+ if ratio > 5:
287
+ sigma = min(sigma * 2.2, 1e6)
288
+ elif 1 / ratio > 5:
289
+ sigma = max(sigma / 1.65, 1e-3)
290
+
291
+
292
+
293
+ w = w / Zscale
294
+
295
+ return w, beta
296
+
297
+ def fit(self, X, y):
298
+ self.classes_ = np.sort(np.unique(y))
299
+
300
+ if len(self.classes_) == 2:
301
+ # Mode Biner
302
+ self.is_multiclass = False
303
+ self.c_neg, self.c_pos = self.classes_[0], self.classes_[1]
304
+ y_bin = np.where(y == self.c_pos, 1, -1)
305
+
306
+ res = self._binary_fit(X, y_bin, self.C)
307
+
308
+ self.w_, self.beta_ = res
309
+
310
+ elif len(self.classes_) > 2:
311
+ # Mode Multi-kelas (One-vs-One)
312
+ self.is_multiclass = True
313
+ self.models_ = {}
314
+
315
+
316
+ for i in range(len(self.classes_)):
317
+ for j in range(i + 1, len(self.classes_)):
318
+ c_i = self.classes_[i]
319
+ c_j = self.classes_[j]
320
+
321
+
322
+
323
+ mask = (y == c_i) | (y == c_j)
324
+ y_mask = np.where(y[mask] == c_i, -1, 1)
325
+
326
+
327
+ res = self._binary_fit(X[mask], y_mask, self.C)
328
+
329
+ w_bin, b_bin = res
330
+
331
+ self.models_[(c_i, c_j)] = (w_bin, b_bin)
332
+ else:
333
+ raise ValueError("Data label 'y' hanya memiliki 1 kelas. Tidak dapat diklasifikasi.")
334
+
335
+ return self
336
+
337
+ def predict(self, X):
338
+ if not self.is_multiclass:
339
+ # Prediksi Biner
340
+ proyeksi = np.dot(X, self.w_) + self.beta_
341
+ y_pred_bin = np.where(proyeksi > 0, 1, -1).flatten()
342
+ return np.where(y_pred_bin == 1, self.c_pos, self.c_neg)
343
+
344
+ else:
345
+ # Prediksi Multi-kelas (Voting / Akumulasi Skor)
346
+ n_samples = X.shape[0]
347
+ n_classes = len(self.classes_)
348
+ accumulated_scores = np.zeros((n_samples, n_classes), dtype=float)
349
+
350
+ for (c_i, c_j), (w, b) in self.models_.items():
351
+ score = np.dot(X, w) + b
352
+ score = score.reshape(-1)
353
+
354
+ idx_i = np.where(self.classes_ == c_i)[0][0]
355
+ idx_j = np.where(self.classes_ == c_j)[0][0]
356
+
357
+ accumulated_scores[:, idx_j] += score
358
+ accumulated_scores[:, idx_i] -= score
359
+
360
+ final_indices = np.argmax(accumulated_scores, axis=1)
361
+ return self.classes_[final_indices]
@@ -0,0 +1,90 @@
1
+ Metadata-Version: 2.4
2
+ Name: genDWD
3
+ Version: 0.1.0
4
+ Summary: Generalized Distance Weighted Discrimination (DWD) with sGS-ADDM
5
+ Author: Thoriq Al Mahdi
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: numpy
11
+ Requires-Dist: scipy
12
+ Dynamic: author
13
+ Dynamic: classifier
14
+ Dynamic: description
15
+ Dynamic: description-content-type
16
+ Dynamic: license-file
17
+ Dynamic: requires-dist
18
+ Dynamic: summary
19
+
20
+ # genDWD: Generalized Distance Weighted Discrimination
21
+
22
+ **genDWD** is a high-performance Python implementation of the Generalized Distance Weighted Discrimination (DWD) algorithm.
23
+
24
+ While **Support Vector Machines (SVM)** are widely used, they often suffer from the "data piling" phenomenon in **HDLLS (High-Dimension, Low Large Sample)** settings, where many data points project onto the same location on the decision boundary, leading to poor generalization. DWD was specifically developed to overcome this by accounting for the relative distance of all data points, providing better robustness in high-dimensional spaces.
25
+
26
+ Originally formulated for binary classification, this implementation extends the algorithm's utility by featuring built-in **Multiclass classification** support, enabling its application to complex, real-world datasets with multiple categories. It is designed to handle these tasks efficiently using the sGS-ADMM framework for large-scale optimization.
27
+
28
+ ---
29
+
30
+ ## Core Research Basis
31
+
32
+ This implementation is strictly constructed based on the numerical procedures described in the following scientific paper:
33
+
34
+ > **"Fast algorithms for large scale generalized distance weighted discrimination"**
35
+ >
36
+ > — *Xin Yee Lam, J.S. Marron, Defeng Sun, and Kim-Chuan Toh (September 5, 2018)*
37
+
38
+ ## Key Features
39
+
40
+ - **Automatic Detection**: Automatically switches between Binary and Multiclass (One-vs-One) logic.
41
+ - **Adaptive Solvers**: Dynamically selects between Cholesky, SMW, or PSQMR based on $n$ and $d$.
42
+ - **sGS-ADMM Framework**: Ensures fast convergence using Symmetric Gauss-Seidel ADMM.
43
+ - **Penalty Tuning**: Automatic $C$ parameter calculation using median inter-class distance.
44
+
45
+ ## Quick Start
46
+
47
+ 1. Install the package:
48
+ ```bash
49
+ pip install genDWD
50
+ ```
51
+
52
+ ## Implementation Guide
53
+
54
+ To use the `genDWD` class in your project, follow the instructions below. The model follows the standard `.fit()` and `.predict()` pattern.
55
+
56
+ ### 1. Basic Initialization
57
+ You can customize the model during initialization. The parameter `C='auto'` is recommended as it calculates the penalty based on the dataset's geometry.
58
+
59
+ ```python
60
+ from gendwd import genDWD
61
+
62
+ model = genDWD(C='auto')
63
+ ```
64
+
65
+ ### 2. Model Training and Prediction
66
+
67
+ The model follows the familiar `.fit()` and `.predict()` workflow. It automatically handles both binary and multiclass data.
68
+
69
+ #### Training the Model
70
+ To train the model, pass your feature matrix `X` and label vector `y`. The algorithm will automatically detect if it should use a binary or multiclass (One-vs-One) strategy.
71
+
72
+ ```python
73
+ import numpy as np
74
+
75
+ # Sample Data: 100 samples, 10 features
76
+ X_train = np.random.randn(100, 10)
77
+ # Labels can be integers, strings, or floats
78
+ y_train = np.random.choice(['Class_A', 'Class_B'], size=100)
79
+
80
+ # Train the model
81
+ model.fit(X_train, y_train)
82
+
83
+ # New data for prediction
84
+ X_new = np.random.randn(5, 10)
85
+
86
+ # Get predicted class labels
87
+ predictions = model.predict(X_new)
88
+
89
+ print(f"Predicted Labels: {predictions}")
90
+ ```
@@ -0,0 +1,7 @@
1
+ gendwd/__init__.py,sha256=EbtMsfPLPF_b5O3JVuBdPAlzRzmBKzpmhKUFGsC1T4A,26
2
+ gendwd/gendwd.py,sha256=fXZnYnWRKnktX1y-iaSW8cyenJRKRuh0TKQfPLBhUpk,13184
3
+ gendwd-0.1.0.dist-info/licenses/LICENSE,sha256=u7lYZflRTuVo3maaTYD4iVmUvXJPDALrMt5hinBpngg,1091
4
+ gendwd-0.1.0.dist-info/METADATA,sha256=XYcVBGKLbzx7bQ7cAhRkDN0Bj6pEgX5-1ZHuNTq4heM,3578
5
+ gendwd-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
6
+ gendwd-0.1.0.dist-info/top_level.txt,sha256=naPpbv6J0Q16BxSptJDIqO2KiBZuYA3NDLxBLsJ2lZ4,7
7
+ gendwd-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Thoriq Al Mahdi
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.
@@ -0,0 +1 @@
1
+ gendwd