最大熵用于文本分类

https://blog.csdn.net/golden1314521/article/details/45576089

https://github.com/doubleEN/Maxent一个实例

原始数据集和完整的代码见 http://download.csdn.net/detail/u012176591/8675665

一个相关的论文《使用最大熵模型进行中文文本分类》

1.改进的迭代尺度算法IIS:
输入:特征函数f1,f2,⋯,fnf1,f2,⋯,fn;经验分布P~(X,Y)P~(X,Y),模型Pw(y|x)Pw(y|x)
输出:最优参数值wiwi;最优模型PwPw
对所有i∈{1,2,⋯,n}i∈{1,2,⋯,n},取初值wi=0wi=0
对每一i∈{1,2,⋯,n}i∈{1,2,⋯,n}:

(a)令δiδi是方程 
∑x,yP~(x)P(y|x)fi(x,y)exp(δif#(x,y))=EP~(fi)∑x,yP~(x)P(y|x)fi(x,y)exp(δif#(x,y))=EP~(fi) 
的解,这里 
f#(x,y)=∑ni=1fi(x,y)f#(x,y)=∑i=1nfi(x,y)
(b) 更新wiwi值:wi←wi+δiwi←wi+δi
如果不是所有的wiwi都收敛,重复步骤2.

这一算法关键的一步是2(a),即求方程中的δiδi。如果f#(x,y)f#(x,y)是常数,即对任何x,yx,y,有f#(x,y)=Mf#(x,y)=M,那么δi=1MlogEP~(fi)EP(fi)δi=1MlogEP~(fi)EP(fi)
2.怎么用最大熵做文本分类
代码中的prob就是Pw(y|x)Pw(y|x),代表文本x属于类别的概率,prob是一个文本数*类别的矩阵。

Pw(y|x)=1Zw(x)exp(∑ni=1wifi(x,y))Pw(y|x)=1Zw(x)exp(∑i=1nwifi(x,y))
其中Zw(x)=∑yexp(∑ni=1wifi(x,y))Zw(x)=∑yexp(∑i=1nwifi(x,y))
EP_prior就是EP~(f)=∑x,yP~(x,y)f(x,y)EP~(f)=∑x,yP~(x,y)f(x,y),是特征函数f(x,y)f(x,y)关于先验分布P~(x,y)P~(x,y)的期望值。

EP_post就是EP(f)=∑x,yP~(x)P(y|x)f(x,y)EP(f)=∑x,yP~(x)P(y|x)f(x,y),是特征函数f(x,y)f(x,y)关于后验分布P(x,y)=P~(x)P(y|x)P(x,y)=P~(x)P(y|x)的期望值,其中P(y|x)P(y|x)是我们的模型。

最大熵模型P(y|x)P(y|x)要求EP~(f)=EP(f)EP~(f)=EP(f),这意味着wordNum∗ctgyNumwordNum∗ctgyNum个约束条件,每个特征有一个约束条件。

预测文本类别是所用的公式Pw(y|x)=exp(∑ni=1wifi(x,y))∑yexp(∑ni=1wifi(x,y))Pw(y|x)=exp(∑i=1nwifi(x,y))∑yexp(∑i=1nwifi(x,y)),对于一个文本xx,Pw(y|x)Pw(y|x)最大的类yy就是判断的类。可以发现分母部分∑yexp(∑ni=1wifi(x,y))∑yexp(∑i=1nwifi(x,y))与类别的判断无关,所以只要计算分子部分exp(∑ni=1wifi(x,y)exp(∑i=1nwifi(x,y)即可。

对于词ww和类别CC,它的特征函数
fw,C′(C,t)={#(t,w)#(t)0C=C′C≠C′
fw,C′(C,t)={#(t,w)#(t)C=C′0C≠C′

其中#(t,w)#(t)#(t,w)#(t)表示词ww在文档tt中出现的先验概率。
假设所有文本的单词的不重复集合的元素数目是wordNumwordNum,类别的个数是ctgyNumctgyNum,那么特征一共有wordNum∗ctgyNumwordNum∗ctgyNum个,,组成一个wordNum∗ctgyNumwordNum∗ctgyNum的特征矩阵。由特征函数公式,可知对每个文本tt,其单词(包括在该文本中出现和未出现的)与类别CC的数目在特征函数的作用下形成一个特征矩阵。易知该矩阵在此文本非所属的类别所在的列元素全为0,该文本中不出现的单词所在的行也全为0,非0元素的和为1。在程序中对每个文本只记录其非00特征及其特征值(texts中的元素是字典,就是反映的这种思路,字典中隐含的是该文本所属的类别CC)。EP~(f)EP~(f)和EP(f)EP(f)与特征矩阵的维度大小相同,而且EP~(f)EP~(f)本质上是对所有文本的特征矩阵的求和(本例中所有的文本的出现概率视为均等,故EP~(f)EP~(f)和EP(f)EP(f)表达式中的P~(x)P~(x)相同,忽略其不影响)。

本例中EP~(f)=∑x,yP~(x,y)f(x,y)=∑xf(x,y)EP~(f)=∑x,yP~(x,y)f(x,y)=∑xf(x,y),因为只有yy等于xx的类别时P~(x,y)P~(x,y)为1textNum1textNum,即单个文本的出现概率,其余都为00,这就是为什么说EP~(f)EP~(f)本质上是对所有文本的特征矩阵的求和。

P~(x)P~(x)就是单个文本的概率,这里每个文本都不相同且仅属于一个类别,故P~(x)=1textNumP~(x)=1textNum
故由EP~(f)=EP(f)EP~(f)=EP(f)可推知∑xf(x,y)=∑x,yP(y|x)f(x,y)∑xf(x,y)=∑x,yP(y|x)f(x,y),这是在特定应用场景下的模型约束,下面的分析中就是这个模型。

更新量δi=1MlogEP~(fi)EP(fi)δi=1MlogEP~(fi)EP(fi),其中M=max∑ki=1f(xi)M=max∑i=1kf(xi),即所有文本中特征值的和的最大值,由fw,C′(C,t)fw,C′(C,t)的公式知这里MM的值肯定为1。

3.训练和测试效果
训练效果:
迭代0次后的模型效果: 
训练总文本个数:2741 总错误个数:1 总错误率:0.000364830353885 
迭代1次后的模型效果: 
训练总文本个数:2741 总错误个数:5 总错误率:0.00182415176943 
迭代2次后的模型效果: 
训练总文本个数:2741 总错误个数:7 总错误率:0.0025538124772 
迭代3次后的模型效果: 
训练总文本个数:2741 总错误个数:8 总错误率:0.00291864283108 
迭代4次后的模型效果: 
训练总文本个数:2741 总错误个数:7 总错误率:0.0025538124772 
迭代5次后的模型效果: 
训练总文本个数:2741 总错误个数:7 总错误率:0.0025538124772 
迭代6次后的模型效果: 
训练总文本个数:2741 总错误个数:7 总错误率:0.0025538124772 
迭代7次后的模型效果: 
训练总文本个数:2741 总错误个数:5 总错误率:0.00182415176943 
迭代8次后的模型效果: 
训练总文本个数:2741 总错误个数:4 总错误率:0.00145932141554 
迭代9次后的模型效果: 
训练总文本个数:2741 总错误个数:3 总错误率:0.00109449106166

测试效果:
迭代0次后的模型效果: 
测试总文本个数:709 总错误个数:129 总错误率:0.181946403385 
迭代1次后的模型效果: 
测试总文本个数:709 总错误个数:121 总错误率:0.170662905501 
迭代2次后的模型效果: 
测试总文本个数:709 总错误个数:118 总错误率:0.166431593794 
迭代3次后的模型效果: 
测试总文本个数:709 总错误个数:118 总错误率:0.166431593794 
迭代4次后的模型效果: 
测试总文本个数:709 总错误个数:118 总错误率:0.166431593794 
迭代5次后的模型效果: 
测试总文本个数:709 总错误个数:119 总错误率:0.16784203103 
迭代6次后的模型效果: 
测试总文本个数:709 总错误个数:120 总错误率:0.169252468265 
迭代7次后的模型效果: 
测试总文本个数:709 总错误个数:117 总错误率:0.165021156559 
迭代8次后的模型效果: 
测试总文本个数:709 总错误个数:117 总错误率:0.165021156559 
迭代9次后的模型效果: 
测试总文本个数:709 总错误个数:118 总错误率:0.166431593794

将历次迭代中的训练和测试错误率可视化如下图: 
 
发现在训练集上模型的效果很好,但是在测试集上效果差得太多,可能是模型有偏差吧。

特征权值的分布


从上图可以看出绝大部分的特征的权值都为0. 
为了更清晰地分析非0权值,做出下面这张图,可以看到特征的权值遵循正态分布。 


4.Python源码
已经有了足够的注释,如果你还看不懂,你或者去揣摩最大熵模型的原理,或者去学习Python编程。

def get_ctgy(fname):#根据文件名称获取类别的数字编号
        index = {'fi':0,'lo':1,'co':2,'ho':3,'ed':4,'te':5,
                 'ca':6,'ta':7,'sp':8,'he':9,'ar':10,'fu':11}
        return index[fname[:2]]

def updateWeight():

        #EP_post是 单词数*类别 的矩阵
        for i in range(wordNum):
            for j in range(ctgyNum):
                EP_post[i][j] = 0.0 #[[0.0 for x in range(ctgyNum)] for y in range(wordNum)]
        # prob是 文本数*类别 的矩阵,记录每个文本属于每个类别的概率
        cond_prob_textNum_ctgyNum = [[0.0 for x in range(ctgyNum)] for y in range(textNum)]
        #计算p(类别|文本)

        for i in range(textNum):#对每一个文本
                zw = 0.0  #归一化因子
                for j in range(ctgyNum):#对每一个类别
                        tmp = 0.0
                        #texts_list_dict每个元素对应一个文本,该元素的元素是单词序号:频率所组成的字典。
                        for (feature,feature_value) in texts_list_dict[i].items():
                            #v就是特征f(x,y),非二值函数,而是实数函数,
                            #k是某文本中的单词,v是该单词的次数除以该文本不重复单词的个数。
                                #feature_weight是 单词数*类别 的矩阵,与EP_prior相同
                                tmp+=feature_weight[feature][j]*feature_value #feature_weight是最终要求的模型参数,其元素与特征一一对应,即一个特征对应一个权值
                        tmp = math.exp(tmp)
                        zw+=tmp #zw关于类别求和
                        cond_prob_textNum_ctgyNum[i][j]=tmp                        
                for j in range(ctgyNum):
                        cond_prob_textNum_ctgyNum[i][j]/=zw
        #上面的部分根据当前的feature_weight矩阵计算得到prob矩阵(文本数*类别的矩阵,每个元素表示文本属于某类别的概率),
        #下面的部分根据prob矩阵更新feature_weight矩阵。


        for x in range(textNum):
                ctgy = category[x] #根据文本序号获取类别序号
                for (feature,feature_value) in texts_list_dict[x].items(): #该文本中的单词和对应的频率
                        EP_post[feature][ctgy] += (cond_prob_textNum_ctgyNum[x][ctgy]*feature_value)#认p(x)的先验概率相同        
        #更新特征函数的权重w
        for i in range(wordNum):
                for j in range(ctgyNum):
                        if (EP_post[i][j]<1e-17) |  (EP_prior[i][j]<1e-17) :
                                continue                        

                        feature_weight[i][j] += math.log(EP_prior[i][j]/EP_post[i][j])        

def modelTest():
        testFiles = os.listdir('data\\test\\')
        errorCnt = 0
        totalCnt = 0

        #matrix是类别数*类别数的矩阵,存储评判结果
        matrix = [[0 for x in range(ctgyNum)] for y in range(ctgyNum)]
        for fname in testFiles: #对每个文件

                lines = open('data\\test\\'+fname)
                ctgy = get_ctgy(fname) #根据文件名的前两个字符给出类别的序号
                probEst = [0.0 for x in range(ctgyNum)]         #各类别的后验概率
                for line in lines: #该文件的每一行是一个单词和该单词在该文件中出现的频率
                        arr = line.split('\t')
                        if not words_dict.has_key(arr[0]) : 
                            continue        #测试集中的单词如果在训练集中没有出现则直接忽略
                        word_id,freq = words_dict[arr[0]],float(arr[1])
                        for index in range(ctgyNum):#对于每个类别
                            #feature_weight是单词数*类别墅的矩阵
                            probEst[index] += feature_weight[word_id][index]*freq
                ctgyEst = 0
                maxProb = -1
                for index in range(ctgyNum):
                        if probEst[index]>maxProb:
                            ctgyEst = index
                            maxProb = probEst[index]
                totalCnt+=1
                if ctgyEst!=ctgy: 
                    errorCnt+=1
                matrix[ctgy][ctgyEst]+=1
                lines.close()
        #print "%-5s" % ("类别"),
        #for i in range(ctgyNum):
        #    print "%-5s" % (ctgyName[i]),  
        #print '\n',
        #for i in range(ctgyNum):
        #    print "%-5s" % (ctgyName[i]), 
        #    for j in range(ctgyNum):
        #        print "%-5d" % (matrix[i][j]), 
        #    print '\n',
        print "测试总文本个数:"+str(totalCnt)+"  总错误个数:"+str(errorCnt)+"  总错误率:"+str(errorCnt/float(totalCnt))

def prepare():
        i = 0
        lines = open('data\\words.txt').readlines()
        #words_dict给出了每一个中文词及其对应的全局统一的序号,是字典类型,示例:{'\xd0\xde\xb5\xc0\xd4\xba': 0}
        for word in lines:
                word = word.strip()
                words_dict[word] = i
                i+=1
        #计算约束函数f的经验期望EP(f)
        files = os.listdir('data\\train\\') #train下面都是.txt文件
        index = 0
        for fname in files: #对训练数据集中的每个文本文件
                file_feature_dict = {}
                lines = open('data\\train\\'+fname)
                ctgy = get_ctgy(fname) #根据文件名的前两个汉字,也就是中文类别来确定类别的序号

                category[index] = ctgy #data/train/下每个文本对应的类别序号
                for line in lines: #每行内容:古迹  0.00980392156863
                        # line的第一个字符串是中文单词,第二个字符串是该单词的频率
                        arr = line.split('\t')
                        #获取单词的序号和频率
                        word_id,freq= words_dict[arr[0]],float(arr[1])

                        file_feature_dict[word_id] = freq
                        #EP_prior是单词数*类别的矩阵
                        EP_prior[word_id][ctgy]+=freq
                texts_list_dict[index] = file_feature_dict
                index+=1
                lines.close()
def train():
        for loop in range(4):
            print "迭代%d次后的模型效果:" % loop
            updateWeight()
            modelTest()


textNum = 2741  # data/train/下的文件的个数
wordNum = 44120 #data/words.txt的单词数,也是行数
ctgyNum = 12


#feature_weight是单词数*类别墅的矩阵
feature_weight = np.zeros((wordNum,ctgyNum))#[[0 for x in range(ctgyNum)] for y in range(wordNum)]

ctgyName = ['财经','地域','电脑','房产','教育','科技','汽车','人才','体育','卫生','艺术','娱乐']
words_dict = {}

# EP_prior是个12(类)* 44197(所有类的单词数)的矩阵,存储对应的频率
EP_prior = np.zeros((wordNum,ctgyNum))
EP_post = np.zeros((wordNum,ctgyNum))
#print np.shape(EP_prior)
texts_list_dict = [0]*textNum #所有的训练文本 
category = [0]*textNum        #每个文本对应的类别

print "初始化:......"
prepare()
print "初始化完毕,进行权重训练....."
train()

源码中出现的文件数据格式的解析


其中words.txt 里是所有在训练数据集中出现的单词的不重复集合,每行一个单词,如下:

test和train文件夹中分别是测试数据集和训练数据集,这两个文件夹下的文件格式和内容格式是没有区别的。每个文件对应一个文本,文件名的前两个字符表示对应的文本所属的类别,文件内容是该文本中所有出现的单词在该文本中的概率,如下: 


作hist图的代码:

weight = feature_weight.reshape((1,44120*12))[0]
#weight = np.log10(weight+1)
plt.hist(weight, 100)
plt.title(u'特征的权值分布图',{'fontname':'STFangsong','fontsize':18})
plt.xlabel(u'特征的权值',{'fontname':'STFangsong','fontsize':18})
plt.ylabel(u'个数',{'fontname':'STFangsong','fontsize':18})


作错误率的代码:

train_precision = [0.000364830353885,0.00182415176943,0.0025538124772,0.00291864283108,0.0025538124772,
                  0.0025538124772,0.0025538124772,0.00182415176943,0.00145932141554,0.00109449106166]

test_precision = [0.181946403385,0.170662905501,0.166431593794,0.166431593794,0.166431593794,0.16784203103,
                 0.169252468265,0.165021156559,0.165021156559,0.166431593794]
iteration = range(1,11)

fig,ax = plt.subplots(nrows=1,ncols=1)
ax.plot(iteration,train_precision,'-og',label=u'训练误差')
ax.plot(iteration,test_precision,'-sr',label=u'测试误差')


ax.legend(prop={'family':'SimHei','size':15})

ax.set_xlabel(u'迭代次数',{'fontname':'STFangsong','fontsize':18})

ax.set_ylabel(u'误判比例',{'fontname':'STFangsong','fontsize':18})
ax.set_title(u'训练和测试误差曲线',{'fontname':'STFangsong','fontsize':18})

进一步阅读
自然语言处理的最大熵模型 
http://icl.pku.edu.cn/ICLseminars/2003spring/%E6%9C%80%E5%A4%A7%E7%86%B5%E6%A8%A1%E5%9E%8B.pdf
MaxEntModel.ppt 
http://read.pudn.com/downloads131/ebook/557682/MaxEntModel%20.ppt 
里面有对称硬币问题(表达能力、不确定度、为什么取对数、一次取三个最小值的霍夫曼编码),条件熵的公式定义的理解,马鞍点问题(详细见鞍点 
http://zh.wikipedia.org/wiki/%E9%9E%8D%E9%BB%9E ,黑塞矩阵 http://zh.wikipedia.org/wiki/%E6%B5%B7%E6%A3%AE%E7%9F%A9%E9%98%B5 
),极大似然函数由联合熵变成条件熵的推导过程
最大熵的Java实现 
http://www.hankcs.com/nlp/maximum-entropy-java-implementation.html
最大熵文本分类 算法实现(有代码) 
http://www.blogbus.com/myjuno-logs/240428649.html 
数据集 http://www.searchforum.org.cn/tansongbo/corpus.htm
最大熵模型的简单实现(有两份代码,其中一个是另一个的简版) 
http://yjliu.net/blog/2012/07/22/easy-implementation-on-maxent.html
张乐:Maximum Entropy Modeling 
http://homepages.inf.ed.ac.uk/lzhang10/maxent.html
Jar包(没有源码)的maxent: Maxent software for species habitat modeling 
https://www.cs.princeton.edu/~schapire/maxent/
A simple C++ library for maximum entropy classification 
http://www.logos.ic.i.u-tokyo.ac.jp/~tsuruoka/maxent/
最大熵模型的图模型(factor graph): 
概率图模型-贝叶斯-最大熵-隐马-最大熵马-马随机场
 

猜你喜欢

转载自blog.csdn.net/QFire/article/details/86540462