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}