百度千言-中文文本相似度实战

文章目录

  • 百度千言-中文文本相似度实战
      • 任务1:报名比赛,下载比赛数据集并完成读取
      • 任务2:对句子对提取TFIDF以及统计特征,训练和预测
      • 任务3:加载中文词向量,自己训练中文词向量
      • 任务4:使用中文词向量完成mean/max/sif句子编码
      • 任务5:搭建SiamCNN/LSTM模型,训练和预测
      • 任务6:搭建InferSent模型,训练和预测
        • 6.1 模型搭建与训练
        • 6.2 使用不同交叉方法训练结果
      • 任务7:搭建ESIM模型,训练和预测
        • 7.1 ESIM模型搭建与训练
        • 7.2 结果分析
      • 任务8:使用BERT或ERNIE完成NSP任务
      • 任务9:Bert-flow、Bert-white、SimCSE

百度千言-中文文本相似度实战

任务1:报名比赛,下载比赛数据集并完成读取

  • 步骤1 :登录&报名比赛:https://aistudio.baidu.com/aistudio/competition/detail/45/0/task-definition
    已报名
  • 步骤2 :下载比赛数据集
  • 步骤3 :使用Pandas完成数据读取。
    数据读取:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
#隐藏警告
import warnings
warnings.filterwarnings('ignore')

def read_tsv(input_file,columns):
    with open(input_file,"r",encoding="utf-8") as file:
        lines = []
        count = 1
        for line in file:
            if len(line.strip().split("\t")) != 1:
                lines.append([count]+line.strip().split("\t"))
                count += 1
        df = pd.DataFrame(lines)
        df.columns = columns
    return df

bq_train = read_tsv('data/bq_corpus/train.tsv',['index','sentences1','sentences2','label'])
bq_train.head()

百度千言-中文文本相似度实战_第1张图片

任务2:对句子对提取TFIDF以及统计特征,训练和预测

参考代码:kaggle参考案例

  • 步骤1 :对句子对(句子A和句子B统计)如下特征
    • 句子A包含的字符个数、句子B包含的字符个数
    • 句子A与句子B的编辑距离
    • 句子A与句子B共有单词的个数
    • 句子A与句子B共有字符的个数
    • 句子A与句子B共有单词的个数 / 句子A字符个数
    • 句子A与句子B共有单词的个数 / 句子B字符个数

文本预处理

  • 1、文本中的表情符号去除
  • 2、大写字母转化为小写字母
  • 3、仅保留中文(这个未尝试)
#大写字母转为小写字母
def upper2lower(sentence):
    new_sentence=sentence.lower()
    return new_sentence
bq_train['chinese_sentences1'] = bq_train['sentences1'].apply(upper2lower)
bq_train['chinese_sentences2'] = bq_train['sentences2'].apply(upper2lower)

#去除文本中的表情字符(只保留中英文和数字)
import re
def clear_character(sentence):
    pattern1= '\[.*?\]'     
    pattern2 = re.compile('[^\u4e00-\u9fa5^a-z^A-Z^0-9]')   
    line1=re.sub(pattern1,'',sentence)
    line2=re.sub(pattern2,'',line1)   
    new_sentence=''.join(line2.split()) #去除空白
    return new_sentence
bq_train['chinese_sentences1'] = bq_train['chinese_sentences1'].apply(clear_character)
bq_train['chinese_sentences2'] = bq_train['chinese_sentences2'].apply(clear_character)
#统计句子长度分布:
sentenses1 = pd.Series(bq_train['chinese_sentences1'].tolist()).astype(str)
sentences2 = pd.Series(bq_train['chinese_sentences2'].tolist()).astype(str)
dist1 = sentenses1.apply(len)
dist2 = sentences2.apply(len)
plt.figure(figsize=(15,10))
plt.hist(dist1, bins=200, range=[0,200], color='red', label='sentences1')
plt.hist(dist2, bins=200, range=[0,200], color='blue', label='sentences2')
plt.title('Sentences length', fontsize=15)
plt.legend()
plt.xlabel('Number of characters', fontsize=15)
plt.ylabel('Frequency', fontsize=15)
plt.show()

百度千言-中文文本相似度实战_第2张图片

统计句子A和句子B的符号数量:

bq_train['sentences1_length'], bq_train['sentences2_length'] = pd.DataFrame(dist1), pd.DataFrame(dist2)

句子A与句子B的编辑距离

# Compute edit dstance
import distance

def edit_distance(s1, s2):
    return distance.levenshtein(s1, s2)

bq_train["edit_distance"] = bq_train.apply(lambda x: edit_distance(x["chinese_sentences1"], x["chinese_sentences2"]), axis=1)

plt.hist(bq_train['edit_distance'])
plt.xlabel("Edit distance")
plt.ylabel('Frequency')
plt.title('the Edit Distance of Chinese Sentences 1 and Chinese Sentences2')
plt.show();
#可见,sentence1和sentence2的编辑距离大部分落在1~20之间

百度千言-中文文本相似度实战_第3张图片

句子A与句子B共有单词(word)的个数
句子A和句子B共有字符(char)的个数

#计算共有字符chars/Word
from collections import Counter
#计算两个句子相同在字符个数
def count_sample_char(sentence1, sentence2, WORD=False):
    if WORD:
        #说明此时输入的是'jieba_sen_{1/2}',要以','拆分成列表,再建词汇表。
        sentence1 = sentence1.split(',')
        sentence2 = sentence2.split(',')
    #构造两个句子的词汇表
    vocab1 = Counter(list(sentence1))
    vocab2 = Counter(list(sentence2))
    samekey_num = 0
    #判断keys()有没有相同的
    for key in vocab1.keys():
        if key in vocab2.keys():
            samekey_num += min(vocab1[key], vocab2[key])
        else:
            continue
    return samekey_num

def count_df_char(sentences1, sentences2, WORD=False):
    length = sentences1.shape[0]
    num_char = []
    for i in range(length):
        num_char.append(count_sample_char(sentences1[i], sentences2[i], WORD))
    return pd.DataFrame(num_char)
    
bq_train['same_char_num'] = count_df_char(bq_train['chinese_sentences1'], bq_train['chinese_sentences2'])
bq_train['same_word_num'] = count_df_char(bq_train['jieba_sen1'], bq_train['jieba_sen2'], WORD=True)

句子A与句子B共有单词的个数 / 句子A字符个数
句子A与句子B共有单词的个数 / 句子B字符个数

#句子A与句子B共有单词的个数 / 句子A字符个数
#句子A与句子B共有单词的个数 / 句子B字符个数
bq_train['sen1&sen2/sen1'] = bq_train['same_char_num']/bq_train['sentences1_length']
bq_train['sen1&sen2/sen2'] = bq_train['same_char_num']/bq_train['sentences2_length']

数据处理完毕!

  • 步骤2 :计算TFIDF,并对句子A和句子B进行特征转换
  • 步骤3 :计算句子A与句子B的TFIDF向量的内积距离
#将两个句子转换为tfidf向量,并计算两个tfidf向量内积
from sklearn.feature_extraction.text import TfidfVectorizer
def sentences_tfidf_inner(sentences1, sentences2):
    length = sentenses1.shape[0]
    tfidf_segment1, tfidf_segment2, inner_sens= [], [], []
    for i in range(length):
        tfidf_sen12_vector = TfidfVectorizer(analyzer="char").fit_transform([sentences1[i], sentences2[i]]).toarray()
        tfidf_segment1.append(tfidf_sen12_vector[0])
        tfidf_segment2.append(tfidf_sen12_vector[1])
        inner_sens.append(np.inner(tfidf_sen12_vector[0], tfidf_sen12_vector[1]))
    return tfidf_segment1, tfidf_segment2, pd.DataFrame(inner_sens)

tfidf_sentences1, tfidf_sentences2, bq_train['sentences_inner']= sentences_tfidf_inner(bq_train['chinese_sentences1'], bq_train['chinese_sentences2'])
  • 步骤4 :将上述特征送入分类模型,训练并预测,将结果预测提交到比赛网站。
# 构建lightgbm二分类模型
#在这里尝试使用lightgbm二分类进行训练
import lightgbm as lgb
params_lgb ={
            'learning_rate':0.1, 
            'boosting_type':'gbdt',
            'objective':'binary',
            'nthread':4,
            'metric':'auc',
            'max_depth':5,
            'num_leaves': 30,
            'subsample':0.8,                # 数据采样
            'colsample_bytree': 0.8,        # 特征采样
}
lgb_train = lgb.Dataset(train_bq, train_bq_y)
cv_results = lgb.cv(params_lgb, lgb_train, num_boost_round=200, nfold=5, stratified=False, shuffle=True, metrics='auc', callbacks=[lgb.early_stopping(stopping_rounds=50)], seed=420)
print('best n_estimators:', len(cv_results['auc-mean']))
print('best cv score:', pd.Series(cv_results['auc-mean']).max())

任务3:加载中文词向量,自己训练中文词向量

  • 步骤1 :使用jieba对中文句子进行分词
import jieba
#使用jieba进行分词
def segment_sen(sen):
    sen_list = []
    try:
        sen_list = jieba.lcut(sen)
    except:
        pass
    return sen_list
sen1_list = [segment_sen(i) for i in bq_train['chinese_sentences1']]
sen2_list = [segment_sen(i) for i in bq_train['chinese_sentences2']]
  • 步骤2 :使用gensim中Word2Vec训练分词后的句子,得到词向量。
from gensim.models import word2vec
model1 = word2vec.Word2Vec(sen1_list, min_count=1)
model2 = word2vec.Word2Vec(sen2_list, min_count=1)
model1.save('word2vec_1.model')
model2.save('word2vec_2.model')

生成词向量:

#将词向量保存为txt文件
def word_dict(model_name,  filename):
    word_vector_dict = {}
    for word in model_name.wv.index_to_key:
        word_vector_dict[word] = list(model_name.wv[word])
    with open(filename, 'w', encoding='UTF-8') as f:
        f.write(str(word_vector_dict))
 
word_dict(model1, "sentence1_word_vector.txt")
word_dict(model2, "sentence2_word_vector.txt")

任务4:使用中文词向量完成mean/max/sif句子编码

  • 步骤1 :单词通过word2vec编码为100维向量,则句子编码为N∗100N∗100的矩阵,N为句子单词个数。
  • 步骤2 :将N*100的矩阵进行max-pooling编码,转为100维度。
#将N*100的矩阵进行max-pooling编码,转为100维度。
def build_sentence_vector_max(sentence, size, w2v_model):
    sen_vec = np.zeros(size).reshape((1,size))
    for word in sentence:
        try:
            #使用maximum来进行max-pooling
            sen_vec = np.maximum(sen_vec, w2v_model.wv[word].reshape((1,size)))
        except KeyError:
            continue
    return sen_vec
  • 步骤3 :将N*100的矩阵进行mean-pooling编码,转为100维度。
#对每个句子的所有向量去平均值,来生成一个句子的vector
def build_sentence_vector(sentence, size, w2v_model):
    sen_vec = np.zeros(size).reshape((1,size))
    count = 0
    for word in sentence:
        try:
            sen_vec += w2v_model.wv[word].reshape((1,size))
            count += 1
        except KeyError:
            continue
    if count != 0:
        sen_vec = sen_vec/count
    return sen_vec
  • 步骤4 :将N*100的矩阵与单词的IDF进行矩阵相乘,即按照单词的词频进行加权,进行tfidf-pooling编码,转为100维度。
from nltk.text import TextCollection
from nltk.tokenize import word_tokenize

#构建sentences1和sentences2的语料库corpus1和corpus2
corpus1=TextCollection(sen1_list)  #构建语料库
corpus2 = TextCollection(sen2_list)

根据tfidf词频权重对句子进行编码

import math
def build_sentence_vector_tfidf_weight(sentence,size,w2v_model,corpus):
    #得到词的权重
    tfidf_weight = {}
    for word in sentence:
        tfidf_weight[word] = corpus.tf_idf(word, sentence)
    sen_vec=np.zeros(size).reshape((1,size))
    count=0
    for word in sentence:
        try:
            if word in tfidf_weight.keys():
                sen_vec+=(np.dot(w2v_model.wv[word],math.exp(tfidf_weight[word]))).reshape((1,size))
                count+=1
            else:
                sen_vec+=w2v_model[word].reshape((1,size))
                count+=1
        except KeyError:
            continue
    if count!=0:
        sen_vec/=count
    return sen_vec
  • 步骤5 :学习SIF编码的原理,进行sif编码,转为100维度。
    https://github.com/PrincetonML/SIF/blob/master/src/SIF_embedding.py#L30
    https://openreview.net/pdf?id=SyK00v5xx

这篇博客是我看SIF论文时记的笔记:【论文阅读】SIF:一种简单却很有效的句子编码方法
SIF编码如下:

from nltk.text import TextCollection

#构建sentences1和sentences2的语料库corpus1和corpus2
corpus1 = TextCollection(sen1_list)  
corpus2 = TextCollection(sen2_list)

#得到sif权重
def get_SIF_weight(a, sentences, corpus):
    SIF_weight = {}
    for sentence in sentences:
        for word in sentence:
            SIF_weight[word] =  a / a + corpus.tf_idf(word, sentence)
    return SIF_weight

a = 0.001
SIF_weight1 = get_SIF_weight(a, sen1_list, corpus1)
SIF_weight2 = get_SIF_weight(a, sen2_list, corpus2)

#获得基于SIF改进后的句子向量-->句子列表,返回的由所有句子组成的sentences metrix
import math
def build_sentences_vector_sif_weight(sentences,size,w2v_model,sif_weight):
    all_sentences_metrix = np.zeros((1, size))
    sen_vec=np.zeros(size).reshape((1,size))
    for index, sentence in enumerate(sentences):
        count=0
        for word in sentence:
            try:
                if word in sif_weight.keys():
                    sen_vec+=(np.dot(w2v_model.wv[word],math.exp(sif_weight[word]*0.001))).reshape((1,size))
                    count+=1
                else:
                    sen_vec+=w2v_model[word].reshape((1,size))
                    count+=1
            except KeyError:
                continue
        if count!=0:
            sen_vec/=count
        if index == 0:
            all_sentences_metrix = sen_vec
        else:
            all_sentences_metrix = np.vstack((all_sentences_metrix, sen_vec))
    return all_sentences_metrix

import numpy as np
from sklearn.decomposition import TruncatedSVD

def compute_pc(X,npc=1):
    """
    Compute the principal components. DO NOT MAKE THE DATA ZERO MEAN!
    :param X: X[i,:] is a data point
    :param npc: number of principal components to remove
    :return: component_[i,:] is the i-th pc
    """
    svd = TruncatedSVD(n_components=npc, n_iter=7, random_state=0)
    svd.fit(X)
    return svd.components_

def remove_pc(X, npc=1):
    """
    Remove the projection on the principal components
    :param X: X[i,:] is a data point
    :param npc: number of principal components to remove
    :return: XX[i, :] is the data point after removing its projection
    """
    pc = compute_pc(X, npc)
    if npc==1:
        XX = X - X.dot(pc.transpose()) * pc
    else:
        XX = X - X.dot(pc.transpose()).dot(pc)
    return XX


def SIF_embedding(sentences, size, w2v_model, sif_weight, npc):
    """
    Compute the scores between pairs of sentences using weighted average + removing the projection on the first principal component
    :param We: We[i,:] is the vector for word i
    :param x: x[i, :] are the indices of the words in the i-th sentence
    :param w: w[i, :] are the weights for the words in the i-th sentence
    :param params.rmpc: if >0, remove the projections of the sentence embeddings to their first principal component
    :return: emb, emb[i, :] is the embedding for sentence i
    """
    emb = build_sentences_vector_sif_weight(sentences,size,w2v_model,sif_weight)
    if  npc > 0:
        emb = remove_pc(emb, npc)
    return emb

调用上面的函数,获得句子的sif_enbedding向量:

sif_embedding_1= SIF_embedding(sen1_list, 100, model1, SIF_weight1, 1)
sif_embedding_2= SIF_embedding(sen2_list, 100, model2, SIF_weight2, 1)

结果如下:

array([[ 0.1631529 ,  0.68570732, -0.56345899, ...,  0.05019303,
         0.08823364,  0.73780842],
       [ 0.1631529 ,  0.68570732, -0.56345899, ...,  0.05019303,
         0.08823364,  0.73780842],
       [-0.45038705,  0.14196152, -0.29118772, ..., -0.94526421,
         0.30650289,  0.00765647],
       ...,
       [-0.17717167, -0.1151685 ,  0.64575971, ...,  0.43572011,
        -0.2061713 , -0.20033421],
       [-0.24399607, -0.0214033 ,  0.93929948, ...,  0.97110859,
        -0.61965028, -0.61955608],
       [ 0.11829696, -0.06113644, -0.26674104, ..., -0.30710764,
         0.16443198, -0.07984093]])
  • 步骤6(可选) :通过上述步骤2-步骤5的编码,计算相似句子的相似度 vs 不相似句子的相似度, 绘制得到分布图,哪一种编码最优?

任务5:搭建SiamCNN/LSTM模型,训练和预测

  • 步骤1 :将训练好的word2vex作为深度学习embeeding层的初始化参数。

数据准备

from collections import Counter
from keras.preprocessing.sequence import pad_sequences
import numpy as np
from gensim.models import Word2Vec


def select_best_length(train,limit_ratio=0.95):
    """
    根据数据集的句子长度,选择最佳的样本max-length
    :param limit_ratio:句子长度覆盖度,默认覆盖95%以上的句子
    :return:
    """
    len_list = []
    max_length = 0
    cover_rate = 0.0
    for q1, q2 in zip(train['q1'], train['q2']):
        len_list.append(len(q1))
        len_list.append(len(q2))
    all_sent = len(len_list)
    sum_length = 0
    len_dict = Counter(len_list).most_common()
    for i in len_dict:
        sum_length += i[1] * i[0]
    average_length = sum_length / all_sent
    for i in len_dict:
        rate = i[1] / all_sent
        cover_rate += rate
        if cover_rate >= limit_ratio:
            max_length = i[0]
            break
    print('average_length:', average_length)
    print('max_length:', max_length)
    return max_length


# select_best_length()


def build_data(train):
    """
    构建数据集
    :return:
    """
    sample_x_left = train.q1.apply(lambda x: [char for char in x if char]).tolist()
    sample_x_right = train.q2.apply(lambda x: [char for char in x if char]).tolist()
    vocabs = {'UNK'}
    for x_left, x_right in zip(sample_x_left, sample_x_right):
        for char in x_left + x_right:
            vocabs.add(char)

    sample_x = [sample_x_left, sample_x_right]
    sample_y = train.label.tolist()
    print(len(sample_x_left), len(sample_x_right))
    datas = [sample_x, sample_y]
    word_dict = {wd: index for index, wd in enumerate(list(vocabs))}
    vocab_path = 'model/vocab.txt'
    with open(vocab_path, 'w', encoding='utf-8') as f:
        f.write('\n'.join(list(vocabs)))
    return datas, word_dict


def convert_data(datas, word_dict, MAX_LENGTH):
    """
    将数据转换成keras所能处理的格式
    :return:
    """
    sample_x = datas[0]
    sample_y = datas[1]
    sample_x_left = sample_x[0]
    sample_x_right = sample_x[1]
    left_x_train = [[word_dict[char] for char in data] for data in sample_x_left]
    right_x_train = [[word_dict[char] for char in data] for data in sample_x_right]
    y_train = [int(i) for i in sample_y]
    left_x_train = pad_sequences(left_x_train, MAX_LENGTH, padding='pre')
    right_x_train = pad_sequences(right_x_train, MAX_LENGTH, padding='pre')
    y_train = np.expand_dims(y_train, 1)
    return left_x_train, right_x_train, y_train


def train_w2v(datas):
    """
    训练词向量
    :return:
    """
    sents = datas[0][0] + datas[0][1]
    model = Word2Vec(sentences=sents, vector_size=300,  min_count=1)
    model.wv.save_word2vec_format('model/token_vec_300.bin', binary=False)


def load_pretrained_embedding():
    """
    加载预训练的词向量
    :return:
    """
    embedding_file = 'model/token_vec_300.bin'
    embeddings_dict = {}
    with open(embedding_file, 'r', encoding='utf-8') as f:
        for line in f:
            values = line.strip().split(' ')
            if len(values) < 300:
                continue
            word = values[0]
            coefs = np.asarray(values[1:], dtype='float32')
            embeddings_dict[word] = coefs
    print('Found %s word vectors.' % len(embeddings_dict))
    return embeddings_dict


def build_embedding_matrix(word_dict, embedding_dict, VOCAB_SIZE, EMBEDDING_DIM):
    """
    加载词向量矩阵
    :return:
    """
    embedding_matrix = np.zeros((VOCAB_SIZE + 1, EMBEDDING_DIM))
    for word, i in word_dict.items():
        embedding_vector = embedding_dict.get(word)
        if embedding_vector is not None:
            embedding_matrix[i] = embedding_vector
    return embedding_matrix

  • 步骤2 :搭建SiamCNN(Word2Vec句子编码 + 1D CNN +FC)的孪生网络结构,完成训练和预测,提交测试集预测结果。
    搭建SiamCNN网络
def create_cnn_base_network(input_shape):
    """搭建CNN编码层网络,用于权重共享"""
    input = Input(shape=input_shape)
    cnn1 = Conv1D(64, 3, padding='valid', input_shape=(25, 100), activation='relu', name='conv_1d_layer1')(input)
    cnn1 = Dropout(0.2)(cnn1)
    cnn2 = Conv1D(32, 3, padding='valid', input_shape=(25, 100), activation='relu', name='conv_1d_layer2')(cnn1)
    cnn2 = Dropout(0.2)(cnn2)
    return Model(input, cnn2)

def train_model():
    '''训练模型'''
    #训练siamcnn模型
    model = bilstm_siamcnn_model()
    #训练siamLSTM模型
    # model = bilstm_siamese_model()
    # from keras.utils import plot_model
    # plot_model(model, to_file='model/model.png', show_shapes=True)
    history = model.fit(
        x=[left_x_train, right_x_train],
        y=y_train,
        validation_split=0.2,
        batch_size=BATCH_SIZE,
        epochs=EPOCHS,
    )
    draw_train(history)
    model.save(model_path)
    return model

train_model()

结果如下

average_length: 11.859765
max_length: 25
100000 100000
Found 1527 word vectors.
Model: "model_1"
__________________________________________________________________________________________________
 Layer (type)                   Output Shape         Param #     Connected to                     
==================================================================================================
 left_x (InputLayer)            [(None, 25)]         0           []                               
                                                                                                  
 right_x (InputLayer)           [(None, 25)]         0           []                               
                                                                                                  
 embedding (Embedding)          (None, 25, 100)      152900      ['left_x[0][0]',                 
                                                                  'right_x[0][0]']                
                                                                                                  
 model (Functional)             (None, 21, 32)       25440       ['embedding[0][0]',              
                                                                  'embedding[1][0]']              
                                                                                                  
 lambda (Lambda)                (None, 1, 32)        0           ['model[0][0]',                  
                                                                  'model[1][0]']                  
                                                                                                  
==================================================================================================
Total params: 178,340
Trainable params: 25,440
Non-trainable params: 152,900

结果有点奇怪???再改改看!

  • 步骤3 :搭建SiamLSTM(Word2Vec句子编码 + LSTM + FC)的孪生网络结构,完成训练和预测,提交测试集预测结果。

搭建SiamLSTM模型如下

from keras import backend as K
from keras.models import Model
from keras.layers import Input, Embedding, LSTM, Dropout, Lambda, Bidirectional
import matplotlib.pyplot as plt
import os

os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
from bulid_input import *
from load_data import *


# 参数设置
BATCH_SIZE = 512
EMBEDDING_DIM = 300
EPOCHS = 20
model_path = 'model/tokenvec_bilstm2_siamese_model.h5'

# 数据准备
train = read_bq('data/bq_corpus/train.tsv',['line_num','q1','q2','label'])

MAX_LENGTH = select_best_length(train)
datas, word_dict = build_data(train)
train_w2v(datas)
VOCAB_SIZE = len(word_dict)
embeddings_dict = load_pretrained_embedding()
embedding_matrix = build_embedding_matrix(word_dict, embeddings_dict,
                                          VOCAB_SIZE, EMBEDDING_DIM)
left_x_train, right_x_train, y_train = convert_data(datas, word_dict, MAX_LENGTH)

def create_base_network(input_shape):
    '''搭建编码层网络,用于权重共享'''
    input = Input(shape=input_shape)
    lstm1 = Bidirectional(LSTM(128, return_sequences=True))(input)
    lstm1 = Dropout(0.5)(lstm1)
    lstm2 = Bidirectional(LSTM(32))(lstm1)
    lstm2 = Dropout(0.5)(lstm2)
    return Model(input, lstm2)


def exponent_neg_manhattan_distance(sent_left, sent_right):
    '''基于曼哈顿空间距离计算两个字符串语义空间表示相似度计算'''
    return K.exp(-K.sum(K.abs(sent_left - sent_right), axis=1, keepdims=True))


def bilstm_siamese_model():
    '''搭建网络'''
    embedding_layer = Embedding(VOCAB_SIZE + 1,
                                EMBEDDING_DIM,
                                weights=[embedding_matrix],
                                input_length=MAX_LENGTH,
                                trainable=False,
                                mask_zero=True)
    left_input = Input(shape=(MAX_LENGTH,), dtype='float32', name="left_x")
    right_input = Input(shape=(MAX_LENGTH,), dtype='float32', name='right_x')
    encoded_left = embedding_layer(left_input)
    encoded_right = embedding_layer(right_input)
    shared_lstm = create_base_network(input_shape=(MAX_LENGTH, EMBEDDING_DIM))
    left_output = shared_lstm(encoded_left)
    right_output = shared_lstm(encoded_right)
    distance = Lambda(lambda x: exponent_neg_manhattan_distance(x[0], x[1]),
                      output_shape=lambda x: (x[0][0], 1))([left_output, right_output])
    model = Model([left_input, right_input], distance)
    model.compile(loss='binary_crossentropy',
                  optimizer='nadam',
                  metrics=['accuracy'])
    model.summary()
    return model


def draw_train(history):
    '''绘制训练曲线'''
    # Plot training & validation accuracy values
    plt.plot(history.history['acc'])
    plt.plot(history.history['val_acc'])
    plt.title('Model accuracy')
    plt.ylabel('Accuracy')
    plt.xlabel('Epoch')
    plt.legend(['Train', 'Test'], loc='upper left')
    plt.show()

    # Plot training & validation loss values
    plt.plot(history.history['loss'])
    plt.plot(history.history['val_loss'])
    plt.title('Model loss')
    plt.ylabel('Loss')
    plt.xlabel('Epoch')
    plt.legend(['Train', 'Test'], loc='upper left')
    plt.savefig("model/result_atec.png")
    plt.show()


def train_model():
    '''训练模型'''
    model = bilstm_siamese_model()
    # from keras.utils import plot_model
    # plot_model(model, to_file='model/model.png', show_shapes=True)
    history = model.fit(
        x=[left_x_train, right_x_train],
        y=y_train,
        validation_split=0.2,
        batch_size=BATCH_SIZE,
        epochs=EPOCHS,
    )
    draw_train(history)
    model.save(model_path)
    return model
   
train_model()

结果如下:

Model: "model_1"
__________________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to                     
==================================================================================================
left_x (InputLayer)             [(None, 25)]         0                                            
__________________________________________________________________________________________________
right_x (InputLayer)            [(None, 25)]         0                                            
__________________________________________________________________________________________________
embedding (Embedding)           (None, 25, 100)      152900      left_x[0][0]                     
                                                                 right_x[0][0]                    
__________________________________________________________________________________________________
model (Functional)              (None, 16)           908048      embedding[0][0]                  
                                                                 embedding[1][0]                  
__________________________________________________________________________________________________
lambda (Lambda)                 (None, 1)            0           model[0][0]                      
                                                                 model[1][0]                      
==================================================================================================
Total params: 1,060,948
Trainable params: 908,048
Non-trainable params: 152,900
__________________________________________________________________________________________________
Epoch 1/20
157/157 [==============================] - 110s 559ms/step - loss: 0.6208 - accuracy: 0.6481 - val_loss: 0.5626 - val_accuracy: 0.7121
Epoch 2/20
157/157 [==============================] - 95s 603ms/step - loss: 0.5633 - accuracy: 0.7096 - val_loss: 0.5576 - val_accuracy: 0.7219
Epoch 3/20
157/157 [==============================] - 96s 614ms/step - loss: 0.5362 - accuracy: 0.7325 - val_loss: 0.4977 - val_accuracy: 0.7618
Epoch 4/20
157/157 [==============================] - 90s 576ms/step - loss: 0.5189 - accuracy: 0.7492 - val_loss: 0.5009 - val_accuracy: 0.7671
Epoch 5/20
157/157 [==============================] - 86s 545ms/step - loss: 0.5043 - accuracy: 0.7591 - val_loss: 0.4736 - val_accuracy: 0.7817
Epoch 6/20
157/157 [==============================] - 87s 555ms/step - loss: 0.4905 - accuracy: 0.7697 - val_loss: 0.4637 - val_accuracy: 0.7908
Epoch 7/20
157/157 [==============================] - 85s 543ms/step - loss: 0.4912 - accuracy: 0.7690 - val_loss: 0.4680 - val_accuracy: 0.7881
Epoch 8/20
157/157 [==============================] - 87s 554ms/step - loss: 0.4747 - accuracy: 0.7798 - val_loss: 0.4486 - val_accuracy: 0.7979
Epoch 9/20
157/157 [==============================] - 87s 555ms/step - loss: 0.4847 - accuracy: 0.7772 - val_loss: 0.4511 - val_accuracy: 0.7969
Epoch 10/20
157/157 [==============================] - 84s 535ms/step - loss: 0.4617 - accuracy: 0.7909 - val_loss: 0.4681 - val_accuracy: 0.8005
Epoch 11/20
157/157 [==============================] - 79s 505ms/step - loss: 0.4416 - accuracy: 0.8028 - val_loss: 0.4258 - val_accuracy: 0.8114
Epoch 12/20
157/157 [==============================] - 81s 517ms/step - loss: 0.4378 - accuracy: 0.8045 - val_loss: 0.4330 - val_accuracy: 0.8141
Epoch 13/20
157/157 [==============================] - 85s 539ms/step - loss: 0.4255 - accuracy: 0.8113 - val_loss: 0.4397 - val_accuracy: 0.8103
Epoch 14/20
157/157 [==============================] - 78s 499ms/step - loss: 0.4481 - accuracy: 0.7992 - val_loss: 0.4367 - val_accuracy: 0.8079
Epoch 15/20
157/157 [==============================] - 84s 536ms/step - loss: 0.4439 - accuracy: 0.8047 - val_loss: 0.4453 - val_accuracy: 0.8080
Epoch 16/20
157/157 [==============================] - 86s 548ms/step - loss: 0.4424 - accuracy: 0.8033 - val_loss: 0.4273 - val_accuracy: 0.8127
Epoch 17/20
157/157 [==============================] - 79s 504ms/step - loss: 0.4373 - accuracy: 0.8065 - val_loss: 0.4493 - val_accuracy: 0.8127
Epoch 18/20
157/157 [==============================] - 77s 490ms/step - loss: 0.4226 - accuracy: 0.8146 - val_loss: 0.4395 - val_accuracy: 0.8109
Epoch 19/20
157/157 [==============================] - 76s 484ms/step - loss: 0.4182 - accuracy: 0.8156 - val_loss: 0.4337 - val_accuracy: 0.8113
Epoch 20/20
157/157 [==============================] - 76s 484ms/step - loss: 0.3997 - accuracy: 0.8296 - val_loss: 0.4159 - val_accuracy: 0.8220

Process finished with exit code 0

最后验证集的准确度有0.82,还算不错,后面可是尝试将所有数据组合在一起进行训练,或许准确率还会有所提升。

任务6:搭建InferSent模型,训练和预测

  • 步骤1 :将训练好的word2vex作为深度学习embeeding层的初始化参数。
  • 步骤2 :搭建InferSent模型,尝试不同的交叉方法。
  • 步骤3 :训练InferSent模型,提交测试集预测结果。

由于篇幅有限,因此将InferSent的结构介绍写在另一篇博客中InferSent模型介绍,,根据论文中给出的模型结构图,使用keras,搭建InferSent网络,其中编码器使用BiLSTM-Maxpooling。
百度千言-中文文本相似度实战_第4张图片

6.1 模型搭建与训练

import warnings
warnings.filterwarnings('ignore')
from keras import backend as K
from keras.models import Model
from keras.layers import Input, Embedding, LSTM, Dropout, Lambda, Bidirectional, Dense, Flatten, merge
import tensorflow as tf
import matplotlib.pyplot as plt
import os
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
from bulid_input import *
from load_data import *

# 参数设置
BATCH_SIZE = 512
EMBEDDING_DIM = 100
EPOCHS = 20
model_path = 'model/tokenvec_bilstm2_siamese_model.h5'

# 数据准备
train = read_bq('data/bq_corpus/train.tsv',['line_num','q1','q2','label'])

MAX_LENGTH = select_best_length(train)
datas, word_dict = build_data(train)
train_w2v(datas)
VOCAB_SIZE = len(word_dict)
embeddings_dict = load_pretrained_embedding()
embedding_matrix = build_embedding_matrix(word_dict, embeddings_dict,
                                          VOCAB_SIZE, EMBEDDING_DIM)
left_x_train, right_x_train, y_train = convert_data(datas, word_dict, MAX_LENGTH)


# 搭建InferSent模型
def create_bilstm_base_network(input_shape):
    '''搭建Bi-LSTM编码层网络,用于权重共享'''
    input = Input(shape=input_shape)
    lstm1 = Bidirectional(LSTM(128, return_sequences=True))(input)
    lstm1 = Dropout(0.5)(lstm1)

    lstm2 = Bidirectional(LSTM(32))(lstm1)
    lstm2 = Dropout(0.5)(lstm2)

    return Model(input, lstm2)


def exponent_neg_manhattan_distance(sent_left, sent_right):
    '''基于曼哈顿空间距离计算两个字符串语义空间表示相似度计算'''
    return K.exp(-K.sum(K.abs(sent_left - sent_right), axis=1, keepdims=True))


def cross_layer(vector_left, vector_right, cross_method):
    vector = None
    if cross_method == 'L1':
        vector = K.abs(vector_left - vector_right)
    elif cross_method == 'concat':
        vector = K.concatenate(vector_left, vector_right, axis=1)
    elif cross_method == ' multi':
        vector = tf.multiply(vector_left, vector_right)
    else:
        print("Cross_method input error!")
    return vector
    

def infersent_bilstm_model():
    '''搭建网络'''
    # embedding层,其中使用word2vec向量先进行embedding
    embedding_layer = Embedding(VOCAB_SIZE + 1,
                                EMBEDDING_DIM,
                                weights=[embedding_matrix],
                                input_length=MAX_LENGTH,
                                trainable=False,
                                mask_zero=True)
    left_input = Input(shape=(MAX_LENGTH,), dtype='float32', name="left_x")
    right_input = Input(shape=(MAX_LENGTH,), dtype='float32', name='right_x')
    encoded_left = embedding_layer(left_input)
    encoded_right = embedding_layer(right_input)
    shared_lstm = create_bilstm_base_network(input_shape=(MAX_LENGTH, EMBEDDING_DIM))
    left_output = shared_lstm(encoded_left)
    right_output = shared_lstm(encoded_right)       # (None, 64)
    cross_vector = Lambda(lambda x: cross_layer(x[0], x[1], 'multi'),
                      output_shape=lambda x: (x[0][0], 1))([left_output, right_output])         # (None, 64)
    # 加上三层全连接神经网络
    fc1 = Dense(1024, activation='relu', name='fc1')(cross_vector)
    fc2 = Dense(512, activation='relu', name='fc2')(fc1)
    fc3 = Dense(16, activation='relu', name='fc3')(fc2)
    sigmoid_layer = Dense(1, activation='sigmoid', name='sigmoid')(fc3)
    model = Model([left_input, right_input], sigmoid_layer)
    model.compile(loss='binary_crossentropy',
                  optimizer='nadam',
                  metrics=['accuracy'])
    model.summary()
    return model


def draw_train(history):
    '''绘制训练曲线'''
    # Plot training & validation accuracy values
    plt.plot(history.history['accuracy'])
    plt.plot(history.history['val_accuracy'])
    plt.title('Model accuracy')
    plt.ylabel('Accuracy')
    plt.xlabel('Epoch')
    plt.legend(['Train', 'Test'], loc='upper left')
    plt.show()

    # Plot training & validation loss values
    plt.plot(history.history['loss'])
    plt.plot(history.history['val_loss'])
    plt.title('Model loss')
    plt.ylabel('Loss')
    plt.xlabel('Epoch')
    plt.legend(['Train', 'Test'], loc='upper left')
    plt.savefig("model/result_atec.png")
    plt.show()


def train_model():
    '''训练模型'''
    # 训练siamcnn模型
    # model = bilstm_siamcnn_model()
    # 训练siamLSTM模型
    model = bilstm_siamese_model()
    from keras.utils import plot_model
    plot_model(model, to_file='model/model.png', show_shapes=True)
    history = model.fit(
        x=[left_x_train, right_x_train],
        y=y_train,
        validation_split=0.2,
        batch_size=BATCH_SIZE,
        epochs=EPOCHS,
    )
    draw_train(history)
    model.save(model_path)
    return model


train_model()

模型结果如下

# 使用两句子向量矩阵对应位置相乘的交叉特征方法,得到的模型结果如下:
average_length: 11.859765
max_length: 25
100000 100000
Found 1527 word vectors.
Model: "model_1"
__________________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to                     
==================================================================================================
left_x (InputLayer)             [(None, 25)]         0                                            
__________________________________________________________________________________________________
right_x (InputLayer)            [(None, 25)]         0                                            
__________________________________________________________________________________________________
embedding (Embedding)           (None, 25, 100)      152900      left_x[0][0]                     
                                                                 right_x[0][0]                    
__________________________________________________________________________________________________
model (Functional)              (None, 64)           308480      embedding[0][0]                  
                                                                 embedding[1][0]                  
__________________________________________________________________________________________________
lambda (Lambda)                 (None, 64)           0           model[0][0]                      
                                                                 model[1][0]                      
__________________________________________________________________________________________________
fc1 (Dense)                     (None, 1024)         66560       lambda[0][0]                     
__________________________________________________________________________________________________
fc2 (Dense)                     (None, 512)          524800      fc1[0][0]                        
__________________________________________________________________________________________________
fc3 (Dense)                     (None, 16)           8208        fc2[0][0]                        
__________________________________________________________________________________________________
sigmoid (Dense)                 (None, 1)            17          fc3[0][0]                        
==================================================================================================
Total params: 1,060,965
Trainable params: 908,065
Non-trainable params: 152,900
__________________________________________________________________________________________________
Epoch 1/20
157/157 [==============================] - 116s 598ms/step - loss: 0.5817 - accuracy: 0.6871 - val_loss: 0.5459 - val_accuracy: 0.7078
Epoch 2/20
157/157 [==============================] - 78s 499ms/step - loss: 0.5141 - accuracy: 0.7454 - val_loss: 0.4991 - val_accuracy: 0.7438
Epoch 3/20
157/157 [==============================] - 86s 547ms/step - loss: 0.4793 - accuracy: 0.7706 - val_loss: 0.4536 - val_accuracy: 0.7732
Epoch 4/20
157/157 [==============================] - 79s 503ms/step - loss: 0.4493 - accuracy: 0.7903 - val_loss: 0.4569 - val_accuracy: 0.7728
Epoch 5/20
157/157 [==============================] - 79s 506ms/step - loss: 0.4234 - accuracy: 0.8052 - val_loss: 0.4196 - val_accuracy: 0.7958
Epoch 6/20
157/157 [==============================] - 83s 529ms/step - loss: 0.4006 - accuracy: 0.8182 - val_loss: 0.4271 - val_accuracy: 0.7977
Epoch 7/20
157/157 [==============================] - 81s 516ms/step - loss: 0.3824 - accuracy: 0.8294 - val_loss: 0.3913 - val_accuracy: 0.8094
Epoch 8/20
157/157 [==============================] - 76s 486ms/step - loss: 0.3638 - accuracy: 0.8393 - val_loss: 0.3763 - val_accuracy: 0.8211
Epoch 9/20
157/157 [==============================] - 79s 501ms/step - loss: 0.3462 - accuracy: 0.8493 - val_loss: 0.3713 - val_accuracy: 0.8278
Epoch 10/20
157/157 [==============================] - 82s 525ms/step - loss: 0.3351 - accuracy: 0.8544 - val_loss: 0.3881 - val_accuracy: 0.8314
Epoch 11/20
157/157 [==============================] - 78s 498ms/step - loss: 0.3205 - accuracy: 0.8631 - val_loss: 0.3438 - val_accuracy: 0.8446
Epoch 12/20
157/157 [==============================] - 84s 535ms/step - loss: 0.3075 - accuracy: 0.8693 - val_loss: 0.3349 - val_accuracy: 0.8515
Epoch 13/20
157/157 [==============================] - 80s 511ms/step - loss: 0.2942 - accuracy: 0.8766 - val_loss: 0.3312 - val_accuracy: 0.8572
Epoch 14/20
157/157 [==============================] - 80s 509ms/step - loss: 0.2859 - accuracy: 0.8796 - val_loss: 0.3210 - val_accuracy: 0.8580
Epoch 15/20
157/157 [==============================] - 77s 489ms/step - loss: 0.2738 - accuracy: 0.8856 - val_loss: 0.3004 - val_accuracy: 0.8706
Epoch 16/20
157/157 [==============================] - 85s 539ms/step - loss: 0.2647 - accuracy: 0.8898 - val_loss: 0.3114 - val_accuracy: 0.8724
Epoch 17/20
157/157 [==============================] - 76s 485ms/step - loss: 0.2566 - accuracy: 0.8948 - val_loss: 0.3065 - val_accuracy: 0.8722
Epoch 18/20
157/157 [==============================] - 76s 486ms/step - loss: 0.2482 - accuracy: 0.8971 - val_loss: 0.2850 - val_accuracy: 0.8791
Epoch 19/20
157/157 [==============================] - 82s 523ms/step - loss: 0.2432 - accuracy: 0.8992 - val_loss: 0.2847 - val_accuracy: 0.8816
Epoch 20/20
157/157 [==============================] - 83s 529ms/step - loss: 0.2327 - accuracy: 0.9049 - val_loss: 0.2914 - val_accuracy: 0.8787

Process finished with exit code 0

其中句子编码器encode使用BiLSTM-Maxpooling,原论文中提到的几种交叉验证方法,(我的理解是三种选一种分别进行实验),实验结果如下:

6.2 使用不同交叉方法训练结果

  • 使用vector对应位置相乘的交叉特征方法在验证集上的准确率有88.16%
  • 使用两句子向量concatenate的交叉方法,查看验证集准确是91.43%,分析其比对应相乘准确率高的原因可能有:1、在concatenate时,vector的维度由原来的64变为128维,后接FC层时需要使用更多的参数进行训练,模型的网络更大,可以学习到语义中更复杂的信息,因此准确率高;2、使用两句子向量concatenate的交叉方法比对应相乘的交叉方法更能够提取出两句子向量的异同,对后续进行分类效果更好。
  • 使用两句子向量相减后取绝对值的交叉方法,结果不如上面两种交叉方法(只有78.%多),且存在比较严重的过拟合。

综上,使用两句子向量concatenate的交叉方法可以使模型在验证集的准确率更高,模型更快收敛。

任务7:搭建ESIM模型,训练和预测

  • 步骤1 :将训练好的word2vex作为深度学习embeeding层的初始化参数。
  • 步骤2 :搭建ESIM模型,尝试不同的交叉方法。
  • 步骤3 :训练ESIM模型,提交测试集预测结果。

由于本博客篇幅有限,关于ESIM模型的详细结构见我的另一篇博客:ESIM模型详解

7.1 ESIM模型搭建与训练

import warnings
warnings.filterwarnings('ignore')
from keras.layers import *
from keras.activations import softmax
from keras.models import Model
import os
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
from bulid_input import *
from load_data import *


def StaticEmbedding(embedding_matrix):
    # Embedding metrix
    in_dim, out_dim = embedding_matrix.shape
    return Embedding(in_dim, out_dim, weights=[embedding_matrix], trainable=False)


def subtract(input_1, input_2):
    minus_input_2 = Lambda(lambda x: -x)(input_2)
    return add([input_1, minus_input_2])


def aggregate(input_1, input_2, num_dense=300, dropout_rate=0.5):
    feat1 = concatenate([GlobalAvgPool1D()(input_1), GlobalMaxPool1D()(input_1)])
    feat2 = concatenate([GlobalAvgPool1D()(input_2), GlobalMaxPool1D()(input_2)])
    x = concatenate([feat1, feat2])
    x = BatchNormalization()(x)
    x = Dense(num_dense, activation='relu')(x)
    x = BatchNormalization()(x)
    x = Dropout(dropout_rate)(x)
    x = Dense(num_dense, activation='relu')(x)
    x = BatchNormalization()(x)
    x = Dropout(dropout_rate)(x)
    return x


def align(input_1, input_2):
    attention = Dot(axes=-1, name='attention-layer')([input_1, input_2])
    w_att_1 = Lambda(lambda x: softmax(x, axis=1))(attention)
    w_att_2 = Permute((2, 1))(Lambda(lambda x: softmax(x, axis=2))(attention))
    in1_aligned = Dot(axes=1)([w_att_1, input_1])
    in2_aligned = Dot(axes=1)([w_att_2, input_2])
    return in1_aligned, in2_aligned


def build_model(embedding_matrix, num_class=1, max_length=30, lstm_dim=300):
    q1 = Input(shape=(max_length,))
    q2 = Input(shape=(max_length,))

    # Embedding
    embedding = StaticEmbedding(embedding_matrix)
    q1_embed = BatchNormalization(axis=2)(embedding(q1))
    q2_embed = BatchNormalization(axis=2)(embedding(q2))

    # Encoding
    encode = Bidirectional(LSTM(lstm_dim, return_sequences=True))
    q1_encoded = encode(q1_embed)
    q2_encoded = encode(q2_embed)

    # Alignment
    q1_aligned, q2_aligned = align(q1_encoded, q2_encoded)

    # Compare
    q1_combined = concatenate(
        [q1_encoded, q2_aligned, subtract(q1_encoded, q2_aligned), multiply([q1_encoded, q2_aligned])])
    q2_combined = concatenate(
        [q2_encoded, q1_aligned, subtract(q2_encoded, q1_aligned), multiply([q2_encoded, q1_aligned])])
    compare = Bidirectional(LSTM(lstm_dim, return_sequences=True))
    q1_compare = compare(q1_combined)
    q2_compare = compare(q2_combined)

    # Aggregate
    x = aggregate(q1_compare, q2_compare)
    x = Dense(num_class, activation='sigmoid')(x)
    model = Model(inputs=[q1, q2], outputs=x)
    model.compile(loss='binary_crossentropy',
                  optimizer='nadam',
                  metrics=['accuracy'])
    model.summary()
    return model


if __name__ == "__main__":
    # 参数设置
    BATCH_SIZE = 512
    EMBEDDING_DIM = 100
    EPOCHS = 20
    model_path = 'model/tokenvec_esim_model.h5'

    # 数据准备
    train = read_bq('data/bq_corpus/train.tsv', ['line_num', 'q1', 'q2', 'label'])

    MAX_LENGTH = select_best_length(train)
    datas, word_dict = build_data(train)
    train_w2v(datas)
    VOCAB_SIZE = len(word_dict)
    embeddings_dict = load_pretrained_embedding()
    embedding_matrix = build_embedding_matrix(word_dict, embeddings_dict,
                                              VOCAB_SIZE, EMBEDDING_DIM)
    left_x_train, right_x_train, y_train = convert_data(datas, word_dict, MAX_LENGTH)
    model = build_model(embedding_matrix, max_length=MAX_LENGTH, lstm_dim=128)
    from keras.utils.vis_utils import plot_model
    plot_model(model, to_file='model/model_esim.png', show_shapes=True)
    history = model.fit(
        x=[left_x_train, right_x_train],
        y=y_train,
        validation_split=0.2,
        batch_size=BATCH_SIZE,
        epochs=EPOCHS,
    )
    model.save(model_path)

7.2 结果分析

根据原论文,应该是同时使用相减,点积等交叉方法,结果如下:

average_length: 11.859765
max_length: 25
100000 100000
Found 1527 word vectors.
Model: "model"
__________________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to                     
==================================================================================================
input_1 (InputLayer)            [(None, 25)]         0                                            
__________________________________________________________________________________________________
input_2 (InputLayer)            [(None, 25)]         0                                            
__________________________________________________________________________________________________
embedding (Embedding)           (None, 25, 100)      152900      input_1[0][0]                    
                                                                 input_2[0][0]                    
__________________________________________________________________________________________________
batch_normalization (BatchNorma (None, 25, 100)      400         embedding[0][0]                  
__________________________________________________________________________________________________
batch_normalization_1 (BatchNor (None, 25, 100)      400         embedding[1][0]                  
__________________________________________________________________________________________________
bidirectional (Bidirectional)   (None, 25, 256)      234496      batch_normalization[0][0]        
                                                                 batch_normalization_1[0][0]      
__________________________________________________________________________________________________
dot (Dot)                       (None, 25, 25)       0           bidirectional[0][0]              
                                                                 bidirectional[1][0]              
__________________________________________________________________________________________________
lambda_1 (Lambda)               (None, 25, 25)       0           dot[0][0]                        
__________________________________________________________________________________________________
permute (Permute)               (None, 25, 25)       0           lambda_1[0][0]                   
__________________________________________________________________________________________________
lambda (Lambda)                 (None, 25, 25)       0           dot[0][0]                        
__________________________________________________________________________________________________
dot_2 (Dot)                     (None, 25, 256)      0           permute[0][0]                    
                                                                 bidirectional[1][0]              
__________________________________________________________________________________________________
dot_1 (Dot)                     (None, 25, 256)      0           lambda[0][0]                     
                                                                 bidirectional[0][0]              
__________________________________________________________________________________________________
lambda_2 (Lambda)               (None, 25, 256)      0           dot_2[0][0]                      
__________________________________________________________________________________________________
lambda_3 (Lambda)               (None, 25, 256)      0           dot_1[0][0]                      
__________________________________________________________________________________________________
add (Add)                       (None, 25, 256)      0           bidirectional[0][0]              
                                                                 lambda_2[0][0]                   
__________________________________________________________________________________________________
multiply (Multiply)             (None, 25, 256)      0           bidirectional[0][0]              
                                                                 dot_2[0][0]                      
__________________________________________________________________________________________________
add_1 (Add)                     (None, 25, 256)      0           bidirectional[1][0]              
                                                                 lambda_3[0][0]                   
__________________________________________________________________________________________________
multiply_1 (Multiply)           (None, 25, 256)      0           bidirectional[1][0]              
                                                                 dot_1[0][0]                      
__________________________________________________________________________________________________
concatenate (Concatenate)       (None, 25, 1024)     0           bidirectional[0][0]              
                                                                 dot_2[0][0]                      
                                                                 add[0][0]                        
                                                                 multiply[0][0]                   
__________________________________________________________________________________________________
concatenate_1 (Concatenate)     (None, 25, 1024)     0           bidirectional[1][0]              
                                                                 dot_1[0][0]                      
                                                                 add_1[0][0]                      
                                                                 multiply_1[0][0]                 
__________________________________________________________________________________________________
bidirectional_1 (Bidirectional) (None, 25, 256)      1180672     concatenate[0][0]                
                                                                 concatenate_1[0][0]              
__________________________________________________________________________________________________
global_average_pooling1d (Globa (None, 256)          0           bidirectional_1[0][0]            
__________________________________________________________________________________________________
global_max_pooling1d (GlobalMax (None, 256)          0           bidirectional_1[0][0]            
__________________________________________________________________________________________________
global_average_pooling1d_1 (Glo (None, 256)          0           bidirectional_1[1][0]            
__________________________________________________________________________________________________
global_max_pooling1d_1 (GlobalM (None, 256)          0           bidirectional_1[1][0]            
__________________________________________________________________________________________________
concatenate_2 (Concatenate)     (None, 512)          0           global_average_pooling1d[0][0]   
                                                                 global_max_pooling1d[0][0]       
__________________________________________________________________________________________________
concatenate_3 (Concatenate)     (None, 512)          0           global_average_pooling1d_1[0][0] 
                                                                 global_max_pooling1d_1[0][0]     
__________________________________________________________________________________________________
concatenate_4 (Concatenate)     (None, 1024)         0           concatenate_2[0][0]              
                                                                 concatenate_3[0][0]              
__________________________________________________________________________________________________
batch_normalization_2 (BatchNor (None, 1024)         4096        concatenate_4[0][0]              
__________________________________________________________________________________________________
dense (Dense)                   (None, 300)          307500      batch_normalization_2[0][0]      
__________________________________________________________________________________________________
batch_normalization_3 (BatchNor (None, 300)          1200        dense[0][0]                      
__________________________________________________________________________________________________
dropout (Dropout)               (None, 300)          0           batch_normalization_3[0][0]      
__________________________________________________________________________________________________
dense_1 (Dense)                 (None, 300)          90300       dropout[0][0]                    
__________________________________________________________________________________________________
batch_normalization_4 (BatchNor (None, 300)          1200        dense_1[0][0]                    
__________________________________________________________________________________________________
dropout_1 (Dropout)             (None, 300)          0           batch_normalization_4[0][0]      
__________________________________________________________________________________________________
dense_2 (Dense)                 (None, 1)            301         dropout_1[0][0]                  
==================================================================================================
Total params: 1,973,465
Trainable params: 1,816,917
Non-trainable params: 156,548
__________________________________________________________________________________________________
Epoch 1/20
157/157 [==============================] - 27s 102ms/step - loss: 0.6268 - accuracy: 0.7018 - val_loss: 0.6260 - val_accuracy: 0.6199
Epoch 2/20
157/157 [==============================] - 14s 91ms/step - loss: 0.4399 - accuracy: 0.7983 - val_loss: 0.4808 - val_accuracy: 0.7659
Epoch 3/20
157/157 [==============================] - 14s 91ms/step - loss: 0.3649 - accuracy: 0.8392 - val_loss: 0.4087 - val_accuracy: 0.8142
Epoch 4/20
157/157 [==============================] - 14s 91ms/step - loss: 0.3029 - accuracy: 0.8706 - val_loss: 0.3541 - val_accuracy: 0.8462
Epoch 5/20
157/157 [==============================] - 14s 91ms/step - loss: 0.2456 - accuracy: 0.8984 - val_loss: 0.3566 - val_accuracy: 0.8507
Epoch 6/20
157/157 [==============================] - 14s 91ms/step - loss: 0.1913 - accuracy: 0.9231 - val_loss: 0.3457 - val_accuracy: 0.8686
Epoch 7/20
157/157 [==============================] - 14s 91ms/step - loss: 0.1468 - accuracy: 0.9427 - val_loss: 0.4154 - val_accuracy: 0.8534
Epoch 8/20
157/157 [==============================] - 14s 91ms/step - loss: 0.1164 - accuracy: 0.9547 - val_loss: 0.3759 - val_accuracy: 0.8827
Epoch 9/20
157/157 [==============================] - 14s 91ms/step - loss: 0.0847 - accuracy: 0.9680 - val_loss: 0.3767 - val_accuracy: 0.8822
Epoch 10/20
157/157 [==============================] - 14s 91ms/step - loss: 0.0706 - accuracy: 0.9731 - val_loss: 0.4152 - val_accuracy: 0.8832
Epoch 11/20
157/157 [==============================] - 14s 91ms/step - loss: 0.0588 - accuracy: 0.9777 - val_loss: 0.3578 - val_accuracy: 0.9011
Epoch 12/20
157/157 [==============================] - 14s 91ms/step - loss: 0.0479 - accuracy: 0.9825 - val_loss: 0.3783 - val_accuracy: 0.8989
Epoch 13/20
157/157 [==============================] - 14s 91ms/step - loss: 0.0400 - accuracy: 0.9852 - val_loss: 0.4218 - val_accuracy: 0.8934
Epoch 14/20
157/157 [==============================] - 14s 91ms/step - loss: 0.0368 - accuracy: 0.9867 - val_loss: 0.4127 - val_accuracy: 0.8985
Epoch 15/20
157/157 [==============================] - 14s 91ms/step - loss: 0.0329 - accuracy: 0.9883 - val_loss: 0.4502 - val_accuracy: 0.8981
Epoch 16/20
157/157 [==============================] - 14s 91ms/step - loss: 0.0293 - accuracy: 0.9894 - val_loss: 0.5123 - val_accuracy: 0.8892
Epoch 17/20
157/157 [==============================] - 14s 91ms/step - loss: 0.0314 - accuracy: 0.9888 - val_loss: 0.4081 - val_accuracy: 0.9037
Epoch 18/20
157/157 [==============================] - 14s 91ms/step - loss: 0.0283 - accuracy: 0.9897 - val_loss: 0.4041 - val_accuracy: 0.9062
Epoch 19/20
157/157 [==============================] - 14s 91ms/step - loss: 0.0223 - accuracy: 0.9923 - val_loss: 0.4386 - val_accuracy: 0.9031
Epoch 20/20
157/157 [==============================] - 14s 91ms/step - loss: 0.0198 - accuracy: 0.9929 - val_loss: 0.4379 - val_accuracy: 0.9055

Process finished with exit code 0

ESIM在训练集上准确率接近99%,但是在测试集上最高只有90%,稳定在90%上下1%左右。检查了网络结构,没有写错。可能是存在一定程度的过拟合了。

任务8:使用BERT或ERNIE完成NSP任务

参考代码:
https://aistudio.baidu.com/aistudio/projectdetail/3168859
bert-nsp代码

  • 步骤1 :学习Bert模型的使用。

之前看过挺多遍BERT模型,所以这里不再赘述BERT的模型结果,想了解的同学可以看看这篇博客:【NLP】Google BERT模型原理详解。
好!现在开始使用BERT进行句子编码!

  • 步骤2 :使用Bert完成NSP任务的训练和预测,提交测试集预测结果。
import warnings
warnings.filterwarnings('ignore')       # 隐藏警告!
from transformers import logging
logging.set_verbosity_warning()
import pandas as pd
import torch
from sklearn.model_selection import train_test_split
from torch.utils.data import Dataset, DataLoader, TensorDataset
import numpy as np
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'


def read_tsv(input_file,columns):
    with open(input_file,"r",encoding="utf-8") as file:
        lines = []
        count = 1
        for line in file:
            if len(line.strip().split("\t")) != 1:
                lines.append([count]+line.strip().split("\t"))
                count += 1
        df = pd.DataFrame(lines)
        df.columns = columns
    return df

# 数据集读取
class bqDataset(Dataset):
    def __init__(self, encodings, labels):
        self.encodings = encodings
        self.labels = labels

    # 读取单个样本
    def __getitem__(self, idx):
        item = {key: torch.tensor(val[idx]) for key, val in self.encodings.items()}
        item['labels'] = torch.tensor(int(self.labels[idx]))
        return item

    def __len__(self):
        return len(self.labels)

# 精度计算
def flat_accuracy(preds, labels):
    pred_flat = np.argmax(preds, axis=1).flatten()
    labels_flat = labels.flatten()
    return np.sum(pred_flat == labels_flat) / len(labels_flat)


# 训练函数
def train():
    model.train()
    total_train_loss = 0
    iter_num = 0
    total_iter = len(train_loader)
    for batch in train_loader:
        # 正向传播
        optim.zero_grad()
        input_ids = batch['input_ids'].to(device)
        attention_mask = batch['attention_mask'].to(device)
        labels = batch['labels'].to(device)
        outputs = model(input_ids, attention_mask=attention_mask, labels=labels)
        loss = outputs[0]
        total_train_loss += loss.item()

        # 反向梯度信息
        loss.backward()
        torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)

        # 参数更新
        optim.step()

        iter_num += 1
        if (iter_num % 100 == 0):
            print("epoth: %d, iter_num: %d, loss: %.4f, %.2f%%" % (
            epoch, iter_num, loss.item(), iter_num / total_iter * 100))

    print("Epoch: %d, Average training loss: %.4f" % (epoch, total_train_loss / len(train_loader)))


def validation():
    model.eval()
    total_eval_accuracy = 0
    total_eval_loss = 0
    for batch in val_dataloader:
        with torch.no_grad():
            # 正常传播
            input_ids = batch['input_ids'].to(device)
            attention_mask = batch['attention_mask'].to(device)
            labels = batch['labels'].to(device)
            outputs = model(input_ids, attention_mask=attention_mask, labels=labels)

        loss = outputs[0]
        logits = outputs[1]

        total_eval_loss += loss.item()
        logits = logits.detach().cpu().numpy()
        label_ids = labels.to('cpu').numpy()
        total_eval_accuracy += flat_accuracy(logits, label_ids)

    avg_val_accuracy = total_eval_accuracy / len(val_dataloader)
    print("Accuracy: %.4f" % (avg_val_accuracy))
    print("Average testing loss: %.4f" % (total_eval_loss / len(val_dataloader)))
    print("-------------------------------")


if __name__ == '__main__':
    bq_train = read_tsv('data/bq_corpus/train.tsv', ['index', 'question1', 'question2', 'label'])
    q1_train, q1_val, q2_train, q2_val, train_label, test_label = train_test_split(
        bq_train['question1'].iloc[:],
        bq_train['question2'].iloc[:],
        bq_train['label'].iloc[:],
        test_size=0.1,
        stratify=bq_train['label'].iloc[:])

    # input_ids:字的编码
    # token_type_ids:标识是第一个句子还是第二个句子
    # attention_mask:标识是不是填充

    # transformers bert相关的模型使用和加载
    from transformers import BertTokenizer

    # 分词器,词典
    tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')
    train_encoding = tokenizer(list(q1_train), list(q2_train),
                               truncation=True, padding=True, max_length=100)
    val_encoding = tokenizer(list(q1_val), list(q2_val),
                             truncation=True, padding=True, max_length=100)

    train_dataset = bqDataset(train_encoding, list(train_label))
    val_dataset = bqDataset(val_encoding, list(test_label))

    from transformers import BertForNextSentencePrediction, AdamW, get_linear_schedule_with_warmup

    model = BertForNextSentencePrediction.from_pretrained('bert-base-chinese')
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    model.to(device)

    # 单个读取到批量读取
    train_loader = DataLoader(train_dataset, batch_size=16, num_workers=6, pin_memory=True, shuffle=True)
    val_dataloader = DataLoader(val_dataset, batch_size=16, num_workers=6, pin_memory=True, shuffle=True)

    # 优化方法
    optim = AdamW(model.parameters(), lr=1e-5)
    for epoch in range(20):
        print("------------Epoch: %d ----------------" % epoch)
        train()
        validation()
        torch.save(model.state_dict(), f'model_{epoch}.pt')

进行测试

def predict():
    model.eval()
    test_predict = []
    for batch in test_dataloader:
        with torch.no_grad():
            # 正常传播
            input_ids = batch['input_ids'].to(device)
            attention_mask = batch['attention_mask'].to(device)
            labels = batch['labels'].to(device)
            outputs = model(input_ids, attention_mask=attention_mask, labels=labels)
        
        loss = outputs[0]
        logits = outputs[1]

        logits = logits.detach().cpu().numpy()
        label_ids = labels.to('cpu').numpy()
        test_predict += list(np.argmax(logits, axis=1).flatten())
        
    return test_predict

生成结果

test_label = predict()
pd.DataFrame({'label':test_label}).to_csv('submit.csv', index=None)

结果还错!在第5个epoch时,验证集准确率达到92%。后面进行细微的调参应该还有更不错的效果。

任务9:Bert-flow、Bert-white、SimCSE

  • 步骤1 :学习Bert-white原理和实现
  • 步骤2 :学习SimCSE原理和实现

Bert-flow,Bert-whitning和SimCSE三种方法总计记录在我的这篇博客中:文本表达:解决BERT中的各向异性方法总结

你可能感兴趣的:(NLP,自然语言处理,python,深度学习)