
2. 核心细节解析与实操要点2.1 Tensor的基础认知先搞懂我们要处理的对象Tensor张量这个词听起来很高大上但实际上就是多维数组的统称。标量是0维张量向量是1维张量矩阵是2维张量再往上堆叠就是3维、4维甚至更高维的张量。在深度学习项目里我们经常和形状为 (batch_size, channels, height, width) 的4维张量打交道也就是常说的 NCHW 布局。比如一张 224x224 的 RGB 图片传入模型后往往变成 (1, 3, 224, 224) 的张量。而有些框架用的是 NHWC 布局即 (batch_size, height, width, channels)。搞清楚布局至关重要因为后续做 numpy 数组转换、数值归一化、维度重排时顺序错了结果就完全不对。我做这个脚本时的第一步就是先确认我的张量到底长什么样、在哪个维度上存放了什么信息。使用 nested_lookup 函数是另一种办法。自己写的递归函数遍历整个 JSON 树只要键名包含 target_key就把对应的值收集起来。例如 target_key 设为 tensor就可以把嵌套结构里所有 tensor 字段的值都提取出来。实测下来这种方式最灵活尤其在应对多层级嵌套、键名不固定的模型文件时特别有效。这里要特别提醒解析 ONNX 文件时graph.input 和 graph.output 里的张量定义通常只是元信息名称、形状、类型真正的权重数据在 graph.initializer 里。而 PyTorch 的 .pt 文件用的是 zipfile 格式内部是多个二进制数据块需要用 torch.load 或 safetensors 库来解析。所以脚本里我做了分支处理根据文件扩展名自动选择解析器。2.3 获取张量数值深入了解 API 的差异拿到了张量对象之后要把它变成能写进 PSD 的数值数据这一步有不少门道。PyTorch 的 tensor.detach().cpu().numpy() 是经典组合拳。detach() 用来切断梯度计算图中的关联防止后续操作影响梯度cpu() 确保数据在内存中而不是显存中numpy() 把数据转为 ndarray。如果用 .numpy() 直接操作遇到 requires_gradTrue 的张量会报错遇到 GPU 上的张量也会报错所以要养成先 detach 再 cpu 的习惯。TensorFlow 那边则是 tensor.numpy()但需要确保张量在 eager 模式下。如果遇到的是 tf.Tensor 但处于 graph 模式下就需要用 tf.sess.run(tensor) 来取值。不过幸运的是大多数模型导出时已经是 eager 模式了直接 .numpy() 基本够用。numpy 数组的 dtype 也要注意。float32 是常见格式但 PSD 文件里需要的是 8bit 或 16bit 整数所以必须做转换。float32 数值范围通常不是 0-255直接 round 转 uint8 会丢失负数和大于 255 的信息必须先做归一化或截断。2.4 PSD 文件结构探秘为什么需要 psd-tools 这类库PSD 是 Adobe Photoshop 的专有格式底层结构相当复杂。文件由文件头、颜色模式数据、图像资源、图层和蒙版信息、图像数据等部分组成。手写二进制解析器去生成 PSD 不太现实成熟的库能帮我们搞定大部分工作。psd-tools 是一个 Python 库支持读取和写入 PSD 文件。它提供了面向对象的 API可以创建 PSD 文件、添加图层、修改混合模式、设置透明度等。底层实现里它对 PSD 的字节流做了解析和组装屏蔽了底层二进制细节。不过坦白讲psd-tools 的功能偏向读取和简单编辑复杂特效的支持有限。如果只是把张量数据变成带有简单遮罩的 PSDpsd-tools 足够用。我的脚本核心诉求就是把数据可视化所以选它没问题。如果要做更复杂的编辑操作比如添加滤镜、调整图层、路径等那可能要考虑用 Photoshop 自身的脚本接口或者换用商业级第三方库。写 PSD 时psd-tools 要求传入 PIL.Image 对象而不是直接传 numpy 数组。所以流程是numpy 数组 → PIL.Image → PSD 图层。这个转换过程看起来多了一步但其实是很有必要的因为 PIL.Image 自带格式转换、尺寸调整、颜色模式转换能力处理起来比直接操作 numpy 数组更顺。2.5 实际操作中的坑Pillow 和 psd-tools 的版本兼容性用 psd-tools 写 PSD 时给它传 PIL.Image 是常规操作但版本兼容性是个常见坑。psd-tools 某些版本对 Pillow 的版本有约束如果 Pillow 版本太新可能 API 不兼容导致报错。我在脚本里 lock 了 Pillow 的版本范围实测下来 psd-tools Pillow 8.x 的组合最稳。另一个坑是 PSD 图层名称不能包含特殊字符某些版本的 psd-tools 遇到中文图层名会编码出错。解决方法是把图层名改为 ASCII 字符或者用拼音或者干脆用编号替代。我的脚本里默认用 layer_xxx 这种命名方式省心。3. 实操过程与核心环节实现3.1 准备环境三步搭好运行环境写 Python 脚本前的环境准备是老生常谈但也是最容易出问题的地方。我用的是 Python 3.8测试环境是 Windows 10当然 macOS 和 Linux 上也跑过只要依赖装好基本都兼容。第一步创建虚拟环境。Python 3.8 自带 venv 模块节省了安装 virtualenv 的功夫。在项目目录下执行 python -m venv venv 就能创建一个干净的环境。激活方式在 Windows 上是 venv\Scripts\activateLinux/macOS 上是 source venv/bin/activate。第二步安装依赖。requirements.txt 里我固定了这几个库numpy、Pillow、psd-tools、torch、tensorflow、onnx、netron。这里说明一下torch 和 tensorflow 体积较大如果只是做可视化展示可以只安装其中一个。onnx 是用来解析 ONNX 模型的。第三步准备测试用的张量。没有现成模型的话可以先手动创建张量来跑通流程。用 torch.randn(3, 224, 224) 生成一个三通道的随机张量或者用 torch.ones(3, 64, 64) 生成全 1 张量这些数据足够测试脚本的整个流程了。3.2 编写核心转换函数从张量到图像的完整转换流程核心转换流程可以拆成两个函数tensor_to_image 和 image_to_psd。前者把张量变成 PIL.Image后者把 PIL.Image 写入 PSD 图层。tensor_to_image 函数里做的事接收 numpy 数组作为输入因为各种框架的张量已经统一转成 numpy 数组了。如果数组有4个维度先选择 Batch 和 Channel默认取第一个 Batch如果 Channel 大于1做降维或映射处理。将数据归一化到 [0, 1] 区间。这里我用的方法是 (data - data.min()) / (data.max() - data.min())这样能保留数据的相对分布并且适合可视化。乘以 255 并转为 uint8。如果有3个通道按 RGB 组装如果只有1个通道用灰度模式。image_to_psd 函数里做的事接收 PIL.Image 对象。调用 psd_tools.api.PSDImage.new() 创建新 PSD。生成新图层并设置图层名称、混合模式。保存 PSD 文件。组合起来主流程就是读取模型文件 → 解析张量 → 转为 numpy 数组 → 转为 PIL.Image → 写入 PSD。3.3 代码逐行拆解先看核心代码段我会把关键逻辑逐行解释清楚。import os import numpy as np from PIL import Image from psd_tools import PSDImage from psd_tools.api.layers import PixelLayer这些是基础依赖。os 用来处理路径numpy 做数值计算Pillow 处理图像psd-tools 处理 PSD。def tensor_to_image(tensor_data, modeauto): arr np.array(tensor_data) if arr.ndim 4: arr arr[0] if arr.ndim 3 and arr.shape[0] 3 and arr.shape[2] ! 3: arr np.transpose(arr, (1, 2, 0)) if arr.dtype ! np.uint8: arr (arr - arr.min()) / (arr.max() - arr.min() 1e-8) arr (arr * 255).astype(np.uint8) if arr.ndim 2: return Image.fromarray(arr, modeL) elif arr.ndim 3 and arr.shape[2] 3: return Image.fromarray(arr, modeRGB) else: raise ValueError(fUnsupported array shape: {arr.shape})逻辑拆解先统一转成 numpy 数组因为不管来自框架还是文件解析最终都会落到这个格式。如果有 batch 维度默认取第一个样本。如果是三通道但维度顺序是 CHW就调整成 HWC因为 Pillow 的图像数据要 HWC。归一化时加了 1e-8 避免除零错误。最后一层灰度模式下单通道就输出 L 模式三通道就输出 RGB 模式。def create_psd_from_images(images, output_path, layer_namesNone): psd PSDImage.new(modeRGB, sizeimages[0].size) for idx, img in enumerate(images): layer_name flayer_{idx} if layer_names is None else layer_names[idx] layer PixelLayer(psd, namelayer_name) layer.visible True layer.opacity 255 psd._layers.append(layer) layer.image img psd.save(output_path) return psd创建 PSD 时指定 RGB 模式和图像尺寸。生成图层时给每个图层起名字方便后面在 Photoshop 里识别。图层不透明度设为 255也就是全不透明。把图像数据挂到图层的 image 属性上最后保存。这里的 PixelLayer 用法是 psd-tools 推荐的方式但不是唯一方式。有些版本支持 layer.save() 方式单独保存或者用 psd.append_layer() 方法。我实测下来 PixelLayer _layers 手动追加的写法在某些版本稳定一些但为了保险起见可以试试 psd-tools 提供的 paste(psd, img, (x, y)) 或者 add_text_layer 等方法。我自己的脚本里用的是 PSDImage.composite_image 配合 paste 方式这样更接近官方推荐。3.4 完整的执行流程有了核心函数后主流程长这样解析输入文件判断是 .pt、.onnx、.pb 还是其他格式。读取张量数据并转换为 numpy 数组。调用 tensor_to_image 得到 PIL.Image。如果有多个张量逐个处理并收集图像。调用 create_psd_from_images 把所有图像写入一个 PSD 文件。这里有个设计思路与其在内存里反复转换不如先用一个 list 收集中间图像最后一次性写入 PSD。这样如果中途某个张量处理失败已经处理完的图像还在内存里还可以继续如果直接写文件半路失败可能导致文件损坏。另外把多个张量放到同一个 PSD 里也方便在 Photoshop 里通过隐藏/显示图层来对比不同通道或不同层的信息。3.5 用命令行包装脚本为了让脚本更好用我加了 argparse 参数支持指定输入文件、输出文件、目标张量名称等。比如 python draw_tensor2psd.py0126v1.py --input model.pt --output result.psd --tensor-name conv1.weight这样在命令行就能直接控制。import argparse def main(): parser argparse.ArgumentParser(descriptionConvert tensors to PSD file.) parser.add_argument(--input, requiredTrue, helpInput model file path.) parser.add_argument(--output, defaultoutput.psd, helpOutput PSD file path.) parser.add_argument(--tensor-name, default, helpTarget tensor name, empty means all.) args parser.parse_args() tensors parse_model_file(args.input) if args.tensor_name: tensors {k: v for k, v in tensors.items() if k args.tensor_name} images [tensor_to_image(v) for v in tensors.values()] create_psd_from_images(images, args.output, layer_nameslist(tensors.keys())) print(fSaved to {args.output}) if __name__ __main__: main()3.6 处理多个张量的策略一个模型文件里通常有成百上千个张量全部转换出来图像数量多且不适于直接观察。我的策略是默认按名称过滤比如只提取名字中包含 weight、bias、conv 等关键字的张量如果参数指定了具体张量名就只转换那一个如果不做任何过滤就把所有张量堆叠到同一个 PSD 的不同图层里。这样做有一个好处可以快速对比同一模型不同层的权重分布。例如把 conv1.weight 和 conv2.weight 放在同一个 PSD 里切换图层肉眼就能看出平滑度、分布范围差异这是单纯用 print 输出数值看不到的。4. 常见问题与排查技巧实录4.1 张量维度太长导致图像尺寸异常怎么办最典型的报错是图像尺寸为 0 或过大。出现这种问题的原因是张量里有巨大的维度值尤其是全连接层或注意力机制里的权重形状可能达到 (1024, 1024) 甚至更大。直接转成图像还好但要生成 PSD 时图层尺寸过大会导致内存爆掉。我的处理策略是设置 max_size 参数比如 img.size[0] * 2 4096 时做等比例缩小。这个缩放在 PIL.Image 的 resize 中干用 Image.LANCZOS 插值保证质量。损失一些细节换来可打开性值得。4.2 数值分布太集中导致图像全黑/全白怎么办这个问题非常经典。权重值如果集中在 0 附近归一化后大量像素会落在灰度中段人眼看就是灰蒙蒙一片细节不清楚。预防的方法是用百分位裁剪比如把 1% 和 99% 分位作为范围中间数据线性拉伸超出部分截断。这样能把主要数据分布拉开图像细节更清晰。代码上可以用 np.percentile(arr, [1, 99]) 得到裁剪范围再用 np.clip 截断最后做归一化。实测下来比 min-max 归一化效果好很多。4.3 GPU 上的张量无法直接转成 numpy 数组这是新手经常踩的坑。GPU 上的张量在显存里numpy 数组无法直接访问显存数据。必须先调用 .cpu() 把数据移到内存再用 .numpy() 转换。如果你的张量是 PyTorch 的并且开了梯度记录还要先 .detach()。另外TensorFlow 的 GPU 张量如果放在 tf.device(/GPU:0) 上也是类似的处理方式。4.4 PSD 图层数量过多导致 Photoshop 卡顿有些模型例如 Transformer张量特别多如果在 PSD 里创建了几百个图层Photoshop 打开时可能会卡死。我的建议是默认只导出前 N 个张量比如前 20 个或者合并部分通道到一个图层中。合并的方式是把多通道分别作为 RGB 通道拼到一张图里这样一张图就能看多个通道的信息。4.5 文件太大或者输出失败大尺寸、高精度16bit数据直接生成 PSD 会导致文件体积非常大。常见的解决办法是缩小图像尺寸或者把数据类型降为 8bit。如果 PSD 里要放几十张大图文件可能轻松超过 200MB保存时间也很长。我加了压缩参数实在不行就只保存当前图层而不是一次性全部保存。4.6 老街避坑心得版本兼容性和命名规范psd-tools 库是我踩过最多坑的地方。它的一些 API 在不同版本间有调整比如 PSDImage.new() 的参数在不同版本可能行为不一样。因此我强烈建议在 requirements.txt 里固定版本避免意外升级带来的 API 变化。图层命名也要注意。中文名在某些版本下会乱码我建议统一用 ASCII 命名或者在命名后手动检查。如果必须用中文在脚本里做一层编码转换比如用 layer.name.encode(utf-8)实测能减少乱码概率但保险起见还是别用中文。另外直接操作 psd._layers 是内部 API不建议依赖。如果你的 psd-tools 版本不是常用的请优先用官方公开 API。Pillow 的 Image.fromarray 也可能因为 dtype 或 mode 不匹配报错所以在调用前打印 arr.dtype 和 arr.shape 是好习惯。5. 进一步的扩展玩法这个脚本虽然叫 draw_tensor2psd.py0126v1但它的基本能力可以扩展到不少场景。我最近就在用类似思路做模型权重热力图把 tensor 映射成颜色然后导出成 PSD 做人工检查。还有把不同 epoch 的模型权重并排导出用图层透明度做对比观察训练过程中权重变化。如果结合 netron 做模型可视化这个脚本可以作为补充工具netron 负责看计算图结构本脚本负责把具体权重数值变成可视觉检查的图像。两者搭配使用排查模型问题效率会高很多。另一个玩法是改变图层的混合模式和透明度。比如在 PSD 里把 conv1 和 conv2 的权重图叠在一起把上层设为 Difference 混合模式能直观看到两层权重之间的差异。这种可视化方法在分析权重初始化、剪枝效果、量化误差时非常实用。6. 最后的实操心得我在写这个脚本时最大的体会是可视化方向选对了能省一半调试时间。人眼对图像敏感度远高于对数字表格特别在处理权重分布、卷积核模式这类数据时把数值变成图片再用 Photoshop 做叠加对比很多细节一下子就能看出来。如果后续要扩展建议把脚本扩展成支持批量处理多个模型文件并加入自动对比功能。比如一次加载两个模型权重逐层做差异图最后输出一个对比 PSD。这个思路比纯数值计算直观得多适合用在模型蒸馏、微调对比、量化分析这类场景。最后再提一个细节把 PSD 保存后如果想在 Python 里继续验证可以使用 psd-tools 的读取 API 把它再读回来检查图层数、尺寸、像素值是否正确。这虽然多了一步操作但能有效避免文件生成错误带来的返工。如果你手头有现成的模型文件建议直接拿这个脚本跑一遍把一层卷积权重转成 PSD 看看效果不用在乎精度在乎的是把数据变成图像之后你能不能发现原本在数值里看不到的规律。视觉化的力量用过一次就回不去了。