PabloRod11's picture
Release evaluated occupation classifier and model card
a8b5f43 verified
Raw History Blame Contribute Delete
5.77 kB
"""Load the safe JSON baseline. Inference needs only Python's standard library."""
import json
import math
from pathlib import Path
import re
from collections import Counter
TOKEN_PATTERN = r"(?u)\b\w\w+\b"
def route_confidence(confidence, thresholds, known_features=True):
"""Model routing policy; absence of known text always requires escalation."""
if isinstance(confidence, bool) or not isinstance(confidence, (int, float)) or not math.isfinite(confidence) or not 0 <= confidence <= 1:
raise ValueError('Confidence must be a finite probability')
auto = thresholds['auto_accept']
return ('ESCALATE' if not known_features or confidence < thresholds['escalate']
else 'AUTO_ACCEPT' if auto is not None and confidence >= auto else 'HUMAN_REVIEW')
def validate_model(model):
"""Fail at load time rather than emit plausible predictions from corrupt data."""
try:
if type(model['format_version']) is not int or model['format_version'] != 1:
raise ValueError('Unsupported model format')
expected = {'token_pattern': TOKEN_PATTERN, 'ngram_range': [1, 2],
'lowercase': True, 'sublinear_tf': True, 'norm': 'l2'}
if model['vectorizer'] != expected:
raise ValueError('Unsupported vectorizer configuration')
classes, vocabulary = model['classes'], model['vocabulary']
if not isinstance(classes, list) or len(classes) != 5 or any(not isinstance(c, str) or not c for c in classes) or len(set(classes)) != 5:
raise ValueError('Expected five unique classes')
if not isinstance(model['model_version'], str) or not model['model_version'].strip():
raise ValueError('Missing model version')
if not isinstance(vocabulary, dict) or not vocabulary or any(not isinstance(k, str) or type(v) is not int for k, v in vocabulary.items()) or set(vocabulary.values()) != set(range(len(vocabulary))):
raise ValueError('Invalid vocabulary indices')
width = len(vocabulary)
def numbers(values, size):
return isinstance(values, list) and len(values) == size and all(type(v) in (int, float) and math.isfinite(v) for v in values)
if not numbers(model['idf'], width) or any(v <= 0 for v in model['idf']):
raise ValueError('Invalid IDF values')
if not numbers(model['intercept'], 5) or not isinstance(model['coefficients'], list) or len(model['coefficients']) != 5 or any(not numbers(row, width) for row in model['coefficients']):
raise ValueError('Invalid classifier dimensions or weights')
thresholds = model['thresholds']
if not isinstance(thresholds, dict) or set(thresholds) != {'auto_accept', 'escalate'}:
raise ValueError('Invalid threshold configuration')
for value in thresholds.values():
if value is not None and (type(value) not in (int, float) or not math.isfinite(value) or not 0 <= value <= 1):
raise ValueError('Invalid routing threshold')
if thresholds['escalate'] is None or (thresholds['auto_accept'] is not None and thresholds['auto_accept'] < thresholds['escalate']):
raise ValueError('Inconsistent routing thresholds')
except (KeyError, TypeError) as exc:
raise ValueError('Incomplete or malformed model artifact') from exc
class OccupationClassifier:
def __init__(self, path=None):
directory = Path(__file__).parent
self.path = Path(path) if path is not None else (directory / 'model.json' if (directory / 'model.json').exists() else directory / 'model' / 'model.json')
self.model = json.loads(self.path.read_text())
validate_model(self.model)
self.classes = self.model['classes']
self.thresholds = self.model['thresholds']
def predict(self, text):
if not isinstance(text, str) or not text.strip():
raise ValueError('Enter a nonempty job title or description.')
if len(text) > 10000:
raise ValueError('Text must be at most 10,000 characters.')
tokens = re.findall(TOKEN_PATTERN, text.lower())
terms = tokens + [' '.join(pair) for pair in zip(tokens, tokens[1:])]
counts = Counter(terms)
vocabulary = self.model['vocabulary']
features = {vocabulary[t]: (1 + math.log(n)) * self.model['idf'][vocabulary[t]]
for t, n in counts.items() if t in vocabulary}
norm = math.sqrt(sum(value * value for value in features.values()))
if norm:
features = {index: value / norm for index, value in features.items()}
logits = [bias + sum(weights[index] * value for index, value in features.items())
for bias, weights in zip(self.model['intercept'], self.model['coefficients'])]
maximum = max(logits)
exp = [math.exp(value - maximum) for value in logits]
probabilities = [value / sum(exp) for value in exp]
winner = max(range(len(probabilities)), key=probabilities.__getitem__)
confidence = probabilities[winner]
route = route_confidence(confidence, self.thresholds, bool(features))
return {'label': self.classes[winner], 'prediction': self.classes[winner], 'confidence': confidence, 'route': route,
'probabilities': dict(zip(self.classes, probabilities)),
'known_features': len(features), 'model_version': self.model['model_version']}
Classifier = OccupationClassifier
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('text')
parser.add_argument('--model', type=Path)
args = parser.parse_args()
print(json.dumps(OccupationClassifier(args.model).predict(args.text), indent=2))