Kai Su http://yihui.name 2022-10-25T05:52:26+00:00 xie@yihui.name 最近几篇Attention CNNs https://7color94.github.io/2019/01/attention-cnns/ 2019-01-02T00:00:00+00:00 Kai Su https://7color94.github.io/2019/01/attention-cnns 1.BAM

BAM: Bottleneck Attention Module,BMVC 2018

对SENet的改进:1)channel attention和SENet一样;2)SENet只关注channel attention,BAM增加了spatial attention,说白了过几个conv,其中有两个dilated conv去学spatial context information

2.CBAM

CBAM: Convolutional Block Attention Module,ECCV 2018

对SENet的改进:1)SENet采用global average pooling来embed spatial global information,CBAM同时使用了avg/max pooling;2)SENet只关注channel attention,CBAM增加了spatial attention,也是采用avg/max pooling去embed channel global information

无论是SENet,BAM,CBAM,无论是channel/spatial attention branch,相应的branch总得做点什么(比如pooling,dilated conv)去学该branch的context information,而不能简单地过conv和sigmoid ??

3.Non-Local 系列

“capture long-range dependencies directly by computing interactions between ay two positions, regardless of their positional distance”

Non-local Neural Networks,CVPR 2018

A^2-Nets: Double Attention Networks,NIPS 2018

两篇用Non-Local做分割的,感觉没啥区别:

Dual Attention Network for Scene Segmentation,AAAI 2019

OCNet: Object Context Network for Scene Parsing,arxiv 2018

最先有Non-Local思想应该是Attention Is All You Need:

Attention Is All You Need,NIPS 2017

4.PAN

Pyramid Attention Network for Semantic Segmentation,BMVC 2018

“Furthermore, high-level features with abundant category information can be used to weight low-level information to select precise resolution details.”

“Our Global Attention Upsample module performs global average pooling to provide global context as a guidance of low-level features to select category localization details.”

]]>
Mask R-CNN https://7color94.github.io/2018/12/mask-rcnn/ 2018-12-24T00:00:00+00:00 Kai Su https://7color94.github.io/2018/12/mask-rcnn
  • Paper: Mask R-CNN
  • Source Code: mmdetection
  • 论文就不多讲了(一搜一大堆),这里主要记录读源代码时想记下来的东西。

    1.网络结构

    首先Mask R-CNN属于TwoStageDetector,结构组成有:

    • neck:基于ResNet的FPN结构,主要用来提取5个不同level的feature maps(分辨率分别是原图的1/4,1/8,1/16,1/32和1/64),其中1/64的feature map是对1/32做stride=2的max_pooling得到。
    • rpn_head:最经典的RPN了。。对5个不同level的feature maps逐个先过3x3的conv和relu,再经过不同的1x1 conv分别得到(N,3,H,W)的rpn_cls_score和(N,12,H,W)的rpn_bbox_pred,N为batch size,3是指每个点3个anchors(下面会提到)。
    • bbox_head:先用bbox_roi_extractor对proposals提取到7x7x256的roi特征,然后经过2层shared fc,再经过不同的1层fc分别得到81类的cls_score(cls branch)和bbox_pred(box branch)。
    • mask_head:先用mask_roi_extractor提取得到14x14x256的roi特征,然后经过4层conv/deconv的小型FCN,最终得到28x28x80的mask map。

    bbox_head和mask_head具体网络结构可以看paper的Figure 4。

    2.数据加载

    CocoDataset继承了CustomDataset

    这里主要是做了random fliprandom scale。若想真正地开启random scale training的话,需要在cfg设置多个scale,目前支持“value”和“range”两种scale采样的方式,最后通过pad确保scale后图片的长宽都能被32整除。

    还有一个值得注意的问题是由于coco数据集图片的长宽不一致,所以把同一个batch但不同大小的图片cat一起送进torch的网络之前,会对小图做pad

    3.训练阶段

    下面进入训练

    3.1 rpn_head

    先用AnchorGenerator为每个level的feature map获取anchors,每个点3个(1:1,2:1,1:2),并进行anchor的过滤

    之后通过MaxIoUAssigner为每个anchor分配gt bbox或者background,原则是阈值pos_iou_thr=0.7最大iou。然后采样num=256个正负anchors,正负比例最好是1:1,去训rpn。正anchor和对应的gt bbox之间的回归量通过bbox2delta计算。

    最终loss由cls和reg组成

    3.2 bbox_head和mask_head

    rpn_head和bbox_head、mask_head可以joint training。在计算好rpn_head的loss之后,rpn_head进入“前向测试”状态,获取bbox_head和mask_head需要的proposals,具体来说:rpn_head还是先生成anchors,然后根据rpn_cls_score去排序anchors,选择前cfg.nms_pre=2000个anchors,把回归量rpn_bbox_pred计算在anchors上得到proposals,再经由nms(nms_thr=0.7)得到最多2000个proposals。

    拿到proposals之后,rcnn根据之前的rpn的规则(这里pos_iou_thr=0.5)为每个proposals分配gt bbox,并采样num=512个正负proposals,正负比例1:3

    有了proposals,后面不管什么head/branch,就简单了。

    bbox_head针对正负proposals,训练分类(81类)和回归任务(box refine)。

    mask_head只针对正proposals准备对应的mask gt,去训练mask任务。

    4.测试阶段

    ]]>
    vid2vid https://7color94.github.io/2018/12/vid2vid/ 2018-12-09T00:00:00+00:00 Kai Su https://7color94.github.io/2018/12/vid2vid vid2vid提出了一种通用的video to video的生成框架,可以用于很多视频生成任务。常用的pix2pix没有对temporal dynamics建模,所以不能直接用于video synthesis。下面就pose2body对着vid2vide code简单记录一二。

    推荐观看vid2vid youtube

    1. Network Architecture

    1.1 Sequential Generator

    sequential generator在生成当前帧时需要考虑:1)当前帧的输入图片;2)前几帧的输入图片;3)前几帧的生成图片。回看多少帧由n_frames_G设置,如下图中n_frames_G设置为3:

    CompositeGenerator

    sequential generator可以得到当前帧的预测图intermediate image、前一帧到当前帧的光流图flow map和occlusion mask。之后当前帧的生成结果由前一帧生成结果经由光流warp后的图片(img_warp)和当前帧的预测图(img_raw)通过mask 加权而成,这样做是因为前后连续帧间有非常多的相似信息。

    同时对于高分辨率的图片生成,和pix2pixHD一样,提出了coarse-to-fine的generator,通过n_scales_spatial控制:

    CompositeLocalGenerator

    如果是采用coarse-to-fine generator,在训练时可以通过设置niter_fix_global让Local 部分(高分辨率部分)单独先训几个epoch,和pix2pixHD中做法类似。

    1.2 Image Discriminator

    image discriminator的目的是给定相同的图片输入,让生成帧和真实帧保持一致。这和pix2pixHD的discriminator并无差别,结构也采用了Multi-scale PatchGAN。

    1.3 Video Discriminator

    video discriminator的目的是给定相同的光流,让连续的生成帧之间的光流信息和连续的真实帧之间的保持一致。所以image discriminator作用在图片上,video discriminator作用在光流上。

    另外,vid2vid设计了temporally multi-scale video discriminator。这里“multi-scale”目的是通过采样连续帧的密度由密到疏,以便捕捉short-term和long-term的光流一致性,具体采样细节在代码get_skipped_frames中体现。每个“scale”(密度)的video discriminator都是一个Multi-scale PatchGAN。

    VideoDiscriminator

    2. DataLoader

    2.1 PoseDataset

    PoseDataset用来加载openpose、densepose的label和真实的img,同时在训练时做random scalerandom crop

    3. Training

    vid2vid训练时的loss比较多。

    4. Test

    利用sequential generator不断生成就可以了。

    ]]>
    pix2pix和CycleGAN https://7color94.github.io/2018/10/pix2pix-CycleGAN/ 2018-10-10T00:00:00+00:00 Kai Su https://7color94.github.io/2018/10/pix2pix-CycleGAN 1. pix2pix

    1.1 网络结构

    Generator

    遵循Encoder-Decoder,先下采样,再上采样回到input的尺寸,conv后面一般都跟bn、relu。常见Generator的结构有:ResnetGenerator、UnetGenerator(skip-layer)。具体的网络结构细节可以看代码

    Discriminator

    Discriminator会判断input中的每个N x N的patch是real还是fake,也就是所说的PatchGAN

    1.2 前向

    real_A过Generator得到fake_B

    1.3 反向

    先定义loss:

    • D的目的是区分real和fake,所以当输入(real_A, real_B)时,D的输出得分尽量高,当输入(real_A, fake_B)时,D的输出得分尽量低
    • G的目的是欺骗D,即当输入(real_A, fake_B)时,D的输出得分尽量高。另外,G的另外一个监督loss是让fake_B尽量接近real_B,即fake_B和real_B之间挂一个l1 loss

    反向传播的时候先固定G优化D,再固定D优化G,具体可以看代码

    1.4 Image pool

    Image pool存放一定数量的(real_A, fake_B)对,帮助D去记忆历史信息,而不仅仅是当前迭代的一对。

    2. pix2pixHD

    先前的的pix2pix生成的image分辨率为256 x 256,pix2pixHD将生成分辨率提高到了2048 x 1024。

    2.1 网络结构

    Coarse-to-fine Generator

    生成器由两部分组成:Global generator network和Local enhancer network,两个network均是先上采样后下采样的架构。

    • Global采用pix2pix中的ResnetGenerator
    • Local分辨率是Global的4x,Local部分先下采样得到的feature map和Global上采样的feature map融合,去做Local的上采样步骤,得到最终G的输出

    Local数量可以增加,每增加一个,分辨率x4,一般设为1个。

    Multi-scale Discriminator

    判断器由三个同样网络结构的子判别器组成,每个子判别器的input分别是三种递减的分辨率。

    具体网络结构细节可以看代码

    2.2 反向

    pix2pixHD修改了pix2pix的loss:

    • D的目的是区分real和fake,所以当输入(real_A, real_B)时,D的输出得分尽量高,当输入(real_A, fake_B)时,D的输出得分尽量低
    • G的目的是欺骗D,即当输入(real_A, fake_B)时,D的输出得分尽量高。另外,G生成fake_B经过D得到的N层feature map要和real_B的尽可能相似,这是feature matching loss。G生成fake_B经过VGG得到的N层feature map要和real_B的尽可能相似(l1 loss),这是perceptual loss

    反向传播的时候先固定D优化G,再固定G优化D,具体可以看代码

    2.3 数据

    代码显示input label是经过one-hot编码处理的。

    3. CycleGAN

    CycleGAN解决的是unpair image-to-image translation,可以理解为两个domain之间的transfer。

    3.1 网络结构

    两个Generator G_A和G_B,两个Discriminator D_A和D_B。

    Cycle的定义是这样的:real_A经过netG_A生成fake_B,fake_B再经过netG_B生成rec_A,这是其一。real_B经过netG_B生成fake_A,fake_A再经过netG_A生成rec_B,这是另外一个cycle。

    具体细节可以看代码

    3.2 反向

    Generator

    G_A要欺骗D_A,所以fake_B经过D_A后的得分要尽量高,另外重构的rec_B要和原先的real_B尽可能相似(l1 loss)。同理G_B要欺骗D_B,所以fake_A经过D_B后的得分要尽量高,重构的rec_A要和原先的real_A尽可能相似(l1 loss)。

    另外,CycleGAN还引入了Identity loss,当输入是real_B时,G_A生成的idt_A要和real_B保持identity(l1 loss),同理,输入是real_A时,G_B生成的idt_B要和real_A保持一致。

    Discriminator

    D_A的目的是区分real_B和fake_B,D_B的目的是区分real_A和fake_A。

    反向传播的时候先固定D_A、D_B优化G_A、G_B,再固定G_A、G_B优化D_A和D_B,具体可以看代码

    ]]>
    ECCV-18 COCO and PoseTrack Challenge https://7color94.github.io/2018/09/eccv-18-coco-posetrack/ 2018-09-10T00:00:00+00:00 Kai Su https://7color94.github.io/2018/09/eccv-18-coco-posetrack ECCV-18 COCO Keypoint Challenge

    coco keypoints leaderboard,浏览了前4-5名的方法:

    1.需要使用额外数据(+0.7 ~ 1.0 AP):AI Challenge。我们使用方法不对,用AI Challenge去pre-train再在COCO上finetune,指标没有提升,也不知道什么原因。

    2.需要集成更多的backbone:Resnet-101,Resnet-152,Resnext-152,Se-ResNeXt101,ResNet101-dilation等等。其他队集成了4-5个,我们集成了Resnet-101和152这两个。原因是我们太过于相信Large Batch,训练时使用了Sync-BN导致训练很慢,像SENet-154、Resnext-101都得很久才能训完,然后就放弃了。还有就是深的backbone没train好,还不知道什么原因。还有就是因为有49w的bounding box,当时集成2个backbone都跑了半天,集成>=4的backbone的时候肯定太耗时间了,主要就是拼机器数量了。

    3.需要集成更多的human detector:虽然没有像MegDet性能那么好的detector,我们后来想了下,应该可以通过集成多个human detector提升性能。

    4.Inference Trick:我们在inference的时候用了Flip,Rotation,还有[The Sea Monsters]提出的Multi-Scale inference,这个影响应该不会太大。

    所以这次拿了并列第5,经验不足,炼丹水平不足

    ECCV-18 PoseTrack Challenge

    PoseTrack ECCV 2018 Challenge,AP是第5,MOTA是第3

    这个比赛太依赖调参,要过滤很多False Positive,指标才能上去。

    而且主办方很多问题,训练数据迟迟不放,最后评测只按照最后一次提交算,很多人都不清楚。

    ]]>
    CNNs https://7color94.github.io/2018/09/cnns/ 2018-09-08T00:00:00+00:00 Kai Su https://7color94.github.io/2018/09/cnns 1.LeNet

    Gradient Based Learning Applied to Document Recognition,1998

    一般用于手写识别,paper中input为32x32x1,我实现时的mnist数据集好像是28x28x1,然后两层的conv(5x5,stride=1)、pooling(2x2,stride=2),之后fc、relu,最基本的cnn

    2.Alxnet

    ImageNet Classification with Deep Convolutional Neural Networks,NIPS 2012

    输入224x224x3,五层的conv、pooling、relu,为了防过拟合使用了dropout和data augmentation(1.extract 224x224 from 256x256,2.horizontal reflections,3.alter the intensities of the RGB channels),还有weight decay

    实验时一张卡放不下,把model拆成2份放到2张卡训练

    3.VGG

    Very Deep Convolutional Networks for Large-Scale Image Recognition,ICLR 2015

    使用3x3 conv构成两种sequence结构:VGG-16和VGG-19

    VGG的方法是一层一层地堆conv,继续增加深度会有训练困难、参数量增加等问题

    4.GoogleNet系列

    4.1 Inception v1

    Going Deeper with Convolutions,CVPR 2015

    设计一种较宽的Inception module,(a)naive版本:将1x1,3x3,5x5的conv和3x3的pooling,都stack在一起,一方面增加了网络的width,另一方面增加了网络对尺度的适应性. (主要是因为1x1, 3x3, 5x5, 3x3 pooling的作用都不一样,索性都stack在一起,让模型自己选)(b)降维版本:在计算量大的conv之前,先用1x1降维,减少计算量

    网络的组成:在低层的时候仍用传统的卷积方式(sequence结构),高层开始堆Inception module;中间loss监督防止梯度消失;使用global average pooling

    4.2 Inception v2

    Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift,arXiv 2015

    使用Batch normalization,将每层输入归一化到N(0,1)的高斯分布

    网络结构方面学习VGG用2个3x3代替一个5x5,参数了变少,但感受视野一样

    4.3 Inception v3

    Rethinking the inception architecture for computer vision,CVPR 2016

    卷积进一步分解,5x5用2个3x3卷积替换,7x7用3个3x3卷积替换,3x3卷积核可以进一步用1x3和3x1的卷积核组合来替换,7x7分解成两个一维的卷积(1x7,7x1),进一步减少计算量,好处,既可以加速计算(多余的计算能力可以用来加深网络),又可以将1个conv拆成2个conv,使得网络深度进一步增加,增加了网络的非线性

    4.4 Inception v4

    Inception-v4, Inception-ResNet and the Impact of Residual Connections on Learning,AAAI 2017

    探索residual connection对Inception module的影响:收敛加速,但是最终效果好像提升很少。paper中提出了一些residual和 non-residual Inception networks

    4.5 Xception

    Xception: Deep Learning with Depthwise Separable Convolutions,CVPR 2017

    Xception将分解的思想推到了极致:跨通道的相关性和空间相关性是完全可分离的,最好不要联合映射它们,先pointwise + relu再depthwise + relu(和mobilenet相反)

    5.ResNet

    Deep Residual Learning for Image Recognition,CVPR 2016 Best Paper

    提出Residual Learning,两种bottleneck:1.3x3xc,3x3xc ; 2.1x1x(c/4),3x3x(c/4),1x1xc

    Identity Mappings in Deep Residual Networks,ECCV 2016

    做实验探索residual bottlneck里面是conv,bn,relu还是bn,relu,conv,实验效果后者好,但是一般我还是用前者,TF和Pytorch放出来的Imagenet pretrained model基本都是前者

    6.DenseNet

    Densely Connected Convolutional Networks,CVPR 2017

    DenseNet将residual connection思想推到极致,每一层输出都直连到后面的所有层,可以更好地复用特征,每一层都比较浅,融合了来自前面所有层的所有特征,很容易训练。缺点是显存占用更大并且反向传播计算更复杂一点

    7.ResNeXt

    Aggregated Residual Transformations for Deep Neural Networks,CVPR 2017

    借鉴了Inception加宽的思想,使用分组卷积,所以计算量减少,bottleneck的维度可以适当增加,效果提升:1x1x(c/2),3x3x(c/2),1x1xc

    8.DPN

    Dual Path Networks,NIPS 2017

    把ResNeXt(feature re-usage)和DenseNet(new features exploration)合并

    9.WRN

    Wide Residual Networks,BMVC 2017

    把ResNet变宽:增加output channel的数量来使模型变得更wider,深度可以不用太深了

    10.SENet

    Squeeze-and-Excitation Networks,CVPR 2018

    Feature map的channel-wise attention

    11.NASNet

    Learning Transferable Architectures for Scalable Image Recognition,arXiv 2017

    Google的AutoML

    12.MobileNet

    12.1 MobileNet v1

    MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications,CVPR 2017

    用depth wise conv + point wise conv替代标准conv,减少计算量

    12.2 MobileNet v2

    MobileNetV2: Inverted Residuals and Linear Bottlenecks,arXiv 2018

    Paper在探索这样的问题:如何把residual bottleneck应用到mobile net v1?一种改进是通过先扩张在收缩的方式,让depth wise conv提取的特征更丰富些,还有就是在和indentify mapping元素相加时去掉了relu,因为paper做实验证明relu只适合用于维度多的feature map的激活

    13.ShuffleNet

    13.1 ShuffleNet v1

    ShuffleNet: An Extremely Efficient Convolutional Neural Network for Mobile Devices,CVPR 2018

    进一步用group conv + channel wise替代mobilenet中的point wise conv

    13.2 ShuffleNet v2

    ShuffleNet V2: Practical Guidelines for Efficient CNN Architecture Design,ECCV 2018

    Paper指出FLOPs并不能完全衡量模型速度,并给出4个实验结论:

    • 卷积层的输入和输出特征通道数相等时memory access cost最小,模型速度最快
    • 过多的group操作会增大MAC,从而使模型速度变慢
    • 模型中的分支数量越少,模型速度越快
    • element-wise操作所带来的时间消耗远比在FLOPs上的体现的数值要多,因此要尽可能减少element-wise操作

    然后根据上述4个规则重新设计了shuffle net v2结构

    14.Analysis

    An Analysis Of Deep Neural Network Models For Practical Applications

    从paper的Figure. 2可以看出,比较划算的是Inception、Resnet系列

    References

    ]]>
    理一理卷积神经网络中的前向和反向传播 https://7color94.github.io/2017/12/cnn-forward-and-backward/ 2017-12-21T00:00:00+00:00 Kai Su https://7color94.github.io/2017/12/cnn-forward-and-backward 通过阅读[1],我们能从数学角度清楚卷积神经网络的工作原理,i.e.,convolution layer、fully connected layer、max pooling layer的前向和反向传播公式。配合Github开源的代码[2],我们进一步清楚了各层前向和反向的实现细节。下面,我再简单地理一理。

    1 convolution layer

    1.1 卷积层的前向过程

    卷积的过程可以用复杂的内嵌循环粗暴地实现。Caffe作者在[5]中也提到,优化复杂的嵌套循环不是一件容易的事。为了在很短地时间内实现一个速度不错的卷积层,作者采用了一个trick:用im2col函数把所有需要和卷积核进行卷积操作的local patches摊平组织成一个二维大矩阵,并把卷积核也摊平组织成一个二维矩阵。这样,卷积的过程成功转换为GEMM(General Matrix Matrix Multiply)操作,并能用高性能的BLAS库去加速GEMM,如下图所示。

    假设第 \(l\) 层卷积层的输入为 \(x^l \in \mathbb{R}^{H^l{\times}W^l{\times}D^l{\times}N}\) ,卷积参数为 \(F \in \mathbb{R}^{H{\times}W{\times}D^l{\times}D}\) ,输出为 \(Y \in \mathbb{R}^{H^{l+1}{\times}W^{l+1}{\times}D{\times}N}\) , \(N\) 表示batch size。

    通过im2col函数可以将 \(x^l\) 摊平成大小为 \((HWD^{l}){\times}(H^{l+1}W^{l+1}N)\) 的二维矩阵x_col,卷积核 \(F\) 摊平成大小为 \(D{\times}(HWD^{l})\) 二维矩阵w_col。w_col.dot(x_col)出来后reshape成 \(H^{l+1}{\times}W^{l+1}{\times}D{\times}N\) 就是卷积后的输出 \(Y\),也即 \(x^{l+1}\) 。

    具体如何实现,可以阅读代码[2]中的函数conv_forward,其基于numpy的fancy indexing实现了im2col

    进一步分析,x_col矩阵的元素均来源于 \(x^l\) ,即x_col和 \(x^l\) 的元素之间存在映射关系。因为 \(x^l\) 中的某个元素可能在卷积核滑动的过程中参与了不止一次的卷积运算,所以 \(x^l\) 到 x_col 的元素映射是一对多的关系,x_col到 \(x^l\) 则是一对一的映射关系。

    1.2 卷积层的反向过程

    卷积层的反向过程需要计算两个信息:loss function \(z\) 对卷积参数 \(F\) 的导数 \(\frac{\partial z}{\partial F}\) 和 \(z\) 对输入 \(X\) 的导数 \(\frac{\partial z}{\partial X}\)。前者用于更新卷积层的卷积参数,后者作为error supervision information往前面的层传。

    根据[1]:\(\frac{\partial z}{\partial X}\) 的计算稍复杂些,它通过对 \({\frac{\partial z}{\partial Y}} F^T\) 矩阵进行某种操作得到,其中 \(\frac{\partial z}{\partial Y}\) 是 \(l+1\) 层传给当前 \(l\) 层的error supervision information。 \({\frac{\partial z}{\partial Y}} F^T\) 的大小和 x_col一致,所以之前讲的元素映射关系可以用到 \({\frac{\partial z}{\partial Y}} F^T\) 和 \(x^l\) 之间。由[1]的推导可知, \(\frac{\partial z}{\partial X}\) 矩阵某个元素的值等于其在 \({\frac{\partial z}{\partial Y}} F^T\) 矩阵中所有映射位置的元素值之和(一对多的映射关系,所以是所有映射元素之和)。

    [2]conv_backward通过numpy.add.at函数实现了这种相应映射元素之间的相加运算。

    2 fully connected layer

    全连接层完全可以当成卷积层去看待:如果全连接层的输入 \(x^l \in \mathbb{R}^{H^l{\times}W^l{\times}D^l}\) ,我们可以用大小为 \(H^l{\times}W^l{\times}D^l{\times}D\) 的卷积核模拟全连接。那么,全连接层的前向和后向完全可以复用conv_forwardconv_backward实现。

    3 max pooling layer

    3.1 max pooling层的前向过程

    max_pooling_forward思路和conv_forward类似。假设max pooling layer的输入为 \(x^l \in \mathbb{R}^{H^l{\times}W^l{\times}D^l{\times}N}\),pooling层超参数为 \(H{\times}W\),输出 \(Y \in \mathbb{R}^{H^{l+1}{\times}W^{l+1}{\times}D^l{\times}N}\)。

    将 \(x^l\) 摊平成大小为 \(HW{\times}H^{l+1}W^{l+1}DN\) 的二维矩阵 x_col,x_col的每一列表示需要进行max运算的一个local patch。摊平运算完全可以复用im2col函数。

    3.2 max pooling层的反向过程

    max pooling layer没有参数,所以只需要计算 \(\frac{\partial z}{\partial X}\) 。根据[1], \(\frac{\partial z}{\partial vec(x^l)}\) 由 \(\frac{\partial z}{\partial vec(y)}\) 进行元素映射得到。具体过程阅读代码[2]中的max_pooling_backward

    4 结束语

    其他比如relu layer、softmax loss layer也都比较简单。

    文章理得也不算清楚,所以还是建议直接阅读[1],并配合着[2]一起。读代码过程中善于利用pdb调试工具,这样理解得更清晰些。

    5 参考资料

    ]]>
    聊一聊Bitmap https://7color94.github.io/2017/12/bitmap/ 2017-12-16T00:00:00+00:00 Kai Su https://7color94.github.io/2017/12/bitmap 什么是Bitmap

    Bitmap,也叫做bit array,顾名思义,是一种以bit为单位进行信息存储的数组。但Bitmap存储的“信息”又有些特殊,这是因为一个bit只能有两种取值0、1,所以bitmap通常用来存储这种能编码为0、1的状态信息:absent/present,dark/light,invalid/valid等。

    我们就拿absent/present来举例,如果某个bit的值是1,那就表示该bit是存在的。所以我们完全可以把Bitmap当成“集合”(准确地说是带hash功能的set)去使用:集合中的元素就由bit数组中所有值为1的元素所在的位置组成。反过来看,Bitmap就是把某些元素(通常是整数)转化为为0、1的一种映射。

    Bitmap的诞生是为了解决什么问题

    Bitmap最大的优势就是采用了bit为单位存储信息,极大地节约了存储空间。举个例子来说,如果我们要存储[4,2,1,3]四个整数,在c++中,我们通常会这样做:int[] arr = {4, 3, 2, 1};。这样的做法会产生一个占用4 * 4 = 16个字节的数组arr。

    但如果我们用Bitmap呢?比如给定一个长度是10bit的内存空间,我们现在插入4,2,1,3。我们可以将长度为10的bitmap的每一个bit位分别对应着0到9这10个整数。一开始bitmap所有位都是0(对应着图中的蓝色),之后每插入一个整数,该整数对应的存储位置的bit置为1(对应图中的红色)。

    这样,我们可以用10bit的内存空间去表示{4, 3, 2, 1}四个整数,相比起之前的20个字节,极大地节省了存储空间。如果整数个数更多,空间节省也就越明显。

    如何实现一个简单的Bitmap

    我们可以用整型数组表示bit数组。如果一个整数32位,那么表示N个连续的整数,bit数组的长度只需要N/32。bitmap通常有三种操作:set(置位),clear(清位),get(读位)。

    // http://blog.csdn.net/QIBAOYUAN/article/details/5914662
    
    #define WORD 32
    #define SHIFT 5 ////移动5个位,左移则相当于乘以32,右移相当于除以32取整
    #define MASK 0x1F //16进制下的31
    #define N 10000000
    int bitmap[1 + N / WORD];
    /* 
     * 置位函数——用"|"操作符,i&MASK相当于mod操作
     * m mod n 运算,当n = 2的X次幂的时候,m mod n = m&(n-1)
     */
    void set(int i) {
        bitmap[i >> SHIFT] |= (1 << (i & MASK));
    }
    /* 清除位操作,用&~操作符 */
    void clear(int i) {
        bitmap[i >> SHIFT] &= ~(1 << (i & MASK));
    }
    /* 测试位操作用&操作符 */
    int get(int i) {
        return bitmap[i >> SHIFT] & (1 << (i & MASK));
    }
    

    Bitmap存在的问题

    同样的空间大小,bit数组存储的元素个数虽然比整型数组多,但是由于bit数组能存储的元素最大值一般等于bit数组元素个数。所以,如果面对稀疏的整数存储任务,bit数组会开辟出很大的存储空间,而且会造成严重的空间碎片。

    但这也不是问题,通过引入RLW:Running Length Word,可以避免空间的浪费。具体可以阅读Bitmap算法(进阶篇)

    Bitmap的常见应用

    • 给40亿个不重复的unsigned int整数,未排序,然后再给一个数,如何快速判断这个数是否在40亿个数中?

    如果40亿个数直接放在内存中,需要大约16GB内存,用bitmap不超过512MB。

    • 在2.5亿个整数中找出不重复的整数。注:内存不足以容纳2.5亿个整数

    不重复包含了三种状态:00表示不存在,01表示出现一次,10表示多次,11无意义。所以和之前不同,这里我们需要给每个整数分配2个bit,也就是用2-Bitmap去实现。

    #define WORD 16
    #define SHIFT 4 ////移动4个位,左移则相当于乘以16,右移相当于除以16取整
    #define MASK 0xF //16进制下的15
    #define MAX_N 10000000
    
    /* 
     * 置位函数——用"|"操作符,i&MASK相当于mod操作
     * m mod n 运算,当n = 2的X次幂的时候,m mod n = m&(n-1)
     */
    void bitmap_set(int i, int* bitmap, unsigned int count) {
        int j = i & MASK;
        // unsigned int t = (bitmap[i >> SHIFT] & ~((0x3 << (2 * j))&0xffffffff)) | (((count % 4) << (2 * j)) & 0xffffffff);
        unsigned int t = (bitmap[i >> SHIFT] & ~(0x3 << (2 * j))) | ((count % 4) << (2 * j));
        bitmap[i >> SHIFT] = t;
    }
    
    int bitmap_get(int i, int* bitmap) {
        int j = i & MASK;
        // 0x3 十六进制表示00 00 00 11
        unsigned int count = (bitmap[i >> SHIFT] & (0x3 << (2 * j))) >> (2 * j);
        return count;
    }
    
    void add_count(int i, int* bitmap) {
        unsigned int count = bitmap_get(i, bitmap);
        if (count >= 2) return;
        bitmap_set(i, bitmap, count+1);
    }
    
    int main() {
        int* bitmap = NULL;
        freopen("in.txt", "r", stdin);
        bitmap = (int*)malloc(sizeof(int)*(1+MAX_N/WORD));
        memset(bitmap, 0, sizeof(int)*(1+MAX_N/WORD));
        int num;
        while (scanf("%d", &num) != EOF) add_count(num, bitmap);
        for (int i = 0; i < 1000000; i++) {
            if (bitmap_get(i, bitmap) == 1) printf("%d\n", i);
        }
        return 0;
    }
    

    如果考虑负整数的话,再单独开辟出一个2-Bitmap为负整数服务就可以。

    参考资料

    ]]>
    《算法》第4版相关笔记 https://7color94.github.io/2017/09/reading-algorithms-4th/ 2017-09-23T00:00:00+00:00 Kai Su https://7color94.github.io/2017/09/reading-algorithms-4th 实验室今天停电,翻阅起手边的《算法》4th,发现很多知识点遗忘了,决定重新温习一次,并把相关代码片段敲下来,下次就可以直接读这篇文章了。

    第1章 基础

    • 二分查找
    int binary_search(vector<int> arr, int key) {
        int lo = 0, hi = arr.size() - 1;
        while (lo <= hi) {
            int mid = lo + (hi - lo) / 2;
            if (arr[mid] > key) hi = mid - 1;
            else if (arr[mid] < key) lo = mid + 1;
            else return mid;
        }
        return -1;
    }
    

    lower_bound:寻找有序数组中第一个大于等于target的元素索引

    int bs_lower_bound(vector<int>& nums, int target) {
        int lo = 0, high = nums.size() - 1, mid;
        while (lo <= high) {
            mid = lo + (high - lo) / 2;
            if (nums[mid] < target) lo = mid + 1;
            else high = mid - 1;
        }
        return lo;
    }
    

    upper_bound:寻找有序数组中第一个大于target的元素索引

    int bs_upper_bound(vector<int>& nums, int target) {
        int lo = 0, high = nums.size() - 1, mid;
        while (lo <= high) {
            mid = lo + (high - lo) / 2;
            if (nums[mid] <= target) lo = mid + 1;
            else high = mid - 1;
        }
        return lo;
    }
    
    • 并查集

    并查集并不难,union操作用于合并两个连通分量,find操作用来返回当前节点所在连通分析的标识id。书中给出了加权的并查集算法,来保证合并两个连通分量之后的树尽量平衡些。

    int pre[1000]; // 保存节点p的父亲节点编号
    int sz[1000];  // 保存节点p的子孙节点数目
    
    int find(int p) {
        while (p != pre[p]) p = pre[p];
        return p;
    }
    
    void union(int p, int q) {
        int i = find(p);
        int j = find(q);
        if (i == j) return;
        // 将节点数目少的树连接到数目多的树
        if (sz[i] < sz[j]) { pre[i] = j; sz[j] += sz[i]; }
        else { pre[j] = i; sz[i] += sz[j]; }
    }
    

    当然,也可以在find时路径压缩,不使用sz[ ]数组,达到同样的效果

    int find(int p) {
        while (pre[p] != p) {
            pre[p] = pre[pre[p]];
            p = pre[p];
        }
        return p;
    }
    
    void union(int p, int q) {
        int i = find(p);
        int j = find(q);
        if (i == j) return;
        else pre[i] = j;
    }
    

    第2章 排序

    • 归并排序

    归并排序核心的操作单元是:归并,即将两个有序的数组归并成一个更大的有序数组。那么,归并排序的思想是递归地将数组分成两半分别排序,然后将排好序的两半子数组归并起来。

    int arr[1000];
    int aux[1000]; // 归并所需的辅助数组
    
    // 将a[lo..mid] 和 a[mid+1..hi] 归并
    void merge(int lo, int mid, int hi) {
        int i = lo, j = mid + 1;
        for (int k = lo; k <= hi; k++) aux[k] = a[k];
        while (int k = lo; k <= hi; k++) {
            if (i > mid) a[k++] = aux[j++];
            else if (j > hi) a[k++] = aux[i++];
            else if (aux[i] < aux[j]) a[k++] = aux[i++];
            else a[k++] = aux[j++];
        }
    }
    
    void merge_sort(int lo, int hi) {
        if (lo >= hi) return;
        int mid = lo + (hi - lo) / 2;
        merge_sort(lo, mid);
        merge_sort(mid + 1, hi);
        merge(lo, mid, hi);
    }
    
    void main() {
        merge_sort(0, a.length - 1);
    }
    

    上面介绍的是自顶向下的归并排序方法,将大数组的排序问题递归地分割成两个小数组的排序问题,然后用所有小问题的答案去归并出大问题的答案。实现归并排序的另一种思路是自底向上的方式,先归并微型数组,然后成对地归并得到排好序的子数组,如此反复直到整个数组归并在一起。

    void merge_sort() {
        int N = a.length;
        for (int sz = 1; sz < N; sz += sz) {
            for (int lo = 0; lo + sz < N; lo += sz + sz) {
                merge(lo, lo + sz - 1, min(lo + sz + sz - 1, N - 1));
            }
        }
    }
    
    • 快速排序

    快速排序也是一种基于分治思想的排序算法,它将一个大数组的排序任务划分为两个独立的子数组的排序任务。快排在大数组中选择一个元素作为基准,基于基准元素对大数组进行划分,使得数组左半部分元素均小于基准,数组右半部分元素均大于基准,之后递归地对左/右两部分单独进行排序。快排不需要归并的过程,因为子数组排序好,大数组自然也就是有序的数组了。

    int a[1000];
    
    // 划分算法
    int partition(int lo, int hi) {
        int i = lo, j = hi + 1;
        int v = a[lo]; // 基准元素v
        while (true) {
            while (a[++i] < v) {
                if (i == hi) break;
            }
            while (a[--j] > v) {
                if (j == lo) break;
            }
            if (i >= j) break;
            swap(i, j);
        }
        swap(lo, j);
        return j;
    }
    
    void quick_sort(int lo, int hi) {
        if (lo >= hi) return;
        int j = partition(lo, hi);
        quick_sort(lo, j - 1);
        quick_sort(j + 1, hi);
    }
    
    void main() {
        quick_sort(0, a.length - 1);
    }
    
    • 优先队列:堆

    这里我们就只讲最简单的二叉大顶堆:在二叉堆的数组中,每个元素都大于等于它的两个子节点。堆的核心操作是:上浮和下沉。如果堆的有序状态因为某个节点比其父节点更大,那么需要通过上浮来交换它和它的父节点进行修复。如果堆的有序状态因为某个节点比它的两个子节点中之一更小,那么需要通过下沉来交换它和它的子节点中较大者来修复。

    // 上浮
    void swim(int k) {
        while (k > 1 && a[k] > a[k / 2]) {
            swap(k, k / 2);
            k = k / 2;
        }
    }
    
    // 下沉
    void sink(int k) {
        while (2 * k <= N) {
            int j = 2 * k;
            if (j < N && a[j] < a[j + 1]) j++;
            if (a[k] >= a[j]) break;
            swap(k, j);
            k = j;
        }
    }
    

    第3章 查找

    • 2-3查找树

    这个之前有专门整理过:https://7color94.github.io/blog/2017/07/2-3-search-tree-notes/

    • 红黑树

    之后有时间再整理这部分。

    • 散列表(hash表)

    暂时先不做整理。

    第4章 图

    • 图的深搜和广搜

    不多说。

    • 最小生成树

    不多说,知名的两种算法有:primkruscal

    • 最短路径

    这部分很有意思,常用的最短路径算法有dijkstra、bellman-ford、spfa(bellman-ford的队列改进版)、floyd。

    1.dijkstra

    dijkstra采用了和prime类似的方法来计算最短路径树。首先将distTo[s]初始化为0,distTo[ ]中其他元素初始化为正无穷。然后将distTo[ ]中最小的非树顶点加入树中,同时用此顶点去松弛其余顶点的distTo[ ]值,直到所有顶点都在树中或所有有非树顶点的distTo[ ]值均为无穷大。其中,从distTo[ ]中找出元素最小的非树顶点可以用优先级队列来管理。

    dijstra可以用于无向图,也可以用于有向图,但是不能用于含有负权边的图,可以读一读stackoverflow给出的例子:https://stackoverflow.com/questions/6799172/negative-weights-using-dijkstras-algorithm

    2.bellman-ford

    bellman-ford算法流程很简单,重复以任意顺序对图的每条边松弛V轮。bellman-ford可以用于无向图/有向图,也可以用于含有负权重边的图,在算法的最后一步,还有判断图中是否存在负权环的功能。

    3.spfa

    在bellman-ford每一轮松弛中,只有distTo[ ]值发生变化的顶点指出的边才能改变其他顶点的distTo[ ]值,为了记录这些点,spfa利用队列记录。

    三种算法代码实现:

    int N, M, S, T;
    int edgenum;
    int first[100010];
    int dis[100010];
    int vis[100010];
    int cnt[100010];
    
    struct edge {
        int u, v;
        int w;
        int next;
    } e[3000000];
    
    struct CompareByFirst {
        bool operator()(pair<int, int> const &a, pair<int, int> const &b) {
            return a.first < b.first;
        }
    };
    
    void addEdge(int u, int v, int w) {
        e[edgenum].u = u;
        e[edgenum].v = v;
        e[edgenum].w = w;
        e[edgenum].next = first[u];
        first[u] = edgenum++;
    
        e[edgenum].u = v;
        e[edgenum].v = u;
        e[edgenum].w = w; 
        e[edgenum].next = first[v];
        first[v] = edgenum++;
    }
    
    void dijkstra() {
        priority_queue< pair<int, int>, vector< pair<int, int> >, CompareByFirst > Q;
        for (int i = 1; i <= N; i++) dis[i] = (i == S ? 0 : INT_MAX);
        Q.push(make_pair(0, S));
        while (!Q.empty()) {
            pair<int, int> t = Q.top();
            Q.pop();
            int u = t.second;
            if (t.first != dis[u]) continue;
            for (int k = first[u]; k != -1; k = e[k].next) {
                int v = e[k].v;
                if (dis[v] > dis[u] + e[k].w) {
                    dis[v] = dis[u] + e[k].w;
                    Q.push(make_pair(dis[v], v));
                }
            }
        }
    }
    
    bool bellman_ford() {
        for (int i = 1; i <= N; i++) dis[i] = (i == S ? 0 : INT_MAX);
        for (int i = 1; i <= N; i++) {
            bool changed = false;
            for (int j = 0; j < edgenum; j++) {
                if (dis[e[j].v] > dis[e[j].u] + e[j].w && dis[e[j].u] != INT_MAX) {
                    dis[e[j].v] = dis[e[j].u] + e[j].w;
                    changed = true;
                }
            }
            if (!changed) return true;
            if (i == N && changed) return false;
        }
        return false;
    }
    
    bool spfa() {
        queue<int> Q;
        memset(vis, 0, sizeof(vis));
        memset(cnt, 0, sizeof(cnt));
        for (int i = 1; i <= N; i++) dis[i] = (i == S ? 0 : INT_MAX);
        vis[S] = 1;
        Q.push(S);
        while (!Q.empty()) {
            int u = Q.front();
            vis[u] = 0;
            Q.pop();
            for (int k = first[u]; k != -1; k = e[k].next) {
                int v = e[k].v;
                if (dis[v] > dis[u] + e[k].w) {
                    dis[v] = dis[u] + e[k].w;
                    if (0 == vis[v]) {
                        Q.push(v);
                        vis[v] = 1;
                        if (++cnt[v] > N) return false;
                    }
                }
            }
        }
        return true;
    }
    
    int main() {
        freopen("in.txt", "r", stdin);
        scanf("%d %d %d %d", &N, &M, &S, &T);
        edgenum = 0;
        memset(first, -1, sizeof(first));
        int u, v, w;
        for (int i = 1; i <= M; i++) {
            scanf("%d %d %d", &u, &v, &w);
            addEdge(u, v, w);
        }
        // dijkstra();
        // bellman_ford();
        spfa();
        printf("%d\n", dis[T]);
        return 0;
    }
    

    4.floyd

    floyd算法用于计算一张图的全源最短路径算法,可用于负权图。

    int N, M;
    int e[110][110];
    
    int main() {
        freopen("in.txt", "r", stdin);
        scanf("%d %d", &N, &M);
        int u, v, w;
        for (int i = 1; i <= N; i++) {
            for (int j = 1; j <= N; j++) {
                if (i == j) e[i][j] = 0;
                else e[i][j] = 100000;
            }
        }
        for (int i = 1; i <= M; i++) {
            scanf("%d %d %d", &u, &v, &w);
            e[u][v] = e[v][u] = min(w, e[u][v]);
        }
        
        for (int k = 1; k <= N; k++) {
            for (int i = 1; i <= N; i++) {
                for (int j = 1; j <= N; j++) {
                    if (e[i][j] > e[i][k] + e[k][j]) e[i][j] = e[i][k] + e[k][j];
                }
            }
        }
    
        for (int i = 1; i <= N; i++) {
            for (int j = 1; j < N; j++) printf("%d ", e[i][j]);
            printf("%d\n", e[i][N]);
        }
        
        return 0;
    }
    

    第5章 字符串

    • 字符串排序

    待整理

    • 单词查找树

    最基本的Trie树实现:

    #define CHAR_RANGE 26
    
    struct TrieNode {
        bool flag;
        int count;
        TrieNode *childs[CHAR_RANGE];
        TrieNode *parent;
        TrieNode() :parent(NULL), flag(false), count(0) {
            for (int i = 0; i < CHAR_RANGE; i++) childs[i] = NULL;
        }
    };
    
    void insertWord(TrieNode *p, char *word) {
        if ((*word) == '\0') {
            p->flag = true;
            return;
        }
        int index = word[0] - 'a';
        if (p->childs[index] == NULL) p->childs[index] = new TrieNode();
        if (p->childs[index]->parent == NULL) p->childs[index]->parent = p;
        p->childs[index]->count++;
        insertWord(p->childs[index], word + 1);
    }
    
    int findPrefix(TrieNode *p, char *word) {
        if ((*word) == '\0') {
            return p->count;
        }
        int index = word[0] - 'a';
        if (p->childs[index] == NULL) return 0;
        return findPrefix(p->childs[index], word + 1);
    }
    
    • 字符串查找

    其中KMP算法之前有整理过:https://7color94.github.io/blog/2017/01/kmp/

    其余字符串查找算法有待整理

    • 正则表达式和数据压缩

    这部分待整理

    ]]>
    笔记:平衡查找树之2-3查找树 https://7color94.github.io/2017/07/2-3-search-tree-notes/ 2017-07-19T00:00:00+00:00 Kai Su https://7color94.github.io/2017/07/2-3-search-tree-notes 如果节点插入序列有序,构造出的二分查找树就只有左(或者右)子树,此时查找会退化为线性查找。为了使原本“瘦高”的树结构变得“矮胖”,在节点的插入、删除中保证树的结构尽量平衡些,就能保证所有查找都能在O(logn)左右结束。

    平衡查找树常用算法有多种,下面通过阅读书籍和资料记录相关内容。

    一. 2-3 查找树

    1.1 定义

    2-3 查找树是理解平衡查找树的基础。在标准二叉查找树中,树中的节点称为2-结点(含有一个键和两条孩子链接)。2-3 查找树在2-节点基础上又引入3-结点(含有两个键和三条链接)。

    • 2-结点:左链接指向的2-3树中的键都小于该结点,右链接指向的2-3树中的键都大于该结点。
    • 3-结点:左链接指向的2-3树中的键都小于该结点,中链接指向的2-3树中的键都位于该结点的两个键之间,右链接指向的2-3树种的键都大于该结点。

    一颗完美的2-3查找树中的所有空链接到根结点的距离都应该是相同的,而且在动态插入过程中,这条性质永远满足。

    1.2 查找

    这个比较简单。

    1.3 插入

    为了保持动态插入过程中保持完美平衡,所以在2-3查找树的插入算法稍微复杂些,需要分类来讨论。

    • 向2-结点中插入新键

    这是插入中最简单的情况,只需要将新插入的键保存在2-结点中,将2-结点替换为3-结点即可。

    向3-结点插入新键,则要麻烦一些。

    • 向一颗只含有一个3-结点的树中插入新键

    我们临时先将新键存入3-结点,使其变为4-结点,然后将4-结点的中键向上浮动,转为一颗由3个2-结点组成的2-3树。

    这个例子很简单,但说明了2-3树是如何生长的,它遵循一种由下向上的生成方式。之前已经说过,2-3树种所有空链接到根结点的距离都应该是相同的,这个距离只有在根节点被分解为3个2-节点时,距离才会加1,对应着树长高了。

    • 向一个父结点为2-结点的3-结点插入新键

    遵循刚才的原则,将新键暂存入3-结点构造出4-结点,然后将其分解。但此时我们不会为中键创建一个新结点,而且将其移动到父结点,使得父结点成为3-结点。

    观察发现,这次转换并不影响2-3树的完美平衡,而且树仍然是有序的。插入后所有空连接到根结点的距离仍然相同。

    • 向一个父结点为3-结点的3-结点中插入新键

    还是和刚刚一样,构造临时的4-结点然后分解它,将它的中键插入父结点。父结点成为了4-结点,需要继续分解这个父结点将它的中键插入到它的父结点,直到遇到一个2-结点将其替换为一个不需要继续分解的3-结点或者是达到3-结点的根。

    • 分解根结点

    如果插入结点到根结点的路径上全是3-结点,我们的根结点最终变成一个临时的4-结点,之后将根4-结点分解为3个2-结点,树的高度相应加1。这次变换仍然保持了树的完美平衡性。

    • 局部变换

    2-3树的插入算法的根本在于上面的这些变换都是局部的,除了相关的结点和链接之外不必修改或者检查树的其他部分。

    • 全局性质。

    刚刚我们也说了,局部变换并不会影响树的全局有序性和平衡性:任何空链接到根结点的路径长度都是相等的。只有在根节点被分解为3个2-结点时,所有空链接到根结点的路径长度才会加1。

    命题:在一颗大小为N的2-3树种,查找和插入操作访问的结点必然不超过logN个。

    二. 总结

    尽管我们可以用不同的数据类型表示2-结点和3-结点,但是离我们真正的实现2-3树还有一段距离,2-3树原理清晰但代码操作并不方便,需要处理的情况太多。平衡一棵树的目的是为了消除最坏情况,同时我们也希望所需的代码能够越少越好。

    下面,我们将学习一种名为红黑树的简单数据结构来表达并实现2-3树。


    References

    • 《算法 4》
    ]]>