product_attribute / src /train.py
kshitiz14's picture
Fix Dockerfile for HF Spaces (port 7860), stop tracking model artifact
896a559
Raw History Blame Contribute Delete
3.15 kB
"""
Trains one multi-label classifier per attribute type on the labeled dataset.
Approach
--------
- Features: character n-gram TF-IDF (char_wb, 2-5) over the raw description.
Character n-grams generalize better than word n-grams on a 61-row dataset
because they share sub-word signal across morphological variants
("embroidered"/"embroidery", "beaded"/"beading", "off-shoulder"/
"off shoulder") without needing a stemmer.
- Label space: for each attribute type, the label space is exactly the
canonical vocabulary in lexicon.py (colors use COLOR_VOCAB). This keeps the
model closed-vocabulary and directly comparable to the rules baseline.
- Classifier: OneVsRestClassifier(LogisticRegression) per attribute type,
i.e. independent binary classifiers per label, combined into a multi-label
prediction per attribute (a description can have 0, 1, or many labels for
a given attribute, e.g. two colors).
- Given the small dataset, model artifacts are ensembled at inference time
with the deterministic rules extractor (see predict.py) rather than relied
on in isolation.
"""
import json
import joblib
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.multiclass import OneVsRestClassifier
from sklearn.preprocessing import MultiLabelBinarizer
from lexicon import ATTRIBUTE_LEXICON, COLOR_VOCAB
ATTR_TYPES = ["silhouette", "fabric", "neckline", "sleeve", "length",
"embellishment", "category", "color"]
def label_space(attr):
if attr == "color":
return COLOR_VOCAB
return list(ATTRIBUTE_LEXICON[attr].keys())
def load_dataset(path="../data/dataset.json"):
with open(path) as f:
return json.load(f)
def train_all(dataset_path="../data/dataset.json", out_path="model.joblib"):
data = load_dataset(dataset_path)
texts = [d["text"] for d in data]
vectorizer = TfidfVectorizer(analyzer="char_wb", ngram_range=(2, 5), min_df=1)
X = vectorizer.fit_transform(texts)
models = {}
binarizers = {}
for attr in ATTR_TYPES:
classes = label_space(attr)
mlb = MultiLabelBinarizer(classes=classes)
y_labels = [d["labels"][attr] for d in data]
Y = mlb.fit_transform(y_labels)
# Skip labels with zero positive examples (OneVsRest can't fit them);
# in practice every label in our lexicon appears >=1 time in the 61
# rows, but guard anyway for robustness on future dataset edits.
if Y.sum() == 0:
models[attr] = None
binarizers[attr] = mlb
continue
clf = OneVsRestClassifier(
LogisticRegression(max_iter=1000, class_weight="balanced")
)
clf.fit(X, Y)
models[attr] = clf
binarizers[attr] = mlb
artifact = {
"vectorizer": vectorizer,
"models": models,
"binarizers": binarizers,
"attr_types": ATTR_TYPES,
}
joblib.dump(artifact, out_path)
print(f"Trained models for attrs: {ATTR_TYPES}")
print(f"Saved artifact to {out_path}")
return artifact
if __name__ == "__main__":
train_all()