对于每个学习数据挖掘的人来说,总会在某个时刻想要自己从头开始一项数据挖掘工程。这不同于用一用搜狗的新闻资料库,或者是kaggle、天池等竞赛的资料库,要自己从头开始收集数据,使用爬虫收集,然后去重,数据清理等等。
使用已有的数据库,好处是省略了自己收集的过程,遇到各种奇奇怪怪的问题,网上也有解答,更重要的是,我们心里有底,知道如果过程不出问题,最后总是能得到一个不错的结果。但是自己收集数据则不是这样,我们收集的数据是独一无二的,可能隐藏许许多多问题,需要我们自己去处理。在一个数据挖掘工程中,这部分要花费80%以上的时间,但是我们所做的实验,由于种种原因,会将这一步简化甚至略过。这不能不说是一种遗憾。这也是我开始自己从头开始的初衷。
在本实验中,我选择进行英文文本的分类任务。
在数据预处理中,训练集和测试集均由本人自己编写爬虫获得,数据源为浙江大学图书馆文献资料库。
分词、标记、去停用词:使用Windows下Python 3.7 的nltk包。
所使用的分类算法包括如下几个:自编朴素贝叶斯算法; svm:libsvm和liblinear;lda:Python 3.7 的 lda包。
浙江大学图书馆的求是学术搜索拥有较为全面的中英文期刊数据。对于每篇文献,拥有对应的文献关键词、文献类别、文献作者、期刊名称等信息。并且期刊的摘要为显式显示。对比其他数据库如社交信息、新闻等数据,该数据库数据较为规范,分类较为准确,具有较高的实验价值。
本次实验爬取了2005-2019年数据库中十余类共约200万篇英文文献(3.51G),类别分别为农学、解剖学、经济学、化学、教育学、地理学、语言学、法学、数学、物理学等。每类文档从四万到十五万不等。
数据来源:http://libweb.zju.edu.cn
NLTK大概是最知名的Python自然语言处理工具了,全称"Natural Language Toolkit", 诞生于宾夕法尼亚大学,以研究和教学为目的而生,因此也特别适合入门学习。NLTK虽然主要面向英文,但是它的很多NLP模型或者模块是语言无关的,因此如果某种语言有了初步的Tokenization或者分词,NLTK的很多工具包是可以复用的。
本次实验使用了NLTK包的WordNetLemmatizer进行词形还原,pos_tag进行词性标注。词形还原是把一个任何形式的语言词汇还原为一般形式(能表达完整语义)。相对而言,词干提取是简单的轻量级的词形归并方式,最后获得的结果为词干,并不一定具有实际意义。词形还原处理相对复杂,获得结果为词的原形,能够承载一定意义,与词干提取相比,更具有研究和应用价值。
详细的介绍参见该网址。
LIBSVM是台湾大学林智仁(Lin Chih-Jen)教授等开发设计的一个简单、易于使用和快速有效的SVM模式识别与回归的软件包,他不但提供了编译好的可在Windows系列系统的执行文件,还提供了源代码,方便改进、修改以及在其它操作系统上应用;该软件对SVM所涉及的参数调节相对比较少,提供了很多的默认参数,利用这些默认参数可以解决很多问题;并提供了交互检验(Cross Validation)的功能。该软件可以解决C-SVM、ν-SVM、ε-SVR和ν-SVR等问题,包括基于一对一算法的多类模式识别问题。
本次实验首先使用了libsvm进行分类,但是出现了训练时间过长的缺点。因此又选择了liblinear进行尝试。Liblinear是libsvm的特殊实现,其核函数指定为线性核,因此进行了许多优化。另外,libsvm进行n类多分类时,需要训练n(n-1)/2个模型,而liblinear仅需训练n个,这极大缩短了训练和分类时间。
LDA(Latent dirichlet allocation)是由Blei于2003年提出的三层贝叶斯主题模型,通过无监督的学习方法发现文本中隐含的主题信息,目的是要以无指导学习的方法从文本中发现隐含的语义维度-即“Topic”或者“Concept”。隐性语义分析的实质是要利用文本中词项(term)的共现特征来发现文本的Topic结构,这种方法不需要任何关于文本的背景知识。
在综合考虑了各个数据来源之后,我选择了浙江大学图书馆作为数据源。一方面,这个网站我比较熟悉,另一方面,其反爬虫机制也较为孱弱:若约三十分钟内同一IP访问次数多余若干次(约为2000次),则在一段时间内不再响应该IP的访问。另外,对于某一关键词的搜索,最多显示前1000条数据,其余数据不予显示。
这两种机制很容易进行反反爬虫,前者使用代理,我从该网站购买了三百万次IP转发,从而解决了同一IP访问过多的问题。但由于IP转发比直接访问延迟要大得多,因此我选择将爬虫程序部署在我的服务器上,同时进行并行化,对所有类别数据同时进行爬取。在这里我发现,Python自带的并行化方法(threads.append)并不是起若干个进程同时爬取,而是若干个线程进行爬取,这样对效率的提高微乎其微。还是粗暴的同时运行所有脚本效率最高。另外,在爬取过程中可能出现许多意外情况,比如某篇文章缺失作者、缺失摘要等情况,因此要灵活使用except进行异常处理。为了防止脚本意外关闭,使用supervisor守护进程确保在进程被杀掉的时候重启进程。这里要注意一点,浙江大学图书馆可以根据发表时间、分类等进行精确查找。因此可以设置监视哨监视当前的爬取进度,每过一段时间更新监视哨,这样在重启进程时可以根据监视哨快速定位爬取进度。这里我选择时间作为监视哨,爬取方式为按周进行某一类别的查询。这样就可以获取一周内某一类文档的前1000篇,这样也同时解决了后一个反爬虫的问题。
由于转发机制的存在,我从11月23日开始爬取,爬取到11月28日才全部爬取完毕。如下图所示为在服务器上保存的全部原始数据:
如图为使用的监视哨:
图中参数表示最后一次请求搜索的条件为2019年12月22日到29日,第40页,共爬取到154115条数据。
最开始我是选择中文文献进行爬取,但是在爬完第一类农业时,发现该网站的文献标注的一大问题:大量文献没有中文标注,中英文标注混乱等情况时有发生。具体表现为:当我选择类别为农业时,尝试爬取条件为“语言:中文”、“类别:农业”、“时间:2010年-2019年”的数据时,共爬取到约十万条数据,其中,中文数据约三万条,其余均为英文数据。然后我去寻找原因,才发现许多中文文献的内容为中文,但是发表在诸如《上海交通大学学报·农业版(英文版)》上面,因此摘要为英文。因此为了达到要求的数据量,我不得不将原来的中文文本分类改为英文文本分类。
然后我发现了另一类问题,即分类不完善,例如,通过关键词查找,可以找到许多有关某一类别的文档,但是直接指定该类别进行查找时却找不到,即存在大量缺失类别标注的文献。同样为了达到数据量,我选择关键词+类别两类同时进行爬取。这样最终得到了可以使用的数据集,但是由于关键词与类别之间的对应关系并不一定是绝对准确的,这也在一定程度上影响了分类的准确性。
我们知道无论是朴素贝叶斯、svm还是lda,在对数据进行分类时,输入的都不是一串字符串,而是n维向量,数据预处理就是把上一节爬取到的数据转换为n维向量。
在转换之前,我们需要一个字典,字典是单词与数字序号的映射,通过查询字典,我们可以将任何一条数据转换为一个n维向量。因此数据预处理首先要获得字典。
英文文本分类相比于中文,少了一步分词,这是其较中文文本分类难度下降的一大原因。但是比中文文本分类多了词干提取这一步骤。由于英文单词存在变形,因此可能不同单词是同一单词的其他形式,我们应将其还原,从而构建出字典。
字典的键是单词的原始形式,键值为数字序号。然而,若是提取原始数据中的所有单词,是很不必要的,因为介词、冠词等意义不大,而且这样一来冗余过多。实际上选择的优先级是名词>形容词>动词。因此我选择将这三类作为字典的元素。由于需要提取这三类单词,那么词性标注就是必须的。
在具体处理之前,首先将数据集分成测试集和训练集两部分。由于我的数据集按时间有序,因此不能简单的进行分隔,需要使用sklearn将其分割成训练集和测试集两部分。之后使用nltk可以很容易的实现。首先使用word_tokenize将原始数据按空格进行转换。然后使用stopwords去除停用词。接下来使用pos_tag进行词性标注。对于词性为动词、名词、形容词的词语,进行词形还原并加入字典。代码如下:
for row in csv_reader:
tmp1 = nltk.word_tokenize(row[3])
tmp2=[w for w in tmp1 if len(w.lower())>1 and (w.lower() not in stopwords.words("english"))]
tmp3=nltk.pos_tag(tmp2)
tmp_pure=list()
for i in tmp3:
if "NN" in i[1] or "JJ" in i[1] or "VB" in i[1]:
tmp4=""
if "NN" in i[1]:
if easy=="easy":
tmp4=i[0]
else:
tmp4=wnl.lemmatize(i[0],'n')
if tmp4 not in dic_n:
dic_n[tmp4]=0
dic_n[tmp4]+=1
if "JJ" in i[1]:
if easy=="easy":
tmp4=i[0]
else:
tmp4=wnl.lemmatize(i[0],'a')
if tmp4 not in dic_adj:
dic_adj[tmp4]=0
dic_adj[tmp4]+=1
if "VB" in i[1]:
if easy=="easy":
tmp4=i[0]
else:
tmp4=wnl.lemmatize(i[0],'v')
if tmp4 not in dic_v:
dic_v[tmp4]=0
dic_v[tmp4]+=1
if tmp4 not in dic:
dic[tmp4]=0
dic[tmp4]+=1
tmp_pure.append(tmp4)
以农业为例,得到的字典如下:
经过处理后的训练集数据如下:
在上一节我们提到,算法输入需要向量。向量维数越高,那么算法复杂度就越大,需要的算力也就越大。同样以农业为例,全部训练集得到的字典为25万维,所有类别的字典合在一起将达到上百万维。因此必须进行维度压缩。维度压缩应选择最能够代表本类的单词,本实验中选择CHI算法。CHI算法通过卡方检验来获得较为具有代表性的单词。其具体计算方法如下:
设A:包含特征词w且属于类别c的文档频数,B:包含特征词w但不属于类别c的文档频数,C:属于类别c但不包含特征词w的文档频数,D:既不属于c也不包含特征词w的文档频数,N:文档总数,计算
然后由大到小排列即可。代码如下:
def create_A(self):
for name in self.major:
for key in self.dict_all[name]["num"]:
self.A[name][key]=self.dict_all[name]["num"][key]
def create_B(self):
for name in self.major:
for key in self.dict_all[name]["num"]:
if key not in self.B[name]:
self.B[name][key]=0
for _name in self.major:
if _name!=name:
if key in self.dict_all[_name]["num"]:
self.B[name][key]+=self.dict_all[_name]["num"][key]
def create_C(self):
for name in self.major:
for key in self.dict_all[name]["num"]:
self.C[name][key]=self.train_data_num[name]-self.A[name][key]
def create_D(self):
for name in self.major:
for key in self.dict_all[name]["num"]:
self.D[name][key]=self.train_data_sum-self.train_data_num[name]-self.B[name][key]
def create_N(self):
self.N=self.train_data_sum
def cul_CHI(self):
for name in self.major:
i=0
for key in self.dict_all[name]["num"]:
AD_BC=self.A[name][key]*self.D[name][key]-self.B[name][key]*self.C[name][key]
i+=1
if AD_BC<=0 or i>20000:
self.chi_data[name][key]=0
else:
self.chi_data[name][key]=self.N*AD_BC*AD_BC*1.0/(1.0*(self.A[name][key]+self.C[name][key])*(self.B[name][key]+self.D[name][key])*(self.A[name][key]+self.B[name][key])*(self.C[name][key]+self.D[name][key]))
以农业为例,计算得到的CHI如下:
与上面的用频率排序的字典进行对比,可以发现CHI得到的单词更能代表农业类别。下图为前2000单词CHI的数值分布.
可以看出,单词分布呈现出明显的左侧堆积特征,这说明左侧的部分单词已经可以极大的代表该类别了。
虽然我们已经将维度进行了压缩,但是不同单词的权重仍可能不同。这里使用tf-idf进行单词的权重计算。tf-idf是一种简单的衡量单词重要性的指标,其中tf为某一单词在所有文章中出现的次数/所有文章的总单词数,idf为文章总数/(含某一单词的文章数+1),取对数。代码如下:
def cul_tf(self):
for name in self.major:
for key in self.CHI[name]["num"]:
for _name in self.major:
if key in self.dict_all[_name]["num"]:
if key not in self.tf[name]:
self.tf[name][key]=0
self.tf[name][key]+=self.dict_all[_name]["num"][key]
for key in self.tf[name]:
self.tf[name][key]=self.tf[name][key]/(self.word_count*1.0)
#for name in self.major:
# for key in self.dict_all[name]["num"]:
# self.tf[name][key]=self.dict_all[name]["num"][key]/(self.class_word_count[name]*1.0)
def cul_tf_idf(self):
for name in self.major:
for key in self.CHI[name]["num"]:
for _name in self.major:
if key in self.dict_all[_name]["num"]:
if key not in self.idf[name]:
self.idf[name][key]=0
self.idf[name][key]+=self.dict_all[_name]["num"][key]
tfidf_min=1000
tfidf_max=0
for key in self.idf[name]:
self.idf[name][key]=np.log(self.train_count*1.0/self.idf[name][key])
self.tf_idf[name][key]=self.tf[name][key]*1.0*self.idf[name][key]
if self.tf_idf[name][key]>tfidf_max:
tfidf_max=self.tf_idf[name][key]
if self.tf_idf[name][key]
注意要进行归一化处理。如下为农业类别的tf-idf值:
使用CHI查询对应单词的tf-idf值就可以得到对应的权重了。
朴素贝叶斯的原理很简单,就是基于贝叶斯公式:
想要自己实现朴素贝叶斯,必须理解这一公式。在这一公式中,以我们的实验为例,
P(B|A)就是“当测试样本含有A这一系列单词时,样本被分类为B的概率”,
P(A|B)就是“训练集中分类为B的样本中含有A这一系列单词的比例”,
P(B)是“训练集中分类为B的样本在全部训练集样本中的比例”,
P(A)是“训练集中包含A这一系列单词的样本在全部训练集样本中的比例”。
可以看出,右侧的所有值都是通过训练集得到的,因此训练集的好坏、是否具有较强的代表性将极大影响贝叶斯的效果。
由于本实验是多分类,因此针对每一个类别都需要有一个朴素贝叶斯分类器。每个分类器都会给出一个测试样本属于该类别的概率,从中选择出最大的即可。
我的实现思路如下:考虑到P(A|B)中包含一系列单词,因此需要分别计算每个单词在该类别下的条件概率,P(B)则可以直接计算得到。因此计算条件概率的代码如下:
def train(self):
#self.word_num=self.major_dict.iloc[0:len(self.pVect_CHI)]["num"].sum()
self.word_num=self.major_dict.iloc[0:self.major_dict.shape[0]]["num"].sum()
self.fault_value=1.0/(self.word_num+len(self.pVect_CHI))
for key in self.feature_dict_CHI['word']:
#self.pVect_CHI[i]=np.log((self.major_dict.iloc[i]["num"]+1)/(self.word_num*1.0+len(self.pVect_CHI)))
#print(self.major_dict[self.major_dict.word==key]["num"])
self.pVect_CHI[self.feature_dict_CHI['word'][key]]=np.log((self.major_dict[self.major_dict.word==key]["num"]+1)/(self.word_num*1.0+len(self.pVect_CHI)))
self.word_weight[self.feature_dict_CHI['word'][key]]=np.log(self.feature_dict_TFIDF['weight'][key])
#for key in self.feature_dict_TFIDF['word']:
#self.pVect_TFIDF[self.feature_dict_TFIDF['word'][key]]=np.log((self.major_dict[self.major_dict.word==key]["num"]+1)/(self.word_num*1.0+len(self.pVect_TFIDF)))
输入的文本数据需要转为词向量形式,转换函数如下:
def word2vec(self,sen:str):
res_CHI=np.zeros(len(self.pVect_CHI))
match_num_CHI=0
for word in sen.split():
if word in self.feature_dict_CHI['word']:
#res_CHI[self.feature_dict_CHI['word'][word]]+=1
res_CHI[self.feature_dict_CHI['word'][word]]=self.weight_dict["tfidf"][word]
match_num_CHI+=1
最终计算结果代码如下:
def test(self,test_case:np.ndarray,unmatch_num:int,all_sum:int,alpha:int):
res_CHI=sum(alpha*test_case*self.pVect_CHI+(1-alpha)*test_case*self.word_weight)+(np.log(self.fault_value))*unmatch_num+np.log(self.word_num/(all_sum*1.0))
return res_CHI
这里有一点很重要,就是我选择的字典大概率不会包含全部输入的单词,对于字典中没有的单词,也需要给它计算一个条件概率。本实验中我选择了拉普拉斯平滑。
本实验中,朴素贝叶斯耗费了我最多的时间。主要原因在于最开始使用的维度压缩的方法不好。同时由于学科交叉与分类模糊,部分数据难以划分到一个特别准确的类别。例如,讨论语言的教育的论文,其分类可以既是语言又是教育,而不是单独的一个。这使得我的准确率不高——长期徘徊在72%左右。如下表所示:
最初我每类选择的字典为800维,为了减少测试时间,我选择了一个小一点的测试子集进行训练,其准确率约为72.8%。最高的是数学,准确率88%,最低的是语言,仅有53%。随后我将字典扩展到1200维,得到了73.7%的准确率。之后我尝试将语言类的字典单独增长到1200维,其余仍保持在800维,准确率也有73.4%。也就是说,我仅增加语言的字典维度就有了0.6%的准确率提高,将所有的字典全部增加到1200维,则仅仅再提高了0.3%。这说明可以通过有针对性的提高某一表现效果较差的分类器的字典来提高分类性能。实际上来看也是这样,语言类的准确率在仅将其提高到1200维时由53.8%提高到61.9%。这也很好理解,毕竟字典维数高了,命中的几率肯定就大。然后我分别增加字典维度,最后的结果是其他字典2000维,农学和语言学增加到4000维,在总的113万训练集上得到了75.3%的准确率。对比全2000维的74.2%的准确率有了1.1%的提高,对比全800维的72.8%的准确率有了2.5%的提高。这是我没有加入tfidf计算的权重,仅仅使用频率的结果。在使用权重代替频率之后的准确率为76.6%,又有了1.3%的提高。因此我们可以看出,字典能否保证“该类命中率高同时其他类命中率低”,是取得较好效果的关键。简而言之,字典的质量决定最后的效果。
最终的混淆矩阵如下:
可以看出,农学与解剖学、经济学的重叠最高;解剖学与化学重叠率最高等。
最终的准确率和召回率如下:
时间花费方面,朴素贝叶斯的训练时间为5.27分钟,测试时间为117.29分钟。测试时间很长是因为要将字符串转换为词向量等操作,因此耗费了许多时间。
Libsvm由于有了现成的工具箱,因此不必进行繁琐的推导和实现,实在是很好。最开始我调用libsvm的python库,但是遇到了很大的麻烦——训练集需要一次性调入内存,然后整体进行计算,这要求很大的内存,我的8G是根本不够看的。即使我使用了gc等机制仍然无法克服这个问题,因此最后去使用了C++版,C++可以说是运行速度又快,消耗内存又少。很完美了。
Libsvm中,我的主要工作是将数据转换为libsvm可以接受的格式:与朴素贝叶斯类似,但是朴素贝叶斯的词向量仅为800-4000维,但是libsvm要求将所有的字典合在一起,那么总的维度在8000-40000维。40000维是无法忍受的,因此我选择8000维。同时,由于这个向量十分稀疏,8000维中非零向量最多在100个左右,因此可以用“序号:值”的方式进行压缩存储,减少内存消耗。将数据转换为libsvm的形式并不难,只是有点繁琐。
def create_train(self):
current_path = os.getcwd()
path = current_path+"\\train_data_chosen\\"
if os.path.exists(current_path+"\\SVMData\\train_data_win_dic_800_20000.csv")==True:
return
for i in range(len(self.major)):
#train_data=csv.reader(open(path+self.major[i]+"_train_chosen_2.csv","r",encoding='utf-8'))
train_data=pd.DataFrame(pd.read_csv(path+self.major[i]+"_train_chosen_2.csv",index_col=0,error_bad_lines=False))
data_num=0
#with open(path+self.major[i]+"_train_chosen_2.csv","r",encoding='utf-8',errors='ignore') as train_data:
for j in range(len(train_data)):
if data_num>20000:
break
data_num+=1
try:
tmp=train_data.iloc[j,2].split()
if len(tmp)<20 or len(tmp)>300:
continue
word_vec=[0]*len(self.main_dict["num"])
for word in tmp:
if word in self.main_dict["num"]:
word_vec[int(self.main_dict["num"][word])]=self.main_dict_num["tfidf"][word]
win_vec=str(i)
k=1
for d in word_vec:
if d>0:
win_vec+=" "+str(k)+":"+str(round(d,10))
k+=1
with open(current_path+"\\SVMData\\train_data_win_dic_800_20000.csv","a",newline='',encoding='utf-8') as csvfile:
csv_writer = csv.writer(csvfile)
csv_writer.writerow([win_vec])
except:
continue
print("已创建"+self.major[i]+"的训练集")
得到的输入文件内容如下:
最开始我使用正态核,效果很差:准确率约为34%。后来改用线性核,参数为-t 0 –m 4000,即线性核,限制内存使用为4G,效果如下图:
在小规模样本上准确率为79.8576%,在总的数据集上准确率为79.4989%。混淆矩阵如下:
准确率和召回率如下:
其最大缺点是训练时间过长,约12小时,测试时间约14小时。这令我难以忍受。因此我开始寻找其他解决方案。
最终我找到的解决方案为liblinear。其输入格式与libsvm相同,也就不再需要进行格式转换了。
最开始我使用参数为-s 0,即L2正则逻辑回归,训练速度极快,约五分钟,测试速度也很快。最终准确率有78.9799%,如下图:
之后我使用-s 2,L2正则化L2损失支持向量分类,达到了最好的性能。其有一个参数C,即代价系数,针对这一系数进行交叉验证,如下:
最终结果如下:
即当C取到64时准确率最高,使用新的参数再次训练如下:
结果如下:
准确率高达81.9997%。已经很让我满意了。在极大缩短了训练时间和测试时间的基础上又明显提高了准确率。混淆矩阵如下:
准确率和召回率如下:
Lda我是直接使用的python的lda包进行实验。由于我使用的lda包没有针对稀疏向量进行优化,因此输入的向量都是近八千维。其直接后果是,使用我自己的电脑,无法对所有测试集和训练集进行训练和测试。实际上当数据超过十万条,原始csv文件大小就超过了2G,这还是在我选择使用六位小数保存而不是python默认的64位double的基础上进行压缩的。因此我只对五万训练集进行训练,使用五万测试集测试来看看效果。即使这样,还需要一定的gc机制来释放内存,否则会导致内存错误。实在是太难了。
做这个实验process explorer是常开的,在跑lda时,如下图:
可以看到虽然我已经用的是很少的数据集了,但内存占用仍然高达5G。
首先我们载入数据,如下:
def load_data():
'''
加载lda中的数据
'''
current_path = os.getcwd()
tmp1=pd.read_csv(current_path+"\\LDAData\\train_data_dic_800_5000.csv",header=None,dtype=np.int)
print("train.CSV读完了~")
tmp2=pd.DataFrame(tmp1,dtype=np.int)
print("train.CSV转为dataframe~")
del tmp1
gc.collect()
X=tmp2.values
print("label.dataframe转为list~")
del tmp2
gc.collect()
current_path = os.getcwd()
tmp1=pd.read_csv(current_path+"\\LDAData\\test_data_dic_800_5000.csv",header=None,dtype=np.int)
print("test.CSV读完了~")
tmp2=pd.DataFrame(tmp1,dtype=np.int)
print("test.CSV转为dataframe~")
del tmp1
gc.collect()
Y=tmp2.values
print("label.dataframe转为list~")
del tmp2
gc.collect()
print(X.shape)
print(X.sum())
return X,Y
由于lda需要的数据为list,因此要先读入,然后转为dataframe,再转为list。之后把中间变量删除。接下来进行训练:
def Model(X,Y):
'''
构建模型
'''
model=lda.LDA(n_topics=10, n_iter=800, random_state=1)
model.fit(X) # model.fit_transform(X) is also available
topic_word=model.topic_word_ # model.components_ also works
n_top_words = 8
print('-|'*50)
doc_topic = model.doc_topic_
for i in range(10):
predict=[0]*10
for j in range(5000):
#print("{} (top topic: {})".format(" ", doc_topic[i].argmax()))
predict[doc_topic[i*5000+j].argmax()]+=1
print("第"+str(i)+"类准确率="+str(max(predict)*1.0/5000))
print('-|'*50)
plt.plot(model.loglikelihoods_[5:])
plt.savefig('lda_test.png')
Z=model.transform(Y)
whole_right=0
for i in range(10):
predict=[0]*10
for j in range(5000):
#print("{} (top topic: {})".format(" ", doc_topic[i].argmax()))
predict[Z[i*5000+j].argmax()]+=1
print("第"+str(i)+"类准确率="+str(max(predict)*1.0/5000))
whole_right+=max(predict)
print("总准确率="+str(whole_right*1.0/50000))
方法fit用于训练,lda为无监督模型,类似kmeans,所以对训练集的训练就有一个准确度了。参数中,n_topics为分类的类别数,这里为10;n_tier为迭代次数,这里选择为800。进行测试时使用transform方法即可,结果如下:
这是训练集的:
这是测试集的:
可以看到准确率不是很高,这是由于lda对于长文本有较好的效果。我已经将数据限制在最少50个单词了,但效果仍然不是十分理想。
第一个优化方向就是从最开始进行数据清洗,这是我在后来慢慢体会到的。可以使用kmeans等先进行一次聚类,将那些离群点等都删除掉,效果应该好很多。第二个就是数据源问题,应选择更好的数据源,使得数据之间差异更大,但是目前我还没找到。第三个是删除过长或过短的数据,我还是太仁慈了,选择区间为[20,300]的数据,实际上如果数据的长度相近的话,效果会更加好。