多模态大模型学习笔记(十三)——transformer学习之位置编码

多模态大模型学习笔记(十三)------Transformer学习之位置编码

在Transformer架构中,自注意力机制本身是"无序"的------它只关注Token之间的语义关联,无法感知Token在序列中的先后顺序。而位置编码(Positional Encoding, PE)正是为了弥补这一缺陷,将位置信息注入到模型中,让Transformer能像人类一样理解"先有因后有果"的序列逻辑。

这篇笔记将从绝对位置编码、相对位置编码、旋转位置编码(RoPE) 三类核心方案入手,由浅入深地讲解其设计思想、数学原理,并补充可直接运行的经典代码实现,彻底吃透位置编码的演进脉络。


1. 为什么Transformer需要位置编码?

1.1 自注意力的"无序"本质

自注意力机制的核心是计算Token之间的注意力分数:
Attention(Q,K,V)=softmax(QKTd)V \text{Attention}(Q,K,V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d}}\right)V Attention(Q,K,V)=softmax(d QKT)V

这个公式只依赖于Query和Key的向量点积,与Token在序列中的位置无关。比如句子"我爱中国"和"中国爱我",在自注意力机制下的计算结果完全一致,但语义却截然不同------这显然不符合人类对语言的理解方式。

1.2 位置编码的核心作用

位置编码的核心作用,就是给每个Token注入位置信息,让模型能区分Token的先后顺序,从而理解序列的语义结构。它是Transformer能处理文本、图像、音频等序列数据的关键基础。


2. 绝对位置编码:最经典的"位置注入"方案

绝对位置编码是Transformer原生采用的方案,核心思想是:给每个位置生成一个固定的位置向量,直接与Token Embedding相加,让位置信息融入到Token的语义表示中。

2.1 核心公式与实现

对于序列中第 pospospos 个位置(pos∈0,seq_len−1pos \in 0, \\text{seq\\_len}-1pos∈0,seq_len−1)、向量的第 2i2i2i 和 2i+12i+12i+1 维,位置编码的计算公式为:
PEpos,2i=sin⁡(pos100002i/dmodel) PE_{pos, 2i} = \sin\left( \frac{pos}{10000^{2i / d_{\text{model}}}} \right) PEpos,2i=sin(100002i/dmodelpos)
PEpos,2i+1=cos⁡(pos100002i/dmodel) PE_{pos, 2i+1} = \cos\left( \frac{pos}{10000^{2i / d_{\text{model}}}} \right) PEpos,2i+1=cos(100002i/dmodelpos)

其中:

  • dmodeld_{\text{model}}dmodel 是Transformer的隐藏层维度(如BERT-base的768维);
  • iii 是维度索引,范围为 0,dmodel/2−10, d_{\\text{model}}/2 - 10,dmodel/2−1

最终,Transformer的输入嵌入为:
xt=et+pt x_t = e_t + p_t xt=et+pt

  • ete_tet:第 ttt 个Token的Token Embedding;
  • ptp_tpt:第 ttt 个位置的位置编码向量。

2.2 经典代码实现(PyTorch版)

python 复制代码
import torch
import math

class AbsolutePositionalEncoding(torch.nn.Module):
    def __init__(self, d_model: int, max_len: int = 512):
        super().__init__()
        self.d_model = d_model
        
        # 初始化位置编码矩阵 [max_len, d_model]
        pe = torch.zeros(max_len, d_model)
        # 生成位置索引 [max_len, 1]
        position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
        # 计算分母项,避免重复计算
        div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
        
        # 偶数维度用sin,奇数维度用cos
        pe[:, 0::2] = torch.sin(position * div_term)
        pe[:, 1::2] = torch.cos(position * div_term)
        
        # 注册为缓冲区(不参与训练)
        self.register_buffer('pe', pe.unsqueeze(0))  # [1, max_len, d_model]

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        参数:
            x: 输入Token Embedding,形状 [batch_size, seq_len, d_model]
        返回:
            注入位置信息后的Embedding,形状 [batch_size, seq_len, d_model]
        """
        # 只取与输入序列长度匹配的位置编码
        x = x + self.pe[:, :x.size(1), :]
        return x

# 测试代码
if __name__ == "__main__":
    # 初始化绝对位置编码层(768维,最大序列长度512)
    pe_layer = AbsolutePositionalEncoding(d_model=768, max_len=512)
    # 模拟输入:batch_size=2,seq_len=10,d_model=768
    token_emb = torch.randn(2, 10, 768)
    # 注入位置编码
    output = pe_layer(token_emb)
    print(f"输入形状: {token_emb.shape}")
    print(f"输出形状: {output.shape}")
    print(f"位置编码矩阵形状: {pe_layer.pe.shape}")

2.3 设计思想与优势

  1. 三角函数的周期性 :通过正弦和余弦函数的周期性,让模型能学习到Token之间的相对位置关系。比如位置 pospospos 和 pos+kpos+kpos+k 的位置编码,可以通过三角函数的和角公式关联起来,让模型感知"间隔k个位置"的相对关系。
  2. 可外推性:位置编码的计算不依赖于训练时的序列长度,因此模型可以处理训练中未见过的更长序列(比如训练时最大长度512,推理时可处理长度1024的文本)。
  3. 实现简单:直接与Token Embedding相加,无需修改自注意力机制的核心逻辑,兼容性极强。

2.4 核心局限

  1. 绝对位置的限制:原生绝对位置编码只编码了"绝对位置",对"相对位置"的建模能力较弱,在长文本场景下,位置信息的区分度会逐渐衰减。
  2. 无法适配跨模态场景:在多模态大模型中,文本、图像、音频的序列长度差异极大,绝对位置编码的外推能力会受到限制。

3. 相对位置编码:关注"间隔"而非"坐标"

相对位置编码的核心思想是:不再编码Token的绝对位置,而是编码Token之间的相对距离,让注意力分数直接依赖于Token的相对位置,更贴合人类对序列的理解方式。

3.1 核心公式与实现

在自注意力机制的注意力分数计算中,引入相对位置偏置项 b(i−j)b(i-j)b(i−j):
score(i,j)=qi⋅kjd+b(i−j) \text{score}_{(i,j)} = \frac{q_i \cdot k_j}{\sqrt{d}} + b(i-j) score(i,j)=d qi⋅kj+b(i−j)

其中:

  • qiq_iqi:第 iii 个Token的Query向量;
  • kjk_jkj:第 jjj 个Token的Key向量;
  • b(i−j)b(i-j)b(i−j):相对位置偏置项,代表第 iii 个Token和第 jjj 个Token之间的相对位置信息,可学习或固定。

3.2 经典代码实现(PyTorch版)

python 复制代码
import torch
import torch.nn as nn
import math

class RelativePositionBias(nn.Module):
    def __init__(self, num_heads: int, max_rel_dist: int = 128):
        super().__init__()
        self.num_heads = num_heads
        self.max_rel_dist = max_rel_dist
        
        # 可学习的相对位置偏置矩阵 [num_heads, 2*max_rel_dist + 1]
        self.rel_pos_bias = nn.Embedding(2 * max_rel_dist + 1, num_heads)
        
        # 预计算相对位置索引
        self.register_buffer("rel_pos_indices", self._compute_rel_pos_indices())

    def _compute_rel_pos_indices(self):
        """计算相对位置索引,范围 [-max_rel_dist, max_rel_dist]"""
        # 生成位置范围
        coords = torch.arange(self.max_rel_dist)
        # 计算相对位置矩阵 [max_rel_dist, max_rel_dist]
        rel_pos = coords[:, None] - coords[None, :]
        # 映射到非负索引(偏移max_rel_dist)
        rel_pos += self.max_rel_dist
        # 限制范围,避免越界
        rel_pos = torch.clamp(rel_pos, 0, 2 * self.max_rel_dist)
        return rel_pos

    def forward(self, seq_len: int) -> torch.Tensor:
        """
        参数:
            seq_len: 输入序列长度
        返回:
            相对位置偏置,形状 [num_heads, seq_len, seq_len]
        """
        # 截取与当前序列长度匹配的索引
        rel_pos_indices = self.rel_pos_indices[:seq_len, :seq_len]
        # 获取偏置值 [seq_len, seq_len, num_heads]
        rel_bias = self.rel_pos_bias(rel_pos_indices)
        # 调整维度 [num_heads, seq_len, seq_len]
        rel_bias = rel_bias.permute(2, 0, 1)
        return rel_bias

# 带相对位置编码的自注意力层
class RelativeAttention(nn.Module):
    def __init__(self, d_model: int, num_heads: int, max_rel_dist: int = 128):
        super().__init__()
        self.d_model = d_model
        self.num_heads = num_heads
        self.head_dim = d_model // num_heads
        
        # 线性投影层
        self.q_proj = nn.Linear(d_model, d_model)
        self.k_proj = nn.Linear(d_model, d_model)
        self.v_proj = nn.Linear(d_model, d_model)
        self.out_proj = nn.Linear(d_model, d_model)
        
        # 相对位置偏置层
        self.rel_pos_bias = RelativePositionBias(num_heads, max_rel_dist)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        参数:
            x: 输入张量,形状 [batch_size, seq_len, d_model]
        返回:
            注意力输出,形状 [batch_size, seq_len, d_model]
        """
        batch_size, seq_len, _ = x.shape
        
        # 投影到Q/K/V [batch_size, seq_len, d_model]
        q = self.q_proj(x)
        k = self.k_proj(x)
        v = self.v_proj(x)
        
        # 分拆多头 [batch_size, num_heads, seq_len, head_dim]
        q = q.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
        k = k.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
        v = v.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
        
        # 计算注意力分数 [batch_size, num_heads, seq_len, seq_len]
        attn_scores = (q @ k.transpose(-2, -1)) / math.sqrt(self.head_dim)
        
        # 添加相对位置偏置
        rel_bias = self.rel_pos_bias(seq_len)  # [num_heads, seq_len, seq_len]
        attn_scores += rel_bias.unsqueeze(0)  # 广播到batch维度
        
        # 计算注意力权重
        attn_weights = torch.softmax(attn_scores, dim=-1)
        
        # 计算注意力输出 [batch_size, num_heads, seq_len, head_dim]
        attn_output = attn_weights @ v
        
        # 拼接多头 [batch_size, seq_len, d_model]
        attn_output = attn_output.transpose(1, 2).contiguous().view(batch_size, seq_len, self.d_model)
        
        # 最终投影
        output = self.out_proj(attn_output)
        return output

# 测试代码
if __name__ == "__main__":
    # 初始化相对位置注意力层
    rel_attn = RelativeAttention(d_model=768, num_heads=12, max_rel_dist=128)
    # 模拟输入:batch_size=2,seq_len=10,d_model=768
    x = torch.randn(2, 10, 768)
    # 前向传播
    output = rel_attn(x)
    print(f"输入形状: {x.shape}")
    print(f"输出形状: {output.shape}")

3.3 设计思想与优势

  1. 聚焦相对关系:直接建模Token之间的"间隔",而非"坐标",更符合语言中"语序决定语义"的本质。比如"我爱中国"中,"我"和"中国"的相对位置是"前-后",而"中国爱我"中是"后-前",模型能通过相对位置偏置清晰区分。
  2. 长文本适配性更强:相对位置编码对长文本的位置信息建模更稳定,不会像绝对位置编码那样随着序列长度增加而衰减。
  3. 可学习性:相对位置偏置项可以通过训练学习,适配不同任务的位置依赖关系(比如翻译任务中,源语言和目标语言的相对位置关系不同)。

3.4 核心局限

  1. 实现复杂度高:需要修改自注意力机制的核心计算逻辑,兼容性不如绝对位置编码。
  2. 外推能力受限:如果相对位置偏置项的最大间隔是固定的(比如最大支持间隔512),当处理更长序列时,超出间隔的位置信息无法编码,外推能力受限。

4. 旋转位置编码(RoPE):兼顾绝对与相对的"最优解"

旋转位置编码(Rotary Positional Embedding, RoPE)是目前大模型(如GPT-3、LLaMA、Qwen)的主流方案,核心思想是:通过旋转矩阵给Query和Key向量注入位置信息,让注意力分数天然包含相对位置关系,同时保留绝对位置的感知能力,兼顾了绝对位置编码的外推性和相对位置编码的精准性。

4.1 核心公式与实现

对于第 ttt 个位置的Query向量 qtq_tqt 和Key向量 ktk_tkt,将其按维度两两分组(第 2r2r2r 和 2r+12r+12r+1 维),通过旋转矩阵进行变换:

q2rq2r+1\]↦\[q2rcos⁡θt−q2r+1sin⁡θtq2rsin⁡θt+q2r+1cos⁡θt\] \\begin{bmatrix} q_{2r} \\\\ q_{2r+1} \\end{bmatrix} \\mapsto \\begin{bmatrix} q_{2r}\\cos\\theta_t - q_{2r+1}\\sin\\theta_t \\\\ q_{2r}\\sin\\theta_t + q_{2r+1}\\cos\\theta_t \\end{bmatrix} \[q2rq2r+1\]↦\[q2rcosθt−q2r+1sinθtq2rsinθt+q2r+1cosθt

k2rk2r+1\]↦\[k2rcos⁡θt−k2r+1sin⁡θtk2rsin⁡θt+k2r+1cos⁡θt\] \\begin{bmatrix} k_{2r} \\\\ k_{2r+1} \\end{bmatrix} \\mapsto \\begin{bmatrix} k_{2r}\\cos\\theta_t - k_{2r+1}\\sin\\theta_t \\\\ k_{2r}\\sin\\theta_t + k_{2r+1}\\cos\\theta_t \\end{bmatrix} \[k2rk2r+1\]↦\[k2rcosθt−k2r+1sinθtk2rsinθt+k2r+1cosθt

其中,旋转角度 θt\theta_tθt 定义为:
θt=t⋅θ0 \theta_t = t \cdot \theta_0 θt=t⋅θ0
θ0=10000−2r/dmodel \theta_0 = 10000^{-2r/d_{\text{model}}} θ0=10000−2r/dmodel

变换后的Query和Key向量点积,天然包含了相对位置信息:
qiTkj=q~iTk~j⋅cos⁡((i−j)θ0)+其他项 q_i^T k_j = \tilde{q}_i^T \tilde{k}_j \cdot \cos((i-j)\theta_0) + \text{其他项} qiTkj=q~iTk~j⋅cos((i−j)θ0)+其他项

其中 q~i\tilde{q}_iq~i 和 k~j\tilde{k}_jk~j 是未旋转的向量,点积结果与相对位置 (i−j)(i-j)(i−j) 直接相关,让注意力分数自然融入了位置依赖。

4.2 经典代码实现(PyTorch版)

python 复制代码
import torch
import torch.nn as nn
import math

class RotaryPositionalEncoding(nn.Module):
    def __init__(self, d_model: int, max_len: int = 2048):
        super().__init__()
        self.d_model = d_model
        self.max_len = max_len
        
        # 预计算旋转角度 [d_model//2]
        theta = 1.0 / (10000.0 ** (torch.arange(0, d_model, 2).float() / d_model))
        self.register_buffer("theta", theta)
        
        # 预计算位置索引 [max_len]
        positions = torch.arange(0, max_len, dtype=torch.float)
        self.register_buffer("positions", positions)
        
        # 预计算旋转矩阵的cos和sin值 [max_len, d_model//2]
        self._compute_rotary_matrices()

    def _compute_rotary_matrices(self):
        """预计算cos和sin矩阵"""
        # 计算角度 [max_len, d_model//2]
        angles = self.positions.unsqueeze(1) * self.theta.unsqueeze(0)
        # 计算cos和sin值
        cos_angles = torch.cos(angles)
        sin_angles = torch.sin(angles)
        
        self.register_buffer("cos_angles", cos_angles)
        self.register_buffer("sin_angles", sin_angles)

    def rotate_half(self, x: torch.Tensor) -> torch.Tensor:
        """将向量按维度两两分组,后半部分翻转"""
        # x: [batch_size, seq_len, num_heads, head_dim]
        x1 = x[..., :x.shape[-1]//2]
        x2 = x[..., x.shape[-1]//2:]
        return torch.cat([-x2, x1], dim=-1)

    def forward(self, x: torch.Tensor, seq_len: int) -> torch.Tensor:
        """
        对输入向量应用旋转位置编码
        参数:
            x: 输入向量(Q/K),形状 [batch_size, seq_len, num_heads, head_dim]
            seq_len: 当前序列长度
        返回:
            旋转后的向量,形状 [batch_size, seq_len, num_heads, head_dim]
        """
        # 获取当前序列长度对应的cos和sin值 [seq_len, head_dim//2]
        cos = self.cos_angles[:seq_len, :].unsqueeze(0).unsqueeze(2)
        sin = self.sin_angles[:seq_len, :].unsqueeze(0).unsqueeze(2)
        
        # 扩展维度以匹配head_dim
        cos = cos.repeat(1, 1, 1, 2)  # [1, seq_len, 1, head_dim]
        sin = sin.repeat(1, 1, 1, 2)  # [1, seq_len, 1, head_dim]
        
        # 应用旋转
        x_rotated = x * cos + self.rotate_half(x) * sin
        return x_rotated

# 带RoPE的自注意力层
class RoPEAttention(nn.Module):
    def __init__(self, d_model: int, num_heads: int, max_len: int = 2048):
        super().__init__()
        self.d_model = d_model
        self.num_heads = num_heads
        self.head_dim = d_model // num_heads
        
        # 线性投影层
        self.q_proj = nn.Linear(d_model, d_model)
        self.k_proj = nn.Linear(d_model, d_model)
        self.v_proj = nn.Linear(d_model, d_model)
        self.out_proj = nn.Linear(d_model, d_model)
        
        # RoPE层
        self.rope = RotaryPositionalEncoding(self.head_dim, max_len)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        参数:
            x: 输入张量,形状 [batch_size, seq_len, d_model]
        返回:
            注意力输出,形状 [batch_size, seq_len, d_model]
        """
        batch_size, seq_len, _ = x.shape
        
        # 投影到Q/K/V [batch_size, seq_len, d_model]
        q = self.q_proj(x)
        k = self.k_proj(x)
        v = self.v_proj(x)
        
        # 分拆多头 [batch_size, seq_len, num_heads, head_dim]
        q = q.view(batch_size, seq_len, self.num_heads, self.head_dim)
        k = k.view(batch_size, seq_len, self.num_heads, self.head_dim)
        v = v.view(batch_size, seq_len, self.num_heads, self.head_dim)
        
        # 应用RoPE旋转
        q_rot = self.rope(q, seq_len)
        k_rot = self.rope(k, seq_len)
        
        # 调整维度用于计算 [batch_size, num_heads, seq_len, head_dim]
        q_rot = q_rot.transpose(1, 2)
        k_rot = k_rot.transpose(1, 2)
        v = v.transpose(1, 2)
        
        # 计算注意力分数 [batch_size, num_heads, seq_len, seq_len]
        attn_scores = (q_rot @ k_rot.transpose(-2, -1)) / math.sqrt(self.head_dim)
        
        # 计算注意力权重
        attn_weights = torch.softmax(attn_scores, dim=-1)
        
        # 计算注意力输出 [batch_size, num_heads, seq_len, head_dim]
        attn_output = attn_weights @ v
        
        # 拼接多头 [batch_size, seq_len, d_model]
        attn_output = attn_output.transpose(1, 2).contiguous().view(batch_size, seq_len, self.d_model)
        
        # 最终投影
        output = self.out_proj(attn_output)
        return output

# 测试代码
if __name__ == "__main__":
    # 初始化RoPE注意力层
    rope_attn = RoPEAttention(d_model=768, num_heads=12, max_len=2048)
    # 模拟输入:batch_size=2,seq_len=10,d_model=768
    x = torch.randn(2, 10, 768)
    # 前向传播
    output = rope_attn(x)
    print(f"输入形状: {x.shape}")
    print(f"输出形状: {output.shape}")

4.3 设计思想与优势

  1. 兼顾绝对与相对:通过旋转矩阵注入绝对位置信息,同时让注意力分数天然依赖于相对位置,完美解决了传统位置编码的缺陷。
  2. 强外推性:与原生绝对位置编码一样,RoPE的计算不依赖于训练时的序列长度,可轻松外推到更长的文本序列。
  3. 工业级适配:无需修改自注意力机制的核心结构,仅对Query和Key向量做变换,兼容性极强,已成为GPT-3、LLaMA等大模型的标配。

4.4 核心局限

  1. 实现细节复杂:需要对Query和Key向量按维度分组并进行旋转操作,工程实现上比绝对位置编码更复杂。
  2. 对硬件要求高:旋转操作会增加一定的计算量,在端侧设备上部署时需要优化。

5. 位置编码在多模态大模型中的拓展

随着多模态大模型的发展,位置编码不再局限于文本序列,而是需要适配图像、音频、视频等不同模态的序列结构,核心拓展方向包括:

5.1 图像位置编码

在Vision Transformer(ViT)中,图像被切分为多个Patch(如16×16像素),每个Patch对应一个Token,位置编码的方式与文本一致:

  • 绝对位置编码:给每个Patch生成一个固定的位置向量,与Patch Embedding相加。
  • 相对位置编码:建模Patch之间的相对距离(如上下左右的空间关系),让模型理解图像的空间结构。
  • RoPE:对图像的Query和Key向量进行旋转,注入空间位置信息。

5.2 音频位置编码

在音频Transformer中,音频波形被切分为多个帧,每个帧对应一个Token,位置编码需要适配音频的时序结构:

  • 采用RoPE等强外推性的位置编码方案,适配长音频序列(如1小时以上的音频)。
  • 引入相对位置偏置,建模音频帧之间的时序依赖(如语音的韵律、节奏)。

5.3 跨模态位置编码

在多模态大模型(如GPT-4V、Qwen-VL)中,文本和图像/音频的Token被拼接成一个统一的序列,位置编码需要同时适配不同模态的位置信息:

  • 采用统一的位置编码方案(如RoPE),让模型能理解文本和图像/音频的相对位置关系(如"图中左上角的猫"对应文本中的"猫")。
  • 引入模态特有的位置偏置,区分不同模态的Token位置信息。

6. 总结与学习提示

位置编码是Transformer架构的核心基础模块,从绝对位置编码到相对位置编码,再到RoPE,其演进脉络始终围绕"如何更精准地建模序列位置信息"展开。

对于学习和落地的几点提示:

  1. 入门学习:先从原生绝对位置编码入手,理解位置信息注入的核心逻辑,再逐步深入相对位置编码和RoPE。
  2. 工业落地:在大模型场景中,优先选择RoPE方案,兼顾外推性和精准性;在跨模态场景中,需根据模态特性适配位置编码方案。
  3. 进阶优化:在长文本场景中,可通过RoPE的外推优化(如NTK-aware RoPE)提升模型对超长序列的处理能力;在多模态场景中,可引入模态特有的位置偏置提升跨模态对齐精度。
  4. 代码实践:先跑通本文提供的基础代码,再尝试将位置编码集成到完整的Transformer模型中,通过实际运行理解位置编码的作用。

关键点回顾

  1. 绝对位置编码通过三角函数生成固定位置向量,实现简单但对长文本支持差,是Transformer原生方案;
  2. 相对位置编码直接建模Token间的相对距离,语义更精准但实现复杂,外推能力受限;
  3. RoPE通过旋转矩阵注入位置信息,兼顾绝对/相对位置建模能力,是当前大模型的主流选择,本文提供的代码可直接用于工程实践。
相关推荐
IT_陈寒7 分钟前
Vue这个坑我跳了两次,原来问题出在这
前端·人工智能·后端
新新技术迷33 分钟前
Node给AI接口做SSE代理与鉴权
人工智能
redreamSo1 小时前
大模型是不是到顶了?瓶颈到底在哪
人工智能·openai
Oo9201 小时前
Tool Use 背后的技术逻辑
人工智能
姗姗来迟了1 小时前
Vue3封装AI流式对话组件踩坑实录
人工智能
码上天下2 小时前
用Pinia管理AI多会话状态
人工智能
用户054324329703 小时前
Next.js接大模型流式SSE实操踩坑
人工智能
Assby3 小时前
从 Function Calling 到 MCP:理解 Agent 工具调用的底层通信机制
人工智能·后端
小星AI3 小时前
Claude Code 从入门到精通,一步到位
人工智能
后端小肥肠3 小时前
Codex + Obsidian 做人生副本视频:输入主题文案,直通剪映草稿
人工智能·aigc·agent