跳到主要內容

005互動

一個身體,兩個頭:在瀏覽器裡訓練同時畫框又畫遮罩的 HydraNet

用 emoji 水果當訓練資料,在瀏覽器裡從零訓練一個共用主幹、兩個輸出頭的小網路。十秒鐘它就找得到水果。然後我們誠實地量一次:多任務學習到底幫了什麼忙,又要付出什麼代價。

發布
閱讀時間
9 分鐘

下面這個網路現在什麼都不會。它的工作是在一張 32×32 的小圖裡找到一顆水果,而且要用兩種方式回答:畫一個把它圈起來,再畫一張遮罩標出哪些像素屬於它。

按「開始訓練」。大約十秒鐘後,框就會貼上去。

fig 01/hydranet / train
要訓練哪些頭
  • 模型畫的框
  • 正確的框
  • 模型認為是水果的地方
看過的圖
0
框 IoU
遮罩 IoU
速度
張/秒

由遮罩取外框:

還沒開始訓練。權重是隨機的,所以框縮在中間、遮罩是雜訊。

四條邊的分佈

框的頭沒有開,所以沒有分佈可以看。

左邊是訓練時沒看過的測試圖。青色實線是模型畫的框,粉紅色虛線是正確答案,亮的區域是模型認為屬於水果的地方。點下面的小圖可以換一張。

在我的機器上(Chrome、macOS、Apple 的 emoji),成績是這樣走的:

訓練時間看過的圖框 IoU遮罩 IoU
0 秒00.000.13
10 秒4,0000.800.77
40 秒16,0000.860.85
80 秒32,0000.880.89

你的數字會不一樣,原因很有趣,下面會講到。

資料不用錢

訓練物件偵測最貴的從來不是算力,而是標註:得有人一張一張把框拉好、把輪廓描出來。這裡完全不用,因為圖是我們自己畫的。

做法是在一張透明的畫布上畫一顆 emoji,隨機決定大小、位置、旋轉角度,再把它貼到一張有雜訊的背景上。畫完之後,畫布的 alpha 通道(透明度)就已經精確地記錄了「哪些像素是水果」。遮罩就是 alpha,框就是遮罩的外接矩形:

fig 02/free labels
訓練圖(32×32)
遮罩標註(16×16)
框標註
同一張圖和它的兩種標註。沒有人標過任何東西:遮罩來自 emoji 的透明度,框是遮罩的外框。
content/posts/hydranet-fruit/components/scene.ts
// 一個門檻同時決定框和遮罩,兩種標註永遠不會互相矛盾
if (alpha[y * SIZE + x] < 0.5) continue;
if (x < x0) x0 = x;
if (x > x1) x1 = x;

這件事有一個副作用。每個作業系統的 emoji 是不同的圖:Apple、Google、Microsoft 各畫各的蘋果。 所以你剛才訓練的,是「你的系統的水果」的偵測器。把在 iPhone 上訓練好的權重拿到 Android 上用,成績會掉。這就是機器學習裡說的 domain shift,而你手上剛好有一個活生生的例子。

一個身體,兩個頭

畫框和畫遮罩看起來是兩件事,但它們需要的低階特徵幾乎一樣:邊緣在哪裡、顏色在哪裡變了、哪一塊和背景不同。所以合理的設計是只算一次這些特徵,再讓兩個很小的「頭」各自去讀。

部分做什麼參數
主幹三層卷積,把 32×32 的圖變成 8×8 的特徵3,712
頸部把 8×8 的特徵放大回 16×16,和淺層的特徵接在一起1,296
遮罩頭一個 1×1 卷積9
框頭一個 1×1 卷積36
合計5,053

看最後兩列:兩個頭加起來只有 45 個參數,不到全部的 1%。網路的力氣全花在共用的那一段。這個結構叫 HydraNet,名字來自九頭蛇:一個身體,很多顆頭。Andrej Karpathy 在 2019 年介紹 Tesla 的感知系統時用的就是這個詞:同一個共用主幹上接了許多顆頭,同時處理車道線、號誌、行人等等不同的任務。理由很實際:車上的算力是固定的,共用主幹才塞得下。

頸部那一步值得多說一句。主幹為了看得廣,把解析度一路降到 8×8,但畫遮罩需要細節。所以頸部把深層特徵放大之後,會把還保有細節的淺層特徵接回來一起用。這是 U-Net 的 skip connection。

框為什麼這麼難

遮罩是「每個像素各自回答是或不是」,這對卷積網路來說很自然。框不一樣:它要輸出四個連續的數字,而卷積網路天生擅長的是「在哪一格」,不是「是多少」。

我第一版用的是最直覺的做法:把圖切成 8×8 格,讓網路選出水果中心在哪一格,再從那一格回歸到四條邊的距離。結果框的 IoU 只有 0.56,每條邊平均差 2.2 個像素。

框的做法框 IoU每邊誤差
選一格,再回歸四個距離0.562.2 px
同上,把格子加密到 16×160.572.1 px
同上,換一種 loss0.602.0 px
不訓練框頭,直接對預測的遮罩取外框0.63
把每條邊變成一個分佈(下一節)0.741.1 px

第四列很傷人:辛苦訓練出來的框頭,輸給了「對遮罩取最小值和最大值」這種一行程式。把格子加密、換 loss 都只是小修小補。問題不在細節,而在「選一格、再從那一格猜距離」這個輸出方式本身:只要選錯格,後面的回歸全部白費,而且梯度只從被選中的那一格流回去。

上面的儀器裡有一行小字「由遮罩取外框」,就是這個基準線在你的瀏覽器裡的即時成績,可以拿來和框頭的 IoU 比。

把邊界變成分佈

有效的做法是換一個問法。不要問「左邊界是多少」,而是問「左邊界在每個位置的機率是多少」。

框頭輸出四張 16×16 的圖,一條邊一張。以左邊界為例:把那張圖沿著垂直方向取平均,壓成 16 個數字,過一次 softmax 變成機率分佈,然後算這個分佈的期望值

x^left=i=015pixi其中p=softmax(meany(mapleft))\hat{x}_{\text{left}} = \sum_{i=0}^{15} p_i \cdot x_i \qquad\text{其中}\quad p = \mathrm{softmax}\big(\operatorname{mean}_y(\text{map}_{\text{left}})\big)

xix_i 是第 ii 格的中心位置。這個做法叫 integral regression,也叫 soft-argmax。它有三個好處:

  • 整條路都可微。不需要「選一格」這種不可微的動作,梯度會流到每一格。
  • 答案可以落在兩格之間。兩格各佔一半機率,期望值就在中間,所以沒有量化誤差。
  • 可以直接畫出來。儀器右下方的四張小圖就是這四個分佈。紫色長條是機率,青色實線是期望值,粉紅色虛線是正確答案。
content/posts/hydranet-fruit/components/model.ts
const maps = t.conv2d(d, P.boxK, P.boxB, { h: HALF, w: HALF, k: 1 });
const lr = t.softmax(t.marginal(t.sliceRows(maps, 0, 2), { h: HALF, w: HALF, axis: "x" }));
const tb = t.softmax(t.marginal(t.sliceRows(maps, 2, 2), { h: HALF, w: HALF, axis: "y" }));
box = t.matmul(t.concatRows([lr, tb]), this.positions); // 期望值就是和位置向量做內積

回到上面重新訓練一次,這次盯著那四張小圖。一開始四條分佈都是平的,期望值落在正中間,所以框縮成中央一小塊。接著你會看到每一條分佈長出一個峰,峰變尖,然後滑向水果的邊緣。沒有任何一行程式叫它這樣做,這是 loss 把它推過去的。

共享是有代價的

多任務學習常見的說法是「兩個任務會互相幫忙」:學畫遮罩會逼主幹學到更好的邊緣特徵,框也因此變準。這個說法很吸引人。我量了,在這個規模下沒有看到

離線的對照實驗(同樣的架構、同樣三組種子):

指標單獨訓練和另一個頭一起訓練
遮罩 IoU0.8250.812
框 IoU0.7940.735

兩個任務都是單獨訓練比較好。一起訓練時,它們在搶同一個主幹的容量,而這個主幹只有三千多個參數。

那為什麼還要共用?因為帳要這樣算。在這個模型裡,兩個頭只有 45 個參數,幾乎所有的計算都花在主幹和頸部。所以「只有框」的網路並不比「兩個一起」的網路快,想分別得到框和遮罩,就得把主幹整個算兩次。共用主幹省了將近一半的算力,代價是各掉一點準確度。 在算力固定的裝置上,這筆交易通常划算;而且頭越多越划算,因為主幹的成本被更多顆頭分攤。

下面讓你自己量一次。三個網路從同一組隨機權重出發、看同一批圖:

fig 03/heads race

三個網路從同一組隨機權重出發、看同一批圖,各訓練 6,000 張。每按一次會換一組隨機種子,多比幾次,看差距會不會反過來。

設定框 IoU遮罩 IoU張/秒訓練時間
兩個一起尚未執行
只有框尚未執行
只有遮罩尚未執行
兩個單頭網路合計
最後一列是兩個單頭網路的訓練時間加總,拿來和「兩個一起」那一列的時間比。每個網路只訓練 6,000 張,所以成績比上面低;要看的是三列之間的差距。

時間那一欄很穩定:兩個單頭網路合計大約是雙頭的兩倍。準確度那兩欄就不是了。我第一次跑的時候,框是一起訓練比較好(0.810 對 0.781),遮罩是單獨訓練比較好(0.807 對 0.778),和上面離線實驗的結論只對了一半。多按幾次,你會看到差距忽大忽小,有時候反過來。這本身就是重點:一次實驗不能支持「多任務有幫助」這種結論,要支持它得跑很多次。 文獻上兩個方向的結果都有。Standley 等人 2020 年的論文系統性地量過哪些視覺任務適合一起學,結論是「看任務組合而定」,有些組合互相幫忙,有些互相傷害。

它會在哪裡失敗

這一節的數字是用幾何圖形離線量的:同一個模型訓練 12,000 張之後,拿它沒看過的情境去考它。正常情況下它的成績是框 0.82、遮罩 0.75。

  • 兩個物體:框和「同時包住兩個」的正確答案相比,IoU 只有 0.32。但它並不是落在兩個中間:框的中心掉進兩者之間空隙的情況只有 12%。絕大多數時候它是挑了其中一個,和那一個的 IoU 有 0.65。softmax 的分佈喜歡只留一個峰,贏家全拿。遮罩頭完全沒有這個問題,兩個物體的遮罩 IoU 是 0.74,和一個物體時幾乎一樣;拿這張遮罩取外框,反而得到 0.85。
  • 前景和背景顏色接近:模型靠的主要是顏色對比。物體只比背景亮 0.3(顏色範圍是 0 到 1)時,遮罩掉到 0.56;只亮 0.15 時,遮罩剩 0.16、框剩 0.46。有趣的是框比遮罩耐得住:框只需要知道物體大概在哪,遮罩得逐格判斷。
  • 沒看過的 emoji:訓練時只有十種水果。我猜它對沒看過的水果也多半找得到,因為從上一點看起來,它學到的比較像「一團和背景不同的東西」而不是「水果」。但這個我沒有量過,所以只是猜測。
  • 換一個作業系統:前面說過的 domain shift。這個我也還沒有跨裝置量過。

這和真的系統差在哪

  • 物體數量:真實場景有任意多個物體,框頭要換成每個位置各自預測的設計。
  • loss 權重:這裡兩個 loss 直接相加。頭一多,怎麼配重就是大問題。Kendall 等人 2018 年提出讓網路自己學每個任務的不確定性,用它來決定權重。
  • 誰來標註:這裡的標註是免費的。真實系統的做法常常是用又大又慢的基礎模型離線產生標註,再拿去訓練一個又小又快的多頭網路,讓它在邊緣裝置上每一幀只跑一次前向傳播。這正是我在工作上做的事。
  • 規模:五千個參數對上幾百萬個。原理一樣。

參考資料