第9章 学生Notebook:看图盘点助手¶
把"看起来像"变成证据分级
本Notebook完成以下任务:
- 用开放提问和约束提问分别描述图片
- 对模型每句结论做证据分级(A/B/C)
- 用扰动(遮挡/裁剪/降质)检查模型边界
- 修改一个变量,观察变化
准备:确认 data/ground_truth.json 存在。
1. 加载数据¶
读取图片标注和标准答案。
import json
import numpy as np
from pathlib import Path
data_path = Path('../data/ground_truth.json')
with open(data_path, 'r', encoding='utf-8') as f:
data = json.load(f)
images = data['images']
questions = data['questions']
print(f'图片数量: {len(images)}')
print(f'提问方式: {len(questions)}种')
print()
for img in images:
n_items = len(img['items'])
print(f' {img["id"]}: {img["description"]} ({n_items}个已知物品)')
print()
print(f'开放提问: {questions["open"]}')
print(f'约束提问: {questions["constrained"]}')
import os
# 预录的模型输出(离线兜底)
RECORDED_OUTPUTS = {
'desk_normal': {
'open': '图片中有一台笔记本电脑、一本蓝色封面的书、一支铅笔和一个白色杯子。',
'constrained': '1. 笔记本电脑,位于桌面中央偏左。2. 蓝色封面书本,位于右上角。3. 铅笔,位于笔记本右侧。4. 白色杯子,位于右下角。'
},
'desk_occluded': {
'open': '图片中有一台笔记本电脑、一些书和文具。看起来还有一个杯子。',
'constrained': '1. 笔记本电脑,位于桌面中央,可以确认。2. 书本的一部分,右上角露出蓝色封面,但不确定完整书名。3. 疑似有铅笔,被笔记本压住,只能看到一部分。'
},
'desk_dark': {
'open': '图片中有一台发光的笔记本电脑,桌上还有一些物品但不太清楚。可能有一本书和一个杯子。',
'constrained': '1. 笔记本电脑,屏幕发光可以确认。2. 其他区域太暗,无法确认具体物品。'
},
'desk_tricky': {
'open': '图片中有一台笔记本电脑、一个咖啡杯和一支铅笔。',
'constrained': '1. 笔记本电脑,可以确认。2. 咖啡杯——需要确认是真实杯子还是图片中的杯子。3. 铅笔,可以确认。'
},
'desk_cluttered': {
'open': '图片中有很多物品,包括笔记本、手机、耳机、钥匙、文具、水杯、书本、眼镜等。',
'constrained': '桌面上有很多小物品密集排列。可以确认有笔记本电脑和一些文具。其他物品太小或太密集,无法逐一确认。'
},
'desk_empty': {
'open': '图片中有一张木色桌面,上面什么都没有。可能远处有一个小物件。',
'constrained': '桌面基本为空。没有可以确认的物品。'
}
}
def get_model_description(image_id, question_type):
"""获取模型描述:尝试本地模型,失败则用预录输出"""
# 尝试本地视觉模型
try:
import urllib.request
img_path = Path(f'../data/images/{image_id}.png')
if img_path.exists():
import base64
img_data = base64.b64encode(img_path.read_bytes()).decode()
url = 'http://localhost:11434/api/generate'
q = questions[question_type]
payload = json.dumps({
'model': 'qwen2.5-vl:7b',
'prompt': q,
'images': [img_data],
'stream': False
}).encode()
req = urllib.request.Request(url, data=payload, headers={'Content-Type': 'application/json'})
with urllib.request.urlopen(req, timeout=30) as resp:
result = json.loads(resp.read())
return result['response'], 'local_vlm'
except Exception:
pass
# 离线兜底
return RECORDED_OUTPUTS.get(image_id, {}).get(question_type, '无可用输出'), 'recorded'
# 测试
desc, source = get_model_description('desk_normal', 'open')
print(f'模型路径: {source}')
print(f'desk_normal 开放提问回答: {desc}')
3. 开放提问 vs 约束提问¶
运行:对每张图片,分别用开放和约束提问获取描述。
print(f'{"图片":<20} {"开放提问":<40} {"约束提问":<40}')
print('-' * 100)
for img in images:
open_desc, _ = get_model_description(img['id'], 'open')
const_desc, _ = get_model_description(img['id'], 'constrained')
print(f'{img["id"]:<20}')
print(f' 开放: {open_desc[:60]}...' if len(open_desc) > 60 else f' 开放: {open_desc}')
print(f' 约束: {const_desc[:60]}...' if len(const_desc) > 60 else f' 约束: {const_desc}')
print()
思考:
- 约束提问是否让模型更谨慎?
- 有没有开放提问中"看到"但约束提问中"不确定"的物品?
- desk_empty 的开放提问中,模型是否编造了物品?
4. 证据分级¶
对 desk_normal 和 desk_tricky 的约束提问回答,逐句做证据分级。
- A(画面直接可见):能在原图中指出该物品
- B(疑似):有局部特征但不能完全确认
- C(无法确认):画面中没有足够证据
def grade_claims(image_id, model_description, ground_truth_items):
"""对模型描述中的每个声明做证据分级"""
gt_names = {item['name'] for item in ground_truth_items}
gt_evidence = {item['name']: item['evidence'] for item in ground_truth_items}
results = []
# 简单关键词匹配(教学演示用)
for item in ground_truth_items:
name = item['name']
# 检查模型是否提到了这个物品
mentioned = any(keyword in model_description for keyword in name.split('(')[0].split())
if mentioned:
results.append({
'item': name,
'model_mentioned': True,
'ground_truth_evidence': item['evidence'],
'student_grade': '' # 学生填写
})
return results
# desk_normal 证据分级
normal_img = images[0]
normal_desc, _ = get_model_description('desk_normal', 'constrained')
grades = grade_claims('desk_normal', normal_desc, normal_img['items'])
print('desk_normal 证据分级:')
print(f'{"物品":<20} {"模型提到":<10} {"标准答案":<10} {"你的分级":<10}')
print('-' * 50)
for g in grades:
print(f'{g["item"]:<20} {"是" if g["model_mentioned"] else "否":<10} {g["ground_truth_evidence"]:<10} {g["student_grade"] or "待填写":<10}')
print()
# desk_tricky 证据分级(注意陷阱)
tricky_img = images[3]
tricky_desc, _ = get_model_description('desk_tricky', 'constrained')
print(f'desk_tricky 约束提问回答: {tricky_desc}')
print(f'陷阱: {tricky_img.get("trap", "无")}')
print('模型是否把打印图片中的咖啡杯当成了真实杯子?')
# 预录的扰动输出
PERTURBATION_OUTPUTS = {
'desk_normal_cropped': '图片中有一台笔记本电脑的局部,看起来还有书的一角。',
'desk_normal_lowres': '图片中有一台笔记本电脑,其他物品不太清楚。可能有一个杯子。',
'desk_normal_occluded': '图片中有一台笔记本电脑,右上角被遮挡。可以看到铅笔的一部分。'
}
print('desk_normal 扰动对照:')
print()
print(f'原始: {RECORDED_OUTPUTS["desk_normal"]["constrained"]}')
print()
for key, output in PERTURBATION_OUTPUTS.items():
label = key.replace('desk_normal_', '')
print(f'{label}: {output}')
print()
# 分析哪些结论在扰动后改变了
print('扰动影响分析:')
print(' 裁剪后: 杯子消失了(被裁掉)')
print(' 低清后: 铅笔变成"可能"(细节丢失)')
print(' 遮挡后: 书和铅笔变成部分可见')
6. B档修改¶
从以下两项中选择一项:
选项A:为证据分级新增一条规则
- 例如:"如果模型提到颜色但图片是黑白的,标为C"
- 先预测这条规则会影响哪些结论
选项B:修改约束提问的措辞
- 让提问更严格或更宽松
- 先预测模型回答会怎样变化
# 我选择:选项___
#
# 修改前预测:
#
# === 选项A:新增证据分级规则 ===
# new_rule = "如果___,则标为___"
# print(f'新增规则: {new_rule}')
# print('受影响的结论:')
# === 选项B:修改约束提问 ===
# modified_question = questions['constrained'] + ' 额外要求:___'
# print(f'修改后的提问: {modified_question}')
7. 验收¶
print('验收清单:')
checks = [
'运行了开放提问和约束提问对照',
'完成了 desk_normal 和 desk_tricky 的证据分级',
'分析了至少一种扰动的影响',
'完成了B档修改',
'能指出模型编造的内容(desk_empty 或 desk_tricky)',
]
for c in checks:
print(f' [ ] {c}')
print()
print('关键概念检查:')
print(' 1. 视觉模型的输入是___,输出是___')
print(' 2. 视觉幻觉的常见原因有___')
print(' 3. 证据分级的三个等级是___')
print(' 4. 规则模型和视觉模型的分工是___')