现代C++中的从头开始深度学习:【5/8】卷积

一、说明

         在上一个故事中,我们介绍了机器学习的一些最相关的编码方面,例如 functional 规划、矢量化线性代数规划

        现在,让我们通过使用 2D 卷积实现实际编码深度学习模型来开始我们的道路。让我们开始吧。

二、关于本系列

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

这个故事是:在C++中编码 2D 卷积

查看其他故事:

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

2 — 使用 Lambda 的成本函数

3 — 实现梯度下降

4 — 激活函数

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

三、卷 积

        卷积是信号处理领域的老朋友。最初,它的定义如下:

        在机器学习术语中:

  • 我(...通常称为输入
  • K(...作为内核,以及
  • F(...)作为给定 K 的 I(x) 的特征映射

考虑一个多维离散域,我们可以将积分转换为以下求和:

最后,对于2D数字图像,我们可以将其重写为:

理解卷积的一种更简单的方法是下图:

有效卷积 — 作者图片

        我们可以很容易地看到内核在输入矩阵上滑动,生成另一个矩阵作为输出。这是卷积的简单情况,称为有效卷积。在这种情况下,矩阵的维度由下式给出:Output

dim(Output) = (m-k+1, n-k+1)

        这里:

  • m分别是输入矩阵中的行数和列数,以及n
  • k是平方核的大小。

        现在,让我们对第一个 2D 卷积进行编码。

四、使用循环对 2D 卷积进行编码

        实现卷积的最直观方法是使用循环:

auto Convolution2D = [](const Matrix &input, const Matrix &kernel)
{
    const int kernel_rows = kernel.rows();
    const int kernel_cols = kernel.cols();
    const int rows = (input.rows() - kernel_rows) + 1;
    const int cols = (input.cols() - kernel_cols) + 1;

    Matrix result = Matrix::Zero(rows, cols);

    for (int i = 0; i < rows; ++i) 
    {
        for (int j = 0; j < cols; ++j) 
        {
            double sum = input.block(i, j, kernel_rows, kernel_cols).cwiseProduct(kernel).sum();
            result(i, j) = sum;
        }
    }

    return result;
};

        这里没有秘密。我们将内核滑过列和行,为每个步骤应用内积。现在,我们可以像以下那样简单地使用它:

#include <iostream>
#include <Eigen/Core>

using Matrix = Eigen::MatrixXd;

auto Convolution2D = ...;

int main(int, char **) 
{
    Matrix kernel(3, 3);
    kernel << 
        -1, 0, 1,
        -1, 0, 1,
        -1, 0, 1;

    std::cout << "Kernel:\n" << kernel << "\n\n";

    Matrix input(6, 6);
    input << 3, 1, 0, 2, 5, 6,
        4, 2, 1, 1, 4, 7,
        5, 4, 0, 0, 1, 2,
        1, 2, 2, 1, 3, 4,
        6, 3, 1, 0, 5, 2,
        3, 1, 0, 1, 3, 3;

    std::cout << "Input:\n" << input << "\n\n";

    auto output = Convolution2D(input, kernel);
    std::cout << "Convolution:\n" << output << "\n";

    return 0;
}

        这是我们第一次实现卷积 2D,设计为易于理解。有一段时间,我们不关心性能或输入验证。让我们继续前进以获得更多见解。

在接下来的故事中,我们将学习如何使用快速傅立叶变换和托普利兹矩阵来实现卷积。

五、填充

        在前面的示例中,我们注意到输出矩阵始终小于输入矩阵。有时,这种减少是好的,有时是坏的。我们可以通过在输入矩阵周围添加填充来避免这种减少:

        填充为 1 的输入图像

        卷积中填充的结果如下所示:

        填充卷积 — 作者图片

        实现填充卷积的一种简单(和蛮力)方法如下:

auto Convolution2D = [](const Matrix &input, const Matrix &kernel, int padding)
{
    int kernel_rows = kernel.rows();
    int kernel_cols = kernel.cols();
    int rows = input.rows() - kernel_rows + 2*padding + 1;
    int cols = input.cols() - kernel_cols + 2*padding + 1;

    Matrix padded = Matrix::Zero(input.rows() + 2*padding, input.cols() + 2*padding);
    padded.block(padding, padding, input.rows(), input.cols()) = input;

    Matrix result = Matrix::Zero(rows, cols);

    for(int i = 0; i < rows; ++i) 
    {
        for(int j = 0; j < cols; ++j) 
        {
            double sum = padded.block(i, j, kernel_rows, kernel_cols).cwiseProduct(kernel).sum();
            result(i, j) = sum;
        }
    }

    return result;
};

此代码很简单,但在内存使用方面非常昂贵。请注意,我们正在制作输入矩阵的完整副本以创建填充版本:

Matrix padded = Matrix::Zero(input.rows() + 2*padding, input.cols() + 2*padding);
padded.block(padding, padding, input.rows(), input.cols()) = input;

更好的解决方案可以使用指针来控制切片和内核边界:

auto Convolution2D_v2 = [](const Matrix &input, const Matrix &kernel, int padding)
{
    const int input_rows = input.rows();
    const int input_cols = input.cols();
    const int kernel_rows = kernel.rows();
    const int kernel_cols = kernel.cols();

    if (input_rows < kernel_rows) throw std::invalid_argument("The input has less rows than the kernel");
    if (input_cols < kernel_cols) throw std::invalid_argument("The input has less columns than the kernel");
    
    const int rows = input_rows - kernel_rows + 2*padding + 1;
    const int cols = input_cols - kernel_cols + 2*padding + 1;

    Matrix result = Matrix::Zero(rows, cols);

    auto fit_dims = [&padding](int pos, int k, int length) 
    {
        int input = pos - padding;
        int kernel = 0;
        int size = k;
        if (input < 0) 
        {
            kernel = -input;
            size += input;
            input = 0;
        }
        if (input + size > length) 
        {
            size = length - input;
        }
        return std::make_tuple(input, kernel, size);
    };

    for(int i = 0; i < rows; ++i) 
    {
        const auto [input_i, kernel_i, size_i] = fit_dims(i, kernel_rows, input_rows);
        for(int j = 0; size_i > 0 && j < cols; ++j) 
        {
            const auto [input_j, kernel_j, size_j] = fit_dims(j, kernel_cols, input_cols);
            if (size_j > 0) 
            {
                auto input_tile = input.block(input_i, input_j, size_i, size_j);
                auto input_kernel = kernel.block(kernel_i, kernel_j, size_i, size_j);
                result(i, j) = input_tile.cwiseProduct(input_kernel).sum();
            }
        }
    }
    return result;
};

        这个新代码要好得多,因为这里我们没有分配一个临时内存来保存填充的输入。但是,它仍然可以改进。调用和内存成本也很高。input.block(…)kernel.block(…)

调用的一种解决方案是使用 CwiseNullaryOp 替换它们。block(…)

        我们可以通过以下方式运行填充卷积:

#include <iostream>

#include <Eigen/Core>
using Matrix = Eigen::MatrixXd;
auto Convolution2D = ...; // or Convolution2D_v2

int main(int, char **) 
{
    Matrix kernel(3, 3);
    kernel << 
        -1, 0, 1,
        -1, 0, 1,
        -1, 0, 1;
    std::cout << "Kernel:\n" << kernel << "\n\n";

    Matrix input(6, 6);
    input << 
        3, 1, 0, 2, 5, 6,
        4, 2, 1, 1, 4, 7,
        5, 4, 0, 0, 1, 2,
        1, 2, 2, 1, 3, 4,
        6, 3, 1, 0, 5, 2,
        3, 1, 0, 1, 3, 3;
    std::cout << "Input:\n" << input << "\n\n";

    const int padding = 1;
    auto output = Convolution2D(input, kernel, padding);
    std::cout << "Convolution:\n" << output << "\n";

    return 0;
}

        请注意,现在,输入和输出矩阵具有相同的维度。因此,它被称为填充。默认填充模式,即无填充,通常称为填充。我们的代码允许 ,或任何非负填充。samevalidsamevalid

六、内核

        在深度学习模型中,核通常是奇次矩阵,如、等。有些内核非常有名,比如 Sobel 的过滤器:3x35x511x11

索贝尔过滤器 Gx 和 Gy

        更容易看到每个 Sobel 滤镜对图像的影响:

应用 Sobel 滤镜  

使用 Sobel 过滤器的代码在这里。

        Gy 突出显示水平边缘,Gx 突出显示垂直边缘。因此,Sobel 内核 Gx 和 Gy 通常被称为“边缘检测器”。

        边缘是图像的原始特征,例如纹理、亮度、颜色等。现代计算机视觉的关键点是使用算法直接从数据中自动查找内核,例如Sobel过滤器。或者,使用更好的术语,通过迭代训练过程拟合内核。

        事实证明,训练过程教会计算机程序实现如何执行复杂的任务,例如识别和检测物体、理解自然语言等......内核的训练将在下一个故事中介绍。

七、结论和下一步

        在这个故事中,我们编写了第一个2D卷积,并使用Sobel滤波器作为将此卷积应用于图像的说明性案例。卷积在深度学习中起着核心作用。它们被大量用于当今每个现实世界的机器学习模型中。我们将重新审视卷积,以学习如何改进我们的实现,并涵盖一些功能,如步幅。

        在下一个故事中,我们将讨论机器学习中最核心的问题:成本函数。

引用

用于深度学习的卷积算法指南

深度学习之书,古德费罗

神经网络和深度学习:教科书,Aggarwal

计算机视觉:算法和应用,Szeliski。

信号和系统,罗伯茨

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

VS+Qt+C++旅游景区地图导航源码实例

程序示例精选 VSQtC旅游景区地图导航 如需安装运行环境或远程调试&#xff0c;见文章底部个人QQ名片&#xff0c;由专业技术人员远程协助&#xff01; 前言 这篇博客针对<<VSQtC旅游景区地图导航>>编写代码&#xff0c;代码整洁&#xff0c;规则&#xff0c;易读。…

服务器数据恢复-RAID5上层Hyper-V虚拟机数据恢复案例

服务器数据恢复环境&#xff1a; 一台Windows Server服务器&#xff0c;部署Hyper-V虚拟化环境&#xff0c;虚拟机的硬盘文件和配置文件存放在一台DELL存储中。该存储中有一组由4块硬盘组建的RAID5阵列&#xff0c;用来存放虚拟机的数据文件&#xff0c;另外还有一块大容量硬盘…

Android Studio实现Spinner下拉列表

效果图 点击下拉列表 点击某一个下拉列表 MainActivity package com.example.spinneradapterpro;import androidx.appcompat.app.AppCompatActivity;import android.os.Bundle; import android.view.View; import android.widget.AdapterView; import android.widget.Spinn…

原型模式与享元模式:提升系统性能的利器

原型模式和享元模式&#xff0c;前者是在创建多个实例时&#xff0c;对创建过程的性能进行调优&#xff1b;后者是用减 少创建实例的方式&#xff0c;来调优系统性能。这么看&#xff0c;你会不会觉得两个模式有点相互矛盾呢&#xff1f; 在有些场景下&#xff0c;我们需要重复…

Java # Spring(2)

一、Spring事物 一、分类 编程式事物&#xff1a;代码中硬编码&#xff08;不推荐使用&#xff09; 声明式事物&#xff1a;配置文件中配置&#xff08;推荐使用&#xff09; 分类&#xff1a; 基于xml的声明式事物基于注解的声明式事物 二、隔离级别 ISOLATION_DEFAULT&…

Android 视频播放器dkplayer

列表播放如图所示&#xff1a; 一、依赖 //添加RecyclerView的依赖包implementation androidx.recyclerview:recyclerview:1.2.1// 异步加载图片依赖implementation com.squareup.picasso:picasso:2.5.2// 上拉刷新、下来加载依赖implementation com.scwang.smartrefresh:Smart…

mysql之存储过程

目录 一、mysql之存储过程的相关知识 1&#xff09;存储过程的概念 2&#xff09;存储过程的优点 二、存储过程的管理 1&#xff09;创建存储过程 基本格式&#xff1a; 2&#xff09;调用存储过程 格式&#xff1a; call 存储过程名称 3&#xff09;查看存储过程 查…

Leetcode-每日一题【剑指 Offer 13. 机器人的运动范围】

题目 地上有一个m行n列的方格&#xff0c;从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动&#xff0c;它每次可以向左、右、上、下移动一格&#xff08;不能移动到方格外&#xff09;&#xff0c;也不能进入行坐标和列坐标的数位之和大于k的格子。例…

51单片机学习--红外遥控(外部中断)

需要利用下面这个红外接收头&#xff0c;OUT口会发出红外信号对应的高低电平&#xff0c;由于发送的速度很快&#xff0c;所以需要把OUT引脚接在外部中断引脚上&#xff0c;当OUT一旦产生下降沿&#xff0c;马上进中断&#xff0c;这样响应会更及时。 外部中断引脚位于P3_2和P…

【云原生】kubernetes控制器deployment的使用

目录 ​编辑 1 Controller 控制器 1.1 什么是 Controller 1.2 常见的 Controller 控制器 1.3 Controller 如何管理 Pod 2 Deployment 2.1 创建 deployment 2.2 查看 deployment 2.3 扩缩 deployment 2.4 回滚 deployment 2.5 删除 deployment 1 Controller 控制器 …

条条大路通罗马系列—— 使用 Hiredis-cluster 连接 Amazon ElastiCache for Redis 集群

前言 Amazon ElastiCache for Redis 是速度超快的内存数据存储&#xff0c;能够提供亚毫秒级延迟来支持 实时应用程序。适用于 Redis 的 ElastiCache 基于开源 Redis 构建&#xff0c;可与 Redis API 兼容&#xff0c;能够与 Redis 客户端配合工作&#xff0c;并使用开放的 Re…

【算法篇C++实现】五大常规算法

文章目录 &#x1f680;一、分治法⛳&#xff08;一&#xff09;算法思想⛳&#xff08;二&#xff09;相关代码 &#x1f680;二、动态规划算法⛳&#xff08;一&#xff09;算法思想⛳&#xff08;二&#xff09;相关代码 &#x1f680;三、回溯算法⛳&#xff08;一&#xf…

数据挖掘具体步骤

数据挖掘具体步骤 1、理解业务与数据 2、准备数据 数据清洗&#xff1a; 缺失值处理&#xff1a; 异常值: 数据标准化&#xff1a; 特征选择&#xff1a; 数据采样处理&#xff1a; 3、数据建模 分类问题&#xff1a; 聚类问题&#xff1a; 回归问题 关联分析 集成学习 image B…

区块链学习6-长安链部署:如何创建特定共识节点数和同步节点数的链

正常prepare的时候只支持4 7 13 16个节点个数&#xff0c;想要创建10个节点&#xff0c;其中5个是共识节点&#xff0c;如何实现&#xff1f; 1. 注释掉prepare.sh的这几行&#xff1a; 2. 修改 crytogen的模板文件&#xff1a; 如果是cert模式&#xff1a;chainmaker-crypt…

idea+gradle阅读spring5.2.9源码之源码构建报错解决方案

注意 1、先确保gradle版本和spring、jdk版本对应 本文:gradle:5.6.4/spring 5.2.9/jdk1.8&#xff08;gradle和jdk都要先安装好&#xff0c;gradle还要配置好本地资源文件路径&#xff09; 2、原来项目乱了的话&#xff0c;先重新导入下载的源码项目 3、进入源码所在根目录&…

Redis数据库的下载和安装

目录 第一章、Redis数据库的下载和安装1.1&#xff09;nosql数据库和 Redis 介绍1.2&#xff09;Windows中下载安装Redis数据库1.3&#xff09;Linux中安装Redis数据库1.4&#xff09;Linux中启动redis1.5&#xff09;Linux中关闭redis 第二章、三种Redis客户端连接Redis数据库…

java代码审计9之XXE

文章目录 1、简介2、 java XXE审计函数3、漏洞3.1、正常的业务3.2、有回显的情况3.3、无回显的情况3.4、修复 之前的文章&#xff0c; php代码审计9之XXE 1、简介 XXE&#xff08;XML外部实体注⼊&#xff0c;XML External Entity) &#xff0c;在应⽤程序解析XML输⼊时&…

【Java-16】动态代理的使用方法及原理实现

代理模式&#xff1a;静态代理 目标 了解静态代理模式实现 路径 静态代理概述静态代理案例 静态代理概述 静态代理&#xff1a; 是由程序员创建或工具生成代理类的源码&#xff0c;再编译成为字节码 &#xff08;字节码文件在没有运行java之前就存在了&#xff09; 在编译…

深度学习:使用卷积神经网络CNN实现MNIST手写数字识别

引言 本项目基于pytorch构建了一个深度学习神经网络&#xff0c;网络包含卷积层、池化层、全连接层&#xff0c;通过此网络实现对MINST数据集手写数字的识别&#xff0c;通过本项目代码&#xff0c;从原理上理解手写数字识别的全过程&#xff0c;包括反向传播&#xff0c;梯度…

【第一阶段】kotlin语言的Nothing类型

fun main() {show(60) } //两种写法一样 private fun show(num:Int){when(num){//下面这句话不是注释提示&#xff0c;会终止程序-1->TODO("不符合")in 0..59->println("不及格")in 60..89->println("及格")in 90..100->println(&qu…