# LAB 11 · 从 RGB 到像素:读出、FCN 与边界 按问题继续 前景/背景/mIoU口径、RGB到mask训练、FCN与1×1读出及边界 对应课堂(6 页) 可先阅读已有结果,再决定是否运行。重算时请核对各节记录的数据、参数、环境与日期。 **学习路径:** [像素、边界与对象身份](/course/study/pixels-instances.html) 一张框正确的图,仍未说明椭圆边缘和两物体间的背景缝。本册对应 [P36–38](/course/pages/l3-r2-p36.html),训练 RGB→三类像素输出,并比较逐像素线性读出和小型 FCN;它没有训练实例身份。 先读 [两种模型的已有对照](/course/blocks/pixel-model-positive-result.html):RGB 基线在原合成分布满分,模糊后 FCN 更稳健。再追踪输入、目标、粗细分支和混淆矩阵。前面的已知 mask 降采样只是采样热身,其 [指标算例](/course/blocks/mask-iou-worked-example.html) 与看图预测须分别解释。 产出一段具体判断:RGB 颜色何时已经够用,什么干预才显示邻域的作用?需要公式查 [CLS 08](https://deepnote.com/project/38755e4e-23f6-4f5f-8c68-8a2e4114b1c1/notebook/2d81e2a1936043349eacb71c53c0defa);需要采样短练习去 [LAB 03](https://deepnote.com/project/38755e4e-23f6-4f5f-8c68-8a2e4114b1c1/notebook/49a2c030eafa4e1db6fc4f1a08c3810a)。复跑按脚本创建/追加的顺序执行,另存 `lab11-results-rerun`;也可只读已有结果完成比较。 [此段独立阅读](/course/notebook-blocks/f47f5c4b80a34f958790cac821d40ec9.html) ## 先预测采样结果,再判断指标名称 8×8 真值有 16 个前景、48 个背景。每个 2×2 小块求平均得到 4×4 覆盖率;最近邻扩大回 8×8,再以 ≥.5 二值化,会把半覆盖小块全部判前景,因此预测前景有 24 个像素。前景交集 16、并集 24,$IoU_{fg}=2/3$;背景交集 40、并集 48,$IoU_{bg}=5/6$。两类平均为 .75,8 个像素错,准确率为 .875。 双线性插值在四个邻近覆盖率之间加权,`align_corners=False` 明确规定坐标对应。它不凭空恢复原边界。本例阈值仍为 .5:双线性只有 6 个错误像素,像素准确率 .90625,却仍然得到前景 IoU=2/3;误差位置、前景集合大小变化会使不同指标给出不同读数。它的背景 IoU=.88462,两类 mIoU=.77564。不能把“更平滑”直接改写成“知道了真实轮廓”。 对 RGB 或 logits 的双线性插值可以混合实值。对类别 mask,编号 1、2 不是数轴上可平均的强度:几何变换应采用最近邻保持离散标签;ignore=255 也必须保持身份。下面的训练增强只水平翻转,对图像和 mask 执行同一个随机选择。 [此段独立阅读](/course/notebook-blocks/d1a5e426d75643fcb95866b2042b4f18.html)展开完整实现代码 ``` import os, tempfile from pathlib import Path # Isolated ephemeral workspace supports detached runs with readonly project storage. SPATIAL_RUN_DIR = Path(tempfile.mkdtemp(prefix='lecture03-')) os.chdir(SPATIAL_RUN_DIR) print('Ephemeral experiment directory:', SPATIAL_RUN_DIR) ``` [此段独立阅读](/course/notebook-blocks/2240f73632414eacbe7d39922342aee0.html)展开完整实现代码 ``` import importlib.util, subprocess, sys from pathlib import Path # The only download, if needed, is open-source Python packages, not model weights. if importlib.util.find_spec('torch') is None: subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'torch', '--index-url', 'https://download.pytorch.org/whl/cpu']) missing=[p for p in ['numpy','matplotlib'] if importlib.util.find_spec(p) is None] if missing: subprocess.check_call([sys.executable,'-m','pip','install',*missing]) import torch, numpy as np print('Python',sys.version.split()[0], 'torch',torch.__version__, 'numpy',np.__version__, 'device: CPU') ``` [此段独立阅读](/course/notebook-blocks/69c46b79dec64732aba46f41c673c245.html) ## 三类逐像素预测的可执行定义 生成图像为 `[B,3,48,48]`,真值为 `int64 [B,48,48]`,logits 为 `[B,3,48,48]`。通道 0/1/2 分别是背景/红色矩形/蓝色椭圆。384 张训练图与 128 张保留图使用原始场景种子 7101 和 7901;没有把同图的翻转分到两侧。对象数、位置、尺寸和亮度变化,含空图与多对象图。 每张 mask 的一像素边框和 `[20:23,20:23]` 区域标为 255,模拟未标注区域。255 不代表第四类;loss 用 `ignore_index=255` 排除,混淆矩阵也只统计有效像素。本次保留集有 269,696 个有效像素、25,216 个忽略像素。忽略区仍然会产生模型输出,但不能据此判断对错。 以下代码格依次写出同一个自包含脚本。数据生成和度量函数在本册内完整给出,不依赖另一册先执行。 [此段独立阅读](/course/notebook-blocks/c9ffa01c285a40059ef2e6c94b9c23c7.html)展开完整实现代码 ``` %%writefile lab11_segmentation.py """LAB11: sampling warm-up, learned pixel readout, and image-to-mask FCN. Run: python lab11_segmentation.py --out lab11-results --epochs 20 Dependencies: torch numpy matplotlib. Self-contained; CPU only. """ """Shared transparent geometry and image construction for LAB10/11; no downloads.""" import json, hashlib, platform, sys, time from pathlib import Path from datetime import datetime, timezone import numpy as np import torch from torch import nn from torch.nn import functional as F import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt from matplotlib.patches import Rectangle def seed_all(seed=7): np.random.seed(seed); torch.manual_seed(seed); torch.set_num_threads(2) def iou_matrix(a,b): if len(a)==0 or len(b)==0: return torch.zeros((len(a),len(b))) lo=torch.maximum(a[:,None,:2],b[None,:,:2]); hi=torch.minimum(a[:,None,2:],b[None,:,2:]) inter=(hi-lo).clamp(min=0).prod(-1) area_a=(a[:,2:]-a[:,:2]).clamp(min=0).prod(-1) area_b=(b[:,2:]-b[:,:2]).clamp(min=0).prod(-1) return inter/(area_a[:,None]+area_b[None,:]-inter).clamp(min=1e-8) def make_scenes(n,seed,size=48,scale=1.0): """Independent seed defines original sample, scale changes only object size. Boxes xyxy normalized; red rectangle=1, blue ellipse=2; zero to three objects. Images are rasterized from latent scene, labels are never a network input. """ rng=np.random.default_rng(seed); xs=[]; ys=[]; masks=[] yy,xx=np.mgrid[:size,:size] for index in range(n): image=rng.uniform(.035,.095,(3,size,size)).astype('float32') num=int(rng.choice([0,1,2,3],p=[.18,.30,.36,.16])) boxes=[]; labels=[]; mask=np.zeros((size,size),np.int64) original=[] for obj in range(num): for attempt in range(60): w,h=rng.integers(9,19,size=2); x=rng.integers(2,size-w-2); y=rng.integers(2,size-h-2) box=np.array([x,y,x+w,y+h],np.float32)/size if not original or iou_matrix(torch.tensor(np.array(original)),torch.tensor(box[None])).max()<.08: break original.append(box); label=int(rng.integers(1,3)); color=rng.uniform(.7,.98) cx,cy=(box[0]+box[2])/2,(box[1]+box[3])/2 sw,sh=(box[2]-box[0])*scale,(box[3]-box[1])*scale x1,y1,x2,y2=np.round(np.array([cx-sw/2,cy-sh/2,cx+sw/2,cy+sh/2])*size).astype(int) x2=max(x2,x1+2); y2=max(y2,y1+2) region=(xx>=x1)&(xx=y1)&(yy=score_threshold: rows.append((float(p['scores'][j]),i,j,p['boxes'][j])) rows.sort(key=lambda r:(-r[0],r[1],r[2])); tp=[]; fp=[] for score,i,j,b in rows: ok=False if len(gt[i]): overlaps=iou_matrix(b[None],gt[i])[0]; best=int(overlaps.argmax()) ok=float(overlaps[best])>=iou_threshold and best not in used[i] if ok:used[i].add(best) tp.append(int(ok));fp.append(int(not ok)) tp=np.cumsum(tp);fp=np.cumsum(fp) recall=tp/max(total,1); precision=tp/np.maximum(tp+fp,1) mr=np.r_[0,recall,1];mp=np.r_[0,precision,0] mp=np.maximum.accumulate(mp[::-1])[::-1];loc=np.where(mr[1:]!=mr[:-1])[0] ap=float(((mr[loc+1]-mr[loc])*mp[loc+1]).sum()) if total else None result[str(cls)]={'AP':ap,'GT':total,'TP':int(tp[-1]) if len(tp) else 0,'FP':int(fp[-1]) if len(fp) else 0} all_tp+=result[str(cls)]['TP'];all_fp+=result[str(cls)]['FP'];all_gt+=total return {'per_class':result,'mAP':float(np.mean([r['AP'] for r in result.values() if r['AP'] is not None])), 'precision':all_tp/max(all_tp+all_fp,1),'recall':all_tp/max(all_gt,1),'iou_threshold':iou_threshold,'score_threshold':score_threshold} def provenance(path,started): p=Path(path) return {'utc_started':started,'utc_completed':datetime.now(timezone.utc).isoformat(),'execution':'CPU, complete script', 'python':sys.version,'torch':torch.__version__,'numpy':np.__version__,'platform':platform.platform(),'threads':torch.get_num_threads(), 'code_sha256':hashlib.sha256(p.read_bytes()).hexdigest(), 'device':'CPU', 'source_file':p.name} def save_json(path,data):Path(path).write_text(json.dumps(data,indent=2,ensure_ascii=False)) def segmentation_metrics(pred,target): valid=target!=255; t=target[valid];p=pred[valid] cm=torch.bincount(t*3+p,minlength=9).reshape(3,3).double() intersection=cm.diag();union=cm.sum(0)+cm.sum(1)-intersection iou=intersection/union.clamp(min=1) return {'IoU_background':float(iou[0]),'IoU_red_rectangle':float(iou[1]),'IoU_blue_ellipse':float(iou[2]), 'mIoU_3_classes':float(iou[union>0].mean()),'pixel_accuracy':float(intersection.sum()/cm.sum()), 'valid_pixels':int(valid.sum()),'ignored_pixels':int((~valid).sum()),'confusion_matrix_true_rows':cm.int().tolist()} import argparse ``` [此段独立阅读](/course/notebook-blocks/1086347139a144a386633c49d3bc189a.html) ## 热身作为可回归检查 `binary_metrics` 分别建立背景、前景的交集与并集,明确平均的类别集合。代码断言最近邻的前景 2/3、背景 5/6、mIoU=.75 和 8 个错误像素。若后续修改插值坐标或阈值,这些测试会直接提醒:你已经改变了原先的实验条件。 [此段独立阅读](/course/notebook-blocks/db968106cfcc441d831de4d9217b92ba.html) ## 为什么同时训练 1×1 读出和 FCN 逐像素读出是 `Conv2d(3,3,1)`,只有 12 个参数:每个像素用共享线性变换和 softmax 分类,没有邻域、形状或对象关系。它是原正文曾承诺而旧代码未实现的可学习逻辑读出。 FCN 有 20,590 个参数。细分支在 48×48 上提取边缘;粗分支两次 stride=2 得到 12×12 特征,再用 1×1 卷积产生三通道 logits。粗 logits 双线性恢复到 48×48,与细分支 logits 相加。细分支保留空间细节,粗分支提供较大邻域;两者通过同一个像素交叉熵一起更新。这个结构借鉴 FCN 的粗细融合思想,并非 D2L 预训练 ResNet-18/转置卷积实现的逐行复制。 损失 $-\sum_{u\in valid}\log softmax(z_u)_{y_u}/|valid|$ 对所有有效像素平均。没有任何一项要求不同物体的实例 ID,因此相邻的两个蓝椭圆都应是类别 2;但椭圆间属于背景的窄缝仍必须保留。 [此段独立阅读](/course/notebook-blocks/a83492c549764a6aad90a23cfb322167.html)展开完整实现代码 ``` %%writefile -a lab11_segmentation.py def binary_metrics(pred,target): ious=[] for cls in [0,1]: inter=((pred==cls)&(target==cls)).sum().item();union=((pred==cls)|(target==cls)).sum().item() ious.append(inter/union) return {'background_IoU':ious[0],'foreground_IoU':ious[1],'mIoU_two_classes':sum(ious)/2, 'pixel_accuracy':float((pred==target).float().mean()),'error_pixels':int((pred!=target).sum())} def sampling_warmup(out): mask=torch.zeros((8,8),dtype=torch.long);mask[2:6,3:7]=1 coarse=F.avg_pool2d(mask.float()[None,None],2) nearest=F.interpolate(coarse,scale_factor=2,mode='nearest')[0,0]>=.5 bilinear=F.interpolate(coarse,scale_factor=2,mode='bilinear',align_corners=False)[0,0]>=.5 nm=binary_metrics(nearest,mask);bm=binary_metrics(bilinear,mask) assert abs(nm['foreground_IoU']-2/3)<1e-12 assert abs(nm['background_IoU']-5/6)<1e-12 assert abs(nm['mIoU_two_classes']-.75)<1e-12 and nm['error_pixels']==8 # Bilinear interpolates probabilities/logits; categorical labels use nearest. fig,axes=plt.subplots(1,4,figsize=(10,3)) for ax,data,title in zip(axes,[mask,coarse[0,0],nearest,bilinear],['Known mask','2x2 average','Nearest >=0.5','Bilinear >=0.5']): ax.imshow(data,cmap='gray',vmin=0,vmax=1,interpolation='nearest');ax.set_title(title);ax.axis('off') fig.tight_layout();fig.savefig(out/'sampling-warmup.png',dpi=150);plt.close(fig) return {'nearest':nm,'bilinear_align_corners_false':bm,'identity':'known-mask resampling only; no image model'} class PixelReadout(nn.Module): """A learned softmax regression at every RGB pixel; no spatial receptive field.""" def __init__(self):super().__init__();self.readout=nn.Conv2d(3,3,1) def forward(self,x,mode='bilinear'):return self.readout(x) class SmallFCN(nn.Module): def __init__(self): super().__init__() self.fine=nn.Sequential(nn.Conv2d(3,12,3,padding=1),nn.ReLU(),nn.Conv2d(12,12,3,padding=1),nn.ReLU()) self.coarse=nn.Sequential(nn.Conv2d(12,24,3,stride=2,padding=1),nn.ReLU(),nn.Conv2d(24,32,3,stride=2,padding=1),nn.ReLU(),nn.Conv2d(32,32,3,padding=1),nn.ReLU()) self.head=nn.Conv2d(32,3,1);self.skip=nn.Conv2d(12,3,1) def forward(self,x,mode='bilinear'): fine=self.fine(x);coarse_logits=self.head(self.coarse(fine)) kw={'align_corners':False} if mode=='bilinear' else {} return F.interpolate(coarse_logits,size=x.shape[-2:],mode=mode,**kw)+self.skip(fine) def with_ignore(masks): masks=masks.clone();masks[:,0,:]=255;masks[:,-1,:]=255;masks[:,:,0]=255;masks[:,:,-1]=255 # A fixed small region represents missing annotation, not another semantic class. masks[:,20:23,20:23]=255 return masks ``` [此段独立阅读](/course/notebook-blocks/455f430e56104e59b4a5886436462ef5.html) ## 参数更新和三个单条件比较 像素读出训练 25 epoch、Adam lr=.03;FCN 训练 20 epoch、Adam lr=.003。每个 batch 同时翻转图像与标签、前向得到 logits、计算 CE、清零梯度、反向传播、更新参数。 三个比较均使用同一训练后权重:(1) 只把粗 logits 的 bilinear 换 nearest;(2) 相同潜在场景的对象尺寸乘 .45,其余生成条件保持一致;(3) RGB 做 5×5 均值模糊,几何和 target 不变。前两者分别检查重采样方式和尺度,第三者检查边缘观测变弱。像素读出也参加尺度与模糊对照,避免把一个完美基准遗漏在困难条件之外。 [此段独立阅读](/course/notebook-blocks/6b3780f4d85d41e5b17de37044b3ddd5.html)展开完整实现代码 ``` %%writefile -a lab11_segmentation.py def train_model(model,x,target,epochs,lr): opt=torch.optim.Adam(model.parameters(),lr=lr);history=[] for epoch in range(epochs): model.train();losses=[] for ids in torch.randperm(len(x)).split(32): images=x[ids];labels=target[ids] # One geometric decision applies to both image and mask. if torch.rand(())<.5:images=images.flip(-1);labels=labels.flip(-1) logits=model(images) assert logits.shape==(len(ids),3,48,48) and labels.dtype==torch.long loss=F.cross_entropy(logits,labels,ignore_index=255) opt.zero_grad();loss.backward();opt.step();losses.append(float(loss.detach())) history.append(float(np.mean(losses))) if epoch==0 or (epoch+1)%5==0:print(type(model).__name__,epoch+1,history[-1],flush=True) return history @torch.no_grad() def predict(model,x,mode='bilinear'): model.eval();return torch.cat([model(b,mode=mode).argmax(1) for b in x.split(32)]) def plot_predictions(images,truth,pred,path,title,indices=None): if indices is None:indices=[0,1,2,3] palette=np.array([[.04,.04,.05],[.95,.22,.15],[.10,.45,1],[.55,.55,.55]]) fig,axes=plt.subplots(len(indices),4,figsize=(10,2.6*len(indices)),squeeze=False) for row,i in enumerate(indices): mask=truth[i].numpy().copy();mask[mask==255]=3 error=(pred[i]!=truth[i])&(truth[i]!=255) data=[images[i].permute(1,2,0).numpy(),palette[mask],palette[pred[i].numpy()],error.numpy()] for ax,d,name in zip(axes[row],data,['Input RGB','Target / gray=ignore','Model prediction','Error on valid pixels']): ax.imshow(d,cmap='magma',interpolation='nearest');ax.axis('off');ax.set_title(f'{name} #{i}',fontsize=9) fig.suptitle(title);fig.tight_layout();fig.savefig(path,dpi=140);plt.close(fig) ``` [此段独立阅读](/course/notebook-blocks/9ac44bfb569543debd1b305a066b77bb.html)展开完整实现代码 ``` %%writefile -a lab11_segmentation.py def run(out='lab11-results',epochs=20): started=datetime.now(timezone.utc).isoformat();start=time.time();out=Path(out);out.mkdir(exist_ok=True,parents=True);seed_all(19) warmup=sampling_warmup(out) train,_,raw_train=make_scenes(384,7101);test,_,raw_test=make_scenes(128,7901) small,_,raw_small=make_scenes(128,7901,scale=.45) target=with_ignore(raw_train);test_target=with_ignore(raw_test);small_target=with_ignore(raw_small) readout=PixelReadout();pixel_history=train_model(readout,train,target,epochs=25,lr=.03) fcn=SmallFCN();random_pred=predict(fcn,test);history=train_model(fcn,train,target,epochs=epochs,lr=.003) pred=predict(fcn,test);nearest_pred=predict(fcn,test,mode='nearest');small_pred=predict(fcn,small) # Same image/target, blur changes RGB only, preserving geometry and label. blurred=F.avg_pool2d(F.pad(test,(2,2,2,2),mode='reflect'),5,stride=1);blur_pred=predict(fcn,blurred) # ignore_index check: arbitrarily changing predictions only at ignored pixels leaves metrics identical. altered=pred.clone();altered[test_target==255]=(altered[test_target==255]+1)%3 assert segmentation_metrics(pred,test_target)==segmentation_metrics(altered,test_target) # Synchronized flip can be undone on both image and categorical target. assert torch.equal(train.flip(-1).flip(-1),train) and torch.equal(target.flip(-1).flip(-1),target) results={'provenance':provenance(__file__,started),'config':{'image_shape':[3,48,48],'logits_shape':['B',3,48,48],'target_shape':['B',48,48], 'target_dtype':'int64','classes':{'0':'background','1':'red rectangle','2':'blue ellipse'},'ignore_label':255, 'train_count':384,'heldout_count':128,'split_seeds':[7101,7901],'seed':19,'FCN_epochs':epochs,'readout_epochs':25, 'loss':'cross_entropy on valid pixels, ignore_index=255','optimizer':'Adam FCN lr=.003; pixel readout lr=.03', 'FCN_parameters':sum(p.numel() for p in fcn.parameters()),'readout_parameters':sum(p.numel() for p in readout.parameters()), 'upsampling':'12x12 coarse logits ->48x48 bilinear align_corners=False + fine skip logits','augmentation':'paired horizontal flip', 'data_identity':'generated scenes with color/shape correlated; heldout original scene seeds; no pretrained weights'}, 'sampling_warmup':warmup,'random_FCN':segmentation_metrics(random_pred,test_target), 'learned_pixel_readout':segmentation_metrics(predict(readout,test),test_target),'trained_FCN':segmentation_metrics(pred,test_target), 'condition_nearest_logits':segmentation_metrics(nearest_pred,test_target),'condition_scale_045':segmentation_metrics(small_pred,small_target), 'condition_RGB_blur5':segmentation_metrics(blur_pred,test_target),'readout_RGB_blur5':segmentation_metrics(predict(readout,blurred),test_target),'readout_scale_045':segmentation_metrics(predict(readout,small),small_target),'history_FCN':history,'history_pixel_readout':pixel_history, 'ignore_metric_invariance_test':'passed','paired_flip_involution_test':'passed','sampling_known_values_tests':'passed','seconds':time.time()-start} counts=((pred!=test_target)&(test_target!=255)).flatten(1).sum(1);worst=counts.argsort(descending=True)[:4].tolist() results['failure_examples']=[{'sample_index':i,'wrong_valid_pixels':int(counts[i])} for i in worst] torch.save({'state_dict':fcn.state_dict(),'config':results['config']},out/'fcn.pt');torch.save(readout.state_dict(),out/'pixel-readout.pt') np.savez_compressed(out/'heldout-input-target-predictions.npz',images=test.numpy(),target=test_target.numpy(),prediction=pred.numpy(), half_scale_images=small.numpy(),half_scale_target=small_target.numpy(),half_scale_prediction=small_pred.numpy(),blur_images=blurred.numpy(),blur_prediction=blur_pred.numpy()) plot_predictions(test,test_target,pred,out/'heldout-segmentation.png','FCN heldout image-to-mask predictions') plot_predictions(test,test_target,pred,out/'failure-segmentation.png','Four largest valid-pixel error counts',worst) plot_predictions(small,small_target,small_pred,out/'small-object-segmentation.png','Same latent scenes: object dimensions x0.45') plot_predictions(blurred,test_target,blur_pred,out/'blur-segmentation.png','Same geometry, RGB blurred with 5x5 average') fig,ax=plt.subplots(figsize=(7,4));ax.plot(history,label='FCN');ax.plot(pixel_history,label='pixel softmax readout');ax.set(xlabel='epoch (zero based)',ylabel='valid-pixel CE');ax.legend();fig.tight_layout();fig.savefig(out/'training-curve.png',dpi=150);plt.close(fig) save_json(out/'run.json',results);print(json.dumps({k:results[k]['mIoU_3_classes'] for k in ['random_FCN','learned_pixel_readout','trained_FCN','condition_scale_045','condition_RGB_blur5']},indent=2),flush=True) return results if __name__=='__main__': p=argparse.ArgumentParser();p.add_argument('--out',default='lab11-results');p.add_argument('--epochs',type=int,default=20);a=p.parse_args();run(a.out,a.epochs) ``` [此段独立阅读](/course/notebook-blocks/26c6c8535ba248998b17337eae768d44.html) ## 运行:模型输出必须和输入、真值一起看 下格在新 Python 进程完整运行。每张结果图按“RGB / target / prediction / 有效像素错误”排列,target 中灰色是 ignore,不是模型应该学出的类别。脚本还检查:任意修改 ignore 区预测不改变指标;同步翻转两次恢复输入和标签。 [此段独立阅读](/course/notebook-blocks/a27735d7ff6b479189f78046ff168f4a.html)展开完整实现代码 ``` import subprocess,sys,json subprocess.run([sys.executable,'lab11_segmentation.py','--out','lab11-results-rerun','--epochs','20'],check=True) R=json.loads(Path('lab11-results-rerun/run.json').read_text()) print('sampling:',R['sampling_warmup']) for key in ['random_FCN','learned_pixel_readout','trained_FCN','condition_nearest_logits','condition_scale_045','condition_RGB_blur5','readout_RGB_blur5']: r=R[key];print(key,{k:round(v,5) for k,v in r.items() if 'IoU' in k or k=='pixel_accuracy'}) print(R['provenance']) from IPython.display import Image,display for filename in ['sampling-warmup.png','heldout-segmentation.png','failure-segmentation.png','small-object-segmentation.png','blur-segmentation.png']: display(Image(filename='lab11-results-rerun/'+filename)) ``` [此段独立阅读](/course/notebook-blocks/54cfa2249229431fa7e651cfd5d0244a.html) ## 已完成结果:复杂模型没有被安排成赢家 本地 CPU 完整运行开始于 `2026-09-22T16:28:24.497499+00:00`,PyTorch `2.14.0+cpu`;源码 SHA-256 为 `ecbe38c4a53eb691b7e31be95eaa8ab3a0b93578005d2605072196cc5dc66445`。FCN 训练 CE 从 .85632 降到 .00492。下面 mIoU 均对背景、红矩形、蓝椭圆三个类别等权平均;先在整份保留集累计混淆矩阵,再计算逐类 IoU,未对每张图的 IoU 简单平均。 | 模型 / 输入条件 | 三类 mIoU | 结论能说到哪里 | | --- | --- | --- | | 随机初始化 FCN | .01470 | 尚未学到类别读出 | | 训练后 1×1 RGB 读出 | 1.00000 | 本合成集每个像素颜色已足以分类 | | 训练后 FCN,双线性 | .99041 | 学到图像到 mask,边界仍有少量错 | | 同 FCN,最近邻 logits | .91180 | 改变上采样坐标权重影响空间决策 | | 同 FCN,对象尺寸 ×.45 | .94396 | 细小蓝对象和窄缝更难保持 | | 同 FCN,RGB 5×5 模糊 | .92397 | 边缘观测变弱时性能下降 | | 同 RGB 读出,5×5 模糊 | .84996 | 无邻域基准受混合颜色更大影响 | 像素读出比 FCN 的原分布结果更好,是有意义的负结果:更复杂结构不是得分保证,数据又带有强颜色线索。在尺寸 ×.45 条件下读出仍为 1.0,说明纯颜色规则不会因为面积变小而失效;在模糊条件下,邻域 FCN 才表现出比单像素读出更强的耐受。这不能直接推广到真实语义分割,真实同类物体的颜色并不恒定。 ![采样热身](/assets/display/0eb14c8f3f86cf2d8b3c9b65.webp) ![保留集逐像素预测](/assets/display/1b11f25259c1fe3405e0b7a5.webp) 错误最多的图 89 有 25 个有效像素错,图 125 有 18 个,图 56 有 16 个,图 92 有 13 个。图 89/125 的两个蓝椭圆间出现背景窄缝被填平的错误:mask 所需的边界证据可能在低分辨率表示中混合,即使细分支能改善,也不保证完全恢复。此处蓝色连通不等于实例计数正确,本册没有训练实例分割。 ![错误像素与窄缝](/assets/display/f2028b85a049b53efbcbbb7a.webp) 尺寸 ×.45 后像素准确率仍有 .99816,但蓝椭圆 IoU 只有 .85763,红矩形 .97613,背景 .99812。小对象在整图像素数中占比太低,仅报告准确率会掩盖其损失。最近邻对照的蓝类 IoU 进一步降到 .80928,而背景 .98558,展示了不同类别边界受采样影响不同。 ![小对象条件](/assets/display/a3b94d7df0f7bd4472d4f87d.webp) 下一次只把训练增强加入模糊,保持场景、epoch 和网络不变,再看“干净图/模糊图”两列是否同时改善。若想检验颜色捷径,应重新定义颜色与类别的生成关系并同时调整训练与测试协议;不能把标签含义改变的分布转移误说成边界采样问题。 [此段独立阅读](/course/notebook-blocks/16412ad68e504e8f9b564ed686a53b4c.html) ## 来源与结构输出的连接 2026-09-22 实时核验 [D2L §14.11 Fully Convolutional Networks](https://d2l.ai/chapter_computer-vision/fcn.html),采用全卷积逐像素读出、上采样和双线性坐标的解释;本实现使用可学习粗/细分支加双线性插值,没有预训练骨干,也没有宣称复现其 VOC 实验。ignore、三类混淆矩阵、两个颜色场景和条件对照是本课程的独立教学构造。 LAB10 中检测框正确只说明包围区域满足评价阈值。LAB11 中椭圆边缘、相邻对象间的背景缝、未标注像素仍是不同问题:输出粒度决定什么算错误,采样与任务头决定哪些空间信息能留下。返回网页的空间主线时,可以用本册三张同源输入/目标/错误图说明这一步转折,而不只说“语义分割更精细”。 [此段独立阅读](/course/notebook-blocks/365db128e760487bbb6de81c4068f3a7.html)