mirror of
https://github.com/Llewellynvdm/php-ml.git
synced 2025-01-10 00:37:55 +00:00
refactor kmeans subclasses
This commit is contained in:
parent
c0513e9b82
commit
7572304d50
@ -1,5 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types = 1);
|
|
||||||
|
declare (strict_types = 1);
|
||||||
|
|
||||||
namespace Phpml\Clustering;
|
namespace Phpml\Clustering;
|
||||||
|
|
||||||
@ -8,23 +9,33 @@ use Phpml\Exception\InvalidArgumentException;
|
|||||||
|
|
||||||
class KMeans implements Clusterer
|
class KMeans implements Clusterer
|
||||||
{
|
{
|
||||||
|
const INIT_RANDOM = 1;
|
||||||
|
const INIT_KMEANS_PLUS_PLUS = 2;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var int
|
* @var int
|
||||||
*/
|
*/
|
||||||
private $clustersNumber;
|
private $clustersNumber;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private $initialization;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param int $clustersNumber
|
* @param int $clustersNumber
|
||||||
|
* @param int $initialization
|
||||||
*
|
*
|
||||||
* @throws InvalidArgumentException
|
* @throws InvalidArgumentException
|
||||||
*/
|
*/
|
||||||
public function __construct(int $clustersNumber)
|
public function __construct(int $clustersNumber, int $initialization = self::INIT_KMEANS_PLUS_PLUS)
|
||||||
{
|
{
|
||||||
if($clustersNumber <= 0) {
|
if ($clustersNumber <= 0) {
|
||||||
throw InvalidArgumentException::invalidClustersNumber();
|
throw InvalidArgumentException::invalidClustersNumber();
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->clustersNumber = $clustersNumber;
|
$this->clustersNumber = $clustersNumber;
|
||||||
|
$this->initialization = $initialization;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -40,12 +51,10 @@ class KMeans implements Clusterer
|
|||||||
}
|
}
|
||||||
|
|
||||||
$clusters = [];
|
$clusters = [];
|
||||||
foreach ($space->solve($this->clustersNumber) as $cluster)
|
foreach ($space->solve($this->clustersNumber, $this->initialization) as $cluster) {
|
||||||
{
|
|
||||||
$clusters[] = $cluster->getPoints();
|
$clusters[] = $cluster->getPoints();
|
||||||
}
|
}
|
||||||
|
|
||||||
return $clusters;
|
return $clusters;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,101 +1,137 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
declare(strict_types = 1);
|
declare (strict_types = 1);
|
||||||
|
|
||||||
namespace Phpml\Clustering\KMeans;
|
namespace Phpml\Clustering\KMeans;
|
||||||
|
|
||||||
use \IteratorAggregate;
|
use IteratorAggregate;
|
||||||
use \Countable;
|
use Countable;
|
||||||
use \SplObjectStorage;
|
use SplObjectStorage;
|
||||||
use \LogicException;
|
use LogicException;
|
||||||
|
|
||||||
class Cluster extends Point implements IteratorAggregate, Countable
|
class Cluster extends Point implements IteratorAggregate, Countable
|
||||||
{
|
{
|
||||||
protected $space;
|
/**
|
||||||
|
* @var Space
|
||||||
|
*/
|
||||||
|
protected $space;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var SplObjectStorage|Point[]
|
* @var SplObjectStorage|Point[]
|
||||||
*/
|
*/
|
||||||
protected $points;
|
protected $points;
|
||||||
|
|
||||||
public function __construct(Space $space, array $coordinates)
|
/**
|
||||||
{
|
* @param Space $space
|
||||||
parent::__construct($space, $coordinates);
|
* @param array $coordinates
|
||||||
$this->points = new SplObjectStorage;
|
*/
|
||||||
}
|
public function __construct(Space $space, array $coordinates)
|
||||||
|
{
|
||||||
|
parent::__construct($coordinates);
|
||||||
|
$this->space = $space;
|
||||||
|
$this->points = new SplObjectStorage();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function getPoints()
|
public function getPoints()
|
||||||
{
|
{
|
||||||
$points = [];
|
$points = [];
|
||||||
foreach ($this->points as $point) {
|
foreach ($this->points as $point) {
|
||||||
$points[] = $point->toArray();
|
$points[] = $point->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
return $points;
|
return $points;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function toArray()
|
/**
|
||||||
{
|
* @return array
|
||||||
$points = array();
|
*/
|
||||||
foreach ($this->points as $point)
|
public function toArray()
|
||||||
$points[] = $point->toArray();
|
{
|
||||||
|
return array(
|
||||||
|
'centroid' => parent::toArray(),
|
||||||
|
'points' => $this->getPoints(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return array(
|
/**
|
||||||
'centroid' => parent::toArray(),
|
* @param Point $point
|
||||||
'points' => $points,
|
*
|
||||||
);
|
* @return Point
|
||||||
}
|
*/
|
||||||
|
public function attach(Point $point)
|
||||||
|
{
|
||||||
|
if ($point instanceof self) {
|
||||||
|
throw new LogicException('cannot attach a cluster to another');
|
||||||
|
}
|
||||||
|
|
||||||
public function attach(Point $point)
|
$this->points->attach($point);
|
||||||
{
|
|
||||||
if ($point instanceof self)
|
|
||||||
throw new LogicException("cannot attach a cluster to another");
|
|
||||||
|
|
||||||
$this->points->attach($point);
|
return $point;
|
||||||
return $point;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public function detach(Point $point)
|
/**
|
||||||
{
|
* @param Point $point
|
||||||
$this->points->detach($point);
|
*
|
||||||
return $point;
|
* @return Point
|
||||||
}
|
*/
|
||||||
|
public function detach(Point $point)
|
||||||
|
{
|
||||||
|
$this->points->detach($point);
|
||||||
|
|
||||||
public function attachAll(SplObjectStorage $points)
|
return $point;
|
||||||
{
|
}
|
||||||
$this->points->addAll($points);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function detachAll(SplObjectStorage $points)
|
/**
|
||||||
{
|
* @param SplObjectStorage $points
|
||||||
$this->points->removeAll($points);
|
*/
|
||||||
}
|
public function attachAll(SplObjectStorage $points)
|
||||||
|
{
|
||||||
|
$this->points->addAll($points);
|
||||||
|
}
|
||||||
|
|
||||||
public function updateCentroid()
|
/**
|
||||||
{
|
* @param SplObjectStorage $points
|
||||||
if (!$count = count($this->points))
|
*/
|
||||||
return;
|
public function detachAll(SplObjectStorage $points)
|
||||||
|
{
|
||||||
|
$this->points->removeAll($points);
|
||||||
|
}
|
||||||
|
|
||||||
$centroid = $this->space->newPoint(array_fill(0, $this->dimention, 0));
|
public function updateCentroid()
|
||||||
|
{
|
||||||
|
if (!$count = count($this->points)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($this->points as $point)
|
$centroid = $this->space->newPoint(array_fill(0, $this->dimension, 0));
|
||||||
for ($n=0; $n<$this->dimention; $n++)
|
|
||||||
$centroid->coordinates[$n] += $point->coordinates[$n];
|
|
||||||
|
|
||||||
for ($n=0; $n<$this->dimention; $n++)
|
foreach ($this->points as $point) {
|
||||||
$this->coordinates[$n] = $centroid->coordinates[$n] / $count;
|
for ($n = 0; $n < $this->dimension; ++$n) {
|
||||||
}
|
$centroid->coordinates[$n] += $point->coordinates[$n];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function getIterator()
|
for ($n = 0; $n < $this->dimension; ++$n) {
|
||||||
{
|
$this->coordinates[$n] = $centroid->coordinates[$n] / $count;
|
||||||
return $this->points;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function count()
|
/**
|
||||||
{
|
* @return Point[]|SplObjectStorage
|
||||||
return count($this->points);
|
*/
|
||||||
}
|
public function getIterator()
|
||||||
|
{
|
||||||
|
return $this->points;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function count()
|
||||||
|
{
|
||||||
|
return count($this->points);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,95 +1,124 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types = 1);
|
|
||||||
|
declare (strict_types = 1);
|
||||||
|
|
||||||
namespace Phpml\Clustering\KMeans;
|
namespace Phpml\Clustering\KMeans;
|
||||||
|
|
||||||
use \ArrayAccess;
|
use ArrayAccess;
|
||||||
use \LogicException;
|
|
||||||
|
|
||||||
class Point implements ArrayAccess
|
class Point implements ArrayAccess
|
||||||
{
|
{
|
||||||
protected $space;
|
/**
|
||||||
protected $dimention;
|
* @var int
|
||||||
protected $coordinates;
|
*/
|
||||||
|
protected $dimension;
|
||||||
|
|
||||||
public function __construct(Space $space, array $coordinates)
|
/**
|
||||||
{
|
* @var array
|
||||||
$this->space = $space;
|
*/
|
||||||
$this->dimention = $space->getDimention();
|
protected $coordinates;
|
||||||
$this->coordinates = $coordinates;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function toArray()
|
/**
|
||||||
{
|
* @param array $coordinates
|
||||||
return $this->coordinates;
|
*/
|
||||||
}
|
public function __construct(array $coordinates)
|
||||||
|
{
|
||||||
|
$this->dimension = count($coordinates);
|
||||||
|
$this->coordinates = $coordinates;
|
||||||
|
}
|
||||||
|
|
||||||
public function getDistanceWith(self $point, $precise = true)
|
/**
|
||||||
{
|
* @return array
|
||||||
if ($point->space !== $this->space)
|
*/
|
||||||
throw new LogicException("can only calculate distances from points in the same space");
|
public function toArray()
|
||||||
|
{
|
||||||
|
return $this->coordinates;
|
||||||
|
}
|
||||||
|
|
||||||
$distance = 0;
|
/**
|
||||||
for ($n=0; $n<$this->dimention; $n++) {
|
* @param Point $point
|
||||||
$difference = $this->coordinates[$n] - $point->coordinates[$n];
|
* @param bool $precise
|
||||||
$distance += $difference * $difference;
|
*
|
||||||
}
|
* @return int|mixed
|
||||||
|
*/
|
||||||
|
public function getDistanceWith(self $point, $precise = true)
|
||||||
|
{
|
||||||
|
$distance = 0;
|
||||||
|
for ($n = 0; $n < $this->dimension; ++$n) {
|
||||||
|
$difference = $this->coordinates[$n] - $point->coordinates[$n];
|
||||||
|
$distance += $difference * $difference;
|
||||||
|
}
|
||||||
|
|
||||||
return $precise ? sqrt($distance) : $distance;
|
return $precise ? sqrt($distance) : $distance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getClosest($points)
|
/**
|
||||||
{
|
* @param $points
|
||||||
foreach($points as $point) {
|
*
|
||||||
$distance = $this->getDistanceWith($point, false);
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function getClosest($points)
|
||||||
|
{
|
||||||
|
foreach ($points as $point) {
|
||||||
|
$distance = $this->getDistanceWith($point, false);
|
||||||
|
|
||||||
if (!isset($minDistance)) {
|
if (!isset($minDistance)) {
|
||||||
$minDistance = $distance;
|
$minDistance = $distance;
|
||||||
$minPoint = $point;
|
$minPoint = $point;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($distance < $minDistance) {
|
if ($distance < $minDistance) {
|
||||||
$minDistance = $distance;
|
$minDistance = $distance;
|
||||||
$minPoint = $point;
|
$minPoint = $point;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $minPoint;
|
return $minPoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function belongsTo(Space $space)
|
/**
|
||||||
{
|
* @return array
|
||||||
return $this->space === $space;
|
*/
|
||||||
}
|
public function getCoordinates()
|
||||||
|
{
|
||||||
|
return $this->coordinates;
|
||||||
|
}
|
||||||
|
|
||||||
public function getSpace()
|
/**
|
||||||
{
|
* @param mixed $offset
|
||||||
return $this->space;
|
*
|
||||||
}
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function offsetExists($offset)
|
||||||
|
{
|
||||||
|
return isset($this->coordinates[$offset]);
|
||||||
|
}
|
||||||
|
|
||||||
public function getCoordinates()
|
/**
|
||||||
{
|
* @param mixed $offset
|
||||||
return $this->coordinates;
|
*
|
||||||
}
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function offsetGet($offset)
|
||||||
|
{
|
||||||
|
return $this->coordinates[$offset];
|
||||||
|
}
|
||||||
|
|
||||||
public function offsetExists($offset)
|
/**
|
||||||
{
|
* @param mixed $offset
|
||||||
return isset($this->coordinates[$offset]);
|
* @param mixed $value
|
||||||
}
|
*/
|
||||||
|
public function offsetSet($offset, $value)
|
||||||
|
{
|
||||||
|
$this->coordinates[$offset] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
public function offsetGet($offset)
|
/**
|
||||||
{
|
* @param mixed $offset
|
||||||
return $this->coordinates[$offset];
|
*/
|
||||||
}
|
public function offsetUnset($offset)
|
||||||
|
{
|
||||||
public function offsetSet($offset, $value)
|
unset($this->coordinates[$offset]);
|
||||||
{
|
}
|
||||||
$this->coordinates[$offset] = $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function offsetUnset($offset)
|
|
||||||
{
|
|
||||||
unset($this->coordinates[$offset]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,216 +1,271 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types = 1);
|
|
||||||
|
declare (strict_types = 1);
|
||||||
|
|
||||||
namespace Phpml\Clustering\KMeans;
|
namespace Phpml\Clustering\KMeans;
|
||||||
|
|
||||||
use \SplObjectStorage;
|
use Phpml\Clustering\KMeans;
|
||||||
use \LogicException;
|
use SplObjectStorage;
|
||||||
use \InvalidArgumentException;
|
use LogicException;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
|
||||||
class Space extends SplObjectStorage
|
class Space extends SplObjectStorage
|
||||||
{
|
{
|
||||||
// Default seeding method, initial cluster centroid are randomly choosen
|
/**
|
||||||
const SEED_DEFAULT = 1;
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $dimension;
|
||||||
|
|
||||||
// Alternative seeding method by David Arthur and Sergei Vassilvitskii
|
/**
|
||||||
// (see http://en.wikipedia.org/wiki/K-means++)
|
* @param $dimension
|
||||||
const SEED_DASV = 2;
|
*/
|
||||||
|
public function __construct($dimension)
|
||||||
|
{
|
||||||
|
if ($dimension < 1) {
|
||||||
|
throw new LogicException('a space dimension cannot be null or negative');
|
||||||
|
}
|
||||||
|
|
||||||
protected $dimention;
|
$this->dimension = $dimension;
|
||||||
|
}
|
||||||
|
|
||||||
public function __construct($dimention)
|
/**
|
||||||
{
|
* @return array
|
||||||
if ($dimention < 1)
|
*/
|
||||||
throw new LogicException("a space dimention cannot be null or negative");
|
public function toArray()
|
||||||
|
{
|
||||||
|
$points = [];
|
||||||
|
foreach ($this as $point) {
|
||||||
|
$points[] = $point->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
$this->dimention = $dimention;
|
return ['points' => $points];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function toArray()
|
/**
|
||||||
{
|
* @param array $coordinates
|
||||||
$points = array();
|
*
|
||||||
foreach ($this as $point)
|
* @return Point
|
||||||
$points[] = $point->toArray();
|
*/
|
||||||
|
public function newPoint(array $coordinates)
|
||||||
|
{
|
||||||
|
if (count($coordinates) != $this->dimension) {
|
||||||
|
throw new LogicException('('.implode(',', $coordinates).') is not a point of this space');
|
||||||
|
}
|
||||||
|
|
||||||
return array('points' => $points);
|
return new Point($coordinates);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function newPoint(array $coordinates)
|
/**
|
||||||
{
|
* @param array $coordinates
|
||||||
if (count($coordinates) != $this->dimention)
|
* @param null $data
|
||||||
throw new LogicException("(" . implode(',', $coordinates) . ") is not a point of this space");
|
*/
|
||||||
|
public function addPoint(array $coordinates, $data = null)
|
||||||
|
{
|
||||||
|
return $this->attach($this->newPoint($coordinates), $data);
|
||||||
|
}
|
||||||
|
|
||||||
return new Point($this, $coordinates);
|
/**
|
||||||
}
|
* @param object $point
|
||||||
|
* @param null $data
|
||||||
|
*/
|
||||||
|
public function attach($point, $data = null)
|
||||||
|
{
|
||||||
|
if (!$point instanceof Point) {
|
||||||
|
throw new InvalidArgumentException('can only attach points to spaces');
|
||||||
|
}
|
||||||
|
|
||||||
public function addPoint(array $coordinates, $data = null)
|
return parent::attach($point, $data);
|
||||||
{
|
}
|
||||||
return $this->attach($this->newPoint($coordinates), $data);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function attach($point, $data = null)
|
/**
|
||||||
{
|
* @return int
|
||||||
if (!$point instanceof Point)
|
*/
|
||||||
throw new InvalidArgumentException("can only attach points to spaces");
|
public function getDimension()
|
||||||
|
{
|
||||||
|
return $this->dimension;
|
||||||
|
}
|
||||||
|
|
||||||
return parent::attach($point, $data);
|
/**
|
||||||
}
|
* @return array|bool
|
||||||
|
*/
|
||||||
|
public function getBoundaries()
|
||||||
|
{
|
||||||
|
if (!count($this)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public function getDimention()
|
$min = $this->newPoint(array_fill(0, $this->dimension, null));
|
||||||
{
|
$max = $this->newPoint(array_fill(0, $this->dimension, null));
|
||||||
return $this->dimention;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getBoundaries()
|
foreach ($this as $point) {
|
||||||
{
|
for ($n = 0; $n < $this->dimension; ++$n) {
|
||||||
if (!count($this))
|
($min[$n] > $point[$n] || $min[$n] === null) && $min[$n] = $point[$n];
|
||||||
return false;
|
($max[$n] < $point[$n] || $max[$n] === null) && $max[$n] = $point[$n];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$min = $this->newPoint(array_fill(0, $this->dimention, null));
|
return array($min, $max);
|
||||||
$max = $this->newPoint(array_fill(0, $this->dimention, null));
|
}
|
||||||
|
|
||||||
foreach ($this as $point) {
|
/**
|
||||||
for ($n=0; $n < $this->dimention; $n++) {
|
* @param Point $min
|
||||||
($min[$n] > $point[$n] || $min[$n] === null) && $min[$n] = $point[$n];
|
* @param Point $max
|
||||||
($max[$n] < $point[$n] || $max[$n] === null) && $max[$n] = $point[$n];
|
*
|
||||||
}
|
* @return Point
|
||||||
}
|
*/
|
||||||
|
public function getRandomPoint(Point $min, Point $max)
|
||||||
|
{
|
||||||
|
$point = $this->newPoint(array_fill(0, $this->dimension, null));
|
||||||
|
|
||||||
return array($min, $max);
|
for ($n = 0; $n < $this->dimension; ++$n) {
|
||||||
}
|
$point[$n] = rand($min[$n], $max[$n]);
|
||||||
|
}
|
||||||
|
|
||||||
public function getRandomPoint(Point $min, Point $max)
|
return $point;
|
||||||
{
|
}
|
||||||
$point = $this->newPoint(array_fill(0, $this->dimention, null));
|
|
||||||
|
|
||||||
for ($n=0; $n < $this->dimention; $n++)
|
/**
|
||||||
$point[$n] = rand($min[$n], $max[$n]);
|
* @param $nbClusters
|
||||||
|
* @param int $seed
|
||||||
|
* @param null $iterationCallback
|
||||||
|
*
|
||||||
|
* @return array|Cluster[]
|
||||||
|
*/
|
||||||
|
public function solve($nbClusters, $seed = KMeans::INIT_RANDOM, $iterationCallback = null)
|
||||||
|
{
|
||||||
|
if ($iterationCallback && !is_callable($iterationCallback)) {
|
||||||
|
throw new InvalidArgumentException('invalid iteration callback');
|
||||||
|
}
|
||||||
|
|
||||||
return $point;
|
// initialize K clusters
|
||||||
}
|
$clusters = $this->initializeClusters($nbClusters, $seed);
|
||||||
|
|
||||||
/**
|
// there's only one cluster, clusterization has no meaning
|
||||||
* @param $nbClusters
|
if (count($clusters) == 1) {
|
||||||
* @param int $seed
|
return $clusters[0];
|
||||||
* @param null $iterationCallback
|
}
|
||||||
* @return array|Cluster[]
|
|
||||||
*/
|
|
||||||
public function solve($nbClusters, $seed = self::SEED_DEFAULT, $iterationCallback = null)
|
|
||||||
{
|
|
||||||
if ($iterationCallback && !is_callable($iterationCallback))
|
|
||||||
throw new InvalidArgumentException("invalid iteration callback");
|
|
||||||
|
|
||||||
// initialize K clusters
|
// until convergence is reached
|
||||||
$clusters = $this->initializeClusters($nbClusters, $seed);
|
do {
|
||||||
|
$iterationCallback && $iterationCallback($this, $clusters);
|
||||||
|
} while ($this->iterate($clusters));
|
||||||
|
|
||||||
// there's only one cluster, clusterization has no meaning
|
// clustering is done.
|
||||||
if (count($clusters) == 1)
|
return $clusters;
|
||||||
return $clusters[0];
|
}
|
||||||
|
|
||||||
// until convergence is reached
|
/**
|
||||||
do {
|
* @param $nbClusters
|
||||||
$iterationCallback && $iterationCallback($this, $clusters);
|
* @param $seed
|
||||||
} while ($this->iterate($clusters));
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function initializeClusters($nbClusters, $seed)
|
||||||
|
{
|
||||||
|
if ($nbClusters <= 0) {
|
||||||
|
throw new InvalidArgumentException('invalid clusters number');
|
||||||
|
}
|
||||||
|
|
||||||
// clustering is done.
|
switch ($seed) {
|
||||||
return $clusters;
|
// the default seeding method chooses completely random centroid
|
||||||
}
|
case KMeans::INIT_RANDOM:
|
||||||
|
// get the space boundaries to avoid placing clusters centroid too far from points
|
||||||
|
list($min, $max) = $this->getBoundaries();
|
||||||
|
|
||||||
protected function initializeClusters($nbClusters, $seed)
|
// initialize N clusters with a random point within space boundaries
|
||||||
{
|
for ($n = 0; $n < $nbClusters; ++$n) {
|
||||||
if ($nbClusters <= 0)
|
$clusters[] = new Cluster($this, $this->getRandomPoint($min, $max)->getCoordinates());
|
||||||
throw new InvalidArgumentException("invalid clusters number");
|
}
|
||||||
|
|
||||||
switch ($seed) {
|
break;
|
||||||
// the default seeding method chooses completely random centroid
|
|
||||||
case self::SEED_DEFAULT:
|
|
||||||
// get the space boundaries to avoid placing clusters centroid too far from points
|
|
||||||
list($min, $max) = $this->getBoundaries();
|
|
||||||
|
|
||||||
// initialize N clusters with a random point within space boundaries
|
// the DASV seeding method consists of finding good initial centroids for the clusters
|
||||||
for ($n=0; $n<$nbClusters; $n++)
|
case KMeans::INIT_KMEANS_PLUS_PLUS:
|
||||||
$clusters[] = new Cluster($this, $this->getRandomPoint($min, $max)->getCoordinates());
|
// find a random point
|
||||||
|
$position = rand(1, count($this));
|
||||||
|
for ($i = 1, $this->rewind(); $i < $position && $this->valid(); $i++, $this->next());
|
||||||
|
$clusters[] = new Cluster($this, $this->current()->getCoordinates());
|
||||||
|
|
||||||
break;
|
// retains the distances between points and their closest clusters
|
||||||
|
$distances = new SplObjectStorage();
|
||||||
|
|
||||||
// the DASV seeding method consists of finding good initial centroids for the clusters
|
// create k clusters
|
||||||
case self::SEED_DASV:
|
for ($i = 1; $i < $nbClusters; ++$i) {
|
||||||
// find a random point
|
$sum = 0;
|
||||||
$position = rand(1, count($this));
|
|
||||||
for ($i=1, $this->rewind(); $i<$position && $this->valid(); $i++, $this->next());
|
|
||||||
$clusters[] = new Cluster($this, $this->current()->getCoordinates());
|
|
||||||
|
|
||||||
// retains the distances between points and their closest clusters
|
// for each points, get the distance with the closest centroid already choosen
|
||||||
$distances = new SplObjectStorage;
|
foreach ($this as $point) {
|
||||||
|
$distance = $point->getDistanceWith($point->getClosest($clusters));
|
||||||
|
$sum += $distances[$point] = $distance;
|
||||||
|
}
|
||||||
|
|
||||||
// create k clusters
|
// choose a new random point using a weighted probability distribution
|
||||||
for ($i=1; $i<$nbClusters; $i++) {
|
$sum = rand(0, (int) $sum);
|
||||||
$sum = 0;
|
foreach ($this as $point) {
|
||||||
|
if (($sum -= $distances[$point]) > 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// for each points, get the distance with the closest centroid already choosen
|
$clusters[] = new Cluster($this, $point->getCoordinates());
|
||||||
foreach ($this as $point) {
|
break;
|
||||||
$distance = $point->getDistanceWith($point->getClosest($clusters));
|
}
|
||||||
$sum += $distances[$point] = $distance;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// choose a new random point using a weighted probability distribution
|
break;
|
||||||
$sum = rand(0, $sum);
|
}
|
||||||
foreach ($this as $point) {
|
|
||||||
if (($sum -= $distances[$point]) > 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
$clusters[] = new Cluster($this, $point->getCoordinates());
|
// assing all points to the first cluster
|
||||||
break;
|
$clusters[0]->attachAll($this);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
return $clusters;
|
||||||
}
|
}
|
||||||
|
|
||||||
// assing all points to the first cluster
|
/**
|
||||||
$clusters[0]->attachAll($this);
|
* @param $clusters
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function iterate($clusters)
|
||||||
|
{
|
||||||
|
$continue = false;
|
||||||
|
|
||||||
return $clusters;
|
// migration storages
|
||||||
}
|
$attach = new SplObjectStorage();
|
||||||
|
$detach = new SplObjectStorage();
|
||||||
|
|
||||||
protected function iterate($clusters)
|
// calculate proximity amongst points and clusters
|
||||||
{
|
foreach ($clusters as $cluster) {
|
||||||
$continue = false;
|
foreach ($cluster as $point) {
|
||||||
|
// find the closest cluster
|
||||||
|
$closest = $point->getClosest($clusters);
|
||||||
|
|
||||||
// migration storages
|
// move the point from its old cluster to its closest
|
||||||
$attach = new SplObjectStorage;
|
if ($closest !== $cluster) {
|
||||||
$detach = new SplObjectStorage;
|
isset($attach[$closest]) || $attach[$closest] = new SplObjectStorage();
|
||||||
|
isset($detach[$cluster]) || $detach[$cluster] = new SplObjectStorage();
|
||||||
|
|
||||||
// calculate proximity amongst points and clusters
|
$attach[$closest]->attach($point);
|
||||||
foreach ($clusters as $cluster) {
|
$detach[$cluster]->attach($point);
|
||||||
foreach ($cluster as $point) {
|
|
||||||
// find the closest cluster
|
|
||||||
$closest = $point->getClosest($clusters);
|
|
||||||
|
|
||||||
// move the point from its old cluster to its closest
|
$continue = true;
|
||||||
if ($closest !== $cluster) {
|
}
|
||||||
isset($attach[$closest]) || $attach[$closest] = new SplObjectStorage;
|
}
|
||||||
isset($detach[$cluster]) || $detach[$cluster] = new SplObjectStorage;
|
}
|
||||||
|
|
||||||
$attach[$closest]->attach($point);
|
// perform points migrations
|
||||||
$detach[$cluster]->attach($point);
|
foreach ($attach as $cluster) {
|
||||||
|
$cluster->attachAll($attach[$cluster]);
|
||||||
|
}
|
||||||
|
|
||||||
$continue = true;
|
foreach ($detach as $cluster) {
|
||||||
}
|
$cluster->detachAll($detach[$cluster]);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// perform points migrations
|
// update all cluster's centroids
|
||||||
foreach ($attach as $cluster)
|
foreach ($clusters as $cluster) {
|
||||||
$cluster->attachAll($attach[$cluster]);
|
$cluster->updateCentroid();
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($detach as $cluster)
|
return $continue;
|
||||||
$cluster->detachAll($detach[$cluster]);
|
}
|
||||||
|
|
||||||
// update all cluster's centroids
|
|
||||||
foreach ($clusters as $cluster)
|
|
||||||
$cluster->updateCentroid();
|
|
||||||
|
|
||||||
return $continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -65,5 +65,4 @@ class InvalidArgumentException extends \Exception
|
|||||||
{
|
{
|
||||||
return new self('Invalid clusters number');
|
return new self('Invalid clusters number');
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,6 @@ class DBSCANTest extends \PHPUnit_Framework_TestCase
|
|||||||
|
|
||||||
$this->assertEquals($clustered, $dbscan->cluster($samples));
|
$this->assertEquals($clustered, $dbscan->cluster($samples));
|
||||||
|
|
||||||
|
|
||||||
$samples = [[1, 1], [6, 6], [1, -1], [5, 6], [-1, -1], [7, 8], [-1, 1], [7, 7]];
|
$samples = [[1, 1], [6, 6], [1, -1], [5, 6], [-1, -1], [7, 8], [-1, 1], [7, 7]];
|
||||||
$clustered = [
|
$clustered = [
|
||||||
[[1, 1], [1, -1], [-1, -1], [-1, 1]],
|
[[1, 1], [1, -1], [-1, -1], [-1, 1]],
|
||||||
@ -31,5 +30,4 @@ class DBSCANTest extends \PHPUnit_Framework_TestCase
|
|||||||
|
|
||||||
$this->assertEquals($clustered, $dbscan->cluster($samples));
|
$this->assertEquals($clustered, $dbscan->cluster($samples));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types = 1);
|
|
||||||
|
declare (strict_types = 1);
|
||||||
|
|
||||||
namespace tests\Clustering;
|
namespace tests\Clustering;
|
||||||
|
|
||||||
@ -7,7 +8,6 @@ use Phpml\Clustering\KMeans;
|
|||||||
|
|
||||||
class KMeansTest extends \PHPUnit_Framework_TestCase
|
class KMeansTest extends \PHPUnit_Framework_TestCase
|
||||||
{
|
{
|
||||||
|
|
||||||
public function testKMeansSamplesClustering()
|
public function testKMeansSamplesClustering()
|
||||||
{
|
{
|
||||||
$samples = [[1, 1], [8, 7], [1, 2], [7, 8], [2, 1], [8, 9]];
|
$samples = [[1, 1], [8, 7], [1, 2], [7, 8], [2, 1], [8, 9]];
|
||||||
@ -18,7 +18,7 @@ class KMeansTest extends \PHPUnit_Framework_TestCase
|
|||||||
$this->assertEquals(2, count($clusters));
|
$this->assertEquals(2, count($clusters));
|
||||||
|
|
||||||
foreach ($samples as $index => $sample) {
|
foreach ($samples as $index => $sample) {
|
||||||
if(in_array($sample, $clusters[0]) || in_array($sample, $clusters[1])) {
|
if (in_array($sample, $clusters[0]) || in_array($sample, $clusters[1])) {
|
||||||
unset($samples[$index]);
|
unset($samples[$index]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -28,16 +28,16 @@ class KMeansTest extends \PHPUnit_Framework_TestCase
|
|||||||
public function testKMeansMoreSamplesClustering()
|
public function testKMeansMoreSamplesClustering()
|
||||||
{
|
{
|
||||||
$samples = [
|
$samples = [
|
||||||
[80,55],[86,59],[19,85],[41,47],[57,58],
|
[80, 55], [86, 59], [19, 85], [41, 47], [57, 58],
|
||||||
[76,22],[94,60],[13,93],[90,48],[52,54],
|
[76, 22], [94, 60], [13, 93], [90, 48], [52, 54],
|
||||||
[62,46],[88,44],[85,24],[63,14],[51,40],
|
[62, 46], [88, 44], [85, 24], [63, 14], [51, 40],
|
||||||
[75,31],[86,62],[81,95],[47,22],[43,95],
|
[75, 31], [86, 62], [81, 95], [47, 22], [43, 95],
|
||||||
[71,19],[17,65],[69,21],[59,60],[59,12],
|
[71, 19], [17, 65], [69, 21], [59, 60], [59, 12],
|
||||||
[15,22],[49,93],[56,35],[18,20],[39,59],
|
[15, 22], [49, 93], [56, 35], [18, 20], [39, 59],
|
||||||
[50,15],[81,36],[67,62],[32,15],[75,65],
|
[50, 15], [81, 36], [67, 62], [32, 15], [75, 65],
|
||||||
[10,47],[75,18],[13,45],[30,62],[95,79],
|
[10, 47], [75, 18], [13, 45], [30, 62], [95, 79],
|
||||||
[64,11],[92,14],[94,49],[39,13],[60,68],
|
[64, 11], [92, 14], [94, 49], [39, 13], [60, 68],
|
||||||
[62,10],[74,44],[37,42],[97,60],[47,73],
|
[62, 10], [74, 44], [37, 42], [97, 60], [47, 73],
|
||||||
];
|
];
|
||||||
|
|
||||||
$kmeans = new KMeans(4);
|
$kmeans = new KMeans(4);
|
||||||
@ -46,13 +46,12 @@ class KMeansTest extends \PHPUnit_Framework_TestCase
|
|||||||
$this->assertEquals(4, count($clusters));
|
$this->assertEquals(4, count($clusters));
|
||||||
|
|
||||||
foreach ($samples as $index => $sample) {
|
foreach ($samples as $index => $sample) {
|
||||||
for($i=0; $i<4; $i++) {
|
for ($i = 0; $i < 4; ++$i) {
|
||||||
if(in_array($sample, $clusters[$i])) {
|
if (in_array($sample, $clusters[$i])) {
|
||||||
unset($samples[$index]);
|
unset($samples[$index]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$this->assertEquals(0, count($samples));
|
$this->assertEquals(0, count($samples));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user