十二周完整教案

包含全部正文、代码与答案。代码统一以下载包为准。

L01 Python 科研脚本:从数字到实验记录

第 1 周 · 150 分钟

目标:能使用函数、字典、路径和 JSON 保存一组结果

先修:会写简单的 Python 循环;微积分和线性代数基础

组会联系:组会上每个表格都应能追溯到生成它的程序。

建议节奏:回顾 10 分钟 → 讲解与阅读 35 → 运行示例 25 → 独立练习 50 → 自检与记录 30。

把一次实验想成一个输入明确的函数:输入配置和数据,输出指标与文件。先把计算写成函数,再在主程序里读写文件,比把所有操作混在交互窗口中更容易重跑。变量名应包含单位,例如 minutes 和 seconds。

列表保存同类记录,字典保存一条记录的字段。路径用 pathlib.Path 组合,避免 Windows 反斜杠转义。JSON 保存简单数据而非模型对象;每次写文件都注明配置,不凭记忆判断结果来自哪次运行。

先预测输出再运行。运行成功只说明语法和执行路径可用;还需要 assert 验证边界。例如空列表的平均数没有定义,函数应明确报错。

mean = sum(values) / len(values)

[30,45,20,60,25] 的总和是 180,均值 36,大于均值的是 45 和 60。

资料

  • Python 官方教程:第 4 节函数;第 5 节数据结构;第 7.2 节读写文件,30 分钟。

运行命令与预期

课程根目录的 PowerShell;代码依赖练习包中的 course_lab.py。

& ..\learned-image-compression-study\.venv\Scripts\python.exe python/answers/L01.py

count=5、total=180、mean=36、above_mean=[45,60];空列表被拒绝。

独立练习

补完 summarize:返回 count、total、mean、above_mean,并写入 JSON;再读取验证。

提示 1:先用 sum 和 len 算均值,再用列表推导筛选。

提示 2:字典字段保持固定;用 json.loads 检查落盘内容与原值相等。

拓展:增加中位数,并解释偶数个数据时的计算规则。

常见错误:路径错误:先打印 resolve();不要把脚本所在目录和终端当前目录当成同一件事。

可运行参考答案

def lesson01():
    def summarize(values):
        if not values: raise ValueError('values must not be empty')
        mean=sum(values)/len(values)
        return dict(count=len(values),total=sum(values),mean=mean,above_mean=[x for x in values if x>mean])
    result=summarize([30,45,20,60,25]); assert result['mean']==36
    try: summarize([])
    except ValueError: pass
    else: raise AssertionError('empty input not rejected')
    path=emit('L01.json',result)
    assert json.loads(path.read_text(encoding='utf-8'))==result
    print(result)

相关工具实现

def emit(name, value):
    path=OUT/name
    path.write_text(json.dumps(value,ensure_ascii=False,indent=2,allow_nan=False),encoding='utf-8')
    print(path)
    return path

理解题与参考答案

1. 为什么用函数?

把输入输出固定下来,便于重用、单独测试和定位错误。

2. JSON 能保存张量吗?

不能直接保存;小张量可转列表,模型权重用框架专用保存接口。

3. 空列表怎么办?

先检查长度,抛出有意义的异常,不能悄悄返回零。

验收:能解释全部四个字段;空输入触发明确错误,JSON 往返不丢字段

未通过时:返回本课先修内容,用较小输入逐步打印中间结果。

L02 张量与图像:形状就是接口

第 1 周 · 150 分钟

目标:区分 NCHW、HWC、逐元素运算和矩阵乘法

先修:完成 L01 的自检;不熟悉时先返回上一课

组会联系:卷积、注意力和潜变量图都依赖形状约定。

建议节奏:回顾 10 分钟 → 讲解与阅读 35 → 运行示例 25 → 独立练习 50 → 自检与记录 30。

张量是带 shape、dtype、device 的多维数组。RGB 图片常以 HWC 保存,PyTorch 卷积通常接收 NCHW。转换维度顺序用 permute,增加批量维用 unsqueeze。reshape 改的是元素排列的解释,不能代替通道交换。

uint8 的 0~255 像素转换成 float32 后除以 255,才得到 0~1。分类模型可能还需要特定均值方差归一化;压缩预训练模型通常要求 0~1,不能机械照搬分类预处理。

a*b 对应逐元素乘法,a@b 对应矩阵乘法。广播从最后一维向前匹配,维度相同或其中之一为 1 才兼容。每次运算先写出预期形状,可以提前发现大多数初学错误。

(H,W,3) → permute(2,0,1) → (3,H,W) → unsqueeze(0) → (1,3,H,W)

一张 16×20 RGB 图变成 [1,3,16,20];两个样本堆叠后是 [2,3,16,20]。

资料

  • Learn the Basics:Tensors:shape、dtype、device、索引与矩阵运算,25 分钟。

运行命令与预期

课程根目录的 PowerShell;代码依赖练习包中的 course_lab.py。

& ..\learned-image-compression-study\.venv\Scripts\python.exe python/answers/L02.py

输出 [1,3,16,20],转回像素与原数组相同。

独立练习

把 HWC 数组转为 NCHW,计算每通道均值,并断言范围在 0~1。

提示 1:先转换 float32 并除以 255。

提示 2:对 N、H、W 三个轴求均值,留下 C 轴。

拓展:从 NCHW 转回图片,检查像素是否一致。

常见错误:颜色错乱通常是通道顺序错误;过暗常是重复除以 255。

可运行参考答案

def lesson02():
    hwc=torch.arange(16*20*3,dtype=torch.int64).reshape(16,20,3)%256
    x=hwc.float().permute(2,0,1).unsqueeze(0)/255
    assert x.shape==(1,3,16,20) and 0<=x.min()<=x.max()<=1
    means=x.mean(dim=(0,2,3)); print(x.shape,means)
    restored=(x.squeeze(0).permute(1,2,0)*255).round().long()
    assert torch.equal(restored,hwc)

相关工具实现

仅使用标准库或直接框架接口。

理解题与参考答案

1. reshape 能代替 permute 吗?

不能,前者不按语义交换轴,可能把相邻像素误当成通道。

2. batch 维是什么?

同时处理的样本数量,不是颜色或空间维。

3. 模型和数据 device 不同?

会出现设备不匹配,二者需要显式放到同一设备。

验收:形状与预期一致;能解释 mean(dim=(0,2,3))

未通过时:返回本课先修内容,用较小输入逐步打印中间结果。

L03 自动微分与线性回归

第 1 周 · 150 分钟

目标:用梯度把 y=wx+b 拟合到已知直线

先修:完成 L02 的自检;不熟悉时先返回上一课

组会联系:压缩模型参数也通过同样的反传和更新学习。

建议节奏:回顾 10 分钟 → 讲解与阅读 35 → 运行示例 25 → 独立练习 50 → 自检与记录 30。

自动微分记录计算图,并按链式法则求参数对损失的导数。梯度描述局部变化方向,参数沿负梯度移动可能降低损失;步长过大会越过较好位置甚至发散。

训练循环依次计算预测和损失、清空旧梯度、反向传播、更新参数。PyTorch 默认累加梯度,所以每步一般需要清空;backward 只算梯度,不会替你更新权重。

用人工生成的 y=2x+1 做检查,可以把数据不确定性排除。先验证最终误差降低,再看 w、b 是否接近 2、1;不能用真实任务上一次随机下降代替这类检查。

L = mean((wx+b-y)²);w ← w − η ∂L/∂w

x=1,w=0,b=0,y=3 时,单样本损失为 9,对 w 和 b 的梯度都是 −6。

资料

运行命令与预期

课程根目录的 PowerShell;代码依赖练习包中的 course_lab.py。

& ..\learned-image-compression-study\.venv\Scripts\python.exe python/answers/L03.py

w 接近 2、b 接近 1,最终损失远小于初始损失。

独立练习

补全四步训练循环,记录初始和最终损失,验证参数发生变化。

提示 1:先用 MSELoss。

提示 2:每次打印用 loss.item(),不要保存一长串带计算图的张量。

拓展:只改变学习率,比较 0.01、0.1 和 2.0;保留失败结果。

常见错误:没有梯度时检查 requires_grad、detach、no_grad 是否用错。

可运行参考答案

def lesson03():
    seed(); x=torch.linspace(-1,1,50).unsqueeze(1); y=2*x+1
    model=nn.Linear(1,1); opt=torch.optim.SGD(model.parameters(),lr=.1)
    before=[p.detach().clone() for p in model.parameters()]; history=[]
    for _ in range(150):
        loss=F.mse_loss(model(x),y); assert torch.isfinite(loss)
        opt.zero_grad(); loss.backward(); opt.step(); history.append(loss.item())
    assert history[-1]<history[0] and abs(model.weight.item()-2)<.02
    assert any(not torch.equal(a,b) for a,b in zip(before,model.parameters()))
    print('w,b,initial,final:',model.weight.item(),model.bias.item(),history[0],history[-1])
    emit('L03.json',history)

相关工具实现

def seed(value=42):
    torch.manual_seed(value)
    np.random.seed(value)

def emit(name, value):
    path=OUT/name
    path.write_text(json.dumps(value,ensure_ascii=False,indent=2,allow_nan=False),encoding='utf-8')
    print(path)
    return path

理解题与参考答案

1. backward 后权重变了吗?

没变,优化器 step 才更新。

2. 为什么 zero_grad?

避免上一轮梯度无意累加到本轮。

3. 学习率越大越好吗?

不是,步长过大会振荡或发散。

验收:有限损失且最终低于初始;能手算一个点的梯度

未通过时:返回本课先修内容,用较小输入逐步打印中间结果。

L04 Dataset 与 DataLoader

第 2 周 · 150 分钟

目标:能把样本组织成可训练的批次

先修:完成 L03 的自检;不熟悉时先返回上一课

组会联系:编码实验同样需要稳定的数据读取与预处理。

建议节奏:回顾 10 分钟 → 讲解与阅读 35 → 运行示例 25 → 独立练习 50 → 自检与记录 30。

Dataset 定义第 i 个样本是什么,DataLoader 决定如何组成批次、是否打乱以及并行读取。样本通常返回输入和目标;图像重建可让目标等于原图,分类则需要整数标签。

本课程先用可重复生成的横条和竖条图训练分类器,便于离线运行和理解流程。它是合成教学数据,准确率不能用于评价真实视觉能力。换到真实数据时必须重新检查标签、尺度和分布。

训练和验证使用不同随机种子生成不同样本,并检查不重叠。训练集可以 shuffle,验证集通常不需要;最后一批可能不足 batch_size,代码不要把批次大小写死。

batch 输入 [N,1,16,16];分类目标 [N],dtype=int64

10 个样本按 batch_size=4 分成 4、4、2,而不是三个相同长度的批次。

资料

运行命令与预期

课程根目录的 PowerShell;代码依赖练习包中的 course_lab.py。

& ..\learned-image-compression-study\.venv\Scripts\python.exe python/answers/L04.py

三个批次分别有 4、4、2 个样本。

独立练习

构建 TensorDataset 和 DataLoader,统计遍历样本总数并验证末批大小。

提示 1:标签转为 long。

提示 2:总数累加 len(x),不要每批固定加 batch_size。

拓展:加一个噪声强度参数,分别生成训练和验证数据。

常见错误:Windows 初学阶段 num_workers=0;多进程需要主入口保护。

可运行参考答案

def lesson04():
    x,y=stripes(10); loader=DataLoader(TensorDataset(x,y),batch_size=4,shuffle=False)
    sizes=[]
    for xb,yb in loader:
        print(xb.shape,yb.shape,yb.dtype); sizes.append(len(xb))
    assert sizes==[4,4,2] and sum(sizes)==10

相关工具实现

def stripes(n=64, noise=.15, random_seed=42):
    g=torch.Generator().manual_seed(random_seed)
    labels=torch.arange(n)%2
    x=torch.zeros(n,1,16,16)
    x[labels==0,0,::2,:]=1
    x[labels==1,0,:,::2]=1
    x=(x+noise*torch.randn(x.shape,generator=g)).clamp(0,1)
    return x,labels.long()

理解题与参考答案

1. Dataset 与 Loader 的区别?

前者定义样本,后者组织批次。

2. 为何不能先混合再随机增强后拆分?

同一原图的变体可能跨集合,造成泄漏。

3. 最后一批较小会怎样?

用实际 shape[0] 计算统计量即可。

验收:所有样本恰好被访问一次;打印前三个批次形状

未通过时:返回本课先修内容,用较小输入逐步打印中间结果。

L05 神经网络与训练循环

第 2 周 · 150 分钟

目标:训练一个能学习横竖条纹的分类器

先修:完成 L04 的自检;不熟悉时先返回上一课

组会联系:从堆叠层走向理解优化过程。

建议节奏:回顾 10 分钟 → 讲解与阅读 35 → 运行示例 25 → 独立练习 50 → 自检与记录 30。

nn.Module 组织可学习参数和前向计算。线性层后接非线性激活,才可能表达非线性关系;连续线性层若无非线性,本质仍是一个线性变换。

CrossEntropyLoss 接收未做 softmax 的 logits 和类别编号。它内部处理对数归一化;训练前多做一次 softmax 往往会改变损失的数值行为。预测类别可以直接取 logits.argmax。

先让模型在小数据上学会,再增加数据规模。每个 epoch 记录按样本加权的平均损失,而不是把大小不等的批次均值简单平均。记录随机种子与学习率,便于解释差异。

logits: [N,K];loss = CrossEntropy(logits, labels)

[2.0,−1.0] 的 argmax 是 0;logits 不要求非负,也不要求和为 1。

资料

运行命令与预期

课程根目录的 PowerShell;代码依赖练习包中的 course_lab.py。

& ..\learned-image-compression-study\.venv\Scripts\python.exe python/answers/L05.py

生成本课 outputs 文件并通过代码中的断言;具体数值随训练与硬件变化,不以固定准确率作为承诺。

独立练习

补全模型前向计算与分类损失,训练并记录每轮损失。

提示 1:Flatten 后每张图有 256 个输入值。

提示 2:输出层有两个神经元,对应两个类别。

拓展:改隐藏层宽度,记录参数量变化。

常见错误:标签若是 float 或 one-hot,先核对损失接口预期。

可运行参考答案

def lesson05():
    seed(); model=classifier(); history=fit_classifier(model)
    assert history[-1]<history[0]; print('loss:',history)
    print('parameters:',sum(p.numel() for p in model.parameters()))
    emit('L05.json',history)

相关工具实现

def seed(value=42):
    torch.manual_seed(value)
    np.random.seed(value)

def emit(name, value):
    path=OUT/name
    path.write_text(json.dumps(value,ensure_ascii=False,indent=2,allow_nan=False),encoding='utf-8')
    print(path)
    return path

def classifier(cnn=False):
    if cnn:
        return nn.Sequential(nn.Conv2d(1,4,3,padding=1),nn.ReLU(),nn.Flatten(),nn.Linear(4*256,2))
    return nn.Sequential(nn.Flatten(),nn.Linear(256,16),nn.ReLU(),nn.Linear(16,2))

def fit_classifier(model, epochs=6, lr=.08):
    x,y=stripes(); loader=DataLoader(TensorDataset(x,y),batch_size=16,shuffle=True)
    opt=torch.optim.SGD(model.parameters(),lr=lr)
    history=[]
    for _ in range(epochs):
        model.train(); total=0.
        for xb,yb in loader:
            loss=F.cross_entropy(model(xb),yb)
            assert torch.isfinite(loss)
            opt.zero_grad(); loss.backward(); opt.step()
            total+=float(loss.detach())*len(xb)
        history.append(total/len(x))
    return history

理解题与参考答案

1. 线性层为何加 ReLU?

引入非线性,使模型能表达更复杂关系。

2. 交叉熵前要 softmax 吗?

此接口不需要,应输入 logits。

3. loss 降了就代表泛化好吗?

不代表,还要看独立验证集。

验收:损失保持有限且总体下降;能解释输入输出维度

未通过时:返回本课先修内容,用较小输入逐步打印中间结果。

L06 验证、保存与重新加载

第 2 周 · 150 分钟

目标:在独立样本上评价并确认模型文件可用

先修:完成 L05 的自检;不熟悉时先返回上一课

组会联系:科研结果要能由重启后的程序重现。

建议节奏:回顾 10 分钟 → 讲解与阅读 35 → 运行示例 25 → 独立练习 50 → 自检与记录 30。

model.eval() 改变 Dropout、BatchNorm 等层的行为,no_grad() 则关闭梯度记录。它们解决不同问题;评测通常两个都需要。验证过程不能调用 optimizer.step。

保存 state_dict、模型配置和实验配置。加载时先构建相同架构,再加载参数。训练设备和读取设备可能不同,用 map_location 显式控制;不要加载不可信的任意 pickle 文件。

验证保存是否成功的可靠方法是建立新模型,重载权重,在同一输入上比较输出。文件存在并不等于内容正确。准确率用正确数量除以样本总数,避免批次大小造成偏差。

accuracy = 正确预测数量 / 总样本数量

两批分别正确 4/4 和 0/1,总准确率 80%,不是两批准确率均值 50%。

资料

  • Learn the Basics:Save and Load the Model;Optimization 中的 test_loop,30 分钟。

运行命令与预期

课程根目录的 PowerShell;代码依赖练习包中的 course_lab.py。

& ..\learned-image-compression-study\.venv\Scripts\python.exe python/answers/L06.py

生成本课 outputs 文件并通过代码中的断言;具体数值随训练与硬件变化,不以固定准确率作为承诺。

独立练习

补全评测函数和保存加载流程,断言重载前后输出相同。

提示 1:评测时累计正确数和样本数。

提示 2:保存前后都进入 eval 模式再比较。

拓展:尝试改变网络宽度再加载,解释形状不匹配错误。

常见错误:不要把测试集效果用于反复选超参数;用验证集做选择。

可运行参考答案

def lesson06():
    seed(); model=classifier(); fit_classifier(model)
    x,y=stripes(random_seed=99); scores=evaluate(model,x,y)
    path=OUT/'L06.pt'; torch.save(model.state_dict(),path)
    restored=classifier(); restored.load_state_dict(torch.load(path,map_location='cpu',weights_only=True)); restored.eval()
    with torch.inference_mode(): assert torch.allclose(model(x),restored(x))
    print(scores,'reload checked'); emit('L06.json',scores)

相关工具实现

def seed(value=42):
    torch.manual_seed(value)
    np.random.seed(value)

def emit(name, value):
    path=OUT/name
    path.write_text(json.dumps(value,ensure_ascii=False,indent=2,allow_nan=False),encoding='utf-8')
    print(path)
    return path

def stripes(n=64, noise=.15, random_seed=42):
    g=torch.Generator().manual_seed(random_seed)
    labels=torch.arange(n)%2
    x=torch.zeros(n,1,16,16)
    x[labels==0,0,::2,:]=1
    x[labels==1,0,:,::2]=1
    x=(x+noise*torch.randn(x.shape,generator=g)).clamp(0,1)
    return x,labels.long()

def classifier(cnn=False):
    if cnn:
        return nn.Sequential(nn.Conv2d(1,4,3,padding=1),nn.ReLU(),nn.Flatten(),nn.Linear(4*256,2))
    return nn.Sequential(nn.Flatten(),nn.Linear(256,16),nn.ReLU(),nn.Linear(16,2))

def fit_classifier(model, epochs=6, lr=.08):
    x,y=stripes(); loader=DataLoader(TensorDataset(x,y),batch_size=16,shuffle=True)
    opt=torch.optim.SGD(model.parameters(),lr=lr)
    history=[]
    for _ in range(epochs):
        model.train(); total=0.
        for xb,yb in loader:
            loss=F.cross_entropy(model(xb),yb)
            assert torch.isfinite(loss)
            opt.zero_grad(); loss.backward(); opt.step()
            total+=float(loss.detach())*len(xb)
        history.append(total/len(x))
    return history

def evaluate(model,x,y):
    model.eval()
    with torch.inference_mode():
        logits=model(x)
        return dict(loss=float(F.cross_entropy(logits,y)), accuracy=float((logits.argmax(1)==y).float().mean()))

理解题与参考答案

1. eval 等于不计算梯度吗?

不等于;关闭梯度需要 no_grad 或 inference_mode。

2. 保存整个模型还是权重?

本课保存 state_dict 并同时记录架构配置。

3. 如何检验重载?

新实例加载后在固定输入上用 allclose 比较输出。

验收:验证过程不修改权重;重载输出通过 allclose

未通过时:返回本课先修内容,用较小输入逐步打印中间结果。

L07 卷积的通道、填充与步幅

第 3 周 · 150 分钟

目标:先算出形状,再验证卷积输出

先修:完成 L06 的自检;不熟悉时先返回上一课

组会联系:压缩编码器通过下采样形成潜变量。

建议节奏:回顾 10 分钟 → 讲解与阅读 35 → 运行示例 25 → 独立练习 50 → 自检与记录 30。

卷积核在空间上共享参数,利用局部邻域提取特征。输入通道和输出通道有不同角色:每个输出通道通常整合所有输入通道,而不是只看一个颜色。

步幅控制采样间距,padding 控制边缘处理。输出尺寸依赖核大小、步幅、填充与 dilation;先用整数公式计算,再与运行结果对照。下采样损失空间细节,网络需要在通道表示中保留有用信息。

参数量和运算量不是一回事。同样的卷积参数放在更大的图上,参数量不变,计算量却增加。组会上比较低复杂度模型时要同时交代输入尺寸。

Hout = floor((H+2p−d(k−1)−1)/s+1);参数量 = Cout(Cin k²+1)

H=32,k=3,p=1,s=2,d=1 得到 Hout=16;3→8 通道卷积有 224 个参数。

资料

运行命令与预期

课程根目录的 PowerShell;代码依赖练习包中的 course_lab.py。

& ..\learned-image-compression-study\.venv\Scripts\python.exe python/answers/L07.py

输出尺寸分别为 16、16、15;参数公式匹配。

独立练习

实现输出尺寸计算函数,对照两层卷积实际 shape。

提示 1:先只计算一个空间维。

提示 2:用整除 // 对应 floor,考虑 dilation。

拓展:把输入改成奇数尺寸,解释输出为何取整。

常见错误:通道不匹配时查看 Conv2d 的 in_channels 与输入第二维。

可运行参考答案

def lesson07():
    def output_size(h,k,p,s,d=1): return (h+2*p-d*(k-1)-1)//s+1
    for h,k,p,s in [(32,3,1,2),(31,5,2,2),(17,3,0,1)]:
        layer=nn.Conv2d(3,8,k,stride=s,padding=p)
        y=layer(torch.zeros(1,3,h,h)); expected=output_size(h,k,p,s)
        assert y.shape[-1]==expected
        assert sum(x.numel() for x in layer.parameters())==8*(3*k*k+1)
        print(h,'->',y.shape,'parameters',8*(3*k*k+1))

相关工具实现

仅使用标准库或直接框架接口。

理解题与参考答案

1. 输出通道指什么?

不同的可学习特征图数量。

2. stride=2 一定减半吗?

要结合核与填充计算,不能只看步幅。

3. 增大图像会增加参数吗?

普通卷积不会,但乘加次数增加。

验收:至少验证三组参数;手算参数量与 numel 相同

未通过时:返回本课先修内容,用较小输入逐步打印中间结果。

L08 CNN、过拟合与实验诊断

第 3 周 · 150 分钟

目标:能区分训练失败、过拟合和分布变化

先修:完成 L07 的自检;不熟悉时先返回上一课

组会联系:先排查基础问题,再讨论新模块是否有效。

建议节奏:回顾 10 分钟 → 讲解与阅读 35 → 运行示例 25 → 独立练习 50 → 自检与记录 30。

CNN 利用空间结构,但并不会自动保证泛化。训练误差下降而验证误差上升,提示模型可能开始记住训练集特征;两个误差都高,则可能容量不足、优化困难或数据错误。

过拟合诊断需要固定验证集与连续曲线。一次随机准确率下降不足以下结论。小样本上随机标签也可能被大模型记住,因此高训练准确率不是学习到有用规律的证据。

本课用条纹合成数据练习诊断流程,先画训练和验证趋势,再只改变噪声强度。增强、权重衰减等方法应分别尝试,不一次改多个因素。

generalization gap = validation loss − training loss(仅作诊断)

训练损失 0.05、验证损失 0.8 提醒你检查过拟合,但还要检查两者预处理是否一致。

资料

运行命令与预期

课程根目录的 PowerShell;代码依赖练习包中的 course_lab.py。

& ..\learned-image-compression-study\.venv\Scripts\python.exe python/answers/L08.py

生成本课 outputs 文件并通过代码中的断言;具体数值随训练与硬件变化,不以固定准确率作为承诺。

独立练习

训练小 CNN,分别记录同分布和高噪声验证结果,解释边界。

提示 1:先保持网络与种子不变。

提示 2:用表格记录数据噪声、损失、准确率。

拓展:加入 weight_decay,并与零衰减做同配置比较。

常见错误:发现验证异常先核对 train/eval 和预处理,别立刻加层。

可运行参考答案

def lesson08():
    seed(); model=classifier(cnn=True); history=fit_classifier(model)
    clean=evaluate(model,*stripes(random_seed=98)); noisy=evaluate(model,*stripes(noise=.8,random_seed=99))
    emit('L08.json',dict(synthetic=True,train_loss=history,validation=clean,noisy_validation=noisy))
    print(clean,noisy)

相关工具实现

def seed(value=42):
    torch.manual_seed(value)
    np.random.seed(value)

def emit(name, value):
    path=OUT/name
    path.write_text(json.dumps(value,ensure_ascii=False,indent=2,allow_nan=False),encoding='utf-8')
    print(path)
    return path

def stripes(n=64, noise=.15, random_seed=42):
    g=torch.Generator().manual_seed(random_seed)
    labels=torch.arange(n)%2
    x=torch.zeros(n,1,16,16)
    x[labels==0,0,::2,:]=1
    x[labels==1,0,:,::2]=1
    x=(x+noise*torch.randn(x.shape,generator=g)).clamp(0,1)
    return x,labels.long()

def classifier(cnn=False):
    if cnn:
        return nn.Sequential(nn.Conv2d(1,4,3,padding=1),nn.ReLU(),nn.Flatten(),nn.Linear(4*256,2))
    return nn.Sequential(nn.Flatten(),nn.Linear(256,16),nn.ReLU(),nn.Linear(16,2))

def fit_classifier(model, epochs=6, lr=.08):
    x,y=stripes(); loader=DataLoader(TensorDataset(x,y),batch_size=16,shuffle=True)
    opt=torch.optim.SGD(model.parameters(),lr=lr)
    history=[]
    for _ in range(epochs):
        model.train(); total=0.
        for xb,yb in loader:
            loss=F.cross_entropy(model(xb),yb)
            assert torch.isfinite(loss)
            opt.zero_grad(); loss.backward(); opt.step()
            total+=float(loss.detach())*len(xb)
        history.append(total/len(x))
    return history

def evaluate(model,x,y):
    model.eval()
    with torch.inference_mode():
        logits=model(x)
        return dict(loss=float(F.cross_entropy(logits,y)), accuracy=float((logits.argmax(1)==y).float().mean()))

理解题与参考答案

1. 两个 loss 都高?

先查数据、标签、学习率和容量,不能直接说过拟合。

2. 测试集能用来调参吗?

不能反复根据测试结果选择配置。

3. 增强越强越好吗?

不一定,可能破坏标签或改变目标分布。

验收:训练和验证分开记录;不把合成数据效果写成真实任务性能

未通过时:返回本课先修内容,用较小输入逐步打印中间结果。

L09 JPEG、MSE、PSNR 与 bpp

第 3 周 · 150 分钟

目标:亲手测量图像文件大小和重建误差

先修:完成 L08 的自检;不熟悉时先返回上一课

组会联系:RD 曲线是压缩组会最常见的共同语言。

建议节奏:回顾 10 分钟 → 讲解与阅读 35 → 运行示例 25 → 独立练习 50 → 自检与记录 30。

JPEG 是有损编码。质量参数调节编码行为,不是“保存百分之多少信息”。同一质量参数处理不同图片,得到的字节数不同,因此比较应该看实际码率和失真。

MSE 对所有像素通道的平方误差取均值。PSNR 从 MSE 和像素峰值计算,峰值取 1 还是 255 必须与输入范围一致。MSE=0 时 PSNR 为正无穷,不应除零崩溃。

bpp 用总比特除以原图宽高,不再除以颜色通道数。本课 JPEG 测量包含文件头的完整字节数;重建图保存成 PNG 的大小不能代表原编码器的码率。

bpp=8×bytes/(H×W);PSNR=10 log10(MAX²/MSE)

100×100 图像压缩文件 5000 字节,bpp=4;0~1 图像 MSE=0.01,PSNR=20 dB。

资料

运行命令与预期

课程根目录的 PowerShell;代码依赖练习包中的 course_lab.py。

& ..\learned-image-compression-study\.venv\Scripts\python.exe python/answers/L09.py

CSV 含三个档位;实际 JPEG 字节数、bpp、PSNR 均可查看。

独立练习

对同一图片用 20、50、85 三档 JPEG,记录文件字节、bpp 和 PSNR。

提示 1:用 BytesIO 得到完整文件长度。

提示 2:把解码图转 float 后再相减,避免 uint8 溢出。

拓展:保持质量不变,比较平滑图和纹理图的码率。

常见错误:彩色图统一转 RGB;预处理与原图尺寸必须一致。

可运行参考答案

def lesson09():
    args=cli(); img=Image.open(args.image).convert('RGB') if args.image else to_image(picture(96))
    name=Path(args.image).name if args.image else 'synthetic-demo'
    rows=[jpeg_row(img,q,name) for q in [20,50,85]]
    for r in rows: print(r['quality'],r['file_bpp'],r['psnr'])
    assert psnr(pil_tensor(img),pil_tensor(img))==math.inf
    csv_write('L09_jpeg.csv',rows)
    to_image(picture(96)).save(OUT/'synthetic-demo.png')

相关工具实现

def picture(size=32, random_seed=42):
    # Synthetic calibration image, not a natural-image benchmark.
    g=torch.Generator().manual_seed(random_seed)
    v=torch.linspace(0,1,size)
    yy,xx=torch.meshgrid(v,v,indexing='ij')
    rgb=torch.stack([xx,yy,.5+.4*torch.sin(xx*12)*torch.cos(yy*9)])
    return (rgb+.02*torch.randn(rgb.shape,generator=g)).clamp(0,1).unsqueeze(0)

def pil_tensor(img):
    a=np.asarray(img.convert('RGB'),dtype=np.float32)/255
    return torch.from_numpy(a.copy()).permute(2,0,1).unsqueeze(0)

def to_image(x):
    a=x.detach().cpu().squeeze(0).clamp(0,1).permute(1,2,0).numpy()
    return Image.fromarray(np.round(a*255).astype(np.uint8))

def psnr(x,y):
    mse=float(torch.mean((x.double()-y.double())**2))
    return math.inf if mse==0 else -10*math.log10(mse)

def csv_write(name, rows):
    if not rows: raise ValueError('empty results')
    path=OUT/name
    with path.open('w',newline='',encoding='utf-8') as f:
        w=csv.DictWriter(f,fieldnames=list(rows[0])); w.writeheader(); w.writerows(rows)
    print(path)
    return path

def jpeg_row(img,quality,name='synthetic'):
    buf=io.BytesIO(); img.save(buf,format='JPEG',quality=quality,subsampling=0)
    reconstructed=Image.open(io.BytesIO(buf.getvalue())).convert('RGB')
    x,y=pil_tensor(img),pil_tensor(reconstructed)
    return dict(image=name,model='jpeg',quality=quality,width=img.width,height=img.height,
      file_bytes=len(buf.getvalue()),payload_bytes='',file_bpp=bpp(len(buf.getvalue()),img.height,img.width),
      payload_bpp='',estimated_bpp='',psnr=psnr(x,y),side_bytes='',encode_seconds='',decode_seconds='')

def cli():
    p=argparse.ArgumentParser()
    p.add_argument('--image'); p.add_argument('--images'); p.add_argument('--results'); p.add_argument('--bitstream')
    p.add_argument('--project',default=str(Path(__file__).resolve().parents[2]/'learned-image-compression-study'))
    p.add_argument('--hpcm',default=str(Path(__file__).resolve().parents[2]/'LIC-HPCM'))
    p.add_argument('--quality',type=int,default=1); p.add_argument('--model',choices=['factorized','hyperprior'],default='factorized')
    p.add_argument('--self-test',action='store_true'); p.add_argument('--cuda',action='store_true')
    return p.parse_args()

理解题与参考答案

1. PSNR 越高一定更好看吗?

不一定,它衡量像素误差而非完整感知质量。

2. bpp 除以 3 吗?

不除,以空间像素数为分母。

3. 为什么需重新读取 JPEG?

误差应计算编码再解码后的图像,不是编码前数据。

验收:三档结果保存为 CSV;MSE=0 正确处理

未通过时:返回本课先修内容,用较小输入逐步打印中间结果。

L10 编码器与解码器:从分类到重建

第 4 周 · 150 分钟

目标:解释潜变量尺寸与信息瓶颈

先修:完成 L09 的自检;不熟悉时先返回上一课

组会联系:g_a 和 g_s 就是学习式压缩的分析与合成变换。

建议节奏:回顾 10 分钟 → 讲解与阅读 35 → 运行示例 25 → 独立练习 50 → 自检与记录 30。

分类网络输出类别,重建网络输出与输入同尺寸的图像。编码器把 x 映射到 y,解码器由 y 生成 x_hat。中间表示较小可以形成瓶颈,但把浮点 y 存下来并不自动构成有效压缩。

空间尺寸减小的同时通道可能增加。例如输入 3×32×32 经过两次 stride=2 后成为 8×8×8,元素数量减少,但每个浮点元素通常仍占四字节。量化、概率建模和熵编码才能进一步转成码流。

先追踪每层形状,不急着训练。用上采样加卷积构造简单解码器,保证输出尺寸与目标相同。奇数尺寸要明确 padding 和最终裁剪规则。

y=g_a(x);x_hat=g_s(y)

3×32×32 是 3072 个值,8×8×8 是 512 个值;元素压缩比不等于文件压缩比。

资料

运行命令与预期

课程根目录的 PowerShell;代码依赖练习包中的 course_lab.py。

& ..\learned-image-compression-study\.venv\Scripts\python.exe python/answers/L10.py

生成本课 outputs 文件并通过代码中的断言;具体数值随训练与硬件变化,不以固定准确率作为承诺。

独立练习

实现两级编码器与解码器,打印各层形状和潜变量元素数。

提示 1:编码卷积步幅为 2。

提示 2:解码用 interpolate 配合 Conv2d。

拓展:比较潜变量通道 4、8、16 的参数量。

常见错误:输出层可用 sigmoid 限制 0~1;别把分类 softmax 用于 RGB 重建。

可运行参考答案

def lesson10():
    seed(); model=Autoencoder(); x=picture()
    for layer in model.encoder:
        x=layer(x); print(type(layer).__name__,tuple(x.shape))
    assert x.shape==(1,8,8,8)
    out=model(picture()); assert out.shape==picture().shape
    print('input/latent values:',picture().numel(),x.numel())

相关工具实现

def seed(value=42):
    torch.manual_seed(value)
    np.random.seed(value)

def picture(size=32, random_seed=42):
    # Synthetic calibration image, not a natural-image benchmark.
    g=torch.Generator().manual_seed(random_seed)
    v=torch.linspace(0,1,size)
    yy,xx=torch.meshgrid(v,v,indexing='ij')
    rgb=torch.stack([xx,yy,.5+.4*torch.sin(xx*12)*torch.cos(yy*9)])
    return (rgb+.02*torch.randn(rgb.shape,generator=g)).clamp(0,1).unsqueeze(0)

class Autoencoder(nn.Module):
    def __init__(self, channels=8):
        super().__init__()
        self.encoder=nn.Sequential(nn.Conv2d(3,8,3,2,1),nn.ReLU(),nn.Conv2d(8,channels,3,2,1))
        self.decoder=nn.Sequential(nn.Conv2d(channels,8,3,padding=1),nn.ReLU(),nn.Conv2d(8,3,3,padding=1),nn.Sigmoid())
    def forward(self,x):
        y=self.encoder(x)
        return self.decoder(F.interpolate(y,size=x.shape[-2:],mode='bilinear',align_corners=False))

理解题与参考答案

1. 小潜变量等于小码流?

不等于,还取决于精度、分布和编码开销。

2. 解码器知道原图吗?

实际解码时只能使用码流和双方已知的信息。

3. 为什么检查输出尺寸?

损失需要对应像素,隐式广播会掩盖错误。

验收:重建 shape 与输入一致;区分元素数与字节数

未通过时:返回本课先修内容,用较小输入逐步打印中间结果。

L11 训练卷积自编码器

第 4 周 · 150 分钟

目标:把一张图重建出来并观察误差位置

先修:完成 L10 的自检;不熟悉时先返回上一课

组会联系:学习压缩之前先确认重建网络能够学习。

建议节奏:回顾 10 分钟 → 讲解与阅读 35 → 运行示例 25 → 独立练习 50 → 自检与记录 30。

自编码器以输入本身为目标,训练时最小化重建误差。它不需要类别标签,但仍需要独立测试集判断泛化。单图过拟合是调试工具,可快速检查网络、梯度和数据范围。

训练前后保存重建图和误差图。误差图放大后有助于定位边缘、纹理等难点,但必须说明放大倍率;不能把不同倍率的可视化直接当作误差大小比较。

纯 MSE 训练倾向于像素平均意义上的正确。模型容量或训练不足时会模糊,不能仅看到模糊就归因于压缩率太低:本课还没有率项和真实码流。

L_recon = mean((x_hat−x)²)

重建图与原图同为 [1,3,32,32];所有像素输出 0.5 也能得到有限损失,但不说明模型学会了结构。

资料

  • Learn the Basics:Optimization:把分类损失替换为重建 MSE 的流程,25 分钟。

运行命令与预期

课程根目录的 PowerShell;代码依赖练习包中的 course_lab.py。

& ..\learned-image-compression-study\.venv\Scripts\python.exe python/answers/L11.py

生成本课 outputs 文件并通过代码中的断言;具体数值随训练与硬件变化,不以固定准确率作为承诺。

独立练习

训练自编码器,保存初始与最终损失、原图、重建图和绝对误差图。

提示 1:先固定种子与单张小图。

提示 2:每次迭代重新前向计算,保存图像时 detach。

拓展:换一张未见图,比较训练图与测试图误差。

常见错误:训练损失不降时检查优化器是否包含模型参数。

可运行参考答案

def lesson11():
    model,record=fit_autoencoder(); x=picture()
    with torch.inference_mode(): y=model(x)
    to_image(x).save(OUT/'L11_original.png'); to_image(y).save(OUT/'L11_reconstruction.png')
    to_image((x-y).abs()*4).save(OUT/'L11_error_x4.png')
    torch.save(model.state_dict(),OUT/'L11.pt'); emit('L11.json',record); print(record)

相关工具实现

def emit(name, value):
    path=OUT/name
    path.write_text(json.dumps(value,ensure_ascii=False,indent=2,allow_nan=False),encoding='utf-8')
    print(path)
    return path

def picture(size=32, random_seed=42):
    # Synthetic calibration image, not a natural-image benchmark.
    g=torch.Generator().manual_seed(random_seed)
    v=torch.linspace(0,1,size)
    yy,xx=torch.meshgrid(v,v,indexing='ij')
    rgb=torch.stack([xx,yy,.5+.4*torch.sin(xx*12)*torch.cos(yy*9)])
    return (rgb+.02*torch.randn(rgb.shape,generator=g)).clamp(0,1).unsqueeze(0)

def to_image(x):
    a=x.detach().cpu().squeeze(0).clamp(0,1).permute(1,2,0).numpy()
    return Image.fromarray(np.round(a*255).astype(np.uint8))

def fit_autoencoder(channels=8, steps=60, random_seed=42):
    seed(random_seed); model=Autoencoder(channels); x=picture()
    opt=torch.optim.Adam(model.parameters(),lr=.01)
    initial=float(F.mse_loss(model(x),x).detach())
    old=[p.detach().clone() for p in model.parameters()]
    for _ in range(steps):
        loss=F.mse_loss(model(x),x); assert torch.isfinite(loss)
        opt.zero_grad(); loss.backward(); opt.step()
    model.eval()
    with torch.inference_mode(): out=model(x); final=float(F.mse_loss(out,x))
    assert final<initial, (initial,final)
    assert any(not torch.equal(p,q) for p,q in zip(old,model.parameters()))
    return model,dict(initial_mse=initial,final_mse=final,steps=steps,seed=random_seed,synthetic=True,train_test_overlap=True)

理解题与参考答案

1. 为何先过拟合一张图?

验证优化链路是否具备学习能力。

2. 这已是完整压缩吗?

不是,尚无量化、率项和码流。

3. 误差图为何需要统一尺度?

否则不同显示倍率会造成视觉误判。

验收:最终损失低于初始;能指出重建最差的区域

未通过时:返回本课先修内容,用较小输入逐步打印中间结果。

L12 潜变量量化:连续表示变成符号

第 4 周 · 150 分钟

目标:理解舍入误差、量化步长和训练近似

先修:完成 L11 的自检;不熟悉时先返回上一课

组会联系:组会中的量化首先可能指编码潜变量,而不是 INT8 部署。

建议节奏:回顾 10 分钟 → 讲解与阅读 35 → 运行示例 25 → 独立练习 50 → 自检与记录 30。

浮点潜变量取值连续,需要离散化才方便无歧义编码。均匀量化把数映射到等间隔格点,解码端由整数索引和步长恢复近似值。步长越大通常误差越大,可用符号数往往减少。

round 几乎处处导数为零,直接放进训练链路会阻碍编码器学习。可用加均匀噪声或直通估计构造训练近似,但这些并不是舍入函数的真实导数。评测时必须回到实际量化规则。

PyTorch round 对恰好半整数采用 ties-to-even。跨语言实现应明确舍入规则;示例中普通均匀量化本身不产生码流,要继续接概率模型和熵编码。

q=round(y/Δ);y_hat=qΔ;无截断时 |y−y_hat|≤Δ/2

y=0.37,Δ=0.25,q=1,重建 0.25,误差 0.12。

资料

运行命令与预期

课程根目录的 PowerShell;代码依赖练习包中的 course_lab.py。

& ..\learned-image-compression-study\.venv\Scripts\python.exe python/answers/L12.py

误差不超过半步长;STE 梯度为 1。

独立练习

实现均匀量化,比较步长 0.1、0.5、1.0 的误差和符号数。

提示 1:先不设置截断范围。

提示 2:比较的是 q 的唯一值数,不是浮点文件大小。

拓展:验证直通估计的前向值和反向梯度分别是什么。

常见错误:不要声称 STE 是 round 的真实导数。

可运行参考答案

def lesson12():
    y=torch.linspace(-2,2,101)
    for step in [.1,.5,1.]:
        q=torch.round(y/step); reconstructed=q*step; error=(y-reconstructed).abs()
        assert error.max()<=step/2+1e-6
        print('step,max_error,symbols:',step,float(error.max()),len(q.unique()))
    y=torch.tensor([.37],requires_grad=True)
    ste=y+(y.round()-y).detach(); ste.sum().backward(); assert y.grad.item()==1

相关工具实现

仅使用标准库或直接框架接口。

理解题与参考答案

1. 量化一定减少文件大小吗?

不保证,还取决于符号分布及存储编码方式。

2. round 可直接正常反传吗?

导数几乎处处为零,需训练近似。

3. Δ/2 界何时不适用?

发生额外截断、饱和或其他数值误差时。

验收:误差满足半步长界;能区分潜变量量化和权重量化

未通过时:返回本课先修内容,用较小输入逐步打印中间结果。

L13 概率、信息量与概率密度

第 5 周 · 150 分钟

目标:把符号概率转换为理想编码长度

先修:完成 L12 的自检;不熟悉时先返回上一课

组会联系:熵模型输出影响最终需要多少比特。

建议节奏:回顾 10 分钟 → 讲解与阅读 35 → 运行示例 25 → 独立练习 50 → 自检与记录 30。

概率表示离散事件发生的权重,所有互斥完备事件概率之和为一。常见符号可以分配短码,少见符号需要更长码。−log2 p 给出理想信息量,但单个实际码字长度通常还受编码算法约束。

连续变量用概率密度描述,密度可以大于一,不能直接当作离散符号概率。量化得到整数 k 后,需要对对应区间的密度积分;高斯模型可用两个 CDF 值之差计算。

避免把零概率代入 log。训练实现常设置极小下界保持数值稳定,但下界过大也会改变率估计。先用四个离散符号理解信息量,再回到神经网络的分布参数。

I(k)=−log₂ p(k);P(k)=CDF(k+0.5)−CDF(k−0.5)

概率 1/2 的符号理想需要 1 bit,概率 1/8 需要 3 bits;密度值 2 不是 200% 概率。

资料

  • 2.6 概率:离散概率、随机变量、条件概率,25 分钟。

运行命令与预期

课程根目录的 PowerShell;代码依赖练习包中的 course_lab.py。

& ..\learned-image-compression-study\.venv\Scripts\python.exe python/answers/L13.py

信息量为 1、2、3、3 bits;标准正态区间质量约 0.3829。

独立练习

计算四符号信息量,并用高斯 CDF 差计算整数 0 对应区间的概率。

提示 1:使用 math.log2 和 math.erf。

提示 2:标准正态 CDF 可由 erf 表示,区间概率是上界减下界。

拓展:改变标准差,观察中心符号与远端符号概率。

常见错误:负概率通常是 CDF 上下界写反;极小负误差需要数值排查。

可运行参考答案

def lesson13():
    probabilities=[.5,.25,.125,.125]
    bits=[-math.log2(p) for p in probabilities]
    cdf=lambda x:.5*(1+math.erf(x/math.sqrt(2)))
    mass=cdf(.5)-cdf(-.5)
    assert bits==[1,2,3,3] and 0<mass<1
    print('bits:',bits,'standard normal mass [-.5,.5]:',mass)

相关工具实现

仅使用标准库或直接框架接口。

理解题与参考答案

1. 概率能大于一吗?

离散概率不能,连续密度可以。

2. 为什么取负对数?

把独立事件概率的乘积转成可相加的信息量。

3. 小概率意味着什么?

在该模型下更意外,理想编码长度更长。

验收:能区分密度和概率质量;信息量随概率减小而增大

未通过时:返回本课先修内容,用较小输入逐步打印中间结果。

L14 熵、交叉熵与模型失配

第 5 周 · 150 分钟

目标:解释为什么更准的概率模型能省码率

先修:完成 L13 的自检;不熟悉时先返回上一课

组会联系:上下文模型通常通过更准确预测概率改善编码。

建议节奏:回顾 10 分钟 → 讲解与阅读 35 → 运行示例 25 → 独立练习 50 → 自检与记录 30。

熵是按真实概率加权的信息量平均值。若符号并非等概率,固定长度编码会浪费可利用的统计结构。熵给出特定假设下的理论基准,不是每个有限文件都能精确达到的字节数。

实际编码使用模型 q,而数据来自分布 p。平均理想码长为交叉熵,等于熵加上 KL 散度。模型越偏离真实分布,编码成本通常越高;更复杂模型也可能带来时间和侧信息成本。

本课比较两套概率表,不实现完整算术编码器。实验应注明是理想码长估计;真实码流还有有限精度、终止状态和头部开销。

H(p)=−Σp log₂p;H(p,q)=H(p)+KL(p||q)

p=[0.75,0.25] 的熵约 0.811 bits;用 q=[0.5,0.5] 编码平均需 1 bit。

资料

  • 2.6 概率:条件概率与期望的相关小节,20 分钟。

运行命令与预期

课程根目录的 PowerShell;代码依赖练习包中的 course_lab.py。

& ..\learned-image-compression-study\.venv\Scripts\python.exe python/answers/L14.py

熵约 0.8113、交叉熵 1、差约 0.1887。

独立练习

实现 entropy 和 cross_entropy,验证两者差非负且 q=p 时相等。

提示 1:只对 p>0 的项求和。

提示 2:q 在 p>0 的位置为零时,理论交叉熵无穷大。

拓展:用不同样本量估计同一分布,观察估计波动。

常见错误:自然对数与 log2 单位不同;前者是 nat,后者是 bit。

可运行参考答案

def lesson14():
    def cross_entropy(p,q):
        return sum(-a*math.log2(b) if b>0 else math.inf for a,b in zip(p,q) if a>0)
    p=[.75,.25]; h=cross_entropy(p,p); cross=cross_entropy(p,[.5,.5])
    assert abs(h-.811278124459)<1e-8 and cross>=h
    print('entropy,cross_entropy,KL:',h,cross,cross-h)

相关工具实现

仅使用标准库或直接框架接口。

理解题与参考答案

1. 交叉熵能小于熵吗?

同一真实分布下理论上不能,差为非负 KL。

2. 更大模型必然更省总成本吗?

不一定,还需考虑速度、模型和侧信息。

3. 经验直方图代表真实分布吗?

是样本估计,小样本可能偏差很大。

验收:数值符合 0.811 和 1 的例子;不把理想 bit 数标成实际文件大小

未通过时:返回本课先修内容,用较小输入逐步打印中间结果。

L15 率失真训练与可微近似

第 5 周 · 150 分钟

目标:把率项和失真项连到同一损失

先修:完成 L14 的自检;不熟悉时先返回上一课

组会联系:R+λD 是多条组会研究线的共同目标。

建议节奏:回顾 10 分钟 → 讲解与阅读 35 → 运行示例 25 → 独立练习 50 → 自检与记录 30。

重建网络只优化失真,会倾向于保留更多信息。加入率项后,模型必须为信息成本付费。R 通常是总负对数概率除以原图像素数,D 可取 MSE;二者量纲不同,lambda 的数值依赖具体定义。

常见实现对 0~1 图像的 MSE 乘以 255²,因此不能拿两份代码中相同 lambda 当作相同权衡。比较前先写清公式和像素范围。增大 lambda 在 R+lambda D 写法下强调重建质量。

训练时加噪声近似量化、测试时舍入会带来差异。看训练 loss 之外还要定期评测真实舍入后的重建;本地教学模型报告的仍是 estimated bpp。

R=−Σlog₂p(y_hat)/(N H W);L=R+λD

R=0.5,D=0.01,λ=10 时 L=0.6;若 D 改为 255²MSE,同一 λ 的含义完全不同。

资料

运行命令与预期

课程根目录的 PowerShell;代码依赖练习包中的 course_lab.py。

& ..\learned-image-compression-study\.venv\Scripts\python.exe python/answers/L15.py

R=2.25 bpp;三种 lambda 给出不同总损失。

独立练习

计算同一预测下三种 lambda 的总损失,分别输出 R、D、L。

提示 1:所有 likelihood 分支都要计入。

提示 2:先检查概率范围与有限值,再求 log2。

拓展:实现 STE 版本,与噪声近似比较梯度。

常见错误:出现负 bpp 时检查是否误用了密度而非离散概率。

可运行参考答案

def lesson15():
    likelihood=torch.tensor([.5,.25,.125,.125]).reshape(1,1,2,2)
    rate=-likelihood.log2().sum()/4; mse=torch.tensor(.01)
    for lam in [1,10,100]: print('R,D,L:',rate.item(),mse.item(),(rate+lam*mse).item())
    assert rate.item()==2.25
    print('scaled MSE:',float(mse*255**2),'not the same lambda convention')

相关工具实现

仅使用标准库或直接框架接口。

理解题与参考答案

1. 率项除以潜变量个数吗?

计算 bpp 应除以输入原图像素数,批量时含 N。

2. lambda 变大通常怎样?

在指定公式下更重视减小失真,但结果还受优化影响。

3. 为什么加噪声?

提供可用梯度的量化近似,并非生成真实码流。

验收:各分量分别可见;公式与像素范围写入记录

未通过时:返回本课先修内容,用较小输入逐步打印中间结果。

L16 阅读本地教学压缩项目

第 6 周 · 150 分钟

目标:沿着输入、潜变量、概率、重建定位代码

先修:完成 L15 的自检;不熟悉时先返回上一课

组会联系:把组会中的符号对应到可运行实现。

建议节奏:回顾 10 分钟 → 讲解与阅读 35 → 运行示例 25 → 独立练习 50 → 自检与记录 30。

先读 README 的能力边界,再读 model.py 的 forward。依次找编码器、训练噪声与评测舍入、概率质量计算、解码器。把每个变量的形状写在纸上,不从最底层工具函数开始通读。

本地 FactorizedPriorCodec 使用按通道的高斯先验估计概率,是简化教学模型;它不是 CompressAI 的同名官方架构。模型文件、训练代码和评价脚本承担不同职责,不要只看训练入口。

使用静态搜索找到 train_toy.py 和 metrics.py 中的损失与指标。记录哪些步骤已实现、哪些未实现。没有 compress/decompress 和实际字节串,就不能宣称已经完成真实编码。

x → encoder → y → noise/round → y_hat → decoder → x_hat

看到 likelihood 只能证明有率估计;看到 checkpoint 只能证明保存了模型,都不等于图像码流。

资料

运行命令与预期

课程根目录的 PowerShell;代码依赖练习包中的 course_lab.py。

& ..\learned-image-compression-study\.venv\Scripts\python.exe python/answers/L16.py

生成本课 outputs 文件并通过代码中的断言;具体数值随训练与硬件变化,不以固定准确率作为承诺。

独立练习

运行源码定位脚本,列出模型、损失、指标三处入口,并写一段数据流说明。

提示 1:从已知项目根目录定位文件。

提示 2:搜索 def forward、likelihood、rd_lambda 等词,再检查上下文。

拓展:用 forward hook 记录模型各层输出 shape。

常见错误:已有实验结果不表示你已经理解代码;先解释再复跑。

可运行参考答案

def lesson16():
    args=cli(); root=Path(args.project)/'src'/'lic_study'
    for filename in ['model.py','train_toy.py','metrics.py']:
        p=root/filename
        if not p.exists(): raise FileNotFoundError(f'{p}: pass --project with the local study project')
        print('\n',p)
        for i,line in enumerate(p.read_text(encoding='utf-8').splitlines(),1):
            if any(k in line for k in ['def forward','def likelihood','def rate_distortion','def estimated_bpp','rd_lambda']): print(i,line.strip())

相关工具实现

def cli():
    p=argparse.ArgumentParser()
    p.add_argument('--image'); p.add_argument('--images'); p.add_argument('--results'); p.add_argument('--bitstream')
    p.add_argument('--project',default=str(Path(__file__).resolve().parents[2]/'learned-image-compression-study'))
    p.add_argument('--hpcm',default=str(Path(__file__).resolve().parents[2]/'LIC-HPCM'))
    p.add_argument('--quality',type=int,default=1); p.add_argument('--model',choices=['factorized','hyperprior'],default='factorized')
    p.add_argument('--self-test',action='store_true'); p.add_argument('--cuda',action='store_true')
    return p.parse_args()

理解题与参考答案

1. 应该先看哪个入口?

README 和模型 forward,再追到损失与评测。

2. 教学模型等于官方基线吗?

不等于,必须用实际架构与实现名称。

3. 何时能说真实编码?

产生实际码流且能独立解码后才可以。

验收:能指出率估计位置;明确缺少真实熵编码

未通过时:返回本课先修内容,用较小输入逐步打印中间结果。

L17 单图过拟合与训练排错

第 6 周 · 150 分钟

目标:用固定小输入确认模型能够学习

先修:完成 L16 的自检;不熟悉时先返回上一课

组会联系:新模块失败时先验证基本链路。

建议节奏:回顾 10 分钟 → 讲解与阅读 35 → 运行示例 25 → 独立练习 50 → 自检与记录 30。

单图过拟合是受控诊断:固定图片、种子、预处理和模型,观察模型能否显著降低该图的损失。它故意让训练和评测重叠,不可用于泛化性能结论。

先运行很少步检查 shape、有限 loss、参数更新与保存。再增加步数,看重建是否由灰图变成有结构。若加率项后始终坍缩,可分别检查学习率、lambda 尺度、概率模型和量化近似。

已有历史实验同时改过步数与 lambda,不能把性能变化只归因于 lambda。新实验每次只改一项,记录失败配置;不要删掉不好看的曲线。

诊断顺序:数据范围 → 前向尺寸 → 有限损失 → 梯度 → 参数更新 → 重建

20 步与 200 步且 lambda 不同,是两个因素变化;这不能证明某一个因素造成提升。

资料

运行命令与预期

课程根目录的 PowerShell;代码依赖练习包中的 course_lab.py。

& ..\learned-image-compression-study\.venv\Scripts\python.exe python/answers/L17.py

生成本课 outputs 文件并通过代码中的断言;具体数值随训练与硬件变化,不以固定准确率作为承诺。

独立练习

运行单图训练诊断,断言参数变化、loss 有限、最终重建误差降低。

提示 1:先固定一个种子。

提示 2:保存更新前参数的 clone,不能保留同一个引用。

拓展:对本地教学模型固定 lambda,只改变 20/100/200 步。

常见错误:clone 才能保留旧值;直接 old=model.weight 会一起改变。

可运行参考答案

def lesson17():
    _,record=fit_autoencoder(steps=30)
    print('single-image diagnostic, NOT generalization:',record); emit('L17.json',record)

相关工具实现

def emit(name, value):
    path=OUT/name
    path.write_text(json.dumps(value,ensure_ascii=False,indent=2,allow_nan=False),encoding='utf-8')
    print(path)
    return path

def fit_autoencoder(channels=8, steps=60, random_seed=42):
    seed(random_seed); model=Autoencoder(channels); x=picture()
    opt=torch.optim.Adam(model.parameters(),lr=.01)
    initial=float(F.mse_loss(model(x),x).detach())
    old=[p.detach().clone() for p in model.parameters()]
    for _ in range(steps):
        loss=F.mse_loss(model(x),x); assert torch.isfinite(loss)
        opt.zero_grad(); loss.backward(); opt.step()
    model.eval()
    with torch.inference_mode(): out=model(x); final=float(F.mse_loss(out,x))
    assert final<initial, (initial,final)
    assert any(not torch.equal(p,q) for p,q in zip(old,model.parameters()))
    return model,dict(initial_mse=initial,final_mse=final,steps=steps,seed=random_seed,synthetic=True,train_test_overlap=True)

理解题与参考答案

1. 单图指标能写成基准结果吗?

不能,只能说明代码有一定拟合能力。

2. 灰图说明什么?

可能未收敛、瓶颈坍缩或预处理错误,需要逐项排查。

3. 如何证明 step 生效?

比较更新前后的参数,并验证有非零有限梯度。

验收:记录训练评测重叠;能解释一项失败的检查

未通过时:返回本课先修内容,用较小输入逐步打印中间结果。

L18 独立测试与控制变量

第 6 周 · 150 分钟

目标:为实验制作固定清单、种子和配置

先修:完成 L17 的自检;不熟悉时先返回上一课

组会联系:可信比较比多跑几组数字更重要。

建议节奏:回顾 10 分钟 → 讲解与阅读 35 → 运行示例 25 → 独立练习 50 → 自检与记录 30。

训练集用于优化参数,验证集用于选择配置,测试集用于最终比较。文件名不同也可能是同一图的拷贝,所以除路径外可以用文件哈希检测重叠;裁剪衍生图还需按源图分组。

比较 A 与 B 时固定数据、训练预算、预处理和评测口径,只改变要研究的因素。随机种子固定有助于复现,但一个种子不能衡量稳定性,关键结论可增加多个种子。

清单记录相对路径、哈希和分组,配置记录设备与软件版本。出现异常应保留日志和配置,先定位偏差,再决定是否重跑。

实验 = 固定数据清单 + 明确配置 + 执行代码版本 + 原始输出

把同一图片改名复制到 test,不会让它成为独立测试样本。

资料

运行命令与预期

课程根目录的 PowerShell;代码依赖练习包中的 course_lab.py。

& ..\learned-image-compression-study\.venv\Scripts\python.exe python/answers/L18.py

生成本课 outputs 文件并通过代码中的断言;具体数值随训练与硬件变化,不以固定准确率作为承诺。

独立练习

生成文件清单并用 SHA256 检查训练/测试交集,发现重复时明确报错。

提示 1:比较哈希集合交集。

提示 2:记录配置 JSON,包含 seed、image_range、device。

拓展:添加同一原图不同裁剪的 source_id 分组。

常见错误:先拆分源图再增强,防止增强版本泄漏。

可运行参考答案

def lesson18():
    def digest(data): return hashlib.sha256(data).hexdigest()
    train={'a.png':digest(b'image A'),'b.png':digest(b'image B')}
    test={'renamed.png':digest(b'image A')}
    overlap=set(train.values())&set(test.values()); assert len(overlap)==1
    print('deliberate leakage detected:',overlap)
    test={'c.png':digest(b'image C')}; assert not set(train.values())&set(test.values())
    emit('L18_manifest_example.json',dict(synthetic_fixture=True,train=train,test=test,seed=42,range='0..1',device='cpu'))

相关工具实现

def emit(name, value):
    path=OUT/name
    path.write_text(json.dumps(value,ensure_ascii=False,indent=2,allow_nan=False),encoding='utf-8')
    print(path)
    return path

理解题与参考答案

1. 验证集和测试集角色一样吗?

不一样,验证集参与选择,测试集保留作最终评价。

2. 固定种子保证跨设备逐位相同吗?

不保证,算法、硬件和实现也影响数值。

3. 改模型又改分辨率能公平比较吗?

不能把差异单独归因于模型。

验收:重复样本能被发现;实验配置与输出同目录保存

未通过时:返回本课先修内容,用较小输入逐步打印中间结果。

L19 本机 WSL 与独立 Linux 环境

第 7 周 · 150 分钟

目标:理解解释器、系统和依赖隔离

先修:完成 L18 的自检;不熟悉时先返回上一课

组会联系:真实熵编码包含原生扩展,环境也是实验的一部分。

建议节奏:回顾 10 分钟 → 讲解与阅读 35 → 运行示例 25 → 独立练习 50 → 自检与记录 30。

Windows Python 与 WSL 内的 Linux Python 是两个环境。Windows 的 .venv 不能复制成 Linux 虚拟环境使用;模型数据可以共享,但依赖必须在各自系统中安装。

先在 PowerShell 执行 wsl --status 和 wsl --list --verbose。若没有可用发行版,再按实验区安装说明安装 Ubuntu;管理员权限、虚拟化和重启是系统条件,课程网页不会自动执行这些操作。

Linux 中新建 ~/lic-course 和独立 venv,使用 python -m pip 避免装到错误解释器。先用 CPU 跑小图真实编解码,GPU 是后续加速选项。安装成功后保存 pip freeze 和设备信息;首次运行需要下载公开权重。

PowerShell → wsl → Ubuntu shell → Linux venv → CompressAI

/mnt/e/Projects/research 对应 Windows 的 E:\Projects\research,但两个系统中的 python.exe/python 不能混用。

资料

  • 安装 WSL:Install WSL command、发行版检查与重启,25 分钟。

运行命令与预期

课程根目录的 PowerShell;代码依赖练习包中的 course_lab.py。

& ..\learned-image-compression-study\.venv\Scripts\python.exe python/answers/L19.py

打印实际解释器、系统、版本;检查不等于安装成功。

独立练习

运行环境报告;按实验区步骤核对 Linux Python 和 CompressAI 版本,保存结果。

提示 1:先打印 sys.executable 与 platform.system()。

提示 2:不要对系统 Python 使用 sudo pip。

拓展:核对 WSL 中 GPU 是否可见,并记录 CUDA 版本。

常见错误:课程提供安装指导,权限或网络阻塞不应通过反复重装旧环境解决。

可运行参考答案

def lesson19():
    import importlib.util
    report=dict(executable=sys.executable,system=platform.system(),python=sys.version,torch=torch.__version__,
      cuda=torch.cuda.is_available(),compressai_installed=importlib.util.find_spec('compressai') is not None)
    emit('L19_environment.json',report); print(report)
    print('Environment inspection only; does not install WSL or packages.')

相关工具实现

def emit(name, value):
    path=OUT/name
    path.write_text(json.dumps(value,ensure_ascii=False,indent=2,allow_nan=False),encoding='utf-8')
    print(path)
    return path

理解题与参考答案

1. 能直接复用 Windows venv 吗?

不能,二进制和路径约定不同。

2. 没有 WSL 权限怎么办?

记录阻塞,继续本地课,不把真实编码标完成。

3. 为什么先用 CPU?

减少 CUDA 环境变量,先验证真实码流链路。

验收:Windows/Linux 解释器能够区分;安装失败保存完整命令与报错

未通过时:返回本课先修内容,用较小输入逐步打印中间结果。

L20 CompressAI:从图像到码流再回来

第 7 周 · 150 分钟

目标:运行真实 compress/decompress

先修:完成 L19 的自检;不熟悉时先返回上一课

组会联系:神经网络前向重建和实际编解码是两条路径。

建议节奏:回顾 10 分钟 → 讲解与阅读 35 → 运行示例 25 → 独立练习 50 → 自检与记录 30。

使用 bmshj2018-factorized 的预训练权重,调用 eval 与 update,输入 RGB、0~1 张量。根据模型下采样倍率补齐空间尺寸,解码后裁回原图;本课统一补到 64 的倍数。

forward 常用于训练和估计率;compress 产生字节串,decompress 读取字节串并重建。保存码流时还要保存模型名称、质量档位、原尺寸及必要形状信息,使另一个进程能够解码。

本课程二进制教学容器保存 JSON 头和原始编码字节,不使用 PNG 冒充码流。它是课程自定义格式,不是通用标准。解码必须拥有相同模型与参数。

payload bits = 8 Σlen(stream);file bits = 8×容器文件字节数

一张 65×70 图补齐为 128×128,解码后裁回 65×70;bpp 分母用原图 65×70。

资料

运行命令与预期

WSL Ubuntu(按环境指南指定 TORCH_HOME);代码依赖练习包中的 course_lab.py。

.venv-linux/bin/python python/answers/L20.py --image python/test-images/kodim01.png

产生真实 L20.lic、重建 PNG 与 CSV;重载容器解码和原 stream 解码相同。

独立练习

在 Linux 运行真实编码脚本,保存 .lic 文件,在独立解码调用中重建并检查尺寸。

提示 1:先使用 factorized quality=1 和小图。

提示 2:比较的是编码后和重载码流后的重建一致性,而非要求等于原图。

拓展:对奇数尺寸图片重复实验。

常见错误:Linux/权重不可用时脚本会失败并记录原因,不回退为伪造码流。

可运行参考答案

def lesson20():
    args=cli()
    if not args.image: raise ValueError('Use --image /path/to/photo.png inside the Linux environment')
    img=Image.open(args.image).convert('RGB')
    row=real_encode(img,args.model,args.quality,OUT/'L20.lic'); row['image']=Path(args.image).name
    csv_write('L20.csv',[row]); print(row)

相关工具实现

def csv_write(name, rows):
    if not rows: raise ValueError('empty results')
    path=OUT/name
    with path.open('w',newline='',encoding='utf-8') as f:
        w=csv.DictWriter(f,fieldnames=list(rows[0])); w.writeheader(); w.writerows(rows)
    print(path)
    return path

def real_encode(img,name,quality,path,model=None):
    model=model or load_codec(name,quality)
    x=pil_tensor(img); h,w=x.shape[-2:]; padded=F.pad(x,(0,(-w)%64,0,(-h)%64))
    with torch.inference_mode():
        start=time.perf_counter(); enc=model.compress(padded); encoding=time.perf_counter()-start
        forward=model(padded)
        estimated=sum(float(-torch.log2(p.clamp_min(1e-12)).sum()) for p in forward['likelihoods'].values())/(h*w)
    pack_stream(path,dict(model=name,quality=quality,height=h,width=w,shape=list(enc['shape'])),enc['strings'])
    meta,streams=unpack_stream(path)
    with torch.inference_mode():
        start=time.perf_counter(); decoded=model.decompress(streams,meta['shape'])['x_hat'][...,:h,:w].clamp(0,1); decoding=time.perf_counter()-start
        reference=model.decompress(enc['strings'],enc['shape'])['x_hat'][...,:h,:w].clamp(0,1)
    assert decoded.shape==x.shape and torch.equal(decoded,reference)
    to_image(decoded).save(Path(path).with_suffix('.reconstruction.png'))
    payload=sum(len(s) for group in streams for s in group)
    side=sum(len(s) for group in streams[1:] for s in group)
    return dict(image='',model=name,quality=quality,width=w,height=h,file_bytes=Path(path).stat().st_size,
      payload_bytes=payload,file_bpp=bpp(Path(path).stat().st_size,h,w),payload_bpp=bpp(payload,h,w),
      estimated_bpp=estimated,psnr=psnr(x,decoded),side_bytes=side,encode_seconds=encoding,decode_seconds=decoding)

def cli():
    p=argparse.ArgumentParser()
    p.add_argument('--image'); p.add_argument('--images'); p.add_argument('--results'); p.add_argument('--bitstream')
    p.add_argument('--project',default=str(Path(__file__).resolve().parents[2]/'learned-image-compression-study'))
    p.add_argument('--hpcm',default=str(Path(__file__).resolve().parents[2]/'LIC-HPCM'))
    p.add_argument('--quality',type=int,default=1); p.add_argument('--model',choices=['factorized','hyperprior'],default='factorized')
    p.add_argument('--self-test',action='store_true'); p.add_argument('--cuda',action='store_true')
    return p.parse_args()

理解题与参考答案

1. 只调用 model(x) 算真实编码吗?

不算,需要 compress/decompress 和实际字节串。

2. 为什么保存 shape?

熵解码和重建需要正确的潜变量空间尺寸。

3. 权重文件计入每张图吗?

通常视为双方已有模型,但必须说明这一约定。

验收:生成非空 .lic 且可重载解码;原尺寸、模型、质量档位写入头部

未通过时:返回本课先修内容,用较小输入逐步打印中间结果。

L21 准确测量码率与编码开销

第 7 周 · 150 分钟

目标:同时报告 estimated、payload 和 file bpp

先修:完成 L20 的自检;不熟悉时先返回上一课

组会联系:组会里很小的提升可能被评价口径影响。

建议节奏:回顾 10 分钟 → 讲解与阅读 35 → 运行示例 25 → 独立练习 50 → 自检与记录 30。

模型 likelihood 得到的是估计长度;熵编码后的字节串给出 payload 长度;加头部后的文件给出实际容器长度。三者应分列,不能混成一个 bpp 字段。

有限精度概率表、终止状态与字节对齐会使估计和实际长度不同。对于小图,头部占比可能很大;对于大图,固定头部的 bpp 开销会下降。任何比较都要说明是否计入头部和侧信息。

超先验模型有多个 stream 分支,应全部计入。原图被 padding 后仍以原始 H×W 为分母,但 padding 也可能增加编码成本,因此所有方法需说明其策略。

estimated_bpp=−Σlog₂p/(HW);payload_bpp=8Bpayload/(HW);file_bpp=8Bfile/(HW)

1000 字节码流加 100 字节头部用于 100×100 图,分别为 0.8 和 0.88 bpp。

资料

运行命令与预期

WSL Ubuntu(按环境指南指定 TORCH_HOME);代码依赖练习包中的 course_lab.py。

.venv-linux/bin/python python/answers/L21.py --bitstream python/outputs/L20.lic

新进程从落盘码流解码出原图尺寸的 L21_decoded.png;payload 与 header 相加等于完整文件大小。

独立练习

解析上一课容器,计算 payload/file bpp 与头部开销;核对长度守恒。

提示 1:不要把模型权重大小算成 payload。

提示 2:验证文件总长=头部及长度字段+所有流字节。

拓展:比较两种尺寸的头部 bpp。

常见错误:比较 JPEG 完整文件与神经 codec 的 payload 时应另列口径,不能直接混报。

可运行参考答案

def lesson21():
    args=cli()
    if args.self_test:
        path=OUT/'L21_serializer_fixture.lic'
        pack_stream(path,dict(height=100,width=100),[[b'abc'],[b'xy']])
        meta,streams=unpack_stream(path); assert streams==[[b'abc'],[b'xy']]
        print('SERIALIZER TEST ONLY: arbitrary bytes, NOT an entropy coding result')
    else:
        if not args.bitstream: raise ValueError('Pass --bitstream outputs/L20.lic')
        path=Path(args.bitstream); meta,streams=unpack_stream(path)
        model=load_codec(meta['model'],int(meta['quality']))
        with torch.inference_mode():
            decoded=model.decompress(streams,meta['shape'])['x_hat'][...,:meta['height'],:meta['width']].clamp(0,1)
        assert tuple(decoded.shape)==(1,3,meta['height'],meta['width'])
        reconstructed=OUT/'L21_decoded.png'
        to_image(decoded).save(reconstructed)
        print('Decoded from stored bytes in a fresh process:',reconstructed,tuple(decoded.shape))
    payload=sum(len(s) for group in streams for s in group); total=path.stat().st_size
    assert total>payload
    print(dict(payload_bytes=payload,header_bytes=total-payload,payload_bpp=bpp(payload,meta['height'],meta['width']),file_bpp=bpp(total,meta['height'],meta['width'])))

相关工具实现

def to_image(x):
    a=x.detach().cpu().squeeze(0).clamp(0,1).permute(1,2,0).numpy()
    return Image.fromarray(np.round(a*255).astype(np.uint8))

def bpp(byte_count,h,w):
    if byte_count<0 or h<=0 or w<=0: raise ValueError('invalid byte count or dimensions')
    return 8*byte_count/(h*w)

def pack_stream(path,meta,strings):
    header=dict(meta); header['stream_lengths']=[[len(s) for s in group] for group in strings]
    blob=json.dumps(header,separators=(',',':')).encode('utf-8')
    Path(path).write_bytes(MAGIC+struct.pack('>I',len(blob))+blob+b''.join(s for group in strings for s in group))

def unpack_stream(path):
    data=Path(path).read_bytes()
    if not data.startswith(MAGIC): raise ValueError('not a course bitstream')
    pos=len(MAGIC)
    if len(data)<pos+4: raise ValueError('truncated header')
    length=struct.unpack('>I',data[pos:pos+4])[0]; pos+=4
    if length>1_000_000 or pos+length>len(data): raise ValueError('invalid header length')
    meta=json.loads(data[pos:pos+length]); pos+=length; strings=[]
    for lengths in meta['stream_lengths']:
        group=[]
        for size in lengths:
            if not isinstance(size,int) or size<0 or pos+size>len(data): raise ValueError('truncated stream')
            group.append(data[pos:pos+size]); pos+=size
        strings.append(group)
    if pos!=len(data): raise ValueError('unexpected trailing bytes')
    return meta,strings

def load_codec(name,quality):
    if platform.system()!='Linux': raise RuntimeError('Use the separate WSL Linux environment for CompressAI; see environment.md')
    from compressai.zoo import bmshj2018_factorized,bmshj2018_hyperprior
    choices={'factorized':bmshj2018_factorized,'hyperprior':bmshj2018_hyperprior}
    model=choices[name](quality=quality,pretrained=True).eval().cpu()
    model.update(force=True)
    return model

def cli():
    p=argparse.ArgumentParser()
    p.add_argument('--image'); p.add_argument('--images'); p.add_argument('--results'); p.add_argument('--bitstream')
    p.add_argument('--project',default=str(Path(__file__).resolve().parents[2]/'learned-image-compression-study'))
    p.add_argument('--hpcm',default=str(Path(__file__).resolve().parents[2]/'LIC-HPCM'))
    p.add_argument('--quality',type=int,default=1); p.add_argument('--model',choices=['factorized','hyperprior'],default='factorized')
    p.add_argument('--self-test',action='store_true'); p.add_argument('--cuda',action='store_true')
    return p.parse_args()

理解题与参考答案

1. 漏算 z 分支会怎样?

低估超先验总码率,比较失真。

2. 小图开销为何突出?

固定头部除以较小像素数后占比变大。

3. 估计比实际大是 bug 吗?

不一定,先核对量化、概率表、padding 和计量范围。

验收:所有分支均计入;三种码率标注清楚

未通过时:返回本课先修内容,用较小输入逐步打印中间结果。

L22 五张图片、三个档位:建立基准

第 8 周 · 150 分钟

目标:形成固定测试集上的原始结果表

先修:完成 L21 的自检;不熟悉时先返回上一课

组会联系:实验表应该从脚本生成,而不是手工拼数。

建议节奏:回顾 10 分钟 → 讲解与阅读 35 → 运行示例 25 → 独立练习 50 → 自检与记录 30。

先固定至少五张 RGB 测试图片并生成哈希清单。每张图片同时跑 JPEG 的 20/50/85 和 factorized 的 1/3/5 档位;数字并不一一对应,只代表各自模型的工作点。

保留每图结果、重建文件和每档汇总。bpp 可以按图平均或按像素加权,但必须明确,不能混用。PSNR 的图平均与先合并 MSE 再换算也不同,本课程默认逐图 PSNR 平均。

预训练模型在该测试集上不再调参。合成图可以做 smoke test,但正式报告需要注明图片来源;不能用重复裁剪凑成独立的五张图。

mean_bpp = mean_i(bpp_i);mean_PSNR = mean_i(PSNR_i)

100×100 与 1000×1000 图的逐图平均会给二者相同权重,而像素加权不同。

资料

运行命令与预期

WSL Ubuntu(按环境指南指定 TORCH_HOME);代码依赖练习包中的 course_lab.py。

.venv-linux/bin/python python/answers/L22.py --images python/test-images

至少 30 行真实结果及清单、环境 JSON。

独立练习

运行基准入口,对五张图片与两种编码器生成至少 30 行结果,保留完整文件 bpp。

提示 1:先用一张小图检查路径,再批量。

提示 2:汇总前确认每种模型每档图片数量一致。

拓展:添加 scale-hyperprior 的 1/3/5 档,保持同一图片清单。

常见错误:不要直接比较码率完全不重叠的两个点并宣布某模型更好。

可运行参考答案

def lesson22():
    args=cli()
    if not args.images: raise ValueError('Pass --images folder containing at least 5 fixed test images in Linux')
    print(benchmark(args.images))

相关工具实现

def benchmark(folder,include_hyper=False):
    files=sorted(p for p in Path(folder).iterdir() if p.suffix.lower() in {'.png','.jpg','.jpeg'})
    if len(files)<5: raise ValueError('Provide at least five fixed, distinct test images')
    hashes=[hashlib.sha256(p.read_bytes()).hexdigest() for p in files]
    if len(set(hashes))!=len(hashes): raise ValueError('duplicate image files')
    emit('test_manifest.json',[dict(file=p.name,sha256=h) for p,h in zip(files,hashes)])
    rows=[]; models=['factorized']+(['hyperprior'] if include_hyper else [])
    for p in files:
        img=Image.open(p).convert('RGB')
        for q in [20,50,85]: rows.append(jpeg_row(img,q,p.name))
    for name in models:
        for q in [1,3,5]:
            model=load_codec(name,q)
            for i,p in enumerate(files):
                row=real_encode(Image.open(p).convert('RGB'),name,q,OUT/f'{i}_{p.stem}_{name}_q{q}.lic',model)
                row['image']=p.name; rows.append(row)
                csv_write('benchmark_partial.csv',rows)
    path=csv_write('benchmark.csv',rows)
    import importlib.metadata as im
    emit('benchmark_environment.json',dict(python=sys.version,torch=torch.__version__,compressai=im.version('compressai'),
      platform=platform.platform(),device='cpu',psnr='mean per image RGB range 0..1',bpp='full file / original pixels',
      padding='right/bottom zero to multiples of 64',jpeg_subsampling=0,models_shared=True,
      timing='compress/decompress only, excludes model load, file IO, forward rate estimation; single samples, not a latency benchmark'))
    return path

def cli():
    p=argparse.ArgumentParser()
    p.add_argument('--image'); p.add_argument('--images'); p.add_argument('--results'); p.add_argument('--bitstream')
    p.add_argument('--project',default=str(Path(__file__).resolve().parents[2]/'learned-image-compression-study'))
    p.add_argument('--hpcm',default=str(Path(__file__).resolve().parents[2]/'LIC-HPCM'))
    p.add_argument('--quality',type=int,default=1); p.add_argument('--model',choices=['factorized','hyperprior'],default='factorized')
    p.add_argument('--self-test',action='store_true'); p.add_argument('--cuda',action='store_true')
    return p.parse_args()

理解题与参考答案

1. JPEG 50 等于 learned quality 3 吗?

没有这种对应,应比较曲线工作点。

2. 为什么保留每图结果?

汇总值可能掩盖某类图片的大幅退化。

3. 只挑效果好的图可行吗?

会产生选择偏差,应先固定清单。

验收:至少 5×3×2 行结果;清单、版本、命令与 CSV 同时保存

未通过时:返回本课先修内容,用较小输入逐步打印中间结果。

L23 RD 曲线与公平比较

第 8 周 · 150 分钟

目标:根据实际工作点解释码率—失真取舍

先修:完成 L22 的自检;不熟悉时先返回上一课

组会联系:BD-rate 是曲线比较,不是单个点的百分比。

建议节奏:回顾 10 分钟 → 讲解与阅读 35 → 运行示例 25 → 独立练习 50 → 自检与记录 30。

RD 图横轴为 bpp、纵轴为 PSNR,左上通常更理想。点之间的连线用于阅读,不代表每一个中间码率都能直接达到。不同图片结果先按固定规则聚合。

比较时先看码率和质量覆盖范围;曲线可能交叉,不应给全域单一结论。BD-rate 依赖公共质量区间与插值方法,本阶段先学会读曲线,不用三个稀疏点草率给精确排名。

耗时也要报告。低 bpp 但慢十倍的模型是否更好取决于场景;写结论时交代图像集、评价指标、硬件和计时范围。

固定质量比较码率;固定码率比较失真;避免公共区间外外推

A 在低码率更好、B 在高码率更好时,应描述交叉区间,而不是只报平均值。

资料

运行命令与预期

课程根目录的 PowerShell;代码依赖练习包中的 course_lab.py。

& ..\learned-image-compression-study\.venv\Scripts\python.exe python/answers/L23.py --results python/outputs/benchmark.csv

生成有模型图例、bpp/PSNR 轴的 L23_RD.svg。

独立练习

从真实 CSV 生成可打开的 SVG RD 图,标明模型和 full-file bpp 口径。

提示 1:先按模型与质量档聚合。

提示 2:按 bpp 排序再连线,保留点标记。

拓展:为每张图各画一张曲线,检查是否有异常样本。

常见错误:图上没有共同质量区间时不计算 BD-rate。

可运行参考答案

def lesson23():
    args=cli()
    if not args.results: raise ValueError('Pass --results outputs/benchmark.csv, or explicitly use L09_jpeg.csv for a synthetic teaching plot')
    rows=read_csv(args.results); rd_svg(rows,OUT/'L23_RD.svg')
    print('source:',args.results,'Do not present synthetic-image plots as natural-image benchmark results.')

相关工具实现

def read_csv(path):
    with Path(path).open(encoding='utf-8',newline='') as f: return list(csv.DictReader(f))

def rd_svg(rows,path):
    from html import escape
    groups={}
    for r in rows:
        k=(r['model'],str(r['quality']))
        groups.setdefault(k,[]).append((float(r['file_bpp']),float(r['psnr'])))
    points={}
    for (model,q),vals in groups.items():
        points.setdefault(model,[]).append((statistics.mean(v[0] for v in vals),statistics.mean(v[1] for v in vals),q))
    flat=[v for values in points.values() for v in values]
    if not flat or not all(math.isfinite(a) and math.isfinite(b) for a,b,_ in flat): raise ValueError('finite measured results required')
    xmin=0; xmax=max(v[0] for v in flat)*1.1 or 1
    ymin=min(v[1] for v in flat)-2; ymax=max(v[1] for v in flat)+2
    sx=lambda x:65+x/xmax*530; sy=lambda y:285-(y-ymin)/(ymax-ymin)*225
    chunks=['<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 350"><rect width="760" height="350" fill="#fffdf7"/>',
      '<g font-family="sans-serif" font-size="12" fill="#1e2c2a"><path d="M65 45V285H595" fill="none" stroke="#1e2c2a"/>',
      '<text x="225" y="332">Full-file bpp (mean per image)</text><text x="12" y="25">PSNR / dB</text>']
    for i in range(5):
        x=xmax*i/4; y=ymin+(ymax-ymin)*i/4
        chunks += [f'<text x="{sx(x):.1f}" y="306">{x:.2f}</text>',f'<text x="15" y="{sy(y):.1f}">{y:.1f}</text>']
    for i,(model,values) in enumerate(points.items()):
        c=['#b94631','#315b6f','#446c5c'][i%3]; values.sort()
        coords=' '.join(f'{sx(x):.2f},{sy(y):.2f}' for x,y,q in values)
        chunks.append(f'<polyline points="{coords}" fill="none" stroke="{c}" stroke-width="2"/>')
        chunks.append(f'<text x="610" y="{65+i*25}" fill="{c}">{escape(model)}</text>')
        for x,y,q in values: chunks.append(f'<circle cx="{sx(x):.2f}" cy="{sy(y):.2f}" r="4" fill="{c}"/>')
    chunks.append('</g></svg>'); Path(path).write_text(''.join(chunks),encoding='utf-8')
    print(path)

def cli():
    p=argparse.ArgumentParser()
    p.add_argument('--image'); p.add_argument('--images'); p.add_argument('--results'); p.add_argument('--bitstream')
    p.add_argument('--project',default=str(Path(__file__).resolve().parents[2]/'learned-image-compression-study'))
    p.add_argument('--hpcm',default=str(Path(__file__).resolve().parents[2]/'LIC-HPCM'))
    p.add_argument('--quality',type=int,default=1); p.add_argument('--model',choices=['factorized','hyperprior'],default='factorized')
    p.add_argument('--self-test',action='store_true'); p.add_argument('--cuda',action='store_true')
    return p.parse_args()

理解题与参考答案

1. 曲线越左上说明什么?

在对应区间用更少 bits 达到较高 PSNR。

2. 三个点能确定完整曲线吗?

不能,连线只是近似展示。

3. 如何处理交叉?

报告各自优势区间,避免全域结论。

验收:图有轴名、单位和图例;结论限定测试集与区间

未通过时:返回本课先修内容,用较小输入逐步打印中间结果。

L24 第一份实验报告与组会表达

第 8 周 · 150 分钟

目标:用证据回答一个清楚的问题

先修:完成 L23 的自检;不熟悉时先返回上一课

组会联系:本科生可以先汇报可复现的基础实验。

建议节奏:回顾 10 分钟 → 讲解与阅读 35 → 运行示例 25 → 独立练习 50 → 自检与记录 30。

报告按问题、方法、设置、结果、限制和下一步组织。先说你比较了什么和发现了什么,再解释运行细节。一个真实而范围小的结论比未经验证的“大幅提升”更有价值。

结果段必须能追溯到 CSV 与图片。图表同时标清模型、数据、单位和聚合方法;文字区分观察、解释和仍需验证的假设。遇到反常样本应展示而不是删除。

准备三分钟口头说明:三十秒介绍问题,一分钟讲设置和结果,最后说明限制与一个具体疑问。即使实验受环境阻塞,也可以汇报已完成步骤和报错证据,不编造数字。

结论句 = 在什么条件下 + 观察到什么 + 证据在哪 + 不能推断什么

“在固定五图测试中,某区间 A 的 file bpp 更低”比“A 比 B 强”更可检验。

资料

运行命令与预期

课程根目录的 PowerShell;代码依赖练习包中的 course_lab.py。

& ..\learned-image-compression-study\.venv\Scripts\python.exe python/answers/L24.py --results python/outputs/benchmark.csv

根据实际 CSV 生成报告骨架;观察和结论由你填写。

独立练习

用真实 CSV 生成报告骨架,补充两条观察、一条限制和一个下一步问题。

提示 1:先插入数据来源与命令。

提示 2:不要修改原始 CSV 让结论更漂亮。

拓展:录制三分钟口头说明,检查是否先讲了问题。

常见错误:不要把模板中的待填写字段保留在交给老师的最终报告里。

可运行参考答案

def lesson24():
    args=cli()
    if not args.results: raise ValueError('Pass --results with an actual CSV')
    rows=read_csv(args.results); assert rows
    text='# 第一份压缩实验报告\n\n数据来源:'+str(Path(args.results).resolve())+'\n\n'
    text+='原始记录数:'+str(len(rows))+'\n\n'
    text+='## 问题与设置\n请根据实际运行填写图片来源、模型、版本、设备、预处理、统计口径。\n\n'
    text+='## 两条观察\n逐条指向 CSV 或图片;区分观察与假设。\n\n## 限制\n说明样本量、速度计量和环境阻塞。\n\n## 下一步\n写一个可以用控制变量实验回答的问题。\n'
    path=OUT/'L24_report.md'; path.write_text(text,encoding='utf-8'); print(path)

相关工具实现

def read_csv(path):
    with Path(path).open(encoding='utf-8',newline='') as f: return list(csv.DictReader(f))

def cli():
    p=argparse.ArgumentParser()
    p.add_argument('--image'); p.add_argument('--images'); p.add_argument('--results'); p.add_argument('--bitstream')
    p.add_argument('--project',default=str(Path(__file__).resolve().parents[2]/'learned-image-compression-study'))
    p.add_argument('--hpcm',default=str(Path(__file__).resolve().parents[2]/'LIC-HPCM'))
    p.add_argument('--quality',type=int,default=1); p.add_argument('--model',choices=['factorized','hyperprior'],default='factorized')
    p.add_argument('--self-test',action='store_true'); p.add_argument('--cuda',action='store_true')
    return p.parse_args()

理解题与参考答案

1. 没有正向提升能写报告吗?

能,可信的负结果同样能指导下一步。

2. 观察与解释的区别?

观察来自数据;机制解释需要进一步实验支持。

3. 环境未跑通怎么写?

写清已验证部分、阻塞命令及待完成项。

验收:每个数字有来源;能明确陈述一条限制

未通过时:返回本课先修内容,用较小输入逐步打印中间结果。

L25 精读因子化先验论文

第 9 周 · 150 分钟

目标:把模型图、目标函数和实验连成一条论证

先修:完成 L24 的自检;不熟悉时先返回上一课

组会联系:理解新方法之前先建立一个清楚的基线。

建议节奏:回顾 10 分钟 → 讲解与阅读 35 → 运行示例 25 → 独立练习 50 → 自检与记录 30。

阅读 Ballé 2017 时先找问题:怎样联合学习变换与量化表示,使码率和失真达到更好的权衡。第一遍只追踪分析变换、量化、合成变换与熵模型,不要求马上理解所有变分推导。

因子化先验把潜变量联合概率近似为各分量概率的乘积。这简化了编码,却可能忽略空间或通道依赖。两份数据拥有相同单点分布但不同相关结构时,独立模型无法利用这种差异。

论文中的 GDN、可学习密度与本课程简化高斯模型有区别。做阅读笔记时分成论文原方法、库中实现、教学近似三栏,避免把方便解释的简化当成作者原设计。

q(y_hat)=∏ᵢ qᵢ(y_hatᵢ);−log₂q=Σᵢ−log₂qᵢ

四个独立等概率二元符号需要 4 bits;若它们始终相同,独立模型没有利用这份冗余。

资料

运行命令与预期

课程根目录的 PowerShell;代码依赖练习包中的 course_lab.py。

& ..\learned-image-compression-study\.venv\Scripts\python.exe python/answers/L25.py

理想码长 [1,2,2,3,3],合计 11 bits。

独立练习

计算一组潜变量的因子化理想码长,写下论文模型与教学模型的两处差异。

提示 1:先列每个符号的概率。

提示 2:把乘积转成对数和,避免概率连乘下溢。

拓展:阅读 GDN 公式,指出归一化中通道之间的联系。

常见错误:引用论文实验数字时带上测试集和指标,不直接搬成自己的结果。

可运行参考答案

def lesson25():
    p=torch.tensor([.5,.25,.125,.125]); symbols=torch.tensor([0,1,1,2,3])
    lengths=-p[symbols].log2(); assert float(lengths.sum())==11
    print('ideal factorized lengths:',lengths.tolist(),'total',float(lengths.sum()))
    print('No actual bitstream produced; model ignores dependencies.')

相关工具实现

仅使用标准库或直接框架接口。

理解题与参考答案

1. factorized 表示真实独立吗?

表示模型采用独立近似,不保证数据真的独立。

2. 先读公式还是图?

先明确变量和数据流,再读对应公式。

3. 教学高斯等于原论文密度吗?

不是,记录实现差异。

验收:能解释独立近似;笔记区分原方法与教学简化

未通过时:返回本课先修内容,用较小输入逐步打印中间结果。

L26 超先验与侧信息

第 9 周 · 150 分钟

目标:解释为什么额外传 z 可能反而省比特

先修:完成 L25 的自检;不熟悉时先返回上一课

组会联系:h_a、h_s 是为概率预测传递辅助信息。

建议节奏:回顾 10 分钟 → 讲解与阅读 35 → 运行示例 25 → 独立练习 50 → 自检与记录 30。

潜变量不同区域的变化强度可能不同。超编码器由 y 提取 z,把有关分布的信息先发送给解码端;双方由 z_hat 预测 y_hat 的概率参数,从而更准确地编码主潜变量。

z 自己也需要比特。只有改善 y 的编码所省的比特超过 z 的成本,总码率才降低。不能只展示主分支变小就声称整体改善。解码先得到 z_hat,再得到分布参数,最后解码 y_hat。

Scale hyperprior 主要建模尺度;后续模型还可能预测均值和其他参数。不要把所有“超先验”都当作一个完全相同的实现。

R_total = R_z + R_y|z

原先 y 需 1000 bits;加 z 后 y 需 700、z 需 120,总共 820,节省 180。

资料

运行命令与预期

课程根目录的 PowerShell;代码依赖练习包中的 course_lab.py。

& ..\learned-image-compression-study\.venv\Scripts\python.exe python/answers/L26.py

side=120 时净省 180 bits;side=300 时净省 0。

独立练习

实现总码率收益计算,找出侧信息成本使方案不再划算的边界。

提示 1:先算主码流节省量。

提示 2:减去 side_bits 才得到净收益。

拓展:用简单高斯区间概率模拟不同尺度的交叉熵。

常见错误:不要漏算 z,也不要把 z 的文件大小当模型权重大小。

可运行参考答案

def lesson26():
    original=1000; conditioned=700
    rows=[]
    for side in [0,120,300,400]:
        total=conditioned+side; rows.append(dict(side=side,total=total,saving=original-total))
    assert rows[1]['saving']==180 and rows[2]['saving']==0
    print(rows)

相关工具实现

仅使用标准库或直接框架接口。

理解题与参考答案

1. 为什么先解码 z?

需要用 z 的信息得到解码 y 的概率模型。

2. 侧信息越多越好吗?

不一定,需要计入其自身编码成本。

3. 超先验直接传原图吗?

传的是潜变量分布的辅助表示,具体含义由模型学习。

验收:总成本包含两分支;能画出正确解码顺序

未通过时:返回本课先修内容,用较小输入逐步打印中间结果。

L27 因子化与超先验的实测比较

第 9 周 · 150 分钟

目标:在同一清单和指标下比较两个真实模型

先修:完成 L26 的自检;不熟悉时先返回上一课

组会联系:从读论文走到验证某种结构是否有用。

建议节奏:回顾 10 分钟 → 讲解与阅读 35 → 运行示例 25 → 独立练习 50 → 自检与记录 30。

使用 CompressAI 的 factorized 和 scale-hyperprior 预训练模型。选择相同图片和预处理,不把同质量编号当作同一码率。每种模型跑多个档位,再比较曲线重叠区间。

保存每个 stream 的大小,让侧信息成本可见。超先验可能主分支更省、总文件却未必更省;图片内容、工作点和头部开销都会影响结果。

论文结果与本实验不同并不必然说明代码错。先核对版本、权重、训练指标、RGB/YUV、测试集和 padding。若无法保证一致,写清“教学比较”而不是“复现论文全部结论”。

净收益 = B_factorized − (B_y + B_z + 对应容器开销)

两模型同为 quality=3 只能说明各自选了第三档,不能直接计算“同码率画质提升”。

资料

运行命令与预期

WSL Ubuntu(按环境指南指定 TORCH_HOME);代码依赖练习包中的 course_lab.py。

.venv-linux/bin/python python/answers/L27.py --images python/test-images

三种编码器至少 45 行结果,超先验的 side_bytes 单独保留。

独立练习

用同一五图清单跑两种模型,生成曲线与侧信息占比表。

提示 1:先检查结果 CSV 的 model 和 quality 字段。

提示 2:保留原图清单哈希,确保与第八周一致。

拓展:找一张超先验收益很小的图片,提出可检验的解释。

常见错误:汇总前确认图片没有漏跑,失败样本需要显式列出。

可运行参考答案

def lesson27():
    args=cli()
    if not args.images: raise ValueError('Pass --images with the same fixed test folder in Linux')
    path=benchmark(args.images,include_hyper=True)
    rows=read_csv(path); rd_svg(rows,OUT/'L27_RD.svg')
    for r in rows:
        if r['model']=='hyperprior': print(r['image'],r['quality'],'side bytes:',r['side_bytes'])

相关工具实现

def read_csv(path):
    with Path(path).open(encoding='utf-8',newline='') as f: return list(csv.DictReader(f))

def rd_svg(rows,path):
    from html import escape
    groups={}
    for r in rows:
        k=(r['model'],str(r['quality']))
        groups.setdefault(k,[]).append((float(r['file_bpp']),float(r['psnr'])))
    points={}
    for (model,q),vals in groups.items():
        points.setdefault(model,[]).append((statistics.mean(v[0] for v in vals),statistics.mean(v[1] for v in vals),q))
    flat=[v for values in points.values() for v in values]
    if not flat or not all(math.isfinite(a) and math.isfinite(b) for a,b,_ in flat): raise ValueError('finite measured results required')
    xmin=0; xmax=max(v[0] for v in flat)*1.1 or 1
    ymin=min(v[1] for v in flat)-2; ymax=max(v[1] for v in flat)+2
    sx=lambda x:65+x/xmax*530; sy=lambda y:285-(y-ymin)/(ymax-ymin)*225
    chunks=['<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 350"><rect width="760" height="350" fill="#fffdf7"/>',
      '<g font-family="sans-serif" font-size="12" fill="#1e2c2a"><path d="M65 45V285H595" fill="none" stroke="#1e2c2a"/>',
      '<text x="225" y="332">Full-file bpp (mean per image)</text><text x="12" y="25">PSNR / dB</text>']
    for i in range(5):
        x=xmax*i/4; y=ymin+(ymax-ymin)*i/4
        chunks += [f'<text x="{sx(x):.1f}" y="306">{x:.2f}</text>',f'<text x="15" y="{sy(y):.1f}">{y:.1f}</text>']
    for i,(model,values) in enumerate(points.items()):
        c=['#b94631','#315b6f','#446c5c'][i%3]; values.sort()
        coords=' '.join(f'{sx(x):.2f},{sy(y):.2f}' for x,y,q in values)
        chunks.append(f'<polyline points="{coords}" fill="none" stroke="{c}" stroke-width="2"/>')
        chunks.append(f'<text x="610" y="{65+i*25}" fill="{c}">{escape(model)}</text>')
        for x,y,q in values: chunks.append(f'<circle cx="{sx(x):.2f}" cy="{sy(y):.2f}" r="4" fill="{c}"/>')
    chunks.append('</g></svg>'); Path(path).write_text(''.join(chunks),encoding='utf-8')
    print(path)

def benchmark(folder,include_hyper=False):
    files=sorted(p for p in Path(folder).iterdir() if p.suffix.lower() in {'.png','.jpg','.jpeg'})
    if len(files)<5: raise ValueError('Provide at least five fixed, distinct test images')
    hashes=[hashlib.sha256(p.read_bytes()).hexdigest() for p in files]
    if len(set(hashes))!=len(hashes): raise ValueError('duplicate image files')
    emit('test_manifest.json',[dict(file=p.name,sha256=h) for p,h in zip(files,hashes)])
    rows=[]; models=['factorized']+(['hyperprior'] if include_hyper else [])
    for p in files:
        img=Image.open(p).convert('RGB')
        for q in [20,50,85]: rows.append(jpeg_row(img,q,p.name))
    for name in models:
        for q in [1,3,5]:
            model=load_codec(name,q)
            for i,p in enumerate(files):
                row=real_encode(Image.open(p).convert('RGB'),name,q,OUT/f'{i}_{p.stem}_{name}_q{q}.lic',model)
                row['image']=p.name; rows.append(row)
                csv_write('benchmark_partial.csv',rows)
    path=csv_write('benchmark.csv',rows)
    import importlib.metadata as im
    emit('benchmark_environment.json',dict(python=sys.version,torch=torch.__version__,compressai=im.version('compressai'),
      platform=platform.platform(),device='cpu',psnr='mean per image RGB range 0..1',bpp='full file / original pixels',
      padding='right/bottom zero to multiples of 64',jpeg_subsampling=0,models_shared=True,
      timing='compress/decompress only, excludes model load, file IO, forward rate estimation; single samples, not a latency benchmark'))
    return path

def cli():
    p=argparse.ArgumentParser()
    p.add_argument('--image'); p.add_argument('--images'); p.add_argument('--results'); p.add_argument('--bitstream')
    p.add_argument('--project',default=str(Path(__file__).resolve().parents[2]/'learned-image-compression-study'))
    p.add_argument('--hpcm',default=str(Path(__file__).resolve().parents[2]/'LIC-HPCM'))
    p.add_argument('--quality',type=int,default=1); p.add_argument('--model',choices=['factorized','hyperprior'],default='factorized')
    p.add_argument('--self-test',action='store_true'); p.add_argument('--cuda',action='store_true')
    return p.parse_args()

理解题与参考答案

1. 为什么需要多个点?

比较不同模型的率失真关系,而不是任意两个工作点。

2. 侧信息怎么报告?

另列 y、z 字节,同时给出总文件 bpp。

3. 能否直接复用论文表格?

不能,自己的实验必须来自实际输出。

验收:数据和评价口径一致;至少指出一种不能下结论的情况

未通过时:返回本课先修内容,用较小输入逐步打印中间结果。

L28 条件概率与上下文的因果约束

第 10 周 · 150 分钟

目标:判断某条上下文在解码时是否真的可用

先修:完成 L27 的自检;不熟悉时先返回上一课

组会联系:上下文模型不能偷看尚未解码的值。

建议节奏:回顾 10 分钟 → 讲解与阅读 35 → 运行示例 25 → 独立练习 50 → 自检与记录 30。

概率链式法则可把联合概率写成一系列条件概率乘积。编码器看到完整输入,但解码器只知道已经解码的符号和共享信息,因此条件上下文必须在解码当时可获得。

自回归模型依次解码,通常能利用丰富依赖,但串行步骤会增加延迟。分组或棋盘式方法用一部分并行性换取上下文限制,HPCM 的层级顺序也可以从这种权衡理解。

验证方法是给解码步骤编号,逐项列出其依赖。若第 2 步依赖第 4 步才知道的符号,就形成不可用上下文;编码器端实验好看也不能改变这个事实。

p(y₁,…,yₙ)=p(y₁)∏ᵢ₌₂ⁿp(yᵢ|y₁,…,yᵢ₋₁)

y2 总等于 y1 时,先传 y1 后 y2 的条件理想码长可以为 0;但双方必须共享该规则。

资料

运行命令与预期

课程根目录的 PowerShell;代码依赖练习包中的 course_lab.py。

& ..\learned-image-compression-study\.venv\Scripts\python.exe python/answers/L28.py

有效依赖通过,未来依赖被拒绝。

独立练习

写一个依赖检查器,验证有效顺序,并让未来依赖案例触发错误。

提示 1:维护 known 集合。

提示 2:每步 dependencies 必须是 known 的子集,再加入本步。

拓展:把可同时解码的节点分组,比较组数。

常见错误:不能用编码端真实 y 代替解码端可重建 y_hat。

可运行参考答案

def lesson28():
    def check(steps):
        known={'z'}
        for name,deps in steps:
            if not set(deps)<=known: raise ValueError(f'{name}: future dependency {set(deps)-known}')
            known.add(name)
        return known
    print(check([('y1',['z']),('y2',['z','y1'])]))
    try: check([('y1',['y2']),('y2',['z'])])
    except ValueError as e: print('correctly rejected:',e)
    else: raise AssertionError('leakage not detected')

相关工具实现

仅使用标准库或直接框架接口。

理解题与参考答案

1. 编码器能看整图,解码器也能吗?

不能,解码器只有码流和共享信息。

2. 条件越多必然更快吗?

不,依赖可能造成串行等待。

3. 如何检查信息泄漏?

列出每一步当时已知的变量,检查每条依赖。

验收:能拒绝未来依赖;解释并行性与预测准确性的取舍

未通过时:返回本课先修内容,用较小输入逐步打印中间结果。

L29 注意力:从矩阵乘法到特征汇总

第 10 周 · 150 分钟

目标:手算一个小型 QKᵀV 并检查形状

先修:完成 L28 的自检;不熟悉时先返回上一课

组会联系:HPCM 的跨步骤信息融合用到了注意力。

建议节奏:回顾 10 分钟 → 讲解与阅读 35 → 运行示例 25 → 独立练习 50 → 自检与记录 30。

Query 与 Key 的相似性决定从哪些 Value 聚合信息。注意力并不是挑出一个最重要位置,而通常给每个可见位置一个归一化权重,再做加权和。

Q 为 Nq×d、K 为 Nk×d、V 为 Nk×dv,则分数为 Nq×Nk、输出为 Nq×dv。softmax 沿 Key 轴归一化;轴写错时形状可能仍合法,却改变语义。

缩放 1/sqrt(d) 调节分数幅度。因果 mask 把不可见位置设为负无穷,再做 softmax;窗口注意力只在局部集合内计算,不能等同全局注意力。

Attention(Q,K,V)=softmax(QKᵀ/√d + mask)V

权重 [0.25,0.75]、Value 为 [2,6],加权结果为 5,不是直接选 6。

资料

运行命令与预期

课程根目录的 PowerShell;代码依赖练习包中的 course_lab.py。

& ..\learned-image-compression-study\.venv\Scripts\python.exe python/answers/L29.py

输出 [3,2];权重每行和为 1,未来位置为零。

独立练习

手写缩放点积注意力,验证权重和为 1、因果 mask 后未来权重为 0。

提示 1:先只用单头二维矩阵。

提示 2:输出形状由 Q 行数和 V 列数决定。

拓展:把 mask 改成固定窗口并比较结果。

常见错误:多头不是直接增加 batch;先理解单头再拆分维度。

可运行参考答案

def lesson29():
    seed(); q=torch.randn(3,4); k=torch.randn(3,4); v=torch.randn(3,2)
    scores=q@k.T/math.sqrt(q.shape[-1])
    mask=torch.triu(torch.ones(3,3,dtype=torch.bool),diagonal=1)
    weights=scores.masked_fill(mask,-torch.inf).softmax(dim=-1)
    out=weights@v
    assert out.shape==(3,2) and torch.allclose(weights.sum(-1),torch.ones(3))
    assert torch.all(weights[mask]==0)
    print('weights:',weights,'output:',out)

相关工具实现

def seed(value=42):
    torch.manual_seed(value)
    np.random.seed(value)

理解题与参考答案

1. softmax 对哪个维度?

对每个 Query 对应的 Key 维度。

2. 输出长度由谁决定?

由 Query 数量决定。

3. 全行都被 mask 会怎样?

softmax 可能产生 NaN,需要保证至少一个可见位置。

验收:权重归一化轴正确;未来位置严格不可见

未通过时:返回本课先修内容,用较小输入逐步打印中间结果。

L30 HPCM:论文图与代码互相验证

第 10 周 · 150 分钟

目标:解释分层编码和逐步融合的数据流

先修:完成 L29 的自检;不熟悉时先返回上一课

组会联系:连接你读过的论文及组内真实研究方向。

建议节奏:回顾 10 分钟 → 讲解与阅读 35 → 运行示例 25 → 独立练习 50 → 自检与记录 30。

先读论文方法图,再看本地 HPCM_Base.py 的 forward 与 forward_hpcm。记录 x、y、z、y_hat_so_far、context_next 和 x_hat 的职责;不要从所有模型变体同时开始。

分层掩码选择与重排不是普通插值缩图。已解码位置回填后成为后续步骤的条件。使用本地代码阅读指南定位函数,但行号可能随版本变化,最终以当前源码中的函数与调用为准。

Base 与 PhiContext 等变体不能混为同一实现。记录当前 commit、类名与实际新增编码步骤,并检查注意力 Query/Key/Value 来源。初次目标是读懂依赖,而不是在笔记本上从头训练论文模型。

已知侧信息 + 已重建潜变量 → 当前分布参数 → 当前解码 → 更新上下文

本地阅读指南指出某版本新增步骤为 2+3+6;必须回到当前 mask 切片验证,不能只数注释。

资料

  • HPCM 官方代码:src/models/HPCM_Base.py:forward 与 forward_hpcm,30 分钟。

运行命令与预期

课程根目录的 PowerShell;代码依赖练习包中的 course_lab.py。

& ..\learned-image-compression-study\.venv\Scripts\python.exe python/answers/L30.py

显示当前 commit 与函数定位;不会执行原论文模型。

独立练习

运行源码定位脚本,写出 forward 的六步数据流,并标注每步解码端已知信息。

提示 1:先定位函数名,不依赖旧行号。

提示 2:对照第 28 课依赖检查原则。

拓展:比较 Base 与 PhiContext 的 context_next 更新方式。

常见错误:仓库含 Linux 原生扩展及旧依赖,不直接装入现有 Windows 教学环境。

可运行参考答案

def lesson30():
    args=cli(); root=Path(args.hpcm); p=root/'src'/'models'/'HPCM_Base.py'
    if not p.exists(): raise FileNotFoundError(f'{p}: use --hpcm with repository path')
    import subprocess
    result=subprocess.run(['git','-C',str(root),'rev-parse','HEAD'],capture_output=True,text=True)
    print('commit:',result.stdout.strip() or 'unavailable')
    for i,line in enumerate(p.read_text(encoding='utf-8').splitlines(),1):
        if any(k in line for k in ['def forward','context_next','y_hat_so_far','self.g_a(x)','self.h_a(y)']): print(i,line.strip())
    print('Static code reading only; original model has not been executed.')

相关工具实现

def cli():
    p=argparse.ArgumentParser()
    p.add_argument('--image'); p.add_argument('--images'); p.add_argument('--results'); p.add_argument('--bitstream')
    p.add_argument('--project',default=str(Path(__file__).resolve().parents[2]/'learned-image-compression-study'))
    p.add_argument('--hpcm',default=str(Path(__file__).resolve().parents[2]/'LIC-HPCM'))
    p.add_argument('--quality',type=int,default=1); p.add_argument('--model',choices=['factorized','hyperprior'],default='factorized')
    p.add_argument('--self-test',action='store_true'); p.add_argument('--cuda',action='store_true')
    return p.parse_args()

理解题与参考答案

1. mask 重排等于缩放吗?

不是,选择位置并重排与插值会产生不同数据。

2. 为什么保存 commit?

论文代码会变,保证阅读和实验对象可定位。

3. 本课要求跑完整论文吗?

不要求,重点是静态数据流与解码依赖验证。

验收:区分两种模型变体;每个上下文都有可用性解释

未通过时:返回本课先修内容,用较小输入逐步打印中间结果。

L31 潜变量量化与网络量化

第 11 周 · 150 分钟

目标:理解 PTQ、尺度、截断与伪量化

先修:完成 L30 的自检;不熟悉时先返回上一课

组会联系:组会整数化 LIC 同时涉及编码表示与计算精度。

建议节奏:回顾 10 分钟 → 讲解与阅读 35 → 运行示例 25 → 独立练习 50 → 自检与记录 30。

潜变量量化决定传输符号;网络量化用整数近似权重和激活。后者通常引入 scale、zero-point、有限位宽与截断。两种量化可以同时存在,但目标和误差传播路径不同。

对称量化近似 x=sq,非对称量化还包含零点。伪量化执行量化再反量化,输出仍可能是浮点,方便模拟误差;它不证明实际卷积已使用整数指令,也不保证速度提高。

静态尺度由校准数据决定,遇到异常值可能饱和。逐通道尺度能减少部分误差,但整数算子必须处理尺度对齐。不要只把 tensor 转成 int8 就认为完成部署。

q=clip(round(x/s)+z,qmin,qmax);x_hat=s(q−z)

s=0.1,z=0,x=0.26 得 q=3、重建 0.3;若上限 qmax=2,则饱和到 0.2。

资料

  • Learn the Basics:Tensors:dtype 与数值类型;本课原创数值实验,20 分钟。

运行命令与预期

课程根目录的 PowerShell;代码依赖练习包中的 course_lab.py。

& ..\learned-image-compression-study\.venv\Scripts\python.exe python/answers/L31.py

生成本课 outputs 文件并通过代码中的断言;具体数值随训练与硬件变化,不以固定准确率作为承诺。

独立练习

比较 4、8、12 位模拟量化的 MSE 和饱和比例,明确标为误差模拟。

提示 1:先固定 scale 计算规则。

提示 2:使用同一组包含异常值的数据。

拓展:比较全张量和逐通道尺度,但不声称得到真实加速。

常见错误:低位宽整数乘法可能溢出,累加位宽要单独分析。

可运行参考答案

def lesson31():
    x=torch.tensor([-3.,-.9,-.1,0.,.26,.9,3.])
    for bits in [4,8,12]:
        qmax=2**(bits-1)-1; scale=1/qmax # calibrated range [-1,1], deliberate outliers
        q=(x/scale).round().clamp(-qmax,qmax); restored=q*scale
        print(bits,'MSE:',float(F.mse_loss(restored,x)),'saturated:',float((x.abs()>1).float().mean()))
    print('Fake quantization simulation; NOT an integer kernel speed benchmark.')

相关工具实现

仅使用标准库或直接框架接口。

理解题与参考答案

1. fake quant 是真实整数卷积吗?

不是,它可能仍用浮点计算。

2. 校准数据有什么作用?

帮助确定激活范围与量化尺度。

3. 位宽变低一定更快吗?

不一定,取决于内核、硬件、转换和访存成本。

验收:同时报告误差和饱和;明确结果不代表部署耗时

未通过时:返回本课先修内容,用较小输入逐步打印中间结果。

L32 整数计算与编解码一致性

第 11 周 · 150 分钟

目标:检查舍入、溢出和共享概率表

先修:完成 L31 的自检;不熟悉时先返回上一课

组会联系:跨设备解码依赖可复现的概率决策。

建议节奏:回顾 10 分钟 → 讲解与阅读 35 → 运行示例 25 → 独立练习 50 → 自检与记录 30。

熵编码器和解码器需要一致的整数 CDF 和符号映射。如果某个概率预测因浮点差异跨过离散决策边界,后续解码可能失配。不能笼统说任何编码器潜变量舍入差异都会让解码器失效:需指出双方必须共享的计算具体在哪里。

整数化要同时规定乘法、累加、移位、舍入和饱和。仅把两个操作数位数相加不足以证明整个表达式安全,偏置、多个乘积的累加以及舍入常数都可能增加范围。

右移负数的规则与取整方式不能凭直觉等同。先用 Python 大整数计算理论范围,再验证目标位宽;本课是数值边界检查,不替代跨硬件真实码流一致性测试。

累加绝对值上界 ≤ K·max|a|·max|b| + |bias| + rounding_offset

100 个 int8 乘积即使每个乘积能放进 int16,总和也可能超过 int16。

资料

运行命令与预期

课程根目录的 PowerShell;代码依赖练习包中的 course_lab.py。

& ..\learned-image-compression-study\.venv\Scripts\python.exe python/answers/L32.py

K=200000 时上界超过 int32;显示负数取整差异。

独立练习

计算不同 K 下累加范围,判断是否能放入有符号 32 位,并测试负数舍入。

提示 1:用 Python int 保存上界。

提示 2:对照 −2^31 到 2^31−1。

拓展:构造两个相邻浮点数落在不同量化格的例子,说明发生在哪一决策环节。

常见错误:边界判断不能先用 numpy.int32 计算上界,否则可能已溢出。

可运行参考答案

def lesson32():
    limit=2**31-1
    for count in [1,100,200000]:
        upper=count*128*128+100+64
        print('K,bound,fits_int32:',count,upper,upper<=limit)
    assert 200000*128*128>limit
    print('negative rounding: floor division',-3//2,'ties-to-even round',round(-3/2))

相关工具实现

仅使用标准库或直接框架接口。

理解题与参考答案

1. INT12×INT20 就能证明全部 INT32 安全?

不能,还要分析有符号端点、累加、偏置和取整偏移。

2. 一致性关键是什么?

双方用于熵编解码的 CDF 与符号规则一致。

3. Python 整数为何适合算边界?

可避免固定宽度整数先溢出再检查的错误。

验收:能找出一个溢出案例;不把本课测试称为跨设备认证

未通过时:返回本课先修内容,用较小输入逐步打印中间结果。

L33 复杂度、吞吐与可靠计时

第 11 周 · 150 分钟

目标:分别记录参数量、延迟和测量范围

先修:完成 L32 的自检;不熟悉时先返回上一课

组会联系:低复杂度模型的价值需要可靠测量支持。

建议节奏:回顾 10 分钟 → 讲解与阅读 35 → 运行示例 25 → 独立练习 50 → 自检与记录 30。

参数量描述存储规模,MACs 描述特定输入下的乘加数量,延迟描述实际系统时间,三者不能互相替代。内存访问、并行性和算子支持都会影响实际速度。

GPU 操作通常异步,直接用 CPU 时钟包住调用可能只量到提交任务时间。预热后同步,再用重复测量报告中位数及范围;本课 CPU 默认可运行,CUDA 则显式同步。

模型 forward、熵编码、文件 I/O 是不同计时范围。预训练权重下载和初始化通常单独报告,不混进稳定推理延迟。比较时固定 batch、尺寸、精度与设备。

batch=1 时近似 FPS=1/每帧秒数;批量吞吐=样本数/总时间

10 ms/帧约为 100 FPS,但若不含熵编码,不能称完整 codec 达到 100 FPS。

资料

  • CUDA semantics:Asynchronous execution 与 CUDA 同步,25 分钟。

运行命令与预期

课程根目录的 PowerShell;代码依赖练习包中的 course_lab.py。

& ..\learned-image-compression-study\.venv\Scripts\python.exe python/answers/L33.py

生成本课 outputs 文件并通过代码中的断言;具体数值随训练与硬件变化,不以固定准确率作为承诺。

独立练习

测一个小 CNN 的参数量和多次推理时间,报告中位数并保存测量配置。

提示 1:用 perf_counter。

提示 2:计时前后同步,测量期间使用 eval 与 inference_mode。

拓展:改变 batch size,分别报告延迟与吞吐。

常见错误:不在计时循环里 print 或保存图片,除非目标就是端到端 I/O。

可运行参考答案

def lesson33():
    args=cli(); device='cuda' if args.cuda else 'cpu'
    if device=='cuda' and not torch.cuda.is_available(): raise RuntimeError('CUDA requested but unavailable')
    seed(); model=classifier(cnn=True).to(device).eval(); x=stripes(1)[0].to(device)
    sync=lambda: torch.cuda.synchronize() if device=='cuda' else None
    times=[]
    with torch.inference_mode():
        for _ in range(5): model(x)
        for _ in range(20):
            sync(); start=time.perf_counter(); model(x); sync(); times.append(time.perf_counter()-start)
    record=dict(device=device,shape=list(x.shape),parameters=sum(p.numel() for p in model.parameters()),
      median_seconds=statistics.median(times),min_seconds=min(times),max_seconds=max(times),scope='forward only; no codec or I/O',threads=torch.get_num_threads())
    print(record); emit('L33.json',record)

相关工具实现

def seed(value=42):
    torch.manual_seed(value)
    np.random.seed(value)

def emit(name, value):
    path=OUT/name
    path.write_text(json.dumps(value,ensure_ascii=False,indent=2,allow_nan=False),encoding='utf-8')
    print(path)
    return path

def stripes(n=64, noise=.15, random_seed=42):
    g=torch.Generator().manual_seed(random_seed)
    labels=torch.arange(n)%2
    x=torch.zeros(n,1,16,16)
    x[labels==0,0,::2,:]=1
    x[labels==1,0,:,::2]=1
    x=(x+noise*torch.randn(x.shape,generator=g)).clamp(0,1)
    return x,labels.long()

def classifier(cnn=False):
    if cnn:
        return nn.Sequential(nn.Conv2d(1,4,3,padding=1),nn.ReLU(),nn.Flatten(),nn.Linear(4*256,2))
    return nn.Sequential(nn.Flatten(),nn.Linear(256,16),nn.ReLU(),nn.Linear(16,2))

def cli():
    p=argparse.ArgumentParser()
    p.add_argument('--image'); p.add_argument('--images'); p.add_argument('--results'); p.add_argument('--bitstream')
    p.add_argument('--project',default=str(Path(__file__).resolve().parents[2]/'learned-image-compression-study'))
    p.add_argument('--hpcm',default=str(Path(__file__).resolve().parents[2]/'LIC-HPCM'))
    p.add_argument('--quality',type=int,default=1); p.add_argument('--model',choices=['factorized','hyperprior'],default='factorized')
    p.add_argument('--self-test',action='store_true'); p.add_argument('--cuda',action='store_true')
    return p.parse_args()

理解题与参考答案

1. MACs 少一定快吗?

不一定,硬件利用率与访存可能主导。

2. 为什么预热?

减少初始化与首次算子选择等一次性开销。

3. 为什么同步 CUDA?

等待异步任务完成,避免只计到发射时间。

验收:记录设备、形状、范围;不把 forward 时间冒充完整编解码时间

未通过时:返回本课先修内容,用较小输入逐步打印中间结果。

L34 把组会研究线放回共同框架

第 12 周 · 150 分钟

目标:分清数据对象、冗余来源、成本和指标

先修:完成 L33 的自检;不熟悉时先返回上一课

组会联系:为以后选择具体课题建立比较维度。

建议节奏:回顾 10 分钟 → 讲解与阅读 35 → 运行示例 25 → 独立练习 50 → 自检与记录 30。

视频压缩还利用时间冗余,需要理解参考帧、运动预测、残差和误差传播;码率控制在总预算下分配各帧资源。它不等于每帧使用同一质量参数。

点云压缩面对三维位置与属性,八叉树占用表示描述哪些空间子块有点;几何失真不等于二维像素 PSNR。3D Gaussian Splatting 则用可渲染场景表示生成视图,存储对象与评价方式又不同。

MoE、低复杂度和整数化是可能跨对象复用的思路,但具体实验约束不同。建立四列地图:输入对象、利用的冗余、传输内容、评价指标。不要根据一个缩写猜作者的精确定义。

研究问题 = 数据对象 + 可利用结构 + 资源约束 + 评价方法

视频关心帧间关联;点云关心空间占用;3D 场景表示还要测渲染质量和模型体积。

资料

运行命令与预期

课程根目录的 PowerShell;代码依赖练习包中的 course_lab.py。

& ..\learned-image-compression-study\.venv\Scripts\python.exe python/answers/L34.py

预算分配 [2000,1000,500,500],总计 4000。

独立练习

生成研究线比较表,再用四帧复杂度做一次简单的比例码率分配。

提示 1:总分配必须等于预算。

提示 2:比例分配只是教学启发式,不是最优码控算法。

拓展:从最近一次组会补一条研究线并标记尚未确认的信息。

常见错误:组会总结是线索;具体技术结论以原报告和公开论文为准。

可运行参考答案

def lesson34():
    complexity=[4,2,1,1]; budget=4000
    bits=[budget*c//sum(complexity) for c in complexity]; bits[-1]+=budget-sum(bits)
    assert sum(bits)==budget
    print('heuristic allocation, NOT optimal rate control:',bits)
    rows=[dict(object='image',redundancy='spatial',metric='RGB PSNR / bpp'),
      dict(object='video',redundancy='temporal + spatial',metric='quality / bitrate / FPS'),
      dict(object='point cloud',redundancy='3D occupancy',metric='geometry distortion / bits per point'),
      dict(object='3D scene',redundancy='shared scene',metric='rendering quality / storage')]
    csv_write('L34_research_map.csv',rows)

相关工具实现

def csv_write(name, rows):
    if not rows: raise ValueError('empty results')
    path=OUT/name
    with path.open('w',newline='',encoding='utf-8') as f:
        w=csv.DictWriter(f,fieldnames=list(rows[0])); w.writeheader(); w.writerows(rows)
    print(path)
    return path

理解题与参考答案

1. 同一个 PSNR 能跨任务直接比较吗?

通常不能,误差定义、范围和数据对象可能不同。

2. 视频平均分 bits 最优吗?

不一定,各帧复杂度和参考影响不同。

3. 缩写不认识怎么办?

查报告定义或询问汇报者,不凭扩写猜算法。

验收:分清四类对象;码率分配守恒且不声称最优

未通过时:返回本课先修内容,用较小输入逐步打印中间结果。

L35 完成一次单变量小实验

第 12 周 · 150 分钟

目标:提前写假设,再用固定设置检验

先修:完成 L34 的自检;不熟悉时先返回上一课

组会联系:把“我想试试”转成能回答的问题。

建议节奏:回顾 10 分钟 → 讲解与阅读 35 → 运行示例 25 → 独立练习 50 → 自检与记录 30。

本课默认问题是:在固定种子、数据和训练预算下,把小自编码器瓶颈通道从 4 改到 8,会怎样影响重建 MSE、参数量和 forward 耗时。它研究教学重建模型,不声称改善真实压缩率。

先记录假设与评价方法,再运行。其余设置保持一致,使用三个种子并报告逐次结果。若要改研究问题,先写出变量与控制条件,而不是跑完后再挑最好看的解释。

统计差异不自动解释机制。更宽模型效果好可能来自容量、优化或其他因素;后续可再改变训练预算去检验,但那应成为下一次独立实验。

A: latent_channels=4;B: latent_channels=8;其他设置固定

若 B 误差更低但耗时更长,结论应同时报告收益与代价。

资料

运行命令与预期

课程根目录的 PowerShell;代码依赖练习包中的 course_lab.py。

& ..\learned-image-compression-study\.venv\Scripts\python.exe python/answers/L35.py

输出两种通道数、三个种子的六行结果,含测试 MSE、参数数目和 CPU forward 中位时间。

独立练习

运行六组小实验并保存 CSV;写一段只由这些数据支持的结论。

提示 1:固定训练图、测试图、步数、优化器。

提示 2:按配置和种子记录每行,不只输出平均。

拓展:用同一记录模板设计一个真实 codec 的后续问题,但不混入本次结论。

常见错误:若训练未收敛,注明预算限制,不能把差异都解释为结构优劣。

可运行参考答案

def lesson35():
    rows=[]
    for random_seed in [11,22,33]:
        for channels in [4,8]:
            model,record=fit_autoencoder(channels=channels,steps=40,random_seed=random_seed)
            test=picture(random_seed=99)
            model.eval()
            with torch.inference_mode():
                for _ in range(5): model(test)
                timings=[]
                for _ in range(20):
                    t0=time.perf_counter()
                    pred=model(test)
                    timings.append((time.perf_counter()-t0)*1000)
                error=float(F.mse_loss(pred,test))
            rows.append(dict(channels=channels,seed=random_seed,steps=40,train_mse=record['final_mse'],
              test_mse=error,parameters=sum(p.numel() for p in model.parameters()),
              forward_median_ms=statistics.median(timings),device='cpu',warmup=5,timed_runs=20,synthetic=True))
    csv_write('L35_ablation.csv',rows)
    print('Different synthetic noise realization; not a diverse natural-image generalization benchmark.')

相关工具实现

def picture(size=32, random_seed=42):
    # Synthetic calibration image, not a natural-image benchmark.
    g=torch.Generator().manual_seed(random_seed)
    v=torch.linspace(0,1,size)
    yy,xx=torch.meshgrid(v,v,indexing='ij')
    rgb=torch.stack([xx,yy,.5+.4*torch.sin(xx*12)*torch.cos(yy*9)])
    return (rgb+.02*torch.randn(rgb.shape,generator=g)).clamp(0,1).unsqueeze(0)

def csv_write(name, rows):
    if not rows: raise ValueError('empty results')
    path=OUT/name
    with path.open('w',newline='',encoding='utf-8') as f:
        w=csv.DictWriter(f,fieldnames=list(rows[0])); w.writeheader(); w.writerows(rows)
    print(path)
    return path

def fit_autoencoder(channels=8, steps=60, random_seed=42):
    seed(random_seed); model=Autoencoder(channels); x=picture()
    opt=torch.optim.Adam(model.parameters(),lr=.01)
    initial=float(F.mse_loss(model(x),x).detach())
    old=[p.detach().clone() for p in model.parameters()]
    for _ in range(steps):
        loss=F.mse_loss(model(x),x); assert torch.isfinite(loss)
        opt.zero_grad(); loss.backward(); opt.step()
    model.eval()
    with torch.inference_mode(): out=model(x); final=float(F.mse_loss(out,x))
    assert final<initial, (initial,final)
    assert any(not torch.equal(p,q) for p,q in zip(old,model.parameters()))
    return model,dict(initial_mse=initial,final_mse=final,steps=steps,seed=random_seed,synthetic=True,train_test_overlap=True)

理解题与参考答案

1. 为什么先写假设?

避免根据结果不断改变原问题而不自知。

2. 三个种子足够证明普遍性吗?

不足,只是比单次更能观察稳定性。

3. 没提升如何处理?

保留结果,检查设置,再说明假设未获支持。

验收:只有一个主要变量;六组结果全部保留

未通过时:返回本课先修内容,用较小输入逐步打印中间结果。

L36 总结、汇报与下一步选题

第 12 周 · 150 分钟

目标:用证据说明学会了什么、还不确定什么

先修:完成 L35 的自检;不熟悉时先返回上一课

组会联系:把见习准备变成可讨论的具体能力和兴趣。

建议节奏:回顾 10 分钟 → 讲解与阅读 35 → 运行示例 25 → 独立练习 50 → 自检与记录 30。

最终汇报分两部分:真实码流基线说明你理解完整评测链路;单变量实验说明你能控制条件并解释结果。二者数据集和目的不同,分别陈述,不合成一个夸大的“新方法”。

建立能力清单:独立读代码、运行、排错、测量和解释。对每项给出一个文件或实验作为证据。尚未跑通的 WSL/CompressAI 实验标记阻塞,不用阅读笔记替代运行证据。

联系老师时给出短报告和一至两个问题,例如想深入熵模型还是效率部署,以及希望从什么基础任务开始。选题还需要结合带教安排、算力、时间与组内正在进行的项目。

问题 → 设置 → 数据 → 结论 → 限制 → 下一步

“我能重载码流并解释三种 bpp 的差别”是能力证据;“我看完了很多论文”还不够具体。

资料

运行命令与预期

课程根目录的 PowerShell;代码依赖练习包中的 course_lab.py。

& ..\learned-image-compression-study\.venv\Scripts\python.exe python/answers/L36.py

生成最终报告模板;不会自动填入尚未发生的实验结果。

独立练习

生成最终报告模板,填入真实文件路径、两张关键图、三条结论和两条未解决问题。

提示 1:用 L24 和 L35 的输出作为证据。

提示 2:分别列已完成、待复习、环境阻塞。

拓展:准备五分钟汇报,邀请同学只根据你的说明重跑一个实验。

常见错误:模板、示例数据和真实结果必须分清;不得把待填写项当已验证结论。

可运行参考答案

def lesson36():
    template='''# 十二周学习总结

## 能力与证据
分别列出:训练循环、保存加载、图像重建、真实码流、评价指标、论文代码阅读。
每项附一个真实文件路径;未完成或受阻的项明确标记。

## 实验一:真实压缩基线
问题 / 测试集及哈希清单 / 模型版本 / 设置 / CSV / RD 图 / 结论 / 限制。

## 实验二:单变量实验
预先假设 / 唯一变量 / 固定条件 / 每个种子结果 / 观察 / 替代解释。

## 尚未解决的问题
写两个具体问题,并给出已尝试的排查。

## 下一步与指导需求
说明感兴趣的方向、可投入时间、希望得到的基础任务或反馈。
'''
    path=OUT/'L36_final_report.md'; path.write_text(template,encoding='utf-8'); print(path)
    print('Template generated. Fill using your real results; generation does not mark the course complete.')

相关工具实现

仅使用标准库或直接框架接口。

理解题与参考答案

1. 报告需要全是好结果吗?

不需要,可信的失败和限制也是进展。

2. 能承诺八周发论文吗?

不能,本课程目标是科研基本能力。

3. 下一步选题凭什么?

兴趣、基础、可用指导与资源共同决定。

验收:结论有证据且限制明确;下一步是一个可执行问题

未通过时:返回本课先修内容,用较小输入逐步打印中间结果。

本机运行指南

Windows 基础课

已检查的解释器为 `E:\Projects\research\learned-image-compression-study\.venv\Scripts\python.exe`,PyTorch 2.11.0+cu128,CUDA 可用,显卡 RTX 3060 Laptop 6GB。此环境不含 torchvision 或 matplotlib。本课程离线示例使用 torch、numpy、Pillow 和标准库,绘图生成 SVG,不要求额外安装这两项。

下载练习包后解压,保留目录结构。以下 PowerShell 命令在课程根目录执行:

cd E:\Projects\research\pytorch-learning-plan
& ..\learned-image-compression-study\.venv\Scripts\python.exe python\answers\L01.py

练习版在 `python\exercises`,答案在 `python\answers`。两者使用共享工具 `python\course_lab.py`,应下载整个练习包。练习中的 TODO 会主动报错,防止未填写就误认为完成。小数据由代码生成,标注为教学合成数据。输出位于 `python\outputs`,重复执行会覆盖该课同名输出;正式比较应先复制到带日期的实验目录。

`course_lab.py` 中包括训练循环、图像转换和指标函数。课程页的“相关工具实现”会显示本课用到的关键函数;不要只运行入口而跳过它们。

WSL 现状与进入方式

2026-09-23 已核实:发行版 `MyMigratedUbuntu` 使用 WSL2,Python 3.12.3;课程独立环境已装入 torch 2.11.0+cpu 和 CompressAI 1.2.8。WSL 提示 Windows localhost 代理未镜像,但安装与编解码已经成功。

PowerShell 中检查、进入:

wsl --status
wsl --list --verbose
wsl -d MyMigratedUbuntu

仅当另一台机器未安装 WSL 时,参照微软文档,在管理员 PowerShell 运行 `wsl --install -d Ubuntu`,按提示重启并创建 Linux 用户。不要为了本课重复安装已有发行版。

独立 Linux 环境

下面的命令在 Ubuntu shell 执行,不在 PowerShell 执行:

cd /mnt/e/Projects/research/pytorch-learning-plan
bash python/setup_linux.sh
.venv-linux/bin/python python/answers/L19.py

脚本仅在课程目录创建 `.venv-linux`,锁定 torch 2.11.0、torchvision 0.26.0、CompressAI 1.2.8,以 CPU 推理为基础。`downloads/linux-environment-lock.txt` 在安装成功后记录实际依赖。若 venv 提示缺少 ensurepip,需要先由你在 Ubuntu 配置 python3-venv;不要向系统 Python 使用 sudo pip。网络、证书或软件源报错应保留完整日志,不以跳过 TLS 校验解决。

实际验证范围以网页实验区“验证记录”为准。Linux 脚本不自动安装系统组件。

第一次真实编解码

先下载 Kodak 01~05,并在 WSL 中指定课程已核验的权重缓存。新机器若没有权重,可以运行 Windows Python 的 `python/download_codec_weights.py`(在线下载约 100 MB),下载器校验官方文件名包含的 SHA256 前缀。

export TORCH_HOME=/mnt/e/Projects/research/pytorch-learning-plan/python/weights
.venv-linux/bin/python python/answers/L20.py --image python/test-images/kodim01.png
.venv-linux/bin/python python/answers/L21.py --bitstream python/outputs/L20.lic

若权重缓存为空,CompressAI 会从官方地址下载公开预训练权重。脚本会生成真实 `.lic` 教学容器并从落盘字节解码;原图尺寸、码流分支、潜变量形状、模型与质量档位都写入头部。解码双方默认共享同一模型权重。重建图片只用于显示,PNG 文件大小不用于计算压缩率。

五张固定图片与基准实验

使用下载工具获取 Kodak 01~05;这组图只用于评测,不用于本课程正式训练。工具会检查图片尺寸并生成 SHA256 清单;下载失败不会伪造替代数据。

.venv-linux/bin/python python/download_test_images.py
.venv-linux/bin/python python/answers/L22.py --images python/test-images
.venv-linux/bin/python python/answers/L23.py --results python/outputs/benchmark.csv
.venv-linux/bin/python python/answers/L24.py --results python/outputs/benchmark.csv
# 第九周:增加超先验,再做同口径比较
.venv-linux/bin/python python/answers/L27.py --images python/test-images

JPEG 固定 subsampling=0;档位 20/50/85。学习式模型档位 1/3/5,输入 RGB 范围 0~1,右侧与底部零填充到 64 倍数,解码后裁剪;以原始像素数为 bpp 分母。汇总取逐图 bpp 和逐图 PSNR 的算术平均。主比较使用完整文件 bpp,另外保留 payload、estimated bpp 和 side_bytes。

`benchmark_partial.csv` 保存已完成项,完整结束才写 `benchmark.csv`。失败时保留异常,不把部分数据当完整比较。报告要说明小样本局限、padding 策略以及模型共享约定。编码计时不包含文件写入、初始化、权重下载和估计率前向;单次计时仅是运行记录,严谨效率比较用 L33 的重复测量方式。

本地教学项目与 HPCM

L16 默认读取相邻的 `learned-image-compression-study`;下载包放到其他目录时传入 `--project`。L30 同样可用 `--hpcm` 指定 HPCM 源码位置。它们只做静态定位,不安装或运行原论文仓库。

查看已有教学模型的率失真训练:

cd E:\Projects\research\learned-image-compression-study
.\.venv\Scripts\lic-train-toy.exe --help
.\.venv\Scripts\lic-eval-toy.exe --help

先读 README 与 `docs/experiment_log.md`,再按实际参数执行。当前教学模型未接熵编码,其 bpp 是估计值。已有 Kodak 单图训练结果只作调试,不能并入新的独立测试报告。

遇到问题时

1. 环境:打印 sys.executable、torch.__version__ 和 device,确认不是混用解释器。

2. 数据:打印 shape、dtype、min/max,确认 NCHW 与 0~1。

3. 梯度:检查 requires_grad、有限梯度和更新前后的参数差。

4. 指标:检查原始 H/W、所有码流分支、像素范围和完整文件口径。

5. Linux 阻塞:保留命令及报错,在课程中标记“环境阻塞”;其他课程仍可继续。

来源:PyTorch 官方文档、CompressAI 官方发布与安装说明、Microsoft WSL 安装文档。命令和版本需结合验证记录使用。