File size: 17,585 Bytes
d0d9388
 
ac29848
d0d9388
 
 
ac29848
d0d9388
 
ac29848
 
 
 
 
 
 
 
d0d9388
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ac29848
 
 
 
d0d9388
ac29848
 
 
 
 
 
 
 
 
 
d0d9388
ac29848
 
 
 
 
 
 
 
 
 
 
d0d9388
ac29848
d0d9388
ac29848
 
 
 
d0d9388
ac29848
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d0d9388
ac29848
d0d9388
ac29848
d0d9388
 
 
 
ac29848
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d0d9388
ac29848
 
d0d9388
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ac29848
d0d9388
 
ac29848
d0d9388
 
 
ac29848
d0d9388
 
ac29848
d0d9388
 
 
 
 
 
 
 
 
 
ac29848
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d0d9388
 
 
 
 
 
ac29848
d0d9388
 
 
ac29848
d0d9388
 
ac29848
 
d0d9388
 
 
 
 
 
 
ac29848
 
d0d9388
 
 
 
 
 
ac29848
 
 
d0d9388
ac29848
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d0d9388
ac29848
 
 
 
 
 
 
d0d9388
 
 
 
 
 
 
 
 
ac29848
 
d0d9388
 
 
 
 
 
 
 
 
 
ac29848
d0d9388
 
 
 
 
ac29848
d0d9388
 
 
 
 
 
ac29848
 
d0d9388
 
 
 
 
 
 
 
 
 
 
 
 
ac29848
 
d0d9388
 
 
ac29848
d0d9388
 
 
ac29848
d0d9388
ac29848
d0d9388
ac29848
 
d0d9388
ac29848
d0d9388
ac29848
d0d9388
ac29848
d0d9388
ac29848
d0d9388
ac29848
d0d9388
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
import torch
import time
import re
from transformers import pipeline, AutoTokenizer, AutoModelForSeq2SeqLM, Trainer, TrainingArguments
from datasets import Dataset 
import pandas as pd
from config import MODEL_MAP, BATCH_SIZE_LIMIT, DATASET_PATH, FINETUNED_MODEL_BASE_DIR, DEFAULT_MODEL_CHOICE
from utils import preprocess_text, load_iwslt_dataset, preprocess_pair
import os 
import json 

# --- THÊM CÁC IMPORTS MỚI CHO ĐÁNH GIÁ METRIC ---
import evaluate
import numpy as np

# Tải metric BLEU một lần
metric_bleu = evaluate.load("sacrebleu") 

class TranslationEngine:
    def __init__(self):
        self.translators = {}
        self.tokenizers = {}
        self.models = {}
        self.train_data, self.val_data, self.test_data = self.load_dataset()

    def load_dataset(self):
        """Load IWSLT15 dataset for training or evaluation"""
        train_data, val_data, test_data = load_iwslt_dataset(DATASET_PATH)
        if train_data is None:
            print("Failed to load dataset. Using default behavior without dataset.")
            return None, None, None
        return train_data, val_data, test_data

    def load_model_and_tokenizer(self, model_choice_display_name): # Sử dụng tên tham số nhất quán
        # Kiểm tra xem mô hình này đã được tải vào bộ nhớ chưa
        if model_choice_display_name in self.translators:
            return self.translators[model_choice_display_name], self.tokenizers[model_choice_display_name]

        print(f"Loading model: {model_choice_display_name}")
        
        # Lấy thông tin model từ MODEL_MAP
        model_info = MODEL_MAP.get(model_choice_display_name)
        if not model_info:
            print(f"  !!! ERROR: Model '{model_choice_display_name}' not found in MODEL_MAP. Falling back to default.")
            model_info = MODEL_MAP[DEFAULT_MODEL_CHOICE] # Fallback nếu tên không hợp lệ
        
        model_type = model_info["type"]
        model_identifier = model_info["identifier"] # Có thể là ID HF hoặc tên thư mục cục bộ

        tokenizer = None
        model = None
        
        if model_type == "hub":
            # Tải mô hình từ Hugging Face Hub (pretrained)
            print(f"  --> Loading pre-trained model from Hugging Face Hub: {model_identifier}")
            tokenizer = AutoTokenizer.from_pretrained(model_identifier)
            model = AutoModelForSeq2SeqLM.from_pretrained(model_identifier)
        elif model_type == "local_finetuned":
            # Tải mô hình đã fine-tuned từ local
            finetuned_model_path_local = os.path.join(FINETUNED_MODEL_BASE_DIR, model_identifier)
            
            print(f"  --> Loading fine-tuned model from local directory: {finetuned_model_path_local}")
            
            # --- Gỡ lỗi chi tiết các file quan trọng ---
            config_json_path = os.path.join(finetuned_model_path_local, "config.json")
            tokenizer_config_json_path = os.path.join(finetuned_model_path_local, "tokenizer_config.json")
            model_safetensors_path = os.path.join(finetuned_model_path_local, "model.safetensors")
            
            print(f"  - config.json exists: {os.path.exists(config_json_path)}")
            print(f"  - tokenizer_config.json exists: {os.path.exists(tokenizer_config_json_path)}")
            print(f"  - model.safetensors exists: {os.path.exists(model_safetensors_path)}")
            print(f"  - source.spm exists: {os.path.exists(os.path.join(finetuned_model_path_local, 'source.spm'))}")
            print(f"  - target.spm exists: {os.path.exists(os.path.join(finetuned_model_path_local, 'target.spm'))}")

            try:
                # Cố gắng tải CẢ tokenizer VÀ model từ thư mục fine-tuned cục bộ
                tokenizer = AutoTokenizer.from_pretrained(finetuned_model_path_local)
                model = AutoModelForSeq2SeqLM.from_pretrained(finetuned_model_path_local)
                print(f"  --> Successfully loaded fine-tuned model and tokenizer from local path.")
            except Exception as e:
                print(f"  !!! ERROR: Failed to load fine-tuned model/tokenizer from '{finetuned_model_path_local}': {e}")
                print(f"  --> Falling back to loading original pre-trained model: {MODEL_MAP[DEFAULT_MODEL_CHOICE]['identifier']}")
                # Nếu tải fine-tuned thất bại, tải lại mô hình pretrained gốc
                tokenizer = AutoTokenizer.from_pretrained(MODEL_MAP[DEFAULT_MODEL_CHOICE]['identifier'])
                model = AutoModelForSeq2SeqLM.from_pretrained(MODEL_MAP[DEFAULT_MODEL_CHOICE]['identifier'])
        else:
            # Nếu thư mục fine-tuned model không tồn tại
            print(f"  --> Local fine-tuned directory '{finetuned_model_path_local}' not found. Falling back to pre-trained model: {MODEL_MAP[DEFAULT_MODEL_CHOICE]['identifier']}")
            tokenizer = AutoTokenizer.from_pretrained(MODEL_MAP[DEFAULT_MODEL_CHOICE]['identifier'])
            model = AutoModelForSeq2SeqLM.from_pretrained(MODEL_MAP[DEFAULT_MODEL_CHOICE]['identifier'])
        
        # --- KẾT THÚC LOGIC TẢI MÔ HÌNH ---
        
        device = 0 if torch.cuda.is_available() else -1
        if device == 0:
            model = model.cuda()
            print("Using GPU for faster inference")
        
        translator = pipeline(
            "translation",
            model=model,
            tokenizer=tokenizer,
            device=device,
            batch_size=BATCH_SIZE_LIMIT,
            torch_dtype=torch.float16 if device == 0 and torch.cuda.is_available() else None
        )
        
        self.translators[model_choice_display_name] = translator
        self.tokenizers[model_choice_display_name] = tokenizer
        self.models[model_choice_display_name] = model
        
        print(f"Model {model_choice_display_name} loaded successfully")
        
        return self.translators[model_choice_display_name], self.tokenizers[model_choice_display_name]

    def fine_tune_model(self, model_choice_display_name, output_dir_base=FINETUNED_MODEL_BASE_DIR): # Tham số này giờ là display name
        if not self.train_data or not self.val_data:
            print("Dataset not loaded or missing validation split. Cannot fine-tune model.")
            return

        # Xác định ID mô hình gốc (từ display name của bản fine-tuned) để tạo thư mục lưu
        # Chúng ta giả định rằng model_choice_display_name sẽ là "English to Vietnamese (OpusMT) - Fine-tuned"
        # và identifier của nó sẽ là "English to Vietnamese (OpusMT)" như trong config.py
        if model_choice_display_name == "English to Vietnamese (OpusMT) - Fine-tuned":
            model_identifier_for_save = MODEL_MAP[model_choice_display_name]["identifier"]
        else:
            # Fallback nếu người dùng chọn model khác để fine-tune (ví dụ: bản pretrained)
            # Trong trường hợp này, chúng ta sẽ lưu nó theo identifier của nó.
            model_identifier_for_save = MODEL_MAP[model_choice_display_name]["identifier"]

        # Tải mô hình và tokenizer ban đầu (sẽ được dùng để fine-tune)
        # load_model_and_tokenizer sẽ tải mô hình gốc (pretrained) khi bắt đầu fine-tune
        # vì chúng ta muốn fine-tune từ một điểm khởi đầu sạch.
        # Sử dụng tên hiển thị của bản pretrained để tải đúng model_id gốc
        pretrained_display_name = "English to Vietnamese (OpusMT) - Pretrained"
        translator, tokenizer = self.load_model_and_tokenizer(pretrained_display_name) 
        model = self.models[pretrained_display_name] # Lấy instance mô hình đã được tải

        # Đường dẫn lưu model đã fine-tuned cụ thể
        current_finetune_output_dir = os.path.join(output_dir_base, model_identifier_for_save)
        
        if not os.path.exists(current_finetune_output_dir):
            os.makedirs(current_finetune_output_dir)
            print(f"Đã tạo thư mục lưu mô hình fine-tuned: {current_finetune_output_dir}")

        def tokenize_data(example):
            en_texts_batch = []
            vi_texts_batch = []
            
            for pair_dict in example["translation"]:
                en_text, vi_text = preprocess_pair(pair_dict["en"], pair_dict["vi"])
                en_texts_batch.append(en_text)
                vi_texts_batch.append(vi_text)
            
            inputs = tokenizer(
                en_texts_batch,
                truncation=True,
                padding="max_length",
                max_length=128, 
                return_tensors="pt"
            )
            labels = tokenizer(
                vi_texts_batch,
                truncation=True,
                padding="max_length",
                max_length=128, 
                return_tensors="pt"
            )
            inputs["labels"] = labels["input_ids"]
            return inputs

        print("Tokenizing training data...")
        tokenized_train = self.train_data.map(tokenize_data, batched=True, remove_columns=self.train_data.column_names)
        print("Tokenizing validation data...")
        tokenized_val = self.val_data.map(tokenize_data, batched=True, remove_columns=self.val_data.column_names)

        def compute_metrics(eval_preds):
            preds, labels = eval_preds
            if isinstance(preds, tuple):
                preds = preds[0]
            
            decoded_preds = tokenizer.batch_decode(preds, skip_special_tokens=True)
            labels = np.where(labels != -100, labels, tokenizer.pad_token_id)
            decoded_labels = tokenizer.batch_decode(labels, skip_special_tokens=True)

            decoded_preds = [pred.strip() for pred in decoded_preds]
            decoded_labels = [[label.strip()] for label in decoded_labels] 

            result = metric_bleu.compute(predictions=decoded_preds, references=decoded_labels)
            
            result = {"bleu": round(result["score"], 2)}
            return result

        training_args = TrainingArguments(
            output_dir=current_finetune_output_dir,
            evaluation_strategy="epoch",
            learning_rate=2e-5,
            per_device_train_batch_size=8,
            per_device_eval_batch_size=8,
            num_train_epochs=1, 
            weight_decay=0.01,
            save_strategy="epoch",
            load_best_model_at_end=True,
            logging_dir=f'{current_finetune_output_dir}/logs',
            logging_steps=500,
            save_total_limit=1,
            report_to="none", 
            predict_with_generate=True,
        )

        trainer = Trainer(
            model=model,
            args=training_args,
            train_dataset=tokenized_train,
            eval_dataset=tokenized_val,
            tokenizer=tokenizer,          
            compute_metrics=compute_metrics, 
        )

        print("Starting fine-tuning...")
        trainer.train()
        print(f"Fine-tuning completed. Model saved to {current_finetune_output_dir}")

        trainer.save_model(current_finetune_output_dir)
        tokenizer.save_pretrained(current_finetune_output_dir)
        print(f"Mô hình và tokenizer đã được lưu đầy đủ vào: {current_finetune_output_dir}")

        # Sau khi fine-tune xong, cập nhật pipeline cho bản fine-tuned để sử dụng ngay lập tức
        print(f"Reloading fine-tuned model into pipeline from {current_finetune_output_dir} for immediate inference...")
        try:
            reloaded_tokenizer = AutoTokenizer.from_pretrained(current_finetune_output_dir)
            reloaded_model = AutoModelForSeq2SeqLM.from_pretrained(current_finetune_output_dir)
            if torch.cuda.is_available():
                reloaded_model = reloaded_model.cuda()
            
            # CẬP NHẬT TRÌNH DỊCH VỚI TÊN HIỂN THỊ CỦA BẢN FINE-TUNED (model_choice_display_name)
            self.translators[model_choice_display_name] = pipeline(
                "translation",
                model=reloaded_model, 
                tokenizer=reloaded_tokenizer,
                device=0 if torch.cuda.is_available() else -1,
                batch_size=BATCH_SIZE_LIMIT,
                torch_dtype=torch.float16 if torch.cuda.is_available() else None
            )
            self.models[model_choice_display_name] = reloaded_model 
            self.tokenizers[model_choice_display_name] = reloaded_tokenizer 
            print(f"Fine-tuned model for '{model_choice_display_name}' reloaded successfully for inference.")
        except Exception as e_reload:
            print(f"!!! CRITICAL: Failed to reload saved model for immediate inference: {e_reload}")
            # Nếu không thể tải lại bản fine-tuned, hãy đảm bảo tải bản pretrained để app không crash
            print(f"!!! Reverting to pre-trained model for current inference.")
            pretrained_info = MODEL_MAP["English to Vietnamese (OpusMT) - Pretrained"]
            tokenizer = AutoTokenizer.from_pretrained(pretrained_info["identifier"])
            model = AutoModelForSeq2SeqLM.from_pretrained(pretrained_info["identifier"])
            if torch.cuda.is_available():
                model = model.cuda()
            self.translators[model_choice_display_name] = pipeline(
                "translation", model=model, tokenizer=tokenizer, device=0 if torch.cuda.is_available() else -1,
                batch_size=BATCH_SIZE_LIMIT, torch_dtype=torch.float16 if torch.cuda.is_available() else None
            )
            self.models[model_choice_display_name] = model
            self.tokenizers[model_choice_display_name] = tokenizer


    def clean_vietnamese_output(self, text):
        if not text:
            return ""
        text = text.replace("_", " ")
        text = re.sub(r"\s+", " ", text).strip()
        return text

    def translate_batch(self, texts, translator, max_length, num_beams=1):
        try:
            results = translator(
                texts,
                max_length=max_length,
                num_beams=num_beams,
                batch_size=BATCH_SIZE_LIMIT
            )
            cleaned_results = [self.clean_vietnamese_output(r["translation_text"]) for r in results]
            return cleaned_results
        except Exception as e:
            print(f"Batch translation error: {e}")
            results = []
            for text in texts:
                try:
                    result = translator(
                        text,
                        max_length=max_length,
                        num_beams=num_beams
                    )[0]["translation_text"]
                    results.append(self.clean_vietnamese_output(result)) 
                except Exception as e2:
                    print(f"Individual translation error for '{text}': {e2}")
                    results.append(f"Translation error: {str(e2)}")
            return results

    def translate_single_text(self, text, model_choice_display_name, max_length=512, num_beams=1): # Đổi tên tham số
        if not text:
            return None
        
        start_time_total = time.time()
        processed_text = preprocess_text(text) 
        
        # Gọi load_model_and_tokenizer với tên hiển thị
        translator, tokenizer = self.load_model_and_tokenizer(model_choice_display_name) 
        
        tokens = tokenizer(processed_text, return_tensors="pt", truncation=False)
        if len(tokens["input_ids"][0]) > max_length:
            sentences = [s.strip() + '.' for s in processed_text.split('.') if s.strip()]
            if not sentences and processed_text.strip():
                 sentences = [processed_text.strip()]
            
            results = self.translate_batch(sentences, translator, max_length, num_beams)
            result = ' '.join(results)
        else:
            results = self.translate_batch([processed_text], translator, max_length, num_beams)
            result = results[0]
        
        result = self.clean_vietnamese_output(result)
        
        print(f"Translation completed in: {time.time() - start_time_total:.2f}s")
        return result

    def translate_file_content(self, texts, model_choice_display_name, max_length=512, num_beams=1): # Đổi tên tham số
        start_time = time.time()
        
        processed_texts = [preprocess_text(t) for t in texts]
        translator, _ = self.load_model_and_tokenizer(model_choice_display_name) # Gọi với tên hiển thị
        
        translated_results_raw = self.translate_batch(processed_texts, translator, max_length, num_beams)
        
        translated_results_cleaned = [self.clean_vietnamese_output(r) for r in translated_results_raw]

        print(f"File translation completed in: {time.time() - start_time:.2f}s")
        return list(zip(processed_texts, translated_results_cleaned))

    def warm_up_model(self, model_choice_display_name): # Đổi tên tham số
        try:
            translator, _ = self.load_model_and_tokenizer(model_choice_display_name) # Gọi với tên hiển thị
            _ = translator("Hello", max_length=50, num_beams=1)
            print(f"Model {model_choice_display_name} warmed up successfully")
        except Exception as e:
            print(f"Warm-up failed for {model_choice_display_name}: {e}")

    def get_loaded_models(self):
        return list(self.translators.keys())