dimension = $dimension; } public function toArray(): array { $points = []; /** @var Point $point */ foreach ($this as $point) { $points[] = $point->toArray(); } return ['points' => $points]; } /** * @param mixed $label */ public function newPoint(array $coordinates, $label = null): Point { if (count($coordinates) !== $this->dimension) { throw new LogicException('('.implode(',', $coordinates).') is not a point of this space'); } return new Point($coordinates, $label); } /** * @param mixed $label * @param mixed $data */ public function addPoint(array $coordinates, $label = null, $data = null): void { $this->attach($this->newPoint($coordinates, $label), $data); } /** * @param object $point * @param mixed $data */ public function attach($point, $data = null): void { if (!$point instanceof Point) { throw new InvalidArgumentException('can only attach points to spaces'); } parent::attach($point, $data); } public function getDimension(): int { return $this->dimension; } /** * @return array|bool */ public function getBoundaries() { if (count($this) === 0) { return false; } $min = $this->newPoint(array_fill(0, $this->dimension, null)); $max = $this->newPoint(array_fill(0, $this->dimension, null)); /** @var Point $point */ foreach ($this as $point) { for ($n = 0; $n < $this->dimension; ++$n) { if ($min[$n] === null || $min[$n] > $point[$n]) { $min[$n] = $point[$n]; } if ($max[$n] === null || $max[$n] < $point[$n]) { $max[$n] = $point[$n]; } } } return [$min, $max]; } public function getRandomPoint(Point $min, Point $max): Point { $point = $this->newPoint(array_fill(0, $this->dimension, null)); for ($n = 0; $n < $this->dimension; ++$n) { $point[$n] = random_int($min[$n], $max[$n]); } return $point; } /** * @return Cluster[] */ public function cluster(int $clustersNumber, int $initMethod = KMeans::INIT_RANDOM): array { $clusters = $this->initializeClusters($clustersNumber, $initMethod); do { } while (!$this->iterate($clusters)); return $clusters; } /** * @return Cluster[] */ protected function initializeClusters(int $clustersNumber, int $initMethod): array { switch ($initMethod) { case KMeans::INIT_RANDOM: $clusters = $this->initializeRandomClusters($clustersNumber); break; case KMeans::INIT_KMEANS_PLUS_PLUS: $clusters = $this->initializeKMPPClusters($clustersNumber); break; default: return []; } $clusters[0]->attachAll($this); return $clusters; } /** * @param Cluster[] $clusters */ protected function iterate(array $clusters): bool { $convergence = true; $attach = new SplObjectStorage(); $detach = new SplObjectStorage(); foreach ($clusters as $cluster) { foreach ($cluster as $point) { $closest = $point->getClosest($clusters); if ($closest === null) { continue; } if ($closest !== $cluster) { $attach[$closest] ?? $attach[$closest] = new SplObjectStorage(); $detach[$cluster] ?? $detach[$cluster] = new SplObjectStorage(); $attach[$closest]->attach($point); $detach[$cluster]->attach($point); $convergence = false; } } } /** @var Cluster $cluster */ foreach ($attach as $cluster) { $cluster->attachAll($attach[$cluster]); } /** @var Cluster $cluster */ foreach ($detach as $cluster) { $cluster->detachAll($detach[$cluster]); } foreach ($clusters as $cluster) { $cluster->updateCentroid(); } return $convergence; } /** * @return Cluster[] */ protected function initializeKMPPClusters(int $clustersNumber): array { $clusters = []; $this->rewind(); /** @var Point $current */ $current = $this->current(); $clusters[] = new Cluster($this, $current->getCoordinates()); $distances = new SplObjectStorage(); for ($i = 1; $i < $clustersNumber; ++$i) { $sum = 0; /** @var Point $point */ foreach ($this as $point) { $closest = $point->getClosest($clusters); if ($closest === null) { continue; } $distance = $point->getDistanceWith($closest); $sum += $distances[$point] = $distance; } $sum = random_int(0, (int) $sum); /** @var Point $point */ foreach ($this as $point) { $sum -= $distances[$point]; if ($sum > 0) { continue; } $clusters[] = new Cluster($this, $point->getCoordinates()); break; } } return $clusters; } /** * @return Cluster[] */ private function initializeRandomClusters(int $clustersNumber): array { $clusters = []; [$min, $max] = $this->getBoundaries(); for ($n = 0; $n < $clustersNumber; ++$n) { $clusters[] = new Cluster($this, $this->getRandomPoint($min, $max)->getCoordinates()); } return $clusters; } }