赞
踩
本预选赛要求选手建立文本情感分类模型,选手用训练好的模型对测试集中的文本情感进行预测,判断其情感为「Negative」或者「Positive」。所提交的结果按照指定的评价指标使用在线评测数据进行评测,达到或超过规定的分数线即通过预选赛。
数据样本格式:
其中,训练集的样本规模为6328,测试集的样本规模为2712。为保证比赛结果的真实性。每周一中午12点,将替换为新的测试集数据,供队伍答题使用。选手提交结果的评估指标是AUC(Area Under Curve)
硬件配置:CORE i5 7thGen处理器,8G内存,1050显卡
软件配置:Anaconda python3 Spyder
数据集:train.csv、20190506_test.csv
本实验是一个典型的二分类问题,所有用于解决分类问题的模型如罗吉斯特回归、支持向量机、神经网络、朴素贝叶斯等都能适用。
其次,本实验还属于NLP中情感分析问题,处理的数据是印度尼西亚语的评论。情感分析一般有两种方法,一种是带词典的,一种是不带词典的。由于印尼语是小语种,词典难以寻找,所以采用不带词典的方法。
我们需要分类的目标是句子,换句话说,句子是模型的输入。如何表示句子是解决该问题的关键。
one-hot向量将类别变量转换为机器学习算法易于利用的一种形式的过程,这个向量的表示为一项属性的特征向量,也就是同一时间只有一个激活点(不为0),这个向量只有一个特征是不为0的,其他都是0,特别稀疏。
句子可由每个词的one-hot向量相加得到。
Word2vec 可以根据给定的语料库,通过优化后的训练模型快速有效地将一个词语表达成向量形式,为自然语言处理领域的应用研究提供了新的工具。Word2vec依赖skip-grams或连续词袋(CBOW)来建立神经词嵌入。
句子可由每个词的词向量直接相加或拼接成大的矩阵来表示。
Doc2Vec 或者叫做 paragraph2vec, sentence embeddings,是一种非监督式算法,可以获得 sentences/paragraphs/documents 的向量表达,是 word2vec 的拓展。
该方法可以直接得到句子的表示,表示形式为向量。
TF-IDF(term frequency–inverse document frequency,词频-逆向文件频率)是一种用于信息检索(information retrieval)与文本挖掘(text mining)的常用加权技术。
TF-IDF是一种统计方法,用以评估一字词对于一个文件集或一个语料库中的其中一份文件的重要程度。字词的重要性随着它在文件中出现的次数成正比增加,但同时会随着它在语料库中出现的频率成反比下降。
TF-IDF的主要思想是:如果某个单词在一篇文章中出现的频率TF高,并且在其他文章中很少出现,则认为此词或者短语具有很好的类别区分能力,适合用来分类。
句子可由One-Hot思想,用每个单词的tf-idf值代替“1”来表示。
朴素贝叶斯分类是一种十分简单的分类算法,叫它朴素贝叶斯分类是因为这种方法的思想真的很朴素,朴素贝叶斯的思想基础是这样的:对于给出的待分类项,求解在此项出现的条件下各个类别出现的概率,哪个最大,就认为此待分类项属于哪个类别。
Logistic Regression是目前应用比较广泛的一种优化算法,利用logistic regression进行分类的只要思想是:根据现有数据对分类边界线建立回归公式,以此进行分类。“回归”一词源于最佳拟合,表示要找到最佳拟合参数集。
Logistic回归的因变量可以是二分类的,也可以是多分类的,但是二分类的更为常用,也更加容易解释。所以实际中最常用的就是二分类的Logistic回归。
K最近邻(k-Nearest Neighbor,KNN)分类算法,是一个理论上比较成熟的方法,也是最简单的机器学习算法之一。该方法的思路是:如果一个样本在特征空间中的k个最相似(即特征空间中最邻近)的样本中的大多数属于某一个类别,则该样本也属于这个类别。
支持向量机(support vector machines)是一种二分类模型,它的目的是寻找一个超平面来对样本进行分割,分割的原则是间隔最大化,最终转化为一个凸二次规划问题来求解。
随机森林,指的是利用多棵树对样本进行训练并预测的一种分类器。该分类器最早由Leo Breiman和Adele Cutler提出,并被注册成了商标。简单来说,随机森林就是由多棵CART(Classification And Regression Tree)构成的。对于每棵树,它们使用的训练集是从总的训练集中有放回采样出来的,这意味着,总的训练集中的有些样本可能多次出现在一棵树的训练集中,也可能从未出现在一棵树的训练集中。
前馈神经网络(Feedforward Neural Network),简称前馈网络,是人工神经网络的一种。在此种神经网络中,各神经元从输入层开始,接收前一级输入,并输出到下一级,直至输出层。整个网络中无反馈,可用一个有向无环图表示。
官网给的标准是计算AUC,由于我们只有训练集的Label数据,而官网提交次数有限,因此可计算训练集的AUC作为参考再进行提交。
AUC(Area Under Curve)被定义为ROC曲线下与坐标轴围成的面积,显然这个面积的数值不会大于1。又由于ROC曲线一般都处于y=x这条直线的上方,所以AUC的取值范围在0.5和1之间。使用AUC值作为评价标准是因为很多时候ROC曲线并不能清晰的说明哪个分类器的效果更好,而作为一个数值,对应AUC更大的分类器效果更好。
句子由词组成,不管是为了得到one-hot、词向量、还是其他,处理的基本单元都是单词。而因为是外文单词,采用NLTK的分词可以帮助我们有效分词。
NLTK是一个高效的Python构建的平台,用来处理人类自然语言数据。它提供了易于使用的接口,通过这些接口可以访问超过50个语料库和词汇资源(如WordNet),还有一套用于分类、标记化、词干标记、解析和语义推理的文本处理库,以及工业级NLP库的封装器和一个活跃的讨论论坛。
rev_cut():对评论进行分词,分好的词放在“rev_cut.txt”中。
def rev_cut(review):
cut_file = './rev_cut.txt'
with open(cut_file, 'w', encoding='utf-8') as f:
for rev in review:
rev_cut = " ".join(nltk.word_tokenize(rev))#对句子进行分词
f.writelines(rev_cut +'\n')
return cut_file
Gensim是一款开源的第三方Python工具包,用于从原始的非结构化的文本中,无监督地学习到文本隐层的主题向量表达。 它支持包括TF-IDF,LSA,LDA,和word2vec在内的多种主题模型算法。
word_vec(cut_file,model_name,dimension):由分好的词训练得到每个单词的词向量,并保存。
get_sents_word_vec (review,dimension,model_name):载入训练好得模型,由词向量的平均值得到句向量。
def word_vec(cut_file,model_name,dimension): logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO) sentences = gensim.models.word2vec.LineSentence(cut_file) #单词隔开,换行符换行 model = gensim.models.word2vec.Word2Vec(sentences, hs=1,min_count=1,window=3,size=dimension) model.save(model_name) def get_sents_word_vec(review,dimension,model_name): model = gensim.models.word2vec.Word2Vec.load(model_name) sent_vec = np.array([0]*dimension) sents_vec = [] for rev in review: i = 0 for word in nltk.word_tokenize(rev): sent_vec = sent_vec + model[word] i = i + 1 sent_vec = sent_vec / float(i) sents_vec.append(sent_vec) sents_vec = np.array(sents_vec) return sents_vec
同样用Gensim库,可得到句向量,具体如下。
sent_vec(cut_file,model_name,dimension):由分好的词训练得到句子向量并将模型保存。
get_sents_sent_vec (review,dimension,model_name):载入模型,获取每条评论对应得句向量。
Sklearn库提供了比较方便和一体的机器学习方法和tf-idf值计算,NLTK得到one-hot编码,这两部分相较比较简单,和具体的方法写在了一起,详情可见第(二)点模型训练。
Sklearn库封装了大量机器学习的算法,对于应用只需调用,无需了解深度的细节。由于很多方法代码基本一致,因此在这里以结果最好的tf-idf和朴素贝叶斯的组合。
def train_NB_tfidf_skl(train_data,test_rev,all_rev): labels = train_data['label'] train_rev = train_data['review'] ID = test_data['ID'] train_lab = get_lab(labels) labs = train_lab corpus = all_rev vectorizer=CountVectorizer() transformer = TfidfTransformer() tfidf = transformer.fit_transform(vectorizer.fit_transform(corpus)) train_X = tfidf[0:len(train_rev)] test_X = tfidf[len(train_rev):len(train_rev)+len(test_rev)] #使用朴素贝叶斯进行训练 mnb = MultinomialNB(alpha=0.5, class_prior=None, fit_prior=True) mnb.fit(train_X,labs) # 利用训练数据对模型参数进行估计 train_score = [pred[1]for pred in mnb.predict_proba(train_X)] test_score = [pred[1]for pred in mnb.predict_proba(test_X)] print ('The Accuracy of Naive Bayes Classifier is:', mnb.score(train_X,labs)) train_score = np.array(train_score,dtype="float32") test_score = np.array(test_score,dtype="float32") print("AUC: ",cal_auc(train_score,train_lab)) result = pd.DataFrame({'ID':ID.T,'Pred':test_score}) result.to_csv("./result.csv",index = None)
使用tensorflow底层代码自行搭建前馈神经网络,分为输入层、隐层、输出层。其中隐层共有两层,每层有25个结点。输入层结点数与评论数对应,输出层与分类数对应,由于二分类,因此输出层可以只有一个结点。用预测感情得分与真实分值的均方误差作为损失函数,用自适应梯度下降算法求解,用Relu做激活函数。
def train_simplenn(train_data,sents_vec,test_sentsvec): labels = train_data['label'] lab = get_lab(labels) dataset_size = sents_vec.shape[0]#样本数 dimension = sents_vec.shape[1]#输入特征维度(特征个数) emb_unit_counts_1 = 25 #隐层结点数 emb_unit_counts_2 = 25 #隐层结点数 batch_size = 16 #定义训练数据batch的大小 tf.reset_default_graph() w1 = tf.Variable(tf.random_normal([dimension,emb_unit_counts_1],stddev=1,seed=1)) w2 = tf.Variable(tf.random_normal([emb_unit_counts_1,emb_unit_counts_2],stddev=1,seed=2))#初试化权重矩阵(生成相应大小的两个随机正态分布,均值为0,方差为1) w3 = tf.Variable(tf.random_normal([emb_unit_counts_2,1],stddev=1,seed=3)) x = tf.placeholder(tf.float32,shape=(None,dimension),name='x-input') #在第一个维度上利用None,可以方便使用不大的batch大小 y_ = tf.placeholder(tf.float32,shape=(None,1),name='y-input') #在第一个维度上利用None,可以方便使用不大的batch大小 b1 = tf.Variable(tf.zeros([emb_unit_counts_1]))# 隐藏层的偏向bias b2 = tf.Variable(tf.zeros([emb_unit_counts_2]))# 隐藏层的偏向bias b3 = tf.Variable(tf.zeros([1]))# 输出层的偏向bias learning_rate = 0.0015 #learning_rate = 0.01 #定义前向传播过程 a1 = tf.nn.softplus(tf.add(tf.matmul(x,w1),b1)) #计算隐层的值 a2 = tf.nn.softplus(tf.add(tf.matmul(a1,w2),b2)) #计算隐层的值 y = tf.nn.sigmoid(tf.add(tf.matmul(a2,w3),b3)) #计算输出值 #定义损失函数和反向传播算法 loss = tf.reduce_mean(tf.square(y_-y)) #loss = -tf.reduce_mean(y_*tf.log(tf.clip_by_value(y,1e-10,1.0)))#cross_entropy #learning_rate = 0.001 #tf.train.exponential_dacay(0.1 ,STEPS,dataset_size/batch_size , 0.96 , staircase = True) #optimizer = tf.train.MomentumOptimizer(learning_rate,momentum).minimize(loss) #在此神经网络中只有基础学习率的设置,没有指数衰减率,也没有正则项和滑动平均模型。 optimizer = tf.train.AdamOptimizer(learning_rate).minimize(loss) #在此神经网络中只有基础学习率的设置,没有指数衰减率,也没有正则项和滑动平均模型。 X = sents_vec Y = lab #以下创建一个session会话来运行TensorFlow程序 with tf.Session() as sess: init_op = tf.initialize_all_variables() sess.run(init_op) #在此利用以上两行对其中涉及的参数进行统一初始化 STEPS = 100000#设定训练的轮数 for i in range(STEPS): start = (i*batch_size) %dataset_size end = min(start+batch_size,dataset_size)#每次选取batch_size个样本进行训练 sess.run(optimizer,feed_dict = {x:X[start:end],y_:Y[start:end]})#通过选取的样本训练神经网络并更新其中的参数 if i%1000==0: score,loss_value = sess.run([y,loss],feed_dict={x:X,y_:Y}) print("After%dtraining step(s),loss on all data is%g"%(i,loss_value)) saver=tf.train.Saver(max_to_keep=1) saver.save(sess,'ckpt/emtion')#,global_step=i+1 print("AUC: ",cal_auc(score,lab)) X2 = test_sentsvec test_pred = sess.run(y,feed_dict = {x:X2}) ID = [] for s in range(1,len(X2)+1): ID.append(s) ID = np.array(ID) result = pd.DataFrame({'ID':ID.T,'Pred':test_pred.T[0]}) result.to_csv("./result.csv",index = None)
AUC计算需先构造混淆矩阵,再画出ROC曲线,最后利用定积分方法计算面积得到。
def cal_auc(score,lab): threhold = 0 dx = 0.005 #dx = 0.1 auc = 0 pos_x = [] pos_y = [] while(threhold<=1): TP = 0 FN = 0 FP = 0 TN = 0 pred = [] for i in range(0,len(score)): if(score[i] >= threhold):pred.append(1) else: pred.append(0) pred = np.array(pred) for i in range(0,len(lab)): if(lab[i] == 1): #正例 if(pred[i] == 1): #真正例 TP = TP + 1 elif(pred[i] == 0):#假负例 FN = FN + 1 elif(lab[i] == 0): #负例 if(pred[i] == 1): #假正例 FP = FP + 1 elif(pred[i] == 0): #真负例 TN = TN + 1 FPR = FP/float(FP+TN) TPR = TP/float(TP+FN) pos_x.append(FPR) pos_y.append(TPR) threhold = threhold + dx x = np.array(pos_x) y = np.array(pos_y) plt.xlim(0,1) plt.ylim(0,1) plt.scatter(x,y) auc = abs(np.trapz(y,x,dx=dx)) return auc
同样以朴素贝叶斯+tf-idf的方法为例,其效果图如下图所示,AUC值为AUC: 0.9872392308651139。该图只表明在训练集上我们模型的效果,由于过拟合或面积计算不精确等原因,在测试集上会有较大偏差,但一般来说,训练集AUC越大,曲线越连续平滑,在测试集上越好,对于我们提交有指导意义。
由于提交次数有限,还借了几个账号进行测试。总共做的实验及其结果如下表所示:
实验编号 | 实验方法 | 提交评分(AUC) |
1 | tf-idf+朴素贝叶斯 | 0.85319362 |
2 | tf-idf+罗吉斯特回归 | 0.8514 |
3 | one-hot+朴素贝叶斯 | 0.81020273 |
4 | tf-idf+随机森林 | 0.79925816 |
5 | tf-idf+K近邻 | 0.78078132 |
6 | tf-idf+支持向量机 | 0.77722 |
7 | word2vec(平均)+前馈神经网络 | 0.73 |
8 | tf-idf+高斯贝叶斯 | 0.70844343 |
9 | doc2vec+前馈神经网络 | 0.62 |
10 | word2vec(平均)+高斯贝叶斯 | 0.61831131 |
11 | word2vec(拼接)+前馈神经网络 | 0.61 |
12 | doc2vec+高斯贝叶斯 | 0.52883599 |
从实验结果和编程调试的情况上来看,对于本次实验,特征表示最好的方法是tf-idf,其次是one-hot编码,再是word2vec、doc2vec。这与理论上分析不太一致,因为词向量、句向量理论上更能表示特征。造成这种情况的原因可能在于预选赛数据集较少,作为data driven的方法代表,词向量表现的不是很好。
这同样也体现在模型方法上,作为一开始比较期待的神经网络方法,尽管未采用CNN、RNN、LSTM等高端网络结构,也未用deep learning经典网络结构如Resnet、VGG等,前馈神经网络表现得不是很尽如人意,劣于大多数传统机器学习方法。
这个实验结果和代码调试的经验告诉我,data driven方法并不是万金油,其容易过拟合,且在小数据集上还是表现不佳,传统和经典方法,有些尽管简单,但却直接、高效。因此,往后对于特定问题还是要具体分析,不能盲目选择方法。
完整的数据集和实验代码见这里。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。