推荐 / NOTEBOOK 课程

04 · 召回、样本与检索

真实快照 + 机制练习

真实数据实验只读 · 已保存的实验

已有输出来自保存的教学实验,并非打开页面时重新执行。 代码和原始输出保持不变;数据与历史注释保留原始语言。

召回、样本与检索

Goal

把概念变成可检查的输入、运算、梯度与对照。

数据类型:真实快照 + 机制练习。本地教学参数不是生产默认值;机制实验不是原论文完整复现。

Setup

每本首次从 Setup 起顺序运行,后面的格会复用前面的变量。之后只改一个参数,重跑对应格及下游格。不要把构造数据当作真实业务收益。

PYTHON · 01
from pathlib import Path
import sys
import inspect
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from IPython.display import display, Code

PROJECT = next(p for p in [Path.cwd(), *Path.cwd().parents]
               if (p / "reclab/models.py").exists())
if str(PROJECT) not in sys.path:
    sys.path.insert(0, str(PROJECT))
from reclab.data import Dataset
from reclab.paths import ARTIFACTS, PROCESSED, read_json

plt.rcParams.update({"figure.figsize": (9, 3.8), "font.size": 11,
    "axes.spines.top": False, "axes.spines.right": False,
    "axes.labelcolor": "#182b2b", "text.color": "#182b2b",
    "axes.facecolor": "white", "figure.facecolor": "white",
    "grid.color": "#d4ddda", "axes.axisbelow": True})
dataset = Dataset()
print("Project:", PROJECT)
print("Dataset version:", dataset.manifest["dataset_version"])
print("Python:", sys.executable)
assert Path(sys.executable).parent == PROJECT / ".venv/bin", "请选择 Recommendation Lab 内核"
Notebook 保存的输出 · 本页未执行
Project: /Users/lizuolin/local/recommendation-lab
Dataset version: 2772cab0aca8cb81
Python: /Users/lizuolin/local/recommendation-lab/.venv/bin/python

Steps

讲义对应:02_Retrieval_01.pdf, 02_Retrieval_02.pdf, 02_Retrieval_03.pdf, 02_Retrieval_04.pdf, 02_Retrieval_05.pdf, 02_Retrieval_06.pdf, 02_Retrieval_07.pdf, 02_Retrieval_08.pdf, 02_Retrieval_09.pdf, 02_Retrieval_10.pdf, 02_Retrieval_11.pdf, 02_Retrieval_12.pdf

UserCF 与 Swing

构造的可手算矩阵。Swing 是不加用户权重、每个无序用户对只计一次的教学变体。真实 ItemCF 见第 01 本。

先预测: 让两个用户额外共同喜欢十件物品,Swing 的单对贡献会变大还是变小?

PYTHON · 02
from reclab.learning.retrieval import (binary_matrix, usercf, swing,
    MatrixFactorization, objective_losses, kmeans, partition_retrieval,
    beam_search, SideInfoEncoder, mask_fields)
import torch
from torch.nn import functional as F
torch.set_num_threads(4); torch.manual_seed(42)
matrix = binary_matrix([(0,0),(0,1),(0,2),(1,0),(1,1),(1,2),(2,0),(2,1),(3,2),(3,3)],4,4)
display(pd.DataFrame(matrix))
display(Code(inspect.getsource(swing),language="python"))
display(pd.DataFrame(swing(matrix)))
score, neighbors, similarity = usercf(matrix,0,2)
display(pd.DataFrame({"neighbor":neighbors,"cosine":similarity}))
assert np.allclose(swing(matrix),swing(matrix).T)
Notebook 保存的输出 · 本页未执行
0 1 2 3
0 1.0 1.0 1.0 0.0
1 1.0 1.0 1.0 0.0
2 1.0 1.0 0.0 0.0
3 0.0 0.0 1.0 1.0
def swing(matrix, alpha=1.0):
    """Unweighted Swing teaching variant. Each unordered user-pair is counted once."""
    if alpha <= 0:
        raise ValueError("alpha must be positive")
    histories = [set(np.flatnonzero(row)) for row in matrix]
    result = np.zeros((matrix.shape[1], matrix.shape[1]), dtype=float)
    for u, v in combinations(range(len(histories)), 2):
        common = histories[u] & histories[v]
        contribution = 1 / (alpha + len(common))
        for i, j in combinations(sorted(common), 2):
            result[i, j] += contribution
            result[j, i] += contribution
    return result
0 1 2 3
0 0.000000 0.916667 0.25 0.0
1 0.916667 0.000000 0.25 0.0
2 0.250000 0.250000 0.00 0.0
3 0.000000 0.000000 0.00 0.0
neighbor cosine
0 1 1.000000
1 2 0.816497

真实训练交互上的矩阵分解 / BPR

只使用训练期。固定抽样负例可能包含潜在兴趣,训练 loss 下降不代表验证提升。

先预测: 把向量维度从 12 改成 4,参数量和拟合能力会怎样变化?

PYTHON · 03
pairs = dataset.train[["user_idx","item_idx"]].drop_duplicates().head(1500).to_numpy()
u = torch.tensor(pairs[:,0],dtype=torch.long); p = torch.tensor(pairs[:,1],dtype=torch.long)
n = torch.randint(1,dataset.n_items,(len(pairs),))
n = torch.where(n==p, n%(dataset.n_items-1)+1, n)
mf = MatrixFactorization(dataset.n_users,dataset.n_items,dim=12)
opt = torch.optim.Adam(mf.parameters(),lr=.03); curve=[]
for step in range(25):
    opt.zero_grad(); loss=mf.bpr_loss(u,p,n)
    loss.backward(); opt.step(); curve.append(float(loss.detach()))
assert curve[-1] < curve[0]
display(Code(inspect.getsource(MatrixFactorization),language="python"))
plt.plot(curve,color="#006e60"); plt.xlabel("Step"); plt.ylabel("Fixed-pair BPR loss"); plt.ylim(bottom=0); plt.show()
Notebook 保存的输出 · 本页未执行
class MatrixFactorization(nn.Module):
    def __init__(self, n_users, n_items, dim=12):
        super().__init__()
        self.users = nn.Embedding(n_users, dim)
        self.items = nn.Embedding(n_items, dim)
        nn.init.normal_(self.users.weight, std=0.1)
        nn.init.normal_(self.items.weight, std=0.1)

    def forward(self, users, items):
        return (self.users(users) * self.items(items)).sum(-1)

    def bpr_loss(self, users, positive, negative):
        return F.softplus(-(self(users, positive) - self(users, negative))).mean()
<Figure size 900x380 with 1 Axes>
Notebook 保存的图表

三种损失、采样幂次与 LogQ

loss 尺度不可跨定义比较。讲义的 50/50 困难负例需要排序淘汰日志,现有数据没有该日志。

先预测: 指数改成 0 或 1,热门物品成为负例的概率会怎样变化?

PYTHON · 04
p=torch.tensor([1.,2.,.5],requires_grad=True); n=torch.tensor([0.,1.,1.],requires_grad=True)
display(pd.Series({k:float(v.detach()) for k,v in objective_losses(p,n).items()}).to_frame("loss"))
counts=np.array([1.,10.,100.,1000.]); exponent=.75
sampling=counts**exponent; sampling/=sampling.sum()
display(pd.DataFrame({"count":counts,"uniform":.25,"power_sampling":sampling,"in_batch_prior":counts/counts.sum()}))
logits=torch.ones(1,4)
display(pd.DataFrame({"raw":logits.softmax(-1)[0].numpy(),
    "logq_corrected":(logits-torch.log(torch.tensor(sampling))[None,:]).softmax(-1)[0].numpy()}))
Notebook 保存的输出 · 本页未执行
loss
pointwise 0.705656
pairwise 0.533533
listwise 0.533533
count uniform power_sampling in_batch_prior
0 1.0 0.25 0.004628 0.000900
1 10.0 0.25 0.026025 0.009001
2 100.0 0.25 0.146352 0.090009
3 1000.0 0.25 0.822995 0.900090
raw logq_corrected
0 0.25 0.822995
1 0.25 0.146352
2 0.25 0.026025
3 0.25 0.004628

真实物品向量缓存与分区近似检索

和精确 Top20 的重合率只衡量索引误差,不是用户偏好 Recall。未部署大型 ANN 系统。

先预测: 为什么增加 probes 通常会增加代价?全量和增量更新时缓存版本应如何配对?

PYTHON · 05
vectors=np.load(ARTIFACTS/"runs/baseline-history32/item_vectors.npy")[1:]
query=vectors[0]; exact=np.argsort(-(vectors@query))[:20]
centers, labels=kmeans(vectors,clusters=12,iterations=5)
rows=[]
for probes in [1,3,6,12]:
    found,count=partition_retrieval(vectors,query,centers,labels,probes=probes,k=20)
    rows.append({"probes":probes,"scored":count,"exact_top20_overlap":len(set(found)&set(exact))/20})
display(pd.DataFrame(rows))
assert rows[-1]["exact_top20_overlap"]==1.
Notebook 保存的输出 · 本页未执行
probes scored exact_top20_overlap
0 1 325 0.35
1 3 1122 1.00
2 6 2609 1.00
3 12 5000 1.00

Deep Retrieval 的路径搜索环节

仅复现条件路径概率与 beam search,未实现神经路径学习和交替优化,不能称作完整 Deep Retrieval。

先预测: 局部概率最大,为什么不等于整条路径概率最大?

PYTHON · 06
transition={():{"A":.6,"B":.4},("A",):{"x":.5,"y":.5},("B",):{"x":.99,"y":.01}}
display(Code(inspect.getsource(beam_search),language="python"))
display(pd.DataFrame([{"width":w,"best":beam_search(transition,width=w)[0][0],
    "probability":np.exp(beam_search(transition,width=w)[0][1])} for w in [1,2,4]]))
assert beam_search(transition,width=1)[0][0] != beam_search(transition,width=4)[0][0]
Notebook 保存的输出 · 本页未执行
def beam_search(transition, depth=2, width=1):
    """Conditional path probabilities. No neural path/index learning is claimed."""
    beams = [((), 0.0)]
    for _ in range(depth):
        expanded = []
        for path, log_prob in beams:
            for node, probability in transition[path].items():
                expanded.append((path + (node,), log_prob + np.log(probability)))
        beams = sorted(expanded, key=lambda row: (-row[1], row[0]))[:width]
    return beams
width best probability
0 1 (A, x) 0.300
1 2 (B, x) 0.396
2 4 (B, x) 0.396

字段自监督与 Bloom 过滤

使用真实训练截止属性做 SSL 梯度诊断;没有将辅助任务接入正式双塔。Bloom 使用构造的整数 ID,不等同于真实曝光日志。

先预测: Bloom 存储翻倍会怎样?字段 Mask 和隐藏层 dropout 有什么区别?

PYTHON · 07
cats={v:i+1 for i,v in enumerate(sorted({x["category"] for x in dataset.items}))}
ids=torch.arange(1,65); categories=torch.tensor([cats[x["category"]] for x in dataset.items[:64]])
encoder=SideInfoEncoder(dataset.n_items,len(cats)+1)
a=encoder(*mask_fields(ids,categories,.5)); b=encoder(*mask_fields(ids,categories,.5))
ssl=F.cross_entropy(a@b.T/.2,torch.arange(64)); ssl.backward()
assert torch.isfinite(ssl)
print("SSL mechanism loss:",float(ssl.detach()))
from reclab.learning.systems import BloomFilter
bloom=BloomFilter(bits=512,hashes=4)
for i in range(100):bloom.add(i)
assert all(bloom.contains(i) for i in range(100))
print("False positives on 5000 unseen IDs:",np.mean([bloom.contains(i) for i in range(100,5100)]))
print("Author/geo/social retrieval requires matching fields; see explicitly simulated social exercise in chapter 10.")
Notebook 保存的输出 · 本页未执行
SSL mechanism loss: 3.8621013164520264
False positives on 5000 unseen IDs: 0.093
Author/geo/social retrieval requires matching fields; see explicitly simulated social exercise in chapter 10.

Checks

所有断言和输出都可回溯到代码。它们验证算子、数据快照或机制,不是线上收益。

Next Steps

先记录预测,只改一个变量,再运行对应实验格。需要查看辅助函数时,打开 reclab/learning/。

继续阅读原文

本章来源

作者课程中文

王树森 · 工业界的推荐系统

作者课程对应模块;见下方逐份讲义

45份讲义与逐课实践入口;主题有练习不代表完整复现论文。原件保留原语言与原作者权利。

作者课程中文

Swing

02_Retrieval_02.pdf · 12页

对应教学机制可读;原讲义完整方法仍是进阶目标。

作者课程中文

UserCF

02_Retrieval_03.pdf · 37页

对应教学机制可读;原讲义完整方法仍是进阶目标。

作者课程中文

矩阵补充与 ANN

02_Retrieval_05.pdf · 34页

对应教学机制可读;原讲义完整方法仍是进阶目标。

作者课程中文

双塔与自监督学习

02_Retrieval_09.pdf · 38页

对应教学机制可读;原讲义完整方法仍是进阶目标。

作者课程中文

Deep Retrieval

02_Retrieval_10.pdf · 48页

对应教学机制可读;原讲义完整方法仍是进阶目标。

学习标记可选,仅保存在当前浏览器中。打开页面不会自动标记已学。