ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MXNet Gluon 自定义损失函数实战:以 Contrastive Loss 与 Siamese 网络为例

MXNet Gluon 自定义损失函数实战:以 Contrastive Loss 与 Siamese 网络为例 MXNet Gluon 自定义损失函数实战以 Contrastive Loss 与 Siamese 网络为例【免费下载链接】mxnetLightweight, Portable, Flexible Distributed/Mobile Deep Learning with Dynamic, Mutation-aware Dataflow Dep Scheduler; for Python, R, Julia, Scala, Go, Javascript and more项目地址: https://gitcode.com/gh_mirrors/mxnet1/mxnetGluon 提供了 L1Loss、L2Loss、SoftmaxCrossEntropyLoss 等常用内置损失但实际任务往往需要定制自己的损失函数。本文以经典的对比损失Contrastive Loss为例完整演示如何在 MXNet Gluon 中继承Loss基类、实现hybrid_forward方法并配合 Siamese孪生网络完成从数据构造、训练到推理的全流程同时剖析 Gluon 损失模块的底层机制与常见踩坑点。读完本文你将掌握自定义可混合hybridizable损失函数的标准范式并能在自己的模型中直接复用。损失函数与 Gluon 的 Loss 基类神经网络训练的核心之一是损失函数loss function——它量化网络预测与真实标签之间的差距网络通过不断更新权重来最小化该数值。不同任务通常搭配不同损失回归常用 L1Loss平均绝对误差与 L2Loss均方误差分类常用 SigmoidBinaryCrossEntropyLoss 与 SoftmaxCrossEntropyLoss嵌入/度量学习常用 HingeLoss、TripletLoss 等。这些内置损失全部定义在 python/mxnet/gluon/loss.py 中它们的共同基类是Loss。从源码可以看到Loss本身是HybridBlock的子类class Loss(HybridBlock): Base class for loss. Parameters ---------- weight : float or None Global scalar weight for loss. batch_axis : int, default 0 The axis that represents mini-batch. def __init__(self, weight, batch_axis, **kwargs): super(Loss, self).__init__(**kwargs) self._weight weight self._batch_axis batch_axis def hybrid_forward(self, F, x, *args, **kwargs): raise NotImplementedError其中weight是作用于整个损失的全局标量权重batch_axis指定 mini-batch 所在轴默认 0。hybrid_forward是留给子类实现的抽象入口。另外源码中还有两个供内置损失复用的辅助函数_apply_weighting(F, loss, weight, sample_weight)将全局权重weight与逐样本权重sample_weight应用到损失张量上_reshape_like(F, x, y)把张量x重塑为与y相同的形状用于对齐预测与标签。理解这两个辅助函数有助于写出风格统一的自定义损失。在开始自定义之前先按惯例导入依赖import matplotlib.pyplot as plt import mxnet as mx from mxnet import autograd, gluon, nd from mxnet.gluon.loss import Loss import random对比损失Contrastive Loss原理对比损失是一种基于距离的损失函数通常用于 Siamese 网络孪生网络。训练时成对图片被送入模型若两张图片相似同类损失趋近于 0若不相似异类损失趋近于 1。其中Y是表示两张训练图片是否相似的二值标签D即上式中的D_W是图片之间的欧氏距离。对比损失由两项之和构成相似点对的损失项(1-Y) * 0.5 * D²当标签为相似Y0时该项生效惩罚同类样本之间的距离不相似点对的损失项Y * 0.5 * max(0, m-D)²当标签为不相似Y1时生效仅在距离小于 marginm时产生惩罚即不相似样本只有落在 margin 之内才贡献损失。marginm的作用是不相似样本对的损失只有在距离处于某个边界之内时才会被计入避免对已经足够远区分度足够高的样本对过度惩罚。在 Gluon 中自定义 ContrastiveLoss要在 Gluon 中实现这样的自定义损失只需定义一个继承自Loss基类的子类并在hybrid_forward中实现对比损失的逻辑class ContrastiveLoss(Loss): def __init__(self, margin6., weightNone, batch_axis0, **kwargs): super(ContrastiveLoss, self).__init__(weight, batch_axis, **kwargs) self.margin margin def hybrid_forward(self, F, image1, image2, label): distances image1 - image2 distances_squared F.sum(F.square(distances), 1, keepdimsTrue) euclidean_distances F.sqrt(distances_squared 0.0001) d F.clip(self.margin - euclidean_distances, 0, self.margin) loss (1 - label) * distances_squared label * F.square(d) loss 0.5*loss return loss loss ContrastiveLoss(margin6.0)逐行拆解这段实现构造函数调用父类Loss.__init__(weight, batch_axis, **kwargs)完成基类初始化并保存超参数margin默认 6.0。计算距离平方distances image1 - image2得到逐元素差F.sum(F.square(distances), 1, keepdimsTrue)沿特征轴求和得到平方距离。注意keepdimsTrue保留了批维度这对后续张量广播与形状匹配至关重要。欧氏距离F.sqrt(distances_squared 0.0001)中加了一个小 epsilon0.0001防止对接近 0 的值开平方导致 NaN。margin 裁剪F.clip(self.margin - euclidean_distances, 0, self.margin)实现max(0, m-D)并进一步裁剪上限。两项合成(1 - label) * distances_squared label * F.square(d)分别对应相似点对与不相似点对的损失项最后乘以 0.5。hybrid_forward中的第一个参数F是计算前端当网络以命令式imperative方式运行时F是mxnet.ndarray当网络被hybridize()混合化后F变成mxnet.symbol。由于Loss基类本质上是HybridBlock因此自定义损失既可以命令式运行也可以符号式运行——对损失函数做混合化可以获得性能提升且对用户代码完全透明。定义 Siamese 网络Siamese 网络由两个共享权重的完全相同的子网络组成两个输入图片分别经过其中一个子网络标签指示这对图片是否相似网络学会区分两幅输入图像。本示例的网络由两个卷积层 最大池化层下采样输入图像输出经过一个 256 隐单元的稠密层再接一个 2 隐单元的稠密层class Siamese(gluon.HybridBlock): def __init__(self, **kwargs): super(Siamese, self).__init__(**kwargs) with self.name_scope(): self.cnn gluon.nn.HybridSequential() with self.cnn.name_scope(): self.cnn.add(gluon.nn.Conv2D(64, 5, activationrelu)) self.cnn.add(gluon.nn.MaxPool2D(2, 2)) self.cnn.add(gluon.nn.Conv2D(64, 5, activationrelu)) self.cnn.add(gluon.nn.MaxPool2D(2, 2)) self.cnn.add(gluon.nn.Dense(256, activationrelu)) self.cnn.add(gluon.nn.Dense(2, activationsoftrelu)) def hybrid_forward(self, F, input0, input1): out0 self.cnn(input0) out1 self.cnn(input1) return out0, out1关键点hybrid_forward中对两路输入调用同一个self.cnn因此两个分支天然共享权重最后一层用softrelu保证输出为非负便于后续计算欧氏距离。构造成对训练数据示例使用 Omniglot 数据集包含 50 个字母表、1623 个手绘字符训练。为此需要构造一个随机包含相似对与不相似对的数据集。这里继承了 Gluon 的ImageFolderDataset覆写__getitem__随机返回相似/不相似图片对class GetImagePairs(mx.gluon.data.vision.ImageFolderDataset): def __init__(self, root): super(GetImagePairs, self).__init__(root, flag0) self.root root def __getitem__(self, index): items_with_index list(enumerate(self.items)) image0_index, image0_tuple random.choice(items_with_index) should_get_same_class random.randint(0, 1) if should_get_same_class: while True: image1_index, image1_tuple random.choice(items_with_index) if image0_tuple[1] image1_tuple[1]: break else: image1_index, image1_tuple random.choice(items_with_index) image0 super().__getitem__(image0_index) image1 super().__getitem__(image1_index) label mx.nd.array([int(image1_tuple[1] ! image0_tuple[1])]) return image0[0], image1[0], label def __len__(self): return super().__len__()逻辑说明以 50% 概率决定返回同类对should_get_same_class1此时循环采样直到两张图的类别索引image1_tuple[1]与image0_tuple[1]相同否则直接随机取另一张作为不相似对。标签label为1表示不相似0表示相似与对比损失公式中的Y一致。返回三元组(image0[0], image1[0], label)即去掉数据集中额外字段后的两张图与标签。训练使用 Omniglot 的Tifinagh提非纳格字母表子集训练完成后在Inuktitut因纽特音节文字字母表上测试。数据预处理与 DataLoader 构造如下def transform(img0, img1, label): normalized_img0 nd.transpose(img0.astype(float32), (2, 0, 1))/255.0 normalized_img1 nd.transpose(img1.astype(float32), (2, 0, 1))/255.0 return normalized_img0, normalized_img1, label training_dir images_background/Tifinagh testing_dir images_background/Inuktitut_(Canadian_Aboriginal_Syllabics) train GetImagePairs(training_dir) test GetImagePairs(testing_dir) train_dataloader gluon.data.DataLoader(train.transform(transform), shuffleTrue, batch_size16) test_dataloader gluon.data.DataLoader(test.transform(transform), shuffleFalse, batch_size1)transform把图像从 HWC 布局转成 CHW 布局并归一化到 [0,1]训练批大小为 16 并打乱测试批大小为 1 不打乱。下面的代码从测试集中取出一对样本并可视化img1, img2, label test[0] print(Same: {}.format(int(label.asscalar()) 0)) fig, (ax0, ax1) plt.subplots(ncols2, figsize(10, 5)) ax0.imshow(img1.asnumpy()[:,:,0], cmapgray) ax0.axis(off) ax1.imshow(img2.asnumpy()[:,:,0], cmapgray) ax1.axis(off) plt.show()训练 Siamese 网络训练前先实例化自定义对比损失并初始化模型。这里使用 Xavier 初始化Adam 优化器学习率 0.001model Siamese() model.initialize(initmx.init.Xavier()) trainer gluon.Trainer(model.collect_params(), adam, {learning_rate: 0.001}) loss ContrastiveLoss(margin6.0)训练循环如下for epoch in range(10): for i, data in enumerate(train_dataloader): image1, image2, label data with autograd.record(): output1, output2 model(image1, image2) loss_contrastive loss(output1, output2, label) loss_contrastive.backward() trainer.step(image1.shape[0]) loss_mean loss_contrastive.mean().asscalar() print(Epoch number {}\n Current loss {}\n.format(epoch, loss_mean))要点autograd.record()记录前向计算图以便随后反向传播自定义损失以与内置损失完全相同的方式参与计算图——传入模型的两个输出output1, output2与标签labelloss_contrastive.backward()自动计算梯度trainer.step(batch_size)用 Adam 更新参数每步打印当前平均损失用于观察收敛。测试以欧氏距离度量相似性推理阶段对 Siamese 网络输出的两个特征向量计算欧氏距离距离大表示不相似距离小表示相似。for i, data in enumerate(test_dataloader): img1, img2, label data output1, output2 model(img1, img2) dist_sq mx.ndarray.sum(mx.ndarray.square(output1 - output2)) dist mx.ndarray.sqrt(dist_sq).asscalar() print(Euclidean Distance:, dist, Test label, label[0].asscalar()) fig, (ax0, ax1) plt.subplots(ncols2, figsize(10, 5)) ax0.imshow(img1.asnumpy()[0, 0, :, :], cmapgray) ax0.axis(off) ax1.imshow(img2.asnumpy()[0, 0, :, :], cmapgray) ax1.axis(off) plt.show()理想情况下同类别样本对的距离应显著小于异类别样本对的距离两者之间的分界线由训练时设定的 margin 塑造。自定义损失函数的常见陷阱自定义损失时容易踩到以下几类坑。如果损失不下降、或前向/反向传播崩溃应依次排查最后一层的激活函数检查网络最后一层是否用了正确的激活函数例如二分类任务需要在输出上应用 sigmoid。如果最后一层已经用了 sigmoid再搭配 Gluon 的SigmoidBinaryCrossEntropy等于对 sigmoid 结果又做了一次 sigmoid损失将无法按预期收敛。若不加任何激活函数Gluon 默认应用线性激活。中间损失值的数值稳定性本例中计算了两图平方距离的平方根F.sqrt(distances_squared)。当两张图片非常相似时distances_squared接近 0对接近 0 的值开平方可能产生NaN。在distances_squared上加一个极小 epsilon如 0.0001即可规避该问题——这也是源码中众多损失实现采用类似数值稳定技巧如SigmoidBinaryCrossEntropyLoss使用 log-sum-exp 稳定公式的原因。中间张量形状大多数情况下形状不对会在数据与标签比较时报错但有些时候训练可以正常运行却不收敛。例如自定义损失中漏掉keepdimsTrue张量形状变化后示例依然能跑但训练不会收敛。遇到类似问题时最有效的做法是在损失函数的每一步计算后检查张量形状。可微性反向传播要求损失函数可微。如果自定义损失中存在不可微的分支反向传播阶段会崩溃。在设计损失时需保证所有参与梯度回传的运算都是可微的或采取合适的平滑近似。总结本文以对比损失为例完整演示了 MXNet Gluon 自定义损失的三个层次继承Loss基类并实现hybrid_forwardpython/mxnet/gluon/loss.py 中所有内置损失均遵循此模式将其嵌入 Siamese 网络的训练循环以及排查激活函数、数值稳定性、张量形状与可微性四类常见问题。得益于Loss继承自HybridBlock自定义损失天然支持hybridize()混合化可在命令式与符号式执行间无缝切换并获得性能收益。若想进一步了解 Gluon 内置损失的完整清单与数学定义可参考 Loss 函数教程需要自定义度量指标metric做离线评估时可参考 callback 与 metric 相关实现。【免费下载链接】mxnetLightweight, Portable, Flexible Distributed/Mobile Deep Learning with Dynamic, Mutation-aware Dataflow Dep Scheduler; for Python, R, Julia, Scala, Go, Javascript and more项目地址: https://gitcode.com/gh_mirrors/mxnet1/mxnet创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表