Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 69e683bcaf | |||
| 0e177f8491 |
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Events\PerceptronInitialization;
|
use App\Events\PerceptronInitialization;
|
||||||
|
use App\Http\Requests\RunPerceptronRequest;
|
||||||
use App\Models\NetworksTraining\ADALINEPerceptronTraining;
|
use App\Models\NetworksTraining\ADALINEPerceptronTraining;
|
||||||
use App\Models\NetworksTraining\GradientDescentPerceptronTraining;
|
use App\Models\NetworksTraining\GradientDescentPerceptronTraining;
|
||||||
use App\Models\NetworksTraining\MonoLayerPerceptronTraining;
|
use App\Models\NetworksTraining\MonoLayerPerceptronTraining;
|
||||||
@@ -17,8 +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;
|
|
||||||
use Illuminate\Support\Facades\Validator;
|
|
||||||
|
|
||||||
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;
|
||||||
@@ -168,20 +174,10 @@ class PerceptronController extends Controller
|
|||||||
return new RandomOrderDataSetReader($dataSetFileName);
|
return new RandomOrderDataSetReader($dataSetFileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function run(Request $request, ISynapticWeightsProvider $synapticWeightsProvider)
|
public function run(RunPerceptronRequest $request, ISynapticWeightsProvider $synapticWeightsProvider)
|
||||||
{
|
{
|
||||||
$startTime = microtime(true);
|
$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');
|
$perceptronType = $request->input('type');
|
||||||
$hiddenLayers = $request->input('hidden_layers', 2);
|
$hiddenLayers = $request->input('hidden_layers', 2);
|
||||||
$hiddenLayersNeurons = $request->input('hidden_layers_neurons', 3);
|
$hiddenLayersNeurons = $request->input('hidden_layers_neurons', 3);
|
||||||
@@ -193,14 +189,10 @@ 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;
|
||||||
}
|
} elseif ($weightInitMethod === 'zeros') {
|
||||||
else if ($weightInitMethod === 'zeros') {
|
|
||||||
$synapticWeightsProvider = new ZeroSynapticWeights;
|
$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;
|
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);
|
||||||
|
|||||||
@@ -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,7 +8,7 @@ class LinearOrderDataSetReader implements IDataSetReader
|
|||||||
{
|
{
|
||||||
public array $lines = [];
|
public array $lines = [];
|
||||||
|
|
||||||
private array $currentLines = [];
|
private int $currentLineIndex = 0;
|
||||||
|
|
||||||
private int $lastReadLineIndex = -1;
|
private int $lastReadLineIndex = -1;
|
||||||
|
|
||||||
@@ -35,13 +35,13 @@ class LinearOrderDataSetReader implements IDataSetReader
|
|||||||
|
|
||||||
public function getNextLine(): ?array
|
public function getNextLine(): ?array
|
||||||
{
|
{
|
||||||
if (! isset($this->currentLines[0])) {
|
if (! isset($this->lines[$this->currentLineIndex])) {
|
||||||
return null; // No more lines to read
|
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
|
public function getInputSize(): int
|
||||||
@@ -53,18 +53,20 @@ class LinearOrderDataSetReader implements IDataSetReader
|
|||||||
{
|
{
|
||||||
// Count the number of unique labels in the dataset
|
// Count the number of unique labels in the dataset
|
||||||
$labels = array_map(fn ($line) => end($line), $this->lines);
|
$labels = array_map(fn ($line) => end($line), $this->lines);
|
||||||
|
|
||||||
return count(array_unique($labels));
|
return count(array_unique($labels));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getLabels(): array
|
public function getLabels(): array
|
||||||
{
|
{
|
||||||
$labels = array_map(fn ($line) => end($line), $this->lines);
|
$labels = array_map(fn ($line) => end($line), $this->lines);
|
||||||
|
|
||||||
return array_values(array_unique($labels));
|
return array_values(array_unique($labels));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function reset(): void
|
public function reset(): void
|
||||||
{
|
{
|
||||||
$this->currentLines = $this->lines;
|
$this->currentLineIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getLastReadLineIndex(): int
|
public function getLastReadLineIndex(): int
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ class RandomOrderDataSetReader implements IDataSetReader
|
|||||||
{
|
{
|
||||||
public array $lines = [];
|
public array $lines = [];
|
||||||
|
|
||||||
private array $currentLines = [];
|
private array $currentLineIndexes = [];
|
||||||
|
|
||||||
|
private int $currentLineIndex = 0;
|
||||||
|
|
||||||
private int $lastReadLineIndex = -1;
|
private int $lastReadLineIndex = -1;
|
||||||
|
|
||||||
@@ -35,19 +37,14 @@ class RandomOrderDataSetReader implements IDataSetReader
|
|||||||
|
|
||||||
public function getNextLine(): ?array
|
public function getNextLine(): ?array
|
||||||
{
|
{
|
||||||
if (empty($this->currentLines)) {
|
if (! isset($this->currentLineIndexes[$this->currentLineIndex])) {
|
||||||
return null; // No more lines to read
|
return null; // No more lines to read
|
||||||
}
|
}
|
||||||
$randomNumber = array_rand($this->currentLines);
|
$lineIndex = $this->currentLineIndexes[$this->currentLineIndex++];
|
||||||
$randomLine = $this->currentLines[$randomNumber];
|
|
||||||
|
|
||||||
// Remove the line from the current lines to avoid repetition
|
$this->lastReadLineIndex = $lineIndex;
|
||||||
unset($this->currentLines[$randomNumber]);
|
|
||||||
|
|
||||||
// Remember the index of the last read line in the full list
|
return $this->lines[$lineIndex];
|
||||||
$this->lastReadLineIndex = array_search($randomLine, $this->lines, true);
|
|
||||||
|
|
||||||
return $randomLine;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getInputSize(): int
|
public function getInputSize(): int
|
||||||
@@ -59,18 +56,22 @@ class RandomOrderDataSetReader implements IDataSetReader
|
|||||||
{
|
{
|
||||||
// Count the number of unique labels in the dataset
|
// Count the number of unique labels in the dataset
|
||||||
$labels = array_map(fn ($line) => end($line), $this->lines);
|
$labels = array_map(fn ($line) => end($line), $this->lines);
|
||||||
|
|
||||||
return count(array_unique($labels));
|
return count(array_unique($labels));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getLabels(): array
|
public function getLabels(): array
|
||||||
{
|
{
|
||||||
$labels = array_map(fn ($line) => end($line), $this->lines);
|
$labels = array_map(fn ($line) => end($line), $this->lines);
|
||||||
|
|
||||||
return array_values(array_unique($labels));
|
return array_values(array_unique($labels));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function reset(): void
|
public function reset(): void
|
||||||
{
|
{
|
||||||
$this->currentLines = $this->lines;
|
$this->currentLineIndexes = array_keys($this->lines);
|
||||||
|
shuffle($this->currentLineIndexes);
|
||||||
|
$this->currentLineIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getLastReadLineIndex(): int
|
public function getLastReadLineIndex(): int
|
||||||
|
|||||||
@@ -2,27 +2,28 @@
|
|||||||
|
|
||||||
namespace App\Services\IterationEventBuffer;
|
namespace App\Services\IterationEventBuffer;
|
||||||
|
|
||||||
|
use App\Events\PerceptronTrainingIteration;
|
||||||
|
|
||||||
class PerceptronIterationEventBuffer implements IPerceptronIterationEventBuffer
|
class PerceptronIterationEventBuffer implements IPerceptronIterationEventBuffer
|
||||||
{
|
{
|
||||||
private $data;
|
private array $data = [];
|
||||||
|
|
||||||
private int $nextSizeIncreaseThreshold;
|
private ?float $lastBroadcastAt = null;
|
||||||
|
|
||||||
private int $underSizeIncreaseCount = 0;
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private string $sessionId,
|
private string $sessionId,
|
||||||
private string $trainingId,
|
private string $trainingId,
|
||||||
private int $sizeIncreaseStart = 10,
|
) {}
|
||||||
private int $sizeIncreaseFactor = 2,
|
|
||||||
) {
|
|
||||||
$this->data = [];
|
|
||||||
$this->nextSizeIncreaseThreshold = $sizeIncreaseStart;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function flush(): void
|
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 = [];
|
$this->data = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,27 +36,38 @@ class PerceptronIterationEventBuffer implements IPerceptronIterationEventBuffer
|
|||||||
'weights' => $synaptic_weights,
|
'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;
|
$this->data[] = $iteration;
|
||||||
|
|
||||||
if ($this->underSizeIncreaseCount <= $this->sizeIncreaseStart) { // We can still send a single date because we are under the increase start threshold
|
if ($this->data !== [] && $this->payloadExceedsLimit()) {
|
||||||
$this->underSizeIncreaseCount++;
|
$lastIteration = array_pop($this->data);
|
||||||
$this->flush();
|
$this->flush();
|
||||||
} elseif (count($this->data) >= $this->nextSizeIncreaseThreshold) {
|
$this->data[] = $lastIteration;
|
||||||
$this->flush();
|
}
|
||||||
$this->nextSizeIncreaseThreshold *= $this->sizeIncreaseFactor;
|
|
||||||
|
|
||||||
if ($this->nextSizeIncreaseThreshold > config('perceptron.broadcast_iteration_size')) {
|
if (count($this->data) >= config('perceptron.broadcast_iteration_size')) {
|
||||||
$this->nextSizeIncreaseThreshold = config('perceptron.broadcast_iteration_size'); // Cap the threshold to the maximum 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;
|
namespace App\Services\IterationEventBuffer;
|
||||||
|
|
||||||
|
use App\Events\PerceptronTrainingIteration;
|
||||||
|
|
||||||
class PerceptronLimitedEpochEventBuffer implements IPerceptronIterationEventBuffer
|
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(
|
public function __construct(
|
||||||
private string $sessionId,
|
private string $sessionId,
|
||||||
private string $trainingId,
|
private string $trainingId,
|
||||||
private int $epochInterval,
|
private int $epochInterval,
|
||||||
private int $sizeIncreaseStart = 10,
|
) {}
|
||||||
) {
|
|
||||||
$this->data = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function flush(): void
|
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 = [];
|
$this->data = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,16 +41,42 @@ class PerceptronLimitedEpochEventBuffer implements IPerceptronIterationEventBuff
|
|||||||
'weights' => $synaptic_weights,
|
'weights' => $synaptic_weights,
|
||||||
];
|
];
|
||||||
|
|
||||||
$lastEpoch = $this->data[0]['epoch'] ?? null;
|
if ($this->activeEpoch !== $epoch) {
|
||||||
if ($this->data && $lastEpoch !== $epoch) { // Current Epoch has changed from the last one
|
$this->flush();
|
||||||
if ($lastEpoch == 1 || $lastEpoch % $this->epochInterval === 0) { // The last saved epoch need to be sent
|
$this->activeEpoch = $epoch;
|
||||||
$this->flush(); // Flush all data from the previous epoch
|
$this->shouldBroadcastEpoch = $epoch === 1 || $epoch % $this->epochInterval === 0;
|
||||||
} else {
|
|
||||||
$this->data = []; // We clear the data without sending it as we are saving the next epoch data
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$lastEpoch = $epoch;
|
if (! $this->shouldBroadcastEpoch) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->data[] = $newData;
|
$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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
0, 0, -1
|
|
||||||
0, 1, 1
|
|
||||||
1, 0, 1
|
|
||||||
1, 1, 1
|
|
||||||
|
@@ -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
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ChartData } from 'chart.js';
|
import type { ChartDataset } from 'chart.js';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { Bar } from 'vue-chartjs';
|
import { Chart } from 'vue-chartjs';
|
||||||
import { colors, gridColor, gridColorBold } from '@/types/graphs';
|
import { colors, gridColor, gridColorBold } from '@/types/graphs';
|
||||||
import type { Iteration } from '@/types/perceptron';
|
import type { Iteration } from '@/types/perceptron';
|
||||||
import Toggle from './ui/toggle/Toggle.vue';
|
import Toggle from './ui/toggle/Toggle.vue';
|
||||||
@@ -11,16 +11,16 @@ const props = defineProps<{
|
|||||||
isRegression: boolean;
|
isRegression: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
type ErrorValue = number | [number, number] | null;
|
||||||
|
type ErrorDataset = ChartDataset<'bar' | 'line', ErrorValue[]>;
|
||||||
|
|
||||||
const epochErrorOnly = ref<boolean>(false);
|
const epochErrorOnly = ref<boolean>(false);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Datasets of the iterations with the form { label: `Exemple ${exampleIndex}`, data: [error for iteration 1, error for iteration 2, ...] }
|
* Datasets of the iterations with the form { label: `Exemple ${exampleIndex}`, data: [error for iteration 1, error for iteration 2, ...] }
|
||||||
*/
|
*/
|
||||||
const datasets = computed<
|
const datasets = computed<ErrorDataset[]>(() => {
|
||||||
ChartData<'bar', (number | [number, number] | null)[]>[]
|
const datasets: ErrorDataset[] = [];
|
||||||
>(() => {
|
|
||||||
const datasets: ChartData<'bar', (number | [number, number] | null)[]>[] =
|
|
||||||
[];
|
|
||||||
const epochAverageError: number[] = [];
|
const epochAverageError: number[] = [];
|
||||||
|
|
||||||
const backgroundColors = colors;
|
const backgroundColors = colors;
|
||||||
@@ -28,6 +28,8 @@ const datasets = computed<
|
|||||||
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<
|
|||||||
}
|
}
|
||||||
dataset.data.push(
|
dataset.data.push(
|
||||||
props.isRegression
|
props.isRegression
|
||||||
? Math.abs(iteration.error)
|
? Math.abs(error)
|
||||||
: iteration.error,
|
: error,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,19 +57,19 @@ const datasets = computed<
|
|||||||
// 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, ...)
|
||||||
datasets.sort((a, b) => {
|
datasets.sort((a, b) => {
|
||||||
const aIndex = parseInt(a.label.split(' ')[1]);
|
const aIndex = parseInt((a.label ?? '').split(' ')[1]);
|
||||||
const bIndex = parseInt(b.label.split(' ')[1]);
|
const bIndex = parseInt((b.label ?? '').split(' ')[1]);
|
||||||
return aIndex - bIndex;
|
return aIndex - bIndex;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Epoch error
|
// Epoch error
|
||||||
const epochErrorDataset = {
|
const epochErrorDataset: ErrorDataset = {
|
||||||
type: 'line',
|
type: 'line' as const,
|
||||||
label: "Erreur quadratique moyenne de l'époque",
|
label: "Erreur quadratique moyenne de l'époque",
|
||||||
data: [],
|
data: [],
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
@@ -88,7 +90,8 @@ const datasets = computed<
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Bar
|
<Chart
|
||||||
|
type="bar"
|
||||||
class="bg-primary dark:bg-transparent!"
|
class="bg-primary dark:bg-transparent!"
|
||||||
:options="{
|
:options="{
|
||||||
responsive: true,
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Head } from '@inertiajs/vue3';
|
import { Head } from '@inertiajs/vue3';
|
||||||
import { useEcho } from '@laravel/echo-vue';
|
|
||||||
import {
|
import {
|
||||||
Chart as ChartJS,
|
Chart as ChartJS,
|
||||||
Title,
|
Title,
|
||||||
@@ -14,13 +13,8 @@ import {
|
|||||||
} from 'chart.js';
|
} from 'chart.js';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import LinkHeader from '@/components/LinkHeader.vue';
|
import LinkHeader from '@/components/LinkHeader.vue';
|
||||||
import type {
|
import { usePerceptronTraining } from '@/composables/usePerceptronTraining';
|
||||||
Dataset,
|
import type { Dataset, DatasetPoint, InitializationMethod, PerceptronType } from '@/types/perceptron';
|
||||||
DatasetPoint,
|
|
||||||
InitializationMethod,
|
|
||||||
Iteration,
|
|
||||||
PerceptronType,
|
|
||||||
} from '@/types/perceptron';
|
|
||||||
import IterationTable from '../components/IterationTable.vue';
|
import IterationTable from '../components/IterationTable.vue';
|
||||||
import PerceptronDecisionGraph from '../components/PerceptronDecisionGraph.vue';
|
import PerceptronDecisionGraph from '../components/PerceptronDecisionGraph.vue';
|
||||||
import PerceptronIterationsErrorsGraph from '../components/PerceptronIterationsErrorsGraph.vue';
|
import PerceptronIterationsErrorsGraph from '../components/PerceptronIterationsErrorsGraph.vue';
|
||||||
@@ -86,74 +80,16 @@ const cleanedDataset = computed<
|
|||||||
const hiddenLayers = ref(3);
|
const hiddenLayers = ref(3);
|
||||||
const hiddenLayersNeurons = ref(3);
|
const hiddenLayersNeurons = ref(3);
|
||||||
const initializationMethod = ref<InitializationMethod>(props.type === 'multilayer' ? 'random' : 'zeros');
|
const initializationMethod = ref<InitializationMethod>(props.type === 'multilayer' ? 'random' : 'zeros');
|
||||||
|
const {
|
||||||
console.log('Session ID:', props.sessionId);
|
activationFunction,
|
||||||
|
iterations,
|
||||||
useEcho(
|
setTrainingId,
|
||||||
`${props.sessionId}-perceptron-training`,
|
trainingEnded,
|
||||||
'PerceptronTrainingIteration',
|
trainingEndReason,
|
||||||
percpetronIteration,
|
} = usePerceptronTraining(props.sessionId);
|
||||||
[{}],
|
|
||||||
'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 isRegression = computed(
|
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 {
|
function getActivationFunction(type: string): (x: number) => number {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'step':
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -197,11 +127,7 @@ function resetTraining() {
|
|||||||
selectedDatasetName = newValue;
|
selectedDatasetName = newValue;
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@update:training-id="
|
@update:training-id="setTrainingId"
|
||||||
(newValue) => {
|
|
||||||
trainingId = newValue;
|
|
||||||
resetTraining();
|
|
||||||
}"
|
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="align-items-start justify-content-center flex h-full min-h-dvh max-w-dvw"
|
class="align-items-start justify-content-center flex h-full min-h-dvh max-w-dvw"
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user