emiloetting commited on
Commit
83d7647
·
verified ·
1 Parent(s): cd899da

Model Upload

Browse files
Files changed (5) hide show
  1. config.json +0 -0
  2. food_classifier.py +145 -0
  3. model.safetensors +3 -0
  4. processor.pkl +3 -0
  5. requirements.txt +3 -0
config.json ADDED
The diff for this file is too large to render. See raw diff
 
food_classifier.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ast
2
+ import re
3
+ from collections.abc import Iterable, Mapping
4
+
5
+ import torch
6
+ import torch.nn as nn
7
+ from huggingface_hub import PyTorchModelHubMixin
8
+
9
+
10
+ class DataProcessor:
11
+
12
+ def __init__(self):
13
+ self.vocab = None
14
+
15
+ def _check_readiness(self) -> bool:
16
+ if self.vocab is None:
17
+ raise ValueError('Missing vocab. Ensure loading respective vocab.json.')
18
+
19
+ @staticmethod
20
+ def _parse_tags(tags) -> list:
21
+ """Read tag lists, including list strings loaded from CSV."""
22
+ if isinstance(tags, Mapping):
23
+ return []
24
+ if isinstance(tags, str):
25
+ control_escapes = {code: f'\\x{code:02x}' for code in range(32)}
26
+ values = []
27
+ for match in re.finditer('\'(?:\\\\.|[^\'\\\\])*\'|"(?:\\\\.|[^"\\\\])*"', tags):
28
+ try:
29
+ values.append(ast.literal_eval(match.group(0).translate(control_escapes)))
30
+ except (SyntaxError, ValueError):
31
+ continue
32
+ elif isinstance(tags, Iterable):
33
+ values = tags
34
+ else:
35
+ return []
36
+ return values
37
+
38
+ @staticmethod
39
+ def preprocess_tags(tags) -> list[str]:
40
+ """Normalize tags using the vocabulary extraction rules."""
41
+ cleaned = []
42
+ for value in DataProcessor._parse_tags(tags):
43
+ if not isinstance(value, str):
44
+ continue
45
+ tag = value.lower()
46
+ for old, new in ((' - ', ''), ('-', ''), ('.', ''), ('|', ''), ('ä', 'a'), ('ö', 'o'), ('ü', 'u')):
47
+ tag = tag.replace(old, new)
48
+ if tag:
49
+ cleaned.append(tag)
50
+ return cleaned
51
+
52
+ def normalize_tags(self, tags) -> list[str]:
53
+ """Preprocess raw or normalized tags and keep only known tags."""
54
+ self._check_readiness()
55
+ return [tag for tag in self.preprocess_tags(tags) if tag in self.known_tags_dict]
56
+
57
+ def vectorize_tags(self, tags) -> torch.Tensor:
58
+ """Creates onehot-encoded vector of used tags for classification.
59
+ Ignores unknown tags.
60
+
61
+ Returns vector full of zeros if no valid tags were found or tags to normalize were [] or `None`"""
62
+ self._check_readiness()
63
+ base = torch.zeros(self.nr_of_known_tags, dtype=torch.float32)
64
+ for p_tag in self.normalize_tags(tags):
65
+ base[int(self.known_tags_dict[p_tag])] = 1
66
+ return base
67
+
68
+ def process_api(self, response_dict: dict) -> torch.Tensor:
69
+ """Vectorize an OFF product dictionary; missing tags produce a zero vector."""
70
+ self._check_readiness()
71
+ return self.vectorize_tags(response_dict.get('categories_tags'))
72
+
73
+
74
+ class MLP(nn.Module, PyTorchModelHubMixin):
75
+ def __init__(self, input_features:int):
76
+ super().__init__()
77
+
78
+ self.class_mapping = {
79
+ 0: "Obst & Gemüse",
80
+ 1: "Fleisch, Fisch & Alternativen",
81
+ 2: "Milchprodukte & Eier",
82
+ 3: "Brot & Backwaren",
83
+ 4: "Nudeln, Reis & Getreide",
84
+ 5: "Fertigprodukte",
85
+ 6: "Backzutaten",
86
+ 7: "Getränke",
87
+ 8: "Snacks & Süßigkeiten",
88
+ 9: "Öle, Gewürze & Saucen",
89
+ 10: "Brotaufstriche",
90
+ 11: "Sonstiges"
91
+ }
92
+
93
+ self.input_layer = nn.Linear(in_features=input_features, out_features=2000) # classes 0-11
94
+ self.hidden1 = nn.Linear(self.input_layer.out_features, 1000)
95
+ self.hidden2 = nn.Linear(self.hidden1.out_features, 100)
96
+ self.hidden3 = nn.Linear(self.hidden2.out_features, 12)
97
+
98
+ self.softmax = nn.Softmax(1)
99
+ self.leaky_r = nn.LeakyReLU(inplace=True)
100
+ self.dropout1 = nn.Dropout(p=.2)
101
+ self.dropout2 = nn.Dropout(p=.2)
102
+
103
+ self.forward_seq = nn.Sequential(
104
+ self.input_layer,
105
+ self.leaky_r,
106
+
107
+ self.hidden1,
108
+ self.dropout1,
109
+ self.leaky_r,
110
+
111
+ self.hidden2,
112
+ self.dropout2,
113
+ self.leaky_r,
114
+
115
+ self.hidden3
116
+ )
117
+
118
+ def forward(self, X:torch.Tensor) -> torch.Tensor:
119
+ """Performs forward pass."""
120
+ return self.forward_seq(X)
121
+
122
+ def predict(self, X, string_classes:bool=True) -> torch.Tensor:
123
+ if X.ndim == 1:
124
+ X = X.unsqueeze(0)
125
+ preds = self.forward_seq(X)
126
+ res = int(torch.argmax(preds, dim=1).cpu())
127
+ return self.class_mapping[res] if string_classes else res
128
+
129
+
130
+ class FoodClassifier(MLP):
131
+ """MLP with preprocessing and the standard Hugging Face Hub mixin."""
132
+
133
+ def __init__(self, vocab: list[str]):
134
+ super().__init__(input_features=len(vocab))
135
+ # The mixin saves the ordered vocabulary in config.json.
136
+ self.processor = DataProcessor()
137
+ self.processor.vocab = list(vocab)
138
+ self.processor.known_tags_dict = {tag: idx for idx, tag in enumerate(vocab)}
139
+ self.processor.nr_of_known_tags = len(vocab)
140
+
141
+ @torch.inference_mode()
142
+ def predict(self, product: dict) -> str:
143
+ """Return the category name for a product containing categories_tags."""
144
+ vector = self.processor.process_api(product).to(next(self.parameters()).device)
145
+ return super().predict(vector, string_classes=True)
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e8521261dcf0a7ce48d9bdb633990b237c1cb2453bb4c9fd53289925e7e414bb
3
+ size 153666304
processor.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:64cd27462ff42ae0aaba6cc26399e4ac09dc611763e86322874262e9082eacf0
3
+ size 547363
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ torch>=2.15.0
2
+ huggingface_hub>=2.4.0
3
+ safetensors>=0.8.0