ErzhanAb commited on
Commit
90f8bf4
·
verified ·
1 Parent(s): 251d9fe

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +101 -45
app.py CHANGED
@@ -4,37 +4,60 @@ from html import unescape
4
  import gradio as gr
5
  import numpy as np
6
 
7
- # ====== TF-IDF + LR (joblib / sklearn) ======
8
- PIPE = None
9
- try:
 
 
10
  import joblib
11
- PIPE = joblib.load("model.joblib") # сохранённый пайплайн TF-IDF+LR
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  except Exception as e:
13
- PIPE = None
14
- print(f"[WARN] Не удалось загрузить model.joblib: {e}")
15
 
16
- # ====== Transformer (ruBERT-tiny2) ======
 
 
17
  TRANSFORMER = {"model": None, "tokenizer": None, "device": "cpu"}
18
  try:
19
  import torch
20
  from transformers import AutoTokenizer, AutoModelForSequenceClassification
21
 
22
- MODEL_DIR = "." # в корне лежат config.json, model.safetensors, tokenizer.*
23
  device = "cuda" if torch.cuda.is_available() else "cpu"
24
 
25
  tokenizer = AutoTokenizer.from_pretrained(MODEL_DIR, local_files_only=True)
26
  model = AutoModelForSequenceClassification.from_pretrained(MODEL_DIR, local_files_only=True)
27
  model.to(device).eval()
28
 
29
- TRANSFORMER["model"] = model
30
- TRANSFORMER["tokenizer"] = tokenizer
31
- TRANSFORMER["device"] = device
32
  except Exception as e:
33
  print(f"[WARN] Не удалось загрузить ruBERT: {e}")
34
 
35
- # ====== Порог по умолчанию ======
36
- DEFAULT_THRESHOLD = 0.70 # как просили
37
- # если есть inference_config.json от обучения трансформера — подхватим рекомендованный
 
38
  try:
39
  if os.path.exists("inference_config.json"):
40
  with open("inference_config.json", "r", encoding="utf-8") as f:
@@ -43,7 +66,9 @@ try:
43
  except Exception:
44
  pass
45
 
46
- # ====== Предобработка для трансформера (как в обучении) ======
 
 
47
  from nltk.stem.snowball import RussianStemmer
48
  stemmer = RussianStemmer(ignore_stopwords=False)
49
 
@@ -69,29 +94,52 @@ def clean_and_stem(s: str) -> str:
69
  out.append(t if t in {"url", "tag", "num"} else stemmer.stem(t))
70
  return " ".join(out)
71
 
72
- # ====== Инференс ======
73
- def infer_tfidf(text: str) -> float:
74
- """Вернёт P(toxic) из TF-IDF+LR. В пайплайне уже есть свой preprocessor."""
 
 
 
 
 
75
  if PIPE is None:
76
- return 0.0
77
- proba = PIPE.predict_proba([text])[0, 1]
78
- return float(proba)
79
-
80
- def infer_transformer(text: str) -> float:
81
- """Вернёт P(toxic) из ruBERT-tiny2 (локальный чекпойнт)."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  if TRANSFORMER["model"] is None:
83
- return 0.0
84
  import torch
85
-
86
- text = clean_and_stem(text)
87
- if not text:
88
- return 0.0
89
- tok = TRANSFORMER["tokenizer"](text, return_tensors="pt", truncation=True, max_length=256)
90
  tok = {k: v.to(TRANSFORMER["device"]) for k, v in tok.items()}
91
- with torch.inference_mode():
92
- logits = TRANSFORMER["model"](**tok).logits
93
- p = torch.softmax(logits, dim=1)[0, 1].detach().cpu().item()
94
- return float(p)
 
 
 
95
 
96
  def predict(model_name: str, comment: str, threshold: float):
97
  comment = (comment or "").strip()
@@ -99,10 +147,16 @@ def predict(model_name: str, comment: str, threshold: float):
99
  return {"Токсичный": 0.0, "Не токсичный": 1.0}, "—"
100
 
101
  if model_name == "ruBERT-tiny2 (трансформер)":
102
- p_toxic = infer_transformer(comment)
103
- else: # TF-IDF + Логистическая регрессия
104
- p_toxic = infer_tfidf(comment)
 
 
 
 
 
105
 
 
106
  pred = "Токсичный" if p_toxic >= threshold else "Не токсичный"
107
  dist = {"Токсичный": p_toxic, "Не токсичный": 1 - p_toxic}
108
  expl = (
@@ -116,7 +170,9 @@ def predict(model_name: str, comment: str, threshold: float):
116
  def clear_all():
117
  return "ruBERT-tiny2 (трансформер)", "", DEFAULT_THRESHOLD, {"Токсичный": 0.0, "Не токсичный": 1.0}, "—"
118
 
119
- # ====== UI ======
 
 
120
  TITLE = "Анализатор токсичности (две модели)"
121
  DESCRIPTION = "Выберите модель, задайте порог (по умолчанию 0.70) и введите комментарий."
122
 
@@ -127,20 +183,19 @@ CUSTOM_CSS = """
127
 
128
  ABOUT_MD = """
129
  ### Параметры и описание моделей
130
-
131
  **1) ruBERT-tiny2 (трансформер)**
132
  - База: `cointegrated/rubert-tiny2` (BERT-tiny для русского).
133
  - Токенизация: BERT WordPiece.
134
  - Предобработка: удаление пунктуации, нормализация спец-токенов (`url`, `tag`, `num`), стемминг Snowball.
135
  - Обучение: 10 эпох с early stopping (по macro-F1), class weights (balanced).
136
- - Рекомендованный порог по валидации: ~**0.70**.
137
 
138
  **2) TF-IDF + Логистическая регрессия**
139
- - Векторизация: `TfidfVectorizer(analyzer="char_wb", ngram_range=(4,5), max_features=200k, min_df≈1.75e-4, max_df≈0.96)`.
140
- - Классификатор: `LogisticRegression(penalty="l1", solver="liblinear", C≈5.52, class_weight="balanced", max_iter=5000, tol≈2.4e-4)`.
141
- - Рекомендованный порог (по ранее полученным метрикам): ~**0.40**.
142
 
143
- **Порог** можно свободно менять слайдером — выберите баланс precision/recall под задачу.
144
  """
145
 
146
  with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"), css=CUSTOM_CSS) as demo:
@@ -155,7 +210,8 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"), cs
155
  label="Модель"
156
  )
157
  comment_input = gr.Textbox(label="Текст комментария", lines=6, placeholder="Напишите что-нибудь…")
158
- thr = gr.Slider(label="Порог классификации", minimum=0.0, maximum=1.0, value=DEFAULT_THRESHOLD, step=0.01)
 
159
  with gr.Row():
160
  analyze_btn = gr.Button("Анализ", variant="primary")
161
  clear_btn = gr.Button("Очистить", variant="secondary")
 
4
  import gradio as gr
5
  import numpy as np
6
 
7
+ # ==============================
8
+ # TF-IDF + LR (joblib / sklearn)
9
+ # ==============================
10
+ PIPE, PIPE_PATH = None, None
11
+ def _load_tfidf():
12
  import joblib
13
+ candidates = [
14
+ "model.joblib",
15
+ "artifacts/model.joblib",
16
+ "tfidf/model.joblib",
17
+ ]
18
+ last_err = None
19
+ for p in candidates:
20
+ if os.path.exists(p):
21
+ try:
22
+ pipe = joblib.load(p)
23
+ return pipe, p
24
+ except Exception as e:
25
+ last_err = e
26
+ if last_err:
27
+ print(f"[WARN] TF-IDF load failed: {last_err}")
28
+ else:
29
+ print("[WARN] TF-IDF model not found in", candidates)
30
+ return None, None
31
+
32
+ try:
33
+ PIPE, PIPE_PATH = _load_tfidf()
34
  except Exception as e:
35
+ print(f"[WARN] Не удалось инициализировать TF-IDF: {e}")
36
+ PIPE, PIPE_PATH = None, None
37
 
38
+ # ==============================
39
+ # Transformer (ruBERT-tiny2)
40
+ # ==============================
41
  TRANSFORMER = {"model": None, "tokenizer": None, "device": "cpu"}
42
  try:
43
  import torch
44
  from transformers import AutoTokenizer, AutoModelForSequenceClassification
45
 
46
+ MODEL_DIR = "." # файлы трансформера лежат в корне Space
47
  device = "cuda" if torch.cuda.is_available() else "cpu"
48
 
49
  tokenizer = AutoTokenizer.from_pretrained(MODEL_DIR, local_files_only=True)
50
  model = AutoModelForSequenceClassification.from_pretrained(MODEL_DIR, local_files_only=True)
51
  model.to(device).eval()
52
 
53
+ TRANSFORMER.update({"model": model, "tokenizer": tokenizer, "device": device})
 
 
54
  except Exception as e:
55
  print(f"[WARN] Не удалось загрузить ruBERT: {e}")
56
 
57
+ # ==============================
58
+ # Порог по умолчанию
59
+ # ==============================
60
+ DEFAULT_THRESHOLD = 0.70
61
  try:
62
  if os.path.exists("inference_config.json"):
63
  with open("inference_config.json", "r", encoding="utf-8") as f:
 
66
  except Exception:
67
  pass
68
 
69
+ # ==============================
70
+ # Предобработка (как при обучении трансформера)
71
+ # ==============================
72
  from nltk.stem.snowball import RussianStemmer
73
  stemmer = RussianStemmer(ignore_stopwords=False)
74
 
 
94
  out.append(t if t in {"url", "tag", "num"} else stemmer.stem(t))
95
  return " ".join(out)
96
 
97
+ # ==============================
98
+ # Инференс
99
+ # ==============================
100
+ def infer_tfidf(text: str):
101
+ """
102
+ Вернёт (proba, err_msg). Если всё ок: (float in [0,1], None).
103
+ Если модели нет/ошибка: (None, 'сообщение').
104
+ """
105
  if PIPE is None:
106
+ return None, f"TF-IDF модель не загружена (ожидалась в {PIPE_PATH or 'model.joblib / artifacts/model.joblib'})."
107
+ try:
108
+ # предпочтительно predict_proba
109
+ if hasattr(PIPE, "predict_proba"):
110
+ proba = PIPE.predict_proba([text])[0, 1]
111
+ else:
112
+ # fallback: decision_function -> сигмоида
113
+ if hasattr(PIPE, "decision_function"):
114
+ z = PIPE.decision_function([text])[0]
115
+ proba = 1.0 / (1.0 + np.exp(-z))
116
+ else:
117
+ return None, "У модели нет predict_proba/decision_function."
118
+ # страховка от числ. артефактов
119
+ proba = float(np.clip(proba, 0.0, 1.0))
120
+ return proba, None
121
+ except Exception as e:
122
+ return None, f"Ошибка инференса TF-IDF: {e}"
123
+
124
+ def infer_transformer(text: str):
125
+ """
126
+ Вернёт (proba, err_msg) аналогично TF-IDF.
127
+ """
128
  if TRANSFORMER["model"] is None:
129
+ return None, "Модель ruBERT не загружена."
130
  import torch
131
+ text_prep = clean_and_stem(text)
132
+ if not text_prep:
133
+ return 0.0, None
134
+ tok = TRANSFORMER["tokenizer"](text_prep, return_tensors="pt", truncation=True, max_length=256)
 
135
  tok = {k: v.to(TRANSFORMER["device"]) for k, v in tok.items()}
136
+ try:
137
+ with torch.inference_mode():
138
+ logits = TRANSFORMER["model"](**tok).logits
139
+ proba = torch.softmax(logits, dim=1)[0, 1].detach().cpu().item()
140
+ return float(proba), None
141
+ except Exception as e:
142
+ return None, f"Ошибка инференса ruBERT: {e}"
143
 
144
  def predict(model_name: str, comment: str, threshold: float):
145
  comment = (comment or "").strip()
 
147
  return {"Токсичный": 0.0, "Не токсичный": 1.0}, "—"
148
 
149
  if model_name == "ruBERT-tiny2 (трансформер)":
150
+ p_toxic, err = infer_transformer(comment)
151
+ else:
152
+ p_toxic, err = infer_tfidf(comment)
153
+
154
+ if err is not None or p_toxic is None:
155
+ dist = {"Токсичный": 0.0, "Не токсичный": 1.0}
156
+ expl = f"Модель: **{model_name}**\n\n⚠️ {err}"
157
+ return dist, expl
158
 
159
+ # ВЕРДИКТ ТОЛЬКО ПО ЗАДАННОМУ ПОРОГУ:
160
  pred = "Токсичный" if p_toxic >= threshold else "Не токсичный"
161
  dist = {"Токсичный": p_toxic, "Не токсичный": 1 - p_toxic}
162
  expl = (
 
170
  def clear_all():
171
  return "ruBERT-tiny2 (трансформер)", "", DEFAULT_THRESHOLD, {"Токсичный": 0.0, "Не токсичный": 1.0}, "—"
172
 
173
+ # ==============================
174
+ # UI
175
+ # ==============================
176
  TITLE = "Анализатор токсичности (две модели)"
177
  DESCRIPTION = "Выберите модель, задайте порог (по умолчанию 0.70) и введите комментарий."
178
 
 
183
 
184
  ABOUT_MD = """
185
  ### Параметры и описание моделей
 
186
  **1) ruBERT-tiny2 (трансформер)**
187
  - База: `cointegrated/rubert-tiny2` (BERT-tiny для русского).
188
  - Токенизация: BERT WordPiece.
189
  - Предобработка: удаление пунктуации, нормализация спец-токенов (`url`, `tag`, `num`), стемминг Snowball.
190
  - Обучение: 10 эпох с early stopping (по macro-F1), class weights (balanced).
191
+ - Рекомендованный порог: ~**0.70**.
192
 
193
  **2) TF-IDF + Логистическая регрессия**
194
+ - `TfidfVectorizer(analyzer="char_wb", ngram_range=(4,5), max_features200k, min_df≈1.75e-4, max_df≈0.96)`.
195
+ - `LogisticRegression(penalty="l1", solver="liblinear", C≈5.52, class_weight="balanced", max_iter=5000, tol≈2.4e-4)`.
196
+ - Рекомендованный порог: ~**0.40**.
197
 
198
+ > Порог можно свободно менять слайдером — подбирайте нужный баланс precision/recall.
199
  """
200
 
201
  with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"), css=CUSTOM_CSS) as demo:
 
210
  label="Модель"
211
  )
212
  comment_input = gr.Textbox(label="Текст комментария", lines=6, placeholder="Напишите что-нибудь…")
213
+ thr = gr.Slider(label="Порог классификации", minimum=0.0, maximum=1.0,
214
+ value=DEFAULT_THRESHOLD, step=0.01)
215
  with gr.Row():
216
  analyze_btn = gr.Button("Анализ", variant="primary")
217
  clear_btn = gr.Button("Очистить", variant="secondary")