diff --git a/docs/src/architecture.md b/docs/src/architecture.md index cd3f8c2..9b17db7 100644 --- a/docs/src/architecture.md +++ b/docs/src/architecture.md @@ -86,7 +86,8 @@ Trait composition is the foundation of everything: ```rust,ignore // Core trait vocabulary — implemented by the sciencekit crates. pub trait SKEstimator { /* hyperparameters and fit */ } -pub trait SKPredictor: SKEstimator { /* predict */ } +pub trait SKRegressorPredictor: SKEstimator { /* predict -> Array1 */ } +pub trait SKClassifierPredictor: SKEstimator { /* labels -> Array1 */ } pub trait SKTransformer: SKEstimator { /* transform / fit_transform */ } pub trait SKDataSource { /* eager: full in-memory access */ } @@ -96,7 +97,7 @@ pub trait SKToOnnx { /* ONNX export */ } pub trait SKComputeBackend { /* compute device abstraction */ } ``` -Algorithms compose exactly the traits they support: an `SKSGDClassifier` implements `SKPredictor` + `SKLazySource`; an `SKStandardScaler` implements only `SKTransformer`. +Algorithms compose exactly the traits they support: an `SKSGDClassifier` implements `SKClassifierPredictor` + `SKLazySource`; an `SKStandardScaler` implements only `SKTransformer`. ## Memory management @@ -202,7 +203,7 @@ Feature flags have dual granularity: per capability (`parallel`, `allocator-*`, - **Single internal format:** extended Safetensors — JSON header with `"sciencekit_format_version"`, hyperparameters, training state and recoverability metadata (checkpointing). - **Partial writes:** in-place tensor updates at known offsets; sharding for large models; header padding for small ones. - **Compression:** `.safetensors.gz`, `.safetensors.brotli`, `.snappy.safetensors`. -- **ONNX:** every estimator implements `SKToOnnx`; external models load as a generic `SKPredictor`. +- **ONNX:** every estimator implements `SKToOnnx`; external models load as a generic predictor. - **JSON debug:** human-readable serialization through the same central export trait. Errors use `thiserror` enums per algorithm with uniform propagation of common errors; observability uses `tracing` oriented to OpenTelemetry, disableable at near-zero cost. diff --git a/docs/src/development-explained/ch02-common-core-contracts.md b/docs/src/development-explained/ch02-common-core-contracts.md index 5a2a8f9..77c751a 100644 --- a/docs/src/development-explained/ch02-common-core-contracts.md +++ b/docs/src/development-explained/ch02-common-core-contracts.md @@ -29,7 +29,8 @@ future crate compiles against" (as its own `lib.rs` header says). It decides: - how failures are reported (`SKError`), - how feature data and target data enter an algorithm (`SKDataView`, `SKTargetView`), - what it means for an algorithm to *fit*, *predict*, or *transform* - (`SKSupervisedFit`, `SKUnsupervisedFit`, `SKPredictor`, `SKFeatureTransformer`). + (`SKSupervisedFit`, `SKUnsupervisedFit`, `SKRegressorPredictor`, + `SKClassifierPredictor`, `SKFeatureTransformer`). Without this crate, `sciencekit` would be dozens of disconnected experiments. With it, every algorithm starts from a proven, shared foundation — like every room in a building @@ -260,9 +261,10 @@ match on, instead of two code paths. In **supervised** learning you have features (the input, e.g. pixel values, measurements) and a **target** (what you want to predict — the answer). Three flavors matter here: -- **Continuous**: a real number you predict, e.g. house price → `f64` (regression). -- **Integer**: a whole-number target, e.g. star ratings → `i64` (can be elevated to - continuous losslessly). +- **Continuous**: a real number you predict, e.g. house price → `F`, the model + scalar (regression). +- **Integer**: a whole-number target, e.g. star ratings → `i64` (elevated to `F` + on demand). - **Nominal**: a category, e.g. `"cat"`, `"dog"`, `"bird"` → strings (classification). `SKTargetView` is the single type that holds any of these three. Unsupervised algorithms @@ -333,13 +335,13 @@ variants. ### `SKTargetView` (the zero-copy target view) *What it is:* A `#[non_exhaustive]` enum over three target representations: -`Continuous(ArrayView1<'a, f64>)`, `Integer(ArrayView1<'a, i64>)`, and +`Continuous(ArrayView1<'a, F>)`, `Integer(ArrayView1<'a, i64>)`, and `Nominal(&'a [&'a str])`. Also derives `Debug, Clone, Copy`. *Why it exists:* Supervised algorithms need a single type for "the answers" that covers regression (continuous), integer targets, and classification (nominal). `.as_continuous()` -elevates continuous or integer targets to `f64` **losslessly** (integers in the f64- -representable range) and rejects nominal with a clear suggestion. +elevates continuous or integer targets to the model scalar `F` (exact while the value +is representable in `F`) and rejects nominal with a clear suggestion. *What we rejected:* Separate per-kind target arguments (forces the caller to know the kind up front) and forcing nominal text to be copied (we borrow the strings instead — spec @@ -370,17 +372,34 @@ signature lack targets makes it *impossible* to pass them (compile-time rejectio *What we rejected:* Reusing one fit trait with an optional/ignored target parameter — that would let callers pass meaningless targets and lose the type-level guarantee. -### `SKPredictor` (the prediction contract) +### `SKRegressorPredictor` (the regressor prediction contract) -*What it is:* A trait with an associated `Error` whose `predict(&self, features)` returns -`Result, Self::Error>` over the `TryInto` seam. +*What it is:* A trait generic over `F: SKFloat` with an associated `Error` whose +`predict(&self, features)` returns `Result, Self::Error>` over the +`TryInto` seam. *Why it exists:* Prediction lives on the **fitted model**, never on the configured estimator (spec `estimator-contracts`). That makes "predict before fit" a compile-time -error. `predict` returns a *dense* `Array1` — predicted scores or class indices. +error. `predict` returns a *dense* `Array1` — responses in the same scalar the model +was fitted on, so an `f32` pipeline never pays an `f64` detour. -*What we rejected:* Putting `predict` on the estimator type. That would break the "cannot -predict before fitting" guarantee and the shared model across threads. +*What we rejected:* A single `f64`-only `predict` for every model. It forced an upcast +on every `f32` prediction and a cast back to chain the next stage — and it encoded +class labels as floats (`0.0`/`1.0`), which is a lie of a type. + +### `SKClassifierPredictor` (the classifier prediction contract) + +*What it is:* A trait generic over `F: SKFloat` with an associated `Error` whose +`predict_labels(&self, features)` returns `Result, Self::Error>` +and whose `predict_probabilities(&self, features)` returns +`Result, Self::Error>`, both over the `TryInto` seam. + +*Why it exists:* Classifier outputs are labels, not floats. Labels are canonical `i64` +indices — independent of the feature scalar, so `f32` and `f64` runs agree exactly — +while probabilities preserve the model scalar `F`. + +*What we rejected:* Putting `predict_labels` on the estimator type. That would break the +"cannot predict before fitting" guarantee and the shared model across threads. ### `SKFeatureTransformer` (the feature-transformer contract) @@ -396,25 +415,35 @@ compatibility checking to runtime. Here is the whole family at a glance: -``` - sciencekit_common - | - +-----------------------+----------------------+ - | | - numbers data & errors - | | - SKFloat (sealed) <-- f32, f64 SKError (enum) - | | - +-- SKFloatSealed (private seal) SKDataView (Dense | Sparse) - SKTargetView (Continuous | Integer | Nominal) - | - +----------------+----------------+ - | | - SKSupervisedFit SKUnsupervisedFit - (features + targets) (features only) - | | - SKPredictor (on the model) (model type) - SKFeatureTransformer (transform) +```mermaid +flowchart TD + accTitle: The sciencekit_common vocabulary at a glance + accDescr: Numbers, data views and errors feed the fit traits on the estimator and the prediction and transform traits on the fitted model. + COMMON[sciencekit_common] --> NUM[numbers] + COMMON --> DATA[data and errors] + NUM --> FL[SKFloat sealed: f32, f64] + FL --> SEAL[SKFloatSealed private seal] + DATA --> ERR[SKError enum] + DATA --> DV[SKDataView: Dense or Sparse] + DATA --> TV[SKTargetView: Continuous, Integer or Nominal] + TV --> SF[SKSupervisedFit: features plus targets] + DV --> SF + DV --> UF[SKUnsupervisedFit: features only] + SF --> RP[SKRegressorPredictor on the model] + SF --> CP[SKClassifierPredictor on the model] + UF --> M[model type] + DV --> RP + DV --> CP + RP --> FT[SKFeatureTransformer: transform] + + style COMMON fill:#e0f2fe,stroke:#0284c7,color:#0c4a6e + style NUM fill:#ede9fe,stroke:#7c3aed,color:#4c1d95 + style DATA fill:#fef3c7,stroke:#d97706,color:#78350f + style SF fill:#f0fdf4,stroke:#16a34a,color:#14532d + style UF fill:#f0fdf4,stroke:#16a34a,color:#14532d + style RP fill:#f0fdf4,stroke:#16a34a,color:#14532d + style CP fill:#f0fdf4,stroke:#16a34a,color:#14532d + style FT fill:#f0fdf4,stroke:#16a34a,color:#14532d ``` --- @@ -561,26 +590,30 @@ the data views: ```mermaid flowchart TD accTitle: The fit, predict and transform contracts and their shared inputs - accDescr: The configured estimator carries SKSupervisedFit and SKUnsupervisedFit, which take features and targets and produce a fitted model. The fitted model carries SKPredictor and SKFeatureTransformer. All of them consume SKDataView and SKTargetView, and all report errors through SKError. + accDescr: The configured estimator carries SKSupervisedFit and SKUnsupervisedFit, which take features and targets and produce a fitted model. The fitted model carries SKRegressorPredictor for continuous responses in the model scalar, SKClassifierPredictor for scalar-independent integer labels plus scalar-preserving probabilities, and SKFeatureTransformer. All of them consume SKDataView and SKTargetView, and all report errors through SKError. subgraph est[Configured estimator - immutable, reusable] SF[SKSupervisedFit] -->|fit features + targets| M1(Model type) UF[SKUnsupervisedFit] -->|fit features only| M2(Model type) end subgraph model[Fitted model - sole bearer of learned state, thread-shareable] - P[SKPredictor] -->|predict features| OUT[Array1 f64] + RP[SKRegressorPredictor] -->|predict features| OUT[Array1 F] + CP[SKClassifierPredictor] -->|predict labels| OUTL[Array1 i64] + CP -->|predict probabilities| OUTP[Array2 F] FT[SKFeatureTransformer] -->|transform features| OUT2[Output type] end DV["SKDataView: Dense or Sparse"] --> SF DV --> UF - DV --> P + DV --> RP + DV --> CP DV --> FT TV["SKTargetView: Continuous, Integer or Nominal"] --> SF ER[SKError] --> SF ER --> UF - ER --> P + ER --> RP + ER --> CP ER --> FT style est fill:#e0f2fe,stroke:#0284c7,color:#0c4a6e @@ -596,10 +629,40 @@ from the estimator, and both depend only on the shared data views and the centra --- -## 7. Recap +## 7. Which float should I use? + +Both `f32` and `f64` satisfy `SKFloat`, and a model fitted on one predicts in the +same one — the scalar flows end to end with no implicit cast. The choice is a +trade-off: + +- **`f32`** halves memory traffic and doubles throughput on SIMD and GPU kernels, + which is what out-of-core and large-batch workloads want. Prefer it unless you + need the extra precision. +- **`f64`** keeps roughly 15–16 decimal digits of precision for ill-conditioned + solves and long accumulations. Prefer it when numerical stability dominates. + +A pipeline carries a single scalar: every stage agrees on `F`, so mixing an `f32` +stage into an `f64` pipeline fails at compile time until an explicit cast stage +bridges them. + +### Migration note + +Before this change, `predict` returned `Array1` and continuous targets were +`ArrayView1` regardless of the feature scalar. After it, regressor +predictions and continuous targets use `Array1`/`ArrayView1`, and classifier +labels are `Array1` instead of float-encoded indices. Call sites that assumed +`f64` update to the model scalar `F`; classifiers switch from comparing floats to +comparing label indices. + +--- + +## 8. Recap - **`SKFloat` is sealed** — only `f32` and `f64` are valid continuous numbers, enforced at compile time by a private supertrait (`private::SKFloatSealed`). +- **Predictions preserve the model scalar** — regressors return `Array1`, classifiers + return `Array1` labels plus `Array2` probabilities; continuous targets are + `SKTargetView::Continuous(ArrayView1)`. - **`SKError` is one precise taxonomy** — shape, unsupported representation, hyperparameter, execution mode, batch overflow, convergence, I/O, and conversion — built with `thiserror`, `#[non_exhaustive]`, and automatic `From` conversion into per-algorithm errors. @@ -609,7 +672,8 @@ from the estimator, and both depend only on the shared data views and the centra - **Public inputs are declared over `TryInto`** — any type that can convert into a view is accepted, making the library open to third-party integration without breaking. - **Fit, predict, and transform are separate contracts** — `SKSupervisedFit` and - `SKUnsupervisedFit` split by supervision; `SKPredictor` and `SKFeatureTransformer` live on + `SKUnsupervisedFit` split by supervision; `SKRegressorPredictor`, + `SKClassifierPredictor`, and `SKFeatureTransformer` live on the *model* type, making "predict before fit" a compile-time error. *Next chapter:* now that we have the shared vocabulary of numbers, errors, and data views, diff --git a/docs/src/development-explained/ch06-module-hygiene.md b/docs/src/development-explained/ch06-module-hygiene.md index a48070e..4619382 100644 --- a/docs/src/development-explained/ch06-module-hygiene.md +++ b/docs/src/development-explained/ch06-module-hygiene.md @@ -341,14 +341,14 @@ folder. It checks a design contract: a scorer has a "pure" form that scores from ```rust #[test] -fn pure_form_does_not_re_infer() { - let scorer = accuracy::(); - let truth = array![1.0_f64, 1.0, 0.0]; - let predictions = array![1.0_f64, 1.0, 1.0]; - let score = >::score_from_predictions( +fn label_pure_form_does_not_re_infer() { + let scorer = accuracy::(); + let truth = array![1_i64, 1, 0]; + let labels = array![1_i64, 1, 1]; + let score = >::score_from_labels( &scorer, SKTargetView::try_from(truth.view()).unwrap(), - predictions.view(), + labels.view(), ) .unwrap(); // 2 of 3 correct (predicted 1,1,1 vs true 1,1,0). @@ -356,15 +356,15 @@ fn pure_form_does_not_re_infer() { } ``` -The test sets up truth `[1,1,0]` and predictions `[1,1,1]`. A `ConstantPredictor` always -predicts 1.0, so 2 of the 3 predictions match the true labels. The score should be -2/3. We call the *pure* form directly, `score_from_predictions`, handing it both the -truth and the predictions — and check the result equals 2/3 (within floating-point +The test sets up truth `[1,1,0]` and labels `[1,1,1]`. A `ConstantClassifier` always +predicts label 1, so 2 of the 3 labels match the true labels. The score should be +2/3. We call the *pure* form directly, `score_from_labels`, handing it both the +truth and the labels — and check the result equals 2/3 (within floating-point tolerance, hence the `.abs() < 1e-9`). -The sibling test `convenient_form_runs_inference_and_delegates` then verifies the *other* -path: the convenient form runs the model once to get predictions, then delegates to the -same pure form — and the two scores match. Together, the two tests pin down that the +The sibling test `label_convenient_form_runs_inference_and_delegates` then verifies the +*other* path: the convenient form runs the model once to get labels, then delegates to +the same pure form — and the two scores match. Together, the two tests pin down that the pure form never re-infers and that both paths agree. All of this lives in a companion folder *beside* the scoring implementation, exactly as the hygiene rules demand. diff --git a/docs/src/index.md b/docs/src/index.md index 1c7fd4e..0de6bee 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -157,7 +157,7 @@ will run against the library itself.
Current status: Phase 0 (foundations) is complete — Cargo workspace, core traits -(SKEstimator, SKPredictor, SKTransformer), the pure-Rust faer math +(SKEstimator, SKRegressorPredictor/SKClassifierPredictor, SKTransformer), the pure-Rust faer math kernel, and the automatic execution-decision + observability layer have all landed. Next up is Phase 1 (preprocessing). Follow the full plan in Plan & Roadmap.
diff --git a/docs/src/tutorials/adding-a-linear-model.md b/docs/src/tutorials/adding-a-linear-model.md index a4684ee..c23a365 100644 --- a/docs/src/tutorials/adding-a-linear-model.md +++ b/docs/src/tutorials/adding-a-linear-model.md @@ -234,9 +234,9 @@ impl SKLinearRegression { `predict` on the model, honest minimum: ```rust -impl SKPredictor for SKLinearRegressionModel { +impl SKRegressorPredictor for SKLinearRegressionModel { type Error = SKError; - fn predict<'a, X>(&self, features: X) -> Result, Self::Error> + fn predict<'a, X>(&self, features: X) -> Result, Self::Error> where X: TryInto, Error = SKError> { let view = features.try_into()?.as_dense()?; let (rows, _cols) = view.dim(); @@ -297,7 +297,7 @@ equations got *kept* streaming but with the §2 caveat printed as a test contrac | Lots and little data | four-row exact fixture and the rank-deficient one — two shapes, one solver | | Under concurrency | model is `Sync` + `Copy`-safe; scoring under rayon exercised in the scorers chapter | | Model export | coefficients + intercept + design options serialize (export conventions live in the model chapter of the anchor) | -| Metrics | `SKSupervisedScorer` gives `score()` free — the R² chapter exercises it against these models | +| Metrics | `SKSupervisedScorer` (regressor) / `SKLabelScorer` (classifier) give `score()` free — the R² chapter exercises it against these models | **Chapter sources:** diff --git a/docs/src/tutorials/adding-a-neighbors-model.md b/docs/src/tutorials/adding-a-neighbors-model.md index 32452f9..334270f 100644 --- a/docs/src/tutorials/adding-a-neighbors-model.md +++ b/docs/src/tutorials/adding-a-neighbors-model.md @@ -109,9 +109,9 @@ citations to read the source before choosing: // crates/sciencekit_neighbors/src/k_neighbors/core_implementation.rs pub enum SKNeighborWeights { Uniform, Distance } -pub struct SKKNeighborsModel> { +pub struct SKKNeighborsModel> { base: S, // the training rows — the whole storage question - targets: ndarray::Array1, // owned targets (canonicalized indices stored as f64) + targets: ndarray::Array1, // owned continuous targets (regressor only) labels: Option, // classifier only — frozen, grow-only nearest_neighbors_count: usize, weights: SKNeighborWeights, @@ -121,7 +121,7 @@ pub struct SKKNeighborsModel> { execution_intent: SKExecutionMode, } -impl> SKKNeighborsModel { +impl> SKKNeighborsModel { pub fn execution_intent(&self) -> SKExecutionMode { self.execution_intent } } ``` @@ -141,13 +141,15 @@ Two notes before the code: ## 4. The crown code ```rust -impl SKPredictor for SKKNeighborsModel +// Regressor: responses preserve the model scalar. The classifier sibling +// implements SKClassifierPredictor instead, returning Array1 labels. +impl SKRegressorPredictor for SKKNeighborsModel where F: SKFloat, S: SKMappableSource + Sync, { type Error = SKError; - fn predict<'a, X>(&self, queries: X) -> Result, Self::Error> + fn predict<'a, X>(&self, queries: X) -> Result, Self::Error> where X: TryInto, Error = SKError> { let queries = queries.try_into()?.as_dense()?; sk_run_operation( @@ -168,7 +170,7 @@ where let plan = sk_resolve_execution_plan(self.execution_intent(), &context)?; let rows = queries.nrows(); - let mut out = ndarray::Array1::::zeros(rows); + let mut out = ndarray::Array1::::zeros(rows); // A per-query answer: every query row depends only on the base, // so there is no reduction and no shared accumulator — par_azip! is safe. @@ -188,12 +190,12 @@ where } // One query row: distance to every base row, then top-k selection, then reduce. -impl SKKNeighborsModel<...> { - fn answer_for_query( +impl> SKKNeighborsModel { + fn answer_for_query( &self, query: &ndarray::ArrayView1, parallelism: usize, - ) -> f64 { + ) -> F { // squared distances: x·x + b·b − 2·x·b, per-chunk over the mappable base let distances = sciencekit_math::pairwise::sk_squared_euclidean_distance_matrix( query.insert_axis(ndarray::Axis(0)), // 1 × p @@ -268,7 +270,7 @@ fn memory_mapped_base_matches_in_memory_base() { | Lots and little data | the memory-mapped regime *is* the "lots" story (§5 test 4) | | Under concurrency | chunked distance rows + per-query partitioning, `plan.parallelism` from the plan | | Model export | base path + label table + hyper-settings (see anchor §10; memmap is exactly what the PRD §4.4 asks for mmappable bases) | -| Metrics | classifier accuracy / regressor R² through `SKSupervisedScorer` (free, no new scorer) | +| Metrics | classifier accuracy through `SKLabelScorer` / regressor R² through `SKSupervisedScorer` (free, no new scorer) | **Chapter sources** (see §2.1's citations): Bentley, Cover & Hart, Muja & Lowe, the `sklearn.neighbors` docs and its `npy`-neighbour algorithm notes on the diff --git a/docs/src/tutorials/adding-a-streaming-model.md b/docs/src/tutorials/adding-a-streaming-model.md index db1a55f..449bb08 100644 --- a/docs/src/tutorials/adding-a-streaming-model.md +++ b/docs/src/tutorials/adding-a-streaming-model.md @@ -239,8 +239,8 @@ one batch in: Xᵦ, yᵦ → gradient(θ) ← (1/|B|) (Xᵦ θ − yᵦ)ᵀ Xᵦ The classifier's `predict` reduces probabilities across categories via the *same* label table — `argmax` over the class weights, then `SKLabelTable::label_of(index)` decodes the -integer back into its frozen label string. The regressor simply returns one `f64` per row — -matching the anchor §3.2 rule: `SKPredictor` always returns `Array1`. +integer back into its frozen label string. The regressor simply returns one `F` per row — +matching the anchor §3.2 rule: `SKRegressorPredictor` returns `Array1`. The structural rule this encodes: label lookups always go *through* the table, never an ad-hoc map in the model — §5's grow-only test is the enforcement. @@ -282,7 +282,7 @@ fn per_batch_partial_fit_spans() { | Lots and little data | §5 test 1 — literally the regimes-equivalence | | Under concurrency | gradient is parallel (partial accumulation), θ update sequential; `&self` fit; model `Sync` | | Model export | θ + clock + label table — plain fitted structure | -| Metrics | `SKSupervisedScorer` (R² for the regressor, accuracy for the classifier through the scorer chapter) | +| Metrics | `SKSupervisedScorer` (R² for the regressor) / `SKLabelScorer` (accuracy for the classifier through the scorer chapter) | Standing **study sources** (each also explained §2): diff --git a/docs/src/tutorials/how-to-add-an-algorithm.md b/docs/src/tutorials/how-to-add-an-algorithm.md index 4c74861..b1f12f7 100644 --- a/docs/src/tutorials/how-to-add-an-algorithm.md +++ b/docs/src/tutorials/how-to-add-an-algorithm.md @@ -7,7 +7,7 @@ recipe and refers to it by section number. > **Contract level.** Everything below is written against the merged master contracts of > `sciencekit_common` and `sciencekit_math` as of the `parallel-kernel-execution` wave: -> `SKBuilder`/`SKBuilderState`, `SKSupervisedFit`/`SKUnsupervisedFit`/`SKFeatureTransformer`/`SKPredictor`, +> `SKBuilder`/`SKBuilderState`, `SKSupervisedFit`/`SKUnsupervisedFit`/`SKFeatureTransformer`/`SKRegressorPredictor`/`SKClassifierPredictor`, > `SKDataView`/`SKTargetView`, `SKExecutionContext`/`sk_resolve_execution_plan`, > `sk_run_streaming_driver`, `SKLazySource`/`SKMappableSource`, the measured-grain kernels and > the `SKMathBackend` abstraction. When a real algorithm crate lands and any signature has @@ -36,7 +36,7 @@ flowchart TD PLN -->|"InProcess* | whole array"| K["measured-grain kernels
plan.parallelism threads"]:::regNode PLN -->|">RAM, sequential"| Dr["sk_run_streaming_driver
State + update() per batch"]:::regNode PLN -->|">RAM, RandomAccess"| Ma["SKMappableSource
memory-mapped rows"]:::regNode - K --> Ms["Model (distinct type)
SKPredictor /
transform verbs"]:::outsNode + K --> Ms["Model (distinct type)
predictor /
transform verbs"]:::outsNode Dr --> Ms Ma --> Ms Ms --> Chk["§8.7 acceptance + export + scorers
fmt · clippy · test · mdbook build"]:::outsNode @@ -91,7 +91,7 @@ Three hard rules from the conventions chapter: - **No abbreviations, ever**, with the single exception of the project prefix. The builder field is `maximum_number_of_iterations: usize`, not `max_iter`; the count of nearest neighbors is `nearest_neighbors_count`, not `k`. -- Structs and traits: `SK` + PascalCase → `SKStandardScaler`, `SKPredictor`. +- Structs and traits: `SK` + PascalCase → `SKStandardScaler`, `SKRegressorPredictor`. - Free public functions, public variables, public modules: `sk_` + snake_case → `sk_resolve_execution_plan`, `sk_axis_sum`. - **Methods get no prefix** — standing inside an `impl` is the privilege that earns it: @@ -158,17 +158,27 @@ Three details carry the design and are easy to miss in a skim: ```rust // Implemented on the MODEL, never on the estimator. -pub trait SKPredictor { +pub trait SKRegressorPredictor { type Error: From; - fn predict<'a, X>(&self, features: X) -> Result, Self::Error> + fn predict<'a, X>(&self, features: X) -> Result, Self::Error> + where + X: TryInto, Error = SKError>; +} + +pub trait SKClassifierPredictor { + type Error: From; + fn predict_labels<'a, X>(&self, features: X) -> Result, Self::Error> + where + X: TryInto, Error = SKError>; + fn predict_probabilities<'a, X>(&self, features: X) -> Result, Self::Error> where X: TryInto, Error = SKError>; } ``` -Note `predict` returns `Array1` regardless of the feature dtype `F` — predictions are -always the precision-maximal number (`SKTargetView::Continuous` is also `f64`-only, which is -how targets stay independent of feature dtype). +Note `predict` returns `Array1` in the model scalar — an `f32` model predicts +`f32` with no `f64` detour — while classifier labels are scalar-independent `i64` +indices (`SKTargetView::Continuous` is likewise generic over `F`). --- diff --git a/docs/src/vision.md b/docs/src/vision.md index f9c6161..211f21b 100644 --- a/docs/src/vision.md +++ b/docs/src/vision.md @@ -79,7 +79,8 @@ Three target audiences: ```rust,ignore // Core trait vocabulary — implemented by the sciencekit crates. pub trait SKEstimator { /* hyperparameters and fit */ } -pub trait SKPredictor: SKEstimator { /* predict */ } +pub trait SKRegressorPredictor: SKEstimator { /* predict -> Array1 */ } +pub trait SKClassifierPredictor: SKEstimator { /* labels -> Array1 */ } pub trait SKTransformer: SKEstimator { /* transform / fit_transform */ } pub trait SKDataSource { /* eager: full in-memory access */ } @@ -88,13 +89,13 @@ pub trait SKMappableSource { /* memmap: O(1) random access */ } ``` Algorithms compose exactly the traits they support: an `SKSGDClassifier` implements -`SKPredictor` + `SKLazySource`; an `SKStandardScaler` implements only `SKTransformer`. +`SKClassifierPredictor` + `SKLazySource`; an `SKStandardScaler` implements only `SKTransformer`. ## Interoperability - **Single internal format:** extended Safetensors — JSON header with hyperparameters, training state and recoverability metadata (checkpointing), with partial writes via sharding or header padding. -- **ONNX:** every estimator implements `SKToOnnx`; external models load as a generic `SKPredictor`. +- **ONNX:** every estimator implements `SKToOnnx`; external models load as a generic predictor. - **Data:** pluggable sources behind arbitrary I/O traits, with conversions for Polars and DataFusion. - **Observability:** `tracing` oriented to OpenTelemetry, disableable at near-zero cost.