之前参加了一个英文文本的分类比赛。比赛结束到了过年,加上开学又有一些事情,所以总结的工作就一直没有进行。现在空了一些,所以把之前的工作写一写,比赛中用到的代码也会放到github上。
对这个比赛的任务介绍以及数据可以看这篇写过的文章。简单说,就是一个英文文本的有监督二分类问题,对一条英文问句判断是否为一条有意义的问题。这篇文章里还对数据处理介绍了两种简单的方法,一个是embedding词向量,另一个是基于tfidf的统计特征抽取,两种方法分别是深度学习和机器学习在nlp领域对特征表示上常用的方法。整体上使用的Keras框架。
好,下面主要从两个方面对我们做的工作进行总结,一是数据处理和句子表示,另一个是模型的选取和融合。
对数据的处理首先是简单的数据清洗,包括标点符号的额整理,颜文字等特殊符号的整理,最后是大小写转换。因为这个比赛是不允许使用额外数据的,没法使用额外词库进行拼写错误的检查,所以拼写错误我们使用了一个手写的小字典进行纠正。
在进行完数据清洗后,我们使用Keras的tokenizer接口将句子进行符号化,为将来的向量化做准备。
下面是数据清洗的代码:这里贴两个数据清洗的代码
puncts = [',', '.', '"', ':', ')', '(', '-', '!', '?', '|', ';', "'", '$', '&', '/', '[', ']', '>', '%', '=', '#', '*', '+', '\\', '•', '~', '@', '£',
'·', '_', '{', '}', '©', '^', '®', '`', '<', '→', '°', '€', '™', '›', '♥', '←', '×', '§', '″', '′', 'Â', '█', '½', 'à', '…',
'“', '★', '”', '–', '●', 'â', '►', '−', '¢', '²', '¬', '░', '¶', '↑', '±', '¿', '▾', '═', '¦', '║', '―', '¥', '▓', '—', '‹', '─',
'▒', ':', '¼', '⊕', '▼', '▪', '†', '■', '’', '▀', '¨', '▄', '♫', '☆', 'é', '¯', '♦', '¤', '▲', 'è', '¸', '¾', 'Ã', '⋅', '‘', '∞',
'∙', ')', '↓', '、', '│', '(', '»', ',', '♪', '╩', '╚', '³', '・', '╦', '╣', '╔', '╗', '▬', '❤', 'ï', 'Ø', '¹', '≤', '‡', '√', ]
punct_mapping = {"‘": "'", "₹": "e", "´": "'", "°": "", "€": "e", "™": "tm", "√": " sqrt ", "×": "x", "²": "2", "—": "-", "–": "-", "’": "'", "_": "-", "`": "'",
'“': '"', '”': '"', '“': '"', "£": "e", '∞': 'infinity', 'θ': 'theta', '÷': '/', 'α': 'alpha', '•': '.', 'à': 'a', '−': '-', 'β': 'beta', '∅': '',
'³': '3', 'π': 'pi', }
def clean_text(x):
x = str(x)
for p in punct_mapping:
x = x.replace(p, punct_mapping[p])
for punct in puncts:
x = x.replace(punct, f' {punct} ')
return x
def clean_numbers(x):
x = re.sub('[0-9]{5,}', '#####', x)
x = re.sub('[0-9]{4}', '####', x)
x = re.sub('[0-9]{3}', '###', x)
x = re.sub('[0-9]{2}', '##', x)
return x
mispell_dict = {"ain't": "is not", "aren't": "are not","can't": "cannot"} // 这里简写了,为了看起来方便些,完整的字典可以去github上看
def _get_mispell(mispell_dict):
mispell_re = re.compile('(%s)' % '|'.join(mispell_dict.keys()))
return mispell_dict, mispell_re
mispellings, mispellings_re = _get_mispell(mispell_dict)
def replace_typical_misspell(text):
def replace(match):
return mispellings[match.group(0)]
return mispellings_re.sub(replace, text)
def load_and_prec():
train_df = pd.read_csv("../input/train.csv")
test_df = pd.read_csv("../input/test.csv")
# lower
train_df["question_text"] = train_df["question_text"].apply(lambda x: x.lower())
test_df["question_text"] = test_df["question_text"].apply(lambda x: x.lower())
# Clean the text
train_df["question_text"] = train_df["question_text"].apply(lambda x: clean_text(x))
test_df["question_text"] = test_df["question_text"].apply(lambda x: clean_text(x))
# Clean numbers
train_df["question_text"] = train_df["question_text"].apply(lambda x: clean_numbers(x))
test_df["question_text"] = test_df["question_text"].apply(lambda x: clean_numbers(x))
# Clean speelings
train_df["question_text"] = train_df["question_text"].apply(lambda x: replace_typical_misspell(x))
test_df["question_text"] = test_df["question_text"].apply(lambda x: replace_typical_misspell(x))
def clean_text(text):
print(text)
## Remove puncuation
text = text.translate(string.punctuation)
## Convert words to lower case and split them
text = text.lower()
## Remove stop words
#text = text.split()
#stops = set(stopwords.words("english"))
#text = [w for w in text if not w in stops and len(w) >= 3]
#text = " ".join(text)
# Clean the text
text = re.sub(r"[^A-Za-z0-9^,!.\/'+-=]", " ", text)
text = re.sub(r"what's", "what is ", text)
text = re.sub(r"\'s", " ", text)
text = re.sub(r"\'ve", " have ", text)
text = re.sub(r"n't", " not ", text)
text = re.sub(r"i'm", "i am ", text)
text = re.sub(r"\'re", " are ", text)
text = re.sub(r"\'d", " would ", text)
text = re.sub(r"\'ll", " will ", text)
text = re.sub(r",", " ", text)
text = re.sub(r"\.", " ", text)
text = re.sub(r"!", " ! ", text)
text = re.sub(r"\/", " ", text)
text = re.sub(r"\^", " ^ ", text)
text = re.sub(r"\+", " + ", text)
text = re.sub(r"\-", " - ", text)
text = re.sub(r"\=", " = ", text)
text = re.sub(r"'", " ", text)
text = re.sub(r"(\d+)(k)", r"\g<1>000", text)
text = re.sub(r":", " : ", text)
text = re.sub(r" e g ", " eg ", text)
text = re.sub(r" b g ", " bg ", text)
text = re.sub(r" u s ", " american ", text)
text = re.sub(r"\0s", "0", text)
text = re.sub(r" 9 11 ", "911", text)
text = re.sub(r"e - mail", "email", text)
text = re.sub(r"j k", "jk", text)
text = re.sub(r"\s{2,}", " ", text)
#text = text.split()
#stemmer = SnowballStemmer('english')
#stemmed_words = [stemmer.stem(word) for word in text]
#text = " ".join(stemmed_words)
print(text)
print("")
return text
在数据清洗完成后,就可以进行词向量的映射了。有两种方式,一种是使用预训练好的词向量,比如glove,paragram,wiki-news以及Google等等。另一种是自训练词向量,就是我们针对自己的数据单独训练词向量。我们两种方式都使用过,分别进行介绍。
因为我们有多个预训练的词向量可选,所以需要做一个选择工作,也就是哪一个词向量效果会更好一些。这一点上之前写了一篇文章,可以参考,这里就不多说了。通过分析,glove,paragram,wiki-news这三个词向量的表示是较好的,所以实验中使用了这三个。三个词向量的使用方式都是一样的,所以这里只贴出paragram的代码,其他的可以看github。
def load_para(word_index):
EMBEDDING_FILE = '../input/embeddings/paragram_300_sl999/paragram_300_sl999.txt'
def get_coefs(word,*arr): return word, np.asarray(arr, dtype='float32')
embeddings_index = dict(get_coefs(*o.split(" ")) for o in open(EMBEDDING_FILE, encoding="utf8", errors='ignore') if len(o)>100)
all_embs = np.stack(embeddings_index.values())
emb_mean,emb_std = -0.0053247833,0.49346462
embed_size = all_embs.shape[1]
print(emb_mean,emb_std,"para")
# word_index = tokenizer.word_index
nb_words = min(max_features, len(word_index))
embedding_matrix = np.random.normal(emb_mean, emb_std, (nb_words, embed_size))
for word, i in word_index.items():
if i >= max_features: continue
embedding_vector = embeddings_index.get(word)
if embedding_vector is not None: embedding_matrix[i] = embedding_vector
return embedding_matrix
自训练的词向量这篇文章中有介绍,其中也贴了完整的代码,这里就不多说了。使用自训练的词向量要比使用预训练的词向量效果要好一些,毕竟是更为贴近语料的。
上面词向量的表示方法会将每一个单词表示成一个300维的向量,这样一个句子就被表示成了一个二维的矩阵。但有时候我们的需求是让一个句子被表示为一个一维的向量,也就是我们这里要说的句向量的表示方法。句向量的表示方法有很多,也是
工业界经常使用的,面试时也多次被问到对这方面的了解,有基于统计的方法,也有基于神经网络的方法,这里就不展开讲了,感兴趣的同学可以查一下。
这里想说的是在这个比赛中用到的一种句向量的表示方法,也是一个创新的地方,是将TFIDF与embedding词向量融合的表示方法。简单来说就是将tfidf视为一种权重,将embedding词向量进行加权表示,以得到一种句向量的表示。这一部分在之前也介绍过,具体可以查看这两篇文章:文本分类实战—tfidf+embedding—1,文本分类实战–-tfidf+embedding—2。
到这里,我们就把数据的表示方法介绍完了,下面的工作就是将得到的表示向量放大分类模型中进行分类训练了。工作主要集中在模型的选取以及调参上。
模型的选择上我们开始尝试了很多模型,总的来说,深度学习模型会比机器学习模型效果好,所最终使用的模型是深度的,但他们内部之间的差异不大,一般说来是LSTM和GRU比CNN好一些,胶囊网络效果与LSTM差不多,加入attention机制会比不好一些。
这一部分我们只把建立模型的主体代码贴出来,整个项目的代码可以从后面给出的github地址中查看。
下面贴出实验中用到的模型:
xgboost是结合tfidf以及句子特征做的。完整代码可以参见:https://github.com/pnnngchg/kaggle_quora/blob/master/xgboost-tfidf.ipynb
def build_xgb(train_X, train_y, valid_X, valid_y=None, subsample=0.75):
xgtrain = xgb.DMatrix(train_X, label=train_y)
if valid_y is not None:
xgvalid = xgb.DMatrix(valid_X, label=valid_y)
else:
xgvalid = None
model_params = {}
# binary 0 or 1
model_params['objective'] = 'binary:logistic'
# eta is the learning_rate, [default=0.3]
model_params['eta'] = 0.3
# depth of the tree, deeper more complex.
model_params['max_depth'] = 6
# 0 [default] print running messages, 1 means silent mode
model_params['silent'] = 1
model_params['eval_metric'] = 'auc'
# will give up further partitioning [default=1]
model_params['min_child_weight'] = 1
# subsample ratio for the training instance
model_params['subsample'] = subsample
# subsample ratio of columns when constructing each tree
model_params['colsample_bytree'] = subsample
# random seed
model_params['seed'] = 2018
# imbalance data ratio
#model_params['scale_pos_weight'] =
# convert params to list
model_params = list(model_params.items())
return xgtrain, xgvalid, model_params
def train_xgboost(xgtrain, xgvalid, model_params, num_rounds=500, patience=20):
if xgvalid is not None:
# watchlist what information should be printed. specify validation monitoring
watchlist = [ (xgtrain, 'train'), (xgvalid, 'test') ]
#early_stopping_rounds = stop if performance does not improve for k rounds
model = xgb.train(model_params, xgtrain, num_rounds, watchlist, early_stopping_rounds=patience)
else:
model = xgb.train(model_params, xgtrain, num_rounds)
return model
lightgbm对数据的处理方式以及表示方式与xgboost是一样的,只是模型进行了改变。https://github.com/pnnngchg/kaggle_quora/blob/master/lightgbm.ipynb
# create dataset for lightgbm
lgb_train = lgb.Dataset(input_train, y_train )
lgb_eval = lgb.Dataset(input_valid, y_val, reference=lgb_train)
# specify your configurations as a dict
params = {
'boosting_type': 'gbdt',
'objective': 'binary',
'metric': {'binary_logloss', 'auc'}, #二进制对数损失
'num_leaves': 5,
'max_depth': 6,
'min_data_in_leaf': 450,
'learning_rate': 0.1,
'feature_fraction': 0.9,
'bagging_fraction': 0.95,
'bagging_freq': 5,
'lambda_l1': 1,
'lambda_l2': 0.001, # 越小l2正则程度越高
'min_gain_to_split': 0.2,
'verbose': 5,
'is_unbalance': True
}
# train
print('Start training...')
gbm = lgb.train(params,
lgb_train,
num_boost_round=10000,
valid_sets=lgb_eval,
early_stopping_rounds=500)
print('Start predicting...')
preds = gbm.predict(input_valid, num_iteration=gbm.best_iteration) # 输出的是概率结果
胶囊网络模型准确率很高,但是训练时间也很长,主要是因为动态路由机制需要迭代,并且对向量的处理要比对标量的计算规模更大。从胶囊网络开始下面的网络都是深度学习的网络了,使用的是Keras框架,数据的表示也就都是二维的了。
https://github.com/pnnngchg/kaggle_quora/blob/master/capsule.ipynb
def squash(x, axis=-1):
s_squared_norm = K.sum(K.square(x), axis, keepdims=True)
scale = K.sqrt(s_squared_norm + K.epsilon())
return x / scale
class Capsule(Layer):
def __init__(self, num_capsule, dim_capsule, routings=3, kernel_size=(9, 1), share_weights=True,
activation='default', **kwargs):
super(Capsule, self).__init__(**kwargs)
self.num_capsule = num_capsule
self.dim_capsule = dim_capsule
self.routings = routings
self.kernel_size = kernel_size
self.share_weights = share_weights
if activation == 'default':
self.activation = squash
else:
self.activation = Activation(activation)
def build(self, input_shape):
super(Capsule, self).build(input_shape)
input_dim_capsule = input_shape[-1]
if self.share_weights:
self.W = self.add_weight(name='capsule_kernel',
shape=(1, input_dim_capsule,
self.num_capsule * self.dim_capsule),
# shape=self.kernel_size,
initializer='glorot_uniform',
trainable=True)
else:
input_num_capsule = input_shape[-2]
self.W = self.add_weight(name='capsule_kernel',
shape=(input_num_capsule,
input_dim_capsule,
self.num_capsule * self.dim_capsule),
initializer='glorot_uniform',
trainable=True)
def call(self, u_vecs):
if self.share_weights:
u_hat_vecs = K.conv1d(u_vecs, self.W)
else:
u_hat_vecs = K.local_conv1d(u_vecs, self.W, [1], [1])
batch_size = K.shape(u_vecs)[0]
input_num_capsule = K.shape(u_vecs)[1]
u_hat_vecs = K.reshape(u_hat_vecs, (batch_size, input_num_capsule,
self.num_capsule, self.dim_capsule))
u_hat_vecs = K.permute_dimensions(u_hat_vecs, (0, 2, 1, 3))
# final u_hat_vecs.shape = [None, num_capsule, input_num_capsule, dim_capsule]
b = K.zeros_like(u_hat_vecs[:, :, :, 0]) # shape = [None, num_capsule, input_num_capsule]
for i in range(self.routings):
b = K.permute_dimensions(b, (0, 2, 1)) # shape = [None, input_num_capsule, num_capsule]
c = K.softmax(b)
c = K.permute_dimensions(c, (0, 2, 1))
b = K.permute_dimensions(b, (0, 2, 1))
outputs = self.activation(tf.keras.backend.batch_dot(c, u_hat_vecs, [2, 2]))
if i < self.routings - 1:
b = tf.keras.backend.batch_dot(outputs, u_hat_vecs, [2, 3])
return outputs
def compute_output_shape(self, input_shape):
return (None, self.num_capsule, self.dim_capsule)
给出两种CNN,一维的和二维的。因为我们最终模型融合使用的5个模型,CNN,LSTM(两个),GRU(两个),他们的整体代码在同一个ipynb文件中。https://github.com/pnnngchg/kaggle_quora/blob/master/embedding-tfidf-blend-models.ipynb
def model_conv1d():
filters = 128
inp = Input(shape=(maxlen, ))
embed = Embedding(max_features, embed_size * 3, weights=[embedding_matrix], trainable=False)(inp)
x = embed
x = Conv1D(filters, 1, activation='relu')(x)
x = Dropout(0.1)(x)
x = Conv1D(filters, 2, activation='relu')(x)
x = Dropout(0.1)(x)
x = Conv1D(filters, 3, activation='relu')(x)
x = Dropout(0.1)(x)
x = Conv1D(filters, 5, activation='relu')(x)
x = Dropout(0.1)(x)
#x = Flatten()(x)
x = GlobalAveragePooling1D()(x)
x = Dropout(0.3)(x)
x = Dense(128, activation='relu')(x)
outp = Dense(1, activation="sigmoid")(x)
model = Model(inputs=inp, outputs=outp)
model.compile(loss='binary_crossentropy',
optimizer='adam',
metrics=['accuracy'])
return model
def model_cnn(embedding_matrix): # 1 epoch enough or none
filter_sizes = [1,2,3,5]
num_filters = 36
# inp = Input(shape=(maxlen,))
inp = Input(shape=(maxlen*2, )) # (?, 140)
inp_emb = Lambda(row_slice, arguments={'start':0, 'end':70})(inp) # (?, 70)
inp_emb = Lambda(change_type)(inp_emb) # tensor
x = Embedding(max_features, embed_size*2, weights=[embedding_matrix])(inp_emb)
x = Reshape((maxlen, embed_size*2, 1))(x)
maxpool_pool = []
for i in range(len(filter_sizes)):
conv = Conv2D(num_filters, kernel_size=(filter_sizes[i], embed_size*2),
kernel_initializer='he_normal', activation='elu')(x)
maxpool_pool.append(MaxPool2D(pool_size=(maxlen - filter_sizes[i] + 1, 1))(conv))
z = Concatenate(axis=1)(maxpool_pool)
z = Flatten()(z)
z = Dropout(0.1)(z)
outp = Dense(1, activation="sigmoid")(z)
model = Model(inputs=inp, outputs=outp)
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
return model
数据表示方式是embedding词向量
def model_lstm_atten(embedding_matrix): # 2 epoches enough
inp = Input(shape=(maxlen,))
x = Embedding(max_features, embed_size*2, weights=[embedding_matrix], trainable=False)(inp)
x = Bidirectional(CuDNNLSTM(128, return_sequences=True))(x)
x = Bidirectional(CuDNNLSTM(64, return_sequences=True))(x)
x = Attention(maxlen)(x)
x = Dense(64, activation="relu")(x)
x = Dense(1, activation="sigmoid")(x)
model = Model(inputs=inp, outputs=x)
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
return model
数据表示方式是embedding词向量结合tfidf
def model_gru_srk_atten(embedding_matrix): # 2 epoches enough
# inp = Input(shape=(maxlen,))
inp = Input(shape=(maxlen*2, )) # (?, 140)
inp_emb = Lambda(row_slice, arguments={'start':0, 'end':70})(inp) # (?, 70)
inp_emb = Lambda(change_type)(inp_emb) # tensor
x = Embedding(max_features, embed_size*2, weights=[embedding_matrix])(inp_emb)
x = Bidirectional(CuDNNGRU(64, return_sequences=True))(x)
x = Attention(maxlen)(x) # New
x = Dense(16, activation="relu")(x)
x = Dropout(0.1)(x)
x = Dense(1, activation="sigmoid")(x)
model = Model(inputs=inp, outputs=x)
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
return model
数据表示方式是embedding词向量结合tfidf
def model_lstm_du(embedding_matrix): # 3 epoches enough
# inp = Input(shape=(maxlen,))
inp = Input(shape=(maxlen*2, )) # (?, 140)
inp_emb = Lambda(row_slice, arguments={'start':0, 'end':70})(inp) # (?, 70)
inp_emb = Lambda(change_type)(inp_emb) # tensor
x = Embedding(max_features, embed_size*2, weights=[embedding_matrix])(inp_emb)
x = Bidirectional(CuDNNGRU(64, return_sequences=True))(x)
avg_pool = GlobalAveragePooling1D()(x)
max_pool = GlobalMaxPooling1D()(x)
conc = concatenate([avg_pool, max_pool])
conc = Dense(64, activation="relu")(conc)
conc = Dropout(0.1)(conc)
outp = Dense(1, activation="sigmoid")(conc)
model = Model(inputs=inp, outputs=outp)
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
return model
def model_gru_atten_3(embedding_matrix): # 3 epoches enough
# inp = Input(shape=(maxlen,))
inp = Input(shape=(maxlen*2, )) # (?, 140)
inp_emb = Lambda(row_slice, arguments={'start':0, 'end':70})(inp) # (?, 70)
inp_emb = Lambda(change_type)(inp_emb) # tensor
x = Embedding(max_features, embed_size*2, weights=[embedding_matrix], trainable=False)(inp_emb)
x = Bidirectional(CuDNNGRU(128, return_sequences=True))(x)
x = Bidirectional(CuDNNGRU(100, return_sequences=True))(x)
x = Bidirectional(CuDNNGRU(64, return_sequences=True))(x)
x = Attention(maxlen)(x)
x = Dense(1, activation="sigmoid")(x)
model = Model(inputs=inp, outputs=x)
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
return model
到这里,实验中主要用到的模型就介绍完了。需要注意的是,机器学习的模型都是用了特征向量和tfidf作为输入,深度学习的模型可以接受二维的输入,比如embedding和embedding与tfidf融合。
我们最终的结果是通过模型融合得出的,具体做法是,我们分别在训练集上训练5个模型,然后针对结果使用LR尝试得到每个模型的融合系数,这里的融合系数就是LR的回归系数,然后在验证集上用融合后的模型计算最佳阈值。
高准确率的模型通常会消耗更多的训练时间,在这个任务中,时间消耗最多的是胶囊网络和LSTM,CNN会快一些。为了平衡准确率和时间的问题,在模型融合时,我们选取了5个模型进行了融合。
计算融合系数的代码如下:
from sklearn.linear_model import LinearRegression
X = np.asarray([outputs[i][0] for i in range(len(outputs))])
X = X[...,0]
reg = LinearRegression().fit(X.T, y_val)
print(reg.score(X.T, y_val),reg.coef_)
计算阈值的代码如下:
pred_val_y = np.sum([outputs[i][0] * reg.coef_[i] for i in range(len(outputs))], axis = 0)
# pred_val_y = np.mean([outputs[i][0] for i in range(len(outputs))], axis = 0) # to avoid overfitting, just take average
thresholds = []
for thresh in np.arange(0.1, 0.501, 0.01):
thresh = np.round(thresh, 2)
res = metrics.f1_score(y_val, (pred_val_y > thresh).astype(int))
thresholds.append([thresh, res])
print("F1 score at threshold {0} is {1}".format(thresh, res))
thresholds.sort(key=lambda x: x[1], reverse=True)
best_thresh = thresholds[0][0]
print("Best threshold: ", best_thresh)
最终的结果:
pred_test_y = np.sum([outputs[i][1] * reg.coef_[i] for i in range(len(outputs))], axis = 0)
# pred_test_y = np.mean([outputs[i][1] for i in range(len(outputs))], axis = 0)
pred_test_y = (pred_test_y > best_thresh).astype(int)
test_df = pd.read_csv("../input/test.csv", usecols=["qid"])
out_df = pd.DataFrame({"qid":test_df["qid"].values})
out_df['prediction'] = pred_test_y
out_df.to_csv("submission.csv", index=False)
在这里简单做一下总结:
1.词向量的选取很重要,不同的词向量一般效果是不一样的。另外,自训练的词向量一般会比与训练好的词向量好一些,但这只是针对确定的数据集而言的,如果数据集发生变化,那么自训练的词向量效果会变差。还有一种选择是,使用预训练好的词向量,但是在训练模型的过程中对词向量进行微调。
2.找nlp相关工作的同学需要关注一下句向量的表示方法,工业界貌似对将一句话表示成为一个一维向量很看重。
3.深度学习模型会比机器学习模型效果好一些,但这是建立在大量时间消耗的基础上的。同时深度学习模型对输入的要求也会宽泛一些,可以做端到端的应用。
4.模型其实差别不会太大。模型的融合时提升准确率的利器,bagging,boosting等等都需要了解一些。
5.并不是数据处理的越干净越好,要视具体任务而言。比如在这个任务中,如果过多的处理掉一些杂乱的符号,最终的分类效果反而不好。我猜测这可能是因为包含这个杂乱符号的句子更多的是无意义的句子,而有意义的句子本身更加标准整洁,所以有没有这个符号本身就是一种区分的特征,如果把这些特征去掉,反而效果不好了。
6.交叉验证是提升效果的重要方法
7.数据不平衡的问题,这个问题其实我没有搞清楚,如果测试集与训练集同样是不平衡的数据,我们是否需要针对数据不平衡问题做处理?我们尝试过使用过采样和降采样对训练数据进行处理,结果是验证集准确率大大提升,但是测试集下降很多。因为我们看不到测试集数据的分布,所以对这个问题一直没有弄清楚。如果有同学了解,欢迎留言,学习一下~~
8.另外还有一些模型和代码是在实验中尝试过的,一起放到github上了。有兴趣可以看一下。
https://github.com/pnnngchg/kaggle_quora