NikitaMY commited on
Commit
d75604e
·
verified ·
1 Parent(s): 9bccffe

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +273 -185
app.py CHANGED
@@ -5,214 +5,302 @@ import re
5
  import numpy as np
6
  from sentence_transformers import SentenceTransformer
7
  from typing import List, Tuple, Optional
 
8
 
9
- #Модель часть 1, конфигурация
10
- DEFAULT_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
11
- model = SentenceTransformer(DEFAULT_MODEL)
 
 
12
 
13
- #Модель часть 2, ограничения
14
- MAX_TEXTS = 500 # Максимум документов в корпусе текста
15
- MAX_CHARS_PER_TEXT = 2000 # Максимум символов в одном документе
16
- MAX_QUERY_CHARS = 2000 # Максимум символов в запросе
17
 
18
- #Модуль А - нормализация текста
19
- def clean_text(s:str) -> str:
20
- """
21
- Приводим строку к нормальному виду
22
- - Если None, то делаем пустую строку
23
- - Множественные пробелы заменяем одним
24
- - Обрезаем пробелы по краям
25
- """
26
- s = "" if s is None else str(s)
27
 
28
- s = re.sub(r'\s+',' ', s).strip()
 
29
 
30
- return s
 
 
 
31
 
32
- #Модуль B - парсинг текста при ручном вводе (каждая строка - документ)
33
- def parser_manual_texts(raw:str) -> List[str]:
34
- raw = "" if raw is None else str(raw)
35
- lines = [clean_text(x) for x in raw.splitlines()]
36
- lines = [x for x in lines if x] #Оставляем только не пустые строки, if x - проверка, что строка не пустая
37
 
38
- return lines[:MAX_TEXTS]
 
 
 
 
 
39
 
40
- #Модуль C - парсинг текста для файла
41
  def parser_file(file_obj) -> List[str]:
42
- if file_obj is None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  return []
44
- path = file_obj.name
45
- if path.lower().endswith(".txt"):
46
- with open(path,"r",encoding="utf-8",errors="ignore") as f:
47
- lines = [clean_text(x) for x in f.read().splitlines()]
48
- lines = [x for x in lines if x]
49
 
50
- return lines[:MAX_TEXTS]
51
-
52
- if path.lower().endswith(".csv"):
53
- df = pd.read_csv(path)
54
- if "text" in df.columns:
55
- col = "text"
56
- else:
57
- col = df.columns(0)
58
-
59
- texts = [clean_text(x) for x in df[col].astype(str).tolist()]
60
- texts = [x for x in texts if x]
61
-
62
- return texts[:MAX_TEXTS]
63
-
64
- return []
65
-
66
- #Модуль D - косинусное сходство
67
  def cosine_sim_matrix(query_emb: np.ndarray, docs_emb: np.ndarray) -> np.ndarray:
68
- """
69
- cosine_similarity = Q * D / (|Q|*|D|)
70
- """
71
- q = query_emb/(np.linalg.norm(query_emb)+1e-12)
72
- d = docs_emb/(np.linalg.norm(docs_emb,axis=1,keepdims=True)+1e-12)
73
-
74
- return d @ q
75
-
76
- #Модуль E - Построение индекса
77
- def build_index(texts:List[str]) -> Tuple[List[str], Optional[np.ndarray], str]:
78
- if not texts:
79
- return [], None, "База пуста, добавьте текст"
80
- texts = [t[:MAX_CHARS_PER_TEXT] for t in texts] # Обрезка документа по длине
81
 
82
- try:
83
- emb = model.encode(texts,convert_to_numpy=True,show_progress_bar=False)
84
- return texts, emb, f"Индекс построен: {len(texts)} текстов"
85
- except Exception as e:
86
- return [], None, f"Ошибка построения индекса: {type(e).__name__}: {e}"
 
 
 
 
 
 
 
 
87
 
88
- #Модуль F - Основной обработчик кнопки
89
  def search_similar(
90
  query: str,
91
  manual_texts: str,
92
  file_obj,
93
  top_k: int,
 
 
94
  state_texts,
95
- state_emb
 
96
  ):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
- query = clean_text(query)[:MAX_QUERY_CHARS]
99
- if not query:
100
- return None, "Введите запрос", state_texts, state_emb
101
- texts = parser_manual_texts(manual_texts)
102
- texts_from_file = parser_file(file_obj)
103
-
104
- texts.extend(texts_from_file)
105
- uniq = []
106
- seen = set()
107
- for t in texts:
108
- if t not in seen:
109
- uniq.append(t)
110
- seen.add(t)
111
- texts = uniq[:MAX_TEXTS]
112
- needs_rebuild = (state_texts is None) or (texts != state_texts) or (state_emb is None)
113
- status_msgs = []
114
- if needs_rebuild:
115
- idx_texts, idx_emb, msg = build_index(texts)
116
- status_msgs.append(msg)
117
- state_texts, state_emb = idx_texts, idx_emb
118
- else:
119
- status_msgs.append(f"Используем готовый индекс: {len(state_texts)} текстов")
120
-
121
- if state_emb is None or not state_texts:
122
- return None, "\n".join(status_msgs), state_texts, state_emb
123
-
124
- try:
125
- q_emb = model.encode([query], convert_to_numpy=True,show_progress_bar=False)[0]
126
- except Exception as e:
127
- return None, f"Ошибка эмбеддинга: {type(e).__name__}: {e}", state_texts, state_emb
128
-
129
- sims = cosine_sim_matrix(q_emb, state_emb)
130
- top_k = int(top_k)
131
- top_k = max(1,min(top_k,len(state_texts)))
132
- idx = np.argsort(-sims)[:top_k] # Сортировка по возрастанию
133
-
134
- rows = []
135
- for rank, i in enumerate(idx, start = 1):
136
- rows.append({
137
- "rank": rank,
138
- "similarity": float(sims[i]),
139
- "text": state_texts[i]
140
- })
141
- df = pd.DataFrame(rows)
142
-
143
- status_msgs.append(f"Топ - {top_k} найден")
144
- return df, "\n".join(status_msgs), state_texts, state_emb
145
-
146
- #Модуль G - Интерфейс Gradio
147
- with gr.Blocks() as demo:
148
- gr.Markdown("""
149
- **Модель эмбеддингов** 'all-MiniLM-L6-v2'
150
- **Идея модели** косинусное расстояние
151
- """
152
- )
153
- with gr.Row():
154
- query = gr.Textbox(
155
- label = "Что ищем",
156
- lines = 4,
157
- placeholder = "Клиент жалуется на задержку"
 
 
 
 
 
 
 
 
 
158
  )
159
- top_k = gr.Slider(
160
- 1,
161
- 20,
162
- value = 5,
163
- step = 1,
164
- label = "top_k похожих текстов"
 
 
 
 
 
 
 
 
 
165
  )
166
- with gr.Row():
167
- manual_texts = gr.Textbox(
168
- label = "База текстов, каждая строка отдельный документ",
169
- lines = 10,
170
- placeholder = "Текст 1 \n текст 2 \n текст 3"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  )
172
- file_obj = gr.File(
173
- label = "Загрузите файл txt, csv"
174
- )
175
- run_btn = gr.Button(
176
- "Найти похожее"
177
- )
178
- out_table = gr.Dataframe(
179
- label = "Результаты",
180
- interactive = False
181
- )
182
- status = gr.Textbox(
183
- label = "Статус",
184
- lines = 4
185
- )
186
- state_texts = gr.State(None)
187
- state_emb = gr.State(None)
188
- run_btn.click(
189
- search_similar,
190
- inputs=[query, manual_texts, file_obj, top_k, state_texts, state_emb],
191
- outputs = [out_table, status, state_texts, state_emb]
192
- )
193
 
194
- # Примеры: удобная демо-загрузка корпуса и запроса
195
- gr.Examples(
196
- examples=[
197
- [
198
- "У меня списали деньги дважды за заказ.",
199
- "Деньги списались два раза за один и тот же заказ.\n"
200
- "Не могу войти в личный кабинет, пишет неверный пароль.\n"
201
- "Доставка задерживается уже на 5 дней, где мой заказ?\n"
202
- "Хочу вернуть товар, он не подошел по размеру.\n"
203
- "Поддержка не отвечает, жду ответа третий день.\n"
204
- "Оплата не проходит, ошибка на этапе подтверждения."
205
- ],
206
- [
207
- "Не могу оплатить, постоянно ошибка.",
208
- "Оплата не проходит, ошибка на этапе подтверждения.\n"
209
- "Платеж отклоняется банком, хотя карта рабочая.\n"
210
- "Хочу отменить заказ и вернуть деньги.\n"
211
- "Курьер не пришел, доставка переносится.\n"
212
- "Личный кабинет не открывается."
213
- ],
214
- ],
215
- inputs=[query, manual_texts],
216
- label="Примеры"
217
- )
218
- demo.launch()
 
5
  import numpy as np
6
  from sentence_transformers import SentenceTransformer
7
  from typing import List, Tuple, Optional
8
+ from functools import lru_cache
9
 
10
+ #ЗАДАНИЕ 2: модели
11
+ MODEL_CHOICES = {
12
+ "all-MiniLM-L6-v2": "sentence-transformers/all-MiniLM-L6-v2",
13
+ "paraphrase-multilingual-MiniLM-L12-v2": "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
14
+ }
15
 
16
+ DEFAULT_MODEL_NAME = "all-MiniLM-L6-v2"
 
 
 
17
 
18
+ @lru_cache(maxsize=2)
19
+ def load_model(model_key: str):
20
+ """Кэшируем модели в памяти"""
21
+ model_path = MODEL_CHOICES[model_key]
22
+ return SentenceTransformer(model_path)
 
 
 
 
23
 
24
+ # Инициализация начальной модели
25
+ model = load_model(DEFAULT_MODEL_NAME)
26
 
27
+ # Ограничения
28
+ MAX_TEXTS = 500
29
+ MAX_CHARS_PER_TEXT = 2000
30
+ MAX_QUERY_CHARS = 2000
31
 
32
+ # Модуль А - нормализация текста
33
+ def clean_text(s: str) -> str:
34
+ s = "" if s is None else str(s)
35
+ s = re.sub(r'\s+', ' ', s).strip()
36
+ return s
37
 
38
+ # Модуль B - парсинг текста при ручном вводе
39
+ def parser_manual_texts(raw: str) -> List[str]:
40
+ raw = "" if raw is None else str(raw)
41
+ lines = [clean_text(x) for x in raw.splitlines()]
42
+ lines = [x for x in lines if x]
43
+ return lines[:MAX_TEXTS]
44
 
45
+ # Модуль C - парсинг текста для файла
46
  def parser_file(file_obj) -> List[str]:
47
+ if file_obj is None:
48
+ return []
49
+ path = file_obj.name
50
+ if path.lower().endswith(".txt"):
51
+ with open(path, "r", encoding="utf-8", errors="ignore") as f:
52
+ lines = [clean_text(x) for x in f.read().splitlines()]
53
+ lines = [x for x in lines if x]
54
+ return lines[:MAX_TEXTS]
55
+
56
+ if path.lower().endswith(".csv"):
57
+ df = pd.read_csv(path)
58
+ if "text" in df.columns:
59
+ col = "text"
60
+ else:
61
+ col = df.columns[0]
62
+
63
+ texts = [clean_text(x) for x in df[col].astype(str).tolist()]
64
+ texts = [x for x in texts if x]
65
+ return texts[:MAX_TEXTS]
66
+
67
  return []
 
 
 
 
 
68
 
69
+ # Модуль D - косинусное сходство
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  def cosine_sim_matrix(query_emb: np.ndarray, docs_emb: np.ndarray) -> np.ndarray:
71
+ q = query_emb / (np.linalg.norm(query_emb) + 1e-12)
72
+ d = docs_emb / (np.linalg.norm(docs_emb, axis=1, keepdims=True) + 1e-12)
73
+ return d @ q
 
 
 
 
 
 
 
 
 
 
74
 
75
+ # Модуль E - Построение индекса (с учетом выбранной модели)
76
+ def build_index(texts: List[str], model_name: str) -> Tuple[List[str], Optional[np.ndarray], str]:
77
+ if not texts:
78
+ return [], None, "База пуста, добавьте текст"
79
+
80
+ texts = [t[:MAX_CHARS_PER_TEXT] for t in texts]
81
+
82
+ try:
83
+ current_model = load_model(model_name)
84
+ emb = current_model.encode(texts, convert_to_numpy=True, show_progress_bar=False)
85
+ return texts, emb, f"Индекс построен: {len(texts)} текстов (модель: {model_name})"
86
+ except Exception as e:
87
+ return [], None, f"Ошибка построения индекса: {type(e).__name__}: {e}"
88
 
89
+ # Модуль F - Основной обработчик кнопки
90
  def search_similar(
91
  query: str,
92
  manual_texts: str,
93
  file_obj,
94
  top_k: int,
95
+ min_sim: float, # ЗАДАНИЕ A: параметр минимальной похожести
96
+ model_name: str, # ЗАДАНИЕ B: выбранная модель
97
  state_texts,
98
+ state_emb,
99
+ state_model # Новое состояние для хранения модели индекса
100
  ):
101
+ query = clean_text(query)[:MAX_QUERY_CHARS]
102
+ if not query:
103
+ return None, "Введите запрос", state_texts, state_emb, state_model
104
+
105
+ # Парсинг текстов
106
+ texts = parser_manual_texts(manual_texts)
107
+ texts_from_file = parser_file(file_obj)
108
+ texts.extend(texts_from_file)
109
+
110
+ # Удаление дубликатов
111
+ uniq = []
112
+ seen = set()
113
+ for t in texts:
114
+ if t not in seen:
115
+ uniq.append(t)
116
+ seen.add(t)
117
+ texts = uniq[:MAX_TEXTS]
118
+
119
+ status_msgs = []
120
+
121
+ # Проверка необходимости перестроения индекса
122
+ needs_rebuild = (
123
+ state_texts is None or
124
+ texts != state_texts or
125
+ state_emb is None or
126
+ state_model != model_name # ЗАДАНИЕ B: перестраиваем если сменили модель
127
+ )
128
+
129
+ if needs_rebuild:
130
+ idx_texts, idx_emb, msg = build_index(texts, model_name)
131
+ status_msgs.append(msg)
132
+ state_texts, state_emb = idx_texts, idx_emb
133
+ state_model = model_name # Сохраняем модель индекса
134
+ else:
135
+ status_msgs.append(f"Используем готовый индекс: {len(state_texts)} текстов")
136
+
137
+ if state_emb is None or not state_texts:
138
+ return None, "\n".join(status_msgs), state_texts, state_emb, state_model
139
+
140
+ try:
141
+ current_model = load_model(model_name)
142
+ q_emb = current_model.encode([query], convert_to_numpy=True, show_progress_bar=False)[0]
143
+ except Exception as e:
144
+ return None, f"Ошибка эмбеддинга: {type(e).__name__}: {e}", state_texts, state_emb, state_model
145
+
146
+ # Расчет сходства
147
+ sims = cosine_sim_matrix(q_emb, state_emb)
148
+
149
+ # ЗАДАНИЕ 1: фильтрация по минимальному порогу
150
+ above_threshold = sims >= min_sim
151
+ if not above_threshold.any():
152
+ return None, "Ничего не найдено (похожесть ниже порога)", state_texts, state_emb, state_model
153
+
154
+ # Получаем индексы, удовлетворяющие порогу
155
+ valid_indices = np.where(above_threshold)[0]
156
+ valid_sims = sims[valid_indices]
157
+
158
+ # Сортировка и выбор top_k
159
+ top_k = int(top_k)
160
+ top_k = max(1, min(top_k, len(valid_indices)))
161
+
162
+ # Сортируем по убыванию сходства среди отфильтрованных
163
+ top_indices = valid_indices[np.argsort(-valid_sims)[:top_k]]
164
+
165
+ # Формирование результатов
166
+ rows = []
167
+ for rank, i in enumerate(top_indices, start=1):
168
+ rows.append({
169
+ "rank": rank,
170
+ "similarity": float(sims[i]),
171
+ "text": state_texts[i]
172
+ })
173
+
174
+ df = pd.DataFrame(rows)
175
+ status_msgs.append(f"Найдено {len(valid_indices)} текстов с похожестью ≥ {min_sim}. Показано топ-{top_k}")
176
+
177
+ return df, "\n".join(status_msgs), state_texts, state_emb, state_model
178
 
179
+ # Модуль G - Интерфейс Gradio
180
+ with gr.Blocks(title="Поиск похожих текстов") as demo:
181
+ gr.Markdown("""
182
+ # Поиск похожих текстов с использованием эмбеддингов
183
+
184
+ **Инструкция:**
185
+ 1. Введите тексты в поле или загрузите файл
186
+ 2. Выберите модель и настройте параметры
187
+ 3. Введите запрос и нажмите "Найти похожее"
188
+ """)
189
+
190
+ with gr.Row():
191
+ with gr.Column(scale=2):
192
+ query = gr.Textbox(
193
+ label="Что ищем",
194
+ lines=4,
195
+ placeholder="Клиент жалуется на задержку"
196
+ )
197
+ with gr.Column(scale=1):
198
+ # ЗАДАНИЕ B: выбор модели
199
+ model_dropdown = gr.Dropdown(
200
+ choices=list(MODEL_CHOICES.keys()),
201
+ value=DEFAULT_MODEL_NAME,
202
+ label="Модель эмбеддингов"
203
+ )
204
+
205
+ # ЗАДАНИЕ 1: ползунок минимальной похожести
206
+ min_sim = gr.Slider(
207
+ 0.0, 1.0,
208
+ value=0.3,
209
+ step=0.01,
210
+ label="Минимальная похожесть",
211
+ info="Показывать только результаты с похожестью выше этого значения"
212
+ )
213
+
214
+ top_k = gr.Slider(
215
+ 1, 20,
216
+ value=5,
217
+ step=1,
218
+ label="Количество результатов"
219
+ )
220
+
221
+ with gr.Row():
222
+ manual_texts = gr.Textbox(
223
+ label="База текстов (каждая строка - отдельный документ)",
224
+ lines=10,
225
+ placeholder="Текст 1\nТекст 2\nТекст 3"
226
+ )
227
+
228
+ file_obj = gr.File(
229
+ label="Или загрузите файл (txt, csv)",
230
+ file_types=[".txt", ".csv"]
231
+ )
232
+
233
+ run_btn = gr.Button(
234
+ "Найти похожее",
235
+ variant="primary"
236
+ )
237
+
238
+ with gr.Row():
239
+ out_table = gr.Dataframe(
240
+ label="Результаты поиска",
241
+ interactive=False,
242
+ wrap=True
243
+ )
244
+
245
+ status = gr.Textbox(
246
+ label="Статус выполнения",
247
+ lines=4
248
  )
249
+
250
+ # Состояния
251
+ state_texts = gr.State(None)
252
+ state_emb = gr.State(None)
253
+ state_model = gr.State(DEFAULT_MODEL_NAME) # Новое состояние для модели
254
+
255
+ # Обработчик кнопки
256
+ run_btn.click(
257
+ search_similar,
258
+ inputs=[
259
+ query, manual_texts, file_obj, top_k,
260
+ min_sim, model_dropdown, # Добавлены новые параметры
261
+ state_texts, state_emb, state_model
262
+ ],
263
+ outputs=[out_table, status, state_texts, state_emb, state_model]
264
  )
265
+
266
+ # Примеры
267
+ gr.Examples(
268
+ examples=[
269
+ [
270
+ "У меня списали деньги дважды за заказ.",
271
+ "Деньги списались два раза за один и тот же заказ.\n"
272
+ "Не могу войти в личный кабинет, пишет неверный пароль.\n"
273
+ "Доставка задерживается уже на 5 дней, где мой заказ?\n"
274
+ "Хочу вернуть товар, он не подошел по размеру.\n"
275
+ "Поддержка не отвечает, жду ответа третий день.\n"
276
+ "Оплата не проходит, ошибка на этапе подтверждения."
277
+ ],
278
+ [
279
+ "Не могу оплатить, постоянно ошибка.",
280
+ "Оплата не проходит, ошибка на этапе подтверждения.\n"
281
+ "Платеж отклоняется банком, хотя карта рабочая.\n"
282
+ "Хочу отменить заказ и вернуть деньги.\n"
283
+ "Курьер не пришел, доставка переносится.\n"
284
+ "Личный кабинет не открывается."
285
+ ],
286
+ ],
287
+ inputs=[query, manual_texts],
288
+ label="Примеры запросов"
289
  )
290
+
291
+ # Информация о моделях
292
+ with gr.Accordion("Информация о моделях", open=False):
293
+ gr.Markdown("""
294
+ **all-MiniLM-L6-v2:**
295
+ - Размер: 80 МБ
296
+ - Размерность эмбеддингов: 384
297
+ - Язык: английский
298
+
299
+ **paraphrase-multilingual-MiniLM-L12-v2:**
300
+ - Размер: 420 МБ
301
+ - Размерность эмбеддингов: 384
302
+ - Языки: мультиязычная (поддерживает русский)
303
+ """)
 
 
 
 
 
 
 
304
 
305
+ if __name__ == "__main__":
306
+ demo.launch(share=False)