【CNN】很详细的讲解啥以及为啥是卷积(Convolution)!

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了【CNN】很详细的讲解啥以及为啥是卷积(Convolution)!相关的知识,希望对你有一定的参考价值。

参考技术A


卷积这个概念,很早以前就学过,但是一直没有搞懂。教科书上通常会给出定义,给出很多性质,也会用实例和图形进行解释,但究竟为什么要这么设计,这么计算,背后的意义是什么,往往语焉不详。作为一个学物理出身的人,一个公式倘若倘若给不出结合实际的直观的通俗的解释(也就是背后的“物理”意义),就觉得少了点什么,觉得不是真的懂了。


教科书上一般定义函数  f, g  的卷积  f * g(n)  如下:


连续形式:

离散形式:


并且也解释了,先对g函数进行翻转,相当于在数轴上把g函数从右边褶到左边去,也就是卷积的“卷”的由来。


然后再把g函数平移到n,在这个位置对两个函数的对应点相乘,然后相加,这个过程是卷积的“积”的过程。


这个只是从计算的方式上对公式进行了解释,从数学上讲无可挑剔,但进一步追问,为什么要先翻转再平移,这么设计有何用意?还是有点费解。


在知乎,已经很多的热心网友对卷积举了很多形象的例子进行了解释,如卷地毯、丢骰子、打耳光、存钱等等。读完觉得非常生动有趣,但过细想想,还是感觉有些地方还是没解释清楚,甚至可能还有瑕疵,或者还可以改进(这些后面我会做一些分析)。


带着问题想了两个晚上,终于觉得有些问题想通了,所以就写出来跟网友分享,共同学习提高。不对的地方欢迎评论拍砖。。。


明确一下,这篇文章主要想解释两个问题:


1. 卷积这个名词是怎么解释?“卷”是什么意思?“积”又是什么意思?


2. 卷积背后的意义是什么,该如何解释?




为了更好地理解这些问题,我们先给出两个典型的应用场景:


1. 信号分析


一个输入信号 f(t) ,经过一个线性系统(其特征可以用单位冲击响应函数 g(t) 描述)以后,输出信号应该是什么?实际上通过卷积运算就可以得到输出信号。


2. 图像处理


输入一幅图像 f(x,y) ,经过特定设计的卷积核 g(x,y) 进行卷积处理以后,输出图像将会得到模糊,边缘强化等各种效果。




对卷积这个名词的理解:所谓两个函数的卷积,本质上就是先将一个函数翻转,然后进行滑动叠加。


在连续情况下,叠加指的是对两个函数的乘积求积分,在离散情况下就是加权求和,为简单起见就统一称为叠加。


整体看来是这么个过程:


翻转——>滑动——>叠加——>滑动——>叠加——>滑动——>叠加.....


多次滑动得到的一系列叠加值,构成了卷积函数。


卷积的“卷”,指的的函数的翻转,从  g(t)  变成  g(-t)  的这个过程; 同时,“卷”还有滑动的意味在里面(吸取了网友李文清的建议)。如果把卷积翻译为“褶积”,那么这个“褶”字就只有翻转的含义了。


卷积的“积”,指的是积分/加权求和。


有些文章只强调滑动叠加求和,而没有说函数的翻转,我觉得是不全面的;有的文章对“卷”的理解其实是“积”,我觉得是张冠李戴。


对卷积的意义的理解:


1. 从“积”的过程可以看到,我们得到的叠加值,是个全局的概念。以信号分析为例,卷积的结果是不仅跟当前时刻输入信号的响应值有关,也跟过去所有时刻输入信号的响应都有关系,考虑了对过去的所有输入的效果的累积。在图像处理的中,卷积处理的结果,其实就是把每个像素周边的,甚至是整个图像的像素都考虑进来,对当前像素进行某种加权处理。所以说,“积”是全局概念,或者说是一种“混合”,把两个函数在时间或者空间上进行混合。


2. 那为什么要进行“卷”?直接相乘不好吗?我的理解,进行“卷”(翻转)的目的其实是施加一种约束,它指定了在“积”的时候以什么为参照。在信号分析的场景,它指定了在哪个特定时间点的前后进行“积”,在空间分析的场景,它指定了在哪个位置的周边进行累积处理。




下面举几个例子说明为什么要翻转,以及叠加求和的意义。



如下图所示,输入信号是  f(t)  ,是随时间变化的。系统响应函数是  g(t)  ,图中的响应函数是随时间指数下降的,它的物理意义是说:如果在  t =0 的时刻有一个输入,那么随着时间的流逝,这个输入将不断衰减。换言之,到了  t =T时刻,原来在  t =0 时刻的输入 f (0)的值将衰减为 f (0) g (T)。



考虑到信号是连续输入的,也就是说,每个时刻都有新的信号进来,所以,最终输出的是所有之前输入信号的累积效果。如下图所示,在T=10时刻,输出结果跟图中带标记的区域整体有关。其中,f(10)因为是刚输入的,所以其输出结果应该是f(10)g(0),而时刻t=9的输入f(9),只经过了1个时间单位的衰减,所以产生的输出应该是 f(9)g(1),如此类推,即图中虚线所描述的关系。这些对应点相乘然后累加,就是T=10时刻的输出信号值,这个结果也是f和g两个函数在T=10时刻的卷积值。



显然,上面的对应关系看上去比较难看,是拧着的,所以,我们把g函数对折一下,变成了g(-t),这样就好看一些了。看到了吗?这就是为什么卷积要“卷”,要翻转的原因,这是从它的物理意义中给出的。



上图虽然没有拧着,已经顺过来了,但看上去还有点错位,所以再进一步平移T个单位,就是下图。它就是本文开始给出的卷积定义的一种图形的表述:



所以,在以上计算T时刻的卷积时,要维持的约束就是: t+ (T-t) = T   。这种约束的意义,大家可以自己体会。



在本问题 如何通俗易懂地解释卷积?中排名第一的马同学在中举了一个很好的例子(下面的一些图摘自马同学的文章,在此表示感谢),用丢骰子说明了卷积的应用。


要解决的问题是:有两枚骰子,把它们都抛出去,两枚骰子点数加起来为4的概率是多少?


分析一下,两枚骰子点数加起来为4的情况有三种情况:1+3=4, 2+2=4, 3+1=4


因此,两枚骰子点数加起来为4的概率为:


写成卷积的方式就是:



在这里我想进一步用上面的翻转滑动叠加的逻辑进行解释。


首先,因为两个骰子的点数和是4,为了满足这个约束条件,我们还是把函数 g 翻转一下,然后阴影区域上下对应的数相乘,然后累加,相当于求自变量为4的卷积值,如下图所示:



进一步,如此翻转以后,可以方便地进行推广去求两个骰子点数和为 n 时的概率,为f 和 g的卷积 f*g(n),如下图所示:



由上图可以看到,函数 g 的滑动,带来的是点数和的增大。这个例子中对f和g的约束条件就是点数和,它也是卷积函数的自变量。有兴趣还可以算算,如果骰子的每个点数出现的概率是均等的,那么两个骰子的点数和n=7的时候,概率最大。



还是引用知乎问题 如何通俗易懂地解释卷积?中马同学的例子。图像可以表示为矩阵形式(下图摘自马同学的文章):

对图像的处理函数(如平滑,或者边缘提取),也可以用一个g矩阵来表示,如:



注意,我们在处理平面空间的问题,已经是二维函数了,相当于:



那么函数f和g的在(u,v)处的卷积该如何计算呢?


首先我们在原始图像矩阵中取出(u,v)处的矩阵:



然后将图像处理矩阵翻转(这个翻转有点意思,不是延x轴和y轴两个方向翻转,而是沿右上到左下的对角线翻转,这是为了凑后面的内积公式。),如下:



可对比下图:


计算卷积时,就可以用和的内积:





请注意,以上公式有一个特点,做乘法的两个对应变量a,b的下标之和都是(u,v),其目的是对这种加权求和进行一种约束。这也是为什么要将矩阵g进行翻转的原因。以上矩阵下标之所以那么写,并且进行了翻转,是为了让大家更清楚地看到跟卷积的关系。这样做的好处是便于推广,也便于理解其物理意义。实际在计算的时候,都是用翻转以后的矩阵,直接求矩阵内积就可以了。

以上计算的是(u,v)处的卷积,延x轴或者y轴滑动,就可以求出图像中各个位置的卷积,其输出结果是处理以后的图像(即经过平滑、边缘提取等各种处理的图像)。


再深入思考一下,在算图像卷积的时候,我们是直接在原始图像矩阵中取了(u,v)处的矩阵,为什么要取这个位置的矩阵,本质上其实是为了满足以上的约束。因为我们要算(u,v)处的卷积,而g矩阵是3x3的矩阵,要满足下标跟这个3x3矩阵的和是(u,v),只能是取原始图像中以(u,v)为中心的这个3x3矩阵,即图中的阴影区域的矩阵。


推而广之,如果如果g矩阵不是3x3,而是6x6,那我们就要在原始图像中取以(u,v)为中心的6x6矩阵进行计算。由此可见,这种卷积就是把原始图像中的相邻像素都考虑进来,进行混合。相邻的区域范围取决于g矩阵的维度,维度越大,涉及的周边像素越多。而矩阵的设计,则决定了这种混合输出的图像跟原始图像比,究竟是模糊了,还是更锐利了。


比如说,如下图像处理矩阵将使得图像变得更为平滑,显得更模糊,因为它联合周边像素进行了平均处理:




而如下图像处理矩阵将使得像素值变化明显的地方更为明显,强化边缘,而变化平缓的地方没有影响,达到提取边缘的目的:


5、对一些解释的不同意见


上面一些对卷积的形象解释,如知乎问题 卷积为什么叫「卷」积?中荆哲以及问题 如何通俗易懂地解释卷积?中马同学等人提出的如下比喻:




其实图中“卷”的方向,是沿该方向进行积分求和的方向,并无翻转之意。因此,这种解释,并没有完整描述卷积的含义,对“卷”的理解值得商榷。


6、一些参考资料


《数字信号处理(第二版)》程乾生,北京大学出版社

《信号与系统引论》 郑君里,应启珩,杨为理,高等教育出版社


卷积神经网络(CNN)讲解及代码

相关文章:
1. 经典反向传播算法公式详细推导
2. 卷积神经网络(CNN)反向传播算法公式详细推导

网上有很多关于CNN的教程讲解,在这里我们抛开长篇大论,只针对代码来谈。本文用的是matlab编写的deeplearning toolbox,包括NN、CNN、DBN、SAE、CAE。在这里我们感谢作者编写了这样一个简单易懂,适用于新手学习的代码。由于本文直接针对代码,这就要求读者有一定的CNN基础,可以参考Lecun的Gradient-Based Learning Applied to Document Recognitiontornadomeet的博文
首先把Toolbox下载下来,解压缩到某位置。然后打开Matlab,把文件夹内的util和data利用Set Path添加至路径中。接着打开tests文件夹的test_example_CNN.m。最后在文件夹CNN中运行该代码。

下面是test_example_CNN.m中的代码及注释,比较简单。

load mnist_uint8;  %读取数据

% 把图像的灰度值变成0~1,因为本代码采用的是sigmoid激活函数
train_x = double(reshape(train_x',28,28,60000))/255;
test_x = double(reshape(test_x',28,28,10000))/255;
train_y = double(train_y');
test_y = double(test_y');

%% 卷积网络的结构为 6c-2s-12c-2s 
% 1 epoch 会运行大约200s, 错误率大约为11%。而 100 epochs 的错误率大约为1.2%。

rand('state',0) %指定状态使每次运行产生的随机结果相同

cnn.layers = 
    struct('type', 'i') % 输入层
    struct('type', 'c', 'outputmaps', 6, 'kernelsize', 5) % 卷积层
    struct('type', 's', 'scale', 2) % pooling层
    struct('type', 'c', 'outputmaps', 12, 'kernelsize', 5) % 卷积层
    struct('type', 's', 'scale', 2) % pooling层
;


opts.alpha = 1;  % 梯度下降的步长
opts.batchsize = 50; % 每次批处理50张图
opts.numepochs = 1; % 所有图片循环处理一次

cnn = cnnsetup(cnn, train_x, train_y); % 初始化CNN
cnn = cnntrain(cnn, train_x, train_y, opts); % 训练CNN

[er, bad] = cnntest(cnn, test_x, test_y); % 测试CNN

%plot mean squared error
figure; plot(cnn.rL);
assert(er<0.12, 'Too big error');

下面是cnnsetup.m中的代码及注释。

function net = cnnsetup(net, x, y)
    assert(~isOctave() || compare_versions(OCTAVE_VERSION, '3.8.0', '>='), ['Octave 3.8.0 or greater is required for CNNs as there is a bug in convolution in previous versions. See http://savannah.gnu.org/bugs/?39314. Your version is ' myOctaveVersion]);  %判断版本
    inputmaps = 1;  % 由于网络的输入为1张特征图,因此inputmaps为1
    mapsize = size(squeeze(x(:, :, 1)));  %squeeze():除去x中为1的维度,即得到28*28

    for l = 1 : numel(net.layers)   % 网络层数
        if strcmp(net.layersl.type, 's') % 如果是pooling层
            mapsize = mapsize / net.layersl.scale; % pooling之后图的大小            
            assert(all(floor(mapsize)==mapsize), ['Layer ' num2str(l) ' size must be integer. Actual: ' num2str(mapsize)]);
            for j = 1 : inputmaps
                net.layersl.bj = 0; % 偏置项
            end
        end
        if strcmp(net.layersl.type, 'c') % 如果是卷积层
            mapsize = mapsize - net.layersl.kernelsize + 1; % 确定卷积之后图像大小
            fan_out = net.layersl.outputmaps * net.layersl.kernelsize ^ 2; % 上一层连到该层的权值参数总个数
            for j = 1 : net.layersl.outputmaps  % 第l层特征图的数量
                fan_in = inputmaps * net.layersl.kernelsize ^ 2; % 上一层连到该层的第j个特征图的权值参数
                for i = 1 : inputmaps  % 上一层特征图的数量
                    net.layersl.kij = (rand(net.layersl.kernelsize) - 0.5) * 2 * sqrt(6 / (fan_in + fan_out));   % 初始化权值,见论文Understanding the difficulty of training deep feedforward neural networks
                end
                net.layersl.bj = 0; % 偏置项
            end
            inputmaps = net.layersl.outputmaps;  % 用该层的outputmaps更新inputmaps的值并为下一层所用
        end
    end
    % 'onum' is the number of labels, that's why it is calculated using size(y, 1). If you have 20 labels so the output of the network will be 20 neurons.
    % 'fvnum' is the number of output neurons at the last layer, the layer just before the output layer.
    % 'ffb' is the biases of the output neurons.
    % 'ffW' is the weights between the last layer and the output neurons. Note that the last layer is fully connected to the output layer, that's why the size of the weights is (onum * fvnum)
    fvnum = prod(mapsize) * inputmaps; % 最终输出层前一层的结点数目
    onum = size(y, 1);  % 类别数目

    % 最后一层全连接网络的参数
    net.ffb = zeros(onum, 1); 
    net.ffW = (rand(onum, fvnum) - 0.5) * 2 * sqrt(6 / (onum + fvnum));
end

下面是cnntrain.m中的代码及注释。

function net = cnntrain(net, x, y, opts)
    m = size(x, 3);
    numbatches = m / opts.batchsize; 
    if rem(numbatches, 1) ~= 0
        error('numbatches not integer');
    end
    net.rL = [];
    for i = 1 : opts.numepochs
        disp(['epoch ' num2str(i) '/' num2str(opts.numepochs)]);
        tic; %tic和toc配套使用来求运行时间 
        kk = randperm(m);
        for l = 1 : numbatches
            batch_x = x(:, :, kk((l - 1) * opts.batchsize + 1 : l * opts.batchsize));
            batch_y = y(:,    kk((l - 1) * opts.batchsize + 1 : l * opts.batchsize));

            net = cnnff(net, batch_x);   % 前向传播
            net = cnnbp(net, batch_y);   % BP反向传播
            net = cnnapplygrads(net, opts);
            if isempty(net.rL)
                net.rL(1) = net.L;
            end
            net.rL(end + 1) = 0.99 * net.rL(end) + 0.01 * net.L;
        end
        toc;
    end

end

下面是cnnff.m中的代码及注释。

function net = cnnff(net, x)
    n = numel(net.layers);
    net.layers1.a1 = x;
    inputmaps = 1;

    for l = 2 : n   % 除输入层以外的每一层
        if strcmp(net.layersl.type, 'c') % 卷积层
            %  !!below can probably be handled by insane matrix operations
            for j = 1 : net.layersl.outputmaps   % 该层的每一个特征图
                % 该层的输出:上一层图片大小-kernel+1
                z = zeros(size(net.layersl - 1.a1) - [net.layersl.kernelsize - 1 net.layersl.kernelsize - 1 0]);
                for i = 1 : inputmaps   % 对于每一个输入特征图(本例中为全连接)
                    % 每个特征图的卷积都相加,convn()为matlab自带卷积函数
                    z = z + convn(net.layersl - 1.ai, net.layersl.kij, 'valid');
                end
                % 加入偏置项,并通过sigmoid函数(现今一般采用RELU)
                net.layersl.aj = sigm(z + net.layersl.bj);
            end
            % 用该层的outputmaps更新inputmaps的值并为下一层所用
            inputmaps = net.layersl.outputmaps;
        elseif strcmp(net.layersl.type, 's')
            % mean-pooling
            for j = 1 : inputmaps
                z = convn(net.layersl - 1.aj, ones(net.layersl.scale) / (net.layersl.scale ^ 2), 'valid');   %  !! replace with variable
                % 取出有效的mean-pooling矩阵,即每隔net.layersl.scale提取一个值
                net.layersl.aj = z(1 : net.layersl.scale : end, 1 : net.layersl.scale : end, :);
            end
        end
    end

    % 把最后一层展开变成一行向量方便操作
    net.fv = [];
    for j = 1 : numel(net.layersn.a)
        sa = size(net.layersn.aj);
        net.fv = [net.fv; reshape(net.layersn.aj, sa(1) * sa(2), sa(3))];
    end
    % 加上权值和偏置项并通入sigmoid(多类别神经网络的输出一般采用softmax形式,损失函数使用cross entropy )
    net.o = sigm(net.ffW * net.fv + repmat(net.ffb, 1, size(net.fv, 2)));

end

下面是cnnbp.m中的代码及注释,比较复杂。首先要有普通BP的基础,可以参考CeleryChen的博客,而CNN的反向传播略有不同,可以参考tornadomeet的博客

function net = cnnbp(net, y)
    n = numel(net.layers);
    net.e = net.o - y; % 误差
    % loss函数,这里采用的方法是MSE(多类别神经网络的输出一般采用softmax形式,损失函数使用cross entropy)
    net.L = 1/2* sum(net.e(:) .^ 2) / size(net.e, 2);

    %%  backprop deltas
    net.od = net.e .* (net.o .* (1 - net.o));   %  输出层delta,包括sigmoid求导(delta为loss函数对该层未经激活函数结点的导数)
    net.fvd = (net.ffW' * net.od);              %  隐藏层(倒数第二层)delta
    if strcmp(net.layersn.type, 'c')         % 只有卷积层才可以通过sigmoid函数,本CNN结构最后一个隐藏层为pooling
        net.fvd = net.fvd .* (net.fv .* (1 - net.fv));
    end

    % 把最后一隐藏层的delta向量变成形如a的向量
    sa = size(net.layersn.a1);
    fvnum = sa(1) * sa(2);
    for j = 1 : numel(net.layersn.a)
        net.layersn.dj = reshape(net.fvd(((j - 1) * fvnum + 1) : j * fvnum, :), sa(1), sa(2), sa(3));
    end

    % 逆向传播,计算各层的delta
    for l = (n - 1) : -1 : 1
        if strcmp(net.layersl.type, 'c') % 卷积层且下一层为pooling层
            for j = 1 : numel(net.layersl.a) % 对该层每一个特征图操作(考虑sigmoid函数,upsample)
                net.layersl.dj = net.layersl.aj .* (1 - net.layersl.aj) .* (expand(net.layersl + 1.dj, [net.layersl + 1.scale net.layersl + 1.scale 1]) / net.layersl + 1.scale ^ 2);
            end
        elseif strcmp(net.layersl.type, 's') % pooling层且下一层为卷积层
            for i = 1 : numel(net.layersl.a)
                z = zeros(size(net.layersl.a1));
                for j = 1 : numel(net.layersl + 1.a) % 第l+1层所有特征核的贡献之和
                    X =  net.layersl + 1.kij;
                    z = z + convn(net.layersl + 1.dj, flipdim(flipdim(X, 1), 2), 'full');
                    %z = z + convn(net.layersl + 1.dj, rot180(net.layersl + 1.kij), 'full');
                end
                net.layersl.di = z;
            end
        end
    end

    %% 通过delta计算梯度
    for l = 2 : n
        if strcmp(net.layersl.type, 'c')  % 只有卷积层计算梯度,pooling层的参数固定不变
            for j = 1 : numel(net.layersl.a)
                for i = 1 : numel(net.layersl - 1.a)
                    net.layersl.dkij = convn(flipall(net.layersl - 1.ai), net.layersl.dj, 'valid') / size(net.layersl.dj, 3);  % 旋转180°,卷积,再旋转180°
                end
                net.layersl.dbj = sum(net.layersl.dj(:)) / size(net.layersl.dj, 3); % 所有delta相加,除以每一次批处理的个数
            end
        end
    end
    net.dffW = net.od * (net.fv') / size(net.od, 2); %除以每次批处理个数
    net.dffb = mean(net.od, 2);

    function X = rot180(X)
        X = flipdim(flipdim(X, 1), 2);
    end
end

下面是cnnapplygrads.m中的代码,比较简单,在这里就不进行注释了。

function net = cnnapplygrads(net, opts)
    for l = 2 : numel(net.layers)
        if strcmp(net.layersl.type, 'c')
            for j = 1 : numel(net.layersl.a)
                for ii = 1 : numel(net.layersl - 1.a)
                    net.layersl.kiij = net.layersl.kiij - opts.alpha * net.layersl.dkiij;
                end
                net.layersl.bj = net.layersl.bj - opts.alpha * net.layersl.dbj;
            end
        end
    end

    net.ffW = net.ffW - opts.alpha * net.dffW;
    net.ffb = net.ffb - opts.alpha * net.dffb;
end

以上是关于【CNN】很详细的讲解啥以及为啥是卷积(Convolution)!的主要内容,如果未能解决你的问题,请参考以下文章

卷积神经网络(CNN)讲解及代码

卷积神经网络(CNN)详细介绍及其原理详解

[人工智能-深度学习-26]:卷积神经网络CNN - 为啥要卷积神经网络以及卷积神经网络的应用

卷积神经网络(CNN)反向传播算法公式详细推导

卷积神经网络(CNN)反向传播算法公式详细推导

深度学习核心技术精讲100篇(六十三)-CNN一文详细讲解前因后果