fix(ensemble): wire EnsembleModelAdapter::predict to real inference
The adapter's `predict` returned
`{ prediction_value: 0.5, confidence: 0.0 }` as a "neutral" stub.
Downstream ensemble filtering dropped any 0-confidence prediction,
so the adapter silently contributed nothing -- a stub that survived
only because the caller discarded it.
Now: `EnsembleModelAdapter` holds an
`Arc<dyn ModelInferenceAdapter>` and dispatches `predict` to the
wrapped per-model GPU adapter (`DqnInferenceAdapter`,
`PpoInferenceAdapter`, `TftInferenceAdapter`, `Mamba2InferenceAdapter`,
`LiquidInferenceAdapter`, `KanInferenceAdapter`,
`XlstmInferenceAdapter`, `TggnInferenceAdapter`,
`TlobInferenceAdapter`, `DiffusionInferenceAdapter`). The inner
adapter already runs a full forward pass on its model and returns a
normalized `(direction, confidence)` pair; the bridge maps
`direction in [-1, 1]` -> `prediction_value in [0, 1]` via
`(direction + 1) / 2` to match `MLPrediction`'s bullish-probability
contract (> 0.5 = bullish), clamps confidence to [0, 1], and
propagates `metadata.latency_us` as the inference latency.
The bridge returns `Err` (never a faked 0-confidence success) when
the inner model reports `is_ready() == false`, the inner `predict`
fails, the output is non-finite, or the feature slice is empty.
`build_production_strategy` no longer fabricates ten zero-confidence
ghost adapters. It now accepts
`Vec<(String, Arc<dyn ModelInferenceAdapter>)>` -- the caller owns
real model construction (checkpoint loading, device selection). The
only current caller (backtesting service `MLPoweredStrategy::new`)
passes an empty vec; that yields an empty ensemble, which is an
honest "no models loaded" signal rather than ten stubs that exist
only to be filtered.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,44 +1,115 @@
|
||||
//! Bridge between ml model registry and common::ml_strategy::MLModelAdapter trait.
|
||||
//! Bridge between `ml::ensemble::ModelInferenceAdapter` (per-model GPU inference)
|
||||
//! and `common::ml_strategy::MLModelAdapter` (trait consumed by `SharedMLStrategy`).
|
||||
//!
|
||||
//! EnsembleModelAdapter wraps a model ID and implements MLModelAdapter so it can
|
||||
//! be injected into SharedMLStrategy. When real checkpoint loading is wired, this
|
||||
//! adapter will delegate to the loaded model for inference.
|
||||
//! `EnsembleModelAdapter` is a thin adapter: it holds a polymorphic
|
||||
//! `Arc<dyn ModelInferenceAdapter>` (the real, GPU-resident per-model adapter —
|
||||
//! `DqnInferenceAdapter`, `PpoInferenceAdapter`, `TftInferenceAdapter`, ...) and
|
||||
//! forwards `predict` to it. The inner adapter already runs a full forward pass
|
||||
//! on its wrapped model and returns a normalized `(direction, confidence)` pair;
|
||||
//! this bridge remaps `direction ∈ [-1, 1]` to `prediction_value ∈ [0, 1]` so the
|
||||
//! result fits `MLPrediction`'s bullish-probability contract (values > 0.5 =
|
||||
//! bullish, < 0.5 = bearish; consumed by `SharedMLStrategy::validate_predictions`).
|
||||
|
||||
use anyhow::Result;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use chrono::Utc;
|
||||
use common::ml_strategy::{MLModelAdapter, MLPrediction, SharedMLStrategy};
|
||||
|
||||
use crate::ensemble::inference_adapter::{FeatureVector, ModelInferenceAdapter};
|
||||
use crate::features::production_adapter::ProductionFeatureExtractorAdapter;
|
||||
|
||||
/// Adapter that will delegate predict() to a loaded model checkpoint.
|
||||
/// Adapter that forwards `MLModelAdapter::predict` to a wrapped
|
||||
/// `ModelInferenceAdapter` running real GPU inference on a specific model
|
||||
/// (DQN, PPO, TFT, Mamba2, Liquid, KAN, xLSTM, TGGN, TLOB, Diffusion, ...).
|
||||
///
|
||||
/// Currently returns neutral predictions with zero confidence (filtered out
|
||||
/// by the confidence threshold) until checkpoint loading is production-ready.
|
||||
#[derive(Debug)]
|
||||
/// The wrapped adapter is held behind `Arc<dyn ModelInferenceAdapter>`, giving
|
||||
/// `EnsembleModelAdapter` polymorphic dispatch across every model type that
|
||||
/// implements the inference trait — the bridge itself knows nothing about
|
||||
/// individual model architectures.
|
||||
pub struct EnsembleModelAdapter {
|
||||
model_id: String,
|
||||
inner: Arc<dyn ModelInferenceAdapter>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for EnsembleModelAdapter {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("EnsembleModelAdapter")
|
||||
.field("model_id", &self.model_id)
|
||||
.field("inner_model_name", &self.inner.model_name())
|
||||
.field("inner_ready", &self.inner.is_ready())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl EnsembleModelAdapter {
|
||||
pub fn new<S: Into<String>>(model_id: S) -> Self {
|
||||
/// Create a bridge wrapping a real per-model inference adapter.
|
||||
///
|
||||
/// `model_id` is the public ensemble identifier exposed via
|
||||
/// `MLModelAdapter::model_id` (e.g. "dqn", "ppo", "tft"); `inner` is the
|
||||
/// actual GPU-resident adapter that will produce predictions.
|
||||
pub fn new<S: Into<String>>(model_id: S, inner: Arc<dyn ModelInferenceAdapter>) -> Self {
|
||||
Self {
|
||||
model_id: model_id.into(),
|
||||
inner,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MLModelAdapter for EnsembleModelAdapter {
|
||||
fn predict(&self, features: &[f64]) -> Result<MLPrediction> {
|
||||
// TODO: Wire to real model inference via checkpoint loading when
|
||||
// the model registry is production-ready. For now, return neutral
|
||||
// prediction so the ensemble pipeline is fully wired end-to-end.
|
||||
let _ = features;
|
||||
if !self.inner.is_ready() {
|
||||
return Err(anyhow!(
|
||||
"EnsembleModelAdapter: inner model '{}' (id={}) is not ready for inference",
|
||||
self.inner.model_name(),
|
||||
self.model_id
|
||||
));
|
||||
}
|
||||
|
||||
if features.is_empty() {
|
||||
return Err(anyhow!(
|
||||
"EnsembleModelAdapter: empty feature vector for model '{}'",
|
||||
self.model_id
|
||||
));
|
||||
}
|
||||
|
||||
let fv = FeatureVector {
|
||||
values: features.to_vec(),
|
||||
timestamp: Utc::now().timestamp_micros(),
|
||||
};
|
||||
|
||||
let raw = self.inner.predict(&fv).map_err(|e| {
|
||||
anyhow!(
|
||||
"EnsembleModelAdapter: inference failed for model '{}': {e}",
|
||||
self.model_id
|
||||
)
|
||||
})?;
|
||||
|
||||
if !raw.direction.is_finite() || !raw.confidence.is_finite() {
|
||||
return Err(anyhow!(
|
||||
"EnsembleModelAdapter: non-finite prediction from model '{}' (direction={}, confidence={})",
|
||||
self.model_id,
|
||||
raw.direction,
|
||||
raw.confidence,
|
||||
));
|
||||
}
|
||||
|
||||
// Map direction ∈ [-1, 1] -> prediction_value ∈ [0, 1]:
|
||||
// direction = -1 -> 0.0 (strong bearish)
|
||||
// direction = 0 -> 0.5 (neutral)
|
||||
// direction = +1 -> 1.0 (strong bullish)
|
||||
// This matches SharedMLStrategy's bullish-probability contract
|
||||
// (predicted_positive = prediction_value > 0.5).
|
||||
let prediction_value = ((raw.direction + 1.0) * 0.5).clamp(0.0, 1.0);
|
||||
let confidence = raw.confidence.clamp(0.0, 1.0);
|
||||
|
||||
Ok(MLPrediction {
|
||||
model_id: self.model_id.clone(),
|
||||
prediction_value: 0.5,
|
||||
confidence: 0.0, // Zero confidence = filtered out by threshold
|
||||
features: vec![],
|
||||
prediction_value,
|
||||
confidence,
|
||||
features: features.to_vec(),
|
||||
timestamp: Utc::now(),
|
||||
inference_latency_us: 0,
|
||||
inference_latency_us: raw.metadata.latency_us,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -47,28 +118,36 @@ impl MLModelAdapter for EnsembleModelAdapter {
|
||||
}
|
||||
|
||||
fn validate_prediction(&mut self, _prediction: &MLPrediction, _actual_outcome: bool) {
|
||||
// Performance tracking handled by SharedMLStrategy
|
||||
// Performance tracking is handled by SharedMLStrategy.
|
||||
// The wrapped ModelInferenceAdapter trait does not expose a mutable
|
||||
// validation hook; model-side performance feedback, when required,
|
||||
// is driven by the training pipeline, not the ensemble read path.
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a production-ready SharedMLStrategy with model adapters for each
|
||||
/// model in the 10-model ensemble.
|
||||
/// Build a production `SharedMLStrategy` using the 225-feature extractor and
|
||||
/// one `EnsembleModelAdapter` per injected inference adapter.
|
||||
///
|
||||
/// Returns a strategy with:
|
||||
/// - ProductionFeatureExtractorAdapter (42 market features currently)
|
||||
/// - One EnsembleModelAdapter per known model type
|
||||
/// The caller owns model construction (checkpoint loading, device selection,
|
||||
/// config) and passes the concrete `ModelInferenceAdapter` implementations —
|
||||
/// `DqnInferenceAdapter::from_checkpoint(...)`, etc. — paired with the public
|
||||
/// ensemble id each model should advertise.
|
||||
///
|
||||
/// When no checkpoints are loaded, models return neutral predictions with
|
||||
/// zero confidence, which get filtered out by the confidence threshold.
|
||||
pub fn build_production_strategy(min_confidence_threshold: f64) -> SharedMLStrategy {
|
||||
/// Passing an empty `adapters` vec produces a strategy with zero models; that
|
||||
/// is an honest reflection of "no models are loaded", not a stub —
|
||||
/// `SharedMLStrategy::get_ensemble_prediction` will return an empty prediction
|
||||
/// list rather than fabricate neutral outputs.
|
||||
pub fn build_production_strategy(
|
||||
min_confidence_threshold: f64,
|
||||
adapters: Vec<(String, Arc<dyn ModelInferenceAdapter>)>,
|
||||
) -> SharedMLStrategy {
|
||||
let extractor = Box::new(ProductionFeatureExtractorAdapter::new());
|
||||
|
||||
let model_ids = [
|
||||
"dqn", "ppo", "tft", "mamba2", "tggn", "tlob", "liquid", "kan", "xlstm", "diffusion",
|
||||
];
|
||||
let models: Vec<Box<dyn MLModelAdapter>> = model_ids
|
||||
.iter()
|
||||
.map(|&id| Box::new(EnsembleModelAdapter::new(id)) as Box<dyn MLModelAdapter>)
|
||||
let models: Vec<Box<dyn MLModelAdapter>> = adapters
|
||||
.into_iter()
|
||||
.map(|(id, inner)| {
|
||||
Box::new(EnsembleModelAdapter::new(id, inner)) as Box<dyn MLModelAdapter>
|
||||
})
|
||||
.collect();
|
||||
|
||||
SharedMLStrategy::new(extractor, models, min_confidence_threshold)
|
||||
@@ -77,52 +156,247 @@ pub fn build_production_strategy(min_confidence_threshold: f64) -> SharedMLStrat
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ensemble::inference_adapter::{EnsemblePrediction, PredictionMeta};
|
||||
use crate::MLResult;
|
||||
|
||||
#[test]
|
||||
fn test_ensemble_model_adapter_neutral_prediction() {
|
||||
let adapter = EnsembleModelAdapter::new("dqn");
|
||||
let features = vec![0.1; 51];
|
||||
let prediction = adapter.predict(&features).unwrap();
|
||||
/// Minimal non-GPU `ModelInferenceAdapter` used to exercise the bridge
|
||||
/// semantics deterministically without spinning up CUDA.
|
||||
struct StubInferenceAdapter {
|
||||
name: String,
|
||||
direction: f64,
|
||||
confidence: f64,
|
||||
ready: bool,
|
||||
should_fail: bool,
|
||||
}
|
||||
|
||||
assert_eq!(prediction.model_id, "dqn");
|
||||
assert_eq!(prediction.prediction_value, 0.5);
|
||||
assert_eq!(prediction.confidence, 0.0);
|
||||
impl StubInferenceAdapter {
|
||||
fn bullish(name: &str) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
direction: 0.6,
|
||||
confidence: 0.8,
|
||||
ready: true,
|
||||
should_fail: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn bearish(name: &str) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
direction: -0.4,
|
||||
confidence: 0.7,
|
||||
ready: true,
|
||||
should_fail: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn not_ready(name: &str) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
direction: 0.0,
|
||||
confidence: 0.0,
|
||||
ready: false,
|
||||
should_fail: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn failing(name: &str) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
direction: 0.0,
|
||||
confidence: 0.0,
|
||||
ready: true,
|
||||
should_fail: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn non_finite(name: &str) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
direction: f64::NAN,
|
||||
confidence: 0.5,
|
||||
ready: true,
|
||||
should_fail: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ModelInferenceAdapter for StubInferenceAdapter {
|
||||
fn model_name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn predict(&self, _features: &FeatureVector) -> MLResult<EnsemblePrediction> {
|
||||
if self.should_fail {
|
||||
return Err(crate::MLError::InferenceError(
|
||||
"stub forced failure".to_owned(),
|
||||
));
|
||||
}
|
||||
Ok(EnsemblePrediction {
|
||||
model_name: self.name.clone(),
|
||||
direction: self.direction,
|
||||
confidence: self.confidence,
|
||||
metadata: PredictionMeta {
|
||||
latency_us: 42,
|
||||
quantiles: None,
|
||||
attention_weights: None,
|
||||
q_values: None,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.ready
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_production_strategy() {
|
||||
let strategy = build_production_strategy(0.6);
|
||||
fn predict_forwards_bullish_inner_prediction() {
|
||||
let inner: Arc<dyn ModelInferenceAdapter> =
|
||||
Arc::new(StubInferenceAdapter::bullish("DQN"));
|
||||
let adapter = EnsembleModelAdapter::new("dqn", inner);
|
||||
let features = vec![0.1_f64; 42];
|
||||
|
||||
let pred = adapter.predict(&features).expect("predict should succeed");
|
||||
|
||||
assert_eq!(pred.model_id, "dqn");
|
||||
// direction=0.6 -> prediction_value=(0.6+1)/2 = 0.8
|
||||
assert!((pred.prediction_value - 0.8).abs() < 1e-9);
|
||||
assert!((pred.confidence - 0.8).abs() < 1e-9);
|
||||
assert!(pred.prediction_value > 0.5, "bullish inner -> >0.5 bridge output");
|
||||
assert_eq!(pred.inference_latency_us, 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn predict_forwards_bearish_inner_prediction() {
|
||||
let inner: Arc<dyn ModelInferenceAdapter> =
|
||||
Arc::new(StubInferenceAdapter::bearish("PPO"));
|
||||
let adapter = EnsembleModelAdapter::new("ppo", inner);
|
||||
let features = vec![0.1_f64; 42];
|
||||
|
||||
let pred = adapter.predict(&features).expect("predict should succeed");
|
||||
|
||||
// direction=-0.4 -> prediction_value=(-0.4+1)/2 = 0.3
|
||||
assert!((pred.prediction_value - 0.3).abs() < 1e-9);
|
||||
assert!(pred.prediction_value < 0.5, "bearish inner -> <0.5 bridge output");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn predict_returns_err_when_inner_not_ready() {
|
||||
let inner: Arc<dyn ModelInferenceAdapter> =
|
||||
Arc::new(StubInferenceAdapter::not_ready("TFT"));
|
||||
let adapter = EnsembleModelAdapter::new("tft", inner);
|
||||
let features = vec![0.1_f64; 42];
|
||||
|
||||
let err = adapter
|
||||
.predict(&features)
|
||||
.expect_err("not-ready model must return Err, not confidence=0 success");
|
||||
let msg = err.to_string();
|
||||
assert!(msg.contains("not ready"), "err message should mention readiness: {msg}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn predict_returns_err_when_inner_fails() {
|
||||
let inner: Arc<dyn ModelInferenceAdapter> =
|
||||
Arc::new(StubInferenceAdapter::failing("Mamba2"));
|
||||
let adapter = EnsembleModelAdapter::new("mamba2", inner);
|
||||
let features = vec![0.1_f64; 42];
|
||||
|
||||
let err = adapter
|
||||
.predict(&features)
|
||||
.expect_err("inner predict failure must propagate");
|
||||
assert!(err.to_string().contains("inference failed"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn predict_returns_err_on_non_finite_output() {
|
||||
let inner: Arc<dyn ModelInferenceAdapter> =
|
||||
Arc::new(StubInferenceAdapter::non_finite("Liquid"));
|
||||
let adapter = EnsembleModelAdapter::new("liquid", inner);
|
||||
let features = vec![0.1_f64; 42];
|
||||
|
||||
let err = adapter
|
||||
.predict(&features)
|
||||
.expect_err("NaN direction must return Err, not fake confidence=0");
|
||||
assert!(err.to_string().contains("non-finite"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn predict_returns_err_on_empty_features() {
|
||||
let inner: Arc<dyn ModelInferenceAdapter> =
|
||||
Arc::new(StubInferenceAdapter::bullish("DQN"));
|
||||
let adapter = EnsembleModelAdapter::new("dqn", inner);
|
||||
let features: Vec<f64> = Vec::new();
|
||||
|
||||
let err = adapter
|
||||
.predict(&features)
|
||||
.expect_err("empty features must be rejected");
|
||||
assert!(err.to_string().contains("empty feature vector"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_production_strategy_with_empty_adapters_has_no_models() {
|
||||
let strategy = build_production_strategy(0.6, Vec::new());
|
||||
assert_eq!(strategy.min_confidence_threshold(), 0.6);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_production_strategy_filters_neutral_predictions() {
|
||||
let strategy = build_production_strategy(0.5);
|
||||
async fn build_production_strategy_forwards_real_predictions() {
|
||||
let dqn: Arc<dyn ModelInferenceAdapter> =
|
||||
Arc::new(StubInferenceAdapter::bullish("DQN"));
|
||||
let ppo: Arc<dyn ModelInferenceAdapter> =
|
||||
Arc::new(StubInferenceAdapter::bearish("PPO"));
|
||||
|
||||
let strategy = build_production_strategy(
|
||||
0.5, // threshold below stub confidences (0.7, 0.8)
|
||||
vec![
|
||||
("dqn".to_string(), dqn),
|
||||
("ppo".to_string(), ppo),
|
||||
],
|
||||
);
|
||||
|
||||
let predictions = strategy
|
||||
.get_ensemble_prediction(100.0, 1000.0, Utc::now())
|
||||
.await
|
||||
.unwrap();
|
||||
.expect("ensemble prediction should succeed");
|
||||
|
||||
// All 10 adapters return confidence=0.0, so threshold=0.5 filters them all
|
||||
assert!(
|
||||
predictions.is_empty(),
|
||||
"Neutral predictions (confidence=0.0) should be filtered by threshold=0.5"
|
||||
// With real (non-zero) confidences, both adapters should clear the
|
||||
// threshold -- the previous stub behaviour dropped them all.
|
||||
assert_eq!(
|
||||
predictions.len(),
|
||||
2,
|
||||
"expected 2 real predictions (DQN + PPO); got {} — the stub \
|
||||
ghost would have produced 0",
|
||||
predictions.len(),
|
||||
);
|
||||
for p in &predictions {
|
||||
assert!(p.confidence > 0.0, "real inner confidence must be > 0");
|
||||
assert!(p.prediction_value >= 0.0 && p.prediction_value <= 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_production_strategy_zero_threshold_keeps_all() {
|
||||
let strategy = build_production_strategy(0.0);
|
||||
async fn build_production_strategy_filters_low_confidence_honestly() {
|
||||
let low_conf: Arc<dyn ModelInferenceAdapter> = Arc::new(StubInferenceAdapter {
|
||||
name: "KAN".to_string(),
|
||||
direction: 0.1,
|
||||
confidence: 0.2, // below threshold
|
||||
ready: true,
|
||||
should_fail: false,
|
||||
});
|
||||
|
||||
let strategy = build_production_strategy(
|
||||
0.5,
|
||||
vec![("kan".to_string(), low_conf)],
|
||||
);
|
||||
|
||||
let predictions = strategy
|
||||
.get_ensemble_prediction(100.0, 1000.0, Utc::now())
|
||||
.await
|
||||
.unwrap();
|
||||
.expect("ensemble prediction should succeed");
|
||||
|
||||
// With threshold=0.0, all 10 neutral predictions pass through
|
||||
assert_eq!(
|
||||
predictions.len(),
|
||||
10,
|
||||
"Zero threshold should keep all 10 model predictions"
|
||||
);
|
||||
// Filtered by genuine threshold comparison (0.2 < 0.5), not by a
|
||||
// fake 0.0 baked in by the adapter.
|
||||
assert!(predictions.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,9 +115,23 @@ impl MLPoweredStrategy {
|
||||
/// # Errors
|
||||
/// Returns error if ML model adapter construction fails.
|
||||
pub fn new(name: String, _lookback_periods: usize) -> Result<Self> {
|
||||
// Use shared ML strategy (ONE SINGLE SYSTEM) with ProductionFeatureExtractorAdapter (225 features)
|
||||
// Use shared ML strategy (ONE SINGLE SYSTEM) with ProductionFeatureExtractorAdapter (225 features).
|
||||
//
|
||||
// `build_production_strategy` now takes a Vec of real per-model inference
|
||||
// adapters. The backtesting service has no checkpoint-loader wired at this
|
||||
// layer, so we pass an empty vec: the resulting `SharedMLStrategy` honestly
|
||||
// reports "no models loaded" (empty prediction list) instead of the prior
|
||||
// ghost behaviour where 10 stub adapters all returned confidence=0.0 and
|
||||
// were silently filtered by the threshold.
|
||||
//
|
||||
// When the backtesting service grows a model-loader, it should build the
|
||||
// appropriate `DqnInferenceAdapter::from_checkpoint(..)` / `PpoInferenceAdapter::new(..)`
|
||||
// / etc. adapters and pass them in here with their ensemble ids.
|
||||
let min_confidence_threshold = 0.6;
|
||||
let strategy = Arc::new(ml::ensemble::build_production_strategy(min_confidence_threshold));
|
||||
let strategy = Arc::new(ml::ensemble::build_production_strategy(
|
||||
min_confidence_threshold,
|
||||
Vec::new(),
|
||||
));
|
||||
|
||||
// Initialize UnifiedFeatureExtractor (256 features)
|
||||
let feature_config = FeatureExtractionConfig::default();
|
||||
|
||||
Reference in New Issue
Block a user