手写transformer中自注意力机制,并解释每个矩阵及其运算的含义

手写transformer中自注意力机制,代码:

python 复制代码
import numpy as np

# ==========================================
# 0. 准备数据 (Input Data)
# ==========================================
# 假设我们输入了一句话,包含 3 个词,比如:"我", "爱", "你"。
# 在自然语言处理里,词不能直接算,必须先变成数字向量(这叫 Embedding)。
# 这里假设每个词被表示成了一个 4 维的向量。
x = np.array([
    [1, 0, 1, 0],  # 代表第 1 个词 "我" 的特征
    [0, 2, 0, 2],  # 代表第 2 个词 "爱" 的特征
    [1, 1, 1, 1]   # 代表第 3 个词 "你" 的特征
])
# x 的形状 (3, 4) 意味着:Sequence Length (句子长度) = 3,Embedding Dimension (词向量维度) = 4。
print(f"1. 输入矩阵 X (shape={x.shape}):\n{x}\n")


# ==========================================
# 1. 初始化权重矩阵 (Initialize Weights)
# ==========================================
# 为什么需要权重矩阵?因为原始的 4 维词向量只是"字面意思"。
# 我们需要把它们映射到一个新的空间里去寻找"查询(Q)"、"键(K)"和"值(V)"。
# 假设我们要把 4 维映射到 3 维 (这个 3 就是 head_dim,多头注意力里的"头"维度)。
input_dim = 4
head_dim = 3

np.random.seed(42)  # 固定随机种子,为了每次跑出来的随机数都一样,方便调试。

# 随机生成 W_q, W_k, W_v 这三个转换器。
# 它们的形状都是 (4, 3)。也就是接收 4 维输入,吐出 3 维输出。
# 注意:在真实的 DeepSeek 或 ChatGPT 里,这三个矩阵里的几百亿个数字,就是消耗了成千上万张显卡"炼丹"训练出来的心血!现在我们只是随机模拟。
W_q = np.random.randint(0, 2, (input_dim, head_dim))
W_k = np.random.randint(0, 2, (input_dim, head_dim))
W_v = np.random.randint(0, 2, (input_dim, head_dim))

print(f"2. 权重矩阵 Wq:\n{W_q}\n")
print(f"2. 权重矩阵 Wk:\n{W_k}\n") # 修正了你原代码这里的打印错别字
print(f"2. 权重矩阵 Wv:\n{W_v}\n")


# ==========================================
# 2. 计算 Q, K, V (Linear Projections)
# ==========================================
# 矩阵乘法 np.dot(x, W_q):
# 数学上:维度 (3, 4) × (4, 3) = 结果维度 (3, 3)。
# 业务含义:让 3 个词同时去经过 W_q 转换器。第一行出来就是"我"的查询向量,第二行就是"爱"的查询向量。
Q = np.dot(x, W_q)   # Q (Query): 我想找什么样的上下文?
K = np.dot(x, W_k)   # K (Key): 我具备什么样的特征标签?
V = np.dot(x, W_v)   # V (Value): 我实际包含的内容/语义是什么?

print(f"3. 查询向量 Q (Query):\n{Q}\n")
print(f"   索引向量 K (Key):\n{K}\n")
print(f"   内容向量 V (Value):\n{V}\n")


# ==========================================
# 3. 计算注意力分数 (Attention Scores)
# ==========================================
# 公式:Scores = Q * K^T (K的转置)
# 为什么是点积?在向量空间里,两个向量的点积越大,代表它们越相似、越匹配。
# Q 是 (3, 3),K.T 是 (3, 3)。点积结果 scores 的形状是 (3, 3)。
scores = np.dot(Q, K.T)
print(f"4. 原始分数 (Q点积K):\n{scores}\n")
# 极其关键的解释:
# 这个 3x3 的矩阵,是一张"相亲打分表"。
# scores[0][1] 代表:用第 1 个词("我")的 Q,去和第 2 个词("爱")的 K 计算点积。
# 得出的数字,就是 "我" 在阅读时,对 "爱" 这个字应该分配多少注意力。


# ==========================================
# 4. 缩放 (Scale)
# ==========================================
# 这一步是为了算法的稳定性。
# 维度 head_dim 越大,点积算出来的 scores 数值就可能极其庞大。
# 数值太大塞进下一步的 Softmax 里,会导致结果变成极端的 [1.0, 0.0, 0.0],导致"梯度消失"(也就是 AI 学不进东西了)。
# 所以除以维度的平方根 np.sqrt(d_k) 来把数值强行拉小,让打分变得平滑。
d_k = head_dim
scaled_scores = scores / np.sqrt(d_k)

print(f"scaled_scores:\n{scaled_scores}\n")


# ==========================================
# 5. 归一化 (Softmax)
# ==========================================
# Softmax 的作用:把一堆乱七八糟的打分,变成总和为 1 的概率分布。
def softmax(x):
    # np.max 减去最大值是一个工程学上的小 trick,专门为了防止 np.exp(x) 数值溢出(比如算 e 的 1000 次方内存会爆)。不影响最终概率比例。
    print(f"np.max(x, axis=-1, keepdims=True):\n{np.max(x, axis=-1, keepdims=True)}\n")
    e_x = np.exp(x - np.max(x, axis=-1, keepdims=True))
    print(f"x - np.max(x, axis=-1, keepdims=True):\n{x - np.max(x, axis=-1, keepdims=True)}\n")
    print(f"e_x:\n{e_x}\n")
    # 将每个 e^x 除以所在行的总和,得到百分比权重。
    print(f"np.sum(e_x, axis=-1, keepdims=True):\n{np.sum(e_x, axis=-1, keepdims=True)}\n")
    return e_x / np.sum(e_x, axis=-1, keepdims=True)

attention_weights = softmax(scaled_scores)
print(f"5. 注意力权重 (Softmax后):\n{np.round(attention_weights, 2)}\n")
# 解释:假设 attention_weights 第一行是 [0.2, 0.7, 0.1]。
# 意思就是:"我"这个词在更新自己含义时,决定吸收 20%的"我"本身,70%的"爱",和 10%的"你"。


# ==========================================
# 6. 加权求和 (Weighted Sum)
# ==========================================
# 公式:Output = Weights * V
# 数学上:维度 (3, 3) × (3, 3) = 结果维度 (3, 3)。
# 根据第 5 步算出来的关注度比例(Weights),去乘以每个词的实际内容(V)。
output = np.dot(attention_weights, V)

print(f"6. 最终输出 (Z):\n{output}")
# 最终解释:
# 这算出来的 output (3行3列),依然代表这 3 个词。
# 但是!第一行的"我",已经不再是第 0 步里那个孤立的"我"了。
# 它变成了一个"知晓全局",融合了后面"爱"和"你"语义的全新向量。
# 这就是 Transformer 能够理解上下文的终极奥义!

那这里面每个矩阵及其运算分别是什么含义呢?(注意下面的数字和上面代码中的数字不相同)

我们设定一个极简的场景:

假设我们输入了一句话,只有 3 个 Token:["我", "爱", "你"]

经过 Embedding 和权重矩阵映射后,我们得到了 Q,K,VQ, K, VQ,K,V 三个矩阵。假设特征维度(Hidden Size)是 3。


前置定义:矩阵的物理结构(行与列到底是什么?)

在进入乘法之前,必须死死咬住 Q,K,VQ, K, VQ,K,V 的行列定义,这是不晕车的根本:

  • 每一行(Row): 代表一个具体的 Token(字/词)。
  • 每一列(Column): 代表这个 Token 的 特征维度(Feature)。你可以通俗理解为:列 1 可能是"名词属性",列 2 是"情感属性",列 3 是"动作属性"。
text 复制代码
[Q 矩阵 (Query:我想找什么?)]        [K 矩阵 (Key:我具有什么特征?)]       [V 矩阵 (Value:我实际的内容/语义)]
          特征1  特征2  特征3                 特征1  特征2  特征3                 特征1  特征2  特征3
Token1(我) [  1 ,   0 ,   2  ]      Token1(我) [  1 ,   0 ,   2  ]      Token1(我) [ 10 ,   0 ,   0  ]
Token2(爱) [  0 ,   2 ,   0  ]      Token2(爱) [  0 ,   1 ,   0  ]      Token2(爱) [  0 ,  10 ,   0  ]
Token3(你) [  1 ,   1 ,   0  ]      Token3(你) [  1 ,   1 ,   0  ]      Token3(你) [  0 ,   0 ,  10  ]

(注:为了方便心算,我特意把 VVV 矩阵设成了 10 的倍数,后面算出来的结果你会觉得极其神清气爽)


第一步:计算注意力得分(Q⋅KTQ \cdot K^TQ⋅KT)------ "匹配度打分"

底层逻辑: 矩阵相乘(点积)的本质,是在计算两个向量的相似度 。QQQ 乘以 KTK^TKT,就是让每个 Token 的"查询需求",去和所有 Token 的"自身特征"做一一匹配。

我们把 KKK 矩阵转置成 KTK^TKT:

text 复制代码
[K的转置矩阵 (K^T)]
          Token1(我) Token2(爱) Token3(你)
特征1        1          0          1
特征2        0          1          1
特征3        2          0          0

开始矩阵乘法(手撕时刻):
Score=Q⋅KT=[102020110][101011200]=[501022112]Score = Q \cdot K^T = \begin{bmatrix} 1 & 0 & 2 \\ 0 & 2 & 0 \\ 1 & 1 & 0 \end{bmatrix} \begin{bmatrix} 1 & 0 & 1 \\ 0 & 1 & 1 \\ 2 & 0 & 0 \end{bmatrix} = \begin{bmatrix} 5 & 0 & 1 \\ 0 & 2 & 2 \\ 1 & 1 & 2 \end{bmatrix}Score=Q⋅KT= 101021200 102010110 = 501021122

【极其核心:Score 矩阵元素的物理含义!】

这个 3×33 \times 33×3 的矩阵,就是你图纸里的第一步结果,它的行列含义发生了质变

  • 行(Row): 发出 Query 的 Token。
  • 列(Column): 被观察的 Key Token。
  • 元素值: 关注度(分数越大,说明越匹配)。
text 复制代码
[Score 矩阵 (未归一化的注意力得分)]
              看Token1(我)  看Token2(爱)  看Token3(你)
Token1(我)看...      5            0            1       <-- "我"极其关注"我"自己(5),稍微关注"你"(1),完全不关注"爱"(0)
Token2(爱)看...      0            2            2       <-- "爱"平均地关注了自己(2)和"你"(2)
Token3(你)看...      1            1            2       <-- "你"最关注自己(2),也分了点注意力给"我"(1)和"爱"(1)

第二步:Softmax 归一化 ------ "分配注意力预算"

底层逻辑: Score 矩阵里的分数有大有小(比如 5、0、1),不方便统一处理。Softmax 的作用就是把每一行的分数,变成加起来等于 100% 的概率(权重)

(为了演示直观,我不套用复杂的 exe^xex 公式,直接给出概念性的百分比结果)

text 复制代码
[Attention Weights 矩阵 (A) - 算出了最终的权重占比]
              看Token1(我)  看Token2(爱)  看Token3(你)
Token1(我)的视角:   80%          0%          20%
Token2(爱)的视角:    0%         50%          50%
Token3(你)的视角:   20%         20%          60%

第三步:乘以 Value 矩阵(A⋅VA \cdot VA⋅V)------ "信息融合(输出新特征)"

底层逻辑: 既然已经知道了"我应该看谁,看多少比例",接下来就是用这个比例,去提取他们实际的内容(VVV 矩阵),把它们融合成一个全新的自己

Output=A⋅V=[0.80.00.20.00.50.50.20.20.6][100001000010]=[802055226]Output = A \cdot V = \begin{bmatrix} 0.8 & 0.0 & 0.2 \\ 0.0 & 0.5 & 0.5 \\ 0.2 & 0.2 & 0.6 \end{bmatrix} \begin{bmatrix} 10 & 0 & 0 \\ 0 & 10 & 0 \\ 0 & 0 & 10 \end{bmatrix} = \begin{bmatrix} 8 & 0 & 2 \\ 0 & 5 & 5 \\ 2 & 2 & 6 \end{bmatrix}Output=A⋅V= 0.80.00.20.00.50.20.20.50.6 100001000010 = 802052256

【终极解析:输出矩阵到底是个啥?】

现在的输出矩阵,恢复了最初 Q,K,VQ, K, VQ,K,V 的尺寸形状,但灵魂已经完全变了!

text 复制代码
[Output 矩阵 (融合了上下文的全新 Token 向量)]
               特征1  特征2  特征3
新 Token1(我) [  8 ,   0 ,   2  ]  <-- 仔细看!原本"我"的V是[10,0,0]。现在它吸收了20%的"你"[0,0,10],变成了[8,0,2]!
新 Token2(爱) [  0 ,   5 ,   5  ]  <-- 这是一个兼具了"爱"和"你"特征的全新混合体。
新 Token3(你) [  2 ,   2 ,   6  ]  <-- 吸收了全局信息的"你"。

这三步跑完,Transformer 最惊艳的魔法就实现了:
在没有引入任何额外结构的情况下,原本孤立的词汇,通过自己对自己所在句子的"扫描"和"加权融合",变成了包含整个句子上下文语境的超级词汇。 这就是"自注意力(Self-Attention)"名字的由来。

最终这个Output 矩阵又代表什么呢?怎么解释?还是说这是不可解释的?

1. 坦白局:它在物理层面上,确实是个"黑盒"

首先要承认,在深度学习里,这几百上千个特征维度(Feature Dimensions),是没有具体的人类语义标签的

我们在代码里不会,也无法定义"特征 1 代表词性"、"特征 2 代表情感"。这些数字是模型在海量数据中自己"反向传播"学出来的隐变量(Latent Variables) 。所以,从字面意思上说,单个数字 8 确实是不可直接解释的。

2. 但在业务架构上,它的含义极其清晰:叫做"上下文语境化(Contextualization)"

虽然我们不知道"特征 1"具体叫什么名字,但我们完全知道这个混合动作(加权求和)在干嘛

咱们举个最经典的例子。在没有 Transformer 之前,早期的 NLP 技术(比如 Word2Vec)用的是**"静态词典"**:

比如 "打" 这个字,在静态词典里它的特征向量可能是 [10, 0, 0](假设特征 1 代表"物理攻击性")。

  • 在"打人"里,它是物理攻击。
  • 在"打车"里,它是呼叫服务。
  • 在"打游戏"里,它是娱乐行为。
    如果用静态向量,机器根本分不清这三个"打"的区别,因为它们在内存里是同一个对象。

但引入了注意力矩阵的加权混合后,魔法出现了:

当机器看到 "打车" 这个词时,开始算注意力分数。

  • "打"这个字,可能分配了 20% 的注意力给自己,却分配了 80% 的注意力给"车"。
  • 假设"车"的特征向量是 [0, 10, 0](特征 2 代表"交通工具性")。

混合后的新"打" = 0.2×[10,0,0]+0.8×[0,10,0]=[2,8,0]0.2 \times [10, 0, 0] + 0.8 \times [0, 10, 0] = [2, 8, 0]0.2×[10,0,0]+0.8×[0,10,0]=[2,8,0]。

你看!经过注意力的混合,这个新的"打"字,它的物理攻击性(特征 1)从 10 降到了 2,而交通工具属性(特征 2)从 0 飙升到了 8。
这个混合出来的 [2, 8, 0],代表的不再是字典里的"打",而是被"车"这个字感染后的、具有特定业务场景的"打"。

总结:混合结果到底代表什么?

那个混合出来的 8,代表着**"在当前这个具体句子里,融合了全局语境后的、高度定制化的词意特征"**。

通过这种混合,Transformer 把人类语言中最难处理的**"语境依赖"**问题给暴力破解了。它不需要人工去写各种 if-else 规则(如果后面跟着"车"则视为交通动作),而是通过矩阵的互相渗透,让词与词之间发生了"化学反应"。

相关推荐
这张生成的图像能检测吗2 小时前
(论文速读)Mono3DVLT:基于单眼视频的3D视觉语言跟踪
深度学习·计算机视觉·视觉语言模型·3d目标追踪·单目视频
信鸽爱好者2 小时前
RTX5060显卡+windows CUDA12.8+cuDNN8.9.7+pytorch安装
人工智能·pytorch·windows·深度学习
deephub2 小时前
高级 RAG 技术:查询转换与查询分解
人工智能·深度学习·大语言模型·agent·rag
信鸽爱好者3 小时前
RTX5060 GPU CUDA12.8 +vscode 设计一个torch实例程序
人工智能·vscode·深度学习·编辑器
AI人工智能+3 小时前
基于深度学习的表格识别技术:通过多模态预处理、神经网络分析和高精度OCR识别,实现复杂银行流水的自动化解析
深度学习·计算机视觉·ocr·表格识别
郑泰科技3 小时前
一键脚本安装OpenClaw时遇到问题怎么办?
人工智能·深度学习·agi
放下华子我只抽RuiKe53 小时前
机器学习启航:从数据直觉到模型构建的第一块基石
人工智能·深度学习·机器学习·语言模型·数据挖掘·语音识别·聚类
集芯微电科技有限公司3 小时前
氮化镓GaN FET/GaN HEMT功率驱动器选型一览表
人工智能·单片机·嵌入式硬件·深度学习·神经网络·生成对抗网络
Tisfy3 小时前
LeetCode 1878.矩阵中最大的三个菱形和:斜向前缀和
算法·leetcode·矩阵