線性表的鏈式存儲–單鏈表

Java之線性表的鏈式存儲——單鏈表

我們都知道,線性表的存儲結構分為兩種,順序存儲結構和鏈式存儲結構,線性表的分類可以參考下圖來學習記憶。今天我們主要來學習一下鏈式存儲結構。

一、鏈式存儲介紹

“鏈式存儲結構,地址可以連續也可以不連續的存儲單元存儲數據元素”——來自定義。

其實,你可以想象這樣一個場景,你想找一個人(他的名字叫小譚),於是你首先去問 A , A 說他不知道,但是他說 B 可能知道,並告訴了你 B 在哪裡,於是你找到 B ,B 說他不知道,但是他說 C 可能知道,並告訴了你 C 的地址,於是你去找到 C ,C 真的知道小譚在何處。

上面場景其實可以幫助我們去理解鏈表,其實每一個鏈表都包含多個節點,節點又包含兩個部分,一個是數據域(儲存節點含有的信息),一個是指針域(儲存下一個節點或者上一個節點的地址),而這個指針域就相當於你去問B,B知道C的地址,這個指針域就是存放的 C 的地址。

鏈表下面其實又細分了3種:單鏈表、雙向鏈表和循環鏈表。今天我們先講單鏈表。

二、單鏈表介紹

什麼是單鏈表呢?單鏈表就是每一個節點只有一個指針域的鏈表。如下圖所示,就是一個帶頭節點的單鏈表。下面我們需要知道什麼是頭指針,頭節點和首元節點。

頭指針:指向鏈表節點的第一個節點的指針

頭節點:指在鏈表的首元節點之前附設的一個節點

首元節點:指在鏈表中存儲第一個實際數據元素的節點(比如上圖的 a1 節點)

三、單鏈表的創建

單鏈表的創建有兩種方式,分別是頭插法和尾插法。

1、頭插法

頭插法,顧名思義就是把新元素插入到頭部的位置,每次新加的元素都作為鏈表的第一個節點。那麼頭插入法在Java中怎麼實現呢。首先我們需要定義一個節點,如下

public class ListNode {
  public int val; //數據域
  public ListNode next;//指針域
}

然後我們就創建一個頭指針(不帶頭節點)

//元素個數
int n = 5;
//創建一個頭指針
ListNode headNode = new ListNode();
//頭插入法
headNode= createHead(headNode, n);

然後創建一個私有方法去實現頭插法,這裏我們插入5個新元素,頭插入的核心是要先斷開首元節點和頭指針的連接,也就是需要先將原來首元節點的地址存放到新節點的指針域里,也就是 newNode.next = headNode.next,然後再讓頭指針指向新的節點 headNode.next = newNode,這兩步是頭插入的核心,一定要理解。

/**
 * 頭插法
 * 新的節點放在頭節點的後面,之前的就放在新節點的後面
 * @param headNode 頭指針
 * @return
 */
private static ListNode createHead(ListNode headNode, int n) {
  //插入5個新節點
  for (int i = 1; i <= n; i++) {
    ListNode newNode = new ListNode();
    newNode.val = i;
    //將之前的所有節點指向新的節點(也就是新節點指向之前的所有節點)
    newNode.next = headNode.next;
    //將頭指針指向新的節點
    headNode.next = newNode;
  }
  return headNode;
}

最後我把鏈表打印輸出一下(其實也是單鏈表的遍歷),判斷條件就是只有當指針域為空的時候才是最後一個節點。

private static void printLinkedList(ListNode headNode) {
  int countNode = 0;
  while (headNode.next != null){
    countNode++;
    System.out.println(headNode.next.val);
    headNode = headNode.next;
  }
  System.out.println("該單鏈表的節點總數:" +countNode);
}

最後的輸出結果顯然是逆序,因為沒一個新的元素都是從頭部插入的,自然第一個就是最後一個,最後一個就是第一個:

2、尾插法

尾插法,顧名思義就是把新元素插入到尾部的位置(也就是最後一個位置),每次新加的元素都作為鏈表的第最後節點。那麼尾插法在 Java 中怎麼實現呢,這裏還是採用不帶頭節點的實現方式,頭節點和頭指針和頭插入的實現方式一樣,這裏我就直接將如何實現:

/**
 * 尾插法
 * 找到鏈表的末尾結點,把新添加的數據作為末尾結點的後續結點
 * @param headNode
 */
private static ListNode createByTail(ListNode headNode, int n) {
  //讓尾指針也指向頭指針
  ListNode tailNode = headNode;
  for (int i = 1; i <= n; i++) {
    ListNode newNode = new ListNode();
    newNode.val = i;
    newNode.next = null;

    //插入到鏈表尾部
    tailNode.next = newNode;
    //指向新的尾節點,tailer永遠存儲最後一個節點的地址
    tailNode = newNode;

  }
  return headNode;
}

和頭插入不同的是,我們需要聲明一個尾指針來輔助我們實現,最開始,尾指針指向頭指針,每插入一個元素,尾指針就后移一下,這裏我們來講一下原理:每次往末尾新加一個節點,我們就需要把原來的連接斷開,那怎麼斷開呢,我們首先需要讓尾指針指向新的節點,也就是 tailNode.next = newNode; 然後再讓尾指針后移一個位置,讓尾指針指向最後一個節點。也就是尾指針始終指向最後一個節點,最後將頭指針返回,輸出最後結果:

四、單鏈表的刪除

既然單鏈表創建好了,怎麼在鏈表裡面刪除元素呢,單鏈表的刪除,我分為了兩種情況刪除,分別是刪除第i個節點和刪除指定元素的節點。

1、刪除第i個節點

我們可以先來理一下思路:在單鏈表裡,節點與節點之間都是通過指針域鏈接起來的,所以如果我們想實現刪除的操作,實際上是需要我們去改變相應指針域對應得地址的。當想去刪除第i個元素的時候,比如要刪除上圖的第3個元素(也就是3),實際上我們要做的就是要讓2號元素指向4號元素(其實就是需要修改2號元素的指針域,讓2號元素的指針域存儲4號元素)。那麼怎麼做才能實現這一步呢?很顯然,要實現這個步驟,我們必須要找到4號元素和2號元素,但是再仔細想一下,其實我們只需要找到2號元素就可以了,因為4號元素的地址存儲再2號的下一個元素的指針域裏面。

所以綜上所述分析我們可以得出刪除的兩個核心步驟:

1.刪除第i個節點,需要先找到第 i-1 個個節點,也就是第i個節點的前一個節點;

2.然後讓第 i-1 個節點指向第 i-1 個節點的下下個節點

下面的代碼具體實現了怎麼刪除第i個元素。

/**
 * 刪除第i個節點
 * 1,2 4,4,5
 * 刪除之後應該是1,2,4,5
 * @param headNode
 * @param index
 * @return
 */
public static ListNode deleteNodeByIndex(ListNode headNode, int index) {
  int count = 1;
  //將引用給它
  ListNode preNode = headNode;
  //看計數器是不是到了i-1,如果到了i-1,就找到了第i-1個節點
  while (preNode.next != null && count <= index -1){
    //尋找要刪除的當前節點的前一個節點
    count++;
    preNode = preNode.next;
  }
  if (preNode != null){
    preNode.next = preNode.next.next;
  }
  return headNode;
}

2、刪除指定元素的那個節點

刪除指定元素節點的實現方法有兩種,第一種就是先找到指定元素對應的鏈表的位置( index ),然後再按照刪除第 i 個節點的思路刪除即可。實現方法如下圖所示:

/**
 * 刪除鏈表指定數值的節點
 * @param headNode
 * @param val
 * @return
 */
private static ListNode deleteNodeByNum(ListNode headNode, int val) {
  ListNode deleteOne = headNode;
  int countByDeleteOne = 1;
  while (deleteOne.next != null){
    if (deleteOne.next.val == val){
      deleteOne = deleteOne.next;
      break;
    }
    countByDeleteOne ++;
    deleteOne = deleteOne.next;
  }
  return deleteNodeByIndex(headNode, countByDeleteOne);
}

第二種方法的實現就很精妙(前提是此節點不是尾節點)

public void deleteNode(ListNode node) {
  //刪除node即通過將後面的值賦給node,然後更改node的指針指向下下一個結點即可
  node.val = node.next.val;
  node.next = node.next.next;
}

五、單鏈表的查詢(及修改)

單鏈表的查詢實現很簡單,就是遍歷當前單鏈表,然後用一個計數器累加到當前下標,那麼當前的這個節點就是要查詢的那個節點,然後再返回即可,當然需要判斷傳過來的這個下標是否合法。當然如果需要修改,就需要把當前找到的節點的數據域重新賦上需要修改的值即可,這裏就不上代碼了。具體實現如下:

private static ListNode searchLinkedList(ListNode headNode, int index) {
  //如果下標是不合法的下標就表示找不到
  if (index < 1 || index > getLinkedListLength(headNode)){
      return null;
  }
  for (int i = 0; i < index; i++) {
    headNode = headNode.next;
  }
  return headNode;
}

獲取單鏈表的長度(注意我這裏定義的 headNode 是頭指針不是頭節點)

/**
 * 求單鏈表長度
 * @param headNode
 * @return
 */
private static int getLinkedListLength(ListNode headNode) {
  int countNode = 0;
  while (headNode.next != null){
    countNode++;
    headNode = headNode.next;
  }
 return countNode;
}

六、小結

單鏈表的相關操作就講解完了,其實通過上面對單鏈表的相關操作,我們不難發現,單鏈表的刪除和插入其實很方便,只需要改變指針的指向就可以完成,但是查找元素的時候就比較麻煩,因為在查找的時候,需要把整個鏈表從頭到尾遍歷一次。

公眾號:良許Linux

有收穫?希望老鐵們來個三連擊,給更多的人看到這篇文章

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

Docker 基礎知識 – Docker 概述

Docker 是一個開發、發布和運行應用程序的開放平台。Docker使您能夠將應用程序與基礎架構分離,以便快速交付軟件。有了 Docker,你可以像管理應用程序一樣管理你的基礎設施。通過利用 Docker 快速發布、測試和部署代碼的方法,您可以顯著減少編寫代碼和在生產環境中運行它之間的延遲。

Docker 平台

Docker 提供了在鬆散隔離的環境(稱為容器)中打包和運行應用程序的能力。隔離和安全性允許您在給定的主機上同時運行多個容器。容器是輕量級的,因為它們不需要額外的hypervisor負載,而是直接在主機的內核中運行。這意味着您可以在給定的硬件組合上運行比使用虛擬機時更多的容器。你甚至可以在實際是虛擬機的主機中運行 Docker 容器!

Docker 提供了工具和平台來管理容器的生命周期:

  • 使用容器開發應用程序及其支持組件。
  • 容器成為分發和測試應用程序的單元。
  • 準備就緒后,將應用程序作為容器或編排好的服務部署到生產環境中。無論您的生產環境是本地數據中心、雲提供商還是兩者的混合,操作都是一樣的。

Docker 引擎

Docker 引擎是一個 客戶端-服務器 應用程序,具有以下主要組件:

  • 一個服務器,它是一種稱為守護進程(dockerd 命令)的長時間運行程序。
  • 一個 REST API,它指定程序可以用來與守護進程對話並指示它做什麼的接口。
  • 一個命令行界面(CLI)客戶端(docker命令)。

CLI 使用Docker REST API通過腳本或直接CLI命令控制Docker守護進程或與之交互。
許多其他Docker應用程序使用底層API和CLI。

這個守護進程創建和管理 Docker 對象,如鏡像、容器、網絡和卷(images, containers, networks, and volumes)。

注意: Docker使用的是開源 Apache 2.0 許可證。

有關更多細節,請參閱下面的 Docker 架構。

我可以用 Docker 做什麼?

快速、一致地交付應用程序

Docker 允許開發人員使用提供應用程序和服務的本地容器,在標準化的環境中工作,從而簡化了開發生命周期。容器對於持續集成和持續交付(CI/CD)工作流非常有用。

考慮以下示例場景:

  • 開發人員在本地編寫代碼,並使用 Docker 容器與同事共享他們的工作。
  • 他們使用 Docker 將應用程序推送到測試環境,並執行自動和手動測試。
  • 當開發人員發現 bug 時,他們可以在開發環境中修復它們,並將它們重新部署到測試環境中進行測試和驗證。
  • 當測試完成時,向客戶提供修復就像將更新后的鏡像推送到生產環境一樣簡單。

響應式部署和擴展

Docker 的基於容器的平台允許高度可移植的工作負載。Docker 容器可以運行在開發人員的本地筆記本電腦上、數據中心的物理或虛擬機上、雲提供商上或在混合的環境中。

Docker 的可移植性和輕量級性質也使得它可以很容易地動態管理工作負載,根據業務需要,在接近實時的情況下擴展或拆除應用程序和服務。

在相同硬件上運行更多工作負載

Docker 是輕量級和快速的。它為基於管理程序的虛擬機提供了一種可行的、經濟有效的替代方案,因此您可以使用更多的計算能力來實現業務目標。Docker 非常適合高密度環境和中小型部署,在這些環境中,您需要用更少的資源做更多的事情。

Docker 架構

Docker 使用客戶端-服務器架構。Docker 客戶端與 Docker 守護進程通信,後者負責構建、運行和分發Docker 容器等繁重的工作。Docker 客戶端和守護進程可以運行在同一個系統上,或者您可以將一個 Docker 客戶端連接到一個遠程 Docker 守護進程。Docker 客戶端和守護進程通過 UNIX 套接字或網絡接口使用 REST API 進行通信。

Docker 守護進程

Docker 守護進程(dockerd)偵聽 Docker API 請求並管理 Docker 對象,如鏡像、容器、網絡和卷。
守護進程還可以與其他守護進程通信來管理 Docker 服務。

Docker 客戶端

Docker 客戶端(docker)是許多 Docker 用戶與 Docker 交互的主要方式。當您使用諸如docker run之類的命令時,客戶端將這些命令發送給dockerd, dockerd 會執行這些命令。docker 命令使用 Docker API。Docker 客戶端可以與多個守護進程通信。

Docker 註冊表

Docker 註冊表存儲 Docker 鏡像。
Docker Hub 是一個任何人都可以使用的公共註冊表,默認情況下 Docker 被配置為在 Docker Hub 上尋找鏡像。您甚至可以運行自己的私有註冊表。如果您使用 Docker 數據中心(DDC),它包括 Docker 可信註冊表(DTR)。

當您使用 docker pulldocker run 命令時,所需的鏡像將從配置的註冊表中拉取。當您使用 docker push 命令時,您的鏡像將被推送到您配置的註冊表中。

Docker 對象

當您使用 Docker 時,您正在創建和使用鏡像、容器、網絡、卷、插件和其他對象。本節簡要介紹其中一些對象。

鏡像(IMAGES)

鏡像是一個只讀模板,帶有創建 Docker 容器的指令。鏡像通常基於另一個鏡像,並進行一些額外的定製。例如,您可以構建基於 ubuntu 鏡像的鏡像,但是安裝了 Apache web server 和您的應用程序,以及運行應用程序所需的配置細節。

您可以創建自己的鏡像,也可以只使用其他人創建併發布在註冊表中的鏡像。要構建自己的鏡像,需要創建一個 Dockerfile,其中包含一個簡單的語法,用於定義創建鏡像並運行它所需的步驟。Dockerfile 中的每條指令都會在鏡像中創建一個層。當你改變 Dockerfile 並重建鏡像時,只有那些已經改變的層才會重建。這是使鏡像與其他虛擬化技術相比如此輕量級、小巧和快速的原因之一。

容器(CONTAINERS)

容器是鏡像的可運行實例。您可以使用 Docker API 或 CLI 創建、啟動、停止、移動或刪除容器。您可以將一個容器連接到一個或多個網絡,將存儲附加到該容器,甚至基於其當前狀態創建一個新鏡像。

默認情況下,容器與其他容器及其主機相對隔離良好。您可以控制容器的網絡、存儲或其他底層子系統與其他容器或主機的隔離程度。

容器是由它的鏡像以及創建或啟動它時提供給它的任何配置選項定義的。當刪除容器時,對其狀態的任何未存儲在持久存儲中的更改都會消失。

docker run 命令示例

下面的命令運行一個 ubuntu 容器,以交互方式連接到本地命令行會話,並運行 /bin/bash

$ docker run -i -t ubuntu /bin/bash

當你運行這個命令時,會發生以下情況(假設你使用默認的註冊表配置):

  1. 如果你沒有本地的 ubuntu 鏡像,Docker會從你配置的註冊表中拉取它,就像你已經手動運行 docker pull ubuntu 一樣。
  2. Docker 創建一個新的容器,就像手動運行 docker container create 命令一樣。
  3. Docker 為容器分配一個讀寫文件系統,作為容器的最後一層。這允許運行中的容器在其本地文件系統中創建或修改文件和目錄。
  4. Docker 創建一個網絡接口,將容器連接到默認網絡,因為您沒有指定任何網絡選項。這包括為容器分配IP地址。默認情況下,容器可以使用主機的網絡連接連接到外部網絡。
  5. Docker 啟動容器並執行 /bin/bash。由於容器以交互方式運行並連接到你的終端(由於有-i-t標誌),所以可以將輸出記錄到終端,同時你可以使用鍵盤提供輸入。
  6. 當您鍵入 exit 終止 /bin/bash 命令時,容器將停止,但不會被刪除。您可以重新啟動或刪除它。

服務(SERVICES)

服務允許您跨多個 Docker 守護進程擴展容器,這些守護進程組成一個集群,多個管理者和工作者一起工作。一個集群的每個成員都是一個 Docker 守護進程,所有的守護進程都使用 Docker API 進行通信。服務允許您定義所需的狀態,例如在任何給定時間必須可用的服務副本的數量。默認情況下,服務在所有工作節點之間進行負載均衡。對於消費者來說,Docker 服務看起來像一個單獨的應用程序。Docker 引擎在 Docker 1.12 及更高的版本支持集群模式。

底層技術

Docker 是用 Go 編寫的,並利用 Linux 內核的幾個特性來實現其功能。

命名空間

Docker 使用名為命名空間的技術來提供稱為容器的隔離工作區。當您運行一個容器時,Docker 為該容器創建一組命名空間。

這些命名空間提供了一個隔離層。容器的每個方面都在一個單獨的命名空間中運行,其訪問權限僅限於該命名空間。

Docker 引擎在 Linux 上使用如下命名空間:

  • pid 命名空間: 進程隔離 (PID: 進程ID)。
  • net 命名空間: 管理網絡接口 (NET: Networking)。
  • ipc 命名空間: 管理對 IPC 資源的訪問 (IPC: 進程間通信)。
  • mnt 命名空間: 管理文件系統掛載點 (MNT: Mount)。
  • uts 命名空間: 隔離內核標識符和版本標識符 (UTS: Unix分時系統)。

控制組

Linux 上的 Docker 引擎還依賴於另一種稱為控制組(cgroups)的技術。cgroup 將應用程序限製為特定的資源集。控制組允許 Docker 引擎將可用的硬件資源共享給容器,並可以選擇強制限制和約束。例如,可以限制特定容器的可用內存。

聯合文件系統

聯合文件系統,或 UnionFS,是通過創建層來操作的文件系統,使其非常輕便和快速。Docker 引擎使用 UnionFS 為容器提供構建塊。Docker 引擎可以使用多種 UnionFS 變體,包括 AUFS、btrfs、vfs 和 DeviceMapper。

容器格式

Docker 引擎將命名空間、控制組和 UnionFS 組合到一個稱為容器格式的包裝器中。默認的容器格式是 libcontainer。未來,Docker 可能會通過與 BSD Jails 或 Solaris Zones 等技術集成來支持其他容器格式。

作者 : Docker 官網
譯者 : 技術譯民
出品 : 技術譯站
鏈接 : 英文原文
公眾號:技術譯站

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

【C++和C#的區別雜談】后自增運算符的結算時機

C++和C#的前自增++n和后自增n++,都是先自增后取值先取值后自增的含義,但在複雜一點的賦值語句中,我發現細節上有很大的差異。

發現這個問題主要是一個無聊的晚上,我想搞清楚后自增是什麼時候結算,自己搗鼓了一下之後我把我自己的測試結果告訴了朋友,結果學java的朋友和我爭論了半天,最後發現同樣的代碼,大家輸出是不一樣的。之後我又用了C#寫了同樣的測試代碼,發現輸出和他java是一樣的,這讓我豁然開朗,遂寫下這篇博客希望分享我的結論,如果有錯誤的話,歡迎評論區指正。

前排提醒:C++和C#我都是用的VS2017,不同編譯環境的C++代碼輸出結果可能不一樣

先說我的結論:

C++:i++遇到順序點(逗號,分號)之後i才自增,即我一直以來認為的一條語句結束后結算。但++i前自增的值會影響賦值語句所有i的值。

C#:C#不允許使用‘,’逗號運算符,但也並非到’;’分號才結算,賦值語句是從左到右,按序取值,i++在取完i的值之後馬上就自增了,但不影響之前取的i的值,隻影響後續i的取值。

心得:實際開發還是盡量自增單行寫,第一個是可讀性好,第二個是不容易出問題。(應該也就只有筆試會出現以下實驗的代碼了)

結論看不明白的可以看看我無聊的小實驗:

最簡單常見的版本:

int arr[] = { 0,0,0 };
int n = 0;
arr[n] = n++;//語句a
for (int num : arr)
{
	cout << num << ' ';
}
//輸出是0 0 0,語句a結束后n自增到1

上面這段代碼其實在C#也是一樣的輸出,但如果下面這樣寫,結果就不一樣了。

C++版:

int arr[] = { 0,0,0 };
int n = 0;
arr[n++] = n;//語句a
for (int num : arr)
{
	cout << num << ' ';
}
//輸出是0 0 0,語句a結束后n自增到1,C++結果和上面的一樣

C#版:

int[] arr = { 0, 0, 0 };
int n = 0;
arr[n++] = n;//語句a
foreach (int num in arr)
{
	Console.Write(num + " ");
}
//輸出是1 0 0,現在應該能理解結論了,從左到右,先取n作為索引值,然後自增之後影響到了等號右邊的n的值
//語句a則為arr[0] = 1;

就是這樣的差異開始引申出了問題,也是讓我迷惑的開始,接下來我會通過更複雜的例子和反彙編的指令來解釋和證明我的結論,先列舉C++的部分,之後再C#,感興趣的可以往下看。

C++部分:

//代碼01
int arr[] = { 0,0,0 };
int n = 0;
arr[n++] = n + (n++) + ++n;
for (int num : arr)
{
	cout << num << ' ';
}
//輸出為0 3 0,具體操作流程為語句先結算了++n,使得n自增到1.而出現的兩個n++都在賦值語句結束后結算
//所以編譯結果為arr[1] = 1 + 1 + 1; 然後i自增兩次到3

以下為反彙編的結果:

一開始看到這個我有點懵逼,但是通過比對下面這段代碼的反彙編指令,就能看出來了。

//代碼02
int[] arr = { 0, 0, 0 };
int n = 0;
arr[++n] = n + (n++) + ++n;//把索引處的n++改為了++n
foreach (int num in arr)
{
	Console.Write(num + " ");
}
//輸出為0 0 6,具體操作流程為語句先結算兩次++n,使得n自增到2.而n++在賦值語句結束后結算
//所以編譯結果為arr[2] = 2 + 2 + 2; 然後i自增到3
//這下應該發現n在這條語句中取值的時候都是同樣的值了

以下為上面代碼02的反彙編結果:

通過和上面的指令比對可以看出來了,具體差異在00251F3A這段指令,arr[++n]這段代碼02在累加前多進行了一次對n的自增,然後將自增后的值賦給了n,然後開始進行累加,而上面arr[n++]那段代碼01是在累加結束之後,在01161F4A那條指令對n++進行自增的結算,所以就算看不懂全部的指令,也應該能通過比對這兩段代碼看出他們的差異,從而證明了C++對於后自增的處理,是在語句結束之後結算

說到語句結束,我之前寫了一篇關於逗號運算符的博客,可以結合今天這個結論看看下面這段代碼的輸出結果。

int arr[] = { 0,0,0 };
int n = 0;
arr[n] = (n++, n + (n++) + ++n);//在逗號左邊添加了n++的語句
for (int num : arr)
{
	cout << num << ' ';
}
//輸出為0 0 6
//原因為逗號也屬於一條語句結束的標誌,所以結算了n++,n=1,然後執行新的語句n + (n++) + ++n
//逗號右邊的語句先結算了++n,n=2,所以最後賦值語句為arr[2] = 2 + 2 + 2; 然後n++到3

至此C++的部分結束。

接下來C#的測試結果將顛覆上面的結論,如果不用java或者C#的話,下面的內容可能沒有用(朋友java的輸出結果和我C#一樣,不過也是希望大家自己試試,我自己沒有試過)

C#部分:

//代碼01 C#版
int[] arr = { 0, 0, 0 };
int n = 0;
arr[n++] = n + (n++) + ++n;
foreach (int num in arr)
{
	Console.Write(num + " ");
}
//輸出為5 0 0 和VC++完全不一樣對吧
//具體原因為C#的賦值語句是從左到右,先取了n的值作為索引,然後馬上對n自增,n=1
//來到等號右邊,第一個n取值為1,第二個n取值為1,然後自增到2,第三個n先自增到3,然後取值為3
//所以最終編譯結果為arr[0] = 1 + 1 + 3;  即5 0 0的原因
//為了節省篇幅,我直接告訴你arr[++n] = n + (n++) + ++n的結果為0 5 0,如果你上面看懂了這個應該也懂

貼一下C#這段代碼的反彙編結果:

可以看到第一行是取n的值,然後把n作為索引值,之後inc指令對n自增1。證明了上面的結論。

即:C#的賦值語句為從左到右,按序取值,n++在取完n的值之後馬上自增,但不影響之前取的值,隻影響後續n的取值

小結:

所以++n 先自增后取值n++ 先取值后自增是能直接套在C#上的,而對於VC++來說,后自增的結算是非常“緩慢”的,到語句結束才結算。

感謝您的觀看。

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

2015第六屆廣州國際新能源汽車工業展覽會

綠色科技   領航未來

時間:2015年5月16-18日

地點:廣州琶洲保利世貿博覽館

主辦單位:廣州中汽國際展覽有限公司

  • 專業的組織機構——展會是中汽國際展覽有限公司按照“專業化、國際化、品牌化”原則舉辦的新能源汽車及配套設施行業國際品牌盛會。國內外多家單位協辦,既有政府支持,又有行業權威的參與。展會舉行期間,將有商務部及省市領導、業界權威人士蒞臨展會參觀指導並出席開幕剪綵。
  • 龐大採購團隊——盛會將邀請來自中國、美國、德國、法國、英國、義大利、巴西、墨西哥、西班牙、俄羅斯、瑞典、捷克、匈牙利、中東、日本、韓國、印度、土耳其、新加坡、越南、泰國、臺灣、香港、澳門等國家和地區眾多新能源汽車及配套設施進出口貿易商、代理商、經銷商及國際著名相關採購協會組織率團到會參觀採購。
  • 高端論壇——展會將邀請新能源汽車及配套設施行業權威專家,討論中外新能源汽車及配套設施行業最新動態、發展趨勢、分工格局、相關對策等多個熱門議題。就中國新能源汽車及配套設施行業發展現狀及所面臨的問題作深入報告,針對中國新能源汽車及配套設施行業市場行情作研究報告,並對產品開發、技術創新等作細緻的演講。屆時,將安排我們認為在國際新能源汽車及配套設施領域,具有影響力的企業介紹產品概況、最新技術動向等進行講座和交流。擬邀請中國政府高級官員,有關專家、學者,國際著名機構代表,著名新能源汽車及配套設施產品供應商、採購商,中國新能源汽車及配套設施市場企業代表和其他專業觀眾出席,其行業的引導性和權威性令人期待,是中國新能源汽車及配套設施產業發展和國際交流合作的風向標,將是獲取新能源汽車及配套設施行業資訊和把握國際市場的最佳平臺。

展會介紹

在能源匱乏的時代,綠色、節能、環保成為經濟發展的核心主題,新能源汽車具有良好的環保性能和燃料經濟性好、運行成本低等優勢,既可以保護環境,又可以緩解能源的短缺並能調整能源的結構,保障能源的安全。

21世紀是一個面臨能源和環境巨大挑戰的世紀,傳統燃油汽車將向高效低排放的電動汽車及混合動力車方向發展。大力發展新能源汽車是能源與環境的必然要求,而且,中國發展新能源汽車的壓力更為緊迫。根據國家《汽車與新能源汽車產業發展規劃》(2011-2020),將在“十二五”期間重點發展清潔能源汽車,未來十年僅中央財政就投入上千億元用來支持以純電動車、混合動力汽車為代表的節能與新能源汽車的研發與推廣。2014年多地出臺補貼政策,2014年5月24日上午,國家主席習近平在上海汽車集團考察時強調,發展新能源汽車是我國從汽車大國邁向汽車強國的必由之路,要加大研發力度,認真研究市場,用好用活政策,開發適應各種需求的產品,使之成為一個強勁的增長點。可以預見,未來我國新能源汽車將會快速發展。

廣州是廣東省會,改革開放前沿城市,中國對外貿易的重要視窗,經濟實力雄厚,市場潛力巨大。廣東是泛珠三角經濟區域的中心,毗鄰港、澳、台,輻射東南亞,海、陸、空交通便利,市場輻射面廣,經濟發達。隨著CEPA的實施,粵、港、澳以及泛珠三角(9+2)區域合作與發展的良性互動,必將給新能源汽車及配套設施產業創造無限美好的發展前景。為順應高速發展的新能源汽車及配套設施行業,廣州中汽國際展覽有限公司聯合行業權威機構定于2015年6月8-10日在廣州琶洲保利世貿博覽館舉辦“2015第三屆廣州國際新能源汽車工業展覽會”(NEA CHINA 2015),展會將深化活動內涵,秉乘推動行業發展、為企業服務的宗旨,為商家提供一個拓展業務、技術交流、展示實力、獲取資訊、結交客戶、推廣新產品、尋找合作夥伴的國際商貿平臺。

我們將以“突出品牌、開拓創新、注重實效、強化服務”的辦展宗旨,憑藉獨特的創意,科學的組織管理和卓越的服務,以全新的理念為廣大中外參展商提供一個“高水準、高品味、高品質”的展示交流平臺,為全球新能源汽車及配套設施行業提供更多的合作機會,有力推動中國新能源汽車及配套設施產品全面進入全球採購體系,與世界各國新能源汽車及配套設施產業協調合作、互利共贏、共同發展進步。

展品範圍

  • 純電動車:轎車、大巴、公車、各旅行車、各種純電動特種車(環衛車、電力車、郵政車、小型客貨車、高爾夫車、房車、叉車、搬運車、旅遊觀光車、醫療車、警用車、摩托車、三輪車等);
  • 混合動力車:轎車、大巴、公車、各型旅行車等;
  • 其他能源車:超級電容、燃料電池、氫能、生物燃料、太陽能及氫能源、天然氣等各種新能源、清潔燃料及低排放、環保節能型車等;
  • 零部件:低排放節能型發動機、混合動力發動機及清潔燃料發動機;動力電池與管理系統;整車匯流排與控制系統;電機與電控系統;充電裝置;儲能裝置等;能源管理系統;電力電容器、飛輪、逆變器、電熱泵、電動助力轉向、電動空調、功率模組等;相關材料、工藝、技術;相關檢測、監控、試驗、安全防護裝備;維修、製造設備和工具等;
  • 充電設施:充電站智慧型網路專案規劃及成果展示,加油站擴建充(換)電站、加油充電綜合服務站展示,太陽能、風能互補新能源汽車充電站技術產品,充電站充電機、充電樁、配電設備、變壓器、更換設備、電能、監控系統、有源濾波裝置、配電櫃、電覽、直接充電設備、管理輔助設備、充換電池及電池管理系統、停車場充電設施、智慧監控、充電站供電解決方案、充電站等。
  • 其他:新能源汽車的整車及系統控制設計等。

目標觀眾

主辦單位將重點邀請的目標觀眾包括:

1、商務部、發改委、科技部、工信部、國家環保局等各局、司、中心、所領導;
2、全國各省市主管部門領導、大型企事業、機關單位領導;
3、全國各高校、科研單位、設計院、研究院、協(學)會領導;
4、公交、出租、環衛、郵政等單位負責人;車站、機場、碼頭、房地產、大型物業公司、高爾夫球場、旅遊景點、公園、體育場館、大專院校、醫院、療養院、度假村等單位負責人;
5、國內外著名生產、代理、經銷商、貿易公司等業內人士參觀、參展、技術交流。

展會日程
報到布展:2015年5月14-15日
展出時間:2015年5月16-18日
撤展時間:2015年5月18日下午

歡迎業界同仁踴躍報名參展,現正接受申請,請速與組委會聯繫,索取參展申請表及展位平面圖!
充分利用NEA CHINA 2015,鞏固您的市場地位!

Official website/大會官方網站:

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

【其他文章推薦】

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

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

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

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

※超省錢租車方案

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

隨著環保意識抬頭,越來越多地方政府推政策因應環保趨勢。桃園縣環保局鼓勵民眾使用電動機車,除了購車補助額度位居全國第一,更推出免費試騎 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/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

意法半導體推出新款車規碳化矽(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/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

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>

pp1 都是People類的對象。0x7fd30e60be80p 的地址,0x7fd30e60be48p1 的地址。可以看到不同的對象的地址是不同的,它們是兩不同的實例,互不干擾。

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 類,它有三個屬性,分別是namesexage

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

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

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

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

5,私有屬性和方法

私有屬性

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

>>> 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,setget 方法

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

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

  • 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 值
'小麗'

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

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

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

(完。)

推薦閱讀:

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

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

【其他文章推薦】

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

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

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

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

※超省錢租車方案