deep-river is a Python library for online deep learning. deep-river's ambition is to enable online machine learning for neural networks. It combines the river API with the capabilities of designing neural networks based on PyTorch.
π Documentation
The documentation contains an overview of all features of this repository as well as the repository's full features list. In each of these, the git repo reference is listed in a section that shows examples of the features and functionality. As we are always looking for further use cases and examples, feel free to contribute to the documentation or the repository itself via a pull request
pip install deep-riveror
pip install "river[deep]"You can install the latest development version from GitHub as so:
pip install https://github.com/online-ml/deep-river/archive/refs/heads/master.zipFor contributing to deep-river, we recommend using uv for fast dependency management and environment setup:
# Install uv if you haven't already
curl -LsSf https://astral.sh/uv/install.sh | sh
# Clone the repository
git clone https://github.com/online-ml/deep-river.git
cd deep-river
# Install all dependencies (including dev dependencies)
uv sync --extra dev
# Run tests
make test
# Format code
make format
# Build documentation
make docWe build the development of neural networks on top of the river API and refer to the rivers design principles. The following example creates a simple MLP architecture based on PyTorch and incrementally predicts and trains on the website phishing dataset. For further examples check out the Documentation.
>>> import random, numpy as np
>>> from river import metrics, datasets, preprocessing, compose
>>> from deep_river.classification import Classifier
>>> from torch import nn, manual_seed
>>> _ = manual_seed(42); random.seed(42); np.random.seed(42)
>>> first_x, _ = next(iter(datasets.Phishing()))
>>> n_features = len(first_x)
>>> class MyModule(nn.Module):
... def __init__(self, n_features):
... super().__init__()
... self.net = nn.Sequential(
... nn.Linear(n_features, 16),
... nn.ReLU(),
... nn.Linear(16, 2)
... )
... def forward(self, x):
... return self.net(x)
>>> model = compose.Pipeline(
... preprocessing.StandardScaler(),
... Classifier(
... module=MyModule(n_features),
... loss_fn='cross_entropy',
... optimizer_fn='adam',
... lr=1e-3,
... is_class_incremental=True
... )
... )
>>> metric = metrics.Accuracy()
>>> for i, (x, y) in enumerate(datasets.Phishing().take(200)):
... if i > 0:
... y_pred = model.predict_one(x)
... metric.update(y, y_pred)
... model.learn_one(x, y)
>>> assert 0.5 <= metric.get() <= 1.0
>>> print(f"Accuracy: {metric.get():.4f}") # doctest: +ELLIPSIS
Accuracy: ...>>> import random, numpy as np
>>> from river import stream, metrics, preprocessing, compose
>>> from sklearn import datasets as sk_datasets
>>> from deep_river.regression import MultiTargetRegressor
>>> from torch import nn, manual_seed
>>> _ = manual_seed(42); random.seed(42); np.random.seed(42)
>>> linnerud_stream = stream.iter_sklearn_dataset(sk_datasets.load_linnerud(), shuffle=True, seed=42)
>>> first_x, first_y = next(iter(linnerud_stream))
>>> n_features, n_outputs = len(first_x), len(first_y)
>>> class TinyNet(nn.Module):
... def __init__(self, n_features, n_outputs):
... super().__init__()
... self.net = nn.Sequential(
... nn.Linear(n_features, 16),
... nn.ReLU(),
... nn.Linear(16, n_outputs)
... )
... def forward(self, x):
... return self.net(x)
>>> model = compose.Pipeline(
... preprocessing.StandardScaler(), # feature scaling stabilizes training
... MultiTargetRegressor(
... module=TinyNet(n_features, n_outputs),
... loss_fn='mse', optimizer_fn='adam', lr=5e-3,
... is_feature_incremental=False, is_target_incremental=False,
... gradient_clip_value=1.0,
... )
... )
>>> mae_micro = metrics.multioutput.MicroAverage(metrics.MAE())
>>> mae_macro = metrics.multioutput.MacroAverage(metrics.MAE())
>>> rmse_micro = metrics.multioutput.MicroAverage(metrics.RMSE())
>>> # Recreate the iterator (the first sample was consumed to infer shapes)
>>> linnerud_stream = stream.iter_sklearn_dataset(sk_datasets.load_linnerud(), shuffle=True, seed=42)
>>> for i, (x, y_dict) in enumerate(linnerud_stream):
... if i > 0:
... y_pred = model.predict_one(x)
... mae_micro.update(y_dict, y_pred)
... mae_macro.update(y_dict, y_pred)
... rmse_micro.update(y_dict, y_pred)
... model.learn_one(x, y_dict)
>>> assert 0.0 <= mae_micro.get() < 300.0
>>> assert 0.0 <= mae_macro.get() < 300.0
>>> assert 0.0 <= rmse_micro.get() < 400.0
>>> print({
... 'MAE_micro': round(mae_micro.get(), 4),
... 'MAE_macro': round(mae_macro.get(), 4),
... 'RMSE_micro': round(rmse_micro.get(), 4)
... }) # doctest: +ELLIPSIS
{'MAE_micro': ..., 'MAE_macro': ..., 'RMSE_micro': ...}>>> import random, numpy as np
>>> from torch import nn, manual_seed
>>> from river import metrics
>>> from deep_river.anomaly import Autoencoder
>>> _ = manual_seed(42); random.seed(42); np.random.seed(42)
>>> # Create a deterministic synthetic stream: normals (y=0) vs. anomalies (y=1)
>>> def synthetic_stream(n_norm=2000, n_anom=200, n_features=8, seed=42):
... rng = np.random.default_rng(seed)
... # Normals around 0, anomalies shifted
... X_norm = rng.normal(loc=0.0, scale=1.0, size=(n_norm, n_features))
... y_norm = np.zeros(n_norm, dtype=int)
... X_anom = rng.normal(loc=5.0, scale=1.0, size=(n_anom, n_features))
... y_anom = np.ones(n_anom, dtype=int)
... # Interleave anomalies every k steps to keep streaming flavour
... k = max(1, n_norm // n_anom)
... X, Y = [], []
... i_norm = i_anom = 0
... for t in range(n_norm + n_anom):
... take_anom = (t % k == 0) and (i_anom < n_anom)
... if take_anom:
... X.append(X_anom[i_anom]); Y.append(y_anom[i_anom]); i_anom += 1
... else:
... if i_norm < n_norm:
... X.append(X_norm[i_norm]); Y.append(y_norm[i_norm]); i_norm += 1
... elif i_anom < n_anom:
... X.append(X_anom[i_anom]); Y.append(y_anom[i_anom]); i_anom += 1
... for row, label in zip(X, Y):
... yield {f"f{i}": float(v) for i, v in enumerate(row)}, int(label)
>>> n_features = 8
>>> class MyAutoEncoder(nn.Module):
... def __init__(self, n_features, latent_dim=4):
... super().__init__()
... self.encoder = nn.Sequential(
... nn.Linear(n_features, latent_dim),
... nn.LeakyReLU()
... )
... self.decoder = nn.Sequential(
... nn.Linear(latent_dim, n_features),
... nn.Sigmoid()
... )
... def forward(self, x):
... z = self.encoder(x)
... return self.decoder(z)
>>> ae = Autoencoder(module=MyAutoEncoder(n_features), lr=5e-3, optimizer_fn='adam')
>>> metric = metrics.ROCAUC()
>>> # Train only on normal samples to keep the model focused on the normal manifold
>>> for x, y in synthetic_stream(n_norm=2000, n_anom=200, n_features=n_features, seed=42):
... score = ae.score_one(x)
... if y == 0:
... ae.learn_one(x)
... metric.update(y, score)
>>> print(f"ROCAUC: {metric.get():.4f}") # doctest: +ELLIPSIS
ROCAUC: ...To acknowledge the use of the DeepRiver library in your research, please refer to our paper published on Journal of Open Source Software (JOSS):
@article{Kulbach2025,
doi = {10.21105/joss.07226},
url = {https://doi.org/10.21105/joss.07226},
year = {2025},
publisher = {The Open Journal},
volume = {10},
number = {105},
pages = {7226},
author = {Cedric Kulbach and Lucas Cazzonelli and Hoang-Anh Ngo and Max Halford and Saulo Martiello Mastelini},
title = {DeepRiver: A Deep Learning Library for Data Streams},
journal = {Journal of Open Source Software}
}