Dynamic-dimension Embedding Variable

背景

在典型的推荐场景中,同类特征的出现频次往往极度不均匀,以商品的id item_id 这个特征为例,前3%的item_id, 占据了80%的曝光,前20%的item_id,占据了98%的曝光。目前,所有特征的Embeding都会被设定成统一维度,如果Embedding维度过高,低频特征容易过拟合,而且会额外耗费大量内存;如果维度设置过低,那么高频部征特征可能会由于表达不够而欠拟合,最终的效果因此而大打折扣。 ​

Dynamic Dimension EmbeddingVariable功能依据特征的频度来给Embedding动态分配维度;高频高维度,低频低维度; 高频特征可以被给到很高的维度而不担心欠拟合问题, 而低频特征因为给定低维度embedding,一定程度上起到了正则的作用,既缓解了过拟合的问题,而且可以极大程度节省内存(低频长尾特征的数量占据绝对优势) ​

img_1.png

Dynamic-dimension Embedding Variable示例图

用户接口

下面是使用dynamic-dimension embedding variable接口的用户接口,目前提供了两种接口: 第一个是底层API get_dynamic_dimension_embedding_variable:

def get_dynamic_dimension_embedding_variable(
    name,
    embedding_block_dimension, # 表示单元EV的dimension
    embedding_block_num, # 新增,表示单元EV的数目 
    key_dtype=dtypes.int64,
    value_dtype=None,
    initializer=None,
    regularizer=None,
    trainable=True,
    collections=None,
    caching_device=None,
    partitioner=None,
    validate_shape=True,
    custom_getter=None,
    constraint=None,
    steps_to_live=None,
    steps_to_live_l2reg=None,
    l2reg_theta=None
)

另一个是利用sparse_column_with_dynamic_dimension_embedding:

def sparse_column_with_dynamice_dimension_embedding(
    column_name, dtype=dtypes.string, partition_num=None, steps_to_live=None,
    embedding_block_dimension, # 表示单元EV的dimension
    embedding_block_num, # 新增,表示单元EV的数目
)

dynamic dimension embedding variable的大部分参数都和get_embedding_variable相同,下面解释dynamic dimension embedding variable中主要的参数:

  • embedding_block_dimension: 每一个block的维度

  • embedding_block_num: 一个特征的embedding最多可以有多少个block

对于一个dynamic-dimension embedding variable,他的Embedding的最大size就是embedding_block_dimension * embedding_block_num。 ​

当使用dynamic dimension embedding variable的时候,在embedding_lookup的时候需要传入blocknum上参数,用来指示每一个特征对应的blocknum

'''
blocknums: 新加参数,是一个和ids一一对应的tensor,代表每个id需要去几个fae的ev blocks中去找
example:
创建了一个dynamic embedding variable, 
其中embedding_block_dimension=6, 
embedding_block_num=4

ids=[21,34,78,99,56],blocknums=[4, 1, 4, 3, 1]

那么embedding_lookup的结果就是:(1是embedding的学习结果,0为补齐)
[[1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
 [1. 1. 1. 1. 1. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
 [1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 0. 0. 0. 0. 0. 0.]
 [1. 1. 1. 1. 1. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]]
'''
def embedding_lookup(
    params,
    ids,
    partition_strategy="mod",
    name=None,
    blocknums=None    
)

使用示例

动态维度调整策略

对于每一个特征有两个统计量,分别是累积特征频次freq_acc以及特征在当前时段的出现速度freq_current_speed,freq_acc和freq_current_speed都会被初始化为0,freq_acc平时不更新,会在特定的step根据一定的规则进行更新,而freq_current_speed则会随着特征被访问而更新,特征每被访问一次freq_current_speed就会+1,然后会在特定的step被置为0。 目前使用的更新freq_acc的规则如下: $freq_acc = \beta*freq_acc + (1.0-\beta)*freq_current_speed$, 其中 $0\le\beta\le1$,$\beta$可以设置为0.9。 ​

在获得每个特征的blocknum时,首先根据freq_acc和freq_current_speed计算每个特征对应的freq,公式如下:

$freq=\frac{\beta*freq_acc + (1.0-\beta)*freq_current_speed}{(1.0-\beta^t)}$ , t是freq_acc被reset的次数

在得到每一个特征的freq后,需要计算不同的blocknum数量对应的阈值:具体步骤如下:

  1. 对所有freq_acc超过一个阈值的特征进行ranking(该阈值可以设置为0.000001),超过阈值的特征的数量记为total_rank。

  2. 考虑到推荐场景中的特征频次分布符合幂律分布,我们给出一个指数分布来划分阈值空间,即给出一个p,得到一个$[p^0,p^1,....,p^{n+1}]$,其中$1<p$,n是blocknum的数量。

  3. 对于每一个$k\in[1,....,n+1]$,计算$rank =(p^{n+1-k}-p^0)/(p^{n+1}-p^0) * total_rank$,然后根据freq_acc选出top-rank个freq_acc, 这之中的最小值即是第i个block对应的阈值。

  4. 对于每一个特征,将freq与阈值比较即可决定每个对应的embedding的长度。