entrenar/optim/dp/
dp_sgd.rs1use super::accountant::RdpAccountant;
9use super::config::DpSgdConfig;
10use super::error::{DpError, Result};
11use super::gradient::{add_gaussian_noise, clip_gradient};
12
13#[derive(Debug, Clone)]
20pub struct DpSgd {
21 config: DpSgdConfig,
23 accountant: RdpAccountant,
25 learning_rate: f64,
27}
28
29impl DpSgd {
30 pub fn new(learning_rate: f64, config: DpSgdConfig) -> Result<Self> {
32 config.validate()?;
33 Ok(Self { config, accountant: RdpAccountant::new(), learning_rate })
34 }
35
36 pub fn privacy_spent(&self) -> (f64, f64) {
38 self.accountant.get_privacy_spent(self.config.budget.delta)
39 }
40
41 pub fn current_epsilon(&self) -> f64 {
43 self.privacy_spent().0
44 }
45
46 pub fn remaining_budget(&self) -> f64 {
48 self.config.budget.remaining(self.current_epsilon())
49 }
50
51 pub fn is_budget_exhausted(&self) -> bool {
53 !self.config.budget.allows(self.current_epsilon())
54 }
55
56 pub fn n_steps(&self) -> usize {
58 self.accountant.n_steps()
59 }
60
61 pub fn config(&self) -> &DpSgdConfig {
63 &self.config
64 }
65
66 pub fn privatize_gradients(&mut self, per_sample_grads: &[Vec<f64>]) -> Result<Vec<f64>> {
70 if per_sample_grads.is_empty() {
71 return Err(DpError::GradientError("No gradients provided".to_string()));
72 }
73
74 if self.config.strict_budget && self.is_budget_exhausted() {
76 return Err(DpError::BudgetExhausted {
77 spent: self.current_epsilon(),
78 budget: self.config.budget.epsilon,
79 });
80 }
81
82 let n_samples = per_sample_grads.len();
83 let grad_dim = per_sample_grads[0].len();
84
85 let clipped: Vec<Vec<f64>> =
87 per_sample_grads.iter().map(|g| clip_gradient(g, self.config.max_grad_norm)).collect();
88
89 let mut averaged = vec![0.0; grad_dim];
91 for g in &clipped {
92 for (i, &val) in g.iter().enumerate() {
93 averaged[i] += val / n_samples as f64;
94 }
95 }
96
97 let mut rng = rand::rng();
99 let noise_std = self.config.noise_std() / n_samples as f64;
100 let noised = add_gaussian_noise(&averaged, noise_std, &mut rng);
101
102 self.accountant.step(self.config.noise_multiplier, self.config.sample_rate);
104
105 Ok(noised)
106 }
107
108 pub fn apply_update(&self, params: &mut [f64], grad: &[f64]) {
110 for (p, g) in params.iter_mut().zip(grad.iter()) {
111 *p -= self.learning_rate * g;
112 }
113 }
114
115 pub fn step(
117 &mut self,
118 params: &mut [f64],
119 per_sample_grads: &[Vec<f64>],
120 ) -> Result<(f64, f64)> {
121 let grad = self.privatize_gradients(per_sample_grads)?;
122 self.apply_update(params, &grad);
123 Ok(self.privacy_spent())
124 }
125
126 pub fn reset(&mut self) {
128 self.accountant.reset();
129 }
130}