【免费下载链接】caffe-faceThis branch is developed for deep face recognition项目地址https://gitcode.com/gh_mirrors/ca/caffe-face点击查看免费下载如果你正在研究caffe-face 深度人脸识别项目那么它的核心创新点 CenterLoss中心损失层源码一定会出现在你的书单上。这篇文章带你逐行拆解 CenterLoss 的完整实现从LayerSetUp的参数初始化、Reshape的维度定义到 CPU/GPU 前向与反向传播以及 CUDA kernel 的写法帮助你真正读懂这个让同一人特征聚成一团的损失层。一、CenterLoss 到底是什么为什么人脸模型需要它传统人脸识别只靠 Softmax 分类损失只要求分得对不要求长得近——同类样本的特征可以散落在特征空间各处。CenterLoss 的思路非常简单为每一个类别维护一个可学习的中心点 center让该类的每张人脸特征尽量靠近自己的中心。用数学语言表达前向损失就是Loss (1 / 2M) * Σ || x_i - c_{y(i)} ||²其中x_i是第 i 张人脸的特征向量c_{y(i)}是该人脸所属类别的类别中心M 是 mini-batch 大小。直观理解Softmax 损失管类间分开CenterLoss 管类内拉近两者叠加后得到的特征才既分得开、又聚得拢人脸比对准确率才能大幅提升。下面这张图是 Caffe 教程中网络前向/反向传播的总体示意理解它就能理解我们后面每一段代码在做什么二、源码地图CenterLoss 一共只有 3 个文件caffe-face 在原版 Caffe 基础上新增的代码非常精简与 CenterLoss 直接相关的文件如下文件职责caffe.proto定义CenterLossParameter配置结构num_output、axis、center_fillercenter_loss_layer.hpp声明CenterLossLayer类骨架center_loss_layer.cpp初始化 CPU 版前向/反向传播center_loss_layer.cuGPU 版前向/反向传播2 个 CUDA kernel头文件 center_loss_layer.hpp 里有几行值得注意的声明virtual inline int ExactNumBottomBlobs() const { return 2; }它要求恰好 2 个输入bottom[0]是人脸特征bottom[1]是类别标签label。再看成员变量M_mini-batch 样本数K_特征维度axis 之后的维度展平N_类别总数对应num_output参数distance_临时缓存每个样本与所属中心的距离向量variation_sum_反向传播时累加距离和的临时缓存三个整型维度 M、K、N 是整个层的核心下面的代码全都围绕它们展开。三、LayerSetUp 逐行解析创建并初始化类别中心LayerSetUp只在整个网络生命周期中执行一次加载预训练权重或首次创建时负责初始化可学习参数。我们逐段看 center_loss_layer.cppconst int num_output this-layer_param_.center_loss_param().num_output(); N_ num_output; // 类别数 N const int axis bottom[0]-CanonicalAxisIndex(...axis()); K_ bottom[0]-count(axis); // 特征维度 K前四行确定两个维度N_直接来自 prototxt 中的num_output即网络中的人脸身份总数K_是bottom[0]从 axis 轴开始往后的元素总数。举例如果特征形状是 (N, C, H, W) 且axis 1那么K_ C*H*W。接下来是最关键的一段——创建类别中心这个参数vectorint center_shape(2); center_shape[0] N_; // 行数 类别数 center_shape[1] K_; // 列数 特征维度 this-blobs_[0].reset(new BlobDtype(center_shape)); shared_ptrFillerDtype center_filler(GetFillerDtype( this-layer_param_.center_loss_param().center_filler())); center_filler-Fill(this-blobs_[0].get()); // 按配置初始化中心也就是说CenterLoss 的参数就是一个 N × K 的矩阵第 j 行就是第 j 个类别的中心c_j。初始化策略由 prototxt 中的center_filler决定人脸示例用的是 xavier 初始化见下文配置。最后两行} // parameter initialization this-param_propagate_down_.resize(this-blobs_.size(), true);如果加载到了预训练权重就跳过初始化打印 Skipping parameter initializationparam_propagate_down_置为 true 表示反向传播时要对中心参数计算梯度并更新它。新手提示这里的中心矩阵和普通层的权重矩阵不同它没有权重矩阵 W 和偏置 b 之分就是一个会被训练、也受 weight decay 约束的参数。四、Reshape 逐行解析M_ 从哪里来Reshape在每次输入尺寸变化时调用center_loss_layer.cpp 的逻辑很短CHECK_EQ(bottom[1]-channels(), 1); // label 形状必须是 CHECK_EQ(bottom[1]-height(), 1); // (M, 1, 1, 1) CHECK_EQ(bottom[1]-width(), 1); M_ bottom[0]-num(); // batch 大小 M LossLayerDtype::Reshape(bottom, top); distance_.ReshapeLike(*bottom[0]); // 与特征同形状 variation_sum_.ReshapeLike(*this-blobs_[0]); // 与中心同形状三个 CHECK 在强制 label 的合法形状然后M_取bottom[0]-num()即 batch 大小。最后两个 Reshape 把临时缓存distance_和variation_sum_对齐到正确尺寸供前反向使用。至此 M、K、N 全部就位三个维度在计算中的角色总结M样本数一个 mini-batch 里有几张人脸K特征维度每张人脸特征向量的长度N类别数整个数据集里有多少个人五、Forward_cpu 逐行解析两步算出损失CPU 前向传播 center_loss_layer.cpp 只有三步先看图理解数据流向第一步逐个样本求距离向量for (int i 0; i M_; i) { const int label_value static_castint(label[i]); // D(i,:) X(i,:) - C(y(i),:) caffe_sub(K_, bottom_data i * K_, center label_value * K_, distance_data i * K_); }对第 i 张人脸取出它的标签y(i)用特征X(i,:)减去所属类别的中心C(y(i),:)得到距离向量并存进distance_缓存。caffe_sub是 Caffe 数学函数库里的逐元素相减。第二步平方和 / 2M 得到标量损失Dtype dot caffe_cpu_dot(M_ * K_, distance_.cpu_data(), distance_.cpu_data()); Dtype loss dot / M_ / Dtype(2); top[0]-mutable_cpu_data()[0] loss;caffe_cpu_dot(a, a)计算向量与自身的点积等价于所有距离分量的平方和再除以 M 和 2正好得到前文公式Loss (1/2M) Σ ||x_i - c_{y(i)}||²写入top[0]这个标量输出。六、Backward_cpu 逐行解析两个方向的梯度反向传播要回答两个问题中心参数怎么更新、特征层的梯度怎么回传。看 center_loss_layer.cpp方向一对类别中心求梯度当param_propagate_down_[0]为 true 时caffe_set(N_ * K_, (Dtype)0., variation_sum_.mutable_cpu_data()); for (int n 0; n N_; n) { int count 0; for (int m 0; m M_; m) { const int label_value static_castint(label[m]); if (label_value n) { count; // 累加 Σ_{y_ij} (x_i - c_j) caffe_sub(K_, variation_sum_data n * K_, distance_data m * K_, variation_sum_data n * K_); } } caffe_axpy(K_, (Dtype)1./(count (Dtype)1.), variation_sum_data n * K_, center_diff n * K_); }翻译成人话对类别 n先遍历 batch 找出所有标签等于 n 的样本把它们与中心的距离向量累加起来最后乘上系数1 / (count 1)写入center_diff。对应数学形式∂Loss/∂c_j (1 / (count_j 1)) * Σ_{y_i j} (x_i - c_j)注意分母是count 1而不是count——这个 1 是平滑技巧当 batch 中某个类别一个样本都没有count 0时普通除法会除零崩溃加 1 后梯度自然归零训练依然稳定。方向二对输入特征求梯度if (propagate_down[0]) { caffe_copy(M_ * K_, distance_.cpu_data(), bottom[0]-mutable_cpu_diff()); caffe_scal(M_ * K_, top[0]-cpu_diff()[0] / M_, bottom[0]-mutable_cpu_diff()); } if (propagate_down[1]) { LOG(FATAL) this-type() Layer cannot backpropagate to label inputs.; }先拷贝距离向量作为梯度因为∂Loss/∂x_i (x_i - c_{y(i)})/M再用caffe_scal乘上上游梯度top[0]-cpu_diff()[0] / M_完成链式法则。最后一句很有意思试图向 label 回传梯度会直接LOG(FATAL)崩溃——标签是输入不可导。七、CUDA 实现两个 kernel 完成 GPU 加速GPU 版 center_loss_layer.cu 把 CPU 循环改写成了两个并行 kernel这是读懂 Caffe 自定义层的典型范式。Kernel 1距离计算前向__global__ void Compute_distance_data_gpu(int nthreads, const int K, const Dtype* bottom, const Dtype* label, const Dtype* center, Dtype* distance) { CUDA_KERNEL_LOOP(index, nthreads) { int m index / K; int k index % K; const int label_value static_castint(label[m]); // distance(i) x(i) - c_{y(i)} distance[index] bottom[index] - center[label_value * K k]; } }共启动M_ * K_个线程每个线程负责一个特征元素用index / K还原出是第几张人脸、index % K是第几个维度其余与 CPU 版完全对应。启动配置是标准的CAFFE_GET_BLOCKS / CAFFE_CUDA_NUM_THREADS写法见 Forward_gpuint nthreads M_ * K_; Compute_distance_data_gpuDtypeCAFFE_GET_BLOCKS(nthreads), CAFFE_CUDA_NUM_THREADS(nthreads, K_, ...);Kernel 2中心梯度反向__global__ void Compute_center_diff_gpu(int nthreads, const int M, const int K, const Dtype* label, const Dtype* distance, Dtype* variation_sum, Dtype* center_diff) { CUDA_KERNEL_LOOP(index, nthreads) { int count 0; for (int m 0; m M; m) { if (static_castint(label[m]) index) { // 线程 index 负责类别 index count; for (int k 0; k K; k) variation_sum[index * K k] - distance[m * K k]; } } for (int k 0; k K; k) center_diff[index * K k] variation_sum[index * K k] / (count 1.); } }这里的设计很巧妙线程总数是N_类别数每个线程独占一个类别串行扫描整个 batch 累加该类样本的距离因此线程之间互不冲突天然无竞争。源码细读彩蛋在 Backward_gpu 中有一行caffe_gpu_set(N_ * K_, (Dtype)0., variation_sum_.mutable_cpu_data());它调用了 GPU 版本的置零函数但传入的却是 CPU 指针——这是原作者留下的一个笔误正确应为mutable_gpu_data()。由于 kernel 内部从减起累计实际训练效果通常不受影响但如果你在 GPU 上调试数值问题这里值得留意。八、实战在 prototxt 中配置 CenterLoss 层源码看懂了配置更简单。caffe-face 自带两个真实示例人脸模型的完整配置在 face_train_test.prototxtlayer { name: center_loss type: CenterLoss bottom: fc5 bottom: label top: center_loss param { lr_mult: 1 decay_mult: 2 } center_loss_param { num_output: 10572 # 人脸身份总数 center_filler { type: xavier } } loss_weight: 0.008 }逐项解读关键参数参数含义建议bottom特征层 标签层顺序不能反特征在前、label 在后num_output类别身份总数决定中心矩阵行数必须等于数据集人数center_filler中心矩阵初始化方式常用xavierloss_weight该损失在总损失中的权重人脸示例 0.008MNIST 示例 0.01decay_mult权重衰减倍数对中心矩阵加 L2 正则人脸示例 2MNIST 示例 0axis特征展平起始轴默认 1一般不用改两个示例对比能看出经验规律身份越多、特征维度越高loss_weight 应该越小人脸 1 万多身份用 0.008decay_mult则控制正则强度让中心点不会漂移到离原点很远的地方。配置好后即可按 README 的训练命令启动./build/tools/caffe train -solver face_example/face_solver.prototxt -gpu X,Y九、总结一张表回顾 CenterLoss 的一生阶段函数核心动作初始化LayerSetUp创建 N×K 中心矩阵并按 filler 初始化整形Reshape确定 M/K/N对齐临时缓存前向Forward_cpu/gpu逐样本减中心 → 平方和 / 2M 得 loss反向Backward_cpu/gpu按类累加距离得中心梯度带 1 平滑距离 ×上游梯度/M 回传特征整个实现不到 300 行却完整覆盖了 Caffe 自定义 Loss 层的全部套路参数声明、Blob 管理、BLAS 工具函数caffe_sub/caffe_axpy/caffe_scal、CUDA kernel 与双端注册INSTANTIATE_CLASSREGISTER_LAYER_CLASS。如果你想继续深入推荐接着看 layers 教程 与 前向/反向传播教程再对照仓库中 filler.hpp 了解 xavier 等初始化策略就能具备独立实现一个自定义损失层的能力了。赞分享【免费下载链接】caffe-faceThis branch is developed for deep face recognition项目地址https://gitcode.com/gh_mirrors/ca/caffe-face点击查看免费下载相关推荐Caffe PReLU 层深度解析从配置参数到源码实现与反向传播Caffe PReLU 层深度解析从配置参数到源码实现与反向传播 PReLUParameterized Rectified Linear Unit参数化修深度学习计算机视觉jrebel-license-active-server 生产环境部署最佳实践JRebel激活服务器 HTTPS 与高可用完整指南jrebel license active server 生产环境部署最佳实践JRebel激活服务器 HTTPS 与高可用完整指南 jrebel licens后端认证鉴权Caffe Clip Layer 完全指南参数配置、前向/反向传播源码解析与梯度测试Caffe Clip Layer 完全指南参数配置、前向/反向传播源码解析与梯度测试 Clip Layer 是 Caffe 深度学习框架中一个基础而实用的神经深度学习计算机视觉创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
阅读完成 · 觉得有帮助?