check(); if ($dimensions < 1) { throw new InvalidArgumentException('Dimensions must be' . " greater than 0, $dimensions given."); } $this->dimensions = $dimensions; } /** * Return the data types that this transformer is compatible with. * * @internal * * @return list */ public function compatibility() : array { return [ DataType::continuous(), ]; } /** * Is the transformer fitted? * * @return bool */ public function fitted() : bool { return isset($this->components); } /** * Return the percentage of information lost due to the transformation. * * @return float|null */ public function lossiness() : ?float { return $this->lossiness; } /** * Fit the transformer to a dataset. * * @param Dataset $dataset * @throws InvalidArgumentException */ public function fit(Dataset $dataset) : void { SamplesAreCompatibleWithTransformer::with($dataset, $this)->check(); $svd = Matrix::build($dataset->samples())->svd(); $singularValues = $svd->singularValues(); $components = $svd->vT()->asArray(); $totalStdDev = array_sum($singularValues); $singularValues = array_slice($singularValues, 0, $this->dimensions); $components = array_slice($components, 0, $this->dimensions); $components = Matrix::quick($components)->transpose(); $noiseStdDev = $totalStdDev - array_sum($singularValues); $lossiness = $noiseStdDev / ($totalStdDev ?: EPSILON); $this->components = $components; $this->lossiness = $lossiness; } /** * Transform the dataset in place. * * @param list> $samples * @throws RuntimeException */ public function transform(array &$samples) : void { if (!$this->components) { throw new RuntimeException('Transformer has not been fitted.'); } $samples = Matrix::build($samples) ->matmul($this->components) ->asArray(); } /** * Return the string representation of the object. * * @internal * * @return string */ public function __toString() : string { return "Truncated SVD (dimensions: {$this->dimensions})"; } }