Se hai bisogno urgente del nostro intervento puoi contattarci al numero 370 148 9430
RENOR & Partners è una società di consulenza d'impresa con alti standard qualitativi e con i prezzi più bassi del mercato.
RENOR & Partners S.r.l.
Via Cicerone, 15 - Ariccia (RM)
info@renor.it
This post is also available in:
English (Inglese)
Inizio questo articolo facendo una premessa… PHP non è certamente il linguaggio ideale quando si parla di Intelligenza artificiale. Le reti neurali sono solitamente appannaggio di più scientifici come Python, che offre librerie ottimizzate a questo scopo come PyTorch e NumPy, ed è questo solitamente il linguaggio che utilizzo per lavorare in contesti di questo genere.
Tuttavia qualche mese fa parlavo con un amico davanti ad uno spritz e abbiamo cominciato, come al solito, a dire una serie di sciocchezze parlando di AI (vi risparmio le idee malsane che ci sono uscite), ma ci siamo poi concentrati su un tema: “Sarebbe possibile creare una rete neurale in PHP?” La risposta breve è sì, seppur con delle limitazioni.
Per comprendere questo articolo bisogno prima fare qualche premessa…
Nel panorama tecnologico odierno l’Intelligenza Artificiale (IA in italiano o AI in inglese), non è più un argomento di nicchia, ma un pilastro strategico per quasi qualsiasi tipo di azienda che vogli estrarre valore dai propri dati, ottimizzare processi e offrire prodotti realmente competitivi. In ambito HR & Workforce management, per esempio, i modelli predittivi permettono di anticipare assenza improvvise, calibrarie i turni in modo dinamico e, per ultimo, risparmiare tempo e risorse: esattamnete il binomio “velocità/qualità” di cui avevamo discusso in questo articolo: https://renor.it/velocita-e-qualita-nei-progetti/.
Ma chi di voi sa veramente cosa sia l’intelligenza artificiale?
Con il termine Intelligenza Artificiale si indica l’insieme di tecniche che permettono ad un sistema informatico di mostrare capacità normalmente attribuite all’intelligenza umana: ragionare, apprendere, prendere delle decisioni, riconoscere pattern complessi. All’interno di questo grande contenitore, il Machine Learning rappresenta l’approccio in cui l’apprendimento avviene attraverso l’analisi statistica dei dati, senza che tutte le regole sia codificate manualmente. Negli ultimi anni, l’affermazione del Deep Learning ha dato un’ulteriore spinta evolutiva: reti neurali molto profonde, formate da decine o centinaia di livelli di elaborazione, riescono a cogliere strutture nei dati che sfuggono ai modelli più tradizionali.
Una rete neurale artificiale è un modello matematico che si ispira alla struttura dei neuroni del nostro cervello. Del resto quasi tutte le scoperte umane si rifanno all’osservazione di ciò che esiste già in natura.
La struttura neurale del cervello permette di risolvere problemi dove la relazione tra input (ciò che percepiamo dall’esterno) e l’output (ciò che ne ricaviamo) è non lineare o di difficile formalizzazione.
Immaginiamo, ad esempio, di voler prevedere il rischio di ritardo di un dipendente in base a variabili come il traffico, le condizioni meteo, la cronologia personale di ritardi e assenze ed i programmi di trasporto pubblico. Il legame tra questi fattori è troppo complesso per essere descritto con poche istruzioni condizionali, ma una rete neurale ben addestrata può impararlo analizzando una grande quantità di esempi storici.
Il motivo di questa capacità risiede nel fatto che ogni connessione tra neuroni è associata ad un peso e ad un termine di bias. Capiremo poi cosa siano.
Durante la fase di addestramento la rete modifica questi parametri per minimizzare una funzione di errore che misura la discrepanza fra la sua previsione e il valore reale. Il procedimento di affinamento avviene attraverso l’algoritmo di back-propagation, responsabile del calcolo dei gradienti, e un metodo di ottimizzazione come la discesa del gradiente stocastico che aggiorna iterativamente i pesi. Alla fine del processo la rete non possiede una serie di regole scritte da chi l’ha programmata, ma una serie di coefficienti numerici che codificano in modo distribuito la conoscenza ricavata dai dati.
Ora che abbiamo un quadro generale possiamo capire i ruoli e le responsabilità di pesi e bias.
Il peso è il coefficiente numerico che modula l’intensità con cui un segnale in ingresso contribuisce all’attivazione del neurone successivo. Possiamo pensare che sia una manopola di un volume, aumentando il volume amplifichiamo il contributo di quella specifica caratteristica, mentre abbassando il volume la attenuiamo fino a poterne anche invertire il segno. Dal punto di vista matematico il peso moltiplica il valore dell’input e determina l’inclinazione della funzione che la rete sta apprendendo; un peso elevato indica che l’input in questione è fortemente correlato all’output, un peso prossimo allo zero lo rende praticamente irrilevante.
Il bias invece svolge il ruolo di traslatore sommandosi al prodotto tra input e peso, sposta il risultato complessivo verso l’alto o verso il basso prima che venga applicata la funzione di attivazione. Pertanto se i pesi, come abbiamo visto, rappresentano l’inclinazione di una retta, il bias ne rappresenta l’intercetta sull’ordinata, consentendo alla rete di modellare funzioni che non passano necessariamente per l’origine. In pratica il bias permette al neurone di accendersi anche quando tutti gli input sono nulli, introducendo quella flessibilità che rende l’insieme dei modelli neurali un vero e proprio approssimatore.
Durante l’addestramento i pesi e i bias vengono aggiornati con l’algoritmo di back-propagation: il gradiente della funzione di errore indica in quale direzione e di quanto variare ciascun parametro per ridurre lo scarto tra la previsione della rete e il valore reale. Iterazione dopo iterazione, la rete regola questi due tipi di parametri in modo coordinato, affinando sia la pendenza sia la posizione delle sue curve decisionali, fino a catturare la complessità del fenomeno che vogliamo modellare.
In sintesi peso e bias sono i mattoni elementari dell’intelligenza adattiva della rete: il primo controllo l’importanza relativa degli input, il secondo offre la libertà di muoversi nello spazio delle soluzioni senza vincoli geometrici predefiniti.
Ci si potrebbe chiedere se abbia senso costruire una rete neurale in un linguaggio tradizionalmente considerato da backend web. La risposta è sì, per alcune situazioni ben definite. Innanzitutto l’implementazione nativa evita di introdurre un runtime aggiuntivo, tipicamente Python, e dunque semplifica il ciclo di build, test e deploy quando l’intero stack applicativo è già in PHP. Inoltre, per microservizi che richiedono modelli leggeri e inferenze dell’ordine di pochi millisecondi, una soluzione self-contained risulta più che adeguata. Inoltre c’è il caro vecchio aspetto didattico non va sottovalutato: scrivere la rete riga per riga smonta l’aura di “scatola nera” che accompagni molti framework di deep learning e mette gli sviluppatori in condizione di comprendere, ottimizzare e soprattutto debuggare ogni passaggio del calcolo così da avere una panoramica puntuale sul funzionamento di una rete neurale comprendendone a pieno il funzionamento.
Per proseguire occorre definire l’ossatura di una rete neurale “bare-metal” che possiamo realizzare con il solo motore di PHP, senza dipendere da estensioni C o librerie esterne. In pratica si tratta di modellare, con strutture dati native, tre elementi fondamentali:
Ogni livello sarà rappresentato da un semplice array bidimensionale di pesi ($weights
) e da un array monodimensionale di bias ($biases
). Il passaggio di attivazioni fra un layer e l’altro avverrà tramite la classica moltiplicazione matrice-vettore seguita dall’applicazione di una funzione di attivazione (sigmoide, ReLU o tanh a seconda dei casi d’uso). Questo schema minimalista ha il pregio di rimanere leggibile e di faborire il debug passo passo, ma impone alcune scelte progettuali: niente parallelizzazione automatica, niente ottimizazioni SIMD, ed estrema attenzione alla complessità computazionale poiché l’uso spregiudicato di cicli foreach nidificati in PHP può farci impennare i tempi di inferenza.
Nonostante ciò, per reti a una o due hidden layer e un numero di neuroni nell’ordine delle centinaia, le performance rimangono sorprendentemente dignitose se si sfruttano gli op-cache e si evitano allocazioni ridondanti. In sostanza, prima di immergerci nel codice vero e proprio, è importante comprendere che in PHP i neuroni non sono altro che righe di array e i gradienti sono valori float aggiornati dentro un ciclo. La semplicità di implementazione rende facilmente comprensibile l’aritmetica delle rete, rendendo evidente ogni singolo passo dell’apprendimento.
A questo punto dell’articolo è opportuno presentare nei dettagli l’implementazione bare-metal di rete neurale feed-forward a due strati scritta interamente in PHP 8.1. Il codice che segue mantiene la massima trasparenza possibile: ogni operazione matematica è esplicitata mediante semplici cicli for, ogni variabile intermedia è memorizzata in modo da poter essere ispezionata durante il debug e gli unici prerequisiti sono il motore PHP e l’opcache abilitato in produzione.
Per rendere il progetto facilmente riutilizzabile ho suddiviso il codice in due file distinti. Il primo, NeuralNetwork.php, contiene tutta la logica di rete neurale, completa di classi, funzioni di attivazione, forward-pass, back-propagation e routine di training. Il secondo, demo_xor.php, è un semplice script d’esecuzione che importa la classe, istanzia la rete, l’addestra sul classico problema XOR e stampa a video i risultati.
<?php
declare(strict_types=1);
/**
* Minimal feed-forward neural network in pure PHP
* MIT License – (c) 2025
*/
/* ---------- Activation functions ---------- */
/** Sigmoid activation */
function sigmoid(float $x): float
{
return 1.0 / (1.0 + exp(-$x));
}
/** Derivative of the sigmoid */
function sigmoid_derivative(float $x): float
{
$s = sigmoid($x);
return $s * (1 - $s);
}
/** ReLU activation */
function relu(float $x): float
{
return max(0.0, $x);
}
/** Derivative of ReLU */
function relu_derivative(float $x): float
{
return $x > 0 ? 1.0 : 0.0;
}
/* ---------- Layer class ---------- */
final class Layer
{
public readonly int $in; // number of input neurons
public readonly int $out; // number of output neurons
/** @var float[][] weight matrix [out][in] */
public array $W = [];
/** @var float[] bias vector [out] */
public array $b = [];
private array $lastInput = [];
private array $lastZ = [];
private array $lastOutput = [];
public function __construct(
int $in,
int $out,
callable $act,
callable $actDer
) {
$this->in = $in;
$this->out = $out;
$this->activation = $act;
$this->activation_d = $actDer;
// Xavier/Glorot uniform initialization
$limit = sqrt(6 / ($in + $out));
for ($i = 0; $i < $out; $i++) {
$this->b[$i] = 0.0;
for ($j = 0; $j < $in; $j++) {
$this->W[$i][$j] = (mt_rand() / mt_getrandmax()) * 2 * $limit - $limit;
}
}
}
/** Forward propagation */
public function forward(array $input): array
{
$this->lastInput = $input;
$this->lastZ = [];
$this->lastOutput = [];
for ($i = 0; $i < $this->out; $i++) {
$z = $this->b[$i];
for ($j = 0; $j < $this->in; $j++) {
$z += $this->W[$i][$j] * $input[$j];
}
$this->lastZ[$i] = $z;
$this->lastOutput[$i] = ($this->activation)($z);
}
return $this->lastOutput;
}
/** Back-propagation, returns gradient for previous layer */
public function backward(array $gradOutput, float $lr): array
{
$gradInput = array_fill(0, $this->in, 0.0);
for ($i = 0; $i < $this->out; $i++) {
$delta = $gradOutput[$i] * ($this->activation_d)($this->lastZ[$i]);
// Propagate gradient and update weights
for ($j = 0; $j < $this->in; $j++) {
$gradInput[$j] += $delta * $this->W[$i][$j];
$this->W[$i][$j] -= $lr * $delta * $this->lastInput[$j];
}
// Update bias
$this->b[$i] -= $lr * $delta;
}
return $gradInput;
}
/* callable */ private $activation;
/* callable */ private $activation_d;
}
/* ---------- NeuralNetwork class ---------- */
final class NeuralNetwork
{
/** @var Layer[] */
private array $layers = [];
/** Add a layer to the network */
public function addLayer(Layer $layer): void
{
$this->layers[] = $layer;
}
/** Forward pass through all layers */
public function predict(array $x): array
{
$out = $x;
foreach ($this->layers as $layer) {
$out = $layer->forward($out);
}
return $out;
}
/**
* Train the network with SGD and mean squared error
* @param float[][] $xTrain
* @param float[][] $yTrain
*/
public function train(
array $xTrain,
array $yTrain,
int $epochs = 1000,
float $lr = 0.1,
bool $verbose = true,
int $logStride = 100
): void {
$n = count($xTrain);
for ($e = 1; $e <= $epochs; $e++) {
$loss = 0.0;
for ($k = 0; $k < $n; $k++) {
$out = $this->predict($xTrain[$k]);
// MSE derivative: 2*(ŷ - y)
$grad = [];
foreach ($out as $i => $o) {
$diff = $o - $yTrain[$k][$i];
$grad[$i] = 2 * $diff;
$loss += $diff ** 2;
}
// Backward pass
for ($l = count($this->layers) - 1; $l >= 0; $l--) {
$grad = $this->layers[$l]->backward($grad, $lr);
}
}
if ($verbose && $e % $logStride === 0) {
printf("Epoch %d/%d - loss: %.6f\n", $e, $epochs, $loss / $n);
}
}
}
}
In questo file troviamo le funzioni di attivazione, sigmoide e ReLU, assieme ai loro gradienti. Averle a livello globale e non incapsulate nella classe, rende più lieve l’over-head di chiamate e permette di passarle come callable direttamente al costruttore dei laure, mantenendo flessibilità senza sacrificare performance.
La classe Layer è dichiarata final per evitare estensioni indesiderate e rappresenta l’unità logica di calcolo. Possiede gli interi in e out marcati readonly, così la loro integrità è garantita per l’intero ciclo di vita dell’oggetto. La matrice dei pesi e il vettore dei bias sono inizializzati secondo Xavier, tecnica che distribuisce i valori su un intervallo proporzionato alla radice del numero totale di connessioni in ingresso e in uscita; questo artificio matematico impedisce che le attivazioni si saturino già alle prime epoche, fenomeno che comprometterebbe l’apprendimento.
dove indica l’uniforme continua tra a e b; il termine sotto radice,
, funge da limite superiore e inferiore dell’intervallo di campionamento. In alternativa si può usare una distribuzione gaussiana a media zero con varianza
, ma l’espressione qui sopra, adottata nell’esempio di codice, è la forma uniforme originaria proposta da Glorot e Bengio.
Nella propagazione in avanti ogni neurone somma prodotto scalare e bias, poi applica la funzione di attivazione scelta; i risultati intermeti lastInput, lastZ e lastOutput vengono memorizzati per essere riutilizzati in retro-propagazione, così il calcolo del gradiente può procedere senza riconteggiare nulla, ideale per il debug passo-passo. Il metodo backward riceve il gradiente dell’errore dal layer successivo, lo combina con la derivata locale dell’attivazione e aggiorna persi e bias sottraendo una frazione proporzionale al learning rate, restituendo nel contempo il gradiente da inviare allo strato precedente.
Il ciclo interno è interamente manuale, scelta che evidenzia la matematica sottostante e rende il codice perfettamente trasparente anche a chi non ha mai usato librerie specialistiche.
La classe NeuralNetwork, anch’essa final, funge da orchestratore: mantiene l’array di layer e fornisce il metodo predict, che incanala un vettore di input attraverso ciascuno strato. Il metodo train implementa la discesa del gradiente stocastico con errore quadratico medio; scorre sull’insieme di addestramento per un numero di epoche specificato, calcola per ogni esempio la differenza fra output previsto e reale, raddoppia il valore e propaga il gradiente a ritroso aggiornando i layer in ordine inverso. A ogni iterazione somma la perdita per offrire un indicatore globale che, se il flag verbose è attivo, viene stampato a intervalli regolari così da monitorare in tempo reale la convergenza del modello.
Il costruttore del layer accetta callable, quindi, qualora in futuro si volessero usare funzioni di attivazione diverse, basterebbe passarne i riferimenti senza toccare l’architettura.
<?php
declare(strict_types=1);
// Include the neural network implementation
require_once __DIR__ . '/NeuralNetwork.php';
// Training data for XOR
$xTrain = [
[0, 0],
[0, 1],
[1, 0],
[1, 1],
];
$yTrain = [
[0],
[1],
[1],
[0],
];
// Build the network: 2-3-1 with sigmoid activations
$net = new NeuralNetwork();
$net->addLayer(new Layer(2, 3, 'sigmoid', 'sigmoid_derivative'));
$net->addLayer(new Layer(3, 1, 'sigmoid', 'sigmoid_derivative'));
// Train the network
$net->train($xTrain, $yTrain, epochs: 5000, lr: 0.5, logStride: 500);
// Test predictions
foreach ($xTrain as $sample) {
$out = $net->predict($sample)[0];
printf("Input %s ⇒ Output %.4f\n", json_encode($sample), $out);
}
In questo frammento funzionale, vengono dichiarati i dataset di addestramento e di verità attesa per il classico problema XOR: quattro coppie di input binari, ciascuna accompagnata dal relativo output, che consentono di verificare la capacità del modello di apprendere una funzione non lineare.
Il cuore logico comincia con l’istanziamento dell’oggetto NeuralNetwork. Viene quindi costruita una topologia a due livelli nascosti: il primo strato accetta le due feature in ingresso e le proietta su tre neuroni di uscita, il secondo riceve quelle tre attivazioni intermedie e restituisce un unico valore scalare. In entrambi i passaggi si impiega la funzione di attivazione sigmoide, scelta per la sua semplciità didattica e per la facilità con cui il suo gradiente viene calcolato nella fase di retro-propagazione.
Riportiamo anche la funzione sigmoide:
dove è la costante di Nepero e
rappresenta il valore reale in ingresso al neurone. Questa espressione garantisce un output continuo compreso tra 0 e 1, con un punto di flesso nell’origine che ne determina l’andamento a “S” caratteristico: per valori negativi molto grandi la funzione tende asintoticamente a 0, mentre per valori positivi molto grandi si avvicina a 1. In ambito di apprendimento automatico si ricorre spesso anche alla derivata della sigmoide, utile nella retro-propagazione, la cui forma compatta è
Quest’ultima relazione discende direttamente dalla definizione originaria e consente di calcolare il gradiente senza dover ricorrere ad altre funzioni esponenziali, ottimizzando la fase di aggiornamento dei pesi.
Proseguendo, il metodo train avvia l’allenamento vero e proprio, iterando cinquemila epoche sullo stesso piccolo set di esempi. Con un tasso di apprendimento fissato a 0.5 e un log della perdita ogni cinquecento iterazioni, il ciclo compie la discesa del gradiente sul valore medio dell’errore quadratico, aggiornando pesi e bias di entrambi i layer a ogni osservazione. Al termine dell’addestramneto, un semplice ciclo foreach scorre di nuovo sui quattro pattenr di ingresso, li passa al metodo predict e stampa a schermo le uscite numeriche della rete, permettendo di confrontarle con gli output attesi e di valutarne immediatamente la bontà del modello. In un contesto di produzione, lo stesso schema potrebbe essere incapsulato in un endpoint REST o in un comando da linea di comando, ma in questa forma minimalista offre già una dimostrazione completa di come PHP possa gestire l’intero ciclo di vita di una mini rete neurale, dalla definizione dei layer fino alla previsione finale.
L’esperimento dimostra che, nonostante PHP non sia nato per il calcolo numerico, è possibile implementare una rete neurale essenziale ma funzionante, addestrarla in tempi ragionevoli su problemi di piccola taglia e distribuirla in produzione senza introdurre un runtime aggiuntivo.
L’inizializzazione Xavier preserva la stabilità del segnale fin dalle prime epoche, la sigmoide garantisce un gradiente ben definito e l’approccio completamente trasparente, primo di blackbox, rende il modello un ottimo strumento didattico: ogni peso, ogni bias e ogni step di retro-propagazione sono sotto il pieno controllo. È chiaro che questa soluzione non intende neanche avvicinarsi a competere con framework ottimizzati su GPU, ma quando l’obiettivo è integrare inferenze leggere in uno stack già scritto in PHP, o semplicemente ocmprendere intimamente come funziona una rete neurale, l’implementazione presentata offre una via elegante e facile.
In conclusione, l’aspetto più interessante di questo articolo non voleva essere costruire un nuovo ChatGPT ma dare consapevolezza ed apprendere i principi matematici, riga dopo riga alla base della costruzione di una semplice rete neurale.
La vera potenza dell’intelligenza artificiale risiede secondo me nella comprensione dei principi scientifici su cui si basa e non tanto sui linguaggi che utilizziamo per codificarla.