mantissa-embed 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.
- mantissa_embed-0.1.0/LICENSE +21 -0
- mantissa_embed-0.1.0/PKG-INFO +198 -0
- mantissa_embed-0.1.0/README.md +178 -0
- mantissa_embed-0.1.0/mantissa_embed/__init__.py +31 -0
- mantissa_embed-0.1.0/mantissa_embed/losses.py +81 -0
- mantissa_embed-0.1.0/mantissa_embed/model.py +299 -0
- mantissa_embed-0.1.0/mantissa_embed/models.py +38 -0
- mantissa_embed-0.1.0/mantissa_embed.egg-info/PKG-INFO +198 -0
- mantissa_embed-0.1.0/mantissa_embed.egg-info/SOURCES.txt +12 -0
- mantissa_embed-0.1.0/mantissa_embed.egg-info/dependency_links.txt +1 -0
- mantissa_embed-0.1.0/mantissa_embed.egg-info/requires.txt +8 -0
- mantissa_embed-0.1.0/mantissa_embed.egg-info/top_level.txt +1 -0
- mantissa_embed-0.1.0/pyproject.toml +41 -0
- mantissa_embed-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tekin Ertekin
|
|
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.
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mantissa-embed
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CNN metric learning on the mantissa C engine: image embeddings for similarity, verification and retrieval
|
|
5
|
+
Author: Tekin Ertekin
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/tekinertekin/mantissa-embed
|
|
8
|
+
Project-URL: Base, https://github.com/tekinertekin/mantissa-cnn
|
|
9
|
+
Project-URL: Engine, https://github.com/tekinertekin/mantissa
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: numpy>=1.20
|
|
14
|
+
Requires-Dist: mantissa-cnn>=0.2.2
|
|
15
|
+
Provides-Extra: viz
|
|
16
|
+
Requires-Dist: matplotlib>=3.5; extra == "viz"
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# mantissa-embed
|
|
22
|
+
|
|
23
|
+

|
|
24
|
+

|
|
25
|
+
[](https://github.com/tekinertekin/mantissa-cnn)
|
|
26
|
+
[](https://github.com/tekinertekin/mantissa)
|
|
27
|
+
|
|
28
|
+
**Learning what "similar" means, with a C engine.** A classifier answers *which
|
|
29
|
+
class*; an embedder answers *how alike* — it maps each image to a point in a
|
|
30
|
+
vector space where same-thing images land close together and different-thing
|
|
31
|
+
images land far apart. That single geometry powers similarity search,
|
|
32
|
+
verification ("are these two the same?") and retrieval ("show me the nearest
|
|
33
|
+
matches"), for classes the model was never trained to name.
|
|
34
|
+
|
|
35
|
+
`mantissa-embed` trains that embedding with **metric learning** — the
|
|
36
|
+
contrastive and triplet losses — on top of a
|
|
37
|
+
[mantissa-cnn](https://github.com/tekinertekin/mantissa-cnn) trunk: its
|
|
38
|
+
Conv2D / MaxPool2D / Flatten / Dense layers, its
|
|
39
|
+
[mantissa](https://github.com/tekinertekin/mantissa) C-engine and pure-numpy
|
|
40
|
+
backends, and its dataset loaders are reused, not reimplemented. This package
|
|
41
|
+
adds only what metric learning needs on top of a classifier: the two losses,
|
|
42
|
+
an `Embedder` that trains a trunk with them, and the four things embeddings buy
|
|
43
|
+
you — `fit` / `embed` / `retrieve` / `verify`.
|
|
44
|
+
|
|
45
|
+
## The mantissa family
|
|
46
|
+
|
|
47
|
+
Part of the **mantissa** family: a low-precision engine written in C, with
|
|
48
|
+
small Python packages built on top. Each package sits under the one it depends
|
|
49
|
+
on — ⭐ marks where you are, and every other name links to its repo.
|
|
50
|
+
|
|
51
|
+
- [mantissa](https://github.com/tekinertekin/mantissa) — low-precision neural-network engine in C (the core)
|
|
52
|
+
- [mantissa-perceptron](https://github.com/tekinertekin/mantissa-perceptron) — perceptron & ADALINE, the linear classics
|
|
53
|
+
- [mantissa-nn](https://github.com/tekinertekin/mantissa-nn) — shared neural-net primitives (layers, engine binding)
|
|
54
|
+
- [mantissa-cnn](https://github.com/tekinertekin/mantissa-cnn) — convolutional networks for images
|
|
55
|
+
- [mantissa-auto-encoder](https://github.com/tekinertekin/mantissa-auto-encoder) — autoencoders for denoising & super-resolution
|
|
56
|
+
- [mantissa-interpret](https://github.com/tekinertekin/mantissa-interpret) — CNN interpretability (occlusion, saliency, Grad-CAM)
|
|
57
|
+
- ⭐ **mantissa-embed** — CNN metric learning (image embeddings for similarity & retrieval) *(you are here)*
|
|
58
|
+
- [mantissa-mlp](https://github.com/tekinertekin/mantissa-mlp) — multilayer perceptrons, fully-connected nets
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
## New to metric learning?
|
|
62
|
+
|
|
63
|
+
A classifier learns a fixed set of labels and a decision boundary between them.
|
|
64
|
+
Metric learning learns something more basic and more reusable: a **distance**.
|
|
65
|
+
It trains the network so that the Euclidean distance between two images'
|
|
66
|
+
embeddings *is* a measure of how similar they are — and it does that using only
|
|
67
|
+
the relation between examples ("these two are the same kind", "these two are
|
|
68
|
+
not"), never a class *name*. Two consequences follow:
|
|
69
|
+
|
|
70
|
+
- **It generalizes to classes it never trained on.** Because the loss only ever
|
|
71
|
+
says "closer" or "farther", a model trained on some identities produces useful
|
|
72
|
+
distances for identities it has never seen — the basis of face verification,
|
|
73
|
+
where you cannot retrain for every new person.
|
|
74
|
+
- **One embedding serves many tasks.** Compute it once per image, then:
|
|
75
|
+
*verify* by thresholding a distance, *retrieve* by nearest-neighbour search,
|
|
76
|
+
*cluster* by feeding the vectors to any clustering method.
|
|
77
|
+
|
|
78
|
+
Two classic losses do the training, and this package implements both:
|
|
79
|
+
|
|
80
|
+
- **Contrastive loss** works on **pairs**. A same-class pair pays its squared
|
|
81
|
+
distance (pulled together); a different-class pair pays a hinge that is zero
|
|
82
|
+
once the pair is at least a `margin` apart (pushed apart, but only until far
|
|
83
|
+
enough) — Hadsell, Chopra & LeCun (2006), "Dimensionality Reduction by
|
|
84
|
+
Learning an Invariant Mapping", *CVPR*.
|
|
85
|
+
- **Triplet loss** works on **triples** of (anchor, positive, negative). It asks
|
|
86
|
+
only that the negative be farther from the anchor than the positive, by at
|
|
87
|
+
least a `margin` — a *relative* constraint, which is often easier to satisfy
|
|
88
|
+
and to scale than pinning absolute distances — Schroff, Kalenichenko &
|
|
89
|
+
Philbin (2015), "FaceNet: A Unified Embedding for Face Recognition and
|
|
90
|
+
Clustering", *CVPR*.
|
|
91
|
+
|
|
92
|
+
**A Siamese/triplet network is just one network run more than once.** The two
|
|
93
|
+
legs of a pair (or three of a triplet) share the *same* weights, so there is no
|
|
94
|
+
second network to manage: stack the legs into one batch, run a single forward
|
|
95
|
+
pass, compute the loss and its per-row gradient on the resulting embeddings,
|
|
96
|
+
and backpropagate that gradient through the one trunk. That is exactly what
|
|
97
|
+
`Embedder.fit` does — the same custom forward/loss/backward loop the rest of the
|
|
98
|
+
family uses, with a metric loss where the classifier's softmax would be.
|
|
99
|
+
|
|
100
|
+
## Install
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
pip install mantissa-embed
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Pulls in `mantissa-cnn >= 0.2.2` (and transitively `mantissa-nn` + the
|
|
107
|
+
`mantissa-core` engine). For the demo's plots, `pip install mantissa-embed[viz]`.
|
|
108
|
+
|
|
109
|
+
From checkouts (works today, no PyPI needed): clone this repo, `cnn`,
|
|
110
|
+
`mantissa-nn` and [mantissa](https://github.com/tekinertekin/mantissa) side by
|
|
111
|
+
side, build the engine (`make dist` there), then here:
|
|
112
|
+
|
|
113
|
+
```sh
|
|
114
|
+
pip install -e ../mantissa -e ../mantissa-nn -e ../cnn && pip install -e ".[viz]"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
mantissa-cnn finds the sibling engine checkout automatically, and its dataset
|
|
118
|
+
loaders find a `data/` directory via `MANTISSA_CNN_DATA` (the demo points this
|
|
119
|
+
at the sibling `cnn/data/` for you).
|
|
120
|
+
|
|
121
|
+
## Quickstart
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
# datasets are mantissa-cnn's; nothing downloads implicitly — fetch once:
|
|
125
|
+
python -m mantissa_cnn.datasets download mnist
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from mantissa_cnn import datasets
|
|
130
|
+
from mantissa_embed import Embedder, models
|
|
131
|
+
|
|
132
|
+
Xtr, ytr, Xte, yte = datasets.subset("mnist", 6000, 2000, seed=0)
|
|
133
|
+
|
|
134
|
+
emb = Embedder(models.small_cnn_embedder(embed_dim=16), loss="triplet") # or "contrastive"
|
|
135
|
+
emb.fit(Xtr, ytr, epochs=8, batch_size=64, lr=0.05, verbose=True)
|
|
136
|
+
|
|
137
|
+
Z = emb.embed(Xte) # (2000, 16) embedding vectors
|
|
138
|
+
idx, dist = emb.retrieve(Xte[0], Z, k=5) # 5 nearest test images to query 0
|
|
139
|
+
same = emb.verify(Xte[0], Xte[1], threshold=1.0) # same identity? (bool)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Or compose your own trunk from mantissa-cnn's layers — any Conv/Pool/Flatten
|
|
143
|
+
stack ending in `Dense(embed_dim, act="identity")`:
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from mantissa_cnn import Conv2D, MaxPool2D, Flatten, Dense
|
|
147
|
+
from mantissa_embed import Embedder
|
|
148
|
+
|
|
149
|
+
emb = Embedder(
|
|
150
|
+
[Conv2D(16, 3, pad=1), MaxPool2D(2), Flatten(), Dense(32)], # 32-D embedding
|
|
151
|
+
loss="contrastive", margin=1.0, seed=0)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## The API
|
|
155
|
+
|
|
156
|
+
`Embedder(layers, loss="triplet"|"contrastive", margin=None, seed=0, backend="mantissa")`,
|
|
157
|
+
then:
|
|
158
|
+
|
|
159
|
+
| method | does | returns |
|
|
160
|
+
|---|---|---|
|
|
161
|
+
| `fit(X, y, epochs, batch_size, lr, verbose)` | trains the trunk by metric learning; `y` only decides same/different | `self` (`history_["loss"]` per epoch) |
|
|
162
|
+
| `embed(X)` | one forward pass, chunked | `(n, embed_dim)` embeddings |
|
|
163
|
+
| `retrieve(query, gallery, k)` | k nearest gallery items in embedding space (numpy `argpartition`, no sklearn) | `(indices, distances)`, nearest-first |
|
|
164
|
+
| `verify(a, b, threshold=None)` | embedding distance between `a` and `b`; with `threshold`, a boolean same/different | distance, or bool |
|
|
165
|
+
|
|
166
|
+
`retrieve` and `verify` accept raw images **or** a pre-computed `(., embed_dim)`
|
|
167
|
+
matrix, so a fixed gallery is embedded once and reused across queries.
|
|
168
|
+
|
|
169
|
+
Deliberately minimal, like the rest of the family: NCHW float32 images, plain
|
|
170
|
+
SGD, the two classic losses, plain Euclidean distances. No autograd graph, no
|
|
171
|
+
optimizer zoo, no ANN index. The trunk's convolutions run in C on zero-copy
|
|
172
|
+
float32 buffers; the losses are memory-bound reductions over embedding vectors
|
|
173
|
+
and honestly stay in numpy (`mantissa_embed.losses`, importable and gradient-
|
|
174
|
+
checked on their own). Layers allocate scratch once per batch shape and reuse
|
|
175
|
+
it.
|
|
176
|
+
|
|
177
|
+
## Results
|
|
178
|
+
|
|
179
|
+
A `small_cnn_embedder` (52,528 params, 16-D embedding) trained on a 6k-image
|
|
180
|
+
MNIST subset with the triplet loss and the mantissa C engine (8 epochs, triplet
|
|
181
|
+
loss **0.090 → 0.016**), then embedding the held-out 2k test set. Reproduce with
|
|
182
|
+
`python examples/embeddings_demo.py`.
|
|
183
|
+
|
|
184
|
+
The embedding separates the digits with no label ever entering the loss — colour
|
|
185
|
+
is the true digit, shown only for the plot:
|
|
186
|
+
|
|
187
|
+

|
|
188
|
+
|
|
189
|
+
And nearest-neighbour retrieval returns same-digit images (green border = a
|
|
190
|
+
correct match, i.e. the neighbour's true digit equals the query's):
|
|
191
|
+
|
|
192
|
+

|
|
193
|
+
|
|
194
|
+
## License
|
|
195
|
+
|
|
196
|
+
MIT — © Tekin Ertekin. Base package:
|
|
197
|
+
[mantissa-cnn](https://github.com/tekinertekin/mantissa-cnn); engine:
|
|
198
|
+
[mantissa](https://github.com/tekinertekin/mantissa) — same author, MIT.
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# mantissa-embed
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+
[](https://github.com/tekinertekin/mantissa-cnn)
|
|
6
|
+
[](https://github.com/tekinertekin/mantissa)
|
|
7
|
+
|
|
8
|
+
**Learning what "similar" means, with a C engine.** A classifier answers *which
|
|
9
|
+
class*; an embedder answers *how alike* — it maps each image to a point in a
|
|
10
|
+
vector space where same-thing images land close together and different-thing
|
|
11
|
+
images land far apart. That single geometry powers similarity search,
|
|
12
|
+
verification ("are these two the same?") and retrieval ("show me the nearest
|
|
13
|
+
matches"), for classes the model was never trained to name.
|
|
14
|
+
|
|
15
|
+
`mantissa-embed` trains that embedding with **metric learning** — the
|
|
16
|
+
contrastive and triplet losses — on top of a
|
|
17
|
+
[mantissa-cnn](https://github.com/tekinertekin/mantissa-cnn) trunk: its
|
|
18
|
+
Conv2D / MaxPool2D / Flatten / Dense layers, its
|
|
19
|
+
[mantissa](https://github.com/tekinertekin/mantissa) C-engine and pure-numpy
|
|
20
|
+
backends, and its dataset loaders are reused, not reimplemented. This package
|
|
21
|
+
adds only what metric learning needs on top of a classifier: the two losses,
|
|
22
|
+
an `Embedder` that trains a trunk with them, and the four things embeddings buy
|
|
23
|
+
you — `fit` / `embed` / `retrieve` / `verify`.
|
|
24
|
+
|
|
25
|
+
## The mantissa family
|
|
26
|
+
|
|
27
|
+
Part of the **mantissa** family: a low-precision engine written in C, with
|
|
28
|
+
small Python packages built on top. Each package sits under the one it depends
|
|
29
|
+
on — ⭐ marks where you are, and every other name links to its repo.
|
|
30
|
+
|
|
31
|
+
- [mantissa](https://github.com/tekinertekin/mantissa) — low-precision neural-network engine in C (the core)
|
|
32
|
+
- [mantissa-perceptron](https://github.com/tekinertekin/mantissa-perceptron) — perceptron & ADALINE, the linear classics
|
|
33
|
+
- [mantissa-nn](https://github.com/tekinertekin/mantissa-nn) — shared neural-net primitives (layers, engine binding)
|
|
34
|
+
- [mantissa-cnn](https://github.com/tekinertekin/mantissa-cnn) — convolutional networks for images
|
|
35
|
+
- [mantissa-auto-encoder](https://github.com/tekinertekin/mantissa-auto-encoder) — autoencoders for denoising & super-resolution
|
|
36
|
+
- [mantissa-interpret](https://github.com/tekinertekin/mantissa-interpret) — CNN interpretability (occlusion, saliency, Grad-CAM)
|
|
37
|
+
- ⭐ **mantissa-embed** — CNN metric learning (image embeddings for similarity & retrieval) *(you are here)*
|
|
38
|
+
- [mantissa-mlp](https://github.com/tekinertekin/mantissa-mlp) — multilayer perceptrons, fully-connected nets
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
## New to metric learning?
|
|
42
|
+
|
|
43
|
+
A classifier learns a fixed set of labels and a decision boundary between them.
|
|
44
|
+
Metric learning learns something more basic and more reusable: a **distance**.
|
|
45
|
+
It trains the network so that the Euclidean distance between two images'
|
|
46
|
+
embeddings *is* a measure of how similar they are — and it does that using only
|
|
47
|
+
the relation between examples ("these two are the same kind", "these two are
|
|
48
|
+
not"), never a class *name*. Two consequences follow:
|
|
49
|
+
|
|
50
|
+
- **It generalizes to classes it never trained on.** Because the loss only ever
|
|
51
|
+
says "closer" or "farther", a model trained on some identities produces useful
|
|
52
|
+
distances for identities it has never seen — the basis of face verification,
|
|
53
|
+
where you cannot retrain for every new person.
|
|
54
|
+
- **One embedding serves many tasks.** Compute it once per image, then:
|
|
55
|
+
*verify* by thresholding a distance, *retrieve* by nearest-neighbour search,
|
|
56
|
+
*cluster* by feeding the vectors to any clustering method.
|
|
57
|
+
|
|
58
|
+
Two classic losses do the training, and this package implements both:
|
|
59
|
+
|
|
60
|
+
- **Contrastive loss** works on **pairs**. A same-class pair pays its squared
|
|
61
|
+
distance (pulled together); a different-class pair pays a hinge that is zero
|
|
62
|
+
once the pair is at least a `margin` apart (pushed apart, but only until far
|
|
63
|
+
enough) — Hadsell, Chopra & LeCun (2006), "Dimensionality Reduction by
|
|
64
|
+
Learning an Invariant Mapping", *CVPR*.
|
|
65
|
+
- **Triplet loss** works on **triples** of (anchor, positive, negative). It asks
|
|
66
|
+
only that the negative be farther from the anchor than the positive, by at
|
|
67
|
+
least a `margin` — a *relative* constraint, which is often easier to satisfy
|
|
68
|
+
and to scale than pinning absolute distances — Schroff, Kalenichenko &
|
|
69
|
+
Philbin (2015), "FaceNet: A Unified Embedding for Face Recognition and
|
|
70
|
+
Clustering", *CVPR*.
|
|
71
|
+
|
|
72
|
+
**A Siamese/triplet network is just one network run more than once.** The two
|
|
73
|
+
legs of a pair (or three of a triplet) share the *same* weights, so there is no
|
|
74
|
+
second network to manage: stack the legs into one batch, run a single forward
|
|
75
|
+
pass, compute the loss and its per-row gradient on the resulting embeddings,
|
|
76
|
+
and backpropagate that gradient through the one trunk. That is exactly what
|
|
77
|
+
`Embedder.fit` does — the same custom forward/loss/backward loop the rest of the
|
|
78
|
+
family uses, with a metric loss where the classifier's softmax would be.
|
|
79
|
+
|
|
80
|
+
## Install
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
pip install mantissa-embed
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Pulls in `mantissa-cnn >= 0.2.2` (and transitively `mantissa-nn` + the
|
|
87
|
+
`mantissa-core` engine). For the demo's plots, `pip install mantissa-embed[viz]`.
|
|
88
|
+
|
|
89
|
+
From checkouts (works today, no PyPI needed): clone this repo, `cnn`,
|
|
90
|
+
`mantissa-nn` and [mantissa](https://github.com/tekinertekin/mantissa) side by
|
|
91
|
+
side, build the engine (`make dist` there), then here:
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
pip install -e ../mantissa -e ../mantissa-nn -e ../cnn && pip install -e ".[viz]"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
mantissa-cnn finds the sibling engine checkout automatically, and its dataset
|
|
98
|
+
loaders find a `data/` directory via `MANTISSA_CNN_DATA` (the demo points this
|
|
99
|
+
at the sibling `cnn/data/` for you).
|
|
100
|
+
|
|
101
|
+
## Quickstart
|
|
102
|
+
|
|
103
|
+
```sh
|
|
104
|
+
# datasets are mantissa-cnn's; nothing downloads implicitly — fetch once:
|
|
105
|
+
python -m mantissa_cnn.datasets download mnist
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from mantissa_cnn import datasets
|
|
110
|
+
from mantissa_embed import Embedder, models
|
|
111
|
+
|
|
112
|
+
Xtr, ytr, Xte, yte = datasets.subset("mnist", 6000, 2000, seed=0)
|
|
113
|
+
|
|
114
|
+
emb = Embedder(models.small_cnn_embedder(embed_dim=16), loss="triplet") # or "contrastive"
|
|
115
|
+
emb.fit(Xtr, ytr, epochs=8, batch_size=64, lr=0.05, verbose=True)
|
|
116
|
+
|
|
117
|
+
Z = emb.embed(Xte) # (2000, 16) embedding vectors
|
|
118
|
+
idx, dist = emb.retrieve(Xte[0], Z, k=5) # 5 nearest test images to query 0
|
|
119
|
+
same = emb.verify(Xte[0], Xte[1], threshold=1.0) # same identity? (bool)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Or compose your own trunk from mantissa-cnn's layers — any Conv/Pool/Flatten
|
|
123
|
+
stack ending in `Dense(embed_dim, act="identity")`:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from mantissa_cnn import Conv2D, MaxPool2D, Flatten, Dense
|
|
127
|
+
from mantissa_embed import Embedder
|
|
128
|
+
|
|
129
|
+
emb = Embedder(
|
|
130
|
+
[Conv2D(16, 3, pad=1), MaxPool2D(2), Flatten(), Dense(32)], # 32-D embedding
|
|
131
|
+
loss="contrastive", margin=1.0, seed=0)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## The API
|
|
135
|
+
|
|
136
|
+
`Embedder(layers, loss="triplet"|"contrastive", margin=None, seed=0, backend="mantissa")`,
|
|
137
|
+
then:
|
|
138
|
+
|
|
139
|
+
| method | does | returns |
|
|
140
|
+
|---|---|---|
|
|
141
|
+
| `fit(X, y, epochs, batch_size, lr, verbose)` | trains the trunk by metric learning; `y` only decides same/different | `self` (`history_["loss"]` per epoch) |
|
|
142
|
+
| `embed(X)` | one forward pass, chunked | `(n, embed_dim)` embeddings |
|
|
143
|
+
| `retrieve(query, gallery, k)` | k nearest gallery items in embedding space (numpy `argpartition`, no sklearn) | `(indices, distances)`, nearest-first |
|
|
144
|
+
| `verify(a, b, threshold=None)` | embedding distance between `a` and `b`; with `threshold`, a boolean same/different | distance, or bool |
|
|
145
|
+
|
|
146
|
+
`retrieve` and `verify` accept raw images **or** a pre-computed `(., embed_dim)`
|
|
147
|
+
matrix, so a fixed gallery is embedded once and reused across queries.
|
|
148
|
+
|
|
149
|
+
Deliberately minimal, like the rest of the family: NCHW float32 images, plain
|
|
150
|
+
SGD, the two classic losses, plain Euclidean distances. No autograd graph, no
|
|
151
|
+
optimizer zoo, no ANN index. The trunk's convolutions run in C on zero-copy
|
|
152
|
+
float32 buffers; the losses are memory-bound reductions over embedding vectors
|
|
153
|
+
and honestly stay in numpy (`mantissa_embed.losses`, importable and gradient-
|
|
154
|
+
checked on their own). Layers allocate scratch once per batch shape and reuse
|
|
155
|
+
it.
|
|
156
|
+
|
|
157
|
+
## Results
|
|
158
|
+
|
|
159
|
+
A `small_cnn_embedder` (52,528 params, 16-D embedding) trained on a 6k-image
|
|
160
|
+
MNIST subset with the triplet loss and the mantissa C engine (8 epochs, triplet
|
|
161
|
+
loss **0.090 → 0.016**), then embedding the held-out 2k test set. Reproduce with
|
|
162
|
+
`python examples/embeddings_demo.py`.
|
|
163
|
+
|
|
164
|
+
The embedding separates the digits with no label ever entering the loss — colour
|
|
165
|
+
is the true digit, shown only for the plot:
|
|
166
|
+
|
|
167
|
+

|
|
168
|
+
|
|
169
|
+
And nearest-neighbour retrieval returns same-digit images (green border = a
|
|
170
|
+
correct match, i.e. the neighbour's true digit equals the query's):
|
|
171
|
+
|
|
172
|
+

|
|
173
|
+
|
|
174
|
+
## License
|
|
175
|
+
|
|
176
|
+
MIT — © Tekin Ertekin. Base package:
|
|
177
|
+
[mantissa-cnn](https://github.com/tekinertekin/mantissa-cnn); engine:
|
|
178
|
+
[mantissa](https://github.com/tekinertekin/mantissa) — same author, MIT.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""mantissa-embed: CNN metric learning on the mantissa C engine, built on
|
|
2
|
+
mantissa-nn and mantissa-cnn.
|
|
3
|
+
|
|
4
|
+
>>> from mantissa_embed import Embedder, models
|
|
5
|
+
>>> from mantissa_cnn import datasets
|
|
6
|
+
>>> Xtr, ytr, Xte, yte = datasets.subset("mnist", 4000, 1000, seed=0)
|
|
7
|
+
>>> emb = Embedder(models.small_cnn_embedder(embed_dim=16), loss="triplet")
|
|
8
|
+
>>> emb.fit(Xtr, ytr, epochs=5, batch_size=64, lr=0.05)
|
|
9
|
+
>>> idx, dist = emb.retrieve(Xte[0], Xte[1:], k=5) # 5 nearest neighbours
|
|
10
|
+
|
|
11
|
+
The shared base (Dense/Flatten layers, both backends) comes from mantissa-nn;
|
|
12
|
+
the convolution and pooling layers and the image datasets come from
|
|
13
|
+
mantissa-cnn. This package adds the metric losses (contrastive, triplet), an
|
|
14
|
+
``Embedder`` that trains a trunk with them, and a small-CNN trunk factory. The
|
|
15
|
+
public API is fit / embed / retrieve / verify — similarity, retrieval and
|
|
16
|
+
verification from learned image embeddings.
|
|
17
|
+
"""
|
|
18
|
+
try:
|
|
19
|
+
import mantissa_cnn # noqa: F401 (Conv2D/MaxPool2D; pulls the mantissa-nn base)
|
|
20
|
+
except ImportError:
|
|
21
|
+
raise ImportError(
|
|
22
|
+
"mantissa-cnn is not installed — run: pip install mantissa-cnn"
|
|
23
|
+
) from None
|
|
24
|
+
|
|
25
|
+
from .losses import contrastive_loss_grad, triplet_loss_grad
|
|
26
|
+
from .model import Embedder
|
|
27
|
+
from . import models
|
|
28
|
+
|
|
29
|
+
__version__ = "0.1.0"
|
|
30
|
+
__all__ = ["Embedder", "contrastive_loss_grad", "triplet_loss_grad",
|
|
31
|
+
"models", "__version__"]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Metric-learning losses, computed in numpy: contrastive and triplet.
|
|
2
|
+
|
|
3
|
+
Both are memory-bound elementwise reductions over a batch of embeddings — one
|
|
4
|
+
subtract, one square-sum per row — so numpy is the right tool; the convolution
|
|
5
|
+
trunk that produced the embeddings still runs in the C engine (see
|
|
6
|
+
:mod:`mantissa_embed.model`). Each returns the scalar loss and the per-row
|
|
7
|
+
gradient(s) of that loss w.r.t. its embedding input(s), already scaled by
|
|
8
|
+
1/batch so one learning rate works regardless of how many pairs/triplets a
|
|
9
|
+
batch forms.
|
|
10
|
+
|
|
11
|
+
Convention: distances are plain Euclidean. Contrastive uses the distance
|
|
12
|
+
itself; triplet uses the squared distance, following each paper.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
__all__ = ["contrastive_loss_grad", "triplet_loss_grad"]
|
|
19
|
+
|
|
20
|
+
_EPS = np.float32(1e-8)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def contrastive_loss_grad(Za, Zb, same, margin: float = 1.0):
|
|
24
|
+
"""Contrastive loss over a batch of pairs (Hadsell, Chopra & LeCun, 2006,
|
|
25
|
+
"Dimensionality Reduction by Learning an Invariant Mapping", CVPR).
|
|
26
|
+
|
|
27
|
+
For each pair with Euclidean distance ``d = ||Za - Zb||``:
|
|
28
|
+
similar pairs (``same == 1``) pay ``d**2``; dissimilar pairs pay
|
|
29
|
+
``max(margin - d, 0)**2`` — pulled together, or pushed apart until at
|
|
30
|
+
least ``margin`` away. Returns ``(loss, dZa, dZb)``; ``dZb == -dZa`` since
|
|
31
|
+
the loss sees only the difference ``Za - Zb``.
|
|
32
|
+
"""
|
|
33
|
+
Za = np.asarray(Za, dtype=np.float32)
|
|
34
|
+
Zb = np.asarray(Zb, dtype=np.float32)
|
|
35
|
+
same = np.asarray(same, dtype=np.float32).reshape(-1, 1)
|
|
36
|
+
n = Za.shape[0]
|
|
37
|
+
|
|
38
|
+
diff = Za - Zb # (n, d)
|
|
39
|
+
d = np.sqrt(np.sum(diff * diff, axis=1, keepdims=True) + _EPS) # (n, 1)
|
|
40
|
+
hinge = np.maximum(margin - d, 0.0) # dissimilar push term
|
|
41
|
+
|
|
42
|
+
per = same * (d * d) + (1.0 - same) * (hinge * hinge)
|
|
43
|
+
loss = float(np.sum(per)) / n
|
|
44
|
+
|
|
45
|
+
# d(d**2)/dZa = 2*diff ; d(hinge**2)/dZa = -2*hinge*diff/d (when hinge>0)
|
|
46
|
+
g = same * 2.0 * diff - (1.0 - same) * 2.0 * hinge * diff / d
|
|
47
|
+
dZa = (g / n).astype(np.float32)
|
|
48
|
+
return loss, dZa, -dZa
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def triplet_loss_grad(Za, Zp, Zn, margin: float = 0.2):
|
|
52
|
+
"""Triplet loss over a batch of (anchor, positive, negative) triples
|
|
53
|
+
(Schroff, Kalenichenko & Philbin, 2015, "FaceNet: A Unified Embedding for
|
|
54
|
+
Face Recognition and Clustering", CVPR).
|
|
55
|
+
|
|
56
|
+
With squared Euclidean distances ``d_ap = ||Za - Zp||**2`` and
|
|
57
|
+
``d_an = ||Za - Zn||**2``, each triple pays
|
|
58
|
+
``max(d_ap - d_an + margin, 0)`` — the anchor is pulled toward its
|
|
59
|
+
positive and pushed off its negative until the negative is at least
|
|
60
|
+
``margin`` farther than the positive. Non-violating triples contribute no
|
|
61
|
+
loss and no gradient. Returns ``(loss, dZa, dZp, dZn)``.
|
|
62
|
+
"""
|
|
63
|
+
Za = np.asarray(Za, dtype=np.float32)
|
|
64
|
+
Zp = np.asarray(Zp, dtype=np.float32)
|
|
65
|
+
Zn = np.asarray(Zn, dtype=np.float32)
|
|
66
|
+
n = Za.shape[0]
|
|
67
|
+
|
|
68
|
+
d_ap = np.sum((Za - Zp) ** 2, axis=1, keepdims=True)
|
|
69
|
+
d_an = np.sum((Za - Zn) ** 2, axis=1, keepdims=True)
|
|
70
|
+
viol = d_ap - d_an + margin
|
|
71
|
+
active = (viol > 0.0).astype(np.float32) # (n, 1)
|
|
72
|
+
loss = float(np.sum(np.maximum(viol, 0.0))) / n
|
|
73
|
+
|
|
74
|
+
# gradients of (d_ap - d_an) on active rows, scaled by 1/n:
|
|
75
|
+
# dZa = 2(Za-Zp) - 2(Za-Zn) = 2(Zn-Zp)
|
|
76
|
+
# dZp = -2(Za-Zp) ; dZn = 2(Za-Zn)
|
|
77
|
+
s = (active * (2.0 / n)).astype(np.float32)
|
|
78
|
+
dZa = s * (Zn - Zp)
|
|
79
|
+
dZp = s * (Zp - Za)
|
|
80
|
+
dZn = s * (Za - Zn)
|
|
81
|
+
return loss, dZa.astype(np.float32), dZp.astype(np.float32), dZn.astype(np.float32)
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""Embedder: a CNN trunk trained by metric learning (contrastive / triplet).
|
|
2
|
+
|
|
3
|
+
Same custom training loop as the rest of the family (mantissa_cnn.Sequential,
|
|
4
|
+
mantissa_autoencoder.Autoencoder), with the classifier/regression head swapped
|
|
5
|
+
for a metric loss over embeddings:
|
|
6
|
+
|
|
7
|
+
- Shuffled mini-batches (seeded ``np.random.default_rng``), plain SGD.
|
|
8
|
+
- Each batch forms pairs (contrastive) or triplets (triplet) from the labels,
|
|
9
|
+
then uses the family's one clean trick: the two/three legs of every
|
|
10
|
+
pair/triplet are STACKED into a single batch, pushed through the trunk in
|
|
11
|
+
ONE forward pass, the loss + per-row gradient are computed in numpy (see
|
|
12
|
+
:mod:`mantissa_embed.losses`), and that gradient is seeded straight into the
|
|
13
|
+
backward chain — one backward, then ``step`` every layer. No second forward,
|
|
14
|
+
no shared-weight bookkeeping: a Siamese/triplet network is just one network
|
|
15
|
+
run on a stacked batch.
|
|
16
|
+
- The trunk's convolutions run in the C engine (or the numpy backend); only
|
|
17
|
+
the metric loss — a memory-bound reduction over embedding vectors — is numpy.
|
|
18
|
+
|
|
19
|
+
Data contract: X is NCHW float32 (the datasets module gives [0, 1]); y is
|
|
20
|
+
integer class ids, used only to decide which samples are "same" and which are
|
|
21
|
+
"different" — the embedding itself is trained without ever seeing a class
|
|
22
|
+
*name*, which is the point of metric learning.
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import numpy as np
|
|
27
|
+
|
|
28
|
+
from mantissa_nn import _numpy_backend
|
|
29
|
+
from mantissa_cnn._engine import cnn_engine # CNN feature gate: the trunk uses Conv2D
|
|
30
|
+
|
|
31
|
+
from .losses import contrastive_loss_grad, triplet_loss_grad
|
|
32
|
+
|
|
33
|
+
__all__ = ["Embedder"]
|
|
34
|
+
|
|
35
|
+
_DEFAULT_MARGIN = {"triplet": 0.2, "contrastive": 1.0}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Embedder:
|
|
39
|
+
"""A convolutional trunk that maps images to embedding vectors, trained so
|
|
40
|
+
that same-class images land close and different-class images land far.
|
|
41
|
+
|
|
42
|
+
Parameters
|
|
43
|
+
----------
|
|
44
|
+
layers : list of Layer
|
|
45
|
+
A Conv2D/MaxPool2D/Flatten/Dense trunk ending in
|
|
46
|
+
``Dense(embed_dim, act="identity")`` — see
|
|
47
|
+
:func:`mantissa_embed.models.small_cnn_embedder`.
|
|
48
|
+
loss : {"triplet", "contrastive"}
|
|
49
|
+
The metric objective. "triplet" (FaceNet) forms (anchor, positive,
|
|
50
|
+
negative) triples; "contrastive" (Hadsell-Chopra-LeCun) forms
|
|
51
|
+
same/different pairs.
|
|
52
|
+
margin : float, optional
|
|
53
|
+
Loss margin. Defaults to 0.2 for triplet, 1.0 for contrastive.
|
|
54
|
+
seed : int
|
|
55
|
+
Seeds one rng stream for weight init, epoch shuffling and pair/triplet
|
|
56
|
+
sampling — two models with the same seed and backend train identically.
|
|
57
|
+
backend : {"mantissa", "numpy"}
|
|
58
|
+
"mantissa" (default) requires the C engine and raises
|
|
59
|
+
ImportError/RuntimeError with the exact fix otherwise.
|
|
60
|
+
|
|
61
|
+
Fitted attributes
|
|
62
|
+
-----------------
|
|
63
|
+
history_ : dict with "loss" (per-epoch mean training loss).
|
|
64
|
+
embed_dim_ : embedding width, i.e. the trunk's output size, after build().
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(self, layers, loss: str = "triplet", margin=None,
|
|
68
|
+
seed: int = 0, backend: str = "mantissa"):
|
|
69
|
+
if loss not in _DEFAULT_MARGIN:
|
|
70
|
+
raise ValueError(f"loss must be 'triplet' or 'contrastive', got {loss!r}")
|
|
71
|
+
if backend == "mantissa":
|
|
72
|
+
tk = cnn_engine() # raises with the exact fix
|
|
73
|
+
# mantissa >= 0.2.2: a per-model Session memoizes each buffer's
|
|
74
|
+
# ctypes pointer by identity. Same signatures; older engines just
|
|
75
|
+
# take the plain methods.
|
|
76
|
+
self._backend = tk.session() if hasattr(tk, "session") else tk
|
|
77
|
+
elif backend == "numpy":
|
|
78
|
+
self._backend = _numpy_backend
|
|
79
|
+
else:
|
|
80
|
+
raise ValueError(f"backend must be 'mantissa' or 'numpy', got {backend!r}")
|
|
81
|
+
self.backend = backend
|
|
82
|
+
self.layers = list(layers)
|
|
83
|
+
self.loss = loss
|
|
84
|
+
self.margin = float(margin) if margin is not None else _DEFAULT_MARGIN[loss]
|
|
85
|
+
self.seed = int(seed)
|
|
86
|
+
self._rng = np.random.default_rng(self.seed)
|
|
87
|
+
self._built = False
|
|
88
|
+
|
|
89
|
+
# -- construction ---------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
def build(self, in_shape):
|
|
92
|
+
"""Initialize parameters for input shape (c, h, w). Called by fit()
|
|
93
|
+
automatically; call it yourself to inspect summary() before training."""
|
|
94
|
+
shape = tuple(int(d) for d in in_shape)
|
|
95
|
+
self.in_shape_ = shape
|
|
96
|
+
for layer in self.layers:
|
|
97
|
+
shape = layer.build(shape, self._rng)
|
|
98
|
+
if len(shape) != 1:
|
|
99
|
+
raise ValueError(f"embedder output must be a flat vector, got shape "
|
|
100
|
+
f"{shape} — end the trunk with Flatten()/Dense(embed_dim)")
|
|
101
|
+
self.embed_dim_ = shape[0]
|
|
102
|
+
self._built = True
|
|
103
|
+
return self
|
|
104
|
+
|
|
105
|
+
def summary(self) -> str:
|
|
106
|
+
"""Per-layer output shapes and parameter counts (build() first)."""
|
|
107
|
+
if not self._built:
|
|
108
|
+
raise RuntimeError("summary() needs parameters — call build(in_shape) or fit() first")
|
|
109
|
+
rows = [(type(l).__name__, str(l.out_shape), l.param_count())
|
|
110
|
+
for l in self.layers]
|
|
111
|
+
total = sum(r[2] for r in rows)
|
|
112
|
+
w = max(len(r[0]) for r in rows)
|
|
113
|
+
lines = [f"{'layer':<{w}} {'out shape':<16} params",
|
|
114
|
+
"-" * (w + 26)]
|
|
115
|
+
lines += [f"{name:<{w}} {shape:<16} {p:,}" for name, shape, p in rows]
|
|
116
|
+
lines.append(f"embedding dim: {self.embed_dim_}")
|
|
117
|
+
lines.append(f"total params: {total:,}")
|
|
118
|
+
return "\n".join(lines)
|
|
119
|
+
|
|
120
|
+
# -- training -------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
def fit(self, X, y, epochs: int = 10, batch_size: int = 32,
|
|
123
|
+
lr: float = 0.01, verbose: bool = False):
|
|
124
|
+
"""Train the embedding by metric learning on labeled images.
|
|
125
|
+
|
|
126
|
+
Labels ``y`` are used only to sample same/different pairs (contrastive)
|
|
127
|
+
or (anchor, positive, negative) triples (triplet) — one pair/triple per
|
|
128
|
+
anchor per batch, sampled from the whole training set.
|
|
129
|
+
"""
|
|
130
|
+
X = self._check(X, name="X")
|
|
131
|
+
y = np.ascontiguousarray(y, dtype=np.int64).ravel()
|
|
132
|
+
n = len(X)
|
|
133
|
+
if len(y) != n:
|
|
134
|
+
raise ValueError(f"X has {n} samples but y has {len(y)}")
|
|
135
|
+
if not self._built:
|
|
136
|
+
self.build(X.shape[1:])
|
|
137
|
+
|
|
138
|
+
classes = np.unique(y)
|
|
139
|
+
if len(classes) < 2:
|
|
140
|
+
raise ValueError("metric learning needs at least 2 classes in y")
|
|
141
|
+
pos_pool = {int(c): np.flatnonzero(y == c) for c in classes}
|
|
142
|
+
neg_pool = {int(c): np.flatnonzero(y != c) for c in classes}
|
|
143
|
+
for c in classes:
|
|
144
|
+
if len(pos_pool[int(c)]) < 2:
|
|
145
|
+
raise ValueError(f"class {int(c)} has <2 samples — cannot form a "
|
|
146
|
+
f"positive pair")
|
|
147
|
+
|
|
148
|
+
backend = self._backend
|
|
149
|
+
layers = self.layers
|
|
150
|
+
bs = min(int(batch_size), n)
|
|
151
|
+
self.history_ = {"loss": []}
|
|
152
|
+
|
|
153
|
+
for epoch in range(int(epochs)):
|
|
154
|
+
order = self._rng.permutation(n)
|
|
155
|
+
loss_sum = 0.0
|
|
156
|
+
count = 0
|
|
157
|
+
for start in range(0, n, bs):
|
|
158
|
+
a_idx = order[start:start + bs]
|
|
159
|
+
ya = y[a_idx]
|
|
160
|
+
pos = self._sample(pos_pool, ya, classes)
|
|
161
|
+
neg = self._sample(neg_pool, ya, classes)
|
|
162
|
+
|
|
163
|
+
if self.loss == "triplet":
|
|
164
|
+
batch = np.concatenate([X[a_idx], X[pos], X[neg]])
|
|
165
|
+
Z = self._forward(batch, layers, backend)
|
|
166
|
+
t = len(a_idx)
|
|
167
|
+
loss, dZa, dZp, dZn = triplet_loss_grad(
|
|
168
|
+
Z[:t], Z[t:2 * t], Z[2 * t:], self.margin)
|
|
169
|
+
dZ = np.concatenate([dZa, dZp, dZn])
|
|
170
|
+
else: # contrastive
|
|
171
|
+
same = (self._rng.random(len(a_idx)) < 0.5)
|
|
172
|
+
b_idx = np.where(same, pos, neg)
|
|
173
|
+
batch = np.concatenate([X[a_idx], X[b_idx]])
|
|
174
|
+
Z = self._forward(batch, layers, backend)
|
|
175
|
+
p = len(a_idx)
|
|
176
|
+
loss, dZa, dZb = contrastive_loss_grad(
|
|
177
|
+
Z[:p], Z[p:], same, self.margin)
|
|
178
|
+
dZ = np.concatenate([dZa, dZb])
|
|
179
|
+
|
|
180
|
+
grad = np.ascontiguousarray(dZ, dtype=np.float32)
|
|
181
|
+
for i in range(len(layers) - 1, -1, -1):
|
|
182
|
+
grad = layers[i].backward(grad, backend, need_dx=i > 0)
|
|
183
|
+
for layer in layers: # after ALL grads: dX of layer i
|
|
184
|
+
layer.step(backend, lr) # depends on its pre-step params
|
|
185
|
+
|
|
186
|
+
loss_sum += loss * len(a_idx)
|
|
187
|
+
count += len(a_idx)
|
|
188
|
+
|
|
189
|
+
self.history_["loss"].append(loss_sum / count)
|
|
190
|
+
if verbose:
|
|
191
|
+
print(f"epoch {epoch + 1}/{epochs} "
|
|
192
|
+
f"loss {self.history_['loss'][-1]:.6f}")
|
|
193
|
+
return self
|
|
194
|
+
|
|
195
|
+
def _sample(self, pool, ya, classes):
|
|
196
|
+
"""One index per anchor drawn (with replacement) from ``pool[label]``
|
|
197
|
+
— the same-class pool for positives, the complement pool for
|
|
198
|
+
negatives. Vectorized per class (there are only a handful)."""
|
|
199
|
+
out = np.empty(len(ya), dtype=np.int64)
|
|
200
|
+
for c in classes:
|
|
201
|
+
mask = ya == c
|
|
202
|
+
m = int(mask.sum())
|
|
203
|
+
if m:
|
|
204
|
+
out[mask] = self._rng.choice(pool[int(c)], size=m)
|
|
205
|
+
return out
|
|
206
|
+
|
|
207
|
+
# -- inference ------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
def embed(self, X):
|
|
210
|
+
"""Embedding vectors for images X, shape (n, embed_dim_)."""
|
|
211
|
+
self._require_built()
|
|
212
|
+
X = self._check(X, expect=self.in_shape_, name="X")
|
|
213
|
+
return self._embed_batched(X)
|
|
214
|
+
|
|
215
|
+
def retrieve(self, query, gallery, k: int = 5):
|
|
216
|
+
"""k nearest gallery items to each query in embedding space.
|
|
217
|
+
|
|
218
|
+
``query`` is one image ``(c, h, w)`` or a batch ``(nq, c, h, w)``;
|
|
219
|
+
``gallery`` is a batch of images ``(m, c, h, w)``. Either may instead
|
|
220
|
+
be a pre-computed embedding matrix ``(., embed_dim_)`` — handy when the
|
|
221
|
+
gallery is fixed across many queries. Returns ``(indices, distances)``,
|
|
222
|
+
each ``(k,)`` for a single query or ``(nq, k)`` for a batch, sorted
|
|
223
|
+
nearest-first by Euclidean distance.
|
|
224
|
+
"""
|
|
225
|
+
self._require_built()
|
|
226
|
+
single = (np.ndim(query) == len(self.in_shape_)) # one image, no batch axis
|
|
227
|
+
Q = self._as_embeddings(query if not single else np.asarray(query)[None])
|
|
228
|
+
G = self._as_embeddings(gallery)
|
|
229
|
+
k = min(int(k), G.shape[0])
|
|
230
|
+
|
|
231
|
+
# ||q - g||^2 = |q|^2 + |g|^2 - 2 q.g ; argpartition for the k smallest.
|
|
232
|
+
d2 = (np.sum(Q * Q, axis=1, keepdims=True)
|
|
233
|
+
+ np.sum(G * G, axis=1)[None, :]
|
|
234
|
+
- 2.0 * Q @ G.T) # (nq, m)
|
|
235
|
+
np.maximum(d2, 0.0, out=d2)
|
|
236
|
+
part = np.argpartition(d2, k - 1, axis=1)[:, :k]
|
|
237
|
+
rows = np.arange(Q.shape[0])[:, None]
|
|
238
|
+
order = np.argsort(d2[rows, part], axis=1)
|
|
239
|
+
idx = part[rows, order]
|
|
240
|
+
dist = np.sqrt(d2[rows, idx])
|
|
241
|
+
if single:
|
|
242
|
+
return idx[0], dist[0]
|
|
243
|
+
return idx, dist
|
|
244
|
+
|
|
245
|
+
def verify(self, a, b, threshold=None):
|
|
246
|
+
"""Euclidean distance between embeddings of ``a`` and ``b`` (lower =
|
|
247
|
+
more likely the same identity). ``a``/``b`` are single images or equal-
|
|
248
|
+
length batches. With ``threshold`` given, returns a boolean "same"
|
|
249
|
+
decision (``distance <= threshold``) instead of the distance.
|
|
250
|
+
"""
|
|
251
|
+
self._require_built()
|
|
252
|
+
Za = self._as_embeddings(a)
|
|
253
|
+
Zb = self._as_embeddings(b)
|
|
254
|
+
dist = np.sqrt(np.maximum(np.sum((Za - Zb) ** 2, axis=1), 0.0))
|
|
255
|
+
out = dist if threshold is None else (dist <= float(threshold))
|
|
256
|
+
return out[0] if out.shape[0] == 1 else out
|
|
257
|
+
|
|
258
|
+
# -- internals ------------------------------------------------------------
|
|
259
|
+
|
|
260
|
+
def _as_embeddings(self, A):
|
|
261
|
+
"""Coerce images (2, 3 or 4-D) or a ready (., embed_dim_) matrix to a
|
|
262
|
+
2-D embedding batch."""
|
|
263
|
+
A = np.asarray(A, dtype=np.float32)
|
|
264
|
+
if A.ndim == 2 and A.shape[1] == self.embed_dim_:
|
|
265
|
+
return A # already embeddings
|
|
266
|
+
if A.ndim == len(self.in_shape_):
|
|
267
|
+
A = A[None] # single image -> batch
|
|
268
|
+
return self._embed_batched(self._check(A, expect=self.in_shape_))
|
|
269
|
+
|
|
270
|
+
def _embed_batched(self, X, chunk: int = 256):
|
|
271
|
+
out = np.empty((len(X), self.embed_dim_), dtype=np.float32)
|
|
272
|
+
for s in range(0, len(X), chunk):
|
|
273
|
+
h = X[s:s + chunk] # contiguous slice view
|
|
274
|
+
for layer in self.layers:
|
|
275
|
+
h = layer.forward(h, self._backend)
|
|
276
|
+
out[s:s + chunk] = h # copy out: scratch is reused
|
|
277
|
+
return out
|
|
278
|
+
|
|
279
|
+
@staticmethod
|
|
280
|
+
def _forward(batch, layers, backend):
|
|
281
|
+
out = np.ascontiguousarray(batch, dtype=np.float32)
|
|
282
|
+
for layer in layers:
|
|
283
|
+
out = layer.forward(out, backend)
|
|
284
|
+
return out
|
|
285
|
+
|
|
286
|
+
def _require_built(self):
|
|
287
|
+
if not self._built:
|
|
288
|
+
raise RuntimeError("model has no parameters — call build(in_shape) or fit() first")
|
|
289
|
+
|
|
290
|
+
def _check(self, X, expect=None, name="X"):
|
|
291
|
+
X = np.ascontiguousarray(X, dtype=np.float32)
|
|
292
|
+
want_ndim = 1 + (len(expect) if expect is not None else 3)
|
|
293
|
+
if X.ndim != want_ndim:
|
|
294
|
+
raise ValueError(f"{name} must have shape (n,) + {expect or '(c, h, w)'} "
|
|
295
|
+
f"float32, got ndim={X.ndim}")
|
|
296
|
+
if expect is not None and tuple(X.shape[1:]) != tuple(expect):
|
|
297
|
+
raise ValueError(f"{name} has sample shape {tuple(X.shape[1:])}, "
|
|
298
|
+
f"expected {tuple(expect)}")
|
|
299
|
+
return X
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Trunk factory: a small convolutional embedding network.
|
|
2
|
+
|
|
3
|
+
Returns a *layer list* (not a built model) — hand it to
|
|
4
|
+
:class:`mantissa_embed.Embedder`, which owns the backend and seeds weight init
|
|
5
|
+
at build time. Built the same way as :func:`mantissa_cnn.models.lenet5`: a
|
|
6
|
+
Conv/Pool/Flatten/Dense stack, but the head is ``Dense(embed_dim,
|
|
7
|
+
act="identity")`` — it emits an embedding vector, not class logits. The metric
|
|
8
|
+
loss lives on top of that vector (see :mod:`mantissa_embed.losses`).
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from mantissa_cnn import Conv2D, Dense, Flatten, MaxPool2D
|
|
13
|
+
|
|
14
|
+
__all__ = ["small_cnn_embedder"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def small_cnn_embedder(in_shape=(1, 28, 28), embed_dim: int = 16, seed: int = 0):
|
|
18
|
+
"""A LeNet-shaped trunk mapping an image to an ``embed_dim`` vector.
|
|
19
|
+
|
|
20
|
+
Conv 8@3x3 -> pool -> Conv 16@3x3 -> pool -> Flatten -> Dense 64 ->
|
|
21
|
+
Dense(embed_dim). relu convolutions (He init), identity embedding head
|
|
22
|
+
(Glorot init) — the metric loss, not a softmax, sits on the head.
|
|
23
|
+
|
|
24
|
+
``in_shape`` is accepted for symmetry with the family's model factories and
|
|
25
|
+
to document the intended input; the layers infer their shapes at build
|
|
26
|
+
time. ``seed`` is likewise reserved for signature symmetry — weight init is
|
|
27
|
+
seeded by the ``Embedder`` when it builds this list, not here (an unbuilt
|
|
28
|
+
layer carries no randomness).
|
|
29
|
+
"""
|
|
30
|
+
return [
|
|
31
|
+
Conv2D(8, 3, pad=1, act="relu"),
|
|
32
|
+
MaxPool2D(2),
|
|
33
|
+
Conv2D(16, 3, pad=1, act="relu"),
|
|
34
|
+
MaxPool2D(2),
|
|
35
|
+
Flatten(),
|
|
36
|
+
Dense(64, act="relu"),
|
|
37
|
+
Dense(int(embed_dim), act="identity"),
|
|
38
|
+
]
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mantissa-embed
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CNN metric learning on the mantissa C engine: image embeddings for similarity, verification and retrieval
|
|
5
|
+
Author: Tekin Ertekin
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/tekinertekin/mantissa-embed
|
|
8
|
+
Project-URL: Base, https://github.com/tekinertekin/mantissa-cnn
|
|
9
|
+
Project-URL: Engine, https://github.com/tekinertekin/mantissa
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: numpy>=1.20
|
|
14
|
+
Requires-Dist: mantissa-cnn>=0.2.2
|
|
15
|
+
Provides-Extra: viz
|
|
16
|
+
Requires-Dist: matplotlib>=3.5; extra == "viz"
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# mantissa-embed
|
|
22
|
+
|
|
23
|
+

|
|
24
|
+

|
|
25
|
+
[](https://github.com/tekinertekin/mantissa-cnn)
|
|
26
|
+
[](https://github.com/tekinertekin/mantissa)
|
|
27
|
+
|
|
28
|
+
**Learning what "similar" means, with a C engine.** A classifier answers *which
|
|
29
|
+
class*; an embedder answers *how alike* — it maps each image to a point in a
|
|
30
|
+
vector space where same-thing images land close together and different-thing
|
|
31
|
+
images land far apart. That single geometry powers similarity search,
|
|
32
|
+
verification ("are these two the same?") and retrieval ("show me the nearest
|
|
33
|
+
matches"), for classes the model was never trained to name.
|
|
34
|
+
|
|
35
|
+
`mantissa-embed` trains that embedding with **metric learning** — the
|
|
36
|
+
contrastive and triplet losses — on top of a
|
|
37
|
+
[mantissa-cnn](https://github.com/tekinertekin/mantissa-cnn) trunk: its
|
|
38
|
+
Conv2D / MaxPool2D / Flatten / Dense layers, its
|
|
39
|
+
[mantissa](https://github.com/tekinertekin/mantissa) C-engine and pure-numpy
|
|
40
|
+
backends, and its dataset loaders are reused, not reimplemented. This package
|
|
41
|
+
adds only what metric learning needs on top of a classifier: the two losses,
|
|
42
|
+
an `Embedder` that trains a trunk with them, and the four things embeddings buy
|
|
43
|
+
you — `fit` / `embed` / `retrieve` / `verify`.
|
|
44
|
+
|
|
45
|
+
## The mantissa family
|
|
46
|
+
|
|
47
|
+
Part of the **mantissa** family: a low-precision engine written in C, with
|
|
48
|
+
small Python packages built on top. Each package sits under the one it depends
|
|
49
|
+
on — ⭐ marks where you are, and every other name links to its repo.
|
|
50
|
+
|
|
51
|
+
- [mantissa](https://github.com/tekinertekin/mantissa) — low-precision neural-network engine in C (the core)
|
|
52
|
+
- [mantissa-perceptron](https://github.com/tekinertekin/mantissa-perceptron) — perceptron & ADALINE, the linear classics
|
|
53
|
+
- [mantissa-nn](https://github.com/tekinertekin/mantissa-nn) — shared neural-net primitives (layers, engine binding)
|
|
54
|
+
- [mantissa-cnn](https://github.com/tekinertekin/mantissa-cnn) — convolutional networks for images
|
|
55
|
+
- [mantissa-auto-encoder](https://github.com/tekinertekin/mantissa-auto-encoder) — autoencoders for denoising & super-resolution
|
|
56
|
+
- [mantissa-interpret](https://github.com/tekinertekin/mantissa-interpret) — CNN interpretability (occlusion, saliency, Grad-CAM)
|
|
57
|
+
- ⭐ **mantissa-embed** — CNN metric learning (image embeddings for similarity & retrieval) *(you are here)*
|
|
58
|
+
- [mantissa-mlp](https://github.com/tekinertekin/mantissa-mlp) — multilayer perceptrons, fully-connected nets
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
## New to metric learning?
|
|
62
|
+
|
|
63
|
+
A classifier learns a fixed set of labels and a decision boundary between them.
|
|
64
|
+
Metric learning learns something more basic and more reusable: a **distance**.
|
|
65
|
+
It trains the network so that the Euclidean distance between two images'
|
|
66
|
+
embeddings *is* a measure of how similar they are — and it does that using only
|
|
67
|
+
the relation between examples ("these two are the same kind", "these two are
|
|
68
|
+
not"), never a class *name*. Two consequences follow:
|
|
69
|
+
|
|
70
|
+
- **It generalizes to classes it never trained on.** Because the loss only ever
|
|
71
|
+
says "closer" or "farther", a model trained on some identities produces useful
|
|
72
|
+
distances for identities it has never seen — the basis of face verification,
|
|
73
|
+
where you cannot retrain for every new person.
|
|
74
|
+
- **One embedding serves many tasks.** Compute it once per image, then:
|
|
75
|
+
*verify* by thresholding a distance, *retrieve* by nearest-neighbour search,
|
|
76
|
+
*cluster* by feeding the vectors to any clustering method.
|
|
77
|
+
|
|
78
|
+
Two classic losses do the training, and this package implements both:
|
|
79
|
+
|
|
80
|
+
- **Contrastive loss** works on **pairs**. A same-class pair pays its squared
|
|
81
|
+
distance (pulled together); a different-class pair pays a hinge that is zero
|
|
82
|
+
once the pair is at least a `margin` apart (pushed apart, but only until far
|
|
83
|
+
enough) — Hadsell, Chopra & LeCun (2006), "Dimensionality Reduction by
|
|
84
|
+
Learning an Invariant Mapping", *CVPR*.
|
|
85
|
+
- **Triplet loss** works on **triples** of (anchor, positive, negative). It asks
|
|
86
|
+
only that the negative be farther from the anchor than the positive, by at
|
|
87
|
+
least a `margin` — a *relative* constraint, which is often easier to satisfy
|
|
88
|
+
and to scale than pinning absolute distances — Schroff, Kalenichenko &
|
|
89
|
+
Philbin (2015), "FaceNet: A Unified Embedding for Face Recognition and
|
|
90
|
+
Clustering", *CVPR*.
|
|
91
|
+
|
|
92
|
+
**A Siamese/triplet network is just one network run more than once.** The two
|
|
93
|
+
legs of a pair (or three of a triplet) share the *same* weights, so there is no
|
|
94
|
+
second network to manage: stack the legs into one batch, run a single forward
|
|
95
|
+
pass, compute the loss and its per-row gradient on the resulting embeddings,
|
|
96
|
+
and backpropagate that gradient through the one trunk. That is exactly what
|
|
97
|
+
`Embedder.fit` does — the same custom forward/loss/backward loop the rest of the
|
|
98
|
+
family uses, with a metric loss where the classifier's softmax would be.
|
|
99
|
+
|
|
100
|
+
## Install
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
pip install mantissa-embed
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Pulls in `mantissa-cnn >= 0.2.2` (and transitively `mantissa-nn` + the
|
|
107
|
+
`mantissa-core` engine). For the demo's plots, `pip install mantissa-embed[viz]`.
|
|
108
|
+
|
|
109
|
+
From checkouts (works today, no PyPI needed): clone this repo, `cnn`,
|
|
110
|
+
`mantissa-nn` and [mantissa](https://github.com/tekinertekin/mantissa) side by
|
|
111
|
+
side, build the engine (`make dist` there), then here:
|
|
112
|
+
|
|
113
|
+
```sh
|
|
114
|
+
pip install -e ../mantissa -e ../mantissa-nn -e ../cnn && pip install -e ".[viz]"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
mantissa-cnn finds the sibling engine checkout automatically, and its dataset
|
|
118
|
+
loaders find a `data/` directory via `MANTISSA_CNN_DATA` (the demo points this
|
|
119
|
+
at the sibling `cnn/data/` for you).
|
|
120
|
+
|
|
121
|
+
## Quickstart
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
# datasets are mantissa-cnn's; nothing downloads implicitly — fetch once:
|
|
125
|
+
python -m mantissa_cnn.datasets download mnist
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from mantissa_cnn import datasets
|
|
130
|
+
from mantissa_embed import Embedder, models
|
|
131
|
+
|
|
132
|
+
Xtr, ytr, Xte, yte = datasets.subset("mnist", 6000, 2000, seed=0)
|
|
133
|
+
|
|
134
|
+
emb = Embedder(models.small_cnn_embedder(embed_dim=16), loss="triplet") # or "contrastive"
|
|
135
|
+
emb.fit(Xtr, ytr, epochs=8, batch_size=64, lr=0.05, verbose=True)
|
|
136
|
+
|
|
137
|
+
Z = emb.embed(Xte) # (2000, 16) embedding vectors
|
|
138
|
+
idx, dist = emb.retrieve(Xte[0], Z, k=5) # 5 nearest test images to query 0
|
|
139
|
+
same = emb.verify(Xte[0], Xte[1], threshold=1.0) # same identity? (bool)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Or compose your own trunk from mantissa-cnn's layers — any Conv/Pool/Flatten
|
|
143
|
+
stack ending in `Dense(embed_dim, act="identity")`:
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from mantissa_cnn import Conv2D, MaxPool2D, Flatten, Dense
|
|
147
|
+
from mantissa_embed import Embedder
|
|
148
|
+
|
|
149
|
+
emb = Embedder(
|
|
150
|
+
[Conv2D(16, 3, pad=1), MaxPool2D(2), Flatten(), Dense(32)], # 32-D embedding
|
|
151
|
+
loss="contrastive", margin=1.0, seed=0)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## The API
|
|
155
|
+
|
|
156
|
+
`Embedder(layers, loss="triplet"|"contrastive", margin=None, seed=0, backend="mantissa")`,
|
|
157
|
+
then:
|
|
158
|
+
|
|
159
|
+
| method | does | returns |
|
|
160
|
+
|---|---|---|
|
|
161
|
+
| `fit(X, y, epochs, batch_size, lr, verbose)` | trains the trunk by metric learning; `y` only decides same/different | `self` (`history_["loss"]` per epoch) |
|
|
162
|
+
| `embed(X)` | one forward pass, chunked | `(n, embed_dim)` embeddings |
|
|
163
|
+
| `retrieve(query, gallery, k)` | k nearest gallery items in embedding space (numpy `argpartition`, no sklearn) | `(indices, distances)`, nearest-first |
|
|
164
|
+
| `verify(a, b, threshold=None)` | embedding distance between `a` and `b`; with `threshold`, a boolean same/different | distance, or bool |
|
|
165
|
+
|
|
166
|
+
`retrieve` and `verify` accept raw images **or** a pre-computed `(., embed_dim)`
|
|
167
|
+
matrix, so a fixed gallery is embedded once and reused across queries.
|
|
168
|
+
|
|
169
|
+
Deliberately minimal, like the rest of the family: NCHW float32 images, plain
|
|
170
|
+
SGD, the two classic losses, plain Euclidean distances. No autograd graph, no
|
|
171
|
+
optimizer zoo, no ANN index. The trunk's convolutions run in C on zero-copy
|
|
172
|
+
float32 buffers; the losses are memory-bound reductions over embedding vectors
|
|
173
|
+
and honestly stay in numpy (`mantissa_embed.losses`, importable and gradient-
|
|
174
|
+
checked on their own). Layers allocate scratch once per batch shape and reuse
|
|
175
|
+
it.
|
|
176
|
+
|
|
177
|
+
## Results
|
|
178
|
+
|
|
179
|
+
A `small_cnn_embedder` (52,528 params, 16-D embedding) trained on a 6k-image
|
|
180
|
+
MNIST subset with the triplet loss and the mantissa C engine (8 epochs, triplet
|
|
181
|
+
loss **0.090 → 0.016**), then embedding the held-out 2k test set. Reproduce with
|
|
182
|
+
`python examples/embeddings_demo.py`.
|
|
183
|
+
|
|
184
|
+
The embedding separates the digits with no label ever entering the loss — colour
|
|
185
|
+
is the true digit, shown only for the plot:
|
|
186
|
+
|
|
187
|
+

|
|
188
|
+
|
|
189
|
+
And nearest-neighbour retrieval returns same-digit images (green border = a
|
|
190
|
+
correct match, i.e. the neighbour's true digit equals the query's):
|
|
191
|
+
|
|
192
|
+

|
|
193
|
+
|
|
194
|
+
## License
|
|
195
|
+
|
|
196
|
+
MIT — © Tekin Ertekin. Base package:
|
|
197
|
+
[mantissa-cnn](https://github.com/tekinertekin/mantissa-cnn); engine:
|
|
198
|
+
[mantissa](https://github.com/tekinertekin/mantissa) — same author, MIT.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
mantissa_embed/__init__.py
|
|
5
|
+
mantissa_embed/losses.py
|
|
6
|
+
mantissa_embed/model.py
|
|
7
|
+
mantissa_embed/models.py
|
|
8
|
+
mantissa_embed.egg-info/PKG-INFO
|
|
9
|
+
mantissa_embed.egg-info/SOURCES.txt
|
|
10
|
+
mantissa_embed.egg-info/dependency_links.txt
|
|
11
|
+
mantissa_embed.egg-info/requires.txt
|
|
12
|
+
mantissa_embed.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mantissa_embed
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mantissa-embed"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "CNN metric learning on the mantissa C engine: image embeddings for similarity, verification and retrieval"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Tekin Ertekin" }]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"numpy>=1.20",
|
|
15
|
+
# Trains a Conv2D/MaxPool2D/Flatten/Dense trunk with the same custom
|
|
16
|
+
# forward/backward loop the rest of the family uses, so it runs on the
|
|
17
|
+
# mantissa C engine (or the pure-numpy backend). mantissa-cnn transitively
|
|
18
|
+
# pulls in mantissa-nn and the mantissa-core engine. For the dev layout,
|
|
19
|
+
# `pip install -e ../cnn ../mantissa-nn ../mantissa` first.
|
|
20
|
+
"mantissa-cnn>=0.2.2",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
viz = ["matplotlib>=3.5"]
|
|
25
|
+
dev = ["pytest>=7"]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/tekinertekin/mantissa-embed"
|
|
29
|
+
Base = "https://github.com/tekinertekin/mantissa-cnn"
|
|
30
|
+
Engine = "https://github.com/tekinertekin/mantissa"
|
|
31
|
+
|
|
32
|
+
[tool.setuptools]
|
|
33
|
+
packages = ["mantissa_embed"]
|
|
34
|
+
|
|
35
|
+
[tool.pytest.ini_options]
|
|
36
|
+
filterwarnings = [
|
|
37
|
+
# numpy 2.x on Apple Accelerate emits spurious FPE RuntimeWarnings from the
|
|
38
|
+
# BLAS matmul kernel even on finite inputs (same issue documented across
|
|
39
|
+
# the family: mantissa-cnn / mantissa-autoencoder pyproject).
|
|
40
|
+
"ignore:.*encountered in matmul:RuntimeWarning",
|
|
41
|
+
]
|