Skip to content

Commit 4629383

Browse files
committed
improve training
1 parent 810d25e commit 4629383

3 files changed

Lines changed: 64 additions & 39 deletions

File tree

model2vec/train/base.py

Lines changed: 54 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@ def __init__(
4646
weights: torch.Tensor | None = None,
4747
freeze: bool = False,
4848
normalize: bool = True,
49+
freeze_weights: bool = False,
4950
) -> None:
50-
"""
51-
Initialize a trainable StaticModel from a StaticModel.
51+
"""Initialize a trainable StaticModel from a StaticModel.
5252
5353
:param vectors: The embeddings of the staticmodel.
5454
:param tokenizer: The tokenizer.
@@ -60,6 +60,7 @@ def __init__(
6060
:param weights: The weights of the model. If None, the weights are initialized to zeros.
6161
:param freeze: Whether to freeze the embeddings. This should be set to False in most cases.
6262
:param normalize: Whether to normalize the embeddings.
63+
:param freeze_weights: Whether to freeze the learned token weights.
6364
"""
6465
super().__init__()
6566
self.pad_id = pad_id
@@ -68,6 +69,7 @@ def __init__(
6869
self.hidden_dim = hidden_dim
6970
self.n_layers = n_layers
7071
self.normalize = normalize
72+
self.freeze_weights = freeze_weights
7173

7274
self.vectors = vectors
7375
if self.vectors.dtype != torch.float32:
@@ -93,26 +95,31 @@ def construct_weights(self) -> nn.Parameter:
9395
"""Construct the weights for the model."""
9496
if self._weights is not None:
9597
w = logit(self._weights)
96-
return nn.Parameter(w.float(), requires_grad=True)
97-
weights = torch.zeros(len(self.token_mapping))
98-
weights[self.pad_id] = -10_000
99-
return nn.Parameter(weights, requires_grad=not self.freeze)
98+
else:
99+
w = torch.zeros(len(self.token_mapping)).float()
100+
w[self.pad_id] = -10_000
101+
return nn.Parameter(w, requires_grad=not self.freeze_weights)
100102

101103
def construct_head(self) -> nn.Sequential:
104+
"""Constructs a simple classifier head."""
105+
return self.construct_mlp(self.n_layers, self.embed_dim, self.hidden_dim, self.out_dim)
106+
107+
@staticmethod
108+
def construct_mlp(n_layers: int, embed_dim: int, hidden_dim: int, out_dim: int) -> nn.Sequential:
102109
"""Constructs a simple classifier head."""
103110
modules: list[nn.Module] = []
104-
if self.n_layers == 0:
105-
modules.append(nn.Linear(self.embed_dim, self.out_dim))
111+
if n_layers == 0:
112+
modules.append(nn.Linear(embed_dim, out_dim))
106113
else:
107114
# If we have a hidden layer, we should first project to hidden_dim
108115
modules = [
109-
nn.Linear(self.embed_dim, self.hidden_dim),
116+
nn.Linear(embed_dim, hidden_dim),
110117
nn.ReLU(),
111118
]
112-
for _ in range(self.n_layers - 1):
113-
modules.extend([nn.Linear(self.hidden_dim, self.hidden_dim), nn.ReLU()])
119+
for _ in range(n_layers - 1):
120+
modules.extend([nn.Linear(hidden_dim, hidden_dim), nn.ReLU()])
114121
# We always have a layer mapping from hidden to out.
115-
modules.append(nn.Linear(self.hidden_dim, self.out_dim))
122+
modules.append(nn.Linear(hidden_dim, out_dim))
116123

117124
linear_modules = [module for module in modules if isinstance(module, nn.Linear)]
118125
if linear_modules:
@@ -137,7 +144,11 @@ def _initialize(self) -> None:
137144

138145
@classmethod
139146
def from_pretrained(
140-
cls: type[ModelType], path: str = "minishlab/potion-base-32m", *, token: str | None = None, **kwargs: Any
147+
cls: type[ModelType],
148+
path: str = "minishlab/potion-base-32m",
149+
*,
150+
token: str | None = None,
151+
**kwargs: Any,
141152
) -> ModelType:
142153
"""Load the model from a pretrained model2vec model."""
143154
if model_name := kwargs.pop("model_name", None):
@@ -148,7 +159,11 @@ def from_pretrained(
148159

149160
@classmethod
150161
def from_static_model(
151-
cls: type[ModelType], *, model: StaticModel, pad_token: str | None = None, **kwargs: Any
162+
cls: type[ModelType],
163+
*,
164+
model: StaticModel,
165+
pad_token: str | None = None,
166+
**kwargs: Any,
152167
) -> ModelType:
153168
"""Load the model from a static model."""
154169
model.embedding = np.nan_to_num(model.embedding)
@@ -172,23 +187,23 @@ def from_static_model(
172187
)
173188

174189
def _encode(self, input_ids: torch.Tensor) -> torch.Tensor:
175-
"""
176-
A forward pass and mean pooling.
190+
"""A forward pass and mean pooling.
177191
178192
This function is analogous to `StaticModel.encode`, but reimplemented to allow gradients
179193
to pass through.
180194
181195
:param input_ids: A 2D tensor of input ids. All input ids are have to be within bounds.
182196
:return: The mean over the input ids, weighted by token weights.
183197
"""
184-
w = self.w[input_ids]
185-
w = torch.sigmoid(w)
186198
zeros = (input_ids != self.pad_id).float()
187-
w = w * zeros
188199
# Add a small epsilon to avoid division by zero
189200
length = zeros.sum(1) + 1e-16
190201
input_ids_embeddings = self.token_mapping[input_ids]
191202
embedded = self.embeddings(input_ids_embeddings)
203+
204+
w = self.w[input_ids]
205+
w = torch.sigmoid(w)
206+
w = w * zeros
192207
# Weigh each token
193208
embedded = torch.bmm(w[:, None, :], embedded).squeeze(1)
194209
# Mean pooling by dividing by the length
@@ -218,8 +233,7 @@ def forward(self, input_ids: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
218233
return self.head(encoded), encoded
219234

220235
def tokenize(self, texts: list[str], max_length: int | None = 512) -> torch.Tensor:
221-
"""
222-
Tokenize a bunch of strings into a single padded 2D tensor.
236+
"""Tokenize a bunch of strings into a single padded 2D tensor.
223237
224238
Note that this is not used during training.
225239
@@ -238,13 +252,22 @@ def device(self) -> torch.device:
238252

239253
def to_static_model(self) -> StaticModel:
240254
"""Convert the model to a static model."""
241-
emb = self.embeddings.weight.detach().cpu().numpy()
242-
w = torch.sigmoid(self.w).detach().cpu().numpy()
255+
with torch.no_grad():
256+
emb = self.embeddings.weight
257+
emb = emb.detach().cpu().numpy()
258+
if self.w is not None:
259+
w = torch.sigmoid(self.w).detach().cpu().numpy()
260+
else:
261+
w = np.ones(len(emb))
243262
# If the weights and emb are the same length, the model was not quantized before training.
244263
if len(w) == len(emb):
245264
emb = emb * w[:, None]
246265
return StaticModel(
247-
vectors=emb, weights=None, tokenizer=self.tokenizer, normalize=self.normalize, token_mapping=None
266+
vectors=emb,
267+
weights=None,
268+
tokenizer=self.tokenizer,
269+
normalize=self.normalize,
270+
token_mapping=None,
248271
)
249272
return StaticModel(
250273
vectors=emb,
@@ -268,7 +291,12 @@ def _determine_batch_size(self, batch_size: int | None, train_length: int) -> in
268291
return batch_size
269292

270293
def _check_val_split(
271-
self, X: list[str], y: list, X_val: list[str] | None, y_val: list | None, test_size: float
294+
self,
295+
X: list[str],
296+
y: list,
297+
X_val: list[str] | None,
298+
y_val: list | None,
299+
test_size: float,
272300
) -> tuple[list[str], list[str], Sequence, Sequence]:
273301
if (X_val is not None) != (y_val is not None):
274302
raise ValueError("Both X_val and y_val must be provided together, or neither.")
@@ -368,8 +396,7 @@ def _determine_val_check_interval(
368396
return val_check_interval, check_val_every_epoch
369397

370398
def _prepare_dataset(self, X: list[str], y: torch.Tensor, max_length: int = 512) -> TextDataset:
371-
"""
372-
Prepare a dataset.
399+
"""Prepare a dataset.
373400
374401
:param X: The texts.
375402
:param y: The labels.

model2vec/train/classifier.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ def __init__(
3838
weights: torch.Tensor | None = None,
3939
freeze: bool = False,
4040
normalize: bool = True,
41+
freeze_weights: bool = False,
4142
) -> None:
4243
"""Initialize a standard classifier model."""
4344
# Alias: Follows scikit-learn. Set to dummy classes
@@ -55,6 +56,7 @@ def __init__(
5556
hidden_dim=hidden_dim,
5657
n_layers=n_layers,
5758
normalize=normalize,
59+
freeze_weights=freeze_weights,
5860
)
5961

6062
@property
@@ -65,8 +67,7 @@ def classes(self) -> np.ndarray:
6567
def predict(
6668
self, X: list[str], show_progress_bar: bool = False, batch_size: int = 1024, threshold: float = 0.5
6769
) -> np.ndarray:
68-
"""
69-
Predict labels for a set of texts.
70+
"""Predict labels for a set of texts.
7071
7172
In single-label mode, each prediction is a single class.
7273
In multilabel mode, each prediction is a list of classes.
@@ -93,8 +94,7 @@ def predict(
9394
return np.array(pred)
9495

9596
def predict_proba(self, X: list[str], show_progress_bar: bool = False, batch_size: int = 1024) -> np.ndarray:
96-
"""
97-
Predict probabilities for each class.
97+
"""Predict probabilities for each class.
9898
9999
In single-label mode, returns softmax probabilities.
100100
In multilabel mode, returns sigmoid probabilities.
@@ -125,8 +125,7 @@ def fit(
125125
validation_steps: int | None = None,
126126
random_seed: int = _DEFAULT_RANDOM_SEED,
127127
) -> StaticModelForClassification:
128-
"""
129-
Fit a model.
128+
"""Fit a model.
130129
131130
This function creates a Lightning Trainer object and fits the model to the data.
132131
It supports both single-label and multi-label classification.
@@ -222,8 +221,7 @@ def _determine_class_weight(
222221
def evaluate(
223222
self, X: list[str], y: LabelType, batch_size: int = 1024, threshold: float = 0.5, output_dict: bool = False
224223
) -> str | dict[str, dict[str, float]]:
225-
"""
226-
Evaluate the classifier on a given dataset using scikit-learn's classification report.
224+
"""Evaluate the classifier on a given dataset using scikit-learn's classification report.
227225
228226
:param X: The texts to predict on.
229227
:param y: The ground truth labels.
@@ -239,8 +237,7 @@ def evaluate(
239237
return report
240238

241239
def _initialize_on_labels(self, y: LabelType) -> None:
242-
"""
243-
Sets the output dimensionality, the classes, and initializes the head.
240+
"""Sets the output dimensionality, the classes, and initializes the head.
244241
245242
:param y: The labels.
246243
:raises ValueError: If the labels are inconsistent.

model2vec/train/similarity.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ def __init__(
3030
weights: torch.Tensor | None = None,
3131
freeze: bool = False,
3232
normalize: bool = True,
33+
freeze_weights: bool = False,
3334
) -> None:
3435
"""Initialize a standard similarity model."""
3536
super().__init__(
@@ -43,6 +44,7 @@ def __init__(
4344
hidden_dim=hidden_dim,
4445
n_layers=n_layers,
4546
normalize=normalize,
47+
freeze_weights=freeze_weights,
4648
)
4749

4850
def fit(
@@ -61,8 +63,7 @@ def fit(
6163
validation_steps: int | None = None,
6264
random_seed: int = _DEFAULT_RANDOM_SEED,
6365
) -> StaticModelForSimilarity:
64-
"""
65-
Fit a model.
66+
"""Fit a model.
6667
6768
This function creates a Lightning Trainer object and fits the model to the data.
6869
We use early stopping. After training, the weights of the best model are loaded back into the model.

0 commit comments

Comments
 (0)