Skip to main content

teeny_vision/mnist/
mod.rs

1/*
2 * Copyright (c) 2026 Teenygrad.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *   http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17//! LeNet-5 — Yann LeCun's convolutional network for MNIST (1998).
18//!
19//! Original paper: "Gradient-Based Learning Applied to Document Recognition"
20//! LeCun, Bottou, Bengio, Haffner (1998).
21//!
22//! Architecture (adapted to use ReLU in place of the original tanh/sigmoid):
23//!
24//! ```text
25//! Input         [N,  1, 28, 28]
26//! Conv2d(1→6,   5×5, pad=2)  →  [N,  6, 28, 28]   (same-padding keeps spatial dims)
27//! ReLU
28//! AvgPool2d(2×2, stride=2)   →  [N,  6, 14, 14]
29//! Conv2d(6→16,  5×5, pad=0)  →  [N, 16, 10, 10]
30//! ReLU
31//! AvgPool2d(2×2, stride=2)   →  [N, 16,  5,  5]
32//! Flatten                    →  [N, 400]
33//! Linear(400→120)
34//! ReLU
35//! Linear(120→84)
36//! ReLU
37//! Linear(84→10)
38//! Softmax(dim=1)             →  [N, 10]  class probabilities
39//! ```
40//!
41//! This example traces the model symbolically using `SymTensor`, extracts the
42//! computation graph, and prints every node in topological order.
43
44/// MNIST parquet dataset reading.
45pub mod dataset;
46pub use dataset::{MnistBatch, MnistDataset};
47
48use teeny_core::{
49    dtype::Float,
50    graph::SymTensor,
51    nn::{
52        Layer,
53        activation::{relu::Relu, softmax::Softmax},
54        conv2d::Conv2d,
55        flatten::Flatten,
56        linear::Linear,
57        pool::AvgPool2d,
58    },
59    sequential,
60};
61
62/// LeNet-5 (valid-convolution variant) — no padding, no bias, returns raw
63/// logits.  Use this for training with a cross-entropy loss kernel that
64/// computes log-softmax internally.
65///
66/// ```text
67/// Input         [N,  1, 28, 28]
68/// Conv2d(1→6,   5×5, pad=0) →  [N,  6, 24, 24]
69/// ReLU
70/// AvgPool2d(2×2, stride=2)  →  [N,  6, 12, 12]
71/// Conv2d(6→16,  5×5, pad=0) →  [N, 16,  8,  8]
72/// ReLU
73/// AvgPool2d(2×2, stride=2)  →  [N, 16,  4,  4]
74/// Flatten                   →  [N, 256]
75/// Linear(256→120)
76/// ReLU
77/// Linear(120→84)
78/// ReLU
79/// Linear(84→10)             →  [N, 10]  raw logits
80/// ```
81pub fn mnist_lenet5<D: Float>() -> impl Fn(SymTensor) -> SymTensor {
82    sequential![
83        Conv2d::<D, _, _, 4>::new(1, 6, (5, 5), (1, 1), (0, 0), false),
84        Relu::<D, _, 4>::new(),
85        AvgPool2d::<D, _, _, 4>::new((2, 2), (2, 2)),
86        Conv2d::<D, _, _, 4>::new(6, 16, (5, 5), (1, 1), (0, 0), false),
87        Relu::<D, _, 4>::new(),
88        AvgPool2d::<D, _, _, 4>::new((2, 2), (2, 2)),
89        Flatten::<D, _, _>::new(),
90        Linear::<D, _, _, 2>::new(256, 120, false),
91        Relu::<D, _, 2>::new(),
92        Linear::<D, _, _, 2>::new(120, 84, false),
93        Relu::<D, _, 2>::new(),
94        Linear::<D, _, _, 2>::new(84, 10, false)
95    ]
96}
97
98/// LeNet-5 (valid-convolution variant) — no padding, no bias, with final
99/// softmax.  Use this for inference when you need class probabilities.
100///
101/// ```text
102/// Input         [N,  1, 28, 28]
103/// Conv2d(1→6,   5×5, pad=0) →  [N,  6, 24, 24]
104/// ReLU
105/// AvgPool2d(2×2, stride=2)  →  [N,  6, 12, 12]
106/// Conv2d(6→16,  5×5, pad=0) →  [N, 16,  8,  8]
107/// ReLU
108/// AvgPool2d(2×2, stride=2)  →  [N, 16,  4,  4]
109/// Flatten                   →  [N, 256]
110/// Linear(256→120)
111/// ReLU
112/// Linear(120→84)
113/// ReLU
114/// Linear(84→10)
115/// Softmax(dim=1)            →  [N, 10]
116/// ```
117pub fn mnist_valid<D: Float>() -> impl Fn(SymTensor) -> SymTensor {
118    sequential![
119        Conv2d::<D, _, _, 4>::new(1, 6, (5, 5), (1, 1), (0, 0), false),
120        Relu::<D, _, 4>::new(),
121        AvgPool2d::<D, _, _, 4>::new((2, 2), (2, 2)),
122        Conv2d::<D, _, _, 4>::new(6, 16, (5, 5), (1, 1), (0, 0), false),
123        Relu::<D, _, 4>::new(),
124        AvgPool2d::<D, _, _, 4>::new((2, 2), (2, 2)),
125        Flatten::<D, _, _>::new(),
126        Linear::<D, _, _, 2>::new(256, 120, false),
127        Relu::<D, _, 2>::new(),
128        Linear::<D, _, _, 2>::new(120, 84, false),
129        Relu::<D, _, 2>::new(),
130        Linear::<D, _, _, 2>::new(84, 10, false),
131        Softmax::<D, _, 2>::new(1)
132    ]
133}
134
135/// Fully-connected MLP for MNIST — no conv layers, suitable for end-to-end training.
136///
137/// ```text
138/// Input         [N,  1, 28, 28]
139/// Flatten       →  [N, 784]
140/// Linear(784→256, no bias)
141/// ReLU
142/// Linear(256→64, no bias)
143/// ReLU
144/// Linear(64→10,  no bias)   →  [N, 10]  raw logits (no softmax)
145/// ```
146pub fn mnist_mlp<D: Float>() -> impl Fn(SymTensor) -> SymTensor {
147    sequential![
148        Flatten::<D, _, _>::new(),
149        Linear::<D, _, _, 2>::new(784, 256, false),
150        Relu::<D, _, 2>::new(),
151        Linear::<D, _, _, 2>::new(256, 64, false),
152        Relu::<D, _, 2>::new(),
153        Linear::<D, _, _, 2>::new(64, 10, false)
154    ]
155}
156
157/// Another LeNet-5 pipeline (see [`mnist_lenet5`]), with biased convolutions and same-padding on
158/// the first layer.
159pub fn mnist<D: Float>() -> impl Fn(SymTensor) -> SymTensor {
160    // -----------------------------------------------------------------------
161    // Build the LeNet-5 model as a sequential pipeline.
162    // Every layer is parameterised by its IO tensor type (SymTensor here) so
163    // the same definition compiles for both symbolic tracing and eager
164    // execution once the eager backend is implemented.
165    // -----------------------------------------------------------------------
166    sequential![
167        // Block 1 — C1/S2
168        Conv2d::<D, _, _, 4>::new(
169            1,      // in_channels  (grayscale)
170            6,      // out_channels
171            (5, 5), // kernel_size
172            (1, 1), // stride
173            (2, 2), // padding=2 keeps the 28×28 spatial size
174            true,   // has_bias
175        ),
176        Relu::<D, _, 4>::new(),
177        AvgPool2d::<D, _, _, 4>::new((2, 2), (2, 2)),
178        // Block 2 — C3/S4
179        Conv2d::<D, _, _, 4>::new(
180            6,      // in_channels
181            16,     // out_channels
182            (5, 5), // kernel_size
183            (1, 1), // stride
184            (0, 0), // no padding → 14×14 → 10×10
185            true,
186        ),
187        Relu::<D, _, 4>::new(),
188        AvgPool2d::<D, _, _, 4>::new((2, 2), (2, 2)),
189        // Flatten 16×5×5 = 400
190        Flatten::<D, _, _>::new(),
191        // Classifier — C5/F6/Output
192        Linear::<D, _, _, 2>::new(400, 120, true),
193        Relu::<D, _, 2>::new(),
194        Linear::<D, _, _, 2>::new(120, 84, true),
195        Relu::<D, _, 2>::new(),
196        Linear::<D, _, _, 2>::new(84, 10, true),
197        Softmax::<D, _, 2>::new(1)
198    ]
199}