File size: 7,174 Bytes
f4cbb91
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7bdb384
 
 
 
 
 
f4cbb91
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7bdb384
f4cbb91
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
---
datasets:
- NagaYu/signpost-label-quality
language:
- de
- en
- es
- fr
- it
- ja
- ko
- pt
- ru
- zh
library_name: generic
license: mit
metrics:
- accuracy
- f1
- precision
- recall
pipeline_tag: text-classification
tags:
- accessibility
- a11y
- aria
- wcag
- label-quality
- multilingual
- synthetic
- signpost-hashed
---

# Signpost label-quality classifier (hashed backend)

Judges whether an accessibility label actually identifies the control it is
attached to -- the question left over after `axe-core` and Xcode's Accessibility
Inspector have both passed. There *is* a name; does the name mean anything on
this screen?

Repository: `NagaYu/signpost-classifier`

| | |
|---|---|
| Code and the full evaluation | <https://github.com/NagaYu/signpost> |
| Dataset it was trained on | <https://huggingface.co/datasets/NagaYu/signpost-label-quality> |
| Try it in your browser | <https://huggingface.co/spaces/NagaYu/signpost> |

It classifies one label into eight classes: `ok` plus seven ways a label fails
(`generic`, `role_echo`, `identifier`, `duplicate`, `verbose`, `missing`,
`mismatch`), and reports the character span of the offending part.


## How to use it

```python
from signpost.context import ContextBuilder, ContextConfig
from signpost.model import QualityClassifier

clf = QualityClassifier.load("path/to/this/checkpoint")
builder = ContextBuilder(ContextConfig(level="full"))  # must match the checkpoint
bundles = builder.build_screen(screen)               # a signpost.types.ScreenTree
for bundle, prediction in zip(bundles, clf.predict(bundles)):
    print(bundle.name, prediction.error_type, round(prediction.confidence, 3))
```

or from the command line, over an axe run, an XCUITest dump or a uiautomator
dump:

```bash
signpost check tree.json --model path/to/this/checkpoint
```

The context level is pinned in the checkpoint and enforced on every call:
feeding it bundles built at another level raises rather than returning numbers
that look fine and mean nothing.

## What was measured

Held-out **origins** -- no site or app on the test side appears in training --
with 860 labels, 309 planted defects and 511 labels the strict quality gate calls
good.

| what was measured | value | on |
| --- | --- | --- |
| of the planted defects the rule baselines missed, share Signpost reported | 54.6% [48.3%, 60.8%] | 238 missed defects |
| the same, with the right error type | 53.4% | 238 missed defects |
| good labels this model reported anyway (over-flagging) | 0.59% [0.20%, 1.71%] | 511 good labels |
| the same, for the rule baselines | 0.00% | 511 good labels |
| hard set: defects every syntactic check still passes | 40.6% | n=96 |
| error-type accuracy over the eight classes | 0.824 | - |
| macro F1 over the eight classes | 0.710 | - |
| median time to judge one label | 0.054 ms | 17585 labels/s |

The operating threshold is **0.644**, chosen on the *training* split as the most
talkative cut whose measured over-flagging stayed inside a 2% budget. The test
numbers above were computed at that threshold and it was never tuned on them.

### Per class

| class | precision | recall | F1 | support |
| --- | --- | --- | --- | --- |
| ok | 0.79 | 0.99 | 0.88 | 515 |
| generic | 1.00 | 0.69 | 0.82 | 55 |
| role_echo | 0.75 | 0.19 | 0.30 | 48 |
| identifier | 0.94 | 0.57 | 0.71 | 54 |
| duplicate | 0.74 | 0.39 | 0.51 | 36 |
| verbose | 1.00 | 1.00 | 1.00 | 36 |
| missing | 1.00 | 0.96 | 0.98 | 27 |
| mismatch | 0.94 | 0.32 | 0.48 | 53 |

### Does the context help?

The same backend, the same hyperparameters, the same seed and the same screens,
trained once per context level. The only thing that varies is what the model was
allowed to look at.

| context level | what the model may see | macro F1 | over-flagging | hard set |
| --- | --- | --- | --- | --- |
| none | role and name -- what a rule checker sees | 0.611 | 2.90% | 10.4% |
| local | + value, hint, visible text, icon asset, traits | 0.612 | 2.12% | 10.4% |
| full | + siblings, neighbouring text, screen title, inferred action | 0.774 | 0.97% | 49.0% |

## Training

- Corpus: `signpost/data/seed_screens.jsonl (synthetic)`
- Split: by origin, 30% of nodes held out, stratified by language; train and
  test share no origin, checked before training.
- Defects: planted by `signpost/inject.py` at a rate of 35%, with a different
  seed on each side of the split so a test defect is not a training defect
  relocated.
- Backend: `hashed`, class weighting `balanced`, seed 0.
- **No base model.** The hashed backend is a matrix of floats over
  hashed context features, trained from scratch. There is no encoder to
  attribute, which is why the frontmatter carries no `base_model`.
- Fit: 0.7 s on Darwin arm64 python 3.13.5.
- No rewrite adapter is included. The template rewriters in `signpost.rewrite` need no model and are what `signpost check` uses by default.

## Limitations -- read these before quoting a number above

- **The corpus is synthetic.** Every screen was composed by
  `scripts/make_seed_corpus.py` from a vocabulary written for this project, and
  every defect was planted by a generator this project also wrote. A model
  evaluated here is partly being measured on how learnable that generator is.
  Real screens are messier: more abbreviations, more house style, more labels
  that are bad in ways nobody thought to plant. Treat these figures as an upper
  bound on a real codebase rather than an estimate of one.
- **Some classes it is bad at.** Weakest in this run:
  `role_echo` recall 0.19 on 48 rows; `mismatch` recall 0.32 on 53 rows.
  In the project's reference run (`python benchmarks/run.py`, seed 0) the two
  weakest are `role_echo` recall 0.21 and `mismatch` recall 0.32 -- and
  `mismatch` is the class this project exists for. A defect this model misses is
  a defect nobody is told about.
- **It is not a replacement for anything.** Not for `axe-core`, not for Xcode's
  Accessibility Inspector or Android's lint checks -- they are better than this
  model at the classes they cover, `missing` above all -- and not for review by
  people who use assistive technology. It narrows what a human has to look at.
- **It does not know what anyone will experience.** Every finding points at a
  documented guideline (WCAG 2.2, Apple's and Google's own guidance) through
  `signpost.cite`, and says nothing about how any person will perceive a screen.
- **`duplicate` is a property of a screen**, not of a label. A row taken out of
  its screen cannot be judged for that class.
- **The threshold is a policy, not a fact.** A team gating a merge queue can
  afford fewer false alarms than a team reviewing a hundred screens by hand;
  `calibrate_threshold` is how they pick their own, and `set_threshold` is how
  they impose it.

## Reproducing this checkpoint

```bash
python scripts/train.py --backend hashed --seed 0 --test-frac 0.30 --inject-rate 0.35
```

Same seed, same bytes: the hashed backend trains full-batch with no sampling
anywhere in the loop.

## Licence

`mit`. The corpus is synthetic and carries no third-party redistribution
question; `signpost/collect.py` is the path for real pages and keeps per-row
provenance.