Zhangzhe's Blog

The projection of my life.

0%

URL

TL;DR

  • langchainSemi-structured RAGdemo,包含 PDF 的 解析 -> 向量化 -> 存储 -> 检索 -> 回答 的全流程
  • 来自 langchain 官方的 cookbook,值得参考

总体流程

langchain_rag_demo.png

  1. 解析 PDF 文件。用 partition_pdf 工具,将 PDF 文件解析为 chunks,分成文本和表格两种
  2. 总结 chunks。用大模型 API 总结 chunks,得到 summary,用哪家的模型都行
  3. 向量化 summary。调用 embedding 模型 APIsummary 进行向量化,作为索引的 keyvalue 是原始文本/表格),存到 Chroma 数据库中
  4. 问答时自动检索。在问答时,会自动根据问题向量在 Chroma 数据库中检索出最相似的 summary,将 summary 向量对应的原始文本/表格作为 prompt 中的 context 域传给大模型,得到回答

具体实现代码

1. 解析 PDF 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from typing import Any

from pydantic import BaseModel
from unstructured.partition.pdf import partition_pdf

path = "/Users/rlm/Desktop/Papers/LLaMA2/"
# Get elements
raw_pdf_elements = partition_pdf(
filename=path + "LLaMA2.pdf",
# Unstructured first finds embedded image blocks
extract_images_in_pdf=False,
# Use layout model (YOLOX) to get bounding boxes (for tables) and find titles
# Titles are any sub-section of the document
infer_table_structure=True,
# Post processing to aggregate text once we have the title
chunking_strategy="by_title",
# Chunking params to aggregate text blocks
# Attempt to create a new chunk 3800 chars
# Attempt to keep chunks > 2000 chars
max_characters=4000,
new_after_n_chars=3800,
combine_text_under_n_chars=2000,
image_output_dir_path=path,
)

2. 总结 chunks 得到 summary

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI

# Prompt
prompt_text = """You are an assistant tasked with summarizing tables and text. \
Give a concise summary of the table or text. Table or text chunk: {element} """
prompt = ChatPromptTemplate.from_template(prompt_text)

# Summary chain
model = ChatOpenAI(temperature=0, model="gpt-4")
summarize_chain = {"element": lambda x: x} | prompt | model | StrOutputParser()

# Apply to tables
tables = [i.text for i in table_elements]
table_summaries = summarize_chain.batch(tables, {"max_concurrency": 5})

# Apply to texts
texts = [i.text for i in text_elements]
text_summaries = summarize_chain.batch(texts, {"max_concurrency": 5})

langchain| 符号就可以把多个工具串成一个 chain,非常方便

3. 向量化 summary 并存到 Chroma 数据库中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import uuid

from langchain.retrievers.multi_vector import MultiVectorRetriever
from langchain.storage import InMemoryStore
from langchain_chroma import Chroma
from langchain_core.documents import Document
from langchain_openai import OpenAIEmbeddings

# The vectorstore to use to index the child chunks
vectorstore = Chroma(collection_name="summaries", embedding_function=OpenAIEmbeddings())

# The storage layer for the parent documents
store = InMemoryStore()
id_key = "doc_id"

# The retriever (empty to start)
retriever = MultiVectorRetriever(
vectorstore=vectorstore,
docstore=store,
id_key=id_key,
)

# Add texts
doc_ids = [str(uuid.uuid4()) for _ in texts]
summary_texts = [
Document(page_content=s, metadata={id_key: doc_ids[i]})
for i, s in enumerate(text_summaries)
]
retriever.vectorstore.add_documents(summary_texts)
retriever.docstore.mset(list(zip(doc_ids, texts)))

# Add tables
table_ids = [str(uuid.uuid4()) for _ in tables]
summary_tables = [
Document(page_content=s, metadata={id_key: table_ids[i]})
for i, s in enumerate(table_summaries)
]
retriever.vectorstore.add_documents(summary_tables)
retriever.docstore.mset(list(zip(table_ids, tables)))

4. 问答时自动检索

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from langchain_core.runnables import RunnablePassthrough

# Prompt template
template = """Answer the question based only on the following context, which can include text and tables:
{context}
Question: {question}
"""
prompt = ChatPromptTemplate.from_template(template)

# LLM
model = ChatOpenAI(temperature=0, model="gpt-4")

# RAG pipeline
chain = (
{"context": retriever, "question": RunnablePassthrough()}
| prompt
| model
| StrOutputParser()
)

# run the chain
chain.invoke("What is the number of training tokens for LLaMA2?")

总结

  1. langchain 做了非常多的工具,并给出一种将这些工具非常容易组合使用的方法
  2. 更重要的是,langsmith 提供了非常方便的 trace 功能,可以非常方便地追踪一次问答过程中经过了哪些模型/工具/行为,以及这些模型/工具/行为的 input / output / 耗时等,非常方便

URL

TL;DR

  • Qwen2.5-VLQwen 团队推出的一个多模态大模型,在 Qwen2.5 的基础上,增加了视觉模态的支持,输入支持 textimagevideo 的混合输入,输出为 text

Algorithm

qwen2.5-vl.png

模型架构

  • 视觉编码器(Vision Encoder):基于重新设计的 Vision TransformerViT),处理原生分辨率的图像和视频输入
  • 语言模型(Large Language ModelLLM):基于 Qwen2.5 LLM,负责文本理解和生成,初始化时预训练权重被微调以支持多模态任务
  • 视觉-语言合并器(Vision-Language Merger):一个 MLP-based 模块,压缩视觉特征以匹配文本嵌入维度,减少计算开销

三阶段训练

第一阶段

  • 随机初始化 Vision Encoder 开始训练
  • 使用的 (text, image) 数据如下:
    • Image captions:图像和对应的文本描述
    • Visual knowledge: 涵盖名人、地标、动植物等识别数据,帮助模型积累视觉常识
    • OCR 数据:从图像中提取的文本信息
  • 用了 CLIP 作为优化目标,对齐 ViTQwen2.5text 模态
  • token 长度为 8k,数据规模为 1.5T tokens

第二阶段

  • ViTQwen2.5 的联合预训练
  • token 长度为 8k,数据规模为 2T tokens

第三阶段

  • 长上下文优化,目标是视频/文档序列理解
  • token 长度为 32k,数据规模为 0.6T tokens

关键技术解析

1. 动态 ViT 架构

  • 输入尺寸自适应:图像按 14×14 分块,尺寸调整为 28 的倍数
  • 窗口注意力:32 层中仅 4 层用全局注意力,其余用 112×112 窗口注意力(计算复杂度从 O(n²) 降至 O(n)
  • 位置编码:2D 旋转位置嵌入(RoPE)保留空间关系

2. 多模态动态处理

  • 空间维度:
    • 原生分辨率坐标:直接使用图像实际尺寸表示物体位置(非相对坐标)
    • 支持 JSON/XML 格式输出,兼容开放词汇检测(10,000+ 类别)
  • 时间维度:
    • 动态帧率采样:适应不同速度的视频内容
    • 绝对时间对齐:RoPE 时间 ID 与时间戳直接绑定,理解事件节奏(图1机制)

3. 多模态位置编码(MRoPE)​

  • 三维分解:时间、高度、宽度独立位置 ID
  • 视频处理:时间 ID 按帧递增,空间 ID 与静态图像一致
  • 升级点:时间 ID 关联绝对时间戳,解决 Qwen2-VL 的时序理解局限

性能

  • 共有 3B / 7B / 72B 三个尺寸
    qwen2.5-vl_2.png

Thoughts

  • VLM 将视觉信息融入语言模态,重点还是 LM

URL

TL;DR

  • 标题是 “用于端到端分层序列建模的动态分块” ,实际上包含了不少信息量:
    • “端到端”:意味着 token free,真正的端到端语言模型,输入输出都是字节流,不需要 tokenization 过程
    • “分层”:意味着 H-Net 模型结构是 递归 的,H-Net 模型由 encoder + main network + decoder 组成,其中 main network 还可以是 H-Net 模型
    • “动态分块”:意味着 H-Net 模型可以动态地调整 chunk 的大小,可以理解为维护了一个隐式的动态 tokenizer,模型会在学习过程中找到最优的隐式分词方法
  • 本质是一个基于 SSM + Transformer 结构的大模型,抛弃了 tokenization 过程,直接在字节流上进行训练和推理

Algorithm

H-Net 总体结构

hnet.png

现有的分词机制的缺陷

  • 当前主流语言模型(如 ChatGPT )依赖预定义的分词器(如 BPE ),存在以下问题:
    • 语义割裂​​:分词器基于统计规则,无法根据上下文动态调整边界(如将 “product” 错误拆分为 “pro-duct” )
    • 跨语言/模态适配性差​​:在中文、代码或 DNA 序列等缺乏显式分隔符的领域表现不佳
  • 直接字节级建模(如 MambaByte ),计算开销巨大,且性能低于分词模型
  • 一些启发式分块规则(如 MegaByteSpaceByte)依赖​​启发式分块规则​​(如固定步长或空格分隔),无法学习数据驱动的分块策略,限制了模型对复杂信息的表达能力

H-Net 的解决方案

层级化处理架构(U-Net 式设计)​​

  • 三级模块
    • 编码器(E)​​:处理原始字节(小规模 SSM 层,高效捕获细粒度特征)
    • 主网络(M)​​:处理压缩后的语义块(大规模 Transformer 层,学习高层抽象)
    • 解码器(D)​​:恢复原始分辨率(SSM 层)
  • 递归扩展​​:主网络可嵌套 H-Net 自身,形成多级抽象(如字符 → 词 → 短语)

Encoder Network

  1. 路由模块(Routing Module
    • 用相邻向量的余弦相似度判断是否需要分块
    • pt=12(1sim(qt,kt1))p_t=\frac{1}{2}(1-sim(q_t,k_{t-1}))
      • tt 表示 position
      • qq 表示 query
      • kk 表示 key
      • simsim 表示余弦相似度
      • ptp_t 表示当前位置分块概率
    • 当相邻向量语义变化大时,ptp_t 会趋近于 1,表示需要分块
  2. 下采样模块(Downsampler
    • 将上一步分块的序列下采样
    • 具体使用的下采样方式是 只保留路由模块选定的边界向量,其他向量直接丢弃

Main Network

  • 一个最常见的 Transformer 结构
  • 输入是 Encoder Network 输出的下采样后的向量
  • 输出和输入的 shape 相同

Decoder Network

  1. 平滑模块(Smoothing Module
    • 用指数移动平均法解决离散决策的梯度不可导问题
    • zˉt=Pz^t1+(1Pt)zˉt1\bar z_t=P\cdot \hat z_{t-1}+(1-P_t)\cdot \bar z_{t-1}
      • PtP_t 表示当前位置分块概率
      • z^t\hat z_t 表示主网络输出的压缩向量
      • zˉt1\bar z_{t-1} 表示上一步平滑后的向量
      • zˉt\bar z_t 表示平滑后的向量
  2. 上采样模块(Upsampler

    ct={ptif bt=11ptif bt=0c_t=\left\{ \begin{array}{l} p_t & \text{if}\ b_t=1 \\ 1- p_t & \text{if}\ b_t=0 \end{array} \right.

    Upsamplert=STE(ct)z~t\text{Upsampler}_t = STE(c_t)\cdot \tilde z_t

    • btb_t 表示当前位置是否分块,bt=1b_t=1 表示分块,bt=0b_t=0 表示不分块
    • ptp_t 表示当前位置分块概率
    • ctc_t 表示当前位置 main network 输出的置信度
    • z~t\tilde z_t 表示当前位置 main network 输出的向量 复制到原始分辨率

效果

  • 效果比 tokenization 更好
  • 在语言、代码、DNA等异构数据中验证普适性,为多模态基础模型提供新范式

Thoughts

  • 能看出 SSM 模型已经开始改变战略,从正面和 Transformer 硬刚到 曲线救国
  • STE 真是个万金油,哪哪都有它

URL

TL;DR

  • 本文提出一种新的缓存利用机制 CacheBlend,旨在解决 RAG 系统中知识 KV Cache 的可用性,通过一种稀疏近似的方式平衡速度和准确性。

Algorithm

知识注入的两种常见方式

1. 微调

  • 收集知识库中的文档数据,微调模型,使其能够在生成文本时利用这些知识。
  • 优点:推理时不会有额外的延迟。
  • 缺点:
    1. 动态更新困难
    2. 模块化困难(无法要求模型使用哪些知识,不用哪些知识)

2. 知识检索

  • 在推理时,检索相关文档并将其作为上下文输入到模型中。
  • 优点:动态更新知识库,模块化。
  • 缺点:推理时需要额外的延迟,一是因为检索本身的延迟,二是因为模型需要计算额外的上下文注意力。

如何同时解决模块化和速度的问题?

  • 如果在知识检索技术的基础上,检索系统提前缓存了 知识库中文档对应的 KV Cache,那么就可以在推理时直接使用这些 KV Cache 无需计算这部分的注意力(只需要检索延迟)。

这样做有什么问题?

  • 一个很重要的问题是:优于现如今绝大多数 LLMdecoder only 架构,缓存的知识库的 KV Cache 必须作为 prefix 注入到模型中,且只能用一个,否则模型无法利用这些知识。
  • 例如:
    • 记检索到的文档的 KV CacheA,历史对话信息和用户输入为 B
    • 那么模型的输入应该是 A + B,而不是 B + A
    • 因为 KV Cache 位置 i 的值依赖于 0 ~ i-1 位置的值,如果不作为 prefix 注入,那么模型无法利用这些知识。

cacheblend_2.png
cacheblend_3.png

CacheBlend 想要解决的问题

  • CacheBlend 目的就是为了 解决 RAG 系统中知识 KV Cache 只能作为 prefix 注入的问题
  • CacheBlend 做不到和完全重新计算数学上等价,只能是近似
  • 原理本质上讲是平衡,即 在完全重新计算 KV Cache 和完全使用缓存的 KV Cache 之间进行平衡,如下图:

cacheblend.png

CacheBlend 的解决方案

cacheblend_4.png

  • CacheBlend 通过 稀疏近似 的方式,平衡速度和准确性,具体来说,对非 prefixKV Cache,做:
    1. 1 层​​:计算所有 TokenKV 偏差,选择 Top 20% 作为候选 HKVD (High-KV-Deviation) Token
    2. ​后续层​​:仅对前一层的 HKVD Token 计算偏差,从中选择 Top ri% (ri<ri1)Top\ r_i\%\ (r_i < r_{i-1}) 作为当前层 HKVD Token
    3. ​​终止​​:最终每层仅更新约 10-15% Token,偏差显著降低
  • 总体上就是用逐层用新的 KV Cache 替换旧的 KV Cache,替换依据是 KV 偏差,偏差越大的 Token 替换的优先级越高。

CacheBlend 的优势

  • 质量保障​​:仅更新 10-15% Token 即可达到 Full KV Recompute 的质量(F1/Rouge-L 偏差 <0.02
  • ​​延迟隐藏​​:KV 重新计算与 KV 缓存加载流水线并行,使额外延迟近乎为零
  • 提速​​:TTFT (Time To First Token) 降低 2.2 ~ 3.3 倍,吞吐量提升 2.8 ~ 5

Thoughts

  • 稀疏无处不在

URL

TL;DR

  • 大模型并不是全能的,大模型 + 工具可以更好地解决问题,本文提出一种新的训练方法,让大模型可以自我学习使用如何工具。
  • 本文的主要贡献是提出了一种新的训练方法,包括通过大模型构造工具调用数据集、清洗数据集、使用工具调用数据集微调大模型。

Algorithm

ToolFormer 的意义

  1. 众所周知,大模型在很多任务上表现出色,但它们并不是全能的,比如去数 “strawberry” 这个单词中有多少个 “r”。
  2. 工具可以帮助大模型更好地解决问题,比如计算器、日历、知识库等。
  3. 一种常见的大模型和工具结合的方式是:通过 Agent 多角色(user / llm / function)多轮对话 形式调用,简单来说就是:
    1. 大模型在需要调用工具的时候,输出一段特定格式的文本
    2. 外部程序解析这段文本,调用相应的工具,调用得到结果
    3. 新的结果作为 Function 角色的输入,继续和大模型对话
  4. ToolFormer 采用的方式和 Agent 有相似之处,也有不同的地方:
    • 相似点:
      1. 都需要大模型输出一段特定格式的文本来调用工具
      2. 都需要一段 endless loop 程序来解析大模型的输出,调用工具
    • 不同点:
      1. ToolFormer 不是通过 多角色多轮对话 的方式调用工具,而是通过 单角色单轮对话 的方式调用工具
      2. ToolFormer 需要对大模型进行 微调,而 Agent 不需要
  5. ToolFormer 可以将存在确定答案的 专用 任务转化为工具调用任务(例如:计算、翻译、问答等),让大模型可以更专注在 通用 任务上(例如:上下文理解、常识知识运用等)。

ToolFormer 的工作方式

  1. ToolFormer 是经过工具调用微调的大模型,知道有哪些工具可以调用,也知道如何调用这些工具。
  2. 假设模型输入的问题是:
    1
    Pittsburgh is also known as
  3. 这个时候,模型会意识到这个问题可以通过调用 Question Answering 工具来解决,于是模型会续写:
    1
    Pittsburgh is also known as <API>QA(Pittsburgh is also known as)</API>
  4. 输出 </API> 之后,推理进程会暂停推理模型,等待外部监听程序的调用结束。
  5. 外部监听程序会解析模型输出文本中的工具调用指令(通过 <API> </API> 格式),然后调用 Question Answering 工具,得到结果:
    1
    the Steel City
  6. 推理程序将工具调用结果和模型的历史输出(去掉调用相关信息)拼接起来,继续推理模型:
    1
    Pittsburgh is also known as the Steel City.

应该怎么得到 ToolFormer

  1. 假设上面提到的 ToolFormer 工作方式是一个愿景,那么接下来就是考虑应该如何得到一个这样的模型。
  2. 显然,直接拿一个预训练或经过微调的大模型来使用是行不通的,因为它并不知道哪些工具可以用,以及如何调用。
  3. 那么需要做的事就是:通过微调,让大模型自己学习如何使用工具
  4. 最困难的部分就是:如何构造工具调用数据集,因为不存在现成的工具调用数据集,需要自己构造。
  5. 这篇论文花了很大的篇幅就在讲一件事:如何用大模型来构造工具调用数据集

如何构造工具调用数据集

toolformer_1.png

上面这张图展示了如何用大模型来构造工具调用数据集的流程。

  1. 首先,还是用预训练数据做为基础,假设一条预训练数据是:
    1
    Pittsburgh is also known as the Steel City.
  2. 然后,使用大模型(例如:GPT-3)来在数据中找到可以调用工具的位置和工具类型,并给出调用工具的参数,例如:
    1
    Pittsburgh is also known as <API>QA(What other name is Pittsburgh known by?) -> Steel City</API> the Steel City.
    1
    Pittsburgh is also known as <API>QA(Which country is Pittsburgh in?) -> United States</API> the Steel City.
  3. 计算调用工具带来的损失收益,剔除负收益的数据,保留正收益的数据。
    • 损失的计算方式(离调用工具位置越远,损失权重 wjiw_{j-i} 越小):

    Li(z)=j=inwjilogpM(xjz,xi;j1)L_i(z) = -\sum_{j=i}^n w_{j-i}\cdot \log p_M(x_j|z,x_{i;j-1})

    • 调用工具的损失:

    Li+=Li(e(ci,ri))L_i^+=L_i(e(c_i,r_i))

    • 不调用工具 / 调用工具没有返回的损失:

    Li=min(Li(ϵ),Li(e(ci,ϵ)))L_i^- = \min(L_i(\epsilon),L_i(e(c_i,\epsilon)))

    • 只保留调用工具损失收益大于阈值的样本:

    LiLi+>τfL_i^- - L_i^+ > \tau_f

  4. 用筛选后的数据来微调大模型,得到 ToolFormer

toolformer_2.png

  • 上图展示了 ToolFormer 支持的五种工具类型:
    1. Question Answering:问答工具
    2. Wikipedia Search:维基百科搜索工具
    3. Calculator:计算器工具
    4. Calendar:日历工具
    5. Machine Translation:机器翻译工具

Thoughts

  • ToolFormer 这种通过大模型自我学习使用工具的方式感觉挺好的,但似乎在实际使用中没有得到大范围推广,目前主流外挂工具的方式基本还是 Agent 的多角色多轮对话方式。
  • 可能是因为 ToolFormer 的方式需要对大模型进行微调,而 Agent 的方式不需要微调,直接使用预训练模型就可以。

URL

TL;DR

  • 人类通过语言推理(分解目标、调整计划)与行动(获取外部信息)的​​协同机制​​高效完成任务(如烹饪时动态调整步骤)。
  • 受到人类智能启发,本文提出一种 ReasoningActing 相结合的框架,称为 ReAct,这种新的推理范式可以使语言模型在复杂任务中表现更好。
  • 主要优势包括:
    • 事实性:减少模型幻觉(虚构信息)。
    • 决策鲁棒性:增强模型在复杂环境中的泛化能力。
    • 可解释性:人类可以追踪模型的推理轨迹。

Algorithm

ReAct 框架

ReACT.png

  • 纯推理模型(如 Chain-of-Thought)​​:易产生事实幻觉(如虚构信息)和错误传播(如算术推理错误),缺乏实时环境交互能力(图 1b)。
  • ​​纯行动模型(如 WebGPT)​​:缺乏高层规划能力,难以处理多步决策(图1c)。
  • ReAct 模型:交替进行推理和行动,能够在复杂任务中表现更好(图 1d)。

核心贡献​​

  • 首提协同框架​​:统一推理与行动,解决静态推理与无规划行动的缺陷。
  • 实践价值​​:
    • 提升模型​​事实性​​(减少幻觉)与​​决策鲁棒性​​(复杂环境泛化)。
    • 增强​​可解释性​​:人类可追踪推理轨迹。

Thoughts

  • 论文提出的算法一眼开门,非常符合直觉,从哲学角度讲,一个事物的超集一般都优于其本身,因为最差的情况就是超集退化为本身(因此常见的一个情况是:新的论文称一篇老的论文是其某个参数设置下的一个特例)。
  • 这些所有的 ReasoningActingAgent 功能,都建立在大模型超长上下文的基础上,因此模型支持超长上下文长度是模型是否具有高级智能潜质的先决条件(至少现有范式是这样)。

URL

TL;DR

  • 本文是 Google Research 团队发表的一篇论文,这篇论文是在已开源的 非推理 模型上,不做微调,而是通过 Chain-of-Thought Prompting 的方式来引导模型进行推理,取得了很好的效果。
  • 具体来说,就是在输入 prompt 中加入少量的 输入-思维链-输出 三元组示例,引导模型生成中间推理步骤,最终给出答案。
  • 在算术、常识和符号推理等任务上,Chain-of-Thought Prompting 的效果都非常好,比直接给出答案的效果好很多。
  • Chain-of-Thought Prompting 是在模型规模达到一定程度(>= 100B)后,才涌现出的能力,小模型没有这个能力。

论文详情

1. 核心方法:思维链提示(Chain-of-Thought Prompting)

  • 论文提出一种简单方法:在提示(prompt)中提供 输入-思维链-输出 的三元组示例,引导大型语言模型生成一系列中间推理步骤(称为 “思维链”),再得出最终答案。
  • 思维链类似于人类逐步推理的过程(例如,解决数学题时先分解步骤:“先计算A,再计算B,最后得出答案”)。

2. 关键优势

  • 提升复杂推理能力:在算术(如数学题 GSM8K)、常识(如 CSQA)和符号推理(如字母拼接游戏)任务上,思维链提示显著优于标准提示(standard prompting)。
  • GSM8K 数学题基准上,PaLM 540B 模型使用思维链提示后,准确率从 17.9% 提升至 56.9%,甚至超过微调的 GPT-3 模型。
  • 在常识推理任务(如 StrategyQA)上,准确率从 68.6% 提升至 77.8%
  • 模型规模涌现特性:思维链推理是大型模型(约 100B 参数以上)的 “涌现能力” —— 小模型无法生成逻辑思维链,但足够大的模型(如 GPT-3 175BPaLM 540B)能自然学习此模式。
  • 无需微调:仅需在提示中添加少量示例(如 8 个),即可激发模型能力,无需额外训练或数据标注。
  • 可解释性与泛化性:思维链提供透明推理路径,便于调试;且适用于多种任务(数学、常识、符号等),甚至能泛化到更长序列。

3. 实验验证

  • 任务覆盖:
    • 算术推理:在 GSM8KSVAMP 等数据集上,思维链提示将性能提升高达 39%PaLM 540B)。
    • 常识推理:在 StrategyQADate Understanding 等任务上,模型表现接近或超越人类水平。
    • 符号推理:在硬币翻转(coin flip)和字母拼接(last letter concatenation)任务中,模型能处理未见过的长序列。
  • 鲁棒性:不同注释者编写的思维链示例均有效,且对示例顺序、数量变化不敏感。

4. 局限性与启示

  • 模型规模依赖:思维链仅在大型模型(≥100B 参数)中有效,小模型生成逻辑混乱。
  • 潜在错误:生成的推理路径可能不准确(如算术计算错误或语义误解),需外部验证(如添加计算器)。
  • 应用意义:该方法拓展了提示技术的边界,证明大型模型能通过自然语言示例学习复杂推理,减少对标注数据的依赖。

论文核心贡献

  • 思维链提示是一种低成本、高效的方法,通过模拟人类逐步推理过程,释放大型语言模型在复杂任务上的潜力。论文强调,这是 “模型规模涌现” 的典型例子——推理能力随模型增大而自然出现,为未来 AI 推理研究提供了新方向。

Thoughts

  • 一定要注意,这篇论文讨论的对象 不是 Reasoning 模型(这个论文出来的时候还没有 Reasoning 模型的概念),而是普通的 LLM 模型。
  • 本质是一种通过 prompt 引导模型通过增加推理计算预算的方式,来提升模型的推理能力的方法。
  • 依托于 LLM 恐怖的指令遵循和上下文学习能力。

URL

TL;DR

  • LLaDA 提出了一个新概念,叫 “扩散语言模型”,和主流的自回归语言模型 predict next token 的方式不同,LLaDA 使用类似 Diffusion 去噪的方法,一次性生成多个 token,通过多次生成,得到一个完整的生成文本。
  • 但细看就会发现,Diffusion 就是一个彻头彻尾的噱头,和经典的热力学扩散过程没有鸡毛关系,LLaDA 本质就是一个大 BERT 模型,用完形填空的方式来生成文本(一次可以做多个完形填空),只是下图所示的每轮迭代的过程看起来有点像 Diffusion 的去噪(没关系硬蹭)。
    llada

上图来自官方 repoREADME

Algorithm

总体流程

  • 虽然多少有点标题党,但这篇论文本身是值得一读的,将文本生成任务做了重新定义,确实可大幅提高生成速度。

模型架构

  • 纯纯 Transformer encoder 架构,和 BERT 类似,双向注意力,模型参数规模达 8B

训练过程

  1. 预训练
    • 使用随机 mask 一定比例的 token,然后使用 Transformer 预测被 masktoken(完形填空)
    • 损失函数:mask 部分的 cross-entropy 损失
    • 数据规模:2.3 万亿 token,包含通用文本、代码和多语言数据
  2. SFT
    • 目标:使模型具备指令跟随能力
    • 数据格式:成对数据 (pθ,rθ)(p_\theta,r_\theta),其中 pθp_\theta 是指令,rθr_\theta 是响应
    • 掩码策略:仅对响应部分掩码,保持指令完整
    • 损失函数:仅对响应部分计算 cross-entropy 损失
    • 数据规模:450 万对指令响应对,涵盖代码、数学和多轮对话

推理与生成

  • 过程:从全掩码的响应开始,逐步预测并更新掩码 token,直到生成完整响应
  • 重掩码策略(预测之后 mask 一部分生成结果做二次生成):
    • 随机重掩码:基础策略,与扩散过程对齐
    • 低置信度重掩码:优先掩码预测置信度低的 token
    • 半自回归策略(SFT后):分块生成,块内并行预测以提高效率
  • 生成效果:支持多轮对话、多语言翻译和复杂推理任务

和自回归模型对比

特性 自回归模型(如GPT LLaDA
生成顺序 严格从左到右逐 token 生成 并行预测 + 动态调整
计算效率 需串行预测多次 仅需少量迭代(块级并行)
错误修正能力 无法修改已生成 token 通过重掩码可修正低置信度位置
逆向推理支持 受限于单向建模 双向注意力机制支持逆向推理

Thought

  • 预测下一个词的大模型范式是否一定是最优的?可能未必。这篇论文就提出了一个不错的思路
  • make bert great again 手动滑稽

URL

TL;DR

  • LDMstable diffusion 系列的开山之作,让 Diffusion ModelImage Synthesis 领域大放异彩
  • 传统的 Diffusion Model 有两大问题:
    1. 没办法控制生成内容,只能确保生成的内容和训练数据集风格比较类似
    2. 在像素尺度上做去噪,计算量大,导致只能生成较小分辨率的图,且很慢
  • LDM 解决了上述的两个问题:
    1. 通过编码器将 文本 / mask / bbox 等条件信息转成 conditioning embedding,再通过 cross attention 机制将条件信息和 latent space 中的噪声结合起来做去噪,让条件信息可引导图片生成
    2. 通过 VAE 将图片压缩到 latent space 中,再进行去噪,计算量小,速度快

Algorithm

总体流程

LDM.png

  1. 生成隐空间下的随机噪声
  2. 将条件信息通过各自类型的编码器编码成 conditioning embedding,例如文本编码使用 CLIP text encoder
  3. latent noiseconditioning embeddingtimestep embedding 输入到 UNet 中进行多轮迭代去噪(50 step)
  4. 去噪后的 latent 通过 VAE decoder 解码成图片

Conditioning UNet

  • 通过 交叉注意力机制文本条件时序 通过 UNet 嵌入到 Noised Latent
  • 具体来说:TimeEmbedding 直接和图像特征相加,TextEmbedding 和图像特征做 CrossAttention,(TextEmbedding 作为 KV,图像特征作为 Q)
  • Conditioning UNet 示意图,两次下采样 + 中间块 + 两次上采样
graph TD
    Input[Noised Latent: 32x32x4] --> DownBlock1[CrossAttnDownBlock2D]
    DownBlock1 --> DownBlock2[CrossAttnDownBlock2D]
    DownBlock2 --> MidBlock[UNetMidBlock2DCrossAttn]
    MidBlock --> UpBlock1[CrossAttnUpBlock2D]
    UpBlock1 --> UpBlock2[CrossAttnUpBlock2D]
    UpBlock2 --> Output[Denoised Latent: 32x32x4]
  
    TextEncoder[Text Encoder] -->|Text Embedding| DownBlock1
    TextEncoder -->|Text Embedding| DownBlock2
    TextEncoder -->|Text Embedding| MidBlock
    TextEncoder -->|Text Embedding| UpBlock1
    TextEncoder -->|Text Embedding| UpBlock2
  
    Time[Timestep] -->|Time Embedding| DownBlock1
    Time -->|Time Embedding| DownBlock2
    Time -->|Time Embedding| MidBlock
    Time -->|Time Embedding| UpBlock1
    Time -->|Time Embedding| UpBlock2
  • CrossAttnBlock2D 结构示意
graph TD
    %% 输入节点
    Input[输入特征图 h_in] --> ResNet
    TimeEmb[时间嵌入 t_emb] --> MLP
    TextEmb[文本条件 y_text] --> ProjText
  
    %% 主干计算路径
    ResNet[ResNet块] --> Add
    MLP[MLP时间投影] --> Add
    Add[逐元素相加] --> GroupNorm
    GroupNorm[GroupNorm] --> Conv1
    Conv1[Conv2D 1x1] --> CrossAttn
  
    %% 交叉注意力分支
    ProjText[文本投影 W_k/W_v] --> CrossAttn
    Conv2[Conv2D 1x1] --> Merge
    CrossAttn[交叉注意力层] --> Merge
  
    %% 残差连接
    Input --> Conv2
    Merge[特征合并] --> LayerNorm
    LayerNorm[LayerNorm] --> Output[输出特征图 h_out]
  • DecoderAttentionBlock2D 结构示意
graph TD
    X[("Input x
Shape: 1,512,32,32")] --> Norm["Normalize (GroupNorm)
Output: 1,512,32,32"] Norm --> Q["Q Conv2d(1x1)
Output: 1,512,32,32"] Norm --> K["K Conv2d(1x1)
Output: 1,512,32,32"] Norm --> V["V Conv2d(1x1)
Output: 1,512,32,32"] Q --> ReshapeQ["Reshape & Permute
1,512,32,32 → 1,1024,512"] K --> ReshapeK["Reshape
1,512,32,32 → 1,512,1024"] ReshapeQ --> MatmulQK["Matmul(Q,K)
1,1024,512 × 1,512,1024 → 1,1024,1024"] ReshapeK --> MatmulQK MatmulQK --> Scale["Scale (×1/√512)
1,1024,1024"] Scale --> Softmax["Softmax
1,1024,1024"] V --> ReshapeV["Reshape
1,512,32,32 → 1,512,1024"] Softmax --> PermuteSoftmax["Permute
1,1024,1024 → 1,1024,1024"] ReshapeV --> MatmulVW["Matmul(V, Softmax)
1,512,1024 × 1,1024,1024 → 1,512,1024"] PermuteSoftmax --> MatmulVW MatmulVW --> ReshapeOut["Reshape
1,512,1024 → 1,512,32,32"] ReshapeOut --> ProjOut["Proj_out Conv2d(1x1)
1,512,32,32"] ProjOut --> Add["Add (x + h_)
1,512,32,32"] X --> Add Add --> Output[("Final Output
1,512,32,32")]
  • 下面附上 Conditioning UNet block 的实现代码,可以看出非常优雅:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
import torch
import torch.nn as nn
import torch.nn.functional as F


class Attention(nn.Module):
def __init__(self, in_dim, context_dim=None):
super().__init__()
self.to_q = nn.Linear(in_dim, in_dim, bias=False)
self.to_k = nn.Linear(
context_dim if context_dim else in_dim, in_dim, bias=False
)
self.to_v = nn.Linear(
context_dim if context_dim else in_dim, in_dim, bias=False
)
self.to_out = nn.Sequential(nn.Linear(in_dim, in_dim), nn.Dropout(0.0))

def forward(self, x, context=None):
q = self.to_q(x)
k = self.to_k(context if context is not None else x)
v = self.to_v(context if context is not None else x)

attn = torch.einsum("b i d, b j d -> b i j", q, k) * (x.shape[-1] ** -0.5)
attn = F.softmax(attn, dim=-1)
out = torch.einsum("b i j, b j d -> b i d", attn, v)
return self.to_out(out)


class GEGLU(nn.Module):
def __init__(self, in_dim, hidden_dim):
super().__init__()
self.proj = nn.Linear(in_dim, hidden_dim * 2)

def forward(self, x):
x_proj = self.proj(x)
x1, x2 = x_proj.chunk(2, dim=-1)
return x1 * F.gelu(x2)


class FeedForward(nn.Module):
def __init__(self, in_dim, hidden_dim):
super().__init__()
self.net = nn.Sequential(
GEGLU(in_dim, hidden_dim), nn.Dropout(0.0), nn.Linear(hidden_dim, in_dim)
)

def forward(self, x):
return self.net(x)


class BasicTransformerBlock(nn.Module):
def __init__(self, dim):
super().__init__()
self.norm1 = nn.LayerNorm(dim, eps=1e-5)
self.attn1 = Attention(dim)
self.norm2 = nn.LayerNorm(dim, eps=1e-5)
self.attn2 = Attention(dim, context_dim=768)
self.norm3 = nn.LayerNorm(dim, eps=1e-5)
self.ff = FeedForward(dim, 1280)

def forward(self, x, context=None):
# Self attention
x = self.attn1(self.norm1(x)) + x
# Cross attention
x = self.attn2(self.norm2(x), context=context) + x
# Feed forward
x = self.ff(self.norm3(x)) + x
return x


class Transformer2DModel(nn.Module):
def __init__(self, in_channels):
super().__init__()
self.norm = nn.GroupNorm(32, in_channels, eps=1e-6, affine=True)
self.proj_in = nn.Conv2d(in_channels, in_channels, kernel_size=1)
self.transformer_blocks = nn.ModuleList([BasicTransformerBlock(in_channels)])
self.proj_out = nn.Conv2d(in_channels, in_channels, kernel_size=1)

def forward(self, x, context=None):
b, c, h, w = x.shape
x_in = x
x = self.norm(x)
x = self.proj_in(x)
x = x.permute(0, 2, 3, 1).reshape(b, h * w, c)

for block in self.transformer_blocks:
x = block(x, context)

x = x.reshape(b, h, w, c).permute(0, 3, 1, 2)
x = self.proj_out(x)
return x + x_in


class ResnetBlock2D(nn.Module):
def __init__(self, in_channels):
super().__init__()
self.norm1 = nn.GroupNorm(32, in_channels, eps=1e-5, affine=True)
self.conv1 = nn.Conv2d(in_channels, in_channels, kernel_size=3, padding=1)
self.time_emb_proj = nn.Linear(1280, in_channels)
self.norm2 = nn.GroupNorm(32, in_channels, eps=1e-5, affine=True)
self.dropout = nn.Dropout(0.0)
self.conv2 = nn.Conv2d(in_channels, in_channels, kernel_size=3, padding=1)
self.nonlinearity = nn.SiLU()

def forward(self, x, time_emb=None):
h = x
h = self.norm1(h)
h = self.nonlinearity(h)
h = self.conv1(h)

if time_emb is not None:
time_emb = self.nonlinearity(time_emb)
time_emb = self.time_emb_proj(time_emb)[:, :, None, None]
h = h + time_emb

h = self.norm2(h)
h = self.nonlinearity(h)
h = self.dropout(h)
h = self.conv2(h)
return h + x


class Downsample2D(nn.Module):
def __init__(self, channels):
super().__init__()
self.conv = nn.Conv2d(channels, channels, kernel_size=3, stride=2, padding=1)

def forward(self, x):
return self.conv(x)


class CrossAttnDownBlock2D(nn.Module):
def __init__(self, in_channels=320):
super().__init__()
self.attentions = nn.ModuleList(
[Transformer2DModel(in_channels) for _ in range(2)]
)
self.resnets = nn.ModuleList([ResnetBlock2D(in_channels) for _ in range(2)])
self.downsamplers = nn.ModuleList([Downsample2D(in_channels)])

def forward(self, x, context=None, time_emb=None):
for attn, resnet in zip(self.attentions, self.resnets):
x = attn(x, context)
x = resnet(x, time_emb)

for downsampler in self.downsamplers:
x = downsampler(x)

return x


# 测试代码
if __name__ == "__main__":
block = CrossAttnDownBlock2D(in_channels=320)
x = torch.randn(1, 320, 64, 64)
context = torch.randn(1, 77, 768) # 文本条件,77 个 token 组成的文本序列经过 CLIP 编码成向量
time_emb = torch.randn(1, 1280) # 时间嵌入,一个时间步(例如:961)变成一个 enbedding

output = block(x, context, time_emb)
print(f"输入形状: {x.shape} -> 输出形状: {output.shape}")
# 预期输出: torch.Size([1, 320, 32, 32])

Thoughts

  • 论文思路无比清晰,且说服力很强,把很多领域的知识结合起来,真正把图像生成在实用性方面推到了一个新的高度

URL

TL;DR

  • 这篇论文提出了去噪扩散隐式模型 Denoising Diffusion Implicit Models (DDIM) 模型,可以看作是对 Denoising Diffusion Probabilistic Models (DDPM) 模型的改进。
  • DDPM 的采样过程是一个 Markov 过程,Markov 过程只有知道 t 时刻的状态才能计算第 t-1 时刻,而 DDIM 的采样过程是一个非 Markov 过程。
  • DDIM 的优势是:
    1. 去噪速度更快。可以在采样(去噪)时使用更少的时间步数,从而加快采样速度,并且用 DDPM 训练的模型可以直接用于 DDIM 的采样,二者可以无缝衔接。
    2. 确定性。在 DDIM 中,给定一个模型和一个噪声图像和时间步数,可以确定性地生成一个图像(运行再多次也是同一个图)。在 DDPM 中,给定一个模型和一个噪声图像和时间步数,生成的图像是随机的(每次跑都不一样)。

Algorithm

  • DDIM 的公式是从 DDPM 公式通过复杂推导得到的,推导过程比较复杂,这里不做详细介绍,重点讲二者逆向过程(去噪)公式区别和使用上的区别。

DDPM 逆向过程公式

xt1=1αt(xtβt1αˉtϵθ(xt,t))x_{t-1}=\frac{1}{\sqrt{\alpha_t}}(x_t-\frac{\beta_t}{\sqrt{1-\bar\alpha_t}}\cdot \epsilon_\theta(x_t,t))

  • 其中:
    • ϵθ(xt,t)\epsilon_\theta(x_t,t) 是模型预测的噪声,即 model(x_t, t)
    • αt=1βt\alpha_t=1-\beta_t
    • αˉt=s=1tαs\bar\alpha_t=\prod_{s=1}^t\alpha_s
    • t 取值是 999, 998,..., 1, 0,即从 T-10 逐步去噪

DDIM 逆向过程公式

xt1=αˉt1(xtαˉt1ϵθ(xt,t)αˉt)+1αˉt1ϵθ(xt,t)x_{t-1}=\sqrt {\bar\alpha_t-1}(\frac{x_t-\sqrt {\bar\alpha_t-1}\cdot\epsilon_\theta(x_t,t)}{\sqrt {\bar\alpha_t}})+\sqrt{1-\bar\alpha_{t-1}}\cdot\epsilon_\theta(x_t,t)

  • 其中:
    • 大多数符号含义都和 DDPM 一样
    • 只有 t 取值是 980, 960,..., 20, 0 这种非连续的取值

DDIM 公式拆解:

1. 预测原输入

predx0=xtαˉt1ϵθ(xt,t)αˉtpred_{x0}=\frac{x_t-\sqrt {\bar\alpha_t-1}\cdot\epsilon_\theta(x_t,t)}{\sqrt{\bar\alpha_t}}

  • 通过当前噪声隐变量 xtx_t 和模型预测的噪声 ϵθ(xt,t)\epsilon_\theta(x_t,t) 估计原始输入 x0x_0,即去噪

2. 计算调整方向

direction_point=1αˉt1ϵθ(xt,t)direction\_point=\sqrt{1-\bar\alpha_{t-1}}\cdot\epsilon_\theta(x_t,t)

  • 根据噪声预测结果,计算从当前时间步 t 到前一个时间步 t-1 的调整方向,这一方向结合了噪声预测和噪声调度参数,用于引导隐变量的更新。

3. 更新隐变量

xt1=αˉt1predx0+direction_pointx_{t-1}=\sqrt {\bar\alpha_t-1}\cdot pred_{x0}+direction\_point

  • 将预测的原始输入 predx0pred_{x0} 与调整方向结合,生成前一时刻的隐变量 xt1x_{t-1}。此步骤通过线性组合逐步去噪,最终逼近目标数据 x0x_0

DDPM 和 DDIM 使用上的区别

  • DDPM 去噪过程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
model = UNet2DModel.from_pretrained("google/ddpm-celebahq-256").to(device)
scheduler = DDPMScheduler.from_pretrained("google/ddpm-celebahq-256")
# Get precalculated alphas and alpha bars from the scheduler
alphas = scheduler.alphas
alphas_cumprod = scheduler.alphas_cumprod
# Initialize sample with static random noise
sample = torch.load("random_noise.pt").to(device)
# DDPM denoising loop
# scheduler.timesteps = [999, 998, ..., 1, 0]
for t in tqdm.tqdm(scheduler.timesteps):
with torch.no_grad():
# Model prediction (noise residual)
residual = model(sample, t).sample
# DDPM denoising formula
sample = (
sample - (1 - alphas[t]) / torch.sqrt(1 - alphas_cumprod[t]) * residual
) / torch.sqrt(alphas[t])
# Add random noise only for t > 1
if t > 1:
noise = torch.randn_like(sample).to(device)
sample += torch.sqrt(1 - alphas[t]) * noise
  • DDIM 去噪过程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
model = UNet2DModel.from_pretrained("google/ddpm-celebahq-256").to(device)
scheduler = DDIMScheduler.from_pretrained("google/ddpm-celebahq-256")
# set inference steps
scheduler.set_timesteps(num_inference_steps=50)
# Initialize sample with static random noise
sample = torch.load("random_noise.pt").to(device)
# DDIM denoising loop
# scheduler.timesteps = [980, 960,..., 20, 0]
for i, t in enumerate(tqdm.tqdm(scheduler.timesteps)):
# 将时间步转换为LongTensor并确保在正确设备上
t = t.to(device).long()
# 获取当前和上一步的alpha累积乘积
alpha_cumprod_t = scheduler.alphas_cumprod[t]
alpha_cumprod_prev = (
scheduler.alphas_cumprod[scheduler.timesteps[i + 1]]
if i + 1 < len(scheduler.timesteps)
else torch.tensor(1.0)
)
# 将alpha值转换到相同设备
alpha_cumprod_t = alpha_cumprod_t.to(device)
alpha_cumprod_prev = alpha_cumprod_prev.to(device)
with torch.no_grad():
# 1. 预测噪声残差
residual = model(sample, t).sample
# 2. 计算预测的原始图像x0(去噪后的图像)
pred_x0 = (sample - torch.sqrt(1.0 - alpha_cumprod_t) * residual) / torch.sqrt(
alpha_cumprod_t
)
# 3. 计算下一步的样本方向
direction_xt = torch.sqrt(1.0 - alpha_cumprod_prev) * residual
# 4. 组合得到新的样本
sample = torch.sqrt(alpha_cumprod_prev) * pred_x0 + direction_xt

二者对比分析

  1. DDIM 只需要 50 次迭代就能生成高质量的图像,而 DDPM 需要 1000 次迭代。
  2. 生成的图像质量相似,DDIM 生成的图像质量略高。
  3. 上面的代码中加载的噪声图是静态的,DDIM 跑多次生成的图像是一样的,而 DDPM 跑多次生成的图像是不一样的。
  4. 二者去噪结果对比,左侧是 DDIM,右侧是 DDPM
    concat.png

Thoughts

  • DDIM 解决了 DDPM 的两大痛点,算是一个很好的改进。
  • 为后续的 LDM 等模型打下了基础。