Pytorch之Embedding与Linear的爱恨纠葛

最近遇到的网络模型许多都已Embedding层作为第一层,但回想前几年的网络,多以Linear层作为第一层。两者有什么区别呢?

In [1]:

import torch 
from torch.nn import Embedding
from torch.nn import Linear
import numpy as np

In [20]:

torch.manual_seed(1)

Out[20]:

<torch._C.Generator at 0x7f89641806d0>

最近遇到的网络模型许多都已Embedding层作为第一层,但回想前几年的网络,多以Linear层作为第一层。两者有什么区别呢?

1 Embedding

1.1 Embedding的作用

Embedding层的作用是将有限集合中的元素,转变成指定size的向量。这个有限集合可以使NLP中的词汇表,可以使分类任务中的label,当然无论是什么,最终都要以元素索引传递给Embedding。例如,将包含3个元素的词汇表W={'优', '良', '差'}中的每个元素转换为5维向量。如下所示:

In [15]:

# 先定义一个Embedding层:
emb = Embedding(num_embeddings=3, embedding_dim=5)

In [16]:

# 转换第一个元素
emb(torch.tensor([0],dtype=torch.int64))

Out[16]:

tensor([[ 0.6589,  0.4041,  1.1573, -2.3446, -0.1704]],
       grad_fn=<EmbeddingBackward0>)

In [18]:

# 转换第二个元素
emb(torch.tensor([1],dtype=torch.int64))

Out[18]:

tensor([[ 0.6609, -0.1838, -1.8531,  2.6256, -0.9550]],
       grad_fn=<EmbeddingBackward0>)

In [19]:

# 转换第三个元素
emb(torch.tensor([2],dtype=torch.int64))

Out[19]:

tensor([[-0.3594,  0.0348, -1.0858, -0.6675,  1.9936]],
       grad_fn=<EmbeddingBackward0>)

如果超出词库规模,就会产生异常错误:

In [29]:

# 转换第四个元素
emb(torch.tensor([3],dtype=torch.int64))
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
Cell In [29], line 2
      1 # 转换第四个元素
----> 2 emb(torch.tensor([3],dtype=torch.int64))

File ~/apps/anaconda3/envs/pytorch_1_13_0/lib/python3.10/site-packages/torch/nn/modules/module.py:1190, in Module._call_impl(self, *input, **kwargs)
   1186 # If we don't have any hooks, we want to skip the rest of the logic in
   1187 # this function, and just call forward.
   1188 if not (self._backward_hooks or self._forward_hooks or self._forward_pre_hooks or _global_backward_hooks
   1189         or _global_forward_hooks or _global_forward_pre_hooks):
-> 1190     return forward_call(*input,**kwargs)
   1191 # Do not call functions when jit is used
   1192 full_backward_hooks, non_full_backward_hooks = [], []

File ~/apps/anaconda3/envs/pytorch_1_13_0/lib/python3.10/site-packages/torch/nn/modules/sparse.py:160, in Embedding.forward(self, input)
    159 def forward(self, input: Tensor) -> Tensor:
--> 160     return F.embedding(
    161 input,self.weight,self.padding_idx,self.max_norm,
    162 self.norm_type,self.scale_grad_by_freq,self.sparse)

File ~/apps/anaconda3/envs/pytorch_1_13_0/lib/python3.10/site-packages/torch/nn/functional.py:2210, in embedding(input, weight, padding_idx, max_norm, norm_type, scale_grad_by_freq, sparse)
   2204     # Note [embedding_renorm set_grad_enabled]
   2205     # XXX: equivalent to
   2206     # with torch.no_grad():
   2207     #   torch.embedding_renorm_
   2208     # remove once script supports set_grad_enabled
   2209     _no_grad_embedding_renorm_(weight, input, max_norm, norm_type)
-> 2210 return torch.embedding(weight,input,padding_idx,scale_grad_by_freq,sparse)

IndexError: index out of range in self

初始时,所有向量表示都是随机的,但却并非一成不变的,例如在NLP任务中,随着网络的训练,表示'优'与'良'的两个向量相似度会逐渐减小,而表示'优'与'差'的两个向量相似度会逐渐增大。

1.2 Embedding的用法

接下来我们详细说说pytorch中Embedding层的使用方法。Embedding类主要参数如下:

  • num_embeddings (int) - 嵌入字典的大小,即共有多少个元素需要转换

  • embedding_dim (int) - 每个嵌入向量的大小,即转换后获得向量的size

  • padding_idx (int, optional) - 如果提供的话,输出遇到此下标时用零填充

  • max_norm (float, optional) - 如果提供的话,会重新归一化词嵌入,使它们的范数小于提供的值

  • norm_type (float, optional) - 对于max_norm选项计算p范数时的p

  • scale_grad_by_freq (boolean, optional) - 如果提供的话,会根据字典中单词频率缩放梯度

  • weight weight (Tensor) -形状为(num_embeddings, embedding_dim)的模块中可学习的权值

Embedding是怎么实现的呢?其实,在初始化Embedding层时,Embedding会根据默认随机初始化num_embeddings * embedding_dim的正态分布的权重。以上面例子为例,我们看看它的参数:

In [23]:

emb.weight

Out[23]:

Parameter containing:
tensor([[ 0.6589,  0.4041,  1.1573, -2.3446, -0.1704],
        [ 0.6609, -0.1838, -1.8531,  2.6256, -0.9550],
        [-0.3594,  0.0348, -1.0858, -0.6675,  1.9936]], requires_grad=True)

仔细观察这些权重值,每一行都与上方{'优', '良', '差'}对应。当我们在emb中输入张量torch.tensor([0])时,输出了第一行,当我们在emb中输入张量torch.tensor([1])时,输出了第二行。所以,我们可以猜测,Embedding的工作原理就是初始化一个指定shape的矩阵,在进行转换是,根据输入的tensor值,索引矩阵的行。确实如此,Embedding源码就是这么做的。

当然,Embedding的权重参数也不一定非得随机初始化,也可以手动指定。如下所示,我们先手动初始化一个3 * 5的矩阵,然后将其作为Embedding的权重参数:

In [51]:

# 随机初始化一个3 * 5 的矩阵
emb_weight = torch.rand(3, 5, requires_grad=True)

这里需要注意,手动初始化参数时,最好设置requires_grad=True,后续训练时才能更新权重。

In [52]:

emb_weight

Out[52]:

tensor([[0.4766, 0.1663, 0.8045, 0.6552, 0.1768],
        [0.8248, 0.8036, 0.9434, 0.2197, 0.4177],
        [0.4903, 0.5730, 0.1205, 0.1452, 0.7720]], requires_grad=True)

In [26]:

# 通过这个预先定义的矩阵,初始化Embedding层
emb2 = Embedding.from_pretrained(emb_weight)

In [27]:

# 转换第一个元素
emb2(torch.tensor([0],dtype=torch.int64))

Out[27]:

tensor([[0.7576, 0.2793, 0.4031, 0.7347, 0.0293]])

In [28]:

# 查看所有权重参数
emb2.weight

Out[28]:

Parameter containing:
tensor([[0.7576, 0.2793, 0.4031, 0.7347, 0.0293],
        [0.7999, 0.3971, 0.7544, 0.5695, 0.4388],
        [0.6387, 0.5247, 0.6826, 0.3051, 0.4635]])

这种手动指定参数参数话Embedding层的方式在迁移学习中非常实用,例如在NLP任务中,我们可以使用开源的词向量模型进行初始化,使得我们的模型更快收敛。

2 Linear层

2.1 Linear层的作用

Linear层是最古老、最基础的一种网络结构了,作用是对输入向量进行线性变换,输出指定size的向量。例如,将size为3的向量,转为size为5的向量:

In [30]:

# 初始化一个Linear层
lin = Linear(in_features=3, out_features=5)

In [32]:

# 随机初始化一个size为3的向量
x = torch.rand(3)
x

Out[32]:

tensor([0.7140, 0.2676, 0.9906])

In [39]:

x.shape

Out[39]:

torch.Size([3])

In [37]:

# 经Linear层进行转换
y = lin(x)
y

Out[37]:

tensor([ 0.1443,  0.7431, -0.1405, -0.3098, -0.1214], grad_fn=<AddBackward0>)

In [38]:

y.shape

Out[38]:

torch.Size([5])

2.2 Linear的用法

Linear类就3个参数:

  • in_features:指的是输入张量的size
  • out_features:指的是输出张量的size
  • bias:是否使用偏置,默认为True,表示使用

参数也简单,不多说。我们来介绍Linear的工作原理。Linear的本质就是矩阵相乘,公式如下: $$Y=XW^T+B$$ 式中,$X$是我们输入的向量,$W$是Linear层的权重参数,$B$是偏置向量。我们分别输出看看:

In [47]:

w = lin.weight
w

Out[47]:

Parameter containing:
tensor([[-0.0520,  0.0837, -0.0023],
        [ 0.5047,  0.1797, -0.2150],
        [-0.3487, -0.0968, -0.2490],
        [-0.1850,  0.0276,  0.3442],
        [ 0.3138, -0.5644,  0.3579]], requires_grad=True)

In [48]:

b = lin.bias
b

Out[48]:

Parameter containing:
tensor([ 0.1613,  0.5476,  0.3811, -0.5260, -0.5489], requires_grad=True)

我们尝试进行手动运算:

In [49]:

x.matmul(w.T) + b

Out[49]:

tensor([ 0.1443,  0.7431, -0.1405, -0.3098, -0.1214], grad_fn=<AddBackward0>)

看,结果与上方直接使用Linear层进行转换也是一样的。

Linear层的参数也可以进行手动修改:

In [55]:

lin_weight = torch.rand(3, 5, requires_grad=True)
lin_weight

Out[55]:

tensor([[0.0555, 0.8639, 0.4259, 0.7812, 0.6607],
        [0.1251, 0.6004, 0.6201, 0.1652, 0.2628],
        [0.6705, 0.5896, 0.2873, 0.3486, 0.9579]], requires_grad=True)

In [56]:

from torch.nn import Parameter

不过必须转为Parameter才能成功:

In [57]:

lin.weight = Parameter(lin_weight)

3 Embedding与Linear的区别

  1. Embedding只针对数据集规模有限的离散型数据,Linear即可用于离散型数据,也可用于连续型数据,且对数据集规模无限制。对于Embedding能实现的功能,Liner都能实现,只不过需要先进性一次手动one-hot编码。

  2. Embedding本质是通过元素的索引,获取矩阵对应行作为输出,而Linear本质是矩阵相乘。