Upload model source code
Browse files- src/1_build_shared_vocab.py +214 -0
- src/2_encode_data.py +335 -0
- src/__init__.py +0 -0
- src/add_direction_tokens.py +62 -0
- src/app.py +413 -0
- src/bidirectional_translate.py +546 -0
- src/checkpoint_manager.py +251 -0
- src/complete_transformer.py +539 -0
- src/config.py +36 -0
- src/create_bidirectional_dataset.py +91 -0
- src/data_preprocessing.py +468 -0
- src/dataloader_module.py +420 -0
- src/encode_mtet_bidirectional.py +240 -0
- src/finetune.py +520 -0
- src/finetune_hospital.py +343 -0
- src/gpu_safety.py +197 -0
- src/inference_evaluation.py +563 -0
- src/inference_vlsp.py +278 -0
- src/main_pipeline.py +550 -0
- src/prepare_hospital_data.py +204 -0
- src/shared_vocab_utils.py +338 -0
- src/train_mtet_bidirectional.py +347 -0
- src/training_module.py +771 -0
- src/transformer_components.py +378 -0
- src/transformer_encoder_decoder.py +403 -0
src/1_build_shared_vocab.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
BƯỚC 1: TẠO BỘ TỪ ĐIỂN CHUNG (SHARED VOCABULARY)
|
| 3 |
+
Train BPE tokenizer từ text gốc để tạo shared vocabulary cho cả tiếng Việt và tiếng Anh
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from tokenizers import Tokenizer, models, pre_tokenizers, decoders, trainers
|
| 8 |
+
|
| 9 |
+
# ============================================================================
|
| 10 |
+
# PATH CONFIGURATION
|
| 11 |
+
# ============================================================================
|
| 12 |
+
|
| 13 |
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
| 14 |
+
DATA_DIR = PROJECT_ROOT / 'data'
|
| 15 |
+
RAW_DATA_DIR = DATA_DIR / 'raw'
|
| 16 |
+
PROCESSED_DATA_DIR = DATA_DIR / 'processed'
|
| 17 |
+
|
| 18 |
+
# ============================================================================
|
| 19 |
+
# BUILD SHARED VOCABULARY
|
| 20 |
+
# ============================================================================
|
| 21 |
+
|
| 22 |
+
def build_shared_vocab(
|
| 23 |
+
vocab_size=32000,
|
| 24 |
+
min_frequency=2,
|
| 25 |
+
special_tokens=None,
|
| 26 |
+
output_path=None
|
| 27 |
+
):
|
| 28 |
+
"""
|
| 29 |
+
Train BPE tokenizer từ text files để tạo shared vocabulary
|
| 30 |
+
|
| 31 |
+
Args:
|
| 32 |
+
vocab_size: Kích thước vocabulary (mặc định 32000)
|
| 33 |
+
min_frequency: Tần suất tối thiểu để giữ lại token
|
| 34 |
+
special_tokens: List special tokens [PAD, SOS, EOS, UNK]
|
| 35 |
+
output_path: Đường dẫn lưu tokenizer
|
| 36 |
+
|
| 37 |
+
Returns:
|
| 38 |
+
tokenizer: Trained tokenizer
|
| 39 |
+
"""
|
| 40 |
+
if special_tokens is None:
|
| 41 |
+
# Quan trọng: Đặt Special Tokens ĐÚNG THỨ TỰ
|
| 42 |
+
# 0=PAD, 1=SOS, 2=EOS, 3=UNK (phải khớp với code hiện tại)
|
| 43 |
+
special_tokens = ["<pad>", "<sos>", "<eos>", "<unk>"]
|
| 44 |
+
|
| 45 |
+
if output_path is None:
|
| 46 |
+
output_path = PROCESSED_DATA_DIR / 'tokenizer_shared.json'
|
| 47 |
+
|
| 48 |
+
print("="*70)
|
| 49 |
+
print("TẠO SHARED VOCABULARY (BPE TOKENIZER)")
|
| 50 |
+
print("="*70)
|
| 51 |
+
|
| 52 |
+
# 1. Cấu hình Tokenizer BPE
|
| 53 |
+
print("\n1. Khởi tạo BPE Tokenizer...")
|
| 54 |
+
tokenizer = Tokenizer(models.BPE(unk_token="<unk>"))
|
| 55 |
+
|
| 56 |
+
# Pre-tokenizer: ByteLevel (xử lý Unicode tốt)
|
| 57 |
+
tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False)
|
| 58 |
+
tokenizer.decoder = decoders.ByteLevel()
|
| 59 |
+
|
| 60 |
+
# 2. Trainer configuration
|
| 61 |
+
print(f"2. Cấu hình trainer:")
|
| 62 |
+
print(f" - Vocab size: {vocab_size}")
|
| 63 |
+
print(f" - Min frequency: {min_frequency}")
|
| 64 |
+
print(f" - Special tokens: {special_tokens}")
|
| 65 |
+
|
| 66 |
+
trainer = trainers.BpeTrainer(
|
| 67 |
+
vocab_size=vocab_size,
|
| 68 |
+
special_tokens=special_tokens,
|
| 69 |
+
min_frequency=min_frequency,
|
| 70 |
+
show_progress=True
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
# 3. Tìm file text gốc
|
| 74 |
+
# Ưu tiên: MTet CSV (nếu có MTET_MAX_ROWS) > Raw files > cleaned_data.json
|
| 75 |
+
files = []
|
| 76 |
+
|
| 77 |
+
# Option 0: Dùng MTet CSV nếu có MTET_MAX_ROWS set
|
| 78 |
+
from data_preprocessing import CUSTOM_CSV_PATH, CUSTOM_CSV_MAX_ROWS
|
| 79 |
+
if CUSTOM_CSV_PATH.exists() and CUSTOM_CSV_MAX_ROWS and CUSTOM_CSV_MAX_ROWS > 0:
|
| 80 |
+
print(f"\n3. Ưu tiên dùng MTet CSV với {CUSTOM_CSV_MAX_ROWS:,} câu:")
|
| 81 |
+
print(f" - {CUSTOM_CSV_PATH}")
|
| 82 |
+
print(f" Tạo temporary text files từ CSV...")
|
| 83 |
+
|
| 84 |
+
import json
|
| 85 |
+
import tempfile
|
| 86 |
+
import pandas as pd
|
| 87 |
+
|
| 88 |
+
# Đọc CSV
|
| 89 |
+
df = pd.read_csv(CUSTOM_CSV_PATH, nrows=CUSTOM_CSV_MAX_ROWS)
|
| 90 |
+
if {'src', 'tgt'}.issubset(df.columns):
|
| 91 |
+
df = df.rename(columns={'src': 'vi', 'tgt': 'en'})
|
| 92 |
+
elif {'vi', 'en'}.issubset(df.columns):
|
| 93 |
+
pass
|
| 94 |
+
else:
|
| 95 |
+
raise ValueError("CSV cần có cột 'src'/'tgt' hoặc 'vi'/'en'")
|
| 96 |
+
|
| 97 |
+
df = df[['vi', 'en']].dropna().reset_index(drop=True)
|
| 98 |
+
|
| 99 |
+
# Tạo temp files
|
| 100 |
+
temp_dir = Path(tempfile.mkdtemp())
|
| 101 |
+
temp_vi = temp_dir / 'train.vi'
|
| 102 |
+
temp_en = temp_dir / 'train.en'
|
| 103 |
+
|
| 104 |
+
with open(temp_vi, 'w', encoding='utf-8') as f_vi, \
|
| 105 |
+
open(temp_en, 'w', encoding='utf-8') as f_en:
|
| 106 |
+
for _, row in df.iterrows():
|
| 107 |
+
f_vi.write(str(row['vi']).strip() + '\n')
|
| 108 |
+
f_en.write(str(row['en']).strip() + '\n')
|
| 109 |
+
|
| 110 |
+
files = [str(temp_vi), str(temp_en)]
|
| 111 |
+
print(f" ✓ Đã tạo temp files: {len(df):,} cặp câu từ MTet CSV")
|
| 112 |
+
|
| 113 |
+
# Option 1: Dùng raw files
|
| 114 |
+
elif (RAW_DATA_DIR / 'train.vi').exists() and (RAW_DATA_DIR / 'train.en').exists():
|
| 115 |
+
train_vi = RAW_DATA_DIR / 'train.vi'
|
| 116 |
+
train_en = RAW_DATA_DIR / 'train.en'
|
| 117 |
+
files = [str(train_vi), str(train_en)]
|
| 118 |
+
print(f"\n3. Tìm thấy raw files:")
|
| 119 |
+
print(f" - {train_vi}")
|
| 120 |
+
print(f" - {train_en}")
|
| 121 |
+
|
| 122 |
+
# Option 2: Fallback - dùng cleaned_data.json
|
| 123 |
+
else:
|
| 124 |
+
# Dùng cleaned_data.json nếu có
|
| 125 |
+
cleaned_data_path = PROCESSED_DATA_DIR / 'cleaned_data.json'
|
| 126 |
+
if cleaned_data_path.exists():
|
| 127 |
+
print(f"\n3. Không tìm thấy raw files, dùng cleaned_data.json")
|
| 128 |
+
print(f" Tạo temporary text files từ cleaned_data.json...")
|
| 129 |
+
|
| 130 |
+
import json
|
| 131 |
+
import tempfile
|
| 132 |
+
|
| 133 |
+
with open(cleaned_data_path, 'r', encoding='utf-8') as f:
|
| 134 |
+
cleaned_data = json.load(f)
|
| 135 |
+
|
| 136 |
+
# Tạo temp files
|
| 137 |
+
temp_dir = Path(tempfile.mkdtemp())
|
| 138 |
+
temp_vi = temp_dir / 'train.vi'
|
| 139 |
+
temp_en = temp_dir / 'train.en'
|
| 140 |
+
|
| 141 |
+
with open(temp_vi, 'w', encoding='utf-8') as f_vi, \
|
| 142 |
+
open(temp_en, 'w', encoding='utf-8') as f_en:
|
| 143 |
+
for item in cleaned_data.get('train', []):
|
| 144 |
+
f_vi.write(item['vi'] + '\n')
|
| 145 |
+
f_en.write(item['en'] + '\n')
|
| 146 |
+
|
| 147 |
+
files = [str(temp_vi), str(temp_en)]
|
| 148 |
+
print(f" ✓ Đã tạo temp files: {len(cleaned_data.get('train', []))} cặp câu")
|
| 149 |
+
else:
|
| 150 |
+
raise FileNotFoundError(
|
| 151 |
+
f"Không tìm thấy dữ liệu để train tokenizer!\n"
|
| 152 |
+
f"Vui lòng đảm bảo có một trong các file sau:\n"
|
| 153 |
+
f" - {train_vi} và {train_en}\n"
|
| 154 |
+
f" - {cleaned_data_path}"
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
# 4. Train tokenizer
|
| 158 |
+
print(f"\n4. Đang train tokenizer từ {len(files)} file(s)...")
|
| 159 |
+
print(f" (Quá trình này có thể mất 5-10 phút tùy kích thước dữ liệu)")
|
| 160 |
+
|
| 161 |
+
tokenizer.train(files, trainer)
|
| 162 |
+
|
| 163 |
+
# 5. Lưu tokenizer
|
| 164 |
+
PROCESSED_DATA_DIR.mkdir(parents=True, exist_ok=True)
|
| 165 |
+
tokenizer.save(str(output_path))
|
| 166 |
+
|
| 167 |
+
print(f"\n5. ✓ Đã lưu tokenizer: {output_path}")
|
| 168 |
+
|
| 169 |
+
# 6. Kiểm tra special tokens
|
| 170 |
+
print(f"\n6. Kiểm tra Special Tokens:")
|
| 171 |
+
print(f" - PAD (<pad>): ID {tokenizer.token_to_id('<pad>')} (phải là 0)")
|
| 172 |
+
print(f" - SOS (<sos>): ID {tokenizer.token_to_id('<sos>')} (phải là 1)")
|
| 173 |
+
print(f" - EOS (<eos>): ID {tokenizer.token_to_id('<eos>')} (phải là 2)")
|
| 174 |
+
print(f" - UNK (<unk>): ID {tokenizer.token_to_id('<unk>')} (phải là 3)")
|
| 175 |
+
|
| 176 |
+
# 7. Thống kê
|
| 177 |
+
vocab_size_actual = tokenizer.get_vocab_size()
|
| 178 |
+
print(f"\n7. Thống kê:")
|
| 179 |
+
print(f" - Vocab size thực tế: {vocab_size_actual}")
|
| 180 |
+
print(f" - File output: {output_path}")
|
| 181 |
+
|
| 182 |
+
print("\n" + "="*70)
|
| 183 |
+
print("✓ HOÀN TẤT TẠO SHARED VOCABULARY!")
|
| 184 |
+
print("="*70)
|
| 185 |
+
|
| 186 |
+
return tokenizer
|
| 187 |
+
|
| 188 |
+
# ============================================================================
|
| 189 |
+
# MAIN
|
| 190 |
+
# ============================================================================
|
| 191 |
+
|
| 192 |
+
if __name__ == "__main__":
|
| 193 |
+
import argparse
|
| 194 |
+
|
| 195 |
+
parser = argparse.ArgumentParser(description='Build Shared Vocabulary')
|
| 196 |
+
parser.add_argument('--vocab_size', type=int, default=32000,
|
| 197 |
+
help='Vocabulary size (default: 32000)')
|
| 198 |
+
parser.add_argument('--min_frequency', type=int, default=2,
|
| 199 |
+
help='Minimum token frequency (default: 2)')
|
| 200 |
+
parser.add_argument('--output', type=str, default=None,
|
| 201 |
+
help='Output path for tokenizer (default: data/processed/tokenizer_shared.json)')
|
| 202 |
+
|
| 203 |
+
args = parser.parse_args()
|
| 204 |
+
|
| 205 |
+
tokenizer = build_shared_vocab(
|
| 206 |
+
vocab_size=args.vocab_size,
|
| 207 |
+
min_frequency=args.min_frequency,
|
| 208 |
+
output_path=args.output
|
| 209 |
+
)
|
| 210 |
+
|
| 211 |
+
print("\n📝 BƯỚC TIẾP THEO:")
|
| 212 |
+
print(" Chạy: python src/2_encode_data.py")
|
| 213 |
+
print(" để encode lại dữ liệu với shared vocabulary này")
|
| 214 |
+
|
src/2_encode_data.py
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
BƯỚC 2: ENCODE LẠI DỮ LIỆU VỚI SHARED VOCABULARY
|
| 3 |
+
Dùng tokenizer_shared.json để encode lại tất cả dữ liệu từ text gốc
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import json
|
| 7 |
+
import pickle
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from tokenizers import Tokenizer
|
| 10 |
+
from tqdm import tqdm
|
| 11 |
+
|
| 12 |
+
# ============================================================================
|
| 13 |
+
# PATH CONFIGURATION
|
| 14 |
+
# ============================================================================
|
| 15 |
+
|
| 16 |
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
| 17 |
+
DATA_DIR = PROJECT_ROOT / 'data'
|
| 18 |
+
RAW_DATA_DIR = DATA_DIR / 'raw'
|
| 19 |
+
PROCESSED_DATA_DIR = DATA_DIR / 'processed'
|
| 20 |
+
|
| 21 |
+
# ============================================================================
|
| 22 |
+
# ENCODE DATA WITH SHARED VOCABULARY
|
| 23 |
+
# ============================================================================
|
| 24 |
+
|
| 25 |
+
def encode_file_with_shared_vocab(src_file, tgt_file, tokenizer, split_name='train'):
|
| 26 |
+
"""
|
| 27 |
+
Encode một cặp file (source và target) với shared tokenizer
|
| 28 |
+
|
| 29 |
+
Args:
|
| 30 |
+
src_file: Path to source file (Vietnamese)
|
| 31 |
+
tgt_file: Path to target file (English)
|
| 32 |
+
tokenizer: Shared tokenizer
|
| 33 |
+
split_name: Name of split (train/validation/test)
|
| 34 |
+
|
| 35 |
+
Returns:
|
| 36 |
+
data: List of (src_ids, tgt_ids) tuples
|
| 37 |
+
"""
|
| 38 |
+
print(f"\nĐang encode {split_name} set...")
|
| 39 |
+
|
| 40 |
+
# Lấy special token IDs
|
| 41 |
+
SOS_ID = tokenizer.token_to_id("<sos>") # Thường là 1
|
| 42 |
+
EOS_ID = tokenizer.token_to_id("<eos>") # Thường là 2
|
| 43 |
+
|
| 44 |
+
if SOS_ID is None or EOS_ID is None:
|
| 45 |
+
raise ValueError("Không tìm thấy <sos> hoặc <eos> trong tokenizer!")
|
| 46 |
+
|
| 47 |
+
data = []
|
| 48 |
+
|
| 49 |
+
# Đọc và encode từng cặp câu
|
| 50 |
+
with open(src_file, 'r', encoding='utf-8') as f_src, \
|
| 51 |
+
open(tgt_file, 'r', encoding='utf-8') as f_tgt:
|
| 52 |
+
|
| 53 |
+
src_lines = f_src.readlines()
|
| 54 |
+
tgt_lines = f_tgt.readlines()
|
| 55 |
+
|
| 56 |
+
if len(src_lines) != len(tgt_lines):
|
| 57 |
+
print(f"⚠️ Warning: Số dòng không khớp ({len(src_lines)} vs {len(tgt_lines)})")
|
| 58 |
+
min_len = min(len(src_lines), len(tgt_lines))
|
| 59 |
+
src_lines = src_lines[:min_len]
|
| 60 |
+
tgt_lines = tgt_lines[:min_len]
|
| 61 |
+
|
| 62 |
+
# Encode từng cặp
|
| 63 |
+
for src_text, tgt_text in tqdm(zip(src_lines, tgt_lines),
|
| 64 |
+
total=len(src_lines),
|
| 65 |
+
desc=f"Encoding {split_name}"):
|
| 66 |
+
# Clean và encode
|
| 67 |
+
src_text = src_text.strip()
|
| 68 |
+
tgt_text = tgt_text.strip()
|
| 69 |
+
|
| 70 |
+
if not src_text or not tgt_text:
|
| 71 |
+
continue
|
| 72 |
+
|
| 73 |
+
# Encode text -> IDs
|
| 74 |
+
src_encoded = tokenizer.encode(src_text)
|
| 75 |
+
tgt_encoded = tokenizer.encode(tgt_text)
|
| 76 |
+
|
| 77 |
+
src_ids = src_encoded.ids
|
| 78 |
+
tgt_ids = tgt_encoded.ids
|
| 79 |
+
|
| 80 |
+
# Thêm SOS và EOS thủ công để đảm bảo đúng format
|
| 81 |
+
# Format: [SOS, ...token_ids..., EOS]
|
| 82 |
+
src_full = [SOS_ID] + src_ids + [EOS_ID]
|
| 83 |
+
tgt_full = [SOS_ID] + tgt_ids + [EOS_ID]
|
| 84 |
+
|
| 85 |
+
# Filter quá dài (giới hạn 100 tokens)
|
| 86 |
+
if len(src_full) > 100:
|
| 87 |
+
src_full = src_full[:99] + [EOS_ID]
|
| 88 |
+
if len(tgt_full) > 100:
|
| 89 |
+
tgt_full = tgt_full[:99] + [EOS_ID]
|
| 90 |
+
|
| 91 |
+
data.append((src_full, tgt_full))
|
| 92 |
+
|
| 93 |
+
print(f" ✓ Đã encode {len(data)} cặp câu")
|
| 94 |
+
return data
|
| 95 |
+
|
| 96 |
+
def encode_from_cleaned_data(tokenizer, cleaned_data_path):
|
| 97 |
+
"""
|
| 98 |
+
Encode từ cleaned_data.json nếu không có raw files
|
| 99 |
+
|
| 100 |
+
Args:
|
| 101 |
+
tokenizer: Shared tokenizer
|
| 102 |
+
cleaned_data_path: Path to cleaned_data.json
|
| 103 |
+
|
| 104 |
+
Returns:
|
| 105 |
+
processed_data: Dict với keys 'train', 'validation', 'test'
|
| 106 |
+
"""
|
| 107 |
+
print("\nĐang encode từ cleaned_data.json...")
|
| 108 |
+
|
| 109 |
+
with open(cleaned_data_path, 'r', encoding='utf-8') as f:
|
| 110 |
+
cleaned_data = json.load(f)
|
| 111 |
+
|
| 112 |
+
SOS_ID = tokenizer.token_to_id("<sos>")
|
| 113 |
+
EOS_ID = tokenizer.token_to_id("<eos>")
|
| 114 |
+
|
| 115 |
+
processed_data = {
|
| 116 |
+
'train': [],
|
| 117 |
+
'validation': [],
|
| 118 |
+
'test': []
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
for split in ['train', 'validation', 'test']:
|
| 122 |
+
print(f"\nĐang encode {split} set...")
|
| 123 |
+
|
| 124 |
+
for item in tqdm(cleaned_data.get(split, []), desc=f"Encoding {split}"):
|
| 125 |
+
src_text = item['vi'].strip()
|
| 126 |
+
tgt_text = item['en'].strip()
|
| 127 |
+
|
| 128 |
+
if not src_text or not tgt_text:
|
| 129 |
+
continue
|
| 130 |
+
|
| 131 |
+
# Encode
|
| 132 |
+
src_encoded = tokenizer.encode(src_text)
|
| 133 |
+
tgt_encoded = tokenizer.encode(tgt_text)
|
| 134 |
+
|
| 135 |
+
src_ids = [SOS_ID] + src_encoded.ids + [EOS_ID]
|
| 136 |
+
tgt_ids = [SOS_ID] + tgt_encoded.ids + [EOS_ID]
|
| 137 |
+
|
| 138 |
+
# Filter quá dài
|
| 139 |
+
if len(src_ids) > 100:
|
| 140 |
+
src_ids = src_ids[:99] + [EOS_ID]
|
| 141 |
+
if len(tgt_ids) > 100:
|
| 142 |
+
tgt_ids = tgt_ids[:99] + [EOS_ID]
|
| 143 |
+
|
| 144 |
+
processed_data[split].append((src_ids, tgt_ids))
|
| 145 |
+
|
| 146 |
+
print(f" ✓ {split}: {len(processed_data[split])} cặp câu")
|
| 147 |
+
|
| 148 |
+
return processed_data
|
| 149 |
+
|
| 150 |
+
def main():
|
| 151 |
+
"""
|
| 152 |
+
Main function để encode lại toàn bộ dữ liệu
|
| 153 |
+
"""
|
| 154 |
+
print("="*70)
|
| 155 |
+
print("ENCODE LẠI DỮ LIỆU VỚI SHARED VOCABULARY")
|
| 156 |
+
print("="*70)
|
| 157 |
+
|
| 158 |
+
# 1. Load shared tokenizer
|
| 159 |
+
tokenizer_path = PROCESSED_DATA_DIR / 'tokenizer_shared.json'
|
| 160 |
+
|
| 161 |
+
if not tokenizer_path.exists():
|
| 162 |
+
raise FileNotFoundError(
|
| 163 |
+
f"Không tìm thấy tokenizer_shared.json!\n"
|
| 164 |
+
f"Vui lòng chạy: python src/1_build_shared_vocab.py trước"
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
print(f"\n1. Đang load tokenizer từ: {tokenizer_path}")
|
| 168 |
+
tokenizer = Tokenizer.from_file(str(tokenizer_path))
|
| 169 |
+
print(f" ✓ Vocab size: {tokenizer.get_vocab_size()}")
|
| 170 |
+
|
| 171 |
+
# 2. Kiểm tra và encode data
|
| 172 |
+
# Ưu tiên: MTet CSV (nếu có MTET_MAX_ROWS) > Raw files > cleaned_data.json
|
| 173 |
+
processed_data = {}
|
| 174 |
+
|
| 175 |
+
# Option 0: Dùng MTet CSV nếu có MTET_MAX_ROWS set
|
| 176 |
+
from data_preprocessing import CUSTOM_CSV_PATH, CUSTOM_CSV_MAX_ROWS
|
| 177 |
+
if CUSTOM_CSV_PATH.exists() and CUSTOM_CSV_MAX_ROWS and CUSTOM_CSV_MAX_ROWS > 0:
|
| 178 |
+
print("\n2. Ưu tiên dùng MTet CSV với 1M câu...")
|
| 179 |
+
import pandas as pd
|
| 180 |
+
|
| 181 |
+
# Đọc CSV
|
| 182 |
+
df = pd.read_csv(CUSTOM_CSV_PATH, nrows=CUSTOM_CSV_MAX_ROWS)
|
| 183 |
+
if {'src', 'tgt'}.issubset(df.columns):
|
| 184 |
+
df = df.rename(columns={'src': 'vi', 'tgt': 'en'})
|
| 185 |
+
elif {'vi', 'en'}.issubset(df.columns):
|
| 186 |
+
pass
|
| 187 |
+
else:
|
| 188 |
+
raise ValueError("CSV cần có cột 'src'/'tgt' hoặc 'vi'/'en'")
|
| 189 |
+
|
| 190 |
+
df = df[['vi', 'en']].dropna().reset_index(drop=True)
|
| 191 |
+
total = len(df)
|
| 192 |
+
|
| 193 |
+
# Split: 90% train, 5% val, 5% test
|
| 194 |
+
train_end = int(total * 0.90)
|
| 195 |
+
val_end = train_end + int(total * 0.05)
|
| 196 |
+
|
| 197 |
+
train_df = df.iloc[:train_end]
|
| 198 |
+
val_df = df.iloc[train_end:val_end]
|
| 199 |
+
test_df = df.iloc[val_end:]
|
| 200 |
+
|
| 201 |
+
print(f" - Total: {total:,} cặp câu")
|
| 202 |
+
print(f" - Train: {len(train_df):,} ({len(train_df)/total*100:.1f}%)")
|
| 203 |
+
print(f" - Validation: {len(val_df):,} ({len(val_df)/total*100:.1f}%)")
|
| 204 |
+
print(f" - Test: {len(test_df):,} ({len(test_df)/total*100:.1f}%)")
|
| 205 |
+
|
| 206 |
+
# Encode từng split
|
| 207 |
+
SOS_ID = tokenizer.token_to_id("<sos>")
|
| 208 |
+
EOS_ID = tokenizer.token_to_id("<eos>")
|
| 209 |
+
|
| 210 |
+
def encode_split(split_df, split_name):
|
| 211 |
+
data = []
|
| 212 |
+
for _, row in tqdm(split_df.iterrows(), total=len(split_df), desc=f"Encoding {split_name}"):
|
| 213 |
+
src_text = str(row['vi']).strip()
|
| 214 |
+
tgt_text = str(row['en']).strip()
|
| 215 |
+
|
| 216 |
+
if not src_text or not tgt_text:
|
| 217 |
+
continue
|
| 218 |
+
|
| 219 |
+
src_encoded = tokenizer.encode(src_text)
|
| 220 |
+
tgt_encoded = tokenizer.encode(tgt_text)
|
| 221 |
+
|
| 222 |
+
src_ids = [SOS_ID] + src_encoded.ids + [EOS_ID]
|
| 223 |
+
tgt_ids = [SOS_ID] + tgt_encoded.ids + [EOS_ID]
|
| 224 |
+
|
| 225 |
+
if len(src_ids) > 100:
|
| 226 |
+
src_ids = src_ids[:99] + [EOS_ID]
|
| 227 |
+
if len(tgt_ids) > 100:
|
| 228 |
+
tgt_ids = tgt_ids[:99] + [EOS_ID]
|
| 229 |
+
|
| 230 |
+
data.append((src_ids, tgt_ids))
|
| 231 |
+
return data
|
| 232 |
+
|
| 233 |
+
processed_data['train'] = encode_split(train_df, 'train')
|
| 234 |
+
processed_data['validation'] = encode_split(val_df, 'validation')
|
| 235 |
+
processed_data['test'] = encode_split(test_df, 'test')
|
| 236 |
+
|
| 237 |
+
# Option 1: Có raw files
|
| 238 |
+
elif all(f.exists() for f in [RAW_DATA_DIR / 'train.vi', RAW_DATA_DIR / 'train.en']):
|
| 239 |
+
train_vi = RAW_DATA_DIR / 'train.vi'
|
| 240 |
+
train_en = RAW_DATA_DIR / 'train.en'
|
| 241 |
+
dev_vi = RAW_DATA_DIR / 'dev.vi'
|
| 242 |
+
dev_en = RAW_DATA_DIR / 'dev.en'
|
| 243 |
+
test_vi = RAW_DATA_DIR / 'test.vi'
|
| 244 |
+
test_en = RAW_DATA_DIR / 'test.en'
|
| 245 |
+
print("\n2. Tìm thấy raw files, đang encode...")
|
| 246 |
+
|
| 247 |
+
# Encode train
|
| 248 |
+
train_data = encode_file_with_shared_vocab(
|
| 249 |
+
train_vi, train_en, tokenizer, 'train'
|
| 250 |
+
)
|
| 251 |
+
processed_data['train'] = train_data
|
| 252 |
+
|
| 253 |
+
# Encode validation (nếu có)
|
| 254 |
+
if dev_vi.exists() and dev_en.exists():
|
| 255 |
+
val_data = encode_file_with_shared_vocab(
|
| 256 |
+
dev_vi, dev_en, tokenizer, 'validation'
|
| 257 |
+
)
|
| 258 |
+
processed_data['validation'] = val_data
|
| 259 |
+
else:
|
| 260 |
+
print("\n⚠️ Không tìm thấy dev files, bỏ qua validation set")
|
| 261 |
+
processed_data['validation'] = []
|
| 262 |
+
|
| 263 |
+
# Encode test (nếu có)
|
| 264 |
+
if test_vi.exists() and test_en.exists():
|
| 265 |
+
test_data = encode_file_with_shared_vocab(
|
| 266 |
+
test_vi, test_en, tokenizer, 'test'
|
| 267 |
+
)
|
| 268 |
+
processed_data['test'] = test_data
|
| 269 |
+
else:
|
| 270 |
+
print("\n⚠️ Không tìm thấy test files, bỏ qua test set")
|
| 271 |
+
processed_data['test'] = []
|
| 272 |
+
|
| 273 |
+
# Option 2: Dùng cleaned_data.json
|
| 274 |
+
else:
|
| 275 |
+
cleaned_data_path = PROCESSED_DATA_DIR / 'cleaned_data.json'
|
| 276 |
+
|
| 277 |
+
if not cleaned_data_path.exists():
|
| 278 |
+
raise FileNotFoundError(
|
| 279 |
+
f"Không tìm thấy raw files hoặc cleaned_data.json!\n"
|
| 280 |
+
f"Vui lòng đảm bảo có một trong các file sau:\n"
|
| 281 |
+
f" - {train_vi}, {train_en}\n"
|
| 282 |
+
f" - {cleaned_data_path}"
|
| 283 |
+
)
|
| 284 |
+
|
| 285 |
+
print("\n2. Không tìm thấy raw files, dùng cleaned_data.json...")
|
| 286 |
+
processed_data = encode_from_cleaned_data(tokenizer, cleaned_data_path)
|
| 287 |
+
|
| 288 |
+
# 3. Lưu processed data
|
| 289 |
+
print("\n3. Đang lưu processed data...")
|
| 290 |
+
|
| 291 |
+
# Format: {'train': [(src_ids, tgt_ids), ...], ...}
|
| 292 |
+
output_path = PROCESSED_DATA_DIR / 'processed_data_shared.pkl'
|
| 293 |
+
|
| 294 |
+
with open(output_path, 'wb') as f:
|
| 295 |
+
pickle.dump(processed_data, f)
|
| 296 |
+
|
| 297 |
+
print(f" ✓ Đã lưu: {output_path}")
|
| 298 |
+
|
| 299 |
+
# 4. Thống kê
|
| 300 |
+
print("\n4. Thống kê:")
|
| 301 |
+
print(f" - Train: {len(processed_data['train'])} cặp câu")
|
| 302 |
+
print(f" - Validation: {len(processed_data['validation'])} cặp câu")
|
| 303 |
+
print(f" - Test: {len(processed_data['test'])} cặp câu")
|
| 304 |
+
|
| 305 |
+
# 5. Lưu thông tin tokenizer (để dùng sau)
|
| 306 |
+
tokenizer_info = {
|
| 307 |
+
'vocab_size': tokenizer.get_vocab_size(),
|
| 308 |
+
'sos_id': tokenizer.token_to_id("<sos>"),
|
| 309 |
+
'eos_id': tokenizer.token_to_id("<eos>"),
|
| 310 |
+
'pad_id': tokenizer.token_to_id("<pad>"),
|
| 311 |
+
'unk_id': tokenizer.token_to_id("<unk>"),
|
| 312 |
+
'tokenizer_path': str(tokenizer_path)
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
info_path = PROCESSED_DATA_DIR / 'shared_vocab_info.json'
|
| 316 |
+
with open(info_path, 'w', encoding='utf-8') as f:
|
| 317 |
+
json.dump(tokenizer_info, f, indent=2)
|
| 318 |
+
|
| 319 |
+
print(f" ✓ Đã lưu tokenizer info: {info_path}")
|
| 320 |
+
|
| 321 |
+
print("\n" + "="*70)
|
| 322 |
+
print("✓ HOÀN TẤT ENCODE DỮ LIỆU!")
|
| 323 |
+
print("="*70)
|
| 324 |
+
|
| 325 |
+
print("\n📝 BƯỚC TIẾP THEO:")
|
| 326 |
+
print(" 1. Cập nhật model để hỗ trợ shared vocabulary")
|
| 327 |
+
print(" 2. Chạy training với shared vocab mode")
|
| 328 |
+
|
| 329 |
+
# ============================================================================
|
| 330 |
+
# RUN
|
| 331 |
+
# ============================================================================
|
| 332 |
+
|
| 333 |
+
if __name__ == "__main__":
|
| 334 |
+
main()
|
| 335 |
+
|
src/__init__.py
ADDED
|
File without changes
|
src/add_direction_tokens.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Thêm direction tokens (<en2vi> và <vi2en>) vào tokenizer hiện có
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from tokenizers import Tokenizer
|
| 7 |
+
import json
|
| 8 |
+
|
| 9 |
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
| 10 |
+
PROCESSED_DATA_DIR = PROJECT_ROOT / 'data' / 'processed'
|
| 11 |
+
TOKENIZER_PATH = PROCESSED_DATA_DIR / 'tokenizer_shared.json'
|
| 12 |
+
|
| 13 |
+
def add_direction_tokens():
|
| 14 |
+
"""Thêm direction tokens vào tokenizer"""
|
| 15 |
+
print("="*70)
|
| 16 |
+
print("THÊM DIRECTION TOKENS VÀO TOKENIZER")
|
| 17 |
+
print("="*70)
|
| 18 |
+
|
| 19 |
+
# Load tokenizer hiện tại
|
| 20 |
+
if not TOKENIZER_PATH.exists():
|
| 21 |
+
raise FileNotFoundError(f"Không tìm thấy tokenizer tại {TOKENIZER_PATH}")
|
| 22 |
+
|
| 23 |
+
print(f"\n📚 Đang load tokenizer từ: {TOKENIZER_PATH}")
|
| 24 |
+
tokenizer = Tokenizer.from_file(str(TOKENIZER_PATH))
|
| 25 |
+
|
| 26 |
+
# Kiểm tra xem tokens đã có chưa
|
| 27 |
+
vocab = tokenizer.get_vocab()
|
| 28 |
+
has_en2vi = '<en2vi>' in vocab
|
| 29 |
+
has_vi2en = '<vi2en>' in vocab
|
| 30 |
+
|
| 31 |
+
if has_en2vi and has_vi2en:
|
| 32 |
+
print("✓ Direction tokens đã có trong vocabulary!")
|
| 33 |
+
print(f" <en2vi>: ID {vocab['<en2vi']}")
|
| 34 |
+
print(f" <vi2en>: ID {vocab['<vi2en']}")
|
| 35 |
+
return tokenizer
|
| 36 |
+
|
| 37 |
+
print("\n➕ Đang thêm direction tokens...")
|
| 38 |
+
|
| 39 |
+
# Thêm tokens mới
|
| 40 |
+
# Lấy vocab size hiện tại
|
| 41 |
+
current_vocab_size = len(vocab)
|
| 42 |
+
|
| 43 |
+
# Thêm tokens mới với ID tiếp theo
|
| 44 |
+
tokenizer.add_tokens(["<en2vi>", "<vi2en>"])
|
| 45 |
+
|
| 46 |
+
# Save lại
|
| 47 |
+
print(f"\n💾 Đang lưu tokenizer với direction tokens...")
|
| 48 |
+
tokenizer.save(str(TOKENIZER_PATH))
|
| 49 |
+
|
| 50 |
+
# Kiểm tra lại
|
| 51 |
+
vocab = tokenizer.get_vocab()
|
| 52 |
+
print(f"\n✓ Đã thêm direction tokens!")
|
| 53 |
+
print(f" <en2vi>: ID {vocab.get('<en2vi>', 'NOT FOUND')}")
|
| 54 |
+
print(f" <vi2en>: ID {vocab.get('<vi2en>', 'NOT FOUND')}")
|
| 55 |
+
print(f" Vocab size: {len(vocab):,} (tăng từ {current_vocab_size:,})")
|
| 56 |
+
|
| 57 |
+
return tokenizer
|
| 58 |
+
|
| 59 |
+
if __name__ == '__main__':
|
| 60 |
+
add_direction_tokens()
|
| 61 |
+
print("\n✅ Hoàn thành!")
|
| 62 |
+
|
src/app.py
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
GRADIO UI - VIETNAMESE TO ENGLISH TRANSLATION
|
| 3 |
+
Modern web interface for Transformer translation model
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import glob
|
| 7 |
+
import importlib
|
| 8 |
+
import os
|
| 9 |
+
import pickle
|
| 10 |
+
import sys
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
import gradio as gr
|
| 14 |
+
import torch
|
| 15 |
+
from shared_vocab_utils import (
|
| 16 |
+
create_shared_vocab_wrapper,
|
| 17 |
+
load_shared_vocab_info,
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
# ============================================================================
|
| 21 |
+
# PATH CONFIGURATION
|
| 22 |
+
# ============================================================================
|
| 23 |
+
|
| 24 |
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
| 25 |
+
if str(PROJECT_ROOT) not in sys.path:
|
| 26 |
+
sys.path.append(str(PROJECT_ROOT))
|
| 27 |
+
SRC_DIR = PROJECT_ROOT / 'src'
|
| 28 |
+
if str(SRC_DIR) not in sys.path:
|
| 29 |
+
sys.path.append(str(SRC_DIR))
|
| 30 |
+
|
| 31 |
+
DATA_PROCESSED_DIR = PROJECT_ROOT / 'data' / 'processed'
|
| 32 |
+
CHECKPOINT_DIR = PROJECT_ROOT / 'checkpoints'
|
| 33 |
+
|
| 34 |
+
# ============================================================================
|
| 35 |
+
# DYNAMIC IMPORTS (giữ compatibility với pickle)
|
| 36 |
+
# ============================================================================
|
| 37 |
+
|
| 38 |
+
data_preprocessing = importlib.import_module('src.data_preprocessing')
|
| 39 |
+
sys.modules.setdefault('data_preprocessing', data_preprocessing)
|
| 40 |
+
Vocabulary = data_preprocessing.Vocabulary
|
| 41 |
+
clean_text = data_preprocessing.clean_text
|
| 42 |
+
|
| 43 |
+
complete_transformer = importlib.import_module('src.complete_transformer')
|
| 44 |
+
create_model = complete_transformer.create_model
|
| 45 |
+
|
| 46 |
+
inference_module = importlib.import_module('src.inference_evaluation')
|
| 47 |
+
translate_sentence = inference_module.translate_sentence
|
| 48 |
+
beam_search_decode = inference_module.beam_search_decode
|
| 49 |
+
greedy_decode = inference_module.greedy_decode
|
| 50 |
+
|
| 51 |
+
# ============================================================================
|
| 52 |
+
# LOAD MODEL & VOCABULARIES
|
| 53 |
+
# ============================================================================
|
| 54 |
+
|
| 55 |
+
class TranslationModel:
|
| 56 |
+
"""Wrapper class để quản lý model và vocabularies"""
|
| 57 |
+
|
| 58 |
+
def __init__(self):
|
| 59 |
+
self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
| 60 |
+
self.model = None
|
| 61 |
+
self.vi_vocab = None
|
| 62 |
+
self.en_vocab = None
|
| 63 |
+
self.model_info = {}
|
| 64 |
+
|
| 65 |
+
def load_latest_model(self):
|
| 66 |
+
"""Load model mới nhất từ checkpoints"""
|
| 67 |
+
print("🔄 Loading model và vocabularies...")
|
| 68 |
+
|
| 69 |
+
# Try shared vocabulary first (tokenizer_shared.json)
|
| 70 |
+
try:
|
| 71 |
+
vocab_info = load_shared_vocab_info()
|
| 72 |
+
self.vi_vocab, self.en_vocab = create_shared_vocab_wrapper()
|
| 73 |
+
self.pad_idx = vocab_info.get("pad_id", 0)
|
| 74 |
+
self.sos_idx = vocab_info.get("sos_id", 1)
|
| 75 |
+
self.eos_idx = vocab_info.get("eos_id", 2)
|
| 76 |
+
vocab_size = vocab_info.get("vocab_size")
|
| 77 |
+
print(f"✓ Loaded shared vocabulary (size={vocab_size})")
|
| 78 |
+
except Exception as shared_err:
|
| 79 |
+
print(f"⚠️ Shared vocab load failed, fallback to pickled vocabs: {shared_err}")
|
| 80 |
+
# Load legacy pickled vocabs
|
| 81 |
+
vi_vocab_path = DATA_PROCESSED_DIR / 'vi_vocab.pkl'
|
| 82 |
+
en_vocab_path = DATA_PROCESSED_DIR / 'en_vocab.pkl'
|
| 83 |
+
try:
|
| 84 |
+
with open(vi_vocab_path, 'rb') as f:
|
| 85 |
+
self.vi_vocab = pickle.load(f)
|
| 86 |
+
with open(en_vocab_path, 'rb') as f:
|
| 87 |
+
self.en_vocab = pickle.load(f)
|
| 88 |
+
self.pad_idx = getattr(self.vi_vocab, "PAD_IDX", 0)
|
| 89 |
+
vocab_size = len(self.vi_vocab)
|
| 90 |
+
print(f"✓ Loaded vocabularies (VI: {len(self.vi_vocab)}, EN: {len(self.en_vocab)})")
|
| 91 |
+
except Exception as e:
|
| 92 |
+
raise Exception(f"❌ Không thể load vocabularies (shared & pickled đều lỗi): {e}")
|
| 93 |
+
|
| 94 |
+
# Tìm model mới nhất
|
| 95 |
+
checkpoint_dir = CHECKPOINT_DIR
|
| 96 |
+
best_model_path = checkpoint_dir / 'best_model.pt'
|
| 97 |
+
|
| 98 |
+
# Ưu tiên best_model.pt
|
| 99 |
+
if os.path.exists(best_model_path):
|
| 100 |
+
checkpoint_path = best_model_path
|
| 101 |
+
print(f"✓ Found best model: {best_model_path}")
|
| 102 |
+
else:
|
| 103 |
+
# Tìm checkpoint mới nhất
|
| 104 |
+
checkpoints = glob.glob(str(checkpoint_dir / 'checkpoint_epoch_*.pt'))
|
| 105 |
+
if not checkpoints:
|
| 106 |
+
raise Exception(f"❌ Không tìm thấy checkpoint nào trong {checkpoint_dir}")
|
| 107 |
+
|
| 108 |
+
# Sắp xếp theo thời gian modified
|
| 109 |
+
checkpoint_path = max(checkpoints, key=os.path.getmtime)
|
| 110 |
+
print(f"✓ Found latest checkpoint: {checkpoint_path}")
|
| 111 |
+
|
| 112 |
+
# Load checkpoint để lấy thông tin
|
| 113 |
+
checkpoint = torch.load(checkpoint_path, map_location=self.device)
|
| 114 |
+
|
| 115 |
+
# Tạo model
|
| 116 |
+
print("🔨 Creating model...")
|
| 117 |
+
# Detect model size từ checkpoint path (simple heuristic)
|
| 118 |
+
path_str = str(checkpoint_path).lower()
|
| 119 |
+
model_size = 'custom_25m' # Default to trained config (d_model=384, 6 layers)
|
| 120 |
+
for size in ['custom_25m', 'medium', 'base', 'small', 'tiny', 'large']:
|
| 121 |
+
if size in path_str or ('25m' in path_str and size == 'custom_25m'):
|
| 122 |
+
model_size = size
|
| 123 |
+
break
|
| 124 |
+
|
| 125 |
+
self.model, model_config = create_model(
|
| 126 |
+
src_vocab_size=len(self.vi_vocab),
|
| 127 |
+
tgt_vocab_size=len(self.en_vocab),
|
| 128 |
+
model_size=model_size,
|
| 129 |
+
pad_idx=getattr(self, "pad_idx", 0),
|
| 130 |
+
use_shared_vocab=True,
|
| 131 |
+
use_weight_tying=True,
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
# Load weights
|
| 135 |
+
self.model.load_state_dict(checkpoint['model_state_dict'])
|
| 136 |
+
self.model = self.model.to(self.device)
|
| 137 |
+
self.model.eval()
|
| 138 |
+
|
| 139 |
+
# Lưu thông tin model
|
| 140 |
+
self.model_info = {
|
| 141 |
+
'checkpoint': checkpoint_path,
|
| 142 |
+
'epoch': checkpoint.get('epoch', 'N/A'),
|
| 143 |
+
'val_loss': checkpoint.get('val_loss', 'N/A'),
|
| 144 |
+
'val_ppl': checkpoint.get('val_ppl', 'N/A'),
|
| 145 |
+
'model_size': model_size,
|
| 146 |
+
'device': str(self.device),
|
| 147 |
+
'parameters': sum(p.numel() for p in self.model.parameters()),
|
| 148 |
+
'vocab_size': len(self.vi_vocab),
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
print(f"✓ Model loaded successfully!")
|
| 152 |
+
print(f" - Epoch: {self.model_info['epoch']}")
|
| 153 |
+
print(f" - Val Loss: {self.model_info['val_loss']}")
|
| 154 |
+
print(f" - Device: {self.model_info['device']}")
|
| 155 |
+
|
| 156 |
+
return self.model_info
|
| 157 |
+
|
| 158 |
+
def translate(self, text, direction='vi2en', use_beam_search=True, beam_size=5):
|
| 159 |
+
"""Dịch một câu theo hướng chỉ định"""
|
| 160 |
+
if not self.model or not self.vi_vocab or not self.en_vocab:
|
| 161 |
+
return "❌ Model chưa được load. Vui lòng reload model."
|
| 162 |
+
|
| 163 |
+
try:
|
| 164 |
+
# Xác định ngôn ngữ source
|
| 165 |
+
src_lang = 'en' if direction == 'en2vi' else 'vi'
|
| 166 |
+
|
| 167 |
+
# Clean text
|
| 168 |
+
text = clean_text(text, src_lang)
|
| 169 |
+
|
| 170 |
+
if not text.strip():
|
| 171 |
+
lang_name = "tiếng Anh" if direction == 'en2vi' else "tiếng Việt"
|
| 172 |
+
return f"⚠️ Vui lòng nhập văn bản {lang_name}."
|
| 173 |
+
|
| 174 |
+
# Với shared vocabulary, cả vi_vocab và en_vocab đều là cùng một tokenizer
|
| 175 |
+
# Model có thể dịch cả hai chiều vì dùng shared vocabulary
|
| 176 |
+
# Translate với shared vocab (vi_vocab và en_vocab giống nhau)
|
| 177 |
+
translation = translate_sentence(
|
| 178 |
+
self.model, text, self.vi_vocab, self.en_vocab,
|
| 179 |
+
self.device, use_beam_search, beam_size,
|
| 180 |
+
src_lang=src_lang,
|
| 181 |
+
repetition_penalty=1.3,
|
| 182 |
+
no_repeat_ngram_size=3
|
| 183 |
+
)
|
| 184 |
+
|
| 185 |
+
return translation
|
| 186 |
+
|
| 187 |
+
except Exception as e:
|
| 188 |
+
import traceback
|
| 189 |
+
error_detail = traceback.format_exc()
|
| 190 |
+
return f"❌ Lỗi khi dịch: {str(e)}\n\nChi tiết:\n{error_detail}"
|
| 191 |
+
|
| 192 |
+
# Khởi tạo model
|
| 193 |
+
translator = TranslationModel()
|
| 194 |
+
|
| 195 |
+
# ============================================================================
|
| 196 |
+
# GRADIO INTERFACE
|
| 197 |
+
# ============================================================================
|
| 198 |
+
|
| 199 |
+
def translate_text(text, direction, use_beam_search, beam_size):
|
| 200 |
+
"""Wrapper function cho Gradio"""
|
| 201 |
+
return translator.translate(text, direction, use_beam_search, beam_size)
|
| 202 |
+
|
| 203 |
+
def reload_model():
|
| 204 |
+
"""Reload model mới nhất"""
|
| 205 |
+
try:
|
| 206 |
+
info = translator.load_latest_model()
|
| 207 |
+
return f"""
|
| 208 |
+
✅ **Model đã được load thành công!**
|
| 209 |
+
|
| 210 |
+
📊 **Thông tin model:**
|
| 211 |
+
- Checkpoint: `{info['checkpoint']}`
|
| 212 |
+
- Epoch: {info['epoch']}
|
| 213 |
+
- Validation Loss: {f"{info['val_loss']:.4f}" if isinstance(info['val_loss'], float) else info['val_loss']}
|
| 214 |
+
- Model Size: {info['model_size'].upper()}
|
| 215 |
+
- Device: {info['device']}
|
| 216 |
+
- Parameters: {info['parameters']:,}
|
| 217 |
+
- Vocab size: {info.get('vocab_size', 'N/A')}
|
| 218 |
+
"""
|
| 219 |
+
except Exception as e:
|
| 220 |
+
return f"❌ Lỗi khi load model: {str(e)}"
|
| 221 |
+
|
| 222 |
+
def get_model_info():
|
| 223 |
+
"""Lấy thông tin model hiện tại"""
|
| 224 |
+
if not translator.model:
|
| 225 |
+
return "⚠️ Model chưa được load. Click 'Reload Model' để load model."
|
| 226 |
+
|
| 227 |
+
info = translator.model_info
|
| 228 |
+
return f"""
|
| 229 |
+
📊 **Model hiện tại:**
|
| 230 |
+
- Checkpoint: `{info.get('checkpoint', 'N/A')}`
|
| 231 |
+
- Epoch: {info.get('epoch', 'N/A')}
|
| 232 |
+
- Validation Loss: {f"{info['val_loss']:.4f}" if isinstance(info.get('val_loss'), float) else info.get('val_loss', 'N/A')}
|
| 233 |
+
- Model Size: {str(info.get('model_size', 'N/A')).upper()}
|
| 234 |
+
- Device: {info.get('device', 'N/A')}
|
| 235 |
+
- Parameters: {info.get('parameters', 0):,}
|
| 236 |
+
- Vocab size: {info.get('vocab_size', 'N/A')}
|
| 237 |
+
"""
|
| 238 |
+
|
| 239 |
+
# ============================================================================
|
| 240 |
+
# BUILD GRADIO APP
|
| 241 |
+
# ============================================================================
|
| 242 |
+
|
| 243 |
+
def create_app():
|
| 244 |
+
"""Tạo Gradio app"""
|
| 245 |
+
|
| 246 |
+
with gr.Blocks(
|
| 247 |
+
title="Bidirectional Translation: English ↔ Vietnamese"
|
| 248 |
+
) as app:
|
| 249 |
+
|
| 250 |
+
# Header
|
| 251 |
+
gr.Markdown("""
|
| 252 |
+
# 🌐 Bidirectional Translation: English ↔ Vietnamese
|
| 253 |
+
### Powered by Transformer Neural Network
|
| 254 |
+
|
| 255 |
+
Dịch văn bản hai chiều giữa **Tiếng Anh** và **Tiếng Việt** sử dụng mô hình Transformer được train từ đầu.
|
| 256 |
+
""")
|
| 257 |
+
|
| 258 |
+
# Main content
|
| 259 |
+
with gr.Row():
|
| 260 |
+
with gr.Column(scale=1):
|
| 261 |
+
# Direction selector
|
| 262 |
+
gr.Markdown("### 🔄 Chọn hướng dịch")
|
| 263 |
+
direction = gr.Radio(
|
| 264 |
+
choices=[
|
| 265 |
+
("Vietnamese → English", "vi2en"),
|
| 266 |
+
("English → Vietnamese", "en2vi")
|
| 267 |
+
],
|
| 268 |
+
value="vi2en",
|
| 269 |
+
label="Hướng dịch",
|
| 270 |
+
info="Chọn ngôn ngữ đầu vào và đầu ra"
|
| 271 |
+
)
|
| 272 |
+
|
| 273 |
+
# Input
|
| 274 |
+
gr.Markdown("### 📝 Nhập văn bản")
|
| 275 |
+
input_text = gr.Textbox(
|
| 276 |
+
label="Văn bản đầu vào",
|
| 277 |
+
placeholder="Nhập câu cần dịch...\nVí dụ: Xin chào, tôi là sinh viên.\nHoặc: Hello, I am a student.",
|
| 278 |
+
lines=5,
|
| 279 |
+
max_lines=10
|
| 280 |
+
)
|
| 281 |
+
|
| 282 |
+
# Options
|
| 283 |
+
with gr.Row():
|
| 284 |
+
use_beam_search = gr.Checkbox(
|
| 285 |
+
label="🔍 Beam Search",
|
| 286 |
+
value=True,
|
| 287 |
+
info="Kết quả tốt hơn nhưng chậm hơn"
|
| 288 |
+
)
|
| 289 |
+
beam_size = gr.Slider(
|
| 290 |
+
minimum=1,
|
| 291 |
+
maximum=10,
|
| 292 |
+
value=5,
|
| 293 |
+
step=1,
|
| 294 |
+
label="Beam Size",
|
| 295 |
+
info="Số candidates (cao hơn = tốt hơn nhưng chậm hơn)"
|
| 296 |
+
)
|
| 297 |
+
|
| 298 |
+
# Buttons
|
| 299 |
+
with gr.Row():
|
| 300 |
+
translate_btn = gr.Button("🚀 Dịch", variant="primary", size="lg")
|
| 301 |
+
clear_btn = gr.ClearButton([input_text], value="🗑️ Xóa")
|
| 302 |
+
|
| 303 |
+
with gr.Column(scale=1):
|
| 304 |
+
# Output
|
| 305 |
+
gr.Markdown("### 🎯 Kết quả dịch")
|
| 306 |
+
output_text = gr.Textbox(
|
| 307 |
+
label="Văn bản đã dịch",
|
| 308 |
+
placeholder="Kết quả dịch sẽ hiển thị ở đây...",
|
| 309 |
+
lines=5,
|
| 310 |
+
max_lines=10,
|
| 311 |
+
interactive=False
|
| 312 |
+
)
|
| 313 |
+
|
| 314 |
+
# Examples
|
| 315 |
+
gr.Markdown("### 💡 Ví dụ")
|
| 316 |
+
gr.Examples(
|
| 317 |
+
examples=[
|
| 318 |
+
["vi2en", "Xin chào, tôi tên là Nam.", True, 5],
|
| 319 |
+
["vi2en", "Hôm nay thời tiết đẹp.", True, 5],
|
| 320 |
+
["vi2en", "Tôi đang học tiếng Anh.", True, 5],
|
| 321 |
+
["vi2en", "Bạn khỏe không?", True, 5],
|
| 322 |
+
["en2vi", "Hello, my name is Nam.", True, 5],
|
| 323 |
+
["en2vi", "The weather is beautiful today.", True, 5],
|
| 324 |
+
["en2vi", "I am learning English.", True, 5],
|
| 325 |
+
["en2vi", "How are you?", True, 5],
|
| 326 |
+
],
|
| 327 |
+
inputs=[direction, input_text, use_beam_search, beam_size],
|
| 328 |
+
outputs=output_text,
|
| 329 |
+
fn=translate_text,
|
| 330 |
+
cache_examples=False,
|
| 331 |
+
)
|
| 332 |
+
|
| 333 |
+
# Model info section
|
| 334 |
+
with gr.Accordion("⚙️ Thông tin Model & Cài đặt", open=False):
|
| 335 |
+
model_info_display = gr.Markdown(get_model_info())
|
| 336 |
+
|
| 337 |
+
with gr.Row():
|
| 338 |
+
reload_btn = gr.Button("🔄 Reload Model", variant="secondary")
|
| 339 |
+
info_btn = gr.Button("ℹ️ Xem thông tin", variant="secondary")
|
| 340 |
+
|
| 341 |
+
reload_output = gr.Markdown()
|
| 342 |
+
|
| 343 |
+
# Connect buttons
|
| 344 |
+
translate_btn.click(
|
| 345 |
+
fn=translate_text,
|
| 346 |
+
inputs=[input_text, direction, use_beam_search, beam_size],
|
| 347 |
+
outputs=output_text
|
| 348 |
+
)
|
| 349 |
+
|
| 350 |
+
reload_btn.click(
|
| 351 |
+
fn=reload_model,
|
| 352 |
+
outputs=reload_output
|
| 353 |
+
)
|
| 354 |
+
|
| 355 |
+
info_btn.click(
|
| 356 |
+
fn=get_model_info,
|
| 357 |
+
outputs=model_info_display
|
| 358 |
+
)
|
| 359 |
+
|
| 360 |
+
# Footer
|
| 361 |
+
gr.Markdown("""
|
| 362 |
+
---
|
| 363 |
+
### 📚 Hướng dẫn sử dụng
|
| 364 |
+
1. **Chọn hướng dịch**: Vietnamese → English hoặc English → Vietnamese
|
| 365 |
+
2. **Nhập văn bản** vào ô bên trái
|
| 366 |
+
3. **Chọn phương pháp dịch**:
|
| 367 |
+
- ✅ Beam Search: Chất lượng cao hơn (khuyên dùng)
|
| 368 |
+
- ❌ Greedy Search: Nhanh hơn nhưng kém chính xác hơn
|
| 369 |
+
4. **Click "Dịch"** để xem kết quả
|
| 370 |
+
5. **Reload Model** nếu bạn vừa train xong model mới
|
| 371 |
+
|
| 372 |
+
### 🔧 Tips
|
| 373 |
+
- Model hỗ trợ dịch hai chiều: Việt ↔ Anh
|
| 374 |
+
- Beam size càng cao thì kết quả càng tốt nhưng chậm hơn (khuyên dùng 5)
|
| 375 |
+
- Câu ngắn dịch nhanh hơn câu dài
|
| 376 |
+
- Model hoạt động tốt nhất với câu có độ dài 5-20 từ
|
| 377 |
+
- Model sử dụng shared vocabulary và checkpoint hiện có, không cần train lại
|
| 378 |
+
""")
|
| 379 |
+
|
| 380 |
+
return app
|
| 381 |
+
|
| 382 |
+
# ============================================================================
|
| 383 |
+
# LAUNCH APP
|
| 384 |
+
# ============================================================================
|
| 385 |
+
|
| 386 |
+
if __name__ == "__main__":
|
| 387 |
+
print("="*70)
|
| 388 |
+
print("KHỞI ĐỘNG TRANSLATION WEB APP")
|
| 389 |
+
print("="*70)
|
| 390 |
+
|
| 391 |
+
# Load model khi khởi động
|
| 392 |
+
try:
|
| 393 |
+
translator.load_latest_model()
|
| 394 |
+
except Exception as e:
|
| 395 |
+
print(f"⚠️ Warning: {e}")
|
| 396 |
+
print("💡 Bạn có thể reload model sau trong giao diện web.")
|
| 397 |
+
|
| 398 |
+
# Create and launch app
|
| 399 |
+
app = create_app()
|
| 400 |
+
|
| 401 |
+
print("\n" + "="*70)
|
| 402 |
+
print("🚀 LAUNCHING WEB APP...")
|
| 403 |
+
print("="*70)
|
| 404 |
+
|
| 405 |
+
import os
|
| 406 |
+
server_port = int(os.environ.get("GRADIO_SERVER_PORT", "7860"))
|
| 407 |
+
|
| 408 |
+
app.launch(
|
| 409 |
+
server_name="0.0.0.0", # Cho phép truy cập từ máy khác
|
| 410 |
+
server_port=server_port,
|
| 411 |
+
share=False, # Set True nếu muốn share link public
|
| 412 |
+
inbrowser=True, # Tự động mở browser
|
| 413 |
+
)
|
src/bidirectional_translate.py
ADDED
|
@@ -0,0 +1,546 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
BIDIRECTIONAL TRANSLATION SCRIPT
|
| 3 |
+
Dịch hai chiều: English ↔ Vietnamese
|
| 4 |
+
Sử dụng checkpoint và vocab hiện có, không cần train lại
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
import argparse
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from tqdm import tqdm
|
| 11 |
+
import sys
|
| 12 |
+
|
| 13 |
+
# Add parent directory to path
|
| 14 |
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
| 15 |
+
|
| 16 |
+
from src.complete_transformer import TransformerShared
|
| 17 |
+
from src.shared_vocab_utils import load_shared_tokenizer, load_shared_vocab_info, clean_decoded_output
|
| 18 |
+
from src.inference_evaluation import beam_search_decode, greedy_decode
|
| 19 |
+
from src.data_preprocessing import clean_text
|
| 20 |
+
|
| 21 |
+
# ============================================================================
|
| 22 |
+
# CONFIGURATION
|
| 23 |
+
# ============================================================================
|
| 24 |
+
|
| 25 |
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
| 26 |
+
DATA_DIR = PROJECT_ROOT / 'data'
|
| 27 |
+
RESULTS_DIR = PROJECT_ROOT / 'results'
|
| 28 |
+
CHECKPOINT_DIR = PROJECT_ROOT / 'checkpoints'
|
| 29 |
+
|
| 30 |
+
# ============================================================================
|
| 31 |
+
# LOAD MODEL
|
| 32 |
+
# ============================================================================
|
| 33 |
+
|
| 34 |
+
def load_model(checkpoint_path, model_size='custom_25m', device='cuda'):
|
| 35 |
+
"""
|
| 36 |
+
Load model từ checkpoint
|
| 37 |
+
|
| 38 |
+
Args:
|
| 39 |
+
checkpoint_path: Đường dẫn đến checkpoint file
|
| 40 |
+
model_size: Kích thước model ('custom_25m', 'base', etc.)
|
| 41 |
+
device: Device để load model
|
| 42 |
+
|
| 43 |
+
Returns:
|
| 44 |
+
model: Loaded model
|
| 45 |
+
vocab_info: Vocabulary info dict
|
| 46 |
+
"""
|
| 47 |
+
print(f"\n{'='*70}")
|
| 48 |
+
print(f"LOADING MODEL")
|
| 49 |
+
print(f"{'='*70}")
|
| 50 |
+
|
| 51 |
+
# Load vocab info
|
| 52 |
+
vocab_info = load_shared_vocab_info()
|
| 53 |
+
vocab_size = vocab_info['vocab_size']
|
| 54 |
+
|
| 55 |
+
print(f"Vocab size: {vocab_size}")
|
| 56 |
+
|
| 57 |
+
# Model configs theo model_size
|
| 58 |
+
model_configs = {
|
| 59 |
+
'custom_25m': {
|
| 60 |
+
'd_model': 384,
|
| 61 |
+
'n_layers': 6,
|
| 62 |
+
'n_heads': 8,
|
| 63 |
+
'd_ff': 1536,
|
| 64 |
+
'dropout': 0.1
|
| 65 |
+
},
|
| 66 |
+
'base': {
|
| 67 |
+
'd_model': 512,
|
| 68 |
+
'n_layers': 6,
|
| 69 |
+
'n_heads': 8,
|
| 70 |
+
'd_ff': 2048,
|
| 71 |
+
'dropout': 0.1
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
config = model_configs.get(model_size, model_configs['custom_25m'])
|
| 76 |
+
|
| 77 |
+
# Tạo model
|
| 78 |
+
model = TransformerShared(
|
| 79 |
+
vocab_size=vocab_size,
|
| 80 |
+
d_model=config['d_model'],
|
| 81 |
+
n_layers=config['n_layers'],
|
| 82 |
+
n_heads=config['n_heads'],
|
| 83 |
+
d_ff=config['d_ff'],
|
| 84 |
+
dropout=config['dropout'],
|
| 85 |
+
pad_idx=vocab_info['pad_id']
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
# Load checkpoint
|
| 89 |
+
checkpoint = torch.load(checkpoint_path, map_location=device)
|
| 90 |
+
model.load_state_dict(checkpoint['model_state_dict'])
|
| 91 |
+
model = model.to(device)
|
| 92 |
+
model.eval()
|
| 93 |
+
|
| 94 |
+
print(f"✓ Model loaded from {checkpoint_path}")
|
| 95 |
+
print(f"✓ Model size: {model_size}")
|
| 96 |
+
print(f"✓ Device: {device}")
|
| 97 |
+
print(f"{'='*70}\n")
|
| 98 |
+
|
| 99 |
+
return model, vocab_info
|
| 100 |
+
|
| 101 |
+
# ============================================================================
|
| 102 |
+
# VOCABULARY WRAPPER
|
| 103 |
+
# ============================================================================
|
| 104 |
+
|
| 105 |
+
class VocabWrapper:
|
| 106 |
+
"""Wrapper cho tokenizer để tương thích với inference functions"""
|
| 107 |
+
def __init__(self, tokenizer, vocab_info):
|
| 108 |
+
self.tokenizer = tokenizer
|
| 109 |
+
self.SOS_IDX = vocab_info['sos_id']
|
| 110 |
+
self.EOS_IDX = vocab_info['eos_id']
|
| 111 |
+
self.PAD_IDX = vocab_info['pad_id']
|
| 112 |
+
self.UNK_IDX = vocab_info['unk_id']
|
| 113 |
+
|
| 114 |
+
def encode(self, text):
|
| 115 |
+
"""Encode text thành token IDs"""
|
| 116 |
+
encoding = self.tokenizer.encode(text)
|
| 117 |
+
return encoding.ids
|
| 118 |
+
|
| 119 |
+
def decode(self, token_ids):
|
| 120 |
+
"""Decode token IDs thành text (nhận list of token IDs)"""
|
| 121 |
+
# Filter out special tokens
|
| 122 |
+
filtered_ids = [tid for tid in token_ids
|
| 123 |
+
if tid not in [self.SOS_IDX, self.EOS_IDX, self.PAD_IDX]]
|
| 124 |
+
if not filtered_ids:
|
| 125 |
+
return ""
|
| 126 |
+
# Decode using tokenizer
|
| 127 |
+
decoded = self.tokenizer.decode(filtered_ids)
|
| 128 |
+
# Post-processing: Clean output
|
| 129 |
+
decoded = clean_decoded_output(decoded)
|
| 130 |
+
return decoded
|
| 131 |
+
|
| 132 |
+
# ============================================================================
|
| 133 |
+
# POST-PROCESSING: CLEAN TRANSLATION OUTPUT
|
| 134 |
+
# ============================================================================
|
| 135 |
+
|
| 136 |
+
def clean_translation_output(text):
|
| 137 |
+
"""
|
| 138 |
+
Làm sạch output translation để loại bỏ rác ở cuối câu
|
| 139 |
+
|
| 140 |
+
Args:
|
| 141 |
+
text: Text cần làm sạch
|
| 142 |
+
|
| 143 |
+
Returns:
|
| 144 |
+
Text đã được làm sạch
|
| 145 |
+
"""
|
| 146 |
+
if not text:
|
| 147 |
+
return text
|
| 148 |
+
|
| 149 |
+
import re
|
| 150 |
+
|
| 151 |
+
# Loại bỏ các pattern rác ở cuối câu
|
| 152 |
+
# Pattern: – số., – ", – [ [ [, etc.
|
| 153 |
+
text = re.sub(r'\s*–\s*\d+\.?\s*$', '', text) # – 7., – 1.
|
| 154 |
+
text = re.sub(r'\s*–\s*["\']\s*$', '', text) # – ", – '
|
| 155 |
+
text = re.sub(r'\s*–\s*\[\s*\[\s*\[\s*$', '', text) # – [ [ [
|
| 156 |
+
text = re.sub(r'\s*–\s*$', '', text) # – ở cuối
|
| 157 |
+
text = re.sub(r'\s*–\s*[^\w\s.,!?;:()\[\]{}"\'-]+$', '', text) # – ký tự đặc biệt
|
| 158 |
+
|
| 159 |
+
# Loại bỏ số trang và footnote ở cuối
|
| 160 |
+
text = re.sub(r'\s+\d+\.\s*$', '', text) # " 7. "
|
| 161 |
+
text = re.sub(r'\s+\[\d+\]\s*$', '', text) # " [1] "
|
| 162 |
+
|
| 163 |
+
# Loại bỏ các ký tự đặc biệt thừa ở cuối (nhưng giữ dấu câu hợp lệ)
|
| 164 |
+
text = re.sub(r'[^\w\s.,!?;:()\[\]{}"\'-]+$', '', text)
|
| 165 |
+
|
| 166 |
+
# Loại bỏ khoảng trắng thừa
|
| 167 |
+
text = ' '.join(text.split())
|
| 168 |
+
|
| 169 |
+
# Loại bỏ lặp từ ở cuối (nếu có pattern như "word word word")
|
| 170 |
+
words = text.split()
|
| 171 |
+
if len(words) >= 3:
|
| 172 |
+
# Kiểm tra 3 từ cuối có lặp không
|
| 173 |
+
last_3 = words[-3:]
|
| 174 |
+
if len(set(last_3)) == 1: # Tất cả giống nhau
|
| 175 |
+
text = ' '.join(words[:-2]) # Loại bỏ 2 từ cuối
|
| 176 |
+
|
| 177 |
+
return text.strip()
|
| 178 |
+
|
| 179 |
+
# ============================================================================
|
| 180 |
+
# TRANSLATION FUNCTIONS
|
| 181 |
+
# ============================================================================
|
| 182 |
+
|
| 183 |
+
def translate_sentence_bidirectional(
|
| 184 |
+
model,
|
| 185 |
+
sentence,
|
| 186 |
+
direction,
|
| 187 |
+
tokenizer,
|
| 188 |
+
vocab_info,
|
| 189 |
+
device='cuda',
|
| 190 |
+
use_beam_search=True,
|
| 191 |
+
beam_size=5,
|
| 192 |
+
max_len=100,
|
| 193 |
+
repetition_penalty=1.3,
|
| 194 |
+
no_repeat_ngram_size=3
|
| 195 |
+
):
|
| 196 |
+
"""
|
| 197 |
+
Dịch một câu theo hướng chỉ định
|
| 198 |
+
|
| 199 |
+
Args:
|
| 200 |
+
model: Transformer model
|
| 201 |
+
sentence: Source sentence (string)
|
| 202 |
+
direction: 'en2vi' hoặc 'vi2en'
|
| 203 |
+
tokenizer: Tokenizer
|
| 204 |
+
vocab_info: Vocabulary info dict
|
| 205 |
+
device: Device
|
| 206 |
+
use_beam_search: Dùng beam search hay greedy
|
| 207 |
+
beam_size: Beam size
|
| 208 |
+
max_len: Max decode length
|
| 209 |
+
repetition_penalty: Penalty cho tokens lặp lại
|
| 210 |
+
no_repeat_ngram_size: Kích thước n-gram để tránh lặp lại
|
| 211 |
+
|
| 212 |
+
Returns:
|
| 213 |
+
translation: Translated sentence
|
| 214 |
+
"""
|
| 215 |
+
model.eval()
|
| 216 |
+
|
| 217 |
+
# Xác định ngôn ngữ source để clean text
|
| 218 |
+
src_lang = 'en' if direction == 'en2vi' else 'vi'
|
| 219 |
+
|
| 220 |
+
# Tiền xử lý câu
|
| 221 |
+
sentence = clean_text(sentence, src_lang)
|
| 222 |
+
|
| 223 |
+
if not sentence.strip():
|
| 224 |
+
return ""
|
| 225 |
+
|
| 226 |
+
# Tạo vocab wrapper
|
| 227 |
+
vocab = VocabWrapper(tokenizer, vocab_info)
|
| 228 |
+
|
| 229 |
+
# Lấy direction token ID
|
| 230 |
+
EN2VI_ID = tokenizer.token_to_id("<en2vi>")
|
| 231 |
+
VI2EN_ID = tokenizer.token_to_id("<vi2en>")
|
| 232 |
+
SOS_ID = vocab_info['sos_id']
|
| 233 |
+
|
| 234 |
+
if EN2VI_ID is None or VI2EN_ID is None:
|
| 235 |
+
raise ValueError("Không tìm thấy direction tokens trong tokenizer!")
|
| 236 |
+
|
| 237 |
+
direction_id = EN2VI_ID if direction == 'en2vi' else VI2EN_ID
|
| 238 |
+
|
| 239 |
+
# Encode
|
| 240 |
+
try:
|
| 241 |
+
tokens = vocab.encode(sentence)
|
| 242 |
+
if not tokens:
|
| 243 |
+
return ""
|
| 244 |
+
# Thêm SOS và direction token vào đầu
|
| 245 |
+
tokens = [SOS_ID, direction_id] + tokens
|
| 246 |
+
except Exception as e:
|
| 247 |
+
print(f"⚠️ Lỗi khi encode: {e}")
|
| 248 |
+
return ""
|
| 249 |
+
|
| 250 |
+
src = torch.LongTensor([tokens]).to(device)
|
| 251 |
+
|
| 252 |
+
# Decode với shared vocabulary (cùng vocab cho cả source và target)
|
| 253 |
+
if use_beam_search:
|
| 254 |
+
_, translation = beam_search_decode(
|
| 255 |
+
model, src, vocab, vocab,
|
| 256 |
+
device, beam_size, max_len,
|
| 257 |
+
alpha=0.6,
|
| 258 |
+
repetition_penalty=repetition_penalty,
|
| 259 |
+
no_repeat_ngram_size=no_repeat_ngram_size
|
| 260 |
+
)
|
| 261 |
+
else:
|
| 262 |
+
_, translation = greedy_decode(
|
| 263 |
+
model, src, vocab, vocab,
|
| 264 |
+
device, max_len,
|
| 265 |
+
repetition_penalty=repetition_penalty,
|
| 266 |
+
no_repeat_ngram_size=no_repeat_ngram_size
|
| 267 |
+
)
|
| 268 |
+
|
| 269 |
+
return translation
|
| 270 |
+
|
| 271 |
+
def translate_file_bidirectional(
|
| 272 |
+
model,
|
| 273 |
+
tokenizer,
|
| 274 |
+
vocab_info,
|
| 275 |
+
source_file,
|
| 276 |
+
output_file,
|
| 277 |
+
direction='vi2en',
|
| 278 |
+
device='cuda',
|
| 279 |
+
use_beam_search=True,
|
| 280 |
+
beam_size=5,
|
| 281 |
+
max_len=100,
|
| 282 |
+
repetition_penalty=1.3,
|
| 283 |
+
no_repeat_ngram_size=3
|
| 284 |
+
):
|
| 285 |
+
"""
|
| 286 |
+
Dịch file source và lưu kết quả
|
| 287 |
+
|
| 288 |
+
Args:
|
| 289 |
+
model: Transformer model
|
| 290 |
+
tokenizer: Tokenizer
|
| 291 |
+
vocab_info: Vocabulary info dict
|
| 292 |
+
source_file: File chứa source sentences (mỗi dòng 1 câu)
|
| 293 |
+
output_file: File output để lưu translations
|
| 294 |
+
direction: 'en2vi' hoặc 'vi2en'
|
| 295 |
+
device: Device
|
| 296 |
+
use_beam_search: Dùng beam search hay greedy
|
| 297 |
+
beam_size: Beam size
|
| 298 |
+
max_len: Max decode length
|
| 299 |
+
"""
|
| 300 |
+
print(f"\n{'='*70}")
|
| 301 |
+
print(f"INFERENCE - {direction.upper()}")
|
| 302 |
+
print(f"{'='*70}")
|
| 303 |
+
print(f"Source file: {source_file}")
|
| 304 |
+
print(f"Output file: {output_file}")
|
| 305 |
+
print(f"Direction: {'English → Vietnamese' if direction == 'en2vi' else 'Vietnamese → English'}")
|
| 306 |
+
print(f"Method: {'Beam Search' if use_beam_search else 'Greedy Search'}")
|
| 307 |
+
if use_beam_search:
|
| 308 |
+
print(f"Beam size: {beam_size}")
|
| 309 |
+
print(f"{'='*70}\n")
|
| 310 |
+
|
| 311 |
+
# Load source sentences
|
| 312 |
+
with open(source_file, 'r', encoding='utf-8') as f:
|
| 313 |
+
source_sentences = [line.strip() for line in f.readlines()]
|
| 314 |
+
|
| 315 |
+
print(f"Loaded {len(source_sentences)} source sentences")
|
| 316 |
+
|
| 317 |
+
# Translate
|
| 318 |
+
translations = []
|
| 319 |
+
model.eval()
|
| 320 |
+
|
| 321 |
+
with torch.no_grad():
|
| 322 |
+
for src_text in tqdm(source_sentences, desc='Translating'):
|
| 323 |
+
translation = translate_sentence_bidirectional(
|
| 324 |
+
model=model,
|
| 325 |
+
sentence=src_text,
|
| 326 |
+
direction=direction,
|
| 327 |
+
tokenizer=tokenizer,
|
| 328 |
+
vocab_info=vocab_info,
|
| 329 |
+
device=device,
|
| 330 |
+
use_beam_search=use_beam_search,
|
| 331 |
+
beam_size=beam_size,
|
| 332 |
+
max_len=max_len,
|
| 333 |
+
repetition_penalty=repetition_penalty,
|
| 334 |
+
no_repeat_ngram_size=no_repeat_ngram_size
|
| 335 |
+
)
|
| 336 |
+
# Post-process: Loại bỏ rác ở cuối câu
|
| 337 |
+
translation = clean_translation_output(translation)
|
| 338 |
+
translations.append(translation)
|
| 339 |
+
|
| 340 |
+
# Save translations
|
| 341 |
+
RESULTS_DIR.mkdir(exist_ok=True, parents=True)
|
| 342 |
+
with open(output_file, 'w', encoding='utf-8') as f:
|
| 343 |
+
for trans in translations:
|
| 344 |
+
f.write(trans + '\n')
|
| 345 |
+
|
| 346 |
+
print(f"\n✓ Saved {len(translations)} translations to {output_file}")
|
| 347 |
+
print(f"{'='*70}\n")
|
| 348 |
+
|
| 349 |
+
def interactive_translation_bidirectional(
|
| 350 |
+
model,
|
| 351 |
+
tokenizer,
|
| 352 |
+
vocab_info,
|
| 353 |
+
device='cuda',
|
| 354 |
+
use_beam_search=True,
|
| 355 |
+
beam_size=5,
|
| 356 |
+
repetition_penalty=1.3,
|
| 357 |
+
no_repeat_ngram_size=3
|
| 358 |
+
):
|
| 359 |
+
"""
|
| 360 |
+
Chế độ dịch tương tác hai chiều
|
| 361 |
+
|
| 362 |
+
Args:
|
| 363 |
+
model: Transformer model
|
| 364 |
+
tokenizer: Tokenizer
|
| 365 |
+
vocab_info: Vocabulary info dict
|
| 366 |
+
device: Device
|
| 367 |
+
use_beam_search: Sử dụng beam search
|
| 368 |
+
beam_size: Beam size
|
| 369 |
+
"""
|
| 370 |
+
print("\n" + "="*70)
|
| 371 |
+
print("CHẾ ĐỘ DỊCH TƯƠNG TÁC HAI CHIỀU")
|
| 372 |
+
print("="*70)
|
| 373 |
+
print("Hướng dẫn:")
|
| 374 |
+
print(" - Nhập 'en:' hoặc 'vi:' để chỉ định ngôn ngữ đầu vào")
|
| 375 |
+
print(" - Ví dụ: 'en: Hello, how are you?' → dịch sang tiếng Việt")
|
| 376 |
+
print(" - Ví dụ: 'vi: Xin chào, bạn khỏe không?' → dịch sang tiếng Anh")
|
| 377 |
+
print(" - Gõ 'quit' hoặc 'exit' để thoát")
|
| 378 |
+
print("="*70 + "\n")
|
| 379 |
+
|
| 380 |
+
while True:
|
| 381 |
+
user_input = input("Nhập câu (hoặc 'en:...' / 'vi:...'): ").strip()
|
| 382 |
+
|
| 383 |
+
if user_input.lower() in ['quit', 'exit', '']:
|
| 384 |
+
print("Tạm biệt!")
|
| 385 |
+
break
|
| 386 |
+
|
| 387 |
+
# Xác định hướng dịch
|
| 388 |
+
if user_input.startswith('en:'):
|
| 389 |
+
direction = 'en2vi'
|
| 390 |
+
sentence = user_input[3:].strip()
|
| 391 |
+
target_lang = "Tiếng Việt"
|
| 392 |
+
elif user_input.startswith('vi:'):
|
| 393 |
+
direction = 'vi2en'
|
| 394 |
+
sentence = user_input[3:].strip()
|
| 395 |
+
target_lang = "Tiếng Anh"
|
| 396 |
+
else:
|
| 397 |
+
# Mặc định: tự động phát hiện (đơn giản)
|
| 398 |
+
# Nếu có nhiều ký tự Latin không dấu → tiếng Anh
|
| 399 |
+
# Ngược lại → tiếng Việt
|
| 400 |
+
has_vietnamese_chars = any(ord(c) >= 0x0100 for c in user_input)
|
| 401 |
+
if has_vietnamese_chars:
|
| 402 |
+
direction = 'vi2en'
|
| 403 |
+
target_lang = "Tiếng Anh"
|
| 404 |
+
else:
|
| 405 |
+
direction = 'en2vi'
|
| 406 |
+
target_lang = "Tiếng Việt"
|
| 407 |
+
sentence = user_input
|
| 408 |
+
|
| 409 |
+
if not sentence:
|
| 410 |
+
print("⚠️ Vui lòng nhập câu cần dịch.\n")
|
| 411 |
+
continue
|
| 412 |
+
|
| 413 |
+
try:
|
| 414 |
+
translation = translate_sentence_bidirectional(
|
| 415 |
+
model=model,
|
| 416 |
+
sentence=sentence,
|
| 417 |
+
direction=direction,
|
| 418 |
+
tokenizer=tokenizer,
|
| 419 |
+
vocab_info=vocab_info,
|
| 420 |
+
device=device,
|
| 421 |
+
use_beam_search=use_beam_search,
|
| 422 |
+
beam_size=beam_size,
|
| 423 |
+
repetition_penalty=repetition_penalty,
|
| 424 |
+
no_repeat_ngram_size=no_repeat_ngram_size
|
| 425 |
+
)
|
| 426 |
+
print(f"{target_lang}: {translation}\n")
|
| 427 |
+
except Exception as e:
|
| 428 |
+
print(f"❌ Lỗi khi dịch: {e}\n")
|
| 429 |
+
|
| 430 |
+
# ============================================================================
|
| 431 |
+
# MAIN
|
| 432 |
+
# ============================================================================
|
| 433 |
+
|
| 434 |
+
def main():
|
| 435 |
+
parser = argparse.ArgumentParser(
|
| 436 |
+
description='Dịch hai chiều: English ↔ Vietnamese',
|
| 437 |
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
| 438 |
+
epilog="""
|
| 439 |
+
Ví dụ sử dụng:
|
| 440 |
+
# Dịch file tiếng Việt sang tiếng Anh
|
| 441 |
+
python src/bidirectional_translate.py --checkpoint checkpoints/best_model.pt \\
|
| 442 |
+
--source data/raw/test.vi --output results/test_en.txt --direction vi2en
|
| 443 |
+
|
| 444 |
+
# Dịch file tiếng Anh sang tiếng Việt
|
| 445 |
+
python src/bidirectional_translate.py --checkpoint checkpoints/best_model.pt \\
|
| 446 |
+
--source data/raw/test.en --output results/test_vi.txt --direction en2vi
|
| 447 |
+
|
| 448 |
+
# Chế độ tương tác
|
| 449 |
+
python src/bidirectional_translate.py --checkpoint checkpoints/best_model.pt --interactive
|
| 450 |
+
"""
|
| 451 |
+
)
|
| 452 |
+
parser.add_argument('--checkpoint', type=str, required=True,
|
| 453 |
+
help='Đường dẫn đến checkpoint file')
|
| 454 |
+
parser.add_argument('--source', type=str, default=None,
|
| 455 |
+
help='File source (mỗi dòng 1 câu)')
|
| 456 |
+
parser.add_argument('--output', type=str, default=None,
|
| 457 |
+
help='File output để lưu translations')
|
| 458 |
+
parser.add_argument('--direction', type=str, default='vi2en',
|
| 459 |
+
choices=['vi2en', 'en2vi'],
|
| 460 |
+
help='Hướng dịch: vi2en (Việt→Anh) hoặc en2vi (Anh→Việt)')
|
| 461 |
+
parser.add_argument('--model_size', type=str, default='custom_25m',
|
| 462 |
+
choices=['custom_25m', 'base'],
|
| 463 |
+
help='Kích thước model')
|
| 464 |
+
parser.add_argument('--beam_size', type=int, default=5,
|
| 465 |
+
help='Beam size cho beam search')
|
| 466 |
+
parser.add_argument('--greedy', action='store_true',
|
| 467 |
+
help='Dùng greedy search thay vì beam search')
|
| 468 |
+
parser.add_argument('--max_len', type=int, default=100,
|
| 469 |
+
help='Maximum decode length')
|
| 470 |
+
parser.add_argument('--repetition_penalty', type=float, default=1.3,
|
| 471 |
+
help='Repetition penalty factor (>1.0 để giảm repetition)')
|
| 472 |
+
parser.add_argument('--no_repeat_ngram_size', type=int, default=3,
|
| 473 |
+
help='Kích thước n-gram để tránh lặp lại (0 = tắt)')
|
| 474 |
+
parser.add_argument('--device', type=str, default='cuda',
|
| 475 |
+
help='Device (cuda/cpu)')
|
| 476 |
+
parser.add_argument('--interactive', action='store_true',
|
| 477 |
+
help='Chế độ dịch tương tác')
|
| 478 |
+
|
| 479 |
+
args = parser.parse_args()
|
| 480 |
+
|
| 481 |
+
# Check checkpoint
|
| 482 |
+
checkpoint_path = Path(args.checkpoint)
|
| 483 |
+
if not checkpoint_path.exists():
|
| 484 |
+
print(f"❌ Không tìm thấy checkpoint: {checkpoint_path}")
|
| 485 |
+
return
|
| 486 |
+
|
| 487 |
+
# Device
|
| 488 |
+
device = args.device
|
| 489 |
+
if device == 'cuda' and not torch.cuda.is_available():
|
| 490 |
+
print("⚠️ CUDA không khả dụng, dùng CPU")
|
| 491 |
+
device = 'cpu'
|
| 492 |
+
|
| 493 |
+
# Load model
|
| 494 |
+
model, vocab_info = load_model(
|
| 495 |
+
checkpoint_path,
|
| 496 |
+
args.model_size,
|
| 497 |
+
device
|
| 498 |
+
)
|
| 499 |
+
|
| 500 |
+
# Load tokenizer
|
| 501 |
+
tokenizer = load_shared_tokenizer()
|
| 502 |
+
|
| 503 |
+
# Interactive mode
|
| 504 |
+
if args.interactive:
|
| 505 |
+
interactive_translation_bidirectional(
|
| 506 |
+
model=model,
|
| 507 |
+
tokenizer=tokenizer,
|
| 508 |
+
vocab_info=vocab_info,
|
| 509 |
+
device=device,
|
| 510 |
+
use_beam_search=not args.greedy,
|
| 511 |
+
beam_size=args.beam_size,
|
| 512 |
+
repetition_penalty=args.repetition_penalty,
|
| 513 |
+
no_repeat_ngram_size=args.no_repeat_ngram_size
|
| 514 |
+
)
|
| 515 |
+
return
|
| 516 |
+
|
| 517 |
+
# File translation mode
|
| 518 |
+
if not args.source or not args.output:
|
| 519 |
+
print("❌ Cần cung cấp --source và --output (hoặc dùng --interactive)")
|
| 520 |
+
parser.print_help()
|
| 521 |
+
return
|
| 522 |
+
|
| 523 |
+
source_path = Path(args.source)
|
| 524 |
+
if not source_path.exists():
|
| 525 |
+
print(f"❌ Không tìm thấy source file: {source_path}")
|
| 526 |
+
return
|
| 527 |
+
|
| 528 |
+
# Translate
|
| 529 |
+
translate_file_bidirectional(
|
| 530 |
+
model=model,
|
| 531 |
+
tokenizer=tokenizer,
|
| 532 |
+
vocab_info=vocab_info,
|
| 533 |
+
source_file=source_path,
|
| 534 |
+
output_file=Path(args.output),
|
| 535 |
+
direction=args.direction,
|
| 536 |
+
device=device,
|
| 537 |
+
use_beam_search=not args.greedy,
|
| 538 |
+
beam_size=args.beam_size,
|
| 539 |
+
max_len=args.max_len,
|
| 540 |
+
repetition_penalty=args.repetition_penalty,
|
| 541 |
+
no_repeat_ngram_size=args.no_repeat_ngram_size
|
| 542 |
+
)
|
| 543 |
+
|
| 544 |
+
if __name__ == '__main__':
|
| 545 |
+
main()
|
| 546 |
+
|
src/checkpoint_manager.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
CHECKPOINT MANAGER
|
| 3 |
+
Tiện ích để quản lý và chuyển đổi giữa các checkpoint
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
import argparse
|
| 8 |
+
import json
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from datetime import datetime
|
| 11 |
+
|
| 12 |
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
| 13 |
+
CHECKPOINT_DIR = PROJECT_ROOT / 'checkpoints'
|
| 14 |
+
|
| 15 |
+
def list_checkpoints():
|
| 16 |
+
"""Liệt kê tất cả checkpoint có sẵn"""
|
| 17 |
+
print("="*70)
|
| 18 |
+
print("DANH SÁCH CHECKPOINT")
|
| 19 |
+
print("="*70)
|
| 20 |
+
|
| 21 |
+
checkpoints = list(CHECKPOINT_DIR.glob('*.pt'))
|
| 22 |
+
|
| 23 |
+
if not checkpoints:
|
| 24 |
+
print("❌ Không tìm thấy checkpoint nào!")
|
| 25 |
+
return
|
| 26 |
+
|
| 27 |
+
# Phân loại checkpoint
|
| 28 |
+
best_models = []
|
| 29 |
+
epoch_checkpoints = []
|
| 30 |
+
finetune_checkpoints = []
|
| 31 |
+
other_checkpoints = []
|
| 32 |
+
|
| 33 |
+
for ckpt in sorted(checkpoints):
|
| 34 |
+
name = ckpt.name
|
| 35 |
+
if name == 'best_model.pt':
|
| 36 |
+
best_models.append(('best_model.pt', 'Original best model'))
|
| 37 |
+
elif name == 'best_model_finetuned.pt':
|
| 38 |
+
best_models.append(('best_model_finetuned.pt', 'Finetuned best model'))
|
| 39 |
+
elif name.startswith('checkpoint_epoch_'):
|
| 40 |
+
epoch_checkpoints.append(ckpt)
|
| 41 |
+
elif name.startswith('finetune_checkpoint_epoch_'):
|
| 42 |
+
finetune_checkpoints.append(ckpt)
|
| 43 |
+
else:
|
| 44 |
+
other_checkpoints.append(ckpt)
|
| 45 |
+
|
| 46 |
+
# In best models
|
| 47 |
+
if best_models:
|
| 48 |
+
print("\n📌 BEST MODELS:")
|
| 49 |
+
for name, desc in best_models:
|
| 50 |
+
ckpt_path = CHECKPOINT_DIR / name
|
| 51 |
+
if ckpt_path.exists():
|
| 52 |
+
try:
|
| 53 |
+
ckpt = torch.load(ckpt_path, map_location='cpu')
|
| 54 |
+
epoch = ckpt.get('epoch', 'N/A')
|
| 55 |
+
val_loss = ckpt.get('val_loss', 'N/A')
|
| 56 |
+
size_mb = ckpt_path.stat().st_size / (1024 * 1024)
|
| 57 |
+
print(f" ✓ {name}")
|
| 58 |
+
print(f" - {desc}")
|
| 59 |
+
print(f" - Epoch: {epoch}")
|
| 60 |
+
print(f" - Val Loss: {val_loss:.4f}" if isinstance(val_loss, float) else f" - Val Loss: {val_loss}")
|
| 61 |
+
print(f" - Size: {size_mb:.2f} MB")
|
| 62 |
+
|
| 63 |
+
# Kiểm tra finetune config
|
| 64 |
+
if 'finetune_config' in ckpt:
|
| 65 |
+
finetune_cfg = ckpt['finetune_config']
|
| 66 |
+
print(f" - Finetuned from: {Path(finetune_cfg.get('checkpoint_path', 'N/A')).name}")
|
| 67 |
+
print(f" - LR Factor: {finetune_cfg.get('finetune_lr_factor', 'N/A')}")
|
| 68 |
+
print()
|
| 69 |
+
except Exception as e:
|
| 70 |
+
print(f" ⚠️ {name} (lỗi khi đọc: {e})")
|
| 71 |
+
|
| 72 |
+
# In epoch checkpoints
|
| 73 |
+
if epoch_checkpoints:
|
| 74 |
+
print(f"\n📦 EPOCH CHECKPOINTS ({len(epoch_checkpoints)} files):")
|
| 75 |
+
for ckpt in epoch_checkpoints[:5]: # Chỉ hiển thị 5 đầu
|
| 76 |
+
try:
|
| 77 |
+
ckpt_data = torch.load(ckpt, map_location='cpu')
|
| 78 |
+
epoch = ckpt_data.get('epoch', 'N/A')
|
| 79 |
+
val_loss = ckpt_data.get('val_loss', 'N/A')
|
| 80 |
+
size_mb = ckpt.stat().st_size / (1024 * 1024)
|
| 81 |
+
print(f" - {ckpt.name}: Epoch {epoch}, Val Loss: {val_loss:.4f}" if isinstance(val_loss, float) else f" - {ckpt.name}: Epoch {epoch}, Val Loss: {val_loss}")
|
| 82 |
+
print(f" Size: {size_mb:.2f} MB")
|
| 83 |
+
except:
|
| 84 |
+
print(f" - {ckpt.name} (lỗi khi đọc)")
|
| 85 |
+
|
| 86 |
+
if len(epoch_checkpoints) > 5:
|
| 87 |
+
print(f" ... và {len(epoch_checkpoints) - 5} checkpoint khác")
|
| 88 |
+
|
| 89 |
+
# In finetune checkpoints
|
| 90 |
+
if finetune_checkpoints:
|
| 91 |
+
print(f"\n🔧 FINETUNE CHECKPOINTS ({len(finetune_checkpoints)} files):")
|
| 92 |
+
for ckpt in finetune_checkpoints[:5]: # Chỉ hiển thị 5 đầu
|
| 93 |
+
try:
|
| 94 |
+
ckpt_data = torch.load(ckpt, map_location='cpu')
|
| 95 |
+
epoch = ckpt_data.get('epoch', 'N/A')
|
| 96 |
+
val_loss = ckpt_data.get('val_loss', 'N/A')
|
| 97 |
+
size_mb = ckpt.stat().st_size / (1024 * 1024)
|
| 98 |
+
print(f" - {ckpt.name}: Epoch {epoch}, Val Loss: {val_loss:.4f}" if isinstance(val_loss, float) else f" - {ckpt.name}: Epoch {epoch}, Val Loss: {val_loss}")
|
| 99 |
+
print(f" Size: {size_mb:.2f} MB")
|
| 100 |
+
except:
|
| 101 |
+
print(f" - {ckpt.name} (lỗi khi đọc)")
|
| 102 |
+
|
| 103 |
+
if len(finetune_checkpoints) > 5:
|
| 104 |
+
print(f" ... và {len(finetune_checkpoints) - 5} checkpoint khác")
|
| 105 |
+
|
| 106 |
+
# In other checkpoints
|
| 107 |
+
if other_checkpoints:
|
| 108 |
+
print(f"\n📁 OTHER CHECKPOINTS ({len(other_checkpoints)} files):")
|
| 109 |
+
for ckpt in other_checkpoints:
|
| 110 |
+
size_mb = ckpt.stat().st_size / (1024 * 1024)
|
| 111 |
+
print(f" - {ckpt.name} ({size_mb:.2f} MB)")
|
| 112 |
+
|
| 113 |
+
print("\n" + "="*70)
|
| 114 |
+
|
| 115 |
+
def switch_checkpoint(source_name, target_name='best_model.pt', backup=True):
|
| 116 |
+
"""
|
| 117 |
+
Chuyển đổi checkpoint (copy checkpoint thành best_model.pt)
|
| 118 |
+
|
| 119 |
+
Args:
|
| 120 |
+
source_name: Tên checkpoint nguồn
|
| 121 |
+
target_name: Tên checkpoint đích (mặc định: best_model.pt)
|
| 122 |
+
backup: Có backup checkpoint cũ không
|
| 123 |
+
"""
|
| 124 |
+
source_path = CHECKPOINT_DIR / source_name
|
| 125 |
+
target_path = CHECKPOINT_DIR / target_name
|
| 126 |
+
|
| 127 |
+
if not source_path.exists():
|
| 128 |
+
print(f"❌ Không tìm thấy checkpoint: {source_path}")
|
| 129 |
+
return False
|
| 130 |
+
|
| 131 |
+
# Backup checkpoint cũ nếu có
|
| 132 |
+
if target_path.exists() and backup:
|
| 133 |
+
backup_name = f"{target_name}.backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
| 134 |
+
backup_path = CHECKPOINT_DIR / backup_name
|
| 135 |
+
print(f"→ Đang backup checkpoint cũ: {backup_name}")
|
| 136 |
+
import shutil
|
| 137 |
+
shutil.copy2(target_path, backup_path)
|
| 138 |
+
print(f" ✓ Đã backup: {backup_path}")
|
| 139 |
+
|
| 140 |
+
# Copy checkpoint mới
|
| 141 |
+
print(f"→ Đang copy {source_name} → {target_name}")
|
| 142 |
+
import shutil
|
| 143 |
+
shutil.copy2(source_path, target_path)
|
| 144 |
+
print(f" ✓ Đã chuyển đổi checkpoint!")
|
| 145 |
+
|
| 146 |
+
# Hiển thị thông tin checkpoint mới
|
| 147 |
+
try:
|
| 148 |
+
ckpt = torch.load(target_path, map_location='cpu')
|
| 149 |
+
epoch = ckpt.get('epoch', 'N/A')
|
| 150 |
+
val_loss = ckpt.get('val_loss', 'N/A')
|
| 151 |
+
print(f"\n Thông tin checkpoint mới:")
|
| 152 |
+
print(f" - Epoch: {epoch}")
|
| 153 |
+
print(f" - Val Loss: {val_loss:.4f}" if isinstance(val_loss, float) else f" - Val Loss: {val_loss}")
|
| 154 |
+
|
| 155 |
+
if 'finetune_config' in ckpt:
|
| 156 |
+
finetune_cfg = ckpt['finetune_config']
|
| 157 |
+
print(f" - Finetuned from: {Path(finetune_cfg.get('checkpoint_path', 'N/A')).name}")
|
| 158 |
+
except Exception as e:
|
| 159 |
+
print(f" ⚠️ Lỗi khi đọc checkpoint: {e}")
|
| 160 |
+
|
| 161 |
+
return True
|
| 162 |
+
|
| 163 |
+
def compare_checkpoints(checkpoint1_name, checkpoint2_name):
|
| 164 |
+
"""So sánh 2 checkpoint"""
|
| 165 |
+
ckpt1_path = CHECKPOINT_DIR / checkpoint1_name
|
| 166 |
+
ckpt2_path = CHECKPOINT_DIR / checkpoint2_name
|
| 167 |
+
|
| 168 |
+
if not ckpt1_path.exists():
|
| 169 |
+
print(f"❌ Không tìm thấy checkpoint: {checkpoint1_name}")
|
| 170 |
+
return
|
| 171 |
+
|
| 172 |
+
if not ckpt2_path.exists():
|
| 173 |
+
print(f"❌ Không tìm thấy checkpoint: {checkpoint2_name}")
|
| 174 |
+
return
|
| 175 |
+
|
| 176 |
+
print("="*70)
|
| 177 |
+
print("SO SÁNH CHECKPOINT")
|
| 178 |
+
print("="*70)
|
| 179 |
+
|
| 180 |
+
try:
|
| 181 |
+
ckpt1 = torch.load(ckpt1_path, map_location='cpu')
|
| 182 |
+
ckpt2 = torch.load(ckpt2_path, map_location='cpu')
|
| 183 |
+
|
| 184 |
+
print(f"\n📌 {checkpoint1_name}:")
|
| 185 |
+
print(f" - Epoch: {ckpt1.get('epoch', 'N/A')}")
|
| 186 |
+
print(f" - Val Loss: {ckpt1.get('val_loss', 'N/A'):.4f}" if isinstance(ckpt1.get('val_loss'), float) else f" - Val Loss: {ckpt1.get('val_loss', 'N/A')}")
|
| 187 |
+
print(f" - Size: {ckpt1_path.stat().st_size / (1024 * 1024):.2f} MB")
|
| 188 |
+
|
| 189 |
+
print(f"\n📌 {checkpoint2_name}:")
|
| 190 |
+
print(f" - Epoch: {ckpt2.get('epoch', 'N/A')}")
|
| 191 |
+
print(f" - Val Loss: {ckpt2.get('val_loss', 'N/A'):.4f}" if isinstance(ckpt2.get('val_loss'), float) else f" - Val Loss: {ckpt2.get('val_loss', 'N/A')}")
|
| 192 |
+
print(f" - Size: {ckpt2_path.stat().st_size / (1024 * 1024):.2f} MB")
|
| 193 |
+
|
| 194 |
+
# So sánh val_loss
|
| 195 |
+
val_loss1 = ckpt1.get('val_loss')
|
| 196 |
+
val_loss2 = ckpt2.get('val_loss')
|
| 197 |
+
|
| 198 |
+
if isinstance(val_loss1, float) and isinstance(val_loss2, float):
|
| 199 |
+
print(f"\n📊 So sánh:")
|
| 200 |
+
if val_loss1 < val_loss2:
|
| 201 |
+
print(f" ✓ {checkpoint1_name} tốt hơn (val_loss thấp hơn {val_loss2 - val_loss1:.4f})")
|
| 202 |
+
elif val_loss2 < val_loss1:
|
| 203 |
+
print(f" ✓ {checkpoint2_name} tốt hơn (val_loss thấp hơn {val_loss1 - val_loss2:.4f})")
|
| 204 |
+
else:
|
| 205 |
+
print(f" = Cả 2 checkpoint có val_loss bằng nhau")
|
| 206 |
+
|
| 207 |
+
# Kiểm tra finetune config
|
| 208 |
+
if 'finetune_config' in ckpt1:
|
| 209 |
+
print(f"\n🔧 {checkpoint1_name} là model đã finetune")
|
| 210 |
+
if 'finetune_config' in ckpt2:
|
| 211 |
+
print(f"\n🔧 {checkpoint2_name} là model đã finetune")
|
| 212 |
+
|
| 213 |
+
except Exception as e:
|
| 214 |
+
print(f"❌ Lỗi khi so sánh: {e}")
|
| 215 |
+
|
| 216 |
+
print("="*70)
|
| 217 |
+
|
| 218 |
+
def main():
|
| 219 |
+
parser = argparse.ArgumentParser(description='Checkpoint Manager')
|
| 220 |
+
subparsers = parser.add_subparsers(dest='command', help='Command to execute')
|
| 221 |
+
|
| 222 |
+
# List command
|
| 223 |
+
list_parser = subparsers.add_parser('list', help='List all checkpoints')
|
| 224 |
+
|
| 225 |
+
# Switch command
|
| 226 |
+
switch_parser = subparsers.add_parser('switch', help='Switch checkpoint (copy to best_model.pt)')
|
| 227 |
+
switch_parser.add_argument('source', type=str, help='Source checkpoint name')
|
| 228 |
+
switch_parser.add_argument('--target', type=str, default='best_model.pt',
|
| 229 |
+
help='Target checkpoint name (default: best_model.pt)')
|
| 230 |
+
switch_parser.add_argument('--no-backup', action='store_true',
|
| 231 |
+
help='Do not backup old checkpoint')
|
| 232 |
+
|
| 233 |
+
# Compare command
|
| 234 |
+
compare_parser = subparsers.add_parser('compare', help='Compare two checkpoints')
|
| 235 |
+
compare_parser.add_argument('checkpoint1', type=str, help='First checkpoint name')
|
| 236 |
+
compare_parser.add_argument('checkpoint2', type=str, help='Second checkpoint name')
|
| 237 |
+
|
| 238 |
+
args = parser.parse_args()
|
| 239 |
+
|
| 240 |
+
if args.command == 'list':
|
| 241 |
+
list_checkpoints()
|
| 242 |
+
elif args.command == 'switch':
|
| 243 |
+
switch_checkpoint(args.source, args.target, backup=not args.no_backup)
|
| 244 |
+
elif args.command == 'compare':
|
| 245 |
+
compare_checkpoints(args.checkpoint1, args.checkpoint2)
|
| 246 |
+
else:
|
| 247 |
+
parser.print_help()
|
| 248 |
+
|
| 249 |
+
if __name__ == "__main__":
|
| 250 |
+
main()
|
| 251 |
+
|
src/complete_transformer.py
ADDED
|
@@ -0,0 +1,539 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
COMPLETE TRANSFORMER MODEL
|
| 3 |
+
Mô hình Transformer hoàn chỉnh cho dịch máy Seq2Seq
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
import torch.nn as nn
|
| 8 |
+
from .transformer_encoder_decoder import (
|
| 9 |
+
Encoder, Decoder,
|
| 10 |
+
create_padding_mask, create_target_mask
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
# ============================================================================
|
| 14 |
+
# TRANSFORMER MODEL
|
| 15 |
+
# ============================================================================
|
| 16 |
+
|
| 17 |
+
class Transformer(nn.Module):
|
| 18 |
+
"""
|
| 19 |
+
Mô hình Transformer hoàn chỉnh cho Neural Machine Translation
|
| 20 |
+
|
| 21 |
+
Args:
|
| 22 |
+
src_vocab_size: Kích thước vocabulary source language
|
| 23 |
+
tgt_vocab_size: Kích thước vocabulary target language
|
| 24 |
+
d_model: Dimension của model (mặc định 512)
|
| 25 |
+
n_layers: Số lượng encoder/decoder layers (mặc định 6)
|
| 26 |
+
n_heads: Số lượng attention heads (mặc định 8)
|
| 27 |
+
d_ff: Dimension của feed-forward network (mặc định 2048)
|
| 28 |
+
dropout: Dropout rate (mặc định 0.1)
|
| 29 |
+
max_len: Maximum sequence length (mặc định 5000)
|
| 30 |
+
pad_idx: Index của padding token (mặc định 0)
|
| 31 |
+
"""
|
| 32 |
+
def __init__(
|
| 33 |
+
self,
|
| 34 |
+
src_vocab_size,
|
| 35 |
+
tgt_vocab_size,
|
| 36 |
+
d_model=512,
|
| 37 |
+
n_layers=6,
|
| 38 |
+
n_heads=8,
|
| 39 |
+
d_ff=2048,
|
| 40 |
+
dropout=0.1,
|
| 41 |
+
max_len=5000,
|
| 42 |
+
pad_idx=0
|
| 43 |
+
):
|
| 44 |
+
super().__init__()
|
| 45 |
+
|
| 46 |
+
self.pad_idx = pad_idx
|
| 47 |
+
|
| 48 |
+
# Encoder
|
| 49 |
+
self.encoder = Encoder(
|
| 50 |
+
vocab_size=src_vocab_size,
|
| 51 |
+
d_model=d_model,
|
| 52 |
+
n_layers=n_layers,
|
| 53 |
+
n_heads=n_heads,
|
| 54 |
+
d_ff=d_ff,
|
| 55 |
+
dropout=dropout,
|
| 56 |
+
max_len=max_len
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
# Decoder
|
| 60 |
+
self.decoder = Decoder(
|
| 61 |
+
vocab_size=tgt_vocab_size,
|
| 62 |
+
d_model=d_model,
|
| 63 |
+
n_layers=n_layers,
|
| 64 |
+
n_heads=n_heads,
|
| 65 |
+
d_ff=d_ff,
|
| 66 |
+
dropout=dropout,
|
| 67 |
+
max_len=max_len
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
# Khởi tạo weights
|
| 71 |
+
self._init_weights()
|
| 72 |
+
|
| 73 |
+
def _init_weights(self):
|
| 74 |
+
"""
|
| 75 |
+
Khởi tạo weights theo Xavier Uniform
|
| 76 |
+
"""
|
| 77 |
+
for p in self.parameters():
|
| 78 |
+
if p.dim() > 1:
|
| 79 |
+
nn.init.xavier_uniform_(p)
|
| 80 |
+
|
| 81 |
+
def forward(self, src, tgt):
|
| 82 |
+
"""
|
| 83 |
+
Forward pass
|
| 84 |
+
|
| 85 |
+
Args:
|
| 86 |
+
src: Source sequence [batch_size, src_len]
|
| 87 |
+
tgt: Target sequence [batch_size, tgt_len]
|
| 88 |
+
|
| 89 |
+
Returns:
|
| 90 |
+
output: Logits [batch_size, tgt_len, tgt_vocab_size]
|
| 91 |
+
"""
|
| 92 |
+
# Tạo masks
|
| 93 |
+
src_mask = create_padding_mask(src, self.pad_idx)
|
| 94 |
+
tgt_mask = create_target_mask(tgt, self.pad_idx)
|
| 95 |
+
|
| 96 |
+
# Encoder
|
| 97 |
+
encoder_output = self.encoder(src, src_mask)
|
| 98 |
+
|
| 99 |
+
# Decoder
|
| 100 |
+
output = self.decoder(tgt, encoder_output, src_mask, tgt_mask)
|
| 101 |
+
|
| 102 |
+
return output
|
| 103 |
+
|
| 104 |
+
def encode(self, src):
|
| 105 |
+
"""
|
| 106 |
+
Chỉ chạy encoder (dùng khi inference)
|
| 107 |
+
|
| 108 |
+
Args:
|
| 109 |
+
src: Source sequence [batch_size, src_len]
|
| 110 |
+
|
| 111 |
+
Returns:
|
| 112 |
+
encoder_output: [batch_size, src_len, d_model]
|
| 113 |
+
src_mask: [batch_size, 1, 1, src_len]
|
| 114 |
+
"""
|
| 115 |
+
src_mask = create_padding_mask(src, self.pad_idx)
|
| 116 |
+
encoder_output = self.encoder(src, src_mask)
|
| 117 |
+
return encoder_output, src_mask
|
| 118 |
+
|
| 119 |
+
def decode(self, tgt, encoder_output, src_mask):
|
| 120 |
+
"""
|
| 121 |
+
Chỉ chạy decoder (dùng khi inference)
|
| 122 |
+
|
| 123 |
+
Args:
|
| 124 |
+
tgt: Target sequence [batch_size, tgt_len]
|
| 125 |
+
encoder_output: Encoder output [batch_size, src_len, d_model]
|
| 126 |
+
src_mask: Source mask [batch_size, 1, 1, src_len]
|
| 127 |
+
|
| 128 |
+
Returns:
|
| 129 |
+
output: Logits [batch_size, tgt_len, tgt_vocab_size]
|
| 130 |
+
"""
|
| 131 |
+
tgt_mask = create_target_mask(tgt, self.pad_idx)
|
| 132 |
+
output = self.decoder(tgt, encoder_output, src_mask, tgt_mask)
|
| 133 |
+
return output
|
| 134 |
+
|
| 135 |
+
# ============================================================================
|
| 136 |
+
# TRANSFORMER WITH SHARED VOCABULARY & WEIGHT TYING
|
| 137 |
+
# ============================================================================
|
| 138 |
+
|
| 139 |
+
class TransformerShared(nn.Module):
|
| 140 |
+
"""
|
| 141 |
+
Transformer với Shared Vocabulary và Weight Tying
|
| 142 |
+
|
| 143 |
+
Đặc điểm:
|
| 144 |
+
- Dùng chung 1 vocabulary cho cả source và target
|
| 145 |
+
- Embedding input và output layer chia sẻ weights (Weight Tying)
|
| 146 |
+
- Tiết kiệm ~50% parameters so với model riêng biệt
|
| 147 |
+
- Học được mối liên hệ trực tiếp giữa 2 ngôn ngữ tốt hơn
|
| 148 |
+
|
| 149 |
+
Args:
|
| 150 |
+
vocab_size: Kích thước shared vocabulary
|
| 151 |
+
d_model: Dimension của model (mặc định 512)
|
| 152 |
+
n_layers: Số lượng encoder/decoder layers (mặc định 6)
|
| 153 |
+
n_heads: Số lượng attention heads (mặc định 8)
|
| 154 |
+
d_ff: Dimension của feed-forward network (mặc định 2048)
|
| 155 |
+
dropout: Dropout rate (mặc định 0.1)
|
| 156 |
+
max_len: Maximum sequence length (mặc định 5000)
|
| 157 |
+
pad_idx: Index của padding token (mặc định 0)
|
| 158 |
+
use_weight_tying: Có dùng weight tying không (mặc định True)
|
| 159 |
+
"""
|
| 160 |
+
def __init__(
|
| 161 |
+
self,
|
| 162 |
+
vocab_size,
|
| 163 |
+
d_model=512,
|
| 164 |
+
n_layers=6,
|
| 165 |
+
n_heads=8,
|
| 166 |
+
d_ff=2048,
|
| 167 |
+
dropout=0.1,
|
| 168 |
+
max_len=5000,
|
| 169 |
+
pad_idx=0,
|
| 170 |
+
use_weight_tying=True
|
| 171 |
+
):
|
| 172 |
+
super().__init__()
|
| 173 |
+
|
| 174 |
+
self.pad_idx = pad_idx
|
| 175 |
+
self.d_model = d_model
|
| 176 |
+
self.use_weight_tying = use_weight_tying
|
| 177 |
+
|
| 178 |
+
# --- KHÁC BIỆT LỚN NHẤT ---
|
| 179 |
+
# Chỉ tạo 1 Embedding matrix dùng cho cả 2 ngôn ngữ
|
| 180 |
+
from .transformer_components import Embedding, PositionalEncoding
|
| 181 |
+
self.shared_embedding = Embedding(vocab_size, d_model)
|
| 182 |
+
|
| 183 |
+
# Positional Encoding
|
| 184 |
+
self.pos_encoding = PositionalEncoding(d_model, max_len, dropout)
|
| 185 |
+
|
| 186 |
+
# Encoder (dùng shared embedding)
|
| 187 |
+
# Tạo Encoder nhưng sẽ thay embedding sau
|
| 188 |
+
self.encoder = Encoder(
|
| 189 |
+
vocab_size=vocab_size, # Dùng chung vocab_size
|
| 190 |
+
d_model=d_model,
|
| 191 |
+
n_layers=n_layers,
|
| 192 |
+
n_heads=n_heads,
|
| 193 |
+
d_ff=d_ff,
|
| 194 |
+
dropout=dropout,
|
| 195 |
+
max_len=max_len
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
# Thay thế embedding của encoder bằng shared embedding
|
| 199 |
+
# Quan trọng: Phải thay thế sau khi tạo Encoder
|
| 200 |
+
self.encoder.embedding = self.shared_embedding
|
| 201 |
+
|
| 202 |
+
# Decoder (dùng shared embedding)
|
| 203 |
+
# Tạo Decoder nhưng sẽ thay embedding sau
|
| 204 |
+
self.decoder = Decoder(
|
| 205 |
+
vocab_size=vocab_size, # Dùng chung vocab_size
|
| 206 |
+
d_model=d_model,
|
| 207 |
+
n_layers=n_layers,
|
| 208 |
+
n_heads=n_heads,
|
| 209 |
+
d_ff=d_ff,
|
| 210 |
+
dropout=dropout,
|
| 211 |
+
max_len=max_len
|
| 212 |
+
)
|
| 213 |
+
|
| 214 |
+
# Thay thế embedding của decoder bằng shared embedding
|
| 215 |
+
# Quan trọng: Phải thay thế sau khi tạo Decoder
|
| 216 |
+
self.decoder.embedding = self.shared_embedding
|
| 217 |
+
|
| 218 |
+
# QUAN TRỌNG: Bỏ qua fc_out của Decoder vì chúng ta dùng output_layer riêng
|
| 219 |
+
# Decoder.fc_out sẽ được thay thế bằng identity function
|
| 220 |
+
self.decoder.fc_out = nn.Identity()
|
| 221 |
+
|
| 222 |
+
# Output layer
|
| 223 |
+
self.output_layer = nn.Linear(d_model, vocab_size, bias=False)
|
| 224 |
+
|
| 225 |
+
# Khởi tạo weights TRƯỚC khi weight tying
|
| 226 |
+
self._init_weights()
|
| 227 |
+
|
| 228 |
+
# --- KÍCH HOẠT WEIGHT TYING ---
|
| 229 |
+
if use_weight_tying:
|
| 230 |
+
# Dòng này giúp tiết kiệm 50% tham số vocab
|
| 231 |
+
# Embedding weight và output layer weight chia sẻ nhau
|
| 232 |
+
# QUAN TRỌNG: Phải gán SAU _init_weights() để không bị reset
|
| 233 |
+
self.output_layer.weight = self.shared_embedding.embedding.weight
|
| 234 |
+
print("✓ Weight Tying enabled: Embedding và Output layer chia sẻ weights")
|
| 235 |
+
|
| 236 |
+
def _init_weights(self):
|
| 237 |
+
"""
|
| 238 |
+
Khởi tạo weights theo Xavier Uniform
|
| 239 |
+
"""
|
| 240 |
+
for p in self.parameters():
|
| 241 |
+
if p.dim() > 1:
|
| 242 |
+
nn.init.xavier_uniform_(p)
|
| 243 |
+
|
| 244 |
+
def forward(self, src, tgt):
|
| 245 |
+
"""
|
| 246 |
+
Forward pass
|
| 247 |
+
|
| 248 |
+
Args:
|
| 249 |
+
src: Source sequence [batch_size, src_len]
|
| 250 |
+
tgt: Target sequence [batch_size, tgt_len]
|
| 251 |
+
|
| 252 |
+
Returns:
|
| 253 |
+
output: Logits [batch_size, tgt_len, vocab_size]
|
| 254 |
+
"""
|
| 255 |
+
# Tạo masks
|
| 256 |
+
src_mask = create_padding_mask(src, self.pad_idx)
|
| 257 |
+
tgt_mask = create_target_mask(tgt, self.pad_idx)
|
| 258 |
+
|
| 259 |
+
# Encoder (dùng shared embedding)
|
| 260 |
+
encoder_output = self.encoder(src, src_mask)
|
| 261 |
+
|
| 262 |
+
# Decoder (dùng shared embedding)
|
| 263 |
+
decoder_output = self.decoder(tgt, encoder_output, src_mask, tgt_mask)
|
| 264 |
+
|
| 265 |
+
# Debug: Kiểm tra shape
|
| 266 |
+
# print(f"DEBUG: decoder_output shape: {decoder_output.shape}")
|
| 267 |
+
# print(f"DEBUG: output_layer weight shape: {self.output_layer.weight.shape}")
|
| 268 |
+
|
| 269 |
+
# Output layer (có thể share weight với embedding nếu use_weight_tying=True)
|
| 270 |
+
output = self.output_layer(decoder_output)
|
| 271 |
+
|
| 272 |
+
return output
|
| 273 |
+
|
| 274 |
+
def encode(self, src):
|
| 275 |
+
"""
|
| 276 |
+
Chỉ chạy encoder (dùng khi inference)
|
| 277 |
+
|
| 278 |
+
Args:
|
| 279 |
+
src: Source sequence [batch_size, src_len]
|
| 280 |
+
|
| 281 |
+
Returns:
|
| 282 |
+
encoder_output: [batch_size, src_len, d_model]
|
| 283 |
+
src_mask: [batch_size, 1, 1, src_len]
|
| 284 |
+
"""
|
| 285 |
+
src_mask = create_padding_mask(src, self.pad_idx)
|
| 286 |
+
encoder_output = self.encoder(src, src_mask)
|
| 287 |
+
return encoder_output, src_mask
|
| 288 |
+
|
| 289 |
+
def decode(self, tgt, encoder_output, src_mask):
|
| 290 |
+
"""
|
| 291 |
+
Chỉ chạy decoder (dùng khi inference)
|
| 292 |
+
|
| 293 |
+
Args:
|
| 294 |
+
tgt: Target sequence [batch_size, tgt_len]
|
| 295 |
+
encoder_output: Encoder output [batch_size, src_len, d_model]
|
| 296 |
+
src_mask: Source mask [batch_size, 1, 1, src_len]
|
| 297 |
+
|
| 298 |
+
Returns:
|
| 299 |
+
output: Logits [batch_size, tgt_len, vocab_size]
|
| 300 |
+
"""
|
| 301 |
+
tgt_mask = create_target_mask(tgt, self.pad_idx)
|
| 302 |
+
decoder_output = self.decoder(tgt, encoder_output, src_mask, tgt_mask)
|
| 303 |
+
output = self.output_layer(decoder_output)
|
| 304 |
+
return output
|
| 305 |
+
|
| 306 |
+
# ============================================================================
|
| 307 |
+
# MODEL CONFIGURATION
|
| 308 |
+
# ============================================================================
|
| 309 |
+
|
| 310 |
+
def get_model_config(model_size='base'):
|
| 311 |
+
"""
|
| 312 |
+
Trả về config cho các kích thước model khác nhau
|
| 313 |
+
|
| 314 |
+
Args:
|
| 315 |
+
model_size: 'tiny', 'small', 'base', 'large'
|
| 316 |
+
|
| 317 |
+
Returns:
|
| 318 |
+
config: Dictionary chứa hyperparameters
|
| 319 |
+
"""
|
| 320 |
+
configs = {
|
| 321 |
+
'tiny': {
|
| 322 |
+
'd_model': 256,
|
| 323 |
+
'n_layers': 2,
|
| 324 |
+
'n_heads': 4,
|
| 325 |
+
'd_ff': 1024,
|
| 326 |
+
'dropout': 0.1
|
| 327 |
+
},
|
| 328 |
+
'small': {
|
| 329 |
+
'd_model': 256,
|
| 330 |
+
'n_layers': 4,
|
| 331 |
+
'n_heads': 8,
|
| 332 |
+
'd_ff': 1024,
|
| 333 |
+
'dropout': 0.1
|
| 334 |
+
},
|
| 335 |
+
'medium': { # ~25M parameters với 32k vocab + weight tying
|
| 336 |
+
'd_model': 384,
|
| 337 |
+
'n_layers': 5,
|
| 338 |
+
'n_heads': 8,
|
| 339 |
+
'd_ff': 1536,
|
| 340 |
+
'dropout': 0.1
|
| 341 |
+
},
|
| 342 |
+
'custom_25m': { # ~25M parameters với 32k vocab + weight tying
|
| 343 |
+
'd_model': 384,
|
| 344 |
+
'n_layers': 6,
|
| 345 |
+
'n_heads': 8,
|
| 346 |
+
'd_ff': 1536, # 4 * d_model
|
| 347 |
+
'dropout': 0.1
|
| 348 |
+
},
|
| 349 |
+
'base': {
|
| 350 |
+
'd_model': 512,
|
| 351 |
+
'n_layers': 6,
|
| 352 |
+
'n_heads': 8,
|
| 353 |
+
'd_ff': 2048,
|
| 354 |
+
'dropout': 0.1
|
| 355 |
+
},
|
| 356 |
+
'large': {
|
| 357 |
+
'd_model': 1024,
|
| 358 |
+
'n_layers': 6,
|
| 359 |
+
'n_heads': 16,
|
| 360 |
+
'd_ff': 4096,
|
| 361 |
+
'dropout': 0.1
|
| 362 |
+
}
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
return configs.get(model_size, configs['base'])
|
| 366 |
+
|
| 367 |
+
def create_model(src_vocab_size, tgt_vocab_size, model_size='base', pad_idx=0,
|
| 368 |
+
use_shared_vocab=True, use_weight_tying=True):
|
| 369 |
+
"""
|
| 370 |
+
Tạo Transformer model với Shared Vocabulary
|
| 371 |
+
|
| 372 |
+
Args:
|
| 373 |
+
src_vocab_size: Kích thước shared vocabulary
|
| 374 |
+
tgt_vocab_size: Bỏ qua (giữ để tương thích, phải = src_vocab_size)
|
| 375 |
+
model_size: Kích thước model ('tiny', 'small', 'base', 'large')
|
| 376 |
+
pad_idx: Padding index
|
| 377 |
+
use_shared_vocab: Luôn True (giữ để tương thích)
|
| 378 |
+
use_weight_tying: Có dùng weight tying không (mặc định True)
|
| 379 |
+
|
| 380 |
+
Returns:
|
| 381 |
+
model: TransformerShared model
|
| 382 |
+
config: Model configuration
|
| 383 |
+
"""
|
| 384 |
+
config = get_model_config(model_size)
|
| 385 |
+
|
| 386 |
+
# Luôn dùng shared vocabulary
|
| 387 |
+
vocab_size = src_vocab_size
|
| 388 |
+
model = TransformerShared(
|
| 389 |
+
vocab_size=vocab_size,
|
| 390 |
+
d_model=config['d_model'],
|
| 391 |
+
n_layers=config['n_layers'],
|
| 392 |
+
n_heads=config['n_heads'],
|
| 393 |
+
d_ff=config['d_ff'],
|
| 394 |
+
dropout=config['dropout'],
|
| 395 |
+
pad_idx=pad_idx,
|
| 396 |
+
use_weight_tying=use_weight_tying
|
| 397 |
+
)
|
| 398 |
+
|
| 399 |
+
return model, config
|
| 400 |
+
|
| 401 |
+
# ============================================================================
|
| 402 |
+
# UTILITY FUNCTIONS
|
| 403 |
+
# ============================================================================
|
| 404 |
+
|
| 405 |
+
def count_parameters(model):
|
| 406 |
+
"""
|
| 407 |
+
Đếm số lượng parameters của model
|
| 408 |
+
|
| 409 |
+
Args:
|
| 410 |
+
model: PyTorch model
|
| 411 |
+
|
| 412 |
+
Returns:
|
| 413 |
+
total: Tổng số parameters
|
| 414 |
+
trainable: Số parameters có thể train
|
| 415 |
+
"""
|
| 416 |
+
total = sum(p.numel() for p in model.parameters())
|
| 417 |
+
trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
|
| 418 |
+
|
| 419 |
+
return total, trainable
|
| 420 |
+
|
| 421 |
+
def print_model_info(model, model_size='base', use_shared_vocab=False):
|
| 422 |
+
"""
|
| 423 |
+
In thông tin về model
|
| 424 |
+
|
| 425 |
+
Args:
|
| 426 |
+
model: Transformer model
|
| 427 |
+
model_size: Kích thước model
|
| 428 |
+
use_shared_vocab: Có dùng shared vocabulary không
|
| 429 |
+
"""
|
| 430 |
+
total_params, trainable_params = count_parameters(model)
|
| 431 |
+
|
| 432 |
+
print("="*70)
|
| 433 |
+
print("THÔNG TIN MÔ HÌNH TRANSFORMER")
|
| 434 |
+
print("="*70)
|
| 435 |
+
print(f"\nKích thước model: {model_size.upper()}")
|
| 436 |
+
|
| 437 |
+
if use_shared_vocab:
|
| 438 |
+
print(f" Mode: SHARED VOCABULARY + WEIGHT TYING")
|
| 439 |
+
if isinstance(model, TransformerShared) and model.use_weight_tying:
|
| 440 |
+
print(f" ✓ Weight Tying: Enabled (tiết kiệm ~50% vocab params)")
|
| 441 |
+
else:
|
| 442 |
+
print(f" Mode: SEPARATE VOCABULARIES")
|
| 443 |
+
|
| 444 |
+
print(f"\nSố lượng parameters:")
|
| 445 |
+
print(f" - Total: {total_params:,}")
|
| 446 |
+
print(f" - Trainable: {trainable_params:,}")
|
| 447 |
+
print(f" - Model size: ~{total_params * 4 / (1024**2):.2f} MB (float32)")
|
| 448 |
+
|
| 449 |
+
config = get_model_config(model_size)
|
| 450 |
+
print(f"\nCấu hình:")
|
| 451 |
+
print(f" - d_model: {config['d_model']}")
|
| 452 |
+
print(f" - n_layers: {config['n_layers']}")
|
| 453 |
+
print(f" - n_heads: {config['n_heads']}")
|
| 454 |
+
print(f" - d_ff: {config['d_ff']}")
|
| 455 |
+
print(f" - dropout: {config['dropout']}")
|
| 456 |
+
print("="*70)
|
| 457 |
+
|
| 458 |
+
# ============================================================================
|
| 459 |
+
# TEST COMPLETE MODEL
|
| 460 |
+
# ============================================================================
|
| 461 |
+
|
| 462 |
+
if __name__ == "__main__":
|
| 463 |
+
print("="*70)
|
| 464 |
+
print("KIỂM TRA TRANSFORMER MODEL HOÀN CHỈNH")
|
| 465 |
+
print("="*70)
|
| 466 |
+
|
| 467 |
+
# Hyperparameters
|
| 468 |
+
src_vocab_size = 10000
|
| 469 |
+
tgt_vocab_size = 8000
|
| 470 |
+
batch_size = 4
|
| 471 |
+
src_len = 15
|
| 472 |
+
tgt_len = 20
|
| 473 |
+
pad_idx = 0
|
| 474 |
+
|
| 475 |
+
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
| 476 |
+
print(f"\nDevice: {device}\n")
|
| 477 |
+
|
| 478 |
+
# Test với các kích thước model khác nhau
|
| 479 |
+
for model_size in ['tiny', 'small', 'base']:
|
| 480 |
+
print(f"\n{'='*70}")
|
| 481 |
+
print(f"TEST MODEL SIZE: {model_size.upper()}")
|
| 482 |
+
print(f"{'='*70}\n")
|
| 483 |
+
|
| 484 |
+
# Tạo model
|
| 485 |
+
model, config = create_model(
|
| 486 |
+
src_vocab_size=src_vocab_size,
|
| 487 |
+
tgt_vocab_size=tgt_vocab_size,
|
| 488 |
+
model_size=model_size,
|
| 489 |
+
pad_idx=pad_idx
|
| 490 |
+
)
|
| 491 |
+
model = model.to(device)
|
| 492 |
+
|
| 493 |
+
# In thông tin model
|
| 494 |
+
print_model_info(model, model_size)
|
| 495 |
+
|
| 496 |
+
# Tạo dummy data
|
| 497 |
+
src = torch.randint(1, src_vocab_size, (batch_size, src_len)).to(device)
|
| 498 |
+
tgt = torch.randint(1, tgt_vocab_size, (batch_size, tgt_len)).to(device)
|
| 499 |
+
|
| 500 |
+
# Forward pass
|
| 501 |
+
print(f"\nForward pass:")
|
| 502 |
+
print(f" Source shape: {src.shape}")
|
| 503 |
+
print(f" Target shape: {tgt.shape}")
|
| 504 |
+
|
| 505 |
+
with torch.no_grad():
|
| 506 |
+
output = model(src, tgt)
|
| 507 |
+
|
| 508 |
+
print(f" Output shape: {output.shape}")
|
| 509 |
+
print(f" Expected: [{batch_size}, {tgt_len}, {tgt_vocab_size}]")
|
| 510 |
+
print(f" ✓ Shape correct!")
|
| 511 |
+
|
| 512 |
+
# Test encode và decode riêng
|
| 513 |
+
print(f"\nTest encode & decode separately:")
|
| 514 |
+
with torch.no_grad():
|
| 515 |
+
encoder_output, src_mask = model.encode(src)
|
| 516 |
+
decoder_output = model.decode(tgt, encoder_output, src_mask)
|
| 517 |
+
|
| 518 |
+
print(f" Encoder output shape: {encoder_output.shape}")
|
| 519 |
+
print(f" Decoder output shape: {decoder_output.shape}")
|
| 520 |
+
print(f" ✓ Encode/Decode work correctly!")
|
| 521 |
+
|
| 522 |
+
# Kiểm tra output giống nhau
|
| 523 |
+
print(f"\nVerify output consistency:")
|
| 524 |
+
with torch.no_grad():
|
| 525 |
+
output_combined = model(src, tgt)
|
| 526 |
+
|
| 527 |
+
is_same = torch.allclose(output_combined, decoder_output, atol=1e-6)
|
| 528 |
+
print(f" Forward == Encode+Decode: {is_same}")
|
| 529 |
+
print(f" ✓ Model is consistent!")
|
| 530 |
+
|
| 531 |
+
print("\n" + "="*70)
|
| 532 |
+
print("✓ TẤT CẢ TESTS PASSED!")
|
| 533 |
+
print("="*70)
|
| 534 |
+
|
| 535 |
+
print("\n📝 GỢI Ý SỬ DỤNG:")
|
| 536 |
+
print(" - Dùng 'tiny' để debug và test nhanh")
|
| 537 |
+
print(" - Dùng 'small' để train trên CPU hoặc GPU nhỏ")
|
| 538 |
+
print(" - Dùng 'base' để có kết quả tốt (cần GPU)")
|
| 539 |
+
print(" - Dùng 'large' chỉ khi có GPU mạnh")
|
src/config.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
CONFIGURATION FILE
|
| 3 |
+
Centralized configuration for the project
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
class Config:
|
| 10 |
+
"""Global configuration"""
|
| 11 |
+
|
| 12 |
+
# Paths
|
| 13 |
+
PROJECT_ROOT = Path(__file__).parent.parent
|
| 14 |
+
DATA_DIR = PROJECT_ROOT / 'data'
|
| 15 |
+
CHECKPOINT_DIR = PROJECT_ROOT / 'checkpoints'
|
| 16 |
+
RESULTS_DIR = PROJECT_ROOT / 'results'
|
| 17 |
+
LOGS_DIR = PROJECT_ROOT / 'logs'
|
| 18 |
+
|
| 19 |
+
# Data
|
| 20 |
+
PROCESSED_DATA_DIR = DATA_DIR / 'processed'
|
| 21 |
+
VI_VOCAB_PATH = PROCESSED_DATA_DIR / 'vi_vocab.pkl'
|
| 22 |
+
EN_VOCAB_PATH = PROCESSED_DATA_DIR / 'en_vocab.pkl'
|
| 23 |
+
PROCESSED_DATA_PATH = PROCESSED_DATA_DIR / 'processed_data.pkl'
|
| 24 |
+
|
| 25 |
+
# Model
|
| 26 |
+
MODEL_SIZE = 'base' # 'tiny', 'small', 'base', 'large'
|
| 27 |
+
PAD_IDX = 0
|
| 28 |
+
|
| 29 |
+
# Training
|
| 30 |
+
NUM_EPOCHS = 20
|
| 31 |
+
BATCH_SIZE = 32
|
| 32 |
+
WARMUP_STEPS = 4000
|
| 33 |
+
LABEL_SMOOTHING = 0.1
|
| 34 |
+
|
| 35 |
+
# Device
|
| 36 |
+
DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'
|
src/create_bidirectional_dataset.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Tạo Bidirectional Dataset từ dataset hiện tại
|
| 3 |
+
Reverse các cặp câu để model học được cả hai chiều
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import pickle
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from tqdm import tqdm
|
| 9 |
+
|
| 10 |
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
| 11 |
+
PROCESSED_DATA_DIR = PROJECT_ROOT / 'data' / 'processed'
|
| 12 |
+
|
| 13 |
+
def create_bidirectional_dataset():
|
| 14 |
+
"""
|
| 15 |
+
Tạo bidirectional dataset bằng cách:
|
| 16 |
+
1. Giữ nguyên các cặp vi→en
|
| 17 |
+
2. Thêm các cặp en→vi (reverse)
|
| 18 |
+
"""
|
| 19 |
+
print("="*70)
|
| 20 |
+
print("TẠO BIDIRECTIONAL DATASET")
|
| 21 |
+
print("="*70)
|
| 22 |
+
|
| 23 |
+
# Load data hiện tại
|
| 24 |
+
data_path = PROCESSED_DATA_DIR / 'processed_data_shared.pkl'
|
| 25 |
+
if not data_path.exists():
|
| 26 |
+
print(f"❌ Không tìm thấy: {data_path}")
|
| 27 |
+
print(" Vui lòng chạy: python src/2_encode_data.py trước")
|
| 28 |
+
return
|
| 29 |
+
|
| 30 |
+
print(f"\n📂 Loading dataset từ: {data_path}")
|
| 31 |
+
with open(data_path, 'rb') as f:
|
| 32 |
+
data = pickle.load(f)
|
| 33 |
+
|
| 34 |
+
# Hiển thị thống kê ban đầu
|
| 35 |
+
print("\n📊 Thống kê dataset hiện tại:")
|
| 36 |
+
for split in ['train', 'validation', 'test']:
|
| 37 |
+
if split in data:
|
| 38 |
+
print(f" {split}: {len(data[split])} cặp câu (vi→en)")
|
| 39 |
+
|
| 40 |
+
# Tạo bidirectional data
|
| 41 |
+
print("\n🔄 Tạo bidirectional dataset...")
|
| 42 |
+
bidirectional_data = {
|
| 43 |
+
'train': [],
|
| 44 |
+
'validation': [],
|
| 45 |
+
'test': []
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
for split in ['train', 'validation', 'test']:
|
| 49 |
+
if split not in data:
|
| 50 |
+
continue
|
| 51 |
+
|
| 52 |
+
print(f"\n Processing {split}...")
|
| 53 |
+
original_count = len(data[split])
|
| 54 |
+
|
| 55 |
+
# Giữ nguyên chiều vi→en
|
| 56 |
+
bidirectional_data[split].extend(data[split])
|
| 57 |
+
|
| 58 |
+
# Thêm chiều ngược lại en→vi
|
| 59 |
+
for src_ids, tgt_ids in tqdm(data[split], desc=f" Reversing {split}"):
|
| 60 |
+
bidirectional_data[split].append((tgt_ids, src_ids)) # Swap!
|
| 61 |
+
|
| 62 |
+
new_count = len(bidirectional_data[split])
|
| 63 |
+
print(f" ✓ {split}: {original_count} → {new_count} cặp câu (x2)")
|
| 64 |
+
|
| 65 |
+
# Lưu lại
|
| 66 |
+
output_path = PROCESSED_DATA_DIR / 'processed_data_bidirectional.pkl'
|
| 67 |
+
print(f"\n💾 Lưu bidirectional dataset vào: {output_path}")
|
| 68 |
+
with open(output_path, 'wb') as f:
|
| 69 |
+
pickle.dump(bidirectional_data, f)
|
| 70 |
+
|
| 71 |
+
# Thống kê cuối cùng
|
| 72 |
+
print("\n" + "="*70)
|
| 73 |
+
print("KẾT QUẢ")
|
| 74 |
+
print("="*70)
|
| 75 |
+
total_pairs = 0
|
| 76 |
+
for split in ['train', 'validation', 'test']:
|
| 77 |
+
count = len(bidirectional_data[split])
|
| 78 |
+
total_pairs += count
|
| 79 |
+
print(f" {split}: {count:,} cặp câu")
|
| 80 |
+
print(f"\n Tổng cộng: {total_pairs:,} cặp câu")
|
| 81 |
+
print(f" ✓ Dataset đã sẵn sàng để train bidirectional!")
|
| 82 |
+
print("="*70)
|
| 83 |
+
|
| 84 |
+
print("\n📝 Bước tiếp theo:")
|
| 85 |
+
print(" 1. Cập nhật training code để dùng 'processed_data_bidirectional.pkl'")
|
| 86 |
+
print(" 2. Train lại model với dataset mới")
|
| 87 |
+
print(" 3. Model sẽ học được cả vi→en và en→vi")
|
| 88 |
+
|
| 89 |
+
if __name__ == '__main__':
|
| 90 |
+
create_bidirectional_dataset()
|
| 91 |
+
|
src/data_preprocessing.py
ADDED
|
@@ -0,0 +1,468 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
DATA PREPROCESSING - PHIÊN BẢN ĐƠN GIẢN
|
| 3 |
+
Load dữ liệu từ nhiều nguồn
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import re
|
| 7 |
+
import json
|
| 8 |
+
import pickle
|
| 9 |
+
import os
|
| 10 |
+
from collections import Counter
|
| 11 |
+
from typing import List, Dict
|
| 12 |
+
|
| 13 |
+
import numpy as np
|
| 14 |
+
import pandas as pd
|
| 15 |
+
from tqdm import tqdm
|
| 16 |
+
from pathlib import Path
|
| 17 |
+
|
| 18 |
+
# ============================================================================
|
| 19 |
+
# PATH CONFIGURATION
|
| 20 |
+
# ============================================================================
|
| 21 |
+
|
| 22 |
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
| 23 |
+
DATA_DIR = PROJECT_ROOT / 'data'
|
| 24 |
+
RAW_DATA_DIR = DATA_DIR / 'raw'
|
| 25 |
+
PROCESSED_DATA_DIR = DATA_DIR / 'processed'
|
| 26 |
+
CUSTOM_CSV_PATH = PROJECT_ROOT / 'mtet_cleaned.csv'
|
| 27 |
+
# Set to 0 or negative to use all rows, or a positive number to limit
|
| 28 |
+
# Default: 0 (use all rows) - change to a number like '500000' to limit
|
| 29 |
+
_max_rows_env = os.environ.get('MTET_MAX_ROWS', '0')
|
| 30 |
+
CUSTOM_CSV_MAX_ROWS = int(_max_rows_env) if _max_rows_env and int(_max_rows_env) > 0 else None
|
| 31 |
+
|
| 32 |
+
# ============================================================================
|
| 33 |
+
# 1. LOAD DATASET
|
| 34 |
+
# ============================================================================
|
| 35 |
+
|
| 36 |
+
def load_iwslt_data():
|
| 37 |
+
"""
|
| 38 |
+
Load IWSLT dataset với nhiều fallback options
|
| 39 |
+
"""
|
| 40 |
+
# Option 0: Custom CSV (e.g., MTet mirror)
|
| 41 |
+
if CUSTOM_CSV_PATH.exists():
|
| 42 |
+
print("Đang tải dataset từ custom CSV:", CUSTOM_CSV_PATH)
|
| 43 |
+
try:
|
| 44 |
+
return load_custom_csv(CUSTOM_CSV_PATH)
|
| 45 |
+
except Exception as e: # noqa: BLE001
|
| 46 |
+
print(f" ✗ Failed custom CSV: {e}")
|
| 47 |
+
|
| 48 |
+
print("Đang tải dataset IWSLT Vi-En...")
|
| 49 |
+
|
| 50 |
+
# Option 1: Thử load từ Hugging Face
|
| 51 |
+
try:
|
| 52 |
+
from datasets import load_dataset
|
| 53 |
+
print(" Thử Option 1: Hugging Face mt_eng_vietnamese...")
|
| 54 |
+
dataset = load_dataset("mt_eng_vietnamese", "iwslt2015-vi-en", trust_remote_code=True)
|
| 55 |
+
print(" ✓ Loaded successfully!")
|
| 56 |
+
return dataset
|
| 57 |
+
except Exception as e:
|
| 58 |
+
print(f" ✗ Failed: {e}")
|
| 59 |
+
|
| 60 |
+
# Option 2: Thử dataset khác
|
| 61 |
+
try:
|
| 62 |
+
from datasets import load_dataset
|
| 63 |
+
print(" Thử Option 2: Hugging Face iwslt2017...")
|
| 64 |
+
dataset = load_dataset("iwslt2017", "iwslt2017-vi-en", trust_remote_code=True)
|
| 65 |
+
print(" ✓ Loaded successfully!")
|
| 66 |
+
return dataset
|
| 67 |
+
except Exception as e:
|
| 68 |
+
print(f" ✗ Failed: {e}")
|
| 69 |
+
|
| 70 |
+
# Option 3: Load từ local files (nếu đã download)
|
| 71 |
+
try:
|
| 72 |
+
print(" Thử Option 3: Load từ local files...")
|
| 73 |
+
dataset = load_from_local()
|
| 74 |
+
print(" ✓ Loaded successfully!")
|
| 75 |
+
return dataset
|
| 76 |
+
except Exception as e:
|
| 77 |
+
print(f" ✗ Failed: {e}")
|
| 78 |
+
|
| 79 |
+
# Option 4: Tạo sample dataset nhỏ
|
| 80 |
+
print(" ⚠️ Dùng Option 4: Sample dataset (chỉ để test)")
|
| 81 |
+
print(" 📝 Tải dataset thật từ: https://nlp.stanford.edu/projects/nmt/")
|
| 82 |
+
return create_sample_dataset()
|
| 83 |
+
|
| 84 |
+
def load_from_local():
|
| 85 |
+
"""
|
| 86 |
+
Load dataset từ file local
|
| 87 |
+
Yêu cầu: Đã download và đặt trong data/raw/
|
| 88 |
+
"""
|
| 89 |
+
from datasets import Dataset, DatasetDict
|
| 90 |
+
|
| 91 |
+
data_dir = RAW_DATA_DIR
|
| 92 |
+
|
| 93 |
+
# Đọc các file
|
| 94 |
+
splits = {}
|
| 95 |
+
for split in ['train', 'dev', 'test']:
|
| 96 |
+
vi_file = data_dir / f'{split}.vi'
|
| 97 |
+
en_file = data_dir / f'{split}.en'
|
| 98 |
+
|
| 99 |
+
if not vi_file.exists() or not en_file.exists():
|
| 100 |
+
raise FileNotFoundError(f"Không tìm thấy {vi_file} hoặc {en_file}")
|
| 101 |
+
|
| 102 |
+
with open(vi_file, 'r', encoding='utf-8') as f:
|
| 103 |
+
vi_lines = f.readlines()
|
| 104 |
+
|
| 105 |
+
with open(en_file, 'r', encoding='utf-8') as f:
|
| 106 |
+
en_lines = f.readlines()
|
| 107 |
+
|
| 108 |
+
# Tạo dataset
|
| 109 |
+
translations = [
|
| 110 |
+
{'vi': vi.strip(), 'en': en.strip()}
|
| 111 |
+
for vi, en in zip(vi_lines, en_lines)
|
| 112 |
+
]
|
| 113 |
+
|
| 114 |
+
splits[split if split != 'dev' else 'validation'] = Dataset.from_dict({
|
| 115 |
+
'translation': translations
|
| 116 |
+
})
|
| 117 |
+
|
| 118 |
+
return DatasetDict(splits)
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def load_custom_csv(csv_path, train_ratio=0.98, val_ratio=0.01, seed=42, max_rows=None):
|
| 122 |
+
"""
|
| 123 |
+
Load dataset từ CSV custom (ví dụ MTet) với cột src/tgt hoặc vi/en.
|
| 124 |
+
"""
|
| 125 |
+
from datasets import Dataset, DatasetDict
|
| 126 |
+
|
| 127 |
+
df = pd.read_csv(csv_path)
|
| 128 |
+
if {'src', 'tgt'}.issubset(df.columns):
|
| 129 |
+
df = df.rename(columns={'src': 'vi', 'tgt': 'en'})
|
| 130 |
+
elif {'vi', 'en'}.issubset(df.columns):
|
| 131 |
+
pass
|
| 132 |
+
else:
|
| 133 |
+
raise ValueError("CSV cần có cột 'src'/'tgt' hoặc 'vi'/'en'.")
|
| 134 |
+
|
| 135 |
+
df = df[['vi', 'en']].dropna().reset_index(drop=True)
|
| 136 |
+
if df.empty:
|
| 137 |
+
raise ValueError("CSV không có dữ liệu hợp lệ.")
|
| 138 |
+
|
| 139 |
+
original_total = len(df)
|
| 140 |
+
max_rows = max_rows if max_rows is not None else CUSTOM_CSV_MAX_ROWS
|
| 141 |
+
if max_rows is not None and max_rows > 0 and original_total > max_rows:
|
| 142 |
+
print(f" 🔎 Sampling {max_rows} rows (từ {original_total}) để phù hợp bộ nhớ.")
|
| 143 |
+
df = df.sample(n=max_rows, random_state=seed).reset_index(drop=True)
|
| 144 |
+
elif max_rows is None or max_rows <= 0:
|
| 145 |
+
print(f" 📊 Using all {original_total} rows from CSV.")
|
| 146 |
+
|
| 147 |
+
df = df.sample(frac=1.0, random_state=seed).reset_index(drop=True)
|
| 148 |
+
total = len(df)
|
| 149 |
+
train_end = max(1, int(total * train_ratio))
|
| 150 |
+
val_end = train_end + max(1, int(total * val_ratio))
|
| 151 |
+
if val_end >= total:
|
| 152 |
+
val_end = total - 1
|
| 153 |
+
if val_end <= train_end:
|
| 154 |
+
val_end = min(total - 1, train_end + 1)
|
| 155 |
+
|
| 156 |
+
splits = {
|
| 157 |
+
'train': df.iloc[:train_end],
|
| 158 |
+
'validation': df.iloc[train_end:val_end],
|
| 159 |
+
'test': df.iloc[val_end:]
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
dataset_dict = {}
|
| 163 |
+
for split, split_df in splits.items():
|
| 164 |
+
examples = [{'vi': row['vi'], 'en': row['en']} for _, row in split_df.iterrows()]
|
| 165 |
+
dataset_dict[split] = Dataset.from_dict({'translation': examples})
|
| 166 |
+
|
| 167 |
+
hf_dataset = DatasetDict(dataset_dict)
|
| 168 |
+
print(f" 📊 Custom CSV dataset: train={len(hf_dataset['train'])}, "
|
| 169 |
+
f"val={len(hf_dataset['validation'])}, test={len(hf_dataset['test'])}")
|
| 170 |
+
|
| 171 |
+
return hf_dataset
|
| 172 |
+
def create_sample_dataset():
|
| 173 |
+
"""
|
| 174 |
+
Tạo dataset mẫu để test code
|
| 175 |
+
"""
|
| 176 |
+
from datasets import Dataset, DatasetDict
|
| 177 |
+
|
| 178 |
+
# Sample Vietnamese-English pairs
|
| 179 |
+
samples = [
|
| 180 |
+
('xin chào', 'hello'),
|
| 181 |
+
('tạm biệt', 'goodbye'),
|
| 182 |
+
('cảm ơn', 'thank you'),
|
| 183 |
+
('xin lỗi', 'sorry'),
|
| 184 |
+
('tôi tên là Nam', 'my name is Nam'),
|
| 185 |
+
('tôi là sinh viên', 'i am a student'),
|
| 186 |
+
('hôm nay thời tiết đẹp', 'the weather is nice today'),
|
| 187 |
+
('tôi đang học tiếng Anh', 'i am learning English'),
|
| 188 |
+
('bạn khỏe không', 'how are you'),
|
| 189 |
+
('tôi yêu Việt Nam', 'i love Vietnam'),
|
| 190 |
+
('chúc bạn một ngày tốt lành', 'have a nice day'),
|
| 191 |
+
('rất vui được gặp bạn', 'nice to meet you'),
|
| 192 |
+
('tôi đang làm việc', 'i am working'),
|
| 193 |
+
('hãy giúp tôi', 'please help me'),
|
| 194 |
+
('tôi không hiểu', 'i do not understand'),
|
| 195 |
+
('bạn nói tiếng Anh không', 'do you speak English'),
|
| 196 |
+
('tôi đói bụng', 'i am hungry'),
|
| 197 |
+
('mấy giờ rồi', 'what time is it'),
|
| 198 |
+
('tôi muốn đi', 'i want to go'),
|
| 199 |
+
('đây là gì', 'what is this'),
|
| 200 |
+
]
|
| 201 |
+
|
| 202 |
+
# Tạo train set (lớn hơn)
|
| 203 |
+
train_data = []
|
| 204 |
+
for _ in range(200): # Nhân lên
|
| 205 |
+
for vi, en in samples:
|
| 206 |
+
train_data.append({'vi': vi, 'en': en})
|
| 207 |
+
|
| 208 |
+
# Val và test
|
| 209 |
+
val_data = [{'vi': vi, 'en': en} for vi, en in samples[:5]] * 10
|
| 210 |
+
test_data = [{'vi': vi, 'en': en} for vi, en in samples[5:10]] * 10
|
| 211 |
+
|
| 212 |
+
dataset = DatasetDict({
|
| 213 |
+
'train': Dataset.from_dict({'translation': train_data}),
|
| 214 |
+
'validation': Dataset.from_dict({'translation': val_data}),
|
| 215 |
+
'test': Dataset.from_dict({'translation': test_data})
|
| 216 |
+
})
|
| 217 |
+
|
| 218 |
+
print(f" 📊 Sample dataset: {len(train_data)} train, {len(val_data)} val, {len(test_data)} test")
|
| 219 |
+
|
| 220 |
+
return dataset
|
| 221 |
+
|
| 222 |
+
# ============================================================================
|
| 223 |
+
# 2. CLEANING
|
| 224 |
+
# ============================================================================
|
| 225 |
+
|
| 226 |
+
def clean_text(text: str, lang: str = 'vi') -> str:
|
| 227 |
+
"""Làm sạch văn bản"""
|
| 228 |
+
text = text.lower()
|
| 229 |
+
text = re.sub(r'\s+', ' ', text)
|
| 230 |
+
text = text.strip()
|
| 231 |
+
text = re.sub(r'([.,!?;:])', r' \1 ', text)
|
| 232 |
+
text = re.sub(r'\s+', ' ', text)
|
| 233 |
+
return text
|
| 234 |
+
|
| 235 |
+
def clean_dataset(dataset):
|
| 236 |
+
"""Làm sạch toàn bộ dataset"""
|
| 237 |
+
print("\n" + "="*70)
|
| 238 |
+
print("LÀM SẠCH DỮ LIỆU")
|
| 239 |
+
print("="*70)
|
| 240 |
+
|
| 241 |
+
cleaned_data = {
|
| 242 |
+
'train': [],
|
| 243 |
+
'validation': [],
|
| 244 |
+
'test': []
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
for split in ['train', 'validation', 'test']:
|
| 248 |
+
print(f"\nĐang làm sạch {split} set...")
|
| 249 |
+
|
| 250 |
+
for example in tqdm(dataset[split], desc=f"Cleaning {split}"):
|
| 251 |
+
vi_text = clean_text(example['translation']['vi'], 'vi')
|
| 252 |
+
en_text = clean_text(example['translation']['en'], 'en')
|
| 253 |
+
|
| 254 |
+
# Filter
|
| 255 |
+
vi_words = len(vi_text.split())
|
| 256 |
+
en_words = len(en_text.split())
|
| 257 |
+
|
| 258 |
+
if 1 <= vi_words <= 100 and 1 <= en_words <= 100:
|
| 259 |
+
ratio = max(vi_words, en_words) / max(min(vi_words, en_words), 1)
|
| 260 |
+
if ratio <= 3:
|
| 261 |
+
cleaned_data[split].append({
|
| 262 |
+
'vi': vi_text,
|
| 263 |
+
'en': en_text
|
| 264 |
+
})
|
| 265 |
+
|
| 266 |
+
print(f" Giữ lại: {len(cleaned_data[split])}/{len(dataset[split])} cặp câu")
|
| 267 |
+
|
| 268 |
+
return cleaned_data
|
| 269 |
+
|
| 270 |
+
# ============================================================================
|
| 271 |
+
# 3. VOCABULARY
|
| 272 |
+
# ============================================================================
|
| 273 |
+
|
| 274 |
+
class Vocabulary:
|
| 275 |
+
"""Lớp xây dựng và quản lý từ điển"""
|
| 276 |
+
def __init__(self, freq_threshold=2):
|
| 277 |
+
self.freq_threshold = freq_threshold
|
| 278 |
+
self.word2idx = {}
|
| 279 |
+
self.idx2word = {}
|
| 280 |
+
self.word_freq = Counter()
|
| 281 |
+
|
| 282 |
+
self.PAD_TOKEN = '<pad>'
|
| 283 |
+
self.SOS_TOKEN = '<sos>'
|
| 284 |
+
self.EOS_TOKEN = '<eos>'
|
| 285 |
+
self.UNK_TOKEN = '<unk>'
|
| 286 |
+
|
| 287 |
+
self.PAD_IDX = 0
|
| 288 |
+
self.SOS_IDX = 1
|
| 289 |
+
self.EOS_IDX = 2
|
| 290 |
+
self.UNK_IDX = 3
|
| 291 |
+
|
| 292 |
+
def build_vocabulary(self, sentences: List[str]):
|
| 293 |
+
"""Xây dựng từ điển từ danh sách câu"""
|
| 294 |
+
print("Đang xây dựng từ điển...")
|
| 295 |
+
|
| 296 |
+
for sentence in tqdm(sentences, desc="Counting words"):
|
| 297 |
+
for word in sentence.split():
|
| 298 |
+
self.word_freq[word] += 1
|
| 299 |
+
|
| 300 |
+
idx = 0
|
| 301 |
+
for token in [self.PAD_TOKEN, self.SOS_TOKEN, self.EOS_TOKEN, self.UNK_TOKEN]:
|
| 302 |
+
self.word2idx[token] = idx
|
| 303 |
+
self.idx2word[idx] = token
|
| 304 |
+
idx += 1
|
| 305 |
+
|
| 306 |
+
for word, freq in self.word_freq.most_common():
|
| 307 |
+
if freq >= self.freq_threshold:
|
| 308 |
+
self.word2idx[word] = idx
|
| 309 |
+
self.idx2word[idx] = word
|
| 310 |
+
idx += 1
|
| 311 |
+
|
| 312 |
+
print(f"Kích thước từ điển: {len(self.word2idx)}")
|
| 313 |
+
|
| 314 |
+
def encode(self, sentence: str) -> List[int]:
|
| 315 |
+
"""Chuyển câu thành chuỗi index"""
|
| 316 |
+
tokens = [self.SOS_IDX]
|
| 317 |
+
for word in sentence.split():
|
| 318 |
+
tokens.append(self.word2idx.get(word, self.UNK_IDX))
|
| 319 |
+
tokens.append(self.EOS_IDX)
|
| 320 |
+
return tokens
|
| 321 |
+
|
| 322 |
+
def decode(self, indices: List[int]) -> str:
|
| 323 |
+
"""Chuyển chuỗi index thành câu"""
|
| 324 |
+
words = []
|
| 325 |
+
for idx in indices:
|
| 326 |
+
if idx == self.EOS_IDX:
|
| 327 |
+
break
|
| 328 |
+
if idx not in [self.PAD_IDX, self.SOS_IDX]:
|
| 329 |
+
words.append(self.idx2word.get(idx, self.UNK_TOKEN))
|
| 330 |
+
return ' '.join(words)
|
| 331 |
+
|
| 332 |
+
def __len__(self):
|
| 333 |
+
return len(self.word2idx)
|
| 334 |
+
|
| 335 |
+
# ============================================================================
|
| 336 |
+
# 4. PREPARE DATA
|
| 337 |
+
# ============================================================================
|
| 338 |
+
|
| 339 |
+
def prepare_data_with_padding(cleaned_data, vi_vocab, en_vocab, max_len=100):
|
| 340 |
+
"""Chuẩn bị dữ liệu với padding"""
|
| 341 |
+
print("\n" + "="*70)
|
| 342 |
+
print("CHUẨN BỊ DỮ LIỆU VỚI PADDING")
|
| 343 |
+
print("="*70)
|
| 344 |
+
|
| 345 |
+
processed_data = {}
|
| 346 |
+
|
| 347 |
+
for split in ['train', 'validation', 'test']:
|
| 348 |
+
print(f"\nĐang xử lý {split} set...")
|
| 349 |
+
|
| 350 |
+
src_data = []
|
| 351 |
+
tgt_data = []
|
| 352 |
+
|
| 353 |
+
for item in tqdm(cleaned_data[split], desc=f"Processing {split}"):
|
| 354 |
+
src_tokens = vi_vocab.encode(item['vi'])
|
| 355 |
+
tgt_tokens = en_vocab.encode(item['en'])
|
| 356 |
+
|
| 357 |
+
if len(src_tokens) > max_len:
|
| 358 |
+
src_tokens = src_tokens[:max_len-1] + [vi_vocab.EOS_IDX]
|
| 359 |
+
if len(tgt_tokens) > max_len:
|
| 360 |
+
tgt_tokens = tgt_tokens[:max_len-1] + [en_vocab.EOS_IDX]
|
| 361 |
+
|
| 362 |
+
src_data.append(src_tokens)
|
| 363 |
+
tgt_data.append(tgt_tokens)
|
| 364 |
+
|
| 365 |
+
processed_data[split] = {
|
| 366 |
+
'src': src_data,
|
| 367 |
+
'tgt': tgt_data
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
print(f" Số mẫu: {len(src_data)}")
|
| 371 |
+
|
| 372 |
+
return processed_data
|
| 373 |
+
|
| 374 |
+
# ============================================================================
|
| 375 |
+
# 5. SAVE
|
| 376 |
+
# ============================================================================
|
| 377 |
+
|
| 378 |
+
def save_data(cleaned_data, vi_vocab, en_vocab, processed_data):
|
| 379 |
+
"""Lưu dữ liệu và từ điển"""
|
| 380 |
+
print("\n" + "="*70)
|
| 381 |
+
print("LƯU DỮ LIỆU")
|
| 382 |
+
print("="*70)
|
| 383 |
+
|
| 384 |
+
os.makedirs(PROCESSED_DATA_DIR, exist_ok=True)
|
| 385 |
+
|
| 386 |
+
with open(PROCESSED_DATA_DIR / 'cleaned_data.json', 'w', encoding='utf-8') as f:
|
| 387 |
+
json.dump(cleaned_data, f, ensure_ascii=False, indent=2)
|
| 388 |
+
print("✓ Đã lưu cleaned_data.json")
|
| 389 |
+
|
| 390 |
+
with open(PROCESSED_DATA_DIR / 'vi_vocab.pkl', 'wb') as f:
|
| 391 |
+
pickle.dump(vi_vocab, f)
|
| 392 |
+
print("✓ Đã lưu vi_vocab.pkl")
|
| 393 |
+
|
| 394 |
+
with open(PROCESSED_DATA_DIR / 'en_vocab.pkl', 'wb') as f:
|
| 395 |
+
pickle.dump(en_vocab, f)
|
| 396 |
+
print("✓ Đã lưu en_vocab.pkl")
|
| 397 |
+
|
| 398 |
+
with open(PROCESSED_DATA_DIR / 'processed_data.pkl', 'wb') as f:
|
| 399 |
+
pickle.dump(processed_data, f)
|
| 400 |
+
print("✓ Đã lưu processed_data.pkl")
|
| 401 |
+
|
| 402 |
+
# ============================================================================
|
| 403 |
+
# 6. MAIN
|
| 404 |
+
# ============================================================================
|
| 405 |
+
|
| 406 |
+
def main():
|
| 407 |
+
"""Pipeline xử lý dữ liệu hoàn chỉnh"""
|
| 408 |
+
|
| 409 |
+
print("\n" + "="*70)
|
| 410 |
+
print("DATA PREPROCESSING PIPELINE")
|
| 411 |
+
print("="*70)
|
| 412 |
+
|
| 413 |
+
# Load dataset
|
| 414 |
+
dataset = load_iwslt_data()
|
| 415 |
+
|
| 416 |
+
# Explore
|
| 417 |
+
print("\n" + "="*70)
|
| 418 |
+
print("THỐNG KÊ DỮ LIỆU")
|
| 419 |
+
print("="*70)
|
| 420 |
+
for split in dataset.keys():
|
| 421 |
+
print(f" - {split}: {len(dataset[split])} cặp câu")
|
| 422 |
+
|
| 423 |
+
# Clean
|
| 424 |
+
cleaned_data = clean_dataset(dataset)
|
| 425 |
+
|
| 426 |
+
# Build vocabularies
|
| 427 |
+
print("\n" + "="*70)
|
| 428 |
+
print("XÂY DỰNG TỪ ĐIỂN")
|
| 429 |
+
print("="*70)
|
| 430 |
+
|
| 431 |
+
vi_sentences = [item['vi'] for item in cleaned_data['train']]
|
| 432 |
+
en_sentences = [item['en'] for item in cleaned_data['train']]
|
| 433 |
+
|
| 434 |
+
print("\nTiếng Việt:")
|
| 435 |
+
vi_vocab = Vocabulary(freq_threshold=2)
|
| 436 |
+
vi_vocab.build_vocabulary(vi_sentences)
|
| 437 |
+
|
| 438 |
+
print("\nTiếng Anh:")
|
| 439 |
+
en_vocab = Vocabulary(freq_threshold=2)
|
| 440 |
+
en_vocab.build_vocabulary(en_sentences)
|
| 441 |
+
|
| 442 |
+
# Prepare data
|
| 443 |
+
processed_data = prepare_data_with_padding(cleaned_data, vi_vocab, en_vocab)
|
| 444 |
+
|
| 445 |
+
# Save
|
| 446 |
+
save_data(cleaned_data, vi_vocab, en_vocab, processed_data)
|
| 447 |
+
|
| 448 |
+
print("\n" + "="*70)
|
| 449 |
+
print("✓✓✓ HOÀN TẤT XỬ LÝ DỮ LIỆU! ✓✓✓")
|
| 450 |
+
print("="*70)
|
| 451 |
+
print(f"\nTóm tắt:")
|
| 452 |
+
print(f" - Kích thước từ điển VI: {len(vi_vocab)}")
|
| 453 |
+
print(f" - Kích thước từ điển EN: {len(en_vocab)}")
|
| 454 |
+
print(f" - Train samples: {len(processed_data['train']['src'])}")
|
| 455 |
+
print(f" - Validation samples: {len(processed_data['validation']['src'])}")
|
| 456 |
+
print(f" - Test samples: {len(processed_data['test']['src'])}")
|
| 457 |
+
|
| 458 |
+
print("\n📝 LƯU Ý:")
|
| 459 |
+
if len(processed_data['train']['src']) < 10000:
|
| 460 |
+
print(" ⚠️ Đang dùng sample dataset nhỏ!")
|
| 461 |
+
print(" 📥 Để có BLEU tốt, hãy download dataset thật:")
|
| 462 |
+
print(" 1. Vào: https://nlp.stanford.edu/projects/nmt/")
|
| 463 |
+
print(" 2. Download: IWSLT'15 Vietnamese-English")
|
| 464 |
+
print(" 3. Đặt vào: data/raw/")
|
| 465 |
+
print(" 4. Chạy lại script này")
|
| 466 |
+
|
| 467 |
+
if __name__ == "__main__":
|
| 468 |
+
main()
|
src/dataloader_module.py
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
DATALOADER MODULE - TỐI ỨU HÓA BATCH PROCESSING (FIXED FOR WINDOWS)
|
| 3 |
+
Xử lý batch với dynamic padding để tăng tốc huấn luyện
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
from torch.utils.data import Dataset, DataLoader
|
| 8 |
+
from torch.nn.utils.rnn import pad_sequence
|
| 9 |
+
import pickle
|
| 10 |
+
import numpy as np
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
# ============================================================================
|
| 14 |
+
# PATH CONFIGURATION
|
| 15 |
+
# ============================================================================
|
| 16 |
+
|
| 17 |
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
| 18 |
+
PROCESSED_DATA_DIR = PROJECT_ROOT / 'data' / 'processed'
|
| 19 |
+
|
| 20 |
+
# ============================================================================
|
| 21 |
+
# 1. CUSTOM DATASET
|
| 22 |
+
# ============================================================================
|
| 23 |
+
|
| 24 |
+
class TranslationDataset(Dataset):
|
| 25 |
+
"""
|
| 26 |
+
Dataset class cho dữ liệu dịch máy
|
| 27 |
+
"""
|
| 28 |
+
def __init__(self, src_data, tgt_data):
|
| 29 |
+
"""
|
| 30 |
+
Args:
|
| 31 |
+
src_data: List of source sequences (đã encode thành indices)
|
| 32 |
+
tgt_data: List of target sequences (đã encode thành indices)
|
| 33 |
+
"""
|
| 34 |
+
assert len(src_data) == len(tgt_data), "Source và Target phải có cùng số lượng mẫu"
|
| 35 |
+
|
| 36 |
+
self.src_data = src_data
|
| 37 |
+
self.tgt_data = tgt_data
|
| 38 |
+
|
| 39 |
+
def __len__(self):
|
| 40 |
+
return len(self.src_data)
|
| 41 |
+
|
| 42 |
+
def __getitem__(self, idx):
|
| 43 |
+
"""
|
| 44 |
+
Trả về một cặp (source, target) dưới dạng tensor
|
| 45 |
+
"""
|
| 46 |
+
src = torch.LongTensor(self.src_data[idx])
|
| 47 |
+
tgt = torch.LongTensor(self.tgt_data[idx])
|
| 48 |
+
|
| 49 |
+
return src, tgt
|
| 50 |
+
|
| 51 |
+
# ============================================================================
|
| 52 |
+
# 2. COLLATE FUNCTION - DYNAMIC PADDING
|
| 53 |
+
# ============================================================================
|
| 54 |
+
|
| 55 |
+
def collate_fn(batch, pad_idx=0):
|
| 56 |
+
"""
|
| 57 |
+
Collate function với dynamic padding
|
| 58 |
+
Chỉ pad đến độ dài câu dài nhất trong batch, không phải max_len cố định
|
| 59 |
+
|
| 60 |
+
Args:
|
| 61 |
+
batch: List of (src, tgt) tuples
|
| 62 |
+
pad_idx: Index của padding token
|
| 63 |
+
|
| 64 |
+
Returns:
|
| 65 |
+
src_batch: Padded source sequences [batch_size, max_src_len]
|
| 66 |
+
tgt_batch: Padded target sequences [batch_size, max_tgt_len]
|
| 67 |
+
src_lengths: Độ dài thực của mỗi source sequence
|
| 68 |
+
tgt_lengths: Độ dài thực của mỗi target sequence
|
| 69 |
+
"""
|
| 70 |
+
# Tách source và target
|
| 71 |
+
src_batch, tgt_batch = zip(*batch)
|
| 72 |
+
|
| 73 |
+
# Lấy độ dài thực của mỗi sequence (trước khi pad)
|
| 74 |
+
src_lengths = torch.LongTensor([len(s) for s in src_batch])
|
| 75 |
+
tgt_lengths = torch.LongTensor([len(t) for t in tgt_batch])
|
| 76 |
+
|
| 77 |
+
# Padding - pad_sequence tự động pad đến độ dài max trong batch
|
| 78 |
+
src_batch = pad_sequence(src_batch, batch_first=True, padding_value=pad_idx)
|
| 79 |
+
tgt_batch = pad_sequence(tgt_batch, batch_first=True, padding_value=pad_idx)
|
| 80 |
+
|
| 81 |
+
return src_batch, tgt_batch, src_lengths, tgt_lengths
|
| 82 |
+
|
| 83 |
+
# ============================================================================
|
| 84 |
+
# COLLATE WRAPPER - FIX CHO WINDOWS MULTIPROCESSING
|
| 85 |
+
# ============================================================================
|
| 86 |
+
|
| 87 |
+
class CollateWrapper:
|
| 88 |
+
"""
|
| 89 |
+
Wrapper cho collate_fn để tránh lỗi pickle với lambda trên Windows
|
| 90 |
+
"""
|
| 91 |
+
def __init__(self, pad_idx=0):
|
| 92 |
+
self.pad_idx = pad_idx
|
| 93 |
+
|
| 94 |
+
def __call__(self, batch):
|
| 95 |
+
return collate_fn(batch, self.pad_idx)
|
| 96 |
+
|
| 97 |
+
# ============================================================================
|
| 98 |
+
# 3. TẠO DATALOADER
|
| 99 |
+
# ============================================================================
|
| 100 |
+
|
| 101 |
+
def create_dataloaders(processed_data, batch_size=32, num_workers=0):
|
| 102 |
+
"""
|
| 103 |
+
Tạo DataLoader cho train, validation và test
|
| 104 |
+
|
| 105 |
+
Args:
|
| 106 |
+
processed_data: Dict chứa 'train', 'validation', 'test'
|
| 107 |
+
batch_size: Kích thước batch
|
| 108 |
+
num_workers: Số worker threads (set = 0 cho Windows)
|
| 109 |
+
|
| 110 |
+
Returns:
|
| 111 |
+
train_loader, val_loader, test_loader
|
| 112 |
+
"""
|
| 113 |
+
print("\n" + "="*70)
|
| 114 |
+
print("TẠO DATALOADERS")
|
| 115 |
+
print("="*70)
|
| 116 |
+
|
| 117 |
+
# Tạo datasets
|
| 118 |
+
train_dataset = TranslationDataset(
|
| 119 |
+
processed_data['train']['src'],
|
| 120 |
+
processed_data['train']['tgt']
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
val_dataset = TranslationDataset(
|
| 124 |
+
processed_data['validation']['src'],
|
| 125 |
+
processed_data['validation']['tgt']
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
test_dataset = TranslationDataset(
|
| 129 |
+
processed_data['test']['src'],
|
| 130 |
+
processed_data['test']['tgt']
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
print(f"\nKích thước datasets:")
|
| 134 |
+
print(f" - Train: {len(train_dataset)}")
|
| 135 |
+
print(f" - Validation: {len(val_dataset)}")
|
| 136 |
+
print(f" - Test: {len(test_dataset)}")
|
| 137 |
+
|
| 138 |
+
# Tạo collate wrapper
|
| 139 |
+
collate_wrapper = CollateWrapper(pad_idx=0)
|
| 140 |
+
|
| 141 |
+
# Tạo dataloaders
|
| 142 |
+
train_loader = DataLoader(
|
| 143 |
+
train_dataset,
|
| 144 |
+
batch_size=batch_size,
|
| 145 |
+
shuffle=True,
|
| 146 |
+
collate_fn=collate_wrapper,
|
| 147 |
+
num_workers=num_workers,
|
| 148 |
+
pin_memory=torch.cuda.is_available() # Chỉ pin_memory nếu có GPU
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
val_loader = DataLoader(
|
| 152 |
+
val_dataset,
|
| 153 |
+
batch_size=batch_size,
|
| 154 |
+
shuffle=False,
|
| 155 |
+
collate_fn=collate_wrapper,
|
| 156 |
+
num_workers=num_workers,
|
| 157 |
+
pin_memory=torch.cuda.is_available()
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
test_loader = DataLoader(
|
| 161 |
+
test_dataset,
|
| 162 |
+
batch_size=batch_size,
|
| 163 |
+
shuffle=False,
|
| 164 |
+
collate_fn=collate_wrapper,
|
| 165 |
+
num_workers=num_workers,
|
| 166 |
+
pin_memory=torch.cuda.is_available()
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
print(f"\nSố batch mỗi epoch:")
|
| 170 |
+
print(f" - Train: {len(train_loader)}")
|
| 171 |
+
print(f" - Validation: {len(val_loader)}")
|
| 172 |
+
print(f" - Test: {len(test_loader)}")
|
| 173 |
+
|
| 174 |
+
return train_loader, val_loader, test_loader
|
| 175 |
+
|
| 176 |
+
# ============================================================================
|
| 177 |
+
# 4. BUCKET SAMPLER - TỐI ƯU HƠN (OPTIONAL)
|
| 178 |
+
# ============================================================================
|
| 179 |
+
|
| 180 |
+
class BucketSampler(torch.utils.data.Sampler):
|
| 181 |
+
"""
|
| 182 |
+
Sampler nhóm các câu có độ dài tương tự vào cùng batch
|
| 183 |
+
Giảm thiểu padding, tăng tốc huấn luyện
|
| 184 |
+
|
| 185 |
+
TỐI ƯU: Chỉ sort một phần dataset để tránh lag với dataset lớn
|
| 186 |
+
"""
|
| 187 |
+
def __init__(self, data_source, batch_size, sort_key=lambda x: len(x)):
|
| 188 |
+
self.data_source = data_source
|
| 189 |
+
self.batch_size = batch_size
|
| 190 |
+
self.sort_key = sort_key
|
| 191 |
+
|
| 192 |
+
def __iter__(self):
|
| 193 |
+
# Sort toàn bộ dataset theo độ dài như trước
|
| 194 |
+
indices = list(range(len(self.data_source)))
|
| 195 |
+
lengths = [self.sort_key(self.data_source[i]) for i in indices]
|
| 196 |
+
all_indices = [i for i, _ in sorted(zip(indices, lengths), key=lambda x: x[1])]
|
| 197 |
+
|
| 198 |
+
# Chia thành các batch
|
| 199 |
+
batches = [all_indices[i:i+self.batch_size]
|
| 200 |
+
for i in range(0, len(all_indices), self.batch_size)]
|
| 201 |
+
|
| 202 |
+
# Shuffle thứ tự các batch (không shuffle trong batch)
|
| 203 |
+
np.random.shuffle(batches)
|
| 204 |
+
|
| 205 |
+
# Flatten
|
| 206 |
+
for batch in batches:
|
| 207 |
+
for idx in batch:
|
| 208 |
+
yield idx
|
| 209 |
+
|
| 210 |
+
def __len__(self):
|
| 211 |
+
return len(self.data_source)
|
| 212 |
+
|
| 213 |
+
def create_dataloaders_with_bucketing(processed_data, batch_size=32, num_workers=0, val_batch_size=None):
|
| 214 |
+
"""
|
| 215 |
+
Tạo DataLoader với BucketSampler để tối ưu padding
|
| 216 |
+
|
| 217 |
+
IMPORTANT: num_workers phải = 0 trên Windows
|
| 218 |
+
|
| 219 |
+
Args:
|
| 220 |
+
processed_data: Dictionary chứa train/validation/test data
|
| 221 |
+
batch_size: Batch size cho training (mặc định 32)
|
| 222 |
+
num_workers: Số workers cho DataLoader (mặc định 0)
|
| 223 |
+
val_batch_size: Batch size cho validation (mặc định = batch_size // 2 để tránh OOM)
|
| 224 |
+
"""
|
| 225 |
+
# Validation batch size nhỏ hơn để tránh OOM
|
| 226 |
+
if val_batch_size is None:
|
| 227 |
+
val_batch_size = max(1, batch_size // 2) # Giảm 50% so với train
|
| 228 |
+
print("\n" + "="*70)
|
| 229 |
+
print("TẠO DATALOADERS VỚI BUCKET SAMPLING")
|
| 230 |
+
print("="*70)
|
| 231 |
+
|
| 232 |
+
# Tạo datasets
|
| 233 |
+
train_dataset = TranslationDataset(
|
| 234 |
+
processed_data['train']['src'],
|
| 235 |
+
processed_data['train']['tgt']
|
| 236 |
+
)
|
| 237 |
+
|
| 238 |
+
val_dataset = TranslationDataset(
|
| 239 |
+
processed_data['validation']['src'],
|
| 240 |
+
processed_data['validation']['tgt']
|
| 241 |
+
)
|
| 242 |
+
|
| 243 |
+
test_dataset = TranslationDataset(
|
| 244 |
+
processed_data['test']['src'],
|
| 245 |
+
processed_data['test']['tgt']
|
| 246 |
+
)
|
| 247 |
+
|
| 248 |
+
# Tạo BucketSampler cho train - Sort toàn bộ dataset theo độ dài
|
| 249 |
+
train_sampler = BucketSampler(
|
| 250 |
+
train_dataset.src_data,
|
| 251 |
+
batch_size=batch_size,
|
| 252 |
+
sort_key=lambda x: len(x)
|
| 253 |
+
)
|
| 254 |
+
|
| 255 |
+
# Tạo collate wrapper
|
| 256 |
+
collate_wrapper = CollateWrapper(pad_idx=0)
|
| 257 |
+
|
| 258 |
+
# Tạo dataloaders với tối ưu hóa
|
| 259 |
+
use_gpu = torch.cuda.is_available()
|
| 260 |
+
pin_memory = use_gpu and num_workers > 0
|
| 261 |
+
persistent_workers = num_workers > 0
|
| 262 |
+
prefetch_factor = 2 if num_workers > 0 else None
|
| 263 |
+
|
| 264 |
+
train_loader = DataLoader(
|
| 265 |
+
train_dataset,
|
| 266 |
+
batch_size=batch_size,
|
| 267 |
+
sampler=train_sampler,
|
| 268 |
+
collate_fn=collate_wrapper,
|
| 269 |
+
num_workers=num_workers,
|
| 270 |
+
pin_memory=pin_memory,
|
| 271 |
+
persistent_workers=persistent_workers,
|
| 272 |
+
prefetch_factor=prefetch_factor
|
| 273 |
+
)
|
| 274 |
+
|
| 275 |
+
val_loader = DataLoader(
|
| 276 |
+
val_dataset,
|
| 277 |
+
batch_size=val_batch_size, # Dùng batch size nhỏ hơn cho validation
|
| 278 |
+
shuffle=False,
|
| 279 |
+
collate_fn=collate_wrapper,
|
| 280 |
+
num_workers=num_workers,
|
| 281 |
+
pin_memory=pin_memory,
|
| 282 |
+
persistent_workers=persistent_workers,
|
| 283 |
+
prefetch_factor=prefetch_factor
|
| 284 |
+
)
|
| 285 |
+
|
| 286 |
+
test_loader = DataLoader(
|
| 287 |
+
test_dataset,
|
| 288 |
+
batch_size=val_batch_size, # Test cũng dùng batch size nhỏ hơn
|
| 289 |
+
shuffle=False,
|
| 290 |
+
collate_fn=collate_wrapper,
|
| 291 |
+
num_workers=num_workers,
|
| 292 |
+
pin_memory=pin_memory,
|
| 293 |
+
persistent_workers=persistent_workers,
|
| 294 |
+
prefetch_factor=prefetch_factor
|
| 295 |
+
)
|
| 296 |
+
|
| 297 |
+
print(f"✓ Đã tạo DataLoaders với Bucket Sampling")
|
| 298 |
+
print(f" Train batch size: {batch_size}")
|
| 299 |
+
print(f" Val/Test batch size: {val_batch_size} (giảm để tránh OOM)")
|
| 300 |
+
print(f" Bucket Sampling giúp giảm padding, tăng tốc ~15-20%")
|
| 301 |
+
if num_workers == 0:
|
| 302 |
+
print(f" ⚠️ num_workers=0 (Windows compatibility mode)")
|
| 303 |
+
|
| 304 |
+
return train_loader, val_loader, test_loader
|
| 305 |
+
|
| 306 |
+
# ============================================================================
|
| 307 |
+
# 5. HELPER FUNCTIONS
|
| 308 |
+
# ============================================================================
|
| 309 |
+
|
| 310 |
+
def load_data_and_vocab(use_shared_vocab=True, use_bidirectional=False):
|
| 311 |
+
"""
|
| 312 |
+
Load dữ liệu và vocabulary với Shared Vocabulary
|
| 313 |
+
|
| 314 |
+
Args:
|
| 315 |
+
use_shared_vocab: Luôn True (giữ để tương thích)
|
| 316 |
+
use_bidirectional: Nếu True, load bidirectional dataset (có cả vi→en và en→vi)
|
| 317 |
+
|
| 318 |
+
Returns:
|
| 319 |
+
processed_data: Dict với keys 'train', 'validation', 'test'
|
| 320 |
+
vi_vocab: Shared vocabulary wrapper (Vietnamese)
|
| 321 |
+
en_vocab: Shared vocabulary wrapper (English, cùng tokenizer với vi_vocab)
|
| 322 |
+
"""
|
| 323 |
+
print("Đang load shared vocabulary data...")
|
| 324 |
+
|
| 325 |
+
from shared_vocab_utils import (
|
| 326 |
+
load_shared_processed_data,
|
| 327 |
+
create_shared_vocab_wrapper
|
| 328 |
+
)
|
| 329 |
+
|
| 330 |
+
processed_data = load_shared_processed_data(use_bidirectional=use_bidirectional)
|
| 331 |
+
|
| 332 |
+
# Convert format từ list of tuples sang dict với 'src' và 'tgt'
|
| 333 |
+
# Format: {'train': [(src_ids, tgt_ids), ...]}
|
| 334 |
+
# → {'train': {'src': [src_ids, ...], 'tgt': [tgt_ids, ...]}}
|
| 335 |
+
converted_data = {}
|
| 336 |
+
for split in ['train', 'validation', 'test']:
|
| 337 |
+
if split in processed_data:
|
| 338 |
+
src_list = [item[0] for item in processed_data[split]]
|
| 339 |
+
tgt_list = [item[1] for item in processed_data[split]]
|
| 340 |
+
converted_data[split] = {
|
| 341 |
+
'src': src_list,
|
| 342 |
+
'tgt': tgt_list
|
| 343 |
+
}
|
| 344 |
+
else:
|
| 345 |
+
converted_data[split] = {'src': [], 'tgt': []}
|
| 346 |
+
|
| 347 |
+
# Load shared vocab wrappers
|
| 348 |
+
vi_vocab, en_vocab = create_shared_vocab_wrapper()
|
| 349 |
+
|
| 350 |
+
print("✓ Đã load shared vocabulary data!")
|
| 351 |
+
|
| 352 |
+
return converted_data, vi_vocab, en_vocab
|
| 353 |
+
|
| 354 |
+
def test_dataloader(loader, vi_vocab, en_vocab, num_batches=2):
|
| 355 |
+
"""
|
| 356 |
+
Test dataloader và xem dữ liệu
|
| 357 |
+
"""
|
| 358 |
+
print("\n" + "="*70)
|
| 359 |
+
print("KIỂM TRA DATALOADER")
|
| 360 |
+
print("="*70)
|
| 361 |
+
|
| 362 |
+
for i, (src, tgt, src_len, tgt_len) in enumerate(loader):
|
| 363 |
+
if i >= num_batches:
|
| 364 |
+
break
|
| 365 |
+
|
| 366 |
+
print(f"\nBatch {i+1}:")
|
| 367 |
+
print(f" Source shape: {src.shape}")
|
| 368 |
+
print(f" Target shape: {tgt.shape}")
|
| 369 |
+
print(f" Source lengths: {src_len[:5].tolist()}...")
|
| 370 |
+
print(f" Target lengths: {tgt_len[:5].tolist()}...")
|
| 371 |
+
|
| 372 |
+
# Decode ví dụ đầu tiên
|
| 373 |
+
print(f"\n Ví dụ đầu tiên trong batch:")
|
| 374 |
+
src_text = vi_vocab.decode(src[0].tolist())
|
| 375 |
+
tgt_text = en_vocab.decode(tgt[0].tolist())
|
| 376 |
+
print(f" VI: {src_text}")
|
| 377 |
+
print(f" EN: {tgt_text}")
|
| 378 |
+
|
| 379 |
+
# ============================================================================
|
| 380 |
+
# 6. MAIN
|
| 381 |
+
# ============================================================================
|
| 382 |
+
|
| 383 |
+
if __name__ == "__main__":
|
| 384 |
+
# Load dữ liệu
|
| 385 |
+
processed_data, vi_vocab, en_vocab = load_data_and_vocab()
|
| 386 |
+
|
| 387 |
+
# Tạo dataloaders thông thường
|
| 388 |
+
print("\n" + "="*70)
|
| 389 |
+
print("PHƯƠNG ÁN 1: DATALOADER THÔNG THƯỜNG")
|
| 390 |
+
print("="*70)
|
| 391 |
+
train_loader, val_loader, test_loader = create_dataloaders(
|
| 392 |
+
processed_data,
|
| 393 |
+
batch_size=32,
|
| 394 |
+
num_workers=0 # 0 cho Windows
|
| 395 |
+
)
|
| 396 |
+
|
| 397 |
+
# Test
|
| 398 |
+
test_dataloader(train_loader, vi_vocab, en_vocab, num_batches=2)
|
| 399 |
+
|
| 400 |
+
# Tạo dataloaders với bucketing (khuyên dùng)
|
| 401 |
+
print("\n" + "="*70)
|
| 402 |
+
print("PHƯƠNG ÁN 2: DATALOADER VỚI BUCKET SAMPLING (KHUYÊN DÙNG)")
|
| 403 |
+
print("="*70)
|
| 404 |
+
train_loader_bucket, val_loader_bucket, test_loader_bucket = create_dataloaders_with_bucketing(
|
| 405 |
+
processed_data,
|
| 406 |
+
batch_size=32,
|
| 407 |
+
num_workers=0 # 0 cho Windows
|
| 408 |
+
)
|
| 409 |
+
|
| 410 |
+
# Test
|
| 411 |
+
test_dataloader(train_loader_bucket, vi_vocab, en_vocab, num_batches=2)
|
| 412 |
+
|
| 413 |
+
print("\n" + "="*70)
|
| 414 |
+
print("✓ HOÀN TẤT TẠO DATALOADER!")
|
| 415 |
+
print("="*70)
|
| 416 |
+
print("\nGợi ý:")
|
| 417 |
+
print(" - Sử dụng batch_size=32 hoặc 64 tùy GPU")
|
| 418 |
+
print(" - Sử dụng BucketSampler để tối ưu tốc độ")
|
| 419 |
+
print(" - num_workers=0 trên Windows (multiprocessing issue)")
|
| 420 |
+
print(" - num_workers=2-4 trên Linux/Mac để tăng tốc")
|
src/encode_mtet_bidirectional.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Encode mtet_bidirectional.csv với shared vocabulary
|
| 3 |
+
Chia train/validation/test và lưu thành processed data
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import pandas as pd
|
| 7 |
+
import pickle
|
| 8 |
+
import json
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from tqdm import tqdm
|
| 11 |
+
from tokenizers import Tokenizer
|
| 12 |
+
import sys
|
| 13 |
+
|
| 14 |
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
| 15 |
+
PROCESSED_DATA_DIR = PROJECT_ROOT / 'data' / 'processed'
|
| 16 |
+
INPUT_FILE = PROJECT_ROOT / 'mtet_bidirectional.csv'
|
| 17 |
+
|
| 18 |
+
def load_shared_tokenizer():
|
| 19 |
+
"""Load shared tokenizer"""
|
| 20 |
+
tokenizer_path = PROCESSED_DATA_DIR / 'tokenizer_shared.json'
|
| 21 |
+
if not tokenizer_path.exists():
|
| 22 |
+
raise FileNotFoundError(
|
| 23 |
+
f"Không tìm thấy tokenizer_shared.json tại {tokenizer_path}!\n"
|
| 24 |
+
f"Vui lòng chạy: python src/1_build_shared_vocab.py trước"
|
| 25 |
+
)
|
| 26 |
+
return Tokenizer.from_file(str(tokenizer_path))
|
| 27 |
+
|
| 28 |
+
def encode_csv_to_processed_data(
|
| 29 |
+
csv_file,
|
| 30 |
+
tokenizer,
|
| 31 |
+
train_ratio=0.95,
|
| 32 |
+
val_ratio=0.025,
|
| 33 |
+
test_ratio=0.025,
|
| 34 |
+
max_length=150,
|
| 35 |
+
chunk_size=100000
|
| 36 |
+
):
|
| 37 |
+
"""
|
| 38 |
+
Encode CSV file thành processed data và chia train/val/test
|
| 39 |
+
|
| 40 |
+
Args:
|
| 41 |
+
csv_file: Path to CSV file (có cột src và tgt)
|
| 42 |
+
tokenizer: Shared tokenizer
|
| 43 |
+
train_ratio: Tỷ lệ train (mặc định 0.95)
|
| 44 |
+
val_ratio: Tỷ lệ validation (mặc định 0.025)
|
| 45 |
+
test_ratio: Tỷ lệ test (mặc định 0.025)
|
| 46 |
+
max_length: Độ dài tối đa của sequence (mặc định 150)
|
| 47 |
+
chunk_size: Kích thước chunk để xử lý
|
| 48 |
+
|
| 49 |
+
Returns:
|
| 50 |
+
processed_data: Dict với keys 'train', 'validation', 'test'
|
| 51 |
+
"""
|
| 52 |
+
print("="*70)
|
| 53 |
+
print("ENCODE MTET BIDIRECTIONAL DATASET")
|
| 54 |
+
print("="*70)
|
| 55 |
+
print(f"Input: {csv_file}")
|
| 56 |
+
print(f"Train: {train_ratio*100:.1f}% | Val: {val_ratio*100:.1f}% | Test: {test_ratio*100:.1f}%")
|
| 57 |
+
print(f"Max length: {max_length} tokens")
|
| 58 |
+
print("="*70 + "\n")
|
| 59 |
+
|
| 60 |
+
# Kiểm tra file
|
| 61 |
+
if not csv_file.exists():
|
| 62 |
+
raise FileNotFoundError(f"Không tìm thấy file: {csv_file}")
|
| 63 |
+
|
| 64 |
+
# Lấy special token IDs
|
| 65 |
+
SOS_ID = tokenizer.token_to_id("<sos>")
|
| 66 |
+
EOS_ID = tokenizer.token_to_id("<eos>")
|
| 67 |
+
EN2VI_ID = tokenizer.token_to_id("<en2vi>")
|
| 68 |
+
VI2EN_ID = tokenizer.token_to_id("<vi2en>")
|
| 69 |
+
|
| 70 |
+
if SOS_ID is None or EOS_ID is None:
|
| 71 |
+
raise ValueError("Không tìm thấy <sos> hoặc <eos> trong tokenizer!")
|
| 72 |
+
if EN2VI_ID is None or VI2EN_ID is None:
|
| 73 |
+
raise ValueError("Không tìm thấy <en2vi> hoặc <vi2en> trong tokenizer!")
|
| 74 |
+
|
| 75 |
+
# Đếm tổng số dòng
|
| 76 |
+
print("📊 Đang đếm số dòng...")
|
| 77 |
+
total_rows = sum(1 for _ in open(csv_file, 'r', encoding='utf-8')) - 1 # Trừ header
|
| 78 |
+
print(f"✓ Tổng số dòng: {total_rows:,}\n")
|
| 79 |
+
|
| 80 |
+
# Encode tất cả data
|
| 81 |
+
print("🔄 Đang encode data...")
|
| 82 |
+
all_data = []
|
| 83 |
+
skipped = 0
|
| 84 |
+
|
| 85 |
+
# Đọc và encode theo chunks
|
| 86 |
+
for chunk_df in tqdm(
|
| 87 |
+
pd.read_csv(csv_file, chunksize=chunk_size, encoding='utf-8'),
|
| 88 |
+
total=(total_rows // chunk_size) + 1,
|
| 89 |
+
desc="Encoding chunks"
|
| 90 |
+
):
|
| 91 |
+
for _, row in chunk_df.iterrows():
|
| 92 |
+
src_text = str(row['src']).strip()
|
| 93 |
+
tgt_text = str(row['tgt']).strip()
|
| 94 |
+
|
| 95 |
+
if not src_text or not tgt_text or src_text == 'nan' or tgt_text == 'nan':
|
| 96 |
+
skipped += 1
|
| 97 |
+
continue
|
| 98 |
+
|
| 99 |
+
try:
|
| 100 |
+
# Detect direction: kiểm tra xem src là tiếng Anh hay tiếng Việt
|
| 101 |
+
# Phát hiện đơn giản: nếu có ký tự tiếng Việt (Unicode > 0x0100) thì là tiếng Việt
|
| 102 |
+
has_vietnamese_chars = any(ord(c) >= 0x0100 for c in src_text)
|
| 103 |
+
direction_id = VI2EN_ID if has_vietnamese_chars else EN2VI_ID
|
| 104 |
+
|
| 105 |
+
# Encode
|
| 106 |
+
src_encoded = tokenizer.encode(src_text)
|
| 107 |
+
tgt_encoded = tokenizer.encode(tgt_text)
|
| 108 |
+
|
| 109 |
+
src_ids = src_encoded.ids
|
| 110 |
+
tgt_ids = tgt_encoded.ids
|
| 111 |
+
|
| 112 |
+
# Thêm SOS, direction token, và EOS
|
| 113 |
+
src_full = [SOS_ID, direction_id] + src_ids + [EOS_ID]
|
| 114 |
+
tgt_full = [SOS_ID] + tgt_ids + [EOS_ID]
|
| 115 |
+
|
| 116 |
+
# Filter quá dài
|
| 117 |
+
if len(src_full) > max_length:
|
| 118 |
+
src_full = src_full[:max_length-1] + [EOS_ID]
|
| 119 |
+
if len(tgt_full) > max_length:
|
| 120 |
+
tgt_full = tgt_full[:max_length-1] + [EOS_ID]
|
| 121 |
+
|
| 122 |
+
# Bỏ qua nếu quá ngắn (chỉ có SOS, direction token và EOS)
|
| 123 |
+
if len(src_full) <= 3 or len(tgt_full) <= 2:
|
| 124 |
+
skipped += 1
|
| 125 |
+
continue
|
| 126 |
+
|
| 127 |
+
all_data.append((src_full, tgt_full))
|
| 128 |
+
|
| 129 |
+
except Exception as e:
|
| 130 |
+
skipped += 1
|
| 131 |
+
continue
|
| 132 |
+
|
| 133 |
+
print(f"\n✓ Đã encode {len(all_data):,} cặp câu")
|
| 134 |
+
if skipped > 0:
|
| 135 |
+
print(f"⚠️ Đã bỏ qua {skipped:,} cặp câu (lỗi hoặc quá ngắn)")
|
| 136 |
+
|
| 137 |
+
# Chia train/val/test
|
| 138 |
+
print("\n📊 Đang chia train/validation/test...")
|
| 139 |
+
import random
|
| 140 |
+
random.seed(42)
|
| 141 |
+
random.shuffle(all_data)
|
| 142 |
+
|
| 143 |
+
total = len(all_data)
|
| 144 |
+
train_end = int(total * train_ratio)
|
| 145 |
+
val_end = train_end + int(total * val_ratio)
|
| 146 |
+
|
| 147 |
+
train_data = all_data[:train_end]
|
| 148 |
+
val_data = all_data[train_end:val_end]
|
| 149 |
+
test_data = all_data[val_end:]
|
| 150 |
+
|
| 151 |
+
processed_data = {
|
| 152 |
+
'train': train_data,
|
| 153 |
+
'validation': val_data,
|
| 154 |
+
'test': test_data
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
print(f"✓ Train: {len(train_data):,} cặp câu ({len(train_data)/total*100:.1f}%)")
|
| 158 |
+
print(f"✓ Validation: {len(val_data):,} cặp câu ({len(val_data)/total*100:.1f}%)")
|
| 159 |
+
print(f"✓ Test: {len(test_data):,} cặp câu ({len(test_data)/total*100:.1f}%)")
|
| 160 |
+
|
| 161 |
+
return processed_data
|
| 162 |
+
|
| 163 |
+
def main():
|
| 164 |
+
"""Main function"""
|
| 165 |
+
import argparse
|
| 166 |
+
|
| 167 |
+
parser = argparse.ArgumentParser(description='Encode mtet_bidirectional.csv')
|
| 168 |
+
parser.add_argument('--input', type=str, default=None,
|
| 169 |
+
help='File CSV input (mặc định: mtet_bidirectional.csv)')
|
| 170 |
+
parser.add_argument('--output', type=str, default=None,
|
| 171 |
+
help='File output (mặc định: processed_data_mtet_bidirectional.pkl)')
|
| 172 |
+
parser.add_argument('--train_ratio', type=float, default=0.95,
|
| 173 |
+
help='Tỷ lệ train (mặc định: 0.95)')
|
| 174 |
+
parser.add_argument('--val_ratio', type=float, default=0.025,
|
| 175 |
+
help='Tỷ lệ validation (mặc định: 0.025)')
|
| 176 |
+
parser.add_argument('--test_ratio', type=float, default=0.025,
|
| 177 |
+
help='Tỷ lệ test (mặc định: 0.025)')
|
| 178 |
+
parser.add_argument('--max_length', type=int, default=150,
|
| 179 |
+
help='Độ dài tối đa sequence (mặc định: 150)')
|
| 180 |
+
parser.add_argument('--chunk_size', type=int, default=100000,
|
| 181 |
+
help='Kích thước chunk (mặc định: 100000)')
|
| 182 |
+
|
| 183 |
+
args = parser.parse_args()
|
| 184 |
+
|
| 185 |
+
# Paths
|
| 186 |
+
input_file = Path(args.input) if args.input else INPUT_FILE
|
| 187 |
+
output_file = args.output if args.output else (PROCESSED_DATA_DIR / 'processed_data_mtet_bidirectional.pkl')
|
| 188 |
+
output_file = Path(output_file)
|
| 189 |
+
|
| 190 |
+
# Kiểm tra ratios
|
| 191 |
+
if abs(args.train_ratio + args.val_ratio + args.test_ratio - 1.0) > 0.01:
|
| 192 |
+
print("⚠️ Warning: Tổng các tỷ lệ không bằng 1.0, sẽ tự động điều chỉnh")
|
| 193 |
+
total = args.train_ratio + args.val_ratio + args.test_ratio
|
| 194 |
+
args.train_ratio /= total
|
| 195 |
+
args.val_ratio /= total
|
| 196 |
+
args.test_ratio /= total
|
| 197 |
+
|
| 198 |
+
# Load tokenizer
|
| 199 |
+
print("📚 Đang load shared tokenizer...")
|
| 200 |
+
tokenizer = load_shared_tokenizer()
|
| 201 |
+
vocab_size = tokenizer.get_vocab_size()
|
| 202 |
+
print(f"✓ Vocab size: {vocab_size:,}\n")
|
| 203 |
+
|
| 204 |
+
# Encode và chia data
|
| 205 |
+
processed_data = encode_csv_to_processed_data(
|
| 206 |
+
csv_file=input_file,
|
| 207 |
+
tokenizer=tokenizer,
|
| 208 |
+
train_ratio=args.train_ratio,
|
| 209 |
+
val_ratio=args.val_ratio,
|
| 210 |
+
test_ratio=args.test_ratio,
|
| 211 |
+
max_length=args.max_length,
|
| 212 |
+
chunk_size=args.chunk_size
|
| 213 |
+
)
|
| 214 |
+
|
| 215 |
+
# Lưu file
|
| 216 |
+
print(f"\n💾 Đang lưu processed data...")
|
| 217 |
+
PROCESSED_DATA_DIR.mkdir(parents=True, exist_ok=True)
|
| 218 |
+
|
| 219 |
+
with open(output_file, 'wb') as f:
|
| 220 |
+
pickle.dump(processed_data, f)
|
| 221 |
+
|
| 222 |
+
print(f"✓ Đã lưu vào: {output_file}")
|
| 223 |
+
|
| 224 |
+
# Thống kê
|
| 225 |
+
print("\n" + "="*70)
|
| 226 |
+
print("THỐNG KÊ")
|
| 227 |
+
print("="*70)
|
| 228 |
+
total = sum(len(v) for v in processed_data.values())
|
| 229 |
+
for split in ['train', 'validation', 'test']:
|
| 230 |
+
count = len(processed_data[split])
|
| 231 |
+
print(f"{split.capitalize():12s}: {count:>10,} cặp câu ({count/total*100:>5.1f}%)")
|
| 232 |
+
print(f"{'Tổng':12s}: {total:>10,} cặp câu")
|
| 233 |
+
print("="*70)
|
| 234 |
+
|
| 235 |
+
print(f"\n✅ Hoàn thành! File đã sẵn sàng để train.")
|
| 236 |
+
print(f" Sử dụng: python src/main_pipeline.py --use_mtet_bidirectional")
|
| 237 |
+
|
| 238 |
+
if __name__ == '__main__':
|
| 239 |
+
main()
|
| 240 |
+
|
src/finetune.py
ADDED
|
@@ -0,0 +1,520 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
FINETUNING SCRIPT
|
| 3 |
+
Script để finetune model đã được train trước đó
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
import argparse
|
| 8 |
+
import json
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
import sys
|
| 11 |
+
|
| 12 |
+
# Import modules
|
| 13 |
+
from dataloader_module import load_data_and_vocab, create_dataloaders_with_bucketing
|
| 14 |
+
from complete_transformer import create_model, print_model_info
|
| 15 |
+
from training_module import train_model, plot_training_history, load_checkpoint
|
| 16 |
+
from shared_vocab_utils import load_shared_vocab_info
|
| 17 |
+
|
| 18 |
+
# ============================================================================
|
| 19 |
+
# CONFIGURATION
|
| 20 |
+
# ============================================================================
|
| 21 |
+
|
| 22 |
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
| 23 |
+
DATA_DIR = PROJECT_ROOT / 'data'
|
| 24 |
+
CHECKPOINT_DIR = PROJECT_ROOT / 'checkpoints'
|
| 25 |
+
RESULTS_DIR = PROJECT_ROOT / 'results'
|
| 26 |
+
PROCESSED_DATA_DIR = DATA_DIR / 'processed'
|
| 27 |
+
|
| 28 |
+
class FinetuneConfig:
|
| 29 |
+
"""Configuration cho finetuning"""
|
| 30 |
+
|
| 31 |
+
def __init__(self):
|
| 32 |
+
# Paths
|
| 33 |
+
self.checkpoint_dir = CHECKPOINT_DIR
|
| 34 |
+
self.results_dir = RESULTS_DIR
|
| 35 |
+
|
| 36 |
+
# Model
|
| 37 |
+
self.model_size = 'custom_25m' # Phải khớp với checkpoint
|
| 38 |
+
self.use_weight_tying = True
|
| 39 |
+
|
| 40 |
+
# Finetuning settings (Transfer Learning cổ điển)
|
| 41 |
+
self.checkpoint_path = None # Sẽ được set từ args
|
| 42 |
+
self.learning_rate = 1e-5 # LR rất nhỏ (1e-5 hoặc 5e-6) - nhỏ hơn 10-100 lần training ban đầu
|
| 43 |
+
self.num_epochs = 5 # 5-10 epochs cho Transfer Learning
|
| 44 |
+
self.batch_size = 128
|
| 45 |
+
self.grad_accum_steps = 2
|
| 46 |
+
self.label_smoothing = 0.1
|
| 47 |
+
self.precision = 'bf16' # 'bf16', 'amp', hoặc 'fp32'
|
| 48 |
+
self.dropout = None # None = giữ nguyên, hoặc set giá trị mới (ví dụ: 0.2 nếu overfitting)
|
| 49 |
+
|
| 50 |
+
# Freeze layers (tùy chọn)
|
| 51 |
+
self.freeze_encoder = False
|
| 52 |
+
self.freeze_decoder = False
|
| 53 |
+
self.freeze_embeddings = False
|
| 54 |
+
|
| 55 |
+
# Device
|
| 56 |
+
self.device = 'cuda' if torch.cuda.is_available() else 'cpu'
|
| 57 |
+
|
| 58 |
+
# Num workers
|
| 59 |
+
import platform
|
| 60 |
+
self.num_workers = 0 if platform.system() == 'Windows' else 2
|
| 61 |
+
|
| 62 |
+
# Checkpoint
|
| 63 |
+
self.save_every = 1
|
| 64 |
+
|
| 65 |
+
# ============================================================================
|
| 66 |
+
# FREEZE LAYERS
|
| 67 |
+
# ============================================================================
|
| 68 |
+
|
| 69 |
+
def freeze_layers(model, freeze_encoder=False, freeze_decoder=False, freeze_embeddings=False):
|
| 70 |
+
"""
|
| 71 |
+
Freeze một số layers của model
|
| 72 |
+
|
| 73 |
+
Args:
|
| 74 |
+
model: Transformer model
|
| 75 |
+
freeze_encoder: Freeze toàn bộ encoder
|
| 76 |
+
freeze_decoder: Freeze toàn bộ decoder
|
| 77 |
+
freeze_embeddings: Freeze embedding layers
|
| 78 |
+
"""
|
| 79 |
+
if freeze_embeddings:
|
| 80 |
+
print("→ Freezing embedding layers...")
|
| 81 |
+
if hasattr(model, 'shared_embedding'):
|
| 82 |
+
for param in model.shared_embedding.parameters():
|
| 83 |
+
param.requires_grad = False
|
| 84 |
+
print(" ✓ Embeddings frozen")
|
| 85 |
+
|
| 86 |
+
if freeze_encoder:
|
| 87 |
+
print("→ Freezing encoder...")
|
| 88 |
+
for param in model.encoder.parameters():
|
| 89 |
+
param.requires_grad = False
|
| 90 |
+
print(" ✓ Encoder frozen")
|
| 91 |
+
|
| 92 |
+
if freeze_decoder:
|
| 93 |
+
print("→ Freezing decoder...")
|
| 94 |
+
for param in model.decoder.parameters():
|
| 95 |
+
param.requires_grad = False
|
| 96 |
+
print(" ✓ Decoder frozen")
|
| 97 |
+
|
| 98 |
+
# Đếm số parameters trainable
|
| 99 |
+
total_params = sum(p.numel() for p in model.parameters())
|
| 100 |
+
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
|
| 101 |
+
frozen_params = total_params - trainable_params
|
| 102 |
+
|
| 103 |
+
print(f"\n Total parameters: {total_params:,}")
|
| 104 |
+
print(f" Trainable: {trainable_params:,} ({100*trainable_params/total_params:.1f}%)")
|
| 105 |
+
print(f" Frozen: {frozen_params:,} ({100*frozen_params/total_params:.1f}%)")
|
| 106 |
+
|
| 107 |
+
# ============================================================================
|
| 108 |
+
# MAIN FINETUNING FUNCTION
|
| 109 |
+
# ============================================================================
|
| 110 |
+
|
| 111 |
+
def finetune_model(
|
| 112 |
+
checkpoint_path,
|
| 113 |
+
model_size='custom_25m',
|
| 114 |
+
learning_rate=1e-5, # LR rất nhỏ cho Transfer Learning
|
| 115 |
+
num_epochs=5,
|
| 116 |
+
batch_size=128,
|
| 117 |
+
grad_accum_steps=2,
|
| 118 |
+
precision='bf16',
|
| 119 |
+
freeze_encoder=False,
|
| 120 |
+
freeze_decoder=False,
|
| 121 |
+
freeze_embeddings=False,
|
| 122 |
+
dropout=None, # None = giữ nguyên, hoặc set giá trị mới
|
| 123 |
+
use_hospital_data=False, # True = dùng dữ liệu Hospital, False = dùng dữ liệu gốc
|
| 124 |
+
save_every=1
|
| 125 |
+
):
|
| 126 |
+
"""
|
| 127 |
+
Finetune model từ checkpoint
|
| 128 |
+
|
| 129 |
+
Args:
|
| 130 |
+
checkpoint_path: Đường dẫn đến checkpoint
|
| 131 |
+
model_size: Kích thước model (phải khớp với checkpoint)
|
| 132 |
+
finetune_lr_factor: Hệ số nhân learning rate (0.1 = 10% của lr ban đầu)
|
| 133 |
+
num_epochs: Số epochs để finetune
|
| 134 |
+
batch_size: Batch size
|
| 135 |
+
grad_accum_steps: Gradient accumulation steps
|
| 136 |
+
warmup_steps: Warmup steps (thường ít hơn training ban đầu)
|
| 137 |
+
precision: 'bf16', 'amp', hoặc 'fp32'
|
| 138 |
+
freeze_encoder: Freeze encoder layers
|
| 139 |
+
freeze_decoder: Freeze decoder layers
|
| 140 |
+
freeze_embeddings: Freeze embedding layers
|
| 141 |
+
save_every: Lưu checkpoint mỗi N epochs
|
| 142 |
+
"""
|
| 143 |
+
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
| 144 |
+
|
| 145 |
+
print("="*70)
|
| 146 |
+
print("FINETUNING MODEL - TRANSFER LEARNING")
|
| 147 |
+
print("="*70)
|
| 148 |
+
print(f"\nCheckpoint: {checkpoint_path}")
|
| 149 |
+
print(f"Model size: {model_size}")
|
| 150 |
+
print(f"Learning rate: {learning_rate} (rất nhỏ - Transfer Learning)")
|
| 151 |
+
print(f"Epochs: {num_epochs} (5-10 epochs cho Transfer Learning)")
|
| 152 |
+
print(f"Device: {device}")
|
| 153 |
+
if dropout is not None:
|
| 154 |
+
print(f"Dropout: {dropout} (điều chỉnh để tránh overfitting)")
|
| 155 |
+
print()
|
| 156 |
+
|
| 157 |
+
# Load vocabulary info
|
| 158 |
+
print("→ Loading vocabulary...")
|
| 159 |
+
vocab_info = load_shared_vocab_info()
|
| 160 |
+
vocab_size = vocab_info['vocab_size']
|
| 161 |
+
pad_idx = vocab_info['pad_id']
|
| 162 |
+
print(f" ✓ Vocab size: {vocab_size}")
|
| 163 |
+
print(f" ✓ PAD ID: {pad_idx}")
|
| 164 |
+
|
| 165 |
+
# Create model
|
| 166 |
+
print("\n→ Creating model...")
|
| 167 |
+
model, model_config = create_model(
|
| 168 |
+
src_vocab_size=vocab_size,
|
| 169 |
+
tgt_vocab_size=vocab_size,
|
| 170 |
+
model_size=model_size,
|
| 171 |
+
pad_idx=pad_idx,
|
| 172 |
+
use_shared_vocab=True,
|
| 173 |
+
use_weight_tying=True
|
| 174 |
+
)
|
| 175 |
+
|
| 176 |
+
# Điều chỉnh dropout nếu cần (để tránh overfitting)
|
| 177 |
+
if dropout is not None:
|
| 178 |
+
print(f"→ Điều chỉnh dropout: {dropout}")
|
| 179 |
+
# Điều chỉnh dropout trong encoder và decoder
|
| 180 |
+
for module in model.modules():
|
| 181 |
+
if isinstance(module, torch.nn.Dropout):
|
| 182 |
+
module.p = dropout
|
| 183 |
+
print(f" ✓ Dropout đã được set thành {dropout}")
|
| 184 |
+
|
| 185 |
+
model = model.to(device)
|
| 186 |
+
print(" ✓ Model created")
|
| 187 |
+
|
| 188 |
+
# Load checkpoint
|
| 189 |
+
print(f"\n→ Loading checkpoint: {checkpoint_path}")
|
| 190 |
+
checkpoint = torch.load(checkpoint_path, map_location=device)
|
| 191 |
+
model.load_state_dict(checkpoint['model_state_dict'])
|
| 192 |
+
|
| 193 |
+
# Print checkpoint info
|
| 194 |
+
checkpoint_epoch = checkpoint.get('epoch', 'N/A')
|
| 195 |
+
checkpoint_val_loss = checkpoint.get('val_loss', 'N/A')
|
| 196 |
+
print(f" ✓ Checkpoint loaded")
|
| 197 |
+
print(f" - Epoch: {checkpoint_epoch}")
|
| 198 |
+
print(f" - Val Loss: {checkpoint_val_loss:.4f}" if isinstance(checkpoint_val_loss, float) else f" - Val Loss: {checkpoint_val_loss}")
|
| 199 |
+
|
| 200 |
+
# Freeze layers nếu cần
|
| 201 |
+
if freeze_encoder or freeze_decoder or freeze_embeddings:
|
| 202 |
+
print("\n→ Freezing layers...")
|
| 203 |
+
freeze_layers(model, freeze_encoder, freeze_decoder, freeze_embeddings)
|
| 204 |
+
|
| 205 |
+
# Load data (có thể dùng dữ liệu Hospital hoặc dữ liệu gốc)
|
| 206 |
+
print("\n→ Loading data...")
|
| 207 |
+
|
| 208 |
+
# Kiểm tra xem có dữ liệu Hospital không
|
| 209 |
+
hospital_data_path = PROCESSED_DATA_DIR / 'hospital_data_encoded.pkl'
|
| 210 |
+
use_hospital = use_hospital_data and hospital_data_path.exists()
|
| 211 |
+
|
| 212 |
+
if use_hospital:
|
| 213 |
+
print(" → Sử dụng dữ liệu Hospital để finetune")
|
| 214 |
+
import pickle
|
| 215 |
+
with open(hospital_data_path, 'rb') as f:
|
| 216 |
+
processed_data = pickle.load(f)
|
| 217 |
+
|
| 218 |
+
# Load vocab (dùng shared vocab)
|
| 219 |
+
from shared_vocab_utils import create_shared_vocab_wrapper
|
| 220 |
+
vi_vocab, en_vocab = create_shared_vocab_wrapper()
|
| 221 |
+
|
| 222 |
+
print(f" ✓ Hospital Train: {len(processed_data['train']['src'])} cặp câu")
|
| 223 |
+
print(f" ✓ Hospital Validation: {len(processed_data['validation']['src'])} cặp câu")
|
| 224 |
+
if 'test' in processed_data:
|
| 225 |
+
print(f" ✓ Hospital Test: {len(processed_data['test']['src'])} cặp câu")
|
| 226 |
+
else:
|
| 227 |
+
print(" → Sử dụng dữ liệu gốc")
|
| 228 |
+
processed_data, vi_vocab, en_vocab = load_data_and_vocab(use_shared_vocab=True)
|
| 229 |
+
print(f" ✓ Train: {len(processed_data['train']['src'])} cặp câu")
|
| 230 |
+
print(f" ✓ Validation: {len(processed_data['validation']['src'])} cặp câu")
|
| 231 |
+
|
| 232 |
+
# Create dataloaders
|
| 233 |
+
print("\n→ Creating dataloaders...")
|
| 234 |
+
train_loader, val_loader, test_loader = create_dataloaders_with_bucketing(
|
| 235 |
+
processed_data,
|
| 236 |
+
batch_size=batch_size,
|
| 237 |
+
num_workers=0 if device.type == 'cpu' else 2
|
| 238 |
+
)
|
| 239 |
+
print(" ✓ Dataloaders created")
|
| 240 |
+
|
| 241 |
+
# Setup optimizer với learning rate nhỏ hơn
|
| 242 |
+
# TransformerLRScheduler sẽ nhân với factor, nhưng chúng ta cần điều chỉnh base lr
|
| 243 |
+
# Thay vì thay đổi scheduler, chúng ta sẽ dùng finetune_lr_factor trong scheduler
|
| 244 |
+
|
| 245 |
+
# Train với learning rate cố định rất nhỏ (Transfer Learning cổ điển)
|
| 246 |
+
print("\n" + "="*70)
|
| 247 |
+
print("BẮT ĐẦU FINETUNING - TRANSFER LEARNING")
|
| 248 |
+
print("="*70)
|
| 249 |
+
print(f"Phương pháp: Transfer Learning cổ điển (không dùng QLoRA/Unsloth)")
|
| 250 |
+
print(f"Learning rate: {learning_rate} (rất nhỏ - nhỏ hơn 10-100 lần training ban đầu)")
|
| 251 |
+
print(f"Optimizer: Adam mới tinh (không load từ checkpoint)")
|
| 252 |
+
print("="*70 + "\n")
|
| 253 |
+
|
| 254 |
+
# QUAN TRỌNG: Optimizer mới tinh (không load từ checkpoint)
|
| 255 |
+
# Đây là điểm khác biệt với resume training
|
| 256 |
+
import torch.optim as optim
|
| 257 |
+
|
| 258 |
+
# Chỉ optimize các parameters có requires_grad=True
|
| 259 |
+
trainable_params = [p for p in model.parameters() if p.requires_grad]
|
| 260 |
+
|
| 261 |
+
# Setup optimizer với learning rate cố định rất nhỏ
|
| 262 |
+
optimizer = optim.Adam(
|
| 263 |
+
trainable_params,
|
| 264 |
+
lr=learning_rate, # LR cố định rất nhỏ cho Transfer Learning
|
| 265 |
+
betas=(0.9, 0.98),
|
| 266 |
+
eps=1e-9
|
| 267 |
+
)
|
| 268 |
+
|
| 269 |
+
# Scheduler đơn giản: StepLR hoặc CosineAnnealingLR
|
| 270 |
+
# Hoặc không dùng scheduler, chỉ dùng LR cố định
|
| 271 |
+
from torch.optim.lr_scheduler import StepLR
|
| 272 |
+
scheduler = StepLR(optimizer, step_size=num_epochs, gamma=0.5) # Giảm 50% mỗi num_epochs
|
| 273 |
+
# Hoặc không dùng scheduler: scheduler = None và không gọi scheduler.step()
|
| 274 |
+
|
| 275 |
+
# Setup loss
|
| 276 |
+
from training_module import LabelSmoothingLoss
|
| 277 |
+
criterion = LabelSmoothingLoss(
|
| 278 |
+
vocab_size=vocab_size,
|
| 279 |
+
pad_idx=pad_idx,
|
| 280 |
+
smoothing=0.1
|
| 281 |
+
)
|
| 282 |
+
|
| 283 |
+
# Training history (bắt đầu mới cho finetune)
|
| 284 |
+
history = {
|
| 285 |
+
'train_loss': [],
|
| 286 |
+
'train_ppl': [],
|
| 287 |
+
'val_loss': [],
|
| 288 |
+
'val_ppl': [],
|
| 289 |
+
'lr': []
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
# Lấy best_val_loss từ checkpoint gốc để so sánh
|
| 293 |
+
original_val_loss = checkpoint.get('val_loss', float('inf'))
|
| 294 |
+
best_val_loss = original_val_loss
|
| 295 |
+
print(f"→ Validation loss của checkpoint gốc: {original_val_loss:.4f}")
|
| 296 |
+
|
| 297 |
+
# Import training functions
|
| 298 |
+
from training_module import train_epoch, validate, calculate_perplexity
|
| 299 |
+
import time
|
| 300 |
+
from tqdm import tqdm
|
| 301 |
+
|
| 302 |
+
use_amp = precision == 'amp' and device.type == 'cuda'
|
| 303 |
+
use_bf16 = precision == 'bf16' and device.type == 'cuda'
|
| 304 |
+
from torch.cuda.amp import GradScaler
|
| 305 |
+
scaler = GradScaler(enabled=use_amp and not use_bf16) if use_amp and not use_bf16 else None
|
| 306 |
+
|
| 307 |
+
print("="*70)
|
| 308 |
+
print("FINETUNING - TRANSFER LEARNING")
|
| 309 |
+
print("="*70)
|
| 310 |
+
print(f"Device: {device}")
|
| 311 |
+
print(f"Number of epochs: {num_epochs}")
|
| 312 |
+
print(f"Learning rate: {learning_rate} (cố định)")
|
| 313 |
+
print(f"Grad accumulation steps: {grad_accum_steps}")
|
| 314 |
+
print(f"Original val loss: {original_val_loss:.4f}")
|
| 315 |
+
if use_bf16:
|
| 316 |
+
print(f"Precision: BF16")
|
| 317 |
+
elif use_amp:
|
| 318 |
+
print(f"Precision: AMP")
|
| 319 |
+
else:
|
| 320 |
+
print(f"Precision: FP32")
|
| 321 |
+
print("="*70 + "\n")
|
| 322 |
+
|
| 323 |
+
for epoch in range(1, num_epochs + 1):
|
| 324 |
+
epoch_start_time = time.time()
|
| 325 |
+
|
| 326 |
+
# Training
|
| 327 |
+
train_loss, train_ppl = train_epoch(
|
| 328 |
+
model,
|
| 329 |
+
train_loader,
|
| 330 |
+
optimizer,
|
| 331 |
+
scheduler,
|
| 332 |
+
criterion,
|
| 333 |
+
device,
|
| 334 |
+
epoch,
|
| 335 |
+
grad_accum_steps=grad_accum_steps,
|
| 336 |
+
use_amp=use_amp,
|
| 337 |
+
use_bf16=use_bf16,
|
| 338 |
+
scaler=scaler,
|
| 339 |
+
)
|
| 340 |
+
|
| 341 |
+
# Validation
|
| 342 |
+
val_loss, val_ppl = validate(
|
| 343 |
+
model,
|
| 344 |
+
val_loader,
|
| 345 |
+
criterion,
|
| 346 |
+
device,
|
| 347 |
+
use_amp=use_amp,
|
| 348 |
+
use_bf16=use_bf16,
|
| 349 |
+
)
|
| 350 |
+
|
| 351 |
+
epoch_time = time.time() - epoch_start_time
|
| 352 |
+
|
| 353 |
+
# Save history
|
| 354 |
+
history['train_loss'].append(train_loss)
|
| 355 |
+
history['train_ppl'].append(train_ppl)
|
| 356 |
+
history['val_loss'].append(val_loss)
|
| 357 |
+
history['val_ppl'].append(val_ppl)
|
| 358 |
+
current_lr = scheduler.get_last_lr()[0] if scheduler else learning_rate
|
| 359 |
+
history['lr'].append(current_lr)
|
| 360 |
+
|
| 361 |
+
# Save checkpoint
|
| 362 |
+
checkpoint_msg = ""
|
| 363 |
+
best_msg = ""
|
| 364 |
+
|
| 365 |
+
if epoch % save_every == 0:
|
| 366 |
+
checkpoint_path_save = CHECKPOINT_DIR / f'finetune_checkpoint_epoch_{epoch}.pt'
|
| 367 |
+
torch.save({
|
| 368 |
+
'epoch': epoch,
|
| 369 |
+
'model_state_dict': model.state_dict(),
|
| 370 |
+
'optimizer_state_dict': optimizer.state_dict(),
|
| 371 |
+
'scheduler_state_dict': scheduler.state_dict() if scheduler else None,
|
| 372 |
+
'train_loss': train_loss,
|
| 373 |
+
'val_loss': val_loss,
|
| 374 |
+
'history': history,
|
| 375 |
+
'finetune_config': {
|
| 376 |
+
'checkpoint_path': str(checkpoint_path),
|
| 377 |
+
'learning_rate': learning_rate,
|
| 378 |
+
'model_size': model_size,
|
| 379 |
+
'original_val_loss': original_val_loss,
|
| 380 |
+
'method': 'transfer_learning_classic'
|
| 381 |
+
}
|
| 382 |
+
}, checkpoint_path_save)
|
| 383 |
+
checkpoint_msg = " | ✓ Saved checkpoint"
|
| 384 |
+
|
| 385 |
+
# Save best model
|
| 386 |
+
if val_loss < best_val_loss:
|
| 387 |
+
best_val_loss = val_loss
|
| 388 |
+
best_model_path = CHECKPOINT_DIR / 'best_model_finetuned.pt'
|
| 389 |
+
torch.save({
|
| 390 |
+
'epoch': epoch,
|
| 391 |
+
'model_state_dict': model.state_dict(),
|
| 392 |
+
'val_loss': val_loss,
|
| 393 |
+
'val_ppl': val_ppl,
|
| 394 |
+
'finetune_config': {
|
| 395 |
+
'checkpoint_path': str(checkpoint_path),
|
| 396 |
+
'learning_rate': learning_rate,
|
| 397 |
+
'model_size': model_size,
|
| 398 |
+
'original_val_loss': original_val_loss,
|
| 399 |
+
'improvement': original_val_loss - val_loss,
|
| 400 |
+
'method': 'transfer_learning_classic'
|
| 401 |
+
}
|
| 402 |
+
}, best_model_path)
|
| 403 |
+
improvement = original_val_loss - val_loss
|
| 404 |
+
best_msg = f" | ✓ New best! (Cải thiện: {improvement:.4f})"
|
| 405 |
+
|
| 406 |
+
current_lr = scheduler.get_last_lr()[0] if scheduler else learning_rate
|
| 407 |
+
print(f"Epoch {epoch}/{num_epochs} | Time: {epoch_time:.2f}s | Train Loss: {train_loss:.4f} | Train PPL: {train_ppl:.2f} | Val Loss: {val_loss:.4f} | Val PPL: {val_ppl:.2f} | LR: {current_lr:.6f}{checkpoint_msg}{best_msg}")
|
| 408 |
+
|
| 409 |
+
# Step scheduler (nếu có)
|
| 410 |
+
if scheduler:
|
| 411 |
+
scheduler.step()
|
| 412 |
+
|
| 413 |
+
print("\n" + "="*70)
|
| 414 |
+
print("HOÀN TẤT FINETUNING - TRANSFER LEARNING")
|
| 415 |
+
print("="*70)
|
| 416 |
+
print(f"Original validation loss: {original_val_loss:.4f}")
|
| 417 |
+
print(f"Best validation loss: {best_val_loss:.4f}")
|
| 418 |
+
improvement = original_val_loss - best_val_loss
|
| 419 |
+
if improvement > 0:
|
| 420 |
+
print(f"✓ Cải thiện: {improvement:.4f} ({improvement/original_val_loss*100:.2f}%)")
|
| 421 |
+
else:
|
| 422 |
+
print(f"⚠️ Không cải thiện (có thể cần điều chỉnh LR hoặc epochs)")
|
| 423 |
+
print(f"Best validation perplexity: {calculate_perplexity(best_val_loss):.2f}")
|
| 424 |
+
|
| 425 |
+
# Plot history
|
| 426 |
+
finetune_plot_path = RESULTS_DIR / 'finetune_history.png'
|
| 427 |
+
plot_training_history(history, save_path=finetune_plot_path)
|
| 428 |
+
|
| 429 |
+
# Save history
|
| 430 |
+
finetune_history_path = RESULTS_DIR / 'finetune_history.json'
|
| 431 |
+
with open(finetune_history_path, 'w') as f:
|
| 432 |
+
json.dump(history, f, indent=2)
|
| 433 |
+
|
| 434 |
+
print(f"\n✓ Saved finetune history to {finetune_history_path}")
|
| 435 |
+
print(f"✓ Saved finetune plot to {finetune_plot_path}")
|
| 436 |
+
|
| 437 |
+
return history
|
| 438 |
+
|
| 439 |
+
# ============================================================================
|
| 440 |
+
# MAIN
|
| 441 |
+
# ============================================================================
|
| 442 |
+
|
| 443 |
+
def main():
|
| 444 |
+
parser = argparse.ArgumentParser(description='Finetune Transformer Model')
|
| 445 |
+
|
| 446 |
+
# Checkpoint
|
| 447 |
+
parser.add_argument('--checkpoint', type=str,
|
| 448 |
+
default='checkpoints/best_model.pt',
|
| 449 |
+
help='Path to checkpoint to finetune from')
|
| 450 |
+
|
| 451 |
+
# Model
|
| 452 |
+
parser.add_argument('--model_size', type=str, default='custom_25m',
|
| 453 |
+
help='Model size (must match checkpoint)')
|
| 454 |
+
|
| 455 |
+
# Finetuning settings (Transfer Learning cổ điển)
|
| 456 |
+
parser.add_argument('--learning_rate', type=float, default=1e-5,
|
| 457 |
+
help='Learning rate (rất nhỏ: 1e-5 hoặc 5e-6, nhỏ hơn 10-100 lần training ban đầu)')
|
| 458 |
+
parser.add_argument('--epochs', type=int, default=5,
|
| 459 |
+
help='Number of epochs (5-10 epochs cho Transfer Learning)')
|
| 460 |
+
parser.add_argument('--batch_size', type=int, default=128,
|
| 461 |
+
help='Batch size')
|
| 462 |
+
parser.add_argument('--grad_accum_steps', type=int, default=2,
|
| 463 |
+
help='Gradient accumulation steps')
|
| 464 |
+
parser.add_argument('--precision', type=str, default='bf16',
|
| 465 |
+
choices=['bf16', 'amp', 'fp32'],
|
| 466 |
+
help='Training precision')
|
| 467 |
+
parser.add_argument('--dropout', type=float, default=None,
|
| 468 |
+
help='Dropout rate (None = giữ nguyên, hoặc set 0.2 nếu overfitting)')
|
| 469 |
+
parser.add_argument('--use_hospital', action='store_true',
|
| 470 |
+
help='Use Hospital domain data for finetuning (requires prepare_hospital_data.py first)')
|
| 471 |
+
|
| 472 |
+
# Freeze layers
|
| 473 |
+
parser.add_argument('--freeze_encoder', action='store_true',
|
| 474 |
+
help='Freeze encoder layers (chỉ train decoder)')
|
| 475 |
+
parser.add_argument('--freeze_decoder', action='store_true',
|
| 476 |
+
help='Freeze decoder layers (chỉ train encoder)')
|
| 477 |
+
parser.add_argument('--freeze_embeddings', action='store_true',
|
| 478 |
+
help='Freeze embedding layers')
|
| 479 |
+
|
| 480 |
+
args = parser.parse_args()
|
| 481 |
+
|
| 482 |
+
# Resolve checkpoint path
|
| 483 |
+
checkpoint_path = Path(args.checkpoint)
|
| 484 |
+
if not checkpoint_path.is_absolute():
|
| 485 |
+
checkpoint_path = PROJECT_ROOT / checkpoint_path
|
| 486 |
+
|
| 487 |
+
if not checkpoint_path.exists():
|
| 488 |
+
print(f"❌ Error: Checkpoint not found: {checkpoint_path}")
|
| 489 |
+
print(f" Available checkpoints:")
|
| 490 |
+
for ckpt in CHECKPOINT_DIR.glob('*.pt'):
|
| 491 |
+
print(f" - {ckpt}")
|
| 492 |
+
sys.exit(1)
|
| 493 |
+
|
| 494 |
+
# Run finetuning
|
| 495 |
+
finetune_model(
|
| 496 |
+
checkpoint_path=str(checkpoint_path),
|
| 497 |
+
model_size=args.model_size,
|
| 498 |
+
learning_rate=args.learning_rate,
|
| 499 |
+
num_epochs=args.epochs,
|
| 500 |
+
batch_size=args.batch_size,
|
| 501 |
+
grad_accum_steps=args.grad_accum_steps,
|
| 502 |
+
precision=args.precision,
|
| 503 |
+
freeze_encoder=args.freeze_encoder,
|
| 504 |
+
freeze_decoder=args.freeze_decoder,
|
| 505 |
+
freeze_embeddings=args.freeze_embeddings,
|
| 506 |
+
dropout=args.dropout,
|
| 507 |
+
use_hospital_data=args.use_hospital,
|
| 508 |
+
save_every=1
|
| 509 |
+
)
|
| 510 |
+
|
| 511 |
+
print("\n" + "="*70)
|
| 512 |
+
print("🎉 FINETUNING HOÀN TẤT!")
|
| 513 |
+
print("="*70)
|
| 514 |
+
print(f"\nBest finetuned model: {CHECKPOINT_DIR / 'best_model_finetuned.pt'}")
|
| 515 |
+
print(f"Training history: {RESULTS_DIR / 'finetune_history.json'}")
|
| 516 |
+
print(f"Training plot: {RESULTS_DIR / 'finetune_history.png'}")
|
| 517 |
+
|
| 518 |
+
if __name__ == "__main__":
|
| 519 |
+
main()
|
| 520 |
+
|
src/finetune_hospital.py
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Finetune model với Hospital dataset (1M Hospital + 400k old data)
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
# Set CUDA memory allocation để tránh fragmentation
|
| 7 |
+
os.environ['PYTORCH_ALLOC_CONF'] = 'expandable_segments:True'
|
| 8 |
+
|
| 9 |
+
import torch
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
import sys
|
| 12 |
+
import argparse
|
| 13 |
+
|
| 14 |
+
# Add parent directory to path
|
| 15 |
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
| 16 |
+
|
| 17 |
+
from src.complete_transformer import TransformerShared, get_model_config
|
| 18 |
+
from src.shared_vocab_utils import load_shared_tokenizer, load_shared_vocab_info
|
| 19 |
+
from src.dataloader_module import create_dataloaders_with_bucketing
|
| 20 |
+
from src.training_module import train_model, load_checkpoint
|
| 21 |
+
from src.gpu_safety import get_gpu_memory_info, check_and_adjust_batch_size, clear_gpu_cache
|
| 22 |
+
import pickle
|
| 23 |
+
|
| 24 |
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
| 25 |
+
PROCESSED_DATA_DIR = PROJECT_ROOT / 'data' / 'processed'
|
| 26 |
+
CHECKPOINT_DIR = PROJECT_ROOT / 'checkpoints'
|
| 27 |
+
|
| 28 |
+
def load_hospital_finetune_data():
|
| 29 |
+
"""Load processed hospital finetune data (đã làm sạch)"""
|
| 30 |
+
data_path = PROCESSED_DATA_DIR / "processed_data_hospital_finetune_cleaned.pkl"
|
| 31 |
+
|
| 32 |
+
if not data_path.exists():
|
| 33 |
+
raise FileNotFoundError(
|
| 34 |
+
f"Không tìm thấy processed data tại {data_path}!\n"
|
| 35 |
+
f"Vui lòng chạy: python src/encode_mtet_bidirectional.py --input hospital_finetune_bidirectional_cleaned.csv --output data/processed/processed_data_hospital_finetune_cleaned.pkl trước"
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
print(f"📂 Loading data từ: {data_path}")
|
| 39 |
+
with open(data_path, 'rb') as f:
|
| 40 |
+
data = pickle.load(f)
|
| 41 |
+
|
| 42 |
+
# Convert format từ list of tuples sang dict với 'src' và 'tgt'
|
| 43 |
+
converted_data = {}
|
| 44 |
+
for split in ['train', 'validation', 'test']:
|
| 45 |
+
if split in data:
|
| 46 |
+
src_list = [item[0] for item in data[split]]
|
| 47 |
+
tgt_list = [item[1] for item in data[split]]
|
| 48 |
+
converted_data[split] = {
|
| 49 |
+
'src': src_list,
|
| 50 |
+
'tgt': tgt_list
|
| 51 |
+
}
|
| 52 |
+
else:
|
| 53 |
+
converted_data[split] = {'src': [], 'tgt': []}
|
| 54 |
+
|
| 55 |
+
return converted_data
|
| 56 |
+
|
| 57 |
+
def main():
|
| 58 |
+
parser = argparse.ArgumentParser(description="Finetune với Hospital dataset")
|
| 59 |
+
parser.add_argument(
|
| 60 |
+
"--checkpoint",
|
| 61 |
+
type=str,
|
| 62 |
+
default="checkpoints/best_model.pt",
|
| 63 |
+
help="Checkpoint để load (mặc định: checkpoints/best_model.pt)",
|
| 64 |
+
)
|
| 65 |
+
parser.add_argument(
|
| 66 |
+
"--num_epochs",
|
| 67 |
+
type=int,
|
| 68 |
+
default=5,
|
| 69 |
+
help="Số epochs (mặc định: 5)",
|
| 70 |
+
)
|
| 71 |
+
parser.add_argument(
|
| 72 |
+
"--batch_size",
|
| 73 |
+
type=int,
|
| 74 |
+
default=64,
|
| 75 |
+
help="Batch size (mặc định: 64)",
|
| 76 |
+
)
|
| 77 |
+
parser.add_argument(
|
| 78 |
+
"--model_size",
|
| 79 |
+
type=str,
|
| 80 |
+
default="custom_25m",
|
| 81 |
+
choices=["custom_25m", "base", "small"],
|
| 82 |
+
help="Kích thước model (mặc định: custom_25m)",
|
| 83 |
+
)
|
| 84 |
+
parser.add_argument(
|
| 85 |
+
"--device",
|
| 86 |
+
type=str,
|
| 87 |
+
default="cuda",
|
| 88 |
+
help="Device (cuda/cpu, mặc định: cuda)",
|
| 89 |
+
)
|
| 90 |
+
parser.add_argument(
|
| 91 |
+
"--precision",
|
| 92 |
+
type=str,
|
| 93 |
+
default="bf16",
|
| 94 |
+
choices=["fp32", "amp", "bf16"],
|
| 95 |
+
help="Precision (mặc định: bf16)",
|
| 96 |
+
)
|
| 97 |
+
parser.add_argument(
|
| 98 |
+
"--grad_accum_steps",
|
| 99 |
+
type=int,
|
| 100 |
+
default=2,
|
| 101 |
+
help="Gradient accumulation steps (mặc định: 2)",
|
| 102 |
+
)
|
| 103 |
+
parser.add_argument(
|
| 104 |
+
"--warmup_steps",
|
| 105 |
+
type=int,
|
| 106 |
+
default=2000,
|
| 107 |
+
help="Warmup steps (mặc định: 2000 steps cho finetune)",
|
| 108 |
+
)
|
| 109 |
+
parser.add_argument(
|
| 110 |
+
"--target_max_lr",
|
| 111 |
+
type=float,
|
| 112 |
+
default=1e-5,
|
| 113 |
+
help="Đỉnh learning rate cho finetune (mặc định: 1e-5)",
|
| 114 |
+
)
|
| 115 |
+
parser.add_argument(
|
| 116 |
+
"--label_smoothing",
|
| 117 |
+
type=float,
|
| 118 |
+
default=0.1,
|
| 119 |
+
help="Label smoothing (mặc định: 0.1)",
|
| 120 |
+
)
|
| 121 |
+
parser.add_argument(
|
| 122 |
+
"--checkpoint_dir",
|
| 123 |
+
type=str,
|
| 124 |
+
default=None,
|
| 125 |
+
help="Checkpoint directory (mặc định: checkpoints)",
|
| 126 |
+
)
|
| 127 |
+
parser.add_argument(
|
| 128 |
+
"--no_compile",
|
| 129 |
+
action="store_true",
|
| 130 |
+
help="Tắt torch.compile (mặc định: BẬT nếu grad_accum_steps=1)",
|
| 131 |
+
)
|
| 132 |
+
parser.add_argument(
|
| 133 |
+
"--num_workers",
|
| 134 |
+
type=int,
|
| 135 |
+
default=None,
|
| 136 |
+
help="Số workers cho DataLoader (mặc định: 2 cho CUDA, 0 cho CPU)",
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
args = parser.parse_args()
|
| 140 |
+
|
| 141 |
+
# Device
|
| 142 |
+
device = torch.device(args.device)
|
| 143 |
+
if device.type == 'cuda' and not torch.cuda.is_available():
|
| 144 |
+
print("⚠️ CUDA không khả dụng, dùng CPU")
|
| 145 |
+
device = torch.device('cpu')
|
| 146 |
+
|
| 147 |
+
print("="*70)
|
| 148 |
+
print("FINETUNE MODEL VỚI HOSPITAL DATASET")
|
| 149 |
+
print("="*70)
|
| 150 |
+
print(f"Device: {device}")
|
| 151 |
+
print(f"Model size: {args.model_size}")
|
| 152 |
+
print(f"Checkpoint: {args.checkpoint}")
|
| 153 |
+
|
| 154 |
+
# Kiểm tra checkpoint
|
| 155 |
+
checkpoint_path = Path(args.checkpoint)
|
| 156 |
+
if not checkpoint_path.exists():
|
| 157 |
+
raise FileNotFoundError(
|
| 158 |
+
f"Không tìm thấy checkpoint: {checkpoint_path}\n"
|
| 159 |
+
f"Vui lòng chỉ định checkpoint hợp lệ"
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
# Kiểm tra GPU memory và điều chỉnh batch size nếu cần
|
| 163 |
+
if device.type == 'cuda':
|
| 164 |
+
clear_gpu_cache()
|
| 165 |
+
mem_info = get_gpu_memory_info()
|
| 166 |
+
if mem_info:
|
| 167 |
+
print(f"\n📊 GPU Memory Info:")
|
| 168 |
+
print(f" Total: {mem_info['total']:.2f} GB")
|
| 169 |
+
print(f" Free: {mem_info['free']:.2f} GB")
|
| 170 |
+
print(f" Usage: {mem_info['usage_percent']:.1f}%")
|
| 171 |
+
|
| 172 |
+
# Kiểm tra và điều chỉnh batch size
|
| 173 |
+
safe_batch_size, warning = check_and_adjust_batch_size(args.batch_size)
|
| 174 |
+
if warning:
|
| 175 |
+
print(f"\n{warning}")
|
| 176 |
+
response = input(f"\n❓ Có muốn giảm batch_size xuống {safe_batch_size} không? (y/n, mặc định: n): ").strip().lower()
|
| 177 |
+
if response == 'y':
|
| 178 |
+
args.batch_size = safe_batch_size
|
| 179 |
+
print(f"✓ Đã giảm batch_size xuống {safe_batch_size}")
|
| 180 |
+
else:
|
| 181 |
+
print(f"⚠️ Giữ nguyên batch_size={args.batch_size}, có thể gặp OOM")
|
| 182 |
+
else:
|
| 183 |
+
print(f"✓ Batch size {args.batch_size} an toàn với GPU memory hiện tại")
|
| 184 |
+
|
| 185 |
+
print(f"\nBatch size: {args.batch_size}")
|
| 186 |
+
print(f"Gradient accumulation: {args.grad_accum_steps}")
|
| 187 |
+
effective_batch = args.batch_size * args.grad_accum_steps
|
| 188 |
+
print(f"Effective batch size: {effective_batch} ({args.batch_size} × {args.grad_accum_steps})")
|
| 189 |
+
print(f"Epochs: {args.num_epochs}")
|
| 190 |
+
print(f"Precision: {args.precision}")
|
| 191 |
+
print(f"Learning rate: {args.target_max_lr} (finetune - thấp hơn train từ đầu)")
|
| 192 |
+
print(f"Warmup steps: {args.warmup_steps}")
|
| 193 |
+
print()
|
| 194 |
+
print("🔄 FINETUNE SETUP:")
|
| 195 |
+
print(" - Dataset: 994k Hospital (đã làm sạch) + 400k old data (bidirectional)")
|
| 196 |
+
print(" - Dữ liệu đã được làm sạch: loại bỏ rác, lặp từ, đảm bảo alignment")
|
| 197 |
+
print(" - Learning rate thấp để tránh quên kiến thức cũ")
|
| 198 |
+
print(" - Warmup để ổn định training")
|
| 199 |
+
print(" - Label smoothing để tránh overfitting")
|
| 200 |
+
print("="*70 + "\n")
|
| 201 |
+
|
| 202 |
+
# Load vocab info
|
| 203 |
+
print("📚 Loading vocabulary...")
|
| 204 |
+
vocab_info = load_shared_vocab_info()
|
| 205 |
+
vocab_size = vocab_info['vocab_size']
|
| 206 |
+
pad_idx = vocab_info['pad_id']
|
| 207 |
+
print(f"✓ Vocab size: {vocab_size:,}")
|
| 208 |
+
print(f"✓ Pad ID: {pad_idx}\n")
|
| 209 |
+
|
| 210 |
+
# Load data
|
| 211 |
+
print("📂 Loading Hospital finetune data...")
|
| 212 |
+
processed_data = load_hospital_finetune_data()
|
| 213 |
+
|
| 214 |
+
print(f"✓ Train: {len(processed_data['train']['src']):,} cặp câu")
|
| 215 |
+
print(f"✓ Validation: {len(processed_data['validation']['src']):,} cặp câu")
|
| 216 |
+
print(f"✓ Test: {len(processed_data['test']['src']):,} cặp câu\n")
|
| 217 |
+
|
| 218 |
+
# Create dataloaders
|
| 219 |
+
print("🔄 Creating dataloaders...")
|
| 220 |
+
if args.num_workers is None:
|
| 221 |
+
num_workers = 2 if device.type == 'cuda' else 0
|
| 222 |
+
else:
|
| 223 |
+
num_workers = args.num_workers
|
| 224 |
+
train_loader, val_loader, test_loader = create_dataloaders_with_bucketing(
|
| 225 |
+
processed_data,
|
| 226 |
+
batch_size=args.batch_size,
|
| 227 |
+
num_workers=num_workers,
|
| 228 |
+
)
|
| 229 |
+
print(f"✓ Dataloaders created với BucketSampler (num_workers={num_workers})\n")
|
| 230 |
+
|
| 231 |
+
# Create model
|
| 232 |
+
print("🔨 Creating model...")
|
| 233 |
+
model_config = get_model_config(args.model_size)
|
| 234 |
+
|
| 235 |
+
model = TransformerShared(
|
| 236 |
+
vocab_size=vocab_size,
|
| 237 |
+
d_model=model_config['d_model'],
|
| 238 |
+
n_layers=model_config['n_layers'],
|
| 239 |
+
n_heads=model_config['n_heads'],
|
| 240 |
+
d_ff=model_config['d_ff'],
|
| 241 |
+
dropout=model_config['dropout'],
|
| 242 |
+
pad_idx=pad_idx,
|
| 243 |
+
use_weight_tying=True
|
| 244 |
+
)
|
| 245 |
+
|
| 246 |
+
model = model.to(device)
|
| 247 |
+
|
| 248 |
+
# Load checkpoint
|
| 249 |
+
print(f"📥 Loading checkpoint từ: {checkpoint_path}")
|
| 250 |
+
try:
|
| 251 |
+
model, checkpoint = load_checkpoint(model, str(checkpoint_path), device)
|
| 252 |
+
print(f"✓ Đã load checkpoint từ epoch {checkpoint.get('epoch', 'unknown')}")
|
| 253 |
+
print(f"✓ Best validation loss: {checkpoint.get('best_val_loss', checkpoint.get('val_loss', 'unknown'))}")
|
| 254 |
+
except Exception as e:
|
| 255 |
+
print(f"⚠️ Không thể load checkpoint: {e}")
|
| 256 |
+
print(" Sẽ train từ đầu")
|
| 257 |
+
|
| 258 |
+
# Tối ưu: Compile model với torch.compile (PyTorch 2.0+)
|
| 259 |
+
compile_model = not args.no_compile and args.grad_accum_steps == 1
|
| 260 |
+
if compile_model and hasattr(torch, 'compile') and device.type == 'cuda':
|
| 261 |
+
print("🔧 Compiling model với torch.compile để tăng tốc...")
|
| 262 |
+
try:
|
| 263 |
+
model = torch.compile(model, mode='default')
|
| 264 |
+
print("✓ Model đã được compile\n")
|
| 265 |
+
except Exception as e:
|
| 266 |
+
print(f"⚠️ Không thể compile model: {e}\n")
|
| 267 |
+
else:
|
| 268 |
+
if args.grad_accum_steps > 1:
|
| 269 |
+
print("⚠️ torch.compile đã tắt (không tương thích với gradient accumulation)\n")
|
| 270 |
+
elif args.no_compile:
|
| 271 |
+
print("⚠️ torch.compile đã tắt (--no_compile)\n")
|
| 272 |
+
|
| 273 |
+
# Count parameters
|
| 274 |
+
total_params = sum(p.numel() for p in model.parameters())
|
| 275 |
+
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
|
| 276 |
+
model_size_mb = total_params * 4 / (1024**2)
|
| 277 |
+
print(f"✓ Model created")
|
| 278 |
+
print(f" - Total parameters: {total_params:,}")
|
| 279 |
+
print(f" - Trainable parameters: {trainable_params:,}")
|
| 280 |
+
print(f" - Model size: ~{model_size_mb:.2f} MB (float32)")
|
| 281 |
+
|
| 282 |
+
# Kiểm tra lại GPU memory sau khi load model
|
| 283 |
+
if device.type == 'cuda':
|
| 284 |
+
clear_gpu_cache()
|
| 285 |
+
mem_info = get_gpu_memory_info()
|
| 286 |
+
if mem_info:
|
| 287 |
+
print(f"\n📊 GPU Memory sau khi load model:")
|
| 288 |
+
print(f" Allocated: {mem_info['allocated']:.2f} GB")
|
| 289 |
+
print(f" Reserved: {mem_info['reserved']:.2f} GB")
|
| 290 |
+
print(f" Free: {mem_info['free']:.2f} GB")
|
| 291 |
+
print(f" Usage: {mem_info['usage_percent']:.1f}%")
|
| 292 |
+
|
| 293 |
+
print()
|
| 294 |
+
|
| 295 |
+
# Checkpoint directory - Lưu vào thư mục riêng để không ghi đè checkpoint cũ
|
| 296 |
+
if args.checkpoint_dir:
|
| 297 |
+
checkpoint_dir = Path(args.checkpoint_dir)
|
| 298 |
+
else:
|
| 299 |
+
# Tự động tạo thư mục riêng cho finetune
|
| 300 |
+
checkpoint_dir = CHECKPOINT_DIR / 'finetune_hospital'
|
| 301 |
+
checkpoint_dir.mkdir(parents=True, exist_ok=True)
|
| 302 |
+
print(f"📁 Checkpoint sẽ được lưu tại: {checkpoint_dir}")
|
| 303 |
+
print(f" (Không ghi đè checkpoint cũ trong {CHECKPOINT_DIR})\n")
|
| 304 |
+
|
| 305 |
+
# Clear cache trước khi training
|
| 306 |
+
if device.type == 'cuda':
|
| 307 |
+
clear_gpu_cache()
|
| 308 |
+
print("✓ Đã clear GPU cache trước khi training\n")
|
| 309 |
+
|
| 310 |
+
# Train
|
| 311 |
+
print("🚀 Starting finetune...")
|
| 312 |
+
print("="*70 + "\n")
|
| 313 |
+
|
| 314 |
+
history = train_model(
|
| 315 |
+
model=model,
|
| 316 |
+
train_loader=train_loader,
|
| 317 |
+
val_loader=val_loader,
|
| 318 |
+
num_epochs=args.num_epochs,
|
| 319 |
+
device=device,
|
| 320 |
+
d_model=model_config["d_model"],
|
| 321 |
+
warmup_steps=args.warmup_steps,
|
| 322 |
+
label_smoothing=args.label_smoothing,
|
| 323 |
+
grad_accum_steps=args.grad_accum_steps,
|
| 324 |
+
precision=args.precision,
|
| 325 |
+
checkpoint_dir=checkpoint_dir,
|
| 326 |
+
save_every=1,
|
| 327 |
+
resume_from=None, # Không resume khi finetune
|
| 328 |
+
target_max_lr=args.target_max_lr,
|
| 329 |
+
)
|
| 330 |
+
|
| 331 |
+
print("\n" + "="*70)
|
| 332 |
+
print("✅ FINETUNE HOÀN TẤT!")
|
| 333 |
+
print("="*70)
|
| 334 |
+
print(f"Checkpoints được lưu tại: {checkpoint_dir}")
|
| 335 |
+
print(f"Best model: {checkpoint_dir / 'best_model.pt'}")
|
| 336 |
+
print(f"\n💡 Lưu ý: Checkpoint cũ vẫn an toàn tại {CHECKPOINT_DIR}")
|
| 337 |
+
print(f" - Model gốc: {CHECKPOINT_DIR / 'best_model.pt'}")
|
| 338 |
+
print(f" - Model finetune: {checkpoint_dir / 'best_model.pt'}")
|
| 339 |
+
print("="*70)
|
| 340 |
+
|
| 341 |
+
if __name__ == '__main__':
|
| 342 |
+
main()
|
| 343 |
+
|
src/gpu_safety.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Cơ chế an toàn GPU: Tự động điều chỉnh batch size để tránh OOM
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
import gc
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def get_gpu_memory_info():
|
| 10 |
+
"""Lấy thông tin GPU memory"""
|
| 11 |
+
if not torch.cuda.is_available():
|
| 12 |
+
return None
|
| 13 |
+
|
| 14 |
+
memory_allocated = torch.cuda.memory_allocated() / (1024**3) # GB
|
| 15 |
+
memory_reserved = torch.cuda.memory_reserved() / (1024**3) # GB
|
| 16 |
+
memory_total = torch.cuda.get_device_properties(0).total_memory / (1024**3) # GB
|
| 17 |
+
memory_free = memory_total - memory_reserved
|
| 18 |
+
|
| 19 |
+
return {
|
| 20 |
+
'allocated': memory_allocated,
|
| 21 |
+
'reserved': memory_reserved,
|
| 22 |
+
'total': memory_total,
|
| 23 |
+
'free': memory_free,
|
| 24 |
+
'usage_percent': (memory_reserved / memory_total) * 100
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def calculate_safe_batch_size(current_batch_size, model_size_mb, vocab_size,
|
| 29 |
+
max_seq_len=150, safety_margin=0.15):
|
| 30 |
+
"""
|
| 31 |
+
Tính toán batch size an toàn dựa trên GPU memory còn lại
|
| 32 |
+
|
| 33 |
+
Args:
|
| 34 |
+
current_batch_size: Batch size hiện tại
|
| 35 |
+
model_size_mb: Kích thước model (MB)
|
| 36 |
+
vocab_size: Kích thước vocabulary
|
| 37 |
+
max_seq_len: Độ dài sequence tối đa
|
| 38 |
+
safety_margin: Margin an toàn (15% mặc định)
|
| 39 |
+
|
| 40 |
+
Returns:
|
| 41 |
+
safe_batch_size: Batch size an toàn
|
| 42 |
+
"""
|
| 43 |
+
if not torch.cuda.is_available():
|
| 44 |
+
return current_batch_size
|
| 45 |
+
|
| 46 |
+
mem_info = get_gpu_memory_info()
|
| 47 |
+
if mem_info is None:
|
| 48 |
+
return current_batch_size
|
| 49 |
+
|
| 50 |
+
# Ước tính memory cần cho một batch
|
| 51 |
+
# Memory cho model: model_size_mb
|
| 52 |
+
# Memory cho activations: batch_size * seq_len * hidden_size * 4 bytes (float32)
|
| 53 |
+
# Memory cho gradients: tương tự activations
|
| 54 |
+
# Memory cho optimizer states: ~2x model size (Adam)
|
| 55 |
+
|
| 56 |
+
# Ước tính đơn giản: mỗi batch cần ~50-100MB tùy vào seq_len
|
| 57 |
+
estimated_memory_per_batch_mb = 80 # Conservative estimate
|
| 58 |
+
|
| 59 |
+
# Memory còn lại có thể dùng (trừ safety margin)
|
| 60 |
+
available_memory_gb = mem_info['free'] * (1 - safety_margin)
|
| 61 |
+
available_memory_mb = available_memory_gb * 1024
|
| 62 |
+
|
| 63 |
+
# Trừ memory cho model và optimizer
|
| 64 |
+
model_memory_mb = model_size_mb * 3 # Model + gradients + optimizer states
|
| 65 |
+
usable_memory_mb = available_memory_mb - model_memory_mb
|
| 66 |
+
|
| 67 |
+
if usable_memory_mb < 0:
|
| 68 |
+
# Không đủ memory, giảm batch size xuống tối thiểu
|
| 69 |
+
return max(1, current_batch_size // 4)
|
| 70 |
+
|
| 71 |
+
# Tính batch size an toàn
|
| 72 |
+
safe_batch_size = int(usable_memory_mb / estimated_memory_per_batch_mb)
|
| 73 |
+
|
| 74 |
+
# Giới hạn trong khoảng hợp lý
|
| 75 |
+
safe_batch_size = max(1, min(safe_batch_size, current_batch_size * 2))
|
| 76 |
+
|
| 77 |
+
return safe_batch_size
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def check_and_adjust_batch_size(current_batch_size, threshold=0.85):
|
| 81 |
+
"""
|
| 82 |
+
Kiểm tra GPU memory và đề xuất batch size mới nếu cần
|
| 83 |
+
|
| 84 |
+
Args:
|
| 85 |
+
current_batch_size: Batch size hiện tại
|
| 86 |
+
threshold: Ngưỡng cảnh báo (85% mặc định)
|
| 87 |
+
|
| 88 |
+
Returns:
|
| 89 |
+
new_batch_size: Batch size đề xuất (có thể giống hoặc nhỏ hơn)
|
| 90 |
+
warning: Cảnh báo nếu cần
|
| 91 |
+
"""
|
| 92 |
+
if not torch.cuda.is_available():
|
| 93 |
+
return current_batch_size, None
|
| 94 |
+
|
| 95 |
+
mem_info = get_gpu_memory_info()
|
| 96 |
+
if mem_info is None:
|
| 97 |
+
return current_batch_size, None
|
| 98 |
+
|
| 99 |
+
usage_percent = mem_info['usage_percent']
|
| 100 |
+
warning = None
|
| 101 |
+
|
| 102 |
+
if usage_percent > threshold * 100:
|
| 103 |
+
# GPU memory gần hết, đề xuất giảm batch size
|
| 104 |
+
reduction_factor = 0.75 # Giảm 25%
|
| 105 |
+
new_batch_size = max(1, int(current_batch_size * reduction_factor))
|
| 106 |
+
warning = (
|
| 107 |
+
f"⚠️ GPU memory usage: {usage_percent:.1f}% "
|
| 108 |
+
f"(Free: {mem_info['free']:.2f}GB / Total: {mem_info['total']:.2f}GB)\n"
|
| 109 |
+
f" Đề xuất giảm batch_size từ {current_batch_size} xuống {new_batch_size}"
|
| 110 |
+
)
|
| 111 |
+
return new_batch_size, warning
|
| 112 |
+
elif usage_percent > 0.7 * 100:
|
| 113 |
+
# Cảnh báo sớm
|
| 114 |
+
warning = (
|
| 115 |
+
f"⚠️ GPU memory usage: {usage_percent:.1f}% "
|
| 116 |
+
f"(Free: {mem_info['free']:.2f}GB / Total: {mem_info['total']:.2f}GB)\n"
|
| 117 |
+
f" Nên theo dõi, có thể cần giảm batch_size nếu tiếp tục tăng"
|
| 118 |
+
)
|
| 119 |
+
return current_batch_size, warning
|
| 120 |
+
|
| 121 |
+
return current_batch_size, None
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def clear_gpu_cache():
|
| 125 |
+
"""Xóa GPU cache để giải phóng memory"""
|
| 126 |
+
if torch.cuda.is_available():
|
| 127 |
+
torch.cuda.empty_cache()
|
| 128 |
+
gc.collect()
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def monitor_gpu_memory(interval=100):
|
| 132 |
+
"""
|
| 133 |
+
Decorator để monitor GPU memory trong training loop
|
| 134 |
+
|
| 135 |
+
Args:
|
| 136 |
+
interval: Kiểm tra mỗi N batches
|
| 137 |
+
"""
|
| 138 |
+
def decorator(func):
|
| 139 |
+
def wrapper(*args, **kwargs):
|
| 140 |
+
# Chạy function và monitor
|
| 141 |
+
result = func(*args, **kwargs)
|
| 142 |
+
|
| 143 |
+
# Kiểm tra memory sau mỗi interval batches
|
| 144 |
+
if hasattr(wrapper, 'batch_count'):
|
| 145 |
+
wrapper.batch_count += 1
|
| 146 |
+
else:
|
| 147 |
+
wrapper.batch_count = 1
|
| 148 |
+
|
| 149 |
+
if wrapper.batch_count % interval == 0:
|
| 150 |
+
mem_info = get_gpu_memory_info()
|
| 151 |
+
if mem_info:
|
| 152 |
+
print(f"[GPU Monitor] Memory: {mem_info['usage_percent']:.1f}% "
|
| 153 |
+
f"({mem_info['free']:.2f}GB free)")
|
| 154 |
+
|
| 155 |
+
return result
|
| 156 |
+
return wrapper
|
| 157 |
+
return decorator
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def handle_oom_error(current_batch_size, reduction_factor=0.75, min_batch_size=8):
|
| 161 |
+
"""
|
| 162 |
+
Xử lý OOM error bằng cách giảm batch size
|
| 163 |
+
|
| 164 |
+
Args:
|
| 165 |
+
current_batch_size: Batch size hiện tại
|
| 166 |
+
reduction_factor: Hệ số giảm (mặc định: 0.75 = giảm 25%)
|
| 167 |
+
min_batch_size: Batch size tối thiểu
|
| 168 |
+
|
| 169 |
+
Returns:
|
| 170 |
+
new_batch_size: Batch size mới sau khi giảm
|
| 171 |
+
"""
|
| 172 |
+
new_batch_size = max(min_batch_size, int(current_batch_size * reduction_factor))
|
| 173 |
+
|
| 174 |
+
# Xóa cache
|
| 175 |
+
clear_gpu_cache()
|
| 176 |
+
|
| 177 |
+
return new_batch_size
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def check_memory_spike(threshold=0.9):
|
| 181 |
+
"""
|
| 182 |
+
Kiểm tra xem GPU memory có đang spike không
|
| 183 |
+
|
| 184 |
+
Args:
|
| 185 |
+
threshold: Ngưỡng cảnh báo (90% mặc định)
|
| 186 |
+
|
| 187 |
+
Returns:
|
| 188 |
+
is_spike: True nếu memory > threshold
|
| 189 |
+
mem_info: Thông tin memory
|
| 190 |
+
"""
|
| 191 |
+
mem_info = get_gpu_memory_info()
|
| 192 |
+
if mem_info is None:
|
| 193 |
+
return False, None
|
| 194 |
+
|
| 195 |
+
is_spike = mem_info['usage_percent'] > threshold * 100
|
| 196 |
+
return is_spike, mem_info
|
| 197 |
+
|
src/inference_evaluation.py
ADDED
|
@@ -0,0 +1,563 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
INFERENCE & EVALUATION
|
| 3 |
+
Greedy Search, Beam Search, BLEU Score evaluation
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
import torch.nn.functional as F
|
| 8 |
+
from collections import Counter
|
| 9 |
+
import math
|
| 10 |
+
from tqdm import tqdm
|
| 11 |
+
|
| 12 |
+
# ============================================================================
|
| 13 |
+
# 1. GREEDY SEARCH DECODING
|
| 14 |
+
# ============================================================================
|
| 15 |
+
|
| 16 |
+
def greedy_decode(model, src, src_vocab, tgt_vocab, device, max_len=100, repetition_penalty=1.5, no_repeat_ngram_size=3):
|
| 17 |
+
"""
|
| 18 |
+
Greedy Decoding - Chọn token có xác suất cao nhất mỗi bước với Repetition Penalty
|
| 19 |
+
|
| 20 |
+
Args:
|
| 21 |
+
model: Transformer model
|
| 22 |
+
src: Source sequence [1, src_len] hoặc list of tokens
|
| 23 |
+
src_vocab: Source vocabulary
|
| 24 |
+
tgt_vocab: Target vocabulary
|
| 25 |
+
device: Device
|
| 26 |
+
max_len: Maximum length để generate
|
| 27 |
+
repetition_penalty: Penalty factor cho tokens lặp lại (>1.0 để giảm repetition)
|
| 28 |
+
no_repeat_ngram_size: Kích thước n-gram để tránh lặp lại (0 = tắt)
|
| 29 |
+
|
| 30 |
+
Returns:
|
| 31 |
+
decoded_tokens: List of decoded token indices
|
| 32 |
+
decoded_sentence: Decoded sentence (string)
|
| 33 |
+
"""
|
| 34 |
+
model.eval()
|
| 35 |
+
|
| 36 |
+
# Nếu src là list, convert sang tensor
|
| 37 |
+
if isinstance(src, list):
|
| 38 |
+
src = torch.LongTensor([src]).to(device)
|
| 39 |
+
elif src.dim() == 1:
|
| 40 |
+
src = src.unsqueeze(0)
|
| 41 |
+
|
| 42 |
+
with torch.no_grad():
|
| 43 |
+
# Encode source
|
| 44 |
+
encoder_output, src_mask = model.encode(src)
|
| 45 |
+
|
| 46 |
+
# Khởi tạo target với <sos> token
|
| 47 |
+
tgt_tokens = [tgt_vocab.SOS_IDX]
|
| 48 |
+
|
| 49 |
+
for step in range(max_len):
|
| 50 |
+
# Tạo target tensor
|
| 51 |
+
tgt = torch.LongTensor([tgt_tokens]).to(device)
|
| 52 |
+
|
| 53 |
+
# Decode
|
| 54 |
+
output = model.decode(tgt, encoder_output, src_mask)
|
| 55 |
+
|
| 56 |
+
# Lấy prediction cho token cuối cùng
|
| 57 |
+
next_token_logits = output[0, -1, :]
|
| 58 |
+
|
| 59 |
+
# Áp dụng repetition penalty cho từng token
|
| 60 |
+
token_counts = {}
|
| 61 |
+
for token_id in tgt_tokens:
|
| 62 |
+
if token_id not in [tgt_vocab.SOS_IDX, tgt_vocab.EOS_IDX, tgt_vocab.PAD_IDX]:
|
| 63 |
+
token_counts[token_id] = token_counts.get(token_id, 0) + 1
|
| 64 |
+
|
| 65 |
+
# Áp dụng penalty: giảm logit của các tokens đã xuất hiện
|
| 66 |
+
for token_id, count in token_counts.items():
|
| 67 |
+
if count > 0 and token_id < len(next_token_logits):
|
| 68 |
+
# Penalty tăng theo số lần lặp lại
|
| 69 |
+
penalty = repetition_penalty ** count
|
| 70 |
+
next_token_logits[token_id] = next_token_logits[token_id] / penalty
|
| 71 |
+
|
| 72 |
+
# N-gram repetition penalty: tránh lặp cụm từ
|
| 73 |
+
if no_repeat_ngram_size > 0 and len(tgt_tokens) >= no_repeat_ngram_size:
|
| 74 |
+
# Lấy n-gram cuối cùng
|
| 75 |
+
last_ngram = tuple(tgt_tokens[-(no_repeat_ngram_size-1):])
|
| 76 |
+
|
| 77 |
+
# Kiểm tra xem n-gram này đã xuất hiện trước đó chưa
|
| 78 |
+
for i in range(len(tgt_tokens) - no_repeat_ngram_size + 1):
|
| 79 |
+
ngram = tuple(tgt_tokens[i:i+no_repeat_ngram_size-1])
|
| 80 |
+
if ngram == last_ngram:
|
| 81 |
+
# Nếu n-gram đã xuất hiện, giảm logit của token tiếp theo
|
| 82 |
+
if i + no_repeat_ngram_size - 1 < len(tgt_tokens):
|
| 83 |
+
repeated_token = tgt_tokens[i + no_repeat_ngram_size - 1]
|
| 84 |
+
if repeated_token < len(next_token_logits):
|
| 85 |
+
next_token_logits[repeated_token] = next_token_logits[repeated_token] / (repetition_penalty ** 2)
|
| 86 |
+
|
| 87 |
+
# Lấy token có xác suất cao nhất
|
| 88 |
+
next_token = next_token_logits.argmax().item()
|
| 89 |
+
|
| 90 |
+
# Thêm vào sequence
|
| 91 |
+
tgt_tokens.append(next_token)
|
| 92 |
+
|
| 93 |
+
# Dừng nếu gặp <eos>
|
| 94 |
+
if next_token == tgt_vocab.EOS_IDX:
|
| 95 |
+
break
|
| 96 |
+
|
| 97 |
+
# Decode thành sentence
|
| 98 |
+
decoded_sentence = tgt_vocab.decode(tgt_tokens)
|
| 99 |
+
|
| 100 |
+
return tgt_tokens, decoded_sentence
|
| 101 |
+
|
| 102 |
+
# ============================================================================
|
| 103 |
+
# 2. BEAM SEARCH DECODING
|
| 104 |
+
# ============================================================================
|
| 105 |
+
|
| 106 |
+
class BeamSearchNode:
|
| 107 |
+
"""
|
| 108 |
+
Node trong Beam Search
|
| 109 |
+
"""
|
| 110 |
+
def __init__(self, tokens, log_prob, length):
|
| 111 |
+
self.tokens = tokens
|
| 112 |
+
self.log_prob = log_prob
|
| 113 |
+
self.length = length
|
| 114 |
+
|
| 115 |
+
def eval(self, alpha=0.6):
|
| 116 |
+
"""
|
| 117 |
+
Tính score với length normalization
|
| 118 |
+
|
| 119 |
+
Score = log_prob / (length^alpha)
|
| 120 |
+
|
| 121 |
+
Args:
|
| 122 |
+
alpha: Length penalty factor
|
| 123 |
+
"""
|
| 124 |
+
return self.log_prob / (self.length ** alpha)
|
| 125 |
+
|
| 126 |
+
def beam_search_decode(model, src, src_vocab, tgt_vocab, device, beam_size=5, max_len=100, alpha=0.6, repetition_penalty=1.5, no_repeat_ngram_size=3):
|
| 127 |
+
"""
|
| 128 |
+
Beam Search Decoding - Giữ top-k candidates tốt nhất với Repetition Penalty
|
| 129 |
+
|
| 130 |
+
Args:
|
| 131 |
+
model: Transformer model
|
| 132 |
+
src: Source sequence
|
| 133 |
+
src_vocab: Source vocabulary
|
| 134 |
+
tgt_vocab: Target vocabulary
|
| 135 |
+
device: Device
|
| 136 |
+
beam_size: Beam size (số lượng candidates)
|
| 137 |
+
max_len: Maximum length
|
| 138 |
+
alpha: Length penalty factor
|
| 139 |
+
repetition_penalty: Penalty factor cho tokens lặp lại (>1.0 để giảm repetition)
|
| 140 |
+
no_repeat_ngram_size: Kích thước n-gram để tránh lặp lại (0 = tắt)
|
| 141 |
+
|
| 142 |
+
Returns:
|
| 143 |
+
best_tokens: List of best token indices
|
| 144 |
+
best_sentence: Best decoded sentence
|
| 145 |
+
"""
|
| 146 |
+
model.eval()
|
| 147 |
+
|
| 148 |
+
# Nếu src là list, convert sang tensor
|
| 149 |
+
if isinstance(src, list):
|
| 150 |
+
src = torch.LongTensor([src]).to(device)
|
| 151 |
+
elif src.dim() == 1:
|
| 152 |
+
src = src.unsqueeze(0)
|
| 153 |
+
|
| 154 |
+
with torch.no_grad():
|
| 155 |
+
# Encode source
|
| 156 |
+
encoder_output, src_mask = model.encode(src)
|
| 157 |
+
|
| 158 |
+
# Khởi tạo beam với <sos> token
|
| 159 |
+
beams = [BeamSearchNode(
|
| 160 |
+
tokens=[tgt_vocab.SOS_IDX],
|
| 161 |
+
log_prob=0.0,
|
| 162 |
+
length=1
|
| 163 |
+
)]
|
| 164 |
+
|
| 165 |
+
completed_beams = []
|
| 166 |
+
|
| 167 |
+
for step in range(max_len):
|
| 168 |
+
candidates = []
|
| 169 |
+
|
| 170 |
+
for beam in beams:
|
| 171 |
+
# Nếu đã kết thúc, thêm vào completed
|
| 172 |
+
if beam.tokens[-1] == tgt_vocab.EOS_IDX:
|
| 173 |
+
completed_beams.append(beam)
|
| 174 |
+
continue
|
| 175 |
+
|
| 176 |
+
# Tạo target tensor
|
| 177 |
+
tgt = torch.LongTensor([beam.tokens]).to(device)
|
| 178 |
+
|
| 179 |
+
# Decode
|
| 180 |
+
output = model.decode(tgt, encoder_output, src_mask)
|
| 181 |
+
|
| 182 |
+
# Lấy log probabilities cho token cuối
|
| 183 |
+
next_token_logits = output[0, -1, :]
|
| 184 |
+
|
| 185 |
+
# Áp dụng repetition penalty
|
| 186 |
+
# Đếm số lần xuất hiện của mỗi token trong sequence hiện tại
|
| 187 |
+
token_counts = {}
|
| 188 |
+
for token_id in beam.tokens:
|
| 189 |
+
if token_id not in [tgt_vocab.SOS_IDX, tgt_vocab.EOS_IDX, tgt_vocab.PAD_IDX]:
|
| 190 |
+
token_counts[token_id] = token_counts.get(token_id, 0) + 1
|
| 191 |
+
|
| 192 |
+
# Áp dụng penalty: giảm logit của các tokens đã xuất hiện (mạnh hơn)
|
| 193 |
+
for token_id, count in token_counts.items():
|
| 194 |
+
if count > 0 and token_id < len(next_token_logits):
|
| 195 |
+
# Penalty tăng mạnh theo số lần lặp lại
|
| 196 |
+
# count=1: penalty nhẹ, count=2+: penalty rất mạnh
|
| 197 |
+
penalty = repetition_penalty ** (count * 1.5) # Tăng penalty mạnh hơn
|
| 198 |
+
next_token_logits[token_id] = next_token_logits[token_id] / penalty
|
| 199 |
+
|
| 200 |
+
# N-gram repetition penalty: tránh lặp cụm từ
|
| 201 |
+
if no_repeat_ngram_size > 0 and len(beam.tokens) >= no_repeat_ngram_size:
|
| 202 |
+
# Lấy n-gram cuối cùng
|
| 203 |
+
last_ngram = tuple(beam.tokens[-(no_repeat_ngram_size-1):])
|
| 204 |
+
|
| 205 |
+
# Kiểm tra xem n-gram này đã xuất hiện trước đó chưa
|
| 206 |
+
for i in range(len(beam.tokens) - no_repeat_ngram_size + 1):
|
| 207 |
+
ngram = tuple(beam.tokens[i:i+no_repeat_ngram_size-1])
|
| 208 |
+
if ngram == last_ngram:
|
| 209 |
+
# Nếu n-gram đã xuất hiện, giảm logit của token tiếp theo rất mạnh
|
| 210 |
+
if i + no_repeat_ngram_size - 1 < len(beam.tokens):
|
| 211 |
+
repeated_token = beam.tokens[i + no_repeat_ngram_size - 1]
|
| 212 |
+
if repeated_token < len(next_token_logits):
|
| 213 |
+
# Penalty rất mạnh cho n-gram repetition
|
| 214 |
+
next_token_logits[repeated_token] = next_token_logits[repeated_token] / (repetition_penalty ** 3)
|
| 215 |
+
|
| 216 |
+
log_probs = F.log_softmax(next_token_logits, dim=-1)
|
| 217 |
+
|
| 218 |
+
# Lấy top-k tokens
|
| 219 |
+
top_log_probs, top_tokens = torch.topk(log_probs, beam_size)
|
| 220 |
+
|
| 221 |
+
# Tạo candidates mới
|
| 222 |
+
for log_prob, token in zip(top_log_probs, top_tokens):
|
| 223 |
+
new_beam = BeamSearchNode(
|
| 224 |
+
tokens=beam.tokens + [token.item()],
|
| 225 |
+
log_prob=beam.log_prob + log_prob.item(),
|
| 226 |
+
length=beam.length + 1
|
| 227 |
+
)
|
| 228 |
+
candidates.append(new_beam)
|
| 229 |
+
|
| 230 |
+
# Nếu không còn candidates, dừng
|
| 231 |
+
if not candidates:
|
| 232 |
+
break
|
| 233 |
+
|
| 234 |
+
# Chọn top beam_size candidates tốt nhất
|
| 235 |
+
beams = sorted(candidates, key=lambda x: x.eval(alpha), reverse=True)[:beam_size]
|
| 236 |
+
|
| 237 |
+
# Nếu tất cả beams đã complete, dừng
|
| 238 |
+
if len(completed_beams) >= beam_size:
|
| 239 |
+
break
|
| 240 |
+
|
| 241 |
+
# Thêm các beams chưa complete vào completed
|
| 242 |
+
completed_beams.extend(beams)
|
| 243 |
+
|
| 244 |
+
# Chọn beam tốt nhất
|
| 245 |
+
best_beam = max(completed_beams, key=lambda x: x.eval(alpha))
|
| 246 |
+
|
| 247 |
+
# Decode thành sentence
|
| 248 |
+
best_sentence = tgt_vocab.decode(best_beam.tokens)
|
| 249 |
+
|
| 250 |
+
return best_beam.tokens, best_sentence
|
| 251 |
+
|
| 252 |
+
# ============================================================================
|
| 253 |
+
# 3. BLEU SCORE CALCULATION
|
| 254 |
+
# ============================================================================
|
| 255 |
+
|
| 256 |
+
def calculate_ngrams(tokens, n):
|
| 257 |
+
"""
|
| 258 |
+
Tính n-grams từ list of tokens
|
| 259 |
+
|
| 260 |
+
Args:
|
| 261 |
+
tokens: List of tokens
|
| 262 |
+
n: n-gram size
|
| 263 |
+
|
| 264 |
+
Returns:
|
| 265 |
+
ngrams: Counter of n-grams
|
| 266 |
+
"""
|
| 267 |
+
ngrams = []
|
| 268 |
+
for i in range(len(tokens) - n + 1):
|
| 269 |
+
ngram = tuple(tokens[i:i+n])
|
| 270 |
+
ngrams.append(ngram)
|
| 271 |
+
return Counter(ngrams)
|
| 272 |
+
|
| 273 |
+
def calculate_bleu_score(references, hypotheses, max_n=4, weights=None):
|
| 274 |
+
"""
|
| 275 |
+
Tính BLEU score
|
| 276 |
+
|
| 277 |
+
BLEU = BP * exp(sum(w_n * log(p_n)))
|
| 278 |
+
|
| 279 |
+
Args:
|
| 280 |
+
references: List of reference sentences (list of token lists)
|
| 281 |
+
hypotheses: List of hypothesis sentences (list of token lists)
|
| 282 |
+
max_n: Maximum n-gram size (mặc định 4)
|
| 283 |
+
weights: Weights cho mỗi n-gram (mặc định uniform)
|
| 284 |
+
|
| 285 |
+
Returns:
|
| 286 |
+
bleu_score: BLEU score (0-100)
|
| 287 |
+
"""
|
| 288 |
+
if weights is None:
|
| 289 |
+
weights = [1.0/max_n] * max_n
|
| 290 |
+
|
| 291 |
+
# Tính precision cho mỗi n-gram
|
| 292 |
+
precisions = []
|
| 293 |
+
|
| 294 |
+
for n in range(1, max_n + 1):
|
| 295 |
+
matched = 0
|
| 296 |
+
total = 0
|
| 297 |
+
|
| 298 |
+
for ref, hyp in zip(references, hypotheses):
|
| 299 |
+
# Tính n-grams
|
| 300 |
+
ref_ngrams = calculate_ngrams(ref, n)
|
| 301 |
+
hyp_ngrams = calculate_ngrams(hyp, n)
|
| 302 |
+
|
| 303 |
+
# Đếm matched n-grams
|
| 304 |
+
for ngram, count in hyp_ngrams.items():
|
| 305 |
+
matched += min(count, ref_ngrams.get(ngram, 0))
|
| 306 |
+
|
| 307 |
+
total += max(len(hyp) - n + 1, 0)
|
| 308 |
+
|
| 309 |
+
# Tính precision
|
| 310 |
+
if total > 0:
|
| 311 |
+
precision = matched / total
|
| 312 |
+
else:
|
| 313 |
+
precision = 0
|
| 314 |
+
|
| 315 |
+
precisions.append(precision)
|
| 316 |
+
|
| 317 |
+
# Tính Brevity Penalty (BP)
|
| 318 |
+
ref_length = sum(len(ref) for ref in references)
|
| 319 |
+
hyp_length = sum(len(hyp) for hyp in hypotheses)
|
| 320 |
+
|
| 321 |
+
if hyp_length > ref_length:
|
| 322 |
+
bp = 1.0
|
| 323 |
+
elif hyp_length == 0:
|
| 324 |
+
bp = 0.0
|
| 325 |
+
else:
|
| 326 |
+
bp = math.exp(1 - ref_length / hyp_length)
|
| 327 |
+
|
| 328 |
+
# Tính BLEU score
|
| 329 |
+
if min(precisions) > 0:
|
| 330 |
+
log_precisions = [w * math.log(p) for w, p in zip(weights, precisions)]
|
| 331 |
+
bleu = bp * math.exp(sum(log_precisions))
|
| 332 |
+
else:
|
| 333 |
+
bleu = 0.0
|
| 334 |
+
|
| 335 |
+
return bleu * 100 # Convert sang 0-100 scale
|
| 336 |
+
|
| 337 |
+
# ============================================================================
|
| 338 |
+
# 4. EVALUATE ON TEST SET
|
| 339 |
+
# ============================================================================
|
| 340 |
+
|
| 341 |
+
def evaluate_model(model, test_loader, src_vocab, tgt_vocab, device,
|
| 342 |
+
use_beam_search=True, beam_size=5, max_len=100):
|
| 343 |
+
"""
|
| 344 |
+
Đánh giá model trên test set
|
| 345 |
+
|
| 346 |
+
Args:
|
| 347 |
+
model: Transformer model
|
| 348 |
+
test_loader: Test DataLoader
|
| 349 |
+
src_vocab: Source vocabulary
|
| 350 |
+
tgt_vocab: Target vocabulary
|
| 351 |
+
device: Device
|
| 352 |
+
use_beam_search: Sử dụng beam search hay greedy search
|
| 353 |
+
beam_size: Beam size (nếu dùng beam search)
|
| 354 |
+
max_len: Maximum decode length
|
| 355 |
+
|
| 356 |
+
Returns:
|
| 357 |
+
bleu_score: BLEU score
|
| 358 |
+
translations: List of (source, reference, hypothesis) tuples
|
| 359 |
+
"""
|
| 360 |
+
model.eval()
|
| 361 |
+
|
| 362 |
+
references = []
|
| 363 |
+
hypotheses = []
|
| 364 |
+
translations = []
|
| 365 |
+
|
| 366 |
+
print(f"\n{'='*70}")
|
| 367 |
+
print(f"ĐÁNH GIÁ TRÊN TEST SET")
|
| 368 |
+
print(f"{'='*70}")
|
| 369 |
+
print(f"Decoding method: {'Beam Search' if use_beam_search else 'Greedy Search'}")
|
| 370 |
+
if use_beam_search:
|
| 371 |
+
print(f"Beam size: {beam_size}")
|
| 372 |
+
print(f"{'='*70}\n")
|
| 373 |
+
|
| 374 |
+
with torch.no_grad():
|
| 375 |
+
for src, tgt, _, _ in tqdm(test_loader, desc='Evaluating'):
|
| 376 |
+
src = src.to(device)
|
| 377 |
+
|
| 378 |
+
batch_size = src.size(0)
|
| 379 |
+
|
| 380 |
+
for i in range(batch_size):
|
| 381 |
+
src_seq = src[i]
|
| 382 |
+
tgt_seq = tgt[i]
|
| 383 |
+
|
| 384 |
+
# Decode
|
| 385 |
+
if use_beam_search:
|
| 386 |
+
_, hypothesis = beam_search_decode(
|
| 387 |
+
model, src_seq, src_vocab, tgt_vocab,
|
| 388 |
+
device, beam_size, max_len, alpha=0.6, repetition_penalty=1.5, no_repeat_ngram_size=3
|
| 389 |
+
)
|
| 390 |
+
else:
|
| 391 |
+
_, hypothesis = greedy_decode(
|
| 392 |
+
model, src_seq, src_vocab, tgt_vocab,
|
| 393 |
+
device, max_len, repetition_penalty=1.5, no_repeat_ngram_size=3
|
| 394 |
+
)
|
| 395 |
+
|
| 396 |
+
# Reference
|
| 397 |
+
reference = tgt_vocab.decode(tgt_seq.tolist())
|
| 398 |
+
|
| 399 |
+
# Source
|
| 400 |
+
source = src_vocab.decode(src_seq.tolist())
|
| 401 |
+
|
| 402 |
+
# Tokenize để tính BLEU
|
| 403 |
+
ref_tokens = reference.split()
|
| 404 |
+
hyp_tokens = hypothesis.split()
|
| 405 |
+
|
| 406 |
+
references.append(ref_tokens)
|
| 407 |
+
hypotheses.append(hyp_tokens)
|
| 408 |
+
|
| 409 |
+
translations.append((source, reference, hypothesis))
|
| 410 |
+
|
| 411 |
+
# Tính BLEU score
|
| 412 |
+
bleu = calculate_bleu_score(references, hypotheses)
|
| 413 |
+
|
| 414 |
+
print(f"\n{'='*70}")
|
| 415 |
+
print(f"KẾT QUẢ ĐÁNH GIÁ")
|
| 416 |
+
print(f"{'='*70}")
|
| 417 |
+
print(f"BLEU Score: {bleu:.2f}")
|
| 418 |
+
print(f"{'='*70}\n")
|
| 419 |
+
|
| 420 |
+
return bleu, translations
|
| 421 |
+
|
| 422 |
+
# ============================================================================
|
| 423 |
+
# 5. PRINT SAMPLE TRANSLATIONS
|
| 424 |
+
# ============================================================================
|
| 425 |
+
|
| 426 |
+
def print_sample_translations(translations, num_samples=10):
|
| 427 |
+
"""
|
| 428 |
+
In một số ví dụ dịch
|
| 429 |
+
|
| 430 |
+
Args:
|
| 431 |
+
translations: List of (source, reference, hypothesis) tuples
|
| 432 |
+
num_samples: Số lượng samples để in
|
| 433 |
+
"""
|
| 434 |
+
print(f"\n{'='*70}")
|
| 435 |
+
print(f"MỘT SỐ VÍ DỤ DỊCH")
|
| 436 |
+
print(f"{'='*70}\n")
|
| 437 |
+
|
| 438 |
+
for i, (src, ref, hyp) in enumerate(translations[:num_samples], 1):
|
| 439 |
+
print(f"Ví dụ {i}:")
|
| 440 |
+
print(f" Source: {src}")
|
| 441 |
+
print(f" Reference: {ref}")
|
| 442 |
+
print(f" Hypothesis: {hyp}")
|
| 443 |
+
print()
|
| 444 |
+
|
| 445 |
+
# ============================================================================
|
| 446 |
+
# 6. TRANSLATE SINGLE SENTENCE
|
| 447 |
+
# ============================================================================
|
| 448 |
+
|
| 449 |
+
def translate_sentence(model, sentence, src_vocab, tgt_vocab, device,
|
| 450 |
+
use_beam_search=True, beam_size=5, max_len=100, src_lang='vi',
|
| 451 |
+
repetition_penalty=1.5, no_repeat_ngram_size=3):
|
| 452 |
+
"""
|
| 453 |
+
Dịch một câu đơn
|
| 454 |
+
|
| 455 |
+
Args:
|
| 456 |
+
model: Transformer model
|
| 457 |
+
sentence: Source sentence (string)
|
| 458 |
+
src_vocab: Source vocabulary
|
| 459 |
+
tgt_vocab: Target vocabulary
|
| 460 |
+
device: Device
|
| 461 |
+
use_beam_search: Sử dụng beam search hay greedy
|
| 462 |
+
beam_size: Beam size
|
| 463 |
+
max_len: Maximum length
|
| 464 |
+
src_lang: Ngôn ngữ source ('vi' hoặc 'en')
|
| 465 |
+
|
| 466 |
+
Returns:
|
| 467 |
+
translation: Translated sentence
|
| 468 |
+
"""
|
| 469 |
+
model.eval()
|
| 470 |
+
|
| 471 |
+
# Tiền xử lý câu
|
| 472 |
+
from data_preprocessing import clean_text
|
| 473 |
+
sentence = clean_text(sentence, src_lang)
|
| 474 |
+
|
| 475 |
+
# Encode
|
| 476 |
+
tokens = src_vocab.encode(sentence)
|
| 477 |
+
src = torch.LongTensor([tokens]).to(device)
|
| 478 |
+
|
| 479 |
+
# Decode
|
| 480 |
+
if use_beam_search:
|
| 481 |
+
_, translation = beam_search_decode(
|
| 482 |
+
model, src, src_vocab, tgt_vocab,
|
| 483 |
+
device, beam_size, max_len, alpha=0.6,
|
| 484 |
+
repetition_penalty=repetition_penalty,
|
| 485 |
+
no_repeat_ngram_size=no_repeat_ngram_size
|
| 486 |
+
)
|
| 487 |
+
else:
|
| 488 |
+
_, translation = greedy_decode(
|
| 489 |
+
model, src, src_vocab, tgt_vocab,
|
| 490 |
+
device, max_len,
|
| 491 |
+
repetition_penalty=repetition_penalty,
|
| 492 |
+
no_repeat_ngram_size=no_repeat_ngram_size
|
| 493 |
+
)
|
| 494 |
+
|
| 495 |
+
return translation
|
| 496 |
+
|
| 497 |
+
# ============================================================================
|
| 498 |
+
# 7. INTERACTIVE TRANSLATION
|
| 499 |
+
# ============================================================================
|
| 500 |
+
|
| 501 |
+
def interactive_translation(model, src_vocab, tgt_vocab, device,
|
| 502 |
+
use_beam_search=True, beam_size=5, direction='vi2en'):
|
| 503 |
+
"""
|
| 504 |
+
Chế độ dịch tương tác
|
| 505 |
+
|
| 506 |
+
Args:
|
| 507 |
+
model: Transformer model
|
| 508 |
+
src_vocab: Source vocabulary
|
| 509 |
+
tgt_vocab: Target vocabulary
|
| 510 |
+
device: Device
|
| 511 |
+
use_beam_search: Sử dụng beam search
|
| 512 |
+
beam_size: Beam size
|
| 513 |
+
direction: 'vi2en' hoặc 'en2vi'
|
| 514 |
+
"""
|
| 515 |
+
print("\n" + "="*70)
|
| 516 |
+
print("CHẾ ĐỘ DỊCH TƯƠNG TÁC")
|
| 517 |
+
print("="*70)
|
| 518 |
+
if direction == 'vi2en':
|
| 519 |
+
print("Nhập câu tiếng Việt để dịch sang tiếng Anh")
|
| 520 |
+
else:
|
| 521 |
+
print("Nhập câu tiếng Anh để dịch sang tiếng Việt")
|
| 522 |
+
print("Gõ 'quit' hoặc 'exit' để thoát")
|
| 523 |
+
print("="*70 + "\n")
|
| 524 |
+
|
| 525 |
+
src_lang = 'en' if direction == 'en2vi' else 'vi'
|
| 526 |
+
src_label = "Tiếng Anh" if direction == 'en2vi' else "Tiếng Việt"
|
| 527 |
+
tgt_label = "Tiếng Việt" if direction == 'en2vi' else "Tiếng Anh"
|
| 528 |
+
|
| 529 |
+
while True:
|
| 530 |
+
sentence = input(f"{src_label}: ").strip()
|
| 531 |
+
|
| 532 |
+
if sentence.lower() in ['quit', 'exit', '']:
|
| 533 |
+
print("Tạm biệt!")
|
| 534 |
+
break
|
| 535 |
+
|
| 536 |
+
translation = translate_sentence(
|
| 537 |
+
model, sentence, src_vocab, tgt_vocab, device,
|
| 538 |
+
use_beam_search, beam_size, src_lang=src_lang, max_len=100
|
| 539 |
+
)
|
| 540 |
+
|
| 541 |
+
print(f"{tgt_label}: {translation}\n")
|
| 542 |
+
|
| 543 |
+
# ============================================================================
|
| 544 |
+
# 8. SAVE TRANSLATIONS
|
| 545 |
+
# ============================================================================
|
| 546 |
+
|
| 547 |
+
def save_translations(translations, output_file='translations.txt'):
|
| 548 |
+
"""
|
| 549 |
+
Lưu translations ra file
|
| 550 |
+
|
| 551 |
+
Args:
|
| 552 |
+
translations: List of (source, reference, hypothesis) tuples
|
| 553 |
+
output_file: Output file path
|
| 554 |
+
"""
|
| 555 |
+
with open(output_file, 'w', encoding='utf-8') as f:
|
| 556 |
+
for i, (src, ref, hyp) in enumerate(translations, 1):
|
| 557 |
+
f.write(f"Example {i}:\n")
|
| 558 |
+
f.write(f"Source: {src}\n")
|
| 559 |
+
f.write(f"Reference: {ref}\n")
|
| 560 |
+
f.write(f"Hypothesis: {hyp}\n")
|
| 561 |
+
f.write("\n")
|
| 562 |
+
|
| 563 |
+
print(f"✓ Saved translations to {output_file}")
|
src/inference_vlsp.py
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
INFERENCE SCRIPT CHO VLSP
|
| 3 |
+
Dịch test set và lưu kết quả vào file submission
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
import argparse
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from tqdm import tqdm
|
| 10 |
+
import sys
|
| 11 |
+
|
| 12 |
+
# Add parent directory to path
|
| 13 |
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
| 14 |
+
|
| 15 |
+
from src.complete_transformer import TransformerShared
|
| 16 |
+
from src.shared_vocab_utils import load_shared_tokenizer, load_shared_vocab_info
|
| 17 |
+
from src.inference_evaluation import beam_search_decode, greedy_decode
|
| 18 |
+
|
| 19 |
+
# ============================================================================
|
| 20 |
+
# CONFIGURATION
|
| 21 |
+
# ============================================================================
|
| 22 |
+
|
| 23 |
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
| 24 |
+
DATA_DIR = PROJECT_ROOT / 'data'
|
| 25 |
+
RESULTS_DIR = PROJECT_ROOT / 'results'
|
| 26 |
+
CHECKPOINT_DIR = PROJECT_ROOT / 'checkpoints'
|
| 27 |
+
|
| 28 |
+
# ============================================================================
|
| 29 |
+
# LOAD MODEL
|
| 30 |
+
# ============================================================================
|
| 31 |
+
|
| 32 |
+
def load_model(checkpoint_path, model_size='custom_25m', device='cuda'):
|
| 33 |
+
"""
|
| 34 |
+
Load model từ checkpoint
|
| 35 |
+
|
| 36 |
+
Args:
|
| 37 |
+
checkpoint_path: Đường dẫn đến checkpoint file
|
| 38 |
+
model_size: Kích thước model ('custom_25m', 'base', etc.)
|
| 39 |
+
device: Device để load model
|
| 40 |
+
|
| 41 |
+
Returns:
|
| 42 |
+
model: Loaded model
|
| 43 |
+
"""
|
| 44 |
+
print(f"\n{'='*70}")
|
| 45 |
+
print(f"LOADING MODEL")
|
| 46 |
+
print(f"{'='*70}")
|
| 47 |
+
|
| 48 |
+
# Load vocab info
|
| 49 |
+
vocab_info = load_shared_vocab_info()
|
| 50 |
+
vocab_size = vocab_info['vocab_size']
|
| 51 |
+
|
| 52 |
+
print(f"Vocab size: {vocab_size}")
|
| 53 |
+
|
| 54 |
+
# Model configs theo model_size
|
| 55 |
+
model_configs = {
|
| 56 |
+
'custom_25m': {
|
| 57 |
+
'd_model': 384,
|
| 58 |
+
'n_layers': 6,
|
| 59 |
+
'n_heads': 8,
|
| 60 |
+
'd_ff': 1536,
|
| 61 |
+
'dropout': 0.1
|
| 62 |
+
},
|
| 63 |
+
'base': {
|
| 64 |
+
'd_model': 512,
|
| 65 |
+
'n_layers': 6,
|
| 66 |
+
'n_heads': 8,
|
| 67 |
+
'd_ff': 2048,
|
| 68 |
+
'dropout': 0.1
|
| 69 |
+
}
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
config = model_configs.get(model_size, model_configs['custom_25m'])
|
| 73 |
+
|
| 74 |
+
# Tạo model
|
| 75 |
+
model = TransformerShared(
|
| 76 |
+
vocab_size=vocab_size,
|
| 77 |
+
d_model=config['d_model'],
|
| 78 |
+
n_layers=config['n_layers'],
|
| 79 |
+
n_heads=config['n_heads'],
|
| 80 |
+
d_ff=config['d_ff'],
|
| 81 |
+
dropout=config['dropout'],
|
| 82 |
+
pad_idx=vocab_info['pad_id']
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
# Load checkpoint
|
| 86 |
+
checkpoint = torch.load(checkpoint_path, map_location=device)
|
| 87 |
+
model.load_state_dict(checkpoint['model_state_dict'])
|
| 88 |
+
model = model.to(device)
|
| 89 |
+
model.eval()
|
| 90 |
+
|
| 91 |
+
print(f"✓ Model loaded from {checkpoint_path}")
|
| 92 |
+
print(f"✓ Model size: {model_size}")
|
| 93 |
+
print(f"✓ Device: {device}")
|
| 94 |
+
print(f"{'='*70}\n")
|
| 95 |
+
|
| 96 |
+
return model, vocab_info
|
| 97 |
+
|
| 98 |
+
# ============================================================================
|
| 99 |
+
# VOCABULARY WRAPPER
|
| 100 |
+
# ============================================================================
|
| 101 |
+
|
| 102 |
+
class VocabWrapper:
|
| 103 |
+
"""Wrapper cho tokenizer để tương thích với inference functions"""
|
| 104 |
+
def __init__(self, tokenizer, vocab_info):
|
| 105 |
+
self.tokenizer = tokenizer
|
| 106 |
+
self.SOS_IDX = vocab_info['sos_id']
|
| 107 |
+
self.EOS_IDX = vocab_info['eos_id']
|
| 108 |
+
self.PAD_IDX = vocab_info['pad_id']
|
| 109 |
+
self.UNK_IDX = vocab_info['unk_id']
|
| 110 |
+
|
| 111 |
+
def encode(self, text):
|
| 112 |
+
"""Encode text thành token IDs"""
|
| 113 |
+
encoding = self.tokenizer.encode(text)
|
| 114 |
+
return encoding.ids
|
| 115 |
+
|
| 116 |
+
def decode(self, token_ids):
|
| 117 |
+
"""Decode token IDs thành text (nhận list of token IDs)"""
|
| 118 |
+
# Filter out special tokens
|
| 119 |
+
filtered_ids = [tid for tid in token_ids
|
| 120 |
+
if tid not in [self.SOS_IDX, self.EOS_IDX, self.PAD_IDX]]
|
| 121 |
+
if not filtered_ids:
|
| 122 |
+
return ""
|
| 123 |
+
# Decode using tokenizer
|
| 124 |
+
decoded = self.tokenizer.decode(filtered_ids)
|
| 125 |
+
return decoded
|
| 126 |
+
|
| 127 |
+
# ============================================================================
|
| 128 |
+
# INFERENCE
|
| 129 |
+
# ============================================================================
|
| 130 |
+
|
| 131 |
+
def translate_file(
|
| 132 |
+
model,
|
| 133 |
+
tokenizer,
|
| 134 |
+
vocab_info,
|
| 135 |
+
source_file,
|
| 136 |
+
output_file,
|
| 137 |
+
device='cuda',
|
| 138 |
+
use_beam_search=True,
|
| 139 |
+
beam_size=5,
|
| 140 |
+
max_len=100
|
| 141 |
+
):
|
| 142 |
+
"""
|
| 143 |
+
Dịch file source và lưu kết quả
|
| 144 |
+
|
| 145 |
+
Args:
|
| 146 |
+
model: Transformer model
|
| 147 |
+
tokenizer: Tokenizer
|
| 148 |
+
vocab_info: Vocabulary info dict
|
| 149 |
+
source_file: File chứa source sentences (mỗi dòng 1 câu)
|
| 150 |
+
output_file: File output để lưu translations
|
| 151 |
+
device: Device
|
| 152 |
+
use_beam_search: Dùng beam search hay greedy
|
| 153 |
+
beam_size: Beam size
|
| 154 |
+
max_len: Max decode length
|
| 155 |
+
"""
|
| 156 |
+
print(f"\n{'='*70}")
|
| 157 |
+
print(f"INFERENCE")
|
| 158 |
+
print(f"{'='*70}")
|
| 159 |
+
print(f"Source file: {source_file}")
|
| 160 |
+
print(f"Output file: {output_file}")
|
| 161 |
+
print(f"Method: {'Beam Search' if use_beam_search else 'Greedy Search'}")
|
| 162 |
+
if use_beam_search:
|
| 163 |
+
print(f"Beam size: {beam_size}")
|
| 164 |
+
print(f"{'='*70}\n")
|
| 165 |
+
|
| 166 |
+
# Load source sentences
|
| 167 |
+
with open(source_file, 'r', encoding='utf-8') as f:
|
| 168 |
+
source_sentences = [line.strip() for line in f.readlines()]
|
| 169 |
+
|
| 170 |
+
print(f"Loaded {len(source_sentences)} source sentences")
|
| 171 |
+
|
| 172 |
+
# Create vocab wrapper
|
| 173 |
+
vocab = VocabWrapper(tokenizer, vocab_info)
|
| 174 |
+
|
| 175 |
+
# Translate
|
| 176 |
+
translations = []
|
| 177 |
+
model.eval()
|
| 178 |
+
|
| 179 |
+
with torch.no_grad():
|
| 180 |
+
for src_text in tqdm(source_sentences, desc='Translating'):
|
| 181 |
+
# Encode source
|
| 182 |
+
src_ids = vocab.encode(src_text)
|
| 183 |
+
src_tensor = torch.LongTensor([src_ids]).to(device)
|
| 184 |
+
|
| 185 |
+
# Decode
|
| 186 |
+
if use_beam_search:
|
| 187 |
+
_, translation = beam_search_decode(
|
| 188 |
+
model, src_tensor, vocab, vocab, device, beam_size, max_len,
|
| 189 |
+
alpha=0.6, repetition_penalty=1.5, no_repeat_ngram_size=3
|
| 190 |
+
)
|
| 191 |
+
else:
|
| 192 |
+
_, translation = greedy_decode(
|
| 193 |
+
model, src_tensor, vocab, vocab, device, max_len,
|
| 194 |
+
repetition_penalty=1.5, no_repeat_ngram_size=3
|
| 195 |
+
)
|
| 196 |
+
|
| 197 |
+
translations.append(translation)
|
| 198 |
+
|
| 199 |
+
# Save translations
|
| 200 |
+
RESULTS_DIR.mkdir(exist_ok=True, parents=True)
|
| 201 |
+
with open(output_file, 'w', encoding='utf-8') as f:
|
| 202 |
+
for trans in translations:
|
| 203 |
+
f.write(trans + '\n')
|
| 204 |
+
|
| 205 |
+
print(f"\n✓ Saved {len(translations)} translations to {output_file}")
|
| 206 |
+
print(f"{'='*70}\n")
|
| 207 |
+
|
| 208 |
+
# ============================================================================
|
| 209 |
+
# MAIN
|
| 210 |
+
# ============================================================================
|
| 211 |
+
|
| 212 |
+
def main():
|
| 213 |
+
parser = argparse.ArgumentParser(description='Inference cho VLSP')
|
| 214 |
+
parser.add_argument('--checkpoint', type=str, required=True,
|
| 215 |
+
help='Đường dẫn đến checkpoint file')
|
| 216 |
+
parser.add_argument('--source', type=str,
|
| 217 |
+
default='data/raw/Hospital/public_test.en.txt',
|
| 218 |
+
help='File source (tiếng Anh)')
|
| 219 |
+
parser.add_argument('--output', type=str,
|
| 220 |
+
default='results/submission_vlsp.txt',
|
| 221 |
+
help='File output (tiếng Việt)')
|
| 222 |
+
parser.add_argument('--model_size', type=str, default='custom_25m',
|
| 223 |
+
choices=['custom_25m', 'base'],
|
| 224 |
+
help='Kích thước model')
|
| 225 |
+
parser.add_argument('--beam_size', type=int, default=5,
|
| 226 |
+
help='Beam size cho beam search')
|
| 227 |
+
parser.add_argument('--greedy', action='store_true',
|
| 228 |
+
help='Dùng greedy search thay vì beam search')
|
| 229 |
+
parser.add_argument('--max_len', type=int, default=100,
|
| 230 |
+
help='Maximum decode length')
|
| 231 |
+
parser.add_argument('--device', type=str, default='cuda',
|
| 232 |
+
help='Device (cuda/cpu)')
|
| 233 |
+
|
| 234 |
+
args = parser.parse_args()
|
| 235 |
+
|
| 236 |
+
# Check files
|
| 237 |
+
checkpoint_path = Path(args.checkpoint)
|
| 238 |
+
if not checkpoint_path.exists():
|
| 239 |
+
print(f"❌ Không tìm thấy checkpoint: {checkpoint_path}")
|
| 240 |
+
return
|
| 241 |
+
|
| 242 |
+
source_path = Path(args.source)
|
| 243 |
+
if not source_path.exists():
|
| 244 |
+
print(f"❌ Không tìm thấy source file: {source_path}")
|
| 245 |
+
return
|
| 246 |
+
|
| 247 |
+
# Device
|
| 248 |
+
device = args.device
|
| 249 |
+
if device == 'cuda' and not torch.cuda.is_available():
|
| 250 |
+
print("⚠️ CUDA không khả dụng, dùng CPU")
|
| 251 |
+
device = 'cpu'
|
| 252 |
+
|
| 253 |
+
# Load model
|
| 254 |
+
model, vocab_info = load_model(
|
| 255 |
+
checkpoint_path,
|
| 256 |
+
args.model_size,
|
| 257 |
+
device
|
| 258 |
+
)
|
| 259 |
+
|
| 260 |
+
# Load tokenizer
|
| 261 |
+
tokenizer = load_shared_tokenizer()
|
| 262 |
+
|
| 263 |
+
# Translate
|
| 264 |
+
translate_file(
|
| 265 |
+
model=model,
|
| 266 |
+
tokenizer=tokenizer,
|
| 267 |
+
vocab_info=vocab_info,
|
| 268 |
+
source_file=source_path,
|
| 269 |
+
output_file=Path(args.output),
|
| 270 |
+
device=device,
|
| 271 |
+
use_beam_search=not args.greedy,
|
| 272 |
+
beam_size=args.beam_size,
|
| 273 |
+
max_len=args.max_len
|
| 274 |
+
)
|
| 275 |
+
|
| 276 |
+
if __name__ == '__main__':
|
| 277 |
+
main()
|
| 278 |
+
|
src/main_pipeline.py
ADDED
|
@@ -0,0 +1,550 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MAIN PIPELINE - HOÀN CHỈNH (FIXED)
|
| 3 |
+
Script chính để chạy toàn bộ pipeline: Data → Train → Evaluate
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
import argparse
|
| 8 |
+
import os
|
| 9 |
+
import pickle
|
| 10 |
+
import json
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
# Import modules - IMPORTANT: Import Vocabulary class
|
| 14 |
+
from data_preprocessing import main as prepare_data, Vocabulary
|
| 15 |
+
from dataloader_module import create_dataloaders_with_bucketing, load_data_and_vocab
|
| 16 |
+
from complete_transformer import create_model, print_model_info
|
| 17 |
+
from training_module import train_model, plot_training_history, load_checkpoint
|
| 18 |
+
from inference_evaluation import (
|
| 19 |
+
evaluate_model, print_sample_translations,
|
| 20 |
+
save_translations, interactive_translation
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
# ============================================================================
|
| 24 |
+
# PATH SETUP & CONFIGURATION
|
| 25 |
+
# ============================================================================
|
| 26 |
+
|
| 27 |
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
| 28 |
+
DATA_DIR = PROJECT_ROOT / 'data'
|
| 29 |
+
PROCESSED_DATA_DIR = DATA_DIR / 'processed'
|
| 30 |
+
CHECKPOINT_DIR = PROJECT_ROOT / 'checkpoints'
|
| 31 |
+
RESULTS_DIR = PROJECT_ROOT / 'results'
|
| 32 |
+
LOGS_DIR = PROJECT_ROOT / 'logs'
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class Config:
|
| 36 |
+
"""
|
| 37 |
+
Configuration cho training
|
| 38 |
+
"""
|
| 39 |
+
# Data
|
| 40 |
+
data_dir = DATA_DIR
|
| 41 |
+
checkpoint_dir = CHECKPOINT_DIR
|
| 42 |
+
results_dir = RESULTS_DIR
|
| 43 |
+
logs_dir = LOGS_DIR
|
| 44 |
+
|
| 45 |
+
# Model
|
| 46 |
+
# Cấu hình cho 1M dataset: ~25M parameters, 32k shared vocab
|
| 47 |
+
model_size = 'medium' # 'tiny', 'small', 'medium' (~25M), 'base', 'large'
|
| 48 |
+
use_weight_tying = True # Weight tying enabled (tiết kiệm ~50% vocab params)
|
| 49 |
+
|
| 50 |
+
# Training - Tối ưu cho RTX 5060 Ti 16GB
|
| 51 |
+
num_epochs = 20
|
| 52 |
+
batch_size = 128 # Batch size lớn để tận dụng GPU
|
| 53 |
+
grad_accum_steps = 2 # Effective batch = 128 * 2 = 256
|
| 54 |
+
warmup_steps = 4000
|
| 55 |
+
label_smoothing = 0.1
|
| 56 |
+
gradient_clip = 1.0
|
| 57 |
+
precision = 'bf16' # 'bf16' (recommended), 'amp' (float16), hoặc 'fp32'
|
| 58 |
+
|
| 59 |
+
# Decoding
|
| 60 |
+
beam_size = 5
|
| 61 |
+
max_decode_len = 100
|
| 62 |
+
|
| 63 |
+
# Device
|
| 64 |
+
device = 'cuda' if torch.cuda.is_available() else 'cpu'
|
| 65 |
+
|
| 66 |
+
# Num workers - set to 0 on Windows to avoid multiprocessing issues
|
| 67 |
+
import platform
|
| 68 |
+
num_workers = 0 if platform.system() == 'Windows' else 2
|
| 69 |
+
|
| 70 |
+
# Checkpoint
|
| 71 |
+
save_every = 1
|
| 72 |
+
|
| 73 |
+
def __str__(self):
|
| 74 |
+
config_str = "\n" + "="*70 + "\n"
|
| 75 |
+
config_str += "CONFIGURATION\n"
|
| 76 |
+
config_str += "="*70 + "\n"
|
| 77 |
+
for key, value in self.__dict__.items():
|
| 78 |
+
if not key.startswith('_'):
|
| 79 |
+
config_str += f"{key}: {value}\n"
|
| 80 |
+
config_str += "="*70 + "\n"
|
| 81 |
+
return config_str
|
| 82 |
+
|
| 83 |
+
# ============================================================================
|
| 84 |
+
# MAIN FUNCTIONS
|
| 85 |
+
# ============================================================================
|
| 86 |
+
|
| 87 |
+
def setup_directories():
|
| 88 |
+
"""
|
| 89 |
+
Tạo các thư mục cần thiết
|
| 90 |
+
"""
|
| 91 |
+
for directory in [CHECKPOINT_DIR, RESULTS_DIR, LOGS_DIR]:
|
| 92 |
+
os.makedirs(directory, exist_ok=True)
|
| 93 |
+
print("✓ Created directories")
|
| 94 |
+
|
| 95 |
+
def stage_1_prepare_data():
|
| 96 |
+
"""
|
| 97 |
+
STAGE 1: Chuẩn bị dữ liệu (Shared Vocabulary) - 1M dataset từ MTet
|
| 98 |
+
"""
|
| 99 |
+
print("\n" + "="*70)
|
| 100 |
+
print("STAGE 1: CHUẨN BỊ DỮ LIỆU (SHARED VOCABULARY - 1M DATASET)")
|
| 101 |
+
print("="*70 + "\n")
|
| 102 |
+
|
| 103 |
+
# Kiểm tra xem đã có shared vocab data chưa
|
| 104 |
+
shared_data_files = [
|
| 105 |
+
PROCESSED_DATA_DIR / 'tokenizer_shared.json',
|
| 106 |
+
PROCESSED_DATA_DIR / 'processed_data_shared.pkl',
|
| 107 |
+
PROCESSED_DATA_DIR / 'shared_vocab_info.json'
|
| 108 |
+
]
|
| 109 |
+
|
| 110 |
+
if all(os.path.exists(f) for f in shared_data_files):
|
| 111 |
+
print("✓ Shared vocabulary data đã được chuẩn bị trước đó")
|
| 112 |
+
print(f" - Tokenizer: {shared_data_files[0]}")
|
| 113 |
+
print(f" - Processed data: {shared_data_files[1]}")
|
| 114 |
+
print(f" - Vocab info: {shared_data_files[2]}")
|
| 115 |
+
|
| 116 |
+
# Kiểm tra số lượng data
|
| 117 |
+
import pickle
|
| 118 |
+
with open(shared_data_files[1], 'rb') as f:
|
| 119 |
+
data = pickle.load(f)
|
| 120 |
+
train_size = len(data.get('train', []))
|
| 121 |
+
print(f" - Train samples: {train_size:,}")
|
| 122 |
+
|
| 123 |
+
if train_size < 500000:
|
| 124 |
+
print(f"\n⚠️ Dataset hiện tại chỉ có {train_size:,} câu")
|
| 125 |
+
print(" Để train 1M dataset, cần tạo lại từ MTet CSV")
|
| 126 |
+
response = input("Bạn có muốn tạo lại với 1M câu từ MTet không? (y/n): ")
|
| 127 |
+
if response.lower() == 'y':
|
| 128 |
+
print("\n→ Đang tạo lại với 1M câu từ MTet...")
|
| 129 |
+
import os
|
| 130 |
+
os.environ['MTET_MAX_ROWS'] = '1000000'
|
| 131 |
+
# Chạy lại build vocab và encode
|
| 132 |
+
from subprocess import run
|
| 133 |
+
run(['python3', 'src/1_build_shared_vocab.py'], check=True)
|
| 134 |
+
run(['python3', 'src/2_encode_data.py'], check=True)
|
| 135 |
+
print("✓ Đã tạo lại với 1M câu!")
|
| 136 |
+
else:
|
| 137 |
+
response = input("Bạn có mu���n chuẩn bị lại không? (y/n): ")
|
| 138 |
+
if response.lower() != 'y':
|
| 139 |
+
print("→ Bỏ qua stage 1")
|
| 140 |
+
return
|
| 141 |
+
else:
|
| 142 |
+
# Tự động setup với 1M câu từ MTet
|
| 143 |
+
print("📝 Tự động setup shared vocabulary với 1M câu từ MTet...")
|
| 144 |
+
import os
|
| 145 |
+
os.environ['MTET_MAX_ROWS'] = '1000000'
|
| 146 |
+
|
| 147 |
+
print("\n→ Chạy: python src/1_build_shared_vocab.py")
|
| 148 |
+
from subprocess import run
|
| 149 |
+
try:
|
| 150 |
+
run(['python3', 'src/1_build_shared_vocab.py'], check=True)
|
| 151 |
+
print("\n→ Chạy: python src/2_encode_data.py")
|
| 152 |
+
run(['python3', 'src/2_encode_data.py'], check=True)
|
| 153 |
+
print("\n✓ Đã setup xong với 1M câu!")
|
| 154 |
+
except Exception as e:
|
| 155 |
+
print(f"\n⚠️ Lỗi khi setup: {e}")
|
| 156 |
+
print("Vui lòng chạy thủ công:")
|
| 157 |
+
print(" 1. export MTET_MAX_ROWS=1000000")
|
| 158 |
+
print(" 2. python src/1_build_shared_vocab.py")
|
| 159 |
+
print(" 3. python src/2_encode_data.py")
|
| 160 |
+
return
|
| 161 |
+
|
| 162 |
+
print("\n✓ HOÀN TẤT STAGE 1")
|
| 163 |
+
|
| 164 |
+
def stage_2_create_model(config):
|
| 165 |
+
"""
|
| 166 |
+
STAGE 2: Tạo model với Shared Vocabulary
|
| 167 |
+
"""
|
| 168 |
+
print("\n" + "="*70)
|
| 169 |
+
print("STAGE 2: TẠO MODEL (SHARED VOCABULARY)")
|
| 170 |
+
print("="*70 + "\n")
|
| 171 |
+
|
| 172 |
+
# Load shared vocabulary
|
| 173 |
+
from shared_vocab_utils import load_shared_vocab_info, create_shared_vocab_wrapper
|
| 174 |
+
|
| 175 |
+
vocab_info = load_shared_vocab_info()
|
| 176 |
+
vocab_size = vocab_info['vocab_size']
|
| 177 |
+
pad_idx = vocab_info['pad_id']
|
| 178 |
+
|
| 179 |
+
print(f"Loading shared vocabulary:")
|
| 180 |
+
print(f" - Vocab size: {vocab_size}")
|
| 181 |
+
print(f" - PAD ID: {pad_idx}")
|
| 182 |
+
print(f" - SOS ID: {vocab_info['sos_id']}")
|
| 183 |
+
print(f" - EOS ID: {vocab_info['eos_id']}")
|
| 184 |
+
|
| 185 |
+
# Tạo shared vocab wrappers
|
| 186 |
+
vi_vocab, en_vocab = create_shared_vocab_wrapper()
|
| 187 |
+
|
| 188 |
+
print(f"\n✓ Shared vocabulary loaded (cả 2 ngôn ngữ dùng chung)")
|
| 189 |
+
|
| 190 |
+
# Tạo model với shared vocab
|
| 191 |
+
model, model_config = create_model(
|
| 192 |
+
src_vocab_size=vocab_size,
|
| 193 |
+
tgt_vocab_size=vocab_size,
|
| 194 |
+
model_size=config.model_size,
|
| 195 |
+
pad_idx=pad_idx,
|
| 196 |
+
use_shared_vocab=True,
|
| 197 |
+
use_weight_tying=config.use_weight_tying
|
| 198 |
+
)
|
| 199 |
+
|
| 200 |
+
# Print model info
|
| 201 |
+
print_model_info(model, config.model_size, use_shared_vocab=True)
|
| 202 |
+
|
| 203 |
+
# Move to device
|
| 204 |
+
device = torch.device(config.device)
|
| 205 |
+
model = model.to(device)
|
| 206 |
+
|
| 207 |
+
# Kiểm tra và load checkpoint cũ nếu có (chỉ khi KHÔNG train bidirectional)
|
| 208 |
+
# Nếu train bidirectional, không load checkpoint cũ để train từ đầu
|
| 209 |
+
use_bidirectional = getattr(config, 'use_bidirectional', False)
|
| 210 |
+
|
| 211 |
+
if not use_bidirectional:
|
| 212 |
+
# Chỉ load checkpoint khi train vi-en (không bidirectional)
|
| 213 |
+
checkpoint_dir = Path(config.checkpoint_dir)
|
| 214 |
+
best_model_path = checkpoint_dir / 'best_model.pt'
|
| 215 |
+
|
| 216 |
+
# Tìm checkpoint epoch cao nhất
|
| 217 |
+
max_epoch = 0
|
| 218 |
+
latest_checkpoint = None
|
| 219 |
+
for checkpoint_file in checkpoint_dir.glob('checkpoint_epoch_*.pt'):
|
| 220 |
+
try:
|
| 221 |
+
epoch_num = int(checkpoint_file.stem.split('_')[-1])
|
| 222 |
+
if epoch_num > max_epoch:
|
| 223 |
+
max_epoch = epoch_num
|
| 224 |
+
latest_checkpoint = checkpoint_file
|
| 225 |
+
except:
|
| 226 |
+
pass
|
| 227 |
+
|
| 228 |
+
# Load checkpoint nếu có
|
| 229 |
+
if latest_checkpoint and latest_checkpoint.exists():
|
| 230 |
+
print(f"\n→ Tìm thấy checkpoint cũ: {latest_checkpoint.name} (epoch {max_epoch})")
|
| 231 |
+
print(f"→ Model sẽ được load từ checkpoint này để tiếp tục training")
|
| 232 |
+
try:
|
| 233 |
+
checkpoint = torch.load(latest_checkpoint, map_location=device)
|
| 234 |
+
model.load_state_dict(checkpoint['model_state_dict'])
|
| 235 |
+
print(f"✓ Đã load weights từ checkpoint epoch {max_epoch}")
|
| 236 |
+
except Exception as e:
|
| 237 |
+
print(f"⚠️ Không thể load checkpoint: {e}")
|
| 238 |
+
print(f" Sẽ train từ đầu với weights mới")
|
| 239 |
+
elif best_model_path.exists():
|
| 240 |
+
print(f"\n→ Tìm thấy best_model.pt")
|
| 241 |
+
print(f"→ Model sẽ được load từ best_model để tiếp tục training")
|
| 242 |
+
try:
|
| 243 |
+
checkpoint = torch.load(best_model_path, map_location=device)
|
| 244 |
+
model.load_state_dict(checkpoint['model_state_dict'])
|
| 245 |
+
print(f"✓ Đã load weights từ best_model")
|
| 246 |
+
except Exception as e:
|
| 247 |
+
print(f"⚠️ Không thể load checkpoint: {e}")
|
| 248 |
+
print(f" Sẽ train từ đầu với weights mới")
|
| 249 |
+
else:
|
| 250 |
+
print(f"\n→ Không tìm thấy checkpoint cũ")
|
| 251 |
+
print(f"→ Sẽ train từ đầu với weights mới")
|
| 252 |
+
else:
|
| 253 |
+
# Train bidirectional: KHÔNG load checkpoint cũ, train từ đầu
|
| 254 |
+
print(f"\n→ Training với bidirectional dataset")
|
| 255 |
+
print(f"→ KHÔNG load checkpoint cũ, sẽ train từ đầu với weights mới")
|
| 256 |
+
print(f"→ Dropout: {model_config.get('dropout', 0.1)} (đã có trong model)")
|
| 257 |
+
|
| 258 |
+
print(f"\n✓ Model moved to {device}")
|
| 259 |
+
print("✓ HOÀN TẤT STAGE 2")
|
| 260 |
+
|
| 261 |
+
return model, vi_vocab, en_vocab, model_config
|
| 262 |
+
|
| 263 |
+
def stage_3_prepare_dataloaders(config):
|
| 264 |
+
"""
|
| 265 |
+
STAGE 3: Chuẩn bị DataLoaders với Shared Vocabulary
|
| 266 |
+
"""
|
| 267 |
+
print("\n" + "="*70)
|
| 268 |
+
print("STAGE 3: CHUẨN BỊ DATALOADERS (SHARED VOCABULARY)")
|
| 269 |
+
print("="*70 + "\n")
|
| 270 |
+
|
| 271 |
+
# Load shared vocab data
|
| 272 |
+
from dataloader_module import load_data_and_vocab
|
| 273 |
+
|
| 274 |
+
# Check if bidirectional flag is set
|
| 275 |
+
use_bidirectional = getattr(config, 'use_bidirectional', False)
|
| 276 |
+
|
| 277 |
+
processed_data, vi_vocab, en_vocab = load_data_and_vocab(
|
| 278 |
+
use_shared_vocab=True,
|
| 279 |
+
use_bidirectional=use_bidirectional
|
| 280 |
+
)
|
| 281 |
+
|
| 282 |
+
print(f"✓ Loaded shared vocab data:")
|
| 283 |
+
print(f" - Train: {len(processed_data['train']['src'])} cặp câu")
|
| 284 |
+
print(f" - Validation: {len(processed_data['validation']['src'])} cặp câu")
|
| 285 |
+
print(f" - Test: {len(processed_data['test']['src'])} cặp câu")
|
| 286 |
+
|
| 287 |
+
# Tạo dataloaders
|
| 288 |
+
train_loader, val_loader, test_loader = create_dataloaders_with_bucketing(
|
| 289 |
+
processed_data,
|
| 290 |
+
batch_size=config.batch_size,
|
| 291 |
+
num_workers=config.num_workers
|
| 292 |
+
)
|
| 293 |
+
|
| 294 |
+
print("\n✓ HOÀN TẤT STAGE 3")
|
| 295 |
+
|
| 296 |
+
return train_loader, val_loader, test_loader
|
| 297 |
+
|
| 298 |
+
def stage_4_train_model(model, train_loader, val_loader, config, model_config):
|
| 299 |
+
"""
|
| 300 |
+
STAGE 4: Huấn luyện model
|
| 301 |
+
"""
|
| 302 |
+
print("\n" + "="*70)
|
| 303 |
+
print("STAGE 4: HUẤN LUYỆN MODEL")
|
| 304 |
+
print("="*70 + "\n")
|
| 305 |
+
|
| 306 |
+
# Kiểm tra có checkpoint không và tự động tiếp tục
|
| 307 |
+
# CHỈ resume khi KHÔNG train bidirectional
|
| 308 |
+
use_bidirectional = getattr(config, 'use_bidirectional', False)
|
| 309 |
+
resume_from = None
|
| 310 |
+
|
| 311 |
+
if not use_bidirectional:
|
| 312 |
+
# Chỉ resume khi train vi-en (không bidirectional)
|
| 313 |
+
checkpoint_dir = Path(config.checkpoint_dir)
|
| 314 |
+
|
| 315 |
+
# Tìm checkpoint epoch cao nhất
|
| 316 |
+
max_epoch = 0
|
| 317 |
+
for checkpoint_file in checkpoint_dir.glob('checkpoint_epoch_*.pt'):
|
| 318 |
+
try:
|
| 319 |
+
epoch_num = int(checkpoint_file.stem.split('_')[-1])
|
| 320 |
+
if epoch_num > max_epoch:
|
| 321 |
+
max_epoch = epoch_num
|
| 322 |
+
resume_from = checkpoint_file
|
| 323 |
+
except:
|
| 324 |
+
pass
|
| 325 |
+
|
| 326 |
+
# Nếu không tìm thấy checkpoint epoch, thử dùng best_model
|
| 327 |
+
if resume_from is None or not resume_from.exists():
|
| 328 |
+
best_model_path = checkpoint_dir / 'best_model.pt'
|
| 329 |
+
if best_model_path.exists():
|
| 330 |
+
# Kiểm tra epoch trong best_model
|
| 331 |
+
try:
|
| 332 |
+
checkpoint = torch.load(best_model_path, map_location='cpu')
|
| 333 |
+
if 'epoch' in checkpoint:
|
| 334 |
+
max_epoch = checkpoint['epoch']
|
| 335 |
+
resume_from = best_model_path
|
| 336 |
+
print(f"→ Tìm thấy best_model.pt (epoch {max_epoch})")
|
| 337 |
+
print(f"→ Sẽ tiếp tục training từ epoch {max_epoch + 1}")
|
| 338 |
+
except:
|
| 339 |
+
pass
|
| 340 |
+
|
| 341 |
+
if resume_from and resume_from.exists():
|
| 342 |
+
print(f"→ Tìm thấy checkpoint: {resume_from.name}")
|
| 343 |
+
print(f"→ Tự động tiếp tục training từ epoch {max_epoch + 1}")
|
| 344 |
+
else:
|
| 345 |
+
print(f"→ Không tìm thấy checkpoint cũ, sẽ train từ đầu")
|
| 346 |
+
else:
|
| 347 |
+
# Train bidirectional: KHÔNG resume, train từ đầu
|
| 348 |
+
print(f"→ Training với bidirectional dataset")
|
| 349 |
+
print(f"→ KHÔNG resume từ checkpoint cũ, sẽ train từ đầu (epoch 1)")
|
| 350 |
+
resume_from = None
|
| 351 |
+
|
| 352 |
+
# Train
|
| 353 |
+
history = train_model(
|
| 354 |
+
model=model,
|
| 355 |
+
train_loader=train_loader,
|
| 356 |
+
val_loader=val_loader,
|
| 357 |
+
num_epochs=config.num_epochs,
|
| 358 |
+
device=torch.device(config.device),
|
| 359 |
+
d_model=model_config['d_model'],
|
| 360 |
+
warmup_steps=config.warmup_steps,
|
| 361 |
+
label_smoothing=config.label_smoothing,
|
| 362 |
+
grad_accum_steps=config.grad_accum_steps,
|
| 363 |
+
precision=config.precision,
|
| 364 |
+
checkpoint_dir=config.checkpoint_dir,
|
| 365 |
+
save_every=config.save_every,
|
| 366 |
+
resume_from=str(resume_from) if resume_from else None
|
| 367 |
+
)
|
| 368 |
+
|
| 369 |
+
# Plot history
|
| 370 |
+
training_plot_path = Path(config.results_dir) / 'training_history.png'
|
| 371 |
+
plot_training_history(history, save_path=training_plot_path)
|
| 372 |
+
|
| 373 |
+
# Save history
|
| 374 |
+
training_history_path = Path(config.results_dir) / 'training_history.json'
|
| 375 |
+
with open(training_history_path, 'w') as f:
|
| 376 |
+
json.dump(history, f, indent=2)
|
| 377 |
+
|
| 378 |
+
print("\n✓ HOÀN TẤT STAGE 4")
|
| 379 |
+
|
| 380 |
+
return history
|
| 381 |
+
|
| 382 |
+
def stage_5_evaluate_model(model, test_loader, vi_vocab, en_vocab, config):
|
| 383 |
+
"""
|
| 384 |
+
STAGE 5: Đánh giá model
|
| 385 |
+
"""
|
| 386 |
+
print("\n" + "="*70)
|
| 387 |
+
print("STAGE 5: ĐÁNH GIÁ MODEL")
|
| 388 |
+
print("="*70 + "\n")
|
| 389 |
+
|
| 390 |
+
device = torch.device(config.device)
|
| 391 |
+
|
| 392 |
+
# Load best model
|
| 393 |
+
best_model_path = Path(config.checkpoint_dir) / 'best_model.pt'
|
| 394 |
+
if os.path.exists(best_model_path):
|
| 395 |
+
print("Loading best model...")
|
| 396 |
+
model, _ = load_checkpoint(model, best_model_path, device)
|
| 397 |
+
|
| 398 |
+
# Evaluate với Greedy Search
|
| 399 |
+
print("\n--- GREEDY SEARCH ---")
|
| 400 |
+
bleu_greedy, translations_greedy = evaluate_model(
|
| 401 |
+
model, test_loader, vi_vocab, en_vocab, device,
|
| 402 |
+
use_beam_search=False, max_len=config.max_decode_len
|
| 403 |
+
)
|
| 404 |
+
|
| 405 |
+
# Evaluate với Beam Search
|
| 406 |
+
print("\n--- BEAM SEARCH ---")
|
| 407 |
+
bleu_beam, translations_beam = evaluate_model(
|
| 408 |
+
model, test_loader, vi_vocab, en_vocab, device,
|
| 409 |
+
use_beam_search=True, beam_size=config.beam_size, max_len=config.max_decode_len
|
| 410 |
+
)
|
| 411 |
+
|
| 412 |
+
# In samples
|
| 413 |
+
print("\n" + "="*70)
|
| 414 |
+
print("GREEDY SEARCH SAMPLES")
|
| 415 |
+
print("="*70)
|
| 416 |
+
print_sample_translations(translations_greedy, num_samples=10)
|
| 417 |
+
|
| 418 |
+
print("\n" + "="*70)
|
| 419 |
+
print("BEAM SEARCH SAMPLES")
|
| 420 |
+
print("="*70)
|
| 421 |
+
print_sample_translations(translations_beam, num_samples=10)
|
| 422 |
+
|
| 423 |
+
# Save translations
|
| 424 |
+
save_translations(translations_greedy, Path(config.results_dir) / 'translations_greedy.txt')
|
| 425 |
+
save_translations(translations_beam, Path(config.results_dir) / 'translations_beam.txt')
|
| 426 |
+
|
| 427 |
+
# Save scores
|
| 428 |
+
scores = {
|
| 429 |
+
'greedy_bleu': bleu_greedy,
|
| 430 |
+
'beam_bleu': bleu_beam,
|
| 431 |
+
'beam_size': config.beam_size
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
with open(Path(config.results_dir) / 'bleu_scores.json', 'w') as f:
|
| 435 |
+
json.dump(scores, f, indent=2)
|
| 436 |
+
|
| 437 |
+
print("\n" + "="*70)
|
| 438 |
+
print("KẾT QUẢ CUỐI CÙNG")
|
| 439 |
+
print("="*70)
|
| 440 |
+
print(f"Greedy Search BLEU: {bleu_greedy:.2f}")
|
| 441 |
+
print(f"Beam Search BLEU: {bleu_beam:.2f}")
|
| 442 |
+
print(f"Improvement: {bleu_beam - bleu_greedy:.2f}")
|
| 443 |
+
print("="*70)
|
| 444 |
+
|
| 445 |
+
print("\n✓ HOÀN TẤT STAGE 5")
|
| 446 |
+
|
| 447 |
+
return bleu_greedy, bleu_beam
|
| 448 |
+
|
| 449 |
+
def stage_6_interactive_mode(model, vi_vocab, en_vocab, config):
|
| 450 |
+
"""
|
| 451 |
+
STAGE 6: Chế độ dịch tương tác
|
| 452 |
+
"""
|
| 453 |
+
print("\n" + "="*70)
|
| 454 |
+
print("STAGE 6: CHẾ ĐỘ DỊCH TƯƠNG TÁC")
|
| 455 |
+
print("="*70 + "\n")
|
| 456 |
+
|
| 457 |
+
device = torch.device(config.device)
|
| 458 |
+
|
| 459 |
+
# Load best model
|
| 460 |
+
best_model_path = os.path.join(config.checkpoint_dir, 'best_model.pt')
|
| 461 |
+
if os.path.exists(best_model_path):
|
| 462 |
+
model, _ = load_checkpoint(model, best_model_path, device)
|
| 463 |
+
|
| 464 |
+
# Interactive translation
|
| 465 |
+
interactive_translation(
|
| 466 |
+
model, vi_vocab, en_vocab, device,
|
| 467 |
+
use_beam_search=True, beam_size=config.beam_size
|
| 468 |
+
)
|
| 469 |
+
|
| 470 |
+
print("\n✓ HOÀN TẤT STAGE 6")
|
| 471 |
+
|
| 472 |
+
# ============================================================================
|
| 473 |
+
# MAIN PIPELINE
|
| 474 |
+
# ============================================================================
|
| 475 |
+
|
| 476 |
+
def main():
|
| 477 |
+
"""
|
| 478 |
+
Main pipeline
|
| 479 |
+
"""
|
| 480 |
+
# Parse arguments
|
| 481 |
+
parser = argparse.ArgumentParser(description='Transformer Machine Translation')
|
| 482 |
+
parser.add_argument('--stage', type=str, default='all',
|
| 483 |
+
help='Which stage to run: all, 1, 2, 3, 4, 5, 6')
|
| 484 |
+
parser.add_argument('--model_size', type=str, default='medium',
|
| 485 |
+
help='Model size: tiny, small, medium (~25M), base, large')
|
| 486 |
+
parser.add_argument('--epochs', type=int, default=20,
|
| 487 |
+
help='Number of epochs')
|
| 488 |
+
parser.add_argument('--batch_size', type=int, default=32,
|
| 489 |
+
help='Batch size')
|
| 490 |
+
parser.add_argument('--beam_size', type=int, default=5,
|
| 491 |
+
help='Beam size for beam search')
|
| 492 |
+
parser.add_argument('--grad_accum_steps', type=int, default=2,
|
| 493 |
+
help='Gradient accumulation steps (effective batch = batch_size * grad_accum_steps)')
|
| 494 |
+
parser.add_argument('--precision', type=str, default='bf16', choices=['bf16', 'amp', 'fp32'],
|
| 495 |
+
help='Training precision: bf16 (bfloat16, recommended), amp (float16), or fp32')
|
| 496 |
+
parser.add_argument('--no_weight_tying', action='store_true',
|
| 497 |
+
help='Disable weight tying (default: weight tying enabled)')
|
| 498 |
+
parser.add_argument('--bidirectional', action='store_true',
|
| 499 |
+
help='Use bidirectional dataset (vi↔en)')
|
| 500 |
+
args = parser.parse_args()
|
| 501 |
+
|
| 502 |
+
# Setup config
|
| 503 |
+
config = Config()
|
| 504 |
+
config.model_size = args.model_size
|
| 505 |
+
config.num_epochs = args.epochs
|
| 506 |
+
config.use_bidirectional = args.bidirectional
|
| 507 |
+
config.batch_size = args.batch_size
|
| 508 |
+
config.beam_size = args.beam_size
|
| 509 |
+
config.grad_accum_steps = max(1, args.grad_accum_steps)
|
| 510 |
+
config.precision = args.precision
|
| 511 |
+
config.use_weight_tying = not args.no_weight_tying
|
| 512 |
+
|
| 513 |
+
print(config)
|
| 514 |
+
|
| 515 |
+
# Setup directories
|
| 516 |
+
setup_directories()
|
| 517 |
+
|
| 518 |
+
# Run stages
|
| 519 |
+
stage = args.stage.lower()
|
| 520 |
+
|
| 521 |
+
if stage in ['all', '1']:
|
| 522 |
+
stage_1_prepare_data()
|
| 523 |
+
|
| 524 |
+
if stage in ['all', '2', '3', '4', '5', '6']:
|
| 525 |
+
model, vi_vocab, en_vocab, model_config = stage_2_create_model(config)
|
| 526 |
+
|
| 527 |
+
if stage in ['all', '3', '4', '5']:
|
| 528 |
+
train_loader, val_loader, test_loader = stage_3_prepare_dataloaders(config)
|
| 529 |
+
|
| 530 |
+
if stage in ['all', '4']:
|
| 531 |
+
history = stage_4_train_model(model, train_loader, val_loader, config, model_config)
|
| 532 |
+
|
| 533 |
+
if stage in ['all', '5']:
|
| 534 |
+
bleu_greedy, bleu_beam = stage_5_evaluate_model(
|
| 535 |
+
model, test_loader, vi_vocab, en_vocab, config
|
| 536 |
+
)
|
| 537 |
+
|
| 538 |
+
if stage in ['6', 'interactive']:
|
| 539 |
+
stage_6_interactive_mode(model, vi_vocab, en_vocab, config)
|
| 540 |
+
|
| 541 |
+
print("\n" + "="*70)
|
| 542 |
+
print("🎉 HOÀN TẤT TOÀN BỘ PIPELINE!")
|
| 543 |
+
print("="*70)
|
| 544 |
+
|
| 545 |
+
# ============================================================================
|
| 546 |
+
# RUN
|
| 547 |
+
# ============================================================================
|
| 548 |
+
|
| 549 |
+
if __name__ == "__main__":
|
| 550 |
+
main()
|
src/prepare_hospital_data.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
PREPARE HOSPITAL DATA FOR FINETUNING
|
| 3 |
+
Xử lý dữ liệu Hospital domain để finetune model
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import pickle
|
| 7 |
+
import json
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from tqdm import tqdm
|
| 10 |
+
|
| 11 |
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
| 12 |
+
DATA_DIR = PROJECT_ROOT / 'data'
|
| 13 |
+
RAW_DATA_DIR = DATA_DIR / 'raw'
|
| 14 |
+
PROCESSED_DATA_DIR = DATA_DIR / 'processed'
|
| 15 |
+
HOSPITAL_DIR = RAW_DATA_DIR / 'Hospital'
|
| 16 |
+
|
| 17 |
+
def load_hospital_data():
|
| 18 |
+
"""
|
| 19 |
+
Load dữ liệu Hospital từ các file .txt
|
| 20 |
+
"""
|
| 21 |
+
print("="*70)
|
| 22 |
+
print("LOADING HOSPITAL DATA")
|
| 23 |
+
print("="*70)
|
| 24 |
+
|
| 25 |
+
train_vi_path = HOSPITAL_DIR / 'train.vi.txt'
|
| 26 |
+
train_en_path = HOSPITAL_DIR / 'train.en.txt'
|
| 27 |
+
test_vi_path = HOSPITAL_DIR / 'public_test.vi.txt'
|
| 28 |
+
test_en_path = HOSPITAL_DIR / 'public_test.en.txt'
|
| 29 |
+
|
| 30 |
+
# Load training data
|
| 31 |
+
print("\n→ Loading training data...")
|
| 32 |
+
with open(train_vi_path, 'r', encoding='utf-8') as f:
|
| 33 |
+
train_vi = [line.strip() for line in f if line.strip()]
|
| 34 |
+
with open(train_en_path, 'r', encoding='utf-8') as f:
|
| 35 |
+
train_en = [line.strip() for line in f if line.strip()]
|
| 36 |
+
|
| 37 |
+
print(f" ✓ Train VI: {len(train_vi)} câu")
|
| 38 |
+
print(f" ✓ Train EN: {len(train_en)} câu")
|
| 39 |
+
|
| 40 |
+
# Load test data
|
| 41 |
+
print("\n→ Loading test data...")
|
| 42 |
+
with open(test_vi_path, 'r', encoding='utf-8') as f:
|
| 43 |
+
test_vi = [line.strip() for line in f if line.strip()]
|
| 44 |
+
with open(test_en_path, 'r', encoding='utf-8') as f:
|
| 45 |
+
test_en = [line.strip() for line in f if line.strip()]
|
| 46 |
+
|
| 47 |
+
print(f" ✓ Test VI: {len(test_vi)} câu")
|
| 48 |
+
print(f" ✓ Test EN: {len(test_en)} câu")
|
| 49 |
+
|
| 50 |
+
# Kiểm tra số lượng khớp
|
| 51 |
+
if len(train_vi) != len(train_en):
|
| 52 |
+
print(f"⚠️ Warning: Train VI ({len(train_vi)}) != Train EN ({len(train_en)})")
|
| 53 |
+
min_len = min(len(train_vi), len(train_en))
|
| 54 |
+
train_vi = train_vi[:min_len]
|
| 55 |
+
train_en = train_en[:min_len]
|
| 56 |
+
print(f" → Đã cắt về {min_len} cặp câu")
|
| 57 |
+
|
| 58 |
+
if len(test_vi) != len(test_en):
|
| 59 |
+
print(f"⚠️ Warning: Test VI ({len(test_vi)}) != Test EN ({len(test_en)})")
|
| 60 |
+
min_len = min(len(test_vi), len(test_en))
|
| 61 |
+
test_vi = test_vi[:min_len]
|
| 62 |
+
test_en = test_en[:min_len]
|
| 63 |
+
print(f" → Đã cắt về {min_len} cặp câu")
|
| 64 |
+
|
| 65 |
+
# Split train thành train và validation (90-10)
|
| 66 |
+
print("\n→ Splitting train/validation (90-10)...")
|
| 67 |
+
split_idx = int(len(train_vi) * 0.9)
|
| 68 |
+
val_vi = train_vi[split_idx:]
|
| 69 |
+
val_en = train_en[split_idx:]
|
| 70 |
+
train_vi = train_vi[:split_idx]
|
| 71 |
+
train_en = train_en[:split_idx]
|
| 72 |
+
|
| 73 |
+
print(f" ✓ Train: {len(train_vi)} cặp câu")
|
| 74 |
+
print(f" ✓ Validation: {len(val_vi)} cặp câu")
|
| 75 |
+
print(f" ✓ Test: {len(test_vi)} cặp câu")
|
| 76 |
+
|
| 77 |
+
return {
|
| 78 |
+
'train': {'vi': train_vi, 'en': train_en},
|
| 79 |
+
'validation': {'vi': val_vi, 'en': val_en},
|
| 80 |
+
'test': {'vi': test_vi, 'en': test_en}
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
def encode_hospital_data(hospital_data):
|
| 84 |
+
"""
|
| 85 |
+
Encode dữ liệu Hospital với shared vocabulary hiện có
|
| 86 |
+
"""
|
| 87 |
+
print("\n" + "="*70)
|
| 88 |
+
print("ENCODING HOSPITAL DATA")
|
| 89 |
+
print("="*70)
|
| 90 |
+
|
| 91 |
+
# Load shared vocabulary
|
| 92 |
+
from shared_vocab_utils import load_shared_vocab_info, create_shared_vocab_wrapper
|
| 93 |
+
|
| 94 |
+
print("\n→ Loading shared vocabulary...")
|
| 95 |
+
vocab_info = load_shared_vocab_info()
|
| 96 |
+
vi_vocab, en_vocab = create_shared_vocab_wrapper()
|
| 97 |
+
|
| 98 |
+
print(f" ✓ Vocab size: {vocab_info['vocab_size']}")
|
| 99 |
+
print(f" ✓ PAD ID: {vocab_info['pad_id']}")
|
| 100 |
+
print(f" ✓ SOS ID: {vocab_info['sos_id']}")
|
| 101 |
+
print(f" ✓ EOS ID: {vocab_info['eos_id']}")
|
| 102 |
+
|
| 103 |
+
# Encode data
|
| 104 |
+
print("\n→ Encoding data...")
|
| 105 |
+
encoded_data = {}
|
| 106 |
+
|
| 107 |
+
for split in ['train', 'validation', 'test']:
|
| 108 |
+
print(f"\n Encoding {split}...")
|
| 109 |
+
vi_sentences = hospital_data[split]['vi']
|
| 110 |
+
en_sentences = hospital_data[split]['en']
|
| 111 |
+
|
| 112 |
+
encoded_vi = []
|
| 113 |
+
encoded_en = []
|
| 114 |
+
|
| 115 |
+
for vi_sent, en_sent in tqdm(zip(vi_sentences, en_sentences),
|
| 116 |
+
total=len(vi_sentences),
|
| 117 |
+
desc=f" {split}"):
|
| 118 |
+
# Encode VI
|
| 119 |
+
vi_tokens = vi_vocab.encode(vi_sent)
|
| 120 |
+
encoded_vi.append(vi_tokens)
|
| 121 |
+
|
| 122 |
+
# Encode EN
|
| 123 |
+
en_tokens = en_vocab.encode(en_sent)
|
| 124 |
+
encoded_en.append(en_tokens)
|
| 125 |
+
|
| 126 |
+
encoded_data[split] = {
|
| 127 |
+
'src': encoded_vi,
|
| 128 |
+
'tgt': encoded_en
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
print(f" ✓ {split}: {len(encoded_vi)} cặp câu")
|
| 132 |
+
|
| 133 |
+
return encoded_data, vi_vocab, en_vocab
|
| 134 |
+
|
| 135 |
+
def save_hospital_data(encoded_data):
|
| 136 |
+
"""
|
| 137 |
+
Lưu dữ liệu Hospital đã encode
|
| 138 |
+
"""
|
| 139 |
+
print("\n" + "="*70)
|
| 140 |
+
print("SAVING HOSPITAL DATA")
|
| 141 |
+
print("="*70)
|
| 142 |
+
|
| 143 |
+
hospital_data_path = PROCESSED_DATA_DIR / 'hospital_data_encoded.pkl'
|
| 144 |
+
|
| 145 |
+
print(f"\n→ Saving to {hospital_data_path}...")
|
| 146 |
+
with open(hospital_data_path, 'wb') as f:
|
| 147 |
+
pickle.dump(encoded_data, f)
|
| 148 |
+
|
| 149 |
+
print(f" ✓ Saved: {hospital_data_path}")
|
| 150 |
+
|
| 151 |
+
# Save info
|
| 152 |
+
info = {
|
| 153 |
+
'train_size': len(encoded_data['train']['src']),
|
| 154 |
+
'validation_size': len(encoded_data['validation']['src']),
|
| 155 |
+
'test_size': len(encoded_data['test']['src']),
|
| 156 |
+
'total_size': sum(len(encoded_data[split]['src']) for split in ['train', 'validation', 'test'])
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
info_path = PROCESSED_DATA_DIR / 'hospital_data_info.json'
|
| 160 |
+
with open(info_path, 'w', encoding='utf-8') as f:
|
| 161 |
+
json.dump(info, f, indent=2, ensure_ascii=False)
|
| 162 |
+
|
| 163 |
+
print(f" ✓ Saved info: {info_path}")
|
| 164 |
+
print(f"\n Summary:")
|
| 165 |
+
print(f" - Train: {info['train_size']:,} cặp câu")
|
| 166 |
+
print(f" - Validation: {info['validation_size']:,} cặp câu")
|
| 167 |
+
print(f" - Test: {info['test_size']:,} cặp câu")
|
| 168 |
+
print(f" - Total: {info['total_size']:,} cặp câu")
|
| 169 |
+
|
| 170 |
+
def main():
|
| 171 |
+
"""
|
| 172 |
+
Main function: Xử lý dữ liệu Hospital
|
| 173 |
+
"""
|
| 174 |
+
print("\n" + "="*70)
|
| 175 |
+
print("PREPARE HOSPITAL DATA FOR FINETUNING")
|
| 176 |
+
print("="*70)
|
| 177 |
+
|
| 178 |
+
# Kiểm tra thư mục Hospital
|
| 179 |
+
if not HOSPITAL_DIR.exists():
|
| 180 |
+
print(f"❌ Không tìm thấy thư mục: {HOSPITAL_DIR}")
|
| 181 |
+
return
|
| 182 |
+
|
| 183 |
+
# Tạo thư mục processed nếu chưa có
|
| 184 |
+
PROCESSED_DATA_DIR.mkdir(parents=True, exist_ok=True)
|
| 185 |
+
|
| 186 |
+
# Load dữ liệu
|
| 187 |
+
hospital_data = load_hospital_data()
|
| 188 |
+
|
| 189 |
+
# Encode dữ liệu
|
| 190 |
+
encoded_data, vi_vocab, en_vocab = encode_hospital_data(hospital_data)
|
| 191 |
+
|
| 192 |
+
# Lưu dữ liệu
|
| 193 |
+
save_hospital_data(encoded_data)
|
| 194 |
+
|
| 195 |
+
print("\n" + "="*70)
|
| 196 |
+
print("✓ HOÀN TẤT XỬ LÝ DỮ LIỆU HOSPITAL")
|
| 197 |
+
print("="*70)
|
| 198 |
+
print(f"\nDữ liệu đã sẵn sàng để finetune!")
|
| 199 |
+
print(f" - Encoded data: {PROCESSED_DATA_DIR / 'hospital_data_encoded.pkl'}")
|
| 200 |
+
print(f" - Info: {PROCESSED_DATA_DIR / 'hospital_data_info.json'}")
|
| 201 |
+
|
| 202 |
+
if __name__ == "__main__":
|
| 203 |
+
main()
|
| 204 |
+
|
src/shared_vocab_utils.py
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
UTILITY FUNCTIONS CHO SHARED VOCABULARY
|
| 3 |
+
Helper functions để load và sử dụng shared tokenizer
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import json
|
| 7 |
+
import pickle
|
| 8 |
+
import re
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from tokenizers import Tokenizer
|
| 11 |
+
from typing import Tuple, Optional
|
| 12 |
+
|
| 13 |
+
# ============================================================================
|
| 14 |
+
# PATH CONFIGURATION
|
| 15 |
+
# ============================================================================
|
| 16 |
+
|
| 17 |
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
| 18 |
+
PROCESSED_DATA_DIR = PROJECT_ROOT / 'data' / 'processed'
|
| 19 |
+
|
| 20 |
+
# ============================================================================
|
| 21 |
+
# LOAD SHARED VOCABULARY
|
| 22 |
+
# ============================================================================
|
| 23 |
+
|
| 24 |
+
def load_shared_tokenizer(tokenizer_path: Optional[Path] = None) -> Tokenizer:
|
| 25 |
+
"""
|
| 26 |
+
Load shared tokenizer từ file
|
| 27 |
+
|
| 28 |
+
Args:
|
| 29 |
+
tokenizer_path: Đường dẫn đến tokenizer_shared.json (mặc định tự tìm)
|
| 30 |
+
|
| 31 |
+
Returns:
|
| 32 |
+
tokenizer: Loaded tokenizer
|
| 33 |
+
"""
|
| 34 |
+
if tokenizer_path is None:
|
| 35 |
+
tokenizer_path = PROCESSED_DATA_DIR / 'tokenizer_shared.json'
|
| 36 |
+
|
| 37 |
+
if not tokenizer_path.exists():
|
| 38 |
+
raise FileNotFoundError(
|
| 39 |
+
f"Không tìm thấy tokenizer_shared.json tại {tokenizer_path}!\n"
|
| 40 |
+
f"Vui lòng chạy: python src/1_build_shared_vocab.py trước"
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
tokenizer = Tokenizer.from_file(str(tokenizer_path))
|
| 44 |
+
return tokenizer
|
| 45 |
+
|
| 46 |
+
def load_shared_vocab_info(info_path: Optional[Path] = None) -> dict:
|
| 47 |
+
"""
|
| 48 |
+
Load thông tin về shared vocabulary
|
| 49 |
+
|
| 50 |
+
Args:
|
| 51 |
+
info_path: Đường dẫn đến shared_vocab_info.json (mặc định tự tìm)
|
| 52 |
+
|
| 53 |
+
Returns:
|
| 54 |
+
info: Dictionary chứa vocab_size, sos_id, eos_id, pad_id, unk_id
|
| 55 |
+
"""
|
| 56 |
+
if info_path is None:
|
| 57 |
+
info_path = PROCESSED_DATA_DIR / 'shared_vocab_info.json'
|
| 58 |
+
|
| 59 |
+
if not info_path.exists():
|
| 60 |
+
# Tạo từ tokenizer nếu chưa có
|
| 61 |
+
tokenizer = load_shared_tokenizer()
|
| 62 |
+
info = {
|
| 63 |
+
'vocab_size': tokenizer.get_vocab_size(),
|
| 64 |
+
'sos_id': tokenizer.token_to_id("<sos>"),
|
| 65 |
+
'eos_id': tokenizer.token_to_id("<eos>"),
|
| 66 |
+
'pad_id': tokenizer.token_to_id("<pad>"),
|
| 67 |
+
'unk_id': tokenizer.token_to_id("<unk>"),
|
| 68 |
+
'tokenizer_path': str(PROCESSED_DATA_DIR / 'tokenizer_shared.json')
|
| 69 |
+
}
|
| 70 |
+
return info
|
| 71 |
+
|
| 72 |
+
with open(info_path, 'r', encoding='utf-8') as f:
|
| 73 |
+
info = json.load(f)
|
| 74 |
+
|
| 75 |
+
return info
|
| 76 |
+
|
| 77 |
+
def load_shared_processed_data(data_path: Optional[Path] = None, use_bidirectional: bool = False) -> dict:
|
| 78 |
+
"""
|
| 79 |
+
Load processed data đã encode với shared vocabulary
|
| 80 |
+
|
| 81 |
+
Args:
|
| 82 |
+
data_path: Đường dẫn đến processed_data file (mặc định tự tìm)
|
| 83 |
+
use_bidirectional: Nếu True, load bidirectional dataset (có cả vi→en và en→vi)
|
| 84 |
+
|
| 85 |
+
Returns:
|
| 86 |
+
processed_data: Dict với keys 'train', 'validation', 'test'
|
| 87 |
+
Mỗi value là list of (src_ids, tgt_ids) tuples
|
| 88 |
+
"""
|
| 89 |
+
if data_path is None:
|
| 90 |
+
if use_bidirectional:
|
| 91 |
+
data_path = PROCESSED_DATA_DIR / 'processed_data_bidirectional.pkl'
|
| 92 |
+
if not data_path.exists():
|
| 93 |
+
# Fallback to regular dataset if bidirectional doesn't exist
|
| 94 |
+
print("⚠️ Bidirectional dataset không tồn tại, dùng dataset thường")
|
| 95 |
+
data_path = PROCESSED_DATA_DIR / 'processed_data_shared.pkl'
|
| 96 |
+
else:
|
| 97 |
+
data_path = PROCESSED_DATA_DIR / 'processed_data_shared.pkl'
|
| 98 |
+
|
| 99 |
+
if not data_path.exists():
|
| 100 |
+
raise FileNotFoundError(
|
| 101 |
+
f"Không tìm thấy data file tại {data_path}!\n"
|
| 102 |
+
f"Vui lòng chạy: python src/2_encode_data.py trước"
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
with open(data_path, 'rb') as f:
|
| 106 |
+
processed_data = pickle.load(f)
|
| 107 |
+
|
| 108 |
+
if use_bidirectional and 'bidirectional' in str(data_path):
|
| 109 |
+
print(f"✓ Loaded bidirectional dataset từ {data_path.name}")
|
| 110 |
+
else:
|
| 111 |
+
print(f"✓ Loaded dataset từ {data_path.name}")
|
| 112 |
+
|
| 113 |
+
return processed_data
|
| 114 |
+
|
| 115 |
+
# ============================================================================
|
| 116 |
+
# POST-PROCESSING: CLEAN DECODED OUTPUT
|
| 117 |
+
# ============================================================================
|
| 118 |
+
|
| 119 |
+
def clean_decoded_output(text: str) -> str:
|
| 120 |
+
"""
|
| 121 |
+
Clean decoded output để loại bỏ padding artifacts và các lỗi kỹ thuật
|
| 122 |
+
|
| 123 |
+
Fixes:
|
| 124 |
+
1. Loại bỏ khoảng trắng thừa
|
| 125 |
+
2. Loại bỏ ký tự lạ (như ‹¶, <pad>, etc.)
|
| 126 |
+
3. Normalize spacing quanh dấu câu
|
| 127 |
+
4. Strip leading/trailing whitespace
|
| 128 |
+
|
| 129 |
+
Args:
|
| 130 |
+
text: Raw decoded text
|
| 131 |
+
|
| 132 |
+
Returns:
|
| 133 |
+
cleaned: Cleaned text
|
| 134 |
+
"""
|
| 135 |
+
if not text:
|
| 136 |
+
return ""
|
| 137 |
+
|
| 138 |
+
# Loại bỏ các ký tự đặc biệt không mong muốn
|
| 139 |
+
# Loại bỏ các ký tự control và non-printable (trừ space, newline, tab)
|
| 140 |
+
text = re.sub(r'[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F-\x9F]', '', text)
|
| 141 |
+
|
| 142 |
+
# Loại bỏ các ký tự lạ như ‹¶ và các ký tự Unicode không hợp lệ
|
| 143 |
+
text = re.sub(r'[‹¶]', '', text)
|
| 144 |
+
|
| 145 |
+
# Loại bỏ các token đặc biệt nếu còn sót
|
| 146 |
+
text = text.replace('<pad>', '').replace('</s>', '').replace('<s>', '')
|
| 147 |
+
text = text.replace('<eos>', '').replace('<sos>', '').replace('<unk>', '')
|
| 148 |
+
|
| 149 |
+
# Normalize spacing quanh dấu câu (loại bỏ space trước dấu câu)
|
| 150 |
+
text = re.sub(r'\s+([,.!?;:])', r'\1', text)
|
| 151 |
+
# Thêm space sau dấu câu nếu chưa có (trừ khi là cuối câu)
|
| 152 |
+
text = re.sub(r'([,.!?;:])([^\s])', r'\1 \2', text)
|
| 153 |
+
|
| 154 |
+
# Loại bỏ multiple spaces
|
| 155 |
+
text = re.sub(r' +', ' ', text)
|
| 156 |
+
|
| 157 |
+
# Loại bỏ space ở đầu và cuối
|
| 158 |
+
text = text.strip()
|
| 159 |
+
|
| 160 |
+
# Loại bỏ space thừa ở đầu câu (nếu có)
|
| 161 |
+
text = text.lstrip()
|
| 162 |
+
|
| 163 |
+
return text
|
| 164 |
+
|
| 165 |
+
# ============================================================================
|
| 166 |
+
# ENCODE/DECODE HELPERS
|
| 167 |
+
# ============================================================================
|
| 168 |
+
|
| 169 |
+
class SharedVocabulary:
|
| 170 |
+
"""
|
| 171 |
+
Wrapper class để dùng shared tokenizer như Vocabulary cũ
|
| 172 |
+
Tương thích với code hiện tại
|
| 173 |
+
"""
|
| 174 |
+
def __init__(self, tokenizer: Tokenizer):
|
| 175 |
+
self.tokenizer = tokenizer
|
| 176 |
+
self.PAD_TOKEN = "<pad>"
|
| 177 |
+
self.SOS_TOKEN = "<sos>"
|
| 178 |
+
self.EOS_TOKEN = "<eos>"
|
| 179 |
+
self.UNK_TOKEN = "<unk>"
|
| 180 |
+
|
| 181 |
+
self.PAD_IDX = tokenizer.token_to_id(self.PAD_TOKEN) or 0
|
| 182 |
+
self.SOS_IDX = tokenizer.token_to_id(self.SOS_TOKEN) or 1
|
| 183 |
+
self.EOS_IDX = tokenizer.token_to_id(self.EOS_TOKEN) or 2
|
| 184 |
+
self.UNK_IDX = tokenizer.token_to_id(self.UNK_TOKEN) or 3
|
| 185 |
+
|
| 186 |
+
def encode(self, sentence: str) -> list:
|
| 187 |
+
"""
|
| 188 |
+
Encode câu thành list of token IDs
|
| 189 |
+
|
| 190 |
+
Args:
|
| 191 |
+
sentence: Input sentence (string)
|
| 192 |
+
|
| 193 |
+
Returns:
|
| 194 |
+
tokens: List of token IDs [SOS_ID, ...token_ids..., EOS_ID]
|
| 195 |
+
"""
|
| 196 |
+
# Clean sentence (lowercase, normalize spaces)
|
| 197 |
+
sentence = sentence.lower().strip()
|
| 198 |
+
|
| 199 |
+
# Encode với tokenizer
|
| 200 |
+
encoded = self.tokenizer.encode(sentence)
|
| 201 |
+
token_ids = encoded.ids
|
| 202 |
+
|
| 203 |
+
# Thêm SOS và EOS
|
| 204 |
+
return [self.SOS_IDX] + token_ids + [self.EOS_IDX]
|
| 205 |
+
|
| 206 |
+
def decode(self, token_ids: list) -> str:
|
| 207 |
+
"""
|
| 208 |
+
Decode list of token IDs thành câu
|
| 209 |
+
|
| 210 |
+
Args:
|
| 211 |
+
token_ids: List of token IDs
|
| 212 |
+
|
| 213 |
+
Returns:
|
| 214 |
+
sentence: Decoded sentence (string)
|
| 215 |
+
"""
|
| 216 |
+
# Loại bỏ SOS, EOS, PAD
|
| 217 |
+
filtered_ids = [
|
| 218 |
+
idx for idx in token_ids
|
| 219 |
+
if idx not in [self.PAD_IDX, self.SOS_IDX, self.EOS_IDX]
|
| 220 |
+
]
|
| 221 |
+
|
| 222 |
+
if not filtered_ids:
|
| 223 |
+
return ""
|
| 224 |
+
|
| 225 |
+
# Decode với tokenizer
|
| 226 |
+
decoded = self.tokenizer.decode(filtered_ids, skip_special_tokens=True)
|
| 227 |
+
|
| 228 |
+
# Post-processing: Clean output
|
| 229 |
+
decoded = clean_decoded_output(decoded)
|
| 230 |
+
|
| 231 |
+
return decoded
|
| 232 |
+
|
| 233 |
+
def __len__(self):
|
| 234 |
+
"""Kích thước vocabulary"""
|
| 235 |
+
return self.tokenizer.get_vocab_size()
|
| 236 |
+
|
| 237 |
+
def token_to_id(self, token: str) -> int:
|
| 238 |
+
"""Convert token string to ID"""
|
| 239 |
+
return self.tokenizer.token_to_id(token) or self.UNK_IDX
|
| 240 |
+
|
| 241 |
+
def id_to_token(self, idx: int) -> str:
|
| 242 |
+
"""Convert ID to token string"""
|
| 243 |
+
return self.tokenizer.id_to_token(idx) or self.UNK_TOKEN
|
| 244 |
+
|
| 245 |
+
def create_shared_vocab_wrapper() -> Tuple[SharedVocabulary, SharedVocabulary]:
|
| 246 |
+
"""
|
| 247 |
+
Tạo wrapper cho shared vocabulary (tương thích với code cũ)
|
| 248 |
+
|
| 249 |
+
Returns:
|
| 250 |
+
vi_vocab, en_vocab: Cả 2 đều là SharedVocabulary (cùng tokenizer)
|
| 251 |
+
"""
|
| 252 |
+
tokenizer = load_shared_tokenizer()
|
| 253 |
+
|
| 254 |
+
# Cả 2 ngôn ngữ dùng chung tokenizer
|
| 255 |
+
vi_vocab = SharedVocabulary(tokenizer)
|
| 256 |
+
en_vocab = SharedVocabulary(tokenizer)
|
| 257 |
+
|
| 258 |
+
return vi_vocab, en_vocab
|
| 259 |
+
|
| 260 |
+
# ============================================================================
|
| 261 |
+
# CHECK SHARED VOCAB SETUP
|
| 262 |
+
# ============================================================================
|
| 263 |
+
|
| 264 |
+
def check_shared_vocab_setup() -> bool:
|
| 265 |
+
"""
|
| 266 |
+
Kiểm tra xem shared vocabulary đã được setup chưa
|
| 267 |
+
|
| 268 |
+
Returns:
|
| 269 |
+
is_setup: True nếu đã setup đầy đủ
|
| 270 |
+
"""
|
| 271 |
+
tokenizer_path = PROCESSED_DATA_DIR / 'tokenizer_shared.json'
|
| 272 |
+
data_path = PROCESSED_DATA_DIR / 'processed_data_shared.pkl'
|
| 273 |
+
|
| 274 |
+
return tokenizer_path.exists() and data_path.exists()
|
| 275 |
+
|
| 276 |
+
def print_shared_vocab_status():
|
| 277 |
+
"""
|
| 278 |
+
In trạng thái setup của shared vocabulary
|
| 279 |
+
"""
|
| 280 |
+
print("="*70)
|
| 281 |
+
print("KIỂM TRA SHARED VOCABULARY SETUP")
|
| 282 |
+
print("="*70)
|
| 283 |
+
|
| 284 |
+
tokenizer_path = PROCESSED_DATA_DIR / 'tokenizer_shared.json'
|
| 285 |
+
data_path = PROCESSED_DATA_DIR / 'processed_data_shared.pkl'
|
| 286 |
+
info_path = PROCESSED_DATA_DIR / 'shared_vocab_info.json'
|
| 287 |
+
|
| 288 |
+
print(f"\n1. Tokenizer:")
|
| 289 |
+
if tokenizer_path.exists():
|
| 290 |
+
print(f" ✓ {tokenizer_path}")
|
| 291 |
+
try:
|
| 292 |
+
tokenizer = load_shared_tokenizer()
|
| 293 |
+
print(f" ✓ Vocab size: {tokenizer.get_vocab_size()}")
|
| 294 |
+
except Exception as e:
|
| 295 |
+
print(f" ✗ Lỗi khi load: {e}")
|
| 296 |
+
else:
|
| 297 |
+
print(f" ✗ Không tìm thấy: {tokenizer_path}")
|
| 298 |
+
print(f" → Chạy: python src/1_build_shared_vocab.py")
|
| 299 |
+
|
| 300 |
+
print(f"\n2. Processed Data:")
|
| 301 |
+
if data_path.exists():
|
| 302 |
+
print(f" ✓ {data_path}")
|
| 303 |
+
try:
|
| 304 |
+
data = load_shared_processed_data()
|
| 305 |
+
print(f" ✓ Train: {len(data.get('train', []))} cặp câu")
|
| 306 |
+
print(f" ✓ Validation: {len(data.get('validation', []))} cặp câu")
|
| 307 |
+
print(f" ✓ Test: {len(data.get('test', []))} cặp câu")
|
| 308 |
+
except Exception as e:
|
| 309 |
+
print(f" ✗ Lỗi khi load: {e}")
|
| 310 |
+
else:
|
| 311 |
+
print(f" ✗ Không tìm thấy: {data_path}")
|
| 312 |
+
print(f" → Chạy: python src/2_encode_data.py")
|
| 313 |
+
|
| 314 |
+
print(f"\n3. Info File:")
|
| 315 |
+
if info_path.exists():
|
| 316 |
+
print(f" ✓ {info_path}")
|
| 317 |
+
else:
|
| 318 |
+
print(f" ⚠️ Chưa có (sẽ tự tạo khi cần)")
|
| 319 |
+
|
| 320 |
+
print("\n" + "="*70)
|
| 321 |
+
|
| 322 |
+
if check_shared_vocab_setup():
|
| 323 |
+
print("✓ SHARED VOCABULARY ĐÃ SẴN SÀNG!")
|
| 324 |
+
else:
|
| 325 |
+
print("⚠️ SHARED VOCABULARY CHƯA ĐƯỢC SETUP")
|
| 326 |
+
print("\n📝 Các bước cần làm:")
|
| 327 |
+
print(" 1. python src/1_build_shared_vocab.py")
|
| 328 |
+
print(" 2. python src/2_encode_data.py")
|
| 329 |
+
|
| 330 |
+
print("="*70)
|
| 331 |
+
|
| 332 |
+
# ============================================================================
|
| 333 |
+
# MAIN
|
| 334 |
+
# ============================================================================
|
| 335 |
+
|
| 336 |
+
if __name__ == "__main__":
|
| 337 |
+
print_shared_vocab_status()
|
| 338 |
+
|
src/train_mtet_bidirectional.py
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Train model với mtet_bidirectional dataset
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
# Set CUDA memory allocation để tránh fragmentation
|
| 7 |
+
os.environ['PYTORCH_ALLOC_CONF'] = 'expandable_segments:True'
|
| 8 |
+
|
| 9 |
+
import torch
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
import sys
|
| 12 |
+
import argparse
|
| 13 |
+
|
| 14 |
+
# Add parent directory to path
|
| 15 |
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
| 16 |
+
|
| 17 |
+
from src.complete_transformer import TransformerShared, get_model_config
|
| 18 |
+
from src.shared_vocab_utils import load_shared_tokenizer, load_shared_vocab_info
|
| 19 |
+
from src.dataloader_module import create_dataloaders_with_bucketing
|
| 20 |
+
from src.training_module import train_model
|
| 21 |
+
from src.gpu_safety import get_gpu_memory_info, check_and_adjust_batch_size, clear_gpu_cache
|
| 22 |
+
import pickle
|
| 23 |
+
|
| 24 |
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
| 25 |
+
PROCESSED_DATA_DIR = PROJECT_ROOT / 'data' / 'processed'
|
| 26 |
+
CHECKPOINT_DIR = PROJECT_ROOT / 'checkpoints'
|
| 27 |
+
|
| 28 |
+
def load_mtet_bidirectional_data():
|
| 29 |
+
"""Load processed mtet bidirectional data"""
|
| 30 |
+
# Dùng bản dữ liệu đã được clean & bidirectional từ mtet_cleaned.csv
|
| 31 |
+
data_path = PROCESSED_DATA_DIR / "processed_data_mtet_bidirectional_cleaned.pkl"
|
| 32 |
+
|
| 33 |
+
if not data_path.exists():
|
| 34 |
+
raise FileNotFoundError(
|
| 35 |
+
f"Không tìm thấy processed data tại {data_path}!\n"
|
| 36 |
+
f"Vui lòng chạy: python src/encode_mtet_bidirectional.py trước"
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
print(f"📂 Loading data từ: {data_path}")
|
| 40 |
+
with open(data_path, 'rb') as f:
|
| 41 |
+
data = pickle.load(f)
|
| 42 |
+
|
| 43 |
+
# Convert format từ list of tuples sang dict với 'src' và 'tgt'
|
| 44 |
+
converted_data = {}
|
| 45 |
+
for split in ['train', 'validation', 'test']:
|
| 46 |
+
if split in data:
|
| 47 |
+
src_list = [item[0] for item in data[split]]
|
| 48 |
+
tgt_list = [item[1] for item in data[split]]
|
| 49 |
+
converted_data[split] = {
|
| 50 |
+
'src': src_list,
|
| 51 |
+
'tgt': tgt_list
|
| 52 |
+
}
|
| 53 |
+
else:
|
| 54 |
+
converted_data[split] = {'src': [], 'tgt': []}
|
| 55 |
+
|
| 56 |
+
return converted_data
|
| 57 |
+
|
| 58 |
+
def main():
|
| 59 |
+
parser = argparse.ArgumentParser(description="Train với mtet_bidirectional dataset")
|
| 60 |
+
parser.add_argument(
|
| 61 |
+
"--num_epochs",
|
| 62 |
+
type=int,
|
| 63 |
+
default=30,
|
| 64 |
+
help="Số epochs (mặc định: 30)",
|
| 65 |
+
)
|
| 66 |
+
parser.add_argument(
|
| 67 |
+
"--batch_size",
|
| 68 |
+
type=int,
|
| 69 |
+
default=32,
|
| 70 |
+
help="Batch size (mặc định: 32, giảm để tránh OOM)",
|
| 71 |
+
)
|
| 72 |
+
parser.add_argument(
|
| 73 |
+
"--model_size",
|
| 74 |
+
type=str,
|
| 75 |
+
default="custom_25m",
|
| 76 |
+
choices=["custom_25m", "base", "small"],
|
| 77 |
+
help="Kích thước model (mặc định: custom_25m)",
|
| 78 |
+
)
|
| 79 |
+
parser.add_argument(
|
| 80 |
+
"--device",
|
| 81 |
+
type=str,
|
| 82 |
+
default="cuda",
|
| 83 |
+
help="Device (cuda/cpu, mặc định: cuda)",
|
| 84 |
+
)
|
| 85 |
+
parser.add_argument(
|
| 86 |
+
"--precision",
|
| 87 |
+
type=str,
|
| 88 |
+
default="bf16",
|
| 89 |
+
choices=["fp32", "amp", "bf16"],
|
| 90 |
+
help="Precision (mặc định: bf16)",
|
| 91 |
+
)
|
| 92 |
+
parser.add_argument(
|
| 93 |
+
"--grad_accum_steps",
|
| 94 |
+
type=int,
|
| 95 |
+
default=1,
|
| 96 |
+
help="Gradient accumulation steps (mặc định: 1)",
|
| 97 |
+
)
|
| 98 |
+
parser.add_argument(
|
| 99 |
+
"--warmup_steps",
|
| 100 |
+
type=int,
|
| 101 |
+
default=4000,
|
| 102 |
+
help="Warmup steps (mặc định: 4000 steps)",
|
| 103 |
+
)
|
| 104 |
+
parser.add_argument(
|
| 105 |
+
"--target_max_lr",
|
| 106 |
+
type=float,
|
| 107 |
+
default=3e-4,
|
| 108 |
+
help="Đỉnh learning rate mong muốn cho scheduler (mặc định: 3e-4)",
|
| 109 |
+
)
|
| 110 |
+
parser.add_argument(
|
| 111 |
+
"--label_smoothing",
|
| 112 |
+
type=float,
|
| 113 |
+
default=0.1,
|
| 114 |
+
help="Label smoothing (mặc định: 0.1)",
|
| 115 |
+
)
|
| 116 |
+
parser.add_argument(
|
| 117 |
+
"--checkpoint_dir",
|
| 118 |
+
type=str,
|
| 119 |
+
default=None,
|
| 120 |
+
help="Checkpoint directory (mặc định: checkpoints)",
|
| 121 |
+
)
|
| 122 |
+
parser.add_argument(
|
| 123 |
+
"--resume_from",
|
| 124 |
+
type=str,
|
| 125 |
+
default=None,
|
| 126 |
+
help="Resume từ checkpoint (đường dẫn đến file .pt)",
|
| 127 |
+
)
|
| 128 |
+
parser.add_argument(
|
| 129 |
+
"--no_compile",
|
| 130 |
+
action="store_true",
|
| 131 |
+
help="Tắt torch.compile (mặc định: BẬT để tăng tốc)",
|
| 132 |
+
)
|
| 133 |
+
parser.add_argument(
|
| 134 |
+
"--num_workers",
|
| 135 |
+
type=int,
|
| 136 |
+
default=None,
|
| 137 |
+
help="Số workers cho DataLoader (mặc định: 2 cho CUDA, 0 cho CPU)",
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
args = parser.parse_args()
|
| 141 |
+
|
| 142 |
+
# Device
|
| 143 |
+
device = torch.device(args.device)
|
| 144 |
+
if device.type == 'cuda' and not torch.cuda.is_available():
|
| 145 |
+
print("⚠️ CUDA không khả dụng, dùng CPU")
|
| 146 |
+
device = torch.device('cpu')
|
| 147 |
+
|
| 148 |
+
print("="*70)
|
| 149 |
+
print("TRAIN MODEL VỚI MTET BIDIRECTIONAL DATASET")
|
| 150 |
+
print("="*70)
|
| 151 |
+
print(f"Device: {device}")
|
| 152 |
+
print(f"Model size: {args.model_size}")
|
| 153 |
+
|
| 154 |
+
# Kiểm tra GPU memory và điều chỉnh batch size nếu cần
|
| 155 |
+
if device.type == 'cuda':
|
| 156 |
+
clear_gpu_cache() # Xóa cache trước khi kiểm tra
|
| 157 |
+
mem_info = get_gpu_memory_info()
|
| 158 |
+
if mem_info:
|
| 159 |
+
print(f"\n📊 GPU Memory Info:")
|
| 160 |
+
print(f" Total: {mem_info['total']:.2f} GB")
|
| 161 |
+
print(f" Free: {mem_info['free']:.2f} GB")
|
| 162 |
+
print(f" Usage: {mem_info['usage_percent']:.1f}%")
|
| 163 |
+
|
| 164 |
+
# Kiểm tra và điều chỉnh batch size
|
| 165 |
+
safe_batch_size, warning = check_and_adjust_batch_size(args.batch_size)
|
| 166 |
+
if warning:
|
| 167 |
+
print(f"\n{warning}")
|
| 168 |
+
response = input(f"\n❓ Có muốn giảm batch_size xuống {safe_batch_size} không? (y/n, mặc định: n): ").strip().lower()
|
| 169 |
+
if response == 'y':
|
| 170 |
+
args.batch_size = safe_batch_size
|
| 171 |
+
print(f"✓ Đã giảm batch_size xuống {safe_batch_size}")
|
| 172 |
+
else:
|
| 173 |
+
print(f"⚠️ Giữ nguyên batch_size={args.batch_size}, có thể gặp OOM")
|
| 174 |
+
else:
|
| 175 |
+
print(f"✓ Batch size {args.batch_size} an toàn với GPU memory hiện tại")
|
| 176 |
+
|
| 177 |
+
print(f"\nBatch size: {args.batch_size}")
|
| 178 |
+
print(f"Gradient accumulation: {args.grad_accum_steps}")
|
| 179 |
+
effective_batch = args.batch_size * args.grad_accum_steps
|
| 180 |
+
print(f"Effective batch size: {effective_batch} ({args.batch_size} × {args.grad_accum_steps})")
|
| 181 |
+
print(f"Epochs: {args.num_epochs}")
|
| 182 |
+
print(f"Precision: {args.precision}")
|
| 183 |
+
print()
|
| 184 |
+
print("🔄 BIDIRECTIONAL TRAINING:")
|
| 185 |
+
print(" Model sẽ học ĐỒNG THỜI cả vi→en và en→vi")
|
| 186 |
+
print(" Dataset đã hỗn hợp cả hai chiều (~50% mỗi chiều)")
|
| 187 |
+
print("="*70 + "\n")
|
| 188 |
+
|
| 189 |
+
# Load vocab info
|
| 190 |
+
print("📚 Loading vocabulary...")
|
| 191 |
+
vocab_info = load_shared_vocab_info()
|
| 192 |
+
vocab_size = vocab_info['vocab_size']
|
| 193 |
+
pad_idx = vocab_info['pad_id']
|
| 194 |
+
print(f"✓ Vocab size: {vocab_size:,}")
|
| 195 |
+
print(f"✓ Pad ID: {pad_idx}\n")
|
| 196 |
+
|
| 197 |
+
# Load data
|
| 198 |
+
print("📂 Loading data...")
|
| 199 |
+
processed_data = load_mtet_bidirectional_data()
|
| 200 |
+
|
| 201 |
+
print(f"✓ Train: {len(processed_data['train']['src']):,} cặp câu")
|
| 202 |
+
print(f"✓ Validation: {len(processed_data['validation']['src']):,} cặp câu")
|
| 203 |
+
print(f"✓ Test: {len(processed_data['test']['src']):,} cặp câu\n")
|
| 204 |
+
|
| 205 |
+
# Create dataloaders
|
| 206 |
+
print("🔄 Creating dataloaders...")
|
| 207 |
+
# Tối ưu num_workers: 8 quá nhiều gây overhead, dùng 2-4 thay vì
|
| 208 |
+
# Với dataset lớn (7M samples), num_workers cao gây lag do context switching
|
| 209 |
+
if args.num_workers is None:
|
| 210 |
+
num_workers = 2 if device.type == 'cuda' else 0 # Mặc định: 2 cho CUDA
|
| 211 |
+
else:
|
| 212 |
+
num_workers = args.num_workers
|
| 213 |
+
train_loader, val_loader, test_loader = create_dataloaders_with_bucketing(
|
| 214 |
+
processed_data,
|
| 215 |
+
batch_size=args.batch_size,
|
| 216 |
+
num_workers=num_workers,
|
| 217 |
+
)
|
| 218 |
+
print(f"✓ Dataloaders created với BucketSampler (num_workers={num_workers})\n")
|
| 219 |
+
|
| 220 |
+
# Create model
|
| 221 |
+
print("🔨 Creating model...")
|
| 222 |
+
model_config = get_model_config(args.model_size)
|
| 223 |
+
|
| 224 |
+
model = TransformerShared(
|
| 225 |
+
vocab_size=vocab_size,
|
| 226 |
+
d_model=model_config['d_model'],
|
| 227 |
+
n_layers=model_config['n_layers'],
|
| 228 |
+
n_heads=model_config['n_heads'],
|
| 229 |
+
d_ff=model_config['d_ff'],
|
| 230 |
+
dropout=model_config['dropout'],
|
| 231 |
+
pad_idx=pad_idx,
|
| 232 |
+
use_weight_tying=True
|
| 233 |
+
)
|
| 234 |
+
|
| 235 |
+
model = model.to(device)
|
| 236 |
+
|
| 237 |
+
# Tối ưu: Compile model với torch.compile (PyTorch 2.0+)
|
| 238 |
+
# LƯU Ý: torch.compile KHÔNG tương thích với gradient accumulation
|
| 239 |
+
# Nếu grad_accum_steps > 1, tự động tắt compile để tránh lỗi CUDAGraphs
|
| 240 |
+
compile_model = not args.no_compile and args.grad_accum_steps == 1
|
| 241 |
+
if compile_model and hasattr(torch, 'compile') and device.type == 'cuda':
|
| 242 |
+
print("🔧 Compiling model với torch.compile để tăng tốc...")
|
| 243 |
+
print(" ⚠️ Lưu ý: Có thể chậm ở đầu training (~10-20% epoch đầu)")
|
| 244 |
+
print(" 💡 Tăng tốc ~20-30% sau khi compile xong")
|
| 245 |
+
try:
|
| 246 |
+
# Dùng mode='default' thay vì 'reduce-overhead' để giảm memory
|
| 247 |
+
model = torch.compile(model, mode='default')
|
| 248 |
+
print("✓ Model đã được compile (mode='default' để giảm memory)\n")
|
| 249 |
+
except Exception as e:
|
| 250 |
+
print(f"⚠️ Không thể compile model: {e}")
|
| 251 |
+
print(" Tiếp tục với model không compile\n")
|
| 252 |
+
else:
|
| 253 |
+
if args.grad_accum_steps > 1:
|
| 254 |
+
print("⚠️ torch.compile đã tắt (không tương thích với gradient accumulation)\n")
|
| 255 |
+
elif args.no_compile:
|
| 256 |
+
print("⚠️ torch.compile đã tắt (--no_compile)\n")
|
| 257 |
+
else:
|
| 258 |
+
print("⚠️ torch.compile không khả dụng\n")
|
| 259 |
+
|
| 260 |
+
# Count parameters
|
| 261 |
+
total_params = sum(p.numel() for p in model.parameters())
|
| 262 |
+
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
|
| 263 |
+
model_size_mb = total_params * 4 / (1024**2) # float32
|
| 264 |
+
print(f"✓ Model created")
|
| 265 |
+
print(f" - Total parameters: {total_params:,}")
|
| 266 |
+
print(f" - Trainable parameters: {trainable_params:,}")
|
| 267 |
+
print(f" - Model size: ~{model_size_mb:.2f} MB (float32)")
|
| 268 |
+
|
| 269 |
+
# Kiểm tra lại GPU memory sau khi load model
|
| 270 |
+
if device.type == 'cuda':
|
| 271 |
+
clear_gpu_cache()
|
| 272 |
+
mem_info = get_gpu_memory_info()
|
| 273 |
+
if mem_info:
|
| 274 |
+
print(f"\n📊 GPU Memory sau khi load model:")
|
| 275 |
+
print(f" Allocated: {mem_info['allocated']:.2f} GB")
|
| 276 |
+
print(f" Reserved: {mem_info['reserved']:.2f} GB")
|
| 277 |
+
print(f" Free: {mem_info['free']:.2f} GB")
|
| 278 |
+
print(f" Usage: {mem_info['usage_percent']:.1f}%")
|
| 279 |
+
|
| 280 |
+
# Kiểm tra lại batch size sau khi load model
|
| 281 |
+
safe_batch_size, warning = check_and_adjust_batch_size(args.batch_size)
|
| 282 |
+
if warning and safe_batch_size < args.batch_size:
|
| 283 |
+
print(f"\n{warning}")
|
| 284 |
+
print(f"⚠️ Khuyến nghị: Giảm batch_size xuống {safe_batch_size} để tránh OOM")
|
| 285 |
+
print(f" Hoặc tiếp tục với batch_size={args.batch_size} (có thể gặp OOM)")
|
| 286 |
+
|
| 287 |
+
print()
|
| 288 |
+
|
| 289 |
+
# Checkpoint directory
|
| 290 |
+
checkpoint_dir = Path(args.checkpoint_dir) if args.checkpoint_dir else CHECKPOINT_DIR
|
| 291 |
+
checkpoint_dir.mkdir(parents=True, exist_ok=True)
|
| 292 |
+
|
| 293 |
+
# Resume từ checkpoint nếu có
|
| 294 |
+
resume_from = None
|
| 295 |
+
if args.resume_from:
|
| 296 |
+
resume_path = Path(args.resume_from)
|
| 297 |
+
if resume_path.exists():
|
| 298 |
+
resume_from = str(resume_path)
|
| 299 |
+
print(f"🔄 Sẽ resume từ: {resume_from}")
|
| 300 |
+
else:
|
| 301 |
+
print(f"⚠️ Không tìm thấy checkpoint: {resume_from}")
|
| 302 |
+
print(" Sẽ train từ đầu")
|
| 303 |
+
else:
|
| 304 |
+
# Tự động tìm checkpoint mới nhất nếu có
|
| 305 |
+
checkpoints = list(checkpoint_dir.glob('checkpoint_epoch_*.pt'))
|
| 306 |
+
if checkpoints:
|
| 307 |
+
latest = max(checkpoints, key=lambda p: int(p.stem.split('_')[-1]))
|
| 308 |
+
print(f"💡 Tìm thấy checkpoint cũ: {latest.name}")
|
| 309 |
+
print(f" Để resume, dùng: --resume_from {latest}")
|
| 310 |
+
print(f" Hoặc xóa checkpoints cũ để train từ đầu\n")
|
| 311 |
+
|
| 312 |
+
# Clear cache trước khi training
|
| 313 |
+
if device.type == 'cuda':
|
| 314 |
+
clear_gpu_cache()
|
| 315 |
+
print("✓ Đã clear GPU cache trước khi training\n")
|
| 316 |
+
|
| 317 |
+
# Train
|
| 318 |
+
print("🚀 Starting training...")
|
| 319 |
+
print("="*70 + "\n")
|
| 320 |
+
|
| 321 |
+
history = train_model(
|
| 322 |
+
model=model,
|
| 323 |
+
train_loader=train_loader,
|
| 324 |
+
val_loader=val_loader,
|
| 325 |
+
num_epochs=args.num_epochs,
|
| 326 |
+
device=device,
|
| 327 |
+
d_model=model_config["d_model"],
|
| 328 |
+
warmup_steps=args.warmup_steps,
|
| 329 |
+
label_smoothing=args.label_smoothing,
|
| 330 |
+
grad_accum_steps=args.grad_accum_steps,
|
| 331 |
+
precision=args.precision,
|
| 332 |
+
checkpoint_dir=checkpoint_dir,
|
| 333 |
+
save_every=1,
|
| 334 |
+
resume_from=resume_from,
|
| 335 |
+
target_max_lr=args.target_max_lr,
|
| 336 |
+
)
|
| 337 |
+
|
| 338 |
+
print("\n" + "="*70)
|
| 339 |
+
print("✅ TRAINING HOÀN TẤT!")
|
| 340 |
+
print("="*70)
|
| 341 |
+
print(f"Checkpoints được lưu tại: {checkpoint_dir}")
|
| 342 |
+
print(f"Best model: {checkpoint_dir / 'best_model.pt'}")
|
| 343 |
+
print("="*70)
|
| 344 |
+
|
| 345 |
+
if __name__ == '__main__':
|
| 346 |
+
main()
|
| 347 |
+
|
src/training_module.py
ADDED
|
@@ -0,0 +1,771 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
"""
|
| 4 |
+
PHẦN C: HUẤN LUYỆN VÀ ĐÁNH GIÁ (FIXED)
|
| 5 |
+
Training loop, Loss function, Optimizer, Learning Rate Scheduler
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
import torch.nn as nn
|
| 10 |
+
import torch.optim as optim
|
| 11 |
+
from torch.optim.lr_scheduler import _LRScheduler
|
| 12 |
+
from torch.cuda.amp import GradScaler
|
| 13 |
+
import math
|
| 14 |
+
import time
|
| 15 |
+
from tqdm import tqdm
|
| 16 |
+
import matplotlib.pyplot as plt
|
| 17 |
+
from pathlib import Path
|
| 18 |
+
|
| 19 |
+
# ============================================================================
|
| 20 |
+
# 1. LABEL SMOOTHING CROSS ENTROPY LOSS (FIXED)
|
| 21 |
+
# ============================================================================
|
| 22 |
+
|
| 23 |
+
class LabelSmoothingLoss(nn.Module):
|
| 24 |
+
"""
|
| 25 |
+
Label Smoothing Cross Entropy Loss
|
| 26 |
+
|
| 27 |
+
Làm mịn nhãn để tránh overfitting:
|
| 28 |
+
- True label: 1 - smoothing
|
| 29 |
+
- Other labels: smoothing / (vocab_size - 1)
|
| 30 |
+
|
| 31 |
+
Args:
|
| 32 |
+
vocab_size: Kích thước vocabulary
|
| 33 |
+
pad_idx: Index của padding token (không tính loss)
|
| 34 |
+
smoothing: Label smoothing factor (mặc định 0.1)
|
| 35 |
+
"""
|
| 36 |
+
def __init__(self, vocab_size, pad_idx=0, smoothing=0.1):
|
| 37 |
+
super().__init__()
|
| 38 |
+
|
| 39 |
+
self.vocab_size = vocab_size
|
| 40 |
+
self.pad_idx = pad_idx
|
| 41 |
+
self.smoothing = smoothing
|
| 42 |
+
self.confidence = 1.0 - smoothing
|
| 43 |
+
|
| 44 |
+
def forward(self, pred, target):
|
| 45 |
+
"""
|
| 46 |
+
Args:
|
| 47 |
+
pred: Predictions [batch_size, seq_len, vocab_size]
|
| 48 |
+
target: Target labels [batch_size, seq_len]
|
| 49 |
+
|
| 50 |
+
Returns:
|
| 51 |
+
loss: Scalar loss value
|
| 52 |
+
"""
|
| 53 |
+
batch_size, seq_len, vocab_size = pred.size()
|
| 54 |
+
|
| 55 |
+
# Reshape - FIXED: use .reshape() instead of .view() for non-contiguous tensors
|
| 56 |
+
pred = pred.reshape(-1, vocab_size) # [batch*seq_len, vocab_size]
|
| 57 |
+
target = target.reshape(-1) # [batch*seq_len]
|
| 58 |
+
|
| 59 |
+
# Log softmax
|
| 60 |
+
log_probs = torch.log_softmax(pred, dim=-1)
|
| 61 |
+
|
| 62 |
+
# Tạo smoothed labels
|
| 63 |
+
smoothed_targets = torch.zeros_like(log_probs)
|
| 64 |
+
smoothed_targets.fill_(self.smoothing / (vocab_size - 2)) # -2 vì trừ true label và pad
|
| 65 |
+
smoothed_targets.scatter_(1, target.unsqueeze(1), self.confidence)
|
| 66 |
+
smoothed_targets[:, self.pad_idx] = 0 # Không tính loss cho padding
|
| 67 |
+
|
| 68 |
+
# Mask padding
|
| 69 |
+
mask = (target != self.pad_idx).float()
|
| 70 |
+
|
| 71 |
+
# Tính loss
|
| 72 |
+
loss = -(smoothed_targets * log_probs).sum(dim=-1)
|
| 73 |
+
loss = (loss * mask).sum() / mask.sum()
|
| 74 |
+
|
| 75 |
+
return loss
|
| 76 |
+
|
| 77 |
+
# ============================================================================
|
| 78 |
+
# 2. LEARNING RATE SCHEDULER - WARMUP + DECAY
|
| 79 |
+
# ============================================================================
|
| 80 |
+
|
| 81 |
+
class TransformerLRScheduler(_LRScheduler):
|
| 82 |
+
"""
|
| 83 |
+
Learning Rate Scheduler với Warmup + Decay cho Adam optimizer
|
| 84 |
+
|
| 85 |
+
lr = d_model^(-0.5) * min(step^(-0.5), step * warmup_steps^(-1.5)) * decay_factor
|
| 86 |
+
|
| 87 |
+
Args:
|
| 88 |
+
optimizer: PyTorch optimizer
|
| 89 |
+
d_model: Dimension của model
|
| 90 |
+
warmup_steps: Số steps warmup (mặc định 4000)
|
| 91 |
+
factor: Scaling factor (điều chỉnh để đạt max LR mong muốn)
|
| 92 |
+
decay_factor: Decay factor sau warmup (mặc định 0.8, giảm 20% mỗi epoch)
|
| 93 |
+
"""
|
| 94 |
+
def __init__(self, optimizer, d_model, warmup_steps=10000, factor=0.98, decay_factor=0.98):
|
| 95 |
+
self.d_model = d_model
|
| 96 |
+
self.warmup_steps = warmup_steps
|
| 97 |
+
# factor chỉnh để peak LR ≈ 0.0005 (5e-4)
|
| 98 |
+
# Tính toán: factor = 0.0005 / (384^(-0.5) * 10000^(-0.5)) ≈ 0.98
|
| 99 |
+
self.factor = factor
|
| 100 |
+
self.decay_factor = decay_factor # Giảm từ 0.95 → 0.98 (decay chậm hơn)
|
| 101 |
+
self.num_steps = 0
|
| 102 |
+
self.last_epoch = 0
|
| 103 |
+
|
| 104 |
+
super().__init__(optimizer)
|
| 105 |
+
|
| 106 |
+
def get_lr(self):
|
| 107 |
+
"""
|
| 108 |
+
Tính learning rate cho step hiện tại với decay
|
| 109 |
+
"""
|
| 110 |
+
self.num_steps += 1
|
| 111 |
+
|
| 112 |
+
# Base Transformer schedule
|
| 113 |
+
base_lr = self.factor * (
|
| 114 |
+
self.d_model ** (-0.5) *
|
| 115 |
+
min(self.num_steps ** (-0.5),
|
| 116 |
+
self.num_steps * self.warmup_steps ** (-1.5))
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
# Apply decay after warmup (giảm dần sau khi warmup xong)
|
| 120 |
+
# Decay interval: warmup_steps (thay vì warmup_steps//2) để decay chậm hơn
|
| 121 |
+
if self.num_steps > self.warmup_steps:
|
| 122 |
+
decay_steps = (self.num_steps - self.warmup_steps) // self.warmup_steps
|
| 123 |
+
lr = base_lr * (self.decay_factor ** decay_steps)
|
| 124 |
+
else:
|
| 125 |
+
lr = base_lr
|
| 126 |
+
|
| 127 |
+
return [lr for _ in self.base_lrs]
|
| 128 |
+
|
| 129 |
+
# ============================================================================
|
| 130 |
+
# 3. PERPLEXITY METRIC
|
| 131 |
+
# ============================================================================
|
| 132 |
+
|
| 133 |
+
def calculate_perplexity(loss):
|
| 134 |
+
"""
|
| 135 |
+
Tính Perplexity từ loss
|
| 136 |
+
|
| 137 |
+
Perplexity = exp(loss)
|
| 138 |
+
|
| 139 |
+
Args:
|
| 140 |
+
loss: Cross-entropy loss
|
| 141 |
+
|
| 142 |
+
Returns:
|
| 143 |
+
perplexity: Perplexity score
|
| 144 |
+
"""
|
| 145 |
+
return math.exp(min(loss, 100)) # Cap để tránh overflow
|
| 146 |
+
|
| 147 |
+
# ============================================================================
|
| 148 |
+
# 4. TRAINING FUNCTION
|
| 149 |
+
# ============================================================================
|
| 150 |
+
|
| 151 |
+
def train_epoch(
|
| 152 |
+
model,
|
| 153 |
+
train_loader,
|
| 154 |
+
optimizer,
|
| 155 |
+
scheduler,
|
| 156 |
+
criterion,
|
| 157 |
+
device,
|
| 158 |
+
epoch,
|
| 159 |
+
grad_accum_steps=1,
|
| 160 |
+
use_amp=False,
|
| 161 |
+
use_bf16=False,
|
| 162 |
+
scaler: GradScaler | None = None,
|
| 163 |
+
):
|
| 164 |
+
"""
|
| 165 |
+
Train một epoch
|
| 166 |
+
|
| 167 |
+
Args:
|
| 168 |
+
model: Transformer model
|
| 169 |
+
train_loader: Training DataLoader
|
| 170 |
+
optimizer: Optimizer
|
| 171 |
+
scheduler: Learning rate scheduler
|
| 172 |
+
criterion: Loss function
|
| 173 |
+
device: Device (cuda/cpu)
|
| 174 |
+
epoch: Epoch number
|
| 175 |
+
use_amp: Use float16 mixed precision (requires GradScaler)
|
| 176 |
+
use_bf16: Use bfloat16 mixed precision (no GradScaler needed)
|
| 177 |
+
scaler: GradScaler for float16 (not needed for bf16)
|
| 178 |
+
|
| 179 |
+
Returns:
|
| 180 |
+
avg_loss: Average loss
|
| 181 |
+
avg_perplexity: Average perplexity
|
| 182 |
+
"""
|
| 183 |
+
model.train()
|
| 184 |
+
|
| 185 |
+
total_loss = 0
|
| 186 |
+
total_tokens = 0
|
| 187 |
+
|
| 188 |
+
progress_bar = tqdm(train_loader, desc=f'Epoch {epoch}', ncols=120, file=None, dynamic_ncols=False)
|
| 189 |
+
|
| 190 |
+
optimizer.zero_grad(set_to_none=True)
|
| 191 |
+
# Chỉ dùng GradScaler cho float16, không cần cho bf16
|
| 192 |
+
if use_amp and not use_bf16:
|
| 193 |
+
scaler = scaler or GradScaler(enabled=True)
|
| 194 |
+
num_batches = len(train_loader)
|
| 195 |
+
|
| 196 |
+
# Import GPU safety functions
|
| 197 |
+
try:
|
| 198 |
+
from src.gpu_safety import clear_gpu_cache, check_memory_spike
|
| 199 |
+
except ImportError:
|
| 200 |
+
def clear_gpu_cache():
|
| 201 |
+
if torch.cuda.is_available():
|
| 202 |
+
torch.cuda.empty_cache()
|
| 203 |
+
def check_memory_spike(threshold=0.9):
|
| 204 |
+
return False, None
|
| 205 |
+
|
| 206 |
+
skipped_batches = 0
|
| 207 |
+
skipped_long_seq = 0 # Đếm riêng skip do sequence quá dài
|
| 208 |
+
skipped_oom = 0 # Đếm riêng skip do OOM
|
| 209 |
+
max_skipped_batches = 20 # Giới hạn tổng số batch skip
|
| 210 |
+
max_skipped_long_seq = 15 # Giới hạn skip do sequence dài
|
| 211 |
+
processed_batches = 0
|
| 212 |
+
|
| 213 |
+
for batch_idx, (src, tgt, src_len, tgt_len) in enumerate(progress_bar):
|
| 214 |
+
# Kiểm tra giới hạn skip để tránh vòng lặp vô tận
|
| 215 |
+
if skipped_batches >= max_skipped_batches:
|
| 216 |
+
print(f"\n⚠️ Đã skip {skipped_batches} batches (giới hạn: {max_skipped_batches})")
|
| 217 |
+
print(f" - Skip do sequence dài: {skipped_long_seq}")
|
| 218 |
+
print(f" - Skip do OOM: {skipped_oom}")
|
| 219 |
+
print(" Dừng training để tránh vòng lặp vô tận")
|
| 220 |
+
break
|
| 221 |
+
|
| 222 |
+
# Kiểm tra nếu skip quá nhiều do sequence dài
|
| 223 |
+
if skipped_long_seq >= max_skipped_long_seq:
|
| 224 |
+
print(f"\n⚠️ Đã skip {skipped_long_seq} batches do sequence quá dài (giới hạn: {max_skipped_long_seq})")
|
| 225 |
+
print(" Khuyến nghị: Kiểm tra lại dataset hoặc tăng max_seq_len")
|
| 226 |
+
break
|
| 227 |
+
|
| 228 |
+
try:
|
| 229 |
+
# Kiểm tra memory spike trước khi xử lý batch
|
| 230 |
+
if device.type == 'cuda' and batch_idx % 100 == 0:
|
| 231 |
+
is_spike, mem_info = check_memory_spike(threshold=0.9)
|
| 232 |
+
if is_spike and mem_info:
|
| 233 |
+
print(f"\n⚠️ GPU Memory spike detected: {mem_info['usage_percent']:.1f}%")
|
| 234 |
+
print(f" Free: {mem_info['free']:.2f}GB")
|
| 235 |
+
clear_gpu_cache()
|
| 236 |
+
|
| 237 |
+
# Kiểm tra batch size quá lớn (sequence dài)
|
| 238 |
+
max_seq_len = max(src.size(1), tgt.size(1))
|
| 239 |
+
if max_seq_len > 200: # Batch có sequence quá dài
|
| 240 |
+
skipped_batches += 1
|
| 241 |
+
skipped_long_seq += 1
|
| 242 |
+
if skipped_long_seq % 5 == 0: # Log mỗi 5 batches
|
| 243 |
+
print(f"\n⚠️ Đã skip {skipped_long_seq} batches do sequence quá dài (>200 tokens)")
|
| 244 |
+
continue
|
| 245 |
+
|
| 246 |
+
# Move to device
|
| 247 |
+
src = src.to(device)
|
| 248 |
+
tgt = tgt.to(device)
|
| 249 |
+
|
| 250 |
+
# Target input (bỏ token cuối) và output (bỏ token đầu)
|
| 251 |
+
tgt_input = tgt[:, :-1]
|
| 252 |
+
tgt_output = tgt[:, 1:]
|
| 253 |
+
|
| 254 |
+
# Autocast với dtype phù hợp (API mới torch.amp.autocast)
|
| 255 |
+
if use_bf16:
|
| 256 |
+
with torch.amp.autocast('cuda', enabled=True, dtype=torch.bfloat16):
|
| 257 |
+
output = model(src, tgt_input)
|
| 258 |
+
loss = criterion(output, tgt_output)
|
| 259 |
+
loss_to_backward = loss / grad_accum_steps
|
| 260 |
+
elif use_amp:
|
| 261 |
+
with torch.amp.autocast('cuda', enabled=True):
|
| 262 |
+
output = model(src, tgt_input)
|
| 263 |
+
loss = criterion(output, tgt_output)
|
| 264 |
+
loss_to_backward = loss / grad_accum_steps
|
| 265 |
+
else:
|
| 266 |
+
output = model(src, tgt_input)
|
| 267 |
+
loss = criterion(output, tgt_output)
|
| 268 |
+
loss_to_backward = loss / grad_accum_steps
|
| 269 |
+
|
| 270 |
+
# Backward pass
|
| 271 |
+
if use_amp and not use_bf16:
|
| 272 |
+
scaler.scale(loss_to_backward).backward()
|
| 273 |
+
else:
|
| 274 |
+
loss_to_backward.backward()
|
| 275 |
+
|
| 276 |
+
should_step = (
|
| 277 |
+
((batch_idx + 1) % grad_accum_steps == 0) or (batch_idx + 1 == num_batches)
|
| 278 |
+
)
|
| 279 |
+
|
| 280 |
+
if should_step:
|
| 281 |
+
if use_amp and not use_bf16:
|
| 282 |
+
scaler.unscale_(optimizer)
|
| 283 |
+
# Gradient clipping mạnh tay hơn để giảm loss spikes
|
| 284 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=0.5)
|
| 285 |
+
|
| 286 |
+
if use_amp and not use_bf16:
|
| 287 |
+
scaler.step(optimizer)
|
| 288 |
+
scaler.update()
|
| 289 |
+
else:
|
| 290 |
+
optimizer.step()
|
| 291 |
+
|
| 292 |
+
scheduler.step()
|
| 293 |
+
optimizer.zero_grad(set_to_none=True)
|
| 294 |
+
|
| 295 |
+
# Statistics
|
| 296 |
+
num_tokens = (tgt_output != criterion.pad_idx).sum().item()
|
| 297 |
+
total_loss += loss.item() * num_tokens
|
| 298 |
+
total_tokens += num_tokens
|
| 299 |
+
processed_batches += 1
|
| 300 |
+
|
| 301 |
+
# Update progress bar
|
| 302 |
+
current_loss = loss.item()
|
| 303 |
+
current_ppl = calculate_perplexity(current_loss)
|
| 304 |
+
current_lr = scheduler.get_last_lr()[0]
|
| 305 |
+
|
| 306 |
+
progress_bar.set_postfix({
|
| 307 |
+
'loss': f'{current_loss:.4f}',
|
| 308 |
+
'ppl': f'{current_ppl:.2f}',
|
| 309 |
+
'lr': f'{current_lr:.6f}',
|
| 310 |
+
'skip': f'{skipped_batches}'
|
| 311 |
+
})
|
| 312 |
+
|
| 313 |
+
except RuntimeError as e:
|
| 314 |
+
# Xử lý OOM error
|
| 315 |
+
error_msg = str(e)
|
| 316 |
+
if 'out of memory' in error_msg.lower() or 'cuda' in error_msg.lower():
|
| 317 |
+
print(f"\n❌ OOM Error tại batch {batch_idx}: {error_msg}")
|
| 318 |
+
print(" Đang xóa cache và skip batch này...")
|
| 319 |
+
|
| 320 |
+
# Xóa cache
|
| 321 |
+
clear_gpu_cache()
|
| 322 |
+
|
| 323 |
+
# Skip batch này
|
| 324 |
+
skipped_batches += 1
|
| 325 |
+
skipped_oom += 1
|
| 326 |
+
|
| 327 |
+
# Reset gradients
|
| 328 |
+
optimizer.zero_grad(set_to_none=True)
|
| 329 |
+
|
| 330 |
+
# Log mỗi 5 OOM errors
|
| 331 |
+
if skipped_oom % 5 == 0:
|
| 332 |
+
print(f"\n⚠️ Đã skip {skipped_oom} batches do OOM")
|
| 333 |
+
|
| 334 |
+
# Sau khi xử lý OOM, skip batch hiện tại
|
| 335 |
+
continue
|
| 336 |
+
continue
|
| 337 |
+
else:
|
| 338 |
+
# Lỗi khác, raise lại
|
| 339 |
+
raise
|
| 340 |
+
|
| 341 |
+
if total_tokens > 0:
|
| 342 |
+
avg_loss = total_loss / total_tokens
|
| 343 |
+
avg_perplexity = calculate_perplexity(avg_loss)
|
| 344 |
+
else:
|
| 345 |
+
avg_loss = float('inf')
|
| 346 |
+
avg_perplexity = float('inf')
|
| 347 |
+
print("\n⚠️ Không có batch nào được xử lý thành công!")
|
| 348 |
+
|
| 349 |
+
# Thống kê chi tiết về skip
|
| 350 |
+
if skipped_batches > 0:
|
| 351 |
+
print(f"\n📊 THỐNG KÊ SKIP BATCHES:")
|
| 352 |
+
print(f" Tổng số batches đã xử lý: {processed_batches}")
|
| 353 |
+
print(f" Tổng số batches đã skip: {skipped_batches}")
|
| 354 |
+
print(f" - Skip do sequence quá dài (>200 tokens): {skipped_long_seq}")
|
| 355 |
+
print(f" - Skip do OOM: {skipped_oom}")
|
| 356 |
+
if processed_batches > 0:
|
| 357 |
+
success_rate = processed_batches / (processed_batches + skipped_batches) * 100
|
| 358 |
+
print(f" Tỷ lệ thành công: {success_rate:.1f}%")
|
| 359 |
+
|
| 360 |
+
return avg_loss, avg_perplexity
|
| 361 |
+
|
| 362 |
+
# ============================================================================
|
| 363 |
+
# 5. VALIDATION FUNCTION
|
| 364 |
+
# ============================================================================
|
| 365 |
+
|
| 366 |
+
def validate(model, val_loader, criterion, device, use_amp=False, use_bf16=False):
|
| 367 |
+
"""
|
| 368 |
+
Đánh giá trên validation set
|
| 369 |
+
|
| 370 |
+
Args:
|
| 371 |
+
model: Transformer model
|
| 372 |
+
val_loader: Validation DataLoader
|
| 373 |
+
criterion: Loss function
|
| 374 |
+
device: Device (cuda/cpu)
|
| 375 |
+
use_amp: Use float16 mixed precision
|
| 376 |
+
use_bf16: Use bfloat16 mixed precision
|
| 377 |
+
|
| 378 |
+
Returns:
|
| 379 |
+
avg_loss: Average loss
|
| 380 |
+
avg_perplexity: Average perplexity
|
| 381 |
+
"""
|
| 382 |
+
# Import GPU safety functions
|
| 383 |
+
try:
|
| 384 |
+
from src.gpu_safety import clear_gpu_cache, check_memory_spike
|
| 385 |
+
except ImportError:
|
| 386 |
+
def clear_gpu_cache():
|
| 387 |
+
if torch.cuda.is_available():
|
| 388 |
+
torch.cuda.empty_cache()
|
| 389 |
+
def check_memory_spike(threshold=0.9):
|
| 390 |
+
return False, None
|
| 391 |
+
|
| 392 |
+
# Clear cache trước khi validation
|
| 393 |
+
if device.type == 'cuda':
|
| 394 |
+
clear_gpu_cache()
|
| 395 |
+
|
| 396 |
+
model.eval()
|
| 397 |
+
|
| 398 |
+
total_loss = 0
|
| 399 |
+
total_tokens = 0
|
| 400 |
+
skipped_batches = 0
|
| 401 |
+
max_skipped_batches = 10 # Giới hạn số batch skip để tránh vòng lặp vô tận
|
| 402 |
+
total_batches = len(val_loader)
|
| 403 |
+
processed_batches = 0
|
| 404 |
+
|
| 405 |
+
with torch.no_grad():
|
| 406 |
+
for batch_idx, (src, tgt, src_len, tgt_len) in enumerate(tqdm(val_loader, desc='Validation', ncols=120, file=None, dynamic_ncols=False)):
|
| 407 |
+
# Kiểm tra nếu đã skip quá nhiều - dừng validation để tránh vòng lặp vô tận
|
| 408 |
+
if skipped_batches >= max_skipped_batches:
|
| 409 |
+
print(f"\n⚠️ Đã skip {skipped_batches} batches trong validation (giới hạn: {max_skipped_batches})")
|
| 410 |
+
print(" Dừng validation để tránh vòng lặp vô tận")
|
| 411 |
+
break
|
| 412 |
+
|
| 413 |
+
# Kiểm tra nếu đã xử lý đủ batches (ít nhất 50% để có kết quả hợp lý)
|
| 414 |
+
if processed_batches > 0 and skipped_batches > 0:
|
| 415 |
+
success_rate = processed_batches / (processed_batches + skipped_batches)
|
| 416 |
+
if success_rate < 0.3: # Nếu tỷ lệ thành công < 30%
|
| 417 |
+
print(f"\n⚠️ Tỷ lệ thành công quá thấp ({success_rate*100:.1f}%)")
|
| 418 |
+
print(" Dừng validation để tránh kết quả không chính xác")
|
| 419 |
+
break
|
| 420 |
+
|
| 421 |
+
try:
|
| 422 |
+
# Kiểm tra memory spike
|
| 423 |
+
if device.type == 'cuda' and batch_idx % 50 == 0:
|
| 424 |
+
is_spike, mem_info = check_memory_spike(threshold=0.9)
|
| 425 |
+
if is_spike and mem_info:
|
| 426 |
+
print(f"\n⚠️ GPU Memory spike trong validation: {mem_info['usage_percent']:.1f}%")
|
| 427 |
+
clear_gpu_cache()
|
| 428 |
+
|
| 429 |
+
# Kiểm tra batch size quá lớn
|
| 430 |
+
max_seq_len = max(src.size(1), tgt.size(1))
|
| 431 |
+
if max_seq_len > 200:
|
| 432 |
+
skipped_batches += 1
|
| 433 |
+
if skipped_batches % 5 == 0:
|
| 434 |
+
print(f"\n⚠️ Đã skip {skipped_batches} batches do sequence quá dài (>200 tokens)")
|
| 435 |
+
continue
|
| 436 |
+
|
| 437 |
+
# Move to device
|
| 438 |
+
src = src.to(device)
|
| 439 |
+
tgt = tgt.to(device)
|
| 440 |
+
|
| 441 |
+
# Target input và output
|
| 442 |
+
tgt_input = tgt[:, :-1]
|
| 443 |
+
tgt_output = tgt[:, 1:]
|
| 444 |
+
|
| 445 |
+
# Forward pass với dtype phù hợp (API mới torch.amp.autocast)
|
| 446 |
+
if use_bf16:
|
| 447 |
+
with torch.amp.autocast('cuda', enabled=True, dtype=torch.bfloat16):
|
| 448 |
+
output = model(src, tgt_input)
|
| 449 |
+
loss = criterion(output, tgt_output)
|
| 450 |
+
elif use_amp:
|
| 451 |
+
with torch.amp.autocast('cuda', enabled=True):
|
| 452 |
+
output = model(src, tgt_input)
|
| 453 |
+
loss = criterion(output, tgt_output)
|
| 454 |
+
else:
|
| 455 |
+
output = model(src, tgt_input)
|
| 456 |
+
loss = criterion(output, tgt_output)
|
| 457 |
+
|
| 458 |
+
# Statistics
|
| 459 |
+
num_tokens = (tgt_output != criterion.pad_idx).sum().item()
|
| 460 |
+
total_loss += loss.item() * num_tokens
|
| 461 |
+
total_tokens += num_tokens
|
| 462 |
+
processed_batches += 1
|
| 463 |
+
|
| 464 |
+
# Update progress log every 100 batches
|
| 465 |
+
current_loss = loss.item()
|
| 466 |
+
current_ppl = calculate_perplexity(current_loss)
|
| 467 |
+
if (batch_idx + 1) % 100 == 0:
|
| 468 |
+
tqdm.write(f"Validation Batch {batch_idx+1}/{total_batches}: loss={current_loss:.4f}, ppl={current_ppl:.2f}")
|
| 469 |
+
|
| 470 |
+
except RuntimeError as e:
|
| 471 |
+
# Xử lý OOM trong validation
|
| 472 |
+
error_msg = str(e)
|
| 473 |
+
if 'out of memory' in error_msg.lower() or 'cuda' in error_msg.lower():
|
| 474 |
+
print(f"\n❌ OOM Error trong validation tại batch {batch_idx}")
|
| 475 |
+
clear_gpu_cache()
|
| 476 |
+
skipped_batches += 1
|
| 477 |
+
if skipped_batches >= max_skipped_batches:
|
| 478 |
+
print(f"\n⚠️ Đã skip {skipped_batches} batches trong validation (giới hạn: {max_skipped_batches})")
|
| 479 |
+
print(" Dừng validation để tránh vòng lặp vô tận")
|
| 480 |
+
break
|
| 481 |
+
continue
|
| 482 |
+
else:
|
| 483 |
+
raise
|
| 484 |
+
|
| 485 |
+
# Kiểm tra kết quả
|
| 486 |
+
if total_tokens > 0:
|
| 487 |
+
avg_loss = total_loss / total_tokens
|
| 488 |
+
avg_perplexity = calculate_perplexity(avg_loss)
|
| 489 |
+
else:
|
| 490 |
+
avg_loss = float('inf')
|
| 491 |
+
avg_perplexity = float('inf')
|
| 492 |
+
print("\n⚠️ Không có batch nào được xử lý thành công trong validation!")
|
| 493 |
+
|
| 494 |
+
if skipped_batches > 0:
|
| 495 |
+
print(f"\n⚠️ Đã skip {skipped_batches}/{total_batches} batches trong validation")
|
| 496 |
+
print(f" Đã xử lý thành công: {processed_batches} batches")
|
| 497 |
+
|
| 498 |
+
return avg_loss, avg_perplexity
|
| 499 |
+
|
| 500 |
+
# ============================================================================
|
| 501 |
+
# 6. TRAINING LOOP
|
| 502 |
+
# ============================================================================
|
| 503 |
+
|
| 504 |
+
def train_model(
|
| 505 |
+
model,
|
| 506 |
+
train_loader,
|
| 507 |
+
val_loader,
|
| 508 |
+
num_epochs,
|
| 509 |
+
device,
|
| 510 |
+
d_model,
|
| 511 |
+
warmup_steps=4000,
|
| 512 |
+
label_smoothing=0.1,
|
| 513 |
+
grad_accum_steps=1,
|
| 514 |
+
precision="amp",
|
| 515 |
+
checkpoint_dir="checkpoints",
|
| 516 |
+
save_every=1,
|
| 517 |
+
resume_from=None,
|
| 518 |
+
target_max_lr: float | None = None,
|
| 519 |
+
):
|
| 520 |
+
"""
|
| 521 |
+
Huấn luyện model hoàn chỉnh
|
| 522 |
+
Args:
|
| 523 |
+
model: Transformer model
|
| 524 |
+
train_loader: Training DataLoader
|
| 525 |
+
val_loader: Validation DataLoader
|
| 526 |
+
num_epochs: Số epochs
|
| 527 |
+
device: Device (cuda/cpu)
|
| 528 |
+
d_model: Model dimension (để setup scheduler)
|
| 529 |
+
warmup_steps: Warmup steps cho scheduler (mặc định: 4000)
|
| 530 |
+
label_smoothing: Label smoothing factor
|
| 531 |
+
checkpoint_dir: Directory để lưu checkpoints
|
| 532 |
+
save_every: Lưu checkpoint mỗi N epochs
|
| 533 |
+
resume_from: Path to checkpoint to resume from (optional)
|
| 534 |
+
|
| 535 |
+
Returns:
|
| 536 |
+
history: Dictionary chứa training history
|
| 537 |
+
"""
|
| 538 |
+
use_amp = precision == 'amp' and device.type == 'cuda'
|
| 539 |
+
use_bf16 = precision == 'bf16' and device.type == 'cuda'
|
| 540 |
+
# Chỉ dùng GradScaler cho float16, bf16 không cần
|
| 541 |
+
scaler = GradScaler(enabled=use_amp and not use_bf16) if use_amp and not use_bf16 else None
|
| 542 |
+
|
| 543 |
+
checkpoint_dir = Path(checkpoint_dir)
|
| 544 |
+
checkpoint_dir.mkdir(parents=True, exist_ok=True)
|
| 545 |
+
|
| 546 |
+
# Setup optimizer
|
| 547 |
+
optimizer = optim.Adam(
|
| 548 |
+
model.parameters(),
|
| 549 |
+
lr=1.0, # Sẽ được điều chỉnh bởi scheduler
|
| 550 |
+
betas=(0.9, 0.98),
|
| 551 |
+
eps=1e-9,
|
| 552 |
+
)
|
| 553 |
+
|
| 554 |
+
# Setup scheduler
|
| 555 |
+
# Nếu người dùng chỉ định target_max_lr, tính factor tương ứng để đạt
|
| 556 |
+
# đỉnh LR xấp xỉ target_max_lr tại cuối warmup.
|
| 557 |
+
if target_max_lr is not None:
|
| 558 |
+
# lr_peak ≈ factor * d_model^(-0.5) * warmup_steps^(-0.5)
|
| 559 |
+
base = (d_model ** -0.5) * (warmup_steps ** -0.5)
|
| 560 |
+
factor = float(target_max_lr) / float(base)
|
| 561 |
+
else:
|
| 562 |
+
# Giữ nguyên behaviour cũ: peak LR ≈ 5e-4 cho d_model=384, warmup=10000
|
| 563 |
+
factor = 0.98
|
| 564 |
+
|
| 565 |
+
scheduler = TransformerLRScheduler(
|
| 566 |
+
optimizer,
|
| 567 |
+
d_model=d_model,
|
| 568 |
+
warmup_steps=warmup_steps,
|
| 569 |
+
factor=factor,
|
| 570 |
+
)
|
| 571 |
+
|
| 572 |
+
# Setup loss function
|
| 573 |
+
criterion = LabelSmoothingLoss(
|
| 574 |
+
vocab_size=model.output_layer.out_features if hasattr(model, 'output_layer') else model.decoder.fc_out.out_features,
|
| 575 |
+
pad_idx=model.pad_idx,
|
| 576 |
+
smoothing=label_smoothing
|
| 577 |
+
)
|
| 578 |
+
|
| 579 |
+
# Training history
|
| 580 |
+
history = {
|
| 581 |
+
'train_loss': [],
|
| 582 |
+
'train_ppl': [],
|
| 583 |
+
'val_loss': [],
|
| 584 |
+
'val_ppl': [],
|
| 585 |
+
'lr': []
|
| 586 |
+
}
|
| 587 |
+
|
| 588 |
+
best_val_loss = float('inf')
|
| 589 |
+
start_epoch = 1
|
| 590 |
+
|
| 591 |
+
# Resume from checkpoint if provided
|
| 592 |
+
if resume_from and Path(resume_from).exists():
|
| 593 |
+
print(f"\n→ Đang tiếp tục từ checkpoint: {resume_from}")
|
| 594 |
+
checkpoint = torch.load(resume_from, map_location=device)
|
| 595 |
+
model.load_state_dict(checkpoint['model_state_dict'])
|
| 596 |
+
optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
|
| 597 |
+
scheduler.load_state_dict(checkpoint['scheduler_state_dict'])
|
| 598 |
+
start_epoch = checkpoint['epoch'] + 1
|
| 599 |
+
history = checkpoint.get('history', history)
|
| 600 |
+
best_val_loss = min(history.get('val_loss', [float('inf')]))
|
| 601 |
+
print(f"→ Tiếp tục từ epoch {start_epoch}")
|
| 602 |
+
print(f"→ Best validation loss trước đó: {best_val_loss:.4f}")
|
| 603 |
+
|
| 604 |
+
print("="*70)
|
| 605 |
+
print("BẮT ĐẦU HUẤN LUYỆN")
|
| 606 |
+
print("="*70)
|
| 607 |
+
print(f"Device: {device}")
|
| 608 |
+
print(f"Number of epochs: {num_epochs}")
|
| 609 |
+
print(f"Starting from epoch: {start_epoch}")
|
| 610 |
+
print(f"Steps per epoch: {len(train_loader):,}")
|
| 611 |
+
print(f"Warmup steps: {warmup_steps:,}")
|
| 612 |
+
print(f"Label smoothing: {label_smoothing}")
|
| 613 |
+
print(f"Grad accumulation steps: {grad_accum_steps}")
|
| 614 |
+
if use_bf16:
|
| 615 |
+
print(f"Precision: BF16 (bfloat16 - recommended for stability)")
|
| 616 |
+
elif use_amp:
|
| 617 |
+
print(f"Precision: AMP (float16 mixed precision)")
|
| 618 |
+
else:
|
| 619 |
+
print(f"Precision: FP32")
|
| 620 |
+
print("="*70 + "\n")
|
| 621 |
+
|
| 622 |
+
for epoch in range(start_epoch, num_epochs + 1):
|
| 623 |
+
epoch_start_time = time.time()
|
| 624 |
+
|
| 625 |
+
# Training
|
| 626 |
+
train_loss, train_ppl = train_epoch(
|
| 627 |
+
model,
|
| 628 |
+
train_loader,
|
| 629 |
+
optimizer,
|
| 630 |
+
scheduler,
|
| 631 |
+
criterion,
|
| 632 |
+
device,
|
| 633 |
+
epoch,
|
| 634 |
+
grad_accum_steps=grad_accum_steps,
|
| 635 |
+
use_amp=use_amp,
|
| 636 |
+
use_bf16=use_bf16,
|
| 637 |
+
scaler=scaler,
|
| 638 |
+
)
|
| 639 |
+
|
| 640 |
+
# Validation
|
| 641 |
+
val_loss, val_ppl = validate(
|
| 642 |
+
model,
|
| 643 |
+
val_loader,
|
| 644 |
+
criterion,
|
| 645 |
+
device,
|
| 646 |
+
use_amp=use_amp,
|
| 647 |
+
use_bf16=use_bf16,
|
| 648 |
+
)
|
| 649 |
+
|
| 650 |
+
epoch_time = time.time() - epoch_start_time
|
| 651 |
+
|
| 652 |
+
# Lưu history
|
| 653 |
+
history['train_loss'].append(train_loss)
|
| 654 |
+
history['train_ppl'].append(train_ppl)
|
| 655 |
+
history['val_loss'].append(val_loss)
|
| 656 |
+
history['val_ppl'].append(val_ppl)
|
| 657 |
+
history['lr'].append(scheduler.get_last_lr()[0])
|
| 658 |
+
|
| 659 |
+
# In kết quả - single line format
|
| 660 |
+
checkpoint_msg = ""
|
| 661 |
+
best_msg = ""
|
| 662 |
+
|
| 663 |
+
# Lưu checkpoint
|
| 664 |
+
if epoch % save_every == 0:
|
| 665 |
+
checkpoint_path = checkpoint_dir / f'checkpoint_epoch_{epoch}.pt'
|
| 666 |
+
torch.save({
|
| 667 |
+
'epoch': epoch,
|
| 668 |
+
'model_state_dict': model.state_dict(),
|
| 669 |
+
'optimizer_state_dict': optimizer.state_dict(),
|
| 670 |
+
'scheduler_state_dict': scheduler.state_dict(),
|
| 671 |
+
'train_loss': train_loss,
|
| 672 |
+
'val_loss': val_loss,
|
| 673 |
+
'history': history
|
| 674 |
+
}, checkpoint_path)
|
| 675 |
+
checkpoint_msg = " | ✓ Saved checkpoint"
|
| 676 |
+
|
| 677 |
+
# Lưu best model
|
| 678 |
+
if val_loss < best_val_loss:
|
| 679 |
+
best_val_loss = val_loss
|
| 680 |
+
best_model_path = checkpoint_dir / 'best_model.pt'
|
| 681 |
+
torch.save({
|
| 682 |
+
'epoch': epoch,
|
| 683 |
+
'model_state_dict': model.state_dict(),
|
| 684 |
+
'val_loss': val_loss,
|
| 685 |
+
'val_ppl': val_ppl
|
| 686 |
+
}, best_model_path)
|
| 687 |
+
best_msg = " | ✓ New best model!"
|
| 688 |
+
|
| 689 |
+
print(f"Epoch {epoch}/{num_epochs} | Time: {epoch_time:.2f}s | Train Loss: {train_loss:.4f} | Train PPL: {train_ppl:.2f} | Val Loss: {val_loss:.4f} | Val PPL: {val_ppl:.2f} | LR: {scheduler.get_last_lr()[0]:.6f}{checkpoint_msg}{best_msg}")
|
| 690 |
+
|
| 691 |
+
print("\n" + "="*70)
|
| 692 |
+
print("HOÀN TẤT HUẤN LUYỆN")
|
| 693 |
+
print("="*70)
|
| 694 |
+
print(f"Best validation loss: {best_val_loss:.4f}")
|
| 695 |
+
print(f"Best validation perplexity: {calculate_perplexity(best_val_loss):.2f}")
|
| 696 |
+
|
| 697 |
+
return history
|
| 698 |
+
|
| 699 |
+
# ============================================================================
|
| 700 |
+
# 7. PLOT TRAINING HISTORY
|
| 701 |
+
# ============================================================================
|
| 702 |
+
|
| 703 |
+
def plot_training_history(history, save_path='training_history.png'):
|
| 704 |
+
"""
|
| 705 |
+
Vẽ đồ thị training history
|
| 706 |
+
|
| 707 |
+
Args:
|
| 708 |
+
history: Training history dictionary
|
| 709 |
+
save_path: Path để lưu hình
|
| 710 |
+
"""
|
| 711 |
+
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
|
| 712 |
+
|
| 713 |
+
epochs = range(1, len(history['train_loss']) + 1)
|
| 714 |
+
|
| 715 |
+
# Plot Loss
|
| 716 |
+
axes[0].plot(epochs, history['train_loss'], 'b-', label='Train Loss')
|
| 717 |
+
axes[0].plot(epochs, history['val_loss'], 'r-', label='Val Loss')
|
| 718 |
+
axes[0].set_xlabel('Epoch')
|
| 719 |
+
axes[0].set_ylabel('Loss')
|
| 720 |
+
axes[0].set_title('Training and Validation Loss')
|
| 721 |
+
axes[0].legend()
|
| 722 |
+
axes[0].grid(True)
|
| 723 |
+
|
| 724 |
+
# Plot Perplexity
|
| 725 |
+
axes[1].plot(epochs, history['train_ppl'], 'b-', label='Train PPL')
|
| 726 |
+
axes[1].plot(epochs, history['val_ppl'], 'r-', label='Val PPL')
|
| 727 |
+
axes[1].set_xlabel('Epoch')
|
| 728 |
+
axes[1].set_ylabel('Perplexity')
|
| 729 |
+
axes[1].set_title('Training and Validation Perplexity')
|
| 730 |
+
axes[1].legend()
|
| 731 |
+
axes[1].grid(True)
|
| 732 |
+
|
| 733 |
+
# Plot Learning Rate
|
| 734 |
+
axes[2].plot(epochs, history['lr'], 'g-')
|
| 735 |
+
axes[2].set_xlabel('Epoch')
|
| 736 |
+
axes[2].set_ylabel('Learning Rate')
|
| 737 |
+
axes[2].set_title('Learning Rate Schedule')
|
| 738 |
+
axes[2].grid(True)
|
| 739 |
+
|
| 740 |
+
save_path = Path(save_path)
|
| 741 |
+
save_path.parent.mkdir(parents=True, exist_ok=True)
|
| 742 |
+
|
| 743 |
+
plt.tight_layout()
|
| 744 |
+
plt.savefig(save_path, dpi=300, bbox_inches='tight')
|
| 745 |
+
print(f"\n✓ Saved training history plot to {save_path}")
|
| 746 |
+
plt.close()
|
| 747 |
+
|
| 748 |
+
# ============================================================================
|
| 749 |
+
# 8. LOAD CHECKPOINT
|
| 750 |
+
# ============================================================================
|
| 751 |
+
|
| 752 |
+
def load_checkpoint(model, checkpoint_path, device):
|
| 753 |
+
"""
|
| 754 |
+
Load checkpoint
|
| 755 |
+
|
| 756 |
+
Args:
|
| 757 |
+
model: Transformer model
|
| 758 |
+
checkpoint_path: Path to checkpoint
|
| 759 |
+
device: Device
|
| 760 |
+
|
| 761 |
+
Returns:
|
| 762 |
+
model: Model with loaded weights
|
| 763 |
+
checkpoint: Checkpoint dictionary
|
| 764 |
+
"""
|
| 765 |
+
checkpoint = torch.load(checkpoint_path, map_location=device)
|
| 766 |
+
model.load_state_dict(checkpoint['model_state_dict'])
|
| 767 |
+
print(f"✓ Loaded checkpoint from {checkpoint_path}")
|
| 768 |
+
print(f" Epoch: {checkpoint.get('epoch', 'N/A')}")
|
| 769 |
+
print(f" Val Loss: {checkpoint.get('val_loss', 'N/A'):.4f}")
|
| 770 |
+
|
| 771 |
+
return model, checkpoint
|
src/transformer_components.py
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
PHẦN B: XÂY DỰNG KIẾN TRÚC TRANSFORMER FROM SCRATCH
|
| 3 |
+
Các thành phần cốt lõi: Attention, Positional Encoding, Encoder, Decoder
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
import torch.nn as nn
|
| 8 |
+
import torch.nn.functional as F
|
| 9 |
+
import math
|
| 10 |
+
|
| 11 |
+
# ============================================================================
|
| 12 |
+
# 1. SCALED DOT-PRODUCT ATTENTION
|
| 13 |
+
# ============================================================================
|
| 14 |
+
|
| 15 |
+
class ScaledDotProductAttention(nn.Module):
|
| 16 |
+
"""
|
| 17 |
+
Scaled Dot-Product Attention
|
| 18 |
+
|
| 19 |
+
Attention(Q, K, V) = softmax(Q·K^T / sqrt(d_k)) · V
|
| 20 |
+
|
| 21 |
+
Args:
|
| 22 |
+
d_k: Dimension của key (để scale)
|
| 23 |
+
"""
|
| 24 |
+
def __init__(self, d_k):
|
| 25 |
+
super().__init__()
|
| 26 |
+
self.d_k = d_k
|
| 27 |
+
self.softmax = nn.Softmax(dim=-1)
|
| 28 |
+
|
| 29 |
+
def forward(self, Q, K, V, mask=None):
|
| 30 |
+
"""
|
| 31 |
+
Args:
|
| 32 |
+
Q: Query [batch_size, n_heads, seq_len, d_k]
|
| 33 |
+
K: Key [batch_size, n_heads, seq_len, d_k]
|
| 34 |
+
V: Value [batch_size, n_heads, seq_len, d_v]
|
| 35 |
+
mask: Mask [batch_size, 1, seq_len, seq_len] hoặc [batch_size, 1, 1, seq_len]
|
| 36 |
+
|
| 37 |
+
Returns:
|
| 38 |
+
output: [batch_size, n_heads, seq_len, d_v]
|
| 39 |
+
attention_weights: [batch_size, n_heads, seq_len, seq_len]
|
| 40 |
+
"""
|
| 41 |
+
# Tính attention scores: Q·K^T
|
| 42 |
+
# [batch, n_heads, seq_len, d_k] @ [batch, n_heads, d_k, seq_len]
|
| 43 |
+
# -> [batch, n_heads, seq_len, seq_len]
|
| 44 |
+
scores = torch.matmul(Q, K.transpose(-2, -1))
|
| 45 |
+
|
| 46 |
+
# Scale bởi sqrt(d_k) để tránh gradient quá nhỏ
|
| 47 |
+
scores = scores / math.sqrt(self.d_k)
|
| 48 |
+
|
| 49 |
+
# Apply mask (nếu có)
|
| 50 |
+
if mask is not None:
|
| 51 |
+
mask = mask.to(dtype=torch.bool)
|
| 52 |
+
neg_inf = torch.finfo(scores.dtype).min
|
| 53 |
+
scores = scores.masked_fill(~mask, neg_inf)
|
| 54 |
+
|
| 55 |
+
# Softmax để có attention weights
|
| 56 |
+
attention_weights = self.softmax(scores)
|
| 57 |
+
|
| 58 |
+
# Apply attention weights to values
|
| 59 |
+
# [batch, n_heads, seq_len, seq_len] @ [batch, n_heads, seq_len, d_v]
|
| 60 |
+
# -> [batch, n_heads, seq_len, d_v]
|
| 61 |
+
output = torch.matmul(attention_weights, V)
|
| 62 |
+
|
| 63 |
+
return output, attention_weights
|
| 64 |
+
|
| 65 |
+
# ============================================================================
|
| 66 |
+
# 2. MULTI-HEAD ATTENTION
|
| 67 |
+
# ============================================================================
|
| 68 |
+
|
| 69 |
+
class MultiHeadAttention(nn.Module):
|
| 70 |
+
"""
|
| 71 |
+
Multi-Head Attention
|
| 72 |
+
|
| 73 |
+
Chia input thành nhiều heads, mỗi head học các representation khác nhau
|
| 74 |
+
|
| 75 |
+
Args:
|
| 76 |
+
d_model: Dimension của model
|
| 77 |
+
n_heads: Số lượng attention heads
|
| 78 |
+
dropout: Dropout rate
|
| 79 |
+
"""
|
| 80 |
+
def __init__(self, d_model, n_heads, dropout=0.1):
|
| 81 |
+
super().__init__()
|
| 82 |
+
|
| 83 |
+
assert d_model % n_heads == 0, "d_model phải chia hết cho n_heads"
|
| 84 |
+
|
| 85 |
+
self.d_model = d_model
|
| 86 |
+
self.n_heads = n_heads
|
| 87 |
+
self.d_k = d_model // n_heads # Dimension của mỗi head
|
| 88 |
+
|
| 89 |
+
# Linear layers để project Q, K, V
|
| 90 |
+
self.W_q = nn.Linear(d_model, d_model)
|
| 91 |
+
self.W_k = nn.Linear(d_model, d_model)
|
| 92 |
+
self.W_v = nn.Linear(d_model, d_model)
|
| 93 |
+
|
| 94 |
+
# Scaled Dot-Product Attention
|
| 95 |
+
self.attention = ScaledDotProductAttention(self.d_k)
|
| 96 |
+
|
| 97 |
+
# Output projection
|
| 98 |
+
self.W_o = nn.Linear(d_model, d_model)
|
| 99 |
+
|
| 100 |
+
self.dropout = nn.Dropout(dropout)
|
| 101 |
+
|
| 102 |
+
def forward(self, Q, K, V, mask=None):
|
| 103 |
+
"""
|
| 104 |
+
Args:
|
| 105 |
+
Q: Query [batch_size, seq_len, d_model]
|
| 106 |
+
K: Key [batch_size, seq_len, d_model]
|
| 107 |
+
V: Value [batch_size, seq_len, d_model]
|
| 108 |
+
mask: Mask tensor
|
| 109 |
+
|
| 110 |
+
Returns:
|
| 111 |
+
output: [batch_size, seq_len, d_model]
|
| 112 |
+
attention_weights: [batch_size, n_heads, seq_len, seq_len]
|
| 113 |
+
"""
|
| 114 |
+
batch_size = Q.size(0)
|
| 115 |
+
|
| 116 |
+
# 1. Linear projection và chia thành multiple heads
|
| 117 |
+
# [batch, seq_len, d_model] -> [batch, seq_len, n_heads, d_k] -> [batch, n_heads, seq_len, d_k]
|
| 118 |
+
Q = self.W_q(Q).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
|
| 119 |
+
K = self.W_k(K).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
|
| 120 |
+
V = self.W_v(V).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
|
| 121 |
+
|
| 122 |
+
# 2. Apply attention
|
| 123 |
+
# output: [batch, n_heads, seq_len, d_k]
|
| 124 |
+
output, attention_weights = self.attention(Q, K, V, mask)
|
| 125 |
+
|
| 126 |
+
# 3. Concatenate heads
|
| 127 |
+
# [batch, n_heads, seq_len, d_k] -> [batch, seq_len, n_heads, d_k] -> [batch, seq_len, d_model]
|
| 128 |
+
output = output.transpose(1, 2).contiguous().view(batch_size, -1, self.d_model)
|
| 129 |
+
|
| 130 |
+
# 4. Final linear projection
|
| 131 |
+
output = self.W_o(output)
|
| 132 |
+
output = self.dropout(output)
|
| 133 |
+
|
| 134 |
+
return output, attention_weights
|
| 135 |
+
|
| 136 |
+
# ============================================================================
|
| 137 |
+
# 3. POSITION-WISE FEED-FORWARD NETWORK
|
| 138 |
+
# ============================================================================
|
| 139 |
+
|
| 140 |
+
class PositionwiseFeedForward(nn.Module):
|
| 141 |
+
"""
|
| 142 |
+
Position-wise Feed-Forward Network
|
| 143 |
+
|
| 144 |
+
FFN(x) = max(0, xW1 + b1)W2 + b2
|
| 145 |
+
|
| 146 |
+
Áp dụng 2 linear transformations với ReLU ở giữa
|
| 147 |
+
|
| 148 |
+
Args:
|
| 149 |
+
d_model: Dimension của model
|
| 150 |
+
d_ff: Dimension của hidden layer (thường = 4 * d_model)
|
| 151 |
+
dropout: Dropout rate
|
| 152 |
+
"""
|
| 153 |
+
def __init__(self, d_model, d_ff, dropout=0.1):
|
| 154 |
+
super().__init__()
|
| 155 |
+
|
| 156 |
+
self.linear1 = nn.Linear(d_model, d_ff)
|
| 157 |
+
self.linear2 = nn.Linear(d_ff, d_model)
|
| 158 |
+
self.dropout = nn.Dropout(dropout)
|
| 159 |
+
|
| 160 |
+
def forward(self, x):
|
| 161 |
+
"""
|
| 162 |
+
Args:
|
| 163 |
+
x: [batch_size, seq_len, d_model]
|
| 164 |
+
|
| 165 |
+
Returns:
|
| 166 |
+
output: [batch_size, seq_len, d_model]
|
| 167 |
+
"""
|
| 168 |
+
# x -> W1 -> ReLU -> Dropout -> W2 -> Dropout
|
| 169 |
+
x = self.linear1(x)
|
| 170 |
+
x = F.relu(x)
|
| 171 |
+
x = self.dropout(x)
|
| 172 |
+
x = self.linear2(x)
|
| 173 |
+
x = self.dropout(x)
|
| 174 |
+
|
| 175 |
+
return x
|
| 176 |
+
|
| 177 |
+
# ============================================================================
|
| 178 |
+
# 4. POSITIONAL ENCODING
|
| 179 |
+
# ============================================================================
|
| 180 |
+
|
| 181 |
+
class PositionalEncoding(nn.Module):
|
| 182 |
+
"""
|
| 183 |
+
Positional Encoding (Sinusoidal)
|
| 184 |
+
|
| 185 |
+
PE(pos, 2i) = sin(pos / 10000^(2i/d_model))
|
| 186 |
+
PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))
|
| 187 |
+
|
| 188 |
+
Thêm thông tin về vị trí của token trong sequence
|
| 189 |
+
|
| 190 |
+
Args:
|
| 191 |
+
d_model: Dimension của model
|
| 192 |
+
max_len: Maximum sequence length
|
| 193 |
+
dropout: Dropout rate
|
| 194 |
+
"""
|
| 195 |
+
def __init__(self, d_model, max_len=5000, dropout=0.1):
|
| 196 |
+
super().__init__()
|
| 197 |
+
|
| 198 |
+
self.dropout = nn.Dropout(dropout)
|
| 199 |
+
|
| 200 |
+
# Tạo positional encoding matrix
|
| 201 |
+
pe = torch.zeros(max_len, d_model)
|
| 202 |
+
position = torch.arange(0, max_len).unsqueeze(1).float()
|
| 203 |
+
|
| 204 |
+
# Tính div_term cho công thức
|
| 205 |
+
div_term = torch.exp(torch.arange(0, d_model, 2).float() *
|
| 206 |
+
-(math.log(10000.0) / d_model))
|
| 207 |
+
|
| 208 |
+
# Apply sin cho các vị trí chẵn, cos cho các vị trí lẻ
|
| 209 |
+
pe[:, 0::2] = torch.sin(position * div_term)
|
| 210 |
+
pe[:, 1::2] = torch.cos(position * div_term)
|
| 211 |
+
|
| 212 |
+
# Thêm batch dimension
|
| 213 |
+
pe = pe.unsqueeze(0) # [1, max_len, d_model]
|
| 214 |
+
|
| 215 |
+
# Register as buffer (không train)
|
| 216 |
+
self.register_buffer('pe', pe)
|
| 217 |
+
|
| 218 |
+
def forward(self, x):
|
| 219 |
+
"""
|
| 220 |
+
Args:
|
| 221 |
+
x: [batch_size, seq_len, d_model]
|
| 222 |
+
|
| 223 |
+
Returns:
|
| 224 |
+
output: [batch_size, seq_len, d_model]
|
| 225 |
+
"""
|
| 226 |
+
seq_len = x.size(1)
|
| 227 |
+
|
| 228 |
+
# Cộng positional encoding
|
| 229 |
+
x = x + self.pe[:, :seq_len, :]
|
| 230 |
+
|
| 231 |
+
return self.dropout(x)
|
| 232 |
+
|
| 233 |
+
# ============================================================================
|
| 234 |
+
# 5. LAYER NORMALIZATION
|
| 235 |
+
# ============================================================================
|
| 236 |
+
|
| 237 |
+
class LayerNorm(nn.Module):
|
| 238 |
+
"""
|
| 239 |
+
Layer Normalization
|
| 240 |
+
|
| 241 |
+
Chuẩn hóa theo dimension cuối (features)
|
| 242 |
+
|
| 243 |
+
Args:
|
| 244 |
+
d_model: Dimension của model
|
| 245 |
+
eps: Epsilon cho numerical stability
|
| 246 |
+
"""
|
| 247 |
+
def __init__(self, d_model, eps=1e-6):
|
| 248 |
+
super().__init__()
|
| 249 |
+
|
| 250 |
+
# Learnable parameters
|
| 251 |
+
self.gamma = nn.Parameter(torch.ones(d_model))
|
| 252 |
+
self.beta = nn.Parameter(torch.zeros(d_model))
|
| 253 |
+
self.eps = eps
|
| 254 |
+
|
| 255 |
+
def forward(self, x):
|
| 256 |
+
"""
|
| 257 |
+
Args:
|
| 258 |
+
x: [batch_size, seq_len, d_model]
|
| 259 |
+
|
| 260 |
+
Returns:
|
| 261 |
+
output: [batch_size, seq_len, d_model]
|
| 262 |
+
"""
|
| 263 |
+
mean = x.mean(-1, keepdim=True)
|
| 264 |
+
std = x.std(-1, keepdim=True)
|
| 265 |
+
|
| 266 |
+
return self.gamma * (x - mean) / (std + self.eps) + self.beta
|
| 267 |
+
|
| 268 |
+
# ============================================================================
|
| 269 |
+
# 6. RESIDUAL CONNECTION
|
| 270 |
+
# ============================================================================
|
| 271 |
+
|
| 272 |
+
class ResidualConnection(nn.Module):
|
| 273 |
+
"""
|
| 274 |
+
Residual Connection với Layer Normalization
|
| 275 |
+
|
| 276 |
+
output = LayerNorm(x + Sublayer(x))
|
| 277 |
+
|
| 278 |
+
Args:
|
| 279 |
+
d_model: Dimension của model
|
| 280 |
+
dropout: Dropout rate
|
| 281 |
+
"""
|
| 282 |
+
def __init__(self, d_model, dropout=0.1):
|
| 283 |
+
super().__init__()
|
| 284 |
+
|
| 285 |
+
self.norm = LayerNorm(d_model)
|
| 286 |
+
self.dropout = nn.Dropout(dropout)
|
| 287 |
+
|
| 288 |
+
def forward(self, x, sublayer):
|
| 289 |
+
"""
|
| 290 |
+
Args:
|
| 291 |
+
x: Input [batch_size, seq_len, d_model]
|
| 292 |
+
sublayer: Function (callable) để apply
|
| 293 |
+
|
| 294 |
+
Returns:
|
| 295 |
+
output: [batch_size, seq_len, d_model]
|
| 296 |
+
"""
|
| 297 |
+
# Residual: x + sublayer(norm(x))
|
| 298 |
+
return x + self.dropout(sublayer(self.norm(x)))
|
| 299 |
+
|
| 300 |
+
# ============================================================================
|
| 301 |
+
# 7. EMBEDDING LAYER
|
| 302 |
+
# ============================================================================
|
| 303 |
+
|
| 304 |
+
class Embedding(nn.Module):
|
| 305 |
+
"""
|
| 306 |
+
Embedding layer với scaling
|
| 307 |
+
|
| 308 |
+
Args:
|
| 309 |
+
vocab_size: Kích thước vocabulary
|
| 310 |
+
d_model: Dimension của model
|
| 311 |
+
"""
|
| 312 |
+
def __init__(self, vocab_size, d_model):
|
| 313 |
+
super().__init__()
|
| 314 |
+
|
| 315 |
+
self.d_model = d_model
|
| 316 |
+
self.embedding = nn.Embedding(vocab_size, d_model)
|
| 317 |
+
|
| 318 |
+
def forward(self, x):
|
| 319 |
+
"""
|
| 320 |
+
Args:
|
| 321 |
+
x: [batch_size, seq_len]
|
| 322 |
+
|
| 323 |
+
Returns:
|
| 324 |
+
output: [batch_size, seq_len, d_model]
|
| 325 |
+
"""
|
| 326 |
+
# Scale embedding by sqrt(d_model) như trong paper
|
| 327 |
+
return self.embedding(x) * math.sqrt(self.d_model)
|
| 328 |
+
|
| 329 |
+
# ============================================================================
|
| 330 |
+
# 8. TEST COMPONENTS
|
| 331 |
+
# ============================================================================
|
| 332 |
+
|
| 333 |
+
if __name__ == "__main__":
|
| 334 |
+
print("="*70)
|
| 335 |
+
print("KIỂM TRA CÁC THÀNH PHẦN TRANSFORMER")
|
| 336 |
+
print("="*70)
|
| 337 |
+
|
| 338 |
+
# Hyperparameters
|
| 339 |
+
batch_size = 2
|
| 340 |
+
seq_len = 10
|
| 341 |
+
d_model = 512
|
| 342 |
+
n_heads = 8
|
| 343 |
+
d_ff = 2048
|
| 344 |
+
vocab_size = 10000
|
| 345 |
+
|
| 346 |
+
# Test Embedding
|
| 347 |
+
print("\n1. Test Embedding:")
|
| 348 |
+
embedding = Embedding(vocab_size, d_model)
|
| 349 |
+
x = torch.randint(0, vocab_size, (batch_size, seq_len))
|
| 350 |
+
embedded = embedding(x)
|
| 351 |
+
print(f" Input shape: {x.shape}")
|
| 352 |
+
print(f" Output shape: {embedded.shape}")
|
| 353 |
+
|
| 354 |
+
# Test Positional Encoding
|
| 355 |
+
print("\n2. Test Positional Encoding:")
|
| 356 |
+
pos_enc = PositionalEncoding(d_model)
|
| 357 |
+
pos_encoded = pos_enc(embedded)
|
| 358 |
+
print(f" Input shape: {embedded.shape}")
|
| 359 |
+
print(f" Output shape: {pos_encoded.shape}")
|
| 360 |
+
|
| 361 |
+
# Test Multi-Head Attention
|
| 362 |
+
print("\n3. Test Multi-Head Attention:")
|
| 363 |
+
mha = MultiHeadAttention(d_model, n_heads)
|
| 364 |
+
output, attn_weights = mha(pos_encoded, pos_encoded, pos_encoded)
|
| 365 |
+
print(f" Input shape: {pos_encoded.shape}")
|
| 366 |
+
print(f" Output shape: {output.shape}")
|
| 367 |
+
print(f" Attention weights shape: {attn_weights.shape}")
|
| 368 |
+
|
| 369 |
+
# Test Feed-Forward Network
|
| 370 |
+
print("\n4. Test Feed-Forward Network:")
|
| 371 |
+
ffn = PositionwiseFeedForward(d_model, d_ff)
|
| 372 |
+
ffn_output = ffn(output)
|
| 373 |
+
print(f" Input shape: {output.shape}")
|
| 374 |
+
print(f" Output shape: {ffn_output.shape}")
|
| 375 |
+
|
| 376 |
+
print("\n" + "="*70)
|
| 377 |
+
print("✓ TẤT CẢ THÀNH PHẦN HOẠT ĐỘNG ĐÚNG!")
|
| 378 |
+
print("="*70)
|
src/transformer_encoder_decoder.py
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
TRANSFORMER ENCODER & DECODER (FIXED)
|
| 3 |
+
Xây dựng hoàn chỉnh Encoder và Decoder layers
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
import torch.nn as nn
|
| 8 |
+
from .transformer_components import (
|
| 9 |
+
MultiHeadAttention,
|
| 10 |
+
PositionwiseFeedForward,
|
| 11 |
+
ResidualConnection,
|
| 12 |
+
LayerNorm
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
# ============================================================================
|
| 16 |
+
# 1. ENCODER LAYER
|
| 17 |
+
# ============================================================================
|
| 18 |
+
|
| 19 |
+
class EncoderLayer(nn.Module):
|
| 20 |
+
"""
|
| 21 |
+
Một layer của Transformer Encoder
|
| 22 |
+
|
| 23 |
+
Gồm:
|
| 24 |
+
1. Multi-Head Self-Attention
|
| 25 |
+
2. Add & Norm
|
| 26 |
+
3. Feed-Forward Network
|
| 27 |
+
4. Add & Norm
|
| 28 |
+
|
| 29 |
+
Args:
|
| 30 |
+
d_model: Dimension của model
|
| 31 |
+
n_heads: Số lượng attention heads
|
| 32 |
+
d_ff: Dimension của feed-forward network
|
| 33 |
+
dropout: Dropout rate
|
| 34 |
+
"""
|
| 35 |
+
def __init__(self, d_model, n_heads, d_ff, dropout=0.1):
|
| 36 |
+
super().__init__()
|
| 37 |
+
|
| 38 |
+
# Multi-Head Self-Attention
|
| 39 |
+
self.self_attention = MultiHeadAttention(d_model, n_heads, dropout)
|
| 40 |
+
|
| 41 |
+
# Feed-Forward Network
|
| 42 |
+
self.feed_forward = PositionwiseFeedForward(d_model, d_ff, dropout)
|
| 43 |
+
|
| 44 |
+
# Residual Connections
|
| 45 |
+
self.residual1 = ResidualConnection(d_model, dropout)
|
| 46 |
+
self.residual2 = ResidualConnection(d_model, dropout)
|
| 47 |
+
|
| 48 |
+
def forward(self, x, mask=None):
|
| 49 |
+
"""
|
| 50 |
+
Args:
|
| 51 |
+
x: Input [batch_size, seq_len, d_model]
|
| 52 |
+
mask: Mask tensor [batch_size, 1, 1, seq_len] (để mask padding)
|
| 53 |
+
|
| 54 |
+
Returns:
|
| 55 |
+
output: [batch_size, seq_len, d_model]
|
| 56 |
+
"""
|
| 57 |
+
# 1. Self-Attention với Residual Connection
|
| 58 |
+
x = self.residual1(x, lambda x: self.self_attention(x, x, x, mask)[0])
|
| 59 |
+
|
| 60 |
+
# 2. Feed-Forward với Residual Connection
|
| 61 |
+
x = self.residual2(x, self.feed_forward)
|
| 62 |
+
|
| 63 |
+
return x
|
| 64 |
+
|
| 65 |
+
# ============================================================================
|
| 66 |
+
# 2. ENCODER
|
| 67 |
+
# ============================================================================
|
| 68 |
+
|
| 69 |
+
class Encoder(nn.Module):
|
| 70 |
+
"""
|
| 71 |
+
Transformer Encoder - Stack của N encoder layers
|
| 72 |
+
|
| 73 |
+
Args:
|
| 74 |
+
vocab_size: Kích thước vocabulary
|
| 75 |
+
d_model: Dimension của model
|
| 76 |
+
n_layers: Số lượng encoder layers
|
| 77 |
+
n_heads: Số lượng attention heads
|
| 78 |
+
d_ff: Dimension của feed-forward network
|
| 79 |
+
dropout: Dropout rate
|
| 80 |
+
max_len: Maximum sequence length
|
| 81 |
+
"""
|
| 82 |
+
def __init__(self, vocab_size, d_model, n_layers, n_heads, d_ff, dropout=0.1, max_len=5000):
|
| 83 |
+
super().__init__()
|
| 84 |
+
|
| 85 |
+
from .transformer_components import Embedding, PositionalEncoding
|
| 86 |
+
|
| 87 |
+
# Embedding layer
|
| 88 |
+
self.embedding = Embedding(vocab_size, d_model)
|
| 89 |
+
|
| 90 |
+
# Positional Encoding
|
| 91 |
+
self.pos_encoding = PositionalEncoding(d_model, max_len, dropout)
|
| 92 |
+
|
| 93 |
+
# Stack of Encoder Layers
|
| 94 |
+
self.layers = nn.ModuleList([
|
| 95 |
+
EncoderLayer(d_model, n_heads, d_ff, dropout)
|
| 96 |
+
for _ in range(n_layers)
|
| 97 |
+
])
|
| 98 |
+
|
| 99 |
+
# Final Layer Normalization
|
| 100 |
+
self.norm = LayerNorm(d_model)
|
| 101 |
+
|
| 102 |
+
def forward(self, src, src_mask=None):
|
| 103 |
+
"""
|
| 104 |
+
Args:
|
| 105 |
+
src: Source sequence [batch_size, src_len]
|
| 106 |
+
src_mask: Source mask [batch_size, 1, 1, src_len]
|
| 107 |
+
|
| 108 |
+
Returns:
|
| 109 |
+
output: [batch_size, src_len, d_model]
|
| 110 |
+
"""
|
| 111 |
+
# 1. Embedding + Positional Encoding
|
| 112 |
+
x = self.embedding(src)
|
| 113 |
+
x = self.pos_encoding(x)
|
| 114 |
+
|
| 115 |
+
# 2. Pass through encoder layers
|
| 116 |
+
for layer in self.layers:
|
| 117 |
+
x = layer(x, src_mask)
|
| 118 |
+
|
| 119 |
+
# 3. Final normalization
|
| 120 |
+
x = self.norm(x)
|
| 121 |
+
|
| 122 |
+
return x
|
| 123 |
+
|
| 124 |
+
# ============================================================================
|
| 125 |
+
# 3. DECODER LAYER
|
| 126 |
+
# ============================================================================
|
| 127 |
+
|
| 128 |
+
class DecoderLayer(nn.Module):
|
| 129 |
+
"""
|
| 130 |
+
Một layer của Transformer Decoder
|
| 131 |
+
|
| 132 |
+
Gồm:
|
| 133 |
+
1. Masked Multi-Head Self-Attention
|
| 134 |
+
2. Add & Norm
|
| 135 |
+
3. Multi-Head Cross-Attention (với Encoder output)
|
| 136 |
+
4. Add & Norm
|
| 137 |
+
5. Feed-Forward Network
|
| 138 |
+
6. Add & Norm
|
| 139 |
+
|
| 140 |
+
Args:
|
| 141 |
+
d_model: Dimension của model
|
| 142 |
+
n_heads: Số lượng attention heads
|
| 143 |
+
d_ff: Dimension của feed-forward network
|
| 144 |
+
dropout: Dropout rate
|
| 145 |
+
"""
|
| 146 |
+
def __init__(self, d_model, n_heads, d_ff, dropout=0.1):
|
| 147 |
+
super().__init__()
|
| 148 |
+
|
| 149 |
+
# Masked Multi-Head Self-Attention
|
| 150 |
+
self.self_attention = MultiHeadAttention(d_model, n_heads, dropout)
|
| 151 |
+
|
| 152 |
+
# Multi-Head Cross-Attention (Encoder-Decoder Attention)
|
| 153 |
+
self.cross_attention = MultiHeadAttention(d_model, n_heads, dropout)
|
| 154 |
+
|
| 155 |
+
# Feed-Forward Network
|
| 156 |
+
self.feed_forward = PositionwiseFeedForward(d_model, d_ff, dropout)
|
| 157 |
+
|
| 158 |
+
# Residual Connections
|
| 159 |
+
self.residual1 = ResidualConnection(d_model, dropout)
|
| 160 |
+
self.residual2 = ResidualConnection(d_model, dropout)
|
| 161 |
+
self.residual3 = ResidualConnection(d_model, dropout)
|
| 162 |
+
|
| 163 |
+
def forward(self, x, encoder_output, src_mask=None, tgt_mask=None):
|
| 164 |
+
"""
|
| 165 |
+
Args:
|
| 166 |
+
x: Target input [batch_size, tgt_len, d_model]
|
| 167 |
+
encoder_output: Encoder output [batch_size, src_len, d_model]
|
| 168 |
+
src_mask: Source mask [batch_size, 1, 1, src_len]
|
| 169 |
+
tgt_mask: Target mask [batch_size, 1, tgt_len, tgt_len] (causal mask)
|
| 170 |
+
|
| 171 |
+
Returns:
|
| 172 |
+
output: [batch_size, tgt_len, d_model]
|
| 173 |
+
"""
|
| 174 |
+
# 1. Masked Self-Attention với Residual Connection
|
| 175 |
+
x = self.residual1(x, lambda x: self.self_attention(x, x, x, tgt_mask)[0])
|
| 176 |
+
|
| 177 |
+
# 2. Cross-Attention với Encoder output
|
| 178 |
+
# Q từ decoder, K, V từ encoder
|
| 179 |
+
x = self.residual2(x, lambda x: self.cross_attention(x, encoder_output, encoder_output, src_mask)[0])
|
| 180 |
+
|
| 181 |
+
# 3. Feed-Forward với Residual Connection
|
| 182 |
+
x = self.residual3(x, self.feed_forward)
|
| 183 |
+
|
| 184 |
+
return x
|
| 185 |
+
|
| 186 |
+
# ============================================================================
|
| 187 |
+
# 4. DECODER
|
| 188 |
+
# ============================================================================
|
| 189 |
+
|
| 190 |
+
class Decoder(nn.Module):
|
| 191 |
+
"""
|
| 192 |
+
Transformer Decoder - Stack của N decoder layers
|
| 193 |
+
|
| 194 |
+
Args:
|
| 195 |
+
vocab_size: Kích thước vocabulary
|
| 196 |
+
d_model: Dimension của model
|
| 197 |
+
n_layers: Số lượng decoder layers
|
| 198 |
+
n_heads: Số lượng attention heads
|
| 199 |
+
d_ff: Dimension của feed-forward network
|
| 200 |
+
dropout: Dropout rate
|
| 201 |
+
max_len: Maximum sequence length
|
| 202 |
+
"""
|
| 203 |
+
def __init__(self, vocab_size, d_model, n_layers, n_heads, d_ff, dropout=0.1, max_len=5000):
|
| 204 |
+
super().__init__()
|
| 205 |
+
|
| 206 |
+
from .transformer_components import Embedding, PositionalEncoding
|
| 207 |
+
|
| 208 |
+
# Embedding layer
|
| 209 |
+
self.embedding = Embedding(vocab_size, d_model)
|
| 210 |
+
|
| 211 |
+
# Positional Encoding
|
| 212 |
+
self.pos_encoding = PositionalEncoding(d_model, max_len, dropout)
|
| 213 |
+
|
| 214 |
+
# Stack of Decoder Layers
|
| 215 |
+
self.layers = nn.ModuleList([
|
| 216 |
+
DecoderLayer(d_model, n_heads, d_ff, dropout)
|
| 217 |
+
for _ in range(n_layers)
|
| 218 |
+
])
|
| 219 |
+
|
| 220 |
+
# Final Layer Normalization
|
| 221 |
+
self.norm = LayerNorm(d_model)
|
| 222 |
+
|
| 223 |
+
# Output projection to vocabulary
|
| 224 |
+
self.fc_out = nn.Linear(d_model, vocab_size)
|
| 225 |
+
|
| 226 |
+
def forward(self, tgt, encoder_output, src_mask=None, tgt_mask=None):
|
| 227 |
+
"""
|
| 228 |
+
Args:
|
| 229 |
+
tgt: Target sequence [batch_size, tgt_len]
|
| 230 |
+
encoder_output: Encoder output [batch_size, src_len, d_model]
|
| 231 |
+
src_mask: Source mask [batch_size, 1, 1, src_len]
|
| 232 |
+
tgt_mask: Target mask [batch_size, 1, tgt_len, tgt_len]
|
| 233 |
+
|
| 234 |
+
Returns:
|
| 235 |
+
output: [batch_size, tgt_len, vocab_size]
|
| 236 |
+
"""
|
| 237 |
+
# 1. Embedding + Positional Encoding
|
| 238 |
+
x = self.embedding(tgt)
|
| 239 |
+
x = self.pos_encoding(x)
|
| 240 |
+
|
| 241 |
+
# 2. Pass through decoder layers
|
| 242 |
+
for layer in self.layers:
|
| 243 |
+
x = layer(x, encoder_output, src_mask, tgt_mask)
|
| 244 |
+
|
| 245 |
+
# 3. Final normalization
|
| 246 |
+
x = self.norm(x)
|
| 247 |
+
|
| 248 |
+
# 4. Project to vocabulary
|
| 249 |
+
output = self.fc_out(x)
|
| 250 |
+
|
| 251 |
+
return output
|
| 252 |
+
|
| 253 |
+
# ============================================================================
|
| 254 |
+
# 5. MASK FUNCTIONS (FIXED)
|
| 255 |
+
# ============================================================================
|
| 256 |
+
|
| 257 |
+
def create_padding_mask(seq, pad_idx=0):
|
| 258 |
+
"""
|
| 259 |
+
Tạo mask cho padding tokens
|
| 260 |
+
|
| 261 |
+
Args:
|
| 262 |
+
seq: Sequence [batch_size, seq_len]
|
| 263 |
+
pad_idx: Index của padding token
|
| 264 |
+
|
| 265 |
+
Returns:
|
| 266 |
+
mask: [batch_size, 1, 1, seq_len] (bool type)
|
| 267 |
+
"""
|
| 268 |
+
# Tạo mask: True cho non-padding, False cho padding
|
| 269 |
+
mask = (seq != pad_idx).unsqueeze(1).unsqueeze(2)
|
| 270 |
+
return mask # Returns bool tensor
|
| 271 |
+
|
| 272 |
+
def create_causal_mask(seq_len, device):
|
| 273 |
+
"""
|
| 274 |
+
Tạo causal mask (look-ahead mask) cho decoder
|
| 275 |
+
Ngăn decoder nhìn thấy future tokens
|
| 276 |
+
|
| 277 |
+
Args:
|
| 278 |
+
seq_len: Length của sequence
|
| 279 |
+
device: Device (cuda hoặc cpu)
|
| 280 |
+
|
| 281 |
+
Returns:
|
| 282 |
+
mask: [1, 1, seq_len, seq_len] (bool type)
|
| 283 |
+
"""
|
| 284 |
+
# Tạo lower triangular matrix - FIXED: convert to bool
|
| 285 |
+
mask = torch.tril(torch.ones(seq_len, seq_len, device=device))
|
| 286 |
+
mask = mask.bool() # Convert to bool
|
| 287 |
+
mask = mask.unsqueeze(0).unsqueeze(1)
|
| 288 |
+
return mask
|
| 289 |
+
|
| 290 |
+
def create_target_mask(tgt, pad_idx=0):
|
| 291 |
+
"""
|
| 292 |
+
Tạo mask kết hợp cho target sequence (padding + causal)
|
| 293 |
+
|
| 294 |
+
Args:
|
| 295 |
+
tgt: Target sequence [batch_size, tgt_len]
|
| 296 |
+
pad_idx: Index của padding token
|
| 297 |
+
|
| 298 |
+
Returns:
|
| 299 |
+
mask: [batch_size, 1, tgt_len, tgt_len] (bool type)
|
| 300 |
+
"""
|
| 301 |
+
batch_size, tgt_len = tgt.size()
|
| 302 |
+
device = tgt.device
|
| 303 |
+
|
| 304 |
+
# Padding mask - returns bool
|
| 305 |
+
padding_mask = (tgt != pad_idx).unsqueeze(1).unsqueeze(2) # [batch, 1, 1, tgt_len]
|
| 306 |
+
|
| 307 |
+
# Causal mask - returns bool
|
| 308 |
+
causal_mask = create_causal_mask(tgt_len, device) # [1, 1, tgt_len, tgt_len]
|
| 309 |
+
|
| 310 |
+
# Kết hợp cả 2 masks - both are bool now
|
| 311 |
+
mask = padding_mask & causal_mask
|
| 312 |
+
|
| 313 |
+
return mask
|
| 314 |
+
|
| 315 |
+
# ============================================================================
|
| 316 |
+
# 6. TEST ENCODER & DECODER
|
| 317 |
+
# ============================================================================
|
| 318 |
+
|
| 319 |
+
if __name__ == "__main__":
|
| 320 |
+
print("="*70)
|
| 321 |
+
print("KIỂM TRA ENCODER & DECODER")
|
| 322 |
+
print("="*70)
|
| 323 |
+
|
| 324 |
+
# Hyperparameters
|
| 325 |
+
batch_size = 2
|
| 326 |
+
src_len = 10
|
| 327 |
+
tgt_len = 12
|
| 328 |
+
src_vocab_size = 10000
|
| 329 |
+
tgt_vocab_size = 8000
|
| 330 |
+
d_model = 512
|
| 331 |
+
n_layers = 6
|
| 332 |
+
n_heads = 8
|
| 333 |
+
d_ff = 2048
|
| 334 |
+
dropout = 0.1
|
| 335 |
+
pad_idx = 0
|
| 336 |
+
|
| 337 |
+
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
| 338 |
+
print(f"\nDevice: {device}")
|
| 339 |
+
|
| 340 |
+
# Tạo dummy data
|
| 341 |
+
src = torch.randint(1, src_vocab_size, (batch_size, src_len)).to(device)
|
| 342 |
+
tgt = torch.randint(1, tgt_vocab_size, (batch_size, tgt_len)).to(device)
|
| 343 |
+
|
| 344 |
+
# Tạo masks
|
| 345 |
+
src_mask = create_padding_mask(src, pad_idx).to(device)
|
| 346 |
+
tgt_mask = create_target_mask(tgt, pad_idx).to(device)
|
| 347 |
+
|
| 348 |
+
print(f"\nInput shapes:")
|
| 349 |
+
print(f" Source: {src.shape}")
|
| 350 |
+
print(f" Target: {tgt.shape}")
|
| 351 |
+
print(f" Source mask: {src_mask.shape}, dtype: {src_mask.dtype}")
|
| 352 |
+
print(f" Target mask: {tgt_mask.shape}, dtype: {tgt_mask.dtype}")
|
| 353 |
+
|
| 354 |
+
# Test Encoder
|
| 355 |
+
print("\n" + "="*70)
|
| 356 |
+
print("Test Encoder")
|
| 357 |
+
print("="*70)
|
| 358 |
+
|
| 359 |
+
encoder = Encoder(
|
| 360 |
+
vocab_size=src_vocab_size,
|
| 361 |
+
d_model=d_model,
|
| 362 |
+
n_layers=n_layers,
|
| 363 |
+
n_heads=n_heads,
|
| 364 |
+
d_ff=d_ff,
|
| 365 |
+
dropout=dropout
|
| 366 |
+
).to(device)
|
| 367 |
+
|
| 368 |
+
encoder_output = encoder(src, src_mask)
|
| 369 |
+
print(f"Encoder output shape: {encoder_output.shape}")
|
| 370 |
+
print(f"Expected: [{batch_size}, {src_len}, {d_model}]")
|
| 371 |
+
|
| 372 |
+
# Test Decoder
|
| 373 |
+
print("\n" + "="*70)
|
| 374 |
+
print("Test Decoder")
|
| 375 |
+
print("="*70)
|
| 376 |
+
|
| 377 |
+
decoder = Decoder(
|
| 378 |
+
vocab_size=tgt_vocab_size,
|
| 379 |
+
d_model=d_model,
|
| 380 |
+
n_layers=n_layers,
|
| 381 |
+
n_heads=n_heads,
|
| 382 |
+
d_ff=d_ff,
|
| 383 |
+
dropout=dropout
|
| 384 |
+
).to(device)
|
| 385 |
+
|
| 386 |
+
decoder_output = decoder(tgt, encoder_output, src_mask, tgt_mask)
|
| 387 |
+
print(f"Decoder output shape: {decoder_output.shape}")
|
| 388 |
+
print(f"Expected: [{batch_size}, {tgt_len}, {tgt_vocab_size}]")
|
| 389 |
+
|
| 390 |
+
# Số lượng parameters
|
| 391 |
+
encoder_params = sum(p.numel() for p in encoder.parameters())
|
| 392 |
+
decoder_params = sum(p.numel() for p in decoder.parameters())
|
| 393 |
+
|
| 394 |
+
print("\n" + "="*70)
|
| 395 |
+
print("THỐNG KÊ MÔ HÌNH")
|
| 396 |
+
print("="*70)
|
| 397 |
+
print(f"Encoder parameters: {encoder_params:,}")
|
| 398 |
+
print(f"Decoder parameters: {decoder_params:,}")
|
| 399 |
+
print(f"Total parameters: {encoder_params + decoder_params:,}")
|
| 400 |
+
|
| 401 |
+
print("\n" + "="*70)
|
| 402 |
+
print("✓ ENCODER & DECODER HOẠT ĐỘNG ĐÚNG!")
|
| 403 |
+
print("="*70)
|