前言
Elmo(Embeddings from Language Models)是一种基于深度双向语言模型(Deep Bidirectional Language Model)的上下文相关的词向量表示方法;可以解决一词多义的问题!
传统方法将每个词表示为一个固定的向量,而Elmo考虑了每个词在不同上下文环境中的多个表示。
Elmo模型有两个关键组成部分:
- 前向语言模型(Forward Language Model):这个模型从左到右阅读输入序列,并试图预测下一个词。每个词的前向隐藏状态表示了该词在上下文中的信息。
- 后向语言模型(Backward Language Model):这个模型从右到左阅读输入序列,并试图预测前一个词。每个词的后向隐藏状态表示了该词在更远上下文中的信息。
ELMo的构成和原理
- 双向LSTM架构:
ELMo模型由两个方向的LSTM组成:一个从左到右(forward LSTM),一个从右到左(backward LSTM)。
对于给定一个序列的 N 个tokens,前向语言模型计算序列的概率:
后向语言模型计算序列的概率:
双向的模型BiLM结合了前向和反向LM,联合最大化log 似然函数:
- 字符级别的卷积神经网络(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帮我码字~~