第一把“AI手术刀”:用Jupyter Notebook解剖并可视化一个神经网络
2026/7/22 12:37:21 网站建设 项目流程

第一把“AI手术刀”:用Jupyter Notebook解剖并可视化一个神经网络

当黑箱不再神秘:在单元格之间,逐层剥开深度学习的“直觉”


楔子:为什么我们需要“看见”神经网络?

深度学习常被诟病为“炼金术”——调参如撒盐,效果看缘分。但真正让工程师失眠的,不是数学公式的复杂,而是推理过程的不透明。我们丢进一张猫图,网络吐出“猫”的标签,中间发生了什么?几十亿个参数像一片黑暗森林,没有地图,没有路径。

但如果我们能解剖它呢?不是用抽象的张量形状,而是用可视化的手术刀,一刀一刀切下去,看到每一层如何扭曲输入空间,如何放大特征,如何形成决策边界。

这就是本文的目标:用 Jupyter Notebook 作为手术台,PyTorch 作为刀柄,Matplotlib 作为显微镜,完整解剖一个为 MNIST 设计的浅层卷积神经网络。我们不做训练演示(那太无聊),而是做事后尸检与动态探查——加载预训练权重,逐层钩取激活值,绘制特征地图,并最终用交互式控件“拨动”神经元的响应。

准备好你的 Jupyter 环境。我们开始动刀。


第一章:手术器械清单 —— 环境与模型骨架

首先,确保你的笔记本里有这些工具:

importtorchimporttorch.nnasnnimporttorch.nn.functionalasFimportnumpyasnpimportmatplotlib.pyplotaspltfrommatplotlib.widgetsimportSlider,Buttonimporttorchvisionimporttorchvision.transformsastransformsfromPILimportImageimportwarnings warnings.filterwarnings('ignore')

我们使用一个极简的 ConvNet,专门为 28×28 灰度图设计。它只有 3 个卷积块和 2 个全连接层,总参数量约 12 万——足够小,可以完全装进大脑,又足够深,能展示层次语义。

classNet(nn.Module):def__init__(self):super().__init__()self.conv1=nn.Conv2d(1,16,3,padding=1)self.conv2=nn.Conv2d(16,32,3,padding=1)self.conv3=nn.Conv2d(32,64,3,padding=1)self.pool=nn.MaxPool2d(2,2)self.fc1=nn.Linear(64*4*4,128)self.fc2=nn.Linear(128,10)self.drop=nn.Dropout(0.25)defforward(self,x,return_activations=False):# 我们在此处埋下钩子,返回各层输出acts={}x=self.pool(F.relu(self.conv1(x)))acts['conv1']=x x=self.pool(F.relu(self.conv2(x)))acts['conv2']=x x=self.pool(F.relu(self.conv3(x)))acts['conv3']=x x=x.view(-1,64*4*4)x=F.relu(self.fc1(x))acts['fc1']=x x=self.drop(x)x=self.fc2(x)acts['logits']=xifreturn_activations:returnx,actsreturnx

我们用 MNIST 测试集上一轮预训练好的权重(精度 99.2%)。你可以自己训练 5 个 epoch,或加载我提供的权重文件。这里假设你已经有了model.pth

model=Net()model.load_state_dict(torch.load('model.pth',map_location='cpu'))model.eval()

第二章:第一刀 —— 输入样本的“术前成像”

任何解剖都需要先看“病人”全貌。选一张数字“5”的图片,我们把它转换成模型真正看到的张量——归一化后的 1×28×28。

transform=transforms.Compose([transforms.ToTensor(),transforms.Normalize((0.1307,),(0.3081,))])# 取测试集第一个样本testset=torchvision.datasets.MNIST(root='./data',train=False,download=True,transform=transform)img,label=testset[5]# 索引5对应一个手写"5"img_batch=img.unsqueeze(0)plt.figure(figsize=(4,4))plt.imshow(img.squeeze(),cmap='gray')plt.title(f"原始输入 - 标签{label}")plt.axis('off')plt.show()

这一刀没有血,只是看清起点。


第三章:第二刀 —— 前向钩子:截取每一层的“组织切片”

PyTorch 的register_forward_hook是我们的活检钳。我们在每个卷积层和全连接层后挂载钩子,记录激活张量。

activation_outputs={}defget_hook(name):defhook(module,input,output):activation_outputs[name]=output.detach()returnhook# 注册handles=[]forname,layerinmodel.named_children():ifisinstance(layer,(nn.Conv2d,nn.Linear)):handles.append(layer.register_forward_hook(get_hook(name)))

执行一次前向传播,同时收集激活:

withtorch.no_grad():output,acts=model(img_batch,return_activations=True)# activation_outputs 里现在有 'conv1', 'conv2', 'conv3', 'fc1' 的 tensor

现在,我们把这些“切片”摊开在显微镜下。


第四章:第三刀 —— 卷积核的可视化:第一层在“看”什么?

第一层卷积核(16 个 3×3 滤波器)是直接作用在原始像素上的。我们把它画出来,就能理解网络最底层的“感受野偏好”。

kernels=model.conv1.weight.data.cpu().numpy()# shape: [16, 1, 3, 3]fig,axes=plt.subplots(4,4,figsize=(8,8))fori,axinenumerate(axes.flat):# 归一化到0-1显示k=kernels[i,0]k=(k-k.min())/(k.max()-k.min()+1e-8)ax.imshow(k,cmap='RdBu',interpolation='nearest')ax.set_title(f'Filter{i+1}')ax.axis('off')plt.suptitle('第一层卷积核 (3x3 边缘/纹理检测器)',fontsize=14)plt.tight_layout()plt.show()

你会看到:有些核像水平边缘检测器,有些像垂直边缘,有些像斑点——这印证了视觉皮层初级细胞的经典发现。


第五章:第四刀 —— 特征图接力:从边缘到语义的“变形记”

这是最精彩的部分。我们绘制三层卷积输出的特征图(取前 8 个通道),观察同一张“5”如何被逐层抽象。

defplot_activation_maps(acts_dict,layer_name,num_channels=8):tensor=acts_dict[layer_name].squeeze(0)# [C, H, W]c=min(num_channels,tensor.shape[0])fig,axes=plt.subplots(2,c//2,figsize=(12,4))axes=axes.flatten()foriinrange(c):fm=tensor[i].cpu().numpy()fm=(fm-fm.min())/(fm.max()-fm.min()+1e-8)axes[i].imshow(fm,cmap='viridis')axes[i].set_title(f'ch{i+1}')axes[i].axis('off')plt.suptitle(f'{layer_name}特征图 (前{c}通道)',fontsize=14)plt.tight_layout()plt.show()forlayerin['conv1','conv2','conv3']:plot_activation_maps(acts,layer,num_channels=8)

观察结果

  • conv1(16通道,14×14):特征图仍清晰保留数字轮廓,每个通道响应不同的局部纹理(边缘方向)。
  • conv2(32通道,7×7):开始出现“碎片化”,某些通道对数字的特定弧度敏感,但空间位置仍可辨识。
  • conv3(64通道,4×4):特征图已经高度抽象,每个小图几乎看不出原始数字形状,而是某种“语义码”——比如某个通道专门响应上半部分的圆弧,另一个响应右下角的收笔。

这就是层次特征复用的直观证据


第六章:第五刀 —— 全连接层的“决策热力”:哪个神经元被点燃?

fc1有 128 个神经元,它们是最后的“特征向量”。我们把这 128 个激活值画成条形图,并高亮前 10 个最强响应。

fc1_act=acts['fc1'].squeeze().cpu().numpy()# [128]plt.figure(figsize=(14,4))plt.bar(range(128),fc1_act,color='steelblue',alpha=0.7)plt.scatter(np.argsort(fc1_act)[-10:],np.sort(fc1_act)[-10:],color='red',s=50,label='Top 10 激活')plt.xlabel('神经元索引')plt.ylabel('ReLU 激活值')plt.title('全连接层 fc1 的响应分布 (128维)')plt.legend()plt.grid(axis='y',alpha=0.3)plt.show()

通常,这 128 维向量中只有不到 20% 的神经元有显著响应(>0.5),其余接近零——说明网络内部表示是稀疏的。

更进一步,我们可以看输出 logits(10 维):

logits=acts['logits'].squeeze().cpu().numpy()probs=F.softmax(torch.tensor(logits),dim=0).numpy()plt.figure(figsize=(8,4))plt.bar(range(10),probs,color=['green'ifi==labelelse'gray'foriinrange(10)])plt.xticks(range(10))plt.ylim(0,1)plt.title(f'输出概率分布 — 正确类{label}置信度{probs[label]:.3f}')plt.show()

此时,你会看到数字 5 的置信度通常在 0.95 以上,其他类被有效压制。


第七章:第六刀 —— 交互式“神经元拨片”:滑动 ReLU 阈值,观察特征图变化

静态图不过瘾。我们构建一个交互控件:用一个滑块调整某个特征图的二值化阈值,实时看到哪些空间位置被激活。

fig,ax=plt.subplots(figsize=(6,5))plt.subplots_adjust(bottom=0.2)# 选取 conv2 的第 5 个通道作为演示feature_map=acts['conv2'][0,4].cpu().numpy()# [7,7]im=ax.imshow(feature_map,cmap='hot',interpolation='bilinear')ax.set_title('conv2 第5通道 - 拖动阈值查看高响应区域')ax.axis('off')ax_thresh=plt.axes([0.2,0.05,0.6,0.03])slider=Slider(ax_thresh,'阈值',0.0,feature_map.max(),valinit=feature_map.mean())defupdate(val):th=slider.val masked=np.where(feature_map>th,feature_map,np.nan)im.set_data(masked)ax.set_title(f'阈值 ={th:.2f}(响应像素{np.sum(~np.isnan(masked))}个)')fig.canvas.draw_idle()slider.on_changed(update)plt.show()

拖动滑块时,你会看到背景噪声逐渐消失,只有数字“5”的笔画区域保留强响应。这说明卷积层已经学会了空间对齐的特征检测。


第八章:第七刀 —— 错误案例的“尸检报告”:当网络看走眼时,内部发生了什么?

找一张模型预测错误的样本(如果有),重复上述钩子流程,然后对比正确样本的激活分布差异。这里我们强行选一个混淆样本(比如将“3”误判为“5”)。

# 从测试集中找第一个预测错误的样本model.eval()wrong_idx=Noneforiinrange(len(testset)):img_i,lbl_i=testset[i]withtorch.no_grad():pred=model(img_i.unsqueeze(0)).argmax().item()ifpred!=lbl_i:wrong_idx=ibreakifwrong_idxisnotNone:err_img,err_label=testset[wrong_idx]withtorch.no_grad():_,err_acts=model(err_img.unsqueeze(0),return_activations=True)# 对比 conv3 平均激活值correct_avg=acts['conv3'].mean().item()wrong_avg=err_acts['conv3'].mean().item()print(f"错误样本索引{wrong_idx}: 真实={err_label}, 预测={pred}")print(f"正确样本 conv3 平均激活:{correct_avg:.4f}")print(f"错误样本 conv3 平均激活:{wrong_avg:.4f}")print("差异率: {:.2f}%".format(abs(correct_avg-wrong_avg)/correct_avg*100))# 并排显示两张图fig,(ax1,ax2)=plt.subplots(1,2,figsize=(8,4))ax1.imshow(testset[5][0].squeeze(),cmap='gray')ax1.set_title('正确样本 (5)')ax1.axis('off')ax2.imshow(err_img.squeeze(),cmap='gray')ax2.set_title(f'错误样本 ({err_label}{pred})')ax2.axis('off')plt.show()

通常你会发现:错误样本的中间层激活值整体偏低,或者在某些关键通道上的响应分布与正确样本存在结构性差异——这为对抗样本检测不确定性估计提供了直观线索。


第九章:第八刀 —— 梯度反向传播的热力图(Grad-CAM 简易版)

我们不止看前向,还要看反向——即输入像素对最终决策的贡献度。用简单的“输入梯度”方法:

img_tensor=img_batch.clone().requires_grad_(True)output=model(img_tensor)target_class=output[0,label]# 对正确类求梯度target_class.backward()grad=img_tensor.grad.squeeze().cpu().numpy()# [28,28]grad=np.abs(grad)# 取绝对值grad=(grad-grad.min())/(grad.max()-grad.min()+1e-8)plt.figure(figsize=(8,4))plt.subplot(1,2,1)plt.imshow(img.squeeze(),cmap='gray')plt.title('原始图像')plt.axis('off')plt.subplot(1,2,2)plt.imshow(grad,cmap='hot',interpolation='bilinear')plt.title('输入梯度热力图 (对正确类的敏感区域)')plt.axis('off')plt.show()

这张热力图会告诉你:网络在做决策时,最关注图像的哪些像素区域。对于数字“5”,热点往往集中在左上弧线和右下收笔处——这与人类识别数字的关键特征惊人一致。


第十章:缝合与启示 —— 我们解剖出了什么?

经过这八刀,我们得到了以下病理解剖报告

  1. 第一层:边缘检测器,可解释性最高。
  2. 第二层:局部组合纹理,部分可解释。
  3. 第三层:语义碎片,几乎不可直接解读,但统计上区分度最强。
  4. 全连接层:稀疏编码,只有少数神经元对特定输入“兴奋”。
  5. 输出层:概率分布,置信度反映决策清晰度。
  6. 梯度反向:输入空间中的关键决策区域,与人类视觉注意力有重叠。

更重要的是,我们证明了:神经网络不是黑箱,而是一座分层分解的滤波器组。每一层都在做“从具体到抽象”的降维与升维转换。Jupyter Notebook 赋予我们即时交互的能力,让每一次Shift+Enter都像一次外科手术的精确切割。


附:手术室注意事项(实用建议)

  • 钩子管理:用完记得for h in handles: h.remove(),避免内存泄漏。
  • 大模型适配:对于 ResNet-50 或 Transformer,请使用torch.utils.vizTensorBoard,本文方法同样适用,只需调整特征图尺寸。
  • 交互性能:在 Notebook 中使用%matplotlib notebook替代inline,可获得更流畅的滑块体验。
  • 权重复用:若没有预训练权重,可自行训练 3 个 epoch,MNIST 很快收敛,不影响可视化效果。

最后一把刀:留给你的思考

现在,把这张图片换成你自己的照片、一个手绘符号,甚至一段噪声——观察网络如何从“确定”滑向“困惑”。你可以修改网络结构(增加 BN、改变激活函数),然后再次切开它,看内部组织是否发生形态变化。

可视化不是终点,而是理解的开端。每一把“AI 手术刀”切下的,不是参数的血肉,而是工程直觉的基石。当你下一次调参失败时,不妨先打开 Notebook,看看你的网络在“想”什么——答案往往就在那几张特征图里。

代码完整版已封装为单个.ipynb文件,可随本文附件下载。建议你亲手运行每一个单元格,并试着替换不同的输入样本。那才是真正的“主刀时刻”。


(全文完)

如果你自己有电子文档需要在线阅读的需求:
如果你有word\Excel\ppt文档需要在线阅读的需求:
如果你希望你的电子文档在手机、平板、电脑阅读时进度同步的需求:
可以试试【个人文档管理平台】:www.mcbook.site

一杯奶茶钱就可以成为会员,省去了文档在公司/家里/邮箱 传来传去的麻烦。

更多技术文章见公众号: 大城市小农民

推荐阅读:个人文档/书籍管理平台介绍

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询