Rulga commited on
Commit
59b4796
·
1 Parent(s): 68997ff

Add analizer and tuner bot

Browse files
analytics/chat_analyzer.py ADDED
@@ -0,0 +1,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Модуль для анализа истории чатов и извлечения полезных данных для обучения
3
+ """
4
+
5
+ import json
6
+ from typing import List, Dict, Any, Tuple, Optional
7
+ from collections import Counter, defaultdict
8
+ import re
9
+ from datetime import datetime
10
+ from src.knowledge_base.dataset import DatasetManager
11
+
12
+ class ChatAnalyzer:
13
+ def __init__(self, dataset_manager: Optional[DatasetManager] = None):
14
+ """
15
+ Инициализация анализатора чатов
16
+
17
+ Args:
18
+ dataset_manager: Менеджер датасетов для получения истории чатов
19
+ """
20
+ self.dataset_manager = dataset_manager or DatasetManager()
21
+
22
+ def get_chat_data(self) -> List[Dict[str, Any]]:
23
+ """
24
+ Получение всех данных чатов из датасета
25
+
26
+ Returns:
27
+ Список историй чатов
28
+ """
29
+ success, chat_data = self.dataset_manager.get_chat_history()
30
+ if not success or not chat_data:
31
+ return []
32
+ return chat_data
33
+
34
+ def extract_question_answer_pairs(self, min_question_length: int = 10) -> List[Dict[str, str]]:
35
+ """
36
+ Извлечение пар вопрос-ответ из истории чатов
37
+
38
+ Args:
39
+ min_question_length: Минимальная длина вопроса для включения в выборку
40
+
41
+ Returns:
42
+ Список пар вопрос-ответ в формате [{"question": "...", "answer": "..."}]
43
+ """
44
+ chat_data = self.get_chat_data()
45
+ qa_pairs = []
46
+
47
+ for chat in chat_data:
48
+ messages = chat.get("messages", [])
49
+
50
+ # Проходим по сообщениям и собираем пары вопрос-ответ
51
+ for i in range(len(messages) - 1):
52
+ if messages[i].get("role") == "user" and messages[i+1].get("role") == "assistant":
53
+ question = messages[i].get("content", "").strip()
54
+ answer = messages[i+1].get("content", "").strip()
55
+
56
+ # Фильтруем по длине вопроса
57
+ if len(question) >= min_question_length and answer:
58
+ qa_pairs.append({
59
+ "question": question,
60
+ "answer": answer
61
+ })
62
+
63
+ return qa_pairs
64
+
65
+ def analyze_common_questions(self, top_n: int = 10) -> List[Tuple[str, int]]:
66
+ """
67
+ Анализ наиболее часто задаваемых вопросов
68
+
69
+ Args:
70
+ top_n: Количество самых популярных вопросов для возврата
71
+
72
+ Returns:
73
+ Список кортежей (вопрос, количество)
74
+ """
75
+ qa_pairs = self.extract_question_answer_pairs()
76
+
77
+ # Извлекаем только вопросы
78
+ questions = [qa["question"] for qa in qa_pairs]
79
+
80
+ # Предварительная обработка вопросов для лучшего группирования
81
+ processed_questions = []
82
+ for q in questions:
83
+ # Преобразуем в нижний регистр
84
+ q = q.lower()
85
+ # Удаляем пунктуацию и лишние пробелы
86
+ q = re.sub(r'[^\w\s]', ' ', q)
87
+ q = re.sub(r'\s+', ' ', q).strip()
88
+ processed_questions.append(q)
89
+
90
+ # Подсчет частоты вопросов
91
+ question_counter = Counter(processed_questions)
92
+
93
+ # Получаем top_n самых частых вопросов
94
+ return question_counter.most_common(top_n)
95
+
96
+ def analyze_user_satisfaction(self) -> Dict[str, Any]:
97
+ """
98
+ Анализ удовлетворенности пользователей на основе истории чатов
99
+
100
+ Returns:
101
+ Словарь с метриками удовлетворенности
102
+ """
103
+ chat_data = self.get_chat_data()
104
+
105
+ # Инициализация метрик
106
+ metrics = {
107
+ "total_conversations": len(chat_data),
108
+ "avg_messages_per_conversation": 0,
109
+ "avg_conversation_duration": 0, # в секундах
110
+ "follow_up_questions_rate": 0, # процент диалогов с дополнительными вопросами
111
+ }
112
+
113
+ if not chat_data:
114
+ return metrics
115
+
116
+ # Подсчет общего количества сообщений и длительности диалогов
117
+ total_messages = 0
118
+ conversations_with_followups = 0
119
+ total_duration = 0
120
+
121
+ for chat in chat_data:
122
+ messages = chat.get("messages", [])
123
+ total_messages += len(messages)
124
+
125
+ # Проверка наличия дополнительных вопросов от пользователя
126
+ user_messages = [m for m in messages if m.get("role") == "user"]
127
+ if len(user_messages) > 1:
128
+ conversations_with_followups += 1
129
+
130
+ # Расчет длительности диалога, если есть временные метки
131
+ if len(messages) >= 2 and all(["timestamp" in m for m in [messages[0], messages[-1]]]):
132
+ try:
133
+ start_time = datetime.fromisoformat(messages[0]["timestamp"])
134
+ end_time = datetime.fromisoformat(messages[-1]["timestamp"])
135
+ duration = (end_time - start_time).total_seconds()
136
+ total_duration += duration
137
+ except (ValueError, KeyError):
138
+ pass
139
+
140
+ # Расчет средних значений
141
+ metrics["avg_messages_per_conversation"] = total_messages / len(chat_data)
142
+ metrics["follow_up_questions_rate"] = conversations_with_followups / len(chat_data) * 100
143
+
144
+ # Расчет средней длительности, если есть данные
145
+ if total_duration > 0:
146
+ metrics["avg_conversation_duration"] = total_duration / len(chat_data)
147
+
148
+ return metrics
149
+
150
+ def extract_failed_questions(self) -> List[str]:
151
+ """
152
+ Извлечение вопросов, на которые бот не смог дать удовлетворительный ответ
153
+
154
+ Returns:
155
+ Список вопросов, требующих улучшения
156
+ """
157
+ chat_data = self.get_chat_data()
158
+ failed_questions = []
159
+
160
+ # Ключевые слова, указывающие на неудовлетворительный ответ
161
+ failure_indicators = [
162
+ "не знаю", "не могу ответить", "затрудняюсь ответить",
163
+ "у меня нет информации", "не имею данных"
164
+ ]
165
+
166
+ for chat in chat_data:
167
+ messages = chat.get("messages", [])
168
+
169
+ for i in range(len(messages) - 1):
170
+ if messages[i].get("role") == "user" and messages[i+1].get("role") == "assistant":
171
+ question = messages[i].get("content", "").strip()
172
+ answer = messages[i+1].get("content", "").strip().lower()
173
+
174
+ # Проверяем, содержит ли ответ индикаторы неудачи
175
+ if any(indicator in answer for indicator in failure_indicators):
176
+ failed_questions.append(question)
177
+
178
+ return failed_questions
179
+
180
+ def export_training_data(self, output_file: str) -> Tuple[bool, str]:
181
+ """
182
+ Экспорт данных для обучения в формате JSONL
183
+
184
+ Args:
185
+ output_file: Путь к выходному файлу
186
+
187
+ Returns:
188
+ (успех, сообщение)
189
+ """
190
+ try:
191
+ qa_pairs = self.extract_question_answer_pairs()
192
+
193
+ if not qa_pairs:
194
+ return False, "Нет достаточного количества данных для экспорта"
195
+
196
+ with open(output_file, "w", encoding="utf-8") as f:
197
+ for pair in qa_pairs:
198
+ training_example = {
199
+ "messages": [
200
+ {"role": "user", "content": pair["question"]},
201
+ {"role": "assistant", "content": pair["answer"]}
202
+ ]
203
+ }
204
+ f.write(json.dumps(training_example, ensure_ascii=False) + "\n")
205
+
206
+ return True, f"Данные для обучения успешно экспортированы в {output_file}. Экспортировано {len(qa_pairs)} примеров."
207
+ except Exception as e:
208
+ return False, f"Ошибка при экспорте данных для обучения: {str(e)}"
209
+
210
+ def generate_analytics_report(self) -> Dict[str, Any]:
211
+ """
212
+ Генерация полного аналитического отчета
213
+
214
+ Returns:
215
+ Словарь с различными метриками и анализом
216
+ """
217
+ report = {}
218
+
219
+ # Базовые метрики
220
+ chat_data = self.get_chat_data()
221
+ report["total_conversations"] = len(chat_data)
222
+
223
+ # Удовлетворенность пользователей
224
+ report["satisfaction_metrics"] = self.analyze_user_satisfaction()
225
+
226
+ # Частые вопросы
227
+ report["common_questions"] = self.analyze_common_questions(top_n=20)
228
+
229
+ # Вопросы без ответов
230
+ report["failed_questions"] = self.extract_failed_questions()
231
+ report["failed_questions_count"] = len(report["failed_questions"])
232
+
233
+ # Статистика по количеству пар вопрос-ответ
234
+ qa_pairs = self.extract_question_answer_pairs()
235
+ report["qa_pairs_count"] = len(qa_pairs)
236
+
237
+ return report
238
+
239
+ if __name__ == "__main__":
240
+ # Пример использования
241
+ analyzer = ChatAnalyzer()
242
+ report = analyzer.generate_analytics_report()
243
+ print(f"Всего диалогов: {report['total_conversations']}")
244
+ print(f"Пар вопрос-ответ для обучения: {report['qa_pairs_count']}")
245
+ print("\nСамые популярные вопросы:")
246
+ for question, count in report['common_questions'][:5]:
247
+ print(f" - {question} ({count} раз)")
training/fine_tuner.py ADDED
@@ -0,0 +1,367 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Модуль для дообучения языковой модели на основе собранных данных
3
+ """
4
+
5
+ import os
6
+ import json
7
+ import tempfile
8
+ from typing import List, Dict, Any, Tuple, Optional
9
+ import logging
10
+ from huggingface_hub import HfApi, HfFolder
11
+ from peft import LoraConfig, get_peft_model, TaskType
12
+ from transformers import (
13
+ AutoModelForCausalLM,
14
+ AutoTokenizer,
15
+ Trainer,
16
+ TrainingArguments,
17
+ DataCollatorForLanguageModeling
18
+ )
19
+ from datasets import load_dataset
20
+ from src.analytics.chat_analyzer import ChatAnalyzer
21
+ from config.settings import MODEL_PATH, TRAINING_OUTPUT_DIR
22
+
23
+ # Настройка логирования
24
+ logging.basicConfig(
25
+ level=logging.INFO,
26
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
27
+ )
28
+ logger = logging.getLogger(__name__)
29
+
30
+ class FineTuner:
31
+ def __init__(
32
+ self,
33
+ base_model_id: str = "IlyaGusev/saiga_7b_lora",
34
+ output_dir: Optional[str] = None,
35
+ device: str = "cuda" if os.environ.get("CUDA_VISIBLE_DEVICES") else "cpu"
36
+ ):
37
+ """
38
+ Инициализация модуля для дообучения модели
39
+
40
+ Args:
41
+ base_model_id: Идентификатор базовой модели на Hugging Face Hub
42
+ output_dir: Директория для сохранения результатов обучения
43
+ device: Устройство для обучения ('cuda' или 'cpu')
44
+ """
45
+ self.base_model_id = base_model_id
46
+ self.output_dir = output_dir or TRAINING_OUTPUT_DIR
47
+ self.device = device
48
+ self.tokenizer = None
49
+ self.model = None
50
+ self.chat_analyzer = ChatAnalyzer()
51
+
52
+ # Создаем директорию для результатов, если её нет
53
+ os.makedirs(self.output_dir, exist_ok=True)
54
+
55
+ def prepare_training_data(self, output_file: Optional[str] = None) -> str:
56
+ """
57
+ Подготовка данных для обучения
58
+
59
+ Args:
60
+ output_file: Путь к выходному файлу (если None, создается временный файл)
61
+
62
+ Returns:
63
+ Путь к файлу с данными для обучения
64
+ """
65
+ if output_file is None:
66
+ # Создаем временный файл для данных
67
+ temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".jsonl")
68
+ output_file = temp_file.name
69
+ temp_file.close()
70
+
71
+ # Экспортируем данные для обучения
72
+ success, message = self.chat_analyzer.export_training_data(output_file)
73
+
74
+ if not success:
75
+ raise ValueError(f"Ошибка при подготовке данных: {message}")
76
+
77
+ logger.info(message)
78
+ return output_file
79
+
80
+ def load_model_and_tokenizer(self):
81
+ """
82
+ Загрузка базовой модели и токенизатора
83
+ """
84
+ try:
85
+ logger.info(f"Загрузка модели {self.base_model_id}...")
86
+
87
+ # Загрузка токенизатора
88
+ self.tokenizer = AutoTokenizer.from_pretrained(
89
+ self.base_model_id,
90
+ trust_remote_code=True
91
+ )
92
+
93
+ # Специальные токены для диалогов
94
+ special_tokens = {
95
+ "pad_token": "<PAD>",
96
+ "eos_token": "</s>",
97
+ "bos_token": "<s>"
98
+ }
99
+
100
+ # Добавляем специальные токены, если их нет
101
+ for token_name, token_value in special_tokens.items():
102
+ if getattr(self.tokenizer, token_name) is None:
103
+ setattr(self.tokenizer, token_name, token_value)
104
+
105
+ # Загрузка модели
106
+ self.model = AutoModelForCausalLM.from_pretrained(
107
+ self.base_model_id,
108
+ trust_remote_code=True,
109
+ device_map="auto" if self.device == "cuda" else None
110
+ )
111
+
112
+ logger.info("Модель и токенизатор успешно загружены")
113
+ except Exception as e:
114
+ logger.error(f"Ошибка при загрузке модели: {str(e)}")
115
+ raise
116
+
117
+ def setup_lora_config(
118
+ self,
119
+ r: int = 16,
120
+ lora_alpha: int = 32,
121
+ lora_dropout: float = 0.05
122
+ ) -> LoraConfig:
123
+ """
124
+ Настройка конфигурации LoRA для эффективного дообучения
125
+
126
+ Args:
127
+ r: Ранг матриц LoRA
128
+ lora_alpha: Альфа параметр LoRA
129
+ lora_dropout: Вероятность dropout в LoRA слоях
130
+
131
+ Returns:
132
+ Конфигурация LoRA
133
+ """
134
+ # Создаем конфигурацию LoRA
135
+ lora_config = LoraConfig(
136
+ task_type=TaskType.CAUSAL_LM,
137
+ r=r,
138
+ lora_alpha=lora_alpha,
139
+ lora_dropout=lora_dropout,
140
+ bias="none",
141
+ target_modules=["q_proj", "v_proj", "k_proj", "o_proj"]
142
+ )
143
+
144
+ return lora_config
145
+
146
+ def prepare_model_for_training(self):
147
+ """
148
+ Подготовка модели к обучению с использованием LoRA
149
+ """
150
+ if self.model is None:
151
+ self.load_model_and_tokenizer()
152
+
153
+ # Настройка LoRA
154
+ lora_config = self.setup_lora_config()
155
+
156
+ # Применяем LoRA к модели
157
+ self.model = get_peft_model(self.model, lora_config)
158
+
159
+ # Вывод информации о параметрах
160
+ trainable_params = sum(p.numel() for p in self.model.parameters() if p.requires_grad)
161
+ all_params = sum(p.numel() for p in self.model.parameters())
162
+ logger.info(f"Обучаемых параметров: {trainable_params:,} из {all_params:,} ({trainable_params/all_params:.2%})")
163
+
164
+ def tokenize_dataset(self, dataset):
165
+ """
166
+ Токенизация датасета для обучения
167
+
168
+ Args:
169
+ dataset: Датасет для токенизации
170
+
171
+ Returns:
172
+ Токенизированный датасет
173
+ """
174
+ def tokenize_function(examples):
175
+ # Форматируем диалоги в единую строку
176
+ texts = []
177
+ for dialog in examples["messages"]:
178
+ text = ""
179
+ for message in dialog:
180
+ if message["role"] == "user":
181
+ text += f"User: {message['content']}\n"
182
+ elif message["role"] == "assistant":
183
+ text += f"Assistant: {message['content']}\n"
184
+ texts.append(text)
185
+
186
+ # Токенизируем тексты
187
+ tokenized = self.tokenizer(
188
+ texts,
189
+ padding="max_length",
190
+ truncation=True,
191
+ max_length=1024,
192
+ return_tensors="pt"
193
+ )
194
+
195
+ return tokenized
196
+
197
+ # Применяем функцию токенизации
198
+ tokenized_dataset = dataset.map(
199
+ tokenize_function,
200
+ batched=True,
201
+ remove_columns=["messages"]
202
+ )
203
+
204
+ return tokenized_dataset
205
+
206
+ def train(
207
+ self,
208
+ training_data_path: Optional[str] = None,
209
+ num_train_epochs: int = 3,
210
+ per_device_train_batch_size: int = 4,
211
+ gradient_accumulation_steps: int = 4,
212
+ learning_rate: float = 2e-4,
213
+ logging_steps: int = 10,
214
+ save_strategy: str = "epoch"
215
+ ) -> Tuple[bool, str]:
216
+ """
217
+ Запуск процесса дообучения модели
218
+
219
+ Args:
220
+ training_data_path: Путь к данным для обучения (если None, данные будут подготовлены автоматически)
221
+ num_train_epochs: Количество эпох обучения
222
+ per_device_train_batch_size: Размер батча на устройство
223
+ gradient_accumulation_steps: Количество шагов накопления градиента
224
+ learning_rate: Скорость обучения
225
+ logging_steps: Частота логирования
226
+ save_strategy: Стратегия сохранения модели
227
+
228
+ Returns:
229
+ (успех, сообщение)
230
+ """
231
+ try:
232
+ # Подготовка данных для обучения, если не указан путь
233
+ if training_data_path is None:
234
+ training_data_path = self.prepare_training_data()
235
+ temp_data = True
236
+ else:
237
+ temp_data = False
238
+
239
+ # Загрузка модели и токенизатора, если не загружены
240
+ if self.model is None or self.tokenizer is None:
241
+ self.load_model_and_tokenizer()
242
+
243
+ # Подготовка модели для обучения
244
+ self.prepare_model_for_training()
245
+
246
+ # Загрузка датасета
247
+ dataset = load_dataset("json", data_files=training_data_path, split="train")
248
+ logger.info(f"Загружено {len(dataset)} примеров из {training_data_path}")
249
+
250
+ # Токенизация датасета
251
+ tokenized_dataset = self.tokenize_dataset(dataset)
252
+
253
+ # Создание колатора данных
254
+ data_collator = DataCollatorForLanguageModeling(
255
+ tokenizer=self.tokenizer,
256
+ mlm=False
257
+ )
258
+
259
+ # Настройка аргументов обучения
260
+ training_args = TrainingArguments(
261
+ output_dir=self.output_dir,
262
+ num_train_epochs=num_train_epochs,
263
+ per_device_train_batch_size=per_device_train_batch_size,
264
+ gradient_accumulation_steps=gradient_accumulation_steps,
265
+ learning_rate=learning_rate,
266
+ weight_decay=0.01,
267
+ warmup_ratio=0.1,
268
+ logging_steps=logging_steps,
269
+ save_strategy=save_strategy,
270
+ save_total_limit=2,
271
+ remove_unused_columns=False,
272
+ push_to_hub=False,
273
+ report_to="tensorboard",
274
+ load_best_model_at_end=True
275
+ )
276
+
277
+ # Создание тренера
278
+ trainer = Trainer(
279
+ model=self.model,
280
+ args=training_args,
281
+ train_dataset=tokenized_dataset,
282
+ data_collator=data_collator,
283
+ tokenizer=self.tokenizer
284
+ )
285
+
286
+ # Запуск обучения
287
+ logger.info("Начало обучения модели...")
288
+ trainer.train()
289
+
290
+ # Сохранение модели
291
+ logger.info(f"Сохранение обученной модели в {self.output_dir}")
292
+ trainer.save_model(self.output_dir)
293
+ self.tokenizer.save_pretrained(self.output_dir)
294
+
295
+ # Удаляем временный файл, если он был создан
296
+ if temp_data and os.path.exists(training_data_path):
297
+ os.remove(training_data_path)
298
+
299
+ return True, f"Модель успешно обучена и сохранена в {self.output_dir}"
300
+ except Exception as e:
301
+ logger.error(f"Ошибка в процессе обучения: {str(e)}")
302
+ return False, f"Ошибка в процессе обучения: {str(e)}"
303
+
304
+ def upload_model_to_hub(
305
+ self,
306
+ repo_id: str,
307
+ private: bool = True,
308
+ token: Optional[str] = None
309
+ ) -> Tuple[bool, str]:
310
+ """
311
+ Загрузка обученной модели на Hugging Face Hub
312
+
313
+ Args:
314
+ repo_id: Идентификатор репозитория на Hugging Face Hub
315
+ private: Флаг приватности репозитория
316
+ token: Токен доступа к Hugging Face Hub
317
+
318
+ Returns:
319
+ (успех, сообщение)
320
+ """
321
+ try:
322
+ if not os.path.exists(os.path.join(self.output_dir, "pytorch_model.bin")):
323
+ return False, "Обученная модель не найдена. Сначала выполните обучение."
324
+
325
+ # Инициализация API
326
+ api = HfApi(token=token)
327
+
328
+ # Загрузка модели на Hub
329
+ api.create_repo(repo_id=repo_id, private=private, repo_type="model", exist_ok=True)
330
+ api.upload_folder(
331
+ folder_path=self.output_dir,
332
+ repo_id=repo_id,
333
+ repo_type="model"
334
+ )
335
+
336
+ return True, f"Модель успешно загружена на Hugging Face Hub: {repo_id}"
337
+ except Exception as e:
338
+ return False, f"Ошибка при загрузке модели на Hub: {str(e)}"
339
+
340
+ def finetune_from_chat_history(epochs: int = 3) -> Tuple[bool, str]:
341
+ """
342
+ Функция для запуска процесса дообучения на основе истории чатов
343
+
344
+ Args:
345
+ epochs: Количество эпох обучения
346
+
347
+ Returns:
348
+ (успех, сообщение)
349
+ """
350
+ # Анализ чатов и подготовка данных
351
+ analyzer = ChatAnalyzer()
352
+ report = analyzer.generate_analytics_report()
353
+
354
+ # Проверка наличия достаточного количества данных
355
+ if report["qa_pairs_count"] < 10:
356
+ return False, f"Недостаточно данных для дообучения. Найдено всего {report['qa_pairs_count']} пар вопрос-ответ."
357
+
358
+ # Создание и запуск процесса дообучения
359
+ tuner = FineTuner()
360
+ success, message = tuner.train(num_train_epochs=epochs)
361
+
362
+ return success, message
363
+
364
+ if __name__ == "__main__":
365
+ # Пример использования
366
+ success, message = finetune_from_chat_history()
367
+ print(message)
training/model_manager.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Модуль для управления моделями и их версиями
3
+ """
4
+
5
+ import os
6
+ import json
7
+ import shutil
8
+ from datetime import datetime
9
+ from typing import List, Dict, Any, Tuple, Optional
10
+ import logging
11
+ from huggingface_hub import HfApi, snapshot_download, hf_hub_download
12
+ from transformers import AutoModelForCausalLM, AutoTokenizer
13
+ from config.settings import MODEL_PATH, MODELS_REGISTRY_PATH
14
+
15
+ # Настройка логирования
16
+ logging.basicConfig(
17
+ level=logging.INFO,
18
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
19
+ )
20
+ logger = logging.getLogger(__name__)
21
+
22
+ class ModelManager:
23
+ def __init__(self, registry_path: Optional[str] = None):
24
+ """
25
+ Инициализация менеджера моделей
26
+
27
+ Args:
28
+ registry_path: Путь к реестру моделей
29
+ """
30
+ self.registry_path = registry_path or MODELS_REGISTRY_PATH
31
+ self.models_dir = MODEL_PATH
32
+
33
+ # Создаем директории, если их нет
34
+ os.makedirs(self.registry_path, exist_ok=True)
35
+ os.makedirs(self.models_dir, exist_ok=True)
36
+
37
+ # Путь к файлу реестра
38
+ self.registry_file = os.path.join(self.registry_path, "models_registry.json")
39
+
40
+ # Загружаем реестр или создаем новый
41
+ self.load_registry()
42
+
43
+ def load_registry(self):
44
+ """
45
+ Загрузка реестра моделей
46
+ """
47
+ if os.path.exists(self.registry_file):
48
+ try:
49
+ with open(self.registry_file, "r", encoding="utf-8") as f:
50
+ self.registry = json.load(f)
51
+ except Exception as e:
52
+ logger.error(f"Ошибка загрузки реестра моделей: {str(e)}")
53
+ self.registry = {"models": []}
54
+ else:
55
+ self.registry = {"models": []}
56
+
57
+ def save_registry(self):
58
+ """
59
+ Сохранение реестра моделей
60
+ """
61
+ try:
62
+ with open(self.registry_file, "w", encoding="utf-8") as f:
63
+ json.dump(self.registry, f, ensure_ascii=False, indent=2)
64
+ except Exception as e:
65
+ logger.error(f"Ошибка сохранения реестра моделей: {str(e)}")
66
+
67
+ def register_model(
68
+ self,
69
+ model_id: str,
70
+ version: str,
71
+ source: str,
72
+ description: str = "",
73
+ metrics: Optional[Dict[str, Any]] = None,
74
+ is_active: bool = False
75
+ ) -> Tuple[bool, str]:
76
+ """
77
+ Регистрация модели в реестре
78
+
79
+ Args:
80
+ model_id: Идентификатор модели (например, 'saiga_7b_lora')
81
+ version: Версия модели
82
+ source: Источник модели (например, URL или локальный путь)
83
+ description: Описание модели
84
+ metrics: Метрики качества модели
85
+ is_active: Флаг активности модели
86
+
87
+ Returns:
88
+ (успех, сообщение)
89
+ """
90
+ try:
91
+ # Создаем запись о модели
92
+ model_entry = {
93
+ "model_id": model_id,
94
+ "version": version,
95
+ "source": source,
96
+ "description": description,
97
+ "metrics": metrics or {},
98
+ "is_active": is_active,
99
+ "registration_date": datetime.now().isoformat(),
100
+ "local_path": os.path.join(self.models_dir, f"{model_id}_{version}")
101
+ }
102
+
103
+ # Проверяем, есть ли уже такая модель в реестре
104
+ for i, model in enumerate(self.registry["models"]):
105
+ if model["model_id"] == model_id and model["version"] == version:
106
+ # Обновляем существующую запись
107
+ self.registry["models"][i] = model_entry
108
+ self.save_registry()
109
+ return True, f"Мод