CodexQA

Industry & PracticeTools & Frameworks

Inspect 评分指标:准确率、标准误、头条数字和多轮归约

CodexQA 团队12 min read

Inspect 的评分器默认带准确率和标准误。头条数字可用 headline_metric 指定;分组用 grouped(),聚类标准误用 stderr(cluster=)。多轮默认按 mean 归约,frequency() 则看未归约的每一轮。

In this piece

Inspect 评分指标:准确率、标准误、头条数字和多轮归约

本文说明 Inspect 里评分指标怎么选、怎么分组、怎么报标准误,以及多个轮次的分数怎样合成一个数。Inspect 是英国人工智能安全研究所发布的评测框架。指标(metric)是把许多样本的分数收成一个数或一组数的函数。本文依据 Inspect 文档《Scoring Metrics》,许可证为 MIT。

版权声明按 MIT 许可证保留:Copyright (c) 2024 UK AI Security Institute。允许复制、修改和再发布本软件及其配套文档,但须保留版权声明和许可声明。

###### 概述

每个评分器自带一个或多个内置指标,通常是准确率(accuracy,答对的比例)和标准误(stderr,standard error,样本均值波动有多大的估计)。这些是该评分器最常用的指标。

可以把另一份指标清单传给 Task,从而换掉评分器的内置指标。例如:

Task(
    dataset=dataset,
    solver=[
        system_message(SYSTEM_MESSAGE),
        multiple_choice()
    ],
    scorer=choice(),
    metrics=[custom_metric()]
)

如果仍要计算内置指标,就把它们和自定义指标一起再写上:

metrics=[accuracy(), stderr(), custom_metric()]

内置指标

Inspect 带有计算准确率、均值等的简单内置指标。它们从 inspect_ai.scorer 模块导入。下面是这些指标的摘要。完整函数签名和选项见 inspect_ai.scorer 参考。

  • accuracy():计算全部答案里正确的比例。对标成 1 或 0 的对错分数,可以选择把部分正确记为 0.5。
  • mean():全部分数的均值。
  • var():全部分数上的样本方差。
  • std():全部分数上的标准差。聚类标准误的算法见下文。
  • stderr():均值的标准误。
  • bootstrap_stderr():对均值做自助法估计后的标准差。默认取 1000 个样本,可用 num_samples 修改。自助法(bootstrap)是反复有放回地重抽样本,看估计值怎么晃。
  • ci():均值的置信区间,报告成带下界和上界的映射。默认是 95% 的学生 t 区间(均值 ± t · 标准误,自由度为 n − 1;若设置了 cluster=,自由度为簇数 − 1)。用 level= 改置信水平,或用 method="bootstrap" 做百分位(聚类)自助区间。它可用于判断,例如两个模型的准确率区间是否重叠。对二元分数,优先用 ci_wilson()。
  • ci_wilson():二元(0/1)分数均值的 Wilson 得分置信区间,把它当成二项比例。Wilson 区间是一种让比例的置信界始终落在 0 和 1 之间的算法。对准确率这类二元分数,优先用它而不是 ci():界始终留在 [0, 1] 内,并且在小样本、比例靠近 0 或 1 时仍然校准得好。用 level= 改置信水平;加上 cluster= 时,区间使用由聚类标准误导出的有效样本量。
  • frequency():每个不同的类别分数值出现的频率。默认报告已评分观察上的比例;传入 normalize=False 则报告次数。若任务使用轮次(epoch,同一道样本重复跑的一次),frequency() 收到的是尚未归约的各轮分数,因此每个已评分的样本轮次都是一次观察。
  • categorical():给类别评分器的便利函数。它返回带可选声明类别的 frequency(),类别通常来自一个 StrEnum,这样次数为零的类别也会出现,并且指标可以从日志重算。
  • krippendorff_alpha():多个评判者或评分器之间的 Krippendorff α 系数。Krippendorff α 是衡量多名评判者是否一致的一个数,1 表示完全一致。每个样本的 Score.value 必须是一串逐评判者的评分(每个评判者一个元素)。做法是把 multi_scorer() 和 collect 归约器配在一起(见「多评判者一致性」)。该指标在全部样本上计算 α。测量尺度要和评分匹配:
  • level="nominal"(默认):无序类别,例如对错标签或类别编号。任何不同都算完全不一致。
  • level="ordinal":间隔不相等的有序类别,例如 1–5 的李克特评分。距离按中间等级的边际频率加权(Krippendorff 2007)。
  • level="interval":等间隔尺度上的数字。距离是数值差的平方。

对 ordinal 或 interval,非数字评分需要一个 to_float 映射。传入 to_float=value_to_float(),把 CORRECT、INCORRECT、PARTIAL、NOANSWER 这些字符串映射成 1、0、0.5、0;也可以为自己的文本类别提供排序。两个评判者时,名义尺度上的 α 与 Scott 的 π 重合(其多评判者类比是 Fleiss 的 κ);由于 α 有小样本修正,精确重合只发生在大样本极限。

头条指标

有几处要把一次评测收成单个数字:查看器里的日志列表、运行时的进度行,以及 evals_df() 的 score_headline_* 列。默认这个数字是第一个分数的第一个指标。这个默认通常是对的,但在评分器把 stderr() 排在 accuracy() 前面、一个便宜的健全性评分器跑在前面,或你关心的数字来自某个特定轮次归约器时,就不是。

在 Task 上声明 headline_metric,可以明确指定一个。普通字符串指名评分器:

Task(
    dataset=dataset,
    scorer=[sanity_check(), model_graded_qa()],
    headline_metric="model_graded_qa",
)

传入 HeadlineMetric 可以多说一点。它的四个字段都可选,没写的字段退回默认约定:

字段匹配对象不设置的含义
scorerEvalScore.scorer每一个分数。若同时设置了 metric,则是带有该指标的第一个分数;否则是第一个分数。
scoreEvalScore.name所选评分器的每一个分数。
metricEvalScore.metrics 的一个键所选分数的第一个指标。
reducerEvalScore.reducer第一个匹配的分数,不论它的归约器是什么。

因此常见情形可以写得很短。在单评分器任务里改选另一个指标:

from inspect_ai.log import HeadlineMetric

headline_metric=HeadlineMetric(metric="accuracy")

返回多个值的评分器会为每个值的键产生一个分数,名字就是那个键。字符串形式用 "<scorer>.<score>" 指向其中一个:

headline_metric="grader.coherence"

这个简写只在字符串上可用。HeadlineMetric 的字段按字面匹配,所以名字里本身带点的评分器(@scorer(name="judge.v2"))要写在 scorer 字段上:

headline_metric=HeadlineMetric(scorer="grader", score="coherence", metric="mean")
headline_metric=HeadlineMetric(scorer="judge.v2")

按组计算指标

grouped() 把给定指标用到由样本元数据里某个键定义的子组上,为每一组各建一个指标,并另建一个跨全部样本或全部分组汇总的 "all" 指标。每个样本对用于分组的那个键都必须有值。

例如,要为 Sample 元数据里每个不同的 “category” 变量各做一个准确率指标:

@task
def gpqa():
    return Task(
        dataset=read_gpqa_dataset("gpqa_main.csv"),
        solver=[
            system_message(SYSTEM_MESSAGE),
            multiple_choice(),
        ],
        scorer=choice(),
        metrics=[grouped(accuracy(), "category"), stderr()]
    )

传给 Task 的指标会覆盖 choice() 评分器的默认指标。

注意:"all" 指标默认是在全部样本上计算所选指标。若希望它取各分组值的均值,传入 all="groups":

grouped(accuracy(), "category", all="groups")

可以用 name_template 自定义指标名。模板用 {group_name} 作为组值的占位符:

grouped(accuracy(), "category", name_template="category_{group_name}")

这样得到的指标名是 category_physics、category_chemistry 等,而不是 physics、chemistry。它不影响 “all” 指标,那个可以另外命名。

按键汇总

有些评分器发出的 Score.value 是字典,每个样本有若干数值字段。aggregate() 把给定指标用到由 key 选出的单个字段上,因此任何标准指标都可以按键计算:

from inspect_ai.scorer import aggregate, mean, stderr

@metric
def element_accuracy() -> Metric:
    return aggregate("element_acc", mean())

@metric
def element_accuracy_stderr() -> Metric:
    return aggregate("element_acc", stderr())

默认把抽出的值直接交给内部指标,因此适用内部指标自己的转换。只有内部指标自己不能转换该值时才传入 to_float=。例如把 "C"、"I" 这种字符串等级送进期望数值的 mean():

from inspect_ai.scorer import value_to_float

aggregate("verdict", mean(), to_float=value_to_float())

键缺失(或存在但为 None)的样本走 on_missing:"error"(默认)会抛出,"skip" 排除该样本,"zero" 把它计为 0.0。某一键上的 NaN 始终视为未评分并跳过,与框架在别处处理 NaN 分数的方式一致。若每个样本都被滤掉,aggregate() 返回 NaN。

聚类标准误

stderr() 支持用 cluster 参数计算聚类标准误。聚类标准误把同一组里的样本当成相关的,避免把重复观测当成独立样本而把标准误算得过小。多数评分器已经把 stderr() 当作内置指标,所以要算聚类标准误,就得为任务指定自定义指标,这会覆盖评分器的内置指标。

例如,按 Sample 元数据里的 “category” 变量聚类:

@task
def gpqa():
    return Task(
        dataset=read_gpqa_dataset("gpqa_main.csv"),
        solver=[
            system_message(SYSTEM_MESSAGE),
            multiple_choice(),
        ],
        scorer=choice(),
        metrics=[accuracy(), stderr(cluster="category")]
    )

传给 Task 的指标会覆盖 choice() 评分器的默认指标。

多评判者一致性

krippendorff_alpha() 衡量给每个样本打分的那些评判者之间的一致程度,因此它需要 Score.value 是一列逐评判者的评分,而不是一个已经汇总的值。多数 multi_scorer() 归约器("mean"、"mode" 等)把多个评判者收成单个值,丢掉了 α 需要的逐评判者信息。改用 "collect" 归约器,它把每个评判者的评分留成一个列表:

from inspect_ai.scorer import krippendorff_alpha, multi_scorer

@task
def judge_reliability():
    return Task(
        ...,
        scorer=multi_scorer(
            scorers=[judge1, judge2, judge3],
            reducer="collect",
        ),
        metrics=[krippendorff_alpha(level="ordinal")],
    )

这时每个样本的 Score.value 是 [judge1_rating, judge2_rating, judge3_rating],krippendorff_alpha() 跨样本汇总来计算 α。无序类别标签用 level="nominal",李克特式评分用 level="ordinal",连续分数用 level="interval"。细节见该指标的文档字符串。

自定义指标

也可以用带 @metric 装饰器的函数添加自己的指标。下面是 mean 指标的实现:

import numpy as np

from inspect_ai.scorer import Metric, Score, metric

@metric
def mean() -> Metric:
    """Compute mean of all scores.

    Returns:
       mean metric
    """

    def metric(scores: list[SampleScore]) -> float:
        return np.mean([score.score.as_float() for score in scores]).item()

    return metric

Score 类包含一个 Value,它是若干标量和集合类型的联合。为方便起见,Score 提供一组访问方法,把值当成更简单的形式(例如上面用了 score.as_float())。

示例

这个任务把一个浮点评分器和自定义的 pass_rate() 指标配在一起。pass_rate() 是分数达到或超过阈值的样本比例,并与内置的 mean() 和 stderr() 一起报告:

from inspect_ai import Task, task
from inspect_ai.dataset import Sample
from inspect_ai.scorer import (
    Metric,
    SampleScore,
    Score,
    Target,
    mean,
    metric,
    scorer,
    stderr,
)
from inspect_ai.solver import TaskState, generate

@metric
def pass_rate(threshold: float = 0.5) -> Metric:
    """Proportion of samples scoring at or above `threshold`."""

    def metric(scores: list[SampleScore]) -> float:
        if not scores:
            return 0.0
        passed = [s for s in scores if s.score.as_float() >= threshold]
        return len(passed) / len(scores)

    return metric

@scorer(metrics=[mean(), stderr(), pass_rate()])
def word_overlap():
    async def score(state: TaskState, target: Target) -> Score:
        output = state.output.completion.lower()
        words = target.text.lower().split()
        hits = sum(1 for word in words if word in output)
        return Score(value=hits / len(words) if words else 0.0)

    return score

@task
def colors():
    return Task(
        dataset=[Sample(input="Name three primary colors.", target="red green blue")],
        solver=generate(),
        scorer=word_overlap(),
    )

评测日志为 word_overlap 评分器报告 mean、stderr 和 pass_rate。因为 pass_rate 通过 @scorer(metrics=...) 挂在评分器上,它会自动应用。也可以像概述里那样,按任务覆盖评分器的指标。

合并多个轮次

若任务跑了不止一个轮次,每个样本会生成多个分数。指标通常作用在归约后的分数视图上:每个样本各轮的分数被合成一个分数。归约器(reducer)就是做这个合成的函数。

默认的归约视图用 mean 建立。可以传入 Epochs 来指定其他策略。Epochs 同时包含次数,以及一个或多个用来合并样本分数的归约器。例如:

注意:一个整数形式的轮次覆盖只改次数,并保留任务定义的归约器。这适用于 eval(..., epochs=n)、eval_set(..., epochs=n) 和 --epochs n。要替换任务的归约器,传入 Epochs(n, reducer),或把 --epochs n 与 --epochs-reducer reducer 合用。把 --epochs n 与 --no-epochs-reducer 合用,则关闭轮次归约。

带有固定自定义归约器的任务,可以直接在 Task 里定义次数;用户覆盖次数时,归约器仍然有效。当改变次数也应该改变归约器配置时,任务参数 epochs 才有用。

@task
def gpqa():
    return Task(
        dataset=read_gpqa_dataset("gpqa_main.csv"),
        solver=[
            system_message(SYSTEM_MESSAGE),
            multiple_choice(),
        ],
        scorer=choice(),
        epochs=Epochs(5, "mode"),
    )

也可以指定不止一个归约器,指标会用每一个归约器各算一遍。例如:

@task
def gpqa():
    return Task(
        ...
        epochs=Epochs(5, ["at_least_2", "at_least_5"]),
    )

有些指标需要尚未归约的轮次分数。这些指标在原始的样本–轮次分数上只跑一次,即使配置了显式归约器:

@scorer(metrics=[accuracy(), frequency()])
def my_scorer() -> Scorer:
    ...

在 epochs=Epochs(5, "mode") 时,accuracy() 收到每个样本一个按众数归约的分数,而 frequency() 收到全部已评分的样本–轮次值。有多个归约器时,需要归约的指标对每个归约器跑一次,不归约的指标跑一次。

若用 Epochs(n, []) 或 --no-epochs-reducer 关闭归约器,旧的 scores="auto" 指标会收到未归约的样本–轮次分数,以保持既有行为。明确声明 scores="reduced" 的指标,在存在多个轮次时要求有一个归约器。

内置归约器

Inspect 包含若干内置归约器,摘要如下。

归约器说明
mean归约成全部分数的平均。
median归约成全部分数的中位数。
mode归约成最常见的分数。
majority归约成被超过一半的待归约分数所持有的那个分数,否则为未评分。
max归约成全部分数的最大值。
pass_at_{k}给定 k 个轮次,至少有 1 个样本正确的概率(https://arxiv.org/pdf/2107.03374)。
pass_k_{k}全部 k 次轮次尝试都成功的概率(https://arxiv.org/pdf/2406.12045)。
at_least_{k}至少 k 个样本正确则为 1,否则为 0。
collect把全部分数收成一个列表,保留每个值,而不是汇总。

注意:内置归约器会为分数计算一个归约值,并且只有当 answer 和 explanation 在所有轮次上相等时才填写这两个字段。metadata 字段始终归约成第一个轮次的 metadata 值。若自定义指标函数对字段归约需要不同行为,还应实现自己的自定义归约器,并以某种方式合并或保留字段。

自定义归约器

也可以用带 @score_reducer 装饰器的函数添加自己的归约器。下面是 mean 归约器代码的一个略作简化的版本:

import statistics

from inspect_ai.scorer import (
    Score, ScoreReducer, score_reducer, value_to_float
)

@score_reducer(name="mean")
def mean_score() -> ScoreReducer:
    to_float = value_to_float()

    def reduce(scores: list[Score]) -> Score:
        """Compute a mean value of all scores."""
        values = [to_float(score.value) for score in scores]
        mean_value = statistics.mean(values)

        return Score(value=mean_value)

    return reduce

指标与归约器

指标可以用 scores 声明自己需要未归约的分数才能正确计算。例如:

@metric(scores="unreduced")
def frequency_by_label() -> Metric:
    ...

默认 scores="auto",因此除非显式关闭轮次归约器,指标收到的是归约后的分数。需要每个样本一个分数的指标用 scores="reduced";需要每个样本轮次一个分数的指标用 scores="unreduced"。

指标的分数视图使用轮次时的输入
scores="auto"归约后的分数,除非轮次归约器被显式关闭。
scores="reduced"归约后的分数;存在多个轮次分数时要求有一个轮次归约器。
scores="unreduced"原始的样本–轮次分数,每个已评分轮次一次观察。

例如,frequency() 声明 scores="unreduced",因为它报告的是全部已评分观察上类别结果的分布。

出处:UK AI Security Institute,Scoring Metrics,https://inspect.aisi.org.uk/metrics.html,MIT License,Copyright (c) 2024 UK AI Security Institute。

Found it useful? Pass it on

WeChat

Scan with WeChat to open it on your phone and forward it.

Subscribe via RSS

Submit a correction