《GoMLX从入门到精通》第11章:卷积与注意力——两大基础架构
Chapter 11
---
11.1 全连接的困境
上一章的两层 MLP 处理 MNIST(28×28=784 维)没问题,784 个输入 → 128 个隐层神经元 → 7936 个参数。不大。
但如果你处理一张 224×224 的彩色图像:224×224×3 = 150,528 个输入。连接到 128 个神经元 = 150,528 × 128 ≈ 1927 万个参数。仅第一层。而且——你把一张 224×224 的图像展平成一维向量时,完全破坏了空间结构。相邻像素的空间关系、远处的边缘、形状的轮廓——全丢了。
卷积神经网络(CNN)就是为这个场景发明的。
---
11.2 卷积的核心思想:参数共享 + 局部感受野
卷积用一个小的滑动窗口(卷积核)扫过整张图。同一个窗口,用同一组参数,在图像的不同位置检测同一种模式。
import (
"github.com/gomlx/gomlx/pkg/ml/context"
"github.com/gomlx/gomlx/pkg/ml/layers"
)
// 输入:[batch, height, width, channels]
// 卷积:3×3 窗口,32 个输出通道
x := layers.Convolution(ctx.In("conv1"), input).
Channels(32).KernelSizePerAxis(3, 3).Strides(1).PadSame().Done()
一个 3×3 的卷积核只有 9 个权重 + 1 个偏置 = 10 个参数。无论输入图像是 32×32 还是 224×224,卷积核的参数数量不变——因为它在整张图上共享。这就是参数共享:10 个参数检测全图的一种模式,而不是每个位置各用 10 个参数。
32 个输出通道 = 32 种不同的模式检测器:一个检测水平边缘、一个检测竖直边缘、一个检测某种颜色过渡……
多层卷积的层级抽象:
- 第一层:检测边缘、颜色块
- 第二层:检测由边缘组成的纹理
- 第三层:检测由纹理组成的物体部件
- 第四层及以上:检测完整物体
11.3 一个完整的 CNN 流水线
import (
"github.com/gomlx/gomlx/pkg/ml/context"
"github.com/gomlx/gomlx/pkg/core/graph"
"github.com/gomlx/gomlx/pkg/ml/layers"
"github.com/gomlx/gomlx/pkg/ml/layers/activations"
)
func CNNModel(ctx *context.Context, input *graph.Node) *graph.Node {
// Block 1: Conv → ReLU → MaxPool
x := layers.Convolution(ctx.In("conv1"), input).
Channels(32).KernelSizePerAxis(3, 3).PadSame().Done()
x = activations.Relu(x)
x = graph.MaxPool(x).WindowPerAxis(2, 2).Strides(2).Done()
// Block 2: 更多卷积
x = layers.Convolution(ctx.In("conv2"), x).
Channels(64).KernelSizePerAxis(3, 3).PadSame().Done()
x = activations.Relu(x)
x = graph.MaxPool(x).WindowPerAxis(2, 2).Strides(2).Done()
// Block 3: 更深
x = layers.Convolution(ctx.In("conv3"), x).
Channels(128).KernelSizePerAxis(3, 3).PadSame().Done()
x = activations.Relu(x)
// 展平 → 全连接分类头
// 注意:两次 MaxPool 后尺寸减半两次,假设输入为 H×W,则此时为 (H/4)×(W/4)
x = graph.Reshape(x, -1, 128*(input.Shape().Dimensions[1]/4)*(input.Shape().Dimensions[2]/4))
x = layers.DenseWithBias(ctx.In("classifier"), x, 10)
return x
}
PadSame() 自动计算 padding,输出尺寸 = 输入尺寸(stride=1 时)。MaxPool 做下采样——把 2×2 区域的最大值取出来,图像尺寸减半,特征图数量不变。
---
11.4 注意力机制:从"固定权重"到"上下文相关权重"
全连接层:每个输入和每个输出之间的权重是训练好就固定的——模型无论看到什么输入,权重都一样。
注意力机制:权重是根据输入动态计算的。模型看到不同的输入,它会"关注"输入中的不同部分。
这就是"注意力"这个名字的来源——不是所有输入同等重要。对于当前任务,有些输入比另一些更重要。
---
11.5 Query、Key、Value 是怎么回事?
注意力的核心靠 Query、Key、Value 三个概念驱动:
Query(查询): "我想找什么?"
Key(键): "我有什么?"
Value(值): "我提供的实际内容是什么?"
注意力 = Softmax(Query · Key^T / √d) · Value
一步步拆解:
1. Query · Key^T:计算 Query 和所有 Key 的匹配分数。如果 Q 和某个 K 的向量方向一致(点积大),说明这个位置跟当前的查询高度相关。
2. Softmax(...):把匹配分数转成概率分布(所有权重加起来等于 1)。
3. × Value:按权重加权求和 Value——相关位置的值被放大,不相关位置的值被抑制。
用日常类比:你在图书馆找一本关于"Go 并发"的书。Query = "Go 并发"。书架上的每本书都有一个 Key(书的主题摘要)。你对比 Q 和每本书的 K,找到最匹配的几本(Softmax 给高分)。然后你按匹配度加权阅读这些书的 Value(书的实际内容)。
---
11.6 GoMLX 中的注意力
import (
"github.com/gomlx/gomlx/pkg/ml/context"
"github.com/gomlx/gomlx/pkg/core/graph"
"github.com/gomlx/gomlx/pkg/ml/layers"
"github.com/gomlx/gomlx/pkg/ml/layers/attention"
)
// 多头自注意力——Transformer 的核心
func SelfAttentionBlock(ctx *context.Context, x *graph.Node) *graph.Node {
// MultiHeadAttention:Q, K, V 都来自同一个输入 x(自注意力)
attnOut := attention.MultiHeadAttention(
ctx.In("attn"), x, x, x, // query, key, value
8, // 8 个注意力头
64, // 每头维度 64 → 总输出维度 8×64=512
).Done()
// 残差连接 + LayerNorm
x = graph.Add(x, attnOut)
x = layers.LayerNormalization(ctx.In("norm1"), x, -1).Done()
return x
}
多头注意力:同时做多组 Q/K/V,每组关注不同的方面。8 个头 = 8 种不同的"注意力模式"——一个关注句首、一个关注动词、一个关注数字……
---
11.7 CNN vs Attention:什么时候用哪个?
| 维度 | CNN | Attention |
|---|---|---|
| 核心操作 | 固定卷积核滑动 | 动态计算注意力权重 |
| 参数效率 | 高(参数共享) | 中等(QKV 投影有参数) |
| 对位置信息的利用 | 天生具备(局部感受野) | 需要位置编码辅助 |
| 擅长处理 | 局部模式、平移不变性 | 长距离依赖、全局关系 |
| 计算复杂度 | O(N)(N = 像素数) | O(N²)(N = 序列长度) |
| 经典应用 | 图像分类、目标检测 | NLP、多模态、ViT(视觉Transformer) |
---
11.8 收束:两种哲学,一个框架
CNN 的哲学:重要的模式在局部——用一个小窗口扫过去,共享参数。
Attention 的哲学:重要的模式在全局——让模型自己决定关注什么位置。
GoMLX 让你同时拥有两种武器。第 12 章,我们用 CNN 打一场完整的仗。
---
> 作者:步子哥,资深AI研究员 | 连载中:《GoMLX从入门到精通》
[上一章:第10章 神经网络——层、激活函数与前向传播] | [下一章:第12章 实战:CIFAR-10 图像分类全流程]
🌟 智谱 GLM-5 已上线
我正在智谱大模型开放平台 BigModel.cn 上打造 AI 应用,智谱新一代旗舰模型 GLM-5 已上线,在推理、代码、智能体综合能力达到开源模型 SOTA 水平。
🎁 领取 2000万 Tokens