Refactor and optimizations

This commit is contained in:
2026-09-08 19:06:02 +02:00
parent 8a50f492ac
commit 0e177f8491
11 changed files with 277 additions and 177 deletions
+4 -15
View File
@@ -3,6 +3,7 @@
namespace App\Http\Controllers;
use App\Events\PerceptronInitialization;
use App\Http\Requests\RunPerceptronRequest;
use App\Models\NetworksTraining\ADALINEPerceptronTraining;
use App\Models\NetworksTraining\GradientDescentPerceptronTraining;
use App\Models\NetworksTraining\MonoLayerPerceptronTraining;
@@ -18,7 +19,6 @@ use App\Services\SynapticWeightsProvider\RandomSynapticWeights;
use App\Services\SynapticWeightsProvider\ZeroSynapticWeights;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
class PerceptronController extends Controller
{
@@ -168,20 +168,10 @@ class PerceptronController extends Controller
return new RandomOrderDataSetReader($dataSetFileName);
}
public function run(Request $request, ISynapticWeightsProvider $synapticWeightsProvider)
public function run(RunPerceptronRequest $request, ISynapticWeightsProvider $synapticWeightsProvider)
{
$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);
@@ -194,13 +184,12 @@ class PerceptronController extends Controller
$trainingId = $request->input('training_id');
// 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();
// Zero initialization prevents hidden layers from receiving a gradient.
if ($perceptronType === 'multilayer' && $weightInitMethod === 'zeros') {
$synapticWeightsProvider = new RandomSynapticWeights;
}
else if ($weightInitMethod === 'zeros') {
} elseif ($weightInitMethod === 'zeros') {
$synapticWeightsProvider = new ZeroSynapticWeights;
}
@@ -0,0 +1,29 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class RunPerceptronRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'type' => ['required', 'string', 'in:simple,gradientdescent,adaline,monolayer,multilayer'],
'dataset' => ['required', 'string', 'max:100', 'regex:/^[A-Za-z0-9_-]+$/'],
'weight_init_method' => ['required', 'string', 'in:random,zeros'],
'learning_rate' => ['required', 'numeric', 'min:0'],
'min_error' => ['required', 'numeric', 'min:0'],
'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'],
'session_id' => ['required', 'string', 'max:100'],
'training_id' => ['required', 'string', 'max:100'],
];
}
}
@@ -8,7 +8,7 @@ class LinearOrderDataSetReader implements IDataSetReader
{
public array $lines = [];
private array $currentLines = [];
private int $currentLineIndex = 0;
private int $lastReadLineIndex = -1;
@@ -35,13 +35,13 @@ class LinearOrderDataSetReader implements IDataSetReader
public function getNextLine(): ?array
{
if (! isset($this->currentLines[0])) {
if (! isset($this->lines[$this->currentLineIndex])) {
return null; // No more lines to read
}
$this->lastReadLineIndex = array_search($this->currentLines[0], $this->lines, true);
$this->lastReadLineIndex = $this->currentLineIndex;
return array_shift($this->currentLines);
return $this->lines[$this->currentLineIndex++];
}
public function getInputSize(): int
@@ -53,18 +53,20 @@ class LinearOrderDataSetReader implements IDataSetReader
{
// Count the number of unique labels in the dataset
$labels = array_map(fn ($line) => end($line), $this->lines);
return count(array_unique($labels));
}
public function getLabels(): array
{
$labels = array_map(fn ($line) => end($line), $this->lines);
return array_values(array_unique($labels));
}
public function reset(): void
{
$this->currentLines = $this->lines;
$this->currentLineIndex = 0;
}
public function getLastReadLineIndex(): int
@@ -8,7 +8,9 @@ class RandomOrderDataSetReader implements IDataSetReader
{
public array $lines = [];
private array $currentLines = [];
private array $currentLineIndexes = [];
private int $currentLineIndex = 0;
private int $lastReadLineIndex = -1;
@@ -35,19 +37,14 @@ class RandomOrderDataSetReader implements IDataSetReader
public function getNextLine(): ?array
{
if (empty($this->currentLines)) {
if (! isset($this->currentLineIndexes[$this->currentLineIndex])) {
return null; // No more lines to read
}
$randomNumber = array_rand($this->currentLines);
$randomLine = $this->currentLines[$randomNumber];
$lineIndex = $this->currentLineIndexes[$this->currentLineIndex++];
// Remove the line from the current lines to avoid repetition
unset($this->currentLines[$randomNumber]);
$this->lastReadLineIndex = $lineIndex;
// Remember the index of the last read line in the full list
$this->lastReadLineIndex = array_search($randomLine, $this->lines, true);
return $randomLine;
return $this->lines[$lineIndex];
}
public function getInputSize(): int
@@ -59,18 +56,22 @@ class RandomOrderDataSetReader implements IDataSetReader
{
// Count the number of unique labels in the dataset
$labels = array_map(fn ($line) => end($line), $this->lines);
return count(array_unique($labels));
}
public function getLabels(): array
{
$labels = array_map(fn ($line) => end($line), $this->lines);
return array_values(array_unique($labels));
}
public function reset(): void
{
$this->currentLines = $this->lines;
$this->currentLineIndexes = array_keys($this->lines);
shuffle($this->currentLineIndexes);
$this->currentLineIndex = 0;
}
public function getLastReadLineIndex(): int
@@ -2,27 +2,24 @@
namespace App\Services\IterationEventBuffer;
use App\Events\PerceptronTrainingIteration;
class PerceptronIterationEventBuffer implements IPerceptronIterationEventBuffer
{
private $data;
private int $nextSizeIncreaseThreshold;
private int $underSizeIncreaseCount = 0;
private array $data = [];
public function __construct(
private string $sessionId,
private string $trainingId,
private int $sizeIncreaseStart = 10,
private int $sizeIncreaseFactor = 2,
) {
$this->data = [];
$this->nextSizeIncreaseThreshold = $sizeIncreaseStart;
}
) {}
public function flush(): void
{
event(new \App\Events\PerceptronTrainingIteration($this->data, $this->sessionId, $this->trainingId));
if ($this->data === []) {
return;
}
event(new PerceptronTrainingIteration($this->data, $this->sessionId, $this->trainingId));
$this->data = [];
}
@@ -35,27 +32,24 @@ class PerceptronIterationEventBuffer implements IPerceptronIterationEventBuffer
'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++;
if ($this->data !== [] && $this->payloadExceedsLimit()) {
$lastIteration = array_pop($this->data);
$this->flush();
} elseif (count($this->data) >= $this->nextSizeIncreaseThreshold) {
$this->flush();
$this->nextSizeIncreaseThreshold *= $this->sizeIncreaseFactor;
$this->data[] = $lastIteration;
}
if ($this->nextSizeIncreaseThreshold > config('perceptron.broadcast_iteration_size')) {
$this->nextSizeIncreaseThreshold = config('perceptron.broadcast_iteration_size'); // Cap the threshold to the maximum size
if (count($this->data) >= config('perceptron.broadcast_iteration_size')) {
$this->flush();
}
}
private function payloadExceedsLimit(): bool
{
return strlen(json_encode([
'iterations' => $this->data,
'trainingId' => $this->trainingId,
], JSON_THROW_ON_ERROR)) > config('broadcasting.broadcast_max_payload_size');
}
}
@@ -2,24 +2,29 @@
namespace App\Services\IterationEventBuffer;
use App\Events\PerceptronTrainingIteration;
class PerceptronLimitedEpochEventBuffer implements IPerceptronIterationEventBuffer
{
private array $data;
private array $data = [];
private int $underSizeIncreaseCount = 0;
private ?int $activeEpoch = null;
private bool $shouldBroadcastEpoch = false;
public function __construct(
private string $sessionId,
private string $trainingId,
private int $epochInterval,
private int $sizeIncreaseStart = 10,
) {
$this->data = [];
}
) {}
public function flush(): void
{
event(new \App\Events\PerceptronTrainingIteration($this->data, $this->sessionId, $this->trainingId));
if ($this->data === []) {
return;
}
event(new PerceptronTrainingIteration($this->data, $this->sessionId, $this->trainingId));
$this->data = [];
}
@@ -32,16 +37,28 @@ class PerceptronLimitedEpochEventBuffer implements IPerceptronIterationEventBuff
'weights' => $synaptic_weights,
];
$lastEpoch = $this->data[0]['epoch'] ?? null;
if ($this->data && $lastEpoch !== $epoch) { // Current Epoch has changed from the last one
if ($lastEpoch == 1 || $lastEpoch % $this->epochInterval === 0) { // The last saved epoch need to be sent
$this->flush(); // Flush all data from the previous epoch
} else {
$this->data = []; // We clear the data without sending it as we are saving the next epoch data
if ($this->activeEpoch !== $epoch) {
$this->flush();
$this->activeEpoch = $epoch;
$this->shouldBroadcastEpoch = $epoch === 1 || $epoch % $this->epochInterval === 0;
}
$lastEpoch = $epoch;
if (! $this->shouldBroadcastEpoch) {
return;
}
$this->data[] = $newData;
if ($this->payloadExceedsLimit() || count($this->data) >= config('perceptron.broadcast_iteration_size')) {
$this->flush();
}
}
private function payloadExceedsLimit(): bool
{
return strlen(json_encode([
'iterations' => $this->data,
'trainingId' => $this->trainingId,
], JSON_THROW_ON_ERROR)) > config('broadcasting.broadcast_max_payload_size');
}
}
-4
View File
@@ -1,4 +0,0 @@
0, 0, -1
0, 1, 1
1, 0, 1
1, 1, 1
1 0 0 -1
2 0 1 1
3 1 0 1
4 1 1 1
@@ -1,7 +1,7 @@
<script setup lang="ts">
import type { ChartData } from 'chart.js';
import type { ChartDataset } from 'chart.js';
import { computed, ref } from 'vue';
import { Bar } from 'vue-chartjs';
import { Chart } from 'vue-chartjs';
import { colors, gridColor, gridColorBold } from '@/types/graphs';
import type { Iteration } from '@/types/perceptron';
import Toggle from './ui/toggle/Toggle.vue';
@@ -11,16 +11,16 @@ const props = defineProps<{
isRegression: boolean;
}>();
type ErrorValue = number | [number, number] | null;
type ErrorDataset = ChartDataset<'bar' | 'line', ErrorValue[]>;
const epochErrorOnly = ref<boolean>(false);
/**
* Datasets of the iterations with the form { label: `Exemple ${exampleIndex}`, data: [error for iteration 1, error for iteration 2, ...] }
*/
const datasets = computed<
ChartData<'bar', (number | [number, number] | null)[]>[]
>(() => {
const datasets: ChartData<'bar', (number | [number, number] | null)[]>[] =
[];
const datasets = computed<ErrorDataset[]>(() => {
const datasets: ErrorDataset[] = [];
const epochAverageError: number[] = [];
const backgroundColors = colors;
@@ -60,14 +60,14 @@ const datasets = computed<
// Sort dataset by label (Exemple 0, Exemple 1, ...)
datasets.sort((a, b) => {
const aIndex = parseInt(a.label.split(' ')[1]);
const bIndex = parseInt(b.label.split(' ')[1]);
const aIndex = parseInt((a.label ?? '').split(' ')[1]);
const bIndex = parseInt((b.label ?? '').split(' ')[1]);
return aIndex - bIndex;
});
// Epoch error
const epochErrorDataset = {
type: 'line',
const epochErrorDataset: ErrorDataset = {
type: 'line' as const,
label: "Erreur quadratique moyenne de l'époque",
data: [],
backgroundColor: '#fff',
@@ -88,7 +88,8 @@ const datasets = computed<
</script>
<template>
<Bar
<Chart
type="bar"
class="bg-primary dark:bg-transparent!"
:options="{
responsive: true,
@@ -0,0 +1,104 @@
import { useEcho } from '@laravel/echo-vue';
import { onBeforeUnmount, ref, shallowRef } from 'vue';
import type { Iteration } from '@/types/perceptron';
type TrainingEvent = {
trainingId: string;
};
type IterationEvent = TrainingEvent & {
iterations: Iteration[];
};
type InitializationEvent = TrainingEvent & {
activationFunction: string;
};
type TrainingEndedEvent = TrainingEvent & {
reason: string;
};
export function usePerceptronTraining(sessionId: string) {
const trainingId = ref('');
const iterations = shallowRef<Iteration[]>([]);
const trainingEnded = ref(false);
const trainingEndReason = ref('');
const activationFunction = ref('');
let pendingIterations: Iteration[] = [];
let renderFrame: number | null = null;
function isCurrentTraining(event: TrainingEvent): boolean {
return event.trainingId === trainingId.value;
}
function flushPendingIterations(): void {
renderFrame = null;
if (pendingIterations.length === 0) {
return;
}
iterations.value = [...iterations.value, ...pendingIterations];
pendingIterations = [];
}
function handleIterations(event: IterationEvent): void {
if (!isCurrentTraining(event)) {
return;
}
pendingIterations.push(...event.iterations);
if (renderFrame === null) {
renderFrame = requestAnimationFrame(flushPendingIterations);
}
}
function handleInitialization(event: InitializationEvent): void {
if (isCurrentTraining(event)) {
activationFunction.value = event.activationFunction;
}
}
function handleTrainingEnded(event: TrainingEndedEvent): void {
if (!isCurrentTraining(event)) {
return;
}
flushPendingIterations();
trainingEnded.value = true;
trainingEndReason.value = event.reason;
}
function reset(): void {
if (renderFrame !== null) {
cancelAnimationFrame(renderFrame);
renderFrame = null;
}
pendingIterations = [];
iterations.value = [];
trainingEnded.value = false;
trainingEndReason.value = '';
activationFunction.value = '';
}
function setTrainingId(newTrainingId: string): void {
reset();
trainingId.value = newTrainingId;
}
const channel = `${sessionId}-perceptron-training`;
useEcho(channel, 'PerceptronTrainingIteration', handleIterations, [{}], 'public');
useEcho(channel, 'PerceptronTrainingEnded', handleTrainingEnded, [{}], 'public');
useEcho(channel, 'PerceptronInitialization', handleInitialization, [{}], 'public');
onBeforeUnmount(reset);
return {
activationFunction,
iterations,
setTrainingId,
trainingEnded,
trainingEndReason,
};
}
+11 -85
View File
@@ -1,6 +1,5 @@
<script setup lang="ts">
import { Head } from '@inertiajs/vue3';
import { useEcho } from '@laravel/echo-vue';
import {
Chart as ChartJS,
Title,
@@ -14,13 +13,8 @@ import {
} from 'chart.js';
import { computed, ref } from 'vue';
import LinkHeader from '@/components/LinkHeader.vue';
import type {
Dataset,
DatasetPoint,
InitializationMethod,
Iteration,
PerceptronType,
} from '@/types/perceptron';
import { usePerceptronTraining } from '@/composables/usePerceptronTraining';
import type { Dataset, DatasetPoint, InitializationMethod, PerceptronType } from '@/types/perceptron';
import IterationTable from '../components/IterationTable.vue';
import PerceptronDecisionGraph from '../components/PerceptronDecisionGraph.vue';
import PerceptronIterationsErrorsGraph from '../components/PerceptronIterationsErrorsGraph.vue';
@@ -86,74 +80,16 @@ const cleanedDataset = computed<
const hiddenLayers = ref(3);
const hiddenLayersNeurons = ref(3);
const initializationMethod = ref<InitializationMethod>(props.type === 'multilayer' ? 'random' : 'zeros');
console.log('Session ID:', props.sessionId);
useEcho(
`${props.sessionId}-perceptron-training`,
'PerceptronTrainingIteration',
percpetronIteration,
[{}],
'public',
);
useEcho(
`${props.sessionId}-perceptron-training`,
'PerceptronTrainingEnded',
perceptronTrainingEnded,
[{}],
'public',
);
useEcho(
`${props.sessionId}-perceptron-training`,
'PerceptronInitialization',
perceptroninitialization,
[{}],
'public',
);
const iterations = ref<Iteration[]>([]);
const trainingId = ref<string>('');
function percpetronIteration(data: any) {
console.log('Received perceptron iteration data:', data);
if (data.trainingId !== trainingId.value) {
console.warn(
`Received iteration for training ID ${data.trainingId}, but current training ID is ${trainingId.value}. Ignoring this iteration.`
);
return;
}
iterations.value.push(...data.iterations);
}
const trainingEnded = ref(false);
const trainingEndReason = ref('');
function perceptronTrainingEnded(data: any) {
console.log('Perceptron training ended:', data);
if (data.trainingId !== trainingId.value) {
console.warn(
`Received training ended event for training ID ${data.trainingId}, but current training ID is ${trainingId.value}. Ignoring this event.`
);
return;
}
trainingEnded.value = true;
trainingEndReason.value = data.reason;
}
const activationFunction = ref<string>('');
const {
activationFunction,
iterations,
setTrainingId,
trainingEnded,
trainingEndReason,
} = usePerceptronTraining(props.sessionId);
const isRegression = computed(
() => props.type === 'multilayer' && activationFunction.value === 'linear',
() => (props.type === 'multilayer' || props.type === 'monolayer') && activationFunction.value === 'linear',
);
function perceptroninitialization(data: any) {
console.log('Perceptron training initialized:', data);
if (data.trainingId !== trainingId.value) {
console.warn(
`Received initialization event for training ID ${data.trainingId}, but current training ID is ${trainingId.value}. Ignoring this event.`
);
return;
}
activationFunction.value = data.activationFunction;
}
function getActivationFunction(type: string): (x: number) => number {
switch (type) {
case 'step':
@@ -169,12 +105,6 @@ function getActivationFunction(type: string): (x: number) => number {
}
}
function resetTraining() {
iterations.value = [];
trainingEnded.value = false;
trainingEndReason.value = '';
activationFunction.value = '';
}
</script>
<template>
@@ -197,11 +127,7 @@ function resetTraining() {
selectedDatasetName = newValue;
}
"
@update:training-id="
(newValue) => {
trainingId = newValue;
resetTraining();
}"
@update:training-id="setTrainingId"
/>
<div
class="align-items-start justify-content-center flex h-full min-h-dvh max-w-dvw"
@@ -0,0 +1,41 @@
<?php
namespace Tests\Unit\Services;
use App\Events\PerceptronTrainingIteration;
use App\Services\IterationEventBuffer\PerceptronIterationEventBuffer;
use App\Services\IterationEventBuffer\PerceptronLimitedEpochEventBuffer;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;
class IterationEventBufferTest extends TestCase
{
public function test_iterations_are_sent_as_a_single_batch(): void
{
Event::fake();
$buffer = new PerceptronIterationEventBuffer('session', 'training');
$buffer->addIteration(1, 0, 0.5, []);
$buffer->addIteration(1, 1, 0.25, []);
$buffer->flush();
Event::assertDispatched(PerceptronTrainingIteration::class, function (PerceptronTrainingIteration $event): bool {
return count($event->iterations) === 2;
});
}
public function test_limited_buffer_discards_non_selected_epochs(): void
{
Event::fake();
$buffer = new PerceptronLimitedEpochEventBuffer('session', 'training', 2);
$buffer->addIteration(1, 0, 0.5, []);
$buffer->addIteration(2, 0, 0.25, []);
$buffer->flush();
Event::assertDispatched(PerceptronTrainingIteration::class, function (PerceptronTrainingIteration $event): bool {
return count($event->iterations) === 1
&& $event->iterations[0]['epoch'] === 1;
});
}
}