Skip to main content

only_brain/
io.rs

1//! Saving and loading networks in the model file format.
2//!
3//! # Format
4//!
5//! Every file starts with an 8-byte magic, `ONLYBRN\0`, and a format version. Version
6//! 1, the one written today, continues as follows. Every number is little-endian.
7//!
8//! | Field | Type | Meaning |
9//! |---|---|---|
10//! | magic | 8 bytes | `ONLYBRN\0` |
11//! | version | `u16` | `1` |
12//! | inputs | `u32` | number of input neurons |
13//! | layers | `u32` | number of layers after the input layer |
14//!
15//! Then, for each layer after the input layer:
16//!
17//! | Field | Type | Meaning |
18//! |---|---|---|
19//! | neurons | `u32` | number of neurons in this layer |
20//! | activation | `u8` | 0 sigmoid, 1 tanh, 2 ReLU, 3 binary step, 4 identity |
21//! | weights | `f64` × neurons × previous layer's neurons | one row per neuron |
22//! | biases | `f64` × neurons | one per neuron |
23//!
24//! The weights and biases, read in file order, are exactly the network's [flat
25//! parameter view](crate::NeuralNetwork#flat-parameter-view).
26//!
27//! Files written before 0.3 have no magic: they are the bincode 1 encoding of the
28//! network, with one activation function for the whole network. They still load, and
29//! every layer gets that activation function.
30
31use std::fs::File;
32use std::io::{BufReader, BufWriter, Read, Write};
33use std::path::Path;
34
35use crate::neural_network::{LayerData, NetworkData};
36use crate::{ActivationFunction, NeuralNetwork};
37
38mod legacy;
39
40/// The bytes every model file written since 0.3 starts with.
41const MAGIC: [u8; 8] = *b"ONLYBRN\0";
42
43/// The model format version [`dump_model`] and [`write_model`] write. [`load_model`] and
44/// [`read_model`] read this version and every earlier one.
45pub const MODEL_FORMAT_VERSION: u16 = 1;
46
47/// Something that went wrong while saving or loading a model.
48#[derive(Debug, thiserror::Error)]
49#[non_exhaustive]
50pub enum ModelError {
51    /// The file could not be read or written.
52    #[error("could not access the model file")]
53    Io(#[from] std::io::Error),
54
55    /// The data is not a model written by this library.
56    #[error("data is not a model produced by this library")]
57    UnrecognizedFormat,
58
59    /// The model was written by a newer version of this library, in a format this
60    /// version does not know.
61    #[error("model format version {found} is not supported, the newest known is {newest}")]
62    UnsupportedVersion {
63        /// The version stored in the file.
64        found: u16,
65        /// The newest version this library reads, [`MODEL_FORMAT_VERSION`].
66        newest: u16,
67    },
68
69    /// The data starts like a model but is truncated or corrupted.
70    #[error("model data is corrupted: {reason}")]
71    Malformed {
72        /// What was wrong.
73        reason: &'static str,
74    },
75
76    /// The stored network has a different shape than the type asked for.
77    #[error("model has {found} {end} neurons but {expected} were expected")]
78    DimensionMismatch {
79        /// Which end of the network mismatched.
80        end: &'static str,
81        /// The width required by the requested type.
82        expected: usize,
83        /// The width actually found in the file.
84        found: usize,
85    },
86
87    /// The file decoded to a network with no layers at all.
88    #[error("model file contains a network with no layers")]
89    EmptyNetwork,
90
91    /// A stored layer does not fit with itself or with the layer before it, which means
92    /// the file was corrupted or edited by hand.
93    #[error("layer {layer} of the model is inconsistent: {reason}")]
94    InconsistentLayer {
95        /// The layer number, counting the input layer as 0.
96        layer: usize,
97        /// What disagreed.
98        reason: &'static str,
99    },
100}
101
102/// Writes a model to `path`, in the newest format version.
103///
104/// # Errors
105///
106/// Returns [`ModelError::Io`] if the file cannot be written.
107///
108/// # Example
109///
110/// ```no_run
111/// # use only_brain::{dump_model, NeuralNetwork};
112/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
113/// let nn = NeuralNetwork::<2, 1>::new(&[2]);
114/// dump_model(&nn, "model.bin")?;
115/// # Ok(())
116/// # }
117/// ```
118pub fn dump_model<const IN: usize, const OUT: usize>(
119    model: &NeuralNetwork<IN, OUT>,
120    path: impl AsRef<Path>,
121) -> Result<(), ModelError> {
122    let mut file = BufWriter::new(File::create(path)?);
123    write_model(model, &mut file)?;
124    file.flush()?;
125
126    Ok(())
127}
128
129/// Reads a model from `path`, checking that its shape matches `IN` and `OUT`.
130///
131/// Files written by every earlier version of this library load too. Every layer is
132/// checked against its neighbours, so a corrupted file is reported here rather than
133/// panicking later in [`NeuralNetwork::feed_forward`], and the file must end where the
134/// model does.
135///
136/// # Errors
137///
138/// Returns [`ModelError::Io`] if the file cannot be read,
139/// [`ModelError::UnrecognizedFormat`] if it is not a model,
140/// [`ModelError::UnsupportedVersion`] if a newer version of this library wrote it,
141/// [`ModelError::Malformed`] if it is truncated or corrupted,
142/// [`ModelError::DimensionMismatch`] if the stored network does not fit
143/// `NeuralNetwork<IN, OUT>`, and [`ModelError::EmptyNetwork`] or
144/// [`ModelError::InconsistentLayer`] if its layers do not form a network at all.
145///
146/// # Example
147///
148/// ```no_run
149/// # use only_brain::{load_model, NeuralNetwork};
150/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
151/// let nn: NeuralNetwork<2, 1> = load_model("model.bin")?;
152/// # Ok(())
153/// # }
154/// ```
155pub fn load_model<const IN: usize, const OUT: usize>(
156    path: impl AsRef<Path>,
157) -> Result<NeuralNetwork<IN, OUT>, ModelError> {
158    let mut file = BufReader::new(File::open(path)?);
159    let model = read_model(&mut file)?;
160
161    if file.read(&mut [0])? != 0 {
162        return Err(ModelError::Malformed {
163            reason: "the file continues after the model",
164        });
165    }
166
167    Ok(model)
168}
169
170/// Writes a model to any writer, in the newest format version.
171///
172/// The model is encoded in memory and written with a single `write_all`, so the writer
173/// does not need to be buffered. Use it with a `Vec<u8>` to keep a model in memory,
174/// for example to store it in a database.
175///
176/// # Errors
177///
178/// Returns [`ModelError::Io`] if writing fails.
179///
180/// # Example
181///
182/// ```
183/// # use only_brain::{read_model, write_model, NeuralNetwork};
184/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
185/// let nn = NeuralNetwork::<2, 1>::new(&[2]);
186///
187/// let mut bytes = Vec::new();
188/// write_model(&nn, &mut bytes)?;
189///
190/// let copy: NeuralNetwork<2, 1> = read_model(bytes.as_slice())?;
191/// assert_eq!(copy, nn);
192/// # Ok(())
193/// # }
194/// ```
195pub fn write_model<const IN: usize, const OUT: usize>(
196    model: &NeuralNetwork<IN, OUT>,
197    mut writer: impl Write,
198) -> Result<(), ModelError> {
199    let data = NetworkData::from(model);
200    let mut bytes = Vec::with_capacity(MAGIC.len() + 10 + 8 * model.parameter_count() + 5 * data.layers.len());
201
202    bytes.extend_from_slice(&MAGIC);
203    bytes.extend_from_slice(&MODEL_FORMAT_VERSION.to_le_bytes());
204    bytes.extend_from_slice(&format_count(IN).to_le_bytes());
205    bytes.extend_from_slice(&format_count(data.layers.len()).to_le_bytes());
206
207    for layer in &data.layers {
208        bytes.extend_from_slice(&format_count(layer.weights.len()).to_le_bytes());
209        bytes.push(layer.activation.code());
210        for value in layer.weights.iter().flatten().chain(&layer.biases) {
211            bytes.extend_from_slice(&value.to_le_bytes());
212        }
213    }
214
215    writer.write_all(&bytes)?;
216    Ok(())
217}
218
219/// Reads one model from any reader, checking that its shape matches `IN` and `OUT`.
220///
221/// Reading stops where the model ends, so several models can be read one after the
222/// other from the same stream. Wrap slow readers such as files in a
223/// [`std::io::BufReader`].
224///
225/// # Errors
226///
227/// The same as [`load_model`], except that data after the model is left unread rather
228/// than reported.
229pub fn read_model<const IN: usize, const OUT: usize>(
230    reader: impl Read,
231) -> Result<NeuralNetwork<IN, OUT>, ModelError> {
232    let mut decoder = Decoder::new(reader);
233
234    let start: [u8; 8] = decoder.bytes().map_err(|_| ModelError::UnrecognizedFormat)?;
235    let data = if start == MAGIC {
236        match decoder.u16()? {
237            1 => read_v1(&mut decoder)?,
238            found => {
239                return Err(ModelError::UnsupportedVersion {
240                    found,
241                    newest: MODEL_FORMAT_VERSION,
242                })
243            }
244        }
245    } else {
246        legacy::read(u64::from_le_bytes(start), &mut decoder)?
247    };
248
249    NeuralNetwork::try_from(data)
250}
251
252/// Reads the rest of a version 1 model, after the magic and the version.
253fn read_v1<R: Read>(decoder: &mut Decoder<R>) -> Result<NetworkData, ModelError> {
254    let mut inputs = decoder.u32()? as usize;
255    let layer_count = decoder.u32()?;
256
257    let mut layers = Vec::new();
258    for _ in 0..layer_count {
259        let neurons = decoder.u32()? as usize;
260        // Every later read then consumes data, so a corrupted count runs out of input
261        // instead of looping without end.
262        if neurons == 0 || inputs == 0 {
263            return Err(ModelError::Malformed {
264                reason: "a layer has no neurons",
265            });
266        }
267        let activation = ActivationFunction::from_code(decoder.u8()?).ok_or(ModelError::Malformed {
268            reason: "unknown activation function code",
269        })?;
270
271        let mut weights = Vec::new();
272        for _ in 0..neurons {
273            weights.push(decoder.f64s(inputs)?);
274        }
275        let biases = decoder.f64s(neurons)?;
276
277        layers.push(LayerData {
278            activation,
279            weights,
280            biases,
281        });
282        inputs = neurons;
283    }
284
285    Ok(NetworkData { layers })
286}
287
288/// A count as the format stores it.
289fn format_count(count: usize) -> u32 {
290    u32::try_from(count).expect("a layer cannot have more than u32::MAX neurons")
291}
292
293/// Reads little-endian values, reporting a short read as a malformed model.
294struct Decoder<R> {
295    reader: R,
296}
297
298impl<R: Read> Decoder<R> {
299    fn new(reader: R) -> Self {
300        Self { reader }
301    }
302
303    fn bytes<const N: usize>(&mut self) -> Result<[u8; N], ModelError> {
304        let mut bytes = [0; N];
305        self.reader.read_exact(&mut bytes).map_err(|error| match error.kind() {
306            std::io::ErrorKind::UnexpectedEof => ModelError::Malformed {
307                reason: "the data ends before the model does",
308            },
309            _ => ModelError::Io(error),
310        })?;
311        Ok(bytes)
312    }
313
314    fn u8(&mut self) -> Result<u8, ModelError> {
315        Ok(u8::from_le_bytes(self.bytes()?))
316    }
317
318    fn u16(&mut self) -> Result<u16, ModelError> {
319        Ok(u16::from_le_bytes(self.bytes()?))
320    }
321
322    fn u32(&mut self) -> Result<u32, ModelError> {
323        Ok(u32::from_le_bytes(self.bytes()?))
324    }
325
326    fn u64(&mut self) -> Result<u64, ModelError> {
327        Ok(u64::from_le_bytes(self.bytes()?))
328    }
329
330    fn f64(&mut self) -> Result<f64, ModelError> {
331        Ok(f64::from_le_bytes(self.bytes()?))
332    }
333
334    /// Reads `count` floats. The count comes from the data, so memory is reserved as
335    /// values arrive rather than up front, and a corrupted count fails at the end of the
336    /// data instead of exhausting memory.
337    fn f64s(&mut self, count: usize) -> Result<Vec<f64>, ModelError> {
338        let mut values = Vec::with_capacity(count.min(4096));
339        for _ in 0..count {
340            values.push(self.f64()?);
341        }
342        Ok(values)
343    }
344}