@dniskav/neuron 0.3.0 → 0.3.1
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 +88 -1
- package/dist/index.d.mts +118 -1
- package/dist/index.d.ts +118 -1
- package/dist/index.js +755 -0
- package/dist/index.mjs +749 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -22,12 +22,14 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
Adam: () => Adam,
|
|
24
24
|
AttentionHead: () => AttentionHead,
|
|
25
|
+
Augmenter: () => Augmenter,
|
|
25
26
|
Autoencoder: () => Autoencoder,
|
|
26
27
|
BatchNorm: () => BatchNorm,
|
|
27
28
|
BiasVector: () => BiasVector,
|
|
28
29
|
CausalConv1D: () => CausalConv1D,
|
|
29
30
|
ClipOptimizer: () => ClipOptimizer,
|
|
30
31
|
ClippedOptimizerFactory: () => ClippedOptimizerFactory,
|
|
32
|
+
ContrastiveLearning: () => ContrastiveLearning,
|
|
31
33
|
Conv1D: () => Conv1D,
|
|
32
34
|
Conv2D: () => Conv2D,
|
|
33
35
|
DataAugmentation: () => DataAugmentation,
|
|
@@ -46,6 +48,7 @@ __export(index_exports, {
|
|
|
46
48
|
LSTMLayer: () => LSTMLayer,
|
|
47
49
|
Layer: () => Layer,
|
|
48
50
|
LayerNorm: () => LayerNorm,
|
|
51
|
+
LearnedPositionalEncoding: () => LearnedPositionalEncoding,
|
|
49
52
|
LinearRegression: () => LinearRegression,
|
|
50
53
|
LogisticRegression: () => LogisticRegression,
|
|
51
54
|
LossPlotter: () => LossPlotter,
|
|
@@ -62,18 +65,21 @@ __export(index_exports, {
|
|
|
62
65
|
NeuronN: () => NeuronN,
|
|
63
66
|
PCA: () => PCA,
|
|
64
67
|
Perceptron: () => Perceptron,
|
|
68
|
+
PositionalEncoding: () => PositionalEncoding,
|
|
65
69
|
RNN: () => RNN,
|
|
66
70
|
SGD: () => SGD,
|
|
67
71
|
SOM: () => SOM,
|
|
68
72
|
Seq2Seq: () => Seq2Seq,
|
|
69
73
|
SoftmaxRegression: () => SoftmaxRegression,
|
|
70
74
|
TCN: () => TCN,
|
|
75
|
+
TSNE: () => TSNE,
|
|
71
76
|
Trainer: () => Trainer,
|
|
72
77
|
TransformerBlock: () => TransformerBlock,
|
|
73
78
|
VAE: () => VAE,
|
|
74
79
|
Value: () => Value,
|
|
75
80
|
WeightInspector: () => WeightInspector,
|
|
76
81
|
WeightMatrix: () => WeightMatrix,
|
|
82
|
+
Word2Vec: () => Word2Vec,
|
|
77
83
|
accuracy: () => accuracy,
|
|
78
84
|
auc: () => auc,
|
|
79
85
|
classificationReport: () => classificationReport,
|
|
@@ -4850,6 +4856,749 @@ var TCN = class {
|
|
|
4850
4856
|
}
|
|
4851
4857
|
};
|
|
4852
4858
|
|
|
4859
|
+
// src/Word2Vec.ts
|
|
4860
|
+
var Word2Vec = class {
|
|
4861
|
+
constructor(embeddingDim = 50, options = {}) {
|
|
4862
|
+
this._trained = false;
|
|
4863
|
+
this.embeddingDim = embeddingDim;
|
|
4864
|
+
this._windowSize = options.windowSize ?? 2;
|
|
4865
|
+
this._model = options.model ?? "skipgram";
|
|
4866
|
+
this._minCount = options.minCount ?? 1;
|
|
4867
|
+
this.embeddings = [];
|
|
4868
|
+
this._W2 = [];
|
|
4869
|
+
this.vocab = /* @__PURE__ */ new Map();
|
|
4870
|
+
this._indexToWord = [];
|
|
4871
|
+
this.vocabSize = 0;
|
|
4872
|
+
}
|
|
4873
|
+
// ── buildVocab ─────────────────────────────────────────────────────────────
|
|
4874
|
+
// Scans the corpus, counts word frequencies, discards rare words (< minCount),
|
|
4875
|
+
// and assigns each remaining word a unique integer index.
|
|
4876
|
+
buildVocab(sentences) {
|
|
4877
|
+
const freq = /* @__PURE__ */ new Map();
|
|
4878
|
+
for (const sentence of sentences) {
|
|
4879
|
+
for (const word of sentence) {
|
|
4880
|
+
freq.set(word, (freq.get(word) ?? 0) + 1);
|
|
4881
|
+
}
|
|
4882
|
+
}
|
|
4883
|
+
this.vocab = /* @__PURE__ */ new Map();
|
|
4884
|
+
this._indexToWord = [];
|
|
4885
|
+
for (const [word, count] of freq) {
|
|
4886
|
+
if (count >= this._minCount) {
|
|
4887
|
+
const idx = this._indexToWord.length;
|
|
4888
|
+
this.vocab.set(word, idx);
|
|
4889
|
+
this._indexToWord.push(word);
|
|
4890
|
+
}
|
|
4891
|
+
}
|
|
4892
|
+
this.vocabSize = this._indexToWord.length;
|
|
4893
|
+
if (this.vocabSize === 0) {
|
|
4894
|
+
throw new Error("Word2Vec.buildVocab: vocabulary is empty after applying minCount filter");
|
|
4895
|
+
}
|
|
4896
|
+
const scale1 = Math.sqrt(1 / this.embeddingDim);
|
|
4897
|
+
const scale2 = Math.sqrt(1 / this.vocabSize);
|
|
4898
|
+
this.embeddings = Array.from(
|
|
4899
|
+
{ length: this.vocabSize },
|
|
4900
|
+
() => Array.from({ length: this.embeddingDim }, () => (Math.random() * 2 - 1) * scale1)
|
|
4901
|
+
);
|
|
4902
|
+
this._W2 = Array.from(
|
|
4903
|
+
{ length: this.embeddingDim },
|
|
4904
|
+
() => Array.from({ length: this.vocabSize }, () => (Math.random() * 2 - 1) * scale2)
|
|
4905
|
+
);
|
|
4906
|
+
this._trained = false;
|
|
4907
|
+
}
|
|
4908
|
+
// ── tokenize ───────────────────────────────────────────────────────────────
|
|
4909
|
+
// Simple tokenizer: lowercase, strip punctuation, split on whitespace.
|
|
4910
|
+
// Returns an array of tokens suitable for buildVocab / train.
|
|
4911
|
+
static tokenize(text) {
|
|
4912
|
+
return text.toLowerCase().replace(/[^a-z0-9\s'-]/g, " ").split(/\s+/).filter((t) => t.length > 0);
|
|
4913
|
+
}
|
|
4914
|
+
// ── train ──────────────────────────────────────────────────────────────────
|
|
4915
|
+
// Runs SGD over all (center, context) pairs in the corpus for `epochs` passes.
|
|
4916
|
+
// Returns the average cross-entropy loss per epoch.
|
|
4917
|
+
//
|
|
4918
|
+
// Note: uses full-vocabulary softmax (not negative sampling) for educational
|
|
4919
|
+
// clarity. This is O(vocabSize) per step — for large vocabularies you would
|
|
4920
|
+
// normally switch to negative sampling or hierarchical softmax.
|
|
4921
|
+
train(sentences, lr = 0.025, epochs = 5) {
|
|
4922
|
+
if (this.vocabSize === 0) this.buildVocab(sentences);
|
|
4923
|
+
const lossHistory = [];
|
|
4924
|
+
for (let epoch = 0; epoch < epochs; epoch++) {
|
|
4925
|
+
let totalLoss = 0;
|
|
4926
|
+
let nPairs = 0;
|
|
4927
|
+
for (const sentence of sentences) {
|
|
4928
|
+
const indices = sentence.map((w) => this.vocab.get(w)).filter((idx) => idx !== void 0);
|
|
4929
|
+
for (let t = 0; t < indices.length; t++) {
|
|
4930
|
+
const centerIdx = indices[t];
|
|
4931
|
+
const contextIndices = [];
|
|
4932
|
+
for (let offset = -this._windowSize; offset <= this._windowSize; offset++) {
|
|
4933
|
+
if (offset === 0) continue;
|
|
4934
|
+
const pos = t + offset;
|
|
4935
|
+
if (pos >= 0 && pos < indices.length) {
|
|
4936
|
+
contextIndices.push(indices[pos]);
|
|
4937
|
+
}
|
|
4938
|
+
}
|
|
4939
|
+
if (contextIndices.length === 0) continue;
|
|
4940
|
+
if (this._model === "skipgram") {
|
|
4941
|
+
for (const contextIdx of contextIndices) {
|
|
4942
|
+
totalLoss += this._skipgramStep(centerIdx, contextIdx, lr);
|
|
4943
|
+
nPairs++;
|
|
4944
|
+
}
|
|
4945
|
+
} else {
|
|
4946
|
+
totalLoss += this._cbowStep(centerIdx, contextIndices, lr);
|
|
4947
|
+
nPairs++;
|
|
4948
|
+
}
|
|
4949
|
+
}
|
|
4950
|
+
}
|
|
4951
|
+
lossHistory.push(nPairs > 0 ? totalLoss / nPairs : 0);
|
|
4952
|
+
}
|
|
4953
|
+
this._trained = true;
|
|
4954
|
+
return lossHistory;
|
|
4955
|
+
}
|
|
4956
|
+
// ── getEmbedding ───────────────────────────────────────────────────────────
|
|
4957
|
+
// Returns the learned embedding vector for a word. Throws if unknown.
|
|
4958
|
+
getEmbedding(word) {
|
|
4959
|
+
const idx = this.vocab.get(word);
|
|
4960
|
+
if (idx === void 0) throw new Error(`Word2Vec: unknown word "${word}"`);
|
|
4961
|
+
return this.embeddings[idx];
|
|
4962
|
+
}
|
|
4963
|
+
// ── similarity ─────────────────────────────────────────────────────────────
|
|
4964
|
+
// Cosine similarity between two words.
|
|
4965
|
+
// cos(v1, v2) = (v1 · v2) / (‖v1‖ · ‖v2‖)
|
|
4966
|
+
// Returns a value in [-1, 1]. Higher → more similar context usage.
|
|
4967
|
+
similarity(word1, word2) {
|
|
4968
|
+
const v1 = this.getEmbedding(word1);
|
|
4969
|
+
const v2 = this.getEmbedding(word2);
|
|
4970
|
+
return this._cosine(v1, v2);
|
|
4971
|
+
}
|
|
4972
|
+
// ── mostSimilar ────────────────────────────────────────────────────────────
|
|
4973
|
+
// Returns the topK words (excluding `word` itself) sorted by cosine similarity.
|
|
4974
|
+
mostSimilar(word, topK = 10) {
|
|
4975
|
+
const v = this.getEmbedding(word);
|
|
4976
|
+
return this._nearestByVector(v, topK, /* @__PURE__ */ new Set([word]));
|
|
4977
|
+
}
|
|
4978
|
+
// ── analogy ───────────────────────────────────────────────────────────────
|
|
4979
|
+
// Vector arithmetic analogy: positive1 - negative + positive2 ≈ result
|
|
4980
|
+
//
|
|
4981
|
+
// getAnalogy('king', 'man', 'woman') finds the word closest to
|
|
4982
|
+
// vec('king') - vec('man') + vec('woman') ≈ vec('queen')
|
|
4983
|
+
//
|
|
4984
|
+
// The result is excluded from the input words so they don't pollute the top-K.
|
|
4985
|
+
analogy(positive1, negative, positive2, topK = 5) {
|
|
4986
|
+
const vPos1 = this.getEmbedding(positive1);
|
|
4987
|
+
const vNeg = this.getEmbedding(negative);
|
|
4988
|
+
const vPos2 = this.getEmbedding(positive2);
|
|
4989
|
+
const target = vPos1.map((v, i) => v - vNeg[i] + vPos2[i]);
|
|
4990
|
+
const exclude = /* @__PURE__ */ new Set([positive1, negative, positive2]);
|
|
4991
|
+
return this._nearestByVector(target, topK, exclude);
|
|
4992
|
+
}
|
|
4993
|
+
// ── Private: skip-gram step ───────────────────────────────────────────────
|
|
4994
|
+
// Forward + backward for one (center, target) pair.
|
|
4995
|
+
// Returns the cross-entropy loss for this pair.
|
|
4996
|
+
_skipgramStep(centerIdx, targetIdx, lr) {
|
|
4997
|
+
const h = this.embeddings[centerIdx];
|
|
4998
|
+
const scores = this._hiddenToScores(h);
|
|
4999
|
+
const probs = _softmax(scores);
|
|
5000
|
+
const loss = -Math.log(probs[targetIdx] + 1e-12);
|
|
5001
|
+
const err = probs.map((p, j) => j === targetIdx ? p - 1 : p);
|
|
5002
|
+
const dh = new Array(this.embeddingDim).fill(0);
|
|
5003
|
+
for (let d = 0; d < this.embeddingDim; d++) {
|
|
5004
|
+
for (let j = 0; j < this.vocabSize; j++) {
|
|
5005
|
+
this._W2[d][j] -= lr * h[d] * err[j];
|
|
5006
|
+
dh[d] += this._W2[d][j] * err[j];
|
|
5007
|
+
}
|
|
5008
|
+
}
|
|
5009
|
+
for (let d = 0; d < this.embeddingDim; d++) {
|
|
5010
|
+
this.embeddings[centerIdx][d] -= lr * dh[d];
|
|
5011
|
+
}
|
|
5012
|
+
return loss;
|
|
5013
|
+
}
|
|
5014
|
+
// ── Private: CBOW step ────────────────────────────────────────────────────
|
|
5015
|
+
// Forward + backward for one (contextIndices → centerIdx) pair.
|
|
5016
|
+
// h is the mean of all context embeddings. The gradient is distributed
|
|
5017
|
+
// equally back to each context word's embedding row.
|
|
5018
|
+
_cbowStep(centerIdx, contextIndices, lr) {
|
|
5019
|
+
const k = contextIndices.length;
|
|
5020
|
+
const h = new Array(this.embeddingDim).fill(0);
|
|
5021
|
+
for (const ci of contextIndices) {
|
|
5022
|
+
for (let d = 0; d < this.embeddingDim; d++) {
|
|
5023
|
+
h[d] += this.embeddings[ci][d];
|
|
5024
|
+
}
|
|
5025
|
+
}
|
|
5026
|
+
for (let d = 0; d < this.embeddingDim; d++) h[d] /= k;
|
|
5027
|
+
const scores = this._hiddenToScores(h);
|
|
5028
|
+
const probs = _softmax(scores);
|
|
5029
|
+
const loss = -Math.log(probs[centerIdx] + 1e-12);
|
|
5030
|
+
const err = probs.map((p, j) => j === centerIdx ? p - 1 : p);
|
|
5031
|
+
const dh = new Array(this.embeddingDim).fill(0);
|
|
5032
|
+
for (let d = 0; d < this.embeddingDim; d++) {
|
|
5033
|
+
for (let j = 0; j < this.vocabSize; j++) {
|
|
5034
|
+
this._W2[d][j] -= lr * h[d] * err[j];
|
|
5035
|
+
dh[d] += this._W2[d][j] * err[j];
|
|
5036
|
+
}
|
|
5037
|
+
}
|
|
5038
|
+
for (const ci of contextIndices) {
|
|
5039
|
+
for (let d = 0; d < this.embeddingDim; d++) {
|
|
5040
|
+
this.embeddings[ci][d] -= lr * dh[d] / k;
|
|
5041
|
+
}
|
|
5042
|
+
}
|
|
5043
|
+
return loss;
|
|
5044
|
+
}
|
|
5045
|
+
// Computes scores = h · W2 → [vocabSize]
|
|
5046
|
+
_hiddenToScores(h) {
|
|
5047
|
+
const scores = new Array(this.vocabSize).fill(0);
|
|
5048
|
+
for (let d = 0; d < this.embeddingDim; d++) {
|
|
5049
|
+
for (let j = 0; j < this.vocabSize; j++) {
|
|
5050
|
+
scores[j] += h[d] * this._W2[d][j];
|
|
5051
|
+
}
|
|
5052
|
+
}
|
|
5053
|
+
return scores;
|
|
5054
|
+
}
|
|
5055
|
+
// Returns topK words (from all embeddings) sorted by cosine similarity to v,
|
|
5056
|
+
// skipping any word in the exclude set.
|
|
5057
|
+
_nearestByVector(v, topK, exclude) {
|
|
5058
|
+
const results = [];
|
|
5059
|
+
for (let i = 0; i < this.vocabSize; i++) {
|
|
5060
|
+
const w = this._indexToWord[i];
|
|
5061
|
+
if (exclude.has(w)) continue;
|
|
5062
|
+
results.push({ word: w, score: this._cosine(v, this.embeddings[i]) });
|
|
5063
|
+
}
|
|
5064
|
+
results.sort((a, b) => b.score - a.score);
|
|
5065
|
+
return results.slice(0, topK);
|
|
5066
|
+
}
|
|
5067
|
+
// Cosine similarity: (v1 · v2) / (‖v1‖ · ‖v2‖)
|
|
5068
|
+
_cosine(v1, v2) {
|
|
5069
|
+
let dot = 0, n1 = 0, n2 = 0;
|
|
5070
|
+
for (let i = 0; i < v1.length; i++) {
|
|
5071
|
+
dot += v1[i] * v2[i];
|
|
5072
|
+
n1 += v1[i] * v1[i];
|
|
5073
|
+
n2 += v2[i] * v2[i];
|
|
5074
|
+
}
|
|
5075
|
+
const denom = Math.sqrt(n1) * Math.sqrt(n2);
|
|
5076
|
+
return denom < 1e-12 ? 0 : dot / denom;
|
|
5077
|
+
}
|
|
5078
|
+
};
|
|
5079
|
+
function _softmax(scores) {
|
|
5080
|
+
const max = Math.max(...scores);
|
|
5081
|
+
const exps = scores.map((s) => Math.exp(s - max));
|
|
5082
|
+
const sum = exps.reduce((a, b) => a + b, 0);
|
|
5083
|
+
return exps.map((e) => e / sum);
|
|
5084
|
+
}
|
|
5085
|
+
|
|
5086
|
+
// src/TSNE.ts
|
|
5087
|
+
var TSNE = class {
|
|
5088
|
+
constructor(options = {}) {
|
|
5089
|
+
// KL divergence tracked during the last fit() call.
|
|
5090
|
+
this._klDivergence = 0;
|
|
5091
|
+
// P matrix stored for kl() reporting.
|
|
5092
|
+
this._P = [];
|
|
5093
|
+
this._nComponents = options.nComponents ?? 2;
|
|
5094
|
+
this._perplexity = options.perplexity ?? 30;
|
|
5095
|
+
this._lr = options.lr ?? 200;
|
|
5096
|
+
this._nIter = options.nIter ?? 1e3;
|
|
5097
|
+
this._seed = options.seed;
|
|
5098
|
+
this.embedding = [];
|
|
5099
|
+
}
|
|
5100
|
+
// ── fit ────────────────────────────────────────────────────────────────────
|
|
5101
|
+
// Runs the full t-SNE algorithm on X (shape [n][d]).
|
|
5102
|
+
// Stores the result in this.embedding ([n][nComponents]).
|
|
5103
|
+
fit(X) {
|
|
5104
|
+
const n = X.length;
|
|
5105
|
+
if (n < 2) throw new Error("TSNE.fit: need at least 2 data points");
|
|
5106
|
+
if (this._perplexity >= n) {
|
|
5107
|
+
throw new Error(
|
|
5108
|
+
`TSNE.fit: perplexity (${this._perplexity}) must be less than n (${n})`
|
|
5109
|
+
);
|
|
5110
|
+
}
|
|
5111
|
+
const rng = this._seed !== void 0 ? _mulberry32(this._seed) : Math.random;
|
|
5112
|
+
const distSq = _pairwiseDistSq(X, n);
|
|
5113
|
+
const Pcond = this._computePcond(distSq, n);
|
|
5114
|
+
const P = _symmetrize(Pcond, n);
|
|
5115
|
+
this._P = P;
|
|
5116
|
+
let Y = Array.from({ length: n }, () => {
|
|
5117
|
+
return Array.from({ length: this._nComponents }, () => {
|
|
5118
|
+
const u1 = Math.max(rng(), 1e-12);
|
|
5119
|
+
const u2 = rng();
|
|
5120
|
+
const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
|
|
5121
|
+
return z * 0.01;
|
|
5122
|
+
});
|
|
5123
|
+
});
|
|
5124
|
+
let Yprev = Y.map((row) => [...row]);
|
|
5125
|
+
const EXAGGERATION_ITERS = 50;
|
|
5126
|
+
const EXAGGERATION_FACTOR = 4;
|
|
5127
|
+
const MOMENTUM_SWITCH = 20;
|
|
5128
|
+
for (let iter = 0; iter < this._nIter; iter++) {
|
|
5129
|
+
const momentum = iter < MOMENTUM_SWITCH ? 0.5 : 0.8;
|
|
5130
|
+
const pScale = iter < EXAGGERATION_ITERS ? EXAGGERATION_FACTOR : 1;
|
|
5131
|
+
const { Q, invDist } = _computeQ(Y, n, this._nComponents);
|
|
5132
|
+
const grad = Array.from(
|
|
5133
|
+
{ length: n },
|
|
5134
|
+
() => new Array(this._nComponents).fill(0)
|
|
5135
|
+
);
|
|
5136
|
+
for (let i = 0; i < n; i++) {
|
|
5137
|
+
for (let j = 0; j < n; j++) {
|
|
5138
|
+
if (i === j) continue;
|
|
5139
|
+
const pq = pScale * P[i][j] - Q[i][j];
|
|
5140
|
+
const c = 4 * pq * invDist[i][j];
|
|
5141
|
+
for (let d = 0; d < this._nComponents; d++) {
|
|
5142
|
+
grad[i][d] += c * (Y[i][d] - Y[j][d]);
|
|
5143
|
+
}
|
|
5144
|
+
}
|
|
5145
|
+
}
|
|
5146
|
+
const Ynext = Array.from(
|
|
5147
|
+
{ length: n },
|
|
5148
|
+
(_, i) => Array.from(
|
|
5149
|
+
{ length: this._nComponents },
|
|
5150
|
+
(_2, d) => Y[i][d] - this._lr * grad[i][d] + momentum * (Y[i][d] - Yprev[i][d])
|
|
5151
|
+
)
|
|
5152
|
+
);
|
|
5153
|
+
Yprev = Y;
|
|
5154
|
+
Y = Ynext;
|
|
5155
|
+
}
|
|
5156
|
+
this.embedding = Y;
|
|
5157
|
+
const { Q: Qfinal } = _computeQ(Y, n, this._nComponents);
|
|
5158
|
+
let kl = 0;
|
|
5159
|
+
for (let i = 0; i < n; i++) {
|
|
5160
|
+
for (let j = 0; j < n; j++) {
|
|
5161
|
+
if (i === j) continue;
|
|
5162
|
+
const p = P[i][j];
|
|
5163
|
+
if (p > 1e-12) {
|
|
5164
|
+
kl += p * Math.log(p / (Qfinal[i][j] + 1e-12));
|
|
5165
|
+
}
|
|
5166
|
+
}
|
|
5167
|
+
}
|
|
5168
|
+
this._klDivergence = kl;
|
|
5169
|
+
}
|
|
5170
|
+
// ── fitTransform ───────────────────────────────────────────────────────────
|
|
5171
|
+
// Convenience: fit() then return this.embedding.
|
|
5172
|
+
fitTransform(X) {
|
|
5173
|
+
this.fit(X);
|
|
5174
|
+
return this.embedding;
|
|
5175
|
+
}
|
|
5176
|
+
// ── kl ─────────────────────────────────────────────────────────────────────
|
|
5177
|
+
// Returns the KL divergence KL(P ‖ Q) from the last fit() call.
|
|
5178
|
+
// Lower is better. Useful for comparing perplexity settings or iteration counts.
|
|
5179
|
+
kl() {
|
|
5180
|
+
return this._klDivergence;
|
|
5181
|
+
}
|
|
5182
|
+
// ── Private: binary search for σi ─────────────────────────────────────────
|
|
5183
|
+
// For each point i, find σi such that the Shannon entropy of P(·|i) equals
|
|
5184
|
+
// log₂(perplexity). We use binary search on σ².
|
|
5185
|
+
_computePcond(distSq, n) {
|
|
5186
|
+
const targetEntropy = Math.log2(this._perplexity);
|
|
5187
|
+
const Pcond = Array.from({ length: n }, () => new Array(n).fill(0));
|
|
5188
|
+
for (let i = 0; i < n; i++) {
|
|
5189
|
+
let sigmaLo = 0;
|
|
5190
|
+
let sigmaHi = 1e10;
|
|
5191
|
+
let sigma2 = 1;
|
|
5192
|
+
for (let attempt = 0; attempt < 50; attempt++) {
|
|
5193
|
+
const dists = distSq[i];
|
|
5194
|
+
let sumExp = 0;
|
|
5195
|
+
const exps = new Array(n).fill(0);
|
|
5196
|
+
for (let j = 0; j < n; j++) {
|
|
5197
|
+
if (j === i) continue;
|
|
5198
|
+
const e = Math.exp(-dists[j] / (2 * sigma2));
|
|
5199
|
+
exps[j] = e;
|
|
5200
|
+
sumExp += e;
|
|
5201
|
+
}
|
|
5202
|
+
if (sumExp < 1e-12) break;
|
|
5203
|
+
let H = 0;
|
|
5204
|
+
for (let j = 0; j < n; j++) {
|
|
5205
|
+
if (j === i) continue;
|
|
5206
|
+
const p = exps[j] / sumExp;
|
|
5207
|
+
Pcond[i][j] = p;
|
|
5208
|
+
if (p > 1e-12) H -= p * Math.log2(p);
|
|
5209
|
+
}
|
|
5210
|
+
const delta = H - targetEntropy;
|
|
5211
|
+
if (Math.abs(delta) < 1e-5) break;
|
|
5212
|
+
if (delta > 0) {
|
|
5213
|
+
sigmaHi = sigma2;
|
|
5214
|
+
sigma2 = (sigmaLo + sigma2) / 2;
|
|
5215
|
+
} else {
|
|
5216
|
+
sigmaLo = sigma2;
|
|
5217
|
+
sigma2 = sigmaHi < 1e9 ? (sigma2 + sigmaHi) / 2 : sigma2 * 2;
|
|
5218
|
+
}
|
|
5219
|
+
}
|
|
5220
|
+
}
|
|
5221
|
+
return Pcond;
|
|
5222
|
+
}
|
|
5223
|
+
};
|
|
5224
|
+
function _pairwiseDistSq(X, n) {
|
|
5225
|
+
const D = Array.from({ length: n }, () => new Array(n).fill(0));
|
|
5226
|
+
for (let i = 0; i < n; i++) {
|
|
5227
|
+
for (let j = i + 1; j < n; j++) {
|
|
5228
|
+
let d = 0;
|
|
5229
|
+
for (let k = 0; k < X[i].length; k++) {
|
|
5230
|
+
const diff = X[i][k] - X[j][k];
|
|
5231
|
+
d += diff * diff;
|
|
5232
|
+
}
|
|
5233
|
+
D[i][j] = d;
|
|
5234
|
+
D[j][i] = d;
|
|
5235
|
+
}
|
|
5236
|
+
}
|
|
5237
|
+
return D;
|
|
5238
|
+
}
|
|
5239
|
+
function _symmetrize(Pcond, n) {
|
|
5240
|
+
const P = Array.from({ length: n }, () => new Array(n).fill(0));
|
|
5241
|
+
for (let i = 0; i < n; i++) {
|
|
5242
|
+
for (let j = 0; j < n; j++) {
|
|
5243
|
+
P[i][j] = (Pcond[i][j] + Pcond[j][i]) / (2 * n);
|
|
5244
|
+
}
|
|
5245
|
+
}
|
|
5246
|
+
return P;
|
|
5247
|
+
}
|
|
5248
|
+
function _computeQ(Y, n, nComponents) {
|
|
5249
|
+
const num = Array.from({ length: n }, () => new Array(n).fill(0));
|
|
5250
|
+
let Z = 0;
|
|
5251
|
+
for (let i = 0; i < n; i++) {
|
|
5252
|
+
for (let j = i + 1; j < n; j++) {
|
|
5253
|
+
let d2 = 0;
|
|
5254
|
+
for (let d = 0; d < nComponents; d++) {
|
|
5255
|
+
const diff = Y[i][d] - Y[j][d];
|
|
5256
|
+
d2 += diff * diff;
|
|
5257
|
+
}
|
|
5258
|
+
const inv = 1 / (1 + d2);
|
|
5259
|
+
num[i][j] = inv;
|
|
5260
|
+
num[j][i] = inv;
|
|
5261
|
+
Z += 2 * inv;
|
|
5262
|
+
}
|
|
5263
|
+
}
|
|
5264
|
+
if (Z < 1e-12) Z = 1e-12;
|
|
5265
|
+
const Q = Array.from(
|
|
5266
|
+
{ length: n },
|
|
5267
|
+
(_, i) => num[i].map((v) => v / Z)
|
|
5268
|
+
);
|
|
5269
|
+
return { Q, invDist: num };
|
|
5270
|
+
}
|
|
5271
|
+
function _mulberry32(seed) {
|
|
5272
|
+
let s = seed >>> 0;
|
|
5273
|
+
return function() {
|
|
5274
|
+
s = s + 1831565813 >>> 0;
|
|
5275
|
+
let z = s;
|
|
5276
|
+
z = Math.imul(z ^ z >>> 15, z | 1);
|
|
5277
|
+
z ^= z + Math.imul(z ^ z >>> 7, z | 61);
|
|
5278
|
+
z = (z ^ z >>> 14) >>> 0;
|
|
5279
|
+
return z / 4294967296;
|
|
5280
|
+
};
|
|
5281
|
+
}
|
|
5282
|
+
|
|
5283
|
+
// src/PositionalEncoding.ts
|
|
5284
|
+
var PositionalEncoding = class _PositionalEncoding {
|
|
5285
|
+
// Compute the full PE vector for one token at position `pos`.
|
|
5286
|
+
// Returns an array of length `dModel`.
|
|
5287
|
+
//
|
|
5288
|
+
// Each pair of dimensions (2i, 2i+1) shares the same frequency 1/10000^(2i/dModel)
|
|
5289
|
+
// but is 90° out of phase (sin vs cos), which ensures no two positions produce
|
|
5290
|
+
// the identical vector.
|
|
5291
|
+
static encode(pos, dModel) {
|
|
5292
|
+
const pe = new Array(dModel);
|
|
5293
|
+
for (let i = 0; i < Math.floor(dModel / 2); i++) {
|
|
5294
|
+
const freq = Math.pow(1e4, 2 * i / dModel);
|
|
5295
|
+
pe[2 * i] = Math.sin(pos / freq);
|
|
5296
|
+
pe[2 * i + 1] = Math.cos(pos / freq);
|
|
5297
|
+
}
|
|
5298
|
+
if (dModel % 2 !== 0) {
|
|
5299
|
+
const i = Math.floor(dModel / 2);
|
|
5300
|
+
const freq = Math.pow(1e4, 2 * i / dModel);
|
|
5301
|
+
pe[dModel - 1] = Math.sin(pos / freq);
|
|
5302
|
+
}
|
|
5303
|
+
return pe;
|
|
5304
|
+
}
|
|
5305
|
+
// Build the full positional encoding matrix for a sequence of `seqLen` tokens.
|
|
5306
|
+
// Returns shape [seqLen][dModel].
|
|
5307
|
+
//
|
|
5308
|
+
// In practice this matrix is computed once and cached — it doesn't change
|
|
5309
|
+
// across examples, batches, or epochs.
|
|
5310
|
+
static encodeSequence(seqLen, dModel) {
|
|
5311
|
+
return Array.from(
|
|
5312
|
+
{ length: seqLen },
|
|
5313
|
+
(_, pos) => _PositionalEncoding.encode(pos, dModel)
|
|
5314
|
+
);
|
|
5315
|
+
}
|
|
5316
|
+
// Add positional encoding to an existing embedding matrix (in-place on a copy).
|
|
5317
|
+
//
|
|
5318
|
+
// `embeddings` shape: [seqLen][dModel].
|
|
5319
|
+
// `seqLen` is optional; defaults to embeddings.length.
|
|
5320
|
+
//
|
|
5321
|
+
// The sum e = token_embedding + PE is what actually enters the first
|
|
5322
|
+
// Transformer layer. Summing (rather than concatenating) keeps the model
|
|
5323
|
+
// dimension fixed and lets the network distribute its capacity freely —
|
|
5324
|
+
// it can choose how much of each dimension to allocate to content vs. position.
|
|
5325
|
+
static apply(embeddings, seqLen) {
|
|
5326
|
+
const len = seqLen ?? embeddings.length;
|
|
5327
|
+
const dModel = embeddings[0].length;
|
|
5328
|
+
const pe = _PositionalEncoding.encodeSequence(len, dModel);
|
|
5329
|
+
return embeddings.map(
|
|
5330
|
+
(emb, pos) => emb.map((val, d) => val + pe[pos][d])
|
|
5331
|
+
);
|
|
5332
|
+
}
|
|
5333
|
+
};
|
|
5334
|
+
var LearnedPositionalEncoding = class {
|
|
5335
|
+
constructor(maxSeqLen, dModel) {
|
|
5336
|
+
this.maxSeqLen = maxSeqLen;
|
|
5337
|
+
this.dModel = dModel;
|
|
5338
|
+
const limit = Math.sqrt(1 / dModel);
|
|
5339
|
+
this.weights = Array.from(
|
|
5340
|
+
{ length: maxSeqLen },
|
|
5341
|
+
() => Array.from({ length: dModel }, () => (Math.random() * 2 - 1) * limit)
|
|
5342
|
+
);
|
|
5343
|
+
}
|
|
5344
|
+
// Return the learned encoding for one position.
|
|
5345
|
+
// Returns a copy so callers cannot accidentally mutate the weight table.
|
|
5346
|
+
getEncoding(pos) {
|
|
5347
|
+
if (pos >= this.maxSeqLen) {
|
|
5348
|
+
throw new Error(
|
|
5349
|
+
`Position ${pos} exceeds maxSeqLen=${this.maxSeqLen}. Learned encodings cannot generalize beyond their training length.`
|
|
5350
|
+
);
|
|
5351
|
+
}
|
|
5352
|
+
return [...this.weights[pos]];
|
|
5353
|
+
}
|
|
5354
|
+
// Add learned positional encodings to `embeddings` (returns a new matrix).
|
|
5355
|
+
// Shape: [seqLen][dModel] → [seqLen][dModel].
|
|
5356
|
+
apply(embeddings, seqLen) {
|
|
5357
|
+
const len = seqLen ?? embeddings.length;
|
|
5358
|
+
if (len > this.maxSeqLen) {
|
|
5359
|
+
throw new Error(
|
|
5360
|
+
`Sequence length ${len} exceeds maxSeqLen=${this.maxSeqLen}.`
|
|
5361
|
+
);
|
|
5362
|
+
}
|
|
5363
|
+
return embeddings.map(
|
|
5364
|
+
(emb, pos) => emb.map((val, d) => val + this.weights[pos][d])
|
|
5365
|
+
);
|
|
5366
|
+
}
|
|
5367
|
+
// Apply gradient update to position encoding weights.
|
|
5368
|
+
//
|
|
5369
|
+
// `dWeights` has the same shape as `weights`: [maxSeqLen][dModel].
|
|
5370
|
+
// Each entry is dL/dW_pos[pos][d] — the loss gradient w.r.t. that weight.
|
|
5371
|
+
//
|
|
5372
|
+
// Simple SGD is used here (matching EmbeddingMatrix in MatMul.ts):
|
|
5373
|
+
// position embeddings are updated every step for all positions in the batch,
|
|
5374
|
+
// so the sparse-update problem of token embeddings doesn't apply.
|
|
5375
|
+
update(dWeights, lr) {
|
|
5376
|
+
for (let pos = 0; pos < this.maxSeqLen; pos++) {
|
|
5377
|
+
for (let d = 0; d < this.dModel; d++) {
|
|
5378
|
+
this.weights[pos][d] += lr * dWeights[pos][d];
|
|
5379
|
+
}
|
|
5380
|
+
}
|
|
5381
|
+
}
|
|
5382
|
+
};
|
|
5383
|
+
|
|
5384
|
+
// src/ContrastiveLearning.ts
|
|
5385
|
+
var Augmenter = class _Augmenter {
|
|
5386
|
+
// Add zero-mean Gaussian noise with standard deviation `sigma`.
|
|
5387
|
+
//
|
|
5388
|
+
// Uses the Box-Muller transform to produce normally distributed noise from
|
|
5389
|
+
// two uniform random variables:
|
|
5390
|
+
// z = √(-2·ln(u₁)) · cos(2π·u₂) where u₁, u₂ ~ Uniform(0, 1)
|
|
5391
|
+
//
|
|
5392
|
+
// This keeps us dependency-free while yielding proper Gaussian samples.
|
|
5393
|
+
static addNoise(x, sigma = 0.05) {
|
|
5394
|
+
return x.map((v) => {
|
|
5395
|
+
const u1 = Math.max(1e-10, Math.random());
|
|
5396
|
+
const u2 = Math.random();
|
|
5397
|
+
const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
|
|
5398
|
+
return v + sigma * z;
|
|
5399
|
+
});
|
|
5400
|
+
}
|
|
5401
|
+
// Randomly zero out features with probability `rate`.
|
|
5402
|
+
//
|
|
5403
|
+
// Analogous to masking in BERT or random crops in vision contrastive learning.
|
|
5404
|
+
// The encoder must learn representations that are robust to missing features —
|
|
5405
|
+
// it cannot simply memorize individual dimensions.
|
|
5406
|
+
static dropoutFeatures(x, rate = 0.1) {
|
|
5407
|
+
return x.map((v) => Math.random() < rate ? 0 : v);
|
|
5408
|
+
}
|
|
5409
|
+
// Apply both noise and feature dropout in sequence.
|
|
5410
|
+
//
|
|
5411
|
+
// Combining augmentations is standard in SimCLR — stronger augmentations
|
|
5412
|
+
// force the encoder to learn more robust, abstract representations.
|
|
5413
|
+
static augment(x, noiseStd = 0.05, dropRate = 0.1) {
|
|
5414
|
+
return _Augmenter.dropoutFeatures(_Augmenter.addNoise(x, noiseStd), dropRate);
|
|
5415
|
+
}
|
|
5416
|
+
// Generate a positive pair: [original, augmented_copy].
|
|
5417
|
+
//
|
|
5418
|
+
// These two views are used as the (i, j) positive pair in NT-Xent.
|
|
5419
|
+
// Everything else in the batch acts as a negative.
|
|
5420
|
+
static makePair(x) {
|
|
5421
|
+
return [x, _Augmenter.augment(x)];
|
|
5422
|
+
}
|
|
5423
|
+
};
|
|
5424
|
+
var ContrastiveLearning = class _ContrastiveLearning {
|
|
5425
|
+
// encoderHidden: hidden layer sizes for the encoder (not counting input/output).
|
|
5426
|
+
// e.g. inputSize=64, encoderHidden=[256, 128] → NetworkN([64, 256, 128])
|
|
5427
|
+
// The encoder output dimension is encoderHidden[last].
|
|
5428
|
+
//
|
|
5429
|
+
// projectionDim: dimension of the projection head output (the z space).
|
|
5430
|
+
// e.g. 64. Typically smaller than the encoder's output.
|
|
5431
|
+
//
|
|
5432
|
+
// The encoder uses ReLU activations throughout — empirically stronger than
|
|
5433
|
+
// sigmoid for representation learning because it doesn't saturate.
|
|
5434
|
+
constructor(inputSize, encoderHidden, projectionDim, options = {}) {
|
|
5435
|
+
if (encoderHidden.length === 0) {
|
|
5436
|
+
throw new Error("encoderHidden must have at least one element.");
|
|
5437
|
+
}
|
|
5438
|
+
this.temperature = options.temperature ?? 0.5;
|
|
5439
|
+
const encoderStructure = [inputSize, ...encoderHidden];
|
|
5440
|
+
const encoderActivations = encoderHidden.map(() => relu);
|
|
5441
|
+
this.encoder = new NetworkN(encoderStructure, {
|
|
5442
|
+
activations: encoderActivations,
|
|
5443
|
+
...options.encoderOptions
|
|
5444
|
+
});
|
|
5445
|
+
const encoderOut = encoderHidden[encoderHidden.length - 1];
|
|
5446
|
+
const projHidden = Math.max(projectionDim, Math.floor(encoderOut / 2));
|
|
5447
|
+
this.projectionHead = new NetworkN(
|
|
5448
|
+
[encoderOut, projHidden, projectionDim],
|
|
5449
|
+
{ activations: [relu, relu] }
|
|
5450
|
+
);
|
|
5451
|
+
}
|
|
5452
|
+
// ── Inference (downstream tasks use this, not project()) ─────────────────
|
|
5453
|
+
//
|
|
5454
|
+
// Returns h — the encoder representation before the projection head.
|
|
5455
|
+
// This is the vector to use for classification, clustering, retrieval, etc.
|
|
5456
|
+
//
|
|
5457
|
+
// The projection head is only active during training.
|
|
5458
|
+
encode(x) {
|
|
5459
|
+
return this.encoder.predict(x);
|
|
5460
|
+
}
|
|
5461
|
+
// ── Training path: encode then project ───────────────────────────────────
|
|
5462
|
+
//
|
|
5463
|
+
// Returns z — the projected representation used to compute NT-Xent.
|
|
5464
|
+
// Do NOT use this for downstream tasks (see encode() above).
|
|
5465
|
+
project(x) {
|
|
5466
|
+
const h = this.encoder.predict(x);
|
|
5467
|
+
return this.projectionHead.predict(h);
|
|
5468
|
+
}
|
|
5469
|
+
// ── Cosine similarity ─────────────────────────────────────────────────────
|
|
5470
|
+
//
|
|
5471
|
+
// sim(u, v) = uᵀv / (||u|| · ||v||)
|
|
5472
|
+
//
|
|
5473
|
+
// Range: [-1, 1]. We use cosine rather than Euclidean distance because it is
|
|
5474
|
+
// scale-invariant — only the direction of the projection matters, not its
|
|
5475
|
+
// magnitude. This prevents the trivial solution of making ||z|| → ∞.
|
|
5476
|
+
static cosineSimilarity(a, b) {
|
|
5477
|
+
let dot = 0, normA = 0, normB = 0;
|
|
5478
|
+
for (let d = 0; d < a.length; d++) {
|
|
5479
|
+
dot += a[d] * b[d];
|
|
5480
|
+
normA += a[d] * a[d];
|
|
5481
|
+
normB += b[d] * b[d];
|
|
5482
|
+
}
|
|
5483
|
+
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
|
5484
|
+
return denom < 1e-10 ? 0 : dot / denom;
|
|
5485
|
+
}
|
|
5486
|
+
// ── NT-Xent loss (no weight update) ──────────────────────────────────────
|
|
5487
|
+
//
|
|
5488
|
+
// Forward-only pass. Used for validation / monitoring during training.
|
|
5489
|
+
computeLoss(pairs) {
|
|
5490
|
+
const { projections, N } = this._forwardProjections(pairs);
|
|
5491
|
+
return this._ntXentLoss(projections, N);
|
|
5492
|
+
}
|
|
5493
|
+
// ── Training step ─────────────────────────────────────────────────────────
|
|
5494
|
+
//
|
|
5495
|
+
// Given a batch of positive pairs, compute NT-Xent loss and update weights
|
|
5496
|
+
// via finite-difference gradient approximation.
|
|
5497
|
+
//
|
|
5498
|
+
// Full analytical backprop through NT-Xent is complex to implement from
|
|
5499
|
+
// scratch without an autograd engine. Finite differences are slower but
|
|
5500
|
+
// correct and keep the implementation readable for educational purposes.
|
|
5501
|
+
// For production use, couple this with the Tape (autograd) module.
|
|
5502
|
+
//
|
|
5503
|
+
// Step-by-step:
|
|
5504
|
+
// 1. Forward all 2N inputs through encoder + projection head → { z_i }.
|
|
5505
|
+
// 2. Build the 2N×2N cosine similarity matrix (scaled by 1/τ).
|
|
5506
|
+
// 3. For each anchor i, identify its positive pair and all 2N-2 negatives.
|
|
5507
|
+
// 4. Apply softmax over the row; loss = -log(softmax at positive index).
|
|
5508
|
+
// 5. Average over all 2N anchors.
|
|
5509
|
+
// 6. Approximate ∂L/∂w per weight with finite differences and apply update.
|
|
5510
|
+
//
|
|
5511
|
+
// Returns: NT-Xent loss before the weight update.
|
|
5512
|
+
trainStep(pairs, lr) {
|
|
5513
|
+
const loss = this.computeLoss(pairs);
|
|
5514
|
+
const eps = 1e-4;
|
|
5515
|
+
for (const layer of this.encoder.layers) {
|
|
5516
|
+
for (const neuron of layer.neurons) {
|
|
5517
|
+
for (let j = 0; j < neuron.weights.length; j++) {
|
|
5518
|
+
neuron.weights[j] += eps;
|
|
5519
|
+
const lossPlus2 = this.computeLoss(pairs);
|
|
5520
|
+
neuron.weights[j] -= 2 * eps;
|
|
5521
|
+
const lossMinus2 = this.computeLoss(pairs);
|
|
5522
|
+
neuron.weights[j] += eps;
|
|
5523
|
+
const grad2 = (lossPlus2 - lossMinus2) / (2 * eps);
|
|
5524
|
+
neuron.weights[j] += lr * -grad2;
|
|
5525
|
+
}
|
|
5526
|
+
neuron.bias += eps;
|
|
5527
|
+
const lossPlus = this.computeLoss(pairs);
|
|
5528
|
+
neuron.bias -= 2 * eps;
|
|
5529
|
+
const lossMinus = this.computeLoss(pairs);
|
|
5530
|
+
neuron.bias += eps;
|
|
5531
|
+
const grad = (lossPlus - lossMinus) / (2 * eps);
|
|
5532
|
+
neuron.bias += lr * -grad;
|
|
5533
|
+
}
|
|
5534
|
+
}
|
|
5535
|
+
for (const layer of this.projectionHead.layers) {
|
|
5536
|
+
for (const neuron of layer.neurons) {
|
|
5537
|
+
for (let j = 0; j < neuron.weights.length; j++) {
|
|
5538
|
+
neuron.weights[j] += eps;
|
|
5539
|
+
const lossPlus2 = this.computeLoss(pairs);
|
|
5540
|
+
neuron.weights[j] -= 2 * eps;
|
|
5541
|
+
const lossMinus2 = this.computeLoss(pairs);
|
|
5542
|
+
neuron.weights[j] += eps;
|
|
5543
|
+
const grad2 = (lossPlus2 - lossMinus2) / (2 * eps);
|
|
5544
|
+
neuron.weights[j] += lr * -grad2;
|
|
5545
|
+
}
|
|
5546
|
+
neuron.bias += eps;
|
|
5547
|
+
const lossPlus = this.computeLoss(pairs);
|
|
5548
|
+
neuron.bias -= 2 * eps;
|
|
5549
|
+
const lossMinus = this.computeLoss(pairs);
|
|
5550
|
+
neuron.bias += eps;
|
|
5551
|
+
const grad = (lossPlus - lossMinus) / (2 * eps);
|
|
5552
|
+
neuron.bias += lr * -grad;
|
|
5553
|
+
}
|
|
5554
|
+
}
|
|
5555
|
+
return loss;
|
|
5556
|
+
}
|
|
5557
|
+
// ── Private: forward all pairs through the projection head ───────────────
|
|
5558
|
+
//
|
|
5559
|
+
// Returns a flat array of 2N projections.
|
|
5560
|
+
// Layout: [ z_0, z_0', z_1, z_1', ..., z_{N-1}, z_{N-1}' ]
|
|
5561
|
+
// Even indices 2i → original view of pair i
|
|
5562
|
+
// Odd indices 2i+1 → augmented view of pair i (the positive)
|
|
5563
|
+
_forwardProjections(pairs) {
|
|
5564
|
+
const N = pairs.length;
|
|
5565
|
+
const projections = [];
|
|
5566
|
+
for (const [x, xAug] of pairs) {
|
|
5567
|
+
projections.push(this.project(x));
|
|
5568
|
+
projections.push(this.project(xAug));
|
|
5569
|
+
}
|
|
5570
|
+
return { projections, N };
|
|
5571
|
+
}
|
|
5572
|
+
// ── Private: NT-Xent loss over a set of 2N projections ───────────────────
|
|
5573
|
+
//
|
|
5574
|
+
// pairs[2i] and pairs[2i+1] are positives.
|
|
5575
|
+
// All other 2N-2 samples are negatives for each anchor.
|
|
5576
|
+
_ntXentLoss(projections, N) {
|
|
5577
|
+
const total = 2 * N;
|
|
5578
|
+
const tau = this.temperature;
|
|
5579
|
+
const sim = Array.from(
|
|
5580
|
+
{ length: total },
|
|
5581
|
+
(_, i) => Array.from(
|
|
5582
|
+
{ length: total },
|
|
5583
|
+
(_2, j) => _ContrastiveLearning.cosineSimilarity(projections[i], projections[j]) / tau
|
|
5584
|
+
)
|
|
5585
|
+
);
|
|
5586
|
+
let totalLoss = 0;
|
|
5587
|
+
for (let i = 0; i < total; i++) {
|
|
5588
|
+
const posIdx = i % 2 === 0 ? i + 1 : i - 1;
|
|
5589
|
+
const numerator = Math.exp(sim[i][posIdx]);
|
|
5590
|
+
let denominator = 0;
|
|
5591
|
+
for (let k = 0; k < total; k++) {
|
|
5592
|
+
if (k !== i) {
|
|
5593
|
+
denominator += Math.exp(sim[i][k]);
|
|
5594
|
+
}
|
|
5595
|
+
}
|
|
5596
|
+
totalLoss += -Math.log(numerator / (denominator + 1e-10));
|
|
5597
|
+
}
|
|
5598
|
+
return totalLoss / total;
|
|
5599
|
+
}
|
|
5600
|
+
};
|
|
5601
|
+
|
|
4853
5602
|
// src/GAN.ts
|
|
4854
5603
|
var GAN = class {
|
|
4855
5604
|
constructor(latentDim, generatorHidden, outputDim, discriminatorHidden, options) {
|
|
@@ -5661,12 +6410,14 @@ function _sampleNormal() {
|
|
|
5661
6410
|
0 && (module.exports = {
|
|
5662
6411
|
Adam,
|
|
5663
6412
|
AttentionHead,
|
|
6413
|
+
Augmenter,
|
|
5664
6414
|
Autoencoder,
|
|
5665
6415
|
BatchNorm,
|
|
5666
6416
|
BiasVector,
|
|
5667
6417
|
CausalConv1D,
|
|
5668
6418
|
ClipOptimizer,
|
|
5669
6419
|
ClippedOptimizerFactory,
|
|
6420
|
+
ContrastiveLearning,
|
|
5670
6421
|
Conv1D,
|
|
5671
6422
|
Conv2D,
|
|
5672
6423
|
DataAugmentation,
|
|
@@ -5685,6 +6436,7 @@ function _sampleNormal() {
|
|
|
5685
6436
|
LSTMLayer,
|
|
5686
6437
|
Layer,
|
|
5687
6438
|
LayerNorm,
|
|
6439
|
+
LearnedPositionalEncoding,
|
|
5688
6440
|
LinearRegression,
|
|
5689
6441
|
LogisticRegression,
|
|
5690
6442
|
LossPlotter,
|
|
@@ -5701,18 +6453,21 @@ function _sampleNormal() {
|
|
|
5701
6453
|
NeuronN,
|
|
5702
6454
|
PCA,
|
|
5703
6455
|
Perceptron,
|
|
6456
|
+
PositionalEncoding,
|
|
5704
6457
|
RNN,
|
|
5705
6458
|
SGD,
|
|
5706
6459
|
SOM,
|
|
5707
6460
|
Seq2Seq,
|
|
5708
6461
|
SoftmaxRegression,
|
|
5709
6462
|
TCN,
|
|
6463
|
+
TSNE,
|
|
5710
6464
|
Trainer,
|
|
5711
6465
|
TransformerBlock,
|
|
5712
6466
|
VAE,
|
|
5713
6467
|
Value,
|
|
5714
6468
|
WeightInspector,
|
|
5715
6469
|
WeightMatrix,
|
|
6470
|
+
Word2Vec,
|
|
5716
6471
|
accuracy,
|
|
5717
6472
|
auc,
|
|
5718
6473
|
classificationReport,
|