{
 "nbformat": 4,
 "nbformat_minor": 5,
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  },
  "lecture_migration": {
   "project_id": "c0b185e7-d7e3-48b6-a605-8f1acdfe243c",
   "notebook_id": "602a2741a9a9479482814db63e3664ac",
   "destination": "L2",
   "cloud_run": false
  },
  "title": "LAB 12 · Source、Norm 与 Tent"
 },
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "deepnote": {
     "block_id": "f0c6ec4163ae4e2db900335c7d8fe01a",
     "type": "text-cell-h1",
     "source_block_id": null,
     "logical_key": null
    },
    "html_anchor": "cell-1"
   },
   "source": "LAB 12 · Source、Norm 与 Tent",
   "id": "f0c6ec4163ae4e2db900335c7d8fe01a"
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "deepnote": {
     "block_id": "29069d80f6f54d31881b08dffdb1ceae",
     "type": "markdown",
     "source_block_id": null,
     "logical_key": null
    },
    "html_anchor": "cell-2"
   },
   "source": [
    "[返回第二讲 · Source、Norm 与 Tent 参考](https://codingai-lec02.pages.dev/course/reference.html#reliability) · [网页阅读版](https://codingai-lec02.pages.dev/course/lab14.html) · [第二讲导读](https://codingai-lec02.pages.dev/course/guide.html) · [问卷作业 ↗](https://tally.so/r/VLroVv)（右键在新标签页打开，保留当前材料）\n",
    "\n",
    "**降低预测熵，会降低真实错误吗？** 本册是适应研究的独立选读，承接 [CLS08 第 7–9 节](https://codingai-lec02.pages.dev/course/c09.html)，不属于第二讲动态计算的默认课堂实验。\n",
    "\n",
    "先读第 2 节三臂表和结果图，再读第 3 节的预测时点：Source 固定源统计；Norm 使用当前 batch 统计；Tent 在相同统计设置上更新 BN affine。每次比较先问“变了哪一项”，再谈改善。\n",
    "\n",
    "模型是 `DigitsMultiExitBN-integration-20260918-lcde-r1` 的最终出口，与 LAB11 使用相同数据划分、训练代码和 seed；本册从头独立重建，不读取 LAB11 文件。保存状态哈希可核对模型身份。原始视觉 TTT 的旋转辅助任务与这里的熵目标不同。\n",
    "\n",
    "可先读 2026-09-18 保存结果，无需运行。独立重算需 `numpy torch scikit-learn matplotlib`，会重新训练源模型并运行全部流；这些 digits 条件不是 Tent 论文基准复现。\n",
    "\n",
    "[参考图解：一次熵更新](https://codingai-lec02.pages.dev/course/reference/frontier-tent-update-step.html) · [参考图解：低熵反例](https://codingai-lec02.pages.dev/course/reference/frontier-entropy-wrong-confidence.html)"
   ],
   "id": "29069d80f6f54d31881b08dffdb1ceae"
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "deepnote": {
     "block_id": "a5ebaca6797e47c2a4c60d7eeba3b6c5",
     "type": "text-cell-h2",
     "source_block_id": null,
     "logical_key": null
    },
    "html_anchor": "cell-3"
   },
   "source": "## 先读这次保存运行，再作预测",
   "id": "a5ebaca6797e47c2a4c60d7eeba3b6c5"
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "deepnote": {
     "block_id": "5fbee98408744292bd87fb9722e152f9",
     "type": "markdown",
     "source_block_id": null,
     "logical_key": null
    },
    "html_anchor": "cell-4"
   },
   "source": "在固定噪声、类别分段的流中，source 准确率为 **80.83%**，norm 与 Tent 都为 **79.72%**；在打乱顺序的同一噪声图像上，source 为 80.83%，norm 与 Tent 为 81.94%。顺序改变没有重新抽噪声。\n\n**本次 Tent 与 norm 在四个预设情境中得到相同分类结果。** 熵发生了很小变化、BN affine 确实更新，但没有观察到额外的分类改善。source→Tent 的改善或下降不能全部归因于熵梯度；这里必须先考察 batch 统计的影响。另一个严格对照是：单步、每批重置、报告更新前预测时，episodic Tent 与 norm 数值相同。",
   "id": "5fbee98408744292bd87fb9722e152f9"
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "deepnote": {
     "block_id": "d21afa9e1d4f4026983d9d9e7d1e30e0",
     "type": "text-cell-h2",
     "source_block_id": null,
     "logical_key": null
    },
    "html_anchor": "cell-5"
   },
   "source": "## 1. 固定输入、模型身份和允许使用的信息",
   "id": "d21afa9e1d4f4026983d9d9e7d1e30e0"
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "deepnote": {
     "block_id": "0c7eb6984f9147bcb5b90cdfad0cae3a",
     "type": "markdown",
     "source_block_id": null,
     "logical_key": null
    },
    "html_anchor": "cell-6"
   },
   "source": "划分、源训练和 checkpoint 选择与 本讲 LAB 11 完全相同。训练数据有标签；目标适应函数只接收图像及顺序，不接收目标标签。评价在所有预测完成后调用标签。两种事先固定的变化：`clip(0.55*x+0.25,0,1)` 与 `clip(x+Normal(0,0.30),0,1)`，后者按 sample ID 固定噪声。它们是这批 digits 上的教学变化，不是 ImageNet-C 标准实现。\n\n按相同 test ID 组成四个流：两种 corruption，各用固定随机顺序和按类别分段顺序。类别分段是研究者用标签离线构造的压力测试，不能声称实际部署已获得标签；适应算法仍只收到图像。batch=32（最后一批 8），每个 scenario／arm 从同一源 checkpoint 重置。Tent 默认每批 1 步 SGD lr=0.01、无 momentum，固定参数，未用目标标签调参。持续模式保留批间 affine 更新；额外 episodic 对照每批重置模型与优化器。",
   "id": "0c7eb6984f9147bcb5b90cdfad0cae3a"
  },
  {
   "cell_type": "code",
   "metadata": {
    "deepnote": {
     "block_id": "e4787c36bc6f498bb5c8f457bd07187c",
     "type": "code",
     "source_block_id": null,
     "logical_key": null
    },
    "html_anchor": "cell-7"
   },
   "source": "LAB_ID='LAB14-E'\n\nfrom pathlib import Path\nfrom copy import deepcopy\nimport csv, hashlib, json, platform, sys, time\nimport numpy as np\nimport torch\nfrom torch import nn\nimport torch.nn.functional as F\nimport sklearn\nfrom sklearn.datasets import load_digits\nfrom sklearn.model_selection import train_test_split\nimport matplotlib.pyplot as plt\nSEED = 20260918\nnp.random.seed(SEED); torch.manual_seed(SEED)\ntorch.set_num_threads(1)\ntorch.use_deterministic_algorithms(True)\nOUTPUT = Path('outputs') / LAB_ID\nOUTPUT.mkdir(parents=True, exist_ok=True)\nplt.rcParams.update({'figure.dpi':120, 'font.size':10, 'axes.spines.top':False, 'axes.spines.right':False})\ndef save_json(name, obj):\n    (OUTPUT/name).write_text(json.dumps(obj,ensure_ascii=False,indent=2))\ndef save_csv(name, rows):\n    with (OUTPUT/name).open('w',newline='') as f:\n        w=csv.DictWriter(f, fieldnames=list(rows[0])); w.writeheader(); w.writerows(rows)\ndef digest_array(a): return hashlib.sha256(np.ascontiguousarray(a).tobytes()).hexdigest()\ndef print_table(rows, cols=None):\n    cols=cols or list(rows[0]); print(' | '.join(cols)); print(' | '.join(['---']*len(cols)))\n    for row in rows:\n        print(' | '.join(f'{row[c]:.5f}' if isinstance(row[c],float) else str(row[c]) for c in cols))\ncpu_lines=Path('/proc/cpuinfo').read_text().splitlines() if Path('/proc/cpuinfo').exists() else []\ncpu_name=next((line.split(':',1)[1].strip() for line in cpu_lines if line.startswith('model name')),platform.processor())\nenvironment={'python':sys.version.split()[0],'torch':torch.__version__,'numpy':np.__version__,\n             'sklearn':sklearn.__version__,'cpu':cpu_name,'platform':platform.platform(),\n             'torch_threads':torch.get_num_threads(),'device':'cpu','dtype':'float32',\n             'seed':SEED,'exclusive_hardware':False}\nsave_json('environment.json',environment)\nprint(json.dumps(environment,ensure_ascii=False,indent=2))\n",
   "id": "e4787c36bc6f498bb5c8f457bd07187c",
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "deepnote": {
     "block_id": "c3d9ca75984f4f4c8163ef7dd8f0db7f",
     "type": "markdown",
     "source_block_id": null,
     "logical_key": null
    },
    "html_anchor": "cell-8"
   },
   "source": "**教师本地保存输出 · 2026-09-18**\n\n```text\n{\n  \"python\": \"3.12.14\",\n  \"torch\": \"2.14.0+cpu\",\n  \"numpy\": \"2.3.5\",\n  \"sklearn\": \"1.8.0\",\n  \"cpu\": \"INTEL(R) XEON(R) PLATINUM 8573C\",\n  \"platform\": \"Linux-6.18.44-x86_64-with-glibc2.39\",\n  \"torch_threads\": 1,\n  \"device\": \"cpu\",\n  \"dtype\": \"float32\",\n  \"seed\": 20260918,\n  \"exclusive_hardware\": false\n}\n```",
   "id": "c3d9ca75984f4f4c8163ef7dd8f0db7f"
  },
  {
   "cell_type": "code",
   "metadata": {
    "deepnote": {
     "block_id": "984c27c0820348bebc997967f8bca957",
     "type": "code",
     "source_block_id": null,
     "logical_key": null
    },
    "html_anchor": "cell-9"
   },
   "source": "dataset = load_digits()\nX = torch.tensor(dataset.images[:,None,:,:] / 16.0, dtype=torch.float32)\ny = torch.tensor(dataset.target, dtype=torch.long)\nall_ids = np.arange(len(y))\ntrainval_ids, test_ids = train_test_split(all_ids, test_size=0.2, random_state=SEED, stratify=y.numpy())\ntrain_ids, val_ids = train_test_split(trainval_ids, test_size=0.25, random_state=SEED, stratify=y.numpy()[trainval_ids])\nassert not (set(train_ids)&set(val_ids) or set(train_ids)&set(test_ids) or set(val_ids)&set(test_ids))\nassert len(train_ids)+len(val_ids)+len(test_ids)==len(y)\nsplit_manifest={'dataset':'sklearn.datasets.load_digits; 1797 real 8x8 images; pixel/16',\n                'counts':{'train':len(train_ids),'validation':len(val_ids),'test':len(test_ids)},\n                'train_ids':train_ids.tolist(),'validation_ids':val_ids.tolist(),'test_ids':test_ids.tolist(),\n                'input_sha256':digest_array(X.numpy()),'labels_sha256':digest_array(y.numpy()),\n                'split_policy':'stratified random by sample, NOT writer-disjoint', 'test_labels':'evaluation only'}\nsave_json('split-manifest.json',split_manifest)\nprint('Split:',split_manifest['counts'])\n",
   "id": "984c27c0820348bebc997967f8bca957",
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "deepnote": {
     "block_id": "61e904649d634918bf705705bd3292cb",
     "type": "markdown",
     "source_block_id": null,
     "logical_key": null
    },
    "html_anchor": "cell-10"
   },
   "source": "**教师本地保存输出 · 2026-09-18**\n\n```text\nSplit: {'train': 1077, 'validation': 360, 'test': 360}\n```",
   "id": "61e904649d634918bf705705bd3292cb"
  },
  {
   "cell_type": "code",
   "metadata": {
    "deepnote": {
     "block_id": "d1e61a761dc04f8eb6d716ccbff84c9b",
     "type": "code",
     "source_block_id": null,
     "logical_key": null
    },
    "html_anchor": "cell-11"
   },
   "source": "MODEL_ID = 'DigitsMultiExitBN-integration-20260918-lcde-r1'\nclass DigitsMultiExitBN(nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.conv1=nn.Conv2d(1,4,3,padding=1,bias=False)\n        self.bn1=nn.BatchNorm2d(4)\n        self.early=nn.Linear(4*4*4,10)\n        self.conv2=nn.Conv2d(4,16,3,padding=1,bias=False)\n        self.bn2=nn.BatchNorm2d(16)\n        self.fc=nn.Linear(16*4*4,32)\n        self.final=nn.Linear(32,10)\n    def forward(self,x):\n        h=F.avg_pool2d(F.relu(self.bn1(self.conv1(x))),2)\n        early=self.early(h.flatten(1))\n        h=F.relu(self.bn2(self.conv2(h)))\n        final=self.final(F.relu(self.fc(h.flatten(1))))\n        return early,final\n\ndef train_source():\n    torch.manual_seed(SEED)\n    model=DigitsMultiExitBN()\n    optimizer=torch.optim.Adam(model.parameters(),lr=0.003)\n    order_generator=torch.Generator().manual_seed(SEED+1)\n    best_loss=float('inf'); best_state=None; best_epoch=None; history=[]\n    for epoch in range(1,51):\n        model.train()\n        order=torch.as_tensor(train_ids)[torch.randperm(len(train_ids),generator=order_generator)]\n        total=0.0\n        for ids in order.split(64):\n            optimizer.zero_grad(set_to_none=True)\n            early,final=model(X[ids]); loss=0.35*F.cross_entropy(early,y[ids])+F.cross_entropy(final,y[ids])\n            loss.backward(); optimizer.step(); total += loss.item()*len(ids)\n        model.eval()\n        with torch.no_grad():\n            ve,vf=model(X[val_ids]); val_loss=F.cross_entropy(vf,y[val_ids]).item()\n        history.append({'epoch':epoch,'train_weighted_loss':total/len(train_ids),\n                        'validation_final_ce':val_loss,'validation_early_acc':(ve.argmax(1)==y[val_ids]).float().mean().item(),\n                        'validation_final_acc':(vf.argmax(1)==y[val_ids]).float().mean().item()})\n        if val_loss < best_loss:\n            best_loss=val_loss; best_state=deepcopy(model.state_dict()); best_epoch=epoch\n    model.load_state_dict(best_state); model.eval()\n    return model,history,best_epoch\nsource_model,train_history,best_epoch=train_source()\nsource_state=deepcopy(source_model.state_dict())\nstate_hash=hashlib.sha256(b''.join(k.encode()+v.cpu().numpy().tobytes() for k,v in sorted(source_state.items()))).hexdigest()\nsave_csv('source-training.csv',train_history)\n# Transparent JSON state is a local convenience; notebook always reconstructs from readable code.\nsave_json('source-state.json',{k:v.tolist() for k,v in source_state.items()})\nmodel_manifest={'model_id':MODEL_ID,'state_sha256':state_hash,'best_epoch':best_epoch,\n                'checkpoint_selection':'minimum validation final-exit CE over 50 epochs',\n                'train_loss':'0.35*CE(early)+CE(final)','optimizer':'Adam lr=0.003 batch=64',\n                'initialization_seed':SEED,'params':sum(p.numel() for p in source_model.parameters()),\n                'old_LAB02_dependency':False,'data_split_sha256':hashlib.sha256(json.dumps(split_manifest,sort_keys=True).encode()).hexdigest()}\nsave_json('model-manifest.json',model_manifest)\nprint(json.dumps(model_manifest,indent=2))\n",
   "id": "d1e61a761dc04f8eb6d716ccbff84c9b",
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "deepnote": {
     "block_id": "4c7408f755ea4c3bad9b01a1046dcd6d",
     "type": "markdown",
     "source_block_id": null,
     "logical_key": null
    },
    "html_anchor": "cell-12"
   },
   "source": "**教师本地保存输出 · 2026-09-18**\n\n```text\n{\n  \"model_id\": \"DigitsMultiExitBN-integration-20260918-lcde-r1\",\n  \"state_sha256\": \"544bb0f79f5759b66656565e3e4c5a59a5dd8f2893a067d93a7fe331439c685c\",\n  \"best_epoch\": 48,\n  \"checkpoint_selection\": \"minimum validation final-exit CE over 50 epochs\",\n  \"train_loss\": \"0.35*CE(early)+CE(final)\",\n  \"optimizer\": \"Adam lr=0.003 batch=64\",\n  \"initialization_seed\": 20260918,\n  \"params\": 9856,\n  \"old_LAB02_dependency\": false,\n  \"data_split_sha256\": \"6f705c12f1651f0c5b871f8ff65f4241bdbc1ffbd61a5f0fc56ac95bbb0cf129\"\n}\n```",
   "id": "4c7408f755ea4c3bad9b01a1046dcd6d"
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "deepnote": {
     "block_id": "0058516977f4404697996d4f5ae98b21",
     "type": "text-cell-h2",
     "source_block_id": null,
     "logical_key": null
    },
    "html_anchor": "cell-13"
   },
   "source": "## 2. 三个臂究竟改变了什么？",
   "id": "0058516977f4404697996d4f5ae98b21"
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "deepnote": {
     "block_id": "0fa085497f744ff1a0f6131721a3178c",
     "type": "markdown",
     "source_block_id": null,
     "logical_key": null
    },
    "html_anchor": "cell-14"
   },
   "source": "| 臂 | 用于归一化的统计 | running buffers | 梯度更新 | 预测时点 |\n|---|---|---|---|---|\n| source | 源训练 running mean／var | 保留并冻结 | 无 | 该次 forward |\n| norm | 当前目标 batch | `track_running_stats=False`，mean／var 置 None；计数器保留但不递增 | 无 | 该次 forward |\n| Tent | 当前目标 batch | 与 norm 相同 | 仅两个 BN 的 weight／bias（40 个标量） | 每批更新前的 logits |\n\n同一目标 batch 同时参与估计统计与预测，因而不是逐样本独立预测。所有臂卷积与分类头固定；norm 不做 affine 更新；Tent 只有 affine 被允许求梯度。没有把 buffers 禁用写成“更新 running 统计”。\n\n官方 `forward_and_adapt` 先得到 outputs，再计算熵、反传、step，返回的仍是该次更新前 outputs。下面对应这个时点；不额外前向测更新后效果，也不暗中忽略其成本。每臂完成后比较参数／buffer，并断言只有允许状态发生变化。",
   "id": "0fa085497f744ff1a0f6131721a3178c"
  },
  {
   "cell_type": "code",
   "metadata": {
    "deepnote": {
     "block_id": "d3f7688791d84165897db48192640df5",
     "type": "code",
     "source_block_id": null,
     "logical_key": null
    },
    "html_anchor": "cell-15"
   },
   "source": "BATCH=32; TTA_LR=.01\nsample_noise=np.stack([np.random.default_rng(SEED+int(s)*17).normal(0,.30,(1,8,8)) for s in test_ids])\ntarget_inputs={'contrast':torch.clamp(.55*X[test_ids]+.25,0,1),\n               'noise':torch.clamp(X[test_ids]+torch.tensor(sample_noise,dtype=torch.float32),0,1)}\norders={'shuffled':np.random.default_rng(SEED+77).permutation(len(test_ids)),\n        'class_sorted':np.argsort(y[test_ids].numpy(),kind='stable')}\nsave_json('tta-protocol.json',{'batch':BATCH,'learning_rate':TTA_LR,'steps_per_batch':1,'optimizer':'SGD; momentum=0',\n 'corruptions':{'contrast':'clip(0.55*x+0.25,0,1)','noise':'clip(x+N(0,0.30),0,1); seed=SEED+sample_id*17'},\n 'prediction_timing':'logits from forward before this batch optimizer.step()',\n 'reset':'same source checkpoint for each scenario and arm; episodic resets each batch',\n 'label_use':'only post-prediction evaluation; class_sorted stress order constructed offline with labels',\n 'target_input_hashes':{k:digest_array(v.numpy()) for k,v in target_inputs.items()},\n 'orders':{k:v.tolist() for k,v in orders.items()},'test_hyperparameter_tuning':False})\nfig,axs=plt.subplots(3,6,figsize=(9,4.3))\nfor row,(name,inputs) in enumerate([('clean',X[test_ids]),*target_inputs.items()]):\n    for j in range(6):\n        axs[row,j].imshow(inputs[j,0],cmap='gray',vmin=0,vmax=1);axs[row,j].set_xticks([]);axs[row,j].set_yticks([])\n        if row==0:axs[row,j].set_title(f'id={test_ids[j]} y={int(y[test_ids[j]])}',fontsize=9)\n    axs[row,0].set_ylabel(name)\nfig.suptitle('Same inputs; fixed display range [0,1]');fig.tight_layout();fig.savefig(OUTPUT/'tta-inputs.png');plt.show()\n",
   "id": "d3f7688791d84165897db48192640df5",
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "deepnote": {
     "block_id": "9a67b0dca7404b54b78a097b9dd6adb9",
     "type": "markdown",
     "source_block_id": null,
     "logical_key": null
    },
    "html_anchor": "cell-16"
   },
   "source": "**教师本地保存输出 · 2026-09-18**\n\n![LAB 12 · 本地实际运行保存图 1](https://codingai-lec02.pages.dev/course/images/LAB14-E/tta-inputs.png)",
   "id": "9a67b0dca7404b54b78a097b9dd6adb9"
  },
  {
   "cell_type": "code",
   "metadata": {
    "deepnote": {
     "block_id": "d3f222821c164d938d54962dd9132359",
     "type": "code",
     "source_block_id": null,
     "logical_key": null
    },
    "html_anchor": "cell-17"
   },
   "source": "def entropy(logits): return -(logits.softmax(1)*logits.log_softmax(1)).sum(1)\ndef new_arm(arm):\n    model=DigitsMultiExitBN();model.load_state_dict(source_state);model.requires_grad_(False)\n    if arm=='source': model.eval(); return model,None\n    model.train()\n    affine=[]\n    for module in model.modules():\n        if isinstance(module,nn.BatchNorm2d):\n            module.track_running_stats=False;module.running_mean=None;module.running_var=None\n            if arm=='tent':\n                module.weight.requires_grad_(True);module.bias.requires_grad_(True)\n                affine += [module.weight,module.bias]\n    opt=torch.optim.SGD(affine,lr=TTA_LR,momentum=0) if arm=='tent' else None\n    return model,opt\n\ndef adapt_stream(images,order,arm,episodic=False):\n    # No labels argument. Source state read-only; each invocation starts independently.\n    model,opt=new_arm(arm)\n    initial_parameters={n:p.detach().clone() for n,p in model.named_parameters()}\n    initial_buffers={n:b.detach().clone() for n,b in model.named_buffers()}\n    logits=np.zeros((len(images),10),dtype=np.float32); batch_records=[]\n    allowed={'bn1.weight','bn1.bias','bn2.weight','bn2.bias'} if arm=='tent' else set()\n    for batch_index,start in enumerate(range(0,len(order),BATCH)):\n        if episodic: model,opt=new_arm(arm)\n        local=order[start:start+BATCH]; batch=images[local]\n        if arm=='tent':\n            opt.zero_grad(set_to_none=True)\n            _,out=model(batch)\n            loss=entropy(out).mean()\n            # Save the prediction from before the current update; no extra forward.\n            logits[local]=out.detach().numpy()\n            loss.backward(); opt.step()\n        else:\n            with torch.no_grad(): _,out=model(batch)\n            logits[local]=out.numpy();loss=entropy(out).mean()\n        changed_this_batch=[n for n,p in model.named_parameters() if not torch.equal(p.detach(),initial_parameters[n])]\n        assert set(changed_this_batch)<=allowed\n        batch_records.append({'batch_index':batch_index,'n':len(local),'entropy_pre_update':float(loss.detach()),\n            'affine_delta_l2':float(sum((p.detach()-initial_parameters[n]).square().sum() for n,p in model.named_parameters()).sqrt())})\n    deltas={n:float((p.detach()-initial_parameters[n]).abs().max()) for n,p in model.named_parameters()}\n    changed=[n for n,d in deltas.items() if d>0]\n    buffer_changes=[n for n,b in model.named_buffers() if not torch.equal(b,initial_buffers[n])]\n    assert set(changed)<=allowed and not buffer_changes\n    if arm=='tent': assert changed and sum(p.numel() for p in model.parameters() if p.requires_grad)==40\n    audit={'arm':arm,'episodic':episodic,'changed_parameters':changed,'max_absolute_parameter_deltas':deltas,\n           'changed_buffers_during_stream':buffer_changes,\n           'running_means_none':{n:m.running_mean is None for n,m in model.named_modules() if isinstance(m,nn.BatchNorm2d)},\n           'num_batches_tracked':{n:int(m.num_batches_tracked) for n,m in model.named_modules() if isinstance(m,nn.BatchNorm2d)},\n           'trainable_parameter_count':sum(p.numel() for p in model.parameters() if p.requires_grad)}\n    return logits,batch_records,audit\n\n# Compute every scenario before calling target labels for evaluation below.\nruns={};audits={};batch_rows=[]\nfor corruption,images in target_inputs.items():\n    for order_name,order in orders.items():\n        for arm,episodic in [('source',False),('norm',False),('tent',False),('tent',True)]:\n            name=f'{corruption}/{order_name}/{arm}'+('-episodic' if episodic else '')\n            result,batches,audit=adapt_stream(images,order,arm,episodic)\n            runs[name]=result;audits[name]=audit\n            batch_rows += [{'run':name,**row} for row in batches]\n# At the first batch all affine values coincide: Tent prediction must equal Norm prediction.\nfor corruption in target_inputs:\n    for order_name,order in orders.items():\n        idx=order[:BATCH]\n        assert np.allclose(runs[f'{corruption}/{order_name}/tent'][idx],runs[f'{corruption}/{order_name}/norm'][idx],atol=1e-6)\n        # With per-batch reset and before-update prediction, episodic Tent outputs equal Norm for every batch.\n        assert np.allclose(runs[f'{corruption}/{order_name}/tent-episodic'],runs[f'{corruption}/{order_name}/norm'],atol=1e-6)\nsave_json('parameter-buffer-audit.json',audits);save_csv('batch-trajectories.csv',batch_rows)\nprint('Passed: only allowed BN affine changed; no running buffers updated; first-batch predictions match Norm;')\nprint('episodic + one step + before-update predictions match Norm across the whole stream.')\n",
   "id": "d3f222821c164d938d54962dd9132359",
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "deepnote": {
     "block_id": "ab84f573a89842f9b98978fbef42b97c",
     "type": "markdown",
     "source_block_id": null,
     "logical_key": null
    },
    "html_anchor": "cell-18"
   },
   "source": "**教师本地保存输出 · 2026-09-18**\n\n```text\nPassed: only allowed BN affine changed; no running buffers updated; first-batch predictions match Norm;\nepisodic + one step + before-update predictions match Norm across the whole stream.\n```",
   "id": "ab84f573a89842f9b98978fbef42b97c"
  },
  {
   "cell_type": "code",
   "metadata": {
    "deepnote": {
     "block_id": "cb6a4720b0f5430a829e5d0c2f22b6b9",
     "type": "code",
     "source_block_id": null,
     "logical_key": null
    },
    "html_anchor": "cell-19"
   },
   "source": "truth=y[test_ids].numpy();summary=[];prediction_rows=[]\nfor name,logits in runs.items():\n    probs=torch.tensor(logits).softmax(1).numpy();pred=probs.argmax(1)\n    ents=entropy(torch.tensor(logits)).numpy(); wrong=pred!=truth\n    summary.append({'run':name,'accuracy':float(np.mean(~wrong)),'errors':int(wrong.sum()),\n                    'mean_entropy':float(ents.mean()),'wrong_confident_p_ge_09':int(np.sum(wrong&(probs.max(1)>=.9)))})\n    for i,sample_id in enumerate(test_ids):\n        prediction_rows.append({'run':name,'sample_id':int(sample_id),'target':int(truth[i]),'prediction':int(pred[i]),\n              'confidence':float(probs[i].max()),'entropy':float(ents[i]),**{f'logit_{k}':float(logits[i,k]) for k in range(10)}})\nsave_csv('tta-summary.csv',summary);save_csv('tta-predictions.csv',prediction_rows)\nprint_table(summary)\nfig,axs=plt.subplots(1,2,figsize=(11,4))\nlabels=[c+'/'+o for c in target_inputs for o in orders]\nxx=np.arange(len(labels));colors=['#52616b','#2f80a0','#d07b27']\nfor i,arm in enumerate(['source','norm','tent']):\n    selected=[next(r for r in summary if r['run']==f'{label}/{arm}') for label in labels]\n    axs[0].bar(xx+(i-1)*.24,[100*(1-r['accuracy']) for r in selected],width=.24,label=arm,color=colors[i])\n    axs[1].bar(xx+(i-1)*.24,[r['mean_entropy'] for r in selected],width=.24,label=arm,color=colors[i])\nfor ax in axs: ax.set_xticks(xx,labels,rotation=18); ax.legend(fontsize=8)\naxs[0].set(ylabel='Error rate (%)',title='Target labels used only for evaluation')\naxs[1].set(ylabel='Mean prediction entropy (nats)',title='Proxy objective is not correctness')\nfig.tight_layout();fig.savefig(OUTPUT/'tta-comparison.png');plt.show()\ncomparisons=[]\nfor label in labels:\n    s,n,t=[next(r for r in summary if r['run']==f'{label}/{arm}') for arm in ['source','norm','tent']]\n    comparisons.append({'scenario':label,'tent_minus_source_error_pp':100*(s['accuracy']-t['accuracy']),\n                        'tent_minus_norm_error_pp':100*(n['accuracy']-t['accuracy']),\n                        'tent_minus_norm_entropy':t['mean_entropy']-n['mean_entropy']})\nsave_csv('tta-differences.csv',comparisons);print_table(comparisons)\n",
   "id": "cb6a4720b0f5430a829e5d0c2f22b6b9",
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "deepnote": {
     "block_id": "cf184756d67a4a41b090ccdaf2e820d2",
     "type": "markdown",
     "source_block_id": null,
     "logical_key": null
    },
    "html_anchor": "cell-20"
   },
   "source": "**教师本地保存输出 · 2026-09-18**\n\n```text\nrun | accuracy | errors | mean_entropy | wrong_confident_p_ge_09\n--- | --- | --- | --- | ---\ncontrast/shuffled/source | 0.95278 | 17 | 0.14519 | 3\ncontrast/shuffled/norm | 0.97500 | 9 | 0.05240 | 4\ncontrast/shuffled/tent | 0.97500 | 9 | 0.05233 | 4\ncontrast/shuffled/tent-episodic | 0.97500 | 9 | 0.05240 | 4\ncontrast/class_sorted/source | 0.95278 | 17 | 0.14519 | 3\ncontrast/class_sorted/norm | 0.96944 | 11 | 0.06980 | 5\ncontrast/class_sorted/tent | 0.96944 | 11 | 0.06979 | 5\ncontrast/class_sorted/tent-episodic | 0.96944 | 11 | 0.06980 | 5\nnoise/shuffled/source | 0.80833 | 69 | 0.23194 | 17\nnoise/shuffled/norm | 0.81944 | 65 | 0.21923 | 20\nnoise/shuffled/tent | 0.81944 | 65 | 0.21916 | 20\nnoise/shuffled/tent-episodic | 0.81944 | 65 | 0.21923 | 20\nnoise/class_sorted/source | 0.80833 | 69 | 0.23194 | 17\nnoise/class_sorted/norm | 0.79722 | 73 | 0.23695 | 20\nnoise/class_sorted/tent | 0.79722 | 73 | 0.23695 | 20\nnoise/class_sorted/tent-episodic | 0.79722 | 73 | 0.23695 | 20\nscenario | tent_minus_source_error_pp | tent_minus_norm_error_pp | tent_minus_norm_entropy\n--- | --- | --- | ---\ncontrast/shuffled | -2.22222 | 0.00000 | -0.00007\ncontrast/class_sorted | -1.66667 | 0.00000 | -0.00002\nnoise/shuffled | -1.11111 | 0.00000 | -0.00007\nnoise/class_sorted | 1.11111 | 0.00000 | -0.00001\n```\n\n![LAB 12 · 本地实际运行保存图 2](https://codingai-lec02.pages.dev/course/images/LAB14-E/tta-comparison.png)",
   "id": "cf184756d67a4a41b090ccdaf2e820d2"
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "deepnote": {
     "block_id": "4b0ee2d6e1b94c4d90f5938517877960",
     "type": "text-cell-h2",
     "source_block_id": null,
     "logical_key": null
    },
    "html_anchor": "cell-21"
   },
   "source": "## 3. 为什么 episodic 与 norm 会重合？",
   "id": "4b0ee2d6e1b94c4d90f5938517877960"
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "deepnote": {
     "block_id": "47d51d0169b74c68b54713e99841ba4c",
     "type": "markdown",
     "source_block_id": null,
     "logical_key": null
    },
    "html_anchor": "cell-22"
   },
   "source": [
    "![本批先记录更新前预测，随后更新BN affine；持续模式继承到下一批，每批重置模式丢弃；Norm没有梯度更新。](https://codingai-lec02.pages.dev/course/visuals/deepnote-final/tent-prediction-timing.png)\n",
    "\n",
    "[查看矢量图](https://codingai-lec02.pages.dev/course/visuals/deepnote-final/tent-prediction-timing.svg)\n",
    "\n",
    "**先读箭头，再读结果。** 本册每批只走一步，返回该步更新前的 logits。每批先重置时，这次 forward 的 affine 与 Norm 相同；更新发生在答案已被记录之后，又在下一批开始前被丢弃。因此单步 episodic Tent 与 Norm 的逐样本预测重合，正是当前协议的预期结果，不是实现没执行更新。\n",
    "\n",
    "持续模式保留更新，才可能影响下一批。要考察“本批更新后预测”，必须额外前向；它改变了预测时点和成本，不能把现有保存数值换个标题就算验证。\n",
    "\n",
    "**改变一个条件。** 固定每批输入和一次更新，只将“每批重置”改成“持续保留”。第一批预测是否应相同？从哪批起可能不同？对照代码中第一批一致性断言与参数审计回答。\n",
    "\n",
    "类别分段还会改变当前 batch 统计的代表性。因此先用 Source→Norm 检查统计作用，再用 Norm→Tent 检查额外梯度作用，不能把两者合成一个解释。"
   ],
   "id": "47d51d0169b74c68b54713e99841ba4c"
  },
  {
   "cell_type": "code",
   "metadata": {
    "deepnote": {
     "block_id": "5f84fa43f59940d1ab31dc9ab9ee6c41",
     "type": "code",
     "source_block_id": null,
     "logical_key": null
    },
    "html_anchor": "cell-23"
   },
   "source": "# Inspect actual model mistakes. Prefer cases where Tent is wrong with high confidence.\nworst_name=max((n for n in runs if n.endswith('/tent')),key=lambda n:int((runs[n].argmax(1)!=truth).sum()))\nlogits=runs[worst_name];p=torch.tensor(logits).softmax(1).numpy();pred=p.argmax(1)\nwrong=np.flatnonzero(pred!=truth); chosen=wrong[np.argsort(-p[wrong].max(1))[:6]]\ncorruption=worst_name.split('/')[0]\nfig,axs=plt.subplots(1,6,figsize=(10,2.4))\nfor ax,i in zip(axs,chosen):\n    ax.imshow(target_inputs[corruption][i,0],cmap='gray',vmin=0,vmax=1);ax.axis('off')\n    ax.set_title(f'id={test_ids[i]} y={truth[i]}\\npred={pred[i]}, p={p[i].max():.2f}',fontsize=9)\nfig.suptitle('Descriptive failure gallery: '+worst_name,fontsize=11)\nfig.tight_layout();fig.savefig(OUTPUT/'tta-failures.png');plt.show()\nprint('Gallery chosen after evaluation to describe failure, not to select algorithm or report a best run.')\n# Logical counterexample; separate from empirical frequency above.\nwrong_logits=torch.tensor([[0.,2.,0.]])  # true class 0, predicted class 1\ncounterexample=[]\nfor scale in [1.,2.,5.]:\n    z=scale*wrong_logits\n    counterexample.append({'logit_scale':scale,'prediction':int(z.argmax(1)),\n                          'entropy':float(entropy(z)),'correct':bool(z.argmax(1)==0)})\nprint_table(counterexample);save_csv('exact-entropy-counterexample.csv',counterexample)\n",
   "id": "5f84fa43f59940d1ab31dc9ab9ee6c41",
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "deepnote": {
     "block_id": "603b3a7afad4468290a03e90e73773ac",
     "type": "markdown",
     "source_block_id": null,
     "logical_key": null
    },
    "html_anchor": "cell-24"
   },
   "source": "**教师本地保存输出 · 2026-09-18**\n\n```text\nGallery chosen after evaluation to describe failure, not to select algorithm or report a best run.\nlogit_scale | prediction | entropy | correct\n--- | --- | --- | ---\n1.00000 | 1 | 0.66557 | False\n2.00000 | 1 | 0.17732 | False\n5.00000 | 1 | 0.00100 | False\n```\n\n![LAB 12 · 本地实际运行保存图 3](https://codingai-lec02.pages.dev/course/images/LAB14-E/tta-failures.png)",
   "id": "603b3a7afad4468290a03e90e73773ac"
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "deepnote": {
     "block_id": "38dca55ff03d4ad6a903665c55208c5e",
     "type": "text-cell-h2",
     "source_block_id": null,
     "logical_key": null
    },
    "html_anchor": "cell-25"
   },
   "source": "## 4. 再改一个条件，明确什么会推翻解释",
   "id": "38dca55ff03d4ad6a903665c55208c5e"
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "deepnote": {
     "block_id": "1514ec4b6a674f8c90e762251cafa849",
     "type": "markdown",
     "source_block_id": null,
     "logical_key": null
    },
    "html_anchor": "cell-26"
   },
   "source": [
    "### 先读现有四种条件，不必运行\n",
    "\n",
    "保持噪声图像不变，只比较 shuffled 与 class_sorted：Source 都为 80.83%；Norm／Tent 从 81.94% 变为 79.72%。这里没有重新抽噪声，改变的是顺序和 batch 同伴。请指出哪两行支持“统计作用”，哪两行才能检验“额外熵梯度作用”。\n",
    "\n",
    "**完成一个两句解释。** “Source→Norm 显示____；Norm→Tent 显示____。”准确率相同不能单独证明 logits 相同，需结合逐样本记录与状态审计；不要把小熵变化自动改写成分类收益。\n",
    "\n",
    "### 再改一个条件\n",
    "\n",
    "若选择运行：保持样本、噪声与顺序，只把 batch=32 改为 batch=8。运行前预测统计的代表性与结果可能如何变化；运行后同时报告错误、熵和允许更新的参数。没有预设单向答案。根据当前测试标签挑出最好的 batch 或学习率属于开发，不能仍称零标签调参。\n",
    "\n",
    "保存结果覆盖全部预设条件；失败图是评价后选出的解释样例，总体频率以 360 张完整表为准。logits 缩放反例证明低熵不蕴含正确，与实际失败频率是两种证据。\n",
    "\n",
    "历史本地执行包含源模型训练、16 个流／臂、参数与 buffers 审计、更新前预测核对及逐样本记录；不表示已在你的 Deepnote 副本重新运行。\n",
    "\n",
    "来源：[固定版本 Tent 实现](https://github.com/DequanWang/tent/blob/e9e926a668d85244c66a6d5c006efbd2b82e83e8/tent.py)、[BatchNorm2d](https://docs.pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html)、[Tent 论文](https://arxiv.org/abs/2006.10726)。"
   ],
   "id": "1514ec4b6a674f8c90e762251cafa849"
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "deepnote": {
     "block_id": "3888aa59f16b4e71ab29b3e6e331222e",
     "type": "markdown",
     "source_block_id": null,
     "logical_key": null
    },
    "html_anchor": "cell-27"
   },
   "source": [
    "[返回第二讲 · 适应参考](https://codingai-lec02.pages.dev/course/reference.html#reliability) · [网页阅读版与保存结果](https://codingai-lec02.pages.dev/course/lab14.html) · [下载 Lecture02-LAB12.ipynb](https://codingai-lec02.pages.dev/course/materials/Lecture02-LAB12.ipynb) · [课程导读](https://codingai-lec02.pages.dev/course/guide.html)"
   ],
   "id": "3888aa59f16b4e71ab29b3e6e331222e"
  }
 ]
}