最大熵模型(MEM)

最大熵模型的優點在於可以添加任意自定義特徵,且不需要保持維度一致性(通俗而言,就是每個樣本滿足哪些特徵,就放哪些特徵;特徵函數的取值均爲 0 或 1;放進來的特徵即表示該樣本在該特徵維度上取值爲 1,否則默認爲 0),因此適用於特徵維度極大的分類任務;例如 NLP 工作中的詞性標註,依據上文出現的詞語判斷詞性,出現的詞語即表示爲 1,語料庫中詞彙的數量通常高達數十萬,在通常模型中構造維度高達數十萬的訓練樣本顯然是不現實的,而通過最大熵模型,每個樣本只需要提供個位數的特徵信息。相對於隱馬爾科夫模型,最大熵模型破除了齊次性的假設,同時引入特徵函數,使模型預測得以考慮更多複雜特徵,而非只有上一時間步長的狀態。

模型訓練時計算經驗分佈下的特徵權重和當前訓練樣本下的特徵權重,通過算法收斂使得訓練樣本下的模型特徵權重逐漸接近經驗分佈下的特徵權重。當測試樣本中出現超出訓練樣本特徵集的特徵,模型在計算其針對各個分類的條件概率 p(yxi;x1,x2,...xn)p(y|x_i;x_1,x_2,...x_n) 時默認其爲均勻分佈,即最大熵最核心的思想——最大熵。由於該模型的數學原理較爲複雜,筆者暫時只呈現相關代碼 (今後有空閒時間再補充),對相關原理感興趣可先參考這一篇文章:最大熵模型

以下代碼來自鄭捷所著《NLP漢語自然語言處理,原理與實踐》,使用如下函數進行梯度更新:ΔW=1MlogEpEp\Delta W = \frac{1}{M}log\frac{E_{p^*}}{E_p}Ep,i=exp(jwjf(xj))ZE_{p,i}=\frac{exp\big(\sum_jw_jf(x_j)\big)}{Z}MM 爲所有訓練樣本中在某一訓練樣本同時滿足的特徵最大數量(通常不大於十位整數),用於控制算法收斂速度;EpE_{p} 爲當前特徵權重下的特徵期望,EpE_{p^*} 爲經驗分佈下的特徵期望,後者是前者的收斂目標;Z 爲歸一化因子;隨着模型訓練進程的不斷推進,迭代次數逐漸增加,EpEpE_{p}\rightarrow E_{p^*}

import math
from copy import deepcopy
from collections import defaultdict


class MEM(object):
    
    def __init__(self):
        self.features = defaultdict(int)                    #特徵後驗分佈/Ep序號
        self.train = []                                     #訓練集
        self.label = []                                     #標籤集
        self.size = 0                                       #訓練集長度
        self.w = []                                         #特徵權重
        self.lastw = []                                     #上一次更新前的特徵權重(用於檢測是否實現收斂)
        self.Ep = []                                        #經驗分佈下的特徵分佈
        self.Ep_ = []                                       #訓練樣本下的特徵分佈(待收斂對象)
        self.M = 0                                          #GIS算法的M參數
        
    def fit(self,train,label,max_iter=1000):
        '''訓練模型
        '''
        assert isinstance(train,list)
        assert isinstance(label,list)
        assert len(train) == len(label)
        self.train += [case.split() for case in train]
        self.label += label
        
        for case,label in zip(self.train,self.label):
            for feature in case:
                self.features[(label,feature)] += 1
        self.init_params()                                  
        
        for i in range(max_iter):
            self.Ep = self.compute_Ep()                     #計算模型權重下的特徵期望
            self.lastw = deepcopy(self.w)                   #防止同步賦值
            for t,w in enumerate(self.w):
                delta = 1.0/self.M*math.log(self.Ep_[t]/self.Ep[t])
                self.w[t] += delta                          #更新
            if self.if_converged():    break                #收斂完成後停止迭代
            
    def init_params(self):
        '''初始化參數及中間變量
        '''
        self.size = len(self.train)
        self.M = max([len(case) for case in self.train])
        self.Ep_ = [0.0]*len(self.features)
        for i,feature in enumerate(self.features):
            self.Ep_[i] = float(self.features[feature]/(self.size))    #根據經驗分佈計算特徵期望
            self.features[feature] = i                      #爲每個特徵函數分配id
        self.w = [0.0]*len(self.features)                   #初始化權重
        self.lastw = [0.0]*len(self.features)
        
    def compute_Ep(self):
        '''特徵函數
        '''
        Ep = [0.0]*len(self.features)
        for case in self.train:                             #從訓練集中迭代輸入特徵
            prob = self.compute_prob(case)                  #計算分類條件概率p(y|x)
            for feature in case:
                for weight,label in prob:
                    if (label,feature) in self.features.keys():
                        idx = self.features[(label,feature)]
                        Ep[idx] += float(weight/(self.size))    #sum[f(y,x)*p(y|x)*1/N],f(y,x)=0,1
        return Ep
    
    def compute_prob(self,case):
        '''計算分類條件概率p(y|x)
        '''
        weights = [(self.compute_w(case,label),label) for label in set(self.label)]    
        Z = sum([weight for weight,label in weights])       #參數歸一化
        prob = [(weight/Z,label) for weight,label in weights]     #概率向量
        return prob
    
    def compute_w(self,case,label):
        '''計算特徵總權重的指數
        '''
        weight = 0.0
        for feature in case:
            if (label,feature) in self.features.keys():
                weight += self.w[self.features[(label,feature)]]
        return math.exp(weight)
    
    def if_converged(self):    
        '''收斂——終止條件
        '''
        for w1,w2 in zip(self.lastw,self.w):
            if abs(w1-w2) >= 0.01: return False
        return True
    
    def predict(self,test):
        '''預測分類
        '''
        assert isinstance(test,list)
        results = []
        for case in test:
            result = self.compute_prob(case.strip().split())
            result.sort(reverse=True)
            results.append(result[0][1])
            print(result)
        return results
    
    
if __name__ == '__main__':
    
    train = ['銀行卡有餘額 家人',
             '銀行卡有餘額 不加班 朋友',
             '銀行卡有餘額 開心 朋友',
             '銀行卡有餘額 開心 家人',
             '銀行卡餘額不多 開心 加班 朋友',
             '銀行卡餘額不多 不開心 不加班 朋友',
             '銀行卡餘額不多 不開心 不加班 家人',
             '銀行卡餘額不多 開心 加班 家人',
             '銀行卡餘額用盡 開心',
             '銀行卡餘額用盡 不開心',
             '銀行卡餘額用盡 不開心 家人',
             '銀行卡餘額用盡 不開心 加班',
             '銀行卡餘額用盡 不開心 加班 朋友 家人']
    label = ['不出遊','出遊','出遊','出遊','出遊','不出遊','出遊','不出遊','不出遊','不出遊','不出遊','不出遊','不出遊']
    test = ['銀行卡餘額不多 開心 朋友',
            '銀行卡餘額用盡 開心 朋友',
            '銀行卡餘額用盡 不開心',
            '銀行卡餘額用盡',
            '漲薪 獲得一萬元信用額度 朋友請客']    #訓練樣本中未出現的特徵
    
    model = MEM()
    model.fit(train,label)
    model.predict(test)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章