Lecture 03 学习讲义
正文
工具
按问题查找
区域、像素与实例怎样直接学习一张像素类别地图?

LAB 11 · 从 RGB 到像素:读出、FCN 与边界

按问题继续

前景/背景/mIoU口径、RGB到mask训练、FCN与1×1读出及边界

对应课堂(6 页)

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

一张框正确的图,仍未说明椭圆边缘和两物体间的背景缝。本册对应 P36–38,训练 RGB→三类像素输出,并比较逐像素线性读出和小型 FCN;它没有训练实例身份。

先读 两种模型的已有对照:RGB 基线在原合成分布满分,模糊后 FCN 更稳健。再追踪输入、目标、粗细分支和混淆矩阵。前面的已知 mask 降采样只是采样热身,其 指标算例 与看图预测须分别解释。

产出一段具体判断:RGB 颜色何时已经够用,什么干预才显示邻域的作用?需要公式查 CLS 08;需要采样短练习去 LAB 03。复跑按脚本创建/追加的顺序执行,另存 lab11-results-rerun;也可只读已有结果完成比较。

此段独立阅读
区域、像素与实例怎样直接学习一张像素类别地图?

先预测采样结果,再判断指标名称

8×8 真值有 16 个前景、48 个背景。每个 2×2 小块求平均得到 4×4 覆盖率;最近邻扩大回 8×8,再以 ≥.5 二值化,会把半覆盖小块全部判前景,因此预测前景有 24 个像素。前景交集 16、并集 24,IoUfg=2/3IoU_{fg}=2/3;背景交集 40、并集 48,IoUbg=5/6IoU_{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 执行同一个随机选择。

此段独立阅读
展开完整实现代码
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)
此段独立阅读
展开完整实现代码
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')
此段独立阅读
区域、像素与实例怎样直接学习一张像素类别地图?

三类逐像素预测的可执行定义

生成图像为 [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 个忽略像素。忽略区仍然会产生模型输出,但不能据此判断对错。

以下代码格依次写出同一个自包含脚本。数据生成和度量函数在本册内完整给出,不依赖另一册先执行。

此段独立阅读
展开完整实现代码
%%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<x2)&(yy>=y1)&(yy<y2)
            if label==2: region &= ((xx+.5-(x1+x2)/2)/((x2-x1)/2))**2+((yy+.5-(y1+y2)/2)/((y2-y1)/2))**2<=1
            image[:,region]*=.2
            image[0 if label==1 else 2,region]=color
            image[1,region]=.15+color*.12
            mask[region]=label
            boxes.append([x1/size,y1/size,x2/size,y2/size]); labels.append(label)
        xs.append(image); masks.append(mask)
        ys.append({'boxes':torch.tensor(boxes,dtype=torch.float32).reshape(-1,4),'labels':torch.tensor(labels,dtype=torch.long),'sample_id':f'{seed}:{index}'})
    return torch.from_numpy(np.stack(xs)),ys,torch.from_numpy(np.stack(masks))

def nms(boxes,scores,threshold=.35):
    order=torch.argsort(scores,descending=True,stable=True); keep=[]
    while len(order):
        i=order[0]; keep.append(i.item()); order=order[1:]
        if len(order): order=order[iou_matrix(boxes[i:i+1],boxes[order])[0]<=threshold]
    return torch.tensor(keep,dtype=torch.long)

def average_precision(predictions,targets,iou_threshold=.5,score_threshold=0):
    """Per-class global score order, max-IoU GT then unused check; all-points AP."""
    result={}; all_tp=all_fp=all_gt=0
    for cls in [1,2]:
        gt={i:t['boxes'][t['labels']==cls] for i,t in enumerate(targets)}
        used={i:set() for i in gt}; total=sum(len(v) for v in gt.values())
        rows=[]
        for i,p in enumerate(predictions):
            for j in range(len(p['scores'])):
                if int(p['labels'][j])==cls and float(p['scores'][j])>=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

此段独立阅读
区域、像素与实例怎样直接学习一张像素类别地图?

热身作为可回归检查

binary_metrics 分别建立背景、前景的交集与并集,明确平均的类别集合。代码断言最近邻的前景 2/3、背景 5/6、mIoU=.75 和 8 个错误像素。若后续修改插值坐标或阈值,这些测试会直接提醒:你已经改变了原先的实验条件。

此段独立阅读
区域、像素与实例怎样直接学习一张像素类别地图?

为什么同时训练 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/转置卷积实现的逐行复制。

损失 −∑u∈validlog⁡softmax(zu)yu/∣valid∣-\sum_{u\in valid}\log softmax(z_u)_{y_u}/|valid| 对所有有效像素平均。没有任何一项要求不同物体的实例 ID,因此相邻的两个蓝椭圆都应是类别 2;但椭圆间属于背景的窄缝仍必须保留。

此段独立阅读
展开完整实现代码
%%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

此段独立阅读
区域、像素与实例怎样直接学习一张像素类别地图?

参数更新和三个单条件比较

像素读出训练 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 不变。前两者分别检查重采样方式和尺度,第三者检查边缘观测变弱。像素读出也参加尺度与模糊对照,避免把一个完美基准遗漏在困难条件之外。

此段独立阅读
展开完整实现代码
%%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)

此段独立阅读
展开完整实现代码
%%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)
此段独立阅读
区域、像素与实例怎样直接学习一张像素类别地图?

运行:模型输出必须和输入、真值一起看

下格在新 Python 进程完整运行。每张结果图按“RGB / target / prediction / 有效像素错误”排列,target 中灰色是 ignore,不是模型应该学出的类别。脚本还检查:任意修改 ignore 区预测不改变指标;同步翻转两次恢复输入和标签。

此段独立阅读
展开完整实现代码
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))
此段独立阅读
区域、像素与实例怎样直接学习一张像素类别地图?

已完成结果:复杂模型没有被安排成赢家

本地 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 才表现出比单像素读出更强的耐受。这不能直接推广到真实语义分割,真实同类物体的颜色并不恒定。

采样热身

保留集逐像素预测

错误最多的图 89 有 25 个有效像素错,图 125 有 18 个,图 56 有 16 个,图 92 有 13 个。图 89/125 的两个蓝椭圆间出现背景窄缝被填平的错误:mask 所需的边界证据可能在低分辨率表示中混合,即使细分支能改善,也不保证完全恢复。此处蓝色连通不等于实例计数正确,本册没有训练实例分割。

错误像素与窄缝

尺寸 ×.45 后像素准确率仍有 .99816,但蓝椭圆 IoU 只有 .85763,红矩形 .97613,背景 .99812。小对象在整图像素数中占比太低,仅报告准确率会掩盖其损失。最近邻对照的蓝类 IoU 进一步降到 .80928,而背景 .98558,展示了不同类别边界受采样影响不同。

小对象条件

下一次只把训练增强加入模糊,保持场景、epoch 和网络不变,再看“干净图/模糊图”两列是否同时改善。若想检验颜色捷径,应重新定义颜色与类别的生成关系并同时调整训练与测试协议;不能把标签含义改变的分布转移误说成边界采样问题。

此段独立阅读
区域、像素与实例怎样直接学习一张像素类别地图?

来源与结构输出的连接

2026-09-22 实时核验 D2L §14.11 Fully Convolutional Networks,采用全卷积逐像素读出、上采样和双线性坐标的解释;本实现使用可学习粗/细分支加双线性插值,没有预训练骨干,也没有宣称复现其 VOC 实验。ignore、三类混淆矩阵、两个颜色场景和条件对照是本课程的独立教学构造。

LAB10 中检测框正确只说明包围区域满足评价阈值。LAB11 中椭圆边缘、相邻对象间的背景缝、未标注像素仍是不同问题:输出粒度决定什么算错误,采样与任务头决定哪些空间信息能留下。返回网页的空间主线时,可以用本册三张同源输入/目标/错误图说明这一步转折,而不只说“语义分割更精细”。

此段独立阅读