现代C++中的从头开始深度学习:激活函数

news2024/11/17 11:41:16

一、说明

        让我们通过在C++中实现激活函数来获得乐趣。人工神经网络是生物启发模型的一个例子。在人工神经网络中,称为神经元的处理单元被分组在计算层中,通常用于执行模式识别任务。

        在这个模型中,我们通常更喜欢控制每一层的输出以服从一些约束。例如,我们可以将神经元的输出限制为 [0, 1]、[0, ∞] 或 [-1,+1] 的区间。另一个非常常见的场景是保证来自同一层的神经元总是相加 1。应用这些约束的方法是使用激活函数

在这个故事中,我们将介绍 5 个重要的激活函数:sigmoid、tanh、ReLU、identity 和 Softmax。

二、关于本系列

在本系列中,我们将学习如何仅使用普通和现代C++对必须知道的深度学习算法进行编码,例如卷积、反向传播、激活函数、优化器、深度神经网络等。

这个故事是:C++中的激活函数

查看其他故事:

0 — 现代C++深度学习编程基础

1 — 在C++中编码 2D 卷积

2 — 使用 Lambda 的成本函数

3 — 实现梯度下降

...更多内容即将推出。

三、sigmoid激活

从历史上看,最著名的激活是Sigmoid函数:

Sigmoid 函数和一阶导数

此图表显示了 sigmoid 的三个重要属性:

  • 其输出限制在 0 和 1 之间;
  • 它是平滑的,或者用更好的数学术语来说,它是可微分的;
  • 它是S形的。

你应该想知道为什么形状很重要?S 形模型意味着曲线类似于原点邻域中的线性曲线:

这有助于更快地收敛小输入。有两种方法可以定义 sigmoid 公式:

这两个公式是等效的,但在实现时,我们更愿意使用后者:

double sigmoid(double x)
{
    return 1. / (1. + exp(-x));
}

我们更喜欢第二个公式的原因是第一个公式在数值上更不稳定。很多时候,我们在实现 sigmoid 时使用短路:

double sigmoid(double x)
{
    double result;
    if (x >= 45.) result = 1.;
    else if (x <= -45.) result = 0.;
    else result = 1. / (1. + exp(-x));
    return result;
}

这节省了大量处理并避免了以下情况 |x|很大。

四、sigmoid导数

        使用链式法则,我们可以找到 sigmoid 导数为:

        为方便起见,我们将 sigmoid 及其一阶导数分组为一个函子:

class Sigmoid : public ActivationFunction
{
    public:

        virtual Matrix operator()(const Matrix &z) const
        {
            return z.unaryExpr(std::ref(Sigmoid::helper));
        }

        virtual Matrix jacobian(const Vector &z) const
        {
            Vector output = (*this)(z);

            Vector diagonal = output.unaryExpr([](double y) {
                return (1. - y) * y;
            });

            DiagonalMatrix result = diagonal.asDiagonal();

            return result;
        }

    private:

        static double helper(double z)
        {
            double result;
            if (z >= 45.) result = 1.;
            else if (z <= -45.) result = 0.;
            else result = 1. / (1. + exp(-z));
            return result;
        }

};

我们将看到在介绍反向传播算法时如何使用激活函数导数。

Sigmoid 主要用于二元分类器或回归系统的输出层,其中结果始终为非负。如果输出可以是负值,请考虑使用下面描述的 Tanh 激活。


五、Tanh 激活

        顾名思义,tanh 激活由双曲正切三角函数定义:

        与 sigmoid 一样,tanh 也是 S 形且可微的。然而,tanh 的界限是 -1 和 1:

Tanh 函数和一阶导数

tanh 激活和 sigmoid 激活紧密相关:

请注意,由于 tanh 可以输出负值,因此我们不能将其与 logcosh 等损失函数一起使用。

tanh 的一阶导数为:

我们可以将 tanh 及其导数打包到一个函子中:

class Tanh : public ActivationFunction
{
    public:

        virtual Matrix operator()(const Matrix &z) const
        {
            return z.unaryExpr(std::ref(tanh));
        }

        virtual Matrix jacobian(const Vector &z) const
        {
            Vector output = (*this)(z);

            Vector diagonal = output.unaryExpr([](double y) {
                return (1. - y * y);
            });

            DiagonalMatrix result = diagonal.asDiagonal();

            return result;
        }
};

六、RELU

        Sigmoid 和 Tanh 的一个问题是它们的计算成本非常高,使得训练时间更长。ReLU是一个简单的激活:

ReLU活化和一阶导数

由于ReLU是一个简单的比较,因此与其他函数相比,其计算成本非常低。

我们可以按如下方式实现 ReLU:

class ReLU : public ActivationFunction
{

    public:

        virtual Matrix operator()(const Matrix &z) const
        {
            return z.unaryExpr([](double v) {
                return std::max(0., v);
            });
        }

        virtual Matrix jacobian(const Vector &z) const
        {

            Vector output = (*this)(z);
            Vector diagonal = output.unaryExpr([](double y) {
                double result = 0.;
                if (y > 0) result = 1.; 
                return result;
            });

            DiagonalMatrix result = diagonal.asDiagonal();

            return result;
        }

};

        相关要点是:

  • 它对负值有界,但对正 x 值未绑定:[0, ∞]
  • 当 x = 0 时,它是不可微分的。在实践中,我们通过假设当 x = 0 时导数 dRelu(x)/dx 为 0 来放宽此条件。

        由于 ReLU 基本上由单个比较组成,因此我们谈论的是一个非常快速的计算操作。它的第一阶导数也可以快速计算:

        尽管有其优点,但ReLu有三个主要缺点:

  • 由于它不是正有界的,我们不能使用它来控制输出到 [0, 1]。正因为如此,在实践中,ReLU通常只存在于内部(隐藏)层中。
  • 由于 ReLu 对于任何 x < 0 都是 0,有时我们的模型在训练过程中只是“死亡”,因为部分或所有神经元都停留在仅输出 0 的状态。
  • 由于 ReLU 的导数在 x = 0 时不连续,因此对于某些输入,模型的训练可能不稳定。

有一些替代方法可以解决这些问题(参见Softplus,leakyReLU,ELU和GeLU)。然而,由于相当大的好处,ReLU仍然广泛用于现实世界的模型。

七、身份激活

        身份激活的定义很简单:

        其导数为:

        使用身份激活意味着神经元的输出不会以任何方式被修改。在这种情况下,实现非常简单:

class Identity : public ActivationFunction
{
    public:
        virtual Matrix operator()(const Matrix &z) const { return z; }

        virtual Matrix jacobian(const Vector &z) const
        {

            Vector diagonal = Vector::Ones(z.rows());

            DiagonalMatrix result = diagonal.asDiagonal();

            return result;
        }
};

恒等式和一阶导数

八、softmax

        考虑到我们有一张宠物的照片,我们需要确定它是哪种动物:狗?一只猫?仓鼠?一只鸟?豚鼠?在机器学习中,我们通常将此类问题建模为分类问题,并将模型称为分类器

        Softmax非常适合作为分类器的输出,因为它实际上表示离散概率分布。例如,请考虑以下示例:

猫、狗和鸟的分类器

在前面的示例中,网络非常确定图像中的宠物是猫。在下一个示例中,模型将图像评分为狗:

在深度学习模型中,我们使用 Softmax 来表示这种类型的输出。

这张惊人的宠物照片是由Amber Janssens拍摄的

8.1 定义SoftMax

        Softmax的原始公式是:

        这个公式意味着,如果我们有 k 个神经元,第 i 个神经元的输出由 x i 的指数除以每个神经元 xj 的指数之和给出。

Softmax的第一个实现可以是:

const auto buggy_softmax(const Vector &z) {

    Vector expo = z.array().exp();
    Vector sums = expo.colwise().sum();
    Vector result = expo.array().rowwise() / sums.transpose().array();
    return result;

};

我们很快就会看到这种实现存在严重缺陷。但是这段代码的工作是说明softmax最重要的方面:每个神经元的结果取决于每个单独的输入。

我们可以运行以下代码:

Vector input1 = Vector::Zero(3);
input1 << 0.1, 1., -2.;

std::cout << "Input 1:\n" << input1.transpose() << "\n\n";
std::cout << "results in:\n" << buggy_softmax(input1).transpose() << "\n\n";

到输出:

Softmax最重要的两个方面是:

  • 所有神经元的总和始终为 1
  • 每个神经元值在区间 [0, 1] 内

8.2 实现SoftMax

        我们之前的 softmax 实现的问题在于指数函数增长非常快。例如,e¹⁰ 大约是 22,026,但 e¹⁰⁰ 是 2.688117142×10 ⁴³,这是一个令人生畏的巨大数字。事实证明,即使我们使用适度的数字作为输入,我们的实现也会失败:

Vector input2 = Vector::Zero(4);
input2 << 100, 1000., -500., 200.;

std::cout << "Input 2:\n" << input2.transpose() << "\n\n";
std::cout << "results in:\n" << buggy_softmax(input2).transpose() << "\n\n";
std::cout << "using the buggy implementation.\n";

        发生这种情况是因为C++浮点具有固定的表示形式。使用常规 64 位处理器,任何通过 750 或更多字符的调用都会导致数字。cmath exp(x)inf

        幸运的是,我们可以使用以下技巧来修复它:

        其中 m 是最大输入:

        现在,通过修复代码,我们得到:

const auto good_softmax(const Vector &z) {

    Vector maxs = z.colwise().maxCoeff();
    Vector reduc = z.rowwise() - maxs.transpose();
    Vector expo = reduc.array().exp();
    Vector sums = expo.colwise().sum();
    Vector result = expo.array().rowwise() / sums.transpose().array();
    return result;

};

溢出是数值不稳定的一个来源。

当我们开发现实世界的深度学习系统时,数值稳定性是一个非常普遍的问题。

8.3 Softmax衍生产品

        Softmax和其他激活之间存在非常明显的差异。通常,像sigmoid或ReLU这样的激活是系数操作,即一个系数的值不会影响其他系数。当然,在 Softmax 中,这不是真的,因为所有值都需要求和 1。这种依赖性使得softmax导数的计算有点棘手。尽管如此,经过一点点的计算并使用我们的老朋友链规则,我们可以弄清楚:

如果您想阅读此衍生品的发展,请告诉我。

例如,如果我们有 5 个神经元,则每个神经元相对于同一层中每个神经元的导数由下式给出:

这个导数将在下一个故事中应用,当我们训练第一个分类器时。

九、包装SoftMax以供进一步使用

最后,我们可以按如下方式实现 Softmax 函子:

class Softmax : public ActivationFunction
{
    public:

        virtual Matrix operator()(const Matrix &z) const
        {

            if (z.rows() == 1)
            {
                throw std::invalid_argument("Softmax is not suitable for single value outputs. Use sigmoid/tanh instead.");
            }
            Vector maxs = z.colwise().maxCoeff();
            Matrix reduc = z.rowwise() - maxs.transpose();
            Matrix expo = reduc.array().exp();
            Vector sums = expo.colwise().sum();
            Matrix result = expo.array().rowwise() / sums.transpose().array();
            return result;
        }

        virtual Matrix jacobian(const Vector &z) const
        {
            Matrix output = (*this)(z);

            Matrix outputAsDiagonal = output.asDiagonal();

            Matrix result = outputAsDiagonal - (output * output.transpose());

            return result;
        }

};

如今,几乎每个分类器都在输出层中使用 Softmax。我们将在接下来的故事中介绍softmax的一些真实示例。

十、其他激活函数

        还有其他几个激活函数。除了这里描述的那些,我们还可以列出Softplus,Softsign,SeLU,Elu,GeLU,指数,swish等。一般来说,它们是sigmoid或ReLU的一些变体。

十一、结论和下一步

        激活函数是机器学习模型最重要的构建块之一。在这个故事中,我们学习了一些最重要的:Sigmoid,Tanh,ReLU,Identity和Softmax。

        在下一个故事中,我们将深入探讨最重要的深度学习算法的实现:反向传播。从零开始,在C++和本征。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/804293.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

详解python中的垃圾回收机制

目录 什么是垃圾回收机制 垃圾回收的工作流程 为什么要进行垃圾回收 详解python中的垃圾回收机制 总结 什么是垃圾回收机制 垃圾回收&#xff08;Garbage Collection&#xff09;是一种自动内存管理机制&#xff0c;用于检测和释放不再被程序使用的内存资源&#xff0c;以…

【数据结构】实验十一:图

实验十一 图 一、实验目的与要求 1&#xff09;掌握图的存储表示与操作实现。 2&#xff09;掌握图的连通性及其应用。 二、 实验内容 1.用邻接表存储一个图形结构&#xff0c;并计算每个顶点的度。 2. 采用深度和广度优先搜索算法&#xff0c;遍历上述这张图&#xff0c;…

CSS之允许点击穿透

一、pointer-events 属性用于设置元素是否对鼠标事件做出反应。 二、属性值 三、如果设置点击穿透效果&#xff0c;使用 pointer-events:none; 设置作用元素即可 .your-classname {pointer-events:none; }

23款奔驰S400豪华型升级后排电动腿托系统,提升后排乘坐舒适性

奔驰S400L后排座椅是不带腿托和脚托的&#xff0c;也没有一键躺平功能&#xff0c;相对于奔驰S级高配车型上配置的右边老板位座椅&#xff0c;舒适性就差强了一些。

AX88179A千兆网卡芯片,支持switch联网

AX88179是世界上第一个USB 3.0&#xff0c;千兆以太网控制器&#xff0c;它在单一芯片上集成了USB 3.0 PHY和10/100/1000Mbps千兆以太网MAC / PHY。AX88179是最新此外ASIX的USB-到-LAN产品组合&#xff0c;提供一个小的形式因素的解决方案和插头-和-打法可用性&#xff0c;使嵌…

重学C++系列之模板

一、什么模板 模板的引入跟泛型编程有关&#xff0c;泛型编程指编写和编译时&#xff0c;对于参数的类型是一个不确定的类型&#xff0c;直到程序运行时&#xff0c;才能确定真正的类型。而泛型编程的实现主要通过函数模板和类模板。 二、模板有几种 模板有两种&#xff0c;函…

无涯教程-jQuery - hide( )方法函数

如果显示了 hide()方法&#xff0c;它们只是隐藏每个匹配元素集。此方法还有另一种形式&#xff0c;可以控制动画的速度。 hide( ) - 语法 selector.hide( ); hide( ) - 示例 以下是一个简单的示例&#xff0c;简单说明了此方法的用法- <html><head><title…

软件测试人员一定要会的用例设计思路

职场新人对测试用例的困惑无非有以下几点 1、什么是测试用例&#xff0c;为什么要写测试用例&#xff1f; 2、不知道怎么写&#xff0c;写了也不知道写的是否完整。 一、什么是测试用例&#xff1f; 百科的释义&#xff1a; 测试用例是对一项特定的软件产品进行测试任务的…

day45-Netflix Mobile Navigation(左边侧边栏动态导航)

50 天学习 50 个项目 - HTMLCSS and JavaScript day45-Netflix Mobile Navigation&#xff08;左边侧边栏动态导航&#xff09; 效果 index.html <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta name&…

pinia在vue3中的使用

总结&#xff1a; 在store文件夹中建一个pinia的文件userStore.js 1.要想使用pinia必须先引入defineStore 这里我们使用es6的模块化语法导出的 import { defineStore } from pinia 2.然后使用export const useUserStore defineStore(user,{}) defineStore 方法有两个参数&…

NAT协议(网络地址转换协议)详解

NAT协议&#xff08;网络地址转换协议&#xff09;详解 为什么需要NATNAT的实现方式静态NAT动态NATNAPT NAT技术的优缺点优点缺点 NAT协议是将IP数据报头中的IP地址转换为另外一个IP地址的过程&#xff0c;主要用于实现私有网络访问公有网络的功能。这种通过使用少量的IP地址代…

机器视觉系统组成,你知道多少?

机器视觉系统是一个复杂而高效的技术体系&#xff0c;它的组成主要包括以下几个核心部件&#xff1a; 相机和镜头&#xff1a;相机是机器视觉系统的眼睛&#xff0c;用于捕捉被测物的图像。镜头是相机的重要组成部分&#xff0c;它可以调节焦距、光圈和通光量&#xff0c;帮助获…

第3章 配置与服务

1 CoreCms.Net.Configuration.AppSettingsHelper using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration.Json; namespace CoreCms.Net.Configuration { /// <summary> /// 【应用设置助手--类】 /// <remarks> /// 摘要&#x…

LLVM(2)IR入门

1 不支持类型的隐式转换 int factorial(int val);int factorial(int val) {if (val < 2)return 1;return factorial(val - 1) factorial(val - 2); }int main(int argc, char **argv) {return factorial(2) * 7 42; }生成IR代码 clang -emit-llvm -S t3.cpp -o t3.ll ;…

Android平台GB28181设备接入侧如何同时对外输出RTSP流?

技术背景 GB28181的应用场景非常广泛&#xff0c;如公共安全、交通管理、企业安全、教育、医疗等众多领域&#xff0c;细分场景可用于如执法记录仪、智能安全帽、智能监控、智慧零售、智慧教育、远程办公、明厨亮灶、智慧交通、智慧工地、雪亮工程、平安乡村、生产运输、车载终…

云原生架构

1. 何为云原生&#xff1f; 很多IT业内小伙伴会经常听到这个名词&#xff0c;那么什么是云原生呢&#xff1f;云原生是在云计算环境中构建、部署和管理现代应用程序的软件方法。 当今时代&#xff0c;众多企业希望构建高度可扩展、灵活且有弹性的应用程序&#xff0c;以便能够快…

Linux CentOS 8 编译安装Apache Subversion

前言 距离上一篇发表已经过去了5年零2个多月&#xff0c;这次重新开始写技术博客&#xff0c;理由和原来一样&#xff0c;也就是想把自己学习和工作中遇到的问题和知识记录下来&#xff0c;今天记录一下Linux CentOS 8通过编译安装svn的过程。 下载SVN 下载地址&#xff1a;…

使用frp中的xtcp映射穿透指定服务实现不依赖公网ip网速的内网穿透p2p

使用frp中的xtcp映射穿透指定服务实现不依赖公网ip网速的内网穿透p2p 管理员Ubuntu配置公网服务端frps配置service自启(可选) 配置内网服务端frpc配置service自启(可选) 使用者配置service自启(可选) 效果 通过frp实现内网client访问另外一个内网服务器 管理员 1&#xff09;…

PHP8的注释-PHP8知识详解

欢迎你来到PHP服务网&#xff0c;学习《PHP8知识详解》系列教程&#xff0c;本文学习的是《PHP8的注释》。 什么是注释&#xff1f; 注释是在程序代码中添加的文本&#xff0c;用于解释和说明代码的功能、逻辑或其他相关信息。注释通常不会被编译器或解释器处理&#xff0c;而…

深度学习实战44-Keras框架下实现高中数学题目的智能分类功能应用

大家好,我是微学AI ,今天给大家介绍一下深度学习实战44-Keras框架实现高中数学题目的智能分类功能应用,该功能是基于人工智能技术的创新应用,通过对数学题目进行智能分类,提供个性化的学习辅助和教学支持。该功能的实现可以通过以下步骤:首先,采集大量的高中数学题目数据…