Skip to content

Commit 2ca4841

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

11 files changed

Lines changed: 495 additions & 212 deletions

File tree

.github/workflows/tests.yml

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
name: Tests
2+
3+
on: [push, pull_request]
4+
5+
permissions: {}
6+
7+
jobs:
8+
base:
9+
runs-on: ubuntu-24.04
10+
strategy:
11+
matrix:
12+
python-version: ["3.10", "3.13"]
13+
steps:
14+
- name: Checkout code
15+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
16+
with:
17+
persist-credentials: false
18+
- name: Set up Python
19+
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405
20+
with:
21+
python-version: ${{ matrix.python-version }}
22+
- name: Install development dependencies
23+
run: ./configure --dev
24+
- name: Run code checks
25+
run: |
26+
venv/bin/ruff check
27+
make doc8
28+
- name: Run tests
29+
run: venv/bin/pytest -q
30+
- name: Build distributions
31+
if: matrix.python-version == '3.13'
32+
run: |
33+
venv/bin/python -m build
34+
venv/bin/twine check dist/*
35+
36+
ml:
37+
runs-on: ubuntu-24.04
38+
steps:
39+
- name: Checkout code
40+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
41+
with:
42+
persist-credentials: false
43+
- name: Set up Python
44+
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405
45+
with:
46+
python-version: "3.12"
47+
- name: Install ML dependencies
48+
run: |
49+
./configure --dev
50+
venv/bin/pip install -e ".[training]"
51+
- name: Run ML and integration tests
52+
env:
53+
USE_TF: "0"
54+
run: |
55+
venv/bin/pytest -q \
56+
tests/test_training.py \
57+
tests/test_model.py \
58+
tests/test_export.py \
59+
tests/test_inference.py \
60+
tests/test_model_rules.py

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: 8 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
@@ -78,6 +85,7 @@ onnx =
7885
onnx >= 1.16
7986
onnxruntime >= 1.18
8087
dev =
88+
build
8189
pytest >= 7.0.1
8290
pytest-xdist >= 2
8391
aboutcode-toolkit >= 7.0.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)