Compare commits
27 Commits
5391eb465f
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 0876588550 | |||
| a37afcec07 | |||
| b052d792f8 | |||
| 5880024933 | |||
| a92a47288c | |||
| bcaf334380 | |||
| dea908c63e | |||
| 236fa503fb | |||
| 0f92af4a1e | |||
| ef90236adc | |||
| 9d4b02fab5 | |||
| 88a932391b | |||
| abb16aa6c1 | |||
| 29498e45ac | |||
| af67830fbb | |||
| 42e07de287 | |||
| 47991fe736 | |||
| 03d8688f56 | |||
| 72775d09ef | |||
| 87aa942e0b | |||
| 8af26db16c | |||
| 4717254da4 | |||
| 977c259cb9 | |||
| e3b656a036 | |||
| 2d1c5e6397 | |||
| a99108649c | |||
| 946d6b0e68 |
63
.github/workflows/lint.yml
vendored
63
.github/workflows/lint.yml
vendored
@@ -20,30 +20,59 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
quality:
|
quality:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# Cache Composer
|
||||||
|
# -------------------------
|
||||||
|
- name: Cache Composer dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.composer/cache
|
||||||
|
key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
composer-${{ runner.os }}-
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# Cache Node
|
||||||
|
# -------------------------
|
||||||
|
- name: Cache Node dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.npm
|
||||||
|
key: node-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
node-${{ runner.os }}-
|
||||||
|
|
||||||
- name: Setup PHP
|
- name: Setup PHP
|
||||||
uses: shivammathur/setup-php@v2
|
uses: shivammathur/setup-php@v2
|
||||||
with:
|
with:
|
||||||
php-version: '8.4'
|
php-version: '8.4'
|
||||||
|
coverage: none
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# Install dependencies
|
||||||
|
# -------------------------
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: |
|
run: |
|
||||||
composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
|
composer install --no-interaction --prefer-dist --no-progress --no-scripts
|
||||||
npm install
|
npm ci
|
||||||
|
|
||||||
- name: Run Pint
|
# -------------------------
|
||||||
run: composer lint
|
# Run linters in parallel
|
||||||
|
# -------------------------
|
||||||
- name: Format Frontend
|
- name: Run linters
|
||||||
run: npm run format
|
run: |
|
||||||
|
composer lint &
|
||||||
- name: Lint Frontend
|
npm run format &
|
||||||
run: npm run lint
|
npm run lint &
|
||||||
|
wait
|
||||||
# - name: Commit Changes
|
|
||||||
# uses: stefanzweifel/git-auto-commit-action@v7
|
|
||||||
# with:
|
|
||||||
# commit_message: fix code style
|
|
||||||
# commit_options: '--no-verify'
|
|
||||||
|
|||||||
56
.github/workflows/tests.yml
vendored
56
.github/workflows/tests.yml
vendored
@@ -25,32 +25,66 @@ jobs:
|
|||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# Cache Composer
|
||||||
|
# -------------------------
|
||||||
|
- name: Cache Composer dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.composer/cache
|
||||||
|
key: composer-${{ runner.os }}-${{ matrix.php-version }}-${{ hashFiles('**/composer.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
composer-${{ runner.os }}-${{ matrix.php-version }}-
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# Cache Node
|
||||||
|
# -------------------------
|
||||||
|
- name: Cache Node dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.npm
|
||||||
|
key: node-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
node-${{ runner.os }}-
|
||||||
|
|
||||||
- name: Setup PHP
|
- name: Setup PHP
|
||||||
uses: shivammathur/setup-php@v2
|
uses: shivammathur/setup-php@v2
|
||||||
with:
|
with:
|
||||||
php-version: ${{ matrix.php-version }}
|
php-version: ${{ matrix.php-version }}
|
||||||
tools: composer:v2
|
tools: composer:v2
|
||||||
coverage: xdebug
|
coverage: none
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '22'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# Install dependencies
|
||||||
|
# -------------------------
|
||||||
- name: Install Node Dependencies
|
- name: Install Node Dependencies
|
||||||
run: npm i
|
run: npm ci
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install PHP Dependencies
|
||||||
run: composer install --no-interaction --prefer-dist --optimize-autoloader
|
run: composer install --no-interaction --prefer-dist --optimize-autoloader --no-progress
|
||||||
|
|
||||||
- name: Copy Environment File
|
# -------------------------
|
||||||
run: cp .env.example .env
|
# Laravel setup
|
||||||
|
# -------------------------
|
||||||
- name: Generate Application Key
|
- name: Prepare environment
|
||||||
run: php artisan key:generate
|
run: |
|
||||||
|
cp .env.example .env
|
||||||
|
php artisan key:generate
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# Build (optional – remove if not needed for tests)
|
||||||
|
# -------------------------
|
||||||
- name: Build Assets
|
- name: Build Assets
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: Tests
|
# -------------------------
|
||||||
run: ./vendor/bin/phpunit
|
# Run tests (parallel)
|
||||||
|
# -------------------------
|
||||||
|
- name: Run Tests
|
||||||
|
run: php artisan test --parallel
|
||||||
|
|||||||
52
README.md
Normal file
52
README.md
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# Perceptron and neuronal networks
|
||||||
|
|
||||||
|
Using Laravel and Vue JS
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Install PHP, Composer and NodeJs
|
||||||
|
- With Herd
|
||||||
|
<https://herd.laravel.com/windows>
|
||||||
|
|
||||||
|
- Using a single command from the [Laravel installation page](https://laravel.com/docs/12.x/installation)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Run as administrator...
|
||||||
|
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://php.new/install/windows/8.4'))
|
||||||
|
```
|
||||||
|
|
||||||
|
- Manually :
|
||||||
|
1. PHP
|
||||||
|
<https://www.php.net/downloads.php>
|
||||||
|
2. Composer
|
||||||
|
<https://getcomposer.org/download/>
|
||||||
|
3. NodeJs (Node + NPM)
|
||||||
|
<https://nodejs.org/en/download>
|
||||||
|
|
||||||
|
2. Setup project and install dependencies
|
||||||
|
|
||||||
|
```shell
|
||||||
|
composer run setup
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the project
|
||||||
|
|
||||||
|
There is a script inside `composer.json` that will launch each part of the application in parallel.
|
||||||
|
|
||||||
|
To run this script :
|
||||||
|
|
||||||
|
```shell
|
||||||
|
composer run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
And go with your favorite browser to <http://127.0.0.1:8000/>
|
||||||
|
|
||||||
|
### Nothing is happening when I press "Lancer"
|
||||||
|
|
||||||
|
Look a the output of `composer run dev`, if you see that reverb is still sending events, that means you need to clear the job queue in a new terminal with the command
|
||||||
|
|
||||||
|
```shell
|
||||||
|
php artisan queue:clear
|
||||||
|
```
|
||||||
|
|
||||||
|
You then need to repress the "Lancer" button.
|
||||||
@@ -21,8 +21,7 @@ class PerceptronInitialization implements ShouldBroadcast
|
|||||||
public ActivationsFunctions $activationFunction,
|
public ActivationsFunctions $activationFunction,
|
||||||
public string $sessionId,
|
public string $sessionId,
|
||||||
public string $trainingId,
|
public string $trainingId,
|
||||||
)
|
) {
|
||||||
{
|
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,7 +33,7 @@ class PerceptronInitialization implements ShouldBroadcast
|
|||||||
public function broadcastOn(): array
|
public function broadcastOn(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
new Channel($this->sessionId . '-perceptron-training'),
|
new Channel($this->sessionId.'-perceptron-training'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ namespace App\Events;
|
|||||||
|
|
||||||
use Illuminate\Broadcasting\Channel;
|
use Illuminate\Broadcasting\Channel;
|
||||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||||
use Illuminate\Broadcasting\PresenceChannel;
|
|
||||||
use Illuminate\Broadcasting\PrivateChannel;
|
|
||||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||||
use Illuminate\Foundation\Events\Dispatchable;
|
use Illuminate\Foundation\Events\Dispatchable;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
@@ -21,8 +19,7 @@ class PerceptronTrainingEnded implements ShouldBroadcast
|
|||||||
public string $reason,
|
public string $reason,
|
||||||
public string $sessionId,
|
public string $sessionId,
|
||||||
public string $trainingId,
|
public string $trainingId,
|
||||||
)
|
) {
|
||||||
{
|
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,7 +31,7 @@ class PerceptronTrainingEnded implements ShouldBroadcast
|
|||||||
public function broadcastOn(): array
|
public function broadcastOn(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
new Channel($this->sessionId . '-perceptron-training'),
|
new Channel($this->sessionId.'-perceptron-training'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ class PerceptronTrainingIteration implements ShouldBroadcast
|
|||||||
public array $iterations, // ["epoch" => int, "exampleIndex" => int, "error" => float, "synaptic_weights" => array]
|
public array $iterations, // ["epoch" => int, "exampleIndex" => int, "error" => float, "synaptic_weights" => array]
|
||||||
public string $sessionId,
|
public string $sessionId,
|
||||||
public string $trainingId,
|
public string $trainingId,
|
||||||
)
|
) {
|
||||||
{
|
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,7 +32,7 @@ class PerceptronTrainingIteration implements ShouldBroadcast
|
|||||||
{
|
{
|
||||||
// Log::debug("Broadcasting on channel: " . $this->sessionId . '-perceptron-training');
|
// Log::debug("Broadcasting on channel: " . $this->sessionId . '-perceptron-training');
|
||||||
return [
|
return [
|
||||||
new Channel($this->sessionId . '-perceptron-training'),
|
new Channel($this->sessionId.'-perceptron-training'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,16 @@
|
|||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Events\PerceptronInitialization;
|
use App\Events\PerceptronInitialization;
|
||||||
use App\Models\GradientDescentPerceptronTraining;
|
use App\Models\NetworksTraining\ADALINEPerceptronTraining;
|
||||||
use App\Models\SimpleBinaryPerceptronTraining;
|
use App\Models\NetworksTraining\GradientDescentPerceptronTraining;
|
||||||
use App\Services\DataSetReader;
|
use App\Models\NetworksTraining\SimpleBinaryPerceptronTraining;
|
||||||
use App\Services\ISynapticWeightsProvider;
|
use App\Services\DatasetReader\IDataSetReader;
|
||||||
use App\Services\PerceptronIterationEventBuffer;
|
use App\Services\DatasetReader\LinearOrderDataSetReader;
|
||||||
use App\Services\PerceptronLimitedEpochEventBuffer;
|
use App\Services\DatasetReader\RandomOrderDataSetReader;
|
||||||
use App\Services\ZeroSynapticWeights;
|
use App\Services\IterationEventBuffer\PerceptronIterationEventBuffer;
|
||||||
|
use App\Services\IterationEventBuffer\PerceptronLimitedEpochEventBuffer;
|
||||||
|
use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider;
|
||||||
|
use App\Services\SynapticWeightsProvider\ZeroSynapticWeights;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class PerceptronController extends Controller
|
class PerceptronController extends Controller
|
||||||
@@ -23,15 +26,16 @@ class PerceptronController extends Controller
|
|||||||
|
|
||||||
$learningRate = 0.01;
|
$learningRate = 0.01;
|
||||||
$maxIterations = 200;
|
$maxIterations = 200;
|
||||||
$minError = 0.6;
|
$minError = 0.1;
|
||||||
|
|
||||||
switch ($perceptronType) {
|
switch ($perceptronType) {
|
||||||
case 'simple':
|
case 'simple':
|
||||||
$learningRate = 0.015;
|
$learningRate = 0.015;
|
||||||
$maxIterations = 150;
|
$maxIterations = 150;
|
||||||
break;
|
break;
|
||||||
case 'gradientdescent':
|
case 'gradientdescent' || 'adaline':
|
||||||
$learningRate = 0.00003;
|
$learningRate = 0.00003;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return inertia('PerceptronViewer', [
|
return inertia('PerceptronViewer', [
|
||||||
@@ -53,7 +57,7 @@ class PerceptronController extends Controller
|
|||||||
if (pathinfo($file, PATHINFO_EXTENSION) === 'csv') {
|
if (pathinfo($file, PATHINFO_EXTENSION) === 'csv') {
|
||||||
$dataset = [];
|
$dataset = [];
|
||||||
$dataset['label'] = str_replace('.csv', '', $file);
|
$dataset['label'] = str_replace('.csv', '', $file);
|
||||||
$dataSetReader = new DataSetReader($dataSetsDirectory . '/' . $file);
|
$dataSetReader = new LinearOrderDataSetReader($dataSetsDirectory.'/'.$file);
|
||||||
$dataset['data'] = [];
|
$dataset['data'] = [];
|
||||||
switch (count($dataSetReader->lines[0])) {
|
switch (count($dataSetReader->lines[0])) {
|
||||||
case 3:
|
case 3:
|
||||||
@@ -84,6 +88,11 @@ class PerceptronController extends Controller
|
|||||||
switch ($perceptronType) {
|
switch ($perceptronType) {
|
||||||
case 'gradientdescent':
|
case 'gradientdescent':
|
||||||
$dataset['defaultLearningRate'] = 0.3;
|
$dataset['defaultLearningRate'] = 0.3;
|
||||||
|
$dataset['defaultMinError'] = 0.125;
|
||||||
|
break;
|
||||||
|
case 'adaline':
|
||||||
|
$dataset['defaultLearningRate'] = 0.05;
|
||||||
|
$dataset['defaultMinError'] = 0.125;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -92,26 +101,27 @@ class PerceptronController extends Controller
|
|||||||
case 'simple':
|
case 'simple':
|
||||||
$dataset['defaultLearningRate'] = 0.015;
|
$dataset['defaultLearningRate'] = 0.015;
|
||||||
break;
|
break;
|
||||||
case 'gradientdescent':
|
case 'gradientdescent' || 'adaline':
|
||||||
$dataset['defaultLearningRate'] = 0.001;
|
$dataset['defaultLearningRate'] = 0.001;
|
||||||
$dataset['defaultMinError'] = 2.0;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'table_2_11':
|
case 'table_2_11':
|
||||||
$dataset['defaultMinError'] = 1.0;
|
$dataset['defaultMinError'] = 0.02;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
$datasets[] = $dataset;
|
$datasets[] = $dataset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $datasets;
|
return $datasets;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getDataSetReader(string $dataSet): DataSetReader
|
private function getDataSetReader(string $dataSet): IDataSetReader
|
||||||
{
|
{
|
||||||
$dataSetFileName = "data_sets/{$dataSet}.csv";
|
$dataSetFileName = "data_sets/{$dataSet}.csv";
|
||||||
return new DataSetReader($dataSetFileName);
|
|
||||||
|
return new RandomOrderDataSetReader($dataSetFileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function run(Request $request, ISynapticWeightsProvider $synapticWeightsProvider)
|
public function run(Request $request, ISynapticWeightsProvider $synapticWeightsProvider)
|
||||||
@@ -121,30 +131,30 @@ class PerceptronController extends Controller
|
|||||||
$weightInitMethod = $request->input('weight_init_method', 'random');
|
$weightInitMethod = $request->input('weight_init_method', 'random');
|
||||||
$dataSet = $request->input('dataset');
|
$dataSet = $request->input('dataset');
|
||||||
$learningRate = $request->input('learning_rate', 0.015);
|
$learningRate = $request->input('learning_rate', 0.015);
|
||||||
$maxIterations = $request->input('max_iterations', 100);
|
$maxEpochs = $request->input('max_iterations', 100);
|
||||||
$sessionId = $request->input('session_id', session()->getId());
|
$sessionId = $request->input('session_id', session()->getId());
|
||||||
$trainingId = $request->input('training_id');
|
$trainingId = $request->input('training_id');
|
||||||
|
|
||||||
if ($weightInitMethod === 'zeros') {
|
if ($weightInitMethod === 'zeros') {
|
||||||
$synapticWeightsProvider = new ZeroSynapticWeights();
|
$synapticWeightsProvider = new ZeroSynapticWeights;
|
||||||
}
|
}
|
||||||
|
|
||||||
$iterationEventBuffer = new PerceptronIterationEventBuffer($sessionId, $trainingId);
|
$iterationEventBuffer = new PerceptronIterationEventBuffer($sessionId, $trainingId);
|
||||||
if ($maxIterations > config('perceptron.limited_broadcast_iterations')) {
|
if ($maxEpochs > config('perceptron.limited_broadcast_iterations')) {
|
||||||
$iterationsInterval = (int)($maxIterations / config('perceptron.limited_broadcast_iterations'));
|
$iterationsInterval = (int) ($maxEpochs / config('perceptron.limited_broadcast_iterations'));
|
||||||
$iterationEventBuffer = new PerceptronLimitedEpochEventBuffer($sessionId, $trainingId, $iterationsInterval);
|
$iterationEventBuffer = new PerceptronLimitedEpochEventBuffer($sessionId, $trainingId, $iterationsInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
$dataSetReader = $this->getDataSetReader($dataSet);
|
$datasetReader = $this->getDataSetReader($dataSet);
|
||||||
|
|
||||||
|
|
||||||
$networkTraining = match ($perceptronType) {
|
$networkTraining = match ($perceptronType) {
|
||||||
'simple' => new SimpleBinaryPerceptronTraining($dataSetReader, $learningRate, $maxIterations, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId),
|
'simple' => new SimpleBinaryPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId),
|
||||||
'gradientdescent' => new GradientDescentPerceptronTraining($dataSetReader, $learningRate, $maxIterations, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError),
|
'gradientdescent' => new GradientDescentPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError),
|
||||||
|
'adaline' => new ADALINEPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError),
|
||||||
default => null,
|
default => null,
|
||||||
};
|
};
|
||||||
|
|
||||||
event(new PerceptronInitialization($dataSetReader->lines, $networkTraining->activationFunction, $sessionId, $trainingId));
|
event(new PerceptronInitialization($datasetReader->lines, $networkTraining->activationFunction, $sessionId, $trainingId));
|
||||||
|
|
||||||
$networkTraining->start();
|
$networkTraining->start();
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
abstract class Network
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
106
app/Models/NetworksTraining/ADALINEPerceptronTraining.php
Normal file
106
app/Models/NetworksTraining/ADALINEPerceptronTraining.php
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
class ADALINEPerceptronTraining extends NetworkTraining
|
||||||
|
{
|
||||||
|
private Perceptron $perceptron;
|
||||||
|
|
||||||
|
public ActivationsFunctions $activationFunction = ActivationsFunctions::LINEAR;
|
||||||
|
|
||||||
|
private float $epochError;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
IDataSetReader $datasetReader,
|
||||||
|
protected float $learningRate,
|
||||||
|
int $maxEpochs,
|
||||||
|
protected ISynapticWeightsProvider $synapticWeightsProvider,
|
||||||
|
IPerceptronIterationEventBuffer $iterationEventBuffer,
|
||||||
|
string $sessionId,
|
||||||
|
string $trainingId,
|
||||||
|
private float $minError,
|
||||||
|
) {
|
||||||
|
parent::__construct($datasetReader, $maxEpochs, $iterationEventBuffer, $sessionId, $trainingId);
|
||||||
|
$this->perceptron = new GradientDescentPerceptron($synapticWeightsProvider->generate($datasetReader->getInputSize()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function start(): void
|
||||||
|
{
|
||||||
|
$this->epoch = 0;
|
||||||
|
do {
|
||||||
|
$this->epochError = 0;
|
||||||
|
$this->epoch++;
|
||||||
|
|
||||||
|
$inputsForCurrentEpoch = [];
|
||||||
|
|
||||||
|
while ($nextRow = $this->datasetReader->getNextLine()) {
|
||||||
|
$inputsForCurrentEpoch[] = $nextRow;
|
||||||
|
$inputs = array_slice($nextRow, 0, -1);
|
||||||
|
$correctOutput = (float) end($nextRow);
|
||||||
|
|
||||||
|
$iterationError = $this->iterationFunction($inputs, $correctOutput);
|
||||||
|
|
||||||
|
// Synaptic weights correction after each example
|
||||||
|
$synaptic_weights = $this->perceptron->getSynapticWeights();
|
||||||
|
$inputs_with_bias = array_merge([1], $inputs); // Add bias input
|
||||||
|
$new_weights = array_map(
|
||||||
|
fn ($weight, $weightIndex) => $weight + ($this->learningRate * $iterationError * $inputs_with_bias[$weightIndex]),
|
||||||
|
$synaptic_weights,
|
||||||
|
array_keys($synaptic_weights)
|
||||||
|
);
|
||||||
|
$this->perceptron->setSynapticWeights($new_weights);
|
||||||
|
|
||||||
|
// Broadcast the training iteration event
|
||||||
|
$this->addIterationToBuffer($iterationError, [[$this->perceptron->getSynapticWeights()]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculte the average error for the epoch with the last synaptic weights
|
||||||
|
foreach ($inputsForCurrentEpoch as $inputsWithLabel) {
|
||||||
|
$inputs = array_slice($inputsWithLabel, 0, -1);
|
||||||
|
$correctOutput = (float) end($inputsWithLabel);
|
||||||
|
$output = $this->perceptron->test($inputs);
|
||||||
|
$iterationError = $correctOutput - $output;
|
||||||
|
$this->epochError += ($iterationError ** 2) / 2; // Squared error for the example
|
||||||
|
}
|
||||||
|
$this->epochError /= $this->datasetReader->getEpochExamplesCount(); // Average error for the epoch
|
||||||
|
|
||||||
|
$this->datasetReader->reset(); // Reset the dataset for the next iteration
|
||||||
|
} while ($this->epoch < $this->maxEpochs && ! $this->stopCondition());
|
||||||
|
|
||||||
|
$this->iterationEventBuffer->flush(); // Ensure all iterations are sent to the frontend
|
||||||
|
|
||||||
|
$this->checkPassedMaxIterations($this->epochError);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function stopCondition(): bool
|
||||||
|
{
|
||||||
|
$condition = $this->epochError <= $this->minError;
|
||||||
|
if ($condition === true) {
|
||||||
|
event(new PerceptronTrainingEnded('Le perceptron à atteint l\'erreur minimale', $this->sessionId, $this->trainingId));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function iterationFunction(array $inputs, int $correctOutput)
|
||||||
|
{
|
||||||
|
$output = $this->perceptron->test($inputs);
|
||||||
|
|
||||||
|
$error = $correctOutput - $output;
|
||||||
|
|
||||||
|
return $error;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSynapticWeights(): array
|
||||||
|
{
|
||||||
|
return [[$this->perceptron->getSynapticWeights()]];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models\NetworksTraining;
|
||||||
|
|
||||||
use App\Events\PerceptronTrainingEnded;
|
use App\Events\PerceptronTrainingEnded;
|
||||||
use App\Services\DataSetReader;
|
use App\Models\ActivationsFunctions;
|
||||||
use App\Services\IPerceptronIterationEventBuffer;
|
use App\Models\Perceptrons\GradientDescentPerceptron;
|
||||||
use App\Services\ISynapticWeightsProvider;
|
use App\Models\Perceptrons\Perceptron;
|
||||||
use App\Services\PerceptronIterationEventBuffer;
|
use App\Services\DatasetReader\IDataSetReader;
|
||||||
|
use App\Services\IterationEventBuffer\IPerceptronIterationEventBuffer;
|
||||||
|
use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider;
|
||||||
|
|
||||||
class GradientDescentPerceptronTraining extends NetworkTraining
|
class GradientDescentPerceptronTraining extends NetworkTraining
|
||||||
{
|
{
|
||||||
@@ -17,7 +19,7 @@ class GradientDescentPerceptronTraining extends NetworkTraining
|
|||||||
private float $epochError;
|
private float $epochError;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
DataSetReader $datasetReader,
|
IDataSetReader $datasetReader,
|
||||||
protected float $learningRate,
|
protected float $learningRate,
|
||||||
int $maxEpochs,
|
int $maxEpochs,
|
||||||
protected ISynapticWeightsProvider $synapticWeightsProvider,
|
protected ISynapticWeightsProvider $synapticWeightsProvider,
|
||||||
@@ -38,7 +40,7 @@ class GradientDescentPerceptronTraining extends NetworkTraining
|
|||||||
$epochCorrectorPerWeight = [];
|
$epochCorrectorPerWeight = [];
|
||||||
$this->epoch++;
|
$this->epoch++;
|
||||||
|
|
||||||
while ($nextRow = $this->datasetReader->getRandomLine()) {
|
while ($nextRow = $this->datasetReader->getNextLine()) {
|
||||||
$inputs = array_slice($nextRow, 0, -1);
|
$inputs = array_slice($nextRow, 0, -1);
|
||||||
$correctOutput = (float) end($nextRow);
|
$correctOutput = (float) end($nextRow);
|
||||||
|
|
||||||
@@ -55,17 +57,19 @@ class GradientDescentPerceptronTraining extends NetworkTraining
|
|||||||
$this->addIterationToBuffer($iterationError, [[$this->perceptron->getSynapticWeights()]]);
|
$this->addIterationToBuffer($iterationError, [[$this->perceptron->getSynapticWeights()]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->epochError /= $this->datasetReader->getEpochExamplesCount(); // Average error for the epoch
|
||||||
|
|
||||||
// Synaptic weights correction after each epoch
|
// Synaptic weights correction after each epoch
|
||||||
$synaptic_weights = $this->perceptron->getSynapticWeights();
|
$synaptic_weights = $this->perceptron->getSynapticWeights();
|
||||||
$new_weights = array_map(
|
$new_weights = array_map(
|
||||||
fn($weight, $weightIndex) => $weight + $this->learningRate * array_sum($epochCorrectorPerWeight[$weightIndex]),
|
fn ($weight, $weightIndex) => $weight + $this->learningRate * array_sum($epochCorrectorPerWeight[$weightIndex]),
|
||||||
$synaptic_weights,
|
$synaptic_weights,
|
||||||
array_keys($synaptic_weights)
|
array_keys($synaptic_weights)
|
||||||
);
|
);
|
||||||
$this->perceptron->setSynapticWeights($new_weights);
|
$this->perceptron->setSynapticWeights($new_weights);
|
||||||
|
|
||||||
$this->datasetReader->reset(); // Reset the dataset for the next iteration
|
$this->datasetReader->reset(); // Reset the dataset for the next iteration
|
||||||
} while ($this->epoch < $this->maxEpochs && !$this->stopCondition());
|
} while ($this->epoch < $this->maxEpochs && ! $this->stopCondition());
|
||||||
|
|
||||||
$this->iterationEventBuffer->flush(); // Ensure all iterations are sent to the frontend
|
$this->iterationEventBuffer->flush(); // Ensure all iterations are sent to the frontend
|
||||||
|
|
||||||
@@ -74,10 +78,11 @@ class GradientDescentPerceptronTraining extends NetworkTraining
|
|||||||
|
|
||||||
protected function stopCondition(): bool
|
protected function stopCondition(): bool
|
||||||
{
|
{
|
||||||
$condition = $this->epochError <= $this->minError && $this->perceptron->getSynapticWeights() !== [[0.0, 0.0, 0.0]];
|
$condition = $this->epochError <= $this->minError;
|
||||||
if ($condition === true) {
|
if ($condition === true) {
|
||||||
event(new PerceptronTrainingEnded('Le perceptron à atteint l\'erreur minimale', $this->sessionId, $this->trainingId));
|
event(new PerceptronTrainingEnded('Le perceptron à atteint l\'erreur minimale', $this->sessionId, $this->trainingId));
|
||||||
}
|
}
|
||||||
|
|
||||||
return $condition;
|
return $condition;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,4 +94,9 @@ class GradientDescentPerceptronTraining extends NetworkTraining
|
|||||||
|
|
||||||
return $error;
|
return $error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getSynapticWeights(): array
|
||||||
|
{
|
||||||
|
return [[$this->perceptron->getSynapticWeights()]];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models\NetworksTraining;
|
||||||
|
|
||||||
use App\Events\PerceptronTrainingEnded;
|
use App\Events\PerceptronTrainingEnded;
|
||||||
use App\Services\DataSetReader;
|
use App\Models\ActivationsFunctions;
|
||||||
use App\Services\IPerceptronIterationEventBuffer;
|
use App\Services\DatasetReader\IDataSetReader;
|
||||||
|
use App\Services\IterationEventBuffer\IPerceptronIterationEventBuffer;
|
||||||
|
|
||||||
abstract class NetworkTraining
|
abstract class NetworkTraining
|
||||||
{
|
{
|
||||||
@@ -12,23 +13,23 @@ abstract class NetworkTraining
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @abstract
|
* @abstract
|
||||||
* @var ActivationsFunctions
|
|
||||||
*/
|
*/
|
||||||
public ActivationsFunctions $activationFunction;
|
public ActivationsFunctions $activationFunction;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
protected DataSetReader $datasetReader,
|
protected IDataSetReader $datasetReader,
|
||||||
protected int $maxEpochs,
|
protected int $maxEpochs,
|
||||||
protected IPerceptronIterationEventBuffer $iterationEventBuffer,
|
protected IPerceptronIterationEventBuffer $iterationEventBuffer,
|
||||||
protected string $sessionId,
|
protected string $sessionId,
|
||||||
protected string $trainingId,
|
protected string $trainingId,
|
||||||
) {
|
) {}
|
||||||
}
|
|
||||||
|
abstract public function start(): void;
|
||||||
|
|
||||||
abstract public function start() : void;
|
|
||||||
abstract protected function stopCondition(): bool;
|
abstract protected function stopCondition(): bool;
|
||||||
|
|
||||||
protected function checkPassedMaxIterations(?float $finalError) {
|
protected function checkPassedMaxIterations(?float $finalError)
|
||||||
|
{
|
||||||
if ($this->epoch >= $this->maxEpochs) {
|
if ($this->epoch >= $this->maxEpochs) {
|
||||||
$message = 'Le nombre maximal d\'epoch a été atteint';
|
$message = 'Le nombre maximal d\'epoch a été atteint';
|
||||||
if ($finalError) {
|
if ($finalError) {
|
||||||
@@ -39,7 +40,15 @@ abstract class NetworkTraining
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function addIterationToBuffer(float $error, array $synapticWeights) {
|
protected function addIterationToBuffer(float $error, array $synapticWeights)
|
||||||
|
{
|
||||||
$this->iterationEventBuffer->addIteration($this->epoch, $this->datasetReader->getLastReadLineIndex(), $error, $synapticWeights);
|
$this->iterationEventBuffer->addIteration($this->epoch, $this->datasetReader->getLastReadLineIndex(), $error, $synapticWeights);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getEpoch(): int
|
||||||
|
{
|
||||||
|
return $this->epoch;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract public function getSynapticWeights(): array;
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,19 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models\NetworksTraining;
|
||||||
|
|
||||||
use App\Events\PerceptronTrainingEnded;
|
use App\Events\PerceptronTrainingEnded;
|
||||||
use App\Services\DataSetReader;
|
use App\Models\ActivationsFunctions;
|
||||||
use App\Services\IPerceptronIterationEventBuffer;
|
use App\Models\Perceptrons\Perceptron;
|
||||||
use App\Services\ISynapticWeightsProvider;
|
use App\Models\Perceptrons\SimpleBinaryPerceptron;
|
||||||
|
use App\Services\DatasetReader\IDataSetReader;
|
||||||
|
use App\Services\IterationEventBuffer\IPerceptronIterationEventBuffer;
|
||||||
|
use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider;
|
||||||
|
|
||||||
class SimpleBinaryPerceptronTraining extends NetworkTraining
|
class SimpleBinaryPerceptronTraining extends NetworkTraining
|
||||||
{
|
{
|
||||||
private Perceptron $perceptron;
|
private Perceptron $perceptron;
|
||||||
|
|
||||||
private int $iterationErrorCounter = 0;
|
private int $iterationErrorCounter = 0;
|
||||||
|
|
||||||
public ActivationsFunctions $activationFunction = ActivationsFunctions::STEP;
|
public ActivationsFunctions $activationFunction = ActivationsFunctions::STEP;
|
||||||
@@ -17,7 +21,7 @@ class SimpleBinaryPerceptronTraining extends NetworkTraining
|
|||||||
public const MIN_ERROR = 0;
|
public const MIN_ERROR = 0;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
DataSetReader $datasetReader,
|
IDataSetReader $datasetReader,
|
||||||
protected float $learningRate,
|
protected float $learningRate,
|
||||||
int $maxEpochs,
|
int $maxEpochs,
|
||||||
protected ISynapticWeightsProvider $synapticWeightsProvider,
|
protected ISynapticWeightsProvider $synapticWeightsProvider,
|
||||||
@@ -48,7 +52,7 @@ class SimpleBinaryPerceptronTraining extends NetworkTraining
|
|||||||
$this->addIterationToBuffer($error, [[$this->perceptron->getSynapticWeights()]]);
|
$this->addIterationToBuffer($error, [[$this->perceptron->getSynapticWeights()]]);
|
||||||
}
|
}
|
||||||
$this->datasetReader->reset(); // Reset the dataset for the next iteration
|
$this->datasetReader->reset(); // Reset the dataset for the next iteration
|
||||||
} while ($this->epoch < $this->maxEpochs && !$this->stopCondition());
|
} while ($this->epoch < $this->maxEpochs && ! $this->stopCondition());
|
||||||
|
|
||||||
$this->iterationEventBuffer->flush(); // Ensure all iterations are sent to the frontend
|
$this->iterationEventBuffer->flush(); // Ensure all iterations are sent to the frontend
|
||||||
|
|
||||||
@@ -61,6 +65,7 @@ class SimpleBinaryPerceptronTraining extends NetworkTraining
|
|||||||
if ($condition === true) {
|
if ($condition === true) {
|
||||||
event(new PerceptronTrainingEnded('Le perceptron ne commet plus d\'erreurs sur aucune des données', $this->sessionId, $this->trainingId));
|
event(new PerceptronTrainingEnded('Le perceptron ne commet plus d\'erreurs sur aucune des données', $this->sessionId, $this->trainingId));
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->iterationErrorCounter == 0;
|
return $this->iterationErrorCounter == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,9 +81,15 @@ class SimpleBinaryPerceptronTraining extends NetworkTraining
|
|||||||
if ($error !== 0) { // Update synaptic weights if needed
|
if ($error !== 0) { // Update synaptic weights if needed
|
||||||
$synaptic_weights = $this->perceptron->getSynapticWeights();
|
$synaptic_weights = $this->perceptron->getSynapticWeights();
|
||||||
$inputs_with_bias = array_merge([1], $inputs); // Add bias input
|
$inputs_with_bias = array_merge([1], $inputs); // Add bias input
|
||||||
$new_weights = array_map(fn($weight, $input) => $weight + $this->learningRate * $error * $input, $synaptic_weights, $inputs_with_bias);
|
$new_weights = array_map(fn ($weight, $input) => $weight + $this->learningRate * $error * $input, $synaptic_weights, $inputs_with_bias);
|
||||||
$this->perceptron->setSynapticWeights($new_weights);
|
$this->perceptron->setSynapticWeights($new_weights);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $error;
|
return $error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getSynapticWeights(): array
|
||||||
|
{
|
||||||
|
return [[$this->perceptron->getSynapticWeights()]];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models\Perceptrons;
|
||||||
|
|
||||||
class GradientDescentPerceptron extends Perceptron {
|
|
||||||
|
|
||||||
|
class GradientDescentPerceptron extends Perceptron
|
||||||
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
array $synaptic_weights,
|
array $synaptic_weights,
|
||||||
) {
|
) {
|
||||||
@@ -14,5 +14,4 @@ class GradientDescentPerceptron extends Perceptron {
|
|||||||
{
|
{
|
||||||
return $weighted_sum;
|
return $weighted_sum;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models\Perceptrons;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
@@ -17,10 +17,11 @@ abstract class Perceptron extends Model
|
|||||||
$inputs = array_merge([1], $inputs); // Add bias input
|
$inputs = array_merge([1], $inputs); // Add bias input
|
||||||
|
|
||||||
if (count($inputs) !== count($this->synaptic_weights)) { // Check
|
if (count($inputs) !== count($this->synaptic_weights)) { // Check
|
||||||
throw new \InvalidArgumentException("Number of inputs must match number of synaptic weights.");
|
throw new \InvalidArgumentException('Number of inputs must match number of synaptic weights.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$weighted_sum = array_sum(array_map(fn($input, $weight) => $input * $weight, $inputs, $this->synaptic_weights));
|
$weighted_sum = array_sum(array_map(fn ($input, $weight) => $input * $weight, $inputs, $this->synaptic_weights));
|
||||||
|
|
||||||
return $this->activationFunction($weighted_sum);
|
return $this->activationFunction($weighted_sum);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models\Perceptrons;
|
||||||
|
|
||||||
class SimpleBinaryPerceptron extends Perceptron {
|
|
||||||
|
|
||||||
|
class SimpleBinaryPerceptron extends Perceptron
|
||||||
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
array $synaptic_weights,
|
array $synaptic_weights,
|
||||||
) {
|
) {
|
||||||
@@ -14,5 +14,4 @@ class SimpleBinaryPerceptron extends Perceptron {
|
|||||||
{
|
{
|
||||||
return $weighted_sum >= 0.0 ? 1.0 : 0.0;
|
return $weighted_sum >= 0.0 ? 1.0 : 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,6 @@ use Carbon\CarbonImmutable;
|
|||||||
use Illuminate\Support\Facades\Date;
|
use Illuminate\Support\Facades\Date;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
use Illuminate\Validation\Rules\Password;
|
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
use App\Services\ISynapticWeightsProvider;
|
use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider;
|
||||||
use App\Services\RandomSynapticWeights;
|
use App\Services\SynapticWeightsProvider\RandomSynapticWeights;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
class InitialSynapticWeightsProvider extends ServiceProvider
|
class InitialSynapticWeightsProvider extends ServiceProvider
|
||||||
@@ -14,7 +14,7 @@ class InitialSynapticWeightsProvider extends ServiceProvider
|
|||||||
public function register(): void
|
public function register(): void
|
||||||
{
|
{
|
||||||
$this->app->singleton(ISynapticWeightsProvider::class, function ($app) {
|
$this->app->singleton(ISynapticWeightsProvider::class, function ($app) {
|
||||||
return new RandomSynapticWeights();
|
return new RandomSynapticWeights;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Services;
|
namespace App\Services;
|
||||||
|
|
||||||
class CsvReader {
|
class CsvReader
|
||||||
|
{
|
||||||
private $file;
|
private $file;
|
||||||
// private array $headers;
|
// private array $headers;
|
||||||
|
|
||||||
@@ -10,11 +11,10 @@ class CsvReader {
|
|||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public string $filename,
|
public string $filename,
|
||||||
)
|
) {
|
||||||
{
|
$this->file = fopen($filename, 'r');
|
||||||
$this->file = fopen($filename, "r");
|
if (! $this->file) {
|
||||||
if (!$this->file) {
|
throw new \RuntimeException('Failed to open file: '.$filename);
|
||||||
throw new \RuntimeException("Failed to open file: " . $filename);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// $this->headers = $this->readNextLine();
|
// $this->headers = $this->readNextLine();
|
||||||
@@ -22,9 +22,10 @@ class CsvReader {
|
|||||||
|
|
||||||
public function readNextLine(): ?array
|
public function readNextLine(): ?array
|
||||||
{
|
{
|
||||||
if (($data = fgetcsv($this->file, 1000, ",")) !== FALSE) {
|
if (($data = fgetcsv($this->file, 1000, ',')) !== false) {
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null; // End of file or error
|
return null; // End of file or error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
16
app/Services/DatasetReader/IDataSetReader.php
Normal file
16
app/Services/DatasetReader/IDataSetReader.php
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\DatasetReader;
|
||||||
|
|
||||||
|
interface IDataSetReader
|
||||||
|
{
|
||||||
|
public function getNextLine(): ?array;
|
||||||
|
|
||||||
|
public function getInputSize(): int;
|
||||||
|
|
||||||
|
public function reset(): void;
|
||||||
|
|
||||||
|
public function getLastReadLineIndex(): int;
|
||||||
|
|
||||||
|
public function getEpochExamplesCount(): int;
|
||||||
|
}
|
||||||
66
app/Services/DatasetReader/LinearOrderDataSetReader.php
Normal file
66
app/Services/DatasetReader/LinearOrderDataSetReader.php
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\DatasetReader;
|
||||||
|
|
||||||
|
use App\Services\CsvReader;
|
||||||
|
|
||||||
|
class LinearOrderDataSetReader implements IDataSetReader
|
||||||
|
{
|
||||||
|
public array $lines = [];
|
||||||
|
|
||||||
|
private array $currentLines = [];
|
||||||
|
|
||||||
|
private int $lastReadLineIndex = -1;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public string $filename,
|
||||||
|
) {
|
||||||
|
// For now, we only support CSV files, so we can delegate to CsvReader
|
||||||
|
$csvReader = new CsvReader($filename);
|
||||||
|
$this->readEntireFile($csvReader);
|
||||||
|
$this->reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function readEntireFile(CsvReader $reader): void
|
||||||
|
{
|
||||||
|
while ($line = $reader->readNextLine()) {
|
||||||
|
$newLine = [];
|
||||||
|
foreach ($line as $value) { // Transform to float
|
||||||
|
$newLine[] = (float) $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->lines[] = $newLine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getNextLine(): ?array
|
||||||
|
{
|
||||||
|
if (! isset($this->currentLines[0])) {
|
||||||
|
return null; // No more lines to read
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->lastReadLineIndex = array_search($this->currentLines[0], $this->lines, true);
|
||||||
|
|
||||||
|
return array_shift($this->currentLines);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getInputSize(): int
|
||||||
|
{
|
||||||
|
return count($this->lines[0]) - 1; // Don't count the label
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reset(): void
|
||||||
|
{
|
||||||
|
$this->currentLines = $this->lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLastReadLineIndex(): int
|
||||||
|
{
|
||||||
|
return $this->lastReadLineIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEpochExamplesCount(): int
|
||||||
|
{
|
||||||
|
return count($this->lines);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Services;
|
namespace App\Services\DatasetReader;
|
||||||
|
|
||||||
class DataSetReader {
|
use App\Services\CsvReader;
|
||||||
|
|
||||||
|
class RandomOrderDataSetReader implements IDataSetReader
|
||||||
|
{
|
||||||
public array $lines = [];
|
public array $lines = [];
|
||||||
|
|
||||||
private array $currentLines = [];
|
private array $currentLines = [];
|
||||||
|
|
||||||
private int $lastReadLineIndex = -1;
|
private int $lastReadLineIndex = -1;
|
||||||
@@ -25,16 +29,11 @@ class DataSetReader {
|
|||||||
$newLine[] = (float) $value;
|
$newLine[] = (float) $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the dataset is for regression, we add a fake label of 0
|
|
||||||
if (count($newLine) === 2) {
|
|
||||||
$newLine[] = 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->lines[] = $newLine;
|
$this->lines[] = $newLine;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getRandomLine(): array | null
|
public function getNextLine(): ?array
|
||||||
{
|
{
|
||||||
if (empty($this->currentLines)) {
|
if (empty($this->currentLines)) {
|
||||||
return null; // No more lines to read
|
return null; // No more lines to read
|
||||||
@@ -51,16 +50,6 @@ class DataSetReader {
|
|||||||
return $randomLine;
|
return $randomLine;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getNextLine(): array | null {
|
|
||||||
if (!isset($this->currentLines[0])) {
|
|
||||||
return null; // No more lines to read
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->lastReadLineIndex = array_search($this->currentLines[0], $this->lines, true);
|
|
||||||
|
|
||||||
return array_shift($this->currentLines);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getInputSize(): int
|
public function getInputSize(): int
|
||||||
{
|
{
|
||||||
return count($this->lines[0]) - 1; // Don't count the label
|
return count($this->lines[0]) - 1; // Don't count the label
|
||||||
@@ -75,4 +64,9 @@ class DataSetReader {
|
|||||||
{
|
{
|
||||||
return $this->lastReadLineIndex;
|
return $this->lastReadLineIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getEpochExamplesCount(): int
|
||||||
|
{
|
||||||
|
return count($this->lines);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Services;
|
|
||||||
|
|
||||||
interface IPerceptronIterationEventBuffer {
|
|
||||||
|
|
||||||
public function flush(): void ;
|
|
||||||
|
|
||||||
public function addIteration(int $iteration, int $exampleIndex, float $error, array $synaptic_weights): void ;
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Services;
|
|
||||||
|
|
||||||
interface ISynapticWeightsProvider {
|
|
||||||
public function generate(int $input_size): array;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\IterationEventBuffer;
|
||||||
|
|
||||||
|
interface IPerceptronIterationEventBuffer
|
||||||
|
{
|
||||||
|
public function flush(): void;
|
||||||
|
|
||||||
|
public function addIteration(int $iteration, int $exampleIndex, float $error, array $synaptic_weights): void;
|
||||||
|
}
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Services;
|
namespace App\Services\IterationEventBuffer;
|
||||||
|
|
||||||
class PerceptronIterationEventBuffer implements IPerceptronIterationEventBuffer {
|
class PerceptronIterationEventBuffer implements IPerceptronIterationEventBuffer
|
||||||
|
{
|
||||||
private $data;
|
private $data;
|
||||||
|
|
||||||
private int $nextSizeIncreaseThreshold;
|
private int $nextSizeIncreaseThreshold;
|
||||||
|
|
||||||
private int $underSizeIncreaseCount = 0;
|
private int $underSizeIncreaseCount = 0;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
@@ -17,24 +20,25 @@ class PerceptronIterationEventBuffer implements IPerceptronIterationEventBuffer
|
|||||||
$this->nextSizeIncreaseThreshold = $sizeIncreaseStart;
|
$this->nextSizeIncreaseThreshold = $sizeIncreaseStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function flush(): void {
|
public function flush(): void
|
||||||
|
{
|
||||||
event(new \App\Events\PerceptronTrainingIteration($this->data, $this->sessionId, $this->trainingId));
|
event(new \App\Events\PerceptronTrainingIteration($this->data, $this->sessionId, $this->trainingId));
|
||||||
$this->data = [];
|
$this->data = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function addIteration(int $epoch, int $exampleIndex, float $error, array $synaptic_weights): void {
|
public function addIteration(int $epoch, int $exampleIndex, float $error, array $synaptic_weights): void
|
||||||
|
{
|
||||||
$this->data[] = [
|
$this->data[] = [
|
||||||
"epoch" => $epoch,
|
'epoch' => $epoch,
|
||||||
"exampleIndex" => $exampleIndex,
|
'exampleIndex' => $exampleIndex,
|
||||||
"error" => $error,
|
'error' => $error,
|
||||||
"weights" => $synaptic_weights,
|
'weights' => $synaptic_weights,
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($this->underSizeIncreaseCount <= $this->sizeIncreaseStart) { // We can still send a single date because we are under the increase start threshold
|
if ($this->underSizeIncreaseCount <= $this->sizeIncreaseStart) { // We can still send a single date because we are under the increase start threshold
|
||||||
$this->underSizeIncreaseCount++;
|
$this->underSizeIncreaseCount++;
|
||||||
$this->flush();
|
$this->flush();
|
||||||
}
|
} elseif (count($this->data) >= $this->nextSizeIncreaseThreshold) {
|
||||||
else if (count($this->data) >= $this->nextSizeIncreaseThreshold) {
|
|
||||||
$this->flush();
|
$this->flush();
|
||||||
$this->nextSizeIncreaseThreshold *= $this->sizeIncreaseFactor;
|
$this->nextSizeIncreaseThreshold *= $this->sizeIncreaseFactor;
|
||||||
|
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Services;
|
namespace App\Services\IterationEventBuffer;
|
||||||
|
|
||||||
class PerceptronLimitedEpochEventBuffer implements IPerceptronIterationEventBuffer {
|
class PerceptronLimitedEpochEventBuffer implements IPerceptronIterationEventBuffer
|
||||||
|
{
|
||||||
private array $data;
|
private array $data;
|
||||||
|
|
||||||
private int $underSizeIncreaseCount = 0;
|
private int $underSizeIncreaseCount = 0;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
@@ -15,33 +17,27 @@ class PerceptronLimitedEpochEventBuffer implements IPerceptronIterationEventBuff
|
|||||||
$this->data = [];
|
$this->data = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function flush(): void {
|
public function flush(): void
|
||||||
|
{
|
||||||
event(new \App\Events\PerceptronTrainingIteration($this->data, $this->sessionId, $this->trainingId));
|
event(new \App\Events\PerceptronTrainingIteration($this->data, $this->sessionId, $this->trainingId));
|
||||||
$this->data = [];
|
$this->data = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function addIteration(int $epoch, int $exampleIndex, float $error, array $synaptic_weights): void {
|
public function addIteration(int $epoch, int $exampleIndex, float $error, array $synaptic_weights): void
|
||||||
|
{
|
||||||
$newData = [
|
$newData = [
|
||||||
"epoch" => $epoch,
|
'epoch' => $epoch,
|
||||||
"exampleIndex" => $exampleIndex,
|
'exampleIndex' => $exampleIndex,
|
||||||
"error" => $error,
|
'error' => $error,
|
||||||
"weights" => $synaptic_weights,
|
'weights' => $synaptic_weights,
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($this->underSizeIncreaseCount <= $this->sizeIncreaseStart) { // Special case where we need to send each iteration separately
|
|
||||||
$this->underSizeIncreaseCount++;
|
|
||||||
$this->data[] = $newData;
|
|
||||||
$this->flush();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$lastEpoch = $this->data[0]['epoch'] ?? null;
|
$lastEpoch = $this->data[0]['epoch'] ?? null;
|
||||||
if ($this->data && $lastEpoch !== $epoch) { // Current Epoch has changed from the last one
|
if ($this->data && $lastEpoch !== $epoch) { // Current Epoch has changed from the last one
|
||||||
if ($lastEpoch % $this->epochInterval === 0) { // The last epoch need to be sent
|
if ($lastEpoch == 1 || $lastEpoch % $this->epochInterval === 0) { // The last saved epoch need to be sent
|
||||||
$this->flush(); // Flush all data from the previous epoch
|
$this->flush(); // Flush all data from the previous epoch
|
||||||
}
|
} else {
|
||||||
else {
|
$this->data = []; // We clear the data without sending it as we are saving the next epoch data
|
||||||
$this->data = [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$lastEpoch = $epoch;
|
$lastEpoch = $epoch;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\SynapticWeightsProvider;
|
||||||
|
|
||||||
|
interface ISynapticWeightsProvider
|
||||||
|
{
|
||||||
|
public function generate(int $input_size): array;
|
||||||
|
}
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Services;
|
namespace App\Services\SynapticWeightsProvider;
|
||||||
|
|
||||||
class RandomSynapticWeights implements ISynapticWeightsProvider {
|
class RandomSynapticWeights implements ISynapticWeightsProvider
|
||||||
|
{
|
||||||
public function generate(int $input_size): array
|
public function generate(int $input_size): array
|
||||||
{
|
{
|
||||||
$weights = [];
|
$weights = [];
|
||||||
for ($i = 0; $i < $input_size + 1; $i++) { // +1 for bias weight
|
for ($i = 0; $i < $input_size + 1; $i++) { // +1 for bias weight
|
||||||
$weights[] = rand(-100, 100) / 100; // Random weights between -1 and 1
|
$weights[] = rand(-100, 100) / 100; // Random weights between -1 and 1
|
||||||
}
|
}
|
||||||
|
|
||||||
return $weights;
|
return $weights;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Services;
|
namespace App\Services\SynapticWeightsProvider;
|
||||||
|
|
||||||
class ZeroSynapticWeights implements ISynapticWeightsProvider {
|
class ZeroSynapticWeights implements ISynapticWeightsProvider
|
||||||
|
{
|
||||||
public function generate(int $input_size): array
|
public function generate(int $input_size): array
|
||||||
{
|
{
|
||||||
$weights = [];
|
$weights = [];
|
||||||
for ($i = 0; $i < $input_size + 1; $i++) { // +1 for bias weight
|
for ($i = 0; $i < $input_size + 1; $i++) { // +1 for bias weight
|
||||||
$weights[] = 0; // Zero weights
|
$weights[] = 0; // Zero weights
|
||||||
}
|
}
|
||||||
|
|
||||||
return $weights;
|
return $weights;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
"laravel/wayfinder": "^0.1.9"
|
"laravel/wayfinder": "^0.1.9"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
|
"brianium/paratest": "^7.8",
|
||||||
"fakerphp/faker": "^1.23",
|
"fakerphp/faker": "^1.23",
|
||||||
"laravel/pail": "^1.2.2",
|
"laravel/pail": "^1.2.2",
|
||||||
"laravel/pint": "^1.24",
|
"laravel/pint": "^1.24",
|
||||||
@@ -50,7 +51,7 @@
|
|||||||
],
|
],
|
||||||
"dev": [
|
"dev": [
|
||||||
"Composer\\Config::disableProcessTimeout",
|
"Composer\\Config::disableProcessTimeout",
|
||||||
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"npm run dev\" \"php artisan reverb:start --debug\" --names=server,queue,logs,vite,reverb --kill-others"
|
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74,#79dff0\" \"php artisan serve\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"npm run dev\" \"php artisan reverb:start --debug\" --names=server,queue,logs,vite,reverb --kill-others"
|
||||||
],
|
],
|
||||||
"dev:ssr": [
|
"dev:ssr": [
|
||||||
"npm run build:ssr",
|
"npm run build:ssr",
|
||||||
|
|||||||
216
composer.lock
generated
216
composer.lock
generated
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "a72ab6feeee69457d0085c4a5e4580f7",
|
"content-hash": "93a44ad3435bb0cb19a8bd3b2b700b4f",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "bacon/bacon-qr-code",
|
"name": "bacon/bacon-qr-code",
|
||||||
@@ -7544,6 +7544,99 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"packages-dev": [
|
"packages-dev": [
|
||||||
|
{
|
||||||
|
"name": "brianium/paratest",
|
||||||
|
"version": "v7.8.5",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/paratestphp/paratest.git",
|
||||||
|
"reference": "9b324c8fc319cf9728b581c7a90e1c8f6361c5e5"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/paratestphp/paratest/zipball/9b324c8fc319cf9728b581c7a90e1c8f6361c5e5",
|
||||||
|
"reference": "9b324c8fc319cf9728b581c7a90e1c8f6361c5e5",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-dom": "*",
|
||||||
|
"ext-pcre": "*",
|
||||||
|
"ext-reflection": "*",
|
||||||
|
"ext-simplexml": "*",
|
||||||
|
"fidry/cpu-core-counter": "^1.3.0",
|
||||||
|
"jean85/pretty-package-versions": "^2.1.1",
|
||||||
|
"php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
|
||||||
|
"phpunit/php-code-coverage": "^11.0.12",
|
||||||
|
"phpunit/php-file-iterator": "^5.1.0",
|
||||||
|
"phpunit/php-timer": "^7.0.1",
|
||||||
|
"phpunit/phpunit": "^11.5.46",
|
||||||
|
"sebastian/environment": "^7.2.1",
|
||||||
|
"symfony/console": "^6.4.22 || ^7.3.4 || ^8.0.3",
|
||||||
|
"symfony/process": "^6.4.20 || ^7.3.4 || ^8.0.3"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"doctrine/coding-standard": "^12.0.0",
|
||||||
|
"ext-pcov": "*",
|
||||||
|
"ext-posix": "*",
|
||||||
|
"phpstan/phpstan": "^2.1.33",
|
||||||
|
"phpstan/phpstan-deprecation-rules": "^2.0.3",
|
||||||
|
"phpstan/phpstan-phpunit": "^2.0.11",
|
||||||
|
"phpstan/phpstan-strict-rules": "^2.0.7",
|
||||||
|
"squizlabs/php_codesniffer": "^3.13.5",
|
||||||
|
"symfony/filesystem": "^6.4.13 || ^7.3.2 || ^8.0.1"
|
||||||
|
},
|
||||||
|
"bin": [
|
||||||
|
"bin/paratest",
|
||||||
|
"bin/paratest_for_phpstorm"
|
||||||
|
],
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"ParaTest\\": [
|
||||||
|
"src/"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Brian Scaturro",
|
||||||
|
"email": "scaturrob@gmail.com",
|
||||||
|
"role": "Developer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Filippo Tessarotto",
|
||||||
|
"email": "zoeslam@gmail.com",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Parallel testing for PHP",
|
||||||
|
"homepage": "https://github.com/paratestphp/paratest",
|
||||||
|
"keywords": [
|
||||||
|
"concurrent",
|
||||||
|
"parallel",
|
||||||
|
"phpunit",
|
||||||
|
"testing"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/paratestphp/paratest/issues",
|
||||||
|
"source": "https://github.com/paratestphp/paratest/tree/v7.8.5"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/sponsors/Slamdunk",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://paypal.me/filippotessarotto",
|
||||||
|
"type": "paypal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-01-08T08:02:38+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "fakerphp/faker",
|
"name": "fakerphp/faker",
|
||||||
"version": "v1.24.1",
|
"version": "v1.24.1",
|
||||||
@@ -7607,6 +7700,67 @@
|
|||||||
},
|
},
|
||||||
"time": "2024-11-21T13:46:39+00:00"
|
"time": "2024-11-21T13:46:39+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "fidry/cpu-core-counter",
|
||||||
|
"version": "1.3.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/theofidry/cpu-core-counter.git",
|
||||||
|
"reference": "db9508f7b1474469d9d3c53b86f817e344732678"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678",
|
||||||
|
"reference": "db9508f7b1474469d9d3c53b86f817e344732678",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.2 || ^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"fidry/makefile": "^0.2.0",
|
||||||
|
"fidry/php-cs-fixer-config": "^1.1.2",
|
||||||
|
"phpstan/extension-installer": "^1.2.0",
|
||||||
|
"phpstan/phpstan": "^2.0",
|
||||||
|
"phpstan/phpstan-deprecation-rules": "^2.0.0",
|
||||||
|
"phpstan/phpstan-phpunit": "^2.0",
|
||||||
|
"phpstan/phpstan-strict-rules": "^2.0",
|
||||||
|
"phpunit/phpunit": "^8.5.31 || ^9.5.26",
|
||||||
|
"webmozarts/strict-phpunit": "^7.5"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Fidry\\CpuCoreCounter\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Théo FIDRY",
|
||||||
|
"email": "theo.fidry@gmail.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Tiny utility to get the number of CPU cores.",
|
||||||
|
"keywords": [
|
||||||
|
"CPU",
|
||||||
|
"core"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/theofidry/cpu-core-counter/issues",
|
||||||
|
"source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/theofidry",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-08-14T07:29:31+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "filp/whoops",
|
"name": "filp/whoops",
|
||||||
"version": "2.18.4",
|
"version": "2.18.4",
|
||||||
@@ -7729,6 +7883,66 @@
|
|||||||
},
|
},
|
||||||
"time": "2025-04-30T06:54:44+00:00"
|
"time": "2025-04-30T06:54:44+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "jean85/pretty-package-versions",
|
||||||
|
"version": "2.1.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/Jean85/pretty-package-versions.git",
|
||||||
|
"reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/4d7aa5dab42e2a76d99559706022885de0e18e1a",
|
||||||
|
"reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"composer-runtime-api": "^2.1.0",
|
||||||
|
"php": "^7.4|^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"friendsofphp/php-cs-fixer": "^3.2",
|
||||||
|
"jean85/composer-provided-replaced-stub-package": "^1.0",
|
||||||
|
"phpstan/phpstan": "^2.0",
|
||||||
|
"phpunit/phpunit": "^7.5|^8.5|^9.6",
|
||||||
|
"rector/rector": "^2.0",
|
||||||
|
"vimeo/psalm": "^4.3 || ^5.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "1.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Jean85\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Alessandro Lai",
|
||||||
|
"email": "alessandro.lai85@gmail.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "A library to get pretty versions strings of installed dependencies",
|
||||||
|
"keywords": [
|
||||||
|
"composer",
|
||||||
|
"package",
|
||||||
|
"release",
|
||||||
|
"versions"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/Jean85/pretty-package-versions/issues",
|
||||||
|
"source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.1"
|
||||||
|
},
|
||||||
|
"time": "2025-03-19T14:43:43+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/pail",
|
"name": "laravel/pail",
|
||||||
"version": "v1.2.6",
|
"version": "v1.2.6",
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ return [
|
|||||||
* Beyond this number of iterations, the broadcast will be splitted every x iterations,
|
* Beyond this number of iterations, the broadcast will be splitted every x iterations,
|
||||||
* x is limited_broadcast_number
|
* x is limited_broadcast_number
|
||||||
*/
|
*/
|
||||||
'limited_broadcast_iterations' => 200,
|
'limited_broadcast_iterations' => 100,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How much broadcasts is sent when in limmited broadcast mode
|
* How much broadcasts is sent when in limmited broadcast mode
|
||||||
*/
|
*/
|
||||||
'limited_broadcast_number' => 200,
|
'limited_broadcast_number' => 100,
|
||||||
|
|
||||||
'broadcast_iteration_size' => 75,
|
'broadcast_iteration_size' => 75,
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
8
package-lock.json
generated
8
package-lock.json
generated
@@ -14,7 +14,7 @@
|
|||||||
"laravel-vite-plugin": "^2.0.0",
|
"laravel-vite-plugin": "^2.0.0",
|
||||||
"lucide-vue-next": "^0.468.0",
|
"lucide-vue-next": "^0.468.0",
|
||||||
"radix-ui": "^1.4.3",
|
"radix-ui": "^1.4.3",
|
||||||
"reka-ui": "^2.9.0",
|
"reka-ui": "^2.9.2",
|
||||||
"tailwind-merge": "^3.2.0",
|
"tailwind-merge": "^3.2.0",
|
||||||
"tailwindcss": "^4.1.1",
|
"tailwindcss": "^4.1.1",
|
||||||
"tw-animate-css": "^1.2.5",
|
"tw-animate-css": "^1.2.5",
|
||||||
@@ -7386,9 +7386,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/reka-ui": {
|
"node_modules/reka-ui": {
|
||||||
"version": "2.9.0",
|
"version": "2.9.2",
|
||||||
"resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.9.2.tgz",
|
||||||
"integrity": "sha512-5dpp80u109iLTbRBu+jhAk8R/877/JN20gYGjb3GsuAgS7E/5QTX5ZxuzWtZAVbChBDYDpXc8pkaQAFpa6s+4w==",
|
"integrity": "sha512-/t4e6y1hcG+uDuRfpg6tbMz3uUEvRzNco6NeYTufoJeUghy5Iosxos5YL/p+ieAsid84sdMX9OrgDqpEuCJhBw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/dom": "^1.6.13",
|
"@floating-ui/dom": "^1.6.13",
|
||||||
"@floating-ui/vue": "^1.1.6",
|
"@floating-ui/vue": "^1.1.6",
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
"laravel-vite-plugin": "^2.0.0",
|
"laravel-vite-plugin": "^2.0.0",
|
||||||
"lucide-vue-next": "^0.468.0",
|
"lucide-vue-next": "^0.468.0",
|
||||||
"radix-ui": "^1.4.3",
|
"radix-ui": "^1.4.3",
|
||||||
"reka-ui": "^2.9.0",
|
"reka-ui": "^2.9.2",
|
||||||
"tailwind-merge": "^3.2.0",
|
"tailwind-merge": "^3.2.0",
|
||||||
"tailwindcss": "^4.1.1",
|
"tailwindcss": "^4.1.1",
|
||||||
"tw-animate-css": "^1.2.5",
|
"tw-animate-css": "^1.2.5",
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
<testsuite name="Unit">
|
<testsuite name="Unit">
|
||||||
<directory>tests/Unit</directory>
|
<directory>tests/Unit</directory>
|
||||||
</testsuite>
|
</testsuite>
|
||||||
<testsuite name="Feature">
|
<!-- <testsuite name="Feature">
|
||||||
<directory>tests/Feature</directory>
|
<directory>tests/Feature</directory>
|
||||||
</testsuite>
|
</testsuite> -->
|
||||||
</testsuites>
|
</testsuites>
|
||||||
<source>
|
<source>
|
||||||
<include>
|
<include>
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 46 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 24 KiB |
@@ -1,3 +0,0 @@
|
|||||||
<svg width="166" height="166" viewBox="0 0 166 166" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M162.041 38.7592C162.099 38.9767 162.129 39.201 162.13 39.4264V74.4524C162.13 74.9019 162.011 75.3435 161.786 75.7325C161.561 76.1216 161.237 76.4442 160.847 76.6678L131.462 93.5935V127.141C131.462 128.054 130.977 128.897 130.186 129.357L68.8474 164.683C68.707 164.763 68.5538 164.814 68.4007 164.868C68.3432 164.887 68.289 164.922 68.2284 164.938C67.7996 165.051 67.3489 165.051 66.9201 164.938C66.8499 164.919 66.7861 164.881 66.7191 164.855C66.5787 164.804 66.4319 164.76 66.2979 164.683L4.97219 129.357C4.58261 129.133 4.2589 128.81 4.0337 128.421C3.8085 128.032 3.68976 127.591 3.68945 127.141L3.68945 22.0634C3.68945 21.8336 3.72136 21.6101 3.7788 21.393C3.79794 21.3196 3.84262 21.2526 3.86814 21.1791C3.91601 21.0451 3.96068 20.9078 4.03088 20.7833C4.07874 20.7003 4.14894 20.6333 4.20638 20.5566C4.27977 20.4545 4.34678 20.3491 4.43293 20.2598C4.50632 20.1863 4.60205 20.1321 4.68501 20.0682C4.77755 19.9916 4.86051 19.9086 4.96581 19.848L35.6334 2.18492C36.0217 1.96139 36.4618 1.84375 36.9098 1.84375C37.3578 1.84375 37.7979 1.96139 38.1862 2.18492L68.8506 19.848H68.857C68.9591 19.9118 69.0452 19.9916 69.1378 20.065C69.2207 20.1289 69.3133 20.1863 69.3867 20.2566C69.476 20.3491 69.5398 20.4545 69.6164 20.5566C69.6707 20.6333 69.7441 20.7003 69.7887 20.7833C69.8621 20.911 69.9036 21.0451 69.9546 21.1791C69.9802 21.2526 70.0248 21.3196 70.044 21.3962C70.1027 21.6138 70.1328 21.8381 70.1333 22.0634V87.6941L95.686 72.9743V39.4232C95.686 39.1997 95.7179 38.9731 95.7753 38.7592C95.7977 38.6826 95.8391 38.6155 95.8647 38.5421C95.9157 38.408 95.9604 38.2708 96.0306 38.1463C96.0785 38.0633 96.1487 37.9962 96.2029 37.9196C96.2795 37.8175 96.3433 37.7121 96.4326 37.6227C96.506 37.5493 96.5986 37.495 96.6815 37.4312C96.7773 37.3546 96.8602 37.2716 96.9623 37.2109L127.633 19.5479C128.021 19.324 128.461 19.2062 128.91 19.2062C129.358 19.2062 129.798 19.324 130.186 19.5479L160.85 37.2109C160.959 37.2748 161.042 37.3546 161.137 37.428C161.217 37.4918 161.31 37.5493 161.383 37.6195C161.473 37.7121 161.536 37.8175 161.613 37.9196C161.67 37.9962 161.741 38.0633 161.785 38.1463C161.859 38.2708 161.9 38.408 161.951 38.5421C161.98 38.6155 162.021 38.6826 162.041 38.7592ZM157.018 72.9743V43.8477L146.287 50.028L131.462 58.5675V87.6941L157.021 72.9743H157.018ZM126.354 125.663V96.5176L111.771 104.85L70.1301 128.626V158.046L126.354 125.663ZM8.80126 26.4848V125.663L65.0183 158.043V128.629L35.6494 112L35.6398 111.994L35.6271 111.988C35.5281 111.93 35.4452 111.847 35.3526 111.777C35.2729 111.713 35.1803 111.662 35.1101 111.592L35.1038 111.582C35.0208 111.502 34.9634 111.403 34.8932 111.314C34.8293 111.228 34.7528 111.154 34.7017 111.065L34.6985 111.055C34.6411 110.96 34.606 110.845 34.5645 110.736C34.523 110.64 34.4688 110.551 34.4432 110.449C34.4113 110.328 34.4049 110.197 34.3922 110.072C34.3794 109.976 34.3539 109.881 34.3539 109.785V109.778V41.2045L19.5322 32.6619L8.80126 26.4848ZM36.913 7.35007L11.3635 22.0634L36.9066 36.7768L62.4529 22.0602L36.9066 7.35007H36.913ZM50.1999 99.1736L65.0215 90.6374V26.4848L54.2906 32.6651L39.4657 41.2045V105.357L50.1999 99.1736ZM128.91 24.713L103.363 39.4264L128.91 54.1397L154.453 39.4232L128.91 24.713ZM126.354 58.5675L111.529 50.028L100.798 43.8477V72.9743L115.619 81.5106L126.354 87.6941V58.5675ZM67.5711 124.205L105.042 102.803L123.772 92.109L98.2451 77.4053L68.8538 94.3341L42.0663 109.762L67.5711 124.205Z" fill="#FF2D20"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.5 KiB |
@@ -46,7 +46,7 @@ const rowBgDark = computed(() => {
|
|||||||
v-for="(iteration, index) in props.iterations"
|
v-for="(iteration, index) in props.iterations"
|
||||||
v-bind:key="index"
|
v-bind:key="index"
|
||||||
:class="{
|
:class="{
|
||||||
'bg-gray-900': rowBgDark[index],
|
'bg-gray-300 dark:bg-gray-900': rowBgDark[index],
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<td>{{ iteration.epoch }}</td>
|
<td>{{ iteration.epoch }}</td>
|
||||||
@@ -60,7 +60,7 @@ const rowBgDark = computed(() => {
|
|||||||
<td>{{ iteration.error.toFixed(2) }}</td>
|
<td>{{ iteration.error.toFixed(2) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr v-if="props.trainingEnded" class="bg-red-900 text-center">
|
<tr v-if="props.trainingEnded" class="bg-red-400 dark:bg-red-900 text-center">
|
||||||
<td colspan="100%">
|
<td colspan="100%">
|
||||||
<strong>Entraînement terminé :</strong>
|
<strong>Entraînement terminé :</strong>
|
||||||
{{ props.trainingEndReason }}
|
{{ props.trainingEndReason }}
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ const links = [
|
|||||||
href: '/perceptron',
|
href: '/perceptron',
|
||||||
data: { type: 'gradientdescent' },
|
data: { type: 'gradientdescent' },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'ADALINE',
|
||||||
|
href: '/perceptron',
|
||||||
|
data: { type: 'adaline' },
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const isActiveLink = (link: any) => {
|
const isActiveLink = (link: any) => {
|
||||||
|
|||||||
@@ -20,17 +20,45 @@ const farLeftDataPointX = computed(() => {
|
|||||||
if (props.cleanedDataset.length === 0) {
|
if (props.cleanedDataset.length === 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
const minX = Math.min(...props.cleanedDataset.flatMap((d) => d.data.map((point) => point.x)));
|
const minX = Math.min(
|
||||||
|
...props.cleanedDataset.flatMap((d) => d.data.map((point) => point.x)),
|
||||||
|
);
|
||||||
return minX;
|
return minX;
|
||||||
});
|
});
|
||||||
const farRightDataPointX = computed(() => {
|
const farRightDataPointX = computed(() => {
|
||||||
if (props.cleanedDataset.length === 0) {
|
if (props.cleanedDataset.length === 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
const maxX = Math.max(...props.cleanedDataset.flatMap((d) => d.data.map((point) => point.x)));
|
const maxX = Math.max(
|
||||||
|
...props.cleanedDataset.flatMap((d) => d.data.map((point) => point.x)),
|
||||||
|
);
|
||||||
return maxX;
|
return maxX;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function getPerceptronOutput(weightsNetwork: number[][], inputs: number[]): number[] {
|
||||||
|
for (const layer of weightsNetwork) {
|
||||||
|
const nextInputs: number[] = [];
|
||||||
|
|
||||||
|
for (const neuron of layer) {
|
||||||
|
const bias = neuron[0];
|
||||||
|
const weights = neuron.slice(1);
|
||||||
|
|
||||||
|
let sum = bias;
|
||||||
|
|
||||||
|
for (let i = 0; i < weights.length; i++) {
|
||||||
|
sum += weights[i] * inputs[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
const activated = props.activationFunction(sum);
|
||||||
|
|
||||||
|
nextInputs.push(activated);
|
||||||
|
}
|
||||||
|
|
||||||
|
inputs = nextInputs;
|
||||||
|
}
|
||||||
|
return inputs;
|
||||||
|
}
|
||||||
|
|
||||||
function getPerceptronDecisionBoundaryDataset(
|
function getPerceptronDecisionBoundaryDataset(
|
||||||
networkWeights: number[][][],
|
networkWeights: number[][][],
|
||||||
activationFunction: (x: number) => number = (x) => x,
|
activationFunction: (x: number) => number = (x) => x,
|
||||||
@@ -44,11 +72,17 @@ function getPerceptronDecisionBoundaryDataset(
|
|||||||
if (
|
if (
|
||||||
networkWeights.length == 1 &&
|
networkWeights.length == 1 &&
|
||||||
networkWeights[0].length == 1 &&
|
networkWeights[0].length == 1 &&
|
||||||
networkWeights[0][0].length == 3
|
networkWeights[0][0].length <= 3
|
||||||
) { // Unique, 3 weights perceptron
|
) {
|
||||||
|
// Unique, 3 weights perceptron
|
||||||
const perceptronWeights = networkWeights[0][0]; // We take the unique perceptron
|
const perceptronWeights = networkWeights[0][0]; // We take the unique perceptron
|
||||||
|
|
||||||
function perceptronLine(x: number): number {
|
function perceptronLine(x: number): number {
|
||||||
|
if (perceptronWeights.length < 3) {
|
||||||
|
// If we have less than 3 weights, we assume missing weights are zero
|
||||||
|
perceptronWeights.push(getPerceptronOutput(networkWeights, [x])[0]);
|
||||||
|
}
|
||||||
|
|
||||||
// w0 + w1*x + w2*y = 0 => y = -(w1/w2)*x - w0/w2
|
// w0 + w1*x + w2*y = 0 => y = -(w1/w2)*x - w0/w2
|
||||||
const w2 = perceptronWeights[2] == 0 ? 1e-6 : perceptronWeights[2]; // Avoid division by zero
|
const w2 = perceptronWeights[2] == 0 ? 1e-6 : perceptronWeights[2]; // Avoid division by zero
|
||||||
return -(perceptronWeights[1] / w2) * x - perceptronWeights[0] / w2;
|
return -(perceptronWeights[1] / w2) * x - perceptronWeights[0] / w2;
|
||||||
@@ -132,7 +166,7 @@ function getPerceptronDecisionBoundaryDataset(
|
|||||||
<template>
|
<template>
|
||||||
<Chart
|
<Chart
|
||||||
v-if="props.cleanedDataset.length > 0 || props.iterations.length > 0"
|
v-if="props.cleanedDataset.length > 0 || props.iterations.length > 0"
|
||||||
class="flex"
|
class="flex bg-primary dark:bg-transparent!"
|
||||||
:options="{
|
:options="{
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: true,
|
maintainAspectRatio: true,
|
||||||
@@ -189,8 +223,7 @@ function getPerceptronDecisionBoundaryDataset(
|
|||||||
type: 'scatter',
|
type: 'scatter',
|
||||||
label: `Label ${dataset.label}`,
|
label: `Label ${dataset.label}`,
|
||||||
data: dataset.data,
|
data: dataset.data,
|
||||||
backgroundColor:
|
backgroundColor: colors[index] || '#AAA',
|
||||||
colors[index] || '#AAA',
|
|
||||||
})),
|
})),
|
||||||
|
|
||||||
// Perceptron decision boundary
|
// Perceptron decision boundary
|
||||||
|
|||||||
@@ -1,46 +1,55 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ChartData } from 'chart.js';
|
import type { ChartData } from 'chart.js';
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
import { Bar } from 'vue-chartjs';
|
import { Bar } from 'vue-chartjs';
|
||||||
import { colors, gridColor, gridColorBold } from '@/types/graphs';
|
import { colors, gridColor, gridColorBold } from '@/types/graphs';
|
||||||
import type { Iteration } from '@/types/perceptron';
|
import type { Iteration } from '@/types/perceptron';
|
||||||
|
import Toggle from './ui/toggle/Toggle.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
iterations: Iteration[];
|
iterations: Iteration[];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const epochErrorOnly = ref<boolean>(false);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the datasets of the iterations with the form { label: `Exemple ${exampleIndex}`, data: [error for iteration 1, error for iteration 2, ...] }
|
* Datasets of the iterations with the form { label: `Exemple ${exampleIndex}`, data: [error for iteration 1, error for iteration 2, ...] }
|
||||||
*/
|
*/
|
||||||
function getPerceptronErrorsPerIteration(): ChartData<
|
const datasets = computed<
|
||||||
'bar',
|
ChartData<'bar', (number | [number, number] | null)[]>[]
|
||||||
(number | [number, number] | null)[]
|
>(() => {
|
||||||
>[] {
|
|
||||||
const datasets: ChartData<'bar', (number | [number, number] | null)[]>[] =
|
const datasets: ChartData<'bar', (number | [number, number] | null)[]>[] =
|
||||||
[];
|
[];
|
||||||
const epochAverageError: number[] = [];
|
const epochAverageError: number[] = [];
|
||||||
|
|
||||||
const backgroundColors = colors;
|
const backgroundColors = colors;
|
||||||
|
|
||||||
|
const exampleCountPerEpoch: Record<number, number> = {};
|
||||||
|
|
||||||
props.iterations.forEach((iteration) => {
|
props.iterations.forEach((iteration) => {
|
||||||
const exampleLabel = `Exemple ${iteration.exampleIndex}`;
|
if (!epochErrorOnly.value) {
|
||||||
let dataset = datasets.find((d) => d.label === exampleLabel);
|
const exampleLabel = `Exemple ${iteration.exampleIndex}`;
|
||||||
if (!dataset) {
|
let dataset = datasets.find((d) => d.label === exampleLabel);
|
||||||
dataset = {
|
if (!dataset) {
|
||||||
label: exampleLabel,
|
dataset = {
|
||||||
data: [],
|
label: exampleLabel,
|
||||||
order: 1,
|
data: [],
|
||||||
backgroundColor:
|
order: 1,
|
||||||
backgroundColors[
|
backgroundColor:
|
||||||
iteration.exampleIndex % backgroundColors.length
|
backgroundColors[
|
||||||
],
|
iteration.exampleIndex % backgroundColors.length
|
||||||
};
|
],
|
||||||
datasets.push(dataset);
|
};
|
||||||
|
datasets.push(dataset);
|
||||||
|
}
|
||||||
|
dataset.data.push(iteration.error);
|
||||||
}
|
}
|
||||||
dataset.data.push(iteration.error);
|
|
||||||
|
exampleCountPerEpoch[iteration.epoch] = (exampleCountPerEpoch[iteration.epoch] || 0) + 1;
|
||||||
|
|
||||||
// Epoch error
|
// Epoch error
|
||||||
epochAverageError[iteration.epoch - 1] =
|
epochAverageError[iteration.epoch] =
|
||||||
(epochAverageError[iteration.epoch - 1] || 0) +
|
(epochAverageError[iteration.epoch] || 0) +
|
||||||
iteration.error ** 2 / 2;
|
iteration.error ** 2 / 2;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -54,7 +63,7 @@ function getPerceptronErrorsPerIteration(): ChartData<
|
|||||||
// Epoch error
|
// Epoch error
|
||||||
const epochErrorDataset = {
|
const epochErrorDataset = {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
label: "Erreur de l'époque",
|
label: "Erreur quadratique moyenne de l'époque",
|
||||||
data: [],
|
data: [],
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
borderColor: '#fff',
|
borderColor: '#fff',
|
||||||
@@ -62,19 +71,20 @@ function getPerceptronErrorsPerIteration(): ChartData<
|
|||||||
tension: 0.3,
|
tension: 0.3,
|
||||||
};
|
};
|
||||||
|
|
||||||
epochAverageError.forEach((error) => {
|
epochAverageError.forEach((error, index) => {
|
||||||
epochErrorDataset.data.push(error);
|
const exampleCount = exampleCountPerEpoch[index] || 1; // Avoid division by zero
|
||||||
|
epochErrorDataset.data.push(error / exampleCount);
|
||||||
});
|
});
|
||||||
|
|
||||||
datasets.push(epochErrorDataset);
|
datasets.push(epochErrorDataset);
|
||||||
|
|
||||||
return datasets;
|
return datasets;
|
||||||
}
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Bar
|
<Bar
|
||||||
class="flex"
|
class="bg-primary dark:bg-transparent!"
|
||||||
:options="{
|
:options="{
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: true,
|
maintainAspectRatio: true,
|
||||||
@@ -111,7 +121,12 @@ function getPerceptronErrorsPerIteration(): ChartData<
|
|||||||
}
|
}
|
||||||
return labels;
|
return labels;
|
||||||
}, [] as string[]),
|
}, [] as string[]),
|
||||||
datasets: getPerceptronErrorsPerIteration(),
|
datasets: datasets,
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<Toggle v-model="epochErrorOnly" class="cursor-pointer" variant="outline" id="epoch-error-only"
|
||||||
|
>Afficher uniquement l'erreur quadratique moyenne de l'époque</Toggle
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ watch(selectedDatasetCopy, (newValue) => {
|
|||||||
name="dataset"
|
name="dataset"
|
||||||
id="dataset-select"
|
id="dataset-select"
|
||||||
v-model="selectedDatasetCopy"
|
v-model="selectedDatasetCopy"
|
||||||
|
class="cursor-pointer"
|
||||||
>
|
>
|
||||||
<NativeSelectOption value="" disabled
|
<NativeSelectOption value="" disabled
|
||||||
>Sélectionnez un dataset</NativeSelectOption
|
>Sélectionnez un dataset</NativeSelectOption
|
||||||
@@ -154,6 +155,7 @@ watch(selectedDatasetCopy, (newValue) => {
|
|||||||
name="weight_init_method"
|
name="weight_init_method"
|
||||||
id="weight_init_method"
|
id="weight_init_method"
|
||||||
v-model="selectedMethod"
|
v-model="selectedMethod"
|
||||||
|
class="cursor-pointer"
|
||||||
>
|
>
|
||||||
<NativeSelectOption
|
<NativeSelectOption
|
||||||
v-for="method in ['zeros', 'random']"
|
v-for="method in ['zeros', 'random']"
|
||||||
|
|||||||
35
resources/js/components/ui/toggle/Toggle.vue
Normal file
35
resources/js/components/ui/toggle/Toggle.vue
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { ToggleEmits, ToggleProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import type { ToggleVariants } from "."
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { Toggle, useForwardPropsEmits } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { toggleVariants } from "."
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<ToggleProps & {
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
variant?: ToggleVariants["variant"]
|
||||||
|
size?: ToggleVariants["size"]
|
||||||
|
}>(), {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
disabled: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const emits = defineEmits<ToggleEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class", "size", "variant")
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Toggle
|
||||||
|
v-slot="slotProps"
|
||||||
|
data-slot="toggle"
|
||||||
|
v-bind="forwarded"
|
||||||
|
:class="cn(toggleVariants({ variant, size }), props.class)"
|
||||||
|
>
|
||||||
|
<slot v-bind="slotProps" />
|
||||||
|
</Toggle>
|
||||||
|
</template>
|
||||||
28
resources/js/components/ui/toggle/index.ts
Normal file
28
resources/js/components/ui/toggle/index.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import type { VariantProps } from "class-variance-authority"
|
||||||
|
import { cva } from "class-variance-authority"
|
||||||
|
|
||||||
|
export { default as Toggle } from "./Toggle.vue"
|
||||||
|
|
||||||
|
export const toggleVariants = cva(
|
||||||
|
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-transparent",
|
||||||
|
outline:
|
||||||
|
"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-9 px-2 min-w-9",
|
||||||
|
sm: "h-8 px-1.5 min-w-8",
|
||||||
|
lg: "h-10 px-2.5 min-w-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export type ToggleVariants = VariantProps<typeof toggleVariants>
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { Head, Link } from '@inertiajs/vue3';
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Head title="Welcome">
|
|
||||||
</Head>
|
|
||||||
<div
|
|
||||||
class="flex min-h-screen flex-col items-center bg-[#FDFDFC] p-6 text-[#1b1b18] lg:justify-center lg:p-8 dark:bg-[#0a0a0a]"
|
|
||||||
>
|
|
||||||
<header
|
|
||||||
class="mb-6 w-full max-w-[335px] text-sm not-has-[nav]:hidden lg:max-w-4xl"
|
|
||||||
>
|
|
||||||
<nav class="flex items-center justify-end gap-4">
|
|
||||||
<Link
|
|
||||||
href="/test"
|
|
||||||
class="inline-block rounded-sm border border-[#19140035] px-5 py-1.5 text-sm leading-normal text-[#1b1b18] hover:border-[#1915014a] dark:border-[#3E3E3A] dark:text-[#EDEDEC] dark:hover:border-[#62605b]"
|
|
||||||
view-transition
|
|
||||||
>
|
|
||||||
Test
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href="/perceptron"
|
|
||||||
class="inline-block rounded-sm border border-[#19140035] px-5 py-1.5 text-sm leading-normal text-[#1b1b18] hover:border-[#1915014a] dark:border-[#3E3E3A] dark:text-[#EDEDEC] dark:hover:border-[#62605b]"
|
|
||||||
:data="{type: 'simple'}"
|
|
||||||
view-transition
|
|
||||||
>
|
|
||||||
Perceptron Simple
|
|
||||||
</Link>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
<div
|
|
||||||
class="flex w-full items-center justify-center opacity-100 transition-opacity duration-750 lg:grow starting:opacity-0"
|
|
||||||
>
|
|
||||||
<main
|
|
||||||
class="flex w-full max-w-[335px] flex-col-reverse overflow-hidden rounded-lg lg:max-w-4xl lg:flex-row"
|
|
||||||
>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { Head, Link } from '@inertiajs/vue3';
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<p>
|
|
||||||
I'm you father
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
import type { Point } from "chart.js";
|
|
||||||
|
|
||||||
export type Iteration = {
|
export type Iteration = {
|
||||||
epoch: number;
|
epoch: number;
|
||||||
exampleIndex: number;
|
exampleIndex: number;
|
||||||
|
|||||||
@@ -33,7 +33,6 @@
|
|||||||
<title inertia>{{ config('app.name', 'Laravel') }}</title>
|
<title inertia>{{ config('app.name', 'Laravel') }}</title>
|
||||||
|
|
||||||
<link rel="icon" href="/favicon.ico" sizes="any">
|
<link rel="icon" href="/favicon.ico" sizes="any">
|
||||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
|
||||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||||
|
|
||||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Http\Controllers\PerceptronController;
|
use App\Http\Controllers\PerceptronController;
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::post('perceptron/run', [PerceptronController::class, 'run'])->name('perceptron.run');
|
Route::post('perceptron/run', [PerceptronController::class, 'run'])->name('perceptron.run');
|
||||||
|
|||||||
@@ -2,6 +2,6 @@
|
|||||||
|
|
||||||
use Illuminate\Support\Facades\Broadcast;
|
use Illuminate\Support\Facades\Broadcast;
|
||||||
|
|
||||||
Broadcast::channel(session()->getId() . '-perceptron-training', function ($user) {
|
Broadcast::channel(session()->getId().'-perceptron-training', function ($user) {
|
||||||
return $user;
|
return $user;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,9 +4,7 @@ use App\Http\Controllers\PerceptronController;
|
|||||||
use Illuminate\Support\Facades\Broadcast;
|
use Illuminate\Support\Facades\Broadcast;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::inertia('/', 'Home')->name('home');
|
Route::redirect('/', '/perceptron?type=simple')->name('home');
|
||||||
|
|
||||||
Route::inertia('/test', 'Test')->name('test');
|
|
||||||
|
|
||||||
Route::resource('perceptron', PerceptronController::class)->only(['index']);
|
Route::resource('perceptron', PerceptronController::class)->only(['index']);
|
||||||
|
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature\Auth;
|
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use Illuminate\Support\Facades\RateLimiter;
|
|
||||||
use Laravel\Fortify\Features;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class AuthenticationTest extends TestCase
|
|
||||||
{
|
|
||||||
use RefreshDatabase;
|
|
||||||
|
|
||||||
public function test_login_screen_can_be_rendered()
|
|
||||||
{
|
|
||||||
$response = $this->get(route('login'));
|
|
||||||
|
|
||||||
$response->assertOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_users_can_authenticate_using_the_login_screen()
|
|
||||||
{
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
$response = $this->post(route('login.store'), [
|
|
||||||
'email' => $user->email,
|
|
||||||
'password' => 'password',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->assertAuthenticated();
|
|
||||||
$response->assertRedirect(route('dashboard', absolute: false));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_users_with_two_factor_enabled_are_redirected_to_two_factor_challenge()
|
|
||||||
{
|
|
||||||
if (! Features::canManageTwoFactorAuthentication()) {
|
|
||||||
$this->markTestSkipped('Two-factor authentication is not enabled.');
|
|
||||||
}
|
|
||||||
|
|
||||||
Features::twoFactorAuthentication([
|
|
||||||
'confirm' => true,
|
|
||||||
'confirmPassword' => true,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
$user->forceFill([
|
|
||||||
'two_factor_secret' => encrypt('test-secret'),
|
|
||||||
'two_factor_recovery_codes' => encrypt(json_encode(['code1', 'code2'])),
|
|
||||||
'two_factor_confirmed_at' => now(),
|
|
||||||
])->save();
|
|
||||||
|
|
||||||
$response = $this->post(route('login'), [
|
|
||||||
'email' => $user->email,
|
|
||||||
'password' => 'password',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response->assertRedirect(route('two-factor.login'));
|
|
||||||
$response->assertSessionHas('login.id', $user->id);
|
|
||||||
$this->assertGuest();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_users_can_not_authenticate_with_invalid_password()
|
|
||||||
{
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
$this->post(route('login.store'), [
|
|
||||||
'email' => $user->email,
|
|
||||||
'password' => 'wrong-password',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->assertGuest();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_users_can_logout()
|
|
||||||
{
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
$response = $this->actingAs($user)->post(route('logout'));
|
|
||||||
|
|
||||||
$this->assertGuest();
|
|
||||||
$response->assertRedirect(route('home'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_users_are_rate_limited()
|
|
||||||
{
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
RateLimiter::increment(md5('login'.implode('|', [$user->email, '127.0.0.1'])), amount: 5);
|
|
||||||
|
|
||||||
$response = $this->post(route('login.store'), [
|
|
||||||
'email' => $user->email,
|
|
||||||
'password' => 'wrong-password',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response->assertTooManyRequests();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature\Auth;
|
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use Illuminate\Auth\Events\Verified;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use Illuminate\Support\Facades\Event;
|
|
||||||
use Illuminate\Support\Facades\URL;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class EmailVerificationTest extends TestCase
|
|
||||||
{
|
|
||||||
use RefreshDatabase;
|
|
||||||
|
|
||||||
public function test_email_verification_screen_can_be_rendered()
|
|
||||||
{
|
|
||||||
$user = User::factory()->unverified()->create();
|
|
||||||
|
|
||||||
$response = $this->actingAs($user)->get(route('verification.notice'));
|
|
||||||
|
|
||||||
$response->assertOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_email_can_be_verified()
|
|
||||||
{
|
|
||||||
$user = User::factory()->unverified()->create();
|
|
||||||
|
|
||||||
Event::fake();
|
|
||||||
|
|
||||||
$verificationUrl = URL::temporarySignedRoute(
|
|
||||||
'verification.verify',
|
|
||||||
now()->addMinutes(60),
|
|
||||||
['id' => $user->id, 'hash' => sha1($user->email)],
|
|
||||||
);
|
|
||||||
|
|
||||||
$response = $this->actingAs($user)->get($verificationUrl);
|
|
||||||
|
|
||||||
Event::assertDispatched(Verified::class);
|
|
||||||
$this->assertTrue($user->fresh()->hasVerifiedEmail());
|
|
||||||
$response->assertRedirect(route('dashboard', absolute: false).'?verified=1');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_email_is_not_verified_with_invalid_hash()
|
|
||||||
{
|
|
||||||
$user = User::factory()->unverified()->create();
|
|
||||||
|
|
||||||
Event::fake();
|
|
||||||
|
|
||||||
$verificationUrl = URL::temporarySignedRoute(
|
|
||||||
'verification.verify',
|
|
||||||
now()->addMinutes(60),
|
|
||||||
['id' => $user->id, 'hash' => sha1('wrong-email')],
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->actingAs($user)->get($verificationUrl);
|
|
||||||
|
|
||||||
Event::assertNotDispatched(Verified::class);
|
|
||||||
$this->assertFalse($user->fresh()->hasVerifiedEmail());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_email_is_not_verified_with_invalid_user_id(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->unverified()->create();
|
|
||||||
|
|
||||||
Event::fake();
|
|
||||||
|
|
||||||
$verificationUrl = URL::temporarySignedRoute(
|
|
||||||
'verification.verify',
|
|
||||||
now()->addMinutes(60),
|
|
||||||
['id' => 123, 'hash' => sha1($user->email)],
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->actingAs($user)->get($verificationUrl);
|
|
||||||
|
|
||||||
Event::assertNotDispatched(Verified::class);
|
|
||||||
$this->assertFalse($user->fresh()->hasVerifiedEmail());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_verified_user_is_redirected_to_dashboard_from_verification_prompt(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
Event::fake();
|
|
||||||
|
|
||||||
$response = $this->actingAs($user)->get(route('verification.notice'));
|
|
||||||
|
|
||||||
Event::assertNotDispatched(Verified::class);
|
|
||||||
$response->assertRedirect(route('dashboard', absolute: false));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_already_verified_user_visiting_verification_link_is_redirected_without_firing_event_again(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
Event::fake();
|
|
||||||
|
|
||||||
$verificationUrl = URL::temporarySignedRoute(
|
|
||||||
'verification.verify',
|
|
||||||
now()->addMinutes(60),
|
|
||||||
['id' => $user->id, 'hash' => sha1($user->email)],
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->actingAs($user)->get($verificationUrl)
|
|
||||||
->assertRedirect(route('dashboard', absolute: false).'?verified=1');
|
|
||||||
|
|
||||||
Event::assertNotDispatched(Verified::class);
|
|
||||||
$this->assertTrue($user->fresh()->hasVerifiedEmail());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature\Auth;
|
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use Inertia\Testing\AssertableInertia as Assert;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class PasswordConfirmationTest extends TestCase
|
|
||||||
{
|
|
||||||
use RefreshDatabase;
|
|
||||||
|
|
||||||
public function test_confirm_password_screen_can_be_rendered()
|
|
||||||
{
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
$response = $this->actingAs($user)->get(route('password.confirm'));
|
|
||||||
|
|
||||||
$response->assertOk();
|
|
||||||
|
|
||||||
$response->assertInertia(fn (Assert $page) => $page
|
|
||||||
->component('auth/ConfirmPassword'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_password_confirmation_requires_authentication()
|
|
||||||
{
|
|
||||||
$response = $this->get(route('password.confirm'));
|
|
||||||
|
|
||||||
$response->assertRedirect(route('login'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature\Auth;
|
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use Illuminate\Auth\Notifications\ResetPassword;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use Illuminate\Support\Facades\Notification;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class PasswordResetTest extends TestCase
|
|
||||||
{
|
|
||||||
use RefreshDatabase;
|
|
||||||
|
|
||||||
public function test_reset_password_link_screen_can_be_rendered()
|
|
||||||
{
|
|
||||||
$response = $this->get(route('password.request'));
|
|
||||||
|
|
||||||
$response->assertOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_reset_password_link_can_be_requested()
|
|
||||||
{
|
|
||||||
Notification::fake();
|
|
||||||
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
$this->post(route('password.email'), ['email' => $user->email]);
|
|
||||||
|
|
||||||
Notification::assertSentTo($user, ResetPassword::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_reset_password_screen_can_be_rendered()
|
|
||||||
{
|
|
||||||
Notification::fake();
|
|
||||||
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
$this->post(route('password.email'), ['email' => $user->email]);
|
|
||||||
|
|
||||||
Notification::assertSentTo($user, ResetPassword::class, function ($notification) {
|
|
||||||
$response = $this->get(route('password.reset', $notification->token));
|
|
||||||
|
|
||||||
$response->assertOk();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_password_can_be_reset_with_valid_token()
|
|
||||||
{
|
|
||||||
Notification::fake();
|
|
||||||
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
$this->post(route('password.email'), ['email' => $user->email]);
|
|
||||||
|
|
||||||
Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) {
|
|
||||||
$response = $this->post(route('password.update'), [
|
|
||||||
'token' => $notification->token,
|
|
||||||
'email' => $user->email,
|
|
||||||
'password' => 'password',
|
|
||||||
'password_confirmation' => 'password',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response
|
|
||||||
->assertSessionHasNoErrors()
|
|
||||||
->assertRedirect(route('login'));
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_password_cannot_be_reset_with_invalid_token(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
$response = $this->post(route('password.update'), [
|
|
||||||
'token' => 'invalid-token',
|
|
||||||
'email' => $user->email,
|
|
||||||
'password' => 'newpassword123',
|
|
||||||
'password_confirmation' => 'newpassword123',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response->assertSessionHasErrors('email');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature\Auth;
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class RegistrationTest extends TestCase
|
|
||||||
{
|
|
||||||
use RefreshDatabase;
|
|
||||||
|
|
||||||
public function test_registration_screen_can_be_rendered()
|
|
||||||
{
|
|
||||||
$response = $this->get(route('register'));
|
|
||||||
|
|
||||||
$response->assertOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_new_users_can_register()
|
|
||||||
{
|
|
||||||
$response = $this->post(route('register.store'), [
|
|
||||||
'name' => 'Test User',
|
|
||||||
'email' => 'test@example.com',
|
|
||||||
'password' => 'password',
|
|
||||||
'password_confirmation' => 'password',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->assertAuthenticated();
|
|
||||||
$response->assertRedirect(route('dashboard', absolute: false));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature\Auth;
|
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use Inertia\Testing\AssertableInertia as Assert;
|
|
||||||
use Laravel\Fortify\Features;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class TwoFactorChallengeTest extends TestCase
|
|
||||||
{
|
|
||||||
use RefreshDatabase;
|
|
||||||
|
|
||||||
public function test_two_factor_challenge_redirects_to_login_when_not_authenticated(): void
|
|
||||||
{
|
|
||||||
if (! Features::canManageTwoFactorAuthentication()) {
|
|
||||||
$this->markTestSkipped('Two-factor authentication is not enabled.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$response = $this->get(route('two-factor.login'));
|
|
||||||
|
|
||||||
$response->assertRedirect(route('login'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_two_factor_challenge_can_be_rendered(): void
|
|
||||||
{
|
|
||||||
if (! Features::canManageTwoFactorAuthentication()) {
|
|
||||||
$this->markTestSkipped('Two-factor authentication is not enabled.');
|
|
||||||
}
|
|
||||||
|
|
||||||
Features::twoFactorAuthentication([
|
|
||||||
'confirm' => true,
|
|
||||||
'confirmPassword' => true,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
$user->forceFill([
|
|
||||||
'two_factor_secret' => encrypt('test-secret'),
|
|
||||||
'two_factor_recovery_codes' => encrypt(json_encode(['code1', 'code2'])),
|
|
||||||
'two_factor_confirmed_at' => now(),
|
|
||||||
])->save();
|
|
||||||
|
|
||||||
$this->post(route('login'), [
|
|
||||||
'email' => $user->email,
|
|
||||||
'password' => 'password',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->get(route('two-factor.login'))
|
|
||||||
->assertOk()
|
|
||||||
->assertInertia(fn (Assert $page) => $page
|
|
||||||
->component('auth/TwoFactorChallenge'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature\Auth;
|
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use Illuminate\Auth\Notifications\VerifyEmail;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use Illuminate\Support\Facades\Notification;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class VerificationNotificationTest extends TestCase
|
|
||||||
{
|
|
||||||
use RefreshDatabase;
|
|
||||||
|
|
||||||
public function test_sends_verification_notification(): void
|
|
||||||
{
|
|
||||||
Notification::fake();
|
|
||||||
|
|
||||||
$user = User::factory()->unverified()->create();
|
|
||||||
|
|
||||||
$this->actingAs($user)
|
|
||||||
->post(route('verification.send'))
|
|
||||||
->assertRedirect(route('home'));
|
|
||||||
|
|
||||||
Notification::assertSentTo($user, VerifyEmail::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_does_not_send_verification_notification_if_email_is_verified(): void
|
|
||||||
{
|
|
||||||
Notification::fake();
|
|
||||||
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
$this->actingAs($user)
|
|
||||||
->post(route('verification.send'))
|
|
||||||
->assertRedirect(route('dashboard', absolute: false));
|
|
||||||
|
|
||||||
Notification::assertNothingSent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature;
|
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class DashboardTest extends TestCase
|
|
||||||
{
|
|
||||||
use RefreshDatabase;
|
|
||||||
|
|
||||||
public function test_guests_are_redirected_to_the_login_page()
|
|
||||||
{
|
|
||||||
$response = $this->get(route('dashboard'));
|
|
||||||
$response->assertRedirect(route('login'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_authenticated_users_can_visit_the_dashboard()
|
|
||||||
{
|
|
||||||
$user = User::factory()->create();
|
|
||||||
$this->actingAs($user);
|
|
||||||
|
|
||||||
$response = $this->get(route('dashboard'));
|
|
||||||
$response->assertOk();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature;
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class ExampleTest extends TestCase
|
|
||||||
{
|
|
||||||
use RefreshDatabase;
|
|
||||||
|
|
||||||
public function test_returns_a_successful_response()
|
|
||||||
{
|
|
||||||
$response = $this->get(route('home'));
|
|
||||||
|
|
||||||
$response->assertOk();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature\Settings;
|
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use Illuminate\Support\Facades\Hash;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class PasswordUpdateTest extends TestCase
|
|
||||||
{
|
|
||||||
use RefreshDatabase;
|
|
||||||
|
|
||||||
public function test_password_update_page_is_displayed()
|
|
||||||
{
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
$response = $this
|
|
||||||
->actingAs($user)
|
|
||||||
->get(route('user-password.edit'));
|
|
||||||
|
|
||||||
$response->assertOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_password_can_be_updated()
|
|
||||||
{
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
$response = $this
|
|
||||||
->actingAs($user)
|
|
||||||
->from(route('user-password.edit'))
|
|
||||||
->put(route('user-password.update'), [
|
|
||||||
'current_password' => 'password',
|
|
||||||
'password' => 'new-password',
|
|
||||||
'password_confirmation' => 'new-password',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response
|
|
||||||
->assertSessionHasNoErrors()
|
|
||||||
->assertRedirect(route('user-password.edit'));
|
|
||||||
|
|
||||||
$this->assertTrue(Hash::check('new-password', $user->refresh()->password));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_correct_password_must_be_provided_to_update_password()
|
|
||||||
{
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
$response = $this
|
|
||||||
->actingAs($user)
|
|
||||||
->from(route('user-password.edit'))
|
|
||||||
->put(route('user-password.update'), [
|
|
||||||
'current_password' => 'wrong-password',
|
|
||||||
'password' => 'new-password',
|
|
||||||
'password_confirmation' => 'new-password',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response
|
|
||||||
->assertSessionHasErrors('current_password')
|
|
||||||
->assertRedirect(route('user-password.edit'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature\Settings;
|
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class ProfileUpdateTest extends TestCase
|
|
||||||
{
|
|
||||||
use RefreshDatabase;
|
|
||||||
|
|
||||||
public function test_profile_page_is_displayed()
|
|
||||||
{
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
$response = $this
|
|
||||||
->actingAs($user)
|
|
||||||
->get(route('profile.edit'));
|
|
||||||
|
|
||||||
$response->assertOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_profile_information_can_be_updated()
|
|
||||||
{
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
$response = $this
|
|
||||||
->actingAs($user)
|
|
||||||
->patch(route('profile.update'), [
|
|
||||||
'name' => 'Test User',
|
|
||||||
'email' => 'test@example.com',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response
|
|
||||||
->assertSessionHasNoErrors()
|
|
||||||
->assertRedirect(route('profile.edit'));
|
|
||||||
|
|
||||||
$user->refresh();
|
|
||||||
|
|
||||||
$this->assertSame('Test User', $user->name);
|
|
||||||
$this->assertSame('test@example.com', $user->email);
|
|
||||||
$this->assertNull($user->email_verified_at);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_email_verification_status_is_unchanged_when_the_email_address_is_unchanged()
|
|
||||||
{
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
$response = $this
|
|
||||||
->actingAs($user)
|
|
||||||
->patch(route('profile.update'), [
|
|
||||||
'name' => 'Test User',
|
|
||||||
'email' => $user->email,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response
|
|
||||||
->assertSessionHasNoErrors()
|
|
||||||
->assertRedirect(route('profile.edit'));
|
|
||||||
|
|
||||||
$this->assertNotNull($user->refresh()->email_verified_at);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_user_can_delete_their_account()
|
|
||||||
{
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
$response = $this
|
|
||||||
->actingAs($user)
|
|
||||||
->delete(route('profile.destroy'), [
|
|
||||||
'password' => 'password',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response
|
|
||||||
->assertSessionHasNoErrors()
|
|
||||||
->assertRedirect(route('home'));
|
|
||||||
|
|
||||||
$this->assertGuest();
|
|
||||||
$this->assertNull($user->fresh());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_correct_password_must_be_provided_to_delete_account()
|
|
||||||
{
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
$response = $this
|
|
||||||
->actingAs($user)
|
|
||||||
->from(route('profile.edit'))
|
|
||||||
->delete(route('profile.destroy'), [
|
|
||||||
'password' => 'wrong-password',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response
|
|
||||||
->assertSessionHasErrors('password')
|
|
||||||
->assertRedirect(route('profile.edit'));
|
|
||||||
|
|
||||||
$this->assertNotNull($user->fresh());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature\Settings;
|
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use Inertia\Testing\AssertableInertia as Assert;
|
|
||||||
use Laravel\Fortify\Features;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class TwoFactorAuthenticationTest extends TestCase
|
|
||||||
{
|
|
||||||
use RefreshDatabase;
|
|
||||||
|
|
||||||
public function test_two_factor_settings_page_can_be_rendered()
|
|
||||||
{
|
|
||||||
if (! Features::canManageTwoFactorAuthentication()) {
|
|
||||||
$this->markTestSkipped('Two-factor authentication is not enabled.');
|
|
||||||
}
|
|
||||||
|
|
||||||
Features::twoFactorAuthentication([
|
|
||||||
'confirm' => true,
|
|
||||||
'confirmPassword' => true,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
$this->actingAs($user)
|
|
||||||
->withSession(['auth.password_confirmed_at' => time()])
|
|
||||||
->get(route('two-factor.show'))
|
|
||||||
->assertInertia(fn (Assert $page) => $page
|
|
||||||
->component('settings/TwoFactor')
|
|
||||||
->where('twoFactorEnabled', false),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_two_factor_settings_page_requires_password_confirmation_when_enabled()
|
|
||||||
{
|
|
||||||
if (! Features::canManageTwoFactorAuthentication()) {
|
|
||||||
$this->markTestSkipped('Two-factor authentication is not enabled.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
Features::twoFactorAuthentication([
|
|
||||||
'confirm' => true,
|
|
||||||
'confirmPassword' => true,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response = $this->actingAs($user)
|
|
||||||
->get(route('two-factor.show'));
|
|
||||||
|
|
||||||
$response->assertRedirect(route('password.confirm'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_two_factor_settings_page_does_not_requires_password_confirmation_when_disabled()
|
|
||||||
{
|
|
||||||
if (! Features::canManageTwoFactorAuthentication()) {
|
|
||||||
$this->markTestSkipped('Two-factor authentication is not enabled.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
Features::twoFactorAuthentication([
|
|
||||||
'confirm' => true,
|
|
||||||
'confirmPassword' => false,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->actingAs($user)
|
|
||||||
->get(route('two-factor.show'))
|
|
||||||
->assertOk()
|
|
||||||
->assertInertia(fn (Assert $page) => $page
|
|
||||||
->component('settings/TwoFactor'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_two_factor_settings_page_returns_forbidden_response_when_two_factor_is_disabled()
|
|
||||||
{
|
|
||||||
if (! Features::canManageTwoFactorAuthentication()) {
|
|
||||||
$this->markTestSkipped('Two-factor authentication is not enabled.');
|
|
||||||
}
|
|
||||||
|
|
||||||
config(['fortify.features' => []]);
|
|
||||||
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
$this->actingAs($user)
|
|
||||||
->withSession(['auth.password_confirmed_at' => time()])
|
|
||||||
->get(route('two-factor.show'))
|
|
||||||
->assertForbidden();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Services\IterationEventBuffer;
|
||||||
|
|
||||||
|
use App\Services\IterationEventBuffer\IPerceptronIterationEventBuffer;
|
||||||
|
|
||||||
|
class DullIterationEventBuffer implements IPerceptronIterationEventBuffer
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function flush(): void {}
|
||||||
|
|
||||||
|
public function addIteration(int $epoch, int $exampleIndex, float $error, array $synaptic_weights): void {}
|
||||||
|
}
|
||||||
73
tests/Unit/Training/ADALINEPerceptronTest.php
Normal file
73
tests/Unit/Training/ADALINEPerceptronTest.php
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit\Training;
|
||||||
|
|
||||||
|
use App\Models\NetworksTraining\ADALINEPerceptronTraining;
|
||||||
|
use App\Services\DatasetReader\LinearOrderDataSetReader;
|
||||||
|
use App\Services\SynapticWeightsProvider\ZeroSynapticWeights;
|
||||||
|
use Tests\Services\IterationEventBuffer\DullIterationEventBuffer;
|
||||||
|
|
||||||
|
class ADALINEPerceptronTest extends TrainingTestCase
|
||||||
|
{
|
||||||
|
public function test_adaline_perceptron_training_logic_and()
|
||||||
|
{
|
||||||
|
$training = new ADALINEPerceptronTraining(
|
||||||
|
datasetReader: new LinearOrderDataSetReader(public_path('data_sets/logic_and_gradient.csv')),
|
||||||
|
learningRate: 0.03,
|
||||||
|
maxEpochs: 10000,
|
||||||
|
synapticWeightsProvider: new ZeroSynapticWeights,
|
||||||
|
iterationEventBuffer: new DullIterationEventBuffer,
|
||||||
|
sessionId: 'test-session',
|
||||||
|
trainingId: 'test-training',
|
||||||
|
minError: 0.1251,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->verifyTrainingResults(
|
||||||
|
training: $training,
|
||||||
|
expectedWeights: [[[-1.503867, 0.992594, 0.976844]]],
|
||||||
|
expectedEpochs: 202,
|
||||||
|
marginOfError: 0.1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// public function test_adaline_perceptron_training_table_2_9()
|
||||||
|
// {
|
||||||
|
// $training = new ADALINEPerceptronTraining(
|
||||||
|
// datasetReader: new LinearOrderDataSetReader(public_path('data_sets/table_2_9.csv')),
|
||||||
|
// learningRate: 0.0012,
|
||||||
|
// maxEpochs: 1000,
|
||||||
|
// synapticWeightsProvider: new ZeroSynapticWeights(),
|
||||||
|
// iterationEventBuffer: new DullIterationEventBuffer(),
|
||||||
|
// sessionId: 'test-session',
|
||||||
|
// trainingId: 'test-training',
|
||||||
|
// minError: 5.670337, // Impossible pour un dataset avec des labels -1 et 1 d'avoir une erreur moyenne supérieure à 2
|
||||||
|
// );
|
||||||
|
|
||||||
|
// $this->verifyTrainingResults(
|
||||||
|
// training: $training,
|
||||||
|
// expectedWeights: [[[-0.664816, -0.522798, 0.342044]]],
|
||||||
|
// expectedEpochs: 92
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
public function test_adaline_perceptron_training_table_2_10()
|
||||||
|
{
|
||||||
|
$training = new ADALINEPerceptronTraining(
|
||||||
|
datasetReader: new LinearOrderDataSetReader(public_path('data_sets/table_2_10.csv')),
|
||||||
|
learningRate: 0.0015,
|
||||||
|
maxEpochs: 1000,
|
||||||
|
synapticWeightsProvider: new ZeroSynapticWeights,
|
||||||
|
iterationEventBuffer: new DullIterationEventBuffer,
|
||||||
|
sessionId: 'test-session',
|
||||||
|
trainingId: 'test-training',
|
||||||
|
// minError: 16.597077, // Impossible pour un dataset avec des labels -1 et 1 d'avoir une erreur moyenne supérieure à 2
|
||||||
|
minError: 0.000000001,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->verifyTrainingResults(
|
||||||
|
training: $training,
|
||||||
|
expectedWeights: [[[-0.022673, -0.25422, 0.294955]]],
|
||||||
|
expectedEpochs: 1000
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
72
tests/Unit/Training/GradientDescentPerceptronTest.php
Normal file
72
tests/Unit/Training/GradientDescentPerceptronTest.php
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit\Training;
|
||||||
|
|
||||||
|
use App\Models\NetworksTraining\GradientDescentPerceptronTraining;
|
||||||
|
use App\Services\DatasetReader\LinearOrderDataSetReader;
|
||||||
|
use App\Services\SynapticWeightsProvider\ZeroSynapticWeights;
|
||||||
|
use Tests\Services\IterationEventBuffer\DullIterationEventBuffer;
|
||||||
|
|
||||||
|
class GradientDescentPerceptronTest extends TrainingTestCase
|
||||||
|
{
|
||||||
|
public function test_gradient_descent_perceptron_training_logic_and()
|
||||||
|
{
|
||||||
|
$training = new GradientDescentPerceptronTraining(
|
||||||
|
datasetReader: new LinearOrderDataSetReader(public_path('data_sets/logic_and_gradient.csv')),
|
||||||
|
learningRate: 0.2,
|
||||||
|
maxEpochs: 100,
|
||||||
|
synapticWeightsProvider: new ZeroSynapticWeights,
|
||||||
|
iterationEventBuffer: new DullIterationEventBuffer,
|
||||||
|
sessionId: 'test-session',
|
||||||
|
trainingId: 'test-training',
|
||||||
|
minError: 0.125001,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->verifyTrainingResults(
|
||||||
|
training: $training,
|
||||||
|
expectedWeights: [[[-1.497898, 0.998228, 0.998228]]],
|
||||||
|
expectedEpochs: 49
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// public function test_gradient_descent_perceptron_training_table_2_9()
|
||||||
|
// {
|
||||||
|
// $training = new GradientDescentPerceptronTraining(
|
||||||
|
// datasetReader: new LinearOrderDataSetReader(public_path('data_sets/table_2_9.csv')),
|
||||||
|
// learningRate: 0.0011,
|
||||||
|
// maxEpochs: 1000,
|
||||||
|
// synapticWeightsProvider: new ZeroSynapticWeights(),
|
||||||
|
// iterationEventBuffer: new DullIterationEventBuffer(),
|
||||||
|
// sessionId: 'test-session',
|
||||||
|
// trainingId: 'test-training',
|
||||||
|
// minError: 5.524889, // Impossible pour un dataset avec des labels -1 et 1 d'avoir une erreur moyenne supérieure à 2
|
||||||
|
// );
|
||||||
|
|
||||||
|
// $this->verifyTrainingResults(
|
||||||
|
// training: $training,
|
||||||
|
// expectedWeights: [[[-0.664816, -0.522798, 0.342044]]],
|
||||||
|
// expectedEpochs: 402
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
public function test_gradient_descent_perceptron_training_table_2_10()
|
||||||
|
{
|
||||||
|
$training = new GradientDescentPerceptronTraining(
|
||||||
|
datasetReader: new LinearOrderDataSetReader(public_path('data_sets/table_2_10.csv')),
|
||||||
|
learningRate: 0.0015,
|
||||||
|
maxEpochs: 1000,
|
||||||
|
synapticWeightsProvider: new ZeroSynapticWeights,
|
||||||
|
iterationEventBuffer: new DullIterationEventBuffer,
|
||||||
|
sessionId: 'test-session',
|
||||||
|
trainingId: 'test-training',
|
||||||
|
// minError: 16.388103, // Impossible pour un dataset avec des labels -1 et 1 d'avoir une erreur moyenne supérieure à 2
|
||||||
|
minError: 0.000000001,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->verifyTrainingResults(
|
||||||
|
training: $training,
|
||||||
|
expectedWeights: [[[-0.04107, -0.263527, 0.286406]]],
|
||||||
|
expectedEpochs: 1000
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
30
tests/Unit/Training/SimplePerceptronTest.php
Normal file
30
tests/Unit/Training/SimplePerceptronTest.php
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit\Training;
|
||||||
|
|
||||||
|
use App\Models\NetworksTraining\SimpleBinaryPerceptronTraining;
|
||||||
|
use App\Services\DatasetReader\LinearOrderDataSetReader;
|
||||||
|
use App\Services\SynapticWeightsProvider\ZeroSynapticWeights;
|
||||||
|
use Tests\Services\IterationEventBuffer\DullIterationEventBuffer;
|
||||||
|
|
||||||
|
class SimplePerceptronTest extends TrainingTestCase
|
||||||
|
{
|
||||||
|
public function test_simple_perceptron_training_logic_and()
|
||||||
|
{
|
||||||
|
$training = new SimpleBinaryPerceptronTraining(
|
||||||
|
datasetReader: new LinearOrderDataSetReader(public_path('data_sets/logic_and.csv')),
|
||||||
|
learningRate: 1.0,
|
||||||
|
maxEpochs: 100,
|
||||||
|
synapticWeightsProvider: new ZeroSynapticWeights,
|
||||||
|
iterationEventBuffer: new DullIterationEventBuffer,
|
||||||
|
sessionId: 'test-session',
|
||||||
|
trainingId: 'test-training',
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->verifyTrainingResults(
|
||||||
|
training: $training,
|
||||||
|
expectedWeights: [[[-3.0, 2.0, 1.0]]],
|
||||||
|
expectedEpochs: 6
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
23
tests/Unit/Training/TrainingTestCase.php
Normal file
23
tests/Unit/Training/TrainingTestCase.php
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit\Training;
|
||||||
|
|
||||||
|
use App\Models\NetworksTraining\NetworkTraining;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class TrainingTestCase extends TestCase
|
||||||
|
{
|
||||||
|
public const DEFAULT_MARGIN_OF_ERROR = 0.001;
|
||||||
|
|
||||||
|
public function verifyTrainingResults(NetworkTraining $training, array $expectedWeights, int $expectedEpochs, float $marginOfError = self::DEFAULT_MARGIN_OF_ERROR): void
|
||||||
|
{
|
||||||
|
$training->start();
|
||||||
|
|
||||||
|
// Assert that the final synaptic weights are as expected withing the margin of error
|
||||||
|
// $finalWeights = $training->getSynapticWeights();
|
||||||
|
// $this->assertEqualsWithDelta($expectedWeights, $finalWeights, $marginOfError, "Final synaptic weights do not match expected values.");
|
||||||
|
|
||||||
|
// Assert that the number of epochs taken is as expected
|
||||||
|
$this->assertEquals($expectedEpochs, $training->getEpoch(), "Expected training to take $expectedEpochs epochs, but it took {$training->getEpoch()} epochs.");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user