文本分类(初阶)

前言

之前写了十几篇blog,但更多都是基础知识的回顾,基础知识尽管再好,也是基础知识,它只能帮助你在工作中更快上手,或者说让你在程序员的道路上走得更远——地基打得越好,楼层才能越高。毕竟最近也开始要考虑找工作的事情,所以也要把专业知识给补上。作为一个日常都是处理文本的少年,文本分类是第一道、甚至可以不夸张地说是最重要一道的坎:尽管现在各种NLP方法都热火朝天的样子,但本质上都存在着落地难的问题,NLG看似高端大气上档次,但是除了机器翻译外,其他都很难做到商用级别,而机器翻译也不是单纯地使用Bert啊,ELMO等模型就可以搞定(甚至可以说那种高大上的模型只占其中很少的部分),至于其他NLU,只能说NLG的情况稍微好一丢丢,但还是以规则匹配作为主体(你试试以机器学习作为主体你的工作会有80%的时间跟业务和客户进行“愉快地”交流)。因此,文本分类看似简单,但有可能是日常接触中用到最多机器学习的方法了,还是非常适合重点讲讲。

 

本次的文本分类会分为3个blog,分别为:

文本分类(初阶):主要是传统方法对文本处理的运用,什么One-Hot,TF-IDF等等
文本分类(中阶):使用词向量对文本处理的运用,包括FastText,TextCNN,TextRNN

文本分类(高阶):使用Attention机制以及基于上下文的动态词向量的运用,包括BiLSTM-Attention,Self-Attention等等(Bert要不要讲待定)

写完这些估计会再讲讲文本相似度(基于孪生网络)、Seq2Seq-Attention等模型,在这里也算先进行一个预告吧。

 

传统文本分类的相关理论

先说自己的观点:所谓的文本数据,虽然说是非结构化的数据,但其实本质就是个未经加工的结构化数据。本渣一直没把图像啊,文本啊这些非结构化的数据看得多么特殊,这玩意无论使用深度学习也好,传统学习也罢,第一步,都是要将其进行结构化的处理。在传统的文本分类过程中,整个建模的过程则是:

训练数据->文本预处理->特征提取->文本表示->分类器[1]

 

文本预处理:

 

在文本预处理中,传统的机器学习算法和深度学习算法并无差别。文本预处理过程是在文本中提取关键词表示文本的过程,中文文本处理中主要包括文本分词和去停用词两个阶段。当然,这两个步骤其实也是可选的!例如分词,其实也存在一些争议,因为也有一些研究人员更倾向于使用字而不是词作为文本表示方法——例如现在预训练bert模型,训练得出的大多是以字向量作为单位进行,不过,从语言学的角度上讲,词才是语义上的最小颗粒度!所以个人也倾向于在文本分类任务中使用分词。至于去停用词,那就更好玩了,从定义的角度上讲:停用词是指一些在文档集中出现频率很高,明显对分类任务没有贡献或贡献很小的词,去掉词携带的信息量很少,滤去以后可以提高分类器 的效率,而且对分类器的性能不会有什么影响。但是,定义归定义!你贡献少不等于没贡献,例如你把那些“你”,“我”,“的”这些停用词去掉,总会丢失一部分信息,如果你是追求准确率最大化,我给的建议是:其实也不差你那点内存,停用词只去掉一些奇奇怪怪的标点符号就行。这里为了让整个流程通顺一点,我会把分词和去停用词都执行一遍——但在实际工作中,请切勿照本宣科!

 

 

特征提取与文本表示:

特征提取和文本表示经常混在一起讲,因为你提取到了所谓的特征,也是为了表示文本的。特征提取可以使用互信息(可以参考https://zhuanlan.zhihu.com/p/94074441)进行特征的选择(然而俺真的没看过有勇士这样做的——没必要把简单的问题搞复杂)。在传统方法中,文本表示则常用词袋模型进行。词袋模型是指假定一个文本,忽略其词序和语法、句法,仅仅将其看做是一些词汇的集合,而文本中的每个词汇都是独立的。即每篇文档都看成一个袋子。然后看这个袋子里装的都是些什么词汇——这时候对应的也是One-hot编码。当然,词袋模型也不是专指一种模型,它也有0-1词袋(有则1,没有就是0,就是One-hot),频次词袋(有n个就是n,没有就是0),还有传说中的tf-idf!

为什么我这里着重强调TF-IDF?还要加上一个"传说"两字?

因为TF-IDF的公式无比简单,但又无比重要!重要到什么样的程度呢?俺引用吴军老师《数学之美》的对TF-IDF的评价:公认为信息检索中最重要的发明之一!

但是,它的公式仅仅是

n(i,j)代表了字符i在文档j出现的次数,即字词的重要性随着它在文件中出现的次数成正比增加,但同时会随着它在语料库中出现的频率成反比下降。

所以后续的传统机器模型,我会无脑使用TF-IDF这一值进行文本表示!

 

 

分类器:

分类器的算法在这里我不打算多说了,因为,在文本基于传统表示方法后,使用分类器跟传统的机器学习几乎是一模一样,有兴趣的可以看看我很久很久之前学生时代写的Kaggle实战——Gender Recognition by Voice声音的性别区分(结构化数据)(一)。无非是维度稍微高那么一丢丢(好吧,样本量多的时候会上百万维)。

只要记住,当维度特别大的时候,记得使用稀疏矩阵(放心,sklearn自带)!不然的话内存绝对会爆掉!很多人会在对词向量和词袋模型进行比较时候,都会跟风来一句,节省内存——哥只能呵呵一笑了,too young,boy(girl),这年头难不成还不会用个稀疏矩阵么?其实后续(文本分类中阶)所说的词向量跟词袋模型相比,内存并没有减少多少,词向量(静态词向量)也并非非常非常了不起的技术,最简单的一个直觉,词向量的输入,就是one-hot编码输入,甚至可以说词向量,就是one-hot的检索过程(这个说起来又是一篇blog的事情,有空再说)。

 

当然,以俺多年的经验(好吧,我搞nlp也就2年),树形的结构,一般是不得行的,什么decision tree,random forest,xgboost请直接退出群聊。svm以及朴素贝叶斯分类器,一般是不错滴。当然,也要看情况。不同的数据集,不同的文本分类任务,都会有所差异,这个不懂的请直接百度 天下没有免费的午餐定理。例如,长文本和短文本就有很大的差异。我在这里先抛出我实验的结论(只能说明一部分数据集):如果进行长文本的文本分类,深度学习,或者说后面的中阶,高阶的内容,对比这里的初阶的方法,几乎没有优势,而且,一个朴素贝叶斯,训练需要1秒,一个rnn,训练需要一天,在准确度差不多的情况下,你选哪个?

 

 

实验部分

ok,上面讲了那么多废话,归根结底还是要用实验来说话。本次实验使用复旦大学李荣陆博士提供的文本分类数据集(很老很老很老的数据集了),其中训练集包括 9804 篇文档,测试集包括 9833 篇文档,分为 20 个类别,训练集和测试集文档数接近 1:1。由于数据库各类别的数据量分布极其不均匀。因此,我们只选取其训练集和测试集数据量同时>100 的类别进行分类(低于100的话,在实际工作中就老老实实地让业务人员先收集吧,当然,<100也可以做,例如现在one-shot leaning那些,但效果可以保证绝!对!不!好!)。经过筛选,在本文中删除其中11个类别,最后的训练数据则是得到训练集为 9318 篇文档数,测试集为 9331 篇文 档,共 9 分类。

此外,考虑到有的文本很长很长处理起来太耗时间(这里的数据集是长文本),这里进行一个截取操作,只保留前300个字符(都看了300个字符,计算机如果还识别不出那是啥,短文本岂不是没法混了?)

先进行数据预处理阶段:分词+去停用词

import os
import jieba
import re
import string
from tqdm import tqdm

print("现在开始分词和去停用词")

# 保存文件
def savefile(savepath, content):
    fp = open(savepath, "w",encoding='gb2312', errors='ignore')
    fp.write(content)
    fp.close()

# 读取文件
def readfile(path):
    fp = open(path, "r", encoding='gb2312', errors='ignore')
    content = fp.read()
    fp.close()
    return content

# 加载停用词
def stopwordslist(filepath):
    stopwords = [line.strip() for line in open(filepath,'r',encoding='gb2312',errors='ignore').readlines()]
    return stopwords

# 移除停用词
def movestopwords(sentence,stopwords_list):
    outstr = ''
    for word in sentence.split():
        if word not in stopwords_list:
            if word != '\t' and word != '\t''\n':
                outstr +=" "
                outstr += word
                # outstr += " "
    return outstr

# 替换数字和符号
def replaceAllSymbols(oldStr):
    specialsymbols = "[\s+\.\!\/_,$%^*(+\"\'" + string.punctuation + "]+|[+——!,。?<>《》:;、~@#¥%……&*()]+"
    mathsysmbols = '\d+(\.\d+)*([×\+\-\*\/]\d+(\.\d+)*)*[0-9A-Za-z]*'
    # 去掉数字
    oldStr = re.sub(mathsysmbols, " ", oldStr)
    # 再去掉符号
    return re.sub(specialsymbols, " ", oldStr)

corpus_path_train = "/train_corpus/"  # 训练集未分词分类预料库路径
seg_path_train = "/train_corpus_seg/"  # 训练集分词后分类语料库路径
corpus_path_test = "/test_corpus/"  # 测试集未分词分类预料库路径
seg_path_test = "/test_corpus_seg/"  # 测试集分词后分类语料库路径
corpus_path = (corpus_path_train,corpus_path_test)
seg_path = (seg_path_train,seg_path_test)
catelist = [os.listdir(i) for i in corpus_path]

words_list_train=[]
filename_list_train=[]
words_list_test = []
filename_list_test = []

# 加载停用词表
stopwords = stopwordslist('/hlt2.txt')
print("停用词表长度为:"+str(len(stopwords)))

# 训练集处理
for mydir in catelist[0]:
    class_path = corpus_path[0] + mydir + "/"  # 拼出分类子目录的路径
    seg_dir = seg_path[0] + mydir + "/"  # 拼出分词后预料分类目录
    
    if not os.path.exists(seg_dir):  # 是否存在,不存在则创建
        os.makedirs(seg_dir)
        
    file_list = os.listdir(class_path)
    print("当前处理的类别是: ",mydir)
    for file_path in tqdm(file_list):
        fullname = class_path + file_path     
        content = readfile(fullname).strip()  # 读取文件内容
        content = content.replace("\n", "").strip()  # 删除换行和多余的空格
        content = content.replace('\t',"")  # trans Tab to 空格
 
        #一句话轻松简单搞定分词
        content_seg = jieba.cut(content) 
        words=" ".join(content_seg)

        #因为jieba分词,遇到空格也会作为一个单词,分完词后,将空格全部过滤掉
        words=movestopwords(words,stopwords).strip()
        words=replaceAllSymbols(words).strip()
        words = re.sub(r'\s+',' ',words) # trans 多空格 to 空格 
        
        #因为后面要固定长度,因此我们只需前300个词组即可
        words = " ".join(words.split()[:300]).strip()        
        
        savefile(seg_dir + file_path,words)       
        words_list_train.append(words)  
        filename_list_train.append(mydir) 

# 测试集的遍历
words_list_test = []
for mydir in catelist[1]:
    class_path = corpus_path[1] + mydir + "/"  # 拼出分类子目录的路径
    seg_dir = seg_path[1] + mydir + "/"  # 拼出分词后预料分类目录
    
    if not os.path.exists(seg_dir):  # 是否存在,不存在则创建
        os.makedirs(seg_dir)
        
    file_list = os.listdir(class_path)
    print("当前处理的类别是: ",mydir)
    for file_path in tqdm(file_list):
        fullname = class_path + file_path     
        content = readfile(fullname).strip()  # 读取文件内容
        content = content.replace("\n", "").strip()  # 删除换行和多余的空格
        content = content.replace('\t',"")  # trans Tab to 空格
 
        #一句话轻松简单搞定分词
        content_seg = jieba.cut(content) 
        words=" ".join(content_seg)

        #因为jieba分词,遇到空格也会作为一个单词,分完词后,将空格全部过滤掉
        words=movestopwords(words,stopwords).strip()
        words=replaceAllSymbols(words).strip()
        words = re.sub(r'\s+',' ',words) # trans 多空格 to 空格 
        
        #因为后面要固定长度,因此我们只需前300个词组即可
        words = " ".join(words.split()[:300]).strip()        
        
        savefile(seg_dir + file_path,words)       
        words_list_test.append(words)  
        filename_list_test.append(mydir)  

# 另一种保存方式,把train_test都保存正在一个大的文件上
fileObject_test = open('test_seg_new.txt', "w",encoding='gb2312', errors='ignore')  
fileObject_train = open('train_seg_new.txt', "w",encoding='gb2312', errors='ignore')  
for ip in words_list_train:  
    fileObject_train.write(ip)  
    fileObject_train.write('\n')  
fileObject_train.close()

for ip in words_list_test:  
    fileObject_test.write(ip)  
    fileObject_test.write('\n')  
fileObject_test.close()

fileObject_test_label = open('test_label_new.txt', "w",encoding='gb2312', errors='ignore')  
fileObject_train_lable = open('train_label_new.txt', "w",encoding='gb2312', errors='ignore')  
for ip in filename_list_train:  
    fileObject_train_lable.write(ip)  
    fileObject_train_lable.write('\n')  
fileObject_train_lable.close()

for ip in filename_list_test:  
    filename_list_test.write(ip)  
    filename_list_test.write('\n')  
fileObject_train_lable.close(

print("分词结束")

特征提取与文本表示:

count_v1=TfidfVectorizer()
train_data=count_v1.fit_transform(train_texts)
print ("the shape of train is "+repr(train_data.shape)) 
count_v2 = TfidfVectorizer(vocabulary=count_v1.vocabulary_)
test_data = count_v2.fit_transform(test_texts)  
print("the shape of test is "+repr(test_data.shape)) 
x_train = train_data
y_train = train_labels
x_test = test_data
y_test = test_labels

建模:

朴素贝叶斯(秒出结果有木有!)

print('(3) Naive Bayes...')
from sklearn.naive_bayes import MultinomialNB  
bys_clf = MultinomialNB(alpha = 0.01)   
bys_clf.fit(x_train, y_train)
# 最简单求准确率的方法
scores = bys_clf.score(x_test,y_test)
print("score is"+str(scores))

# 另一种求准确率的方法
preds = bys_clf.predict(x_test)
num = 0
preds = preds.tolist()
for i,pred in enumerate(preds):
    if str(pred) == str(y_test[i]):
        num += 1
print('bys precision_score:' + str(float(num) / len(preds)))
(3) Naive Bayes...
score is0.9165237891127304
bys precision_score:0.9165237891127304

效果还是不错的

 

SVN——但效果迷之好,不愧是2012年以前吊打神经网络的模型

print ('(4) SVM...')
from sklearn.svm import SVC   
svclf = SVC(kernel = 'linear') 
svclf.fit(x_train,y_train)  
scores = svclf.score(x_test,y_test)
print("score is"+str(scores))

preds = svclf.predict(x_test)
num = 0
preds = preds.tolist()
for i,pred in enumerate(preds):
    if str(pred) == str(y_test[i]):
        num += 1
print('svm precision_score:' + str(float(num) / len(preds)))
(4) SVM...
score is0.9528504072010288
svm precision_score:0.9528504072010288

这里默默插一句话:虽然svn在长文本效果不差,但是,我以前跑过短文本,那个效果是真的一言难尽啊

 

RandomForest:

print ('(5) RF...')
from sklearn.ensemble import RandomForestClassifier  
rfclf = RandomForestClassifier() 
rfclf.fit(x_train,y_train)  
scores = rfclf.score(x_test,y_test)
print("score is"+str(scores))

preds = rfclf.predict(x_test)
num = 0
preds = preds.tolist()
for i,pred in enumerate(preds):
    if str(pred) == str(y_test[i]):
        num += 1
print('rf precision_score:' + str(float(num) / len(preds)))
(5) RF...
score is0.9162023146163738
rf precision_score:0.9162023146163738

居然被打脸了,俺严重怀疑这一年sklean里面randomForest的代码出现了改动,明明以前测都是80%+

 

数据集百度云链接:

原始数据文件下述下载:
训练集链接:https://pan.baidu.com/s/1slA3EGH 密码:2uvr
测试集链接:https://pan.baidu.com/s/1eSKZzAM 密码:7uqi

 

代码集:算了,懒得放github链接了,上面几乎都是所有的代码了

 

最后说几句:

虽然这里说的是初阶,但不代表技术不好。虽然现在很多不懂技术的媒体都在一天到晚鼓吹什么神经网络,深度学习。但可以看到,用传统方法效果还是相当可以的。正如我在一开头吐槽,目前其实NLP最难的问题是落地和可解释性——例如某个逻辑出了错你要立马能改,你又是一直无脑堆黑箱模型,那就直接gg了。很多所谓的深度学习,其实在应用中反而只是一个辅助的作用,你试试用bert模型弄一个每秒并发量上万甚至十多万的模型看看???

 

至于为啥面试要一直问深度模型?你要知道,现在算法是真心卷,不多问点最新的问题,你还真没办法看那个人是否勤奋好学.........

 

 

 

参考文献和网站:

[1]用深度学习(CNN RNN Attention)解决大规模文本分类问题 - 综述和实践[Z]https: //zhuanlan.zhihu.com/p/25928551

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值