桃園縣大力推廣電動機車 購買補助額度全國第一

隨著環保意識抬頭,越來越多地方政府推政策因應環保趨勢。桃園縣環保局鼓勵民眾使用電動機車,除了購車補助額度位居全國第一,更推出免費試騎 7 天的活動,讓民眾能夠體驗電動機車的高續航力及充電迅速等便利性,增加使用環保運具的意願。

桃園縣政府環境保護局為改善空氣品質,積極推廣使用電動機車等低污染交通工具,環保局局長陳世偉表示,電動車輛具有零加油、零廢氣、低保養、低噪音及低充電費等特性,是最環保的交通工具;尤其現在電池技術成熟,使得電動車輛的續航力大增,壽命也有效延長保固。

  (Source:)

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

奧迪開發新款純電動車 將 Tesla Model S 視為對手

Audi 近期正在開發全新、續航力達 450 公里的純電動車款,預計於 2017 年推出,Audi 技術總監 Prof. Dr. Ulrich Hackenberg 透露該系列定位為 Tesla Model S 的對手,目前仍不確定該電動車是否會加入奧迪現有車系,例如像先前曾推出過的 Audi A3 e-Tron,或是會以全新的車款來販售。   Ulrich Hackenberg 指出,這部正在開發中的純電動車,將會整合 R8 e-tron 的設計及技術,目標是要達成 450 公里的續航力。Hackenberg 進一步表示,這輛車將在 2017 年上市,但不會是跑車。   能達到 450 公里的續航力主要是拜 Audi 新一代的電動馬達及電池所賜,該系統具有更高的電力效能,Volkswagen 動力開發總監 Dr. Heinz-Jakob Neußer 即指出,該電動馬達比現今的電動車(像是 e-Golf)具有高達 5 倍的電力效能。   Hackenberg 對 Audi 未來全電動車系的細節仍不願透露,但預計會採用轎車大小的車體,因為轎車的空間較有利在車板下方放置更大且更有力的電池,如此才不會影響電池運作或減少乘客空間。

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

日產電動車全球銷量突破 20 萬台 市占近 6 成

日產汽車 (Nissan) 26 日宣佈,雷諾日產聯盟 (Renault-Nissan Alliance) 電動車 (EV) 全球累計銷售量已正式突破 20 萬台,於全球 EV 市場的市佔率高達 58%;其中日產 EV「Leaf」銷售量約 15 萬台,包含雷諾「Twizy」等其他 4 款車種銷售量約 5 萬台。日產指出,20 萬台 EV 的行駛距離已高達約 40 億 km、足可繞地球跑 10 萬圈以上,且節省了 2 億公升燃料、減排了 4.5 億公斤 CO2。   日產指出,2014 年 1 月至 11 月初期間,雷諾日產聯盟 EV 全球銷售量較去年同期大增 20% 至 6 萬 6,500 台,其中 Leaf 於美國市場的銷售量比其他同業 EV 車款及插電式油電混合車 (PHV) 車款的合計銷售量還高,有望成為美國今年最暢銷的 EV 車款。   日產表示,今年 Leaf 美國銷售量預估將年增 35%,且其月銷售量已連 21 個月創史上新高紀錄,而 1 至 10 月的銷售量已超越 2013 年全年水準,篤定將再創新高紀錄。  

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

鎳礦出口禁令再提前 印尼宣布立即停止

摘錄自2019年10月28日中央社報導

印尼投資協調委員會主席巴赫利爾(Bahlil Lahadalia)28日指出,鎳礦業者已和政府達成協議,將立即停止鎳礦出口。這項禁令是為振興印尼冶煉產業發展。印尼上個月已將停止鎳礦出口的時間從2022年提早到明年1月實施,如今這項決定讓時程再度提前。

鎳礦產業協會秘書長梅迪(Meidy Lengkey)告訴路透社,只要政府能協助,讓鎳礦在國內市場能以公平售價賣出,鎳礦業者對於禁止出口的政策沒有意見。

 

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

中興通訊每年投逾50億 進軍新能源汽車

中國手機廠商中興通訊推出的自主研發的大功率無線充電系統,已進入公共交通領域。2014年10月,中興通訊攜手蜀都客車共同發佈全球首個無線充電城市微循環解決方案。該方案可以在不專門征地的情況下,在社區附近將任何現有停車位或路面直接改造為安全快捷的充電站。   “目前我們已經與10多個城市簽約,包括四川、河北、雲南、河南等省的城市,與當地政府就公共汽車的無線充電進行合作。”中興通訊副總裁孫枕戈稱,下一步將會把無線充電落實到私家車領域。據孫枕戈介紹,今後新能源汽車有可能將以PPP(政府與私人組織之間合作建設城市基礎設施專案)模式進行運營,而且將獲得中央政府和地方政府的補貼。   孫枕戈表示,從2015年開始,中興通訊每年將投入至少10億元人民幣(約合新臺幣50.6億)進行無線充電設施的商業運作。中興通訊未來的戰略是將通訊技術與各行各業相連,即M-ICT戰略,無線充電項目是其中的一項,該項目是中興與大專院校、研究機構共同合作的結果。

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

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

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

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

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

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

助力新能源車 意法半導體推新款車規碳化矽二極體

意法半導體推出新款車規碳化矽(SiC)二極體,以滿足電動汽車和插電式混合動力車(PHEVs,Plug-in Hybrids)等新能源汽車對車載充電器(OBCs,on-board battery chargers)在有限空間內處理大功率的苛刻要求。  

  新款二極體採用先進的技術可防止高電流突波燒毀裝置,其過電流保護是額定電流的2.5倍,因此設計人員可選用更小、更經濟實惠且可靠性和效能都不會受到影響的電流更小的二極體。此新碳化矽二極體通過車規產品測試,反向擊穿電壓提高到650V,能滿足設計人員和汽車廠商欲降低電壓補償係數的要求,以確保車載充電半導體元件的標準與瞬間峰值電壓之間有充足的安全邊際。   這次推出的650V二極體包括TO-220AC功率封裝的10A STPSC10H065DY和TO-220AC封裝的12A STPSC12H065DY。此外,TO-220AB封裝的STPSC20H065CTY和TO-247封裝的STPSC20H065CWY是內建2個10A二極體的雙二極體(dual-diode )產品,可最大幅度地提升空間利用度並減少車載充電器的重量。

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

【其他文章推薦】

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

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

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

※南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

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

※超省錢租車方案

硬件對同步的支持-TAS和CAS指令

目錄

  • Test and Set
  • Compare and Swap
    • 使用CAS實現線程安全的數據結構。

現在主流的多處理器架構都在硬件水平上提供了對併發同步的支持。
今天我們討論兩個很重要的硬件同步指令:Test-and-Set和Compare-and-Swap

Test and Set

一個Test-and-Set(TAS)指令包括兩個子步驟,把給定的內存地址設置為1,然後返回之前的舊值。
這兩個子步驟在硬件上實現為一個原子操作,執行期間不會被其他處理器打斷。
(一個CPU可以使用諸如Dual-port RAM电子原件提供的TAS指令,此外,CPU自身也可以提供CAS指令)
值得注意的是,TAS指令是在單個內存word(或字節)上實現,這限制了變量非0即1,並且TAS總是把變量設置為1。
可見,TAS生而為自旋鎖,下面是使用TAS實現自旋鎖的偽代碼[2]:

lock = 0 //shared state
while(test_and_set(lock)==0){ //try lock
       //do nothing   
}
// 臨界區代碼
lock = 0   //release 

當第一個線程執行這段代碼時,TAS指令會立即把lock設置為1,並返回0 ,線程退出while循環進入臨界區。
如果另一個線程嘗試進入臨界區,TAS會把lock設置為1,但是也會返回1(由第一個線程的TAS指令設置為1),
此時第二個線程會一直while循環(忙等待),直到第一個線程退出臨界區代碼,執行了lock=0,即釋放了鎖。
這種通過while-loop等待獲取鎖的實現稱為自旋鎖(spin lock)。

Compare and Swap

compare-and-swap(CAS)指令和TAS指令類似,但是比TAS要更複雜。
與TAS只有一個參數不同,CAS指令需要三個參數,一個內存位置(V)、一個期望舊值(A)、一個新值(B)。
CAS指令的執行過程:

1.比較內存V的值是否與A相等?
2.如果相等,則用新值B替換內存位置V的舊值
3.如果不相等,不做任何操作。
4.無論哪個情況,CAS都會把內存V原來的值返回。

偽代碼:

int CAS(int *ptr,int oldvalue,int newvalue)
{
   int temp = *ptr;
   if(*ptr == oldvalue)
       *ptr = newvalue
   return temp;
}

以上過程在CAS指令中是原子操作。
CAS支持32bit/64bit的數據類型,這使得CAS能夠實現一些更複雜的自旋鎖。

使用CAS實現線程安全的數據結構。

我們使用CAS實現一個併發的平衡二叉搜索樹。[1]
基本思路是,所有的更新操作都要在二叉樹的副本上進行,更新完成后,使用CAS指令把新的根節點引用替換舊的引用。

//共享變量
  root = pointer to the root of the tree
//插入操作
do
  old_root = root
  new_root = new Tree
  # copy old_root into new_root
  # do insertion into new_root
until compare_and_swap (root, old_root, new_root) == old_root
平衡操作:
do
  old_root = root
  new_root = balanced_copy_of (old_root)
until compare_and_swap (root, old_root, new_root) == old_root

如果并行的執行平衡操作和插入操作,插入操作完成後會把二叉樹的根節點指向新的引用。
等到平衡操作完成后,compare_and_swap將會失敗,因為此時根節點指向了插入操作產生的新地址,不再等於old_root,
平衡操作會重複循環執行,直到成功更新根節點。
同樣的,如果平衡操作先於插入操作完成,插入操作的CAS指令也會失敗(根節點已指向平衡操作產生的新地址),然後插入操作重複循環,直到成功。

基於CAS指令可以實現基礎的信號量、互斥量之外,還可以實現更複雜的lock-free和wait-free算法。

延伸閱讀:

1.康奈爾大學操作系統課件:Architectural support for synchronization
2.wiki:Test-and-Set指令
3.wiki:Compare-and-swap指令
4.TAS vs CAS

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

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

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

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

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

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

Python 簡明教程 — 19,Python 類與對象

微信公眾號:碼農充電站pro
個人主頁:https://codeshellme.github.io

那些能用計算機迅速解決的問題,就別用手做了。
—— Tom Duff

目錄

上一節 我們介紹了Python 面向對象的相關概念,我們已經知道類與對象是面向對象編程中非常重要的概念。

類就是一個模板,是抽象的。對象是由類創建出來的實例,是具體的。由同一個類創建出來的對象擁有相同的方法和屬性,但屬性的值可以是不同的。不同的對象是不同的實例,互不干擾。

1,類的定義

如下,是一個最簡單的類,實際上是一個空類,不能做任何事情:

class People:
    pass

在Python 中定義一個類,需要用到class 關鍵字,後邊是類名,然後是一個冒號:,然後下一行是類中的代碼,注意要有縮進。

2,創建對象

People 雖然是一個空類,但依然可以創建對象,創建一個對象的語法為:

對象名 = 類名(參數列表)

參數列表是跟__init__ 構造方法相匹配的,如果沒有編寫__init__ 方法,創建對象時,就不需要寫參數,如下:

>>> p = People()
>>> p
<__main__.People object at 0x7fd30e60be80>
>>> 
>>> p1 = People()
>>> p1
<__main__.People object at 0x7fd30e60be48>

p 和 p1 都是People類的對象。0x7fd30e60be80 是p 的地址,0x7fd30e60be48 是p1 的地址。可以看到不同的對象的地址是不同的,它們是兩不同的實例,互不干擾。

3,屬性

類中可以包含屬性(類中的變量),創建出來的對象就會擁有相應的屬性,每個對象的屬性的值可以不同。

創建好對象后,可以用如下方法給對象添加屬性:

>>> p = People()
>>> p.name = '小明' # 添加 name 屬性
>>> p.sex = '男'    # 添加 sex 屬性
>>> p.name         # 訪問對象的屬性
'小明'
>>> p.sex          # 訪問對象的屬性
'男'

雖然在技術上可以這樣做,但是一般情況下,我們並不這樣為對象添加屬性,這樣會破壞類的封裝性,使得代碼混亂,不利於維護。

當訪問一個不存在的屬性時,會出現異常:

>>> p.job         # 一個不存在的屬性
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'People' object has no attribute 'job'

我們一般會在__init__ 方法中為類添加屬性並賦值。

4,__init__ 方法

在Python 的類中,以雙下劃線__開頭和結尾的方法,被稱為魔法方法,每個魔法方法都有特定的含義。Python 為我們規定了一些魔法方法,讓我們自己實現這些方法。

__init__ 方法叫做構造方法,用來初始化對象。Python 解釋器會在生成對象時,自動執行構造方法,而無需用戶显示調用。

__init__ 方法不需要有返回值。

類中的所有實例方法 方法,都至少有一個參數,就是self。Python 中的self 相當於C++ 和Java 中的this 指針,都是代表當前對象。只是Python 中的self 需要显示寫在方法的第一個參數,而this 指針則不需要寫在方法參數中。

構造方法一般用於初始化對象的一些屬性,構造函數可以不寫,也可以只有一個self 參數。

當構造函數只有一個self 參數時,創建該類的對象時,不需要添加參數。當構造函數除了self 參數還有其它參數時,創建該類的對象時,則需要添加相匹配的參數。

比如,我們定義一個People 類,它有三個屬性,分別是name,sex,age:

class People:

    def __init__(self, name, sex, age):
        self.name = name
        self.sex = sex
        self.age = age
        print('執行了 __init__ 方法')

    def print_info(self):
        print('people:%s sex:%s age:%s' % (
            self.name, self.sex, self.age))

在這個People 類中除了有一個__init__ 方法外,還有一個print_info 方法,每個方法中的都有self 參數,並且是第一個參數,self 代表當前對象。

在創建該類的對象時,需要傳遞匹配的參數(self 參數不用傳遞):

>>> p = People('小明', '男', 18)
執行了 __init__ 方法
>>> p 
<People.People object at 0x7feb6276bda0>
>>> p.print_info()
people:小明 sex:男 age:18
>>>
>>> p1 = People('小美', '女', 18)
執行了 __init__ 方法
>>> p1
<People.People object at 0x7fd54352be48>
>>> p1.print_info()
people:小美 sex:女 age:18

可以看到,在創建p 和p1 對象時,字符串執行了 __init__ 方法 被打印了出來,而我們並沒有显示調用該方法,說明__init__ 方法被默認執行了。

對象p 和p1 是兩個不同的對象,擁有相同的屬性和方法,但是屬性值是不一樣的。兩個對象互不干擾,對象p 的地址為0x7feb6276bda0,p1 的地址是0x7fd54352be48。

執行代碼p.print_info(),是調用p 對象的print_info() 方法,因為,在定義該方法的時候,只有一個self 參數,所以在調用該方法的時候,不需要有參數。

5,私有屬性和方法

私有屬性

普通的屬性,就像上面的name,sex和age 屬性,都是公有屬性,在類的外部都可以被任意的訪問,就是可以用對象.屬性名的方式來訪問屬性,如下:

>>> p = People('小明', '男', 18)
執行了 __init__ 方法
>>> p.name  # 訪問屬性
'小明'
>>> p.name = '小麗'  # 修改屬性
>>> p.name  # 訪問屬性
'小麗'

這樣就破壞了數據的封裝性,這種訪問方式是不可控(會不受限制的被任意訪問)的,不利於代碼的維護,不符合面向對象的編程規範。

所以,通常我們會將類中的屬性,改為私有屬性,就是不能以對象.屬性名 這樣的方式訪問類屬性。

在Python 中,通過在屬性名的前邊添加雙下劃線__,來將公有屬性變為私有屬性,如下:

#! /usr/bin/env python3

class People:

    def __init__(self, name, sex, age):
        self.__name = name   # 兩個下劃線
        self.__sex = sex     # 兩個下劃線
        self._age = age      # 一個下劃線
        print('執行了 __init__ 方法')

    def print_info(self):
        print('people:%s sex:%s age:%s' % (
            self.__name, self.__sex, self._age))

這樣就無法通過對象.屬性名的方式來訪問屬性了,如下:

>>> p = People('小美', '女', 18)
執行了 __init__ 方法
>>> p.__name        # 出現異常
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'People' object has no attribute '__name'

但是,Python 中這種私有屬性的方式,並不是真正的私有屬性,Python 只是將__name 轉換為了_People__name,即是在__name 的前邊加上了_類名(_People),我們依然可以這樣訪問__name 屬性:

>>> p._People__name
'小美'

但我們並不提倡這種方式,這會讓代碼變得混亂難懂。

可以注意到,People 類中的_age 屬性是以單下劃線開頭的,這種以單下劃線開頭的屬性是可以在類的外部被訪問的:

>>> p._age
18

但是根據Python 規範,以單下劃線開頭的屬性,也被認為是私有屬性,也不應該在類的外部訪問(雖然在技術上是可以訪問的)。

注意:以雙下劃線__ 開頭且結尾的屬性__xxx__,是特殊屬性,是公有的,可在類的外部訪問

私有方法

私有方法與私有屬性類似,也可以在方法名的前邊加上雙下劃線__,來將某個方法變成私有的,一般不需要被外部訪問的方法,應該將其設置為私有方法。

6,set 和 get 方法

為了數據的封裝性,我們不應該直接在類的外部以對象.屬性名的方式訪問屬性,那麼如果我們需要訪問類的屬性該怎麼辦呢?

這時我們需要為每個私有屬性都提供兩個方法:

  • set 方法:用於設置屬性的值
  • get 方法:用於訪問屬性的值

為了減少代碼量,這裏只為__name 屬性設置了這兩個方法,代碼如下:

#! /usr/bin/env python3

class People:

    def __init__(self, name, sex, age):
        self.__name = name
        self.__sex = sex
        self._age = age
        print('執行了 __init__ 方法')

    def print_info(self):
        print('people:%s sex:%s age:%s' % (
            self.__name, self.__sex, self._age))

    # set 和 get 方法
    def set_name(self, name):
        self.__name = name

    def get_name(self):
        return self.__name

用戶可以這樣設置和訪問類的屬性:

>>> from People import People
>>> p = People('小美', '女', 18)
執行了 __init__ 方法
>>> p.get_name()         # 獲取 name 值
'小美'
>>> p.set_name('小麗')   # 設置新的值
>>> p.get_name()        # 再次獲取name 值
'小麗'

因為這種set 和 get 方法,是由類的開發者提供的,是被開發者控制的。

類的開發者會根據需要,來控制類的使用者如何使用該類,即哪些類的屬性和方法應該被使用者訪問,以及如何被使用者訪問。

如此,類的使用者就不能隨便的訪問類中的屬性,這就達到了封裝的目的。

(完。)

推薦閱讀:

Python 簡明教程 — 14,Python 數據結構進階

Python 簡明教程 — 15,Python 函數

Python 簡明教程 — 16,Python 高階函數

Python 簡明教程 — 17,Python 模塊與包

Python 簡明教程 — 18,Python 面向對象

歡迎關注作者公眾號,獲取更多技術乾貨。

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

【其他文章推薦】

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

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

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

※南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

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

※超省錢租車方案

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)](

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

【其他文章推薦】

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

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

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

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

※超省錢租車方案

人類的「故鄉」找到了! 20萬年前誕生於非洲波札那

摘錄自2019年10月29日中央社報導

科學家29日表示,人類祖先是在距今20萬年前誕生於非洲的波札那北部,當地現遍布沙漠和鹽地,但20萬年前卻是廣大的湖泊溼地,生機盎然。

科學期刊「自然」(Nature)刊出由澳洲雪梨大學(University of Sydney)和嘉文醫學研究所(Garvan Institute of Medical Research and University of Sydney)遺傳學家海斯(Vanessa Hayes)所率研究團隊做的這份研究。

法新社報導,現居南非與納米比亞的科伊桑人(Khoisan)是一支古老種族,他們體內有高比例的線粒體DNA支系L0,國際研究團隊從200名科伊桑人身上採集DNA樣本,和地理分布、考古、氣候變遷資料相比對。

在涵蓋現今波札那北部、納米比亞、辛巴威的地區,20萬年前有一個相當於目前維多利亞湖(Lake Victoria,面積約6.8萬平方公里)兩倍的大湖破裂後,形成廣袤的溼地,人類的祖先就誕生於此。

海斯說:「我們談的是構造上的現代人種,即現今的人類。現今人類事實上最老就是回溯到L0支系,這個支系全回溯到這個起源地。」

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

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

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

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

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