Compare commits

...

32 Commits

Author SHA1 Message Date
Ninluc 21c02706de Dark mode by default
linter / quality (push) Successful in 4m25s
tests / ci (8.3) (push) Successful in 4m17s
2026-09-19 18:04:29 +02:00
Ninluc 7c223e0ba8 Increase max request time
linter / quality (push) Successful in 5m1s
tests / ci (8.3) (push) Successful in 3m47s
2026-09-19 15:28:52 +02:00
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
Ninluc 2345b850b8 ix scatter graph
linter / quality (push) Successful in 5m52s
tests / ci (8.3) (push) Successful in 6m19s
2026-09-17 19:45:02 +02:00
Ninluc 538ddc9679 Fix reverb
linter / quality (push) Successful in 6m42s
tests / ci (8.3) (push) Successful in 5m46s
2026-09-17 18:58:54 +02:00
Ninluc 099318c79e USE secrets
linter / quality (push) Successful in 4m10s
tests / ci (8.3) (push) Successful in 4m21s
2026-09-17 09:30:50 +02:00
Ninluc 1517920fce Use env var
linter / quality (push) Successful in 4m46s
tests / ci (8.3) (push) Successful in 4m23s
2026-09-16 23:06:34 +02:00
Ninluc d4f98517cd misc
linter / quality (push) Successful in 4m27s
tests / ci (8.3) (push) Successful in 4m9s
2026-09-16 22:41:11 +02:00
Ninluc feac0feaeb Trust proxies 2026-09-16 22:41:03 +02:00
Ninluc ba4b951a69 Revert "Migrate database"
linter / quality (push) Successful in 5m8s
tests / ci (8.3) (push) Successful in 5m49s
This reverts commit 41b0e8a64a.
2026-09-16 21:58:12 +02:00
Ninluc 41b0e8a64a Migrate database
linter / quality (push) Failing after 2m48s
tests / ci (8.3) (push) Successful in 4m38s
2026-09-16 21:34:38 +02:00
Ninluc da53bf16e9 Fix repository name
linter / quality (push) Successful in 4m11s
tests / ci (8.3) (push) Successful in 4m12s
2026-09-16 20:50:39 +02:00
Ninluc 3ad2e7d999 Fix tests versions 2026-09-16 20:47:33 +02:00
Ninluc 00f379811f Renammed project 2026-09-16 20:35:08 +02:00
Ninluc 3f1fcef1bc Fix docker publish work
linter / quality (push) Successful in 5m1s
tests / ci (8.4) (push) Successful in 4m14s
tests / ci (8.5) (push) Successful in 4m17s
2026-09-16 20:34:36 +02:00
Ninluc 5d71962893 Docker and CICD
linter / quality (push) Successful in 4m26s
tests / ci (8.4) (push) Successful in 4m24s
tests / ci (8.5) (push) Successful in 4m24s
2026-09-16 20:03:18 +02:00
Ninluc 9086364db7 Form error message
linter / quality (push) Successful in 4m19s
tests / ci (8.4) (push) Successful in 5m21s
tests / ci (8.5) (push) Successful in 4m34s
2026-09-16 19:27:27 +02:00
Ninluc 6d4fdffee9 Wiki
linter / quality (push) Successful in 5m1s
tests / ci (8.4) (push) Successful in 5m8s
tests / ci (8.5) (push) Successful in 6m31s
2026-09-15 19:44:58 +02:00
Ninluc ca6c9fe38c Reworked the table scroll
linter / quality (push) Successful in 5m3s
tests / ci (8.4) (push) Failing after 4m29s
tests / ci (8.5) (push) Failing after 5m27s
2026-09-09 19:34:30 +02:00
108 changed files with 39903 additions and 293 deletions
+56
View File
@@ -0,0 +1,56 @@
APP_NAME="PerceptronViewer"
APP_ENV=production
APP_KEY=base64:WE5DlJGDeIrl+B1ZqahVJSg4TN6oY3N1ph8CDKNo0w4=
APP_DEBUG=false
APP_URL=https://perceptron.matthiasg.dev
ASSET_URL=https://perceptron.matthiasg.dev
APP_LOCALE=fr
APP_FALLBACK_LOCALE=fr
APP_FAKER_LOCALE=fr_FR
APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database
PHP_CLI_SERVER_WORKERS=4
BCRYPT_ROUNDS=12
LOG_CHANNEL=single
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
BROADCAST_CONNECTION=reverb
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
CACHE_STORE=database
# CACHE_PREFIX=
MEMCACHED_HOST=memcached
VITE_APP_NAME="${APP_NAME}"
REVERB_SERVER_HOST=0.0.0.0
REVERB_SERVER_PORT=8080
REVERB_APP_ID=848245
REVERB_APP_KEY=hkybzbkdvdl1hsvtzuqk
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}"
VITE_REVERB_HOST="${REVERB_HOST}"
VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}"
+65
View File
@@ -0,0 +1,65 @@
name: Build and Push Docker Image
on:
workflow_run:
workflows: ["tests"]
types:
- completed
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
env:
RUNNER_TOOL_CACHE: /toolcache
REVERB_APP_KEY: ${{ secrets.REVERB_APP_KEY }}
REVERB_APP_SECRET: ${{ secrets.REVERB_APP_SECRET }}
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Get version
id: get_version
run: echo "::set-output name=version::$(cat VERSION)"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
registry: git.matthiasg.dev
username: ninluc
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Cache Docker layers (registry-backed)
uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
key: buildx-cache-${{ github.sha }}
restore-keys: |
buildx-cache-
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
file: Dockerfile
push: true
build-args: |
VITE_REVERB_APP_KEY=${{ secrets.REVERB_APP_KEY }}
VITE_REVERB_HOST=perceptron.matthiasg.dev
VITE_REVERB_PORT=443
VITE_REVERB_SCHEME=https
tags: git.matthiasg.dev/ninluc/perceptron-viewer/perceptron-viewer:latest, git.matthiasg.dev/ninluc/perceptron-viewer/perceptron-viewer:${{ steps.get_version.outputs.version }}
# notify:
# needs: build
# runs-on: ubuntu-latest
# steps:
# - name: Notify Watchtower
# run: "curl -i -X POST -H \"Authorization: Bearer ${{ secrets.WATCHTOWER_TOKEN }}\" https://watchtower.beermaker.matthiasg.dev/v1/update"
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
php-version: ['8.4', '8.5']
php-version: ['8.3']
steps:
- name: Checkout code
@@ -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)
+69
View File
@@ -0,0 +1,69 @@
# Composer dependencies
FROM composer:2 AS vendor
WORKDIR /usr/app
COPY . .
RUN composer install --no-dev --optimize-autoloader --no-interaction
# Build frontend assets
# Install node and npm
RUN apk add --no-cache nodejs npm
RUN npm install
ARG VITE_REVERB_APP_KEY
ARG VITE_REVERB_HOST
ARG VITE_REVERB_PORT=443
ARG VITE_REVERB_SCHEME=https
ENV VITE_REVERB_APP_KEY=$VITE_REVERB_APP_KEY \
VITE_REVERB_HOST=$VITE_REVERB_HOST \
VITE_REVERB_PORT=$VITE_REVERB_PORT \
VITE_REVERB_SCHEME=$VITE_REVERB_SCHEME
RUN npm run build
# Final PHP image
FROM dunglas/frankenphp:1.12.2-php8.3-alpine AS final
COPY docker/php.ini-production "$PHP_INI_DIR/php.ini"
# Install system dependencies
RUN apk add --no-cache \
bash \
libpng-dev \
libjpeg-turbo-dev \
freetype-dev \
oniguruma-dev \
zip \
unzip \
git \
curl \
libpq-dev \
php-pgsql
# Install PHP extensions
RUN docker-php-ext-install pdo pgsql pdo_pgsql mbstring exif pcntl bcmath gd
WORKDIR /app
# Copy app
COPY . .
RUN mv .env.docker .env
# Copy dependencies
COPY --from=vendor /usr/app/vendor ./vendor
# Copy built frontend
COPY --from=vendor /usr/app/public/build ./public/build
# Permissions
#RUN chown -R www-data:www-data /app
#RUN chown www-data:www-data /app/dockerEntryPoint.sh
RUN chmod +x /app/dockerEntryPoint.sh
HEALTHCHECK --interval=30s --timeout=30s --start-period=30s --retries=3 CMD curl -f http://127.0.0.1/up || exit 1
EXPOSE 80
EXPOSE 8080
CMD ["./dockerEntryPoint.sh"]
+1 -1
View File
@@ -1,4 +1,4 @@
# Perceptron and neuronal networks
# PerceptronViewer
Using Laravel and Vue JS
+1
View File
@@ -0,0 +1 @@
1.0.0
@@ -0,0 +1,9 @@
<?php
namespace App\Exceptions;
use RuntimeException;
class TrainingCancelledException extends RuntimeException
{
}
+75 -20
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;
@@ -37,12 +54,15 @@ class PerceptronController extends Controller
$learningRate = 0.015;
$maxIterations = 150;
break;
case 'gradientdescent':
case 'adaline':
$learningRate = 0.00003;
break;
case 'gradientdescent':
$learningRate = 0.00003;
$maxIterations = 300;
break;
case 'monolayer':
$learningRate = 0.03;
$learningRate = 0.001;
break;
case 'multilayer':
$learningRate = 0.8;
@@ -57,6 +77,7 @@ class PerceptronController extends Controller
'minError' => $minError,
'learningRate' => $learningRate,
'maxIterations' => $maxIterations,
'maxIterationsLimit' => config('perceptron.max_iterations'),
'maxDisplayedWeights' => config('perceptron.max_displayed_weights'),
]);
}
@@ -68,9 +89,14 @@ class PerceptronController extends Controller
$datasets = [];
foreach ($files as $file) {
if (pathinfo($file, PATHINFO_EXTENSION) === 'csv') {
if (str_starts_with($file, 'hidden')) {
continue;
}
$dataset = [];
$dataset['label'] = str_replace('.csv', '', $file);
$dataSetReader = new LinearOrderDataSetReader($dataSetsDirectory.'/'.$file);
$dataset['inputCount'] = count($dataSetReader->lines[0]) - 1;
$dataset['data'] = [];
switch (count($dataSetReader->lines[0])) {
case 3:
@@ -97,7 +123,7 @@ class PerceptronController extends Controller
}
switch ($dataset['label']) {
case 'logic_and_gradient':
case 'Classification_-_Porte_logique_ET_(linéairement_séparable)':
switch ($perceptronType) {
case 'gradientdescent':
$dataset['defaultLearningRate'] = 0.3;
@@ -107,9 +133,12 @@ class PerceptronController extends Controller
$dataset['defaultLearningRate'] = 0.05;
$dataset['defaultMinError'] = 0.125;
break;
case 'monolayer':
$dataset['defaultMinError'] = 0.001;
break;
}
break;
case 'logic_xor':
case 'Classification_-_Porte_logique_XOR_(non_linéairement_séparable)':
switch ($perceptronType) {
case 'multilayer':
$dataset['defaultLearningRate'] = 0.3;
@@ -120,7 +149,8 @@ class PerceptronController extends Controller
break;
}
break;
case 'table_2_9':
case 'Classification_-_Oblique_(linéairement_séparable)':
case 'Classification_-_Oblique_modifié_(non_linéairement_séparable)':
switch ($perceptronType) {
case 'simple':
$dataset['defaultLearningRate'] = 0.015;
@@ -128,27 +158,45 @@ class PerceptronController extends Controller
case 'gradientdescent':
case 'adaline':
$dataset['defaultLearningRate'] = 0.001;
$dataset['defaultMinError'] = 0.09;
$dataset['defaultMaxIterations'] = 2000;
break;
}
break;
case 'table_2_11':
case 'Régression_-_Oblique':
$dataset['defaultMinError'] = 0.02;
switch ($perceptronType) {
case 'gradientdescent':
case 'adaline':
$dataset['defaultMinError'] = 0.68;
$dataset['defaultMaxIterations'] = 100;
break;
case 'monolayer':
$dataset['defaultLearningRate'] = 0.0015;
break;
}
break;
case 'table_4_12':
case 'Classification_-_3_classes_(linéairement_séparable)':
switch ($perceptronType) {
case 'multilayer':
$dataset['defaultLearningRate'] = 0.8;
$dataset['defaultMinError'] = 0.001;
$dataset['defaultMaxIterations'] = 2000;
case 'monolayer':
$dataset['defaultLearningRate'] = 0.005;
$dataset['defaultMaxIterations'] = 100;
break;
}
break;
case 'table_4_17':
case 'Classification_-_Donut_(non_linéairement_séparable)':
switch ($perceptronType) {
case 'multilayer':
$dataset['defaultLearningRate'] = 0.8;
$dataset['defaultMinError'] = 0.0001;
$dataset['defaultMaxIterations'] = 2000;
$dataset['defaultHiddenLayers'] = 3;
$dataset['defaultHiddenLayersNeurons'] = 4;
break;
}
break;
case 'Régression_-_Vague':
$dataset['defaultMinError'] = 0.055;
switch ($perceptronType) {
case 'multilayer':
@@ -189,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;
@@ -205,19 +255,24 @@ 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 response()->json([
return back()->with('success', [
'message' => 'Training completed',
'execution_time' => microtime(true) - $startTime,
]);
+2 -2
View File
@@ -15,13 +15,13 @@ class RunPerceptronRequest extends FormRequest
{
return [
'type' => ['required', 'string', 'in:simple,gradientdescent,adaline,monolayer,multilayer'],
'dataset' => ['required', 'string', 'max:100', 'regex:/^[A-Za-z0-9_-]+$/'],
'dataset' => ['required', 'string', 'max:200'],
'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'],
'max_iterations' => ['required', 'integer', 'min:1', 'max:'.config('perceptron.max_iterations')],
'session_id' => ['required', 'string', 'max:100'],
'training_id' => ['required', 'string', 'max:100'],
];
@@ -2,13 +2,13 @@
namespace App\Models\NetworksTraining;
use App\Events\PerceptronTrainingEnded;
use App\Models\ActivationsFunctions;
use App\Models\Perceptrons\GradientDescentPerceptron;
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
{
@@ -27,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()));
}
@@ -84,7 +85,7 @@ class ADALINEPerceptronTraining extends NetworkTraining
{
$condition = $this->epochError <= $this->minError;
if ($condition === true) {
event(new PerceptronTrainingEnded('Le perceptron à atteint l\'erreur minimale', $this->sessionId, $this->trainingId));
$this->broadcastTrainingEnded('Le perceptron à atteint l\'erreur minimale');
}
return $condition;
@@ -2,13 +2,13 @@
namespace App\Models\NetworksTraining;
use App\Events\PerceptronTrainingEnded;
use App\Models\ActivationsFunctions;
use App\Models\Perceptrons\GradientDescentPerceptron;
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
{
@@ -27,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()));
}
@@ -80,7 +81,7 @@ class GradientDescentPerceptronTraining extends NetworkTraining
{
$condition = $this->epochError <= $this->minError;
if ($condition === true) {
event(new PerceptronTrainingEnded('Le perceptron à atteint l\'erreur minimale', $this->sessionId, $this->trainingId));
$this->broadcastTrainingEnded('Le perceptron à atteint l\'erreur minimale');
}
return $condition;
@@ -2,7 +2,6 @@
namespace App\Models\NetworksTraining;
use App\Events\PerceptronTrainingEnded;
use App\Models\ActivationsFunctions;
use App\Models\Perceptrons\GradientDescentPerceptron;
use App\Models\Perceptrons\NetworkPerceptron;
@@ -11,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
@@ -19,6 +19,8 @@ class MonoLayerPerceptronTraining extends NetworkTraining
private array $labels;
private bool $isRegression;
public ActivationsFunctions $activationFunction = ActivationsFunctions::LINEAR;
public ?ActivationsFunctions $presentationLayerActivationFunction = ActivationsFunctions::STEP;
@@ -34,13 +36,15 @@ 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(
$networkWeightsProvider->generate(
$datasetReader->getInputSize(),
$datasetReader->getOutputSize(),
$this->isRegression ? 1 : $datasetReader->getOutputSize(),
0, // No hidden layer
0, // No hidden layer neurons
),
@@ -103,7 +107,7 @@ class MonoLayerPerceptronTraining extends NetworkTraining
{
$condition = $this->epochError <= $this->minError;
if ($condition === true) {
event(new PerceptronTrainingEnded('Le perceptron à atteint l\'erreur minimale', $this->sessionId, $this->trainingId));
$this->broadcastTrainingEnded('Le perceptron à atteint l\'erreur minimale');
}
return $condition;
@@ -140,6 +144,10 @@ class MonoLayerPerceptronTraining extends NetworkTraining
private function getDesiredOutputFromCorrectOutput(float $correctOutput): array
{
if ($this->isRegression) {
return [$correctOutput];
}
$desiredOutput = array_fill(0, count($this->labels), -1);
$labelIndex = Arr::first(array_keys($this->labels), fn ($key) => $this->labels[$key] == $correctOutput);
if ($labelIndex !== null) {
@@ -2,7 +2,6 @@
namespace App\Models\NetworksTraining;
use App\Events\PerceptronTrainingEnded;
use App\Models\ActivationsFunctions;
use App\Models\Perceptrons\GradientDescentPerceptron;
use App\Models\Perceptrons\NetworkPerceptron;
@@ -12,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
@@ -38,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
@@ -116,7 +117,7 @@ class MultiLayerPerceptronTraining extends NetworkTraining
{
$condition = $this->epochError <= $this->minError;
if ($condition === true) {
event(new PerceptronTrainingEnded('Le perceptron à atteint l\'erreur minimale', $this->sessionId, $this->trainingId));
$this->broadcastTrainingEnded('Le perceptron à atteint l\'erreur minimale');
}
return $condition;
@@ -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;
@@ -33,7 +36,7 @@ abstract class NetworkTraining
protected function checkPassedMaxIterations(?float $finalError)
{
if ($this->epoch >= $this->maxEpochs) {
$message = 'Le nombre maximal d\'epoch a été atteint';
$message = 'Le nombre maximal d\'époques a été atteint';
if ($finalError) {
$message .= " avec une erreur finale de $finalError";
}
@@ -42,11 +45,26 @@ abstract class NetworkTraining
}
}
protected function broadcastTrainingEnded(string $reason): void
{
$this->iterationEventBuffer->flush();
event(new PerceptronTrainingEnded($reason, $this->sessionId, $this->trainingId));
}
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()));
}
@@ -8,6 +8,8 @@ class PerceptronLimitedEpochEventBuffer implements IPerceptronIterationEventBuff
{
private array $data = [];
private array $lastEpochData = [];
private ?int $activeEpoch = null;
private bool $shouldBroadcastEpoch = false;
@@ -22,14 +24,20 @@ class PerceptronLimitedEpochEventBuffer implements IPerceptronIterationEventBuff
public function flush(): void
{
if ($this->data === []) {
if ($this->data !== []) {
$this->flushSelectedData();
return;
}
if ($this->lastEpochData === [] || $this->shouldBroadcastEpoch) {
return;
}
$this->waitForBroadcastInterval();
event(new PerceptronTrainingIteration($this->data, $this->sessionId, $this->trainingId));
event(new PerceptronTrainingIteration($this->lastEpochData, $this->sessionId, $this->trainingId));
$this->lastBroadcastAt = microtime(true);
$this->data = [];
$this->lastEpochData = [];
}
public function addIteration(int $epoch, int $exampleIndex, float $error, array $synaptic_weights): void
@@ -42,22 +50,37 @@ class PerceptronLimitedEpochEventBuffer implements IPerceptronIterationEventBuff
];
if ($this->activeEpoch !== $epoch) {
$this->flush();
$this->flushSelectedData();
$this->lastEpochData = [];
$this->activeEpoch = $epoch;
$this->shouldBroadcastEpoch = $epoch === 1 || $epoch % $this->epochInterval === 0;
}
if (! $this->shouldBroadcastEpoch) {
$this->lastEpochData[] = $newData;
return;
}
$this->data[] = $newData;
if ($this->payloadExceedsLimit() || count($this->data) >= config('perceptron.broadcast_iteration_size')) {
$this->flush();
$this->flushSelectedData();
}
}
private function flushSelectedData(): void
{
if ($this->data === []) {
return;
}
$this->waitForBroadcastInterval();
event(new PerceptronTrainingIteration($this->data, $this->sessionId, $this->trainingId));
$this->lastBroadcastAt = microtime(true);
$this->data = [];
}
private function payloadExceedsLimit(): bool
{
return strlen(json_encode([
+3
View File
@@ -23,6 +23,9 @@ return Application::configure(basePath: dirname(__DIR__))
HandleInertiaRequests::class,
AddLinkHeadersForPreloadedAssets::class,
]);
$middleware->trustProxies(at: [
'*'
]);
})
->withExceptions(function (Exceptions $exceptions): void {
//
+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
+3 -6
View File
@@ -22,7 +22,7 @@ return [
/**
* Minimum time between training progress broadcasts, in milliseconds.
*/
'broadcast_minimum_interval_ms' => 150,
'broadcast_minimum_interval_ms' => 100,
/**
* Maximum number of weights for which all iteration weights are broadcast
@@ -30,9 +30,6 @@ return [
*/
'max_displayed_weights' => 5,
'run_inputs_validation' => [
'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',
]
'max_iterations' => 5000,
];
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+10
View File
@@ -0,0 +1,10 @@
#!/bin/sh
# Migrate the database
php ./artisan migrate --force || exit 1
# Cache
php ./artisan optimize:clear && php ./artisan optimize
# Start the server
exec frankenphp php-server --root=public
+190 -11
View File
@@ -6,15 +6,21 @@
"": {
"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",
"@vueuse/core": "^12.8.2",
"chart.js": "^4.5.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"katex": "^0.18.7",
"laravel-vite-plugin": "^2.0.0",
"lucide-vue-next": "^0.468.0",
"markdown-it": "^15.0.1",
"markdown-it-katex": "^2.0.3",
"radix-ui": "^1.4.3",
"reka-ui": "^2.9.2",
"reka-ui": "^2.10.4",
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.1",
"tw-animate-css": "^1.2.5",
@@ -856,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",
@@ -925,6 +936,15 @@
"integrity": "sha512-yZYIr1iwuCQ7LFI+GsJk9vacw1HWMp3ZlDlW0pdfz3zXyKeu4US7oH79KmQQ031L0cYaSyaUMo/Ha1D4BosKqw==",
"dev": true
},
"node_modules/@lucide/vue": {
"version": "1.46.0",
"resolved": "https://registry.npmjs.org/@lucide/vue/-/vue-1.46.0.tgz",
"integrity": "sha512-xp4mUamXKM/jYCve8TOcEcuikidFh3Jn9gs0THCeEKn/fAdeU0mKhPqk0fmIwn42fI4CwGLRWrLneLA69h8Nqw==",
"license": "ISC",
"peerDependencies": {
"vue": ">=3.0.1"
}
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.12",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
@@ -2978,6 +2998,29 @@
"node": ">= 20"
}
},
"node_modules/@tailwindcss/typography": {
"version": "0.5.20",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.20.tgz",
"integrity": "sha512-hwbzQuNUfcPvbegQFatVPl/MY/tcM9KLl963hQ5laJKPh81TEZ1+dNG9PirGvcaDBkp+BCshExAyKVPW91dozw==",
"dependencies": {
"postcss-selector-parser": "6.0.10"
},
"peerDependencies": {
"tailwindcss": ">=3.0.0 || >=4.0.0 || insiders"
}
},
"node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": {
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@tailwindcss/vite": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.1.tgz",
@@ -3830,6 +3873,7 @@
"version": "12.8.2",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.8.2.tgz",
"integrity": "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==",
"license": "MIT",
"dependencies": {
"@types/web-bluetooth": "^0.0.21",
"@vueuse/metadata": "12.8.2",
@@ -4300,6 +4344,14 @@
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "15.0.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz",
"integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==",
"engines": {
"node": ">=22.12.0"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -4362,7 +4414,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"dev": true,
"bin": {
"cssesc": "bin/cssesc"
},
@@ -4490,9 +4541,10 @@
}
},
"node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="
"version": "6.1.7",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz",
"integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
"license": "MIT"
},
"node_modules/delayed-stream": {
"version": "1.0.0",
@@ -6254,6 +6306,21 @@
"json5": "lib/cli.js"
}
},
"node_modules/katex": {
"version": "0.18.7",
"resolved": "https://registry.npmjs.org/katex/-/katex-0.18.7.tgz",
"integrity": "sha512-h+UCwkZ+4Jz8WQ7MLGfj7UVFrRCizGb912fwF4luGdYsC5paYG1vx+jy+KRcC/XkpjGva/P7nAWuxNnPzRvzHw==",
"funding": [
"https://opencollective.com/katex",
"https://github.com/sponsors/katex"
],
"dependencies": {
"commander": "^15.0.0"
},
"bin": {
"katex": "cli.js"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -6554,6 +6621,24 @@
"url": "https://opencollective.com/parcel"
}
},
"node_modules/linkify-it": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-6.1.0.tgz",
"integrity": "sha512-wJ/TwpSDTLepCrQoYWYIExIKg5Zchex2Nn5yk2mFnB+6PtdkHtyLx742md9csRjjOnGkKIS/RrbY7l8D6gT9Vw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/puzrin"
},
{
"type": "github",
"url": "https://github.com/sponsors/markdown-it"
}
],
"dependencies": {
"uc.micro": "^3.0.0"
}
},
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -6602,6 +6687,82 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/markdown-it": {
"version": "15.0.1",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-15.0.1.tgz",
"integrity": "sha512-9/7gE95FNPkfUWrjJIoHZza2iLmuJlPD0UNMxPi7bxUrbCR525YZY0r+zyfes0dZI5ZZ/uNIXUJca0pJvtw41g==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/puzrin"
},
{
"type": "github",
"url": "https://github.com/sponsors/markdown-it"
}
],
"dependencies": {
"argparse": "^3.0.0",
"entities": "^8.0.0",
"linkify-it": "^6.0.0",
"mdurl": "^2.1.0",
"punycode.js": "^2.3.1",
"uc.micro": "^3.0.0"
},
"bin": {
"markdown-it": "bin/markdown-it.mjs"
}
},
"node_modules/markdown-it-katex": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/markdown-it-katex/-/markdown-it-katex-2.0.3.tgz",
"integrity": "sha512-nUkkMtRWeg7OpdflamflE/Ho/pWl64Lk9wNBKOmaj33XkQdumhXAIYhI0WO03GeiycPCsxbmX536V5NEXpC3Ng==",
"dependencies": {
"katex": "^0.6.0"
}
},
"node_modules/markdown-it-katex/node_modules/katex": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/katex/-/katex-0.6.0.tgz",
"integrity": "sha512-rS4mY3SvHYg5LtQV6RBcK0if7ur6plyEukAOV+jGGPqFImuzu8fHL6M752iBmRGoUyF0bhZbAPoezehn7xYksA==",
"dependencies": {
"match-at": "^0.1.0"
},
"bin": {
"katex": "cli.js"
}
},
"node_modules/markdown-it/node_modules/argparse": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-3.0.1.tgz",
"integrity": "sha512-nM4mHF/KM1v59ZNKX7zfusQz5wUAxR511YG8Vo6TyiV4aqhu++rbJW4v04xsWhpSsHFj66flT8P7znVpyO20xQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/puzrin"
},
{
"type": "github",
"url": "https://github.com/sponsors/nodeca"
}
]
},
"node_modules/markdown-it/node_modules/entities": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz",
"integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==",
"engines": {
"node": ">=20.19.0"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/match-at": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/match-at/-/match-at-0.1.1.tgz",
"integrity": "sha512-h4Yd392z9mST+dzc+yjuybOGFNOZjmXIPKWjxBd1Bb23r4SmDOsk2NYCU2BMUBGbSpZqwVsZYNq26QS3xfaT3Q=="
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -6610,6 +6771,11 @@
"node": ">= 0.4"
}
},
"node_modules/mdurl": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz",
"integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg=="
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -7137,6 +7303,14 @@
"node": ">=6"
}
},
"node_modules/punycode.js": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
"engines": {
"node": ">=6"
}
},
"node_modules/pusher-js": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-8.4.0.tgz",
@@ -7386,9 +7560,10 @@
}
},
"node_modules/reka-ui": {
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.9.2.tgz",
"integrity": "sha512-/t4e6y1hcG+uDuRfpg6tbMz3uUEvRzNco6NeYTufoJeUghy5Iosxos5YL/p+ieAsid84sdMX9OrgDqpEuCJhBw==",
"version": "2.10.4",
"resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.10.4.tgz",
"integrity": "sha512-kbS5GAbkHkYj0EVKAg5ZPEPndhBjeUFSa1Aq5m5ftQwyHnB1v5tw8X6fcwfKH/ry3StPXblMWM1DW82MVS71cw==",
"license": "MIT",
"dependencies": {
"@floating-ui/dom": "^1.6.13",
"@floating-ui/vue": "^1.1.6",
@@ -7398,7 +7573,7 @@
"@vueuse/core": "^14.1.0",
"@vueuse/shared": "^14.1.0",
"aria-hidden": "^1.2.4",
"defu": "^6.1.4",
"defu": "^6.1.5",
"ohash": "^2.0.11"
},
"funding": {
@@ -8298,6 +8473,11 @@
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/uc.micro": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-3.0.0.tgz",
"integrity": "sha512-U3PppEkleoTnIfi8BozMx3yju3qc/L6SwqWo2Sw+54PX+PX0q9I+r1Um5HCmqD7n9VDX5/v3vQH/AjA6deDdtw=="
},
"node_modules/unbox-primitive": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
@@ -8417,8 +8597,7 @@
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/vee-validate": {
"version": "4.15.1",
+7 -1
View File
@@ -37,15 +37,21 @@
},
"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",
"@vueuse/core": "^12.8.2",
"chart.js": "^4.5.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"katex": "^0.18.7",
"laravel-vite-plugin": "^2.0.0",
"lucide-vue-next": "^0.468.0",
"markdown-it": "^15.0.1",
"markdown-it-katex": "^2.0.3",
"radix-ui": "^1.4.3",
"reka-ui": "^2.9.2",
"reka-ui": "^2.10.4",
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.1",
"tw-animate-css": "^1.2.5",
+72
View File
@@ -0,0 +1,72 @@
# ADAptative LINear Element
ADALINE est une variante de la descente du gradient
La descente du gradient attend d'avoir évalué chaque exemple du dataset avant de corriger les poids synaptiques et de calculer l'erreur quadratique moyenne. ADALINE rends le processus plus rapide en corrigeant les poids à chaque exemple, comme le Perceptron.
On peut considérer ADALINE comme un Perceptron avec la fonction linéaire comme fonction d'activation et une condition d'arrêt de l'entraînement qui utilise l'erreur quadratique moyenne.
## Structure d'ADALINE
Pas de changement par rapport à la structure de la Descente du gradient.
![Schéma général de la structure d'un neurone ADALINE](helptext/img/gradientDescentSchema.png)
## Fonctionnement de l'entraînement
En comparaison à la Descente du gradient, la correction des poids se fait à chaque évaluation d'un exemple, comme pour le Perceptron.
1. On initialise les poids synaptiques.
2. On commence l'entraînement : pour chaque exemple du dataset choisi, on évalue la sortie du Perceptron ($y$).
3. On fait le calcul de l'erreur (notée $e$), en soustrayant la sortie du Perceptron à la sortie attendue (le *label*).
4. On met à jour les poids synaptiques avec la même formule que le Perceptron : on ajoute à la valeur du poids l'erreur multipliée par la valeur de l'exemple assignée à l'entrée, multipliée par le **Taux d'apprentissage**. Ce qui donne : $w_i' = w_i + \eta \times (\text{label } - y) \times x_i$ pour tout poids synaptique $i$ où $w_i'$ est le poids synaptique corrigé et $x_i$ est l'entrée numéro $i$.
5. Une fois tous les exemples du dataset parcourus, on calcule l'erreur quadratique moyenne ($E$) avec les derniers poids synaptiques.
6. Si l'erreur quadratique moyenne est plus petite que l'**erreur minimale** ou que le nombre maximal d'époques a été atteint, l'entraînement est terminé. Sinon, on recommence à partir du point 2.
<div class="exercice">
Reprenons le dataset oblique afin de faire un test contrôle :
- <span>Dataset</span> : `Classification - Oblique (linéairement séparable)`
Puis cliquez sur "Lancer" tout en laissant les autres paramètres intacts.
Cliquez sur le bouton `Afficher uniquement l'erreur quadratique moyenne`, la ligne que l'on peut voir est moins "douce" et contient des "bosses" avec les mêmes paramètres que la descente du gradient. Cela est dû au fait que les corrections se font à chaque exemple du dataset. Le taux d'apprentissage devrait être mis un peu plus bas pour faire disparaître ces "bosses", mais ici elles sont encore petites et ne posent pas de problèmes.
</div>
## Ajustements du Taux d'apprentissage
La descente du gradient est plus lente que le Perceptron. Est-ce qu'ADALINE règle ce problème ?
<div class="exercice">
Reprenons le dataset oblique afin de pouvoir comparer les résultats, et augmentons un peu le taux d'apprentissage :
- <span>Dataset</span> : `Classification - Oblique (linéairement séparable)`
- <span>Taux d'apprentissage</span> : entre $0.002$ et $0.01$ (ex: $0.005$)
- (Optionnel) <span>Nombre maximum d'époques</span> : $1000$ (plus d'époques seront affichées)
Puis cliquez sur "Lancer" tout en laissant les autres paramètres intacts.
On peut voir que le nombre d'époques que l'entraînement a pris est bien plus petit : de 200 à 900 époques en fonction du taux d'apprentissage choisi, contre presque 2000 pour le test contrôle.
Cliquez sur le bouton `Afficher uniquement l'erreur quadratique moyenne`, la ligne que l'on peut observer est encore moins douce. Les "bosses" sont amplifiées à cause de la hausse du taux d'apprentissage.
</div>
### Avec un taux d'apprentissage trop haut
On a déjà vu avec la descente du gradient ce que faisait un taux d'apprentissage trop bas et trop haut. Mais comme on le voit, l'erreur quadratique moyenne du perceptron forme des bosses qui s'amplifient avec le taux d'apprentissage. Que se passe-t-il alors quand le paramètre est trop haut ?
<div class="exercice">
Reprenons le dataset oblique afin de pouvoir comparer les résultats, et augmentons le taux d'apprentissage à une valeur trop grande :
- <span>Dataset</span> : `Classification - Oblique (linéairement séparable)`
- <span>Taux d'apprentissage</span> : $0.023$
Puis cliquez sur "Lancer" tout en laissant les autres paramètres intacts.
Les erreurs vacillent et le neurone a du mal à converger.
Cliquez sur le bouton `Afficher uniquement l'erreur quadratique moyenne`, les bosses de la ligne sont trop amplifiées et l'entraînement se joue sur la chance de tomber sur le bon prochain exemple. Relancer l'entraînement plusieurs fois et vous aurez des nombres d'époque finale différents. Ce n'est bien sûr pas optimal sauf si l'on se sent chanceux.
</div>
Mettre une valeur de taux d'apprentissage encore plus haute va donner le même résultat que la descente du gradient : des valeurs de poids qui s'emballent dans des extrêmes.
+9
View File
@@ -0,0 +1,9 @@
<div class="exercice">
Reprenons le dataset oblique afin de pouvoir comparer les résultats, et diminuons l'erreur minimale :
- <span>Dataset</span> : `Classification - Oblique (linéairement séparable)`
- <span>Erreur minimale</span> : en dessous de $0.05$ ou même $0$
- (Optionnel) <span>Nombre maximum d'époques</span> : $4000$
Puis cliquez sur "Lancer" tout en laissant les autres paramètres intacts.
</div>
+137
View File
@@ -0,0 +1,137 @@
# La Descente du gradient
Comme on a pu le voir, le [Perceptron](/perceptron?type=simple) présente quelques limitations :
- Il est incapable de faire de la régression
- Il n'optimise pas sa solution, la limite de décision "colle" les points du dataset.
La descente du gradient essaye d'améliorer l'algorithme d'entraînement et rend les valeurs des entrées et des sorties réelles (des nombres à décimale, positifs, négatifs ou nuls).
La descente du gradient n'essaye plus de ne plus faire aucune erreur comme le Perceptron, mais à la place va essayer de **minimiser l'erreur quadratique moyenne** (d'avoir une erreur quadratique moyenne la plus petite possible). Cette méthode de minimisation de l'erreur est appelée *descente du gradient*. L'erreur quadratique moyenne est calculée comme suit : la somme de l'erreur du réseau sur tous les exemples du dataset au carré, le tout divisé par $2$. Cela revient à faire
$$E = \frac{1}{2} \sum^K_{k=1}{(e^{(k)})^2}$$
où :
- $E$ est l'erreur quadratique moyenne.
- $K$ le nombre d'exemples dans le dataset.
- $e^{(k)}$ l'erreur du réseau sur l'exemple $k$ du dataset. ($e = \text{ label } - y$)
## Structure de la Descente du gradient
La structure ne change pas par rapport au Perceptron simple, si ce n'est que la fonction d'activation est maintenant une droite $y = x$. Cela veut dire que la descente du gradient retourne des valeurs réelles non bornées.
![Schéma général de la structure d'un neurone avec la descente du gradient](helptext/img/gradientDescentSchema.png)
## Fonctionnement de l'entraînement
Par rapport au Perceptron, la correction des poids se fait après chaque époque, et la condition de fin d'entraînement s'établit sur l'erreur quadratique moyenne.
1. On initialise les poids synaptiques.
2. On commence l'entraînement : pour chaque exemple du dataset choisi, on évalue la sortie du Perceptron ($y$).
3. On fait le calcul de l'erreur (notée $e$), en soustrayant la sortie du Perceptron à la sortie attendue (le *label*). Comme l'entrée et la sortie de la descente du gradient sont des réels, ici l'erreur est plus précise que le perceptron qui avait une erreur de -1, 0 ou 1.
4. On calcule, pour chaque poids synaptique, un signal d'erreur simmilaire au perceptron. Le signal d'erreur est égal à l'erreur multipliée par la donnée de l'exemple assignée à cette l'entrée, multipliée par le Taux d'apprentissage. Ce signal est ajouté à une variable de correction (notée $Dw_1$, $Dw_2$, $Dw_3$, ...) qui garde la somme des signaux d'erreurs pour chaque poids synaptique. Cette variable de correction est utilisée car on ne modifie les poids synaptiques qu'à la fin de l'époque, quand tous les exemples ont été évalués.
5. Une fois tous les exemples du dataset parcourus, on actualise les poids synaptiques en appliquant leur variable de correction respectives.
6. On calcule l'erreur quadratique moyenne ($E$) avec les nouveaux poids.
7. Si l'erreur quadratique moyenne est plus petite que l'**erreur minimale** ou que le nombre maximal d'époques a été atteint, l'entraînement est terminé. Sinon, on recommence à partir du point 2. L'erreur minimale est un paramètre détaillé plus bas.
<div class="exercice">
Reprenons le dataset oblique utilisé avec le Perceptron afin de vérifier que la descente du gradient corrige bien ses limitations :
- <span>Dataset</span> : `Classification - Oblique (linéairement séparable)`
Puis cliquez sur "Lancer" tout en laissant les autres paramètres intacts.
On peut voir que la ligne de décision est au centre entre les deux classes, et qu'elle ne colle pas aux points. Toutefois un point négatif que l'on peut noter est qu'il est bien **plus lent**. Le nombre d'itérations est au-dessus de 1000 comparé à une centaine pour le perceptron.
</div>
## Qu'est-ce que l'Erreur minimale fait ?
L'erreur minimale est un paramètre choisi par le développeur, c'est le seuil que le réseau doit atteindre pour pouvoir terminer l'entraînement. Plus le seuil est bas, plus le réseau doit être précis avant de pouvoir arrêter l'entraînement.
### Avec une erreur minimale trop basse
L'on pourrait alors se dire de mettre l'erreur minimale à 0, pour avoir un modèle le plus précis possible.
<div class="exercice">
Reprenons le dataset oblique afin de pouvoir comparer les résultats, et diminuons l'erreur minimale :
- <span>Dataset</span> : `Classification - Oblique (linéairement séparable)`
- <span>Erreur minimale</span> : en dessous de $0.05$ ou même $0$
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})$ (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>
### Avec une erreur minimale trop haute
Alors que fait une erreur minimale mise trop haute par le développeur ?
<div class="exercice">
Reprenons le dataset oblique afin de pouvoir comparer les résultats, et augmentons l'erreur minimale :
- <span>Dataset</span> : `Classification - Oblique (linéairement séparable)`
- <span>Erreur minimale</span> : au-dessus de $0.15$ (ex: $0.2$)
Puis cliquez sur "Lancer" tout en laissant les autres paramètres intacts.
L'entraînement est très rapide, même un peu trop rapide... Le réseau ne donne pas une bonne solution, voire une solution sur laquelle l'entraînement du perceptron ne se serait pas arrêté.
</div>
### Comment choisir une bonne valeur pour l'erreur minimale ?
Une méthode consiste à mettre l'erreur minimale à 0, mettre le nombre maximum d'époques à une valeur très haute, et surveiller l'erreur quadratique moyenne : si elle stagne, le réseau ne fera plus beaucoup d'avancées sur un nombre raisonnable d'époques.
## Qu'est-ce que le Taux d'Apprentissage fait ?
Comme expliqué dans le processus d'entraînement du Perceptron et de la descente du gradient, le taux d'apprentissage est un paramètre permettant de contrôler avec quelle "force" les poids synaptiques se mettent à jour.
### Avec un Taux d'Apprentissage trop haut
<div class="exercice">
Reprenons le même dataset oblique que le test contrôle, mais en augmentant considérablement le Taux d'Apprentissage :
- <span>Dataset</span> : `Classification - Oblique (linéairement séparable)`
- <span>Taux d'apprentissage</span> : au dessus de $0.001$ (ex: $0.002$)
- (Optionnel) <span>Nombre maximum d'époques</span> : $200$
Puis cliquez sur "Lancer" tout en laissant les autres paramètres intacts.
Les valeurs des graphes et du tableau vont dans les extrêmes (même à NaN ("Not A Number")). À cause du taux d'apprentissage trop haut, à chaque correction des poids synaptiques, leurs valeurs ont augmentées de façon exponentielle. Créant un cycle vicieux.
</div>
### Avec un Taux d'Apprentissage trop bas
<div class="exercice">
Reprenons le même dataset oblique que le test contrôle, mais en diminuant considérablement le Taux d'Apprentissage :
- <span>Dataset</span> : `Classification - Oblique (linéairement séparable)`
- <span>Taux d'apprentissage</span> : en dessous de $0.0005$ (ex: $0.0001$)
- (Optionnel) <span>Nombre maximum d'époques</span> : $4000$
Puis cliquez sur "Lancer" tout en laissant les autres paramètres intacts.
L'entraînement est beaucoup plus long, le réseau atteint même peut-être le nombre maximum d'époques. Cela est dû à la correction des poids synaptiques qui est devenue plus petite à chaque époque. L'on pourrait enlever la limite du nombre maximum d'époques, attendre un peu plus longtemps, et avoir un résultat précis. Mais, le temps, c'est de l'argent. Faisons le parallèle avec OpenAI : l'entraînement d'un nouveau modèle coûte des millions de dollars en puissance de calcul et peut prendre des mois entiers. Si l'on réduit la vitesse à laquelle le modèle s'entraîne, la facture de l'entraînement peut vite être salée.
</div>
En note annexe : dans les frameworks actuels, cette variable est gérée automatiquement et je ne pense pas qu'elle pose vraiment un problème à OpenAI.
### Comment choisir une bonne valeur pour le taux d'apprentissage ?
À l'essai et erreur ! Commencer avec une valeur arbitraire et diminuer tant que les poids synaptiques partent dans des valeurs infiniment grandes. Augmenter dans le cas contraire jusqu'à trouver une bonne valeur.
## Test avec la régression
Comme dit dans l'[introduction](/), quand un réseau de neurones fait de la régression, il essaye de créer la fonction mathématique qui épouse au mieux le nuage de données du dataset. Une des limitations du perceptron est le fait qu'il est incapable de faire de la régression. Grâce à ses entrées et sorties réelles, la descente du gradient est capable de faire de la régression.
<div class="exercice">
Testons la régression avec un dataset simple :
- <span>Dataset</span> : `Régression - Oblique`
Une fois sélectionné, on peut voir que le nuage de points du dataset forme presque une droite oblique. Imaginer une droite qui traverse au mieux tous les points, c'est-à-dire une droite qui est le plus proche possible de tous les points en même temps.
Cliquez sur "Lancer" tout en laissant les autres paramètres intacts.
On peut constater que la droite se dirige rapidement au bon endroit, le reste des époques ne font que du peaufinage. La descente du gradient essaye de minimiser l'erreur sans essayer de n'en faire aucune, ce qui est parfait comme avec un dataset comme celui-ci, il est impossible de relier tous les points avec une droite, et donc impossible de ne pas faire d'erreur.
</div>
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

+96
View File
@@ -0,0 +1,96 @@
# Perceptron Viewer
Cet outil vous permet d'expérimenter avec les réseaux de neurones et les perceptrons. Vous pouvez ajuster les paramètres et observer l'apprentissage en temps réel.
## Contexte
Ce projet a été d'abord créé pour un cours de réseaux de neurones artificiels que j'ai suivi durant mon master en Architecture des Systèmes Informatiques à la Haute École de la Province de Liège. Il s'agit ici d'une version plus conviviale, documentée et optimisée. Cependant le projet n'est pas très mobile friendly et je conseille l'utilisation d'un écran horizontal. 📵
Je conseille de finir de lire les prochains chapitres qui traitent des bases des réseaux de neurones et qui présentent l'interface. Je conseille ensuite d'utiliser les onglets au-dessus de la page dans l'ordre indiqué. Chaque onglet présente une évolution des réseaux de neurones dans l'ordre chronologique des découvertes et des améliorations faites.
## Introduction aux Réseaux de Neurones Artificiels
Le but principal d'un réseau de neurones artificiels est de **reproduire une fonction mathématique complexe**. En résumé, un réseau de neurones est conçu pour apprendre des relations complexes entre des données d'entrée et des données de sortie (la prédiction souhaitée). Il apprend à faire correspondre une entrée $X$ à une sortie $Y$ via une série de calculs pondérés et d'activations.
### Premières Découvertes des neurones artificiels
Les bases des réseaux de neurones remontent à la modélisation biologique du cerveau. Les premières tentatives par un neuropsychiatre et logicien vers 1943 visaient à créer des modèles simplifiés pour comprendre comment le cerveau traite l'information. Il s'agissait de **simuler la capacité du cerveau à apprendre et à prendre des décisions à partir de données brutes**, ce qui était essentiel pour comprendre la cognition et le traitement du *signal*.
Le résultat qui nous intéresse de cette recherche est l'"Electronic Brain", très semblable au [Perceptron](/perceptron?type=simple) que l'on va voir au premier onglet. Il transforme les données d'entrée ($X$) pour en sortie donner une décision ($Y$)
Exemple d'Electronic Brain à 3 entrées : ![Exemple d'Electronic Brain](helptext/img/electronicBrain.png)
### Le Machine Learning
Les neurones artificiels sont la structure "intelligente" du réseau. Cependant, il faut d'abord l'entraîner afin de déterminer les paramètres qui rendent ce réseau intelligent. C'est là que rentre en jeu le Machine Learning (souvent abrévié "ML"). On distingue principalement deux grandes catégories :
#### L'Apprentissage Supervisé
Des données d'entraînement sont données au modèle, appelées *exemples*. À chaque exemple est associée une bonne réponse appelée *tag* (ou *étiquette* en français).
Pendant l'apprentissage, pour chaque exemple, le réseau de neurones est consulté. Si la sortie du réseau ($Y$) n'est pas égale à la réponse attendue (le *tag* de l'exemple), les paramètres du réseau sont modifiés. Enfin, l'on passe à l'exemple suivant.
L'apprentissage supervisé demande donc d'étiqueter (ou "tagger") chaque exemple avec une réponse attendue précise et fiable. À défaut, le réseau de neurones pourrait manquer de précision et de fiabilité.
#### L'Apprentissage Non Supervisé
L'apprentissage non supervisé n'utilise pas de *tag* dans les données. À la place, il utilise des méthodes d'analyse de données mathématiques comme l'[ACP](https://notes.matthiasg.dev/#/page/school%2Fexploration%20de%20donn%C3%A9es%2F1.%20acp).
Cet apprentissage est utilisé pour trouver des pattern dans des données.
Tous les réseaux présentés ici utilisent l'**Apprentissage Supervisé**. L'apprentissage non supervisé ne sera donc pas vu plus en détail.
### Types de problèmes à résoudre
Les réseaux de neurones peuvent résoudre plusieurs types de problèmes. Nous allons ici en voir deux :
#### La Régression
Le réseau de neurones estime la fonction mathématique continue d'une suite ou d'un nuage de points, afin par exemple de pouvoir prédire les données suivantes.
Exemple : à partir des températures des cinq dernières années, entraîner un réseau de neurones pour qu'il reproduise la fonction mathématique qui se rapproche le plus de tous les points. Une fois entraîné, le réseau est capable d'estimer les prochaines températures.
![Exemple de régression](helptext/img/regressionExample.png)
#### La classification
Le réseau de neurones va attribuer la catégorie la plus probable à chaque exemple. Ces catégories sont appelées *classes* en mathématique. Un réseau de neurones peut classer les données dans autant de classes que nécessaire, mais le réseau doit être adapté en conséquence au niveau de la *couche de sortie* que l'on verra dans les réseaux multicouches.
Exemples : détection d'objets dans une image, déterminer si un e-mail est un spam ou non, déterminer l'état de santé d'un patient à partir de ses données vitales, ...
![Exemple de classification](helptext/img/classificationExample.png)
Chaque point contenu dans les zones sera catégorisé par le réseau de neurones dans la classe correspondante.
### [Quand la technologie copie la nature](https://fr.wikipedia.org/wiki/Biomim%C3%A9tisme)
Les neurones artificiels sont très semblables aux neurones biologiques. Un neurone biologique est une unité de traitement, capable de recevoir des signaux, de les traiter et de transmettre des informations via des connexions appelées *synapses*.
### Anatomie du Neurone Artificiel
Un neurone artificiel (ou une couche neuronale) est composé de plusieurs éléments interconnectés qui collaborent pour produire une sortie. Voici les composants clés :
![Schéma général d'un neurone](helptext/img/neuronSchema.png)
- **Entrée ($x_1$, $x_2$, $x_3$, ...)** : Les données brutes des exemples donnés au réseau (ex : la température en janvier, février, mars, etc.).
- **Poids Synaptique ($w_1$, $w_2$, $w_3$, ...)** : Un coefficient numérique qui détermine l'importance de chaque entrée pour la prochaine étape. Ce sont les paramètres que le réseau modifie quand il "apprend".
- **Biais ou Entrée Fictive ($w_0$)** : Une poids ajouté qui permet de déplacer la fonction d'activation, aidant le modèle à mieux s'ajuster aux données. Ce poids est toujours lié à une entrée constante avec comme valeur $1$.
- **Potentiel ($p$)** : La somme pondérée de toutes les entrées et des biais : $p = (\sum w_i x_i) + w_0$.
- **Fonction d'Activation ($f(x)$)** : Une fonction non linéaire qui va borner la valeur. Cette fonction est choisie par le développeur ou imposée par le type de neurone, comme on va le voir par exemple pour [ADALINE](perceptron?type=adaline).
- **Sortie ($y$)** : Le résultat final du neurone, la sortie du réseau, ce que le réseau a déterminé à partir de l'exemple.
## Autre vocabulaire
- **Dataset** : collections d'exemples étiquetté ayant la même structure et traitant du même sujet. Exemple : un dataset contenant la température enregistrée pour chaque jour des cinq dernières années. Pour chaque exemple on a donc le jour et la température.
## Présentation de Perceptron Viewer
Perceptron Viewer permet de modifier les paramètres d'entraînement, d'entraîner des réseaux de neurones et perceptrons sur un serveur, et de visualiser en direct les erreurs, les zones décisionnelles et les prédictions du réseau.
### Présentation de l'interface
![Présentation globale de l'interface](helptext/img/globalInterfacePresentation.png)
La configuration de l'entraînement et les graphes seront expliqués plus en détail dans l'onglet [Perceptron](/perceptron?type=simple).
---
Je vous invite dès à présent à vous rendre sur l'[onglet 1 : Perceptron Simple](/perceptron?type=simple).
+51
View File
@@ -0,0 +1,51 @@
# Le Réseau Monocouche
Jusqu'ici, nous avons vu des réseaux avec un seul neurone. Une des limites d'un tel réseau est qu'**un seul neurone ne peut séparer que deux classes**. En effet, un neurone ne peut pas donner simultanément l'appartenance ou non à plusieurs classes.
Pour séparer des données de plusieurs classes, il devient nécessaire d'utiliser plusieurs neurones, chacun étant spécialisé pour détecter l'appartenance ou non à une classe spécifique.
Exemples :
- Pour séparer 3 classes, il faut au moins 3 neurones.
- Pour séparer 2 classes, un seul neurone suffit (si $y > 0.5$ alors classe A, sinon classe B).
C'est exactement ce qu'un réseau monocouche fait : il permet d'avoir une quantité infinie de neurones. Le nombre de neurones est contrôlé automatiquement par PerceptronViewer en fonction du nombre de classes différentes dans le dataset.
<div class="exercice">
Reprenons le dataset oblique afin de pouvoir comparer les résultats avec ceux d'un réseau à un neurone :
- <span>Dataset</span> : `Classification - Oblique (linéairement séparable)`
Puis cliquez sur "Lancer" tout en laissant les autres paramètres intacts.
On peut voir maintenant des zones apparaître sur le graphe de décision. Chacune de ces zones représente la ligne de décision d'un neurone : si un point se trouve dans une de ces zones, il sera considéré comme faisant partie de la classe correspondante.
</div>
## Structure du réseau monocouche
Le réseau monocouche permet de choisir quel type de neurone utiliser. Dans le cadre de PerceptronViewer, des neurones ADALINE ont été utilisés.
Afin de clarifier les schémas de réseaux complexes, les perceptrons sont représentés par un cercle.
![Schéma général de la structure d'un réseau de neurones monocouche](helptext/img/monolayerSchema.png)
Chaque neurone est connecté à toutes les entrées et la valeur de leur sortie va déterminer la classe à laquelle l'exemple appartient.
## Fonctionnement de l'entraînement
L'entraînement va dépendre du type de neurone utilisé. Globalement, deux approches principales existent :
1. Entraîner tous les neurones simultanément : au moment de la correction des poids synaptiques, on parcourt tous les neurones.
2. Entraîner les neurones séparément : considérer les neurones comme des réseaux séparés. On entraîne le premier réseau, puis on passe au suivant.
La méthode d'entraînement utilisée dans PerceptronViewer est la première car elle se rapproche de l'entraînement du réseau multicouches, ce qui évite d'implémenter des méthodes différentes.
<div class="exercice">
Exploitons l'avantage du réseau monocouche en utilisant un dataset composé de 3 classes :
- <span>Dataset</span> : `Classification - 3 classes (linéairement séparable)`
Puis cliquez sur "Lancer" tout en laissant les autres paramètres intacts.
On obtient une séparation nette entre les 3 classes qui est assez bien généralisée (si l'on ajoute un nouveau point, il sera dans la classe la plus proche)
</div>
+78
View File
@@ -0,0 +1,78 @@
# Le Réseau Multicouches
> Et les datasets non linéairement séparables dans tout ça ?
Jusqu'à présent, nous avons utilisé que des datasets linéairement séparables, pour la simple raison que les réseaux à une seule couche ne produisent que des frontières de décision linéaires (des droites).
Pour séparer des classes de manière non linéaire, il est nécessaire d'utiliser des fonctions non linéaires au sein du réseau. C'est là que les réseaux multicouches rentrent en jeu : utiliser plusieurs couches permet d'avoir des frontières de décision non linéaires.
## Structure du réseau monocouche
Par rapport au réseau monocouche, on rajoute des couches dans lesquelles tous les neurones sont interconnectés aux neurones de la couche suivante.
![Schéma général de la structure d'un réseau de neurones multicouches](helptext/img/multilayerSchema.png)
On distingue 3 types de couches :
- La couche d'entrée : ne contient pas de neurones à proprement parler car ils n'effectuent aucun traitement sur les données d'entrées.
- Les couches cachées : chaque couche contient des neurones qui vont "mâcher" les données pour les couches suivantes. Dans PerceptronViewer, le nombre de neurones par couches cachées peut être choisi avec le paramètre **Nombre de neurones par couche cachée** et le nombre de couches cachées peut être choisi avec le paramètre **Nombre de couches cachées**.
- La couche de sortie : contient les neurones finaux qui vont transformer les données de sortie de la couche cachée en données utilisables (prédiction ou classification).
## Fonctionnement de l'entraînement
L'entraînement d'un réseau multicouche demande plus de technique à cause de la présence de plusieurs couches.
Voici le fonctionnement de l'entraînement dans les grandes lignes :
1. On initialise les poids synaptiques.
2. On commence l'entraînement : pour chaque exemple du dataset choisi, on évalue la sortie du réseau avec la *propagation avant* (forward propagation en anglais) : les données passent de la couche d'entrée, traversent les couches cachées et arrivent enfin à la couche de sortie. Pour chaque couche, l'entrée de chaque neurone correspond à la sortie des neurones de la couche précédente.
3. On calcule l'erreur du réseau sur cet exemple en utilisant la *rétropropagation* (backpropagation en anglais) : on calcule le signal d'erreur de la couche de sortie et on le propage à travers les couches jusqu'à la couche d'entrée.
4. On met à jour les poids synaptiques en fonction du signal d'erreur.
Je ne détaillerai pas ici la rétropropagation et la mise à jour des poids synaptiques. Cependant, si vous êtes intéressés, vous pouvez trouver les détails ici : [Wikipédia - Apprentissage du perceptron multicouche](https://fr.wikipedia.org/wiki/Perceptron_multicouche#Apprentissage).
<div class="exercice">
Exploitons l'avantage du réseau multicouche avec un dataset non linéairement séparable :
- <span>Dataset</span> : `Classification - Donut (non linéairement séparable)`
Puis cliquez sur "Lancer" tout en laissant les autres paramètres intacts.
Le réseau commence avec une zone uniforme qui couvre tout le graphe, ensuite, progressivement, crée une forme ovale qui englobe la classe du milieu du donut. Le reste des époques est passé à peaufiner la forme pour qu'elle englobe au mieux tous les exemples.
</div>
## Qu'est-ce que le nombre de couches et de neurones par couche fait ?
Ces deux paramètres sont très importants car ils vont déterminer la précision et le temps d'entraînement du réseau. Il n'y a pas réellement de formule magique, les bonnes valeurs sont obtenues par essai et erreur. Voici tout de même ce que va changer chaque paramètre et comment mieux choisir les valeurs.
### Quels sont les effets
Augmenter les paramètres va augmenter le nombre total de neurones. Plus il y a de neurones, plus la ligne de décision peut être complexe. Une ligne plus complexe va pouvoir dessiner les contours plus précisément et suivre des courbes complexes.
Il faut un minimum de nombre total de couches en fonction du dataset :
- 1 seule couche : la ligne de décision est une droite, comme les perceptrons et le réseau monocouche vu précédemment.
- 2 couches : la ligne de décision peux courber et modéliser des formes non linéaires (résout des datasets non linéairement séparables).
- +2 couches : la ligne de décision a la possibilité d'être de plus en plus complexe, plus le nombre de couches est haut.
Il est recommandé de ne pas avoir plus de neurones par couche qu'il y a d'exemples dans le dataset. Sinon le modèle risque d'apprendre par cœur le dataset en retenant un exemple par neurone. Ce problème est appelé **surapprentissage**, il apparaît quand le modèle est tellement bon sur le dataset, qu'il est incapable de généraliser.
Trop peu de neurones empêche le réseau de pouvoir faire le contour des classes et de devoir faire des erreurs.
## La régression avec un réseau multicouches
La régression peut également demander plus de couches de neurones pour pouvoir épouser des formes complexes.
<div class="exercice">
Essayons cela avec un dataset de régression trop complexe pour les réseaux à une seule couche :
- <span>Dataset</span> : `Régression - Vague`
Puis cliquez sur "Lancer" tout en laissant les autres paramètres intacts.
Là où un réseau monocouche essaierait de trouver la moyenne des points avec une droite, le réseau multicouches est capable d'épouser les formes complexes du nuage.
</div>
---
Ici s'achève la théorie. Quelques datasets n'ont pas été utilisés comme `Classification - Oblique modifié (non linéairement séparable)`, qui contient quelques points ajoutés ou dont la classe a été inversée (erreur d'encodage ? Anomalies dans la récolte des données ? Valeur anormale ? À vous de voir). Ce dataset est intéressant à faire avec tous les types de perceptrons et réseaux car il montre le comportement de chacun face à un dataset non linéairement séparable mais toujours réalisable.
+80
View File
@@ -0,0 +1,80 @@
# Perceptron Simple
Un perceptron est un neurone artificiel avec un mécanisme d'apprentissage supervisé.
Découvert par Frank Rosenblatt en simulant le neurone artificiel sur une machine IBM 704
Le nombre d'entrées du Perceptron ($x_1$, $x_2$, $x_3$, ...) peut être infini, mais le Perceptron n'aura toujours qu'une seule sortie ($y$). Étant donné que le perceptron ne contient qu'un seul neurone, il n'est capable de sortir qu'une réponse binaire, ce qui veut dire qu'il ne peut faire que de la classification à $2$ classes.
## Structure du Perceptron
Structure classique d'un neurone, avec l'utilisation de la fonction en escalier qui va limiter les valeurs possibles de sortie à $0$ ou $1$.
![Schéma général de la structure d'un perceptron simple](helptext/img/simplePerceptronSchema.png)
Voici le procédé qu'un Perceptron utilise pour transformer un exemple d'entrée ($X$) en signal de sortie ($Y$) :
1. On assigne chaque donnée de l'exemple du dataset à une entrée ($x_1$, $x_2$, $x_3$, ...). Dans notre cas, on utilise des points dans un espace 2D, donc $x_1 = \text{ Coordonnee x du point d'exemple}$ et $x_2 = \text{ Coordonnee y du point d'exemple}$ pour la classification.
2. On multiplie chaque entrée par le poids synaptique correspondant ($x_1 \times w_1$, $x_2 \times w_2$, ...), sans oublier l'entrée fictive ($1 \times w_0$).
3. On additionne le tout ($(1 \times w_1) + (x_1 \times w_1) + (x_2 \times w_2) + \cdots$), ce qui donne le *potentiel* ($p$).
4. On passe le potentiel dans la fonction d'activation qui, dans le cas du Perceptron, est la fonction en escalier. Cela revient à faire : si $p < 0$ alors le Perceptron retourne $0$, sinon le Perceptron retourne $1$. Le résultat du Perceptron est le signal $y$.
## Fonctionnement de l'entraînement
1. On initialise les poids synaptiques. Il existe plusieurs manières de les initialiser, ici PerceptronViewer permet de choisir entre "zeros" et "random" (aléatoire). Il n'y a pas beaucoup de différence entre les deux méthodes, si ce n'est qu'avec l'initialisation à $0$, l'on peux reproduire le comportement de l'entraînement. Cette reproductibilité permet de voir directement l'impact de modifications des autres paramètres d'entraînement.
2. On commence l'entraînement : pour chaque exemple du dataset choisis, on évalue la sortie du Perceptron ($y$), avec le procédé vu plus haut. Le passage sur tous les exemples du dataset s'appelle une **époque** ("epoch" en anglais).
3. On fait le calcul de l'erreur (notée $e$), en soustrayant la sortie du Perceptron à la sortie attendue (le *label*). Cela revient à $e = \text{ label } - y$. Si le perceptron a bien estimé la sortie, alors l'erreur est de $0$. Dans le cas contraire où le Perceptron n'a pas bien estimé la sortie, l'erreur est de $1$ ou $-1$.
4. Si l'erreur n'est pas nulle ($\neq 0$), alors on *corrige les poids synaptiques* du Perceptron ($w_0$, $w_1$, $w_2$, ...) comme suit pour chaque poids synaptique :
- Ajouter à la valeur du poids l'erreur multipliée par la valeur de l'exemple assignée à l'entrée, multipliée par une variable nommée **Taux d'apprentissage** (notée $\eta$). Ce qui donne : $w_i' = w_i + \eta \times (\text{label } - y) \times x_i$ pour tout poids synaptique $i$ où $w_i'$ est le poids synaptique corrigé et $x_i$ est l'entrée numéro $i$. Le taux d'apprentissage est défini par le développeur et est un paramètre très important comme on le verra par la suite.
5. Une fois tous les exemples du dataset parcourus. Si le Perceptron a commis une erreur ($e \neq 0$) pour un des exemples de ce dataset, alors l'entraînement n'est pas terminé et l'on recommence à partir du point 2. Si le Perceptron n'a pas commis d'erreur, alors l'entraînement est terminé.
Afin d'éviter que le Perceptron ne s'entraîne sans s'arrêter pour les datasets impossibles, un paramètre **Nombre maximum d'époques** a été ajouté. L'entraînement s'arrêtera si le nombre d'époques maximal a été atteint.
<div class="exercice">
Il est temps de tester ! Essayez de lancer un entraînement avec :
- <span>Dataset</span> : `Classification - Porte logique ET (linéairement séparable)`
Puis cliquez sur "Lancer" tout en laissant les autres paramètres intacts. Le Perceptron devrait vite converger et trouver une solution (de 3 à 8 époques d'après mes tests).
Le graphe du dessus montre le dataset avec chaque donnée représentée comme un point ayant la couleur de la classe correspondante. Dans le cas du dataset de la porte logique ET, il n'y a qu'un seul point faisant partie de la classe 1.
⚠️ Le nommage des classes est arbitraire et ne représente pas l'étiquette. La ligne de décision (en blanc) représente la bordure entre la zone où le Perceptron va considérer le point comme faisant partie de la classe A ou B. Comme on peut le voir, la ligne de décision est très proche des points, c'est une des limites dont nous parlerons plus bas.
Le graphe du dessous montre les erreurs faites par le perceptron, chaque bâton représente une époque, avec chaque bloc de couleur représentant l'erreur pour l'exemple correspondant. Le graphe contient aussi une ligne de l'**Erreur quadratique moyenne de l'époque** qui est une métrique utilisée pour examiner l'évolution de l'erreur du réseau de neurones sur le dataset à chaque époque. Comme on va le voir plus loin, cette métrique permet de mieux comprendre ce qu'il se passe durant l'entraînement et de juger les modifications nécessaires. Pour l'instant, on peut voir que cette métrique tombe à $0$ à la dernière époque, ce qui montre que le Perceptron ne fait plus d'erreurs sur le dataset et a donc terminé son entraînement.
</div>
## Comment choisir un dataset ?
Comme vous pouvez le voir dans la liste déroulante "Dataset", il en existe plusieurs types. Voici leurs significations :
### Classification VS Régression
Comme écrit dans l'introduction, la classification et la régression sont deux types de problèmes que les réseaux de neurones que nous voyons ici peuvent résoudre. Comme tous les datasets contiennent des points en 2D, cela veux dire :
- Pour la classification, le Perceptron va avoir 2 entrées ($x_1 = \text{ Coordonnee x du point d'exemple}$ et $x_2 = \text{ Coordonnee y du point d'exemple}$) et une sortie qui indique si le point fait partie d'une classe A ou B.
- Pour la régression, le Perceptron va avoir 1 entrée ($x_1 = \text{ Coordonnee x du point d'exemple}$) et la sortie représentera la coordonnée y du point d'exemple puisqu'il n'y a qu'une seule classe dans un dataset de régression.
### Classification : linéairement séparable ou non linéairement séparable
La notion de séparabilité linéaire dans la classification fait référence à la capacité d'un modèle à séparer les classes par un plan (ou une frontière) simple. Si les données sont linéairement séparables, cela veut dire qu'elles peuvent être séparées à l'aide d'une fonction mathématique linéaire (une droite).
## Limitations du Perceptron
Étant donné que sa sortie est binaire, le Perceptron présente deux limitations fondamentales :
- Il est incapable de faire de la régression. Car il s'arrête dès qu'il ne fait plus d'erreurs, mais avec une seule classe, il est impossible de faire une erreur.
- Il est incapable de faire de la classification à plus de deux classes différentes.
La limite de décision du Perceptron est souvent collée aux points. Une solution parfaite mettrait la limite à égale distance entre les points des deux classes. Cele est dû à l'entraînement qui s'arrête au moment où il n'y a plus d'erreurs, sans chercher à "optimiser" ou "généraliser" la solution.
<div class="exercice">
Pour voir le perceptron qui colle les points, essayez de lancer un entraînement avec :
- <span>Dataset</span> : `Classification - Oblique (linéairement séparable)`
L'on peut voir que l'entraînement dure déjà plus longtemps car il faut que le Perceptron trouve une pente et une hauteur qui fait passer la droite de décision dans un "trou" plus étroit. Mais l'on voit également que le Perceptron va coller les points à l'entrée et à la sortie de ce "trou".
</div>
Ces limitations ont été éliminées par [la descente du gradient](/perceptron?type=gradientdescent) qui utilise des entrées et des sorties réelles.
+15 -1
View File
@@ -1,10 +1,10 @@
@import 'tailwindcss';
@plugin "@tailwindcss/typography";
@import 'tw-animate-css';
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
@source '../../storage/framework/views/*.php';
@custom-variant dark (&:is(.dark *));
@theme inline {
@@ -170,3 +170,17 @@
@apply bg-background text-foreground;
}
}
@layer components {
.quill-content .ql-align-center { text-align: center; }
.quill-content .ql-align-right { text-align: right; }
.quill-content .ql-align-justify { text-align: justify; }
.quill-content .ql-size-small { font-size: 0.875rem; }
.quill-content .ql-size-large { font-size: 1.25rem; }
.quill-content .ql-size-huge { font-size: 1.5rem; }
.quill-content .ql-indent-1 { padding-left: 1.5rem; }
.quill-content .ql-indent-2 { padding-left: 3rem; }
.quill-content .ql-indent-3 { padding-left: 4.5rem; }
}
+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: {
+2 -4
View File
@@ -1,13 +1,11 @@
<script setup lang="ts">
import { Monitor, Moon, Sun } from 'lucide-vue-next';
import { Moon } from 'lucide-vue-next';
import { useAppearance } from '@/composables/useAppearance';
const { appearance, updateAppearance } = useAppearance();
const tabs = [
{ value: 'light', Icon: Sun, label: 'Light' },
{ value: 'dark', Icon: Moon, label: 'Dark' },
{ value: 'system', Icon: Monitor, label: 'System' },
] as const;
</script>
@@ -18,7 +16,7 @@ const tabs = [
<button
v-for="{ value, Icon, label } in tabs"
:key="value"
@click="updateAppearance(value)"
@click="updateAppearance()"
:class="[
'flex items-center rounded-md px-3.5 py-1.5 transition-colors',
appearance === value
+1 -1
View File
@@ -1,7 +1,6 @@
<script setup lang="ts">
import { Form } from '@inertiajs/vue3';
import { useTemplateRef } from 'vue';
import ProfileController from '@/actions/App/Http/Controllers/Settings/ProfileController';
import Heading from '@/components/Heading.vue';
import InputError from '@/components/InputError.vue';
import { Button } from '@/components/ui/button';
@@ -17,6 +16,7 @@ import {
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import ProfileController from '@/actions/App/Http/Controllers/Settings/ProfileController';
const passwordInput = useTemplateRef('passwordInput');
</script>
+96
View File
@@ -0,0 +1,96 @@
<script lang="ts" setup>
import MarkdownIt from 'markdown-it';
import markdownItKatex from 'markdown-it-katex';
import { ref, onMounted } from 'vue';
import 'katex/dist/katex.min.css';
const props = defineProps<{
markdownFileName: string;
}>();
const emit = defineEmits<{
loaded: [];
}>();
const html = ref<string>('');
const title = ref<string>('');
onMounted(() => {
const fullUrl = `/helptext/${props.markdownFileName}.md`;
fetch(fullUrl)
.then((res) => {
if (!res.ok) throw new Error('Failed to load file');
return res.text();
})
.then((markdown) => {
const md = new MarkdownIt({
html: true,
linkify: true,
typographer: true,
});
md.use(markdownItKatex);
md.renderer.rules.link_open = (
tokens,
index,
options,
env,
self,
) => {
const token = tokens[index];
token.attrSet('target', '_blank');
token.attrSet('rel', 'noopener noreferrer');
return self.renderToken(tokens, index, options);
};
html.value = md.render(markdown);
const firstHeading = html.value.match(/<h[1-6]>(.*?)<\/h[1-6]>/i);
title.value = firstHeading?.[1].trim() || '';
html.value = firstHeading
? html.value.replace(firstHeading[0], '')
: html.value;
emit('loaded');
})
.catch((err) => {
console.error('Could not load help file:', err);
html.value = '';
title.value = '';
});
});
</script>
<template>
<div class="p-6 w-full">
<h1 class="b-4 mb-7 text-4xl font-semibold">{{ title }}</h1>
<div
v-html="html"
class="max-w-none block prose prose-lg text-justify prose-headings:text-left dark:prose-invert prose-code:before:hidden prose-code:after:hidden"
role="region"
aria-label="Help text"
></div>
</div>
</template>
<style scoped>
.prose :deep(.katex) {
white-space: nowrap;
}
.prose :deep(.katex .vlist) {
font-size: 0.7em;
line-height: 0em;
}
</style>
<style>
/* Custom styles for the help text */
.exercice {
padding: 0.1px 1rem;
background-color: var(--accent);
border-radius: 0.5rem;
}
.exercice ul li span {
font-style: oblique;
}
</style>
+5 -4
View File
@@ -40,15 +40,16 @@ const rowBgDark = computed(() => {
<template>
<table class="table w-full border-collapse border border-gray-300">
<tr class="text-left" v-if="props.iterations.length > 0">
<th>Époch</th>
<th>Exemple</th>
<th class="sticky top-0 z-10 bg-background">Époque</th>
<th class="sticky top-0 z-10 bg-background">Exemple</th>
<th
v-for="(weight, index) in displayedWeights"
v-bind:key="index"
class="sticky top-0 z-10 bg-background"
>
X<sub>{{ index }}</sub>
w<sub>{{ index }}</sub>
</th>
<th>Erreur</th>
<th class="sticky top-0 z-10 bg-background">Erreur</th>
</tr>
<tr
v-for="(iteration, index) in props.iterations"
+13 -8
View File
@@ -6,42 +6,47 @@ const page = usePage();
const links = [
{
name: 'Perceptron Simple',
name: 'Introduction',
href: '/',
data: {},
},
{
name: '1. Perceptron Simple',
href: '/perceptron',
data: { type: 'simple' },
},
{
name: 'Descente du gradient',
name: '2. Descente du gradient',
href: '/perceptron',
data: { type: 'gradientdescent' },
},
{
name: 'ADALINE',
name: '3. ADALINE',
href: '/perceptron',
data: { type: 'adaline' },
},
{
name: 'Mono-couche',
name: '4. Monocouche',
href: '/perceptron',
data: { type: 'monolayer' },
},
{
name: 'Multi-couche',
name: '5. Multicouches',
href: '/perceptron',
data: { type: 'multilayer' },
},
];
const isActiveLink = (link: any) => {
return page.component === 'PerceptronViewer' && page.props.type === link.data.type;
return (page.component === 'Home' && page.props.type === link.data?.type) || (page.component === 'PerceptronViewer' && page.props.type === link.data?.type);
};
</script>
<template>
<header
class="my-6 w-full text-sm not-has-[nav]:hidden"
class="my-6 w-full overflow-x-auto text-sm not-has-[nav]:hidden"
>
<nav class="flex items-center justify-center gap-4">
<nav class="min-w-max flex items-center justify-center gap-4">
<Link
v-for="link in links"
:key="link.name"
@@ -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) {
@@ -129,8 +129,8 @@ const datasets = computed<ErrorDataset[]>(() => {
}"
:data="{
labels: props.iterations.reduce((labels, iteration) => {
if (!labels.includes(`Époch ${iteration.epoch}`)) {
labels.push(`Époch ${iteration.epoch}`);
if (!labels.includes(`Époque ${iteration.epoch}`)) {
labels.push(`Époque ${iteration.epoch}`);
}
return labels;
}, [] as string[]),
+188 -56
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
// import { Form } from '@inertiajs/vue3';
import { useForm } from '@inertiajs/vue3';
import { trackUmamiEvent } from '@jaseeey/vue-umami-plugin';
import { ref, watch } from 'vue';
import {
Form,
@@ -13,17 +13,21 @@ import {
NativeSelect,
NativeSelectOption,
} from '@/components/ui/native-select';
import { cancel } from '@/routes/perceptron';
import type {
Dataset,
InitializationMethod,
PerceptronType,
ValidationErrors,
} from '@/types/perceptron';
import Button from './ui/button/Button.vue';
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 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;
@@ -36,6 +40,8 @@ const props = defineProps<{
defaultLearningRate: number;
sessionId: string;
defaultMaxIterations: number;
maxIterationsLimit: number;
errors: ValidationErrors;
}>();
const selectedDatasetCopy = ref(props.selectedDataset);
@@ -45,89 +51,167 @@ const hiddenLayersNeurons = ref(props.hiddenLayersNeurons);
const minError = ref(props.minError);
const learningRate = ref(props.defaultLearningRate);
const maxIterations = ref(props.defaultMaxIterations);
const maxIterationsInput = ref<{
inputElement: HTMLInputElement | null;
} | null>(null);
const form = useForm({
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,
session_id: props.sessionId,
training_id: '',
max_iterations: maxIterations.value,
});
function handleMaxIterationsInput(event: Event) {
const input = event.target as HTMLInputElement;
const numericValue = Number(input.value);
const normalizedValue = Number.isFinite(numericValue)
? Math.min(Math.max(numericValue, 1), props.maxIterationsLimit)
: 1;
console.debug('[max_iterations] native input', {
raw: input.value,
numericValue,
normalizedValue,
limit: props.maxIterationsLimit,
});
input.value = String(normalizedValue);
maxIterations.value = normalizedValue;
}
watch(maxIterations, (value) => {
const input = maxIterationsInput.value?.inputElement;
console.debug('[max_iterations] ref changed', {
value,
displayed: input?.value,
});
if (input && input.value !== String(value)) {
input.value = String(value);
}
});
watch(selectedDatasetCopy, (newvalue) => {
const selectedDatasetCopy = props.datasets.find(
(dataset) => dataset.label === newvalue
) || null;
form.clearErrors('dataset');
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) {
alert('Veuillez sélectionner un dataset avant de lancer l\'entraînement.');
form.setError(
'dataset',
"Un dataset est nécessaire avant de lancer l'entraînement.",
);
console.log(form.errors);
return;
}
trainingId.value = `${props.sessionId}-${Date.now()}`; // Unique training ID based on session and timestamp
emit('update:trainingId', trainingId.value); // Emit the training ID to the parent component
trainingId.value = `${props.sessionId}-${Date.now()}`;
emit('update:trainingId', trainingId.value);
fetch('/api/perceptron/run', {
Object.assign(form, {
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,
session_id: props.sessionId,
training_id: trainingId.value,
max_iterations: maxIterations.value,
});
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({
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,
session_id: props.sessionId,
training_id: trainingId.value,
max_iterations: maxIterations.value,
}),
})
.then((response) => {
if (!response.ok) {
if (response.status === 429) {
alert('Trop de requêtes pour mon petit serveur. Veuillez réessayer dans une minute.');
} else {
alert('Erreur lors du lancement de l\'entraînement. Veuillez réessayer.');
}
throw new Error('Network response was not ok');
}
return response.json();
})
.then((data) => {
console.log('Perceptron training started:', data);
})
.catch((error) => {
console.error('Error starting perceptron training:', error);
});
});
}
const emit = defineEmits(['update:selectedDataset', 'update:trainingId']);
watch(selectedDatasetCopy, (newValue) => {
emit('update:selectedDataset', newValue);
});
@@ -141,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">
@@ -161,10 +246,16 @@ watch(selectedDatasetCopy, (newValue) => {
v-bind:key="dataset.label"
:value="dataset.label"
>
{{ dataset.label }}
{{ dataset.label.replace(/_/g, ' ') }}
</NativeSelectOption>
</NativeSelect>
</FormControl>
<FormError
:error="
form.errors.dataset ||
props.errors?.selectedDatasetCopy
"
/>
</FormItem>
</FormField>
@@ -182,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"
>
@@ -194,12 +287,16 @@ 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>
<!-- TODO : MAX input -->
<Input
ref="maxIterationsInput"
type="number"
v-model="hiddenLayers"
min="1"
@@ -212,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
@@ -264,21 +366,51 @@ watch(selectedDatasetCopy, (newValue) => {
<!-- MAX ITERATIONS -->
<FormField name="max_iterations">
<FormItem>
<FormLabel>Nombre maximum d'itérations</FormLabel>
<FormLabel>Nombre maximum d'époques</FormLabel>
<FormControl>
<Input
type="number"
v-model="maxIterations"
min="0"
max="5000"
:model-value="maxIterations"
min="1"
:max="props.maxIterationsLimit"
step="1"
class="w-min"
@input="handleMaxIterationsInput"
/>
</FormControl>
<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>
+1 -1
View File
@@ -9,8 +9,8 @@ import {
} from '@/components/ui/dropdown-menu';
import UserInfo from '@/components/UserInfo.vue';
import { logout } from '@/routes';
import { edit } from '@/routes/profile';
import type { User } from '@/types';
import { edit } from '@/routes/profile';
type Props = {
user: User;
@@ -0,0 +1,18 @@
<script setup lang="ts">
import type { AccordionRootEmits, AccordionRootProps } from "reka-ui"
import {
AccordionRoot,
useForwardPropsEmits,
} from "reka-ui"
const props = defineProps<AccordionRootProps>()
const emits = defineEmits<AccordionRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<AccordionRoot v-slot="slotProps" data-slot="accordion" v-bind="forwarded">
<slot v-bind="slotProps" />
</AccordionRoot>
</template>
@@ -0,0 +1,23 @@
<script setup lang="ts">
import type { AccordionContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { AccordionContent } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<AccordionContentProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<AccordionContent
data-slot="accordion-content"
v-bind="delegatedProps"
class="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
>
<div :class="cn('pt-0 pb-4', props.class)">
<slot />
</div>
</AccordionContent>
</template>
@@ -0,0 +1,24 @@
<script setup lang="ts">
import type { AccordionItemProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { AccordionItem, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<AccordionItemProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<AccordionItem
v-slot="slotProps"
data-slot="accordion-item"
v-bind="forwardedProps"
:class="cn('border-b last:border-b-0', props.class)"
>
<slot v-bind="slotProps" />
</AccordionItem>
</template>
@@ -0,0 +1,37 @@
<script setup lang="ts">
import type { AccordionTriggerProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { ChevronDown } from "@lucide/vue"
import { reactiveOmit } from "@vueuse/core"
import {
AccordionHeader,
AccordionTrigger,
} from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<AccordionTriggerProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<AccordionHeader class="flex">
<AccordionTrigger
data-slot="accordion-trigger"
v-bind="delegatedProps"
:class="
cn(
'focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-3 disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180',
props.class,
)
"
>
<slot />
<slot name="icon">
<ChevronDown
class="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200"
/>
</slot>
</AccordionTrigger>
</AccordionHeader>
</template>
@@ -0,0 +1,4 @@
export { default as Accordion } from "./Accordion.vue"
export { default as AccordionContent } from "./AccordionContent.vue"
export { default as AccordionItem } from "./AccordionItem.vue"
export { default as AccordionTrigger } from "./AccordionTrigger.vue"
@@ -27,6 +27,7 @@ export const buttonVariants = cva(
"icon": "size-9",
"icon-sm": "size-8",
"icon-lg": "size-10",
"icon-xl": "size-13",
},
},
defaultVariants: {
@@ -0,0 +1,19 @@
<script lang="ts" setup>
import type { DrawerRootEmits, DrawerRootProps } from "reka-ui"
import { DrawerRoot, useForwardPropsEmits } from "reka-ui"
const props = defineProps<DrawerRootProps>()
const emits = defineEmits<DrawerRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DrawerRoot
v-slot="slotProps"
v-bind="forwarded"
>
<slot v-bind="slotProps" />
</DrawerRoot>
</template>
@@ -0,0 +1,15 @@
<script lang="ts" setup>
import type { DrawerCloseProps } from "reka-ui"
import { DrawerClose } from "reka-ui"
const props = defineProps<DrawerCloseProps>()
</script>
<template>
<DrawerClose
data-slot="drawer-close"
v-bind="props"
>
<slot />
</DrawerClose>
</template>
@@ -0,0 +1,48 @@
<script lang="ts" setup>
import type { DrawerContentEmits, DrawerContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
DrawerContent,
DrawerHandle,
DrawerPortal,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/lib/utils"
import DrawerOverlay from "./DrawerOverlay.vue"
defineOptions({
inheritAttrs: false,
})
const props = defineProps<DrawerContentProps & { class?: HTMLAttributes["class"] }>()
const emits = defineEmits<DrawerContentEmits>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DrawerPortal>
<DrawerOverlay />
<DrawerContent
data-slot="drawer-content"
v-bind="{ ...$attrs, ...forwarded }"
:class="cn(
'group/drawer-content bg-background fixed z-50 flex h-auto flex-col',
'will-change-transform transform-[translate3d(var(--drawer-swipe-movement-x,0px),var(--drawer-swipe-movement-y,0px),0)]',
'transition-transform duration-500 ease-[cubic-bezier(0.32,0.72,0,1)] data-swiping:duration-0 data-swiping:select-none',
'data-[state=open]:animate-in data-[state=closed]:animate-out',
'data-[swipe-direction=up]:inset-x-0 data-[swipe-direction=up]:top-0 data-[swipe-direction=up]:mb-24 data-[swipe-direction=up]:max-h-[80vh] data-[swipe-direction=up]:rounded-b-lg data-[swipe-direction=up]:data-[state=open]:slide-in-from-top data-[swipe-direction=up]:data-[state=closed]:slide-out-to-top',
'data-[swipe-direction=down]:inset-x-0 data-[swipe-direction=down]:bottom-0 data-[swipe-direction=down]:mt-24 data-[swipe-direction=down]:max-h-[80vh] data-[swipe-direction=down]:rounded-t-lg data-[swipe-direction=down]:data-[state=open]:slide-in-from-bottom data-[swipe-direction=down]:data-[state=closed]:slide-out-to-bottom',
'data-[swipe-direction=right]:inset-y-0 data-[swipe-direction=right]:right-0 data-[swipe-direction=right]:w-3/4 data-[swipe-direction=right]:sm:max-w-sm data-[swipe-direction=right]:data-[state=open]:slide-in-from-right data-[swipe-direction=right]:data-[state=closed]:slide-out-to-right',
'data-[swipe-direction=left]:inset-y-0 data-[swipe-direction=left]:left-0 data-[swipe-direction=left]:w-3/4 data-[swipe-direction=left]:sm:max-w-sm data-[swipe-direction=left]:data-[state=open]:slide-in-from-left data-[swipe-direction=left]:data-[state=closed]:slide-out-to-left',
props.class,
)"
>
<DrawerHandle class="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[swipe-direction=down]/drawer-content:block" />
<slot />
</DrawerContent>
</DrawerPortal>
</template>
@@ -0,0 +1,21 @@
<script lang="ts" setup>
import type { DrawerDescriptionProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { DrawerDescription } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DrawerDescriptionProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<DrawerDescription
data-slot="drawer-description"
v-bind="delegatedProps"
:class="cn('text-muted-foreground text-sm', props.class)"
>
<slot />
</DrawerDescription>
</template>
@@ -0,0 +1,17 @@
<script lang="ts" setup>
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div
data-slot="drawer-footer"
:class="cn('mt-auto flex flex-col gap-2 p-4', props.class)"
>
<slot />
</div>
</template>
@@ -0,0 +1,17 @@
<script lang="ts" setup>
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div
data-slot="drawer-header"
:class="cn('flex flex-col gap-1.5 p-4', props.class)"
>
<slot />
</div>
</template>
@@ -0,0 +1,19 @@
<script lang="ts" setup>
import type { DrawerOverlayProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { DrawerOverlay } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DrawerOverlayProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<DrawerOverlay
data-slot="drawer-overlay"
v-bind="delegatedProps"
:class="cn('data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80 duration-500', props.class)"
/>
</template>
@@ -0,0 +1,21 @@
<script lang="ts" setup>
import type { DrawerTitleProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { DrawerTitle } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DrawerTitleProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<DrawerTitle
data-slot="drawer-title"
v-bind="delegatedProps"
:class="cn('text-foreground font-semibold', props.class)"
>
<slot />
</DrawerTitle>
</template>
@@ -0,0 +1,15 @@
<script lang="ts" setup>
import type { DrawerTriggerProps } from "reka-ui"
import { DrawerTrigger } from "reka-ui"
const props = defineProps<DrawerTriggerProps>()
</script>
<template>
<DrawerTrigger
data-slot="drawer-trigger"
v-bind="props"
>
<slot />
</DrawerTrigger>
</template>
@@ -0,0 +1,9 @@
export { default as Drawer } from "./Drawer.vue"
export { default as DrawerClose } from "./DrawerClose.vue"
export { default as DrawerContent } from "./DrawerContent.vue"
export { default as DrawerDescription } from "./DrawerDescription.vue"
export { default as DrawerFooter } from "./DrawerFooter.vue"
export { default as DrawerHeader } from "./DrawerHeader.vue"
export { default as DrawerOverlay } from "./DrawerOverlay.vue"
export { default as DrawerTitle } from "./DrawerTitle.vue"
export { default as DrawerTrigger } from "./DrawerTrigger.vue"
@@ -0,0 +1,10 @@
<script lang="ts" setup>
const props = defineProps<{
error?: string
}>()
</script>
<template>
<div class="text-sm text-red-500">{{ props.error }}</div>
</template>
+13 -6
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { useVModel } from "@vueuse/core"
import { ref } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
@@ -13,15 +13,22 @@ const emits = defineEmits<{
(e: "update:modelValue", payload: string | number): void
}>()
const modelValue = useVModel(props, "modelValue", emits, {
passive: true,
defaultValue: props.defaultValue,
})
const inputElement = ref<HTMLInputElement | null>(null)
defineExpose({ inputElement })
function handleInput(event: Event) {
const input = event.target as HTMLInputElement
emits("update:modelValue", input.value)
}
</script>
<template>
<input
v-model="modelValue"
ref="inputElement"
:value="modelValue ?? defaultValue ?? ''"
v-bind="$attrs"
@input="handleInput"
data-slot="input"
:class="cn(
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
+21
View File
@@ -0,0 +1,21 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<kbd
:class="cn(
'bg-muted text-muted-foreground pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium select-none',
`[&_svg:not([class*='size-'])]:size-3`,
'[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10',
props.class,
)"
>
<slot />
</kbd>
</template>
@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<kbd
data-slot="kbd-group"
:class="cn('inline-flex items-center gap-1', props.class)"
>
<slot />
</kbd>
</template>
+2
View File
@@ -0,0 +1,2 @@
export { default as Kbd } from "./Kbd.vue"
export { default as KbdGroup } from "./KbdGroup.vue"
@@ -0,0 +1,168 @@
<script setup lang="ts">
import type { ScrollAreaRootProps } from "reka-ui"
import { onBeforeUnmount, onMounted, ref, toRef, type HTMLAttributes } from "vue"
import { reactiveOmit, useScroll } from "@vueuse/core"
import {
ScrollAreaCorner,
ScrollAreaRoot,
ScrollAreaViewport,
} from "reka-ui"
import { cn } from "@/lib/utils"
import ScrollBar from "./ScrollBar.vue"
const props = defineProps<ScrollAreaRootProps & { class?: HTMLAttributes["class"], keepBottom?: boolean }>()
const delegatedProps = reactiveOmit(props, "class")
const scrollAreaRootRef = ref<{ viewport: HTMLElement } | null>(null)
const { arrivedState } = useScroll(() => scrollAreaRootRef.value?.viewport)
const bottom = toRef(arrivedState, 'bottom')
const isFollowingBottom = ref(props.keepBottom ?? false)
let isAutoScrolling = false
let mutationObserver: MutationObserver | null = null
let resizeObserver: ResizeObserver | null = null
let followFrame: number | null = null
let previousScrollTop = 0
const getViewport = () => scrollAreaRootRef.value?.viewport
const isNearBottom = () => {
const viewport = getViewport()
return viewport ? viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight <= 4 : true
}
const handleScroll = () => {
const viewport = getViewport()
if (!viewport) {
return
}
const scrollTop = viewport.scrollTop
if (scrollTop < previousScrollTop) {
isAutoScrolling = false
isFollowingBottom.value = false
}
previousScrollTop = scrollTop
if (isAutoScrolling) {
if (isNearBottom()) {
isAutoScrolling = false
}
return
}
isFollowingBottom.value = isNearBottom()
}
const handleUserScroll = () => {
isAutoScrolling = false
isFollowingBottom.value = false
const viewport = getViewport()
viewport?.scrollTo({ top: viewport.scrollTop, behavior: 'auto' })
}
const followBottomAfterUpdate = () => {
followFrame = null
if (isFollowingBottom.value) {
scrollToBottom('smooth')
}
}
const scheduleFollowBottom = () => {
if (followFrame === null) {
followFrame = requestAnimationFrame(followBottomAfterUpdate)
}
}
const isBottom = () => bottom.value
const scrollToBottom = (behavior: string = 'auto') => {
const viewport = getViewport()
if (viewport) {
isFollowingBottom.value = true
isAutoScrolling = behavior === 'smooth'
previousScrollTop = viewport.scrollTop
viewport.scrollTo({
top: viewport.scrollHeight,
behavior: behavior === 'smooth' ? 'smooth' : 'auto',
})
}
}
const followBottom = (force = false) => {
if (!force && !isFollowingBottom.value) {
return
}
isFollowingBottom.value = true
scrollToBottom('smooth')
}
const scrollToTop = (behavior: ScrollBehavior = 'smooth') => {
const viewport = getViewport()
if (viewport) {
isAutoScrolling = false
isFollowingBottom.value = false
viewport.scrollTo({ top: 0, behavior })
}
}
onMounted(() => {
const viewport = getViewport()
if (!viewport) {
return
}
viewport.addEventListener('scroll', handleScroll, { passive: true })
viewport.addEventListener('wheel', handleUserScroll, { passive: true })
viewport.addEventListener('touchstart', handleUserScroll, { passive: true })
mutationObserver = new MutationObserver(() => {
if (props.keepBottom) {
scheduleFollowBottom()
}
})
mutationObserver.observe(viewport, { childList: true, subtree: true })
resizeObserver = new ResizeObserver(scheduleFollowBottom)
resizeObserver.observe(viewport)
if (props.keepBottom) {
scrollToBottom()
}
})
onBeforeUnmount(() => {
const viewport = getViewport()
viewport?.removeEventListener('scroll', handleScroll)
viewport?.removeEventListener('wheel', handleUserScroll)
viewport?.removeEventListener('touchstart', handleUserScroll)
mutationObserver?.disconnect()
resizeObserver?.disconnect()
if (followFrame !== null) {
cancelAnimationFrame(followFrame)
}
})
interface Expose {
isBottom: () => boolean
isFollowingBottom: typeof isFollowingBottom
followBottom: (force?: boolean) => void
scrollToBottom: (behavior?: string) => void
scrollToTop: (behavior?: ScrollBehavior) => void
}
defineExpose<Expose>({
isBottom,
isFollowingBottom,
followBottom,
scrollToBottom,
scrollToTop,
})
</script>
<template>
<ScrollAreaRoot
v-bind="delegatedProps"
:class="cn('relative overflow-hidden', props.class)"
ref="scrollAreaRootRef"
>
<ScrollAreaViewport class="h-full w-full overflow-y-auto rounded-[inherit]">
<slot />
</ScrollAreaViewport>
<slot name="overlay" />
<ScrollBar />
<ScrollAreaCorner />
</ScrollAreaRoot>
</template>
@@ -0,0 +1,32 @@
<script setup lang="ts">
import type { ScrollAreaScrollbarProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { ScrollAreaScrollbar, ScrollAreaThumb } from "reka-ui"
import { cn } from "@/lib/utils"
const props = withDefaults(defineProps<ScrollAreaScrollbarProps & { class?: HTMLAttributes["class"] }>(), {
orientation: "vertical",
})
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
v-bind="delegatedProps"
:class="
cn('flex touch-none p-px transition-colors select-none',
orientation === 'vertical'
&& 'h-full w-2.5 border-l border-l-transparent',
orientation === 'horizontal'
&& 'h-2.5 flex-col border-t border-t-transparent',
props.class)"
>
<ScrollAreaThumb
data-slot="scroll-area-thumb"
class="bg-border relative flex-1 rounded-full"
/>
</ScrollAreaScrollbar>
</template>
@@ -0,0 +1,2 @@
export { default as ScrollArea } from "./ScrollArea.vue"
export { default as ScrollBar } from "./ScrollBar.vue"
+11 -73
View File
@@ -10,24 +10,12 @@ export type UseAppearanceReturn = {
updateAppearance: (value: Appearance) => void;
};
export function updateTheme(value: Appearance): void {
export function updateTheme(): void {
if (typeof window === 'undefined') {
return;
}
if (value === 'system') {
const mediaQueryList = window.matchMedia(
'(prefers-color-scheme: dark)',
);
const systemTheme = mediaQueryList.matches ? 'dark' : 'light';
document.documentElement.classList.toggle(
'dark',
systemTheme === 'dark',
);
} else {
document.documentElement.classList.toggle('dark', value === 'dark');
}
document.documentElement.classList.add('dark');
}
const setCookie = (name: string, value: string, days = 365) => {
@@ -40,80 +28,30 @@ const setCookie = (name: string, value: string, days = 365) => {
document.cookie = `${name}=${value};path=/;max-age=${maxAge};SameSite=Lax`;
};
const mediaQuery = () => {
if (typeof window === 'undefined') {
return null;
}
return window.matchMedia('(prefers-color-scheme: dark)');
};
const getStoredAppearance = () => {
if (typeof window === 'undefined') {
return null;
}
return localStorage.getItem('appearance') as Appearance | null;
};
const prefersDark = (): boolean => {
if (typeof window === 'undefined') {
return false;
}
return window.matchMedia('(prefers-color-scheme: dark)').matches;
};
const handleSystemThemeChange = () => {
const currentAppearance = getStoredAppearance();
updateTheme(currentAppearance || 'system');
};
export function initializeTheme(): void {
if (typeof window === 'undefined') {
return;
}
// Initialize theme from saved preference or default to system...
const savedAppearance = getStoredAppearance();
updateTheme(savedAppearance || 'system');
// Set up system theme change listener...
mediaQuery()?.addEventListener('change', handleSystemThemeChange);
updateTheme();
}
const appearance = ref<Appearance>('system');
const appearance = ref<Appearance>('dark');
export function useAppearance(): UseAppearanceReturn {
onMounted(() => {
const savedAppearance = localStorage.getItem(
'appearance',
) as Appearance | null;
if (savedAppearance) {
appearance.value = savedAppearance;
}
appearance.value = 'dark';
});
const resolvedAppearance = computed<ResolvedAppearance>(() => {
if (appearance.value === 'system') {
return prefersDark() ? 'dark' : 'light';
}
return appearance.value;
return 'dark';
});
function updateAppearance(value: Appearance) {
appearance.value = value;
// Store in localStorage for client-side persistence...
localStorage.setItem('appearance', value);
// Store in cookie for SSR...
setCookie('appearance', value);
updateTheme(value);
function updateAppearance() {
appearance.value = 'dark';
localStorage.setItem('appearance', 'dark');
setCookie('appearance', 'dark');
updateTheme();
}
return {
+1
View File
@@ -1,6 +1,7 @@
<script setup lang="ts">
import AppLayout from '@/layouts/app/AppSidebarLayout.vue';
import type { BreadcrumbItem } from '@/types';
import { useColorMode, useDark } from '@vueuse/core';
type Props = {
breadcrumbs?: BreadcrumbItem[];
+2 -2
View File
@@ -5,11 +5,11 @@ import { Button } from '@/components/ui/button';
import { Separator } from '@/components/ui/separator';
import { useCurrentUrl } from '@/composables/useCurrentUrl';
import { toUrl } from '@/lib/utils';
import { show } from '@/routes/two-factor';
import type { NavItem } from '@/types';
import { edit as editAppearance } from '@/routes/appearance';
import { edit as editProfile } from '@/routes/profile';
import { show } from '@/routes/two-factor';
import { edit as editPassword } from '@/routes/user-password';
import type { NavItem } from '@/types';
const sidebarNavItems: NavItem[] = [
{
+17
View File
@@ -0,0 +1,17 @@
<script lang="ts" setup>
import { Head } from '@inertiajs/vue3';
import HelpText from '@/components/HelpText.vue';
import LinkHeader from '@/components/LinkHeader.vue';
</script>
<template>
<Head title="Perceptron Viewer"></Head>
<main class="flex min-h-dvh flex-col gap-6 pb-6">
<LinkHeader class="w-full my-0! py-6 sticky top-0 z-10 bg-background" />
<div
class="flex flex-col items-center gap-6 rounded-lg p-6 shadow sm:p-8"
>
<HelpText markdownFileName="intro" class="max-w-[90ch]" />
</div>
</main>
</template>
+208 -31
View File
@@ -1,5 +1,8 @@
<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,
Title,
@@ -10,11 +13,32 @@ import {
LinearScale,
PointElement,
LineElement,
ScatterController,
LineController,
BarController,
LogarithmicScale,
} from 'chart.js';
import { computed, ref } from 'vue';
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,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { usePerceptronTraining } from '@/composables/usePerceptronTraining';
import type { Dataset, DatasetPoint, InitializationMethod, PerceptronType } from '@/types/perceptron';
import type { Dataset, DatasetPoint, InitializationMethod, PerceptronType, ValidationErrors } from '@/types/perceptron';
import IterationTable from '../components/IterationTable.vue';
import PerceptronDecisionGraph from '../components/PerceptronDecisionGraph.vue';
import PerceptronIterationsErrorsGraph from '../components/PerceptronIterationsErrorsGraph.vue';
@@ -29,6 +53,10 @@ ChartJS.register(
LinearScale,
PointElement,
LineElement,
ScatterController,
LineController,
BarController,
LogarithmicScale
);
ChartJS.defaults.font.size = 16;
ChartJS.defaults.color = '#FFF';
@@ -41,10 +69,15 @@ const props = defineProps<{
minError: number;
learningRate: number;
maxIterations: number;
maxIterationsLimit: number;
maxDisplayedWeights: number;
errors: ValidationErrors;
}>();
const selectedDatasetName = ref<string>('');
const selectedDataset = computed(() =>
props.datasets.find((item) => item.label === selectedDatasetName.value),
);
const dataset = computed<DatasetPoint[]>(() => {
const selected = props.datasets.find(
(d) => d.label === selectedDatasetName.value,
@@ -88,8 +121,43 @@ const {
trainingEndReason,
} = usePerceptronTraining(props.sessionId);
const isRegression = computed(
() => (props.type === 'multilayer' || props.type === 'monolayer') && activationFunction.value === 'linear',
() => selectedDataset.value?.inputCount === 1 && activationFunction.value === 'linear',
);
const drawerOpen = ref(false);
const helpScrollContainer = ref<HTMLElement | null>(null);
const helpScrollTop = ref(0);
const handleHelpScroll = (event: Event) => {
helpScrollTop.value = (event.currentTarget as HTMLElement).scrollTop;
};
const restoreHelpScroll = async () => {
await nextTick();
if (helpScrollContainer.value) {
helpScrollContainer.value.scrollTop = helpScrollTop.value;
}
};
const handleDrawerOpenChange = async (open: boolean) => {
if (!open && helpScrollContainer.value) {
helpScrollTop.value = helpScrollContainer.value.scrollTop;
}
drawerOpen.value = open;
if (open) {
await restoreHelpScroll();
trackUmamiEvent('wiki-opened', { perceptronType: props.type });
}
};
useEventListener('keydown', (event: KeyboardEvent) => {
if (event.key.toLowerCase() === 'w' /* && (event.ctrlKey || event.metaKey)*/) {
event.preventDefault();
void handleDrawerOpenChange(!drawerOpen.value);
}
});
function getActivationFunction(type: string): (x: number) => number {
switch (type) {
case 'step':
@@ -105,43 +173,139 @@ function getActivationFunction(type: string): (x: number) => number {
}
}
const iterationScrollArea = ref<{
followBottom: (force?: boolean) => void;
isFollowingBottom: boolean;
scrollToTop: (behavior?: ScrollBehavior) => void;
} | null>(null);
watch([iterations, trainingEnded], async () => {
await nextTick();
iterationScrollArea.value?.followBottom();
});
</script>
<template>
<Head title="Perceptron Viewer"></Head>
<main class="space-y-6">
<main class="flex min-h-dvh flex-col pb-6 space-y-6">
<LinkHeader class="w-full" />
<PerceptronSetup
:type="props.type"
:datasets="props.datasets"
:selectedDataset="selectedDatasetName"
:initializationMethod="initializationMethod"
:hidden-layers="hiddenLayers"
:hidden-layers-neurons="hiddenLayersNeurons"
:minError="props.minError"
:sessionId="props.sessionId"
:defaultLearningRate="props.learningRate"
:defaultMaxIterations="props.maxIterations"
@update:selected-dataset="
(newValue) => {
selectedDatasetName = newValue;
}
"
@update:training-id="setTrainingId"
/>
<Drawer
:open="drawerOpen"
swipe-direction="left"
@update:open="handleDrawerOpenChange"
>
<TooltipProvider :delay-duration="300">
<UiTooltip>
<TooltipTrigger as-child>
<DrawerTrigger as-child>
<Button
variant="default"
size="icon-xl"
class="sticky top-3 z-20 ml-3 cursor-pointer rounded-full"
title="Wiki"
aria-label="Wiki"
>
<BookOpenText class="size-7" />
</Button>
</DrawerTrigger>
</TooltipTrigger>
<TooltipContent side="right">
<span>Wiki </span>
<KbdGroup>
<Kbd>W</Kbd>
</KbdGroup>
</TooltipContent>
</UiTooltip>
</TooltipProvider>
<DrawerContent class="w-[100ch]! max-w-[85vw]!">
<DrawerTitle class="sr-only">Wiki</DrawerTitle>
<div
ref="helpScrollContainer"
class="no-scrollbar overflow-y-auto px-4"
@scroll="handleHelpScroll"
>
<HelpText
:markdownFileName="props.type"
@loaded="restoreHelpScroll"
/>
</div>
</DrawerContent>
</Drawer>
<PerceptronSetup
:type="props.type"
:datasets="props.datasets"
:selectedDataset="selectedDatasetName"
:initializationMethod="initializationMethod"
:hidden-layers="hiddenLayers"
:hidden-layers-neurons="hiddenLayersNeurons"
:minError="props.minError"
:sessionId="props.sessionId"
:defaultLearningRate="props.learningRate"
:defaultMaxIterations="props.maxIterations"
:maxIterationsLimit="props.maxIterationsLimit"
:errors="props.errors"
@update:selected-dataset="
(newValue) => {
selectedDatasetName = newValue;
}
"
@update:training-id="setTrainingId"
/>
<div
class="align-items-start justify-content-center flex h-full min-h-dvh max-w-dvw"
class="align-items-start justify-content-center grid max-w-dvw grid-cols-2"
v-if="selectedDatasetName || iterations.length > 0"
>
<div class="max-h-full w-full overflow-y-scroll">
<IterationTable
:iterations="iterations"
:trainingEnded="trainingEnded"
:trainingEndReason="trainingEndReason"
:maxDisplayedWeights="props.maxDisplayedWeights"
/>
<div class="pt-3 h-dvh max-h-dvh min-h-0 w-full min-w-0">
<ScrollArea ref="iterationScrollArea" :keep-bottom="true" type="scroll" class="relative h-full max-h-full min-h-0 w-full">
<IterationTable
:iterations="iterations"
:trainingEnded="trainingEnded"
:trainingEndReason="trainingEndReason"
:maxDisplayedWeights="props.maxDisplayedWeights"
/>
<template #overlay>
<div class="absolute right-4 bottom-4 z-10 flex flex-col items-end gap-2">
<Button
type="button"
variant="secondary"
size="icon"
class="size-9 rounded-full shadow-md"
aria-label="Revenir en haut du tableau"
title="Revenir en haut"
@click="iterationScrollArea?.scrollToTop()"
>
<ArrowUp class="size-4" />
</Button>
<Transition
enter-active-class="transition-opacity duration-300 ease-in-out"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
leave-active-class="transition-opacity duration-300 ease-in-out"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<Button
v-if="iterationScrollArea && !iterationScrollArea.isFollowingBottom"
type="button"
variant="secondary"
size="icon"
class="size-9 rounded-full shadow-md"
aria-label="Reprendre le défilement automatique"
title="Reprendre le défilement automatique"
@click="iterationScrollArea?.followBottom(true)"
>
<ArrowDown class="size-4" />
</Button>
</Transition>
</div>
</template>
</ScrollArea>
</div>
<div class="sticky top-0 h-full w-full">
<div class="h-full w-full min-w-0 space-y-6">
<div>
<PerceptronDecisionGraph
:cleanedDataset="cleanedDataset"
@@ -163,3 +327,16 @@ function getActivationFunction(type: string): (x: number) => number {
</div>
</main>
</template>
<style lang="css" scoped>
/* we will explain what these classes do next! */
.v-enter-active,
.v-leave-active {
transition: opacity 0.5s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
</style>
+3
View File
@@ -7,6 +7,7 @@ export type Iteration = {
export type Dataset = {
label: string;
inputCount: number;
data: DatasetPoint[];
defaultLearningRate?: number;
defaultMinError?: number;
@@ -24,3 +25,5 @@ export type DatasetPoint = {
export type InitializationMethod = 'zeros' | 'random';
export type PerceptronType = 'simple' | 'gradientdescent' | 'adaline' | 'monolayer' | 'multilayer';
export type ValidationErrors = Record<string, string>;
+2 -2
View File
@@ -1,4 +1,4 @@
export type Appearance = 'light' | 'dark' | 'system';
export type ResolvedAppearance = 'light' | 'dark';
export type Appearance = 'dark';
export type ResolvedAppearance = 'dark';
export type AppShellVariant = 'header' | 'sidebar';

Some files were not shown because too many files have changed in this diff Show More