File size: 33,377 Bytes
c95c7b0 | 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 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 | import abc
import logging
import re
import datasets
import os
import numpy as np
import promptsource.templates
from abc import abstractmethod
from typing import Callable, List, Mapping, Optional, Tuple, Union
from lm_eval.api import utils
from lm_eval.api.metric import (
bits_per_byte,
bleu,
mean,
rouge,
sari,
weighted_perplexity,
)
from lm_eval.api.request import Request, rf
logger = logging.getLogger(__name__)
class Task(abc.ABC):
"""A task represents an entire benchmark including its dataset, problems,
answers, and evaluation methods. See BoolQ for a simple example implementation
A `doc` can be any python object which represents one instance of evaluation.
This is usually a dictionary e.g.
{"question": ..., "answer": ...} or
{"question": ..., question, answer)
"""
VERSION = 0
# The name of the `Task` benchmark as denoted in the HuggingFace datasets Hub
# or a path to a custom `datasets` loading script.
DATASET_PATH: str = None
# The name of a subset within `DATASET_PATH`.
DATASET_NAME: str = None
def __init__(
self,
data_dir: Optional[str] = None,
cache_dir: Optional[str] = None,
download_mode: Optional[str] = None,
file_path: Optional[str] = None,
):
"""
Args:
data_dir (str, optional, defaults to None):
Stores the path to a local folder containing the `Task`'s data
files. Use this to specify the path to manually downloaded data
(usually when the dataset is not publicly accessible).
cache_dir (str, optional, defaults to None):
The directory to read/write the `Task` dataset. This follows the
HuggingFace `datasets` API with the default cache directory located
at:
`~/.cache/huggingface/datasets`
NOTE: You can change the cache location globally for a given
process by setting the shell environment variable,
`HF_DATASETS_CACHE`, to another directory:
`export HF_DATASETS_CACHE="/path/to/another/directory"`
download_mode (datasets.DownloadMode, optional, defaults to None):
How to treat pre-existing `Task` downloads and data.
- `datasets.DownloadMode.REUSE_DATASET_IF_EXISTS`
Reuse download and reuse dataset.
- `datasets.DownloadMode.REUSE_CACHE_IF_EXISTS`
Reuse download with fresh dataset.
- `datasets.DownloadMode.FORCE_REDOWNLOAD`
Fresh download and fresh dataset.
"""
if file_path:
self.load_from_file(file_path, cache_dir,
download_mode=datasets.DownloadMode.FORCE_REDOWNLOAD)
else:
self.download(data_dir, cache_dir, download_mode)
self._training_docs = None
self._fewshot_docs = None
def download(
self,
data_dir: Optional[str] = None,
cache_dir: Optional[str] = None,
download_mode: Optional[str] = None,
):
"""Downloads and returns the task dataset.
NOTE: Override this method to download the dataset from a custom API.
"""
self.dataset = datasets.load_dataset(
path=self.DATASET_PATH,
name=self.DATASET_NAME,
data_dir=data_dir,
cache_dir=cache_dir,
download_mode=download_mode,
)
def load_from_file(
self,
file_path,
cache_dir: Optional[str] = None,
download_mode: Optional[str] = None,
):
# get split names
splits = {}
dirname = os.path.dirname(file_path)
for filename in os.listdir(dirname):
if not filename.startswith(os.path.basename(file_path)):
continue
if filename.count(".") == 2:
splitname = filename.split(".")[1]
splits[splitname] = os.path.join(dirname, filename)
else:
splits["train"] = os.path.join(dirname, filename)
self.dataset = datasets.load_dataset(
"json",
data_files=splits,
cache_dir=cache_dir,
download_mode=download_mode,
)
@abstractmethod
def has_training_docs(self):
"""Whether the task has a training set"""
pass
@abstractmethod
def has_validation_docs(self):
"""Whether the task has a validation set"""
pass
@abstractmethod
def has_test_docs(self):
"""Whether the task has a test set"""
pass
def training_docs(self) -> datasets.Dataset:
"""
Returns:
A dataset of training documents.
"""
return datasets.Dataset.from_dict({})
def validation_docs(self) -> datasets.Dataset:
"""
Returns:
A dataset of validation documents.
"""
return datasets.Dataset.from_dict({})
def test_docs(self) -> datasets.Dataset:
"""
Returns:
A dataset of test documents.
"""
return datasets.Dataset.from_dict({})
def _process_doc(self, doc):
"""Override this to process (detokenize, strip, replace, etc.) individual
documents. This can be used in a map over documents of a data split.
E.g. `map(self._process_doc, self.dataset["validation"])`
Returns:
The processed version of the specified `doc`.
"""
return doc
@abstractmethod
def doc_to_text(self, doc: dict) -> str:
pass
@abstractmethod
def doc_to_target(self, doc: dict) -> str:
pass
@abstractmethod
def construct_requests(self, doc: dict, ctx: str, args: dict) -> List[Request]:
"""Uses RequestFactory to construct Requests and returns an iterable of
Requests which will be sent to the LM.
Args:
doc (dict):
The document as returned from training_docs, validation_docs, or
test_docs.
ctx (str):
The context string, generated by fewshot_context. This includes
the natural language description, as well as the few shot examples,
and the question part of the document for `doc`.
args (dict):
The specifics of the context, including number of few shots.
Returns:
An iterable of `Request` objects.
"""
pass
@abstractmethod
def process_results(
self, doc: dict, results: list
) -> Union[dict, Tuple[dict, dict]]:
"""Take a single document and the LM results and evaluates, returning a
dict where keys are the names of sub-metrics and values are the values of
the metric for that one document.
Args:
doc (dict):
The document as returned from training_docs, validation_docs, or
test_docs.
results (list):
The results of the requests created in construct_requests.
Returns:
A dict of metric results.
"""
pass
@abstractmethod
def aggregation(self) -> Mapping[str, Callable]:
"""
Returns:
A dictionary where keys are the names of sub-metrics and values are
functions that aggregate a list of metric scores.
{str: [metric_score] -> float}
"""
pass
@abstractmethod
def higher_is_better(self) -> Mapping[str, bool]:
"""
Returns:
A dictionary where keys are the names of sub-metrics and values are
whether a higher value of the sub-metric is better.
{str: bool}
"""
pass
class PromptSourceTask(Task):
"""These are the metrics from promptsource that we have
added default behavior for. If you want to add default behavior for a new metric,
update the functions below. If you want to use one of the following metrics,
*and* add additional custom processing, override `process_results`, `higher_is_better`, and `aggregation`.
"""
CONFIGURED_RANKED_CHOICE_PS_METRICS = {"Accuracy"}
CONFIGURED_GENERATION_PS_METRICS = {"BLEU", "ROUGE", "SARI"}
SPLIT = None
def __init__(
self,
data_dir: Optional[str] = None,
cache_dir: Optional[str] = None,
download_mode: Optional[str] = None,
prompt_template: Optional[promptsource.templates.Template] = None,
example_separator: Optional[str] = "\n###\n",
text_target_separator: Optional[str] = " ",
save_examples: Optional[bool] = True,
file_path: Optional[str] = None,
):
"""
Args:
save_examples (bool, optional, defaults to True):
Whether to save each example and corresponding model predictions
to an output `dict`.
> Few-shot prompting args
example_separator (str, optional, defaults to '\n###\n'):
The string that will be used to separate the few-shot examples
from the prompt example.
Default: '\n###\n'
See Webson & Pavlick (2022) https://arxiv.org/pdf/2109.01247.pdf
for justification of this separator.
text_target_separator (str, optional, defaults to ' '):
The string that will be used to separate the prompt example
from the target text.
NOTE: This is assumed to be some form of whitespace-only separation,
e.g. "\n\n", "\t", " ", etc. Otherwise, you should update
the Task's `promptsource` template with the appropriate
separator(s).
Example:
Q: Where is the Eiffel Tower located? A:{text_target_separator}Paris
"""
assert isinstance(save_examples, bool), "`save_examples` must be a bool."
assert isinstance(example_separator, str) and isinstance(
text_target_separator, str
), "Separator args must be strings."
assert (
text_target_separator.isspace()
), f"`text_target_separator` must be whitespace only. Got: `{text_target_separator}`"
if file_path:
super().__init__(cache_dir=cache_dir, file_path=file_path,
download_mode=download_mode)
else:
super().__init__(data_dir, cache_dir, download_mode)
self.prompt_template = prompt_template
self.save_examples = save_examples
self.example_separator = example_separator
self.text_target_separator = text_target_separator
def stop_sequences(self) -> List[str]:
"""Denote where the generation should end based on the few-shot example
separator.
NOTE: Override this if you want to use a sequence other than just the
task's few-shot example separator.
"""
return [self.example_separator]
def max_generation_length(self) -> Optional[int]:
"""Denote where the max length of the generation if it is obvious from the task."""
return None
def evaluation_docs(self) -> datasets.Dataset:
"""Returns the `dataset` split to be used for evaluation."""
if self.has_test_docs():
return self.test_docs()
elif self.has_validation_docs():
return self.validation_docs()
else:
raise RuntimeError("Task has neither test_docs nor validation_docs")
def fewshot_docs(self) -> datasets.Dataset:
"""Returns the `dataset` split that the few-shot examples should be sample
from. This prioritizes the `train_docs` split as the few-shot example
source, then `validation_docs`, and lastly `test_docs`.
"""
if self.has_training_docs():
return self.training_docs()
elif self.has_validation_docs():
return self.validation_docs()
else:
return self.test_docs()
def doc_to_text(self, doc: dict) -> str:
"""Returns the input string for a particular example, given the hf dict."""
if self.prompt_template is None:
return self.null_prompt_doc_to_text(doc)
# is just a string
text, _ = self.prompt_template.apply(doc)
return text
def null_prompt_doc_to_text(self, doc: dict) -> str:
return NotImplementedError("Override this method in your task!")
def doc_to_target(self, doc: dict) -> List[str]:
"""Returns the target string for a particular example, given the hf dict."""
if self.prompt_template is None:
return self.null_prompt_doc_to_target(doc)
# is a list of strings where it usually only has one element: the correct answer
_, target = self.prompt_template.apply(doc)
return target
def null_prompt_doc_to_target(self, doc: dict) -> List[str]:
return NotImplementedError("Override this method in your task!")
def doc_to_rawtext(self, doc: dict) -> str:
"""This should be used for selecting the raw text of the document.
The current use case is for computing SARI which requires the text
without the prompt. The `text` field is not standardized across tasks
so this is task specific.
"""
raise NotImplementedError("This is task specific.")
def invalid_doc_for_prompt(self, doc) -> bool:
"""Some prompts may not work for some documents.
Default: False
"""
return False
def format_example(self, text: str, target: str, separator: str) -> str:
"""Returns the text and target combined by the specified `separator`"""
return text + separator + target
def null_prompt_answer_choices(self, doc: dict) -> List[str]:
return NotImplementedError("Override this method in your task!")
def fewshot_examples(
self,
docs: datasets.Dataset,
k: int,
rng: np.random.Generator,
prompt: dict = None,
) -> Tuple[List[dict], List[int]]:
"""Returns `k` random examples from the set of documents in `docs`.
Args:
docs (datasets.Dataset):
The dataset of documents to sample few-shot examples from.
k (int):
The number of few-shot examples.
rng (np.random.Generator):
The pseudo-random number generator used to randomly sample examples.
prompt (Optional[dict]):
The prompt document. Specify this to ensure the prompt is not in
the set of few-shot examples.
Returns:
A tuple of two lists. The first list contains the few-shot examples
"""
random_indices = np.arange(len(docs)).tolist()
rng.shuffle(random_indices)
i = 0
fewshot_examples, fewshot_idx = [], []
for idx in random_indices:
if i >= k: # Break when we have enough examples.
break
is_same_prompt = prompt is not None and all(
# Skips the `doc_id` key assigned to `prompt`s during eval pre-processing.
docs[idx][k] == prompt[k]
for k in docs[idx].keys()
)
if self.invalid_doc_for_prompt(docs[idx]) or is_same_prompt:
continue
fewshot_examples.append(docs[idx])
fewshot_idx.append(int(idx))
i += 1
return fewshot_examples, fewshot_idx
def fewshot_context(
self, doc: dict, num_fewshot: int, rng: Optional[np.random.Generator]
) -> Tuple[str, dict]:
"""Returns a few-shot context string made up of `num_fewshot` number of
labeled examples, and an appended prompt example without labeling.
Args:
doc (dict):
The document as returned from training_docs, validation_docs, or test_docs.
num_fewshot (int):
The number of fewshot examples to provide in the returned context string.
rng (numpy.random.Generator):
The pseudo-random number generator used to randomly sample few-shot examples.
Returns:
A few-shot context string and a dictionary containing few-shot context
logging information.
ctx (str):
The fewshot context.
logging_info (dict):
A `dict` of logging info that can be used to identify few-shot
sources.
"""
assert (
rng is not None
), "A `numpy.random.Generator` argument must be provided to `rng`"
if num_fewshot == 0:
labeled_examples = ""
fewshot_idx, fewshot_target_idx, fewshot_src = ([], [], None)
else:
# Construct few-shot labeled examples.
fewshot_docs = self.fewshot_docs()
fewshot_src = str(fewshot_docs.split)
fewshot_examples, fewshot_idx = self.fewshot_examples(
fewshot_docs, k=num_fewshot, rng=rng, prompt=doc
)
labeled_examples_list = []
fewshot_target_idx = []
for fewshot_example in fewshot_examples:
text = self.doc_to_text(fewshot_example)
targets = self.doc_to_target(fewshot_example)
# Choose 1 random target from multi-reference targets.
target_idx = int(rng.integers(0, len(targets)))
target = targets[target_idx].strip()
labeled_examples_list.append(
self.format_example(text, target, self.text_target_separator)
)
fewshot_target_idx.append(target_idx)
labeled_examples = self.example_separator.join(labeled_examples_list)
# Leave an extra `example_separator` right before the prompt.
labeled_examples += self.example_separator
prompt = self.doc_to_text(doc)
ctx = labeled_examples + prompt
logging_info = {
"fewshot_idx": fewshot_idx,
"fewshot_target_idx": fewshot_target_idx,
"fewshot_source": fewshot_src,
"fewshot_num": num_fewshot,
"ctx": ctx,
}
return ctx, logging_info
def construct_requests(self, doc: dict, ctx: str, args: dict) -> List[Request]:
"""Uses RequestFactory to construct Requests and returns an iterable of
Requests which will be sent to the LM.
Args:
doc (dict):
The document as returned from training_docs, validation_docs, or
test_docs.
ctx (str):
The context string, generated by fewshot_context. This includes
the natural language description, as well as the few shot examples,
and the question part of the document for `doc`.
args (dict):
The specifics of the context, including number of few shots.
Returns:
An iterable of `Request` objects.
"""
requests = []
if self.prompt_template is None:
answer_choices_list = self.null_prompt_answer_choices(doc)
else:
answer_choices_list = self.prompt_template.get_answer_choices_list(doc)
if answer_choices_list:
# If answer_choices_list, then this is a ranked choice prompt.
for answer_choice in answer_choices_list:
ll_answer_choice, _ = rf.loglikelihood(
ctx, self.text_target_separator + answer_choice
)
requests.append(ll_answer_choice)
else:
# If not, then this is a generation prompt.
request_args = {
"stop_sequences": self.stop_sequences(),
"max_generation_length": self.max_generation_length(),
"num_fewshot": args["num_fewshot"],
}
cont_request = rf.greedy_until(ctx, request_args)
requests.append(cont_request)
return requests
def process_results(
self, doc: dict, results: list
) -> Union[dict, Tuple[dict, dict]]:
"""Take a single document and the LM results and evaluates, returning a
dict where keys are the names of sub-metrics and values are the values of
the metric for that one document.
NOTE: This function automates processing by using the `promptsource`
metadata to determine the metric.
Args:
doc (dict):
The document as returned from training_docs, validation_docs, or
test_docs.
results (list):
The results of the requests created in construct_requests.
Returns:
A dict of metric results.
"""
if self.prompt_template is None:
answer_choices_list = self.null_prompt_answer_choices(doc)
else:
answer_choices_list = self.prompt_template.get_answer_choices_list(doc)
target = self.doc_to_target(doc)
if answer_choices_list:
# If answer_choices_list, then this is a ranked choice prompt.
# NOTE: In the future, target could be a list of strings.
assert isinstance(target, list) and len(target) == 1
target = target[0].strip()
try:
target_idx = answer_choices_list.index(target)
except ValueError as e:
print("answer_choices_list:", answer_choices_list)
print("target:", target)
raise ValueError(e)
pred = answer_choices_list[np.argmax(results)]
out = {}
metric_list = ["Accuracy"] # TODO: CLI framework for specifying metrics
if self.prompt_template:
metric_list = self.prompt_template.metadata.metrics
for metric in metric_list:
if metric not in self.CONFIGURED_RANKED_CHOICE_PS_METRICS:
logger.warning(
f"Unexpected metric: `{metric}`. Add it, or use a task-specific solution."
)
if metric == "Accuracy":
out["acc"] = pred == target
# Byte-length normalization.
completion_len = np.array(
[float(len(i)) for i in answer_choices_list]
)
out["acc_norm"] = (
1.0
if np.argmax(results / completion_len) == target_idx
else 0.0
)
# TODO: Add metrics here.
else:
# If not, then this is a generation prompt.
# NOTE: In the future, target will be a list of strings.
assert isinstance(target, list)
pred = results[0].strip()
out = {}
for metric in self.prompt_template.metadata.metrics:
if metric not in self.CONFIGURED_GENERATION_PS_METRICS:
logger.warning(
f"Unexpected metric: `{metric}`. Add it, or use a task-specific solution."
)
if metric == "BLEU":
out["bleu"] = (target, pred)
elif metric == "ROUGE":
# TODO: This computes all rouge sub-metrics. Find a generic
# way to handle user specified rouge sub-metrics to avoid extra
# compute.
rouge_scores = rouge(target, pred)
# Flatten rouge score dict.
rouge_scores = utils.flatten(rouge_scores)
# Merge all the rouge-type scores into the `out` dict.
out = {**out, **rouge_scores}
elif metric == "SARI":
out["sari"] = sari(self.doc_to_rawtext(doc), pred, target)
# TODO: Wrap process results s.t. override impl do not
# override the save examples.
if self.save_examples:
example = {
"pred": pred,
"target": target,
"answer_choices_list": answer_choices_list,
}
return out, example
return out
def aggregation(self) -> Mapping[str, Callable]:
out = {}
metric_list = ["Accuracy"]
if self.prompt_template:
metric_list = self.prompt_template.metadata.metrics
for metric in metric_list:
if metric == "Accuracy":
out["acc"] = mean
out["acc_norm"] = mean
elif metric == "BLEU":
out["bleu"] = bleu
elif metric == "ROUGE":
# TODO: Find a generic way to handle user specified rouge metrics.
out["rouge1_precision"] = mean
out["rouge1_recall"] = mean
out["rouge1_fmeasure"] = mean
out["rouge2_precision"] = mean
out["rouge2_recall"] = mean
out["rouge2_fmeasure"] = mean
out["rougeL_precision"] = mean
out["rougeL_recall"] = mean
out["rougeL_fmeasure"] = mean
out["rougeLsum_precision"] = mean
out["rougeLsum_recall"] = mean
out["rougeLsum_fmeasure"] = mean
elif metric == "SARI":
out["sari"] = mean
return out
def higher_is_better(self) -> Mapping[str, bool]:
out = {}
for metric in self.prompt_template.metadata.metrics:
if metric == "Accuracy":
out["acc"] = True
out["acc_norm"] = True
elif metric == "BLEU":
out["bleu"] = True
elif metric == "ROUGE":
# TODO: Find a generic way to handle user specified rouge metrics.
out["rouge1_precision"] = True
out["rouge1_recall"] = True
out["rouge1_fmeasure"] = True
out["rouge2_precision"] = True
out["rouge2_recall"] = True
out["rouge2_fmeasure"] = True
out["rougeL_precision"] = True
out["rougeL_recall"] = True
out["rougeL_fmeasure"] = True
out["rougeLsum_precision"] = True
out["rougeLsum_recall"] = True
out["rougeLsum_fmeasure"] = True
elif metric == "SARI":
out["sari"] = True
return out
def get_logging_info(self):
if self.prompt_template is None:
return self.null_prompt_get_logging_info()
return {
"fixed_answer_choice_list": self.prompt_template.get_fixed_answer_choices_list(),
"dataset_path": self.DATASET_PATH,
"dataset_name": self.DATASET_NAME,
"subset": self.SPLIT,
"prompt_name": self.prompt_template.get_name(),
"prompt_id": self.prompt_template.get_id(),
"prompt_jinja": self.prompt_template.jinja,
"prompt_original_task": self.prompt_template.metadata.original_task,
# Placeholder for comment in post-processing.
"comment": "",
}
class TranslationTask(PromptSourceTask):
# Language specific functions.
@classmethod
def zh_split(cls, zh_text: str) -> List[str]:
"""Chinese splitting"""
import jieba
return [" ".join(jieba.cut(txt.strip())) for txt in zh_text]
@classmethod
def ja_split(cls, ja_text: str) -> List[str]:
"""Japanese splitting"""
import nagisa
return [" ".join(nagisa.tagging(txt.strip()).words) for txt in ja_text]
NO_SPACE_LANG = {"zh": zh_split, "ja": ja_split}
def invalid_doc_for_prompt(self, doc) -> bool:
# Skip docs with empty references.
if self.doc_to_target(doc) == [""]:
return True
return False
def _get_src_ref_codes(self, template_name: str) -> Tuple[str, str]:
"""Returns a 2-tuple of (src_lang, ref_lang) codes from the prompt template name."""
# Get the lang codes from the dataset name.
lang_pairs = self.DATASET_NAME.split("-")
# Template name ordering defines the src and ref lang codes.
if self.DATASET_NAME in template_name:
return lang_pairs[0], lang_pairs[1]
# Flip the lang pairs following the prompt source.
return lang_pairs[1], lang_pairs[0]
def process_results(
self, doc: dict, results: list
) -> Union[dict, Tuple[dict, dict]]:
answer_choices_list = self.prompt_template.get_answer_choices_list(doc)
target = self.doc_to_target(doc)
# Add spaces between words for BLEU score calculation of target languages like Chinese
_, tar_lang_code = self._get_src_ref_codes(self.prompt_template.name)
if tar_lang_code in self.NO_SPACE_LANG:
target = [self.NO_SPACE_LANG[tar_lang_code]([t])[0] for t in target]
results = self.NO_SPACE_LANG[tar_lang_code](results)
pred = results[0].strip()
out = {}
for metric in self.prompt_template.metadata.metrics:
assert (
metric in self.CONFIGURED_GENERATION_PS_METRICS
), "Unexpected metric. Add it, or use a task-specific solution."
if metric == "BLEU":
out["bleu"] = (target, pred)
elif metric == "ROUGE":
# TODO: This computes all rouge sub-metrics. Find a generic
# way to handle user specified rouge sub-metrics to avoid extra
# compute.
rouge_scores = rouge(target, pred)
# Flatten rouge score dict.
rouge_scores = utils.flatten(rouge_scores)
# Merge all the rouge-type scores into the `out` dict.
out = {**out, **rouge_scores}
# TODO: Wrap process results s.t. override impl do not
# override the save examples.
if self.save_examples:
example = {
"pred": pred,
"target": target,
"answer_choices_list": answer_choices_list,
}
return out, example
return out
class PerplexityTask(PromptSourceTask):
"""NOTE: Prompts are ignored for perplexity tasks."""
def doc_to_text(self, doc: dict) -> str:
return ""
def doc_to_target(self, doc: dict) -> List[str]:
"""Because prompts are ignored, return the relevant text from doc."""
raise NotImplementedError()
def fewshot_context(
self,
doc: dict,
num_fewshot: int,
rng: Optional[np.random.Generator],
) -> Tuple[str, dict]:
assert (
num_fewshot == 0
), "The number of fewshot examples must be 0 for perplexity tasks."
assert (
rng is not None
), "A `numpy.random.Generator` argument must be provided to `rng`"
return (
"",
{
"fewshot_idx": [],
"fewshot_target_idx": [],
"fewshot_source": None,
"fewshot_num": 0,
"ctx": "",
},
)
def construct_requests(self, doc: dict, ctx: str, args: dict) -> List[Request]:
assert not ctx
string = self.doc_to_target(doc)[0]
req = rf.loglikelihood_rolling(string)
return req
def process_results(
self, doc: dict, results: list
) -> Union[dict, Tuple[dict, dict]]:
(loglikelihood,) = results
target = self.doc_to_target(doc)[0]
words = self.count_words(target)
bytes_ = self.count_bytes(target)
out = {
"word_perplexity": (loglikelihood, words),
"byte_perplexity": (loglikelihood, bytes_),
"bits_per_byte": (loglikelihood, bytes_),
}
if self.save_examples:
return out, {
"word_perplexity_instance": weighted_perplexity(
[(loglikelihood, words)]
),
"byte_perplexity_instance": weighted_perplexity(
[(loglikelihood, bytes_)]
),
"bits_per_byte_instance": bits_per_byte([(loglikelihood, bytes_)]),
}
return out
def aggregation(self) -> Mapping[str, Callable]:
return {
"word_perplexity": weighted_perplexity,
"byte_perplexity": weighted_perplexity,
"bits_per_byte": bits_per_byte,
}
def higher_is_better(self) -> Mapping[str, bool]:
return {
"word_perplexity": False,
"byte_perplexity": False,
"bits_per_byte": False,
}
@classmethod
def count_bytes(cls, doc):
return len(doc.encode("utf-8"))
@classmethod
def count_words(cls, doc):
"""Downstream tasks with custom word boundaries should override this!"""
return len(re.split(r"\s+", doc))
def get_logging_info(self):
return {
"prompt_name": None,
}
|