@danielsimonjr/mathts-autograd 0.1.0 → 0.1.2
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/README.md +21 -0
- package/dist/index.d.ts +321 -2
- package/dist/index.js +1504 -5
- package/package.json +58 -58
package/dist/index.js
CHANGED
|
@@ -6,7 +6,9 @@ var DualTensor = class _DualTensor {
|
|
|
6
6
|
tangent;
|
|
7
7
|
constructor(shape, primal, tangent) {
|
|
8
8
|
if (primal.length !== tangent.length) {
|
|
9
|
-
throw new Error(
|
|
9
|
+
throw new Error(
|
|
10
|
+
`DualTensor: primal length ${primal.length} != tangent length ${tangent.length}`
|
|
11
|
+
);
|
|
10
12
|
}
|
|
11
13
|
this.shape = shape;
|
|
12
14
|
this.primal = primal;
|
|
@@ -137,7 +139,7 @@ function forwardGrad(fn, x) {
|
|
|
137
139
|
}
|
|
138
140
|
|
|
139
141
|
// src/tape.ts
|
|
140
|
-
import { Tensor as Tensor3 } from "@danielsimonjr/mathts-tensor";
|
|
142
|
+
import { Tensor as Tensor3, tensorSvd, tensorEig } from "@danielsimonjr/mathts-tensor";
|
|
141
143
|
var Tape = class {
|
|
142
144
|
nodes = [];
|
|
143
145
|
inputGradSlots = /* @__PURE__ */ new Map();
|
|
@@ -175,12 +177,24 @@ var Tape = class {
|
|
|
175
177
|
}
|
|
176
178
|
};
|
|
177
179
|
var TapedTensor = class _TapedTensor {
|
|
178
|
-
constructor(shape, primal, tape, id) {
|
|
180
|
+
constructor(shape, primal, tape, id, axisLabels) {
|
|
179
181
|
this.shape = shape;
|
|
180
182
|
this.primal = primal;
|
|
181
183
|
this.tape = tape;
|
|
182
184
|
this.id = id;
|
|
185
|
+
if (axisLabels !== void 0) {
|
|
186
|
+
this.axisLabels = [...axisLabels];
|
|
187
|
+
}
|
|
183
188
|
}
|
|
189
|
+
shape;
|
|
190
|
+
primal;
|
|
191
|
+
tape;
|
|
192
|
+
id;
|
|
193
|
+
/**
|
|
194
|
+
* Optional per-axis Index labels. When set, enables `contract`.
|
|
195
|
+
* Must have the same length as `shape` when present.
|
|
196
|
+
*/
|
|
197
|
+
axisLabels;
|
|
184
198
|
/**
|
|
185
199
|
* S5 fix: existing engine ops (e.g. lower, pderiv, contract) reach into
|
|
186
200
|
* `.data`. The getter returns the primal so those ops still work when a
|
|
@@ -192,10 +206,17 @@ var TapedTensor = class _TapedTensor {
|
|
|
192
206
|
}
|
|
193
207
|
static fromTensorAsInput(t, tape) {
|
|
194
208
|
const { id } = tape.allocate(t.data.length);
|
|
195
|
-
return new _TapedTensor(t.shape, new Float64Array(t.data), tape, id);
|
|
209
|
+
return new _TapedTensor(t.shape, new Float64Array(t.data), tape, id, t.axisLabels);
|
|
196
210
|
}
|
|
197
211
|
toPrimalTensor() {
|
|
198
|
-
return new Tensor3(this.shape, new Float64Array(this.primal));
|
|
212
|
+
return new Tensor3(this.shape, new Float64Array(this.primal), this.axisLabels);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Reconstruct the Tensor primal with the given axisLabels (used internally
|
|
216
|
+
* when the primal was computed from an op that produces labelled output).
|
|
217
|
+
*/
|
|
218
|
+
toPrimalTensorWith(axisLabels) {
|
|
219
|
+
return new Tensor3(this.shape, new Float64Array(this.primal), axisLabels);
|
|
199
220
|
}
|
|
200
221
|
add(other) {
|
|
201
222
|
this.checkSameShape(other, "add");
|
|
@@ -225,6 +246,39 @@ var TapedTensor = class _TapedTensor {
|
|
|
225
246
|
});
|
|
226
247
|
return new _TapedTensor(this.shape, out, this.tape, id);
|
|
227
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* Elementwise division: this / other.
|
|
251
|
+
*
|
|
252
|
+
* Adjoints (quotient rule):
|
|
253
|
+
* dA = dY / b
|
|
254
|
+
* dB = −dY · a / b²
|
|
255
|
+
*
|
|
256
|
+
* The alias case (a.divide(a)) is handled explicitly: the combined gradient
|
|
257
|
+
* is dA + dB = dY/a − dY·a/a² = dY/a − dY/a = 0. This is correct since
|
|
258
|
+
* a/a = 1 everywhere and d(1)/da = 0.
|
|
259
|
+
*/
|
|
260
|
+
divide(other) {
|
|
261
|
+
this.checkSameShape(other, "divide");
|
|
262
|
+
const out = new Float64Array(this.primal.length);
|
|
263
|
+
for (let i = 0; i < out.length; i++) out[i] = this.primal[i] / other.primal[i];
|
|
264
|
+
const thisPrimal = new Float64Array(this.primal);
|
|
265
|
+
const otherPrimal = new Float64Array(other.primal);
|
|
266
|
+
const thisGradSlot = this.tape.getInputGrad(this.id);
|
|
267
|
+
const otherGradSlot = this.tape.getInputGrad(other.id);
|
|
268
|
+
const isAliased = this === other;
|
|
269
|
+
const { id } = this.tape.record([this.id, other.id], out.length, (outputGrad) => {
|
|
270
|
+
for (let i = 0; i < outputGrad.length; i++) {
|
|
271
|
+
if (isAliased) {
|
|
272
|
+
} else {
|
|
273
|
+
const b = otherPrimal[i];
|
|
274
|
+
const a = thisPrimal[i];
|
|
275
|
+
thisGradSlot[i] += outputGrad[i] / b;
|
|
276
|
+
otherGradSlot[i] += -outputGrad[i] * a / (b * b);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
return new _TapedTensor(this.shape, out, this.tape, id);
|
|
281
|
+
}
|
|
228
282
|
mul(other) {
|
|
229
283
|
this.checkSameShape(other, "mul");
|
|
230
284
|
const out = new Float64Array(this.primal.length);
|
|
@@ -257,12 +311,1457 @@ var TapedTensor = class _TapedTensor {
|
|
|
257
311
|
});
|
|
258
312
|
return new _TapedTensor(this.shape, out, this.tape, id);
|
|
259
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* Reverse-mode AD over `Tensor.contract`.
|
|
316
|
+
*
|
|
317
|
+
* Both operands must carry `axisLabels`; the resulting TapedTensor inherits
|
|
318
|
+
* the contracted-output axisLabels (non-shared axis concatenation).
|
|
319
|
+
*
|
|
320
|
+
* Adjoint derivation (T-notation):
|
|
321
|
+
* Let Y = A.contract(B) (contraction over shared indices S).
|
|
322
|
+
* dA = dY.contract(B') where B' = B with its free axes re-labelled to
|
|
323
|
+
* match the shared positions in A. Equivalently: for each element of A,
|
|
324
|
+
* dA[...a_free, ...s] = Σ_{...b_free} dY[...a_free, ...b_free] · B[...s, ...b_free]
|
|
325
|
+
* dB = A'.contract(dY) symmetrically.
|
|
326
|
+
*
|
|
327
|
+
* Implementation: because `Tensor.contract` matches by Index identity, we
|
|
328
|
+
* build the backward contraction by re-labelling the free axes of dY with
|
|
329
|
+
* the Index objects from the other operand — so `Tensor.contract` automatically
|
|
330
|
+
* finds the right shared axes.
|
|
331
|
+
*/
|
|
332
|
+
contract(other) {
|
|
333
|
+
const aPrimal = this.toPrimalTensorWith(this.axisLabels);
|
|
334
|
+
const bPrimal = other.toPrimalTensorWith(other.axisLabels);
|
|
335
|
+
const yPrimal = aPrimal.contract(bPrimal);
|
|
336
|
+
const aLabels = this.axisLabels;
|
|
337
|
+
const bLabels = other.axisLabels;
|
|
338
|
+
const yLabels = yPrimal.axisLabels;
|
|
339
|
+
const aFreeAxes = [];
|
|
340
|
+
const aSharedAxes = [];
|
|
341
|
+
for (let i = 0; i < aLabels.length; i++) {
|
|
342
|
+
const inOutput = yLabels.some((yl) => yl.matches(aLabels[i]));
|
|
343
|
+
if (inOutput) aFreeAxes.push(i);
|
|
344
|
+
else aSharedAxes.push(i);
|
|
345
|
+
}
|
|
346
|
+
const bFreeAxes = [];
|
|
347
|
+
const bSharedAxes = [];
|
|
348
|
+
for (let i = 0; i < bLabels.length; i++) {
|
|
349
|
+
const inOutput = yLabels.some((yl) => yl.matches(bLabels[i]));
|
|
350
|
+
if (inOutput) bFreeAxes.push(i);
|
|
351
|
+
else bSharedAxes.push(i);
|
|
352
|
+
}
|
|
353
|
+
const aGradSlot = this.tape.getInputGrad(this.id);
|
|
354
|
+
const bGradSlot = this.tape.getInputGrad(other.id);
|
|
355
|
+
const aPrimalData = new Float64Array(this.primal);
|
|
356
|
+
const bPrimalData = new Float64Array(other.primal);
|
|
357
|
+
const aShape = [...this.shape];
|
|
358
|
+
const bShape = [...other.shape];
|
|
359
|
+
const { id } = this.tape.record(
|
|
360
|
+
[this.id, other.id],
|
|
361
|
+
yPrimal.data.length,
|
|
362
|
+
(outputGrad) => {
|
|
363
|
+
const dY = new Tensor3([...yPrimal.shape], new Float64Array(outputGrad), yLabels);
|
|
364
|
+
let bRelabelled = new Tensor3(bShape, bPrimalData, bLabels);
|
|
365
|
+
for (let k = 0; k < bSharedAxes.length; k++) {
|
|
366
|
+
const bAxis = bSharedAxes[k];
|
|
367
|
+
const aAxis = aSharedAxes[k];
|
|
368
|
+
bRelabelled = bRelabelled.replaceIndex(bLabels[bAxis], aLabels[aAxis]);
|
|
369
|
+
}
|
|
370
|
+
const dAFull = dY.contract(bRelabelled);
|
|
371
|
+
const dAData = _scatterToOriginalAxes(dAFull, aLabels);
|
|
372
|
+
for (let i = 0; i < aGradSlot.length; i++) {
|
|
373
|
+
aGradSlot[i] += dAData[i];
|
|
374
|
+
}
|
|
375
|
+
let aRelabelled = new Tensor3(aShape, aPrimalData, aLabels);
|
|
376
|
+
for (let k = 0; k < aSharedAxes.length; k++) {
|
|
377
|
+
const aAxis = aSharedAxes[k];
|
|
378
|
+
const bAxis = bSharedAxes[k];
|
|
379
|
+
aRelabelled = aRelabelled.replaceIndex(aLabels[aAxis], bLabels[bAxis]);
|
|
380
|
+
}
|
|
381
|
+
const dBFull = aRelabelled.contract(dY);
|
|
382
|
+
const dBData = _scatterToOriginalAxes(dBFull, bLabels);
|
|
383
|
+
for (let i = 0; i < bGradSlot.length; i++) {
|
|
384
|
+
bGradSlot[i] += dBData[i];
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
);
|
|
388
|
+
return new _TapedTensor(
|
|
389
|
+
[...yPrimal.shape],
|
|
390
|
+
new Float64Array(yPrimal.data),
|
|
391
|
+
this.tape,
|
|
392
|
+
id,
|
|
393
|
+
yLabels
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Reverse-mode AD over batched matmul.
|
|
398
|
+
*
|
|
399
|
+
* Accepts operands of rank ≥ 2. Convention: trailing 2 axes are the matrix
|
|
400
|
+
* dims (rows × cols); all leading axes are batch dims (must be broadcast-
|
|
401
|
+
* compatible). For rank-2 inputs this is classical matrix multiplication.
|
|
402
|
+
*
|
|
403
|
+
* Adjoint derivation (classical matrix calc, extended to batched):
|
|
404
|
+
* Y = A @ B (A: ...×m×k, B: ...×k×n → Y: ...×m×n)
|
|
405
|
+
* dA = dY @ Bᵀ (dA: ...×m×k)
|
|
406
|
+
* dB = Aᵀ @ dY (dB: ...×k×n)
|
|
407
|
+
*
|
|
408
|
+
* Implemented via `Tensor.einsum` with a dynamically-built spec:
|
|
409
|
+
* forward: '...ik,...kj->...ij'
|
|
410
|
+
* dA: '...ij,...kj->...ik' (contract dY with B on j)
|
|
411
|
+
* dB: '...ki,...kj->...ij' (contract A on k with dY on k)
|
|
412
|
+
*/
|
|
413
|
+
matmul(other) {
|
|
414
|
+
const rankA = this.shape.length;
|
|
415
|
+
const rankB = other.shape.length;
|
|
416
|
+
if (rankA < 2) {
|
|
417
|
+
throw new Error(`TapedTensor.matmul: 'this' operand must have rank \u2265 2, got rank ${rankA}`);
|
|
418
|
+
}
|
|
419
|
+
if (rankB < 2) {
|
|
420
|
+
throw new Error(`TapedTensor.matmul: 'other' operand must have rank \u2265 2, got rank ${rankB}`);
|
|
421
|
+
}
|
|
422
|
+
const batchRankA = rankA - 2;
|
|
423
|
+
const batchRankB = rankB - 2;
|
|
424
|
+
if (batchRankA !== batchRankB) {
|
|
425
|
+
throw new Error(
|
|
426
|
+
`TapedTensor.matmul: batch rank mismatch (${batchRankA} vs ${batchRankB}); both operands must have the same rank`
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
for (let b = 0; b < batchRankA; b++) {
|
|
430
|
+
if (this.shape[b] !== other.shape[b]) {
|
|
431
|
+
throw new Error(
|
|
432
|
+
`TapedTensor.matmul: batch dimension ${b} mismatch (${this.shape[b]} vs ${other.shape[b]})`
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
const m = this.shape[rankA - 2];
|
|
437
|
+
const k = this.shape[rankA - 1];
|
|
438
|
+
const k2 = other.shape[rankB - 2];
|
|
439
|
+
const n = other.shape[rankB - 1];
|
|
440
|
+
if (k !== k2) {
|
|
441
|
+
throw new Error(`TapedTensor.matmul: inner dimension mismatch (${k} vs ${k2})`);
|
|
442
|
+
}
|
|
443
|
+
const aShape = [...this.shape];
|
|
444
|
+
const bShape = [...other.shape];
|
|
445
|
+
const batchShape = aShape.slice(0, batchRankA);
|
|
446
|
+
const yShape = [...batchShape, m, n];
|
|
447
|
+
const aPrimalData = new Float64Array(this.primal);
|
|
448
|
+
const bPrimalData = new Float64Array(other.primal);
|
|
449
|
+
const yData = _batchedMatmul(aPrimalData, bPrimalData, aShape, bShape);
|
|
450
|
+
const aGradSlot = this.tape.getInputGrad(this.id);
|
|
451
|
+
const bGradSlot = this.tape.getInputGrad(other.id);
|
|
452
|
+
const { id } = this.tape.record(
|
|
453
|
+
[this.id, other.id],
|
|
454
|
+
yData.length,
|
|
455
|
+
(outputGrad) => {
|
|
456
|
+
const dA = _batchedMatmulGradA(outputGrad, bPrimalData, yShape, bShape);
|
|
457
|
+
for (let i = 0; i < aGradSlot.length; i++) {
|
|
458
|
+
aGradSlot[i] += dA[i];
|
|
459
|
+
}
|
|
460
|
+
const dB = _batchedMatmulGradB(aPrimalData, outputGrad, aShape, yShape);
|
|
461
|
+
for (let i = 0; i < bGradSlot.length; i++) {
|
|
462
|
+
bGradSlot[i] += dB[i];
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
);
|
|
466
|
+
return new _TapedTensor(yShape, new Float64Array(yData), this.tape, id);
|
|
467
|
+
}
|
|
260
468
|
checkSameShape(other, op) {
|
|
261
469
|
if (this.shape.length !== other.shape.length || !this.shape.every((v, i) => v === other.shape[i])) {
|
|
262
470
|
throw new Error(`TapedTensor.${op}: shape mismatch [${this.shape}] vs [${other.shape}]`);
|
|
263
471
|
}
|
|
264
472
|
}
|
|
473
|
+
// ---------------------------------------------------------------------------
|
|
474
|
+
// Reductions
|
|
475
|
+
// ---------------------------------------------------------------------------
|
|
476
|
+
/**
|
|
477
|
+
* Sum elements along the given axis/axes (or all axes if omitted).
|
|
478
|
+
*
|
|
479
|
+
* Adjoint: dX[...] = dY[reduced(idx)] broadcast back to input shape.
|
|
480
|
+
* Each input element receives the output-gradient entry from its reduced
|
|
481
|
+
* counterpart (the non-reduced coordinates select the dY element; the
|
|
482
|
+
* reduced coordinates are collapsed to 0 in the keepDims=false case).
|
|
483
|
+
*/
|
|
484
|
+
sum(axis, opts) {
|
|
485
|
+
const primalT = this.toPrimalTensor();
|
|
486
|
+
const outT = primalT.sum(axis, opts);
|
|
487
|
+
const inputShape = [...this.shape];
|
|
488
|
+
const inputSize = this.primal.length;
|
|
489
|
+
const keepDims = opts?.keepDims ?? false;
|
|
490
|
+
const axes = _resolveAxes(axis, this.shape.length);
|
|
491
|
+
const axisSet = new Set(axes);
|
|
492
|
+
const thisGradSlot = this.tape.getInputGrad(this.id);
|
|
493
|
+
const { id } = this.tape.record([this.id], outT.data.length, (outputGrad) => {
|
|
494
|
+
const outShape = [...outT.shape];
|
|
495
|
+
const outStrides = _rowMajorStrides(outShape);
|
|
496
|
+
const inStrides = _rowMajorStrides(inputShape);
|
|
497
|
+
const inCoords = new Array(inputShape.length).fill(0);
|
|
498
|
+
for (let n = 0; n < inputSize; n++) {
|
|
499
|
+
let outFlat = 0;
|
|
500
|
+
let outDim = 0;
|
|
501
|
+
for (let k = 0; k < inputShape.length; k++) {
|
|
502
|
+
if (axisSet.has(k)) {
|
|
503
|
+
if (keepDims) {
|
|
504
|
+
outDim++;
|
|
505
|
+
}
|
|
506
|
+
} else {
|
|
507
|
+
outFlat += inCoords[k] * outStrides[outDim];
|
|
508
|
+
outDim++;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
thisGradSlot[n] += outputGrad[outFlat];
|
|
512
|
+
for (let k = inputShape.length - 1; k >= 0; k--) {
|
|
513
|
+
if (++inCoords[k] < inputShape[k]) break;
|
|
514
|
+
inCoords[k] = 0;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
void inStrides;
|
|
518
|
+
});
|
|
519
|
+
return new _TapedTensor([...outT.shape], new Float64Array(outT.data), this.tape, id);
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Arithmetic mean along the given axis/axes (or all axes if omitted).
|
|
523
|
+
*
|
|
524
|
+
* Adjoint: dX[...] = dY[reduced(idx)] / N, broadcast back to input shape.
|
|
525
|
+
* N = product of reduced-axis dimensions.
|
|
526
|
+
*/
|
|
527
|
+
mean(axis, opts) {
|
|
528
|
+
const primalT = this.toPrimalTensor();
|
|
529
|
+
const outT = primalT.mean(axis, opts);
|
|
530
|
+
const inputShape = [...this.shape];
|
|
531
|
+
const inputSize = this.primal.length;
|
|
532
|
+
const keepDims = opts?.keepDims ?? false;
|
|
533
|
+
const axes = _resolveAxes(axis, this.shape.length);
|
|
534
|
+
const axisSet = new Set(axes);
|
|
535
|
+
const N = axes.reduce((prod, ax) => prod * this.shape[ax], 1);
|
|
536
|
+
const thisGradSlot = this.tape.getInputGrad(this.id);
|
|
537
|
+
const { id } = this.tape.record([this.id], outT.data.length, (outputGrad) => {
|
|
538
|
+
const outShape = [...outT.shape];
|
|
539
|
+
const outStrides = _rowMajorStrides(outShape);
|
|
540
|
+
const inCoords = new Array(inputShape.length).fill(0);
|
|
541
|
+
for (let n = 0; n < inputSize; n++) {
|
|
542
|
+
let outFlat = 0;
|
|
543
|
+
let outDim = 0;
|
|
544
|
+
for (let k = 0; k < inputShape.length; k++) {
|
|
545
|
+
if (axisSet.has(k)) {
|
|
546
|
+
if (keepDims) outDim++;
|
|
547
|
+
} else {
|
|
548
|
+
outFlat += inCoords[k] * outStrides[outDim];
|
|
549
|
+
outDim++;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
thisGradSlot[n] += outputGrad[outFlat] / N;
|
|
553
|
+
for (let k = inputShape.length - 1; k >= 0; k--) {
|
|
554
|
+
if (++inCoords[k] < inputShape[k]) break;
|
|
555
|
+
inCoords[k] = 0;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
return new _TapedTensor([...outT.shape], new Float64Array(outT.data), this.tape, id);
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Product of elements along the given axis/axes (or all axes if omitted).
|
|
563
|
+
*
|
|
564
|
+
* Adjoint: dX_i = dY * (prod_over_axes(x) / x_i) per element.
|
|
565
|
+
*
|
|
566
|
+
* Zero-element corners:
|
|
567
|
+
* - Exactly one x_i = 0 in the reduced group: d/dx_i = product of all others
|
|
568
|
+
* (which is the full product / x_i evaluated via alternate product),
|
|
569
|
+
* and d/dx_j = 0 for all j ≠ i where x_j ≠ 0.
|
|
570
|
+
* - Two or more zeros in the reduced group: gradient is 0 everywhere for that
|
|
571
|
+
* group (because changing any single zero cannot change a product that is
|
|
572
|
+
* zero due to another zero).
|
|
573
|
+
*
|
|
574
|
+
* Implementation: uses prefix/suffix products to handle zeros robustly.
|
|
575
|
+
*/
|
|
576
|
+
prod(axis, opts) {
|
|
577
|
+
const primalT = this.toPrimalTensor();
|
|
578
|
+
const outT = primalT.prod(axis, opts);
|
|
579
|
+
const inputShape = [...this.shape];
|
|
580
|
+
const keepDims = opts?.keepDims ?? false;
|
|
581
|
+
const axes = _resolveAxes(axis, this.shape.length);
|
|
582
|
+
const axisSet = new Set(axes);
|
|
583
|
+
const primalData = new Float64Array(this.primal);
|
|
584
|
+
const thisGradSlot = this.tape.getInputGrad(this.id);
|
|
585
|
+
const { id } = this.tape.record([this.id], outT.data.length, (outputGrad) => {
|
|
586
|
+
const outShape = [...outT.shape];
|
|
587
|
+
const outStrides = _rowMajorStrides(outShape);
|
|
588
|
+
const inStrides = _rowMajorStrides(inputShape);
|
|
589
|
+
const groups = /* @__PURE__ */ new Map();
|
|
590
|
+
const inCoords = new Array(inputShape.length).fill(0);
|
|
591
|
+
for (let n = 0; n < primalData.length; n++) {
|
|
592
|
+
let outFlat = 0;
|
|
593
|
+
let outDim = 0;
|
|
594
|
+
for (let k = 0; k < inputShape.length; k++) {
|
|
595
|
+
if (axisSet.has(k)) {
|
|
596
|
+
if (keepDims) outDim++;
|
|
597
|
+
} else {
|
|
598
|
+
outFlat += inCoords[k] * outStrides[outDim];
|
|
599
|
+
outDim++;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
const group = groups.get(outFlat);
|
|
603
|
+
if (group === void 0) {
|
|
604
|
+
groups.set(outFlat, [n]);
|
|
605
|
+
} else {
|
|
606
|
+
group.push(n);
|
|
607
|
+
}
|
|
608
|
+
for (let k = inputShape.length - 1; k >= 0; k--) {
|
|
609
|
+
if (++inCoords[k] < inputShape[k]) break;
|
|
610
|
+
inCoords[k] = 0;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
void inStrides;
|
|
614
|
+
for (const [outFlat, group] of groups) {
|
|
615
|
+
const dY = outputGrad[outFlat];
|
|
616
|
+
const m = group.length;
|
|
617
|
+
const prefix = new Float64Array(m);
|
|
618
|
+
prefix[0] = 1;
|
|
619
|
+
for (let i = 1; i < m; i++) {
|
|
620
|
+
prefix[i] = prefix[i - 1] * primalData[group[i - 1]];
|
|
621
|
+
}
|
|
622
|
+
const suffix = new Float64Array(m);
|
|
623
|
+
suffix[m - 1] = 1;
|
|
624
|
+
for (let i = m - 2; i >= 0; i--) {
|
|
625
|
+
suffix[i] = suffix[i + 1] * primalData[group[i + 1]];
|
|
626
|
+
}
|
|
627
|
+
for (let i = 0; i < m; i++) {
|
|
628
|
+
thisGradSlot[group[i]] += dY * prefix[i] * suffix[i];
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
return new _TapedTensor([...outT.shape], new Float64Array(outT.data), this.tape, id);
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Maximum value along the given axis/axes (or all axes if omitted).
|
|
636
|
+
*
|
|
637
|
+
* Adjoint: dY is scattered to the argmax position(s).
|
|
638
|
+
* Tie-breaking: "first-wins" — the gradient flows to the first (smallest
|
|
639
|
+
* flat-index) element among those that attain the maximum.
|
|
640
|
+
*/
|
|
641
|
+
max(axis, opts) {
|
|
642
|
+
const primalT = this.toPrimalTensor();
|
|
643
|
+
const outT = primalT.max(axis, opts);
|
|
644
|
+
const inputShape = [...this.shape];
|
|
645
|
+
const keepDims = opts?.keepDims ?? false;
|
|
646
|
+
const axes = _resolveAxes(axis, this.shape.length);
|
|
647
|
+
const axisSet = new Set(axes);
|
|
648
|
+
const primalData = new Float64Array(this.primal);
|
|
649
|
+
const thisGradSlot = this.tape.getInputGrad(this.id);
|
|
650
|
+
const { id } = this.tape.record([this.id], outT.data.length, (outputGrad) => {
|
|
651
|
+
const outShape = [...outT.shape];
|
|
652
|
+
const outStrides = _rowMajorStrides(outShape);
|
|
653
|
+
const argmaxSlot = new Int32Array(outT.data.length).fill(-1);
|
|
654
|
+
const inCoords = new Array(inputShape.length).fill(0);
|
|
655
|
+
for (let n = 0; n < primalData.length; n++) {
|
|
656
|
+
let outFlat = 0;
|
|
657
|
+
let outDim = 0;
|
|
658
|
+
for (let k = 0; k < inputShape.length; k++) {
|
|
659
|
+
if (axisSet.has(k)) {
|
|
660
|
+
if (keepDims) outDim++;
|
|
661
|
+
} else {
|
|
662
|
+
outFlat += inCoords[k] * outStrides[outDim];
|
|
663
|
+
outDim++;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
if (argmaxSlot[outFlat] === -1 || primalData[n] > primalData[argmaxSlot[outFlat]]) {
|
|
667
|
+
argmaxSlot[outFlat] = n;
|
|
668
|
+
}
|
|
669
|
+
for (let k = inputShape.length - 1; k >= 0; k--) {
|
|
670
|
+
if (++inCoords[k] < inputShape[k]) break;
|
|
671
|
+
inCoords[k] = 0;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
for (let outFlat = 0; outFlat < outT.data.length; outFlat++) {
|
|
675
|
+
const winner = argmaxSlot[outFlat];
|
|
676
|
+
if (winner !== -1) {
|
|
677
|
+
thisGradSlot[winner] += outputGrad[outFlat];
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
return new _TapedTensor([...outT.shape], new Float64Array(outT.data), this.tape, id);
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Minimum value along the given axis/axes (or all axes if omitted).
|
|
685
|
+
*
|
|
686
|
+
* Adjoint: dY is scattered to the argmin position(s).
|
|
687
|
+
* Tie-breaking: "first-wins" — gradient flows to the first (smallest
|
|
688
|
+
* flat-index) element that attains the minimum.
|
|
689
|
+
*/
|
|
690
|
+
min(axis, opts) {
|
|
691
|
+
const primalT = this.toPrimalTensor();
|
|
692
|
+
const outT = primalT.min(axis, opts);
|
|
693
|
+
const inputShape = [...this.shape];
|
|
694
|
+
const keepDims = opts?.keepDims ?? false;
|
|
695
|
+
const axes = _resolveAxes(axis, this.shape.length);
|
|
696
|
+
const axisSet = new Set(axes);
|
|
697
|
+
const primalData = new Float64Array(this.primal);
|
|
698
|
+
const thisGradSlot = this.tape.getInputGrad(this.id);
|
|
699
|
+
const { id } = this.tape.record([this.id], outT.data.length, (outputGrad) => {
|
|
700
|
+
const outShape = [...outT.shape];
|
|
701
|
+
const outStrides = _rowMajorStrides(outShape);
|
|
702
|
+
const argminSlot = new Int32Array(outT.data.length).fill(-1);
|
|
703
|
+
const inCoords = new Array(inputShape.length).fill(0);
|
|
704
|
+
for (let n = 0; n < primalData.length; n++) {
|
|
705
|
+
let outFlat = 0;
|
|
706
|
+
let outDim = 0;
|
|
707
|
+
for (let k = 0; k < inputShape.length; k++) {
|
|
708
|
+
if (axisSet.has(k)) {
|
|
709
|
+
if (keepDims) outDim++;
|
|
710
|
+
} else {
|
|
711
|
+
outFlat += inCoords[k] * outStrides[outDim];
|
|
712
|
+
outDim++;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
if (argminSlot[outFlat] === -1 || primalData[n] < primalData[argminSlot[outFlat]]) {
|
|
716
|
+
argminSlot[outFlat] = n;
|
|
717
|
+
}
|
|
718
|
+
for (let k = inputShape.length - 1; k >= 0; k--) {
|
|
719
|
+
if (++inCoords[k] < inputShape[k]) break;
|
|
720
|
+
inCoords[k] = 0;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
for (let outFlat = 0; outFlat < outT.data.length; outFlat++) {
|
|
724
|
+
const winner = argminSlot[outFlat];
|
|
725
|
+
if (winner !== -1) {
|
|
726
|
+
thisGradSlot[winner] += outputGrad[outFlat];
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
return new _TapedTensor([...outT.shape], new Float64Array(outT.data), this.tape, id);
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* p-norm of the tensor.
|
|
734
|
+
*
|
|
735
|
+
* Supported p values: 1, 2, 'fro', 'inf'. Default p = 2.
|
|
736
|
+
* When `opts.axis` is given, reduces along that axis; otherwise reduces all axes.
|
|
737
|
+
*
|
|
738
|
+
* Adjoints:
|
|
739
|
+
* - p=2 / p='fro': dX = dY · x / ‖x‖₂ (Frobenius is the 2-norm of the flattened tensor)
|
|
740
|
+
* - p=1: dX = dY · sign(x) (subgradient = 0 at exact zero)
|
|
741
|
+
* - p='inf': dX scattered to the element(s) of max absolute value;
|
|
742
|
+
* tie-breaking: first-wins. Sign of the scattered gradient
|
|
743
|
+
* matches sign(x_max).
|
|
744
|
+
*/
|
|
745
|
+
norm(opts) {
|
|
746
|
+
const p = opts?.p ?? 2;
|
|
747
|
+
const keepDims = opts?.keepDims ?? false;
|
|
748
|
+
const primalT = this.toPrimalTensor();
|
|
749
|
+
const outT = primalT.norm({ p, axis: opts?.axis, keepDims });
|
|
750
|
+
const inputShape = [...this.shape];
|
|
751
|
+
const primalData = new Float64Array(this.primal);
|
|
752
|
+
const thisGradSlot = this.tape.getInputGrad(this.id);
|
|
753
|
+
const axes = opts?.axis !== void 0 ? [opts.axis] : _resolveAxes(void 0, this.shape.length);
|
|
754
|
+
const axisSet = new Set(axes);
|
|
755
|
+
if (p === 2 || p === "fro") {
|
|
756
|
+
const normData = new Float64Array(outT.data);
|
|
757
|
+
const { id } = this.tape.record([this.id], outT.data.length, (outputGrad) => {
|
|
758
|
+
const outShape = [...outT.shape];
|
|
759
|
+
const outStrides = _rowMajorStrides(outShape);
|
|
760
|
+
const inCoords = new Array(inputShape.length).fill(0);
|
|
761
|
+
for (let n = 0; n < primalData.length; n++) {
|
|
762
|
+
let outFlat = 0;
|
|
763
|
+
let outDim = 0;
|
|
764
|
+
for (let k = 0; k < inputShape.length; k++) {
|
|
765
|
+
if (axisSet.has(k)) {
|
|
766
|
+
if (keepDims) outDim++;
|
|
767
|
+
} else {
|
|
768
|
+
outFlat += inCoords[k] * outStrides[outDim];
|
|
769
|
+
outDim++;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
const normVal = normData[outFlat];
|
|
773
|
+
if (normVal !== 0) {
|
|
774
|
+
thisGradSlot[n] += outputGrad[outFlat] * primalData[n] / normVal;
|
|
775
|
+
}
|
|
776
|
+
for (let k = inputShape.length - 1; k >= 0; k--) {
|
|
777
|
+
if (++inCoords[k] < inputShape[k]) break;
|
|
778
|
+
inCoords[k] = 0;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
return new _TapedTensor([...outT.shape], new Float64Array(outT.data), this.tape, id);
|
|
783
|
+
}
|
|
784
|
+
if (p === 1) {
|
|
785
|
+
const { id } = this.tape.record([this.id], outT.data.length, (outputGrad) => {
|
|
786
|
+
const outShape = [...outT.shape];
|
|
787
|
+
const outStrides = _rowMajorStrides(outShape);
|
|
788
|
+
const inCoords = new Array(inputShape.length).fill(0);
|
|
789
|
+
for (let n = 0; n < primalData.length; n++) {
|
|
790
|
+
let outFlat = 0;
|
|
791
|
+
let outDim = 0;
|
|
792
|
+
for (let k = 0; k < inputShape.length; k++) {
|
|
793
|
+
if (axisSet.has(k)) {
|
|
794
|
+
if (keepDims) outDim++;
|
|
795
|
+
} else {
|
|
796
|
+
outFlat += inCoords[k] * outStrides[outDim];
|
|
797
|
+
outDim++;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
const xi = primalData[n];
|
|
801
|
+
const sign = xi > 0 ? 1 : xi < 0 ? -1 : 0;
|
|
802
|
+
thisGradSlot[n] += outputGrad[outFlat] * sign;
|
|
803
|
+
for (let k = inputShape.length - 1; k >= 0; k--) {
|
|
804
|
+
if (++inCoords[k] < inputShape[k]) break;
|
|
805
|
+
inCoords[k] = 0;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
return new _TapedTensor([...outT.shape], new Float64Array(outT.data), this.tape, id);
|
|
810
|
+
}
|
|
811
|
+
{
|
|
812
|
+
const { id } = this.tape.record([this.id], outT.data.length, (outputGrad) => {
|
|
813
|
+
const outShape = [...outT.shape];
|
|
814
|
+
const outStrides = _rowMajorStrides(outShape);
|
|
815
|
+
const argmaxAbsSlot = new Int32Array(outT.data.length).fill(-1);
|
|
816
|
+
const inCoords = new Array(inputShape.length).fill(0);
|
|
817
|
+
for (let n = 0; n < primalData.length; n++) {
|
|
818
|
+
let outFlat = 0;
|
|
819
|
+
let outDim = 0;
|
|
820
|
+
for (let k = 0; k < inputShape.length; k++) {
|
|
821
|
+
if (axisSet.has(k)) {
|
|
822
|
+
if (keepDims) outDim++;
|
|
823
|
+
} else {
|
|
824
|
+
outFlat += inCoords[k] * outStrides[outDim];
|
|
825
|
+
outDim++;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
const prevWinner = argmaxAbsSlot[outFlat];
|
|
829
|
+
if (prevWinner === -1 || Math.abs(primalData[n]) > Math.abs(primalData[prevWinner])) {
|
|
830
|
+
argmaxAbsSlot[outFlat] = n;
|
|
831
|
+
}
|
|
832
|
+
for (let k = inputShape.length - 1; k >= 0; k--) {
|
|
833
|
+
if (++inCoords[k] < inputShape[k]) break;
|
|
834
|
+
inCoords[k] = 0;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
for (let outFlat = 0; outFlat < outT.data.length; outFlat++) {
|
|
838
|
+
const winner = argmaxAbsSlot[outFlat];
|
|
839
|
+
if (winner !== -1) {
|
|
840
|
+
const xi = primalData[winner];
|
|
841
|
+
const sign = xi > 0 ? 1 : xi < 0 ? -1 : 0;
|
|
842
|
+
thisGradSlot[winner] += outputGrad[outFlat] * sign;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
return new _TapedTensor([...outT.shape], new Float64Array(outT.data), this.tape, id);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
// ---------------------------------------------------------------------------
|
|
850
|
+
// Elementwise transcendentals
|
|
851
|
+
// ---------------------------------------------------------------------------
|
|
852
|
+
/**
|
|
853
|
+
* Elementwise natural logarithm.
|
|
854
|
+
*
|
|
855
|
+
* Adjoint: dX = dY / x
|
|
856
|
+
*/
|
|
857
|
+
log() {
|
|
858
|
+
const out = new Float64Array(this.primal.length);
|
|
859
|
+
for (let i = 0; i < out.length; i++) out[i] = Math.log(this.primal[i]);
|
|
860
|
+
const primalData = new Float64Array(this.primal);
|
|
861
|
+
const thisGradSlot = this.tape.getInputGrad(this.id);
|
|
862
|
+
const { id } = this.tape.record([this.id], out.length, (outputGrad) => {
|
|
863
|
+
for (let i = 0; i < outputGrad.length; i++) {
|
|
864
|
+
thisGradSlot[i] += outputGrad[i] / primalData[i];
|
|
865
|
+
}
|
|
866
|
+
});
|
|
867
|
+
return new _TapedTensor(this.shape, out, this.tape, id);
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Elementwise exponential.
|
|
871
|
+
*
|
|
872
|
+
* Adjoint: dX = dY · y where y = exp(x). Primal output is cached.
|
|
873
|
+
*/
|
|
874
|
+
exp() {
|
|
875
|
+
const out = new Float64Array(this.primal.length);
|
|
876
|
+
for (let i = 0; i < out.length; i++) out[i] = Math.exp(this.primal[i]);
|
|
877
|
+
const expData = new Float64Array(out);
|
|
878
|
+
const thisGradSlot = this.tape.getInputGrad(this.id);
|
|
879
|
+
const { id } = this.tape.record([this.id], out.length, (outputGrad) => {
|
|
880
|
+
for (let i = 0; i < outputGrad.length; i++) {
|
|
881
|
+
thisGradSlot[i] += outputGrad[i] * expData[i];
|
|
882
|
+
}
|
|
883
|
+
});
|
|
884
|
+
return new _TapedTensor(this.shape, out, this.tape, id);
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Elementwise sine.
|
|
888
|
+
*
|
|
889
|
+
* Adjoint: dX = dY · cos(x)
|
|
890
|
+
*/
|
|
891
|
+
sin() {
|
|
892
|
+
const out = new Float64Array(this.primal.length);
|
|
893
|
+
const cosData = new Float64Array(this.primal.length);
|
|
894
|
+
for (let i = 0; i < out.length; i++) {
|
|
895
|
+
out[i] = Math.sin(this.primal[i]);
|
|
896
|
+
cosData[i] = Math.cos(this.primal[i]);
|
|
897
|
+
}
|
|
898
|
+
const thisGradSlot = this.tape.getInputGrad(this.id);
|
|
899
|
+
const { id } = this.tape.record([this.id], out.length, (outputGrad) => {
|
|
900
|
+
for (let i = 0; i < outputGrad.length; i++) {
|
|
901
|
+
thisGradSlot[i] += outputGrad[i] * cosData[i];
|
|
902
|
+
}
|
|
903
|
+
});
|
|
904
|
+
return new _TapedTensor(this.shape, out, this.tape, id);
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Elementwise cosine.
|
|
908
|
+
*
|
|
909
|
+
* Adjoint: dX = −dY · sin(x)
|
|
910
|
+
*/
|
|
911
|
+
cos() {
|
|
912
|
+
const out = new Float64Array(this.primal.length);
|
|
913
|
+
const sinData = new Float64Array(this.primal.length);
|
|
914
|
+
for (let i = 0; i < out.length; i++) {
|
|
915
|
+
out[i] = Math.cos(this.primal[i]);
|
|
916
|
+
sinData[i] = Math.sin(this.primal[i]);
|
|
917
|
+
}
|
|
918
|
+
const thisGradSlot = this.tape.getInputGrad(this.id);
|
|
919
|
+
const { id } = this.tape.record([this.id], out.length, (outputGrad) => {
|
|
920
|
+
for (let i = 0; i < outputGrad.length; i++) {
|
|
921
|
+
thisGradSlot[i] += -outputGrad[i] * sinData[i];
|
|
922
|
+
}
|
|
923
|
+
});
|
|
924
|
+
return new _TapedTensor(this.shape, out, this.tape, id);
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* Elementwise tangent.
|
|
928
|
+
*
|
|
929
|
+
* Adjoint: dX = dY / cos²(x) (= dY · sec²(x))
|
|
930
|
+
*/
|
|
931
|
+
tan() {
|
|
932
|
+
const out = new Float64Array(this.primal.length);
|
|
933
|
+
const cosData = new Float64Array(this.primal.length);
|
|
934
|
+
for (let i = 0; i < out.length; i++) {
|
|
935
|
+
out[i] = Math.tan(this.primal[i]);
|
|
936
|
+
cosData[i] = Math.cos(this.primal[i]);
|
|
937
|
+
}
|
|
938
|
+
const thisGradSlot = this.tape.getInputGrad(this.id);
|
|
939
|
+
const { id } = this.tape.record([this.id], out.length, (outputGrad) => {
|
|
940
|
+
for (let i = 0; i < outputGrad.length; i++) {
|
|
941
|
+
const c = cosData[i];
|
|
942
|
+
thisGradSlot[i] += outputGrad[i] / (c * c);
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
return new _TapedTensor(this.shape, out, this.tape, id);
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Elementwise square root.
|
|
949
|
+
*
|
|
950
|
+
* Adjoint: dX = dY / (2·y) where y = sqrt(x). Primal output is cached.
|
|
951
|
+
*/
|
|
952
|
+
sqrt() {
|
|
953
|
+
const out = new Float64Array(this.primal.length);
|
|
954
|
+
for (let i = 0; i < out.length; i++) out[i] = Math.sqrt(this.primal[i]);
|
|
955
|
+
const sqrtData = new Float64Array(out);
|
|
956
|
+
const thisGradSlot = this.tape.getInputGrad(this.id);
|
|
957
|
+
const { id } = this.tape.record([this.id], out.length, (outputGrad) => {
|
|
958
|
+
for (let i = 0; i < outputGrad.length; i++) {
|
|
959
|
+
thisGradSlot[i] += outputGrad[i] / (2 * sqrtData[i]);
|
|
960
|
+
}
|
|
961
|
+
});
|
|
962
|
+
return new _TapedTensor(this.shape, out, this.tape, id);
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Elementwise square (x²).
|
|
966
|
+
*
|
|
967
|
+
* Adjoint: dX = dY · 2x
|
|
968
|
+
*/
|
|
969
|
+
square() {
|
|
970
|
+
const out = new Float64Array(this.primal.length);
|
|
971
|
+
for (let i = 0; i < out.length; i++) out[i] = this.primal[i] * this.primal[i];
|
|
972
|
+
const primalData = new Float64Array(this.primal);
|
|
973
|
+
const thisGradSlot = this.tape.getInputGrad(this.id);
|
|
974
|
+
const { id } = this.tape.record([this.id], out.length, (outputGrad) => {
|
|
975
|
+
for (let i = 0; i < outputGrad.length; i++) {
|
|
976
|
+
thisGradSlot[i] += outputGrad[i] * 2 * primalData[i];
|
|
977
|
+
}
|
|
978
|
+
});
|
|
979
|
+
return new _TapedTensor(this.shape, out, this.tape, id);
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Elementwise fixed-exponent power: x^k.
|
|
983
|
+
*
|
|
984
|
+
* Only fixed (non-TapedTensor) exponents are supported. Variable-exponent
|
|
985
|
+
* pow(taped, taped) is a follow-up slice.
|
|
986
|
+
*
|
|
987
|
+
* Adjoint: dX = dY · k · x^(k−1)
|
|
988
|
+
*/
|
|
989
|
+
pow(k) {
|
|
990
|
+
const out = new Float64Array(this.primal.length);
|
|
991
|
+
for (let i = 0; i < out.length; i++) out[i] = Math.pow(this.primal[i], k);
|
|
992
|
+
const primalData = new Float64Array(this.primal);
|
|
993
|
+
const thisGradSlot = this.tape.getInputGrad(this.id);
|
|
994
|
+
const { id } = this.tape.record([this.id], out.length, (outputGrad) => {
|
|
995
|
+
for (let i = 0; i < outputGrad.length; i++) {
|
|
996
|
+
thisGradSlot[i] += outputGrad[i] * k * Math.pow(primalData[i], k - 1);
|
|
997
|
+
}
|
|
998
|
+
});
|
|
999
|
+
return new _TapedTensor(this.shape, out, this.tape, id);
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Elementwise reciprocal: 1 / x.
|
|
1003
|
+
*
|
|
1004
|
+
* Adjoint: dX = −dY / x²
|
|
1005
|
+
*/
|
|
1006
|
+
reciprocal() {
|
|
1007
|
+
const out = new Float64Array(this.primal.length);
|
|
1008
|
+
for (let i = 0; i < out.length; i++) out[i] = 1 / this.primal[i];
|
|
1009
|
+
const primalData = new Float64Array(this.primal);
|
|
1010
|
+
const thisGradSlot = this.tape.getInputGrad(this.id);
|
|
1011
|
+
const { id } = this.tape.record([this.id], out.length, (outputGrad) => {
|
|
1012
|
+
for (let i = 0; i < outputGrad.length; i++) {
|
|
1013
|
+
const xi = primalData[i];
|
|
1014
|
+
thisGradSlot[i] += -outputGrad[i] / (xi * xi);
|
|
1015
|
+
}
|
|
1016
|
+
});
|
|
1017
|
+
return new _TapedTensor(this.shape, out, this.tape, id);
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Elementwise absolute value: |x|.
|
|
1021
|
+
*
|
|
1022
|
+
* Adjoint: dX = dY · sign(x)
|
|
1023
|
+
* Subgradient at exact zero is defined as 0 (rather than undefined).
|
|
1024
|
+
*/
|
|
1025
|
+
abs() {
|
|
1026
|
+
const out = new Float64Array(this.primal.length);
|
|
1027
|
+
for (let i = 0; i < out.length; i++) out[i] = Math.abs(this.primal[i]);
|
|
1028
|
+
const primalData = new Float64Array(this.primal);
|
|
1029
|
+
const thisGradSlot = this.tape.getInputGrad(this.id);
|
|
1030
|
+
const { id } = this.tape.record([this.id], out.length, (outputGrad) => {
|
|
1031
|
+
for (let i = 0; i < outputGrad.length; i++) {
|
|
1032
|
+
const xi = primalData[i];
|
|
1033
|
+
const sign = xi > 0 ? 1 : xi < 0 ? -1 : 0;
|
|
1034
|
+
thisGradSlot[i] += outputGrad[i] * sign;
|
|
1035
|
+
}
|
|
1036
|
+
});
|
|
1037
|
+
return new _TapedTensor(this.shape, out, this.tape, id);
|
|
1038
|
+
}
|
|
1039
|
+
// ---------------------------------------------------------------------------
|
|
1040
|
+
// tensordot — generalised dot product over explicit axis pairs.
|
|
1041
|
+
// ---------------------------------------------------------------------------
|
|
1042
|
+
/**
|
|
1043
|
+
* Reverse-mode AD over `Tensor.tensordot`.
|
|
1044
|
+
*
|
|
1045
|
+
* `axes[i] = [a, b]` contracts axis `a` of `this` with axis `b` of `other`.
|
|
1046
|
+
* The result shape is `this`'s non-contracted axes (in original order)
|
|
1047
|
+
* followed by `other`'s non-contracted axes (in original order).
|
|
1048
|
+
*
|
|
1049
|
+
* Adjoint derivation (NumPy/PyTorch tensordot backward, see Townsend 2016 §6,
|
|
1050
|
+
* and the canonical PyTorch implementation `TensorDotBackward0` in
|
|
1051
|
+
* `torch/csrc/autograd/generated/Functions.cpp`):
|
|
1052
|
+
*
|
|
1053
|
+
* Z = tensordot(A, B, axes)
|
|
1054
|
+
* dA = tensordot(dZ, B, [axes_of_dZ_corresponding_to_B's_free, B's_free])
|
|
1055
|
+
* then permute back into A's original axis order.
|
|
1056
|
+
* dB = tensordot(A, dZ, [A's_free, axes_of_dZ_corresponding_to_A's_free])
|
|
1057
|
+
* then permute back into B's original axis order.
|
|
1058
|
+
*
|
|
1059
|
+
* The axis-permutation bookkeeping is the trickiest part: Tensor.tensordot
|
|
1060
|
+
* produces output axes in the order [A's free axes (original A order),
|
|
1061
|
+
* B's free axes (original B order)] — and after the backward contractions
|
|
1062
|
+
* the survivors come out in B's original (resp. A's original) axis order
|
|
1063
|
+
* for the contracted side, which then needs to be permuted into pair order
|
|
1064
|
+
* (so axis k of the contracted block matches the kth pair) and finally
|
|
1065
|
+
* scattered back into A's (resp. B's) full original axis order.
|
|
1066
|
+
*
|
|
1067
|
+
* For the rank-2 × rank-2 single-axis case (i.e. ordinary matmul A·B with
|
|
1068
|
+
* axes = [[1, 0]]), this reduces to dA = dZ · Bᵀ, dB = Aᵀ · dZ — the same
|
|
1069
|
+
* adjoint as `TapedTensor.matmul`.
|
|
1070
|
+
*/
|
|
1071
|
+
tensordot(other, axes) {
|
|
1072
|
+
const aShape = [...this.shape];
|
|
1073
|
+
const bShape = [...other.shape];
|
|
1074
|
+
const aRank = aShape.length;
|
|
1075
|
+
const bRank = bShape.length;
|
|
1076
|
+
const aContractedInPairOrder = [];
|
|
1077
|
+
const bContractedInPairOrder = [];
|
|
1078
|
+
for (const [a, b] of axes) {
|
|
1079
|
+
aContractedInPairOrder.push(a);
|
|
1080
|
+
bContractedInPairOrder.push(b);
|
|
1081
|
+
}
|
|
1082
|
+
const aContractedSet = new Set(aContractedInPairOrder);
|
|
1083
|
+
const bContractedSet = new Set(bContractedInPairOrder);
|
|
1084
|
+
const aFree = [];
|
|
1085
|
+
for (let i = 0; i < aRank; i++) if (!aContractedSet.has(i)) aFree.push(i);
|
|
1086
|
+
const bFree = [];
|
|
1087
|
+
for (let i = 0; i < bRank; i++) if (!bContractedSet.has(i)) bFree.push(i);
|
|
1088
|
+
const aPrimalT = new Tensor3(aShape, new Float64Array(this.primal));
|
|
1089
|
+
const bPrimalT = new Tensor3(bShape, new Float64Array(other.primal));
|
|
1090
|
+
const zPrimal = aPrimalT.tensordot(bPrimalT, axes);
|
|
1091
|
+
const zShape = [...zPrimal.shape];
|
|
1092
|
+
const naFree = aFree.length;
|
|
1093
|
+
const nbFree = bFree.length;
|
|
1094
|
+
const aPrimalData = new Float64Array(this.primal);
|
|
1095
|
+
const bPrimalData = new Float64Array(other.primal);
|
|
1096
|
+
const aGradSlot = this.tape.getInputGrad(this.id);
|
|
1097
|
+
const bGradSlot = this.tape.getInputGrad(other.id);
|
|
1098
|
+
const { id } = this.tape.record(
|
|
1099
|
+
[this.id, other.id],
|
|
1100
|
+
zPrimal.data.length,
|
|
1101
|
+
(outputGrad) => {
|
|
1102
|
+
const dZ = new Tensor3(zShape, new Float64Array(outputGrad));
|
|
1103
|
+
const aPrimalTbk = new Tensor3(aShape, aPrimalData);
|
|
1104
|
+
const bPrimalTbk = new Tensor3(bShape, bPrimalData);
|
|
1105
|
+
const dAaxes = [];
|
|
1106
|
+
for (let i = 0; i < nbFree; i++) {
|
|
1107
|
+
dAaxes.push([naFree + i, bFree[i]]);
|
|
1108
|
+
}
|
|
1109
|
+
const dAPartial = dZ.tensordot(bPrimalTbk, dAaxes);
|
|
1110
|
+
const bContractedSortedByOriginal = [...bContractedInPairOrder].sort((x, y) => x - y);
|
|
1111
|
+
const nPairs = aContractedInPairOrder.length;
|
|
1112
|
+
const tailPerm = [];
|
|
1113
|
+
for (let k = 0; k < nPairs; k++) {
|
|
1114
|
+
const bOrigAxis = bContractedInPairOrder[k];
|
|
1115
|
+
const idx = bContractedSortedByOriginal.indexOf(bOrigAxis);
|
|
1116
|
+
tailPerm.push(idx);
|
|
1117
|
+
}
|
|
1118
|
+
const dAFirstPerm = [];
|
|
1119
|
+
for (let i = 0; i < naFree; i++) dAFirstPerm.push(i);
|
|
1120
|
+
for (let k = 0; k < nPairs; k++) dAFirstPerm.push(naFree + tailPerm[k]);
|
|
1121
|
+
const isFirstPermIdentity = dAFirstPerm.every((v, idx) => v === idx);
|
|
1122
|
+
const dAReordered = isFirstPermIdentity ? dAPartial : dAPartial.transpose(dAFirstPerm);
|
|
1123
|
+
const scatterA = new Array(aRank);
|
|
1124
|
+
for (let a = 0; a < aRank; a++) {
|
|
1125
|
+
const freeIdx = aFree.indexOf(a);
|
|
1126
|
+
if (freeIdx !== -1) {
|
|
1127
|
+
scatterA[a] = freeIdx;
|
|
1128
|
+
} else {
|
|
1129
|
+
const contIdx = aContractedInPairOrder.indexOf(a);
|
|
1130
|
+
scatterA[a] = naFree + contIdx;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
const isScatterAIdentity = scatterA.every((v, idx) => v === idx);
|
|
1134
|
+
const dAFinal = isScatterAIdentity ? dAReordered : dAReordered.transpose(scatterA);
|
|
1135
|
+
for (let i = 0; i < aGradSlot.length; i++) {
|
|
1136
|
+
aGradSlot[i] += dAFinal.data[i];
|
|
1137
|
+
}
|
|
1138
|
+
const dBaxes = [];
|
|
1139
|
+
for (let i = 0; i < naFree; i++) {
|
|
1140
|
+
dBaxes.push([aFree[i], i]);
|
|
1141
|
+
}
|
|
1142
|
+
const dBPartial = aPrimalTbk.tensordot(dZ, dBaxes);
|
|
1143
|
+
const aContractedSortedByOriginal = [...aContractedInPairOrder].sort((x, y) => x - y);
|
|
1144
|
+
const dBHeadPerm = [];
|
|
1145
|
+
for (let k = 0; k < nPairs; k++) {
|
|
1146
|
+
const aOrigAxis = aContractedInPairOrder[k];
|
|
1147
|
+
const idx = aContractedSortedByOriginal.indexOf(aOrigAxis);
|
|
1148
|
+
dBHeadPerm.push(idx);
|
|
1149
|
+
}
|
|
1150
|
+
const dBFirstPerm = [];
|
|
1151
|
+
for (let k = 0; k < nPairs; k++) dBFirstPerm.push(dBHeadPerm[k]);
|
|
1152
|
+
for (let i = 0; i < nbFree; i++) dBFirstPerm.push(nPairs + i);
|
|
1153
|
+
const isDBFirstPermIdentity = dBFirstPerm.every((v, idx) => v === idx);
|
|
1154
|
+
const dBReordered = isDBFirstPermIdentity ? dBPartial : dBPartial.transpose(dBFirstPerm);
|
|
1155
|
+
const scatterB = new Array(bRank);
|
|
1156
|
+
for (let b = 0; b < bRank; b++) {
|
|
1157
|
+
const freeIdx = bFree.indexOf(b);
|
|
1158
|
+
if (freeIdx !== -1) {
|
|
1159
|
+
scatterB[b] = nPairs + freeIdx;
|
|
1160
|
+
} else {
|
|
1161
|
+
const contIdx = bContractedInPairOrder.indexOf(b);
|
|
1162
|
+
scatterB[b] = contIdx;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
const isScatterBIdentity = scatterB.every((v, idx) => v === idx);
|
|
1166
|
+
const dBFinal = isScatterBIdentity ? dBReordered : dBReordered.transpose(scatterB);
|
|
1167
|
+
for (let i = 0; i < bGradSlot.length; i++) {
|
|
1168
|
+
bGradSlot[i] += dBFinal.data[i];
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
);
|
|
1172
|
+
return new _TapedTensor(zShape, new Float64Array(zPrimal.data), this.tape, id);
|
|
1173
|
+
}
|
|
1174
|
+
// ---------------------------------------------------------------------------
|
|
1175
|
+
// SVD — full singular value decomposition on rank-2 input.
|
|
1176
|
+
// ---------------------------------------------------------------------------
|
|
1177
|
+
/**
|
|
1178
|
+
* Reverse-mode AD over the full SVD of a rank-2 matrix.
|
|
1179
|
+
*
|
|
1180
|
+
* Forward: `A = U · diag(S) · Vt`, where for input shape [m, n], k = min(m, n):
|
|
1181
|
+
* - U has shape [m, k]
|
|
1182
|
+
* - S has shape [k]
|
|
1183
|
+
* - Vt has shape [k, n] (Vt is V^T in the standard A = U Σ V^T convention,
|
|
1184
|
+
* i.e. its rows are right-singular-vector components)
|
|
1185
|
+
*
|
|
1186
|
+
* Returned TapedTensors share a single backward closure. When backward()
|
|
1187
|
+
* runs, it pulls dU, dS, dV from each output's gradient slot, assembles
|
|
1188
|
+
* dA, and writes to the input's gradient slot.
|
|
1189
|
+
*
|
|
1190
|
+
* Adjoint (real, distinct nonzero singular values, m = n square case;
|
|
1191
|
+
* extended to rectangular below). Derived directly from the forward
|
|
1192
|
+
* Jacobian; equivalent to PyTorch's `svd_backward`
|
|
1193
|
+
* (`aten/src/ATen/native/BatchLinearAlgebra.cpp`) and Townsend (2016)
|
|
1194
|
+
* "Differentiating the Singular Value Decomposition" §3:
|
|
1195
|
+
*
|
|
1196
|
+
* Let α = skew(U^T · dU), β = skew(V^T · dV) (k×k, antisymmetric)
|
|
1197
|
+
* Build C (k×k):
|
|
1198
|
+
* C[i,i] = dS[i]
|
|
1199
|
+
* C[i,j] = (α[i,j] + β[i,j]) / (s_j − s_i)
|
|
1200
|
+
* + (α[i,j] − β[i,j]) / (s_j + s_i) for i ≠ j
|
|
1201
|
+
* dA_in = U · C · V^T (m×n in-subspace part)
|
|
1202
|
+
*
|
|
1203
|
+
* Rectangular correction (when m > k, i.e. m > n):
|
|
1204
|
+
* dA += (I − U U^T) · dU · diag(1/s) · V^T
|
|
1205
|
+
* Rectangular correction (when n > k, i.e. n > m):
|
|
1206
|
+
* dA += U · diag(1/s) · dV^T · (I − V V^T)
|
|
1207
|
+
*
|
|
1208
|
+
* Regularisation at repeated/near-zero singular values (PyTorch-equivalent
|
|
1209
|
+
* subgradient choice): the (i,j) entry of C is masked to 0 whenever
|
|
1210
|
+
* `|s_j − s_i| < REL_TOL · max(|s|)` (the "difference" denominator) or
|
|
1211
|
+
* `|s_j + s_i| < REL_TOL · max(|s|)` (the "sum" denominator, only relevant
|
|
1212
|
+
* when both are ~0). REL_TOL = 1e-10. This makes the gradient a subgradient
|
|
1213
|
+
* at exact degeneracy — finite, but not the unique true derivative (which
|
|
1214
|
+
* does not exist at degeneracies). The rectangular correction also masks
|
|
1215
|
+
* 1/s_i when |s_i| < REL_TOL · max(|s|).
|
|
1216
|
+
*
|
|
1217
|
+
* Throws if input is not rank-2. For rank > 2 inputs the user should
|
|
1218
|
+
* reshape first.
|
|
1219
|
+
*/
|
|
1220
|
+
svd() {
|
|
1221
|
+
if (this.shape.length !== 2) {
|
|
1222
|
+
throw new Error(
|
|
1223
|
+
`TapedTensor.svd: expected rank-2 input, got rank ${this.shape.length}. Reshape the tensor to 2-D first.`
|
|
1224
|
+
);
|
|
1225
|
+
}
|
|
1226
|
+
const m = this.shape[0];
|
|
1227
|
+
const n = this.shape[1];
|
|
1228
|
+
const k = Math.min(m, n);
|
|
1229
|
+
const inputT = new Tensor3([m, n], new Float64Array(this.primal));
|
|
1230
|
+
const svdResult = tensorSvd(inputT, [0]);
|
|
1231
|
+
if (svdResult.truncatedDim !== k) {
|
|
1232
|
+
throw new Error(
|
|
1233
|
+
`TapedTensor.svd: internal \u2014 tensorSvd returned truncatedDim=${svdResult.truncatedDim}, expected ${k}. (A zero singular value was dropped by the default cutoff?)`
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
const uPrimal = new Float64Array(svdResult.U.data);
|
|
1237
|
+
const sPrimal = new Float64Array(svdResult.S.data);
|
|
1238
|
+
const vtPrimal = new Float64Array(svdResult.V.data);
|
|
1239
|
+
const dUBuf = new Float64Array(m * k);
|
|
1240
|
+
const dSBuf = new Float64Array(k);
|
|
1241
|
+
const dVtBuf = new Float64Array(k * n);
|
|
1242
|
+
const thisGradSlot = this.tape.getInputGrad(this.id);
|
|
1243
|
+
const sharedBackward = () => {
|
|
1244
|
+
const dA = _svdBackward(uPrimal, sPrimal, vtPrimal, dUBuf, dSBuf, dVtBuf, m, n, k);
|
|
1245
|
+
for (let i = 0; i < thisGradSlot.length; i++) {
|
|
1246
|
+
thisGradSlot[i] += dA[i];
|
|
1247
|
+
}
|
|
1248
|
+
};
|
|
1249
|
+
const { id: uId } = this.tape.record([this.id], m * k, (outputGrad) => {
|
|
1250
|
+
for (let i = 0; i < dUBuf.length; i++) dUBuf[i] += outputGrad[i];
|
|
1251
|
+
sharedBackward();
|
|
1252
|
+
});
|
|
1253
|
+
const { id: sId } = this.tape.record([this.id], k, (outputGrad) => {
|
|
1254
|
+
for (let i = 0; i < dSBuf.length; i++) dSBuf[i] += outputGrad[i];
|
|
1255
|
+
});
|
|
1256
|
+
const { id: vId } = this.tape.record([this.id], k * n, (outputGrad) => {
|
|
1257
|
+
for (let i = 0; i < dVtBuf.length; i++) dVtBuf[i] += outputGrad[i];
|
|
1258
|
+
});
|
|
1259
|
+
const uTape = new _TapedTensor([m, k], uPrimal, this.tape, uId);
|
|
1260
|
+
const sTape = new _TapedTensor([k], sPrimal, this.tape, sId);
|
|
1261
|
+
const vTape = new _TapedTensor([k, n], vtPrimal, this.tape, vId);
|
|
1262
|
+
return { U: uTape, S: sTape, V: vTape };
|
|
1263
|
+
}
|
|
1264
|
+
// ---------------------------------------------------------------------------
|
|
1265
|
+
// Symmetric eigendecomposition.
|
|
1266
|
+
// ---------------------------------------------------------------------------
|
|
1267
|
+
/**
|
|
1268
|
+
* Reverse-mode AD over the eigendecomposition of a rank-2 matrix.
|
|
1269
|
+
*
|
|
1270
|
+
* Symmetric path (`symmetric: true`):
|
|
1271
|
+
* Forward: `A = U · diag(Λ) · U^T` for symmetric A (n×n).
|
|
1272
|
+
* Adjoint (Magnus & Neudecker 1999 §10.6.6; PyTorch `linalg_eigh_backward`):
|
|
1273
|
+
* F[i,j] = 1 / (λ_j − λ_i) for i ≠ j, 0 otherwise (with degeneracy mask)
|
|
1274
|
+
* dA_raw = U · (diag(dΛ) + F ∘ (U^T · dU)) · U^T
|
|
1275
|
+
* dA = (dA_raw + dA_raw^T) / 2 (symmetrise)
|
|
1276
|
+
*
|
|
1277
|
+
* Non-symmetric path (`symmetric: false`):
|
|
1278
|
+
* Forward: `A = V · diag(λ) · V^{-1}` (V columns are right eigenvectors).
|
|
1279
|
+
* Adjoint (Magnus & Neudecker 1999 §10.6 / Giles 2008 §3.2 / Townsend 2016 §4;
|
|
1280
|
+
* cross-check: PyTorch `linalg_eig_backward`):
|
|
1281
|
+
* E[i,j] = 1 / (λ_j − λ_i) for i ≠ j, 0 otherwise (with degeneracy mask)
|
|
1282
|
+
* dA = V^{-T} · ( E ∘ (V^T · dV) + diag(dλ) ) · V^T
|
|
1283
|
+
*
|
|
1284
|
+
* Restrictions (all enforced — throw a clear error otherwise):
|
|
1285
|
+
* 1. Eigenvalues must be real. The underlying matrix-eig primitive returns
|
|
1286
|
+
* placeholder eigenvectors (not actual complex vectors) when complex
|
|
1287
|
+
* eigenvalues arise, so the adjoint formula cannot be evaluated. Real-
|
|
1288
|
+
* Schur differentiation would require complex arithmetic infrastructure
|
|
1289
|
+
* throughout the Tape/TapedTensor stack, which is out of scope.
|
|
1290
|
+
* 2. A must be diagonalisable (non-defective). The adjoint assumes V is
|
|
1291
|
+
* invertible; defective inputs have algebraic > geometric multiplicity
|
|
1292
|
+
* so V is rank-deficient. Detected by cond_∞(V) > 1e14.
|
|
1293
|
+
*
|
|
1294
|
+
* Regularisation at repeated eigenvalues (subgradient choice): mask
|
|
1295
|
+
* F/E[i,j] = 0 when `|λ_i − λ_j| < REL_TOL · max(|λ|)`. REL_TOL = 1e-10.
|
|
1296
|
+
*
|
|
1297
|
+
* Throws if input is not rank-2 or square, if `symmetric` is missing, or
|
|
1298
|
+
* (non-symmetric path) on complex eigenvalues / defective input.
|
|
1299
|
+
*/
|
|
1300
|
+
eig(opts) {
|
|
1301
|
+
if (!opts || typeof opts.symmetric !== "boolean") {
|
|
1302
|
+
throw new Error(
|
|
1303
|
+
"TapedTensor.eig: opts.symmetric must be set to true or false. Pass { symmetric: true } for symmetric A, { symmetric: false } for general A."
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1306
|
+
if (this.shape.length !== 2 || this.shape[0] !== this.shape[1]) {
|
|
1307
|
+
throw new Error(`TapedTensor.eig: expected rank-2 square input, got shape [${this.shape}].`);
|
|
1308
|
+
}
|
|
1309
|
+
const n = this.shape[0];
|
|
1310
|
+
if (opts.symmetric === true) {
|
|
1311
|
+
return this._eigSymmetric(n);
|
|
1312
|
+
}
|
|
1313
|
+
return this._eigGeneral(n);
|
|
1314
|
+
}
|
|
1315
|
+
_eigSymmetric(n) {
|
|
1316
|
+
const inputT = new Tensor3([n, n], new Float64Array(this.primal));
|
|
1317
|
+
const eigResult = tensorEig(inputT, [0], { symmetric: true, computeVectors: true });
|
|
1318
|
+
const lambdaPrimal = new Float64Array(eigResult.eigenvalues.data);
|
|
1319
|
+
if (!eigResult.eigenvectors) {
|
|
1320
|
+
throw new Error("TapedTensor.eig: tensorEig did not return eigenvectors");
|
|
1321
|
+
}
|
|
1322
|
+
const uPrimal = new Float64Array(eigResult.eigenvectors.data);
|
|
1323
|
+
const dLambdaBuf = new Float64Array(n);
|
|
1324
|
+
const dUBuf = new Float64Array(n * n);
|
|
1325
|
+
const thisGradSlot = this.tape.getInputGrad(this.id);
|
|
1326
|
+
const sharedBackward = () => {
|
|
1327
|
+
const dA = _eigSymBackward(uPrimal, lambdaPrimal, dUBuf, dLambdaBuf, n);
|
|
1328
|
+
for (let i = 0; i < thisGradSlot.length; i++) {
|
|
1329
|
+
thisGradSlot[i] += dA[i];
|
|
1330
|
+
}
|
|
1331
|
+
};
|
|
1332
|
+
const { id: lId } = this.tape.record([this.id], n, (outputGrad) => {
|
|
1333
|
+
for (let i = 0; i < dLambdaBuf.length; i++) dLambdaBuf[i] += outputGrad[i];
|
|
1334
|
+
sharedBackward();
|
|
1335
|
+
});
|
|
1336
|
+
const { id: uId } = this.tape.record([this.id], n * n, (outputGrad) => {
|
|
1337
|
+
for (let i = 0; i < dUBuf.length; i++) dUBuf[i] += outputGrad[i];
|
|
1338
|
+
});
|
|
1339
|
+
const lTape = new _TapedTensor([n], lambdaPrimal, this.tape, lId);
|
|
1340
|
+
const uTape = new _TapedTensor([n, n], uPrimal, this.tape, uId);
|
|
1341
|
+
return { eigvals: lTape, eigvecs: uTape };
|
|
1342
|
+
}
|
|
1343
|
+
_eigGeneral(n) {
|
|
1344
|
+
const inputT = new Tensor3([n, n], new Float64Array(this.primal));
|
|
1345
|
+
const eigResult = tensorEig(inputT, [0], { symmetric: false, computeVectors: true });
|
|
1346
|
+
if (eigResult.eigenvaluesImaginary) {
|
|
1347
|
+
const imData = eigResult.eigenvaluesImaginary.data;
|
|
1348
|
+
for (let i = 0; i < n; i++) {
|
|
1349
|
+
if (imData[i] !== 0) {
|
|
1350
|
+
throw new Error(
|
|
1351
|
+
"TapedTensor.eig({ symmetric: false }): input has complex eigenvalues. Reverse-mode AD over the general non-symmetric eig is restricted to real eigenvalues (the underlying matrix primitive does not return complex eigenvectors). Either symmetrise the input, or restrict to inputs with a real spectrum."
|
|
1352
|
+
);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
if (!eigResult.eigenvectors) {
|
|
1357
|
+
throw new Error("TapedTensor.eig: tensorEig did not return eigenvectors");
|
|
1358
|
+
}
|
|
1359
|
+
const lambdaPrimal = new Float64Array(eigResult.eigenvalues.data);
|
|
1360
|
+
const vPrimal = new Float64Array(eigResult.eigenvectors.data);
|
|
1361
|
+
const vInvOrNull = _invertWithCondCheck(vPrimal, n, 1e14);
|
|
1362
|
+
if (vInvOrNull === null) {
|
|
1363
|
+
throw new Error(
|
|
1364
|
+
"TapedTensor.eig({ symmetric: false }): input appears defective (eigenvector matrix V has cond_\u221E(V) > 1e14). The general-case adjoint requires diagonalisability. Consider perturbing the input or using the symmetric path if A is symmetric."
|
|
1365
|
+
);
|
|
1366
|
+
}
|
|
1367
|
+
const vInv = vInvOrNull;
|
|
1368
|
+
const dLambdaBuf = new Float64Array(n);
|
|
1369
|
+
const dVBuf = new Float64Array(n * n);
|
|
1370
|
+
const thisGradSlot = this.tape.getInputGrad(this.id);
|
|
1371
|
+
const sharedBackward = () => {
|
|
1372
|
+
const dA = _eigGeneralBackward(vPrimal, vInv, lambdaPrimal, dVBuf, dLambdaBuf, n);
|
|
1373
|
+
for (let i = 0; i < thisGradSlot.length; i++) {
|
|
1374
|
+
thisGradSlot[i] += dA[i];
|
|
1375
|
+
}
|
|
1376
|
+
};
|
|
1377
|
+
const { id: lId } = this.tape.record([this.id], n, (outputGrad) => {
|
|
1378
|
+
for (let i = 0; i < dLambdaBuf.length; i++) dLambdaBuf[i] += outputGrad[i];
|
|
1379
|
+
sharedBackward();
|
|
1380
|
+
});
|
|
1381
|
+
const { id: vId } = this.tape.record([this.id], n * n, (outputGrad) => {
|
|
1382
|
+
for (let i = 0; i < dVBuf.length; i++) dVBuf[i] += outputGrad[i];
|
|
1383
|
+
});
|
|
1384
|
+
const lTape = new _TapedTensor([n], lambdaPrimal, this.tape, lId);
|
|
1385
|
+
const vTape = new _TapedTensor([n, n], vPrimal, this.tape, vId);
|
|
1386
|
+
return { eigvals: lTape, eigvecs: vTape };
|
|
1387
|
+
}
|
|
265
1388
|
};
|
|
1389
|
+
function _svdBackward(U, s, Vt, dU, dS, dVt, m, n, k) {
|
|
1390
|
+
const REL_TOL = 1e-10;
|
|
1391
|
+
let sMax = 0;
|
|
1392
|
+
for (let i = 0; i < k; i++) {
|
|
1393
|
+
const a = Math.abs(s[i]);
|
|
1394
|
+
if (a > sMax) sMax = a;
|
|
1395
|
+
}
|
|
1396
|
+
const tolAbs = REL_TOL * (sMax > 0 ? sMax : 1);
|
|
1397
|
+
const V = new Float64Array(n * k);
|
|
1398
|
+
for (let i = 0; i < n; i++) {
|
|
1399
|
+
for (let j = 0; j < k; j++) {
|
|
1400
|
+
V[i * k + j] = Vt[j * n + i];
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
const dV = new Float64Array(n * k);
|
|
1404
|
+
for (let i = 0; i < n; i++) {
|
|
1405
|
+
for (let j = 0; j < k; j++) {
|
|
1406
|
+
dV[i * k + j] = dVt[j * n + i];
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
const UtdU = _matmulMNK(
|
|
1410
|
+
U,
|
|
1411
|
+
dU,
|
|
1412
|
+
m,
|
|
1413
|
+
k,
|
|
1414
|
+
k,
|
|
1415
|
+
/*aT=*/
|
|
1416
|
+
true,
|
|
1417
|
+
/*bT=*/
|
|
1418
|
+
false
|
|
1419
|
+
);
|
|
1420
|
+
const VtdV = _matmulMNK(
|
|
1421
|
+
V,
|
|
1422
|
+
dV,
|
|
1423
|
+
n,
|
|
1424
|
+
k,
|
|
1425
|
+
k,
|
|
1426
|
+
/*aT=*/
|
|
1427
|
+
true,
|
|
1428
|
+
/*bT=*/
|
|
1429
|
+
false
|
|
1430
|
+
);
|
|
1431
|
+
const C = new Float64Array(k * k);
|
|
1432
|
+
for (let i = 0; i < k; i++) {
|
|
1433
|
+
for (let j = 0; j < k; j++) {
|
|
1434
|
+
if (i === j) {
|
|
1435
|
+
C[i * k + j] = dS[i];
|
|
1436
|
+
} else {
|
|
1437
|
+
const alpha = (UtdU[i * k + j] - UtdU[j * k + i]) * 0.5;
|
|
1438
|
+
const beta = (VtdV[i * k + j] - VtdV[j * k + i]) * 0.5;
|
|
1439
|
+
const denomDiff = s[j] - s[i];
|
|
1440
|
+
const denomSum = s[j] + s[i];
|
|
1441
|
+
let term = 0;
|
|
1442
|
+
if (Math.abs(denomDiff) > tolAbs) {
|
|
1443
|
+
term += (alpha + beta) / denomDiff;
|
|
1444
|
+
}
|
|
1445
|
+
if (Math.abs(denomSum) > tolAbs) {
|
|
1446
|
+
term += (alpha - beta) / denomSum;
|
|
1447
|
+
}
|
|
1448
|
+
C[i * k + j] = term;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
const UC = _matmulMNK(U, C, m, k, k, false, false);
|
|
1453
|
+
const dA = _matmulMNK(UC, Vt, m, k, n, false, false);
|
|
1454
|
+
if (m > k) {
|
|
1455
|
+
const UUtdU = _matmulMNK(U, UtdU, m, k, k, false, false);
|
|
1456
|
+
const dUperp = new Float64Array(m * k);
|
|
1457
|
+
for (let i = 0; i < m * k; i++) dUperp[i] = dU[i] - UUtdU[i];
|
|
1458
|
+
for (let i = 0; i < m; i++) {
|
|
1459
|
+
for (let j = 0; j < k; j++) {
|
|
1460
|
+
const sj = s[j];
|
|
1461
|
+
if (Math.abs(sj) > tolAbs) {
|
|
1462
|
+
dUperp[i * k + j] /= sj;
|
|
1463
|
+
} else {
|
|
1464
|
+
dUperp[i * k + j] = 0;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
const corr = _matmulMNK(dUperp, Vt, m, k, n, false, false);
|
|
1469
|
+
for (let i = 0; i < dA.length; i++) dA[i] += corr[i];
|
|
1470
|
+
}
|
|
1471
|
+
if (n > k) {
|
|
1472
|
+
const VVtdV = _matmulMNK(V, VtdV, n, k, k, false, false);
|
|
1473
|
+
const dVperp = new Float64Array(n * k);
|
|
1474
|
+
for (let i = 0; i < n * k; i++) dVperp[i] = dV[i] - VVtdV[i];
|
|
1475
|
+
for (let i = 0; i < n; i++) {
|
|
1476
|
+
for (let j = 0; j < k; j++) {
|
|
1477
|
+
const sj = s[j];
|
|
1478
|
+
if (Math.abs(sj) > tolAbs) {
|
|
1479
|
+
dVperp[i * k + j] /= sj;
|
|
1480
|
+
} else {
|
|
1481
|
+
dVperp[i * k + j] = 0;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
const corr = _matmulMNK(U, dVperp, m, k, n, false, true);
|
|
1486
|
+
for (let i = 0; i < dA.length; i++) dA[i] += corr[i];
|
|
1487
|
+
}
|
|
1488
|
+
return dA;
|
|
1489
|
+
}
|
|
1490
|
+
function _eigSymBackward(U, lam, dU, dLam, n) {
|
|
1491
|
+
const REL_TOL = 1e-10;
|
|
1492
|
+
let lamMax = 0;
|
|
1493
|
+
for (let i = 0; i < n; i++) {
|
|
1494
|
+
const a = Math.abs(lam[i]);
|
|
1495
|
+
if (a > lamMax) lamMax = a;
|
|
1496
|
+
}
|
|
1497
|
+
const tolAbs = REL_TOL * (lamMax > 0 ? lamMax : 1);
|
|
1498
|
+
const UtdU = _matmulMNK(U, dU, n, n, n, true, false);
|
|
1499
|
+
const M = new Float64Array(n * n);
|
|
1500
|
+
for (let i = 0; i < n; i++) {
|
|
1501
|
+
for (let j = 0; j < n; j++) {
|
|
1502
|
+
if (i === j) {
|
|
1503
|
+
M[i * n + j] = dLam[i];
|
|
1504
|
+
} else {
|
|
1505
|
+
const denom = lam[j] - lam[i];
|
|
1506
|
+
if (Math.abs(denom) > tolAbs) {
|
|
1507
|
+
M[i * n + j] = UtdU[i * n + j] / denom;
|
|
1508
|
+
} else {
|
|
1509
|
+
M[i * n + j] = 0;
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
const UM = _matmulMNK(U, M, n, n, n, false, false);
|
|
1515
|
+
const dAraw = _matmulMNK(UM, U, n, n, n, false, true);
|
|
1516
|
+
const dA = new Float64Array(n * n);
|
|
1517
|
+
for (let i = 0; i < n; i++) {
|
|
1518
|
+
for (let j = 0; j < n; j++) {
|
|
1519
|
+
dA[i * n + j] = (dAraw[i * n + j] + dAraw[j * n + i]) * 0.5;
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
return dA;
|
|
1523
|
+
}
|
|
1524
|
+
function _eigGeneralBackward(V, vInv, lam, dV, dLam, n) {
|
|
1525
|
+
const REL_TOL = 1e-10;
|
|
1526
|
+
let lamMax = 0;
|
|
1527
|
+
for (let i = 0; i < n; i++) {
|
|
1528
|
+
const a = Math.abs(lam[i]);
|
|
1529
|
+
if (a > lamMax) lamMax = a;
|
|
1530
|
+
}
|
|
1531
|
+
const tolAbs = REL_TOL * (lamMax > 0 ? lamMax : 1);
|
|
1532
|
+
const VtdV = _matmulMNK(V, dV, n, n, n, true, false);
|
|
1533
|
+
const VtV = _matmulMNK(V, V, n, n, n, true, false);
|
|
1534
|
+
const diagVtdV = new Float64Array(n);
|
|
1535
|
+
for (let i = 0; i < n; i++) diagVtdV[i] = VtdV[i * n + i];
|
|
1536
|
+
for (let i = 0; i < n; i++) {
|
|
1537
|
+
for (let j = 0; j < n; j++) {
|
|
1538
|
+
VtdV[i * n + j] -= VtV[i * n + j] * diagVtdV[j];
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
const M = new Float64Array(n * n);
|
|
1542
|
+
for (let i = 0; i < n; i++) {
|
|
1543
|
+
for (let j = 0; j < n; j++) {
|
|
1544
|
+
if (i === j) {
|
|
1545
|
+
M[i * n + j] = dLam[i];
|
|
1546
|
+
} else {
|
|
1547
|
+
const denom = lam[j] - lam[i];
|
|
1548
|
+
if (Math.abs(denom) > tolAbs) {
|
|
1549
|
+
M[i * n + j] = VtdV[i * n + j] / denom;
|
|
1550
|
+
} else {
|
|
1551
|
+
M[i * n + j] = 0;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
const VinvT_M = _matmulMNK(vInv, M, n, n, n, true, false);
|
|
1557
|
+
const dA = _matmulMNK(VinvT_M, V, n, n, n, false, true);
|
|
1558
|
+
return dA;
|
|
1559
|
+
}
|
|
1560
|
+
function _invertWithCondCheck(V, n, maxCond) {
|
|
1561
|
+
const LU = new Float64Array(V);
|
|
1562
|
+
const perm = new Int32Array(n);
|
|
1563
|
+
for (let i = 0; i < n; i++) perm[i] = i;
|
|
1564
|
+
let pivotEpsScale = 0;
|
|
1565
|
+
for (let i = 0; i < n * n; i++) {
|
|
1566
|
+
const a = Math.abs(V[i]);
|
|
1567
|
+
if (a > pivotEpsScale) pivotEpsScale = a;
|
|
1568
|
+
}
|
|
1569
|
+
const singularEps = pivotEpsScale === 0 ? 0 : pivotEpsScale * 1e-300;
|
|
1570
|
+
for (let k = 0; k < n; k++) {
|
|
1571
|
+
let maxVal = Math.abs(LU[k * n + k]);
|
|
1572
|
+
let maxIdx = k;
|
|
1573
|
+
for (let i = k + 1; i < n; i++) {
|
|
1574
|
+
const v = Math.abs(LU[i * n + k]);
|
|
1575
|
+
if (v > maxVal) {
|
|
1576
|
+
maxVal = v;
|
|
1577
|
+
maxIdx = i;
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
if (maxIdx !== k) {
|
|
1581
|
+
for (let j = 0; j < n; j++) {
|
|
1582
|
+
const tmp = LU[k * n + j];
|
|
1583
|
+
LU[k * n + j] = LU[maxIdx * n + j];
|
|
1584
|
+
LU[maxIdx * n + j] = tmp;
|
|
1585
|
+
}
|
|
1586
|
+
const t = perm[k];
|
|
1587
|
+
perm[k] = perm[maxIdx];
|
|
1588
|
+
perm[maxIdx] = t;
|
|
1589
|
+
}
|
|
1590
|
+
const pivot = LU[k * n + k];
|
|
1591
|
+
if (!Number.isFinite(pivot) || Math.abs(pivot) <= singularEps) {
|
|
1592
|
+
return null;
|
|
1593
|
+
}
|
|
1594
|
+
for (let i = k + 1; i < n; i++) {
|
|
1595
|
+
LU[i * n + k] /= pivot;
|
|
1596
|
+
const lik = LU[i * n + k];
|
|
1597
|
+
for (let j = k + 1; j < n; j++) {
|
|
1598
|
+
LU[i * n + j] -= lik * LU[k * n + j];
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
const inv = new Float64Array(n * n);
|
|
1603
|
+
const y = new Float64Array(n);
|
|
1604
|
+
for (let col = 0; col < n; col++) {
|
|
1605
|
+
for (let i = 0; i < n; i++) {
|
|
1606
|
+
y[i] = perm[i] === col ? 1 : 0;
|
|
1607
|
+
}
|
|
1608
|
+
for (let i = 0; i < n; i++) {
|
|
1609
|
+
let s = y[i];
|
|
1610
|
+
for (let j = 0; j < i; j++) s -= LU[i * n + j] * y[j];
|
|
1611
|
+
y[i] = s;
|
|
1612
|
+
}
|
|
1613
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
1614
|
+
let s = y[i];
|
|
1615
|
+
for (let j = i + 1; j < n; j++) s -= LU[i * n + j] * inv[j * n + col];
|
|
1616
|
+
inv[i * n + col] = s / LU[i * n + i];
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
let normV = 0;
|
|
1620
|
+
let normInv = 0;
|
|
1621
|
+
for (let i = 0; i < n; i++) {
|
|
1622
|
+
let rowV = 0;
|
|
1623
|
+
let rowI = 0;
|
|
1624
|
+
for (let j = 0; j < n; j++) {
|
|
1625
|
+
rowV += Math.abs(V[i * n + j]);
|
|
1626
|
+
const inv_ij = inv[i * n + j];
|
|
1627
|
+
if (!Number.isFinite(inv_ij)) return null;
|
|
1628
|
+
rowI += Math.abs(inv_ij);
|
|
1629
|
+
}
|
|
1630
|
+
if (rowV > normV) normV = rowV;
|
|
1631
|
+
if (rowI > normInv) normInv = rowI;
|
|
1632
|
+
}
|
|
1633
|
+
const cond = normV * normInv;
|
|
1634
|
+
if (!Number.isFinite(cond) || cond > maxCond) return null;
|
|
1635
|
+
return inv;
|
|
1636
|
+
}
|
|
1637
|
+
function _matmulMNK(A, B, M, K, N, aT, bT) {
|
|
1638
|
+
const C = new Float64Array(M * N);
|
|
1639
|
+
for (let i = 0; i < M; i++) {
|
|
1640
|
+
for (let j = 0; j < N; j++) {
|
|
1641
|
+
let sum = 0;
|
|
1642
|
+
for (let q = 0; q < K; q++) {
|
|
1643
|
+
const aVal = aT ? A[q * M + i] : A[i * K + q];
|
|
1644
|
+
const bVal = bT ? B[j * K + q] : B[q * N + j];
|
|
1645
|
+
sum += aVal * bVal;
|
|
1646
|
+
}
|
|
1647
|
+
C[i * N + j] = sum;
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
return C;
|
|
1651
|
+
}
|
|
1652
|
+
function _resolveAxes(axis, rank) {
|
|
1653
|
+
if (axis === void 0) {
|
|
1654
|
+
return Array.from({ length: rank }, (_, i) => i);
|
|
1655
|
+
}
|
|
1656
|
+
if (typeof axis === "number") {
|
|
1657
|
+
return [axis];
|
|
1658
|
+
}
|
|
1659
|
+
return [...axis].sort((a, b) => a - b);
|
|
1660
|
+
}
|
|
1661
|
+
function _rowMajorStrides(shape) {
|
|
1662
|
+
const strides = new Array(shape.length);
|
|
1663
|
+
let acc = 1;
|
|
1664
|
+
for (let k = shape.length - 1; k >= 0; k--) {
|
|
1665
|
+
strides[k] = acc;
|
|
1666
|
+
acc *= shape[k];
|
|
1667
|
+
}
|
|
1668
|
+
return strides;
|
|
1669
|
+
}
|
|
1670
|
+
function _scatterToOriginalAxes(tensor, targetLabels) {
|
|
1671
|
+
const currentLabels = tensor.axisLabels;
|
|
1672
|
+
const rank = targetLabels.length;
|
|
1673
|
+
const perm = new Array(rank);
|
|
1674
|
+
for (let i = 0; i < rank; i++) {
|
|
1675
|
+
const j = currentLabels.findIndex((lbl) => lbl.matches(targetLabels[i]));
|
|
1676
|
+
if (j === -1) {
|
|
1677
|
+
throw new Error(
|
|
1678
|
+
`_scatterToOriginalAxes: target label ${targetLabels[i].toString()} not found in tensor axisLabels`
|
|
1679
|
+
);
|
|
1680
|
+
}
|
|
1681
|
+
perm[i] = j;
|
|
1682
|
+
}
|
|
1683
|
+
const isIdentity = perm.every((j, i) => j === i);
|
|
1684
|
+
if (isIdentity) {
|
|
1685
|
+
return new Float64Array(tensor.data);
|
|
1686
|
+
}
|
|
1687
|
+
const permuted = tensor.transpose(perm);
|
|
1688
|
+
return new Float64Array(permuted.data);
|
|
1689
|
+
}
|
|
1690
|
+
function _batchedMatmul(aData, bData, aShape, bShape) {
|
|
1691
|
+
const rank = aShape.length;
|
|
1692
|
+
const batchRank = rank - 2;
|
|
1693
|
+
const m = aShape[rank - 2];
|
|
1694
|
+
const k = aShape[rank - 1];
|
|
1695
|
+
const n = bShape[rank - 1];
|
|
1696
|
+
let batchSize = 1;
|
|
1697
|
+
for (let b = 0; b < batchRank; b++) batchSize *= aShape[b];
|
|
1698
|
+
const yData = new Float64Array(batchSize * m * n);
|
|
1699
|
+
for (let batch = 0; batch < batchSize; batch++) {
|
|
1700
|
+
const aOff = batch * m * k;
|
|
1701
|
+
const bOff = batch * k * n;
|
|
1702
|
+
const yOff = batch * m * n;
|
|
1703
|
+
for (let i = 0; i < m; i++) {
|
|
1704
|
+
for (let j = 0; j < n; j++) {
|
|
1705
|
+
let sum = 0;
|
|
1706
|
+
for (let q = 0; q < k; q++) {
|
|
1707
|
+
sum += aData[aOff + i * k + q] * bData[bOff + q * n + j];
|
|
1708
|
+
}
|
|
1709
|
+
yData[yOff + i * n + j] = sum;
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
return yData;
|
|
1714
|
+
}
|
|
1715
|
+
function _batchedMatmulGradA(dYData, bData, yShape, bShape) {
|
|
1716
|
+
const rank = yShape.length;
|
|
1717
|
+
const batchRank = rank - 2;
|
|
1718
|
+
const m = yShape[rank - 2];
|
|
1719
|
+
const n = yShape[rank - 1];
|
|
1720
|
+
const k = bShape[rank - 2];
|
|
1721
|
+
let batchSize = 1;
|
|
1722
|
+
for (let b = 0; b < batchRank; b++) batchSize *= yShape[b];
|
|
1723
|
+
const dAData = new Float64Array(batchSize * m * k);
|
|
1724
|
+
for (let batch = 0; batch < batchSize; batch++) {
|
|
1725
|
+
const dYOff = batch * m * n;
|
|
1726
|
+
const bOff = batch * k * n;
|
|
1727
|
+
const dAOff = batch * m * k;
|
|
1728
|
+
for (let i = 0; i < m; i++) {
|
|
1729
|
+
for (let q = 0; q < k; q++) {
|
|
1730
|
+
let sum = 0;
|
|
1731
|
+
for (let j = 0; j < n; j++) {
|
|
1732
|
+
sum += dYData[dYOff + i * n + j] * bData[bOff + q * n + j];
|
|
1733
|
+
}
|
|
1734
|
+
dAData[dAOff + i * k + q] = sum;
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
return dAData;
|
|
1739
|
+
}
|
|
1740
|
+
function _batchedMatmulGradB(aData, dYData, aShape, yShape) {
|
|
1741
|
+
const rank = aShape.length;
|
|
1742
|
+
const batchRank = rank - 2;
|
|
1743
|
+
const m = aShape[rank - 2];
|
|
1744
|
+
const k = aShape[rank - 1];
|
|
1745
|
+
const n = yShape[rank - 1];
|
|
1746
|
+
let batchSize = 1;
|
|
1747
|
+
for (let b = 0; b < batchRank; b++) batchSize *= aShape[b];
|
|
1748
|
+
const dBData = new Float64Array(batchSize * k * n);
|
|
1749
|
+
for (let batch = 0; batch < batchSize; batch++) {
|
|
1750
|
+
const aOff = batch * m * k;
|
|
1751
|
+
const dYOff = batch * m * n;
|
|
1752
|
+
const dBOff = batch * k * n;
|
|
1753
|
+
for (let q = 0; q < k; q++) {
|
|
1754
|
+
for (let j = 0; j < n; j++) {
|
|
1755
|
+
let sum = 0;
|
|
1756
|
+
for (let i = 0; i < m; i++) {
|
|
1757
|
+
sum += aData[aOff + i * k + q] * dYData[dYOff + i * n + j];
|
|
1758
|
+
}
|
|
1759
|
+
dBData[dBOff + q * n + j] = sum;
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
return dBData;
|
|
1764
|
+
}
|
|
266
1765
|
|
|
267
1766
|
// src/reverse-grad.ts
|
|
268
1767
|
import { Tensor as Tensor4 } from "@danielsimonjr/mathts-tensor";
|