CH 10 / SOURCE EXAMPLE

deepwalk.py

ch10 召回模型/code/ch02_graph_embedding/deepwalk.py

这个示例讲什么

│ ├── deepwalk.py DeepWalk:随机游走 + Skip-Gram

DeepWalk 图嵌入召回(第10章 §10.2.1) 流程: 1. 在用户-物品二部图上做随机游走生成序列(Random Walk) 2. 用 Skip-Gram(Word2Vec 思想)训练 Embedding 3. 线上用 Item Embedding 做最近邻召回 此处用纯 Python 实现,不依赖 gensim/networkx,便于原理演示。

上面来自章节 README 或源码自身的说明,不是自动验证的运行结果,也不能代替 PDF 的书中论断。讲解时请结合实际代码、输入输出与原书页码。

源码结构与阅读入口

  1. build_graph · 第 19 行构建无向二部图(用户节点 ↔ 物品节点)。
  2. random_walk · 第 36 行从 start 节点出发进行随机游走,返回节点序列。
  3. generate_walks · 第 47 行对图中所有节点各做 num_walks 次随机游走。
  4. SkipGram · 第 64 行
  5. cosine_sim · 第 111 行
  6. recall_deepwalk · 第 118 行用用户 Embedding 检索最相似物品。

完整源码 · 162 行

1"""2DeepWalk 图嵌入召回(第10章 §10.2.1)34流程:51. 在用户-物品二部图上做随机游走生成序列(Random Walk)62. 用 Skip-Gram(Word2Vec 思想)训练 Embedding73. 线上用 Item Embedding 做最近邻召回89此处用纯 Python 实现,不依赖 gensim/networkx,便于原理演示。10"""1112import random13import math14from collections import defaultdict151617# ── 1. 构建图 ─────────────────────────────────────────────────────────────────1819def build_graph(user_items: dict[str, list[str]]) -> dict[str, list[str]]:20    """21    构建无向二部图(用户节点 ↔ 物品节点)。22    返回邻接表 {node: [neighbor, ...]}。23    """24    graph: dict[str, list[str]] = defaultdict(list)25    for user, items in user_items.items():26        uid = f"u:{user}"27        for item in items:28            iid = f"i:{item}"29            graph[uid].append(iid)30            graph[iid].append(uid)31    return dict(graph)323334# ── 2. 随机游走 ───────────────────────────────────────────────────────────────3536def random_walk(graph: dict[str, list[str]], start: str, walk_len: int) -> list[str]:37    """从 start 节点出发进行随机游走,返回节点序列。"""38    walk = [start]39    for _ in range(walk_len - 1):40        neighbors = graph.get(walk[-1], [])41        if not neighbors:42            break43        walk.append(random.choice(neighbors))44    return walk454647def generate_walks(48    graph: dict[str, list[str]],49    num_walks: int = 10,50    walk_len: int = 20,51) -> list[list[str]]:52    """对图中所有节点各做 num_walks 次随机游走。"""53    nodes = list(graph.keys())54    walks = []55    for _ in range(num_walks):56        random.shuffle(nodes)57        for node in nodes:58            walks.append(random_walk(graph, node, walk_len))59    return walks606162# ── 3. Skip-Gram 训练(负采样简化版)────────────────────────────────────────6364class SkipGram:65    def __init__(self, vocab: list[str], dim: int = 32, lr: float = 0.025):66        self.vocab = vocab67        self.word2id = {w: i for i, w in enumerate(vocab)}68        self.dim = dim69        self.lr = lr70        n = len(vocab)71        scale = 1.0 / math.sqrt(dim)72        self.emb_in  = [[random.uniform(-scale, scale) for _ in range(dim)] for _ in range(n)]73        self.emb_out = [[0.0] * dim for _ in range(n)]7475    def _sigmoid(self, x: float) -> float:76        return 1.0 / (1.0 + math.exp(-max(-30, min(30, x))))7778    def _update(self, center_id: int, ctx_id: int, label: float) -> None:79        u = self.emb_in[center_id]80        v = self.emb_out[ctx_id]81        score = sum(a * b for a, b in zip(u, v))82        grad = (self._sigmoid(score) - label)83        for k in range(self.dim):84            self.emb_out[ctx_id][k] -= self.lr * grad * u[k]85            self.emb_in[center_id][k] -= self.lr * grad * v[k]8687    def train(self, walks: list[list[str]], window: int = 3, neg_k: int = 5) -> None:88        """Skip-Gram + 负采样训练。"""89        n = len(self.vocab)90        for walk in walks:91            ids = [self.word2id[w] for w in walk if w in self.word2id]92            for i, center in enumerate(ids):93                ctx_range = ids[max(0, i - window): i] + ids[i + 1: i + window + 1]94                for ctx in ctx_range:95                    # 正样本96                    self._update(center, ctx, 1.0)97                    # 负样本98                    for _ in range(neg_k):99                        neg = random.randint(0, n - 1)100                        while neg == ctx:101                            neg = random.randint(0, n - 1)102                        self._update(center, neg, 0.0)103104    def embedding(self, node: str) -> list[float] | None:105        idx = self.word2id.get(node)106        return self.emb_in[idx] if idx is not None else None107108109# ── 4. 召回 ──────────────────────────────────────────────────────────────────110111def cosine_sim(a: list[float], b: list[float]) -> float:112    dot = sum(x * y for x, y in zip(a, b))113    na = math.sqrt(sum(x * x for x in a))114    nb = math.sqrt(sum(x * x for x in b))115    return dot / (na * nb + 1e-9)116117118def recall_deepwalk(119    user_id: str,120    model: SkipGram,121    item_ids: list[str],122    seen: set[str],123    top_k: int = 10,124) -> list[tuple[str, float]]:125    """用用户 Embedding 检索最相似物品。"""126    u_emb = model.embedding(f"u:{user_id}")127    if u_emb is None:128        return []129    scores = []130    for item in item_ids:131        if item in seen:132            continue133        i_emb = model.embedding(f"i:{item}")134        if i_emb is not None:135            scores.append((item, cosine_sim(u_emb, i_emb)))136    scores.sort(key=lambda x: x[1], reverse=True)137    return scores[:top_k]138139140# ── 快速验证 ──────────────────────────────────────────────────────────────────141if __name__ == "__main__":142    random.seed(42)143    user_items = {144        "u1": ["i1", "i2", "i3", "i4"],145        "u2": ["i1", "i2", "i5"],146        "u3": ["i3", "i4", "i6"],147        "u4": ["i2", "i4", "i7"],148        "u5": ["i5", "i6", "i7"],149    }150    graph = build_graph(user_items)151    walks = generate_walks(graph, num_walks=20, walk_len=10)152153    vocab = list(graph.keys())154    model = SkipGram(vocab, dim=16, lr=0.05)155    model.train(walks, window=2, neg_k=3)156157    all_items = [f"i{j}" for j in range(1, 8)]158    seen = {f"i{j}" for j in [1, 2, 3, 4]}159    result = recall_deepwalk("u1", model, all_items, seen, top_k=3)160    print("DeepWalk 召回结果(u1):")161    for item, s in result:162        print(f"  {item}: {s:.4f}")

如何用于学习或编写新代码

  1. 先看章节 README 的依赖和运行成本,定位这个例子的输入、输出及关键函数。
  2. 在原书 PDF 中找到对应概念的具体页;代码演示与书中论断分开标注。
  3. 只在依赖和耗时可控时运行单个示例,修改一个参数并记录真实输出与失败情形。
  4. 参考实现新代码时说明复用、改动、测试与尚未完成的生产依赖;教学示例并非生产系统。