Compare commits
24 Commits
da53bf16e9
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 21c02706de | |||
| 7c223e0ba8 | |||
| 6d8e3438f0 | |||
| ae676343a5 | |||
| 9389aef632 | |||
| fa6c427d76 | |||
| 25b03fc39a | |||
| d80bfe3cdd | |||
| 2332e805e8 | |||
| 4610334f72 | |||
| 9ae3a6942b | |||
| 3a19691591 | |||
| 2d7c1873bb | |||
| 5d68757e9d | |||
| 2de27af03b | |||
| be7b276dec | |||
| 2345b850b8 | |||
| 538ddc9679 | |||
| 099318c79e | |||
| 1517920fce | |||
| d4f98517cd | |||
| feac0feaeb | |||
| ba4b951a69 | |||
| 41b0e8a64a |
@@ -3,6 +3,7 @@ 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
|
||||
@@ -11,6 +12,10 @@ 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
|
||||
@@ -40,6 +45,9 @@ REVERB_APP_SECRET=oosuq0v9jgaslzp9cmhv
|
||||
REVERB_HOST="perceptron.matthiasg.dev"
|
||||
REVERB_PORT=443
|
||||
REVERB_SCHEME=https
|
||||
REVERB_BROADCAST_HOST=reverb
|
||||
REVERB_BROADCAST_PORT=8080
|
||||
REVERB_BROADCAST_SCHEME=http
|
||||
REVERB_MAX_REQUEST_SIZE=100000
|
||||
|
||||
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
|
||||
|
||||
@@ -14,6 +14,8 @@ jobs:
|
||||
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
|
||||
@@ -47,6 +49,11 @@ jobs:
|
||||
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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
+12
@@ -9,11 +9,23 @@ RUN composer install --no-dev --optimize-autoloader --no-interaction
|
||||
# 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 \
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class TrainingCancelledException extends RuntimeException
|
||||
{
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Events\PerceptronInitialization;
|
||||
use App\Exceptions\TrainingCancelledException;
|
||||
use App\Http\Requests\RunPerceptronRequest;
|
||||
use App\Models\NetworksTraining\ADALINEPerceptronTraining;
|
||||
use App\Models\NetworksTraining\GradientDescentPerceptronTraining;
|
||||
@@ -18,15 +19,31 @@ use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider;
|
||||
use App\Services\SynapticWeightsProvider\RandomSynapticWeights;
|
||||
use App\Services\SynapticWeightsProvider\ZeroSynapticWeights;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class PerceptronController extends Controller
|
||||
{
|
||||
private function cancellationKey(string $trainingId): string
|
||||
{
|
||||
return "perceptron-training-cancelled:{$trainingId}";
|
||||
}
|
||||
|
||||
public function cancel(Request $request)
|
||||
{
|
||||
$trainingId = $request->validate([
|
||||
'training_id' => ['required', 'string', 'max:100'],
|
||||
])['training_id'];
|
||||
|
||||
Cache::put($this->cancellationKey($trainingId), true, now()->addHour());
|
||||
|
||||
return response()->noContent();
|
||||
}
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$perceptronType = $request->query('type');
|
||||
$perceptronType = $request->query('type', 'simple'); ;
|
||||
|
||||
$learningRate = 0.01;
|
||||
$maxIterations = 200;
|
||||
@@ -220,6 +237,8 @@ class PerceptronController extends Controller
|
||||
$sessionId = $request->input('session_id', session()->getId());
|
||||
$trainingId = $request->input('training_id');
|
||||
|
||||
Cache::forget($this->cancellationKey($trainingId));
|
||||
|
||||
// Zero initialization prevents hidden layers from receiving a gradient.
|
||||
if ($perceptronType === 'multilayer' && $weightInitMethod === 'zeros') {
|
||||
$synapticWeightsProvider = new RandomSynapticWeights;
|
||||
@@ -236,17 +255,22 @@ class PerceptronController extends Controller
|
||||
$datasetReader = $this->getDataSetReader($dataSet);
|
||||
|
||||
$networkTraining = match ($perceptronType) {
|
||||
'simple' => new SimpleBinaryPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId),
|
||||
'gradientdescent' => new GradientDescentPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError),
|
||||
'adaline' => new ADALINEPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError),
|
||||
'monolayer' => new MonoLayerPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError),
|
||||
'multilayer' => new MultiLayerPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $hiddenLayers, $hiddenLayersNeurons, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError),
|
||||
'simple' => new SimpleBinaryPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, fn (): bool => connection_aborted() || Cache::has($this->cancellationKey($trainingId))),
|
||||
'gradientdescent' => new GradientDescentPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError, fn (): bool => connection_aborted() || Cache::has($this->cancellationKey($trainingId))),
|
||||
'adaline' => new ADALINEPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError, fn (): bool => connection_aborted() || Cache::has($this->cancellationKey($trainingId))),
|
||||
'monolayer' => new MonoLayerPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError, fn (): bool => connection_aborted() || Cache::has($this->cancellationKey($trainingId))),
|
||||
'multilayer' => new MultiLayerPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $hiddenLayers, $hiddenLayersNeurons, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError, fn (): bool => connection_aborted() || Cache::has($this->cancellationKey($trainingId))),
|
||||
default => null,
|
||||
};
|
||||
|
||||
event(new PerceptronInitialization($datasetReader->lines, $networkTraining->activationFunction, $sessionId, $trainingId));
|
||||
|
||||
try {
|
||||
$networkTraining->start();
|
||||
} catch (TrainingCancelledException) {
|
||||
$networkTraining->cancel();
|
||||
Cache::forget($this->cancellationKey($trainingId));
|
||||
}
|
||||
|
||||
return back()->with('success', [
|
||||
'message' => 'Training completed',
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Models\Perceptrons\Perceptron;
|
||||
use App\Services\DatasetReader\IDataSetReader;
|
||||
use App\Services\IterationEventBuffer\IPerceptronIterationEventBuffer;
|
||||
use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider;
|
||||
use Closure;
|
||||
|
||||
class ADALINEPerceptronTraining extends NetworkTraining
|
||||
{
|
||||
@@ -26,8 +27,9 @@ class ADALINEPerceptronTraining extends NetworkTraining
|
||||
string $sessionId,
|
||||
string $trainingId,
|
||||
private float $minError,
|
||||
?Closure $isCancelled = null,
|
||||
) {
|
||||
parent::__construct($datasetReader, $maxEpochs, $iterationEventBuffer, $sessionId, $trainingId);
|
||||
parent::__construct($datasetReader, $maxEpochs, $iterationEventBuffer, $sessionId, $trainingId, $isCancelled);
|
||||
$this->perceptron = new GradientDescentPerceptron($synapticWeightsProvider->generate($datasetReader->getInputSize()));
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Models\Perceptrons\Perceptron;
|
||||
use App\Services\DatasetReader\IDataSetReader;
|
||||
use App\Services\IterationEventBuffer\IPerceptronIterationEventBuffer;
|
||||
use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider;
|
||||
use Closure;
|
||||
|
||||
class GradientDescentPerceptronTraining extends NetworkTraining
|
||||
{
|
||||
@@ -26,8 +27,9 @@ class GradientDescentPerceptronTraining extends NetworkTraining
|
||||
string $sessionId,
|
||||
string $trainingId,
|
||||
private float $minError,
|
||||
?Closure $isCancelled = null,
|
||||
) {
|
||||
parent::__construct($datasetReader, $maxEpochs, $iterationEventBuffer, $sessionId, $trainingId);
|
||||
parent::__construct($datasetReader, $maxEpochs, $iterationEventBuffer, $sessionId, $trainingId, $isCancelled);
|
||||
$this->perceptron = new GradientDescentPerceptron($synapticWeightsProvider->generate($datasetReader->getInputSize()));
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ use App\Services\DatasetReader\IDataSetReader;
|
||||
use App\Services\IterationEventBuffer\IPerceptronIterationEventBuffer;
|
||||
use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider;
|
||||
use App\Services\SynapticWeightsProvider\SimpleNetworkWeightsProvider;
|
||||
use Closure;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class MonoLayerPerceptronTraining extends NetworkTraining
|
||||
@@ -35,8 +36,9 @@ class MonoLayerPerceptronTraining extends NetworkTraining
|
||||
string $sessionId,
|
||||
string $trainingId,
|
||||
private float $minError,
|
||||
?Closure $isCancelled = null,
|
||||
) {
|
||||
parent::__construct($datasetReader, $maxEpochs, $iterationEventBuffer, $sessionId, $trainingId);
|
||||
parent::__construct($datasetReader, $maxEpochs, $iterationEventBuffer, $sessionId, $trainingId, $isCancelled);
|
||||
$this->isRegression = $datasetReader->getInputSize() === 1;
|
||||
$networkWeightsProvider = new SimpleNetworkWeightsProvider($synapticWeightsProvider);
|
||||
$this->network = new NetworkPerceptron(
|
||||
|
||||
@@ -11,6 +11,7 @@ use App\Services\DatasetReader\IDataSetReader;
|
||||
use App\Services\IterationEventBuffer\IPerceptronIterationEventBuffer;
|
||||
use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider;
|
||||
use App\Services\SynapticWeightsProvider\SimpleNetworkWeightsProvider;
|
||||
use Closure;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class MultiLayerPerceptronTraining extends NetworkTraining
|
||||
@@ -37,8 +38,9 @@ class MultiLayerPerceptronTraining extends NetworkTraining
|
||||
string $sessionId,
|
||||
string $trainingId,
|
||||
private float $minError,
|
||||
?Closure $isCancelled = null,
|
||||
) {
|
||||
parent::__construct($datasetReader, $maxEpochs, $iterationEventBuffer, $sessionId, $trainingId);
|
||||
parent::__construct($datasetReader, $maxEpochs, $iterationEventBuffer, $sessionId, $trainingId, $isCancelled);
|
||||
$this->labels = $datasetReader->getLabels();
|
||||
$this->isRegression = $datasetReader->getOutputSize() === 1
|
||||
|| ($datasetReader->getOutputSize() > 2
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
namespace App\Models\NetworksTraining;
|
||||
|
||||
use App\Events\PerceptronTrainingEnded;
|
||||
use App\Exceptions\TrainingCancelledException;
|
||||
use App\Models\ActivationsFunctions;
|
||||
use App\Services\DatasetReader\IDataSetReader;
|
||||
use App\Services\IterationEventBuffer\IPerceptronIterationEventBuffer;
|
||||
use Closure;
|
||||
|
||||
abstract class NetworkTraining
|
||||
{
|
||||
@@ -24,6 +26,7 @@ abstract class NetworkTraining
|
||||
protected IPerceptronIterationEventBuffer $iterationEventBuffer,
|
||||
protected string $sessionId,
|
||||
protected string $trainingId,
|
||||
protected ?Closure $isCancelled = null,
|
||||
) {}
|
||||
|
||||
abstract public function start(): void;
|
||||
@@ -50,9 +53,18 @@ abstract class NetworkTraining
|
||||
|
||||
protected function addIterationToBuffer(float $error, array $synapticWeights)
|
||||
{
|
||||
if ($this->isCancelled !== null && ($this->isCancelled)()) {
|
||||
throw new TrainingCancelledException;
|
||||
}
|
||||
|
||||
$this->iterationEventBuffer->addIteration($this->epoch, $this->datasetReader->getLastReadLineIndex(), $error, $synapticWeights);
|
||||
}
|
||||
|
||||
public function cancel(): void
|
||||
{
|
||||
$this->broadcastTrainingEnded('Entraînement annulé');
|
||||
}
|
||||
|
||||
public function getEpoch(): int
|
||||
{
|
||||
return $this->epoch;
|
||||
|
||||
@@ -9,6 +9,7 @@ use App\Models\Perceptrons\SimpleBinaryPerceptron;
|
||||
use App\Services\DatasetReader\IDataSetReader;
|
||||
use App\Services\IterationEventBuffer\IPerceptronIterationEventBuffer;
|
||||
use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider;
|
||||
use Closure;
|
||||
|
||||
class SimpleBinaryPerceptronTraining extends NetworkTraining
|
||||
{
|
||||
@@ -28,8 +29,9 @@ class SimpleBinaryPerceptronTraining extends NetworkTraining
|
||||
IPerceptronIterationEventBuffer $iterationEventBuffer,
|
||||
string $sessionId,
|
||||
string $trainingId,
|
||||
?Closure $isCancelled = null,
|
||||
) {
|
||||
parent::__construct($datasetReader, $maxEpochs, $iterationEventBuffer, $sessionId, $trainingId);
|
||||
parent::__construct($datasetReader, $maxEpochs, $iterationEventBuffer, $sessionId, $trainingId, $isCancelled);
|
||||
$this->perceptron = new SimpleBinaryPerceptron($synapticWeightsProvider->generate($datasetReader->getInputSize()));
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,9 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
HandleInertiaRequests::class,
|
||||
AddLinkHeadersForPreloadedAssets::class,
|
||||
]);
|
||||
$middleware->trustProxies(at: [
|
||||
'*'
|
||||
]);
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
//
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -22,7 +22,7 @@ return [
|
||||
/**
|
||||
* Minimum time between training progress broadcasts, in milliseconds.
|
||||
*/
|
||||
'broadcast_minimum_interval_ms' => 0,
|
||||
'broadcast_minimum_interval_ms' => 100,
|
||||
|
||||
/**
|
||||
* Maximum number of weights for which all iteration weights are broadcast
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,8 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Migrate the database
|
||||
php ./artisan migrate --force || exit 1
|
||||
|
||||
# Cache
|
||||
php ./artisan optimize:clear && php ./artisan optimize
|
||||
|
||||
|
||||
Generated
+6
@@ -6,6 +6,7 @@
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@inertiajs/vue3": "^2.3.7",
|
||||
"@jaseeey/vue-umami-plugin": "^1.6.1",
|
||||
"@lucide/vue": "^1.46.0",
|
||||
"@tailwindcss/typography": "^0.5.20",
|
||||
"@vee-validate/zod": "^4.15.1",
|
||||
@@ -861,6 +862,11 @@
|
||||
"@swc/helpers": "^0.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jaseeey/vue-umami-plugin": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@jaseeey/vue-umami-plugin/-/vue-umami-plugin-1.6.1.tgz",
|
||||
"integrity": "sha512-a38bB7cYbwMP5HmlrT2Dk2RHdGFxVvLRci2GhY8cIwCpM419qXtvSM6SKmIJ3U3u7zYdcQLVAySOcXY150Z14Q=="
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@inertiajs/vue3": "^2.3.7",
|
||||
"@jaseeey/vue-umami-plugin": "^1.6.1",
|
||||
"@lucide/vue": "^1.46.0",
|
||||
"@tailwindcss/typography": "^0.5.20",
|
||||
"@vee-validate/zod": "^4.15.1",
|
||||
|
||||
@@ -59,7 +59,7 @@ Reprenons le dataset oblique afin de pouvoir comparer les résultats, et diminuo
|
||||
|
||||
Puis cliquez sur "Lancer" tout en laissant les autres paramètres intacts.
|
||||
|
||||
On peut voir en dessous du tableau que l'entraînement s'est arrêté car le nombre maximal d'époques a été atteint. On pourrait l'augmenter (max 5000 pour mon petit serveur), mais ce serait du temps perdu. La solution du réseau est suffisante et il n'y aurait pas beaucoup de gain, même pour le triple d'itérations maximales en plus. Pour le prouver, cliquez sur le bouton `Afficher uniquement l'erreur quadratique moyenne`, la ligne que l'on peut voir ressemble fortement à la fonction logarithme $y = log(x^{-1})$. La progression de la descente du gradient est donc logarithmique ; les plus gros changements se font dans les premières époques.
|
||||
On peut voir en dessous du tableau que l'entraînement s'est arrêté car le nombre maximal d'époques a été atteint. On pourrait l'augmenter (max 5000 pour mon petit serveur), mais ce serait du temps perdu. La solution du réseau est suffisante et il n'y aurait pas beaucoup de gain, même pour le triple d'itérations maximales en plus. Pour le prouver, cliquez sur le bouton `Afficher uniquement l'erreur quadratique moyenne`, la ligne que l'on peut voir ressemble fortement à la fonction logarithme $y = log(x^{-1})$ (attention à l'échelle qui est elle-même logarithmique). La progression de la descente du gradient est donc logarithmique ; les plus gros changements se font dans les premières époques.
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 199 KiB After Width: | Height: | Size: 205 KiB |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useForm } from '@inertiajs/vue3';
|
||||
import { trackUmamiEvent } from '@jaseeey/vue-umami-plugin';
|
||||
import { ref, watch } from 'vue';
|
||||
import {
|
||||
Form,
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
NativeSelect,
|
||||
NativeSelectOption,
|
||||
} from '@/components/ui/native-select';
|
||||
import { cancel } from '@/routes/perceptron';
|
||||
import type {
|
||||
Dataset,
|
||||
InitializationMethod,
|
||||
@@ -23,8 +25,9 @@ import Card from './ui/card/Card.vue';
|
||||
import CardContent from './ui/card/CardContent.vue';
|
||||
import CardHeader from './ui/card/CardHeader.vue';
|
||||
import CardTitle from './ui/card/CardTitle.vue';
|
||||
import Input from './ui/input/Input.vue';
|
||||
import FormError from './ui/form/FormError.vue';
|
||||
import Input from './ui/input/Input.vue';
|
||||
import Spinner from './ui/spinner/Spinner.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
type: PerceptronType;
|
||||
@@ -98,43 +101,69 @@ watch(maxIterations, (value) => {
|
||||
watch(selectedDatasetCopy, (newvalue) => {
|
||||
form.clearErrors('dataset');
|
||||
|
||||
const selectedDatasetCopy = props.datasets.find(
|
||||
(dataset) => dataset.label === newvalue
|
||||
) || null;
|
||||
const selectedDatasetCopy =
|
||||
props.datasets.find((dataset) => dataset.label === newvalue) || null;
|
||||
|
||||
// LearningRate
|
||||
learningRate.value = props.defaultLearningRate;
|
||||
if (selectedDatasetCopy && selectedDatasetCopy.defaultLearningRate !== undefined) {
|
||||
if (
|
||||
selectedDatasetCopy &&
|
||||
selectedDatasetCopy.defaultLearningRate !== undefined
|
||||
) {
|
||||
learningRate.value = selectedDatasetCopy.defaultLearningRate;
|
||||
}
|
||||
// MinError
|
||||
minError.value = props.minError;
|
||||
if (selectedDatasetCopy && selectedDatasetCopy.defaultMinError !== undefined) {
|
||||
if (
|
||||
selectedDatasetCopy &&
|
||||
selectedDatasetCopy.defaultMinError !== undefined
|
||||
) {
|
||||
minError.value = selectedDatasetCopy.defaultMinError;
|
||||
}
|
||||
// MaxIterations
|
||||
maxIterations.value = props.defaultMaxIterations;
|
||||
if (selectedDatasetCopy && selectedDatasetCopy.defaultMaxIterations !== undefined) {
|
||||
if (
|
||||
selectedDatasetCopy &&
|
||||
selectedDatasetCopy.defaultMaxIterations !== undefined
|
||||
) {
|
||||
maxIterations.value = selectedDatasetCopy.defaultMaxIterations;
|
||||
}
|
||||
// HiddenLayers
|
||||
hiddenLayers.value = props.hiddenLayers;
|
||||
if (selectedDatasetCopy && selectedDatasetCopy.defaultHiddenLayers !== undefined) {
|
||||
if (
|
||||
selectedDatasetCopy &&
|
||||
selectedDatasetCopy.defaultHiddenLayers !== undefined
|
||||
) {
|
||||
hiddenLayers.value = selectedDatasetCopy.defaultHiddenLayers;
|
||||
}
|
||||
hiddenLayersNeurons.value = props.hiddenLayersNeurons;
|
||||
if (selectedDatasetCopy && selectedDatasetCopy.defaultHiddenLayersNeurons !== undefined) {
|
||||
hiddenLayersNeurons.value = selectedDatasetCopy.defaultHiddenLayersNeurons;
|
||||
if (
|
||||
selectedDatasetCopy &&
|
||||
selectedDatasetCopy.defaultHiddenLayersNeurons !== undefined
|
||||
) {
|
||||
hiddenLayersNeurons.value =
|
||||
selectedDatasetCopy.defaultHiddenLayersNeurons;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const trainingId = ref<string>('');
|
||||
|
||||
function startTraining() {
|
||||
trackUmamiEvent('perceptron-training-start', {
|
||||
type: props.type,
|
||||
dataset: selectedDatasetCopy.value,
|
||||
weight_init_method: selectedMethod.value,
|
||||
hidden_layers: hiddenLayers.value,
|
||||
hidden_layers_neurons: hiddenLayersNeurons.value,
|
||||
min_error: minError.value,
|
||||
learning_rate: learningRate.value,
|
||||
max_iterations: maxIterations.value,
|
||||
});
|
||||
|
||||
if (!selectedDatasetCopy.value) {
|
||||
form.setError(
|
||||
'dataset',
|
||||
'Un dataset est nécessaire avant de lancer l\'entraînement.',
|
||||
"Un dataset est nécessaire avant de lancer l'entraînement.",
|
||||
);
|
||||
console.log(form.errors);
|
||||
return;
|
||||
@@ -156,19 +185,33 @@ function startTraining() {
|
||||
max_iterations: maxIterations.value,
|
||||
});
|
||||
|
||||
console.debug('[max_iterations] submitting', {
|
||||
displayed: maxIterationsInput.value?.inputElement?.value,
|
||||
refValue: maxIterations.value,
|
||||
formValue: form.max_iterations,
|
||||
limit: props.maxIterationsLimit,
|
||||
});
|
||||
|
||||
form.post('/perceptron/run', {
|
||||
preserveScroll: true,
|
||||
});
|
||||
}
|
||||
|
||||
const emit = defineEmits(['update:selectedDataset', 'update:trainingId']);
|
||||
|
||||
async function cancelTraining() {
|
||||
trackUmamiEvent('perceptron-training-cancel', {
|
||||
training_id: trainingId.value,
|
||||
});
|
||||
|
||||
form.cancel();
|
||||
|
||||
await fetch(cancel().url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({
|
||||
training_id: trainingId.value,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
watch(selectedDatasetCopy, (newValue) => {
|
||||
emit('update:selectedDataset', newValue);
|
||||
});
|
||||
@@ -182,6 +225,7 @@ watch(selectedDatasetCopy, (newValue) => {
|
||||
<CardContent>
|
||||
<Form
|
||||
class="grid auto-cols-max grid-flow-row grid-cols-1 gap-4 space-y-6 md:grid-cols-2"
|
||||
cancel-on-unmount
|
||||
>
|
||||
<!-- DATASET -->
|
||||
<FormField name="dataset">
|
||||
@@ -206,7 +250,12 @@ watch(selectedDatasetCopy, (newValue) => {
|
||||
</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
</FormControl>
|
||||
<FormError :error="form.errors.dataset || props.errors?.selectedDatasetCopy" />
|
||||
<FormError
|
||||
:error="
|
||||
form.errors.dataset ||
|
||||
props.errors?.selectedDatasetCopy
|
||||
"
|
||||
/>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
@@ -224,7 +273,9 @@ watch(selectedDatasetCopy, (newValue) => {
|
||||
class="cursor-pointer"
|
||||
>
|
||||
<NativeSelectOption
|
||||
v-for="method in (props.type == 'multilayer' ? ['random'] : ['zeros', 'random'])"
|
||||
v-for="method in props.type == 'multilayer'
|
||||
? ['random']
|
||||
: ['zeros', 'random']"
|
||||
v-bind:key="method"
|
||||
:value="method"
|
||||
>
|
||||
@@ -236,7 +287,10 @@ watch(selectedDatasetCopy, (newValue) => {
|
||||
</FormField>
|
||||
|
||||
<!-- HIDDEN LAYERS -->
|
||||
<FormField name="hidden_layers" v-if="props.type === 'multilayer'">
|
||||
<FormField
|
||||
name="hidden_layers"
|
||||
v-if="props.type === 'multilayer'"
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel>Nombre de couches cachées</FormLabel>
|
||||
<FormControl>
|
||||
@@ -255,9 +309,14 @@ watch(selectedDatasetCopy, (newValue) => {
|
||||
</FormField>
|
||||
|
||||
<!-- HIDDEN LAYERS NEURONS -->
|
||||
<FormField name="hidden_layers_neurons" v-if="props.type === 'multilayer'">
|
||||
<FormField
|
||||
name="hidden_layers_neurons"
|
||||
v-if="props.type === 'multilayer'"
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel>Nombre de neurones par couche cachée</FormLabel>
|
||||
<FormLabel
|
||||
>Nombre de neurones par couche cachée</FormLabel
|
||||
>
|
||||
<FormControl>
|
||||
<!-- TODO : MAX input -->
|
||||
<Input
|
||||
@@ -319,13 +378,39 @@ watch(selectedDatasetCopy, (newValue) => {
|
||||
@input="handleMaxIterationsInput"
|
||||
/>
|
||||
</FormControl>
|
||||
<div v-if="form.errors.max_iterations || props.errors.max_iterations">
|
||||
{{ form.errors.max_iterations || props.errors.max_iterations }}
|
||||
<div
|
||||
v-if="
|
||||
form.errors.max_iterations ||
|
||||
props.errors.max_iterations
|
||||
"
|
||||
>
|
||||
{{
|
||||
form.errors.max_iterations ||
|
||||
props.errors.max_iterations
|
||||
}}
|
||||
</div>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</Form>
|
||||
<Button variant="outline" class="cursor-pointer mt-6" @click="startTraining">Lancer</Button>
|
||||
|
||||
<Transition name="fade">
|
||||
<Button
|
||||
variant="outline"
|
||||
class="mt-6 cursor-pointer"
|
||||
:disabled="form.processing"
|
||||
@click="startTraining"
|
||||
>Lancer<Spinner v-if="form.processing" class="ml-1"
|
||||
/></Button>
|
||||
</Transition>
|
||||
<Transition name="fade">
|
||||
<Button
|
||||
variant="outline"
|
||||
class="mt-6 ml-4 cursor-pointer"
|
||||
@click="cancelTraining"
|
||||
v-if="form.processing"
|
||||
>Annuler</Button
|
||||
>
|
||||
</Transition>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
@@ -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,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[];
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { Head } from '@inertiajs/vue3';
|
||||
import { trackUmamiEvent } from '@jaseeey/vue-umami-plugin';
|
||||
import { BookOpenText } from '@lucide/vue';
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
@@ -11,11 +13,23 @@ import {
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
ScatterController,
|
||||
LineController,
|
||||
BarController,
|
||||
LogarithmicScale,
|
||||
|
||||
} from 'chart.js';
|
||||
import { ArrowDown, ArrowUp } from 'lucide-vue-next';
|
||||
import { computed, nextTick, ref, watch } from 'vue';
|
||||
import HelpText from '@/components/HelpText.vue';
|
||||
import LinkHeader from '@/components/LinkHeader.vue';
|
||||
import Button from '@/components/ui/button/Button.vue';
|
||||
import Drawer from '@/components/ui/drawer/Drawer.vue';
|
||||
import DrawerContent from '@/components/ui/drawer/DrawerContent.vue';
|
||||
import DrawerTitle from '@/components/ui/drawer/DrawerTitle.vue';
|
||||
import DrawerTrigger from '@/components/ui/drawer/DrawerTrigger.vue';
|
||||
import Kbd from '@/components/ui/kbd/Kbd.vue';
|
||||
import KbdGroup from '@/components/ui/kbd/KbdGroup.vue';
|
||||
import ScrollArea from '@/components/ui/scroll-area/ScrollArea.vue';
|
||||
import {
|
||||
Tooltip as UiTooltip,
|
||||
@@ -29,14 +43,6 @@ import IterationTable from '../components/IterationTable.vue';
|
||||
import PerceptronDecisionGraph from '../components/PerceptronDecisionGraph.vue';
|
||||
import PerceptronIterationsErrorsGraph from '../components/PerceptronIterationsErrorsGraph.vue';
|
||||
import PerceptronSetup from '../components/PerceptronSetup.vue';
|
||||
import HelpText from '@/components/HelpText.vue';
|
||||
import { BookOpenText } from '@lucide/vue';
|
||||
import Drawer from '@/components/ui/drawer/Drawer.vue';
|
||||
import DrawerTrigger from '@/components/ui/drawer/DrawerTrigger.vue';
|
||||
import DrawerContent from '@/components/ui/drawer/DrawerContent.vue';
|
||||
import DrawerTitle from '@/components/ui/drawer/DrawerTitle.vue';
|
||||
import KbdGroup from '@/components/ui/kbd/KbdGroup.vue';
|
||||
import Kbd from '@/components/ui/kbd/Kbd.vue';
|
||||
|
||||
ChartJS.register(
|
||||
Title,
|
||||
@@ -47,6 +53,10 @@ ChartJS.register(
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
ScatterController,
|
||||
LineController,
|
||||
BarController,
|
||||
LogarithmicScale
|
||||
);
|
||||
ChartJS.defaults.font.size = 16;
|
||||
ChartJS.defaults.color = '#FFF';
|
||||
@@ -137,6 +147,7 @@ const handleDrawerOpenChange = async (open: boolean) => {
|
||||
|
||||
if (open) {
|
||||
await restoreHelpScroll();
|
||||
trackUmamiEvent('wiki-opened', { perceptronType: props.type });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -1,31 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" @class(['dark' => ($appearance ?? 'system') == 'dark'])>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
{{-- Inline script to detect system dark mode preference and apply it immediately --}}
|
||||
<script>
|
||||
(function() {
|
||||
const appearance = '{{ $appearance ?? "system" }}';
|
||||
|
||||
if (appearance === 'system') {
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
if (prefersDark) {
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{{-- Anonymous analytics --}}
|
||||
<script defer src="https://analytics.matthiasg.dev/recorder.js" data-website-id="e616767f-e0d3-4ce9-b551-33c7c3806fc0"></script>
|
||||
|
||||
{{-- Inline style to set the HTML background color based on our theme in app.css --}}
|
||||
<style>
|
||||
html {
|
||||
background-color: oklch(1 0 0);
|
||||
}
|
||||
|
||||
html.dark {
|
||||
background-color: oklch(0.145 0 0);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,2 +1,8 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\PerceptronController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::post('perceptron/cancel', [PerceptronController::class, 'cancel'])
|
||||
->name('perceptron.cancel');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user