Adam
SGD梯度下降回顾
优化器是什么?
在知道梯度后,决定一步走多远的是优化器。我要做权重更新,我在知道梯度的情况下,将权重从1更新到0.8,还是从1更新到0.5,还是从1更新到0.2,控制一步走多远的,这是优化器所做的事情。
之前讲的优化器是如下(SGD梯度下降):
θ ← θ − η · ∇L(θ)
η = 学习率(全局固定) ∇L = 梯度
θ 是原始的权重
∇L梯度也就是导数
η 是学习率,是标量,如0.1,学习率有控制更新速度的作用
SGD核心问题
✗ 学习率难调 过大震荡,过小收敛慢
✗ 各参数同等对待 不同特征梯度尺度差异大
✗ 鞍点/平坦区停滞 梯度接近零时无法前进
✗ 无动量 每步独立,方向可能抖动
Adam如何解决?
m ^ t m̂_{t} m^t 一阶矩(动量):累积梯度方向,平滑震荡
v ^ t v̂_{t} v^t 二阶矩(自适应):按参数梯度量级缩放学习率
β 偏差修正:消除初期动量估计的偏差
Adam 原理 --- 动量与自适应学习率
每步更新流程
1.计算梯度
当前参数处的损失梯度 g t = ∇ L ( θ t − 1 ) g_t = \nabla L(\theta_{t-1}) gt=∇L(θt−1)
"在当前参数 θ t − 1 \theta_{t-1} θt−1的位置,计算损失函数 L 的梯度。"
- ∇ L ( θ t − 1 ) \nabla L(\theta_{t-1}) ∇L(θt−1):整体表示损失函数 L L L 关于参数 θ \theta θ 的梯度,并在参数值为 θ t − 1 \theta_{t-1} θt−1 处进行计算。
符号拆解
- t t t:表示当前的时间步(或迭代次数、批次索引)。在训练神经网络时,数据是分批次(Batch)处理的, t = 1 , 2 , 3... t=1, 2, 3... t=1,2,3... 代表第 1 次、第 2 次、第 3 次参数更新。
- θ t − 1 \theta_{t-1} θt−1:表示在第 t t t 次更新之前的模型参数集合。
- θ \theta θ (Theta) 代表神经网络中的所有可学习参数(如权重 weights 和偏置 biases)。
- 下标 t − 1 t-1 t−1 强调这是"旧"的参数,即上一轮迭代结束后的参数状态。
- L ( ⋅ ) L(\cdot) L(⋅):损失函数(Loss Function)。它衡量模型预测值与真实标签之间的差异。我们的目标是最小化这个函数。
- ∇ \nabla ∇:梯度算子(Nabla/Del)。在多维空间中,梯度是一个向量,指向函数值增长最快的方向。
- g t g_t gt:在时间步 t t t 计算出的梯度向量。
求导公式与优化器是独立的。
梯度的计算和SGD无关,梯度计算只和所选的损失函数和模型结构有关,可以查看 神经网络基础原理-使用梯度下降训练二次函数模型简单示例
这篇文章了解梯度计算只和所选的损失函数和模型结构有关。
当损失函数和模型结构确定了,求导公式就已经确定了。Loss函数中包含模型的输出,Loss和model是一体的,这两组成的大公式,用w和b分别对这个大公式求导,得到梯度。后面是把数带入,把梯度具体数值算出来。
2.更新一阶矩(动量)
公式: m t m_{t} mt = β₁ · m t − 1 m_{t-1} mt−1 + (1−β₁) · g t g_{t} gt
β₁ ≈ 0.9,对梯度做指数移动平均,减少噪声
计算系数是β₁ ,β₁ 是个标量,默认值是0.9
m 0 m_{0} m0 = 0。最初的 m 0 m_{0} m0 我们默认初始化为0.
g t g_{t} gt 是第t轮计算的梯度
m 1 m_{1} m1 = β₁ · m 0 m_{0} m0 + (1−β₁) · g 1 g_{1} g1
m 1 m_{1} m1 = (1 - 0.9 )· g 1 g_{1} g1 = 0.1· g 1 g_{1} g1
m 2 m_{2} m2 = β₁ · m 1 m_{1} m1 + (1−β₁) · g 2 g_{2} g2
m 2 m_{2} m2 = 0.9· m 1 m_{1} m1 + 0.1· g 2 g_{2} g2
m 2 m_{2} m2 中包含 m 1 m_{1} m1 , m 1 m_{1} m1中又包含 g 1 g_{1} g1,也就是上一轮的梯度,相当于将上一轮的梯度和本轮的梯度做了差值, m 2 m_{2} m2 中同时包含 g 1 g_{1} g1和 g 2 g_{2} g2
m 3 m_{3} m3 = β₁ · m 2 m_{2} m2 + (1−β₁) · g 3 g_{3} g3
m 3 m_{3} m3 = 0.9 · m 2 m_{2} m2 + 0.1· g 3 g_{3} g3
m 3 m_{3} m3 同时包含 g 1 g_{1} g1 、 g 2 g_{2} g2 、 g 3 g_{3} g3的差值。
也就是前三轮的梯度一定程度上都混合在了 m 3 m_{3} m3 中,以此类推, m 4 m_{4} m4 混合了前4轮的梯度, m t m_{t} mt 混合了前t轮的梯度。
总之, m t m_t mt 动量实际上是前 t 轮梯度的混合,有一个系数 β₁ 控制了混合的比例。
动量(一阶矩)的作用
就像一个球滚下山坡------不仅响应当前坡度,
还保留之前滚动方向的惯性。
• 梯度方向一致 → 加速
• 梯度方向震荡 → 阻尼(平滑)
β₁=0.9 时:动量更新示意
t=1: m₁ = 0.9×0 + 0.1×g₁ = 0.1 g₁
t=2: m₂ = 0.9×m₁ + 0.1×g₂
t→∞: mₜ ≈ 加权平均梯度
3.更新二阶矩(梯度平方)
v t = β 2 ⋅ v t − 1 + ( 1 − β 2 ) ⋅ g t 2 v_t = \beta_2 \cdot v_{t-1} + (1-\beta_2) \cdot g_t^2 vt=β2⋅vt−1+(1−β2)⋅gt2
β₂ ≈ 0.999,估计梯度的方差(无偏方差代理)
v t v_{t} vt 最开始为0,即 v 0 v_{0} v0 = 0
v t v_{t} vt每轮都是前一轮 v t − 1 v_{t-1} vt−1 的差值。
相较于一阶矩,主要差别在一个是前n轮梯度gₜ的混合差值,一个是前n轮梯度平方 g t g_{t} gt² 的混合差值。
4.偏差修正
m ^ t = m t 1 − β 1 t \hat{m}_t = \frac{m_t}{1-\beta_1^t} m^t=1−β1tmt
v ^ t = v t 1 − β 2 t \hat{v}_t = \frac{v_t}{1-\beta_2^t} v^t=1−β2tvt
t 步时 β 1 t → 0 \beta_1^t \to 0 β1t→0,修正初期动量被低估的问题
β₁ = 0.9
β₂ ≈ 0.999
两者的分母都是随着t的增大,越来越趋近于1,分母越来越大的趋势,两者在训练后期都比前期是减弱了。
我的训练后期比训练的前期走的慢一些,使得我的训练先快后慢。
5.参数更新
θ t = θ t − 1 − η ⋅ m ^ t v ^ t + ϵ \theta_t = \theta_{t-1} - \eta \cdot \frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon} θt=θt−1−η⋅v^t +ϵm^t
ϵ ≈ 1 e − 8 \epsilon \approx 1e-8 ϵ≈1e−8,防止除零;梯度大的参数步长自动缩小
θ t − 1 \theta_{t-1} θt−1 是模型的权重w
η 是学习率
原来的SGD中, m ^ t v ^ t + ϵ \frac{\hat{m}_t}{\sqrt{\hat{v}t} + \epsilon} v^t +ϵm^t这块直接是 g t g{t} gt梯度。
实际上相当于是adam优化中将原来SGD公式的θ ← θ − η · ∇L(θ)
∇L(θ) 这个梯度换成了 m ^ t v ^ t + ϵ \frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon} v^t +ϵm^t这个公式。
ε可以不用关注,只是数学上防止除零的操作。
所以实际上我们要关注的是 m ^ t v ^ t \frac{\hat{m}_t}{\sqrt{\hat{v}t}} v^t m^t为什么比用 g t g{t} gt好?
关于 m ^ t \hat{m}_t m^t 的意义:
我们下山的时候,有三波人和你说往下的方向走,后面又遇到一个人,和你说往上的方向走,那我们我们会觉得这个人可能不太认识路,毕竟有那么多的人都告诉你往下走。那么我们是不是可以仍然往下走,我们不回头向上走,但是少走一点。尽可能规避错误样本给我们的错误指导意见。所以我们要尽可能相信大多数人。容忍我们的训练数据中可能有标错的。
所以我们对于过去梯度的差值(一阶矩),本质上是在相信大多数人。那个和我说往上走的梯度和过去的梯度一总结,总的方向还是向下走,只是向下的幅度不那么大了。如果后面再遇到很多人和我说往上走,那么我才可能确实该回头了,我才会扭转过来。
不要抛弃过去几轮的经验,把过去的梯度继承下来。这是 m ^ t \hat{m}_t m^t 的价值
v ^ t \hat{v}_t v^t 的意义:
v ^ t \hat{v}_t v^t 是梯度的绝对值的大小。
如果梯度很大,那么我除以一个较大的数,会使得整体的值较小一点。
如果梯度较小,除以一个较小的数,会使得整体的值较大一点。
以下是详细解析:
- v ^ t \hat{v}_t v^t 的本质:梯度的"二阶矩"估计
- 来源: v ^ t \hat{v}_t v^t 是梯度平方 g t 2 g_t^2 gt2 的指数移动平均(Exponential Moving Average, EMA),并经过偏差修正。
- 物理意义:它反映了该参数在历史迭代中梯度的变化幅度或离散程度。
- 如果某个参数的梯度一直很大且波动剧烈, v ^ t \hat{v}_t v^t 就会很大。
- 如果某个参数的梯度很小且平稳, v ^ t \hat{v}_t v^t 就会很小。
- v ^ t \sqrt{\hat{v}_t} v^t 的具体作用:自适应调节步长
在更新公式中, v ^ t \sqrt{\hat{v}_t} v^t 位于分母位置。这意味着参数更新的步长与该参数历史梯度的幅度成反比:
-
对于梯度波动大(陡峭)的参数:
- v ^ t \sqrt{\hat{v}_t} v^t 值较大。
- 分母变大,导致整体更新步长 m ^ t v ^ t + ϵ \frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon} v^t +ϵm^t 变小。
- 效果:防止在陡峭方向上因步长过大而产生震荡或发散,起到"刹车"作用。
-
对于梯度波动小(平缓)的参数:
- v ^ t \sqrt{\hat{v}_t} v^t 值较小。
- 分母变小,导致整体更新步长变大。
- 效果:在平缓方向上加速前进,避免陷入局部极小值或鞍点时停滞不前,起到"加速"作用。
总之就是梯度绝对值很大的时候,我们走小一点,梯度绝对值很小的时候,我们尽量走大一点。这是一种平衡,一种自适应。
和SGD相比,Adam的优化器原理
考虑了历史训练梯度,设计了学习率自适应的方式。
θ t = θ t − 1 − η ⋅ m ^ t v ^ t + ϵ \theta_t = \theta_{t-1} - \eta \cdot \frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon} θt=θt−1−η⋅v^t +ϵm^t
实际上可以看作如下(忽略 ϵ {\epsilon} ϵ,并且 m ^ t \hat{m}t m^t 可以视同 g t g{t} gt)
θ t = θ t − 1 − ( η v ^ t ) ⋅ g t \theta_t = \theta_{t-1} - \left(\frac{\eta}{\sqrt{\hat{v}t}}\right) \cdot g{t} θt=θt−1−(v^t η)⋅gt
( η v ^ t \frac{\eta}{\sqrt{\hat{v}_t}} v^t η )当作是学习率 。这个公式就和SGD差不多了。
所以我们说adam有一定的自适应学习率的情况。
自适应学习率(二阶矩)的作用
对梯度较大的参数缩小步长,
对梯度较小的参数放大步长。

adam解决SGD核心问题
✗ SGD 学习率难调 过大震荡,过小收敛慢
Adam有自适应学习率的处理。
✗ SGD 各参数同等对待 不同特征梯度尺度差异大
adam不同参数的绝对值不同,每个参数的梯度大小不同,使得调整的幅度不一样。
✗ SGD 鞍点/平坦区停滞 梯度接近零时无法前进
✗ SGD 无动量 每步独立,方向可能抖动
常用优化器AdamW
大部分场景下优化器都可以是adam,图像任务,文本任务等都适用,贝塔值不用调。
pytorch实现:
基本实现

基本参数说明

embedding层(离散符号 → 稠密向量 · 中文 NLP 实例)
为什么需要 Embedding? =》文字不能直接送入神经网络
One-Hot 编码(朴素方案)

每个字对应一个向量,每个向量的长度和你的词表的总长度相同。比如词表有3000个字,那么一个字对应的向量维度就是3000维度。
每个汉字占一个单独的位置,其他位置都是0,也就是2999个0,加一个1.
每个向量之间是正交垂直的,但是有的字表达是接近的,那么我们希望模型看待它的方式也接近。
Embedding解决方案
embedding层将文字转为数字,将每个词映射为固定维度的稠密实数向量
每个字都是同维度的向量。所有字的维度一定是一致的。(one-hot也一样)
每个字对应的向量一开始是随机的(随机初始化),但是训练中梯度更新,反向传播,会调整到合适的位置。我们会发现模型学习出来,那些语义相近的字,他们的向量也比较相近。
embedding层这个向量是可训练向量。one-hot中是不可训练的向量。
维度与词表大小解耦了,两者没有关系了。

Embedding 矩阵结构
Embedding 本质是一张查找表,为每个字准备了一个同维度的向量,它一开始是随机的,在训练过程中会被梯度更新,参与反向传播,最后调整到合适的位置(这一点非常重要)。
最后我们需要用模型做推理的时候,就用文字对应的向量作为输入。

查表过程
输入:词 id(整数)
输出:对应的 d 维向量
python
import torch.nn as nn
# vocab_size=5, embed_dim=4
emb = nn.Embedding(5, 4)
# 查询 id=2('爱')
idx = torch.tensor([2])
vec = emb(idx)
# vec.shape → (1, 4)
关键特性
· 权重可反向传播更新(随机初始化)
· 可加载预训练词向量(如 Word2Vec)
多语种:一张字符表通吃
多语种和单语种对模型而言,没有本质区别,都是靠embedding层来完成。对模型而言,只是将字符映射成向量,至于字符原本是中文还是英文,模型并不在乎,最后拿到的都是数字。多语种完全可以一起训练,各个语言都放在同一张表里即可,让一个模型去通吃所有的语言,只是训练难度会比单语种大些。
随机为中文,英文,日文随机分配一个向量,然后让他去训练。

PAD & UNK
< PAD >:把长短不一的句子拼成矩阵
模型训练时,一个batch必须是固定的矩阵形状。
假设一个字对应一个10维度向量,
一个字是 1 * 10的矩阵。
五个字是 5 * 10的矩阵。
为了方便计算,需要保证是同尺寸的矩阵。
假设下图中每个字符是10维度的向量,我们设定句子的长度是8个字符,那么我们对不足8的长度的句子,需要用pad去补0,0的位置是10维度全0的向量。
7 * 10, 5 * 10 , 1 * 10的矩阵。通过pad,都补齐成 8 * 10的矩阵。
让如下三个样本的形状都补成 8 * 10,那么他们就可以放在一个batch里了。
那么如下,我们一个batch的整体的形状就是 3 * 8 * 10。batch_size是3,是如下三句话,三个样本,每个样本是8 * 10的维度。
pad不参加梯度的反向传播。

对于短句子,我们用pad补0,那么对于长句子,我们可以截断。
尽量保证95%的样本在最大长度以下。因为截断语句可能有歧义。
如果保证100%的样本在最大长度以下,可能比较浪费。不用特别考虑特长的极端样本,可能是录入有误,极端样本该踢就踢。
UNK:词表里没有的字
词表之外的字,只能落到 1 号槽位
unk字符,占据一个向量。词表里没有的字都对应到一个特殊的字符上,用这个字符的向量。
词级(Word-level)
新词、人名、网络用语常年不在词典里
→ UNK 一多,这些词的语义就直接丢了
字符级(Character-level)
字符集小且稳定,任何句子都能逐字拆
→ UNK 极少(本课数据训练集 UNK 占比 0.0%)
两个占位符,语义完全不同:
PAD = 这里没有内容 | UNK = 这个字我不认识
embedding_padding_demo.py
python
#coding:utf8
"""
Embedding 层 与 Padding 操作教学
"""
import torch
import torch.nn as nn
# ── 1. Embedding 本质:一张可训练的查找表 ─────────────────────────────────────
#
# nn.Embedding(num_embeddings, embedding_dim)
# └─ 内部维护一个形状为 [num_embeddings, embedding_dim] 的权重矩阵
# └─ 输入整数 index,直接取对应行,等价于 one-hot × 权重矩阵,但效率更高
#
vocab = {"<pad>":0, "<unk>":1, "你":2, "好":3, "中":4, "国":5, "欢":6, "迎":7}
VOCAB_SIZE = len(vocab) # 8
EMBED_DIM = 4
# 如下这句是在告诉embedding层,我有多少字符(VOCAB_SIZE)需要转向量,我希望向量是多少维度(EMBED_DIM)的。如下就是在声明 我有八个四维的向量要训练。
# embed_dim:每个向量需要是多少维度的
embedding = nn.Embedding(VOCAB_SIZE, EMBED_DIM)
print("── Embedding 权重矩阵(shape = 字符数 × 向量维度)──")
# 如下就是embedding帮我们随机初始化的8 * 4 的矩阵。这个矩阵的每一行,就是每个字符对应的向量
print(embedding.weight, "\n")
# 单个 token 查询:取第 4 行("中")
idx = torch.LongTensor([4])
print("'中' 的向量(第4行):", embedding(idx))
# ── 2. 为什么需要 Padding ──────────────────────────────────────────────────────
#
# 同一个 batch 中,句子长度不同,无法直接堆成矩阵。
# 解决方案:统一截断或补齐到 max_len,不足的位置填特殊 token [<pad>]=0。
#
# "中国欢迎你" → [4,5,6,7,2] 长度5,不需补
# "你好中国" → [2,3,4,5,0] 长度4,补1个 pad
# "feda" → [1,1,1,1,0] 全 unk,补1个 pad
#
MAX_LEN = 5
def encode(text, vocab, max_len):
ids = [vocab.get(ch, vocab["<unk>"]) for ch in text][:max_len]
ids += [vocab["<pad>"]] * (max_len - len(ids)) # 补齐
return ids
sentences = ["中国欢迎你", "你好中国", "feda"]
batch_ids = [encode(s, vocab, MAX_LEN) for s in sentences]
print("batch_ids",batch_ids)
print("\n── Padding 后的 token id ──")
for s, ids in zip(sentences, batch_ids):
print(f" {s!r:8s} → {ids}")
# ── 3. padding_idx:让 [pad] 向量永远为零 ─────────────────────────────────────
#
# 设置 padding_idx=0 后:
# ① Embedding 初始化时第 0 行全为 0
# ② 反向传播时第 0 行梯度不更新([pad] 不携带语义,不应影响训练)
#
embedding_with_pad = nn.Embedding(VOCAB_SIZE, EMBED_DIM, padding_idx=0)
print("\n── 设置 padding_idx=0 后的权重矩阵 ──")
print(embedding_with_pad.weight)
print("第0行([pad] 向量):", embedding_with_pad.weight[0]) # 全零
# ── 4. 完整 batch 送入 Embedding ──────────────────────────────────────────────
x = torch.LongTensor(batch_ids) # shape: [3, 5]
out = embedding_with_pad(x) # shape: [3, 5, 4]
print("\n── Embedding 输出(batch=3, seq_len=5, embed_dim=4)──")
print("shape:", out.shape)
print(out)
print("\n注意:每个句子最后的 pad 位置对应向量全为 0,不影响模型对有效 token 的建模。")
执行如上文件,打印如下内容:
python
/opt/miniconda3/envs/py312/bin/python /Users/yuria/Agent/week3-深度学习常用组件/embedding_padding_demo.py
── Embedding 权重矩阵(shape = 字符数 × 向量维度)──
Parameter containing:
tensor([[ 1.6163, -1.0457, -0.4053, 0.8748],
[-0.0915, -0.2494, 2.2897, 0.3592],
[ 0.1139, -1.2174, 0.7542, 1.4914],
[-2.1809, 1.6299, -0.2293, 0.4906],
[ 1.5605, -1.8889, 0.1665, 0.0144],
[-0.3446, 0.3322, 0.7030, -0.5725],
[-0.0071, -0.0051, -0.5192, -0.6743],
[-1.9431, -1.1434, 0.1922, -0.6515]], requires_grad=True)
'中' 的向量(第4行): tensor([[ 1.5605, -1.8889, 0.1665, 0.0144]], grad_fn=<EmbeddingBackward0>)
batch_ids [[4, 5, 6, 7, 2], [2, 3, 4, 5, 0], [1, 1, 1, 1, 0]]
── Padding 后的 token id ──
'中国欢迎你' → [4, 5, 6, 7, 2]
'你好中国' → [2, 3, 4, 5, 0]
'feda' → [1, 1, 1, 1, 0]
── 设置 padding_idx=0 后的权重矩阵 ──
Parameter containing:
tensor([[ 0.0000, 0.0000, 0.0000, 0.0000],
[-0.4799, -0.8525, -1.1389, 0.3798],
[ 0.0664, 1.1555, 1.2549, 0.8234],
[-1.4880, -1.1912, -0.0732, 0.5310],
[-1.1646, -0.4408, -0.6273, 0.4283],
[-0.3654, 0.7572, 0.0212, -1.0341],
[-0.2374, 0.6735, -1.0020, -1.7913],
[ 0.3574, -1.5409, 0.3040, -0.6921]], requires_grad=True)
第0行([pad] 向量): tensor([0., 0., 0., 0.], grad_fn=<SelectBackward0>)
── Embedding 输出(batch=3, seq_len=5, embed_dim=4)──
shape: torch.Size([3, 5, 4])
tensor([[[-1.1646, -0.4408, -0.6273, 0.4283],
[-0.3654, 0.7572, 0.0212, -1.0341],
[-0.2374, 0.6735, -1.0020, -1.7913],
[ 0.3574, -1.5409, 0.3040, -0.6921],
[ 0.0664, 1.1555, 1.2549, 0.8234]],
[[ 0.0664, 1.1555, 1.2549, 0.8234],
[-1.4880, -1.1912, -0.0732, 0.5310],
[-1.1646, -0.4408, -0.6273, 0.4283],
[-0.3654, 0.7572, 0.0212, -1.0341],
[ 0.0000, 0.0000, 0.0000, 0.0000]],
[[-0.4799, -0.8525, -1.1389, 0.3798],
[-0.4799, -0.8525, -1.1389, 0.3798],
[-0.4799, -0.8525, -1.1389, 0.3798],
[-0.4799, -0.8525, -1.1389, 0.3798],
[ 0.0000, 0.0000, 0.0000, 0.0000]]], grad_fn=<EmbeddingBackward0>)
注意:每个句子最后的 pad 位置对应向量全为 0,不影响模型对有效 token 的建模。
进程已结束,退出代码为 0
为什么一开始初始化权重,就将padding行设置为全零?
因为我们不希望它影响训练,它只是为了把形状拉成一致才凑数进来的东西,我们不希望它对训练结果有影响。把它设置为全零,是为了消除它对后续计算影响的一种相对好的方法。
考虑多语种情况
如果有英文,也可以直接加在vocab中,中英混合没问题的。如下:
vocab = {"":0, "":1, "你":2, "好":3, "中":4, "国":5, "欢":6, "迎":7,"a":8,"b":9,"c":10}
权重的更新
随机初始化的向量会在后续模型的反向传播的训练中进行更新。
为什么不训练出来一个词表大家通用呢?
专用于某些特定场景的去训练,无效字符较少。
训练数据不同,中文,英文,西班牙语 训练数据语言占比不同。
神经网络只认数字不认字符,所以需要转换成向量。
如何获取某个文件的字符?如下即可去重获取某个文件中的字符:

CNN (卷积层)-局部感受野 · 权重共享 · 1D 卷积用于文本
使用卷积层的不多了。它是深度学习下的常见概念。只有理解了这个cnn和rnn,你才能更理解为什么transformer是更好的网络结构。
图像上的卷积(2D卷积-有两个方向,横向和纵向)
图像是天然的数字,本质本身就是一张天然的矩阵。
如1920*1080的分辨率屏显(电脑屏幕的像素)
这是个矩阵,这个矩阵中的每个值,就是像素值,对应到每个色块上。

输入图像和卷积核对位相乘再相加。
卷积核会横向扫描,横向移动,对位相乘再相加。
再纵向移动,对位相乘再相加。再横向移动,对位相乘再相加。
将输入的图扫描一遍。

最后得到的图的特征。
我们对这个对图像进行卷积的操作,叫做特征提取。我们这里实际上训练的是卷积核,卷积核是随机初始化的,我们在训练中会通过反向传播梯度更新卷积核,使得这个卷积核提取的特征更为合理,使得loss越来越小。这个就是卷积神经网络的做法。
图像是它的局部代表一定的信息,单看一个像素是看不出什么东西的,只有一部分的像素一起看,才能表达出来这张图片特定的信息。
所以卷积设计的思想就是,我每次看局部,看它能否给我带来一些特定的信息。我们在训练中会不断的微调卷积核的权重。
卷积核使用举例:
比如判断图像是否是猫,设置特定卷积核,结果得到很高值的矩阵。反映到输出层,会告诉我们是猫的概率很大。如果得到值很小的矩阵,是猫的概率很小。
文本上的卷积(1D卷积)
之所以叫做1D卷积,是因为文本只有一个方向,就是文本的方向。
经过embedding后,文本的一个字是对应一个向量的。
eg:我爱北京天,假设每个字是8维度的向量,那么这个5个字就是 5 * 8的矩阵。
卷积核在滑动的时候,是整列(也就是整个字8维度的高度去卷,不可能是只卷半个字,这没有意义)整列去顺着文本的方向滑动的。
计算的逻辑同图像卷积,也是随机初始化一个核(权重),这个卷积核的大小等于每个字的维度(也就是我们之前设置的embed_dim),横向就是我们指定的宽度(也就是一次需要卷几个字的宽度)。然后去进行移动,在训练过程中,也是调整卷积核的参数。
文本卷积的思路:有些字单独看,看不出来意思,但是把某些字结合着看,更能表达一些信息,所以说我们是用这种方式把这些字卷到一起去看。这就是文本上卷积的思想,我们用更专业的话来表达就是局部感受野。一次感受输入的局部信息,而不是某一个字的信息。
两大核心思想
局部感受野
每次只看输入的一个小窗口(卷积核),
不是一次看全局
权重共享
同一个卷积核在整个序列上滑动,
参数量不随输入变大
图像 vs 文本卷积
图像:Conv2d,核在 H×W 二维滑动
文本:Conv1d,核在序列长度一维滑动

文本卷积核对中文卷一次一般取2-4个字。
可设置一个卷积核,一次卷5个字。再设置一个卷积核,一次卷3个字。这样可以设置多个卷积核。
文本是按照字去卷的,每次都是把多个字的整个向量一起去卷的。卷积核的大小,其中一维是和embedding 的EMBED_DIM的维度一致的。另外一维度是可以调整的。
卷积核在扫描图像或文本的过程中,值是不变的。
只有在每次反向传播,更新了之后,才会变化。在下次扫描过程中还是不变化的。
关键参数:kernel_size · stride · padding

padding的作用:输入图像5 * 5,经过卷积核后,输出为 3 * 3 ,因为它只能横向纵向各移动三次。所以 5 * 5 的输入变成了 3 * 3 的输出。矩阵的形状变化了,但是有的时候,不利于堆叠很多层数,比如图越卷越小,后面再加上同样大小的卷积,就没法用了。比如 3 * 3输入图像,卷积核也是 3 * 3,这就无法卷积操作了。
那么我们可以在图像外额外加一圈padding,这样 就能使得 5 * 5的输入,经过卷积后,仍是5 * 5的输出。所以加padding的好处就是使得你的特征大小不变。
文本上也一样,如 5个字,卷积窗口为3,输出也是3.
我们可以给文本加padding,前后加一个字符,那么输出能保持仍然是5.
图像不需要embedding,它天然就是矩阵,不需要查表,embedding层的意义只是将字符转换成数字。多语种也只用一个embedding即可。embedding是文本类的第一层。卷积核不需要是正方形,也可以是 3 * 4.
卷积这一层,你可以理解是计算公式,代替的是线性层,然后你可以在后面接上其他的层,使得你的网络完整。然后你就可以继续做训练,反向传播。在反向传播的时候,所有层的权重都会更新,卷积层权重,cnn层,激活层,线性层等所有层的权重都会更新。
卷积作用:提取原始输入上的一些局部特征。本质是做了矩阵运算,矩阵运算根据矩阵的值的不同,得到不同的特征值,我们希望某种规律可以通过这个值来反映。
卷积核使用举例:
比如判断图像是否是猫,假如原图的像素分布非常像一只猫的话,它和一个特定训练后的卷积核相乘,得到的值是一个很大的值。如果不是猫的话,乘完是一个很小的值。
比如想根据输入的文本判断,这段文本是夸我的还是骂我的,卷积的矩阵乘上骂我的词,比如傻瓜,刚好得到一个很大的值,那么就告诉我这个文字倾向于是骂我的。
循环层(RNN/LSTM)(序列建模 · 隐状态 · 遗忘门 · 双向)
LSTM是RNN的变种
RNN是nlp领域的思路,天然适合文本的公式。
h0 是hidden_size 有128个零的数组
x1是文本输入(已经被embedding为向量的x1)
tanh 激活函数
w_h和w_x是可训练的,都是矩阵。
h2包含了一部分来自x2的信息,也包含了一部分来自x1的信息。
h3就包含了x1,x2,x3的信息。
h是不断的在更新的(被输入的序列去更新,包含了序列中的每一步)
h一开始是全零的
rnn只适合文本类。因为图像不能一溜一溜列的结合看。
h一直都是128维度的。
不管训练有多长,隐单元都是128维度。
x100 ,文本太长,rnn的效果就变差了,128不够用了。
为了解决这个问题,有了LSTM.
如果记笔记只有一页纸,第一节课,第二节都写在这张纸上。我们可以把第一节课的不重要的东西擦掉,给新的课腾地。这就是遗忘门。
遗忘门:本质也是rnn,
sigmoid激活(最外层的含义),1是打开,0是关闭状态
用计算的结果和我们的记忆做相乘,
0的位置,表示这部分记忆删掉。1的位置,表示这部分记忆保留。
输入门:也是做了激活,对新的输入做筛选
输出门??todo
增加了可训练参数,让他更好拟合规律。
四个rnn.
cnn卷积核,只能和相邻的字有关系,很难和很远的字去联系起来。一个卷积窗就3个长度。
而rnn更符合序列思路的思想。
三个门发生的位置不同。
rnnforward.py
对于rnn的手动实现
hidden_size隐单元的大小
lstmforward.py
如上两个不要求看明白,因为这两个模型使用的不是太多了。
cnn是邻接的
rnn是??todo
lstm是对rnn的改进(记忆遗忘,筛选,保证传递更长的序列)
tanh激活函数换成其他的激活函数一样成立。
rnn中x1是字,是embedding_size的维度,假设100
x2也是100,x3也是100
h是1128的维度
推算出来w_x是128 100的尺寸
w_h是128*128的矩阵
h整层输出是3*128尺寸。因为有三个输出
input_size是x的维度,即100
hidden_size是128
num_layers
双向是使用两个lstm,一个从前往后进行,一个从后往前进行。用的比较多。对序列做双向建模。
有时候人说话,先理解后面的,再理解前面的更好理解些。
输入顺序就是x3. x2. x1
模型可以先cnn,后rnn,也可以反过来先rnn,后cnn
池化层
有个向量,最大池化,就是将向量中只保留最大值。
平均池化,所有的数取平均。
105的矩阵,我们做池化,可以把它变成10 1的向量(因为我们每行取平均或者最大值)。
做5分类,我们的模型输出必须是5维度的
??
池化的作用是啥??
做池化是横向方向去处理。因为文字是横向去理解的。
3128所以池化完是1128。
池化把信息降低维度,后面可以接分类层。
取最大的0.9,其他的0.1变成0.2对模型不影响,微小的改动不影响模型结果,增加了模型的鲁棒性。
甚至0.9变动位置,数值发生翻转,也是一样的结果。
pooling层增加了抗性,增加了抗干扰能力。
但是一定程度上也是一种丢失。
刚embedding-rnn-cnn后才做池化。不是刚embedding就去做池化,会导致丢失很多东西。
rnn有两种用法:h1,h2,h3做池化,??todo
下采样
归一化层
上一层输出矩阵 3*128
接归一化层,可以做什么事情,可以计算所有值的平均数。所有数的方差。
假设有个向量是 -100,0,100
操作后成了 -1,0,1
大小关系是不变的,但是都等比例的被放缩了。
学习率大,爆炸了。小,不训练了。学习率去调整有局限。
Adam是为了反向存在的。
dropout
使得谁都不能偷懒,不能摸鱼。使得这个工作没了谁都能做。
dropout将好学生重置为0,剩下的就不好使了,那不行。
预测的时候,不带dropout层
dropout只在训练,前向计算的时候,用这个dropout层。
dropout.py
看每次结果是否都不一样