Compare commits

...

14 Commits

Author SHA1 Message Date
Ninluc 6d8e3438f0 Track some events
linter / quality (push) Has been cancelled
tests / ci (8.3) (push) Successful in 3m57s
2026-09-19 10:43:05 +02:00
Ninluc ae676343a5 Fix send cancel request
linter / quality (push) Successful in 5m1s
tests / ci (8.3) (push) Successful in 4m28s
2026-09-18 23:58:51 +02:00
Ninluc 9389aef632 Fix cancel route
linter / quality (push) Has been cancelled
tests / ci (8.3) (push) Successful in 3m49s
2026-09-18 23:41:31 +02:00
Ninluc fa6c427d76 Fix cancel ?
linter / quality (push) Failing after 1m3s
tests / ci (8.3) (push) Successful in 3m48s
2026-09-18 23:24:13 +02:00
Ninluc 25b03fc39a Cancel Training
linter / quality (push) Failing after 1m11s
tests / ci (8.3) (push) Successful in 3m55s
2026-09-18 23:08:19 +02:00
Ninluc d80bfe3cdd Updated Interface Image and added excalidraws
linter / quality (push) Successful in 6m26s
tests / ci (8.3) (push) Successful in 5m11s
2026-09-18 14:19:17 +02:00
Ninluc 2332e805e8 Logarithmic error scale
linter / quality (push) Successful in 5m12s
tests / ci (8.3) (push) Successful in 4m29s
2026-09-18 14:12:23 +02:00
Ninluc 4610334f72 Do not exclude get parameters
linter / quality (push) Successful in 4m54s
tests / ci (8.3) (push) Successful in 4m2s
2026-09-18 09:03:46 +02:00
Ninluc 9ae3a6942b Default perrceptron to simple
linter / quality (push) Successful in 4m36s
tests / ci (8.3) (push) Successful in 3m48s
2026-09-17 21:55:58 +02:00
Ninluc 3a19691591 Remove debug config value
linter / quality (push) Successful in 4m11s
tests / ci (8.3) (push) Successful in 3m39s
2026-09-17 21:46:49 +02:00
Ninluc 2d7c1873bb Fix Bar chart
linter / quality (push) Successful in 5m36s
tests / ci (8.3) (push) Successful in 4m42s
2026-09-17 21:24:42 +02:00
Ninluc 5d68757e9d Fix Reverb ?
linter / quality (push) Successful in 6m20s
tests / ci (8.3) (push) Successful in 4m57s
2026-09-17 21:01:15 +02:00
Ninluc 2de27af03b Some anonymous analytics I promise 2026-09-17 20:58:06 +02:00
Ninluc be7b276dec Remove asset building for test 2026-09-17 19:56:38 +02:00
25 changed files with 5767 additions and 254 deletions
+3
View File
@@ -45,6 +45,9 @@ REVERB_APP_SECRET=oosuq0v9jgaslzp9cmhv
REVERB_HOST="perceptron.matthiasg.dev"
REVERB_PORT=443
REVERB_SCHEME=https
REVERB_BROADCAST_HOST=reverb
REVERB_BROADCAST_PORT=8080
REVERB_BROADCAST_SCHEME=http
REVERB_MAX_REQUEST_SIZE=100000
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
+2 -2
View File
@@ -80,8 +80,8 @@ jobs:
# -------------------------
# Build (optional remove if not needed for tests)
# -------------------------
- name: Build Assets
run: npm run build
# - name: Build Assets
# run: npm run build
# -------------------------
# Run tests (parallel)
@@ -0,0 +1,9 @@
<?php
namespace App\Exceptions;
use RuntimeException;
class TrainingCancelledException extends RuntimeException
{
}
+31 -7
View File
@@ -3,6 +3,7 @@
namespace App\Http\Controllers;
use App\Events\PerceptronInitialization;
use App\Exceptions\TrainingCancelledException;
use App\Http\Requests\RunPerceptronRequest;
use App\Models\NetworksTraining\ADALINEPerceptronTraining;
use App\Models\NetworksTraining\GradientDescentPerceptronTraining;
@@ -18,15 +19,31 @@ use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider;
use App\Services\SynapticWeightsProvider\RandomSynapticWeights;
use App\Services\SynapticWeightsProvider\ZeroSynapticWeights;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class PerceptronController extends Controller
{
private function cancellationKey(string $trainingId): string
{
return "perceptron-training-cancelled:{$trainingId}";
}
public function cancel(Request $request)
{
$trainingId = $request->validate([
'training_id' => ['required', 'string', 'max:100'],
])['training_id'];
Cache::put($this->cancellationKey($trainingId), true, now()->addHour());
return response()->noContent();
}
/**
* Display the specified resource.
*/
public function index(Request $request)
{
$perceptronType = $request->query('type');
$perceptronType = $request->query('type', 'simple'); ;
$learningRate = 0.01;
$maxIterations = 200;
@@ -220,6 +237,8 @@ class PerceptronController extends Controller
$sessionId = $request->input('session_id', session()->getId());
$trainingId = $request->input('training_id');
Cache::forget($this->cancellationKey($trainingId));
// Zero initialization prevents hidden layers from receiving a gradient.
if ($perceptronType === 'multilayer' && $weightInitMethod === 'zeros') {
$synapticWeightsProvider = new RandomSynapticWeights;
@@ -236,17 +255,22 @@ class PerceptronController extends Controller
$datasetReader = $this->getDataSetReader($dataSet);
$networkTraining = match ($perceptronType) {
'simple' => new SimpleBinaryPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId),
'gradientdescent' => new GradientDescentPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError),
'adaline' => new ADALINEPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError),
'monolayer' => new MonoLayerPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError),
'multilayer' => new MultiLayerPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $hiddenLayers, $hiddenLayersNeurons, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError),
'simple' => new SimpleBinaryPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, fn (): bool => connection_aborted() || Cache::has($this->cancellationKey($trainingId))),
'gradientdescent' => new GradientDescentPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError, fn (): bool => connection_aborted() || Cache::has($this->cancellationKey($trainingId))),
'adaline' => new ADALINEPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError, fn (): bool => connection_aborted() || Cache::has($this->cancellationKey($trainingId))),
'monolayer' => new MonoLayerPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError, fn (): bool => connection_aborted() || Cache::has($this->cancellationKey($trainingId))),
'multilayer' => new MultiLayerPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $hiddenLayers, $hiddenLayersNeurons, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError, fn (): bool => connection_aborted() || Cache::has($this->cancellationKey($trainingId))),
default => null,
};
event(new PerceptronInitialization($datasetReader->lines, $networkTraining->activationFunction, $sessionId, $trainingId));
$networkTraining->start();
try {
$networkTraining->start();
} catch (TrainingCancelledException) {
$networkTraining->cancel();
Cache::forget($this->cancellationKey($trainingId));
}
return back()->with('success', [
'message' => 'Training completed',
@@ -8,6 +8,7 @@ use App\Models\Perceptrons\Perceptron;
use App\Services\DatasetReader\IDataSetReader;
use App\Services\IterationEventBuffer\IPerceptronIterationEventBuffer;
use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider;
use Closure;
class ADALINEPerceptronTraining extends NetworkTraining
{
@@ -26,8 +27,9 @@ class ADALINEPerceptronTraining extends NetworkTraining
string $sessionId,
string $trainingId,
private float $minError,
?Closure $isCancelled = null,
) {
parent::__construct($datasetReader, $maxEpochs, $iterationEventBuffer, $sessionId, $trainingId);
parent::__construct($datasetReader, $maxEpochs, $iterationEventBuffer, $sessionId, $trainingId, $isCancelled);
$this->perceptron = new GradientDescentPerceptron($synapticWeightsProvider->generate($datasetReader->getInputSize()));
}
@@ -8,6 +8,7 @@ use App\Models\Perceptrons\Perceptron;
use App\Services\DatasetReader\IDataSetReader;
use App\Services\IterationEventBuffer\IPerceptronIterationEventBuffer;
use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider;
use Closure;
class GradientDescentPerceptronTraining extends NetworkTraining
{
@@ -26,8 +27,9 @@ class GradientDescentPerceptronTraining extends NetworkTraining
string $sessionId,
string $trainingId,
private float $minError,
?Closure $isCancelled = null,
) {
parent::__construct($datasetReader, $maxEpochs, $iterationEventBuffer, $sessionId, $trainingId);
parent::__construct($datasetReader, $maxEpochs, $iterationEventBuffer, $sessionId, $trainingId, $isCancelled);
$this->perceptron = new GradientDescentPerceptron($synapticWeightsProvider->generate($datasetReader->getInputSize()));
}
@@ -10,6 +10,7 @@ use App\Services\DatasetReader\IDataSetReader;
use App\Services\IterationEventBuffer\IPerceptronIterationEventBuffer;
use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider;
use App\Services\SynapticWeightsProvider\SimpleNetworkWeightsProvider;
use Closure;
use Illuminate\Support\Arr;
class MonoLayerPerceptronTraining extends NetworkTraining
@@ -35,8 +36,9 @@ class MonoLayerPerceptronTraining extends NetworkTraining
string $sessionId,
string $trainingId,
private float $minError,
?Closure $isCancelled = null,
) {
parent::__construct($datasetReader, $maxEpochs, $iterationEventBuffer, $sessionId, $trainingId);
parent::__construct($datasetReader, $maxEpochs, $iterationEventBuffer, $sessionId, $trainingId, $isCancelled);
$this->isRegression = $datasetReader->getInputSize() === 1;
$networkWeightsProvider = new SimpleNetworkWeightsProvider($synapticWeightsProvider);
$this->network = new NetworkPerceptron(
@@ -11,6 +11,7 @@ use App\Services\DatasetReader\IDataSetReader;
use App\Services\IterationEventBuffer\IPerceptronIterationEventBuffer;
use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider;
use App\Services\SynapticWeightsProvider\SimpleNetworkWeightsProvider;
use Closure;
use Illuminate\Support\Arr;
class MultiLayerPerceptronTraining extends NetworkTraining
@@ -37,8 +38,9 @@ class MultiLayerPerceptronTraining extends NetworkTraining
string $sessionId,
string $trainingId,
private float $minError,
?Closure $isCancelled = null,
) {
parent::__construct($datasetReader, $maxEpochs, $iterationEventBuffer, $sessionId, $trainingId);
parent::__construct($datasetReader, $maxEpochs, $iterationEventBuffer, $sessionId, $trainingId, $isCancelled);
$this->labels = $datasetReader->getLabels();
$this->isRegression = $datasetReader->getOutputSize() === 1
|| ($datasetReader->getOutputSize() > 2
@@ -3,9 +3,11 @@
namespace App\Models\NetworksTraining;
use App\Events\PerceptronTrainingEnded;
use App\Exceptions\TrainingCancelledException;
use App\Models\ActivationsFunctions;
use App\Services\DatasetReader\IDataSetReader;
use App\Services\IterationEventBuffer\IPerceptronIterationEventBuffer;
use Closure;
abstract class NetworkTraining
{
@@ -24,6 +26,7 @@ abstract class NetworkTraining
protected IPerceptronIterationEventBuffer $iterationEventBuffer,
protected string $sessionId,
protected string $trainingId,
protected ?Closure $isCancelled = null,
) {}
abstract public function start(): void;
@@ -50,9 +53,18 @@ abstract class NetworkTraining
protected function addIterationToBuffer(float $error, array $synapticWeights)
{
if ($this->isCancelled !== null && ($this->isCancelled)()) {
throw new TrainingCancelledException;
}
$this->iterationEventBuffer->addIteration($this->epoch, $this->datasetReader->getLastReadLineIndex(), $error, $synapticWeights);
}
public function cancel(): void
{
$this->broadcastTrainingEnded('Entraînement annulé');
}
public function getEpoch(): int
{
return $this->epoch;
@@ -9,6 +9,7 @@ use App\Models\Perceptrons\SimpleBinaryPerceptron;
use App\Services\DatasetReader\IDataSetReader;
use App\Services\IterationEventBuffer\IPerceptronIterationEventBuffer;
use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider;
use Closure;
class SimpleBinaryPerceptronTraining extends NetworkTraining
{
@@ -28,8 +29,9 @@ class SimpleBinaryPerceptronTraining extends NetworkTraining
IPerceptronIterationEventBuffer $iterationEventBuffer,
string $sessionId,
string $trainingId,
?Closure $isCancelled = null,
) {
parent::__construct($datasetReader, $maxEpochs, $iterationEventBuffer, $sessionId, $trainingId);
parent::__construct($datasetReader, $maxEpochs, $iterationEventBuffer, $sessionId, $trainingId, $isCancelled);
$this->perceptron = new SimpleBinaryPerceptron($synapticWeightsProvider->generate($datasetReader->getInputSize()));
}
+4 -4
View File
@@ -36,10 +36,10 @@ return [
'secret' => env('REVERB_APP_SECRET'),
'app_id' => env('REVERB_APP_ID'),
'options' => [
'host' => env('REVERB_HOST'),
'port' => env('REVERB_PORT', 443),
'scheme' => env('REVERB_SCHEME', 'https'),
'useTLS' => env('REVERB_SCHEME', 'https') === 'https',
'host' => env('REVERB_BROADCAST_HOST', env('REVERB_HOST')),
'port' => env('REVERB_BROADCAST_PORT', env('REVERB_PORT', 443)),
'scheme' => env('REVERB_BROADCAST_SCHEME', env('REVERB_SCHEME', 'https')),
'useTLS' => env('REVERB_BROADCAST_SCHEME', env('REVERB_SCHEME', 'https')) === 'https',
],
'client_options' => [
// Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
+1 -1
View File
@@ -22,7 +22,7 @@ return [
/**
* Minimum time between training progress broadcasts, in milliseconds.
*/
'broadcast_minimum_interval_ms' => 0,
'broadcast_minimum_interval_ms' => 100,
/**
* Maximum number of weights for which all iteration weights are broadcast
+6
View File
@@ -6,6 +6,7 @@
"": {
"dependencies": {
"@inertiajs/vue3": "^2.3.7",
"@jaseeey/vue-umami-plugin": "^1.6.1",
"@lucide/vue": "^1.46.0",
"@tailwindcss/typography": "^0.5.20",
"@vee-validate/zod": "^4.15.1",
@@ -861,6 +862,11 @@
"@swc/helpers": "^0.5.0"
}
},
"node_modules/@jaseeey/vue-umami-plugin": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jaseeey/vue-umami-plugin/-/vue-umami-plugin-1.6.1.tgz",
"integrity": "sha512-a38bB7cYbwMP5HmlrT2Dk2RHdGFxVvLRci2GhY8cIwCpM419qXtvSM6SKmIJ3U3u7zYdcQLVAySOcXY150Z14Q=="
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+1
View File
@@ -37,6 +37,7 @@
},
"dependencies": {
"@inertiajs/vue3": "^2.3.7",
"@jaseeey/vue-umami-plugin": "^1.6.1",
"@lucide/vue": "^1.46.0",
"@tailwindcss/typography": "^0.5.20",
"@vee-validate/zod": "^4.15.1",
+1 -1
View File
@@ -59,7 +59,7 @@ Reprenons le dataset oblique afin de pouvoir comparer les résultats, et diminuo
Puis cliquez sur "Lancer" tout en laissant les autres paramètres intacts.
On peut voir en dessous du tableau que l'entraînement s'est arrêté car le nombre maximal d'époques a été atteint. On pourrait l'augmenter (max 5000 pour mon petit serveur), mais ce serait du temps perdu. La solution du réseau est suffisante et il n'y aurait pas beaucoup de gain, même pour le triple d'itérations maximales en plus. Pour le prouver, cliquez sur le bouton `Afficher uniquement l'erreur quadratique moyenne`, la ligne que l'on peut voir ressemble fortement à la fonction logarithme $y = log(x^{-1})$. La progression de la descente du gradient est donc logarithmique ; les plus gros changements se font dans les premières époques.
On peut voir en dessous du tableau que l'entraînement s'est arrêté car le nombre maximal d'époques a été atteint. On pourrait l'augmenter (max 5000 pour mon petit serveur), mais ce serait du temps perdu. La solution du réseau est suffisante et il n'y aurait pas beaucoup de gain, même pour le triple d'itérations maximales en plus. Pour le prouver, cliquez sur le bouton `Afficher uniquement l'erreur quadratique moyenne`, la ligne que l'on peut voir ressemble fortement à la fonction logarithme $y = log(x^{-1})$ (attention à l'échelle qui est elle-même logarithmique). La progression de la descente du gradient est donc logarithmique ; les plus gros changements se font dans les premières époques.
</div>
File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 KiB

After

Width:  |  Height:  |  Size: 205 KiB

File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+15
View File
@@ -1,4 +1,5 @@
import { createInertiaApp } from '@inertiajs/vue3';
import { VueUmamiPlugin } from '@jaseeey/vue-umami-plugin';
import { configureEcho } from '@laravel/echo-vue';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import type { DefineComponent } from 'vue';
@@ -22,6 +23,20 @@ createInertiaApp({
setup({ el, App, props, plugin }) {
createApp({ render: () => h(App, props) })
.use(plugin)
.use(
VueUmamiPlugin({
websiteID: 'e616767f-e0d3-4ce9-b551-33c7c3806fc0',
scriptSrc: 'https://abcd.matthiasg.dev/script.js',
// autoTrack: true,
debug: import.meta.env.DEV,
extraDataAttributes: {
'data-auto-track': 'true',
'data-performance': 'true',
'data-domains': 'perceptron.matthiasg.dev,matthiasg.dev,www.matthiasg.dev',
'data-exclude-search': 'false',
}
}),
)
.mount(el);
},
progress: {
@@ -113,8 +113,8 @@ const datasets = computed<ErrorDataset[]>(() => {
min: 0,
},
y: {
type: !epochErrorOnly ? 'linear' : 'logarithmic',
stacked: true,
beginAtZero: !props.isRegression,
grid: {
color: function (context) {
if (context.tick.value == 0) {
+113 -28
View File
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { useForm } from '@inertiajs/vue3';
import { trackUmamiEvent } from '@jaseeey/vue-umami-plugin';
import { ref, watch } from 'vue';
import {
Form,
@@ -12,6 +13,7 @@ import {
NativeSelect,
NativeSelectOption,
} from '@/components/ui/native-select';
import { cancel } from '@/routes/perceptron';
import type {
Dataset,
InitializationMethod,
@@ -23,8 +25,9 @@ import Card from './ui/card/Card.vue';
import CardContent from './ui/card/CardContent.vue';
import CardHeader from './ui/card/CardHeader.vue';
import CardTitle from './ui/card/CardTitle.vue';
import Input from './ui/input/Input.vue';
import FormError from './ui/form/FormError.vue';
import Input from './ui/input/Input.vue';
import Spinner from './ui/spinner/Spinner.vue';
const props = defineProps<{
type: PerceptronType;
@@ -98,43 +101,69 @@ watch(maxIterations, (value) => {
watch(selectedDatasetCopy, (newvalue) => {
form.clearErrors('dataset');
const selectedDatasetCopy = props.datasets.find(
(dataset) => dataset.label === newvalue
) || null;
const selectedDatasetCopy =
props.datasets.find((dataset) => dataset.label === newvalue) || null;
// LearningRate
learningRate.value = props.defaultLearningRate;
if (selectedDatasetCopy && selectedDatasetCopy.defaultLearningRate !== undefined) {
if (
selectedDatasetCopy &&
selectedDatasetCopy.defaultLearningRate !== undefined
) {
learningRate.value = selectedDatasetCopy.defaultLearningRate;
}
// MinError
minError.value = props.minError;
if (selectedDatasetCopy && selectedDatasetCopy.defaultMinError !== undefined) {
if (
selectedDatasetCopy &&
selectedDatasetCopy.defaultMinError !== undefined
) {
minError.value = selectedDatasetCopy.defaultMinError;
}
// MaxIterations
maxIterations.value = props.defaultMaxIterations;
if (selectedDatasetCopy && selectedDatasetCopy.defaultMaxIterations !== undefined) {
if (
selectedDatasetCopy &&
selectedDatasetCopy.defaultMaxIterations !== undefined
) {
maxIterations.value = selectedDatasetCopy.defaultMaxIterations;
}
// HiddenLayers
hiddenLayers.value = props.hiddenLayers;
if (selectedDatasetCopy && selectedDatasetCopy.defaultHiddenLayers !== undefined) {
if (
selectedDatasetCopy &&
selectedDatasetCopy.defaultHiddenLayers !== undefined
) {
hiddenLayers.value = selectedDatasetCopy.defaultHiddenLayers;
}
hiddenLayersNeurons.value = props.hiddenLayersNeurons;
if (selectedDatasetCopy && selectedDatasetCopy.defaultHiddenLayersNeurons !== undefined) {
hiddenLayersNeurons.value = selectedDatasetCopy.defaultHiddenLayersNeurons;
if (
selectedDatasetCopy &&
selectedDatasetCopy.defaultHiddenLayersNeurons !== undefined
) {
hiddenLayersNeurons.value =
selectedDatasetCopy.defaultHiddenLayersNeurons;
}
})
});
const trainingId = ref<string>('');
const trainingId = ref<string>('');
function startTraining() {
trackUmamiEvent('perceptron-training-start', {
type: props.type,
dataset: selectedDatasetCopy.value,
weight_init_method: selectedMethod.value,
hidden_layers: hiddenLayers.value,
hidden_layers_neurons: hiddenLayersNeurons.value,
min_error: minError.value,
learning_rate: learningRate.value,
max_iterations: maxIterations.value,
});
if (!selectedDatasetCopy.value) {
form.setError(
'dataset',
'Un dataset est nécessaire avant de lancer l\'entraînement.',
"Un dataset est nécessaire avant de lancer l'entraînement.",
);
console.log(form.errors);
return;
@@ -156,19 +185,33 @@ function startTraining() {
max_iterations: maxIterations.value,
});
console.debug('[max_iterations] submitting', {
displayed: maxIterationsInput.value?.inputElement?.value,
refValue: maxIterations.value,
formValue: form.max_iterations,
limit: props.maxIterationsLimit,
});
form.post('/perceptron/run', {
preserveScroll: true,
});
}
const emit = defineEmits(['update:selectedDataset', 'update:trainingId']);
async function cancelTraining() {
trackUmamiEvent('perceptron-training-cancel', {
training_id: trainingId.value,
});
form.cancel();
await fetch(cancel().url, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
credentials: 'same-origin',
body: JSON.stringify({
training_id: trainingId.value,
}),
});
}
watch(selectedDatasetCopy, (newValue) => {
emit('update:selectedDataset', newValue);
});
@@ -182,6 +225,7 @@ watch(selectedDatasetCopy, (newValue) => {
<CardContent>
<Form
class="grid auto-cols-max grid-flow-row grid-cols-1 gap-4 space-y-6 md:grid-cols-2"
cancel-on-unmount
>
<!-- DATASET -->
<FormField name="dataset">
@@ -206,7 +250,12 @@ watch(selectedDatasetCopy, (newValue) => {
</NativeSelectOption>
</NativeSelect>
</FormControl>
<FormError :error="form.errors.dataset || props.errors?.selectedDatasetCopy" />
<FormError
:error="
form.errors.dataset ||
props.errors?.selectedDatasetCopy
"
/>
</FormItem>
</FormField>
@@ -224,7 +273,9 @@ watch(selectedDatasetCopy, (newValue) => {
class="cursor-pointer"
>
<NativeSelectOption
v-for="method in (props.type == 'multilayer' ? ['random'] : ['zeros', 'random'])"
v-for="method in props.type == 'multilayer'
? ['random']
: ['zeros', 'random']"
v-bind:key="method"
:value="method"
>
@@ -236,7 +287,10 @@ watch(selectedDatasetCopy, (newValue) => {
</FormField>
<!-- HIDDEN LAYERS -->
<FormField name="hidden_layers" v-if="props.type === 'multilayer'">
<FormField
name="hidden_layers"
v-if="props.type === 'multilayer'"
>
<FormItem>
<FormLabel>Nombre de couches cachées</FormLabel>
<FormControl>
@@ -255,9 +309,14 @@ watch(selectedDatasetCopy, (newValue) => {
</FormField>
<!-- HIDDEN LAYERS NEURONS -->
<FormField name="hidden_layers_neurons" v-if="props.type === 'multilayer'">
<FormField
name="hidden_layers_neurons"
v-if="props.type === 'multilayer'"
>
<FormItem>
<FormLabel>Nombre de neurones par couche cachée</FormLabel>
<FormLabel
>Nombre de neurones par couche cachée</FormLabel
>
<FormControl>
<!-- TODO : MAX input -->
<Input
@@ -319,13 +378,39 @@ watch(selectedDatasetCopy, (newValue) => {
@input="handleMaxIterationsInput"
/>
</FormControl>
<div v-if="form.errors.max_iterations || props.errors.max_iterations">
{{ form.errors.max_iterations || props.errors.max_iterations }}
<div
v-if="
form.errors.max_iterations ||
props.errors.max_iterations
"
>
{{
form.errors.max_iterations ||
props.errors.max_iterations
}}
</div>
</FormItem>
</FormField>
</Form>
<Button variant="outline" class="cursor-pointer mt-6" @click="startTraining">Lancer</Button>
<Transition name="fade">
<Button
variant="outline"
class="mt-6 cursor-pointer"
:disabled="form.processing"
@click="startTraining"
>Lancer<Spinner v-if="form.processing" class="ml-1"
/></Button>
</Transition>
<Transition name="fade">
<Button
variant="outline"
class="mt-6 ml-4 cursor-pointer"
@click="cancelTraining"
v-if="form.processing"
>Annuler</Button
>
</Transition>
</CardContent>
</Card>
</template>
+15 -8
View File
@@ -1,5 +1,7 @@
<script setup lang="ts">
import { Head } from '@inertiajs/vue3';
import { trackUmamiEvent } from '@jaseeey/vue-umami-plugin';
import { BookOpenText } from '@lucide/vue';
import { useEventListener } from '@vueuse/core';
import {
Chart as ChartJS,
@@ -13,11 +15,21 @@ import {
LineElement,
ScatterController,
LineController,
BarController,
LogarithmicScale,
} from 'chart.js';
import { ArrowDown, ArrowUp } from 'lucide-vue-next';
import { computed, nextTick, ref, watch } from 'vue';
import HelpText from '@/components/HelpText.vue';
import LinkHeader from '@/components/LinkHeader.vue';
import Button from '@/components/ui/button/Button.vue';
import Drawer from '@/components/ui/drawer/Drawer.vue';
import DrawerContent from '@/components/ui/drawer/DrawerContent.vue';
import DrawerTitle from '@/components/ui/drawer/DrawerTitle.vue';
import DrawerTrigger from '@/components/ui/drawer/DrawerTrigger.vue';
import Kbd from '@/components/ui/kbd/Kbd.vue';
import KbdGroup from '@/components/ui/kbd/KbdGroup.vue';
import ScrollArea from '@/components/ui/scroll-area/ScrollArea.vue';
import {
Tooltip as UiTooltip,
@@ -31,14 +43,6 @@ import IterationTable from '../components/IterationTable.vue';
import PerceptronDecisionGraph from '../components/PerceptronDecisionGraph.vue';
import PerceptronIterationsErrorsGraph from '../components/PerceptronIterationsErrorsGraph.vue';
import PerceptronSetup from '../components/PerceptronSetup.vue';
import HelpText from '@/components/HelpText.vue';
import { BookOpenText } from '@lucide/vue';
import Drawer from '@/components/ui/drawer/Drawer.vue';
import DrawerTrigger from '@/components/ui/drawer/DrawerTrigger.vue';
import DrawerContent from '@/components/ui/drawer/DrawerContent.vue';
import DrawerTitle from '@/components/ui/drawer/DrawerTitle.vue';
import KbdGroup from '@/components/ui/kbd/KbdGroup.vue';
import Kbd from '@/components/ui/kbd/Kbd.vue';
ChartJS.register(
Title,
@@ -51,6 +55,8 @@ ChartJS.register(
LineElement,
ScatterController,
LineController,
BarController,
LogarithmicScale
);
ChartJS.defaults.font.size = 16;
ChartJS.defaults.color = '#FFF';
@@ -141,6 +147,7 @@ const handleDrawerOpenChange = async (open: boolean) => {
if (open) {
await restoreHelpScroll();
trackUmamiEvent('wiki-opened', { perceptronType: props.type });
}
};
+3
View File
@@ -19,6 +19,9 @@
})();
</script>
{{-- Anonymous analytics --}}
<script defer src="https://analytics.matthiasg.dev/recorder.js" data-website-id="e616767f-e0d3-4ce9-b551-33c7c3806fc0"></script>
{{-- Inline style to set the HTML background color based on our theme in app.css --}}
<style>
html {
+6
View File
@@ -1,2 +1,8 @@
<?php
use App\Http\Controllers\PerceptronController;
use Illuminate\Support\Facades\Route;
Route::post('perceptron/cancel', [PerceptronController::class, 'cancel'])
->name('perceptron.cancel');