忍野忍

忍野忍 查看完整档案

广州编辑暨南大学  |  软件工程 编辑Dungeon  |  Lord 编辑 oshinonya.com 编辑
编辑

自分の世界を変えるのは自分。 —— モルガナ

jo厨,东方厨,物语厨,月厨,单线程dd
个人博客:https://oshinonya.com

实习中,咕咕咕

个人动态

忍野忍 发布了文章 · 4月2日

Nuxt 项目部署在 Nginx 后,请求传递和资源映射问题的解决思路

原文:《关于 Nginx 和 Nuxt 结合这一事》

最近终于把博客用 Nuxt 重构完了,作为一个跟 nuxt 共处了一个月的猛男,我可以说这框架可以的:SSR解决方案非常巧妙,难度也不大,熟记其生命周期后靠官方文档很快就能上手。如果你有 SEO 的需要完全可以尝试一波

但是在前后端对接和最后部署的时候 nuxt 还是比较多坑的,而且是挺大的那种,这里我列出两个我觉得比较棘手的问题

  • nuxt 默认把 URL /XX 映射到目录 /static/XX,并且无法修改
  • 部分请求是客户端和服务端共享的,如何在封装一个请求方法,使得

    • 在客户端时,请求携带请求头直接发往后端
    • 在服务端时(如首屏渲染),把来自客户端的请求头或 IP 等参数传递给后端

我称他们为资源映射请求传递问题,不知到其他人有没有因此受过困扰,反正我是被这东西卡了挺久的。在此记录这两个问题的解决方案,希望能帮到有需要的人

注:为了简化描述,下文的 Nuxt 服务端简称“服务端”,php 后端简称“后端”,分别是服务器上的两个服务

请求传递问题

要解决这个问题,最好先认真考虑一下浏览器发起的请求是如何在服务器里反复横跳的。假设 nuxt 服务端工作在8080端口,后端(这里 php 为例)工作在9000端口,理想情况下(先不考虑静态资源的请求)如下图,颜色相同的线条为一对请求和响应。

请求流

对应到代码,图中的③和⑤是可能请求到同一个接口的(如asyncData中的请求),两种请求的发起点一个是客户端(本身就带着正确的请求头和IP),另一个是 nuxt 服务端(请求头基本为空,IP为本机IP)。前者符合需求好的,后者问题就大了,后端丢失了客户端的信息。
所以这里的重点是“传递原请求”,即把③的请求头和 IP 更换为②的请求头和 IP

请求头传递

针对请求头的传递,可以利用 context.req 的特性:当在客户端运行时,requndefine;在服务端运行时 req 为nodejs 的 Request 对象,包含请求②的信息,req.headers 就是其请求头。
有了这个特性就可以根据 req 是否为 undefine 判断是否在服务端,只有在服务端时才需要用原请求头覆盖现请求头。下面是实现代码,丢到 plugins 中注册注入至双端即可

import axios from 'axios';
import qs from 'qs';
axios.defaults.timeout = 10000;
axios.defaults.withCredentials = true;

export default ({app,req},inject)=>{
  //封装get方法
  inject('fetch',(url,params={},req=null)=>{
    return new Promise((resolve,reject)=>{
      axios.get(url,{
        params:params,
        headers:req?req.headers:{}
      }).then(response=>{
        resolve(response)
      }).catch(err=>{
        reject(err)
      })
    })
  });
  //封装post方法
  inject('post',(url,data={},req=null)=>{
    return new Promise((resolve,reject)=>{
      axios.post(url,qs.stringify(data),{
        headers:{
          'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
          ...(req?req.headers:{})
        }
      }).then(response=>{
          resolve(response)
        }).catch(err=>{
        reject(err)
      })
    })
  });
}

使用的时候,只要涉及服务端的请求必须要传 req 进去,如 this.$fetch('114514.php',{},req);而只涉及客户端的请求可以省略 req 参数如 this.$post('114514.php',{})

IP 传递

请求的IP原本并不存储于请求头中,而 php 获取的 IP 只认TCP连接的 IP,因此为了传递 IP 需要开辟一个新字段去存储,让后端从这个新字段获取客户端的 IP。Nginx 有现成的 proxy_set_header 指令帮我们在反向代理的时候修改请求头。如果有更多需要,我们甚至可以 set 多几个

upstream nuxtserver{
    server 127.0.0.1:8080;
    keepalive 64;
}
server{
    #...
    location / {
        proxy_redirect off;
        proxy_set_header Host $host;
        # 把客户端的IP的放在 X-Real-IP 字段中
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 1m;
        proxy_connect_timeout 1m;
        proxy_pass http://nuxtserver;
    }
    #...
}

这些新增的字段最终会出现在客户端的 req.headers 中,再使用上面请求头传递的方法,IP 等信息就能传递到后端(如php就能通过$_SERVER['HTTP_X_REAL_IP']获取到设置的IP

映射问题

URL 指向通常有三类:页面、静态资源,后端接口
nuxt 最大的问题是会把除页面外的类型指向 /XX 映射到目录 /static/XX
最简单的解决方法就是把资源文件,接口文件全丢进 static 文件夹里,但这会显得非常违和,哪有接口路径是以 static 开头的。

目前能找到比较有效的方法是在 nginx 的配置文件中写规则进行拦截,一旦确定URL是请求资源的,全部由 nginx 映射到对应的目录或者禁止访问,剩下的访问页面的 URL 就可以放心交给 nuxt

server{
    #...
    # 禁止文件(夹)
    location ^~ /node_modules {
        return 404;
    }
    # 静态资源
    location ~ ^/(downloads|music|site|static|tmp|uploads)/ {
        root html;
        expires 7d;
    }
    # PHP CGI
    location ~ \.php$ {
        root           html;
        fastcgi_pass   127.0.0.1:9000;
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include        fastcgi_params;
    }
    # 最后交由代理
    location / {
        proxy_redirect off;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 1m;
        proxy_connect_timeout 1m;
        proxy_pass http://nuxtserver;
    }
    #...
}

_
记录到此为止,如果有 dalao 有优化建议或者新的解决思路,欢迎提出讨论

查看原文

赞 0 收藏 0 评论 0

忍野忍 发布了文章 · 2月23日

「Vue」 手撸一个图片懒加载/预加载指令

前言

ES6 注意!!

最近在优化个人博客前端,翻看到了自己图片懒加载/预加载的远古代码(通过watch监听实现的),虽说实际效果勉强还行,但总觉得不够 “Vue”,功能上也有所不足。
考虑到现有的 vue-lazyload 插件将懒加载指令化了,于是我想能不能自己也写一个。搜索一波后发现是可行的,并且还挺简单的,这篇文章给了我很多参考,在此之上我又优化并增添了额外功能,最后也不超一百行代码,奥利给,造TA就完了

问:为什么不用现成的插件?
答:代码还是自己撸的爽,vue-lazyload 据说不支持分别指定占位图,而我实现了

图片懒加载

这里给小白的简单介绍一下,熟知此概念的大佬可以跳过,还是看不懂的小白转百度或谷歌

图片的懒加载是指对于首屏加载后未在视野的图片容器 / 视野之外新增的图片容器,先给容器一个缩略图或者默认占位图(size 很小);待容器进入或即将进入视野时在后台下载原图,原图准备好后才替换。效果如下,左右分别是懒加载完成前后的效果

懒加载效果图

这样用户在原图到来之前至少还有缩略图看看,一定程度减缓其不耐烦的心情从而优化用户体验;同时按需加载的特性能够节省流量,这对服务器和用户都是一件好事

所以对于小水管服务器和面对网络拥堵时候,懒加载就显得特别有用

实现关键

首先给数组原型加两个自定义方法,后面会用上,这段代码放在指令调用前即可

// 移除数组指定的元素
if (!Array.prototype.remove){
    Array.prototype.remove = function (item) {
        if (!this.length) return;
        let index = this.indexOf(item);
        if (index > -1) {
            this.splice(index,1);
            return this;
        }
    }
}
// 推入数组当且仅当该数组没有该元素(针对string)
if (!Array.prototype.pushIfNew){
    Array.prototype.pushIfNew = function (...item) {
        for (let i of item)
            if (this.indexOf(i)===-1)
                this.push(i);
        return this
    }
}

位置判断

懒加载关键的之一就是判断该图片容器是否在视野之内,这里要用到节点的 getBoundingClientRect() 方法,返回值是 DOMRect 对象,包含该元素块边框相对于视野左上角的距离,各属性如下

rect.png

如果视野高度为 screenHeight,结合以上属性,我们很容易判断元素是否在视野之内

let top  = el.getBoundingClientRect().top;
let screenHeight = window.innerHeight || document.documentElement.clientHeight;
if (top < screenHeight + 50 && top > -50){
// 不一定要严格地进入视野,可以适当“扩大”视野,能够判断“即将进入”的情况,更符合实际要求
}

后台加载

懒加载关键的之二是后台加载原图,实现起来很简单,当 img 元素的 src 属性被赋值时,加载就会发生,加载成功后执行其 onload 方法,失败时执行 onerror 方法。利用这个特性,当目标进入视野时,可以创建一个临时 img(不用插入document),定义其加载成功和失败的行为,然后给他的 src 赋值即可

let img = new Image();
img.onload = ()=>{
// 成功后替换缩略图
//...
}
img.onerror = ()=>{
// 失败后可以显示 error 图片
// 或什么都不做维持之前的缩略图
// ...
}
img.src = 'original imgSrc'

监听追踪

关键之三就是对目标的监听和追踪了,可以定义两个数组,listenList 存放追踪目标,imgCacheList 存放已加载(已缓存)图片的 src。

当一个 img 元素被新插入文档后,以下操作按序三选一

  • 如果其原图在 imgCacheList 中,直接 src 赋值为原图
  • 如果该 img 在视野之内,开始触发后台加载,加载成功后其 src 加入 imgCacheList
  • 如果该 img 在视野之外,将其加入 listenList 中进行监听

对于 listenList, 我们会绑定全局滚动事件,窗口一滚动就对 listenList 中的所有目标进行位置判断

  • 如果在视野内,触发后台加载,加载成功后其 src 加入 imgCacheList,同时将目标从 listenList 中移除
  • 如果在视野外,什么都不干

当然直接绑定滚动时间会超频繁的触发函数,这里可以对函数做防抖处理

被监听的 img 如果被移除(如页面跳转),listenList 中相应的监听目标要移除

指令注册

因为要对 listenListimgCacheList 进行共享和管理,所以不能简单地进行全局指令注册 Vue.directive(),而是要在其之外开辟一个区域存放这些共享的数据,这就要以插件形式进行指令的注册了

同时指令有多个钩子函数,考虑到 img 要插入文档后才能通过 getBoundingClientRect() 获取位置信息,这里选择 inserted 钩子函数

inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)

虽然跟要求有偏差,但这是最接近要求的钩子函数了,实际使用上又没什么问题,就选他吧。

/*-------lazyload.js-------*/
export default (Vue,options)=>{
    let listenList = [];
    let imgCacheList = [];
    //.....
    Vue.directive('lazyload',{
        inserted:(el,binding)=>{
        
        },
        unbind:(el)=>{
        
        }
    }
}
/*-------main.js-------*/
import LazyLoad from './lazyload';
Vue.use(LazyLoad)

使用

了解了这几个关键点我想最终实现也应该有个大概了,剩下一些细节以注释给出,详看下面的完整代码

// ----lazyload.js----
// 防抖
function throttle(func, wait) {
    let context, args;
    let previous = 0;
    return function() {
        let now = +new Date();
        context = this;
        args = arguments;
        if (now - previous > wait) {
            func.apply(context, args);
            previous = now;
        }
    }
}
export default (Vue,options={})=>{
    //默认设置,可以传入options覆盖
    //preloadClass: 占位状态(原图未加载进来)的 class,可以利用他配合 css 加模糊效果
    //loadErrorClass: 图片加载失败后赋予的 class
    //default: 默认占位图透明
    //error: 出错后显示的图片默认透明,要启用错误处理才生效
    let init = {
        preloadClass:'lazyload-preload',
        loadErrorClass:'lazyload-status-fail',
        default:'data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==',
        error:`data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==`,
        ...options
    };
    let listenList = [];
    let imgCacheList = [];

    // 判断图片是否已经缓存
    const isAlreadyLoad = (imgSrc)=>{
        return imgCacheList.indexOf(imgSrc) > -1;
    };

    // 如果在视野内,触发后台加载返回true,否则返回false
    const tryLoad = (item)=>{
        let {el,src} = item;
        let top  = el.getBoundingClientRect().top;
        let screenHeight = window.innerHeight || document.documentElement.clientHeight;
        if (top < screenHeight + 50 && top > -50){
            let img = new Image();
            //后台加载完:进行替换,加入缓存,移除监听,更新class
            img.onload = ()=>{
                el.src = src;
                el.classList.remove(init.preloadClass);
                imgCacheList.pushIfNew(src);
                listenList.remove(item);
            };
            //如果出错:更新class,移除监听
            img.onerror = ()=>{
                if (item.errorHandle){
                    el.src = init.error;
                    el.style.objectFit = 'none';
                }
                el.classList.remove(init.preloadClass);
                el.classList.add(init.loadErrorClass);
                listenList.remove(item);
            };
            //出发后台加载
            img.src = src;
            return true;
        }else{
            return false;
        }
    };

    //用于标记监听状态,确保只会 addEventListener(由第一张插入的图片触发)
    let listenStatus = false;
    const listenScroll = ()=>{
        if (!listenStatus){
            window.addEventListener('scroll',throttle(()=>{
                let len = listenList.length;
                for (let i = 0; i < len; i++){
                    tryLoad(listenList[i])
                }
            },200));
            listenStatus = true;
        }
    };

    Vue.directive('lazyload',{
        inserted:(el,{value,modifiers})=>{
            let imgSrc,placeholder;
            // 两种方式传参数
            if (typeof value==='string'){
                imgSrc = value;
                placeholder = init.default;
            }else{
                imgSrc = value[0];
                placeholder = value[1]||init.default;
            }
            // 如果已经有缓存,直接使用
            if (isAlreadyLoad(imgSrc)){
                el.src = imgSrc;
                return false;
            }
            let item = {
                el:el,
                src:imgSrc,
                errorHandle:!!modifiers.rude //是否开启错误处理
            };
            // 先给占位图和占位 class
            el.src = placeholder;
            el.classList.add(init.preloadClass);
            if (tryLoad(item)){
                return;
            }
            // 如果在视野外,加入监听
            listenList.pushIfNew(item);
            // 第一张插入的图片负责 addEventListener
            !listenStatus && listenScroll();
        },
        //被监听的图片被移除,取消对其监听
        unbind:(el)=>{
          for(let item of listenList)
            if (item.el===el){
              listenList.remove(item);
              //console.log('remove')
            }
        }
    })
}

使用上和 Vue 装插件一样

/*-------main.js-------*/

import LazyLoad from './lazyload';
Vue.use(LazyLoad)

/*-------xxx.vue-------*/
// 两种方式传参数,指定原图和占位图/只指定原图,占位图默认
<img v-lazyload="[originSrc,thumbnailSrc]">
<img v-lazyload="originSrc">
// 启用错误处理
<img v-lazyload.rude="[originSrc,thumbnailSrc]">

其他

目前该指令只支持 img 标签的懒加载,对于 background-image 这种背景图并未支持(因为自己博客用得少),但我想实现起来也不难 “通过指令的修饰区别两种情况,改一下 tryLoad 函数……” 应该就行了

同时也不支持动态响应的参数(我不知道这样说对不对),也就是如果传入指令的 imgSrc 发生变动,被绑定的元素并不会更新。所以目前该指令只适用于插入一次后不再变更的元素

目前想到的问题就上面两个,如果有什么实用的功能也可以提出来,正好我也想把这个指令做得更精一些

以上

查看原文

赞 1 收藏 1 评论 1

忍野忍 收藏了文章 · 2019-02-13

如何正确的(?)利用 Vue.mixin() 偷懒

前言

最近开发的页面以及功能大都以表格为主,接口获取来的 JSON 数据大都是需要经过处理,比如时间戳需要转换,或者状态码的转义。对于这样的问题,各大主流框架都提供了类似于过滤的方法,在 Vue 中,一般是在页面上定义 filter 然后在模板文件中使用 | 进行处理。

这种方法和以前的遍历数组洗数据是方便了许多,但是,当我发现在许多的页面都有相同的 filter 的时候,每个页面都要复制一遍就显的很蛋疼,遂决定用 Vue.mixin() 实现一次代码,无限复用

最后,还可以将所有的 filter 包装成一个 vue 的插件,使用的时候调用 Vue.use() 即可,甚至可以上传 npm 包,开发不同的项目的时候可以直接 install 使用。(考虑到最近更新的比较快,遂打包上传这步骤先缓缓,等版本稍微稳定了之后来补全)

正文

闲话说够,开始正题。

Vue.mixin 为何物

学习一个新的框架或者 API 的时候,最好的途径就是上官网,这里附上 Vue.mixin() 官方说明。

一句话解释,Vue.mixin() 可以把你创建的自定义方法混入所有的Vue 实例。

示例代码

Vue.mixin({
  created: function(){
    console.log("success")
  }
})

跑起你的项目,你会发现在控制台输出了一坨 success

效果出来了意思也就出来了,所有的 Vue 实例的 created 方法都被改成了我们自定义的方法。

使用 Vue.mixin()

接下来的思路很简单,我们整合所有的 filter 函数到一个文件,在 main.js 中引入即可。

在上代码之前打断一下,代码很简单,但是我们可以写的更加规范化,关于如何做到规范,在 Vue 的官网有比较详细的 风格指南 可以参考。

因为我们的自定义方法会在所有的实例中混入,如果按照以前的方法,难免会有覆盖原先的方法的危险,按照官方的建议,混入的自定义方法名增加前缀 $_ 用作区分。

创建一个 config.js 文件,用于保存状态码对应的含义,将其暴露出去

export const typeConfig = {
  1: "type one",
  2: "type two",
  3: "type three"
}

再创建一个 filters.js 文件,用于保存所有的自定义函数

import { typeConfig } from "./config"
export default {
  filters: {
    $_filterType: (value) => {
      return typeConfig[value] || "type undefined"
    }
  }
}

最后,在 main.js 中引入我们的 filters 方法集

import filter from "./filters"
Vue.mixin(filter)

接下来,我们就可以在 .vue 的模板文件中随意使用自定义函数了

<template>
  <div>{{typeStatus | $_filterType}}<div>
</template>

包装插件

接下来简单应用一下 Vue 中插件的制作方法。创建插件之后,就可以 Vue.use(myPlugin) 来使用了。

首先附上插件的 官方文档

一句话解释,包装的插件需要一个 install 的方法将插件装载到 Vue 上。

关于 Vue.use() 的源码

function initUse (Vue) {
  Vue.use = function (plugin) {
    var installedPlugins = (this._installedPlugins || (this._installedPlugins = []));
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }

    // additional parameters
    var args = toArray(arguments, 1);
    args.unshift(this);
    if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args);
    } else if (typeof plugin === 'function') {
      plugin.apply(null, args);
    }
    installedPlugins.push(plugin);
    return this
  };
}

很直观的就看到他在最后调用了 plugin.install 的方法,我们要做的就是处理好这个 install 函数即可。

上代码

config.js 文件依旧需要,这里保存了所有状态码对应的转义文字

创建一个 myPlugin.js 文件,这个就是我们编写的插件

import { typeConfig } from "./config"

myPlugin.install = (Vue) => {
  Vue.mixin({
    filters: {
      $_filterType: (value) => {
        return typeConfig[value] || "type undefined"
      }
    }
  })
}
export default myPlugin

插件的 install 函数的第一个参数为 Vue 的实例,后面还可以传入一些自定义参数。

main.js 文件中,我们不用 Vue.mixin() 转而使用 Vue.use() 来完成插件的装载。

import myPlugin from "./myPlugin"
Vue.use(myPlugin)

至此,我们已经完成了一个小小的插件,并将我们的状态码转义过滤器放入了所有的 Vue 实例中,在 .vue 的模板文件中,我们可以使用 {{ typeStatus | $_filterType }} 来进行状态码转义了。

结语

Vue.mixin() 可以将自定义的方法混入所有的Vue 实例中。

本着快速开发的目的,一排脑门想到了这个方法,但是这是否是一个好方法有待考证,虽然不是说担心会对原先的代码造成影响,但是所有的Vue 实例也包括了第三方模板

本文可以随意转载,只要附上原文地址即可。

如果您认为我的博文对您有所帮助,请不吝赞赏,点赞也是让我动力满满的手段 =3=。

待完善

发布 npm 包

新增项

在 v-html 中骚骚的使用 filter (2018年05月28日)

最近碰到一个问题,也是关于状态码过滤的,但是实现的效果是希望通过不同的状态码显示不同的 icon 图标,这个就不同于上面的文本过滤了,上文使用的 {{ styleStatus | $_filterStyleStatus }} 是利用 v-text 进行渲染,若碰到需要渲染 html 标签就头疼了。

按照前人的做法,是这样的,我随意上一段代码。

...
<span v-if="item.iconType === 1" class="icon icon-up"></span>
<span v-if="item.iconType === 2" class="icon icon-down"></span>
<span v-if="item.iconType === 3" class="icon icon-left"></span>
<span v-if="item.iconType === 4" class="icon icon-right"></span>
...

不!这太不 fashion 太不 cool,我本能的拒绝这样的写法,但是,问题还是要解决,我转而寻找他法。

在看 Vue 的文档的时候,其中一个 API$options官方文档 就引起了我的注意,我正好需要一个可以访问到当前的 Vue 实例的 API,一拍脑袋,方案就成了。

首先,还是在 config.js 文件中定义一个状态码对应对象,这里我们将其对应的内容设为 html 段落。

export const iconStatus = {
  1: "<span class='icon icon-up'></span>",
  2: "<span class='icon icon-down'></span>",
  3: "<span class='icon icon-left'></span>",
  4: "<span class='icon icon-right'></span>"
}

接着,我们在 filters.js 文件中引入他,写法还是和以前的 filters 一样。

import { iconStatus } from "./config"
export default {
  $_filterIcon: (value) => {
      return iconStatus[value] || "icon undefined"
  }
}

重头戏在这里,我们在模板文件中需要渲染的地方,使用 v-html 来进行渲染。

<span v-html="$options.filters.$_filterIcon(item.iconType)"></span>

大功告成,以后需要根据状态码来渲染 icon 的地方都可以通过这个办法来完成了。

事实证明,懒并不一定是错误的,关键看懒的方向,虽然本篇博客写的标题是 偷懒 ,但其实我只是对于重复性的无意义的搬运代码而感到厌烦,在寻找对应解决办法的时候可是一点都没偷懒,相反的,在寻求更快更好更方便的方法的时候,我逐渐找回了当初敲代码的乐趣。

查看原文

忍野忍 收藏了文章 · 2018-12-16

Vue之网易云音乐PC版轮播图的实现

Github - program-learning-lists
最近在刷网易云音乐歌单时发现首页的轮播图很有意思,正好自己想尝试做一个PC版的网易云音乐,于是就是使用Vue去做这个demo,废话少说,我要出招了,接招吧

网易云音乐PC版轮播图

页面的DOM结构

<template>
  <div class="slider-container" ref='slider'
       :style="sliderStyle"
       @mouseover="pause()"
       @mouseout="play()">
    <div class="slider-content" :class="mask ? 'mask' : ''">
      <div class="slider" v-for="(item, index) in list"
        :key="index"
        :class="setClass(index)"
        @click="onClick(index)" :style="setBGImg(item.src)">
      </div>
      <i v-show="arrow" class="iconfont icon-left" @click="prev()"></i>
      <i v-show="arrow" class="iconfont icon-right" @click="next()"></i>
    </div>
    <div class="dots" v-if="dots">
      <span v-for="(item, index) in list" :key="index"
        :style="setActiveDot(index)"
        @mouseover="currentIndex = index"></span>
    </div>
  </div>
</template>

Slider-container的样式(Stylus)

.slider-container
  width: 100%
  height: 100%
  text-align: center
  padding: 10px 0
  position: relative

这个子组件主要分为两块。
第一块、轮播图,其中它们的业务逻辑是

  • 自动切换
  • 左右icon切换轮播图
  • 点击前后轮播图切换轮播图
  • 鼠标滑动到轮播图停止轮播,离开后继续轮播

Slider-content的DOM结构

<div class="slider-content" :class="mask ? 'mask' : ''">
  <div class="slider" v-for="(item, index) in list"
    :key="index"
    :class="setClass(index)"
    @click="onClick(index)" :style="setBGImg(item.src)">
  </div>
  <i v-show="arrow" class="iconfont icon-left" @click="prev()"></i>
  <i v-show="arrow" class="iconfont icon-right" @click="next()"></i>
</div>

Slider-content的样式(Stylus)

.slider-content
    position: relative
    width: 100%
    height: calc(100% - 20px)
    left: 0%
    top: 0%
    margin: 0px
    padding: 0px
    background-size: inherit
    .slider 
      position: absolute
      margin: 0
      padding: 0
      top: 0
      left: 50%
      width: 65%
      height: 100%
      transition: 500ms all ease-in-out
      background-color: #fff
      background-repeat: no-repeat
      background-position: center
      background-size: inherit
      transform: translate3d(-50%,0,-80px)
      z-index: 1
      &:before
        position: absolute
        content: ""
        width: 100%
        height: 100%
        top: 0
        left: 0
        background-color: rgba(0, 0, 0, 0)
        transition-delay: 100ms!important
        transition: all 500ms
        cursor: pointer
      &.active
        transform: translate3d(-50%, 0, 0)
        z-index: 20
      &.prev
        transform: translate3d(-75%, 0, -100px)
        z-index: 19
      &.next
        transform: translate3d(-25%, 0, -100px)
        z-index: 18
    i
      width: 17.5%
      display: none
      position: absolute
      top: 40%
      font-size: 22px
      color: rgba(255, 255, 255, 0.5)
      text-shadow: 0 0 24px rgba(0, 0, 0, 0.3)
      cursor: pointer
      z-index: 21
      &:first-child
        left: 0
      &:last-child
        right: 0
    &:hover
      i
        color: rgba(255, 255, 255, 0.8)
        display: block
    &.mask
      .slider 
        &.prev, &.next
          &:before
            background-color: rgba(0, 0, 0, 0.50)

第二块、底部的dot, 其中它们的业务逻辑是

  • 当前轮播图对应位置的dot高亮
  • 鼠标移动到相应的dot上切换对应位置的轮播图

Dots的DOM结构

<div class="dots" v-if="dots">
  <span v-for="(item, index) in list" :key="index"
    :style="setActiveDot(index)"
    @mouseover="currentIndex = index"></span>
</div>

Dots的样式(Stylus)

.dots 
  width: 100%
  height: 20px
  span
    display: inline-block
    width: 20px
    height: 2px
    margin: 1px 3px
    cursor: pointer

上面是页面的DOM结构和表现的实现代码,接下来我们要讲的是连招的实现,小心啦,我要摸眼W + R3了。
上面我们讲到轮播图的业务逻辑,接下来,我们就讲讲如何实现的的吧

自动轮播

自动轮播

play () {
  this.pause();
  if (this.autoPlay) {
    this.timer = setInterval(()=>{
      this.next();
    }, this.interval)
  }
}

暂停轮播

暂停轮播

pause () {
  clearInterval(this.timer);
}

Icon切换轮播图

Icon切换轮播图

next () {
  this.currentIndex = ++this.currentIndex % this.list.length;
},
prev () {
  this.currentIndex = this.currentIndex === 0 ? this.list.length - 1 : this.currentIndex - 1;
},

前后轮播图的切换轮播图

前后轮播图的切换轮播图

onClick (i) {
  if (i === this.currentIndex){
    this.$emit('sliderClick', i);
  } else {
    let currentClickClassName = this.sliderDomList[i].className.split(' ')[1]
    console.log(currentClickClassName)
    if (currentClickClassName === 'next') {
      this.next()
    } else {
      this.prev()
    }
  }
}

dots轮播图的切换轮播图

dots轮播图的切换轮播图

这里比较简单,只需要设置它的鼠标事件即可

@mouseover="currentIndex = index"

代码传送门:Vue网易云音乐轮播图的实现

知乎

个人博客

Github

查看原文

忍野忍 发布了文章 · 2018-10-26

【DL-CV】浅谈GoogLeNet(咕咕net)

咕了一个多月后终于重新变成人,今天我们就来谈谈 咕咕net(GoogLeNet) 的结构,在下次咕咕(大表哥2)之前挣扎一下。


GoogLeNet初始的想法很简单,“大力出奇迹”,即通过增加网络的网络的尺寸(深度与宽度)来变强。这脑回路看上去没啥毛病,但是一用在原味版的cnn上问题就来了,尺寸的增加和全连接层的存在带来了巨量的参数,计算成本暴增的同时增加了过拟合的风险。为了解决这一麻烦贯彻“大力出奇迹”方针,新的网络结构被提了出来,而其中的精妙之处就是inception模块,用上该模块的GoogLeNet能更高效的利用计算资源,在相同的计算量下能提取到更多的特征,从而提升训练结果,下面就谈谈他。

Inception 模块

clipboard.png

从图片来看inception模块就用不同尺寸的卷积核同时对输入进行卷积操作,外加一个池化操作,最后把各自的结果汇聚在一起作为总输出(暗示他们都有相同的尺寸)。与传统cnn的串联结构不同,inception模块使用了并行结构并且引入了不同尺寸的卷积核。关于为什么这种改变是好的,下面是一些参考解释:

  • 直观感觉上,在多个尺度上同时进行卷积,能提取到不同尺度的特征,这是好的
  • (最主要的优点)以往为了打破网络对称性和提高学习能力,传统的网络都使用了随机稀疏连接。但是,计算机软硬件对非均匀稀疏数据的计算效率是很差的。那么存不存在既能保持网络结构的稀疏性,又能利用密集矩阵的高计算性能的方法呢?答案就在这个inception里,其实现将稀疏矩阵聚类为较为密集的子矩阵来提高计算性能。

再说下inception的一些设定:

  • 卷积核尺寸使用1,3,5是为了方便对齐,只需padding分别为0,1,2;步长都取1 就能获得相同尺寸的输出以叠加
  • 网络越到后面,特征越抽象,而且每个特征所涉及的感受野也更大了,因此随着层数的增加,3x3和5x5卷积(数量)的比例也要增加

降维操作

然而像上图一样直接投入使用,参数量和计算量还是很大的,为了进一步降低消耗,inception在 3x3 和 5x5 卷积前和池化后引进了 1x1 卷积进行数据降维(事先把数据深度下降),还能顺便增加网络深度。如下图红色区域。另:降维后还是需要经过激活函数
clipboard.png

至于降维操作是否会造成数据丢失?就结果来看来不必担心,别人已经测试过了

GoogLeNet结构

既然最核心的inception模块讲完了,那就直接展示GoogLeNet的结构了,其大部分都是各种inception模块叠加而成的。
clipboard.png

整个网络除了inception外最引人注目的就是中途露出的两个小尾巴了,那是两个辅助分类器。说实话这是GoogLeNet第二个精妙之处了。除了最终的分类结果外,中间节点的分类效果还是不错的,所以GoogLeNet干脆从中间拉了两条分类器出来,然他们按一个较小的权重(如0.3)加到最终的分类结果中,这样做好处有三:

  • 相当于做了模型整合
  • 给网络增加了反向传播的梯度信号,一定程度解决了深网络带来的梯度消失的问题
  • 而且还提供了额外的正则化
  • 喵啊喵啊

当然辅助分类器只用于训练阶段,在测试阶段是要去掉的

其他一些新奇之处就是网络的最后用了平均池化代替了全连接层,然而后面还是接了一个全连接层,这是方便其他人进行迁移学习的。

靓文推荐

以上介绍的就是最原始最开始的GoogLeNet,也叫GoogLeNet Incepetion V1,2014年提出的。在经过多年的改进后GoogLeNet也有几个延伸版本了如使用了BN的V2版本,借鉴了ResNet的V4版本,这里也不再细讲,只推荐几篇我认为比较好的靓文

查看原文

赞 0 收藏 0 评论 0

忍野忍 发布了文章 · 2018-09-13

【DL-CV】更高级的参数更新/优化(二)

【DL-CV】更高级的参数更新(一)

【DL-CV】正则化,Dropout<前篇---后篇>【DL-CV】浅谈GoogLeNet(咕咕net)

Adagrad

全名 Adaptive gradient algorithm ,翻译过来就是“适应梯度算法”,该算法能根据梯度中偏导数的大小给不同的偏导数以不同的学习率,偏导数大(小)的给个小(大)的学习率,以此来减少参数更新时的摆动。

其核心是引入一个参数对历次的梯度的平方进行累加,在更新阶段,基础学习率将除以这个参数的开方。这样大梯度会因为积累大而分得小的学习率,小梯度因为积累小而分得较大的学习率
$$s_{t+1} = s_{t}+\nabla_x L(x_t)^2$$
$$x_{t+1}=x_t-{\alpha\over \sqrt {s_{t+1}}+\epsilon}\nabla_x L(x_t)$$
其中$\epsilon$是一个很小的数,为了防止除法时分母出现零,一般设为1e-4到1e-8之间。下面是代码实现
clipboard.png


缺点:梯度平方的累计值在训练过程中不断增大,作为分母被除了以后学习率衰减过快,更新变慢

RMSprop

RMSprop 是 Geoff Hinton 提出的一种自适应学习率方法,目的是为了解决 Adagrad 学习率急剧下降的问题。其在对梯度平方进行累加时会进行衰减处理(其实是指数加权平均),解决学习率急降的问题的同时又保留了 Adagrad 调节学习率的优点
$$s_{t+1} = \beta s_t +(1-\beta)\nabla_x L(x_t)^2$$
$$x_{t+1}= x_t-{\alpha\over \sqrt {s_{y+1}+}\epsilon}$$
$\gamma$ 通常为0.9/0.99
clipboard.png


以上两种方法解决了SGD的问题2️⃣

Adam

两种动量方法通过引入“速度”解决了问题1️⃣;Adagrad 和 RMSprop 通过引入“梯度平方”解决了问题2️⃣,各有千秋。为何不把两者结合起来然后一次满足两个愿望呢?这么一想,Adam就出来了。

Adam 相当于 RMSprop + Momentum,他除了像 RMSprop 一样存储了过去梯度平方的指数衰减平均值 ,也像 momentum 一样保持了过去梯度的指数衰减平均值:
$$v_{t+1} = \beta_1v_t + (1-\beta_1)\nabla_x L(x_t)$$
$$s_{t+1} = \beta_2s_t + (1-\beta_2)\nabla_x L(x_t)^2$$
如果 v 和 s 被初始化为 0 向量,那它们就会向 0 偏置,所以还需要偏差校正来抵消这些偏差:
$$\hat v_{t+1} = {v_{t+1}\over 1-\beta_1^{t+1}}$$
$$\hat s_{t+1} = {s_{t+1}\over 1-\beta_2^{t+1}}$$
最后的更新就是:
$$x_{t+1}=x_t-{\alpha \hat v_{t+1}\over\sqrt{\hat s_{t+1}}+\epsilon}$$
推荐值$\beta_1=0.9$,$\beta_2=0.99$,$\alpha=10^{-3}/5·10^{-4}$代码实现:
clipboard.png


Adam真的是一个非常棒的算法,实践表明,Adam 比其他适应性学习方法效果要好,几乎成为标配默认的算法啦。所以长篇大论了这么久之后的结论是——推荐首选Adam

查看原文

赞 0 收藏 0 评论 0

忍野忍 发布了文章 · 2018-09-13

【DL-CV】更高级的参数更新/优化(一)

后续【DL-CV】更高级的参数更新/优化(二)

【DL-CV】正则化,Dropout<前篇---后篇>【DL-CV】浅谈GoogLeNet(咕咕net)


原版SGD的问题

原味版的SGD(以下称SGD)是通过死跟负梯度方向来对参数进行更新的,也就是走一步、停下确定方向、再走一步,如此循环。非常简单,非常老实的走法不是麽?但是SGD这个相对简单的算法在实际使用中还是会产生不少的问题,下面我们来看看最主要的几个


1️⃣SGD 一旦陷入损失的局部最小值或鞍点(既不是最大值也不是最小值的临界点)训练将变得缓慢:
clipboard.png

作为问题的引入我们先考虑参数只有一个的情况下损失关于该参数的图(如上,x轴是该参数,y轴是损失)

  • 当SGD遇到局部最小值时,因为梯度为0,参数不会更新,最终就卡在局部最小值这个点了
  • 当SGD遇到或接近鞍点时,同样梯度为0或非常小,参数也几乎不会更新,于是也卡在该鞍点

这是一维的情况,在高维的情况下(上千上万个参数),局部最小值意味着所有参数无论往哪个方向走损失都会增大,这其实是非常罕见的事情。而高维情况下的鞍点可以这样理解,即在该点下某些方向会使损失增大,某些方向会使损失减少;所以在高维情况下遇到鞍点将会是非常常见的事情,在鞍点附近训练速度将会变得缓慢。


2️⃣SGD 对所有参数更新时应用同样的学习率:
梯度由许多偏导数组成,对应着各个参数的更新。对于偏导数大的,我们希望配个小的学习率给他;对于偏导数小的,我们希望配个大的学习率给他,这样各个参数都能获得大致相同的更新幅度,提高网络的健壮性。可惜SGD固定死的学习率不能满足我们的要求


3️⃣SGD 在遇到噪声时,训练将变得缓慢:
SGD 是通过随机抽取小批量样本进行训练的,是对每次损失和梯度的估计(不像批量学习一样全部训练样本全往里塞获得真实的损失和估计),当估计中存在噪音,参数更新的方向会受到影响而偏离,导致训练时间延长


综上,使用原版的SGD进行参数更新带来的体验并不是那么好(需要更新换代啦)。还好的是,伟大的研究人员,或是对原版SGD进行了各种各样的魔改,或是灵光一闪提出新的更新算法,现在已经有多种更高级的参数更新的方法啦,下面就来看一下

普通动量更新

该方法的关键是引入一个速度的概念。速度这个量将对历次求得的梯度进行累加,在每次累加时会有一参数$\gamma$对原速度进行衰减处理后再进行累加。参数更新时,我们不在根据当前的负梯度方向进行更新,而是根据当前速度方向更新。这样引入速度之后 SGD卡在局部最小值和鞍点的问题 会得到有效解决,从而加速训练。

上面的说明不懂没关系,看了下面的比喻你便会豁然开朗

  • 想象有一座高山,其高度代表损失值;有一个小球代表着参数,在山的不同点有不同的值;我们的目标是让小球滚到山底取得最优参数(下山的过程就是训练的过程)。在这种想象下,负梯度就相当于重力在斜面上的分力使得小球有下降的趋势,小球在下降的过程中因为重力做正功速度会增加;也因为这个速度的存在,小球会安然越过小山谷(对应局部最小值)或平原(对应鞍点)继续下山。当小球真正接近“U型”山底时,小球会在山底来回摆动,最后因为阻力(对应参数$\gamma$)而停在山底(参数训练完成)。
  • clipboard.png

懂了下面就放公式,顺便比较SGD和魔改后的SGD,公式中的t代表第t次迭代更新

SGDSGD + 动量
公式(x是待更新参数)$$x_{t+1}=x_t-\alpha\nabla_xL(x_t)$$$$v_{t+1}=\gamma v_t+\alpha\nabla_xL(x_t)$$$$x_{t+1}=x_t-v_{t+1}$$
代码实现clipboard.pngclipboard.png

至于参数$\gamma$,其存在相当于摩擦力,可使使速度衰减。如果没有$\gamma$,小球到达最后的“U型”山底就不会停下来,训练时参数可能就收敛不了。$\gamma$常为[0.5,0.9,0.95,0.99]中的一个,并且通常为0.9。$\gamma$也可以随着训练的进行而逐渐上升,如刚开始将$\gamma$设为0.5而在后面的多个epoch中慢慢提升到0.99

Nesterov动量

Nesterov动量与普通动量原理上有些许不同(Nesterov动量可以说是普通动量的小升级)。在理论上对于凸函数Nesterov动量能得到更好的收敛,在实践中也确实比普通动量表现更好一些。

使用Nesterov动量时,不会计算当前点的梯度,而是先往速度方向步进到下一点,计算这“下一点”的梯度,然后回到原点把这“下一点”的梯度加到速度上,再用累加后的速度在原点上进行步进。这个看似多此一举的操作并不是无意义的,计算“下一点”的梯度可以看作是对未来位置梯度的预测从而提前对速度方向进行修正,从而进一步加快训练速度。下面来对比一下普通动量和Nesterov动量:

普通动量Nesterov动量
图示clipboard.pngclipboard.png
公式$$v_{t+1}=\gamma v_t+\alpha\nabla_xL(x_t)$$$$x_{t+1}=x_t-v_{t+1}$$$$v_{t+1}=\gamma v_t + \alpha \nabla_xL(x_t - \gamma v_t)$$$$x_{t+1} = x_t-v_{t+1}$$

这两个动量更新方法都有效解决了SGD的问题1️⃣

查看原文

赞 1 收藏 0 评论 0

忍野忍 发布了文章 · 2018-09-07

【DL-CV】正则化,Dropout

                      /--->【DL-CV】更高级的参数更新/优化(一)
【DL-CV】批量归一化(BN算法)<前篇---后篇>
                      /--->【DL-CV】更高级的参数更新/优化(二)


在损失函数那篇文章中,我们引入了正则化并简单介绍了L2正则化。其作用是防止网络过拟合,提高网络的泛化能力。本文将介绍一些防止过拟合的方法,

过拟合

所谓过拟合,就是网络对训练集极度适应,以至于训练出来的权重几乎只服务于训练集,导致在训练好的网络在跑训练集的时候表现得很好(准确率高),而跑测试集或其他数据表现得很差。过拟合的出现将降低网络的泛化能力,是非常糟糕的事情。


正则化是抑制网络过拟合,提高网络泛化能力的一种方法。Dropout是其替代品,也能防止过拟合提高泛化能力。下面介绍这两种方法

正则化

正则化通过在损失函数上加上正则化损失来实现,根据正则化损失定义的不同,正则化又能分为好几种。

L2正则化

$$R(W)=\sum_k \sum_l W^2_{k,l}$$
L2正则化是是最常用的正则化方法,即对于网络中的每个权重值$w$,向损失函数中增加一个$\frac{1}{2}\lambda w^2$作为正则化惩罚,其中$\lambda$是正则化强度;乘了个$\frac{1}{2}$是很常见的事,因为这样式子关于 $w$ 梯度就是 $\lambda w$ 而不是 $2\lambda w$ 了。

L2正则化可以直观地理解为对于大数值的权重向量进行严厉惩罚(平方惩罚),驱使权重往更加均匀的方向发展而不是某些权重绝对值很大某些权重绝对值很小,这样能提高网络的泛化能力。

L1正则化

$$R(W)=\sum_k \sum_l| W_{k,l}|$$
L1正则化是另一个常用的正则化方法,即对于网络中每个权重值$w$,向损失函数增加一个$\lambda|w|$作为其正则化惩罚,其中$\lambda$是正则化强度。

L1正则化相对于L2正则化对大数值权重的惩罚没那么重(没有平方惩罚),他会让权值在最训练的过程中非常接近零(L2则是让权重变成分散的小数值)。在实践中,如果不是特别关注某些明确的特征选择,一般说来L2正则化都会比L1正则化效果好


L1正则化也可以和L2正则化组合使用变成$\lambda_1|w|+\lambda_2w^2$,这也被称作Elastic net regularizaton。不过没这么常用就是了

随机失活(Dropout)

Dropout堪称对付过拟合的超级武器,其使用简单,效果强劲。我们先来介绍dropout的使用:

  • 在训练的时候,Dropout的实现方法是让隐层神经元以超参数$1-p$的概率被设置为0,$p$的概率维持激活值(随机失活,加入随机性)
  • 在预测时不使用随机失活,但是每层的输出要乘以$p$(因为训练时每层只有有p的激活值被维持,为维持同样的输出,预测时每层激活值要乘以p,抵消随机性)

clipboard.png

Dropout可以理解为,在每次训练时,随机抽取一部分神经元组成一个子网络,基于输入数据只更新子网络的参数(子网络并不相互独立,因为他们共享着参数)。最后在测试时不使用随机失活,可以理解为对子网络们做了模型集成,以此来计算出一个平均的预测。

p = 0.5 # 激活神经元的概率. p值更高 = 随机失活更弱

def train_step(X):
  """ X中是输入数据 """
  
  # 3层neural network的前向传播
  H1 = np.maximum(0, np.dot(X, W1 ) + b1)
  U1 = np.random.rand(*H1.shape) < p # 第一个随机失活遮罩
  H1 *= U1 # drop!
  H2 = np.maximum(0, np.dot(H1, W2) + b2)
  U2 = np.random.rand(*H2.shape) < p # 第二个随机失活遮罩
  H2 *= U2 # drop!
  out = np.dot(H2, W3) + b3
  
  # 反向传播略
  # 参数更新略
  
def predict(X):
  H1 = np.maximum(0, np.dot(W1, X) + b1) * p # 注意:激活数据要乘以p
  H2 = np.maximum(0, np.dot(W2, H1) + b2) * p # 注意:激活数据要乘以p
  out = np.dot(W3, H2) + b3

Dropout的理解

clipboard.png

一种理解是Dropout避免了特征间的相互适应。如上图,假如网络中的神经元学到了如上特征,“有耳朵”,“有尾巴”,“毛毛的”等等特征,这些特征组合起来判断是否是猫。正常来说(作为人来说),特征越多越好,但在这种情况下(深度学习),完全依靠这些特征将可能导致过拟合的发生。使用Dropout后,网络的部分连接将被中断,网络不能依赖这些特征的组合来给出结果,而是只能使用零散的特征来给出判断,这在某种程度上防止了过拟合,提高了网路的泛化能力。

更多解释和理解可参考此文

推荐:反向随机失活(Inverted Dropout)

上面提到的Dropout方法在测试时每层激活值是要乘$p$的,额外的乘法操作会降低测试时的效率。所以更多时候我们会使用一个叫Inverted Dropout 的方法,他把随机性抵消的操作从预测阶段搬到了训练阶段:在训练阶段的Dropout操作后,我们对神经元的激活值除以$p$来抵消随机性。这样预测阶段会更高效。

p = 0.5 # 激活神经元的概率. p值更高 = 随机失活更弱

def train_step(X):
  """ X中是输入数据 """
  
  # 3层neural network的前向传播
  H1 = np.maximum(0, np.dot(X, W1 ) + b1)
  U1 = (np.random.rand(*H1.shape) < p) / p # 第一个随机失活遮罩,/p
  H1 *= U1 # drop!
  H2 = np.maximum(0, np.dot(H1, W2) + b2)
  U2 = (np.random.rand(*H2.shape) < p) / p # 第二个随机失活遮罩
  H2 *= U2 # drop!
  out = np.dot(H2, W3) + b3
  
  # 反向传播略
  # 参数更新略
  
def predict(X):
  H1 = np.maximum(0, np.dot(X, W1) + b1)  # 免去*p
  H2 = np.maximum(0, np.dot(H1, W2) + b2) 
  out = np.dot(H2, W3) + b3

除了将神经元随机置0的Dropout外,还有将权重值随机置0的DropConnect,兴趣自查

查看原文

赞 1 收藏 0 评论 0

忍野忍 发布了文章 · 2018-09-05

【DL-CV】批量归一化(BN算法)

【DL-CV】数据预处理&权重初始化<前篇---后篇>【DL-CV】正则化,Dropout

先来交代一下背景:在网络训练的过程中,参数的更新会导致网络的各层输入数据的分布不断变化,那么各层在训练的过程中就需要不断的改变以适应这种新的数据分布,从而造成网络训练困难,收敛变慢(而且网络越深越难),在论文中这个问题被称为“Internal Covariate Shift”。为了解决这个问题出现了批量归一化的算法,他对每一层的输入进行归一化,保证每层的输入数据分布是稳定的,从而加速训练

批量归一化(Batch Normalization/BN)

  • Normalization——“归一化”
  • Batch——“批”,一批样本输入,batch_size=m,m个样本

与激活函数层、卷积层(cnn)、全连接层(FC)、池化层一样,批量归一化也属于网络的一层,简称BN。BN通常用在FC/cnn之后,激活函数层之前,他对FC/cnn的输出的每一维进行归一化(归一化至:均值0、方差为1),然后变换重构后再把数据喂给下一层网络。

归一化

对输入数据的每一维xk(每一列)进行归一化得到$\hat{x}^{k}$
$$\hat{x}^{(k)} = \frac{x^{k} - E[x^{k}]}{\sqrt{Var[x^{k}]}}$$

clipboard.png

通过该公式我们强行把数据归一化,特征分布成功被压缩至标准差为1。但这种变化是有破坏性的,原来的特征分布都被搞乱了那还学个鬼?——为了解决这一问题,我们对归一化后的数据再进行变换重构,以恢复原来的特征

变换重构

对于归一化后输入$\hat{x}^{k}$,我们再引入两个可供学习的参数$\gamma$和$\beta$使
$$y^{k} = \gamma^{k}\hat{x}^{k}+\beta^{k}$$
注意如果$\gamma^{k}=\sqrt{Var[x^k]}$,$\beta^{k}=E[x^k]$时,是可以恢复原有的特征的。这就是引入这两个参数的意义,虽然网络不太可能学习到$\gamma^{k}=\sqrt{Var[x^k]}$,$\beta^{k}=E[x^k]$,但是好歹通过学习到这两个参数尽可能地回复原有的特征(实践中的表现说明这是有用的)


最后总结BN操作:(m是mini batch size,根号中的ε是一个很小的数防止分母为0)
clipboard.png
clipboard.png

测试时!

上面讲了这么多其实都是训练时的BN算法,训练时是一个一个batch进去的,训练完成后得到$\gamma$和$\beta$。但是在测试时,我们只是丢一个样本进去让网络预测,试问一个样本何来均值和方差?
均值和方差最好的来源是整个训练集,但是整个训练集往往太大,我们不太想算。能不能从现成的 mini batch 的均值和方差(因为在训练时算了出来)估计出整个训练集的均值和方差呢——当然可以,因为mini batch是随机取样的:
$$E[x] = E_B[\mu_B]$$
$$Var[x] = \frac{m}{m-1}E_B[\sigma_B^2]$$
即平均值为所有mini-batch的平均值的平均值,而方差为每个batch的方差的无偏估计。当网络训练完后,我们通过训练时保存下来的每个batch的均值与方差估算出全局均值与方差供测试时使用,也就是说当网络训练完后每个BN层中的均值也和标准差也会随之确定。

BN好处有啥

  • 能选择较大的初始学习率,提高训练收敛速度
  • 你再也不用去理会过拟合中drop out、L2正则项参数的选择问题,采用BN算法后,你可以移除这两项了参数,或者可以选择更小的L2正则约束参数了,因为BN自带正则化的特性,能提高网络泛化能力
  • 减少梯度爆炸/消失的出现,特别对于tanh,sigmoid这类会两端饱和的激活函数
  • 减少对参数初始化的依赖,就算参数初始化得不好,在BN的加持下网络还是表现较好

虽然BN是2015才提出来的(看上去像个新米),但是他表现也超凡,BN算法已经被大量使用于深度学习中,真的是nb

补充

查看原文

赞 0 收藏 0 评论 0

忍野忍 发布了文章 · 2018-09-05

【DL-CV】数据预处理&权重初始化

【DL-CV】卷积神经网路<前篇---后篇>【DL-CV】批量归一化(BN算法)

数据预处理

在网络训练时,我们通常会对原始数据进行预处理再喂给网络进行训练,而不是直接把原始数据喂给网络。这样能减少训练时问题的发生,提高网络模型的性能。现在我们有原始数据 X,其尺寸是 NxD(N是数据样本数量,D是数据的维度)

均值减法

均值减法(Mean subtraction)是预处理最常用的形式,将数据的每一维特征都减去平均值即可,在numpy的实现是X -= np.mean(X, axis=0),它能使数据零中心化


另外还有一些类似的方法对图像数据预处理来说也很可能用到:

  • 直接减去整个数据的均值,X -= np.mean(X),对于图像,可以移除图像的平均亮度值,因为很多情况下我们对图像亮度并不感兴趣,而是关注其内容,所以将图像整体减去像素均值是有意义的
  • 减去每个颜色通道上的均值

归一化

归一化(Normalization)也是一种预处理的方法,他将数据的所有维度都归一化,使其数值范围都近似相等。先对数据做零中心化处理,然后每个维度都除以其标准差

x = x - np.mean(x, axis=0)
x = x / np.std(x, axis=0)

除此之外,预处理方法还有PCA和白化,但在图像处理这一块他们用得并不是很多,这里就不讲了,有兴趣自查。在图像识别领域,更常用的是零中心化和归一化,特别是零中心化
零中心化在使用时,先将样本数据分成训练/验证/测试集,从训练集中求得平均值,然后将各个集(训练/验证/测试集)中的数据再减去这个均值。

权重初始化

首先,全零初始化想想都知道肯定是错的。

然后,小随机数初始化如W = 0.01 * np.random.randn(D,H)是可以的(randn函数是基于零均值和标准差的一个高斯分布),不过不是越小越好,权重初始化的太小的话计算出来的激活值会很小,在反向传播中就会得到很小的梯度,后果你知道的。

但是,还不是最好的,小随机数初始化有一个问题就是随着输入数据量的增长,随机初始神经元输出数据的方差也增大,这样网络一深问题就大了。解决方法是:除以输入数据量的平方根来调整数值范围,使神经元输出的方差归一化为1,也就是W = np.random.randn(in, out) / sqrt(in) ,其中in是输入数据的数量(若W尺寸 DxH ,则in=D,out=H)。这种更厉害的方法叫做 Xavier初始化,他保证了网络中所有神经元起始时有近似同样的输出分布。实践经验证明,这样做可以提高收敛的速度。

《Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification》论文指出,使用relu激活函数的网络,由于一半的激活值变成了0,相当于一半的输入数据量是对方差无贡献的,为保持输入输出的方差维持不变,初始化方法也作了相应改变(根号中的数据量除以二): W = np.random.randn(in, out) / sqrt(in/2) ,这是针对使用relu的初始化方法。

至于偏置值,通常初始化为0

总结

针对图像识别领域,通常来说

  • 数据预处理使用零中心化
  • 权重初始化使用Xavier;如网络使用relu,使用W = np.random.randn(in, out) / sqrt(in/2)
  • 偏置值初始化为0
查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 103 次点赞
  • 获得 8 枚徽章 获得 0 枚金徽章, 获得 4 枚银徽章, 获得 4 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

注册于 2018-04-13
个人主页被 4.4k 人浏览