手摸手帶你理解Vue的Computed原理

前言

computed 在 Vue 中是很常用的屬性配置,它能夠隨着依賴屬性的變化而變化,為我們帶來很大便利。那麼本文就來帶大家全面理解 computed 的內部原理以及工作流程。

在這之前,希望你能夠對響應式原理有一些理解,因為 computed 是基於響應式原理進行工作。如果你對響應式原理還不是很了解,可以閱讀我的上一篇文章:手摸手帶你理解Vue響應式原理

computed 用法

想要理解原理,最基本就是要知道如何使用,這對於後面的理解有一定的幫助。

第一種,函數聲明:

var vm = new Vue({
  el: '#example',
  data: {
    message: 'Hello'
  },
  computed: {
    // 計算屬性的 getter
    reversedMessage: function () {
      // `this` 指向 vm 實例
      return this.message.split('').reverse().join('')
    }
  }
})

第二種,對象聲明:

computed: {
  fullName: {
    // getter
    get: function () {
      return this.firstName + ' ' + this.lastName
    },
    // setter
    set: function (newValue) {
      var names = newValue.split(' ')
      this.firstName = names[0]
      this.lastName = names[names.length - 1]
    }
  }
}

溫馨提示:computed 內使用的 data 屬性,下文統稱為“依賴屬性”

工作流程

先來了解下 computed 的大概流程,看看計算屬性的核心點是什麼。

入口文件:

// 源碼位置:/src/core/instance/index.js
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

function Vue (options) {
  this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

_init:

// 源碼位置:/src/core/instance/init.js
export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      // mergeOptions 對 mixin 選項和傳入的 options 選項進行合併
      // 這裏的 $options 可以理解為 new Vue 時傳入的對象
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }

    // expose real self
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    // 初始化數據
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

initState:

// 源碼位置:/src/core/instance/state.js 
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  // 這裡會初始化 Computed
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

initComputed:

// 源碼位置:/src/core/instance/state.js 
function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  // 1
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()
    
  for (const key in computed) {
    const userDef = computed[key]
    // 2
    const getter = typeof userDef === 'function' ? userDef : userDef.get

    if (!isSSR) {
      // create internal watcher for the computed property.
      // 3
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        { lazy: true }
      )
    }

    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    if (!(key in vm)) {
      // 4
      defineComputed(vm, key, userDef)
    }
  }
}
  1. 實例上定義 _computedWatchers 對象,用於存儲“計算屬性Watcher”
  2. 獲取計算屬性的 getter,需要判斷是函數聲明還是對象聲明
  3. 創建“計算屬性Watcher”,getter 作為參數傳入,它會在依賴屬性更新時進行調用,並對計算屬性重新取值。需要注意 Watcher 的 lazy 配置,這是實現緩存的標識
  4. defineComputed 對計算屬性進行數據劫持

defineComputed:

// 源碼位置:/src/core/instance/state.js 
const noop = function() {}
// 1
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  // 判斷是否為服務端渲染
  const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    // 2
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  } else {
    // 3
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop
    sharedPropertyDefinition.set = userDef.set || noop
  }
  // 4
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
  1. sharedPropertyDefinition 是計算屬性初始的屬性描述對象
  2. 計算屬性使用函數聲明時,設置屬性描述對象的 get 和 set
  3. 計算屬性使用對象聲明時,設置屬性描述對象的 get 和 set
  4. 對計算屬性進行數據劫持,sharedPropertyDefinition 作為第三個給參數傳入

客戶端渲染使用 createComputedGetter 創建 get,服務端渲染使用 createGetterInvoker 創建 get。它們兩者有很大的不同,服務端渲染不會對計算屬性緩存,而是直接求值:

function createGetterInvoker(fn) {
  return function computedGetter () {
    return fn.call(this, this)
  }
}

但我們平常更多的是討論客戶端渲染,下面看看 createComputedGetter 的實現。

createComputedGetter:

// 源碼位置:/src/core/instance/state.js
function createComputedGetter (key) {
  return function computedGetter () {
    // 1
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      // 2
      if (watcher.dirty) {
        watcher.evaluate()
      }
      // 3
      if (Dep.target) {
        watcher.depend()
      }
      // 4
      return watcher.value
    }
  }
}

這裏就是計算屬性的實現核心,computedGetter 也就是計算屬性進行數據劫持時觸發的 get。

  1. 在上面的 initComputed 函數中,“計算屬性Watcher”就存儲在實例的_computedWatchers上,這裏取出對應的“計算屬性Watcher”
  2. watcher.dirty 是實現計算屬性緩存的觸發點,watcher.evaluate 對計算屬性重新求值
  3. 依賴屬性收集“渲染Watcher”
  4. 計算屬性求值後會將值存儲在 value 中,get 返回計算屬性的值

計算屬性緩存及更新

緩存

下面我們來將 createComputedGetter 拆分,分析它們單獨的工作流程。這是緩存的觸發點:

if (watcher.dirty) {
  watcher.evaluate()
}

接下來看看 Watcher 相關實現:

export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    // dirty 初始值等同於 lazy
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.value = this.lazy
      ? undefined
      : this.get()
  }
}

還記得創建“計算屬性Watcher”,配置的 lazy 為 true。dirty 的初始值等同於 lazy。所以在初始化頁面渲染,對計算屬性進行取值時,會執行一次 watcher.evaluate。

evaluate() {
  this.value = this.get()
  this.dirty = false
}

求值后將值賦給 this.value,上面 createComputedGetter 內的 watcher.value 就是在這裏更新。接着 dirty 置為 false,如果依賴屬性沒有變化,下一次取值時,是不會執行 watcher.evaluate 的, 而是直接就返回 watcher.value,這樣就實現了緩存機制。

更新

依賴屬性在更新時,會調用 dep.notify:

notify() {
  this.subs.forEach(watcher => watcher.update())
}

然後執行 watcher.update:

update() {
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}

由於“計算屬性Watcher”的 lazy 為 true,這裏 dirty 會置為 true。等到頁面渲染對計算屬性取值時,符合觸發點條件,執行 watcher.evaluate 重新求值,計算屬性隨之更新。

依賴屬性收集依賴

收集計算屬性Watcher

初始化時,頁面渲染會將“渲染Watcher”入棧,並掛載到Dep.target

在頁面渲染過程中遇到計算屬性,對其取值,因此執行 watcher.evaluate 的邏輯,接着調用 this.get:

get () {
  // 1
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    // 2
    value = this.getter.call(vm, vm) // 計算屬性求值
  } catch (e) {
    if (this.user) {
      handleError(e, vm, `getter for watcher "${this.expression}"`)
    } else {
      throw e
    }
  } finally {
    popTarget()
    this.cleanupDeps()
  }
  return value
}
Dep.target = null
let stack = []  // 存儲 watcher 的棧

export function pushTarget(watcher) {
  stack.push(watcher)
  Dep.target = watcher
} 

export function popTarget(){
  stack.pop()
  Dep.target = stack[stack.length - 1]
}

pushTarget 輪到“計算屬性Watcher”入棧,並掛載到Dep.target,此時棧中為 [渲染Watcher, 計算屬性Watcher]

this.getter 對計算屬性求值,在獲取依賴屬性時,觸發依賴屬性的 數據劫持get,執行 dep.depend 收集依賴(“計算屬性Watcher”)

收集渲染Watcher

this.getter 求值完成后popTragte,“計算屬性Watcher”出棧,Dep.target 設置為“渲染Watcher”,此時的 Dep.target 是“渲染Watcher”

if (Dep.target) {
  watcher.depend()
}

watcher.depend 收集依賴:

depend() {
  let i = this.deps.length
  while (i--) {
    this.deps[i].depend()
  }
}

deps 內存儲的是依賴屬性的 dep,這一步是依賴屬性收集依賴(“渲染Watcher”)

經過上面兩次收集依賴后,依賴屬性的 subs 存儲兩個 Watcher,[計算屬性Watcher,渲染Watcher]

為什麼依賴屬性要收集渲染Watcher

我在初次閱讀源碼時,很奇怪的是依賴屬性收集到“計算屬性Watcher”不就好了嗎?為什麼依賴屬性還要收集“渲染Watcher”?

第一種場景:模板里同時用到依賴屬性和計算屬性

<template>
  <div>{{msg}} {{msg1}}</div>
</template>

export default {
  data(){
    return {
      msg: 'hello'
    }
  },
  computed:{
    msg1(){
      return this.msg + ' world'      
    }
  }
}

模板有用到依賴屬性,在頁面渲染對依賴屬性取值時,依賴屬性就存儲了“渲染Watcher”,所以 watcher.depend 這步是屬於重複收集的,但 watcher 內部會去重。

這也是我為什麼會產生疑問的點,Vue 作為一個優秀的框架,這麼做肯定有它的道理。於是我想到了另一個場景能合理解釋 watcher.depend 的作用。

第二種場景:模板內只用到計算屬性

<template>
  <div>{{msg1}}</div>
</template>

export default {
  data(){
    return {
      msg: 'hello'
    }
  },
  computed:{
    msg1(){
      return this.msg + ' world'      
    }
  }
}

模板上沒有使用到依賴屬性,頁面渲染時,那麼依賴屬性是不會收集 “渲染Watcher”的。此時依賴屬性里只會有“計算屬性Watcher”,當依賴屬性被修改,只會觸發“計算屬性Watcher”的 update。而計算屬性的 update 里僅僅是將 dirty 設置為 true,並沒有立刻求值,那麼計算屬性也不會被更新。

所以需要收集“渲染Watcher”,在執行完“計算屬性Watcher”后,再執行“渲染Watcher”。頁面渲染對計算屬性取值,執行 watcher.evaluate 才會重新計算求值,頁面計算屬性更新。

總結

計算屬性原理和響應式原理都是大同小異的,同樣的是使用數據劫持以及依賴收集,不同的是計算屬性有做緩存優化,只有在依賴屬性變化時才會重新求值,其它情況都是直接返回緩存值。服務端不對計算屬性緩存。

計算屬性更新的前提需要“渲染Watcher”的配合,因此依賴屬性的 subs 中至少會存儲兩個 Watcher。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

mybatis緩存之一級緩存(一)

對於mybatis框架。彷彿工作中一直是在copy着使用。對於mybatis緩存。並沒有一個準確的認知。趁着假期。學習下mybatis的緩存。這篇主要學習mybatis的一級緩存。

為什麼使用緩存

其實,大家工作久了,就知道很多瓶頸就是在數據庫上。

初識mybatis一級緩存

當然我們還是通過代碼來認識下mybatis的一級緩存

代碼演示

詳細代碼見github,這裏只展示重要的代碼片段

  1. tempMapper.xml
    <select id="getById" resultType="entity.TempEntity">
       select * from  temp where id = #{id}
    </select>
  1. TempTest
public class TempTest {

    Logger logger = Logger.getLogger(this.getClass());
    @Test
    public  void test() throws IOException {
        InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
        SqlSessionFactory build = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession sqlSession = build.openSession();
        TempEntity tempEntity1 = sqlSession.selectOne("dao.TempDao.getById", 1);
        logger.info(tempEntity1);
        TempEntity tempEntity2 = sqlSession.selectOne("dao.TempDao.getById", 1);
        logger.info(tempEntity2);
        logger.info(tempEntity1 == tempEntity2);
    }
}

3.運行結果

2020-06-26 08:57:37,453 DEBUG [dao.TempDao.getById] - ==>  Preparing: select * from temp where id = ? 
2020-06-26 08:57:37,513 DEBUG [dao.TempDao.getById] - ==> Parameters: 1(Integer)
2020-06-26 08:57:37,538 DEBUG [dao.TempDao.getById] - <==      Total: 1
2020-06-26 08:57:37,538 INFO [TempTest] - TempEntity{id=1, value1='11111', value2='aaaaa'}
2020-06-26 08:57:37,538 INFO [TempTest] - TempEntity{id=1, value1='11111', value2='aaaaa'}
2020-06-26 08:57:37,538 INFO [TempTest] - true
  1. 總結

4.1 從上面的結果,我們可以看到,第二次查詢的時候,就直接沒有查詢數據庫,並且返回的是同一個對象。證明第二次走的就是緩存。
4.2 一級緩存是默認開啟的。我們並沒有在代碼中配置任何關於緩存的配置
4.3 代碼回顧

mybatis一級緩存命中原則

mybatis是怎麼樣判斷某兩次查詢是完全相同的查詢?

1.statementId

1.1 mapper.xml

    <select id="getById1" resultType="entity.TempEntity">
       select * from  temp where id = #{id}
    </select>

    <select id="getById2" resultType="entity.TempEntity">
       select * from  temp where id = #{id}
    </select>

1.2 test

    @Test
    public  void test() throws IOException {
        InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
        SqlSessionFactory build = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession sqlSession = build.openSession();
        TempEntity tempEntity1 = sqlSession.selectOne("dao.Temp2Dao.getById1", 1);
        logger.info(tempEntity1);
        TempEntity tempEntity2 = sqlSession.selectOne("dao.Temp2Dao.getById2", 1);
        logger.info(tempEntity2);
        logger.info(tempEntity1 == tempEntity2);
    }

1.3 結果

2020-06-26 09:19:09,926 DEBUG [dao.Temp2Dao.getById1] - ==>  Preparing: select * from temp where id = ? 
2020-06-26 09:19:09,957 DEBUG [dao.Temp2Dao.getById1] - ==> Parameters: 1(Integer)
2020-06-26 09:19:09,969 DEBUG [dao.Temp2Dao.getById1] - <==      Total: 1
2020-06-26 09:19:09,969 INFO [TempTest] - TempEntity{id=1, value1='11111', value2='aaaaa'}
2020-06-26 09:19:09,969 DEBUG [dao.Temp2Dao.getById2] - ==>  Preparing: select * from temp where id = ? 
2020-06-26 09:19:09,970 DEBUG [dao.Temp2Dao.getById2] - ==> Parameters: 1(Integer)
2020-06-26 09:19:09,970 DEBUG [dao.Temp2Dao.getById2] - <==      Total: 1
2020-06-26 09:19:09,971 INFO [TempTest] - TempEntity{id=1, value1='11111', value2='aaaaa'}

1.4 總結
要求查詢的statementId必須完全相同,否則無法命中緩存,即時兩個查詢語句、參數完全相同

2.查詢參數

我們用不同的參數查詢,一個傳1 一個傳2
2.1 test

    @Test
    public  void testParam() throws IOException {
        InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
        SqlSessionFactory build = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession sqlSession = build.openSession();
        TempEntity tempEntity1 = sqlSession.selectOne("dao.Temp2Dao.getById1", 1);
        logger.info(tempEntity1);
        TempEntity tempEntity2 = sqlSession.selectOne("dao.Temp2Dao.getById1", 2);
        logger.info(tempEntity2);
        logger.info(tempEntity1 == tempEntity2);
    }

2.2 結果

2020-06-26 09:24:33,107 DEBUG [dao.Temp2Dao.getById1] - ==>  Preparing: select * from temp where id = ? 
2020-06-26 09:24:33,148 DEBUG [dao.Temp2Dao.getById1] - ==> Parameters: 1(Integer)
2020-06-26 09:24:33,162 DEBUG [dao.Temp2Dao.getById1] - <==      Total: 1
2020-06-26 09:24:33,162 INFO [TempTest] - TempEntity{id=1, value1='11111', value2='aaaaa'}
2020-06-26 09:24:33,162 DEBUG [dao.Temp2Dao.getById1] - ==>  Preparing: select * from temp where id = ? 
2020-06-26 09:24:33,163 DEBUG [dao.Temp2Dao.getById1] - ==> Parameters: 2(Integer)
2020-06-26 09:24:33,164 DEBUG [dao.Temp2Dao.getById1] - <==      Total: 1
2020-06-26 09:24:33,164 INFO [TempTest] - TempEntity{id=2, value1='22222', value2='bbbb'}
2020-06-26 09:24:33,164 INFO [TempTest] - false


2.3 總結

要求傳遞給sql的傳遞參數相同,否則不會命中緩存

3.分頁參數

3.1 傳不同的分頁參數

    @Test
    public  void testPage() throws IOException {
        InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
        SqlSessionFactory build = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession sqlSession = build.openSession();
        RowBounds rowBounds1 = new RowBounds(0,1);
        List<TempEntity> tempEntity1 = sqlSession.selectList("dao.Temp2Dao.getList", null,rowBounds1);
        logger.info(tempEntity1);
        RowBounds rowBounds2 = new RowBounds(0,2);
        List<TempEntity> tempEntity2 = sqlSession.selectList("dao.Temp2Dao.getList",null, rowBounds2);
        logger.info(tempEntity2);
        logger.info(tempEntity1 == tempEntity2);
    }

3.2 結果

2020-06-26 10:10:33,060 DEBUG [dao.Temp2Dao.getList] - ==>  Preparing: select * from temp where 1=1 
2020-06-26 10:10:33,101 DEBUG [dao.Temp2Dao.getList] - ==> Parameters: 
2020-06-26 10:10:33,116 INFO [TempTest] - [TempEntity{id=1, value1='11111', value2='aaaaa'}]
2020-06-26 10:10:33,116 DEBUG [dao.Temp2Dao.getList] - ==>  Preparing: select * from temp where 1=1 
2020-06-26 10:10:33,116 DEBUG [dao.Temp2Dao.getList] - ==> Parameters: 
2020-06-26 10:10:33,118 INFO [TempTest] - [TempEntity{id=1, value1='11111', value2='aaaaa'}, TempEntity{id=2, value1='22222', value2='bbbb'}]
2020-06-26 10:10:33,118 INFO [TempTest] - false

3.3 總結

要求分頁參數必須相同,否則無法命中緩存。緩存的粒度是整個分頁查詢結果,而不是結果中的每個對象

4. sql語句

4.1 mapper文件

    <select id="getById" resultType="entity.TempEntity">
       select * from  temp 
       <where>
           <if test="type ==1">
           id = #{id}
            </if>
           <if test="type ==2">
               1=1 and id = #{id}
           </if>
       </where> 
    </select>

這個就不測試了。
4.2 總結
要求傳遞給jdbc的sql 必須完全相同。就算是1=1 不起作用 也不行

5.環境

這裏的環境指的的是<environment id="dev"> 和 <environment id="test"> 也是會影響的

    <environments default="dev">
        <environment id="dev">
            <!--指定事務管理的類型,這裏簡單使用Java的JDBC的提交和回滾設置-->
            <transactionManager type="JDBC"></transactionManager>
            <!--dataSource 指連接源配置,POOLED是JDBC連接對象的數據源連接池的實現-->
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"></property>
                <property name="url" value="jdbc:mysql://127.0.0.1:3306/test"></property>
                <property name="username" value="root"></property>
                <property name="password" value="root"></property>
            </dataSource>
        </environment>
        <environment id="test">
            <!--指定事務管理的類型,這裏簡單使用Java的JDBC的提交和回滾設置-->
            <transactionManager type="JDBC"></transactionManager>
            <!--dataSource 指連接源配置,POOLED是JDBC連接對象的數據源連接池的實現-->
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"></property>
                <property name="url" value="jdbc:mysql://127.0.0.1:3306/test"></property>
                <property name="username" value="root"></property>
                <property name="password" value="root"></property>
            </dataSource>
        </environment>
    </environments>

總結

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※幫你省時又省力,新北清潔一流服務好口碑

※別再煩惱如何寫文案,掌握八大原則!

Alink漫談(八) : 二分類評估 AUC、K-S、PRC、Precision、Recall、LiftChart 如何實現

Alink漫談(八) : 二分類評估 AUC、K-S、PRC、Precision、Recall、LiftChart 如何實現

目錄

  • Alink漫談(八) : 二分類評估 AUC、K-S、PRC、Precision、Recall、LiftChart 如何實現
    • 0x00 摘要
    • 0x01 相關概念
    • 0x02 示例代碼
      • 2.1 主要思路
    • 0x03 批處理
      • 3.1 EvalBinaryClassBatchOp
      • 3.2 BaseEvalClassBatchOp
        • 3.2.0 調用關係綜述
        • 3.2.1 calLabelPredDetailLocal
          • 3.2.1.1 flatMap
          • 3.2.1.2 reduceGroup
          • 3.2.1.3 mapPartition
        • 3.2.2 ReduceBaseMetrics
        • 3.2.3 SaveDataAsParams
        • 3.2.4 計算混淆矩陣
          • 3.2.4.1 原始矩陣
          • 3.2.4.2 計算標籤
          • 3.2.4.3 具體代碼
    • 0x04 流處理
      • 4.1 示例
        • 4.1.1 主類
        • 4.1.2 TimeMemSourceStreamOp
        • 4.1.3 Source
      • 4.2 BaseEvalClassStreamOp
        • 4.2.1 PredDetailLabel
        • 4.2.2 AllDataMerge
        • 4.2.3 SaveDataStream
        • 4.2.4 Union
          • 4.2.4.1 allOutput
        • 4.2.4.2 windowOutput
    • 0xFF 參考

0x00 摘要

Alink 是阿里巴巴基於實時計算引擎 Flink 研發的新一代機器學習算法平台,是業界首個同時支持批式算法、流式算法的機器學習平台。二分類評估是對二分類算法的預測結果進行效果評估。本文將剖析Alink中對應代碼實現。

0x01 相關概念

如果對本文某些概念有疑惑,可以參見之前文章 [白話解析] 通過實例來梳理概念 :準確率 (Accuracy)、精準率(Precision)、召回率(Recall) 和 F值(F-Measure)

0x02 示例代碼

public class EvalBinaryClassExample {

    AlgoOperator getData(boolean isBatch) {
        Row[] rows = new Row[]{
                Row.of("prefix1", "{\"prefix1\": 0.9, \"prefix0\": 0.1}"),
                Row.of("prefix1", "{\"prefix1\": 0.8, \"prefix0\": 0.2}"),
                Row.of("prefix1", "{\"prefix1\": 0.7, \"prefix0\": 0.3}"),
                Row.of("prefix0", "{\"prefix1\": 0.75, \"prefix0\": 0.25}"),
                Row.of("prefix0", "{\"prefix1\": 0.6, \"prefix0\": 0.4}")
        };

        String[] schema = new String[]{"label", "detailInput"};

        if (isBatch) {
            return new MemSourceBatchOp(rows, schema);
        } else {
            return new MemSourceStreamOp(rows, schema);
        }
    }

    public static void main(String[] args) throws Exception {
        EvalBinaryClassExample test = new EvalBinaryClassExample();
        BatchOperator batchData = (BatchOperator) test.getData(true);

        BinaryClassMetrics metrics = new EvalBinaryClassBatchOp()
                .setLabelCol("label")
                .setPredictionDetailCol("detailInput")
                .linkFrom(batchData)
                .collectMetrics();

        System.out.println("RocCurve:" + metrics.getRocCurve());
        System.out.println("AUC:" + metrics.getAuc());
        System.out.println("KS:" + metrics.getKs());
        System.out.println("PRC:" + metrics.getPrc());
        System.out.println("Accuracy:" + metrics.getAccuracy());
        System.out.println("Macro Precision:" + metrics.getMacroPrecision());
        System.out.println("Micro Recall:" + metrics.getMicroRecall());
        System.out.println("Weighted Sensitivity:" + metrics.getWeightedSensitivity());
    }
}

程序輸出

RocCurve:([0.0, 0.0, 0.0, 0.5, 0.5, 1.0, 1.0],[0.0, 0.3333333333333333, 0.6666666666666666, 0.6666666666666666, 1.0, 1.0, 1.0])
AUC:0.8333333333333333
KS:0.6666666666666666
PRC:0.9027777777777777
Accuracy:0.6
Macro Precision:0.3
Micro Recall:0.6
Weighted Sensitivity:0.6

在 Alink 中,二分類評估有批處理,流處理兩種實現,下面一一為大家介紹( Alink 複雜之一在於大量精細的數據結構,所以下文會大量打印程序中變量以便大家理解)。

2.1 主要思路

  • 把 [0,1] 分成假設 100000個桶(bin)。所以得到positiveBin / negativeBin 兩個100000的數組。

  • 根據輸入給positiveBin / negativeBin賦值。positiveBin就是 TP + FP,negativeBin就是 TN + FN。這些是後續計算的基礎。

  • 遍歷bins中每一個有意義的點,計算出totalTrue和totalFalse,並且在每一個點上計算該點的混淆矩陣,tpr,以及rocCurve,recallPrecisionCurve,liftChart在該點對應的數據;

  • 依據曲線內容計算並且存儲 AUC/PRC/KS

具體後續還有詳細調用關係綜述。

0x03 批處理

3.1 EvalBinaryClassBatchOp

EvalBinaryClassBatchOp是二分類評估的實現,功能是計算二分類的評估指標(evaluation metrics)。

輸入有兩種:

  • label column and predResult column
  • label column and predDetail column。如果有predDetail,則predResult被忽略

我們例子中 "prefix1" 就是 label,"{\"prefix1\": 0.9, \"prefix0\": 0.1}" 就是 predDetail

Row.of("prefix1", "{\"prefix1\": 0.9, \"prefix0\": 0.1}")

具體類摘錄如下:

public class EvalBinaryClassBatchOp extends BaseEvalClassBatchOp<EvalBinaryClassBatchOp> implements BinaryEvaluationParams <EvalBinaryClassBatchOp>, EvaluationMetricsCollector<BinaryClassMetrics> {
  
	@Override
	public BinaryClassMetrics collectMetrics() {
		return new BinaryClassMetrics(this.collect().get(0));
	}  
}

可以看到,其主要工作都是在基類BaseEvalClassBatchOp中完成,所以我們會首先看BaseEvalClassBatchOp。

3.2 BaseEvalClassBatchOp

我們還是從 linkFrom 函數入手,其主要是做了幾件事:

  • 獲取配置信息
  • 從輸入中提取某些列:”label”,”detailInput”
  • calLabelPredDetailLocal會按照partition分別計算evaluation metrics
  • 綜合reduce上述計算結果
  • SaveDataAsParams函數會把最終數值輸入到 output table

具體代碼如下

@Override
public T linkFrom(BatchOperator<?>... inputs) {
    BatchOperator<?> in = checkAndGetFirst(inputs);
    String labelColName = this.get(MultiEvaluationParams.LABEL_COL);
    String positiveValue = this.get(BinaryEvaluationParams.POS_LABEL_VAL_STR);

    // Judge the evaluation type from params.
    ClassificationEvaluationUtil.Type type = ClassificationEvaluationUtil.judgeEvaluationType(this.getParams());

    DataSet<BaseMetricsSummary> res;
    switch (type) {
        case PRED_DETAIL: {
            String predDetailColName = this.get(MultiEvaluationParams.PREDICTION_DETAIL_COL);
            // 從輸入中提取某些列:"label","detailInput" 
            DataSet<Row> data = in.select(new String[] {labelColName, predDetailColName}).getDataSet();
            // 按照partition分別計算evaluation metrics
            res = calLabelPredDetailLocal(data, positiveValue, binary);
            break;
        }
        ......
    }

    // 綜合reduce上述計算結果
    DataSet<BaseMetricsSummary> metrics = res
        .reduce(new EvaluationUtil.ReduceBaseMetrics());

    // 把最終數值輸入到 output table
    this.setOutput(metrics.flatMap(new EvaluationUtil.SaveDataAsParams()),
        new String[] {DATA_OUTPUT}, new TypeInformation[] {Types.STRING});

    return (T)this;
}

// 執行中一些變量如下
labelColName = "label"
predDetailColName = "detailInput"  
type = {ClassificationEvaluationUtil$Type@2532} "PRED_DETAIL"
binary = true
positiveValue = null  

3.2.0 調用關係綜述

因為後續代碼調用關係複雜,所以先給出一個調用關係:

  • 從輸入中提取某些列:”label”,”detailInput”,in.select(new String[] {labelColName, predDetailColName}).getDataSet()。因為可能輸入還有其他列,而只有某些列是我們計算需要的,所以只提取這些列。
  • 按照partition分別計算evaluation metrics,即調用 calLabelPredDetailLocal(data, positiveValue, binary);
    • flatMap會從label列和prediction列中,取出所有labels(注意是取出labels的名字 ),發送給下游算子。
    • reduceGroup主要功能是通過 buildLabelIndexLabelArray 去重 “labels名字”,然後給每一個label一個ID,得到一個 <labels, ID>的map,最後返回是二元組(map, labels),即({prefix1=0, prefix0=1},[prefix1, prefix0])。從後文看,<labels, ID>Map看來是多分類才用到。二分類只用到了labels。
    • mapPartition 分區調用 CalLabelDetailLocal 來計算混淆矩陣,主要是分區調用getDetailStatistics,前文中得到的二元組(map, labels)會作為參數傳遞進來 。
      • getDetailStatistics 遍歷 rows 數據,提取每一個item(比如 “prefix1,{“prefix1”: 0.8, “prefix0”: 0.2}”),然後通過updateBinaryMetricsSummary累積計算混淆矩陣所需數據。
        • updateBinaryMetricsSummary 把 [0,1] 分成假設 100000個桶(bin)。所以得到positiveBin / negativeBin 兩個100000的數組。positiveBin就是 TP + FP,negativeBin就是 TN + FN。
          • 如果某個 sample 為 正例 (positive value) 的概率是 p, 則該 sample 對應的 bin index 就是 p * 100000。如果 p 被預測為正例 (positive value) ,則positiveBin[index]++,
          • 否則就是被預測為負例(negative value) ,則negativeBin[index]++。
  • 綜合reduce上述計算結果,metrics = res.reduce(new EvaluationUtil.ReduceBaseMetrics());
    • 具體計算是在BinaryMetricsSummary.merge,其作用就是Merge the bins, and add the logLoss。
  • 把最終數值輸入到 output table,setOutput(metrics.flatMap(new EvaluationUtil.SaveDataAsParams()..);
    • 歸併所有BaseMetrics后,得到total BaseMetrics,計算indexes存入params。collector.collect(t.toMetrics().serialize());
      • 實際業務在BinaryMetricsSummary.toMetrics,即基於bin的信息計算,然後存儲到params。
        • extractMatrixThreCurve函數取出非空的bins,據此計算出ConfusionMatrix array(混淆矩陣), threshold array, rocCurve/recallPrecisionCurve/LiftChart.
          • 遍歷bins中每一個有意義的點,計算出totalTrue和totalFalse,並且在每一個點上計算:
          • curTrue += positiveBin[index]; curFalse += negativeBin[index];
          • 得到該點的混淆矩陣 new ConfusionMatrix(new long[][] {{curTrue, curFalse}, {totalTrue – curTrue, totalFalse – curFalse}});
          • 得到 tpr = (totalTrue == 0 ? 1.0 : 1.0 * curTrue / totalTrue);
          • rocCurve,recallPrecisionCurve,liftChart在該點對應的數據;
        • 依據曲線內容計算並且存儲 AUC/PRC/KS
        • 對生成的rocCurve/recallPrecisionCurve/LiftChart輸出進行抽樣
        • 依據抽樣后的輸出存儲 RocCurve/RecallPrecisionCurve/LiftChar
        • 存儲正例樣本的度量指標
        • 存儲Logloss
        • Pick the middle point where threshold is 0.5.

3.2.1 calLabelPredDetailLocal

本函數按照partition分別計算評估指標 evaluation metrics。是的,這代碼很短,但是有個地方需要注意。有時候越簡單的地方越容易疏漏。容易疏漏點是:

第一行代碼的結果 labels 是第二行代碼的參數,而並非第二行主體。第二行代碼主體和第一行代碼主體一樣,都是data。

private static DataSet<BaseMetricsSummary> calLabelPredDetailLocal(DataSet<Row> data, final String positiveValue, oolean binary) {
  
    DataSet<Tuple2<Map<String, Integer>, String[]>> labels = data.flatMap(new FlatMapFunction<Row, String>() {
        @Override
        public void flatMap(Row row, Collector<String> collector) {
            TreeMap<String, Double> labelProbMap;
            if (EvaluationUtil.checkRowFieldNotNull(row)) {
                labelProbMap = EvaluationUtil.extractLabelProbMap(row);
                labelProbMap.keySet().forEach(collector::collect);
                collector.collect(row.getField(0).toString());
            }
        }
    }).reduceGroup(new EvaluationUtil.DistinctLabelIndexMap(binary, positiveValue));

    return data
        .rebalance()
        .mapPartition(new CalLabelDetailLocal(binary))
        .withBroadcastSet(labels, LABELS);
}

calLabelPredDetailLocal中具體分為三步驟:

  • 在flatMap會從label列和prediction列中,取出所有labels(注意是取出labels的名字 ),發送給下游算子。
  • reduceGroup的主要功能是去重 “labels名字”,然後給每一個label一個ID,最後結果是一個<labels, ID>Map。
  • mapPartition 是分區調用 CalLabelDetailLocal 來計算混淆矩陣。

下面具體看看。

3.2.1.1 flatMap

在flatMap中,主要是從label列和prediction列中,取出所有labels(注意是取出labels的名字 ),發送給下游算子。

EvaluationUtil.extractLabelProbMap 作用就是解析輸入的json,獲得具體detailInput中的信息。

下游算子是reduceGroup,所以Flink runtime會對這些labels自動去重。如果對這部分有興趣,可以參見我之前介紹reduce的文章。CSDN : [源碼解析] Flink的groupBy和reduce究竟做了什麼 博客園 : [源碼解析] Flink的groupBy和reduce究竟做了什麼

程序中變量如下

row = {Row@8922} "prefix1,{"prefix1": 0.9, "prefix0": 0.1}"
 fields = {Object[2]@8925} 
  0 = "prefix1"
  1 = "{"prefix1": 0.9, "prefix0": 0.1}"
    
labelProbMap = {TreeMap@9008}  size = 2
 "prefix0" -> {Double@9015} 0.1
 "prefix1" -> {Double@9017} 0.9
    
labelProbMap.keySet().forEach(collector::collect); //這裏發送 "prefix0", "prefix1" 
collector.collect(row.getField(0).toString());  // 這裏發送 "prefix1"   
// 因為下一個操作是reduceGroup,所以這些label會被runtime去重
3.2.1.2 reduceGroup

主要功能是通過buildLabelIndexLabelArray去重labels,然後給每一個label一個ID,最後結果是一個<labels, ID>的Map。

reduceGroup(new EvaluationUtil.DistinctLabelIndexMap(binary, positiveValue));

DistinctLabelIndexMap的作用是從label列和prediction列中,取出所有不同的labels,返回一個<labels, ID>的map,根據後續代碼看,這個map是多分類才用到。Get all the distinct labels from label column and prediction column, and return the map of labels and their IDs.

前面已經提到,這裏的參數rows已經被自動去重。

public static class DistinctLabelIndexMap implements
    GroupReduceFunction<String, Tuple2<Map<String, Integer>, String[]>> {
    ......
    @Override
    public void reduce(Iterable<String> rows, Collector<Tuple2<Map<String, Integer>, String[]>> collector) throws Exception {
        HashSet<String> labels = new HashSet<>();
        rows.forEach(labels::add);
        collector.collect(buildLabelIndexLabelArray(labels, binary, positiveValue));
    }
}

// 變量為
labels = {HashSet@9008}  size = 2
 0 = "prefix1"
 1 = "prefix0"
binary = true

buildLabelIndexLabelArray的作用是給每一個label一個ID,得到一個 <labels, ID>的map,最後返回是二元組(map, labels),即({prefix1=0, prefix0=1},[prefix1, prefix0])。

// Give each label an ID, return a map of label and ID.
public static Tuple2<Map<String, Integer>, String[]> buildLabelIndexLabelArray(HashSet<String> set,boolean binary, String positiveValue) {
    String[] labels = set.toArray(new String[0]);
    Arrays.sort(labels, Collections.reverseOrder());

    Map<String, Integer> map = new HashMap<>(labels.length);
    if (binary && null != positiveValue) {
        if (labels[1].equals(positiveValue)) {
            labels[1] = labels[0];
            labels[0] = positiveValue;
        } 
        map.put(labels[0], 0);
        map.put(labels[1], 1);
    } else {
        for (int i = 0; i < labels.length; i++) {
            map.put(labels[i], i);
        }
    }
    return Tuple2.of(map, labels);
}

// 程序變量如下
labels = {String[2]@9013} 
 0 = "prefix1"
 1 = "prefix0"
map = {HashMap@9014}  size = 2
 "prefix1" -> {Integer@9020} 0
 "prefix0" -> {Integer@9021} 1
3.2.1.3 mapPartition

這裏主要功能是分區調用 CalLabelDetailLocal 來為後來計算混淆矩陣做準備。

return data
    .rebalance()
    .mapPartition(new CalLabelDetailLocal(binary)) //這裡是業務所在
    .withBroadcastSet(labels, LABELS);

具體工作是 CalLabelDetailLocal 完成的,其作用是分區調用getDetailStatistics

// Calculate the confusion matrix based on the label and predResult.
static class CalLabelDetailLocal extends RichMapPartitionFunction<Row, BaseMetricsSummary> {
        private Tuple2<Map<String, Integer>, String[]> map;
        private boolean binary;

        @Override
        public void open(Configuration parameters) throws Exception {
            List<Tuple2<Map<String, Integer>, String[]>> list = getRuntimeContext().getBroadcastVariable(LABELS);
            this.map = list.get(0);// 前文生成的二元組(map, labels)
        }

        @Override
        public void mapPartition(Iterable<Row> rows, Collector<BaseMetricsSummary> collector) {
            // 調用到了 getDetailStatistics
            collector.collect(getDetailStatistics(rows, binary, map));
        }
    }  

getDetailStatistics 的作用是:初始化分類評估的度量指標 base classification evaluation metrics,累積計算混淆矩陣需要的數據。主要就是遍歷 rows 數據,提取每一個item(比如 “prefix1,{“prefix1”: 0.8, “prefix0”: 0.2}”),然後累積計算混淆矩陣所需數據。

// Initialize the base classification evaluation metrics. There are two cases: BinaryClassMetrics and MultiClassMetrics.
    private static BaseMetricsSummary getDetailStatistics(Iterable<Row> rows,
                                         String positiveValue,
                                         boolean binary,
                                         Tuple2<Map<String, Integer>, String[]> tuple) {
        BinaryMetricsSummary binaryMetricsSummary = null;
        MultiMetricsSummary multiMetricsSummary = null;
        Tuple2<Map<String, Integer>, String[]> labelIndexLabelArray = tuple;  // 前文生成的二元組(map, labels)

        Iterator<Row> iterator = rows.iterator();
        Row row = null;
        while (iterator.hasNext() && !checkRowFieldNotNull(row)) {
            row = iterator.next();
        }

        Map<String, Integer> labelIndexMap = null;
        if (binary) {
           // 二分法在這裏 
            binaryMetricsSummary = new BinaryMetricsSummary(
                new long[ClassificationEvaluationUtil.DETAIL_BIN_NUMBER],
                new long[ClassificationEvaluationUtil.DETAIL_BIN_NUMBER],
                labelIndexLabelArray.f1, 0.0, 0L);
        } else {
            // 
            labelIndexMap = labelIndexLabelArray.f0; // 前文生成的<labels, ID>Map看來是多分類才用到。
            multiMetricsSummary = new MultiMetricsSummary(
                new long[labelIndexMap.size()][labelIndexMap.size()],
                labelIndexLabelArray.f1, 0.0, 0L);
        }

        while (null != row) {
            if (checkRowFieldNotNull(row)) {
                TreeMap<String, Double> labelProbMap = extractLabelProbMap(row);
                String label = row.getField(0).toString();
                if (ArrayUtils.indexOf(labelIndexLabelArray.f1, label) >= 0) {
                    if (binary) {
                        // 二分法在這裏 
                        updateBinaryMetricsSummary(labelProbMap, label, binaryMetricsSummary);
                    } else {
                        updateMultiMetricsSummary(labelProbMap, label, labelIndexMap, multiMetricsSummary);
                    }
                }
            }
            row = iterator.hasNext() ? iterator.next() : null;
        }

        return binary ? binaryMetricsSummary : multiMetricsSummary;
}

//變量如下
tuple = {Tuple2@9252} "({prefix1=0, prefix0=1},[prefix1, prefix0])"
 f0 = {HashMap@9257}  size = 2
  "prefix1" -> {Integer@9264} 0
  "prefix0" -> {Integer@9266} 1
 f1 = {String[2]@9258} 
  0 = "prefix1"
  1 = "prefix0"
 
row = {Row@9271} "prefix1,{"prefix1": 0.8, "prefix0": 0.2}"
 fields = {Object[2]@9276} 
  0 = "prefix1"
  1 = "{"prefix1": 0.8, "prefix0": 0.2}"
    
labelIndexLabelArray = {Tuple2@9240} "({prefix1=0, prefix0=1},[prefix1, prefix0])"
 f0 = {HashMap@9288}  size = 2
  "prefix1" -> {Integer@9294} 0
  "prefix0" -> {Integer@9296} 1
 f1 = {String[2]@9242} 
  0 = "prefix1"
  1 = "prefix0"
    
labelProbMap = {TreeMap@9342}  size = 2
 "prefix0" -> {Double@9378} 0.1
 "prefix1" -> {Double@9380} 0.9    

先回憶下混淆矩陣:

預測值 0 預測值 1
真實值 0 TN FP
真實值 1 FN TP

針對混淆矩陣,BinaryMetricsSummary 的作用是Save the evaluation data for binary classification。函數具體計算思路是:

  • 把 [0,1] 分成ClassificationEvaluationUtil.DETAIL_BIN_NUMBER(100000)這麼多桶(bin)。所以binaryMetricsSummary的positiveBin/negativeBin分別是兩個100000的數組。如果某一個 sample 為 正例(positive value) 的概率是 p, 則該 sample 對應的 bin index 就是 p * 100000。如果 p 被預測為正例(positive value) ,則positiveBin[index]++,否則就是被預測為負例(negative value) ,則negativeBin[index]++。positiveBin就是 TP + FP,negativeBin就是 TN + FN。

  • 所以這裡會遍歷輸入,如果某一個輸入(以"prefix1", "{\"prefix1\": 0.9, \"prefix0\": 0.1}"為例),0.9 是prefix1(正例) 的概率,0.1 是為prefix0(負例) 的概率。

    • 既然這個算法選擇了 prefix1(正例) ,所以就說明此算法是判別成 positive 的,所以在 positiveBin 的 90000 處 + 1。
    • 假設這個算法選擇了 prefix0(負例) ,則說明此算法是判別成 negative 的,所以應該在 negativeBin 的 90000 處 + 1。

具體對應我們示例代碼的5個採樣,分類如下:

Row.of("prefix1", "{\"prefix1\": 0.9, \"prefix0\": 0.1}"),  positiveBin 90000處+1
Row.of("prefix1", "{\"prefix1\": 0.8, \"prefix0\": 0.2}"),  positiveBin 80000處+1
Row.of("prefix1", "{\"prefix1\": 0.7, \"prefix0\": 0.3}"),  positiveBin 70000處+1
Row.of("prefix0", "{\"prefix1\": 0.75, \"prefix0\": 0.25}"), negativeBin 75000處+1
Row.of("prefix0", "{\"prefix1\": 0.6, \"prefix0\": 0.4}")  negativeBin 60000處+1

具體代碼如下

public static void updateBinaryMetricsSummary(TreeMap<String, Double> labelProbMap,
                                              String label,
                                              BinaryMetricsSummary binaryMetricsSummary) {
    binaryMetricsSummary.total++;
    binaryMetricsSummary.logLoss += extractLogloss(labelProbMap, label);

    double d = labelProbMap.get(binaryMetricsSummary.labels[0]);
    int idx = d == 1.0 ? ClassificationEvaluationUtil.DETAIL_BIN_NUMBER - 1 :
        (int)Math.floor(d * ClassificationEvaluationUtil.DETAIL_BIN_NUMBER);
    if (idx >= 0 && idx < ClassificationEvaluationUtil.DETAIL_BIN_NUMBER) {
        if (label.equals(binaryMetricsSummary.labels[0])) {
            binaryMetricsSummary.positiveBin[idx] += 1;
        } else if (label.equals(binaryMetricsSummary.labels[1])) {
            binaryMetricsSummary.negativeBin[idx] += 1;
        } else {
					.....
        }
    }
}

private static double extractLogloss(TreeMap<String, Double> labelProbMap, String label) {
   Double prob = labelProbMap.get(label);
   prob = null == prob ? 0. : prob;
   return -Math.log(Math.max(Math.min(prob, 1 - LOG_LOSS_EPS), LOG_LOSS_EPS));
}

// 變量如下
ClassificationEvaluationUtil.DETAIL_BIN_NUMBER=100000
  
// 當 "prefix1", "{\"prefix1\": 0.9, \"prefix0\": 0.1}" 時候
labelProbMap = {TreeMap@9305}  size = 2
 "prefix0" -> {Double@9331} 0.1
 "prefix1" -> {Double@9333} 0.9
  
d = 0.9
idx = 90000
binaryMetricsSummary = {BinaryMetricsSummary@9262} 
 labels = {String[2]@9242} 
  0 = "prefix1"
  1 = "prefix0"
 total = 1
 positiveBin = {long[100000]@9263}  // 90000處+1
 negativeBin = {long[100000]@9264} 
 logLoss = 0.10536051565782628
   
// 當 "prefix0", "{\"prefix1\": 0.6, \"prefix0\": 0.4}" 時候  
labelProbMap = {TreeMap@9514}  size = 2
 "prefix0" -> {Double@9546} 0.4
 "prefix1" -> {Double@9547} 0.6
   
d = 0.6
idx = 60000    
 binaryMetricsSummary = {BinaryMetricsSummary@9262} 
 labels = {String[2]@9242} 
  0 = "prefix1"
  1 = "prefix0"
 total = 2
 positiveBin = {long[100000]@9263}  
 negativeBin = {long[100000]@9264} // 60000處+1
 logLoss = 1.0216512475319812  

3.2.2 ReduceBaseMetrics

ReduceBaseMetrics作用是把局部計算的 BaseMetrics 聚合起來。

DataSet<BaseMetricsSummary> metrics = res
    .reduce(new EvaluationUtil.ReduceBaseMetrics());

ReduceBaseMetrics如下

public static class ReduceBaseMetrics implements ReduceFunction<BaseMetricsSummary> {
    @Override
    public BaseMetricsSummary reduce(BaseMetricsSummary t1, BaseMetricsSummary t2) throws Exception {
        return null == t1 ? t2 : t1.merge(t2);
    }
}

具體計算是在BinaryMetricsSummary.merge,其作用就是Merge the bins, and add the logLoss。

@Override
public BinaryMetricsSummary merge(BinaryMetricsSummary binaryClassMetrics) {
    for (int i = 0; i < this.positiveBin.length; i++) {
        this.positiveBin[i] += binaryClassMetrics.positiveBin[i];
    }
    for (int i = 0; i < this.negativeBin.length; i++) {
        this.negativeBin[i] += binaryClassMetrics.negativeBin[i];
    }
    this.logLoss += binaryClassMetrics.logLoss;
    this.total += binaryClassMetrics.total;
    return this;
}

// 程序變量是
this = {BinaryMetricsSummary@9316} 
 labels = {String[2]@9322} 
  0 = "prefix1"
  1 = "prefix0"
 total = 2
 positiveBin = {long[100000]@9320} 
 negativeBin = {long[100000]@9323} 
 logLoss = 1.742969305058623

3.2.3 SaveDataAsParams

this.setOutput(metrics.flatMap(new EvaluationUtil.SaveDataAsParams()),
    new String[] {DATA_OUTPUT}, new TypeInformation[] {Types.STRING});

當歸併所有BaseMetrics之後,得到了total BaseMetrics,計算indexes,存入到params。

public static class SaveDataAsParams implements FlatMapFunction<BaseMetricsSummary, Row> {
    @Override
    public void flatMap(BaseMetricsSummary t, Collector<Row> collector) throws Exception {
        collector.collect(t.toMetrics().serialize());
    }
}

實際業務在BinaryMetricsSummary.toMetrics中完成,即基於bin的信息計算,得到confusionMatrix array, threshold array, rocCurve/recallPrecisionCurve/LiftChart等等,然後存儲到params。

public BinaryClassMetrics toMetrics() {
    Params params = new Params();
    // 生成若干曲線,比如rocCurve/recallPrecisionCurve/LiftChart
    Tuple3<ConfusionMatrix[], double[], EvaluationCurve[]> matrixThreCurve =
        extractMatrixThreCurve(positiveBin, negativeBin, total);

    // 依據曲線內容計算並且存儲 AUC/PRC/KS
    setCurveAreaParams(params, matrixThreCurve.f2);

    // 對生成的rocCurve/recallPrecisionCurve/LiftChart輸出進行抽樣
    Tuple3<ConfusionMatrix[], double[], EvaluationCurve[]> sampledMatrixThreCurve = sample(
        PROBABILITY_INTERVAL, matrixThreCurve);

    // 依據抽樣后的輸出存儲 RocCurve/RecallPrecisionCurve/LiftChar
    setCurvePointsParams(params, sampledMatrixThreCurve);
    ConfusionMatrix[] matrices = sampledMatrixThreCurve.f0;
  
    // 存儲正例樣本的度量指標
    setComputationsArrayParams(params, sampledMatrixThreCurve.f1, sampledMatrixThreCurve.f0);
  
    // 存儲Logloss
    setLoglossParams(params, logLoss, total);
  
    // Pick the middle point where threshold is 0.5.
    int middleIndex = getMiddleThresholdIndex(sampledMatrixThreCurve.f1);  
    setMiddleThreParams(params, matrices[middleIndex], labels);
    return new BinaryClassMetrics(params);
}

extractMatrixThreCurve是全文重點。這裡是 Extract the bins who are not empty, keep the middle threshold 0.5,然後初始化了 RocCurve, Recall-Precision Curve and Lift Curve,計算出ConfusionMatrix array(混淆矩陣), threshold array, rocCurve/recallPrecisionCurve/LiftChart.。

/**
 * Extract the bins who are not empty, keep the middle threshold 0.5.
 * Initialize the RocCurve, Recall-Precision Curve and Lift Curve.
 * RocCurve: (FPR, TPR), starts with (0,0). Recall-Precision Curve: (recall, precision), starts with (0, p), p is the precision with the lowest. LiftChart: (TP+FP/total, TP), starts with (0,0). confusion matrix = [TP FP][FN * TN].
 *
 * @param positiveBin positiveBins.
 * @param negativeBin negativeBins.
 * @param total       sample number
 * @return ConfusionMatrix array, threshold array, rocCurve/recallPrecisionCurve/LiftChart.
 */
static Tuple3<ConfusionMatrix[], double[], EvaluationCurve[]> extractMatrixThreCurve(long[] positiveBin, long[] negativeBin, long total) {
    ArrayList<Integer> effectiveIndices = new ArrayList<>();
    long totalTrue = 0, totalFalse = 0;
  
    // 計算totalTrue,totalFalse,effectiveIndices
    for (int i = 0; i < ClassificationEvaluationUtil.DETAIL_BIN_NUMBER; i++) {
        if (0L != positiveBin[i] || 0L != negativeBin[i]
            || i == ClassificationEvaluationUtil.DETAIL_BIN_NUMBER / 2) {
            effectiveIndices.add(i);
            totalTrue += positiveBin[i];
            totalFalse += negativeBin[i];
        }
    }

// 以我們例子,得到  
effectiveIndices = {ArrayList@9273}  size = 6
 0 = {Integer@9277} 50000 //這裏加入了中間點
 1 = {Integer@9278} 60000
 2 = {Integer@9279} 70000
 3 = {Integer@9280} 75000
 4 = {Integer@9281} 80000
 5 = {Integer@9282} 90000
totalTrue = 3
totalFalse = 2
  
    // 繼續初始化,生成若干curve
    final int length = effectiveIndices.size();
    final int newLen = length + 1;
    final double m = 1.0 / ClassificationEvaluationUtil.DETAIL_BIN_NUMBER;
    EvaluationCurvePoint[] rocCurve = new EvaluationCurvePoint[newLen];
    EvaluationCurvePoint[] recallPrecisionCurve = new EvaluationCurvePoint[newLen];
    EvaluationCurvePoint[] liftChart = new EvaluationCurvePoint[newLen];
    ConfusionMatrix[] data = new ConfusionMatrix[newLen];
    double[] threshold = new double[newLen];
    long curTrue = 0;
    long curFalse = 0;
  
// 以我們例子,得到 
length = 6
newLen = 7
m = 1.0E-5
  
    // 計算, 其中rocCurve,recallPrecisionCurve,liftChart 都可以從代碼中看出
    for (int i = 1; i < newLen; i++) {
        int index = effectiveIndices.get(length - i);
        curTrue += positiveBin[index];
        curFalse += negativeBin[index];
        threshold[i] = index * m;
        // 計算出混淆矩陣
        data[i] = new ConfusionMatrix(
            new long[][] {{curTrue, curFalse}, {totalTrue - curTrue, totalFalse - curFalse}});
        double tpr = (totalTrue == 0 ? 1.0 : 1.0 * curTrue / totalTrue);
        // 比如當 90000 這點,得到 curTrue = 1 curFalse = 0 i = 1 index = 90000 tpr = 0.3333333333333333。totalTrue = 3 totalFalse = 2, 
        // 我們也知道,TPR = TP / (TP + FN) ,所以可以計算 tpr = 1 / 3   
        rocCurve[i] = new EvaluationCurvePoint(totalFalse == 0 ? 1.0 : 1.0 * curFalse / totalFalse, tpr, threshold[i]);
        recallPrecisionCurve[i] = new EvaluationCurvePoint(tpr, curTrue + curTrue == 0 ? 1.0 : 1.0 * curTrue / (curTrue + curFalse), threshold[i]);
        liftChart[i] = new EvaluationCurvePoint(1.0 * (curTrue + curFalse) / total, curTrue, threshold[i]);
    }
  
// 以我們例子,得到 
curTrue = 3
curFalse = 2
  
threshold = {double[7]@9349} 
 0 = 0.0
 1 = 0.9
 2 = 0.8
 3 = 0.7500000000000001
 4 = 0.7000000000000001
 5 = 0.6000000000000001
 6 = 0.5  
   
rocCurve = {EvaluationCurvePoint[7]@9315} 
 1 = {EvaluationCurvePoint@9440} 
  x = 0.0
  y = 0.3333333333333333
  p = 0.9
 2 = {EvaluationCurvePoint@9448} 
  x = 0.0
  y = 0.6666666666666666
  p = 0.8
 3 = {EvaluationCurvePoint@9449} 
  x = 0.5
  y = 0.6666666666666666
  p = 0.7500000000000001
 4 = {EvaluationCurvePoint@9450} 
  x = 0.5
  y = 1.0
  p = 0.7000000000000001
 5 = {EvaluationCurvePoint@9451} 
  x = 1.0
  y = 1.0
  p = 0.6000000000000001
 6 = {EvaluationCurvePoint@9452} 
  x = 1.0
  y = 1.0
  p = 0.5
    
recallPrecisionCurve = {EvaluationCurvePoint[7]@9320} 
 1 = {EvaluationCurvePoint@9444} 
  x = 0.3333333333333333
  y = 1.0
  p = 0.9
 2 = {EvaluationCurvePoint@9453} 
  x = 0.6666666666666666
  y = 1.0
  p = 0.8
 3 = {EvaluationCurvePoint@9454} 
  x = 0.6666666666666666
  y = 0.6666666666666666
  p = 0.7500000000000001
 4 = {EvaluationCurvePoint@9455} 
  x = 1.0
  y = 0.75
  p = 0.7000000000000001
 5 = {EvaluationCurvePoint@9456} 
  x = 1.0
  y = 0.6
  p = 0.6000000000000001
 6 = {EvaluationCurvePoint@9457} 
  x = 1.0
  y = 0.6
  p = 0.5
    
liftChart = {EvaluationCurvePoint[7]@9325} 
 1 = {EvaluationCurvePoint@9458} 
  x = 0.2
  y = 1.0
  p = 0.9
 2 = {EvaluationCurvePoint@9459} 
  x = 0.4
  y = 2.0
  p = 0.8
 3 = {EvaluationCurvePoint@9460} 
  x = 0.6
  y = 2.0
  p = 0.7500000000000001
 4 = {EvaluationCurvePoint@9461} 
  x = 0.8
  y = 3.0
  p = 0.7000000000000001
 5 = {EvaluationCurvePoint@9462} 
  x = 1.0
  y = 3.0
  p = 0.6000000000000001
 6 = {EvaluationCurvePoint@9463} 
  x = 1.0
  y = 3.0
  p = 0.5
    
data = {ConfusionMatrix[7]@9339} 
 0 = {ConfusionMatrix@9486} 
  longMatrix = {LongMatrix@9488} 
   matrix = {long[2][]@9491} 
    0 = {long[2]@9492} 
     0 = 0
     1 = 0
    1 = {long[2]@9493} 
     0 = 3
     1 = 2
   rowNum = 2
   colNum = 2
  labelCnt = 2
  total = 5
  actualLabelFrequency = {long[2]@9489} 
   0 = 3
   1 = 2
  predictLabelFrequency = {long[2]@9490} 
   0 = 0
   1 = 5
  tpCount = 2.0
  tnCount = 2.0
  fpCount = 3.0
  fnCount = 3.0
 1 = {ConfusionMatrix@9435} 
  longMatrix = {LongMatrix@9469} 
   matrix = {long[2][]@9472} 
    0 = {long[2]@9474} 
     0 = 1
     1 = 0
    1 = {long[2]@9475} 
     0 = 2
     1 = 2
   rowNum = 2
   colNum = 2
  labelCnt = 2
  total = 5
  actualLabelFrequency = {long[2]@9470} 
   0 = 3
   1 = 2
  predictLabelFrequency = {long[2]@9471} 
   0 = 1
   1 = 4
  tpCount = 3.0
  tnCount = 3.0
  fpCount = 2.0
  fnCount = 2.0
  ......  
    
    threshold[0] = 1.0;
    data[0] = new ConfusionMatrix(new long[][] {{0, 0}, {totalTrue, totalFalse}});
    rocCurve[0] = new EvaluationCurvePoint(0, 0, threshold[0]);
    recallPrecisionCurve[0] = new EvaluationCurvePoint(0, recallPrecisionCurve[1].getY(), threshold[0]);
    liftChart[0] = new EvaluationCurvePoint(0, 0, threshold[0]);

    return Tuple3.of(data, threshold, new EvaluationCurve[] {new EvaluationCurve(rocCurve),
        new EvaluationCurve(recallPrecisionCurve), new EvaluationCurve(liftChart)});
}

3.2.4 計算混淆矩陣

這裏再給大家講講混淆矩陣如何計算,這裏思路比較繞。

3.2.4.1 原始矩陣

調用之處是:

// 調用之處
data[i] = new ConfusionMatrix(
        new long[][] {{curTrue, curFalse}, {totalTrue - curTrue, totalFalse - curFalse}});
// 調用時候各種賦值
i = 1
index = 90000
totalTrue = 3
totalFalse = 2
curTrue = 1
curFalse = 0

得到原始矩陣,以下都有cur,說明只針對當前點來說。

curTrue = 1 curFalse = 0
totalTrue – curTrue = 2 totalFalse – curFalse = 2
3.2.4.2 計算標籤

後續ConfusionMatrix計算中,由此可以得到

actualLabelFrequency = longMatrix.getColSums();
predictLabelFrequency = longMatrix.getRowSums();

actualLabelFrequency = {long[2]@9322} 
 0 = 3
 1 = 2
predictLabelFrequency = {long[2]@9323} 
 0 = 1
 1 = 4  

可以看出來,Alink算法認為:每列的sum和實際標籤有關;每行sum和預測標籤有關。

得到新矩陣如下

predictLabelFrequency
curTrue = 1 curFalse = 0 1 = curTrue + curFalse
totalTrue – curTrue = 2 totalFalse – curFalse = 2 4 = total – curTrue – curFalse
actualLabelFrequency 3 = totalTrue 2 = totalFalse

後續計算將要基於這些來計算:

計算中就用到longMatrix 對角線上的數據,即longMatrix(0)(0)和 longMatrix(1)(1)。一定要注意,這裏考慮的都是 當前狀態 (畫重點強調)。

longMatrix(0)(0) :curTrue

longMatrix(1)(1) :totalFalse – curFalse

totalFalse :( TN + FN )

totalTrue :( TP + FP )

double numTrueNegative(Integer labelIndex) {
  // labelIndex為 0 時候,return 1 + 5 - 1 - 3 = 2;
  // labelIndex為 1 時候,return 2 + 5 - 4 - 2 = 1;
	return null == labelIndex ? tnCount : longMatrix.getValue(labelIndex, labelIndex) + total - predictLabelFrequency[labelIndex] - actualLabelFrequency[labelIndex];
}

double numTruePositive(Integer labelIndex) {
  // labelIndex為 0 時候,return 1; 這個是 curTrue,就是真實標籤是True,判別也是True。是TP
  // labelIndex為 1 時候,return 2; 這個是 totalFalse - curFalse,總判別錯 - 當前判別錯。這就意味着“本來判別錯了但是當前沒有發現”,所以認為在當前狀態下,這也算是TP
	return null == labelIndex ? tpCount : longMatrix.getValue(labelIndex, labelIndex);
}

double numFalseNegative(Integer labelIndex) {
  // labelIndex為 0 時候,return 3 - 1; 
  // actualLabelFrequency[0] = totalTrue。所以return totalTrue - curTrue,即當前“全部正確”中沒有“判別為正確”,這個就可以認為是“判別錯了且判別為負”
  // labelIndex為 1 時候,return 2 - 2;   
  // actualLabelFrequency[1] = totalFalse。所以return totalFalse - ( totalFalse - curFalse )  = curFalse
	return null == labelIndex ? fnCount : actualLabelFrequency[labelIndex] - longMatrix.getValue(labelIndex, labelIndex);
}

double numFalsePositive(Integer labelIndex) {
  // labelIndex為 0 時候,return 1 - 1;
  // predictLabelFrequency[0] = curTrue + curFalse。
  // 所以 return = curTrue + curFalse - curTrue = curFalse = current( TN + FN ) 這可以認為是判斷錯了實際是正確標籤
  // labelIndex為 1 時候,return 4 - 2; 
  // predictLabelFrequency[1] = total - curTrue - curFalse。
  // 所以 return = total - curTrue - curFalse - (totalFalse - curFalse) = totalTrue - curTrue = ( TP + FP ) - currentTP = currentFP 
	return null == labelIndex ? fpCount : predictLabelFrequency[labelIndex] - longMatrix.getValue(labelIndex, labelIndex);
}

// 最後得到
tpCount = 3.0
tnCount = 3.0
fpCount = 2.0
fnCount = 2.0
3.2.4.3 具體代碼
// 具體計算 
public ConfusionMatrix(LongMatrix longMatrix) {
  
longMatrix = {LongMatrix@9297} 
  0 = {long[2]@9324} 
   0 = 1
   1 = 0
  1 = {long[2]@9325} 
   0 = 2
   1 = 2
     
    this.longMatrix = longMatrix;
    labelCnt = this.longMatrix.getRowNum();
    // 這裏就是計算
    actualLabelFrequency = longMatrix.getColSums();
    predictLabelFrequency = longMatrix.getRowSums();
  
actualLabelFrequency = {long[2]@9322} 
 0 = 3
 1 = 2
predictLabelFrequency = {long[2]@9323} 
 0 = 1
 1 = 4  
labelCnt = 2
total = 5  

    total = longMatrix.getTotal();
    for (int i = 0; i < labelCnt; i++) {
        tnCount += numTrueNegative(i);
        tpCount += numTruePositive(i);
        fnCount += numFalseNegative(i);
        fpCount += numFalsePositive(i);
    }
}

0x04 流處理

4.1 示例

Alink原有python示例代碼中,Stream部分是沒有輸出的,因為MemSourceStreamOp沒有和時間相關聯,而Alink中沒有提供基於時間的StreamOperator,所以只能自己仿照MemSourceBatchOp寫了一個。雖然代碼有些丑,但是至少可以提供輸出,這樣就能夠調試。

4.1.1 主類

public class EvalBinaryClassExampleStream {

    AlgoOperator getData(boolean isBatch) {
        Row[] rows = new Row[]{
                Row.of("prefix1", "{\"prefix1\": 0.9, \"prefix0\": 0.1}")
        };
        String[] schema = new String[]{"label", "detailInput"};
        if (isBatch) {
            return new MemSourceBatchOp(rows, schema);
        } else {
            return new TimeMemSourceStreamOp(rows, schema, new EvalBinaryStreamSource());
        }
    }

    public static void main(String[] args) throws Exception {
        EvalBinaryClassExampleStream test = new EvalBinaryClassExampleStream();
        StreamOperator streamData = (StreamOperator) test.getData(false);
        StreamOperator sOp = new EvalBinaryClassStreamOp()
                .setLabelCol("label")
                .setPredictionDetailCol("detailInput")
                .setTimeInterval(1)
                .linkFrom(streamData);
        sOp.print();
        StreamOperator.execute();
    }
}

4.1.2 TimeMemSourceStreamOp

這個是我自己炮製的。借鑒了MemSourceStreamOp。

public final class TimeMemSourceStreamOp extends StreamOperator<TimeMemSourceStreamOp> {

    public TimeMemSourceStreamOp(Row[] rows, String[] colNames, EvalBinaryStrSource source) {
        super(null);
        init(source, Arrays.asList(rows), colNames);
    }

    private void init(EvalBinaryStreamSource source, List <Row> rows, String[] colNames) {
        Row first = rows.iterator().next();
        int arity = first.getArity();
        TypeInformation <?>[] types = new TypeInformation[arity];

        for (int i = 0; i < arity; ++i) {
            types[i] = TypeExtractor.getForObject(first.getField(i));
        }

        init(source, colNames, types);
    }

    private void init(EvalBinaryStreamSource source, String[] colNames, TypeInformation <?>[] colTypes) {
        DataStream <Row> dastr = MLEnvironmentFactory.get(getMLEnvironmentId())
                .getStreamExecutionEnvironment().addSource(source);
        StringBuilder sbd = new StringBuilder();
        sbd.append(colNames[0]);
      
        for (int i = 1; i < colNames.length; i++) {
            sbd.append(",").append(colNames[i]);
        }
        this.setOutput(dastr, colNames, colTypes);
    }

    @Override
    public TimeMemSourceStreamOp linkFrom(StreamOperator<?>... inputs) {
        return null;
    }
}

4.1.3 Source

定時提供Row,加入了隨機數,讓概率有變化。

class EvalBinaryStreamSource extends RichSourceFunction[Row] {

  override def run(ctx: SourceFunction.SourceContext[Row]) = {
    while (true) {
      val rdm = Math.random() // 這裏加入了隨機數,讓概率有變化
      val rows: Array[Row] = Array[Row](
        Row.of("prefix1", "{\"prefix1\": " + rdm + ", \"prefix0\": " + (1-rdm) + "}"),
        Row.of("prefix1", "{\"prefix1\": 0.8, \"prefix0\": 0.2}"),
        Row.of("prefix1", "{\"prefix1\": 0.7, \"prefix0\": 0.3}"),
        Row.of("prefix0", "{\"prefix1\": 0.75, \"prefix0\": 0.25}"),
        Row.of("prefix0", "{\"prefix1\": 0.6, \"prefix0\": 0.4}"))
      for(row <- rows) {
        println(s"當前值:$row")
        ctx.collect(row)
      }
      Thread.sleep(1000)
    }
  }

  override def cancel() = ???
}

4.2 BaseEvalClassStreamOp

Alink流處理類是 EvalBinaryClassStreamOp,主要工作在其基類 BaseEvalClassStreamOp,所以我們重點看後者。

public class BaseEvalClassStreamOp<T extends BaseEvalClassStreamOp<T>> extends StreamOperator<T> {
    @Override
    public T linkFrom(StreamOperator<?>... inputs) {
        StreamOperator<?> in = checkAndGetFirst(inputs);
        String labelColName = this.get(MultiEvaluationStreamParams.LABEL_COL);
        String positiveValue = this.get(BinaryEvaluationStreamParams.POS_LABEL_VAL_STR);
        Integer timeInterval = this.get(MultiEvaluationStreamParams.TIME_INTERVAL);

        ClassificationEvaluationUtil.Type type = ClassificationEvaluationUtil.judgeEvaluationType(this.getParams());

        DataStream<BaseMetricsSummary> statistics;

        switch (type) {
            case PRED_RESULT: {
              ......
            }
            case PRED_DETAIL: {               
                String predDetailColName = this.get(MultiEvaluationStreamParams.PREDICTION_DETAIL_COL);
                // 
                PredDetailLabel eval = new PredDetailLabel(positiveValue, binary);
                // 獲取輸入數據,重點是timeWindowAll
                statistics = in.select(new String[] {labelColName, predDetailColName})
                    .getDataStream()
                    .timeWindowAll(Time.of(timeInterval, TimeUnit.SECONDS))
                    .apply(eval);
                break;
            }
        }
        // 把各個窗口的數據累積到 totalStatistics,注意,這裡是新變量了。
        DataStream<BaseMetricsSummary> totalStatistics = statistics
            .map(new EvaluationUtil.AllDataMerge())
            .setParallelism(1); // 并行度設置為1

        // 基於兩種 bins 計算&序列化,得到當前的 statistics
        DataStream<Row> windowOutput = statistics.map(
            new EvaluationUtil.SaveDataStream(ClassificationEvaluationUtil.WINDOW.f0));
        // 基於bins計算&序列化,得到累積的 totalStatistics
        DataStream<Row> allOutput = totalStatistics.map(
            new EvaluationUtil.SaveDataStream(ClassificationEvaluationUtil.ALL.f0));

      	// "當前" 和 "累積" 做聯合,最終返回
        DataStream<Row> union = windowOutput.union(allOutput);

        this.setOutput(union,
            new String[] {ClassificationEvaluationUtil.STATISTICS_OUTPUT, DATA_OUTPUT},
            new TypeInformation[] {Types.STRING, Types.STRING});

        return (T)this;
    }
}

具體業務是:

  • PredDetailLabel 會進行去重標籤名字 和 累積計算混淆矩陣所需數據
    • buildLabelIndexLabelArray 去重 “labels名字”,然後給每一個label一個ID,最後結果是一個<labels, ID>Map。
    • getDetailStatistics 遍歷 rows 數據,提取每一個item(比如 “prefix1,{“prefix1”: 0.8, “prefix0”: 0.2}”),然後通過updateBinaryMetricsSummary累積計算混淆矩陣所需數據。
  • 根據標籤從Window中獲取數據 statistics = in.select().getDataStream().timeWindowAll() .apply(eval);
  • EvaluationUtil.AllDataMerge 把各個窗口的數據累積到 totalStatistics 。
  • 得到windowOutput ——– EvaluationUtil.SaveDataStream,對”當前數據statistics”做處理。實際業務在BinaryMetricsSummary.toMetrics,即基於bin的信息計算,然後存儲到params,並序列化返回Row。
    • extractMatrixThreCurve函數取出非空的bins,據此計算出ConfusionMatrix array(混淆矩陣), threshold array, rocCurve/recallPrecisionCurve/LiftChart.
    • 依據曲線內容計算並且存儲 AUC/PRC/KS
    • 對生成的rocCurve/recallPrecisionCurve/LiftChart輸出進行抽樣
    • 依據抽樣后的輸出存儲 RocCurve/RecallPrecisionCurve/LiftChar
    • 存儲正例樣本的度量指標
    • 存儲Logloss
    • Pick the middle point where threshold is 0.5.
  • 得到allOutput ——– EvaluationUtil.SaveDataStream , 對”累積數據totalStatistics”做處理。
    • 詳細處理流程同windowOutput。
  • windowOutput 和 allOutput 做聯合。最終返回 DataStream union = windowOutput.union(allOutput);

4.2.1 PredDetailLabel

static class PredDetailLabel implements AllWindowFunction<Row, BaseMetricsSummary, TimeWindow> {
    @Override
    public void apply(TimeWindow timeWindow, Iterable<Row> rows, Collector<BaseMetricsSummary> collector) throws Exception {
        HashSet<String> labels = new HashSet<>();
        // 首先還是獲取 labels 名字
        for (Row row : rows) {
            if (EvaluationUtil.checkRowFieldNotNull(row)) {
                labels.addAll(EvaluationUtil.extractLabelProbMap(row).keySet());
                labels.add(row.getField(0).toString());
            }
        }
labels = {HashSet@9757}  size = 2
 0 = "prefix1"
 1 = "prefix0"   
        // 之前介紹過,buildLabelIndexLabelArray 去重 "labels名字",然後給每一個label一個ID,最後結果是一個<labels, ID>Map。
        // getDetailStatistics 遍歷 rows 數據,累積計算混淆矩陣所需數據( "TP + FN"  /  "TN + FP")。
        if (labels.size() > 0) {
            collector.collect(
                getDetailStatistics(rows, binary, buildLabelIndexLabelArray(labels, binary, positiveValue)));
        }
    }
}

4.2.2 AllDataMerge

EvaluationUtil.AllDataMerge 把各個窗口的數據累積

/**
 * Merge data from different windows.
 */
public static class AllDataMerge implements MapFunction<BaseMetricsSummary, BaseMetricsSummary> {
    private BaseMetricsSummary statistics;
    @Override
    public BaseMetricsSummary map(BaseMetricsSummary value) {
        this.statistics = (null == this.statistics ? value : this.statistics.merge(value));
        return this.statistics;
    }
}

4.2.3 SaveDataStream

SaveDataStream具體調用的函數之前批處理介紹過,實際業務在BinaryMetricsSummary.toMetrics,即基於bin的信息計算,存儲到params。

這裏與批處理不同的是直接就把”構建出的度量信息“返回給用戶。

public static class SaveDataStream implements MapFunction<BaseMetricsSummary, Row> {
    @Override
    public Row map(BaseMetricsSummary baseMetricsSummary) throws Exception {
        BaseMetricsSummary metrics = baseMetricsSummary;
        BaseMetrics baseMetrics = metrics.toMetrics();
        Row row = baseMetrics.serialize();
        return Row.of(funtionName, row.getField(0));
    }
}

// 最後得到的 row 其實就是最終返回給用戶的度量信息
row = {Row@10008} "{"PRC":"0.9164636268708667","SensitivityArray":"[0.38461538461538464,0.6923076923076923,0.6923076923076923,1.0,1.0,1.0]","ConfusionMatrix":"[[13,8],[0,0]]","MacroRecall":"0.5","MacroSpecificity":"0.5","FalsePositiveRateArray":"[0.0,0.0,0.5,0.5,1.0,1.0]" ...... 還有很多其他的

4.2.4 Union

DataStream<Row> windowOutput = statistics.map(
    new EvaluationUtil.SaveDataStream(ClassificationEvaluationUtil.WINDOW.f0));
DataStream<Row> allOutput = totalStatistics.map(
    new EvaluationUtil.SaveDataStream(ClassificationEvaluationUtil.ALL.f0));

DataStream<Row> union = windowOutput.union(allOutput);

最後返回兩種統計數據

4.2.4.1 allOutput
all|{"PRC":"0.7341146115890359","SensitivityArray":"[0.3333333333333333,0.3333333333333333,0.6666666666666666,0.7333333333333333,0.8,0.8,0.8666666666666667,0.8666666666666667,0.9333333333333333,1.0]","ConfusionMatrix":"[[13,10],[2,0]]","MacroRecall":"0.43333333333333335","MacroSpecificity":"0.43333333333333335","FalsePositiveRateArray":"[0.0,0.5,0.5,0.5,0.5,1.0,1.0,1.0,1.0,1.0]","TruePositiveRateArray":"[0.3333333333333333,0.3333333333333333,0.6666666666666666,0.7333333333333333,0.8,0.8,0.8666666666666667,0.8666666666666667,0.9333333333333333,1.0]","AUC":"0.5666666666666667","MacroAccuracy":"0.52", ......

4.2.4.2 windowOutput

window|{"PRC":"0.7638888888888888","SensitivityArray":"[0.3333333333333333,0.3333333333333333,0.6666666666666666,1.0,1.0,1.0]","ConfusionMatrix":"[[3,2],[0,0]]","MacroRecall":"0.5","MacroSpecificity":"0.5","FalsePositiveRateArray":"[0.0,0.5,0.5,0.5,1.0,1.0]","TruePositiveRateArray":"[0.3333333333333333,0.3333333333333333,0.6666666666666666,1.0,1.0,1.0]","AUC":"0.6666666666666666","MacroAccuracy":"0.6","RecallArray":"[0.3333333333333333,0.3333333333333333,0.6666666666666666,1.0,1.0,1.0]","KappaArray":"[0.28571428571428564,-0.15384615384615377,0.1666666666666666,0.5454545454545455,0.0,0.0]","MicroFalseNegativeRate":"0.4","WeightedRecall":"0.6","WeightedPrecision":"0.36","Recall":"1.0","MacroPrecision":"0.3",......

0xFF 參考

[[白話解析] 通過實例來梳理概念 :準確率 (Accuracy)、精準率(Precision)、召回率(Recall) 和 F值(F-Measure)](

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

2020/6/10 JavaScript高級程序設計 BOM

BOM(瀏覽器對象模型):提供用於訪問瀏覽器的對象。

8.1 window對象

 window是BOM的核心對象,表示瀏覽器的一個實例。

  • JavaScript訪問瀏覽器窗口的接口
  • ECMAScript規定的Global對象

8.1.1 全局作用域

全局變量會成為window的屬性,但是定義全局變量和直接在window對象上定義屬性是有差別的——全局變量不能通過delete刪除,但window對象上定義的可以。

這是因為使用var添加的window屬性[[Configurable]]被設置為false(不可刪除)。

訪問未聲明的變量會發生錯誤,但通過查詢window對象,可以知道某個可能未聲明的變量是否存在。

//這裡會拋出錯誤,因為oldValue未定義
var newValue = oldValue;

//這裏不會拋出錯誤,因為這是一次屬性查詢
var newValue = window.oldValue;  //newValue的值是undefined

8.1.2 窗口關係及框架

如果頁面中包含框架,則每個框架都擁有自己的window對象,並保存在frames集合中。在frames集合中可以通過數值索引/框架名稱來訪問相應的window對象。每個window對象都有一個name屬性,其中包含框架的名稱。

PS1:對於最高層窗口來說:除非最高層窗口是通過window.open()打開的,否則其window對象的name屬性不會包含任何值。

與框架有關的window對象屬性(同時也是對象):

  • top:始終指向最高(最外)層的框架,也就是瀏覽器窗口。使用它可以正確地在一個框架中訪問另一個框架。因為對任意一個框架中的代碼來說,window對象指向的都是那個框架的特定實例,而非最高層框架。
  • parent:始終指向當前框架的直接上層框架。在沒有框架的情況下,parent等於top。
  • self:始終指向window。引入self的目的僅僅是為了和top和parent對象對應,因此他不包含其他值。

8.1.3 窗口位置

用來確定window對象位置的屬性:screenLeft, screenTop / screenX, screenY,分別表示窗口相對於屏幕左邊和上邊的位置。

兩組方法分別支持的瀏覽器:

screenLeft, screenTop IE、Safari、Opera、Chrome
screenX, screenY Firefox、Safari、Chrome

跨瀏覽器取得窗口位置的代碼:

var leftPos = (typeof window.screenLeft == "number") ?
                        window.screenLeft : window.screenX;
var topPos = (typeof window.screenTop == "number") ?
                        window.screenTop : window.screenY;

缺點:

  • screenTop表示的是由從屏幕上邊到window對象表示的頁面可見區域的距離(即頁面可見區域上方瀏覽器工具欄的像素高度)。
  • screenY表示整個瀏覽器窗口相對於屏幕的坐標值(0)。

將窗口精確移動到一個新位置的方法:

  • moveTo():接收最新位置的x,y坐標值。
  • moveBy():接收在水平和垂直方向上移動的像素數。

PS:這兩個方法很可能會被瀏覽器禁用(Opera和IE7+),且不適合框架,只能對最外層window對象使用。

8.1.4 窗口大小

  IE9+、Safari、Firefox Opera Chrome
innerWidth、innerHeight 視圖區大小 該容器中頁面視圖區的大小(減去邊框寬度) 視口大小
outerWidth、outerHeight 瀏覽器窗口本身的尺寸 頁面視圖容器的大小(單個標籤頁對應瀏覽器窗口的大小) 視口大小

document.documentElement.clientWidth / document.documentElement.clientHeight:保存了頁面視口的信息。

resizeTo()和resizeBy():調整瀏覽器窗口的大小。(分別接收新寬度高度和新窗口與原窗口的寬度和高度之差,同樣可能被瀏覽器禁用)

8.1.5 導航和打開窗口

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

Tesla 預計 2017 年推新車 要讓中國消費者負擔得起

電動車厰特斯拉(Tesla)主管法律事務的全球副總裁 James Chen 說,雖然在中國建廠實現本地化生產已在規劃之中,但目前並沒有時間表,要等中國市場擴大到一定程度,才會邁出那一步。他表示,「盡可能擴大在華銷量,是特斯拉汽車今年在中國的核心任務。」   目前在中國銷售的 Model S 價格在 70 至 100 萬人民幣之間。特斯拉期望通過規模化生產降低成本,預計在 2017 年推出價格在 30 萬至 40 萬人民幣間的 Model 3,讓更多中國消費者能負擔得起。  

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

第十六屆上海國際汽車工業展覽會

新能源展區

展會日期:2015年4月22日至29日
媒體日:2015年4月20-21日
展覽會地點:國家會展中心(上海)(盈港東路168號)

展覽會面積:30萬平方米(預計)
新能源展覽面積:5萬平方米(預計)
展覽會觀眾:100萬人次(預計)
新能源專業觀眾:3萬人次(預計)
展覽會宣傳:3,000家媒體及10,000名記者的熱情報導(預計)

主辦單位:
中國汽車工業協會
中國國際貿易促進委員會上海市分會
中國國際貿易促進委員會汽車行業分會

承辦單位:
上海市國際展覽有限公司
特別支援單位:
中國機械工業聯合會
歐洲地區協辦單位:
德國慕尼克國際博覽集團
IMAG國際交易會展覽會有限公司
支援單位:
中國汽車工程學會
新能源展區招展:
北京盛大超越國際展覽有限公司

展會概況:

Auto Shanghai 上海國際汽車展,創辦於1985年,至今已連續成功舉辦了十五屆。30年來,見證了中國汽車工業的欣欣向榮和中國汽車市場的迅猛發展,更是為中外車企構築起廣泛交流與合作的平臺,同時也為汽車愛好者打造了汽車文化的盛會。而上海國際汽車展本身也逐步成長為中國乃至全球最具讚譽的汽車大展之一。在我們迎來第十六屆上海國際汽車展之際,主承辦單位由衷地感謝中外汽車界、新聞界以及廣大熱情的觀眾對展會的厚愛與支持。

Auto Shanghai 上海國際汽車展,一貫秉承“以人為本”的服務理念,凝聚創新與突破,主承辦各方將銳意進取,不斷豐富展會內涵,樹立展會個性,提升展會品質,使上海國際汽車展成為中國乃至國際汽車工業最具品牌價值與影響力的展示、發佈及貿易平臺之一。

Auto Shanghai 上海國際汽車展,始終關注汽車工業的發展步伐,在汽車誕生逾一個世紀以來,科技不斷推動著汽車設計開啟新的篇章。智慧化、新能源、多功能將成為未來汽車發展的趨勢,我們相信科技的不斷進步將使未來的汽車更安全、更便捷、更舒適。這新一輪的技術變革,也讓未來的汽車工業及人類的汽車生活有了更多的可能。中國乃至全球汽車產業也因此將面臨新的機遇與創造。將于明年4月揭開序幕的第十六屆上海國際汽車展以“創新 ▪ 升級”為主題,屆時我們將再次領略汽車工業所迸發的無窮魅力,汽車科技所推動的技術變革,汽車文化所帶來的生活理念。同時,2015年4月的第十六屆上海國際汽車展將移師國家會展中心(上海)這一全新的舞臺舉辦。我們將在中外汽車鉅子攜手搭建的全新舞臺上感受到未來正在發生!

本屆車展是舉辦30年以來首次設立新能源展區,數家車企現場角力。新能源車包括混合動力汽車、電動汽車、燃料電池汽車等新能源汽車、驅動系統、充電設施、相關零部件、汽車設計等。

主承辦單位在長期的辦展歷程中,始終以簡潔、高效、創新的風格精心策劃和運營展會,使廣大觀眾、眾多媒體在車展現場感受到汽車文化的魅力與底蘊。我們熱情邀請您參與和光臨第十六屆上海國際汽車展。

讓我們共同期待2015上海國際汽車工業展覽會成為“創新 ▪ 升級”的舞臺!

新能源展區參展範圍:
節能汽車、純電動車,混合動力車,燃料電池車,輕型電動車,天然氣(液化氣)車,醇類燃料車及其他代用燃料車;
先進內燃機、高效變速器、整車優化設計等節能技術和產品;
電池,燃料電池,電池管理系統,電池的回收和包裝;
電力電容器,飛輪,能源管理系統;
電機,電機保護與控制技術;
充電器及充電站設備及相關配套專案企業、機構;
電動車及其他替代能源汽車的零部件及零部件總成;
加氣設備,儲運設備及技術;
網路管理,可再生能源發電,新型元器件和材料,輕量化;
智慧社區和電網,汽車共用和通訊服務;
檢測,維修,監控,實驗,安全防護裝備;
城市推廣應用示範展示、製造設備、工具及媒體等.

展會諮詢:
北京盛大超越國際展覽有限公司
連絡人:嶽巍                           
手  機:+86 135 5286 5285                         
電  話: +86 10 6329 0215                  
E-mail:                    

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※幫你省時又省力,新北清潔一流服務好口碑

※別再煩惱如何寫文案,掌握八大原則!

2014年歐洲電動汽車銷量榜出爐 日產leaf市占率26%拔頭籌

據統計,2014年歐洲電動汽車總銷量達5.6萬輛,其中日產聆風(Leaf)的銷量佔據了主導地位,達14658輛,市場佔有率為26%。聆風目前與Juke在英國同一條生產線生產,因此價格有所降低,供應量也更大。2014年聆風電動汽車僅在英國就售出4051輛,比2013年的1812輛多出一倍以上。這款車型佔據了55%的英國純電動汽車市場。   另外,雷諾Zoe在歐洲的銷量為11227輛,排在電動汽車市場的第二位,市場佔有率為20%。特斯拉Model S電動汽車以8734輛的成績排名第三,考慮到其與奧迪A6相當的高昂售價,這一表現算得上十分出色。而BMWi3售出5804輛排在第四位,市場佔有率10%。但是i3在2014年年末剛剛上市而且受生產難度限制產量有限,也算是在電動車市場上為德國企業的表現扳回一成。   而電動車銷量榜的第5和第6位分別為大眾汽車的兩款車型。其中e-Up!電動車以5363輛的銷量幾乎比肩BMWi3。可能受限於34900歐元的售價和較晚的發售時間,其全新的電動版高爾夫(e-Golf)只售出3328輛。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

與特斯拉比拼!BMW 及福斯將在美國東西岸設 100 座快速充電站

  為了拉抬自家電動車買氣,BMW、福斯(Volkswagen)宣布與美國充電設施營運商 ChargePoint 合作,將在美國東岸與西岸設置共 100 座快速充電站。除了要藉此建立更完善的充電站網絡外,更希望能讓消費者不被充電便利性受限,而提高購買電動車的意願。   ChargePoint 是目前全美最大的充電設施營運商,在全國一共設置超過 2 萬座充電站,BMW、福斯與其合作,計畫在 2015 年底前,於美國東岸與西岸設置 100 座快速充電站,相當於平均每 50 英里(約 80.5 公里)就有一座快速充電站。   每座充電站配有 2 個輸出功率分別為 50 kW DC 或 24 kW DC 的 2 款高速充電樁,駕駛只要花 20 至 30 分鐘,就能讓電動車充滿 80% 的電量,相較於需花費好幾個小時充電的 ChargePoint 其他充電站,明顯快上許多。  
 

  (Source:)   據紐約時報報導,有別於特斯拉(Telsa)在全美設置的 358 座免費超級充電站(Supercharger),只能提供給自家車款使用,BMW 與福斯設置的充電站,則是與大多數電動車都相容,不過,使用這些充電站的駕駛就沒像特斯拉車主那麼好康了,得自掏腰包付「充電費」。   根據統計,2014 年美國電動車銷量約為 12 萬輛,較 2013 年成長 20%,但2014 年全美各類車種總銷量一共為 1,650 萬輛,電動車占比仍少得可憐。   或許是意識到快速充電樁數量稀少是影響電動車銷售量的一大因素,近來不少電動車製造商設法填補充電樁不足的缺口,希望能讓消費者不被充電便利性受限,好替電動車銷量帶來生機。   (首圖來源:)

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

設計模式系列之代理模式(Proxy Pattern)——對象的間接訪問

說明:設計模式系列文章是讀劉偉所著《設計模式的藝術之道(軟件開發人員內功修鍊之道)》一書的閱讀筆記。個人感覺這本書講的不錯,有興趣推薦讀一讀。詳細內容也可以看看此書作者的博客https://blog.csdn.net/LoveLion/article/details/17517213

模式概述

近年來,代購已逐步成為电子商務的一個重要分支。何謂代購,簡單來說就是找人幫忙購買所需要的商品,當然你可能需要向實施代購的人支付一定的費用。代購通常分為兩種類型:一種是因為在當地買不到某件商品,又或者是因為當地這件商品的價格比其他地區的貴,因此託人在其他地區甚至國外購買該商品,然後通過快遞發貨或者直接攜帶回來;還有一種代購,由於消費者對想要購買的商品相關信息的缺乏,自已無法確定其實際價值而又不想被商家宰,只好委託中介機構幫其講價或為其代買。

在軟件開發中,也有一種設計模式可以提供與代購類似的功能。由於某些原因,客戶端無法直接訪問某個對象或訪問某個對象存在困難時,可以通過一個稱之為“代理”的第三者來實現間接訪問,該方案對應的設計模式被稱為代理模式。

模式定義

代理模式(Proxy Pattern):給某一個對象提供一個代理或佔位符,並由代理對象來控制對原對象的訪問。

代理模式是一種對象結構型模式。在代理模式中引入了一個新的代理對象,代理對象在客戶端對象和目標對象之間起到中介的作用,它去掉客戶不能看到的內容和服務或者增添客戶需要的額外的新服務。

模式結構圖

代理模式的結構比較簡單,其核心是代理類,為了讓客戶端能夠一致性地對待真實對象和代理對象,在代理模式中引入了抽象層,代理模式結構如下圖所示:

代理模式包含如下三個角色:

  • Subject(抽象主題角色):它聲明了真實主題和代理主題的共同接口,這樣一來在任何使用真實主題的地方都可以使用代理主題,客戶端通常需要針對抽象主題角色進行編程。

  • Proxy(代理主題角色):它包含了對真實主題的引用,從而可以在任何時候操作真實主題對象;在代理主題角色中提供一個與真實主題角色相同的接口,以便在任何時候都可以替代真實主題;代理主題角色還可以控制對真實主題的使用,負責在需要的時候創建和刪除真實主題對象,並對真實主題對象的使用加以約束。通常,在代理主題角色中,客戶端在調用所引用的真實主題操作之前或之後還需要執行其他操作,而不僅僅是單純調用真實主題對象中的操作。

  • RealSubject(真實主題角色):它定義了代理角色所代表的真實對象,在真實主題角色中實現了真實的業務操作,客戶端可以通過代理主題角色間接調用真實主題角色中定義的操作。

模式偽代碼

代理模式的結構圖比較簡單,但是在真實的使用和實現過程中要複雜很多,特別是代理類的設計和實現。

抽象主題類聲明了真實主題類和代理類的公共方法,它可以是接口、抽象類或具體類,客戶端針對抽象主題類編程,一致性地對待真實主題和代理主題,典型的抽象主題類代碼如下:

public interface Subject {
    void request();
}

真實主題類實現了抽象主題類,提供了業務方法的具體實現,其典型代碼如下:

public class RealSubject implements Subject {
    @Override
    public void request() {
        //業務方法具體實現代碼
    }
}

代理類也是抽象主題類的子類,它維持一個對真實主題對象的引用,調用在真實主題中實現的業務方法,在調用時可以在原有業務方法的基礎上附加一些新的方法來對功能進行擴充或約束,最簡單的代理類實現代碼如下:

public class Proxy implements Subject {
    // 維持一個對真實主題對象的引用
    private RealSubject realSubject;

    public Proxy(RealSubject realSubject) {
        this.realSubject = realSubject;
    }

    public void preRequest() {
        // ...
    }

    public void postRequest() {
        // ...
    }

    @Override
    public void request() {
        preRequest();
        // 調用真實主題對象的方法
        realSubject.request();
        postRequest();
    }
}

模式應用

像上面代理類所實現的接口和所代理的方法都在代碼中寫死,被稱之為靜態代理。如果要為不同類的不同方法生成靜態代理,代理類的數量將會發生爆炸。Java中也提供了對動態代理的支持。所謂動態代理(Dynamic Proxy),是指系統運行時動態生成代理類。

JDK中提供的動態代理只能代理一個或者多個接口,如果需要動態代理具體類或者抽象類,可以使用CGLib(Code Generation Library)等工具。CGLib是一個功能較為強大、性能也較好的代碼生成包,在許多AOP框架中得到廣泛應用。後面我會專門寫一篇探究動態代理實現的博客,將全面細緻地介紹動態代理。

模式總結

代理模式是常用的結構型設計模式之一,它為對象的間接訪問提供了一個解決方案,可以對對象的訪問進行控制。代理模式類型較多,其中遠程代理、虛擬代理、保護代理等在軟件開發中應用非常廣泛。

主要優點

(1) 能夠協調調用者和被調用者,在一定程度上降低了系統的耦合度。

(2) 客戶端可以針對抽象主題角色進行編程,增加和更換代理類無須修改源代碼,符合開閉原則,系統具有較好的靈活性和可擴展性。

此外,不同類型的代理模式也具有獨特的優點,例如:

(1) 遠程代理為位於兩個不同地址空間對象的訪問提供了一種實現機制,可以將一些消耗資源較多的對象和操作移至性能更好的計算機上,提高系統的整體運行效率。

(2) 虛擬代理通過一個消耗資源較少的對象來代表一個消耗資源較多的對象,可以在一定程度上節省系統的運行開銷。

(3) 緩衝代理為某一個操作的結果提供臨時的緩存存儲空間,以便在後續使用中能夠共享這些結果,優化系統性能,縮短執行時間。

(4) 保護代理可以控制對一個對象的訪問權限,為不同用戶提供不同級別的使用權限。

主要缺點

(1) 由於在客戶端和真實主題之間增加了代理對象,因此有些類型的代理模式可能會造成請求的處理速度變慢,例如保護代理。

(2) 實現代理模式需要額外的工作,而且有些代理模式的實現過程較為複雜,例如遠程代理。

適用場景

代理模式的類型較多,不同類型的代理模式有不同的優缺點,它們應用於不同的場合:

(1) 當客戶端對象需要訪問遠程主機中的對象時可以使用遠程代理。

(2) 當需要用一個消耗資源較少的對象來代表一個消耗資源較多的對象,從而降低系統開銷、縮短運行時間時可以使用虛擬代理,例如一個對象需要很長時間才能完成加載時。

(3) 當需要為某一個被頻繁訪問的操作結果提供一個臨時存儲空間,以供多個客戶端共享訪問這些結果時可以使用緩衝代理。通過使用緩衝代理,系統無須在客戶端每一次訪問時都重新執行操作,只需直接從臨時緩衝區獲取操作結果即可。

(4) 當需要控制對一個對象的訪問,為不同用戶提供不同級別的訪問權限時可以使用保護代理。

(5) 當需要為一個對象的訪問(引用)提供一些額外的操作時可以使用智能引用代理。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

狀態機模式 與 ajax 的結合運用

太神奇了,昨晚做了個夢,夢中我悟出一個道理:凡是涉及到異步操作而且需要返回值的函數,一定要封裝成 Promise 的形式,假如返回值取決於多個異步操作的結果,那麼需要對每個異步操作進行狀態的設計,而且需要封裝一個 next 函數。,到了晚上才覺到很有意思,所以結合 ajax 設置最短返回時間和最大返回時間進行實踐:

  const PENDING = 'PENDING'
  const RESOLVED = 'RESOLVED'
  const REJECTED = 'REJECTED'
  const FULLFILLED = 'FULLFILLED'

  /**
   * @desc 異步操作模擬
   * @param time 響應時間
   * @param isError 是否拋錯
   */
  const mock = (time, isError) => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        if (!isError) {
          resolve({ user: 'ManbaX' })
        } else {
          reject('request error')
        }
      }, time)
    })
  }

  /**
   * @desc 生產不同類型請求的工廠函數
   * @param time 響應時間
   * @param isError 是否拋錯
   */
  var RequestFactory = function (time, isError) {
    var request = function () {
      return new Promise((resolve, reject) => {
        var min = PENDING
        var max = PENDING
        var state = PENDING
        var res = null

        var next = function (name) {
          const cb = function () {
            if (state === RESOLVED) {
              resolve(res)
            } else {
              reject(res)
            }
          }
          if (name === 'res' && min === FULLFILLED) {
            cb()
          }

          if (name === 'min' && (state === RESOLVED || state === REJECTED)) {
            cb()
          }

          if (name === 'max' && state === PENDING) {
            reject('timeout')
          }
        }

        setTimeout(() => {
          min = FULLFILLED
          next('min')
        }, 500)

        setTimeout(() => {
          max = FULLFILLED
          next('max')
        }, 1000)

        mock(time, isError).then(data => {
          res = data
          state = RESOLVED
          next('res')
        }).catch(error => {
          res = error
          state = REJECTED
          next('res')
        })
      })
    }

    return request
  }

  // 不超時, 不返回錯誤
  console.time('r1')
  RequestFactory(200)().then(res => {
    console.log('data: ', res)
  }).finally(() => {
    console.timeEnd('r1')
  })
  
  // 不超時, 返回錯誤
  console.time('r2')
  RequestFactory(200, true)().catch(err => {
    console.log('error', err)
  }).finally(() => {
    console.timeEnd('r2')
  })

  // 超時
  console.time('r3')
  RequestFactory(2000)().catch(res => {
    console.log('error: ', res)
  }).finally(() => {
    console.timeEnd('r3')
  })

上面的運行結果符合預期,本來夢中還有另外一個有意思的東西,但是太模糊了就搞忘記了,下次一定早點記錄。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※幫你省時又省力,新北清潔一流服務好口碑

※別再煩惱如何寫文案,掌握八大原則!