measly 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- measly-0.1.0/.github/workflows/publish.yml +22 -0
- measly-0.1.0/.github/workflows/test.yml +20 -0
- measly-0.1.0/.gitignore +14 -0
- measly-0.1.0/LICENSE +21 -0
- measly-0.1.0/PKG-INFO +122 -0
- measly-0.1.0/README.md +99 -0
- measly-0.1.0/docs/learning-curve.png +0 -0
- measly-0.1.0/examples/capacity.py +127 -0
- measly-0.1.0/pyproject.toml +42 -0
- measly-0.1.0/src/measly/__init__.py +43 -0
- measly-0.1.0/src/measly/analysis.py +287 -0
- measly-0.1.0/src/measly/fit.py +145 -0
- measly-0.1.0/src/measly/interfaces.py +31 -0
- measly-0.1.0/src/measly/plot.py +81 -0
- measly-0.1.0/src/measly/sweep.py +97 -0
- measly-0.1.0/tests/conftest.py +46 -0
- measly-0.1.0/tests/test_analysis.py +104 -0
- measly-0.1.0/tests/test_fit.py +68 -0
- measly-0.1.0/tests/test_plot.py +16 -0
- measly-0.1.0/tests/test_sweep.py +62 -0
- measly-0.1.0/uv.lock +840 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
pypi:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read # a permissions block zeroes every scope not listed
|
|
13
|
+
id-token: write # mints the OIDC token PyPI trades for upload rights
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: astral-sh/setup-uv@v5
|
|
17
|
+
# The release tag and pyproject.toml are two sources of truth for the
|
|
18
|
+
# version. PyPI rejects a duplicate upload, so catch a mismatch here.
|
|
19
|
+
- if: github.event_name == 'release' # on dispatch the ref is a branch
|
|
20
|
+
run: grep -q "^version = \"${GITHUB_REF_NAME#v}\"$" pyproject.toml
|
|
21
|
+
- run: uv build
|
|
22
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
push:
|
|
6
|
+
branches: [main]
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
pytest:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
python: ["3.12", "3.13"]
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: astral-sh/setup-uv@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: ${{ matrix.python }}
|
|
19
|
+
- run: uv sync --group dev --locked
|
|
20
|
+
- run: uv run pytest tests -q
|
measly-0.1.0/.gitignore
ADDED
measly-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Remi Tregan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
measly-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: measly
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Estimate whether a model is data-limited, and whether more capacity would pay off.
|
|
5
|
+
Project-URL: Homepage, https://github.com/tremgan/measly
|
|
6
|
+
Project-URL: Source, https://github.com/tremgan/measly
|
|
7
|
+
Project-URL: Issues, https://github.com/tremgan/measly/issues
|
|
8
|
+
Author-email: Remi Tregan <tregan.remi@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: active-learning,learning-curve,sample-size,scaling-law
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering
|
|
17
|
+
Requires-Python: >=3.12
|
|
18
|
+
Requires-Dist: matplotlib>=3.8
|
|
19
|
+
Requires-Dist: numpy>=1.26
|
|
20
|
+
Requires-Dist: scipy>=1.17.1
|
|
21
|
+
Requires-Dist: xarray>=2026.9.0
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# measly🍽️
|
|
25
|
+
|
|
26
|
+
More data helps. measly estimates how much.
|
|
27
|
+
|
|
28
|
+
measly estimates whether a model is data-limited, meaning whether measuring more samples would improve its performance. It is built for fields where each measurement is slow and expensive, such as the life sciences.
|
|
29
|
+
|
|
30
|
+
Note that measly's sweep trains `'n_draws' * 'n_models' * 'n_fractions'` models, which can be extremely computationally expensive. The premise is that this is (should be) still cheaper than collecting new data. If this is not the case, measly isn't appropriate for your use case.
|
|
31
|
+
|
|
32
|
+
## Problem Definition
|
|
33
|
+
|
|
34
|
+
measly fits a model to many random subsets of the dataset at different fractions of its size. Each draw gives one learning curve, and each curve is fitted with a scaling law. The default is `pow4`:
|
|
35
|
+
|
|
36
|
+
$$L(n) = L_\infty + A\,(n + d)^{-\alpha}$$
|
|
37
|
+
|
|
38
|
+
`pow3`, the three-parameter form most of the literature quotes, is `pow4`
|
|
39
|
+
with $d$ fixed at zero:
|
|
40
|
+
|
|
41
|
+
$$L(n) = L_\infty + A\,n^{-\alpha}$$
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+

|
|
45
|
+
|
|
46
|
+
At 75 examples the 75-feature model is more than three times worse. At 750 it
|
|
47
|
+
is 1% better. The curves cross near 400.
|
|
48
|
+
|
|
49
|
+
A pilot study hides that crossover. Measured only at 75 examples, you would
|
|
50
|
+
have picked the small model and been wrong about every later round. A model's
|
|
51
|
+
rank at one sample size says little about its rank at the next.
|
|
52
|
+
|
|
53
|
+
Both gains are small but positive, and the larger model gains about 40% more.
|
|
54
|
+
So collecting more data helps a little, and mostly the high-capacity model.
|
|
55
|
+
|
|
56
|
+
The gain interval is far tighter than the loss interval. Every size
|
|
57
|
+
in a draw is scored on the same test set, so that draw's whole curve shifts up
|
|
58
|
+
or down together. The shift dominates the loss band and cancels in the gain,
|
|
59
|
+
because each curve is compared against its own level, not an average over
|
|
60
|
+
draws.
|
|
61
|
+
|
|
62
|
+
## Does it work?
|
|
63
|
+
|
|
64
|
+
The example data is synthetic, so the projection can be checked against the
|
|
65
|
+
truth. [`examples/capacity.py`](examples/capacity.py) draws extra rows from the
|
|
66
|
+
same process, refits at four sizes past the measured range, and scores on
|
|
67
|
+
20,000 held-out rows. These are the open diamonds in the figure. The larger
|
|
68
|
+
sets extend the pilot, as collecting more data would. Only the new rows are
|
|
69
|
+
redrawn across the ten repeats.
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
model examples actual projected interval error
|
|
73
|
+
ridge(20 features) 938 1.1592 +-0.0027 1.0795 [0.9534, 1.2388] 6.9%
|
|
74
|
+
ridge(20 features) 1125 1.1539 +-0.0047 1.0762 [0.9357, 1.2321] 6.7%
|
|
75
|
+
ridge(20 features) 1312 1.1519 +-0.0047 1.0737 [0.9322, 1.2308] 6.8%
|
|
76
|
+
ridge(20 features) 1500 1.1499 +-0.0042 1.0717 [0.9312, 1.2301] 6.8%
|
|
77
|
+
ridge(75 features) 938 1.0736 +-0.0091 1.1035 [0.9201, 1.3113] 2.8%
|
|
78
|
+
ridge(75 features) 1125 1.0594 +-0.0090 1.1006 [0.9139, 1.3095] 3.9%
|
|
79
|
+
ridge(75 features) 1312 1.0515 +-0.0091 1.0963 [0.9079, 1.3087] 4.3%
|
|
80
|
+
ridge(75 features) 1500 1.0466 +-0.0057 1.0961 [0.9040, 1.3084] 4.7%
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
All eight land inside their intervals, never more than 6.9% from the median.
|
|
84
|
+
|
|
85
|
+
The projected loss shows no consistent bias. Over eight pilots the projection
|
|
86
|
+
at 2x sat above what actually happened in 3 of 8 for the nearly flat model and
|
|
87
|
+
5 of 8 for the steeply descending one. The mean error was +0.4% and +2.4%, and
|
|
88
|
+
all sixteen intervals held the truth. The scatter comes from scoring on a slice
|
|
89
|
+
of your own pilot.
|
|
90
|
+
|
|
91
|
+
The audit stops at twice the measured pool. The held-back check fits on the
|
|
92
|
+
smaller sizes and predicts the largest two, a stretch of about 1.9x, so that
|
|
93
|
+
is as far as the method carries evidence. Projecting ten times out would
|
|
94
|
+
return a number and support none of it.
|
|
95
|
+
|
|
96
|
+
The error bars on the diamonds are smaller than the markers. Ten independent
|
|
97
|
+
collections at one size vary by 0.0027 to 0.0091, while the band spans about
|
|
98
|
+
0.3, so the band is 30 to 100 times wider. That is the safe direction to err
|
|
99
|
+
in. Most of the width is test-set noise, not uncertainty about the curve.
|
|
100
|
+
|
|
101
|
+
Subsets are drawn without replacement. Each draw resplits the pool, so even the
|
|
102
|
+
full-size fit varies across draws. A bootstrap would add little to that spread
|
|
103
|
+
and would bias the level up, because a resample repeats rows.
|
|
104
|
+
|
|
105
|
+
`analyse` always checks itself. It refits the curves on the smaller sample
|
|
106
|
+
sizes, predicts the largest ones you already measured, and reports the error.
|
|
107
|
+
Above 15% the projection MUST NOT be relied on: the shape fits your data but
|
|
108
|
+
does not predict it.
|
|
109
|
+
|
|
110
|
+
`analyse(...).results` is an `xarray.DataArray` over `(fraction, model, draw)`
|
|
111
|
+
if you want the raw measurements.
|
|
112
|
+
|
|
113
|
+
## Previous work in this area/Inspiration
|
|
114
|
+
|
|
115
|
+
- [The Shape of Learning Curves: a Review](https://arxiv.org/abs/2103.10948). Viering & Loog, 2021.
|
|
116
|
+
- [Deep Learning Scaling is Predictable, Empirically](https://arxiv.org/abs/1712.00409). Hestness et al., 2017.
|
|
117
|
+
- [Scaling Laws for Neural Language Models](https://arxiv.org/abs/2001.08361). Kaplan et al., 2020.
|
|
118
|
+
- [Training Compute-Optimal Large Language Models](https://arxiv.org/abs/2203.15556). Hoffmann et al., 2022.
|
|
119
|
+
- [Revisiting Neural Scaling Laws in Language and Vision](https://arxiv.org/abs/2209.06640). Alabdulmohsin et al., 2022.
|
|
120
|
+
- [Broken Neural Scaling Laws](https://github.com/ethancaballero/broken_neural_scaling_laws). Caballero et al., 2022.
|
|
121
|
+
- [How Much More Data Do I Need?](https://research.nvidia.com/labs/toronto-ai/estimatingrequirements/). Mahmood et al., 2022.
|
|
122
|
+
- [Estimation of Predictive Performance in High-Dimensional Data Settings using Learning Curves](https://arxiv.org/abs/2206.03825). Goedhart et al., 2022.
|
measly-0.1.0/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# measly🍽️
|
|
2
|
+
|
|
3
|
+
More data helps. measly estimates how much.
|
|
4
|
+
|
|
5
|
+
measly estimates whether a model is data-limited, meaning whether measuring more samples would improve its performance. It is built for fields where each measurement is slow and expensive, such as the life sciences.
|
|
6
|
+
|
|
7
|
+
Note that measly's sweep trains `'n_draws' * 'n_models' * 'n_fractions'` models, which can be extremely computationally expensive. The premise is that this is (should be) still cheaper than collecting new data. If this is not the case, measly isn't appropriate for your use case.
|
|
8
|
+
|
|
9
|
+
## Problem Definition
|
|
10
|
+
|
|
11
|
+
measly fits a model to many random subsets of the dataset at different fractions of its size. Each draw gives one learning curve, and each curve is fitted with a scaling law. The default is `pow4`:
|
|
12
|
+
|
|
13
|
+
$$L(n) = L_\infty + A\,(n + d)^{-\alpha}$$
|
|
14
|
+
|
|
15
|
+
`pow3`, the three-parameter form most of the literature quotes, is `pow4`
|
|
16
|
+
with $d$ fixed at zero:
|
|
17
|
+
|
|
18
|
+
$$L(n) = L_\infty + A\,n^{-\alpha}$$
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+

|
|
22
|
+
|
|
23
|
+
At 75 examples the 75-feature model is more than three times worse. At 750 it
|
|
24
|
+
is 1% better. The curves cross near 400.
|
|
25
|
+
|
|
26
|
+
A pilot study hides that crossover. Measured only at 75 examples, you would
|
|
27
|
+
have picked the small model and been wrong about every later round. A model's
|
|
28
|
+
rank at one sample size says little about its rank at the next.
|
|
29
|
+
|
|
30
|
+
Both gains are small but positive, and the larger model gains about 40% more.
|
|
31
|
+
So collecting more data helps a little, and mostly the high-capacity model.
|
|
32
|
+
|
|
33
|
+
The gain interval is far tighter than the loss interval. Every size
|
|
34
|
+
in a draw is scored on the same test set, so that draw's whole curve shifts up
|
|
35
|
+
or down together. The shift dominates the loss band and cancels in the gain,
|
|
36
|
+
because each curve is compared against its own level, not an average over
|
|
37
|
+
draws.
|
|
38
|
+
|
|
39
|
+
## Does it work?
|
|
40
|
+
|
|
41
|
+
The example data is synthetic, so the projection can be checked against the
|
|
42
|
+
truth. [`examples/capacity.py`](examples/capacity.py) draws extra rows from the
|
|
43
|
+
same process, refits at four sizes past the measured range, and scores on
|
|
44
|
+
20,000 held-out rows. These are the open diamonds in the figure. The larger
|
|
45
|
+
sets extend the pilot, as collecting more data would. Only the new rows are
|
|
46
|
+
redrawn across the ten repeats.
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
model examples actual projected interval error
|
|
50
|
+
ridge(20 features) 938 1.1592 +-0.0027 1.0795 [0.9534, 1.2388] 6.9%
|
|
51
|
+
ridge(20 features) 1125 1.1539 +-0.0047 1.0762 [0.9357, 1.2321] 6.7%
|
|
52
|
+
ridge(20 features) 1312 1.1519 +-0.0047 1.0737 [0.9322, 1.2308] 6.8%
|
|
53
|
+
ridge(20 features) 1500 1.1499 +-0.0042 1.0717 [0.9312, 1.2301] 6.8%
|
|
54
|
+
ridge(75 features) 938 1.0736 +-0.0091 1.1035 [0.9201, 1.3113] 2.8%
|
|
55
|
+
ridge(75 features) 1125 1.0594 +-0.0090 1.1006 [0.9139, 1.3095] 3.9%
|
|
56
|
+
ridge(75 features) 1312 1.0515 +-0.0091 1.0963 [0.9079, 1.3087] 4.3%
|
|
57
|
+
ridge(75 features) 1500 1.0466 +-0.0057 1.0961 [0.9040, 1.3084] 4.7%
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
All eight land inside their intervals, never more than 6.9% from the median.
|
|
61
|
+
|
|
62
|
+
The projected loss shows no consistent bias. Over eight pilots the projection
|
|
63
|
+
at 2x sat above what actually happened in 3 of 8 for the nearly flat model and
|
|
64
|
+
5 of 8 for the steeply descending one. The mean error was +0.4% and +2.4%, and
|
|
65
|
+
all sixteen intervals held the truth. The scatter comes from scoring on a slice
|
|
66
|
+
of your own pilot.
|
|
67
|
+
|
|
68
|
+
The audit stops at twice the measured pool. The held-back check fits on the
|
|
69
|
+
smaller sizes and predicts the largest two, a stretch of about 1.9x, so that
|
|
70
|
+
is as far as the method carries evidence. Projecting ten times out would
|
|
71
|
+
return a number and support none of it.
|
|
72
|
+
|
|
73
|
+
The error bars on the diamonds are smaller than the markers. Ten independent
|
|
74
|
+
collections at one size vary by 0.0027 to 0.0091, while the band spans about
|
|
75
|
+
0.3, so the band is 30 to 100 times wider. That is the safe direction to err
|
|
76
|
+
in. Most of the width is test-set noise, not uncertainty about the curve.
|
|
77
|
+
|
|
78
|
+
Subsets are drawn without replacement. Each draw resplits the pool, so even the
|
|
79
|
+
full-size fit varies across draws. A bootstrap would add little to that spread
|
|
80
|
+
and would bias the level up, because a resample repeats rows.
|
|
81
|
+
|
|
82
|
+
`analyse` always checks itself. It refits the curves on the smaller sample
|
|
83
|
+
sizes, predicts the largest ones you already measured, and reports the error.
|
|
84
|
+
Above 15% the projection MUST NOT be relied on: the shape fits your data but
|
|
85
|
+
does not predict it.
|
|
86
|
+
|
|
87
|
+
`analyse(...).results` is an `xarray.DataArray` over `(fraction, model, draw)`
|
|
88
|
+
if you want the raw measurements.
|
|
89
|
+
|
|
90
|
+
## Previous work in this area/Inspiration
|
|
91
|
+
|
|
92
|
+
- [The Shape of Learning Curves: a Review](https://arxiv.org/abs/2103.10948). Viering & Loog, 2021.
|
|
93
|
+
- [Deep Learning Scaling is Predictable, Empirically](https://arxiv.org/abs/1712.00409). Hestness et al., 2017.
|
|
94
|
+
- [Scaling Laws for Neural Language Models](https://arxiv.org/abs/2001.08361). Kaplan et al., 2020.
|
|
95
|
+
- [Training Compute-Optimal Large Language Models](https://arxiv.org/abs/2203.15556). Hoffmann et al., 2022.
|
|
96
|
+
- [Revisiting Neural Scaling Laws in Language and Vision](https://arxiv.org/abs/2209.06640). Alabdulmohsin et al., 2022.
|
|
97
|
+
- [Broken Neural Scaling Laws](https://github.com/ethancaballero/broken_neural_scaling_laws). Caballero et al., 2022.
|
|
98
|
+
- [How Much More Data Do I Need?](https://research.nvidia.com/labs/toronto-ai/estimatingrequirements/). Mahmood et al., 2022.
|
|
99
|
+
- [Estimation of Predictive Performance in High-Dimensional Data Settings using Learning Curves](https://arxiv.org/abs/2206.03825). Goedhart et al., 2022.
|
|
Binary file
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""More capacity is worse on a pilot and better once the data arrives.
|
|
2
|
+
|
|
3
|
+
The small model is misspecified: it sees a subset of the features, so it has a
|
|
4
|
+
floor above the noise. The large model is unbiased but pays variance early.
|
|
5
|
+
The curves therefore cross, and which model is "better" depends entirely on
|
|
6
|
+
how much data you have.
|
|
7
|
+
|
|
8
|
+
Because the data is synthetic the projection can be audited rather than
|
|
9
|
+
trusted. The extra rows are drawn from the same process and the models
|
|
10
|
+
refitted on them, so the figure carries both what measly predicted and what
|
|
11
|
+
actually happened.
|
|
12
|
+
|
|
13
|
+
uv run --group demo python examples/capacity.py
|
|
14
|
+
|
|
15
|
+
Writes docs/learning-curve.png.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
from matplotlib.lines import Line2D
|
|
20
|
+
from sklearn.compose import ColumnTransformer
|
|
21
|
+
from sklearn.linear_model import Ridge
|
|
22
|
+
from sklearn.pipeline import Pipeline
|
|
23
|
+
|
|
24
|
+
from measly import analyse, mean_squared_error, plot
|
|
25
|
+
from measly.plot import GUIDE, PALETTE
|
|
26
|
+
|
|
27
|
+
TOTAL, CAPACITY = 75, 20
|
|
28
|
+
PILOT, NOISE = 3000, 1.0
|
|
29
|
+
TAIL = 0.05
|
|
30
|
+
"""Weight on every feature the small model omits.
|
|
31
|
+
|
|
32
|
+
It alone sets where the curves cross, at `NOISE**2 / TAIL**2` examples. The
|
|
33
|
+
feature counts set how steep the large model's descent is and how wide the
|
|
34
|
+
final gap is, without moving the crossing.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
FACTORS = (1.25, 1.5, 1.75, 2.0)
|
|
38
|
+
"""Multiples of the measured pool to audit the projection at.
|
|
39
|
+
|
|
40
|
+
Capped at 2x on purpose. The held-back check fits on the smaller sizes and
|
|
41
|
+
predicts the largest two, a stretch of `1.0 / 0.5179`, so roughly 1.9x is as
|
|
42
|
+
far as the method carries evidence. Auditing past that measures nothing the
|
|
43
|
+
tool claims.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
REPEATS = 10
|
|
47
|
+
"""Independent collections at each size, so the audit carries its own spread."""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def ridge(capacity):
|
|
51
|
+
"""Ridge that sees only the first `capacity` features."""
|
|
52
|
+
return Pipeline([
|
|
53
|
+
("capacity",
|
|
54
|
+
ColumnTransformer([("keep", "passthrough", slice(0, capacity))])),
|
|
55
|
+
("ridge", Ridge()),
|
|
56
|
+
])
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
rng = np.random.default_rng(0)
|
|
60
|
+
weights = np.concatenate([rng.normal(size=4), np.full(TOTAL - 4, TAIL)])
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def sample(n):
|
|
64
|
+
X = rng.normal(size=(n, TOTAL))
|
|
65
|
+
return X, X @ weights + rng.normal(0.0, NOISE, n)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# Named, because a Pipeline's repr embeds a memory address.
|
|
69
|
+
models = {f"ridge({capacity} features)": ridge(capacity)
|
|
70
|
+
for capacity in (CAPACITY, TOTAL)}
|
|
71
|
+
|
|
72
|
+
X_pilot, y_pilot = sample(PILOT)
|
|
73
|
+
result = analyse(models, X_pilot, y_pilot, rng=0)
|
|
74
|
+
print(result.summary())
|
|
75
|
+
print(f"\ncurves cross near {round(NOISE ** 2 / TAIL ** 2)} examples\n")
|
|
76
|
+
|
|
77
|
+
# Go and collect the data measly was asked about, then check it was right.
|
|
78
|
+
# A lab keeps its pilot and adds to it, so the larger sets contain the smaller
|
|
79
|
+
# ones. Only the new rows are redrawn across repeats, because the pilot is not
|
|
80
|
+
# something that gets collected again.
|
|
81
|
+
X_check, y_check = sample(20_000)
|
|
82
|
+
sizes = [round(result.n_train * factor) for factor in FACTORS]
|
|
83
|
+
X_base, y_base = X_pilot[:result.n_train], y_pilot[:result.n_train]
|
|
84
|
+
|
|
85
|
+
scores = {name: np.empty((REPEATS, len(sizes))) for name in models}
|
|
86
|
+
for repeat in range(REPEATS):
|
|
87
|
+
X_new, y_new = sample(max(sizes) - result.n_train)
|
|
88
|
+
X_more = np.vstack([X_base, X_new])
|
|
89
|
+
y_more = np.concatenate([y_base, y_new])
|
|
90
|
+
for name, model in models.items():
|
|
91
|
+
for column, n in enumerate(sizes):
|
|
92
|
+
model.fit(X_more[:n], y_more[:n])
|
|
93
|
+
scores[name][repeat, column] = mean_squared_error(
|
|
94
|
+
model.predict(X_check), y_check)
|
|
95
|
+
|
|
96
|
+
actual = {name: value.mean(axis=0) for name, value in scores.items()}
|
|
97
|
+
spread = {name: value.std(axis=0, ddof=1) for name, value in scores.items()}
|
|
98
|
+
|
|
99
|
+
print(f"{'model':>20} {'examples':>9} {'actual':>17} {'projected':>10}"
|
|
100
|
+
f" {'interval':>18} {'error':>7}")
|
|
101
|
+
for name in models:
|
|
102
|
+
for factor, n, measured, sd in zip(FACTORS, sizes, actual[name], spread[name]):
|
|
103
|
+
projected = result.project(factor)[name]
|
|
104
|
+
print(f"{name:>20} {n:>9} {f'{measured:.4f} +-{sd:.4f}':>17}"
|
|
105
|
+
f" {projected.loss:>10.4f}"
|
|
106
|
+
f" {f'[{projected.low:.4f}, {projected.high:.4f}]':>18}"
|
|
107
|
+
f" {abs(projected.loss - measured) / measured:>7.1%}"
|
|
108
|
+
f"{'' if projected.low <= measured <= projected.high else ' OUT'}")
|
|
109
|
+
|
|
110
|
+
ax = plot(result, factor=max(FACTORS))
|
|
111
|
+
for index, name in enumerate(result.models):
|
|
112
|
+
colour = PALETTE[index % len(PALETTE)]
|
|
113
|
+
ax.errorbar(sizes, actual[name], yerr=spread[name], fmt="D", ms=5.5,
|
|
114
|
+
mfc="white", mec=colour, mew=1.5, color=colour,
|
|
115
|
+
elinewidth=1.1, capsize=0, zorder=6)
|
|
116
|
+
|
|
117
|
+
legend = ax.get_legend_handles_labels()[0] + [
|
|
118
|
+
Line2D([], [], ls="none", marker="o", color=GUIDE, ms=5,
|
|
119
|
+
label="measured by measly"),
|
|
120
|
+
Line2D([], [], ls="none", marker="D", mfc="white", mec=GUIDE, mew=1.5,
|
|
121
|
+
ms=5.5, label="actual, after collecting more"),
|
|
122
|
+
]
|
|
123
|
+
ax.legend(handles=legend, frameon=False, fontsize=9, loc="upper right",
|
|
124
|
+
handlelength=1.6, borderaxespad=0)
|
|
125
|
+
ax.figure.tight_layout()
|
|
126
|
+
ax.figure.savefig("docs/learning-curve.png", dpi=120)
|
|
127
|
+
print("\nwrote docs/learning-curve.png")
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "measly"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Estimate whether a model is data-limited, and whether more capacity would pay off."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
requires-python = ">=3.12"
|
|
9
|
+
authors = [{ name = "Remi Tregan", email = "tregan.remi@gmail.com" }]
|
|
10
|
+
keywords = ["learning-curve", "scaling-law", "sample-size", "active-learning"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 3 - Alpha",
|
|
13
|
+
"Intended Audience :: Science/Research",
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
"Programming Language :: Python :: 3.13",
|
|
16
|
+
"Topic :: Scientific/Engineering",
|
|
17
|
+
]
|
|
18
|
+
dependencies = [
|
|
19
|
+
"matplotlib>=3.8",
|
|
20
|
+
"numpy>=1.26",
|
|
21
|
+
"scipy>=1.17.1",
|
|
22
|
+
"xarray>=2026.9.0",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/tremgan/measly"
|
|
27
|
+
Source = "https://github.com/tremgan/measly"
|
|
28
|
+
Issues = "https://github.com/tremgan/measly/issues"
|
|
29
|
+
|
|
30
|
+
[dependency-groups]
|
|
31
|
+
dev = ["pytest>=8"]
|
|
32
|
+
demo = ["scikit-learn>=1.4"]
|
|
33
|
+
|
|
34
|
+
[tool.pytest.ini_options]
|
|
35
|
+
pythonpath = ["tests"]
|
|
36
|
+
|
|
37
|
+
[build-system]
|
|
38
|
+
requires = ["hatchling"]
|
|
39
|
+
build-backend = "hatchling.build"
|
|
40
|
+
|
|
41
|
+
[tool.hatch.build.targets.wheel]
|
|
42
|
+
packages = ["src/measly"]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Estimate whether a model is data-limited, and whether more capacity would pay off."""
|
|
2
|
+
|
|
3
|
+
from measly.analysis import (
|
|
4
|
+
DEFAULT_FRACTIONS,
|
|
5
|
+
VALIDATION_WARN,
|
|
6
|
+
Analysis,
|
|
7
|
+
Projection,
|
|
8
|
+
analyse,
|
|
9
|
+
mean_squared_error,
|
|
10
|
+
)
|
|
11
|
+
from measly.fit import (
|
|
12
|
+
POW3,
|
|
13
|
+
POW4,
|
|
14
|
+
CurveEnsemble,
|
|
15
|
+
ScalingLaw,
|
|
16
|
+
fit_scaling_law,
|
|
17
|
+
fit_scaling_laws,
|
|
18
|
+
pow3,
|
|
19
|
+
)
|
|
20
|
+
from measly.interfaces import Model, Score
|
|
21
|
+
from measly.plot import plot
|
|
22
|
+
from measly.sweep import sweep, train_test_split
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"analyse",
|
|
26
|
+
"Analysis",
|
|
27
|
+
"Projection",
|
|
28
|
+
"mean_squared_error",
|
|
29
|
+
"plot",
|
|
30
|
+
"DEFAULT_FRACTIONS",
|
|
31
|
+
"VALIDATION_WARN",
|
|
32
|
+
"Model",
|
|
33
|
+
"Score",
|
|
34
|
+
"train_test_split",
|
|
35
|
+
"sweep",
|
|
36
|
+
"pow3",
|
|
37
|
+
"ScalingLaw",
|
|
38
|
+
"POW3",
|
|
39
|
+
"POW4",
|
|
40
|
+
"CurveEnsemble",
|
|
41
|
+
"fit_scaling_law",
|
|
42
|
+
"fit_scaling_laws",
|
|
43
|
+
]
|