深入理解深度学习之 Embedding

1. 从 one-hot 编码引入 Embedding

在正式介绍 Embedding 之前,我们必须先理解它提出的背景,也就是说研究者提出 Embedding 是为了解决什么问题。

首先我们有一个 one-hot 编码的概念。我们知道神经网络是不能处理字符数据的,所以在文本预处理中对一句话进行数值编码,比如对于一句话“我 从 哪 里 来 要 到 何 处 去”,我们可能会很直观的对每个字进行 从 0 到 N(N 表示所有不同字符的个数) 这样的索引编码,于是就有:

[我 从 哪 里 来 要 到 何 处 去] → [0 1 2 3 4 5 6 7 8 9]

有人会提出质疑,原本没有数值大小的字符经过这种形式的编码之后,好像存在某种大小关系和顺序关系,即:2 > 1,但实际字符 “哪” 与 “从” 却并没有数值含义。所以就有人提出了没有数值含义的 one-hot 编码,也被称为独热编码,对上面的那句话进行独热编码后如下:
[ 1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 ] \begin{bmatrix} 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 \\ \end{bmatrix} 1000000000010000000000100000000001000000000010000000000100000000001000000000010000000000100000000001
这样处理之后好像解决了上面的问题,并且 one-hot 编码具有计算方便,表达能力强的优点。但是缺点也随之而来,一篇硕士论文不同的字可能会达到 1 万,而每篇文章的总字数也会有 5 万,难道我们在编码一篇文章的时候要建立一个 5W x 1W 的矩阵,我们立刻能想到这种编码方式所带来存储量的问题。所以归纳之后,独热编码 one-hot encoding 存在的问题包括如下:

  1. 大量为 0 的稀疏矩阵;
  2. 硬编码,特征没有语义信息;
  3. 特征维度较高,且每个特征都彼此独立,训练所需要的数据量高,容易导致维度灾难;

所以就引入了我们要介绍的 Embedding,我们再来考虑一个场景来说明 Embedding 是如何解决上述问题的,假如我们现在需要对 girl、woman、boy 和 man 四个单词进行编码,如果采用 one-hot 编码形式,就可以得到如下的矩阵:
[ 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 ] \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} 1000010000100001
但是我们从语义上理解这个四个单词的含义就会发现,具有年龄和性别上的差异,假如我们设定性别为男时,记作1,性别为女时,记作0;年龄大于20时,记作1,小于20岁时记作0,于是既可以得到下表:

genderage
girl00
woman01
boy10
man11

我们很自然的挖掘到 2 个语义特征,所以将上面的四个单词编码如下:
[ 0 0 0 1 1 0 1 1 ] \begin{bmatrix} 0 & 0\\ 0 & 1\\ 1 & 0\\ 1 & 1\\ \end{bmatrix} 00110101
从上面的例子中可以看到,特征维度由 4 变成了 2,而且好像具备了语义信息,这就是 Embedding 做的事,从数据中自动学习到输入空间的信息表示的映射 f f f,由于上述计算中没有涉及到 l a b e l label label ,所以 Embedding 的训练过程是无监督的。

注意:这里的例子是对输入张量使用 Embedding 做降维,但是实际上还可以使用 Embedding 来做升维处理,具体的例子可以参考这篇博客

2. PyTorch 实现 Embedding

如果我们仅仅从词向量的角度理解,Embedding 所做的工作就是把一个维数为所有词数量的高维空间嵌入到一个维数低得多的连续向量空间中,每个词或词组被映射为实数域上的向量。

在 PyTorch 中,针对词向量有一个专门的层 nn.Embedding ,用来实现词与词向量的映射。nn.Embedding 相当于一个词表,形状为 (num_embedings, embedding_dim) ,其中 num_embedding 表示词表的长度,embedding_dim 表示词向量的维度,如果输入张量中不同词的个数超过词表的长度,就会报数组越界异常。

  • num_embedding(int):size of the dictionary of embeddings
  • embedding_dim(int):the size of each embedding vector

如果 Embedding 层的输入形状为 NxM(N为batch_size,M是序列的长度),则输出的形状是 N x M x embedding_dim.

注意:输入必须是 LongTensor 类型,可以通过 tensor.long() 方法转成 LongTensor。

import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
 
word_to_idx = {'girl': 0, 'boy': 1, 'woman': 2, 'man': 3}  # 每个单词用一个数字表示
embeds = nn.Embedding(8, 2) # 单词的个数8,2位embedding的维度

inputs = torch.LongTensor([[word_to_idx[key] for key in word_to_idx.keys()]])
inputs = torch.autograd.Variable(inputs)

# 获取Variable对应的embedding,并打印出来
outputs = embeds(inputs)
print(outputs)
print(embeds.weight)
tensor([[[-0.9663, -0.6391],
         [-2.2001,  0.7629],
         [ 0.1813, -1.1935],
         [-0.3214, -0.9004]]], grad_fn=<EmbeddingBackward>)
Parameter containing:
tensor([[-0.9663, -0.6391],
        [-2.2001,  0.7629],
        [ 0.1813, -1.1935],
        [-0.3214, -0.9004],
        [-1.7540,  0.0908],
        [ 1.1053, -0.8680],
        [ 0.2549,  0.2402],
        [ 0.9486, -0.7895]], requires_grad=True)

注:Embedding的权重也是可以训练的,既可以采用随机初始化,也可以采用预训练好的词向量初始化。

3. 使用 Embedding 训练的 RNN

需求:实现一个2层的RNN,加一个Embedding 嵌入层,将字符序列 “hello” 转化为 “ohlol”

class Model(torch.nn.Module):
    def __init__(self, input_size, embedding_size, hidden_size, num_layers, num_class):
        super(Model, self).__init__()
        # 嵌入层
        self.embed = torch.nn.Embedding(input_size, embedding_size)
        # RNN层
        self.rnn = torch.nn.RNN(input_size=embedding_size,
                                hidden_size=hidden_size,
                                num_layers=num_layers,
                                batch_first=True)
        # 全连接层
        self.fc = torch.nn.Linear(hidden_size, num_class)
        
    def forward(self, x):
        # 初始化隐藏层h0
        hidden = torch.zeros(num_layers, x.size(0), hidden_size)
        x = self.embed(x)  # (batch, seqLen, embeddingSize)
        x, _ = self.rnn(x, hidden)
        x = self.fc(x)
        return x.view(-1, num_class)
# 构造数据集
idx2char = ['e', 'h', 'l', 'o']
x_data = [[1, 0, 2, 2, 3]]   # (batch, seq_len)
y_data = [3, 1, 2, 3, 2]    # (batch * seq_len)
inputs = torch.LongTensor(x_data)
labels = torch.LongTensor(y_data)

# 初始化参数
num_class = 4
input_size = 4     # 词典长度
hidden_size = 8
embedding_size = 8
num_layers = 2
batch_size = 1
seq_len = 5
net = Model(input_size, embedding_size, hidden_size, num_layers, num_class)
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(net.parameters(), lr=0.05)

"""
这里就不用再对字符进行one-hot编码了,直接做索引编码即可
"""
for epoch in range(15):
    optimizer.zero_grad()
    outputs = net(inputs)
    loss = criterion(outputs, labels)
    loss.backward()
    optimizer.step()
    
    _, idx = outputs.max(dim=1)
    idx = idx.data.numpy()
    print('Predicted: ', ''.join([idx2char[x] for x in idx]), end='')
    print(', Epoch [%d/15] loss = %.3f' % (epoch + 1, loss.item()))

模型的训练过程如下:

Predicted:  hllll, Epoch [1/15] loss = 1.268
Predicted:  ollll, Epoch [2/15] loss = 1.066
Predicted:  ollll, Epoch [3/15] loss = 0.878
Predicted:  ohlll, Epoch [4/15] loss = 0.679
Predicted:  ohlol, Epoch [5/15] loss = 0.500
Predicted:  ohlol, Epoch [6/15] loss = 0.369
Predicted:  ohlol, Epoch [7/15] loss = 0.237
Predicted:  ohlol, Epoch [8/15] loss = 0.154
Predicted:  ohlol, Epoch [9/15] loss = 0.103
Predicted:  ohlol, Epoch [10/15] loss = 0.069
Predicted:  ohlol, Epoch [11/15] loss = 0.048
Predicted:  ohlol, Epoch [12/15] loss = 0.035
Predicted:  ohlol, Epoch [13/15] loss = 0.025
Predicted:  ohlol, Epoch [14/15] loss = 0.019
Predicted:  ohlol, Epoch [15/15] loss = 0.014

最后我认为 Embedding 一开始可能只有感性的理解,但是随着应用和实践不断的积累,就会对它有新的认识。

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐