# LAB 04 · CLIP:候选、提示与关系检验 主线阅读与优先实验 真实CLIP输入/权重/分数、候选改动、四条件规则与独立关系推理 对应课堂(4 页) 可先阅读已有结果,再决定是否运行。重算时请核对各节记录的数据、参数、环境与日期。 **学习路径:** [文字候选与图文检索](/course/study/clip-retrieval.html) **固定图像与模型,只改文字候选,哪些量应保持,哪些量会变?** 先读[P58](/course/pages/l3-r2-p58.html)和[候选概率算例](/course/blocks/clip-candidate-probability.html),再比较本册第1节猫图的四个条件:加候选、换模板、删去cat。 这里的zero-shot是不使用当前下游数据集再训练模型,直接用已有图像和文字编码器比较候选;并不保证相关概念从未进入预训练资料。读结果时同时记录cosine、候选softmax与top-1,解释为什么新增候选可改变softmax却不改变原有cosine。 最短路线:四臂设计 → 已保存真实结果 → 第5节关系反例。开头持球规则是可跳过的解析热身。想重算时按原代码顺序准备;默认`RUN_CLIP=False`、`RUN_FRESH_CLIP=False`,只准备或读缓存,不能称为本次模型推理。模型、照片和2026-09-22复核的来源在各结果旁保留。 完成后留下一个具体对照:固定了什么、改变了什么、哪个分数因此改变。需要把向量做成图库时再进入[LAB 12](https://deepnote.com/project/38755e4e-23f6-4f5f-8c68-8a2e4114b1c1/notebook/921eeaf784284ce19568c6ca7619b55a),不重复做一份猫图实验。 [此段独立阅读](/course/notebook-blocks/6ee1d767afb744e18fbfa9989ac3cd72.html) ### 先做一个无需模型的关系检验 [此段独立阅读](/course/notebook-blocks/49f411cef216497594cb4ab917f97b2f.html) **观察同一套 A/B 候选,再预测两条规则。** 左右均按观看者位置。 A. `A person in red holds a ball beside a person in blue.` B. `A person in blue holds a ball beside a person in red.` | 条件 1:左红持球、右蓝 | 条件 2:左蓝持球、右红 | | --- | --- | | ![条件1:观看者左侧红衣人持球,右侧蓝衣人不接触球](/course/knowledge-assets/b44ae6205135966005a376cd.jpg) | ![条件2:观看者左侧蓝衣人持球,右侧红衣人不接触球](/course/knowledge-assets/a2fad6392231baa79e7361b8.jpg) | | 条件 3:左红、右蓝持球 | 条件 4:左蓝、右红持球 | | --- | --- | | ![条件3:观看者左侧红衣人,右侧蓝衣人持球](/course/knowledge-assets/429e669b3080a200d8e3b65c.jpg) | ![条件4:观看者左侧蓝衣人,右侧红衣人持球](/course/knowledge-assets/62eb7ad7573b1dc20562ec8a.jpg) | ImageGen 情境插图。对象、衣色、左右与持球身份按条件核对;独立生成图中的姿态、背景与光照也可能变化,因此不是严格单变量照片实验。 在前两种条件里,“看持球者衣色”和“只看左侧衣色”给出相同答案。先不运行代码,分别填入条件 3/4 下两条规则会选 A 还是 B。哪一个改动使两条解释产生不同预测? 共享素材:本讲新版持球四条件,按观看者左右排列;每图两人一个球,非持球人不接触球。图像来自同一人物场景的编辑与条件重组,用于区分两条规则,不是模型推理结果。 [此段独立阅读](/course/notebook-blocks/d19f00cd82bd4f5391c89134e56b7239.html)展开完整实现代码 ``` # 每次整册运行使用临时工作目录,保留项目旧文件与历史结果。 import os, tempfile L03_RUN_DIR=tempfile.mkdtemp(prefix="lecture03-lab04-") os.chdir(L03_RUN_DIR) # 可独立运行;无需前面任何模型、文件或隐藏变量。 # 改 holder 为 left/right,先预测,再看两条规则是否仍然一致。 conditions = [ {'condition': 1, 'left': 'red', 'right': 'blue', 'holder': 'left'}, {'condition': 2, 'left': 'blue', 'right': 'red', 'holder': 'left'}, {'condition': 3, 'left': 'red', 'right': 'blue', 'holder': 'right'}, {'condition': 4, 'left': 'blue', 'right': 'red', 'holder': 'right'}, ] def choice_for_color(color): return {'red': 'A', 'blue': 'B'}[color] def compare_rules(case): relation = choice_for_color(case[case['holder']]) position = choice_for_color(case['left']) return relation, position assert compare_rules({'left':'red','right':'blue','holder':'right'}) == ('B','A') print('条件 | 持球关系规则 | 左侧颜色规则 | 两者不同') for case in conditions: relation, position = compare_rules(case) print(case['condition'], relation, position, relation != position) ``` [此段独立阅读](/course/notebook-blocks/b5078a7304e54d9fa89ee186821582fb.html) **确定性规则结果**(也可直接阅读): | 条件 | 按持球关系 | 按左侧衣色 | 能否区分两条规则 | | --- | --- | --- | --- | | 1 | A | A | 不能 | | 2 | B | B | 不能 | | 3 | B | A | 能 | | 4 | A | B | 能 | 这些值来自条件定义和规则计算,不是 CLIP 相似度。若要检验真实模型,需要固定 A/B 文句,对全部四张图实际推理,再比较结果是否追踪持球关系。即使通过这四例,也仍需更多对象、布局与表达方式。 接下来保留的猫图实验回答另一个问题:候选与提示变化怎样改变真实 CLIP 的相对分数。图文匹配提供整图候选分数;需要区域坐标时看 CLS 03 的 GLIP,需要自由回答时看 LLaVA 的接口与训练。 [此段独立阅读](/course/notebook-blocks/852fd1d91125421093ffcda34307c805.html) ### 1 · 图像不变,换文字候选改变了什么? [此段独立阅读](/course/notebook-blocks/2a5577cae28d48a29bceae894948ab01.html) 历史保存结果使用 **OpenAI CLIP ViT-B/32** 的真实预训练权重与固定真实照片 Chelsea the cat(Stefan van der Walt,CC0,来自 scikit-image v0.25.2)。模型与官方预处理固定,不做任何梯度更新。模型的大规模图文预训练没有在本册重做。 比较分为四臂,避免把候选集合与 prompt 同时改变: | 臂 | 候选 | prompt | 隔离的问题 | | --- | --- | --- | --- | | A | cat / dog / horse | a photo of a `{name}`. | 固定的基线任务 | | B | cat / dog / horse / tiger / fox | 与 A 相同 | 只扩展候选集合 | | C | 与 A 相同 | a cropped photo of a `{name}`. | 只改变描述模板 | | D | dog / horse / tiger / fox | 与 A 相同 | 删除原正确类别,改变可选答案空间 | 记录 **cosine 相似度、模型 logit scale、softmax、top-1**。当只扩展候选集时,共享候选的 cosine 应保持不变,而 softmax 会因分母改变。D 以 B 为直接对照:候选集只删除 cat、保留 B 的其余候选和 prompt。删除 cat 后,模型仍必须从剩余候选选一个;高相对分数不构成“开放世界里确定有它”的证明。 重算需安装 `requirements.txt`,运行 `python fetch_clip.py` 取得固定 revision 权重,再运行本节。下载失败时本节明确标为未运行;没有用随机相似度代替。模型文件约 600 MB,不放入教学小包。 来源:[OpenAI CLIP 官方实现](https://github.com/openai/CLIP);[官方模型卡](https://huggingface.co/openai/clip-vit-base-patch32);[真实照片](https://scikit-image.org/docs/stable/api/skimage.data.html#skimage.data.chelsea)。 **预处理复核(2026-09-18):**本包固定 `transformers==4.57.1` 与模型 revision。保存输出中的 `use_fast` 提醒说明当时使用 `CLIPImageProcessor`,不是执行失败。2026-09-18 的复核将同一慢图像处理器和 `CLIPTokenizerFast` 分开显式指定:RGB → 短边缩放 224(bicubic)→ 224×224 中心裁剪 → 按官方均值/标准差归一化。用包内同一照片及四臂全部 prompt,仅重跑预处理,所得像素张量和 token IDs 与原默认选择完全相同(最大像素张量差 0)。原相似度、图和输出仍是此前模型推理保存结果;该次预处理复核没有重跑模型。详细复核见包内 `results/preprocessing-audit-20260918.json`。 [此段独立阅读](/course/notebook-blocks/fc8ddc9df75d4837b5394962ee0837d3.html) ### 已保存的真实结果:v16 教师 CPU 推理 [此段独立阅读](/course/notebook-blocks/2277d769b3f944bfb8a35fe185c93e32.html) 下图和表是保留的 v16 教师 CPU 推理,当前整理没有重新运行模型,不依赖下一格的 `RUN_CLIP` 值。原权重与照片哈希写在配套 provenance 中。 ![固定照片与候选比较](/assets/display/fed4bc77235bae1509d5761b.webp) | 臂 | top-1 | 候选:cosine / softmax | | --- | --- | --- | | A_base | cat | cat: 0.2869/0.9958;dog: 0.2310/0.0037;horse: 0.2109/0.0005 | | Bexpandonly | cat | cat: 0.2869/0.9819;dog: 0.2310/0.0037;horse: 0.2109/0.0005;tiger: 0.2436/0.0130;fox: 0.2173/0.0009 | | Cpromptonly | cat | cat: 0.2933/0.9951;dog: 0.2368/0.0035;horse: 0.2276/0.0014 | | Dremovecat | tiger | dog: 0.2310/0.2039;horse: 0.2109/0.0273;tiger: 0.2436/0.7174;fox: 0.2173/0.0514 | 扩展候选后共享 cosine 保持不变,softmax 分母改变。D 删除 cat 后仍必须选一个;不能把剩余候选中的相对自信当作开放世界正确性。 [此段独立阅读](/course/notebook-blocks/e8ab1372e4794474b1dd5aa548d4fe86.html) ### 2 · 独立准备并核对预处理 [此段独立阅读](/course/notebook-blocks/def6f681ce524dbb9da83aa79032d1c8.html) 在当前项目准备 `lab10-clip/` 并安装其中的 `requirements.txt`。小配置文件随包附带,代码将逐个校验。默认只进行图像/文字预处理;需要重做模型推理时,再从本目录运行 `python fetch_clip.py` 下载固定 revision 的公开权重(约 600 MB)。本轮不要求下载或运行模型。 [此段独立阅读](/course/notebook-blocks/6a9ad60448b840d8acee32a8714ebc3d.html)展开完整实现代码 ``` # 当前项目中的独立材料:已存在则复用,缺失才从本讲原站准备。 from pathlib import Path import io, zipfile, requests _base = Path.cwd() _folder = 'lab10-clip' _sentinel = 'assets/chelsea-cat.png' _choices = [_base/_folder, _base/'lecture03-labs'/_folder, _base] _ready = next((p for p in _choices if (p/_sentinel).is_file()), None) if _ready is None: _url = 'https://codingai-lec03.pages.dev/course/materials/' + _folder + '.zip' _response = requests.get(_url, timeout=60) _response.raise_for_status() _payload = _response.content with zipfile.ZipFile(io.BytesIO(_payload)) as _archive: _names = [i.filename for i in _archive.infolist() if not i.is_dir()] _prefix = _folder + '/' _prefixed = all(n.startswith(_prefix) for n in _names) _destination = _base if _prefixed else _base/_folder for _item in _archive.infolist(): _target = (_destination/_item.filename).resolve() if not _target.is_relative_to(_destination.resolve()): raise ValueError('材料包中有越界路径,停止解压。') if _item.is_dir(): _target.mkdir(parents=True, exist_ok=True) elif not _target.exists(): _target.parent.mkdir(parents=True, exist_ok=True) _target.write_bytes(_archive.read(_item)) _ready = next((p for p in _choices if (p/_sentinel).is_file()), None) assert _ready is not None, '请将本讲完整材料包解压到当前项目的 ' + _folder + '/;保留文件结构。' LAB_ROOT = _ready print('材料目录已就绪:', LAB_ROOT) import json, hashlib, platform, sys from pathlib import Path import numpy as np import matplotlib.pyplot as plt ROOT=LAB_ROOT if not (ROOT/'assets').is_dir() and (ROOT/'lab10-clip'/'assets').is_dir():ROOT=ROOT/'lab10-clip' assert (ROOT/'assets').is_dir(), '请解压本实验完整包,并在该目录或其父目录运行。' OUT=ROOT/'runs/current'; OUT.mkdir(parents=True,exist_ok=True) FIG=OUT/'figures'; FIG.mkdir(exist_ok=True) plt.rcParams.update({'figure.dpi':120,'font.size':10,'axes.spines.top':False,'axes.spines.right':False}) BLUE,GOLD,GREEN,PURPLE='#002676','#E6A500','#00856A','#69458D' def sha_file(path):return hashlib.sha256(Path(path).read_bytes()).hexdigest() def savefig(name): if not plt.gcf().get_constrained_layout():plt.tight_layout() plt.savefig(FIG/name,dpi=140,bbox_inches='tight');plt.show() import torch from PIL import Image # 若宿主缺少固定预处理依赖,仅装入本次临时目录;不修改项目环境或下载模型权重。 import importlib.util, importlib.metadata, subprocess try: _transformers_version=importlib.metadata.version('transformers') except importlib.metadata.PackageNotFoundError: _transformers_version=None if _transformers_version!='4.57.1': _deps=ROOT/'_temporary_preprocessing_dependencies' subprocess.check_call([sys.executable,'-m','pip','install','--quiet','--disable-pip-version-check', '--no-cache-dir','--target',str(_deps),'transformers==4.57.1', 'numpy=='+np.__version__,'filelock>=3.20.3,<4']) sys.path.insert(0,str(_deps)) importlib.invalidate_caches() from transformers import CLIPProcessor,CLIPImageProcessor,AutoTokenizer import transformers assert transformers.__version__=='4.57.1', '请使用包内固定 requirements.txt。' torch.set_num_threads(2) provenance=json.loads((ROOT/'assets/provenance.json').read_text()) assert sha_file(ROOT/'assets/chelsea-cat.png')==provenance['image_sha256'] REVISION=provenance['revision'] local=ROOT/'.model-cache'/REVISION config_local=ROOT/'model-config' # small verified config files ship in the package; weights are downloaded only on request. manifest=json.loads((config_local/'manifest.json').read_text()) for f,expected in manifest['sha256'].items():assert sha_file(config_local/f)==expected clip_processor=CLIPProcessor(image_processor=CLIPImageProcessor.from_pretrained(config_local,local_files_only=True),tokenizer=AutoTokenizer.from_pretrained(config_local,local_files_only=True,use_fast=True)) photo=Image.open(ROOT/'assets/chelsea-cat.png').convert('RGB') arms={'A_base':(['cat','dog','horse'],'a photo of a {}.'),'B_expand_only':(['cat','dog','horse','tiger','fox'],'a photo of a {}.'),'C_prompt_only':(['cat','dog','horse'],'a cropped photo of a {}.'),'D_remove_cat':(['dog','horse','tiger','fox'],'a photo of a {}.')} image_inputs=clip_processor(images=photo,return_tensors='pt') all_prompts=list(dict.fromkeys(template.format(name) for names,template in arms.values() for name in names)) text_inputs=clip_processor(text=all_prompts,padding=True,return_tensors='pt') assert tuple(image_inputs['pixel_values'].shape)==(1,3,224,224) assert clip_processor.image_processor.__class__.__name__=='CLIPImageProcessor' assert clip_processor.tokenizer.__class__.__name__=='CLIPTokenizerFast' preprocess_verification={'scope':'preprocessing only, no weights loaded','pixel_shape':list(image_inputs['pixel_values'].shape),'pixel_sha256':hashlib.sha256(image_inputs['pixel_values'].numpy().tobytes()).hexdigest(),'prompt_count':len(all_prompts),'tokenizer':clip_processor.tokenizer.__class__.__name__,'image_processor':clip_processor.image_processor.__class__.__name__,'transformers':transformers.__version__,'image_sha256':sha_file(ROOT/'assets/chelsea-cat.png'),'revision':REVISION} historical_preprocess=json.loads((ROOT/'historical-v16/results/preprocessing-audit-20260918.json').read_text()) assert preprocess_verification['pixel_sha256']==historical_preprocess['pixel_values_sha256'] legacy_processor=CLIPProcessor.from_pretrained(config_local,local_files_only=True) legacy_image=legacy_processor(images=photo,return_tensors='pt') legacy_text=legacy_processor(text=all_prompts,padding=True,return_tensors='pt') assert torch.equal(legacy_image['pixel_values'],image_inputs['pixel_values']) assert all(torch.equal(legacy_text[k],text_inputs[k]) for k in text_inputs) preprocess_verification['legacy_default_pixel_max_abs_difference']=float((legacy_image['pixel_values']-image_inputs['pixel_values']).abs().max()) preprocess_verification['all_prompt_token_tensors_equal']=True preprocess_verification['token_ids_sha256']=hashlib.sha256(text_inputs['input_ids'].numpy().tobytes()).hexdigest() (OUT/'preprocessing-verification.json').write_text(json.dumps(preprocess_verification,indent=2));print(json.dumps(preprocess_verification,indent=2)) ``` [此段独立阅读](/course/notebook-blocks/aaa45a4607af423da4c9144dad703b56.html) **本地保存输出(非 Deepnote 云端运行)** ``` Using a slow image processor as `use_fast` is unset and a slow processor was saved with this model. `use_fast=True` will be the default behavior in v4.52, even if the model was saved with a slow processor. This will result in minor differences in outputs. You'll still be able to use a slow processor with `use_fast=False`. { "scope": "preprocessing only, no weights loaded", "pixel_shape": [ 1, 3, 224, 224 ], "pixel_sha256": "59ba65f37c29915984addd8e4d8856f9c1579c284af0ef0260bc4f25eadbbab9", "prompt_count": 8, "tokenizer": "CLIPTokenizerFast", "image_processor": "CLIPImageProcessor", "transformers": "4.57.1", "image_sha256": "596aa1e7cb875eb79f437e310381d26b338a81c2da23439704a73c4651e8c4bb", "revision": "3d74acf9a28c67741b2f4f2ea7635f0aaf6f0268", "legacy_default_pixel_max_abs_difference": 0.0, "all_prompt_token_tensors_equal": true, "token_ids_sha256": "b51f79cb2f07674c29ec6589bd5bc2f1537e340cfc6c78f567d3d09481d2a902" } ``` [此段独立阅读](/course/notebook-blocks/fe26b27f069b4605aa4f794f6c8b4fcc.html) ### 3 · 可选:固定模型,重算四个候选条件 [此段独立阅读](/course/notebook-blocks/7203294161d5450497543d518d80ed6d.html) 默认 `RUN_CLIP=False`,保留猫图历史分数供阅读。若自行重做推理:先准备固定权重,再改为 True,顺序运行。新输出只写 `runs/current/`。只完成预处理不表示已经完成模型推理。 [此段独立阅读](/course/notebook-blocks/d8044a8e957c429c957ef7f6c29f71cc.html)展开完整实现代码 ``` RUN_CLIP=False # 已下载固定权重后改 True;默认只核对准备与预处理。 if RUN_CLIP: from transformers import CLIPModel, CLIPProcessor, CLIPImageProcessor, AutoTokenizer from PIL import Image REVISION='3d74acf9a28c67741b2f4f2ea7635f0aaf6f0268' local=ROOT/'.model-cache'/REVISION assert (local/'pytorch_model.bin').exists(), '先运行 python fetch_clip.py;本单元不伪造相似度。' assert sha_file(local/'pytorch_model.bin')==provenance['model_sha256'], '权重 SHA-256 不匹配,停止。' clip_model=CLIPModel.from_pretrained(local,local_files_only=True).eval() # Make the teacher run's slow image processing + fast tokenizer explicit. # A shared use_fast=False would also change the tokenizer; keep each choice separate. clip_processor=CLIPProcessor( image_processor=CLIPImageProcessor.from_pretrained(local,local_files_only=True), tokenizer=AutoTokenizer.from_pretrained(local,local_files_only=True,use_fast=True)) for parameter in clip_model.parameters():parameter.requires_grad_(False) photo=Image.open(ROOT/'assets/chelsea-cat.png').convert('RGB') image_inputs=clip_processor(images=photo,return_tensors='pt') arms={ 'A_base':(['cat','dog','horse'],'a photo of a {}.'), 'B_expand_only':(['cat','dog','horse','tiger','fox'],'a photo of a {}.'), 'C_prompt_only':(['cat','dog','horse'],'a cropped photo of a {}.'), 'D_remove_cat':(['dog','horse','tiger','fox'],'a photo of a {}.')} clip_records={} with torch.no_grad(): image_feature=clip_model.get_image_features(**image_inputs) image_feature=image_feature/image_feature.norm(dim=-1,keepdim=True) scale=float(clip_model.logit_scale.exp()) for arm,(names,template) in arms.items(): prompts=[template.format(name) for name in names] text_inputs=clip_processor(text=prompts,return_tensors='pt',padding=True) text_features=clip_model.get_text_features(**text_inputs) text_features=text_features/text_features.norm(dim=-1,keepdim=True) cosine=(image_feature@text_features.T).squeeze(0) probabilities=(cosine*scale).softmax(-1) clip_records[arm]={'candidate_names':names,'prompts':prompts,'cosine':cosine.tolist(), 'softmax':probabilities.tolist(),'top1':names[int(cosine.argmax())]} assert np.allclose(clip_records['A_base']['cosine'],clip_records['B_expand_only']['cosine'][:3],atol=2e-6) clip_result={'status':'actually run locally on CPU','model_repo':'openai/clip-vit-base-patch32','revision':REVISION, 'model_sha256':sha_file(local/'pytorch_model.bin'),'image_sha256':sha_file(ROOT/'assets/chelsea-cat.png'), 'image_source':'scikit-image v0.25.2 chelsea.png; Stefan van der Walt; CC0','logit_scale':scale, 'arms':clip_records,'limitations':['one image; no dataset-level accuracy estimate','relative candidate scores are not open-set certainty']} (OUT/'clip-results.json').write_text(json.dumps(clip_result,indent=2)) fig,axes=plt.subplots(1,3,figsize=(12,3.8));axes[0].imshow(photo);axes[0].axis('off');axes[0].set_title('Same real photograph in all arms') for arm,color in [('A_base',BLUE),('B_expand_only',GOLD)]: rr=clip_records[arm]; loc=np.arange(len(rr['candidate_names'])) axes[1].plot(loc,rr['cosine'],marker='o',label=arm,color=color) axes[2].plot(loc,rr['softmax'],marker='o',label=arm,color=color) for ax,title in zip(axes[1:],['Shared cosine remains fixed','Relative softmax changes']): ax.set(xticks=range(5),xticklabels=arms['B_expand_only'][0],title=title);ax.legend(fontsize=8);ax.grid(alpha=.18) axes[1].set_ylabel('Cosine similarity');axes[2].set_ylabel('Softmax over chosen candidates') savefig('05-clip-candidates.png') print('Actual checkpoint SHA-256:',clip_result['model_sha256']) print('arm | top1 | candidate : cosine / softmax') for arm,r in clip_records.items(): print(arm,r['top1'],[(name,round(co,4),round(pr,4)) for name,co,pr in zip(r['candidate_names'],r['cosine'],r['softmax'])]) else: print('CLIP 未运行:请不要将空结果写成真实相似度。') ``` [此段独立阅读](/course/notebook-blocks/07786a60d0614351a320e5da4ea0ef2d.html) **本地保存输出(非 Deepnote 云端运行)** ``` CLIP 未运行:请不要将空结果写成真实相似度。 ``` [此段独立阅读](/course/notebook-blocks/8249b79b5c814406ad65261d54ff139e.html) ### 4 · 只改一个条件 [此段独立阅读](/course/notebook-blocks/30b2d43a6e3247dbace306ecfe1c52bb.html) 让 AI 只增加一个候选名;核对照片、模型 revision、预处理和旧 prompt 未改变,再比较共享候选 cosine 与新的 softmax。或只改同一候选集的描述模板。二者不要同时改变。 一张照片不提供数据集级准确率;预训练不是本实验复现对象,图文匹配也不是 captioning 或 text-to-image。 [此段独立阅读](/course/notebook-blocks/7e3ca724e5f44834b4dce2877cb743e6.html)展开完整实现代码 ``` # 当前单元无法确认宿主平台或是否使用全新进程;unknown 不等于 False。 verification={'identity':'v17 independent CLIP candidates','fresh_process':'unknown','preprocessing_actually_run':True,'clip_model_inference_actually_run':RUN_CLIP,'mnist_loaded':False,'cnn_loaded':False,'cloud_run':'unknown','historical_inference':'v16 saved CPU result; not this execution','revision':REVISION,'environment':{'python':platform.python_version(),'torch':torch.__version__,'transformers':transformers.__version__}} (OUT/'verification.json').write_text(json.dumps(verification,indent=2));print(json.dumps(verification,indent=2)) ``` [此段独立阅读](/course/notebook-blocks/4a7cd45fd86549e5b31fee86f6949ab3.html) **本地保存输出(非 Deepnote 云端运行)** ``` { "identity": "v17 independent CLIP candidates", "fresh_process": true, "preprocessing_actually_run": true, "clip_model_inference_actually_run": false, "mnist_loaded": false, "cnn_loaded": false, "cloud_run": false, "historical_inference": "v16 saved CPU result; not this execution", "revision": "3d74acf9a28c67741b2f4f2ea7635f0aaf6f0268", "environment": { "python": "3.12.14", "torch": "2.14.0+cpu", "transformers": "4.57.1" } } ``` [此段独立阅读](/course/notebook-blocks/f49cbfdaf72c4e6a9129f660c465f555.html) ## 5 · 2026-09-22 保存实测:固定权重复核与关系分数 此前的猫图实验已经包含真实模型,值得复用。2026-09-22 的修复从原站源码 `bbf8a78` 的 `course/materials/lab10-clip.zip` 恢复照片、固定配置、旧结果和 provenance,未把旧分数换成新日期。随后在本地全新 Python 进程实际载入同一 CLIP ViT-B/32 checkpoint,完成图像编码、文字编码、归一化和相似度;没有重新训练 CLIP。 权重 revision 为 `3d74acf9a28c67741b2f4f2ea7635f0aaf6f0268`,权重 SHA-256 为 `a63082132ba4f97a80bea76823f544493bffa8082296d62d71581a4feff1576f`。真实猫图的预处理张量 SHA 与旧记录一致,四臂全部 cosine 与旧输出的最大绝对差为 **1.0431×10⁻⁷**。这项复核支持“使用相同输入和模型得到相同分数”,不把一个样本的成功扩大为模型总体准确率。 [完整代码、输入、缓存和 2026-09-22 输出](/course/materials/lecture03-clip-retrieval.zip),下载解压后先安装 `requirements-cache.txt`。缓存阅读和检索无需下载 605 MB 权重。若要在自己的环境重算编码,安装 `requirements-inference.txt`,执行 `python fetch_clip.py`,再执行 `python run_clip_retrieval.py --encode`。固定权重哈希错误会停止,下载失败不会伪造分数。新运行进入新的 UTC 时间目录。 [此段独立阅读](/course/notebook-blocks/4f42285447374de387bf748b74f29f7f.html)展开完整实现代码 ``` # 独立材料,不依赖另一册 notebook 的内存;重复运行复用本项目已下载内容。 from pathlib import Path import io, json, zipfile, requests, sys, hashlib _candidates=[Path.cwd()/'lecture03-clip-retrieval', Path.cwd()] RETRIEVAL_ROOT=next((p for p in _candidates if (p/'cache/clip-features.npz').exists()),None) if RETRIEVAL_ROOT is None: _response=requests.get('https://codingai-lec03.pages.dev/course/materials/lecture03-clip-retrieval.zip',timeout=60) _response.raise_for_status() _data=_response.content print('下载材料 ZIP:',len(_data),'bytes; SHA256',hashlib.sha256(_data).hexdigest()) with zipfile.ZipFile(io.BytesIO(_data)) as z: for info in z.infolist(): target=(Path.cwd()/info.filename).resolve() assert target.is_relative_to(Path.cwd().resolve()), '拒绝越界路径' if info.is_dir():target.mkdir(parents=True,exist_ok=True) elif not target.exists(): target.parent.mkdir(parents=True,exist_ok=True);target.write_bytes(z.read(info)) RETRIEVAL_ROOT=Path.cwd()/'lecture03-clip-retrieval' assert (RETRIEVAL_ROOT/'cache/provenance.json').is_file() sys.path.insert(0,str(RETRIEVAL_ROOT)) import run_clip_retrieval as cr import numpy as np p=json.loads((RETRIEVAL_ROOT/'cache/provenance.json').read_text()) assert cr.sha(RETRIEVAL_ROOT/'cache/clip-features.npz')==p['cache_sha256'] assert cr.sha(RETRIEVAL_ROOT/'assets/manifest.json')==p['manifest_sha256'] manifest=json.loads((RETRIEVAL_ROOT/'assets/manifest.json').read_text()) for r in manifest['gallery']+manifest['relation']+manifest['holder']: assert cr.sha(RETRIEVAL_ROOT/r['path'])==r['sha256'] a=np.load(RETRIEVAL_ROOT/'cache/clip-features.npz') print('已核验真实 CLIP 缓存;生成时间',p['generated_at_utc']) print('本格只载入缓存,没有在当前 notebook 运行 CLIP 编码器。') print('gallery, queries:',a['gallery'].shape,a['queries'].shape) ``` [此段独立阅读](/course/notebook-blocks/facd5784eb654f089d7c523bab835dce.html) ### 预处理和两条编码支路到底做了什么? 图像先转 RGB,以 bicubic 将短边缩放至 224,再中心裁剪为 224×224,使用 checkpoint 指定的均值和标准差归一化。文本用固定 `CLIPTokenizerFast` 产生 token IDs、起止标记和 padding;本例短句没有超过 77-token 上限。图像批次是 `[B,3,224,224]`,文字批次是 `[Q,L]`,两个编码器的输出均投影为 512 维。它们随后分别除以 L2 范数;这一步之后点积才等于 cosine。 设图像、文字特征为 $v_i,t_j$,归一化后的分数为 $s_{ij}=v_i^\top t_j$。候选内分布 $p(j|i,\mathcal C)=\exp(\alpha s_{ij})/\sum_{k\in\mathcal C}\exp(\alpha s_{ik})$ 还需要固定模型的 $\alpha$,本 checkpoint 为约 100。A→B 改变分母,A→C 改变 token 和文本特征;B→D 删除了正确答案。三种干预不能混为“模型概率变了”。 候选 softmax 既没有估计候选之外的类别,也没有做概率校准。D 的 tiger 为 0.7174,只说明它在删去 cat 后的四个选项里分数最高;不能推出照片有 71.74% 的概率是一只老虎。图搜文是固定图片对多条文字排序,文搜图是固定文字对多张图片排序;两个方向使用同一相似度矩阵的不同维度,都不生成一个新句子。caption 需要逐词条件分布和解码器,详见 LAB 13。 [此段独立阅读](/course/notebook-blocks/e8d09daa81304bbebf6af1b6be1d73f0.html)展开完整实现代码 ``` # 可读的核心路径。默认不再重算;缓存已经由完全相同路径生成。 # 将 RUN_FRESH_CLIP 改为 True 前,按说明准备依赖和固定权重。 RUN_FRESH_CLIP=False if RUN_FRESH_CLIP: import subprocess subprocess.run([sys.executable,str(RETRIEVAL_ROOT/'fetch_clip.py')],check=True) subprocess.run([sys.executable,str(RETRIEVAL_ROOT/'run_clip_retrieval.py'),'--encode'],check=True) else: saved=json.loads((RETRIEVAL_ROOT/'cache/clip-results.json').read_text()) for arm,r in saved['arms'].items(): print(arm,list(zip(r['candidate_names'],np.round(r['cosine'],4),np.round(r['softmax'],4)))) print('保存结果的来源:',p['execution'],p['generated_at_utc']) ``` [此段独立阅读](/course/notebook-blocks/64ada4187bce4109b4173d43c262ff0c.html) ### 同一对象只交换左右,真实 CLIP 是否跟着关系改变? 2026-09-22 的关系实验先把同一猫照片和同一宇航员照片各裁成 224×224 方块,放入固定的 448×448 灰色画布。第二张只交换两个方块的横向位置,灰背景、像素内容、尺寸和两条文字都固定。它是明确的照片拼贴教学构造,来源分别是 Stefan van der Walt 的 CC0 猫图与 NASA 公共领域照片,不声称是自然拍摄场景。 候选 0:`a cat on the left and an astronaut on the right.` 候选 1:`an astronaut on the left and a cat on the right.` | 实际输入 | 候选 0 cosine | 候选 1 cosine | 预测 | 判断 | | --- | --- | --- | --- | --- | | 猫左/宇航员右 | 0.378499 | 0.381837 | 1 | 错 | | 宇航员左/猫右 | 0.356597 | 0.363012 | 1 | 对 | ![相同照片方块的左右交换及真实CLIP分数](/assets/display/63d21d5ec6a87d5ee56b04b2.webp) 分数本身确实随图改变,但两次排序都偏好候选 1。模型能抓住对象共现,不保证正确绑定“谁在左边”。这不是只看生成的句子判断成败,而是用已知坐标定义真值,再检验排序。两例能构成一个明确反例,不能估计关系理解的总体水平。 原四幅持球图也在 2026-09-22 实际编码,分数如下;前文“关系规则”的表仍是规则输出,不能用这个新表倒改其身份。四幅旧插图存在背景和姿态差异,所以 3/4 的局部正确率不是严格单变量因果结论。 | 条件 | 红衣持球文句 A | 蓝衣持球文句 B | CLIP 选择 | 定义真值 | | --- | --- | --- | --- | --- | | 1 | 0.292193 | 0.292328 | B | A | | 2 | 0.292411 | 0.292754 | B | B | | 3 | 0.316829 | 0.317146 | B | B | | 4 | 0.308224 | 0.307590 | A | A | 这里所有差距都很小。只看一张正确图片或只看最高候选名会掩盖这一点;扩充数据前不要把这四个结果称为稳定的关系能力。 **与当前课堂四格的区别:**上表属于已保存的旧四幅输入。v18 的 P61–62 后来改用同源颜色编辑和确定性镜像,不能直接把此处旧分数当作当前四格的模型输出。当前网页四格用于两条规则的预测比较;模型证据请依据本节保存的图片与输入哈希。[当前四格的具体变化](/course/blocks/relation-four-condition-table.html)。 [此段独立阅读](/course/notebook-blocks/fe69124edb824641b98df3d509006d26.html)展开完整实现代码 ``` # 重做缓存后的关系排序;可改排序/显示,不能改真值去迎合模型。 cos=a['relation_images']@a['relation_text'].T print('关系 cosine:',cos) print('预测:',cos.argmax(1),'真值:',[0,1]) assert cos.argmax(1).tolist()==[1,1] # 当前已核验缓存的实际反例 for r in manifest['relation']: from IPython.display import display from PIL import Image display(Image.open(RETRIEVAL_ROOT/r['path'])) ``` [此段独立阅读](/course/notebook-blocks/880250bb392141d3b60ed38535b8d073.html) ### 下一步:同一批真实向量怎样变成检索系统? [LAB 12 · CLIP延伸:缓存与检索(近似索引选读)](https://deepnote.com/workspace/ise3309jingao-5ba66b0a-4283-4f5a-ae06-3f68d3201e99/project/38755e4e-23f6-4f5f-8c68-8a2e4114b1c1/notebook/921eeaf784284ce19568c6ca7619b55a?utm_source=openai&utm_medium=mcp&utm_campaign=openaimcp&utm_content=921eeaf784284ce19568c6ca7619b55a&utm_term=get_notebook) 使用本包的 24×512 图片缓存与 12×512 查询缓存。那一册把“模型有分数”继续拆成三个问题:相关样本是否排在前面,近似索引漏掉了哪些 exact 邻居,以及减少向量运算是否真的减少了这个小系统的实测延迟。 这里保存的 2026-09-22 运行地点是本地 Linux CPU,Python 3.12.14、Torch 2.14.0+cpu、Transformers 4.57.1;未将这次运行标成 Deepnote 云端执行。`legacy-audit.json`、`cache/provenance.json` 与 `verified/execution.json` 分别记录旧源核验、当时完成的真实编码与完整脚本身份。 [此段独立阅读](/course/notebook-blocks/1cff4428a5f343258064d92cb3ab303d.html)