catniff 0.2.3 → 0.2.5

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/dist/core.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export type TensorValue = number | TensorValue[];
2
2
  export interface TensorOptions {
3
- shape?: number[];
4
- strides?: number[];
3
+ shape?: readonly number[];
4
+ strides?: readonly number[];
5
5
  grad?: Tensor;
6
6
  requiresGrad?: boolean;
7
7
  gradFn?: Function;
@@ -9,20 +9,27 @@ export interface TensorOptions {
9
9
  }
10
10
  export declare class Tensor {
11
11
  value: number[] | number;
12
- shape: number[];
13
- strides: number[];
12
+ readonly shape: readonly number[];
13
+ readonly strides: readonly number[];
14
14
  grad?: Tensor;
15
15
  requiresGrad: boolean;
16
16
  gradFn: Function;
17
17
  children: Tensor[];
18
18
  constructor(value: TensorValue, options?: TensorOptions);
19
19
  static flatten(tensor: TensorValue): number[] | number;
20
- static getShape(tensor: TensorValue): number[];
21
- static getStrides(shape: number[]): number[];
22
- static padShape(stridesA: number[], stridesB: number[], shapeA: number[], shapeB: number[]): number[][];
23
- static broadcastShapes(shapeA: number[], shapeB: number[]): number[];
24
- static indexToCoords(index: number, shape: number[], strides: number[]): number[];
25
- static coordsToIndex(coords: number[], shape: number[], strides: number[]): number;
20
+ static getShape(tensor: TensorValue): readonly number[];
21
+ static getStrides(shape: readonly number[]): readonly number[];
22
+ static padShape(stridesA: readonly number[], stridesB: readonly number[], shapeA: readonly number[], shapeB: readonly number[]): [
23
+ readonly number[],
24
+ readonly number[],
25
+ readonly number[],
26
+ readonly number[]
27
+ ];
28
+ static broadcastShapes(shapeA: readonly number[], shapeB: readonly number[]): readonly number[];
29
+ static indexToCoords(index: number, shape: readonly number[], strides: readonly number[]): number[];
30
+ static coordsToUnbroadcastedIndex(coords: number[], shape: readonly number[], strides: readonly number[]): number;
31
+ static coordsToIndex(coords: number[], strides: readonly number[]): number;
32
+ static shapeToSize(shape: readonly number[]): number;
26
33
  static elementWiseAB(tA: Tensor, tB: Tensor, op: (tA: number, tB: number) => number): Tensor;
27
34
  static elementWiseSelf(tA: Tensor, op: (tA: number) => number): Tensor;
28
35
  elementWiseABDAG(other: TensorValue | Tensor, op: (a: number, b: number) => number, thisGrad?: (self: Tensor, other: Tensor, outGrad: Tensor) => Tensor, otherGrad?: (self: Tensor, other: Tensor, outGrad: Tensor) => Tensor): Tensor;
package/dist/core.js CHANGED
@@ -20,8 +20,13 @@ class Tensor {
20
20
  }
21
21
  // Utility to flatten an nD array to be 1D
22
22
  static flatten(tensor) {
23
+ // Handle scalar tensors
23
24
  if (typeof tensor === "number")
24
25
  return tensor;
26
+ // If value is already 1D, we just need to return the value ('s reference)
27
+ if (typeof tensor[0] === "number")
28
+ return tensor;
29
+ // Or else recursively traverse through the nD array to flatten
25
30
  const result = [];
26
31
  function traverse(arr) {
27
32
  if (typeof arr === "number") {
@@ -88,7 +93,7 @@ class Tensor {
88
93
  }
89
94
  return newShape;
90
95
  }
91
- // Convert flat index to array of coordinates
96
+ // Utility to convert flat index to array of coordinates
92
97
  static indexToCoords(index, shape, strides) {
93
98
  const coords = new Array(shape.length);
94
99
  let remaining = index;
@@ -100,17 +105,33 @@ class Tensor {
100
105
  }
101
106
  return coords;
102
107
  }
103
- // Convert array of coordinates to *unbroadcasted* flat index
104
- static coordsToIndex(coords, shape, strides) {
108
+ // Utility to convert array of coordinates to *unbroadcasted* flat index
109
+ static coordsToUnbroadcastedIndex(coords, shape, strides) {
105
110
  let index = 0;
106
111
  for (let i = 0; i < coords.length; i++) {
107
- const coord = coords[i];
108
112
  // Handle broadcasting
109
- const actualCoord = shape[i] === 1 ? 0 : coord;
113
+ const actualCoord = shape[i] === 1 ? 0 : coords[i];
110
114
  index += actualCoord * strides[i];
111
115
  }
112
116
  return index;
113
117
  }
118
+ // Utility to convert array of coordinates to flat index
119
+ static coordsToIndex(coords, strides) {
120
+ let index = 0;
121
+ for (let i = 0; i < coords.length; i++) {
122
+ index += coords[i] * strides[i];
123
+ }
124
+ return index;
125
+ }
126
+ // Utility to convert shape into 1D value array size
127
+ static shapeToSize(shape) {
128
+ let prod = 1;
129
+ for (let i = 0; i < shape.length; i++) {
130
+ prod *= shape[i];
131
+ }
132
+ return prod;
133
+ }
134
+ ;
114
135
  // Utility for binary (two operators involved) element-wise ops
115
136
  static elementWiseAB(tA, tB, op) {
116
137
  if (typeof tA.value === "number" && typeof tB.value === "number") {
@@ -127,15 +148,15 @@ class Tensor {
127
148
  const outputShape = Tensor.broadcastShapes(paddedAShape, paddedBShape);
128
149
  // Get other output info
129
150
  const outputStrides = Tensor.getStrides(outputShape);
130
- const outputSize = outputShape.reduce((a, b) => a * b, 1);
151
+ const outputSize = Tensor.shapeToSize(outputShape);
131
152
  const outputValue = new Array(outputSize);
132
153
  for (let i = 0; i < outputSize; i++) {
133
154
  // Get coordinates from 1D index
134
155
  const coordsOutput = Tensor.indexToCoords(i, outputShape, outputStrides);
135
156
  // Convert the coordinates to 1D index of flattened A with respect to A's shape
136
- const indexA = Tensor.coordsToIndex(coordsOutput, paddedAShape, paddedAStrides);
157
+ const indexA = Tensor.coordsToUnbroadcastedIndex(coordsOutput, paddedAShape, paddedAStrides);
137
158
  // Convert the coordinates to 1D index of flattened B with respect to B's shape
138
- const indexB = Tensor.coordsToIndex(coordsOutput, paddedBShape, paddedBStrides);
159
+ const indexB = Tensor.coordsToUnbroadcastedIndex(coordsOutput, paddedBShape, paddedBStrides);
139
160
  // Calculate with op
140
161
  outputValue[i] = op(tA.value[indexA], tB.value[indexB]);
141
162
  }
@@ -148,7 +169,11 @@ class Tensor {
148
169
  static elementWiseSelf(tA, op) {
149
170
  if (typeof tA.value === "number")
150
171
  return new Tensor(op(tA.value));
151
- return new Tensor(tA.value.map(el => op(el)), { shape: [...tA.shape], strides: [...tA.strides] });
172
+ const newValue = new Array(tA.value.length);
173
+ for (let index = 0; index < tA.value.length; index++) {
174
+ newValue[index] = op(tA.value[index]);
175
+ }
176
+ return new Tensor(newValue, { shape: tA.shape, strides: tA.strides });
152
177
  }
153
178
  // Utility to do element-wise operation and build a dag node with another tensor
154
179
  elementWiseABDAG(other, op, thisGrad = () => new Tensor(0), otherGrad = () => new Tensor(0)) {
@@ -312,15 +337,18 @@ class Tensor {
312
337
  if (typeof dims === "undefined") {
313
338
  dims = Array.from({ length: this.shape.length }, (_, index) => index);
314
339
  }
340
+ // Dims that are reduced now have size-1
315
341
  const outputShape = this.shape.map((dim, i) => dims.includes(i) ? 1 : dim);
316
342
  const outputStrides = Tensor.getStrides(outputShape);
317
- const outputSize = outputShape.reduce((a, b) => a * b, 1);
343
+ const outputSize = Tensor.shapeToSize(outputShape);
318
344
  const outputValue = new Array(outputSize).fill(0);
319
- const originalSize = this.shape.reduce((a, b) => a * b, 1);
345
+ const originalSize = Tensor.shapeToSize(this.shape);
346
+ // Gradient data
320
347
  let gradShape, gradStrides, gradValue = [];
348
+ // Allocate gradient data only when needed
321
349
  if (this.requiresGrad) {
322
- gradShape = [...this.shape];
323
- gradStrides = [...this.strides];
350
+ gradShape = this.shape;
351
+ gradStrides = this.strides;
324
352
  gradValue = new Array(originalSize).fill(0);
325
353
  }
326
354
  for (let index = 0; index < originalSize; index++) {
@@ -328,11 +356,11 @@ class Tensor {
328
356
  // Force 0 on reduced axes to collapse into size-1 dims
329
357
  const outCoords = coords.map((val, i) => dims.includes(i) ? 0 : val);
330
358
  // Convert output coordinates to flat index
331
- const outFlatIndex = outCoords.reduce((acc, val, i) => acc + val * outputStrides[i], 0);
332
- // Accumulate
333
- const realFlatIndex = coords.reduce((acc, val, i) => acc + val * this.strides[i], 0);
359
+ const outFlatIndex = Tensor.coordsToIndex(outCoords, outputStrides);
360
+ // Accumulate, outFlatIndex should match multiple realFlatIndexes
361
+ const realFlatIndex = Tensor.coordsToIndex(coords, this.strides);
334
362
  outputValue[outFlatIndex] += this.value[realFlatIndex];
335
- // Mark for gradient
363
+ // Mark for gradient if needed
336
364
  if (this.requiresGrad) {
337
365
  (gradValue)[realFlatIndex] = 1;
338
366
  }
@@ -535,7 +563,7 @@ class Tensor {
535
563
  }
536
564
  // If same dimension, return copy
537
565
  if (dim1 === dim2) {
538
- return new Tensor(this.value, { shape: [...this.shape], strides: [...this.strides] });
566
+ return new Tensor(this.value, { shape: this.shape, strides: this.strides });
539
567
  }
540
568
  // Create new shape and strides by swapping
541
569
  const newShape = [...this.shape];
@@ -569,6 +597,7 @@ class Tensor {
569
597
  if (this.shape.length !== 1 || other.shape.length !== 1) {
570
598
  throw new Error("Inputs are not 1D tensors");
571
599
  }
600
+ // Simple vector dot product
572
601
  const vectLen = this.shape[0];
573
602
  const vectA = this.value;
574
603
  const vectB = other.value;
@@ -606,6 +635,7 @@ class Tensor {
606
635
  if (this.shape.length !== 2 || other.shape.length !== 2) {
607
636
  throw new Error("Inputs are not matrices");
608
637
  }
638
+ // Simple matrix multiplication
609
639
  const matA = this.value;
610
640
  const matB = other.value;
611
641
  const matAStrides = this.strides;
@@ -618,11 +648,12 @@ class Tensor {
618
648
  throw new Error("Invalid matrices shape for multiplication");
619
649
  const matCShape = [matARows, matBCols];
620
650
  const matCStrides = Tensor.getStrides(matCShape);
621
- const matCSize = matCShape.reduce((a, b) => a * b, 1);
651
+ const matCSize = Tensor.shapeToSize(matCShape);
622
652
  const matC = new Array(matCSize).fill(0);
623
653
  for (let i = 0; i < matARows; i++) {
624
654
  for (let j = 0; j < matBCols; j++) {
625
655
  for (let k = 0; k < matACols; k++) {
656
+ // Tensor values are 1D arrays so we have to get real index using strides
626
657
  matC[i * matCStrides[0] + j * matCStrides[1]] +=
627
658
  matA[i * matAStrides[0] + k * matAStrides[1]] *
628
659
  matB[k * matBStrides[0] + j * matBStrides[1]];
@@ -660,7 +691,7 @@ class Tensor {
660
691
  throw new Error("Input is not a 2D and 1D tensor pair");
661
692
  }
662
693
  // MM with no grad
663
- const thisMat = new Tensor(this.value, { shape: [...this.shape], strides: [...this.strides] });
694
+ const thisMat = new Tensor(this.value, { shape: this.shape, strides: this.strides });
664
695
  const otherMat = new Tensor(other.value, { shape: [other.shape[0], 1], strides: [other.strides[0], 1] });
665
696
  const out = thisMat.mm(otherMat).squeeze(1);
666
697
  // Handle grad with original tensors
@@ -708,7 +739,7 @@ class Tensor {
708
739
  static fullLike(tensor, num, options = {}) {
709
740
  if (typeof tensor.value === "number")
710
741
  return new Tensor(num, options);
711
- return new Tensor(tensor.value.map(el => num), { shape: [...tensor.shape], strides: [...tensor.strides], ...options });
742
+ return new Tensor(new Array(tensor.value.length).fill(num), { shape: tensor.shape, strides: tensor.strides, ...options });
712
743
  }
713
744
  // Reverse-mode autodiff call
714
745
  backward() {
@@ -718,7 +749,7 @@ class Tensor {
718
749
  function build(node) {
719
750
  if (!visited.has(node) && node.requiresGrad) {
720
751
  visited.add(node);
721
- node.grad = Tensor.fullLike(node, 0);
752
+ node.grad = Tensor.fullLike(node, 0); // Reset grad with 0
722
753
  for (let child of node.children)
723
754
  build(child);
724
755
  topo.push(node);
@@ -731,7 +762,7 @@ class Tensor {
731
762
  topo[index].gradFn();
732
763
  }
733
764
  }
734
- // Returns the number/nD array form of tensor
765
+ // Returns the raw number/nD array form of tensor
735
766
  val() {
736
767
  if (typeof this.value === "number")
737
768
  return this.value;
@@ -753,11 +784,11 @@ class Tensor {
753
784
  }
754
785
  return buildNested(this.value, this.shape, this.strides);
755
786
  }
756
- // Returns a copy of the tensor with gradient turned on/off
787
+ // Returns a copy of the tensor with gradient turned on/off and detaches from autograd
757
788
  withGrad(requiresGrad) {
758
789
  return new Tensor(this.value, {
759
- shape: [...this.shape],
760
- strides: [...this.strides],
790
+ shape: this.shape,
791
+ strides: this.strides,
761
792
  requiresGrad
762
793
  });
763
794
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "catniff",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "A cute autograd engine for Javascript",
5
5
  "main": "index.js",
6
6
  "scripts": {