DocUA commited on
Commit
ed124cc
·
1 Parent(s): 611ebab

Оновлено Пакетний режим тестування. Робочий варіант

Browse files
.gitignore CHANGED
@@ -4,4 +4,5 @@ __pycache__/
4
  .env
5
 
6
  *.npz
7
- *.db
 
 
4
  .env
5
 
6
  *.npz
7
+ *.db
8
+ *.csv
.~lock.kw_questions_tested.csv# ADDED
@@ -0,0 +1 @@
 
 
1
+ ,docsa,docsa-HP-ProBook-450-G7,03.02.2025 12:17,file:///home/docsa/.config/libreoffice/4;
app.py CHANGED
@@ -85,24 +85,13 @@ def create_interface(app: ClassifierApp) -> gr.Blocks:
85
 
86
  # Вкладка 3: Пакетна обробка
87
  with gr.TabItem("Пакетна обробка"):
88
- gr.Markdown("## 1) Завантаження даних")
89
  with gr.Row():
90
- csv_input = gr.Textbox(
91
- value="messages.csv",
92
- label="CSV-файл"
93
  )
94
- emb_input = gr.Textbox(
95
- value="embeddings.npy",
96
- label="Numpy Embeddings"
97
- )
98
- load_btn = gr.Button("Завантажити дані")
99
-
100
- load_output = gr.Label(label="Результат завантаження")
101
-
102
- gr.Markdown("## 2) Класифікація")
103
- with gr.Row():
104
- filter_in = gr.Textbox(label="Фільтр (опціонально)")
105
- batch_threshold = gr.Slider(
106
  minimum=0.0,
107
  maximum=1.0,
108
  value=0.3,
@@ -110,12 +99,17 @@ def create_interface(app: ClassifierApp) -> gr.Blocks:
110
  label="Поріг впевненості"
111
  )
112
 
113
- classify_btn = gr.Button("Класифікувати")
114
- classify_out = gr.Dataframe(label="Результат (Message / Target / Scores)")
115
 
116
- gr.Markdown("## 3) Зберегти результати")
117
- save_btn = gr.Button("Зберегти розмічені дані")
118
- save_out = gr.Label()
 
 
 
 
 
 
119
 
120
  # Підключення обробників подій
121
  model_type.change(
@@ -156,22 +150,15 @@ def create_interface(app: ClassifierApp) -> gr.Blocks:
156
  outputs=result_text
157
  )
158
 
159
- load_btn.click(
160
- fn=app.load_data,
161
- inputs=[csv_input, emb_input],
162
- outputs=load_output
163
- )
164
-
165
- classify_btn.click(
166
- fn=app.classify_batch,
167
- inputs=[filter_in, batch_threshold],
168
- outputs=classify_out
169
  )
170
 
171
- save_btn.click(
172
- fn=app.save_results,
173
- inputs=[],
174
- outputs=save_out
175
  )
176
 
177
  return demo
 
85
 
86
  # Вкладка 3: Пакетна обробка
87
  with gr.TabItem("Пакетна обробка"):
88
+ gr.Markdown("## Оцінка класифікації")
89
  with gr.Row():
90
+ csv_input = gr.File(
91
+ label="CSV файл з колонками Category та Question",
92
+ file_types=[".csv"]
93
  )
94
+ threshold_slider_batch = gr.Slider(
 
 
 
 
 
 
 
 
 
 
 
95
  minimum=0.0,
96
  maximum=1.0,
97
  value=0.3,
 
99
  label="Поріг впевненості"
100
  )
101
 
102
+ evaluate_btn = gr.Button("Оцінити класифікацію")
 
103
 
104
+ results_df = gr.DataFrame(
105
+ label="Результати класифікації"
106
+ )
107
+
108
+ stats_md = gr.Markdown("### Статистика класифікації")
109
+
110
+ save_results_btn = gr.Button("Завантажити результати")
111
+ download_file = gr.File(label="Завантажити файл з результатами")
112
+ save_status = gr.Markdown()
113
 
114
  # Підключення обробників подій
115
  model_type.change(
 
150
  outputs=result_text
151
  )
152
 
153
+ evaluate_btn.click(
154
+ fn=app.evaluate_batch,
155
+ inputs=[csv_input, threshold_slider_batch],
156
+ outputs=[results_df, stats_md]
 
 
 
 
 
 
157
  )
158
 
159
+ save_results_btn.click(
160
+ fn=app.save_evaluation_results,
161
+ outputs=[download_file, save_status]
 
162
  )
163
 
164
  return demo
classifier_app.py CHANGED
@@ -5,13 +5,17 @@ import json
5
  import os
6
  from typing import Dict, Tuple, Optional, Any, List
7
  from dataclasses import dataclass, field
 
8
 
9
  # Load environment variables
10
  load_dotenv()
11
 
12
  @dataclass
13
  class Config:
14
- DEFAULT_CLASSES_FILE: str = "classes.json"
 
 
 
15
  DEFAULT_SIGNATURES_FILE: str = "signatures.npz"
16
  CACHE_FILE: str = "embeddings_cache.db"
17
  MODEL_INFO_FILE: str = "model_info.json"
@@ -29,6 +33,7 @@ class ClassifierApp:
29
  "classes_info": {},
30
  "errors": []
31
  }
 
32
 
33
  def initialize_environment(self) -> Tuple[Dict, Optional[SDCClassifier]]:
34
  """Ініціалізація середовища при першому запуску"""
@@ -345,6 +350,86 @@ class ClassifierApp:
345
  }
346
  return self.initial_info
347
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
348
  @staticmethod
349
  def update_system_markdown(info: Dict) -> str:
350
  """Оновлення Markdown з системною інформацією"""
 
5
  import os
6
  from typing import Dict, Tuple, Optional, Any, List
7
  from dataclasses import dataclass, field
8
+ import pandas as pd
9
 
10
  # Load environment variables
11
  load_dotenv()
12
 
13
  @dataclass
14
  class Config:
15
+ # DEFAULT_CLASSES_FILE: str = "classes.json"
16
+ DEFAULT_CLASSES_FILE: str = "kw_questions.json"
17
+
18
+
19
  DEFAULT_SIGNATURES_FILE: str = "signatures.npz"
20
  CACHE_FILE: str = "embeddings_cache.db"
21
  MODEL_INFO_FILE: str = "model_info.json"
 
33
  "classes_info": {},
34
  "errors": []
35
  }
36
+ self.model_type = "Local" # Додати цей рядок
37
 
38
  def initialize_environment(self) -> Tuple[Dict, Optional[SDCClassifier]]:
39
  """Ініціалізація середовища при першому запуску"""
 
350
  }
351
  return self.initial_info
352
 
353
+ def evaluate_batch(self, csv_file, threshold: float) -> tuple[pd.DataFrame, str]:
354
+ """
355
+ Оцінка пакетної класифікації
356
+
357
+ Args:
358
+ csv_file: завантажений CSV файл від gradio
359
+ threshold: поріг впевненості
360
+
361
+ Returns:
362
+ tuple[pd.DataFrame, str]: результати та статистика
363
+ """
364
+ try:
365
+ if self.classifier is None:
366
+ return None, "Помилка: Класифікатор не ініціалізовано"
367
+
368
+ # Перевірка на None
369
+ if csv_file is None:
370
+ return None, "Помилка: Файл не завантажено"
371
+
372
+ # Зберігаємо тимчасовий файл
373
+ temp_path = "temp_upload.csv"
374
+ if hasattr(csv_file, 'name'):
375
+ # Якщо це файловий об'єкт від gradio
376
+ import shutil
377
+ shutil.copy2(csv_file.name, temp_path)
378
+ else:
379
+ # Якщо це шлях до файлу
380
+ temp_path = str(csv_file)
381
+
382
+ # Виконуємо класифікацію
383
+ results_df, statistics = self.classifier.evaluate_classification(temp_path, threshold)
384
+
385
+ # Формуємо текст статистики
386
+ stats_md = f"""### Статистика класифікації
387
+ - Всього зразків: {statistics['total_samples']}
388
+ - Правильний клас на першому місці: {statistics['correct_first_place']['count']} ({statistics['correct_first_place']['percentage']}%)
389
+ - Правильний клас в топ-3: {statistics['in_top3']['count']} ({statistics['in_top3']['percentage']}%)
390
+ - Правильний клас не знайдено: {statistics['not_found']['count']} ({statistics['not_found']['percentage']}%)
391
+
392
+ #### Середня впевненість для правильних класифікацій: {statistics['mean_confidence_correct']}%
393
+
394
+ #### Розподіл впевненості:
395
+ - 90-100%: {statistics['confidence_distribution']['90-100%']['count']} ({statistics['confidence_distribution']['90-100%']['percentage']}%)
396
+ - 70-90%: {statistics['confidence_distribution']['70-90%']['count']} ({statistics['confidence_distribution']['70-90%']['percentage']}%)
397
+ - 50-70%: {statistics['confidence_distribution']['50-70%']['count']} ({statistics['confidence_distribution']['50-70%']['percentage']}%)
398
+ - <50%: {statistics['confidence_distribution']['<50%']['count']} ({statistics['confidence_distribution']['<50%']['percentage']}%)
399
+ """
400
+
401
+ # Зберігаємо результати для подальшого використання
402
+ self.current_evaluation_results = results_df
403
+
404
+ # Видаляємо тимчасовий файл якщо він був створений
405
+ if temp_path == "temp_upload.csv" and os.path.exists(temp_path):
406
+ os.remove(temp_path)
407
+
408
+ return results_df, stats_md
409
+ except Exception as e:
410
+ # У випадку помилки спробуємо видалити тимчасовий файл
411
+ if os.path.exists("temp_upload.csv"):
412
+ os.remove("temp_upload.csv")
413
+ return None, f"Помилка: {str(e)}"
414
+
415
+ def save_evaluation_results(self) -> tuple[str, str]:
416
+ """
417
+ Зберігає результати останньої оцінки класифікації та готує файл для завантаження
418
+
419
+ Returns:
420
+ tuple[str, str]: (шлях до файлу, повідомлення про статус)
421
+ """
422
+ try:
423
+ if not hasattr(self, 'current_evaluation_results'):
424
+ return None, "Помилка: Немає результатів для збереження"
425
+
426
+ output_path = "evaluation_results.csv"
427
+ self.current_evaluation_results.to_csv(output_path, index=False)
428
+ return output_path, f"Результати збережено у файл {output_path}"
429
+
430
+ except Exception as e:
431
+ return None, f"Помилка при збереженні: {str(e)}"
432
+
433
  @staticmethod
434
  def update_system_markdown(info: Dict) -> str:
435
  """Оновлення Markdown з системною інформацією"""
model_info.json CHANGED
@@ -3,8 +3,8 @@
3
  "classes_count": 0,
4
  "signatures_count": 0,
5
  "cache_stats": {
6
- "total_entries": 8746,
7
- "cache_size_mb": 51.91,
8
  "hits": 0,
9
  "misses": 0,
10
  "hit_rate_percent": 0
 
3
  "classes_count": 0,
4
  "signatures_count": 0,
5
  "cache_stats": {
6
+ "total_entries": 7756,
7
+ "cache_size_mb": 30.84,
8
  "hits": 0,
9
  "misses": 0,
10
  "hit_rate_percent": 0
sdc_classifier.py CHANGED
@@ -430,7 +430,186 @@ class SDCClassifier:
430
 
431
  with open(path, 'w', encoding='utf-8') as f:
432
  json.dump(info, f, indent=2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
433
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
434
  @staticmethod
435
  def load_model_info(path: str) -> dict:
436
  """
 
430
 
431
  with open(path, 'w', encoding='utf-8') as f:
432
  json.dump(info, f, indent=2)
433
+
434
+ def evaluate_classification(self, csv_path: str, threshold: float = 0.3) -> pd.DataFrame:
435
+ """
436
+ Оцінка класифікації текстів з CSV файлу
437
+
438
+ Args:
439
+ csv_path: шлях до CSV файлу з колонками Category та Question
440
+ threshold: поріг впевненості для класифікації
441
+
442
+ Returns:
443
+ pd.DataFrame: результати класифікації з додатковими метриками
444
+ """
445
+ if self.class_signatures is None:
446
+ raise ValueError("Спочатку збудуйте signatures!")
447
+
448
+ # Завантаження даних
449
+ df = pd.read_csv(csv_path)
450
+ if not {'Category', 'Question'}.issubset(df.columns):
451
+ raise ValueError("CSV повинен містити колонки 'Category' та 'Question'")
452
+
453
+ # Підготовка результатів
454
+ results = []
455
+
456
+ for idx, row in df.iterrows():
457
+ # Отримуємо ембедінг для питання
458
+ emb = np.array(self.get_embedding(row['Question']))
459
+
460
+ # Нормалізуємо якщо потрібно
461
+ if self.embeddings_mean is not None and self.embeddings_std is not None and not self.using_local:
462
+ emb = (emb - self.embeddings_mean) / self.embeddings_std
463
+
464
+ # Отримуємо всі передбачення
465
+ predictions = self.predict_classes(emb, threshold)
466
+
467
+ # Формуємо список класів за рівнем впевненості
468
+ sorted_classes = list(predictions.keys())
469
+
470
+ # Знаходимо позицію очікуваного класу
471
+ expected_class = row['Category']
472
+ expected_position = sorted_classes.index(expected_class) + 1 if expected_class in sorted_classes else -1
473
+
474
+ # Отримуємо рівень впевненості для очікуваного класу
475
+ expected_confidence = predictions.get(expected_class, 0.0)
476
+
477
+ # Додаємо результат
478
+ results.append({
479
+ 'Category': row['Category'],
480
+ 'Question': row['Question'],
481
+ 'ExpectedClassPosition': expected_position,
482
+ 'ExpectedClassConfidence': expected_confidence,
483
+ 'ClassificationResults': json.dumps(predictions)
484
+ })
485
+
486
+ return pd.DataFrame(results)
487
+
488
+ def save_evaluation_results(self, df: pd.DataFrame, output_path: str = "evaluation_results.csv") -> str:
489
+ """
490
+ Зберігає результати оцінки класифікації
491
+
492
+ Args:
493
+ df: DataFrame з результатами
494
+ output_path: шлях для збереження файлу
495
+
496
+ Returns:
497
+ str: повідомлення про результат
498
+ """
499
+ try:
500
+ df.to_csv(output_path, index=False)
501
+ return f"Результати збережено у файл {output_path}"
502
+ except Exception as e:
503
+ return f"Помилка при збереженні результатів: {str(e)}"
504
+
505
+ def get_evaluation_statistics(self, df: pd.DataFrame) -> dict:
506
+ """
507
+ Розраховує статистику по результатам класифікації
508
+
509
+ Args:
510
+ df: DataFrame з результатами класифікації
511
 
512
+ Returns:
513
+ dict: статистика класифікації
514
+ """
515
+ total = len(df)
516
+ found_mask = df['ExpectedClassPosition'] != -1
517
+ correct_first = (df['ExpectedClassPosition'] == 1).sum()
518
+ in_top3 = (df['ExpectedClassPosition'].between(1, 3)).sum()
519
+ not_found = (~found_mask).sum()
520
+
521
+ # Середня впевненість для коректних класифікацій
522
+ mean_confidence = df[df['ExpectedClassPosition'] == 1]['ExpectedClassConfidence'].mean()
523
+
524
+ # Підрахунок по діапазонах впевненості
525
+ confidence_ranges = {
526
+ "90-100%": ((df['ExpectedClassConfidence'] >= 0.9) & found_mask).sum(),
527
+ "70-90%": ((df['ExpectedClassConfidence'].between(0.7, 0.9)) & found_mask).sum(),
528
+ "50-70%": ((df['ExpectedClassConfidence'].between(0.5, 0.7)) & found_mask).sum(),
529
+ "<50%": ((df['ExpectedClassConfidence'] < 0.5) & found_mask).sum()
530
+ }
531
+
532
+ return {
533
+ "total_samples": total,
534
+ "correct_first_place": {
535
+ "count": int(correct_first),
536
+ "percentage": round(correct_first/total * 100, 1)
537
+ },
538
+ "in_top3": {
539
+ "count": int(in_top3),
540
+ "percentage": round(in_top3/total * 100, 1)
541
+ },
542
+ "not_found": {
543
+ "count": int(not_found),
544
+ "percentage": round(not_found/total * 100, 1)
545
+ },
546
+ "mean_confidence_correct": round(mean_confidence * 100, 1) if not np.isnan(mean_confidence) else 0,
547
+ "confidence_distribution": {
548
+ k: {
549
+ "count": int(v),
550
+ "percentage": round(v/total * 100, 1)
551
+ }
552
+ for k, v in confidence_ranges.items()
553
+ }
554
+ }
555
+
556
+ def evaluate_classification(self, csv_path: str, threshold: float = 0.3) -> tuple[pd.DataFrame, dict]:
557
+ """
558
+ Оцінка класифікації текстів з CSV файлу
559
+
560
+ Args:
561
+ csv_path: шлях до CSV файлу з колонками Category та Question
562
+ threshold: поріг впевненості для класифікації
563
+
564
+ Returns:
565
+ tuple[pd.DataFrame, dict]: результати класифікації та статистика
566
+ """
567
+ if self.class_signatures is None:
568
+ raise ValueError("Спочатку збудуйте signatures!")
569
+
570
+ # Завантаження даних
571
+ df = pd.read_csv(csv_path)
572
+ if not {'Category', 'Question'}.issubset(df.columns):
573
+ raise ValueError("CSV повинен містити колонки 'Category' та 'Question'")
574
+
575
+ # Підготовка результатів
576
+ results = []
577
+
578
+ for idx, row in df.iterrows():
579
+ # Отримуємо ембедінг для питання
580
+ emb = np.array(self.get_embedding(row['Question']))
581
+
582
+ # Нормалізуємо якщо потрібно
583
+ if self.embeddings_mean is not None and self.embeddings_std is not None and not self.using_local:
584
+ emb = (emb - self.embeddings_mean) / self.embeddings_std
585
+
586
+ # Отримуємо всі передбачення
587
+ predictions = self.predict_classes(emb, threshold)
588
+
589
+ # Формуємо список класів за рівнем впевненості
590
+ sorted_classes = list(predictions.keys())
591
+
592
+ # Знаходимо позицію очікуваного класу
593
+ expected_class = row['Category']
594
+ expected_position = sorted_classes.index(expected_class) + 1 if expected_class in sorted_classes else -1
595
+
596
+ # Отримуємо рівень впевненості для очікуваного класу
597
+ expected_confidence = predictions.get(expected_class, 0.0)
598
+
599
+ # Додаємо результат
600
+ results.append({
601
+ 'Category': row['Category'],
602
+ 'Question': row['Question'],
603
+ 'ExpectedClassPosition': expected_position,
604
+ 'ExpectedClassConfidence': expected_confidence,
605
+ 'ClassificationResults': json.dumps(predictions)
606
+ })
607
+
608
+ results_df = pd.DataFrame(results)
609
+ statistics = self.get_evaluation_statistics(results_df)
610
+
611
+ return results_df, statistics
612
+
613
  @staticmethod
614
  def load_model_info(path: str) -> dict:
615
  """