十二周完整教案
包含全部正文、代码与答案。代码统一以下载包为准。
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.pycount=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 分钟。
- 动手学深度学习:2.1 数据操作,20 分钟。
运行命令与预期
课程根目录的 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。
资料
- Learn the Basics:Automatic Differentiation 与 Optimization,30 分钟。
- 动手学深度学习:3.1 线性回归;2.5 自动微分,25 分钟。
运行命令与预期
课程根目录的 PowerShell;代码依赖练习包中的 course_lab.py。
& ..\learned-image-compression-study\.venv\Scripts\python.exe python/answers/L03.pyw 接近 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,而不是三个相同长度的批次。
资料
- Learn the Basics:Datasets & DataLoaders,30 分钟。
运行命令与预期
课程根目录的 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。
资料
- Learn the Basics:Build the Neural Network;Optimization,35 分钟。
运行命令与预期
课程根目录的 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 个参数。
资料
- 动手学深度学习:6.2 图像卷积;6.3 填充和步幅;6.4 多通道,35 分钟。
运行命令与预期
课程根目录的 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 提醒你检查过拟合,但还要检查两者预处理是否一致。
资料
- 动手学深度学习:4.4 过拟合;6.6 LeNet,30 分钟。
运行命令与预期
课程根目录的 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。
资料
- Pillow 图像文件格式:JPEG:quality、subsampling 与保存参数,20 分钟。
运行命令与预期
课程根目录的 PowerShell;代码依赖练习包中的 course_lab.py。
& ..\learned-image-compression-study\.venv\Scripts\python.exe python/answers/L09.pyCSV 含三个档位;实际 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 个值;元素压缩比不等于文件压缩比。
资料
- End-to-end Optimized Image Compression:摘要与模型总览图,先辨认分析、量化、合成三部分,20 分钟。
运行命令与预期
课程根目录的 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。
资料
- End-to-end Optimized Image Compression:方法中 additive uniform noise 与量化近似部分,25 分钟。
运行命令与预期
课程根目录的 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 分钟。
- Variational Image Compression with a Scale Hyperprior:量化分布与卷积均匀分布的定义,20 分钟。
运行命令与预期
课程根目录的 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。
资料
- End-to-end Optimized Image Compression:率项与熵模型部分,25 分钟。
- 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,同一 λ 的含义完全不同。
资料
- CompressAI 训练与编解码教程:RateDistortionLoss 与训练循环,30 分钟。
运行命令与预期
课程根目录的 PowerShell;代码依赖练习包中的 course_lab.py。
& ..\learned-image-compression-study\.venv\Scripts\python.exe python/answers/L15.pyR=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 只能证明保存了模型,都不等于图像码流。
资料
- CompressAI 训练与编解码教程:训练模型、更新概率表、compress/decompress 的区别,25 分钟。
运行命令与预期
课程根目录的 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 不同,是两个因素变化;这不能证明某一个因素造成提升。
资料
- Learn the Basics:Optimization:梯度清零、反传和更新,25 分钟。
运行命令与预期
课程根目录的 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,不会让它成为独立测试样本。
资料
- 动手学深度学习:4.4 模型选择、欠拟合和过拟合,25 分钟。
运行命令与预期
课程根目录的 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 分钟。
- CompressAI 源码与示例:Installation:原生扩展与预编译包,20 分钟。
运行命令与预期
课程根目录的 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。
资料
- CompressAI 预训练模型:bmshj2018_factorized 和输入约定,25 分钟。
- CompressAI 训练与编解码教程:compress 与 decompress 示例,20 分钟。
运行命令与预期
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。
资料
- CompressAI 训练与编解码教程:实际编码与 likelihood 估计的 API,25 分钟。
运行命令与预期
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 图的逐图平均会给二者相同权重,而像素加权不同。
资料
- CompressAI 源码与示例:Evaluation 和 examples/codec.py,25 分钟。
- Kodak 无损彩色图像集:测试图片与来源说明,15 分钟。
运行命令与预期
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 在高码率更好时,应描述交叉区间,而不是只报平均值。
资料
- CompressAI 预训练模型:预训练质量档与评价说明,20 分钟。
运行命令与预期
课程根目录的 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 强”更可检验。
资料
- Variational Image Compression with a Scale Hyperprior:Experiments 与结论:观察作者如何限定比较条件,25 分钟。
运行命令与预期
课程根目录的 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;若它们始终相同,独立模型没有利用这份冗余。
资料
- End-to-end Optimized Image Compression:模型总览、目标函数、量化近似、实验图各读一遍,40 分钟。
运行命令与预期
课程根目录的 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。
资料
- Variational Image Compression with a Scale Hyperprior:图 2、超先验模型与总率项,40 分钟。
运行命令与预期
课程根目录的 PowerShell;代码依赖练习包中的 course_lab.py。
& ..\learned-image-compression-study\.venv\Scripts\python.exe python/answers/L26.pyside=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 只能说明各自选了第三档,不能直接计算“同码率画质提升”。
资料
- CompressAI 预训练模型:bmshj2018_factorized 与 bmshj2018_hyperprior,30 分钟。
运行命令与预期
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;但双方必须共享该规则。
资料
- Variational Image Compression with a Scale Hyperprior:超先验的解码依赖,20 分钟。
- Learned Image Compression with Hierarchical Progressive Context Modeling:层级编码顺序与上下文结构,25 分钟。
运行命令与预期
课程根目录的 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。
资料
- 动手学深度学习:10.3 注意力评分;10.5 多头注意力;10.6 自注意力,35 分钟。
运行命令与预期
课程根目录的 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 切片验证,不能只数注释。
资料
- Learned Image Compression with Hierarchical Progressive Context Modeling:方法图、层级调度和渐进融合,30 分钟。
- 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。
资料
- CompressAI 源码与示例:熵编码接口与整数概率表相关实现,按需浏览,25 分钟。
运行命令与预期
课程根目录的 PowerShell;代码依赖练习包中的 course_lab.py。
& ..\learned-image-compression-study\.venv\Scripts\python.exe python/answers/L32.pyK=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 场景表示还要测渲染质量和模型体积。
资料
- DCVC 官方研究代码:README:视频压缩模型与评测入口,20 分钟。
- 3D Gaussian Splatting:项目摘要和表示总览,20 分钟。
运行命令与预期
课程根目录的 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 误差更低但耗时更长,结论应同时报告收益与代价。
资料
- 动手学深度学习:4.4 模型选择与实验诊断,20 分钟。
运行命令与预期
课程根目录的 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 的差别”是能力证据;“我看完了很多论文”还不够具体。
资料
- Learned Image Compression with Hierarchical Progressive Context Modeling:实验设置和消融:借鉴论证结构,不照搬结果,25 分钟。
运行命令与预期
课程根目录的 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-imagesJPEG 固定 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 安装文档。命令和版本需结合验证记录使用。