pmf-automl源碼分析

初窺項目文件

用jupyter lab打開all_normalized_accuracy_with_pipelineID.csv
在這裏插入圖片描述

all_normalized_accuracy_with_pipelineID.zip contains the performance observations from running 42K pipelines on 553 OpenML datasets. The task was classification and the performance metric was balanced accuracy. Unzip prior to running code.

行表示pipeline id,列表示dataset id,元素表示balanced accuracy

在這裏插入圖片描述
簡單查閱了一下pipelines.json,基本只有pcapolynomial兩種preprocessor。

PMF模型訓練

數據切分

Ytrain, Ytest, Ftrain, Ftest = get_data()
>>> Ytrain.shape
Out[2]: (42000, 464)
>>> Ytest.shape
Out[3]: (42000, 89)
>>> Ftrain.shape
Out[4]: (464, 46)
>>> Ftest.shape
Out[5]: (89, 46)

訓練測試集切分,89個數據集作爲測試集,464個訓練集

初始隱變量

    imp = sklearn.impute.SimpleImputer(missing_values=np.nan, strategy='mean')
    X = sklearn.decomposition.PCA(Q).fit_transform(
                                            imp.fit(Ytrain).transform(Ytrain))
>>> X.shape
Out[7]: (42000, 20)

根據目前的理解,整個訓練過程就是根據GP來訓練X的隱變量。這個隱變量是用PCA初始化的。

處理訓練集的缺失值,並降維爲20維(42K個pipelines,數據集從553降爲20個隱變量)

論文:the elements of YY are given by as nonlinear function of the latent variables, yn,d=fd(xn)+ϵy_{n,d}=f_d(x_n)+\epsilon, where ϵ\epsilon is independent Gaussian noise.

這裏的YY指的是整個42000×46442000\times464矩陣,那麼XX就是pipeline空間的隱變量,這裏隱變量維度Q=20Q=20XX的shape爲42000×2042000\times20

模型的定義與訓練

模型的頂層定義:

    kernel = kernels.Add(kernels.RBF(Q, lengthscale=None), kernels.White(Q))
    m = gplvm.GPLVM(Q, X, Ytrain, kernel, N_max=N_max, D_max=batch_size)
    optimizer = torch.optim.SGD(m.parameters(), lr=lr)
    m = train(m, optimizer, f_callback=f_callback, f_stop=f_stop)

f_callbackf_stop都是兩個local函數

    def f_callback(m, v, it, t):
        varn_list.append(transform_forward(m.variance).item())
        logpr_list.append(m().item()/m.D)
        if it == 1:
            t_list.append(t)
        else:
            t_list.append(t_list[-1] + t)

        if save_checkpoint and not (it % checkpoint_period):
            torch.save(m.state_dict(), fn_checkpoint + '_it%d.pt' % it)

        print('it=%d, f=%g, varn=%g, t: %g'
              % (it, logpr_list[-1], transform_forward(m.variance), t_list[-1]))
    def f_stop(m, v, it, t):

        if it >= maxiter-1:
            print('maxiter (%d) reached' % maxiter)
            return True

        return False

看到訓練函數train

def train(m, optimizer, f_callback=None, f_stop=None):

    it = 0
    while True:

        try:
            t = time.time()

            optimizer.zero_grad()
            nll = m()
            nll.backward()
            optimizer.step()

            it += 1
            t = time.time() - t

            if f_callback is not None:
                f_callback(m, nll, it, t)

            # f_stop should not be a substantial portion of total iteration time
            if f_stop is not None and f_stop(m, nll, it, t):
                break

        except KeyboardInterrupt:
            break

    return m

論文公式(5):

NLLd=12(Ndlog(2π)+logCd+Yc(d).dTCd1Yc(d).d)NLL_d=\frac{1}{2}(N_d log(2 \pi)+log|C_d|+Y^T_{c(d).d} C^{-1}_d Y_{c(d).d})

看到GPLVM的前向函數gplvm.GPLVM#forwardix = self.get_batch()的作用是在數據集這個維度上取了50的batch。

        lp = torch.tensor([0.])
        for j in ix:
            lp += self.GPs[j]()

        return lp*self.D/self.D_max

Dmax=50D_{max}=50,表示數據集方向上的batch數

>>> type(self.GPs)
Out[11]: torch.nn.modules.container.ModuleList
>>> type(self.GPs[0])
Out[12]: gplvm.GP

D個高斯過程的定義

GPs的定義:

self.D = Y.shape[1]
for d in range(self.D):
    ix = np.where(np.invert(np.isnan(Y[:,d])))[0]
    y = Sparse1DTensor(Y[ix,d], torch.tensor(ix))
    self.GPs.append(GP(dim, self.X, y, kernel, **kwargs))

DD表示數據集的數目,這裏爲464464np.invert表示對bool變量取反,ix最終表示的是非nan值的index。

utils.Sparse1DTensor是作者自定義的一個稀疏一維張量。

後驗分佈協方差矩陣的求解

transform_forward與transform_backward函數

transform_forward = lambda x: (1+x.exp()).log()
transform_backward = lambda x: (x.exp()-1).log()

transform_forward:x=log(1+ex)x=log(1+e^x)
transform_backward:x=log(ex1)x=log(e^x-1)
在這裏插入圖片描述在這裏插入圖片描述

get_cov函數的頂層設計

沿數據集方向,每個yy ,即所有pipeline在該數據集上的表現向量,都會被處理爲Sparse1DTensorX,y,kernelX,y,kernel都會被傳入GPGP中。看到gplvm.GP

self.variance = torch.nn.Parameter(
transform_backward(torch.tensor([variance])))

variance會在gplvm.GP#get_cov中使用

看到GP的forward,mn = torch.zeros(ix.numel())表示均值,cov = self.get_cov(ix=ix)表示協方差矩陣,看到get_cov:

return torch.potrf(self.kernel(self.X[ix])
       + torch.eye(ix.numel())
       * transform_forward(self.variance),
       upper=False)

該版本PyTorch不存在potrf函數,我用cholesky代替了他,並去掉了upper關鍵字。

>>> self.X[ix].shape
Out[22]: torch.Size([1000, 20])
>>> ix.shape
Out[23]: torch.Size([1000])

對pipeline的隱變量空間取1000的batch

>>> kernel
Out[5]: 
Add(
  (k1): RBF()
  (k2): White()
)

kernel的RBF

看到kernels.RBF

def sqdist(X, Y):

    assert X.size()[1] == Y.size()[1], 'dims do not match'

    return ((X.reshape(X.size()[0], 1, X.size()[1])
             - Y.reshape(1, Y.size()[0], Y.size()[1]))**2).sum(2)
>>> X.shape
Out[11]: torch.Size([1000, 20])
>>> X.reshape(X.size()[0], 1, X.size()[1]).shape
Out[13]: torch.Size([1000, 1, 20])
>>> Y.reshape(1, Y.size()[0], Y.size()[1]).shape
Out[15]: torch.Size([1, 1000, 20])
>>> (X.reshape(X.size()[0], 1, X.size()[1])
    - Y.reshape(1, Y.size()[0], Y.size()[1])).shape
Out[14]: torch.Size([1000, 1000, 20])
>>> ((X.reshape(X.size()[0], 1, X.size()[1])
    - Y.reshape(1, Y.size()[0], Y.size()[1]))**2).sum(2).shape
Out[16]: torch.Size([1000, 1000])

可以理解爲1000個隱變量,每個隱變量的長度爲20維(X,Y的隱變量長度shape1要一致)。1000個隱變量和自己(也可以是Y)逐個求舉例,形成一個1000×10001000\times1000的距離矩陣,這就是KK

RBF=forword(backward(var))×(12eK(X,X))RBF=forword(backward(var))\times(-\frac{1}{2}e^{K(X,X)})

調試打印變量發現forword(backward(var))=1forword(backward(var))=1,則:

RBF(X)=12eK(X,X)RBF(X)=-\frac{1}{2}e^{K(X,X)}

記錄一些迷惑行爲

>>> transform_forward(self.variance)
Out[18]: tensor([1.], grad_fn=<LogBackward>)
>>> l
Out[19]: 
tensor([[1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
         1., 1.]], grad_fn=<LogBackward>)
>>> self.lengthscale
Out[20]: 
Parameter containing:
tensor([[0.5413, 0.5413, 0.5413, 0.5413, 0.5413, 0.5413, 0.5413, 0.5413, 0.5413,
         0.5413, 0.5413, 0.5413, 0.5413, 0.5413, 0.5413, 0.5413, 0.5413, 0.5413,
         0.5413, 0.5413]], requires_grad=True)

kernel的White

看到kernels.White

return torch.eye(X.size()[0])*transform_forward(self.variance)
>>> transform_forward(self.variance)
Out[21]: tensor([1.], grad_fn=<LogBackward>)
>>> torch.eye(X.size()[0])*transform_forward(self.variance)
Out[22]: 
tensor([[1., 0., 0.,  ..., 0., 0., 0.],
        [0., 1., 0.,  ..., 0., 0., 0.],
        [0., 0., 1.,  ..., 0., 0., 0.],
        ...,
        [0., 0., 0.,  ..., 1., 0., 0.],
        [0., 0., 0.,  ..., 0., 1., 0.],
        [0., 0., 0.,  ..., 0., 0., 1.]], grad_fn=<MulBackward0>)

求了半天是個EE

求協方差矩陣覆盤

最後回到gplvm.GP#get_cov中:

return torch.cholesky(self.kernel(self.X[ix])
       + torch.eye(ix.numel())
       * transform_forward(self.variance),
       upper=False)
>>> self.kernel(self.X[ix]).shape
Out[24]: torch.Size([1000, 1000])
>>> transform_forward(self.variance)
Out[25]: tensor([1.], grad_fn=<LogBackward>)

kernel(X)=RBF(X)+White(X)kernel(X)=RBF(X)+White(X)
=12eK(X,X)+E=-\frac{1}{2}e^{K(X,X)}+E

cov=cholesky(kernel(X)+E)cov=cholesky(kernel(X)+E)
=cholesky(12eK(X,X)+2E)=cholesky(-\frac{1}{2}e^{K(X,X)}+2E)

Cholesky 分解是把一個對稱正定的矩陣表示成一個下三角矩陣L和其轉置的乘積的分解

相當於:

cov×covT=12eK(X,X)+2Ecov\times cov^T=-\frac{1}{2}e^{K(X,X)}+2E

這樣看清楚多了。具體細節還需要在論文中求證一下。

大概瞭解了協方差矩陣是怎麼求的。就是對於XRN×QX\in R^{N\times Q}的pipeline隱變量,按照上面式子的方法利用RBF核函數求解。

GP前向函數的返回值的含義

回到GP的前向函數gplvm.GP#forward

def forward(self, ix=None):

    if ix is None:
        ix = self.get_batch()

    mn = torch.zeros(ix.numel())
    cov = self.get_cov(ix=ix)
    pdf = dist.multivariate_normal.MultivariateNormal(mn, scale_tril=cov)

    return -pdf.log_prob(self.y[ix])
>>> dist.__package__
Out[31]: 'torch.distributions'
>>> self.y[ix].shape
Out[35]: torch.Size([1000])

分析self.y[ix],看到utils.Sparse1DTensor#__getitem__

    def __getitem__(self, k):

        if not len(k.size()):
            return self.v[self.ix[k.item()]]
        else: # 命中的條件分支
            return torch.tensor([self.v[self.ix[kk]] for kk in k.tolist()])
>>> self.v.shape
Out[40]: torch.Size([37224])

對於每個數據集DD,訓練一個GPGPyy表示NN個pipelines在DD上的表現(balancedAccuracy,[1,1])(balancedAccuracy,\in [-1,1])。對於NN個pipelines,根據0均值和核函數求一個概率密度函數ϕ(N)N(0,Σ)\phi (N) \backsim \mathcal N(0,\Sigma)

然後將yy,也就是NN個pipelines在DD上的表現帶入這個分佈,直接求出在這個概率密度函數上的概率密度。

如果如果表現爲0,結果爲1σ2π\frac{1}{\sigma \sqrt{2\pi}}

最後結果是yy在多元概率密度函數上的概率密度,越大說明隱變量越合理。

>>> pdf.log_prob(self.y[ix]).item()
Out[44]: -1400.3924560546875
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章