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}