1use crate::error::{RillError, checked_increment, ensure_finite};
4
5#[derive(Debug, Clone)]
7#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
8pub struct SgdConfig {
9 pub learning_rate: f64,
11 pub l2: f64,
13}
14
15impl Default for SgdConfig {
16 fn default() -> Self {
17 Self {
18 learning_rate: 0.01,
19 l2: 0.0,
20 }
21 }
22}
23
24#[derive(Debug, Clone)]
32#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
33pub struct Sgd {
34 feature_count: usize,
35 config: SgdConfig,
36 samples_seen: u64,
37}
38
39impl Sgd {
40 pub fn new(feature_count: usize, config: SgdConfig) -> Result<Self, RillError> {
42 if feature_count == 0 {
43 return Err(RillError::EmptyFeatures);
44 }
45 ensure_finite("learning_rate", config.learning_rate)?;
46 ensure_finite("l2", config.l2)?;
47 if config.learning_rate <= 0.0 {
48 return Err(RillError::InvalidLearningRate(config.learning_rate));
49 }
50 if config.l2 < 0.0 {
51 return Err(RillError::InvalidParameter {
52 name: "l2",
53 value: config.l2,
54 });
55 }
56 Ok(Self {
57 feature_count,
58 config,
59 samples_seen: 0,
60 })
61 }
62
63 pub const fn learning_rate(&self) -> f64 {
65 self.config.learning_rate
66 }
67
68 pub const fn l2(&self) -> f64 {
70 self.config.l2
71 }
72
73 pub const fn param_count(&self) -> usize {
75 self.feature_count + 1
76 }
77
78 pub const fn samples_seen(&self) -> u64 {
80 self.samples_seen
81 }
82
83 pub fn step(
85 &mut self,
86 weights: &mut [f64],
87 intercept: &mut f64,
88 grad_weights: &[f64],
89 grad_intercept: f64,
90 ) -> Result<(), RillError> {
91 if weights.len() != self.feature_count {
92 return Err(RillError::DimensionMismatch {
93 expected: self.feature_count,
94 actual: weights.len(),
95 });
96 }
97 if grad_weights.len() != self.feature_count {
98 return Err(RillError::DimensionMismatch {
99 expected: self.feature_count,
100 actual: grad_weights.len(),
101 });
102 }
103 for &gradient in grad_weights {
104 ensure_finite("grad_weight", gradient)?;
105 }
106 ensure_finite("grad_intercept", grad_intercept)?;
107 let next_samples = checked_increment(self.samples_seen, "SGD sample")?;
108 let lr = self.config.learning_rate;
109 let l2 = self.config.l2;
110 let next_weights = weights
111 .iter()
112 .zip(grad_weights)
113 .map(|(&weight, &gradient)| {
114 let regularized_gradient = gradient + l2 * weight;
115 ensure_finite("regularized gradient", regularized_gradient)?;
116 let next_weight = weight - lr * regularized_gradient;
117 ensure_finite("weight", next_weight)?;
118 Ok(next_weight)
119 })
120 .collect::<Result<Vec<_>, RillError>>()?;
121 let next_intercept = *intercept - lr * grad_intercept;
122 ensure_finite("intercept", next_intercept)?;
123
124 weights.copy_from_slice(&next_weights);
125 *intercept = next_intercept;
126 self.samples_seen = next_samples;
127 Ok(())
128 }
129
130 pub fn reset(&mut self) {
132 self.samples_seen = 0;
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn sgd_updates_weights() {
142 let mut opt = Sgd::new(
143 2,
144 SgdConfig {
145 learning_rate: 0.1,
146 l2: 0.0,
147 },
148 )
149 .unwrap();
150 let mut w = vec![0.0, 0.0];
151 let mut b = 0.0;
152 opt.step(&mut w, &mut b, &[1.0, 2.0], 0.5).unwrap();
153 assert!((w[0] + 0.1).abs() < 1e-12);
155 assert!((w[1] + 0.2).abs() < 1e-12);
156 assert!((b + 0.05).abs() < 1e-12);
157 }
158
159 #[test]
160 fn sgd_l2_regularization() {
161 let mut opt = Sgd::new(
162 1,
163 SgdConfig {
164 learning_rate: 0.1,
165 l2: 1.0,
166 },
167 )
168 .unwrap();
169 let mut w = vec![10.0];
170 let mut b = 0.0;
171 opt.step(&mut w, &mut b, &[0.0], 0.0).unwrap();
172 assert!((w[0] - 9.0).abs() < 1e-12);
174 assert!((b - 0.0).abs() < 1e-12);
176 }
177
178 #[test]
179 fn invalid_learning_rate_rejected() {
180 assert!(
181 Sgd::new(
182 1,
183 SgdConfig {
184 learning_rate: 0.0,
185 l2: 0.0
186 }
187 )
188 .is_err()
189 );
190 assert!(
191 Sgd::new(
192 1,
193 SgdConfig {
194 learning_rate: -1.0,
195 l2: 0.0
196 }
197 )
198 .is_err()
199 );
200 }
201
202 #[test]
203 fn invalid_l2_rejected() {
204 assert!(
205 Sgd::new(
206 1,
207 SgdConfig {
208 learning_rate: 0.1,
209 l2: -1.0
210 }
211 )
212 .is_err()
213 );
214 }
215
216 #[test]
217 fn failed_step_is_atomic() {
218 let mut opt = Sgd::new(2, SgdConfig::default()).unwrap();
219 let mut weights = vec![1.0, 2.0];
220 let mut intercept = 3.0;
221 let result = opt.step(&mut weights, &mut intercept, &[1.0, f64::INFINITY], 1.0);
222 assert!(result.is_err());
223 assert_eq!(weights, vec![1.0, 2.0]);
224 assert_eq!(intercept, 3.0);
225 assert_eq!(opt.samples_seen(), 0);
226 }
227}