Netflix says Geography, Age, and Gender are “Garbage” for Predicting Taste

FORTUNE 原文連結

一如往常的這背後有很多可以談,但所知不足,也恐怕沒有足夠空閒好好整理出 來,只能簡短打一些。首先這篇點出個人資訊的差異性遠遠不如「好惡」的差異, Netflix 以使用者喜好去 grouping 時,地理位置、年齡、性別等等幾乎完全不 列入考慮。在全世界都是同一個演算法。比如文中例子,90% 動漫流量來自日本 之外,國籍影響遠不如宅度來得大。

以這個類比到廣告產業,有趣的因素就多了。首先影片與文學、繪畫等等有類似 性質,往往可以跨越語言、地理限制等,但廣告常常是與地理高度相關的。比如 新開一家餐廳,若不是高度針對某特殊族群,例如高價位米其林美食,或是主打 韓劇歐巴主角等,往往較為重視餐廳周遭的客人,而非對全市或全縣打廣告,更 不用說全台灣。但若是大品牌廣告,例如某車廠新車上市,就會是全台。再例如 新的化妝品,針對女性就遠大於針對男性。之前忘記從哪裡看到的資料,也說若 發傳單這行為能達到某種針對性,轉化率可以有很大的差異。

把這兩個觀點結合起來會是什麼呢?個人認為這在暗示目前網路廣告準確度還有 非常大空間,而有趣的是,儘管這是個事實,但從廣告主開始,卻似乎還無法做 出很好的應對。這是指台灣情況,再遠我就無知了。廣告主目前的嘗試,多半是 採取風險轉嫁,將 CPV (cost per view), CPC (cost per click) 一步步推到 CPI (cost per installation),或是更廣義的 CPA (cost per action),簡言之 就是「我賺到錢才付你錢」。然後中間又穿插了 agent 下單到各種平台,但以實 際接觸經驗,也多半是各種經驗上、實務上理由來決定怎麼配,中間並沒牽涉到 什麼神奇的 DM/ML 技術。如此無效率的情況下必然中間有市場,往這方向談下去, 立刻就會碰到 Appier 這類公司。

數據要多大才有用?這中間牽涉到媒體、廣告平台、agent、廣告主、以及一大堆 沒寫出的各種角色競合,有興趣可以看看 LUMA Partners 廣受引用的 這些圖。似 乎掌握相當多有效資料的是 Facebook 跟 Google,但在我看到的、弱弱的場景中, 這兩間往往只被當成下單的標的之一,是 agent 在決定這邊下多少、那邊下 多少、怎麼下。在我沒有實際看到的地方,聽說有很多大咖在互相接近,消滅中 間人,直接以各自掌握的資源對接,並藉以壓迫沒跟上時代的人。

這一片亂局中,目前所處的公司正站在哪裡,該站在哪裡,要怎樣才能走過去, 這些我都還不是看得很清楚。

Go 1.5 Vendor Experiment with Git Submodule

一開始當然要先看這篇 Go 1.5 Vendor Experiment了解一下究竟 怎麼做的,簡言之就是把 /vendor 這目錄當成類似 /node_modules 這樣的 概念去用,從目錄最尾端開始往上,有就優先用,都沒有才會用到 GOPATH 裡 面的。

由於最終打算用 docker 包起來,所以想用 git submodule 來管 vendor 底下的東西,這樣 docker build 的過程中可以不需要裝 godep 這樣的 package manager,也不用把關聯的程式碼放入 vcs 裡。但在把專案換過去的時 候,覺得最麻煩的是沒有適合的自動化工具,godep save 是在 GOPATH 的想 法下運作的,若把 GO15VENDOREXPERIMENT=1 打開,會把目前用到的複製到 vendor 底下,然後不包括 vcs 目錄例如 .git,這個在轉換到 submodule 的時候會有問題。研究出來比較簡單的方式是:

  1. glide,或只要類似功能的都 可以。

  2. glide init,如果之前有用 godep 之類其他的,它會自動嘗試匯入,不 然會自己找關聯。跑完會產生 glide.yaml。注意,這邊只會找第一層,也 就是你的專案裡面直接 import 的關聯。glide.yaml是可以自己編的,要 固定版本等等的都是在這邊指定。

  3. glide install,會遍查各層關聯,把所有用到的全部抓下來放到 vendor,並把版本也一併列出放入 glide.lock。這檔案是自動產生,自 己編了下一次跑也會被蓋掉。

  4. 接下來用這支 python script 自動產生命令,餵給 shell 執行。python glide-gitsubmodule.py | sh

Method as Argument in Javascript

前些時候談過 在 Go 中使用 method 作為 argument ,它的觀念、適用場合有點像 partial application ,但當然形式完全不同。若把使用語言換為 JavaScript,要傳 function 通常都 是拿來當 callback,常常是 anonymous。如果用到超過一次,通常就會拉出來取 個名字。如果會重複用但參數略有不同,那可以寫個角色像是 factory 的 function 用來產生「要被當成 callback 的 function」。不過並非所有人都像 我一樣那麼不愛用 JavaScript 的(半殘)物件導向,自然免不了會有「想拿 method 當成 callback」的情況。以下程式碼雖然直覺,但就會掉入 this 陷 阱裡:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
'use strict';

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

Bot.prototype.selfintro = function () {
return "I'm " + this.name;
};

function roll(intro) {
console.log(intro());
}

var no5 = new Bot('Number 5');
roll(no5.selfintro);

原因是執行到 roll 裡面時,this 並未被定義,所以呼叫傳入的參數 no5.selfintro 時,裡面 this 也一樣沒有定義。修正方法是用 bind 指 定 this,把最後一行換成:

1
roll(no5.selfintro.bind(no5));

語法很簡單,但在 JavaScript 程式中盡量把一切都化為資料與 function 個人 覺得是蠻好的設計方式。

高科技人才難尋?

這話題也出來一陣子了,但自身看法似乎比較怪異,沒看到類似的,遂姑且寫下 留個紀錄。由於經驗限制,本文所指人才,範圍侷限在「軟體工程師」。

企業說高科技人才難尋,而觀察周遭同行,抱怨多半集中在老闆沒用、薪水太低、 長官太蠢、工時過長等等,似乎少有人抱怨失業,所以大概可判斷此事為真。從 供給與需求來看,人才難找、就業充分,表示需求高而供給低。

先從需求高談起。需求高,企業搶人,自然有動力盡量提高薪水及福利。常看到 工程師抱怨老闆小氣,但若是提高薪資福利能提高獲利,老闆就會去做。薪水停 滯,背後原因並不一定是簡單的「小氣」,而可能是企業獲利能力沒那麼好,提 高薪資無法反映在獲利上,賺不到錢,自然也付不出錢。

而從供給面談,個人覺得最受忽視的一點是:人才數量,本來就是有限的。 台灣新生的人力有限,條件好的,除資訊外,也有其他學門可選擇。如果條件沒 變好,人才選擇學資訊的比例就不會增加。增設相關科系,只會產出更多不適任 者,不會憑空變出人才來。而若讓排名好的大學增加招生人數,教學品質必定降 低,還會壓擠到原本分配已經極度不均的教學資源。

只看資訊人才的話,與為台灣企業工作相比,外商(包括中國)以及新創公司也 吃掉了不少供給。這都代表價值選擇上,後兩者能夠提供文化、環境、薪資、福 利等綜合起來相對有競爭力的選項。

談完現象,來談解法;一樣從供給與需求入手。

搶不到人,但又無法增加獲利,一個合理的解決方案就是「減少需求」,也就是 減少所需的工程師數量。台灣工程師普遍程度不錯,但賣肝的多,抱怨公司無效 率的也多,與其找更多人,不如設法改善效率。這可由幾個不同面向著手:

  1. 中高階主管的世代交替很重要,因台灣是硬體起家,研發主管懂軟體開發的相 對少,需盡快汰換。

  2. 制度上需加強中階主管的實作責任,不能只是發號施令,要把實作設定為工作 一部分。只有真正參與,才能正確辨認出提昇效率的方法,以及避免錯誤決策。

  3. 與前項搭配,制度上增進開會效率、盡可能資訊透明、組織扁平,以上種種都 是為了減少主管的「非研發庶務」。

  4. 與 2, 3 項搭配,優先升遷具有研發實力以及溝通能力的工程師。簡言之,設 法讓「喜歡研發並擅長溝通」的人出線,而非「不寫程式也沒關係,只要升遷 就好」的人。

工程師的美德是懶惰,若讓能力最佳的工程師最大程度發揮懶惰的美德,並且將 此美德在組織中擴散,整體效率就會提高。不看程式的主管容易讓「寫爛程式快 速達成上級要求」的工程師出頭,而這樣的人上位後也只會搞政治、尸位素餐, 惡性循環。科技業幾間巨頭內部不乏人才,但往往排列沒有最佳化,搖動一下, 會有很大改善空間。

而在業務擴張方面,台灣企業主常一開口就大氣地說要雇幾百個工程師,這很可 能正是問題所在。究竟這幾百人進來要做什麼事?這事能賺多少錢?而這些錢, 分給這幾百人,還剩多少?如果換一下觀念,同樣預算,人找少一點,錢給多一 點,花更多功夫在組織效率上,很可能做得到一樣的事情。差別是錢多了,找來 的是人才,錢少了,只能找人家挑剩的。

另外一個可能解法是「提高獲利能力」。但如何提高?現有的高科技業巨頭,商 業模式已經固定,不太可能突然獲利大增。新創是個機會,但由於世界最佳新創 環境仍在矽谷,以及中國的資金與市場優勢,要成氣候,無可避免要與矽谷或中 國接軌。如此結果,雖然賺到錢的不見得是台灣人,但會是台灣人才的創業經驗, 然後以這些人為種子,才容易產生成功的本土新創事業。如此案例,已經有好幾 個正在發生。

另一機會是傳產。台灣傳產一直很強,而現在的高科技,已經不能只視為單一產 業,而應該是「所有行業的高科技化」。高科技業待遇相對不差,所以傳產要高 科技化的第一個功課,是要狠下心出得起錢。當然,一開始來的不見得是實力派, 很有可能是「口才很好派」,但只要待遇好、講實績、砍人不手軟,遲早會吸引 到高科技業內幹得不爽、敢衝敢拼的實力者。

與此同時,要理解的是,並不是架了網站、發行了 app 就是高科技化,創新才是。 比如 data visualization 鼎鼎大名的 D3.js 創始者之一 Mike Bostock 當時是 任職於紐約時報,傳產要做高科技,應以此為師。從這角度思考,前面所提新創 事業人才與傳產,就有了強烈結合理由。

整理一下以上提出的幾點看法:

  1. 已建立的大型科技企業,應以改善組織效率、精簡人力、提高待遇為優先。對 新增業務,應以精兵為目標。

  2. 以國外經驗與資金為引,培植創業人才,進而培育自有、高獲利之新創企業。

  3. 傳產應跳入競爭,追求創新或新創公司,吸引人才回流。

而其後邏輯是:

人才數量有其自然上限,提高較為困難,不如設法集中資源、提高獲利能力以爭 搶人才。若以「生產更多軟體從業人員」為解方,恐怕適得其反。

Ruby or JavaScript?

最近接觸到一些 RoR 的東西,覺得很有趣,以開發速度而言 RoR 真的很快,且 生態完整,但效能就是問題。反觀 node.js 族群,效能好些,但似乎開發速度還 沒那麼快。很可能接下來會變成看是 Ruby 先出現 HHVM 那樣的 VM,還是 JavaScript 開發時間也越來越短,最終真的一統天下。

以語言來說並不喜歡 JavaScript,就算到 ES6 也還是沒有很愛,但若把目 標放在 full stack,那一定得懂前端,要懂前端,就一定得把 JavaScript 學好。 既然學好了,後端時若能繼續用同樣語言開發,就能少學一種語言。

然而,若以開發時間而論,這不一定是最快的方式。因為很多 best practice (convention) 仍然存在 RoR 之中,所以花時間去學 Ruby,然後直接用 RoR,現 階段來說省下的功夫很可能可以把學習 Ruby 的時間賺回來。

語言跟應用領域,往往有很強的相關性。例如 Linux 之於 C,userspace 的基礎 架構如 database, browser engine, server 之於 C/C++,資料科學之於 R。但 也有相當通用的語言,例如 Python 以及 Java 用途都相當廣,Python 開發快, Java 效能好。所以說,若打定主意專精於 web 領域,那為了 RoR 學習 Ruby 是 很合理的。

但這優勢其實可取代。因 JavaScript 在 web 應用太廣泛,而基於 isomorphic 的需求,JS 會很自然的往後端與 mobile app 那邊長。且 async non-blocking 的天性已經在語言中,這並不只是效能問題,還牽涉到 scale up 的難易程度。 若把 node.js 的 opinionated web framework 例如 sails.js 開發到像 RoR 一 樣的方便,那麼全部轉換到 JS 為基礎的全端架構,有明顯的優勢。

這件事適合熟悉 RoR,知道其中哪些特性相對於其他解決方案是比較有優勢的工 程師去做。相對於寫一個 VM 可能要公司的能力才做得到,把 sails.js 這類 framework 往 RoR 的方向改善則只需要幾個工程師就行了,後者或許比較容易發 生。

另一個有趣的可能性是,Ruby 到 WebAssembly 的 interpreter 被發展出來且內 建到瀏覽器。由於 client 端運算能力日漸強大,interpreter 慢些也還好,說 不定 RoR 也可以用目前的方式往前端長,直接用一樣的方式寫前端行為,打包成 一整個 full stack web app,這樣感覺也是很厲害。

Method as Argument in Go

說來慚愧,這麼好用的作法竟然今天才發現。

一直以來都覺得 net/http 有點難用,http.Handlehttp.Handler,定義是

1
2
3
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}

所以非得有個 method ServeHTTP,但如果要在一個 Handler 底下有好幾個 method 用以處理各類 request,進入點都是 ServeHTTP 就有點麻煩。這時用 http.HandleFunc 就比較合理,只要傳入一個 function:

1
func HandleFunc(pattern string, handler func(ResponseWriter, *Request))

然而,這類函數經常需要 context 才能執行,例如把資料庫連結存在 type 裡。 以往碰到這情況會利用 closure 的特性,在 method 中宣告符合介面的函數,然 後再傳給 HandleFunc,現在才知這完全是不必要的。舉例如下:

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
package test

import (
"log"
"net/http"
)

type Model struct {
// Your database connection, etc.
}

func (t Model) Foo(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("Foo"))
}

func (t Model) Bar(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("Bar"))
}

func Example() {
model := Model{}
http.HandleFunc("/foo", model.Foo) // method as argument
http.HandleFunc("/bar", model.Bar)
log.Fatal(http.ListenAndServe(":8080", nil))
}

實在是蠻漂亮的作法,搭配 Gorilla web toolkit 很容易就可以寫出 簡單明瞭的 server side 程式。

寫 Mobile App 簡單嗎?

之前面試 mobile app 工程師時,一位人選告訴我,他不想在原先公司(頗知名, 薪水也高)繼續做下去的原因,是因為 Android SDK 他已經很熟,再寫覺得也沒 有進步空間,所以想出來學一些其他的,例如底層一些的技術。

這似乎是個常見的說法,從剛入職場就常聽到,但可能年歲漸長,有些不同體悟。 例如寫 driver 就是那樣,系統整合就是那樣,所以想學別的。這其中必定有什 麼不對的地方,但是是什麼呢?

問題出在需求

Android SDK 本來創作出來的用意就是希望盡可能簡單,因為 Google 希望能讓 開發 Android app 是簡單的,吸引更多 developer 投入,才能讓生態圈蓬勃發 展。但開發界面上的簡單、API 的簡單,不表示功能上的簡單。如果覺得 SDK 學 完了,工作就變得太簡單,那應該是因為需求太簡單了。

為何需求是簡單的

Google 新出的 Inbox 功能簡單嗎?Facebook app 簡單嗎?要看由什麼角度來看。 他們致力追求的都是使用界面上的單純,但往往越單純的界面,後面的機制越複 雜。一個極度好用的功能,往往實作上相對困難。所以這裡延伸出的問題是,為 何在知名大廠,所面對的,仍然是實作上的簡單。

Mobile App 之外也經常如此

例如在 ODM 廠,處理來自各廠商的 BSP,做久了架構熟了,釐清的方式也熟了, 有問題找原廠支援,工作就開始容易。這表示 Android 系統面簡單嗎?問題恐怕 出在,為何沒辦法做到複雜的東西。BSP 已經是相對應用端,使用一個本來就是 為了方便使用者的 package,本來就不該困難。

廣 vs 深

於是可以發現,很少有真正簡單的領域,往往問題出在身處那領域的什麼位置。 台灣求深的廠商不多,相對的職位也不多,往往造成求深不如求廣,我自身就是 個例子。但這是個惡性循環,在職涯選擇上,或許可針對某特定領域,透過自我 要求,追求卓越,盡力往研發的上游端前進。

大勢不佳,但人才全球流動,以海盜心態武裝自己,拋棄故鄉,該去哪就去哪, 應該是概率最佳的選擇。若在本土發展,另闢蹊徑,總是比較困難的。

Solarized color 8 (bright black) 與背景雷同問題

使用 Solarized 這套配色對眼睛蠻 舒服的,而且有 light 或 dark 型態可以切換,跟 Base 16Zenburn 比起來,我是最喜歡 Solarized。但他有個長久以來的問題,就是在 terminal 下,background 跟 color 8 (bright black) 是一樣的顏色,都是 base 03。Node.js 很多套件在 彩色顯示時都會用到 bright black,一旦用到就會跟背景同色,看不見。

之前我的解法是把 background 設為 color 0,也就是真的 black,但在 Solarized 設計上,這兩色是鄰近的而不是對比色,所以雖不是同色,但仍然很 難看到。後來參考了內建 Solarized theme 的 Terminator 解 法,是直接把 color 8 設定成 #6a848a,這不論在 light 或 dark 模式下, 反差都很清晰,是目前為止我覺得最好的解。

相關討論可以看 issue 220,這問 題存在很久了但作者就是不爽解決。

多核心下 nginx - node.js 的組合

目前我們網站都是寫在 node.js 上,然後用 nginx 當 reverse proxy 來接。

以四核然後有 VT 為例,nproc 看到的會是 8,此時一些相對應的 nginx.conf 設定:

1
2
worker_processes 8;
worker_cpu_affinity 00010001 00010001 00100010 00100010 01000100 01000100 10001000 10001000;

worker_cpu_affinity 的值是按照 /proc/cpuinfo 內容來決定的,用來做範例的機器排列是 processor 0~3 對應到 core 0~3,然後 processor 4~7 又對應到 core 0~3,所以 processor [0, 4], [1, 5], [2, 6], [3, 7] 各自屬於同一個 core。同一個 core 可以允許 processes 互換,所以上面 worker_cpu_affinity 的意思是指定 worker 0 可以跑在 0, 4 兩個 CPU 上(第 0 跟第 4 bit 為 1),然後 worker 1 也是跑在 0, 4 上面。依此類推。

node.js 部份並沒有用 forever 來跑,而是用 upstart 來看住。相關的一些設定長這樣:

1
2
3
4
5
6
7
8
9
10
11
12
# portal.conf

start on runlevel [2345]

task

script
NPROC=$(nproc)
for N in $(seq $NPROC); do
start portal-worker N=$N
done
end script

用 portal 去跑好幾個 portal-worker 起來。而 portal-worker 則是會看執行時給的 N 值來決定怎麼跑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# portal worker

manual

respawn

instance $N

env NODE_ENV=production
setuid www-data
setgid www-data
chdir /home/johndoe/portal
script
test $N = 1 && exec taskset 0x11 node server.js -c config-1
test $N = 2 && exec taskset 0x11 node server.js -c config-2
test $N = 3 && exec taskset 0x22 node server.js -c config-3
test $N = 4 && exec taskset 0x22 node server.js -c config-4
test $N = 5 && exec taskset 0x44 node server.js -c config-5
test $N = 6 && exec taskset 0x44 node server.js -c config-6
test $N = 7 && exec taskset 0x88 node server.js -c config-7
test $N = 8 && exec taskset 0x88 node server.js -c config-8
end script

portal-worker 的 log 都會在 /var/log/upstart 底下,例如 N=1 時,就會是 /var/log/upstart/portal-worker-1.log。另外可以看到 taskset 的邏輯基本上跟 worker_cpu_affinity 是一樣的。後面 -c 是自訂參數,用來指定不同設定檔,例如規定 node process 聽在不同的 port 上。這樣搭配 nginx 裡頭 upstream 功能就可以平均分配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
upstream portal {
server 127.0.0.1:5001;
server 127.0.0.1:5002;
server 127.0.0.1:5003;
server 127.0.0.1:5004;
server 127.0.0.1:5005;
server 127.0.0.1:5006;
server 127.0.0.1:5007;
server 127.0.0.1:5008;
}

server {
# ...snipped...
location / {
proxy_pass http://portal;
proxy_redirect default;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $hostname;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

值得注意的是,upstart 在 12.04 有個 bug,Supplementary groups not set for user jobs,這會導致上面的 setuid www-data 不如預期,例如 user www-data 的 supplementary group 有一個 groupabc,然後某檔案設定為 groupabc 可以寫,結果 upstart 跑起來以後是寫不進去的。 解法之一是改用 sudo -u www-data -E -- 來跑。14.04 就沒這問題了。

MongoDB 搬空的 collections

show collections 會看到 system.indexes,只 dump 這個 collection 的 話,restore 時就會做出所有 collections 但是是空的。假定每個 collection 至少都有一個 index 的話。這在做測試用的 database 時很方便。

1
2
mongodump -d mydb -c system.indexes
mongorestore -d mydb dump/mydb