Multilayer neuron network
This commit is contained in:
@@ -38,8 +38,17 @@ class PerceptronTrainingIteration implements ShouldBroadcast
|
|||||||
|
|
||||||
public function broadcastWith(): array
|
public function broadcastWith(): array
|
||||||
{
|
{
|
||||||
|
$lastIterationIndex = count($this->iterations) - 1;
|
||||||
|
$iterations = array_map(
|
||||||
|
fn (array $iteration, int $index): array => $index === $lastIterationIndex
|
||||||
|
? $iteration
|
||||||
|
: [...$iteration, 'weights' => []],
|
||||||
|
$this->iterations,
|
||||||
|
array_keys($this->iterations),
|
||||||
|
);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'iterations' => $this->iterations,
|
'iterations' => $iterations,
|
||||||
'trainingId' => $this->trainingId,
|
'trainingId' => $this->trainingId,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use App\Events\PerceptronInitialization;
|
|||||||
use App\Models\NetworksTraining\ADALINEPerceptronTraining;
|
use App\Models\NetworksTraining\ADALINEPerceptronTraining;
|
||||||
use App\Models\NetworksTraining\GradientDescentPerceptronTraining;
|
use App\Models\NetworksTraining\GradientDescentPerceptronTraining;
|
||||||
use App\Models\NetworksTraining\MonoLayerPerceptronTraining;
|
use App\Models\NetworksTraining\MonoLayerPerceptronTraining;
|
||||||
|
use App\Models\NetworksTraining\MultiLayerPerceptronTraining;
|
||||||
use App\Models\NetworksTraining\SimpleBinaryPerceptronTraining;
|
use App\Models\NetworksTraining\SimpleBinaryPerceptronTraining;
|
||||||
use App\Services\DatasetReader\IDataSetReader;
|
use App\Services\DatasetReader\IDataSetReader;
|
||||||
use App\Services\DatasetReader\LinearOrderDataSetReader;
|
use App\Services\DatasetReader\LinearOrderDataSetReader;
|
||||||
@@ -13,12 +14,11 @@ use App\Services\DatasetReader\RandomOrderDataSetReader;
|
|||||||
use App\Services\IterationEventBuffer\PerceptronIterationEventBuffer;
|
use App\Services\IterationEventBuffer\PerceptronIterationEventBuffer;
|
||||||
use App\Services\IterationEventBuffer\PerceptronLimitedEpochEventBuffer;
|
use App\Services\IterationEventBuffer\PerceptronLimitedEpochEventBuffer;
|
||||||
use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider;
|
use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider;
|
||||||
|
use App\Services\SynapticWeightsProvider\RandomSynapticWeights;
|
||||||
use App\Services\SynapticWeightsProvider\ZeroSynapticWeights;
|
use App\Services\SynapticWeightsProvider\ZeroSynapticWeights;
|
||||||
use Illuminate\Contracts\Queue\Job;
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Symfony\Contracts\EventDispatcher\Event;
|
use Illuminate\Support\Facades\Validator;
|
||||||
use Tests\Services\IterationEventBuffer\DullIterationEventBuffer;
|
|
||||||
|
|
||||||
class PerceptronController extends Controller
|
class PerceptronController extends Controller
|
||||||
{
|
{
|
||||||
@@ -38,9 +38,16 @@ class PerceptronController extends Controller
|
|||||||
$learningRate = 0.015;
|
$learningRate = 0.015;
|
||||||
$maxIterations = 150;
|
$maxIterations = 150;
|
||||||
break;
|
break;
|
||||||
case 'gradientdescent' || 'adaline':
|
case 'gradientdescent':
|
||||||
|
case 'adaline':
|
||||||
$learningRate = 0.00003;
|
$learningRate = 0.00003;
|
||||||
break;
|
break;
|
||||||
|
case 'monolayer':
|
||||||
|
$learningRate = 0.03;
|
||||||
|
break;
|
||||||
|
case 'multilayer':
|
||||||
|
$learningRate = 0.8;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return inertia('PerceptronViewer', [
|
return inertia('PerceptronViewer', [
|
||||||
@@ -50,6 +57,7 @@ class PerceptronController extends Controller
|
|||||||
'minError' => $minError,
|
'minError' => $minError,
|
||||||
'learningRate' => $learningRate,
|
'learningRate' => $learningRate,
|
||||||
'maxIterations' => $maxIterations,
|
'maxIterations' => $maxIterations,
|
||||||
|
'maxDisplayedWeights' => config('perceptron.max_displayed_weights'),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +114,8 @@ class PerceptronController extends Controller
|
|||||||
case 'simple':
|
case 'simple':
|
||||||
$dataset['defaultLearningRate'] = 0.015;
|
$dataset['defaultLearningRate'] = 0.015;
|
||||||
break;
|
break;
|
||||||
case 'gradientdescent' || 'adaline':
|
case 'gradientdescent':
|
||||||
|
case 'adaline':
|
||||||
$dataset['defaultLearningRate'] = 0.001;
|
$dataset['defaultLearningRate'] = 0.001;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -114,6 +123,15 @@ class PerceptronController extends Controller
|
|||||||
case 'table_2_11':
|
case 'table_2_11':
|
||||||
$dataset['defaultMinError'] = 0.02;
|
$dataset['defaultMinError'] = 0.02;
|
||||||
break;
|
break;
|
||||||
|
case 'table_4_12':
|
||||||
|
switch ($perceptronType) {
|
||||||
|
case 'multilayer':
|
||||||
|
$dataset['defaultLearningRate'] = 0.8;
|
||||||
|
$dataset['defaultMinError'] = 0.001;
|
||||||
|
$dataset['defaultMaxIterations'] = 2000;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
$datasets[] = $dataset;
|
$datasets[] = $dataset;
|
||||||
}
|
}
|
||||||
@@ -133,7 +151,19 @@ class PerceptronController extends Controller
|
|||||||
{
|
{
|
||||||
$startTime = microtime(true);
|
$startTime = microtime(true);
|
||||||
|
|
||||||
|
// Verifications
|
||||||
|
$validator = Validator::make($request->all(), config('perceptron.run_inputs_validation'));
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Invalid input parameters',
|
||||||
|
'errors' => $validator->errors(),
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
$perceptronType = $request->input('type');
|
$perceptronType = $request->input('type');
|
||||||
|
$hiddenLayers = $request->input('hidden_layers', 2);
|
||||||
|
$hiddenLayersNeurons = $request->input('hidden_layers_neurons', 3);
|
||||||
$minError = $request->input('min_error', 0.01);
|
$minError = $request->input('min_error', 0.01);
|
||||||
$weightInitMethod = $request->input('weight_init_method', 'random');
|
$weightInitMethod = $request->input('weight_init_method', 'random');
|
||||||
$dataSet = $request->input('dataset');
|
$dataSet = $request->input('dataset');
|
||||||
@@ -145,7 +175,11 @@ class PerceptronController extends Controller
|
|||||||
// Remove the jobs for the sessionId
|
// Remove the jobs for the sessionId
|
||||||
DB::table('jobs')->where('payload', 'like', '%s:9:\"sessionId\";s:40:\"'. $sessionId .'\";%')->delete();
|
DB::table('jobs')->where('payload', 'like', '%s:9:\"sessionId\";s:40:\"'. $sessionId .'\";%')->delete();
|
||||||
|
|
||||||
if ($weightInitMethod === 'zeros') {
|
// Zero initialization prevents hidden layers from receiving a gradient.
|
||||||
|
if ($perceptronType === 'multilayer' && $weightInitMethod === 'zeros') {
|
||||||
|
$synapticWeightsProvider = new RandomSynapticWeights;
|
||||||
|
}
|
||||||
|
else if ($weightInitMethod === 'zeros') {
|
||||||
$synapticWeightsProvider = new ZeroSynapticWeights;
|
$synapticWeightsProvider = new ZeroSynapticWeights;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,6 +196,7 @@ class PerceptronController extends Controller
|
|||||||
'gradientdescent' => new GradientDescentPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError),
|
'gradientdescent' => new GradientDescentPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError),
|
||||||
'adaline' => new ADALINEPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError),
|
'adaline' => new ADALINEPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError),
|
||||||
'monolayer' => new MonoLayerPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError),
|
'monolayer' => new MonoLayerPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError),
|
||||||
|
'multilayer' => new MultiLayerPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $hiddenLayers, $hiddenLayersNeurons, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError),
|
||||||
default => null,
|
default => null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ use App\Models\ActivationsFunctions;
|
|||||||
use App\Models\Perceptrons\GradientDescentPerceptron;
|
use App\Models\Perceptrons\GradientDescentPerceptron;
|
||||||
use App\Models\Perceptrons\NetworkPerceptron;
|
use App\Models\Perceptrons\NetworkPerceptron;
|
||||||
use App\Models\Perceptrons\Perceptron;
|
use App\Models\Perceptrons\Perceptron;
|
||||||
use App\Models\Perceptrons\SimpleBinaryPerceptron2;
|
|
||||||
use App\Models\Perceptrons\SimpleBinaryPerceptron;
|
|
||||||
use App\Services\DatasetReader\IDataSetReader;
|
use App\Services\DatasetReader\IDataSetReader;
|
||||||
use App\Services\IterationEventBuffer\IPerceptronIterationEventBuffer;
|
use App\Services\IterationEventBuffer\IPerceptronIterationEventBuffer;
|
||||||
use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider;
|
use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider;
|
||||||
@@ -47,7 +45,7 @@ class MonoLayerPerceptronTraining extends NetworkTraining
|
|||||||
),
|
),
|
||||||
$datasetReader->getInputSize(),
|
$datasetReader->getInputSize(),
|
||||||
GradientDescentPerceptron::class, // No hidden layer
|
GradientDescentPerceptron::class, // No hidden layer
|
||||||
SimpleBinaryPerceptron2::class,
|
GradientDescentPerceptron::class,
|
||||||
);
|
);
|
||||||
$this->labels = $datasetReader->getLabels();
|
$this->labels = $datasetReader->getLabels();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,259 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models\NetworksTraining;
|
||||||
|
|
||||||
|
use App\Events\PerceptronTrainingEnded;
|
||||||
|
use App\Models\ActivationsFunctions;
|
||||||
|
use App\Models\Perceptrons\GradientDescentPerceptron;
|
||||||
|
use App\Models\Perceptrons\NetworkPerceptron;
|
||||||
|
use App\Models\Perceptrons\Perceptron;
|
||||||
|
use App\Models\Perceptrons\SigmoidPerceptron;
|
||||||
|
use App\Services\DatasetReader\IDataSetReader;
|
||||||
|
use App\Services\IterationEventBuffer\IPerceptronIterationEventBuffer;
|
||||||
|
use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider;
|
||||||
|
use App\Services\SynapticWeightsProvider\SimpleNetworkWeightsProvider;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
|
||||||
|
class MultiLayerPerceptronTraining extends NetworkTraining
|
||||||
|
{
|
||||||
|
private Perceptron $network;
|
||||||
|
|
||||||
|
private array $labels;
|
||||||
|
|
||||||
|
private bool $isRegression;
|
||||||
|
|
||||||
|
public ActivationsFunctions $activationFunction = ActivationsFunctions::SIGMOID;
|
||||||
|
public ?ActivationsFunctions $presentationLayerActivationFunction = ActivationsFunctions::STEP;
|
||||||
|
|
||||||
|
private float $epochError;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
IDataSetReader $datasetReader,
|
||||||
|
protected float $learningRate,
|
||||||
|
int $maxEpochs,
|
||||||
|
protected int $hiddenLayers,
|
||||||
|
protected int $hiddenLayersNeurons,
|
||||||
|
ISynapticWeightsProvider $synapticWeightsProvider,
|
||||||
|
IPerceptronIterationEventBuffer $iterationEventBuffer,
|
||||||
|
string $sessionId,
|
||||||
|
string $trainingId,
|
||||||
|
private float $minError,
|
||||||
|
) {
|
||||||
|
parent::__construct($datasetReader, $maxEpochs, $iterationEventBuffer, $sessionId, $trainingId);
|
||||||
|
$this->labels = $datasetReader->getLabels();
|
||||||
|
$this->isRegression = $datasetReader->getOutputSize() === 1
|
||||||
|
|| ($datasetReader->getOutputSize() > 2
|
||||||
|
&& $datasetReader->getOutputSize() * 2 >= $datasetReader->getEpochExamplesCount());
|
||||||
|
if ($this->isRegression) {
|
||||||
|
$this->activationFunction = ActivationsFunctions::LINEAR;
|
||||||
|
}
|
||||||
|
|
||||||
|
$networkWeightsProvider = new SimpleNetworkWeightsProvider($synapticWeightsProvider);
|
||||||
|
$this->network = new NetworkPerceptron(
|
||||||
|
$networkWeightsProvider->generate(
|
||||||
|
$datasetReader->getInputSize(),
|
||||||
|
$this->isRegression ? 1 : $datasetReader->getOutputSize(),
|
||||||
|
$this->hiddenLayers,
|
||||||
|
$this->hiddenLayersNeurons,
|
||||||
|
),
|
||||||
|
$datasetReader->getInputSize(),
|
||||||
|
SigmoidPerceptron::class,
|
||||||
|
GradientDescentPerceptron::class,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function start(): void
|
||||||
|
{
|
||||||
|
$this->epoch = 0;
|
||||||
|
do {
|
||||||
|
$this->epochError = 0;
|
||||||
|
$this->epoch++;
|
||||||
|
|
||||||
|
$inputsForCurrentEpoch = [];
|
||||||
|
|
||||||
|
while ($nextRow = $this->datasetReader->getNextLine()) {
|
||||||
|
$inputsForCurrentEpoch[] = $nextRow;
|
||||||
|
$inputs = array_slice($nextRow, 0, -1);
|
||||||
|
$correctOutput = (float) end($nextRow);
|
||||||
|
|
||||||
|
$iterationError = $this->iterationFunction($inputs, $correctOutput);
|
||||||
|
|
||||||
|
// Synaptic weights correction after each example
|
||||||
|
$synaptic_weights = $this->network->getSynapticWeights();
|
||||||
|
$inputs_with_bias = array_merge([1], $inputs); // Add bias input
|
||||||
|
|
||||||
|
// Updates the weights
|
||||||
|
$this->network->setSynapticWeights(
|
||||||
|
$this->getUpdatedSynapticWeights($synaptic_weights, $iterationError, $inputs_with_bias)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Broadcast the training iteration event
|
||||||
|
$this->addIterationToBuffer(array_sum($iterationError), $this->network->getSynapticWeights());
|
||||||
|
|
||||||
|
// $this->iterationEventBuffer->flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculte the average error for the epoch with the last synaptic weights
|
||||||
|
foreach ($inputsForCurrentEpoch as $inputsWithLabel) {
|
||||||
|
$inputs = array_slice($inputsWithLabel, 0, -1);
|
||||||
|
$correctOutput = (float) end($inputsWithLabel);
|
||||||
|
$iterationError = $this->iterationFunction($inputs, $correctOutput);
|
||||||
|
foreach ($iterationError as $error) {
|
||||||
|
$this->epochError += ($error ** 2) / 2; // Squared error for the example
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->epochError /= $this->datasetReader->getEpochExamplesCount(); // Average error for the epoch
|
||||||
|
|
||||||
|
$this->datasetReader->reset(); // Reset the dataset for the next iteration
|
||||||
|
} while ($this->epoch < $this->maxEpochs && ! $this->stopCondition());
|
||||||
|
|
||||||
|
$this->iterationEventBuffer->flush(); // Ensure all iterations are sent to the frontend
|
||||||
|
|
||||||
|
$this->checkPassedMaxIterations($this->epochError);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function stopCondition(): bool
|
||||||
|
{
|
||||||
|
$condition = $this->epochError <= $this->minError;
|
||||||
|
if ($condition === true) {
|
||||||
|
event(new PerceptronTrainingEnded('Le perceptron à atteint l\'erreur minimale', $this->sessionId, $this->trainingId));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function iterationFunction(array $inputs, float $correctOutput): array
|
||||||
|
{
|
||||||
|
$outputs = $this->network->test($inputs);
|
||||||
|
$desiredOutput = $this->getDesiredOutputFromCorrectOutput($correctOutput);
|
||||||
|
|
||||||
|
$errors = [];
|
||||||
|
foreach ($outputs as $index => $output) {
|
||||||
|
$error = $desiredOutput[$index] - $output;
|
||||||
|
$errors[] = $error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backpropagation of error gradients to update synaptic weights.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private function getUpdatedSynapticWeights(array $synaptic_weights, array $iterationError, array $inputs): array
|
||||||
|
{
|
||||||
|
$layerInputs = [$inputs];
|
||||||
|
|
||||||
|
// Reproduce NetworkPerceptron::test() using each neuron's activation function.
|
||||||
|
foreach ($synaptic_weights as $layerIndex => $layerWeights) {
|
||||||
|
$previousLayerOutputs = $layerInputs[array_key_last($layerInputs)];
|
||||||
|
$layerOutputs = [];
|
||||||
|
|
||||||
|
foreach ($layerWeights as $neuronIndex => $neuronWeights) {
|
||||||
|
$weightedSum = array_sum(array_map(
|
||||||
|
fn ($input, $weight): float => $input * $weight,
|
||||||
|
$previousLayerOutputs,
|
||||||
|
$neuronWeights,
|
||||||
|
));
|
||||||
|
|
||||||
|
$neuron = $this->network->network[$layerIndex + 1][$neuronIndex];
|
||||||
|
$layerOutputs[] = $neuron->activationFunction($weightedSum);
|
||||||
|
}
|
||||||
|
|
||||||
|
$layerInputs[] = array_merge([1], $layerOutputs);
|
||||||
|
}
|
||||||
|
|
||||||
|
$deltas = array_fill(0, count($synaptic_weights), []);
|
||||||
|
$lastLayerIndex = count($synaptic_weights) - 1;
|
||||||
|
|
||||||
|
// Output delta includes the output neuron's activation derivative.
|
||||||
|
foreach ($iterationError as $neuronIndex => $error) {
|
||||||
|
$neuron = $this->network->network[$lastLayerIndex + 1][$neuronIndex];
|
||||||
|
$output = $layerInputs[$lastLayerIndex + 1][$neuronIndex + 1];
|
||||||
|
|
||||||
|
$deltas[$lastLayerIndex][$neuronIndex] =
|
||||||
|
$error * $this->activationDerivative($neuron, $output);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hidden-layer deltas use the original, unchanged weights.
|
||||||
|
for ($layerIndex = $lastLayerIndex - 1; $layerIndex >= 0; $layerIndex--) {
|
||||||
|
foreach ($synaptic_weights[$layerIndex] as $neuronIndex => $unusedNeuronWeights) {
|
||||||
|
$nextLayerDelta = 0.0;
|
||||||
|
|
||||||
|
foreach ($synaptic_weights[$layerIndex + 1] as $nextNeuronIndex => $nextNeuronWeights) {
|
||||||
|
// Index zero is the next layer's bias weight.
|
||||||
|
$nextLayerDelta +=
|
||||||
|
$nextNeuronWeights[$neuronIndex + 1]
|
||||||
|
* $deltas[$layerIndex + 1][$nextNeuronIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
$neuron = $this->network->network[$layerIndex + 1][$neuronIndex];
|
||||||
|
$output = $layerInputs[$layerIndex + 1][$neuronIndex + 1];
|
||||||
|
|
||||||
|
$deltas[$layerIndex][$neuronIndex] =
|
||||||
|
$nextLayerDelta * $this->activationDerivative($neuron, $output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$updatedWeights = [];
|
||||||
|
|
||||||
|
foreach ($synaptic_weights as $layerIndex => $layerWeights) {
|
||||||
|
$updatedLayerWeights = [];
|
||||||
|
|
||||||
|
foreach ($layerWeights as $neuronIndex => $neuronWeights) {
|
||||||
|
$updatedLayerWeights[] = array_map(
|
||||||
|
fn ($weight, $weightIndex): float => $weight
|
||||||
|
+ $this->learningRate
|
||||||
|
* $deltas[$layerIndex][$neuronIndex]
|
||||||
|
* $layerInputs[$layerIndex][$weightIndex],
|
||||||
|
$neuronWeights,
|
||||||
|
array_keys($neuronWeights),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$updatedWeights[] = $updatedLayerWeights;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $updatedWeights;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function activationDerivative(Perceptron $neuron, float $output): float
|
||||||
|
{
|
||||||
|
// Numerical derivative works for any activationFunction implementation.
|
||||||
|
// $epsilon = 1e-6;
|
||||||
|
|
||||||
|
// The activation function requires weighted input, which is unavailable here.
|
||||||
|
// For sigmoid, use its derivative directly.
|
||||||
|
if ($neuron instanceof SigmoidPerceptron) {
|
||||||
|
return $output * (1 - $output);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linear activation, used commonly by gradient-descent output neurons.
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getDesiredOutputFromCorrectOutput(float $correctOutput): array
|
||||||
|
{
|
||||||
|
if ($this->isRegression) {
|
||||||
|
return [$correctOutput];
|
||||||
|
}
|
||||||
|
|
||||||
|
$desiredOutput = array_fill(0, count($this->labels), 0);
|
||||||
|
$labelIndex = Arr::first(
|
||||||
|
array_keys($this->labels),
|
||||||
|
fn ($key) => $this->labels[$key] == $correctOutput
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($labelIndex !== null) {
|
||||||
|
$desiredOutput[$labelIndex] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $desiredOutput;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSynapticWeights(): array
|
||||||
|
{
|
||||||
|
return [[$this->network->getSynapticWeights()]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ class NetworkPerceptron extends Perceptron
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Hidden Layer
|
// Hidden Layer
|
||||||
for ($layerIndex = 0; $layerIndex < count($synaptic_weights) - 2; $layerIndex++) {
|
for ($layerIndex = 0; $layerIndex < count($synaptic_weights) - 1; $layerIndex++) {
|
||||||
$this->network[$layerIndex + 1] = [];
|
$this->network[$layerIndex + 1] = [];
|
||||||
|
|
||||||
foreach ($synaptic_weights[$layerIndex] as $neuronWeights) {
|
foreach ($synaptic_weights[$layerIndex] as $neuronWeights) {
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models\Perceptrons;
|
||||||
|
|
||||||
|
class SigmoidPerceptron extends Perceptron
|
||||||
|
{
|
||||||
|
public function activationFunction(float $weighted_sum): float
|
||||||
|
{
|
||||||
|
$weighted_sum = max(-40, min(40, $weighted_sum));
|
||||||
|
|
||||||
|
return 1 / (1 + exp(-$weighted_sum));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,13 +28,24 @@ class PerceptronIterationEventBuffer implements IPerceptronIterationEventBuffer
|
|||||||
|
|
||||||
public function addIteration(int $epoch, int $exampleIndex, float $error, array $synaptic_weights): void
|
public function addIteration(int $epoch, int $exampleIndex, float $error, array $synaptic_weights): void
|
||||||
{
|
{
|
||||||
$this->data[] = [
|
$iteration = [
|
||||||
'epoch' => $epoch,
|
'epoch' => $epoch,
|
||||||
'exampleIndex' => $exampleIndex,
|
'exampleIndex' => $exampleIndex,
|
||||||
'error' => $error,
|
'error' => $error,
|
||||||
'weights' => $synaptic_weights,
|
'weights' => $synaptic_weights,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'iterations' => [...$this->data, $iteration],
|
||||||
|
'trainingId' => $this->trainingId,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($this->data !== [] && strlen(json_encode($payload, JSON_THROW_ON_ERROR)) > config('broadcasting.broadcast_max_payload_size')) {
|
||||||
|
$this->flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->data[] = $iteration;
|
||||||
|
|
||||||
if ($this->underSizeIncreaseCount <= $this->sizeIncreaseStart) { // We can still send a single date because we are under the increase start threshold
|
if ($this->underSizeIncreaseCount <= $this->sizeIncreaseStart) { // We can still send a single date because we are under the increase start threshold
|
||||||
$this->underSizeIncreaseCount++;
|
$this->underSizeIncreaseCount++;
|
||||||
$this->flush();
|
$this->flush();
|
||||||
|
|||||||
@@ -18,9 +18,11 @@ class SimpleNetworkWeightsProvider implements INetworkSynapticWeightsProvider
|
|||||||
|
|
||||||
// Generate Hidden Layer weights
|
// Generate Hidden Layer weights
|
||||||
for ($hiddenLayerNeuronIndex = 0; $hiddenLayerNeuronIndex < $hidden_layers_count; $hiddenLayerNeuronIndex++) {
|
for ($hiddenLayerNeuronIndex = 0; $hiddenLayerNeuronIndex < $hidden_layers_count; $hiddenLayerNeuronIndex++) {
|
||||||
|
$layer = [];
|
||||||
for ($neuronIndex = 0; $neuronIndex < $hidden_layers_neurons_count; $neuronIndex++) {
|
for ($neuronIndex = 0; $neuronIndex < $hidden_layers_neurons_count; $neuronIndex++) {
|
||||||
$synaptic_weights[] = $this->synapticWeightsProvider->generate($lastLayerSize);
|
$layer[] = $this->synapticWeightsProvider->generate($lastLayerSize);
|
||||||
}
|
}
|
||||||
|
$synaptic_weights[] = $layer;
|
||||||
$lastLayerSize = $hidden_layers_neurons_count;
|
$lastLayerSize = $hidden_layers_neurons_count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,4 +79,9 @@ return [
|
|||||||
|
|
||||||
],
|
],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep broadcast payloads below Pusher's event size limit.
|
||||||
|
*/
|
||||||
|
'broadcast_max_payload_size' => 9000,
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -14,6 +14,19 @@ return [
|
|||||||
*/
|
*/
|
||||||
'limited_broadcast_number' => 100,
|
'limited_broadcast_number' => 100,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum number of iterations that can be sent in a single broadcast.
|
||||||
|
*/
|
||||||
'broadcast_iteration_size' => 75,
|
'broadcast_iteration_size' => 75,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide the weight columns in the iteration table above this count.
|
||||||
|
*/
|
||||||
|
'max_displayed_weights' => 25,
|
||||||
|
|
||||||
|
'run_inputs_validation' => [
|
||||||
|
'hidden_layers' => 'required|integer|min:1|max:5',
|
||||||
|
'hidden_layers_neurons' => 'required|integer|min:1|max:5',
|
||||||
|
'max_iterations' => 'required|integer|min:1|max:5000',
|
||||||
|
]
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const props = defineProps<{
|
|||||||
iterations: Iteration[];
|
iterations: Iteration[];
|
||||||
trainingEnded: boolean;
|
trainingEnded: boolean;
|
||||||
trainingEndReason: string;
|
trainingEndReason: string;
|
||||||
|
maxDisplayedWeights: number;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
// All weight in a simple array
|
// All weight in a simple array
|
||||||
@@ -16,6 +17,12 @@ const allWeightPerIteration: ComputedRef<number[][]> = computed(() => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const displayedWeights = computed(() => {
|
||||||
|
const weights = allWeightPerIteration.value.find((weights) => weights.length > 0) || [];
|
||||||
|
|
||||||
|
return weights.length <= props.maxDisplayedWeights ? weights : [];
|
||||||
|
});
|
||||||
|
|
||||||
const rowBgDark = computed(() => {
|
const rowBgDark = computed(() => {
|
||||||
let isEven = false;
|
let isEven = false;
|
||||||
return props.iterations.map((iteration, index, arr) => {
|
return props.iterations.map((iteration, index, arr) => {
|
||||||
@@ -33,7 +40,7 @@ const rowBgDark = computed(() => {
|
|||||||
<th>Époch</th>
|
<th>Époch</th>
|
||||||
<th>Exemple</th>
|
<th>Exemple</th>
|
||||||
<th
|
<th
|
||||||
v-for="(weight, index) in allWeightPerIteration[0]"
|
v-for="(weight, index) in displayedWeights"
|
||||||
v-bind:key="index"
|
v-bind:key="index"
|
||||||
>
|
>
|
||||||
X<sub>{{ index }}</sub>
|
X<sub>{{ index }}</sub>
|
||||||
@@ -49,12 +56,14 @@ const rowBgDark = computed(() => {
|
|||||||
>
|
>
|
||||||
<td>{{ iteration.epoch }}</td>
|
<td>{{ iteration.epoch }}</td>
|
||||||
<td>{{ iteration.exampleIndex }}</td>
|
<td>{{ iteration.exampleIndex }}</td>
|
||||||
|
<template v-if="displayedWeights.length > 0">
|
||||||
<td
|
<td
|
||||||
v-for="(weight, index) in allWeightPerIteration[index]"
|
v-for="(weight, weightIndex) in allWeightPerIteration[index]"
|
||||||
v-bind:key="index"
|
v-bind:key="weightIndex"
|
||||||
>
|
>
|
||||||
{{ weight.toFixed(2) }}
|
{{ weight.toFixed(2) }}
|
||||||
</td>
|
</td>
|
||||||
|
</template>
|
||||||
<td>{{ iteration.error.toFixed(2) }}</td>
|
<td>{{ iteration.error.toFixed(2) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,16 @@ import { Chart } from 'vue-chartjs';
|
|||||||
import { colors, gridColor, gridColorBold } from '@/types/graphs';
|
import { colors, gridColor, gridColorBold } from '@/types/graphs';
|
||||||
import type { Iteration } from '@/types/perceptron';
|
import type { Iteration } from '@/types/perceptron';
|
||||||
|
|
||||||
|
type GraphDataset = ChartDataset<
|
||||||
|
keyof ChartTypeRegistry,
|
||||||
|
(number | Point | [number, number] | BubbleDataPoint | null)[]
|
||||||
|
>;
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
cleanedDataset: { label: number; data: { x: number; y: number }[] }[];
|
cleanedDataset: { label: number; data: { x: number; y: number }[] }[];
|
||||||
iterations: Iteration[];
|
iterations: Iteration[];
|
||||||
activationFunction: (x: number) => number;
|
activationFunction: (x: number) => number;
|
||||||
|
isRegression: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const examplesNumber = computed(() => {
|
const examplesNumber = computed(() => {
|
||||||
@@ -60,8 +66,9 @@ const farTopDataPointY = computed(() => {
|
|||||||
function getPerceptronOutput(
|
function getPerceptronOutput(
|
||||||
weightsNetwork: number[][][],
|
weightsNetwork: number[][][],
|
||||||
inputs: number[],
|
inputs: number[],
|
||||||
|
activationFunction: (x: number) => number = props.activationFunction,
|
||||||
): number[] {
|
): number[] {
|
||||||
for (const layer of weightsNetwork) {
|
for (const [layerIndex, layer] of weightsNetwork.entries()) {
|
||||||
const nextInputs: number[] = [];
|
const nextInputs: number[] = [];
|
||||||
|
|
||||||
for (const neuron of layer) {
|
for (const neuron of layer) {
|
||||||
@@ -74,7 +81,8 @@ function getPerceptronOutput(
|
|||||||
sum += weights[i] * inputs[i];
|
sum += weights[i] * inputs[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
const activated = props.activationFunction(sum);
|
const isOutputLayer = layerIndex === weightsNetwork.length - 1;
|
||||||
|
const activated = isOutputLayer ? sum : activationFunction(sum);
|
||||||
|
|
||||||
nextInputs.push(activated);
|
nextInputs.push(activated);
|
||||||
}
|
}
|
||||||
@@ -84,17 +92,126 @@ function getPerceptronOutput(
|
|||||||
return inputs;
|
return inputs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeNetworkWeights(weightsNetwork: number[][][][] | number[][][]): number[][][] {
|
||||||
|
if (
|
||||||
|
weightsNetwork.length === 1 &&
|
||||||
|
weightsNetwork[0].length === 1 &&
|
||||||
|
Array.isArray(weightsNetwork[0][0]) &&
|
||||||
|
Array.isArray(weightsNetwork[0][0][0])
|
||||||
|
) {
|
||||||
|
return weightsNetwork[0][0] as unknown as number[][][];
|
||||||
|
}
|
||||||
|
|
||||||
|
return weightsNetwork as number[][][];
|
||||||
|
}
|
||||||
|
|
||||||
const nonLinearGraph = ref<boolean>(false);
|
const nonLinearGraph = ref<boolean>(false);
|
||||||
function getPerceptronDecisionBoundaryDataset(
|
function getPerceptronDecisionBoundaryDataset(
|
||||||
networkWeights: number[][][],
|
rawNetworkWeights: number[][][] | number[][][][],
|
||||||
activationFunction: (x: number) => number = (x) => x,
|
activationFunction: (x: number) => number = (x) => x,
|
||||||
): ChartDataset<
|
): GraphDataset[] {
|
||||||
keyof ChartTypeRegistry,
|
const networkWeights = normalizeNetworkWeights(rawNetworkWeights);
|
||||||
number | Point | [number, number] | BubbleDataPoint | null
|
|
||||||
>[] {
|
|
||||||
const label = 'Ligne de décision du Perceptron';
|
const label = 'Ligne de décision du Perceptron';
|
||||||
console.log('Calculating decision boundary with weights:', networkWeights);
|
console.log('Calculating decision boundary with weights:', networkWeights);
|
||||||
|
|
||||||
|
if (props.isRegression) {
|
||||||
|
const hiddenActivation = (value: number) =>
|
||||||
|
1 / (1 + Math.exp(-value));
|
||||||
|
const inputCount = networkWeights[0]?.[0]?.length - 1;
|
||||||
|
|
||||||
|
if (inputCount === 1) {
|
||||||
|
if (networkWeights.length > 1) {
|
||||||
|
nonLinearGraph.value = true;
|
||||||
|
const data: Point[] = [];
|
||||||
|
const firstIntegerX = Math.ceil(farLeftDataPointX.value - 1);
|
||||||
|
const lastIntegerX = Math.floor(farRightDataPointX.value + 1);
|
||||||
|
|
||||||
|
for (
|
||||||
|
let x = firstIntegerX;
|
||||||
|
x <= lastIntegerX;
|
||||||
|
x+= 0.1
|
||||||
|
) {
|
||||||
|
data.push({
|
||||||
|
x,
|
||||||
|
y: getPerceptronOutput(
|
||||||
|
networkWeights,
|
||||||
|
[x],
|
||||||
|
hiddenActivation,
|
||||||
|
)[0],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: 'line',
|
||||||
|
label: 'Prédictions de régression',
|
||||||
|
data,
|
||||||
|
borderColor: '#FFF',
|
||||||
|
backgroundColor: '#FFF',
|
||||||
|
pointBackgroundColor: '#FFF',
|
||||||
|
pointRadius: 0,
|
||||||
|
borderWidth: 2,
|
||||||
|
tension: 0.4,
|
||||||
|
order: -1,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
nonLinearGraph.value = false;
|
||||||
|
const data: Point[] = [
|
||||||
|
{
|
||||||
|
x: farLeftDataPointX.value - 1,
|
||||||
|
y: getPerceptronOutput(
|
||||||
|
networkWeights,
|
||||||
|
[farLeftDataPointX.value - 1],
|
||||||
|
hiddenActivation,
|
||||||
|
)[0],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: farRightDataPointX.value + 1,
|
||||||
|
y: getPerceptronOutput(
|
||||||
|
networkWeights,
|
||||||
|
[farRightDataPointX.value + 1],
|
||||||
|
hiddenActivation,
|
||||||
|
)[0],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: 'line',
|
||||||
|
label: 'Régression du Perceptron',
|
||||||
|
data,
|
||||||
|
borderColor: '#FFF',
|
||||||
|
borderWidth: 2,
|
||||||
|
pointRadius: 0,
|
||||||
|
order: -1,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
nonLinearGraph.value = true;
|
||||||
|
const predictionPoints = props.cleanedDataset.flatMap((dataset) =>
|
||||||
|
dataset.data.map((point) => ({
|
||||||
|
x: point.x,
|
||||||
|
y: point.y,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: 'scatter',
|
||||||
|
label: 'Prédictions de régression',
|
||||||
|
data: predictionPoints,
|
||||||
|
backgroundColor: '#FFF',
|
||||||
|
pointBackgroundColor: '#FFF8',
|
||||||
|
pointRadius: 5,
|
||||||
|
borderWidth: 0,
|
||||||
|
order: -1,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
networkWeights.length == 1 &&
|
networkWeights.length == 1 &&
|
||||||
networkWeights[0].length == 1 &&
|
networkWeights[0].length == 1 &&
|
||||||
@@ -107,7 +224,7 @@ function getPerceptronDecisionBoundaryDataset(
|
|||||||
function perceptronLine(x: number): number {
|
function perceptronLine(x: number): number {
|
||||||
if (perceptronWeights.length < 3) {
|
if (perceptronWeights.length < 3) {
|
||||||
// If we have less than 3 weights, we assume missing weights are zero
|
// If we have less than 3 weights, we assume missing weights are zero
|
||||||
return getPerceptronOutput(networkWeights, [x])[0];
|
return getPerceptronOutput(networkWeights, [x], activationFunction)[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
// w0 + w1*x + w2*y = 0 => y = -(w1/w2)*x - w0/w2
|
// w0 + w1*x + w2*y = 0 => y = -(w1/w2)*x - w0/w2
|
||||||
@@ -139,15 +256,14 @@ function getPerceptronDecisionBoundaryDataset(
|
|||||||
nonLinearGraph.value = true;
|
nonLinearGraph.value = true;
|
||||||
|
|
||||||
const bubbleTransparency = '30';
|
const bubbleTransparency = '30';
|
||||||
const isInDataThreshold = 0.0;
|
|
||||||
|
|
||||||
|
// -------- Construction des datasets --------
|
||||||
// -------- 1️⃣ Construction des datasets --------
|
|
||||||
const datasets: {
|
const datasets: {
|
||||||
type: string;
|
type: 'scatter';
|
||||||
label: string;
|
label: string;
|
||||||
data: Point[];
|
data: Point[];
|
||||||
backgroundColor: string;
|
backgroundColor: string;
|
||||||
|
pointBackgroundColor: string;
|
||||||
pointRadius: number;
|
pointRadius: number;
|
||||||
borderWidth: number;
|
borderWidth: number;
|
||||||
order: number;
|
order: number;
|
||||||
@@ -155,11 +271,21 @@ function getPerceptronDecisionBoundaryDataset(
|
|||||||
// For the number of neuron in the last layer
|
// For the number of neuron in the last layer
|
||||||
const lastLayer = networkWeights[networkWeights.length - 1];
|
const lastLayer = networkWeights[networkWeights.length - 1];
|
||||||
for (let i = 0; i < lastLayer.length; i++) {
|
for (let i = 0; i < lastLayer.length; i++) {
|
||||||
const dataset = {
|
const dataset: {
|
||||||
|
type: 'scatter';
|
||||||
|
label: string;
|
||||||
|
data: Point[];
|
||||||
|
backgroundColor: string;
|
||||||
|
pointBackgroundColor: string;
|
||||||
|
pointRadius: number;
|
||||||
|
borderWidth: number;
|
||||||
|
order: number;
|
||||||
|
} = {
|
||||||
type: 'scatter',
|
type: 'scatter',
|
||||||
label: label,
|
label: label,
|
||||||
data: [], // Will be filled with the decision boundary points
|
data: [], // Will be filled with the decision boundary points
|
||||||
backgroundColor: colors[i] + bubbleTransparency || '#AAA',
|
backgroundColor: colors[i] || '#AAA',
|
||||||
|
pointBackgroundColor: (colors[i] || '#AAA') + bubbleTransparency,
|
||||||
pointRadius: 15,
|
pointRadius: 15,
|
||||||
borderWidth: 0,
|
borderWidth: 0,
|
||||||
order: -1,
|
order: -1,
|
||||||
@@ -167,7 +293,7 @@ function getPerceptronDecisionBoundaryDataset(
|
|||||||
datasets.push(dataset);
|
datasets.push(dataset);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------- 2️⃣ Échantillonnage grille --------
|
// -------- Échantillonnage grille --------
|
||||||
const step =
|
const step =
|
||||||
Math.abs(
|
Math.abs(
|
||||||
farRightDataPointX.value + 1 - (farLeftDataPointX.value - 1),
|
farRightDataPointX.value + 1 - (farLeftDataPointX.value - 1),
|
||||||
@@ -183,16 +309,21 @@ function getPerceptronDecisionBoundaryDataset(
|
|||||||
y <= farTopDataPointY.value + 1;
|
y <= farTopDataPointY.value + 1;
|
||||||
y += step
|
y += step
|
||||||
) {
|
) {
|
||||||
const values = getPerceptronOutput(networkWeights, [x, y]);
|
const values = getPerceptronOutput(
|
||||||
values.forEach((v, i) => {
|
networkWeights,
|
||||||
if (v > isInDataThreshold) {
|
[x, y],
|
||||||
datasets[i].data.push({ x, y });
|
activationFunction,
|
||||||
|
);
|
||||||
|
const dominantValue = Math.max(...values);
|
||||||
|
const dominantIndex = values.indexOf(dominantValue);
|
||||||
|
|
||||||
|
if (dominantIndex >= 0) {
|
||||||
|
datasets[dominantIndex].data.push({ x, y });
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------- 3️⃣ Dataset ChartJS --------
|
// -------- Dataset ChartJS --------
|
||||||
return datasets;
|
return datasets;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -259,7 +390,7 @@ function getPerceptronDecisionBoundaryDataset(
|
|||||||
datasets: [
|
datasets: [
|
||||||
// Points from the dataset
|
// Points from the dataset
|
||||||
...props.cleanedDataset.map((dataset, index) => ({
|
...props.cleanedDataset.map((dataset, index) => ({
|
||||||
type: 'scatter',
|
type: 'scatter' as const,
|
||||||
label: `Label ${dataset.label}`,
|
label: `Label ${dataset.label}`,
|
||||||
data: dataset.data,
|
data: dataset.data,
|
||||||
backgroundColor: colors[index] || '#AAA',
|
backgroundColor: colors[index] || '#AAA',
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import Toggle from './ui/toggle/Toggle.vue';
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
iterations: Iteration[];
|
iterations: Iteration[];
|
||||||
|
isRegression: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const epochErrorOnly = ref<boolean>(false);
|
const epochErrorOnly = ref<boolean>(false);
|
||||||
@@ -42,7 +43,11 @@ const datasets = computed<
|
|||||||
};
|
};
|
||||||
datasets.push(dataset);
|
datasets.push(dataset);
|
||||||
}
|
}
|
||||||
dataset.data.push(iteration.error);
|
dataset.data.push(
|
||||||
|
props.isRegression
|
||||||
|
? Math.abs(iteration.error)
|
||||||
|
: iteration.error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
exampleCountPerEpoch[iteration.epoch] = (exampleCountPerEpoch[iteration.epoch] || 0) + 1;
|
exampleCountPerEpoch[iteration.epoch] = (exampleCountPerEpoch[iteration.epoch] || 0) + 1;
|
||||||
@@ -91,7 +96,9 @@ const datasets = computed<
|
|||||||
plugins: {
|
plugins: {
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true,
|
||||||
text: 'Nombre d\'erreurs par epoch',
|
text: props.isRegression
|
||||||
|
? 'Erreur de prédiction par epoch'
|
||||||
|
: 'Nombre d\'erreurs par epoch',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
animation: {
|
animation: {
|
||||||
@@ -104,7 +111,7 @@ const datasets = computed<
|
|||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
stacked: true,
|
stacked: true,
|
||||||
beginAtZero: true,
|
beginAtZero: !props.isRegression,
|
||||||
grid: {
|
grid: {
|
||||||
color: function (context) {
|
color: function (context) {
|
||||||
if (context.tick.value == 0) {
|
if (context.tick.value == 0) {
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ const props = defineProps<{
|
|||||||
datasets: Dataset[];
|
datasets: Dataset[];
|
||||||
selectedDataset: string;
|
selectedDataset: string;
|
||||||
initializationMethod: InitializationMethod;
|
initializationMethod: InitializationMethod;
|
||||||
|
hiddenLayers: number;
|
||||||
|
hiddenLayersNeurons: number;
|
||||||
minError: number;
|
minError: number;
|
||||||
defaultLearningRate: number;
|
defaultLearningRate: number;
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
@@ -38,6 +40,8 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const selectedDatasetCopy = ref(props.selectedDataset);
|
const selectedDatasetCopy = ref(props.selectedDataset);
|
||||||
const selectedMethod = ref(props.initializationMethod);
|
const selectedMethod = ref(props.initializationMethod);
|
||||||
|
const hiddenLayers = ref(props.hiddenLayers);
|
||||||
|
const hiddenLayersNeurons = ref(props.hiddenLayersNeurons);
|
||||||
const minError = ref(props.minError);
|
const minError = ref(props.minError);
|
||||||
const learningRate = ref(props.defaultLearningRate);
|
const learningRate = ref(props.defaultLearningRate);
|
||||||
const maxIterations = ref(props.defaultMaxIterations);
|
const maxIterations = ref(props.defaultMaxIterations);
|
||||||
@@ -59,6 +63,9 @@ watch(selectedDatasetCopy, (newvalue) => {
|
|||||||
}
|
}
|
||||||
// MaxIterations
|
// MaxIterations
|
||||||
maxIterations.value = props.defaultMaxIterations;
|
maxIterations.value = props.defaultMaxIterations;
|
||||||
|
if (selectedDatasetCopy && selectedDatasetCopy.defaultMaxIterations !== undefined) {
|
||||||
|
maxIterations.value = selectedDatasetCopy.defaultMaxIterations;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const trainingId = ref<string>('');
|
const trainingId = ref<string>('');
|
||||||
@@ -82,6 +89,8 @@ function startTraining() {
|
|||||||
type: props.type,
|
type: props.type,
|
||||||
dataset: selectedDatasetCopy.value,
|
dataset: selectedDatasetCopy.value,
|
||||||
weight_init_method: selectedMethod.value,
|
weight_init_method: selectedMethod.value,
|
||||||
|
hidden_layers: hiddenLayers.value,
|
||||||
|
hidden_layers_neurons: hiddenLayersNeurons.value,
|
||||||
min_error: minError.value,
|
min_error: minError.value,
|
||||||
learning_rate: learningRate.value,
|
learning_rate: learningRate.value,
|
||||||
session_id: props.sessionId,
|
session_id: props.sessionId,
|
||||||
@@ -158,7 +167,7 @@ watch(selectedDatasetCopy, (newValue) => {
|
|||||||
class="cursor-pointer"
|
class="cursor-pointer"
|
||||||
>
|
>
|
||||||
<NativeSelectOption
|
<NativeSelectOption
|
||||||
v-for="method in ['zeros', 'random']"
|
v-for="method in (props.type == 'multilayer' ? ['random'] : ['zeros', 'random'])"
|
||||||
v-bind:key="method"
|
v-bind:key="method"
|
||||||
:value="method"
|
:value="method"
|
||||||
>
|
>
|
||||||
@@ -169,6 +178,42 @@ watch(selectedDatasetCopy, (newValue) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
<!-- HIDDEN LAYERS -->
|
||||||
|
<FormField name="hidden_layers" v-if="props.type === 'multilayer'">
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Nombre de couches cachées</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<!-- TODO : MAX input -->
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
v-model="hiddenLayers"
|
||||||
|
min="1"
|
||||||
|
max="5"
|
||||||
|
step="1"
|
||||||
|
class="w-min"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<!-- HIDDEN LAYERS NEURONS -->
|
||||||
|
<FormField name="hidden_layers_neurons" v-if="props.type === 'multilayer'">
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Nombre de neurones par couche cachée</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<!-- TODO : MAX input -->
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
v-model="hiddenLayersNeurons"
|
||||||
|
min="1"
|
||||||
|
max="5"
|
||||||
|
step="1"
|
||||||
|
class="w-min"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
<!-- MIN ERROR -->
|
<!-- MIN ERROR -->
|
||||||
<FormField name="min_error" v-if="props.type !== 'simple'">
|
<FormField name="min_error" v-if="props.type !== 'simple'">
|
||||||
<FormItem>
|
<FormItem>
|
||||||
@@ -210,6 +255,7 @@ watch(selectedDatasetCopy, (newValue) => {
|
|||||||
type="number"
|
type="number"
|
||||||
v-model="maxIterations"
|
v-model="maxIterations"
|
||||||
min="0"
|
min="0"
|
||||||
|
max="5000"
|
||||||
step="1"
|
step="1"
|
||||||
class="w-min"
|
class="w-min"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ const props = defineProps<{
|
|||||||
minError: number;
|
minError: number;
|
||||||
learningRate: number;
|
learningRate: number;
|
||||||
maxIterations: number;
|
maxIterations: number;
|
||||||
|
maxDisplayedWeights: number;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const selectedDatasetName = ref<string>('');
|
const selectedDatasetName = ref<string>('');
|
||||||
@@ -82,7 +83,9 @@ const cleanedDataset = computed<
|
|||||||
});
|
});
|
||||||
return cleanedDataset;
|
return cleanedDataset;
|
||||||
});
|
});
|
||||||
const initializationMethod = ref<InitializationMethod>('zeros');
|
const hiddenLayers = ref(3);
|
||||||
|
const hiddenLayersNeurons = ref(3);
|
||||||
|
const initializationMethod = ref<InitializationMethod>(props.type === 'multilayer' ? 'random' : 'zeros');
|
||||||
|
|
||||||
console.log('Session ID:', props.sessionId);
|
console.log('Session ID:', props.sessionId);
|
||||||
|
|
||||||
@@ -145,7 +148,7 @@ function perceptroninitialization(data: any) {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
activationFunction.value = data.activation_function;
|
activationFunction.value = data.activationFunction;
|
||||||
}
|
}
|
||||||
function getActivationFunction(type: string): (x: number) => number {
|
function getActivationFunction(type: string): (x: number) => number {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -179,6 +182,8 @@ function resetTraining() {
|
|||||||
:datasets="props.datasets"
|
:datasets="props.datasets"
|
||||||
:selectedDataset="selectedDatasetName"
|
:selectedDataset="selectedDatasetName"
|
||||||
:initializationMethod="initializationMethod"
|
:initializationMethod="initializationMethod"
|
||||||
|
:hidden-layers="hiddenLayers"
|
||||||
|
:hidden-layers-neurons="hiddenLayersNeurons"
|
||||||
:minError="props.minError"
|
:minError="props.minError"
|
||||||
:sessionId="props.sessionId"
|
:sessionId="props.sessionId"
|
||||||
:defaultLearningRate="props.learningRate"
|
:defaultLearningRate="props.learningRate"
|
||||||
@@ -203,6 +208,7 @@ function resetTraining() {
|
|||||||
:iterations="iterations"
|
:iterations="iterations"
|
||||||
:trainingEnded="trainingEnded"
|
:trainingEnded="trainingEnded"
|
||||||
:trainingEndReason="trainingEndReason"
|
:trainingEndReason="trainingEndReason"
|
||||||
|
:maxDisplayedWeights="props.maxDisplayedWeights"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="sticky top-0 h-full w-full">
|
<div class="sticky top-0 h-full w-full">
|
||||||
@@ -210,6 +216,7 @@ function resetTraining() {
|
|||||||
<PerceptronDecisionGraph
|
<PerceptronDecisionGraph
|
||||||
:cleanedDataset="cleanedDataset"
|
:cleanedDataset="cleanedDataset"
|
||||||
:iterations="iterations"
|
:iterations="iterations"
|
||||||
|
:is-regression="activationFunction === 'linear'"
|
||||||
:activation-function="
|
:activation-function="
|
||||||
getActivationFunction(activationFunction)
|
getActivationFunction(activationFunction)
|
||||||
"
|
"
|
||||||
@@ -218,6 +225,7 @@ function resetTraining() {
|
|||||||
<div>
|
<div>
|
||||||
<PerceptronIterationsErrorsGraph
|
<PerceptronIterationsErrorsGraph
|
||||||
:iterations="iterations"
|
:iterations="iterations"
|
||||||
|
:is-regression="activationFunction === 'linear'"
|
||||||
v-if="iterations.length > 0"
|
v-if="iterations.length > 0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export type Dataset = {
|
|||||||
data: DatasetPoint[];
|
data: DatasetPoint[];
|
||||||
defaultLearningRate?: number;
|
defaultLearningRate?: number;
|
||||||
defaultMinError?: number;
|
defaultMinError?: number;
|
||||||
|
defaultMaxIterations?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DatasetPoint = {
|
export type DatasetPoint = {
|
||||||
@@ -20,4 +21,4 @@ export type DatasetPoint = {
|
|||||||
|
|
||||||
export type InitializationMethod = 'zeros' | 'random';
|
export type InitializationMethod = 'zeros' | 'random';
|
||||||
|
|
||||||
export type PerceptronType = 'simple';
|
export type PerceptronType = 'simple' | 'gradientdescent' | 'adaline' | 'monolayer' | 'multilayer';
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit\Training;
|
||||||
|
|
||||||
|
use App\Models\NetworksTraining\MultiLayerPerceptronTraining;
|
||||||
|
use App\Services\DatasetReader\LinearOrderDataSetReader;
|
||||||
|
use App\Services\SynapticWeightsProvider\ZeroSynapticWeights;
|
||||||
|
use Tests\Services\IterationEventBuffer\DullIterationEventBuffer;
|
||||||
|
|
||||||
|
class MultiLayerPerceptronTest extends TrainingTestCase
|
||||||
|
{
|
||||||
|
public function test_multilayer_perceptron_uses_one_output_for_continuous_targets(): void
|
||||||
|
{
|
||||||
|
$training = new MultiLayerPerceptronTraining(
|
||||||
|
datasetReader: new LinearOrderDataSetReader(public_path('data_sets/table_4_17.csv')),
|
||||||
|
learningRate: 0.01,
|
||||||
|
maxEpochs: 1,
|
||||||
|
hiddenLayers: 1,
|
||||||
|
hiddenLayersNeurons: 3,
|
||||||
|
synapticWeightsProvider: new ZeroSynapticWeights,
|
||||||
|
iterationEventBuffer: new DullIterationEventBuffer,
|
||||||
|
sessionId: 'test-session',
|
||||||
|
trainingId: 'test-training',
|
||||||
|
minError: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
$training->start();
|
||||||
|
|
||||||
|
$weights = $training->getSynapticWeights()[0][0];
|
||||||
|
|
||||||
|
$this->assertCount(1, $weights[count($weights) - 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user