Compare commits
74 Commits
e3b656a036
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 6545d23e4f | |||
| 24b2d29a41 | |||
| 151de1dcb8 | |||
| d3f914ba9a | |||
| 5f4f751406 | |||
| d5409b11b7 | |||
| 7cb528406c | |||
| e10b2f8d1f | |||
| 646e4841bb | |||
| 3efc323d28 | |||
| 21c02706de | |||
| 7c223e0ba8 | |||
| 6d8e3438f0 | |||
| ae676343a5 | |||
| 9389aef632 | |||
| fa6c427d76 | |||
| 25b03fc39a | |||
| d80bfe3cdd | |||
| 2332e805e8 | |||
| 4610334f72 | |||
| 9ae3a6942b | |||
| 3a19691591 | |||
| 2d7c1873bb | |||
| 5d68757e9d | |||
| 2de27af03b | |||
| be7b276dec | |||
| 2345b850b8 | |||
| 538ddc9679 | |||
| 099318c79e | |||
| 1517920fce | |||
| d4f98517cd | |||
| feac0feaeb | |||
| ba4b951a69 | |||
| 41b0e8a64a | |||
| da53bf16e9 | |||
| 3ad2e7d999 | |||
| 00f379811f | |||
| 3f1fcef1bc | |||
| 5d71962893 | |||
| 9086364db7 | |||
| 6d4fdffee9 | |||
| ca6c9fe38c | |||
| d9132fe6ee | |||
| e42849bed4 | |||
| 69e683bcaf | |||
| 0e177f8491 | |||
| 8a50f492ac | |||
| 08aa04fe56 | |||
| 2f4db07918 | |||
| f6620c2eca | |||
| ca9c0dc511 | |||
| 0876588550 | |||
| a37afcec07 | |||
| b052d792f8 | |||
| 5880024933 | |||
| a92a47288c | |||
| bcaf334380 | |||
| dea908c63e | |||
| 236fa503fb | |||
| 0f92af4a1e | |||
| ef90236adc | |||
| 9d4b02fab5 | |||
| 88a932391b | |||
| abb16aa6c1 | |||
| 29498e45ac | |||
| af67830fbb | |||
| 42e07de287 | |||
| 47991fe736 | |||
| 03d8688f56 | |||
| 72775d09ef | |||
| 87aa942e0b | |||
| 8af26db16c | |||
| 4717254da4 | |||
| 977c259cb9 |
@@ -0,0 +1,56 @@
|
|||||||
|
APP_NAME="PerceptronViewer"
|
||||||
|
APP_ENV=production
|
||||||
|
APP_KEY=base64:WE5DlJGDeIrl+B1ZqahVJSg4TN6oY3N1ph8CDKNo0w4=
|
||||||
|
APP_DEBUG=false
|
||||||
|
APP_URL=https://perceptron.matthiasg.dev
|
||||||
|
ASSET_URL=https://perceptron.matthiasg.dev
|
||||||
|
|
||||||
|
APP_LOCALE=fr
|
||||||
|
APP_FALLBACK_LOCALE=fr
|
||||||
|
APP_FAKER_LOCALE=fr_FR
|
||||||
|
|
||||||
|
APP_MAINTENANCE_DRIVER=file
|
||||||
|
# APP_MAINTENANCE_STORE=database
|
||||||
|
|
||||||
|
PHP_CLI_SERVER_WORKERS=4
|
||||||
|
|
||||||
|
BCRYPT_ROUNDS=12
|
||||||
|
|
||||||
|
LOG_CHANNEL=single
|
||||||
|
LOG_STACK=single
|
||||||
|
LOG_DEPRECATIONS_CHANNEL=null
|
||||||
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
|
SESSION_DRIVER=database
|
||||||
|
SESSION_LIFETIME=120
|
||||||
|
SESSION_ENCRYPT=false
|
||||||
|
SESSION_PATH=/
|
||||||
|
|
||||||
|
BROADCAST_CONNECTION=reverb
|
||||||
|
FILESYSTEM_DISK=local
|
||||||
|
QUEUE_CONNECTION=database
|
||||||
|
|
||||||
|
CACHE_STORE=database
|
||||||
|
# CACHE_PREFIX=
|
||||||
|
|
||||||
|
MEMCACHED_HOST=memcached
|
||||||
|
|
||||||
|
VITE_APP_NAME="${APP_NAME}"
|
||||||
|
|
||||||
|
REVERB_SERVER_HOST=0.0.0.0
|
||||||
|
REVERB_SERVER_PORT=8080
|
||||||
|
REVERB_APP_ID=848245
|
||||||
|
REVERB_APP_KEY=hkybzbkdvdl1hsvtzuqk
|
||||||
|
REVERB_APP_SECRET=oosuq0v9jgaslzp9cmhv
|
||||||
|
REVERB_HOST="perceptron.matthiasg.dev"
|
||||||
|
REVERB_PORT=443
|
||||||
|
REVERB_SCHEME=https
|
||||||
|
REVERB_BROADCAST_HOST=reverb
|
||||||
|
REVERB_BROADCAST_PORT=8080
|
||||||
|
REVERB_BROADCAST_SCHEME=http
|
||||||
|
REVERB_MAX_REQUEST_SIZE=100000
|
||||||
|
|
||||||
|
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
|
||||||
|
VITE_REVERB_HOST="${REVERB_HOST}"
|
||||||
|
VITE_REVERB_PORT="${REVERB_PORT}"
|
||||||
|
VITE_REVERB_SCHEME="${REVERB_SCHEME}"
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
name: Build and Push Docker Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["tests"]
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
env:
|
||||||
|
RUNNER_TOOL_CACHE: /toolcache
|
||||||
|
REVERB_APP_KEY: ${{ secrets.REVERB_APP_KEY }}
|
||||||
|
REVERB_APP_SECRET: ${{ secrets.REVERB_APP_SECRET }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Get version
|
||||||
|
id: get_version
|
||||||
|
run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: git.matthiasg.dev
|
||||||
|
username: ninluc
|
||||||
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: Dockerfile
|
||||||
|
push: true
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
build-args: |
|
||||||
|
VITE_REVERB_APP_KEY=${{ secrets.REVERB_APP_KEY }}
|
||||||
|
VITE_REVERB_HOST=perceptron.matthiasg.dev
|
||||||
|
VITE_REVERB_PORT=443
|
||||||
|
VITE_REVERB_SCHEME=https
|
||||||
|
tags: |
|
||||||
|
git.matthiasg.dev/ninluc/perceptron-viewer/perceptron-viewer:latest
|
||||||
|
git.matthiasg.dev/ninluc/perceptron-viewer/perceptron-viewer:${{ steps.get_version.outputs.version }}
|
||||||
|
|
||||||
|
# notify:
|
||||||
|
# needs: build
|
||||||
|
# runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
# steps:
|
||||||
|
# - name: Notify Watchtower
|
||||||
|
# run: "curl -i -X POST -H \"Authorization: Bearer ${{ secrets.WATCHTOWER_TOKEN }}\" https://watchtower.beermaker.matthiasg.dev/v1/update"
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
name: linter
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
- workos
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
- workos
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
quality:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- 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
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '8.4'
|
||||||
|
coverage: none
|
||||||
|
tools: composer:v2
|
||||||
|
env:
|
||||||
|
phpexts: none
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# Install dependencies
|
||||||
|
# -------------------------
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: |
|
||||||
|
composer install --no-interaction --prefer-dist --no-progress --no-scripts
|
||||||
|
npm ci
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# Run linters in parallel
|
||||||
|
# -------------------------
|
||||||
|
- name: Run linters
|
||||||
|
run: |
|
||||||
|
composer lint &
|
||||||
|
npm run format &
|
||||||
|
npm run lint &
|
||||||
|
wait
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
name: tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
- workos
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
- workos
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ci:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
php-version: ['8.3']
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- 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 }}-${{ 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
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: ${{ matrix.php-version }}
|
||||||
|
tools: composer:v2
|
||||||
|
coverage: none
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# Install dependencies
|
||||||
|
# -------------------------
|
||||||
|
- name: Install Node Dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Install PHP Dependencies
|
||||||
|
run: composer install --no-interaction --prefer-dist --optimize-autoloader --no-progress
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# Laravel setup
|
||||||
|
# -------------------------
|
||||||
|
- name: Prepare environment
|
||||||
|
run: |
|
||||||
|
cp .env.example .env
|
||||||
|
php artisan key:generate
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# Build (optional – remove if not needed for tests)
|
||||||
|
# -------------------------
|
||||||
|
# - name: Build Assets
|
||||||
|
# run: npm run build
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# Run tests (parallel)
|
||||||
|
# -------------------------
|
||||||
|
- name: Run Tests
|
||||||
|
run: php artisan test --parallel
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
name: linter
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
- main
|
|
||||||
- master
|
|
||||||
- workos
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
- main
|
|
||||||
- master
|
|
||||||
- workos
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
quality:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- name: Setup PHP
|
|
||||||
uses: shivammathur/setup-php@v2
|
|
||||||
with:
|
|
||||||
php-version: '8.4'
|
|
||||||
|
|
||||||
- name: Install Dependencies
|
|
||||||
run: |
|
|
||||||
composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
|
|
||||||
npm install
|
|
||||||
|
|
||||||
- name: Run Pint
|
|
||||||
run: composer lint
|
|
||||||
|
|
||||||
- name: Format Frontend
|
|
||||||
run: npm run format
|
|
||||||
|
|
||||||
- name: Lint Frontend
|
|
||||||
run: npm run lint
|
|
||||||
|
|
||||||
# - name: Commit Changes
|
|
||||||
# uses: stefanzweifel/git-auto-commit-action@v7
|
|
||||||
# with:
|
|
||||||
# commit_message: fix code style
|
|
||||||
# commit_options: '--no-verify'
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
name: tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
- main
|
|
||||||
- master
|
|
||||||
- workos
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
- main
|
|
||||||
- master
|
|
||||||
- workos
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
ci:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
php-version: ['8.4', '8.5']
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- name: Setup PHP
|
|
||||||
uses: shivammathur/setup-php@v2
|
|
||||||
with:
|
|
||||||
php-version: ${{ matrix.php-version }}
|
|
||||||
tools: composer:v2
|
|
||||||
coverage: xdebug
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '22'
|
|
||||||
|
|
||||||
- name: Install Node Dependencies
|
|
||||||
run: npm i
|
|
||||||
|
|
||||||
- name: Install Dependencies
|
|
||||||
run: composer install --no-interaction --prefer-dist --optimize-autoloader
|
|
||||||
|
|
||||||
- name: Copy Environment File
|
|
||||||
run: cp .env.example .env
|
|
||||||
|
|
||||||
- name: Generate Application Key
|
|
||||||
run: php artisan key:generate
|
|
||||||
|
|
||||||
- name: Build Assets
|
|
||||||
run: npm run build
|
|
||||||
|
|
||||||
- name: Tests
|
|
||||||
run: ./vendor/bin/phpunit
|
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# Composer dependencies
|
||||||
|
FROM composer:2 AS vendor
|
||||||
|
|
||||||
|
WORKDIR /usr/app
|
||||||
|
COPY . .
|
||||||
|
RUN composer install --no-dev --optimize-autoloader --no-interaction
|
||||||
|
|
||||||
|
# Build frontend assets
|
||||||
|
# Install node and npm
|
||||||
|
RUN apk add --no-cache nodejs npm
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
ARG VITE_REVERB_APP_KEY
|
||||||
|
ARG VITE_REVERB_HOST
|
||||||
|
ARG VITE_REVERB_PORT=443
|
||||||
|
ARG VITE_REVERB_SCHEME=https
|
||||||
|
ENV VITE_REVERB_APP_KEY=$VITE_REVERB_APP_KEY \
|
||||||
|
VITE_REVERB_HOST=$VITE_REVERB_HOST \
|
||||||
|
VITE_REVERB_PORT=$VITE_REVERB_PORT \
|
||||||
|
VITE_REVERB_SCHEME=$VITE_REVERB_SCHEME
|
||||||
|
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Final PHP image
|
||||||
|
FROM dunglas/frankenphp:1.12.2-php8.3-alpine AS final
|
||||||
|
|
||||||
|
COPY docker/php.ini-production "$PHP_INI_DIR/php.ini"
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
bash \
|
||||||
|
libpng-dev \
|
||||||
|
libjpeg-turbo-dev \
|
||||||
|
freetype-dev \
|
||||||
|
oniguruma-dev \
|
||||||
|
zip \
|
||||||
|
unzip \
|
||||||
|
git \
|
||||||
|
curl \
|
||||||
|
libpq-dev \
|
||||||
|
php-pgsql
|
||||||
|
|
||||||
|
# Install PHP extensions
|
||||||
|
RUN docker-php-ext-install pdo pgsql pdo_pgsql mbstring exif pcntl bcmath gd
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy app
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN mv .env.docker .env
|
||||||
|
|
||||||
|
# Copy dependencies
|
||||||
|
COPY --from=vendor /usr/app/vendor ./vendor
|
||||||
|
|
||||||
|
# Copy built frontend
|
||||||
|
COPY --from=vendor /usr/app/public/build ./public/build
|
||||||
|
|
||||||
|
# Permissions
|
||||||
|
#RUN chown -R www-data:www-data /app
|
||||||
|
#RUN chown www-data:www-data /app/dockerEntryPoint.sh
|
||||||
|
RUN chmod +x /app/dockerEntryPoint.sh
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=30s --start-period=30s --retries=3 CMD curl -f http://127.0.0.1/up || exit 1
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
CMD ["./dockerEntryPoint.sh"]
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Perceptron and neuronal networks
|
# PerceptronViewer
|
||||||
|
|
||||||
Using Laravel and Vue JS
|
Using Laravel and Vue JS
|
||||||
|
|
||||||
@@ -23,10 +23,10 @@ Using Laravel and Vue JS
|
|||||||
3. NodeJs (Node + NPM)
|
3. NodeJs (Node + NPM)
|
||||||
<https://nodejs.org/en/download>
|
<https://nodejs.org/en/download>
|
||||||
|
|
||||||
2. Install dependencies
|
2. Setup project and install dependencies
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
composer install
|
composer run setup
|
||||||
```
|
```
|
||||||
|
|
||||||
## Running the project
|
## Running the project
|
||||||
@@ -40,3 +40,13 @@ composer run dev
|
|||||||
```
|
```
|
||||||
|
|
||||||
And go with your favorite browser to <http://127.0.0.1:8000/>
|
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.
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ namespace App\Events;
|
|||||||
use App\Models\ActivationsFunctions;
|
use App\Models\ActivationsFunctions;
|
||||||
use Illuminate\Broadcasting\Channel;
|
use Illuminate\Broadcasting\Channel;
|
||||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
|
||||||
use Illuminate\Foundation\Events\Dispatchable;
|
use Illuminate\Foundation\Events\Dispatchable;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
class PerceptronInitialization implements ShouldBroadcast
|
class PerceptronInitialization implements ShouldBroadcastNow
|
||||||
{
|
{
|
||||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||||
|
|
||||||
@@ -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,13 +4,11 @@ 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\Contracts\Broadcasting\ShouldBroadcastNow;
|
||||||
use Illuminate\Broadcasting\PrivateChannel;
|
|
||||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
|
||||||
use Illuminate\Foundation\Events\Dispatchable;
|
use Illuminate\Foundation\Events\Dispatchable;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
class PerceptronTrainingEnded implements ShouldBroadcast
|
class PerceptronTrainingEnded implements ShouldBroadcastNow
|
||||||
{
|
{
|
||||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||||
|
|
||||||
@@ -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'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ namespace App\Events;
|
|||||||
|
|
||||||
use Illuminate\Broadcasting\Channel;
|
use Illuminate\Broadcasting\Channel;
|
||||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
|
||||||
use Illuminate\Foundation\Events\Dispatchable;
|
use Illuminate\Foundation\Events\Dispatchable;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
|
||||||
class PerceptronTrainingIteration implements ShouldBroadcast
|
class PerceptronTrainingIteration implements ShouldBroadcastNow
|
||||||
{
|
{
|
||||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||||
|
|
||||||
@@ -19,8 +20,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,15 +33,47 @@ 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'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function broadcastWith(): array
|
public function broadcastWith(): array
|
||||||
{
|
{
|
||||||
|
$iterations = self::normalizeForJson($this->iterations);
|
||||||
|
$weights = collect($iterations)
|
||||||
|
->pluck('weights')
|
||||||
|
->first(fn (array $weights): bool => $weights !== []);
|
||||||
|
$shouldBroadcastAllWeights = $weights !== null
|
||||||
|
&& count(Arr::flatten($weights)) <= config('perceptron.max_displayed_weights');
|
||||||
|
|
||||||
|
$lastIterationIndex = count($iterations) - 1;
|
||||||
|
$iterations = array_map(
|
||||||
|
fn (array $iteration, int $index): array => $shouldBroadcastAllWeights || $index === $lastIterationIndex
|
||||||
|
? $iteration
|
||||||
|
: [...$iteration, 'weights' => []],
|
||||||
|
$iterations,
|
||||||
|
array_keys($iterations),
|
||||||
|
);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'iterations' => $this->iterations,
|
'iterations' => $iterations,
|
||||||
'trainingId' => $this->trainingId,
|
'trainingId' => $this->trainingId,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function normalizeForJson(mixed $value): mixed
|
||||||
|
{
|
||||||
|
if (is_float($value) && ! is_finite($value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! is_array($value)) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_map(
|
||||||
|
fn (mixed $item): mixed => self::normalizeForJson($item),
|
||||||
|
$value,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exceptions;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
class TrainingCancelledException extends RuntimeException
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -3,35 +3,71 @@
|
|||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Events\PerceptronInitialization;
|
use App\Events\PerceptronInitialization;
|
||||||
use App\Models\GradientDescentPerceptronTraining;
|
use App\Exceptions\TrainingCancelledException;
|
||||||
use App\Models\SimpleBinaryPerceptronTraining;
|
use App\Http\Requests\RunPerceptronRequest;
|
||||||
use App\Services\DataSetReader;
|
use App\Models\NetworksTraining\ADALINEPerceptronTraining;
|
||||||
use App\Services\ISynapticWeightsProvider;
|
use App\Models\NetworksTraining\GradientDescentPerceptronTraining;
|
||||||
use App\Services\PerceptronIterationEventBuffer;
|
use App\Models\NetworksTraining\MonoLayerPerceptronTraining;
|
||||||
use App\Services\PerceptronLimitedEpochEventBuffer;
|
use App\Models\NetworksTraining\MultiLayerPerceptronTraining;
|
||||||
use App\Services\ZeroSynapticWeights;
|
use App\Models\NetworksTraining\SimpleBinaryPerceptronTraining;
|
||||||
|
use App\Services\DatasetReader\IDataSetReader;
|
||||||
|
use App\Services\DatasetReader\LinearOrderDataSetReader;
|
||||||
|
use App\Services\DatasetReader\RandomOrderDataSetReader;
|
||||||
|
use App\Services\IterationEventBuffer\PerceptronIterationEventBuffer;
|
||||||
|
use App\Services\IterationEventBuffer\PerceptronLimitedEpochEventBuffer;
|
||||||
|
use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider;
|
||||||
|
use App\Services\SynapticWeightsProvider\RandomSynapticWeights;
|
||||||
|
use App\Services\SynapticWeightsProvider\ZeroSynapticWeights;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
class PerceptronController extends Controller
|
class PerceptronController extends Controller
|
||||||
{
|
{
|
||||||
|
private function cancellationKey(string $trainingId): string
|
||||||
|
{
|
||||||
|
return "perceptron-training-cancelled:{$trainingId}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cancel(Request $request)
|
||||||
|
{
|
||||||
|
$trainingId = $request->validate([
|
||||||
|
'training_id' => ['required', 'string', 'max:100'],
|
||||||
|
])['training_id'];
|
||||||
|
|
||||||
|
Cache::put($this->cancellationKey($trainingId), true, now()->addHour());
|
||||||
|
|
||||||
|
return response()->noContent();
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Display the specified resource.
|
* Display the specified resource.
|
||||||
*/
|
*/
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$perceptronType = $request->query('type');
|
$perceptronType = $request->query('type', 'simple'); ;
|
||||||
|
|
||||||
$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 'adaline':
|
||||||
|
$learningRate = 0.00003;
|
||||||
|
break;
|
||||||
case 'gradientdescent':
|
case 'gradientdescent':
|
||||||
$learningRate = 0.00003;
|
$learningRate = 0.00003;
|
||||||
|
$maxIterations = 300;
|
||||||
|
break;
|
||||||
|
case 'monolayer':
|
||||||
|
$learningRate = 0.001;
|
||||||
|
break;
|
||||||
|
case 'multilayer':
|
||||||
|
$learningRate = 0.8;
|
||||||
|
$maxIterations = 2000;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return inertia('PerceptronViewer', [
|
return inertia('PerceptronViewer', [
|
||||||
@@ -41,6 +77,8 @@ class PerceptronController extends Controller
|
|||||||
'minError' => $minError,
|
'minError' => $minError,
|
||||||
'learningRate' => $learningRate,
|
'learningRate' => $learningRate,
|
||||||
'maxIterations' => $maxIterations,
|
'maxIterations' => $maxIterations,
|
||||||
|
'maxIterationsLimit' => config('perceptron.max_iterations'),
|
||||||
|
'maxDisplayedWeights' => config('perceptron.max_displayed_weights'),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,9 +89,14 @@ class PerceptronController extends Controller
|
|||||||
$datasets = [];
|
$datasets = [];
|
||||||
foreach ($files as $file) {
|
foreach ($files as $file) {
|
||||||
if (pathinfo($file, PATHINFO_EXTENSION) === 'csv') {
|
if (pathinfo($file, PATHINFO_EXTENSION) === 'csv') {
|
||||||
|
if (str_starts_with($file, 'hidden')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$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['inputCount'] = count($dataSetReader->lines[0]) - 1;
|
||||||
$dataset['data'] = [];
|
$dataset['data'] = [];
|
||||||
switch (count($dataSetReader->lines[0])) {
|
switch (count($dataSetReader->lines[0])) {
|
||||||
case 3:
|
case 3:
|
||||||
@@ -80,76 +123,167 @@ class PerceptronController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch ($dataset['label']) {
|
switch ($dataset['label']) {
|
||||||
case 'logic_and_gradient':
|
case 'Classification_-_Porte_logique_ET_(linéairement_séparable)':
|
||||||
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;
|
||||||
|
case 'monolayer':
|
||||||
|
$dataset['defaultMinError'] = 0.001;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'table_2_9':
|
case 'Classification_-_Porte_logique_XOR_(non_linéairement_séparable)':
|
||||||
|
switch ($perceptronType) {
|
||||||
|
case 'multilayer':
|
||||||
|
$dataset['defaultLearningRate'] = 0.3;
|
||||||
|
$dataset['defaultMinError'] = 0.001;
|
||||||
|
$dataset['defaultMaxIterations'] = 500;
|
||||||
|
$dataset['defaultHiddenLayers'] = 1;
|
||||||
|
$dataset['defaultHiddenLayersNeurons'] = 2;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'Classification_-_Oblique_(linéairement_séparable)':
|
||||||
switch ($perceptronType) {
|
switch ($perceptronType) {
|
||||||
case 'simple':
|
case 'simple':
|
||||||
$dataset['defaultLearningRate'] = 0.015;
|
$dataset['defaultLearningRate'] = 0.015;
|
||||||
break;
|
break;
|
||||||
case 'gradientdescent':
|
case 'gradientdescent':
|
||||||
|
case 'adaline':
|
||||||
$dataset['defaultLearningRate'] = 0.001;
|
$dataset['defaultLearningRate'] = 0.001;
|
||||||
$dataset['defaultMinError'] = 2.0;
|
$dataset['defaultMinError'] = 0.09;
|
||||||
|
$dataset['defaultMaxIterations'] = 2000;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'table_2_11':
|
case 'Classification_-_Oblique_modifié_(non_linéairement_séparable)':
|
||||||
$dataset['defaultMinError'] = 1.0;
|
switch ($perceptronType) {
|
||||||
|
case 'gradientdescent':
|
||||||
|
case 'adaline':
|
||||||
|
$dataset['defaultLearningRate'] = 0.001;
|
||||||
|
$dataset['defaultMinError'] = 0.3;
|
||||||
|
$dataset['defaultMaxIterations'] = 2000;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'Régression_-_Oblique':
|
||||||
|
$dataset['defaultMinError'] = 0.02;
|
||||||
|
switch ($perceptronType) {
|
||||||
|
case 'gradientdescent':
|
||||||
|
case 'adaline':
|
||||||
|
$dataset['defaultMinError'] = 0.68;
|
||||||
|
$dataset['defaultMaxIterations'] = 100;
|
||||||
|
break;
|
||||||
|
case 'monolayer':
|
||||||
|
$dataset['defaultLearningRate'] = 0.0015;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'Classification_-_3_classes_(linéairement_séparable)':
|
||||||
|
switch ($perceptronType) {
|
||||||
|
case 'monolayer':
|
||||||
|
$dataset['defaultLearningRate'] = 0.005;
|
||||||
|
$dataset['defaultMaxIterations'] = 100;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'Classification_-_Donut_(non_linéairement_séparable)':
|
||||||
|
switch ($perceptronType) {
|
||||||
|
case 'multilayer':
|
||||||
|
$dataset['defaultLearningRate'] = 0.8;
|
||||||
|
$dataset['defaultMinError'] = 0.0001;
|
||||||
|
$dataset['defaultMaxIterations'] = 2000;
|
||||||
|
$dataset['defaultHiddenLayers'] = 3;
|
||||||
|
$dataset['defaultHiddenLayersNeurons'] = 4;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'Régression_-_Vague':
|
||||||
|
$dataset['defaultMinError'] = 0.055;
|
||||||
|
switch ($perceptronType) {
|
||||||
|
case 'multilayer':
|
||||||
|
$dataset['defaultLearningRate'] = 0.3;
|
||||||
|
$dataset['defaultMaxIterations'] = 400;
|
||||||
|
$dataset['defaultHiddenLayers'] = 2;
|
||||||
|
$dataset['defaultHiddenLayersNeurons'] = 2;
|
||||||
|
break;
|
||||||
|
}
|
||||||
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(RunPerceptronRequest $request, ISynapticWeightsProvider $synapticWeightsProvider)
|
||||||
{
|
{
|
||||||
|
$startTime = microtime(true);
|
||||||
|
|
||||||
$perceptronType = $request->input('type');
|
$perceptronType = $request->input('type');
|
||||||
|
$hiddenLayers = $request->input('hidden_layers', 2);
|
||||||
|
$hiddenLayersNeurons = $request->input('hidden_layers_neurons', 3);
|
||||||
$minError = $request->input('min_error', 0.01);
|
$minError = $request->input('min_error', 0.01);
|
||||||
$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') {
|
Cache::forget($this->cancellationKey($trainingId));
|
||||||
$synapticWeightsProvider = new ZeroSynapticWeights();
|
|
||||||
|
// Zero initialization prevents hidden layers from receiving a gradient.
|
||||||
|
if ($perceptronType === 'multilayer' && $weightInitMethod === 'zeros') {
|
||||||
|
$synapticWeightsProvider = new RandomSynapticWeights;
|
||||||
|
} elseif ($weightInitMethod === 'zeros') {
|
||||||
|
$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, fn (): bool => connection_aborted() || Cache::has($this->cancellationKey($trainingId))),
|
||||||
'gradientdescent' => new GradientDescentPerceptronTraining($dataSetReader, $learningRate, $maxIterations, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError),
|
'gradientdescent' => new GradientDescentPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError, fn (): bool => connection_aborted() || Cache::has($this->cancellationKey($trainingId))),
|
||||||
|
'adaline' => new ADALINEPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError, fn (): bool => connection_aborted() || Cache::has($this->cancellationKey($trainingId))),
|
||||||
|
'monolayer' => new MonoLayerPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError, fn (): bool => connection_aborted() || Cache::has($this->cancellationKey($trainingId))),
|
||||||
|
'multilayer' => new MultiLayerPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $hiddenLayers, $hiddenLayersNeurons, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError, fn (): bool => connection_aborted() || Cache::has($this->cancellationKey($trainingId))),
|
||||||
default => null,
|
default => null,
|
||||||
};
|
};
|
||||||
|
|
||||||
event(new PerceptronInitialization($dataSetReader->lines, $networkTraining->activationFunction, $sessionId, $trainingId));
|
event(new PerceptronInitialization($datasetReader->lines, $networkTraining->activationFunction, $sessionId, $trainingId));
|
||||||
|
|
||||||
$networkTraining->start();
|
try {
|
||||||
|
$networkTraining->start();
|
||||||
|
} catch (TrainingCancelledException) {
|
||||||
|
$networkTraining->cancel();
|
||||||
|
Cache::forget($this->cancellationKey($trainingId));
|
||||||
|
}
|
||||||
|
|
||||||
return response()->json([
|
return back()->with('success', [
|
||||||
'message' => 'Training completed',
|
'message' => 'Training completed',
|
||||||
|
'execution_time' => microtime(true) - $startTime,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class RunPerceptronRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => ['required', 'string', 'in:simple,gradientdescent,adaline,monolayer,multilayer'],
|
||||||
|
'dataset' => ['required', 'string', 'max:200'],
|
||||||
|
'weight_init_method' => ['required', 'string', 'in:random,zeros'],
|
||||||
|
'learning_rate' => ['required', 'numeric', 'min:0'],
|
||||||
|
'min_error' => ['required', 'numeric', 'min:0'],
|
||||||
|
'hidden_layers' => ['required', 'integer', 'min:1', 'max:5'],
|
||||||
|
'hidden_layers_neurons' => ['required', 'integer', 'min:1', 'max:5'],
|
||||||
|
'max_iterations' => ['required', 'integer', 'min:1', 'max:'.config('perceptron.max_iterations')],
|
||||||
|
'session_id' => ['required', 'string', 'max:100'],
|
||||||
|
'training_id' => ['required', 'string', 'max:100'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
abstract class Network
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use App\Events\PerceptronTrainingEnded;
|
|
||||||
use App\Services\DataSetReader;
|
|
||||||
use App\Services\IPerceptronIterationEventBuffer;
|
|
||||||
|
|
||||||
abstract class NetworkTraining
|
|
||||||
{
|
|
||||||
protected int $epoch = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @abstract
|
|
||||||
* @var ActivationsFunctions
|
|
||||||
*/
|
|
||||||
public ActivationsFunctions $activationFunction;
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
protected DataSetReader $datasetReader,
|
|
||||||
protected int $maxEpochs,
|
|
||||||
protected IPerceptronIterationEventBuffer $iterationEventBuffer,
|
|
||||||
protected string $sessionId,
|
|
||||||
protected string $trainingId,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract public function start() : void;
|
|
||||||
abstract protected function stopCondition(): bool;
|
|
||||||
|
|
||||||
protected function checkPassedMaxIterations(?float $finalError) {
|
|
||||||
if ($this->epoch >= $this->maxEpochs) {
|
|
||||||
$message = 'Le nombre maximal d\'epoch a été atteint';
|
|
||||||
if ($finalError) {
|
|
||||||
$message .= " avec une erreur finale de $finalError";
|
|
||||||
}
|
|
||||||
|
|
||||||
event(new PerceptronTrainingEnded($message, $this->sessionId, $this->trainingId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function addIterationToBuffer(float $error, array $synapticWeights) {
|
|
||||||
$this->iterationEventBuffer->addIteration($this->epoch, $this->datasetReader->getLastReadLineIndex(), $error, $synapticWeights);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models\NetworksTraining;
|
||||||
|
|
||||||
|
use App\Models\ActivationsFunctions;
|
||||||
|
use App\Models\Perceptrons\GradientDescentPerceptron;
|
||||||
|
use App\Models\Perceptrons\Perceptron;
|
||||||
|
use App\Services\DatasetReader\IDataSetReader;
|
||||||
|
use App\Services\IterationEventBuffer\IPerceptronIterationEventBuffer;
|
||||||
|
use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider;
|
||||||
|
use Closure;
|
||||||
|
|
||||||
|
class ADALINEPerceptronTraining extends NetworkTraining
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
?Closure $isCancelled = null,
|
||||||
|
) {
|
||||||
|
parent::__construct($datasetReader, $maxEpochs, $iterationEventBuffer, $sessionId, $trainingId, $isCancelled);
|
||||||
|
$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)[0];
|
||||||
|
$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) {
|
||||||
|
$this->broadcastTrainingEnded('Le perceptron à atteint l\'erreur minimale');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function iterationFunction(array $inputs, float $correctOutput): float
|
||||||
|
{
|
||||||
|
$output = $this->perceptron->test($inputs)[0];
|
||||||
|
|
||||||
|
$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\Models\ActivationsFunctions;
|
||||||
use App\Services\DataSetReader;
|
use App\Models\Perceptrons\GradientDescentPerceptron;
|
||||||
use App\Services\IPerceptronIterationEventBuffer;
|
use App\Models\Perceptrons\Perceptron;
|
||||||
use App\Services\ISynapticWeightsProvider;
|
use App\Services\DatasetReader\IDataSetReader;
|
||||||
use App\Services\PerceptronIterationEventBuffer;
|
use App\Services\IterationEventBuffer\IPerceptronIterationEventBuffer;
|
||||||
|
use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider;
|
||||||
|
use Closure;
|
||||||
|
|
||||||
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,
|
||||||
@@ -25,8 +27,9 @@ class GradientDescentPerceptronTraining extends NetworkTraining
|
|||||||
string $sessionId,
|
string $sessionId,
|
||||||
string $trainingId,
|
string $trainingId,
|
||||||
private float $minError,
|
private float $minError,
|
||||||
|
?Closure $isCancelled = null,
|
||||||
) {
|
) {
|
||||||
parent::__construct($datasetReader, $maxEpochs, $iterationEventBuffer, $sessionId, $trainingId);
|
parent::__construct($datasetReader, $maxEpochs, $iterationEventBuffer, $sessionId, $trainingId, $isCancelled);
|
||||||
$this->perceptron = new GradientDescentPerceptron($synapticWeightsProvider->generate($datasetReader->getInputSize()));
|
$this->perceptron = new GradientDescentPerceptron($synapticWeightsProvider->generate($datasetReader->getInputSize()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,7 +41,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 +58,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,19 +79,25 @@ 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));
|
$this->broadcastTrainingEnded('Le perceptron à atteint l\'erreur minimale');
|
||||||
}
|
}
|
||||||
|
|
||||||
return $condition;
|
return $condition;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function iterationFunction(array $inputs, int $correctOutput)
|
private function iterationFunction(array $inputs, float $correctOutput): float
|
||||||
{
|
{
|
||||||
$output = $this->perceptron->test($inputs);
|
$output = $this->perceptron->test($inputs)[0];
|
||||||
|
|
||||||
$error = $correctOutput - $output;
|
$error = $correctOutput - $output;
|
||||||
|
|
||||||
return $error;
|
return $error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getSynapticWeights(): array
|
||||||
|
{
|
||||||
|
return [[$this->perceptron->getSynapticWeights()]];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models\NetworksTraining;
|
||||||
|
|
||||||
|
use App\Models\ActivationsFunctions;
|
||||||
|
use App\Models\Perceptrons\GradientDescentPerceptron;
|
||||||
|
use App\Models\Perceptrons\NetworkPerceptron;
|
||||||
|
use App\Models\Perceptrons\Perceptron;
|
||||||
|
use App\Services\DatasetReader\IDataSetReader;
|
||||||
|
use App\Services\IterationEventBuffer\IPerceptronIterationEventBuffer;
|
||||||
|
use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider;
|
||||||
|
use App\Services\SynapticWeightsProvider\SimpleNetworkWeightsProvider;
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
|
||||||
|
class MonoLayerPerceptronTraining extends NetworkTraining
|
||||||
|
{
|
||||||
|
private Perceptron $network;
|
||||||
|
|
||||||
|
private array $labels;
|
||||||
|
|
||||||
|
private bool $isRegression;
|
||||||
|
|
||||||
|
public ActivationsFunctions $activationFunction = ActivationsFunctions::LINEAR;
|
||||||
|
|
||||||
|
public ?ActivationsFunctions $presentationLayerActivationFunction = ActivationsFunctions::STEP;
|
||||||
|
|
||||||
|
private float $epochError;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
IDataSetReader $datasetReader,
|
||||||
|
protected float $learningRate,
|
||||||
|
int $maxEpochs,
|
||||||
|
ISynapticWeightsProvider $synapticWeightsProvider,
|
||||||
|
IPerceptronIterationEventBuffer $iterationEventBuffer,
|
||||||
|
string $sessionId,
|
||||||
|
string $trainingId,
|
||||||
|
private float $minError,
|
||||||
|
?Closure $isCancelled = null,
|
||||||
|
) {
|
||||||
|
parent::__construct($datasetReader, $maxEpochs, $iterationEventBuffer, $sessionId, $trainingId, $isCancelled);
|
||||||
|
$this->isRegression = $datasetReader->getInputSize() === 1;
|
||||||
|
$networkWeightsProvider = new SimpleNetworkWeightsProvider($synapticWeightsProvider);
|
||||||
|
$this->network = new NetworkPerceptron(
|
||||||
|
$networkWeightsProvider->generate(
|
||||||
|
$datasetReader->getInputSize(),
|
||||||
|
$this->isRegression ? 1 : $datasetReader->getOutputSize(),
|
||||||
|
0, // No hidden layer
|
||||||
|
0, // No hidden layer neurons
|
||||||
|
),
|
||||||
|
$datasetReader->getInputSize(),
|
||||||
|
GradientDescentPerceptron::class, // No hidden layer
|
||||||
|
GradientDescentPerceptron::class,
|
||||||
|
);
|
||||||
|
$this->labels = $datasetReader->getLabels();
|
||||||
|
}
|
||||||
|
|
||||||
|
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->network->getSynapticWeights();
|
||||||
|
$inputs_with_bias = array_merge([1], $inputs); // Add bias input
|
||||||
|
|
||||||
|
// Updates the weights
|
||||||
|
$this->network->setSynapticWeights(
|
||||||
|
$this->getUpdatedSynapticWeights($synaptic_weights, $iterationError, $inputs_with_bias)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Broadcast the training iteration event
|
||||||
|
$this->addIterationToBuffer(array_sum($iterationError), $this->network->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);
|
||||||
|
$iterationError = $this->iterationFunction($inputs, $correctOutput);
|
||||||
|
foreach ($iterationError as $error) {
|
||||||
|
$this->epochError += ($error ** 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) {
|
||||||
|
$this->broadcastTrainingEnded('Le perceptron à atteint l\'erreur minimale');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function iterationFunction(array $inputs, float $correctOutput): array
|
||||||
|
{
|
||||||
|
$outputs = $this->network->test($inputs);
|
||||||
|
$desiredOutput = $this->getDesiredOutputFromCorrectOutput($correctOutput);
|
||||||
|
|
||||||
|
$errors = [];
|
||||||
|
foreach ($outputs as $index => $output) {
|
||||||
|
$error = $desiredOutput[$index] - $output;
|
||||||
|
$errors[] = $error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getUpdatedSynapticWeights(array $synaptic_weights, array $iterationError, array $inputs): array
|
||||||
|
{
|
||||||
|
$updatedWeights = [];
|
||||||
|
foreach ($synaptic_weights[0] as $neuronIndex => $neuronWeights) { // There is only one layer of weights
|
||||||
|
$updatedNeuronWeights = [];
|
||||||
|
foreach ($neuronWeights as $weightIndex => $weight) {
|
||||||
|
$updatedWeight = $weight + ($this->learningRate * $iterationError[$neuronIndex] * $inputs[$weightIndex]);
|
||||||
|
$updatedNeuronWeights[] = $updatedWeight;
|
||||||
|
}
|
||||||
|
$updatedWeights[] = $updatedNeuronWeights;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$updatedWeights];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getDesiredOutputFromCorrectOutput(float $correctOutput): array
|
||||||
|
{
|
||||||
|
if ($this->isRegression) {
|
||||||
|
return [$correctOutput];
|
||||||
|
}
|
||||||
|
|
||||||
|
$desiredOutput = array_fill(0, count($this->labels), -1);
|
||||||
|
$labelIndex = Arr::first(array_keys($this->labels), fn ($key) => $this->labels[$key] == $correctOutput);
|
||||||
|
if ($labelIndex !== null) {
|
||||||
|
$desiredOutput[$labelIndex] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $desiredOutput;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSynapticWeights(): array
|
||||||
|
{
|
||||||
|
return [[$this->network->getSynapticWeights()]];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models\NetworksTraining;
|
||||||
|
|
||||||
|
use App\Models\ActivationsFunctions;
|
||||||
|
use App\Models\Perceptrons\GradientDescentPerceptron;
|
||||||
|
use App\Models\Perceptrons\NetworkPerceptron;
|
||||||
|
use App\Models\Perceptrons\Perceptron;
|
||||||
|
use App\Models\Perceptrons\SigmoidPerceptron;
|
||||||
|
use App\Services\DatasetReader\IDataSetReader;
|
||||||
|
use App\Services\IterationEventBuffer\IPerceptronIterationEventBuffer;
|
||||||
|
use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider;
|
||||||
|
use App\Services\SynapticWeightsProvider\SimpleNetworkWeightsProvider;
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
|
||||||
|
class MultiLayerPerceptronTraining extends NetworkTraining
|
||||||
|
{
|
||||||
|
private Perceptron $network;
|
||||||
|
|
||||||
|
private array $labels;
|
||||||
|
|
||||||
|
private bool $isRegression;
|
||||||
|
|
||||||
|
public ActivationsFunctions $activationFunction = ActivationsFunctions::SIGMOID;
|
||||||
|
public ?ActivationsFunctions $presentationLayerActivationFunction = ActivationsFunctions::STEP;
|
||||||
|
|
||||||
|
private float $epochError;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
IDataSetReader $datasetReader,
|
||||||
|
protected float $learningRate,
|
||||||
|
int $maxEpochs,
|
||||||
|
protected int $hiddenLayers,
|
||||||
|
protected int $hiddenLayersNeurons,
|
||||||
|
ISynapticWeightsProvider $synapticWeightsProvider,
|
||||||
|
IPerceptronIterationEventBuffer $iterationEventBuffer,
|
||||||
|
string $sessionId,
|
||||||
|
string $trainingId,
|
||||||
|
private float $minError,
|
||||||
|
?Closure $isCancelled = null,
|
||||||
|
) {
|
||||||
|
parent::__construct($datasetReader, $maxEpochs, $iterationEventBuffer, $sessionId, $trainingId, $isCancelled);
|
||||||
|
$this->labels = $datasetReader->getLabels();
|
||||||
|
$this->isRegression = $datasetReader->getOutputSize() === 1
|
||||||
|
|| ($datasetReader->getOutputSize() > 2
|
||||||
|
&& $datasetReader->getOutputSize() * 2 >= $datasetReader->getEpochExamplesCount());
|
||||||
|
if ($this->isRegression) {
|
||||||
|
$this->activationFunction = ActivationsFunctions::LINEAR;
|
||||||
|
}
|
||||||
|
|
||||||
|
$networkWeightsProvider = new SimpleNetworkWeightsProvider($synapticWeightsProvider);
|
||||||
|
$this->network = new NetworkPerceptron(
|
||||||
|
$networkWeightsProvider->generate(
|
||||||
|
$datasetReader->getInputSize(),
|
||||||
|
$this->isRegression ? 1 : $datasetReader->getOutputSize(),
|
||||||
|
$this->hiddenLayers,
|
||||||
|
$this->hiddenLayersNeurons,
|
||||||
|
),
|
||||||
|
$datasetReader->getInputSize(),
|
||||||
|
SigmoidPerceptron::class,
|
||||||
|
GradientDescentPerceptron::class,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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->network->getSynapticWeights();
|
||||||
|
$inputs_with_bias = array_merge([1], $inputs); // Add bias input
|
||||||
|
|
||||||
|
// Updates the weights
|
||||||
|
$this->network->setSynapticWeights(
|
||||||
|
$this->getUpdatedSynapticWeights($synaptic_weights, $iterationError, $inputs_with_bias)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Broadcast the training iteration event
|
||||||
|
$this->addIterationToBuffer(array_sum($iterationError), $this->network->getSynapticWeights());
|
||||||
|
|
||||||
|
// $this->iterationEventBuffer->flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
$iterationError = $this->iterationFunction($inputs, $correctOutput);
|
||||||
|
foreach ($iterationError as $error) {
|
||||||
|
$this->epochError += ($error ** 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) {
|
||||||
|
$this->broadcastTrainingEnded('Le perceptron à atteint l\'erreur minimale');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function iterationFunction(array $inputs, float $correctOutput): array
|
||||||
|
{
|
||||||
|
$outputs = $this->network->test($inputs);
|
||||||
|
$desiredOutput = $this->getDesiredOutputFromCorrectOutput($correctOutput);
|
||||||
|
|
||||||
|
$errors = [];
|
||||||
|
foreach ($outputs as $index => $output) {
|
||||||
|
$error = $desiredOutput[$index] - $output;
|
||||||
|
$errors[] = $error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backpropagation of error gradients to update synaptic weights.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private function getUpdatedSynapticWeights(array $synaptic_weights, array $iterationError, array $inputs): array
|
||||||
|
{
|
||||||
|
$layerInputs = [$inputs];
|
||||||
|
|
||||||
|
// Reproduce NetworkPerceptron::test() using each neuron's activation function.
|
||||||
|
foreach ($synaptic_weights as $layerIndex => $layerWeights) {
|
||||||
|
$previousLayerOutputs = $layerInputs[array_key_last($layerInputs)];
|
||||||
|
$layerOutputs = [];
|
||||||
|
|
||||||
|
foreach ($layerWeights as $neuronIndex => $neuronWeights) {
|
||||||
|
$weightedSum = array_sum(array_map(
|
||||||
|
fn ($input, $weight): float => $input * $weight,
|
||||||
|
$previousLayerOutputs,
|
||||||
|
$neuronWeights,
|
||||||
|
));
|
||||||
|
|
||||||
|
$neuron = $this->network->network[$layerIndex + 1][$neuronIndex];
|
||||||
|
$layerOutputs[] = $neuron->activationFunction($weightedSum);
|
||||||
|
}
|
||||||
|
|
||||||
|
$layerInputs[] = array_merge([1], $layerOutputs);
|
||||||
|
}
|
||||||
|
|
||||||
|
$deltas = array_fill(0, count($synaptic_weights), []);
|
||||||
|
$lastLayerIndex = count($synaptic_weights) - 1;
|
||||||
|
|
||||||
|
// Output delta includes the output neuron's activation derivative.
|
||||||
|
foreach ($iterationError as $neuronIndex => $error) {
|
||||||
|
$neuron = $this->network->network[$lastLayerIndex + 1][$neuronIndex];
|
||||||
|
$output = $layerInputs[$lastLayerIndex + 1][$neuronIndex + 1];
|
||||||
|
|
||||||
|
$deltas[$lastLayerIndex][$neuronIndex] =
|
||||||
|
$error * $this->activationDerivative($neuron, $output);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hidden-layer deltas use the original, unchanged weights.
|
||||||
|
for ($layerIndex = $lastLayerIndex - 1; $layerIndex >= 0; $layerIndex--) {
|
||||||
|
foreach ($synaptic_weights[$layerIndex] as $neuronIndex => $unusedNeuronWeights) {
|
||||||
|
$nextLayerDelta = 0.0;
|
||||||
|
|
||||||
|
foreach ($synaptic_weights[$layerIndex + 1] as $nextNeuronIndex => $nextNeuronWeights) {
|
||||||
|
// Index zero is the next layer's bias weight.
|
||||||
|
$nextLayerDelta +=
|
||||||
|
$nextNeuronWeights[$neuronIndex + 1]
|
||||||
|
* $deltas[$layerIndex + 1][$nextNeuronIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
$neuron = $this->network->network[$layerIndex + 1][$neuronIndex];
|
||||||
|
$output = $layerInputs[$layerIndex + 1][$neuronIndex + 1];
|
||||||
|
|
||||||
|
$deltas[$layerIndex][$neuronIndex] =
|
||||||
|
$nextLayerDelta * $this->activationDerivative($neuron, $output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$updatedWeights = [];
|
||||||
|
|
||||||
|
foreach ($synaptic_weights as $layerIndex => $layerWeights) {
|
||||||
|
$updatedLayerWeights = [];
|
||||||
|
|
||||||
|
foreach ($layerWeights as $neuronIndex => $neuronWeights) {
|
||||||
|
$updatedLayerWeights[] = array_map(
|
||||||
|
fn ($weight, $weightIndex): float => $weight
|
||||||
|
+ $this->learningRate
|
||||||
|
* $deltas[$layerIndex][$neuronIndex]
|
||||||
|
* $layerInputs[$layerIndex][$weightIndex],
|
||||||
|
$neuronWeights,
|
||||||
|
array_keys($neuronWeights),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$updatedWeights[] = $updatedLayerWeights;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $updatedWeights;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function activationDerivative(Perceptron $neuron, float $output): float
|
||||||
|
{
|
||||||
|
// Numerical derivative works for any activationFunction implementation.
|
||||||
|
// $epsilon = 1e-6;
|
||||||
|
|
||||||
|
// The activation function requires weighted input, which is unavailable here.
|
||||||
|
// For sigmoid, use its derivative directly.
|
||||||
|
if ($neuron instanceof SigmoidPerceptron) {
|
||||||
|
return $output * (1 - $output);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linear activation, used commonly by gradient-descent output neurons.
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getDesiredOutputFromCorrectOutput(float $correctOutput): array
|
||||||
|
{
|
||||||
|
if ($this->isRegression) {
|
||||||
|
return [$correctOutput];
|
||||||
|
}
|
||||||
|
|
||||||
|
$desiredOutput = array_fill(0, count($this->labels), 0);
|
||||||
|
$labelIndex = Arr::first(
|
||||||
|
array_keys($this->labels),
|
||||||
|
fn ($key) => $this->labels[$key] == $correctOutput
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($labelIndex !== null) {
|
||||||
|
$desiredOutput[$labelIndex] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $desiredOutput;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSynapticWeights(): array
|
||||||
|
{
|
||||||
|
return [[$this->network->getSynapticWeights()]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models\NetworksTraining;
|
||||||
|
|
||||||
|
use App\Events\PerceptronTrainingEnded;
|
||||||
|
use App\Exceptions\TrainingCancelledException;
|
||||||
|
use App\Models\ActivationsFunctions;
|
||||||
|
use App\Services\DatasetReader\IDataSetReader;
|
||||||
|
use App\Services\IterationEventBuffer\IPerceptronIterationEventBuffer;
|
||||||
|
use Closure;
|
||||||
|
|
||||||
|
abstract class NetworkTraining
|
||||||
|
{
|
||||||
|
protected int $epoch = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @abstract
|
||||||
|
*/
|
||||||
|
public ActivationsFunctions $activationFunction;
|
||||||
|
|
||||||
|
public ?ActivationsFunctions $presentationLayerActivationFunction = null;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
protected IDataSetReader $datasetReader,
|
||||||
|
protected int $maxEpochs,
|
||||||
|
protected IPerceptronIterationEventBuffer $iterationEventBuffer,
|
||||||
|
protected string $sessionId,
|
||||||
|
protected string $trainingId,
|
||||||
|
protected ?Closure $isCancelled = null,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
abstract public function start(): void;
|
||||||
|
|
||||||
|
abstract protected function stopCondition(): bool;
|
||||||
|
|
||||||
|
protected function checkPassedMaxIterations(?float $finalError)
|
||||||
|
{
|
||||||
|
if ($this->epoch >= $this->maxEpochs) {
|
||||||
|
$message = 'Le nombre maximal d\'époques a été atteint';
|
||||||
|
if ($finalError) {
|
||||||
|
$message .= " avec une erreur finale de $finalError";
|
||||||
|
}
|
||||||
|
|
||||||
|
event(new PerceptronTrainingEnded($message, $this->sessionId, $this->trainingId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function broadcastTrainingEnded(string $reason): void
|
||||||
|
{
|
||||||
|
$this->iterationEventBuffer->flush();
|
||||||
|
event(new PerceptronTrainingEnded($reason, $this->sessionId, $this->trainingId));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function addIterationToBuffer(float $error, array $synapticWeights)
|
||||||
|
{
|
||||||
|
if ($this->isCancelled !== null && ($this->isCancelled)()) {
|
||||||
|
throw new TrainingCancelledException;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->iterationEventBuffer->addIteration($this->epoch, $this->datasetReader->getLastReadLineIndex(), $error, $synapticWeights);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cancel(): void
|
||||||
|
{
|
||||||
|
$this->broadcastTrainingEnded('Entraînement annulé');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEpoch(): int
|
||||||
|
{
|
||||||
|
return $this->epoch;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract public function getSynapticWeights(): array;
|
||||||
|
}
|
||||||
@@ -1,15 +1,20 @@
|
|||||||
<?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;
|
||||||
|
use Closure;
|
||||||
|
|
||||||
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,15 +22,16 @@ 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,
|
||||||
IPerceptronIterationEventBuffer $iterationEventBuffer,
|
IPerceptronIterationEventBuffer $iterationEventBuffer,
|
||||||
string $sessionId,
|
string $sessionId,
|
||||||
string $trainingId,
|
string $trainingId,
|
||||||
|
?Closure $isCancelled = null,
|
||||||
) {
|
) {
|
||||||
parent::__construct($datasetReader, $maxEpochs, $iterationEventBuffer, $sessionId, $trainingId);
|
parent::__construct($datasetReader, $maxEpochs, $iterationEventBuffer, $sessionId, $trainingId, $isCancelled);
|
||||||
$this->perceptron = new SimpleBinaryPerceptron($synapticWeightsProvider->generate($datasetReader->getInputSize()));
|
$this->perceptron = new SimpleBinaryPerceptron($synapticWeightsProvider->generate($datasetReader->getInputSize()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,7 +54,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,12 +67,13 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function iterationFunction(array $inputs, int $correctOutput)
|
private function iterationFunction(array $inputs, int $correctOutput)
|
||||||
{
|
{
|
||||||
$output = $this->perceptron->test($inputs);
|
$output = $this->perceptron->test($inputs)[0];
|
||||||
|
|
||||||
$error = $correctOutput - $output;
|
$error = $correctOutput - $output;
|
||||||
if (abs($error) > $this::MIN_ERROR) {
|
if (abs($error) > $this::MIN_ERROR) {
|
||||||
@@ -76,9 +83,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models\Perceptrons;
|
||||||
|
|
||||||
|
class InputNeuron extends Perceptron
|
||||||
|
{
|
||||||
|
private float $input = 0.0;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
) {
|
||||||
|
parent::__construct([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setInput(float $input): void
|
||||||
|
{
|
||||||
|
$this->input = $input;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test(array $inputs): array
|
||||||
|
{
|
||||||
|
return [$this->input];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function activationFunction(float $input): float
|
||||||
|
{
|
||||||
|
return $input; // Identity function for input neurons
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models\Perceptrons;
|
||||||
|
|
||||||
|
class NetworkPerceptron extends Perceptron
|
||||||
|
{
|
||||||
|
public array $network = [];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private array $synaptic_weights,
|
||||||
|
private int $inputLayerNeuronsCount,
|
||||||
|
private string $hiddenLayerNeuronClass,
|
||||||
|
private string $outputLayerNeuronClass,
|
||||||
|
) {
|
||||||
|
parent::__construct($synaptic_weights);
|
||||||
|
$this->initializeNetwork($synaptic_weights);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function initializeNetwork(array $synaptic_weights): void
|
||||||
|
{
|
||||||
|
// Input Layer
|
||||||
|
$this->network[0] = [];
|
||||||
|
foreach (range(0, $this->inputLayerNeuronsCount - 1) as $i) {
|
||||||
|
$this->network[0][] = new InputNeuron();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hidden Layer
|
||||||
|
for ($layerIndex = 0; $layerIndex < count($synaptic_weights) - 1; $layerIndex++) {
|
||||||
|
$this->network[$layerIndex + 1] = [];
|
||||||
|
|
||||||
|
foreach ($synaptic_weights[$layerIndex] as $neuronWeights) {
|
||||||
|
$this->network[$layerIndex + 1][] = new $this->hiddenLayerNeuronClass($neuronWeights);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output Layer
|
||||||
|
$outputLayer = $synaptic_weights[count($synaptic_weights) - 1];
|
||||||
|
$this->network[count($synaptic_weights)] = [];
|
||||||
|
|
||||||
|
foreach ($outputLayer as $neuronWeights) {
|
||||||
|
$this->network[count($synaptic_weights)][] = new $this->outputLayerNeuronClass($neuronWeights);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test(array $inputs): array
|
||||||
|
{
|
||||||
|
// Set the inputs for the input layer
|
||||||
|
foreach ($this->network[0] as $index => $inputNeuron) {
|
||||||
|
$inputNeuron->setInput($inputs[$index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass through the hidden and output layers
|
||||||
|
$output = [];
|
||||||
|
for ($layerIndex = 0; $layerIndex < count($this->network); $layerIndex++) {
|
||||||
|
$lastLayerOutput = $output;
|
||||||
|
$output = [];
|
||||||
|
foreach ($this->network[$layerIndex] as $neuron) {
|
||||||
|
$output[] = $neuron->test($lastLayerOutput)[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function activationFunction(float $weighted_sum): float
|
||||||
|
{
|
||||||
|
return $weighted_sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSynapticWeights(array $synaptic_weights): void
|
||||||
|
{
|
||||||
|
parent::setSynapticWeights($synaptic_weights);
|
||||||
|
$this->network = [];
|
||||||
|
$this->initializeNetwork($synaptic_weights);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models\Perceptrons;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
// use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
abstract class Perceptron extends Model
|
abstract class Perceptron
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private array $synaptic_weights,
|
private array $synaptic_weights,
|
||||||
@@ -12,16 +12,17 @@ abstract class Perceptron extends Model
|
|||||||
$this->synaptic_weights = $synaptic_weights;
|
$this->synaptic_weights = $synaptic_weights;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test(array $inputs): float
|
public function test(array $inputs): array
|
||||||
{
|
{
|
||||||
$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)];
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract public function activationFunction(float $weighted_sum): float;
|
abstract public function activationFunction(float $weighted_sum): float;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models\Perceptrons;
|
||||||
|
|
||||||
|
class SigmoidPerceptron extends Perceptron
|
||||||
|
{
|
||||||
|
public function activationFunction(float $weighted_sum): float
|
||||||
|
{
|
||||||
|
$weighted_sum = max(-40, min(40, $weighted_sum));
|
||||||
|
|
||||||
|
return 1 / (1 + exp(-$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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models\Perceptrons;
|
||||||
|
|
||||||
|
class SimpleBinaryPerceptron2 extends Perceptron
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
array $synaptic_weights,
|
||||||
|
) {
|
||||||
|
parent::__construct($synaptic_weights);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function activationFunction(float $weighted_sum): float
|
||||||
|
{
|
||||||
|
// return $weighted_sum >= 0.0 ? 1.0 : -1.0;
|
||||||
|
return $weighted_sum;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,10 +3,12 @@
|
|||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
use Carbon\CarbonImmutable;
|
use Carbon\CarbonImmutable;
|
||||||
|
use Illuminate\Cache\RateLimiting\Limit;
|
||||||
use Illuminate\Support\Facades\Date;
|
use Illuminate\Support\Facades\Date;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\RateLimiter;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
use Illuminate\Validation\Rules\Password;
|
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
@@ -24,6 +26,13 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
$this->configureDefaults();
|
$this->configureDefaults();
|
||||||
|
|
||||||
|
RateLimiter::for('training', function (Request $request) {
|
||||||
|
return [
|
||||||
|
Limit::perMinute(50),
|
||||||
|
Limit::perMinute(15)->by($request->ip()),
|
||||||
|
];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Services;
|
|
||||||
|
|
||||||
class DataSetReader {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the dataset is for regression, we add a fake label of 0
|
|
||||||
if (count($newLine) === 2) {
|
|
||||||
$newLine[] = 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->lines[] = $newLine;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getRandomLine(): array | null
|
|
||||||
{
|
|
||||||
if (empty($this->currentLines)) {
|
|
||||||
return null; // No more lines to read
|
|
||||||
}
|
|
||||||
$randomNumber = array_rand($this->currentLines);
|
|
||||||
$randomLine = $this->currentLines[$randomNumber];
|
|
||||||
|
|
||||||
// Remove the line from the current lines to avoid repetition
|
|
||||||
unset($this->currentLines[$randomNumber]);
|
|
||||||
|
|
||||||
// Remember the index of the last read line in the full list
|
|
||||||
$this->lastReadLineIndex = array_search($randomLine, $this->lines, true);
|
|
||||||
|
|
||||||
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
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\DatasetReader;
|
||||||
|
|
||||||
|
interface IDataSetReader
|
||||||
|
{
|
||||||
|
public function getNextLine(): ?array;
|
||||||
|
|
||||||
|
public function getInputSize(): int;
|
||||||
|
|
||||||
|
public function getOutputSize(): int;
|
||||||
|
|
||||||
|
public function getLabels(): array;
|
||||||
|
|
||||||
|
public function reset(): void;
|
||||||
|
|
||||||
|
public function getLastReadLineIndex(): int;
|
||||||
|
|
||||||
|
public function getEpochExamplesCount(): int;
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\DatasetReader;
|
||||||
|
|
||||||
|
use App\Services\CsvReader;
|
||||||
|
|
||||||
|
class LinearOrderDataSetReader implements IDataSetReader
|
||||||
|
{
|
||||||
|
public array $lines = [];
|
||||||
|
|
||||||
|
private int $currentLineIndex = 0;
|
||||||
|
|
||||||
|
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->lines[$this->currentLineIndex])) {
|
||||||
|
return null; // No more lines to read
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->lastReadLineIndex = $this->currentLineIndex;
|
||||||
|
|
||||||
|
return $this->lines[$this->currentLineIndex++];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getInputSize(): int
|
||||||
|
{
|
||||||
|
return count($this->lines[0]) - 1; // Don't count the label
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOutputSize(): int
|
||||||
|
{
|
||||||
|
// Count the number of unique labels in the dataset
|
||||||
|
$labels = array_map(fn ($line) => end($line), $this->lines);
|
||||||
|
|
||||||
|
return count(array_unique($labels));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLabels(): array
|
||||||
|
{
|
||||||
|
$labels = array_map(fn ($line) => end($line), $this->lines);
|
||||||
|
|
||||||
|
return array_values(array_unique($labels));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reset(): void
|
||||||
|
{
|
||||||
|
$this->currentLineIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLastReadLineIndex(): int
|
||||||
|
{
|
||||||
|
return $this->lastReadLineIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEpochExamplesCount(): int
|
||||||
|
{
|
||||||
|
return count($this->lines);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\DatasetReader;
|
||||||
|
|
||||||
|
use App\Services\CsvReader;
|
||||||
|
|
||||||
|
class RandomOrderDataSetReader implements IDataSetReader
|
||||||
|
{
|
||||||
|
public array $lines = [];
|
||||||
|
|
||||||
|
private array $currentLineIndexes = [];
|
||||||
|
|
||||||
|
private int $currentLineIndex = 0;
|
||||||
|
|
||||||
|
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->currentLineIndexes[$this->currentLineIndex])) {
|
||||||
|
return null; // No more lines to read
|
||||||
|
}
|
||||||
|
$lineIndex = $this->currentLineIndexes[$this->currentLineIndex++];
|
||||||
|
|
||||||
|
$this->lastReadLineIndex = $lineIndex;
|
||||||
|
|
||||||
|
return $this->lines[$lineIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getInputSize(): int
|
||||||
|
{
|
||||||
|
return count($this->lines[0]) - 1; // Don't count the label
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOutputSize(): int
|
||||||
|
{
|
||||||
|
// Count the number of unique labels in the dataset
|
||||||
|
$labels = array_map(fn ($line) => end($line), $this->lines);
|
||||||
|
|
||||||
|
return count(array_unique($labels));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLabels(): array
|
||||||
|
{
|
||||||
|
$labels = array_map(fn ($line) => end($line), $this->lines);
|
||||||
|
|
||||||
|
return array_values(array_unique($labels));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reset(): void
|
||||||
|
{
|
||||||
|
$this->currentLineIndexes = array_keys($this->lines);
|
||||||
|
shuffle($this->currentLineIndexes);
|
||||||
|
$this->currentLineIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLastReadLineIndex(): int
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\IterationEventBuffer;
|
||||||
|
|
||||||
|
use App\Events\PerceptronTrainingIteration;
|
||||||
|
|
||||||
|
class PerceptronIterationEventBuffer implements IPerceptronIterationEventBuffer
|
||||||
|
{
|
||||||
|
private array $data = [];
|
||||||
|
|
||||||
|
private ?float $lastBroadcastAt = null;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private string $sessionId,
|
||||||
|
private string $trainingId,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function flush(): void
|
||||||
|
{
|
||||||
|
if ($this->data === []) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->waitForBroadcastInterval();
|
||||||
|
event(new PerceptronTrainingIteration($this->data, $this->sessionId, $this->trainingId));
|
||||||
|
$this->lastBroadcastAt = microtime(true);
|
||||||
|
$this->data = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addIteration(int $epoch, int $exampleIndex, float $error, array $synaptic_weights): void
|
||||||
|
{
|
||||||
|
$iteration = [
|
||||||
|
'epoch' => $epoch,
|
||||||
|
'exampleIndex' => $exampleIndex,
|
||||||
|
'error' => $error,
|
||||||
|
'weights' => $synaptic_weights,
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->data[] = $iteration;
|
||||||
|
|
||||||
|
if ($this->data !== [] && $this->payloadExceedsLimit()) {
|
||||||
|
$lastIteration = array_pop($this->data);
|
||||||
|
$this->flush();
|
||||||
|
$this->data[] = $lastIteration;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($this->data) >= config('perceptron.broadcast_iteration_size')) {
|
||||||
|
$this->flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function payloadExceedsLimit(): bool
|
||||||
|
{
|
||||||
|
return strlen(json_encode([
|
||||||
|
'iterations' => PerceptronTrainingIteration::normalizeForJson($this->data),
|
||||||
|
'trainingId' => $this->trainingId,
|
||||||
|
], JSON_THROW_ON_ERROR)) > config('broadcasting.broadcast_max_payload_size');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function waitForBroadcastInterval(): void
|
||||||
|
{
|
||||||
|
if ($this->lastBroadcastAt === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$minimumInterval = config('perceptron.broadcast_minimum_interval_ms') / 1000;
|
||||||
|
$remainingInterval = $minimumInterval - (microtime(true) - $this->lastBroadcastAt);
|
||||||
|
|
||||||
|
if ($remainingInterval > 0) {
|
||||||
|
usleep((int) ceil($remainingInterval * 1_000_000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\IterationEventBuffer;
|
||||||
|
|
||||||
|
use App\Events\PerceptronTrainingIteration;
|
||||||
|
|
||||||
|
class PerceptronLimitedEpochEventBuffer implements IPerceptronIterationEventBuffer
|
||||||
|
{
|
||||||
|
private array $data = [];
|
||||||
|
|
||||||
|
private array $lastEpochData = [];
|
||||||
|
|
||||||
|
private ?int $activeEpoch = null;
|
||||||
|
|
||||||
|
private bool $shouldBroadcastEpoch = false;
|
||||||
|
|
||||||
|
private ?float $lastBroadcastAt = null;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private string $sessionId,
|
||||||
|
private string $trainingId,
|
||||||
|
private int $epochInterval,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function flush(): void
|
||||||
|
{
|
||||||
|
if ($this->data !== []) {
|
||||||
|
$this->flushSelectedData();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->lastEpochData === [] || $this->shouldBroadcastEpoch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->waitForBroadcastInterval();
|
||||||
|
event(new PerceptronTrainingIteration($this->lastEpochData, $this->sessionId, $this->trainingId));
|
||||||
|
$this->lastBroadcastAt = microtime(true);
|
||||||
|
$this->lastEpochData = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addIteration(int $epoch, int $exampleIndex, float $error, array $synaptic_weights): void
|
||||||
|
{
|
||||||
|
$newData = [
|
||||||
|
'epoch' => $epoch,
|
||||||
|
'exampleIndex' => $exampleIndex,
|
||||||
|
'error' => $error,
|
||||||
|
'weights' => $synaptic_weights,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($this->activeEpoch !== $epoch) {
|
||||||
|
$this->flushSelectedData();
|
||||||
|
$this->lastEpochData = [];
|
||||||
|
$this->activeEpoch = $epoch;
|
||||||
|
$this->shouldBroadcastEpoch = $epoch === 1 || $epoch % $this->epochInterval === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $this->shouldBroadcastEpoch) {
|
||||||
|
$this->lastEpochData[] = $newData;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->data[] = $newData;
|
||||||
|
|
||||||
|
if ($this->payloadExceedsLimit() || count($this->data) >= config('perceptron.broadcast_iteration_size')) {
|
||||||
|
$this->flushSelectedData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function flushSelectedData(): void
|
||||||
|
{
|
||||||
|
if ($this->data === []) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->waitForBroadcastInterval();
|
||||||
|
event(new PerceptronTrainingIteration($this->data, $this->sessionId, $this->trainingId));
|
||||||
|
$this->lastBroadcastAt = microtime(true);
|
||||||
|
$this->data = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function payloadExceedsLimit(): bool
|
||||||
|
{
|
||||||
|
return strlen(json_encode([
|
||||||
|
'iterations' => PerceptronTrainingIteration::normalizeForJson($this->data),
|
||||||
|
'trainingId' => $this->trainingId,
|
||||||
|
], JSON_THROW_ON_ERROR)) > config('broadcasting.broadcast_max_payload_size');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function waitForBroadcastInterval(): void
|
||||||
|
{
|
||||||
|
if ($this->lastBroadcastAt === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$minimumInterval = config('perceptron.broadcast_minimum_interval_ms') / 1000;
|
||||||
|
$remainingInterval = $minimumInterval - (microtime(true) - $this->lastBroadcastAt);
|
||||||
|
|
||||||
|
if ($remainingInterval > 0) {
|
||||||
|
usleep((int) ceil($remainingInterval * 1_000_000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Services;
|
|
||||||
|
|
||||||
class PerceptronIterationEventBuffer implements IPerceptronIterationEventBuffer {
|
|
||||||
private $data;
|
|
||||||
private int $nextSizeIncreaseThreshold;
|
|
||||||
private int $underSizeIncreaseCount = 0;
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
private string $sessionId,
|
|
||||||
private string $trainingId,
|
|
||||||
private int $sizeIncreaseStart = 10,
|
|
||||||
private int $sizeIncreaseFactor = 2,
|
|
||||||
) {
|
|
||||||
$this->data = [];
|
|
||||||
$this->nextSizeIncreaseThreshold = $sizeIncreaseStart;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function flush(): void {
|
|
||||||
event(new \App\Events\PerceptronTrainingIteration($this->data, $this->sessionId, $this->trainingId));
|
|
||||||
$this->data = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function addIteration(int $epoch, int $exampleIndex, float $error, array $synaptic_weights): void {
|
|
||||||
$this->data[] = [
|
|
||||||
"epoch" => $epoch,
|
|
||||||
"exampleIndex" => $exampleIndex,
|
|
||||||
"error" => $error,
|
|
||||||
"weights" => $synaptic_weights,
|
|
||||||
];
|
|
||||||
|
|
||||||
if ($this->underSizeIncreaseCount <= $this->sizeIncreaseStart) { // We can still send a single date because we are under the increase start threshold
|
|
||||||
$this->underSizeIncreaseCount++;
|
|
||||||
$this->flush();
|
|
||||||
}
|
|
||||||
else if (count($this->data) >= $this->nextSizeIncreaseThreshold) {
|
|
||||||
$this->flush();
|
|
||||||
$this->nextSizeIncreaseThreshold *= $this->sizeIncreaseFactor;
|
|
||||||
|
|
||||||
if ($this->nextSizeIncreaseThreshold > config('perceptron.broadcast_iteration_size')) {
|
|
||||||
$this->nextSizeIncreaseThreshold = config('perceptron.broadcast_iteration_size'); // Cap the threshold to the maximum size
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Services;
|
|
||||||
|
|
||||||
class PerceptronLimitedEpochEventBuffer implements IPerceptronIterationEventBuffer {
|
|
||||||
private array $data;
|
|
||||||
private int $underSizeIncreaseCount = 0;
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
private string $sessionId,
|
|
||||||
private string $trainingId,
|
|
||||||
private int $epochInterval,
|
|
||||||
private int $sizeIncreaseStart = 10,
|
|
||||||
) {
|
|
||||||
$this->data = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function flush(): void {
|
|
||||||
event(new \App\Events\PerceptronTrainingIteration($this->data, $this->sessionId, $this->trainingId));
|
|
||||||
$this->data = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function addIteration(int $epoch, int $exampleIndex, float $error, array $synaptic_weights): void {
|
|
||||||
$newData = [
|
|
||||||
"epoch" => $epoch,
|
|
||||||
"exampleIndex" => $exampleIndex,
|
|
||||||
"error" => $error,
|
|
||||||
"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;
|
|
||||||
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
|
|
||||||
$this->flush(); // Flush all data from the previous epoch
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$this->data = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$lastEpoch = $epoch;
|
|
||||||
}
|
|
||||||
$this->data[] = $newData;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\SynapticWeightsProvider;
|
||||||
|
|
||||||
|
interface INetworkSynapticWeightsProvider
|
||||||
|
{
|
||||||
|
public function generate(int $input_size, int $output_size, int $hidden_layers_count, int $hidden_layers_neurons_count): array;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\SynapticWeightsProvider;
|
||||||
|
|
||||||
|
use App\Services\SynapticWeightsProvider\INetworkSynapticWeightsProvider;
|
||||||
|
|
||||||
|
class SimpleNetworkWeightsProvider implements INetworkSynapticWeightsProvider
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private ISynapticWeightsProvider $synapticWeightsProvider,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generate(int $input_size, int $output_size, int $hidden_layers_count, int $hidden_layers_neurons_count): array
|
||||||
|
{
|
||||||
|
$synaptic_weights = [];
|
||||||
|
$lastLayerSize = $input_size;
|
||||||
|
|
||||||
|
// Generate Hidden Layer weights
|
||||||
|
for ($hiddenLayerNeuronIndex = 0; $hiddenLayerNeuronIndex < $hidden_layers_count; $hiddenLayerNeuronIndex++) {
|
||||||
|
$layer = [];
|
||||||
|
for ($neuronIndex = 0; $neuronIndex < $hidden_layers_neurons_count; $neuronIndex++) {
|
||||||
|
$layer[] = $this->synapticWeightsProvider->generate($lastLayerSize);
|
||||||
|
}
|
||||||
|
$synaptic_weights[] = $layer;
|
||||||
|
$lastLayerSize = $hidden_layers_neurons_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate Output Layer weights
|
||||||
|
$synaptic_weights[] = [];
|
||||||
|
for ($outputNeuronIndex = 0; $outputNeuronIndex < $output_size; $outputNeuronIndex++) {
|
||||||
|
$synaptic_weights[count($synaptic_weights) -1][] = $this->synapticWeightsProvider->generate($lastLayerSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $synaptic_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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -23,6 +23,9 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
HandleInertiaRequests::class,
|
HandleInertiaRequests::class,
|
||||||
AddLinkHeadersForPreloadedAssets::class,
|
AddLinkHeadersForPreloadedAssets::class,
|
||||||
]);
|
]);
|
||||||
|
$middleware->trustProxies(at: [
|
||||||
|
'*'
|
||||||
|
]);
|
||||||
})
|
})
|
||||||
->withExceptions(function (Exceptions $exceptions): void {
|
->withExceptions(function (Exceptions $exceptions): void {
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -36,10 +36,10 @@ return [
|
|||||||
'secret' => env('REVERB_APP_SECRET'),
|
'secret' => env('REVERB_APP_SECRET'),
|
||||||
'app_id' => env('REVERB_APP_ID'),
|
'app_id' => env('REVERB_APP_ID'),
|
||||||
'options' => [
|
'options' => [
|
||||||
'host' => env('REVERB_HOST'),
|
'host' => env('REVERB_BROADCAST_HOST', env('REVERB_HOST')),
|
||||||
'port' => env('REVERB_PORT', 443),
|
'port' => env('REVERB_BROADCAST_PORT', env('REVERB_PORT', 443)),
|
||||||
'scheme' => env('REVERB_SCHEME', 'https'),
|
'scheme' => env('REVERB_BROADCAST_SCHEME', env('REVERB_SCHEME', 'https')),
|
||||||
'useTLS' => env('REVERB_SCHEME', 'https') === 'https',
|
'useTLS' => env('REVERB_BROADCAST_SCHEME', env('REVERB_SCHEME', 'https')) === 'https',
|
||||||
],
|
],
|
||||||
'client_options' => [
|
'client_options' => [
|
||||||
// Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
|
// Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
|
||||||
@@ -79,4 +79,9 @@ return [
|
|||||||
|
|
||||||
],
|
],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep broadcast payloads below Pusher's event size limit.
|
||||||
|
*/
|
||||||
|
'broadcast_max_payload_size' => 9000,
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -7,13 +7,29 @@ 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 limited broadcast mode
|
||||||
*/
|
*/
|
||||||
'limited_broadcast_number' => 200,
|
'limited_broadcast_number' => 100,
|
||||||
|
|
||||||
'broadcast_iteration_size' => 75,
|
/**
|
||||||
|
* The maximum number of iterations that can be sent in a single broadcast.
|
||||||
|
*/
|
||||||
|
'broadcast_iteration_size' => 75,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimum time between training progress broadcasts, in milliseconds.
|
||||||
|
*/
|
||||||
|
'broadcast_minimum_interval_ms' => 100,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum number of weights for which all iteration weights are broadcast
|
||||||
|
* and displayed in the iteration table.
|
||||||
|
*/
|
||||||
|
'max_displayed_weights' => 5,
|
||||||
|
|
||||||
|
'max_iterations' => 5000,
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Migrate the database
|
||||||
|
php ./artisan migrate --force || exit 1
|
||||||
|
|
||||||
|
# Cache
|
||||||
|
php ./artisan optimize:clear && php ./artisan optimize
|
||||||
|
|
||||||
|
# Start the server
|
||||||
|
exec frankenphp php-server --root=public
|
||||||
@@ -6,15 +6,21 @@
|
|||||||
"": {
|
"": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@inertiajs/vue3": "^2.3.7",
|
"@inertiajs/vue3": "^2.3.7",
|
||||||
|
"@jaseeey/vue-umami-plugin": "^1.6.1",
|
||||||
|
"@lucide/vue": "^1.46.0",
|
||||||
|
"@tailwindcss/typography": "^0.5.20",
|
||||||
"@vee-validate/zod": "^4.15.1",
|
"@vee-validate/zod": "^4.15.1",
|
||||||
"@vueuse/core": "^12.8.2",
|
"@vueuse/core": "^12.8.2",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"katex": "^0.18.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",
|
||||||
|
"markdown-it": "^15.0.1",
|
||||||
|
"markdown-it-katex": "^2.0.3",
|
||||||
"radix-ui": "^1.4.3",
|
"radix-ui": "^1.4.3",
|
||||||
"reka-ui": "^2.9.0",
|
"reka-ui": "^2.10.4",
|
||||||
"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",
|
||||||
@@ -856,6 +862,11 @@
|
|||||||
"@swc/helpers": "^0.5.0"
|
"@swc/helpers": "^0.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@jaseeey/vue-umami-plugin": {
|
||||||
|
"version": "1.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jaseeey/vue-umami-plugin/-/vue-umami-plugin-1.6.1.tgz",
|
||||||
|
"integrity": "sha512-a38bB7cYbwMP5HmlrT2Dk2RHdGFxVvLRci2GhY8cIwCpM419qXtvSM6SKmIJ3U3u7zYdcQLVAySOcXY150Z14Q=="
|
||||||
|
},
|
||||||
"node_modules/@jridgewell/gen-mapping": {
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
"version": "0.3.13",
|
"version": "0.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||||
@@ -925,6 +936,15 @@
|
|||||||
"integrity": "sha512-yZYIr1iwuCQ7LFI+GsJk9vacw1HWMp3ZlDlW0pdfz3zXyKeu4US7oH79KmQQ031L0cYaSyaUMo/Ha1D4BosKqw==",
|
"integrity": "sha512-yZYIr1iwuCQ7LFI+GsJk9vacw1HWMp3ZlDlW0pdfz3zXyKeu4US7oH79KmQQ031L0cYaSyaUMo/Ha1D4BosKqw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@lucide/vue": {
|
||||||
|
"version": "1.46.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lucide/vue/-/vue-1.46.0.tgz",
|
||||||
|
"integrity": "sha512-xp4mUamXKM/jYCve8TOcEcuikidFh3Jn9gs0THCeEKn/fAdeU0mKhPqk0fmIwn42fI4CwGLRWrLneLA69h8Nqw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": ">=3.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@napi-rs/wasm-runtime": {
|
"node_modules/@napi-rs/wasm-runtime": {
|
||||||
"version": "0.2.12",
|
"version": "0.2.12",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
|
||||||
@@ -2978,6 +2998,29 @@
|
|||||||
"node": ">= 20"
|
"node": ">= 20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tailwindcss/typography": {
|
||||||
|
"version": "0.5.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.20.tgz",
|
||||||
|
"integrity": "sha512-hwbzQuNUfcPvbegQFatVPl/MY/tcM9KLl963hQ5laJKPh81TEZ1+dNG9PirGvcaDBkp+BCshExAyKVPW91dozw==",
|
||||||
|
"dependencies": {
|
||||||
|
"postcss-selector-parser": "6.0.10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"tailwindcss": ">=3.0.0 || >=4.0.0 || insiders"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": {
|
||||||
|
"version": "6.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
|
||||||
|
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
|
||||||
|
"dependencies": {
|
||||||
|
"cssesc": "^3.0.0",
|
||||||
|
"util-deprecate": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tailwindcss/vite": {
|
"node_modules/@tailwindcss/vite": {
|
||||||
"version": "4.2.1",
|
"version": "4.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.1.tgz",
|
||||||
@@ -3830,6 +3873,7 @@
|
|||||||
"version": "12.8.2",
|
"version": "12.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.8.2.tgz",
|
||||||
"integrity": "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==",
|
"integrity": "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/web-bluetooth": "^0.0.21",
|
"@types/web-bluetooth": "^0.0.21",
|
||||||
"@vueuse/metadata": "12.8.2",
|
"@vueuse/metadata": "12.8.2",
|
||||||
@@ -4300,6 +4344,14 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/commander": {
|
||||||
|
"version": "15.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz",
|
||||||
|
"integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
@@ -4362,7 +4414,6 @@
|
|||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||||
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
|
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
|
||||||
"dev": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"cssesc": "bin/cssesc"
|
"cssesc": "bin/cssesc"
|
||||||
},
|
},
|
||||||
@@ -4490,9 +4541,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/defu": {
|
"node_modules/defu": {
|
||||||
"version": "6.1.4",
|
"version": "6.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz",
|
||||||
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="
|
"integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/delayed-stream": {
|
"node_modules/delayed-stream": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@@ -6254,6 +6306,21 @@
|
|||||||
"json5": "lib/cli.js"
|
"json5": "lib/cli.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/katex": {
|
||||||
|
"version": "0.18.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/katex/-/katex-0.18.7.tgz",
|
||||||
|
"integrity": "sha512-h+UCwkZ+4Jz8WQ7MLGfj7UVFrRCizGb912fwF4luGdYsC5paYG1vx+jy+KRcC/XkpjGva/P7nAWuxNnPzRvzHw==",
|
||||||
|
"funding": [
|
||||||
|
"https://opencollective.com/katex",
|
||||||
|
"https://github.com/sponsors/katex"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "^15.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"katex": "cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/keyv": {
|
"node_modules/keyv": {
|
||||||
"version": "4.5.4",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||||
@@ -6554,6 +6621,24 @@
|
|||||||
"url": "https://opencollective.com/parcel"
|
"url": "https://opencollective.com/parcel"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/linkify-it": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-wJ/TwpSDTLepCrQoYWYIExIKg5Zchex2Nn5yk2mFnB+6PtdkHtyLx742md9csRjjOnGkKIS/RrbY7l8D6gT9Vw==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/puzrin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/markdown-it"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"uc.micro": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/locate-path": {
|
"node_modules/locate-path": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||||
@@ -6602,6 +6687,82 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/markdown-it": {
|
||||||
|
"version": "15.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-15.0.1.tgz",
|
||||||
|
"integrity": "sha512-9/7gE95FNPkfUWrjJIoHZza2iLmuJlPD0UNMxPi7bxUrbCR525YZY0r+zyfes0dZI5ZZ/uNIXUJca0pJvtw41g==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/puzrin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/markdown-it"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"argparse": "^3.0.0",
|
||||||
|
"entities": "^8.0.0",
|
||||||
|
"linkify-it": "^6.0.0",
|
||||||
|
"mdurl": "^2.1.0",
|
||||||
|
"punycode.js": "^2.3.1",
|
||||||
|
"uc.micro": "^3.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"markdown-it": "bin/markdown-it.mjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/markdown-it-katex": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/markdown-it-katex/-/markdown-it-katex-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-nUkkMtRWeg7OpdflamflE/Ho/pWl64Lk9wNBKOmaj33XkQdumhXAIYhI0WO03GeiycPCsxbmX536V5NEXpC3Ng==",
|
||||||
|
"dependencies": {
|
||||||
|
"katex": "^0.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/markdown-it-katex/node_modules/katex": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/katex/-/katex-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-rS4mY3SvHYg5LtQV6RBcK0if7ur6plyEukAOV+jGGPqFImuzu8fHL6M752iBmRGoUyF0bhZbAPoezehn7xYksA==",
|
||||||
|
"dependencies": {
|
||||||
|
"match-at": "^0.1.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"katex": "cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/markdown-it/node_modules/argparse": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-nM4mHF/KM1v59ZNKX7zfusQz5wUAxR511YG8Vo6TyiV4aqhu++rbJW4v04xsWhpSsHFj66flT8P7znVpyO20xQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/puzrin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/nodeca"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/markdown-it/node_modules/entities": {
|
||||||
|
"version": "8.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz",
|
||||||
|
"integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/match-at": {
|
||||||
|
"version": "0.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/match-at/-/match-at-0.1.1.tgz",
|
||||||
|
"integrity": "sha512-h4Yd392z9mST+dzc+yjuybOGFNOZjmXIPKWjxBd1Bb23r4SmDOsk2NYCU2BMUBGbSpZqwVsZYNq26QS3xfaT3Q=="
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
@@ -6610,6 +6771,11 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mdurl": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg=="
|
||||||
|
},
|
||||||
"node_modules/merge2": {
|
"node_modules/merge2": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||||
@@ -7137,6 +7303,14 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/punycode.js": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pusher-js": {
|
"node_modules/pusher-js": {
|
||||||
"version": "8.4.0",
|
"version": "8.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-8.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-8.4.0.tgz",
|
||||||
@@ -7386,9 +7560,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/reka-ui": {
|
"node_modules/reka-ui": {
|
||||||
"version": "2.9.0",
|
"version": "2.10.4",
|
||||||
"resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.10.4.tgz",
|
||||||
"integrity": "sha512-5dpp80u109iLTbRBu+jhAk8R/877/JN20gYGjb3GsuAgS7E/5QTX5ZxuzWtZAVbChBDYDpXc8pkaQAFpa6s+4w==",
|
"integrity": "sha512-kbS5GAbkHkYj0EVKAg5ZPEPndhBjeUFSa1Aq5m5ftQwyHnB1v5tw8X6fcwfKH/ry3StPXblMWM1DW82MVS71cw==",
|
||||||
|
"license": "MIT",
|
||||||
"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",
|
||||||
@@ -7398,7 +7573,7 @@
|
|||||||
"@vueuse/core": "^14.1.0",
|
"@vueuse/core": "^14.1.0",
|
||||||
"@vueuse/shared": "^14.1.0",
|
"@vueuse/shared": "^14.1.0",
|
||||||
"aria-hidden": "^1.2.4",
|
"aria-hidden": "^1.2.4",
|
||||||
"defu": "^6.1.4",
|
"defu": "^6.1.5",
|
||||||
"ohash": "^2.0.11"
|
"ohash": "^2.0.11"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
@@ -8298,6 +8473,11 @@
|
|||||||
"typescript": ">=4.8.4 <6.0.0"
|
"typescript": ">=4.8.4 <6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uc.micro": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-U3PppEkleoTnIfi8BozMx3yju3qc/L6SwqWo2Sw+54PX+PX0q9I+r1Um5HCmqD7n9VDX5/v3vQH/AjA6deDdtw=="
|
||||||
|
},
|
||||||
"node_modules/unbox-primitive": {
|
"node_modules/unbox-primitive": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
|
||||||
@@ -8417,8 +8597,7 @@
|
|||||||
"node_modules/util-deprecate": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/vee-validate": {
|
"node_modules/vee-validate": {
|
||||||
"version": "4.15.1",
|
"version": "4.15.1",
|
||||||
|
|||||||
@@ -37,15 +37,21 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@inertiajs/vue3": "^2.3.7",
|
"@inertiajs/vue3": "^2.3.7",
|
||||||
|
"@jaseeey/vue-umami-plugin": "^1.6.1",
|
||||||
|
"@lucide/vue": "^1.46.0",
|
||||||
|
"@tailwindcss/typography": "^0.5.20",
|
||||||
"@vee-validate/zod": "^4.15.1",
|
"@vee-validate/zod": "^4.15.1",
|
||||||
"@vueuse/core": "^12.8.2",
|
"@vueuse/core": "^12.8.2",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"katex": "^0.18.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",
|
||||||
|
"markdown-it": "^15.0.1",
|
||||||
|
"markdown-it-katex": "^2.0.3",
|
||||||
"radix-ui": "^1.4.3",
|
"radix-ui": "^1.4.3",
|
||||||
"reka-ui": "^2.9.0",
|
"reka-ui": "^2.10.4",
|
||||||
"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>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 46 KiB |
@@ -0,0 +1,150 @@
|
|||||||
|
2.8,1.9,1
|
||||||
|
2.9,1.8,1
|
||||||
|
2.2,1.5,1
|
||||||
|
1.3,5.6,0
|
||||||
|
-1.2,1.6,-1
|
||||||
|
2.5,1.7,1
|
||||||
|
3.1,2.3,1
|
||||||
|
2.8,2.4,1
|
||||||
|
-1.2,1.5,-1
|
||||||
|
1.4,6.1,0
|
||||||
|
-1.2,1.3,-1
|
||||||
|
3.1,1.8,1
|
||||||
|
3.3,2.5,1
|
||||||
|
3.4,2.3,1
|
||||||
|
1.5,5.9,0
|
||||||
|
1.3,5.6,0
|
||||||
|
-1.2,1,-1
|
||||||
|
3,2.1,1
|
||||||
|
1.4,5.2,0
|
||||||
|
1.2,5.5,0
|
||||||
|
1.4,6.7,0
|
||||||
|
-1.3,1.7,-1
|
||||||
|
1,5.7,0
|
||||||
|
2.6,1.4,1
|
||||||
|
1.6,6,0
|
||||||
|
1.2,5.8,0
|
||||||
|
1.4,6.1,0
|
||||||
|
1.3,5.7,0
|
||||||
|
-1.2,1.4,-1
|
||||||
|
1,5.5,0
|
||||||
|
3.1,2.1,1
|
||||||
|
-1.4,1.7,-1
|
||||||
|
1.6,6.3,0
|
||||||
|
-1.2,1.4,-1
|
||||||
|
2.7,1.9,1
|
||||||
|
1.1,5.5,0
|
||||||
|
3,1.8,1
|
||||||
|
3.8,2,1
|
||||||
|
2.8,2.1,1
|
||||||
|
1,5,0
|
||||||
|
2.8,2,1
|
||||||
|
-1.2,1.6,-1
|
||||||
|
1.3,6.6,0
|
||||||
|
1.6,6,0
|
||||||
|
3,1.6,1
|
||||||
|
-1.1,1.1,-1
|
||||||
|
2.9,1.8,1
|
||||||
|
3,1.8,1
|
||||||
|
-1.2,1.4,-1
|
||||||
|
1,5.8,0
|
||||||
|
1.2,5.7,0
|
||||||
|
2.8,1.5,1
|
||||||
|
-1.4,1.6,-1
|
||||||
|
-1.2,1.4,-1
|
||||||
|
2.8,2.2,1
|
||||||
|
-1.5,1.7,-1
|
||||||
|
1,4.9,0
|
||||||
|
1.3,5.7,0
|
||||||
|
3.2,2.3,1
|
||||||
|
-1.3,1.5,-1
|
||||||
|
-1.2,1.2,-1
|
||||||
|
-1.2,1.6,-1
|
||||||
|
3,2.1,1
|
||||||
|
-1.4,1.5,-1
|
||||||
|
1.3,5.5,0
|
||||||
|
3.3,2.5,1
|
||||||
|
3.1,2.4,1
|
||||||
|
1.8,5.9,0
|
||||||
|
2.7,1.9,1
|
||||||
|
1.5,6.3,0
|
||||||
|
-1.3,1.4,-1
|
||||||
|
1,5,0
|
||||||
|
3.2,2.3,1
|
||||||
|
-1.3,1.4,-1
|
||||||
|
3,2.1,1
|
||||||
|
3.2,2,1
|
||||||
|
1.3,6.3,0
|
||||||
|
1.4,7,0
|
||||||
|
-1.2,1.7,-1
|
||||||
|
1.4,6.6,0
|
||||||
|
1.2,5.8,0
|
||||||
|
3,2,1
|
||||||
|
-1.2,1.6,-1
|
||||||
|
1.5,6.2,0
|
||||||
|
1.5,6.4,0
|
||||||
|
-1.2,1.9,-1
|
||||||
|
3.2,2.3,1
|
||||||
|
-1.2,1.5,-1
|
||||||
|
1.5,5.4,0
|
||||||
|
1.5,6,0
|
||||||
|
1.5,6.9,0
|
||||||
|
-1.4,1.5,-1
|
||||||
|
1.3,6.4,0
|
||||||
|
2.8,2,1
|
||||||
|
-1.2,1.5,-1
|
||||||
|
-1.1,1.5,-1
|
||||||
|
3.6,2.5,1
|
||||||
|
1.1,5.1,0
|
||||||
|
-1.1,1.5,-1
|
||||||
|
-1.2,1.5,-1
|
||||||
|
3.4,2.4,1
|
||||||
|
-1.3,1.3,-1
|
||||||
|
3,2.3,1
|
||||||
|
-1.1,1.5,-1
|
||||||
|
1.1,5.6,0
|
||||||
|
1.3,5.6,0
|
||||||
|
-1.2,1.5,-1
|
||||||
|
-1.2,1.3,-1
|
||||||
|
1.7,6.7,0
|
||||||
|
-1.2,1.3,-1
|
||||||
|
-1.4,1.3,-1
|
||||||
|
2.5,1.8,1
|
||||||
|
-1.4,1.5,-1
|
||||||
|
1.5,6.5,0
|
||||||
|
2.6,2.3,1
|
||||||
|
-1.2,1.4,-1
|
||||||
|
1.5,5.6,0
|
||||||
|
1.3,5.7,0
|
||||||
|
1.2,6.1,0
|
||||||
|
3,2.2,1
|
||||||
|
3,1.8,1
|
||||||
|
-1.1,1.4,-1
|
||||||
|
1.4,6.8,0
|
||||||
|
-1.6,1.6,-1
|
||||||
|
-1.2,1.4,-1
|
||||||
|
3.2,1.8,1
|
||||||
|
-1.2,1.5,-1
|
||||||
|
2.7,1.8,1
|
||||||
|
-1.3,1.4,-1
|
||||||
|
1.3,6.2,0
|
||||||
|
-1.3,1.3,-1
|
||||||
|
3,1.8,1
|
||||||
|
2.7,1.9,1
|
||||||
|
-1.2,1.4,-1
|
||||||
|
-1.2,1.2,-1
|
||||||
|
1.5,6.7,0
|
||||||
|
2.5,1.9,1
|
||||||
|
3.3,2.1,1
|
||||||
|
2.8,1.8,1
|
||||||
|
-1.2,1.3,-1
|
||||||
|
3.8,2.2,1
|
||||||
|
2.5,2,1
|
||||||
|
1.3,6.1,0
|
||||||
|
-1.2,1.4,-1
|
||||||
|
-1.1,1.5,-1
|
||||||
|
3,2.3,1
|
||||||
|
-1.4,1.9,-1
|
||||||
|
-1.2,1.6,-1
|
||||||
|
1,6,0
|
||||||
|
1.3,5.5,0
|
||||||
|
@@ -0,0 +1,4 @@
|
|||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
|
||||||
|
0,0,1,0,0,0,0,1,0,0,1,1,1,1,1,0,0,1,0,0,0,0,1,0,0,1
|
||||||
|
1,0,0,0,1,0,1,0,1,0,0,0,1,0,0,0,1,0,1,0,1,0,0,0,1,2
|
||||||
|
0,0,0,0,0,0,1,1,1,0,0,1,0,1,0,0,1,1,1,0,0,0,0,0,0,3
|
||||||
|
@@ -1,4 +0,0 @@
|
|||||||
0, 0, 0
|
|
||||||
0, 1, 0
|
|
||||||
1, 0, 0
|
|
||||||
1, 1, 1
|
|
||||||
|
@@ -1,4 +0,0 @@
|
|||||||
0, 0, -1
|
|
||||||
0, 1, 1
|
|
||||||
1, 0, 1
|
|
||||||
1, 1, 1
|
|
||||||
|
@@ -1,150 +0,0 @@
|
|||||||
2.8,1.9,-1,-1,1
|
|
||||||
2.9,1.8,-1,-1,1
|
|
||||||
2.2,1.5,-1,-1,1
|
|
||||||
1.3,5.6,-1,1,-1
|
|
||||||
-1.2,1.6,1,-1,-1
|
|
||||||
2.5,1.7,-1,-1,1
|
|
||||||
3.1,2.3,-1,-1,1
|
|
||||||
2.8,2.4,-1,-1,1
|
|
||||||
-1.2,1.5,1,-1,-1
|
|
||||||
1.4,6.1,-1,1,-1
|
|
||||||
-1.2,1.3,1,-1,-1
|
|
||||||
3.1,1.8,-1,-1,1
|
|
||||||
3.3,2.5,-1,-1,1
|
|
||||||
3.4,2.3,-1,-1,1
|
|
||||||
1.5,5.9,-1,1,-1
|
|
||||||
1.3,5.6,-1,1,-1
|
|
||||||
-1.2,1,1,-1,-1
|
|
||||||
3,2.1,-1,-1,1
|
|
||||||
1.4,5.2,-1,1,-1
|
|
||||||
1.2,5.5,-1,1,-1
|
|
||||||
1.4,6.7,-1,1,-1
|
|
||||||
-1.3,1.7,1,-1,-1
|
|
||||||
1,5.7,-1,1,-1
|
|
||||||
2.6,1.4,-1,-1,1
|
|
||||||
1.6,6,-1,1,-1
|
|
||||||
1.2,5.8,-1,1,-1
|
|
||||||
1.4,6.1,-1,1,-1
|
|
||||||
1.3,5.7,-1,1,-1
|
|
||||||
-1.2,1.4,1,-1,-1
|
|
||||||
1,5.5,-1,1,-1
|
|
||||||
3.1,2.1,-1,-1,1
|
|
||||||
-1.4,1.7,1,-1,-1
|
|
||||||
1.6,6.3,-1,1,-1
|
|
||||||
-1.2,1.4,1,-1,-1
|
|
||||||
2.7,1.9,-1,-1,1
|
|
||||||
1.1,5.5,-1,1,-1
|
|
||||||
3,1.8,-1,-1,1
|
|
||||||
3.8,2,-1,-1,1
|
|
||||||
2.8,2.1,-1,-1,1
|
|
||||||
1,5,-1,1,-1
|
|
||||||
2.8,2,-1,-1,1
|
|
||||||
-1.2,1.6,1,-1,-1
|
|
||||||
1.3,6.6,-1,1,-1
|
|
||||||
1.6,6,-1,1,-1
|
|
||||||
3,1.6,-1,-1,1
|
|
||||||
-1.1,1.1,1,-1,-1
|
|
||||||
2.9,1.8,-1,-1,1
|
|
||||||
3,1.8,-1,-1,1
|
|
||||||
-1.2,1.4,1,-1,-1
|
|
||||||
1,5.8,-1,1,-1
|
|
||||||
1.2,5.7,-1,1,-1
|
|
||||||
2.8,1.5,-1,-1,1
|
|
||||||
-1.4,1.6,1,-1,-1
|
|
||||||
-1.2,1.4,1,-1,-1
|
|
||||||
2.8,2.2,-1,-1,1
|
|
||||||
-1.5,1.7,1,-1,-1
|
|
||||||
1,4.9,-1,1,-1
|
|
||||||
1.3,5.7,-1,1,-1
|
|
||||||
3.2,2.3,-1,-1,1
|
|
||||||
-1.3,1.5,1,-1,-1
|
|
||||||
-1.2,1.2,1,-1,-1
|
|
||||||
-1.2,1.6,1,-1,-1
|
|
||||||
3,2.1,-1,-1,1
|
|
||||||
-1.4,1.5,1,-1,-1
|
|
||||||
1.3,5.5,-1,1,-1
|
|
||||||
3.3,2.5,-1,-1,1
|
|
||||||
3.1,2.4,-1,-1,1
|
|
||||||
1.8,5.9,-1,1,-1
|
|
||||||
2.7,1.9,-1,-1,1
|
|
||||||
1.5,6.3,-1,1,-1
|
|
||||||
-1.3,1.4,1,-1,-1
|
|
||||||
1,5,-1,1,-1
|
|
||||||
3.2,2.3,-1,-1,1
|
|
||||||
-1.3,1.4,1,-1,-1
|
|
||||||
3,2.1,-1,-1,1
|
|
||||||
3.2,2,-1,-1,1
|
|
||||||
1.3,6.3,-1,1,-1
|
|
||||||
1.4,7,-1,1,-1
|
|
||||||
-1.2,1.7,1,-1,-1
|
|
||||||
1.4,6.6,-1,1,-1
|
|
||||||
1.2,5.8,-1,1,-1
|
|
||||||
3,2,-1,-1,1
|
|
||||||
-1.2,1.6,1,-1,-1
|
|
||||||
1.5,6.2,-1,1,-1
|
|
||||||
1.5,6.4,-1,1,-1
|
|
||||||
-1.2,1.9,1,-1,-1
|
|
||||||
3.2,2.3,-1,-1,1
|
|
||||||
-1.2,1.5,1,-1,-1
|
|
||||||
1.5,5.4,-1,1,-1
|
|
||||||
1.5,6,-1,1,-1
|
|
||||||
1.5,6.9,-1,1,-1
|
|
||||||
-1.4,1.5,1,-1,-1
|
|
||||||
1.3,6.4,-1,1,-1
|
|
||||||
2.8,2,-1,-1,1
|
|
||||||
-1.2,1.5,1,-1,-1
|
|
||||||
-1.1,1.5,1,-1,-1
|
|
||||||
3.6,2.5,-1,-1,1
|
|
||||||
1.1,5.1,-1,1,-1
|
|
||||||
-1.1,1.5,1,-1,-1
|
|
||||||
-1.2,1.5,1,-1,-1
|
|
||||||
3.4,2.4,-1,-1,1
|
|
||||||
-1.3,1.3,1,-1,-1
|
|
||||||
3,2.3,-1,-1,1
|
|
||||||
-1.1,1.5,1,-1,-1
|
|
||||||
1.1,5.6,-1,1,-1
|
|
||||||
1.3,5.6,-1,1,-1
|
|
||||||
-1.2,1.5,1,-1,-1
|
|
||||||
-1.2,1.3,1,-1,-1
|
|
||||||
1.7,6.7,-1,1,-1
|
|
||||||
-1.2,1.3,1,-1,-1
|
|
||||||
-1.4,1.3,1,-1,-1
|
|
||||||
2.5,1.8,-1,-1,1
|
|
||||||
-1.4,1.5,1,-1,-1
|
|
||||||
1.5,6.5,-1,1,-1
|
|
||||||
2.6,2.3,-1,-1,1
|
|
||||||
-1.2,1.4,1,-1,-1
|
|
||||||
1.5,5.6,-1,1,-1
|
|
||||||
1.3,5.7,-1,1,-1
|
|
||||||
1.2,6.1,-1,1,-1
|
|
||||||
3,2.2,-1,-1,1
|
|
||||||
3,1.8,-1,-1,1
|
|
||||||
-1.1,1.4,1,-1,-1
|
|
||||||
1.4,6.8,-1,1,-1
|
|
||||||
-1.6,1.6,1,-1,-1
|
|
||||||
-1.2,1.4,1,-1,-1
|
|
||||||
3.2,1.8,-1,-1,1
|
|
||||||
-1.2,1.5,1,-1,-1
|
|
||||||
2.7,1.8,-1,-1,1
|
|
||||||
-1.3,1.4,1,-1,-1
|
|
||||||
1.3,6.2,-1,1,-1
|
|
||||||
-1.3,1.3,1,-1,-1
|
|
||||||
3,1.8,-1,-1,1
|
|
||||||
2.7,1.9,-1,-1,1
|
|
||||||
-1.2,1.4,1,-1,-1
|
|
||||||
-1.2,1.2,1,-1,-1
|
|
||||||
1.5,6.7,-1,1,-1
|
|
||||||
2.5,1.9,-1,-1,1
|
|
||||||
3.3,2.1,-1,-1,1
|
|
||||||
2.8,1.8,-1,-1,1
|
|
||||||
-1.2,1.3,1,-1,-1
|
|
||||||
3.8,2.2,-1,-1,1
|
|
||||||
2.5,2,-1,-1,1
|
|
||||||
1.3,6.1,-1,1,-1
|
|
||||||
-1.2,1.4,1,-1,-1
|
|
||||||
-1.1,1.5,1,-1,-1
|
|
||||||
3,2.3,-1,-1,1
|
|
||||||
-1.4,1.9,1,-1,-1
|
|
||||||
-1.2,1.6,1,-1,-1
|
|
||||||
1,6,-1,1,-1
|
|
||||||
1.3,5.5,-1,1,-1
|
|
||||||
|
@@ -1,4 +0,0 @@
|
|||||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,-1,-1,-1
|
|
||||||
0,0,1,0,0,0,0,1,0,0,1,1,1,1,1,0,0,1,0,0,0,0,1,0,0,-1,1,-1,-1
|
|
||||||
1,0,0,0,1,0,1,0,1,0,0,0,1,0,0,0,1,0,1,0,1,0,0,0,1,-1,-1,1,-1
|
|
||||||
0,0,0,0,0,0,1,1,1,0,0,1,0,1,0,0,1,1,1,0,0,0,0,0,0,-1,-1,-1,1
|
|
||||||
|
|
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 |
@@ -0,0 +1,72 @@
|
|||||||
|
# ADAptative LINear Element
|
||||||
|
|
||||||
|
ADALINE est une variante de la descente du gradient
|
||||||
|
|
||||||
|
La descente du gradient attend d'avoir évalué chaque exemple du dataset avant de corriger les poids synaptiques et de calculer l'erreur quadratique moyenne. ADALINE rends le processus plus rapide en corrigeant les poids à chaque exemple, comme le Perceptron.
|
||||||
|
|
||||||
|
On peut considérer ADALINE comme un Perceptron avec la fonction linéaire comme fonction d'activation et une condition d'arrêt de l'entraînement qui utilise l'erreur quadratique moyenne.
|
||||||
|
|
||||||
|
## Structure d'ADALINE
|
||||||
|
|
||||||
|
Pas de changement par rapport à la structure de la Descente du gradient.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Fonctionnement de l'entraînement
|
||||||
|
|
||||||
|
En comparaison à la Descente du gradient, la correction des poids se fait à chaque évaluation d'un exemple, comme pour le Perceptron.
|
||||||
|
|
||||||
|
1. On initialise les poids synaptiques.
|
||||||
|
2. On commence l'entraînement : pour chaque exemple du dataset choisi, on évalue la sortie du Perceptron ($y$).
|
||||||
|
3. On fait le calcul de l'erreur (notée $e$), en soustrayant la sortie du Perceptron à la sortie attendue (le *label*).
|
||||||
|
4. On met à jour les poids synaptiques avec la même formule que le Perceptron : on ajoute à la valeur du poids l'erreur multipliée par la valeur de l'exemple assignée à l'entrée, multipliée par le **Taux d'apprentissage**. Ce qui donne : $w_i' = w_i + \eta \times (\text{label } - y) \times x_i$ pour tout poids synaptique $i$ où $w_i'$ est le poids synaptique corrigé et $x_i$ est l'entrée numéro $i$.
|
||||||
|
5. Une fois tous les exemples du dataset parcourus, on calcule l'erreur quadratique moyenne ($E$) avec les derniers poids synaptiques.
|
||||||
|
6. Si l'erreur quadratique moyenne est plus petite que l'**erreur minimale** ou que le nombre maximal d'époques a été atteint, l'entraînement est terminé. Sinon, on recommence à partir du point 2.
|
||||||
|
|
||||||
|
<div class="exercice">
|
||||||
|
Reprenons le dataset oblique afin de faire un test contrôle :
|
||||||
|
|
||||||
|
- <span>Dataset</span> : `Classification - Oblique (linéairement séparable)`
|
||||||
|
|
||||||
|
Puis cliquez sur "Lancer" tout en laissant les autres paramètres intacts.
|
||||||
|
|
||||||
|
Cliquez sur le bouton `Afficher uniquement l'erreur quadratique moyenne`, la ligne que l'on peut voir est moins "douce" et contient des "bosses" avec les mêmes paramètres que la descente du gradient. Cela est dû au fait que les corrections se font à chaque exemple du dataset. Le taux d'apprentissage devrait être mis un peu plus bas pour faire disparaître ces "bosses", mais ici elles sont encore petites et ne posent pas de problèmes.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Ajustements du Taux d'apprentissage
|
||||||
|
|
||||||
|
La descente du gradient est plus lente que le Perceptron. Est-ce qu'ADALINE règle ce problème ?
|
||||||
|
|
||||||
|
<div class="exercice">
|
||||||
|
Reprenons le dataset oblique afin de pouvoir comparer les résultats, et augmentons un peu le taux d'apprentissage :
|
||||||
|
|
||||||
|
- <span>Dataset</span> : `Classification - Oblique (linéairement séparable)`
|
||||||
|
- <span>Taux d'apprentissage</span> : entre $0.002$ et $0.01$ (ex: $0.005$)
|
||||||
|
- (Optionnel) <span>Nombre maximum d'époques</span> : $1000$ (plus d'époques seront affichées)
|
||||||
|
|
||||||
|
Puis cliquez sur "Lancer" tout en laissant les autres paramètres intacts.
|
||||||
|
|
||||||
|
On peut voir que le nombre d'époques que l'entraînement a pris est bien plus petit : de 200 à 900 époques en fonction du taux d'apprentissage choisi, contre presque 2000 pour le test contrôle.
|
||||||
|
|
||||||
|
Cliquez sur le bouton `Afficher uniquement l'erreur quadratique moyenne`, la ligne que l'on peut observer est encore moins douce. Les "bosses" sont amplifiées à cause de la hausse du taux d'apprentissage.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
### Avec un taux d'apprentissage trop haut
|
||||||
|
|
||||||
|
On a déjà vu avec la descente du gradient ce que faisait un taux d'apprentissage trop bas et trop haut. Mais comme on le voit, l'erreur quadratique moyenne du perceptron forme des bosses qui s'amplifient avec le taux d'apprentissage. Que se passe-t-il alors quand le paramètre est trop haut ?
|
||||||
|
|
||||||
|
<div class="exercice">
|
||||||
|
Reprenons le dataset oblique afin de pouvoir comparer les résultats, et augmentons le taux d'apprentissage à une valeur trop grande :
|
||||||
|
|
||||||
|
- <span>Dataset</span> : `Classification - Oblique (linéairement séparable)`
|
||||||
|
- <span>Taux d'apprentissage</span> : $0.023$
|
||||||
|
|
||||||
|
Puis cliquez sur "Lancer" tout en laissant les autres paramètres intacts.
|
||||||
|
|
||||||
|
Les erreurs vacillent et le neurone a du mal à converger.
|
||||||
|
|
||||||
|
Cliquez sur le bouton `Afficher uniquement l'erreur quadratique moyenne`, les bosses de la ligne sont trop amplifiées et l'entraînement se joue sur la chance de tomber sur le bon prochain exemple. Relancer l'entraînement plusieurs fois et vous aurez des nombres d'époque finale différents. Ce n'est bien sûr pas optimal sauf si l'on se sent chanceux.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Mettre une valeur de taux d'apprentissage encore plus haute va donner le même résultat que la descente du gradient : des valeurs de poids qui s'emballent dans des extrêmes.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<div class="exercice">
|
||||||
|
Reprenons le dataset oblique afin de pouvoir comparer les résultats, et diminuons l'erreur minimale :
|
||||||
|
|
||||||
|
- <span>Dataset</span> : `Classification - Oblique (linéairement séparable)`
|
||||||
|
- <span>Erreur minimale</span> : en dessous de $0.05$ ou même $0$
|
||||||
|
- (Optionnel) <span>Nombre maximum d'époques</span> : $4000$
|
||||||
|
|
||||||
|
Puis cliquez sur "Lancer" tout en laissant les autres paramètres intacts.
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
# La Descente du gradient
|
||||||
|
|
||||||
|
Comme on a pu le voir, le [Perceptron](/perceptron?type=simple) présente quelques limitations :
|
||||||
|
|
||||||
|
- Il est incapable de faire de la régression
|
||||||
|
- Il n'optimise pas sa solution, la limite de décision "colle" les points du dataset.
|
||||||
|
|
||||||
|
La descente du gradient essaye d'améliorer l'algorithme d'entraînement et rend les valeurs des entrées et des sorties réelles (des nombres à décimale, positifs, négatifs ou nuls).
|
||||||
|
|
||||||
|
La descente du gradient n'essaye plus de ne plus faire aucune erreur comme le Perceptron, mais à la place va essayer de **minimiser l'erreur quadratique moyenne** (d'avoir une erreur quadratique moyenne la plus petite possible). Cette méthode de minimisation de l'erreur est appelée *descente du gradient*. L'erreur quadratique moyenne est calculée comme suit : la somme de l'erreur du réseau sur tous les exemples du dataset au carré, le tout divisé par $2$. Cela revient à faire
|
||||||
|
$$E = \frac{1}{2} \sum^K_{k=1}{(e^{(k)})^2}$$
|
||||||
|
où :
|
||||||
|
|
||||||
|
- $E$ est l'erreur quadratique moyenne.
|
||||||
|
- $K$ le nombre d'exemples dans le dataset.
|
||||||
|
- $e^{(k)}$ l'erreur du réseau sur l'exemple $k$ du dataset. ($e = \text{ label } - y$)
|
||||||
|
|
||||||
|
## Structure de la Descente du gradient
|
||||||
|
|
||||||
|
La structure ne change pas par rapport au Perceptron simple, si ce n'est que la fonction d'activation est maintenant une droite $y = x$. Cela veut dire que la descente du gradient retourne des valeurs réelles non bornées.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Fonctionnement de l'entraînement
|
||||||
|
|
||||||
|
Par rapport au Perceptron, la correction des poids se fait après chaque époque, et la condition de fin d'entraînement s'établit sur l'erreur quadratique moyenne.
|
||||||
|
|
||||||
|
1. On initialise les poids synaptiques.
|
||||||
|
2. On commence l'entraînement : pour chaque exemple du dataset choisi, on évalue la sortie du Perceptron ($y$).
|
||||||
|
3. On fait le calcul de l'erreur (notée $e$), en soustrayant la sortie du Perceptron à la sortie attendue (le *label*). Comme l'entrée et la sortie de la descente du gradient sont des réels, ici l'erreur est plus précise que le perceptron qui avait une erreur de -1, 0 ou 1.
|
||||||
|
4. On calcule, pour chaque poids synaptique, un signal d'erreur simmilaire au perceptron. Le signal d'erreur est égal à l'erreur multipliée par la donnée de l'exemple assignée à cette l'entrée, multipliée par le Taux d'apprentissage. Ce signal est ajouté à une variable de correction (notée $Dw_1$, $Dw_2$, $Dw_3$, ...) qui garde la somme des signaux d'erreurs pour chaque poids synaptique. Cette variable de correction est utilisée car on ne modifie les poids synaptiques qu'à la fin de l'époque, quand tous les exemples ont été évalués.
|
||||||
|
5. Une fois tous les exemples du dataset parcourus, on actualise les poids synaptiques en appliquant leur variable de correction respectives.
|
||||||
|
6. On calcule l'erreur quadratique moyenne ($E$) avec les nouveaux poids.
|
||||||
|
7. Si l'erreur quadratique moyenne est plus petite que l'**erreur minimale** ou que le nombre maximal d'époques a été atteint, l'entraînement est terminé. Sinon, on recommence à partir du point 2. L'erreur minimale est un paramètre détaillé plus bas.
|
||||||
|
|
||||||
|
<div class="exercice">
|
||||||
|
Reprenons le dataset oblique utilisé avec le Perceptron afin de vérifier que la descente du gradient corrige bien ses limitations :
|
||||||
|
|
||||||
|
- <span>Dataset</span> : `Classification - Oblique (linéairement séparable)`
|
||||||
|
|
||||||
|
Puis cliquez sur "Lancer" tout en laissant les autres paramètres intacts.
|
||||||
|
|
||||||
|
On peut voir que la ligne de décision est au centre entre les deux classes, et qu'elle ne colle pas aux points. Toutefois un point négatif que l'on peut noter est qu'il est bien **plus lent**. Le nombre d'itérations est au-dessus de 1000 comparé à une centaine pour le perceptron.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Qu'est-ce que l'Erreur minimale fait ?
|
||||||
|
|
||||||
|
L'erreur minimale est un paramètre choisi par le développeur, c'est le seuil que le réseau doit atteindre pour pouvoir terminer l'entraînement. Plus le seuil est bas, plus le réseau doit être précis avant de pouvoir arrêter l'entraînement.
|
||||||
|
|
||||||
|
### Avec une erreur minimale trop basse
|
||||||
|
|
||||||
|
L'on pourrait alors se dire de mettre l'erreur minimale à 0, pour avoir un modèle le plus précis possible.
|
||||||
|
|
||||||
|
<div class="exercice">
|
||||||
|
Reprenons le dataset oblique afin de pouvoir comparer les résultats, et diminuons l'erreur minimale :
|
||||||
|
|
||||||
|
- <span>Dataset</span> : `Classification - Oblique (linéairement séparable)`
|
||||||
|
- <span>Erreur minimale</span> : en dessous de $0.05$ ou même $0$
|
||||||
|
|
||||||
|
Puis cliquez sur "Lancer" tout en laissant les autres paramètres intacts.
|
||||||
|
|
||||||
|
On peut voir en dessous du tableau que l'entraînement s'est arrêté car le nombre maximal d'époques a été atteint. On pourrait l'augmenter (max 5000 pour mon petit serveur), mais ce serait du temps perdu. La solution du réseau est suffisante et il n'y aurait pas beaucoup de gain, même pour le triple d'itérations maximales en plus. Pour le prouver, cliquez sur le bouton `Afficher uniquement l'erreur quadratique moyenne`, la ligne que l'on peut voir ressemble fortement à la fonction logarithme $y = log(x^{-1})$ (attention à l'échelle qui est elle-même logarithmique). La progression de la descente du gradient est donc logarithmique ; les plus gros changements se font dans les premières époques.
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### Avec une erreur minimale trop haute
|
||||||
|
|
||||||
|
Alors que fait une erreur minimale mise trop haute par le développeur ?
|
||||||
|
|
||||||
|
<div class="exercice">
|
||||||
|
Reprenons le dataset oblique afin de pouvoir comparer les résultats, et augmentons l'erreur minimale :
|
||||||
|
|
||||||
|
- <span>Dataset</span> : `Classification - Oblique (linéairement séparable)`
|
||||||
|
- <span>Erreur minimale</span> : au-dessus de $0.15$ (ex: $0.2$)
|
||||||
|
|
||||||
|
Puis cliquez sur "Lancer" tout en laissant les autres paramètres intacts.
|
||||||
|
|
||||||
|
L'entraînement est très rapide, même un peu trop rapide... Le réseau ne donne pas une bonne solution, voire une solution sur laquelle l'entraînement du perceptron ne se serait pas arrêté.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### Comment choisir une bonne valeur pour l'erreur minimale ?
|
||||||
|
|
||||||
|
Une méthode consiste à mettre l'erreur minimale à 0, mettre le nombre maximum d'époques à une valeur très haute, et surveiller l'erreur quadratique moyenne : si elle stagne, le réseau ne fera plus beaucoup d'avancées sur un nombre raisonnable d'époques.
|
||||||
|
|
||||||
|
## Qu'est-ce que le Taux d'Apprentissage fait ?
|
||||||
|
|
||||||
|
Comme expliqué dans le processus d'entraînement du Perceptron et de la descente du gradient, le taux d'apprentissage est un paramètre permettant de contrôler avec quelle "force" les poids synaptiques se mettent à jour.
|
||||||
|
|
||||||
|
### Avec un Taux d'Apprentissage trop haut
|
||||||
|
|
||||||
|
<div class="exercice">
|
||||||
|
Reprenons le même dataset oblique que le test contrôle, mais en augmentant considérablement le Taux d'Apprentissage :
|
||||||
|
|
||||||
|
- <span>Dataset</span> : `Classification - Oblique (linéairement séparable)`
|
||||||
|
- <span>Taux d'apprentissage</span> : au dessus de $0.001$ (ex: $0.002$)
|
||||||
|
- (Optionnel) <span>Nombre maximum d'époques</span> : $200$
|
||||||
|
|
||||||
|
Puis cliquez sur "Lancer" tout en laissant les autres paramètres intacts.
|
||||||
|
|
||||||
|
Les valeurs des graphes et du tableau vont dans les extrêmes (même à NaN ("Not A Number")). À cause du taux d'apprentissage trop haut, à chaque correction des poids synaptiques, leurs valeurs ont augmentées de façon exponentielle. Créant un cycle vicieux.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### Avec un Taux d'Apprentissage trop bas
|
||||||
|
|
||||||
|
<div class="exercice">
|
||||||
|
Reprenons le même dataset oblique que le test contrôle, mais en diminuant considérablement le Taux d'Apprentissage :
|
||||||
|
|
||||||
|
- <span>Dataset</span> : `Classification - Oblique (linéairement séparable)`
|
||||||
|
- <span>Taux d'apprentissage</span> : en dessous de $0.0005$ (ex: $0.0001$)
|
||||||
|
- (Optionnel) <span>Nombre maximum d'époques</span> : $4000$
|
||||||
|
|
||||||
|
Puis cliquez sur "Lancer" tout en laissant les autres paramètres intacts.
|
||||||
|
|
||||||
|
L'entraînement est beaucoup plus long, le réseau atteint même peut-être le nombre maximum d'époques. Cela est dû à la correction des poids synaptiques qui est devenue plus petite à chaque époque. L'on pourrait enlever la limite du nombre maximum d'époques, attendre un peu plus longtemps, et avoir un résultat précis. Mais, le temps, c'est de l'argent. Faisons le parallèle avec OpenAI : l'entraînement d'un nouveau modèle coûte des millions de dollars en puissance de calcul et peut prendre des mois entiers. Si l'on réduit la vitesse à laquelle le modèle s'entraîne, la facture de l'entraînement peut vite être salée.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
En note annexe : dans les frameworks actuels, cette variable est gérée automatiquement et je ne pense pas qu'elle pose vraiment un problème à OpenAI.
|
||||||
|
|
||||||
|
### Comment choisir une bonne valeur pour le taux d'apprentissage ?
|
||||||
|
|
||||||
|
À l'essai et erreur ! Commencer avec une valeur arbitraire et diminuer tant que les poids synaptiques partent dans des valeurs infiniment grandes. Augmenter dans le cas contraire jusqu'à trouver une bonne valeur.
|
||||||
|
|
||||||
|
## Test avec la régression
|
||||||
|
|
||||||
|
Comme dit dans l'[introduction](/), quand un réseau de neurones fait de la régression, il essaye de créer la fonction mathématique qui épouse au mieux le nuage de données du dataset. Une des limitations du perceptron est le fait qu'il est incapable de faire de la régression. Grâce à ses entrées et sorties réelles, la descente du gradient est capable de faire de la régression.
|
||||||
|
|
||||||
|
<div class="exercice">
|
||||||
|
Testons la régression avec un dataset simple :
|
||||||
|
|
||||||
|
- <span>Dataset</span> : `Régression - Oblique`
|
||||||
|
|
||||||
|
Une fois sélectionné, on peut voir que le nuage de points du dataset forme presque une droite oblique. Imaginer une droite qui traverse au mieux tous les points, c'est-à-dire une droite qui est le plus proche possible de tous les points en même temps.
|
||||||
|
|
||||||
|
Cliquez sur "Lancer" tout en laissant les autres paramètres intacts.
|
||||||
|
|
||||||
|
On peut constater que la droite se dirige rapidement au bon endroit, le reste des époques ne font que du peaufinage. La descente du gradient essaye de minimiser l'erreur sans essayer de n'en faire aucune, ce qui est parfait comme avec un dataset comme celui-ci, il est impossible de relier tous les points avec une droite, et donc impossible de ne pas faire d'erreur.
|
||||||
|
</div>
|
||||||
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 205 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 165 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 28 KiB |
@@ -0,0 +1,96 @@
|
|||||||
|
# Perceptron Viewer
|
||||||
|
|
||||||
|
Cet outil vous permet d'expérimenter avec les réseaux de neurones et les perceptrons. Vous pouvez ajuster les paramètres et observer l'apprentissage en temps réel.
|
||||||
|
|
||||||
|
## Contexte
|
||||||
|
|
||||||
|
Ce projet a été d'abord créé pour un cours de réseaux de neurones artificiels que j'ai suivi durant mon master en Architecture des Systèmes Informatiques à la Haute École de la Province de Liège. Il s'agit ici d'une version plus conviviale, documentée et optimisée.
|
||||||
|
|
||||||
|
Je conseille de finir de lire les prochains chapitres qui traitent des bases des réseaux de neurones et qui présentent l'interface. Je conseille ensuite d'utiliser les onglets au-dessus de la page dans l'ordre indiqué. Chaque onglet présente une évolution des réseaux de neurones dans l'ordre chronologique des découvertes et des améliorations faites.
|
||||||
|
|
||||||
|
## Introduction aux Réseaux de Neurones Artificiels
|
||||||
|
|
||||||
|
Le but principal d'un réseau de neurones artificiels est de **reproduire une fonction mathématique complexe**. En résumé, un réseau de neurones est conçu pour apprendre des relations complexes entre des données d'entrée et des données de sortie (la prédiction souhaitée). Il apprend à faire correspondre une entrée $X$ à une sortie $Y$ via une série de calculs pondérés et d'activations.
|
||||||
|
|
||||||
|
### Premières Découvertes des neurones artificiels
|
||||||
|
|
||||||
|
Les bases des réseaux de neurones remontent à la modélisation biologique du cerveau. Les premières tentatives par un neuropsychiatre et logicien vers 1943 visaient à créer des modèles simplifiés pour comprendre comment le cerveau traite l'information. Il s'agissait de **simuler la capacité du cerveau à apprendre et à prendre des décisions à partir de données brutes**, ce qui était essentiel pour comprendre la cognition et le traitement du *signal*.
|
||||||
|
|
||||||
|
Le résultat qui nous intéresse de cette recherche est l'"Electronic Brain", très semblable au [Perceptron](/perceptron?type=simple) que l'on va voir au premier onglet. Il transforme les données d'entrée ($X$) pour en sortie donner une décision ($Y$)
|
||||||
|
|
||||||
|
Exemple d'Electronic Brain à 3 entrées : 
|
||||||
|
|
||||||
|
### Le Machine Learning
|
||||||
|
|
||||||
|
Les neurones artificiels sont la structure "intelligente" du réseau. Cependant, il faut d'abord l'entraîner afin de déterminer les paramètres qui rendent ce réseau intelligent. C'est là que rentre en jeu le Machine Learning (souvent abrévié "ML"). On distingue principalement deux grandes catégories :
|
||||||
|
|
||||||
|
#### L'Apprentissage Supervisé
|
||||||
|
|
||||||
|
Des données d'entraînement sont données au modèle, appelées *exemples*. À chaque exemple est associée une bonne réponse appelée *tag* (ou *étiquette* en français).
|
||||||
|
|
||||||
|
Pendant l'apprentissage, pour chaque exemple, le réseau de neurones est consulté. Si la sortie du réseau ($Y$) n'est pas égale à la réponse attendue (le *tag* de l'exemple), les paramètres du réseau sont modifiés. Enfin, l'on passe à l'exemple suivant.
|
||||||
|
|
||||||
|
L'apprentissage supervisé demande donc d'étiqueter (ou "tagger") chaque exemple avec une réponse attendue précise et fiable. À défaut, le réseau de neurones pourrait manquer de précision et de fiabilité.
|
||||||
|
|
||||||
|
#### L'Apprentissage Non Supervisé
|
||||||
|
|
||||||
|
L'apprentissage non supervisé n'utilise pas de *tag* dans les données. À la place, il utilise des méthodes d'analyse de données mathématiques comme l'[ACP](https://notes.matthiasg.dev/#/page/school%2Fexploration%20de%20donn%C3%A9es%2F1.%20acp).
|
||||||
|
Cet apprentissage est utilisé pour trouver des pattern dans des données.
|
||||||
|
|
||||||
|
Tous les réseaux présentés ici utilisent l'**Apprentissage Supervisé**. L'apprentissage non supervisé ne sera donc pas vu plus en détail.
|
||||||
|
|
||||||
|
### Types de problèmes à résoudre
|
||||||
|
|
||||||
|
Les réseaux de neurones peuvent résoudre plusieurs types de problèmes. Nous allons ici en voir deux :
|
||||||
|
|
||||||
|
#### La Régression
|
||||||
|
|
||||||
|
Le réseau de neurones estime la fonction mathématique continue d'une suite ou d'un nuage de points, afin par exemple de pouvoir prédire les données suivantes.
|
||||||
|
|
||||||
|
Exemple : à partir des températures des cinq dernières années, entraîner un réseau de neurones pour qu'il reproduise la fonction mathématique qui se rapproche le plus de tous les points. Une fois entraîné, le réseau est capable d'estimer les prochaines températures.
|
||||||
|

|
||||||
|
|
||||||
|
#### La classification
|
||||||
|
|
||||||
|
Le réseau de neurones va attribuer la catégorie la plus probable à chaque exemple. Ces catégories sont appelées *classes* en mathématique. Un réseau de neurones peut classer les données dans autant de classes que nécessaire, mais le réseau doit être adapté en conséquence au niveau de la *couche de sortie* que l'on verra dans les réseaux multicouches.
|
||||||
|
|
||||||
|
Exemples : détection d'objets dans une image, déterminer si un e-mail est un spam ou non, déterminer l'état de santé d'un patient à partir de ses données vitales, ...
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Chaque point contenu dans les zones sera catégorisé par le réseau de neurones dans la classe correspondante.
|
||||||
|
|
||||||
|
### [Quand la technologie copie la nature](https://fr.wikipedia.org/wiki/Biomim%C3%A9tisme)
|
||||||
|
|
||||||
|
Les neurones artificiels sont très semblables aux neurones biologiques. Un neurone biologique est une unité de traitement, capable de recevoir des signaux, de les traiter et de transmettre des informations via des connexions appelées *synapses*.
|
||||||
|
|
||||||
|
### Anatomie du Neurone Artificiel
|
||||||
|
|
||||||
|
Un neurone artificiel (ou une couche neuronale) est composé de plusieurs éléments interconnectés qui collaborent pour produire une sortie. Voici les composants clés :
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- **Entrée ($x_1$, $x_2$, $x_3$, ...)** : Les données brutes des exemples donnés au réseau (ex : la température en janvier, février, mars, etc.).
|
||||||
|
- **Poids Synaptique ($w_1$, $w_2$, $w_3$, ...)** : Un coefficient numérique qui détermine l'importance de chaque entrée pour la prochaine étape. Ce sont les paramètres que le réseau modifie quand il "apprend".
|
||||||
|
- **Biais ou Entrée Fictive ($w_0$)** : Une poids ajouté qui permet de déplacer la fonction d'activation, aidant le modèle à mieux s'ajuster aux données. Ce poids est toujours lié à une entrée constante avec comme valeur $1$.
|
||||||
|
- **Potentiel ($p$)** : La somme pondérée de toutes les entrées et des biais : $p = (\sum w_i x_i) + w_0$.
|
||||||
|
- **Fonction d'Activation ($f(x)$)** : Une fonction non linéaire qui va borner la valeur. Cette fonction est choisie par le développeur ou imposée par le type de neurone, comme on va le voir par exemple pour [ADALINE](perceptron?type=adaline).
|
||||||
|
- **Sortie ($y$)** : Le résultat final du neurone, la sortie du réseau, ce que le réseau a déterminé à partir de l'exemple.
|
||||||
|
|
||||||
|
## Autre vocabulaire
|
||||||
|
|
||||||
|
- **Dataset** : collections d'exemples étiquetté ayant la même structure et traitant du même sujet. Exemple : un dataset contenant la température enregistrée pour chaque jour des cinq dernières années. Pour chaque exemple on a donc le jour et la température.
|
||||||
|
|
||||||
|
## Présentation de Perceptron Viewer
|
||||||
|
|
||||||
|
Perceptron Viewer permet de modifier les paramètres d'entraînement, d'entraîner des réseaux de neurones et perceptrons sur un serveur, et de visualiser en direct les erreurs, les zones décisionnelles et les prédictions du réseau.
|
||||||
|
|
||||||
|
### Présentation de l'interface
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
La configuration de l'entraînement et les graphes seront expliqués plus en détail dans l'onglet [Perceptron](/perceptron?type=simple).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Je vous invite dès à présent à vous rendre sur l'[onglet 1 : Perceptron Simple](/perceptron?type=simple).
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# Le Réseau Monocouche
|
||||||
|
|
||||||
|
Jusqu'ici, nous avons vu des réseaux avec un seul neurone. Une des limites d'un tel réseau est qu'**un seul neurone ne peut séparer que deux classes**. En effet, un neurone ne peut pas donner simultanément l'appartenance ou non à plusieurs classes.
|
||||||
|
|
||||||
|
Pour séparer des données de plusieurs classes, il devient nécessaire d'utiliser plusieurs neurones, chacun étant spécialisé pour détecter l'appartenance ou non à une classe spécifique.
|
||||||
|
|
||||||
|
Exemples :
|
||||||
|
|
||||||
|
- Pour séparer 3 classes, il faut au moins 3 neurones.
|
||||||
|
- Pour séparer 2 classes, un seul neurone suffit (si $y > 0.5$ alors classe A, sinon classe B).
|
||||||
|
|
||||||
|
C'est exactement ce qu'un réseau monocouche fait : il permet d'avoir une quantité infinie de neurones. Le nombre de neurones est contrôlé automatiquement par PerceptronViewer en fonction du nombre de classes différentes dans le dataset.
|
||||||
|
|
||||||
|
<div class="exercice">
|
||||||
|
Reprenons le dataset oblique afin de pouvoir comparer les résultats avec ceux d'un réseau à un neurone :
|
||||||
|
|
||||||
|
- <span>Dataset</span> : `Classification - Oblique (linéairement séparable)`
|
||||||
|
|
||||||
|
Puis cliquez sur "Lancer" tout en laissant les autres paramètres intacts.
|
||||||
|
|
||||||
|
On peut voir maintenant des zones apparaître sur le graphe de décision. Chacune de ces zones représente la ligne de décision d'un neurone : si un point se trouve dans une de ces zones, il sera considéré comme faisant partie de la classe correspondante.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Structure du réseau monocouche
|
||||||
|
|
||||||
|
Le réseau monocouche permet de choisir quel type de neurone utiliser. Dans le cadre de PerceptronViewer, des neurones ADALINE ont été utilisés.
|
||||||
|
|
||||||
|
Afin de clarifier les schémas de réseaux complexes, les perceptrons sont représentés par un cercle.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Chaque neurone est connecté à toutes les entrées et la valeur de leur sortie va déterminer la classe à laquelle l'exemple appartient.
|
||||||
|
|
||||||
|
## Fonctionnement de l'entraînement
|
||||||
|
|
||||||
|
L'entraînement va dépendre du type de neurone utilisé. Globalement, deux approches principales existent :
|
||||||
|
|
||||||
|
1. Entraîner tous les neurones simultanément : au moment de la correction des poids synaptiques, on parcourt tous les neurones.
|
||||||
|
2. Entraîner les neurones séparément : considérer les neurones comme des réseaux séparés. On entraîne le premier réseau, puis on passe au suivant.
|
||||||
|
|
||||||
|
La méthode d'entraînement utilisée dans PerceptronViewer est la première car elle se rapproche de l'entraînement du réseau multicouches, ce qui évite d'implémenter des méthodes différentes.
|
||||||
|
|
||||||
|
<div class="exercice">
|
||||||
|
Exploitons l'avantage du réseau monocouche en utilisant un dataset composé de 3 classes :
|
||||||
|
|
||||||
|
- <span>Dataset</span> : `Classification - 3 classes (linéairement séparable)`
|
||||||
|
|
||||||
|
Puis cliquez sur "Lancer" tout en laissant les autres paramètres intacts.
|
||||||
|
|
||||||
|
On obtient une séparation nette entre les 3 classes qui est assez bien généralisée (si l'on ajoute un nouveau point, il sera dans la classe la plus proche)
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# Le Réseau Multicouches
|
||||||
|
|
||||||
|
> Et les datasets non linéairement séparables dans tout ça ?
|
||||||
|
|
||||||
|
Jusqu'à présent, nous avons utilisé que des datasets linéairement séparables, pour la simple raison que les réseaux à une seule couche ne produisent que des frontières de décision linéaires (des droites).
|
||||||
|
|
||||||
|
Pour séparer des classes de manière non linéaire, il est nécessaire d'utiliser des fonctions non linéaires au sein du réseau. C'est là que les réseaux multicouches rentrent en jeu : utiliser plusieurs couches permet d'avoir des frontières de décision non linéaires.
|
||||||
|
|
||||||
|
## Structure du réseau monocouche
|
||||||
|
|
||||||
|
Par rapport au réseau monocouche, on rajoute des couches dans lesquelles tous les neurones sont interconnectés aux neurones de la couche suivante.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
On distingue 3 types de couches :
|
||||||
|
|
||||||
|
- La couche d'entrée : ne contient pas de neurones à proprement parler car ils n'effectuent aucun traitement sur les données d'entrées.
|
||||||
|
- Les couches cachées : chaque couche contient des neurones qui vont "mâcher" les données pour les couches suivantes. Dans PerceptronViewer, le nombre de neurones par couches cachées peut être choisi avec le paramètre **Nombre de neurones par couche cachée** et le nombre de couches cachées peut être choisi avec le paramètre **Nombre de couches cachées**.
|
||||||
|
- La couche de sortie : contient les neurones finaux qui vont transformer les données de sortie de la couche cachée en données utilisables (prédiction ou classification).
|
||||||
|
|
||||||
|
## Fonctionnement de l'entraînement
|
||||||
|
|
||||||
|
L'entraînement d'un réseau multicouche demande plus de technique à cause de la présence de plusieurs couches.
|
||||||
|
|
||||||
|
Voici le fonctionnement de l'entraînement dans les grandes lignes :
|
||||||
|
|
||||||
|
1. On initialise les poids synaptiques.
|
||||||
|
2. On commence l'entraînement : pour chaque exemple du dataset choisi, on évalue la sortie du réseau avec la *propagation avant* (forward propagation en anglais) : les données passent de la couche d'entrée, traversent les couches cachées et arrivent enfin à la couche de sortie. Pour chaque couche, l'entrée de chaque neurone correspond à la sortie des neurones de la couche précédente.
|
||||||
|
3. On calcule l'erreur du réseau sur cet exemple en utilisant la *rétropropagation* (backpropagation en anglais) : on calcule le signal d'erreur de la couche de sortie et on le propage à travers les couches jusqu'à la couche d'entrée.
|
||||||
|
4. On met à jour les poids synaptiques en fonction du signal d'erreur.
|
||||||
|
|
||||||
|
Je ne détaillerai pas ici la rétropropagation et la mise à jour des poids synaptiques. Cependant, si vous êtes intéressés, vous pouvez trouver les détails ici : [Wikipédia - Apprentissage du perceptron multicouche](https://fr.wikipedia.org/wiki/Perceptron_multicouche#Apprentissage).
|
||||||
|
|
||||||
|
<div class="exercice">
|
||||||
|
Exploitons l'avantage du réseau multicouche avec un dataset non linéairement séparable :
|
||||||
|
|
||||||
|
- <span>Dataset</span> : `Classification - Donut (non linéairement séparable)`
|
||||||
|
|
||||||
|
Puis cliquez sur "Lancer" tout en laissant les autres paramètres intacts.
|
||||||
|
|
||||||
|
Le réseau commence avec une zone uniforme qui couvre tout le graphe, ensuite, progressivement, crée une forme ovale qui englobe la classe du milieu du donut. Le reste des époques est passé à peaufiner la forme pour qu'elle englobe au mieux tous les exemples.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Qu'est-ce que le nombre de couches et de neurones par couche fait ?
|
||||||
|
|
||||||
|
Ces deux paramètres sont très importants car ils vont déterminer la précision et le temps d'entraînement du réseau. Il n'y a pas réellement de formule magique, les bonnes valeurs sont obtenues par essai et erreur. Voici tout de même ce que va changer chaque paramètre et comment mieux choisir les valeurs.
|
||||||
|
|
||||||
|
### Quels sont les effets
|
||||||
|
|
||||||
|
Augmenter les paramètres va augmenter le nombre total de neurones. Plus il y a de neurones, plus la ligne de décision peut être complexe. Une ligne plus complexe va pouvoir dessiner les contours plus précisément et suivre des courbes complexes.
|
||||||
|
|
||||||
|
Il faut un minimum de nombre total de couches en fonction du dataset :
|
||||||
|
|
||||||
|
- 1 seule couche : la ligne de décision est une droite, comme les perceptrons et le réseau monocouche vu précédemment.
|
||||||
|
- 2 couches : la ligne de décision peux courber et modéliser des formes non linéaires (résout des datasets non linéairement séparables).
|
||||||
|
- +2 couches : la ligne de décision a la possibilité d'être de plus en plus complexe, plus le nombre de couches est haut.
|
||||||
|
|
||||||
|
Il est recommandé de ne pas avoir plus de neurones par couche qu'il y a d'exemples dans le dataset. Sinon le modèle risque d'apprendre par cœur le dataset en retenant un exemple par neurone. Ce problème est appelé **surapprentissage**, il apparaît quand le modèle est tellement bon sur le dataset, qu'il est incapable de généraliser.
|
||||||
|
|
||||||
|
Trop peu de neurones empêche le réseau de pouvoir faire le contour des classes et de devoir faire des erreurs.
|
||||||
|
|
||||||
|
## La régression avec un réseau multicouches
|
||||||
|
|
||||||
|
La régression peut également demander plus de couches de neurones pour pouvoir épouser des formes complexes.
|
||||||
|
|
||||||
|
<div class="exercice">
|
||||||
|
Essayons cela avec un dataset de régression trop complexe pour les réseaux à une seule couche :
|
||||||
|
|
||||||
|
- <span>Dataset</span> : `Régression - Vague`
|
||||||
|
|
||||||
|
Puis cliquez sur "Lancer" tout en laissant les autres paramètres intacts.
|
||||||
|
|
||||||
|
Là où un réseau monocouche essaierait de trouver la moyenne des points avec une droite, le réseau multicouches est capable d'épouser les formes complexes du nuage.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Ici s'achève la théorie. Quelques datasets n'ont pas été utilisés comme `Classification - Oblique modifié (non linéairement séparable)`, qui contient quelques points ajoutés ou dont la classe a été inversée (erreur d'encodage ? Anomalies dans la récolte des données ? Valeur anormale ? À vous de voir). Ce dataset est intéressant à faire avec tous les types de perceptrons et réseaux car il montre le comportement de chacun face à un dataset non linéairement séparable mais toujours réalisable.
|
||||||