NLP-Beginner任务三学习笔记:基于注意力机制的文本匹配

**输入两个句子判断,判断它们之间的关系。参考ESIM(可以只用LSTM,忽略Tree-LSTM),用双向的注意力机制实现**

数据集:The Stanford Natural Language Processing Group

任务一博客链接:https://blog.csdn.net/qq_51983316/article/details/129314052

任务二博客链接:https://blog.csdn.net/qq_51983316/article/details/129387225

参考论文:
1、Reasoning about Entailment with Neural Attention https://arxiv.org/pdf/1509.06664v1.pdf
2、Enhanced LSTM for Natural Language Inference https://arxiv.org/pdf/1609.06038v3.pdf

目录

一、数据集 

二、知识点学习 

(一)文本匹配

1、基本概念

2、任务定义

(二)BiLSTM 模型

1、LSTM

2、BiLSTM

(三)注意力机制

(四)ESIM 模型详解

1、Input Encoding

2、Local Inference Modeling

3、Inference Composition

4、Prediction

三、实验

(一)代码实现

1、main.py

2、feature_extraction.py

3、绘制对比图

(二)结果分析

一、数据集 

训练集共有 550152 项,语言为英文,匹配关系有蕴含(Entailment)、矛盾(Contradiction)、中立/不冲突(Neutral)和未知(-)四种,具体分布情况如下(-出现的非常少):

Training pairs: 550152

Dev pairs: 10000

Test pairs: 10000

Total pairs: 570152

Train labels: {'entailment': 183416, 'neutral': 182764, '-': 785, 'contradiction': 183187}
Dev labels: {'entailment': 3329, 'neutral': 3235, '-': 158, 'contradiction': 3278}
Test labels: {'entailment': 3368, 'neutral': 3219, '-': 176, 'contradiction': 3237}

sentence1: A black race car starts up in front of a crowd of people.
sentence2: A man is driving down a lonely road.
output: 矛盾(C)

sentence1: A soccer game with multiple males playing.
sentence2: Some men are playing a sport.
output: 蕴含(E)

sentence1: A smiling costumed woman is holding an umbrella.
sentence2:A happy woman in a fairy costume holds an umbrella.
output: 中立(N)

二、知识点学习 

(一)文本匹配

1、基本概念

文本匹配是NLP领域的一个重要的基础问题,顾名思义,文本匹配就是通过计算文本相似度判断两段文本之间的关系,如相似/相悖等。其中,信息检索、自然语言推理、问答匹配、机器翻译、对话系统等NLP任务都可以视为文本匹配问题。文本匹配可以抽象为给定一段文本作为查询(Query),从大量的文档(Documents)匹配出最佳的文档。

例如网页搜索可抽象为网页同用户搜索Query的一个相关性匹配问题,自动问答可抽象为候选答案与问题的满足度匹配问题,文本去重可以抽象为文本与文本的相似度匹配问题。 

2、任务定义

传统的方案主要是基于统计学方法通过词汇重合度来计算两段文本的字面相似,通过字面相似度来衡量文本的匹配度不妥,存在较多的不足,因为同一语义的文本在形式上千变万化,两段文本可以表现为字面相似但词序不同而导致语义完全相反;例如:“我喜欢你”,“我不喜欢你”,这两句话表现的语义完全不一样,但是从字层面表现的结果,相似度较高。

因此引入深度神经网络

表示型模型主要是将两段文本转换成一个语义向量,然后计算向量之间的相似度,其更侧重对语义向量表示层的构建,它的优势是结构简单、解释性强,且易于实现,是深度学习出现之后应用最广泛的深度文本匹配方法。典型的网络结构有 DSSM、LSTM 和 ESIM。

交互型模型摒弃了表示型模型的先建模后匹配的思路,通过attention为代表的结构来对两段文本进行不同粒度的交互(词级、短语级等),然后将各个粒度的匹配结果通过一种结构来聚合起来,作为一个超级特征向量进而得到最终的匹配关系。假设全局的匹配度依赖于局部的匹配度,在输入层就进行词语间的匹配和语义建模,它的优势是可以很好的把握语义焦点,对上下文重要性合理建模,效果较优于表示型,后续被广泛使用。

(二)BiLSTM 模型

1、LSTM

LSTM(Long short-term memory)是一种特殊的RNN,解决了RNN长期记忆能力不足、梯度消失、梯度爆炸等问题。LSTM非常适合对时间数据序列进行处理,原因是在时间序列中的重要事件之间可能存在未知持续时间的滞后,而对于间隙长度的相对不敏感性是LSTM的一大优势。‎

LSTM单元由输入门、遗忘门、输出门和单元状态组成,该单元记住任意时间间隔内的值,三个门控制进出单元的信息流,LSTM模型结构如下图所示。

LSTM 与 RNN一样,也是通过内部状态的传递来发掘序列元素间的依赖关系。LSTM 为了解决RNN 梯度更新上的缺陷,引入了门控机制,LSTM的门控环节分为遗忘、输入、输出三部分并且引入了一个状态单元协调整个网络的运作

公共LSTM单元由输入门、遗忘门、输出门和单元状态组成,该单元记住任意时间间隔内的值,并且三个门控制进出单元的信息流。其中,输入门决定当前时刻网络的输入有多少保存到单元状态;遗忘门决定上一时刻的单元状态有多少保留到当前时刻;而输出门控制当前单元状态有多少需要输出到当前的输出值。LSTM神经元结构和工作机制如图:

对于LSTM神经元结构,x{_t} 为 t 时刻输入,h{_t}为 t 时刻隐藏层状态,C_t为 t 时刻单元内部状态,σ  tanh 为激活函数。

2、BiLSTM

双向长短期记忆神经网络(Bidirectional Long Short Term Memory Network)是过去和未来隐藏层的状态都可以递归,进行反馈的神经网络,可以很好的发掘时间序列数据间隐藏的联系。BiLSTM 网络可以挖掘当前数据同过去及未来时刻数据的内在联系,提升模型预测精度和数据利用率。

相较于单向的 LSTM 网络,BiLSTM 具有正向和反向传播的双向循环结构。除此以外,BiLSTM 在 LSTM 数据从过去到未来单向流动的基础上增加了从未来到过去的数据流向,且用于过去的隐藏层和用于未来的隐藏层之间相互独立,所以 BiLSTM 可以更好的发掘数据的时序特征,使用BiLSTM模型并沿时间轴进行展开,具体结构如下图所示。

其中,x 为模型输入,h 为隐藏层状态,y 为输出。BiLSTM可以同时处理正反两个时间流向的模型,因此其具有两个方向的隐藏层。如图中所示正向传播的隐藏层与反向传播的隐藏层之间并没有发生交互,可以拆分开,当成两个独立且数据流向相反的网络。

假设 \vec{h_t} 为 t 时刻正向 LSTM 网络的隐藏层状态,计算公式如下所示。可以看作是单层的 LSTM 网络,由 t-1 时刻状态 \vec{h_{t-1}},计算时刻状态 \vec{h_t} 的过程,X_t 为 t 时刻的输入。

\vec{h_t}=LSTM\left(X_t,\ \vec{h_{t-1}}\right)

其中,\vec{h_t} 为 t 时刻正向 LSTM 网络的隐藏层状态,LSTM为LSTM单元,X_t 为 t 时刻的输入,\vec{h_{t-1}} 为  t-1 时刻正向 LSTM 网络的隐藏层状态。同样地,反向 LSTM 网络的隐藏层状态 \vec{h_t} 计算方式也和上述公式一致。

举例:

前向的LSTML依次输入“我”,“爱”,“你”得到三个向量{hL0,hL1,hL2}。

后向的LSTMR依次输入“你”,“爱”,“我”得到三个向量{hR0,hR1,hR2}。

最后将前向和后向的隐向量进行拼接得到{[hL0,hR2],[hL1,hR1],[hL2,hR0]},即{h0,h1,h2}。对于情感分类任务来说,我们采用的句子表示往往是[hL2,hR2],因为这其中包含了前向和后向的所有信息。

在这里插入图片描述

(三)注意力机制

注意力机制(Attention Mechanism)最早应用于计算机视觉领域。

顾名思义,该机制通过模拟人的大脑在某一时刻,对大量信息中的某一信息分配更多的注意力,在信息处理中对模型输入序列的接受呈现有选择性的特点。在编解码器框架内,通过在编码段加入Attention机制,对源数据序列进行数据加权变换,或者在解码端引入Attention机制,对目标数据进行加权变化,可以有效提高序列对序列的自然方式下的系统表现。

注意力一般分为两种:
(1)自上而下的有意识的注意力,称为聚焦式注意力(Focus Attention)。聚焦式注意力是指有预定目的、依赖任务的,主动有意识地聚焦于某一对象的注意力。
(2)自下而上的无意识的注意力,称为基于显著性的注意力(Saliency Based Attention)。由外界刺激驱动的注意,不需要主动干预,也和任务无关。如果一个对象的刺激信息不同于其周围信息,一种无意识的“赢者通吃”(Winner-Take-All)或者门控(Gating)机制就可以把注意力转向这个对象。不管这些注意力是有意还是无意,大部分的人脑活动都需要依赖注意力。

量化来说,Attention机制就是给定一组向量集合values,以及一个向量query,该机制根据该query计算values的加权求和,其实就是一个查询(query)到一系列键值(key-value)对的映射。

注意力机制的计算可以分为两步:一是在所有输入信息上计算注意力分布,二是根据注意力分布来计算输入信息的加权平均

我们从输出端,即解码器部分,倒过来一步一步分析Attention机制的原理与公式。

S_t=f(S_{t-1},\ Y_{t-1},C_t)

其中,S_t 是指解码器在 t 时刻的状态输出,S_{t-1} 是指解码器在 t-1 时刻的状态输出,Y_{t-1} 是 t-1 时刻的 label,f 是一个RNN。 

C_t=\sum_{j=1}^{T_x}{a_{tj}h_j}

利用 a_{tj} 进行加权求和得到 C_t。其中,h_j 是指第 j 个输入在编码器里的输出,也称为隐层状态。而 {a_{tj} 是相应的权重注意力分布,其计算公式如下:

a_{tj}=\frac{exp(e_{tj})}{\sum_{k=1}^{T_x}{exp(e_{tk})}}

在此 a 指当前这一步解码器对齐第 j 个输入的程度

e_{tj}=g(S_{t-1},\ h_j)=V\cdot tanh(W\cdot h_{j}+U\cdot S_{t-1}+b)

在此 g 可以用一个小型的神经网络来逼近,它用来计算S_{t-1}h_j这两者的关系分数(打分函数),如果分数大则说明关注度较高,注意力分布就会更加集中在这个输入序列上。 

综上,Attention机制总结来说就是当前一步输出S_t应该对齐哪一步输入,主要取决于前一步输出S_{t-1}和这一步输入的编码器结果h_j,机制原理如下图所示。

 Attention机制具有鲜明的优点。该机制不仅可以灵活的捕捉全局和局部的联系,也通过并行计算减少模型训练时间。Attention机制每一步计算不依赖于上一步的计算结果,因此可以并行处理。除此以外,该机制的模型复杂度小,参数较少,模型效果也较好。

(四)ESIM 模型详解

原始论文:Enhanced LSTM for Natural Language Inference

论文解读:https://zhuanlan.zhihu.com/p/500151241

首先,需要明确自然语言推理(Natural Language Inference,NLI)的定义:主要用来判断两个句子给定前提(premise)和假设(hypothesis),判断语义上的关系,一般可以分为:Entailment(蕴含)、Contradiction(矛盾)、Neutral(中立)。在 NLP 中,判断蕴含或是矛盾的关系十分必要,例如信息检索、语义分析、常识推理等方面都会用到。评价标准简单有效,可以直接在NLI中专注于语义理解和语义表示,如此生成好的句子就可以直接迁移应用到其他的任务。

ESIM的模型框架如下:

作者在文中提到可以采用句法的Tree-LSTM处理,也可以用BiLSTM处理。在此只用LSTM,忽略Tree-LSTM,并用双向的注意力机制实现。

由上述框架可以看出,ESIM一共包含四部分:

  • Input Encoding(输入)
  • Local Inference Modeling(局部推理建模
  • Inference Composition(组合推理
  • Prediction(输出)

1、Input Encoding

首先使用BiLSTM来对输入的两个句子进行embedding嵌入: 

在这里插入图片描述

编码公式如下:

经过BiLSTM的编码之后得到a_bar b_bar,此步主要是对这两个语句中的词语进行上下文表示。其中 \vec{a_i} 和 \vec{b_j} 都是BiLSTM对应时间步上隐藏层的输出构成新的Embedding。

代码实现

""" 定义PyTorch模型类Input_Encoding,将输入的文本序列进行编码 """
class Input_Encoding(nn.Module):
    def __init__(self, len_feature, len_hidden, len_words, longest, weight=None, layer=1, batch_first=True, drop_out=0.5):
        super(Input_Encoding, self).__init__()
        # 输入的特征维度
        self.len_feature = len_feature
        # LSTM 的隐藏层大小
        self.len_hidden = len_hidden
        # 词嵌入的单词数
        self.len_words = len_words
        #  LSTM 的层数
        self.layer = layer
        # 最长的输入句子的长度
        self.longest = longest
        # dropout 层,用于防止过拟合
        self.dropout = nn.Dropout(drop_out)
        # 如果 weight 是 None,则使用 xavier_normal_ 函数初始化一个形状为 (len_words, len_feature) 的张量 x,并使用这个张量作为词嵌入层的权重
        if weight is None:
            x = nn.init.xavier_normal_(torch.Tensor(len_words, len_feature))
            self.embedding = nn.Embedding(num_embeddings=len_words, embedding_dim=len_feature, _weight=x).cuda()
        # 否则,使用给定的权重 weight 初始化词嵌入层
        else:
            self.embedding = nn.Embedding(num_embeddings=len_words, embedding_dim=len_feature, _weight=weight).cuda()
        # 初始化双向 LSTM
        self.lstm = nn.LSTM(input_size=len_feature, hidden_size=len_hidden, num_layers=layer, batch_first=batch_first,
                            bidirectional=True).cuda()

    # 定义前向传播函数,用于对输入的句子进行编码
    def forward(self, x):
        # 将输入的数据转换为 torch.LongTensor 类型并放到 GPU 上
        x = torch.LongTensor(x).cuda()
        # 使用词嵌入层对输入进行词嵌入
        x = self.embedding(x)
        # 使用 dropout 对词嵌入结果进行正则化
        x = self.dropout(x)
        # 使用 flatten_parameters() 函数将 LSTM 的参数展开
        self.lstm.flatten_parameters()
        # 将结果输入到 LSTM 中进行编码
        x, _ = self.lstm(x)
        return x

2、Local Inference Modeling

该层主要任务是进行差异性计算,即计算两个句子词与词之间的相似度,得到相似度矩阵:

对于表示向量,如果两个词之间联系越大,就意味着他们之间的距离和夹角就越少,比如(1,0)和(0,1)之间的联系,就没有(0.5,0.5)和(0.5,0.5)之间的联系大。

其次生成相似性加权后的向量:

这也是ESIM的精髓所在。简言之,比如 a 中有一个单词 "good",首先我分析这个词和另一句话中各个词之间的联系,计算得到的结果 e_{ij} 标准化后作为权重,用另一句话中的各个词向量按照权重去表示 "good",逐个分析对比后得到新的序列。以上过程称为本地推理模型。

得到encoding值与加权encoding值之后,下一步是分别对这两个值做差异性计算,作者认为这样的操作有助于模型效果的提升,论文中有两种计算方法,分别是对位相减与对位相乘,最后把encoding两个状态的值与相减、相乘的值拼接起来。

本质上来说就是对两个句子的表示做差和点积:

代码实现

""" 局部推理模块 """
"""基于注意力机制的编码器-解码器框架:注意力机制通常被用来计算输入序列中每个位置对于输出序列的重要程度,进而加强对于相关信息的关注和利用。
本代码中,a_bar和b_bar分别表示输入序列a和b的编码表示,通过计算矩阵e,可以得到a_bar和b_bar之间的交互关系。
接着,通过softmax函数将e中的数值归一化,得到a_bar和b_bar在交互关系下的注意力分布a_tilde和b_tilde,分别用来加权计算b_bar和a_bar的信息。
最后,将a_bar、a_tilde、b_bar、b_tilde之间的差异和乘积信息进行拼接,形成新的表示m_a和m_b。
进一步用于后续的分类或回归任务。"""
class Local_Inference_Modeling(nn.Module):
    def __init__(self):
        super(Local_Inference_Modeling, self).__init__()
        # 将输入值转换为概率分布
        self.softmax_1 = nn.Softmax(dim=1).cuda()
        self.softmax_2 = nn.Softmax(dim=2).cuda()

    # 这个模型类的前向函数实现了两个句子之间的局部推理模型
    def forward(self, a_bar, b_bar):
        # e 是注意力矩阵。matmul()函数计算两个输入张量a_bar和b_bar的矩阵乘积
        e = torch.matmul(a_bar, b_bar.transpose(1, 2)).cuda()
        # 将 e 矩阵的第二个维度进行 softmax 操作,得到一个概率分布矩阵
        a_tilde = self.softmax_2(e)
        # 将 a_tilde 与 b_bar 做矩阵乘法,得到一个新的矩阵
        a_tilde = a_tilde.bmm(b_bar)
        b_tilde = self.softmax_1(e)
        b_tilde = b_tilde.transpose(1, 2).bmm(a_bar)
        # 这四个矩阵在最后一个维度上进行拼接,得到一个新的矩阵
        m_a = torch.cat([a_bar, a_tilde, a_bar - a_tilde, a_bar * a_tilde], dim=-1)
        m_b = torch.cat([b_bar, b_tilde, b_bar - b_tilde, b_bar * b_tilde], dim=-1)
        return m_a, m_b

3、Inference Composition

由于ESIM还需要综合所有信息进行全局分析,因此通过组合推理模块把所有信息储存在一个序列中。在这一层中,把之前的值再一次送到了BiLSTM中,这里的BiLSTM的作用和之前的并不一样,这里主要是用于捕获局部推理信息 m_a 和 m_b 及其上下文,以便进行推理组合。

 v_{a,t} = BiLSTM(F(m_{a,t}),t)

 v_{b,t} = BiLSTM(F(m_{b,t}),t)

其中,F是一个单层神经网络(ReLU作为激活函数),主要用来减少模型的参数避免过拟合,另外,上面的 t 表示BiLSTM在 t 时刻的输出。 最后把BiLSTM得到的值进行池化操作,分别是最大池化与平均池化,并把池化之后的值再一次的拼接起来。

 代码实现

""" 组合推理模块 (对蕴含假设和前提进行编码和组合) """
class Inference_Composition(nn.Module):
    # len_feature 特征向量的维度;len_hidden_m 在Local_Inference_Modeling中得到的组合向量的维度
    def __init__(self, len_feature, len_hidden_m, len_hidden, layer=1, batch_first=True, drop_out=0.5):
        # 调用父类构造函数以初始化该模块
        super(Inference_Composition, self).__init__()
        # 定义线性变换层,将Local_Inference_Modeling得到的组合向量降维为len_feature维
        self.linear = nn.Linear(len_hidden_m, len_feature).cuda()
        # 定义LSTM层,其中包含了两个方向的隐状态,并将降维后的组合向量作为输入;bidirectional=True表示为双向LSTM
        self.lstm = nn.LSTM(input_size=len_feature, hidden_size=len_hidden, num_layers=layer, batch_first=batch_first,
                            bidirectional=True).cuda()
        self.dropout = nn.Dropout(drop_out).cuda()

    def forward(self, x):
        x = self.linear(x)
        x = self.dropout(x)
        # 将LSTM层的权重参数展开以提高效率
        self.lstm.flatten_parameters()
        # 将降维后的组合向量作为LSTM层的输入;输出为x
        x, _ = self.lstm(x)
        return x

4、Prediction

然后再将 v 与一个全连接层相连,其中全连接层使用了tanh的激活函数,得到的结果送到softmax层以便输出,使用多类别的交叉熵计算损失。

代码实现

""" 预测类 """
class Prediction(nn.Module):
    # len_v 输入的向量的长度;len_mid 表示中间层的大小
    def __init__(self, len_v, len_mid, type_num=4, drop_out=0.5):
        super(Prediction, self).__init__()
        # 定义 mlp 多层感知机
        # nn.Dropout(drop_out):dropout 层,防止过拟合。
        # nn.Linear(len_v, len_mid):全连接层,将输入的向量映射到中间层。
        # nn.Tanh():激活函数,使用双曲正切函数进行非线性变换。
        # nn.Linear(len_mid, type_num):全连接层,将中间层的特征映射到预测的类别数。
        self.mlp = nn.Sequential(nn.Dropout(drop_out), nn.Linear(len_v, len_mid), nn.Tanh(),
                                 nn.Linear(len_mid, type_num)).cuda()

    # 定义前向传播方法
    def forward(self, a, b):
        # 计算 m_a 在第二个维度上的平均值
        v_a_avg = a.sum(1)/a.shape[1]
        # 计算 m_a 在第二个维度上的最大值
        v_a_max = a.max(1)[0]
        # 计算 m_b 在第二个维度上的平均值
        v_b_avg = b.sum(1) / b.shape[1]
        # # 计算 m_b 在第二个维度上的最大值
        v_b_max = b.max(1)[0]
        # 将四个向量连接在一起形成一个新的向量
        out_put = torch.cat((v_a_avg, v_a_max,v_b_avg,v_b_max), dim=-1)
        # 将新的向量输入到多层感知机中进行预测,返回预测结果
        return self.mlp(out_put)

总之,ESIM模型整体架构如下:

ESIM算法总体思路比较清晰,一些过程也是在人为地构建特征。在模型实现过程中则需要考虑批次训练中的数据对应问题,以及可能出现的误计算,例如最大池化的计算。

代码实现

""" ESIM模型 """
"""在 ESIM 模型中,首先将输入的句子 a 和 b 通过 Input_Encoding 模块编码成相应的表示,
然后将编码后的 a 和 b 作为输入传给 Local_Inference_Modeling 模块进行局部推理,
接着将得到的结果传递给 Inference_Composition 模块进行推理融合,
最后通过 Prediction 模块预测两个句子是否具有某种关系"""
class ESIM(nn.Module):
    def __init__(self, len_feature, len_hidden, len_words, longest, type_num=4, weight=None, layer=1, batch_first=True,
                 drop_out=0.5):
        super(ESIM, self).__init__()
        # 将词嵌入的长度和句子中最长的长度分别赋值给实例变量self.len_words和self.longest
        self.len_words = len_words
        self.longest = longest
        # 创建input_encoding对象,是Input_Encoding类的一个实例,Input_Encoding类是用来对输入进行编码的
        # layer表示ESIM模型的层数
        self.input_encoding = Input_Encoding(len_feature, len_hidden, len_words, longest, weight=weight, layer=layer,
                                             batch_first=batch_first, drop_out=drop_out)
        # 创建对象,用来对两个句子进行本地推理的,也就是计算两个句子中每个词语之间的关联度
        self.local_inference_modeling = Local_Inference_Modeling()
        # Inference Composition层中的输入是由四个部分拼接而成的,分别是a_bar, a_tilde, a_bar - a_tilde和 a_bar * a_tilde。
        # 每个部分的特征向量维度都是len_hidden,所以总共是4 * len_hidden,且在拼接之前需要将a_bar和b_bar的维度都扩展为4 * len_hidden。
        # 因此,输入到Inference Composition层中的向量维度是8 * len_hidden。
        self.inference_composition = Inference_Composition(len_feature, 8 * len_hidden, len_hidden, layer=layer,
                                                           batch_first=batch_first, drop_out=drop_out)
        self.prediction = Prediction(8 * len_hidden, len_hidden, type_num=type_num, drop_out=drop_out)

    def forward(self, a, b):
        a_bar=self.input_encoding(a)
        b_bar=self.input_encoding(b)

        m_a, m_b = self.local_inference_modeling(a_bar, b_bar)

        v_a = self.inference_composition(m_a)
        v_b = self.inference_composition(m_b)

        out_put = self.prediction(v_a, v_b)

        return out_put

三、实验

参数设置:

样本个数:550152
训练集:测试集 : 8:2
模型:ESIM
词嵌入初始化:随机初始化;GloVe预训练模型初始化

random_seed:2023
学习率:0.001

len_feature(输入的特征维度):50

len_hidden(LSTM的隐藏层大小):50

iter_times:50

batch_size:1000

运行环境:

python:3.7

pytorch:1.7.0(gpu)

cuda版本:10.1

(一)代码实现

1、main.py

import random
from feature_extraction import Random_embedding, Glove_embedding, get_batch
from comparison_plot import NN_plot, NN_embdding
from Neural_Network import ESIM

# 读取数据
with open('data/snli_1.0/snli_1.0_train.txt', 'r') as f:
    temp = f.readlines()

# 读取预训练词向量模型glove
with open('data/glove.6B.50d.txt', 'rb') as f:
    lines = f.readlines()

# 将GloVe模型训练得到的词向量存储到字典中
trained_dict = dict()
n = len(lines)
for i in range(n):
    line = lines[i].split()
    trained_dict[line[0].decode("utf-8").upper()] = [float(line[j]) for j in range(1, 51)]

# 初始化参数设置
data = temp[1:]
learning_rate = 0.001
len_feature = 50
len_hidden = 50
iter_times = 50
batch_size = 1000

# random embedding
random.seed(2023)
random_embedding = Random_embedding(data=data)
random_embedding.get_words()
random_embedding.get_id()

# trained embedding : glove
random.seed(2023)
glove_embedding = Glove_embedding(data=data, trained_dict=trained_dict)
glove_embedding.get_words()
glove_embedding.get_id()

# 绘图比较结果
NN_plot(random_embedding, glove_embedding, len_feature, len_hidden, learning_rate, batch_size, iter_times)

2、feature_extraction.py

与任务二的方法和代码一致,分别使用随机初始化和Glove预训练模型初始化进行特征提取

import random
import re
import torch
from torch.utils.data import Dataset, DataLoader
from torch.nn.utils.rnn import pad_sequence

""" 训练集和测试集的划分 """
def data_split(data, test_rate=0.2):
    # 创建两个空列表train和test,分别用来存放训练集和测试集数据
    train = list()
    test = list()
    # 计数器i记录添加的数据量
    i = 0
    # 使用for循环遍历输入的数据集data中的每一条数据
    for num in data:
        i += 1
        # 使用random.random()函数生成0-1之间的随机数
        # 如果随机数大于0.2,则将该条数据添加到训练集train中,否则添加到测试集test中。
        if random.random() > test_rate:
            train.append(num)
        else:
            test.append(num)
    # 此时返回分割好的测试集和训练集
    return train, test


""" 定义Random_embedding的类 """
class Random_embedding():
    def __init__(self, data, test_rate=0.2):
        self.dict_words = dict()
        # 将输入的数据集按照制表符(\t)进行分割
        _data = [item.split('\t') for item in data]
        # 列表中第6个、第7个元素分别是 sentence1 和 sentence2,第1个元素是label标签值
        # 将这些元素重新组合成一个新的二维列表,并将其赋值给实例变量self.data
        self.data = [[item[5], item[6], item[0]] for item in _data]
        # 根据第一个句子的长度对数据集进行排序,以便后续的处理
        self.data.sort(key=lambda x: len(x[0].split()))
        self.len_words = 0
        self.train, self.test = data_split(self.data, test_rate=test_rate)
        # 定义字典type_dict,用于将关系类型转化为数字编码
        self.type_dict = {'-': 0, 'contradiction': 1, 'entailment': 2, 'neutral': 3}
        # 将训练集中的每个子列表的第3个元素转化为数字编码,并存储在实例变量 self.train_y 中
        self.train_y = [self.type_dict[term[2]] for term in self.train]
        self.test_y = [self.type_dict[term[2]] for term in self.test]
        # 初始化训练集和测试集中两个句子的矩阵,分别表示第一个句子和第二个句子
        self.train_s1_matrix = list()
        self.test_s1_matrix = list()
        self.train_s2_matrix = list()
        self.test_s2_matrix = list()
        self.longest = 0

    # 定义 get_words 的方法,用于将数据集中的单词存储到词典 dict_words 中
    def get_words(self):
        # 定义正则表达式模式,用于匹配单词
        # 该模式包含大写字母、小写字母、竖线和单引号,可以匹配英文字母和撇号
        pattern = '[A-Za-z|\']+'
        for term in self.data:
            # 这行开始遍历term中的每个字符串,将其转换为大写字母并使用正则表达式模式pattern匹配其中的单词
            # 对于每个匹配到的单词,如果它不在单词字典self.dict_words中,就将它添加到字典中,并为它分配一个新的编号
            # 该函数利用字典的特性,实现了单词计数和编号分配的功能
            for i in range(2):
                s = term[i]
                s = s.upper()
                words = re.findall(pattern, s)
                for word in words:
                    if word not in self.dict_words:
                        self.dict_words[word] = len(self.dict_words)+1
        self.len_words = len(self.dict_words)

    def get_id(self):
        pattern = '[A-Za-z|\']+'
        for term in self.train:
            s = term[0]
            s = s.upper()
            words = re.findall(pattern, s)
            # 对于每个term,将其两个句子分别处理成编号列表item
            item = [self.dict_words[word] for word in words]
            # 将item的长度与当前已处理的最长句子长度self.longest比较,更新self.longest的值
            self.longest = max(self.longest, len(item))
            # 将item添加到对应的train_s1_matrix和train_s2_matrix中
            self.train_s1_matrix.append(item)
            s = term[1]
            s = s.upper()
            words = re.findall(pattern, s)
            item = [self.dict_words[word] for word in words]
            self.longest = max(self.longest, len(item))
            self.train_s2_matrix.append(item)
        for term in self.test:
            s = term[0]
            s = s.upper()
            words = re.findall(pattern, s)
            item = [self.dict_words[word] for word in words]
            self.longest = max(self.longest, len(item))
            self.test_s1_matrix.append(item)
            s = term[1]
            s = s.upper()
            words = re.findall(pattern, s)
            item = [self.dict_words[word] for word in words]
            self.longest = max(self.longest, len(item))
            self.test_s2_matrix.append(item)
        # 表示单词字典中的单词数量(包括一个额外的未使用的编号0)
        self.len_words += 1


""" 定义Glove_embedding的类 """
class Glove_embedding():
    def __init__(self, data, trained_dict, test_rate=0.2):
        self.dict_words = dict()
        _data = [item.split('\t') for item in data]
        self.data = [[item[5], item[6], item[0]] for item in _data]
        self.data.sort(key=lambda x: len(x[0].split()))
        # 已训练好的词向量字典
        self.trained_dict = trained_dict
        self.len_words = 0
        self.train, self.test = data_split(self.data, test_rate=test_rate)
        # 定义关系类型的字典 self.type_dict,其中键为关系类型,值为相应的数字标识
        self.type_dict = {'-': 0, 'contradiction': 1, 'entailment': 2, 'neutral': 3}
        # 将训练集 self.train 中每个元素的第3个元素(即关系类型)转换为数字标识,并赋值给 self.train_y
        self.train_y = [self.type_dict[term[2]] for term in self.train]
        self.test_y = [self.type_dict[term[2]] for term in self.test]
        self.train_s1_matrix = list()
        self.test_s1_matrix = list()
        self.train_s2_matrix = list()
        self.test_s2_matrix = list()
        self.longest = 0
        self.embedding = list()  # 词向量矩阵

    def get_words(self):
        # 首先在嵌入矩阵 self.embedding 中添加一个全零向量,这个向量将会用作填充
        self.embedding.append([0]*50)
        pattern = '[A-Za-z|\']+'
        for term in self.data:
            for i in range(2):
                s = term[i]
                s = s.upper()
                words = re.findall(pattern, s)
                for word in words:  # Process every word
                    if word not in self.dict_words:
                        self.dict_words[word] = len(self.dict_words)
                        # 如果它在预训练的词向量字典 self.trained_dict 中,则将其对应的词向量添加到嵌入矩阵 self.embedding
                        if word in self.trained_dict:
                            self.embedding.append(self.trained_dict[word])
                        else:
                            # 否则将一个全零的向量添加到该矩阵中,将嵌入矩阵中的所有向量长度都填充到50(因为在预训练的词向量字典中,每个词的向量长度都是 50)
                            self.embedding.append([0] * 50)
        self.len_words = len(self.dict_words)

    def get_id(self):
        pattern = '[A-Za-z|\']+'
        # 对于训练集中的每个元素(句子对),将句子中的单词转换为对应的 ID
        for term in self.train:
            # sentence1
            s = term[0]
            s = s.upper()
            words = re.findall(pattern, s)
            item = [self.dict_words[word] for word in words]
            # 记录最长的 ID 序列长度
            self.longest = max(self.longest, len(item))
            # train_s1/s2_matrix 表示句子1和句子2的 ID 序列
            self.train_s1_matrix.append(item)
            # sentence2
            s = term[1]
            s = s.upper()
            words = re.findall(pattern, s)
            item = [self.dict_words[word] for word in words]
            self.longest = max(self.longest, len(item))
            self.train_s2_matrix.append(item)
        # 对于测试集中的每个元素(句子对),将句子中的单词转换为对应的 ID
        for term in self.test:
            s = term[0]
            s = s.upper()
            words = re.findall(pattern, s)
            item = [self.dict_words[word] for word in words]
            self.longest = max(self.longest, len(item))
            self.test_s1_matrix.append(item)
            s = term[1]
            s = s.upper()
            words = re.findall(pattern, s)
            item = [self.dict_words[word] for word in words]
            self.longest = max(self.longest, len(item))
            self.test_s2_matrix.append(item)
        # 将字典中的单词数加1,以便添加一个用于填充句子的特殊符号
        self.len_words += 1


""" 自定义数据集,用于文本分类任务 """
class ClsDataset(Dataset):  # 定义 ClsDataset 的类,继承了 Dataset 类
    def __init__(self, sentence1, sentence2, relation):
        self.sentence1 = sentence1
        self.sentence2 = sentence2
        self.relation = relation  # 标签
    # 用于获取数据集中的一个样本,item 表示样本的索引,函数返回索引为 item 的句子和关系
    def __getitem__(self, item):
        return self.sentence1[item], self.sentence2[item], self.relation[item]
    def __len__(self):
        # 返回数据集中样本的数量
        return len(self.relation)


""" 自定义batch数据的输出形式 """
# 函数 collate_fn是 PyTorch 中 DataLoader 类的一个参数,用于在迭代数据时组合数据样本
# 将一个 batch 中的数据样本按照句子长度进行填充,以便构造成一个张量
def collate_fn(batch_data):
    sents1, sents2, labels = zip(*batch_data)
    # 转换为张量
    sentences1 = [torch.LongTensor(sent) for sent in sents1]
    padded_sents1 = pad_sequence(sentences1, batch_first=True, padding_value=0)
    sentences2 = [torch.LongTensor(sent) for sent in sents2]
    padded_sents2 = pad_sequence(sentences2, batch_first=True, padding_value=0)
    return torch.LongTensor(padded_sents1), torch.LongTensor(padded_sents2),  torch.LongTensor(labels)


# 使用自定义数据集,通过 dataloader 可以实现对整个数据集的批量迭代
def get_batch(x1, x2, y, batch_size):
    dataset = ClsDataset(x1, x2, y)
    dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=False, drop_last=True, collate_fn=collate_fn)
    return dataloader

3、绘制对比图

import matplotlib.pyplot
import torch
import torch.nn.functional as F
from feature_extraction import get_batch
from torch import optim
from Neural_Network import ESIM
import random
import numpy


def NN_embdding(model, train, test, learning_rate, iter_times):
    # Adam优化器初始化优化器对象
    optimizer = optim.Adam(model.parameters(), lr=learning_rate)
    # 定义损失函数为交叉熵损失函数
    loss_fun = F.cross_entropy
    train_loss_record = list()
    test_loss_record = list()
    train_record = list()
    test_record = list()

    for iteration in range(iter_times):
      # 释放缓存
      torch.cuda.empty_cache()
      model.train()
      for i, batch in enumerate(train):
        torch.cuda.empty_cache()
        x1, x2, y = batch
        pred = model(x1, x2).cuda()
        # 清空优化器的梯度缓存
        optimizer.zero_grad()
        y = y.cuda()
        loss = loss_fun(pred, y).cuda()
        # 计算损失对模型参数的梯度
        loss.backward()
        # 更新模型参数
        optimizer.step()
      with torch.no_grad():
        model.eval()
        train_acc = list()
        test_acc = list()
        train_loss = 0
        test_loss = 0
        for i, batch in enumerate(train):
          torch.cuda.empty_cache()
          x1, x2, y = batch
          y=y.cuda()
          pred = model(x1, x2).cuda()
          loss = loss_fun(pred, y).cuda()
          train_loss += loss.item()
          _, y_pre = torch.max(pred, -1)
          acc = torch.mean((torch.tensor(y_pre == y, dtype=torch.float)))
          train_acc.append(acc)

        for i, batch in enumerate(test):
          torch.cuda.empty_cache()
          x1, x2, y = batch
          y=y.cuda()
          pred = model(x1, x2).cuda()
          loss = loss_fun(pred, y).cuda()
          test_loss += loss.item()
          _, y_pre = torch.max(pred, -1)
          acc = torch.mean((torch.tensor(y_pre == y, dtype=torch.float)))
          test_acc.append(acc)

      trains_acc = sum(train_acc) / len(train_acc)
      tests_acc = sum(test_acc) / len(test_acc)

      train_loss_record.append(train_loss / len(train_acc))
      test_loss_record.append(test_loss/ len(test_acc))
      train_record.append(trains_acc.cpu())
      test_record.append(tests_acc.cpu())
      print("---------- 迭代轮次", iteration + 1, "----------")
      print("Train loss:", train_loss / len(train_acc))
      print("Test loss:", test_loss / len(test_acc))
      print("Train accuracy:", trains_acc)
      print("Test accuracy:", tests_acc)

    return train_loss_record, test_loss_record, train_record, test_record


def NN_plot(random_embedding, glove_embedding, len_feature, len_hidden, learning_rate, batch_size, iter_times):
    train_random = get_batch(random_embedding.train_s1_matrix, random_embedding.train_s2_matrix,
                             random_embedding.train_y, batch_size)
    test_random = get_batch(random_embedding.test_s1_matrix, random_embedding.test_s2_matrix,
                            random_embedding.test_y, batch_size)
    train_glove = get_batch(glove_embedding.train_s1_matrix, glove_embedding.train_s2_matrix,
                            glove_embedding.train_y, batch_size)
    test_glove = get_batch(glove_embedding.test_s1_matrix, glove_embedding.test_s2_matrix,
                           glove_embedding.test_y, batch_size)
    random.seed(2023)
    numpy.random.seed(2023)
    torch.cuda.manual_seed(2023)
    torch.manual_seed(2023)
    random_model = ESIM(len_feature, len_hidden, random_embedding.len_words, longest=random_embedding.longest)
    random.seed(2023)
    numpy.random.seed(2023)
    torch.cuda.manual_seed(2023)
    torch.manual_seed(2023)
    glove_model = ESIM(len_feature, len_hidden, glove_embedding.len_words, longest=glove_embedding.longest,
                       weight=torch.tensor(glove_embedding.embedding, dtype=torch.float))
    random.seed(2023)
    numpy.random.seed(2023)
    torch.cuda.manual_seed(2023)
    torch.manual_seed(2023)
    trl_ran, tsl_ran, tra_ran, tea_ran = NN_embdding(random_model, train_random, test_random, learning_rate,
                                                     iter_times)
    random.seed(2023)
    numpy.random.seed(2023)
    torch.cuda.manual_seed(2023)
    torch.manual_seed(2023)
    trl_glo, tsl_glo, tra_glo, tea_glo = NN_embdding(glove_model, train_glove, test_glove, learning_rate,
                                                     iter_times)
    x = list(range(1, iter_times + 1))
    matplotlib.pyplot.subplot(2, 2, 1)
    matplotlib.pyplot.plot(x, trl_ran, 'r--', label='random')
    matplotlib.pyplot.plot(x, trl_glo, 'b--', label='glove')
    matplotlib.pyplot.legend(fontsize=10)
    matplotlib.pyplot.title("Train Loss")
    matplotlib.pyplot.xlabel("Iterations")
    matplotlib.pyplot.ylabel("Loss")
    matplotlib.pyplot.subplot(2, 2, 2)
    matplotlib.pyplot.plot(x, tsl_ran, 'r--', label='random')
    matplotlib.pyplot.plot(x, tsl_glo, 'b--', label='glove')
    matplotlib.pyplot.legend(fontsize=10)
    matplotlib.pyplot.title("Test Loss")
    matplotlib.pyplot.xlabel("Iterations")
    matplotlib.pyplot.ylabel("Loss")
    matplotlib.pyplot.subplot(2, 2, 3)
    matplotlib.pyplot.plot(x, tra_ran, 'r--', label='random')
    matplotlib.pyplot.plot(x, tra_glo, 'b--', label='glove')
    matplotlib.pyplot.legend(fontsize=10)
    matplotlib.pyplot.title("Train Accuracy")
    matplotlib.pyplot.xlabel("Iterations")
    matplotlib.pyplot.ylabel("Accuracy")
    matplotlib.pyplot.ylim(0, 1)
    matplotlib.pyplot.subplot(2, 2, 4)
    matplotlib.pyplot.plot(x, tea_ran, 'r--', label='random')
    matplotlib.pyplot.plot(x, tea_glo, 'b--', label='glove')
    matplotlib.pyplot.legend(fontsize=10)
    matplotlib.pyplot.title("Test Accuracy")
    matplotlib.pyplot.xlabel("Iterations")
    matplotlib.pyplot.ylabel("Accuracy")
    matplotlib.pyplot.ylim(0, 1)
    matplotlib.pyplot.tight_layout()
    fig = matplotlib.pyplot.gcf()
    fig.set_size_inches(8, 8, forward=True)
    matplotlib.pyplot.savefig('result/result.jpg')
    matplotlib.pyplot.show()

(二)结果分析

整体看来,无论是训练集还是测试集,随机初始化的效果均比glove预训练模型初始化的效果好

随机初始化结果:训练集能达到0.8698

glove预训练模型初始化结果:训练集达到0.8260

至于最后的结果随机初始化要优于预训练模型初始化的原因,可能有以下几点:

1、ESIM模型中采用了许多特殊的结构,如BiLSTM、注意力机制等。这些结构可以更好地捕捉语义信息,因此在某些情况下,使用随机初始化可以更好地适应模型结构。

2、随机初始化在某些情况下可能比预训练模型更好,这是因为预训练模型可能会过拟合到特定领域或任务的数据。例如,如果使用一个基于大规模通用语料库训练的预训练模型来解决特定领域的任务,该模型可能会过度拟合通用数据,并不一定能够很好地适应特定领域的数据。此时,使用随机初始化可以避免这种情况,因为它不会受到先前训练数据的影响,更容易适应特定任务的数据。但是在某些情况下,使用预训练模型可以获得更好的性能,这取决于预训练模型的质量和特定任务的数据。

3、ESIM模型可能会更好地适应特定的任务和数据集。预训练模型通常是在大规模的通用语料库上进行训练的,而ESIM模型是在特定的任务和数据集上进行训练的。

上述也只是设想,还请各位大佬批评指正!

总结:

由于是初学者,学习过程中参考了很多大佬的资料和代码,均附上参考链接:

1、https://blog.csdn.net/qq_42365109/article/details/115704688

2、邱锡鹏——《神经网络与深度学习》

3、https://blog.csdn.net/weixin_42691585/article/details/106665861

4、https://blog.csdn.net/Raki_J/article/details/122075646

5、https://blog.csdn.net/David_B/article/details/118703883

6、https://blog.csdn.net/Mr_Meng__NLP/article/details/122120520

7、【NLP】文本匹配——ESIM算法实现 - 知乎 (zhihu.com)

8、语义相似度匹配(二)—— ESIM模型_相似度匹配模型_微知girl的博客-CSDN博客 

9、https://blog.csdn.net/lq_fly_pig/article/details/123956552

10、https://blog.csdn.net/qq_43586043/article/details/114810767

11、双向长短期记忆网络(BiLSTM)详解_敷衍zgf的博客-CSDN博客

以上就是NLP-Beginner的任务三,欢迎各位前辈批评指正!

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
基于深度学文本分类任务是指利用深度学模型对文本进行情感分类。在这个任务中,我们使用了CNN和RNN模型来进行文本分类。数据集包含了15万余项英文文本,情感分为0-4共五类情感。任务的流程如下:输入数据→特征提取→神经网络设计→结果输出。 在特征提取阶段,我们使用了词嵌入(Word embedding)技术。词嵌入是一种将单词映射到低维向量空间的方法,它可以将单词的语义信息编码为向量表示。在本次任务中,我们参考了博客\[NLP-Beginner 任务二:基于深度学文本分类\](https://pytorch.org/Convolutional Neural Networks for Sentence Classification)中的方法,使用了预训练的词嵌入模型。 神经网络设计阶段,我们采用了卷积神经网络(CNN)和循环神经网络(RNN)的结合。具体来说,我们使用了四个卷积核,大小分别为2×d, 3×d, 4×d, 5×d。这样设计的目的是为了挖掘词组的特征。例如,2×d的卷积核用于挖掘两个连续单词之间的关系。在模型中,2×d的卷积核用红色框表示,3×d的卷积核用黄色框表示。 最后,我们将模型的输出结果进行分类,得到文本的情感分类结果。这个任务的目标是通过深度学模型对文本进行情感分类,以便更好地理解和分析文本数据。 #### 引用[.reference_title] - *1* *3* [NLP-Brginner 任务二:基于深度学文本分类](https://blog.csdn.net/m0_61688615/article/details/128713638)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [NLP基本任务二:基于深度学文本分类](https://blog.csdn.net/Mr_green_bean/article/details/90480918)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Q小Q琪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值