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

This commit is contained in:
2026-09-08 20:01:52 +02:00
parent 0e177f8491
commit 69e683bcaf
13 changed files with 120 additions and 33 deletions
+2 -2
View File
@@ -5,11 +5,11 @@ namespace App\Events;
use App\Models\ActivationsFunctions; use App\Models\ActivationsFunctions;
use Illuminate\Broadcasting\Channel; use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast; use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
class PerceptronInitialization implements ShouldBroadcast class PerceptronInitialization implements ShouldBroadcastNow
{ {
use Dispatchable, InteractsWithSockets, SerializesModels; use Dispatchable, InteractsWithSockets, SerializesModels;
+2 -2
View File
@@ -4,11 +4,11 @@ namespace App\Events;
use Illuminate\Broadcasting\Channel; use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast; use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
class PerceptronTrainingEnded implements ShouldBroadcast class PerceptronTrainingEnded implements ShouldBroadcastNow
{ {
use Dispatchable, InteractsWithSockets, SerializesModels; use Dispatchable, InteractsWithSockets, SerializesModels;
+24 -7
View File
@@ -4,12 +4,12 @@ namespace App\Events;
use Illuminate\Broadcasting\Channel; use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast; use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Support\Arr;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Arr;
class PerceptronTrainingIteration implements ShouldBroadcast class PerceptronTrainingIteration implements ShouldBroadcastNow
{ {
use Dispatchable, InteractsWithSockets, SerializesModels; use Dispatchable, InteractsWithSockets, SerializesModels;
@@ -39,19 +39,20 @@ class PerceptronTrainingIteration implements ShouldBroadcast
public function broadcastWith(): array public function broadcastWith(): array
{ {
$weights = collect($this->iterations) $iterations = self::normalizeForJson($this->iterations);
$weights = collect($iterations)
->pluck('weights') ->pluck('weights')
->first(fn (array $weights): bool => $weights !== []); ->first(fn (array $weights): bool => $weights !== []);
$shouldBroadcastAllWeights = $weights !== null $shouldBroadcastAllWeights = $weights !== null
&& count(Arr::flatten($weights)) <= config('perceptron.max_displayed_weights'); && count(Arr::flatten($weights)) <= config('perceptron.max_displayed_weights');
$lastIterationIndex = count($this->iterations) - 1; $lastIterationIndex = count($iterations) - 1;
$iterations = array_map( $iterations = array_map(
fn (array $iteration, int $index): array => $shouldBroadcastAllWeights || $index === $lastIterationIndex fn (array $iteration, int $index): array => $shouldBroadcastAllWeights || $index === $lastIterationIndex
? $iteration ? $iteration
: [...$iteration, 'weights' => []], : [...$iteration, 'weights' => []],
$this->iterations, $iterations,
array_keys($this->iterations), array_keys($iterations),
); );
return [ return [
@@ -59,4 +60,20 @@ class PerceptronTrainingIteration implements ShouldBroadcast
'trainingId' => $this->trainingId, '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,
);
}
} }
@@ -18,7 +18,6 @@ use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider;
use App\Services\SynapticWeightsProvider\RandomSynapticWeights; use App\Services\SynapticWeightsProvider\RandomSynapticWeights;
use App\Services\SynapticWeightsProvider\ZeroSynapticWeights; use App\Services\SynapticWeightsProvider\ZeroSynapticWeights;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class PerceptronController extends Controller class PerceptronController extends Controller
{ {
@@ -134,6 +133,11 @@ class PerceptronController extends Controller
break; break;
case 'table_2_11': case 'table_2_11':
$dataset['defaultMinError'] = 0.02; $dataset['defaultMinError'] = 0.02;
switch ($perceptronType) {
case 'monolayer':
$dataset['defaultLearningRate'] = 0.0015;
break;
}
break; break;
case 'table_4_12': case 'table_4_12':
switch ($perceptronType) { switch ($perceptronType) {
@@ -145,11 +149,13 @@ class PerceptronController extends Controller
} }
break; break;
case 'table_4_17': case 'table_4_17':
$dataset['defaultMinError'] = 0.055;
switch ($perceptronType) { switch ($perceptronType) {
case 'multilayer': case 'multilayer':
$dataset['defaultLearningRate'] = 0.5; $dataset['defaultLearningRate'] = 0.3;
$dataset['defaultMinError'] = 0.08;
$dataset['defaultMaxIterations'] = 400; $dataset['defaultMaxIterations'] = 400;
$dataset['defaultHiddenLayers'] = 2;
$dataset['defaultHiddenLayersNeurons'] = 2;
break; break;
} }
break; break;
@@ -183,9 +189,6 @@ class PerceptronController extends Controller
$sessionId = $request->input('session_id', session()->getId()); $sessionId = $request->input('session_id', session()->getId());
$trainingId = $request->input('training_id'); $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. // Zero initialization prevents hidden layers from receiving a gradient.
if ($perceptronType === 'multilayer' && $weightInitMethod === 'zeros') { if ($perceptronType === 'multilayer' && $weightInitMethod === 'zeros') {
$synapticWeightsProvider = new RandomSynapticWeights; $synapticWeightsProvider = new RandomSynapticWeights;
@@ -20,6 +20,7 @@ class MonoLayerPerceptronTraining extends NetworkTraining
private array $labels; private array $labels;
public ActivationsFunctions $activationFunction = ActivationsFunctions::LINEAR; public ActivationsFunctions $activationFunction = ActivationsFunctions::LINEAR;
public ?ActivationsFunctions $presentationLayerActivationFunction = ActivationsFunctions::STEP; public ?ActivationsFunctions $presentationLayerActivationFunction = ActivationsFunctions::STEP;
private float $epochError; private float $epochError;
@@ -62,7 +63,7 @@ class MonoLayerPerceptronTraining extends NetworkTraining
while ($nextRow = $this->datasetReader->getNextLine()) { while ($nextRow = $this->datasetReader->getNextLine()) {
$inputsForCurrentEpoch[] = $nextRow; $inputsForCurrentEpoch[] = $nextRow;
$inputs = array_slice($nextRow, 0, -1); $inputs = array_slice($nextRow, 0, -1);
$correctOutput = (int) end($nextRow); $correctOutput = (float) end($nextRow);
$iterationError = $this->iterationFunction($inputs, $correctOutput); $iterationError = $this->iterationFunction($inputs, $correctOutput);
@@ -108,7 +109,7 @@ class MonoLayerPerceptronTraining extends NetworkTraining
return $condition; return $condition;
} }
private function iterationFunction(array $inputs, int $correctOutput): array private function iterationFunction(array $inputs, float $correctOutput): array
{ {
$outputs = $this->network->test($inputs); $outputs = $this->network->test($inputs);
$desiredOutput = $this->getDesiredOutputFromCorrectOutput($correctOutput); $desiredOutput = $this->getDesiredOutputFromCorrectOutput($correctOutput);
@@ -137,7 +138,7 @@ class MonoLayerPerceptronTraining extends NetworkTraining
return [$updatedWeights]; return [$updatedWeights];
} }
private function getDesiredOutputFromCorrectOutput(int $correctOutput): array private function getDesiredOutputFromCorrectOutput(float $correctOutput): array
{ {
$desiredOutput = array_fill(0, count($this->labels), -1); $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);
+2
View File
@@ -4,6 +4,8 @@ namespace App\Models\Perceptrons;
class InputNeuron extends Perceptron class InputNeuron extends Perceptron
{ {
private float $input = 0.0;
public function __construct( public function __construct(
) { ) {
parent::__construct([]); parent::__construct([]);
@@ -8,6 +8,8 @@ class PerceptronIterationEventBuffer implements IPerceptronIterationEventBuffer
{ {
private array $data = []; private array $data = [];
private ?float $lastBroadcastAt = null;
public function __construct( public function __construct(
private string $sessionId, private string $sessionId,
private string $trainingId, private string $trainingId,
@@ -19,7 +21,9 @@ class PerceptronIterationEventBuffer implements IPerceptronIterationEventBuffer
return; return;
} }
$this->waitForBroadcastInterval();
event(new PerceptronTrainingIteration($this->data, $this->sessionId, $this->trainingId)); event(new PerceptronTrainingIteration($this->data, $this->sessionId, $this->trainingId));
$this->lastBroadcastAt = microtime(true);
$this->data = []; $this->data = [];
} }
@@ -48,8 +52,22 @@ class PerceptronIterationEventBuffer implements IPerceptronIterationEventBuffer
private function payloadExceedsLimit(): bool private function payloadExceedsLimit(): bool
{ {
return strlen(json_encode([ return strlen(json_encode([
'iterations' => $this->data, 'iterations' => PerceptronTrainingIteration::normalizeForJson($this->data),
'trainingId' => $this->trainingId, 'trainingId' => $this->trainingId,
], JSON_THROW_ON_ERROR)) > config('broadcasting.broadcast_max_payload_size'); ], 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));
}
}
} }
@@ -12,6 +12,8 @@ class PerceptronLimitedEpochEventBuffer implements IPerceptronIterationEventBuff
private bool $shouldBroadcastEpoch = false; private bool $shouldBroadcastEpoch = false;
private ?float $lastBroadcastAt = null;
public function __construct( public function __construct(
private string $sessionId, private string $sessionId,
private string $trainingId, private string $trainingId,
@@ -24,7 +26,9 @@ class PerceptronLimitedEpochEventBuffer implements IPerceptronIterationEventBuff
return; return;
} }
$this->waitForBroadcastInterval();
event(new PerceptronTrainingIteration($this->data, $this->sessionId, $this->trainingId)); event(new PerceptronTrainingIteration($this->data, $this->sessionId, $this->trainingId));
$this->lastBroadcastAt = microtime(true);
$this->data = []; $this->data = [];
} }
@@ -57,8 +61,22 @@ class PerceptronLimitedEpochEventBuffer implements IPerceptronIterationEventBuff
private function payloadExceedsLimit(): bool private function payloadExceedsLimit(): bool
{ {
return strlen(json_encode([ return strlen(json_encode([
'iterations' => $this->data, 'iterations' => PerceptronTrainingIteration::normalizeForJson($this->data),
'trainingId' => $this->trainingId, 'trainingId' => $this->trainingId,
], JSON_THROW_ON_ERROR)) > config('broadcasting.broadcast_max_payload_size'); ], 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, '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, 'limited_broadcast_number' => 100,
@@ -19,6 +19,11 @@ return [
*/ */
'broadcast_iteration_size' => 75, '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 * Maximum number of weights for which all iteration weights are broadcast
* and displayed in the iteration table. * and displayed in the iteration table.
+7 -4
View File
@@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ComputedRef } from 'vue'; import { computed } from 'vue';
import type { ComputedRef } from 'vue';
import type { Iteration } from '@/types/perceptron'; import type { Iteration } from '@/types/perceptron';
const props = defineProps<{ const props = defineProps<{
@@ -13,7 +14,9 @@ const props = defineProps<{
const allWeightPerIteration: ComputedRef<number[][]> = computed(() => { const allWeightPerIteration: ComputedRef<number[][]> = computed(() => {
return props.iterations.map((iteration) => { return props.iterations.map((iteration) => {
// We flatten the weights // 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-for="(weight, weightIndex) in allWeightPerIteration[index]"
v-bind:key="weightIndex" v-bind:key="weightIndex"
> >
{{ weight.toFixed(2) }} {{ Number.isFinite(weight) ? weight.toFixed(2) : 'N/A' }}
</td> </td>
</template> </template>
<td>{{ iteration.error.toFixed(2) }}</td> <td>{{ iteration.error === null ? 'N/A' : iteration.error.toFixed(2) }}</td>
</tr> </tr>
<tr <tr
@@ -28,6 +28,8 @@ const datasets = computed<ErrorDataset[]>(() => {
const exampleCountPerEpoch: Record<number, number> = {}; const exampleCountPerEpoch: Record<number, number> = {};
props.iterations.forEach((iteration) => { props.iterations.forEach((iteration) => {
const error = iteration.error ?? 0;
if (!epochErrorOnly.value) { if (!epochErrorOnly.value) {
const exampleLabel = `Exemple ${iteration.exampleIndex}`; const exampleLabel = `Exemple ${iteration.exampleIndex}`;
let dataset = datasets.find((d) => d.label === exampleLabel); let dataset = datasets.find((d) => d.label === exampleLabel);
@@ -45,8 +47,8 @@ const datasets = computed<ErrorDataset[]>(() => {
} }
dataset.data.push( dataset.data.push(
props.isRegression props.isRegression
? Math.abs(iteration.error) ? Math.abs(error)
: iteration.error, : error,
); );
} }
@@ -55,7 +57,7 @@ const datasets = computed<ErrorDataset[]>(() => {
// Epoch error // Epoch error
epochAverageError[iteration.epoch] = epochAverageError[iteration.epoch] =
(epochAverageError[iteration.epoch] || 0) + (epochAverageError[iteration.epoch] || 0) +
iteration.error ** 2 / 2; error ** 2 / 2;
}); });
// Sort dataset by label (Exemple 0, Exemple 1, ...) // Sort dataset by label (Exemple 0, Exemple 1, ...)
+2 -2
View File
@@ -1,8 +1,8 @@
export type Iteration = { export type Iteration = {
epoch: number; epoch: number;
exampleIndex: number; exampleIndex: number;
weights: number[][][]; weights: (number | null)[][][];
error: number; error: number | null;
}; };
export type Dataset = { export type Dataset = {
@@ -38,4 +38,22 @@ class IterationEventBufferTest extends TestCase
&& $event->iterations[0]['epoch'] === 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));
}
} }