[Person D] Implement evaluation module: metrics, decoding, evaluator, scripts

#9
scripts/evaluate.py CHANGED
@@ -10,11 +10,17 @@
10
  """
11
 
12
  import argparse
 
13
  import sys
14
  from pathlib import Path
15
 
 
 
 
16
  sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src"))
17
 
 
 
18
 
19
  def parse_args():
20
  parser = argparse.ArgumentParser(description="EasyTranslate Evaluation")
@@ -43,7 +49,71 @@ def main():
43
  print(" EasyTranslate - Evaluation")
44
  print("=" * 60)
45
 
46
- raise NotImplementedError("TODO: Person D 实现评估主流程")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
 
49
  if __name__ == "__main__":
 
10
  """
11
 
12
  import argparse
13
+ import json
14
  import sys
15
  from pathlib import Path
16
 
17
+ import torch
18
+ import yaml
19
+
20
  sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src"))
21
 
22
+ from easytranslate.evaluation.evaluator import Evaluator
23
+
24
 
25
  def parse_args():
26
  parser = argparse.ArgumentParser(description="EasyTranslate Evaluation")
 
49
  print(" EasyTranslate - Evaluation")
50
  print("=" * 60)
51
 
52
+ # 1. 加载配置
53
+ with open(args.config, "r", encoding="utf-8") as f:
54
+ config = yaml.safe_load(f)
55
+
56
+ # 应用命令行覆盖 (格式: key.subkey=value)
57
+ for override in cli_overrides:
58
+ if "=" in override:
59
+ key, value = override.split("=", 1)
60
+ keys = key.split(".")
61
+ d = config
62
+ for k in keys[:-1]:
63
+ d = d.setdefault(k, {})
64
+ d[keys[-1]] = yaml.safe_load(value)
65
+
66
+ # 2. 加载检查点并重建模型
67
+ checkpoint = torch.load(args.checkpoint, map_location="cpu")
68
+ model_config = config.get("model", {})
69
+
70
+ from easytranslate.model.transformer import Transformer
71
+ model = Transformer(model_config)
72
+ model.load_state_dict(checkpoint["model_state_dict"])
73
+
74
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
75
+ model = model.to(device)
76
+ model.eval()
77
+
78
+ # 3. 加载 tokenizer 和测试数据
79
+ from easytranslate.data.tokenizer import build_tokenizer
80
+ from easytranslate.data.dataset import TranslationDataset
81
+ from torch.utils.data import DataLoader
82
+
83
+ tokenizer = build_tokenizer(config.get("data", {}).get("tokenizer", {}))
84
+
85
+ test_dataset = TranslationDataset(
86
+ config=config.get("data", {}),
87
+ tokenizer=tokenizer,
88
+ split="test",
89
+ )
90
+ test_loader = DataLoader(
91
+ test_dataset,
92
+ batch_size=config.get("evaluation", {}).get("batch_size", 32),
93
+ shuffle=False,
94
+ )
95
+
96
+ # 4. 构建 Evaluator
97
+ evaluator = Evaluator(model=model, tokenizer=tokenizer, config=config)
98
+
99
+ # 5. 运行评估
100
+ src_texts = [sample["src"] for sample in test_dataset.raw_data]
101
+ ref_texts = [sample["tgt"] for sample in test_dataset.raw_data]
102
+ results = evaluator.evaluate(test_loader, src_texts=src_texts, ref_texts=ref_texts)
103
+
104
+ # 6. 打印和保存结果
105
+ print("\n" + "=" * 60)
106
+ print(" Evaluation Results")
107
+ print("=" * 60)
108
+ for metric, score in results.items():
109
+ if not isinstance(score, list):
110
+ print(f" {metric:>10s}: {score:.4f}")
111
+
112
+ output_path = Path(args.output)
113
+ output_path.parent.mkdir(parents=True, exist_ok=True)
114
+ with open(output_path, "w", encoding="utf-8") as f:
115
+ json.dump(results, f, indent=2, ensure_ascii=False)
116
+ print(f"\n Results saved to: {output_path}")
117
 
118
 
119
  if __name__ == "__main__":
scripts/translate.py CHANGED
@@ -16,8 +16,13 @@ import argparse
16
  import sys
17
  from pathlib import Path
18
 
 
 
 
19
  sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src"))
20
 
 
 
21
 
22
  def parse_args():
23
  parser = argparse.ArgumentParser(description="EasyTranslate Inference")
@@ -39,7 +44,25 @@ def interactive_translate(evaluator):
39
  3. 打印翻译结果
40
  4. 输入 'quit' 退出
41
  """
42
- raise NotImplementedError("TODO: Person D 实现 interactive_translate")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
 
45
  def translate_file(evaluator, input_path: str, output_path: str):
@@ -51,7 +74,33 @@ def translate_file(evaluator, input_path: str, output_path: str):
51
  2. 批量翻译
52
  3. 将结果写入输出文件
53
  """
54
- raise NotImplementedError("TODO: Person D 实现 translate_file")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
 
57
  def launch_web_ui(evaluator):
@@ -64,7 +113,25 @@ def launch_web_ui(evaluator):
64
  3. 输出: 中文翻译结果
65
  4. 调用 evaluator.translate_single()
66
  """
67
- raise NotImplementedError("TODO: Person D 实现 launch_web_ui")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
 
70
  def main():
@@ -74,8 +141,37 @@ def main():
74
  print(" EasyTranslate - Translation")
75
  print("=" * 60)
76
 
77
- # TODO: 加载模型、构建 evaluator,然后根据参数选择模式
78
- raise NotImplementedError("TODO: 实现推理主流程")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
 
80
 
81
  if __name__ == "__main__":
 
16
  import sys
17
  from pathlib import Path
18
 
19
+ import torch
20
+ import yaml
21
+
22
  sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src"))
23
 
24
+ from easytranslate.evaluation.evaluator import Evaluator
25
+
26
 
27
  def parse_args():
28
  parser = argparse.ArgumentParser(description="EasyTranslate Inference")
 
44
  3. 打印翻译结果
45
  4. 输入 'quit' 退出
46
  """
47
+ print("\nInteractive Translation Mode (type 'quit' to exit)")
48
+ print("-" * 40)
49
+
50
+ while True:
51
+ try:
52
+ text = input("\n[EN] > ").strip()
53
+ except (EOFError, KeyboardInterrupt):
54
+ print("\nBye!")
55
+ break
56
+
57
+ if text.lower() in ("quit", "exit", "q"):
58
+ print("Bye!")
59
+ break
60
+
61
+ if not text:
62
+ continue
63
+
64
+ translation = evaluator.translate_single(text)
65
+ print(f"[ZH] > {translation}")
66
 
67
 
68
  def translate_file(evaluator, input_path: str, output_path: str):
 
74
  2. 批量翻译
75
  3. 将结果写入输出文件
76
  """
77
+ input_file = Path(input_path)
78
+ if not input_file.exists():
79
+ print(f"Error: input file not found: {input_path}")
80
+ return
81
+
82
+ with open(input_file, "r", encoding="utf-8") as f:
83
+ lines = [line.strip() for line in f if line.strip()]
84
+
85
+ print(f"Translating {len(lines)} sentences...")
86
+
87
+ # 分批翻译
88
+ batch_size = 32
89
+ translations = []
90
+ for i in range(0, len(lines), batch_size):
91
+ batch = lines[i : i + batch_size]
92
+ batch_translations = evaluator.translate(batch)
93
+ translations.extend(batch_translations)
94
+ print(f" Translated {min(i + batch_size, len(lines))}/{len(lines)}")
95
+
96
+ # 写入输出文件
97
+ out_file = Path(output_path)
98
+ out_file.parent.mkdir(parents=True, exist_ok=True)
99
+ with open(out_file, "w", encoding="utf-8") as f:
100
+ for t in translations:
101
+ f.write(t + "\n")
102
+
103
+ print(f"Results saved to: {output_path}")
104
 
105
 
106
  def launch_web_ui(evaluator):
 
113
  3. 输出: 中文翻译结果
114
  4. 调用 evaluator.translate_single()
115
  """
116
+ try:
117
+ import gradio as gr
118
+ except ImportError:
119
+ print("Error: gradio is not installed. Install with: pip install gradio")
120
+ return
121
+
122
+ def translate_fn(text):
123
+ if not text.strip():
124
+ return ""
125
+ return evaluator.translate_single(text)
126
+
127
+ interface = gr.Interface(
128
+ fn=translate_fn,
129
+ inputs=gr.Textbox(label="English", placeholder="Enter English text..."),
130
+ outputs=gr.Textbox(label="Chinese Translation"),
131
+ title="EasyTranslate - English to Chinese",
132
+ description="Transformer-based English to Chinese translation system.",
133
+ )
134
+ interface.launch()
135
 
136
 
137
  def main():
 
141
  print(" EasyTranslate - Translation")
142
  print("=" * 60)
143
 
144
+ # 加载配置
145
+ with open(args.config, "r", encoding="utf-8") as f:
146
+ config = yaml.safe_load(f)
147
+
148
+ # 加载模型
149
+ checkpoint = torch.load(args.checkpoint, map_location="cpu")
150
+
151
+ from easytranslate.model.transformer import Transformer
152
+ from easytranslate.data.tokenizer import build_tokenizer
153
+
154
+ model_config = config.get("model", {})
155
+ model = Transformer(model_config)
156
+ model.load_state_dict(checkpoint["model_state_dict"])
157
+
158
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
159
+ model = model.to(device)
160
+ model.eval()
161
+
162
+ tokenizer = build_tokenizer(config.get("data", {}).get("tokenizer", {}))
163
+
164
+ # 构建 evaluator
165
+ evaluator = Evaluator(model=model, tokenizer=tokenizer, config=config)
166
+
167
+ # 根据参数选择模式
168
+ if args.web:
169
+ launch_web_ui(evaluator)
170
+ elif args.input:
171
+ output_path = args.output or args.input.replace(".txt", "_translated.txt")
172
+ translate_file(evaluator, args.input, output_path)
173
+ else:
174
+ interactive_translate(evaluator)
175
 
176
 
177
  if __name__ == "__main__":
src/easytranslate/evaluation/decoding.py CHANGED
@@ -47,7 +47,22 @@ def greedy_decode(
47
  d. 如果所有序列都生成了 eos_id,则提前终止
48
  4. 返回生成的 token ids [B, T]
49
  """
50
- raise NotImplementedError("TODO: Person D 实现 greedy_decode")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
 
52
 
53
  @torch.no_grad()
@@ -86,7 +101,76 @@ def beam_search_decode(
86
 
87
  这是翻译任务最关键的解码算法,请仔细实现。
88
  """
89
- raise NotImplementedError("TODO: Person D 实现 beam_search_decode")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
 
91
 
92
  @torch.no_grad()
@@ -111,7 +195,46 @@ def sample_decode(
111
  4. 应用 top-p (nucleus): 只保留累积概率达到 p 的 token
112
  5. 从过滤后的分布中采样: torch.multinomial
113
  """
114
- raise NotImplementedError("TODO: Person D 实现 sample_decode")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
 
117
  def _apply_no_repeat_ngram(
@@ -126,4 +249,26 @@ def _apply_no_repeat_ngram(
126
  1. 从 generated_tokens 中提取所有已出现的 (ngram_size-1)-gram
127
  2. 对于每个可能导致重复 ngram 的 next token,将其 logits 设为 -inf
128
  """
129
- raise NotImplementedError("TODO: Person D 实现 _apply_no_repeat_ngram")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  d. 如果所有序列都生成了 eos_id,则提前终止
48
  4. 返回生成的 token ids [B, T]
49
  """
50
+ encoder_output = model.encode(src_ids, src_padding_mask)
51
+ batch_size = src_ids.size(0)
52
+ device = src_ids.device
53
+
54
+ decoder_input = torch.full((batch_size, 1), bos_id, dtype=torch.long, device=device)
55
+ finished = torch.zeros(batch_size, dtype=torch.bool, device=device)
56
+
57
+ for _ in range(max_len):
58
+ logits = model.decode_step(decoder_input, encoder_output, src_padding_mask)
59
+ next_token = logits.argmax(dim=-1, keepdim=True)
60
+ decoder_input = torch.cat([decoder_input, next_token], dim=1)
61
+ finished = finished | next_token.squeeze(-1).eq(eos_id)
62
+ if finished.all():
63
+ break
64
+
65
+ return decoder_input
66
 
67
 
68
  @torch.no_grad()
 
101
 
102
  这是翻译任务最关键的解码算法,请仔细实现。
103
  """
104
+ batch_size, seq_len = src_ids.size()
105
+ device = src_ids.device
106
+
107
+ # 1. Encode
108
+ encoder_output = encoder_output = model.encode(src_ids, src_padding_mask)
109
+
110
+ # 2. Expand encoder output for beam search: [B*beam, S, D]
111
+ encoder_output = encoder_output.unsqueeze(1).expand(-1, beam_size, -1, -1)
112
+ encoder_output = encoder_output.reshape(batch_size * beam_size, seq_len, -1)
113
+ src_padding_mask_expanded = src_padding_mask.unsqueeze(1).expand(-1, beam_size, -1)
114
+ src_padding_mask_expanded = src_padding_mask_expanded.reshape(batch_size * beam_size, seq_len)
115
+
116
+ # 3. Initialize beams
117
+ beam_scores = torch.zeros(batch_size, beam_size, device=device)
118
+ beam_scores[:, 1:] = float("-inf") # Only first beam is active initially
119
+ beam_tokens = torch.full((batch_size, beam_size, 1), bos_id, dtype=torch.long, device=device)
120
+ finished = torch.zeros(batch_size, beam_size, dtype=torch.bool, device=device)
121
+
122
+ # 4. Iterative decoding
123
+ for _ in range(max_len):
124
+ flat_tokens = beam_tokens.view(batch_size * beam_size, -1)
125
+ logits = model.decode_step(flat_tokens, encoder_output, src_padding_mask_expanded)
126
+ log_probs = F.log_softmax(logits, dim=-1)
127
+ vocab_size = log_probs.size(-1)
128
+
129
+ # (c) Apply no_repeat_ngram constraint
130
+ if no_repeat_ngram_size > 0:
131
+ log_probs = _apply_no_repeat_ngram(log_probs, flat_tokens, no_repeat_ngram_size)
132
+
133
+ # Mask finished beams
134
+ finished_flat = finished.view(batch_size * beam_size)
135
+ if finished_flat.any():
136
+ log_probs[finished_flat] = float("-inf")
137
+ log_probs[finished_flat, eos_id] = 0.0
138
+
139
+ # (d) Compute scores
140
+ scores = beam_scores.unsqueeze(-1) + log_probs.view(batch_size, beam_size, vocab_size)
141
+ scores = scores.view(batch_size, -1) # [B, beam * vocab]
142
+
143
+ # (e) Select top-k
144
+ topk_scores, topk_indices = scores.topk(beam_size, dim=-1)
145
+ beam_indices = topk_indices // vocab_size
146
+ token_indices = topk_indices % vocab_size
147
+
148
+ # (f) Update beam tokens and scores
149
+ new_tokens = []
150
+ new_finished = []
151
+ for b in range(batch_size):
152
+ prev_seqs = beam_tokens[b][beam_indices[b]]
153
+ next_tokens = token_indices[b].unsqueeze(-1)
154
+ new_tokens.append(torch.cat([prev_seqs, next_tokens], dim=-1))
155
+ new_finished.append(finished[b][beam_indices[b]] | token_indices[b].eq(eos_id))
156
+
157
+ beam_tokens = torch.stack(new_tokens, dim=0)
158
+ finished = torch.stack(new_finished, dim=0)
159
+ beam_scores = topk_scores
160
+
161
+ if finished.all():
162
+ break
163
+
164
+ # 5. Apply length penalty
165
+ lengths = beam_tokens.size(-1) - 1 # Exclude BOS
166
+ penalties = lengths ** length_penalty
167
+ final_scores = beam_scores / penalties
168
+
169
+ # 6. Select best beam for each batch
170
+ best_indices = final_scores.argmax(dim=-1)
171
+ best_sequences = beam_tokens[torch.arange(batch_size, device=device), best_indices]
172
+
173
+ return best_sequences
174
 
175
 
176
  @torch.no_grad()
 
195
  4. 应用 top-p (nucleus): 只保留累积概率达到 p 的 token
196
  5. 从过滤后的分布中采样: torch.multinomial
197
  """
198
+ encoder_output = model.encode(src_ids, src_padding_mask)
199
+ batch_size = src_ids.size(0)
200
+ device = src_ids.device
201
+
202
+ decoder_input = torch.full((batch_size, 1), bos_id, dtype=torch.long, device=device)
203
+ finished = torch.zeros(batch_size, dtype=torch.bool, device=device)
204
+
205
+ for _ in range(max_len):
206
+ logits = model.decode_step(decoder_input, encoder_output, src_padding_mask)
207
+
208
+ # 2. Apply temperature
209
+ logits = logits / max(temperature, 1e-8)
210
+
211
+ # 3. Apply top-k filtering
212
+ if top_k > 0:
213
+ k = min(top_k, logits.size(-1))
214
+ topk_values, _ = torch.topk(logits, k, dim=-1)
215
+ threshold = topk_values[:, -1].unsqueeze(-1)
216
+ logits[logits < threshold] = float("-inf")
217
+
218
+ # 4. Apply top-p (nucleus) filtering
219
+ if 0.0 < top_p < 1.0:
220
+ sorted_logits, sorted_indices = torch.sort(logits, descending=True, dim=-1)
221
+ cumulative_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1)
222
+ mask = cumulative_probs - F.softmax(sorted_logits, dim=-1) >= top_p
223
+ sorted_logits[mask] = float("-inf")
224
+ logits = sorted_logits.scatter(1, sorted_indices.argsort(1), sorted_logits)
225
+
226
+ # 5. Sample from filtered distribution
227
+ probs = F.softmax(logits, dim=-1)
228
+ next_token = torch.multinomial(probs, num_samples=1)
229
+
230
+ next_token = torch.where(finished.unsqueeze(-1), torch.full_like(next_token, eos_id), next_token)
231
+ decoder_input = torch.cat([decoder_input, next_token], dim=1)
232
+ finished = finished | next_token.squeeze(-1).eq(eos_id)
233
+
234
+ if finished.all():
235
+ break
236
+
237
+ return decoder_input
238
 
239
 
240
  def _apply_no_repeat_ngram(
 
249
  1. 从 generated_tokens 中提取所有已出现的 (ngram_size-1)-gram
250
  2. 对于每个可能导致重复 ngram 的 next token,将其 logits 设为 -inf
251
  """
252
+ if ngram_size <= 0:
253
+ return logits
254
+
255
+ batch_size = logits.size(0)
256
+ for batch_idx in range(batch_size):
257
+ tokens = generated_tokens[batch_idx].tolist()
258
+ if len(tokens) < ngram_size - 1:
259
+ continue
260
+
261
+ # Build map of (n-1)-gram prefix -> set of next tokens that appeared
262
+ ngram_map: dict[tuple, set] = {}
263
+ for i in range(len(tokens) - ngram_size + 1):
264
+ prefix = tuple(tokens[i : i + ngram_size - 1])
265
+ next_tok = tokens[i + ngram_size - 1]
266
+ ngram_map.setdefault(prefix, set()).add(next_tok)
267
+
268
+ # Check current prefix and ban tokens that would create repeated n-grams
269
+ current_prefix = tuple(tokens[-(ngram_size - 1):])
270
+ if current_prefix in ngram_map:
271
+ banned = list(ngram_map[current_prefix])
272
+ logits[batch_idx, banned] = float("-inf")
273
+
274
+ return logits
src/easytranslate/evaluation/evaluator.py CHANGED
@@ -39,7 +39,65 @@ class Evaluator:
39
  2. 从 config 读取解码策略和评估指标配置
40
  3. 根据策略选择解码函数
41
  """
42
- raise NotImplementedError("TODO: Person D 实现 Evaluator.__init__")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
  def evaluate(
45
  self,
@@ -57,7 +115,35 @@ class Evaluator:
57
  4. 调用 compute_all_metrics 计算指标
58
  5. 返回评估结果 dict
59
  """
60
- raise NotImplementedError("TODO: Person D 实现 evaluate")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
  def translate(self, texts: list[str]) -> list[str]:
63
  """
@@ -69,7 +155,28 @@ class Evaluator:
69
  3. 解码为文本
70
  4. 返回翻译结果列表
71
  """
72
- raise NotImplementedError("TODO: Person D 实现 translate")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
  def translate_single(self, text: str) -> str:
75
  """翻译单��文本。"""
 
39
  2. 从 config 读取解码策略和评估指标配置
40
  3. 根据策略选择解码函数
41
  """
42
+ self.model = model
43
+ self.tokenizer = tokenizer
44
+ self.config = config
45
+
46
+ # 从 config 读取评估和解码配置
47
+ eval_config = config.get("evaluation", {})
48
+ decoding_config = eval_config.get("decoding", {})
49
+
50
+ self.strategy = decoding_config.get("strategy", "beam_search")
51
+ self.max_len = decoding_config.get("max_decode_len", 256)
52
+ self.beam_size = decoding_config.get("beam_size", 5)
53
+ self.length_penalty = decoding_config.get("length_penalty", 1.0)
54
+ self.no_repeat_ngram_size = decoding_config.get("no_repeat_ngram_size", 0)
55
+
56
+ sampling_config = decoding_config.get("sampling", {})
57
+ self.temperature = sampling_config.get("temperature", 1.0)
58
+ self.top_k = sampling_config.get("top_k", 0)
59
+ self.top_p = sampling_config.get("top_p", 1.0)
60
+
61
+ self.metrics = eval_config.get("metrics", ["bleu", "comet", "chrf", "ter"])
62
+
63
+ self.bos_id = tokenizer.bos_token_id
64
+ self.eos_id = tokenizer.eos_token_id
65
+ self.pad_id = tokenizer.pad_token_id
66
+
67
+ # 根据策略选择解码函数
68
+ self.decode_fn = self._get_decode_fn()
69
+
70
+ def _get_decode_fn(self):
71
+ """根据策略选择解码函数。"""
72
+ if self.strategy == "greedy":
73
+ return self._greedy
74
+ elif self.strategy == "sampling":
75
+ return self._sample
76
+ else:
77
+ return self._beam_search
78
+
79
+ def _greedy(self, src_ids, src_padding_mask):
80
+ return greedy_decode(
81
+ self.model, src_ids, src_padding_mask,
82
+ self.bos_id, self.eos_id, max_len=self.max_len,
83
+ )
84
+
85
+ def _beam_search(self, src_ids, src_padding_mask):
86
+ return beam_search_decode(
87
+ self.model, src_ids, src_padding_mask,
88
+ self.bos_id, self.eos_id,
89
+ beam_size=self.beam_size, max_len=self.max_len,
90
+ length_penalty=self.length_penalty,
91
+ no_repeat_ngram_size=self.no_repeat_ngram_size,
92
+ )
93
+
94
+ def _sample(self, src_ids, src_padding_mask):
95
+ return sample_decode(
96
+ self.model, src_ids, src_padding_mask,
97
+ self.bos_id, self.eos_id,
98
+ max_len=self.max_len, temperature=self.temperature,
99
+ top_k=self.top_k, top_p=self.top_p,
100
+ )
101
 
102
  def evaluate(
103
  self,
 
115
  4. 调用 compute_all_metrics 计算指标
116
  5. 返回评估结果 dict
117
  """
118
+ self.model.eval()
119
+ device = next(self.model.parameters()).device
120
+
121
+ hypotheses = []
122
+
123
+ with torch.no_grad():
124
+ for batch in tqdm(dataloader, desc="Evaluating"):
125
+ src_ids = batch["src_ids"].to(device)
126
+ src_padding_mask = batch.get("src_padding_mask")
127
+ if src_padding_mask is None:
128
+ src_padding_mask = src_ids.eq(self.pad_id)
129
+ else:
130
+ src_padding_mask = src_padding_mask.to(device)
131
+
132
+ output_ids = self.decode_fn(src_ids, src_padding_mask)
133
+
134
+ for i in range(output_ids.size(0)):
135
+ text = self.tokenizer.decode(
136
+ output_ids[i].tolist(), skip_special_tokens=True
137
+ )
138
+ hypotheses.append(text)
139
+
140
+ results = compute_all_metrics(
141
+ sources=src_texts,
142
+ hypotheses=hypotheses,
143
+ references=ref_texts,
144
+ metrics=self.metrics,
145
+ )
146
+ return results
147
 
148
  def translate(self, texts: list[str]) -> list[str]:
149
  """
 
155
  3. 解码为文本
156
  4. 返回翻译结果列表
157
  """
158
+ self.model.eval()
159
+ device = next(self.model.parameters()).device
160
+
161
+ # 1. Tokenize
162
+ encoded = [self.tokenizer.encode(t, add_special_tokens=True) for t in texts]
163
+ max_len_src = max(len(ids) for ids in encoded)
164
+ src_ids = torch.full((len(texts), max_len_src), self.pad_id, dtype=torch.long, device=device)
165
+ for i, ids in enumerate(encoded):
166
+ src_ids[i, :len(ids)] = torch.tensor(ids, dtype=torch.long)
167
+ src_padding_mask = src_ids.eq(self.pad_id)
168
+
169
+ # 2. Decode
170
+ with torch.no_grad():
171
+ output_ids = self.decode_fn(src_ids, src_padding_mask)
172
+
173
+ # 3. Convert to text
174
+ translations = []
175
+ for i in range(output_ids.size(0)):
176
+ text = self.tokenizer.decode(output_ids[i].tolist(), skip_special_tokens=True)
177
+ translations.append(text)
178
+
179
+ return translations
180
 
181
  def translate_single(self, text: str) -> str:
182
  """翻译单��文本。"""
src/easytranslate/evaluation/metrics.py CHANGED
@@ -18,8 +18,11 @@
18
  from __future__ import annotations
19
 
20
  import logging
 
21
  from typing import Optional
22
 
 
 
23
  logger = logging.getLogger(__name__)
24
 
25
 
@@ -38,7 +41,15 @@ def compute_bleu(
38
 
39
  注意: references 需要包装为 list of list (支持多参考)
40
  """
41
- raise NotImplementedError("TODO: Person D 实现 compute_bleu")
 
 
 
 
 
 
 
 
42
 
43
 
44
  def compute_comet(
@@ -60,7 +71,17 @@ def compute_comet(
60
 
61
  COMET 需要源语言、翻译结果和参考翻译三者。
62
  """
63
- raise NotImplementedError("TODO: Person D 实现 compute_comet")
 
 
 
 
 
 
 
 
 
 
64
 
65
 
66
  def compute_chrf(
@@ -74,7 +95,8 @@ def compute_chrf(
74
  1. 使用 sacrebleu.corpus_chrf(hypotheses, [references])
75
  2. 返回 {"chrf": score}
76
  """
77
- raise NotImplementedError("TODO: Person D 实现 compute_chrf")
 
78
 
79
 
80
  def compute_ter(
@@ -88,7 +110,8 @@ def compute_ter(
88
  1. 使用 sacrebleu.corpus_ter(hypotheses, [references])
89
  2. 返回 {"ter": score}
90
  """
91
- raise NotImplementedError("TODO: Person D 实现 compute_ter")
 
92
 
93
 
94
  def compute_all_metrics(
@@ -106,4 +129,21 @@ def compute_all_metrics(
106
  3. 合并结果并返回
107
  4. 记录每个指标的计算时间
108
  """
109
- raise NotImplementedError("TODO: Person D 实现 compute_all_metrics")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  from __future__ import annotations
19
 
20
  import logging
21
+ import time
22
  from typing import Optional
23
 
24
+ import sacrebleu
25
+
26
  logger = logging.getLogger(__name__)
27
 
28
 
 
41
 
42
  注意: references 需要包装为 list of list (支持多参考)
43
  """
44
+ bleu = sacrebleu.corpus_bleu(hypotheses, [references], tokenize=tokenize)
45
+ return {
46
+ "bleu": round(float(bleu.score), 4),
47
+ "bleu_1": round(float(bleu.precisions[0]), 4),
48
+ "bleu_2": round(float(bleu.precisions[1]), 4),
49
+ "bleu_3": round(float(bleu.precisions[2]), 4),
50
+ "bleu_4": round(float(bleu.precisions[3]), 4),
51
+ "bp": round(float(bleu.bp), 4),
52
+ }
53
 
54
 
55
  def compute_comet(
 
71
 
72
  COMET 需要源语言、翻译结果和参考翻译三者。
73
  """
74
+ from comet import download_model, load_from_checkpoint
75
+
76
+ model_path = download_model(model_name)
77
+ model = load_from_checkpoint(model_path)
78
+
79
+ data = [{"src": s, "mt": h, "ref": r} for s, h, r in zip(sources, hypotheses, references)]
80
+ prediction = model.predict(data, batch_size=batch_size, gpus=gpus)
81
+
82
+ system_score = prediction.system_score
83
+ segment_scores = [float(s) for s in prediction.scores]
84
+ return {"comet": round(float(system_score), 4), "comet_scores": segment_scores}
85
 
86
 
87
  def compute_chrf(
 
95
  1. 使用 sacrebleu.corpus_chrf(hypotheses, [references])
96
  2. 返回 {"chrf": score}
97
  """
98
+ chrf = sacrebleu.corpus_chrf(hypotheses, [references])
99
+ return {"chrf": round(float(chrf.score), 4)}
100
 
101
 
102
  def compute_ter(
 
110
  1. 使用 sacrebleu.corpus_ter(hypotheses, [references])
111
  2. 返回 {"ter": score}
112
  """
113
+ ter = sacrebleu.corpus_ter(hypotheses, [references])
114
+ return {"ter": round(float(ter.score), 4)}
115
 
116
 
117
  def compute_all_metrics(
 
129
  3. 合并结果并返回
130
  4. 记录每个指标的计算时间
131
  """
132
+ results = {}
133
+ metric_funcs = {
134
+ "bleu": lambda: compute_bleu(hypotheses, references),
135
+ "comet": lambda: compute_comet(sources, hypotheses, references),
136
+ "chrf": lambda: compute_chrf(hypotheses, references),
137
+ "ter": lambda: compute_ter(hypotheses, references),
138
+ }
139
+
140
+ for metric in metrics:
141
+ if metric not in metric_funcs:
142
+ logger.warning("Unknown metric: %s, skipping", metric)
143
+ continue
144
+ start = time.time()
145
+ results.update(metric_funcs[metric]())
146
+ elapsed = time.time() - start
147
+ logger.info("Computed %s in %.2fs", metric, elapsed)
148
+
149
+ return results