File size: 5,863 Bytes
f90f02b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
"""
Dataset and Tokenization module for Part 1 (Vietnamese/Japanese to English Translation).
Handles belumind/en-vi-ja-curated-500k-triplets dataset.
Supports Byte-level BPE tokenization handling non-romanized Japanese and Vietnamese characters.
"""

import os
from typing import Dict, List, Tuple, Optional, Any
import torch
from torch.utils.data import Dataset, DataLoader
from tokenizers import Tokenizer, models, pre_tokenizers, decoders, trainers, processors
from transformers import PreTrainedTokenizerFast


SPECIAL_TOKENS = ["<pad>", "<unk>", "<bos>", "<eos>", "<vi>", "<ja>", "<en>"]


def build_or_load_tokenizer(
    vocab_size: int = 16000,
    save_path: str = "assets/tokenizer_multilingual.json",
    corpus_texts: Optional[List[str]] = None
) -> PreTrainedTokenizerFast:
    """
    Builds or loads a Byte-level BPE tokenizer capable of processing
    arbitrary UTF-8 bytes (Vietnamese diacritics and Japanese Kanji/Kana).
    """
    if os.path.exists(save_path):
        tokenizer = Tokenizer.from_file(save_path)
        fast_tok = PreTrainedTokenizerFast(
            tokenizer_object=tokenizer,
            bos_token="<bos>",
            eos_token="<eos>",
            pad_token="<pad>",
            unk_token="<unk>",
        )
        return fast_tok

    # Build Byte-Level BPE tokenizer from scratch
    raw_tokenizer = Tokenizer(models.BPE(unk_token="<unk>"))
    raw_tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False)
    raw_tokenizer.decoder = decoders.ByteLevel()

    trainer = trainers.BpeTrainer(
        vocab_size=vocab_size,
        special_tokens=SPECIAL_TOKENS,
        initial_alphabet=pre_tokenizers.ByteLevel.alphabet(),
        show_progress=False
    )

    if corpus_texts is not None and len(corpus_texts) > 0:
        raw_tokenizer.train_from_iterator(corpus_texts, trainer=trainer)
    else:
        # Fallback dummy seed training with typical tokens so tokenizer is valid
        seed_data = [
            "Hello world, this is English.",
            "こんにちは世界、日本語のテストです。",
            "Xin chào thế giới, đây là kiểm tra tiếng Việt.",
        ] * 100
        raw_tokenizer.train_from_iterator(seed_data, trainer=trainer)

    os.makedirs(os.path.dirname(os.path.abspath(save_path)), exist_ok=True)
    raw_tokenizer.save(save_path)

    fast_tok = PreTrainedTokenizerFast(
        tokenizer_object=raw_tokenizer,
        bos_token="<bos>",
        eos_token="<eos>",
        pad_token="<pad>",
        unk_token="<unk>",
    )
    return fast_tok


class TranslationDataset(Dataset):
    """
    Dataset creating Vietnamese->English and Japanese->English samples.
    Each triplet (vi, ja, en) produces two training sequences:
    1. <vi> {vi_text} <en> {en_text} <eos>
    2. <ja> {ja_text} <en> {en_text} <eos>
    """
    def __init__(
        self,
        samples: List[Dict[str, str]],
        tokenizer: PreTrainedTokenizerFast,
        max_seq_len: int = 256,
        loss_on_target_only: bool = True
    ):
        self.tokenizer = tokenizer
        self.max_seq_len = max_seq_len
        self.loss_on_target_only = loss_on_target_only
        self.data: List[Dict[str, Any]] = []

        for item in samples:
            en = item.get("en", "").strip()
            vi = item.get("vi", "").strip()
            ja = item.get("ja", "").strip()

            if vi and en:
                self.data.append({
                    "src_lang": "vi",
                    "src_text": vi,
                    "tgt_text": en,
                    "prompt": f"<vi> {vi} <en>",
                    "full_text": f"<vi> {vi} <en> {en} <eos>"
                })
            if ja and en:
                self.data.append({
                    "src_lang": "ja",
                    "src_text": ja,
                    "tgt_text": en,
                    "prompt": f"<ja> {ja} <en>",
                    "full_text": f"<ja> {ja} <en> {en} <eos>"
                })

    def __len__(self) -> int:
        return len(self.data)

    def __getitem__(self, idx: int) -> Dict[str, torch.Tensor]:
        item = self.data[idx]
        full_text = item["full_text"]
        prompt = item["prompt"]

        full_encoding = self.tokenizer(
            full_text,
            max_length=self.max_seq_len,
            padding="max_length",
            truncation=True,
            return_tensors="pt"
        )
        input_ids = full_encoding["input_ids"].squeeze(0)  # (N_ctx,)

        targets = input_ids.clone()
        pad_token_id = self.tokenizer.pad_token_id or 0
        targets[targets == pad_token_id] = -100

        if self.loss_on_target_only:
            prompt_encoding = self.tokenizer(
                prompt,
                max_length=self.max_seq_len,
                truncation=True,
                return_tensors="pt"
            )
            prompt_len = min(prompt_encoding["input_ids"].shape[1], self.max_seq_len)
            targets[:prompt_len] = -100  # Mask out prompt tokens from loss computation

        return {
            "input_ids": input_ids,
            "targets": targets,
            "src_lang": item["src_lang"],
            "src_text": item["src_text"],
            "tgt_text": item["tgt_text"],
            "prompt": item["prompt"]
        }


def collate_fn(batch: List[Dict[str, Any]]) -> Dict[str, Any]:
    input_ids = torch.stack([b["input_ids"] for b in batch])
    targets = torch.stack([b["targets"] for b in batch])
    src_langs = [b["src_lang"] for b in batch]
    src_texts = [b["src_text"] for b in batch]
    tgt_texts = [b["tgt_text"] for b in batch]
    prompts = [b["prompt"] for b in batch]

    return {
        "input_ids": input_ids,
        "targets": targets,
        "src_langs": src_langs,
        "src_texts": src_texts,
        "tgt_texts": tgt_texts,
        "prompts": prompts
    }