集智专栏
资源加载中,请稍后...
集智专栏

这评论有毒!——文本分类的一般套路

作者:程序


文本分类,是各种数据挖掘比赛中比较常见的题,今天我们打算分享了一个评论多分类比赛的一般套路。数据集来源于kaggle比赛:Toxic Comment Classification Challenge,这份代码得分约在0.043分左右。

为方便大家下载数据集,我已经把数据上传到了百度网盘,方便大家下载,具体链接在这,https://pan.baidu.com/s/1rabq4Yo

同时也分享下代码作者的kernel,链接在:https://www.kaggle.com/qqgeogor/keras-lstm-attention-glove840b-lb-0-043/code

好了,我们开始文本分类的套路之旅吧。


1. 数据预处理

1.1 导入数据集

第一件事情自然是先下载数据,然后看看数据的样子咯。

程序说明
数据概览
预处理代码
!cp /mnt/vol0/Kaggle/toxic/* ~/work
示例代码
######################################## ## import packages ######################################## import os import re import csv import codecs import numpy as np import pandas as pd from nltk.corpus import stopwords from nltk.stem import SnowballStemmer from string import punctuation from gensim.models import KeyedVectors from keras.preprocessing.text import Tokenizer from keras.preprocessing.sequence import pad_sequences from keras.layers import Dense, Input, LSTM, Embedding, Dropout, Activation from keras.layers.merge import concatenate from keras.models import Model from keras.layers.normalization import BatchNormalization from keras.callbacks import EarlyStopping, ModelCheckpoint import sys ######################################## ## set directories and parameters ######################################## from keras import backend as K from keras.engine.topology import Layer from keras import initializers, regularizers, constraints train_df = pd.read_csv("train.csv", nrows=9697) test_df = pd.read_csv("test.csv", nrows=24600) train_df.head()


1.2 初步预处理

数据非常简单,comment_text是评论内容,toxic, severe_toxic, obscene, threat, insult, identity_hate是评论标签,没有其他数据。

接下来,我们需要对comment_text数据进行初步的处理。处理的手段包括:

  1. 英文字母大小写的处理,一般都是把单词转为小写字母。
  2. 去停用词,英文中很多功能性的单词,如the、a、an、that等,这些单词对于评论分类没有多大作用,因此常规做法就是把这些停用词直接给丢了。
  3. 正则表达式去除一些空格等奇怪字符。
  4. 提取词干,如went,go等,本质上这2个单词的意义是一样的,因此需要提取它们的词干。

以下便是文本初步处理的代码,但是实际操作中,因为用的pretrained model,所以并没有提取词干和去停用词。而且实测发现,因为使用了glove预训练的结果,不做提词干和去停用词的得分还要高于去停用词和提词干,为什么会出现这样的情况呢?打个比方,单词country,在提取词干后就变成cntry,可能会存在找不到词向量的情况,因此这也从侧面反映出了先验的重要性。

  • 运行下面的代码,可以看到经过这套组合拳后,我们的评论数据所发生的变化。
程序说明
预处理
示例代码
# 一些参数设置,后面用得着 EMBEDDING_FILE = 'glove.840B.300d.txt' TRAIN_DATA_FILE = 'train.csv' TEST_DATA_FILE = 'test.csv' MAX_SEQUENCE_LENGTH = 150 MAX_NB_WORDS = 100000 EMBEDDING_DIM = 300 VALIDATION_SPLIT = 0.1 num_lstm = 300 num_dense = 256 rate_drop_lstm = 0.25 rate_drop_dense = 0.25 act = 'relu' #正则表达式 special_character_removal = re.compile(r'[^a-z\d ]',re.IGNORECASE) replace_numbers = re.compile(r'\d+',re.IGNORECASE) def text_to_wordlist(text, remove_stopwords=False, stem_words=False): #字母小写 text = text.lower().split() # 去停用词 if remove_stopwords: stops = set(stopwords.words("english")) text = [w for w in text if not w in stops] text = " ".join(text) #正则处理 text=special_character_removal.sub('',text) text=replace_numbers.sub('n',text) # 提取词干 if stem_words: text = text.split() stemmer = SnowballStemmer('english') stemmed_words = [stemmer.stem(word) for word in text] text = " ".join(stemmed_words) return(text) print(train_df.loc[0,"comment_text"]) print(text_to_wordlist(train_df.loc[0,"comment_text"]))
程序验证过程
Nonsense? kiss off, geek. what I said is true. I'll have your account terminated. nonsense kiss off geek what i said is true ill have your account terminated


1.3 进阶预处理

然后,我们需要对经过预处理的评论数据进行分词等进一步处理。

这里使用了keras库里的文本和序列预处理的库,Tokenizer是一个用于向量化文本,可将文本转换为序列的类,pad_sequences则用于序列填充。

程序说明
分词
示例代码
list_sentences_train = train_df["comment_text"].fillna("NA").values list_classes = ["toxic", "severe_toxic", "obscene", "threat", "insult", "identity_hate"] y = train_df[list_classes].values list_sentences_test = test_df["comment_text"].fillna("NA").values comments = [] for text in list_sentences_train: comments.append(text_to_wordlist(text)) test_comments=[] for text in list_sentences_test: test_comments.append(text_to_wordlist(text)) tokenizer = Tokenizer(num_words=MAX_NB_WORDS) tokenizer.fit_on_texts(comments + test_comments) sequences = tokenizer.texts_to_sequences(comments) test_sequences = tokenizer.texts_to_sequences(test_comments) word_index = tokenizer.word_index print('Found %s unique tokens' % len(word_index)) data = pad_sequences(sequences, maxlen=MAX_SEQUENCE_LENGTH) print('Shape of data tensor:', data.shape) print('Shape of label tensor:', y.shape) test_data = pad_sequences(test_sequences, maxlen=MAX_SEQUENCE_LENGTH) print('Shape of test_data tensor:', test_data.shape)
程序验证过程
True


代码的意思其实也很简单,就是利用train和test中的comment_text数据,先造了一个字典word_index,在词典中,每个单词都有一个对应的下标序号,texts_to_sequences()的作用则是构造一个list,list的元素都来源于word_index这个字典。

如:单词nonsense,word_index.get("nonsense")就是等于845。pad_seq则是在sequences的基础上进行了填充,填充至150,这样每个sequences的维度就一样了。


1.4 词嵌入

接下来我们再准备词嵌入的操作。在进行词嵌入的时候,我们需要用到golve做的词向量预训练的结果,这相当于给我们的模型增加了一个先验,对于这个赛题而言,成绩的好坏很大程度依赖于这个先验的好坏。

程序说明
词嵌入
预处理代码
!cp /mnt/vol0/Kaggle/toxic/glove.840B.300d.zip ~/work !unzip glove.840B.300d.zip
示例代码
embeddings_index = {} f = open(EMBEDDING_FILE,"rb") for line in f: values = line.split() word = values[0] coefs = np.asarray(values[1:], dtype='float32') embeddings_index[word] = coefs f.close() # embeddings_index 是通过glove预训练词向量构造的一个字典,每个单词都有一个对应的300维度的词向量,词向量来源于glove的预训练。接着,我们构造了一个embedding_matrix,只取了排名靠前的10W单词,并且把词向量填充进embedding_matrix。 nb_words = min(MAX_NB_WORDS, len(word_index)) embedding_matrix = np.zeros((nb_words, EMBEDDING_DIM)) for word, i in word_index.items(): if i >= MAX_NB_WORDS: continue embedding_vector = embeddings_index.get(str.encode(word)) if embedding_vector is not None: # words not found in embedding index will be all-zeros. embedding_matrix[i] = embedding_vector # 接下来,我们对数据进行训练集和验证集的划分。 perm = np.random.permutation(len(data)) idx_train = perm[:int(len(data)*(1-VALIDATION_SPLIT))] idx_val = perm[int(len(data)*(1-VALIDATION_SPLIT)):] data_train=data[idx_train] labels_train=y[idx_train] print(data_train.shape,labels_train.shape) data_val=data[idx_val] labels_val=y[idx_val] print(data_val.shape,labels_val.shape)
程序验证过程
True




2. 深度学习

现在我们将这个词向量矩阵加载到Embedding层中,注意,我们设置trainable=False使得这个编码层不可再训练。需要注意的是,如果输入数据不需要词的语义特征,简单使用Embedding层就可以得到一个对应的词向量矩阵,但如果需要语义特征,我们就需要把glove预训练好的词向量扔到Embedding层中。

2.1 模型构建

接下来就是构造训练模型了,具体的构造代码如下:

程序说明
模型构建
示例代码
embedding_layer = Embedding(input_dim=10000, output_dim=300, weights=[embedding_matrix], input_length=150, trainable=False) class Attention(Layer): def __init__(self, step_dim, W_regularizer=None, b_regularizer=None, W_constraint=None, b_constraint=None, bias=True, **kwargs): """ Keras Layer that implements an Attention mechanism for temporal data. Supports Masking. Follows the work of Raffel et al. [https://arxiv.org/abs/1512.08756] # Input shape 3D tensor with shape: `(samples, steps, features)`. # Output shape 2D tensor with shape: `(samples, features)`. :param kwargs: Just put it on top of an RNN Layer (GRU/LSTM/SimpleRNN) with return_sequences=True. The dimensions are inferred based on the output shape of the RNN. Example: model.add(LSTM(64, return_sequences=True)) model.add(Attention()) """ self.supports_masking = True #self.init = initializations.get('glorot_uniform') self.init = initializers.get('glorot_uniform') self.W_regularizer = regularizers.get(W_regularizer) self.b_regularizer = regularizers.get(b_regularizer) self.W_constraint = constraints.get(W_constraint) self.b_constraint = constraints.get(b_constraint) self.bias = bias self.step_dim = step_dim self.features_dim = 0 super(Attention, self).__init__(**kwargs) def build(self, input_shape): assert len(input_shape) == 3 self.W = self.add_weight((input_shape[-1],), initializer=self.init, name='{}_W'.format(self.name), regularizer=self.W_regularizer, constraint=self.W_constraint) self.features_dim = input_shape[-1] if self.bias: self.b = self.add_weight((input_shape[1],), initializer='zero', name='{}_b'.format(self.name), regularizer=self.b_regularizer, constraint=self.b_constraint) else: self.b = None self.built = True def compute_mask(self, input, input_mask=None): # do not pass the mask to the next layers return None def call(self, x, mask=None): # eij = K.dot(x, self.W) TF backend doesn't support it # features_dim = self.W.shape[0] # step_dim = x._keras_shape[1] features_dim = self.features_dim step_dim = self.step_dim eij = K.reshape(K.dot(K.reshape(x, (-1, features_dim)), K.reshape(self.W, (features_dim, 1))), (-1, step_dim)) if self.bias: eij += self.b eij = K.tanh(eij) a = K.exp(eij) # apply mask after the exp. will be re-normalized next if mask is not None: # Cast the mask to floatX to avoid float64 upcasting in theano a *= K.cast(mask, K.floatx()) # in some cases especially in the early stages of training the sum may be almost zero a /= K.cast(K.sum(a, axis=1, keepdims=True) + K.epsilon(), K.floatx()) a = K.expand_dims(a) weighted_input = x * a #print weigthted_input.shape return K.sum(weighted_input, axis=1) def compute_output_shape(self, input_shape): #return input_shape[0], input_shape[-1] return input_shape[0], self.features_dim lstm_layer = LSTM(num_lstm, dropout=rate_drop_lstm, recurrent_dropout=rate_drop_lstm,return_sequences=True) comment_input = Input(shape=(MAX_SEQUENCE_LENGTH,), dtype='int32') embedded_sequences= embedding_layer(comment_input) x = lstm_layer(embedded_sequences) x = Dropout(rate_drop_dense)(x) merged = Attention(MAX_SEQUENCE_LENGTH)(x) merged = Dense(num_dense, activation=act)(merged) merged = Dropout(rate_drop_dense)(merged) merged = BatchNormalization()(merged) preds = Dense(6, activation='sigmoid')(merged)
程序验证过程
True


具体网络结构如图:


2.2 训练/预测

程序说明
训练/预测
示例代码
model = Model(inputs=[comment_input], \ outputs=preds) model.compile(loss='binary_crossentropy', optimizer='rmsprop', metrics=['accuracy']) print(model.summary()) STAMP = 'simple_lstm_glove_vectors_%.2f_%.2f'%(rate_drop_lstm,rate_drop_dense) print(STAMP) early_stopping =EarlyStopping(monitor='val_loss', patience=5) bst_model_path = STAMP + '.h5' model_checkpoint = ModelCheckpoint(bst_model_path, save_best_only=True, save_weights_only=True) hist = model.fit(data_train, labels_train, \ validation_data=(data_val, labels_val), \ epochs=50, batch_size=256, shuffle=True, \ callbacks=[early_stopping, model_checkpoint]) model.load_weights(bst_model_path) bst_val_score = min(hist.history['val_loss']) y_test = model.predict([test_data], batch_size=1024, verbose=1) sample_submission = pd.read_csv("sample_submission.csv") sample_submission[list_classes] = y_test sample_submission.to_csv('%.4f_'%(bst_val_score) + STAMP + '.csv', index=False)
程序验证过程
True


剩下的时间,自然是等待数据跑完,然后开心的提交结果,等待答案啦,文本分类的套路之旅到这就算是完成了。后续等待toxic比赛结束,我们还会在这个基础上,分享其他几个模型的套路和结果,目前测试CNN单模型能达到0.04分,优于本文的0.043,到时候我们会尝试来一个横向测评,看看究竟是哪个模型表现是最优秀的,大家拭目以待吧。

您也许喜欢这些文章

智能会议的五重境界

发表至业界新闻
语音识别设备已经开始应用于生活中,但目前还没能产生巨大的影响。本文作者总结出了一套用于视频会议AI的发展模式,预计在将来可能会主导公司的各种事务,发展潜力巨大。

用Python和Keras搭建你自己的AlphaZero

发表至趣味项目
本文主要是讲解如何搭建AlphaZero,因此假定你已经对AlphaZero的原理有了大致了解。如果不太熟悉的,可预先查阅一下相关资料。

Python搭建多层神经网络

发表至趣味项目
上一篇的单神经元网络,只能处理简单的线性问题,一旦相关的输入输出不再一一对应,而是涉及到多个参数的组合,便束手无策了。本篇文章将继续拓展之前的代码,在神经网络中加入隐藏层,使其能够处理非线性问题。

文章评论(1)

furch 发表于 3月前回复
不错不错,glove.840B.300d.txt文件可以在这里下载,https://nlp.stanford.edu/projects/glove/