Build a digit recognizer from scratch in C!
- Understanding MNIST dataset
- Image processing in C
- Building a digit recognizer
- Training and testing
- Optimization techniques
- Complete working project
The MNIST database contains:
- 60,000 training images
- 10,000 test images
- 28x28 pixel grayscale images
- Digits 0-9
graph TD
A[MNIST Dataset] --> B[Training Set]
A --> C[Test Set]
B --> D[60,000 images]
B --> E[28x28 pixels]
C --> F[10,000 images]
C --> G[28x28 pixels]
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#define IMAGE_SIZE 784 // 28 * 28
#define NUM_LABELS 10
typedef struct {
uint8_t pixels[IMAGE_SIZE];
uint8_t label;
} MNISTImage;
// Read MNIST image file
int readMNISTImages(const char *filename, MNISTImage **images, int *num_images) {
FILE *file = fopen(filename, "rb");
if (!file) {
printf("Error opening file: %s\n", filename);
return -1;
}
// Read header
uint32_t magic_number, num_imgs, rows, cols;
fread(&magic_number, 4, 1, file);
fread(&num_imgs, 4, 1, file);
fread(&rows, 4, 1, file);
fread(&cols, 4, 1, file);
// Swap endianness if needed (MNIST uses big-endian)
// Note: __builtin_bswap32 is GCC-specific; for portability use:
// ((x>>24)&0xff) | ((x<<8)&0xff0000) | ((x>>8)&0xff00) | ((x<<24)&0xff000000)
magic_number = __builtin_bswap32(magic_number);
num_imgs = __builtin_bswap32(num_imgs);
rows = __builtin_bswap32(rows);
cols = __builtin_bswap32(cols);
*num_images = num_imgs;
*images = (MNISTImage*)malloc(num_imgs * sizeof(MNISTImage));
// Read images
for (int i = 0; i < num_imgs; i++) {
fread((*images)[i].pixels, IMAGE_SIZE, 1, file);
}
fclose(file);
return 0;
}
// Read MNIST labels
int readMNISTLabels(const char *filename, MNISTImage *images, int num_images) {
FILE *file = fopen(filename, "rb");
if (!file) {
printf("Error opening file: %s\n", filename);
return -1;
}
// Read header
uint32_t magic_number, num_labels;
fread(&magic_number, 4, 1, file);
fread(&num_labels, 4, 1, file);
magic_number = __builtin_bswap32(magic_number);
num_labels = __builtin_bswap32(num_labels);
// Read labels
for (int i = 0; i < num_images && i < num_labels; i++) {
fread(&images[i].label, 1, 1, file);
}
fclose(file);
return 0;
}// Normalize pixel values to [0, 1]
void normalizeImage(uint8_t *pixels, double *normalized) {
for (int i = 0; i < IMAGE_SIZE; i++) {
normalized[i] = pixels[i] / 255.0;
}
}
// Display image in ASCII
void displayImage(uint8_t *pixels) {
char chars[] = " .:-=+*#%@";
for (int i = 0; i < 28; i++) {
for (int j = 0; j < 28; j++) {
int pixel = pixels[i * 28 + j];
int index = pixel * 9 / 255;
printf("%c", chars[index]);
}
printf("\n");
}
}
// Extract features (simple averaging)
void extractFeatures(uint8_t *pixels, double *features, int num_features) {
int block_size = IMAGE_SIZE / num_features;
for (int i = 0; i < num_features; i++) {
double sum = 0;
for (int j = 0; j < block_size; j++) {
sum += pixels[i * block_size + j];
}
features[i] = sum / (block_size * 255.0);
}
}graph LR
I[784 Input<br/>Neurons] --> H1[128 Hidden<br/>Layer 1]
H1 --> H2[64 Hidden<br/>Layer 2]
H2 --> O[10 Output<br/>Neurons]
style I fill:#9cf
style H1 fill:#f9c
style H2 fill:#f9c
style O fill:#9f9
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#include <string.h>
#define INPUT_SIZE 784
#define HIDDEN1_SIZE 128
#define HIDDEN2_SIZE 64
#define OUTPUT_SIZE 10
#define LEARNING_RATE 0.01
typedef struct {
// Weights
double **w1; // INPUT_SIZE x HIDDEN1_SIZE
double **w2; // HIDDEN1_SIZE x HIDDEN2_SIZE
double **w3; // HIDDEN2_SIZE x OUTPUT_SIZE
// Biases
double *b1; // HIDDEN1_SIZE
double *b2; // HIDDEN2_SIZE
double *b3; // OUTPUT_SIZE
} DigitRecognizer;
DigitRecognizer* createRecognizer() {
DigitRecognizer *dr = (DigitRecognizer*)malloc(sizeof(DigitRecognizer));
// Allocate and initialize weights
dr->w1 = (double**)malloc(INPUT_SIZE * sizeof(double*));
for (int i = 0; i < INPUT_SIZE; i++) {
dr->w1[i] = (double*)malloc(HIDDEN1_SIZE * sizeof(double));
for (int j = 0; j < HIDDEN1_SIZE; j++) {
dr->w1[i][j] = ((double)rand() / RAND_MAX) * 0.1 - 0.05;
}
}
// Similar for w2 and w3...
return dr;
}
// Forward pass
void forward(DigitRecognizer *dr, double *input, double *output) {
double hidden1[HIDDEN1_SIZE];
double hidden2[HIDDEN2_SIZE];
// Input to Hidden1
for (int j = 0; j < HIDDEN1_SIZE; j++) {
hidden1[j] = dr->b1[j];
for (int i = 0; i < INPUT_SIZE; i++) {
hidden1[j] += input[i] * dr->w1[i][j];
}
hidden1[j] = relu(hidden1[j]);
}
// Hidden1 to Hidden2
for (int j = 0; j < HIDDEN2_SIZE; j++) {
hidden2[j] = dr->b2[j];
for (int i = 0; i < HIDDEN1_SIZE; i++) {
hidden2[j] += hidden1[i] * dr->w2[i][j];
}
hidden2[j] = relu(hidden2[j]);
}
// Hidden2 to Output
double max = -INFINITY;
for (int j = 0; j < OUTPUT_SIZE; j++) {
output[j] = dr->b3[j];
for (int i = 0; i < HIDDEN2_SIZE; i++) {
output[j] += hidden2[i] * dr->w3[i][j];
}
if (output[j] > max) max = output[j];
}
// Softmax
double sum = 0;
for (int j = 0; j < OUTPUT_SIZE; j++) {
output[j] = exp(output[j] - max);
sum += output[j];
}
for (int j = 0; j < OUTPUT_SIZE; j++) {
output[j] /= sum;
}
}
// Predict digit
int predict(DigitRecognizer *dr, uint8_t *pixels) {
double input[INPUT_SIZE];
double output[OUTPUT_SIZE];
// Normalize input
for (int i = 0; i < IMAGE_SIZE; i++) {
input[i] = pixels[i] / 255.0;
}
forward(dr, input, output);
// Find max
int max_idx = 0;
double max_val = output[0];
for (int i = 1; i < OUTPUT_SIZE; i++) {
if (output[i] > max_val) {
max_val = output[i];
max_idx = i;
}
}
return max_idx;
}graph TD
A[Load MNIST Data] --> B[Preprocess Images]
B --> C[Initialize Network]
C --> D[Training Loop]
D --> E[Forward Pass]
E --> F[Compute Loss]
F --> G[Backward Pass]
G --> H[Update Weights]
H --> I{Epoch Complete?}
I -->|No| D
I -->|Yes| J[Evaluate on Test Set]
J --> K[Save Model]
void train(DigitRecognizer *dr, MNISTImage *images, int num_images,
int epochs, int batch_size) {
printf("Training digit recognizer...\n");
for (int epoch = 0; epoch < epochs; epoch++) {
double total_loss = 0;
int correct = 0;
for (int i = 0; i < num_images; i++) {
double input[INPUT_SIZE];
double output[OUTPUT_SIZE];
double target[OUTPUT_SIZE] = {0};
// Normalize input
normalizeImage(images[i].pixels, input);
target[images[i].label] = 1.0;
// Forward pass
forward(dr, input, output);
// Compute loss (cross-entropy)
total_loss -= log(output[images[i].label] + 1e-10);
// Check accuracy
int predicted = 0;
for (int j = 1; j < OUTPUT_SIZE; j++) {
if (output[j] > output[predicted]) {
predicted = j;
}
}
if (predicted == images[i].label) correct++;
// Backward pass and update weights
// (implementation details...)
}
printf("Epoch %d: Loss = %.4f, Accuracy = %.2f%%\n",
epoch + 1, total_loss / num_images,
100.0 * correct / num_images);
}
}void evaluate(DigitRecognizer *dr, MNISTImage *test_images, int num_test) {
int correct = 0;
int confusion_matrix[10][10] = {0};
printf("Evaluating on test set...\n");
for (int i = 0; i < num_test; i++) {
int predicted = predict(dr, test_images[i].pixels);
int actual = test_images[i].label;
confusion_matrix[actual][predicted]++;
if (predicted == actual) correct++;
}
printf("Test Accuracy: %.2f%%\n", 100.0 * correct / num_test);
// Print confusion matrix
printf("\nConfusion Matrix:\n");
printf(" ");
for (int i = 0; i < 10; i++) printf("%4d", i);
printf("\n");
for (int i = 0; i < 10; i++) {
printf("%2d ", i);
for (int j = 0; j < 10; j++) {
printf("%4d", confusion_matrix[i][j]);
}
printf("\n");
}
}int main() {
srand(time(NULL));
// Load MNIST data
MNISTImage *train_images, *test_images;
int num_train, num_test;
printf("Loading MNIST dataset...\n");
readMNISTImages("train-images.idx3-ubyte", &train_images, &num_train);
readMNISTLabels("train-labels.idx1-ubyte", train_images, num_train);
readMNISTImages("t10k-images.idx3-ubyte", &test_images, &num_test);
readMNISTLabels("t10k-labels.idx1-ubyte", test_images, num_test);
printf("Loaded %d training images and %d test images\n", num_train, num_test);
// Create and train recognizer
DigitRecognizer *dr = createRecognizer();
train(dr, train_images, num_train, 10, 32);
// Evaluate
evaluate(dr, test_images, num_test);
// Interactive testing
printf("\nTesting on random images:\n");
for (int i = 0; i < 5; i++) {
int idx = rand() % num_test;
printf("\nImage %d:\n", i + 1);
displayImage(test_images[idx].pixels);
int predicted = predict(dr, test_images[idx].pixels);
printf("Predicted: %d, Actual: %d\n", predicted, test_images[idx].label);
}
// Cleanup
free(train_images);
free(test_images);
return 0;
}- digit_recognizer.c - Complete digit recognizer
- mnist_loader.c - MNIST data loader
- image_processing.c - Image preprocessing
- network_training.c - Training utilities
- model_evaluation.c - Evaluation and metrics
- Implement data augmentation (rotation, scaling)
- Add convolutional layers
- Implement early stopping
- Save and load trained models
- Create a web interface for digit recognition
- Optimize using SIMD instructions
- Implement dropout regularization
- Try different architectures
- MNIST is the "Hello World" of machine learning
- Image preprocessing is crucial
- Neural networks can learn complex patterns
- Training requires careful tuning
- Evaluation metrics help assess performance
- Real-world applications need optimization
Ready for simulations? Head to Module 19: Scientific Computing
Pro Tip: Start with a smaller network and smaller dataset for quick iterations!