import torch
from torch.nn import Embedding
from torch.nn import Linear
import numpy as np
torch.manual_seed(1)
<torch._C.Generator at 0x7f89641806d0>
最近遇到的网络模型许多都已Embedding层作为第一层,但回想前几年的网络,多以Linear层作为第一层。两者有什么区别呢?
Embedding层的作用是将有限集合中的元素,转变成指定size的向量。这个有限集合可以使NLP中的词汇表,可以使分类任务中的label,当然无论是什么,最终都要以元素索引传递给Embedding。例如,将包含3个元素的词汇表W={'优', '良', '差'}中的每个元素转换为5维向量。如下所示:
# 先定义一个Embedding层:
emb = Embedding(num_embeddings=3, embedding_dim=5)
# 转换第一个元素
emb(torch.tensor([0],dtype=torch.int64))
tensor([[ 0.6589, 0.4041, 1.1573, -2.3446, -0.1704]],
grad_fn=<EmbeddingBackward0>)
# 转换第二个元素
emb(torch.tensor([1],dtype=torch.int64))
tensor([[ 0.6609, -0.1838, -1.8531, 2.6256, -0.9550]],
grad_fn=<EmbeddingBackward0>)
# 转换第三个元素
emb(torch.tensor([2],dtype=torch.int64))
tensor([[-0.3594, 0.0348, -1.0858, -0.6675, 1.9936]],
grad_fn=<EmbeddingBackward0>)
如果超出词库规模,就会产生异常错误:
# 转换第四个元素
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的正态分布的权重。以上面例子为例,我们看看它的参数:
emb.weight
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的权重参数:
# 随机初始化一个3 * 5 的矩阵
emb_weight = torch.rand(3, 5, requires_grad=True)
这里需要注意,手动初始化参数时,最好设置requires_grad=True,后续训练时才能更新权重。
emb_weight
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)
# 通过这个预先定义的矩阵,初始化Embedding层
emb2 = Embedding.from_pretrained(emb_weight)
# 转换第一个元素
emb2(torch.tensor([0],dtype=torch.int64))
tensor([[0.7576, 0.2793, 0.4031, 0.7347, 0.0293]])
# 查看所有权重参数
emb2.weight
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任务中,我们可以使用开源的词向量模型进行初始化,使得我们的模型更快收敛。
# 初始化一个Linear层
lin = Linear(in_features=3, out_features=5)
# 随机初始化一个size为3的向量
x = torch.rand(3)
x
tensor([0.7140, 0.2676, 0.9906])
x.shape
torch.Size([3])
# 经Linear层进行转换
y = lin(x)
y
tensor([ 0.1443, 0.7431, -0.1405, -0.3098, -0.1214], grad_fn=<AddBackward0>)
y.shape
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$是偏置向量。我们分别输出看看:
w = lin.weight
w
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)
b = lin.bias
b
Parameter containing:
tensor([ 0.1613, 0.5476, 0.3811, -0.5260, -0.5489], requires_grad=True)
我们尝试进行手动运算:
x.matmul(w.T) + b
tensor([ 0.1443, 0.7431, -0.1405, -0.3098, -0.1214], grad_fn=<AddBackward0>)
看,结果与上方直接使用Linear层进行转换也是一样的。
Linear层的参数也可以进行手动修改:
lin_weight = torch.rand(3, 5, requires_grad=True)
lin_weight
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)
from torch.nn import Parameter
不过必须转为Parameter才能成功:
lin.weight = Parameter(lin_weight)
3 Embedding与Linear的区别¶
-
Embedding只针对数据集规模有限的离散型数据,Linear即可用于离散型数据,也可用于连续型数据,且对数据集规模无限制。对于Embedding能实现的功能,Liner都能实现,只不过需要先进性一次手动one-hot编码。
-
Embedding本质是通过元素的索引,获取矩阵对应行作为输出,而Linear本质是矩阵相乘。