Multilayer neuron network
linter / quality (push) Successful in 7m10s
tests / ci (8.4) (push) Successful in 4m43s
tests / ci (8.5) (push) Successful in 4m47s

This commit is contained in:
2026-09-08 16:29:02 +02:00
parent 2f4db07918
commit 08aa04fe56
17 changed files with 630 additions and 50 deletions
+10 -1
View File
@@ -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,
]; ];
} }
+41 -6
View File
@@ -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()]];
}
}
+1 -1
View File
@@ -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;
} }
+5
View File
@@ -79,4 +79,9 @@ return [
], ],
/**
* Keep broadcast payloads below Pusher's event size limit.
*/
'broadcast_max_payload_size' => 9000,
]; ];
+13
View File
@@ -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',
]
]; ];
+12 -3
View File
@@ -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) {
+47 -1
View File
@@ -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"
/> />
+10 -2
View File
@@ -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>
+2 -1
View File
@@ -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]);
}
}