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->eigenvectors); } /** * 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 { if (!$dataset instanceof Labeled) { throw new InvalidArgumentException('Transformer requires a' . ' Labeled training set.'); } SamplesAreCompatibleWithTransformer::with($dataset, $this)->check(); if ($dataset->labelType() != DataType::categorical()) { throw new InvalidArgumentException('Transformer requires' . " categorical labels, {$dataset->labelType()} given."); } [$m, $n] = $dataset->shape(); $sW = Matrix::zeros($n, $n); foreach ($dataset->stratifyByLabel() as $stratum) { $prior = $stratum->numSamples() / $m; $sW = Matrix::quick($stratum->samples()) ->transpose() ->covariance() ->multiply($prior) ->add($sW); } $eig = Matrix::quick($dataset->samples()) ->transpose() ->covariance() ->subtract($sW) ->eig(true); $eigenvalues = $eig->eigenvalues(); $eigenvectors = $eig->eigenvectors()->asArray(); $totalVariance = array_sum($eigenvalues); array_multisort($eigenvalues, SORT_DESC, $eigenvectors); $eigenvalues = array_slice($eigenvalues, 0, $this->dimensions); $eigenvectors = array_slice($eigenvectors, 0, $this->dimensions); $eigenvectors = Matrix::quick($eigenvectors)->transpose(); $noiseVariance = $totalVariance - array_sum($eigenvalues); $lossiness = $noiseVariance / ($totalVariance ?: EPSILON); $this->eigenvectors = $eigenvectors; $this->lossiness = $lossiness; } /** * Transform the dataset in place. * * @param list> $samples * @throws RuntimeException */ public function transform(array &$samples) : void { if (!$this->eigenvectors) { throw new RuntimeException('Transformer has not been fitted.'); } $samples = Matrix::build($samples) ->matmul($this->eigenvectors) ->asArray(); } /** * Return the string representation of the object. * * @internal * * @return string */ public function __toString() : string { return "Linear Discriminant Analysis (dimensions: {$this->dimensions})"; } }