CodexQA

行业与实践工具与框架

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

CodexQA 团队阅读约 12 分钟

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

本文目录

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。

觉得有用,转给同事

微信扫码

用微信扫一扫,在手机上打开后即可转发。

用 RSS 订阅

提交勘误