Skip to content

Commit 5bff5d1

Browse files
Harden model prediction and rule updates
Signed-off-by: Kaushik <kaushikrjpm10@gmail.com>
1 parent 1745091 commit 5bff5d1

10 files changed

Lines changed: 434 additions & 212 deletions

File tree

README.rst

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,12 @@ reload and validation succeed. See ``docs/source/training.rst`` for details.
5454
Run read-only prediction
5555
========================
5656

57-
Load a validated final model and return candidate required phrases without
58-
changing a ScanCode rule or file:
57+
Install the inference dependencies, then load a validated final model and
58+
return candidate required phrases without changing a ScanCode rule or file:
59+
60+
.. code-block:: console
61+
62+
python -m pip install ".[inference]"
5963
6064
.. code-block:: python
6165
@@ -76,9 +80,11 @@ a final model directory or Hugging Face repository:
7680
7781
add-model-required-phrases --model model-output/final-model --dry-run --verbose
7882
79-
The command validates each candidate with ScanCode's required-phrase helpers and
80-
writes each changed rule once. Rebuild the ScanCode license index after applying
81-
changes without ``--dry-run``.
83+
For a Hugging Face repository, also provide its full commit hash with
84+
``--model-revision``. The command rejects phrase text found more than once in a
85+
rule because ScanCode's mutation helper would mark every occurrence. It validates
86+
the complete rule update and writes each changed rule once. Rebuild the ScanCode
87+
license index after applying changes without ``--dry-run``.
8288

8389
Development
8490
===========

azure-pipelines.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ jobs:
2828
tests/test_training.py \
2929
tests/test_model.py \
3030
tests/test_export.py \
31-
tests/test_inference.py
31+
tests/test_inference.py \
32+
tests/test_model_rules.py
3233
displayName: Run training unit tests
3334
3435
- template: etc/ci/azure-posix.yml

docs/source/model_rules.rst

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ Use the command after reviewing predictions from a validated final model:
1010
--dry-run \
1111
--verbose
1212
13-
``--model`` accepts a local final-model directory or a Hugging Face repository.
14-
The model must pass the hardened publication checks before inference starts.
13+
Install the ``inference`` extra before using this command. ``--model`` accepts
14+
a local final-model directory or a Hugging Face repository. Remote models also
15+
require their full commit hash through ``--model-revision``. The model must pass
16+
the publication checks before inference starts.
1517

1618
The command skips rules that cannot receive generated required phrases and
17-
rules that already contain required-phrase markers. Each prediction must pass
18-
ScanCode's candidate and locatability checks. Accepted phrases are applied in
19-
memory and each changed rule is written once.
19+
rules that already contain required-phrase markers. It rejects phrase text
20+
found more than once because ScanCode would mark every occurrence. The complete
21+
rule update is checked in memory and each changed rule is written once.
2022

2123
Use ``--license-expression`` to process one expression and ``--limit`` for a
2224
small review run. Remove ``--dry-run`` only after reviewing the predictions.

setup.cfg

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,13 @@ console_scripts =
6262

6363

6464
[options.extras_require]
65+
inference =
66+
huggingface-hub == 0.36.2
67+
pytorch-crf == 0.7.2
68+
safetensors >= 0.4
69+
sentencepiece >= 0.2
70+
torch >= 2.0
71+
transformers == 4.57.3
6572
training =
6673
accelerate >= 0.33
6774
huggingface-hub == 0.36.2

src/scancode_required_phrases/inference.py

Lines changed: 9 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from licensedcode.tokenize import required_phrase_splitter
1212

13+
from scancode_required_phrases.training import encode_complete_words
1314
from scancode_required_phrases.training import extract_spans
1415
from scancode_required_phrases.training import first_subword_positions
1516
from scancode_required_phrases.training import ID2LABEL
@@ -41,41 +42,6 @@ def words_from_text(text):
4142
return required_phrase_splitter(unicodedata.normalize("NFKC", text))
4243

4344

44-
def _word_counts(word_ids):
45-
counts = {}
46-
for word_id in word_ids:
47-
if word_id is not None:
48-
counts[word_id] = counts.get(word_id, 0) + 1
49-
return counts
50-
51-
52-
def encode_words(tokenizer, words, max_length):
53-
"""Encode the longest complete-word prefix and report truncation."""
54-
call = dict(
55-
is_split_into_words=True,
56-
add_special_tokens=True,
57-
return_tensors="pt",
58-
)
59-
full = tokenizer(words, truncation=False, **call)
60-
encoding = tokenizer(words, truncation=True, max_length=max_length, **call)
61-
62-
full_counts = _word_counts(full.word_ids())
63-
retained_counts = _word_counts(encoding.word_ids())
64-
covered_words = max(retained_counts, default=-1) + 1
65-
complete_words = covered_words
66-
67-
if covered_words and retained_counts[covered_words - 1] != full_counts[covered_words - 1]:
68-
complete_words -= 1
69-
encoding = tokenizer(words[:complete_words], truncation=False, **call)
70-
71-
if not complete_words:
72-
raise ValueError("Tokenizer retained no complete words")
73-
if encoding["input_ids"].shape[1] > max_length:
74-
raise ValueError("Complete-word encoding exceeds the model maximum length")
75-
76-
return encoding, complete_words < len(words)
77-
78-
7945
def span_confidence(crf, word_emissions, tags, mask, free, span):
8046
"""Return the CRF probability mass agreeing with one decoded span."""
8147
start, end = span
@@ -117,18 +83,22 @@ def predict(self, text):
11783
if not words:
11884
return PredictionResult(words=(), phrases=(), truncated=False)
11985

120-
encoding, truncated = encode_words(
86+
encoding, truncated = encode_complete_words(
87+
tokens=words,
12188
tokenizer=self.tokenizer,
122-
words=words,
12389
max_length=self.max_length,
12490
)
12591
positions = first_subword_positions(encoding.word_ids())
12692
if not positions:
12793
return PredictionResult(words=tuple(words), phrases=(), truncated=False)
12894

12995
device = next(self.model.parameters()).device
130-
input_ids = encoding["input_ids"].to(device)
131-
attention_mask = encoding["attention_mask"].to(device)
96+
input_ids = torch.tensor([encoding["input_ids"]], dtype=torch.long, device=device)
97+
attention_mask = torch.tensor(
98+
[encoding["attention_mask"]],
99+
dtype=torch.long,
100+
device=device,
101+
)
132102

133103
with torch.inference_mode():
134104
emissions = self.model.emissions(input_ids, attention_mask)

0 commit comments

Comments
 (0)