这几周因为在做竞赛所以没怎么看论文刷题写博客,今天抽时间把竞赛用到的东西总结一下。先试水了一个很小众的比赛–文因互联,由 AI100 举办,参赛队不足 20 个,赛题类型是文本分类。选择参赛的主要原因是其不像阿里们举办的竞赛那样,分分钟就干一件事就是特征工程和调参,然后数据又多又乱,不适合入门。其次一个原因就是目前我的研究方向就是 NLP,之前也做过一个文本分类的东西,所以就参赛了。这里将主要介绍我在比赛中用到的几个模型,从理论到代码实现进行总结,代码我也放在了我的 github 中。
大家可以到竞赛官网查看赛题并下载数据集,数据集中主要包含下面几个文件,可见数据集很小也很简单,只需要使用 training.csv 文件进行训练我们的文本分类模型,使用 testing.csv 进行预测并提交结果即可: 下面是训练集的前两行,每一行的第一个数字表示该行文本的类别,后面的描述就是要建模的文本。这个数据集是 11 个公司的描述数据,我们要根据 4774 条训练数据去预测 2381 条数据的类别标签。除此之外,我们还可以看到这些训练数据存在较严重的类别不平衡问题。如下图所示:
- 2,
- 合晟资产是一家专注于股票、债券等二级市场投资,为合格投资者提供专业资产管理服务的企业。公司业务范围包括资产管理、投资咨询和投资顾问服务。公司管理的私募基金产品主要包括股票型、债券型资产管理计划或证券投资基金,管理总资产规模80亿元左右。根据中国证券投资基金业协会数据,公司管理的私募证券投资基金(顾问管理)类规模较大,公司管理规模处于50亿元以上的第一梯队。2,
- 公司的主营业务为向中小微企业、个体工商户、农户等客户提供贷款服务,自设立以来主营业务未发生过变化。
了解完数据集,接下来我们开始进行文本分类,开始提交结果。
在这里插句题外话,往往这种竞赛大家喜欢一上来什么都不做先提交一个结果站站场面 == 也就是提交一个随机结果、均值等。因为我看到这个比赛的时候都已经快结束了,比较匆忙,所以第一次提交的也是直接用随机数生成的,后来还自作多情的按照训练集的类比占比作为每个类别概率生成随机数(结果显示确实有提高),代码如下所示 2333:
- importnumpyasnpwithopen('output/random_out.csv','w')asf:foriinrange(1,2382):
- f.write(str(i))
- f.write(',')
- aa = np.random.random()
- b =0
- ifaa <=0.25:
- b =3
- elifaa <=0.5:
- b =4
- elifaa <=0.7:
- b =6
- elifaa <=0.775:
- b=7
- elifaa <=0.825:
- b =5
- elifaa <=0.875:
- b =8
- elifaa <=0.925:
- b =10
- elifaa <=0.95:
- b =11
- elifaa <=0.975:
- b =2
- elifaa <=1:
- b =9f.write(str(b))
- f.write('\n')
好,接下来说正经的,我用的第一种方法就是朴素贝叶斯,可以参见我之前的一篇博客,介绍了使用 CHI 选择特征,TFIDF 计算特征权重,朴素贝叶斯分类的整体流程。因为之前做了这样的尝试,所以这里直接套过来看看效果如何,代码入下,这里的代码都是自己实现的,太丑,其实可以直接调用 gensim 的接口去做,以后有时间改改代码:
- N=4774
- # 读取停词表
- def stop_words():stop_words_file = open('stop_words_ch.txt','r')
- stopwords_list = []forlineinstop_words_file.readlines():
- stopwords_list.append(line.decode('gbk')[:-1])returnstopwords_listdef jieba_fenci(raw, stopwords_list):
- # 使用结巴分词把文件进行切分word_list = list(jieba.cut(raw, cut_all=False))forwordinword_list:ifwordinstopwords_list:
- word_list.remove(word)# word_set用于统计A[nClass]word_list.remove('\n')
- word_set = set(word_list)returnword_list, word_setdef process_file(train_path, test_path):
- '''
- 本函数用于处理样本集中的所有文件。并返回处理结果所得到的变量
- :param floder_path: 样本集路径
- :return: A:CHI公示中的A值,嵌套字典。用于记录某一类中包含单词t的文档总数。第一层总共9个key,对应9类新闻分类
- 第二层则是某一类中所有单词及其包含该单词的文档数(而不是出现次数)。{{1:{'hello':8,'hai':7}},{2:{'apple':8}}}
- TFIDF:用于计算TFIDF权值。三层嵌套字典。第一层和A一样,key为类别。第二层的key为文件名(这里使用文件编号代替0-99).第三层
- key为单词,value为盖单词在本文件中出现的次数。用于记录每个单词在每个文件中出现的次数。
- train_set:训练样本集。与测试样本集按7:3比例分开。三元组(文档的单词表,类别,文件编号)
- test_set:测试样本集。三元组(文档的单词表,类别,文件编号)
- '''stopwords_list = stop_words()# 用于记录CHI公示中的A值A = {}
- tf = []
- i=0
- # 存储训练集/测试集count = [0]*11train_set = []
- test_set = []withopen(train_path,'r')asf:forlineinf:
- tf.append({})
- label = int(line.split(',')[0])-1
- iflabelnot inA:
- A[label] = {}
- count[label] +=1content =""
- foraainline.split(',')[1:]:
- content += aa
- word_list, word_set = jieba_fenci(content, stopwords_list)
- train_set.append((word_list, label))forwordinword_set:ifA[label].has_key(word):
- A[label][word] +=1
- else:
- A[label][word] =1
- forwordinword_list:iftf[i].has_key(word):
- tf[i][word] +=1
- else:
- tf[i][word] =1i +=1
- print "处理完数据"tf2 = []
- j =0
- withopen(test_path,'r')asg:forlineing:
- tf2.append({})
- label = int(line.split(',')[0])-1content =""
- foraainline.split(',')[1:]:
- content += aa
- word_list, word_set = jieba_fenci(content, stopwords_list)
- test_set.append((word_list, label))forwordinword_list:iftf2[j].has_key(word):
- tf2[j][word] +=1
- else:
- tf2[j][word] =1j +=1
- returnA, tf, tf2, train_set, test_set, countdef calculate_B_from_A(A):
- '''
- :param A: CHI公式中的A值
- :return: B,CHI公职中的B值。不是某一类但是也包含单词t的文档。
- '''B = {}forkeyinA:
- B[key] = {}forwordinA[key]:
- B[key][word] =0
- forkkinA:ifkk != keyandA[kk].has_key(word):
- B[key][word] += A[kk][word]returnBdef feature_select_use_new_CHI(A, B, count):
- '''
- 根据A,B,C,D和CHI计算公式来计算所有单词的CHI值,以此作为特征选择的依据。
- CHI公式:chi = N*(AD-BC)^2/((A+C)*(B+D)*(A+B)*(C+D))其中N,(A+C),(B+D)都是常数可以省去。
- :param A:
- :param B:
- :return: 返回选择出的1000多维特征列表。
- '''word_dict = []
- word_features = []foriinrange(0,11):
- CHI = {}
- M = N - count[i]forwordinA[i]:#print word, A[i][word], B[i][word]temp = (A[i][word] * (M - B[i][word]) - (count[i] - A[i][word]) * B[i][word]) ^2/ (
- (A[i][word] + B[i][word]) * (N - A[i][word] - B[i][word]))
- CHI[word] = log(N / (A[i][word] + B[i][word])) * temp#每一类新闻中只选出150个CHI最大的单词作为特征a = sorted(CHI.iteritems(), key=lambdat: t[1], reverse=True)[:100]
- b = []foraaina:
- b.append(aa[0])
- word_dict.extend(b)forwordinword_dict:ifwordnot inword_features:
- word_features.append(word)returnword_featuresdef document_features(word_features, TF, data, num):
- '''
- 计算每一篇新闻的特征向量权重。即将文件从分词列表转化为分类器可以识别的特征向量输入。
- :param word_features:
- :param TFIDF:
- :param document: 分词列表。存储在train_set,test_set中
- :param cla: 类别
- :param num: 文件编号
- :return: 返回该文件的特征向量权重
- '''document_words = set(data)
- features = {}fori, wordinenumerate(word_features):ifwordindocument_words:
- features[word] =1#TF[num][word]#*log(N/(A[cla][word]+B[cla][word]))
- else:
- features[word] =0
- returnfeatures
- A, tf, tf2, train_set, test_set, count = process_file('data/training.csv','data/testing.csv')
- B = calculate_B_from_A(A)print "开始选择特征词"word_features = feature_select_use_new_CHI(A, B, count)#print word_features
- printlen(word_features)forwordinword_features:printwordprint "开始计算文档的特征向量"documents_feature = [(document_features
- (word_features, tf, data[0], i), data[1])fori, datainenumerate(train_set)]print "测试集"test_documents_feature = [document_features(word_features, tf2, data[0], i)fori, datainenumerate(test_set)]#将我们处理之后的结果保存,这样在之后训练模型是直接读取数据即可,省去重复的数据处理json.dump(documents_feature, open('tmp/documents_feature.txt','w'))
- json.dump(test_documents_feature, open('tmp/test_documents_feature.txt','w'))
这里我们可以为每个类选出最具代表性的十个词语看一下,从下面的特征词可以看出来,我们程序提取的特征词还是很具有类别区分度的,也可以看出第四类和第九类、第五类和第八类较为相似,可能在分类上会比较难区分:
- feature_words = [[u'物业管理',u'物业',u'房地产',u'顾问',u'中介',u'住宅',u'商业',u'开发商',u'招商',u'营销策划'],
- [u'私募',u'融资',u'金融',u'贷款',u'基金',u'股权',u'资产',u'小额贷款',u'投资',u'担保'],
- [u'软件',u'互联网',u'平台',u'信息化',u'软件开发',u'数据',u'移动',u'信息',u'系统集成',u'运营'],
- [u'制造',u'安装',u'设备',u'施工',u'机械',u'工程',u'自动化',u'工业',u'设计',u'装备'],
- [u'药品',u'医药',u'生物',u'原料药',u'药物',u'试剂',u'GMP',u'片剂',u'制剂',u'诊断'],
- [u'材料',u'制品',u'塑料',u'环保',u'新型',u'化学品',u'改性',u'助剂',u'涂料',u'原材料'],
- [u'养殖',u'农业',u'种植',u'食品',u'加工',u'龙头企业',u'产业化',u'饲料',u'基地',u'深加工'],
- [u'医疗器械',u'医疗',u'医院',u'医用',u'康复',u'治疗',u'医疗机构',u'临床',u'护理'],
- [u'汽车',u'零部件',u'发动机',u'整车',u'模具',u'C36',u'配件',u'总成',u'车型'],
- [u'媒体',u'制作',u'策划',u'广告',u'传播',u'创意',u'发行',u'影视',u'电影',u'文化'],
- [u'运输',u'物流',u'仓储',u'货物运输',u'货运',u'装卸',u'配送',u'第三方',u'应链',u'集装箱']]
接下来调用 train.py 函数,就可以得到我们的预测结果,这里我使用了朴素贝叶斯、决策树、SVC 三种算法,但是结果显示朴素贝叶斯效果更好,根据参数不同测试集准确率大概达到了 78%~79% 左右。此外还有几个地方可以调节:
- documents_feature =json.load(open('tmp/documents_feature.txt','r'))
- test_documents_feature = json.load(open('tmp/test_documents_feature.txt','r'))
- print"开始训练分类器"classifier = nltk.NaiveBayesClassifier.train(documents_feature)#classifier = nltk.DecisionTreeClassifier.train(documents_feature)
- #classifier = SklearnClassifier(SVC(), sparse=False).train(documents_feature[:4000])
- #test_error = nltk.classify.accuracy(classifier, documents_feature)
- #print "test_error:", test_error
- #classifier.show_most_informative_features(20)results = classifier.classify_many([fsforfsintest_documents_feature])with open('output/TFIDF_out.csv','w')asf:foriinrange(2381):
- f.write(str(i+1))
- f.write(',')
- f.write(str(results[i]+1))
- f.write('\n')
此外,在获得了上面所说的类别特征词之后(每类取十个),我还尝试着用简单的类别匹配方法进行分类,思路很简单,就是看测试集包含哪个特征集中的单词更多,代码入下:
- result = []
- qq =0
- withopen('data/testing.csv',u'r')asf:forlineinf:
- content =""
- foraainline.split(',')[1:]:
- content += aa
- word_list, word_set = jieba_fenci(content, stopwords_list)
- label =2count =0
- fori, clainenumerate(feature_words):
- tmp =0
- forwordinword_list:ifwordincla:
- tmp +=1
- iftmp > count:
- count = tmp
- label = i+1
- ifcount ==0:
- qq +=1result.append(label)
这个效果一般,准确率好像是在 69% 或者 74% 左右,记不太清了。
考虑到 xgboost 算法在各类竞赛中都有很好的效果,我也决定使用该算法尝试一下效果如何,在网上找了一篇博客,直接套用到这里。我们使用所有的词作为特征进行 one-hot 编码(使用 from sklearn.feature_extraction.text import CountVectorizer 和 from sklearn.feature_extraction.text import TfidfTransformer),代码如下:
- # -*- coding: utf-8 -*-
- importxgboostasxgbimportcsvimportjieba#jieba.load_userdict('wordDict.txt')
- importnumpyasnpfromsklearn.feature_extraction.textimportCountVectorizerfromsklearn.feature_extraction.textimportTfidfTransformer# 读取训练集
- def readtrain(path):
- withopen(path,'rb')ascsvfile:
- reader = csv.reader(csvfile)
- column1 = [rowforrowinreader]
- content_train = [i[1]foriincolumn1]# 第一列为文本内容,并去除列名opinion_train = [int(i[0])-1 foriincolumn1]# 第二列为类别,并去除列名
- print '训练集有 %s 条句子'% len(content_train)
- train = [content_train, opinion_train]returntraindef stop_words():stop_words_file = open('stop_words_ch.txt','r')
- stopwords_list = []forlineinstop_words_file.readlines():
- stopwords_list.append(line.decode('gbk')[:-1])returnstopwords_list# 对列表进行分词并用空格连接
- def segmentWord(cont):stopwords_list = stop_words()
- c = []foriincont:
- text =""word_list = list(jieba.cut(i, cut_all=False))forwordinword_list:ifwordnot instopwords_listandword !='\r\n':
- text += word
- text +=' 'c.append(text)returncdef segmentWord1(cont):c = []foriincont:
- a = list(jieba.cut(i))
- b =" ".join(a)
- c.append(b)returnc
- train = readtrain('data/training.csv')
- train_content = segmentWord1(train[0])
- train_opinion = np.array(train[1])# 需要numpy格式
- print "train data load finished"test = readtrain('data/testing.csv')
- test_content = segmentWord(test[0])print 'test data load finished'vectorizer = CountVectorizer()
- tfidftransformer = TfidfTransformer()
- tfidf = tfidftransformer.fit_transform(vectorizer.fit_transform(train_content))
- weight = tfidf.toarray()printtfidf.shape
- test_tfidf = tfidftransformer.transform(vectorizer.transform(test_content))
- test_weight = test_tfidf.toarray()printtest_weight.shape
- dtrain = xgb.DMatrix(weight, label=train_opinion)
- dtest = xgb.DMatrix(test_weight)# label可以不要,此处需要是为了测试效果param = {'max_depth':6,'eta':0.5,'eval_metric':'merror','silent':1,'objective':'multi:softmax','num_class':11}# 参数evallist = [(dtrain,'train')]# 这步可以不要,用于测试效果num_round =100 # 循环次数bst = xgb.train(param, dtrain, num_round, evallist)
- preds = bst.predict(dtest)withopen('XGBOOST_OUTPUT.csv','w')asf:fori, preinenumerate(preds):
- f.write(str(i +1))
- f.write(',')
- f.write(str(int(pre) +1))
- f.write('\n')
效果不错,测试集可以达到 80% 的准确度,出乎意料的好 == 然后我还尝试将提取出来的特征用到 XGBoost 模型上,也就是在 train.py 中调用 xgboost 模型,结果发现准确度出不多也是 80% 左右,没有很大提升。其实这里也应该做参数优化的工作,比如 xgboost 的 max_depth、n_estimate、学习率等参数等应该进行调节,因为时间太紧我这部分工作也没做,而是使用的默认设置 ==
这里使用 YOON KIM 的模型框架,代码使用 WILDML 的,可以参见我之前的一篇博客,为了适用于本任务,修改一下 data_helpers.py 文件中的代码,增加 load_AI100_data_and_labels() 函数,用于读取训练集和测试集。然后就可以训练了,这里使用随机初始化的词向量,让其随模型训练,效果不错,测试集精确度达到了 82% 以上,之后我还尝试了一下使用 char-cnn 模型,但是效果不太好,根本就没有办法收敛,可能是参数选择的不对或者训练集太小了,但是到这比赛就结束了,我也没有时间和机会去尝试更所得模型和参数 ==
赛后,举办方请第一名的选手分享了方法和经验,我发现他也是使用的卷积神经网络,不过分词的时候加入了词性标注,然后使用 gensim 单独训练词向量,然后卷积层的使用了 1000 个卷积核等等吧,其分享链接为:http://geek.ai100.com.cn/2017/05/18/1580 模型架构如下图所示:
来源: http://blog.csdn.net/liuchonge/article/details/72614524