pear-mt 1.0.0__py3-none-any.whl
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.
- pear/__init__.py +31 -0
- pear/commands/__init__.py +1 -0
- pear/commands/main.py +214 -0
- pear/encoders/__init__.py +8 -0
- pear/encoders/base.py +393 -0
- pear/encoders/xlmr.py +185 -0
- pear/encoders/xlmr_xl.py +77 -0
- pear/inference.py +159 -0
- pear/mbr.py +68 -0
- pear/models/__init__.py +85 -0
- pear/models/base.py +246 -0
- pear/models/pairwise/__init__.py +5 -0
- pear/models/pairwise/pairwise_metric.py +206 -0
- pear/models/utils.py +64 -0
- pear/modules/__init__.py +5 -0
- pear/modules/feedforward.py +98 -0
- pear_mt-1.0.0.dist-info/METADATA +310 -0
- pear_mt-1.0.0.dist-info/RECORD +22 -0
- pear_mt-1.0.0.dist-info/WHEEL +5 -0
- pear_mt-1.0.0.dist-info/entry_points.txt +3 -0
- pear_mt-1.0.0.dist-info/licenses/LICENSE +201 -0
- pear_mt-1.0.0.dist-info/top_level.txt +1 -0
pear/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""PEAR-MT: pairwise QE metrics for machine translation evaluation."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
from pear.inference import (
|
|
6
|
+
build_pairwise_samples,
|
|
7
|
+
build_reference_anchored_samples,
|
|
8
|
+
load_metric,
|
|
9
|
+
score_pairwise,
|
|
10
|
+
score_reference_anchored,
|
|
11
|
+
)
|
|
12
|
+
from pear.mbr import pear_utility_matrix, select_mbr_hypothesis
|
|
13
|
+
from pear.models import download_model, load_from_checkpoint
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
__version__ = version("pear-mt")
|
|
17
|
+
except PackageNotFoundError:
|
|
18
|
+
__version__ = "0+unknown"
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"__version__",
|
|
22
|
+
"build_pairwise_samples",
|
|
23
|
+
"build_reference_anchored_samples",
|
|
24
|
+
"download_model",
|
|
25
|
+
"load_from_checkpoint",
|
|
26
|
+
"load_metric",
|
|
27
|
+
"pear_utility_matrix",
|
|
28
|
+
"score_pairwise",
|
|
29
|
+
"score_reference_anchored",
|
|
30
|
+
"select_mbr_hypothesis",
|
|
31
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Console entry points for PEAR."""
|
pear/commands/main.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Command line interface for PEAR."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import csv
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from pear import __version__
|
|
11
|
+
from pear.inference import load_metric, score_pairwise, score_reference_anchored
|
|
12
|
+
from pear.mbr import pear_utility_matrix, select_mbr_hypothesis
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main() -> None:
|
|
16
|
+
parser = argparse.ArgumentParser(description="Run PEAR MT metric inference.")
|
|
17
|
+
parser.add_argument(
|
|
18
|
+
"--version", action="version", version=f"%(prog)s {__version__}"
|
|
19
|
+
)
|
|
20
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
21
|
+
|
|
22
|
+
score = sub.add_parser("score", help="Score TSV rows with src/mt_0/mt_1 columns.")
|
|
23
|
+
_add_model_args(score)
|
|
24
|
+
score.add_argument("--input", type=Path, required=True)
|
|
25
|
+
score.add_argument("--output", type=Path)
|
|
26
|
+
score.add_argument("--mode", choices=["pairwise", "reference"], default="pairwise")
|
|
27
|
+
score.add_argument("--both", action="store_true", help="Score both pair orders.")
|
|
28
|
+
score.add_argument(
|
|
29
|
+
"--batch-size",
|
|
30
|
+
type=int,
|
|
31
|
+
default=8,
|
|
32
|
+
help="Number of pairwise examples per PEAR forward batch.",
|
|
33
|
+
)
|
|
34
|
+
score.add_argument(
|
|
35
|
+
"--gpus",
|
|
36
|
+
type=_parse_gpus,
|
|
37
|
+
default="auto",
|
|
38
|
+
help=(
|
|
39
|
+
"GPU usage: auto uses one available CUDA/MPS device, 0 forces CPU, "
|
|
40
|
+
"and 1 forces one accelerator device."
|
|
41
|
+
),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
mbr = sub.add_parser("mbr", help="Run PEAR MBR over JSONL n-best lists.")
|
|
45
|
+
_add_model_args(mbr)
|
|
46
|
+
mbr.add_argument(
|
|
47
|
+
"--input",
|
|
48
|
+
type=Path,
|
|
49
|
+
required=True,
|
|
50
|
+
help="JSONL with src and hypotheses fields.",
|
|
51
|
+
)
|
|
52
|
+
mbr.add_argument("--output", type=Path)
|
|
53
|
+
mbr.add_argument("--utility", choices=["full", "half"], default="half")
|
|
54
|
+
mbr.add_argument(
|
|
55
|
+
"--batch-size",
|
|
56
|
+
type=int,
|
|
57
|
+
default=8,
|
|
58
|
+
help="Number of pairwise utility comparisons per PEAR forward batch.",
|
|
59
|
+
)
|
|
60
|
+
mbr.add_argument(
|
|
61
|
+
"--gpus",
|
|
62
|
+
type=_parse_gpus,
|
|
63
|
+
default="auto",
|
|
64
|
+
help=(
|
|
65
|
+
"GPU usage: auto uses one available CUDA/MPS device, 0 forces CPU, "
|
|
66
|
+
"and 1 forces one accelerator device."
|
|
67
|
+
),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
args = parser.parse_args()
|
|
71
|
+
if args.command == "score":
|
|
72
|
+
_score(args)
|
|
73
|
+
elif args.command == "mbr":
|
|
74
|
+
_mbr(args)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _add_model_args(parser: argparse.ArgumentParser) -> None:
|
|
78
|
+
parser.add_argument(
|
|
79
|
+
"--model",
|
|
80
|
+
default="pear",
|
|
81
|
+
help=(
|
|
82
|
+
"Model to load: pear, pear-xl, a Hugging Face repo id, or a local "
|
|
83
|
+
".ckpt path. Defaults to pear."
|
|
84
|
+
),
|
|
85
|
+
)
|
|
86
|
+
parser.add_argument(
|
|
87
|
+
"--hf-model",
|
|
88
|
+
help=(
|
|
89
|
+
"Explicit Hugging Face repo id or alias to load. This is a clearer "
|
|
90
|
+
"alternative to passing the same value through --model."
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
parser.add_argument(
|
|
94
|
+
"--checkpoint",
|
|
95
|
+
type=Path,
|
|
96
|
+
help=(
|
|
97
|
+
"Explicit local Lightning .ckpt path to load. This is a clearer "
|
|
98
|
+
"alternative to passing the same path through --model."
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
parser.add_argument("--cache-dir", type=Path)
|
|
102
|
+
parser.add_argument("--local-files-only", action="store_true")
|
|
103
|
+
parser.add_argument(
|
|
104
|
+
"--revision",
|
|
105
|
+
help="Hugging Face checkpoint revision (branch, tag, or commit SHA).",
|
|
106
|
+
)
|
|
107
|
+
parser.add_argument(
|
|
108
|
+
"--encoder-revision",
|
|
109
|
+
help="Hugging Face base-encoder revision (branch, tag, or commit SHA).",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _parse_gpus(value: str) -> int | str:
|
|
114
|
+
if value == "auto":
|
|
115
|
+
return value
|
|
116
|
+
try:
|
|
117
|
+
gpus = int(value)
|
|
118
|
+
except ValueError as exc:
|
|
119
|
+
raise argparse.ArgumentTypeError("expected auto, 0, or 1") from exc
|
|
120
|
+
if gpus not in {0, 1}:
|
|
121
|
+
raise argparse.ArgumentTypeError("expected auto, 0, or 1")
|
|
122
|
+
return gpus
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _resolve_model_arg(args: argparse.Namespace) -> str | Path:
|
|
126
|
+
selected = [args.checkpoint is not None, args.hf_model is not None]
|
|
127
|
+
if sum(selected) > 1:
|
|
128
|
+
raise SystemExit("Use only one of --checkpoint or --hf-model.")
|
|
129
|
+
if args.checkpoint is not None:
|
|
130
|
+
return args.checkpoint
|
|
131
|
+
if args.hf_model is not None:
|
|
132
|
+
return args.hf_model
|
|
133
|
+
return args.model
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _score(args: argparse.Namespace) -> None:
|
|
137
|
+
model = load_metric(
|
|
138
|
+
_resolve_model_arg(args),
|
|
139
|
+
cache_dir=args.cache_dir,
|
|
140
|
+
local_files_only=args.local_files_only,
|
|
141
|
+
revision=args.revision,
|
|
142
|
+
encoder_revision=args.encoder_revision,
|
|
143
|
+
)
|
|
144
|
+
rows = list(csv.DictReader(args.input.open(newline=""), delimiter="\t"))
|
|
145
|
+
sources = [row["src"] for row in rows]
|
|
146
|
+
mode = "both" if args.both else "single"
|
|
147
|
+
if args.mode == "pairwise":
|
|
148
|
+
scores = score_pairwise(
|
|
149
|
+
model,
|
|
150
|
+
sources,
|
|
151
|
+
[r["mt_0"] for r in rows],
|
|
152
|
+
[r["mt_1"] for r in rows],
|
|
153
|
+
mode=mode,
|
|
154
|
+
batch_size=args.batch_size,
|
|
155
|
+
gpus=args.gpus,
|
|
156
|
+
)
|
|
157
|
+
else:
|
|
158
|
+
scores = score_reference_anchored(
|
|
159
|
+
model,
|
|
160
|
+
sources,
|
|
161
|
+
[r["mt"] for r in rows],
|
|
162
|
+
[r["ref"] for r in rows],
|
|
163
|
+
mode=mode,
|
|
164
|
+
batch_size=args.batch_size,
|
|
165
|
+
gpus=args.gpus,
|
|
166
|
+
)
|
|
167
|
+
if isinstance(scores, dict):
|
|
168
|
+
for key, values in scores.items():
|
|
169
|
+
for row, value in zip(rows, values):
|
|
170
|
+
row[f"pear_{key}"] = value
|
|
171
|
+
else:
|
|
172
|
+
for row, value in zip(rows, scores):
|
|
173
|
+
row["pear_score"] = value
|
|
174
|
+
fieldnames = list(rows[0]) if rows else []
|
|
175
|
+
output = args.output or args.input.with_suffix(".pear.tsv")
|
|
176
|
+
with output.open("w", newline="") as out:
|
|
177
|
+
writer = csv.DictWriter(out, fieldnames=fieldnames, delimiter="\t")
|
|
178
|
+
writer.writeheader()
|
|
179
|
+
writer.writerows(rows)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _mbr(args: argparse.Namespace) -> None:
|
|
183
|
+
model = load_metric(
|
|
184
|
+
_resolve_model_arg(args),
|
|
185
|
+
cache_dir=args.cache_dir,
|
|
186
|
+
local_files_only=args.local_files_only,
|
|
187
|
+
revision=args.revision,
|
|
188
|
+
encoder_revision=args.encoder_revision,
|
|
189
|
+
)
|
|
190
|
+
output = args.output or args.input.with_suffix(".pear-mbr.jsonl")
|
|
191
|
+
with args.input.open() as src, output.open("w") as out:
|
|
192
|
+
for line in src:
|
|
193
|
+
item = json.loads(line)
|
|
194
|
+
matrix = pear_utility_matrix(
|
|
195
|
+
model,
|
|
196
|
+
item["src"],
|
|
197
|
+
item["hypotheses"],
|
|
198
|
+
mode=args.utility,
|
|
199
|
+
batch_size=args.batch_size,
|
|
200
|
+
gpus=args.gpus,
|
|
201
|
+
)
|
|
202
|
+
index, utility = select_mbr_hypothesis(matrix)
|
|
203
|
+
item.update(
|
|
204
|
+
{
|
|
205
|
+
"selected_index": index,
|
|
206
|
+
"selected": item["hypotheses"][index],
|
|
207
|
+
"utility": utility,
|
|
208
|
+
}
|
|
209
|
+
)
|
|
210
|
+
out.write(json.dumps(item, ensure_ascii=False) + "\n")
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
if __name__ == "__main__":
|
|
214
|
+
main()
|
pear/encoders/base.py
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
r"""
|
|
2
|
+
Encoder Model base
|
|
3
|
+
====================
|
|
4
|
+
Module defining the common interface between all pretrained encoder models.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import abc
|
|
8
|
+
from typing import Dict, List
|
|
9
|
+
|
|
10
|
+
import torch
|
|
11
|
+
import torch.nn as nn
|
|
12
|
+
from transformers import PreTrainedTokenizerBase
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Encoder(nn.Module, metaclass=abc.ABCMeta):
|
|
16
|
+
"""Base class for an encoder model."""
|
|
17
|
+
|
|
18
|
+
tokenizer: PreTrainedTokenizerBase
|
|
19
|
+
model: nn.Module
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
@abc.abstractmethod
|
|
23
|
+
def output_units(self) -> int:
|
|
24
|
+
"""
|
|
25
|
+
Hidden size of the encoder model.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
int: Hidden size of the encoder model.
|
|
29
|
+
"""
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
@abc.abstractmethod
|
|
34
|
+
def max_positions(self) -> int:
|
|
35
|
+
"""
|
|
36
|
+
Max number of tokens the encoder handles.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
int: Max number of tokens the encoder handles.
|
|
40
|
+
"""
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
@abc.abstractmethod
|
|
45
|
+
def num_layers(self) -> int:
|
|
46
|
+
"""
|
|
47
|
+
Number of model layers available.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
int: Number of model layers available.
|
|
51
|
+
"""
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
@abc.abstractmethod
|
|
56
|
+
def size_separator(self) -> int:
|
|
57
|
+
"""Size of the seperator.
|
|
58
|
+
E.g: For BERT is just 1 (`[SEP]`) but models such as XLM-R and InfoXLM use 2 (`</s></s>`).
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
int: Number of tokens used between two segments.
|
|
62
|
+
"""
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
@abc.abstractmethod
|
|
67
|
+
def from_pretrained(
|
|
68
|
+
cls,
|
|
69
|
+
pretrained_model: str,
|
|
70
|
+
load_pretrained_weights: bool = True,
|
|
71
|
+
local_files_only: bool = False,
|
|
72
|
+
revision: str | None = None,
|
|
73
|
+
) -> "Encoder":
|
|
74
|
+
"""
|
|
75
|
+
Function that loads a pretrained encoder and the respective tokenizer.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
pretrained_model (str): Name of the pretrained model.
|
|
79
|
+
load_pretrained_weights (bool): Whether to load pretrained weights.
|
|
80
|
+
local_files_only (bool): Whether to only use locally cached files.
|
|
81
|
+
revision (str | None): Hugging Face revision to load.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Encoder: Pretrained model from Hugging Face.
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
NotImplementedError: If the method is not implemented.
|
|
88
|
+
"""
|
|
89
|
+
raise NotImplementedError
|
|
90
|
+
|
|
91
|
+
def freeze(self) -> None:
|
|
92
|
+
"""
|
|
93
|
+
Freezes the entire encoder.
|
|
94
|
+
"""
|
|
95
|
+
for param in self.parameters():
|
|
96
|
+
param.requires_grad = False
|
|
97
|
+
|
|
98
|
+
def unfreeze(self) -> None:
|
|
99
|
+
"""
|
|
100
|
+
Unfreezes the entire encoder.
|
|
101
|
+
"""
|
|
102
|
+
for param in self.parameters():
|
|
103
|
+
param.requires_grad = True
|
|
104
|
+
|
|
105
|
+
@abc.abstractmethod
|
|
106
|
+
def freeze_embeddings(self) -> None:
|
|
107
|
+
"""
|
|
108
|
+
Freezes the embedding layer.
|
|
109
|
+
"""
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
@abc.abstractmethod
|
|
113
|
+
def layerwise_lr(self, lr: float, decay: float) -> List:
|
|
114
|
+
"""
|
|
115
|
+
Calculates the learning rate for each encoder layer by applying a small decay.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
lr (float): Learning rate for the highest encoder layer.
|
|
119
|
+
decay (float): decay percentage for the lower layers.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
List: List of model parameters for all layers and the corresponding learning rate.
|
|
123
|
+
"""
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
@abc.abstractmethod
|
|
127
|
+
def forward(
|
|
128
|
+
self, input_ids: torch.Tensor, attention_mask: torch.Tensor, **kwargs
|
|
129
|
+
) -> torch.Tensor:
|
|
130
|
+
"""
|
|
131
|
+
Encoder model forward.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
input_ids (torch.Tensor): Tokenized batch.
|
|
135
|
+
attention_mask (torch.Tensor): Batch attention mask.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
torch.Tensor: All layers embeddings.
|
|
139
|
+
"""
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
def prepare_sample(
|
|
143
|
+
self,
|
|
144
|
+
sample: List[str],
|
|
145
|
+
) -> Dict[str, torch.Tensor]:
|
|
146
|
+
"""
|
|
147
|
+
Receives a list of strings and applies tokenization and vectorization.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
sample (List[str]): List with text segments to be tokenized and padded.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Dict[str, torch.Tensor]: dict with `'input_ids'` and `'attention_mask'`.
|
|
154
|
+
"""
|
|
155
|
+
return {
|
|
156
|
+
k: v
|
|
157
|
+
for k, v in self.tokenizer(
|
|
158
|
+
sample,
|
|
159
|
+
return_tensors="pt",
|
|
160
|
+
padding=True,
|
|
161
|
+
truncation=True,
|
|
162
|
+
max_length=self.max_positions,
|
|
163
|
+
).items()
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
def pad_tensor(
|
|
167
|
+
self, tensor: torch.Tensor, length: int, padding_index: int
|
|
168
|
+
) -> torch.Tensor:
|
|
169
|
+
"""
|
|
170
|
+
Pad a tensor to length with padding_index.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
tensor (torch.Tensor): Tensor to pad.
|
|
174
|
+
length (int): Sequence length after padding.
|
|
175
|
+
padding_index (int): Index to pad tensor with.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
torch.Tensor: Input batch
|
|
179
|
+
"""
|
|
180
|
+
n_padding = length - tensor.shape[0]
|
|
181
|
+
assert n_padding >= 0
|
|
182
|
+
if n_padding == 0:
|
|
183
|
+
return tensor
|
|
184
|
+
padding = tensor.new(n_padding, *tensor.shape[1:]).fill_(padding_index)
|
|
185
|
+
return torch.cat((tensor, padding), dim=0)
|
|
186
|
+
|
|
187
|
+
def concat_sequences(
|
|
188
|
+
self,
|
|
189
|
+
inputs: List[Dict[str, torch.Tensor]],
|
|
190
|
+
return_span_masks: bool = False,
|
|
191
|
+
) -> Dict[str, torch.Tensor]:
|
|
192
|
+
"""
|
|
193
|
+
Concatenate K tokenized sequences per example into a single encoder input, distributing the length budget across segments before concatenation.
|
|
194
|
+
Optionally it returns span masks for SRC/MT0/MT1 content tokens (separators and pads are 0).
|
|
195
|
+
Works for:
|
|
196
|
+
- K=2 : SRC + TR (Absolute QE)
|
|
197
|
+
- K=3 : SRC + TR1 + TR2 (Pairwise)
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
inputs (List[Dict[str, torch.Tensor]]): List of model inputs.
|
|
201
|
+
return_span_masks (bool): Whether to return span masks. Default: `False`.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Dict[str, torch.Tensor]: Returns a single model input with all sentences concatenated into a single input.
|
|
205
|
+
"""
|
|
206
|
+
if len(inputs) == 0:
|
|
207
|
+
raise ValueError("concat_sequences expects at least one input dict.")
|
|
208
|
+
|
|
209
|
+
# 1) Strip padding via attention masks → python lists of ids per segment/example
|
|
210
|
+
segments: List[List[List[int]]] = []
|
|
211
|
+
for encoder_input in inputs:
|
|
212
|
+
ids = encoder_input["input_ids"] # [B, T]
|
|
213
|
+
am = encoder_input["attention_mask"].bool()
|
|
214
|
+
seg_i = [ids[b][am[b]].tolist() for b in range(ids.size(0))]
|
|
215
|
+
segments.append(seg_i)
|
|
216
|
+
|
|
217
|
+
K = len(segments)
|
|
218
|
+
B = len(segments[0])
|
|
219
|
+
|
|
220
|
+
if K != 2 and K != 3:
|
|
221
|
+
raise ValueError(
|
|
222
|
+
f"concat_sequences expects K=2 or K=3 segments, got K={K}."
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# 2) Specials budget (depends on family via size_separator)
|
|
226
|
+
# BERT: [CLS] s1 [SEP] s2 [SEP] s3 [SEP] → specials = 1 + K
|
|
227
|
+
# RoBERTa/XLM-R/InfoXLM:
|
|
228
|
+
# K>=2: <s> s1 </s></s> s2 </s></s> s3 </s> ... → K + 2 (two double boundaries)
|
|
229
|
+
if self.size_separator == 1:
|
|
230
|
+
specials = 1 + K
|
|
231
|
+
else:
|
|
232
|
+
specials = 2 if K == 1 else 2 + (K - 1) * self.size_separator
|
|
233
|
+
|
|
234
|
+
total_content_budget = max(self.max_positions - specials, 0)
|
|
235
|
+
|
|
236
|
+
# Ids
|
|
237
|
+
sep_id = self.tokenizer.sep_token_id
|
|
238
|
+
cls_id = self.tokenizer.cls_token_id
|
|
239
|
+
if cls_id is None or sep_id is None:
|
|
240
|
+
raise ValueError("Tokenizer must define cls_token_id and sep_token_id.")
|
|
241
|
+
assert isinstance(cls_id, int) and isinstance(sep_id, int), (
|
|
242
|
+
f"cls_token_id and sep_token_id must be integers, got "
|
|
243
|
+
f"{type(cls_id)}: {cls_id!r}, {type(sep_id)}: {sep_id!r}"
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
batch_tokens: List[torch.Tensor] = []
|
|
247
|
+
# When masks requested, we store per-example seg-ids (0=special/pad, 1=SRC, 2=MT0, 3=MT1)
|
|
248
|
+
segid_lists: List[List[int]] = [] if return_span_masks else None # type: ignore
|
|
249
|
+
|
|
250
|
+
for b in range(B):
|
|
251
|
+
# content tokens per segment (strip outer specials)
|
|
252
|
+
contents = [segments[k][b][1:-1] for k in range(K)]
|
|
253
|
+
raw_lens = torch.tensor([len(c) for c in contents], dtype=torch.long)
|
|
254
|
+
|
|
255
|
+
# 3) Equal-split allocation with round-robin handout
|
|
256
|
+
q = total_content_budget // K
|
|
257
|
+
alloc = torch.full((K,), q, dtype=torch.long)
|
|
258
|
+
alloc = torch.minimum(alloc, raw_lens) # respect per-segment caps
|
|
259
|
+
leftover = int(total_content_budget - int(alloc.sum().item()))
|
|
260
|
+
while leftover > 0:
|
|
261
|
+
elig = torch.nonzero(raw_lens > alloc, as_tuple=False).flatten()
|
|
262
|
+
if elig.numel() == 0:
|
|
263
|
+
break
|
|
264
|
+
for t in elig.tolist():
|
|
265
|
+
i = int(t)
|
|
266
|
+
alloc[i] += 1
|
|
267
|
+
leftover -= 1
|
|
268
|
+
if leftover == 0:
|
|
269
|
+
break
|
|
270
|
+
|
|
271
|
+
# truncate contents to allocated budgets
|
|
272
|
+
contents = [contents[k][: int(alloc[k].item())] for k in range(K)]
|
|
273
|
+
|
|
274
|
+
merged: List[int] = []
|
|
275
|
+
segids: List[int] = [] if return_span_masks else None # type: ignore
|
|
276
|
+
|
|
277
|
+
if self.size_separator == 1:
|
|
278
|
+
# BERT: [CLS] s1 [SEP] s2 [SEP] (s3 [SEP])*
|
|
279
|
+
merged.append(cls_id)
|
|
280
|
+
if segids is not None:
|
|
281
|
+
segids.append(0)
|
|
282
|
+
# s1 (SRC)
|
|
283
|
+
for t in contents[0]:
|
|
284
|
+
merged.append(t)
|
|
285
|
+
if segids is not None:
|
|
286
|
+
segids.append(1)
|
|
287
|
+
merged.append(sep_id)
|
|
288
|
+
if segids is not None:
|
|
289
|
+
segids.append(0)
|
|
290
|
+
# s2 (MT0)
|
|
291
|
+
for t in contents[1]:
|
|
292
|
+
merged.append(t)
|
|
293
|
+
if segids is not None:
|
|
294
|
+
segids.append(2)
|
|
295
|
+
merged.append(sep_id)
|
|
296
|
+
if segids is not None:
|
|
297
|
+
segids.append(0)
|
|
298
|
+
# s3 (MT1) if present
|
|
299
|
+
if K == 3:
|
|
300
|
+
for t in contents[2]:
|
|
301
|
+
merged.append(t)
|
|
302
|
+
if segids is not None:
|
|
303
|
+
segids.append(3)
|
|
304
|
+
merged.append(sep_id)
|
|
305
|
+
if segids is not None:
|
|
306
|
+
segids.append(0)
|
|
307
|
+
else:
|
|
308
|
+
# RoBERTa/XLM-R/InfoXLM
|
|
309
|
+
merged.append(cls_id)
|
|
310
|
+
if segids is not None:
|
|
311
|
+
segids.append(0)
|
|
312
|
+
# <s> s1 </s></s> s2 </s></s> [ + s3 </s>]
|
|
313
|
+
# s1 (SRC)
|
|
314
|
+
for t in contents[0]:
|
|
315
|
+
merged.append(t)
|
|
316
|
+
if segids is not None:
|
|
317
|
+
segids.append(1)
|
|
318
|
+
# first double boundary
|
|
319
|
+
merged.append(sep_id)
|
|
320
|
+
merged.append(sep_id)
|
|
321
|
+
if segids is not None:
|
|
322
|
+
segids.append(0)
|
|
323
|
+
segids.append(0)
|
|
324
|
+
# s2 (MT0)
|
|
325
|
+
for t in contents[1]:
|
|
326
|
+
merged.append(t)
|
|
327
|
+
if segids is not None:
|
|
328
|
+
segids.append(2)
|
|
329
|
+
# second double boundary
|
|
330
|
+
merged.append(sep_id)
|
|
331
|
+
merged.append(sep_id)
|
|
332
|
+
if segids is not None:
|
|
333
|
+
segids.append(0)
|
|
334
|
+
segids.append(0)
|
|
335
|
+
# s3.. (MT1 if present)
|
|
336
|
+
if K == 3:
|
|
337
|
+
for t in contents[2]:
|
|
338
|
+
merged.append(t)
|
|
339
|
+
if segids is not None:
|
|
340
|
+
segids.append(3)
|
|
341
|
+
merged.append(sep_id)
|
|
342
|
+
if segids is not None:
|
|
343
|
+
segids.append(0)
|
|
344
|
+
|
|
345
|
+
assert (
|
|
346
|
+
len(merged) <= self.max_positions
|
|
347
|
+
), f"Merged length {len(merged)} exceeds max_positions {self.max_positions}."
|
|
348
|
+
|
|
349
|
+
batch_tokens.append(torch.tensor(merged, dtype=torch.long))
|
|
350
|
+
if return_span_masks:
|
|
351
|
+
segid_lists.append(segids) # type: ignore
|
|
352
|
+
|
|
353
|
+
# 4) Pad and build attention mask
|
|
354
|
+
lengths = [t.size(0) for t in batch_tokens]
|
|
355
|
+
max_len = max(lengths)
|
|
356
|
+
pad_raw = self.tokenizer.pad_token_id
|
|
357
|
+
if not isinstance(pad_raw, int):
|
|
358
|
+
raise ValueError(
|
|
359
|
+
f"Tokenizer must have integer pad_token_id; got {pad_raw!r}."
|
|
360
|
+
)
|
|
361
|
+
pad_id: int = pad_raw
|
|
362
|
+
|
|
363
|
+
padded = [self.pad_tensor(t, max_len, pad_id) for t in batch_tokens]
|
|
364
|
+
padded = torch.stack(padded, dim=0).contiguous() # [B, L]
|
|
365
|
+
lens_t = torch.tensor(lengths, dtype=torch.long)
|
|
366
|
+
arange = torch.arange(max_len, device=padded.device)
|
|
367
|
+
attention_mask = (arange[None, :] < lens_t[:, None]).long() # [B, L]
|
|
368
|
+
|
|
369
|
+
out = {"input_ids": padded, "attention_mask": attention_mask}
|
|
370
|
+
|
|
371
|
+
if return_span_masks:
|
|
372
|
+
# Build masks from segids (0=special/pad)
|
|
373
|
+
src_mask = torch.zeros((B, max_len), dtype=torch.long)
|
|
374
|
+
mt0_mask = torch.zeros((B, max_len), dtype=torch.long)
|
|
375
|
+
mt1_mask = torch.zeros((B, max_len), dtype=torch.long) if K == 3 else None
|
|
376
|
+
for b in range(B):
|
|
377
|
+
Lb = lengths[b]
|
|
378
|
+
seg = segid_lists[b]
|
|
379
|
+
# copy segids into [B,L], pad tail stays 0
|
|
380
|
+
for i in range(Lb):
|
|
381
|
+
sid = seg[i]
|
|
382
|
+
if sid == 1:
|
|
383
|
+
src_mask[b, i] = 1
|
|
384
|
+
elif sid == 2:
|
|
385
|
+
mt0_mask[b, i] = 1
|
|
386
|
+
elif sid == 3 and K == 3:
|
|
387
|
+
mt1_mask[b, i] = 1 # type: ignore
|
|
388
|
+
out["src_mask"] = src_mask
|
|
389
|
+
out["mt0_mask"] = mt0_mask
|
|
390
|
+
if K == 3:
|
|
391
|
+
out["mt1_mask"] = mt1_mask # type: ignore
|
|
392
|
+
|
|
393
|
+
return out
|