robust-adapt 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- robust_adapt/__init__.py +265 -0
- robust_adapt/ala/__init__.py +54 -0
- robust_adapt/ala/adwin.py +243 -0
- robust_adapt/ala/classifier.py +544 -0
- robust_adapt/ala/concept_memory.py +457 -0
- robust_adapt/ala/delayed_labels.py +206 -0
- robust_adapt/ala/detector.py +297 -0
- robust_adapt/ala/drift_classifier.py +222 -0
- robust_adapt/ala/ensemble.py +103 -0
- robust_adapt/ala/replay.py +285 -0
- robust_adapt/ala/tree.py +433 -0
- robust_adapt/backends/__init__.py +25 -0
- robust_adapt/backends/base.py +42 -0
- robust_adapt/backends/numpy_backend.py +37 -0
- robust_adapt/backends/torch_backend.py +51 -0
- robust_adapt/core/__init__.py +121 -0
- robust_adapt/core/config.py +71 -0
- robust_adapt/core/constants.py +27 -0
- robust_adapt/core/diagnostics.py +169 -0
- robust_adapt/core/exceptions.py +56 -0
- robust_adapt/core/telemetry.py +92 -0
- robust_adapt/core/types.py +54 -0
- robust_adapt/core/validation.py +114 -0
- robust_adapt/integrations/__init__.py +50 -0
- robust_adapt/integrations/data.py +251 -0
- robust_adapt/integrations/sklearn.py +351 -0
- robust_adapt/integrations/torch_callback.py +157 -0
- robust_adapt/integrations/torch_module.py +125 -0
- robust_adapt/integrations/torch_optim.py +263 -0
- robust_adapt/minority/__init__.py +26 -0
- robust_adapt/minority/calibration.py +264 -0
- robust_adapt/minority/learner.py +210 -0
- robust_adapt/minority/prior.py +291 -0
- robust_adapt/minority/replay.py +303 -0
- robust_adapt/minority/threshold.py +454 -0
- robust_adapt/py.typed +1 -0
- robust_adapt/rao/__init__.py +42 -0
- robust_adapt/rao/accumulator.py +79 -0
- robust_adapt/rao/adamw.py +366 -0
- robust_adapt/rao/budget.py +159 -0
- robust_adapt/rao/device.py +72 -0
- robust_adapt/rao/memory.py +45 -0
- robust_adapt/rao/profiler.py +87 -0
- robust_adapt/rao/quantizer.py +163 -0
- robust_adapt/rao/recovery.py +178 -0
- robust_adapt/rla/__init__.py +48 -0
- robust_adapt/rla/label_correction.py +135 -0
- robust_adapt/rla/losses.py +293 -0
- robust_adapt/rla/noise_classifier.py +185 -0
- robust_adapt/rla/pipeline.py +272 -0
- robust_adapt/rla/reliability.py +342 -0
- robust_adapt/rla/scorer.py +159 -0
- robust_adapt/rla/weighting.py +64 -0
- robust_adapt/state/__init__.py +10 -0
- robust_adapt/state/checkpoint.py +109 -0
- robust_adapt/state/serializer.py +243 -0
- robust_adapt/ulc/__init__.py +66 -0
- robust_adapt/ulc/arbitrator.py +79 -0
- robust_adapt/ulc/automaton.py +167 -0
- robust_adapt/ulc/confidence.py +66 -0
- robust_adapt/ulc/conflict_resolver.py +280 -0
- robust_adapt/ulc/controller.py +196 -0
- robust_adapt/ulc/directives.py +42 -0
- robust_adapt/ulc/evidence.py +115 -0
- robust_adapt/ulc/explainer.py +62 -0
- robust_adapt/ulc/spatial.py +80 -0
- robust_adapt/ulc/v4_1_controller.py +536 -0
- robust_adapt/ulc/v4_automaton.py +307 -0
- robust_adapt/ulc/v4_controller.py +250 -0
- robust_adapt/ulc/v4_evidence.py +118 -0
- robust_adapt/ulc/v5_controller.py +403 -0
- robust_adapt-1.0.0.dist-info/METADATA +568 -0
- robust_adapt-1.0.0.dist-info/RECORD +77 -0
- robust_adapt-1.0.0.dist-info/WHEEL +5 -0
- robust_adapt-1.0.0.dist-info/licenses/LICENSE +176 -0
- robust_adapt-1.0.0.dist-info/licenses/NOTICE +13 -0
- robust_adapt-1.0.0.dist-info/top_level.txt +1 -0
robust_adapt/__init__.py
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""ROBUST-ADAPT: Robust and Adaptive Learning with Resource-Efficient Optimization.
|
|
2
|
+
|
|
3
|
+
Adaptive. Robust. Resource-Efficient.
|
|
4
|
+
|
|
5
|
+
An open-source Python machine-learning algorithm library designed to improve learning
|
|
6
|
+
under changing data distributions, unreliable training data and computational constraints.
|
|
7
|
+
It complements existing machine-learning frameworks rather than replacing them.
|
|
8
|
+
|
|
9
|
+
Four Independent Pillars:
|
|
10
|
+
- ALA: Adaptive Learning Algorithms (concept drift detection, streaming decision trees, balanced replay)
|
|
11
|
+
- RLA: Robust Learning Algorithms (sample reliability scoring, robust loss portfolio, label correction)
|
|
12
|
+
- RAO: Resource-Aware Optimization (8-bit block-quantized AdamW, multi-dimensional budget management)
|
|
13
|
+
- ULC: Unified Learning Controller (cross-pillar Pareto arbitration, ULC-v4.1/v5 hybrid controller)
|
|
14
|
+
|
|
15
|
+
Additional Capability:
|
|
16
|
+
- Minority: Adaptive Minority Intelligence (online prior tracking, cost-sensitive threshold adjustment)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
# Package version
|
|
22
|
+
__version__ = "1.0.0"
|
|
23
|
+
|
|
24
|
+
# Core Exceptions & Configurations
|
|
25
|
+
from robust_adapt.core.exceptions import (
|
|
26
|
+
AdaptiveMLError,
|
|
27
|
+
ConfigurationError,
|
|
28
|
+
DriftStateError,
|
|
29
|
+
NumericalInstabilityError,
|
|
30
|
+
QuantizationError,
|
|
31
|
+
ResourceBudgetExceededError,
|
|
32
|
+
RobustAdaptError,
|
|
33
|
+
SerializationError,
|
|
34
|
+
ValidationError,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# 1. Adaptive Learning Algorithms (ALA)
|
|
38
|
+
from robust_adapt.ala.adwin import ReliabilityWeightedADWIN
|
|
39
|
+
from robust_adapt.ala.classifier import ALA30AdaptiveLearner, AdaptiveClassifier
|
|
40
|
+
from robust_adapt.ala.concept_memory import (
|
|
41
|
+
ConceptFingerprinter,
|
|
42
|
+
ConceptMemory,
|
|
43
|
+
ConceptSignature,
|
|
44
|
+
ConceptSnapshot,
|
|
45
|
+
RecurringConceptMemory,
|
|
46
|
+
)
|
|
47
|
+
from robust_adapt.ala.delayed_labels import DelayedLabelQueue
|
|
48
|
+
from robust_adapt.ala.detector import (
|
|
49
|
+
BaseDriftDetector,
|
|
50
|
+
CUSUMDetector,
|
|
51
|
+
MultiDetectorConsensus,
|
|
52
|
+
PageHinkleyDetector,
|
|
53
|
+
)
|
|
54
|
+
from robust_adapt.ala.drift_classifier import (
|
|
55
|
+
DriftAnalysisResult,
|
|
56
|
+
DriftClassifier,
|
|
57
|
+
DriftNature,
|
|
58
|
+
DriftType,
|
|
59
|
+
)
|
|
60
|
+
from robust_adapt.ala.replay import ClassBalancedReservoirReplay
|
|
61
|
+
from robust_adapt.ala.tree import VectorizedHoeffdingTree
|
|
62
|
+
|
|
63
|
+
# Canonical ergonomic aliases
|
|
64
|
+
ADWIN = ReliabilityWeightedADWIN
|
|
65
|
+
HoeffdingTree = VectorizedHoeffdingTree
|
|
66
|
+
|
|
67
|
+
# 2. Robust Learning Algorithms (RLA)
|
|
68
|
+
from robust_adapt.rla.label_correction import (
|
|
69
|
+
CorrectionAuditRecord,
|
|
70
|
+
EvidenceBasedLabelCorrector,
|
|
71
|
+
)
|
|
72
|
+
from robust_adapt.rla.losses import (
|
|
73
|
+
FocalLoss,
|
|
74
|
+
GeneralizedCrossEntropyLoss,
|
|
75
|
+
ReliabilityWeightedLoss,
|
|
76
|
+
SymmetricCrossEntropyLoss,
|
|
77
|
+
)
|
|
78
|
+
from robust_adapt.rla.noise_classifier import (
|
|
79
|
+
NoiseTaxonomyClassifier,
|
|
80
|
+
NoiseType,
|
|
81
|
+
PatternType,
|
|
82
|
+
)
|
|
83
|
+
from robust_adapt.rla.pipeline import RLA30RobustLearner, RobustLearningPipeline
|
|
84
|
+
from robust_adapt.rla.reliability import (
|
|
85
|
+
AsymmetricTransitionDetector,
|
|
86
|
+
ClassConditionalReliabilityEstimator,
|
|
87
|
+
MultiSignalReliabilityEngine,
|
|
88
|
+
)
|
|
89
|
+
from robust_adapt.rla.scorer import DensityAwareMarginScorer
|
|
90
|
+
from robust_adapt.rla.weighting import ClassBalancedWeights
|
|
91
|
+
|
|
92
|
+
# 3. Resource-Aware Optimization (RAO)
|
|
93
|
+
from robust_adapt.rao.accumulator import AdaptiveGradientAccumulator
|
|
94
|
+
from robust_adapt.rao.budget import BudgetState, DynamicResourceBudgetManager
|
|
95
|
+
from robust_adapt.rao.device import DeviceCapabilityDetector, DeviceProfile
|
|
96
|
+
from robust_adapt.rao.profiler import ExecutionProfiler
|
|
97
|
+
from robust_adapt.rao.quantizer import BlockQuantizer
|
|
98
|
+
from robust_adapt.rao.recovery import ResilientCheckpointManager
|
|
99
|
+
|
|
100
|
+
# 4. Adaptive Minority Intelligence
|
|
101
|
+
from robust_adapt.minority.calibration import ClassAwareCalibrator
|
|
102
|
+
from robust_adapt.minority.learner import MinorityIntelligence20Learner
|
|
103
|
+
from robust_adapt.minority.prior import (
|
|
104
|
+
BetaBinomialPriorTracker,
|
|
105
|
+
OnlineClassPriorTracker,
|
|
106
|
+
)
|
|
107
|
+
from robust_adapt.minority.replay import MinorityAwareReplayBuffer
|
|
108
|
+
from robust_adapt.minority.threshold import (
|
|
109
|
+
AdaptiveThresholdManager,
|
|
110
|
+
DynamicFPBudgetThresholdManager,
|
|
111
|
+
compute_sublinear_cost_weights,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# 5. Unified Learning Controller (ULC)
|
|
115
|
+
from robust_adapt.ulc.arbitrator import MultiObjectiveArbitrator
|
|
116
|
+
from robust_adapt.ulc.confidence import ConfidenceEngine, ConfidenceScores
|
|
117
|
+
from robust_adapt.ulc.conflict_resolver import CrossPillarConflictResolver
|
|
118
|
+
from robust_adapt.ulc.controller import AdaptiveLearningController
|
|
119
|
+
from robust_adapt.ulc.directives import ControllerDirective, ControllerState
|
|
120
|
+
from robust_adapt.ulc.spatial import compute_spatial_discordance
|
|
121
|
+
from robust_adapt.ulc.v4_automaton import (
|
|
122
|
+
ArbitrationAutomatonV4,
|
|
123
|
+
AutomatonStateV4,
|
|
124
|
+
DirectiveV4,
|
|
125
|
+
)
|
|
126
|
+
from robust_adapt.ulc.v4_controller import UnifiedLearningControllerV4
|
|
127
|
+
from robust_adapt.ulc.v4_evidence import EvidenceSynthesizerV4, EvidenceVectorV4
|
|
128
|
+
from robust_adapt.ulc.v4_1_controller import ULCv41Config, UnifiedLearningControllerV4_1
|
|
129
|
+
from robust_adapt.ulc.v5_controller import (
|
|
130
|
+
InterventionTier,
|
|
131
|
+
ULCv5Config,
|
|
132
|
+
ULCv5Directive,
|
|
133
|
+
UnifiedLearningControllerV5,
|
|
134
|
+
UtilityScorer,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# 6. State Management & Checkpointing
|
|
138
|
+
from robust_adapt.state.checkpoint import CheckpointManager
|
|
139
|
+
from robust_adapt.state.serializer import is_safetensors_available, load_state, save_state
|
|
140
|
+
|
|
141
|
+
__all__ = [
|
|
142
|
+
# Metadata
|
|
143
|
+
"__version__",
|
|
144
|
+
# Core Exceptions
|
|
145
|
+
"RobustAdaptError",
|
|
146
|
+
"AdaptiveMLError",
|
|
147
|
+
"ConfigurationError",
|
|
148
|
+
"ValidationError",
|
|
149
|
+
"DriftStateError",
|
|
150
|
+
"QuantizationError",
|
|
151
|
+
"ResourceBudgetExceededError",
|
|
152
|
+
"NumericalInstabilityError",
|
|
153
|
+
"SerializationError",
|
|
154
|
+
# ALA
|
|
155
|
+
"AdaptiveClassifier",
|
|
156
|
+
"ALA30AdaptiveLearner",
|
|
157
|
+
"HoeffdingTree",
|
|
158
|
+
"VectorizedHoeffdingTree",
|
|
159
|
+
"ADWIN",
|
|
160
|
+
"ReliabilityWeightedADWIN",
|
|
161
|
+
"BaseDriftDetector",
|
|
162
|
+
"CUSUMDetector",
|
|
163
|
+
"PageHinkleyDetector",
|
|
164
|
+
"MultiDetectorConsensus",
|
|
165
|
+
"DriftClassifier",
|
|
166
|
+
"DriftType",
|
|
167
|
+
"DriftNature",
|
|
168
|
+
"DriftAnalysisResult",
|
|
169
|
+
"ConceptMemory",
|
|
170
|
+
"ConceptSignature",
|
|
171
|
+
"ConceptFingerprinter",
|
|
172
|
+
"RecurringConceptMemory",
|
|
173
|
+
"ConceptSnapshot",
|
|
174
|
+
"DelayedLabelQueue",
|
|
175
|
+
"ClassBalancedReservoirReplay",
|
|
176
|
+
# RLA
|
|
177
|
+
"RobustLearningPipeline",
|
|
178
|
+
"RLA30RobustLearner",
|
|
179
|
+
"MultiSignalReliabilityEngine",
|
|
180
|
+
"DensityAwareMarginScorer",
|
|
181
|
+
"AsymmetricTransitionDetector",
|
|
182
|
+
"ClassConditionalReliabilityEstimator",
|
|
183
|
+
"SymmetricCrossEntropyLoss",
|
|
184
|
+
"GeneralizedCrossEntropyLoss",
|
|
185
|
+
"FocalLoss",
|
|
186
|
+
"ReliabilityWeightedLoss",
|
|
187
|
+
"ClassBalancedWeights",
|
|
188
|
+
"NoiseTaxonomyClassifier",
|
|
189
|
+
"NoiseType",
|
|
190
|
+
"PatternType",
|
|
191
|
+
"EvidenceBasedLabelCorrector",
|
|
192
|
+
"CorrectionAuditRecord",
|
|
193
|
+
# RAO
|
|
194
|
+
"LowMemoryAdamW",
|
|
195
|
+
"VectorizedLowMemoryAdamW",
|
|
196
|
+
"BlockQuantizer",
|
|
197
|
+
"AdaptiveGradientAccumulator",
|
|
198
|
+
"DynamicResourceBudgetManager",
|
|
199
|
+
"BudgetState",
|
|
200
|
+
"ExecutionProfiler",
|
|
201
|
+
"DeviceCapabilityDetector",
|
|
202
|
+
"DeviceProfile",
|
|
203
|
+
"ResilientCheckpointManager",
|
|
204
|
+
# Minority Intelligence
|
|
205
|
+
"OnlineClassPriorTracker",
|
|
206
|
+
"BetaBinomialPriorTracker",
|
|
207
|
+
"AdaptiveThresholdManager",
|
|
208
|
+
"DynamicFPBudgetThresholdManager",
|
|
209
|
+
"compute_sublinear_cost_weights",
|
|
210
|
+
"MinorityIntelligence20Learner",
|
|
211
|
+
"MinorityAwareReplayBuffer",
|
|
212
|
+
"ClassAwareCalibrator",
|
|
213
|
+
# ULC
|
|
214
|
+
"AdaptiveLearningController",
|
|
215
|
+
"ControllerState",
|
|
216
|
+
"ControllerDirective",
|
|
217
|
+
"UnifiedLearningControllerV4",
|
|
218
|
+
"AutomatonStateV4",
|
|
219
|
+
"DirectiveV4",
|
|
220
|
+
"EvidenceVectorV4",
|
|
221
|
+
"EvidenceSynthesizerV4",
|
|
222
|
+
"ConfidenceEngine",
|
|
223
|
+
"ConfidenceScores",
|
|
224
|
+
"MultiObjectiveArbitrator",
|
|
225
|
+
"ArbitrationAutomatonV4",
|
|
226
|
+
"CrossPillarConflictResolver",
|
|
227
|
+
"UnifiedLearningControllerV4_1",
|
|
228
|
+
"ULCv41Config",
|
|
229
|
+
"compute_spatial_discordance",
|
|
230
|
+
"UnifiedLearningControllerV5",
|
|
231
|
+
"ULCv5Config",
|
|
232
|
+
"InterventionTier",
|
|
233
|
+
"ULCv5Directive",
|
|
234
|
+
"UtilityScorer",
|
|
235
|
+
# State Management
|
|
236
|
+
"save_state",
|
|
237
|
+
"load_state",
|
|
238
|
+
"CheckpointManager",
|
|
239
|
+
"is_safetensors_available",
|
|
240
|
+
# Integrations
|
|
241
|
+
"ScikitLearnAdaptiveClassifier",
|
|
242
|
+
"SklearnAdaptiveClassifier",
|
|
243
|
+
]
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def __getattr__(name: str):
|
|
247
|
+
if name == "LowMemoryAdamW":
|
|
248
|
+
from robust_adapt.rao.adamw import LowMemoryAdamW
|
|
249
|
+
|
|
250
|
+
return LowMemoryAdamW
|
|
251
|
+
if name == "VectorizedLowMemoryAdamW":
|
|
252
|
+
try:
|
|
253
|
+
from robust_adapt.integrations.torch_optim import VectorizedLowMemoryAdamW
|
|
254
|
+
|
|
255
|
+
return VectorizedLowMemoryAdamW
|
|
256
|
+
except Exception as e:
|
|
257
|
+
raise AttributeError(f"VectorizedLowMemoryAdamW requires PyTorch optional integration: {e}") from e
|
|
258
|
+
if name in ("ScikitLearnAdaptiveClassifier", "SklearnAdaptiveClassifier"):
|
|
259
|
+
try:
|
|
260
|
+
from robust_adapt.integrations.sklearn import SklearnAdaptiveClassifier
|
|
261
|
+
|
|
262
|
+
return SklearnAdaptiveClassifier
|
|
263
|
+
except Exception as e:
|
|
264
|
+
raise AttributeError(f"{name} requires scikit-learn: {e}") from e
|
|
265
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Adaptive Learning Algorithms (ALA) module.
|
|
2
|
+
|
|
3
|
+
Provides streaming classifiers, vector Hoeffding trees, multi-detector drift ensembles,
|
|
4
|
+
drift taxonomy classification, concept memory, and delayed-label streaming.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from robust_adapt.ala.adwin import ReliabilityWeightedADWIN
|
|
8
|
+
from robust_adapt.ala.classifier import ALA30AdaptiveLearner, AdaptiveClassifier
|
|
9
|
+
from robust_adapt.ala.concept_memory import (
|
|
10
|
+
ConceptFingerprinter,
|
|
11
|
+
ConceptMemory,
|
|
12
|
+
ConceptSignature,
|
|
13
|
+
ConceptSnapshot,
|
|
14
|
+
RecurringConceptMemory,
|
|
15
|
+
)
|
|
16
|
+
from robust_adapt.ala.delayed_labels import DelayedLabelQueue
|
|
17
|
+
from robust_adapt.ala.detector import (
|
|
18
|
+
BaseDriftDetector,
|
|
19
|
+
CUSUMDetector,
|
|
20
|
+
MultiDetectorConsensus,
|
|
21
|
+
PageHinkleyDetector,
|
|
22
|
+
)
|
|
23
|
+
from robust_adapt.ala.drift_classifier import (
|
|
24
|
+
DriftAnalysisResult,
|
|
25
|
+
DriftClassifier,
|
|
26
|
+
DriftNature,
|
|
27
|
+
DriftType,
|
|
28
|
+
)
|
|
29
|
+
from robust_adapt.ala.ensemble import StreamingEnsemble
|
|
30
|
+
from robust_adapt.ala.replay import ClassBalancedReservoirReplay
|
|
31
|
+
from robust_adapt.ala.tree import VectorizedHoeffdingTree
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"VectorizedHoeffdingTree",
|
|
35
|
+
"ReliabilityWeightedADWIN",
|
|
36
|
+
"ClassBalancedReservoirReplay",
|
|
37
|
+
"StreamingEnsemble",
|
|
38
|
+
"AdaptiveClassifier",
|
|
39
|
+
"BaseDriftDetector",
|
|
40
|
+
"CUSUMDetector",
|
|
41
|
+
"PageHinkleyDetector",
|
|
42
|
+
"MultiDetectorConsensus",
|
|
43
|
+
"DriftClassifier",
|
|
44
|
+
"DriftType",
|
|
45
|
+
"DriftNature",
|
|
46
|
+
"DriftAnalysisResult",
|
|
47
|
+
"ConceptMemory",
|
|
48
|
+
"ConceptSignature",
|
|
49
|
+
"ConceptFingerprinter",
|
|
50
|
+
"RecurringConceptMemory",
|
|
51
|
+
"ConceptSnapshot",
|
|
52
|
+
"ALA30AdaptiveLearner",
|
|
53
|
+
"DelayedLabelQueue",
|
|
54
|
+
]
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Reliability-Weighted ADWIN (RW-ADWIN) concept drift detector.
|
|
2
|
+
|
|
3
|
+
Mathematical formulation specified in DOC-DES-P6-003 and DOC-DES-P6-009 (Algorithm 3).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import math
|
|
7
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
8
|
+
from robust_adapt.core.constants import EPS_DIV, EPS_RELIABILITY
|
|
9
|
+
from robust_adapt.core.exceptions import ValidationError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class _Bucket:
|
|
13
|
+
"""Compressed exponential histogram bucket."""
|
|
14
|
+
|
|
15
|
+
__slots__ = ("w", "val", "w_sq", "count")
|
|
16
|
+
|
|
17
|
+
def __init__(self, w: float, val: float, w_sq: float, count: int) -> None:
|
|
18
|
+
self.w = float(w) # Sum of weights (reliabilities)
|
|
19
|
+
self.val = float(val) # Sum of weighted values (w * loss)
|
|
20
|
+
self.w_sq = float(w_sq) # Sum of squared weights
|
|
21
|
+
self.count = int(count) # Number of original observations
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ReliabilityWeightedADWIN:
|
|
25
|
+
"""Reliability-Weighted Adaptive Sliding Window (RW-ADWIN) drift detector.
|
|
26
|
+
|
|
27
|
+
Maintains a variable-length window partitioned into sub-windows W = W0 . W1,
|
|
28
|
+
weighting observations by sample reliability to suppress false-positive drift alarms
|
|
29
|
+
caused by label corruption while preserving rapid detection of true concept shifts.
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
delta : float, default=0.002
|
|
34
|
+
Confidence value for the Hoeffding cut bound (1 - delta detection confidence).
|
|
35
|
+
min_subwindow_weight : float, default=5.0
|
|
36
|
+
Minimum cumulative sample reliability required in sub-windows to test split.
|
|
37
|
+
eps_r : float, default=1e-4
|
|
38
|
+
Lower-bound epsilon guard on reliability weights.
|
|
39
|
+
max_buckets_per_level : int, default=5
|
|
40
|
+
Maximum number of buckets of equal size before compression.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
delta: float = 0.002,
|
|
46
|
+
min_subwindow_weight: float = 5.0,
|
|
47
|
+
eps_r: float = EPS_RELIABILITY,
|
|
48
|
+
max_buckets_per_level: int = 5,
|
|
49
|
+
) -> None:
|
|
50
|
+
if delta <= 0.0 or delta >= 1.0:
|
|
51
|
+
raise ValidationError(f"delta must be in (0, 1), got {delta}")
|
|
52
|
+
if min_subwindow_weight <= 0.0:
|
|
53
|
+
raise ValidationError(f"min_subwindow_weight must be positive, got {min_subwindow_weight}")
|
|
54
|
+
|
|
55
|
+
self.delta = float(delta)
|
|
56
|
+
self.min_subwindow_weight = float(min_subwindow_weight)
|
|
57
|
+
self.eps_r = float(eps_r)
|
|
58
|
+
self.max_buckets_per_level = int(max_buckets_per_level)
|
|
59
|
+
|
|
60
|
+
self._buckets: List[_Bucket] = []
|
|
61
|
+
self.window_total_count: int = 0
|
|
62
|
+
self.window_weight_sum: float = 0.0
|
|
63
|
+
self.window_weighted_val_sum: float = 0.0
|
|
64
|
+
self.window_weight_sq_sum: float = 0.0
|
|
65
|
+
self.last_delta_drift: float = 0.0
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def total_samples(self) -> int:
|
|
69
|
+
"""Total observations currently in the active window."""
|
|
70
|
+
return self.window_total_count
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def weighted_mean(self) -> float:
|
|
74
|
+
"""Current reliability-weighted mean of observations in the active window."""
|
|
75
|
+
if self.window_weight_sum <= 0.0:
|
|
76
|
+
return 0.0
|
|
77
|
+
return self.window_weighted_val_sum / self.window_weight_sum
|
|
78
|
+
|
|
79
|
+
def update(self, loss: float, reliability: float = 1.0) -> bool:
|
|
80
|
+
"""Ingest a new observation and return True if concept drift is detected.
|
|
81
|
+
|
|
82
|
+
Parameters
|
|
83
|
+
----------
|
|
84
|
+
loss : float
|
|
85
|
+
Instantaneous error or loss value, typically in [0.0, 1.0].
|
|
86
|
+
reliability : float, default=1.0
|
|
87
|
+
Sample reliability score in [eps_r, 1.0].
|
|
88
|
+
|
|
89
|
+
Returns
|
|
90
|
+
-------
|
|
91
|
+
drift_detected : bool
|
|
92
|
+
True if statistical drift bound was violated and window cut was performed.
|
|
93
|
+
"""
|
|
94
|
+
loss = float(loss)
|
|
95
|
+
reliability = float(reliability)
|
|
96
|
+
|
|
97
|
+
# Numerical bounds validation
|
|
98
|
+
if math.isnan(loss) or math.isinf(loss):
|
|
99
|
+
raise ValidationError(f"Loss must be finite, got {loss}")
|
|
100
|
+
if reliability < self.eps_r:
|
|
101
|
+
reliability = self.eps_r
|
|
102
|
+
elif reliability > 1.0:
|
|
103
|
+
reliability = 1.0
|
|
104
|
+
|
|
105
|
+
# Step 1: Insert new observation at the head (newest end)
|
|
106
|
+
w = reliability
|
|
107
|
+
v = loss
|
|
108
|
+
self.window_total_count += 1
|
|
109
|
+
self.window_weight_sum += w
|
|
110
|
+
self.window_weighted_val_sum += w * v
|
|
111
|
+
self.window_weight_sq_sum += w * w
|
|
112
|
+
|
|
113
|
+
self._buckets.append(_Bucket(w=w, val=w * v, w_sq=w * w, count=1))
|
|
114
|
+
self._compress_buckets()
|
|
115
|
+
|
|
116
|
+
# Step 2: Check sub-window partitions W = W0 . W1 from tail (oldest end)
|
|
117
|
+
drift_detected = False
|
|
118
|
+
self.last_delta_drift = 0.0
|
|
119
|
+
|
|
120
|
+
if self.window_weight_sum < 2.0 * self.min_subwindow_weight:
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
W0_w = 0.0
|
|
124
|
+
W0_val = 0.0
|
|
125
|
+
W0_w_sq = 0.0
|
|
126
|
+
W_total_w = self.window_weight_sum
|
|
127
|
+
W_total_val = self.window_weighted_val_sum
|
|
128
|
+
|
|
129
|
+
cut_idx = -1
|
|
130
|
+
n_buckets = len(self._buckets)
|
|
131
|
+
|
|
132
|
+
for i in range(n_buckets - 1):
|
|
133
|
+
b = self._buckets[i]
|
|
134
|
+
W0_w += b.w
|
|
135
|
+
W0_val += b.val
|
|
136
|
+
W0_w_sq += b.w_sq
|
|
137
|
+
|
|
138
|
+
W1_w = W_total_w - W0_w
|
|
139
|
+
W1_val = W_total_val - W0_val
|
|
140
|
+
W1_w_sq = self.window_weight_sq_sum - W0_w_sq
|
|
141
|
+
|
|
142
|
+
if W0_w < self.min_subwindow_weight or W1_w < self.min_subwindow_weight:
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
# Effective sample sizes via Kish's formula
|
|
146
|
+
N_eff_0 = (W0_w * W0_w) / (W0_w_sq + EPS_DIV)
|
|
147
|
+
N_eff_1 = (W1_w * W1_w) / (W1_w_sq + EPS_DIV)
|
|
148
|
+
m_eff = (N_eff_0 * N_eff_1) / (N_eff_0 + N_eff_1 + EPS_DIV)
|
|
149
|
+
|
|
150
|
+
if m_eff <= 0.0:
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
mu_0 = W0_val / W0_w
|
|
154
|
+
mu_1 = W1_val / W1_w
|
|
155
|
+
abs_diff = abs(mu_0 - mu_1)
|
|
156
|
+
|
|
157
|
+
# Hoeffding cut bound with effective sample size
|
|
158
|
+
eps_cut = math.sqrt((1.0 / (2.0 * m_eff)) * math.log(4.0 * self.window_total_count / self.delta))
|
|
159
|
+
|
|
160
|
+
delta_drift = abs_diff / (eps_cut + EPS_DIV)
|
|
161
|
+
if delta_drift > self.last_delta_drift:
|
|
162
|
+
self.last_delta_drift = delta_drift
|
|
163
|
+
|
|
164
|
+
if abs_diff >= eps_cut:
|
|
165
|
+
drift_detected = True
|
|
166
|
+
cut_idx = i
|
|
167
|
+
break
|
|
168
|
+
|
|
169
|
+
# Step 3: Execute window cut if drift detected
|
|
170
|
+
if drift_detected and cut_idx >= 0:
|
|
171
|
+
# Delete buckets 0..cut_idx (oldest sub-window W0)
|
|
172
|
+
dropped = self._buckets[: cut_idx + 1]
|
|
173
|
+
self._buckets = self._buckets[cut_idx + 1 :]
|
|
174
|
+
|
|
175
|
+
for b in dropped:
|
|
176
|
+
self.window_total_count -= b.count
|
|
177
|
+
self.window_weight_sum -= b.w
|
|
178
|
+
self.window_weighted_val_sum -= b.val
|
|
179
|
+
self.window_weight_sq_sum -= b.w_sq
|
|
180
|
+
|
|
181
|
+
# Clean precision drift
|
|
182
|
+
if self.window_total_count <= 0 or len(self._buckets) == 0:
|
|
183
|
+
self.reset()
|
|
184
|
+
|
|
185
|
+
return drift_detected
|
|
186
|
+
|
|
187
|
+
def _compress_buckets(self) -> None:
|
|
188
|
+
"""Merge buckets of identical count if level exceeds max_buckets_per_level."""
|
|
189
|
+
i = 0
|
|
190
|
+
while i < len(self._buckets) - 2:
|
|
191
|
+
b1 = self._buckets[i]
|
|
192
|
+
b2 = self._buckets[i + 1]
|
|
193
|
+
b3 = self._buckets[i + 2]
|
|
194
|
+
|
|
195
|
+
# If three consecutive buckets share the same count, merge the oldest two
|
|
196
|
+
if b1.count == b2.count == b3.count:
|
|
197
|
+
merged = _Bucket(
|
|
198
|
+
w=b1.w + b2.w,
|
|
199
|
+
val=b1.val + b2.val,
|
|
200
|
+
w_sq=b1.w_sq + b2.w_sq,
|
|
201
|
+
count=b1.count + b2.count,
|
|
202
|
+
)
|
|
203
|
+
self._buckets[i] = merged
|
|
204
|
+
del self._buckets[i + 1]
|
|
205
|
+
else:
|
|
206
|
+
i += 1
|
|
207
|
+
|
|
208
|
+
def reset(self) -> None:
|
|
209
|
+
"""Reset the sliding window to empty state."""
|
|
210
|
+
self._buckets.clear()
|
|
211
|
+
self.window_total_count = 0
|
|
212
|
+
self.window_weight_sum = 0.0
|
|
213
|
+
self.window_weighted_val_sum = 0.0
|
|
214
|
+
self.window_weight_sq_sum = 0.0
|
|
215
|
+
self.last_delta_drift = 0.0
|
|
216
|
+
|
|
217
|
+
def state_dict(self) -> Dict[str, Any]:
|
|
218
|
+
"""Serialize window state to dictionary."""
|
|
219
|
+
return {
|
|
220
|
+
"delta": self.delta,
|
|
221
|
+
"min_subwindow_weight": self.min_subwindow_weight,
|
|
222
|
+
"eps_r": self.eps_r,
|
|
223
|
+
"window_total_count": self.window_total_count,
|
|
224
|
+
"window_weight_sum": self.window_weight_sum,
|
|
225
|
+
"window_weighted_val_sum": self.window_weighted_val_sum,
|
|
226
|
+
"window_weight_sq_sum": self.window_weight_sq_sum,
|
|
227
|
+
"last_delta_drift": self.last_delta_drift,
|
|
228
|
+
"buckets": [{"w": b.w, "val": b.val, "w_sq": b.w_sq, "count": b.count} for b in self._buckets],
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
def load_state_dict(self, state: Dict[str, Any]) -> None:
|
|
232
|
+
"""Restore window state from dictionary."""
|
|
233
|
+
self.delta = float(state["delta"])
|
|
234
|
+
self.min_subwindow_weight = float(state["min_subwindow_weight"])
|
|
235
|
+
self.eps_r = float(state["eps_r"])
|
|
236
|
+
self.window_total_count = int(state["window_total_count"])
|
|
237
|
+
self.window_weight_sum = float(state["window_weight_sum"])
|
|
238
|
+
self.window_weighted_val_sum = float(state["window_weighted_val_sum"])
|
|
239
|
+
self.window_weight_sq_sum = float(state["window_weight_sq_sum"])
|
|
240
|
+
self.last_delta_drift = float(state.get("last_delta_drift", 0.0))
|
|
241
|
+
self._buckets = [
|
|
242
|
+
_Bucket(w=b["w"], val=b["val"], w_sq=b["w_sq"], count=b["count"]) for b in state.get("buckets", [])
|
|
243
|
+
]
|