最大熵用于文本分类

最大熵用于文本分类_第1张图片

作者:金良([email protected]) csdn博客: http://blog.csdn.net/u012176591

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

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

1.改进的迭代尺度算法IIS:

输入:特征函数 f1,f2,,fn ;经验分布 P~(X,Y) ,模型 Pw(y|x)

输出:最优参数值 wi ;最优模型 Pw

  1. 对所有 i{1,2,,n} ,取初值 wi=0
  2. 对每一 i{1,2,,n}

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

这一算法关键的一步是2(a),即求方程中的 δi 。如果 f#(x,y) 是常数,即对任何 x,y ,有 f#(x,y)=M ,那么 δi=1MlogEP~(fi)EP(fi)

2.怎么用最大熵做文本分类

  • 代码中的prob就是 Pw(y|x) ,代表文本x属于类别的概率,prob是一个文本数*类别的矩阵。

    Pw(y|x)=1Zw(x)exp(ni=1wifi(x,y))

    其中 Zw(x)=yexp(ni=1wifi(x,y))

  • EP_prior就是 EP~(f)=x,yP~(x,y)f(x,y) ,是特征函数 f(x,y) 关于先验分布 P~(x,y) 的期望值。

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

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

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

  • 对于词 w 和类别 C ,它的特征函数

    fw,C(C,t)={#(t,w)#(t)0C=CCC

    其中 #(t,w)#(t) 表示词 w 在文档 t 中出现的先验概率。

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

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

    P~(x) 就是单个文本的概率,这里每个文本都不相同且仅属于一个类别,故 P~(x)=1textNum

    故由 EP~(f)=EP(f) 可推知 xf(x,y)=x,yP(y|x)f(x,y) ,这是在特定应用场景下的模型约束,下面的分析中就是这个模型。

  • 更新量 δi=1MlogEP~(fi)EP(fi) ,其中 M=maxki=1f(xi) ,即所有文本中特征值的和的最大值,由 fw,C(C,t) 的公式知这里 M 的值肯定为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

将历次迭代中的训练和测试错误率可视化如下图:
最大熵用于文本分类_第2张图片
发现在训练集上模型的效果很好,但是在测试集上效果差得太多,可能是模型有偏差吧。

特征权值的分布

最大熵用于文本分类_第3张图片

从上图可以看出绝大部分的特征的权值都为0.
为了更清晰地分析非0权值,做出下面这张图,可以看到特征的权值遵循正态分布。
最大熵用于文本分类_第4张图片

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):
    概率图模型-贝叶斯-最大熵-隐马-最大熵马-马随机场

你可能感兴趣的:(文本分类,最大熵)