
1. 项目概述1.1 CNN可视化到底在解决什么问题CNN卷积神经网络实际应用中是一个标准黑盒——数据进去结果出来中间过程几乎不可见。调试网络时经常遇到这种情况模型在测试集上准确率掉了一截但我们只知道它表现变差不知道具体原因。可能是网络层数加深后梯度没有正常回传可能是某些通道一直在输出恒定值死神经元也可能是模型关注了背景而非目标主体。PyTorch可视化系列第二篇聚焦CNN可视化正是为了解决这类问题。其核心价值在于两个层面一是让网络内部状态具象化——特征图、卷积核、梯度和类激活热力图能直接反映模型在哪里、看什么、学成什么样二是让调试从盲目试错变成有据可循——纯粹调学习率、堆层数往往靠运气但看着特征图来判断网络是否学到了有效表征反而更直观。往下读的适合人群大致分两类正在做深度学习研究或毕设的在校生需要把模型内部机制讲清楚、画图写论文已经在做CV方向工程落地的开发者需要诊断模型失效原因、分析误判样例。两类读者诉求不同但对可视化方法本身的需求是共通的。1.2 本博文内容范围说明PyTorch可视化涉及的面非常广数据分布、训练曲线、参数分布、梯度分布、特征图、卷积核、注意力图都属于可视化范畴。本篇只聚焦CNN相关覆盖四个方向特征图可视化、卷积核可视化、梯度可视化和Grad-CAM类激活热力图。训练曲线这类属于训练监控范畴的内容不在本篇展开。工具方面以PyTorch原生API为主体辅助matplotlib和numpy完成绘图和后处理。不依赖额外的可视化库比如某些封装好的可视化工具包因为自己实现一遍才能理解里面的原理和坑点。代码版本基于PyTorch 1.x以上通用写法在所有较新版本中均可直接运行。2. 核心思路CNN可视化方法论2.1 从模型在算什么到模型在学什么做CNN可视化要区分两个层次刚接触可视化的人最容易在这件事上犯迷糊。第一个层次是模型在算什么对应的是前向传播过程中的中间产物——特征图和卷积核。特征图反映的是当前输入在这个层被提取成了什么样的模式组合卷积核反映的是网络在这个位置学到了什么样的局部特征模板。它们回答的问题是模型对某一张具体的图做的具体处理过程。比如猫脸图片输入网络后第一层可能关注到边缘和纹理中间层聚焦到眼睛耳朵这类部件深层则形成整体性的语义概念。第二个层次是模型在学什么对应的是网络学习过程中形成的决策依据——对应刻画的是梯度信息和类激活权重。梯度方向的强弱反映损失函数对每个像素变化有多敏感类激活图反映模型的分类决策主要依赖图像的哪些区域。这些可视化结果不依赖于单张输入的内容变化而是能解释模型整体的行为模式。工程调试中最实用的衔接方式是先用特征图看网络有没有在算正确的东西再用类激活图看模型决策的依据是否合理。两者配合使用常见问题基本都能定位。比如模型总是把输错分类先看特征图确认关键区域的纹理有没有在浅层被提取出来如果很清晰但最终结果仍然错误就去检查深层和分类头的梯度流向。2.2 可视化方案选型的取舍逻辑实际做可视化时有多种方案可选各有侧重。特征图可视化的实现方案有两种常用形态一种是直接拿中间层输出做归一化后转成图像另一种是把不同通道叠加成合成图配合最小值和最大值来观察激活的动态范围。卷积核可视化比特征图麻烦一点。卷积核本质是一个小的张量直接看它数值意义不大——实际值是模型在训练过程中逐渐调整的权重要看模式需要配合归一化和放缩。梯度可视化通常有两种形态对输入图像的梯度可视化也就是常说的显著性图和对卷积核参数的梯度统计。对参数的梯度统计更多用在训练过程诊断上比如某个层的梯度一直极小说明这个层的学习基本停滞。本文这部分重点展示对输入图像的梯度可视化因为它能直观反映模型对于不同空间位置像素的关注程度。Grad-CAM则是另一个维度它结合了特征映射和梯度信息。它的核心优势是特征图可视化只能告诉我们网络提取了什么但Grad-CAM能告诉我们模型做判断时依据什么两者结合才是完整的模型解读。2.3 可视化需要配合数据集来解读结果单看一张特征图能说明的问题有限真正有价值的做法是可视化之后对照数据集去做推断。我的习惯做法是准备一个小型测试集大概5到10张图包含正例、难例、负例。将这些图分别过网络取特征图然后横向对比。举个例子某次我用一个欠拟合的模型做二分类任务单独看特征图感觉每张图的激活区域分布挺均匀没什么明显问题。后来同时捞出训练集和验证集的同类特征图做对比才发现差距很大——训练集特征激活数值高验证集激活数值已经缩水了一半这才快速定位到过拟合问题。如果不做数据集对比单独可视化的信息量是远远不够的。还有一个容易忽略的点可视化的结论必须与任务目标耦合。比如分类任务里激活主要落在目标物体上才是合理的落在背景上就需要警惕分割任务中特征图边缘的锐利程度很重要检测任务里特征图能否很好地区分重叠目标更关键。脱离了任务目标去谈特征图长什么样意义会大打折扣。3. 特征图可视化实战3.1 hook机制的用法与原理PyTorch中做特征图可视化最常见的方式是使用hook。hook分为两种register_forward_hook和register_backward_hook。前者在前向传播结束后调用可以拿到该层的输出后者在反向传播时调用可以拿到梯度信息。forward hook的使用逻辑可以这样理解——在神经网络的计算图里每一层就是一条流水线上的工序。hook相当于在特定工序后面装了一个旁路探头不影响主流程但能把经过这一工序的产品形态记录下来。import torch import torch.nn as nn from torchvision import models # 定义一个捕获输出的容器 activation {} def get_activation(name): def hook(model, input, output): activation[name] output.detach() return hook model models.resnet18(pretrainedTrue) model.eval() # 在指定层注册hook这里以layer4的最后一个Block输出为例 model.layer4[-1].register_forward_hook(get_activation(layer4_最后阶段))注册hook有几个值得注意的关键点hook输出必须用.detach()断开计算图因为特征图可视化只是推理阶段的操作不参与梯度计算。如果不detach大批量特征图会持续保留计算图引用引起显存累积跑几批数据后显存就爆掉了。hook函数签名格式是固定的model, input, output三个参数缺一不可。input是tuple类型即便只拿到一个张量它也是tuple的第一个元素。不熟悉的读者经常在这里出错。在model.eval()模式下注册会更好。训练模式下的BatchNorm层行为会随batch统计量变动得到的特征图分布不稳定不利于观察规律。3.2 特征图归一化与通道排布拿到特征图后shape通常是 [B, C, H, W]直接显示不现实因为通道数动辄几十上百。需要先确定显示哪些通道然后对每个通道做归一化再拼成网格图。归一化这一步直接决定可视化的效果。原始特征图的数值范围可能是[-0.5, 1.2]这类区间而且大部分值集中在一个很窄的范围里。我的做法是先对整个特征图取绝对值然后做min-max归一化到[0, 1]区间用vmin/vmax参数控制显示范围。import matplotlib.pyplot as plt import numpy as np def normalize_channel(channel): channel channel - channel.min() channel channel / (channel.max() - channel.min() 1e-5) return channel def show_feature_maps(feature_map, max_channel16, cols4): feature_map feature_map.squeeze(0) # 去掉batch维度 C, H, W feature_map.shape for idx in range(min(C, max_channel)): channel_data feature_map[idx].numpy() channel_data normalize_channel(channel_data) plt.subplot(max_channel // cols, cols, idx 1) plt.imshow(channel_data, cmapviridis) plt.axis(off) plt.title(fchannel_{idx}, fontsize8) plt.tight_layout() plt.show()网格排布时cols的选择会影响整体观感。通道数较少16个以下时4列排布比较舒服通道数较多比如64个时就改成8列。一个更实用的技巧是自适应列数。import math cols int(math.sqrt(min(C, max_channel))) # 方阵排布 rows math.ceil(min(C, max_channel) / cols)3.3 颜色映射的选择和技巧特征图可视化的颜色映射方案不能随便用。matplotlib默认的viridis在特征图显示中是最常见的因为它的渐变比较平滑从暗紫到明黄激活强弱差异肉眼分得很清楚。不同颜色映射的适用场景是这样的viridis通用选择关注激活强度分布时最好用gray需要叠加在原图上观察位置关系时使用。灰度图不遮挡太多原图细节jet对比强烈但在低值区域会出现视觉混淆青色到蓝色过渡不明显。如果关注的是低响应区域反而不容易看清我自己实际调试时一般准备两张图一张用viridis看整体激活分布一张用gray叠加到原图上观察激活位置与目标物的对应关系。3.4 多层级特征图对比特征图可视化最核心的用法是观察不同层级的特征演变。通常准备一个包含主体的彩色图像样本分别提取模型中各阶段卷积输出的特征图放在同一张面板里对比。以ResNet为例前置卷积输出的特征图分辨率高、通道少显示出来更多是边缘和颜色斑块layer2输出的特征图开始出现结构性的纹理layer3输出的特征图聚焦到局部部件layer4输出的特征图分辨率降低但语义更抽象。这个演进过程恰好能验证从细节到抽象的特征提取逻辑。做多层级对比时有一个值得注意的细节别把不同层的特征图直接用同一个颜色映射范围去显示。浅层特征图数值分布和深层特征图差异极大直接统一尺度显示浅层的对比度会被拉得很低深层的细节又容易过曝。我的做法是每层独立做归一化再分别显示同时把各自的激活均值标注在图上方便于追踪数值变化的趋势。这条思路在很多实际项目中验证过。一个曾做过的分类实验模型效果提升不大但难以定位问题出在哪一层。提取所有层特征图后直观看到从layer2到layer3时特征激活开始变得稀疏——很多通道输出接近0说明特征提取在这一层遇到瓶颈后续只需要针对这个层做重点调优。如果没有可视化这个结论很难如此快速定位。4. 卷积核可视化4.1 从参数张量到可视化图像卷积核是CNN里学习的核心参数它的本质是权重张量。对于单通道输入的灰度图卷积核可视化和图像显示几乎一致因为shape是[H, W]。对于三通道彩色输入的RGB图卷积核shape是[3, H, W]查看时也需要处理维度问题。比较常见的情况是某层有64个3x3的卷积核shape是[64, 3, 3, 3]也就是64个3通道的3x3权重矩阵。这种情况下直接看数值意义有限需要展开成适合人眼观察的形态。def visualize_kernels(conv layer): kernels conv_layer.weight.data.cpu().numpy() # [out_channels, in_channels, kH, kW] out_c, in_c, k_h, k_w kernels.shape cols 8 rows math.ceil(out_c / cols) fig, axes plt.subplots(rows, cols, figsize(cols * 1.5, rows * 1.5)) for out_idx in range(out_c): for in_idx in range(min(in_c, 3)): # 对输入通道做处理 kernel kernels[out_idx, in_idx] # 归一化 k_min, k_max kernel.min(), kernel.max() kernel (kernel - k_min) / (k_max - k_min 1e-5) ax axes[out_idx // cols, out_idx % cols] ax.imshow(kernel, cmapviridis) ax.axis(off) plt.show()4.2 卷积核可视化在调试中的意义卷积核可视化能回答的核心问题模型学到了什么模式的局部特征。以训练好的模型为例第一层卷积核通常呈现出方向性的边缘检测特征——竖线、横线、斜线以及各种角度的边缘。中间层卷积核则出现更复杂的纹理组合模式。如果某个模型的卷积核显示出来杂乱无章几乎看不到有序模式八成说明训练过程存在问题比如学习率设置不当、数据量不足或者正则化过强导致特征没有学出来。初学可视化的人常有个误区要特征图有可视化效果卷积核也急于换成彩色显示。但实际上卷积核可视化最好用灰度显示因为卷积核本身是权重值彩色映射容易分散注意力。灰度图更能清晰看出边缘方向的倾向性。第一层卷积核还有助于判断数据预处理是否恰当。如果输入图片没有做标准化视觉上的卷积核会出现大面积高亮或大面积灰暗的区域说明权重分布偏斜。合理的卷积核可视化应该在不同位置有清晰的方向分化。5. Grad-CAM类激活热力图5.1 Grad-CAM的原理简述Grad-CAM是目前工程中最常用的CNN可解释性方法。它在CAM的基础上引入了梯度信息。CAM需要在最后的全连接层做特殊的全局平均池化操作限制了网络结构必须符合特定形态而Grad-CAM利用反向传播的梯度计算权重通用性大幅提升几乎所有CNN结构都能直接使用。Grad-CAM的核心计算逻辑分四步拎出来拆解第一步是获取目标特征图A以及该特征图对最终得分的梯度G。目标特征图通常选择最后一个卷积层输出因为最后一层卷积的输出已经具备较强的语义信息分辨率又不至于太低。第二步是计算每个通道的权重。权重取该通道梯度对所有空间位置的均值weights torch.mean(grad, dim(2, 3)) # 对H和W维度求平均第三步是用权重对特征图做加权求和得到初步的类激活映射cam torch.sum(weights[:, :, None, None] * activations, dim1)第四步是套ReLU激活函数只保留正向贡献部分cam torch.relu(cam)为什么最后要ReLUCAM的设计初衷是找到模型判断为目标类时所依据的区域。特征图中负值部分代表了对其它类别的支持套上ReLU后负值置零激活图只剩对目标类别的正贡献区域可视化效果更干净。这也是Grad-CAM和得分加权CAM的核心区别之一。5.2 Grad-CAM完整实现代码以下代码实现了完整流程包括hook注册、梯度反向传播、结果后处理。import torch import torch.nn.functional as F import cv2 import numpy as np import matplotlib.pyplot as plt class GradCAM: def __init__(self, model, target_layer): self.model model self.target_layer target_layer self.activations None self.gradients None self._register_hooks() def _register_hooks(self): def forward_hook(module, input, output): self.activations output.detach() def backward_hook(module, grad_input, grad_output): self.gradients grad_output[0].detach() self.target_layer.register_forward_hook(forward_hook) self.target_layer.register_backward_hook(backward_hook) def generate_cam(self, input_tensor, target_classNone): output self.model(input_tensor) if target_class is None: target_class torch.argmax(output, dim1).item() self.model.zero_grad() one_hot torch.zeros_like(output) one_hot[0, target_class] 1 output.backward(gradientone_hot) weights self.gradients.mean(dim(2, 3), keepdimTrue) cam torch.sum(weights * self.activations, dim1, keepdimTrue) cam F.relu(cam) cam F.interpolate(cam, sizeinput_tensor.shape[2:], modebilinear, align_cornersFalse) cam cam.squeeze().cpu().numpy() cam (cam - cam.min()) / (cam.max() - cam.min() 1e-5) return cam def overlay_on_image(self, img_bgr, cam, alpha0.5): heatmap cv2.applyColorMap(np.uint8(255 * cam), cv2.COLORMAP_JET) overlay cv2.addWeighted(img_bgr, 1 - alpha, heatmap, alpha, 0) return overlay # 使用示例 from torchvision import models model models.resnet18(pretrainedTrue) model.eval() gradcam GradCAM(model, model.layer4[-1]) cam gradcam.generate_cam(input_tensor) result gradcam.overlay_on_image(img_bgr, cam)5.3 注意事项目标类选择与梯度清空Grad-CAM里面有三个容易被忽视的坑。第一个坑是backward_hook注册方式。PyTorch的后向hook和其他hook不同官方文档标注backward hook在部分场景下行为会变化。更稳妥的方式是直接利用forward hook拿到activation然后通过torch.autograd.grad计算梯度绕开backward_hook的不确定性class GradCAM: def __init__(self, model, target_layer): self.model model self.target_layer target_layer self.activations None # 只注册forward hook self.target_layer.register_forward_hook(self._save_activation) def _save_activation(self, module, input, output): self.activations output.detach() def generate_cam(self, input_tensor, target_class): output self.model(input_tensor) if target_class is None: target_class output.argmax(dim1).item() one_hot torch.zeros_like(output) one_hot[0, target_class] 1 self.model.zero_grad() # 通过autograd.grad直接计算梯度 gradients torch.autograd.grad( outputsoutput, inputsself.activations, grad_outputsone_hot, retain_graphTrue )[0] weights gradients.mean(dim(2, 3), keepdimTrue) cam torch.sum(weights * self.activations, dim1, keepdimTrue) cam F.relu(cam) # 后续处理同上第二个坑是model.zero_grad()不能忘。如果之前有过反向传播梯度缓冲区会残留上一次迭代的梯度不清理会让权重计算混入干扰信息热力图会错乱。第三个坑是目标类别的语义。如果我们想观察模型对猫这个类别的判断依据指定的target_class应该对应猫的索引。有些分类任务中模型预测错误如果想看模型为什么预测错也可以用argmax获得的预测类别而不是真实类别。这个选择会直接影响热力图的结构需要根据分析目标决定。5.4 Grad-CAM结果解读的方法拿到Grad-CAM热力图之后怎么有效解读不管代码写得多漂亮不会解读的话一切白搭。第一层判断是热力图区域与目标物体是否重合。如果热力图大部分集中在正确目标上说明模型依据合理如果集中在无关背景通常说明模型找错了关键特征可能存在shortcut learning的问题。第二层判断是热力图区域是否稳定。同一张图多次运行热力图应该基本一致。跑几次得到明显不同位置的激活说明模型对这个样本预测不稳定。第三层判断是配合置信度一起看。如果模型置信度很高比如0.95以上但热力图主要落在背景上这是典型的模型学到了干扰特征。这类样本在测试集里的表现很可能是虚高的。热力图可视化每张图判读完后还可以做聚合分析。将测试集中所有误分类样本的热力图叠加平均如果在某个固定区域比如图像的左上角频繁出现高激活那很可能数据集中存在对分类结果有偏见的结构清洗数据时重点关注这个区域。5.5 从CAM到Grad-CAM的演进关系如果不理解CAM到Grad-CAM的演进关系容易混淆两者调参时的行为差异。深层网络最后一层卷积的每个通道对分类结果都有加权贡献。CAM的想法是找到这个线性关系的系数——也就是权重然后用这些权重给特征图加权求和得到激活区域。CAM方法要求全连接层前必须有全局平均池化层。这个限制在实际项目中很麻烦因为预训练模型不一定自带这种结构。为了突破这个限制研究者改用了反向传播的梯度作为权重信息。Grad-CAM因此在适用性上有了本质突破。这个演进过程对工程实践有一个直接启发做可视化工具时优先考虑基于梯度的Grad-CAM而不是基于结构的CAM。前者对于绝大多数现成模型都能直接运行API接口也简洁得多。6. 其他扩展梯度可视化与显著性图6.1 对输入图像的梯度计算对输入图像计算梯度也就是通常所说的显著性图。它的思路很直接求损失对输入像素的偏导梯度大的像素意味着改变它会对最终结果产生较大影响这些区域是模型决策的重要支撑点。def compute_saliency(model, input_tensor, target_class): input_tensor.requires_grad_(True) output model(input_tensor) if target_class is None: target_class output.argmax(dim1).item() loss output[0, target_class] loss.backward() saliency input_tensor.grad.abs().squeeze().cpu().numpy() # 对通道维度做max得到单通道显著性图 saliency np.max(saliency, axis0) saliency (saliency - saliency.min()) / (saliency.max() - saliency.min()) return saliency对输入图像做梯度计算的最大价值在异常检测和鲁棒性分析。正常分类器的显著性图通常贴合目标轮廓。如果显著性图分布在全图说明模型对目标位置的确定度很低。6.2 梯度可视化与Grad-CAM的互补使用训练过程中梯度信息的可视化是另一个重要方向。把一个batch所有样本在某一层的梯度拉出来做一个L2范数统计直方图观察梯度的离散程度。梯度范数过大或过小都能及时发现训练中的问题。梯度可视化与Grad-CAM结合使用时有一个比较实用的做法Grad-CAM告诉我们模型关注哪里梯度显著性告诉我们模型对哪些像素敏感。两者取交集得到的就是模型既关注又敏感的像素集合这是模型真正的注意力核心区。比如做缺陷检测项目时缺陷区域往往在图像中占比很小。单独看Grad-CAM激活可能分散在上百个像素上同时看梯度显著性可聚焦到缺陷边界那几十个像素。两个信息叠加可以极大压缩搜索空间用分水岭算法就能把缺陷轮廓精确找出来。6.3 用可视化方法做模型对比的实操思路同一种可视化方法用在不同的模型上对比结果可以承担模型选型的判断依据。拿分类任务举例我曾经同时保留了两个模型候选一个是轻量级的MobileNetV3版本一个是ResNet18版本两者在测试集上准确率几乎持平。只看测试指标无法作出决策时我提取了两个模型在相同输入上的Grad-CAM热力图做对比。结果是某个难例样本中ResNet18的热力图精准落在主体区域上而MobileNetV3则把一部分激活落在了强干扰背景上。再抽样几十个难例对比发现ResNet18在这类样本上的解释性明显更可靠最终选择ResNet18。视觉化的对比比纯粹的数字对比更直观。这种对比思路也可以用于模型蒸馏的场景如果Student模型的Grad-CAM与Teacher模型的Grad-CAM分布差别很大说明蒸馏的特征信息还没有完整对齐还需要加强蒸馏损失的设计。7. 常见问题与排查技巧实录7.1 hook不生效或覆盖问题hook注册后如果打印activation字典发现某些层的数据始终为空要检查hook是否在forward之前注册。一种非常常见的错误是注册完hook后没有调用model.eval()导致某些BatchNorm层在前向过程中会更新内部状态影响了中间层的输出数值。hook覆盖问题多见于一个模型上注册多个hook且复用同一个activation字典key的情况。直观表现是可视化结果总是最后一层的内容前面的全被覆盖。解决方案是给每个hook的key加上层的唯一标识或者在每轮前向推理前清空字典activation.clear() with torch.no_grad(): output model(input_tensor)7.2 尺寸不匹配的问题Grad-CAM里有个常见错误weights是[B, C, 1, 1]的shape而activations是[B, C, H, W]的shape两者相乘时如果不做扩展会直接报错。接地气的处理方法是用[:, :, None, None]把权重的维度补上或者在mean操作时用keepdimTrue直接从源头保留维度。另一个尺寸问题是上采样与原图对齐。如果目标层特征图分辨率和原图差距过大直接F.interpolate会出现边缘锯齿。展示效果不好时优先把特征图放大到与输入一致再用cv2叠加在原图上。7.3 输出结果全黑或全白特征图全黑一般是归一化出了问题。debug时手动打印一下特征图的最小值和最大值如果max - min的值极小比如1e-7这个数量级说明该通道几乎没有有效响应这种通道本身也不值得展示。如果归一化除以了一个接近0的数出来的图像几乎全是噪声也是需要检查的地方。Grad-CAM全黑通常是ReLU前就已经出现了异常——比如activation从hook拿到的数据没有detach在反向传播时进入了参数梯度路径导致某些层计算过程中数值被清空。用torch.autograd.grad方式实现的话把梯度输出也配合clone()一份会更保险。7.4 显存与速度优化技巧可视化大批量数据时最大的性能瓶颈是前向过程中所有中间特征都被保留。我的两个实用技巧第一个是with torch.no_grad()。如果只做特征图可视化完全不需要梯度信息时用no_grad包裹前向中间结果不会构建计算图显存占用直接降一个量级。Grad-CAM就不同了——它需要反向传播的梯度因此不能用no_grad包裹。第二个技巧是分块处理。要可视化1000张图的特征图时别一次全堆进显存分批跑批量为32的流程每次只保留指定层的输出再在后处理阶段拼装。Tensor操作完成后用delgc.collect()及时释放中间引用。7.5 热力图叠加显示效果优化Grad-CAM原图叠加之后如果出现一片刺眼的红色是alpha值太大。具体项目的经验分类网络返回的热力图比较粗糙alpha用0.45左右高位叠加能看清轮廓分割或检测网络的热力图通常已相对精细alpha用0.3以下就够了避免遮挡细节。还有一个细节容易被忽略默认使用COLORMAP_JET热力图的高值区域是红色低值区域是蓝色。如果任务本身更关注哪里激活了而不是哪里没激活可以反色显示把红色对应到弱激活区域更容易快速捕捉关键信息。这个调整在机器判读过程中效果提升明显。8. 系列后续扩展方向本系列目前覆盖了CNN静态可视化的主要内容。PyTorch可视化还有一个非常庞大的分支是对训练过程的动态可视化包括学习率变化、梯度范数变化、权重分布迁移、激活稀疏度变化这些维度这些内容适合做成独立专题来详细展开。对于已经有了手脚基础、想更进一步提升可视化能力的读者建议下一步研究一下注意力可视化在Transformer类模型中的实现方式以及如何把特征可视化和流形学习结合起来做高维特征的低维投影。前者在近期CV领域十分常用后者在特征分布诊断上价值不小。回到这一篇的核心CNN可视化的本质就是用最直观的方式回答模型到底在做什么的问题。代码本身并不复杂难的是把可视化结果和任务目标、数据分布、模型形态关联起来做合理推理。把这篇文章里的几个方法跑通把解读逻辑的思维框架建立起来你在调试模型时的效率大概率会显著提升。