From 937576037cb41d51b7511514421df85da00755e3 Mon Sep 17 00:00:00 2001 From: Andrew DalPino Date: Sat, 15 Aug 2026 17:50:31 -0500 Subject: [PATCH 1/5] Initial commit --- CHANGELOG.md | 3 ++ src/Transformers/TSNE.php | 9 ++++-- tests/Transformers/TSNETest.php | 56 +++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cde414469..3a0e27a74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +- 2.5.3 + - Fix TSNE gradient computation + - 2.5.2 - Fix bug in One-class SVM inferencing diff --git a/src/Transformers/TSNE.php b/src/Transformers/TSNE.php index d6b81b16d..ef57b0d58 100644 --- a/src/Transformers/TSNE.php +++ b/src/Transformers/TSNE.php @@ -522,13 +522,16 @@ protected function affinities(array $distances) : array */ protected function gradient(Matrix $p, Matrix $y, Matrix $distances) : Matrix { - $q = $distances->divide($this->dofs) + $kernel = $distances->square() + ->divide($this->dofs) ->add(1.0) ->pow((1.0 + $this->dofs) / -2.0); - $q = $q->divide($q->sum()->multiply(2.0)->clipLower(EPSILON)); + $q = $kernel->divide( + max($kernel->sum()->sum() - $kernel->diagonalAsVector()->sum(), EPSILON) + ); - $pqd = $p->subtract($q)->multiply($distances); + $pqd = $p->subtract($q)->multiply($kernel); $gradient = []; diff --git a/tests/Transformers/TSNETest.php b/tests/Transformers/TSNETest.php index 1be227636..5908aa31e 100644 --- a/tests/Transformers/TSNETest.php +++ b/tests/Transformers/TSNETest.php @@ -2,6 +2,7 @@ namespace Rubix\ML\Tests\Transformers; +use ReflectionMethod; use Rubix\ML\Verbose; use Rubix\ML\DataType; use Rubix\ML\Loggers\BlackHole; @@ -10,6 +11,7 @@ use Rubix\ML\Kernels\Distance\Euclidean; use Rubix\ML\Datasets\Generators\Agglomerate; use Rubix\ML\Exceptions\InvalidArgumentException; +use Tensor\Matrix; use PHPUnit\Framework\TestCase; /** @@ -91,6 +93,44 @@ public function compatibility() : void $this->assertEquals($expected, $this->embedder->compatibility()); } + /** + * @test + */ + public function gradient() : void + { + $p = Matrix::quick([ + [0.0, 0.3, 0.2], + [0.3, 0.0, 0.3], + [0.2, 0.3, 0.0], + ]); + + $y = Matrix::quick([ + [1.0], + [2.0], + [3.0], + ]); + + $distances = Matrix::quick([ + [0.0, 1.0, 2.0], + [1.0, 0.0, 1.0], + [2.0, 1.0, 0.0], + ]); + + $gradient = $this->invokeGradient($this->embedder, $p, $y, $distances); + + $expected = [ + [-0.37], + [0.0], + [0.37], + ]; + + foreach ($gradient->asArray() as $i => $row) { + foreach ($row as $j => $value) { + $this->assertEqualsWithDelta($expected[$i][$j], $value, 1e-8); + } + } + } + /** * @test */ @@ -108,4 +148,20 @@ public function transform() : void $this->assertIsArray($losses); $this->assertContainsOnly('float', $losses); } + + /** + * @param Matrix $p + * @param Matrix $y + * @param Matrix $distances + * @param TSNE $embedder + * @return Matrix + */ + private function invokeGradient(TSNE $embedder, Matrix $p, Matrix $y, Matrix $distances) : Matrix + { + $method = new ReflectionMethod(TSNE::class, 'gradient'); + + $method->setAccessible(true); + + return $method->invokeArgs($embedder, [$p, $y, $distances]); + } } From 7906a33bf4b1c7b80150671eda7e4bd2afc46074 Mon Sep 17 00:00:00 2001 From: Andrew DalPino Date: Sat, 15 Aug 2026 17:56:38 -0500 Subject: [PATCH 2/5] A little nicer --- src/Transformers/TSNE.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Transformers/TSNE.php b/src/Transformers/TSNE.php index ef57b0d58..b317006f7 100644 --- a/src/Transformers/TSNE.php +++ b/src/Transformers/TSNE.php @@ -527,9 +527,9 @@ protected function gradient(Matrix $p, Matrix $y, Matrix $distances) : Matrix ->add(1.0) ->pow((1.0 + $this->dofs) / -2.0); - $q = $kernel->divide( - max($kernel->sum()->sum() - $kernel->diagonalAsVector()->sum(), EPSILON) - ); + $norm = $kernel->sum()->sum() - $kernel->diagonalAsVector()->sum(); + + $q = $kernel->divide(max($norm, EPSILON)); $pqd = $p->subtract($q)->multiply($kernel); From bc447c666ac9bc82a1503f8d12778c057448258b Mon Sep 17 00:00:00 2001 From: Andrew DalPino Date: Sat, 15 Aug 2026 18:15:25 -0500 Subject: [PATCH 3/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- tests/Transformers/TSNETest.php | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/Transformers/TSNETest.php b/tests/Transformers/TSNETest.php index 5908aa31e..998e31467 100644 --- a/tests/Transformers/TSNETest.php +++ b/tests/Transformers/TSNETest.php @@ -129,7 +129,23 @@ public function gradient() : void $this->assertEqualsWithDelta($expected[$i][$j], $value, 1e-8); } } - } + + // Also cover dofs > 1 (e.g. 3D embedding => dofs=2) + $embedder = new TSNE(3); + + $y3d = Matrix::quick([ + [1.0, 0.0, 0.0], + [2.0, 0.0, 0.0], + [3.0, 0.0, 0.0], + ]); + + $gradient3d = $this->invokeGradient($embedder, $p, $y3d, $distances)->asArray(); + + $this->assertEqualsWithDelta(-0.424792, $gradient3d[0][0], 1e-6); + $this->assertEqualsWithDelta(0.0, $gradient3d[1][0], 1e-8); + $this->assertEqualsWithDelta(0.424792, $gradient3d[2][0], 1e-6); + $this->assertEqualsWithDelta(0.0, $gradient3d[0][1], 1e-8); + $this->assertEqualsWithDelta(0.0, $gradient3d[0][2], 1e-8); /** * @test From a5aeea92d5a37b209c577d6378df943c8ffddde5 Mon Sep 17 00:00:00 2001 From: Andrew DalPino Date: Sat, 15 Aug 2026 18:15:37 -0500 Subject: [PATCH 4/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- tests/Transformers/TSNETest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Transformers/TSNETest.php b/tests/Transformers/TSNETest.php index 998e31467..91eda8281 100644 --- a/tests/Transformers/TSNETest.php +++ b/tests/Transformers/TSNETest.php @@ -166,10 +166,10 @@ public function transform() : void } /** + * @param TSNE $embedder * @param Matrix $p * @param Matrix $y * @param Matrix $distances - * @param TSNE $embedder * @return Matrix */ private function invokeGradient(TSNE $embedder, Matrix $p, Matrix $y, Matrix $distances) : Matrix From 1f13507207d9ee57cc09066c7eb72025c8e37f14 Mon Sep 17 00:00:00 2001 From: Andrew DalPino Date: Sat, 15 Aug 2026 18:30:55 -0500 Subject: [PATCH 5/5] Fix slop --- src/Transformers/TSNE.php | 17 ++++--- tests/Transformers/TSNETest.php | 82 +++++++++++++++++++++++++++++---- 2 files changed, 83 insertions(+), 16 deletions(-) diff --git a/src/Transformers/TSNE.php b/src/Transformers/TSNE.php index b317006f7..22ad0c9b9 100644 --- a/src/Transformers/TSNE.php +++ b/src/Transformers/TSNE.php @@ -460,7 +460,7 @@ protected function affinities(array $distances) : array foreach ($row as $k => $distance) { if ($i !== $k) { - $affinity = exp(-$distance * $beta); + $affinity = exp(-$distance ** 2 * $beta); $candidate[] = $affinity; $pSigma += $affinity; @@ -476,9 +476,11 @@ protected function affinities(array $distances) : array foreach ($candidate as $k => &$affinity) { $affinity /= $pSigma; - $distSigma += $row[$k] * $affinity; + $distSigma += $row[$k] ** 2 * $affinity; } + unset($affinity); + $entropy = log($pSigma) + $beta * $distSigma; $diff = $this->entropy - $entropy; @@ -522,16 +524,19 @@ protected function affinities(array $distances) : array */ protected function gradient(Matrix $p, Matrix $y, Matrix $distances) : Matrix { - $kernel = $distances->square() + $base = $distances->square() ->divide($this->dofs) - ->add(1.0) - ->pow((1.0 + $this->dofs) / -2.0); + ->add(1.0); + + $kernel = $base->pow((1.0 + $this->dofs) / -2.0); + + $weights = $base->pow(-1.0); $norm = $kernel->sum()->sum() - $kernel->diagonalAsVector()->sum(); $q = $kernel->divide(max($norm, EPSILON)); - $pqd = $p->subtract($q)->multiply($kernel); + $pqd = $p->subtract($q)->multiply($weights); $gradient = []; diff --git a/tests/Transformers/TSNETest.php b/tests/Transformers/TSNETest.php index 91eda8281..b0d2e1b18 100644 --- a/tests/Transformers/TSNETest.php +++ b/tests/Transformers/TSNETest.php @@ -129,23 +129,71 @@ public function gradient() : void $this->assertEqualsWithDelta($expected[$i][$j], $value, 1e-8); } } + } + + /** + * @test + */ + public function gradientWeight() : void + { + $embedder = new TSNE(3, 10.0, 10, 12.0, 500, 1e-7, 10, new Euclidean()); - // Also cover dofs > 1 (e.g. 3D embedding => dofs=2) - $embedder = new TSNE(3); + $p = Matrix::quick([ + [0.0, 0.3, 0.2], + [0.3, 0.0, 0.3], + [0.2, 0.3, 0.0], + ]); - $y3d = Matrix::quick([ + $y = Matrix::quick([ + [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], - [2.0, 0.0, 0.0], [3.0, 0.0, 0.0], ]); - $gradient3d = $this->invokeGradient($embedder, $p, $y3d, $distances)->asArray(); + $distances = Matrix::quick([ + [0.0, 1.0, 3.0], + [1.0, 0.0, 2.0], + [3.0, 2.0, 0.0], + ]); + + $gradient = $this->invokeGradient($embedder, $p, $y, $distances); - $this->assertEqualsWithDelta(-0.424792, $gradient3d[0][0], 1e-6); - $this->assertEqualsWithDelta(0.0, $gradient3d[1][0], 1e-8); - $this->assertEqualsWithDelta(0.424792, $gradient3d[2][0], 1e-6); - $this->assertEqualsWithDelta(0.0, $gradient3d[0][1], 1e-8); - $this->assertEqualsWithDelta(0.0, $gradient3d[0][2], 1e-8); + $expected = [ + [-0.18091856296078745, 0.0, 0.0], + [-0.4321223317436502, 0.0, 0.0], + [0.6130408947044377, 0.0, 0.0], + ]; + + foreach ($gradient->asArray() as $i => $row) { + foreach ($row as $j => $value) { + $this->assertEqualsWithDelta($expected[$i][$j], $value, 1e-8); + } + } + } + + /** + * @test + */ + public function affinities() : void + { + $embedder = new TSNE(1, 10.0, 2, 12.0, 500, 1e-7, 10, new Euclidean()); + + $distances = [ + [0.0, 1.0, 2.0, 3.0], + [1.0, 0.0, 1.0, 2.0], + [2.0, 1.0, 0.0, 1.0], + [3.0, 2.0, 1.0, 0.0], + ]; + + $affinities = $this->invokeAffinities($embedder, $distances); + + $row = $affinities[0]; + + $left = log($row[1] / $row[2]) * ($distances[0][3] ** 2 - $distances[0][2] ** 2); + $right = log($row[2] / $row[3]) * ($distances[0][2] ** 2 - $distances[0][1] ** 2); + + $this->assertEqualsWithDelta($left, $right, 1e-8); + } /** * @test @@ -180,4 +228,18 @@ private function invokeGradient(TSNE $embedder, Matrix $p, Matrix $y, Matrix $di return $method->invokeArgs($embedder, [$p, $y, $distances]); } + + /** + * @param TSNE $embedder + * @param array $distances + * @return array + */ + private function invokeAffinities(TSNE $embedder, array $distances) : array + { + $method = new ReflectionMethod(TSNE::class, 'affinities'); + + $method->setAccessible(true); + + return $method->invokeArgs($embedder, [$distances]); + } }