2019年1月10日 星期四

Julia語言程式設計——實例:體型分佈



Julia語言程式設計——實例:體型分佈
https://blog.csdn.net/conkty/article/details/81490456

使用Julia實現一個比Hello World更複雜點的例子:有1000個人的體型樣本,包括體重與身高兩項指標,不考慮性別和年齡因素,計算每個人的BMIBody Mass Index)指數,並根據關於肥胖的參考標準(見下表),統計各種體型分類的人數。為了程式設計的方便,在表中先對BMI分類進行了編號,對應1~6類。另外,在實現時,初始的樣本資料可採用亂數的方式生成。
                    1-1 體型BMI指數標準(資料來自網路)
BMI分類編號
BMI 分類
參考標準
相關疾病發病的危險性
1
體重過低
BMI < 18.5
低(但其它疾病危險性增加)
2
正常範圍
18.5 BMI < 24
平均水準
3
肥胖前期
24 BMI < 28
輕度增加
4
I度肥胖
28 BMI < 30
中度增加
5
II度肥胖
30 BMI < 40
嚴重增加
6
Ⅲ度肥胖
BMI 40.0
非常嚴重增加
下面,我們採用兩種不同的方式實現上述的需求:一種是基於陣列的方式,另外一種基於複合結構的方式,具體見下文。由於涉及的代碼會比較多,所以本例不再在REPL中演示,而在腳本中實現,詳細源碼可在https://gitee.com/juliaprog/bookexamples.git中下載。
·        陣列實現方式
首先在磁片中新建一個名稱為bmi_array.jl的檔,並使用常用的文字編輯器(例如VS CodeNotepad++SublimeUltraEditorAtom等)打開進行編輯。
第一步,便是使用隨機函數rand()生成原始資料,包括1000個人的身高及體重樣本。實現的代碼如下:
1.  # 使用均勻分佈亂數產生1000個 身高 樣本,取值範圍為 [0,1)
2.   
3.  heights = rand(Float64, 1000)
1.  # 使用均勻分佈亂數產生1000個 體重 樣本,取值範圍為 [0,1)
2.   
3.  weights = rand(Float64, 1000)
其中的heightsweights均是元素類型為Float64的陣列,長度為1000,內容類別似於:
1.  1000-element Array{Float64,1}:
2.   
3.   0.327989
4.   
5.   0.371755
6.   
7.   0.640065
8.   
9.   0.891165
10.  
11.  0.735425
12.  
13.  0.428819
14.  
15.  ... # 已省略
需要注意的是,隨機函數rand()的取值區間為[0,1),所以需要轉換到正常人的身高與體重範圍。我們採用區間映射的方式,函數為:
該函數可以將[amin, amax]中值a映射到區間[bmin, bmax]中的b值。

利用此函數,可以分別將身高heights陣列元素值均映射到[1.5, 1.8)區間,將體重weights陣列元素值映射到[30, 100)區間,即身高分佈在1.5~1.8米範圍同時體重分佈在30~100千克,具體實現語句為:
1.  # 將 身高 資料映射到 [1.5, 1.8)
2.   
3.  heights = heights .* (1.8-1.5)+1.5
4.   
5.   
6.   
7.  # 將 體重 資料映射到 [30, 100) 千克
8.   
9.  weights = weights .* (100-30)+30
由於涉及到標量與向量(陣列)的混合計算,所以採用了Julia特有的點操作(後文會進行深入地學習)。上述語句雖然涉及到陣列的逐元計算,但並沒有使用迴圈結構,語法極為簡潔直觀。
需要說明一點:這兩項資料呈現正態分佈是較為複合實現實情況的,不過為了轉換的方便,我們採用了均勻分佈的rand()函數,有興趣的讀者可以更換為randn()函數,來生成初始的樣本資料。
接下來,定義一個用於計算BMI指數的函數,如下:
bmi(w, h) = w / (h^2)
這是一種Julia式的函式定義方式,適用於實現簡短的函數。其中的bmi是函數名,(w, h)中的wh是輸入參數,分別是體重與身高值。
此後,便可基於已有的身高與體重資料計算上述1000個樣本的BMI指數了,實現如下:
indexes = broadcast(bmi, weights, heights)
按常規,此處應該有迴圈,但是並沒有。我們利用Julia特有的broadcast()函數,實現了將函數bmi()逐一施用於陣列weightsheights元素,並能夠自動取得兩個陣列的對應元素,自動將兩對元素作為bmi()函數的輸入參數,計算對應體型樣本的BMI指數。如果一定想用一下迴圈結構,可以如下實現:
1.  indexes = Array{Float64,1}(1000)              # 創建有1000個元素的一維陣列物件
2.   
3.  for i in 1:1000                              # 迴圈1000
4.   
5.      indexes[i] = bmi(weights[i], heights[i])      # 成對取得體重與身高,計算第i個樣本的BMI指數
6.   
7.  end
可見,這樣的方式中代碼量多出了很多,遠沒有上述的一句話實現簡潔。
更令人驚奇的是,對於BMI指數這種計算簡單的過程,完全可以不用預先定義bmi()函數,而是採用Julia特有的點操作符直接實現:
indexes = weights ./ (heights.^2)
這種同樣能夠將運算過程自動施用於weightsheights的元素中,而且仍不需要迴圈結構,表達即為高效、簡潔。在本書之後的內容中,我們便能夠深入瞭解這種逐元電腦制,即所謂的向量化計算方法。
BMI指數計算完成後,我們定義一個名為bmi_category的函數,用於對得到的指數進行分類,代碼如下:
1.  # BMI指數進行分類
2.   
3.  # 1-體重過低,2-正常範圍,3-肥胖前期,4-I度肥胖,5-II度肥胖,6-III度肥胖
4.   
5.  function bmi_category(index::Float64)
6.   
7.    class = 0
8.   
9.    if index < 18.5
10.  
11.      class = 1
12.  
13.   elseif index < 24
14.  
15.      class = 2
16.  
17.   elseif index < 28
18.  
19.      class = 3
20.  
21.   elseif index < 30
22.  
23.      class = 4
24.  
25.   elseif index < 40
26.  
27.      class = 5
28.  
29.   else
30.  
31.      class = 6
32.  
33.   end
34.  
35.  
36.  
37.   class            # 返回分類編號
38.  
39. end
由於該函數語句較多,所以該函數並沒有採用bmi()函式定義時那種“直接賦值”的方式,而是採用了帶有function關鍵字的常規函式定義方式,並利用if~elseif判斷結構實現了對輸入BMI指數值index進行逐層判斷。
在得到最終的分類編號class之後,需要將其返回。在Julia中,只需在函數結束的最後語句中直接列出該變數即可,顯式的return關鍵字不是必須的。
然後,便可通過該函數對indexes中的1000BMI指數進行分類了,實現語句為:
classes = bmi_category.(indexes)  #注意函數名之後一個小點號
同樣採用點操作實現了陣列的逐元計算。可見,Julia的點操作不僅適用於上述的運算子,也同樣適用于普通定義的函數。該語句執行後,會得到類似如下的結果:
1.  1000-element Array{Int64,1}:
2.   
3.   2
4.   
5.   5
6.   
7.   3
8.   
9.   1
10.  
11.  2
12.  
13.  1
14.  
15.  5
16.  
17.  ... # 其他已省略
最後,對classes中的類別編號進行統計:
1.  # 統計每個類別的數量
2.   
3.  for c in [1 2 3 4 5 6]             # 遍歷6個類別,c為類別ID
4.   
5.    n = count(x->(x==c), classes)   # x->(x==c)為匿名函數
6.   
7.    println("category ", c, " ", n)    # 列印結果
8.   
9.  end
實現中使用了for迴圈結構對類別編號集合進行遍歷,逐一對各類型進行統計。其中的count()函數是Julia內置的,能夠對陣列中滿足條件的元素進行計數,而條件由該函數的第一個參數提供。條件參數需是一個函數物件,有一個輸入參數,並需返回布林型值。在上述代碼中,這個條件函數為x->(x==c),是Julia形式的匿名函數,等效於:
condition(x) = (x==c)
不過,因為簡短,又需作為另外一個函數的參數,所以匿名函數的定義方式是非常合適的。
至此,需求需要實現的功能全部完成了。我們打開的REPL,執行以下語句:
1.  julia> include("/path/to/bmi_array.jl")     # 檔路徑根據實際情況提供
2.   
3.  便可獲得最終的結果,顯式的內容類別似於:
4.   
5.  category 1 291   # 體重過低共計291
6.   
7.  category 2 221
8.   
9.  category 3 151
10.  
11. category 4 77
12.  
13. category 5 238
14.  
15. category 6 22    # Ⅲ度肥胖共計22
這裡,我們總結一下:在整個實現中,資料流程主要以陣列結構表達,並在對陣列的逐元操作中,利用Julia的點操作及braodcast()函數兩種方式進行向量化計算,避免了大量的迴圈結構,代碼的實現極為簡潔、高效、直觀。
·        複合類型實現方式
下面,我們再嘗試另外一種實現方式。同樣地,在磁片中新建一個名稱為bmi_struct.jl的指令檔,並使用文字編輯器進行編輯。
首先,定義個複合結構類型,包括四個成員欄位,分別表示某個人的身高、體重、BMI指數及BMI分類編號。
1.  mutable struct Person
2.   
3.    height       # 身高,單位米
4.   
5.    weight      # 體重,單位千克
6.   
7.    bmi         # 計算得到的BMI指數
8.   
9.    class        # 根據BMI指數計算得到的分類標識
10.  
11.               # 1-體重過低,2-正常範圍,3-肥胖前期,4-I度肥胖,5-II度肥胖,6-III度肥胖
12.  
13. end
再定義一個集合類型,用於容納樣本資料,如下:
people = Set{Person}()
隨之使用均勻分佈生成1000個體型資料,並放入集合people中:
1.  for i = 1:1000
2.   
3.    h = rand() * (1.8-1.5)+1.5        # 生成身高資料,並將其映射到[1.5, 1.8)區間
4.   
5.    w = rand() * (100-30)+30        # 生成體重資料,並將其映射到[30, 100)區間
6.   
7.    p = Person(h, w, 0, 0)           # 基於身高與體重資料創建Person物件p
8.   
9.  # BMI指數和分類編號均初始化為無效的0
10.  
11.  
12.  
13.   push!(people, p)               # 將物件放入集合people
14.  
15. end
然後定義bmi()函數,基於每個Person物件的身高及體重資料,計算其BMI指數並同時進行分類,代碼如下:
1.  function bmi(p::Person)
2.   
3.      p.bmi = p.weight/(p.height^2)      # 計算BMI指數
4.   
5.      p.class = bmi_category(p.bmi)      # 分類,得到類別ID,已在前文實現過
6.   
7.  end
函數bmi()中原型中的::Person用於限定輸入參數p變數只能是Person類型。
最後,遍歷people中的1000個樣本,對BMI類別分別進行統計:
1.  # 1000個樣本執行BMI計算,並統計分佈
2.   
3.  categories = Dict()            # 字典結構,記錄各類的人數
4.   
5.  for p people              # 遍歷1000個樣本
6.   
7.     bmi(p)                   # 計算BMI指數並分類,會直接修改p中的屬性欄位
8.   
9.     categories[p.class] = get(categories, p.class, 0) + 1    # p.class類的計數器累加
10.  
11. end
實現中,對Dict類型的物件categories採用了兩種訪問方式,一種是與陣列下標極為類似,用於獲得類別對應的計數器,另一種是get()函數,也是用於獲得類別對應的計數器內容,區別在於後者能夠在categories還不存在鍵p.class時,也能夠返回有效值(預設值0)。
其中通過字典結構了不同類型的計數。
完成後,列印BMI分類統計的結果。方法極為簡單,只需直接列出變數名:
categories
至此,功能全部實現了。打開的REPL,執行以下語句:
julia> include("/path/to/bmi_struct.jl")     # 檔路徑根據實際情況提供
便可獲得最終結果,內容類別似於:
1.  Dict{Any,Any} with 6 entries:
2.   
3.    4 => 89
4.   
5.    2 => 219
6.   
7.    3 => 158
8.   
9.    5 => 234
10.  
11.   6 => 18        # Ⅲ度肥胖共計18
12.  
13.   1 => 282       # 體重過低共計282
不過注意Dict中的資料是無序的,所以列印的內容中沒有按照類別ID排列。
有別於第一種實現方式,本方式採用複合類型對資料和操作進行了封裝,在業務概念或邏輯上能夠顯得更為條理清晰,而且整個實現過程也並不複雜。


沒有留言:

張貼留言

WOKWI DHT22 & LED , Node-Red + SQLite database

 WOKWI DHT22 & LED , Node-Red + SQLite database Node-Red程式 [{"id":"6f0240353e534bbd","type":"comment&...