Skip to main content

MLKit

A small, dependency-free neural network library written in C. Supports dense feed-forward networks with ReLU / sigmoid / softmax / linear activations, HalfMSE / CrossEntropy losses, mini-batch SGD training, binary weight import/export, and a REINFORCE policy-gradient reinforcement learning API.

#include "MLKit.h"

Python

A ctypes wrapper lives in python/pymlkit and is built from the same C sources.

pip install pymlkit        # or, from a clone: pip install .
from pymlkit import Network, Activations, Losses

model = Network(2)
model.add_dense(16, Activations.ReLU)
model.add_dense(1, Activations.Sigmoid)
model.set_loss(Losses.HalfMSE)
model.fit(train_x, train_y, lr=0.1, epochs=100, batch=16)
print(model.forward([0.1, 0.2]))

Full example: python/examples/circle_classifier.py. To build only the C library: cmake -S . -B build && cmake --build build.


Table of Contents


Core Concepts

A Network is a list of Layers. Layer 0 is always a LAYER_INPUT layer (a pass-through with no weights). Every subsequent layer is LAYER_DENSE, fully connected to the previous layer, with its own activation function.

Each layer stores its parameters as flat float* arrays (weights, biases, outputs, z-values, deltas, errors, gradients) rather than an array of per-neuron structs. The weight for neuron n, incoming connection w in a layer lives at index n * layer->tensor + w inside layer->weights.

Typical supervised usage:

Network* net = nn_network_create(input_size);
nn_add_layer_dense(net, hidden_size, activation_reLU);
nn_add_layer_dense(net, output_size, activation_softmax);
nn_network_set_loss(net, CrossEntropy);

nn_network_fit(net, x_train, num_samples, y_train, lr, epochs, batch_size, print_every);
nn_network_test(net, x_test, y_test, num_test_samples);

nn_network_export(net, "model.mlk");
nn_network_destroy(net);

API Reference

Network lifecycle

Network* nn_network_create(int input_size)

Allocates a new Network with a single LAYER_INPUT layer of input_size neurons. Default loss is HalfMSE. Caller owns the returned pointer and must call nn_network_destroy on it.

Network* net = nn_network_create(784); // 28x28 flattened image

void nn_add_layer_dense(Network* network, int neuron_count, ActivationFunction activation)

Appends a fully-connected layer with neuron_count neurons. The input size is inferred automatically from the previous layer. Weights are randomly initialized and scaled by the activation:

Activation Init scale
activation_reLU He — sqrt(2 / fan_in)
activation_softmax Xavier — sqrt(1 / fan_in)
everything else 1.0
nn_add_layer_dense(net, 128, activation_reLU);
nn_add_layer_dense(net, 10,  activation_softmax); // output layer

⚠️ Add layers in order — output layer last.

void nn_network_set_loss(Network* network, LossFunction loss_f)

Sets the loss function used during training and evaluation. Defaults to HalfMSE if never called.

nn_network_set_loss(net, CrossEntropy);

void nn_network_destroy(Network* network)

Frees all layer arrays and the network struct. No other function should be called on the network after this.


Propagation

float* nn_network_forward(Network* net, float* x)

Runs one input sample through the network. Returns a pointer directly into net->output_layer->outputs — a float* of length output_layer->neuron_count. The pointer is valid until the next forward call or nn_network_destroy.

x must be exactly net->input_size floats — there is no bounds check.

float* out = nn_network_forward(net, x);
printf("class 3 score: %f\n", out[3]);

void nn_network_backward(Network* net, float* y)

Backpropagates for one sample against target vector y (length == output layer's neuron_count). Accumulates gradients into each layer's weights_gradient / bias_gradient buffers — no weight update yet. Call nn_network_forward for the same sample immediately before this.

float y[10] = { 0,0,1,0,0,0,0,0,0,0 }; // one-hot label
nn_network_forward(net, x);
nn_network_backward(net, y);

Gradients are averaged and applied only when nn_apply_gradients is called (which nn_network_fit does automatically every batch steps).

void nn_apply_gradients(Network* net, float lr)

Averages accumulated gradients over the current batch count, applies the SGD update to all weights and biases, then zeroes the gradient buffers. Called automatically by nn_network_fit; call directly only in a custom training loop.

nn_network_forward(net, x);
nn_network_backward(net, y);
// ... repeat for batch_size samples ...
nn_apply_gradients(net, 0.01f);

Training & testing

void nn_network_fit(Network* net, float* x, int data_size, float* y, float lr, int epochs, int batch, int print_steps)

Trains for epochs epochs over a flattened dataset using mini-batch gradient descent.

Parameter Description
x data_size x input_size floats, samples contiguous
y data_size x output_size floats, targets contiguous
lr Learning rate
batch Samples per mini-batch. Pass 1 for per-sample SGD.
print_steps Print a loss readout every print_steps epochs

Prints a live progress bar per epoch and a final "Finished training" message.

nn_network_fit(net, x_train, 60000, y_train, 0.01f, 50, 32, 1);

float nn_calculate_loss(Network* net, float* x, float* y, int data_size)

Returns mean per-sample loss over data_size samples without updating weights. Useful for validation sets or periodic diagnostics.

float val_loss = nn_calculate_loss(net, x_val, y_val, 1000);

void nn_network_test(Network* net, float* x, float* y, int samples)

Reports classification accuracy by comparing argmax(output) to argmax(y) per sample.

Assumes a softmax / one-hot output. Not meaningful for regression or multi-label outputs.

nn_network_test(net, x_test, y_test, 10000);
// -> "The model was correct 9421/10000 times. With accuracy 94.210000%"

void nn_network_print(Network network)

Prints every layer's neurons with bias, input size, output, z-value, delta, and weight count. Intended for debugging small networks.

nn_network_print(*net);

Import / export

Binary format: flat float32 stream, layer-by-layer (input layer skipped), neuron-by-neuron: bias, weight[0], weight[1], ..., weight[tensor-1].

void nn_network_export(Network* net, char* filepath)

Writes all parameters to filepath, overwriting any existing file. Prints the total parameter count on success.

nn_network_export(net, "model.mlk");

int nn_network_import_file(Network* net, char* filepath)

Loads parameters from a file into an already-constructed network with matching architecture. Returns 0 on success, -1 if the file can't be opened.

Architecture mismatch is not validated — mismatched sizes will silently read garbage or overrun the buffer.

Network* net = nn_network_create(784);
nn_add_layer_dense(net, 128, activation_reLU);
nn_add_layer_dense(net, 10,  activation_softmax);
if (nn_network_import_file(net, "model.mlk") != 0) {
    fprintf(stderr, "Failed to load model\n");
}

void nn_network_import(Network* net, float* buffer, int size)

Same as nn_network_import_file but from an in-memory buffer. size is in bytes.

nn_network_import(net, buf, buf_size_in_bytes);

Helpers

int nn_biggest_output_idx(Layer* output_l)

Returns the index of the neuron with the highest output — equivalent to argmax over the output layer.

float* out = nn_network_forward(net, x);
int predicted_class = nn_biggest_output_idx(net->output_layer);

Reinforcement learning

MLKit includes a REINFORCE policy-gradient trainer built on top of the core network API. The workflow is:

  1. Create a Network with a softmax output (action probabilities).
  2. Wrap it in an RLAgent (one or more parallel replay buffers).
  3. Each step: run forward, sample an action, remember the transition.
  4. At the end of a rollout: call rl_policy_fit to update weights.

RLAgent* rl_agent_create(Network* model, int agent_count)

Creates an RLAgent owning agent_count independent ReplayBuffers. The agent borrows model — destroy the network separately.

RLAgent* agent = rl_agent_create(net, 1);

void rl_agent_destroy(RLAgent* agent)

Frees all replay buffers and the agent struct. Does not free the network.

int rl_draw_action(Layer* output_l)

Samples an action index from the output layer's probability distribution (expects softmax). Call after nn_network_forward.

nn_network_forward(net, state);
int action = rl_draw_action(net->output_layer);

void rl_remember(ReplayBuffer* buff, float* state, float reward, int action_idx, int should_discount, int done)

Appends one transition to a replay buffer. Buffer grows dynamically (doubles capacity when full).

Parameter Description
state Input features for this step (copied)
reward Immediate reward received
action_idx The action that was taken
should_discount 1 to include in return computation, 0 to skip
done 1 if this step ends the episode
rl_remember(&agent->buffers[0], state, reward, action, 1, done);

void rl_replaybuff_clear(ReplayBuffer* buff)

Resets the buffer size to 0 (retains allocated capacity). Called automatically by rl_policy_fit after consuming each buffer.

void rl_policy_fit(RLAgent* agent, int* buffer_ids, int buffers_count, float lr, float gamma)

Runs REINFORCE over the selected buffers:

  1. Discounts rewards backward within each episode using gamma.
  2. Computes policy gradient and accumulates gradients for each step.
  3. Calls nn_apply_gradients once to update weights.
  4. Clears all consumed buffers.
int bufs[] = { 0 };
rl_policy_fit(agent, bufs, 1, 0.001f, 0.99f);

Usage Examples

Supervised learning (XOR)

#include <stdio.h>
#include <time.h>
#include "MLKit.h"

int main(void) {
    srand((unsigned)time(NULL));

    float x_train[8] = { 0,0,  0,1,  1,0,  1,1 };
    float y_train[4] = { 0, 1, 1, 0 };

    Network* net = nn_network_create(2);
    nn_add_layer_dense(net, 8, activation_reLU);
    nn_add_layer_dense(net, 1, activation_sigmoid);
    nn_network_set_loss(net, CrossEntropy);

    nn_network_fit(net, x_train, 4, y_train, 0.1f, 2000, 4, 500);

    printf("\nPredictions after training:\n");
    for (int i = 0; i < 4; i++) {
        float* out = nn_network_forward(net, &x_train[i * 2]);
        printf("  %.0f XOR %.0f -> %.4f (expected %.0f)\n",
               x_train[i*2], x_train[i*2+1], out[0], y_train[i]);
    }

    nn_network_export(net, "xor_model.mlk");
    nn_network_destroy(net);
    return 0;
}

Build and run:

gcc -o example example.c MLKit.c -lm
./example

Expected output:

Finished training 2000, 2000 epochs

Predictions after training:
  0 XOR 0 -> 0.0046 (expected 0)
  0 XOR 1 -> 0.9965 (expected 1)
  1 XOR 0 -> 0.9964 (expected 1)
  1 XOR 1 -> 0.0049 (expected 0)
Saved a total of 33 parameters to xor_model.mlk path

Reusing a saved model:

Network* net = nn_network_create(2);
nn_add_layer_dense(net, 8, activation_reLU);
nn_add_layer_dense(net, 1, activation_sigmoid);
nn_network_import_file(net, "xor_model.mlk");

float* out = nn_network_forward(net, sample);
// out[0] is the prediction

Reinforcement learning (REINFORCE)

Skeleton for a single-agent policy gradient loop:

#include <string.h>
#include <time.h>
#include "MLKit.h"

#define STATE_SIZE  8
#define NUM_ACTIONS 4

int main(void) {
    srand((unsigned)time(NULL));

    Network* net = nn_network_create(STATE_SIZE);
    nn_add_layer_dense(net, 64, activation_reLU);
    nn_add_layer_dense(net, NUM_ACTIONS, activation_softmax);
    nn_network_set_loss(net, CrossEntropy);

    RLAgent* agent = rl_agent_create(net, 1);

    for (int episode = 0; episode < 1000; episode++) {
        float state[STATE_SIZE] = { /* initial observation */ };
        int done = 0;

        while (!done) {
            nn_network_forward(net, state);
            int action = rl_draw_action(net->output_layer);

            float reward;
            float next_state[STATE_SIZE];
            // env_step(action, next_state, &reward, &done);

            rl_remember(&agent->buffers[0], state, reward, action, 1, done);
            memcpy(state, next_state, sizeof(state));
        }

        int buf_ids[] = { 0 };
        rl_policy_fit(agent, buf_ids, 1, 0.001f, 0.99f);
    }

    rl_agent_destroy(agent);
    nn_network_destroy(net);
    return 0;
}

See examples/snake/ for a complete Snake game agent using this API.


Types

ActivationFunction

typedef enum {
    activation_reLU,
    activation_linear,
    activation_softmax,
    activation_sigmoid,
    NONE
} ActivationFunction;
Value Behavior
activation_reLU max(0, z). He weight init (sqrt(2/fan_in)).
activation_linear Identity. Use for regression or DQN Q-value heads.
activation_softmax Numerically-stable softmax. Output layer only. Xavier init (sqrt(1/fan_in)).
activation_sigmoid 1 / (1 + e^-z).
NONE No activation. Used internally for the input layer.

LossFunction

typedef enum {
    HalfMSE,
    CrossEntropy
} LossFunction;
Value Formula (per output neuron) Typical pairing
HalfMSE 0.5 * (output - y)^2 activation_linear / activation_sigmoid, regression
CrossEntropy -y * log(output) activation_softmax (multi-class) or activation_sigmoid (binary)

When using CrossEntropy + softmax or CrossEntropy + sigmoid, the library uses the simplified combined gradient delta = output - y directly, cancelling the softmax / cross-entropy Jacobian.

LayerType

typedef enum {
    LAYER_INPUT,
    LAYER_DENSE
} LayerType;

Set automatically by nn_network_create (LAYER_INPUT) and nn_add_layer_dense (LAYER_DENSE). Not intended to be set manually.

Layer

typedef struct {
    int neuron_count;
    float* weights;           // flat: weights[n * tensor + w]
    float* biases;            // one per neuron
    float* outputs;           // activated outputs (a)
    float* z_values;          // pre-activation weighted sums (z)
    float* deltas;            // dL/dz per neuron
    float* errors;            // scratch for softmax delta
    float* weights_gradient;  // accumulated per weight across a batch
    float* bias_gradient;     // accumulated per neuron across a batch
    int tensor;               // == previous layer neuron_count
    ActivationFunction activation;
    LayerType type;
} Layer;

To read the weight connecting input w to neuron n:

float w = layer->weights[n * layer->tensor + w_idx];

Network

typedef struct {
    Layer* layers;
    Layer* output_layer;   // pointer into layers[layers_count - 1]
    int layers_count;
    int layers_capacity;   // grows by doubling; don't touch directly
    int input_size;
    int batched;           // samples accumulated in current batch
    LossFunction loss_function;
} Network;

ReplayBuffer

typedef struct {
    float* states;          // flat: states[step * state_size + i]
    int*   actions_idx;
    float* rewards;
    int*   should_discount;
    int*   done;
    int    state_size;
    int    size;            // current stored transitions
    int    capacity;        // allocated slots (doubles on overflow)
} ReplayBuffer;

RLAgent

typedef struct {
    Network*      network;
    ReplayBuffer* buffers;
    int           buffers_count;
} RLAgent;

Known Limitations

  • nn_network_test accuracy assumes a softmax/one-hot output — don't use it for regression or multi-label models.
  • Mini-batching slices x/y in the order given — no shuffling between epochs, so batch composition is fixed across the whole training run.
  • nn_network_import / nn_network_import_file do not validate that the loaded file matches the target architecture — mismatches fail silently or overread the buffer.
  • No malloc/fopen failure checks in some code paths — treat this as a learning/prototyping library, not production-hardened code.

Metadata

Release files for pymlkit 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pymlkit 0.1.0
File Size Uploaded Provenance
pymlkit-0.1.0.tar.gz 15.8 kB With provenance Details

Built distributions (wheels)

Table of built distributions (wheels) for pymlkit 0.1.0
File
pymlkit-0.1.0-py3-none-win_amd64.whl Python 3 none Windows x86-64 Details
pymlkit-0.1.0-py3-none-win32.whl Python 3 none Windows x86-32 Details
pymlkit-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl Python 3 none Linux glibc 2.17+ x86-64 Details
pymlkit-0.1.0-py3-none-macosx_11_0_arm64.whl Python 3 none macOS 11.0+ ARM64 Details
pymlkit-0.1.0-py3-none-macosx_10_13_x86_64.whl Python 3 none macOS 10.13+ x86-64 Details

Total release size: 133.8 kB

Release files / pymlkit-0.1.0.tar.gz

Download URL pymlkit-0.1.0.tar.gz
Size 15.8 kB
Tags Source
SHA-256 checksum
How to use checksums
bbf630031e358653e1b4a9a59e890973b157cef4d01ad5d59f070044e208e12a
BLAKE2b-256 checksum
How to use checksums
b3f66b2bbabf439edb26c3881cde354eaf455edbb7bae58a4507d723e6f33000
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.

Transparency log

Release files / pymlkit-0.1.0-py3-none-win_amd64.whl

Download URL pymlkit-0.1.0-py3-none-win_amd64.whl
Size 24.2 kB
Tags Python 3 Windows x86-64
SHA-256 checksum
How to use checksums
8c3955b1d620b0a49551c1489ed8e4411ab5b812445f11501dd5cc10ec2da443
BLAKE2b-256 checksum
How to use checksums
747e750edf898ced33dd84e761a9e48178c80b37d7caaa715d20d727874f4aeb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.

Transparency log

Release files / pymlkit-0.1.0-py3-none-win32.whl

Download URL pymlkit-0.1.0-py3-none-win32.whl
Size 21.2 kB
Tags Python 3 Windows x86-32
SHA-256 checksum
How to use checksums
6e8f32a73d79e20616313a20779a9de5b9c8f3cf66570544e7816c0800b767f5
BLAKE2b-256 checksum
How to use checksums
625d12b476052134713f0276a0c741bb0182b73690a967cef1a1c646e31d2832
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.

Transparency log

Release files / pymlkit-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL pymlkit-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 21.7 kB
Tags Linux glibc 2.17+ x86-64 Python 3
SHA-256 checksum
How to use checksums
35a43950e4fea6ebb9c9b140896bd44cdda0d0822542c7de839ff15481b291cb
BLAKE2b-256 checksum
How to use checksums
fa35a11a2dc214f2a4f72e0f9248f1c89f833a072e9ab769b75cbdfdc0075d84
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.

Transparency log

Release files / pymlkit-0.1.0-py3-none-macosx_11_0_arm64.whl

Download URL pymlkit-0.1.0-py3-none-macosx_11_0_arm64.whl
Size 24.7 kB
Tags Python 3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
1285c2cbd7e40d6ca1ed20b91efe144e6ecf72130d8616b866e55e2fd9734281
BLAKE2b-256 checksum
How to use checksums
3ff6048cc616a6289972fec3ebc6eff7357f47f77b17b4d0ad17eb01c17f02d8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.

Transparency log

Release files / pymlkit-0.1.0-py3-none-macosx_10_13_x86_64.whl

Download URL pymlkit-0.1.0-py3-none-macosx_10_13_x86_64.whl
Size 26.3 kB
Tags Python 3 macOS 10.13+ x86-64
SHA-256 checksum
How to use checksums
f1fcd5aac4165e8c4d9bacfada5a46ef86949045c23d7e4797a7ba05e2f3d5c5
BLAKE2b-256 checksum
How to use checksums
479ea6fe205cf6185729c95fc9bce2e79f8ba26b68a61fb2ebcca7bb7b1644e3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.

Transparency log

Security for pymlkit 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 6 files in this release have been uploaded with provenance metadata.

Select from 6 files

Attestation for pymlkit-0.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

6 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page