> */ protected array $means = [ // ]; /** * The variances of each feature of the training set conditioned by class. * * @var array> */ protected array $variances = [ // ]; /** * A small portion of variance to add for smoothing. * * @var float|null */ protected ?float $epsilon = null; /** * @param float[]|null $priors * @param float $smoothing * @throws InvalidArgumentException */ public function __construct(?array $priors = null, float $smoothing = 1e-9) { $logPriors = []; if ($priors) { $total = array_sum($priors); if ($total == 0) { throw new InvalidArgumentException('Total class prior' . ' probability cannot be equal to 0.'); } foreach ($priors as $class => $prior) { if ($prior < 0) { throw new InvalidArgumentException('Prior probability' . " must be greater than 0, $prior given."); } $logPriors[$class] = log($prior / $total); } } if ($smoothing <= 0.0) { throw new InvalidArgumentException('Smoothing must be' . " greater than 0, $smoothing given."); } $this->logPriors = $logPriors; $this->fitPriors = is_null($priors); $this->smoothing = $smoothing; } /** * Return the estimator type. * * @internal * * @return EstimatorType */ public function type() : EstimatorType { return EstimatorType::classifier(); } /** * Return the data types that the estimator is compatible with. * * @internal * * @return list */ public function compatibility() : array { return [ DataType::continuous(), ]; } /** * Return the settings of the hyper-parameters in an associative array. * * @internal * * @return mixed[] */ public function params() : array { return [ 'priors' => $this->fitPriors ? null : $this->priors(), 'smoothing' => $this->smoothing, ]; } /** * Has the learner been trained? * * @return bool */ public function trained() : bool { return $this->means and $this->variances; } /** * Return the class prior probabilities. * * @return float[]|null */ public function priors() : ?array { return $this->logPriors ? array_map('exp', $this->logPriors) : null; } /** * Return the running means of each feature column of the training data by class. * * @return array>|null */ public function means() : ?array { return $this->means; } /** * Return the running variances of each feature column of the training data by class. * * @return array>|null */ public function variances() : ?array { return $this->variances; } /** * Train the learner with a dataset. * * @param \Rubix\ML\Datasets\Labeled $dataset */ public function train(Dataset $dataset) : void { $this->means = $this->variances = $this->weights = []; $this->partial($dataset); } /** * Perform a partial train on the learner. * * @param \Rubix\ML\Datasets\Labeled $dataset */ public function partial(Dataset $dataset) : void { SpecificationChain::with([ new DatasetIsLabeled($dataset), new DatasetIsNotEmpty($dataset), new SamplesAreCompatibleWithEstimator($dataset, $this), new LabelsAreCompatibleWithLearner($dataset, $this), ])->check(); $maxVariance = 0.0; foreach ($dataset->stratifyByLabel() as $class => $stratum) { if (isset($this->means[$class])) { $oldMeans = $this->means[$class]; $oldVariances = $this->variances[$class]; $oldWeight = $this->weights[$class]; $n = $stratum->numSamples(); $weight = $oldWeight + $n; $means = $variances = []; foreach ($stratum->features() as $column => $values) { $oldMean = $oldMeans[$column]; $oldVariance = $oldVariances[$column]; $oldVariance -= $this->epsilon; [$mean, $variance] = Stats::meanVar($values); $means[] = (($n * $mean) + ($oldWeight * $oldMean)) / $weight; $variances[] = ($oldWeight * $oldVariance + ($n * $variance) + ($oldWeight / ($n * $weight)) * ($n * $oldMean - $n * $mean) ** 2) / $weight; } } else { $means = $variances = []; foreach ($stratum->features() as $values) { [$mean, $variance] = Stats::meanVar($values); $means[] = $mean; $variances[] = $variance; } $weight = $stratum->numSamples(); } $maxVariance = max($maxVariance, ...$variances); $this->means[$class] = $means; $this->variances[$class] = $variances; $this->weights[$class] = $weight; } $epsilon = max($this->smoothing * $maxVariance, CPU::epsilon()); foreach ($this->variances as &$variances) { foreach ($variances as &$variance) { $variance += $epsilon; } } if ($this->fitPriors) { $total = array_sum($this->weights); foreach ($this->weights as $class => $weight) { $this->logPriors[$class] = log($weight / $total); } } $this->epsilon = $epsilon; } /** * Calculate the likelihood of the sample being a member of a class and choose the class with the highest likelihood as the prediction. * * @param Dataset $dataset * @throws RuntimeException * @return list */ public function predict(Dataset $dataset) : array { if (!$this->means or !$this->variances) { throw new RuntimeException('Estimator has not been trained.'); } DatasetHasDimensionality::with($dataset, count(current($this->means)))->check(); return array_map([$this, 'predictSample'], $dataset->samples()); } /** * Predict a single sample and return the result. * * @internal * * @param (int|float)[] $sample * @return string */ public function predictSample(array $sample) : string { return argmax($this->jointLogLikelihood($sample)); } /** * Estimate the joint probabilities for each possible outcome. * * @param Dataset $dataset * @throws RuntimeException * @return list> */ public function proba(Dataset $dataset) : array { if (!$this->means or !$this->variances) { throw new RuntimeException('Estimator has not been trained.'); } DatasetHasDimensionality::with($dataset, count(current($this->means)))->check(); return array_map([$this, 'probaSample'], $dataset->samples()); } /** * Predict the probabilities of a single sample and return the joint distribution. * * @internal * * @param (int|float)[] $sample * @return float[] */ public function probaSample(array $sample) : array { $jll = $this->jointLogLikelihood($sample); $total = logsumexp($jll); $dist = []; foreach ($jll as $class => $likelihood) { $dist[$class] = exp($likelihood - $total); } return $dist; } /** * Calculate the joint log likelihood of a sample being a member of each class. * * @param list $sample * @return array */ protected function jointLogLikelihood(array $sample) : array { $likelihoods = []; foreach ($this->means as $class => $means) { $variances = $this->variances[$class]; $likelihood = $this->logPriors[$class] ?? LOG_EPSILON; foreach ($sample as $column => $value) { $mean = $means[$column]; $variance = $variances[$column]; $pdf = -0.5 * log(TWO_PI * $variance); $pdf -= 0.5 * (($value - $mean) ** 2) / $variance; $likelihood += $pdf; } $likelihoods[$class] = $likelihood; } return $likelihoods; } /** * Return the string representation of the object. * * @internal * * @return string */ public function __toString() : string { return 'Gaussian NB (' . Params::stringify($this->params()) . ')'; } }