工作台课程

CS221 · 人工智能:原理与技术

Lecture 5 · 搜索算法 I

Lecture 5 · 搜索算法 I

CS221: Artificial Intelligence — Principles and Techniques · Stanford · Autumn 2025
📅 Oct 6 · 💻 search.py


承上启下

前四讲(Lecture 1 · 张量、梯度与监督学习Lecture 4 · 深度学习)解决的是机器学习:给定训练数据 $\{(x_i, y_i)\}$,通过优化损失函数得到一个预测器 $f_w$。这类预测器是反射式(reflex-based)的——输入进来,一次前向计算,输出就出去,中间没有”思考”的过程。

本讲开始进入课程的第二大模块:基于状态的模型(state-based models)。搜索是其中最基础的一种——在确定性世界中,寻找一个最优的动作序列。下一讲 Lecture 6 · UCS 与 A* 搜索 将把本讲的精确算法推广到有环图;再往后,Lecture 7 · 马尔可夫决策过程 会引入随机性,Lecture 10 · 博弈 I:Minimax 与 α-β 剪枝 会引入对手。


从学习到推理

上周(机器学习)

  • 学习算法:训练数据 {(输入, 输出)} → 预测器
  • 预测器:输入 → 输出(回归输出实数,分类输出类别)

预测器是反射式的(percepts → actions):每次预测的计算量是固定的,不会因为问题更难而”多想一会儿”。但现实世界的很多问题需要推理(reasoning)——思考、问题求解、规划——其本质特征是:答案不是一步查表得到的,而是通过在可能性空间中探索得到的。

graph LR
    P["感知 Perceive"] --> R["推理 Reason"]
    R --> A["行动 Act"]
    A -.反馈.-> L["学习 Learn"]
    L -.改进.-> R

    style R fill:#ffe0b2,stroke:#e65100
    style L fill:#e1f5fe,stroke:#0277bd

本周主题:搜索(确定性世界中的一种推理形式)。

典型例子(它们的共同点是:解是一个序列,而非单个标签):

  • 求解魔方:需要找到一系列旋转步骤,任何单步都不构成解;
  • 从 A 点到 B 点的最短路径:需要输出一条完整路线,且希望总耗时最小;
  • 语言模型的测试时计算(test-time compute):生成文本本质上是在逐 token 地搜索一条”好”的词序列(见第 5 节与 Lecture 17 · 语言模型)。

提示

反射式模型与搜索的分界线在于计算与问题难度的关系。反射式预测器对难题和易题花同样的算力;搜索则允许”难题多算、易题少算”。这正是近年大模型推理(reasoning model)的核心思想:与其把所有智能压进一次前向传播,不如在推理时展开一棵搜索树。


Rich Sutton 的苦涩教训

🔗 The Bitter Lesson (2019)

…利用计算能力的通用方法最终是最有效的,且优势巨大。
能够任意扩展的两种方法是搜索和学习

Sutton 回顾了 70 年 AI 历史后指出:依赖人类知识的精巧方法短期占优,但最终总是被”能随算力增长而变强”的通用方法超越。而能随算力任意扩展的方法恰好只有两类:搜索(用算力探索更多可能性)与学习(用算力拟合更多数据)。

启示:搜索在现代 AI 中越来越重要(尤其是语言模型的测试时计算),但搜索与学习不是二选一——学习负责从数据中获得代价函数/策略/启发式,搜索负责在给定这些组件后找到好的动作序列。本课程后面会反复看到两者的协同(如 Lecture 8 · 强化学习 中学习价值函数、Lecture 11 · 博弈 II:TD 学习与同时博弈 中学习评估函数供搜索使用)。


1. 搜索问题(Search Problem)

1.1 形式化定义

一个搜索问题(search problem)由三个要素组成:起始状态、后继函数(给出每个状态可用的动作及其代价与产生的新状态)、终止判定。

class SearchProblem:
    def start_state(self) -> Any:
        """返回初始状态"""
        raise NotImplementedError

    def successors(self, state: Any) -> list[Step]:
        """从 state 出发的所有后继:(动作, 代价, 新状态)"""
        raise NotImplementedError

    def is_end(self, state: Any) -> bool:
        """state 是否为终止状态"""
        raise NotImplementedError

@dataclass(frozen=True)
class Step:
    action: Any     # 采取的动作
    cost: float     # 该动作的代价
    state: Any      # 动作后到达的状态

用数学记号写出来:搜索问题是四元组 $(s_{\text{start}}, \text{Succ}, \text{IsEnd}, \text{Cost})$,其中 $s_{\text{start}}$ 是起始状态,$\text{Succ}(s)$ 给出状态 $s$ 的全部后继,$\text{IsEnd}(s)$ 判断是否终止,$\text{Cost}(s, a) \ge 0$ 是在状态 $s$ 采取动作 $a$ 的代价。

目标:找到一个解(solution)——从起始状态到某个终止状态的动作序列 $(a_1, \dots, a_T)$,使总代价最小:

$$\min_{a_1, \dots, a_T} \ \sum_{t=1}^{T} \text{Cost}(s_{t-1}, a_t), \quad \text{s.t. } s_0 = s_{\text{start}},\ \text{IsEnd}(s_T)$$

这里 $s_t$ 是执行前 $t$ 个动作后到达的状态,$T$ 是序列长度(本身也是待优化的——解可长可短)。

提示

这个接口刻意做到了建模与求解分离SearchProblem 只描述”世界长什么样”(有哪些状态、动作、代价),完全不关心”怎么找解”。于是同一个求解算法(穷举、动态规划、束搜索……)可以不加修改地套用在旅行问题、魔方、语言生成等任何符合接口的问题上。这与前几讲”模型(假设类)与优化器分离”的思想一脉相承。

1.2 例子:旅行问题

graph LR
    1 --> |walk, 1| 2
    1 --> |tram, 2| 2["2 (×2)"]
    2 --> |walk, 1| 3
    2 --> |tram, 2| 4["4 (×2)"]
    3 --> |walk, 1| 4
    4 --> |walk, 1| 5
    4 --> |tram, 2| 8["8 (×2)"]

    style 1 fill:#c8e6c9,stroke:#2e7d32
    style 8 fill:#ffe0b2,stroke:#e65100
  • 街道上有编号 1 到 $n$ 的街区;
  • 步行从 $i$ 到 $i+1$ 需要 1 分钟;
  • 魔法电车从 $i$ 到 $2i$ 需要 2 分钟;
  • 问题:从 1 到 $n$ 的最少时间是多少?
class TravelSearchProblem(SearchProblem):
    def __init__(self, num_locs: int):
        self.num_locs = num_locs

    def start_state(self) -> int:
        return 1

    def successors(self, state: int) -> list[Step]:
        successors = []
        if state + 1 <= self.num_locs:
            successors.append(Step(action="walk", cost=1, state=state + 1))
        if 2 * state <= self.num_locs:
            successors.append(Step(action="tram", cost=2, state=2 * state))
        return successors

    def is_end(self, state: int) -> bool:
        return state == self.num_locs

例子

候选路径:全程步行 $1 \to 2 \to \cdots \to 8$ 代价 7;全程电车 $1 \xrightarrow{2} 2 \xrightarrow{2} 4 \xrightarrow{2} 8$ 代价 6;混合 $1 \xrightarrow{\text{walk},1} 2 \xrightarrow{\text{tram},2} 4 \xrightarrow{\text{tram},2} 8$ 代价 5——最优。可见”贪心地一直坐电车”并不最优,第一步步行更便宜,这正是需要系统性搜索而非局部贪心的原因。

1.3 状态设计的权衡

问题:如果电车票数量有限怎么办?只知道”当前位置”就不足以判断还能不能坐电车了。

解决:状态不只是位置,还要包含剩余票数:

@dataclass(frozen=True)
class TravelState:
    loc: int        # 当前位置
    tickets: int    # 剩余票数

class LimitedTravelSearchProblem(SearchProblem):
    def start_state(self) -> TravelState:
        return TravelState(loc=1, tickets=self.starting_tickets)

    def successors(self, state: TravelState) -> list[Step]:
        successors = []
        # 步行总是可以
        if state.loc + 1 <= self.num_locs:
            successors.append(Step(
                action="walk", cost=1,
                state=TravelState(loc=state.loc + 1, tickets=state.tickets)
            ))
        # 电车需要有票
        if state.tickets > 0 and 2 * state.loc <= self.num_locs:
            successors.append(Step(
                action="tram", cost=2,
                state=TravelState(loc=2 * state.loc, tickets=state.tickets - 1)
            ))
        return successors

设计原则

  • 状态是对过去动作历史的充分摘要:它应包含”为了最优地选择未来动作所需的全部信息”,而无须记住具体是怎么走到这里的;
  • 但不要包含多余信息——许多算法的复杂度与状态数量成正比,状态里每多一个维度,状态空间就可能按乘法膨胀。

注意

状态设计的两个极端都会坏事。信息不足(如票数有限却只记位置)会导致后继函数无法正确定义——同一个”状态”下可行动作不同;信息过多(如把完整路径塞进状态)会让每条路径都成为独立状态,缓存彻底失效,动态规划退化回穷举搜索。判断标准:未来只依赖状态,不依赖到达状态的方式——这正是马尔可夫性质,将在 Lecture 7 · 马尔可夫决策过程 中正式登场。


2.1 核心概念:未来代价

定义未来代价 $\text{FutureCost}(s)$:从状态 $s$ 出发到达某个终止状态的最小代价。它满足递归关系:

$$\text{FutureCost}(s) = \begin{cases} 0 & \text{IsEnd}(s) \\ \displaystyle\min_{(a,\, c,\, s') \in \text{Succ}(s)} \left[ c + \text{FutureCost}(s') \right] & \text{否则} \end{cases}$$

其中 $(a, c, s')$ 遍历 $s$ 的每个后继步骤:动作 $a$、单步代价 $c$、新状态 $s'$。公式的含义是:从 $s$ 出发的最优解 = 在所有”第一步”中,选择”第一步代价 + 之后走最优”总和最小的那个。这是最优子结构:最优解的任何后缀本身也是(子问题的)最优解。

graph TD
    S["state"] --> S1["successor 1<br/>cost + future_cost"]
    S --> S2["successor 2<br/>cost + future_cost"]
    S --> S3["successor 3<br/>cost + future_cost"]
    S1 --> F["取最小值"]
    S2 --> F
    S3 --> F
    F --> R["future_cost(state)"]

    style R fill:#ffe0b2,stroke:#e65100

2.2 实现

def exhaustive_search(problem: SearchProblem) -> tuple[Solution, int]:
    num_explored = 0

    def future_solution(state: Any) -> Solution:
        nonlocal num_explored
        num_explored += 1

        if problem.is_end(state):
            return Solution(steps=[])  # 基础情况

        # 尝试所有后继
        successors = problem.successors(state)
        solutions = []
        for first_step in successors:
            future_steps = future_solution(first_step.state).steps
            solutions.append(Solution(steps=[first_step] + future_steps))

        # 返回最优解
        return min(solutions, key=lambda x: x.cost)

    return future_solution(problem.start_state()), num_explored

2.3 问题:指数爆炸

problem = TravelSearchProblem(num_locs=4)
solution, num_explored = exhaustive_search(problem)
# num_explored = 9 > 4(状态被重复探索)

problem = TravelSearchProblem(num_locs=17)
solution, num_explored = exhaustive_search(problem)
# num_explored = 32563(指数级增长!)

时间复杂度:$O(b^D)$,其中 $b$ 是分支因子(每个状态平均的后继数,旅行问题中 $b \le 2$),$D$ 是解的最大深度(最长动作序列的长度)。指数的来源是:不同路径可以汇合到同一个状态,但穷举搜索”不认得”它来过,会把该状态下方的整棵子树反复重算。

注意

穷举搜索还隐含一个假设:搜索图是无环的(或解的深度有界)。若存在环(如既能前进又能后退),递归会无限展开。讲义中的补救办法是把”已走步数”计入状态并设置上限,但这会放大状态空间;系统性的解决方案是下一讲 Lecture 6 · UCS 与 A* 搜索 的一致代价搜索。


3. 动态规划(Dynamic Programming)

3.1 核心思想

动态规划 = 穷举搜索 + 缓存(memoization)

关键观察:$\text{FutureCost}(s)$ 只依赖 $s$ 本身,与”怎么走到 $s$”无关(这正是 1.3 节状态设计原则的回报)。所以同一个状态无论从多少条路径到达,其最优未来解都相同——算一次,缓存起来,之后直接查表。

def dynamic_programming(problem: SearchProblem) -> tuple[Solution, int, dict]:
    num_explored = 0
    cache: dict[Any, Solution] = {}  # 状态 → 最优解

    def future_solution(state: Any) -> Solution:
        # 检查缓存
        if state in cache:
            return cache[state]

        nonlocal num_explored
        num_explored += 1

        if problem.is_end(state):
            best_solution = Solution(steps=[])
        else:
            successors = problem.successors(state)
            solutions = []
            for first_step in successors:
                future_steps = future_solution(first_step.state).steps
                solutions.append(Solution(steps=[first_step] + future_steps))
            best_solution = min(solutions, key=lambda x: x.cost)

        # 缓存结果
        cache[state] = best_solution
        return best_solution

    solution = future_solution(problem.start_state())
    return solution, num_explored, cache

3.2 效果对比

num_locs 穷举搜索探索数 动态规划探索数
4 9 4
10 ~1000+ 10
17 32,563 17
100 不可行 100

动态规划将探索数从指数级 $O(b^D)$ 降为状态数 $O(|\mathcal{S}|)$($\mathcal{S}$ 为状态集合;更精确地说,时间为 $O(|\mathcal{S}| \cdot b)$,因为每个状态展开一次全部后继)。

提示

指数加速的本质是把搜索树折叠成搜索图。穷举搜索看到的是一棵树——$2^D$ 片叶子;动态规划看到的是一张图——只有 $|\mathcal{S}|$ 个节点。折叠的许可来自两个条件的合取:最优子结构(子问题的解可复用)+ 重叠子问题(大量路径汇合到同一状态)。这一思想由 Bellman 在 1950 年代提出,其连续/随机版本就是 Lecture 7 · 马尔可夫决策过程 中的价值迭代。

3.3 何时使用动态规划?

适用场景

  • 状态数量适中,缓存能装进内存;
  • 存在大量到达同一状态的不同路径——缓存的每次命中都省掉一整棵子树。

不适用场景

  • 状态数量巨大(如围棋约 $10^{170}$ 种局面),缓存本身就存不下;
  • 几乎每条路径都通向不同状态(如状态里含完整生成历史的语言模型),缓存永远不命中,DP 退化为穷举;
  • 图中有环——递归没有良定义的计算顺序(详见 Lecture 6 · UCS 与 A* 搜索)。

4. 近似搜索

4.1 问题

精确搜索(穷举/动态规划)的时间复杂度至少是 $O(|\mathcal{S}|)$——每个状态至少要碰一次。

但如果状态本身是组合对象,状态数会爆炸:

  • 状态是”位置的集合”:$n$ 个位置就有 $2^n$ 个状态;
  • 状态是”已生成的词序列”:词表大小为 $V$、长度为 $T$ 时有 $V^T$ 个状态。

此时任何精确算法都不可行

解决方案:近似搜索——只探索状态空间的一小部分,用启发式规则决定看哪里。代价是可能错过最优解,但在精确解无望的问题上,”较好的解”远胜于”没有解”。

4.2 Best-of-N 搜索

思路:用某个策略(最简单的是均匀随机)从头到尾走一条完整路径(称为一次 rollout),独立重复 $N$ 次,返回其中总代价最小的一条。

def uniform_policy(problem: SearchProblem, state: Any) -> Step:
    """从 state 的后继中均匀随机选一个"""
    successors = problem.successors(state)
    return random.choice(successors)

def rollout(problem: SearchProblem, policy, max_steps: int = 10) -> Solution:
    """从起始状态按照 policy 采样一条路径"""
    state = problem.start_state()
    steps = []
    while not problem.is_end(state) and len(steps) < max_steps:
        step = policy(problem, state)
        steps.append(step)
        state = step.state
    return Solution(steps=steps)

def best_of_n(problem: SearchProblem, policy, num_candidates: int) -> tuple[Solution, int]:
    solutions = [rollout(problem, policy) for _ in range(num_candidates)]
    return min(solutions, key=lambda x: x.cost), num_candidates

性质

  • 理论保证:只要每条最优路径被采到的概率非零,$N \to \infty$ 时以概率 1 找到最优解;
  • 易并行化:$N$ 条 rollout 相互独立,可以完全并行地跑在不同机器上,采样效率随算力线性扩展;
  • 只需能”打分”:不要求逐步的引导信号,只要事后能比较完整解的好坏(一个验证器/代价函数)即可;
  • ❌ 完全不利用问题结构:好路径与坏路径都被平等采样,最坏情况下需要指数级的 $N$ 才能撞上好解。

说明

Best-of-N 正是当下 LLM 测试时扩展的主力做法之一:对同一道题采样 $N$ 个回答,用验证器(单元测试、答案校验器、奖励模型)挑最好的。Large Language Monkeys(2024, arXiv:2407.21787)系统地测量了这条路线:问题的覆盖率(至少采到一个正确解的概率)随 $N$ 增大近似按幂律持续提升,采样上千次仍未饱和——“苦涩教训”在测试时计算上的直接体现。

思路:不再各条路径独立到底,而是逐步齐头并进:维护一个大小为 beam_width(束宽,记 $k$)的候选集合,每一步把所有候选各自展开全部后继,再从展开结果中只保留代价最小的 $k$ 个。

def beam_search(problem: SearchProblem, beam_width: int) -> tuple[Solution, int]:
    num_explored = 0
    candidates = [Solution(steps=[])]  # 从空解开始

    while True:
        # 扩展所有候选
        new_candidates = []
        for candidate in candidates:
            state = candidate.steps[-1].state if candidate.steps else problem.start_state()
            if problem.is_end(state):
                new_candidates.append(candidate)
            else:
                for successor in problem.successors(state):
                    new_candidates.append(Solution(steps=candidate.steps + [successor]))
                    num_explored += 1

        # 保留最好的 beam_width 个
        new_candidates.sort(key=lambda x: x.cost)
        candidates = new_candidates[:beam_width]

        # 如果所有候选都到终点了,停止
        if all(problem.is_end(c.steps[-1].state) for c in candidates):
            break

    return candidates[0], num_explored

可视化

graph TD
    S["起点"] --> A1["候选1"]
    S --> A2["候选2"]
    S --> A3["候选3"]
    S -.被剪枝.-> X1["候选4 ×"]

    A1 --> B1["候选1.1"]
    A1 --> B2["候选1.2"]
    A2 --> B3["候选2.1"]
    A3 -.被剪枝.-> X2["候选3.1 ×"]

    style S fill:#c8e6c9,stroke:#2e7d32
    style X1 fill:#ffcdd2,stroke:#c62828
    style X2 fill:#ffcdd2,stroke:#c62828

性质

  • ✅ 比 best-of-N 更有结构:坏前缀在中途就被淘汰,算力集中在当前看起来最有希望的前缀上;
  • ✅ 计算量可控:每步至多展开 $k \cdot b$ 个候选,总时间约 $O(k \cdot b \cdot D)$,与束宽线性相关;
  • 无最优性保证:剪枝依据的是前缀代价,而一个”开局昂贵、后程便宜”的最优路径可能在早期就被剪掉,且永远无法挽回。

提示

束宽 $k$ 是一个在”贪心”与”穷举”之间连续调节的旋钮:$k=1$ 时束搜索退化为贪心搜索(每步只选局部最优);$k \to \infty$ 时退化为逐层穷举。与 best-of-N 相比,束搜索用”中途剪枝”换取了对好前缀的聚焦,但也因此丧失了 best-of-N 的两个优点——完全并行与渐近最优保证。两者没有绝对优劣,取决于前缀代价对最终质量的预示能力。


5. 测试时计算:语言模型中的搜索

5.1 语言生成即搜索

语言模型生成文本 = 在巨大的词序列空间中搜索。状态是”提示词 + 已生成的前缀”,动作是”选择下一个 token”,单步代价取该 token 的负对数概率

$$c_t = -\log p_\theta(x_t \mid x_{

于是一条完整生成 $x_{1:T}$ 的总代价为

$$\text{cost}(x_{1:T}) = \sum_{t=1}^{T} -\log p_\theta(x_t \mid x_{

其中 $p_\theta$ 是参数为 $\theta$ 的语言模型给出的条件概率,$x_{最小化总代价 = 最大化整条序列的对数似然——搜索的语言与概率的语言在这里严丝合缝地对上了。

class LanguageModelSearchProblem(SearchProblem):
    def start_state(self) -> str:
        return self.prompt  # 起始状态是提示词

    def successors(self, state: str) -> list[Step]:
        # 用模型计算下一个 token 的概率
        input_ids = self.tokenizer.encode(state, return_tensors="pt")
        with torch.no_grad():
            logits = self.model(input_ids).logits[0, -1, :]
        probs = F.softmax(logits, dim=-1)

        # 返回所有可能的下一个 token(按概率排序)
        successors = []
        for token_id in torch.argsort(probs, descending=True)[:self.top_k]:
            next_token = self.tokenizer.decode([token_id])
            cost = -torch.log(probs[token_id]).item()  # 负对数概率
            successors.append(Step(
                action=next_token,
                cost=cost,
                state=state + next_token
            ))
        return successors

    def is_end(self, state: str) -> bool:
        return state.endswith(".") or len(state) > 100

说明

search.py 用一个小模型(Qwen3-0.6B)实际跑通了这个问题,并且多加了一层:一个验证器(verifier),对通过校验的完整生成额外奖励(代价 $-100$)。这样搜索目标就从”模型认为最像的话”变成”既流畅又被验证为正确的话”——这正是”采样 + 验证”式测试时计算的最小可运行范例。

5.2 应用束搜索

problem = LanguageModelSearchProblem(prompt="The capital of France is")
solution, _ = beam_search(problem, beam_width=5)
print(solution.steps)
# 可能输出:"The capital of France is Paris."

测试时计算的威力

  • 同一个模型,投入更多推理算力(更大的束宽、更多的采样次数、更长的思维链)通常能换来更好的输出质量;
  • OpenAI o1 系列等推理模型把这条曲线做成了产品:训练时学到策略,测试时大量”思考”;
  • 代价是延迟与费用:质量与速度的权衡从训练时延伸到了推理时(LLM 推理的系统层面优化见 CS336 的 Lecture 10 · 推理(未找到对应页面))。

注意

对语言模型而言,”总代价最小(似然最大)的序列”未必是”最好的回答”——极端追求高似然会产出空洞、重复的文本(这也是实践中很少用大束宽做开放生成的原因)。所以现代做法是把外部信号(验证器、奖励模型)注入代价函数,让搜索优化”正确/有用”而不仅是”像人话”。


6. 总结

mindmap
  root((搜索算法))
    建模
      SearchProblem 接口
      状态 / 动作 / 代价
      目标:最小化总代价
    精确方法
      穷举搜索:指数时间
      动态规划:线性于状态数
      缓存 = 指数加速
    近似方法
      Best-of-N:随机采样取最优
      束搜索:每步保留前 k 个候选
      权衡:速度 vs 质量
    应用
      路径规划
      语言模型生成
      测试时计算

关键要点

  • 搜索问题用(状态、动作、代价、终止条件)四要素形式化了”找最优动作序列”,实现了建模与算法的解耦;
  • 穷举搜索沿 $\text{FutureCost}$ 递归展开整棵搜索树,能找到精确解,但时间 $O(b^D)$ 指数爆炸;
  • 动态规划 = 穷举搜索 + 缓存:利用”未来只依赖状态”这一性质把树折叠成图,复杂度降为 $O(|\mathcal{S}|)$,前提是无环且状态数可容纳;
  • 状态数巨大时改用近似搜索:best-of-N(独立采样、可并行、渐近最优)与束搜索(逐步剪枝、聚焦好前缀、无保证);
  • 学习 + 搜索协同:代价(如 $-\log p_\theta$)从数据中学习,搜索在给定代价下找最优序列——这是测试时计算的原型。

下一讲预告:如果图中有环(A → B → C → A),动态规划的递归就没有了合法的计算顺序,怎么办?→ Lecture 6 · UCS 与 A* 搜索


复习自测

题目

状态必须包含”最优选择未来动作所需的全部信息”:票数决定了电车动作是否可用,不放进状态则后继函数无法定义。但把完整路径放进状态会让每条路径成为独立状态——动态规划的缓存永远不会命中(没有两条路径”汇合”到同一状态),复杂度退化回穷举搜索的指数级。状态要充分且最小

题目

代价 5。例如 $1 \xrightarrow{\text{walk},1} 2 \xrightarrow{\text{tram},2} 4 \xrightarrow{\text{tram},2} 8$。全程电车($1\to2\to4\to8$,代价 $2+2+2=6$)反而更贵,因为第一步步行比电车便宜——最优解需要全局比较,贪心不行。

题目

因为 $\text{FutureCost}(s)$ 只依赖状态 $s$ 本身,与到达路径无关,所以每个状态只需计算一次、之后查缓存。前提一:图无环(递归有良定义的展开顺序);前提二:状态可哈希且数量能装进内存,并且确实存在大量路径汇合到同一状态——否则缓存无用武之地。

题目

Best-of-N 占优的情形:需要大规模并行(rollout 相互独立)、前缀代价对最终质量预示性差(中途剪枝容易误杀)、有可靠的终局验证器可以挑选。束搜索占优的情形:前缀代价信息量大(坏开头基本注定坏结尾)、算力预算固定且想把它集中花在有希望的前缀上。极端情形对照:束宽 1 的束搜索 = 贪心;N 无穷的 best-of-N 渐近最优但代价不可控。

题目

总代价是负对数似然之和,最小化它等价于最大化整条序列的概率 $p_\theta(x_{1:T})$。但”模型最可能说的话”未必是最好的回答:高似然序列往往安全、重复、空洞(似然与人类偏好不一致)。因此实践中要么用采样引入多样性,要么把验证器/奖励模型加进代价,让搜索优化正确性而非纯似然。


参考资料