Compare commits

..

2 Commits

Author SHA1 Message Date
Ninluc 69e683bcaf Some bugfixes and misc
linter / quality (push) Successful in 4m24s
tests / ci (8.4) (push) Successful in 4m47s
tests / ci (8.5) (push) Successful in 5m0s
2026-09-08 20:01:52 +02:00
Ninluc 0e177f8491 Refactor and optimizations 2026-09-08 19:06:02 +02:00
19 changed files with 393 additions and 206 deletions
+2 -2
View File
@@ -5,11 +5,11 @@ namespace App\Events;
use App\Models\ActivationsFunctions;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class PerceptronInitialization implements ShouldBroadcast
class PerceptronInitialization implements ShouldBroadcastNow
{
use Dispatchable, InteractsWithSockets, SerializesModels;
+2 -2
View File
@@ -4,11 +4,11 @@ namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class PerceptronTrainingEnded implements ShouldBroadcast
class PerceptronTrainingEnded implements ShouldBroadcastNow
{
use Dispatchable, InteractsWithSockets, SerializesModels;
+24 -7
View File
@@ -4,12 +4,12 @@ namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Support\Arr;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Arr;
class PerceptronTrainingIteration implements ShouldBroadcast
class PerceptronTrainingIteration implements ShouldBroadcastNow
{
use Dispatchable, InteractsWithSockets, SerializesModels;
@@ -39,19 +39,20 @@ class PerceptronTrainingIteration implements ShouldBroadcast
public function broadcastWith(): array
{
$weights = collect($this->iterations)
$iterations = self::normalizeForJson($this->iterations);
$weights = collect($iterations)
->pluck('weights')
->first(fn (array $weights): bool => $weights !== []);
$shouldBroadcastAllWeights = $weights !== null
&& count(Arr::flatten($weights)) <= config('perceptron.max_displayed_weights');
$lastIterationIndex = count($this->iterations) - 1;
$lastIterationIndex = count($iterations) - 1;
$iterations = array_map(
fn (array $iteration, int $index): array => $shouldBroadcastAllWeights || $index === $lastIterationIndex
? $iteration
: [...$iteration, 'weights' => []],
$this->iterations,
array_keys($this->iterations),
$iterations,
array_keys($iterations),
);
return [
@@ -59,4 +60,20 @@ class PerceptronTrainingIteration implements ShouldBroadcast
'trainingId' => $this->trainingId,
];
}
public static function normalizeForJson(mixed $value): mixed
{
if (is_float($value) && ! is_finite($value)) {
return null;
}
if (! is_array($value)) {
return $value;
}
return array_map(
fn (mixed $item): mixed => self::normalizeForJson($item),
$value,
);
}
}
+12 -20
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;
@@ -17,8 +18,6 @@ use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider;
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
{
@@ -134,6 +133,11 @@ class PerceptronController extends Controller
break;
case 'table_2_11':
$dataset['defaultMinError'] = 0.02;
switch ($perceptronType) {
case 'monolayer':
$dataset['defaultLearningRate'] = 0.0015;
break;
}
break;
case 'table_4_12':
switch ($perceptronType) {
@@ -145,11 +149,13 @@ class PerceptronController extends Controller
}
break;
case 'table_4_17':
$dataset['defaultMinError'] = 0.055;
switch ($perceptronType) {
case 'multilayer':
$dataset['defaultLearningRate'] = 0.5;
$dataset['defaultMinError'] = 0.08;
$dataset['defaultLearningRate'] = 0.3;
$dataset['defaultMaxIterations'] = 400;
$dataset['defaultHiddenLayers'] = 2;
$dataset['defaultHiddenLayersNeurons'] = 2;
break;
}
break;
@@ -168,20 +174,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);
@@ -193,14 +189,10 @@ class PerceptronController extends Controller
$sessionId = $request->input('session_id', session()->getId());
$trainingId = $request->input('training_id');
// Remove the jobs for the sessionId
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'],
];
}
}
@@ -20,6 +20,7 @@ class MonoLayerPerceptronTraining extends NetworkTraining
private array $labels;
public ActivationsFunctions $activationFunction = ActivationsFunctions::LINEAR;
public ?ActivationsFunctions $presentationLayerActivationFunction = ActivationsFunctions::STEP;
private float $epochError;
@@ -62,7 +63,7 @@ class MonoLayerPerceptronTraining extends NetworkTraining
while ($nextRow = $this->datasetReader->getNextLine()) {
$inputsForCurrentEpoch[] = $nextRow;
$inputs = array_slice($nextRow, 0, -1);
$correctOutput = (int) end($nextRow);
$correctOutput = (float) end($nextRow);
$iterationError = $this->iterationFunction($inputs, $correctOutput);
@@ -108,7 +109,7 @@ class MonoLayerPerceptronTraining extends NetworkTraining
return $condition;
}
private function iterationFunction(array $inputs, int $correctOutput): array
private function iterationFunction(array $inputs, float $correctOutput): array
{
$outputs = $this->network->test($inputs);
$desiredOutput = $this->getDesiredOutputFromCorrectOutput($correctOutput);
@@ -137,10 +138,10 @@ class MonoLayerPerceptronTraining extends NetworkTraining
return [$updatedWeights];
}
private function getDesiredOutputFromCorrectOutput(int $correctOutput): array
private function getDesiredOutputFromCorrectOutput(float $correctOutput): array
{
$desiredOutput = array_fill(0, count($this->labels), -1);
$labelIndex = Arr::first(array_keys($this->labels), fn($key) => $this->labels[$key] == $correctOutput);
$labelIndex = Arr::first(array_keys($this->labels), fn ($key) => $this->labels[$key] == $correctOutput);
if ($labelIndex !== null) {
$desiredOutput[$labelIndex] = 1;
}
+2
View File
@@ -4,6 +4,8 @@ namespace App\Models\Perceptrons;
class InputNeuron extends Perceptron
{
private float $input = 0.0;
public function __construct(
) {
parent::__construct([]);
@@ -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,28 @@
namespace App\Services\IterationEventBuffer;
use App\Events\PerceptronTrainingIteration;
class PerceptronIterationEventBuffer implements IPerceptronIterationEventBuffer
{
private $data;
private array $data = [];
private int $nextSizeIncreaseThreshold;
private int $underSizeIncreaseCount = 0;
private ?float $lastBroadcastAt = null;
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;
}
$this->waitForBroadcastInterval();
event(new PerceptronTrainingIteration($this->data, $this->sessionId, $this->trainingId));
$this->lastBroadcastAt = microtime(true);
$this->data = [];
}
@@ -35,27 +36,38 @@ 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;
if ($this->nextSizeIncreaseThreshold > config('perceptron.broadcast_iteration_size')) {
$this->nextSizeIncreaseThreshold = config('perceptron.broadcast_iteration_size'); // Cap the threshold to the maximum size
$this->data[] = $lastIteration;
}
if (count($this->data) >= config('perceptron.broadcast_iteration_size')) {
$this->flush();
}
}
private function payloadExceedsLimit(): bool
{
return strlen(json_encode([
'iterations' => PerceptronTrainingIteration::normalizeForJson($this->data),
'trainingId' => $this->trainingId,
], JSON_THROW_ON_ERROR)) > config('broadcasting.broadcast_max_payload_size');
}
private function waitForBroadcastInterval(): void
{
if ($this->lastBroadcastAt === null) {
return;
}
$minimumInterval = config('perceptron.broadcast_minimum_interval_ms') / 1000;
$remainingInterval = $minimumInterval - (microtime(true) - $this->lastBroadcastAt);
if ($remainingInterval > 0) {
usleep((int) ceil($remainingInterval * 1_000_000));
}
}
}
@@ -2,24 +2,33 @@
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;
private ?float $lastBroadcastAt = null;
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;
}
$this->waitForBroadcastInterval();
event(new PerceptronTrainingIteration($this->data, $this->sessionId, $this->trainingId));
$this->lastBroadcastAt = microtime(true);
$this->data = [];
}
@@ -32,16 +41,42 @@ 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' => PerceptronTrainingIteration::normalizeForJson($this->data),
'trainingId' => $this->trainingId,
], JSON_THROW_ON_ERROR)) > config('broadcasting.broadcast_max_payload_size');
}
private function waitForBroadcastInterval(): void
{
if ($this->lastBroadcastAt === null) {
return;
}
$minimumInterval = config('perceptron.broadcast_minimum_interval_ms') / 1000;
$remainingInterval = $minimumInterval - (microtime(true) - $this->lastBroadcastAt);
if ($remainingInterval > 0) {
usleep((int) ceil($remainingInterval * 1_000_000));
}
}
}
+6 -1
View File
@@ -10,7 +10,7 @@ return [
'limited_broadcast_iterations' => 100,
/**
* How much broadcasts is sent when in limmited broadcast mode
* How much broadcasts is sent when in limited broadcast mode
*/
'limited_broadcast_number' => 100,
@@ -19,6 +19,11 @@ return [
*/
'broadcast_iteration_size' => 75,
/**
* Minimum time between training progress broadcasts, in milliseconds.
*/
'broadcast_minimum_interval_ms' => 150,
/**
* Maximum number of weights for which all iteration weights are broadcast
* and displayed in the iteration table.
-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
+7 -4
View File
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { computed, ComputedRef } from 'vue';
import { computed } from 'vue';
import type { ComputedRef } from 'vue';
import type { Iteration } from '@/types/perceptron';
const props = defineProps<{
@@ -13,7 +14,9 @@ const props = defineProps<{
const allWeightPerIteration: ComputedRef<number[][]> = computed(() => {
return props.iterations.map((iteration) => {
// We flatten the weights
return iteration.weights.flat(2);
return iteration.weights
.flat(2)
.filter((weight): weight is number => weight !== null && Number.isFinite(weight));
});
});
@@ -61,10 +64,10 @@ const rowBgDark = computed(() => {
v-for="(weight, weightIndex) in allWeightPerIteration[index]"
v-bind:key="weightIndex"
>
{{ weight.toFixed(2) }}
{{ Number.isFinite(weight) ? weight.toFixed(2) : 'N/A' }}
</td>
</template>
<td>{{ iteration.error.toFixed(2) }}</td>
<td>{{ iteration.error === null ? 'N/A' : iteration.error.toFixed(2) }}</td>
</tr>
<tr
@@ -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;
@@ -28,6 +28,8 @@ const datasets = computed<
const exampleCountPerEpoch: Record<number, number> = {};
props.iterations.forEach((iteration) => {
const error = iteration.error ?? 0;
if (!epochErrorOnly.value) {
const exampleLabel = `Exemple ${iteration.exampleIndex}`;
let dataset = datasets.find((d) => d.label === exampleLabel);
@@ -45,8 +47,8 @@ const datasets = computed<
}
dataset.data.push(
props.isRegression
? Math.abs(iteration.error)
: iteration.error,
? Math.abs(error)
: error,
);
}
@@ -55,19 +57,19 @@ const datasets = computed<
// Epoch error
epochAverageError[iteration.epoch] =
(epochAverageError[iteration.epoch] || 0) +
iteration.error ** 2 / 2;
error ** 2 / 2;
});
// 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 +90,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"
+2 -2
View File
@@ -1,8 +1,8 @@
export type Iteration = {
epoch: number;
exampleIndex: number;
weights: number[][][];
error: number;
weights: (number | null)[][][];
error: number | null;
};
export type Dataset = {
@@ -0,0 +1,59 @@
<?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;
});
}
public function test_non_finite_values_are_normalized_before_broadcasting(): void
{
$event = new PerceptronTrainingIteration([
[
'epoch' => 1,
'exampleIndex' => 0,
'error' => NAN,
'weights' => [[[INF]]],
],
], 'session', 'training');
$payload = $event->broadcastWith();
$this->assertNull($payload['iterations'][0]['error']);
$this->assertNull($payload['iterations'][0]['weights'][0][0][0]);
$this->assertJson(json_encode($payload, JSON_THROW_ON_ERROR));
}
}