You know that feeling when you’re using a powerful tool but have no idea how it works under the hood? That was me with LSTMs (Long Short-Term Memory networks).
Sure, I could import them from PyTorch or TensorFlow, but I felt like I was missing something crucial. So, I decided to do something crazy: implement an LSTM from scratch in C++.
You know that feeling when you’re using a powerful tool but have no idea how it works under the hood? That was me with LSTMs (Long Short-Term Memory networks). Sure, I could import them from PyTorch or TensorFlow, but I felt like I was missing something crucial. So, I decided to do something crazy: implement an LSTM from scratch in C++.
Was it practical? Probably not. Was it an incredible learning experience? Absolutely.

The Journey Begins
Let me tell you why I chose C++. Yes, Python is the go-to language for machine learning, but I wanted to strip away all abstractions. No automatic differentiation, no built-in optimizers, just raw mathematics and memory management. Plus, my background in systems programming made C++ a natural choice.
First Challenge: The Architecture
If you’re familiar with LSTMs, you know they’re not exactly simple. They have these things called gates — forget gate, input gate, and output gate — plus a cell state. Here’s how I broke it down:
struct DataSample {
vector<double> features;
double target;
};This simple structure was my foundation. Each sample would contain features and a target value. Nothing fancy, just the basics.
The Heart of the Beast: LSTM Cell
The real challenge came with implementing the LSTM cell itself. Here’s a snippet of my initialization code:
LSTMCell(int input_size, int hidden_size) {
this->input_size = input_size;
this->hidden_size = hidden_size;
// Initialize weights with small random values
double scale = 0.1;
Wf = vector<vector<double>>(hidden_size,
vector<double>(input_size + hidden_size, 0.0));
// … initialize other weights similarly
}I spent hours debugging this part. Pro tip: initializing weights properly is crucial. Too large or too small, and your network won’t learn.
The Forward Pass: Where the Magic Happens
The forward pass is where things get interesting.
vector<double> forward(vector<double> x, vector<double> h_prev,
vector<double> c_prev) {
// Forget gate
vector<double> f = sigmoid(Wf * concat(x, h_prev) + bf);
// Input gate
vector<double> i = sigmoid(Wi * concat(x, h_prev) + bi);
// Cell state update
vector<double> c = f * c_prev + i * tanh(Wc * concat(x, h_prev) + bc);
// Output
vector<double> o = sigmoid(Wo * concat(x, h_prev) + bo);
vector<double> h = o * tanh(c);
return h;
}Each line here represents hours of reading papers and debugging. The mathematics might look simple, but getting it right was anything but.
The Backpropagation Blues
If you think the forward pass was tricky, wait until you hear about backpropagation. This is where I really gained respect for deep learning frameworks.
void backward(vector<double> dh_next, vector<double> dc_next,
/* other params */) {
vector<double> do_t = dh_t * c_t * o_t * (1 - o_t);
vector<double> dc_t = dh_t * o_t * (1 - c_t * c_t);
// … more gradient computations
}I had to derive these equations by hand. Yes, you read that right. By. Hand. My notebook looked like a mathematical crime scene.
Things I Wish I Knew Before Starting
1. Gradient Clipping is Not Optional: My first implementation exploded. Literally, the numbers became NaN within seconds. Here’s what saved me:
My first implementation exploded. Literally, the numbers became NaN within seconds. Here’s what saved me:
double clip(double value, double min_value, double max_value) {
return max(min_value, min(value, max_value));
}2. Hyperparameters Matter: After much trial and error, I settled on:
— Learning rate: 0.01
— Hidden size: 32
— Number of epochs: 50
3. Memory Management is Crucial: C++ doesn’t have garbage collection. Every vector, every matrix, every tiny bit of memory needs to be managed properly.
What I Learned
This project taught me more than just how LSTMs work. I learned:
1. The importance of proper initialization
2. Why gradient clipping exists (the hard way)
3. How to debug when you can’t print tensors (because they don’t exist)
4. The value of unit tests in machine learning
Results and Reality Check
Did my implementation beat PyTorch’s? Ha! Not even close. But that wasn’t the point. When I finally got it working on some climate data, seeing those loss numbers go down was more satisfying than any pre-built solution could ever be.
What’s Next?
I’m thinking about adding:
- Bidirectional LSTM support
- Different optimization algorithms
- Dropout layers
- Batch normalization
But more importantly, I’m thinking about what other deep learning components I can build from scratch. Maybe transformers? (Just kidding… unless?)
The Code
If you’re crazy enough to want to try this yourself, the complete code is available on my GitHub. Feel free to use it, break it, fix it, or improve it.
Final Thoughts
Would I recommend building an LSTM from scratch for production? Absolutely not. Would I recommend it as a learning experience? Without a doubt. There’s something magical about seeing those matrices multiply and gradients flow, knowing you built every part of it.
If you enjoyed this deep dive into the internals of deep learning, follow me for more adventures in machine learning and software engineering. And yes, I’m already planning my next “build it from scratch” project!
Get in Touch 🤝
I’m always excited to connect with fellow engineers and researchers in AI/ML! Feel free to reach out for:
- Technical discussions about AI/ML/NLP
- Software architecture and cloud solutions
- Research collaboration opportunities
- Open source project contributions
☕Buy me a coffee: (https://buymeacoffee.com/anudeepadi)
🌐 Website: (https://anudeepadi.me)
💼 LinkedIn: (https://linkedin.com/in/adirajuadi)
🐙 GitHub: (https://github.com/anudeepadi)
Based in San Antonio, TX and open to remote opportunities.