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,
        }