tripletlib 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.
- tripletlib-0.1.0/LICENSE +21 -0
- tripletlib-0.1.0/PKG-INFO +82 -0
- tripletlib-0.1.0/README.md +64 -0
- tripletlib-0.1.0/pyproject.toml +24 -0
- tripletlib-0.1.0/setup.cfg +4 -0
- tripletlib-0.1.0/tripletlib/__init__.py +2 -0
- tripletlib-0.1.0/tripletlib/evaluation_utils.py +77 -0
- tripletlib-0.1.0/tripletlib/triplet_utils.py +193 -0
- tripletlib-0.1.0/tripletlib.egg-info/PKG-INFO +82 -0
- tripletlib-0.1.0/tripletlib.egg-info/SOURCES.txt +11 -0
- tripletlib-0.1.0/tripletlib.egg-info/dependency_links.txt +1 -0
- tripletlib-0.1.0/tripletlib.egg-info/requires.txt +8 -0
- tripletlib-0.1.0/tripletlib.egg-info/top_level.txt +1 -0
tripletlib-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 templateprotection
|
|
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,82 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tripletlib
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A lightweight library for training and evaluating embedding models with triplet loss
|
|
5
|
+
Author: Ted Kane
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: torch
|
|
11
|
+
Requires-Dist: numpy
|
|
12
|
+
Requires-Dist: scikit-learn
|
|
13
|
+
Requires-Dist: scipy
|
|
14
|
+
Provides-Extra: examples
|
|
15
|
+
Requires-Dist: matplotlib; extra == "examples"
|
|
16
|
+
Requires-Dist: pandas; extra == "examples"
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# TripletLib
|
|
20
|
+
|
|
21
|
+
TripletLib is a lightweight Python library for training and evaluating embedding models using **triplet loss**. It is designed to help you build models that learn meaningful embeddings for images, time series, or any vector data, enabling tasks like verification, retrieval, or classification based on similarity.
|
|
22
|
+
|
|
23
|
+
## What is Triplet Loss?
|
|
24
|
+
|
|
25
|
+
A "triplet" is a set of 3 samples of data `(anchor, positive, negative)`. The `anchor` is a data sample from a specific class, the `positive` is another sample from the *same* class, and the `negative` is a sample from an entirely different class.
|
|
26
|
+
Triplet loss is a type of metric learning objective that trains an embedding model to map inputs into a vector space such that:
|
|
27
|
+
|
|
28
|
+
- Samples from the **same class** (e.g., anchor and positive) are close together.
|
|
29
|
+
- Samples from **different classes** (e.g., anchor and negative) are far apart.
|
|
30
|
+
|
|
31
|
+
This is particularly useful for tasks like biometric identification (e.g., face recognition), where traditional classification cannot generalize to unseen classes (new faces).
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
- Base `EmbeddingModel` class with a built-in training loop.
|
|
36
|
+
- `TripletDataset` wrapper to automatically generate triplets from any dataset.
|
|
37
|
+
- Utilities for evaluating embeddings with **EER** and **AUC** metrics.
|
|
38
|
+
- Compatible with PyTorch and supports GPU acceleration.
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
You can install TripletLib via pip:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install tripletlib
|
|
46
|
+
```
|
|
47
|
+
## Usage
|
|
48
|
+
```python
|
|
49
|
+
import torch
|
|
50
|
+
from tripletlib.triplet_utils import EmbeddingModel, TripletDataset
|
|
51
|
+
from torch.utils.data import DataLoader
|
|
52
|
+
|
|
53
|
+
# Example: using a simple linear embedding model
|
|
54
|
+
class LinearEmbeddingModel(EmbeddingModel):
|
|
55
|
+
def __init__(self, input_size, embedding_dim=64, lr=1e-3, margin=1.0, device="cpu"):
|
|
56
|
+
super().__init__(embedding_dim, margin=margin, device=device)
|
|
57
|
+
self.fc = torch.nn.Sequential(
|
|
58
|
+
torch.nn.Linear(input_size, 128),
|
|
59
|
+
torch.nn.ReLU(),
|
|
60
|
+
torch.nn.Linear(128, embedding_dim)
|
|
61
|
+
)
|
|
62
|
+
self.optimizer = torch.optim.Adam(self.parameters(), lr=lr)
|
|
63
|
+
|
|
64
|
+
def forward(self, x):
|
|
65
|
+
x = self.fc(x)
|
|
66
|
+
return torch.nn.functional.normalize(x, p=2, dim=1)
|
|
67
|
+
|
|
68
|
+
# Prepare your dataset as a list of (feature, label) tuples
|
|
69
|
+
train_dataset = [(torch.randn(10), label) for label in range(5) for _ in range(20)]
|
|
70
|
+
triplet_train = TripletDataset(train_dataset)
|
|
71
|
+
dataloader_train = DataLoader(triplet_train, batch_size=16, shuffle=True)
|
|
72
|
+
|
|
73
|
+
# Instantiate and train the model
|
|
74
|
+
model = LinearEmbeddingModel(input_size=10, embedding_dim=64, lr=1e-3)
|
|
75
|
+
history = model.fit(dataloader_train, epochs=5)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Examples
|
|
79
|
+
You can find examples of real usages in `/examples`, including:
|
|
80
|
+
- Keystroke Dynamics: Comparing typing patterns to determine if they are from the same individual.
|
|
81
|
+
- More to come...
|
|
82
|
+
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# TripletLib
|
|
2
|
+
|
|
3
|
+
TripletLib is a lightweight Python library for training and evaluating embedding models using **triplet loss**. It is designed to help you build models that learn meaningful embeddings for images, time series, or any vector data, enabling tasks like verification, retrieval, or classification based on similarity.
|
|
4
|
+
|
|
5
|
+
## What is Triplet Loss?
|
|
6
|
+
|
|
7
|
+
A "triplet" is a set of 3 samples of data `(anchor, positive, negative)`. The `anchor` is a data sample from a specific class, the `positive` is another sample from the *same* class, and the `negative` is a sample from an entirely different class.
|
|
8
|
+
Triplet loss is a type of metric learning objective that trains an embedding model to map inputs into a vector space such that:
|
|
9
|
+
|
|
10
|
+
- Samples from the **same class** (e.g., anchor and positive) are close together.
|
|
11
|
+
- Samples from **different classes** (e.g., anchor and negative) are far apart.
|
|
12
|
+
|
|
13
|
+
This is particularly useful for tasks like biometric identification (e.g., face recognition), where traditional classification cannot generalize to unseen classes (new faces).
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- Base `EmbeddingModel` class with a built-in training loop.
|
|
18
|
+
- `TripletDataset` wrapper to automatically generate triplets from any dataset.
|
|
19
|
+
- Utilities for evaluating embeddings with **EER** and **AUC** metrics.
|
|
20
|
+
- Compatible with PyTorch and supports GPU acceleration.
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
You can install TripletLib via pip:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install tripletlib
|
|
28
|
+
```
|
|
29
|
+
## Usage
|
|
30
|
+
```python
|
|
31
|
+
import torch
|
|
32
|
+
from tripletlib.triplet_utils import EmbeddingModel, TripletDataset
|
|
33
|
+
from torch.utils.data import DataLoader
|
|
34
|
+
|
|
35
|
+
# Example: using a simple linear embedding model
|
|
36
|
+
class LinearEmbeddingModel(EmbeddingModel):
|
|
37
|
+
def __init__(self, input_size, embedding_dim=64, lr=1e-3, margin=1.0, device="cpu"):
|
|
38
|
+
super().__init__(embedding_dim, margin=margin, device=device)
|
|
39
|
+
self.fc = torch.nn.Sequential(
|
|
40
|
+
torch.nn.Linear(input_size, 128),
|
|
41
|
+
torch.nn.ReLU(),
|
|
42
|
+
torch.nn.Linear(128, embedding_dim)
|
|
43
|
+
)
|
|
44
|
+
self.optimizer = torch.optim.Adam(self.parameters(), lr=lr)
|
|
45
|
+
|
|
46
|
+
def forward(self, x):
|
|
47
|
+
x = self.fc(x)
|
|
48
|
+
return torch.nn.functional.normalize(x, p=2, dim=1)
|
|
49
|
+
|
|
50
|
+
# Prepare your dataset as a list of (feature, label) tuples
|
|
51
|
+
train_dataset = [(torch.randn(10), label) for label in range(5) for _ in range(20)]
|
|
52
|
+
triplet_train = TripletDataset(train_dataset)
|
|
53
|
+
dataloader_train = DataLoader(triplet_train, batch_size=16, shuffle=True)
|
|
54
|
+
|
|
55
|
+
# Instantiate and train the model
|
|
56
|
+
model = LinearEmbeddingModel(input_size=10, embedding_dim=64, lr=1e-3)
|
|
57
|
+
history = model.fit(dataloader_train, epochs=5)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Examples
|
|
61
|
+
You can find examples of real usages in `/examples`, including:
|
|
62
|
+
- Keystroke Dynamics: Comparing typing patterns to determine if they are from the same individual.
|
|
63
|
+
- More to come...
|
|
64
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tripletlib"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A lightweight library for training and evaluating embedding models with triplet loss"
|
|
9
|
+
authors = [
|
|
10
|
+
{ name = "Ted Kane" }
|
|
11
|
+
]
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
license = { text = "MIT" }
|
|
14
|
+
requires-python = ">=3.8"
|
|
15
|
+
|
|
16
|
+
dependencies = [
|
|
17
|
+
"torch",
|
|
18
|
+
"numpy",
|
|
19
|
+
"scikit-learn",
|
|
20
|
+
"scipy",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
examples = ["matplotlib", "pandas"]
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import torch
|
|
3
|
+
from scipy.interpolate import interp1d
|
|
4
|
+
from scipy.optimize import brentq
|
|
5
|
+
from sklearn.metrics import roc_auc_score, roc_curve
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def compute_eer_from_scores(y_true, y_scores):
|
|
9
|
+
"""
|
|
10
|
+
Compute Equal Error Rate (EER) from true labels and predicted scores.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
y_true (array-like): True binary labels (0 or 1).
|
|
14
|
+
y_scores (array-like): Predicted scores or probabilities.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
eer (float): Equal Error Rate.
|
|
18
|
+
eer_threshold (float): Score threshold corresponding to EER.
|
|
19
|
+
"""
|
|
20
|
+
# Compute FPR, TPR, thresholds
|
|
21
|
+
fpr, tpr, thresholds = roc_curve(y_true, y_scores, pos_label=1)
|
|
22
|
+
|
|
23
|
+
eer = brentq(lambda x: 1. - x - interp1d(fpr, tpr)(x), 0., 1.)
|
|
24
|
+
eer_threshold = interp1d(fpr, thresholds)(eer)
|
|
25
|
+
return 1-eer, eer_threshold
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def compute_eer_from_triplets(anchor_emb, positive_emb, negative_emb):
|
|
29
|
+
"""
|
|
30
|
+
Compute EER from a batch of triplet embeddings.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
anchor_emb (torch.Tensor): [batch_size, embedding_dim]
|
|
34
|
+
positive_emb (torch.Tensor): [batch_size, embedding_dim]
|
|
35
|
+
negative_emb (torch.Tensor): [batch_size, embedding_dim]
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
eer (float): Equal Error Rate.
|
|
39
|
+
eer_threshold (float): Distance threshold corresponding to EER.
|
|
40
|
+
"""
|
|
41
|
+
y_true, y_scores = compute_dists_from_triplets(anchor_emb, positive_emb, negative_emb)
|
|
42
|
+
eer, eer_threshold = compute_eer_from_scores(y_true, y_scores)
|
|
43
|
+
return eer, eer_threshold
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def compute_auc_from_triplets(anchor_emb, positive_emb, negative_emb):
|
|
47
|
+
"""
|
|
48
|
+
Compute ROC AUC from a batch of triplet embeddings.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
anchor_emb (torch.Tensor): [batch_size, embedding_dim]
|
|
52
|
+
positive_emb (torch.Tensor): [batch_size, embedding_dim]
|
|
53
|
+
negative_emb (torch.Tensor): [batch_size, embedding_dim]
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
auc (float): Area Under the ROC Curve.
|
|
57
|
+
"""
|
|
58
|
+
y_true, y_scores = compute_dists_from_triplets(anchor_emb, positive_emb, negative_emb)
|
|
59
|
+
|
|
60
|
+
# Compute ROC AUC
|
|
61
|
+
auc = roc_auc_score(y_true, y_scores)
|
|
62
|
+
return auc
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def compute_dists_from_triplets(anchor_emb, positive_emb, negative_emb):
|
|
66
|
+
anchor_emb = anchor_emb.view(anchor_emb.size(0), -1)
|
|
67
|
+
positive_emb = positive_emb.view(positive_emb.size(0), -1)
|
|
68
|
+
negative_emb = negative_emb.view(negative_emb.size(0), -1)
|
|
69
|
+
|
|
70
|
+
# Compute Euclidean distances
|
|
71
|
+
pos_distances = torch.norm(anchor_emb - positive_emb, p=2, dim=1).cpu().detach().numpy()
|
|
72
|
+
neg_distances = torch.norm(anchor_emb - negative_emb, p=2, dim=1).cpu().detach().numpy()
|
|
73
|
+
|
|
74
|
+
# Labels: 1 for positive (same class), 0 for negative (different class)
|
|
75
|
+
y_alike = np.concatenate([np.ones_like(pos_distances), np.zeros_like(neg_distances)])
|
|
76
|
+
total_dists = np.concatenate([pos_distances, neg_distances])
|
|
77
|
+
return y_alike, total_dists
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
import random
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import torch
|
|
6
|
+
from torch.utils.data import Dataset
|
|
7
|
+
from tqdm import tqdm
|
|
8
|
+
|
|
9
|
+
from .evaluation_utils import compute_eer_from_triplets, compute_auc_from_triplets, compute_dists_from_triplets
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class EmbeddingModel(torch.nn.Module, ABC):
|
|
13
|
+
"""
|
|
14
|
+
Base class for models trained with triplet loss.
|
|
15
|
+
Subclasses must implement forward() to compute embeddings.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, embedding_dim, margin=1.0, device="cpu"):
|
|
19
|
+
super().__init__()
|
|
20
|
+
self.embedding_dim = embedding_dim
|
|
21
|
+
self.device = device
|
|
22
|
+
|
|
23
|
+
# Triplet loss (standard margin-based)
|
|
24
|
+
self.criterion = torch.nn.TripletMarginLoss(margin=margin, p=2)
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def forward(self, x):
|
|
28
|
+
"""
|
|
29
|
+
Compute embeddings for input batch x.
|
|
30
|
+
Must be implemented by subclasses.
|
|
31
|
+
"""
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
def fit(self, dataloader_train, dataloader_val=None, epochs=10, model_out_path='model_out.pth') -> dict[
|
|
35
|
+
str, list[float]]:
|
|
36
|
+
"""
|
|
37
|
+
Train the model using triplet loss.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
dataloader_train: iterable yielding (anchor, positive, negative) batches
|
|
41
|
+
dataloader_val: iterable yielding (anchor, positive, negative) batches
|
|
42
|
+
epochs: number of training epochs
|
|
43
|
+
model_out_path: output path for the model
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
history: a dictionary containing historical performance metrics of the model across all epochs
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
history = dict()
|
|
50
|
+
history['aucs_train'] = []
|
|
51
|
+
history['aucs_val'] = []
|
|
52
|
+
history['losses_train'] = []
|
|
53
|
+
history['losses_val'] = []
|
|
54
|
+
history['eers_train'] = []
|
|
55
|
+
history['eers_val'] = []
|
|
56
|
+
|
|
57
|
+
self.to(self.device)
|
|
58
|
+
self.train()
|
|
59
|
+
|
|
60
|
+
for epoch in range(epochs):
|
|
61
|
+
epoch_loss_train = 0.0
|
|
62
|
+
epoch_auc_train = 0.0
|
|
63
|
+
epoch_eer_train = 0.0
|
|
64
|
+
for (anchor, positive, negative) in tqdm(dataloader_train, total=len(dataloader_train),
|
|
65
|
+
desc='Training Loop'):
|
|
66
|
+
anchor, positive, negative = (
|
|
67
|
+
anchor.to(self.device),
|
|
68
|
+
positive.to(self.device),
|
|
69
|
+
negative.to(self.device),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Forward pass: compute embeddings
|
|
73
|
+
anchor_emb = self.forward(anchor)
|
|
74
|
+
pos_emb = self.forward(positive)
|
|
75
|
+
neg_emb = self.forward(negative)
|
|
76
|
+
|
|
77
|
+
# Compute loss
|
|
78
|
+
loss = self.criterion(anchor_emb, pos_emb, neg_emb)
|
|
79
|
+
|
|
80
|
+
# Backprop
|
|
81
|
+
self.optimizer.zero_grad()
|
|
82
|
+
loss.backward()
|
|
83
|
+
self.optimizer.step()
|
|
84
|
+
|
|
85
|
+
epoch_loss_train += loss.item()
|
|
86
|
+
epoch_auc_train += compute_auc_from_triplets(anchor_emb, pos_emb, neg_emb)
|
|
87
|
+
epoch_eer_train += compute_eer_from_triplets(anchor_emb, pos_emb, neg_emb)[0]
|
|
88
|
+
|
|
89
|
+
mean_loss_train = epoch_loss_train / len(dataloader_train)
|
|
90
|
+
mean_auc_train = epoch_auc_train / len(dataloader_train)
|
|
91
|
+
mean_eer_train = epoch_eer_train / len(dataloader_train)
|
|
92
|
+
history['eers_train'].append(mean_eer_train)
|
|
93
|
+
history['aucs_train'].append(mean_auc_train)
|
|
94
|
+
history['losses_train'].append(mean_loss_train)
|
|
95
|
+
print(f"Epoch {epoch + 1}: Train Loss: {mean_loss_train:.4f}")
|
|
96
|
+
|
|
97
|
+
if dataloader_val is not None:
|
|
98
|
+
self.eval()
|
|
99
|
+
epoch_loss_val = 0.0
|
|
100
|
+
epoch_auc_val = 0.0
|
|
101
|
+
epoch_eer_val = 0.0
|
|
102
|
+
with torch.no_grad():
|
|
103
|
+
for (anchor, positive, negative) in tqdm(dataloader_val, total=len(dataloader_val),
|
|
104
|
+
desc='Validation Loop'):
|
|
105
|
+
anchor, positive, negative = (
|
|
106
|
+
anchor.to(self.device),
|
|
107
|
+
positive.to(self.device),
|
|
108
|
+
negative.to(self.device),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Forward pass: compute embeddings
|
|
112
|
+
anchor_emb = self.forward(anchor)
|
|
113
|
+
pos_emb = self.forward(positive)
|
|
114
|
+
neg_emb = self.forward(negative)
|
|
115
|
+
|
|
116
|
+
# Compute loss
|
|
117
|
+
loss = self.criterion(anchor_emb, pos_emb, neg_emb)
|
|
118
|
+
|
|
119
|
+
epoch_loss_val += loss.item()
|
|
120
|
+
epoch_auc_val += compute_auc_from_triplets(anchor_emb, pos_emb, neg_emb)
|
|
121
|
+
epoch_eer_val += compute_eer_from_triplets(anchor_emb, pos_emb, neg_emb)[0]
|
|
122
|
+
|
|
123
|
+
mean_loss_val = epoch_loss_val / len(dataloader_val)
|
|
124
|
+
mean_auc_val = epoch_auc_val / len(dataloader_val)
|
|
125
|
+
mean_eer_val = epoch_eer_val / len(dataloader_val)
|
|
126
|
+
history['eers_val'].append(mean_eer_val)
|
|
127
|
+
history['aucs_val'].append(mean_auc_val)
|
|
128
|
+
history['losses_val'].append(mean_loss_val)
|
|
129
|
+
print(f"Epoch {epoch + 1}: Validation Loss: {mean_loss_val:.4f}")
|
|
130
|
+
|
|
131
|
+
torch.save(self.state_dict(), model_out_path)
|
|
132
|
+
return history
|
|
133
|
+
|
|
134
|
+
def predict_triplet_distances(self, dataloader):
|
|
135
|
+
total_alike = []
|
|
136
|
+
total_dists = []
|
|
137
|
+
with torch.no_grad():
|
|
138
|
+
for (anchor, positive, negative) in tqdm(dataloader, total=len(dataloader),
|
|
139
|
+
desc='Evaluation Loop'):
|
|
140
|
+
anchor, positive, negative = (
|
|
141
|
+
anchor.to(self.device),
|
|
142
|
+
positive.to(self.device),
|
|
143
|
+
negative.to(self.device),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Forward pass: compute embeddings
|
|
147
|
+
anchor_emb = self.forward(anchor)
|
|
148
|
+
pos_emb = self.forward(positive)
|
|
149
|
+
neg_emb = self.forward(negative)
|
|
150
|
+
y_alike, dists = compute_dists_from_triplets(anchor_emb, pos_emb, neg_emb)
|
|
151
|
+
total_alike.extend(y_alike)
|
|
152
|
+
total_dists.extend(dists)
|
|
153
|
+
|
|
154
|
+
return np.array(total_alike), np.array(total_dists)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class TripletDataset(Dataset):
|
|
158
|
+
def __init__(self, base_dataset):
|
|
159
|
+
"""
|
|
160
|
+
Wraps a basic dataset to generate triplets.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
base_dataset: base dataset to be wrapped
|
|
164
|
+
"""
|
|
165
|
+
self.base_dataset = base_dataset
|
|
166
|
+
|
|
167
|
+
# Build index lists for each class
|
|
168
|
+
self.class_to_indices = {}
|
|
169
|
+
for idx, (_, label) in enumerate(self.base_dataset):
|
|
170
|
+
if label not in self.class_to_indices:
|
|
171
|
+
self.class_to_indices[label] = []
|
|
172
|
+
self.class_to_indices[label].append(idx)
|
|
173
|
+
|
|
174
|
+
self.labels = [label for _, label in self.base_dataset]
|
|
175
|
+
|
|
176
|
+
def __getitem__(self, index):
|
|
177
|
+
anchor_sample, anchor_label = self.base_dataset[index]
|
|
178
|
+
|
|
179
|
+
# Positive: random sample with the same label (different index)
|
|
180
|
+
positive_index = index
|
|
181
|
+
while positive_index == index: # TODO: This can get stuck forever, make sure it's possible.
|
|
182
|
+
positive_index = random.choice(self.class_to_indices[anchor_label])
|
|
183
|
+
positive_sample, _ = self.base_dataset[positive_index]
|
|
184
|
+
|
|
185
|
+
# Negative: random sample from a different label
|
|
186
|
+
negative_label = random.choice([label for label in self.class_to_indices.keys() if label != anchor_label])
|
|
187
|
+
negative_index = random.choice(self.class_to_indices[negative_label])
|
|
188
|
+
negative_sample, _ = self.base_dataset[negative_index]
|
|
189
|
+
|
|
190
|
+
return anchor_sample, positive_sample, negative_sample
|
|
191
|
+
|
|
192
|
+
def __len__(self):
|
|
193
|
+
return len(self.base_dataset)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tripletlib
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A lightweight library for training and evaluating embedding models with triplet loss
|
|
5
|
+
Author: Ted Kane
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: torch
|
|
11
|
+
Requires-Dist: numpy
|
|
12
|
+
Requires-Dist: scikit-learn
|
|
13
|
+
Requires-Dist: scipy
|
|
14
|
+
Provides-Extra: examples
|
|
15
|
+
Requires-Dist: matplotlib; extra == "examples"
|
|
16
|
+
Requires-Dist: pandas; extra == "examples"
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# TripletLib
|
|
20
|
+
|
|
21
|
+
TripletLib is a lightweight Python library for training and evaluating embedding models using **triplet loss**. It is designed to help you build models that learn meaningful embeddings for images, time series, or any vector data, enabling tasks like verification, retrieval, or classification based on similarity.
|
|
22
|
+
|
|
23
|
+
## What is Triplet Loss?
|
|
24
|
+
|
|
25
|
+
A "triplet" is a set of 3 samples of data `(anchor, positive, negative)`. The `anchor` is a data sample from a specific class, the `positive` is another sample from the *same* class, and the `negative` is a sample from an entirely different class.
|
|
26
|
+
Triplet loss is a type of metric learning objective that trains an embedding model to map inputs into a vector space such that:
|
|
27
|
+
|
|
28
|
+
- Samples from the **same class** (e.g., anchor and positive) are close together.
|
|
29
|
+
- Samples from **different classes** (e.g., anchor and negative) are far apart.
|
|
30
|
+
|
|
31
|
+
This is particularly useful for tasks like biometric identification (e.g., face recognition), where traditional classification cannot generalize to unseen classes (new faces).
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
- Base `EmbeddingModel` class with a built-in training loop.
|
|
36
|
+
- `TripletDataset` wrapper to automatically generate triplets from any dataset.
|
|
37
|
+
- Utilities for evaluating embeddings with **EER** and **AUC** metrics.
|
|
38
|
+
- Compatible with PyTorch and supports GPU acceleration.
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
You can install TripletLib via pip:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install tripletlib
|
|
46
|
+
```
|
|
47
|
+
## Usage
|
|
48
|
+
```python
|
|
49
|
+
import torch
|
|
50
|
+
from tripletlib.triplet_utils import EmbeddingModel, TripletDataset
|
|
51
|
+
from torch.utils.data import DataLoader
|
|
52
|
+
|
|
53
|
+
# Example: using a simple linear embedding model
|
|
54
|
+
class LinearEmbeddingModel(EmbeddingModel):
|
|
55
|
+
def __init__(self, input_size, embedding_dim=64, lr=1e-3, margin=1.0, device="cpu"):
|
|
56
|
+
super().__init__(embedding_dim, margin=margin, device=device)
|
|
57
|
+
self.fc = torch.nn.Sequential(
|
|
58
|
+
torch.nn.Linear(input_size, 128),
|
|
59
|
+
torch.nn.ReLU(),
|
|
60
|
+
torch.nn.Linear(128, embedding_dim)
|
|
61
|
+
)
|
|
62
|
+
self.optimizer = torch.optim.Adam(self.parameters(), lr=lr)
|
|
63
|
+
|
|
64
|
+
def forward(self, x):
|
|
65
|
+
x = self.fc(x)
|
|
66
|
+
return torch.nn.functional.normalize(x, p=2, dim=1)
|
|
67
|
+
|
|
68
|
+
# Prepare your dataset as a list of (feature, label) tuples
|
|
69
|
+
train_dataset = [(torch.randn(10), label) for label in range(5) for _ in range(20)]
|
|
70
|
+
triplet_train = TripletDataset(train_dataset)
|
|
71
|
+
dataloader_train = DataLoader(triplet_train, batch_size=16, shuffle=True)
|
|
72
|
+
|
|
73
|
+
# Instantiate and train the model
|
|
74
|
+
model = LinearEmbeddingModel(input_size=10, embedding_dim=64, lr=1e-3)
|
|
75
|
+
history = model.fit(dataloader_train, epochs=5)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Examples
|
|
79
|
+
You can find examples of real usages in `/examples`, including:
|
|
80
|
+
- Keystroke Dynamics: Comparing typing patterns to determine if they are from the same individual.
|
|
81
|
+
- More to come...
|
|
82
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
tripletlib/__init__.py
|
|
5
|
+
tripletlib/evaluation_utils.py
|
|
6
|
+
tripletlib/triplet_utils.py
|
|
7
|
+
tripletlib.egg-info/PKG-INFO
|
|
8
|
+
tripletlib.egg-info/SOURCES.txt
|
|
9
|
+
tripletlib.egg-info/dependency_links.txt
|
|
10
|
+
tripletlib.egg-info/requires.txt
|
|
11
|
+
tripletlib.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tripletlib
|