【NLP学习笔记08 Elmo模型+莫烦pytorch代码理解】


前言

Elmo(Embeddings from Language Models)是一种基于深度双向语言模型(Deep Bidirectional Language Model)的上下文相关的词向量表示方法;可以解决一词多义的问题!

传统方法将每个词表示为一个固定的向量,而Elmo考虑了每个词在不同上下文环境中的多个表示。

Elmo模型有两个关键组成部分:

  • 前向语言模型(Forward Language Model):这个模型从左到右阅读输入序列,并试图预测下一个词。每个词的前向隐藏状态表示了该词在上下文中的信息。
  • 后向语言模型(Backward Language Model):这个模型从右到左阅读输入序列,并试图预测前一个词。每个词的后向隐藏状态表示了该词在更远上下文中的信息。

在这里插入图片描述

ELMo的构成和原理

  1. 双向LSTM架构:
    ELMo模型由两个方向的LSTM组成:一个从左到右(forward LSTM),一个从右到左(backward LSTM)。

对于给定一个序列的 N 个tokens,前向语言模型计算序列的概率:
在这里插入图片描述
后向语言模型计算序列的概率:
在这里插入图片描述
双向的模型BiLM结合了前向和反向LM,联合最大化log 似然函数:
在这里插入图片描述

  1. 字符级别的卷积神经网络(CNN)编码器:
    ELMo还包含一个字符级别的CNN编码器,用于学习字符级特征。该编码器将单词拆分成字符,并通过一系列卷积层来提取字符级别的特征。

优缺点比较🎃

优点:
(1)上下文相关性:ELMo能够捕捉词在不同语境下的含义变化。它考虑了每个词在其周围上下文中的信息。
(2)模型灵活性:ELMo的模型架构是可选的,可以根据各种任务需求进行调整和扩展。它可以用于多种自然语言处理任务,包括文本分类、命名实体识别、问答系统等。
(3)预训练和微调:ELMo学习通用的上下文相关词向量表示。然后,在特定任务上进行微调,使得ELMo可以适应不同任务的要求,提高任务性能。
(4)可解释性:ELMo生成词向量的方式是基于LSTM模型的隐藏状态,这使得词向量的生成过程相对透明和可解释。这种可解释性有助于理解词向量的语义含义和上下文相关性。

缺点:
(1)计算复杂度:由于ELMo采用了多层双向LSTM的结构,其计算复杂度较高。
(2)数据需求:ELMo的预训练过程通常需要大量的无监督数据来学习上下文相关的表示。如果没有足够的数据进行预训练,ELMo的性能可能会下降。
(3)噪声传播:ELMo生成的词向量对输入数据中的噪声敏感。由于ELMo是基于上下文信息的,输入数据中的错误或不准确的上下文可能会导致错误的语义表示。


Elmo的训练过程🏋️

数据准备:

对数据进行处理返回data【包含所有单词】 v2i:词汇表索引字典 i2v:索引到单词的词汇表

""" data的格式
{
    "train": {
        "is_same": [label1, label2, ..., label_n],  # 训练集标签,表示句子关系是否相同
        "s1": [sentence1, sentence2, ..., sentence_n],  # 训练集句子1
        "s2": [sentence1, sentence2, ..., sentence_n],  # 训练集句子2
        "s1id": [sequence1, sequence2, ..., sequence_n],  # 句子1的索引序列
        "s2id": [sequence1, sequence2, ..., sequence_n]  # 句子2的索引序列
    },
    "test": {
        "is_same": [label1, label2, ..., label_m],  # 测试集标签,表示句子关系是否相同
        "s1": [sentence1, sentence2, ..., sentence_m],  # 测试集句子1
        "s2": [sentence1, sentence2, ..., sentence_m],  # 测试集句子2
        "s1id": [sequence1, sequence2, ..., sequence_m],  # 句子1的索引序列
        "s2id": [sequence1, sequence2, ..., sequence_m]  # 句子2的索引序列
    }
}  """
def _process_mrpc(dir="./MRPC", rows=None):
    data = {"train": None, "test": None}
    files = os.listdir(dir)
    for f in files:
        df = pd.read_csv(os.path.join(dir, f), sep='\t', nrows=rows)
        k = "train" if "train" in f else "test"  #f中包含"train"关键字,测存储到训练集,否则存到测试集
        # issame可以用于区别是否是正样本;df.iloc 是 一个数据访问方法df.iloc[row_index, column_index]
        # 可以访问 DataFrame 中名为 #1 String 的列
        data[k] = {"is_same": df.iloc[:, 0].values, "s1": df["#1 String"].values, "s2": df["#2 String"].values}
    vocab = set()  #存储包含train和test中的所有词汇表数据
    for n in ["train", "test"]:
        for m in ["s1", "s2"]:
            for i in range(len(data[n][m])):
                data[n][m][i] = _text_standardize(data[n][m][i].lower())
                cs = data[n][m][i].split(" ")
                vocab.update(set(cs))
    v2i = {v: i for i, v in enumerate(sorted(vocab), start=1)} #按顺序构建词汇表索引字典v2i
    v2i["<PAD>"] = PAD_ID
    v2i["<MASK>"] = len(v2i)
    v2i["<SEP>"] = len(v2i)
    v2i["<GO>"] = len(v2i)  #添加特殊标记到词汇表
    i2v = {i: v for v, i in v2i.items()} #构建索引到单词的词汇表

    #data 字典中的 s1 和 s2 对应的文本数据被转换成了对应的索引序列,并存储在 s1id 和 s2id 键下
    for n in ["train", "test"]:
        for m in ["s1", "s2"]:
            data[n][m + "id"] = [[v2i[v] for v in c.split(" ")] for c in data[n][m]]
    return data, v2i, i2v

在ELMo的训练过程中,需要准备大规模的无监督语料库作为预训练数据。要将文本转换为统一的格式:

class MRPCSingle(tDataset):
    pad_id = PAD_ID

    def __init__(self, data_dir="./MRPC/", rows=None, proxy=None):
        maybe_download_mrpc(save_dir=data_dir, proxy=proxy)

        data, self.v2i, self.i2v = _process_mrpc(data_dir, rows)
        # 计算训练集中样本的最大长度
        self.max_len = max([len(s) + 2 for s in data["train"]["s1id"] + data["train"]["s2id"]])
        # 对句子1进行处理
        x = [
            [self.v2i["<GO>"]] + data["train"]["s1id"][i] + [self.v2i["<SEP>"]]
            for i in range(len(data["train"]["s1id"]))
        ]
        #对句子2进行处理
        x += [
            [self.v2i["<GO>"]] + data["train"]["s2id"][i] + [self.v2i["<SEP>"]]
            for i in range(len(data["train"]["s2id"]))
        ]
        # 对输入序列x进行填充操作
        self.x = pad_zero(x, max_len=self.max_len)
        # 创建一个不包含填充词汇的数组:word_ids
        self.word_ids = np.array(list(set(self.i2v.keys()).difference([self.v2i["<PAD>"]])))

构建语言模型:

使用预训练数据,构建一个双向LSTM语言模型。ELMo使用的是两个方向的LSTM:一个从左到右(forward LSTM)和一个从右到左(backward LSTM)。每个LSTM层都有多个单元(或记忆细胞),每个单元都负责处理输入序列中的不同位置。

#v_dim=dataset.num_word =12880 , emb_dim=UNITS=256, units=UNITS=256, n_layers=N_LAYERS,=2 lr=LEARNING_RATE=2e-3
    def __init__(self, v_dim, emb_dim, units, n_layers, lr):
        super().__init__()
        self.n_layers = n_layers     # 2
        self.units = units    # 256
        self.v_dim = v_dim    # 12880

        # encoder
        self.word_embed = nn.Embedding(num_embeddings=v_dim, embedding_dim=emb_dim, padding_idx=0)
        self.word_embed.weight.data.normal_(0, 0.1)

        # forward LSTM:由两层lstm组成
        self.fs = nn.ModuleList(
            [nn.LSTM(input_size=emb_dim, hidden_size=units, batch_first=True) if i == 0 else nn.LSTM(input_size=units,
                                                                                                     hidden_size=units,
                                                                                                     batch_first=True)
             for i in range(n_layers)])
        self.f_logits = nn.Linear(in_features=units, out_features=v_dim)

        # backward LSTM:由两层lstm组成
        self.bs = nn.ModuleList(
            [nn.LSTM(input_size=emb_dim, hidden_size=units, batch_first=True) if i == 0 else nn.LSTM(input_size=units,
                                                                                                     hidden_size=units,
                                                                                                     batch_first=True)
             for i in range(n_layers)])
        self.b_logits = nn.Linear(in_features=units, out_features=v_dim)

        self.opt = optim.Adam(self.parameters(), lr=lr)  
    
# 返回前向和后向的输出:3个list【自己,前向,后向】
    def forward(self, seqs): #[16,38]   <GO>句子<SEP>
        device = next(self.parameters()).device
        embedded = self.word_embed(seqs)  # [n, step, emb_dim] = [16,38,256]

        fxs = [embedded[:, :-1, :]]  # 去掉所有结束标记[n, step-1, emb_dim] = [16,37,256]    <GO>句子
        bxs = [embedded[:, 1:, :]]  # 去掉所有开始标记[n, step-1, emb_dim]    句子<SEP>

        #初始化前向和后向的隐藏状态
        (h_f, c_f) = (
        torch.zeros(1, seqs.shape[0], self.units).to(device), torch.zeros(1, seqs.shape[0], self.units).to(device))
        (h_b, c_b) = (
        torch.zeros(1, seqs.shape[0], self.units).to(device), torch.zeros(1, seqs.shape[0], self.units).to(device))

        # fxs、bxs 存储前向和后向的输出
        for fl, bl in zip(self.fs, self.bs):
            output_f, (h_f, c_f) = fl(fxs[-1], (h_f, c_f))  # [n, step-1, units]=[16,37,256], [1, n, units]=[1,16,256]
            fxs.append(output_f)
            # 后项传播时,将输入进行了反转再进行的输入
            output_b, (h_b, c_b) = bl(torch.flip(bxs[-1], dims=[1, ]), (h_b, c_b))  # [n, step-1, units], [1, n, units]
            bxs.append(torch.flip(output_b, dims=(1,)))
        return fxs, bxs

训练语言模型:

使用预训练数据,对语言模型进行训练。这通常是通过最大似然估计(Maximum Likelihood Estimation, MLE)来完成的,即优化模型参数,使得模型生成的下一个词的概率最大化。在训练过程中,通过反向传播算法来更新模型的权重参数。

生成上下文相关的词向量:

在训练完成后,ELMo提取每个单词的上下文相关的词向量表示。ELMo的词向量是由前向LSTM和后向LSTM产生的隐藏状态的线性加权和,其中权重的计算是通过softmax函数得到的。
在这里插入图片描述

    def get_emb(self, seqs):  #[4,38]
        fxs, bxs = self(seqs)   #fxs, bxs是前向和后向传播的结果 3个 [4,37,256]
        xs = [
                 torch.cat((fxs[0][:, 1:, :], bxs[0][:, :-1, :]), dim=2).cpu().data.numpy()
             ] + [
                 torch.cat((f[:, 1:, :], b[:, :-1, :]), dim=2).cpu().data.numpy() for f, b in zip(fxs[1:], bxs[1:])
             ]  # 3个[4,36,512]的前向和后向都进行了拼接处理
        for x in xs:
            print("layers shape=", x.shape)
        return xs

定义损失函数:

损失函数是前向和后向的平均损失

    def step(self, seqs):
        self.opt.zero_grad()
        fo, bo = self(seqs)    #fo, bo是前向和后向的输出
        fo = self.f_logits(fo[-1])  # [n, step-1, v_dim] = [16,32,12880]
        bo = self.b_logits(bo[-1])  # [n, step-1, v_dim]
        # loss 是前向和后向的损失平均值
        loss = (
                       cross_entropy(fo.reshape(-1, self.v_dim), seqs[:, 1:].reshape(-1)) +
                       cross_entropy(bo.reshape(-1, self.v_dim), seqs[:, :-1].reshape(-1))) / 2
        loss.backward()
        self.opt.step()
        return loss.cpu().detach().numpy(), (fo, bo)

结果

batch = batch.type(torch.LongTensor).to(device)
            loss, (fo, bo) = model.step(batch)
            if batch_idx % 20 == 0:
                # 取出第一个张量,沿着 axis=1 的维度取得最大值所在的索引   fo/bo = [16,37,12880]
                fp = fo[0].cpu().data.numpy().argmax(axis=1)   #[37]
                bp = bo[0].cpu().data.numpy().argmax(axis=1)
                print("\n\nEpoch: ", i,
                      "| batch: ", batch_idx,
                      "| loss: %.3f" % loss,
                      "\n| tgt: ",
                      " ".join([dataset.i2v[i] for i in batch[0].cpu().data.numpy() if i != dataset.pad_id]),#data原数据
                      "\n| f_prd: ", " ".join([dataset.i2v[i] for i in fp if i != dataset.pad_id]), #fp前向结果
                      "\n| b_prd: ", " ".join([dataset.i2v[i] for i in bp if i != dataset.pad_id]), #bp后项结果
                      )

在这里插入图片描述

微调(Fine-tuning):

在微调阶段,将预训练的ELMo模型应用于具体的下游任务。例如,可以在特定的文本分类或命名实体识别任务中,使用ELMo作为输入特征,并通过反向传播算法对任务特定的损失函数进行微调。这个过程可以在有标注的监督数据上进行。

参考文献

Deep contextualized word representations论文
NLP——ELMO模型csdn
最后,感谢chatGPT帮我码字~~

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值