Skip to main content

burn_dataset/transform/
sampler.rs

1use crate::Dataset;
2use crate::DatasetError;
3use crate::transform::{RngSource, SizeConfig};
4use rand::prelude::SliceRandom;
5use rand::{RngExt, distr::Uniform, rngs::StdRng, seq::IteratorRandom};
6use std::{marker::PhantomData, ops::DerefMut, sync::Mutex};
7
8/// Options to configure a [SamplerDataset].
9#[derive(Debug, PartialEq)]
10pub struct SamplerDatasetOptions {
11    /// The sampling mode.
12    pub replace_samples: bool,
13
14    /// The size source of the wrapper relative to the dataset.
15    pub size_config: SizeConfig,
16
17    /// The source of the random number generator.
18    pub rng_source: RngSource,
19}
20
21impl Default for SamplerDatasetOptions {
22    fn default() -> Self {
23        Self {
24            replace_samples: true,
25            size_config: SizeConfig::Default,
26            rng_source: RngSource::Default,
27        }
28    }
29}
30
31impl<T> From<Option<T>> for SamplerDatasetOptions
32where
33    T: Into<SamplerDatasetOptions>,
34{
35    fn from(option: Option<T>) -> Self {
36        match option {
37            Some(option) => option.into(),
38            None => Self::default(),
39        }
40    }
41}
42
43impl From<usize> for SamplerDatasetOptions {
44    fn from(size: usize) -> Self {
45        Self::default().with_replacement().with_fixed_size(size)
46    }
47}
48
49impl SamplerDatasetOptions {
50    /// Set the replacement mode.
51    pub fn with_replace_samples(self, replace_samples: bool) -> Self {
52        Self {
53            replace_samples,
54            ..self
55        }
56    }
57
58    /// Set the replacement mode to WithReplacement.
59    pub fn with_replacement(self) -> Self {
60        self.with_replace_samples(true)
61    }
62
63    /// Set the replacement mode to WithoutReplacement.
64    pub fn without_replacement(self) -> Self {
65        self.with_replace_samples(false)
66    }
67
68    /// Set the size source.
69    pub fn with_size<S>(self, source: S) -> Self
70    where
71        S: Into<SizeConfig>,
72    {
73        Self {
74            size_config: source.into(),
75            ..self
76        }
77    }
78
79    /// Set the size to the size of the source.
80    pub fn with_source_size(self) -> Self {
81        self.with_size(SizeConfig::Default)
82    }
83
84    /// Set the size to a fixed size.
85    pub fn with_fixed_size(self, size: usize) -> Self {
86        self.with_size(size)
87    }
88
89    /// Set the size to be a multiple of the ration and the source size.
90    pub fn with_size_ratio(self, size_ratio: f64) -> Self {
91        self.with_size(size_ratio)
92    }
93
94    /// Set the `RngSource`.
95    pub fn with_rng<R>(self, rng: R) -> Self
96    where
97        R: Into<RngSource>,
98    {
99        Self {
100            rng_source: rng.into(),
101            ..self
102        }
103    }
104
105    /// Use the system rng.
106    pub fn with_system_rng(self) -> Self {
107        self.with_rng(RngSource::Default)
108    }
109
110    /// Use a rng, built from a seed.
111    pub fn with_seed(self, seed: u64) -> Self {
112        self.with_rng(seed)
113    }
114}
115
116/// Sample items from a dataset.
117///
118/// This is a convenient way of modeling a dataset as a probability distribution of a fixed size.
119/// You have multiple options to instantiate the dataset sampler.
120///
121/// * With replacement (Default): This is the most efficient way of using the sampler because no state is
122///   required to keep indices that have been selected.
123///
124/// * Without replacement: This has a similar effect to using a
125///   [shuffled dataset](crate::transform::ShuffledDataset), but with more flexibility since you can
126///   set the dataset to an arbitrary size. Once every item has been used, a new cycle is
127///   created with a new random suffle.
128pub struct SamplerDataset<D, I> {
129    dataset: D,
130    size: usize,
131    state: Mutex<SamplerState>,
132    input: PhantomData<I>,
133}
134enum SamplerState {
135    WithReplacement(StdRng),
136    WithoutReplacement(StdRng, Vec<usize>),
137}
138
139impl<D, I> SamplerDataset<D, I>
140where
141    D: Dataset<I>,
142    I: Send + Sync,
143{
144    /// Creates a new sampler dataset with replacement.
145    ///
146    /// When the sample size is less than or equal to the source dataset size,
147    /// data will be sampled without replacement from the source dataset in
148    /// a uniformly shuffled order.
149    ///
150    /// When the sample size is greater than the source dataset size,
151    /// the entire source dataset will be sampled once for every multiple
152    /// of the size ratios; with the remaining samples taken without replacement
153    /// uniformly from the source. All samples will be returned uniformly shuffled.
154    ///
155    /// ## Arguments
156    ///
157    /// * `dataset`: the dataset to wrap.
158    /// * `options`: the options to configure the sampler dataset.
159    ///
160    /// ## Examples
161    /// ```rust,ignore
162    /// use burn_dataset::transform::{
163    ///   SamplerDataset,
164    ///   SamplerDatasetOptions,
165    /// };
166    ///
167    /// // Examples below assuming `dataset.len()` = `10`.
168    ///
169    /// // sample size: 5
170    /// // WithReplacement
171    /// // rng: StdRng::from_os_rng()
172    /// SamplerDataset::new(dataset, 5);
173    ///
174    /// // sample size: 10 (source)
175    /// // WithReplacement
176    /// // rng: StdRng::from_os_rng()
177    /// SamplerDataset::new(dataset, SamplerDatasetOptions::default());
178    ///
179    /// // sample size: 15
180    /// // WithoutReplacement
181    /// // rng: StdRng::seed_from_u64(42)
182    /// SamplerDataset::new(
183    ///   dataset,
184    ///   SamplerDatasetOptions::default()
185    ///     .with_size(1.5)
186    ///     .without_replacement()
187    ///     .with_rng(42),
188    /// );
189    /// ```
190    pub fn new<O>(dataset: D, options: O) -> Self
191    where
192        O: Into<SamplerDatasetOptions>,
193    {
194        let options = options.into();
195        let size = options.size_config.resolve(dataset.len());
196        let rng = options.rng_source.into();
197        Self {
198            dataset,
199            size,
200            state: Mutex::new(match options.replace_samples {
201                true => SamplerState::WithReplacement(rng),
202                false => SamplerState::WithoutReplacement(rng, Vec::with_capacity(size)),
203            }),
204            input: PhantomData,
205        }
206    }
207
208    /// Creates a new sampler dataset with replacement.
209    ///
210    /// # Arguments
211    ///
212    /// - `dataset`: the dataset to wrap.
213    /// - `size`: the effective size of the sampled dataset.
214    pub fn with_replacement(dataset: D, size: usize) -> Self {
215        Self::new(
216            dataset,
217            SamplerDatasetOptions::default()
218                .with_replacement()
219                .with_fixed_size(size),
220        )
221    }
222
223    /// Creates a new sampler dataset without replacement.
224    ///
225    /// When the sample size is less than or equal to the source dataset size,
226    /// data will be sampled without replacement from the source dataset in
227    /// a uniformly shuffled order.
228    ///
229    /// When the sample size is greater than the source dataset size,
230    /// the entire source dataset will be sampled once for every multiple
231    /// of the size ratios; with the remaining samples taken without replacement
232    /// uniformly from the source. All samples will be returned uniformly shuffled.
233    ///
234    /// # Arguments
235    /// - `dataset`: the dataset to wrap.
236    /// - `size`: the effective size of the sampled dataset.
237    pub fn without_replacement(dataset: D, size: usize) -> Self {
238        Self::new(
239            dataset,
240            SamplerDatasetOptions::default()
241                .without_replacement()
242                .with_fixed_size(size),
243        )
244    }
245
246    /// Determines if the sampler is using the "with replacement" strategy.
247    ///
248    /// # Returns
249    /// - `true`: If the sampler is configured to sample with replacement.
250    /// - `false`: If the sampler is configured to sample without replacement.
251    pub fn is_with_replacement(&self) -> bool {
252        match self.state.lock().unwrap().deref_mut() {
253            SamplerState::WithReplacement(_) => true,
254            SamplerState::WithoutReplacement(_, _) => false,
255        }
256    }
257
258    fn index(&self) -> usize {
259        match self.state.lock().unwrap().deref_mut() {
260            SamplerState::WithReplacement(rng) => {
261                rng.sample(Uniform::new(0, self.dataset.len()).unwrap())
262            }
263            SamplerState::WithoutReplacement(rng, indices) => {
264                if indices.is_empty() {
265                    // Refill the state.
266                    let idx_range = 0..self.dataset.len();
267                    for _ in 0..(self.size / self.dataset.len()) {
268                        // No need to `.choose_multiple` here because we're using
269                        // the entire source range; and `.choose_multiple` will
270                        // not return a random sample anyway.
271                        indices.extend(idx_range.clone())
272                    }
273
274                    // From `choose_multiple` documentation:
275                    // > Although the elements are selected randomly, the order of elements in
276                    // > the buffer is neither stable nor fully random. If random ordering is
277                    // > desired, shuffle the result.
278                    indices.extend(idx_range.sample(rng, self.size - indices.len()));
279
280                    // The real shuffling is done here.
281                    indices.shuffle(rng);
282                }
283
284                indices.pop().expect("Indices are refilled when empty.")
285            }
286        }
287    }
288}
289
290impl<D, I> Dataset<I> for SamplerDataset<D, I>
291where
292    D: Dataset<I>,
293    I: Send + Sync,
294{
295    fn get(&self, index: usize) -> Result<I, DatasetError> {
296        assert!(
297            index < self.size,
298            "Index out of bounds for SamplerDataset: {} >= {}",
299            index,
300            self.size
301        );
302        self.dataset.get(self.index())
303    }
304
305    fn len(&self) -> usize {
306        self.size
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    #![allow(clippy::bool_assert_comparison)]
313
314    use super::*;
315    use crate::FakeDataset;
316    use rand::SeedableRng;
317    use std::collections::HashMap;
318
319    #[test]
320    fn test_samplerdataset_options() {
321        let options = SamplerDatasetOptions::default();
322        assert_eq!(options.replace_samples, true);
323        assert_eq!(options.size_config, SizeConfig::Default);
324        assert_eq!(options.rng_source, RngSource::Default);
325
326        // ReplacementMode
327        let options = options.with_replace_samples(false);
328        assert_eq!(options.replace_samples, false);
329        let options = options.with_replacement();
330        assert_eq!(options.replace_samples, true);
331        let options = options.without_replacement();
332        assert_eq!(options.replace_samples, false);
333
334        // SourceSize
335        let options = options.with_size(SizeConfig::Default);
336        assert_eq!(options.size_config, SizeConfig::Default);
337        let options = options.with_source_size();
338        assert_eq!(options.size_config, SizeConfig::Default);
339        let options = options.with_fixed_size(10);
340        assert_eq!(options.size_config, SizeConfig::Fixed(10));
341        let options = options.with_size_ratio(1.5);
342        assert_eq!(options.size_config, SizeConfig::Ratio(1.5));
343
344        // RngSource
345        let options = options.with_system_rng();
346        assert_eq!(options.rng_source, RngSource::Default);
347        let options = options.with_seed(42);
348        assert_eq!(options.rng_source, RngSource::Seed(42));
349        let rng = StdRng::seed_from_u64(9);
350        let options = options.with_rng(rng);
351        assert!(matches!(options.rng_source, RngSource::Rng(_)));
352    }
353
354    #[test]
355    fn sampler_dataset_constructors_test() {
356        let ds = SamplerDataset::new(FakeDataset::<u32>::new(10), 15);
357        assert_eq!(ds.len(), 15);
358        assert_eq!(ds.dataset.len(), 10);
359        assert!(ds.is_with_replacement());
360
361        let ds = SamplerDataset::with_replacement(FakeDataset::<u32>::new(10), 15);
362        assert_eq!(ds.len(), 15);
363        assert_eq!(ds.dataset.len(), 10);
364        assert!(ds.is_with_replacement());
365
366        let ds = SamplerDataset::without_replacement(FakeDataset::<u32>::new(10), 15);
367        assert_eq!(ds.len(), 15);
368        assert_eq!(ds.dataset.len(), 10);
369        assert!(!ds.is_with_replacement());
370    }
371
372    #[test]
373    fn sampler_dataset_with_replacement_iter() {
374        let factor = 3;
375        let len_original = 10;
376        let dataset_sampler = SamplerDataset::with_replacement(
377            FakeDataset::<String>::new(len_original),
378            len_original * factor,
379        );
380        let mut total = 0;
381
382        for _item in dataset_sampler.iter() {
383            total += 1;
384        }
385
386        assert_eq!(total, factor * len_original);
387    }
388
389    #[test]
390    fn sampler_dataset_without_replacement_bucket_test() {
391        let factor = 3;
392        let len_original = 10;
393
394        let dataset_sampler = SamplerDataset::new(
395            FakeDataset::<String>::new(len_original),
396            SamplerDatasetOptions::default()
397                .without_replacement()
398                .with_size_ratio(factor as f64),
399        );
400
401        let mut buckets = HashMap::new();
402
403        for item in dataset_sampler.iter().map(Result::unwrap) {
404            let count = match buckets.get(&item) {
405                Some(count) => count + 1,
406                None => 1,
407            };
408
409            buckets.insert(item, count);
410        }
411
412        let mut total = 0;
413        for count in buckets.into_values() {
414            assert_eq!(count, factor);
415            total += count;
416        }
417        assert_eq!(total, factor * len_original);
418    }
419
420    #[test]
421    fn sampler_dataset_without_replacement_uniform_order_test() {
422        // This is a reversion test on the indices.shuffle(rng) call in SamplerDataset::index().
423        let size = 1000;
424        let dataset_sampler =
425            SamplerDataset::without_replacement(FakeDataset::<i32>::new(size), size);
426
427        let indices: Vec<_> = (0..size).map(|_| dataset_sampler.index()).collect();
428        let mean_delta = indices
429            .windows(2)
430            .map(|pair| pair[1].abs_diff(pair[0]))
431            .sum::<usize>() as f64
432            / (size - 1) as f64;
433
434        let expected = (size + 2) as f64 / 3.0;
435
436        assert!(
437            (mean_delta - expected).abs() <= 0.25 * expected,
438            "Sampled indices are not uniformly distributed: mean_delta: {mean_delta}, expected: {expected}"
439        );
440    }
441}