分类——最大熵模型以及Python实现

最大熵模型是一种分类模型,常用于自然语言处理。优点是能充分考虑限制条件,缺点是非常耗时。

核心思想

满足既定事实情况下,模型对不确定部分的预测应该是等可能的,即熵最大。

算法简介

模型

热力学中有熵增定理,意思是系统与外界隔离时,系统的熵将趋于增长。将这种思想应用到机器学习模型中,外界意味着约束,即既定的事实。那么模型只要满足既定的事实,其他不确定的部分就应该”熵增”,故熵最大的模型就是最终的模型,也是最优的模型。熵最大时的条件概率 P(y|X) P ( y | X ) 将被用来预测类,同朴素贝叶斯分类器,取概率最大的类。

基本概念:

特征函数:既定的事实就用特征函数来表示,并且量化。即对于样本(X, y), 若符合特定规则(事实)取值为1,反之为0。例如应用在自然语言处理中,若may后面时动词,翻译为“可能” 是一个规则,那么输入X为may后面的词性,y为may的翻译,若翻译符合规则,f(x,y)=1, 反之f(x,y)=0。当仅有数据,没有经验规则时,可以从数据中提取规则。通常数据中所有的情况都被视为既定的事实。
经验联合分布 P~(X=x,y=y) P ~ ( X = x , y = y ) 的似然估计。
经验边缘分布 P~(X=x) P ~ ( X = x ) 的似然估计。

策略

策略就是熵最大化。定义在条件概率分布 P(Y|X) P ( Y | X ) 的条件熵将达到最大:

H(P)=x,yP~(x)P(y|x)logP(y|x) H ( P ) = − ∑ x , y P ~ ( x ) P ( y | x ) l o g P ( y | x )
同时,当训练数据(X,y)包含的信息被充分挖掘时,特征函数关于 P(Y|X) P ( Y | X ) P(X) P ( X ) 的期望值应该等于特征函数关于 P~(X=x,y=y) P ~ ( X = x , y = y ) 的期望值,此为最优化问题的一个约束条件。具体细节参考李航《统计学习方法》

学习方法

最大化条件熵H(P),是一个最优化问题,可以用拉格朗日乘子法求解。拉格朗日乘子w的个数等于特征函数的个数。w相当于每个特征的权重。其对偶问题的学习方法有GIS和IIS算法(具体参考李航《统计学习方法》)。当每个样本包含的特征个数一样时,IIS算法与GIS算法表达式一致。经过本人测试,GIS算法不稳定,很难收敛。IIS算法尚未测试。

算法流程

  • Input: 训练数据集(X,y), 阈值epsilon,最大训练步骤maxstep
  • Output: 最大熵模型
  • Step1: 建立特征函数的集合,初始化每个特征的权重w, 计算经验联合分布和经验边缘分布
  • Step2: 计算每个特征函数关于经验联合分布的经验期望值
  • Step3: 基于当前特征函数的权重,计算条件分布概率 P(Y|X) P ( Y | X ) ,并计算每个特征关于条件分布概率和经验联合概率的估计期望值
  • Step4: GIS或IIS算法更新权重w,若满足epsilon和maxstep终止条件,则终止,反之转步骤2,3

代码

"""
最大熵模型: 采用IIS最优化算法(M为常数时等同于GIS算法)
"""

import math
from collections import defaultdict

import numpy as np


class MaxEnt:
    def __init__(self, epsilon=1e-3, maxstep=100):
        self.epsilon = epsilon
        self.maxstep = maxstep
        self.w = None  # 特征函数的权重
        self.labels = None  # 标签
        self.fea_list = []  # 特征函数
        self.px = defaultdict(lambda: 0)  # 经验边缘分布概率
        self.pxy = defaultdict(lambda: 0)  # 经验联合分布概率,由于特征函数为取值为0,1的二值函数,所以等同于特征的经验期望值
        self.exp_fea = defaultdict(lambda: 0)  # 每个特征在数据集上的期望
        self.data_list = []  # 样本集,元素为tuple((X),y)
        self.N = None  # 样本总量
        self.M = None  # 某个训练样本包含特征的总数,这里假设每个样本的M值相同,即M为常数。其倒数类似于学习率
        self.n_fea = None  # 特征函数的总数

    def init_param(self, X_data, y_data):
        # 根据传入的数据集(数组)初始化模型参数
        self.N = X_data.shape[0]
        self.labels = np.unique(y_data)

        self.fea_func(X_data, y_data)
        self.n_fea = len(self.fea_list)
        self.w = np.zeros(self.n_fea)
        self._exp_fea(X_data, y_data)
        return

    def fea_func(self, X_data, y_data, rules=None):
        # 特征函数
        if rules is None:  # 若没有特征提取规则,则直接构造特征,此时每个样本没有缺失值的情况下的特征个数相同,等于维度
            for X, y in zip(X_data, y_data):
                X = tuple(X)
                self.px[X] += 1.0 / self.N  # X的经验边缘分布
                self.pxy[(X, y)] += 1.0 / self.N  # X,y的经验联合分布

                for dimension, val in enumerate(X):
                    key = (dimension, val, y)
                    if not key in self.fea_list:
                        self.fea_list.append(key)  # 特征函数,由 维度+维度下的值+标签 构成的元组
            self.M = X_data.shape[1]
        else:
            self.M = defaultdict(int)  # 字典存储每个样本的特征总数
            for i in range(self.N):
                self.M[i] = X_data.shape[1]
            pass  # 根据具体规则构建

    def _exp_fea(self, X_data, y_data):
        # 计算特征的经验期望值
        for X, y in zip(X_data, y_data):
            for dimension, val in enumerate(X):
                fea = (dimension, val, y)
                self.exp_fea[fea] += self.pxy[(tuple(X), y)]  # 特征存在取值为1,否则为0
        return

    def _py_X(self, X):
        # 当前w下的条件分布概率,输入向量X和y的条件概率
        py_X = defaultdict(float)

        for y in self.labels:
            s = 0
            for dimension, val in enumerate(X):
                tmp_fea = (dimension, val, y)
                if tmp_fea in self.fea_list:  # 输入X包含的特征
                    s += self.w[self.fea_list.index(tmp_fea)]
            py_X[y] = math.exp(s)

        normalizer = sum(py_X.values())
        for key, val in py_X.items():
            py_X[key] = val / normalizer
        return py_X

    def _est_fea(self, X_data, y_data):
        # 基于当前模型,获取每个特征估计期望
        est_fea = defaultdict(float)
        for X, y in zip(X_data, y_data):
            py_x = self._py_X(X)[y]
            for dimension, val in enumerate(X):
                est_fea[(dimension, val, y)] += self.px[tuple(X)] * py_x
        return est_fea

    def GIS(self):
        # GIS算法更新delta
        est_fea = self._est_fea(X_data, y_data)
        delta = np.zeros(self.n_fea)
        for j in range(self.n_fea):
            try:
                delta[j] = 1 / self.M * math.log(self.exp_fea[self.fea_list[j]] / est_fea[self.fea_list[j]])
            except:
                continue
        delta = delta / delta.sum()  # 归一化,防止某一个特征权重过大导致,后续计算超过范围
        return delta

    def IIS(self, delta, X_data, y_data):
        # IIS算法更新delta
        g = np.zeros(self.n_fea)
        g_diff = np.zeros(self.n_fea)
        for j in range(self.n_fea):
            for k in range(self.N):
                g[j] += self.px[tuple(X_data[k])] * self._py_X(X_data[k])[y_data[k]] * math.exp(delta[j] * self.M[k])
                g_diff[j] += g[j] * self.M[k]
            g[j] -= self.exp_fea[j]
            delta[j] -= g[j] / g_diff[j]
        return delta

    def fit(self, X_data, y_data):
        # 训练,迭代更新wi
        self.init_param(X_data, y_data)
        if isinstance(self.M, int):
            i = 0
            while i < self.maxstep:
                i += 1
                delta = self.GIS()
                # if max(abs(delta)) < self.epsilon:  # 所有的delta都小于阈值时,停止迭代
                #     break
                self.w += delta
        else:
            i = 0
            delta = np.random.rand(self.n_fea)
            while i < self.maxstep:
                i += 1
                delta = self.IIS(delta, X_data, y_data)
                # if max(abs(delta)) < self.epsilon:
                #     break
                self.w += delta
        return

    def predict(self, X):
        # 输入x(数组),返回条件概率最大的标签
        py_x = self._py_X(X)
        best_label = max(py_x, key=py_x.get)
        return best_label


if __name__ == '__main__':
    from sklearn.datasets import load_iris, load_digits

    data = load_iris()

    X_data = data['data']
    y_data = data['target']

    from machine_learning_algorithm.cross_validation import validate

    g = validate(X_data, y_data, ratio=0.2)
    for item in g:
        X_train, y_train, X_test, y_test = item
        ME = MaxEnt(maxstep=10)
        ME.fit(X_train, y_train)
        score = 0
        for X, y in zip(X_test, y_test):
            if ME.predict(X) == y:
                score += 1
        print(score / len(y_test))

我的GitHub
注:如有不当之处,请指正。

你可能感兴趣的:(机器学习)