adjointly 0.0.0-stage → 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/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +345 -2
- package/dist/index.d.ts +868 -0
- package/dist/index.js +1390 -0
- package/dist/index.js.map +1 -0
- package/docs/README.md +20 -0
- package/docs/api.md +165 -0
- package/docs/concepts.md +143 -0
- package/docs/pitfalls.md +133 -0
- package/docs/recipes.md +151 -0
- package/llms.txt +36 -0
- package/package.json +91 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,1390 @@
|
|
|
1
|
+
// src/primitives.ts
|
|
2
|
+
function unary(name, value, deriv) {
|
|
3
|
+
return Object.freeze({
|
|
4
|
+
name,
|
|
5
|
+
value,
|
|
6
|
+
deriv
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
function binary(name, value, da, db) {
|
|
10
|
+
return Object.freeze({
|
|
11
|
+
name,
|
|
12
|
+
value,
|
|
13
|
+
da,
|
|
14
|
+
db
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
/** Whether `a` or `b` is `NaN`: the one case where neither `a <= b` nor `a > b` holds. */
|
|
18
|
+
function eitherNaN(a, b) {
|
|
19
|
+
return Number.isNaN(a) || Number.isNaN(b);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Logistic sigmoid `1 / (1 + e^-x)`, evaluated without overflow for any `x`: `exp` is only ever
|
|
23
|
+
* called on `-|x|`.
|
|
24
|
+
*/
|
|
25
|
+
function sigmoidValue(x) {
|
|
26
|
+
if (x >= 0) return 1 / (1 + Math.exp(-x));
|
|
27
|
+
const e = Math.exp(x);
|
|
28
|
+
return e / (1 + e);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* `sigmoid'(x) = sigmoid(x) * sigmoid(-x) = e / (1 + e)^2` with `e = exp(-|x|)`. Computed from `x`
|
|
32
|
+
* rather than as `y * (1 - y)`, which loses all precision for large positive `x` where `y` rounds
|
|
33
|
+
* to 1.
|
|
34
|
+
*/
|
|
35
|
+
function sigmoidDeriv(x) {
|
|
36
|
+
const e = Math.exp(-Math.abs(x));
|
|
37
|
+
const s = 1 + e;
|
|
38
|
+
return e / (s * s);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Every primitive, keyed by name. Subgradient conventions at points where the derivative does not
|
|
42
|
+
* exist are part of the contract:
|
|
43
|
+
*
|
|
44
|
+
* | primitive | convention |
|
|
45
|
+
* |---|---|
|
|
46
|
+
* | `abs` | derivative at `0` is `0` |
|
|
47
|
+
* | `max`, `min` | on a tie (`a === b`) the whole derivative goes to the first argument `a` |
|
|
48
|
+
* | `pow` | `d/db` is `0` wherever `a <= 0`; `d/da` is `0` wherever `b === 0` |
|
|
49
|
+
* | `sqrt` | derivative at `0` (and `-0`) is `+Infinity` (the true one-sided limit) |
|
|
50
|
+
* | `max`, `min` | if either argument is `NaN`, the value and both partials are `NaN`, as for `abs` |
|
|
51
|
+
*
|
|
52
|
+
* Everywhere else the partials are the ordinary analytic derivatives, and outside a function's
|
|
53
|
+
* domain (`log` of a negative, `sqrt` of a negative) the value and partials are whatever IEEE
|
|
54
|
+
* arithmetic produces, usually `NaN`.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```ts
|
|
58
|
+
* import { primitives } from 'adjointly';
|
|
59
|
+
* const x = 0.5;
|
|
60
|
+
* const y = primitives.tanh.value(x); //=> tanh(0.5)
|
|
61
|
+
* const dy = primitives.tanh.deriv(x, y); //=> 1 - tanh(0.5) ** 2
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
const primitives = Object.freeze({
|
|
65
|
+
/** `-x`. */
|
|
66
|
+
neg: unary("neg", (x) => -x, () => -1),
|
|
67
|
+
/** `a + b`. */
|
|
68
|
+
add: binary("add", (a, b) => a + b, () => 1, () => 1),
|
|
69
|
+
/** `a - b`. */
|
|
70
|
+
sub: binary("sub", (a, b) => a - b, () => 1, () => -1),
|
|
71
|
+
/** `a * b`. */
|
|
72
|
+
mul: binary("mul", (a, b) => a * b, (_a, b) => b, (a) => a),
|
|
73
|
+
/** `a / b`. */
|
|
74
|
+
div: binary("div", (a, b) => a / b, (_a, b) => 1 / b, (_a, b, y) => -y / b),
|
|
75
|
+
/** `1 / x`. */
|
|
76
|
+
recip: unary("recip", (x) => 1 / x, (_x, y) => -y * y),
|
|
77
|
+
/** `x * x`. */
|
|
78
|
+
square: unary("square", (x) => x * x, (x) => 2 * x),
|
|
79
|
+
/** `sqrt(x)`; derivative `+Infinity` at `0` and at `-0`. */
|
|
80
|
+
sqrt: unary("sqrt", (x) => Math.sqrt(x), (_x, y) => .5 / (y + 0)),
|
|
81
|
+
/** `e^x`. */
|
|
82
|
+
exp: unary("exp", (x) => Math.exp(x), (_x, y) => y),
|
|
83
|
+
/** `e^x - 1`, accurate near `0`. */
|
|
84
|
+
expm1: unary("expm1", (x) => Math.expm1(x), (x) => Math.exp(x)),
|
|
85
|
+
/** Natural logarithm. */
|
|
86
|
+
log: unary("log", (x) => Math.log(x), (x) => 1 / x),
|
|
87
|
+
/** `log(1 + x)`, accurate near `0`. */
|
|
88
|
+
log1p: unary("log1p", (x) => Math.log1p(x), (x) => 1 / (1 + x)),
|
|
89
|
+
/**
|
|
90
|
+
* `a ** b`, differentiable in both. `d/da = b * a^(b-1)`, taken as `0` when `b === 0` (so
|
|
91
|
+
* `0^0` does not produce `0 * Infinity`). `d/db = a^b * log(a)` for `a > 0` and `0` for
|
|
92
|
+
* `a <= 0`, where the real power is not differentiable in the exponent.
|
|
93
|
+
*/
|
|
94
|
+
pow: binary("pow", (a, b) => Math.pow(a, b), (a, b) => b === 0 ? 0 : b * Math.pow(a, b - 1), (a, _b, y) => a > 0 ? y * Math.log(a) : 0),
|
|
95
|
+
/** `|x|`; derivative `0` at `0`. */
|
|
96
|
+
abs: unary("abs", (x) => Math.abs(x), (x) => x > 0 ? 1 : x < 0 ? -1 : x === 0 ? 0 : NaN),
|
|
97
|
+
/**
|
|
98
|
+
* The smaller of `a` and `b`; on a tie the derivative goes to `a`. If either is `NaN`, the value
|
|
99
|
+
* and both partials are `NaN`.
|
|
100
|
+
*/
|
|
101
|
+
min: binary("min", (a, b) => Math.min(a, b), (a, b) => a <= b ? 1 : eitherNaN(a, b) ? NaN : 0, (a, b) => a <= b ? 0 : eitherNaN(a, b) ? NaN : 1),
|
|
102
|
+
/**
|
|
103
|
+
* The larger of `a` and `b`; on a tie the derivative goes to `a`. If either is `NaN`, the value
|
|
104
|
+
* and both partials are `NaN`.
|
|
105
|
+
*/
|
|
106
|
+
max: binary("max", (a, b) => Math.max(a, b), (a, b) => a >= b ? 1 : eitherNaN(a, b) ? NaN : 0, (a, b) => a >= b ? 0 : eitherNaN(a, b) ? NaN : 1),
|
|
107
|
+
/** Sine. */
|
|
108
|
+
sin: unary("sin", (x) => Math.sin(x), (x) => Math.cos(x)),
|
|
109
|
+
/** Cosine. */
|
|
110
|
+
cos: unary("cos", (x) => Math.cos(x), (x) => -Math.sin(x)),
|
|
111
|
+
/** Hyperbolic tangent. */
|
|
112
|
+
tanh: unary("tanh", (x) => Math.tanh(x), (x) => {
|
|
113
|
+
const e = Math.exp(-2 * Math.abs(x));
|
|
114
|
+
const s = 1 + e;
|
|
115
|
+
return 4 * e / (s * s);
|
|
116
|
+
}),
|
|
117
|
+
/** Logistic sigmoid `1 / (1 + e^-x)`, stable for any `x`. */
|
|
118
|
+
sigmoid: unary("sigmoid", sigmoidValue, sigmoidDeriv),
|
|
119
|
+
/**
|
|
120
|
+
* Softplus `log(1 + e^x)`, stable for any `x`: computed as `x + log1p(e^-x)` for positive `x`
|
|
121
|
+
* and `log1p(e^x)` otherwise, so it neither overflows nor rounds to `0` for very negative `x`.
|
|
122
|
+
* Its derivative is `sigmoid(x)`.
|
|
123
|
+
*/
|
|
124
|
+
softplus: unary("softplus", (x) => x > 0 ? x + Math.log1p(Math.exp(-x)) : Math.log1p(Math.exp(x)), (x) => sigmoidValue(x))
|
|
125
|
+
});
|
|
126
|
+
/** The names of the one-argument primitives, in table order. */
|
|
127
|
+
const unaryNames = [
|
|
128
|
+
"neg",
|
|
129
|
+
"recip",
|
|
130
|
+
"square",
|
|
131
|
+
"sqrt",
|
|
132
|
+
"exp",
|
|
133
|
+
"expm1",
|
|
134
|
+
"log",
|
|
135
|
+
"log1p",
|
|
136
|
+
"abs",
|
|
137
|
+
"sin",
|
|
138
|
+
"cos",
|
|
139
|
+
"tanh",
|
|
140
|
+
"sigmoid",
|
|
141
|
+
"softplus"
|
|
142
|
+
];
|
|
143
|
+
/** The names of the two-argument primitives, in table order. */
|
|
144
|
+
const binaryNames = [
|
|
145
|
+
"add",
|
|
146
|
+
"sub",
|
|
147
|
+
"mul",
|
|
148
|
+
"div",
|
|
149
|
+
"pow",
|
|
150
|
+
"min",
|
|
151
|
+
"max"
|
|
152
|
+
];
|
|
153
|
+
// src/arithmetic.ts
|
|
154
|
+
/**
|
|
155
|
+
* The operations with a plain-number operand, each the binary primitive `of` with one operand a
|
|
156
|
+
* constant: the first one when `constantFirst`, otherwise the second. Their values and partials
|
|
157
|
+
* are that primitive's, from the table.
|
|
158
|
+
*/
|
|
159
|
+
const constantOperandOps = {
|
|
160
|
+
addc: {
|
|
161
|
+
of: "add",
|
|
162
|
+
constantFirst: false
|
|
163
|
+
},
|
|
164
|
+
subc: {
|
|
165
|
+
of: "sub",
|
|
166
|
+
constantFirst: false
|
|
167
|
+
},
|
|
168
|
+
csub: {
|
|
169
|
+
of: "sub",
|
|
170
|
+
constantFirst: true
|
|
171
|
+
},
|
|
172
|
+
mulc: {
|
|
173
|
+
of: "mul",
|
|
174
|
+
constantFirst: false
|
|
175
|
+
},
|
|
176
|
+
divc: {
|
|
177
|
+
of: "div",
|
|
178
|
+
constantFirst: false
|
|
179
|
+
},
|
|
180
|
+
cdiv: {
|
|
181
|
+
of: "div",
|
|
182
|
+
constantFirst: true
|
|
183
|
+
},
|
|
184
|
+
powc: {
|
|
185
|
+
of: "pow",
|
|
186
|
+
constantFirst: false
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* {@link Arithmetic} on ordinary numbers: computes values only, with nothing recorded. Use it to
|
|
191
|
+
* evaluate a differentiable function at full speed when no derivative is needed.
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* ```ts
|
|
195
|
+
* import { plain } from 'adjointly';
|
|
196
|
+
* plain.add(plain.constant(1), plain.exp(0)); //=> 2
|
|
197
|
+
* ```
|
|
198
|
+
*/
|
|
199
|
+
const plain = {
|
|
200
|
+
constant: (x) => x,
|
|
201
|
+
value: (a) => a,
|
|
202
|
+
neg: primitives.neg.value,
|
|
203
|
+
add: primitives.add.value,
|
|
204
|
+
sub: primitives.sub.value,
|
|
205
|
+
mul: primitives.mul.value,
|
|
206
|
+
div: primitives.div.value,
|
|
207
|
+
recip: primitives.recip.value,
|
|
208
|
+
square: primitives.square.value,
|
|
209
|
+
sqrt: primitives.sqrt.value,
|
|
210
|
+
exp: primitives.exp.value,
|
|
211
|
+
expm1: primitives.expm1.value,
|
|
212
|
+
log: primitives.log.value,
|
|
213
|
+
log1p: primitives.log1p.value,
|
|
214
|
+
pow: primitives.pow.value,
|
|
215
|
+
abs: primitives.abs.value,
|
|
216
|
+
min: primitives.min.value,
|
|
217
|
+
max: primitives.max.value,
|
|
218
|
+
sin: primitives.sin.value,
|
|
219
|
+
cos: primitives.cos.value,
|
|
220
|
+
tanh: primitives.tanh.value,
|
|
221
|
+
sigmoid: primitives.sigmoid.value,
|
|
222
|
+
softplus: primitives.softplus.value,
|
|
223
|
+
addc: primitives.add.value,
|
|
224
|
+
subc: primitives.sub.value,
|
|
225
|
+
csub: primitives.sub.value,
|
|
226
|
+
mulc: primitives.mul.value,
|
|
227
|
+
divc: primitives.div.value,
|
|
228
|
+
cdiv: primitives.div.value,
|
|
229
|
+
powc: primitives.pow.value
|
|
230
|
+
};
|
|
231
|
+
// src/capacity.ts
|
|
232
|
+
/** `capacity` if it is a positive integer, the default if it is not given; otherwise throws. */
|
|
233
|
+
function initialCapacity(capacity, owner) {
|
|
234
|
+
if (capacity === void 0) return 1024;
|
|
235
|
+
if (!Number.isInteger(capacity) || capacity < 1) throw new RangeError(`${owner}: capacity must be a positive integer, not ${String(capacity)}`);
|
|
236
|
+
return capacity;
|
|
237
|
+
}
|
|
238
|
+
// src/debug.ts
|
|
239
|
+
/** Indices fit in 31 bits (parents are stored in an Int32Array). */
|
|
240
|
+
const STRIDE = 2 ** 31;
|
|
241
|
+
/** Epochs run from 1 to EPOCHS - 1, so that a handle stays below 2^52, an exact integer. */
|
|
242
|
+
const EPOCHS = 2 ** 21;
|
|
243
|
+
let lastEpoch = 0;
|
|
244
|
+
/**
|
|
245
|
+
* A fresh epoch, unique among all debug tapes and Forwards until 2^21 - 1 epochs have been used,
|
|
246
|
+
* after which numbering wraps around; a stale handle could then be mistaken for a live one only if
|
|
247
|
+
* it were used exactly that many clears later, at the same index.
|
|
248
|
+
*/
|
|
249
|
+
function nextEpoch() {
|
|
250
|
+
lastEpoch = lastEpoch % (EPOCHS - 1) + 1;
|
|
251
|
+
return lastEpoch;
|
|
252
|
+
}
|
|
253
|
+
/** The handles of one debug Tape or Forward: issuing them and checking them. */
|
|
254
|
+
var Handles = class {
|
|
255
|
+
epoch = 0;
|
|
256
|
+
/** Every epoch this instance has used, to tell its own stale handles from another's. */
|
|
257
|
+
own = /* @__PURE__ */ new Set();
|
|
258
|
+
/** The epoch each slot was last written in. */
|
|
259
|
+
slotEpoch = /* @__PURE__ */ new Int32Array(64);
|
|
260
|
+
kind;
|
|
261
|
+
first;
|
|
262
|
+
target;
|
|
263
|
+
constructor(kind, first, target) {
|
|
264
|
+
this.kind = kind;
|
|
265
|
+
this.first = first;
|
|
266
|
+
this.target = target;
|
|
267
|
+
this.begin();
|
|
268
|
+
}
|
|
269
|
+
/** Start a new epoch: handles issued from now on are told apart from earlier ones. */
|
|
270
|
+
begin() {
|
|
271
|
+
this.epoch = nextEpoch();
|
|
272
|
+
this.own.add(this.epoch);
|
|
273
|
+
}
|
|
274
|
+
/** The handle for the slot `index` just written. */
|
|
275
|
+
issue(index) {
|
|
276
|
+
while (index >= this.slotEpoch.length) {
|
|
277
|
+
const grown = new Int32Array(this.slotEpoch.length * 2);
|
|
278
|
+
grown.set(this.slotEpoch);
|
|
279
|
+
this.slotEpoch = grown;
|
|
280
|
+
}
|
|
281
|
+
this.slotEpoch[index] = this.epoch;
|
|
282
|
+
return this.epoch * STRIDE + index;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Throw if `value`, given where a plain number belongs, is one of this instance's variables: what
|
|
286
|
+
* a variable mistaken for a number looks like, typically after a `typeof x === 'number'` test,
|
|
287
|
+
* which every variable passes. Only this instance's handles are recognised, so that no ordinary
|
|
288
|
+
* number is ever refused: a handle is a whole number of 2^31 or more, encoding an epoch of this
|
|
289
|
+
* instance and a slot written in that epoch.
|
|
290
|
+
*/
|
|
291
|
+
checkNumber(value, where) {
|
|
292
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value)) return;
|
|
293
|
+
const epoch = Math.floor(value / STRIDE);
|
|
294
|
+
const index = value - epoch * STRIDE;
|
|
295
|
+
if (this.own.has(epoch) && this.slotEpoch[index] === epoch) throw new RangeError(`${where} is a variable, where a plain number belongs. A variable is a number at run time, so typeof x === 'number' is true for it too: keep variables and numbers apart instead.`);
|
|
296
|
+
}
|
|
297
|
+
/** The slot index of `handle`, or a `RangeError` saying what is wrong with it. */
|
|
298
|
+
resolve(handle, where) {
|
|
299
|
+
if (typeof handle !== "number" || !Number.isSafeInteger(handle) || handle < 0) throw new RangeError(`${where} is not a variable (it is ${String(handle)})`);
|
|
300
|
+
const epoch = Math.floor(handle / STRIDE);
|
|
301
|
+
const index = handle - epoch * STRIDE;
|
|
302
|
+
if (epoch === 0) throw new RangeError(`${where} is the plain number ${String(handle)}, not a variable of this ${this.kind}. Numbers enter through constant() or input(), or as the number operand of addc(), mulc() and the like. Do not tell variables from numbers with typeof: both are numbers.`);
|
|
303
|
+
if (!this.own.has(epoch)) throw new RangeError(`${where} is a variable of another Tape or Forward`);
|
|
304
|
+
if (index >= this.first + this.target.size || this.slotEpoch[index] !== epoch) throw new RangeError(`${where} is stale: its node was discarded by clear() or rewind()`);
|
|
305
|
+
return index;
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
const arithmetic = {
|
|
309
|
+
constant: [["n"], "v"],
|
|
310
|
+
value: [["v"], "-"]
|
|
311
|
+
};
|
|
312
|
+
for (const name of unaryNames) arithmetic[name] = [["v"], "v"];
|
|
313
|
+
for (const name of binaryNames) arithmetic[name] = [["v", "v"], "v"];
|
|
314
|
+
for (const [name, { constantFirst }] of Object.entries(constantOperandOps)) arithmetic[name] = [constantFirst ? ["n", "v"] : ["v", "n"], "v"];
|
|
315
|
+
const signatures = {
|
|
316
|
+
Tape: {
|
|
317
|
+
...arithmetic,
|
|
318
|
+
input: [["n"], "v"],
|
|
319
|
+
inputs: [["ns"], "vs"],
|
|
320
|
+
backward: [["v", "n"], "-"],
|
|
321
|
+
vjp: [["vs", "-"], "-"],
|
|
322
|
+
adjoint: [["v"], "-"],
|
|
323
|
+
adjoints: [["vs", "-"], "-"],
|
|
324
|
+
gradient: [[
|
|
325
|
+
"v",
|
|
326
|
+
"vs",
|
|
327
|
+
"-"
|
|
328
|
+
], "-"]
|
|
329
|
+
},
|
|
330
|
+
Forward: {
|
|
331
|
+
...arithmetic,
|
|
332
|
+
input: [["n", "n"], "v"],
|
|
333
|
+
tangent: [["v"], "-"]
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
/**
|
|
337
|
+
* Turn `target`, a freshly constructed Tape or Forward, into its debug version. `first` is the
|
|
338
|
+
* index of its first slot (1 for a Tape, whose slot 0 is reserved; 0 for a Forward).
|
|
339
|
+
*/
|
|
340
|
+
function installChecks(target, kind, first) {
|
|
341
|
+
const handles = new Handles(kind, first, target);
|
|
342
|
+
const self = target;
|
|
343
|
+
const proto = Object.getPrototypeOf(target);
|
|
344
|
+
const element = (where, k) => `${where}, element ${String(k + 1)}`;
|
|
345
|
+
const decode = (arg, value, where) => {
|
|
346
|
+
switch (arg) {
|
|
347
|
+
case "v": return handles.resolve(value, where);
|
|
348
|
+
case "vs": return value.map((h, k) => handles.resolve(h, element(where, k)));
|
|
349
|
+
case "n":
|
|
350
|
+
handles.checkNumber(value, where);
|
|
351
|
+
return value;
|
|
352
|
+
case "ns":
|
|
353
|
+
Array.from(value, (x, k) => {
|
|
354
|
+
handles.checkNumber(x, element(where, k));
|
|
355
|
+
});
|
|
356
|
+
return value;
|
|
357
|
+
case "-": return value;
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
const encode = (arg, value) => arg === "v" ? handles.issue(value) : arg === "vs" ? value.map((i) => handles.issue(i)) : value;
|
|
361
|
+
for (const [name, [args, result]] of Object.entries(signatures[kind])) {
|
|
362
|
+
const run = name === "gradient" ? (output, inputs, out) => {
|
|
363
|
+
proto.backward.call(target, output);
|
|
364
|
+
return proto.adjoints.call(target, inputs, out);
|
|
365
|
+
} : (...decoded) => proto[name].apply(target, decoded);
|
|
366
|
+
self[name] = (...params) => {
|
|
367
|
+
const decoded = params.map((p, i) => decode(args[i] ?? "-", p, `${kind}.${name}: argument ${String(i + 1)}`));
|
|
368
|
+
return encode(result, run(...decoded));
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
self.clear = () => {
|
|
372
|
+
proto.clear.call(target);
|
|
373
|
+
handles.begin();
|
|
374
|
+
};
|
|
375
|
+
if (kind === "Tape") self.rewind = (mark) => {
|
|
376
|
+
proto.rewind.call(target, mark);
|
|
377
|
+
handles.begin();
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
// src/tape.ts
|
|
381
|
+
/**
|
|
382
|
+
* Slot 0 of every array is a sink, not a node: leaves name it as both parents, and unary nodes as
|
|
383
|
+
* their second, always with a zero partial. Every parent index is therefore a valid slot, and the
|
|
384
|
+
* backward sweep's zero rule never writes to the sink.
|
|
385
|
+
*/
|
|
386
|
+
const SINK = 0;
|
|
387
|
+
/**
|
|
388
|
+
* Reverse-mode automatic differentiation: an {@link Arithmetic} over {@link Var} that records
|
|
389
|
+
* every operation as a node on a tape, then computes the derivative of one output with respect to
|
|
390
|
+
* every node in a single backward sweep.
|
|
391
|
+
*
|
|
392
|
+
* Each node stores its value, up to two parent indices and the local partial derivatives with
|
|
393
|
+
* respect to them, computed when the node is recorded, and its adjoint: six typed arrays, 40
|
|
394
|
+
* bytes a node. They double when full, so recording does not allocate once the tape has grown to
|
|
395
|
+
* the size of the computation, and a tape reused across evaluations (via {@link Tape.clear} or
|
|
396
|
+
* {@link Tape.rewind}) never allocates again.
|
|
397
|
+
*
|
|
398
|
+
* The backward sweep visits nodes from the output down to the first, adding
|
|
399
|
+
* `partial * adjoint(child)` into each parent's adjoint, under the zero rule: if the partial or
|
|
400
|
+
* the adjoint is exactly 0 the term is 0, even when the other is infinite or `NaN`, so an unused
|
|
401
|
+
* `sqrt(0)` or `log(0)` does not poison the gradient.
|
|
402
|
+
*
|
|
403
|
+
* @example
|
|
404
|
+
* ```ts
|
|
405
|
+
* import { Tape } from 'adjointly';
|
|
406
|
+
* const t = new Tape();
|
|
407
|
+
* const x = t.input(2);
|
|
408
|
+
* const y = t.input(3);
|
|
409
|
+
* const f = t.mul(x, t.sin(y)); // x * sin(y)
|
|
410
|
+
* t.backward(f);
|
|
411
|
+
* t.adjoint(x); //=> sin(3)
|
|
412
|
+
* t.adjoint(y); //=> 2 * cos(3)
|
|
413
|
+
* ```
|
|
414
|
+
*/
|
|
415
|
+
var Tape = class {
|
|
416
|
+
val;
|
|
417
|
+
pa;
|
|
418
|
+
pb;
|
|
419
|
+
da;
|
|
420
|
+
db;
|
|
421
|
+
adj;
|
|
422
|
+
/** The next free slot; nodes occupy slots 1 to n - 1. */
|
|
423
|
+
n = 1;
|
|
424
|
+
/** An empty tape; see {@link TapeOptions}. */
|
|
425
|
+
constructor(options = {}) {
|
|
426
|
+
const cap = initialCapacity(options.capacity, "Tape") + 1;
|
|
427
|
+
this.val = new Float64Array(cap);
|
|
428
|
+
this.pa = new Int32Array(cap);
|
|
429
|
+
this.pb = new Int32Array(cap);
|
|
430
|
+
this.da = new Float64Array(cap);
|
|
431
|
+
this.db = new Float64Array(cap);
|
|
432
|
+
this.adj = new Float64Array(cap);
|
|
433
|
+
if (options.debug === true) installChecks(this, "Tape", 1);
|
|
434
|
+
}
|
|
435
|
+
/** The number of nodes recorded so far. */
|
|
436
|
+
get size() {
|
|
437
|
+
return this.n - 1;
|
|
438
|
+
}
|
|
439
|
+
/** The number of nodes the tape can hold before it next grows. */
|
|
440
|
+
get capacity() {
|
|
441
|
+
return this.val.length - 1;
|
|
442
|
+
}
|
|
443
|
+
/** Forget every node, keeping the storage. Every `Var` previously issued becomes invalid. */
|
|
444
|
+
clear() {
|
|
445
|
+
this.n = 1;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* A checkpoint at the current end of the tape. {@link Tape.rewind} to it discards everything
|
|
449
|
+
* recorded after it and keeps everything before, so a shared prefix (inputs, parameters,
|
|
450
|
+
* precomputed features) is recorded once and reused across many evaluations.
|
|
451
|
+
*
|
|
452
|
+
* @example
|
|
453
|
+
* ```ts
|
|
454
|
+
* const t = new Tape();
|
|
455
|
+
* const w = t.input(0.5);
|
|
456
|
+
* const m = t.mark();
|
|
457
|
+
* let grad = 0;
|
|
458
|
+
* for (const x of [1, 2, 3]) {
|
|
459
|
+
* t.rewind(m); // drop the previous sample's nodes, keep w
|
|
460
|
+
* const loss = t.square(t.subc(t.mulc(w, x), 1));
|
|
461
|
+
* t.backward(loss);
|
|
462
|
+
* grad += t.adjoint(w);
|
|
463
|
+
* }
|
|
464
|
+
* grad; //=> 2 * ((0.5 - 1) * 1 + (1 - 1) * 2 + (1.5 - 1) * 3)
|
|
465
|
+
* t.size; //=> 4
|
|
466
|
+
* ```
|
|
467
|
+
*/
|
|
468
|
+
mark() {
|
|
469
|
+
return this.n;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Truncate the tape back to `mark`. Vars created before the mark stay valid with their values;
|
|
473
|
+
* Vars created after it become invalid. Throws if `mark` is beyond the current end (the tape was
|
|
474
|
+
* already cleared or rewound further back).
|
|
475
|
+
*/
|
|
476
|
+
rewind(mark) {
|
|
477
|
+
if (mark > this.n || mark < 1) throw new RangeError(`Tape.rewind: mark ${mark} is outside the tape (size ${this.size})`);
|
|
478
|
+
this.n = mark;
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Record an independent variable with value `x`. Identical to {@link Tape.constant} on the tape
|
|
482
|
+
* (both are leaves and both accumulate an adjoint); the separate name says which leaves you
|
|
483
|
+
* intend to read a gradient for.
|
|
484
|
+
*/
|
|
485
|
+
input(x) {
|
|
486
|
+
return this.push(x, SINK, 0, SINK, 0);
|
|
487
|
+
}
|
|
488
|
+
inputs(xs) {
|
|
489
|
+
return Array.from(xs, (x) => this.push(x, SINK, 0, SINK, 0));
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Lift a number into `T` as a constant: its derivative with respect to every input is zero.
|
|
493
|
+
* On a `Tape` it still appends a node (constants are leaves of the tape).
|
|
494
|
+
*/
|
|
495
|
+
constant(x) {
|
|
496
|
+
return this.push(x, SINK, 0, SINK, 0);
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* The current numeric value of `a`. Use it to branch (`if (A.value(x) > 0)`), to log, or to
|
|
500
|
+
* return a result. The number it returns carries no derivative information: anything computed
|
|
501
|
+
* from it with ordinary arithmetic is a constant as far as differentiation is concerned.
|
|
502
|
+
*/
|
|
503
|
+
value(a) {
|
|
504
|
+
return this.val[a];
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Run one backward sweep from `output`, seeding its adjoint with `seed` (default 1). Afterwards
|
|
508
|
+
* {@link Tape.adjoint} of any node `v` is `seed * d(output)/d(v)`. Every call starts from zero
|
|
509
|
+
* adjoints, so call it once per output whose gradient you want; the recorded tape is not
|
|
510
|
+
* changed and can be swept any number of times.
|
|
511
|
+
*/
|
|
512
|
+
backward(output, seed = 1) {
|
|
513
|
+
if (output < 1 || output >= this.n) throw new RangeError(`Tape.backward: ${output} is not a node of this tape (size ${this.size})`);
|
|
514
|
+
const adj = this.adj;
|
|
515
|
+
adj.fill(0, 0, output + 1);
|
|
516
|
+
adj[output] = seed;
|
|
517
|
+
this.sweep(output);
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Vector-Jacobian product: one backward sweep seeded with `weights[k]` at `outputs[k]`.
|
|
521
|
+
* Afterwards {@link Tape.adjoint} of `v` is `sum_k weights[k] * d(outputs[k])/d(v)`. An output
|
|
522
|
+
* listed twice gets the sum of its weights; with no outputs every adjoint is zero.
|
|
523
|
+
*
|
|
524
|
+
* @example
|
|
525
|
+
* ```ts
|
|
526
|
+
* const t = new Tape();
|
|
527
|
+
* const x = t.input(1);
|
|
528
|
+
* const f = t.square(x); // df/dx = 2x
|
|
529
|
+
* const g = t.exp(x); // dg/dx = e^x
|
|
530
|
+
* t.vjp([f, g], [2, 3]); // gradient of 2f + 3g in one sweep
|
|
531
|
+
* t.adjoint(x); //=> 2 * 2 + 3 * E
|
|
532
|
+
* ```
|
|
533
|
+
*/
|
|
534
|
+
vjp(outputs, weights) {
|
|
535
|
+
if (outputs.length !== weights.length) throw new RangeError(`Tape.vjp: ${outputs.length} outputs but ${weights.length} weights`);
|
|
536
|
+
let top = 0;
|
|
537
|
+
for (const o of outputs) {
|
|
538
|
+
if (o < 1 || o >= this.n) throw new RangeError(`Tape.vjp: ${o} is not a node of this tape (size ${this.size})`);
|
|
539
|
+
top = Math.max(top, o);
|
|
540
|
+
}
|
|
541
|
+
const adj = this.adj;
|
|
542
|
+
if (outputs.length === 0) {
|
|
543
|
+
adj.fill(0);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
adj.fill(0, 0, top + 1);
|
|
547
|
+
outputs.forEach((o, k) => {
|
|
548
|
+
adj[o] = adj[o] + weights[k];
|
|
549
|
+
});
|
|
550
|
+
this.sweep(top);
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* The adjoint of `v` from the last {@link Tape.backward} or {@link Tape.vjp}: the derivative of
|
|
554
|
+
* the seeded output(s) with respect to `v`. Zero for a node the output does not depend on.
|
|
555
|
+
* Meaningless for a node recorded after the output of that sweep.
|
|
556
|
+
*/
|
|
557
|
+
adjoint(v) {
|
|
558
|
+
return this.adj[v];
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Read the adjoints of `vars` into `out` (allocated if not given) and return it: the gradient
|
|
562
|
+
* restricted to those variables, in their order.
|
|
563
|
+
*/
|
|
564
|
+
adjoints(vars, out = new Float64Array(vars.length)) {
|
|
565
|
+
const adj = this.adj;
|
|
566
|
+
vars.forEach((v, k) => {
|
|
567
|
+
out[k] = adj[v];
|
|
568
|
+
});
|
|
569
|
+
return out;
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* `backward(output)` then `adjoints(inputs, out)`: the gradient of `output` with respect to
|
|
573
|
+
* `inputs`, in their order.
|
|
574
|
+
*
|
|
575
|
+
* @example
|
|
576
|
+
* ```ts
|
|
577
|
+
* const t = new Tape();
|
|
578
|
+
* const [x, y] = t.inputs([1, 2]);
|
|
579
|
+
* t.gradient(t.mul(x, y), [x, y]); //=> [2, 1]
|
|
580
|
+
* ```
|
|
581
|
+
*/
|
|
582
|
+
gradient(output, inputs, out) {
|
|
583
|
+
this.backward(output);
|
|
584
|
+
return this.adjoints(inputs, out);
|
|
585
|
+
}
|
|
586
|
+
sweep(top) {
|
|
587
|
+
const adj = this.adj, pa = this.pa, pb = this.pb, da = this.da, db = this.db;
|
|
588
|
+
for (let i = top; i > SINK; i--) {
|
|
589
|
+
const g = adj[i];
|
|
590
|
+
if (g === 0) continue;
|
|
591
|
+
const dA = da[i];
|
|
592
|
+
if (dA !== 0) {
|
|
593
|
+
const a = pa[i];
|
|
594
|
+
adj[a] = adj[a] + dA * g;
|
|
595
|
+
}
|
|
596
|
+
const dB = db[i];
|
|
597
|
+
if (dB !== 0) {
|
|
598
|
+
const b = pb[i];
|
|
599
|
+
adj[b] = adj[b] + dB * g;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
push(y, a, dA, b, dB) {
|
|
604
|
+
const i = this.n;
|
|
605
|
+
if (i === this.val.length) this.grow();
|
|
606
|
+
this.val[i] = y;
|
|
607
|
+
this.pa[i] = a;
|
|
608
|
+
this.da[i] = dA;
|
|
609
|
+
this.pb[i] = b;
|
|
610
|
+
this.db[i] = dB;
|
|
611
|
+
this.n = i + 1;
|
|
612
|
+
return i;
|
|
613
|
+
}
|
|
614
|
+
grow() {
|
|
615
|
+
const cap = this.val.length * 2;
|
|
616
|
+
const val = new Float64Array(cap);
|
|
617
|
+
val.set(this.val);
|
|
618
|
+
this.val = val;
|
|
619
|
+
const pa = new Int32Array(cap);
|
|
620
|
+
pa.set(this.pa);
|
|
621
|
+
this.pa = pa;
|
|
622
|
+
const pb = new Int32Array(cap);
|
|
623
|
+
pb.set(this.pb);
|
|
624
|
+
this.pb = pb;
|
|
625
|
+
const da = new Float64Array(cap);
|
|
626
|
+
da.set(this.da);
|
|
627
|
+
this.da = da;
|
|
628
|
+
const db = new Float64Array(cap);
|
|
629
|
+
db.set(this.db);
|
|
630
|
+
this.db = db;
|
|
631
|
+
const adj = new Float64Array(cap);
|
|
632
|
+
adj.set(this.adj);
|
|
633
|
+
this.adj = adj;
|
|
634
|
+
}
|
|
635
|
+
/** `-a`. */
|
|
636
|
+
neg(a) {
|
|
637
|
+
const x = this.val[a];
|
|
638
|
+
const y = primitives.neg.value(x);
|
|
639
|
+
return this.push(y, a, primitives.neg.deriv(x, y), SINK, 0);
|
|
640
|
+
}
|
|
641
|
+
/** `a + b`. */
|
|
642
|
+
add(a, b) {
|
|
643
|
+
const x = this.val[a];
|
|
644
|
+
const z = this.val[b];
|
|
645
|
+
const y = primitives.add.value(x, z);
|
|
646
|
+
return this.push(y, a, primitives.add.da(x, z, y), b, primitives.add.db(x, z, y));
|
|
647
|
+
}
|
|
648
|
+
/** `a - b`. */
|
|
649
|
+
sub(a, b) {
|
|
650
|
+
const x = this.val[a];
|
|
651
|
+
const z = this.val[b];
|
|
652
|
+
const y = primitives.sub.value(x, z);
|
|
653
|
+
return this.push(y, a, primitives.sub.da(x, z, y), b, primitives.sub.db(x, z, y));
|
|
654
|
+
}
|
|
655
|
+
/** `a * b`. */
|
|
656
|
+
mul(a, b) {
|
|
657
|
+
const x = this.val[a];
|
|
658
|
+
const z = this.val[b];
|
|
659
|
+
const y = primitives.mul.value(x, z);
|
|
660
|
+
return this.push(y, a, primitives.mul.da(x, z, y), b, primitives.mul.db(x, z, y));
|
|
661
|
+
}
|
|
662
|
+
/** `a / b`. */
|
|
663
|
+
div(a, b) {
|
|
664
|
+
const x = this.val[a];
|
|
665
|
+
const z = this.val[b];
|
|
666
|
+
const y = primitives.div.value(x, z);
|
|
667
|
+
return this.push(y, a, primitives.div.da(x, z, y), b, primitives.div.db(x, z, y));
|
|
668
|
+
}
|
|
669
|
+
/** `1 / a`. */
|
|
670
|
+
recip(a) {
|
|
671
|
+
const x = this.val[a];
|
|
672
|
+
const y = primitives.recip.value(x);
|
|
673
|
+
return this.push(y, a, primitives.recip.deriv(x, y), SINK, 0);
|
|
674
|
+
}
|
|
675
|
+
/** `a * a`, one node instead of a `mul` of `a` with itself. */
|
|
676
|
+
square(a) {
|
|
677
|
+
const x = this.val[a];
|
|
678
|
+
const y = primitives.square.value(x);
|
|
679
|
+
return this.push(y, a, primitives.square.deriv(x, y), SINK, 0);
|
|
680
|
+
}
|
|
681
|
+
/** `sqrt(a)`; its derivative at `0` is `+Infinity`. */
|
|
682
|
+
sqrt(a) {
|
|
683
|
+
const x = this.val[a];
|
|
684
|
+
const y = primitives.sqrt.value(x);
|
|
685
|
+
return this.push(y, a, primitives.sqrt.deriv(x, y), SINK, 0);
|
|
686
|
+
}
|
|
687
|
+
/** `e^a`. */
|
|
688
|
+
exp(a) {
|
|
689
|
+
const x = this.val[a];
|
|
690
|
+
const y = primitives.exp.value(x);
|
|
691
|
+
return this.push(y, a, primitives.exp.deriv(x, y), SINK, 0);
|
|
692
|
+
}
|
|
693
|
+
/** `e^a - 1`, accurate for small `a`. */
|
|
694
|
+
expm1(a) {
|
|
695
|
+
const x = this.val[a];
|
|
696
|
+
const y = primitives.expm1.value(x);
|
|
697
|
+
return this.push(y, a, primitives.expm1.deriv(x, y), SINK, 0);
|
|
698
|
+
}
|
|
699
|
+
/** Natural logarithm. */
|
|
700
|
+
log(a) {
|
|
701
|
+
const x = this.val[a];
|
|
702
|
+
const y = primitives.log.value(x);
|
|
703
|
+
return this.push(y, a, primitives.log.deriv(x, y), SINK, 0);
|
|
704
|
+
}
|
|
705
|
+
/** `log(1 + a)`, accurate for small `a`. */
|
|
706
|
+
log1p(a) {
|
|
707
|
+
const x = this.val[a];
|
|
708
|
+
const y = primitives.log1p.value(x);
|
|
709
|
+
return this.push(y, a, primitives.log1p.deriv(x, y), SINK, 0);
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* `a ** b`, differentiable in both arguments. The derivative with respect to `b` is `0` wherever
|
|
713
|
+
* `a <= 0`; the derivative with respect to `a` is `0` wherever `b === 0`. For a fixed numeric
|
|
714
|
+
* exponent use {@link Arithmetic.powc}; for `b = 2` prefer {@link Arithmetic.square}, for
|
|
715
|
+
* `b = 0.5` prefer {@link Arithmetic.sqrt}.
|
|
716
|
+
*/
|
|
717
|
+
pow(a, b) {
|
|
718
|
+
const x = this.val[a];
|
|
719
|
+
const z = this.val[b];
|
|
720
|
+
const y = primitives.pow.value(x, z);
|
|
721
|
+
return this.push(y, a, primitives.pow.da(x, z, y), b, primitives.pow.db(x, z, y));
|
|
722
|
+
}
|
|
723
|
+
/** `|a|`; its derivative at `0` is `0`. */
|
|
724
|
+
abs(a) {
|
|
725
|
+
const x = this.val[a];
|
|
726
|
+
const y = primitives.abs.value(x);
|
|
727
|
+
return this.push(y, a, primitives.abs.deriv(x, y), SINK, 0);
|
|
728
|
+
}
|
|
729
|
+
/** The smaller of `a` and `b`; on a tie the derivative flows to `a`. */
|
|
730
|
+
min(a, b) {
|
|
731
|
+
const x = this.val[a];
|
|
732
|
+
const z = this.val[b];
|
|
733
|
+
const y = primitives.min.value(x, z);
|
|
734
|
+
return this.push(y, a, primitives.min.da(x, z, y), b, primitives.min.db(x, z, y));
|
|
735
|
+
}
|
|
736
|
+
/** The larger of `a` and `b`; on a tie the derivative flows to `a`. */
|
|
737
|
+
max(a, b) {
|
|
738
|
+
const x = this.val[a];
|
|
739
|
+
const z = this.val[b];
|
|
740
|
+
const y = primitives.max.value(x, z);
|
|
741
|
+
return this.push(y, a, primitives.max.da(x, z, y), b, primitives.max.db(x, z, y));
|
|
742
|
+
}
|
|
743
|
+
/** Sine. */
|
|
744
|
+
sin(a) {
|
|
745
|
+
const x = this.val[a];
|
|
746
|
+
const y = primitives.sin.value(x);
|
|
747
|
+
return this.push(y, a, primitives.sin.deriv(x, y), SINK, 0);
|
|
748
|
+
}
|
|
749
|
+
/** Cosine. */
|
|
750
|
+
cos(a) {
|
|
751
|
+
const x = this.val[a];
|
|
752
|
+
const y = primitives.cos.value(x);
|
|
753
|
+
return this.push(y, a, primitives.cos.deriv(x, y), SINK, 0);
|
|
754
|
+
}
|
|
755
|
+
/** Hyperbolic tangent. */
|
|
756
|
+
tanh(a) {
|
|
757
|
+
const x = this.val[a];
|
|
758
|
+
const y = primitives.tanh.value(x);
|
|
759
|
+
return this.push(y, a, primitives.tanh.deriv(x, y), SINK, 0);
|
|
760
|
+
}
|
|
761
|
+
/** Logistic sigmoid `1 / (1 + e^-a)`, stable for any `a`. */
|
|
762
|
+
sigmoid(a) {
|
|
763
|
+
const x = this.val[a];
|
|
764
|
+
const y = primitives.sigmoid.value(x);
|
|
765
|
+
return this.push(y, a, primitives.sigmoid.deriv(x, y), SINK, 0);
|
|
766
|
+
}
|
|
767
|
+
/** Softplus `log(1 + e^a)`, stable for any `a`; its derivative is `sigmoid(a)`. */
|
|
768
|
+
softplus(a) {
|
|
769
|
+
const x = this.val[a];
|
|
770
|
+
const y = primitives.softplus.value(x);
|
|
771
|
+
return this.push(y, a, primitives.softplus.deriv(x, y), SINK, 0);
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* `a + c` for a plain number `c`: `add(a, constant(c))` without the constant's tape node. Same
|
|
775
|
+
* value and derivative, bit for bit.
|
|
776
|
+
*/
|
|
777
|
+
addc(a, c) {
|
|
778
|
+
const x = this.val[a];
|
|
779
|
+
const y = primitives.add.value(x, c);
|
|
780
|
+
return this.push(y, a, primitives.add.da(x, c, y), SINK, 0);
|
|
781
|
+
}
|
|
782
|
+
/** `a - c` for a plain number `c`; `sub(a, constant(c))` in one node. */
|
|
783
|
+
subc(a, c) {
|
|
784
|
+
const x = this.val[a];
|
|
785
|
+
const y = primitives.sub.value(x, c);
|
|
786
|
+
return this.push(y, a, primitives.sub.da(x, c, y), SINK, 0);
|
|
787
|
+
}
|
|
788
|
+
/** `c - a` for a plain number `c`; `sub(constant(c), a)` in one node. */
|
|
789
|
+
csub(c, a) {
|
|
790
|
+
const x = this.val[a];
|
|
791
|
+
const y = primitives.sub.value(c, x);
|
|
792
|
+
return this.push(y, a, primitives.sub.db(c, x, y), SINK, 0);
|
|
793
|
+
}
|
|
794
|
+
/** `a * c` for a plain number `c`; `mul(a, constant(c))` in one node. */
|
|
795
|
+
mulc(a, c) {
|
|
796
|
+
const x = this.val[a];
|
|
797
|
+
const y = primitives.mul.value(x, c);
|
|
798
|
+
return this.push(y, a, primitives.mul.da(x, c, y), SINK, 0);
|
|
799
|
+
}
|
|
800
|
+
/** `a / c` for a plain number `c`; `div(a, constant(c))` in one node. */
|
|
801
|
+
divc(a, c) {
|
|
802
|
+
const x = this.val[a];
|
|
803
|
+
const y = primitives.div.value(x, c);
|
|
804
|
+
return this.push(y, a, primitives.div.da(x, c, y), SINK, 0);
|
|
805
|
+
}
|
|
806
|
+
/** `c / a` for a plain number `c`; `div(constant(c), a)` in one node. */
|
|
807
|
+
cdiv(c, a) {
|
|
808
|
+
const x = this.val[a];
|
|
809
|
+
const y = primitives.div.value(c, x);
|
|
810
|
+
return this.push(y, a, primitives.div.db(c, x, y), SINK, 0);
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* `a ** c` for a plain number `c`; `pow(a, constant(c))` in one node, with the same conventions
|
|
814
|
+
* (the derivative is `0` when `c === 0`).
|
|
815
|
+
*/
|
|
816
|
+
powc(a, c) {
|
|
817
|
+
const x = this.val[a];
|
|
818
|
+
const y = primitives.pow.value(x, c);
|
|
819
|
+
return this.push(y, a, primitives.pow.da(x, c, y), SINK, 0);
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
// src/forward.ts
|
|
823
|
+
/**
|
|
824
|
+
* One term of the chain rule, `d * t`, except that a zero factor makes the term zero even when the
|
|
825
|
+
* other factor is infinite or `NaN`. The tape's backward sweep applies the same rule.
|
|
826
|
+
*/
|
|
827
|
+
function times(d, t) {
|
|
828
|
+
return d === 0 || t === 0 ? 0 : d * t;
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Forward-mode automatic differentiation: an {@link Arithmetic} over {@link Dual} where every value
|
|
832
|
+
* carries a tangent, its derivative along one direction chosen when the inputs are created. One
|
|
833
|
+
* evaluation gives the directional derivative `grad f(x) . v` (a Jacobian-vector product) for every
|
|
834
|
+
* output at once, with no backward sweep and nothing kept but a value and a tangent per slot.
|
|
835
|
+
*
|
|
836
|
+
* Tangents propagate as `t(y) = da * t(a) + db * t(b)` using the same partials the {@link Tape}
|
|
837
|
+
* records, under the same zero rule: if a partial or a tangent is exactly 0 the term is 0, even when
|
|
838
|
+
* the other is infinite or `NaN`, so an infinite partial such as `sqrt` at `0` does not turn a zero
|
|
839
|
+
* tangent into `NaN`.
|
|
840
|
+
*
|
|
841
|
+
* Use forward mode when there are few inputs (or one direction) and many outputs; use the tape
|
|
842
|
+
* when there are many inputs and one output.
|
|
843
|
+
*
|
|
844
|
+
* @example
|
|
845
|
+
* ```ts
|
|
846
|
+
* import { Forward } from 'adjointly';
|
|
847
|
+
* const F = new Forward();
|
|
848
|
+
* const x = F.input(2, 1); // seed: direction (1, 0)
|
|
849
|
+
* const y = F.input(3, 0);
|
|
850
|
+
* const f = F.mul(x, F.sin(y)); // x * sin(y)
|
|
851
|
+
* F.value(f); //=> 2 * sin(3)
|
|
852
|
+
* F.tangent(f); //=> sin(3)
|
|
853
|
+
* ```
|
|
854
|
+
*/
|
|
855
|
+
var Forward = class {
|
|
856
|
+
val;
|
|
857
|
+
tan;
|
|
858
|
+
n = 0;
|
|
859
|
+
/** An empty forward-mode context; see {@link ForwardOptions}. */
|
|
860
|
+
constructor(options = {}) {
|
|
861
|
+
const cap = initialCapacity(options.capacity, "Forward");
|
|
862
|
+
this.val = new Float64Array(cap);
|
|
863
|
+
this.tan = new Float64Array(cap);
|
|
864
|
+
if (options.debug === true) installChecks(this, "Forward", 0);
|
|
865
|
+
}
|
|
866
|
+
/** The number of values created so far. */
|
|
867
|
+
get size() {
|
|
868
|
+
return this.n;
|
|
869
|
+
}
|
|
870
|
+
/** Forget every value, keeping the storage. Every `Dual` previously issued becomes invalid. */
|
|
871
|
+
clear() {
|
|
872
|
+
this.n = 0;
|
|
873
|
+
}
|
|
874
|
+
/** An input with value `x` and tangent (seed) `tangent`: its component of the direction. */
|
|
875
|
+
input(x, tangent) {
|
|
876
|
+
return this.push(x, tangent);
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Lift a number into `T` as a constant: its derivative with respect to every input is zero.
|
|
880
|
+
* On a `Tape` it still appends a node (constants are leaves of the tape).
|
|
881
|
+
*/
|
|
882
|
+
constant(x) {
|
|
883
|
+
return this.push(x, 0);
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* The current numeric value of `a`. Use it to branch (`if (A.value(x) > 0)`), to log, or to
|
|
887
|
+
* return a result. The number it returns carries no derivative information: anything computed
|
|
888
|
+
* from it with ordinary arithmetic is a constant as far as differentiation is concerned.
|
|
889
|
+
*/
|
|
890
|
+
value(a) {
|
|
891
|
+
return this.val[a];
|
|
892
|
+
}
|
|
893
|
+
/** The tangent of `a`: its derivative along the direction the inputs were seeded with. */
|
|
894
|
+
tangent(a) {
|
|
895
|
+
return this.tan[a];
|
|
896
|
+
}
|
|
897
|
+
push(y, t) {
|
|
898
|
+
const i = this.n;
|
|
899
|
+
if (i === this.val.length) this.grow();
|
|
900
|
+
this.val[i] = y;
|
|
901
|
+
this.tan[i] = t;
|
|
902
|
+
this.n = i + 1;
|
|
903
|
+
return i;
|
|
904
|
+
}
|
|
905
|
+
grow() {
|
|
906
|
+
const cap = this.val.length * 2;
|
|
907
|
+
const val = new Float64Array(cap);
|
|
908
|
+
val.set(this.val);
|
|
909
|
+
this.val = val;
|
|
910
|
+
const tan = new Float64Array(cap);
|
|
911
|
+
tan.set(this.tan);
|
|
912
|
+
this.tan = tan;
|
|
913
|
+
}
|
|
914
|
+
/** `-a`. */
|
|
915
|
+
neg(a) {
|
|
916
|
+
const x = this.val[a];
|
|
917
|
+
const ta = this.tan[a];
|
|
918
|
+
const y = primitives.neg.value(x);
|
|
919
|
+
return this.push(y, primitives.neg.deriv(x, y) * ta);
|
|
920
|
+
}
|
|
921
|
+
/** `a + b`. */
|
|
922
|
+
add(a, b) {
|
|
923
|
+
const x = this.val[a];
|
|
924
|
+
const z = this.val[b];
|
|
925
|
+
const ta = this.tan[a];
|
|
926
|
+
const tb = this.tan[b];
|
|
927
|
+
const y = primitives.add.value(x, z);
|
|
928
|
+
return this.push(y, primitives.add.da(x, z, y) * ta + primitives.add.db(x, z, y) * tb);
|
|
929
|
+
}
|
|
930
|
+
/** `a - b`. */
|
|
931
|
+
sub(a, b) {
|
|
932
|
+
const x = this.val[a];
|
|
933
|
+
const z = this.val[b];
|
|
934
|
+
const ta = this.tan[a];
|
|
935
|
+
const tb = this.tan[b];
|
|
936
|
+
const y = primitives.sub.value(x, z);
|
|
937
|
+
return this.push(y, primitives.sub.da(x, z, y) * ta + primitives.sub.db(x, z, y) * tb);
|
|
938
|
+
}
|
|
939
|
+
/** `a * b`. */
|
|
940
|
+
mul(a, b) {
|
|
941
|
+
const x = this.val[a];
|
|
942
|
+
const z = this.val[b];
|
|
943
|
+
const ta = this.tan[a];
|
|
944
|
+
const tb = this.tan[b];
|
|
945
|
+
const y = primitives.mul.value(x, z);
|
|
946
|
+
return this.push(y, times(primitives.mul.da(x, z, y), ta) + times(primitives.mul.db(x, z, y), tb));
|
|
947
|
+
}
|
|
948
|
+
/** `a / b`. */
|
|
949
|
+
div(a, b) {
|
|
950
|
+
const x = this.val[a];
|
|
951
|
+
const z = this.val[b];
|
|
952
|
+
const ta = this.tan[a];
|
|
953
|
+
const tb = this.tan[b];
|
|
954
|
+
const y = primitives.div.value(x, z);
|
|
955
|
+
return this.push(y, times(primitives.div.da(x, z, y), ta) + times(primitives.div.db(x, z, y), tb));
|
|
956
|
+
}
|
|
957
|
+
/** `1 / a`. */
|
|
958
|
+
recip(a) {
|
|
959
|
+
const x = this.val[a];
|
|
960
|
+
const ta = this.tan[a];
|
|
961
|
+
const y = primitives.recip.value(x);
|
|
962
|
+
return this.push(y, times(primitives.recip.deriv(x, y), ta));
|
|
963
|
+
}
|
|
964
|
+
/** `a * a`, one node instead of a `mul` of `a` with itself. */
|
|
965
|
+
square(a) {
|
|
966
|
+
const x = this.val[a];
|
|
967
|
+
const ta = this.tan[a];
|
|
968
|
+
const y = primitives.square.value(x);
|
|
969
|
+
return this.push(y, times(primitives.square.deriv(x, y), ta));
|
|
970
|
+
}
|
|
971
|
+
/** `sqrt(a)`; its derivative at `0` is `+Infinity`. */
|
|
972
|
+
sqrt(a) {
|
|
973
|
+
const x = this.val[a];
|
|
974
|
+
const ta = this.tan[a];
|
|
975
|
+
const y = primitives.sqrt.value(x);
|
|
976
|
+
return this.push(y, times(primitives.sqrt.deriv(x, y), ta));
|
|
977
|
+
}
|
|
978
|
+
/** `e^a`. */
|
|
979
|
+
exp(a) {
|
|
980
|
+
const x = this.val[a];
|
|
981
|
+
const ta = this.tan[a];
|
|
982
|
+
const y = primitives.exp.value(x);
|
|
983
|
+
return this.push(y, times(primitives.exp.deriv(x, y), ta));
|
|
984
|
+
}
|
|
985
|
+
/** `e^a - 1`, accurate for small `a`. */
|
|
986
|
+
expm1(a) {
|
|
987
|
+
const x = this.val[a];
|
|
988
|
+
const ta = this.tan[a];
|
|
989
|
+
const y = primitives.expm1.value(x);
|
|
990
|
+
return this.push(y, times(primitives.expm1.deriv(x, y), ta));
|
|
991
|
+
}
|
|
992
|
+
/** Natural logarithm. */
|
|
993
|
+
log(a) {
|
|
994
|
+
const x = this.val[a];
|
|
995
|
+
const ta = this.tan[a];
|
|
996
|
+
const y = primitives.log.value(x);
|
|
997
|
+
return this.push(y, times(primitives.log.deriv(x, y), ta));
|
|
998
|
+
}
|
|
999
|
+
/** `log(1 + a)`, accurate for small `a`. */
|
|
1000
|
+
log1p(a) {
|
|
1001
|
+
const x = this.val[a];
|
|
1002
|
+
const ta = this.tan[a];
|
|
1003
|
+
const y = primitives.log1p.value(x);
|
|
1004
|
+
return this.push(y, times(primitives.log1p.deriv(x, y), ta));
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* `a ** b`, differentiable in both arguments. The derivative with respect to `b` is `0` wherever
|
|
1008
|
+
* `a <= 0`; the derivative with respect to `a` is `0` wherever `b === 0`. For a fixed numeric
|
|
1009
|
+
* exponent use {@link Arithmetic.powc}; for `b = 2` prefer {@link Arithmetic.square}, for
|
|
1010
|
+
* `b = 0.5` prefer {@link Arithmetic.sqrt}.
|
|
1011
|
+
*/
|
|
1012
|
+
pow(a, b) {
|
|
1013
|
+
const x = this.val[a];
|
|
1014
|
+
const z = this.val[b];
|
|
1015
|
+
const ta = this.tan[a];
|
|
1016
|
+
const tb = this.tan[b];
|
|
1017
|
+
const y = primitives.pow.value(x, z);
|
|
1018
|
+
return this.push(y, times(primitives.pow.da(x, z, y), ta) + times(primitives.pow.db(x, z, y), tb));
|
|
1019
|
+
}
|
|
1020
|
+
/** `|a|`; its derivative at `0` is `0`. */
|
|
1021
|
+
abs(a) {
|
|
1022
|
+
const x = this.val[a];
|
|
1023
|
+
const ta = this.tan[a];
|
|
1024
|
+
const y = primitives.abs.value(x);
|
|
1025
|
+
return this.push(y, times(primitives.abs.deriv(x, y), ta));
|
|
1026
|
+
}
|
|
1027
|
+
/** The smaller of `a` and `b`; on a tie the derivative flows to `a`. */
|
|
1028
|
+
min(a, b) {
|
|
1029
|
+
const x = this.val[a];
|
|
1030
|
+
const z = this.val[b];
|
|
1031
|
+
const ta = this.tan[a];
|
|
1032
|
+
const tb = this.tan[b];
|
|
1033
|
+
const y = primitives.min.value(x, z);
|
|
1034
|
+
return this.push(y, times(primitives.min.da(x, z, y), ta) + times(primitives.min.db(x, z, y), tb));
|
|
1035
|
+
}
|
|
1036
|
+
/** The larger of `a` and `b`; on a tie the derivative flows to `a`. */
|
|
1037
|
+
max(a, b) {
|
|
1038
|
+
const x = this.val[a];
|
|
1039
|
+
const z = this.val[b];
|
|
1040
|
+
const ta = this.tan[a];
|
|
1041
|
+
const tb = this.tan[b];
|
|
1042
|
+
const y = primitives.max.value(x, z);
|
|
1043
|
+
return this.push(y, times(primitives.max.da(x, z, y), ta) + times(primitives.max.db(x, z, y), tb));
|
|
1044
|
+
}
|
|
1045
|
+
/** Sine. */
|
|
1046
|
+
sin(a) {
|
|
1047
|
+
const x = this.val[a];
|
|
1048
|
+
const ta = this.tan[a];
|
|
1049
|
+
const y = primitives.sin.value(x);
|
|
1050
|
+
return this.push(y, times(primitives.sin.deriv(x, y), ta));
|
|
1051
|
+
}
|
|
1052
|
+
/** Cosine. */
|
|
1053
|
+
cos(a) {
|
|
1054
|
+
const x = this.val[a];
|
|
1055
|
+
const ta = this.tan[a];
|
|
1056
|
+
const y = primitives.cos.value(x);
|
|
1057
|
+
return this.push(y, times(primitives.cos.deriv(x, y), ta));
|
|
1058
|
+
}
|
|
1059
|
+
/** Hyperbolic tangent. */
|
|
1060
|
+
tanh(a) {
|
|
1061
|
+
const x = this.val[a];
|
|
1062
|
+
const ta = this.tan[a];
|
|
1063
|
+
const y = primitives.tanh.value(x);
|
|
1064
|
+
return this.push(y, times(primitives.tanh.deriv(x, y), ta));
|
|
1065
|
+
}
|
|
1066
|
+
/** Logistic sigmoid `1 / (1 + e^-a)`, stable for any `a`. */
|
|
1067
|
+
sigmoid(a) {
|
|
1068
|
+
const x = this.val[a];
|
|
1069
|
+
const ta = this.tan[a];
|
|
1070
|
+
const y = primitives.sigmoid.value(x);
|
|
1071
|
+
return this.push(y, times(primitives.sigmoid.deriv(x, y), ta));
|
|
1072
|
+
}
|
|
1073
|
+
/** Softplus `log(1 + e^a)`, stable for any `a`; its derivative is `sigmoid(a)`. */
|
|
1074
|
+
softplus(a) {
|
|
1075
|
+
const x = this.val[a];
|
|
1076
|
+
const ta = this.tan[a];
|
|
1077
|
+
const y = primitives.softplus.value(x);
|
|
1078
|
+
return this.push(y, times(primitives.softplus.deriv(x, y), ta));
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* `a + c` for a plain number `c`: `add(a, constant(c))` without the constant's tape node. Same
|
|
1082
|
+
* value and derivative, bit for bit.
|
|
1083
|
+
*/
|
|
1084
|
+
addc(a, c) {
|
|
1085
|
+
const x = this.val[a];
|
|
1086
|
+
const y = primitives.add.value(x, c);
|
|
1087
|
+
return this.push(y, times(primitives.add.da(x, c, y), this.tan[a]));
|
|
1088
|
+
}
|
|
1089
|
+
/** `a - c` for a plain number `c`; `sub(a, constant(c))` in one node. */
|
|
1090
|
+
subc(a, c) {
|
|
1091
|
+
const x = this.val[a];
|
|
1092
|
+
const y = primitives.sub.value(x, c);
|
|
1093
|
+
return this.push(y, times(primitives.sub.da(x, c, y), this.tan[a]));
|
|
1094
|
+
}
|
|
1095
|
+
/** `c - a` for a plain number `c`; `sub(constant(c), a)` in one node. */
|
|
1096
|
+
csub(c, a) {
|
|
1097
|
+
const x = this.val[a];
|
|
1098
|
+
const y = primitives.sub.value(c, x);
|
|
1099
|
+
return this.push(y, times(primitives.sub.db(c, x, y), this.tan[a]));
|
|
1100
|
+
}
|
|
1101
|
+
/** `a * c` for a plain number `c`; `mul(a, constant(c))` in one node. */
|
|
1102
|
+
mulc(a, c) {
|
|
1103
|
+
const x = this.val[a];
|
|
1104
|
+
const y = primitives.mul.value(x, c);
|
|
1105
|
+
return this.push(y, times(primitives.mul.da(x, c, y), this.tan[a]));
|
|
1106
|
+
}
|
|
1107
|
+
/** `a / c` for a plain number `c`; `div(a, constant(c))` in one node. */
|
|
1108
|
+
divc(a, c) {
|
|
1109
|
+
const x = this.val[a];
|
|
1110
|
+
const y = primitives.div.value(x, c);
|
|
1111
|
+
return this.push(y, times(primitives.div.da(x, c, y), this.tan[a]));
|
|
1112
|
+
}
|
|
1113
|
+
/** `c / a` for a plain number `c`; `div(constant(c), a)` in one node. */
|
|
1114
|
+
cdiv(c, a) {
|
|
1115
|
+
const x = this.val[a];
|
|
1116
|
+
const y = primitives.div.value(c, x);
|
|
1117
|
+
return this.push(y, times(primitives.div.db(c, x, y), this.tan[a]));
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* `a ** c` for a plain number `c`; `pow(a, constant(c))` in one node, with the same conventions
|
|
1121
|
+
* (the derivative is `0` when `c === 0`).
|
|
1122
|
+
*/
|
|
1123
|
+
powc(a, c) {
|
|
1124
|
+
const x = this.val[a];
|
|
1125
|
+
const y = primitives.pow.value(x, c);
|
|
1126
|
+
return this.push(y, times(primitives.pow.da(x, c, y), this.tan[a]));
|
|
1127
|
+
}
|
|
1128
|
+
};
|
|
1129
|
+
// src/composites.ts
|
|
1130
|
+
/**
|
|
1131
|
+
* The sum of `xs`, added left to right; `A.constant(0)` for an empty list.
|
|
1132
|
+
*
|
|
1133
|
+
* @example
|
|
1134
|
+
* ```ts
|
|
1135
|
+
* sum(plain, [1, 2, 3]); //=> 6
|
|
1136
|
+
* ```
|
|
1137
|
+
*/
|
|
1138
|
+
function sum(A, xs) {
|
|
1139
|
+
if (xs.length === 0) return A.constant(0);
|
|
1140
|
+
let s = xs[0];
|
|
1141
|
+
for (let i = 1; i < xs.length; i++) s = A.add(s, xs[i]);
|
|
1142
|
+
return s;
|
|
1143
|
+
}
|
|
1144
|
+
/**
|
|
1145
|
+
* The dot product `sum_i xs[i] * ys[i]`; `A.constant(0)` for empty lists. Throws if the lengths
|
|
1146
|
+
* differ.
|
|
1147
|
+
*
|
|
1148
|
+
* @example
|
|
1149
|
+
* ```ts
|
|
1150
|
+
* dot(plain, [1, 2], [3, 4]); //=> 11
|
|
1151
|
+
* ```
|
|
1152
|
+
*/
|
|
1153
|
+
function dot(A, xs, ys) {
|
|
1154
|
+
if (xs.length !== ys.length) throw new RangeError(`dot: lengths differ (${xs.length} and ${ys.length})`);
|
|
1155
|
+
if (xs.length === 0) return A.constant(0);
|
|
1156
|
+
let s = A.mul(xs[0], ys[0]);
|
|
1157
|
+
for (let i = 1; i < xs.length; i++) s = A.add(s, A.mul(xs[i], ys[i]));
|
|
1158
|
+
return s;
|
|
1159
|
+
}
|
|
1160
|
+
/** The largest of the values of `xs` as a plain number; `NaN` if any of them is `NaN`. */
|
|
1161
|
+
function largest(A, xs) {
|
|
1162
|
+
let m = -Infinity;
|
|
1163
|
+
for (const x of xs) m = Math.max(m, A.value(x));
|
|
1164
|
+
return m;
|
|
1165
|
+
}
|
|
1166
|
+
/**
|
|
1167
|
+
* `log(sum_i exp(xs[i]))`, computed stably as `m + log(sum_i exp(xs[i] - m))` where `m` is the
|
|
1168
|
+
* largest value. `m` enters as a constant: the identity holds for any `m`, so its derivative
|
|
1169
|
+
* through `m` is zero and leaving it out keeps the gradient exact (it is `softmax(xs)`).
|
|
1170
|
+
* Throws on an empty list.
|
|
1171
|
+
*
|
|
1172
|
+
* Infinite inputs give the limit of the finite case. If the largest input is `+Infinity`, or every
|
|
1173
|
+
* input is `-Infinity`, the value is that infinity and the gradient is shared equally by the inputs
|
|
1174
|
+
* equal to it, `0` elsewhere; a `-Infinity` among finite inputs has weight `0`. A `NaN` input
|
|
1175
|
+
* makes everything `NaN`.
|
|
1176
|
+
*
|
|
1177
|
+
* @example
|
|
1178
|
+
* ```ts
|
|
1179
|
+
* // The naive log(exp(1000) + exp(1000)) overflows to Infinity.
|
|
1180
|
+
* logsumexp(plain, [1000, 1000]); //=> 1000 + log(2)
|
|
1181
|
+
* gradient((A, xs) => logsumexp(A, xs), [Infinity, 0]).gradient; //=> [1, 0]
|
|
1182
|
+
* ```
|
|
1183
|
+
*/
|
|
1184
|
+
function logsumexp(A, xs) {
|
|
1185
|
+
if (xs.length === 0) throw new RangeError("logsumexp: empty list");
|
|
1186
|
+
const m = largest(A, xs);
|
|
1187
|
+
if (m === Infinity || m === -Infinity) {
|
|
1188
|
+
const top = xs.filter((x) => A.value(x) === m);
|
|
1189
|
+
return A.add(A.mul(sum(A, top), A.constant(1 / top.length)), A.constant(Math.log(top.length)));
|
|
1190
|
+
}
|
|
1191
|
+
const c = A.constant(m);
|
|
1192
|
+
let s = A.exp(A.sub(xs[0], c));
|
|
1193
|
+
for (let i = 1; i < xs.length; i++) s = A.add(s, A.exp(A.sub(xs[i], c)));
|
|
1194
|
+
return A.add(c, A.log(s));
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* The softmax `exp(xs[i]) / sum_j exp(xs[j])`, computed stably by first subtracting the largest
|
|
1198
|
+
* value as a constant (softmax is invariant to the shift, so the gradient stays exact). Throws on
|
|
1199
|
+
* an empty list.
|
|
1200
|
+
*
|
|
1201
|
+
* Infinite inputs give the limit of the finite case. If the largest input is `+Infinity`, or every
|
|
1202
|
+
* input is `-Infinity`, the weights are `1/k` on the `k` inputs equal to it and `0` elsewhere,
|
|
1203
|
+
* returned as constants: the Jacobian there is `0`, which is also its limit. A `-Infinity` among
|
|
1204
|
+
* finite inputs gets weight `0`. A `NaN` input makes every weight `NaN`.
|
|
1205
|
+
*
|
|
1206
|
+
* @example
|
|
1207
|
+
* ```ts
|
|
1208
|
+
* softmax(plain, [0, Math.log(3)]); //=> [0.25, 0.75]
|
|
1209
|
+
* softmax(plain, [Infinity, 0, Infinity]); //=> [0.5, 0, 0.5]
|
|
1210
|
+
* ```
|
|
1211
|
+
*/
|
|
1212
|
+
function softmax(A, xs) {
|
|
1213
|
+
if (xs.length === 0) throw new RangeError("softmax: empty list");
|
|
1214
|
+
const m = largest(A, xs);
|
|
1215
|
+
if (m === Infinity || m === -Infinity) {
|
|
1216
|
+
const k = xs.filter((x) => A.value(x) === m).length;
|
|
1217
|
+
return xs.map((x) => A.constant(A.value(x) === m ? 1 / k : 0));
|
|
1218
|
+
}
|
|
1219
|
+
const c = A.constant(m);
|
|
1220
|
+
const es = xs.map((x) => A.exp(A.sub(x, c)));
|
|
1221
|
+
const inv = A.recip(sum(A, es));
|
|
1222
|
+
return es.map((e) => A.mul(e, inv));
|
|
1223
|
+
}
|
|
1224
|
+
// src/convenience.ts
|
|
1225
|
+
/**
|
|
1226
|
+
* Evaluate `f` at `at` on plain numbers, recording nothing. The value is bit-for-bit the one
|
|
1227
|
+
* {@link gradient} and {@link jvp} report.
|
|
1228
|
+
*
|
|
1229
|
+
* @example
|
|
1230
|
+
* ```ts
|
|
1231
|
+
* evaluate((A, [x]) => A.exp(x), [0]); //=> 1
|
|
1232
|
+
* ```
|
|
1233
|
+
*/
|
|
1234
|
+
function evaluate(f, at) {
|
|
1235
|
+
return f(plain, at);
|
|
1236
|
+
}
|
|
1237
|
+
/**
|
|
1238
|
+
* The value and gradient of `f` at `at`, by reverse mode: one forward pass recording a tape and
|
|
1239
|
+
* one backward sweep, whatever the number of inputs.
|
|
1240
|
+
*
|
|
1241
|
+
* Pass `tape` to reuse its storage across calls in a loop; it is cleared first and left holding
|
|
1242
|
+
* this evaluation.
|
|
1243
|
+
*
|
|
1244
|
+
* @example
|
|
1245
|
+
* ```ts
|
|
1246
|
+
* gradient((A, [x, y]) => A.mul(x, y), [3, 4]); //=> { value: 12, gradient: [4, 3] }
|
|
1247
|
+
* ```
|
|
1248
|
+
*/
|
|
1249
|
+
function gradient(f, at, tape = new Tape()) {
|
|
1250
|
+
tape.clear();
|
|
1251
|
+
const xs = tape.inputs(at);
|
|
1252
|
+
const y = f(tape, xs);
|
|
1253
|
+
tape.backward(y);
|
|
1254
|
+
return {
|
|
1255
|
+
value: tape.value(y),
|
|
1256
|
+
gradient: xs.map((x) => tape.adjoint(x))
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
/**
|
|
1260
|
+
* The value of `f` at `at` and its derivative along `direction` (a Jacobian-vector product), by
|
|
1261
|
+
* forward mode: one pass, no tape. Throws if `direction` and `at` differ in length.
|
|
1262
|
+
*
|
|
1263
|
+
* @example
|
|
1264
|
+
* ```ts
|
|
1265
|
+
* jvp((A, [x, y]) => A.mul(x, y), [3, 4], [1, 0]); //=> { value: 12, derivative: 4 }
|
|
1266
|
+
* ```
|
|
1267
|
+
*/
|
|
1268
|
+
function jvp(f, at, direction, forward = new Forward()) {
|
|
1269
|
+
if (direction.length !== at.length) throw new RangeError(`jvp: ${at.length} inputs but a direction of length ${direction.length}`);
|
|
1270
|
+
forward.clear();
|
|
1271
|
+
const y = f(forward, at.map((x, i) => forward.input(x, direction[i])));
|
|
1272
|
+
return {
|
|
1273
|
+
value: forward.value(y),
|
|
1274
|
+
derivative: forward.tangent(y)
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
// src/check.ts
|
|
1278
|
+
/**
|
|
1279
|
+
* Default relative step for {@link finiteDifference}: about the cube root of machine epsilon,
|
|
1280
|
+
* which balances the central difference's `O(h^2)` truncation error against rounding error.
|
|
1281
|
+
*/
|
|
1282
|
+
const DEFAULT_STEP = 6e-6;
|
|
1283
|
+
/**
|
|
1284
|
+
* The gradient of `f` at `at` by central differences on plain numbers:
|
|
1285
|
+
* `(f(x + h_i e_i) - f(x - h_i e_i)) / (2 h_i)` with `h_i = h * max(1, |at[i]|)`.
|
|
1286
|
+
* Costs two evaluations per input; expect agreement with the true gradient to roughly 1e-8
|
|
1287
|
+
* relative for a smooth, well-scaled function, and nothing useful across a kink or a branch.
|
|
1288
|
+
*
|
|
1289
|
+
* @param h - relative step, default {@link DEFAULT_STEP}.
|
|
1290
|
+
*
|
|
1291
|
+
* @example
|
|
1292
|
+
* ```ts
|
|
1293
|
+
* finiteDifference((A, [x]) => A.square(x), [3]); //=> ~[6]
|
|
1294
|
+
* ```
|
|
1295
|
+
*/
|
|
1296
|
+
function finiteDifference(f, at, h = DEFAULT_STEP) {
|
|
1297
|
+
return centralDifferences(f, at, h).map((d) => d.slope);
|
|
1298
|
+
}
|
|
1299
|
+
/** Units in the last place of error allowed for in each evaluation of `f`. */
|
|
1300
|
+
const ROUNDING_ULPS = 4;
|
|
1301
|
+
/**
|
|
1302
|
+
* Each component's central difference, and an estimate of the error that rounding in the two
|
|
1303
|
+
* evaluations of `f` puts into it: `ROUNDING_ULPS * eps * (|f(x + h)| + |f(x - h)|) / (2 h)`.
|
|
1304
|
+
*/
|
|
1305
|
+
function centralDifferences(f, at, h) {
|
|
1306
|
+
const x = at.slice();
|
|
1307
|
+
const point = x;
|
|
1308
|
+
return at.map((xi, i) => {
|
|
1309
|
+
const step = h * Math.max(1, Math.abs(xi));
|
|
1310
|
+
x[i] = xi + step;
|
|
1311
|
+
const up = f(plain, point);
|
|
1312
|
+
x[i] = xi - step;
|
|
1313
|
+
const down = f(plain, point);
|
|
1314
|
+
x[i] = xi;
|
|
1315
|
+
const span = xi + step - (xi - step);
|
|
1316
|
+
return {
|
|
1317
|
+
slope: (up - down) / span,
|
|
1318
|
+
roundoff: ROUNDING_ULPS * Number.EPSILON * (Math.abs(up) + Math.abs(down)) / span
|
|
1319
|
+
};
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
/**
|
|
1323
|
+
* Compare `f`'s reverse-mode gradient at `at` with central finite differences. Component `i`
|
|
1324
|
+
* passes when `|analytic - numeric| <= atol + rtol * max(|analytic|, |numeric|) + roundoff`.
|
|
1325
|
+
*
|
|
1326
|
+
* `roundoff` allows for the rounding error of the finite difference itself, which grows with
|
|
1327
|
+
* `|f|` and shrinks with the step: `4 * eps * (|f(x + h)| + |f(x - h)|) / (2 h)`, four units in the
|
|
1328
|
+
* last place for each evaluation. Without it an exact gradient of a function whose value is large
|
|
1329
|
+
* next to its slope (`1e5 + 1e-3 * x`) would fail.
|
|
1330
|
+
*
|
|
1331
|
+
* Use it in your own tests to verify a function written against `Arithmetic`, typically at a few
|
|
1332
|
+
* random points away from kinks and branch boundaries, where finite differences are meaningless.
|
|
1333
|
+
*
|
|
1334
|
+
* @example
|
|
1335
|
+
* ```ts
|
|
1336
|
+
* const loss: ScalarFunction = (A, xs) => logsumexp(A, xs.map((x) => A.square(x)));
|
|
1337
|
+
* const r = checkGradient(loss, [0.3, -1.2, 2]);
|
|
1338
|
+
* if (!r.ok) {
|
|
1339
|
+
* const { index, analytic, numeric } = r.worst;
|
|
1340
|
+
* throw new Error(`component ${String(index)}: ${String(analytic)} vs ${String(numeric)}`);
|
|
1341
|
+
* }
|
|
1342
|
+
* r.ok; //=> true
|
|
1343
|
+
* ```
|
|
1344
|
+
*/
|
|
1345
|
+
function checkGradient(f, at, options = {}) {
|
|
1346
|
+
const { h = DEFAULT_STEP, rtol = 1e-5, atol = 1e-8 } = options;
|
|
1347
|
+
const { value, gradient: analytic } = gradient(f, at);
|
|
1348
|
+
const differences = centralDifferences(f, at, h);
|
|
1349
|
+
const numeric = differences.map((d) => d.slope);
|
|
1350
|
+
let ok = true;
|
|
1351
|
+
let worst = {
|
|
1352
|
+
index: -1,
|
|
1353
|
+
analytic: NaN,
|
|
1354
|
+
numeric: NaN,
|
|
1355
|
+
error: 0,
|
|
1356
|
+
allowed: 0,
|
|
1357
|
+
roundoff: 0
|
|
1358
|
+
};
|
|
1359
|
+
let worstRatio = -1;
|
|
1360
|
+
for (let i = 0; i < analytic.length; i++) {
|
|
1361
|
+
const a = analytic[i];
|
|
1362
|
+
const n = numeric[i];
|
|
1363
|
+
const roundoff = differences[i].roundoff;
|
|
1364
|
+
const error = Math.abs(a - n);
|
|
1365
|
+
const allowed = atol + rtol * Math.max(Math.abs(a), Math.abs(n)) + roundoff;
|
|
1366
|
+
const ratio = Number.isNaN(error) ? Infinity : error === 0 ? 0 : error / allowed;
|
|
1367
|
+
if (!(error <= allowed)) ok = false;
|
|
1368
|
+
if (ratio > worstRatio) {
|
|
1369
|
+
worstRatio = ratio;
|
|
1370
|
+
worst = {
|
|
1371
|
+
index: i,
|
|
1372
|
+
analytic: a,
|
|
1373
|
+
numeric: n,
|
|
1374
|
+
error,
|
|
1375
|
+
allowed,
|
|
1376
|
+
roundoff
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
return {
|
|
1381
|
+
ok,
|
|
1382
|
+
value,
|
|
1383
|
+
analytic,
|
|
1384
|
+
numeric,
|
|
1385
|
+
worst
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
export { DEFAULT_STEP, Forward, Tape, binaryNames, checkGradient, dot, evaluate, finiteDifference, gradient, jvp, logsumexp, plain, primitives, softmax, sum, unaryNames };
|
|
1389
|
+
|
|
1390
|
+
//# sourceMappingURL=index.js.map
|