Cong123779 commited on
Commit
51b3b77
·
verified ·
1 Parent(s): 0396d53

Upload model source code

Browse files
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)