深度学习:自然语言处理(上)

自然语言是一套用来表达含义的复杂系统。在这套系统中,词是表义的基本单元。顾名思义,词向量是用来表示词的向量,也可被认为是词的特征向量或表征。把词映射为实数域向量的技术也叫词嵌入(word embedding)。

1、词嵌入

词嵌入:
它将每个词表示成一个定长的向量,并使得这些向量能较好地表达不同词之间的相似和类比关系。
word2vec工具包含了两个模型:

  • 跳字模型(skip-gram)
  • 连续词袋模型(continuous bag of words,CBOW)

1.1 跳字模型

跳字模型假设基于某个词来生成它在文本序列周围的词。

举个例子,假设文本序列是“the”“man”“loves”“his”“son”。以“loves”作为中心词,设背景窗口大小为2。如图10.1所示,跳字模型所关心的是,给定中心词“loves”,生成与它距离不超过2个词的背景词“the”“man”“his”“son”的条件概率,即

P(“the",“man",“his",“son"∣“loves").

条件概率的计算:

损失函数:
训练中我们通过最大化似然函数来学习模型参数,即最大似然估计。这等价于最小化以下损失函数:

1.2 连续词袋模型

连续词袋模型与跳字模型类似。与跳字模型最大的不同在于,连续词袋模型假设基于某中心词在文本序列前后的背景词来生成该中心词。在同样的文本序列“the”“man”“loves”“his”“son”里,以“loves”作为中心词,且背景窗口大小为2时,连续词袋模型关心的是,给定背景词“the”“man”“his”“son”生成中心词“loves”的条件概率P(“loves"∣“the",“man",“his",“son").


2、Word2Vec的实现

word2vec的实现:

点击查看代码
with open('../../data/ptb/ptb.train.txt', 'r') as f:
    lines = f.readlines()
    # st是sentence的缩写
    raw_dataset = [st.split() for st in lines]
# tk是token的缩写
counter = collections.Counter([tk for st in raw_dataset for tk in st])
counter = dict(filter(lambda x: x[1] >= 5, counter.items()))

idx_to_token = [tk for tk, _ in counter.items()]
token_to_idx = {tk: idx for idx, tk in enumerate(idx_to_token)}
dataset = [[token_to_idx[tk] for tk in st if tk in token_to_idx]
           for st in raw_dataset]
num_tokens = sum([len(st) for st in dataset])

def discard(idx):
    return random.uniform(0, 1) < 1 - math.sqrt(
        1e-4 / counter[idx_to_token[idx]] * num_tokens)

subsampled_dataset = [[tk for tk in st if not discard(tk)] for st in dataset]

def compare_counts(token):
    return '# %s: before=%d, after=%d' % (token, sum(
        [st.count(token_to_idx[token]) for st in dataset]), sum(
        [st.count(token_to_idx[token]) for st in subsampled_dataset]))

def get_centers_and_contexts(dataset, max_window_size):
    centers, contexts = [], []
    for st in dataset:
        if len(st) < 2:  # 每个句子至少要有2个词才可能组成一对“中心词-背景词”
            continue
        centers += st
        for center_i in range(len(st)):
            window_size = random.randint(1, max_window_size)
            indices = list(range(max(0, center_i - window_size),
                                 min(len(st), center_i + 1 + window_size)))
            indices.remove(center_i)  # 将中心词排除在背景词之外
            contexts.append([st[idx] for idx in indices])
    return centers, contexts

all_centers, all_contexts = get_centers_and_contexts(subsampled_dataset, 5)

def get_negatives(all_contexts, sampling_weights, K):
    all_negatives, neg_candidates, i = [], [], 0
    population = list(range(len(sampling_weights)))
    for contexts in all_contexts:
        negatives = []
        while len(negatives) < len(contexts) * K:
            if i == len(neg_candidates):
                # 根据每个词的权重(sampling_weights)随机生成k个词的索引作为噪声词。
                # 为了高效计算,可以将k设得稍大一点
                i, neg_candidates = 0, random.choices(
                    population, sampling_weights, k=int(1e5))
            neg, i = neg_candidates[i], i + 1
            # 噪声词不能是背景词
            if neg not in set(contexts):
                negatives.append(neg)
        all_negatives.append(negatives)
    return all_negatives

sampling_weights = [counter[w]**0.75 for w in idx_to_token]
all_negatives = get_negatives(all_contexts, sampling_weights, 5)

class MyDataset(torch.utils.data.Dataset):
    def __init__(self, centers, contexts, negatives):
        assert len(centers) == len(contexts) == len(negatives)
        self.centers = centers
        self.contexts = contexts
        self.negatives = negatives

    def __getitem__(self, index):
        return (self.centers[index], self.contexts[index], self.negatives[index])

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

def batchify(data):
    """用作DataLoader的参数collate_fn: 输入是个长为batchsize的list, 
    list中的每个元素都是Dataset类调用__getitem__得到的结果
    """
    max_len = max(len(c) + len(n) for _, c, n in data)
    centers, contexts_negatives, masks, labels = [], [], [], []
    for center, context, negative in data:
        cur_len = len(context) + len(negative)
        centers += [center]
        contexts_negatives += [context + negative + [0] * (max_len - cur_len)]
        masks += [[1] * cur_len + [0] * (max_len - cur_len)]
        labels += [[1] * len(context) + [0] * (max_len - len(context))]
    return (torch.tensor(centers).view(-1, 1), torch.tensor(contexts_negatives),
            torch.tensor(masks), torch.tensor(labels))

batch_size = 512
num_workers = 0 if sys.platform.startswith('win32') else 4

dataset = MyDataset(all_centers, 
                    all_contexts, 
                    all_negatives)
data_iter = Data.DataLoader(dataset, batch_size, shuffle=True,
                            collate_fn=batchify, 
                            num_workers=num_workers)
for batch in data_iter:
    for name, data in zip(['centers', 'contexts_negatives', 'masks',
                           'labels'], batch):
        print(name, 'shape:', data.shape)
    break

3、子词嵌入

fastText提出了子词嵌入方法。
它在word2vec中的跳字模型的基础上,将中心词向量表示成单词的子词向量之和。
子词嵌入利用构词上的规律,通常可以提升生僻词表示的质量。

在fastText中,每个中心词被表示成子词的集合。
用单词“where”作为例子来了解子词是如何产生的:

  • 首先,我们在单词的首尾分别添加特殊字符“<”和“>”以区分作为前后缀的子词。
  • 然后,将单词当成一个由字符构成的序列来提取n元语法。
  • 例如,当n=3时,我们得到所有长度为3的子词:“”“whe”“her”“ere”“”以及特殊子词“”。

4、全局向量的词嵌入(Glove)

GloVe模型基于平方损失对跳元模型做了三个修改:

    1. 使用变量\(p'_{ij}=x_{ij}\)\(q'_{ij}=\exp(\mathbf{u}_j^\top \mathbf{v}_i)\)
      而非概率分布,并取两者的对数。所以平方损失项是\(\left(\log\,p'_{ij} - \log\,q'_{ij}\right)^2 = \left(\mathbf{u}_j^\top \mathbf{v}_i - \log\,x_{ij}\right)^2\)
    1. 为每个词\(w_i\)添加两个标量模型参数:中心词偏置\(b_i\)和上下文词偏置\(c_i\)
    1. 用权重函数\(h(x_{ij})\)替换每个损失项的权重,其中\(h(x)\)\([0, 1]\)的间隔内递增。

整合代码,训练GloVe是为了尽量降低以下损失函数:

\[\sum_{i\in\mathcal{V}} \sum_{j\in\mathcal{V}} h(x_{ij}) \left(\mathbf{u}_j^\top \mathbf{v}_i + b_i + c_j - \log\,x_{ij}\right)^2. \]

对于权重函数,建议的选择是:当\(x < c\)(例如,\(c = 100\))时,\(h(x) = (x/c) ^\alpha\)(例如\(\alpha = 0.75\));否则\(h(x) = 1\)。在这种情况下,由于\(h(0)=0\),为了提高计算效率,可以省略任意\(x_{ij}=0\)的平方损失项。例如,当使用小批量随机梯度下降进行训练时,在每次迭代中,我们随机抽样一小批量非零\(x_{ij}\)来计算梯度并更新模型参数。注意,这些非零的\(x_{ij}\)是预先计算的全局语料库统计数据;因此,该模型GloVe被称为全局向量

应该强调的是,当词\(w_i\)出现在词\(w_j\)的上下文窗口时,词\(w_j\)也出现在词\(w_i\)的上下文窗口。因此,\(x_{ij}=x_{ji}\)。与拟合非对称条件概率\(p_{ij}\)的word2vec不同,GloVe拟合对称概率\(\log \, x_{ij}\)。因此,在GloVe模型中,任意词的中心词向量和上下文词向量在数学上是等价的。但在实际应用中,由于初始值不同,同一个词经过训练后,在这两个向量中可能得到不同的值:GloVe将它们相加作为输出向量。

5、求近义词和类比词



6、文本情感分类:使用RNN

数据集:

class BiRNN(nn.Module):
    def __init__(self, vocab_size, embed_size, num_hiddens,
                 num_layers, **kwargs):
        super(BiRNN, self).__init__(**kwargs)
        self.embedding = nn.Embedding(vocab_size, embed_size)
        # 将bidirectional设置为True以获取双向循环神经网络
        self.encoder = nn.LSTM(embed_size, num_hiddens, num_layers=num_layers,
                                bidirectional=True)
        self.decoder = nn.Linear(4 * num_hiddens, 2)

    def forward(self, inputs):
        # inputs的形状是(批量大小,时间步数)
        # 因为长短期记忆网络要求其输入的第一个维度是时间维,
        # 所以在获得词元表示之前,输入会被转置。
        # 输出形状为(时间步数,批量大小,词向量维度)
        embeddings = self.embedding(inputs.T)
        self.encoder.flatten_parameters()
        # 返回上一个隐藏层在不同时间步的隐状态,
        # outputs的形状是(时间步数,批量大小,2*隐藏单元数)
        outputs, _ = self.encoder(embeddings)
        # 连结初始和最终时间步的隐状态,作为全连接层的输入,
        # 其形状为(批量大小,4*隐藏单元数)
        encoding = torch.cat((outputs[0], outputs[-1]), dim=1)
        outs = self.decoder(encoding)
        return outs
glove_embedding = d2l.TokenEmbedding('glove.6b.100d')
embeds = glove_embedding[vocab.idx_to_token]
net.embedding.weight.data.copy_(embeds)
net.embedding.weight.requires_grad = False

lr, num_epochs = 0.01, 5
trainer = torch.optim.Adam(net.parameters(), lr=lr)
loss = nn.CrossEntropyLoss(reduction="none")
d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs,
    devices)

6、参考文献

《动手学深度学习》

posted @ 2022-10-01 22:14  朝南烟  阅读(112)  评论(0编辑  收藏  举报
body { color: #000; background-color: #e6e6e6; font-family: "Helvetica Neue",Helvetica,Verdana,Arial,sans-serif; font-size: 12px; min-height: 101%; background: url(https://images.cnblogs.com/cnblogs_com/caolanying/1841633/o_2009041…ly1geq8oc9owbj21hc0u0th5.jpg) fixed; } #home { margin: 0 auto; opacity: 0.8; width: 65%; min-width: 1080px; background-color: #fff; padding: 30px; margin-top: 50px; margin-bottom: 50px; box-shadow: 0 2px 6px rgba(100, 100, 100, 0.3); }