機器學習模型系列 (2) — 利用Log Linear Model做多分類任務(Multiclass task)

Martin Huang
10 min readMay 9, 2024

--

參考閱讀:機器學習模型系列(1) — Log Linear Model

本篇文章想討論的是將原本做二元分類(binary classification)的Log Linear Model,推廣到二元以上的多分類任務。

再回顧一次Log Linear Model的公式:

其中y’是y的標註,在二元分類是0/1,推廣到多元分類則是0/1/2/3...。因此,其實模型是可以用在多元分類的。

單一模型多分類

能用是一回事,但若只用一個線性模型(取log之後為線性)做多分類,從推想的角度來看,應該不容易。因為第三個分類以上的資料點,有可能橫跨在前兩個分類之間。此時,合理推測第三個分類點很容易被分到另外兩個分類,從而一直無法很好的適應資料分布。我們還是用鳶尾花資料集來看吧:

圖1. 鳶尾花資料集petalLengthCm總覽

用目視可以知道資料分布呈現三階段,也就是各有差異。但若只能畫一條線,怎麼分開三者?最理想的情況,是把中間那階的資料一分為二,但這一項的分類還是會被歸到最低那階,或最高那階。即使在更高維的空間,例如三維,一個平面也是沒辦法切開三組資料的。

實驗

使用單一Log Linear Model,將輸出分類從2增為3:

import torch
import torch.nn as nn
import torch.optim as optim

class LoglinearMulti(nn.Module):
def __init__(self, num_class=3, num_feature=4):
super(LoglinearMulti, self).__init__()
self.weight = torch.nn.Parameter(torch.randn((num_feature, num_class)))
self.bia = torch.nn.Parameter(torch.randn((1, num_class)))

def forward(self, x):
exp = torch.exp(torch.add(torch.mm(x, self.weight), self.bia))
sum = torch.sum(exp)
div = torch.div(exp, sum)
return div

基本上為前一篇文章中Log Linear Model的擴增版。把分類數目和特徵數目改成變數,以保持彈性。覺得麻煩的話,把前一篇文章self.weight最後的(8,2)改成(12,3)也可以。這邊還做一個改動,讓輸入的特徵向量完全等於特徵數目,把參數量減少了,以降低模型overfit的機率。從物理意義上看,兩個模型應該是一樣的。

轉換資料為輸入的函數如下:

import numpy as np
def parse_multi(row, num_class):
gt = np.zeros([1,3])
gt[0][int(row['Species'])] = 1
feature = np.asarray([row[["SepalLengthCm_normal", "SepalWidthCm_normal", "PetalLengthCm_normal", "PetalWidthCm_normal"]]])
return torch.tensor(feature, dtype=torch.float), torch.tensor(gt, dtype=torch.float)

訓練的函數則如下:

import torch.optim as optim

def train_multi(optimizer, model, loss_function, train_dataset, num_class=3, batch_size=10):
for epoch in range(15):
print("epoch "+str(epoch))
for index, row in train_dataset.iterrows():
feature, gt = parse_multi(row, num_class)
pred = model(feature)
loss = loss_function(pred.squeeze(), gt)
print(loss.item())
model.zero_grad()
loss.backward()
optimizer.step()
return model

因為是三個分類,全部的資料都要拿進來訓練,不能像前一篇排除掉一部分的資料。來看看模型訓練完之後在測試集的表現:

大約接近一半。模型預測的結果只有第二類和第三類,沒有第一類。雖然是小樣本的推論,但也可以說符合前面提到的想法,即單一線性模型無法很好的切多個分類。

多模型多分類

那有沒有其他方法,可以用線性模型,但還是能做多分類,然後做的還可以呢?所謂「集思廣益」,如果用多個二元分類模型,每一個模型專注在一個分類上,然後用比較的方式,選出各模型之中,認為屬於他分類的信心度(機率)最高的,是不是有機會做得比較好?

例如鳶尾花資料集有三個分類,我們就建三個Log Linear Model,訓練時各模型只要負責一個分類。預測時,同一筆資料個別輸入三個模型,讓模型看看屬於各自負責分類的機率,然後取最高者當作輸出。

實驗一

使用前一篇的模型,建立三個,訓練三個分類。一樣,全部的資料都要丟進去,不能過濾掉,以免模型沒辦法取得足夠有代表性的特徵。

import torch.optim as optim

models = list()
for i in range(3):
print("Training "+str(i)+" model")
model = Loglinear()
optimizer = optim.SGD(model.parameters(), lr=0.001)
loss_function = CELoss()
trained_model = train(optimizer, model, loss_function, train_set, i)
models.append(trained_model)

訓練完之後把模型們集合起來,預測的時候資料重複輸入至各模型。看看預測的結果:

雖然只比前一個實驗進步一點,但模型開始出現三個分類的預測結果了!這是一個重要的進步。

實驗二

實驗一表現不夠好的原因,有可能是在模型之間沒有足夠的「溝通」機制。或者說,每一個模型只判斷「是」或「不是」單一分類,但沒有模型判斷是不是兩個分類的其中之一。要處理這個問題,有兩種方法:第一種是增加判斷任兩種分類的模型,第二種則是將模型串起來,另外使用參數調整各模型的權重,讓他們共同決定輸出。這邊選擇第二種做法。

這種做法還有兩個方向:第一個方向是保持三個模型各自訓練對應的分類,只在最後輸出結果前再用參數調整權重。另一個方向則是同時訓練三個模型,也不僅限於各自對應的分類,所以對於單一模型而言,它不一定有明確對應哪個分類做鑑別。兩個方向對應的圖如下:

如果要和實驗一對標相同條件,以便比較的話,方向一比較適合。但我想同時讓各模型,保留一些關注在除了它自身分類以外的特徵,這樣可以讓模型多了一些「分類之間」的判斷。所以最後我選擇方向二。

class LoglinearResemble(nn.Module):
def __init__(self):
super(LoglinearResemble, self).__init__()
self.a = torch.nn.Parameter(torch.rand((2,1)))
self.b = torch.nn.Parameter(torch.rand((2,1)))
self.c = torch.nn.Parameter(torch.rand((2,1)))
self.Loglinear_a = Loglinear()
self.Loglinear_b = Loglinear()
self.Loglinear_c = Loglinear()

def forward(self, x):
out_a = self.Loglinear_a(x)
out_a = torch.mm(out_a, self.a)
out_b = self.Loglinear_b(x)
out_b = torch.mm(out_b, self.b)
out_c = self.Loglinear_c(x)
out_c = torch.mm(out_c, self.c)
sum = out_a+out_b+out_c
return torch.cat((out_a/sum, out_b/sum, out_c/sum), 1).squeeze()

對了,說到這邊,都還沒提到損失函數。由於cross-entropy本來就是比對「預測」和「答案」兩個向量之間的差別,在向量裡面放多個項目,也是沒問題的。它可以直接推廣到多分類任務上。

看看實驗二的結果吧。

提升非常多!不過也可以發現,其實三個分類的信心度之間沒有差很遠,有些都是差到小數點下二位,正確分類的項目才勝出。或許初始權重設定偏離一點,最後訓練完預測的結果就大逆轉也不一定。15筆資料,只要錯一筆,正確率就會掉很多~

其實方向二,已經隱約有神經網路(neural network)的雛型了。Log linear的模型本身有一層線性,只是多取了指數;後面不指定分類,意義上等同於把各模型的參數拉進一個大群,再按權重做調整,是為第二層線性,最後輸出。第二層線性,可以視為「隱藏層」(hidden layer)。

結論

以上就是Loglinear Model的實驗,從裡面也可以體會到一些數學的原理。其實都用到Pytorch了,包含線性層、損失函數,都可以用package裡面的函數直接建立物件,在這邊用手刻的目的,就是測試看看自己對於模型的運作方式是否了解夠深入,才能復刻出來。實際上使用的話當然還是直接呼叫函數囉,有造好的輪子不用,還偏偏自己再造一個,不是有效率的程式設計方法。

--

--

Martin Huang

崎嶇的發展 目前主攻CV,但正在往NLP的路上。 歡迎合作或聯絡:martin12345m@gmail.com