☰
Caffe Net类源码深度解析:从prototxt到计算图的七步初始化
2026/10/14 7:55:22 网站建设 项目流程

1. 为什么今天还要读 Caffe 的 Net 类源码?

“Caffe 已经过时了”——这句话我听过不下二十遍,每次都是在新项目选型会上,由刚跑通 PyTorch Lightning 的 junior 工程师脱口而出。但去年接手一个某高校实验室遗留的视觉检测系统维护任务时,我打开代码仓库第一眼看到的,就是deploy.prototxt和model.caffemodel。没有 Python 脚本,没有requirements.txt,只有三行注释:“模型训练于2017年,部署于Jetson TX2,不可重训,仅可微调”。而那个微调需求,卡在了Net::Reshape()调用后某层 Blob 的 shape 异常上——报错信息里连具体哪一层都没说清楚。

这就是现实:Caffe 没有退出历史舞台,它只是沉到了产线深处,像老式工业PLC一样,在边缘设备、嵌入式视觉模组、早期AI芯片SDK配套工具链里持续运转。而当你需要真正理解它为何卡住、为何内存暴涨、为何某次 reshape 后梯度消失,唯一可靠的路径,就是回到net.hpp和net.cpp里,一行行看它怎么把.prototxt解析成计算图,怎么调度 Blob 内存,怎么组织前向/反向传播的执行顺序。

这不是怀旧,是工程刚需。Caffe 的Net类,是整个框架的中枢神经:它不负责卷积核计算(那是Layer的事),也不管内存分配细节(那是SyncedMemory的活),但它精确控制着数据流如何在层与层之间穿行、状态如何在训练/测试模式间切换、参数如何被统一管理与序列化。读懂它,你才能在不重训模型的前提下,安全地替换某一层、注入自定义预处理逻辑、诊断 layer 间 shape 不匹配的根因,甚至为老旧硬件做轻量级图优化。

关键词里虽然空着,但实际场景中高频出现的词是:Blob 生命周期管理、Layer 实例化顺序、ShareMode 语义、Reshape 触发时机、param_ 与 blobs_ 的双索引映射、forward_from/to 的剪枝逻辑。这些不是文档里几句话能讲清的概念,它们藏在Net::Init()的 300 行初始化逻辑里,藏在Net::ForwardFromTo()的循环嵌套中,更藏在Net::CopyTrainedLayersFrom()那段看似简单的参数拷贝背后——那里有对ShareMode的严格校验,有对param_索引越界的静默跳过,还有对blobs_与layers_长度不一致时的 panic 式断言。

所以,这篇分析不讲“Caffe 有多快”,也不对比“和 TensorFlow 有什么区别”。我们只聚焦一件事:当你写下shared_ptr<Net<float>> net(new Net<float>("deploy.prototxt", TEST));这行代码时,Net构造函数内部到底发生了什么?它如何把一份纯文本协议描述,变成一个可执行、可调试、可干预的运行时对象?这才是你在产线救火时,真正需要握在手里的那把螺丝刀。

2. Net 初始化全流程:从 prototxt 到内存图谱的七步构建

Caffe 的Net初始化绝非简单加载配置。它是一场精密的七步构建仪式,每一步都环环相扣,任何环节的疏漏都会导致后续Forward()崩溃。我曾在一个定制化人脸识别模块中,因跳过了第三步的layer->SetUp()而让Reshape()在第一次前向时直接 segfault——错误堆栈指向Blob::Reshape(), 但根因却在Net::Init()的第二步里。下面拆解这七步,每一步都附带真实踩坑点。

2.1 第一步:解析 prototxt,生成 LayerParameter 列表

Net::Init()的起点是ReadProtoFromTextFileOrDie(param_file, &param);。这里param是NetParameter类型,一个 Protocol Buffer 定义的嵌套结构。关键在于:它只做语法解析,不做语义校验。这意味着:

  • 你可以写一个type: "Convolution"但漏掉convolution_param字段,ReadProtoFromTextFileOrDie会成功返回;
  • 你可以在input层之后直接接SoftmaxWithLoss,prototxt解析无误,但后续SetUp()会因输入 blob 维度不匹配而失败;
  • 所有include字段(如phase: TRAIN)在此阶段只是被原样载入内存,尚未参与任何逻辑判断。

提示:调试时若怀疑 prototxt 有误,不要只看caffe train命令的报错。直接用caffe.proto的 Python binding 加载并打印param.layer[0],能绕过 C++ 层的隐式转换,看到最原始的字段值。我曾因此发现一个num_output被写成字符串"64"而非整数64的低级错误——prototxt 解析器把它当成了默认值 0。

2.2 第二步:按拓扑顺序创建 Layer 实例,但暂不 SetUp

Net::Init()接下来遍历param.layer(),为每个LayerParameter创建对应的Layer子类实例(如ConvolutionLayer、ReLU)。这里的关键机制是LayerRegistry:一个全局静态 map,通过REGISTER_LAYER_CLASS(Convolution)宏注册。GetLayer函数根据layer_param.type()字符串查找构造函数。

但注意:此时所有 Layer 实例都处于“未初始化”状态。它们的SetUp()方法尚未被调用,内部的blobs_(权重/偏置)、layer_param_(配置参数)虽已存在,但blobs_是空指针,layer_param_也未做有效性检查。这个设计是为了支持ShareMode:多个Net实例可以共享同一组Layer的参数内存,但各自的blobs_(如梯度缓存)必须独立。

踩坑实录:某次为加速推理,我尝试将两个Net实例的layers_[i]指针直接赋值为同一个对象。结果Forward()时第二个Net的blobs_因第一个Net的Reshape()而被意外修改,输出全乱。根源在于Layer的blobs_是vector<shared_ptr<Blob> >,其生命周期绑定在Layer实例上,而非Net上。共享Layer实例 ≠ 共享blobs_。

2.3 第三步:为每个 Layer 执行 SetUp(),触发 Blob 分配与 Shape 推导

这是初始化中最关键的一步。Net::Init()调用layer->SetUp(bottom_vecs_[i], &top_vecs_[i]);。SetUp()的核心任务有三:

  1. 解析layer_param_并校验:例如ConvolutionLayer::SetUp()会检查convolution_param().kernel_size_size()是否大于 0;
  2. 推导输出 Blob 的 shape:基于输入bottom的 shape 和 layer 参数,计算top的维度。例如PoolingLayer根据kernel_size、stride、pad计算输出高宽;
  3. 分配blobs_内存:为权重、偏置等可学习参数分配SyncedMemory,但此时top和bottom的Blob内存尚未分配(那是Reshape()的事)。

这里埋着一个经典陷阱:SetUp()只推导 shape,不分配top/bottom内存。top_vecs_[i]中的Blob对象已创建,但其data_和diff_指针仍为空。真正的内存分配发生在Reshape()阶段。我曾在一个自定义CropLayer中,误在SetUp()里调用top[0]->Reshape(),导致后续Net::Reshape()重复调用,引发内存泄漏。

2.4 第四步:构建 Blob 依赖图,确定 top/bottom 映射关系

Net必须知道哪个Layer的top是下一个Layer的bottom。Net::Init()通过两层映射实现:

  • blob_names_vector:按prototxt中layer的声明顺序,存储所有top名称(如"conv1"、"pool1")。blob_names_[i]对应第i个Blob的名称。
  • blob_name_to_idx_map:将blob名称映射到blob_names_的索引。例如blob_name_to_idx_["conv1"] = 0。

当解析到layer的bottom字段时(如bottom: "data"),Net查blob_name_to_idx_得到索引j,然后将layers_[i]的bottom指向blobs_[j];同理,top字段则将layers_[i]的top指向新创建的Blob,并将其加入blobs_尾部。

关键洞察:blobs_vector 的长度等于网络中所有top的总数(含重复名)。例如data层输出data,conv1层输入data、输出conv1,则blobs_至少有[data, conv1]两个元素。但blob_names_中data可能出现多次(如多个Input层),blob_name_to_idx_只记录最后一次出现的位置。这就是为什么Net::has_blob("data")返回true,但Net::blob_by_name("data")可能返回错误的Blob——它取的是blob_name_to_idx_["data"]对应的索引,而非第一个data。

2.5 第五步:为所有 Blob 分配内存,执行首次 Reshape

Net::Reshape()被显式调用(通常在Init()结束前)。它遍历所有Layer,调用layer->Reshape(bottom_vecs_[i], &top_vecs_[i]);。Reshape()的职责是:

  • 根据当前bottom的 shape,重新计算top的 shape(可能与SetUp()时不同,如 batch size 变化);
  • 为top和bottom的Blob分配或重分配data_和diff_内存。

这里有个易忽略的细节:Reshape()不保证bottom的内存已分配。它只负责top。bottom的内存由前一个Layer的Reshape()分配。因此,Reshape()的执行顺序必须严格按拓扑序(从输入层到输出层)。Caffe 通过Net::TopologicalSort()确保这一点,该函数基于blob_name_to_idx_构建有向图并进行 Kahn 算法排序。

实操技巧:若需动态改变 batch size,不要在Forward()前手动调用blob->Reshape()。正确做法是调用Net::Reshape(),它会自动触发所有Layer的Reshape(),确保依赖关系完整。我曾为省事直接input_blob->Reshape(1,3,224,224),结果conv1的Reshape()因bottomshape 未更新而计算出错的topshape,后续全崩。

2.6 第六步:建立 param_ 索引,连接可学习参数与 Layer

Net用param_vector 统一管理所有可学习参数(权重、偏置),其长度等于网络中所有param字段的总数。例如一个ConvolutionLayer有num_output: 64和kernel_size: 3,若convolution_param中指定了bias_term: true,则它贡献 2 个param(权重 + 偏置)。

Net::Init()为每个Layer分配param_索引:

  • layer_param.param(i).lr_mult()指定学习率倍率;
  • layer_param.param(i).decay_mult()指定权重衰减倍率;
  • Net将这些param的索引存入layers_[i]->param_propagate_down_,用于反向传播时决定是否计算该param的梯度。

关键约束:param_的生命周期独立于Layer。Net::CopyTrainedLayersFrom()拷贝参数时,只拷贝param_的数据,不重建Layer实例。这允许你在不修改网络结构的前提下,热替换模型权重。

2.7 第七步:设置 ShareMode,启用参数共享

ShareMode是 Caffe 支持多任务学习的核心机制。NetParameter中的share_mode字段(SHARE_MODE_NONE/SHARE_MODE_IN_LAYER/SHARE_MODE_ACROSS_NETS)在此步生效:

  • SHARE_MODE_NONE:默认,每个Net实例独占所有param_;
  • SHARE_MODE_IN_LAYER:同一Layer的多个top共享同一组param_(如SplitLayer的多个输出);
  • SHARE_MODE_ACROSS_NETS:多个Net实例共享param_(需手动调用Net::ShareWeights())。

Net::Init()会检查share_mode并设置param_的引用计数。若设为ACROSS_NETS,param_的SyncedMemory将被多个Net共享,data_和diff_指针指向同一块内存。此时Net::Backward()会同时更新所有共享Net的梯度。

注意事项:SHARE_MODE_ACROSS_NETS要求所有共享Net的param_数量、shape、lr_mult必须完全一致,否则Init()会CHECK_EQ失败。我在一个跨模态对齐项目中,因两个Net的ConvolutionLayerkernel_size不同(一个 3x3,一个 5x5),ShareWeights()直接 abort。

这七步完成后,Net对象才真正成为一个可执行的计算图。它不是一个静态配置容器,而是一个动态内存管理者、依赖调度器和状态协调者。理解每一步的输入、输出和副作用,是你在Forward()崩溃时,能快速定位到SetUp()还是Reshape()阶段问题的根本能力。

3. Blob 生命周期管理:Net 如何避免内存碎片与悬垂指针

在 Caffe 的Net中,Blob是数据流动的载体,也是内存管理的焦点。Net本身不直接分配Blob的data_,而是通过SyncedMemory间接管理。这种设计带来了灵活性,也埋下了内存安全的雷区。我曾在 Jetson Nano 上调试一个实时检测模型,Forward()运行 10 分钟后内存占用飙升至 2GB,valgrind显示大量SyncedMemory未释放——根因就在Net对Blob生命周期的管理逻辑里。

3.1 Blob 的三层所有权模型

Blob的内存归属遵循严格的三层模型:

  1. Net拥有blobs_vector:Net的blobs_是vector<shared_ptr<Blob> >,它持有所有top和bottomBlob的智能指针。Net的析构函数会清空此 vector,触发所有Blob的析构。
  2. Blob拥有SyncedMemory:Blob的data_和diff_是shared_ptr<SyncedMemory>。Blob的析构函数会释放这两个指针,若无其他引用,则SyncedMemory析构。
  3. SyncedMemory拥有原始内存:SyncedMemory内部用void* cpu_ptr_和void* gpu_ptr_管理 CPU/GPU 内存。其析构函数调用caffe_free()或cudaFree()。

这个模型的关键在于:Blob的data_和diff_可以被多个Blob共享。例如SplitLayer的多个topBlob会共享同一个data_(即cpu_ptr_指向同一地址),但各自拥有独立的diff_(因为梯度需要分别计算)。

踩坑实录:某次为节省内存,我让两个Net实例的input_blob指向同一个Blob对象。Forward()时一切正常,但Backward()后发现梯度混乱。原因在于Blob::ShareData()只共享data_,不共享diff_;而Backward()会向diff_写入梯度,两个Net的diff_指向不同内存,但data_共享导致前向输出耦合。正确做法是共享param_,而非Blob。

3.2 Reshape() 的内存重分配策略与陷阱

Blob::Reshape()是内存管理的核心操作。它的行为分三种情况:

当前 shape新 shapeReshape()行为风险点
[](未分配)非空分配新内存,cpu_ptr_/gpu_ptr_初始化为 0无
AB,sizeof(A) == sizeof(B)复用内存,不清零若B包含新维度,旧数据残留导致前向错误
AB,sizeof(A) != sizeof(B)释放旧内存,分配新内存频繁调用导致内存碎片

Caffe 默认采用“复用优先”策略:只要新旧内存大小相同,就复用cpu_ptr_,不调用memset清零。这提升了性能,但也引入了隐蔽 bug。我曾在一个视频流处理中,batch_size从 1 变为 4,Reshape()后data_大小不变(因channels*height*width相同),但count_(总元素数)翻了 4 倍。Blob::mutable_cpu_data()返回的指针指向旧内存,count_却是新值,导致Forward()读取了未初始化的内存区域,输出随机噪声。

解决方案:在关键Reshape()后,手动调用blob->mutable_cpu_data()并memset清零。或在Net初始化时,设置Caffe::set_mode(Caffe::CPU)后,重写SyncedMemory::cpu_ptr(),强制在复用时清零(需修改 Caffe 源码)。

3.3 ShareMode 对 Blob 内存的影响

ShareMode不仅影响param_,也影响Blob的data_共享:

  • SHARE_MODE_IN_LAYER:SplitLayer的多个topBlob共享data_,但diff_独立。Backward()时,SplitLayer将上游梯度sum到一个diff_,再copy到各top的diff_。
  • SHARE_MODE_ACROSS_NETS:若两个Net共享param_,它们的Layer实例的blobs_(权重)也共享data_,但top/bottomBlob仍独立。

这里有个致命陷阱:ShareWeights()只共享param_,不共享blobs_。blobs_是Layer的成员,每个Layer实例有自己的blobs_vector。因此,即使param_共享,Layer的blobs_[0](权重)的data_指针也指向共享内存,但blobs_[1](偏置)可能不共享——取决于param_的索引分配。

实操验证:在Net::ShareWeights()后,打印net1->layers_[i]->blobs_[0]->cpu_data()和net2->layers_[i]->blobs_[0]->cpu_data(),若地址相同,则共享成功;若不同,则需检查param_索引是否对齐。

3.4 内存泄漏的典型场景与检测

Net的内存泄漏多源于Blob生命周期的失控。常见场景:

  1. Blob被外部强引用:将Net::blob_by_name("conv1")->cpu_data()的指针保存在全局变量中。Net析构后,Blob被销毁,但外部指针仍指向已释放内存,segfault。
  2. SyncedMemory的 GPU 内存未释放:在Caffe::GPU模式下,SyncedMemory::gpu_ptr_由cudaMalloc分配。若Net析构时cudaFree失败(如 CUDA 上下文已销毁),内存泄露。
  3. Reshape()频繁触发小内存分配:Reshape()每次分配新内存时,若旧内存未及时回收,会产生碎片。尤其在batch_size动态变化的场景(如自适应推理)。

检测技巧:使用nvidia-smi监控 GPU 内存,若Forward()后内存不回落,说明SyncedMemory::gpu_ptr_未释放;用valgrind --tool=memcheck运行 CPU 版本,关注definitely lost行。我曾用此方法定位到一个Layer的SetUp()中,new Blob()后未存入blobs_,导致Blob析构时SyncedMemory未被Net管理,最终泄露。

3.5 优化内存使用的实战策略

针对嵌入式场景(如 Jetson 系列),内存优化是刚需。以下是经实测有效的策略:

  • 预分配大块内存池:修改SyncedMemory,在Net::Init()时一次性malloc一块大内存,后续Reshape()从中切分。需重写SyncedMemory::AllocateSpace()。
  • 禁用 diff_ 内存:对于纯推理Net,在NetParameter中设置state.phase: TEST,并在Net::Init()前调用Caffe::set_mode(Caffe::CPU),然后Net不会为diff_分配内存。可节省近 50% 内存。
  • Blob 复用:在Forward()循环中,复用input_blob,避免每次Reshape()。例如input_blob->Reshape(1,3,224,224)后,用memcpy填充新图像数据,而非新建Blob。

这些策略不是理论,而是在产线设备上反复压测得出的经验。Net的内存模型不是黑盒,它是可预测、可干预的。理解Blob如何被创建、共享、释放,是你掌控模型资源消耗的第一步。

4. 前向/反向传播的执行引擎:Net 如何调度 Layer 的计算顺序

Net的Forward()和Backward()方法,表面看只是循环调用Layer::Forward()和Layer::Backward(),实则是一套精巧的调度引擎。它不依赖编译时图优化,而是通过运行时依赖分析,动态决定计算顺序。这种设计赋予了 Caffe 极高的灵活性(如支持条件分支PythonLayer),也带来了调试复杂性。我曾在一个多分支检测网络中,Backward()时梯度只更新了主干分支,辅助分支权重纹丝不动——问题就出在Net对bottom依赖的静态分析上。

4.1 ForwardFromTo():剪枝式前向传播的核心

Net::ForwardFromTo(int start, int end)是Forward()的底层实现。它不从头到尾执行所有Layer,而是根据start和end索引,只执行指定范围内的Layer。其逻辑如下:

for (int i = start; i <= end; ++i) { layers_[i]->Forward(bottom_vecs_[i], &top_vecs_[i]); }

关键点在于:bottom_vecs_[i]和top_vecs_[i]是预先构建好的vector<Blob*>,它们的元素已按拓扑序排列。Net::Init()时,TopologicalSort()确保了layers_的顺序满足:若Layer A的top是Layer B的bottom,则A的索引小于B的索引。

这意味着ForwardFromTo(0, 5)会执行layers_[0]到layers_[5],且layers_[5]的bottom必然由layers_[0..4]的top提供。这种剪枝能力被广泛用于:

  • 特征提取:ForwardFromTo(0, n-2),跳过最后的Softmax层,获取中间特征图;
  • 梯度检查:ForwardFromTo(0, k)后,手动修改某top的data_,再ForwardFromTo(k+1, end),观察输出变化;
  • Layer 替换:ForwardFromTo(0, i-1)后,用自定义计算填充layers_[i]->top()[0],再ForwardFromTo(i, end)。

踩坑实录:某次想跳过DropoutLayer,直接ForwardFromTo(0, i-1)然后ForwardFromTo(i+1, end)。结果layers_[i+1]的bottom因Dropout缺失而为空指针,segfault。正确做法是ForwardFromTo(0, i-1)后,将layers_[i-1]->top()[0]的data_复制给layers_[i+1]->bottom()[0],再ForwardFromTo(i+1, end)。

4.2 Backward() 的依赖反转:从 Loss 到 Input 的梯度回传

Net::Backward()的逻辑与Forward()对称但更复杂。它从网络末端开始,逆拓扑序执行Layer::Backward():

for (int i = layers_.size() - 1; i >= 0; --i) { layers_[i]->Backward(top_vecs_[i], propagate_down[i], &bottom_vecs_[i]); }

propagate_down[i]是vector<bool>,指示是否将梯度传递给该Layer的bottom。其值由两部分决定:

  • Loss Layer 的硬编码:SoftmaxWithLossLayer::Backward()总是设置propagate_down[0] = false(不传梯度给label),propagate_down[1] = true(传梯度给prediction);
  • 用户配置:LayerParameter中的propagate_down字段(布尔列表),可手动关闭某bottom的梯度。

Backward()的核心挑战是:bottom的梯度是累加的。例如EltwiseLayer的多个bottom都贡献梯度到同一个top,Backward()会将梯度+=到各bottom的diff_。这就要求diff_在Backward()前必须清零,否则上次的梯度会残留。

实操技巧:在Backward()前,调用Net::ClearParamDiffs()清零所有param_的diff_,并手动memset(blob->mutable_cpu_diff(), 0, blob->count() * sizeof(float))清零关键bottom的diff_。我曾因此解决一个BatchNormLayer梯度累积导致的训练发散问题。

4.3 param_propagate_down_:参数梯度传播的开关矩阵

Net用param_propagate_down_vector 控制每个param是否参与反向传播。其长度等于param_的长度,每个元素是bool。Net::Backward()会将此向量传给Layer::Backward(),后者据此决定是否更新blobs_[j]->diff_。

param_propagate_down_的初始化逻辑在Net::Init()的第六步:

  • 若layer_param.param(i).lr_mult() == 0,则param_propagate_down_[idx] = false;
  • 否则为true。

这意味着,lr_mult: 0不仅让 SGD 忽略该参数,还阻止了Backward()计算其梯度,节省了计算。但要注意:lr_mult: 0是静态配置,无法在运行时动态修改。若需冻结某层,必须在prototxt中设置,或在Net::Init()后手动修改param_propagate_down_。

验证方法:在Backward()后,打印net->params()[i]->diff()->cpu_data()[0],若为非零值,则param_propagate_down_[i]为true;若恒为 0,则为false。

4.4 多 Loss 支持与梯度聚合

Caffe 支持多个LossLayer(如SoftmaxWithLoss+EuclideanLoss),Net::Backward()会为每个LossLayer执行一次完整的反向传播。梯度在bottom的diff_上累加。例如:

  • Loss1的梯度写入conv5->diff_;
  • Loss2的梯度也写入conv5->diff_;
  • 最终conv5->diff_是两者之和。

这种设计简化了多任务学习,但也带来风险:若两个Loss量纲差异巨大(如SoftmaxWithLoss输出 ~1,EuclideanLoss输出 ~1000),梯度会严重失衡。解决方案是:

  • 在prototxt中为LossLayer设置loss_weight字段,缩放其梯度;
  • 使用ScaleLayer在Loss前调整输出幅度。

经验分享:在一个人脸属性识别项目中,age回归Loss的loss_weight设为 0.1,gender分类Loss设为 1.0,训练稳定。若不加权,age梯度被淹没,MAE无法下降。

4.5 自定义 Layer 的调度集成要点

要让自定义Layer被Net正确调度,必须遵守三个契约:

  1. LayerSetUp()中,正确设置blobs_的数量和 shape:例如ConvolutionLayer在SetUp()中blobs_.resize(2)(权重 + 偏置),并blobs_[0]->Reshape(...)。
  2. Reshape()中,只推导topshape,不分配data_:data_分配由Net::Reshape()统一管理。
  3. Forward()/Backward()中,严格遵循bottom/top的Blob指针约定:bottom[0]是第一个输入Blob,top[0]是第一个输出Blob,diff_的读写必须与propagate_down一致。

违反任一契约,Net的调度就会失效。我曾写一个ResizeLayer,在Forward()中直接new Blob()并top[0] = new_blob,结果Backward()时top[0]->diff_为空,梯度中断。正确做法是top[0]->Reshape()后,用top[0]->mutable_cpu_data()获取内存。

Net的调度引擎不是魔法,它是一套基于指针和索引的确定性规则。掌握这些规则,你就能像调试普通 C++ 代码一样,精准定位传播链中的断裂点。

5. 参数管理与序列化:Net 如何保证模型可移植与可复现

在产线环境中,“模型可移植”意味着:同一份caffemodel文件,在不同机器、不同 Caffe 版本、不同硬件上,Forward()输出必须一致。Net的参数管理与序列化机制,是这一承诺的技术基石。然而,这份一致性极其脆弱——一个prototxt中的字段顺序变化、一个SyncedMemory的cpu_ptr_对齐方式差异,都可能导致caffemodel在新环境加载失败。我曾为一个安防摄像头固件升级,因Caffe 1.0与Caffe 0.17的BlobProto序列化格式微小差异,导致模型加载后data_全为 0,排查耗时三天。

5.1 param_ 的双索引映射:Net 与 Layer 的参数视图

Net的param_是一个扁平化的vector<shared_ptr<Blob> >,而每个Layer的blobs_是局部的vector<shared_ptr<Blob> >。

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

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

立即咨询