kaiyuanzh commited on
Commit
1e6bb1e
·
0 Parent(s):

Add pplx-pii-masking

Browse files
Files changed (8) hide show
  1. .gitattributes +36 -0
  2. LICENSE +21 -0
  3. README.md +73 -0
  4. config.json +95 -0
  5. example_usage.py +275 -0
  6. model.safetensors +3 -0
  7. tokenizer.json +3 -0
  8. tokenizer_config.json +15 -0
.gitattributes ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ tokenizer.json filter=lfs diff=lfs merge=lfs -text
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Perplexity AI, Inc.
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.
README.md ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ language:
4
+ - en
5
+ - multilingual
6
+ tags:
7
+ - pii
8
+ - token-classification
9
+ - privacy
10
+ - qwen3
11
+ pipeline_tag: token-classification
12
+ ---
13
+
14
+ # pplx-pii-masking
15
+
16
+ PII masking model for conversational data. A ~600M-parameter bidirectional
17
+ Qwen3 encoder
18
+ ([`perplexity-ai/pplx-embed-v1-0.6b`](https://huggingface.co/perplexity-ai/pplx-embed-v1-0.6b)
19
+ backbone, `use_bidirectional_attention=true`) with two heads:
20
+
21
+ - **Token classification head** (1024 -> 37): BIOES tags over 9 PII categories
22
+ (`private_person`, `account_number`, `private_url`, `private_date`,
23
+ `private_address`, `private_email`, `private_phone`, `other_pii`, `secret`),
24
+ decoded with a constrained Viterbi decoder.
25
+ - **Sensitivity head** (1024 -> 1): conversation-level sensitivity classifier
26
+ on mean-pooled hidden states.
27
+
28
+ ## Usage
29
+
30
+ [`example_usage.py`](example_usage.py) in this repo is a self-contained
31
+ reference pipeline (`pip install torch safetensors transformers`). It loads
32
+ the encoder implementation from the
33
+ [backbone repo](https://huggingface.co/perplexity-ai/pplx-embed-v1-0.6b) via
34
+ `trust_remote_code`, swaps in this repo's fine-tuned weights, applies the two
35
+ heads, and decodes spans with the constrained BIOES Viterbi included in the
36
+ script:
37
+
38
+ ```python
39
+ import sys
40
+
41
+ from huggingface_hub import snapshot_download
42
+
43
+ repo = snapshot_download("perplexity-ai/pplx-pii-masking")
44
+ sys.path.insert(0, repo)
45
+ from example_usage import PiiMasker
46
+
47
+ masker = PiiMasker(repo)
48
+
49
+ text = ("Hi, I'm Daniel Whitfield, you can reach me at "
50
+ "daniels@meridiancap.com or 415-555-0123.")
51
+ spans, sensitivity = masker(text)
52
+ for s in spans:
53
+ print(s.label, (s.start, s.end), text[s.start:s.end])
54
+ # private_person (8, 24) Daniel Whitfield
55
+ # private_email (46, 69) daniels@meridiancap.com
56
+ # private_phone (73, 85) 415-555-0123
57
+
58
+ print(masker.mask(text))
59
+ # Hi, I'm [PRIVATE_PERSON], you can reach me at [PRIVATE_EMAIL] or [PRIVATE_PHONE].
60
+ ```
61
+
62
+ ## Checkpoint layout
63
+
64
+ `model.safetensors` holds the fine-tuned backbone (bf16, `backbone.*`), both
65
+ heads (fp32, `token_cls_head.*` / `sensitivity_head.*`), and the Viterbi bias
66
+ scalars (`viterbi.*`). `max_seq_len` is 4096 tokens.
67
+
68
+ Inference outline: tokenize (no BOS/EOS added), run the bidirectional encoder,
69
+ then per token `logits = h @ W_cls.T + b_cls` decoded with a constrained BIOES
70
+ Viterbi, and `sensitivity = sigmoid(mean(h) @ W_sen.T + b_sen)`. The
71
+ `PPLXQwen3Model` encoder implementation (`configuration.py` / `modeling.py`
72
+ referenced by `config.json`'s `auto_map`) ships with the
73
+ [backbone repo](https://huggingface.co/perplexity-ai/pplx-embed-v1-0.6b).
config.json ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "pii_masking",
3
+ "backbone": {
4
+ "vocab_size": 151936,
5
+ "max_position_embeddings": 32768,
6
+ "hidden_size": 1024,
7
+ "intermediate_size": 3072,
8
+ "num_hidden_layers": 28,
9
+ "num_attention_heads": 16,
10
+ "use_sliding_window": false,
11
+ "sliding_window": null,
12
+ "max_window_layers": 28,
13
+ "num_key_value_heads": 8,
14
+ "head_dim": 128,
15
+ "hidden_act": "silu",
16
+ "initializer_range": 0.02,
17
+ "rms_norm_eps": 1e-06,
18
+ "use_cache": false,
19
+ "attention_bias": false,
20
+ "attention_dropout": 0.0,
21
+ "layer_types": [
22
+ "full_attention",
23
+ "full_attention",
24
+ "full_attention",
25
+ "full_attention",
26
+ "full_attention",
27
+ "full_attention",
28
+ "full_attention",
29
+ "full_attention",
30
+ "full_attention",
31
+ "full_attention",
32
+ "full_attention",
33
+ "full_attention",
34
+ "full_attention",
35
+ "full_attention",
36
+ "full_attention",
37
+ "full_attention",
38
+ "full_attention",
39
+ "full_attention",
40
+ "full_attention",
41
+ "full_attention",
42
+ "full_attention",
43
+ "full_attention",
44
+ "full_attention",
45
+ "full_attention",
46
+ "full_attention",
47
+ "full_attention",
48
+ "full_attention",
49
+ "full_attention"
50
+ ],
51
+ "pad_token_id": null,
52
+ "bos_token_id": 151643,
53
+ "eos_token_id": 151643,
54
+ "tie_word_embeddings": true,
55
+ "rope_parameters": {
56
+ "rope_theta": 1000000,
57
+ "rope_type": "default"
58
+ },
59
+ "return_dict": true,
60
+ "output_hidden_states": false,
61
+ "dtype": "bfloat16",
62
+ "chunk_size_feed_forward": 0,
63
+ "is_encoder_decoder": false,
64
+ "architectures": [
65
+ "PPLXQwen3Model"
66
+ ],
67
+ "id2label": {
68
+ "0": "LABEL_0",
69
+ "1": "LABEL_1"
70
+ },
71
+ "label2id": {
72
+ "LABEL_0": 0,
73
+ "LABEL_1": 1
74
+ },
75
+ "problem_type": null,
76
+ "_name_or_path": "perplexity-ai/pplx-embed-v1-0.6b",
77
+ "transformers_version": "5.2.0",
78
+ "auto_map": {
79
+ "AutoConfig": "configuration.PPLXQwen3Config",
80
+ "AutoModel": "modeling.PPLXQwen3Model"
81
+ },
82
+ "model_type": "bidirectional_pplx_qwen3",
83
+ "use_bidirectional_attention": true,
84
+ "output_attentions": false
85
+ },
86
+ "num_token_labels": 37,
87
+ "hidden_size": 1024,
88
+ "loss_weight_token_cls": 1.5,
89
+ "loss_weight_sensitivity": 0.3,
90
+ "dropout": 0.1,
91
+ "max_seq_len": 4096,
92
+ "viterbi_b_bias": 0.0,
93
+ "viterbi_e_bias": 0.0,
94
+ "freeze_backbone": false
95
+ }
example_usage.py ADDED
@@ -0,0 +1,275 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Detect and mask PII with pplx-pii-masking.
3
+
4
+ Self-contained: the encoder implementation (`PPLXQwen3Model`) is pulled from
5
+ the public backbone repo perplexity-ai/pplx-embed-v1-0.6b via
6
+ `trust_remote_code`, the fine-tuned weights come from this repo, and the
7
+ constrained BIOES Viterbi decoder is inlined below.
8
+
9
+ pip install torch safetensors transformers
10
+ python example_usage.py "some text with daniels@meridiancap.com in it"
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import sys
15
+ from dataclasses import dataclass
16
+ from pathlib import Path
17
+
18
+ import torch
19
+ from safetensors.torch import load_file
20
+ from transformers import AutoModel, AutoTokenizer
21
+
22
+ BACKBONE_REPO = "perplexity-ai/pplx-embed-v1-0.6b"
23
+
24
+ PII_TYPES = [
25
+ "private_person",
26
+ "private_email",
27
+ "private_phone",
28
+ "private_address",
29
+ "private_url",
30
+ "private_date",
31
+ "account_number",
32
+ "secret",
33
+ "other_pii",
34
+ ]
35
+ # BIOES label list: O + 9 types x {B, I, E, S} = 37 (matches the checkpoint)
36
+ BIOES_LABELS = ["O"] + [f"{tag}-{t}" for t in PII_TYPES for tag in "BIES"]
37
+
38
+
39
+ # ---------------------------------------------------------------------------
40
+ # Constrained BIOES Viterbi decoder
41
+ # ---------------------------------------------------------------------------
42
+
43
+ @dataclass
44
+ class PredictedSpan:
45
+ start: int # character offset in document text
46
+ end: int
47
+ label: str # e.g. "private_person"
48
+ score: float
49
+
50
+
51
+ def strip_span_whitespace(text: str, spans: list[PredictedSpan]) -> list[PredictedSpan]:
52
+ """Trim leading/trailing whitespace from predicted character spans.
53
+
54
+ Tokenizers fuse the space before a word into the token (e.g. '_john'),
55
+ so the B-token's char_start is one position before the actual PII value.
56
+ """
57
+ out: list[PredictedSpan] = []
58
+ for s in spans:
59
+ new_start, new_end = s.start, s.end
60
+ while new_start < new_end and text[new_start] in (" ", "\t", "\n"):
61
+ new_start += 1
62
+ while new_end > new_start and text[new_end - 1] in (" ", "\t", "\n"):
63
+ new_end -= 1
64
+ if new_start < new_end:
65
+ out.append(PredictedSpan(new_start, new_end, s.label, s.score))
66
+ return out
67
+
68
+
69
+ class ViterbiDecoder:
70
+ """Constrained BIOES Viterbi decoder with transition bias scalars.
71
+
72
+ The bias scalars are added to all ->B and E-> transitions, allowing
73
+ precision/recall trade-offs without retraining.
74
+ """
75
+
76
+ def __init__(self, labels: list[str], b_bias: float = 0.0, e_bias: float = 0.0):
77
+ self.labels = labels
78
+ self.num_labels = len(labels)
79
+ self.label2id = {label: idx for idx, label in enumerate(labels)}
80
+ self.id2label = {idx: label for idx, label in enumerate(labels)}
81
+ self.pii_types = [label[2:] for label in labels if label.startswith("S-")]
82
+ self.b_bias = b_bias
83
+ self.e_bias = e_bias
84
+
85
+ def _build_transition_scores(self) -> torch.Tensor:
86
+ """[num_labels, num_labels] float transition score matrix."""
87
+ n, l2i = self.num_labels, self.label2id
88
+ mask = torch.zeros(n, n, dtype=torch.bool)
89
+ end_states = {l2i["O"]} # states a span can end on (O, E-*, S-*)
90
+ begin_states = {l2i["O"]} # states valid after a boundary (O, B-*, S-*)
91
+ for pii_type in self.pii_types:
92
+ b, i = l2i[f"B-{pii_type}"], l2i[f"I-{pii_type}"]
93
+ e, s = l2i[f"E-{pii_type}"], l2i[f"S-{pii_type}"]
94
+ end_states |= {e, s}
95
+ begin_states |= {b, s}
96
+ mask[b, i] = mask[b, e] = True # B -> I/E of same type
97
+ mask[i, i] = mask[i, e] = True # I -> I/E of same type
98
+ for from_state in end_states:
99
+ for to_state in begin_states:
100
+ mask[from_state, to_state] = True
101
+
102
+ scores = torch.full((n, n), float("-inf"))
103
+ scores[mask] = 0.0
104
+ for pii_type in self.pii_types:
105
+ b, e = l2i[f"B-{pii_type}"], l2i[f"E-{pii_type}"]
106
+ for from_s in range(n):
107
+ if scores[from_s, b] > float("-inf"):
108
+ scores[from_s, b] += self.b_bias # entering B states
109
+ for to_s in range(n):
110
+ if scores[e, to_s] > float("-inf"):
111
+ scores[e, to_s] += self.e_bias # leaving E states
112
+ return scores
113
+
114
+ @torch.no_grad()
115
+ def decode(
116
+ self,
117
+ logits: torch.Tensor,
118
+ offset_mapping: list[tuple[int, int]],
119
+ text: str | None = None,
120
+ ) -> list[PredictedSpan]:
121
+ """Decode a single sequence.
122
+
123
+ Args:
124
+ logits: [T, num_labels] float tensor
125
+ offset_mapping: list of (char_start, char_end) per token
126
+ text: source text; when provided, leading/trailing whitespace is
127
+ stripped from predicted span boundaries.
128
+ """
129
+ T, C = logits.shape
130
+ assert C == self.num_labels
131
+
132
+ trans = self._build_transition_scores() # [C, C]
133
+
134
+ viterbi_scores = torch.full((T, C), float("-inf"))
135
+ backpointers = torch.zeros((T, C), dtype=torch.long)
136
+
137
+ # t=0: only O / B-* / S-* are valid start states
138
+ start_mask = torch.full((C,), float("-inf"))
139
+ for label, idx in self.label2id.items():
140
+ if label == "O" or label.startswith("B-") or label.startswith("S-"):
141
+ start_mask[idx] = 0.0
142
+ viterbi_scores[0] = logits[0] + start_mask
143
+
144
+ for t in range(1, T):
145
+ # [C, 1] + [C, C] -> [C, C]; dim-0 = prev, dim-1 = next
146
+ scores_t = viterbi_scores[t - 1].unsqueeze(1) + trans
147
+ best_prev, best_idx = scores_t.max(dim=0)
148
+ viterbi_scores[t] = logits[t] + best_prev
149
+ backpointers[t] = best_idx
150
+
151
+ # End constraint: only O / E-* / S-* valid at end
152
+ end_mask = torch.full((C,), float("-inf"))
153
+ for label, idx in self.label2id.items():
154
+ if label == "O" or label.startswith("E-") or label.startswith("S-"):
155
+ end_mask[idx] = 0.0
156
+ best_last = int((viterbi_scores[T - 1] + end_mask).argmax().item())
157
+
158
+ path = [best_last]
159
+ for t in range(T - 1, 0, -1):
160
+ path.append(int(backpointers[t, path[-1]].item()))
161
+ path.reverse()
162
+
163
+ spans = self._path_to_spans(path, logits, offset_mapping)
164
+ if text is not None:
165
+ spans = strip_span_whitespace(text, spans)
166
+ return spans
167
+
168
+ def _path_to_spans(
169
+ self,
170
+ path: list[int],
171
+ logits: torch.Tensor,
172
+ offset_mapping: list[tuple[int, int]],
173
+ ) -> list[PredictedSpan]:
174
+ spans: list[PredictedSpan] = []
175
+ T = len(path)
176
+ t = 0
177
+ while t < T:
178
+ label = self.id2label[path[t]]
179
+ char_start, char_end = offset_mapping[t]
180
+
181
+ if label.startswith("S-"):
182
+ pii_type = label[2:]
183
+ score = float(logits[t, path[t]].sigmoid().item())
184
+ if char_start < char_end:
185
+ spans.append(PredictedSpan(char_start, char_end, pii_type, score))
186
+ t += 1
187
+
188
+ elif label.startswith("B-"):
189
+ pii_type = label[2:]
190
+ span_start, span_end = char_start, char_end
191
+ tok_scores = [float(logits[t, path[t]].item())]
192
+ t += 1
193
+ while t < T:
194
+ inner = self.id2label[path[t]]
195
+ if inner == f"I-{pii_type}":
196
+ _, span_end = offset_mapping[t]
197
+ tok_scores.append(float(logits[t, path[t]].item()))
198
+ t += 1
199
+ elif inner == f"E-{pii_type}":
200
+ _, span_end = offset_mapping[t]
201
+ tok_scores.append(float(logits[t, path[t]].item()))
202
+ t += 1
203
+ break
204
+ else:
205
+ break
206
+ if span_start < span_end:
207
+ score = float(torch.tensor(tok_scores).mean().sigmoid().item())
208
+ spans.append(PredictedSpan(span_start, span_end, pii_type, score))
209
+
210
+ else:
211
+ t += 1
212
+
213
+ return spans
214
+
215
+
216
+ # ---------------------------------------------------------------------------
217
+ # Model loading and inference
218
+ # ---------------------------------------------------------------------------
219
+
220
+ class PiiMasker:
221
+ def __init__(self, repo_dir: str | Path, device: str = "cpu"):
222
+ repo_dir = Path(repo_dir)
223
+ self.device = device
224
+ self.tokenizer = AutoTokenizer.from_pretrained(repo_dir)
225
+
226
+ # Encoder architecture from the public backbone repo; weights are then
227
+ # replaced with the fine-tuned backbone from this checkpoint.
228
+ self.backbone = AutoModel.from_pretrained(
229
+ BACKBONE_REPO, dtype=torch.bfloat16, trust_remote_code=True
230
+ ).to(device).eval()
231
+
232
+ sd = load_file(str(repo_dir / "model.safetensors"))
233
+ self.backbone.load_state_dict(
234
+ {k.removeprefix("backbone."): v for k, v in sd.items()
235
+ if k.startswith("backbone.")}
236
+ )
237
+ self.w_cls = sd["token_cls_head.weight"].float().to(device) # [37, 1024]
238
+ self.b_cls = sd["token_cls_head.bias"].float().to(device)
239
+ self.w_sen = sd["sensitivity_head.weight"].float().to(device) # [1, 1024]
240
+ self.b_sen = sd["sensitivity_head.bias"].float().to(device)
241
+ self.viterbi = ViterbiDecoder(
242
+ BIOES_LABELS,
243
+ b_bias=float(sd["viterbi.b_bias"].item()),
244
+ e_bias=float(sd["viterbi.e_bias"].item()),
245
+ )
246
+
247
+ @torch.no_grad()
248
+ def __call__(self, text: str) -> tuple[list[PredictedSpan], float]:
249
+ enc = self.tokenizer(text, return_offsets_mapping=True,
250
+ return_tensors="pt", truncation=True, max_length=4096)
251
+ h = self.backbone(
252
+ input_ids=enc["input_ids"].to(self.device),
253
+ attention_mask=enc["attention_mask"].to(self.device),
254
+ ).last_hidden_state[0].float() # [T, 1024]
255
+ logits = h @ self.w_cls.T + self.b_cls # [T, 37]
256
+ sensitivity = torch.sigmoid(h.mean(0) @ self.w_sen.T + self.b_sen).item()
257
+ offsets = [tuple(o) for o in enc["offset_mapping"][0].tolist()]
258
+ spans = self.viterbi.decode(logits.cpu(), offsets, text=text)
259
+ return spans, sensitivity
260
+
261
+ def mask(self, text: str) -> str:
262
+ spans, _ = self(text)
263
+ for s in sorted(spans, key=lambda s: -s.start):
264
+ text = text[:s.start] + f"[{s.label.upper()}]" + text[s.end:]
265
+ return text
266
+
267
+
268
+ if __name__ == "__main__":
269
+ text = sys.argv[1]
270
+ masker = PiiMasker(Path(__file__).parent)
271
+ spans, sensitivity = masker(text)
272
+ print(f"sensitivity: {sensitivity:.3f}")
273
+ for s in spans:
274
+ print(f" {s.label:18s} [{s.start}:{s.end}] {text[s.start:s.end]!r}")
275
+ print(masker.mask(text))
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f6204155ec540c9323f706e284110ee848b462f0325dc1ece5c7263fc517bbd0
3
+ size 1192293777
tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cae14d1c8dda080f23792355b0692b826bf1f1da3c86ebc1b37548a391cf6526
3
+ size 11422936
tokenizer_config.json ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_prefix_space": false,
3
+ "backend": "tokenizers",
4
+ "bos_token": null,
5
+ "clean_up_tokenization_spaces": false,
6
+ "eos_token": "<|endoftext|>",
7
+ "errors": "replace",
8
+ "mask_token": "â½Ĺ",
9
+ "model_max_length": 131072,
10
+ "pad_token": "<|endoftext|>",
11
+ "sep_token": "<|endoftext|>",
12
+ "split_special_tokens": false,
13
+ "tokenizer_class": "PreTrainedTokenizerFast",
14
+ "unk_token": null
15
+ }