Datawhale《深度学习-NLP》Task4- 传统机器学习

7. 参考 朴素贝叶斯1:sklearn:朴素贝叶斯(naïve beyes) - 专注计算机体系结构 - CSDN博客

LDA数学八卦 lda2:用LDA处理文本(Python) - 专注计算机体系结构 - CSDN博客

合并特征:Python:合并两个numpy矩阵 - 专注计算机体系结构 - CSDN博客

 

1. 朴素贝叶斯

原理

利用朴素贝叶斯模型进行文本分类

数据集说明:一条记录为“一段短文本新闻”,lable为“体育,军事,IT等”新闻类别。 
(1)分词:把一条记录进行分词,保存为word_list,所有的记录保存在data_list,所有的类别保存在class_list 
(2)划分训练集和测试集 
(3)统计词频:统计所有训练集的每个词出现的次数,即词频,放入all_words_dict字典中,对词频进行降序排序,保存为all_words_list 
(4)停用词表:载入停用词表stopwords_set 
(5)文本特征提取:选取n=1000个特征词(词频高到低依次选)保存在feature_words(选取all_words_list中(all_words_list中词频进行降序排序)排除出现在停用词表中的词,排除数字,并且要求词的长度为(1,5)) 
(6)每条记录的表示(表示成长度为1000的特征词):依次遍历feature_words中每个词,对于每次遍历的词word,如果在当前需要表示的记录中,则该记录的这一位为1,否则为0。即features = [1 if word in text_words else 0 for word in feature_words],这里的text_words 表示一条记录。 
(7)训练,预测:使用MultinomialNB进行训练,预测 
(8)deleteN:删掉前面的deleteN个feature_words词,即最高频的deleteN词不作为特征词,而是从其排序后面的feature_words列表中选1000个特征词,重复以上步骤,得到另一个预测结果。deleteN的取值在某个范围时候,分类效果最好。(如下图,400多时候分类效果最好) 
è¿éåå¾çæè¿°

代码

#coding: utf-8
import os
import time
import random
import jieba 
import nltk  
import sklearn
from sklearn.naive_bayes import MultinomialNB
import numpy as np
import pylab as pl
import matplotlib.pyplot as plt
#粗暴的词去重
def make_word_set(words_file):
    words_set = set()
    with open(words_file, 'r',encoding='UTF-8') as fp:
        for line in fp.readlines():
            word = line.strip() #word = line.strip().decode("utf-8")
            if len(word)>0 and word not in words_set: # 去重
                words_set.add(word)
    return words_set


# 文本处理,也就是样本生成过程
def text_processing(folder_path, test_size=0.2):
    folder_list = os.listdir(folder_path)
    data_list = []
    class_list = []

    # 遍历文件夹
    for folder in folder_list:
        new_folder_path = os.path.join(folder_path, folder)
        files = os.listdir(new_folder_path)
        # 读取文件
        j = 1
        for file in files:
            if j > 100:  # 怕内存爆掉,只取100个样本文件
                break
            with open(os.path.join(new_folder_path, file), 'r', encoding='UTF-8') as fp:
                raw = fp.read()
            ## jieba中文分词
            #jieba.enable_parallel(4)  # 开启并行分词模式,参数为并行进程数,不支持windows
            word_cut = jieba.cut(raw, cut_all=False)  # 精确模式
            word_list = list(word_cut)  # genertor转化为list,每个词unicode格式
            #jieba.disable_parallel()  # 关闭并行分词模式

            data_list.append(word_list)  # 训练集list
            class_list.append(folder.encode('utf-8'))  # 类别 class_list.append(folder.decode('utf-8'))
            j += 1

    ## 划分训练集和测试集
    data_class_list = list(zip(data_list, class_list)) #data_class_list = zip(data_list, class_list)
    random.shuffle(data_class_list)
    index = int(len(data_class_list) * test_size) + 1
    train_list = data_class_list[index:]
    test_list = data_class_list[:index]
    train_data_list, train_class_list = zip(*train_list)
    test_data_list, test_class_list = zip(*test_list)

    # 统计词频放入all_words_dict
    all_words_dict = {}
    for word_list in train_data_list:
        for word in word_list:
            if all_words_dict.get(word)!=None:  #if all_words_dict.has_key(word):
                all_words_dict[word] += 1
            else:
                all_words_dict[word] = 1

    # 词频进行降序排序
    all_words_tuple_list = sorted(all_words_dict.items(), key=lambda f: f[1], reverse=True)  # 内建函数sorted参数需为list
    all_words_list = list(list(zip(*all_words_tuple_list))[0]) #all_words_list = list(zip(*all_words_tuple_list)[0])

    return all_words_list, train_data_list, test_data_list, train_class_list, test_class_list


def words_dict(all_words_list, deleteN, stopwords_set=set()):
    # 选取特征词
    feature_words = []
    n = 1
    for t in range(deleteN, len(all_words_list), 1):
        if n > 1000:  # feature_words的维度1000
            break

        if not all_words_list[t].isdigit() and all_words_list[t] not in stopwords_set and 1 < len(
                all_words_list[t]) < 5:
            feature_words.append(all_words_list[t])
            n += 1
    return feature_words

# 文本特征
def text_features(train_data_list, test_data_list, feature_words, flag='nltk'):
    def text_features(text, feature_words):
        text_words = set(text)
        ## -----------------------------------------------------------------------------------
        if flag == 'nltk':
            ## nltk特征 dict
            features = {word:1 if word in text_words else 0 for word in feature_words}
        elif flag == 'sklearn':
            ## sklearn特征 list
            features = [1 if word in text_words else 0 for word in feature_words]
        else:
            features = []
        ## -----------------------------------------------------------------------------------
        return features
    train_feature_list = [text_features(text, feature_words) for text in train_data_list]
    test_feature_list = [text_features(text, feature_words) for text in test_data_list]
    return train_feature_list, test_feature_list

# 分类,同时输出准确率等
def text_classifier(train_feature_list, test_feature_list, train_class_list, test_class_list, flag='nltk'):
    ## -----------------------------------------------------------------------------------
    if flag == 'nltk':
        ## 使用nltk分类器
        train_flist = zip(train_feature_list, train_class_list)
        test_flist = zip(test_feature_list, test_class_list)
        classifier = nltk.classify.NaiveBayesClassifier.train(train_flist)
        test_accuracy = nltk.classify.accuracy(classifier, test_flist)
    elif flag == 'sklearn':
        ## sklearn分类器
        classifier = MultinomialNB().fit(train_feature_list, train_class_list)
        test_accuracy = classifier.score(test_feature_list, test_class_list)
    else:
        test_accuracy = []
    return test_accuracy


print("start")

## 文本预处理
folder_path = './Database/SogouC/Sample'
all_words_list, train_data_list, test_data_list, train_class_list, test_class_list = text_processing(folder_path, test_size=0.2)

# 生成stopwords_set
stopwords_file = './stopwords_cn.txt'
stopwords_set = make_word_set(stopwords_file)

## 文本特征提取和分类
# flag = 'nltk'
flag = 'sklearn'
deleteNs = range(0, 1000, 20)
test_accuracy_list = []
for deleteN in deleteNs:
    # feature_words = words_dict(all_words_list, deleteN)
    feature_words = words_dict(all_words_list, deleteN, stopwords_set)
    train_feature_list, test_feature_list = text_features(train_data_list, test_data_list, feature_words, flag)
    test_accuracy = text_classifier(train_feature_list, test_feature_list, train_class_list, test_class_list, flag)
    test_accuracy_list.append(test_accuracy)
print(test_accuracy_list)

# 结果评价
#plt.figure()
plt.plot(deleteNs, test_accuracy_list)
plt.title('Relationship of deleteNs and test_accuracy')
plt.xlabel('deleteNs')
plt.ylabel('test_accuracy')
#plt.show()
plt.savefig('result.png')

print("finished")

分类器的伪代码:

计算每个类别中的文档数目  #求P(c_0),P(c_1)
对每篇训练文档:
    对每个类别:
        如果词条出现在文档中:增加该词条计数值
        增加所有词条计数值  #求P(w) 
对每个类别:
    对每个词条:
        将该词条的数目除以总词条数目得到条件概率 #求P(w|c_i)

1. 文本预处理:利用文本构建词汇向量

下面代码里给了一段简单的样本做例子,用来简要说明如何处理文本,利用文本构建词汇向量,后面分类器训练函数里也会用到下面的部分代码。

import numpy as np
def loadDataSet(): #导入数据
    #假设数据为最简单的6篇文章,每篇文章大概7~8个词汇左右,如下
    postingList=[['my', 'dog', 'has', 'flea', 'problems', 'help', 'please'],
                 ['maybe', 'not', 'take', 'him', 'to', 'dog', 'park', 'stupid'],
                 ['my', 'dalmation', 'is', 'so', 'cute', 'I', 'love', 'him'],
                 ['stop', 'posting', 'stupid', 'worthless', 'garbage'],
                 ['mr', 'licks', 'ate', 'my', 'steak', 'how', 'to', 'stop', 'him'],
                 ['quit', 'buying', 'worthless', 'dog', 'food', 'stupid']]
    classVec = [0,1,0,1,0,1] #对应上述6篇文章的分类结果,1为侮辱性,0为非侮辱性
    return postingList,classVec 

def createVocabList(dataSet):# 将所有文章中的词汇取并集汇总
    vocabSet = set([])  # 定义一个set(set存储的内容无重复)
    for document in dataSet:# 遍历导入的dataset数据,将所有词汇取并集存储至vocabSet中
        vocabSet = vocabSet | set(document) # | 符号为取并集,即获得所有文章的词汇表
    return list(vocabSet)

#该函数输入参数为词汇表及某篇文章,输出为文档向量,向量每个元素为1或0,分别表示词汇表中的单词在输入文档中是否出现;
def setOfWords2Vec(vocabList, inputSet):
    returnVec = [0]*len(vocabList) #构建一个0向量;
    for word in inputSet: # 遍历词汇表,如果文档中出现了词汇表中单词,则将输出的文档向量中对应值设为1,旨在计算各词汇出现的次数;
        if word in vocabList:
            returnVec[vocabList.index(word)] = 1#因为上一段代码里,给的文章例子里的单词都是不重复的,如果有重复的单词的话,这段代码改写为:returnVec[vocabList.index(word)] += 1更为合适;
        else: print "the word: %s is not in my Vocabulary!" % word
    return returnVec#返回向量化后的某篇文章

2. 训练函数

def trainNB0(trainMatrix,trainCategory):
    numTrainDocs = len(trainMatrix) #计算有多少篇文章
    numWords = len(trainMatrix[0]) #计算第一篇文档的词汇数
    pAbusive = sum(trainCategory)/float(numTrainDocs) #计算p(c_1),p(c_0)=1-p(c_1)
    p0Num = np.zeros(numWords) #构建一个空矩阵,用来计算非侮辱性文章中词汇数
    p1Num = np.zeros(numWords) #构建一个空矩阵,用来计算侮辱性文章中词汇数
    p0Denom = 0.0; p1Denom = 0.0 
    for i in range(numTrainDocs): #遍历每一篇文章,来求P(w|c)
        if trainCategory[i] == 1: #判断该文章是否为侮辱性文章
            p1Num += trainMatrix[i] #累计每个词汇出现的次数
            p1Denom += sum(trainMatrix[i]) #计算所有该类别下不同词汇出现的总次数
        else:#如果该文章为非侮辱性文章
            p0Num += trainMatrix[i] 
            p0Denom += sum(trainMatrix[i])
    p1Vect = p1Num/p1Denom #计算每个词汇出现的概率P(w_i|c_1)
    p0Vect = p0Num/p0Denom #计算每个词汇出现的概率P(w_i|c_0)
    return p0Vect,p1Vect,pAbusive

有了以上几段代码,我们将训练集的每篇文章向量化,然后计算了训练集中各分类中各词汇出现的概率p(wi|c0),p(wi|c1)p(wi|c0),p(wi|c1),和侮辱性文章的概率p(c1)p(c1)、非侮辱性文章的概率p(c0)p(c0)。

  首先先来小试下牛刀,测试下上面这段代码的功能,假设就6篇文章,内容非常非常简单,只有几个单词,现在咱们利用上面几段代码,将这几篇文章向量化,再计算一下各个词汇在不同分类的文章里出现的概率:
 

if __name__ == '__main__':
    listOPosts,listClasses = loadDataSet()
    myVocabList = createVocabList(listOPosts)
    trainMat = []
    for postinDoc in listOPosts:     
        trainMat.append(setOfWords2Vec(myVocabList,postinDoc))
    # 将所有文章转化为向量,并将所有文章中的词汇append到traninMat中
    p0V,p1V,pAb = trainNB0(trainMat,listClasses)# 计算训练集的p(c_1),p(w_i|c_0),p(w_i|c_1)
    print p0V,p1V,pAb

上述代码运行后输出结果如下所示: 

前两个list为p0V,p1V,0.5代表训练集中侮辱性文章出现的概率。

3. 训练函数修正——避免p(wi|ci)=0p(wi|ci)=0的情况

还记得上篇博客里提到的条件概率P(a|A)=0的处理么?

  当某个类别下某个特征项划分没有出现过一次时,就会导致p(wi|ci)=0p(wi|ci)=0,0乘上其他的数结果为0。这就导致我们求得的条件概率结果不准确。为了解决这个问题,我们引入”拉普拉斯修正”,具体思路如下图所示:

其中|D|所有分类的样本总数,|DC|表示分类为C的样本总数。
  在本例中,为了避免上述影响,将所有词汇出现的次数初始化为1,将分母初始化为2。同时为了避免求得概率数值较小导致程序下溢出。在计算概率的时候,对结果进行就对数,虽然结果会不相同,但是不影响最终结果。具体代码改变较简单,大家可以自己试着改改。

def classifyNB(vec2Classify,p0Vec,p1Vec,pClass1):#vec2Classify为输入的一个向量化的新文章
    p1 = sum(vec2Classify * p1Vec) + log(pClass1)# 由于是取对数,所以乘积变为直接求和即可,注意这里list和list是无法相乘,vec2Classify需要为array格式
    p0 = sum(vec2Classify * p0Vec) + log(1-pClass1)
    if(p1>p0):
        return 1
    if(p0>p1):
        return 0

5. 分类器检验

好了,朴素贝叶斯分类函数就已经完成了,通过上述代码的描述,耐心的读者可能看出来,训练集中判断文章是否为侮辱性,主要关键点为是否含有”stupid”这个单词。光说不练假把式,现在来测试一下这个简单的朴素贝叶斯分类器:

def testingNB():
    listOPosts,listClasses = loadDataSet()
    myVocabList = createVocabList(listOPosts)
    trainMat=[]
    for postinDoc in listOPosts:
        trainMat.append(setOfWords2Vec(myVocabList, postinDoc))
    p0V,p1V,pAb = trainNB0(array(trainMat),array(listClasses))
    testEntry = ['love', 'my', 'dalmation']
    thisDoc = array(setOfWords2Vec(myVocabList, testEntry))
    print testEntry,'classified as: ',classifyNB(thisDoc,p0V,p1V,pAb)
    testEntry = ['stupid', 'garbage']
    thisDoc = array(setOfWords2Vec(myVocabList, testEntry))
    print testEntry,'classified as: ',classifyNB(thisDoc,p0V,p1V,pAb)

最终输出结果为:

['my', 'love', 'dalmation'] classified as:  0
['stupid', 'garbage'] classified as:  1

其实大家可以试着改一下训练集里的内容,把’stupid’改成’fuck’或者其他词汇,然后再添加一些训练集,看看该分类器最终输出的结果是否正确,我试了一下还是挺准的。

  《机器学习实战》这一章里最后讲了利用上述代码实现垃圾邮件分类的内容,有兴趣的小伙伴可以看下书中的这一章。最后感谢大家的阅读,如有问题请多多指正!
--------------------- 
来源:CSDN 
原文:https://blog.csdn.net/c369624808/article/details/78906630 
 

2. SVM 支持向量机

原理

支持向量机(SVM)算法被认为是文本分类中效果较为优秀的一种方法,它是一种建立在统计学习理论基础上的机器学习方法。该算法基于结构风险最小化原理,将数据集合压缩到支持向量集合,学习得到分类决策函数。这种技术解决了以往需要无穷大样本数量的问题,它只需要将一定数量的文本通过计算抽象成向量化的训练文本数据,提高了分类的精确率。
支持向量机(SVM)算法是根据有限的样本信息,在模型的复杂性与学习能力之间寻求最佳折中,以求获得最好的推广能力支持向量机算法的主要优点有:

(1)专门针对有限样本情况,其目标是得到现有信息下的最优解而不仅仅是样本数量趋于无穷大时的最优值;

(2)算法最终转化为一个二次型寻优问题,理论上得到的是全局最优点,解决了在神经网络方法中无法避免的局部极值问题;

(3)支持向量机算法能同时适用于稠密特征矢量与稀疏特征矢量两种情况,而其他一些文本分类算法不能同时满足两种情况。

(4)支持向量机算法能够找出包含重要分类信息的支持向量,是强有力的增量学习和主动学习工具,在文本分类中具有很大的应用潜力。

2 基于SVM的文本分类过程 

SVM 文本分类算法主要分四个步骤:文本特征提取、文本特征表示、归一化处理和文本分类

2.1文本特征提取

目前,在对文本特征进行提取时,常采用特征独立性假设来简化特征选择的过程,达到计算时间和计算质量之间的折中。一般的方法是根据文本中词汇的特征向量,通过设置特征阀值的办法选择最佳特征作为文本特征子集,建立特征模型。(特征提取前,先分词,去停用词)。

本特征提取有很多方法,其中最常用的方法是通过词频选择特征。先通过词频计算出权重,按权重从大到小排序,然后剔除无用词,这些词通常是与主题无关的,任何类的文章中都有可能大量出现的,比如“的”“是”“在”一类的词,一般在停词表中已定义好,去除这些词以后,有一个新的序列排下来,然后可以按照实际需求选取权重最高的前8个,10个或者更多词汇来代表该文本的核心内容。 
综上所述,特征项的提取步骤可以总结为:

(1)对全部训练文档进行分词,由这些词作为向量的维数来表示文本;

(2)统计每一类内文档所有出现的词语及其频率,然后过滤,剔除停用词和单字词;

(3)统计每一类内出现词语的总词频,并取其中的若干个频率最高的词汇作为这一类别的特征词集;

(4)去除每一类别中都出现的词,合并所有类别的特征词集,形成总特征词集。最后所得到的特征词集就是我们用到的特征集合,再用该集合去筛选测试集中的特征。

2.2文本特征表示

TF-IDF 公式来计算词的权值:

其中tfik表示特诊次tk在文档di中出现的频率,N为训练文档总数,nk为在训练集中出现词tk的文档数。由TF-IDF公式,一批文档中某词出现的频率越高,它的区分度则越小,权值也越低;而在一个文档中,某词出现的频率越高,区分度则越大,权重越大。

2.3归一化处理

归一化就是要把需要处理的数据经过处理后(通过某种算法)限制在你需要的一定范围内。

公式中a为关键词的词频,min为该词在所有文本中的最小词频,max为该词在所有文本中的最大词频。这一步就是归一化,当用词频进行比较时,容易发生较大的偏差,归一化能使文本分类更加精确。

2.4文本分类

经过文本预处理、特征提取、特征表示、归一化处理后,已经把原来的文本信息抽象成一个向量化的样本集,然后把此样本集与训练好的模板文件进行相似度计算,若不属于该类别,则与其他类别的模板文件进行计算,直到分进相应的类别,这就是SVM 模型的文本分类方式。 
基于SVM的系统实现(如图所示)

 

 

3. pLSA、共轭先验分布

 

主题模型历史:

Papadimitriou、Raghavan、Tamaki和Vempala在1998年发表的一篇论文中提出了潜在语义索引。1999年,Thomas Hofmann又在此基础上,提出了概率性潜在语义索引(Probabilistic Latent Semantic Indexing,简称PLSI)。

隐含狄利克雷分配LDA可能是最常见的主题模型,是一般化的PLSI,由Blei, David M.、吴恩达和Jordan, Michael I于2003年提出。LDA允许文档拥有多种主题。其它主体模型一般是在LDA基础上改进的。例如Pachinko分布在LDA度量词语关联之上,还加入了主题的关联度。

在自然语言理解任务中,我们可以通过一系列的层次来提取含义——从单词、句子、段落,再到文档。在文档层面,理解文本最有效的方式之一就是分析其主题。在文档集合中学习、识别和提取这些主题的过程被称为主题建模

本文是一篇关于主题建模及其相关技术的综述。文中介绍了四种最流行的技术,用于探讨主题建模,它们分别是:LSA、pLSA、LDA,以及最新的、基于深度学习的 lda2vec

概述

所有主题模型都基于相同的基本假设:

  • 每个文档包含多个主题;
  • 每个主题包含多个单词。

换句话说,主题模型围绕着以下观点构建:实际上,文档的语义由一些我们所忽视的隐变量或「潜」变量管理。因此,主题建模的目标就是揭示这些潜在变量——也就是主题,正是它们塑造了我们文档和语料库的含义。这篇博文将继续深入不同种类的主题模型,试图建立起读者对不同主题模型如何揭示这些潜在主题的认知。

 

LSA

潜在语义分析(LSA)是主题建模的基础技术之一。其核心思想是把我们所拥有的文档-术语矩阵分解成相互独立的文档-主题矩阵和主题-术语矩阵。

第一步是生成文档-术语矩阵。如果在词汇表中给出 m 个文档和 n 个单词,我们可以构造一个 m×n 的矩阵 A,其中每行代表一个文档,每列代表一个单词。在 LSA 的最简单版本中,每一个条目可以简单地是第 j 个单词在第 i 个文档中出现次数的原始计数。然而,在实际操作中,原始计数的效果不是很好,因为它们无法考虑文档中每个词的权重。例如,比起「test」来说,「nuclear」这个单词也许更能指出给定文章的主题。

因此,LSA 模型通常用 tf-idf 得分代替文档-术语矩阵中的原始计数。tf-idf,即词频-逆文本频率指数,为文档 i 中的术语 j 分配了相应的权重,如下所示:

直观地说,术语出现在文档中的频率越高,则其权重越大;同时,术语在语料库中出现的频率越低,其权重越大。

一旦拥有文档-术语矩阵 A,我们就可以开始思考潜在主题。问题在于:A 极有可能非常稀疏、噪声很大,并且在很多维度上非常冗余。因此,为了找出能够捕捉单词和文档关系的少数潜在主题,我们希望能降低矩阵 A 的维度。

这种降维可以使用截断 SVD 来执行。SVD,即奇异值分解,是线性代数中的一种技术。该技术将任意矩阵 M 分解为三个独立矩阵的乘积:M=U*S*V,其中 S 是矩阵 M 奇异值的对角矩阵。很大程度上,截断 SVD 的降维方式是:选择奇异值中最大的 t 个数,且只保留矩阵 U 和 V 的前 t 列。在这种情况下,t 是一个超参数,我们可以根据想要查找的主题数量进行选择和调整。

直观来说,截断 SVD 可以看作只保留我们变换空间中最重要的 t 维。

在这种情况下,U∈ℝ^(m⨉t)是我们的文档-主题矩阵,而 V∈ℝ^(n⨉t)则成为我们的术语-主题矩阵。在矩阵 U 和 V 中,每一列对应于我们 t 个主题当中的一个。在 U 中,行表示按主题表达的文档向量;在 V 中,行代表按主题表达的术语向量。

通过这些文档向量和术语向量,现在我们可以轻松应用余弦相似度等度量来评估以下指标:

  • 不同文档的相似度
  • 不同单词的相似度
  • 术语(或「queries」)与文档的相似度(当我们想要检索与查询最相关的段落,即进行信息检索时,这一点将非常有用)

 

代码实现

在 sklearn 中,LSA 的简单实现可能如下所示:

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.decomposition import TruncatedSVD
from sklearn.pipeline import Pipeline
documents = ["doc1.txt", "doc2.txt", "doc3.txt"] 

# raw documents to tf-idf matrix: 
vectorizer = TfidfVectorizer(stop_words='english', 
                             use_idf=True, 
                             smooth_idf=True)
# SVD to reduce dimensionality: 
svd_model = TruncatedSVD(n_components=100,         // num dimensions
                         algorithm='randomized',
                         n_iter=10)
# pipeline of tf-idf + SVD, fit to and applied to documents:
svd_transformer = Pipeline([('tfidf', vectorizer), 
                            ('svd', svd_model)])
svd_matrix = svd_transformer.fit_transform(documents)

# svd_matrix can later be used to compare documents, compare words, or compare queries with documents

LSA 方法快速且高效,但它也有一些主要缺点:

  • 缺乏可解释的嵌入(我们并不知道主题是什么,其成分可能积极或消极,这一点是随机的)
  • 需要大量的文件和词汇来获得准确的结果
  • 表征效率低

PLSA

pLSA,即概率潜在语义分析,采取概率方法替代 SVD 以解决问题。其核心思想是找到一个潜在主题的概率模型,该模型可以生成我们在文档-术语矩阵中观察到的数据。特别是,我们需要一个模型 P(D,W),使得对于任何文档 d 和单词 w,P(d,w) 能对应于文档-术语矩阵中的那个条目。

让我们回想主题模型的基本假设:每个文档由多个主题组成,每个主题由多个单词组成。pLSA 为这些假设增加了概率自旋:

  • 给定文档 d,主题 z 以 P(z|d) 的概率出现在该文档中
  • 给定主题 z,单词 w 以 P(w|z) 的概率从主题 z 中提取出来

从形式上看,一个给定的文档和单词同时出现的联合概率是:

直观来说,等式右边告诉我们理解某个文档的可能性有多大;然后,根据该文档主题的分布情况,在该文档中找到某个单词的可能性有多大。

在这种情况下,P(D)、P(Z|D)、和 P(W|Z) 是我们模型的参数。P(D) 可以直接由我们的语料库确定。P(Z|D) 和 P(W|Z) 利用了多项式分布建模,并且可以使用期望最大化算法(EM)进行训练。EM 无需进行算法的完整数学处理,而是一种基于未观测潜变量(此处指主题)的模型找到最可能的参数估值的方法。

有趣的是,P(D,W) 可以利用不同的的 3 个参数等效地参数化:

可以通过将模型看作一个生成过程来理解这种等价性。在第一个参数化过程中,我们从概率为 P(d) 的文档开始,然后用 P(z|d) 生成主题,最后用 P(w|z) 生成单词。而在上述这个参数化过程中,我们从 P(z) 开始,再用 P(d|z) 和 P(w|z) 单独生成文档。

这个新参数化方法非常有趣,因为我们可以发现 pLSA 模型和 LSA 模型之间存在一个直接的平行对应关系:

其中,主题 P(Z) 的概率对应于奇异主题概率的对角矩阵,给定主题 P(D|Z) 的文档概率对应于文档-主题矩阵 U,给定主题 P(W|Z) 的单词概率对应于术语-主题矩阵 V。

那么,这说明了什么?尽管 pLSA 看起来与 LSA 差异很大、且处理问题的方法完全不同,但实际上 pLSA 只是在 LSA 的基础上添加了对主题和词汇的概率处理罢了。pLSA 是一个更加灵活的模型,但仍然存在一些问题,尤其表现为:

  • 因为我们没有参数来给 P(D) 建模,所以不知道如何为新文档分配概率
  • pLSA 的参数数量随着我们拥有的文档数线性增长,因此容易出现过度拟合问题

我们将不会考虑任何 pLSA 的代码,因为很少会单独使用 pLSA。一般来说,当人们在寻找超出 LSA 基准性能的主题模型时,他们会转而使用 LDA 模型。LDA 是最常见的主题模型,它在 pLSA 的基础上进行了扩展,从而解决这些问题。

 

LDA

LDA 即潜在狄利克雷分布,是 pLSA 的贝叶斯版本。它使用狄利克雷先验来处理文档-主题和单词-主题分布,从而有助于更好地泛化。

我不打算深入讲解狄利克雷分布,不过,我们可以对其做一个简短的概述:即,将狄利克雷视为「分布的分布」。本质上,它回答了这样一个问题:「给定某种分布,我看到的实际概率分布可能是什么样子?」

考虑比较主题混合概率分布的相关例子。假设我们正在查看的语料库有着来自 3 个完全不同主题领域的文档。如果我们想对其进行建模,我们想要的分布类型将有着这样的特征:它在其中一个主题上有着极高的权重,而在其他的主题上权重不大。如果我们有 3 个主题,那么我们看到的一些具体概率分布可能会是:

  • 混合 X:90% 主题 A,5% 主题 B,5% 主题 C
  • 混合 Y:5% 主题 A,90% 主题 B,5% 主题 C
  • 混合 Z:5% 主题 A,5% 主题 B,90% 主题 C

如果从这个狄利克雷分布中绘制一个随机概率分布,并对单个主题上的较大权重进行参数化,我们可能会得到一个与混合 X、Y 或 Z 非常相似的分布。我们不太可能会抽样得到这样一个分布:33%的主题 A,33%的主题 B 和 33%的主题 C。

本质上,这就是狄利克雷分布所提供的:一种特定类型的抽样概率分布法。我们可以回顾一下 pLSA 的模型:

在 pLSA 中,我们对文档进行抽样,然后根据该文档抽样主题,再根据该主题抽样一个单词。以下是 LDA 的模型:

根据狄利克雷分布 Dir(α),我们绘制一个随机样本来表示特定文档的主题分布或主题混合。这个主题分布记为θ。我们可以基于分布从θ选择一个特定的主题 Z。

接下来,从另一个狄利克雷分布 Dir(?),我们选择一个随机样本来表示主题 Z 的单词分布。这个单词分布记为φ。从φ中,我们选择单词 w。

从形式上看,从文档生成每个单词的过程如下(注意,该算法使用 c 而不是 z 来表示主题):

通常而言,LDA 比 pLSA 效果更好,因为它可以轻而易举地泛化到新文档中去。在 pLSA 中,文档概率是数据集中的一个固定点。如果没有看到那个文件,我们就没有那个数据点。然而,在 LDA 中,数据集作为训练数据用于文档-主题分布的狄利克雷分布。即使没有看到某个文件,我们可以很容易地从狄利克雷分布中抽样得来,并继续接下来的操作。

代码实现

LDA 无疑是最受欢迎(且通常来说是最有效的)主题建模技术。它在 gensim 当中可以方便地使用:

from gensim.corpora.Dictionary import load_from_text, doc2bow
from gensim.corpora import MmCorpus
from gensim.models.ldamodel import LdaModel
document = "This is some document..."
# load id->word mapping (the dictionary)
id2word = load_from_text('wiki_en_wordids.txt')
# load corpus iterator
mm = MmCorpus('wiki_en_tfidf.mm')
# extract 100 LDA topics, updating once every 10,000
lda = LdaModel(corpus=mm, id2word=id2word, num_topics=100, update_every=1, chunksize=10000, passes=1)
# use LDA model: transform new doc to bag-of-words, then apply lda
doc_bow = doc2bow(document.split())
doc_lda = lda[doc_bow]
# doc_lda is vector of length num_topics representing weighted presence of each topic in the doc

通过使用 LDA,我们可以从文档语料库中提取人类可解释的主题,其中每个主题都以与之关联度最高的词语作为特征。例如,主题 2 可以用诸如「石油、天然气、钻井、管道、楔石、能量」等术语来表示。此外,在给定一个新文档的条件下,我们可以获得表示其主题混合的向量,例如,5% 的主题 1,70% 的主题 2,10%的主题 3 等。通常来说,这些向量对下游应用非常有用。

深度学习中的 LDA:lda2vec

那么,这些主题模型会将哪些因素纳入更复杂的自然语言处理问题中呢?

在文章的开头,我们谈到能够从每个级别的文本(单词、段落、文档)中提取其含义是多么重要。在文档层面,我们现在知道如何将文本表示为主题的混合。在单词级别上,我们通常使用诸如 word2vec 之类的东西来获取其向量表征。lda2vec 是 word2vec 和 LDA 的扩展,它共同学习单词、文档和主题向量。

以下是其工作原理。

lda2vec 专门在 word2vec 的 skip-gram 模型基础上建模,以生成单词向量。skip-gram 和 word2vec 本质上就是一个神经网络,通过利用输入单词预测周围上下文词语的方法来学习词嵌入。

通过使用 lda2vec,我们不直接用单词向量来预测上下文单词,而是使用上下文向量来进行预测。该上下文向量被创建为两个其它向量的总和:单词向量和文档向量。

单词向量由前面讨论过的 skip-gram word2vec 模型生成。而文档向量更有趣,它实际上是下列两个组件的加权组合:

  • 文档权重向量,表示文档中每个主题的「权重」(稍后将转换为百分比)
  • 主题矩阵,表示每个主题及其相应向量嵌入

文档向量和单词向量协同起来,为文档中的每个单词生成「上下文」向量。lda2vec 的强大之处在于,它不仅能学习单词的词嵌入(和上下文向量嵌入),还同时学习主题表征和文档表征。

 

总结

我们常把主题模型当作「有点用处」的黑箱算法。幸运的是,与许多神经网络算法不同,主题模型实际上是可解释的,它可以更直接地进行诊断、调整和评估。希望这篇博文能够解释基础数学知识、内在驱动力和你所需要的直觉。

 

原文链接:https://medium.com/nanonets/topic-modeling-with-lsa-psla-lda-and-lda2vec-555ff65b0b05

 

 

 

 

1. https://blog.csdn.net/hao5335156/article/details/82716923 

2. https://blog.csdn.net/c369624808/article/details/78906630

3.https://blog.csdn.net/chenfei0328/article/details/73197547

4. https://blog.csdn.net/Oscar6280868/article/details/78437069 

5. https://zhuanlan.zhihu.com/p/37873878 (非常好!)

 

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值