VLM 评测 Benchmark 目录:测什么能力 · 怎么测 · 样题 · 模型如何被打分

这是一份常见 VLM 评测 benchmark 的速查目录。VLM(Vision-Language Model,能看图又能说话的模型)发布时榜单上密密麻麻几十个名字——MMMU、DocVQA、POPE、RefCOCO、MVTec……它们各测一种能力、各用一套判分规则。本文把它们按三层整理成卡片,每个 benchmark 回答五件事:

  1. 测什么能力 —— 它考 VLM 的哪一面?
  2. 测试方法 —— 题型是多选、判断、开放问答,还是输出框/点/掩码?
  3. 测试示例 —— 一道样题长什么样(题型示意)?
  4. 模型如何被测试 —— 图和问题怎么喂进去、模型生成什么、答案怎么被提取、怎么判分?
  5. 指标 —— 用什么数衡量,厂商头版常挂哪个?

三层地图:

VLM 评测三层地图
图 0.1 三层 benchmark 地图:通用能力 / grounding 定位 / 细节瑕疵,各有不同的指标尺子。

§1 先讲清所有 benchmark 共享的「模型被测试的通用流水线」;§2–§6 是分层目录;§7 给一张速查对照表 + 读榜 checklist。真实评分代码 verbatim 引自最常用的评测框架 open-compass/VLMEvalKit 与工业异常库 openvinotoolkit/anomalib;所有 arxiv 编号与数字均经核验。

1. 一个 VLM 是怎么被测试的:通用评测流水线

在钻进各个 benchmark 之前,先看清一件所有 benchmark 共享的事——一个 VLM 到底是怎么被「考」的。不管题型是多选还是画框,评测都走同一条五步流水线。

1.1 五步流水线

一个 benchmark 本质是一组带标准答案的样本(图 + 问题 + 答案)。评测就是对每条样本依次做:

① 取样本 (image, question, answer)
   → ② 拼 prompt(把图和问题/选项组织成模型输入)
   → ③ 模型前向生成(VLM 吃图+文,吐一段自由文本)
   → ④ 答案提取(从自由文本里抠出最终答案:选项字母 / 数值 / 坐标 / 掩码)
   → ⑤ 判分(提取结果与标准答案比对,按该 benchmark 的指标算分)
评测流水线与答案提取
图 1.1 评测流水线:模型自由文本 → 答案提取器 → 抠出答案 → 与标准答案比对。第④步「答案提取」是被低估的隐藏误差源。

不同 benchmark 的区别,几乎只落在第②步(prompt 怎么拼)、第④步(提取什么:字母?坐标?像素图?)、第⑤步(用什么指标)。第③步「模型前向」对所有 benchmark 都一样:VLM 接收图像 token + 文本 token,自回归地吐出一段自由文本。这也是为什么第④步答案提取如此关键——模型不会乖乖只回一个「B」,它会回一整段话,你得先把答案从话里抠出来。

1.2 一个评测循环的最小骨架

把这条流水线写成可运行的代码,核心不到 20 行。下面以多选题为例(其它题型只是换第④步的提取器和第⑤步的指标):

import re
import numpy as np

# 一个 benchmark = 一组结构化样本;这里每条是 {image, question, options, answer}
def build_prompt(item):                                  # ② 拼 prompt
    opts = "\n".join(f"({k}) {v}" for k, v in item["options"].items())
    return f"{item['question']}\n{opts}\n请只回答选项字母。"

def extract_choice(text, choices="ABCDE"):               # ④ 答案提取
    text = re.sub(r"(答案是|the answer is|answer:)", "", text, flags=re.I).strip()
    m = re.search(rf"[{choices}]", text)                 # 抠出第一个选项字母
    return m.group(0) if m else ""                       # 抠不到 → 空串(判错)

def evaluate(model, dataset):                            # 整条流水线
    hits = []
    for item in dataset:                                 # ① 遍历样本
        prompt = build_prompt(item)
        raw = model.generate(item["image"], prompt)      # ③ VLM 前向:吃图+文,吐自由文本
        pred = extract_choice(raw)                       # ④ 提取
        hits.append(int(pred == item["answer"]))         # ⑤ 判分:命中记 1
    return float(np.mean(hits))                          # accuracy = 命中均值

model.generate(image, prompt) 就是 VLM 的前向接口(把一张图和一段文字喂进去,拿回一段文字)。整个评测就是这个循环套在不同 benchmark 的数据 + 提取器 + 指标上。换成 grounding,第④步 extract_choice 变成「从文本里解析出框坐标」、第⑤步变成「算 IoU」;换成画质分割,第④步变成「输出像素掩码」、第⑤步变成「算 mIoU」。骨架不变。

1.3 被低估的第④步:答案提取与 CircularEval

第④步是分数失真的主要来源。真实评测框架在朴素正则之上叠了很多补丁。下面是 VLMEvalKit 的多选答案提取器:

sources/repos/open-compass-VLMEvalKit/vlmeval/dataset/utils/multiple_choice.py:L585-L612 — 从模型自由文本里正则抠出 ABCDE 选项的答案提取器

def extract_characters_regex(s, choices=['(A)', '(B)', '(C)', '(D)', '(E)']):
    if type(s) is dict:
        s = ''
    s = s.strip()
    answer_prefixes = [
        'The best answer is',
        'The correct answer is',
        'The answer is',
        'The answer',
        'The best option is'
        'The correct option is',
        'Best answer:'
        'Best option:',
    ]
    for answer_prefix in answer_prefixes:
        s = s.replace(answer_prefix, '')

    if len(s.split()) > 10 and not re.search('[ABCDE]', s):
        return ''
    matches = re.search(r'[ABCDE]', s)
    if matches is None:
        for choice in choices:
            if s.lower() in choice.lower():
                return choice[1]
        return ''
    return matches[0]

它先删掉「The answer is …」之类套话前缀(免得抓到前缀里的字母),再判断:回答超过 10 个词却找不到 ABCDE 就放弃(返回空串);否则抠第一个选项字母;还兜底匹配「回答正好是某个选项的原文」。规则全失效时上层才调 LLM 当裁判。换一套提取规则,同一个模型的分数就会浮动——这是不同来源分数对不上的常见原因。

提取之后的判分(第⑤步)最基础的形式就是「命中求均值」:

sources/repos/open-compass-VLMEvalKit/vlmeval/dataset/utils/multiple_choice.py:L77-L100 — report_acc:按 split / 能力维度求 hit 均值得到 accuracy

def report_acc(df):
    # assert group in [None, 'category', 'l2-category']
    res = defaultdict(list)

    if 'split' in df:
        splits = list(set(df['split']))
        res['split'] = splits
    else:
        df['split'] = ['none'] * len(df)
        res['split'] = ['none']

    for group in [None, 'l2-category', 'category']:
        if group is None:
            res['Overall'] = [np.mean(df[df['split'] == sp]['hit']) for sp in res['split']]
        elif group not in df:
            continue
        else:
            abilities = list(set(df[group]))
            abilities.sort()
            for ab in abilities:
                ab_name = MMB_abbrs[ab] if ab in MMB_abbrs else ab
                sub_df = df[df[group] == ab]
                res[ab_name] = [np.mean(sub_df[sub_df['split'] == sp]['hit']) for sp in res['split']]
    return pd.DataFrame(res)

hit 列是每题 0/1 的命中,report_acc 对它求 np.mean 就是 accuracy,并顺手按能力维度(category/l2-category)各算一次——这就是为什么 MMBench 这类 benchmark 能给出分维度的细分准确率。

最后一个共享机制:CircularEval(MMBench 提出,§2 会再用到)。四选一靠运气能蒙中 25%,模型还可能有「无脑选某个位置」的偏置。CircularEval 把同一题的选项循环移位问 N 遍(正确答案轮流落在每个位置),N 遍全对才算这题对。它把蒙对概率从 25% 压到约 \(0.25^4\),并让位置偏置无所遁形。后面凡是标「circular」的准确率,都比普通(vanilla)准确率严格。

记住这条流水线,下面每一层的卡片只需要看它在第②④⑤步上有什么不同。

2. 通用能力层:MMMU / MMBench / MME / SEED-Bench

通用能力层的 benchmark 不绑定具体下游任务,目标是用一组覆盖广泛的问题去估计模型「看图 + 推理 + 答题」的综合水平。下面把四个最常被引用的 benchmark 逐个做成卡片:测什么、怎么出题、长什么样、模型怎么被喂入并打分、看哪个指标。

MMMU — 大学级跨学科多模态推理

MMBench — 中英双语 + 防蒙循环评测

MME — yes/no 双问 × 14 子任务

SEED-Bench — 大规模多选 12 维度

▸ 这一层非常规打分:MME 怎么算分

四个 benchmark 里,只有 MME 不是直接报准确率,而是用「acc + acc_plus、满分 2800」这套自定义量纲。它具体怎么算,看 VLMEvalKit 的实现最清楚:

sources/repos/open-compass-VLMEvalKit/vlmeval/dataset/utils/yorn.py:L50-L92 — MME 计分:每图两问都对才算 acc+,最终 score = acc + acc_plus,再汇总 perception/reasoning

def MME_rating(data_file):
    data = load(data_file)
    stats = defaultdict(dict)
    lt = len(data)
    for i in range(lt):
        item = data.iloc[i]
        category = item['category']
        image_path = item['image_path']
        score = item['score']
        if image_path not in stats[category]:
            stats[category][image_path] = []
        stats[category][image_path].append(score)

    def acc(key, mode='normal'):
        res = stats[key]
        values = []
        for val in res.values():
            if mode == 'normal':
                values.extend(val)
            elif mode == 'plus':
                values.append(val[0] * val[1])
        return np.mean(values) * 100

    scores = {}
    for k in stats:
        scores[k] = acc(k) + acc(k, 'plus')

    super_cates = dict(
        perception=[
            'OCR', 'artwork', 'celebrity', 'color', 'count', 'existence',
            'landmark', 'position', 'posters', 'scene'
        ],
        reasoning=['code_reasoning', 'commonsense_reasoning', 'numerical_calculation', 'text_translation']
    )

    ret = {}
    for sc, cate_list in super_cates.items():
        base = 0
        for c in cate_list:
            base += scores[c]
        ret[sc] = base
    ret.update(scores)
    ret = d2df(ret)

对照上面 MME 卡片说的规则,这段代码逐条落地:

可见 MME 那个看上去奇怪的「2800 满分」并非凭空设定,而是 14 类 × 每类 200 的累加结果;读厂商榜单上的 MME 分时,记住它是这套量纲、不能当百分比直接和 MMMU/SEED 的 accuracy 横向比。

3. 会读图会算层:DocVQA / ChartQA / TextVQA / AI2D / MathVista

这一层考的不是"图里有什么物体",而是"图里写了什么、画了什么数据,你能不能据此读出文字、对齐版面、做算术与逻辑推理"。它是商用 VLM 对外宣传时最爱挂在头版的能力区——尤其是 OCR / 文档理解——因为它直接对应发票识别、报表解读、合同抽取这类付费场景。本层的评测共同难点在于:答案是开放式自由文本,没有 ABCD 选项,因此判分必须做容差匹配(数值容差或编辑距离),否则一个多余空格、一次 OCR 拼写抖动就会把对的答案判错。

relaxed accuracy 与 ANLS 对照
图 3.1 两种容差指标:relaxed accuracy(数值 5% 容差)与 ANLS(编辑距离容忍 OCR 拼写差)。

DocVQA — 文档图像问答(发票 / 表单 / 合同)

ChartQA — 图表问答(柱状 / 折线 / 饼图)

TextVQA — 场景文字问答(招牌 / 包装)

AI2D — 科学示意图多选理解(生命周期 / 食物链)

MathVista — 视觉数学推理(既看懂图又算数)

▸ 这一层怎么打分:relaxed accuracy 与 ANLS

前面五个 benchmark 里,真正决定一个答案算不算对的,是两段几十行的容差逻辑。先看 ChartQA 的 relaxed accuracy(VLMEvalKit 实现):

sources/repos/open-compass-VLMEvalKit/vlmeval/dataset/utils/vqa_eval.py:L175-L215 — ChartQA 的 relaxed accuracy:数值答案允许 5% 相对误差,非数值仍需精确匹配

def relaxed_correctness(target: str,
                        prediction: str,
                        max_relative_change: float = 0.05) -> bool:
    """Calculates relaxed correctness.

    The correctness tolerates certain error ratio defined by max_relative_change.
    See https://arxiv.org/pdf/2203.10244.pdf, end of section 5.1:
    “Following Methani et al. (2020), we use a relaxed accuracy measure for the
    numeric answers to allow a minor inaccuracy that may result from the automatic
    data extraction process. We consider an answer to be correct if it is within
    5% of the gold answer. For non-numeric answers, we still need an exact match
    to consider an answer to be correct.”

    Args:
      target: Target string.
      prediction: Predicted string.
      max_relative_change: Maximum relative change.

    Returns:
      Whether the prediction was correct given the specified tolerance.
    """

    def _to_float(text: str) -> Optional[float]:
        try:
            if text.endswith('%'):
                # Convert percentages to floats.
                return float(text.rstrip('%')) / 100.0
            else:
                return float(text)
        except ValueError:
            return None
    prediction = str(prediction)
    target = str(target)
    prediction_float = _to_float(prediction)
    target_float = _to_float(target)
    if prediction_float is not None and target_float:
        relative_change = abs(prediction_float - target_float) / abs(target_float)
        return relative_change <= max_relative_change
    else:
        return prediction.lower() == target.lower()

对照:这段把"5% 容差"翻译成了一行 relative_change <= max_relative_change,其中 max_relative_change 默认 0.05,而 relative_change = abs(prediction_float - target_float) / abs(target_float) 正是相对误差。注意两个分支:只有当预测和参考都能解析成浮点数(prediction_float is not None and target_float)时才走 5% 容差;否则退回 prediction.lower() == target.lower() 的精确匹配——这就是 ChartQA"数值放宽、非数值仍要精确"规则的全部实现。

再看 DocVQA 的 ANLS,核心是编辑距离 + 归一化:

sources/repos/open-compass-VLMEvalKit/vlmeval/dataset/utils/vqa_eval.py:L217-L240 — DocVQA 的 ANLS:用归一化编辑距离容忍 OCR 小拼写差异

def levenshtein_distance(s1, s2):
    if len(s1) > len(s2):
        s1, s2 = s2, s1

    distances = range(len(s1) + 1)
    for i2, c2 in enumerate(s2):
        distances_ = [i2 + 1]
        for i1, c1 in enumerate(s1):
            if c1 == c2:
                distances_.append(distances[i1])
            else:
                distances_.append(1 + min((distances[i1], distances[i1 + 1], distances_[-1])))
        distances = distances_
    return distances[-1]


def anls_compute(groundtruth, prediction):
    gt_answer = ' '.join(groundtruth.strip().lower().split())
    det_answer = ' '.join(prediction.strip().lower().split())
    dist = levenshtein_distance(gt_answer, det_answer)
    length = max(len(groundtruth.upper()), len(prediction.upper()))
    values = 0.0 if length == 0 else float(dist) / float(length)
    return values

对照:这里 levenshtein_distance 是标准编辑距离;anls_compute 先把两串规范化(小写、压缩空白),算出编辑距离 dist,再除以较长串长度得到 values = dist / length关键别读反:anls_compute 返回的是"归一化编辑距离比例"(越小越像),它不是 ANLS 本身。真正的 ANLS = 1 − values(越大越像),且按惯例还要过一个 0.5 阈值——即 1 − values ≥ 0.5(等价于 values ≤ 0.5)才记为命中,低于阈值的相似度直接归零。这样设计正好容忍 OCR 级别的小拼写差(几处字符出入仍能保持高相似度),却拦住语义不同的答案。两段代码合起来,就是本层"开放式回答如何被公平判分"的工程答案:数值看相对误差,文本看编辑距离,都不做朴素的字符串相等。

4. 幻觉与诚实层:POPE / HallusionBench / MMStar

前三层(基础感知、综合理解、推理)给出的都是「答对率」分数。但一个高分,可能来自模型真的看懂了图,也可能来自:模型根本没看图、靠语言先验蒙对(视觉冗余),或模型把训练时背过的测试题答了出来(数据泄漏),又或者模型张口就来、把图里没有的物体说成有(物体幻觉)。这一层的三个 benchmark 专门做一件事:把「虚高的分数」从「真实的能力」里拆出来。它们不测新能力,而是审计前几层分数的诚实度。

POPE — 物体幻觉的二分类探针

HallusionBench — 语言幻觉 × 视觉错觉的配对诊断

MMStar — 视觉冗余与数据泄漏的体检

一句话:POPE 审「有没有幻觉」,HallusionBench 审「是不是真看图、还是靠先验」,MMStar 审「分数本身是不是题目和数据集的虚高」。三者合起来,才能判断前几层的高分是真本事还是水分。

POPE 怎么打分:把 Yes/No 当二分类算 F1

下面是 VLMEvalKit 中 POPE 的计分实现。注意它没有简单地数「答对几道」,而是把每条 Yes/No 回答映射成二分类标签,再算 precision / recall / F1。

sources/repos/open-compass-VLMEvalKit/vlmeval/dataset/utils/yorn.py:L148-L186 — POPE 计分:把 Yes/No 当二分类,算 precision / recall / F1(北极星是 F1)

def POPE_rating(data_file):
    def cal_f1_score(y_true, y_pred):
        tp = sum((y_true == 1) & (y_pred == 1))
        fp = sum((y_true == 0) & (y_pred == 1))
        fn = sum((y_true == 1) & (y_pred == 0))

        precision = tp / (tp + fp) if (tp + fp) != 0 else 0
        recall = tp / (tp + fn) if (tp + fn) != 0 else 0
        f1_score = 2 * (precision * recall) / (precision + recall) if (precision + recall) != 0 else 0
        return f1_score, precision, recall

    data = load(data_file)
    data = data.assign(category=data['category'].str.split(',')).explode('category')
    data['index'] = range(len(data))
    res = dict(split=[], Overall=[], acc=[], precision=[], recall=[])
    y_true = np.array([1 if i == 'Yes' else 0 for i in data['answer']])
    y_pred = np.array([1 if i == 'Yes' else 0 for i in data['extracted']])
    f1_score, precision, recall = cal_f1_score(y_true, y_pred)
    res['split'].append('Overall')
    res['Overall'].append(f1_score * 100)
    res['acc'].append(np.mean(data['score']) * 100)
    res['precision'].append(precision * 100)
    res['recall'].append(recall * 100)

    if 'category' in data:
        cates = list(set(data['category']))
        cates = [c for c in cates if not pd.isna(c)]
        for c in cates:
            sub = data[data['category'] == c]
            y_true = np.array([1 if i == 'Yes' else 0 for i in sub['answer']])
            y_pred = np.array([1 if i == 'Yes' else 0 for i in sub['extracted']])
            f1_score, precision, recall = cal_f1_score(y_true, y_pred)
            res['split'].append(c)
            res['Overall'].append(f1_score * 100)
            res['acc'].append(np.mean(sub['score']) * 100)
            res['precision'].append(precision * 100)
            res['recall'].append(recall * 100)

    ret = pd.DataFrame(res)

对照:

5. Grounding 定位层:RefCOCO / LVIS / ScreenSpot / PixMo-Points

前几层考的是「把文字答对」:看一张图、读一段题,输出一串文本,再用字符串或选项去对答案。这一层换了一个动作——答案不再是文字,而是位置。模型要么框出一个矩形(bounding box),要么点出一个坐标(point)。判分也随之改变:不再比较字符串,而是比较几何重叠。下面逐个 benchmark 做卡片,看清「输入指代语/指令 → 输出框/点 → 怎么判对」这条链路。

更细的 grounding benchmark 盘点见教程《VLM Grounding 全景》../vlm-grounding-2025/;PixMo 的数据构造细节见 Molmo 精读 ../../papers/molmo-2024/

RefCOCO / RefCOCO+ / RefCOCOg — 指代表达定位(referring expression grounding)

LVIS — 长尾词表检测(Large Vocabulary Instance Segmentation)

ScreenSpot / ScreenSpot-Pro — GUI/屏幕元素定位

ScreenSpot GUI 定位
图 5.1 ScreenSpot 类 GUI 定位:给指令,模型点中界面元素。

PixMo-Points — pointing 指向评测

PixMo pointing
图 5.2 Molmo/PixMo-Points 的 pointing:模型用点坐标指出目标。图源:Molmo (arxiv 2409.17146)。

▸ 怎么打分:IoU 与命中阈值

上面所有「框」类判分都绕不开一个量——IoU(交并比)= 两框交集面积 / 并集面积。下面这段就是它的实现:先求两框相交矩形的左上/右下角,算出交集面积;再用两框面积之和减去交集得到并集;最后相除。

sources/repos/open-compass-VLMEvalKit/vlmeval/dataset/groundingme.py:L39-L56 — IoU:两个框的交集面积 / 并集面积

def compute_iou(box1, box2):
    """Compute Intersection over Union (IoU) of two bounding boxes."""
    if box1 == [0, 0, 0, 0]:
        return 1 if box2 == [0, 0, 0, 0] else 0

    x_left = max(box1[0], box2[0])
    y_top = max(box1[1], box2[1])
    x_right = min(box1[2], box2[2])
    y_bottom = min(box1[3], box2[3])

    intersection = max(0, x_right - x_left) * max(0, y_bottom - y_top)
    area1 = (box1[2] - box1[0]) * (box1[3] - box1[1])
    area2 = (box2[2] - box2[0]) * (box2[3] - box2[1])
    union = area1 + area2 - intersection

    return intersection / union if union > 0 else 0

有了 IoU,Acc@x 就是「设一个阈值,过线算 1 分,不过算 0 分,再对全集求均值」。下面这段对每个样本取多个候选框里 IoU 最高的那个(best_iou),分别与 0.5 / 0.75 / 0.9 比较得到 acc_50/75/90,最后用 .mean() 把这些 0/1 命中聚合成命中率。

sources/repos/open-compass-VLMEvalKit/vlmeval/dataset/groundingme.py:L146-L175 — Grounding 命中:best_iou ≥ 0.5/0.75/0.9 得 ACC@x,再对全集求均值

            ious = [compute_iou(gt_bbox, pred) for pred in pred_candidates]
            best_idx = ious.index(max(ious))
            best_iou = ious[best_idx]

            # Compute metrics
            acc_50 = float(best_iou >= 0.5)
            acc_75 = float(best_iou >= 0.75)
            acc_90 = float(best_iou >= 0.9)

            results.append({
                'index': item['index'],
                'subtask_l1': subtask_l1,
                'subtask_l2': subtask_l2,
                'iou': best_iou,
                'acc_50': acc_50,
                'acc_75': acc_75,
                'acc_90': acc_90,
            })

        # Save detailed results
        results_df = pd.DataFrame(results)
        dump(results_df, result_file)

        # Aggregate overall metrics
        metrics = {
            'IoU': results_df['iou'].mean(),
            'ACC@0.5': results_df['acc_50'].mean(),
            'ACC@0.75': results_df['acc_75'].mean(),
            'ACC@0.9': results_df['acc_90'].mean(),
        }

对照着看两段:compute_iou 给出「交 / 并」这一个连续的几何重叠分数;acc_50/75/90 把这个连续分数按阈值二值化成命中,.mean() 再把命中聚合成命中均值。阈值越高越严——Acc@0.9 要求框几乎完全压在标准框上,远比 Acc@0.5 难刷。RefCOCO 头版报的 Acc@0.5,本质就是这条流水线在阈值 0.5 处的那个数;ScreenSpot 的点命中率则是把「点是否落框内」代替了「IoU 是否过阈值」,聚合方式一模一样。

6. 细节 / 瑕疵层:MVTec-AD / VisA / QGround

到这一层,benchmark 的形态彻底变了。前面几层的题目都能归约成「给一道题、读模型的一段答案、判对错」,指标是 accuracy / 命中率这一套。但工业异常与画质瑕疵这类任务,关心的不是「答对了吗」,而是「能不能把异常从正常里区分排序出来」「能不能把缺陷的像素精确圈出来」。于是指标体系换成了 AUROC / PRO / mIoU——这些是排序质量与区域重叠度的度量,与前面的 accuracy 体系不可直接通约:一个模型可以在 VQA 上拿 80% 准确率,但在 MVTec 上的 image-AUROC 跟它的 VQA 分数没有任何换算关系。

更要紧的一点:这一层榜单上刷分的主力大多还不是 VLM,而是专用异常检测模型(PatchCore、PaDiM、EfficientAD 等)。它们不走对话接口,而是吃一张图、吐一个异常分或一张异常热力图。把通用 VLM 拉进来比,往往要专门设计 prompt 或额外训练分割头,接口本身就不对齐。读这一层榜单时务必看清:跑在前列的是不是「VLM」。

工业异常指标的完整全貌可参见教程《VLM 工业质检》../vlm-industrial-2024/;Q-Ground 的方法细节见 Q-Ground 精读

MVTec-AD — 工业异常检测的黄金基准

VisA — 更大更难的像素级异常基准

Q-Ground / QGround — 画质失真分割

怎么打分:image-AUROC

上面反复出现的 AUROC,定义是 ROC 曲线(纵轴 TPR、横轴 FPR)下的面积:它衡量「把异常排在正常前面」的排序能力,与阈值无关——0.5 等于随机猜,1.0 等于完美排序。pixel-AUROC 只是把统计单位从「每张图一个分」换成「每个像素一个分」。下面是 anomalib(Intel/OpenVINO 官方异常检测库)里 image-AUROC 的核心实现:

sources/repos/openvinotoolkit-anomalib/src/anomalib/metrics/auroc.py:L82-L92 — 异常检测的 image-AUROC:先算 ROC 曲线 (fpr,tpr),再取曲线下面积

    def compute(self) -> torch.Tensor:
        """First compute ROC curve, then compute area under the curve.

        Returns:
            torch.Tensor: Value of the AUROC metric
        """
        tpr: torch.Tensor
        fpr: torch.Tensor

        fpr, tpr = self._compute()
        return auc(fpr, tpr, reorder=True)

对照来看:_compute() 先吐出 ROC 曲线上的一串 (fpr, tpr) 点,auc(fpr, tpr) 再对这条曲线求积分得到曲线下面积,就是 AUROC。这个 AUROC 类本质是 torchmetrics BinaryROC 的封装——拿到曲线点后求面积。要从 image-AUROC 切到 pixel-AUROC,代码逻辑不变,只是把「每张图一个预测分 + 一个标签」换成「每个像素一个预测分 + 一个标签」喂进来即可。至于 PRO(Per-Region Overlap),它不走这条「全部样本一起排序」的路:它对每个真实缺陷的连通区域单独算覆盖率再求平均,从而避免大缺陷在整体面积上主导分数、保护那些面积虽小却关键的缺陷。

7. 速查对照表 · 怎么读 leaderboard · 什么时候得自建

7.1 一张速查对照表

把前六层收成一张表,看到任意一个 benchmark 名字都能立刻定位它在测什么、用什么判分:

能力层 代表 benchmark 题型 / 输出 指标 头版常用
通用综合 MMMU / MMBench / MME / SEED-Bench 多选 / yes-no accuracy(MME 用 2800 制) MMMU
会读图会算 DocVQA / ChartQA / TextVQA / AI2D / MathVista 开放问答 / 多选 ANLS / relaxed acc / accuracy DocVQA、MathVista
幻觉 / 诚实 POPE / HallusionBench / MMStar yes-no / 配对 / 给图vs不给图 F1 / 配对准确率 / 视觉必要性 POPE-F1
Grounding 定位 RefCOCO / LVIS / ScreenSpot / PixMo-Points 输出框 / 点 IoU·Acc@0.5 / AP·mAP / 点命中率 RefCOCO Acc@0.5、ScreenSpot 命中率
细节 / 瑕疵 MVTec-AD / VisA / QGround 异常分排序 / 像素分割 image-/pixel-AUROC / PRO / mIoU pixel-AUROC(多为非 VLM 专用模型)
各家头版指标对照矩阵
图 7.1 三层 benchmark × 指标 × 头版指标对照矩阵,以及读榜要警惕的三大陷阱。

7.2 怎么读一张 leaderboard(三大陷阱)

看到一张排名密密麻麻的榜单,别先看谁第一,先按 §5 的教训过三道筛:

  1. 数据污染 / 泄漏:测试题是否可能进过训练语料?进过的话分数就从「考试」退化成「默写背过的答案」。越新、越冷门的 benchmark 越可信。
  2. 视觉冗余:这列有没有「盖住图也能答对」的对照(如 MMStar)?没有的话,纯靠语言先验可蒙的高分要打折看。形式上:视觉必要性 = acc(有图) − acc(无图)。
  3. 格式敏感 / 答案提取差异:这分是哪套 harness、哪个答案提取器跑的?(§1 讲过)换一套提取规则同一个模型分数就会浮动——不同 harness 的分数不可直接横比

还有一条贯穿全文:量纲不统一不能相加。MME 是 2800 制、MMMU 是百分制、AUROC 是 0–1——把它们塞进一个「总分」前必须先各自归一化。

7.3 「头版指标」在代码里是怎么被挑出来的

一个 benchmark 跑完往往同时产出十几个数(总分、各子类、precision/recall…),但榜单上只显示一个。VLMEvalKit 用一个函数从中钦定那个对外汇报的主指标:

sources/repos/open-compass-VLMEvalKit/vlmeval/dataset/image_base.py:L393-L407 — report_primary_metric:发布时只抽出主指标对外汇报

    def report_primary_metric(cls, metrics: dict[str, Any] | None) -> dict[str, float | int]:
        if not isinstance(metrics, dict) or not metrics:
            return {}

        matched_keys = []
        fallback = _choose_primary_metric_key(metrics)
        if fallback is not None:
            matched_keys = [fallback]

        primary_metrics = {}
        for key in matched_keys:
            value = metrics.get(key)
            if is_number(value):
                primary_metrics[str(key)] = to_number(value)
        return primary_metrics

_choose_primary_metric_key 按固定优先级(Overall 优于 Acc 优于 Score)从一堆指标里选出一个键,report_primary_metric 只回传它。换句话说:你在榜上看到的那一列,是框架替你做了「该看哪个」的决定,而被它扔掉的其他指标,往往才是暴露短板的地方——所以 §7.2 才强调要主动去看头版之外的诚实/定位维度。

7.4 什么时候你得自建 benchmark

这份目录覆盖的都是已经成熟、被反复刷过的能力。如果你做的是更前沿的方向——比如双向 grounding、多粒度定位、few-shot pointing——会发现几乎找不到对口的公开评测。这类能力目前没有现成 benchmark,需要自建,而自建时同样套用 §1 的四要素:

给小白的读榜 checklist:① 认能力层(别把不同能力当一个总分);② 认指标与量纲(满分 100 还是 2800?先归一化);③ 认协议(哪套 harness);④ 查视觉冗余;⑤ 查污染/泄漏;⑥ 看一整组而非一个数;⑦ 关心的能力榜上没有对口列 = 该自建的信号。

8. 新一代发布会评分卡:推理 · agentic · 新多模态(2025+)

前七层是 2023–2024 年「VLM 视觉能力」的尺子。但翻开 2025 年起的前沿模型发布卡,会发现榜单换了一批面孔——大量不再是看图答题,而是纯文本推理agentic(工具 / 搜索 / 编码 / 终端),以及更硬的多模态。下面这张 Qwen3.5 系列的发布对比卡就是典型:

2025 前沿模型发布评分卡
图 8.1 一张 2025 年前沿模型发布评分卡(此为 Qwen3.5 系列对比 GPT-5 mini / Claude Sonnet 4.5 / gpt-oss-120b 的示例):12 个 benchmark 横跨推理、agentic、多模态,与 §2–§6 的经典 VLM 榜几乎不重叠。图为厂商发布材料。

这批 benchmark 与前六层有三处根本不同,读它们前先记住:

  1. 很多根本不是 VLM,而是 LLM 的推理 / agent 能力(GPQA、HMMT、BFCL、SWE-bench 都不喂图)。它们和 §3 的 MMMU 不在一根能力轴上,不能混进同一张「视觉能力」对照表。
  2. 判分从「对答案」扩展到「跑环境」:agentic benchmark 不再正则抠选项,而是真的把模型生成的补丁打进仓库跑测试(SWE-bench)、把命令丢进容器看结果(Terminal-Bench)。§1 的第④步「答案提取」被「执行并观察」取代。
  3. 强调防污染:像 HMMT Feb 2025 这种「刚比完的竞赛题」之所以上卡,正是因为它发布得够新、几乎不可能进训练语料——这是 §7.2 第 1 条「数据泄漏」陷阱的正面解法。

按发布卡的四个自然分组逐个说清。

8.1 纯文本推理与知识:GPQA Diamond / HMMT Feb 2025 / MMMLU

这组不喂图、纯文本;放进 VLM 发布卡是为了证明「底座 LLM 的推理没退化」。

GPQA Diamond — 研究生级「Google 也搜不到」的科学推理

HMMT Feb 2025 — 用「刚比完的奥数」防污染

MMMLU — 多语种知识(别和 MMMU 搞混)

8.2 指令遵循:IFBench

IFBench — 对「没见过的约束」还守不守规矩

8.3 Agentic:BFCL V4 / BrowseComp / SWE-bench Verified / Terminal-Bench 2

这组的共同点:模型不再只「说」,而要「做」——调函数、查网、改代码、敲命令;判分靠执行结果。

BFCL V4 — 函数调用 / 工具使用

BrowseComp — 难找信息的「浏览竞赛」

SWE-bench Verified — 真·改 GitHub issue

Terminal-Bench 2 — 终端里的 agent

8.4 新多模态:MMMU-Pro / ERQA / OmniDocBench v1.5 / Video-MME

这组才是真喂图 / 视频的,但比 §2–§4 的经典 VLM 榜更硬、更抗「不看图也能蒙」。

MMMU-Pro — 把 MMMU 的水分拧干

ERQA — 具身推理

OmniDocBench v1.5 — 文档解析

Video-MME(with subtitle)— 视频理解

agentic 怎么打分:SWE-bench 跑隐藏测试看 resolve rate

这一节的 benchmark 里,最能体现「判分从『对答案』变成『跑环境』」的是 SWE-bench。它不正则抠任何东西,而是把补丁打进仓库、跑两组测试,再判这道题算不算「解决」。核心逻辑就这二十几行:

sources/repos/princeton-nlp-SWE-bench/swebench/harness/grading.py:L194-L232 — SWE-bench 解决判定:FAIL_TO_PASS 全过 且 PASS_TO_PASS 全保持,才算 resolved

def compute_fail_to_pass(report: dict[str, dict[str, Any]]) -> float:
    """
    Compute fail-to-pass metric. Accepts single report as argument.
    """
    total = len(report[FAIL_TO_PASS]["success"]) + len(report[FAIL_TO_PASS]["failure"])
    if total == 0:
        return 1
    return len(report[FAIL_TO_PASS]["success"]) / total


def compute_pass_to_pass(report: dict[str, dict[str, Any]]) -> float:
    """
    Compute pass-to-pass metric. Accepts single report as argument.
    """
    total = len(report[PASS_TO_PASS]["success"]) + len(report[PASS_TO_PASS]["failure"])
    if total == 0:
        # TODO: Don't factor in p2p metrics
        return 1
    return len(report[PASS_TO_PASS]["success"]) / total


def get_resolution_status(report: dict[str, dict[str, Any]]) -> str:
    """
    Determine resolved status of an evaluation instance

    Criteria:
        - If fail-to-pass (Resolution) = 1 and pass-to-pass (Maintenance) = 1 -> FULL
        - If (fail-to-pass (Resolution) < 1 and > 0) and pass-to-pass (Maintenance) = 1 -> PARTIAL
        - Otherwise -> NO
    """
    f2p = compute_fail_to_pass(report)
    p2p = compute_pass_to_pass(report)

    if f2p == 1 and p2p == 1:
        return ResolvedStatus.FULL.value
    elif f2p < 1 and f2p > 0 and p2p == 1:
        return ResolvedStatus.PARTIAL.value
    else:
        return ResolvedStatus.NO.value

对照来看:

8.5 这张卡和前七层有什么不同

把新老两代放一起,差异可收成三句话:

  1. 能力轴变多了。§2–§6 几乎都在「看图」这一根轴上分层;发布卡则把纯文本推理、agentic、多模态三根轴并排——所以同一张卡上 GPQA(不看图)和 MMMU-Pro(看图)并列,绝不能加成一个总分(又一次 §7.2 的「量纲 / 能力不可混加」)。
  2. 判分机制升级。从「正则抠答案 + 容差匹配」(§1、§3)走到「执行并观察」:跑测试(SWE-bench)、跑容器(Terminal-Bench)、跑验证器(IFBench)、联网核验(BrowseComp)。第④步不再是提取,而是运行
  3. 防污染成了一等公民。HMMT Feb 2025 用「刚发布」对抗泄漏,MMMU-Pro 用「渲染进图 + 加选项」对抗蒙猜,IFBench 用「全新约束」对抗过拟合——它们本质都在落地 §7.2 的三道筛。

所以读 2025 之后的发布卡,§7.3 的老教训依然成立、且更要紧:先认这一列在哪根能力轴上,再认它判分是「对答案」还是「跑环境」,最后才比数字。一张把 GPQA、SWE-bench、Video-MME 并排的卡,本身就是在说「我这代模型推理、动手、看图三条腿都想要」——而每条腿该用哪把尺子,前八节已经给全了。

参考文献与数据集

通用能力 benchmark

会读图会算 / VQA

幻觉 / 诚实

Grounding 定位

细节 / 瑕疵

新一代发布卡 benchmark(2025+)

评测框架与综述

讨论 / Comments

评论托管在本仓库的 GitHub Discussions, 需 GitHub 账号。