File size: 5,345 Bytes
c1a46f7
 
 
 
 
 
 
a2d6c00
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c1a46f7
 
 
 
a2d6c00
c1a46f7
a2d6c00
 
c1a46f7
a2d6c00
c1a46f7
a2d6c00
 
 
c1a46f7
a2d6c00
c1a46f7
a2d6c00
 
 
 
 
c1a46f7
 
 
a2d6c00
 
c1a46f7
 
 
 
 
a2d6c00
c1a46f7
a2d6c00
 
 
 
c1a46f7
a2d6c00
c1a46f7
a2d6c00
 
 
 
c1a46f7
a2d6c00
c1a46f7
a2d6c00
 
 
 
c1a46f7
 
 
 
 
 
 
a2d6c00
c1a46f7
 
 
a2d6c00
 
c1a46f7
 
 
a2d6c00
 
 
 
 
 
 
 
c1a46f7
 
 
 
 
a2d6c00
c1a46f7
a2d6c00
 
 
 
 
 
 
 
 
 
 
c1a46f7
a2d6c00
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
单元测试 — 数据模块 (Person A 实现后需通过)
"""

import pytest
import torch

from easytranslate.data import (
    DynamicBatchSampler,
    TokenizerWrapper,
    TranslationCollator,
    TranslationDataset,
    clean_text,
    deduplicate_pairs,
    filter_by_length,
    preprocess_pipeline,
    train_bpe_tokenizer,
)


@pytest.fixture()
def tiny_tokenizer() -> TokenizerWrapper:
    texts = [
        "Hello world",
        "Machine translation is useful",
        "I love natural language processing",
        "你好 世界",
        "机器翻译 很 有用",
        "我 喜欢 自然语言处理",
    ]
    return train_bpe_tokenizer(texts, vocab_size=80, min_frequency=1)


class TestTranslationDataset:
    """测试 TranslationDataset 类。"""

    def test_dataset_length(self, tiny_tokenizer):
        """测试数据集长度。"""
        dataset = TranslationDataset(["Hello world"], ["你好 世界"], tokenizer=tiny_tokenizer)
        assert len(dataset) == 1

    def test_getitem_returns_correct_keys(self, tiny_tokenizer):
        """测试 __getitem__ 返回正确的字段。"""
        dataset = TranslationDataset(["Hello world"], ["你好 世界"], tokenizer=tiny_tokenizer)
        item = dataset[0]
        assert set(item) == {"src_ids", "tgt_input_ids", "labels", "src_len", "tgt_len"}

    def test_getitem_tensor_types(self, tiny_tokenizer):
        """测试返回的 tensor 类型正确。"""
        dataset = TranslationDataset(["Hello world"], ["你好 世界"], tokenizer=tiny_tokenizer)
        item = dataset[0]
        assert item["src_ids"].dtype == torch.long
        assert item["tgt_input_ids"].dtype == torch.long
        assert item["labels"].dtype == torch.long

    def test_src_tgt_mismatch_raises(self):
        """测试源目标数量不匹配时抛出异常。"""
        with pytest.raises(ValueError):
            TranslationDataset(["a", "b"], ["甲"], tokenizer=object())


class TestTokenizer:
    """测试分词器。"""

    def test_bpe_train_and_encode(self, tiny_tokenizer):
        """测试 BPE 训练和编码。"""
        ids = tiny_tokenizer.encode("Hello world", add_special_tokens=True)
        assert len(ids) >= 3
        assert ids[0] == tiny_tokenizer.bos_token_id
        assert ids[-1] == tiny_tokenizer.eos_token_id

    def test_encode_decode_roundtrip(self, tiny_tokenizer):
        """测试编码-解码往返一致性。"""
        ids = tiny_tokenizer.encode("Hello world")
        decoded = tiny_tokenizer.decode(ids)
        assert "Hello" in decoded
        assert "world" in decoded

    def test_special_tokens(self, tiny_tokenizer):
        """测试特殊 token 正确。"""
        assert tiny_tokenizer.pad_token_id == 0
        assert tiny_tokenizer.unk_token_id == 1
        assert tiny_tokenizer.bos_token_id == 2
        assert tiny_tokenizer.eos_token_id == 3


class TestPreprocessing:
    """测试预处理。"""

    def test_clean_text_unicode(self):
        """测试 Unicode 标准化。"""
        assert clean_text("ABC\u200b  123") == "ABC 123"

    def test_filter_by_length(self):
        """测试按长度过滤。"""
        assert filter_by_length("hello world", "你好世界", max_src_len=10, max_tgt_len=10)
        assert not filter_by_length("hello " * 300, "你好", max_src_len=256)

    def test_deduplicate(self):
        """测试去重。"""
        pairs = deduplicate_pairs([("a", "甲"), ("a", "甲"), ("b", "乙")])
        assert pairs == [("a", "甲"), ("b", "乙")]

    def test_preprocess_pipeline(self):
        """测试完整预处理流水线。"""
        src, tgt = preprocess_pipeline([" Hello  world ", "", "Hello  world"], [" 你好 世界 ", "空", "你好 世界"])
        assert src == ["Hello world"]
        assert tgt == ["你好 世界"]


class TestCollator:
    """测试数据整理器。"""

    def test_padding(self, tiny_tokenizer):
        """测试 padding 正确。"""
        dataset = TranslationDataset(
            ["Hello world", "Machine translation is useful"],
            ["你好 世界", "机器翻译 很 有用"],
            tokenizer=tiny_tokenizer,
        )
        batch = TranslationCollator(pad_token_id=tiny_tokenizer.pad_token_id)([dataset[0], dataset[1]])
        assert batch["src_ids"].ndim == 2
        assert batch["tgt_input_ids"].ndim == 2
        assert batch["labels"].shape == batch["tgt_input_ids"].shape

    def test_attention_mask(self, tiny_tokenizer):
        """测试 attention mask 正确。"""
        dataset = TranslationDataset(
            ["Hello", "Machine translation is useful"],
            ["你好", "机器翻译 很 有用"],
            tokenizer=tiny_tokenizer,
        )
        batch = TranslationCollator(pad_token_id=tiny_tokenizer.pad_token_id)([dataset[0], dataset[1]])
        assert batch["src_padding_mask"].dtype == torch.bool
        assert torch.equal(batch["src_attention_mask"], (~batch["src_padding_mask"]).long())

    def test_dynamic_batch_sampler(self):
        """测试动态 batch 不超过 token 预算。"""
        sampler = DynamicBatchSampler([5, 6, 20, 21], max_tokens_per_batch=24, shuffle=False)
        batches = list(sampler)
        assert batches
        for batch in batches:
            assert max([5, 6, 20, 21][idx] for idx in batch) * len(batch) <= 24