476 lines
14 KiB
PHP
476 lines
14 KiB
PHP
<?php
|
|
|
|
namespace neuralnetwork\core;
|
|
|
|
use filemanager\core\FileManager;
|
|
|
|
class Genome implements \Serializable{
|
|
|
|
/************************************************
|
|
**** Constants ****
|
|
************************************************/
|
|
const MIN = -1e9;
|
|
const MAX = 1e9;
|
|
|
|
/************************************************
|
|
**** LOCAL ATTRIBUTES ****
|
|
************************************************/
|
|
private $layers; // Number of hidden layers
|
|
|
|
private $inputN; // Number of input neurons
|
|
private $neurons; // Number of neurons for each hidden layer
|
|
private $outputN; // Number of output neurons
|
|
|
|
private $synapses; // Synapses between neurons
|
|
|
|
private $callback; // callback training function
|
|
|
|
private $fitness; // Genome's fitness
|
|
|
|
|
|
/************************************************
|
|
**** Genome Construction ****
|
|
************************************************/
|
|
|
|
/* CONSTRUCTOR
|
|
*
|
|
* -- RANDOM CREATION --
|
|
* @layers<int> Number of hidden layers to manage
|
|
* @neurons<int> Number of neurons per hidden layer
|
|
* @inN<int> Number of input neurons
|
|
* @outN<int> Number of output neurons
|
|
*
|
|
* -- CLONING --
|
|
* @base<Genom> Genome to clone to
|
|
*
|
|
* -- CROSS-OVER CREATION --
|
|
* @father<Genome> First parent
|
|
* @mother<Genome> Second parent
|
|
*
|
|
*
|
|
*/
|
|
public function __construct(){
|
|
/* (1) Get arguments */
|
|
$argv = func_get_args();
|
|
$argc = count($argv);
|
|
|
|
/* (2) If RandomCreation */
|
|
if( $argc > 3 && is_numeric($argv[0]) && is_numeric($argv[1]) && is_numeric($argv[2]) && is_numeric($argv[3]) )
|
|
$this->construct_new($argv[0], $argv[1], $argv[2], $argv[3]);
|
|
|
|
/* (3) If CrossoverCreation */
|
|
else if( $argc > 1 && $argv[0] instanceof Genome && $argv[1] instanceof Genome )
|
|
$this->construct_crossover($argv[0], $argv[1]);
|
|
|
|
/* (4) If InheritanceCreation (clone) */
|
|
else if( $argc > 0 && $argv[0] instanceof Genome )
|
|
$this->construct_inheritance($argv[0]);
|
|
|
|
/* (5) If no match */
|
|
else
|
|
throw new \Exception('Invalid Genome constructor\'s arguments.');
|
|
|
|
/* (6) Default values */
|
|
$this->fitness = null;
|
|
$this->callback = function(){};
|
|
}
|
|
|
|
/* BUILDS A Genome RANDOMLY WITH PARAMETERS
|
|
*
|
|
* @layers<int> The number of hidden layers to manage
|
|
* @neurons<int> The number neurons per layer
|
|
* @inN<int> Number of input neurons
|
|
* @outN<int> Number of output neurons
|
|
*
|
|
* @return created<Boolean> If Genome has been successfully created
|
|
*
|
|
*/
|
|
private function construct_new($layers, $neurons, $inN, $outN){
|
|
/* (1) Checks parameters */
|
|
if( abs(intval($layers)) !== $layers || abs(intval($neurons)) !== $neurons || abs(intval($inN)) !== $inN || abs(intval($outN)) !== $outN )
|
|
return false;
|
|
|
|
// set layers
|
|
$this->layers = $layers;
|
|
|
|
/* (2) Store number of neurons */
|
|
$this->neurons = $neurons;
|
|
$this->inputN = $inN;
|
|
$this->outputN = $outN;
|
|
|
|
/* (3) Creating random synapses */
|
|
$this->synapses = [];
|
|
for( $i = 0, $l = $neurons*($inN+$outN+$neurons*$layers) ; $i < $l ; $i++ )
|
|
$this->synapses[$i] = rand(self::MIN, self::MAX) / self::MAX;
|
|
|
|
// Success status
|
|
return true;
|
|
}
|
|
|
|
/* BUILDS A Genome BASED ON A PARENT
|
|
*
|
|
* @parent<Genome> Parent genome to clone into children
|
|
*
|
|
* @return created<Boolean> If cloned Genome has been created successfully
|
|
*
|
|
*/
|
|
private function construct_inheritance($parent=null){
|
|
/* (1) Checks parent type */
|
|
if( !($parent instanceof Genome) )
|
|
return false;
|
|
|
|
/* (2) Clones into this Genome */
|
|
$this->layers = $parent->layers;
|
|
$this->neurons = $parent->neurons;
|
|
$this->synapses = array_slice($parent->synapses, 0);
|
|
|
|
// Success state
|
|
return true;
|
|
}
|
|
|
|
/* BUILDS A Genome BASED ON TWO PARENTS
|
|
*
|
|
* @father<Genome> First parent ($father)
|
|
* @mother<Genome> Second parent ($mother)
|
|
*
|
|
* @return created<Boolean> If crossed-over Genome has been created successfully
|
|
*
|
|
*/
|
|
private function construct_crossover($father=null, $mother=null){
|
|
/* (1) Checks parent type */
|
|
if( !($father instanceof Genome) || !($mother instanceof Genome) )
|
|
return false;
|
|
|
|
/* (2) Checks the attributes (same species) */
|
|
$diffAttr = $father->layers !== $mother->layers;
|
|
$diffAttr = $diffAttr || $father->inputN !== $mother->inputN;
|
|
$diffAttr = $diffAttr || $father->neurons !== $mother->neurons;
|
|
$diffAttr = $diffAttr || $father->outputN !== $mother->outputN;
|
|
|
|
if( $diffAttr )
|
|
return false;
|
|
|
|
/* (3) Set layer count */
|
|
$this->layers = $father->layers;
|
|
|
|
/* (4) Set neurons number */
|
|
$this->inputN = $father->inputN;
|
|
$this->neurons = $father->neurons;
|
|
$this->outputN = $father->outputN;
|
|
|
|
/* (5) Do random crossover for synapses */
|
|
$this->synapses = [];
|
|
for( $i = 0, $l = count($mother->synapses) ; $i < $l ; $i++ )
|
|
if( !!rand(0,1) ) $this->synapses[$i] = $father->synapses[$i];
|
|
else $this->synapses[$i] = $mother->synapses[$i];
|
|
|
|
// Success state
|
|
return true;
|
|
}
|
|
|
|
|
|
/************************************************
|
|
**** Setters ****
|
|
************************************************/
|
|
|
|
/* SETS THE CALLBACK FUNCTION
|
|
*
|
|
* @callback<Function> Callback function to train with
|
|
*
|
|
*/
|
|
public function setCallback($callback=null){
|
|
/* (1) Checks @callback argument */
|
|
if( !is_callable($callback) )
|
|
throw new \Exception('Wrong argument for Genome\'s callback function.');
|
|
|
|
/* (2) Set callback function */
|
|
$this->callback = $callback;
|
|
}
|
|
|
|
/* SETS THE FITNESS OF THE GENOME
|
|
*
|
|
* @fitness<double> Calculated fitness of the genome
|
|
*
|
|
*/
|
|
public function setFitness($fitness=null){
|
|
/* (1) Checks @fitness argument */
|
|
if( !is_numeric($fitness) )
|
|
throw new \Exception('Wrong argument for specifying Genome\'s fitness.');
|
|
|
|
/* (2) Set fitness */
|
|
$this->fitness = floatval($fitness);
|
|
}
|
|
|
|
|
|
/************************************************
|
|
**** Getter ****
|
|
************************************************/
|
|
|
|
/* RETURNS THE FITNESS OF THE GENOME
|
|
*
|
|
* @return fitness<double> Current fitness (or NULL if not defined)
|
|
*
|
|
*/
|
|
public function getFitness(){
|
|
return $this->fitness;
|
|
}
|
|
|
|
|
|
/************************************************
|
|
**** Genome Actions ****
|
|
************************************************/
|
|
|
|
/* APPLIES A MUTATION ON THE Genome WITH A SPECIFIC @threshold
|
|
*
|
|
* @threshold<double> Mutation threshold
|
|
*
|
|
*/
|
|
public function mutation($threshold=1){
|
|
/* (1) Checks @threshold argument */
|
|
if( floatval($threshold) !== $threshold || $threshold < 0 || $threshold > 1 )
|
|
throw new \Exception('Invalid threshold for Genome mutation.');
|
|
|
|
/* (2) Calculates how many neurons/synapses to mutate */
|
|
$synapsesMutations = round( (count($this->synapses) - 1) * rand(0, $threshold*self::MAX)/self::MAX );
|
|
|
|
/* (3) Choose random synapses' indexes */
|
|
$iSynapses = [];
|
|
while( count($iSynapses) < $synapsesMutations ){
|
|
$r = rand(0, count($this->synapses)-1);
|
|
|
|
if( !in_array($r, $iSynapses) )
|
|
$iSynapses[] = $r;
|
|
}
|
|
|
|
/* (4) Update chosen synapses */
|
|
for( $i = 0, $l = count($iSynapses) ; $i < $l ; $i++ )
|
|
$this->synapses[$iSynapses[$i]] = rand(self::MIN, self::MAX) / self::MAX;
|
|
}
|
|
|
|
/* CALCULATES THE OUTPUT OF THE GENOME
|
|
*
|
|
* @input<Array> Input for which we want fitness
|
|
* @callback<Function> [OPTIONAL] Callback function
|
|
*
|
|
* @note: will call @callback function with INPUTS as 1st argument and OUTPUTS as 2nd arguments
|
|
*
|
|
*/
|
|
public function train($input=null, $callback=null){
|
|
/* (1) Checks @input argument */
|
|
if( !is_array($input) || count($input) !== $this->inputN )
|
|
throw new \Exception('Invalid @input for Genome\'s training.');
|
|
|
|
/* (2) Checks optional @callback argument */
|
|
if( is_callable($callback) )
|
|
$this->setCallback($callback);
|
|
|
|
|
|
/* [1] Set temporary calculation data
|
|
=========================================================*/
|
|
/* (1) Set temporary neurons data */
|
|
$neurons = array_merge( $input, array_fill(0, $this->neurons*$this->layers, 0), array_fill(0, $this->outputN, 0) );
|
|
|
|
/* (2) Set temporary synapses data */
|
|
$synapses = $this->synapses;
|
|
|
|
|
|
/* [2] Calculates output
|
|
=========================================================*/
|
|
/* (1) For each hidden layer
|
|
---------------------------------------------------------*/
|
|
foreach($this->layersIterator() as $lr=>$l){
|
|
|
|
/* (2) For each neuron of this layer
|
|
---------------------------------------------------------*/
|
|
foreach($this->neuronsIterator($l) as $nr=>$n){
|
|
|
|
$neurons[$n] = 0;
|
|
|
|
/* (3) For each synapse between current neuron and last layer
|
|
---------------------------------------------------------*/
|
|
foreach($this->synapsesIterator($l, $nr) as $sr=>$s){
|
|
// echo "current: n#$nr/l#$l = s#$s * n#$sr\n";
|
|
// echo "calc: ${neurons[$n]} += ${synapses[$s]} * ${neurons[$sr]}\n";
|
|
$neurons[$n] += $synapses[$s] * $neurons[$sr];
|
|
// echo "result: ${neurons[$n]}\n\n";
|
|
}
|
|
|
|
// if( $l == 1 ) $neurons[$n] /= $this->inputN;
|
|
// else $neurons[$n] /= $this->neurons;
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
/* [3] Callback the output layer's values
|
|
=========================================================*/
|
|
call_user_func($this->callback, $input, array_slice($neurons, -$this->outputN) );
|
|
}
|
|
|
|
|
|
/************************************************
|
|
**** Utilities ****
|
|
************************************************/
|
|
|
|
/* RETURNS ITERATOR THROUGH EACH LAYER
|
|
*
|
|
* @return indexes<yield> Iterator through each layer (relative->absolute)
|
|
*
|
|
*/
|
|
private function layersIterator(){
|
|
for( $l = 1 ; $l < $this->layers+2 ; $l++ )
|
|
yield $l => $l;
|
|
}
|
|
|
|
/* RETURNS AN ITERATOR ON THE NEURONS OF A LAYER
|
|
*
|
|
* @layer<int> Layer to browse
|
|
*
|
|
* @return indexes<yield> Iterator through the neurons indexes of the layer (relative->absolute)
|
|
*
|
|
*/
|
|
private function neuronsIterator($layer){
|
|
/* (1) Formats @layer argument */
|
|
if( $layer < 0 ) $layer = 0;
|
|
if( $layer > $this->layers+1 ) $layer = $this->layers+1;
|
|
|
|
$layer = intval($layer);
|
|
|
|
/* (2) If between input/hidden */
|
|
if( $layer === 0 ){
|
|
$offset = 0;
|
|
for( $n = $offset, $nl = $this->inputN ; $n < $nl ; $n++ )
|
|
yield $n-$offset => $n;
|
|
|
|
/* (3) If between hidden/output */
|
|
}else if( $layer == $this->layers+1 ){
|
|
$offset = $this->inputN + $this->neurons * $this->layers;
|
|
for( $n = $offset, $nl = $this->inputN + $this->neurons * $this->layers + $this->outputN ; $n < $nl ; $n++ )
|
|
yield $n-$offset => $n;
|
|
|
|
/* (2) If in between hidden layer */
|
|
}else{
|
|
$offset = $this->inputN + $this->neurons * ($layer-1);
|
|
for( $n = $offset, $nl = $this->inputN+$this->neurons*$layer ; $n < $nl ; $n++ )
|
|
yield $n-$offset => $n;
|
|
}
|
|
}
|
|
|
|
/* RETURNS AN ITERATOR ON THE SYNAPSES BETWEEN A NEURON AND ITS PREVIOUS LAYER'S NEURONS
|
|
*
|
|
* @layer<int> Destination layer
|
|
* @neuron<int> Destination neuron
|
|
*
|
|
* @return indexes<yield> Iterator through the synapses indexes between the destination neuron and the source layer's neurons (neuronIndex->SynapseIndex)
|
|
*
|
|
*/
|
|
private function synapsesIterator($layer, $neuron){
|
|
/* (1) Formats @layer argument */
|
|
if( $layer < 1 ) $layer = 1;
|
|
if( $layer > $this->layers+1 ) $layer = $this->layers+1;
|
|
|
|
$layer = intval($layer);
|
|
$prev = $layer-1;
|
|
|
|
/* (2) If between input/hidden */
|
|
if( $layer === 1 ){
|
|
$offset = 0;
|
|
for( $s = $offset+$neuron, $sl = $this->inputN*$this->neurons ; $s < $sl ; $s += $this->neurons )
|
|
yield ($s-$offset-$neuron)/$this->neurons => $s;
|
|
|
|
|
|
/* (3) If between hidden/output */
|
|
}else if( $layer == $this->layers+1 ){
|
|
$offset = $this->neurons * ($this->inputN+$this->neurons*($this->layers-1));
|
|
for( $s = $offset+$neuron, $sl = $offset+$this->neurons*$this->outputN ; $s < $sl ; $s += $this->outputN )
|
|
yield $this->inputN+$this->neurons*($this->layers-1) + ($s-$offset-$neuron)/$this->outputN => $s;
|
|
|
|
/* (2) If in between hidden layer */
|
|
}else{
|
|
$offset = $this->neurons * ($this->inputN+$this->neurons*($prev-1));
|
|
for( $s = $offset+$neuron, $sl = $offset+$this->neurons*$this->neurons ; $s < $sl ; $s += $this->neurons )
|
|
yield $this->inputN+$this->neurons*($prev-1) + ($s-$offset-$neuron)/$this->neurons => $s;
|
|
}
|
|
}
|
|
|
|
|
|
/************************************************
|
|
**** Serialization ****
|
|
************************************************/
|
|
|
|
/* SERIALIZES A Genome
|
|
*
|
|
* @return serialized<String> Serialized representation of the Genome
|
|
*
|
|
*/
|
|
public function serialize(){
|
|
/* (1) Initialize result */
|
|
$csv = '';
|
|
|
|
/* (2) Adds global attributes */
|
|
$csv .= $this->layers .','. $this->inputN .','. $this->neurons .','. $this->outputN .';';
|
|
|
|
/* (3) Adds synapses data */
|
|
$csv .= implode(',', $this->synapses);
|
|
|
|
return $csv;
|
|
}
|
|
|
|
/* BUILDS A Genome BASED ON HIS SERIALIZED REPRESENTATION
|
|
*
|
|
* @serialized<String> Serialized representation of a Genome
|
|
*
|
|
*/
|
|
public function unserialize($serialized){
|
|
/* (1) Segmenting data */
|
|
$segments = explode(';', trim($serialized) );
|
|
|
|
// Manage segmentation error
|
|
if( count($segments) < 2 )
|
|
throw new \Exception('Format error during Genome unserialization.');
|
|
|
|
/* (2) Get global attributes */
|
|
$globals = explode(',', $segments[0]);
|
|
|
|
if( count($globals) < 4 )
|
|
throw new \Exception('Format error during Genome unserialization.');
|
|
|
|
$this->layers = intval($globals[0]);
|
|
$this->inputN = intval($globals[1]);
|
|
$this->neurons = intval($globals[2]);
|
|
$this->outputN = intval($globals[3]);
|
|
|
|
/* (3) Get synapses values */
|
|
$this->synapses = explode(',', $segments[1]);
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/************************************************
|
|
**** USE CASE ****
|
|
************************************************/
|
|
$use_case = false;
|
|
if( $use_case ){
|
|
|
|
/* (1) Basic Creation */
|
|
$a = new Genome(2, 3, 3, 2); // 2 layers of 3 neurons each -> randomly filled + 3 input neurons + 2 output neurons
|
|
|
|
/* (2) Inheritance */
|
|
$b = new Genome($a); // Clone of @a
|
|
|
|
/* (3) Section Title */
|
|
$b->mutation(0.3); // @b has now mutated with a threshold of 30%
|
|
|
|
/* (4) Cross-over (father+mother) */
|
|
$c = new Genome($a, $b); // @c is a randomly-done mix of @a and @b
|
|
|
|
}
|
|
|
|
?>
|