diff --git a/app/Events/PerceptronTrainingIteration.php b/app/Events/PerceptronTrainingIteration.php index f003ded..b00f4e0 100644 --- a/app/Events/PerceptronTrainingIteration.php +++ b/app/Events/PerceptronTrainingIteration.php @@ -38,8 +38,17 @@ class PerceptronTrainingIteration implements ShouldBroadcast 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 [ - 'iterations' => $this->iterations, + 'iterations' => $iterations, 'trainingId' => $this->trainingId, ]; } diff --git a/app/Http/Controllers/PerceptronController.php b/app/Http/Controllers/PerceptronController.php index 16e5bcb..b6ab342 100644 --- a/app/Http/Controllers/PerceptronController.php +++ b/app/Http/Controllers/PerceptronController.php @@ -6,6 +6,7 @@ use App\Events\PerceptronInitialization; use App\Models\NetworksTraining\ADALINEPerceptronTraining; use App\Models\NetworksTraining\GradientDescentPerceptronTraining; use App\Models\NetworksTraining\MonoLayerPerceptronTraining; +use App\Models\NetworksTraining\MultiLayerPerceptronTraining; use App\Models\NetworksTraining\SimpleBinaryPerceptronTraining; use App\Services\DatasetReader\IDataSetReader; use App\Services\DatasetReader\LinearOrderDataSetReader; @@ -13,12 +14,11 @@ use App\Services\DatasetReader\RandomOrderDataSetReader; use App\Services\IterationEventBuffer\PerceptronIterationEventBuffer; use App\Services\IterationEventBuffer\PerceptronLimitedEpochEventBuffer; use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider; +use App\Services\SynapticWeightsProvider\RandomSynapticWeights; use App\Services\SynapticWeightsProvider\ZeroSynapticWeights; -use Illuminate\Contracts\Queue\Job; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; -use Symfony\Contracts\EventDispatcher\Event; -use Tests\Services\IterationEventBuffer\DullIterationEventBuffer; +use Illuminate\Support\Facades\Validator; class PerceptronController extends Controller { @@ -38,9 +38,16 @@ class PerceptronController extends Controller $learningRate = 0.015; $maxIterations = 150; break; - case 'gradientdescent' || 'adaline': + case 'gradientdescent': + case 'adaline': $learningRate = 0.00003; break; + case 'monolayer': + $learningRate = 0.03; + break; + case 'multilayer': + $learningRate = 0.8; + break; } return inertia('PerceptronViewer', [ @@ -50,6 +57,7 @@ class PerceptronController extends Controller 'minError' => $minError, 'learningRate' => $learningRate, 'maxIterations' => $maxIterations, + 'maxDisplayedWeights' => config('perceptron.max_displayed_weights'), ]); } @@ -106,7 +114,8 @@ class PerceptronController extends Controller case 'simple': $dataset['defaultLearningRate'] = 0.015; break; - case 'gradientdescent' || 'adaline': + case 'gradientdescent': + case 'adaline': $dataset['defaultLearningRate'] = 0.001; break; } @@ -114,6 +123,15 @@ class PerceptronController extends Controller case 'table_2_11': $dataset['defaultMinError'] = 0.02; break; + case 'table_4_12': + switch ($perceptronType) { + case 'multilayer': + $dataset['defaultLearningRate'] = 0.8; + $dataset['defaultMinError'] = 0.001; + $dataset['defaultMaxIterations'] = 2000; + break; + } + break; } $datasets[] = $dataset; } @@ -133,7 +151,19 @@ class PerceptronController extends Controller { $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'); + $hiddenLayers = $request->input('hidden_layers', 2); + $hiddenLayersNeurons = $request->input('hidden_layers_neurons', 3); $minError = $request->input('min_error', 0.01); $weightInitMethod = $request->input('weight_init_method', 'random'); $dataSet = $request->input('dataset'); @@ -145,7 +175,11 @@ class PerceptronController extends Controller // Remove the jobs for the sessionId 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; } @@ -162,6 +196,7 @@ class PerceptronController extends Controller 'gradientdescent' => new GradientDescentPerceptronTraining($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), + 'multilayer' => new MultiLayerPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $hiddenLayers, $hiddenLayersNeurons, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError), default => null, }; diff --git a/app/Models/NetworksTraining/MonoLayerPerceptronTraining.php b/app/Models/NetworksTraining/MonoLayerPerceptronTraining.php index e8bfb13..524157c 100644 --- a/app/Models/NetworksTraining/MonoLayerPerceptronTraining.php +++ b/app/Models/NetworksTraining/MonoLayerPerceptronTraining.php @@ -7,8 +7,6 @@ use App\Models\ActivationsFunctions; use App\Models\Perceptrons\GradientDescentPerceptron; use App\Models\Perceptrons\NetworkPerceptron; use App\Models\Perceptrons\Perceptron; -use App\Models\Perceptrons\SimpleBinaryPerceptron2; -use App\Models\Perceptrons\SimpleBinaryPerceptron; use App\Services\DatasetReader\IDataSetReader; use App\Services\IterationEventBuffer\IPerceptronIterationEventBuffer; use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider; @@ -47,7 +45,7 @@ class MonoLayerPerceptronTraining extends NetworkTraining ), $datasetReader->getInputSize(), GradientDescentPerceptron::class, // No hidden layer - SimpleBinaryPerceptron2::class, + GradientDescentPerceptron::class, ); $this->labels = $datasetReader->getLabels(); } diff --git a/app/Models/NetworksTraining/MultiLayerPerceptronTraining.php b/app/Models/NetworksTraining/MultiLayerPerceptronTraining.php new file mode 100644 index 0000000..afdb5d3 --- /dev/null +++ b/app/Models/NetworksTraining/MultiLayerPerceptronTraining.php @@ -0,0 +1,259 @@ +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()]]; + } +} + diff --git a/app/Models/Perceptrons/NetworkPerceptron.php b/app/Models/Perceptrons/NetworkPerceptron.php index e218323..9407ee6 100644 --- a/app/Models/Perceptrons/NetworkPerceptron.php +++ b/app/Models/Perceptrons/NetworkPerceptron.php @@ -25,7 +25,7 @@ class NetworkPerceptron extends Perceptron } // Hidden Layer - for ($layerIndex = 0; $layerIndex < count($synaptic_weights) - 2; $layerIndex++) { + for ($layerIndex = 0; $layerIndex < count($synaptic_weights) - 1; $layerIndex++) { $this->network[$layerIndex + 1] = []; foreach ($synaptic_weights[$layerIndex] as $neuronWeights) { diff --git a/app/Models/Perceptrons/SigmoidPerceptron.php b/app/Models/Perceptrons/SigmoidPerceptron.php new file mode 100644 index 0000000..8f9f4e1 --- /dev/null +++ b/app/Models/Perceptrons/SigmoidPerceptron.php @@ -0,0 +1,13 @@ +data[] = [ + $iteration = [ 'epoch' => $epoch, 'exampleIndex' => $exampleIndex, 'error' => $error, '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 $this->underSizeIncreaseCount++; $this->flush(); diff --git a/app/Services/SynapticWeightsProvider/SimpleNetworkWeightsProvider.php b/app/Services/SynapticWeightsProvider/SimpleNetworkWeightsProvider.php index 2f34756..7565392 100644 --- a/app/Services/SynapticWeightsProvider/SimpleNetworkWeightsProvider.php +++ b/app/Services/SynapticWeightsProvider/SimpleNetworkWeightsProvider.php @@ -18,9 +18,11 @@ class SimpleNetworkWeightsProvider implements INetworkSynapticWeightsProvider // Generate Hidden Layer weights for ($hiddenLayerNeuronIndex = 0; $hiddenLayerNeuronIndex < $hidden_layers_count; $hiddenLayerNeuronIndex++) { + $layer = []; 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; } diff --git a/config/broadcasting.php b/config/broadcasting.php index ebc3fb9..48ae120 100644 --- a/config/broadcasting.php +++ b/config/broadcasting.php @@ -79,4 +79,9 @@ return [ ], + /** + * Keep broadcast payloads below Pusher's event size limit. + */ + 'broadcast_max_payload_size' => 9000, + ]; diff --git a/config/perceptron.php b/config/perceptron.php index 631d39c..90976bf 100644 --- a/config/perceptron.php +++ b/config/perceptron.php @@ -14,6 +14,19 @@ return [ */ 'limited_broadcast_number' => 100, + /** + * The maximum number of iterations that can be sent in a single broadcast. + */ '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', + ] ]; diff --git a/resources/js/components/IterationTable.vue b/resources/js/components/IterationTable.vue index 70aad49..0fbab5e 100644 --- a/resources/js/components/IterationTable.vue +++ b/resources/js/components/IterationTable.vue @@ -6,6 +6,7 @@ const props = defineProps<{ iterations: Iteration[]; trainingEnded: boolean; trainingEndReason: string; + maxDisplayedWeights: number; }>(); // All weight in a simple array @@ -16,6 +17,12 @@ const allWeightPerIteration: ComputedRef = computed(() => { }); }); +const displayedWeights = computed(() => { + const weights = allWeightPerIteration.value.find((weights) => weights.length > 0) || []; + + return weights.length <= props.maxDisplayedWeights ? weights : []; +}); + const rowBgDark = computed(() => { let isEven = false; return props.iterations.map((iteration, index, arr) => { @@ -33,7 +40,7 @@ const rowBgDark = computed(() => { Époch Exemple X{{ index }} @@ -49,12 +56,14 @@ const rowBgDark = computed(() => { > {{ iteration.epoch }} {{ iteration.exampleIndex }} - - {{ weight.toFixed(2) }} - + {{ iteration.error.toFixed(2) }} diff --git a/resources/js/components/PerceptronDecisionGraph.vue b/resources/js/components/PerceptronDecisionGraph.vue index ed5eec6..efc4a41 100644 --- a/resources/js/components/PerceptronDecisionGraph.vue +++ b/resources/js/components/PerceptronDecisionGraph.vue @@ -10,10 +10,16 @@ import { Chart } from 'vue-chartjs'; import { colors, gridColor, gridColorBold } from '@/types/graphs'; import type { Iteration } from '@/types/perceptron'; +type GraphDataset = ChartDataset< + keyof ChartTypeRegistry, + (number | Point | [number, number] | BubbleDataPoint | null)[] +>; + const props = defineProps<{ cleanedDataset: { label: number; data: { x: number; y: number }[] }[]; iterations: Iteration[]; activationFunction: (x: number) => number; + isRegression: boolean; }>(); const examplesNumber = computed(() => { @@ -60,8 +66,9 @@ const farTopDataPointY = computed(() => { function getPerceptronOutput( weightsNetwork: number[][][], inputs: number[], + activationFunction: (x: number) => number = props.activationFunction, ): number[] { - for (const layer of weightsNetwork) { + for (const [layerIndex, layer] of weightsNetwork.entries()) { const nextInputs: number[] = []; for (const neuron of layer) { @@ -74,7 +81,8 @@ function getPerceptronOutput( 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); } @@ -84,17 +92,126 @@ function getPerceptronOutput( 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(false); function getPerceptronDecisionBoundaryDataset( - networkWeights: number[][][], + rawNetworkWeights: number[][][] | number[][][][], activationFunction: (x: number) => number = (x) => x, -): ChartDataset< - keyof ChartTypeRegistry, - number | Point | [number, number] | BubbleDataPoint | null ->[] { +): GraphDataset[] { + const networkWeights = normalizeNetworkWeights(rawNetworkWeights); const label = 'Ligne de décision du Perceptron'; 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 ( networkWeights.length == 1 && networkWeights[0].length == 1 && @@ -107,7 +224,7 @@ function getPerceptronDecisionBoundaryDataset( function perceptronLine(x: number): number { if (perceptronWeights.length < 3) { // 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 @@ -139,15 +256,14 @@ function getPerceptronDecisionBoundaryDataset( nonLinearGraph.value = true; const bubbleTransparency = '30'; - const isInDataThreshold = 0.0; - - // -------- 1️⃣ Construction des datasets -------- + // -------- Construction des datasets -------- const datasets: { - type: string; + type: 'scatter'; label: string; data: Point[]; backgroundColor: string; + pointBackgroundColor: string; pointRadius: number; borderWidth: number; order: number; @@ -155,11 +271,21 @@ function getPerceptronDecisionBoundaryDataset( // For the number of neuron in the last layer const lastLayer = networkWeights[networkWeights.length - 1]; 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', label: label, 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, borderWidth: 0, order: -1, @@ -167,7 +293,7 @@ function getPerceptronDecisionBoundaryDataset( datasets.push(dataset); } - // -------- 2️⃣ Échantillonnage grille -------- + // -------- Échantillonnage grille -------- const step = Math.abs( farRightDataPointX.value + 1 - (farLeftDataPointX.value - 1), @@ -183,16 +309,21 @@ function getPerceptronDecisionBoundaryDataset( y <= farTopDataPointY.value + 1; y += step ) { - const values = getPerceptronOutput(networkWeights, [x, y]); - values.forEach((v, i) => { - if (v > isInDataThreshold) { - datasets[i].data.push({ x, y }); - } - }); + const values = getPerceptronOutput( + networkWeights, + [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; } } @@ -259,7 +390,7 @@ function getPerceptronDecisionBoundaryDataset( datasets: [ // Points from the dataset ...props.cleanedDataset.map((dataset, index) => ({ - type: 'scatter', + type: 'scatter' as const, label: `Label ${dataset.label}`, data: dataset.data, backgroundColor: colors[index] || '#AAA', diff --git a/resources/js/components/PerceptronIterationsErrorsGraph.vue b/resources/js/components/PerceptronIterationsErrorsGraph.vue index 89147c3..5dcdcfd 100644 --- a/resources/js/components/PerceptronIterationsErrorsGraph.vue +++ b/resources/js/components/PerceptronIterationsErrorsGraph.vue @@ -8,6 +8,7 @@ import Toggle from './ui/toggle/Toggle.vue'; const props = defineProps<{ iterations: Iteration[]; + isRegression: boolean; }>(); const epochErrorOnly = ref(false); @@ -42,7 +43,11 @@ const datasets = computed< }; 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; @@ -91,7 +96,9 @@ const datasets = computed< plugins: { title: { display: true, - text: 'Nombre d\'erreurs par epoch', + text: props.isRegression + ? 'Erreur de prédiction par epoch' + : 'Nombre d\'erreurs par epoch', }, }, animation: { @@ -104,7 +111,7 @@ const datasets = computed< }, y: { stacked: true, - beginAtZero: true, + beginAtZero: !props.isRegression, grid: { color: function (context) { if (context.tick.value == 0) { diff --git a/resources/js/components/PerceptronSetup.vue b/resources/js/components/PerceptronSetup.vue index 0addc60..732a59a 100644 --- a/resources/js/components/PerceptronSetup.vue +++ b/resources/js/components/PerceptronSetup.vue @@ -30,6 +30,8 @@ const props = defineProps<{ datasets: Dataset[]; selectedDataset: string; initializationMethod: InitializationMethod; + hiddenLayers: number; + hiddenLayersNeurons: number; minError: number; defaultLearningRate: number; sessionId: string; @@ -38,6 +40,8 @@ const props = defineProps<{ const selectedDatasetCopy = ref(props.selectedDataset); const selectedMethod = ref(props.initializationMethod); +const hiddenLayers = ref(props.hiddenLayers); +const hiddenLayersNeurons = ref(props.hiddenLayersNeurons); const minError = ref(props.minError); const learningRate = ref(props.defaultLearningRate); const maxIterations = ref(props.defaultMaxIterations); @@ -59,6 +63,9 @@ watch(selectedDatasetCopy, (newvalue) => { } // MaxIterations maxIterations.value = props.defaultMaxIterations; + if (selectedDatasetCopy && selectedDatasetCopy.defaultMaxIterations !== undefined) { + maxIterations.value = selectedDatasetCopy.defaultMaxIterations; + } }) const trainingId = ref(''); @@ -82,6 +89,8 @@ function startTraining() { type: props.type, dataset: selectedDatasetCopy.value, weight_init_method: selectedMethod.value, + hidden_layers: hiddenLayers.value, + hidden_layers_neurons: hiddenLayersNeurons.value, min_error: minError.value, learning_rate: learningRate.value, session_id: props.sessionId, @@ -158,7 +167,7 @@ watch(selectedDatasetCopy, (newValue) => { class="cursor-pointer" > @@ -169,6 +178,42 @@ watch(selectedDatasetCopy, (newValue) => { + + + + Nombre de couches cachées + + + + + + + + + + + Nombre de neurones par couche cachée + + + + + + + @@ -210,6 +255,7 @@ watch(selectedDatasetCopy, (newValue) => { type="number" v-model="maxIterations" min="0" + max="5000" step="1" class="w-min" /> diff --git a/resources/js/pages/PerceptronViewer.vue b/resources/js/pages/PerceptronViewer.vue index 95c3c42..e221e2b 100644 --- a/resources/js/pages/PerceptronViewer.vue +++ b/resources/js/pages/PerceptronViewer.vue @@ -47,6 +47,7 @@ const props = defineProps<{ minError: number; learningRate: number; maxIterations: number; + maxDisplayedWeights: number; }>(); const selectedDatasetName = ref(''); @@ -82,7 +83,9 @@ const cleanedDataset = computed< }); return cleanedDataset; }); -const initializationMethod = ref('zeros'); +const hiddenLayers = ref(3); +const hiddenLayersNeurons = ref(3); +const initializationMethod = ref(props.type === 'multilayer' ? 'random' : 'zeros'); console.log('Session ID:', props.sessionId); @@ -145,7 +148,7 @@ function perceptroninitialization(data: any) { ); return; } - activationFunction.value = data.activation_function; + activationFunction.value = data.activationFunction; } function getActivationFunction(type: string): (x: number) => number { switch (type) { @@ -179,6 +182,8 @@ function resetTraining() { :datasets="props.datasets" :selectedDataset="selectedDatasetName" :initializationMethod="initializationMethod" + :hidden-layers="hiddenLayers" + :hidden-layers-neurons="hiddenLayersNeurons" :minError="props.minError" :sessionId="props.sessionId" :defaultLearningRate="props.learningRate" @@ -203,6 +208,7 @@ function resetTraining() { :iterations="iterations" :trainingEnded="trainingEnded" :trainingEndReason="trainingEndReason" + :maxDisplayedWeights="props.maxDisplayedWeights" />
@@ -210,6 +216,7 @@ function resetTraining() {
diff --git a/resources/js/types/perceptron.ts b/resources/js/types/perceptron.ts index e236861..7e1c10d 100644 --- a/resources/js/types/perceptron.ts +++ b/resources/js/types/perceptron.ts @@ -10,6 +10,7 @@ export type Dataset = { data: DatasetPoint[]; defaultLearningRate?: number; defaultMinError?: number; + defaultMaxIterations?: number; }; export type DatasetPoint = { @@ -20,4 +21,4 @@ export type DatasetPoint = { export type InitializationMethod = 'zeros' | 'random'; -export type PerceptronType = 'simple'; +export type PerceptronType = 'simple' | 'gradientdescent' | 'adaline' | 'monolayer' | 'multilayer'; diff --git a/tests/Unit/Training/MultiLayerPerceptronTest.php b/tests/Unit/Training/MultiLayerPerceptronTest.php new file mode 100644 index 0000000..dabb604 --- /dev/null +++ b/tests/Unit/Training/MultiLayerPerceptronTest.php @@ -0,0 +1,33 @@ +start(); + + $weights = $training->getSynapticWeights()[0][0]; + + $this->assertCount(1, $weights[count($weights) - 1]); + } +} \ No newline at end of file