自然语言处理第一番之文本分类器

前言

文本分类应该是自然语言处理中最普遍的一个应用,例如文章自动分类、邮件自动分类、垃圾邮件识别、用户情感分类等等,在生活中有很多例子,这篇文章主要从传统和深度学习两块来解释下我们如何做一个文本分类器。

文本分类方法

传统的文本方法的主要流程是人工设计一些特征,从原始文档中提取特征,然后指定分类器如LR、SVM,训练模型对文章进行分类,比较经典的特征提取方法如频次法、tf-idf、互信息方法、N-Gram。

深度学习火了之后,也有很多人开始使用一些经典的模型如CNN、LSTM这类方法来做特征的提取, 这篇文章会比较粗地描述下,在文本分类的一些实验

传统文本分类方法

这里主要描述两种特征提取方法:频次法、tf-idf、互信息、N-Gram。

频次法

频次法,顾名思义,十分简单,记录每篇文章的次数分布,然后将分布输入机器学习模型,训练一个合适的分类模型,对这类数据进行分类,需要指出的时,在统计次数分布时,可合理提出假设,频次比较小的词对文章分类的影响比较小,因此我们可合理地假设阈值,滤除频次小于阈值的词,减少特征空间维度。

TF-IDF

TF-IDF相对于频次法,有更进一步的考量,词出现的次数能从一定程度反应文章的特点,即TF,而TF-IDF,增加了所谓的反文档频率,如果一个词在某个类别上出现的次数多,而在全部文本上出现的次数相对比较少,我们认为这个词有更强大的文档区分能力,TF-IDF就是综合考虑了频次和反文档频率两个因素。

互信息方法

互信息方法也是一种基于统计的方法,计算文档中出现词和文档类别的相关程度,即互信息

N-Gram

基于N-Gram的方法是把文章序列,通过大小为N的窗口,形成一个个Group,然后对这些Group做统计,滤除出现频次较低的Group,把这些Group组成特征空间,传入分类器,进行分类。

深度学习方法

基于CNN的文本分类方法

  • 最普通的基于CNN的方法就是Keras上的example做情感分析,接Conv1D,指定大小的window size来遍历文章,加上一个maxpool,如此多接入几个,得到特征表示,然后加上FC,进行最终的分类输出。
  • 基于CNN的文本分类方法,最出名的应该是2014 Emnlp的 Convolutional Neural Networks for Sentence Classification,使用不同filter的cnn网络,然后加入maxpool, 然后concat到一起。 

  • 这类CNN的方法,通过设计不同的window size来建模不同尺度的关系,但是很明显,丢失了大部分的上下文关系,Recurrent Convolutional Neural Networks for Text Classification,将每一个词形成向量化表示时,加上上文和下文的信息,每一个词的表示如下:

整个结构框架如下:

  • 如针对这句话”A sunset stroll along the South Bank affords an array of stunning vantage points”,stroll的表示包括c_l(stroll),pre_word2vec(stroll),c_r(stroll), c_l(stroll)编码A sunset的语义,而c_r(stroll)编码along the South Bank affords an array of stunning vantage points的信息,每一个词都如此处理,因此会避免普通cnn方法的上下文缺失的信息。

基于LSTM的方法

  • 和基于CNN的方法中第一种类似,直接暴力地在embedding之后加入LSTM,然后输出到一个FC进行分类,基于LSTM的方法,我觉得这也是一种特征提取方式,可能比较偏向建模时序的特征;
  • 在暴力的方法之上,A C-LSTM Neural Network for Text Classification,将embedding输出不直接接入LSTM,而是接入到cnn,通过cnn得到一些序列,然后吧这些序列再接入到LSTM,文章说这么做会提高最后分类的准去率。

代码实践

语料及任务介绍

训练的语料来自于大概31个新闻类别的新闻语料,但是其中有一些新闻数目比较少,所以取了数量比较多的前20个新闻类比的新闻语料,每篇新闻稿字数从几百到几千不等,任务就是训练合适的分类器然后将新闻分为不同类别:

Bow

Bow对语料处理,得到tokens set:


  1. def __get_all_tokens(self): 
  2.     """ get all tokens of the corpus 
  3.     """ 
  4.     fwrite = open(self.data_path.replace("all.csv","all_token.csv"), 'w') 
  5.     with open(self.data_path, "r") as fread: 
  6.         i = 0 
  7.         # while True: 
  8.         for line in fread.readlines(): 
  9.             try: 
  10.                 line_list = line.strip().split("\t") 
  11.                 label = line_list[0] 
  12.                 self.labels.append(label) 
  13.                 text = line_list[1] 
  14.                 text_tokens = self.cut_doc_obj.run(text) 
  15.                 self.corpus.append(' '.join(text_tokens)) 
  16.                 self.dictionary.add_documents([text_tokens]) 
  17.                 fwrite.write(label+"\t"+"\\".join(text_tokens)+"\n") 
  18.                 i+=1 
  19.             except BaseException as e: 
  20.                 msg = traceback.format_exc() 
  21.                 print msg 
  22.                 print "=====>Read Done<======" 
  23.                 break 
  24.     self.token_len = self.dictionary.__len__() 
  25.     print "all token len "+ str(self.token_len) 
  26.     self.num_data = i 
  27.     fwrite.close()  

然后,tokens set 以频率阈值进行滤除,然后对每篇文章做处理来进行向量化:


  1. def __filter_tokens(self, threshold_num=10): 
  2.     small_freq_ids = [tokenid for tokenid, docfreq in self.dictionary.dfs.items() if docfreq < threshold_num ] 
  3.     self.dictionary.filter_tokens(small_freq_ids) 
  4.     self.dictionary.compactify() 
  5.  
  6. def vec(self): 
  7.     """ vec: get a vec representation of bow 
  8.     """ 
  9.     self.__get_all_tokens() 
  10.     print "before filter, the tokens len: {0}".format(self.dictionary.__len__()) 
  11.     self.__filter_tokens() 
  12.     print "After filter, the tokens len: {0}".format(self.dictionary.__len__()) 
  13.     self.bow = [] 
  14.     for file_token in self.corpus: 
  15.         file_bow = self.dictionary.doc2bow(file_token) 
  16.         self.bow.append(file_bow) 
  17.     # write the bow vec into a file 
  18.     bow_vec_file = open(self.data_path.replace("all.csv","bow_vec.pl"), 'wb') 
  19.     pickle.dump(self.bow,bow_vec_file) 
  20.     bow_vec_file.close() 
  21.     bow_label_file = open(self.data_path.replace("all.csv","bow_label.pl"), 'wb') 
  22.     pickle.dump(self.labels,bow_label_file) 
  23.     bow_label_file.close()  

最终就得到每篇文章的bow的向量,由于这块的代码是在我的笔记本上运行的,直接跑占用内存太大,因为每一篇文章在token set中的表示是极其稀疏的,因此我们可以选择将其转为csr表示,然后进行模型训练,转为csr并保存中间结果代码如下:


  1. def to_csr(self): 
  2.     self.bow = pickle.load(open(self.data_path.replace("all.csv","bow_vec.pl"), 'rb')) 
  3.     self.labels = pickle.load(open(self.data_path.replace("all.csv","bow_label.pl"), 'rb')) 
  4.     data = [] 
  5.     rows = [] 
  6.     cols = [] 
  7.     line_count = 0 
  8.     for line in self.bow: 
  9.         for elem in line: 
  10.             rows.append(line_count) 
  11.             cols.append(elem[0]) 
  12.             data.append(elem[1]) 
  13.         line_count += 1 
  14.     print "dictionary shape ({0},{1})".format(line_count, self.dictionary.__len__()) 
  15.     bow_sparse_matrix = csr_matrix((data,(rows,cols)), shape=[line_count, self.dictionary.__len__()]) 
  16.     print "bow_sparse matrix shape: " 
  17.     print bow_sparse_matrix.shape 
  18.     # rarray=np.random.random(size=line_count) 
  19.     self.train_set, self.test_set, self.train_tag, self.test_tag = train_test_split(bow_sparse_matrix, self.labels, test_size=0.2) 
  20.     print "train set shape: " 
  21.     print self.train_set.shape 
  22.     train_set_file = open(self.data_path.replace("all.csv","bow_train_set.pl"), 'wb') 
  23.     pickle.dump(self.train_set,train_set_file) 
  24.     train_tag_file = open(self.data_path.replace("all.csv","bow_train_tag.pl"), 'wb') 
  25.     pickle.dump(self.train_tag,train_tag_file) 
  26.     test_set_file = open(self.data_path.replace("all.csv","bow_test_set.pl"), 'wb') 
  27.     pickle.dump(self.test_set,test_set_file) 
  28.     test_tag_file = open(self.data_path.replace("all.csv","bow_test_tag.pl"), 'wb') 
  29.     pickle.dump(self.test_tag,test_tag_file)  

最后训练模型代码如下:


  1. def train(self): 
  2.     print "Beigin to Train the model" 
  3.     lr_model = LogisticRegression() 
  4.     lr_model.fit(self.train_set, self.train_tag) 
  5.     print "End Now, and evalution the model with test dataset" 
  6.     # print "mean accuracy: {0}".format(lr_model.score(self.test_set, self.test_tag)) 
  7.     y_pred = lr_model.predict(self.test_set) 
  8.     print classification_report(self.test_tag, y_pred) 
  9.     print confusion_matrix(self.test_tag, y_pred) 
  10.     print "save the trained model to lr_model.pl" 
  11.     joblib.dump(lr_model, self.data_path.replace("all.csv","bow_lr_model.pl"))   

TF-IDF

TF-IDF和Bow的操作十分类似,只是在向量化使使用tf-idf的方法:


  1. def vec(self): 
  2.     """ vec: get a vec representation of bow 
  3.     """ 
  4.     self.__get_all_tokens() 
  5.     print "before filter, the tokens len: {0}".format(self.dictionary.__len__()) 
  6.     vectorizer = CountVectorizer(min_df=1e-5) 
  7.     transformer = TfidfTransformer() 
  8.     # sparse matrix 
  9.     self.tfidf = transformer.fit_transform(vectorizer.fit_transform(self.corpus)) 
  10.     words = vectorizer.get_feature_names() 
  11.     print "word len: {0}".format(len(words)) 
  12.     # print self.tfidf[0] 
  13.     print "tfidf shape ({0},{1})".format(self.tfidf.shape[0], self.tfidf.shape[1]) 
  14.  
  15.     # write the tfidf vec into a file 
  16.     tfidf_vec_file = open(self.data_path.replace("all.csv","tfidf_vec.pl"), 'wb') 
  17.     pickle.dump(self.tfidf,tfidf_vec_file) 
  18.     tfidf_vec_file.close() 
  19.     tfidf_label_file = open(self.data_path.replace("all.csv","tfidf_label.pl"), 'wb') 
  20.     pickle.dump(self.labels,tfidf_label_file) 
  21.     tfidf_label_file.close()  

这两类方法效果都不错,都能达到98+%的准确率。

CNN

语料处理的方法和传统的差不多,分词之后,使用pretrain 的word2vec,这里我遇到一个坑,我开始对我的分词太自信了,最后模型一直不能收敛,后来向我们组博士请教,极有可能是由于分词的词序列中很多在pretrained word2vec里面是不存在的,而我这部分直接丢弃了,所有可能存在问题,分词添加了词典,然后,对于pre-trained word2vec不存在的词做了一个随机初始化,然后就能收敛了,学习了!!!

载入word2vec模型和构建cnn网络代码如下(增加了一些bn和dropout的手段):


  1. def gen_embedding_matrix(self, load4file=True): 
  2.     """ gen_embedding_matrix: generate the embedding matrix 
  3.     """ 
  4.     if load4file: 
  5.         self.__get_all_tokens_v2() 
  6.     else: 
  7.         self.__get_all_tokens() 
  8.     print "before filter, the tokens len: {0}".format( 
  9.         self.dictionary.__len__()) 
  10.     self.__filter_tokens() 
  11.     print "after filter, the tokens len: {0}".format( 
  12.         self.dictionary.__len__()) 
  13.     self.sequence = [] 
  14.     for file_token in self.corpus: 
  15.         temp_sequence = [x for x, y in self.dictionary.doc2bow(file_token)] 
  16.         print temp_sequence 
  17.         self.sequence.append(temp_sequence) 
  18.  
  19.     self.corpus_size = len(self.dictionary.token2id) 
  20.     self.embedding_matrix = np.zeros((self.corpus_size, EMBEDDING_DIM)) 
  21.     print "corpus size: {0}".format(len(self.dictionary.token2id)) 
  22.     for key, v in self.dictionary.token2id.items(): 
  23.         key_vec = self.w2vec.get(key) 
  24.         if key_vec is not None: 
  25.             self.embedding_matrix[v] = key_vec 
  26.         else: 
  27.             self.embedding_matrix[v] = np.random.rand(EMBEDDING_DIM) - 0.5 
  28.     print "embedding_matrix len {0}".format(len(self.embedding_matrix)) 
  29.  
  30. def __build_network(self): 
  31.     embedding_layer = Embedding( 
  32.         self.corpus_size, 
  33.         EMBEDDING_DIM, 
  34.         weights=[self.embedding_matrix], 
  35.         input_length=MAX_SEQUENCE_LENGTH, 
  36.         trainable=False) 
  37.     # train a 1D convnet with global maxpooling 
  38.     sequence_input = Input(shape=(MAX_SEQUENCE_LENGTH, ), dtype='int32') 
  39.     embedded_sequences = embedding_layer(sequence_input) 
  40.     x = Convolution1D(128, 5)(embedded_sequences) 
  41.     x = BatchNormalization()(x) 
  42.     x = Activation('relu')(x) 
  43.     x = MaxPooling1D(5)(x) 
  44.     x = Convolution1D(128, 5)(x) 
  45.     x = BatchNormalization()(x) 
  46.     x = Activation('relu')(x) 
  47.     x = MaxPooling1D(5)(x) 
  48.     print "before 256", x.get_shape() 
  49.     x = Convolution1D(128, 5)(x) 
  50.     x = BatchNormalization()(x) 
  51.     x = Activation('relu')(x) 
  52.     x = MaxPooling1D(15)(x) 
  53.     x = Flatten()(x) 
  54.  
  55.     x = Dense(128)(x) 
  56.     x = BatchNormalization()(x) 
  57.     x = Activation('relu')(x) 
  58.     x = Dropout(0.5)(x) 
  59.     print x.get_shape() 
  60.     preds = Dense(self.class_num, activation='softmax')(x) 
  61.     print preds.get_shape() 
  62.     adam = Adam(lr=0.0001) 
  63.     self.model = Model(sequence_input, preds) 
  64.     self.model.compile( 
  65.         loss='categorical_crossentropy', optimizer=adam, metrics=['acc']) 

另外一种网络结构,韩国人那篇文章,网络构造如下:


  1. def __build_network(self): 
  2.     embedding_layer = Embedding( 
  3.         self.corpus_size, 
  4.         EMBEDDING_DIM, 
  5.         weights=[self.embedding_matrix], 
  6.         input_length=MAX_SEQUENCE_LENGTH, 
  7.         trainable=False) 
  8.     # train a 1D convnet with global maxpooling 
  9.     sequence_input = Input(shape=(MAX_SEQUENCE_LENGTH, ), dtype='int32') 
  10.     embedded_sequences = embedding_layer(sequence_input) 
  11.     conv_blocks = [] 
  12.     for sz in self.filter_sizes: 
  13.         conv = Convolution1D( 
  14.             self.num_filters, 
  15.             sz, 
  16.             activation="relu", 
  17.             padding='valid', 
  18.             strides=1)(embedded_sequences) 
  19.         conv = MaxPooling1D(2)(conv) 
  20.         conv = Flatten()(conv) 
  21.         conv_blocks.append(conv) 
  22.     z = Merge( 
  23.         conv_blocks, 
  24.         mode='concat') if len(conv_blocks) > 1 else conv_blocks[0] 
  25.     z = Dropout(0.5)(z) 
  26.     z = Dense(self.hidden_dims, activation="relu")(z) 
  27.     preds = Dense(self.class_num, activation="softmax")(z) 
  28.     rmsprop = RMSprop(lr=0.001) 
  29.     self.model = Model(sequence_input, preds) 
  30.     self.model.compile( 
  31.         loss='categorical_crossentropy', 
  32.         optimizer=rmsprop, 
  33.         metrics=['acc']) 

LSTM

由于我这边的task是对文章进行分类,序列太长,直接接LSTM后直接爆内存,所以我在文章序列直接,接了两层Conv1D+MaxPool1D来提取维度较低的向量表示然后接入LSTM,网络结构代码如下:


  1. def __build_network(self): 
  2.     embedding_layer = Embedding( 
  3.         self.corpus_size, 
  4.         EMBEDDING_DIM, 
  5.         weights=[self.embedding_matrix], 
  6.         input_length=MAX_SEQUENCE_LENGTH, 
  7.         trainable=False) 
  8.     # train a 1D convnet with global maxpooling 
  9.     sequence_input = Input(shape=(MAX_SEQUENCE_LENGTH, ), dtype='int32') 
  10.     embedded_sequences = embedding_layer(sequence_input) 
  11.     x = Convolution1D( 
  12.         self.num_filters, 5, activation="relu")(embedded_sequences) 
  13.     x = MaxPooling1D(5)(x) 
  14.     x = Convolution1D(self.num_filters, 5, activation="relu")(x) 
  15.     x = MaxPooling1D(5)(x) 
  16.     x = LSTM(64, dropout_W=0.2, dropout_U=0.2)(x) 
  17.     preds = Dense(self.class_num, activation='softmax')(x) 
  18.     print preds.get_shape() 
  19.     rmsprop = RMSprop(lr=0.01) 
  20.     self.model = Model(sequence_input, preds) 
  21.     self.model.compile( 
  22.         loss='categorical_crossentropy', 
  23.         optimizer=rmsprop, 
  24.         metrics=['acc']) 

CNN 结果:

C-LSTM 结果:

整个实验的结果由于深度学习这部分都是在公司资源上跑的,没有真正意义上地去做一些trick来调参来提高性能,这里所有的代码的网络配置包括参数都仅做参考,更深地工作需要耗费更多的时间来做参数的优化。

PS: 这里发现了一个keras 1.2.2的bug, 在写回调函数TensorBoard,当histogram_freq=1时,显卡占用明显增多,M40的24g不够用,个人感觉应该是一个bug,但是考虑到1.2.2而非2.0,可能后面2.0都优化了。

所有的代码都在github上:tensorflow-101/nlp/text_classifier/scripts

总结和展望

在本文的实验效果中,虽然基于深度学习的方法和传统方法相比没有什么优势,可能原因有几个方面:

  • Pretrained Word2vec Model并没有覆盖新闻中切分出来的词,而且比例还挺高,如果能用网络新闻语料训练出一个比较精准的Pretrained Word2vec,效果应该会有很大的提升;
  • 可以增加模型训练收敛的trick以及优化器,看看是否有准确率的提升;
  • 网络模型参数到现在为止,没有做过深的优化。

本文作者:佚名

来源:51CTO

时间: 2024-10-25 22:59:06

自然语言处理第一番之文本分类器的相关文章

Raúl Garreta大神教你5步搭建机器学习文本分类器:MonkeyLearn

更多深度文章,请关注云计算频道:https://yq.aliyun.com/cloud       用机器学习构建一个好的文本分类器是一项很有挑战性的工作.你需要构造训练集.调参.校正模型及其他事情.本文将会描述如何使用MonkeyLearn训练一个文本分类器,具体分为如下5步: 1. 定义类别树 2. 数据收集 3. 数据标记 4. 训练分类器 5. 测试&提升分类器 1.定义类别树 在训练文本分类器之前,首先要确定你要把你的文本数据分成哪些类或者打上哪些标签. 选择类别 假如你要把来自不同网

Facebook 开源的快速文本分类器 FastTex

FastText是Facebook开发的一款快速文本分类器,提供简单而高效的文本分类和表征学习的方法,性能比肩深度学习而且速度更快. fastText 原理 fastText 方法包含三部分:模型架构.层次 Softmax 和 N-gram 特征.下面我们一一介绍. 1.1 模型架构 fastText 模型架构如下图所示.fastText 模型输入一个词的序列(一段文本或者一句话),输出这个词序列属于不同类别的概率.序列中的词和词组组成特征向量,特征向量通过线性变换映射到中间层,中间层再映射到标

JavaScript获取页面中第一个锚定文本的方法_javascript技巧

本文实例讲述了JavaScript获取页面中第一个锚定文本的方法.分享给大家供大家参考.具体如下: 下面的JS代码通过document.archors数组获取第一个archor的innerHTML <!DOCTYPE html> <html> <body> <a name="html">jb51.net</a><br> <a name="css">CSS Tutorial</a

[自定义服务器控件] 第一步:文本框。

最近在整理我写的几个服务器控件,发出来与大家共享吧. 我写的自定义服务器控件呢分为两个类, 一是"简单继承"控件,就是继承框架里的控件然后加点属性了.事件了什么的. 二是"功能"控件,简单的说那是符合控件,但是呢是以实现一种功能为目的地.这种控件要实现某一种功能,比如分页的功能,添加.修改数据的功能,查询的功能. 原先呢还不会使用接口了什么的,所以呢代码写得有点不太"好看",现在使用接口.继承了什么的来整理一下. 先发一个"文本框&qu

JavaScript获取页面中第一个锚定文本的方法

 具体如下: 下面的JS代码通过document.archors数组获取第一个archor的innerHTML 1 2 3 4 5 6 7 8 9 10 11 12 13 <!DOCTYPE html> <html> <body> <a name="html">jb51.net</a><br> <a name="css">CSS Tutorial</a><br>

用神经网络进行文本分类

本文讲的是用神经网络进行文本分类, 理解聊天机器人如何工作是很重要的.聊天机器人内部一个基础的组成部分是文本分类器.让我们一起来探究一个用于文本分类的人工神经网络的内部结构. 多层人工神经网络 我们将会使用两层神经元(包括一个隐层)和词袋模型来组织(organizing 似乎有更好的选择,求建议)我们的训练数据.有三种聊天机器人文本分类的方法:模式匹配,算法,神经网络.尽管基于算法的方法使用的多项式朴素贝叶斯方法效率惊人,但它有三个根本性的缺陷: 该算法的输出是一个评分而非概率.我们想要的是一个

使用 Python+spaCy 进行简易自然语言处理

本文讲的是使用 Python+spaCy 进行简易自然语言处理, 原文地址:Natural Language Processing Made Easy – using SpaCy (in Python) 原文作者:Shivam Bansal 译文出自:掘金翻译计划 本文永久链接:github.com/xitu/gold-m- 译者:lsvih 校对者:yzgyyang,sqrthree 使用 Python+spaCy 进行简易自然语言处理 简介 自然语言处理(NLP)是人工智能领域最重要的部分之

用深度学习(CNN RNN Attention)解决大规模文本分类问题 - 综述和实践

近来在同时做一个应用深度学习解决淘宝商品的类目预测问题的项目,恰好硕士毕业时论文题目便是文本分类问题,趁此机会总结下文本分类领域特别是应用深度学习解决文本分类的相关的思路.做法和部分实践的经验. 业务问题描述: 淘宝商品的一个典型的例子见下图,图中商品的标题是"夏装雪纺条纹短袖t恤女春半袖衣服夏天中长款大码胖mm显瘦上衣夏".淘宝网后台是通过树形的多层的类目体系管理商品的,覆盖叶子类目数量达上万个,商品量也是10亿量级,我们是任务是根据商品标题预测其所在叶子类目,示例中商品归属的类目为

[python] Kmeans文本聚类算法+PAC降维+Matplotlib显示聚类图像

0 前言 本文主要讲述以下几点:        1.通过scikit-learn计算文本内容的tfidf并构造N*M矩阵(N个文档 M个特征词):        2.调用scikit-learn中的K-means进行文本聚类:        3.使用PAC进行降维处理,每行文本表示成两维数据:        4.最后调用Matplotlib显示聚类效果图. 文章更详细的内容参考:http://blog.csdn.net/eastmount/article/details/50473675由于涉及