scalar-autograd 0.1.0
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.
- package/LICENSE +21 -0
- package/Losses.ts +144 -0
- package/Optimizers.ts +222 -0
- package/README.md +113 -0
- package/V.ts +0 -0
- package/Value.edge-cases.spec.ts +60 -0
- package/Value.grad-flow.spec.ts +24 -0
- package/Value.losses-edge-cases.spec.ts +32 -0
- package/Value.memory.spec.ts +25 -0
- package/Value.nn.spec.ts +109 -0
- package/Value.spec.ts +268 -0
- package/Value.ts +456 -0
- package/ValueActivation.ts +51 -0
- package/ValueArithmetic.ts +272 -0
- package/ValueComparison.ts +85 -0
- package/ValueTrig.ts +70 -0
- package/package.json +41 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { Value } from './Value';
|
|
2
|
+
|
|
3
|
+
export class ValueArithmetic {
|
|
4
|
+
static add(a: Value, b: Value): Value {
|
|
5
|
+
return Value.make(
|
|
6
|
+
a.data + b.data,
|
|
7
|
+
a, b,
|
|
8
|
+
(out) => () => {
|
|
9
|
+
if (a.requiresGrad) a.grad += 1 * out.grad;
|
|
10
|
+
if (b.requiresGrad) b.grad += 1 * out.grad;
|
|
11
|
+
},
|
|
12
|
+
`(${a.label}+${b.label})`
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
static sqrt(a: Value): Value {
|
|
16
|
+
if (a.data < 0) {
|
|
17
|
+
throw new Error(`Cannot take sqrt of negative number: ${a.data}`);
|
|
18
|
+
}
|
|
19
|
+
const root = Math.sqrt(a.data);
|
|
20
|
+
return Value.make(
|
|
21
|
+
root,
|
|
22
|
+
a, null,
|
|
23
|
+
(out) => () => {
|
|
24
|
+
if (a.requiresGrad) a.grad += 0.5 / root * out.grad;
|
|
25
|
+
},
|
|
26
|
+
`sqrt(${a.label})`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static mul(a: Value, b: Value): Value {
|
|
31
|
+
return Value.make(
|
|
32
|
+
a.data * b.data,
|
|
33
|
+
a, b,
|
|
34
|
+
(out) => () => {
|
|
35
|
+
if (a.requiresGrad) a.grad += b.data * out.grad;
|
|
36
|
+
if (b.requiresGrad) b.grad += a.data * out.grad;
|
|
37
|
+
},
|
|
38
|
+
`(${a.label}*${b.label})`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
static sub(a: Value, b: Value): Value {
|
|
43
|
+
return Value.make(
|
|
44
|
+
a.data - b.data,
|
|
45
|
+
a, b,
|
|
46
|
+
(out) => () => {
|
|
47
|
+
if (a.requiresGrad) a.grad += 1 * out.grad;
|
|
48
|
+
if (b.requiresGrad) b.grad -= 1 * out.grad;
|
|
49
|
+
},
|
|
50
|
+
`(${a.label}-${b.label})`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
static div(a: Value, b: Value, eps = 1e-12): Value {
|
|
55
|
+
if (Math.abs(b.data) < eps) {
|
|
56
|
+
throw new Error(`Division by zero or near-zero encountered in div: denominator=${b.data}`);
|
|
57
|
+
}
|
|
58
|
+
const safe = b.data;
|
|
59
|
+
return Value.make(
|
|
60
|
+
a.data / safe,
|
|
61
|
+
a, b,
|
|
62
|
+
(out) => () => {
|
|
63
|
+
if (a.requiresGrad) a.grad += (1 / safe) * out.grad;
|
|
64
|
+
if (b.requiresGrad) b.grad -= (a.data / (safe ** 2)) * out.grad;
|
|
65
|
+
},
|
|
66
|
+
`(${a.label}/${b.label})`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
static pow(a: Value, exp: number): Value {
|
|
71
|
+
if (typeof exp !== "number" || Number.isNaN(exp) || !Number.isFinite(exp)) {
|
|
72
|
+
throw new Error(`Exponent must be a finite number, got ${exp}`);
|
|
73
|
+
}
|
|
74
|
+
if (a.data < 0 && Math.abs(exp % 1) > 1e-12) {
|
|
75
|
+
throw new Error(`Cannot raise negative base (${a.data}) to non-integer exponent (${exp})`);
|
|
76
|
+
}
|
|
77
|
+
const safeBase = a.data;
|
|
78
|
+
return Value.make(
|
|
79
|
+
Math.pow(safeBase, exp),
|
|
80
|
+
a, null,
|
|
81
|
+
(out) => () => {
|
|
82
|
+
if (a.requiresGrad) a.grad += exp * Math.pow(safeBase, exp - 1) * out.grad;
|
|
83
|
+
},
|
|
84
|
+
`(${a.label}^${exp})`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
static powValue(a: Value, b: Value, eps = 1e-12): Value {
|
|
89
|
+
if (a.data < 0 && Math.abs(b.data % 1) > eps) {
|
|
90
|
+
throw new Error(`Cannot raise negative base (${a.data}) to non-integer exponent (${b.data})`);
|
|
91
|
+
}
|
|
92
|
+
if (a.data === 0 && b.data <= 0) {
|
|
93
|
+
throw new Error(`0 cannot be raised to zero or negative power: ${b.data}`);
|
|
94
|
+
}
|
|
95
|
+
const safeBase = a.data;
|
|
96
|
+
return Value.make(
|
|
97
|
+
Math.pow(safeBase, b.data),
|
|
98
|
+
a, b,
|
|
99
|
+
(out) => () => {
|
|
100
|
+
a.grad += b.data * Math.pow(safeBase, b.data - 1) * out.grad;
|
|
101
|
+
b.grad += Math.log(Math.max(eps, safeBase)) * Math.pow(safeBase, b.data) * out.grad;
|
|
102
|
+
},
|
|
103
|
+
`(${a.label}^${b.label})`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
static mod(a: Value, b: Value): Value {
|
|
108
|
+
if (typeof b.data !== 'number' || b.data === 0) {
|
|
109
|
+
throw new Error(`Modulo by zero encountered`);
|
|
110
|
+
}
|
|
111
|
+
return Value.make(
|
|
112
|
+
a.data % b.data,
|
|
113
|
+
a, b,
|
|
114
|
+
(out) => () => {
|
|
115
|
+
a.grad += 1 * out.grad;
|
|
116
|
+
// No grad to b (modulus not used in most diff cases)
|
|
117
|
+
},
|
|
118
|
+
`(${a.label}%${b.label})`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
static abs(a: Value): Value {
|
|
123
|
+
const d = Math.abs(a.data);
|
|
124
|
+
return Value.make(
|
|
125
|
+
d,
|
|
126
|
+
a, null,
|
|
127
|
+
(out) => () => {
|
|
128
|
+
if (a.requiresGrad) a.grad += (a.data >= 0 ? 1 : -1) * out.grad;
|
|
129
|
+
},
|
|
130
|
+
`abs(${a.label})`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
static exp(a: Value): Value {
|
|
135
|
+
const e = Math.exp(a.data);
|
|
136
|
+
return Value.make(
|
|
137
|
+
e,
|
|
138
|
+
a, null,
|
|
139
|
+
(out) => () => {
|
|
140
|
+
if (a.requiresGrad) a.grad += e * out.grad;
|
|
141
|
+
},
|
|
142
|
+
`exp(${a.label})`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
static log(a: Value, eps = 1e-12): Value {
|
|
147
|
+
if (a.data <= 0) {
|
|
148
|
+
throw new Error(`Logarithm undefined for non-positive value: ${a.data}`);
|
|
149
|
+
}
|
|
150
|
+
const safe = Math.max(a.data, eps);
|
|
151
|
+
const l = Math.log(safe);
|
|
152
|
+
return Value.make(
|
|
153
|
+
l,
|
|
154
|
+
a, null,
|
|
155
|
+
(out) => () => {
|
|
156
|
+
if (a.requiresGrad) a.grad += (1 / safe) * out.grad;
|
|
157
|
+
},
|
|
158
|
+
`log(${a.label})`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
static min(a: Value, b: Value): Value {
|
|
163
|
+
const d = Math.min(a.data, b.data);
|
|
164
|
+
return Value.make(
|
|
165
|
+
d,
|
|
166
|
+
a, b,
|
|
167
|
+
(out) => () => {
|
|
168
|
+
if (a.requiresGrad) a.grad += (a.data < b.data ? 1 : 0) * out.grad;
|
|
169
|
+
if (b.requiresGrad) b.grad += (b.data < a.data ? 1 : 0) * out.grad;
|
|
170
|
+
},
|
|
171
|
+
`min(${a.label},${b.label})`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
static max(a: Value, b: Value): Value {
|
|
176
|
+
const d = Math.max(a.data, b.data);
|
|
177
|
+
return Value.make(
|
|
178
|
+
d,
|
|
179
|
+
a, b,
|
|
180
|
+
(out) => () => {
|
|
181
|
+
if (a.requiresGrad) a.grad += (a.data > b.data ? 1 : 0) * out.grad;
|
|
182
|
+
if (b.requiresGrad) b.grad += (b.data > a.data ? 1 : 0) * out.grad;
|
|
183
|
+
},
|
|
184
|
+
`max(${a.label},${b.label})`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
static floor(a: Value): Value {
|
|
189
|
+
const fl = Math.floor(a.data);
|
|
190
|
+
return Value.make(
|
|
191
|
+
fl,
|
|
192
|
+
a, null,
|
|
193
|
+
() => () => {},
|
|
194
|
+
`floor(${a.label})`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
static ceil(a: Value): Value {
|
|
199
|
+
const cl = Math.ceil(a.data);
|
|
200
|
+
return Value.make(
|
|
201
|
+
cl,
|
|
202
|
+
a, null,
|
|
203
|
+
() => () => {},
|
|
204
|
+
`ceil(${a.label})`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
static round(a: Value): Value {
|
|
209
|
+
const rd = Math.round(a.data);
|
|
210
|
+
return Value.make(
|
|
211
|
+
rd,
|
|
212
|
+
a, null,
|
|
213
|
+
() => () => {},
|
|
214
|
+
`round(${a.label})`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
static square(a: Value): Value {
|
|
219
|
+
return ValueArithmetic.pow(a, 2);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
static cube(a: Value): Value {
|
|
223
|
+
return ValueArithmetic.pow(a, 3);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
static reciprocal(a: Value, eps = 1e-12): Value {
|
|
227
|
+
if (Math.abs(a.data) < eps) {
|
|
228
|
+
throw new Error(`Reciprocal of zero or near-zero detected`);
|
|
229
|
+
}
|
|
230
|
+
return Value.make(
|
|
231
|
+
1 / a.data,
|
|
232
|
+
a, null,
|
|
233
|
+
(out) => () => {
|
|
234
|
+
if (a.requiresGrad) a.grad += -1 / (a.data * a.data) * out.grad;
|
|
235
|
+
},
|
|
236
|
+
`reciprocal(${a.label})`
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
static clamp(a: Value, min: number, max: number): Value {
|
|
241
|
+
let val = Math.max(min, Math.min(a.data, max));
|
|
242
|
+
return Value.make(
|
|
243
|
+
val,
|
|
244
|
+
a, null,
|
|
245
|
+
(out) => () => {
|
|
246
|
+
a.grad += (a.data > min && a.data < max ? 1 : 0) * out.grad;
|
|
247
|
+
},
|
|
248
|
+
`clamp(${a.label},${min},${max})`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
static sum(vals: Value[]): Value {
|
|
253
|
+
if (!vals.length) return new Value(0);
|
|
254
|
+
return vals.reduce((a, b) => a.add(b));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
static mean(vals: Value[]): Value {
|
|
258
|
+
if (!vals.length) return new Value(0);
|
|
259
|
+
return ValueArithmetic.sum(vals).div(vals.length);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
static neg(a: Value): Value {
|
|
263
|
+
return Value.make(
|
|
264
|
+
-a.data,
|
|
265
|
+
a, null,
|
|
266
|
+
(out) => () => {
|
|
267
|
+
if (a.requiresGrad) a.grad -= out.grad;
|
|
268
|
+
},
|
|
269
|
+
`(-${a.label})`
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Value } from './Value';
|
|
2
|
+
|
|
3
|
+
export class ValueComparison {
|
|
4
|
+
static eq(a: Value, b: Value): Value {
|
|
5
|
+
return Value.make(
|
|
6
|
+
a.data === b.data ? 1 : 0,
|
|
7
|
+
a, b,
|
|
8
|
+
(out) => () => {
|
|
9
|
+
// No gradient - discrete operation
|
|
10
|
+
},
|
|
11
|
+
`(${a.label}==${b.label})`
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
static ifThenElse(cond: Value, thenVal: Value, elseVal: Value): Value {
|
|
16
|
+
return Value.make(
|
|
17
|
+
cond.data ? thenVal.data : elseVal.data,
|
|
18
|
+
cond,
|
|
19
|
+
cond.data ? thenVal : elseVal,
|
|
20
|
+
(out) => () => {
|
|
21
|
+
if (cond.data) {
|
|
22
|
+
thenVal.grad += out.grad;
|
|
23
|
+
} else {
|
|
24
|
+
elseVal.grad += out.grad;
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
`if(${cond.label}){${thenVal.label}}else{${elseVal.label}}`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static neq(a: Value, b: Value): Value {
|
|
32
|
+
return Value.make(
|
|
33
|
+
a.data !== b.data ? 1 : 0,
|
|
34
|
+
a, b,
|
|
35
|
+
(out) => () => {
|
|
36
|
+
// No gradient - discrete operation
|
|
37
|
+
},
|
|
38
|
+
`(${a.label}!=${b.label})`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
static gt(a: Value, b: Value): Value {
|
|
43
|
+
return Value.make(
|
|
44
|
+
a.data > b.data ? 1 : 0,
|
|
45
|
+
a, b,
|
|
46
|
+
(out) => () => {
|
|
47
|
+
// No gradient - discrete operation
|
|
48
|
+
},
|
|
49
|
+
`(${a.label}>${b.label})`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
static lt(a: Value, b: Value): Value {
|
|
54
|
+
return Value.make(
|
|
55
|
+
a.data < b.data ? 1 : 0,
|
|
56
|
+
a, b,
|
|
57
|
+
(out) => () => {
|
|
58
|
+
// No gradient - discrete operation
|
|
59
|
+
},
|
|
60
|
+
`(${a.label}<${b.label})`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
static gte(a: Value, b: Value): Value {
|
|
65
|
+
return Value.make(
|
|
66
|
+
a.data >= b.data ? 1 : 0,
|
|
67
|
+
a, b,
|
|
68
|
+
(out) => () => {
|
|
69
|
+
// No gradient - discrete operation
|
|
70
|
+
},
|
|
71
|
+
`(${a.label}>=${b.label})`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static lte(a: Value, b: Value): Value {
|
|
76
|
+
return Value.make(
|
|
77
|
+
a.data <= b.data ? 1 : 0,
|
|
78
|
+
a, b,
|
|
79
|
+
(out) => () => {
|
|
80
|
+
// No gradient - discrete operation
|
|
81
|
+
},
|
|
82
|
+
`(${a.label}<=${b.label})`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
package/ValueTrig.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Value } from './Value';
|
|
2
|
+
|
|
3
|
+
export class ValueTrig {
|
|
4
|
+
static sin(x: Value): Value {
|
|
5
|
+
const s = Math.sin(x.data);
|
|
6
|
+
return Value.make(
|
|
7
|
+
s,
|
|
8
|
+
x, null,
|
|
9
|
+
(out) => () => {
|
|
10
|
+
if (x.requiresGrad) x.grad += Math.cos(x.data) * out.grad;
|
|
11
|
+
},
|
|
12
|
+
`sin(${x.label})`
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
static cos(x: Value): Value {
|
|
16
|
+
const c = Math.cos(x.data);
|
|
17
|
+
return Value.make(
|
|
18
|
+
c,
|
|
19
|
+
x, null,
|
|
20
|
+
(out) => () => {
|
|
21
|
+
if (x.requiresGrad) x.grad += -Math.sin(x.data) * out.grad;
|
|
22
|
+
},
|
|
23
|
+
`cos(${x.label})`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
static tan(x: Value): Value {
|
|
27
|
+
const t = Math.tan(x.data);
|
|
28
|
+
return Value.make(
|
|
29
|
+
t,
|
|
30
|
+
x, null,
|
|
31
|
+
(out) => () => {
|
|
32
|
+
if (x.requiresGrad) x.grad += (1 / (Math.cos(x.data) ** 2)) * out.grad;
|
|
33
|
+
},
|
|
34
|
+
`tan(${x.label})`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
static asin(x: Value): Value {
|
|
38
|
+
const s = Math.asin(x.data);
|
|
39
|
+
return Value.make(
|
|
40
|
+
s,
|
|
41
|
+
x, null,
|
|
42
|
+
(out) => () => {
|
|
43
|
+
if (x.requiresGrad) x.grad += (1 / Math.sqrt(1 - x.data * x.data)) * out.grad;
|
|
44
|
+
},
|
|
45
|
+
`asin(${x.label})`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
static acos(x: Value): Value {
|
|
49
|
+
const c = Math.acos(x.data);
|
|
50
|
+
return Value.make(
|
|
51
|
+
c,
|
|
52
|
+
x, null,
|
|
53
|
+
(out) => () => {
|
|
54
|
+
if (x.requiresGrad) x.grad += (-1 / Math.sqrt(1 - x.data * x.data)) * out.grad;
|
|
55
|
+
},
|
|
56
|
+
`acos(${x.label})`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
static atan(x: Value): Value {
|
|
60
|
+
const a = Math.atan(x.data);
|
|
61
|
+
return Value.make(
|
|
62
|
+
a,
|
|
63
|
+
x, null,
|
|
64
|
+
(out) => () => {
|
|
65
|
+
if (x.requiresGrad) x.grad += (1 / (1 + x.data * x.data)) * out.grad;
|
|
66
|
+
},
|
|
67
|
+
`atan(${x.label})`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "scalar-autograd",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Scalar-based reverse-mode automatic differentiation in TypeScript.",
|
|
5
|
+
"main": "Value.js",
|
|
6
|
+
"types": "Value.d.ts",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/mfagerlund/ScalarAutograd.git"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"autograd",
|
|
13
|
+
"reverse-mode",
|
|
14
|
+
"automatic-differentiation",
|
|
15
|
+
"scalar",
|
|
16
|
+
"typescript"
|
|
17
|
+
],
|
|
18
|
+
"author": "Mattias Fagerlund <mattias.fagerlund@carretera.se>",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"files": [
|
|
21
|
+
"Value.js",
|
|
22
|
+
"Value.d.ts",
|
|
23
|
+
"V.ts",
|
|
24
|
+
"Value*.js",
|
|
25
|
+
"Value*.ts",
|
|
26
|
+
"Losses.js",
|
|
27
|
+
"Losses.ts",
|
|
28
|
+
"Optimizers.js",
|
|
29
|
+
"Optimizers.ts"
|
|
30
|
+
],
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"ts-node": "^10.0.0",
|
|
33
|
+
"typedoc": "^0.28.5",
|
|
34
|
+
"typescript": "^5.8.3",
|
|
35
|
+
"vitest": "^3.1.4"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsc",
|
|
39
|
+
"test": "vitest run --globals"
|
|
40
|
+
}
|
|
41
|
+
}
|