Lecture 03 学习讲义
正文
工具
按问题查找
视觉与语言语言怎样选择当前需要的视觉信息?

LAB 14 · 图像问答:视觉条件与问题先验

按问题继续

RGB+问题输入、question-only对照、同图换问与同问换图

对应课堂(1 页)

可先阅读已有结果,再决定是否运行。重算时请核对各节记录的数据、参数、环境与日期。

同图换问题,模型读什么会变;同问换图片,答案也应跟着变。先读P54,再看第2节按图切分和第4节逐问题结果。

开头字典例是已知属性oracle,主体才是读取RGB像素与问题ID的CNN,并单独训练question-only。三类问题为数量、左色、右形,答案为有限词表;这不等于开放语言问答。完整控制和证据。

读完应能解释question-only为何约44.44%,并从同图换问/同问换图各拿一个例子支持“模型使用了条件”。只读可先看2026-09-22保存结果,运行则按全部代码顺序;本次整理不改历史成绩。若答案需要一个区域而非类别,继续LAB 15。

同图换问与同问换图

前两幅共享形状、位置和随机噪声,只改左侧颜色;第三幅只用于展示另一个问题条件,不是与前两幅只改一项。想让答案直接成为一个区域,继续 LAB 15 · 语言指代小练习:目标、参照与关系。

此段独立阅读
视觉与语言语言怎样选择当前需要的视觉信息?

0. 先给原例子正确命名

原 with-image 分支读取字典中的 cups 与 left。这些已是答案属性,因此此处保留为 known-attribute oracle。它能说明问题选择不同字段,却没有完成视觉感知。下面打印原例,再与真正从像素学习的模型区分。

此段独立阅读
展开完整实现代码
# Detached readonly runs write only to an isolated ephemeral directory.
import os, tempfile
_LAB_WORKDIR = tempfile.mkdtemp(prefix="lecture03-lab14-")
os.chdir(_LAB_WORKDIR)
print("ephemeral workdir:", _LAB_WORKDIR)

cases=[{'cups':2,'left':'red','q':'count'},{'cups':2,'left':'red','q':'left-color'},{'cups':2,'left':'blue','q':'left-color'}]
for c in cases:
    prior='2' if c['q']=='count' else 'red'
    oracle=str(c['cups']) if c['q']=='count' else c['left']
    print(c,'question-only rule',prior,'known-attribute oracle',oracle)
# No image was read above. The learned visual path begins below.
此段独立阅读
视觉与语言语言怎样选择当前需要的视觉信息?

1. 环境与有限问题接口

依赖 NumPy、Pillow、Matplotlib、PyTorch;Restart & Run All 后无需别册内存。输出保存在 outputs/lab14/。2026-09-22 本地 CPU 已执行,PyTorch 2.14.0+cpu,约十余秒;不代表 Deepnote 云端执行。

我们定义三种英文模板,用 ID 0/1/2 输入嵌入层。文字是模板的可读形式,模型不支持任意自然语言改写。答案空间为 one、two、red、blue、green、square、circle。这是封闭词表 VQA,足以检验条件依赖,但不能证明开放问答、常识推理或语言理解。

此段独立阅读
展开完整实现代码
from pathlib import Path
import json, hashlib, platform, sys, time
from datetime import datetime, timezone
import numpy as np
from PIL import Image, ImageDraw
import matplotlib
if 'get_ipython' not in globals(): matplotlib.use('Agg')
import matplotlib.pyplot as plt
import torch
from torch import nn
import torch.nn.functional as F
OUT=Path('outputs/lab14');OUT.mkdir(parents=True,exist_ok=True)
torch.set_num_threads(1);torch.manual_seed(1403);np.random.seed(1403);START=time.perf_counter()
COLORS={'red':(220,55,55),'blue':(45,95,225),'green':(35,170,75)}
QUESTIONS=['how many objects?','what color is the leftmost object?','what shape is the rightmost object?']
ANSWERS=['one','two','red','blue','green','square','circle']; A={x:i for i,x in enumerate(ANSWERS)}
print('CPU',torch.__version__,'seed 1403; answer vocabulary:',ANSWERS)
此段独立阅读
视觉与语言语言怎样选择当前需要的视觉信息?

2. 先按图像切分,避免同一图的不同问题泄漏

每张图含一个或两个带颜色的方形/圆形。所有图都提出同样三个问题:数量、最左物体颜色、最右物体形状;只有一个物体时,它同时是最左与最右。目标由生成器标注,这是监督数据的来源,不是推理接口。forward(pixels,question_id) 无法读取场景字典。

训练图 seed 14031、保留图 seed 14032;先得到 720/180 张互不相同的图,再扩展为 2160/540 个问答。训练左侧颜色以 75% 概率采红,实际本次为 73.61%;保留集三色各占三分之一。数量与形状平衡设计下各有两种可能。语言模型因此能在训练颜色问题上靠红色先验得高分,却没有随图改变的证据。

预测:只看问题,三个任务的保留集最佳常量回答分别约为 1/2、1/3、1/2,平均为 4/9≈44.4%。这比把七种答案混在一起说“随机率 1/7”更符合任务结构。

此段独立阅读
展开完整实现代码
def draw_scene(count,left_color,right_shape,rng):
    img=Image.new('RGB',(40,32),(244,242,236));d=ImageDraw.Draw(img)
    slots=[10] if count==1 else [10,30]
    for j,cx in enumerate(slots):
        cy=16+int(rng.integers(-3,4));r=int(rng.integers(4,7));cx+=int(rng.integers(-1,2))
        color=left_color if j==0 else str(rng.choice(list(COLORS)))
        shape=right_shape if j==len(slots)-1 else str(rng.choice(['square','circle']))
        box=(cx-r,cy-r,cx+r,cy+r)
        if shape=='square':d.rectangle(box,fill=COLORS[color])
        else:d.ellipse(box,fill=COLORS[color])
    arr=np.array(img,dtype=np.float32)/255
    return np.clip(arr+rng.normal(0,.014,arr.shape),0,1).astype(np.float32)

def make_vqa(n,seed,biased):
    rng=np.random.default_rng(seed);images=[];answers=[];meta=[]
    # Training has a 75% red prior. Test covers all 12 condition combinations equally.
    combos=[(c,col,sh) for c in [1,2] for col in COLORS for sh in ['square','circle']]
    for i in range(n):
        if biased:
            count=int(rng.choice([1,2]));color=str(rng.choice(list(COLORS),p=[.75,.125,.125]));shape=str(rng.choice(['square','circle']))
        else:count,color,shape=combos[i%len(combos)]
        images.append(draw_scene(count,color,shape,rng))
        answers.append([A['one' if count==1 else 'two'],A[color],A[shape]])
        meta.append({'count':count,'left_color':color,'right_shape':shape})
    return torch.tensor(np.stack(images)).permute(0,3,1,2),torch.tensor(answers),meta
Xtr,Ytr,train_meta=make_vqa(720,14031,True)
Xte,Yte,test_meta=make_vqa(180,14032,False)
train_hashes={hashlib.sha256(x.numpy().tobytes()).hexdigest() for x in Xtr}
assert not train_hashes.intersection(hashlib.sha256(x.numpy().tobytes()).hexdigest() for x in Xte)
# Metadata remains only in this data/label construction and plot captions.
# model.forward receives tensors (pixels, question_id), never these dictionaries.
np.savez_compressed(OUT/'lab14-data.npz',train_images=Xtr.numpy(),train_answers=Ytr.numpy(),test_images=Xte.numpy(),test_answers=Yte.numpy())
print('train/test image counts:',len(Xtr),len(Xte),'pairs:',Ytr.numel(),Yte.numel())
print('train left-color prior:',{c:sum(m['left_color']==c for m in train_meta)/len(train_meta) for c in COLORS})
print('test left-color prior:',{c:sum(m['left_color']==c for m in test_meta)/len(test_meta) for c in COLORS})
此段独立阅读
视觉与语言语言怎样选择当前需要的视觉信息?

3. 模型、损失与公平比较范围

像素分支是两层 CNN 与保留布局的全连接表示;问题分支是 12 维可学习嵌入。拼接后输出七个答案的 logits,以交叉熵 −log⁡p(a∣I,q)-\log p(a\mid I,q) 训练。question-only 使用相同问题嵌入维度、答案空间与训练样本,但省去 CNN。两者都真正更新参数,不能把手填“red”冒充训练的语言基线。

完整模型参数更多,因此二者差距本身不是严格的等参数容量结论。我们在同一个完整模型上又做像素置零与错配,固定参数再移除正确图像条件。该消融更直接检验本次预测是否使用图像。

此段独立阅读
展开完整实现代码
class VQA(nn.Module):
    def __init__(self,visual):
        super().__init__();self.visual=visual
        self.question=nn.Embedding(len(QUESTIONS),12)
        if visual:
            self.cnn=nn.Sequential(nn.Conv2d(3,12,3,padding=1),nn.ReLU(),nn.MaxPool2d(2),nn.Conv2d(12,24,3,padding=1),nn.ReLU(),nn.MaxPool2d(2),nn.Flatten(),nn.Linear(24*8*10,64),nn.ReLU())
        self.answer=nn.Sequential(nn.Linear(64+12 if visual else 12,64),nn.ReLU(),nn.Linear(64,len(ANSWERS)))
    def forward(self,pixels,question_id):
        q=self.question(question_id)
        features=torch.cat([self.cnn(pixels),q],-1) if self.visual else q
        return self.answer(features)

full=VQA(True);question_only=VQA(False)
curve=[]
for label,net in [('image+question',full),('question-only',question_only)]:
    optimizer=torch.optim.Adam(net.parameters(),lr=.002)
    for epoch in range(28):
        order=torch.randperm(Ytr.numel());net.train();loss_sum=0.
        for pair_ids in order.split(72):
            image_ids=pair_ids//3;qids=pair_ids%3
            logits=net(Xtr[image_ids],qids);target=Ytr[image_ids,qids]
            loss=F.cross_entropy(logits,target)
            optimizer.zero_grad();loss.backward();optimizer.step();loss_sum+=float(loss.detach())*len(pair_ids)
        if epoch in [0,5,13,27]:print(label,'epoch',epoch+1,'CE',round(loss_sum/Ytr.numel(),4))
        curve.append({'model':label,'epoch':epoch+1,'train_ce':loss_sum/Ytr.numel()})
    torch.save(net.state_dict(),OUT/('lab14-'+('visual' if net.visual else 'question-only')+'-weights.pt'))
此段独立阅读
视觉与语言语言怎样选择当前需要的视觉信息?

4. 逐问题读结果,别只看总准确率

保留集条件 数量 左侧颜色 右侧形状 全部
Image + question 100.0% 100.0% 100.0% 100.0%
Question-only 50.0% 33.3% 50.0% 44.4%
同一模型,像素置零 50.0% 33.3% 50.0% 44.4%
同一模型,图像循环错配 83.3% 50.0% 0.0% 44.4%

循环错配不是各属性独立重抽:数据按组合排列,所以相邻样本常保留数量,却总改变形状。于是 83.3%/50%/0% 不是异常;它正提醒我们干预协议必须说清。不要把这个排列解释为各任务的随机机会水平。

相同保留问答上的视觉、语言、零图像与错配图像比较

同图换问展示问题控制“读什么”,同问换图展示视觉控制“答什么”。先查控制图中红/蓝只变颜色的一对,再看代码记录的九条问答,判断本次证据是否同时满足这两个方向。

此段独立阅读
展开完整实现代码
@torch.no_grad()
def answer_all(net,images):
    net.eval()
    return torch.stack([net(images,torch.full((len(images),),q)).argmax(-1) for q in range(3)],1)
normal=answer_all(full,Xte);qo=answer_all(question_only,Xte)
zero=answer_all(full,torch.zeros_like(Xte));shuffled=answer_all(full,Xte[torch.roll(torch.arange(len(Xte)),1)])
train_full=answer_all(full,Xtr);train_qo=answer_all(question_only,Xtr)
def score(p,y):return {'overall':float(p.eq(y).float().mean()),**{QUESTIONS[q]:float(p[:,q].eq(y[:,q]).float().mean()) for q in range(3)}}
metrics={'test_image_question':score(normal,Yte),'test_question_only':score(qo,Yte),'test_zero_pixels':score(zero,Yte),'test_shuffled_pixels':score(shuffled,Yte),'train_image_question':score(train_full,Ytr),'train_question_only':score(train_qo,Ytr)}
print(json.dumps(metrics,indent=2))
# Recreate two images with matched nuisance randomness, only left color differs.
control_specs=[(2,'red','square'),(2,'blue','square'),(1,'green','circle')]
controls_x=torch.tensor(np.stack([draw_scene(*s,np.random.default_rng(444)) for s in control_specs])).permute(0,3,1,2)
controls_pred=answer_all(full,controls_x);controls_qo=answer_all(question_only,controls_x)
controls=[{'image_condition':list(s),'question':q,'image_question':ANSWERS[int(controls_pred[i,j])],'question_only':ANSWERS[int(controls_qo[i,j])]} for i,s in enumerate(control_specs) for j,q in enumerate(QUESTIONS)]
print('same image / changed question and same question / changed image:')
for row in controls:print(row)
此段独立阅读
视觉与语言语言怎样选择当前需要的视觉信息?

5. 学习曲线与可以被推翻的结论

图像问答与只读问题模型的实际训练交叉熵

完整模型在这个有限合成分布上达到 100%,不等于解决真实 VQA。颜色、边缘和对象数都容易辨别,问题模板也固定。只改一个条件:保持图像不变,把未知改写“which hue is on the left?”输入当前 ID 接口会发生什么?正确结论是它根本没有这个文本编码能力,不应把手工映射改写到 ID 的功能归功于网络。

另一项可运行改动是把训练颜色先验改为均匀,保留模型与测试不变。先预测 question-only 的训练颜色分数会下降,而保留集颜色分数仍约为三分之一。模型可以减少训练与测试的先验落差,却仍需要看图才能逐例回答。

此段独立阅读
展开完整实现代码
fig,axes=plt.subplots(1,3,figsize=(11,4))
for i,ax in enumerate(axes):
    ax.imshow(controls_x[i].permute(1,2,0));ax.axis('off')
    rows=['count: ','left color: ','right shape: ']
    text='\n'.join(rows[j]+ANSWERS[int(controls_pred[i,j])]+' [Q-only: '+ANSWERS[int(controls_qo[i,j])]+']' for j in range(3))
    ax.set_title(text,fontsize=10)
fig.suptitle('LAB14: same questions, changed image; three questions per image',fontsize=13)
fig.tight_layout();fig.savefig(OUT/'lab14-vqa-controls.png',dpi=160);plt.show();plt.close(fig)
fig,ax=plt.subplots(figsize=(10,3.6));labels=['image + Q','Q only','zero pixels','shuffled pixels'];keys=['test_image_question','test_question_only','test_zero_pixels','test_shuffled_pixels'];values=[metrics[k]['overall'] for k in keys]
ax.bar(labels,values,color=['#356b91','#ad8251','#9da6ad','#9da6ad']);ax.set(ylim=(0,1.1),ylabel='answer accuracy (540 held-out pairs)')
for i,v in enumerate(values):ax.text(i,v+.02,f'{v:.1%}',ha='center')
fig.tight_layout();fig.savefig(OUT/'lab14-vqa-ablation.png',dpi=160);plt.show();plt.close(fig)
fig,ax=plt.subplots(figsize=(8,3.2))
for label in ['image+question','question-only']:
    r=[v for v in curve if v['model']==label];ax.plot([v['epoch'] for v in r],[v['train_ce'] for v in r],label=label)
ax.set(xlabel='epoch',ylabel='answer cross entropy');ax.legend();fig.tight_layout();fig.savefig(OUT/'lab14-vqa-learning.png',dpi=160);plt.show();plt.close(fig)
result={'notebook_id':'11d9f74871544a77b42024a9fe06b81a','execution':'local CPU, fresh process; not a Deepnote cloud run','created_utc':datetime.now(timezone.utc).isoformat(),'elapsed_seconds':time.perf_counter()-START,'environment':{'python':sys.version,'torch':torch.__version__,'numpy':np.__version__,'platform':platform.platform()},'split':{'train_seed':14031,'test_seed':14032,'train_images':720,'test_images':180,'questions_per_image':3,'split_before_question_expansion':True,'train_red_prior':.75,'test_color_prior':'uniform by construction'},'metrics':metrics,'curve':curve,'controls':controls,'predictions':[{'truth':[ANSWERS[int(v)] for v in Yte[i]],'image_question':[ANSWERS[int(v)] for v in normal[i]],'question_only':[ANSWERS[int(v)] for v in qo[i]]} for i in range(len(Xte))],'files_sha256':{p.name:hashlib.sha256(p.read_bytes()).hexdigest() for p in sorted(OUT.glob('*.pt'))},'data_sha256':hashlib.sha256((OUT/'lab14-data.npz').read_bytes()).hexdigest()}
(OUT/'lab14-results.json').write_text(json.dumps(result,indent=2));print('saved',OUT,'seconds',round(result['elapsed_seconds'],2))
此段独立阅读
视觉与语言语言怎样选择当前需要的视觉信息?

结果文件、来源与回链

lab14-data.npz 保存像素与答案;两个 .pt 文件对应两个真实训练模型;lab14-results.json 保存每类准确率、逐轮损失、九条受控问答、逐图保留预测及环境/数据哈希。场景元数据只用于监督标签与解读,未传入网络。

VQA 原论文,作者 arXiv 页面 给出图像与问题到答案的任务定义与基线比较。VQA 官方项目 提供真实数据、评价与后续 VQA v2;本实验的精确分类准确率不是官方多人答案共识评分。我们的合成颜色先验是教学构造,不是该数据集的实测统计。

回到 CLS 03:CLIP 排候选描述,caption 自回归生成,VQA 在问题条件下输出答案。它们共享视觉与语言,输出空间和训练目标却不同,不能因为界面都接受文字就互相替代。

此段独立阅读