Download food_classifier.py from emiloetting/FoodCLS: direct link, hf CLI and curl.
- Browser
- Download file 5.35 kB
-
https://huggingface.co/emiloetting/FoodCLS/resolve/main/food_classifier.py
- Command line
-
hf download hf://emiloetting/FoodCLS/food_classifier.py
-
curl -L -o food_classifier.py https://huggingface.co/emiloetting/FoodCLS/resolve/main/food_classifier.py
5.35 kB
| import ast | |
| import re | |
| from collections.abc import Iterable, Mapping | |
| import torch | |
| import torch.nn as nn | |
| from huggingface_hub import PyTorchModelHubMixin | |
| class DataProcessor: | |
| def __init__(self): | |
| self.vocab = None | |
| def _check_readiness(self) -> bool: | |
| if self.vocab is None: | |
| raise ValueError('Missing vocab. Ensure loading respective vocab.json.') | |
| def _parse_tags(tags) -> list: | |
| """Read tag lists, including list strings loaded from CSV.""" | |
| if isinstance(tags, Mapping): | |
| return [] | |
| if isinstance(tags, str): | |
| control_escapes = {code: f'\\x{code:02x}' for code in range(32)} | |
| values = [] | |
| for match in re.finditer('\'(?:\\\\.|[^\'\\\\])*\'|"(?:\\\\.|[^"\\\\])*"', tags): | |
| try: | |
| values.append(ast.literal_eval(match.group(0).translate(control_escapes))) | |
| except (SyntaxError, ValueError): | |
| continue | |
| elif isinstance(tags, Iterable): | |
| values = tags | |
| else: | |
| return [] | |
| return values | |
| def preprocess_tags(tags) -> list[str]: | |
| """Normalize tags using the vocabulary extraction rules.""" | |
| cleaned = [] | |
| for value in DataProcessor._parse_tags(tags): | |
| if not isinstance(value, str): | |
| continue | |
| tag = value.lower() | |
| for old, new in ((' - ', ''), ('-', ''), ('.', ''), ('|', ''), ('ä', 'a'), ('ö', 'o'), ('ü', 'u')): | |
| tag = tag.replace(old, new) | |
| if tag: | |
| cleaned.append(tag) | |
| return cleaned | |
| def normalize_tags(self, tags) -> list[str]: | |
| """Preprocess raw or normalized tags and keep only known tags.""" | |
| self._check_readiness() | |
| return [tag for tag in self.preprocess_tags(tags) if tag in self.known_tags_dict] | |
| def vectorize_tags(self, tags) -> torch.Tensor: | |
| """Creates onehot-encoded vector of used tags for classification. | |
| Ignores unknown tags. | |
| Returns vector full of zeros if no valid tags were found or tags to normalize were [] or `None`""" | |
| self._check_readiness() | |
| base = torch.zeros(self.nr_of_known_tags, dtype=torch.float32) | |
| for p_tag in self.normalize_tags(tags): | |
| base[int(self.known_tags_dict[p_tag])] = 1 | |
| return base | |
| def process_api(self, response_dict: dict) -> torch.Tensor: | |
| """Vectorize an OFF product dictionary; missing tags produce a zero vector.""" | |
| self._check_readiness() | |
| return self.vectorize_tags(response_dict.get('categories_tags')) | |
| class MLP(nn.Module, PyTorchModelHubMixin): | |
| def __init__(self, input_features:int): | |
| super().__init__() | |
| self.class_mapping = { | |
| 0: "Obst & Gemüse", | |
| 1: "Fleisch, Fisch & Alternativen", | |
| 2: "Milchprodukte & Eier", | |
| 3: "Brot & Backwaren", | |
| 4: "Nudeln, Reis & Getreide", | |
| 5: "Fertigprodukte", | |
| 6: "Backzutaten", | |
| 7: "Getränke", | |
| 8: "Snacks & Süßigkeiten", | |
| 9: "Öle, Gewürze & Saucen", | |
| 10: "Brotaufstriche", | |
| 11: "Sonstiges" | |
| } | |
| self.input_layer = nn.Linear(in_features=input_features, out_features=2000) # classes 0-11 | |
| self.hidden1 = nn.Linear(self.input_layer.out_features, 1000) | |
| self.hidden2 = nn.Linear(self.hidden1.out_features, 100) | |
| self.hidden3 = nn.Linear(self.hidden2.out_features, 12) | |
| self.softmax = nn.Softmax(1) | |
| self.leaky_r = nn.LeakyReLU(inplace=True) | |
| self.dropout1 = nn.Dropout(p=.2) | |
| self.dropout2 = nn.Dropout(p=.2) | |
| self.forward_seq = nn.Sequential( | |
| self.input_layer, | |
| self.leaky_r, | |
| self.hidden1, | |
| self.dropout1, | |
| self.leaky_r, | |
| self.hidden2, | |
| self.dropout2, | |
| self.leaky_r, | |
| self.hidden3 | |
| ) | |
| def forward(self, X:torch.Tensor) -> torch.Tensor: | |
| """Performs forward pass.""" | |
| return self.forward_seq(X) | |
| def predict(self, X, string_classes:bool=True) -> torch.Tensor: | |
| if X.ndim == 1: | |
| X = X.unsqueeze(0) | |
| preds = self.forward_seq(X) | |
| res = int(torch.argmax(preds, dim=1).cpu()) | |
| return self.class_mapping[res] if string_classes else res | |
| class FoodClassifier(MLP): | |
| """MLP with preprocessing and the standard Hugging Face Hub mixin.""" | |
| def __init__(self, vocab: list[str]): | |
| super().__init__(input_features=len(vocab)) | |
| # The mixin saves the ordered vocabulary in config.json. | |
| self.processor = DataProcessor() | |
| self.processor.vocab = list(vocab) | |
| self.processor.known_tags_dict = {tag: idx for idx, tag in enumerate(vocab)} | |
| self.processor.nr_of_known_tags = len(vocab) | |
| def predict(self, product: dict) -> str: | |
| """Return the category name for a product containing categories_tags.""" | |
| vector = self.processor.process_api(product).to(next(self.parameters()).device) | |
| return super().predict(vector, string_classes=True) | |