0%

今天遇到了一個 python 程式語言繼承機制的問題,如以下程式碼所示,可以發現子類別 SuperMan 同時繼承了兩個父類別 WomanMan,而且這兩個父類別中都有一個叫 foo 的方法,那 SuperMan 到底會繼承哪一個並呼叫呢 ?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Woman:
@staticmethod
def foo():
print("I am a woman")

class Man:
@staticmethod
def foo():
print("I am man")

class SuperMan(Woman, Man):
pass

SuperMan.foo()

執行結果如以下,答案是會先呼叫在類別繼承括號中最左邊的的 Woman 類別的方法foo。若把兩個父類別順序對換,則這次會先從 Man開始執行。

1
I am a woman

而為什麼會這樣執行而不會出錯呢?這個背後的機制就是 python 的繼承順序機制演算法 MRO(Method Resolution Order)定義了繼承的走訪順序,執行以下的程式碼可以看到 MRO 裡面的類別繼承內容。

1
print(SuperMan.mro())

可以發現從最左邊 SuperMan 自己開始,後一個類別就是 WomanManobject,這裏要注意的是 python 的類別也是一種物件,因此繼承鍊最後也會指向到object

1
[<class '__main__.SuperMan'>, <class '__main__.Woman'>, <class '__main__.Man'>, <class 'object'>]

而 MRO 的繼承順序行為背後是有一個 python 的 C3 演算法來實作的,C3 演算法是 python3 的繼承順序機制,這個演算法改進了過去 python2 繼承演算法的問題,結合了 DFS 與 BFS的優缺點,關於 MRO 的詳細過往可以參考這篇文章的介紹
接著修改了以上的程式如下,在 WomanMan 之上又共同繼承了Human 類別,形成了一個棱形繼承的結構,那麼繼承順序會變得如何呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Human:
@staticmethod
def foo():
print("I am a human")

class Woman(Human):
@staticmethod
def foo():
print("I am a woman")

class Man(Human):
@staticmethod
def foo():
print("I am man")

class SuperMan(Woman, Man):
pass

SuperMan.foo()
print(SuperMan.mro())

程式執行結果如下:

1
2
I am a woman
[<class '__main__.SuperMan'>, <class '__main__.Woman'>, <class '__main__.Man'>, <class '__main__.Human'>,<class 'object'>]

如果是在棱形繼承的結構之下,MRO 的行為會類似 BFS ,從左邊開始以廣度優先的順序來繼承節點。

然而在不是棱形繼承的結構,而是一般的繼承結構又會如何呢? 這裏更改以上的程式如下,移除 ManHuman 的繼承關係。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Human:
@staticmethod
def foo():
print("I am a human")

class Woman(Human):
@staticmethod
def foo():
print("I am a woman")

class Man:
@staticmethod
def foo():
print("I am man")

class SuperMan(Woman, Man):
pass

SuperMan.foo()
print(SuperMan.mro())

程式的執行結果會改變如下:

1
2
I am a woman
[<class '__main__.SuperMan'>, <class '__main__.Woman'>, <class '__main__.Human'>, <class '__main__.Man'>,<class 'object'>]

可以發現繼承的行為改變了!原本繼承 Woman 之後繼承 Man 現在卻變成了繼承 HumanMRO 在非棱形的繼承結構的行為會與 DFS 相同,變成從左到右以深度優先的方式來繼承節點。

reference:
https://makina-corpus.com/blog/metier/2014/python-tutorial-understanding-python-mro-class-search-path
https://www.python-course.eu/python3_multiple_inheritance.php
https://read01.com/kRkykj.html

duck typeing 是程式語言的一種不需要用到繼承就可以有多型的特性。像 python 這種弱型別語言就有 duck typeing,以下程式碼展示了 duck typeing 的狀況:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Duck:
def scream(self):
print("Cuack!")

def walk(self):
print("Walking like a duck...")

class Person:
def scream(self):
print("Ahhh!")

def walk(self):
print("Walking like a human...")

def activate(duck):
duck.scream()
duck.walk()

if __name__ == "__main__":
Donald = Duck()
John = Person()
activate(Donald)
# this is not supported in other languages, because John
# is not a Duck object
activate(John)
1
2
3
4
Cuack!
Walking like a duck...
Ahhh!
Walking like a human...

程式中函式 activate 接收了 duck 物件當作參數並呼叫其中的方法,但由於 python 是弱型別語言,
可以發現函式 activate 不管接收哪種物件,只要物件中有跟 Duck 類別有一樣同名的方法,這些方法都會呼叫。強型別語言由於要在編譯前就嚴格指定參數的型別因此不會有 duck typeing 這個問題。

PCA 是一種特徵提取的技術,利用特徵降維來萃取出資料中最有代表性的主成分。而要注意的是 PCA 並不是從原本資料特徵中捨棄不重要的特徵來降維,而是由這些特徵與特徵向量 (eigenvector) 的線性組合所產生的新特徵來代表原本的資料。

X 表示一個 n x d 的資料集,n 是紀錄數,d 是資料維度也是特徵數,而 W 就是由 X 的共變異數矩陣所計算出來的特徵向量矩陣,d x k 維,其中 1 <= k <= d ,k 就是指定的主成分數,即表示要留下來的特徵向量個數, X*W 就是 PCA 計算結果,矩陣的維度變為 (n x d)*(d x k)=n x k ,如此達到降維的效果。

PCA 演算法步驟

  1. 標準化 d 維原資料集
  2. 建立共變異數矩陣(covariance matrix)
  3. 分解共變異數矩陣(covariance matrix)為特徵向量(eigenvector)與特徵值(eigenvalues)。
  4. 選取 k 個最大特徵值(eigenvalues)相對應k個的特徵向量(eigenvector),其中 k 即為新特徵子空間的維數。
  5. 使用排序最上面的 k 個的特徵向量(eigenvector),建立投影矩陣(project matrix)W。
  6. 使用投影矩陣(project matrix) W 轉換原本 d 維的原數據至新的 k 維特徵子空間。

PCA 異常偵測

PCA 本質上可以想成一個將資料去除雜質的方法,因此可以藉由比較資料去除雜質的差異,來找出這些雜質 (異常)。首先,可以簡單想成 PCA 是由一個原本矩陣 X 乘上投影矩陣 W 得到的結果。因此當然也就可以從 PCA 的結果乘上 W 的轉置矩陣來還原到原本的矩陣空間。因此得到以下公式:

$$\underset{n\times k}{Z} = \underset{n\times d}{X} \times\underset{d\times k}{W}$$

$$\underset{n\times d}{\bar{X}} = \underset{n\times k}{Z}\times\underset{k\times d}{W^\top}$$

$\underset{n\times d}{\bar{X}}$ 就是從 PCA 空間還原的結果,而計算異常的方式就是計算 $\underset{n\times d}{\bar{X}}$ 與 $\underset{n\times d}{X}$ 兩者的歐式距離,即所謂的 reconstruction error ,如下圖所示新的測試資料點與經過 PCA 還原的點的距離就是該點的 reconstruction error,距離越大表示越有可能為異常。

PCA 異常偵測缺點

  1. 不適合在資料量大的情況,拿全部的訓練資料來進行矩陣計算,資料量大時極耗運算資源。
  2. reconstruction error 必須要自行定義 threshold 值來判斷正常或是異常,無法自動得知。

參考資料:
https://stats.stackexchange.com/questions/259806/anomaly-detection-using-pca-reconstruction-error
https://www.kaggle.com/ericlikedata/reconstruct-error-of-pca/data
https://stats.stackexchange.com/questions/229092/how-to-reverse-pca-and-reconstruct-original-variables-from-several-principal-com
https://stats.stackexchange.com/questions/2691/making-sense-of-principal-component-analysis-eigenvectors-eigenvalues
http://arbu00.blogspot.tw/2017/02/6-principal-component-analysispca.html

記錄一些常用的 docker 指令以備不時之需。

  1. 從 Dockerfile 的所在位置建立 image。

    1
    $docker build -t imagename:tag .
    • build 指令參數 -t 用來指定新建立 image 的名稱與 tag。
    • 後面這個點 .表示 Dockerfile 所在的當前目錄,也表示要給 docker daemon build 的 context 路徑,只有在這個路徑之下檔案才會被正確的複製給 docker daemon 來製作
  2. 執行 docker image,這個指令會產生一個 container 來運行該 image

    1
    $docker run --rm -it -p 8000:8000 imagename:tag
    • -it表示顯示出 terminal 的交互環境。
    • -p <server port>:<container port> run 指令的數值參數,把主機的通訊埠所有流量轉發到 container 的通訊埠。
  3. 將 image 發佈到 dockerhub 中。

    1
    2
    3
    $docker login -u <username> -p <password>
    $docker tag imagename:tag namespace/imagename
    $docker push namespace/imagename
    • 首先需要登入個人的 dockerhub 帳號,接著用 tag 指令來指定要用哪個 local image 來 push 到 dockerhub 上,namespace 就是對應的個人帳號。
    • 接著在執行 push 指令就可以發佈了。

參考資料:
https://www.gitbook.com/book/yeasy/docker_practice/details

Transfer learning 是一個在機器學習領域的研究議題,大意是指將一個在某一種任務上學習 model 的知識可以轉移給其他的 model ,來去解不同但相近的任務,藉此來降低重新學習的成本。本人看到這種方法第一個聯想就好像是武俠小說中,一般人吃了25年功力的大還丹就變成絕世高手一樣…..

其中,Transfer learning 在深度學習的 CNN 更是用的火熱。因為深度學習的方法要訓練一個完好的模型所要花費的時間成本、資料量是非常貴的,以影像分類任務來說至少也要收集幾十萬張相關照片,來訓練個好幾天才會有好的結果,然而現實中根本不可能有那麼多大量的資料,因此在實務上深度學習的應用已很少完全重頭訓練模型,而是利用 transfer learning 來做 pre-training 任務。

Transfer learning 的應用上共有兩種:

  1. Feature extractor
    此應用將整個預訓練的 CNN 視為一個 feature extractor,去掉後面的 fully-connected layer,保留前面的捲積層部分當作固定的特徵萃取器,圖片經過捲積層部分的萃取過程後得到的新特徵叫做 CNN codes,之後在接上一個線性的分類器來做分類。
  2. Fine-tune
    此方法除了也將預訓練 CNN 模型後面的 fully-connected layer 去掉以外,也對前面的一部分捲積層做權重的微調,其他捲積層則固定權重不變,進而重新訓練模型來更新權重,微調的網路層深淺視使用情境而定,一般而言較深的網路層會有較高層次抽象的特徵,離資料集本身的特性越接近,而淺的網路層則是比較泛化的特徵,像是線條、顏色等等。

而使用 Transfer learning 的情境,會受到兩個關鍵因素所影響,分別是資料數量資料相似性,因此根據這兩個關鍵因素共可分成以下四種使用策略,如以下這張圖所示:

  1. New dataset is large and similar to the original dataset
    首先是圖中第一象限的情況,資料量多而且性質非常相似,這可以說是最好的情況了,由於資料量很多,因此不怕有 overfitting ,可以採取 fine-tune 對一部分捲積層做權重的微調來重新訓練整個網路。
  2. New dataset is large and very different from the original dataset
    第二象限資料量大且很不同,這種情況其實就可以直接從無到有訓練自己的CNN了,但實際上使用預訓練的網路模型來當作初始的權重還是有助益的,因此這種情況的訓練策略為使用預訓練模型的權重當作初始權重,之後再重新訓練整個網路。
  3. New dataset is small and similar to original dataset
    第三象限由於資料量極少,若採取 fine-tune 方式重新訓練整個網路會有 overfitting 的問題,然而訓練資料與預訓練模型的訓練資料非常相近,因此我們預期深的網路層的抽象特徵與資料本身特性夠接近,採取的作法為 feature extractor,得到 CNN codes 後在其後面接上一個線性分類器,並只訓練這個線性分類器來分類。
  4. New dataset is small but very different from the original dataset
    最後第四象限,資料量不但少且與預訓練模型的訓練資料差異極大,這時候由於資料量少,因此不適合使用 fine-tune 重新訓練整個網路,而將預訓練模型當成 feature extractor 呢?這裏要注意的是由於資料性質差異大,因此就不能使用後面網路的深層特徵了,因為這些特徵只與預訓練模型的資料性質很接近,但與要訓練的新資料特性可是天差地遠了,所以要做 feature extractor 的話,要抽取網路層前面較淺且較泛化的 CNN codes 當特徵,後面在接上線性分類器來訓練。

除了以上兩種應用與四種情境以外,使用 Transfer learning 訓練需注意的另一點為要使用較小的 learning rate,這是因為預訓練模型卷積層的權重已有較好的調校,若使用過大的 learning rate 反而會把本來準確的權重給破壞了。

參考資料:
https://towardsdatascience.com/transfer-learning-using-keras-d804b2e04ef8
http://cs231n.github.io/transfer-learning/#tf

這是我 tensorflow 在 unbantu 16.04 的安裝紀錄。

安裝套件版本:
tensorflow 1.3.0
cuda 8.0.61
cudnn 6.0 for cuda 8

nvidia下載 cuda 以及 cudnn 之後,在安裝前請重新開機進入 BIOS 設定把預設顯示卡功能調整成用內顯,並調整螢幕插頭位置到內顯插槽上,之後按下 Ctrl-Alt-F1 進入 tty1 介面,並輸入以下指令

1
sudo service lightdm stop

根據這篇的說法這條指令的作用是用來達到顯示卡與內顯區別的目的,如果不打這條指令就裝 cuda 的話,重新開機後 unbantu 會畫面全黑或是卡在系統 loading 畫面。
(ps:本人就是這樣才重新裝第二次!)

之後就可以開始安裝 cuda 了,輸入以下安裝指令來安裝。這裡有一點要注意的是要指定 cuda=version 來安裝,不指定的話預設裝上的是 cuda9

1
2
3
$ sudo dpkg --install cuda-repo-ubantu1604_8.0.61-1_amd64.deb
$ sudo apt-get update
$ sudo apt-get install cuda=8.0.61-1

裝完後到 .bashrc 貼上以下環境變數路徑,之後記得先重開機看看能不能順利入系統

1
2
export PATH=$PATH:/usr/local/cuda-8.0/bin
export LD_LIBRARY_PATH=/usr/local/cuda-8.0/lib64

接著開始來安裝 cudnn,輸入以下指令

1
2
3
4
$ tar -xvzf cudnn-8.0-linux-x64-v6.0.tgz
$ sudo cp cuda/include/cudnn.h /usr/local/cuda/include
$ sudo cp cuda/lib64/libcudnn* /usr/local/cuda/lib64
$ sudo chmod a+r /usr/local/cuda/include/cudnn.h /usr/local/cuda/lib64/libcudnn*

以上都裝好的話可以打以下指令來 check 顯卡是否有裝好

1
$ nvidia-smi

之後就是裝 tensorflow 了,基本上都跟官網一樣,沒什麼不同。
本人採用的是 Anaconda 的安裝環境,創一個虛擬的環境後打以下指令

1
$ pip install https://storage.googleapis.com/tensorflow/linux/gpu/tensorflow_gpu-1.3.0-cp35-cp35m-linux_x86_64.whl

到這邊就差不多裝好囉!之後就可以在裝 keras, pandas, numpy 等等其他資料分析常用的套件啦!

參考資料
http://city.shaform.com/blog/2016/10/31/install-tensorflow-with-cuda.html

NumPy 乃是 python 做資料分析的一項重要的函式庫工具,因此有必要理解其底層的實現細節,本文將記錄我對 NumPy 資料結構的理解。首先,由於 python 本身的物件型別有限,很難去支援複雜的科學計算,因此就有了 NumPy 這個函式庫,藉由擴充更多的型態類別定義以支援更複雜的科學計算任務。首先介紹的是ndarray物件, 這是 NumPy 裡用來儲存數組的 container 結構,ndarray 的 size 是固定的,當ndarray物件建立時會分配一段固定大小的連續記憶體空間,同時 ndarray 也是同質性的,裡面的所存放的 items 也都是固定的 size 與 type。 而決定這一個ndarray陣列物件的資料型別是由另一個物件dtype 所決定。 每一個 ndarray 陣列中的 item 都會被視為獨立的dtype物件,dtype即定義ndarray中所存放的 item 是哪一種型別以及大小,關係示意圖如下圖所示:

其中,根據我的理解圖中的 array scalar 就是 dtype 所定義的資料型別,根據 NumPy 文件的定義共有 24 種。彼此間也有繼承的關係,如下圖所示:

可以看出最上層為一個 generic 型別,之後一一往下繼承並擴充,比如我們要 check float32 是否為 number 的子型別,可如下測試:
>>> np.issubdtype(np.float32, np.number)
>>> True

參考資料:
https://docs.scipy.org/doc/numpy/reference/arrays.html

遇到的問題為使用 python 的繪圖套件-matplotlib 在 linux 作業系統 unbantu 16.04 下所遇到的錯誤。錯誤畫面如下:

一開始以為是 race condition 的問題,但後來想想目前程式皆是用 single thread 去寫的,所以應該不是這個問題。
後來又測到了如以下的錯誤:

很明顯的可以發現繪圖套件本身極可能有問題。因此朝了此方向去 google 得知由於 matplotlib 預設是在有圖形使用者介面的環境運作的,需要有 X11 connection,然而很多的 web server 初始就設置 X11 connection ,因此直接轉移到伺服器環境常常會照成一些錯誤。參考了官網解法,加入以下程式碼即可解決。

1
2
3
4
5
# do this before importing pylab or pyplot
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt

方法更新

目前的解法雖然程序不會當掉,每個request都有回應,但是指細檢查每個 request 回傳的內容還是發現裡面還是有 error 的!!!

以下壓力測試以 jmeter 測量,Django 不加上 –nothreading 參數啟動server,同時開 100 thread 發送 request 給 Data Chart API ,django 雖然不會掛掉但可以發現結果圖形很不穩,

在看檢視結果樹每個 request 的回傳結果是有錯誤的!!!

這就是表示雖然有加了 matplotlib 設定檔來指定 Agg backend,但還是沒有解決 thread safe 的問題,每一個 request 同時顯示圖片會發生 race condition 問題!!導致繪圖套件圖片會出錯!

因此,我在繪圖的程式碼區塊上下加上了 lock 來建立 critical section 來解決 race condition 問題

1
2
3
4
5
6
import threading

lock.acquire()
# critical section
# 這裡是被保護的繪圖程式碼
lock.release()

加上之後再重新執行 jmeter,發現不但結果圖形變穩定,100 個 request 都有成功的回應!!!

每個 request 都有成功畫出圖片

最後是執行 1000 個 thread 結果,加上 Lock 後服務變得很穩定

因此加上 Lock 限制或許才是最終的解決之道。

參考資料:
https://matplotlib.org/faq/howto_faq.html
https://www.lookfor404.com/%E8%BF%90%E8%A1%8Cggplot%E5%87%BA%E7%8E%B0%E9%97%AE%E9%A2%98no-display-name-and-no-display-environment-variable/

_proto_
javascript 的物件導向繼承機制與其他靜態語言 java、c#、c++ 有很大的不同,javascript 是沒有所謂的 class 的,ECMAScript 6 中新介紹的 “class” 只是一種語法糖,並沒有改變原來的機制,實際上還是 Function Object。
javascript 的繼承機制是採用物件繼承物件的方式,即原型鍊繼承(Prototypal inheritance),當中的關鍵就是_proto_這個物件屬性,它就好像一個參照指標一樣,被參考的物件就是此物件的原型,例如rabbit.__proto__ = animal 物件animal就是物件rabbit的原型,物件rabbit繼承了物件animal,因此rabbit可以使用animaleat方法,如圖所示

1
2
3
4
var animal = { eats: true }
var rabbit = { jumps: true }
rabbit.__proto__ = animal // inherit
alert(rabbit.eats) // true

而當物件與物件所繼承的parent原型物件皆有同名稱的 method 或屬性時,則呼叫本身的物件的那一個method 或屬性,如以下程式碼表示

1
2
3
4
var animal = { eats: true }
var fedUpRabbit = { eats: false}
fedUpRabbit.__proto__ = animal
alert(fedUpRabbit.eats) // false

詳細的運作情形是javascript的原型鍊繼承有個往上查找的特性,當在本身物件內scope找不到某個屬性時,就會循著 _proto_指向的原型物件去做查找,像以下另一個例子,rabbit 呼叫了 eat method,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var animal = {
eat: function() {
alert( "I'm full" )
this.full = true
}
}

var rabbit = {
jump: function() { /* something */ }
}

rabbit.__proto__ = animal
rabbit.eat()

rabbit本身沒有這個 method,因此在執行的時候 interpreter 就會循著原型鍊往上查找,從原型中找到相應的 method並呼叫

值得注意的是這邊的this是指rabbit物件,rabbit物件只是去 call parent method 而已,跟_proto_無關,因此執行完最後物件的結果圖會長以下這樣

不過使用__proto__屬性並不適宜所有瀏覽器,因為它並不是一個共同標準,有些瀏覽器的只能內部讀取而已,並不能隨意跟改__proto__值,因此就有其他通用方法的出現。

prototype

prototype就是一個跨瀏覽器常用的方法,不過他需要跟 constructor function 一起使用,像是以下程式碼

1
2
3
4
5
function Rabbit(name) { 
this.name = name
}
var rabbit = new Rabbit('John')
alert(rabbit.name) // John

當呼叫了 new function call ,新的 Rabbit 物件就產生了,而此時 new 也會設定這個新產生物件的__proto__屬性指向這個建構子的 prototype 屬性,就像以下這樣

1
2
3
4
5
6
7
8
9
10
var animal = { eats: true }

function Rabbit(name) {
this.name = name;
}

Rabbit.prototype = animal ;
var rabbit = new Rabbit('John');
alert( rabbit.eats ); // true, because rabbit.__proto__ == animal

Rabbit.prototype = animal這段程式碼所要表達的意思就是將所有透過new Rabbit產生的物件的__proto__屬性全部指向animal這個物件並當成原型。

還有一點要注意的是,原型鍊在查找有無物件屬性時,只有該物件沒有該屬性時,才會使用原型的屬性,如果直接在物件上設定屬性的值是不會影響原型物件的屬性的,例如:

1
2
3
4
5
6
7
8
9
function Some() {}
Some.prototype.data = 10;

var s = new Some();
console.log(s.data); // 10

s.data = 20;
console.log(s.data); // 20
console.log(Some.prototype.data); // 10

以上這就是 javascript 原型鍊繼承的概念。

參考資料
http://javascript.info/tutorial/inheritance
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes