[讀書心得] 獨角獸專案 — devops經典作品 鳳凰專案再領讀者更創高峰

重點寫在前面,若看過「鳳凰專案」就不需要再看「獨角獸專案」。但若你還沒看過鳳凰專案,建議你先看過鳳凰專案後還覺得意猶未盡再來看「獨角獸專案」。這兩本書其實是寫在同樣的時空背景,同一家公司的同一個時間但平行敘述兩個觀點。

鳳凰專案是從「Ops的中高管理階層」來看DevOps,而獨角獸專案則是從「Dev工程師與架構師」的角度來看DevOps,兩本書一樣是引人入勝的小說,相信投身於軟體相關工作的人讀完都會心有戚戚焉。不過鳳凰專案是用第一人稱的觀點,但獨角獸專案卻改用第三人稱的視角,而更多的登場角色讓這次故事造成一些混亂。中譯本的序建議讀者第一次先把這本書當作小說看,第二次閱讀再細細品嘗「核心業務與脈絡業務」及「系統組織的五大理念」其中的奧妙。我非常贊成,可以的話最好還可以兩本書同時交叉閱讀。

兩本書都以時序分成三部曲:

閱讀更多

[讀書心得] 機器學習設計模式 – Machine learning design patterns (下)

有彈性的服務

16. Stateless serving function

17. Batch serving

18. Continued model evaluation

部署後的model隨時間改變,可能會漸漸不合用。像是垃報郵件過濾器、股票預測等等,環境可能會隨時間或是被model影響而改變。在部署後我們必需持續評估model的預測準確度,必要時觸發重新訓練的機制。

19. Two-phase predictions

有些model部署的環境可能沒有可靠的網路環境,邊緣裝置是常見的情境。使用Two-Phase Predictions設計時,將問題拆成兩個部份。先從比較小的,比較便宜、可部置到邊緣裝置的model開始。例如先從「ok google」的偵測當作第一個model,接下來的複雜的語音辨識當作第二個問題,只有在使用者提出問題時運行。

20. Keyed predictions

因為網路封包傳輸並不總能保證順序性,另外我們的model也有可能部署在分散環境中。Keyed predictions會在每一個預測請求中加入一個unique key value,把這個unique key當作一個model的feature,但model不做任何運作直接把此key value當作output。這樣可以讓使用者從眾多預測請求中輕鬆分辦出正確的順序。

再現性

21. Transform

作者定義input必須先進行transform才能變成model的feature,例如文字檔案必須先進行embedding才能轉換成model使用的feature。也就是說作者把這些reprocessing或是feature engineering叫作「transform」。一但訓練資料完成transform,model在訓練時並不需要知道如何transform。但在部署model到環境之後,必需將input做transform才能運作。故這些transform的方法必須好好保存。

22. Repeatable splitting

在分割training/validation/test data時,我們常用亂數分割的方法。但這個分割方法是有能力「重現」或是儲存。

23. Bridge schema

我們使用的資料格式(schema)是有可能改變的。例如我們原本有兩種支付方式:現金或卡片,但我們後來可能會把「卡片」再細分三類:信用卡、簽帳金融卡或禮物卡。我們必須讓model能同時處理新資料與舊的資料。

  • Probabilistic method:假設信用卡、簽帳金融卡、禮物卡的機率分別是10%、30%與60%。我們就把原本屬於舊資料的「卡片」在訓練時分別依照機率填入新的格式:信用卡、簽帳金融卡或禮物卡。
  • Static method:直接用one-hot encoding將舊資料代入feature為[0.1, 0.3, 0.6]來做訓練。作者通常會使用此法,因為處理快速且不需要一直呼叫隨機函數。

24. Windowed inference

有的問題是一連串時間相依的輸入,之前的輸入可能會影響之後輸入的預測結果。

例如要預測Dallas Fort Worth (DFW)機場抵達的班機是否為「abnormal arrival delays」,一個班機的延遲是否達到異常程度會受到最近兩小時其他班機的延遲狀態影響。我們收到一個班機抵達資訊可能只是一個timestamp:

2010-02-03 08:45:00,19.0

如上面八點四十五抵達的班機延遲19分鐘,但要從這點資訊來做預測幾乎不可能。這種情況我們可能要前幾個班機抵達的資料,例如:

2010-02-03 06:45:00,?
2010-02-03 06:?:00,?
...
2010-02-03 08:45:00,19.0

這種情境無法只對單一班機做inference,必需提供之前的資訊給model,即「Windowed inference」


25. Workflow pipeline

26. Feature Store

27. Model versioning

Responsible AI

28. Heuristic Benchmark

若我們已經有一個已在使用的預測系統,新的model就應該以其為比較的標的。但若這個新model沒有已存在的標的可做比較,我們就需要一個「Heuristic Benchmark」

  • 一個Stack Overflow問題需要等多久才會被回答
    • 皆預測會花2,120秒,其為中位數時間。
  • 預測自行車租用時間。關鍵為:站點與是否為通勤尖峰時刻。
    • 每一站平均租用時間做查表,且尖峰和離峰需分別列表。
  • 用視網膜影像做疾病預測。
    • 讓三位醫生檢查每張圖片,以多數醫師的看法視為正確解答。看看Model在人類專家中的百分位排名。

29. Explainable Predictions

30. Fairness Lens

[讀書心得] 機器學習設計模式 – Machine learning design patterns (上)

這本書在amazon剛出版沒多久很快就獲得不少好評,年初就買來但一直沒時間好好閱讀。剛好最近博客來需要湊單就連中文版一起下定。譯者是賴屹民,之前就買過這位作者的「流暢的Python」與「精通機器學習」「PHP、MySQL與JavaScript學習手冊」,只能說這次翻譯還是一樣無法恭維。

與四人幫的「設計模式」不同,這本書雖然有一些tf與BigQuery的範例碼,但基本都是在講ML的常用技巧並沒有太多程式架構,命名還不如改成歐萊禮的「CookBook」系列比較合適。

總的來說這本書還是很推薦,不少技巧都很實用且有價值,雖然有些pattern可能學過基本ML的人都會覺得基礎,但仔細閱讀會發現作者會提到其他關連知識或提示,閱讀時建議不要因為看標題就覺得學過而跳過。

閱讀更多

成功的反思-才德的暴政;The tyranny of merit

小時候長輩常叫我們要認真念書將來才會有好工作與賺大錢,而長大之後真正能成功賺大錢的人只有極少數人。朋友中看似成功的醫師與工程師,許多人還是羨慕忌妒其他賺更多錢的人—-不論是其他更成功的醫師、工程師、企業主、地主、暴發戶與富二代…

在我心中也是不時會有懷材不遇的心情,畢竟念了那麼多書又學了好幾個專長,直到最近才發現能發揮這些專長的機會少之又少。

後來才發現努力與成功雖然有正相關,但其中的相關性卻是非常小,遠低於我出社會前的想像。雖然有些人靠努力求上進而賺大錢,但更多人一樣努力卻無法達到相同成就。

成功是1分的天才與9分的努力加上90分的運氣。

成功有超過90%是我們不能控制的部份;我們不能控制我們出生的社會、家庭、國家、時代、身體、基因、性別。我們的天份或感興趣的事物不見得是能賺大錢的工作,我們小時候的養成與教育受限於家庭與社會,我們的能力不見得能被家庭或是社會發現,甚至根本不被重視。

我認為不應以薪水或是工作來衡量一個人的成功或是價值。

努力工作與追求知識不應該視為成功或賺錢的手段,應該視為純粹的美德。

很可惜因為資本主義與個人自由主義的盛行,造成現代社會充斥菁英思想。社會常把一些美德加諸在成功人士身上(例如工作狂、破產也不放棄、從小認真念書)。這些元素也常在我們身邊的人身上看到,但大部份的人都只能過平庸的人生。進一步來說,那些成功人士所稱的美德根本無法證實,他們可能誇大其詞或虛假陳述 ,實際上他們背後可能都是靠一些違背道德的手段來在高度競爭的社會中勝出。

這本書正好點出我煩惱已久的問題,而且他的論證嚴謹與高度完全超過我過去想法,非常推薦大家閱讀。

閱讀更多

人類大歷史

第一次革命發生於七萬年前。

智人(Homo sapines)也就是現代唯一的人類發生了認知革命(Cognitive Revolution)開始創造了文化。智人(簡稱人類或人)開始能【討論虛構的事物】,所以可以開始述說或編造故事。不同的人能有相同的信念所以開始能建立比黑猩猩更大的部落(20 v.s. 150)。人類能熟識的人數不超過150人,故超過這個數目就不能只用人際關係維繫。人類靠者抽象的信念與規則可以建立超過千萬人的組織,例如企業、國家、軍隊與宗教。想像把一千隻黑猩猩聚集在一起必定會爭鬥不休。

認知革命的影響

新能力明顯好處
能夠傳遞更大量關於智人身邊環境的資訊規劃並執行複雜的計劃像是躲開獅子、獵捕野牛。
能夠傳達更大量關於智人社會關係的資訊組織更大、更有凝聚力的團體。規模可達150人
能夠傳達關於虛構概念的資訊,例如部落的守護神、國家、有限公司以及人權。一,大量陌生人之間的合作
二,社會行為的快速創新

最早人類是從兩百五十萬年前的東非的南猿(Austrlopithecus)開始演化。兩百萬年前走出非洲並演化出不同人種,例如歐洲的尼安德塔人、亞洲的直立人,印尼的梭羅人等等。這些人類會使用石器,轉眼讓人類登上頂峰位居食物鏈頂端。十五萬年前開始有些人類會使用火,這時智人也早已於非洲演化出來,大約七萬年前走出非洲並漸漸席捲全世界,滅絕許多動植物與其他人類。

現代社會每周工時大約40-50,有些社會多到60-80小時。但採集社會大概每三天打獵一次,每天採集3-6小時,最貧脊的沙漠裡也不超過每周35-45小時。雖然平均壽命可能才30-40歲,但主要是兒童早夭造成,只要能順利成年大多能活至60-80歲,推估是因為較均衡的飲食與較少的傳染病。

第二部 農業革命

第二次革命發生於約一萬兩千年前

五、世上最大騙局

大部分的動植物實際上都無法被人類馴化。槍砲病菌與鋼鐵也花了許多篇幅描述此事,也推薦大家閱讀這本書。農業是各地的人類獨自演化,並不是緣起於中東再傳到世界各地。只是小麥最早在一萬年前被馴化,之後稻米、玉米、馬鈴薯、小米、大麥陸續被馴化。但實際上是植物馴化了人類,人類的身體結構演化是用來狩獵採集,並不適合農耕。農業革命後反而越來越容易生病,而且營養不均衡,而且要花比狩獵採集時代更多的工時才能獲得足夠的食物。

六、蓋起監獄高牆

從採集進入農耕之後,資源反而不足,且需要規則管理更多的人。所以開始由想像的規則建立秩序。

1.想像建杜的秩序深深與真實的世界結合

現在小孩有自己的房間可以建立個人崇拜與領域,與現代的個人合義結合。但中世紀小孩沒有自己的房間,被教育要為家族保護名聲。

2.想像建構的秩序塑造了我們的欲望

滿足了食與生存後,開始有更高層次的慾望。法老王建造金字塔,以及現在的消費主義。

3.想像建構的秩序存在於人與人的思想連結中

「客觀」事物存在,如放射射的傷害就算不了解原理也是會存在。但「主觀」的事物是由信念所建立,例如足球的規則是人發明的。「互為主體性(inter-subjectivity)」,代表許多個人主觀意識的連結,並不會失因單獨一個人死亡或改變而消失,故能能長久流傳。寶獅汽車存在百萬人心中,不會因為創辦人死亡或是執行長一個人的主觀意識而改變,除非有更高層次的想像,例如「法律」才有可能解散寶獅汽車。

七、大腦記憶過載

人腦不擅長抽像計算與記憶。最早的文字是用來記帳,包含數字等,功能僅限記錄部份資訊,而且很可能這些資訊是沒辦法直接用口語表達。有些書寫用代號並沒有對應發音,而這些文字也可能無法對應且記錄當時口耳相傳的詩歌。

例如安地斯文化的文字是在繩子上打結來表示,稱為結繩語(quipu)。

八、歷史從無正義

作者批判階級命定論與性別

第三部 人類的融合統一

九、歷史的方向

西元前一萬年地球上有數千個文明,西元前2000年只剩數百個,西元1450年只剩下數個,絕大部份的人活在亞非世界(Afro-Asian world)裡。

真正的文明衝突其實是「聾子式的對話」(dialogue of the deaf),也就是雙方完全不知道對方在講什麼。伊朗和美國雖然針鋒相對,但都是在講民族國家、資本主義、國際權利以及核物理等等共通的語言。

三種全球秩序包含:貨幣、政治、宗教。

十、金錢的氣味

金錢是有史以來普遍最有效的互信系統,賓拉登痛恨美國但也必須且樂意使用美元。

十一、帝國的願景

帝國的定義不是由人數或國土定義,應該是族群的數目,通常包含十個以上的種族。通常統治者的種族會漸漸同化其他民族,但最後其他的民族被同化後會取代或融入原本的統治階層。

十二、宗教的法則

宗教的演進:泛靈→多神→一神。

一神教很難解釋惡的存在,為何全能的神創造的世界會有惡與苦難。有人解釋惡的存在是要人類有益是去選擇善,但這會引發許多問題,神為何要創造選擇惡的人。故一神教也會融合其他宗教,例如惡魔的概念起源是善惡兩元論,冊封聖人也是多神的概念。

但一神教要處理律令的問題卻很容易,因為一切是神定的。

佛教說的覺悟與涅槃(nirvana,梵文原意是熄滅)也是意識型態,講的是「苦由欲起」以及如何逃離痛苦。 99%的信徒都無法達到這個境界,故各地佛教徒還是各自創造了不同的多神信仰。

人文主義也是宗教,雖然沒有崇拜神但仍有意識形態。近代社會各種鬥爭都是意識形態造成,與各種宗教鬥爭類似。 三種人文主義包括 :

一,自由人文主義。流行於現代歐洲,因為人性是存在每個人故會廢死刑。藉由表彰殺人犯的人性,讓所有人想起人性的神性於是恢復秩序。保護兇手才能改正他做的錯事。

二,社會人文主義。認為不平等會破壞社會的神聖性,富人不應比窮人有權力。

三,進化人文主義。納粹相信進化論,消滅可能退化的人類才能保護人類。

十三、成敗的秘密

歷史的鐵則就是:事後看來可避免的事,在當時看來總是毫不明顯。歷史還是二階混沌系統(“level two” chaotic system)。一階混沌指的是「不會因為預測而改變」,例如天氣。二階混沌系統,指的是「會受到預測的影響而改變」例如,市場或政治。

若能預測明天的油價,現在價格就會立刻反應,明天不應該有任何漲跌。革命就是無法預測。如果真能預測有革命,革命就永遠不會成真。

研究歷史,不是為了要推知未來,而是要拓展視野,要瞭解現在的種種絕非「自然」,也並非無可避免。

第四部 科學革命

第三次革命發生於約五百年前

關於近代史可能大家大都有認識。而作者最後根據演算法與人工智慧對未來做預測,這部分我幾乎完全不能同意,所以也不加贅述。

Effective Python – 例36 使用itertools幫助iterators與generators

當使用iterator寫一些刁鑽的程式碼時,itertools也許會有現成的工具。

import itertools

it = itertools.chain([1, 2, 3], [4, 5, 6])
print(list(it))
# [1, 2, 3, 4, 5, 6]

it = itertools.repeat('hello', 3)
print(list(it))
# ['hello', 'hello', 'hello']

it = itertools.cycle([1, 2])
result = [next(it) for _ in range(10)]
print(result)
# [1, 2, 1, 2, 1, 2, 1, 2, 1, 2]

it1, it2, it3 = itertools.tee(['first', 'second'], 3)
print(list(it1))
print(list(it2))
print(list(it3))
['first', 'second']
['first', 'second']
['first', 'second']

tee可以平行生出多個iterators,但要小心記憶體可能會使用很多。

keys = ['one', 'two', 'three']
values = [1, 2]

normal = list(zip(keys, values))
print('zip: ', normal)

it = itertools.zip_longest(keys, values, fillvalue='nope')
print('zip_longest:', list(it))

zip_longest類似內建的zip,只是zip會以最短的iterator回傳,而zip_longest會以最長的iterator回傳。預設會以None來補上缺值。

zip:  [('one', 1), ('two', 2)]
zip_longest: [('one', 1), ('two', 2), ('three', 'nope')]
values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

first_five = itertools.islice(values, 5)
print('First five: ', list(first_five))

middle_odds = itertools.islice(values, 2, 8, 2)
print('Middle odds:', list(middle_odds))
First five:  [1, 2, 3, 4, 5]
Middle odds: [3, 5, 7]

islice可以做出類似list的slice功能

values= [1, 2, 3, 4, 5, 4, 3, 2, 1]
it = itertools.takewhile(lambda x: x < 4, values)
print(list(it))
[1, 2, 3]

takewhile需要傳入一個callable object,當其回傳為False會停止iterator。另外有一個dropwhile會從其回傳True的時候開始iterator。

evens = lambda x: x % 2 == 0

filter_result = filter(evens, values)
print('Filter: ', list(filter_result))

filter_false_result = itertools.filterfalse( evens, values)
print('Filter false: ', list(filter_false_result))
Filter:  [2, 4, 4, 2]
Filter false:  [1, 3, 5, 3, 1]

filter_false就是內建的filter的相反。

Effective Python – 例26 定義function decorators使用functools.wraps

python有簡單的語法來使用function decorators,但有可能造成一些不想要的副作用,例如可能會把help給覆蓋。

def trace(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        print(f'{func.__name__}({args!r}, {kwargs!r}) -> {result!r}')
        return result
    return wrapper

@trace
def fibonacci(n):
    """Return the n-th Fibonacci number"""
    if n in (0, 1):
        return n
    return (fibonacci(n - 2) + fibonacci(n -1))

# @trace 等於是 fibonacci = trace(fibonacci)

fibonacci(4)

help(fibonacci)

import pickle
pickle.dumps(fibonacci)
fibonacci((0,), {}) -> 0
fibonacci((1,), {}) -> 1
fibonacci((2,), {}) -> 1
fibonacci((1,), {}) -> 1
fibonacci((0,), {}) -> 0
fibonacci((1,), {}) -> 1
fibonacci((2,), {}) -> 1
fibonacci((3,), {}) -> 2
fibonacci((4,), {}) -> 3
Help on function wrapper in module __main__:

wrapper(*args, **kwargs)

Traceback (most recent call last):
  File "C:/Users/hans/Desktop/item 26.py", line 22, in 
    pickle.dumps(fibonacci)
AttributeError: Can't pickle local object 'trace..wrapper'

在wrapper function加上wraps可以修好help跟pickle

from functools import wraps

def trace(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        print(f'{func.__name__}({args!r}, {kwargs!r}) -> {result!r}')
        return result
    return wrapper

help(fibonacci)
print(pickle.dumps(fibonacci))
Help on function fibonacci in module __main__:

fibonacci(n)
    Return the n-th Fibonacci number

b'\x80\x04\x95\x1a\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\tfibonacci\x94\x93\x94.'

Effective python – 例33 在巢狀generators 使用yield from

def move(period, speed):
    for _ in range(period):
        yield speed

def pause(delay):
    for _ in range(delay):
        yield 0

def animate():
    for delta in move(4, 5.0):
        yield delta
    for delta in pause(3):
        yield delta
    for delta in move(2, 3.0):
        yield delta

def render(delta):
    print(f'Delta: {delta:.1f}')
    # Move the imgaes onscreen
    ...

def run(func):
    for delta in func():
        render(delta)

run(animate)

想像我們在螢幕上用不同速度輪播不同照片來做成動畫,有的時候也會在中間暫停一下。

Delta: 5.0
Delta: 5.0
Delta: 5.0
Delta: 5.0
Delta: 0.0
Delta: 0.0
Delta: 0.0
Delta: 3.0
Delta: 3.0

改用yield from不僅是看起來更簡潔,重點是可以獲得更佳的運算效能。


def animate_composed():
    yield from move(4, 5.0)
    yield from pause(3)
    yield from move(2, 3.0)

run(animate_composed)

Effective Python – 例75 – 在debug時輸出repr字串

使用print()時通常是輸出所謂human readable字串,但有時會反而會造成困擾

print(5)
#5
print('5')
#5

int_value = 5
str_value = '5'
print(f'{int_value} == {str_value} ?')
#5 == 5 ?

我們可能分不清這個變數是字串’5’還是數值「5」。

另外有些字元可能不是printable,故print會看不到東西。

這時候可以改用rerp(),會改成所謂的printable representation的輸出。

a = '\x07'
print(a)
#
print(repr(a))
#'\x07'

Python有一個eval()可以把representation轉回成變數,但要知道eval()很容易造成問題,不該輕易使用。

b = eval(repr(a))
assert a == b

print(repr(5))
#5
print(repr('5'))
#'5'
print(f'{int_value}r != {str_value}r')
#5r != 5r

一些自訂的object預設的print()輸出可能會不如預期

class OpaqueClass:
    def __init__(self, x, y):
        self.x = x
        self.y = y


obj = OpaqueClass(1, 'foo')
print(obj)

<main.OpaqueClass object at 0x037C9088>

我們可以用__repr__來設計repr()的預設行為,另外format string也可以用{}r來輸出
printable representation

class BetterClass:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def __repr__(self):
        return f'BetterClass({self.x!r}, {self.y!r})'


obj = BetterClass(2, 'bar')
print(obj)

BetterClass(2, ‘bar’)

class BetterClass2:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def __repr__(self):
        return f'BetterClass2({self.x!r}, {self.y!r})'
    def __str__(self):
        return f'in __str__ {self.x}, {self.y}'
    

obj = BetterClass2(3, 'bar')
print(obj)
print(repr(obj)

in __str__ 3, bar

BetterClass2(3, ‘bar’)

另外__str__可以設計print()的行為。

若__str__沒有修改,但有__repr__修改,print()會呼叫它。

obj = OpaqueClass(4, 'baz')
print(obj.__dict__)

{‘x’: 4, ‘y’: ‘baz’}

若不能修改物件,我們可以改用__dict__來得到物件的成員所組成的dict

Effective python – 例29 在comprehensions中使用assignment expressions

stock = {
    'nails': 125,
    'screws': 35,
    'wingnuts': 8,
    'washers': 24,
}

def get_batches(count, size):
    return count // size

comprehensions是python常用的一個功能,可以快速生成list, dict, set等變數。但有時會出現一些重覆的計算…

order = ['screws', 'wingnuts', ' clips']

found = {name: get_batches(stock.get(name, 0), 8)
         for name in order
         if get_batches(stock.get(name, 0), 8)}
print(found)

{‘screws’: 4, ‘wingnuts’: 1}

現在我們可以用assignment expressions( :=,又稱walrus operator)來減少這些重覆語句的出現。

found = {name: batches for name in order
         if (batches := get_batches(stock.get(name, 0), 8))}

print(found)

不過要小心assignment expressions可能會汙染comprehensions外的scope,所以本書只建議在if的區域使用assignment expressions。

half = [(last := count // 2) for count in stock.values()]
print(last)
#12
print(batches)
#0

但實際上if區域的變數也是會汙染,書中的建議原因不太確定為何。