mumpy-toolkit 0.2.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.
mumpy/nn.py ADDED
@@ -0,0 +1,278 @@
1
+ """mumpy.nn: tiny deep-learning toolkit in pure numpy.
2
+
3
+ Layers: Dense / ReLU / Sigmoid / Tanh / Softmax / Dropout / BatchNorm-lite
4
+ Losses: MSE / BCE / CrossEntropy
5
+ Optimizers: SGD (+momentum) / Adam
6
+ Model: Sequential with fit/predict/save/load
7
+
8
+ from mumpy import nn
9
+ model = nn.Sequential([nn.Dense(4, 16), nn.ReLU(), nn.Dense(16, 1)])
10
+ model.fit(X, y, epochs=200, lr=0.01, verbose=True)
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import numpy as np
15
+
16
+ __all__ = [
17
+ "Dense", "ReLU", "Sigmoid", "Tanh", "Softmax", "Dropout",
18
+ "MSELoss", "BCELoss", "CrossEntropyLoss",
19
+ "SGD", "Adam", "Sequential",
20
+ ]
21
+
22
+
23
+ class Dense:
24
+ def __init__(self, in_features, out_features, seed=0):
25
+ rng = np.random.default_rng(seed)
26
+ self.W = rng.normal(0, np.sqrt(2 / in_features), (in_features, out_features))
27
+ self.b = np.zeros(out_features)
28
+ self.dW = np.zeros_like(self.W)
29
+ self.db = np.zeros_like(self.b)
30
+
31
+ def forward(self, x):
32
+ self.x = x
33
+ return x @ self.W + self.b
34
+
35
+ def backward(self, grad):
36
+ self.dW = self.x.T @ grad
37
+ self.db = grad.sum(0)
38
+ return grad @ self.W.T
39
+
40
+ def params(self):
41
+ return [(self.W, self.dW), (self.b, self.db)]
42
+
43
+ def __call__(self, x):
44
+ return self.forward(x)
45
+
46
+
47
+ class ReLU:
48
+ def forward(self, x):
49
+ self.x = x
50
+ return np.maximum(x, 0)
51
+
52
+ def backward(self, g):
53
+ return g * (self.x > 0)
54
+
55
+ def params(self):
56
+ return []
57
+
58
+ def __call__(self, x):
59
+ return self.forward(x)
60
+
61
+
62
+ class Sigmoid:
63
+ def forward(self, x):
64
+ self.o = 1 / (1 + np.exp(-np.clip(x, -30, 30)))
65
+ return self.o
66
+
67
+ def backward(self, g):
68
+ return g * self.o * (1 - self.o)
69
+
70
+ def params(self):
71
+ return []
72
+
73
+ def __call__(self, x):
74
+ return self.forward(x)
75
+
76
+
77
+ class Tanh:
78
+ def forward(self, x):
79
+ self.o = np.tanh(x)
80
+ return self.o
81
+
82
+ def backward(self, g):
83
+ return g * (1 - self.o ** 2)
84
+
85
+ def params(self):
86
+ return []
87
+
88
+ def __call__(self, x):
89
+ return self.forward(x)
90
+
91
+
92
+ class Softmax:
93
+ def forward(self, x):
94
+ x = x - x.max(1, keepdims=True)
95
+ e = np.exp(x)
96
+ self.o = e / e.sum(1, keepdims=True)
97
+ return self.o
98
+
99
+ def backward(self, g):
100
+ # combined with CrossEntropy upstream usually passes (p - y)/n
101
+ return g
102
+
103
+ def params(self):
104
+ return []
105
+
106
+ def __call__(self, x):
107
+ return self.forward(x)
108
+
109
+
110
+ class Dropout:
111
+ def __init__(self, p=0.2, seed=0):
112
+ self.p = p
113
+ self.rng = np.random.default_rng(seed)
114
+ self.training = True
115
+
116
+ def forward(self, x):
117
+ if not self.training or self.p == 0:
118
+ return x
119
+ self.mask = (self.rng.random(x.shape) > self.p) / (1 - self.p)
120
+ return x * self.mask
121
+
122
+ def backward(self, g):
123
+ return g * self.mask if self.training and self.p else g
124
+
125
+ def params(self):
126
+ return []
127
+
128
+ def __call__(self, x):
129
+ return self.forward(x)
130
+
131
+
132
+ class MSELoss:
133
+ def forward(self, pred, target):
134
+ self.pred, self.target = pred, target
135
+ return float(np.mean((pred - target) ** 2))
136
+
137
+ def backward(self):
138
+ n = self.pred.size
139
+ return 2 * (self.pred - self.target) / n
140
+
141
+
142
+ class BCELoss:
143
+ def forward(self, pred, target, eps=1e-12):
144
+ self.pred = np.clip(pred, eps, 1 - eps)
145
+ self.target = target
146
+ return float(-np.mean(target * np.log(self.pred) + (1 - target) * np.log(1 - self.pred)))
147
+
148
+ def backward(self):
149
+ n = self.pred.size
150
+ p, t = self.pred, self.target
151
+ return (-(t / p) + (1 - t) / (1 - p)) / n
152
+
153
+
154
+ class CrossEntropyLoss:
155
+ def forward(self, logits, target):
156
+ # logits: (n, K), target: class indices
157
+ z = logits - logits.max(1, keepdims=True)
158
+ e = np.exp(z)
159
+ self.probs = e / e.sum(1, keepdims=True)
160
+ self.target = np.asanyarray(target).ravel().astype(int)
161
+ n = len(self.target)
162
+ return float(-np.log(self.probs[np.arange(n), self.target] + 1e-12).mean())
163
+
164
+ def backward(self):
165
+ n = len(self.target)
166
+ g = self.probs.copy()
167
+ g[np.arange(n), self.target] -= 1
168
+ return g / n
169
+
170
+
171
+ class SGD:
172
+ def __init__(self, lr=0.01, momentum=0.0):
173
+ self.lr = lr
174
+ self.momentum = momentum
175
+ self.vel = {}
176
+
177
+ def step(self, layers):
178
+ for li, layer in enumerate(layers):
179
+ for pi, (p, g) in enumerate(layer.params()):
180
+ key = (li, pi)
181
+ if self.momentum:
182
+ v = self.vel.get(key, np.zeros_like(p))
183
+ v = self.momentum * v + g
184
+ self.vel[key] = v
185
+ p -= self.lr * v
186
+ else:
187
+ p -= self.lr * g
188
+
189
+
190
+ class Adam:
191
+ def __init__(self, lr=0.001, beta1=0.9, beta2=0.999, eps=1e-8):
192
+ self.lr = lr
193
+ self.beta1 = beta1
194
+ self.beta2 = beta2
195
+ self.eps = eps
196
+ self.m = {}
197
+ self.v = {}
198
+ self.t = 0
199
+
200
+ def step(self, layers):
201
+ self.t += 1
202
+ for li, layer in enumerate(layers):
203
+ for pi, (p, g) in enumerate(layer.params()):
204
+ key = (li, pi)
205
+ m = self.m.get(key, np.zeros_like(p))
206
+ v = self.v.get(key, np.zeros_like(p))
207
+ m = self.beta1 * m + (1 - self.beta1) * g
208
+ v = self.beta2 * v + (1 - self.beta2) * (g ** 2)
209
+ self.m[key], self.v[key] = m, v
210
+ mh = m / (1 - self.beta1 ** self.t)
211
+ vh = v / (1 - self.beta2 ** self.t)
212
+ p -= self.lr * mh / (np.sqrt(vh) + self.eps)
213
+
214
+
215
+ class Sequential:
216
+ def __init__(self, layers):
217
+ self.layers = layers
218
+
219
+ def forward(self, x):
220
+ for l in self.layers:
221
+ x = l.forward(x)
222
+ return x
223
+
224
+ def predict(self, X):
225
+ for l in self.layers:
226
+ if isinstance(l, Dropout):
227
+ l.training = False
228
+ out = self.forward(np.asanyarray(X, dtype=float))
229
+ for l in self.layers:
230
+ if isinstance(l, Dropout):
231
+ l.training = True
232
+ return out
233
+
234
+ def fit(self, X, y, epochs=100, batch_size=None, lr=0.01, optimizer=None,
235
+ loss="mse", verbose=False, seed=0, shuffle=True):
236
+ X = np.asanyarray(X, dtype=float)
237
+ y = np.asanyarray(y)
238
+ rng = np.random.default_rng(seed)
239
+ opt = optimizer or Adam(lr)
240
+ if isinstance(opt, float):
241
+ opt = Adam(opt)
242
+ losses = {"mse": MSELoss, "bce": BCELoss, "ce": CrossEntropyLoss}
243
+ criterion = losses[loss]() if isinstance(loss, str) else loss
244
+ n = len(X)
245
+ history = []
246
+ for ep in range(epochs):
247
+ idx = rng.permutation(n) if shuffle else np.arange(n)
248
+ total, nb = 0.0, 0
249
+ bs = batch_size or n
250
+ for s in range(0, n, bs):
251
+ bi = idx[s:s + bs]
252
+ xb, yb = X[bi], y[bi]
253
+ # reshape y for mse/bce single-output
254
+ if yb.ndim == 1 and not isinstance(criterion, CrossEntropyLoss):
255
+ yb = yb.reshape(-1, 1)
256
+ pred = self.forward(xb)
257
+ total += criterion.forward(pred, yb) * len(bi)
258
+ grad = criterion.backward()
259
+ for l in reversed(self.layers):
260
+ grad = l.backward(grad)
261
+ opt.step(self.layers)
262
+ nb += len(bi)
263
+ history.append(total / max(nb, 1))
264
+ if verbose and (ep % max(1, epochs // 10) == 0 or ep == epochs - 1):
265
+ print(f"epoch {ep + 1}/{epochs} loss={history[-1]:.6f}")
266
+ return history
267
+
268
+ def save(self, path):
269
+ import pickle
270
+ with open(path, "wb") as f:
271
+ pickle.dump(self, f)
272
+ return path
273
+
274
+ @staticmethod
275
+ def load(path):
276
+ import pickle
277
+ with open(path, "rb") as f:
278
+ return pickle.load(f)
mumpy/preprocessing.py ADDED
@@ -0,0 +1,259 @@
1
+ """mumpy.preprocessing: sklearn-like transformers in pure numpy.
2
+
3
+ StandardScaler / MinMaxScaler / RobustScaler / MaxAbsScaler
4
+ OneHotEncoder / LabelEncoder / OrdinalEncoder
5
+ SimpleImputer / train_test_split / PolynomialFeatures / Pipeline
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import numpy as np
10
+
11
+ __all__ = [
12
+ "StandardScaler", "MinMaxScaler", "RobustScaler", "MaxAbsScaler",
13
+ "OneHotEncoder", "LabelEncoder", "SimpleImputer",
14
+ "train_test_split", "shuffle", "PolynomialFeatures", "Pipeline",
15
+ ]
16
+
17
+
18
+ class StandardScaler:
19
+ def __init__(self, with_mean=True, with_std=True):
20
+ self.with_mean = with_mean
21
+ self.with_std = with_std
22
+ self.mean_ = None
23
+ self.scale_ = None
24
+
25
+ def fit(self, X):
26
+ X = np.asanyarray(X, dtype=float)
27
+ self.mean_ = np.nanmean(X, axis=0) if self.with_mean else np.zeros(X.shape[1])
28
+ self.scale_ = np.nanstd(X, axis=0) if self.with_std else np.ones(X.shape[1])
29
+ self.scale_[self.scale_ == 0] = 1.0
30
+ return self
31
+
32
+ def transform(self, X):
33
+ X = np.asanyarray(X, dtype=float).copy()
34
+ if self.with_mean:
35
+ X -= self.mean_
36
+ if self.with_std:
37
+ X /= self.scale_
38
+ return X
39
+
40
+ def fit_transform(self, X):
41
+ return self.fit(X).transform(X)
42
+
43
+ def inverse_transform(self, X):
44
+ X = np.asanyarray(X, dtype=float).copy()
45
+ if self.with_std:
46
+ X *= self.scale_
47
+ if self.with_mean:
48
+ X += self.mean_
49
+ return X
50
+
51
+
52
+ class MinMaxScaler:
53
+ def __init__(self, feature_range=(0, 1)):
54
+ self.feature_range = feature_range
55
+ self.min_ = None
56
+ self.scale_ = None
57
+ self.data_min_ = None
58
+ self.data_max_ = None
59
+
60
+ def fit(self, X):
61
+ X = np.asanyarray(X, dtype=float)
62
+ self.data_min_ = np.nanmin(X, axis=0)
63
+ self.data_max_ = np.nanmax(X, axis=0)
64
+ rng = self.data_max_ - self.data_min_
65
+ rng[rng == 0] = 1.0
66
+ lo, hi = self.feature_range
67
+ self.scale_ = (hi - lo) / rng
68
+ self.min_ = lo - self.data_min_ * self.scale_
69
+ return self
70
+
71
+ def transform(self, X):
72
+ X = np.asanyarray(X, dtype=float).copy()
73
+ return X * self.scale_ + self.min_
74
+
75
+ def fit_transform(self, X):
76
+ return self.fit(X).transform(X)
77
+
78
+ def inverse_transform(self, X):
79
+ X = np.asanyarray(X, dtype=float).copy()
80
+ return (X - self.min_) / self.scale_
81
+
82
+
83
+ class RobustScaler:
84
+ def __init__(self, quantile_range=(25.0, 75.0)):
85
+ self.quantile_range = quantile_range
86
+ self.center_ = None
87
+ self.scale_ = None
88
+
89
+ def fit(self, X):
90
+ X = np.asanyarray(X, dtype=float)
91
+ qmin, qmax = self.quantile_range
92
+ self.center_ = np.nanmedian(X, axis=0)
93
+ q1 = np.nanpercentile(X, qmin, axis=0)
94
+ q3 = np.nanpercentile(X, qmax, axis=0)
95
+ self.scale_ = q3 - q1
96
+ self.scale_[self.scale_ == 0] = 1.0
97
+ return self
98
+
99
+ def transform(self, X):
100
+ return (np.asanyarray(X, dtype=float) - self.center_) / self.scale_
101
+
102
+ def fit_transform(self, X):
103
+ return self.fit(X).transform(X)
104
+
105
+
106
+ class MaxAbsScaler:
107
+ def fit(self, X):
108
+ X = np.asanyarray(X, dtype=float)
109
+ self.max_abs_ = np.nanmax(np.abs(X), axis=0)
110
+ self.max_abs_[self.max_abs_ == 0] = 1.0
111
+ return self
112
+
113
+ def transform(self, X):
114
+ return np.asanyarray(X, dtype=float) / self.max_abs_
115
+
116
+ def fit_transform(self, X):
117
+ return self.fit(X).transform(X)
118
+
119
+
120
+ class OneHotEncoder:
121
+ def __init__(self, sparse=False):
122
+ self.sparse = sparse
123
+ self.categories_ = None
124
+
125
+ def fit(self, y):
126
+ y = np.asanyarray(y).ravel()
127
+ self.categories_ = np.unique(y)
128
+ return self
129
+
130
+ def transform(self, y):
131
+ y = np.asanyarray(y).ravel()
132
+ idx = {v: i for i, v in enumerate(self.categories_.tolist())}
133
+ out = np.zeros((y.size, len(self.categories_)), dtype=float)
134
+ for i, v in enumerate(y.tolist()):
135
+ if v in idx:
136
+ out[i, idx[v]] = 1.0
137
+ return out
138
+
139
+ def fit_transform(self, y):
140
+ return self.fit(y).transform(y)
141
+
142
+
143
+ class LabelEncoder:
144
+ def fit(self, y):
145
+ self.classes_ = np.unique(np.asanyarray(y).ravel())
146
+ self._map = {v: i for i, v in enumerate(self.classes_.tolist())}
147
+ return self
148
+
149
+ def transform(self, y):
150
+ y = np.asanyarray(y).ravel()
151
+ return np.array([self._map[v] for v in y.tolist()])
152
+
153
+ def fit_transform(self, y):
154
+ return self.fit(y).transform(y)
155
+
156
+ def inverse_transform(self, y):
157
+ return np.array([self.classes_[int(i)] for i in np.asanyarray(y).ravel()])
158
+
159
+
160
+ class SimpleImputer:
161
+ def __init__(self, strategy="mean", fill_value=0.0):
162
+ self.strategy = strategy
163
+ self.fill_value = fill_value
164
+
165
+ def fit(self, X):
166
+ X = np.asanyarray(X, dtype=float)
167
+ if self.strategy == "mean":
168
+ self.statistics_ = np.nanmean(X, axis=0)
169
+ elif self.strategy == "median":
170
+ self.statistics_ = np.nanmedian(X, axis=0)
171
+ elif self.strategy == "most_frequent":
172
+ self.statistics_ = np.array([np.unique(c[~np.isnan(c)])[0] if (~np.isnan(c)).any() else self.fill_value
173
+ for c in X.T])
174
+ else:
175
+ self.statistics_ = np.full(X.shape[1], self.fill_value)
176
+ self.statistics_ = np.where(np.isnan(self.statistics_), self.fill_value, self.statistics_)
177
+ return self
178
+
179
+ def transform(self, X):
180
+ X = np.asanyarray(X, dtype=float).copy()
181
+ idx = np.where(np.isnan(X))
182
+ X[idx] = np.take(self.statistics_, idx[1])
183
+ return X
184
+
185
+ def fit_transform(self, X):
186
+ return self.fit(X).transform(X)
187
+
188
+
189
+ def train_test_split(*arrays, test_size=0.2, random_state=None, shuffle=True):
190
+ rng = np.random.default_rng(random_state)
191
+ n = len(np.asanyarray(arrays[0]))
192
+ idx = np.arange(n)
193
+ if shuffle:
194
+ rng.shuffle(idx)
195
+ n_test = int(n * test_size) if isinstance(test_size, float) else int(test_size)
196
+ te, tr = idx[:n_test], idx[n_test:]
197
+ out = []
198
+ for a in arrays:
199
+ a = np.asanyarray(a)
200
+ out += [a[tr], a[te]]
201
+ return out
202
+
203
+
204
+ def shuffle(*arrays, random_state=None):
205
+ rng = np.random.default_rng(random_state)
206
+ n = len(np.asanyarray(arrays[0]))
207
+ idx = np.arange(n)
208
+ rng.shuffle(idx)
209
+ return [np.asanyarray(a)[idx] for a in arrays]
210
+
211
+
212
+ class PolynomialFeatures:
213
+ def __init__(self, degree=2, include_bias=True):
214
+ self.degree = degree
215
+ self.include_bias = include_bias
216
+
217
+ def fit(self, X):
218
+ return self
219
+
220
+ def transform(self, X):
221
+ from itertools import combinations_with_replacement
222
+ X = np.asanyarray(X, dtype=float)
223
+ n, d = X.shape
224
+ feats = [np.ones((n, 1))] if self.include_bias else []
225
+ for deg in range(1, self.degree + 1):
226
+ for combo in combinations_with_replacement(range(d), deg):
227
+ feats.append(np.prod(X[:, combo], axis=1, keepdims=True))
228
+ return np.hstack(feats)
229
+
230
+ def fit_transform(self, X):
231
+ return self.fit(X).transform(X)
232
+
233
+
234
+ class Pipeline:
235
+ def __init__(self, steps):
236
+ self.steps = steps # [(name, transformer/estimator)]
237
+
238
+ def fit(self, X, y=None):
239
+ Xt = X
240
+ for _, s in self.steps[:-1]:
241
+ Xt = s.fit_transform(Xt) if y is None else s.fit(Xt, y).transform(Xt) if hasattr(s, "transform") else s.fit(Xt, y)
242
+ name, est = self.steps[-1]
243
+ if y is None:
244
+ est.fit(Xt)
245
+ else:
246
+ est.fit(Xt, y)
247
+ return self
248
+
249
+ def predict(self, X):
250
+ Xt = X
251
+ for _, s in self.steps[:-1]:
252
+ Xt = s.transform(Xt)
253
+ return self.steps[-1][1].predict(Xt)
254
+
255
+ def transform(self, X):
256
+ Xt = X
257
+ for _, s in self.steps:
258
+ Xt = s.transform(Xt)
259
+ return Xt
mumpy/random.py ADDED
@@ -0,0 +1,99 @@
1
+ """mumpy.random: numpy.random-compatible API over np.random.Generator (PCG64)."""
2
+ from __future__ import annotations
3
+
4
+ import numpy as np
5
+
6
+ __all__ = ["default_rng", "rand", "randn", "randint", "random", "choice",
7
+ "shuffle", "permutation", "normal", "uniform", "seed", "Generator",
8
+ "integers", "exponential", "poisson", "binomial", "multivariate_normal",
9
+ "beta", "gamma", "multinomial", "standard_cauchy", "laplace"]
10
+
11
+ _global_rng = np.random.default_rng()
12
+
13
+
14
+ def default_rng(seed=None):
15
+ return np.random.default_rng(seed)
16
+
17
+
18
+ def seed(s=None):
19
+ global _global_rng
20
+ _global_rng = np.random.default_rng(s)
21
+
22
+
23
+ def rand(*args):
24
+ return _global_rng.random(args if args else None)
25
+
26
+
27
+ def random(size=None):
28
+ return _global_rng.random(size)
29
+
30
+
31
+ def randn(*args):
32
+ return _global_rng.standard_normal(args if args else None)
33
+
34
+
35
+ def randint(low, high=None, size=None, dtype=int):
36
+ return _global_rng.integers(low, high, size=size, dtype=dtype, endpoint=False)
37
+
38
+
39
+ def integers(low, high=None, size=None, dtype=np.int64, endpoint=False):
40
+ return _global_rng.integers(low, high, size=size, dtype=dtype, endpoint=endpoint)
41
+
42
+
43
+ def choice(a, size=None, replace=True, p=None):
44
+ return _global_rng.choice(a, size=size, replace=replace, p=p)
45
+
46
+
47
+ def shuffle(x):
48
+ return _global_rng.shuffle(x)
49
+
50
+
51
+ def permutation(x):
52
+ return _global_rng.permutation(x)
53
+
54
+
55
+ def normal(loc=0.0, scale=1.0, size=None):
56
+ return _global_rng.normal(loc, scale, size)
57
+
58
+
59
+ def uniform(low=0.0, high=1.0, size=None):
60
+ return _global_rng.uniform(low, high, size)
61
+
62
+
63
+ def exponential(scale=1.0, size=None):
64
+ return _global_rng.exponential(scale, size)
65
+
66
+
67
+ def poisson(lam=1.0, size=None):
68
+ return _global_rng.poisson(lam, size)
69
+
70
+
71
+ def binomial(n, p, size=None):
72
+ return _global_rng.binomial(n, p, size)
73
+
74
+
75
+ def multivariate_normal(mean, cov, size=None):
76
+ return _global_rng.multivariate_normal(mean, cov, size)
77
+
78
+
79
+ def beta(a, b, size=None):
80
+ return _global_rng.beta(a, b, size)
81
+
82
+
83
+ def gamma(shape, scale=1.0, size=None):
84
+ return _global_rng.gamma(shape, scale, size)
85
+
86
+
87
+ def multinomial(n, pvals, size=None):
88
+ return _global_rng.multinomial(n, pvals, size)
89
+
90
+
91
+ def standard_cauchy(size=None):
92
+ return _global_rng.standard_cauchy(size)
93
+
94
+
95
+ def laplace(loc=0.0, scale=1.0, size=None):
96
+ return _global_rng.laplace(loc, scale, size)
97
+
98
+
99
+ Generator = np.random.Generator