推薦系統實踐 0x12 Embedding

NoMornings發表於2020-12-21

做過深度學習的小夥伴,大家應該多多少少都聽說過Embedding,這麼火的Embedding到底是什麼呢?這篇文章就用來介紹Embedding。另外,基於深度學習的推薦系統方法或者論文還沒有結束,我打算穿插進行講解,畢竟,深度學習出來的推薦框架的演算法實在是太相像了,很難有大的不同。所以,這一篇就聊聊Embedding。

初識Embedding

Embedding又被成為向量化,或者向量的對映。Embedding(嵌入)也是拓撲學裡面的詞,在深度學習領域經常和Manifold(流形)搭配使用。在之前的Embedding的操作,往往都是將稀疏的向量轉換成稠密的向量,以便於交給後面的神經網路處理。其實,Embedding還有更多應用場景,以及更多的實現方法,後面會慢慢談到這些方法。Embedding可以看做是用低維稠密向量表示一個物件,可以是單詞,可以是使用者,可以是電影,可以是音樂,可以是商品等等。只要這個向量能夠包含,或者表達所表示物件的特徵,同時能夠通過向量之間的距離反應物件之間的關係或者相似性,那麼Embedding的作用就體現出來了。

Example

如下圖所示,就是Embedding在自然語言處理當中,對單詞Embedding的一種刻畫:

上圖中從king到queen,與從man到woman的距離幾乎相同,說明Embedding之間的向量運算能夠包含詞之間的語義關係資訊,同理,圖中的詞性例子當中,從進行時態到過去時態也是相同的距離。Embedding可以在大量預料的輸入前提下,發掘出一些通用的常識,比如首都-國家之間的關係。

通過這個例子,我們可以瞭解到,在詞向量空間當中,即使是完全不知道一個詞向量的含義下,僅依靠語義關係加詞向量運算就可以推斷出這個詞的詞向量。Embedding就這樣從一個空間表達物件,同時還可以表示物件之間的關係。

應用

除了在自然語言處理中對單詞進行Embedding之外,我們可以對物品進行Embedding。比如在影視作品推薦當中,“神探夏洛克”與“華生”在Embedding向量空間當中會比較近,“神探夏洛克”與“海綿寶寶”在向量空間中會比較遠。另外,如果在電商推薦領域,“桌子”和“桌布”在向量空間中會比較緊,“桌子”和“滑雪板”在向量空間中會比較遠。

不同領域在使用這些語義資料進行訓練的時候也會有不同,比如電影推薦會將使用者觀看的電影進行Embedding,而電商會對使用者購買歷史進行Embedding。

深度學習推薦系統中的Embedding

一般來說,推薦系統當中會大量使用Embedding操作:

  1. 使用one-hot對類別,id等型別進行編碼會導致特徵向量極其稀疏,使用Embedding可以將高維稀疏的特徵向量轉化為低維稠密的特徵向量。
  2. Embedding具有很強的表達能力,在Graph Embedding提出之後,Embedding可以引入任何資訊進行編碼,包含大量有價值的資訊。
  3. Embedding可以表達物件之間的關係,可以對物品、使用者的相似度進行計算,在推薦系統的召回層經常使用,尤其是在區域性敏感雜湊(Locality-Sensitive Hashing)等快速最近鄰搜尋提出之後,Embedding被用來對物品進行快速篩選,到幾百或幾千的量級之後再進行神經網路的精排。

常見方法

  • Word2Vec
  • Item2Vec
  • Node2Vec

等等方法,以及文中提到的區域性敏感雜湊,也會在後續的系列文章逐步更新介紹。

程式碼

我們看一下在PyTorch框架當中,深度學習裡面的Embedding是怎麼實現的。首先是官方給出的介紹

A simple lookup table that stores embeddings of a fixed dictionary and size. This module is often used to store word embeddings and retrieve them using indices. The input to the module is a list of indices, and the output is the corresponding word embeddings.

翻譯一下就是

一個簡單的查詢表,儲存固定字典和大小的嵌入。該模組通常用於儲存詞嵌入,並使用索引檢索它們。該模組的輸入是一個索引列表,輸出是相應的詞嵌入。

from typing import Optional

import torch
from torch import Tensor
from torch.nn.parameter import Parameter

from .module import Module
from .. import functional as F
from .. import init


class Embedding(Module):
    r"""A simple lookup table that stores embeddings of a fixed dictionary and size.

    This module is often used to store word embeddings and retrieve them using indices.
    The input to the module is a list of indices, and the output is the corresponding
    word embeddings.

    Args:
        num_embeddings (int): size of the dictionary of embeddings
        embedding_dim (int): the size of each embedding vector
        padding_idx (int, optional): If given, pads the output with the embedding vector at :attr:`padding_idx`
                                         (initialized to zeros) whenever it encounters the index.
        max_norm (float, optional): If given, each embedding vector with norm larger than :attr:`max_norm`
                                    is renormalized to have norm :attr:`max_norm`.
        norm_type (float, optional): The p of the p-norm to compute for the :attr:`max_norm` option. Default ``2``.
        scale_grad_by_freq (boolean, optional): If given, this will scale gradients by the inverse of frequency of
                                                the words in the mini-batch. Default ``False``.
        sparse (bool, optional): If ``True``, gradient w.r.t. :attr:`weight` matrix will be a sparse tensor.
                                 See Notes for more details regarding sparse gradients.

    Attributes:
        weight (Tensor): the learnable weights of the module of shape (num_embeddings, embedding_dim)
                         initialized from :math:`\mathcal{N}(0, 1)`

    Shape:
        - Input: :math:`(*)`, LongTensor of arbitrary shape containing the indices to extract
        - Output: :math:`(*, H)`, where `*` is the input shape and :math:`H=\text{embedding\_dim}`

    .. note::
        Keep in mind that only a limited number of optimizers support
        sparse gradients: currently it's :class:`optim.SGD` (`CUDA` and `CPU`),
        :class:`optim.SparseAdam` (`CUDA` and `CPU`) and :class:`optim.Adagrad` (`CPU`)

    .. note::
        With :attr:`padding_idx` set, the embedding vector at
        :attr:`padding_idx` is initialized to all zeros. However, note that this
        vector can be modified afterwards, e.g., using a customized
        initialization method, and thus changing the vector used to pad the
        output. The gradient for this vector from :class:`~torch.nn.Embedding`
        is always zero.

    Examples::

        >>> # an Embedding module containing 10 tensors of size 3
        >>> embedding = nn.Embedding(10, 3)
        >>> # a batch of 2 samples of 4 indices each
        >>> input = torch.LongTensor([[1,2,4,5],[4,3,2,9]])
        >>> embedding(input)
        tensor([[[-0.0251, -1.6902,  0.7172],
                 [-0.6431,  0.0748,  0.6969],
                 [ 1.4970,  1.3448, -0.9685],
                 [-0.3677, -2.7265, -0.1685]],

                [[ 1.4970,  1.3448, -0.9685],
                 [ 0.4362, -0.4004,  0.9400],
                 [-0.6431,  0.0748,  0.6969],
                 [ 0.9124, -2.3616,  1.1151]]])


        >>> # example with padding_idx
        >>> embedding = nn.Embedding(10, 3, padding_idx=0)
        >>> input = torch.LongTensor([[0,2,0,5]])
        >>> embedding(input)
        tensor([[[ 0.0000,  0.0000,  0.0000],
                 [ 0.1535, -2.0309,  0.9315],
                 [ 0.0000,  0.0000,  0.0000],
                 [-0.1655,  0.9897,  0.0635]]])
    """
    __constants__ = ['num_embeddings', 'embedding_dim', 'padding_idx', 'max_norm',
                     'norm_type', 'scale_grad_by_freq', 'sparse']

    num_embeddings: int
    embedding_dim: int
    padding_idx: int
    max_norm: float
    norm_type: float
    scale_grad_by_freq: bool
    weight: Tensor
    sparse: bool

    def __init__(self, num_embeddings: int, embedding_dim: int, padding_idx: Optional[int] = None,
                 max_norm: Optional[float] = None, norm_type: float = 2., scale_grad_by_freq: bool = False,
                 sparse: bool = False, _weight: Optional[Tensor] = None) -> None:
        super(Embedding, self).__init__()
        self.num_embeddings = num_embeddings
        self.embedding_dim = embedding_dim
        if padding_idx is not None:
            if padding_idx > 0:
                assert padding_idx < self.num_embeddings, 'Padding_idx must be within num_embeddings'
            elif padding_idx < 0:
                assert padding_idx >= -self.num_embeddings, 'Padding_idx must be within num_embeddings'
                padding_idx = self.num_embeddings + padding_idx
        self.padding_idx = padding_idx
        self.max_norm = max_norm
        self.norm_type = norm_type
        self.scale_grad_by_freq = scale_grad_by_freq
        if _weight is None:
            self.weight = Parameter(torch.Tensor(num_embeddings, embedding_dim))
            self.reset_parameters()
        else:
            assert list(_weight.shape) == [num_embeddings, embedding_dim], \
                'Shape of weight does not match num_embeddings and embedding_dim'
            self.weight = Parameter(_weight)
        self.sparse = sparse

    def reset_parameters(self) -> None:
        init.normal_(self.weight)
        if self.padding_idx is not None:
            with torch.no_grad():
                self.weight[self.padding_idx].fill_(0)

    def forward(self, input: Tensor) -> Tensor:
        return F.embedding(
            input, self.weight, self.padding_idx, self.max_norm,
            self.norm_type, self.scale_grad_by_freq, self.sparse)

    def extra_repr(self) -> str:
        s = '{num_embeddings}, {embedding_dim}'
        if self.padding_idx is not None:
            s += ', padding_idx={padding_idx}'
        if self.max_norm is not None:
            s += ', max_norm={max_norm}'
        if self.norm_type != 2:
            s += ', norm_type={norm_type}'
        if self.scale_grad_by_freq is not False:
            s += ', scale_grad_by_freq={scale_grad_by_freq}'
        if self.sparse is not False:
            s += ', sparse=True'
        return s.format(**self.__dict__)

    @classmethod
    def from_pretrained(cls, embeddings, freeze=True, padding_idx=None,
                        max_norm=None, norm_type=2., scale_grad_by_freq=False,
                        sparse=False):
        r"""Creates Embedding instance from given 2-dimensional FloatTensor.

        Args:
            embeddings (Tensor): FloatTensor containing weights for the Embedding.
                First dimension is being passed to Embedding as ``num_embeddings``, second as ``embedding_dim``.
            freeze (boolean, optional): If ``True``, the tensor does not get updated in the learning process.
                Equivalent to ``embedding.weight.requires_grad = False``. Default: ``True``
            padding_idx (int, optional): See module initialization documentation.
            max_norm (float, optional): See module initialization documentation.
            norm_type (float, optional): See module initialization documentation. Default ``2``.
            scale_grad_by_freq (boolean, optional): See module initialization documentation. Default ``False``.
            sparse (bool, optional): See module initialization documentation.

        Examples::

            >>> # FloatTensor containing pretrained weights
            >>> weight = torch.FloatTensor([[1, 2.3, 3], [4, 5.1, 6.3]])
            >>> embedding = nn.Embedding.from_pretrained(weight)
            >>> # Get embeddings for index 1
            >>> input = torch.LongTensor([1])
            >>> embedding(input)
            tensor([[ 4.0000,  5.1000,  6.3000]])
        """
        assert embeddings.dim() == 2, \
            'Embeddings parameter is expected to be 2-dimensional'
        rows, cols = embeddings.shape
        embedding = cls(
            num_embeddings=rows,
            embedding_dim=cols,
            _weight=embeddings,
            padding_idx=padding_idx,
            max_norm=max_norm,
            norm_type=norm_type,
            scale_grad_by_freq=scale_grad_by_freq,
            sparse=sparse)
        embedding.weight.requires_grad = not freeze
        return embedding

參考

Embedding in PyTorch
深度學習推薦系統 王喆編著
怎麼形象理解embedding這個概念? - 劉斯坦的回答 - 知乎

相關文章