flashrag-dev 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. flashrag/__init__.py +0 -0
  2. flashrag/config/__init__.py +2 -0
  3. flashrag/config/config.py +219 -0
  4. flashrag/dataset/__init__.py +2 -0
  5. flashrag/dataset/dataset.py +160 -0
  6. flashrag/dataset/utils.py +60 -0
  7. flashrag/evaluator/__init__.py +2 -0
  8. flashrag/evaluator/_bleu.py +209 -0
  9. flashrag/evaluator/evaluator.py +82 -0
  10. flashrag/evaluator/metrics.py +508 -0
  11. flashrag/evaluator/utils.py +19 -0
  12. flashrag/generator/__init__.py +2 -0
  13. flashrag/generator/fid.py +247 -0
  14. flashrag/generator/generator.py +623 -0
  15. flashrag/generator/openai_generator.py +96 -0
  16. flashrag/generator/stop_word_criteria.py +105 -0
  17. flashrag/judger/__init__.py +1 -0
  18. flashrag/judger/judger.py +184 -0
  19. flashrag/pipeline/__init__.py +3 -0
  20. flashrag/pipeline/active_pipeline.py +980 -0
  21. flashrag/pipeline/branching_pipeline.py +250 -0
  22. flashrag/pipeline/pipeline.py +248 -0
  23. flashrag/pipeline/replug_utils.py +249 -0
  24. flashrag/prompt/__init__.py +1 -0
  25. flashrag/prompt/base_prompt.py +165 -0
  26. flashrag/prompt/selfask_examplars.py +108 -0
  27. flashrag/prompt/trace_examplars.py +4015 -0
  28. flashrag/refiner/__init__.py +2 -0
  29. flashrag/refiner/kg_refiner.py +612 -0
  30. flashrag/refiner/llmlingua_compressor.py +2360 -0
  31. flashrag/refiner/refiner.py +252 -0
  32. flashrag/refiner/selective_context_compressor.py +294 -0
  33. flashrag/retriever/__init__.py +3 -0
  34. flashrag/retriever/__main__.py +4 -0
  35. flashrag/retriever/encoder.py +120 -0
  36. flashrag/retriever/index_builder.py +334 -0
  37. flashrag/retriever/reranker.py +145 -0
  38. flashrag/retriever/retriever.py +346 -0
  39. flashrag/retriever/utils.py +49 -0
  40. flashrag/utils/__init__.py +2 -0
  41. flashrag/utils/constants.py +51 -0
  42. flashrag/utils/pred_parse.py +25 -0
  43. flashrag/utils/utils.py +135 -0
  44. flashrag_dev-0.1.1.dist-info/LICENSE +21 -0
  45. flashrag_dev-0.1.1.dist-info/METADATA +616 -0
  46. flashrag_dev-0.1.1.dist-info/RECORD +48 -0
  47. flashrag_dev-0.1.1.dist-info/WHEEL +5 -0
  48. flashrag_dev-0.1.1.dist-info/top_level.txt +1 -0
flashrag/__init__.py ADDED
File without changes
@@ -0,0 +1,2 @@
1
+ from flashrag.config.config import Config
2
+
@@ -0,0 +1,219 @@
1
+ import re
2
+ import os
3
+ import yaml
4
+ import random
5
+ import datetime
6
+
7
+
8
+ class Config:
9
+ def __init__(self, config_file_path=None, config_dict={}):
10
+
11
+ self.yaml_loader = self._build_yaml_loader()
12
+ self.file_config = self._load_file_config(config_file_path)
13
+ self.variable_config = config_dict
14
+
15
+ self.external_config = self._merge_external_config()
16
+
17
+ self.internal_config = self._get_internal_config()
18
+
19
+ self.final_config = self._get_final_config()
20
+
21
+ self._check_final_config()
22
+ self._set_additional_key()
23
+
24
+ self._init_device()
25
+ self._set_seed()
26
+ self._prepare_dir()
27
+
28
+ def _build_yaml_loader(self):
29
+ loader = yaml.FullLoader
30
+ loader.add_implicit_resolver(
31
+ "tag:yaml.org,2002:float",
32
+ re.compile(
33
+ """^(?:
34
+ [-+]?(?:[0-9][0-9_]*)\\.[0-9_]*(?:[eE][-+]?[0-9]+)?
35
+ |[-+]?(?:[0-9][0-9_]*)(?:[eE][-+]?[0-9]+)
36
+ |\\.[0-9_]+(?:[eE][-+][0-9]+)?
37
+ |[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\\.[0-9_]*
38
+ |[-+]?\\.(?:inf|Inf|INF)
39
+ |\\.(?:nan|NaN|NAN))$""",
40
+ re.X,
41
+ ),
42
+ list("-+0123456789."),
43
+ )
44
+ return loader
45
+
46
+ def _load_file_config(self, config_file_path: str):
47
+ file_config = dict()
48
+ if config_file_path:
49
+ with open(config_file_path, "r", encoding="utf-8") as f:
50
+ file_config.update(yaml.load(f.read(), Loader=self.yaml_loader))
51
+ return file_config
52
+
53
+ @staticmethod
54
+ def _update_dict(old_dict: dict, new_dict: dict):
55
+ # Update the original update method of the dictionary:
56
+ # If there is the same key in `old_dict` and `new_dict`, and value is of type dict, update the key in dict
57
+
58
+ same_keys = []
59
+ for key, value in new_dict.items():
60
+ if key in old_dict and isinstance(value, dict):
61
+ same_keys.append(key)
62
+ for key in same_keys:
63
+ old_item = old_dict[key]
64
+ new_item = new_dict[key]
65
+ old_item.update(new_item)
66
+ new_dict[key] = old_item
67
+
68
+ old_dict.update(new_dict)
69
+ return old_dict
70
+
71
+ def _merge_external_config(self):
72
+ external_config = dict()
73
+ external_config = self._update_dict(external_config, self.file_config)
74
+ external_config = self._update_dict(external_config, self.variable_config)
75
+
76
+ return external_config
77
+
78
+ def _get_internal_config(self):
79
+ current_path = os.path.dirname(os.path.realpath(__file__))
80
+ init_config_path = os.path.join(current_path, "basic_config.yaml")
81
+ internal_config = self._load_file_config(init_config_path)
82
+
83
+ return internal_config
84
+
85
+ def _get_final_config(self):
86
+ final_config = dict()
87
+ final_config = self._update_dict(final_config, self.internal_config)
88
+ final_config = self._update_dict(final_config, self.external_config)
89
+
90
+ return final_config
91
+
92
+ def _check_final_config(self):
93
+ # check split
94
+ split = self.final_config["split"]
95
+ if split is None:
96
+ split = ["train", "dev", "test"]
97
+ if isinstance(split, str):
98
+ split = [split]
99
+ self.final_config["split"] = split
100
+
101
+ def _init_device(self):
102
+ gpu_id = self.final_config["gpu_id"]
103
+ if gpu_id is not None:
104
+ os.environ["CUDA_VISIBLE_DEVICES"] = str(gpu_id)
105
+ import torch
106
+
107
+ self.final_config["device"] = torch.device("cuda")
108
+ else:
109
+ import torch
110
+
111
+ self.final_config["device"] = torch.device("cpu")
112
+
113
+ def _set_additional_key(self):
114
+ # set dataset
115
+ dataset_name = self.final_config["dataset_name"]
116
+ data_dir = self.final_config["data_dir"]
117
+ self.final_config["dataset_path"] = os.path.join(data_dir, dataset_name)
118
+
119
+ # set model path
120
+ retrieval_method = self.final_config["retrieval_method"]
121
+ model2path = self.final_config["model2path"]
122
+ model2pooling = self.final_config["model2pooling"]
123
+ method2index = self.final_config["method2index"]
124
+
125
+ generator_model = self.final_config["generator_model"]
126
+
127
+ if self.final_config["index_path"] is None:
128
+ try:
129
+ self.final_config["index_path"] = method2index[retrieval_method]
130
+ except:
131
+ print("Index is empty!!")
132
+ assert False
133
+
134
+ self.final_config["retrieval_model_path"] = model2path.get(retrieval_method, retrieval_method)
135
+ # TODO: not support when `retrieval_model` is path
136
+
137
+ def set_pooling_method(method, model2pooling):
138
+ for key, value in model2pooling.items():
139
+ if key.lower() in method.lower():
140
+ return value
141
+ return "mean"
142
+
143
+ if self.final_config.get("retrieval_pooling_method") is None:
144
+ self.final_config["retrieval_pooling_method"] = set_pooling_method(retrieval_method, model2pooling)
145
+
146
+ rerank_model_name = self.final_config["rerank_model_name"]
147
+ if self.final_config.get("rerank_model_path") is None:
148
+ if rerank_model_name is not None:
149
+ self.final_config["rerank_model_path"] = model2path.get(rerank_model_name, rerank_model_name)
150
+ if self.final_config["rerank_pooling_method"] is None:
151
+ if rerank_model_name is not None:
152
+ self.final_config["rerank_pooling_method"] = set_pooling_method(rerank_model_name, model2pooling)
153
+
154
+ if self.final_config.get("generator_model_path") is None:
155
+ self.final_config["generator_model_path"] = model2path.get(generator_model, generator_model)
156
+
157
+ if "refiner_name" in self.final_config:
158
+ refiner_model = self.final_config["refiner_name"]
159
+ if "refiner_model_path" not in self.final_config or self.final_config["refiner_model_path"] is None:
160
+ self.final_config["refiner_model_path"] = model2path.get(refiner_model, None)
161
+
162
+ # set model path in metric setting
163
+ metric_setting = self.final_config["metric_setting"]
164
+ metric_tokenizer_name = metric_setting.get("tokenizer_name", None)
165
+ from flashrag.utils.constants import OPENAI_MODEL_DICT
166
+
167
+ if metric_tokenizer_name not in OPENAI_MODEL_DICT:
168
+ metric_tokenizer_name = model2path.get(metric_tokenizer_name, metric_tokenizer_name)
169
+ metric_setting["tokenizer_name"] = metric_tokenizer_name
170
+ self.final_config["metric_setting"] = metric_setting
171
+
172
+ def _prepare_dir(self):
173
+ save_note = self.final_config["save_note"]
174
+ current_time = datetime.datetime.now()
175
+ self.final_config["save_dir"] = os.path.join(
176
+ self.final_config["save_dir"],
177
+ f"{self.final_config['dataset_name']}_{current_time.strftime('%Y_%m_%d_%H_%M')}_{save_note}",
178
+ )
179
+ os.makedirs(self.final_config["save_dir"], exist_ok=True)
180
+ # save config parameters
181
+ config_save_path = os.path.join(self.final_config["save_dir"], "config.yaml")
182
+ with open(config_save_path, "w") as f:
183
+ yaml.dump(self.final_config, f)
184
+
185
+ def _set_seed(self):
186
+ import torch
187
+ import numpy as np
188
+
189
+ seed = self.final_config["seed"]
190
+ random.seed(seed)
191
+ np.random.seed(seed)
192
+ torch.manual_seed(seed)
193
+ torch.cuda.manual_seed(seed)
194
+ torch.cuda.manual_seed_all(seed)
195
+ torch.backends.cudnn.benchmark = False
196
+ torch.backends.cudnn.deterministic = True
197
+
198
+ def __setitem__(self, key, value):
199
+ if not isinstance(key, str):
200
+ raise TypeError("index must be a str.")
201
+ self.final_config[key] = value
202
+
203
+ def __getattr__(self, item):
204
+ if "final_config" not in self.__dict__:
205
+ raise AttributeError("'Config' object has no attribute 'final_config'")
206
+ if item in self.final_config:
207
+ return self.final_config[item]
208
+ raise AttributeError(f"'Config' object has no attribute '{item}'")
209
+
210
+ def __getitem__(self, item):
211
+ return self.final_config.get(item)
212
+
213
+ def __contains__(self, key):
214
+ if not isinstance(key, str):
215
+ raise TypeError("index must be a str.")
216
+ return key in self.final_config
217
+
218
+ def __repr__(self):
219
+ return self.final_config.__str__()
@@ -0,0 +1,2 @@
1
+ from flashrag.dataset.dataset import *
2
+ from flashrag.dataset.utils import *
@@ -0,0 +1,160 @@
1
+ import os
2
+ import json
3
+ import random
4
+ import numpy as np
5
+
6
+
7
+ class Item:
8
+ r"""A container class used to store and manipulate a sample within a dataset.
9
+ Information related to this sample during training/inference will be stored in ```self.output```.
10
+ Each attribute of this class can be used like a dict key(also for key in ```self.output```).
11
+
12
+ """
13
+
14
+ def __init__(self, item_dict):
15
+ self.id = item_dict.get("id", None)
16
+ self.question = item_dict.get("question", None)
17
+ self.golden_answers = item_dict.get("golden_answers", [])
18
+ self.choices = item_dict.get("choices", [])
19
+ self.metadata = item_dict.get("metadata", {})
20
+ self.output = item_dict.get("output", {})
21
+
22
+ def update_output(self, key, value):
23
+ r"""Update the output dict and keep a key in self.output can be used as an attribute."""
24
+ if key in ["id", "question", "golden_answers", "output"]:
25
+ raise AttributeError(f"{key} should not be changed")
26
+ else:
27
+ self.output[key] = value
28
+
29
+ def update_evaluation_score(self, metric_name, metric_score):
30
+ r"""Update the evaluation score of this sample for a metric."""
31
+ if "metric_score" not in self.output:
32
+ self.output["metric_score"] = {}
33
+ self.output["metric_score"][metric_name] = metric_score
34
+
35
+ def __getattr__(self, attr_name):
36
+ if attr_name in ["id", "question", "golden_answers", "metadata", "output", "choices"]:
37
+ return super().__getattribute__(attr_name)
38
+ else:
39
+ output = super().__getattribute__("output")
40
+ if attr_name in output:
41
+ return output[attr_name]
42
+ else:
43
+ raise AttributeError(f"Attribute `{attr_name}` not found")
44
+
45
+ def to_dict(self):
46
+ r"""Convert all information within the data sample into a dict. Information generated
47
+ during the inference will be saved into output field.
48
+
49
+ """
50
+ for k, v in self.output.items():
51
+ if isinstance(k, np.ndarray):
52
+ self.output[k] = v.tolist()
53
+ output = {
54
+ "id": self.id,
55
+ "question": self.question,
56
+ "golden_answers": self.golden_answers,
57
+ "output": self.output,
58
+ }
59
+ if self.metadata != {}:
60
+ output["metadata"] = self.metadata
61
+
62
+ return output
63
+
64
+
65
+ class Dataset:
66
+ """A container class used to store the whole dataset. Inside the class, each data sample will be stored
67
+ in ```Item``` class.
68
+ The properties of the dataset represent the list of attributes corresponding to each item in the dataset.
69
+ """
70
+
71
+ def __init__(self, config=None, dataset_path=None, data=None, sample_num=None, random_sample=False):
72
+ self.config = config
73
+ self.dataset_name = config["dataset_name"]
74
+ self.dataset_path = dataset_path
75
+
76
+ self.sample_num = sample_num
77
+ self.random_sample = random_sample
78
+
79
+ if data is None:
80
+ self.data = self._load_data(self.dataset_name, self.dataset_path)
81
+ else:
82
+ self.data = data
83
+
84
+ def _load_data(self, dataset_name, dataset_path):
85
+ """Load data from the provided dataset_path or directly download the file(TODO)."""
86
+
87
+ if not os.path.exists(dataset_path):
88
+ # TODO: auto download: self._download(dataset_name, dataset_path)
89
+ pass
90
+
91
+ data = []
92
+ with open(dataset_path, "r", encoding="utf-8") as f:
93
+ for line in f:
94
+ item_dict = json.loads(line)
95
+ item = Item(item_dict)
96
+ data.append(item)
97
+ if self.sample_num is not None:
98
+ if self.random_sample:
99
+ print(f"Random sample {self.sample_num} items in test set.")
100
+ data = random.sample(data, self.sample_num)
101
+ else:
102
+ data = data[: self.sample_num]
103
+
104
+ return data
105
+
106
+ def update_output(self, key, value_list):
107
+ """Update the overall output field for each sample in the dataset."""
108
+
109
+ assert len(self.data) == len(value_list)
110
+ for item, value in zip(self.data, value_list):
111
+ item.update_output(key, value)
112
+
113
+ @property
114
+ def question(self):
115
+ return [item.question for item in self.data]
116
+
117
+ @property
118
+ def golden_answers(self):
119
+ return [item.golden_answers for item in self.data]
120
+
121
+ @property
122
+ def id(self):
123
+ return [item.id for item in self.data]
124
+
125
+ @property
126
+ def output(self):
127
+ return [item.output for item in self.data]
128
+
129
+ def get_batch_data(self, attr_name: str, batch_size: int):
130
+ """Get an attribute of dataset items in batch."""
131
+
132
+ for i in range(0, len(self.data), batch_size):
133
+ batch_items = self.data[i : i + batch_size]
134
+ yield [item[attr_name] for item in batch_items]
135
+
136
+ def __getattr__(self, attr_name):
137
+ return [item.__getattr__(attr_name) for item in self.data]
138
+
139
+ def get_attr_data(self, attr_name):
140
+ """For the attributes constructed later (not implemented using property),
141
+ obtain a list of this attribute in the entire dataset.
142
+ """
143
+ return [item[attr_name] for item in self.data]
144
+
145
+ def __getitem__(self, index):
146
+ return self.data[index]
147
+
148
+ def __len__(self):
149
+ return len(self.data)
150
+
151
+ def save(self, save_path):
152
+ """Save the dataset into the original format."""
153
+
154
+ def convert_to_float(d):
155
+ return {k: (v.item() if isinstance(v, np.generic) else v) for k, v in d.items()}
156
+
157
+ save_data = [convert_to_float(item.to_dict()) for item in self.data]
158
+
159
+ with open(save_path, "w") as f:
160
+ json.dump(save_data, f, indent=4)
@@ -0,0 +1,60 @@
1
+ from flashrag.dataset import Dataset
2
+
3
+
4
+ def filter_dataset(dataset: Dataset, filter_func=None):
5
+ if filter_func is None:
6
+ return dataset
7
+ data = dataset.data
8
+ for item in data:
9
+ if not filter_func(item):
10
+ data.remove(item)
11
+ return Dataset(config=dataset.config, data=data)
12
+
13
+
14
+ def split_dataset(dataset: Dataset, split_symbol: list):
15
+ assert len(split_symbol) == len(dataset)
16
+
17
+ data = dataset.data
18
+ data_split = {symbol: [] for symbol in set(split_symbol)}
19
+ for symbol in set(split_symbol):
20
+ symbol_data = [
21
+ x for x, x_symbol in zip(data, split_symbol) if x_symbol == symbol
22
+ ]
23
+ data_split[symbol] = Dataset(config=dataset.config, data=symbol_data)
24
+
25
+ return data_split
26
+
27
+
28
+ def merge_dataset(dataset_split: dict, split_symbol: list):
29
+ assert len(split_symbol) == sum(
30
+ [len(data) for data in dataset_split.values()]
31
+ )
32
+ dataset_split_iter = {
33
+ symbol: iter(dataset.data) for symbol, dataset in dataset_split.items()
34
+ }
35
+
36
+ final_data = []
37
+ for item_symbol in split_symbol:
38
+ final_data.append(next(dataset_split_iter[item_symbol]))
39
+ final_dataset = Dataset(
40
+ config=list(dataset_split.values())[0].config, data=final_data
41
+ )
42
+
43
+ return final_dataset
44
+
45
+
46
+ def get_batch_dataset(dataset: Dataset, batch_size=16):
47
+ data = dataset.data
48
+ for idx in range(0, len(data), batch_size):
49
+ batched_data = data[idx : idx + batch_size]
50
+ batch_dataset = Dataset(config=dataset.config, data=batched_data)
51
+ yield batch_dataset
52
+
53
+
54
+ def merge_batch_dataset(dataset_list: Dataset):
55
+ dataset = dataset_list[0]
56
+ total_data = []
57
+ for batch_dataset in dataset_list:
58
+ total_data.extend(batch_dataset.data)
59
+ dataset = Dataset(config=dataset.config, data=total_data)
60
+ return dataset
@@ -0,0 +1,2 @@
1
+ from flashrag.evaluator.evaluator import *
2
+ from flashrag.evaluator.metrics import *
@@ -0,0 +1,209 @@
1
+ # Source: https://github.com/mjpost/sacrebleu/blob/master/sacrebleu/tokenizers/tokenizer_13a.py
2
+ # Copyright 2020 SacreBLEU Authors.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ import re
17
+ from functools import lru_cache
18
+
19
+
20
+ class BaseTokenizer:
21
+ """A base dummy tokenizer to derive from."""
22
+
23
+ def signature(self):
24
+ """
25
+ Returns a signature for the tokenizer.
26
+ :return: signature string
27
+ """
28
+ return "none"
29
+
30
+ def __call__(self, line):
31
+ """
32
+ Tokenizes an input line with the tokenizer.
33
+ :param line: a segment to tokenize
34
+ :return: the tokenized line
35
+ """
36
+ return line
37
+
38
+
39
+ class TokenizerRegexp(BaseTokenizer):
40
+ def signature(self):
41
+ return "re"
42
+
43
+ def __init__(self):
44
+ self._re = [
45
+ # language-dependent part (assuming Western languages)
46
+ (re.compile(r"([\{-\~\[-\` -\&\(-\+\:-\@\/])"), r" \1 "),
47
+ # tokenize period and comma unless preceded by a digit
48
+ (re.compile(r"([^0-9])([\.,])"), r"\1 \2 "),
49
+ # tokenize period and comma unless followed by a digit
50
+ (re.compile(r"([\.,])([^0-9])"), r" \1 \2"),
51
+ # tokenize dash when preceded by a digit
52
+ (re.compile(r"([0-9])(-)"), r"\1 \2 "),
53
+ # one space only between words
54
+ # NOTE: Doing this in Python (below) is faster
55
+ # (re.compile(r'\s+'), r' '),
56
+ ]
57
+
58
+ @lru_cache(maxsize=2**16)
59
+ def __call__(self, line):
60
+ """Common post-processing tokenizer for `13a` and `zh` tokenizers.
61
+ :param line: a segment to tokenize
62
+ :return: the tokenized line
63
+ """
64
+ for _re, repl in self._re:
65
+ line = _re.sub(repl, line)
66
+
67
+ # no leading or trailing spaces, single space within words
68
+ # return ' '.join(line.split())
69
+ # This line is changed with regards to the original tokenizer (seen above) to return individual words
70
+ return line.split()
71
+
72
+
73
+ class Tokenizer13a(BaseTokenizer):
74
+ def signature(self):
75
+ return "13a"
76
+
77
+ def __init__(self):
78
+ self._post_tokenizer = TokenizerRegexp()
79
+
80
+ @lru_cache(maxsize=2**16)
81
+ def __call__(self, line):
82
+ """Tokenizes an input line using a relatively minimal tokenization
83
+ that is however equivalent to mteval-v13a, used by WMT.
84
+ :param line: a segment to tokenize
85
+ :return: the tokenized line
86
+ """
87
+
88
+ # language-independent part:
89
+ line = line.replace("<skipped>", "")
90
+ line = line.replace("-\n", "")
91
+ line = line.replace("\n", " ")
92
+
93
+ if "&" in line:
94
+ line = line.replace("&quot;", '"')
95
+ line = line.replace("&amp;", "&")
96
+ line = line.replace("&lt;", "<")
97
+ line = line.replace("&gt;", ">")
98
+
99
+ return self._post_tokenizer(f" {line} ")
100
+
101
+
102
+ # Copyright 2017 Google Inc. All Rights Reserved.
103
+ #
104
+ # Licensed under the Apache License, Version 2.0 (the "License");
105
+ # you may not use this file except in compliance with the License.
106
+ # You may obtain a copy of the License at
107
+ #
108
+ # http://www.apache.org/licenses/LICENSE-2.0
109
+ #
110
+ # Unless required by applicable law or agreed to in writing, software
111
+ # distributed under the License is distributed on an "AS IS" BASIS,
112
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
113
+ # See the License for the specific language governing permissions and
114
+ # limitations under the License.
115
+ # ==============================================================================
116
+
117
+ """Python implementation of BLEU and smooth-BLEU.
118
+
119
+ This module provides a Python implementation of BLEU and smooth-BLEU.
120
+ Smooth BLEU is computed following the method outlined in the paper:
121
+ Chin-Yew Lin, Franz Josef Och. ORANGE: a method for evaluating automatic
122
+ evaluation metrics for machine translation. COLING 2004.
123
+ """
124
+
125
+ import collections
126
+ import math
127
+
128
+
129
+ def _get_ngrams(segment, max_order):
130
+ """Extracts all n-grams upto a given maximum order from an input segment.
131
+
132
+ Args:
133
+ segment: text segment from which n-grams will be extracted.
134
+ max_order: maximum length in tokens of the n-grams returned by this
135
+ methods.
136
+
137
+ Returns:
138
+ The Counter containing all n-grams upto max_order in segment
139
+ with a count of how many times each n-gram occurred.
140
+ """
141
+ ngram_counts = collections.Counter()
142
+ for order in range(1, max_order + 1):
143
+ for i in range(0, len(segment) - order + 1):
144
+ ngram = tuple(segment[i : i + order])
145
+ ngram_counts[ngram] += 1
146
+ return ngram_counts
147
+
148
+
149
+ def compute_bleu(reference_corpus, translation_corpus, max_order=4, smooth=False):
150
+ """Computes BLEU score of translated segments against one or more references.
151
+
152
+ Args:
153
+ reference_corpus: list of lists of references for each translation. Each
154
+ reference should be tokenized into a list of tokens.
155
+ translation_corpus: list of translations to score. Each translation
156
+ should be tokenized into a list of tokens.
157
+ max_order: Maximum n-gram order to use when computing BLEU score.
158
+ smooth: Whether or not to apply Lin et al. 2004 smoothing.
159
+
160
+ Returns:
161
+ 3-Tuple with the BLEU score, n-gram precisions, geometric mean of n-gram
162
+ precisions and brevity penalty.
163
+ """
164
+ matches_by_order = [0] * max_order
165
+ possible_matches_by_order = [0] * max_order
166
+ reference_length = 0
167
+ translation_length = 0
168
+ for references, translation in zip(reference_corpus, translation_corpus):
169
+ reference_length += min(len(r) for r in references)
170
+ translation_length += len(translation)
171
+
172
+ merged_ref_ngram_counts = collections.Counter()
173
+ for reference in references:
174
+ merged_ref_ngram_counts |= _get_ngrams(reference, max_order)
175
+ translation_ngram_counts = _get_ngrams(translation, max_order)
176
+ overlap = translation_ngram_counts & merged_ref_ngram_counts
177
+ for ngram in overlap:
178
+ matches_by_order[len(ngram) - 1] += overlap[ngram]
179
+ for order in range(1, max_order + 1):
180
+ possible_matches = len(translation) - order + 1
181
+ if possible_matches > 0:
182
+ possible_matches_by_order[order - 1] += possible_matches
183
+
184
+ precisions = [0] * max_order
185
+ for i in range(0, max_order):
186
+ if smooth:
187
+ precisions[i] = (matches_by_order[i] + 1.0) / (possible_matches_by_order[i] + 1.0)
188
+ else:
189
+ if possible_matches_by_order[i] > 0:
190
+ precisions[i] = float(matches_by_order[i]) / possible_matches_by_order[i]
191
+ else:
192
+ precisions[i] = 0.0
193
+
194
+ if min(precisions) > 0:
195
+ p_log_sum = sum((1.0 / max_order) * math.log(p) for p in precisions)
196
+ geo_mean = math.exp(p_log_sum)
197
+ else:
198
+ geo_mean = 0
199
+
200
+ ratio = float(translation_length) / reference_length
201
+
202
+ if ratio > 1.0:
203
+ bp = 1.0
204
+ else:
205
+ bp = math.exp(1 - 1.0 / ratio)
206
+
207
+ bleu = geo_mean * bp
208
+
209
+ return (bleu, precisions, bp, ratio, translation_length, reference_length)