Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ test.model
/vendor
composer.lock
.phpunit.result.cache
.php-cs-fixer.cache
Thumbs.db
.DS_Store
debug.log
Expand Down
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@
"check": [
"php-cs-fixer fix --config=.php-cs-fixer.dist.php -vvv --dry-run --using-cache=no --sequential --show-progress=dots --stop-on-violation"
],
"diff": [
"php-cs-fixer fix --config=.php-cs-fixer.dist.php -vvv --dry-run --diff"
],
"fix": [
"php-cs-fixer fix --config=.php-cs-fixer.dist.php"
],
Expand Down
8 changes: 5 additions & 3 deletions docs/neural-network/activation-functions/softmax.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<span style="float:right;"><a href="https://github.com/RubixML/ML/blob/master/src/NeuralNet/ActivationFunctions/Softmax/Softmax.php">[source]</a></span>
<span style="float:right;"><a href="https://github.com/RubixML/ML/blob/master/src/NeuralNet/ActivationFunctions/Softmax.php">[source]</a></span>

# Softmax
The Softmax function is a generalization of the [Sigmoid](sigmoid.md) function that squashes each activation between 0 and 1 with the addition that all activations add up to 1. Together, these properties allow the output of the Softmax function to be interpretable as a *joint* probability distribution.
The Softmax function is a generalization of the [Sigmoid](sigmoid.md) function that squashes each activation between 0 and 1 with the addition that all activations for each sample add up to 1. Together, these properties allow the output of the Softmax function to be interpretable as a *joint* probability distribution for multiclass classification.

Softmax expects batched network activations in `[classes, batch]` layout, where rows represent classes and columns represent samples. Each sample column is normalized independently.

$$
\text{Softmax}(x_i) = \frac{e^{x_i}}{\sum_{j=1}^{n} e^{x_j}}
Expand All @@ -23,7 +25,7 @@ This activation function does not have any parameters.

## Example
```php
use Rubix\ML\NeuralNet\ActivationFunctions\Softmax\Softmax;
use Rubix\ML\NeuralNet\ActivationFunctions\Softmax;

$activationFunction = new Softmax();
```
1 change: 1 addition & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
backupGlobals="false"
bootstrap="vendor/autoload.php"
cacheDirectory="runtime"
colors="true"
displayDetailsOnTestsThatTriggerDeprecations="true"
displayDetailsOnTestsThatTriggerNotices="true"
Expand Down
12 changes: 6 additions & 6 deletions src/Classifiers/LogisticRegression.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace Rubix\ML\Classifiers;

use Generator;
use NumPower;
use Rubix\ML\Online;
use Rubix\ML\Learner;
use Rubix\ML\Verbose;
Expand Down Expand Up @@ -33,7 +35,6 @@
use Rubix\ML\Specifications\SamplesAreCompatibleWithEstimator;
use Rubix\ML\Exceptions\InvalidArgumentException;
use Rubix\ML\Exceptions\RuntimeException;
use Generator;

use function is_nan;
use function count;
Expand Down Expand Up @@ -429,7 +430,7 @@ public function proba(Dataset $dataset) : array

$activations = $this->network->infer($dataset);

$activations = array_column($activations->asArray(), 0);
$activations = array_column($activations->toArray(), 0);

$probabilities = [];

Expand Down Expand Up @@ -461,10 +462,9 @@ public function featureImportances() : array
throw new RuntimeException('Weight layer not found.');
}

return $layer->weights()
->rowAsVector(0)
->abs()
->asArray();
$weights = NumPower::abs($layer->weights())->toArray();

return $weights[0] ?? [];
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/Classifiers/MultilayerPerceptron.php
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,7 @@ public function proba(Dataset $dataset) : array

$probabilities = [];

foreach ($activations->asArray() as $dist) {
foreach ($activations->toArray() as $dist) {
$probabilities[] = array_combine($this->classes, $dist) ?: [];
}

Expand Down
2 changes: 1 addition & 1 deletion src/Classifiers/SoftmaxClassifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,7 @@ public function proba(Dataset $dataset) : array

$probabilities = [];

foreach ($activations->asArray() as $dist) {
foreach ($activations->toArray() as $dist) {
$probabilities[] = array_combine($this->classes, $dist) ?: [];
}

Expand Down
37 changes: 18 additions & 19 deletions src/NeuralNet/ActivationFunctions/Softmax.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
* The Softmax function is a generalization of the Sigmoid function that squashes
* each activation between 0 and 1, and all activations add up to 1.
*
* Expects network layout `[classes, batch]` and normalizes each sample column.
*
* @category Machine Learning
* @package Rubix/ML
* @author Andrew DalPino
Expand All @@ -26,37 +28,34 @@ class Softmax implements ActivationFunction, OBufferDerivative
* The Softmax function is defined as:
* f(x_i) = exp(x_i) / sum(exp(x_j)) for all j
*
* The Softmax function is a generalization of the Sigmoid function that squashes
* each activation between 0 and 1, and all activations add up to 1.
*
* > **Note:** This function can be rewritten in a more efficient way,
* using NumPower::exp(), NumPower::sum(), and NumPower::divide().
* Currently blocked by implementation of 2nd parameter "axis" for NumPower::sum()
* Numerically stable form subtracts the per-sample max before exponentiation.
*
* @param NDArray $input
* @return NDArray
*/
public function activate(NDArray $input) : NDArray
{
// Convert to PHP array for stable processing
$inputArray = $input->toArray();
$result = [];
$columns = $input->shape()[1];
$values = $input->toArray();

// Process each row separately to ensure row-wise normalization
foreach ($inputArray as $row) {
$expRow = array_map('exp', $row);
$sum = array_sum($expRow);
$softmaxRow = [];
// NumPower::max() has no axis argument, so compute column maxima in PHP.
$maxima = [];

foreach ($expRow as $value) {
// Round to 7 decimal places to match test expectations
$softmaxRow[] = round($value / $sum, 7);
for ($column = 0; $column < $columns; ++$column) {
$maximum = -INF;

foreach ($values as $row) {
$maximum = max($maximum, $row[$column]);
}

$result[] = $softmaxRow;
$maxima[] = $maximum;
}

return NumPower::array($result);
$max = NumPower::reshape(NumPower::array($maxima), [1, $columns]);
$exponentials = NumPower::exp(NumPower::subtract($input, $max));
$totals = NumPower::reshape(NumPower::sum($exponentials, axis: 0), [1, $columns]);

return NumPower::divide($exponentials, $totals);
}

/**
Expand Down
1 change: 0 additions & 1 deletion src/NeuralNet/FeedForward.php
Original file line number Diff line number Diff line change
Expand Up @@ -279,4 +279,3 @@ public function exportGraphviz() : Encoding
return new Encoding($dot);
}
}

11 changes: 6 additions & 5 deletions src/NeuralNet/Layers/Multiclass.php
Original file line number Diff line number Diff line change
Expand Up @@ -158,16 +158,17 @@ public function back(array $labels, Optimizer $optimizer) : array
. ' before backpropagating.');
}

// Build one-hot targets as [classes, batch] to match Dense output layout.
$expected = [];

foreach ($labels as $label) {
$dist = [];
foreach ($this->classes as $class) {
$row = [];

foreach ($this->classes as $class) {
$dist[] = $class == $label ? 1.0 : 0.0;
foreach ($labels as $label) {
$row[] = $class == $label ? 1.0 : 0.0;
}

$expected[] = $dist;
$expected[] = $row;
}

$expected = NumPower::array($expected);
Expand Down
2 changes: 1 addition & 1 deletion tests/Classifiers/LogisticRegressionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ public function testTrainPartialPredict() : void

$this->assertGreaterThanOrEqual(self::MIN_SCORE, $score);

$this->assertEquals('58a6bb3c', $this->estimator->revision());
$this->assertEquals('f2e08c1a', $this->estimator->revision());
}

public function testTrainIncompatible() : void
Expand Down
89 changes: 54 additions & 35 deletions tests/NeuralNet/ActivationFunctions/SoftmaxTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
use PHPUnit\Framework\Attributes\TestDox;
use PHPUnit\Framework\TestCase;
use Rubix\ML\NeuralNet\ActivationFunctions\Softmax;
use Tensor\Matrix;

#[Group('ActivationFunctions')]
#[CoversClass(Softmax::class)]
Expand All @@ -30,46 +29,58 @@ class SoftmaxTest extends TestCase
*/
public static function computeProvider() : Generator
{
// Inputs use network layout [classes, batch].
yield [
NumPower::array([
[2.0, 1.0, -0.5, 0.0],
[2.0],
[1.0],
[-0.5],
[0.0],
]),
[
[0.6307954, 0.2320567, 0.0517789, 0.0853689],
[0.6307955],
[0.2320567],
[0.0517788],
[0.0853688],
],
];

yield [
NumPower::array([
[-0.12, 0.31, -0.49],
[0.99, 0.08, -0.03],
[0.05, -0.52, 0.54],
[-0.12, 0.99, 0.05],
[0.31, 0.08, -0.52],
[-0.49, -0.03, 0.54],
]),
[
[0.3097901, 0.4762271, 0.2139827],
[0.5671765, 0.2283022, 0.2045210],
[0.312711, 0.176846, 0.510443],
[0.3097901, 0.5671766, 0.3127109],
[0.4762272, 0.2283023, 0.1768459],
[0.2139826, 0.2045210, 0.5104430],
],
];

// Test with zeros
yield [
NumPower::array([
[0.0, 0.0, 0.0, 0.0],
[0.0],
[0.0],
[0.0],
[0.0],
]),
[
[0.25, 0.25, 0.25, 0.25],
[0.25],
[0.25],
[0.25],
[0.25],
],
];

yield [
NumPower::array([
[1, 2],
[3, 4],
[1, 3],
[2, 4],
]),
[
[0.2689414, 0.7310585],
[0.2689414, 0.7310585],
[0.2689414, 0.2689414],
[0.7310585, 0.7310585],
],
];
}
Expand All @@ -79,21 +90,22 @@ public static function computeProvider() : Generator
*/
public static function differentiateProvider() : Generator
{
// Test with simple values
yield [
NumPower::array([
[0.6, 0.4],
[0.6],
[0.4],
]),
[
[0.24, -0.24],
[-0.24, 0.24],
],
];

// Test with more complex values
yield [
NumPower::array([
[0.3, 0.5, 0.2],
[0.3],
[0.5],
[0.2],
]),
[
[0.21, -0.15, -0.06],
Expand All @@ -102,10 +114,10 @@ public static function differentiateProvider() : Generator
],
];

// Test 2x2 matrix
yield [
NumPower::array([
[0.2689414, 0.7310585],
[0.2689414],
[0.7310585],
]),
[
[0.1966119, -0.19661192],
Expand All @@ -119,24 +131,29 @@ public static function differentiateProvider() : Generator
*/
public static function sumToOneProvider() : Generator
{
// Test with various input values
yield [
NumPower::array([
[10.0, -5.0, 3.0, 2.0],
[10.0],
[-5.0],
[3.0],
[2.0],
]),
];

yield [
NumPower::array([
[-10.0, -20.0, -30.0],
[-10.0],
[-20.0],
[-30.0],
]),
];

yield [
NumPower::array([
[0.1, 0.2, 0.3, 0.4],
[5.0, 4.0, 3.0, 2.0],
[-1.0, -2.0, -3.0, -4.0],
[0.1, 5.0, -1.0],
[0.2, 4.0, -2.0],
[0.3, 3.0, -3.0],
[0.4, 2.0, -4.0],
]),
];
}
Expand Down Expand Up @@ -183,15 +200,17 @@ public function testDifferentiate(NDArray $output, array $expected) : void
#[DataProvider('sumToOneProvider')]
public function testSumToOne(NDArray $input) : void
{
$activations = $this->activationFn->activate($input);
$activations = $this->activationFn->activate($input)->toArray();

$columns = count($activations[0]);

// Convert to array for easier processing
$activationsArray = $activations->toArray();
for ($column = 0; $column < $columns; ++$column) {
$sum = 0.0;

foreach ($activations as $row) {
$sum += $row[$column];
}

// Check that each row sums to 1
foreach ($activationsArray as $row) {
$sum = array_sum($row);
// Use a slightly larger delta to account for rounding errors
static::assertEqualsWithDelta(1.0, $sum, 1e-7);
}
}
Expand Down
2 changes: 1 addition & 1 deletion tests/NeuralNet/Initializers/LeCunNormalTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ public function testDistributionStatisticsMatchLeCunNormal(int $fanIn, int $fanO
$this->assertThat(
$std,
$this->logicalAnd(
$this->greaterThan($expectedStd * 0.85),
$this->greaterThan($expectedStd * 0.80),
$this->lessThan($expectedStd * 1.1)
),
'Standard deviation does not match Le Cun initialization'
Expand Down
Loading
Loading