您當前的位置:首頁 > 攝影

Tabnet 完全指南(待續,更新於2021-01-06)

作者:由 馬東什麼 發表于 攝影時間:2021-03-18

Tabnet 完全指南(待續,更新於2021-01-06)

首先研讀一下原論文。

這裡不直接翻譯,而是進行總結。

首先,tabnet的主體思想是用nn來表示決策樹,那麼就涉及到這幾個問題:

1、使用什麼樣的nn結構來模擬決策樹的決策過程?

2、決策樹能夠透過比較分裂的增益來進行自動特徵選擇(自動選擇分裂增益最大的特徵進行分裂),如何讓nn也具備這種能力;

我們可以把tabnet當作一個積木搭建起來的城堡,一塊一塊拆解地進行分析:

首先,原文使用了這樣一種結構來模型決策樹的決策過程:

Tabnet 完全指南(待續,更新於2021-01-06)

先看右圖

Tabnet 完全指南(待續,更新於2021-01-06)

這是一個簡單的決策樹在2維特徵下的決策過程,可以看到一共進行了兩次分裂,分別是x1>a or not和x2>d or not,從而將原始的資料劃分為了4個區域。

而tabnet是這麼模擬的:

Tabnet 完全指南(待續,更新於2021-01-06)

仍舊是以x1,x2的二維簡單任務為例,

首先,使用mask分別對x1和x2處進行遮蔽,

這樣就實現了tree分裂的時候分別對每一個特徵進行單獨的分裂增益計算的過程

。在每一維特徵對應的分支裡,都僅僅有這一維的特徵參與了計算。這樣就完美的模擬了tree單獨對每一個特徵進行分裂計算的過程,tabnet在輸入端的每一個子網路結構單獨對這個子網路輸入的一個特徵進行計算。

當然實際上這裡是一個典型的多輸入網路,

Tabnet 完全指南(待續,更新於2021-01-06)

其實每一個mask層都輸入了所有的特徵,只不過透過mask把他們掩蓋了而已。

上面我們已經回答了第一個問題了,

下面回答第二個問題,tabnet怎麼模擬決策樹的分裂計算過程?

實際上,這裡tabnet的設計並沒有模擬決策樹的分裂增益過程,壓根也沒有分裂增益的計算公式在裡面,所有的引數仍舊是透過梯度下降法來進行更新的,但是有意思的是,透過一種特殊的設計,可以實現選擇哪些特徵入模而不選擇哪些特徵入模:

Tabnet 完全指南(待續,更新於2021-01-06)

核心在這個部分,我們單單看x1這一端的輸入即可,首先,這裡的FC是一個設計特殊的全連線層,從上圖能夠看到,x2部分被mask調的地方對應的權重w固定為0,bias固定為-1(這裡的設計其實主要是為了對齊,nn結構沒有辦法真正在物理結構上單獨對特徵進行分裂計算,透過這種曲線救國的方式來模擬,這裡設計了0,0和-1,-1的原因主要在於:

Tabnet 完全指南(待續,更新於2021-01-06)

後面要進行加法計算,如果矩陣的維度不相等沒法進行加法計算。

所以輸出部分:

Tabnet 完全指南(待續,更新於2021-01-06)

-1,-1的這部分的地方就解釋完畢了,然後來看看C1x1-C1a和-C1x1+C1a的部分,很明顯,二者是相反數的關係,也就是說必定一正一負或者都為0,這裡實際上就是模擬決策樹進行特徵選擇的過程,因為C1x1-C1a和-C1x1+C1a必然符號相反或者同為0,經過relu之後的情況無非兩種:

1、C1x1-C1a和-C1x1+C1一個正一個負,則relu之後的結果必定為[a,0,0,0]或[0,a,0,0],a為正數,則表示對這個特徵進行了選擇,並且這個特徵的權重為a;

2、C1x1-C1a和-C1x1+C1都是0,則relu之後全為0,這個特徵沒有被選擇;

tabnet實現的是一種軟性的特徵選擇;特徵是以權重的形式進入後續的計算而不是簡單的0或者1(gbdt的特徵選擇從廣義上來說就是簡單的對特徵進行權重為0或者1的賦權)

為了更好的理解實際的實現,這裡看下實現的程式碼

Google Colaboratory

keras版的實現如上圖,參考了torch的高star的tabnet實現

因為這個版本的tabnet封裝的太完整了,可讀性上沒有直接的實現來的方便,魔改上也不是很方便所以看keras版的相對清晰一點。

輸入從輸入開始,假設輸入資料為x,則

第一步:x = self。bn(x) 輸入資料先經過bn層進行層內標準化

第二步:attention = self。first_block(self。shared(x))[:,:self。n_a]

這裡,self。shared的定義如下:

def GLU(x):

return x * tf。sigmoid(x)

class FCBlock(layers。Layer):

def __init__(self, units):

super()。__init__()

self。layer = layers。Dense(units)

self。bn = layers。BatchNormalization()

def call(self, x):

return GLU(self。bn(self。layer(x)))

class SharedBlock(layers。Layer):

def __init__(self, units, mult=tf。sqrt(0。5)):

super()。__init__()

self。layer1 = FCBlock(units)

self。layer2 = FCBlock(units)

self。mult = mult

def call(self, x):

out1 = self。layer1(x)

out2 = self。layer2(out1)

return out2 + self。mult * out1

可以看到,sharedblock對輸入做了一個 fcblock的變換之後,又做了第二個fcblock的變換,然後透過一個小的skip connection將二者進行求和,其中第一個fcblock的部分乘一個根號0。5的常數(和論文保持一致,具體作用我也不太清楚為啥,可能是為了梯度更新穩定?)

這裡share block就是下圖中的share across decision steps的虛線方框對應的部分。

Tabnet 完全指南(待續,更新於2021-01-06)

keras的實現很簡潔,fc+bn+glu同時實現,注意glu不是gelu,glu=x*sigmoid(x),實現的功能類似於LSTM中的門控機制自動進行特徵選擇(從這個層面上來看和注意力機制有異曲同工之妙),而gelu是

Tabnet 完全指南(待續,更新於2021-01-06)

參考 馬東什麼:關於gelu

而這裡的firstblock對應的是decision block,其定義如下:

class DecisionBlock(SharedBlock):

def __init__(self, units, mult=tf。sqrt(0。5)):

super()。__init__(units, mult)

def call(self, x):

out1 = x * self。mult + self。layer1(x)

out2 = out1 * self。mult + self。layer2(out1)

return out2

直接繼承了sharedblock,實現邏輯和sharedblock非常相似只不過多了一個殘差連線。

Tabnet 完全指南(待續,更新於2021-01-06)

其實對應的就是這個圖裡的decision step dependent的方框中的邏輯。

需要注意,上述的sharedblock和dicision block都是初始化為:

self。shared = SharedBlock(n_d+n_a)

self。first_block = DecisionBlock(n_d+n_a)

這裡n_d和n_a是兩個使用者指定的超引數後面會解釋其意義和作用。

第三步:進入attentive transformer進行特徵選擇:

attention = self。first_block(self。shared(x))[:,:self。n_a]

for i in range(self。steps):

mask = self。attention[i](attention, self。prior_scale。P)

entropy = mask * tf。math。log(mask + self。eps)

mask_losses。append(

-tf。reduce_sum(entropy, axis=-1) / self。steps

prior = self。prior_scale(mask)

out = self。decision_blocks[i](self。shared(x * prior))

attention, output = out[:,:self。n_a], out[:,self。n_a:]

final_outs。append(tf。nn。relu(output))

這裡,首先是n_a部分的意義,n_a這個引數,在使用torch的tabnet實現的時候也會有這個引數,這個引數相當於做bagging,但是這裡的bagging又和xgb中的bagging的意義不一樣。

xgb裡做bagging是直接在原始的特徵上做特徵選擇,而在tabnet中原始的特徵是先經過sharedblock和firstblock的非線性變換之後才進入特徵選擇的,舉個例子,假設輸入特徵為100維,n_d=100,n_a=50,則輸入特徵進入batchnorm之後仍舊是100維,然後進入shareblock變成了150維,然後經過了firstblock之後仍舊是150維,然後取這150維中,n_a這50維對應的部分進入後續計算(注意看論文原圖,有一個first block的概念,即進入第一個shareblock+decisionblock結構的時候,僅輸出n_a,n_d部分是木有的,這個看原始碼也能看出來)

這裡:

attention = self。first_block(self。shared(x))[:,:self。n_a]

中,對輸出進行截斷的部分對應的是:

Tabnet 完全指南(待續,更新於2021-01-06)

論文中的split部分。

接著是這裡, for i in range(self。steps) 中的 steps的部分,其中

self。prior_scale。reset()

final_outs = []

mask_losses = []

x = self。bn(x)

attention = self。first_block(self。shared(x))[:,:self。n_a]

for i in range(self。steps):

mask = self。attention[i](attention, self。prior_scale。P)

entropy = mask * tf。math。log(mask + self。eps)

mask_losses。append(

-tf。reduce_sum(entropy, axis=-1) / self。steps

prior = self。prior_scale(mask)

out = self。decision_blocks[i](self。shared(x * prior))

attention, output = out[:,:self。n_a], out[:,self。n_a:]

final_outs。append(tf。nn。relu(output))

steps也是使用者來定義的,類似於定義xgb中的引數max depth,即進行幾次分裂。在tabnet中,

Tabnet 完全指南(待續,更新於2021-01-06)

steps的大小指定了上圖中紫色框框中的結構重複幾次。

然後進入迴圈部分,

先是:

mask = self。attention[i](attention, self。prior_scale。P)

其中,attention的初始化為:

class AttentiveTransformer(layers。Layer):

def __init__(self, units):

super()。__init__()

self。layer = layers。Dense(units)

self。bn = layers。BatchNormalization()

def call(self, x, prior):

return sparsemax(prior * self。bn(self。layer(x)))

self。attention = [AttentiveTransformer(input_dim)] * steps

注意,最終的mask式的特徵選擇發生在這裡,不過看上面的計算實際上也不能算是嚴格意義上的特徵選擇了(原始特徵都變換成新的了選擇啥),可以看到,加性注意力機制即attentivetransformer和nlp中大家熟知的qvk式的attention也不太一樣,這種簡單的實現方式類似於之前寫過的一篇:

這裡加性注意力機制的邏輯很簡單,首先經過dense層進行恆等變換(即維度不變的線性變換),然後進入bn層,最後乘以一個先驗係數prior之後走sparsemax。

其中sparsemax的含義可見:

sparsemax可以將部分輸出完全置0。那麼prior這個先驗係數是啥呢?

class Prior(layers。Layer):

def __init__(self, gamma=1。1):

super()。__init__()

self。gamma = gamma

def reset(self):

self。P = 1。0

def call(self, mask):

self。P = self。P * (self。gamma - mask)

return self。P

這裡就涉及到gamma這個引數,也是需要使用者手動定義的,

這裡的prior的設計有幾個需要注意的:

1。prior每一個batch會重置一下,初始和重置的prior均為1;

2。prior在for i in range(self。steps)的過程中會不斷更新,其更新公式為

第n個step的prior=第n-1個step的prior *(超引數gamma-第n-1的step的mask)

,這裡模擬了tree中第n次分裂是在第n-1次分裂的基礎上進行的過程

。tabnet的最終的軟性特徵選擇就是透過

prior = self。prior_scale(mask)

out = self。decision_blocks[i](self。shared(x * prior))

Tabnet 完全指南(待續,更新於2021-01-06)

來實現的

第四步:

out = self。decision_blocks[i](self。shared(x * prior))

attention, output = out[:,:self。n_a], out[:,self。n_a:]

final_outs。append(tf。nn。relu(output))

x,原始輸入乘先驗,然後進入featuretransformer,得到的計算結果,這回分兩部分了,n_d部分透過skip connection+relu直接和最終的輸出短接,n_a的部分進入後續的重複迭代的過程。

完整程式碼可見:

補充:關於 mask loss

Tabnet 完全指南(待續,更新於2021-01-06)

對應程式碼為:

@tf。function

def call(self, x):

self。prior_scale。reset()

final_outs = []

mask_losses = []

x = self。bn(x)

attention = self。first_block(self。shared(x))[:,:self。n_a]

for i in range(self。steps):

mask = self。attention[i](attention, self。prior_scale。P)

entropy = mask * tf。math。log(mask + self。eps) ##mask loss在這裡

mask_losses。append(

-tf。reduce_sum(entropy, axis=-1) / self。steps

) # mask loss 在這裡

prior = self。prior_scale(mask)

out = self。decision_blocks[i](self。shared(x * prior))

attention, output = out[:,:self。n_a], out[:,self。n_a:]

final_outs。append(tf。nn。relu(output))

final_out = self。add_layer(final_outs)

mask_loss = self。add_layer(mask_losses)

return self。final(final_out), mask_loss

這裡的mask用來約束mask值的大小,mask整體越小則mask loss越小,即特徵選擇的過程中被pass掉的(即mask=0)的特徵也越多,簡單來說就是一種特定於tabnet這種類tree結構的新的loss的定義,然乎透過dl中的簡單的多目標最佳化方法最佳化。

標簽: self  mask  prior  Attention  __