Text Classification
Transformers
Joblib
ONNX
Safetensors
English
bert
masterformat
masterformat-classifier
csi-masterformat
construction
construction-technology
sequence-classification
tfidf
ensemble
specs
spec-writing
specifications
takeoff
estimating
cost-code
ufgs
public-domain
Eval Results (legacy)
text-embeddings-inference
Instructions to use constructelligence/masterformat-classifier with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use constructelligence/masterformat-classifier with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-classification", model="constructelligence/masterformat-classifier")# pip install -U transformers accelerate # Load model directly from transformers import AutoTokenizer, AutoModelForSequenceClassification tokenizer = AutoTokenizer.from_pretrained("constructelligence/masterformat-classifier") model = AutoModelForSequenceClassification.from_pretrained("constructelligence/masterformat-classifier", device_map="auto") - Notebooks
- Google Colab
- Kaggle
Bid-item shorthand expansion (abbrev.py) in both predictors + 790-item TxDOT bid-item evaluation
Browse files- README.md +43 -3
- abbrev.py +46 -0
- ensemble/predict_ensemble.py +5 -1
- metrics.json +50 -1
- predict.py +9 -3
- txdot_crosswalk.json +46 -0
README.md
CHANGED
|
@@ -75,6 +75,10 @@ MasterFormat codes without a human choosing from a 171-row list.
|
|
| 75 |
> division level. Because the transformer is trained on specification prose, short estimate line items are its
|
| 76 |
> weak spot; the repo therefore also ships a **transformer + TF-IDF ensemble** (`ensemble/`) that lifts
|
| 77 |
> line-item top-1 to **70.2 %** and improves manual-chunk and section accuracy; see *Ensemble*.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
MasterFormat is a registered trademark of CSI / CSC. This project is independent and not affiliated with or
|
| 80 |
endorsed by them.
|
|
@@ -203,6 +207,39 @@ the weights on the 194-item test itself and does not generalise. The transformer
|
|
| 203 |
section *division* accuracy (0.810). A reasonable production split is the transformer for whole sections, the
|
| 204 |
ensemble for terse line items.
|
| 205 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
## Calibration and abstention
|
| 207 |
|
| 208 |
The ensemble is already close to calibrated; a single **temperature** `T = 0.90` (fit on `val`) lowers the
|
|
@@ -223,8 +260,8 @@ enough to file unattended. Reproduce with `python cloud/stats.py`.
|
|
| 223 |
|
| 224 |
Further gains are **evidence-bound**, not architecture-bound (see the repo's `IMPROVEMENT-PLAN.md`):
|
| 225 |
|
| 226 |
-
1. **Enlarge the eval** (≥1,000 line items, ≥400 sections) so changes are measurable at ±3 pts
|
| 227 |
-
|
| 228 |
2. **Real line-item training data** (public bid tabulations, agency item catalogs) — the transformer's
|
| 229 |
line-item ceiling is a data problem; synthetic augmentation was tested and did not transfer (`mf-0.3`).
|
| 230 |
3. **Domain-adaptive pretraining** on construction prose beyond UFGS, then retrain.
|
|
@@ -261,7 +298,8 @@ Further gains are **evidence-bound**, not architecture-bound (see the repo's `IM
|
|
| 261 |
- **Out-of-taxonomy inputs** (sections whose level-2 group is not among the 171) can only be scored at
|
| 262 |
division level.
|
| 263 |
- **Short, terse line items are the hardest inputs**; division (2-digit) accuracy is consistently higher than
|
| 264 |
-
group (6-digit) accuracy.
|
|
|
|
| 265 |
- **Quantized ONNX drifts.** The int8 export is smaller but less faithful than `mf-0.1`'s; prefer fp32.
|
| 266 |
|
| 267 |
## Bias, risks and safety
|
|
@@ -305,6 +343,8 @@ manual-chunk and whole-section accuracy. Constructelligence's proprietary models
|
|
| 305 |
- `onnx/` — ONNX fp32 and int8 exports.
|
| 306 |
- `ensemble/` — `tfidf.joblib` (word + char n-grams, ~112 MB), `blend.json`, `predict_ensemble.py`: the
|
| 307 |
transformer + TF-IDF line-item ensemble.
|
|
|
|
|
|
|
| 308 |
- `IMPROVEMENT-PLAN.md` — the prioritised roadmap (enlarged eval, data, domain-adaptive pretraining, distillation).
|
| 309 |
- `CITATION.cff`, `requirements.txt`.
|
| 310 |
|
|
|
|
| 75 |
> division level. Because the transformer is trained on specification prose, short estimate line items are its
|
| 76 |
> weak spot; the repo therefore also ships a **transformer + TF-IDF ensemble** (`ensemble/`) that lifts
|
| 77 |
> line-item top-1 to **70.2 %** and improves manual-chunk and section accuracy; see *Ensemble*.
|
| 78 |
+
>
|
| 79 |
+
> **Update (Oct 2026): bid-item shorthand.** Both predictors now expand agency bid abbreviations
|
| 80 |
+
> (`CONC BOX CULV` → *concrete box culvert*) before classifying. On **790 real TxDOT bid items** the ensemble
|
| 81 |
+
> goes from **19.9 % to 48.9 %** top-1 with no change on any earlier evaluation; see *Agency bid-item shorthand*.
|
| 82 |
|
| 83 |
MasterFormat is a registered trademark of CSI / CSC. This project is independent and not affiliated with or
|
| 84 |
endorsed by them.
|
|
|
|
| 207 |
section *division* accuracy (0.810). A reasonable production split is the transformer for whole sections, the
|
| 208 |
ensemble for terse line items.
|
| 209 |
|
| 210 |
+
## Agency bid-item shorthand
|
| 211 |
+
|
| 212 |
+
Public bid tabulations write items in a terse shorthand the transformer never saw in spec prose:
|
| 213 |
+
`RE PM W/RET REQ TY I (W)6"(SLD)(060MIL)`, `CL C CONC (RAIL FOUNDATION)`, `CONC BOX CULV (5 FT X 2 FT)`. Read
|
| 214 |
+
literally, the model sends `CL B CONC` to *Structured Cabling* and `RE PM` to *Electrical Utilities*.
|
| 215 |
+
`abbrev.py` expands 82 common US bid abbreviations (`CONC`, `PAV`, `CULV`, `REINF`, `ASPH`, `W/`…) before
|
| 216 |
+
scoring — **all-caps tokens only**, so prose is left alone, and the original text is appended so nothing is lost.
|
| 217 |
+
`predict.py` and `ensemble/predict_ensemble.py` do this by default; `--no-expand` turns it off. The dictionary is
|
| 218 |
+
written from general bid-item conventions, not fitted to the evaluation below.
|
| 219 |
+
|
| 220 |
+
**Evaluation: 790 TxDOT bid items.** Real item descriptions from Texas DOT public bid tabulations, labelled by
|
| 221 |
+
`txdot_crosswalk.json` — a table mapping 41 TxDOT standard-specification item numbers to MasterFormat groups
|
| 222 |
+
(17 groups, Divisions 01–03, 26, 31–34). Items that could belong to more than one group (repairs, temporary
|
| 223 |
+
work-zone items, signs/signals, special provisions) are left out. *Capped* scores keep at most 20 rows per
|
| 224 |
+
TxDOT item, so pavement markings (207 rows) cannot dominate. 95 % Wilson intervals.
|
| 225 |
+
|
| 226 |
+
| Scorer | top-1 | top-1, capped (n = 529) | top-3 | division |
|
| 227 |
+
|---|---:|---:|---:|---:|
|
| 228 |
+
| mf-0.2 (transformer), raw text | 0.125 [0.104, 0.150] | 0.161 | 0.239 | 0.225 |
|
| 229 |
+
| mf-0.2, abbreviations expanded | 0.270 [0.240, 0.302] | 0.272 | 0.546 | 0.352 |
|
| 230 |
+
| TF-IDF, raw text | 0.266 [0.236, 0.298] | 0.246 | 0.451 | 0.339 |
|
| 231 |
+
| TF-IDF, expanded | 0.484 [0.449, 0.518] | 0.348 | 0.629 | 0.559 |
|
| 232 |
+
| Ensemble, raw text | 0.199 [0.172, 0.228] | 0.223 [0.190, 0.260] | 0.416 | 0.285 |
|
| 233 |
+
| **Ensemble, expanded (default)** | **0.489 [0.454, 0.523]** | **0.369 [0.329, 0.411]** | **0.671** | **0.570** |
|
| 234 |
+
|
| 235 |
+
On the earlier sets the expansion changes 1 of 194 line items and 39 of 6,890 manual chunks: line items stay
|
| 236 |
+
at 0.702, manual chunks at 0.330, and whole-section top-1 moves 0.620 → 0.627.
|
| 237 |
+
|
| 238 |
+
**Reading it.** The expansion is the largest single gain this model has had on real estimate text, with
|
| 239 |
+
non-overlapping intervals. It is still well below the 70 % on hand-written building line items: highway
|
| 240 |
+
bid items are out of domain for a UFGS-trained model, and these labels come from an item-number crosswalk
|
| 241 |
+
(reviewed, not per-item), covering civil work only.
|
| 242 |
+
|
| 243 |
## Calibration and abstention
|
| 244 |
|
| 245 |
The ensemble is already close to calibrated; a single **temperature** `T = 0.90` (fit on `val`) lowers the
|
|
|
|
| 260 |
|
| 261 |
Further gains are **evidence-bound**, not architecture-bound (see the repo's `IMPROVEMENT-PLAN.md`):
|
| 262 |
|
| 263 |
+
1. **Enlarge the eval** (≥1,000 line items, ≥400 sections) so changes are measurable at ±3 pts. *Progress:*
|
| 264 |
+
790 crosswalk-labelled TxDOT bid items (±3.5 pts, civil only); building-estimate items are still 194.
|
| 265 |
2. **Real line-item training data** (public bid tabulations, agency item catalogs) — the transformer's
|
| 266 |
line-item ceiling is a data problem; synthetic augmentation was tested and did not transfer (`mf-0.3`).
|
| 267 |
3. **Domain-adaptive pretraining** on construction prose beyond UFGS, then retrain.
|
|
|
|
| 298 |
- **Out-of-taxonomy inputs** (sections whose level-2 group is not among the 171) can only be scored at
|
| 299 |
division level.
|
| 300 |
- **Short, terse line items are the hardest inputs**; division (2-digit) accuracy is consistently higher than
|
| 301 |
+
group (6-digit) accuracy. Agency shorthand is expanded by `abbrev.py`, but abbreviations outside its
|
| 302 |
+
dictionary, and highway work generally, remain weak (48.9 % top-1 on TxDOT bid items).
|
| 303 |
- **Quantized ONNX drifts.** The int8 export is smaller but less faithful than `mf-0.1`'s; prefer fp32.
|
| 304 |
|
| 305 |
## Bias, risks and safety
|
|
|
|
| 343 |
- `onnx/` — ONNX fp32 and int8 exports.
|
| 344 |
- `ensemble/` — `tfidf.joblib` (word + char n-grams, ~112 MB), `blend.json`, `predict_ensemble.py`: the
|
| 345 |
transformer + TF-IDF line-item ensemble.
|
| 346 |
+
- `abbrev.py` — bid-item abbreviation expansion used by both predictors (`--no-expand` to disable).
|
| 347 |
+
- `txdot_crosswalk.json` — TxDOT item number → MasterFormat group table behind the bid-item evaluation.
|
| 348 |
- `IMPROVEMENT-PLAN.md` — the prioritised roadmap (enlarged eval, data, domain-adaptive pretraining, distillation).
|
| 349 |
- `CITATION.cff`, `requirements.txt`.
|
| 350 |
|
abbrev.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Expand standard construction bid-item abbreviations before classification.
|
| 2 |
+
|
| 3 |
+
Agency bid tabulations and estimates write items in a terse shorthand ("CONC BOX CULV (5 FT X 2 FT)",
|
| 4 |
+
"RE PM W/RET REQ TY I") that a model trained on spec prose cannot read. This is a general dictionary of
|
| 5 |
+
common US bid-item abbreviations (DOT and municipal), written from those conventions, not fitted to an eval set.
|
| 6 |
+
Whole ALL-CAPS tokens only; the original text is kept after the expansion so nothing is lost.
|
| 7 |
+
"""
|
| 8 |
+
import re
|
| 9 |
+
|
| 10 |
+
ABBREV = {
|
| 11 |
+
"ACP": "asphalt concrete pavement", "AGGR": "aggregate", "AGG": "aggregate", "ASPH": "asphalt",
|
| 12 |
+
"ASM": "assembly", "ASSM": "assembly", "ASSY": "assembly", "ATTEN": "attenuator", "BARR": "barricade",
|
| 13 |
+
"BKFL": "backfill", "BM": "beam", "BRK": "broken line", "BS": "base", "CEM": "cement", "CL": "class",
|
| 14 |
+
"CMP": "corrugated metal pipe", "COMPL": "complete", "CONC": "concrete", "CONDR": "conductor",
|
| 15 |
+
"CONDT": "conduit", "CONST": "construction", "CONT": "control", "CRCP": "continuously reinforced concrete pavement",
|
| 16 |
+
"CULV": "culvert", "CUSH": "cushion", "DEL": "delineator", "DIA": "diameter", "DRV": "driveway",
|
| 17 |
+
"ELEC": "electrical", "EMBK": "embankment", "EXC": "excavation", "EXCAV": "excavation", "EXST": "existing",
|
| 18 |
+
"EXIST": "existing", "FBR": "fiber", "FEN": "fence", "FL": "flexible", "GD": "guard", "GR": "grade",
|
| 19 |
+
"HMA": "hot mix asphalt", "IL": "illumination", "ILL": "illumination", "INSTL": "install", "INST": "install",
|
| 20 |
+
"LT": "light", "MATL": "material", "MLCH": "mulch", "MRK": "marking", "MRKR": "marker", "MTL": "metal",
|
| 21 |
+
"PAV": "pavement", "PVMT": "pavement", "PERM": "permanent", "PM": "pavement marking", "PREFAB": "prefabricated",
|
| 22 |
+
"PRESTR": "prestressed", "RC": "reinforced concrete", "RCP": "reinforced concrete pipe", "RD": "roadway",
|
| 23 |
+
"RDWY": "roadway", "REFL": "reflectorized", "REINF": "reinforced", "REM": "remove", "REMOV": "remove",
|
| 24 |
+
"RET": "retroreflectivity", "ROW": "right of way", "RT": "right", "SEDMT": "sediment", "STAB": "stabilized",
|
| 25 |
+
"STR": "structure", "STRUCT": "structure", "SW": "sidewalk", "SWK": "sidewalk", "TEMP": "temporary",
|
| 26 |
+
"TRT": "treatment", "TREAT": "treatment", "TY": "type", "TRANS": "transition", "VEH": "vehicle",
|
| 27 |
+
"W/": "with ", "WW": "wingwall", "HW": "headwall",
|
| 28 |
+
}
|
| 29 |
+
_TOK = re.compile(r"[Ww]/(?=[A-Za-z ])|[A-Za-z]+")
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def expand(text: str) -> str:
|
| 33 |
+
"""'CONC BOX CULV (5 FT X 2 FT)' -> 'concrete BOX culvert (5 FT X 2 FT) | CONC BOX CULV (5 FT X 2 FT)'."""
|
| 34 |
+
hit = False
|
| 35 |
+
|
| 36 |
+
def sub(m):
|
| 37 |
+
nonlocal hit
|
| 38 |
+
tok = m.group(0)
|
| 39 |
+
# shorthand is written in capitals: prose words ("row", "cl") are left alone
|
| 40 |
+
w = ABBREV.get(tok.upper()) if tok.upper() == tok else None
|
| 41 |
+
if w:
|
| 42 |
+
hit = True
|
| 43 |
+
return w
|
| 44 |
+
return m.group(0)
|
| 45 |
+
out = _TOK.sub(sub, text)
|
| 46 |
+
return f"{out} | {text}" if hit else text
|
ensemble/predict_ensemble.py
CHANGED
|
@@ -23,6 +23,8 @@ from pathlib import Path
|
|
| 23 |
import numpy as np
|
| 24 |
|
| 25 |
HERE = Path(__file__).resolve().parent
|
|
|
|
|
|
|
| 26 |
MODEL = "constructelligence/masterformat-classifier"
|
| 27 |
|
| 28 |
DIVISIONS = {
|
|
@@ -95,6 +97,7 @@ def main():
|
|
| 95 |
ap.add_argument("--divisions", action="store_true")
|
| 96 |
ap.add_argument("--file")
|
| 97 |
ap.add_argument("--json", action="store_true")
|
|
|
|
| 98 |
a = ap.parse_args()
|
| 99 |
|
| 100 |
if a.weight is None:
|
|
@@ -113,7 +116,8 @@ def main():
|
|
| 113 |
|
| 114 |
tr, tf = Transformer(a.model), Tfidf(a.tfidf)
|
| 115 |
assert tf.classes == list(range(len(tr.labels))), "TF-IDF classes do not align with the transformer labels"
|
| 116 |
-
|
|
|
|
| 117 |
|
| 118 |
results = []
|
| 119 |
for text, row in zip(texts, lp):
|
|
|
|
| 23 |
import numpy as np
|
| 24 |
|
| 25 |
HERE = Path(__file__).resolve().parent
|
| 26 |
+
sys.path.insert(0, str(HERE.parent))
|
| 27 |
+
from abbrev import expand # noqa: E402 bid-item shorthand ("CONC BOX CULV") -> words the models know
|
| 28 |
MODEL = "constructelligence/masterformat-classifier"
|
| 29 |
|
| 30 |
DIVISIONS = {
|
|
|
|
| 97 |
ap.add_argument("--divisions", action="store_true")
|
| 98 |
ap.add_argument("--file")
|
| 99 |
ap.add_argument("--json", action="store_true")
|
| 100 |
+
ap.add_argument("--no-expand", action="store_true", help="do not expand bid-item abbreviations first")
|
| 101 |
a = ap.parse_args()
|
| 102 |
|
| 103 |
if a.weight is None:
|
|
|
|
| 116 |
|
| 117 |
tr, tf = Transformer(a.model), Tfidf(a.tfidf)
|
| 118 |
assert tf.classes == list(range(len(tr.labels))), "TF-IDF classes do not align with the transformer labels"
|
| 119 |
+
mt = texts if a.no_expand else [expand(t) for t in texts]
|
| 120 |
+
lp = norm(w * tr.logprobs(mt) + (1 - w) * tf.logprobs(mt)) # blend of log-probs, renormalised
|
| 121 |
|
| 122 |
results = []
|
| 123 |
for text, row in zip(texts, lp):
|
metrics.json
CHANGED
|
@@ -21,5 +21,54 @@
|
|
| 21 |
"sections": 153,
|
| 22 |
"top1": 0.6066666666666667,
|
| 23 |
"division": 0.8104575163398693
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
}
|
| 25 |
-
}
|
|
|
|
| 21 |
"sections": 153,
|
| 22 |
"top1": 0.6066666666666667,
|
| 23 |
"division": 0.8104575163398693
|
| 24 |
+
},
|
| 25 |
+
"txdot_bid_items": {
|
| 26 |
+
"_doc": "790 TxDOT bid items labelled via txdot_crosswalk.json; capped = <=20 rows per TxDOT item (n=529); 95% Wilson CI",
|
| 27 |
+
"transformer": {
|
| 28 |
+
"raw": {
|
| 29 |
+
"top1": 0.125,
|
| 30 |
+
"top1_ci": [
|
| 31 |
+
0.104,
|
| 32 |
+
0.15
|
| 33 |
+
],
|
| 34 |
+
"capped_top1": 0.161
|
| 35 |
+
},
|
| 36 |
+
"expanded": {
|
| 37 |
+
"top1": 0.27,
|
| 38 |
+
"top1_ci": [
|
| 39 |
+
0.24,
|
| 40 |
+
0.302
|
| 41 |
+
],
|
| 42 |
+
"capped_top1": 0.272,
|
| 43 |
+
"top3": 0.546,
|
| 44 |
+
"division": 0.352
|
| 45 |
+
}
|
| 46 |
+
},
|
| 47 |
+
"ensemble": {
|
| 48 |
+
"raw": {
|
| 49 |
+
"top1": 0.199,
|
| 50 |
+
"top1_ci": [
|
| 51 |
+
0.172,
|
| 52 |
+
0.228
|
| 53 |
+
],
|
| 54 |
+
"capped_top1": 0.223,
|
| 55 |
+
"top3": 0.416,
|
| 56 |
+
"division": 0.285
|
| 57 |
+
},
|
| 58 |
+
"expanded": {
|
| 59 |
+
"top1": 0.489,
|
| 60 |
+
"top1_ci": [
|
| 61 |
+
0.454,
|
| 62 |
+
0.523
|
| 63 |
+
],
|
| 64 |
+
"capped_top1": 0.369,
|
| 65 |
+
"capped_top1_ci": [
|
| 66 |
+
0.329,
|
| 67 |
+
0.411
|
| 68 |
+
],
|
| 69 |
+
"top3": 0.671,
|
| 70 |
+
"division": 0.57
|
| 71 |
+
}
|
| 72 |
+
}
|
| 73 |
}
|
| 74 |
+
}
|
predict.py
CHANGED
|
@@ -17,6 +17,10 @@ Without arguments it classifies one built-in example.
|
|
| 17 |
import argparse
|
| 18 |
import json
|
| 19 |
import sys
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
MODEL = "constructelligence/masterformat-classifier"
|
| 22 |
|
|
@@ -101,9 +105,9 @@ def onnx_scorer(path, top_k):
|
|
| 101 |
return score
|
| 102 |
|
| 103 |
|
| 104 |
-
def predict(texts, score, top_k, want_div):
|
| 105 |
results = []
|
| 106 |
-
for text, preds in zip(texts, score(texts)):
|
| 107 |
pl = []
|
| 108 |
for p in preds:
|
| 109 |
code, name = split_label(p["label"])
|
|
@@ -124,6 +128,8 @@ def main(argv=None):
|
|
| 124 |
ap.add_argument("--divisions", action="store_true", help="also show the level-1 (division) roll-up")
|
| 125 |
ap.add_argument("--file", help="read newline-separated inputs from a file")
|
| 126 |
ap.add_argument("--json", action="store_true", help="emit JSON instead of a table")
|
|
|
|
|
|
|
| 127 |
a = ap.parse_args(argv)
|
| 128 |
|
| 129 |
texts = list(a.text)
|
|
@@ -138,7 +144,7 @@ def main(argv=None):
|
|
| 138 |
ap.error("no input text")
|
| 139 |
|
| 140 |
score = onnx_scorer(a.onnx, a.top_k) if a.onnx else hf_scorer(a.model, a.top_k)
|
| 141 |
-
results = predict(texts, score, a.top_k, a.divisions)
|
| 142 |
|
| 143 |
if a.json:
|
| 144 |
json.dump(results, sys.stdout, indent=2, ensure_ascii=False)
|
|
|
|
| 17 |
import argparse
|
| 18 |
import json
|
| 19 |
import sys
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
|
| 22 |
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
| 23 |
+
from abbrev import expand # noqa: E402 bid-item shorthand ("CONC BOX CULV") -> words the model knows
|
| 24 |
|
| 25 |
MODEL = "constructelligence/masterformat-classifier"
|
| 26 |
|
|
|
|
| 105 |
return score
|
| 106 |
|
| 107 |
|
| 108 |
+
def predict(texts, score, top_k, want_div, model_texts=None):
|
| 109 |
results = []
|
| 110 |
+
for text, preds in zip(texts, score(model_texts or texts)):
|
| 111 |
pl = []
|
| 112 |
for p in preds:
|
| 113 |
code, name = split_label(p["label"])
|
|
|
|
| 128 |
ap.add_argument("--divisions", action="store_true", help="also show the level-1 (division) roll-up")
|
| 129 |
ap.add_argument("--file", help="read newline-separated inputs from a file")
|
| 130 |
ap.add_argument("--json", action="store_true", help="emit JSON instead of a table")
|
| 131 |
+
ap.add_argument("--no-expand", action="store_true",
|
| 132 |
+
help="do not expand bid-item abbreviations (CONC, PAV, CULV, ...) before classifying")
|
| 133 |
a = ap.parse_args(argv)
|
| 134 |
|
| 135 |
texts = list(a.text)
|
|
|
|
| 144 |
ap.error("no input text")
|
| 145 |
|
| 146 |
score = onnx_scorer(a.onnx, a.top_k) if a.onnx else hf_scorer(a.model, a.top_k)
|
| 147 |
+
results = predict(texts, score, a.top_k, a.divisions, None if a.no_expand else [expand(t) for t in texts])
|
| 148 |
|
| 149 |
if a.json:
|
| 150 |
json.dump(results, sys.stdout, indent=2, ensure_ascii=False)
|
txdot_crosswalk.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_doc": "TxDOT standard-specification item number -> MasterFormat level-2 group (the classifier's 171 classes). Labels for eval/line_items_txdot.tsv come from this table, not from per-item judgement. Only items whose TxDOT title maps to one group without doubt are listed; left out on purpose: repair/maintenance items that could equally sit in a maintenance group (351, 354, 361, 438, 446, 677, 678, 7xx maintenance specs), temporary work-zone items (512, 662), signs and signals (636, 644, 680: MasterFormat 34 40 00 is not one of the 171 classes), 400 (mixes structure excavation with cut-and-restore paving), and every 4-digit special specification (title unknown). Built 2026-10-11; review before relying on it.",
|
| 3 |
+
"items": {
|
| 4 |
+
"100": ["Preparing Right of Way", "31 10 00"],
|
| 5 |
+
"104": ["Removing Concrete", "02 40 00"],
|
| 6 |
+
"105": ["Removing Stabilized Base and Asphalt Pavement", "02 40 00"],
|
| 7 |
+
"110": ["Excavation", "31 20 00"],
|
| 8 |
+
"132": ["Embankment", "31 20 00"],
|
| 9 |
+
"164": ["Seeding for Erosion Control", "32 90 00"],
|
| 10 |
+
"247": ["Flexible Base", "32 10 00"],
|
| 11 |
+
"260": ["Lime Treatment (Road-Mixed)", "31 30 00"],
|
| 12 |
+
"275": ["Cement Treatment (Road-Mixed)", "31 30 00"],
|
| 13 |
+
"316": ["Seal Coat", "32 10 00"],
|
| 14 |
+
"360": ["Concrete Pavement", "32 10 00"],
|
| 15 |
+
"416": ["Drilled Shaft Foundations", "31 60 00"],
|
| 16 |
+
"420": ["Concrete Substructures", "03 30 00"],
|
| 17 |
+
"422": ["Concrete Superstructures", "03 30 00"],
|
| 18 |
+
"425": ["Precast Prestressed Concrete Structural Members", "03 40 00"],
|
| 19 |
+
"429": ["Concrete Structure Repair", "03 00 00"],
|
| 20 |
+
"432": ["Riprap", "31 30 00"],
|
| 21 |
+
"462": ["Concrete Box Culverts and Storm Drains", "33 40 00"],
|
| 22 |
+
"464": ["Reinforced Concrete Pipe", "33 40 00"],
|
| 23 |
+
"465": ["Junction Boxes, Manholes, and Inlets", "33 40 00"],
|
| 24 |
+
"466": ["Headwalls and Wingwalls", "33 40 00"],
|
| 25 |
+
"496": ["Removing Structures", "02 40 00"],
|
| 26 |
+
"500": ["Mobilization", "01 70 00"],
|
| 27 |
+
"502": ["Barricades, Signs, and Traffic Handling", "01 50 00"],
|
| 28 |
+
"506": ["Temporary Erosion, Sedimentation, and Environmental Controls", "31 20 00"],
|
| 29 |
+
"529": ["Concrete Curb, Gutter, and Combined Curb and Gutter", "32 10 00"],
|
| 30 |
+
"530": ["Intersections, Driveways, and Turnouts", "32 10 00"],
|
| 31 |
+
"531": ["Sidewalks", "32 10 00"],
|
| 32 |
+
"540": ["Metal Beam Guard Fence", "34 70 00"],
|
| 33 |
+
"544": ["Guardrail End Treatments", "34 70 00"],
|
| 34 |
+
"545": ["Crash Cushion Attenuators", "34 70 00"],
|
| 35 |
+
"550": ["Chain Link Fence", "32 30 00"],
|
| 36 |
+
"610": ["Roadway Illumination Assemblies", "26 50 00"],
|
| 37 |
+
"618": ["Conduit", "26 00 00"],
|
| 38 |
+
"620": ["Electrical Conductors", "26 00 00"],
|
| 39 |
+
"624": ["Ground Boxes", "26 00 00"],
|
| 40 |
+
"658": ["Delineator and Object Marker Assemblies", "34 70 00"],
|
| 41 |
+
"666": ["Retroreflectorized Pavement Markings", "32 10 00"],
|
| 42 |
+
"668": ["Prefabricated Pavement Markings", "32 10 00"],
|
| 43 |
+
"672": ["Raised Pavement Markers", "32 10 00"],
|
| 44 |
+
"752": ["Tree and Brush Removal", "31 10 00"]
|
| 45 |
+
}
|
| 46 |
+
}
|