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 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,
);
}
}
@@ -18,7 +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;
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;
@@ -183,9 +189,6 @@ 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;
@@ -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,6 +8,8 @@ class PerceptronIterationEventBuffer implements IPerceptronIterationEventBuffer
{
private array $data = [];
private ?float $lastBroadcastAt = null;
public function __construct(
private string $sessionId,
private string $trainingId,
@@ -19,7 +21,9 @@ class PerceptronIterationEventBuffer implements IPerceptronIterationEventBuffer
return;
}
$this->waitForBroadcastInterval();
event(new PerceptronTrainingIteration($this->data, $this->sessionId, $this->trainingId));
$this->lastBroadcastAt = microtime(true);
$this->data = [];
}
@@ -48,8 +52,22 @@ class PerceptronIterationEventBuffer implements IPerceptronIterationEventBuffer
private function payloadExceedsLimit(): bool
{
return strlen(json_encode([
'iterations' => $this->data,
'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));
}
}
}
@@ -12,6 +12,8 @@ class PerceptronLimitedEpochEventBuffer implements IPerceptronIterationEventBuff
private bool $shouldBroadcastEpoch = false;
private ?float $lastBroadcastAt = null;
public function __construct(
private string $sessionId,
private string $trainingId,
@@ -24,7 +26,9 @@ class PerceptronLimitedEpochEventBuffer implements IPerceptronIterationEventBuff
return;
}
$this->waitForBroadcastInterval();
event(new PerceptronTrainingIteration($this->data, $this->sessionId, $this->trainingId));
$this->lastBroadcastAt = microtime(true);
$this->data = [];
}
@@ -57,8 +61,22 @@ class PerceptronLimitedEpochEventBuffer implements IPerceptronIterationEventBuff
private function payloadExceedsLimit(): bool
{
return strlen(json_encode([
'iterations' => $this->data,
'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.
+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
@@ -28,6 +28,8 @@ const datasets = computed<ErrorDataset[]>(() => {
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<ErrorDataset[]>(() => {
}
dataset.data.push(
props.isRegression
? Math.abs(iteration.error)
: iteration.error,
? Math.abs(error)
: error,
);
}
@@ -55,7 +57,7 @@ const datasets = computed<ErrorDataset[]>(() => {
// Epoch error
epochAverageError[iteration.epoch] =
(epochAverageError[iteration.epoch] || 0) +
iteration.error ** 2 / 2;
error ** 2 / 2;
});
// Sort dataset by label (Exemple 0, Exemple 1, ...)
+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 = {
@@ -38,4 +38,22 @@ class IterationEventBufferTest extends TestCase
&& $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));
}
}