機器學習模型系列 (1)— Log Linear Model

Martin Huang
機器學習系列
Published in
14 min readSep 12, 2023

除了深度學習,也想再從機器學習相關的理論和實作中繼續深入,有些概念背後其實兩者是相通的。因此開啟這個系列,未來也會分享更多的模型和實作給大家。

至於為什麼是先挑Log Linear model?因為它算是結構相對比較基礎的模型,無論是研究它或實作它的難度會比較低。同時,也可以把重點放在實踐其他機器學習基礎概念上,而非過多鑽研模型數學本身(但不代表這不重要)。

從Log Linear Model本身開始說起。

離散分布型資料的分類

條件機率

舉個簡單的例子好了。例如統計文具店內,原子筆品牌和其筆桿顏色,得到表1數據:

表1.

若根據此資料,想要推算「紫色筆桿為某品牌的機率」,以條件機率表示為p(品牌|紫色)。最直觀的方法是用這個資料紀錄的數目直接推估:例如,是品牌E的機率為1/2,而品牌C也是1/2。利用這個方法,當然,在數據很大的時候是相當可靠的,因為樣本數上升,其分布型態會越有機會趨近於母群體。然而,若僅靠目前的資料,或即使是再增加一些,推算出來的機率可能和實際有誤差。例如,紫色筆桿為品牌A或B的機率都是0。0是一個很極端的狀況,表示「完全否定」,作為機率輸出,不是很好的結果。取而代之的,會希望機率值「極低」,趨近於0,而非0本身。

要這樣做的話,我們必須取一個函數,這個函數和條件機率有相關性,且最好不會輸出0。例如:指數函數。所有的指數都不會輸出0,只有負無窮大才會趨近於0,這之中我們要選數學跟物理學界最愛的自然指數e當底數。

特徵

除此之外,我們也需要從資料中找出一些能協助提高判斷準確度的因子,例如:品牌C實際上只出一款原子筆,且筆桿是紫色。相對來說,品牌E出了花系列筆桿,共五色,包色也只有1/5的機率是紫色。這樣一來,在所有紫色筆桿的原子筆裡面,抽到品牌C的機會可能會大一些。但另一方面,品牌E的紫色筆桿總產量是品牌C的5倍,所以機率差距又被抹平了。這些種種的因素,我們可以把它量化。有時,甚至這些是抽象的,我們只能用一些數學分析方法得到,我們給這些因子一個專有名詞 — 特徵。

綜合以上,可以得到一個函數的雛形:

式1. 預測函數雛形

其中f(x,y)為特徵函數,用於將資料中「可能」或「按照以往經驗」和我們想預測的結果有關的項目提取出來,以代表更原始的資料(例如紀錄的數目,剛剛提過直接使用可能會有比較極端的結果)。θ則是權重,是為一個可調整的參數。他的初始設定可以是根據特徵出現的條件,也可以是出現的機率,甚至是隨機設定,但最後我們會慢慢調整他到一個適當的位置,這個後面再討論。

最重要的是,式1.必為正值,因為θ●f(x,y)不可能為負無限大。

Log Linear Model

我們把式1.在各個特徵的情況下計算出值後,如果要讓它變成最後的預測機率,除了各值必為正之外,還要滿足一個前提,即總值不可超過1。無論是什麼樣的機率,不可能超過100%,對吧!而且,對於單一的x,其所有y的條件機率的加總也不得超過1。所以,還需要再標準化,這邊使用的是一個被稱作配分函數(partition function)的概念,如果查詢這個名詞會被連結到統計物理,扯到熱力學、量子場論,但在這邊,簡單來說就是在特定x下的所有y的值加總。如果直觀一點想,有點類似加權平均的概念吧(很不嚴謹的類比)。

最後把式子寫出來,長這樣:

式2. Log-Linear Model

x屬於X,對應輸入空間。y屬於Y,對應輸出空間。θ為與特徵相關的權重,其對應一實數空間,維度視特徵函數f(x,y)給出的向量而定。θ和特徵函數f對應的實數空間相同。分子即式1.,分母為上面提過的配分函數,用於標準化,讓機率的加總為1。

所以為什麼叫Log Linear Model?這個式子裡既沒有對數,也沒有線性的樣子啊?因為還沒取對數呀。如果把式2.等號兩邊同時取,以e為底的對數的話:

式3.

所以Log Linear Model指的是「取對數的話就會出現線性方程式」的意思。

特徵函數

其實這中間最關鍵的是特徵函數。它代表資料,而選擇哪些來代表資料則是人(深度學習則可以有部分讓模型自行萃取,但其實人還是可以決定哪些讓模型萃取,所以仍不能完全排除)。設計特徵函數的過程被稱為特徵工程。這取決於任務需要,以及資料的形式。

以圖片為例,將畫素轉成紅藍綠的色碼,也是一種特徵。如果是文字,則可以用token、或者word embeddings等方法。

評估model的輸出

loglinear model輸出的是一組數值,在這裡,它的意義是「屬於某一類的機率」。通常,我們有「標準答案」 — 被稱作標註(annotation)的ground truth,在標註中,一定將這組特徵歸於某一個分類,亦即,該分類機率為100%,而其他則為0。怎麼把標註的機率和模型輸出的機率對照?

我們要評估兩種向量的一致性,最常用的是計算他們的距離,由此衍伸出來的是mean square error(方均差,MSE)。在機率,則使用交叉熵(cross entropy的方式),其公式為

例如某資料有之標註三分類,在某項的標註為第二類,其標註型態為[0,1,0],使用one-hot encoding。若該項對應之特徵組,經模型A運算之後,認為各分類的機率為[0.1, 0.7, 0.2],而模型B則輸出[0.4, 0.4, 0.2],依照上面公式

模型A得到的值為

B則為

熵表示混亂程度。所以如果預測和標註的機率越接近,熵應該越低,就如同向量間距離越小,兩者方向越一致。在這個例子中,顯然我們從目視結果即可知A是比較接近標註的,交叉熵的值驗證這一想法,並將其量化。

更多關於cross entropy的描述,請看這篇

知道量化差異之後,我們便可進一步利用偏微分計算梯度,再反向傳播以更新模型內部的參數,然後用下一批資料訓練,再次計算梯度,然後更新權重…直到資料用畢。這樣就完成一輪(epoch)模型的訓練了。

關於反向傳播演算法,這邊不多詳述,以免偏離主題。有興趣的人可到這篇觀看

實作

本次實作使用Pytorch。我是在google colab上面跑的。

資料集使用機器模型界的Hello World資料集 — 鳶尾花(iris)。可從Kaggle取得載點。

建構LogLinear Model

按照其公式,核心為一組線性,即ax+b。使用nn.Module建模,在__init__宣告可更新變數a、b作為權重和誤差,並在forward方法裡建構計算流程,如此一來便不需要寫梯度反向傳播的計算式(即偏導數方程式),這也是nn.Module方便的地方。

import torch
import torch.nn as nn

class Loglinear(nn.Module):
def __init__(self):
super(Loglinear, self).__init__()
self.a = torch.nn.Parameter(torch.randn((8,2)))
self.b = torch.nn.Parameter(torch.randn((2,2)))

def forward(self, x):
exp = torch.exp(torch.add(torch.mm(x, self.a), self.b))
sum = torch.sum(exp)
div = torch.div(exp, sum)
return torch.sum(div, dim=1)

變數須宣告張量大小(tensor size),這視輸入的特徵張量而定。若不想被綁住,可以在self的地方要求輸入tensor size,這樣就可以在建模時隨輸入張量大小建立對應模型。

特徵處理

載好的資料集是一個csv檔案,利用pandas看一下內容:

import pandas as pd

iris = pd.read_csv("/Iris.csv")
print(iris)

可以得知此資料集共有四項特徵,SeptalLength/SeptalWidth/PetalLength/PetalWidth。同時,還有標註分類,是鳶尾花的子分類。

idx = pd.Index(iris["Species"])
print(idx.value_counts())

此資料集的標註有三個分類。我們先做簡單一點,二分類就好了。這裡我決定讓模型根據特徵判斷是否為Iris-setosa。首先將文字標註轉成數字(其實直接轉也可以):

mappings = {
"Iris-setosa": 1,
"Iris-versicolor": 0,
"Iris-virginica": 0
}
iris["Species"] = iris["Species"].apply(lambda x: mappings[x])

然後建立一個將資料轉換成特徵的函數。按照定義,特徵向量應該為(x|y),且不同的y皆須有對應的特徵向量。我這邊的做法是:設定(x|y=1)時為[f,f,f,f,0,0,0,0],而(x|y=0)時為[0,0,0,0,f,f,f,f],其中f為表格中的四項特徵。每一列的特徵都可以有上述兩個組合,因為有兩個分類,所以最後的特徵向量大小為(2,8)。這也是為什麼上面設計模型時的權重張量大小為(8,2),因為要做內積。

import numpy as np

def parse(row):
if row["Species"] == 1:
gt = np.asarray([1, 0])
else:
gt = np.asarray([0, 1])
feature = np.asarray(row[["SepalLengthCm", "SepalWidthCm", "PetalLengthCm", "PetalWidthCm"]])
pad = np.asarray([0,0,0,0])
f_1 = np.concatenate((feature, pad))
f_2 = np.concatenate((pad, feature))
input_ = np.stack((f_1, f_2))
return torch.tensor(input_, dtype=torch.float), torch.tensor(gt, dtype=torch.float)

接下來,要將資料先拆分為訓練集和驗證集。驗證集的資料,在訓練時不會讓模型看到,以免造成資料汙染。

關於驗證:請看這篇

因為資料集為三分類,轉成二元分類之後,資料量會有點失衡。所以我把最後50筆資料刪除了,不使用它們。這樣一來,前50筆是Iris-setosa,後50筆不是。拆分原則使用9:1,所以從是和不是的資料中各隨機抽5筆出來,組成驗證集,剩下的則做為訓練集。

from pandas.core.common import random_state

subset_positive = iris.iloc[:50]
subset_negative = iris.iloc[50:100]
test_positive = subset_positive.sample(n=5, random_state=1)
subset_positive = subset_positive.drop(test_positive.index)
test_negative = subset_negative.sample(n=5, random_state=1)
subset_negative = subset_negative.drop(test_negative.index)
test = pd.concat([test_positive, test_negative], ignore_index=True)
train = pd.concat([subset_positive, subset_negative], ignore_index=True)

損失函數按照cross entropy的定義寫。一樣用nn.Module建立。

class CELoss(nn.Module):
def __init__(self):
super(CELoss, self).__init__()

def forward(self, input, target):
loss = -(target * torch.log(input)+ (1-target) * torch.log(1-input))
return torch.mean(loss)

最後一行,torch.mean或torch.sum都可以。

萬事俱備,可以訓練了。我選用SGD來做為梯度下降的方式,pytoch本身就有對應的優化器可以使用,很方便。

梯度下降法和cross entropy在同一篇文章有描述哦!懶得捲上去看也沒關係,我這邊再放一次連結

import torch.optim as optim

model = Loglinear()
optimizer = optim.SGD(model.parameters(), lr=0.1)
loss_function = CELoss()
for epoch in range(10):
for index, row in train.iterrows():
model.zero_grad()
feature, gt = parse(row)
pred = model(feature)
loss = loss_function(pred.squeeze(), gt)
print(loss.item())
loss.backward()
optimizer.step()

可以看到我總共訓練了10輪。模型表現的結果如何呢?驗證集出場的時機到啦!記得,驗證時不要再傳導梯度了,所以設torch.no_grad()。argmax函數用來把分類中最高值的指標(index)顯示出來。這段code主要是列出預測和標註的答案,並做對照,然後列出成績。

with torch.no_grad():
true = 0
all = 0
for index, row in test.iterrows():
all += 1
feature, gt = parse(row)
output = model(feature)
model_pick = torch.argmax(output)
gt_pick = torch.argmax(gt)
print(model_pick, gt_pick)
if model_pick == gt_pick:
true += 1

print(true / all)

結果:

挺不賴的!

以上就是loglinear model的理論和實作介紹。我們下一個單元再見~

--

--

Martin Huang
機器學習系列

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