今年暑假和朋友(微信公众号:StudyForAI (AI小白入门))一起组队参加了这个比赛,本来以为小比赛人少,没想到参加的人会有几千人。
当时是一边在公司实习一边参加这个比赛,差不多实习结束比赛也就结束了。最后我们队伍取得第四名,虽有很多遗憾,毕竟大佬太多,所以在这里也和队友总结了一份Rank 4的方案,并开源了部分代码,希望对刚接触这个领域的小白新手能有些帮助~~~
竞赛背景
2018年人工智能的发展在运算智能和感知智能已经取得了很大的突破和优于人类的表现。而在以理解人类语言为入口的认知智能上,目前达观数据自然语言处理技术已经可以实现文档自动解析、关键信息提取、文本分类审核、文本智能纠错等一定基础性的文字处理工作,并在各行各业得到充分应用。
自然语言处理一直是人工智能领域的重要话题,而人类语言的复杂性也给 NLP 布下了重重困难等待解决。长文本的智能解析就是颇具挑战性的任务,如何从纷繁多变、信息量庞杂的冗长文本中获取关键信息,一直是文本领域难题。随着深度学习的热潮来临,有许多新方法来到了 NLP 领域,给相关任务带来了更多优秀成果,也给大家带来了更多应用和想象的空间。
此次比赛,达观数据提供了一批长文本数据和分类信息,希望选手动用自己的智慧,结合当下最先进的NLP和人工智能技术,深入分析文本内在结构和语义信息,构建文本分类模型,实现精准分类。未来文本自动化处理的技术突破和应用落地需要人工智能从业者和爱好者的共同努力,相信文本智能处理技术因为你的算法,变得更加智能!
比赛网址
“达观杯”文本智能处理挑战赛-竞赛信息-DC竞赛http://www.dcjingsai.com/common/cmpt/%E2%80%9C%E8%BE%BE%E8%A7%82%E6%9D%AF%E2%80%9D%E6%96%87%E6%9C%AC%E6%99%BA%E8%83%BD%E5%A4%84%E7%90%86%E6%8C%91%E6%88%98%E8%B5%9B_%E7%AB%9E%E8%B5%9B%E4%BF%A1%E6%81%AF.html
解决方案
对于这个比赛,我们尝试了很多方法,最后我们发现了一些对解决这个赛题很不错的解决方案(当然,对于其他任务也可以起到不错的效果)。总结如下:
通过对于词向量做一个增强,即利用word2vec与glove的差异性,构建一个鲁棒性更高的词语向量表征。具体而言,我们对于每个词通过word2vec以及glove分别训练出两个200维度的词量,两种向量相结合,表征出更强的语义信息,从而得到一个 400维度的词向量。大家也可以word2vec+glove+faxttext的组合,对于我来说,效果并不是很好,我觉得可能的原因是faxttext与word2vec的相似性很高,弱化了glove的向量表征,同时,对于glove单独的词向量我也没有尝试过,大家也可以尝试一下。
对于模型的话,选择了一个比较简单的模型,两个双向的GRU模型,然后分别平均池化和最大池化,最后接到FC层。
训练的方式:训练模型我们固定了100个epoch,然后使用早停的策略,根据验证集上的性能,选择验证数据集上具有最佳准确率分数的模型作为最终模型,并评估其在测试数据集上的性能。
具体代码如下
导入数据集
数据集路径根据自己的路径设置。
train=feather.read_dataframe("../data/train_set.feather")
test=feather.read_dataframe("../data/test_set.feather")
词向量维度选取以及句子长度截断一般方法
对于句子长度选择:统计下每条句子的长度,一般可以看置信区间在90,95,98的句子长度,比如有三条句子,句子中分别有2000,1800,1900个词。因此,基于此代码np.percentile([2000,1800,1900],95),可以得到置信区间在95的句子长度为1990。
对于词向量的维度:可以尝试100,200,300等,对于不同的维度,测试模型的效果,从而选择一个相对较好的维度。
def w2v_pad(df_train,df_test,col, maxlen_,victor_size):
tokenizer = text.Tokenizer(num_words=args.num_words, lower=False,filters="")
tokenizer.fit_on_texts(list(df_train[col].values)+list(df_test[col].values))
train_ = sequence.pad_sequences(tokenizer.texts_to_sequences(df_train[col].values), maxlen=maxlen_)
test_ = sequence.pad_sequences(tokenizer.texts_to_sequences(df_test[col].values), maxlen=maxlen_)
word_index = tokenizer.word_index
count = 0
nb_words = len(word_index)
print(nb_words)
all_data=pd.concat([df_train[col],df_test[col]])
file_name = '../embedding/' + 'Word2Vec_' + col +"_"+ str(victor_size) + '.model'
if not os.path.exists(file_name):
model = Word2Vec([[word for word in document.split(' ')] for document in all_data.values],
size=victor_size, window=5, iter=10, workers=11, seed=2018, min_count=2)
model.save(file_name)
else:
model = Word2Vec.load(file_name)
print("add word2vec finished....")
glove_model = {}
with open("../embedding/glove_vectors_word.txt",encoding='utf8') as f:
for line in f:
values = line.rstrip().rsplit(' ')
word = values[0]
coefs = np.asarray(values[1:], dtype='float32')
glove_model[word] = coefs
print("add glove finished....")
embedding_word2vec_matrix = np.zeros((nb_words + 1, victor_size))
for word, i in word_index.items():
embedding_vector = model[word] if word in model else None
if embedding_vector is not None:
count += 1
embedding_word2vec_matrix[i] = embedding_vector
else:
unk_vec = np.random.random(victor_size) * 0.5
unk_vec = unk_vec - unk_vec.mean()
embedding_word2vec_matrix[i] = unk_vec
glove_count=0
embedding_glove_matrix = np.zeros((nb_words + 1, victor_size))
for word, i in word_index.items():
embedding_glove_vector=glove_model[word] if word in glove_model else None
if embedding_glove_vector is not None:
glove_count += 1
embedding_glove_matrix[i] = embedding_glove_vector
else:
unk_vec = np.random.random(victor_size) * 0.5
unk_vec = unk_vec - unk_vec.mean()
embedding_glove_matrix[i] = unk_vec
embedding_matrix=np.concatenate((embedding_word2vec_matrix,embedding_glove_matrix),axis=1)
print (embedding_matrix.shape, train_.shape, test_.shape, count * 1.0 / embedding_matrix.shape[0],glove_count*1.0/embedding_matrix.shape[0])
return train_, test_, word_index, embedding_matrix
模型构建
本次开源一个比较简单的模型,利用双层的BiGRU模型来学习文本的上下文表征,然后通过池化操作,分别提取每一个时间戳的最大池化和平均池化。最后通过FC层的训练,构建文本分类模型,实现精准分类。
def bi_gru_model(sent_length, embeddings_weight,class_num):
print("get_text_gru3")
content = Input(shape=(sent_length,), dtype='int32')
embedding = Embedding(
name="word_embedding",
input_dim=embeddings_weight.shape[0],
weights=[embeddings_weight],
output_dim=embeddings_weight.shape[1],
trainable=False)
x = SpatialDropout1D(0.2)(embedding(content))
x = Bidirectional(CuDNNGRU(200, return_sequences=True))(x)
x = Bidirectional(CuDNNGRU(200, return_sequences=True))(x)
avg_pool = GlobalAveragePooling1D()(x)
max_pool = GlobalMaxPooling1D()(x)
conc = concatenate([avg_pool, max_pool])
x = Dropout(0.2)(Activation(activation="relu")(BatchNormalization()(Dense(1000)(conc))))
x = Activation(activation="relu")(BatchNormalization()(Dense(500)(x)))
output = Dense(class_num, activation="softmax")(x)
model = Model(inputs=content, outputs=output)
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
return model
模型训练
对于模型的训练,我们采用10折交叉进行训练,为了减少时间,可以进行5折交叉进行训练。训练模型我们固定了100个epoch,然后使用早停的策略,根据验证集上的性能,选择验证数据集上具有最佳准确率分数的模型作为最终模型,并评估其在测试数据集上的性能。
def word_model_cv(my_opt):
#参数
lb = LabelEncoder()
train_label = lb.fit_transform(train['class'].values)
train_label = to_categorical(train_label)
if not os.path.exists("../cache/"+my_opt):
os.mkdir("../cache/"+my_opt)
#模型
my_opt=eval(my_opt)
name = str(my_opt.__name__)
kf = KFold(n_splits=args.KFold, shuffle=True, random_state=520).split(train_)
train_model_pred = np.zeros((train_.shape[0], args.classification))
test_model_pred = np.zeros((test_.shape[0], args.classification))
for i, (train_fold, test_fold) in enumerate(kf):
X_train, X_valid, = train_[train_fold, :], train_[test_fold, :]
y_train, y_valid = train_label[train_fold], train_label[test_fold]
print(i, 'fold')
the_path = '../cache/' + name +'/' + name + "_" +args.column_name
model = my_opt(word_seq_len, word_embedding,args.classification)
early_stopping = EarlyStopping(monitor='val_acc', patience=6)
plateau = ReduceLROnPlateau(monitor="val_acc", verbose=1, mode='max', factor=0.5, patience=3)
checkpoint = ModelCheckpoint(the_path + str(i) + '.hdf5', monitor='val_acc', verbose=2, save_best_only=True, mode='max',save_weights_only=True)
if not os.path.exists(the_path + str(i) + '.hdf5'):
print("error")
model.fit(X_train, y_train,
epochs=100,
batch_size=args.batch_size,
validation_data=(X_valid, y_valid),
callbacks=[early_stopping, plateau, checkpoint],
verbose=2)
model.load_weights(the_path + str(i) + '.hdf5')
print (name + ": valid's accuracy: %s" % f1_score(lb.inverse_transform(np.argmax(y_valid, 1)),
lb.inverse_transform(np.argmax(model.predict(X_valid), 1)).reshape(-1,1),
average='micro'))
train_model_pred[test_fold, :] = model.predict(X_valid)
test_model_pred += model.predict(test_)
del model; gc.collect()
K.clear_session()
#线下测试
print (name + ": offline test score: %s" % f1_score(lb.inverse_transform(np.argmax(train_label, 1)),
lb.inverse_transform(np.argmax(train_model_pred, 1)).reshape(-1,1),
average='micro'))
写在后面
本项目主要利用“达观杯”文本比赛的数据,介绍了长文本分类问题的解决方法。我们通过利用两层的BiGRU捕捉长文本的上下文信息,然后通过池化层捕捉最关键的文本特征,接着两层的全连接层对文本的关键特征进行训练,从而构建文本分类模型,实现精准分类。另外,我们通过10折交叉的方式,测试集线上的分数可以达到0.798,仅仅这个单模型可以达到线上前10的效果(在线上赛时)。另外,为了节省运行时间,可以将10折交叉变成5折交叉,减少运行时间。同时,也可以将BiGRU减少一层。
如果您需要运行此代码
可以参考开源代码,github地址: https://github.com/hecongqing/2018-daguan-competition
推荐阅读
Recommended reading
关于我们
AI算法之心是一个介绍python、pyspark、机器学习、自然语言处理、深度学习、算法竞赛的平台。不管你是刚入门的小白,还是资深的算法大佬,欢迎扫一扫下方的二维码与我们在AI的领域中一起学习成长!
喜欢我就给我好看吧