Welcome

目前MSRA intern,现阶段从事文摘生成方向研究。对NLP感兴趣或对博客内容有疑惑及意见建议的,欢迎评论或添加我微信。此外如果有需要内推的同学,也欢迎来骚扰我。联系方式详见contact页面。

统计学习方法|最大熵原理剖析及实现

统计学习方法|最大熵原理剖析及实现

阅读数:2,644

前言

《统计学习方法》一书在前几天正式看完,由于这本书在一定程度上对于初学者是有一些难度的,趁着热乎劲把自己走过的弯路都写出来,后人也能走得更顺畅一点。

以下是我的github地址,其中有《统计学习方法》书中所有涉及到的算法的实现,也是在写博客的同时编写的。在编写宗旨上是希望所有人看完书以后,按照程序的思路再配合书中的公式,能知道怎么讲所学的都应用上。(有点自恋地讲)程序中的注释可能比大部分的博客内容都多。希望大家能够多多捧场,如果可以的话,为我的github打颗星,也能让更多的人看到。

github:

GitHub|手写实现李航《统计学习方法》书中全部算法

相关博文:

  1. 统计学习方法|感知机原理剖析及实现
  2. 统计学习方法|K近邻原理剖析及实现
  3. 统计学习方法|朴素贝叶斯原理剖析及实现
  4. 统计学习方法|决策树原理剖析及实现
  5. 统计学习方法|逻辑斯蒂原理剖析及实现
  6. 统计学习方法|最大熵原理剖析及实现

 

正文

 

哪怕整书已经看过一遍了,我仍然认为最大熵是耗费我时间较多的模型之一。主要原因在于千篇一律的博客以及李航的《统计学习方法》在这一章可能为了提高公式泛化性而简化的关键点。当然了,在阅读上没有问题,可是在程序的编写上可能是我历时最久的。如果读者需要编写最大熵模型,比较建议使用我的博客配合代码学习,忘掉书上的这一章吧。

最大熵的直观理解

为了引出最大熵,我们可能需要举一个所有博客都会举的例子:如果我手里拿着一个骰子,想问你扔下去后是每一面朝上的概率是多少?所有人都能抢答出来是1/6。那么为什么是1/6呢?为什么不是1/2或者1/3?

因为六个面的概率相同呀

emmm….我暂时承认你说的有点道理。可是如果这是老千手里的骰子呢?你还会答1/6吗?

可是你没说是老千手里的骰子呀

你让我哑口无言了,但为什么大家在猜之前不会去假设是不是老千的骰子这种情况呢?

因为你没说是老千手里的骰子呀

完蛋了,又绕回来了。不过我想想也是,如果要考虑老千,那么也可能要考虑骰子是不是破损了,桌面是不是有问题……完蛋了,没头了。

所以1/6最保险呀

如果我告诉你1朝上的概率是1/2呢?

那剩下的就是1/10呀

emmm…我承认在目前的对话中你是明智的,而我像个低龄的儿童。但是我要告诉你,上面几句对话,其实就是最大熵。
当我们在猜测概率时,不确定的部分我们都认为是等可能的,就好像骰子一样,我们知道有六个面,那么在概率的猜测上会倾向于1/6,也就是等可能,换句话讲,也就是倾向于均匀分布。为什么倾向均匀分布?你都告诉我了,因为这样最保险呀!当我们被告知1朝上的概率是1/2时,剩下的我们仍然会以最保险的形式去猜测是1/10。是啊,最大熵就是这样一个朴素的道理:

凡是我们知道的,就把它考虑进去,凡是不知道的,通通均匀分布!

从另一个角度看,均匀分布其实保证的是谁也不偏向谁。我说万一是老千的骰子呢?你说万一骰子有破损呢?骰子好端端地放在那咱俩地里咕噜瞎猜啥呢。咱们与其瞎猜是啥啥啥情况,干脆就均匀分布,谁也不偏向谁。

多朴素的道理啊!

 

朴素贝叶斯分类器的数学角度(配合《统计学习方法》食用更佳)

 

在最大熵章节中我们在公式上仍然参考《统计学习方法》,除此之外会补充一些必要知识点,建议阅读完毕后浏览一下代码知道详细的用法。

最大熵模型是一个分类模型,这样,我们照旧不管中间过程,统统都不管,我们最终可能会想要这样一个式子: P(Y|X)。是啊,P(Y|X)就像一个终极的大门一样,扔进入一个样本x,输出标签y为各种值的概率。我们现在手里有一堆训练数据,此外手握终极大式子P(Y|X),是不是想到了朴素贝叶斯?我们对训练数据进行分析,获得先验概率,从而得到P(Y|X)模型进行预测。最大熵也有数据和相同的终极大式子,那么

最大熵和朴素贝叶斯有什么联系?

可以说在本质上是非常密切的,但具体的联系和区别,我希望读者正式了解了最大熵以后,我们一起放在文章末尾讨论。

文章的最初,我们需要给出最大熵公式,如果对下面这个式子不太熟悉的,可以查看《统计学习方法|决策树原理剖析及实现》一节 :

 

H(P)表现为事件P的熵变,也称为事件P的不确定性。对于最大熵模型而言,我们一直记得那个终极式子P(Y|X),如果放到骰子例子中,P(Y=1|X)表示扔了一个骰子,1朝上的概率,P(Y=2|X)表示2朝上的概率。我们之前怎么说的?我们只知道骰子是六面,至于其他情况我们一概不知,在不知道的情况下,我们就不能瞎做推测,换言之,我们就是要让这个P(Y|X)的不确定性程度最高。

不确定性程度最高?

可能有些读者这么一绕没回过神来,如果我告诉你点数1朝上的概率是1/2,你是不是对这个概率分布开始有了一点认识,那么会将剩下的面认为是1/10(注:1朝上概率是1/2,那么1背面的概率一定会降低,但我们没有考虑这件事情,事实上我们不应该添加任何的主观假设,因为实际上有些骰子1的背面是3,也有些是4,任何主观假设都有可能造成偏见产生错误的推断)。你推断剩余面是1/10的概率是因为我告诉了你信息,你对这个骰子开始有了一些确定的认识,也就是信息的不确定性程度——熵减少了。但事实上我没有告诉你这一信息,你对P(Y|X)的分布是很不确定的,那么我们能不能用使用下面这个式子,让H(P)最大化来表示我们对P(Y|X)的极大不确定呢?事实上当我们令下式中H(P)最大时,P(Y|X)正好等于1/6。也就是说,如果我们要让熵最大,P(Y|X)必须为1/6。换句话说,只有我们把骰子的每一面概率当做1/6时,才能充分表示我们对骰子每一面的概率分布的极度不确定性,骰子的概率这件事情的混乱度最高,同时也传递了一个信息:我们对这件事情没有添加任何主观的假设,对任何未知的东西没有偏见和主观假象,这是最保险、最安全的做法。

 

 

 

 

上式可以理解成x=托马斯全旋法扔骰子,y为各类结果的概率。那么我们并不是说x只有托马斯全旋法这一种,很可能还有王思聪吃热狗法扔骰子、边骂作者啥也不懂边扔骰子法等等等等….我们要使得在所有情况下熵都是最大,所以引出下式:

 

 

加了一个P_(x)是什么意思?就是说我在求每种x对应的y时,要考虑到x出现的概率进行相乘。换句话说,也可以认为是当前P(y|x)的权值。因为我们要保证在所有情况下总的熵是最大的,所以每个部分单独的熵乘以权值后再相加,就是整个事件的熵。上式这个模型,其实就是最大熵模型,我们要让P(y|x)的熵最大,等价于让H(p)最大,我们好像获得了一个新的终极大式子。目前来看,只要能够让H(P)为最大值,那就能获得模型啦。

emmm….不过里面的各个子式好像也不是那么好求的。所以我们暂时先忘记上面的式子,好好回想一下咱们手里有哪些信息,好好整理一下。

 

我认为我手里应该会有一个训练集,包含所有的样本以及样本对应的标签(训练集都没有的话啥模型也训练不了啊,那可太惨了)。

那么我们首先可以得到训练集中x,y的联合概率分布以及x的边缘分布,数据都在我们手上,这两个很简单就能求出来对吧。

 

 

 

在上式中v表示频数,第一个式子的分子也就表示(x, y)组合出现的次数,处于总数N即为(x, y)组合出现的概率,也就是P(X, Y)。第二个式子同理,求得P(X=x)的概率。那么现在P(X, Y)和(X)我们知道了,有什么用呢?emmm….暂时好像还没什么头绪,我们看下式,书中说这是特征函数f(x, y)关于经验分布p(x, y)的期望值,f(x, y)在x和y满足某一事实时结果为1,反之输出0。比如说训练集中某一样本记录是托马斯回旋法扔骰子,结果是5朝上。那么f(x=托马斯回旋法扔骰子|Y=5)=1。其实更简单的理解,可以认为如果(x, y)出现在训练集中,那么它就是1,因为一旦出现在训练集中,说明(x, y)对已经符合了某一样本的事实了。P上面的短横表示这个模型是基于训练集得出来的,只能被称为经验分布。

 

 

于此同时我们可以直接将P_(x, y)拆开来,拆成下式:

 

 

 

 

那么我们将P_(x, y)转换成了P_(x)*P(y|x),发现什么了没有?里面有P_(y|x),这个和我们要得到的最终大式子P(Y|X)很像,那么如果我们只是将其替换进去变成下面这样会有什么结果呢?

 

我们发现,如果我们的预测模型P(y|x)是正确的话,那P(y|x)应该等于p_(y, x),也就是说

 

这两个应该相等!或者说:

 

 

那这个有啥用?emmm….我们将我们目前知道的信息整理出来了上式,老实说,目前来看没啥用…..

咦?等等,我们换种思维想一下,我们知道如果做出来的模型足够正确的画,那应该上面的两个式子相等,那….这算不算一种约束?约束我一定要得到正确的模型?还记得我们要最大化的那个熵的式子吗?这个代表了我们要求得的模型。

 

 

那加上我们刚刚整理出来的约束:

 

 

 

 

 

 

是不是看起来有点像那么回事了,第三个式子也是一个约束,概率和一定为1嘛。在机器学习中不止一次见到了上图的式子形式,但通常是求min而非上图的max,那么我们再转换一下,求最大取个负号就是求最小了嘛。

 

 

 

 

 

 

好了,现在是不是知道该怎么做了?转换成拉格朗日乘子法直接求最小值H(P)呀。

 

 

 

 

 

 

 

 

但是我们并不能直接求到H(P)的最优值,在使用拉格朗日乘子法后引入了变量w,对于w,我们需要先求得L(P,W)的最大值,此时将P看成了一个常数。之后再对P操作求得-H(P)的最小值。也就是说:

 

 

一定要注意min和max下方的参数是不一样的,因为L是考虑到约束函数从生成的目标桉树,在求解最优的参数w以此使得我们的模型能够符合约束时P只是一个定值,此时需要求得最优的w以此保证约束条件对模型的约束性。当求得w后P满足了约束性,此时再求最优的P以此使得-H(p)最小化,也就是H(p)最大化以此得到最优模型。

与此同时,我们可以将上式转换为下式,因为L是P的凸函数,因此两式等价。

 

 

所以我们先求内部的极小化问题,即min L(P, w),因为L是此时是关于P的凸函数,w是一个定值,所以可以直接对P求导求得最小值。H(P)中的P是什么?P就是P(y|x)啊(前文在H(P)的式子中可以显然看到,事实上我们全文也一直在为P(y|x)进行准备)。因为L是P的凸函数,那么直接求导并且令求导结果为0就好啦,从而得到下式。

 

 

 

我们再整理一下:

 

 

 

 

 

上式没有什么区别,只是把分母变成了Zw,从而看起来简化不少。那么我们已经得到最优的P(y|x)了,将其代入L式中再求最优的w不就好了嘛,这个步骤也就是求个导,书上不详细展开了,因为是重复性工作,因此在这里我觉得也没有必要再讲求导式写出来,读者可以自己写一下。

因此综上,只要能够得到最优的Pw(y|x),随后再求得最优的w,全部工作也就迎刃而解了。最主要的是Pw(y|x)问题。对于如何求解最优的Pw(y|x),这里介绍改进的迭代尺度法(IIS)

 

改进的迭代尺度法(IIS)

我们已知目前的解决目标是:

 

 

 

 

 

所有的式子连乘取对数转换为对数似然函数为:

(在《统计学习方法》6.2.4节中说明了为何上文中关于w进行拉格朗日极大化等价于对数似然函数最大化,但由于只是一些简单的公式,这里不再展开。唯一难点在于书中给出的对数似然形式,可以查看该链接学习:最大熵模型中的对数似然函数的解释

 

 

数学家说我们只要求对数似然函数L(w)的极大值,就一定能够得到最优解。确实是这样的,但是具体怎么求呢?我们找极大值一般使用的是求导得到导数为0的点,我没有试过上式对w进行求导能否得到解(应该是不行的,如果可以的话,也就不需要IIS法了)。IIS法的核心思想是每次增加一个量δ,使得L(w+δ)>L(w),以此不断提高L的值,直到达到极大值。我们将L(w+δ) – L(w)写出来:

 

 

 

那么我们要干啥?当然得保证差值大于0呀。只有每次都比前一次的大才能保证不停地往上走。除此之外呢?我们还希望每次增加的量都尽可能大一点,这样才能以最快的速度收敛到极大值。利用不等式-lnα≥1-α我们可以得到下式:

 

 

 

 

 

 

 

 

 

A也可以称之为该变量的下届,就是对于任意的δ,它们的差值一定是大于等于A(δ|w)的。我们应该求一个δ使得尽量增大A(δ|w),这样能保证我们每步都会走得尽可能跨度大,程序耗时也就越少。此外由于直接求A的最大值并不太好求,因此再将A松开一点,建立一个不太紧的方便求极大值的下界B,书中有详细公式推导,并不太难,因此不再讲解。给出最终的式子:

 

 

 

 

 

 

 

对B求导并令其为0,得到最优的δ,从而每次迭代过程中w=w+δ,以此提高L值,得到最优解。这就是算法的全过程。

 

补充(极其重要——针对mnist数据集):

mnist是一个手写数字的数据集,里面有很多的样本,每个样本是28*28的图片,展开是一个784维的向量,代表一个数字,同时给了一个标签告诉你标签时多少。

好的,那么我的问题是:我们全文一直在说P(y|x),可是你真正考虑过在mnist中里面的y和x都是什么吗?

有人说x是每一个样本,y是标签。看起来确实是这样的,那f(x, y)是什么?就是样本x和y是否成对出现过吗?那我如果出现一个训练集中没出现的样本,就变成0了吗?我们都知道手写每个人都不一样,不可能写出来的样本在训练集中一定有一个一模一样的,那么它就变成0了吗?

接下来我要说的事情,只针对Mnist数据集,作者进入机器学习时间尚短,不清楚书中是否是为了提高公式在所有情况下的泛化性而简略了写,因此对于其他方向的使用,保留一些意见。

事实上,对于Mnist而言所有的x都不是样本x!它表示的是样本的特征。以f(x, y)为例,或许应该写成f0(x, y)、f1(x, y), …., fn(x, y),其中n为特征的数目。再详细下去以f0(x, y)为例,f0(x = 0, y = 0)表示是否存在这样一个事实:训练集所有样本的特征中有样本的第0个特征值为0,与此同时y为0。是不是有点理解了?

我们再以P(y|x)为例,例如P1(y=0|x=0)表示的是在训练集中所有样本中,第一个特征为0的样本其中y为0的概率。

我当时也是写程序的时候卡在这里总是不知道该怎么写,直到看了别人的实现以后才发现了这个没有提到的地方,关于具体实现可以查看我的代码,有详细注释。

最后还是需要补充一点,我并不太清楚在别的地方(例如NLP)中最大熵是如何应用的,也不清楚上面提到的这个小tip是否只是针对mnist而言做出的一个特例修改,因此保留对于这件事情的意见。

 

关于最大熵和朴素贝叶斯的联系

两者都使用了P(y|x),区别在于朴素贝叶斯使用先验概率求解,最大熵利用最大化条件熵求解。

朴素贝叶斯倾向于从基本信息中提取新的信息,而最大熵将提取信息的程度进行了量化(就好像强迫自己获得概率一定是要均匀分布一样)。

 

贴代码:(建议去本文最上方的github链接下载,有书中所有算法的实现以及详细注释)

# coding=utf-8
# Author:Dodo
# Date:2018-11-30
# Email:lvtengchao@pku.edu.cn
# Blog:www.pkudodo.com

'''
数据集:Mnist
训练集数量:60000(实际使用:20000)
测试集数量:10000
------------------------------
运行结果:
    正确率:96.9%
    运行时长:8.8h

备注:对于mnist而言,李航的统计学习方法中有一些关键细节没有阐述,
建议先阅读我的个人博客,其中有详细阐述。阅读结束后再看该程序。
Blog:www.pkudodo.com
'''

import time
import numpy as np
from collections import defaultdict

def loadData(fileName):
    '''
    加载Mnist数据集
    :param fileName:要加载的数据集路径
    :return: list形式的数据集及标记
    '''
    # 存放数据及标记的list
    dataList = []; labelList = []
    # 打开文件
    fr = open(fileName, 'r')
    # 将文件按行读取
    for line in fr.readlines():
        # 对每一行数据按切割福','进行切割,返回字段列表
        curLine = line.strip().split(',')
        #十分类,list中放置标签
        if int(curLine[0]) == 0:
            labelList.append(1)
        else:
            labelList.append(0)
        #二值化
        dataList.append([int(int(num) > 128) for num in curLine[1:]])

    #返回data和label
    return dataList, labelList


class maxEnt:
    '''
    最大熵类
    '''
    def __init__(self, trainDataList, trainLabelList, testDataList, testLabelList):
        '''
        各参数初始化
        '''
        self.trainDataList = trainDataList          #训练数据集
        self.trainLabelList = trainLabelList        #训练标签集
        self.testDataList = testDataList            #测试数据集
        self.testLabelList = testLabelList          #测试标签集
        self.featureNum = len(trainDataList[0])     #特征数量

        self.N = len(trainDataList)                 #总训练集长度
        self.n = 0                                  #训练集中(xi,y)对数量
        self.M = 10000                              #
        self.fixy = self.calc_fixy()                #所有(x, y)对出现的次数
        self.w = [0] * self.n                       #Pw(y|x)中的w
        self.xy2idDict, self.id2xyDict = self.createSearchDict()        #(x, y)->id和id->(x, y)的搜索字典
        self.Ep_xy = self.calcEp_xy()               #Ep_xy期望值

    def calcEpxy(self):
        '''
        计算特征函数f(x, y)关于模型P(Y|X)与经验分布P_(X, Y)的期望值(P后带下划线“_”表示P上方的横线
        程序中部分下划线表示“|”,部分表示上方横线,请根据具体公式自行判断,)
        即“6.2.2 最大熵模型的定义”中第二个期望(83页最上方的期望)
        :return:
        '''
        #初始化期望存放列表,对于每一个xy对都有一个期望
        #这里的x是单个的特征,不是一个样本的全部特征。例如x={x1,x2,x3.....,xk},实际上是(x1,y),(x2,y),。。。
        #但是在存放过程中需要将不同特诊的分开存放,李航的书可能是为了公式的泛化性高一点,所以没有对这部分提及
        #具体可以看我的博客,里面有详细介绍  www.pkudodo.com
        Epxy = [0] * self.n
        #对于每一个样本进行遍历
        for i in range(self.N):
            #初始化公式中的P(y|x)列表
            Pwxy = [0] * 2
            #计算P(y = 0 } X)
            #注:程序中X表示是一个样本的全部特征,x表示单个特征,这里是全部特征的一个样本
            Pwxy[0] = self.calcPwy_x(self.trainDataList[i], 0)
            #计算P(y = 1 } X)
            Pwxy[1] = self.calcPwy_x(self.trainDataList[i], 1)

            for feature in range(self.featureNum):
                for y in range(2):
                    if (self.trainDataList[i][feature], y) in self.fixy[feature]:
                        id = self.xy2idDict[feature][(self.trainDataList[i][feature], y)]
                        Epxy[id] += (1 / self.N) * Pwxy[y]
        return Epxy

    def calcEp_xy(self):
        '''
        计算特征函数f(x, y)关于经验分布P_(x, y)的期望值(下划线表示P上方的横线,
        同理Ep_xy中的“_”也表示p上方的横线)
        即“6.2.2 最大熵的定义”中第一个期望(82页最下方那个式子)
        :return: 计算得到的Ep_xy
        '''
        #初始化Ep_xy列表,长度为n
        Ep_xy = [0] * self.n

        #遍历每一个特征
        for feature in range(self.featureNum):
            #遍历每个特征中的(x, y)对
            for (x, y) in self.fixy[feature]:
                #获得其id
                id = self.xy2idDict[feature][(x, y)]
                #将计算得到的Ep_xy写入对应的位置中
                #fixy中存放所有对在训练集中出现过的次数,处于训练集总长度N就是概率了
                Ep_xy[id] = self.fixy[feature][(x, y)] / self.N

        #返回期望
        return Ep_xy

    def createSearchDict(self):
        '''
        创建查询字典
        xy2idDict:通过(x,y)对找到其id,所有出现过的xy对都有一个id
        id2xyDict:通过id找到对应的(x,y)对
        '''
        #设置xy搜多id字典
        #这里的x指的是单个的特征,而不是某个样本,因此将特征存入字典时也需要存入这是第几个特征
        #这一信息,这是为了后续的方便,否则会乱套。
        #比如说一个样本X = (0, 1, 1) label =(1)
        #生成的标签对有(0, 1), (1, 1), (1, 1),三个(x,y)对并不能判断属于哪个特征的,后续就没法往下写
        #不可能通过(1, 1)就能找到对应的id,因为对于(1, 1),字典中有多重映射
        #所以在生成字典的时总共生成了特征数个字典,例如在mnist中样本有784维特征,所以生成784个字典,属于
        #不同特征的xy存入不同特征内的字典中,使其不会混淆
        xy2idDict = [{} for i in range(self.featureNum)]
        #初始化id到xy对的字典。因为id与(x,y)的指向是唯一的,所以可以使用一个字典
        id2xyDict = {}

        #设置缩影,其实就是最后的id
        index = 0
        #对特征进行遍历
        for feature in range(self.featureNum):
            #对出现过的每一个(x, y)对进行遍历
            #fixy:内部存放特征数目个字典,对于遍历的每一个特征,单独读取对应字典内的(x, y)对
            for (x, y) in self.fixy[feature]:
                #将该(x, y)对存入字典中,要注意存入时通过[feature]指定了存入哪个特征内部的字典
                #同时将index作为该对的id号
                xy2idDict[feature][(x, y)] = index
                #同时在id->xy字典中写入id号,val为(x, y)对
                id2xyDict[index] = (x, y)
                #id加一
                index += 1

        #返回创建的两个字典
        return xy2idDict, id2xyDict


    def calc_fixy(self):
        '''
        计算(x, y)在训练集中出现过的次数
        :return:
        '''
        #建立特征数目个字典,属于不同特征的(x, y)对存入不同的字典中,保证不被混淆
        fixyDict = [defaultdict(int) for i in range(self.featureNum)]
        #遍历训练集中所有样本
        for i in range(len(self.trainDataList)):
            #遍历样本中所有特征
            for j in range(self.featureNum):
                #将出现过的(x, y)对放入字典中并计数值加1
                fixyDict[j][(self.trainDataList[i][j], self.trainLabelList[i])] += 1
        #对整个大字典进行计数,判断去重后还有多少(x, y)对,写入n
        for i in fixyDict:
            self.n += len(i)
        #返回大字典
        return fixyDict


    def calcPwy_x(self, X, y):
        '''
        计算“6.23 最大熵模型的学习” 式6.22
        :param X: 要计算的样本X(一个包含全部特征的样本)
        :param y: 该样本的标签
        :return: 计算得到的Pw(Y|X)
        '''
        #分子
        numerator = 0
        #分母
        Z = 0
        #对每个特征进行遍历
        for i in range(self.featureNum):
            #如果该(xi,y)对在训练集中出现过
            if (X[i], y) in self.xy2idDict[i]:
                #在xy->id字典中指定当前特征i,以及(x, y)对:(X[i], y),读取其id
                index = self.xy2idDict[i][(X[i], y)]
                #分子是wi和fi(x,y)的连乘再求和,最后指数
                #由于当(x, y)存在时fi(x,y)为1,因为xy对肯定存在,所以直接就是1
                #对于分子来说,就是n个wi累加,最后再指数就可以了
                #因为有n个w,所以通过id将w与xy绑定,前文的两个搜索字典中的id就是用在这里
                numerator += self.w[index]
            #同时计算其他一种标签y时候的分子,下面的z并不是全部的分母,再加上上式的分子以后
            #才是完整的分母,即z = z + numerator
            if (X[i], 1-y) in self.xy2idDict[i]:
                #原理与上式相同
                index = self.xy2idDict[i][(X[i], 1-y)]
                Z += self.w[index]
        #计算分子的指数
        numerator = np.exp(numerator)
        #计算分母的z
        Z = np.exp(Z) + numerator
        #返回Pw(y|x)
        return numerator / Z


    def maxEntropyTrain(self, iter = 500):
        #设置迭代次数寻找最优解
        for i in range(iter):
            #单次迭代起始时间点
            iterStart = time.time()

            #计算“6.2.3 最大熵模型的学习”中的第二个期望(83页最上方哪个)
            Epxy = self.calcEpxy()

            #使用的是IIS,所以设置sigma列表
            sigmaList = [0] * self.n
            #对于所有的n进行一次遍历
            for j in range(self.n):
                #依据“6.3.1 改进的迭代尺度法” 式6.34计算
                sigmaList[j] = (1 / self.M) * np.log(self.Ep_xy[j] / Epxy[j])

            #按照算法6.1步骤二中的(b)更新w
            self.w = [self.w[i] + sigmaList[i] for i in range(self.n)]

            #单次迭代结束
            iterEnd = time.time()
            #打印运行时长信息
            print('iter:%d:%d, time:%d'%(i, iter, iterStart - iterEnd))

    def predict(self, X):
        '''
        预测标签
        :param X:要预测的样本
        :return: 预测值
        '''
        #因为y只有0和1,所有建立两个长度的概率列表
        result = [0] * 2
        #循环计算两个概率
        for i in range(2):
            #计算样本x的标签为i的概率
            result[i] = self.calcPwy_x(X, i)
        #返回标签
        #max(result):找到result中最大的那个概率值
        #result.index(max(result)):通过最大的那个概率值再找到其索引,索引是0就返回0,1就返回1
        return result.index(max(result))

    def test(self):
        '''
        对测试集进行测试
        :return:
        '''
        #错误值计数
        errorCnt = 0
        #对测试集中所有样本进行遍历
        for i in range(len(self.testDataList)):
            #预测该样本对应的标签
            result = self.predict(self.testDataList[i])
            #如果错误,计数值加1
            if result != self.testLabelList[i]:   errorCnt += 1
        #返回准确率
        return 1 - errorCnt / len(self.testDataList)

if __name__ == '__main__':
    start = time.time()

    # 获取训练集及标签
    print('start read transSet')
    trainData, trainLabel = loadData('../Mnist/mnist_train.csv')

    # 获取测试集及标签
    print('start read testSet')
    testData, testLabel = loadData('../Mnist/mnist_test.csv')

    #初始化最大熵类
    maxEnt = maxEnt(trainData[:20000], trainLabel[:20000], testData, testLabel)

    #开始训练
    print('start to train')
    maxEnt.maxEntropyTrain()

    #开始测试
    print('start to test')
    accuracy = maxEnt.test()
    print('the accuracy is:', accuracy)

    # 打印时间
    print('time span:', time.time() - start)

 

Dodo

9条评论

匿名 发布于9:33 上午 - 12月 2, 2019

确实比那些千篇一律的博客强的多,受益匪浅

匿名 发布于11:19 下午 - 7月 6, 2019

博主,你的博客非常不错,非常受益。对于其中的特征函数f(x,y),应该这么理解,那个就是从数据集里面获取某种约束条件,例如,对于手写数字1的X特征1>128的概率在80%以上这样一个约束。在本例中你全部使用x的特征,那么和决策树的思路是很相似的。决策树是尽可能拟合所有数据里面的信息,然后剪枝去掉过拟合。最大熵模型是先来个均匀分布,然后增加数据集里面的特征约束更新分布。

匿名 发布于9:27 下午 - 6月 18, 2019

请问一下,那个M,书上是说为常数时才可以直接用那个公式吧。但应该不是常数吧,为什么代码里直接用了
sigmaList[j] = (1 / self.M) * np.log(self.Ep_xy[j] / Epxy[j])

    匿名 发布于10:06 下午 - 6月 18, 2019

    。。。仔细看了下,这里就是常数。按照这个特征函数的定义方法,不管是什么x,y对,求和都是28*28… 请问博主我的理解对不对。

匿名 发布于5:23 下午 - 4月 10, 2019

博主研几了?? 拿到offer了?

    Dodo 发布于11:28 下午 - 4月 15, 2019

    研一 现在在微软小冰实习

匿名 发布于10:33 下午 - 1月 14, 2019

不错,继续追踪博主

匿名 发布于9:15 上午 - 12月 9, 2018

17学长前来打call。写的真不错

    Dodo 发布于2:44 下午 - 12月 9, 2018

    哈哈感谢学长 倍受鼓励