| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
English | 简体中文
NumPy-Keras, originally named NumPyMultilayerPerceptron, is a deep learning library implemented purely with numpy, covering the classic architecture trilogy — multilayer perceptrons, convolutional networks and recurrent networks (SimpleRNN/LSTM/GRU). Its purpose is to provide a simple and easy-to-understand implementation, aimed at facilitating learning and teaching.
Figure 1. Multilayer Perceptron
Important
Major Update: A Completely New Implementation
git clone https://github.com/XavierSpycy/NumPy-Keras.git
cd NumPy-Kerasconda create -n numpy_keras python=3.12 -yconda activate numpy_keraspip install -e .To avoid installing extra dependencies for additional features, we have commented out the non-numpy dependencies in requirements.txt. If you need these features, you can uncomment the lines and rerun pip install -r requirements.txt.
If you are using miniconda, you may also need to install dependencies for Jupyter Notebook.
pip3 install jupyter ipywidgetsA Chinese tutorial series in tutorials/ builds the whole trilogy from scratch, one concept per article, each with complete runnable code and measured numbers:
| # | 文章 | 主题 |
|---|---|---|
| 00 | 五分钟上手 | honest train/test split, first MNIST model |
| 01 | 激活函数全解 | 17 activations, derivatives on post-activation values, vanishing gradients |
| 02 | 损失函数 | MSE vs cross-entropy, the softmax+CE combined gradient |
| 03 | 反向传播逐行拆解 | chain rule through Sequential, finite-difference gradient checking, autograd comparison |
| 04 | 优化器进化史 | SGD → Momentum → NAG → Adagrad → Adadelta → Adam |
| 05 | 学习率与九大调度器 | lr sweep, nine schedulers, ReduceLROnPlateau + EarlyStopping |
| 06 | MLP 深入 | initializer scales and the 12-layer deep network |
| 07 | Dropout | inverted dropout and the overfitting comparison |
| 08 | BatchNormalization | train/inference modes and running statistics |
| 09 | CNN 解剖 | im2col, LeNet and feature maps |
| 10 | RNN 三部曲 | SimpleRNN/LSTM/GRU and BPTT |
| 11 | 引擎室 (可选) | the duck-typed layer contract, adding a layer |
| 12 | Cython 加速 (可选) | compiled kernels and benchmark methodology |
| 13 | CuPy GPU 加速 (可选) | backend switch, host/device boundary, where the bottleneck moves |
Each article stands alone, and its code is byte-identical to tutorials/code/*.py; see tutorials/README.md for the full index and the Zhihu/CSDN publishing checklist.
Multi-layer Perceptron (MLP) is one of the most basic neural network models. It consists of an input layer, one or more hidden layers, and an output layer. Each layer consists of multiple neurons, each with an activation function. An MLP is a feedforward neural network, and its output is calculated by forward propagation from the input layer to the output layer.
The current mainstream deep learning frameworks, such as TensorFlow, PyTorch, etc., provide efficient implementations, but the underlying implementations are complex and difficult to understand. Therefore, to better understand the principles of deep learning, we provide from-scratch NumPy implementations of the classic architectures — MLP, CNN and RNN (SimpleRNN/LSTM/GRU) — whose internals are written to read like a textbook.
We mentioned Keras because our implementation was inspired by the Keras interface. From the interface perspective, Keras provides a high-level interface that allows users to easily build neural network models, making it very suitable for beginners because it is simple and easy to understand. For this reason, TensorFlow 2.0 and later versions also use Keras as their high-level interface.
When we open the official website of TensorFlow, we can see the following code example:
import tensorflow as tf
mnist = tf.keras.datasets.mnist
(x_train, y_train),(x_test, y_test) = mnist.load_data()
x_train, x_test = x_train / 255.0, x_test / 255.0
model = tf.keras.models.Sequential([
tf.keras.layers.Flatten(input_shape=(28, 28)),
tf.keras.layers.Dense(128, activation='relu'),
tf.keras.layers.Dropout(0.2),
tf.keras.layers.Dense(10, activation='softmax')
])
model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
model.fit(x_train, y_train, epochs=5)
model.evaluate(x_test, y_test)Meanwhile, we can see the following code example in our framework:
import csv
import itertools
import numpy as np
import numpy_keras as keras
def load_mnist(path, n_rows=None):
"""Load a label-first MNIST CSV: the first column is the label and the
remaining 784 columns are pixels (0-255)."""
with open(path) as f:
rows = list(itertools.islice(csv.reader(f), n_rows))
y = np.array([int(r[0]) for r in rows])
X = np.array([[float(v) for v in r[1:]] for r in rows]) / 255.0
return X, y
# The training and test sets come from two different files.
np.random.seed(0)
X_train, y_train = load_mnist("data/mnist_train_small.csv", n_rows=5000)
X_test, y_test = load_mnist("data/mnist_test.csv", n_rows=1000)
X_train = X_train.reshape(-1, 28, 28)
X_test = X_test.reshape(-1, 28, 28)
model = keras.models.Sequential([
keras.layers.Flatten(input_shape=(28, 28)),
keras.layers.Dense(128, activation='relu', kernel_initializer='he_normal'),
keras.layers.Dropout(0.2),
keras.layers.Dense(10, activation='softmax')
])
model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
history = model.fit(X_train, y_train, epochs=5, verbose=1)
print(f"Accuracy on the training set: {model.evaluate(X_train, y_train):.2%}")
print(f"Accuracy on the test set: {model.evaluate(X_test, y_test):.2%}")
# Outputs (np.random.seed(0), pure NumPy mode, Apple M2 Pro):
# Accuracy on the training set: 96.72%
# Accuracy on the test set: 93.30%We can notice that our framework is very similar to the Keras interface, which allows beginners to refer to a more mature framework and build and train neural network models by themselves.
Detailed usage can be found in our Jupyter Notebook.
In addition to the basic implementation, we also provide some additional features, such as:
To conclude, our framework is a lightweight framework that only depends on the numpy library and provides a simple and easy-to-understand implementation. We hope that this framework can help users better understand the principles of deep learning and better use deep learning frameworks.
A pytest suite in tests covers the layers, losses, optimizers, callbacks and the CNN/RNN paths, plus the Cython parity, GPU (CuPy) parity, float32 and autograd-subpackage tests (python -m pytest tests/ -q, currently 315 tests; the GPU/optional-dependency parts skip automatically when unavailable). The library is developed and tested in the following environment:
Although we expect to achieve the most lightweight implementation, we still provide some additional features for better usability. For example, we provide a progress bar during training to better monitor the training process (requires the tqdm library); we also provide a function to plot the training history to better visualize the training process (requires the matplotlib library).
We have implemented these features using lazy loading, so you only need numpy to run this library when you don't need these features.
To avoid errors, we recommend using our environment or an environment similar to ours.
Here are the versions of tqdm, matplotlib, and autograd:
If there are no major differences in the versions, we believe that this library should work on other versions as well.
The autograd/ subpackage (an automatic-differentiation mirror of the API: forward-only layers plus gradients via autograd.grad) is covered by tests/test_autograd.py, which is skipped automatically when autograd is not installed.
By default, everything runs on pure NumPy. If you would like to speed up training and inference, you can compile the optional Cython kernels (one fused pass per parameter array for the optimizer updates, fused bias/activation passes for Dense, compiled elementwise activations, and compiled col2im / max-pooling kernels for the convolutional layers):
pip install cython>=3.0.10 python build_cython.py build_ext --inplace
The library detects the compiled module at import time and uses it automatically; when it is not built (or when the NUMPY_KERAS_DISABLE_CYTHON environment variable is set), it falls back to the pure NumPy implementations with identical behavior. The pure and compiled paths are pinned against each other by parity tests (tests/test_cython_kernels.py), and benchmarks/bench_cython.py measures the speedup. For the CNN, im2col itself is deliberately not compiled — building the column matrix is a strided copy that NumPy already performs at memcpy speed — but the scatter-add back (col2im, ~16x on one batch) and the max-pooling window scan (~2.7x) are, and the convolution's matmul + activation reuse the fused Dense kernels. The RNN layers have no kernels: each timestep is already a BLAS matrix product, so whether the Python loop over timesteps is worth compiling remains to be measured.
The table below was produced with python benchmarks/bench_cython.py (MLP rows: 5 repetitions; CNN row: 3 repetitions; mean ± standard deviation; the two modes were measured in the same session). Both modes include the pure-Python hot-path fixes (metrics-skip, cached activation lookups), so the speedup shown comes from the Cython layer alone.
| Configuration | Pure NumPy | Cython | Speedup |
|---|---|---|---|
| teaching-scale 3000x64, hidden [128, 64, 1], 5 epochs, batch 32 | 0.195s ± 0.021 | 0.136s ± 0.001 | ~1.4x |
| MNIST-like 10000x784, hidden [256, 256, 10], 3 epochs, batch 64 | 25.16s ± 2.24 | 19.15s ± 1.32 | ~1.3x |
| LeNet-style CNN (6@5x5 / pool / 16@5x5 / pool / 120 / 10) on 2000x28x28 MNIST, 2 epochs, batch 32 | 14.47s ± 0.66 | 8.69s ± 0.30 | ~1.7x |
Measured on an Apple M2 Pro (Mac14,9, 16 GB RAM, macOS arm64) with Python 3.12.8 and NumPy 1.26.4. The speedup depends on the CPU, the BLAS implementation, and the matrix sizes, so do not expect identical numbers on other machines. Absolute times in particular are sensitive to concurrent load — BLAS-heavy workloads are affected the most (in an idle window on the same machine the script measured ~2.5s vs ~4.1s for the MNIST-like configuration), while the speedup ratio stayed in the ~1.3-1.6x band across conditions. Re-run the benchmark on your own machine before drawing conclusions. Training trajectories are essentially identical in both modes: on the CNN configuration above, two epochs of the two modes reached the same loss and accuracy to four decimal places.
The library also ships an optional CuPy GPU backend that follows the same philosophy as the Cython layer: the default behavior (pure NumPy) is unchanged, the GPU path is opt-in, and it degrades gracefully (a warning plus fallback to NumPy when CuPy is not installed).
Installation (pick the wheel matching your CUDA version; cupy-cuda12x works on any driver >= 12.x, e.g. a CUDA 13.0 driver with a 12.1 toolkit):
pip install numpy-keras[cupy] # or: pip install cupy-cuda12x>=13.6.0
Enabling the GPU backend (both ways are equivalent; the environment variable is read at import time, while set_backend can be called at any time, e.g. from a notebook after the package is already imported):
export NUMPY_KERAS_BACKEND=cupy # option 1: environment variable
import numpy_keras
numpy_keras.set_backend("cupy") # option 2: runtime switchnumpy_keras.get_backend() reports the active backend ("numpy" / "cupy"). No manual data movement is needed: at the entry of fit / predict / evaluate the model automatically syncs its parameters, gradients, BatchNorm statistics and optimizer state to the active device (in both directions). Random number generation, data preparation (shuffling, one-hot encoding) and label/metric math always stay on the host — so with the same seed, CPU and GPU runs get bit-identical initial weights and Dropout masks. The two paths are pinned against each other by tests/test_cupy.py (44 tests covering activations, dense/conv/pooling, the three RNNs, the four optimizers, end-to-end training, a finite-difference gradient check and backend switching). Numerically, GPU reduction orders and libm differ from NumPy at the ~1-ulp level; the library stays float64 throughout, and training trajectories agree to 4-5 decimal places.
Measured speedups (2× NVIDIA A800 80GB, Python 3.12.3, cupy-cuda12x 13.6.0, via benchmarks/bench_cupy.py, against the pure NumPy path): matrix-bound operators benefit massively — elementwise activations 61-203x, Dense forward/backward 45-59x, the fused optimizer kernels ~83x (the device twin of the Cython kernels: one GPU kernel per param array — the pure path's per-op launch overhead was 40% of the model-level time), convolution and pooling 5-13x. Model-level (fit timed end to end, including host-side data prep; MLP 10000x784 [256,256,10], 3 epochs, batch 64, warmed-up medians; Cython and CuPy are the library's two optional acceleration layers, and a machine with the Cython kernels compiled gets them on the CPU side automatically):
| Configuration | Pure NumPy | CPU + Cython | CuPy | GPU/Pure NumPy |
|---|---|---|---|---|
| no metrics | 2.47 s | 1.67 s | 0.78 s | ~3.2x |
| + accuracy metric | 2.20 s | 2.13 s | 1.05 s | ~2.1x |
| tutorial script (with metric, tutorials/code/13_cupy.py) | — | 2.20 s | 1.20 s | ~1.8x |
Known exception: RNNs at this teaching scale are slower on the GPU (the per-timestep Python loop is dominated by many small kernel launches; the GPU branch batches the input projection into one 3D matmul, which is not enough to offset the launch overhead — only much larger N/T/U pays off on the device). Re-run the benchmark on your own machine before drawing conclusions.
Compute dtype: Sequential(..., dtype="float32") runs the model in float32 (float64 is the default). Parameters, gradients and every layer output stay at the chosen precision — cupy's clip/maximum turn Python scalars into 0-d float64 arrays and silently promote float32, so the library enforces the dtype at the two choke points (activation outputs and the loss gradient; see the 9 tests in tests/test_float32.py). float32 halves device memory and pays off hugely on consumer GPUs (an RTX 4090's fp64 throughput is 1/64 of its fp32); on the A800 used here cuBLAS runs float64 on FP64 tensor cores too, so at teaching scale the two dtypes measure about the same (f64 1.10s / f32 1.15s, identical accuracy). The Cython kernels stay float64-only; float32 models automatically take the pure NumPy path or the fused GPU kernels.
Perhaps you have a question: We only tested on the MNIST dataset, and our model performed very well in terms of accuracy. But is it possible that our model overfits on the MNIST dataset? How does it perform on other datasets?
We are going to test our model on a simple random dataset and a slightly more complex ten-classification problem to better understand how our model performs on other datasets.
When introducing this module, we may involve some core features. If you have any questions about these concepts, we will explain them in detail in the following sections to help you better understand them.
We will test on a simple random dataset. You can view more details by visiting our Jupyter Notebook.
import numpy as np
import matplotlib.pyplot as pltTo ensure the reproducibility of our experiments and the comparability between different models, we set a random seed.
np.random.seed(3407)
y_1 = np.hstack([np.random.normal(1, 1, size=(100, 2)), np.ones(shape=(100, 1))])
y_2 = np.hstack([np.random.normal(-1, 1, size=(40, 2)), -np.ones(shape=(40, 1))])
dataset = np.vstack([y_1, y_2])
X_train, y_train = dataset[:, 0:2], dataset[:, 2]Following the construction of the dataset, we will visualize the dataset.
Now, we can import our framework to witness the performance of our model on this dataset.
import numpy_keras as kerasTo better visualize the decision boundary, we provide a function to plot the decision boundary.
def plot_decision_boundary(model, X_train, y_train):
xx, yy = np.meshgrid(np.arange(-2, 2, .02), np.arange(-2, 2, .02))
Z = model.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
plt.figure(figsize=(15,7))
plt.subplot(1, 2, 1)
plt.pcolormesh(xx, yy, Z>0, cmap='cool')
plt.scatter(X_train[:, 0], X_train[:, 1], c=[(['b', 'r'])[int(d>0)] for d in y_train], s=100)
plt.xlim(-2, 2)
plt.ylim(-2, 2)
plt.grid()
plt.title('Labels')
plt.subplot(1, 2, 2)
plt.pcolormesh(xx, yy, Z>0, cmap='cool')
plt.scatter(X_train[:, 0], X_train[:, 1], c=[(['b', 'r'])[int(d>0)] for d in model.predict(X_train)], s=100)
plt.xlim(-2, 2)
plt.ylim(-2, 2)
plt.grid()
plt.title('Predictions')We will use a simple model to solve this problem.
layers = [
keras.layers.Input(2),
keras.layers.Dense(3, activation="relu", kernel_initializer='he_normal'),
keras.layers.Dense(1, activation='tanh')
]Although we construct a dataset for a classification problem, in fact, any classification problem can be transformed into a regression problem. We will use the Mean Squared Error (MSE) as the loss function and use $R^2$ as the evaluation metric.
To begin with, we will use the Stochastic Gradient Descent (SGD) optimizer, which is the first optimizer that most beginners encounter and is widely used. To compare the performance under the same learning rate, we will not use the string form to pass the optimizer, but pass an instance of the optimizer class and set the learning rate to $1 \times 10^{-3}$ (usually the default value of the Adam optimizer).
model = keras.Sequential(layers)
model.compile(loss='mse', optimizer=keras.optimizers.SGD(1e-3), metrics=['r2_score'])
history = model.fit(X_train, y_train, batch_size=2, epochs=500, verbose=1)
keras.plot_history(history)
Figure 4. Training History (with SGD Optimizer)
Figure 5. Decision Boundary (with SGD Optimizer)
Next, we will use a more advanced optimizer, the Adam optimizer, to train our model. The Adam optimizer adjusts the learning rate for each parameter by computing the first and second moments of the gradients. In simpler terms, the Adam optimizer is an adaptive learning rate optimizer. It allows us to converge faster and makes it easier to adjust the learning rate.
model.compile(loss='mse', optimizer='adam', metrics=['r2_score'])
Figure 6. Training History (with Adam Optimizer)
Figure 7. Decision Boundary (with Adam Optimizer)
It is worth noting that the decision boundary has an interesting "corner".
We will use a model containing a Dropout layer to solve this problem. Dropout is a regularization technique that randomly drops a portion of neurons during training to reduce overfitting.
layers = [
keras.layers.Input(2),
keras.layers.Dense(3, activation="relu", kernel_initializer='he_normal'),
keras.layers.Dropout(0.2),
keras.layers.Dense(1, activation='tanh')
]
Figure 8. Training History (with Dropout)
Figure 9. Decision Boundary (with Dropout)
We will use a model containing a BatchNormalization layer to solve this problem. BatchNormalization is a technique used in neural networks to standardize the activations of a given input layer in small batches, helping to normalize and accelerate the training process. In common cognition, we usually follow the order of Linear -> BatchNormalization -> Activation -> Dropout. Therefore, we will try this order here.
layers = [
keras.layers.Input(2),
keras.layers.Dense(3, activation=None),
keras.layers.BatchNormalization(),
keras.layers.Activation('relu'),
keras.layers.Dropout(0.2),
keras.layers.Dense(1, activation='tanh')
]
Figure 10. Training History (with BatchNormalization)
Figure 11. Decision Boundary (with BatchNormalization)
Unfortunately, the BatchNormalization layer causes the loss function to oscillate sharply, and from the decision boundary, the model's performance is very poor. This is because our batch size is too small for the BatchNormalization layer to work properly. This is also a drawback of the BatchNormalization layer, as it is very sensitive to the batch size.
In terms of the sensitivity of the BatchNormalization layer to the batch size, we will try different batch sizes to better understand the sensitivity of the BatchNormalization layer to the batch size.
history = model.fit(X_train, y_train, batch_size=16, epochs=500, verbose=1)
Figure 12. Training History (with BatchNormalization, batch size = 16)
Figure 13. Decision Boundary (with BatchNormalization, batch size = 16)
As we can see, when the batch size is 16, the loss function stability of the BatchNormalization layer has improved, and the decision boundary looks more reasonable. However, due to the small size of our dataset, we cannot solve the problem of the BatchNormalization layer by increasing the batch size, nor can we explore the sensitivity of the BatchNormalization layer to the batch size in more depth. We hope this will provide a starting point for further exploration.
We have always considered this problem as a regression problem, but in fact, it is a classification problem. We will use a model containing a Softmax layer and use the cross-entropy loss function to solve this problem. By the way, we have always considered this problem as a regression problem, but in fact, it is a classification problem. We will use a model containing a Softmax layer and use the cross-entropy loss function to solve this problem. By the way, our Sequential class adds layers through the add method, which is very similar to the Sequential class in Keras.
model = keras.Sequential()
model.add(keras.layers.Input(2))
model.add(keras.layers.Dense(3, activation='relu', kernel_initializer='he_normal'))
model.add(keras.layers.Dropout(0.2))
model.add(keras.layers.Dense(2, activation='softmax'))
model.compile(loss='sparse_categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
Figure 14. Training History (Classification Problem)
Figure 15. Decision Boundary (Classification Problem)
In this section, we will test on a slightly more complex ten-classification problem.
Since we already have test data in our dataset, we will not need to split the dataset. However, in real-world scenarios, we usually split the dataset into a training set, a validation set, and a test set. Typically, the validation set and the test set are often confused, although strictly speaking, this is incorrect, especially in production environments, where the test set often does not have real labels and may also have a different distribution from the training data.
To better understand the performance of the model and the performance that the model can achieve, we will treat the test set as both the test set and the validation set. The former is used to simulate the test set as a separate set, while the latter is treated as a validation set split from the training set.
You can view more details by visiting our Jupyter Notebook.
import numpy as np
np.random.seed(42)
X_train = np.load('data/train_data.npy')[:10000]
y_train = np.load('data/train_label.npy').squeeze()[:10000]
X_test = np.load('data/test_data.npy')
y_test = np.load('data/test_label.npy').squeeze()
print(X_train.shape, y_train.shape, X_test.shape, y_test.shape)In general, our dataset contains 50,000 training samples and 10,000 test samples, with a feature dimension of 128. The example below trains on the first 10,000 training samples so that the whole experiment finishes within minutes on an ordinary laptop; remove the slices to train on all 50,000 (about 35-45 minutes with the full 60-epoch budget on the reference machine).
Here, we use numpy_keras to build a multi-layer perceptron (MLP) model. We will use a model with 12 hidden layers. We will use the ELU activation function and use the He uniform initializer to initialize the weights. We will also use the Dropout layer to reduce overfitting.
import numpy_keras as keras
model = keras.Sequential()
model.add(keras.layers.Input(shape=X_train.shape[1]))
model.add(keras.layers.Dense(120, activation='elu', kernel_initializer='he_uniform'))
model.add(keras.layers.Dropout(0.25))
model.add(keras.layers.Dense(112, activation='elu', kernel_initializer='he_uniform'))
model.add(keras.layers.Dropout(0.20))
model.add(keras.layers.Dense(96, activation='elu', kernel_initializer='he_uniform'))
model.add(keras.layers.Dropout(0.15))
model.add(keras.layers.Dense(64, activation='elu', kernel_initializer='he_uniform'))
model.add(keras.layers.Dropout(0.10))
model.add(keras.layers.Dense(32, activation='elu', kernel_initializer='he_uniform'))
model.add(keras.layers.Dense(24, activation='elu', kernel_initializer='he_uniform'))
model.add(keras.layers.Dense(16, activation='elu', kernel_initializer='he_uniform'))
model.add(keras.layers.Dense(10, activation='softmax'))In this section, we also provide the model architecture we used. The selected architecture is based on personal experience with deep learning tasks in the past, and by no means represents an optimal architecture in any practical sense. We strongly recommend that you choose the appropriate model architecture based on your task requirements and dataset characteristics. Feel free to try different architectures and choose the best one based on experimental results.
Figure 16. A Complex Model Architecture
We use the Adam optimizer and the SparseCategoricalCrossentropy loss. We deliberately do not pass metrics: every metric triggers a full-data prediction at the end of each epoch, which on 50,000 samples costs almost as much as the training step itself. The callbacks therefore monitor val_loss (always recorded when validation data is given), and the final accuracies are computed once after training.
early_stop = keras.callbacks.EarlyStopping('val_loss', mode='min', patience=5, restore_best_weights=True)
lr_scheduler = keras.callbacks.ReduceLROnPlateau('val_loss', mode='min', factor=0.5, patience=3, min_lr=1e-6)
model.compile(optimizer='adam', loss='sparse_categorical_crossentropy')history = model.fit(X_train, y_train, epochs=60, batch_size=128, verbose=1, callbacks=[early_stop, lr_scheduler], validation_split=0.1)You can also pass the test set itself as the validation data; the recipe is identical and the main difference is where early stopping lands:
history = model.fit(X_train, y_train, epochs=60, batch_size=128, verbose=1, callbacks=[early_stop, lr_scheduler], validation_data=(X_test, y_test))keras.plot_history(history)
Figure 17. Loss Function and Accuracy Curve
Given the shortcomings of markdown in rendering mathematical formulas, we will only list the functions we have implemented here, rather than displaying the mathematical formulas. However, we highly recommend the documentation of PyTorch. It provides excellent formulas and figures to help you better understand the activation functions. Our implementation refers to this documentation. We would also like to thank the PyTorch development team for their contributions to the open-source community.
In addition to the above activation functions, we have implemented more activation functions in our autograd module, which is used for automatic differentiation. In our autograd module, we have implemented more activation functions in addition to the above activation functions:
For the use of automatic differentiation (autograd), you only need to make the following changes to the code:
Replace the following import statement:
import numpy_keras as keraswith:
import numpy_keras.autograd as kerasyou can seamlessly switch to the functionality of autograd.
Definition:
Batch normalization is a technique used in neural networks to standardize the activations of a given input layer in small batches, helping to normalize and accelerate the training process.
Mathematical Representation:
For a given mini-batch, $B$, of size $m$, with activations $x$:
$\mu_B = \frac{1}{m}\Sigma_{i=1}^m x_i$
$\sigma_B^2 = \frac{1}{m}\Sigma_{i=1}^m (x_i - \mu_B)^2$
$\hat {x_i} = \frac{x_i - \mu_B}{\sqrt{\sigma_B^2 + \epsilon}}$
$y_i = \gamma \hat {x_i} + \beta$
LSTM:
GRU:
Note
Every layer chains through its own activation inside backward (evaluated on its cached post-activation output); the criterion only computes the loss and its raw gradient. For RNN layers, whose output is not a single elementwise activation, the gate and candidate derivatives are applied per timestep inside backward, and their activation property simply reports None. Also, a return_sequences=True RNN outputs (N, T, U); a Dense after it needs a Flatten in between (the model raises a helpful error otherwise). The RNN layers are pure NumPy — no Cython kernels, since the per-timestep work is already BLAS matrix products (see §2.1).
A LeNet-style CNN on the bundled MNIST subset (data/mnist_train_small.csv) builds exactly like the MLPs above:
import csv
import numpy as np
from numpy_keras import Sequential
from numpy_keras import layers
np.random.seed(0)
with open("data/mnist_train_small.csv") as f:
rows = list(csv.reader(f))[:2000] # 2,000 of the 20,000 rows
X = np.array([[float(v) for v in r[1:]] for r in rows]) / 255.0
y = np.array([int(r[0]) for r in rows])
X = X.reshape(-1, 28, 28, 1) # (N, H, W, C)
model = Sequential()
model.add(layers.Input((28, 28, 1)))
model.add(layers.Conv2D(6, kernel_size=5, activation="relu"))
model.add(layers.MaxPool2D(pool_size=2))
model.add(layers.Conv2D(16, kernel_size=5, activation="relu"))
model.add(layers.MaxPool2D(pool_size=2))
model.add(layers.Flatten())
model.add(layers.Dense(120, activation="tanh"))
model.add(layers.Dense(10, activation="softmax"))
model.compile(loss="sparse_categorical_crossentropy", optimizer="adam",
metrics=["accuracy"])
history = model.fit(X, y, batch_size=32, epochs=2, shuffle=True)
# Accuracy on the training set: 93.20% after 2 epochs on 2,000 samples
# (np.random.seed(0), pure NumPy mode). On the first 1,000 rows of
# data/mnist_test.csv the same model reaches 89.30%.The same images can be read row by row as a sequence — 28 timesteps of 28 pixels each:
X = X.reshape(-1, 28, 28) # (N, T, F): one row per timestep
X, y = X[:800], y[:800] # keep the example fast
model = Sequential()
model.add(layers.Input((28, 28)))
model.add(layers.LSTM(32))
model.add(layers.Dense(10, activation="softmax"))
model.compile(loss="sparse_categorical_crossentropy", optimizer="adam",
metrics=["accuracy"])
model.optimizer.learning_rate = 0.01
history = model.fit(X, y, batch_size=32, epochs=10, shuffle=True)
# Accuracy on the training set: ~80-88% after 10 epochs on 800 samples,
# depending on the random seedRight here, we have implemented the optimizers commonly used by beginners to help them better understand the optimization process of deep learning. We believe that these optimizers are very helpful for beginners to understand the optimization process of deep learning.
Note
We believe that the interface of Keras regarding learning rate schedulers is not very elegant. From the perspective of usability, it is very worthwhile to optimize the interface of Keras regarding learning rate schedulers. Although our implementation in this part did not meet our expected standards, we still decided to adjust the implementation of LearningRateScheduler to improve the rationality and usability of the interface. In other words, this part of the implementation is quite different from Keras. We also believe that the torch.optim.lr_scheduler module of PyTorch is very worth learning from. It provides many learning rate schedulers and has a very elegant interface. Therefore, this part of the implementation is mainly inspired by PyTorch. Thanks again!
In addition, although the parameters of the learning rate scheduler are slightly adjusted, the overall interface is very similar to Keras. We believe that such adjustments are reasonable because they make the interface more user-friendly and more intuitive, especially for beginners.
NumPy-Keras is a deep learning library implemented using NumPy, covering the classic architecture trilogy — multilayer perceptrons, convolutional networks and recurrent networks (SimpleRNN/LSTM/GRU). It aims to provide a simple and easy-to-understand neural network implementation to help users learn and teach deep learning. The library does not depend on any third-party deep learning frameworks such as TensorFlow or PyTorch and only requires Python and NumPy to run, making it suitable for beginners to understand the basic principles of deep learning. NumPy-Keras provides an API similar to Keras and supports features such as visualization of the training process, progress bar display, and training history charts. With this lightweight framework, users can quickly build and train models and explore the basic concepts of deep learning.
| Back | FazBrowse Home | New Git URL |