# LAB 05 · 像素 patch 词典:拟合与重建 按问题继续 真实MNIST patch的Lloyd拟合、词典中心、保留误差与历史输出 对应课堂(2 页) 可先阅读已有结果,再决定是否运行。重算时请核对各节记录的数据、参数、环境与日期。 **学习路径:** [从遮挡重建到表示评价](/course/study/masked-reconstruction.html) **不提供数字类别,能否从局部像素学出一套压缩词典?** 先看16个7×7中心和训练距离曲线,再沿“分给最近中心→重算中心”走一轮,最后查看保留图的重建误差。训练排除近空白patch,评价包含全部patch,两项误差不能直接相减当泛化间距。 可交付的一项判断是:把词典从16个中心改成8个时,你预计哪些笔画更难保留,运行结果是否支持?只改词典大小,先保持数据、patch大小和划分。 本册是[CLS 04重建约束](https://deepnote.com/project/38755e4e-23f6-4f5f-8c68-8a2e4114b1c1/notebook/23b0affc4b824723927bd0938fd2bbda)的基础支路;[P19本页说明](/course/pages/l3-r2-p19.html)连接MAE。拟合不读取类别标签,也不等于学到数字语义。想比较可学习瓶颈与遮挡目标,再选[LAB 08](https://deepnote.com/project/38755e4e-23f6-4f5f-8c68-8a2e4114b1c1/notebook/b5ba27f2c5ba4ec0922208a7d5dc80a4)。 [此段独立阅读](/course/notebook-blocks/f5110118913442269208cf213f242d3f.html) ### 1 · 准备与固定输入 [此段独立阅读](/course/notebook-blocks/6a7a48604cfb4b66894002d4e6c25d71.html) 只读取固定640张MNIST像素及样本身份;256张用于词典 fit,256张留作重建评价。像素 /255、右移2px。源包有数字标签,但本册不读取它们。 [此段独立阅读](/course/notebook-blocks/cedabc632fde46aab93d92c28e9d9d53.html)展开完整实现代码 ``` # 当前项目中的独立材料:已存在则复用,缺失才从本讲原站准备。 from pathlib import Path import io, zipfile, urllib.request _base = Path.cwd() _folder = 'lab10-patch' _sentinel = 'assets/target.npz' _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' _payload = urllib.request.urlopen(_url, timeout=60).read() 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-patch'/'assets').is_dir():ROOT=ROOT/'lab10-patch' 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() provenance=json.loads((ROOT/'assets/provenance.json').read_text()) assert sha_file(ROOT/'assets/target.npz')==provenance['target_asset_sha256'] with np.load(ROOT/'assets/target.npz',allow_pickle=False) as z: images=z['images'];sample_ids=z['official_train_ids'];split=z['split'] # digit_labels are in the source archive but deliberately never read here. assert len(set(sample_ids))==640 train=np.flatnonzero(split==0);heldout=np.flatnonzero(split==2) assert not set(sample_ids[train])&set(sample_ids[heldout]) partition=np.random.default_rng(20260908).permutation(60000) assert np.array_equal(sample_ids,partition[12000:12640]) x=images.astype(np.float32)/255 shifted=np.zeros_like(x);shifted[:,:,:,2:]=x[:,:,:,:-2];x=shifted assert x.shape==(640,1,28,28) print('无标签 fit 输入:',len(train),'张;保留:',len(heldout),'张。没有导入 torch、加载 CNN 或读取类别标签。') ``` [此段独立阅读](/course/notebook-blocks/d9b6b1782dd5404895a80b24f6a87f64.html) **本地保存输出(非 Deepnote 云端运行)** ``` 无标签 fit 输入: 256 张;保留: 256 张。没有导入 torch、加载 CNN 或读取类别标签。 ``` [此段独立阅读](/course/notebook-blocks/a4933c58cc07483e87ea57b6088ffffe.html) ### 1 · 没有类别标签,词典到底学了什么? [此段独立阅读](/course/notebook-blocks/a846afb448004d09bf400a885833d6ea.html) 历史 LAB07 原先的两个中心来自预设分组均值;后来加入的 Lloyd 拟合确实执行了优化,但输入仍是构造的二维描述子。本节另建实验身份,改用本册**训练图像的真实 7×7 像素 patch**。每张图切成不重叠的 4×4 个 patch,保留平均亮度大于 0.01 的 patch,学习 16 个中心。训练目标是 patch 到最近中心的平方距离和。 这里没有把数字类别交给词典。它学到的是压缩这些局部像素模式的一组中心;它没有因此证明自己学到了“数字概念”。这也不是 SIFT 描述子拟合,更不是 SimCLR、MAE 或 DINO 的复现。与现代方法的联系在问题层面:学习目标可以来自数据结构,而不一定来自固定类别名称。 **预测 → 改一个条件:**先看 16 个中心,再只改 `N_WORDS` 为 8。重跑本节,对照训练距离、保留 patch 重建误差及可辨认笔画。不要同时改 patch 大小与词典大小。 [此段独立阅读](/course/notebook-blocks/b114c530db554435857d485212f2ecba.html)展开完整实现代码 ``` # Actual Lloyd updates on image patches. Labels are deliberately absent from fit(). N_WORDS=16 PATCH=7 patches=x.reshape(640,1,4,PATCH,4,PATCH).transpose(0,2,4,1,3,5).reshape(640,16,PATCH*PATCH) fit_patches=patches[train].reshape(-1,PATCH*PATCH) fit_patches=fit_patches[fit_patches.mean(1)>.01].astype(np.float64) def fit_lloyd_unlabeled(a,k,seed=20260918,max_iter=80): rng=np.random.default_rng(seed) centers=a[rng.choice(len(a),k,replace=False)].copy() trace=[] for iteration in range(max_iter): distances=((a[:,None,:]-centers[None,:,:])**2).sum(2) assignments=distances.argmin(1) new=np.stack([a[assignments==j].mean(0) if np.any(assignments==j) else centers[j] for j in range(k)]) new_distances=((a[:,None,:]-new[None,:,:])**2).sum(2) sse=float(new_distances.min(1).sum()) trace.append(sse) if np.allclose(new,centers,rtol=0,atol=1e-10): centers=new; break centers=new assert np.all(np.diff(trace)<=1e-7) return centers,trace centers,trace=fit_lloyd_unlabeled(fit_patches,N_WORDS) frozen_centers=centers.copy() test_patches=patches[heldout].reshape(-1,49) test_dist=((test_patches[:,None,:]-frozen_centers[None,:,:])**2).sum(2) test_assignment=test_dist.argmin(1) assert np.array_equal(centers,frozen_centers) kmeans_result={'identity':'L-A true MNIST pixel-patch Lloyd fit','labels_used_in_fit':False, 'patch_shape':[7,7],'n_words':N_WORDS,'train_images':len(train),'train_nonblank_patches':len(fit_patches), 'fit_sample_ids':sample_ids[train].tolist(),'evaluation_sample_ids':sample_ids[heldout].tolist(), 'train_sse_trace':trace,'heldout_mean_squared_pixel_error':float(test_dist.min(1).mean()/49), 'warning':'train excludes nearblank patches; heldout covers all patches, so these objectives are not identically weighted'} np.savez_compressed(OUT/'kmeans-centers.npz',centers=centers) (OUT/'kmeans-results.json').write_text(json.dumps(kmeans_result,indent=2)) fig=plt.figure(figsize=(12,4.4),layout='constrained') outer=fig.add_gridspec(1,2,width_ratios=[1,1.3],wspace=.12) grid=outer[0].subgridspec(int(np.ceil(N_WORDS/4)),4,wspace=.06,hspace=.12) for j,c in enumerate(centers): ax=fig.add_subplot(grid[j//4,j%4]);ax.imshow(c.reshape(7,7),cmap='gray',vmin=0,vmax=1);ax.set_title(f'word {j}',fontsize=8);ax.axis('off') ax=fig.add_subplot(outer[1]);ax.plot(np.arange(1,len(trace)+1),trace,color=BLUE,marker='.',ms=4) ax.set(xlabel='Lloyd iteration',ylabel='Training sum of squared distances',title='Real image patches; no digit labels') savefig('02-unlabeled-fit.png') print('Fitted nonblank patches:',len(fit_patches),'iterations:',len(trace)) print('Held-out pixel reconstruction MSE:',kmeans_result['heldout_mean_squared_pixel_error']) ``` [此段独立阅读](/course/notebook-blocks/e56644fbd8a5464084c3e83da669726c.html) **本地保存输出(非 Deepnote 云端运行)** ![本地保存图](/assets/display/32ef74715b4bea413d1f5b93.webp) ``` Fitted nonblank patches: 2280 iterations: 21 Held-out pixel reconstruction MSE: 0.037023347509276223 ``` [此段独立阅读](/course/notebook-blocks/3f03f0e237f04abb89e76ad9b8564a2a.html) ### 3 · 解释与下一项对照 [此段独立阅读](/course/notebook-blocks/82e1814b9b2c425e8df6cb2ed657dc67.html) 中心确实经过优化产生,评价时只分配给冻结中心。训练距离下降不证明它保留了类别信息。训练时排除近空白 patch,保留评价包含全部 patch,两项误差的权重不同,不能直接相减当泛化间距。 先记录预测,再请 AI 只把 `N_WORDS` 改成8,并检查输出中心数、形状与 fit/evaluation 分离。目标和保留集固定;不要同时改变 patch 大小。再问需要怎样的标签读出,才能检验类别信息。 [此段独立阅读](/course/notebook-blocks/79ba67e8303e4c129fed9fa65769fee6.html)展开完整实现代码 ``` # 当前单元无法确认宿主平台或是否使用全新进程;unknown 不等于 False。 verification={'identity':'v17 independent patch-fit','fresh_process':'unknown','model_loaded':False,'labels_read':False,'labels_used_in_fit':False,'dictionary_frozen_during_evaluation':bool(np.array_equal(centers,frozen_centers)),'monotonic_sse':bool(np.all(np.diff(trace)<=1e-7)),'local_run':'unknown','cloud_run':'unknown','environment':{'python':platform.python_version(),'numpy':np.__version__},'target_sha256':sha_file(ROOT/'assets/target.npz'),'heldout_mse':kmeans_result['heldout_mean_squared_pixel_error']} (OUT/'verification.json').write_text(json.dumps(verification,indent=2));print(json.dumps(verification,indent=2)) ``` [此段独立阅读](/course/notebook-blocks/52428de33b024c91b6d336766334bf2d.html) **本地保存输出(非 Deepnote 云端运行)** ``` { "identity": "v17 independent patch-fit", "fresh_process": true, "model_loaded": false, "labels_read": false, "labels_used_in_fit": false, "dictionary_frozen_during_evaluation": true, "monotonic_sse": true, "local_run": true, "cloud_run": false, "environment": { "python": "3.12.14", "numpy": "2.3.5" }, "target_sha256": "ca069f616b511b5aa9d3e9ab88b6fd12741a5b9c1b66a8befe467e44a796ddd1", "heldout_mse": 0.037023347509276223 } ``` [此段独立阅读](/course/notebook-blocks/6fe298d4e9cf48e982b845f2e5541b85.html) ## 词典实验到这里结束;其他目标按需选读 本册已经完成真实patch词典的拟合与保留重建。教师信号、坍塌与结构—目标配合是有价值的旁支,但不应在这里再开一段必读课程。 - 对“没有类别标签,谁给答案”有兴趣:读 [CLS 06 · 表示的三个检查:目标、信息与新域](https://deepnote.com/workspace/ise3309jingao-5ba66b0a-4283-4f5a-ae06-3f68d3201e99/project/38755e4e-23f6-4f5f-8c68-8a2e4114b1c1/notebook/c0645a4db6e04188ab5b119e87c53bed?utm_source=openai&utm_medium=mcp&utm_campaign=openaimcp&utm_content=c0645a4db6e04188ab5b119e87c53bed&utm_term=get_notebook) 中的固定教师与EMA教师,或直接 [教师模型提供的是哪一种答案?](/course/blocks/teacher-target-source-and-collapse.html)。 - 想从词典走向可学习非线性重建:读 [CLS 04 · 重建:约束、位置与表示评价](https://deepnote.com/workspace/ise3309jingao-5ba66b0a-4283-4f5a-ae06-3f68d3201e99/project/38755e4e-23f6-4f5f-8c68-8a2e4114b1c1/notebook/23b0affc4b824723927bd0938fd2bbda?utm_source=openai&utm_medium=mcp&utm_campaign=openaimcp&utm_content=23b0affc4b824723927bd0938fd2bbda&utm_term=get_notebook),再选 [LAB 08 · 图像重建:遮挡损失与表示评价](https://deepnote.com/workspace/ise3309jingao-5ba66b0a-4283-4f5a-ae06-3f68d3201e99/project/38755e4e-23f6-4f5f-8c68-8a2e4114b1c1/notebook/b5ba27f2c5ba4ec0922208a7d5dc80a4?utm_source=openai&utm_medium=mcp&utm_campaign=openaimcp&utm_content=b5ba27f2c5ba4ec0922208a7d5dc80a4&utm_term=get_notebook)。 DINO已归入CLS 06;ConvNeXt V2的结构—目标配合与原文Table 3保存为 [选读:结构与遮挡目标为什么要共同设计](/course/blocks/architecture-objective-co-design.html),可单独引用。本册默认阅读停在词典的训练目标、中心和保留误差,不把独立论文表格混成这次实验的结果。 [此段独立阅读](/course/notebook-blocks/c2c6ecd7ada84187bccc2b611e21dd00.html)