Spaces:
Running on CPU Upgrade

ziem-io commited on
Commit
bb3d05e
·
1 Parent(s): 394c053

Scaffold: Include custom libs

Browse files
Files changed (3) hide show
  1. app.py +11 -1
  2. lib/bert_regressor.py +113 -0
  3. lib/bert_regressor_utils.py +231 -0
app.py CHANGED
@@ -1,7 +1,17 @@
1
  import gradio as gr
2
  import html
3
 
4
- def predict(review: str, display_mode: str):
 
 
 
 
 
 
 
 
 
 
5
  review = (review or "").strip()
6
  if not review:
7
  # immer zwei Outputs zurückgeben
 
1
  import gradio as gr
2
  import html
3
 
4
+ # Projektspezifische Module
5
+ from lib.bert_regressor import BertMultiHeadRegressor
6
+ from lib.bert_regressor_utils import (
7
+ load_model_and_tokenizer,
8
+ predict_flavours,
9
+ #predict_is_review,
10
+ TARGET_COLUMNS,
11
+ ICONS
12
+ )
13
+
14
+ def predict(review: str, mode: str):
15
  review = (review or "").strip()
16
  if not review:
17
  # immer zwei Outputs zurückgeben
lib/bert_regressor.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ from transformers import AutoModel
4
+
5
+ ###################################################################################
6
+
7
+ # Erweiterte Regressorklasse: Ein gemeinsamer Encoder, aber mehrere unabhängige Köpfe
8
+ class BertMultiHeadRegressor(nn.Module):
9
+ """
10
+ Mehrkopf-Regression auf einem beliebigen HF-Encoder (BERT/RoBERTa/DeBERTa/ModernBERT).
11
+ - Gemeinsamer Encoder
12
+ - n unabhängige Regressionsköpfe (je 1 Wert)
13
+ - Robustes Pooling (Pooler wenn vorhanden, sonst maskiertes Mean)
14
+ - Partielles Unfreezen ab `unfreeze_from`
15
+ """
16
+ def __init__(self, pretrained_model_name: str,
17
+ n_heads: int = 8,
18
+ unfreeze_from: int = 8,
19
+ dropout: float = 0.1):
20
+ super().__init__()
21
+
22
+ # Beliebigen Encoder laden
23
+ self.encoder = AutoModel.from_pretrained(pretrained_model_name)
24
+ hidden_size = self.encoder.config.hidden_size
25
+
26
+ # Erst alles einfrieren …
27
+ for p in self.encoder.parameters():
28
+ p.requires_grad = False
29
+
30
+ # … dann Layer ab `unfreeze_from` freigeben (falls vorhanden)
31
+ # Die meisten Encoder haben `.encoder.layer`
32
+ encoder_block = getattr(self.encoder, "encoder", None)
33
+ layers = getattr(encoder_block, "layer", None)
34
+ if layers is not None:
35
+ for layer in layers[unfreeze_from:]:
36
+ for p in layer.parameters():
37
+ p.requires_grad = True
38
+ else:
39
+ # Fallback: wenn kein klassisches Lagen-Array existiert, nichts tun
40
+ pass
41
+
42
+ self.dropout = nn.Dropout(dropout)
43
+ self.heads = nn.ModuleList([nn.Linear(hidden_size, 1) for _ in range(n_heads)])
44
+
45
+ def _pool(self, outputs, attention_mask):
46
+ """
47
+ Robustes Pooling:
48
+ - Wenn pooler_output vorhanden: nutzen (BERT/RoBERTa)
49
+ - Sonst: maskiertes Mean-Pooling über last_hidden_state (z. B. DeBERTaV3)
50
+ """
51
+ pooler = getattr(outputs, "pooler_output", None)
52
+ if pooler is not None:
53
+ return pooler # [B, H]
54
+
55
+ last_hidden = outputs.last_hidden_state # [B, T, H]
56
+ mask = attention_mask.unsqueeze(-1).float() # [B, T, 1]
57
+ summed = (last_hidden * mask).sum(dim=1) # [B, H]
58
+ denom = mask.sum(dim=1).clamp(min=1e-6) # [B, 1]
59
+ return summed / denom
60
+
61
+ def forward(self, input_ids, attention_mask, token_type_ids=None):
62
+ outputs = self.encoder(
63
+ input_ids=input_ids,
64
+ attention_mask=attention_mask,
65
+ token_type_ids=token_type_ids if token_type_ids is not None else None,
66
+ return_dict=True
67
+ )
68
+ pooled = self._pool(outputs, attention_mask) # [B, H]
69
+ pooled = self.dropout(pooled)
70
+ preds = [head(pooled) for head in self.heads] # n × [B, 1]
71
+ return torch.cat(preds, dim=1) # [B, n_heads]
72
+
73
+ ###################################################################################
74
+
75
+ class BertBinaryClassifier(nn.Module):
76
+ def __init__(self, pretrained_model_name='bert-base-uncased', unfreeze_from=8, dropout=0.3):
77
+ super(BertBinaryClassifier, self).__init__()
78
+
79
+ # BERT-Encoder laden
80
+ self.bert = BertModel.from_pretrained(pretrained_model_name)
81
+
82
+ # Alle Layer zunächst einfrieren
83
+ for param in self.bert.parameters():
84
+ param.requires_grad = False
85
+
86
+ # Höhere Layer freigeben → feineres Fine-Tuning ab `unfreeze_from`
87
+ for layer in self.bert.encoder.layer[unfreeze_from:]:
88
+ for param in layer.parameters():
89
+ param.requires_grad = True
90
+
91
+ # Dropout-Schicht zur Regularisierung
92
+ self.dropout = nn.Dropout(dropout)
93
+
94
+ # Klassifikationskopf: Wandelt das 768-dimensionale BERT-Embedding
95
+ # in einen einzelnen logit-Wert um (für binäre Klassifikation).
96
+ self.classifier = nn.Linear(self.bert.config.hidden_size, 1)
97
+
98
+ def forward(self, input_ids, attention_mask):
99
+ # Eingabe durch BERT verarbeiten → [batch_size, 768]
100
+ outputs = self.bert(input_ids=input_ids, attention_mask=attention_mask)
101
+
102
+ # CLS-Token-Repräsentation extrahieren
103
+ pooled_output = outputs.pooler_output
104
+
105
+ # Dropout anwenden zur Regularisierung
106
+ dropped = self.dropout(pooled_output)
107
+
108
+ # Logits durch linearen Klassifikator erzeugen
109
+ logits = self.classifier(dropped)
110
+
111
+ # Rückgabe der rohen Logits
112
+ return logits
113
+
lib/bert_regressor_utils.py ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from transformers import AutoTokenizer
3
+ from torch.utils.data import Dataset
4
+ import numpy as np
5
+
6
+ from .bert_regressor import BertMultiHeadRegressor, BertBinaryClassifier
7
+
8
+ ###################################################################################
9
+
10
+ # Konstante Liste der acht Aromen-Kategorien für Whisky-Tasting-Notes.
11
+ # Diese wird von Modellen und Evaluierungsfunktionen verwendet.
12
+ TARGET_COLUMNS = [
13
+ "grainy",
14
+ "grassy",
15
+ "fragrant",
16
+ "fruity",
17
+ "peated",
18
+ "woody",
19
+ "winey",
20
+ "off-notes"
21
+ ]
22
+
23
+ ###################################################################################
24
+
25
+ COLORS = {
26
+ "grainy": "#FFF3B0",
27
+ "grassy": "#C4F0C5",
28
+ "fragrant": "#F3C4FB",
29
+ "fruity": "#FFD6B0",
30
+ "peated": "#CFCFCF",
31
+ "woody": "#EAD6C7",
32
+ "winey": "#F7B7A3",
33
+ "off-notes": "#D6E4F0",
34
+ "quantifiers": "#ff8083"
35
+ }
36
+
37
+ ICONS = {
38
+ "grainy": "🌾",
39
+ "grassy": "🌿",
40
+ "fragrant": "🌸",
41
+ "fruity": "🍋",
42
+ "peated": "🔥",
43
+ "woody": "🌲",
44
+ "winey": "🍷",
45
+ "off-notes": "☠️"
46
+ }
47
+
48
+ ###################################################################################
49
+
50
+ class WhiskyDataset(Dataset):
51
+ def __init__(self, texts, targets, tokenizer, max_len):
52
+ self.texts = texts
53
+ self.targets = targets
54
+ self.tokenizer = tokenizer
55
+ self.max_len = max_len
56
+
57
+ def __len__(self):
58
+ return len(self.texts)
59
+
60
+ def __getitem__(self, item):
61
+ text = str(self.texts[item])
62
+ target = self.targets[item]
63
+
64
+ # Einheitliche Tokenisierung über Hilfsfunktion
65
+ encoding = tokenize_input(text, self.tokenizer)
66
+
67
+ return {
68
+ 'input_ids': encoding['input_ids'].squeeze(),
69
+ 'attention_mask': encoding['attention_mask'].squeeze(),
70
+ 'targets': torch.tensor(target, dtype=torch.float)
71
+ }
72
+
73
+ ###################################################################################
74
+
75
+ def get_device(prefer_mps=True, verbose=True):
76
+ """
77
+ Gibt das beste verfügbare Torch-Device zurück (MPS, CUDA oder CPU).
78
+
79
+ Args:
80
+ prefer_mps (bool): Ob bei Apple-Geräten 'mps' (Metal Performance Shaders) bevorzugt werden soll.
81
+ verbose (bool): Ob das erkannte Device ausgegeben werden soll.
82
+
83
+ Returns:
84
+ torch.device: Das beste verfügbare Gerät für das Training.
85
+ """
86
+ if prefer_mps and torch.backends.mps.is_available():
87
+ device = torch.device("mps")
88
+ name = "Apple GPU (MPS)"
89
+ elif torch.cuda.is_available():
90
+ device = torch.device("cuda")
91
+ name = torch.cuda.get_device_name(device)
92
+ else:
93
+ device = torch.device("cpu")
94
+ name = "CPU"
95
+
96
+ if verbose:
97
+ print(f"✅ Verwendetes Gerät: {name} ({device})")
98
+
99
+ return device
100
+
101
+ ###################################################################################
102
+
103
+ def tokenize_input(texts, tokenizer, max_len=256):
104
+
105
+ """
106
+ Einheitliche Tokenisierung für Training und Inferenz.
107
+
108
+ Args:
109
+ texts (str or List[str]): Eingabetext(e).
110
+ tokenizer (PreTrainedTokenizer): z. B. BertTokenizer.
111
+
112
+ Returns:
113
+ dict: Dictionary mit PyTorch-Tensoren (input_ids, attention_mask).
114
+ """
115
+ return tokenizer(
116
+ texts,
117
+ truncation=True,
118
+ padding='max_length',
119
+ max_length=max_len,
120
+ return_tensors='pt'
121
+ )
122
+
123
+ ###################################################################################
124
+
125
+ def load_model_and_tokenizer(model_name, model_path, model_type="multihead"):
126
+ """
127
+ Universelle Ladefunktion für BertMultiHeadRegressor oder BertBinaryClassifier.
128
+
129
+ Args:
130
+ model_name (str): Name des vortrainierten BERT-Modells (z. B. 'bert-base-uncased').
131
+ model_path (str): Pfad zur gespeicherten Modellzustandsdatei (.pt).
132
+ model_type (str): 'multihead' oder 'binary'. Default: 'multihead'.
133
+
134
+ Returns:
135
+ model (nn.Module): Geladenes Modell im Eval-Modus.
136
+ tokenizer (BertTokenizer): Passender Tokenizer.
137
+ device (torch.device): Verwendetes Rechengerät (CPU oder GPU).
138
+ """
139
+ # Gerät automatisch ermitteln (GPU/CPU)
140
+ device = get_device()
141
+
142
+ # Modellzustand und Konfiguration laden
143
+ checkpoint = torch.load(model_path, map_location=device)
144
+ config = checkpoint["model_config"]
145
+
146
+ # Modell je nach Typ initialisieren
147
+ if model_type == "multihead":
148
+ model = BertMultiHeadRegressor(
149
+ pretrained_model_name=config["pretrained_model_name"],
150
+ n_heads=config["n_heads"],
151
+ unfreeze_from=config["unfreeze_from"],
152
+ dropout=config["dropout"]
153
+ )
154
+ elif model_type == "binary":
155
+ model = BertBinaryClassifier(
156
+ pretrained_model_name=config["pretrained_model_name"],
157
+ unfreeze_from=config["unfreeze_from"],
158
+ dropout=config["dropout"]
159
+ )
160
+ else:
161
+ raise ValueError(f"Unbekannter model_type: {model_type}")
162
+
163
+ # Gewichtungen laden und Modell auf Gerät verschieben
164
+ model.to(device)
165
+ model.load_state_dict(checkpoint["model_state_dict"])
166
+ model.eval() # Wechselt in den Inferenzmodus
167
+
168
+ # Lädt den passenden Tokenizer
169
+ tokenizer = AutoTokenizer.from_pretrained(model_name)
170
+
171
+ return model, tokenizer, device
172
+
173
+ ###################################################################################
174
+
175
+ def predict_flavours(review_text, model, tokenizer, device, max_len=256):
176
+ # Modell in den Evaluierungsmodus setzen (kein Dropout etc.)
177
+ model.eval()
178
+
179
+ # Eingabetext tokenisieren und als Tensoren zurückgeben
180
+ encoding = tokenize_input(
181
+ review_text,
182
+ tokenizer
183
+ )
184
+
185
+ # Tokens auf das richtige Device verschieben
186
+ input_ids = encoding['input_ids'].to(device)
187
+ attention_mask = encoding['attention_mask'].to(device)
188
+
189
+ # Inferenz ohne Gradientenberechnung (Effizienz)
190
+ with torch.no_grad():
191
+ outputs = model(input_ids=input_ids, attention_mask=attention_mask) # shape: [1, 8]
192
+ prediction = outputs.cpu().numpy().flatten() # [8] – flach machen
193
+ prediction = np.clip(prediction, 0.0, 4.0)
194
+
195
+ # In ein Dictionary umwandeln (z. B. {"fruity": 2.1, "peated": 3.8, ...})
196
+ result = {
197
+ flavour: round(float(score), 2)
198
+ for flavour, score in zip(TARGET_COLUMNS, prediction)
199
+ }
200
+
201
+ return result
202
+
203
+ ###################################################################################
204
+
205
+ def predict_is_review(review_text, model, tokenizer, device, max_len=256, threshold=0.5):
206
+ # Modell in den Evaluierungsmodus setzen (kein Dropout etc.)
207
+ model.eval()
208
+
209
+ # Eingabetext tokenisieren und als Tensoren zurückgeben
210
+ encoding = tokenize_input(
211
+ review_text,
212
+ tokenizer
213
+ )
214
+
215
+ # Tokens auf das richtige Device verschieben
216
+ input_ids = encoding['input_ids'].to(device)
217
+ attention_mask = encoding['attention_mask'].to(device)
218
+
219
+ with torch.no_grad():
220
+ outputs = model(input_ids=input_ids, attention_mask=attention_mask)
221
+ print(outputs.cpu().numpy()) # <--- Zeigt die rohen Logits
222
+ probs = torch.sigmoid(outputs) # [1, 1]
223
+ prob = float(probs.squeeze().cpu().numpy()) # Skalar
224
+
225
+ return {
226
+ "is_review": prob >= threshold,
227
+ "probability": round(prob, 4)
228
+ }
229
+
230
+ ###################################################################################
231
+