Skip to content

Commit f9e7614

Browse files
authored
feat: remove lightning (#337)
* feat: remove lightning * additional test coverage * reduce steps * weight metrics by batch size
1 parent d81e1d3 commit f9e7614

12 files changed

Lines changed: 405 additions & 965 deletions

File tree

model2vec/train/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Training
22

3-
Aside from [distillation](../../README.md#distillation), `model2vec` also supports training simple classifiers on top of static models, using [pytorch](https://pytorch.org/), [lightning](https://lightning.ai/) and [scikit-learn](https://scikit-learn.org/stable/index.html).
3+
Aside from [distillation](../../README.md#distillation), `model2vec` also supports training simple classifiers on top of static models, using [pytorch](https://pytorch.org/) and [scikit-learn](https://scikit-learn.org/stable/index.html).
44

55
We support both single and multi-label classification, which work seamlessly based on the labels you provide.
66

@@ -53,7 +53,7 @@ print(classification_report)
5353

5454
As you can see, we got a pretty nice 91% accuracy, with only 81 seconds of training.
5555

56-
The training loop is handled by [`lightning`](https://pypi.org/project/lightning/). By default the training loop splits the data into a train and validation split, with 90% of the data being used for training and 10% for validation. By default, it runs with early stopping on the validation set accuracy, with a patience of 5.
56+
The training loop is a plain PyTorch loop (see [`model2vec/train/trainer.py`](trainer.py)). By default the training loop splits the data into a train and validation split, with 90% of the data being used for training and 10% for validation. By default, it runs with early stopping on the validation set accuracy, with a patience of 5.
5757

5858
Note that this model is as fast as you're used to from us:
5959

@@ -142,9 +142,9 @@ The core functionality of the `StaticModelForClassification` is contained in a c
142142
* `train_test_split`: governs the train test split before classification.
143143
* `prepare_dataset`: Selects the `torch.Dataset` that will be used in the `Dataloader` during training.
144144
* `_encode`: The encoding function used in the model.
145-
* `fit`: contains all the lightning-related fitting logic.
145+
* `fit`: contains all the fitting logic.
146146

147-
The training of the model is done in a `lighting.LightningModule`, which can be modified but is very basic.
147+
The training loop itself lives in `model2vec.train.trainer.run_training_loop`, a plain torch loop that is fairly basic and easy to modify. Each task passes in its own loss function (and, for classification, a small function that computes extra validation metrics like accuracy).
148148

149149
# Results
150150

model2vec/train/__init__.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import logging
2-
31
from model2vec.utils import get_package_extras, importable
42

53
_REQUIRED_EXTRA = "train"
@@ -10,9 +8,5 @@
108
from model2vec.train.classifier import StaticModelForClassification
119
from model2vec.train.regression import StaticModelForRegression
1210
from model2vec.train.similarity import StaticModelForSimilarity
13-
from model2vec.train.utils import TipFilter
1411

1512
__all__ = ["StaticModelForClassification", "StaticModelForSimilarity", "StaticModelForRegression"]
16-
17-
18-
logging.getLogger("lightning.pytorch.utilities.rank_zero").addFilter(TipFilter())

model2vec/train/base.py

Lines changed: 23 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,10 @@
22

33
import logging
44
from collections.abc import Sequence
5-
from tempfile import TemporaryDirectory
65
from typing import Any, TypeVar
76

8-
import lightning.pytorch as pl
97
import numpy as np
108
import torch
11-
from lightning.pytorch import LightningModule
12-
from lightning.pytorch.callbacks import Callback, EarlyStopping
139
from tokenizers import Encoding, Tokenizer
1410
from torch import nn
1511
from torch.nn.utils.rnn import pad_sequence
@@ -18,10 +14,10 @@
1814
from model2vec.inference import StaticModelPipeline
1915
from model2vec.model import PathLike, StaticModel
2016
from model2vec.train.dataset import TextDataset
17+
from model2vec.train.trainer import MetricsFn, default_metrics, resolve_device, run_training_loop
2118
from model2vec.train.utils import (
2219
get_probable_pad_token_id,
2320
logit,
24-
suppress_lightning_warnings,
2521
to_pipeline,
2622
train_test_split,
2723
)
@@ -222,10 +218,9 @@ def encode(self, X: list[str], batch_size: int = 1024, show_progress_bar: bool =
222218

223219
return np.concatenate(pred, axis=0)
224220

225-
def forward(self, input_ids: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
221+
def forward(self, input_ids: torch.Tensor) -> torch.Tensor:
226222
"""Forward pass through the mean, and a classifier layer after."""
227-
encoded = self._encode(input_ids)
228-
return self.head(encoded), encoded
223+
return self.head(self._encode(input_ids))
229224

230225
def tokenize(self, texts: list[str], max_length: int | None = 512) -> torch.Tensor:
231226
"""Tokenize a bunch of strings into a single padded 2D tensor.
@@ -308,10 +303,10 @@ def _check_val_split(
308303

309304
return train_texts, validation_texts, train_labels, validation_labels
310305

311-
@suppress_lightning_warnings
312306
def _train(
313307
self,
314-
module: LightningModule,
308+
loss_function: nn.Module,
309+
learning_rate: float,
315310
train_dataset: TextDataset,
316311
val_dataset: TextDataset,
317312
batch_size: int,
@@ -320,48 +315,31 @@ def _train(
320315
max_epochs: int | None,
321316
device: str,
322317
validation_steps: int | None,
318+
compute_metrics: MetricsFn = default_metrics,
323319
) -> None:
324-
callbacks: list[Callback] = []
325-
if early_stopping_patience is not None:
326-
callback = EarlyStopping(
327-
monitor=self.val_metric,
328-
mode=self.early_stopping_direction,
329-
patience=early_stopping_patience,
330-
min_delta=0.001,
331-
)
332-
callbacks.append(callback)
333-
334320
val_check_interval, check_val_every_epoch = self._determine_val_check_interval(
335321
validation_steps, len(train_dataset), batch_size
336322
)
337323

338-
with TemporaryDirectory() as tempdir:
339-
trainer = pl.Trainer(
340-
min_epochs=min_epochs,
341-
max_epochs=max_epochs,
342-
callbacks=callbacks,
343-
val_check_interval=val_check_interval,
344-
check_val_every_n_epoch=check_val_every_epoch,
345-
accelerator=device,
346-
default_root_dir=tempdir,
347-
)
348-
349-
trainer.fit(
350-
module,
351-
train_dataloaders=train_dataset.to_dataloader(shuffle=True, batch_size=batch_size),
352-
val_dataloaders=val_dataset.to_dataloader(shuffle=False, batch_size=batch_size),
353-
)
354-
best_model_path = trainer.checkpoint_callback.best_model_path # type: ignore
355-
best_model_weights = torch.load(best_model_path, weights_only=True)
356-
357-
state_dict = {}
358-
for weight_name, weight in best_model_weights["state_dict"].items():
359-
if "loss_function" in weight_name:
360-
# Skip the loss function class weight as its not needed for predictions
361-
continue
362-
state_dict[weight_name.removeprefix("model.")] = weight
324+
state_dict = run_training_loop(
325+
model=self,
326+
loss_function=loss_function,
327+
learning_rate=learning_rate,
328+
val_metric=self.val_metric,
329+
early_stopping_direction=self.early_stopping_direction,
330+
train_loader=train_dataset.to_dataloader(shuffle=True, batch_size=batch_size),
331+
val_loader=val_dataset.to_dataloader(shuffle=False, batch_size=batch_size),
332+
early_stopping_patience=early_stopping_patience,
333+
min_epochs=min_epochs,
334+
max_epochs=max_epochs,
335+
device=resolve_device(device),
336+
val_check_interval=val_check_interval,
337+
check_val_every_epoch=check_val_every_epoch,
338+
compute_metrics=compute_metrics,
339+
)
363340

364341
self.load_state_dict(state_dict)
342+
self.to("cpu")
365343
self.eval()
366344

367345
@staticmethod

model2vec/train/classifier.py

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,35 @@
55
from itertools import chain
66
from typing import Any, Literal, cast
77

8-
import lightning as pl
98
import numpy as np
109
import torch
10+
from sklearn.metrics import jaccard_score
1111
from tokenizers import Tokenizer
12+
from torch import nn
1213
from tqdm import trange
1314

1415
from model2vec.inference import evaluate_single_or_multi_label
1516
from model2vec.train.base import BaseFinetuneable
16-
from model2vec.train.lightning_modules import ClassifierLightningModule, MultiLabelClassifierLightningModule
17-
from model2vec.train.utils import DEFAULT_RANDOM_SEED
17+
from model2vec.train.utils import DEFAULT_RANDOM_SEED, seed_everything
1818

1919
logger = logging.getLogger(__name__)
2020

2121
LabelType = list[str] | list[list[str]]
2222

2323

24+
def _classifier_metrics(head_out: torch.Tensor, y: torch.Tensor, loss: torch.Tensor) -> dict[str, float]:
25+
"""Validation metrics for single-label classification: loss and accuracy."""
26+
accuracy = (head_out.argmax(dim=1) == y).float().mean()
27+
return {"val_loss": loss.item(), "val_accuracy": accuracy.item()}
28+
29+
30+
def _multilabel_classifier_metrics(head_out: torch.Tensor, y: torch.Tensor, loss: torch.Tensor) -> dict[str, float]:
31+
"""Validation metrics for multi-label classification: loss and Jaccard accuracy."""
32+
preds = (torch.sigmoid(head_out) > 0.5).float()
33+
accuracy = cast(float, jaccard_score(y.cpu(), preds.cpu(), average="samples"))
34+
return {"val_loss": loss.item(), "val_accuracy": accuracy}
35+
36+
2437
class StaticModelForClassification(BaseFinetuneable):
2538
val_metric = "val_accuracy"
2639
early_stopping_direction = "max"
@@ -127,7 +140,7 @@ def fit(
127140
) -> StaticModelForClassification:
128141
"""Fit a model.
129142
130-
This function creates a Lightning Trainer object and fits the model to the data.
143+
This function trains the model with a plain torch training loop.
131144
It supports both single-label and multi-label classification.
132145
We use early stopping. After training, the weights of the best model are loaded back into the model.
133146
@@ -157,7 +170,7 @@ def fit(
157170
:return: The fitted model.
158171
:raises ValueError: If either X_val or y_val are provided, but not both.
159172
"""
160-
pl.seed_everything(random_seed)
173+
seed_everything(random_seed)
161174
logger.info("Re-initializing model.")
162175

163176
# Determine whether the task is multilabel based on the type of y.
@@ -177,16 +190,16 @@ def fit(
177190
train_dataset, val_dataset = self._create_datasets(X, y, X_val, y_val, test_size)
178191
batch_size = self._determine_batch_size(batch_size, len(train_dataset))
179192

180-
c: pl.LightningModule
181193
if self.multilabel:
182-
c = MultiLabelClassifierLightningModule(
183-
self, learning_rate=learning_rate, class_weight=resolved_class_weight
184-
)
194+
loss_function: nn.Module = nn.BCEWithLogitsLoss(pos_weight=resolved_class_weight)
195+
compute_metrics = _multilabel_classifier_metrics
185196
else:
186-
c = ClassifierLightningModule(self, learning_rate=learning_rate, class_weight=resolved_class_weight)
197+
loss_function = nn.CrossEntropyLoss(weight=resolved_class_weight)
198+
compute_metrics = _classifier_metrics
187199

188200
self._train(
189-
module=c,
201+
loss_function=loss_function,
202+
learning_rate=learning_rate,
190203
train_dataset=train_dataset,
191204
val_dataset=val_dataset,
192205
batch_size=batch_size,
@@ -195,6 +208,7 @@ def fit(
195208
max_epochs=max_epochs,
196209
device=device,
197210
validation_steps=validation_steps,
211+
compute_metrics=compute_metrics,
198212
)
199213

200214
return self

model2vec/train/lightning_modules.py

Lines changed: 0 additions & 110 deletions
This file was deleted.

model2vec/train/regression.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@
22

33
import logging
44

5-
from model2vec.train.lightning_modules import RegressionLightningModule
5+
from torch import nn
6+
67
from model2vec.train.similarity import StaticModelForSimilarity
78

89
logger = logging.getLogger(__name__)
910

1011

1112
class StaticModelForRegression(StaticModelForSimilarity):
12-
_lightning_class = RegressionLightningModule
13+
@staticmethod
14+
def _build_loss_function() -> nn.Module:
15+
"""Construct the loss function used to train this model."""
16+
return nn.MSELoss()

0 commit comments

Comments
 (0)