Skip to content

Commit ce8031b

Browse files
committed
transition to a new curve fitting pipeline has begun
1 parent 003f765 commit ce8031b

29 files changed

Lines changed: 1574 additions & 879 deletions

src/fit.rs

Lines changed: 149 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use crate::domain::{
1919
AdamConfig, CurveFamily, CurveParams, FitResult, InputError, LbfgsConfig, NelderMeadConfig,
2020
NewtonCgConfig, OptimizerConfig, Points, SgdConfig, SteepestDescentConfig,
2121
};
22-
use crate::models::{self, GradientComputation};
22+
use crate::models::{self, ObjectiveGrad, ObjectiveHessian, ObjectiveValue, PredictionLoss};
2323

2424
mod curve;
2525
mod simd;
@@ -136,8 +136,6 @@ const STEEPEST_DESCENT_GRAD_TOL: f64 = 1e-12;
136136
const HESSIAN_FD_REL_STEP: f64 = 1e-4;
137137
const HESSIAN_FD_MIN_STEP: f64 = 1e-6;
138138
pub(crate) const HESSIAN_DIAGONAL_JITTER: f64 = models::HESSIAN_DIAGONAL_JITTER;
139-
const GRADIENT_FD_REL_STEP: f64 = 1e-5;
140-
const GRADIENT_FD_MIN_STEP: f64 = 1e-7;
141139

142140
fn positive_x(value: f64) -> f64 {
143141
models::positive_x(value)
@@ -166,12 +164,6 @@ fn array1_as_slice(values: &Array1<f64>) -> &[f64] {
166164
.expect("Array1 parameters must have contiguous memory layout")
167165
}
168166

169-
fn array1_as_slice_mut(values: &mut Array1<f64>) -> &mut [f64] {
170-
values
171-
.as_slice_mut()
172-
.expect("Array1 parameters must have contiguous memory layout")
173-
}
174-
175167
fn stabilize_hessian(hessian: &mut Array2<f64>) {
176168
let dimension = hessian.nrows();
177169
debug_assert_eq!(dimension, hessian.ncols());
@@ -410,6 +402,142 @@ struct CurveProblem {
410402
residual_quantizer: ResidualQuantizer,
411403
}
412404

405+
#[derive(Clone, Copy)]
406+
struct CurveProblemPredictionLoss<'a> {
407+
problem: &'a CurveProblem,
408+
}
409+
410+
impl PredictionLoss for CurveProblemPredictionLoss<'_> {
411+
fn value(&self, prediction: f64, target: f64) -> f64 {
412+
self.problem.loss_value_from_prediction(prediction, target)
413+
}
414+
415+
fn d_prediction(&self, prediction: f64, target: f64) -> f64 {
416+
self.problem
417+
.loss_derivative_from_prediction(prediction, target)
418+
}
419+
420+
fn d2_prediction(&self, prediction: f64, target: f64) -> f64 {
421+
self.problem
422+
.loss_second_derivative_from_prediction(prediction, target)
423+
}
424+
}
425+
426+
struct CurveProblemObjective<'a> {
427+
problem: &'a CurveProblem,
428+
}
429+
430+
impl CurveProblemObjective<'_> {
431+
fn simd_enabled(&self) -> bool {
432+
matches!(
433+
self.problem.metric_quantization,
434+
MetricQuantization::Disabled
435+
)
436+
}
437+
438+
fn value(&self, param: &[f64]) -> f64 {
439+
if self.simd_enabled() && self.problem.family.is_polynomial() {
440+
return simd::polynomial_cost(
441+
param,
442+
self.problem.point_x.as_ref(),
443+
self.problem.point_y.as_ref(),
444+
self.problem.loss_metric,
445+
);
446+
}
447+
if self.simd_enabled() && self.problem.family == CurveFamily::Inverse {
448+
return simd::inverse_cost(
449+
param,
450+
self.problem.point_x.as_ref(),
451+
self.problem.point_y.as_ref(),
452+
self.problem.loss_metric,
453+
);
454+
}
455+
456+
let loss = CurveProblemPredictionLoss {
457+
problem: self.problem,
458+
};
459+
let term = models::DataTerm::new(
460+
self.problem.family,
461+
self.problem.point_x.as_ref(),
462+
self.problem.point_y.as_ref(),
463+
loss,
464+
);
465+
let objective = models::CurveObjective::new(self.problem.family.parameter_count(), term);
466+
objective.value(param)
467+
}
468+
469+
fn value_grad(&self, param: &[f64]) -> (f64, Vec<f64>) {
470+
if self.simd_enabled() && self.problem.family.is_polynomial() {
471+
let mut gradient = vec![0.0; self.problem.family.parameter_count()];
472+
simd::accumulate_polynomial_gradient(
473+
self.problem.point_x.as_ref(),
474+
self.problem.point_y.as_ref(),
475+
param,
476+
self.problem.loss_metric,
477+
&mut gradient,
478+
);
479+
let sample_scale = 1.0 / self.problem.point_x.len() as f64;
480+
for value in &mut gradient {
481+
*value *= sample_scale;
482+
}
483+
let value = simd::polynomial_cost(
484+
param,
485+
self.problem.point_x.as_ref(),
486+
self.problem.point_y.as_ref(),
487+
self.problem.loss_metric,
488+
);
489+
return (value, gradient);
490+
}
491+
if self.simd_enabled() && self.problem.family == CurveFamily::Inverse {
492+
let mut gradient = vec![0.0; self.problem.family.parameter_count()];
493+
simd::accumulate_inverse_gradient(
494+
self.problem.point_x.as_ref(),
495+
self.problem.point_y.as_ref(),
496+
param,
497+
self.problem.loss_metric,
498+
&mut gradient,
499+
);
500+
let sample_scale = 1.0 / self.problem.point_x.len() as f64;
501+
for value in &mut gradient {
502+
*value *= sample_scale;
503+
}
504+
let value = simd::inverse_cost(
505+
param,
506+
self.problem.point_x.as_ref(),
507+
self.problem.point_y.as_ref(),
508+
self.problem.loss_metric,
509+
);
510+
return (value, gradient);
511+
}
512+
513+
let loss = CurveProblemPredictionLoss {
514+
problem: self.problem,
515+
};
516+
let term = models::DataTerm::new(
517+
self.problem.family,
518+
self.problem.point_x.as_ref(),
519+
self.problem.point_y.as_ref(),
520+
loss,
521+
);
522+
let objective = models::CurveObjective::new(self.problem.family.parameter_count(), term);
523+
objective.value_grad(param)
524+
}
525+
526+
fn value_grad_hessian(&self, param: &[f64]) -> (f64, Vec<f64>, Array2<f64>) {
527+
let loss = CurveProblemPredictionLoss {
528+
problem: self.problem,
529+
};
530+
let term = models::DataTerm::new(
531+
self.problem.family,
532+
self.problem.point_x.as_ref(),
533+
self.problem.point_y.as_ref(),
534+
loss,
535+
);
536+
let objective = models::CurveObjective::new(self.problem.family.parameter_count(), term);
537+
objective.value_grad_hessian(param)
538+
}
539+
}
540+
413541
impl CurveProblem {
414542
fn new_with_metric_quantization(
415543
family: CurveFamily,
@@ -456,39 +584,8 @@ impl CurveProblem {
456584
.residual_second_derivative(self.residual(predicted, observed))
457585
}
458586

459-
fn analytic_hessian_for_supported_families(&self, param: &Array1<f64>) -> Option<Array2<f64>> {
460-
if matches!(self.loss_metric, OptimizationLossMetric::Mae) {
461-
return None;
462-
}
463-
464-
let param = array1_as_slice(param);
465-
models::analytic_hessian(
466-
self.family,
467-
self.point_x.as_ref(),
468-
self.point_y.as_ref(),
469-
param,
470-
|predicted, observed| self.loss_derivative_from_prediction(predicted, observed),
471-
|predicted, observed| self.loss_second_derivative_from_prediction(predicted, observed),
472-
)
473-
}
474-
475-
fn numerical_gradient_from_cost(
476-
&self,
477-
param: &[f64],
478-
gradient: &mut [f64],
479-
) -> Result<(), argmin::core::Error> {
480-
let mut probe = vec_to_array1(param);
481-
for index in 0..gradient.len() {
482-
let step =
483-
((param[index].abs() + 1.0) * GRADIENT_FD_REL_STEP).max(GRADIENT_FD_MIN_STEP);
484-
probe[index] = param[index] + step;
485-
let cost_plus = CostFunction::cost(self, &probe)?;
486-
probe[index] = param[index] - step;
487-
let cost_minus = CostFunction::cost(self, &probe)?;
488-
probe[index] = param[index];
489-
gradient[index] = (cost_plus - cost_minus) / (2.0 * step);
490-
}
491-
Ok(())
587+
fn objective(&self) -> CurveProblemObjective<'_> {
588+
CurveProblemObjective { problem: self }
492589
}
493590
}
494591

@@ -1622,48 +1719,12 @@ impl CostFunction for CurveProblem {
16221719

16231720
fn cost(&self, param: &Self::Param) -> Result<Self::Output, argmin::core::Error> {
16241721
let param = array1_as_slice(param);
1625-
if self.family.is_polynomial()
1626-
&& matches!(self.metric_quantization, MetricQuantization::Disabled)
1627-
{
1628-
return Ok(simd::polynomial_cost(
1629-
param,
1630-
self.point_x.as_ref(),
1631-
self.point_y.as_ref(),
1632-
self.loss_metric,
1633-
));
1634-
}
1635-
1636-
if self.family == CurveFamily::Inverse
1637-
&& matches!(self.metric_quantization, MetricQuantization::Disabled)
1638-
{
1639-
return Ok(simd::inverse_cost(
1640-
param,
1641-
self.point_x.as_ref(),
1642-
self.point_y.as_ref(),
1643-
self.loss_metric,
1644-
));
1645-
}
1646-
1647-
let sample_count = self.point_x.len() as f64;
1648-
let mut sum = 0.0;
1649-
1650-
let mut index = 0;
1651-
while index < self.point_x.len() {
1652-
let x = self.point_x[index];
1653-
let y = self.point_y[index];
1654-
let predicted = self.family.evaluate_raw(param, x);
1655-
let residual = self.residual(predicted, y);
1656-
if !residual.is_finite() {
1657-
return Ok(LARGE_COST);
1658-
}
1659-
sum += self.loss_value_from_prediction(predicted, y);
1660-
if !sum.is_finite() {
1661-
return Ok(LARGE_COST);
1662-
}
1663-
index += 1;
1722+
let value = self.objective().value(param);
1723+
if value.is_finite() {
1724+
Ok(value)
1725+
} else {
1726+
Ok(LARGE_COST)
16641727
}
1665-
1666-
Ok(sum / sample_count)
16671728
}
16681729
}
16691730

@@ -1673,57 +1734,13 @@ impl Gradient for CurveProblem {
16731734

16741735
fn gradient(&self, param: &Self::Param) -> Result<Self::Gradient, argmin::core::Error> {
16751736
let param = array1_as_slice(param);
1676-
let mut gradient = Array1::zeros(self.family.parameter_count());
1677-
let sample_scale = 1.0 / self.point_x.len() as f64;
1678-
let gradient_slice = array1_as_slice_mut(&mut gradient);
1679-
1680-
if self.family.is_polynomial()
1681-
&& matches!(self.metric_quantization, MetricQuantization::Disabled)
1682-
{
1683-
simd::accumulate_polynomial_gradient(
1684-
self.point_x.as_ref(),
1685-
self.point_y.as_ref(),
1686-
param,
1687-
self.loss_metric,
1688-
gradient_slice,
1689-
);
1690-
} else if self.family == CurveFamily::Inverse
1691-
&& matches!(self.metric_quantization, MetricQuantization::Disabled)
1692-
{
1693-
simd::accumulate_inverse_gradient(
1694-
self.point_x.as_ref(),
1695-
self.point_y.as_ref(),
1696-
param,
1697-
self.loss_metric,
1698-
gradient_slice,
1699-
);
1700-
} else {
1701-
let mode = models::accumulate_gradient(
1702-
self.family,
1703-
self.point_x.as_ref(),
1704-
self.point_y.as_ref(),
1705-
param,
1706-
|predicted, observed| self.loss_derivative_from_prediction(predicted, observed),
1707-
gradient_slice,
1708-
);
1709-
if matches!(mode, GradientComputation::NeedsNumerical) {
1710-
self.numerical_gradient_from_cost(param, gradient_slice)?;
1711-
// Далее применяется общий sample_scale, поэтому здесь возвращается сумма.
1712-
let sample_count = self.point_x.len() as f64;
1713-
for value in gradient_slice.iter_mut() {
1714-
*value *= sample_count;
1715-
}
1716-
}
1717-
}
1718-
1719-
for value in gradient_slice.iter_mut() {
1720-
*value *= sample_scale;
1737+
let (_, mut gradient) = self.objective().value_grad(param);
1738+
for value in &mut gradient {
17211739
if !value.is_finite() {
17221740
*value = LARGE_COST;
17231741
}
17241742
}
1725-
1726-
Ok(gradient)
1743+
Ok(vec_to_array1(&gradient))
17271744
}
17281745
}
17291746

@@ -1732,7 +1749,8 @@ impl Hessian for CurveProblem {
17321749
type Hessian = Array2<f64>;
17331750

17341751
fn hessian(&self, param: &Self::Param) -> Result<Self::Hessian, argmin::core::Error> {
1735-
if let Some(hessian) = self.analytic_hessian_for_supported_families(param) {
1752+
if !matches!(self.loss_metric, OptimizationLossMetric::Mae) {
1753+
let (_, _, hessian) = self.objective().value_grad_hessian(array1_as_slice(param));
17361754
return Ok(hessian);
17371755
}
17381756
numerical_hessian_from_gradient(self, param)

0 commit comments

Comments
 (0)