ErzhanAb commited on
Commit
783893f
·
verified ·
1 Parent(s): 8f752fe

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +82 -88
app.py CHANGED
@@ -5,7 +5,7 @@ import gradio as gr
5
  import numpy as np
6
 
7
  # ==============================
8
- # 1. ПРЕПРОЦЕССОР ДЛЯ TF-IDF (ВАЖНО: ДОЛЖЕН БЫТЬ ОПРЕДЕЛЕН ПЕРЕД ЗАГРУЗКОЙ)
9
  # ==============================
10
  _URL_RE_TFIDF = re.compile(r'https?://\S+|www\.\S+')
11
  _TAG_RE_TFIDF = re.compile(r'[@#]\w+')
@@ -13,10 +13,7 @@ _NUM_RE_TFIDF = re.compile(r'\d+')
13
  _WS_RE_TFIDF = re.compile(r'\s+')
14
 
15
  def clean_text(s: str) -> str:
16
- """
17
- Эта функция нужна для корректной загрузки model.joblib.
18
- Она должна полностью совпадать с той, что использовалась при обучении.
19
- """
20
  if not isinstance(s, str):
21
  s = str(s)
22
  s = unescape(s).lower()
@@ -28,8 +25,9 @@ def clean_text(s: str) -> str:
28
  return s
29
 
30
  # ==============================
31
- # 2. TF-IDF + LR (joblib / sklearn)
32
  # ==============================
 
33
  PIPE, PIPE_PATH = None, "model.joblib"
34
  try:
35
  import joblib
@@ -41,27 +39,22 @@ except Exception as e:
41
  print(f"[WARN] Не удалось инициализировать TF-IDF: {e}")
42
  PIPE = None
43
 
44
- # ==============================
45
- # 3. Transformer (ruBERT-tiny2) - БЕЗ ИЗМЕНЕНИЙ
46
- # ==============================
47
  TRANSFORMER = {"model": None, "tokenizer": None, "device": "cpu"}
48
  try:
49
  import torch
50
  from transformers import AutoTokenizer, AutoModelForSequenceClassification
51
-
52
- MODEL_DIR = "." # файлы трансформера лежат в корне Space
53
  device = "cuda" if torch.cuda.is_available() else "cpu"
54
-
55
  tokenizer = AutoTokenizer.from_pretrained(MODEL_DIR, local_files_only=True)
56
  model = AutoModelForSequenceClassification.from_pretrained(MODEL_DIR, local_files_only=True)
57
  model.to(device).eval()
58
-
59
  TRANSFORMER.update({"model": model, "tokenizer": tokenizer, "device": device})
60
  except Exception as e:
61
  print(f"[WARN] Не удалось загрузить ruBERT: {e}")
62
 
63
  # ==============================
64
- # Порог по умолчанию - БЕЗ ИЗМЕНЕНИЙ
65
  # ==============================
66
  DEFAULT_THRESHOLD = 0.70
67
  try:
@@ -73,11 +66,10 @@ except Exception:
73
  pass
74
 
75
  # ==============================
76
- # Предобработка (как при обучении трансформера) - БЕЗ ИЗМЕНЕНИЙ
77
  # ==============================
78
  from nltk.stem.snowball import RussianStemmer
79
  stemmer = RussianStemmer(ignore_stopwords=False)
80
-
81
  _URL_RE = re.compile(r'https?://\S+|www\.\S+')
82
  _TAG_RE = re.compile(r'[@#]\w+')
83
  _NUM_RE = re.compile(r'\d+')
@@ -85,49 +77,31 @@ _PUNCT_RE = re.compile(r"[^\w\s]+", flags=re.UNICODE)
85
  _WS_RE = re.compile(r"\s+")
86
 
87
  def clean_and_stem(s: str) -> str:
88
- if not isinstance(s, str):
89
- s = str(s)
90
  s = unescape(s).lower()
91
- s = _URL_RE.sub(" url ", s)
92
- s = _TAG_RE.sub(" tag ", s)
93
- s = _NUM_RE.sub(" num ", s)
94
- s = _PUNCT_RE.sub(" ", s)
95
- s = _WS_RE.sub(" ", s).strip()
96
- if not s:
97
- return s
98
- out = []
99
- for t in s.split(" "):
100
- out.append(t if t in {"url", "tag", "num"} else stemmer.stem(t))
101
- return " ".join(out)
102
 
103
  # ==============================
104
- # Инференс - ИСПРАВЛЕНО СООБЩЕНИЕ ОБ ОШИБКЕ
105
  # ==============================
106
  def infer_tfidf(text: str):
107
  if PIPE is None:
108
- # Указываем точный путь к файлу в сообщении об ошибке
109
- return None, f"TF-IDF модель не загружена (ожидался файл '{PIPE_PATH}'). Проверьте логи запуска."
110
  try:
111
- if hasattr(PIPE, "predict_proba"):
112
- proba = PIPE.predict_proba([text])[0, 1]
113
- elif 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
- proba = float(np.clip(proba, 0.0, 1.0))
119
- return proba, None
120
  except Exception as e:
121
  return None, f"Ошибка инференса TF-IDF: {e}"
122
 
123
- # Остальная часть файла без изменений
124
  def infer_transformer(text: str):
125
  if TRANSFORMER["model"] is None:
126
  return None, "Модель ruBERT не загружена."
127
  import torch
128
  text_prep = clean_and_stem(text)
129
- if not text_prep:
130
- return 0.0, None
131
  tok = TRANSFORMER["tokenizer"](text_prep, return_tensors="pt", truncation=True, max_length=256)
132
  tok = {k: v.to(TRANSFORMER["device"]) for k, v in tok.items()}
133
  try:
@@ -138,12 +112,13 @@ def infer_transformer(text: str):
138
  except Exception as e:
139
  return None, f"Ошибка инференса ruBERT: {e}"
140
 
 
141
  def predict(model_name: str, comment: str, threshold: float):
142
  comment = (comment or "").strip()
143
  if not comment:
144
- return {"Токсичный": 0.0, "Не токсичный": 1.0}, ""
145
 
146
- if model_name == "ruBERT-tiny2 (трансформер)":
147
  p_toxic, err = infer_transformer(comment)
148
  else:
149
  p_toxic, err = infer_tfidf(comment)
@@ -151,74 +126,93 @@ def predict(model_name: str, comment: str, threshold: float):
151
  if err is not None or p_toxic is None:
152
  dist = {"Токсичный": 0.0, "Не токсичный": 1.0}
153
  expl = f"Модель: **{model_name}**\n\n⚠️ {err}"
154
- return dist, expl
155
 
156
- pred = "Токсичный" if p_toxic >= threshold else "Не токсичный"
 
157
  dist = {"Токсичный": p_toxic, "Не токсичный": 1 - p_toxic}
158
  expl = (
159
- f"Модель: **{model_name}** \n"
160
- f"Порог: **{threshold:.2f}** \n"
161
- f"Вероятность токсичности: **{p_toxic:.3f}** \n"
162
- f"Предсказание: **{pred}**"
163
  )
164
- return dist, expl
165
 
 
166
  def clear_all():
167
- return "ruBERT-tiny2 (трансформер)", "", DEFAULT_THRESHOLD, {"Токсичный": 0.0, "Не токсичный": 1.0}, "—"
168
 
169
  # ==============================
170
- # UI - БЕЗ ИЗМЕНЕНИЙ
171
  # ==============================
172
- TITLE = "Анализатор токсичности (две модели)"
173
- DESCRIPTION = "Выберите модель, задайте порог (по умолчанию 0.70) и введите комментарий."
174
 
175
  CUSTOM_CSS = """
176
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');
177
  :root { --font: 'Inter', system-ui, -apple-system, Segoe UI, Roboto, sans-serif; }
178
- """
179
-
180
- ABOUT_MD = """
181
- ### Параметры и описание моделей
182
- **1) ruBERT-tiny2 (трансформер)**
183
- - База: `cointegrated/rubert-tiny2` (BERT-tiny для русского).
184
- - Токенизация: BERT WordPiece.
185
- - Предобработка: удаление пунктуации, нормализация спец-токенов (`url`, `tag`, `num`), стемминг Snowball.
186
- - Обучение: 10 эпох с early stopping (по macro-F1), class weights (balanced).
187
- - Рекомендованный порог: ~**0.70**.
188
- **2) TF-IDF + Логистическая регрессия**
189
- - `TfidfVectorizer(analyzer="char_wb", ngram_range=(4,5), max_features≈200k, min_df≈1.75e-4, max_df≈0.96)`.
190
- - `LogisticRegression(penalty="l1", solver="liblinear", C≈5.52, class_weight="balanced", max_iter=5000, tol≈2.4e-4)`.
191
- - Рекомендованный порог: ~**0.40**.
192
- > Порог можно свободно менять слайдером — подбирайте нужный баланс precision/recall.
193
  """
194
 
195
  with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"), css=CUSTOM_CSS) as demo:
196
  gr.Markdown(f"# {TITLE}")
197
  gr.Markdown(DESCRIPTION)
198
 
199
- with gr.Row():
200
  with gr.Column(scale=2):
201
  model_sel = gr.Dropdown(
202
  ["ruBERT-tiny2 (трансформер)", "TF-IDF + Логистическая регрессия"],
203
- value="ruBERT-tiny2 (трансформер)",
204
- label="Модель"
 
 
 
 
 
 
205
  )
206
- comment_input = gr.Textbox(label="Текст комментария", lines=6, placeholder="Напишите что-нибудь…")
207
- thr = gr.Slider(label="Порог классификации", minimum=0.0, maximum=1.0,
208
- value=DEFAULT_THRESHOLD, step=0.01)
209
  with gr.Row():
210
- analyze_btn = gr.Button("Анализ", variant="primary")
211
- clear_btn = gr.Button("Очистить", variant="secondary")
212
 
213
  with gr.Column(scale=1):
214
- result_label = gr.Label(label="Распределение по классам", num_top_classes=2)
215
- result_md = gr.Markdown(value="—")
216
-
217
- gr.Markdown(ABOUT_MD)
218
-
219
- analyze_btn.click(predict, [model_sel, comment_input, thr], [result_label, result_md])
220
- comment_input.submit(predict, [model_sel, comment_input, thr], [result_label, result_md])
221
- clear_btn.click(clear_all, [], [model_sel, comment_input, thr, result_label, result_md])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
 
223
  if __name__ == "__main__":
224
  demo.launch()
 
5
  import numpy as np
6
 
7
  # ==============================
8
+ # 1. ПРЕПРОЦЕССОР ДЛЯ TF-IDF
9
  # ==============================
10
  _URL_RE_TFIDF = re.compile(r'https?://\S+|www\.\S+')
11
  _TAG_RE_TFIDF = re.compile(r'[@#]\w+')
 
13
  _WS_RE_TFIDF = re.compile(r'\s+')
14
 
15
  def clean_text(s: str) -> str:
16
+ """Эта функция нужна для корректной загрузки model.joblib."""
 
 
 
17
  if not isinstance(s, str):
18
  s = str(s)
19
  s = unescape(s).lower()
 
25
  return s
26
 
27
  # ==============================
28
+ # 2. ЗАГРУЗКА МОДЕЛЕЙ
29
  # ==============================
30
+ # TF-IDF + LR
31
  PIPE, PIPE_PATH = None, "model.joblib"
32
  try:
33
  import joblib
 
39
  print(f"[WARN] Не удалось инициализировать TF-IDF: {e}")
40
  PIPE = None
41
 
42
+ # Transformer (ruBERT-tiny2)
 
 
43
  TRANSFORMER = {"model": None, "tokenizer": None, "device": "cpu"}
44
  try:
45
  import torch
46
  from transformers import AutoTokenizer, AutoModelForSequenceClassification
47
+ MODEL_DIR = "."
 
48
  device = "cuda" if torch.cuda.is_available() else "cpu"
 
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
  TRANSFORMER.update({"model": model, "tokenizer": tokenizer, "device": device})
53
  except Exception as e:
54
  print(f"[WARN] Не удалось загрузить ruBERT: {e}")
55
 
56
  # ==============================
57
+ # 3. КОНФИГУРАЦИЯ
58
  # ==============================
59
  DEFAULT_THRESHOLD = 0.70
60
  try:
 
66
  pass
67
 
68
  # ==============================
69
+ # 4. ПРЕПРОЦЕССОР ДЛЯ ТРАНСФОРМЕРА
70
  # ==============================
71
  from nltk.stem.snowball import RussianStemmer
72
  stemmer = RussianStemmer(ignore_stopwords=False)
 
73
  _URL_RE = re.compile(r'https?://\S+|www\.\S+')
74
  _TAG_RE = re.compile(r'[@#]\w+')
75
  _NUM_RE = re.compile(r'\d+')
 
77
  _WS_RE = re.compile(r"\s+")
78
 
79
  def clean_and_stem(s: str) -> str:
80
+ if not isinstance(s, str): s = str(s)
 
81
  s = unescape(s).lower()
82
+ s = _URL_RE.sub(" url ", s); s = _TAG_RE.sub(" tag ", s); s = _NUM_RE.sub(" num ", s)
83
+ s = _PUNCT_RE.sub(" ", s); s = _WS_RE.sub(" ", s).strip()
84
+ if not s: return s
85
+ return " ".join([t if t in {"url", "tag", "num"} else stemmer.stem(t) for t in s.split(" ")])
 
 
 
 
 
 
 
86
 
87
  # ==============================
88
+ # 5. ФУНКЦИИ ИНФЕРЕНСА
89
  # ==============================
90
  def infer_tfidf(text: str):
91
  if PIPE is None:
92
+ return None, f"TF-IDF модель не загружена (ожидался файл '{PIPE_PATH}'). Проверьте логи."
 
93
  try:
94
+ proba = PIPE.predict_proba([text])[0, 1]
95
+ return float(np.clip(proba, 0.0, 1.0)), None
 
 
 
 
 
 
 
96
  except Exception as e:
97
  return None, f"Ошибка инференса TF-IDF: {e}"
98
 
 
99
  def infer_transformer(text: str):
100
  if TRANSFORMER["model"] is None:
101
  return None, "Модель ruBERT не загружена."
102
  import torch
103
  text_prep = clean_and_stem(text)
104
+ if not text_prep: return 0.0, None
 
105
  tok = TRANSFORMER["tokenizer"](text_prep, return_tensors="pt", truncation=True, max_length=256)
106
  tok = {k: v.to(TRANSFORMER["device"]) for k, v in tok.items()}
107
  try:
 
112
  except Exception as e:
113
  return None, f"Ошибка инференса ruBERT: {e}"
114
 
115
+ # ИЗМЕНЕНО: теперь возвращает 3 значения, включая вердикт
116
  def predict(model_name: str, comment: str, threshold: float):
117
  comment = (comment or "").strip()
118
  if not comment:
119
+ return "—", {"Токсичный": 0.0, "Не токсичный": 1.0}, "Введите текст для анализа"
120
 
121
+ if "ruBERT" in model_name:
122
  p_toxic, err = infer_transformer(comment)
123
  else:
124
  p_toxic, err = infer_tfidf(comment)
 
126
  if err is not None or p_toxic is None:
127
  dist = {"Токсичный": 0.0, "Не токсичный": 1.0}
128
  expl = f"Модель: **{model_name}**\n\n⚠️ {err}"
129
+ return "Ошибка", dist, expl
130
 
131
+ # ГЛАВНОЕ ИЗМЕНЕНИЕ: вердикт определяется порогом
132
+ verdict = "Токсичный" if p_toxic >= threshold else "Не токсичный"
133
  dist = {"Токсичный": p_toxic, "Не токсичный": 1 - p_toxic}
134
  expl = (
135
+ f"**Модель:** {model_name}\n\n"
136
+ f"**Вероятность токсичности:** `{p_toxic:.3f}`\n\n"
137
+ f"**Порог:** `{threshold:.2f}`"
 
138
  )
139
+ return verdict, dist, expl
140
 
141
+ # ИЗМЕНЕНО: добавлено начальное значение для нового поля вердикта
142
  def clear_all():
143
+ return "ruBERT-tiny2 (трансформер)", "", DEFAULT_THRESHOLD, "", None, "—"
144
 
145
  # ==============================
146
+ # 6. ИНТЕРФЕЙС (UI)
147
  # ==============================
148
+ TITLE = "Анализатор токсичности комментариев"
149
+ DESCRIPTION = "Выберите модель, введите комментарий и при необходимости настройте порог классификации."
150
 
151
  CUSTOM_CSS = """
152
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');
153
  :root { --font: 'Inter', system-ui, -apple-system, Segoe UI, Roboto, sans-serif; }
154
+ .gradio-container { max-width: 960px !important; margin: auto !important; }
155
+ #verdict-output span { font-size: 1.8rem !important; font-weight: 700 !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  """
157
 
158
  with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"), css=CUSTOM_CSS) as demo:
159
  gr.Markdown(f"# {TITLE}")
160
  gr.Markdown(DESCRIPTION)
161
 
162
+ with gr.Row(variant="panel"):
163
  with gr.Column(scale=2):
164
  model_sel = gr.Dropdown(
165
  ["ruBERT-tiny2 (трансформер)", "TF-IDF + Логистическая регрессия"],
166
+ value="ruBERT-tiny2 (трансформер)", label="Модель для анализа"
167
+ )
168
+ comment_input = gr.Textbox(
169
+ label="Текст комментария", lines=6, placeholder="Напишите что-нибудь…"
170
+ )
171
+ thr = gr.Slider(
172
+ label="Порог классификации", minimum=0.0, maximum=1.0,
173
+ value=DEFAULT_THRESHOLD, step=0.01
174
  )
 
 
 
175
  with gr.Row():
176
+ analyze_btn = gr.Button("Анализ", variant="primary", scale=2)
177
+ clear_btn = gr.Button("Очистить", variant="secondary", scale=1)
178
 
179
  with gr.Column(scale=1):
180
+ # ИЗМЕНЕНО: Добавлено отдельное поле для вердикта
181
+ verdict_output = gr.Text(label="Вердикт", elem_id="verdict-output", value="—")
182
+ result_label = gr.Label(label="Распределение вероятностей", num_top_classes=2)
183
+ result_md = gr.Markdown(value="—", label="Детали")
184
+
185
+ # ИЗМЕНЕНО: Описание моделей теперь в красивых выпадающих блоках
186
+ with gr.Accordion("Параметры и описание моделей", open=False):
187
+ gr.Markdown(
188
+ """
189
+ ### 🧠 Модель 1: ruBERT-tiny2 (трансформер)
190
+ - **Архитектура:** Нейросеть `cointegrated/rubert-tiny2` (облегченная версия BERT для русского языка), дообученная на задаче классификации.
191
+ - **Особенности:** Хорошо понимает контекст и семантику, но медленнее и требовательнее к ресурсам.
192
+ - **Предобработка:** Удаление пунктуации, стемминг (приведение слов к основе), нормализация URL, тегов и чисел.
193
+ - **Рекомендованный порог:** **~0.70**. При таком пороге модель реже ошибается, когда помечает комментарий как токсичный (высокий `precision`).
194
+ """
195
+ )
196
+ gr.Markdown(
197
+ """
198
+ ### 📊 Модель 2: TF-IDF + Логистическая регрессия
199
+ - **Архитектура:** Классический ML-пайплайн. `TfidfVectorizer` анализирует текст на уровне символьных n-грамм (4-5 символа), а `LogisticRegression` принимает решение.
200
+ - **Особенности:** Очень быстрая и легковесная модель. Хорошо улавливает "токсичные" слова и фразы, но не понимает сложный контекст.
201
+ - **Регуляризация:** L1 (Lasso) для отбора наиболее важных признаков.
202
+ - **Рекомендованный порог:** **~0.40**. Оптимальный баланс между поиском всех токсичных комментариев (`recall`) и точностью вердиктов (`precision`).
203
+ """
204
+ )
205
+ gr.Markdown("> Порог можно свободно менять слайдером, чтобы найти нужный баланс между точностью (precision) и полнотой (recall) для вашей задачи.")
206
+
207
+ # ИЗМЕНЕНО: Обработчики событий теперь обновляют 3 поля вывода
208
+ outputs_list = [verdict_output, result_label, result_md]
209
+ inputs_list = [model_sel, comment_input, thr]
210
+
211
+ analyze_btn.click(predict, inputs_list, outputs_list)
212
+ comment_input.submit(predict, inputs_list, outputs_list)
213
+
214
+ clear_outputs_list = [model_sel, comment_input, thr, verdict_output, result_label, result_md]
215
+ clear_btn.click(clear_all, [], clear_outputs_list)
216
 
217
  if __name__ == "__main__":
218
  demo.launch()