Skip to content

Commit 3f78a9b

Browse files
authored
Merge pull request #69 from emlearn/linreg-fixup
linreg: Enable as external C module, include in Webassembly build
2 parents 41effd9 + 5ca83ee commit 3f78a9b

20 files changed

Lines changed: 779 additions & 491 deletions

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ webassembly: $(WEBASSEMBLY_MICROPYTHON)
8383

8484

8585
check_unix: $(UNIX_MICROPYTHON)
86-
$(UNIX_MICROPYTHON) tests/test_all.py test_iir,test_fft,test_arrayutils
86+
$(UNIX_MICROPYTHON) tests/test_all.py test_iir,test_fft,test_arrayutils,test_linreg
8787
# TODO: enable more modules
8888

8989
rp2: $(PORT_DIR)

docs/getting_started_browser.rst

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
2+
.. Places parent toc into the sidebar
3+
4+
:parenttoc: True
5+
6+
.. _getting_started_browser:
7+
8+
=========================
9+
Getting started for browser
10+
=========================
11+
12+
.. currentmodule:: emlearn-micropython
13+
14+
emlearn-micropython runs on most platforms that MicroPython does.
15+
This includes running in a web browser, using the `Webassembly port <https://github.com/micropython/micropython/tree/master/ports/webassembly>`_ of MicroPython.
16+
The browser integration is enabled by `PyScript <https://docs.pyscript.net/>`_.
17+
18+
Prerequisites
19+
===========================
20+
21+
A web browser and a file editor.
22+
An CPython 3.10+ installation is also recommended, to act as web server.
23+
24+
25+
emlearn-micropython build for browser
26+
==================================
27+
28+
We publish a pre-built MicroPython for each release.
29+
This includes emlearn-micropython as external C modules.
30+
31+
There are two files needed, the `micropython.mjs` and `micropython.wasm`.
32+
They can be downloaded from:
33+
34+
- https://raw.githubusercontent.com/emlearn/emlearn-micropython/refs/heads/gh-pages/builds/latest/ports/webassembly/micropython.mjs
35+
- https://raw.githubusercontent.com/emlearn/emlearn-micropython/refs/heads/gh-pages/builds/latest/ports/webassembly/micropython.wasm
36+
37+
The ``latest`` version can be changed to a tag to have a specific version (``0.11.0`` or later).
38+
39+
40+
Setup web page
41+
==================================
42+
43+
Create an `index.html` page with the following contents:
44+
45+
.. literalinclude:: helloworld_browser/index.html
46+
:language: html
47+
48+
Make sure that you have ``micropython.mjs`` and ``micropython.wasm`` in the same directory.
49+
50+
Try it out
51+
========================
52+
53+
Start a HTTP server to serve the files
54+
55+
.. code-block:: console
56+
57+
python -m http.server
58+
59+
Open your browser at http://localhost:8000
60+
61+
The MicroPython code using ``emlearn_linreg`` from emlearn-micropython should automatically run when you load the page.
62+
The webpage should show an output like like:
63+
64+
``Input: [10.0, 75.0], prediction: 16.96 C``
65+
66+
67+
Serving from device
68+
====================================
69+
70+
On a MicroPython device with networking (like ESP32),
71+
it can serve the browser frontend to clients.
72+
73+
We recommend using the excellent `MicroDot web framework <https://microdot.readthedocs.io/en/latest/>`_.
74+
75+
To be offline compatible, download PyScript and the MicroPython build files to your PC, and copy it to the device. Then update the HTML to have the local paths. See `PyScript offline <https://docs.pyscript.net/2026.3.1/user-guide/offline/#getting-micropython>`_ documentation for more information.
76+
77+
78+

docs/helloworld_browser/index.html

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport" content="width=device-width,initial-scale=1" />
6+
<title>emlearn-micropython in browser with PyScript</title>
7+
<link rel="stylesheet" href="https://pyscript.net/releases/2026.3.1/core.css">
8+
<script type="module" src="https://pyscript.net/releases/2026.3.1/core.js"></script>
9+
</head>
10+
<body>
11+
<!-- This should be in a config file, using mpy-config for brevity. -->
12+
<mpy-config>
13+
interpreter = "/micropython.mjs"
14+
</mpy-config>
15+
<script type="mpy">
16+
from pyscript import document
17+
18+
import emlearn_linreg
19+
import array
20+
21+
# Predict temperature from (hour_of_day, humidity)
22+
# y = 0.5*hour - 0.1*humidity + 15 + noise
23+
X = array.array('f', [
24+
8, 80, # 8am, 80% humidity
25+
12, 60, # noon
26+
16, 55, # 4pm
27+
20, 70, # 8pm
28+
0, 85, # midnight
29+
])
30+
y = array.array('f', [15.2, 18.4, 20.1, 16.8, 13.5])
31+
32+
model = emlearn_linreg.new(2, 0.01, 0.5, 0.0001)
33+
emlearn_linreg.train(model, X, y, max_iterations=100, tolerance=1e-6)
34+
35+
mse = model.score_mse(X, y)
36+
print(f"MSE: {mse:.4f}")
37+
38+
new_sample = array.array('f', [10, 75]) # 10am, 75% humidity
39+
out = model.predict(new_sample)
40+
print(f"Predicted temperature: {out:.1f} C")
41+
42+
document.body.append(f"Input: {list(new_sample)}, prediction: {out:.2f} C")
43+
44+
45+
</script>
46+
</body>
47+
</html>

docs/user_guide.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ User Guide
1616

1717
getting_started_host.rst
1818
getting_started_device.rst
19+
getting_started_browser.rst
1920
support.rst
2021
native_modules.rst
2122
external_modules.rst
25.1 KB
Binary file not shown.
100 KB
Binary file not shown.
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
2+
#!/usr/bin/env python3
3+
"""
4+
Download and preprocess California housing dataset for MicroPython testing.
5+
Saves scaled train/test splits as .npy files.
6+
"""
7+
8+
import os
9+
import time
10+
11+
import numpy as np
12+
from sklearn.linear_model import ElasticNet
13+
from sklearn.metrics import mean_squared_error, r2_score
14+
from sklearn.datasets import fetch_california_housing
15+
from sklearn.model_selection import train_test_split
16+
from sklearn.preprocessing import StandardScaler
17+
18+
19+
def prepare_california_housing_data(data_dir, sample=None):
20+
"""Download, preprocess and save California housing dataset."""
21+
22+
print("Downloading California housing dataset...")
23+
# Load the dataset
24+
housing = fetch_california_housing()
25+
X, y = housing.data, housing.target
26+
27+
if sample is not None:
28+
indices = np.random.choice(X.shape[0], size=sample, replace=False)
29+
X = X[indices]
30+
y = y[indices]
31+
32+
print(f"Dataset shape: X={X.shape}, y={y.shape}")
33+
print(f"Features: {housing.feature_names}")
34+
print(f"Target: median house value in hundreds of thousands of dollars")
35+
36+
# Split into train/test (80/20)
37+
X_train, X_test, y_train, y_test = train_test_split(
38+
X, y, test_size=0.2, random_state=42
39+
)
40+
41+
print(f"Train set: X={X_train.shape}, y={y_train.shape}")
42+
print(f"Test set: X={X_test.shape}, y={y_test.shape}")
43+
44+
# Scale the features (standardization)
45+
scaler = StandardScaler()
46+
X_train_scaled = scaler.fit_transform(X_train)
47+
X_test_scaled = scaler.transform(X_test)
48+
49+
print("\nScaling applied:")
50+
print(f"Feature means: {scaler.mean_}")
51+
print(f"Feature stds: {scaler.scale_}")
52+
53+
# Convert to float32 for MicroPython compatibility
54+
X_train_scaled = X_train_scaled.astype(np.float32)
55+
X_test_scaled = X_test_scaled.astype(np.float32)
56+
y_train = y_train.astype(np.float32)
57+
y_test = y_test.astype(np.float32)
58+
59+
# Save as .npy files
60+
np.save(os.path.join(data_dir, 'X_train.npy'), X_train_scaled)
61+
np.save(os.path.join(data_dir, 'X_test.npy'), X_test_scaled)
62+
np.save(os.path.join(data_dir, 'y_train.npy'), y_train)
63+
np.save(os.path.join(data_dir, 'y_test.npy'), y_test)
64+
65+
print("\nSaved files:")
66+
print(f"X_train.npy: {X_train_scaled.shape} float32")
67+
print(f"X_test.npy: {X_test_scaled.shape} float32")
68+
print(f"y_train.npy: {y_train.shape} float32")
69+
print(f"y_test.npy: {y_test.shape} float32")
70+
71+
# Print some statistics for verification
72+
print("\nData statistics:")
73+
print(f"X_train range: [{X_train_scaled.min():.3f}, {X_train_scaled.max():.3f}]")
74+
print(f"y_train range: [{y_train.min():.3f}, {y_train.max():.3f}]")
75+
print(f"y_train mean: {y_train.mean():.3f}")
76+
77+
return X_train_scaled, X_test_scaled, y_train, y_test
78+
79+
80+
81+
def load_data(data_dir):
82+
"""Load the preprocessed California housing data."""
83+
print("Loading data...")
84+
X_train = np.load(os.path.join(data_dir, 'X_train.npy'))
85+
X_test = np.load(os.path.join(data_dir, 'X_test.npy'))
86+
y_train = np.load(os.path.join(data_dir, 'y_train.npy'))
87+
y_test = np.load(os.path.join(data_dir, 'y_test.npy'))
88+
89+
print(f"Train set: X={X_train.shape}, y={y_train.shape}")
90+
print(f"Test set: X={X_test.shape}, y={y_test.shape}")
91+
print(f"Data types: X={X_train.dtype}, y={y_train.dtype}")
92+
93+
return X_train, X_test, y_train, y_test
94+
95+
def test_elasticnet_configurations(data_dir):
96+
"""Test different ElasticNet configurations to find good baselines."""
97+
98+
X_train, X_test, y_train, y_test = load_data(data_dir)
99+
100+
# Test configurations: (alpha, l1_ratio, description)
101+
configs = [
102+
(0.0, 0.0, "No regularization (OLS)"),
103+
(0.01, 0.0, "Ridge (alpha=0.01)"),
104+
(0.01, 1.0, "LASSO (alpha=0.01)"),
105+
(0.01, 0.5, "ElasticNet (alpha=0.01, l1_ratio=0.5)"),
106+
(0.001, 0.5, "ElasticNet (alpha=0.001, l1_ratio=0.5)"),
107+
(0.1, 0.5, "ElasticNet (alpha=0.1, l1_ratio=0.5)"),
108+
]
109+
110+
print("\n" + "="*70)
111+
print("ElasticNet Configuration Comparison")
112+
print("="*70)
113+
print(f"{'Configuration':<35} {'Train MSE':<12} {'Test MSE':<12} {'R²':<8} {'Time':<8}")
114+
print("-"*70)
115+
116+
results = []
117+
118+
for alpha, l1_ratio, description in configs:
119+
start_time = time.time()
120+
121+
# Create and train model
122+
if alpha == 0.0:
123+
# Use regular linear regression for no regularization
124+
from sklearn.linear_model import LinearRegression
125+
model = LinearRegression()
126+
else:
127+
model = ElasticNet(alpha=alpha, l1_ratio=l1_ratio, max_iter=2000, random_state=42)
128+
129+
model.fit(X_train, y_train)
130+
131+
# Make predictions
132+
y_train_pred = model.predict(X_train)
133+
y_test_pred = model.predict(X_test)
134+
135+
# Calculate metrics
136+
train_mse = mean_squared_error(y_train, y_train_pred)
137+
test_mse = mean_squared_error(y_test, y_test_pred)
138+
test_r2 = r2_score(y_test, y_test_pred)
139+
140+
elapsed_time = time.time() - start_time
141+
142+
print(f"{description:<35} {train_mse:<12.6f} {test_mse:<12.6f} {test_r2:<8.3f} {elapsed_time:<8.3f}")
143+
144+
results.append({
145+
'config': description,
146+
'alpha': alpha,
147+
'l1_ratio': l1_ratio,
148+
'train_mse': train_mse,
149+
'test_mse': test_mse,
150+
'r2': test_r2,
151+
'time': elapsed_time,
152+
'model': model
153+
})
154+
155+
return results
156+
157+
158+
159+
def main():
160+
161+
here = os.path.dirname(__file__)
162+
data_dir = here
163+
164+
# Prepare the data
165+
prepare_california_housing_data(data_dir, sample=4000)
166+
167+
# Test different configurations
168+
results = test_elasticnet_configurations(data_dir)
169+
170+
#print(results)
171+
172+
173+
if __name__ == "__main__":
174+
main()
175+
3.25 KB
Binary file not shown.
12.6 KB
Binary file not shown.

0 commit comments

Comments
 (0)