一、传统推荐算法和embedding
(一)传统推荐算法:博闻强识
1、任务:
需要推荐系统记住一些常用、高频的模式,比如<中国人,春节,饺子>这种模式。
2、方法:
评分卡。
3、利用LR模型实现评分卡
- 评分卡的组织形式:模式和对应的得分(贡献)。其中每个模式的得分代表了对最终目标的贡献,正样本中出现越多的模式贡献相对越大,负样本中出现越多的模式贡献相对越少。
![](https://i-blog.csdnimg.cn/direct/24757f47be2144759d2ffd70cc29a647.png)
- 评分卡总得分:评分卡的得分是一个样本中出现的所有的模式评分之和。
- LR模型的优缺点:①优点:强于记忆,能记住足够多的模式。②缺点:不能挖掘新的特征组合,只能评估每个模式的重要性;只能人工输入模式;对于一些罕见模式,利用正则剔除,系统个性化程度降低。【强于记忆,而扩展弱】
(二)推荐算法:扩展性
1、为什么要提高扩展性
提高扩展性的原因在于,如果要提高推荐系统的个性化推荐性能,就需要将罕见的、低频的、长尾的模式挖掘并记住。
2、扩展性的实现
方法就是将细粒度的概念拆分成粗粒度的特征向量。具体拆解如下所示。
![](https://i-blog.csdnimg.cn/direct/8493440bb2824df28c90193bffb80342.png)
(三)Embedding介绍
1、Embedding的功能
将概念自动拆解为特征向量。
2、Embedding的流程简述
Embedding将特征和函数作为待优化的对象。首先随机初始化特征向量和函数,紧接着特征向量和函数随着主目标被随机梯度下降优化,最后当主目标完成优化以后就能够得到有意义的特征向量和函数。
3、Embedding如何实现扩展性
将精准概念匹配转化成模糊查找。比如最开始采用精确概念匹配,查询科学标签只能找到相应标签下的文章;而采用模糊查找以后,经过embedding,向量空间中的科学向量和科技向量并不是正交的关系,两者存在很小的夹角,因此在查询过程中系统也会检索科技标签下的文章,提高了扩展性。
![](https://i-blog.csdnimg.cn/direct/7e07a077cdda453b9466393588ae939d.png)
(四)Embedding实现细节
1、简单的Embedding过程描述
(1)Embedding矩阵
例如,文章标签filed里面有6个feature:音乐、影视、财经、游戏、军事、历史。定义一个6*4维的矩阵,其中矩阵的行数是要进行embedding的feature总数,矩阵的列数是希望得到的embedding向量长度。首先随机初始化这个embedding矩阵,接着矩阵随着主目标被优化,训练结束得到一个能表达文章标签语义的有意义的embedding矩阵。
![](https://i-blog.csdnimg.cn/direct/4193c1856d9547a89b18967914f41856.png)
(2)Embedding向量
已知Embedding得到的输入是代表每个feature的唯一整数索引,每个整数索引对应了Embeddin矩阵中的行号。因此整个Embedding过程可以理解为,用Embedding矩阵和该特征的one-hot编码进行乘法操作,也就是提取Embedding矩阵该feature整数索引对应的行向量。
2、TensorFlow实现文章分类Embedding
①不同层的任务:
- 映射层:将特征(如 "music", "movie", "finance" 等)映射成唯一的整数索引。这确保了每个特征都有一个独特的整数 ID。
- Embedding层:将整数索引映射为稠密的浮点向量表示。Embedding 层的行数等于特征总数加一(用于处理未知情况的索引),列数为自定义的 Embedding 向量长度。每一行向量表示一个特征的语义信息。
- 输入层:接收输入数据,即待处理的特征。
②步骤:
- 将词汇表中所有词汇映射成整数ID。
- 定义Embedding层。
- 输入层接收输入的feature。
- 将输入的feature通过映射层映射成唯一的整数ID。
- 对于每一个整数 ID,从 Embedding 层的权重矩阵中提取对应的行向量。
import tensorflow as tf
# ----------- 准备
unq_categories = ["music", "movie", "finance", "game", "military", "history"]
# 这一层负责将string转化为int型id
id_mapping_layer = tf.keras.layers.StringLookup(vocabulary=unq_categories)
emb_layer = tf.keras.layers.Embedding(
# 多加一维是为了处理,当输入不包含在unq_categories的情况
input_dim=len(unq_categories) + 1,
output_dim=4) # output_dim指明映射向量的长度
# ----------- Embedding
cate_input = ... # [batch_size,1]的string型"文章分类"向量
cate_ids = id_mapping_layer(cate_input) # string型输入的“文章分类”映射成int型id
# 得到形状=[batch_size,4]的float稠密向量,表示每个“文章分类”的语义
cate_embeddings = emb_layer(cate_ids)
3、文章分类Embedding的前代、回代
①稀疏前代和回代的必要性
Embedding过程数学上相当于Embedding矩阵乘以one-hot向量。直接使用 one-hot 编码向量进行矩阵乘法虽然简单,但在实际计算中非常低效,因为大部分计算都是乘以 0。稀疏前代和回代方法可以大幅提高效率。
稀疏前代方法在计算时并不会真正生成 one-hot 向量,而是直接通过索引操作从 Embedding 矩阵中提取相应的行向量。
在反向传播时,稀疏回代只更新与实际数据对应的 Embedding 矩阵中的行向量,而不涉及整个矩阵的更新。
②初始化
词汇表是文本中所有唯一单词的集合,并且通常会将这些单词映射到整数索引。
def __init__(self, W, vocab_name, field_name):
self.vocab_name = vocab_name
self.field_name = field_name # 这个Embedding Layer对应的Field
self._W = W # 底层的Embedding矩阵,形状是[vocab_size,embed_size]
self._last_input = None
③前代:
针对每个feature filed,比如颜色里面的红色、蓝色、绿色等等。
- 输入:输入是一系列三元组,(example_id,feat_id,feat_val)。其中,example_id是输入训练的样本id,feat_id是特征id,feat_val对应的是该feature在该样本中的特征值;比如说,对于一个蓝色的0号样本来说,在颜色这个特征域里面,红色的feat_id是2,蓝色的feat_id是3,红色的feat_val就是0,蓝色的feat_val就是1。对应样本输入需要保存每次的输入,再下次回代的时候使用。
- 输出:输出的形状是[batch_size, embed_size](行数是批量处理样本的数量)。每一行向量表示的是该样本在当前特征字段(Field)上的嵌入向量加权和。这意味着输出矩阵中的每一行对应一个样本,在该特征字段下,通过将所有相关特征的嵌入向量按照特征值加权求和得到的最终嵌入表示。
def forward(self, X):
"""
:param X: 属于某个Field的一系列稀疏类别Feature的集合
:return: [batch_size, embed_size]
"""
self._last_input = X # 保存本次前代时的输入,回代时要用到
# output:该Field的embedding,形状是[batch_size, embed_size]
output = np.zeros((X.n_total_examples, self._W.shape[1]))
# 稀疏输入是一系列三元组的集合,每个三元组由以下三个元素组成
# example_idx:可以认为是sample的id
# feat_id:每个类别特征(不是Field)的id
# feat_val:每个类别特征对应的特征值(一般情况下都是1)
for example_idx, feat_id, feat_val in X.iterate_non_zeros():
# 根据feature id从Embedding矩阵中取出embedding
embedding = self._W[feat_id, :]
# 某个Field的embedding是,属于这个Field各个feature embedding的加权和,权重就是各feature value
output[example_idx, :] += embedding * feat_val
return output # 这个Field的embedding
③回代
- 输入:只有前代输入的feature才有必要计算梯度,因此利用前代时保存的last_input。
- 计算:prev_grads是上一层传递下来的梯度,表示对应样本 example_idx 的梯度,通常是该样本的损失函数关于其输出嵌入向量的偏导数。grad_from_one_example表示了当前样本中特征的梯度。如果梯度字典dw中已存在该feat_id,那么用当前样本的梯度加权更新该feature的梯度,最后得到的feature梯度是来自于各个样本的梯度加权和;若字典dw中没有该feat_id,则直接创建对应的键值对并添加当前样本的梯度。
grad_from_one_example = prev_grads[example_idx, :] * feat_val
- 输出:输出对于embedding矩阵W的部分行向量梯度(也就是对部分feature的嵌入向量梯度),结果用字典dw表示,键值是feat_id,value是梯度值。
def backward(self, prev_grads):
"""
:param prev_grads: loss对这个Field output的梯度,[batch_size, embed_size]
:return: dw,对Embedding Matrix部分行的梯度
"""
# 只有本次前代中出现的feature id,才有必要计算梯度
# 其结果肯定是非常稀疏的,用dict来保存
dW = {}
# _last_input是前代时的输入,只有其中出现的feature_id才有必要计算梯度
for example_idx, feat_id, feat_val in self._last_input.iterate_non_zeros():
# 由对field output的梯度,根据链式法则,计算出对feature embedding的梯度
# 形状是[1,embed_size]
grad_from_one_example = prev_grads[example_idx, :] * feat_val
if feat_id in dW:
# 一个batch中的多个样本,可能引用了相同的feature
# 因此对某个feature embedding的梯度,应该是来自多个样本的累加
dW[feat_id] += grad_from_one_example
else:
dW[feat_id] = grad_from_one_example
return dW
4、多个Field的Embedding的前代回代:拼接
允许多个Field共享同一套Emebedding Matrix(用vocab_name标识)。
(1)拼接的具体例子
①假设我们有一个批次(batch)包含两个样本,每个样本有两个Field(比如,颜色和形状)。我们用以下特征来表示:Color field里面有两个颜色,红色feat_id是0,蓝色feat_id是1;形状field里面有两个形状,圆形feat_id是2,方形feat_id是3。每个特征的嵌入向量维度(embed_size)为 3。
②初始嵌入矩阵W如下所示:
W = [
[0.1, 0.2, 0.3], # red
[0.4, 0.5, 0.6], # blue
[0.7, 0.8, 0.9], # circle
[1.0, 1.1, 1.2] # square
]
③批次输入
- 样本1:颜色:red(feat_id = 0, feat_val = 1);形状:circle(feat_id = 2, feat_val = 1)
- 样本2:颜色:blue(feat_id = 1, feat_val = 1);形状:square(feat_id = 3, feat_val = 1)
④前向传播:对于每个Field,我们计算每个样本的嵌入向量。
- 颜色Field的嵌入:样本1:red = [0.1, 0.2, 0.3];样本2:blue = [0.4, 0.5, 0.6]
- 形状Field的嵌入:样本1:circle = [0.7, 0.8, 0.9];样本2:square = [1.0, 1.1, 1.2]
⑤嵌入拼接:将不同Field的嵌入向量拼接在一起,得到每个样本的最终嵌入表示。
- 样本1的最终嵌入向量:颜色Field:[0.1, 0.2, 0.3];形状Field:[0.7, 0.8, 0.9];拼接结果:[0.1, 0.2, 0.3, 0.7, 0.8, 0.9]。
- 样本2的最终嵌入向量:颜色Field:[0.4, 0.5, 0.6];形状Field:[1.0, 1.1, 1.2];拼接结果:[0.4, 0.5, 0.6, 1.0, 1.1, 1.2]。
⑥最终输出:最终的输出是一个形状为 [batch_size, total_embed_size]
的矩阵,其中 total_embed_size
是所有Field的嵌入维度之和。在这个例子中,batch_size
是2,每个样本的嵌入向量长度是6(两个Field,每个Field的嵌入维度是3)。
output = [
[0.1, 0.2, 0.3, 0.7, 0.8, 0.9],
[0.4, 0.5, 0.6, 1.0, 1.1, 1.2]
]
(2)代码解释
①前代
- 输入:输入是每个batch里面一个field的稀疏特征输入,是一批三元组。每个Field的稀疏特征输入都是一个由三元组(
example_idx, feat_id, feat_val
)组成的集合,这些三元组描述了特定样本在特定特征上的取值。 - 方法:使用嵌入层的
forward
方法将稀疏特征输入转换为嵌入向量。 - 输出:将得到的嵌入向量添加到
embedded_outputs
列表中,最终形成包含所有Field嵌入的列表。
def forward(self, sparse_inputs):
""" 所有Field,先经过Embedding,再拼接
:param sparse_inputs: dict {field_name: SparseInput}
:return: 每个SparseInput贡献一个embedding vector,返回结果是这些embedding vector的拼接
"""
embedded_outputs = []
for embed_layer in self._embed_layers:
# 获得属于这个Field的稀疏特征输入,sp_input是一组<example_idx, feat_id, feat_val>
sp_input = sparse_inputs[embed_layer.field_name]
# 得到属于当前Field的embedding
embedded_outputs.append(embed_layer.forward(sp_input))
# 最终结果是所有Field Embedding的拼接
# [batch_size, sum of all embed-layer's embed_size]
return np.hstack(embedded_outputs)
②回代
- 拆分梯度矩阵:通过遍历所有嵌入层 (self._embed_layers),我们得到一个每层输出维度的列表。utils.split_column(prev_grads, col_sizes)这个函数将 prev_grads 矩阵按列分割成多个子矩阵。每个子矩阵对应一个Field的梯度。这样我们就可以处理每个Field的梯度。
- 处理每个Field的梯度:调用每个嵌入层的
backward
方法来计算梯度,layer_prev_grads
是对应Field的梯度矩阵。layer_grads_to_embed
是一个字典,键是特征的feat_id
,值是对应的梯度g
。它表示了当前Field对特定特征的梯度。
def backward(self, prev_grads):
"""
:param prev_grads: [batch_size, sum of all embed-layer's embed_size]
上一层传入的, Loss对本层输出(i.e., 所有field embedding拼接)的梯度
"""
# prev_grads是loss对“所有field embedding拼接”的导数
# prev_grads_splits把prev_grads拆解成数组,
# 数组内每个元素对应loss对某个field embedding的导数
col_sizes = [layer.output_dim for layer in self._embed_layers]
prev_grads_splits = utils.split_column(prev_grads, col_sizes)
# _grads_to_embed也只存储"本次前代中出现的各field的各feature"的embedding
# 其结果是超级稀疏的,因此_grads_to_embed是一个dict
self._grads_to_embed.clear() # reset
for layer, layer_prev_grads in zip(self._embed_layers, prev_grads_splits):
# layer_prev_grads: 上一层传入的,Loss对某个field embedding的梯度
# layer_grads_to_feat_embed: dict, feat_id==>grads,
# 某个field的embedding layer造成对某vocab的embedding矩阵的某feat_id对应行的梯度
layer_grads_to_embed = layer.backward(layer_prev_grads)
for feat_id, g in layer_grads_to_embed.items():
# 表示"对某个vocab的embedding weight中的第feat_id行的总导数"
key = "{}@{}".format(layer.vocab_name, feat_id)
if key in self._grads_to_embed:
# 由于允许多个field共享embedding matrix,
# 因此对某个embedding矩阵的某一行的梯度应该是多个field贡献梯度的叠加
self._grads_to_embed[key] += g
else:
self._grads_to_embed[key] = g