【免费下载链接】caffe-face
This branch is developed for deep face recognition
如果你正在研究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_filler) |
| center_loss_layer.hpp | 声明CenterLossLayer类骨架 |
| center_loss_layer.cpp | 初始化 + CPU 版前向/反向传播 |
| center_loss_layer.cu | GPU 版前向/反向传播(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.cpp:
const 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。
接下来是最关键的一段——创建类别中心这个"参数":
vector<int> center_shape(2); center_shape[0] = N_; // 行数 = 类别数 center_shape[1] = K_; // 列数 = 特征维度 this->blobs_[0].reset(new Blob<Dtype>(center_shape)); shared_ptr<Filler<Dtype> > center_filler(GetFiller<Dtype>( 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 initialization");param_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 LossLayer<Dtype>::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_cast<int>(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_cast<int>(label[m]); if (label_value == n) { count++; // 累加 Σ_{y_i==j} (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_cast<int>(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_gpu:
int nthreads = M_ * K_; Compute_distance_data_gpu<Dtype><<<CAFFE_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_cast<int>(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.prototxt:
layer { 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 | 中心矩阵初始化方式 | 常用xavier |
loss_weight | 该损失在总损失中的权重 | 人脸示例 0.008,MNIST 示例 0.01 |
decay_mult | 权重衰减倍数(对中心矩阵加 L2 正则) | 人脸示例 2,MNIST 示例 0 |
axis | 特征展平起始轴,默认 1 | 一般不用改 |
两个示例对比能看出经验规律:身份越多、特征维度越高,loss_weight 应该越小(人脸 1 万多身份用 0.008);decay_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_CLASS+REGISTER_LAYER_CLASS)。如果你想继续深入,推荐接着看 layers 教程 与 前向/反向传播教程,再对照仓库中 filler.hpp 了解 xavier 等初始化策略,就能具备独立实现一个自定义损失层的能力了。
【免费下载链接】caffe-face
This branch is developed for deep face recognition
相关推荐
Caffe PReLU 层深度解析:从配置参数到源码实现与反向传播
Caffe PReLU 层深度解析:从配置参数到源码实现与反向传播 PReLU(Parameterized 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),仅供参考