data格式转换

1
2
3
4
features.ToTensor() # 将PIL图像 or NumPy数组 -> PyTorch的Tensor格式, 像素值[0, 255] ->[0.0, 1.0]
features.cpu().numpy() # PyTorch 的 Tensor 转换成 NumPy 的 ndarray,如果 tensor 在 GPU 上,必须先 .cpu()
features.tolist() # 任意维度Tensor->Python list/int/float
features.item() # 单个元素Tensor->Python int/float

data维度转换

张量 tensor

  • storage:内存中起始元素的位置
  • Shape:元素的个数(2,3,2)
  • Stride :步幅(12,4,1)
  • contiguous:在内存中是否连续
  • offset:基于起始元素的偏移

PyTorch 的张量操作

大多数 PyTorch 模型(比如 ResNet)期望输入 shape 为 [batch_size, channels, height, width],也就是 4 个维度。

.squeeze() & .unsqueeze()

  • shape改变
1
2
.squeeze(dim=None)  # 删除所有长度为1的维度,如果dim长度是1,可删除
.unsqueeze(dim) # 在指定维度上增加一个长度为 1 的新维度

.view()

  • 返回 原内存的 view,仅改变stride和shape

  • tensor 必须是 contiguous(内存连续),否则需要 .contiguous() 先复制数据生成新的连续 tensor

1
2
3
pos_embed.shape # torch.Size([1, 14, 14, 768])
pos_embed_new = pos_embed.view(1, -1, 768)
pos_embed_new.shape # torch.Size([1, 196, 768])

.reshape() = .contiguous().view()

  • 如果张量本身连续reshape() 不会新建内存,只改变 view(视图),即改变张量的 shape 和 strides。
  • 如果张量不是连续的reshape()复制一份数据(新建内存), 再改变张量的 shape 和 strides。
  • reshape 只关注总元素数一样
1
2
3
pos_embed.shape # torch.Size([1, 196, 768])
pos_embed_new.reshape(1, 14, 14, 768)
pos_embed_new.shape # torch.Size([1, 14, 14, 768])

.flatten()

把张量展平成一维向量,默认按行优先 flatten(C-order)

1
2
3
4
5
x = torch.arange(12).reshape(2, 3, 2) # torch.Size([2, 3, 2])
y = x.flatten() # torch.Size([2, 6])
z = x.flatten(start_dim=1) # torch.Size([12])

x.flatten() == x.reshape(-1) #等价

.permute(dim0, dim1, dim2, …)

  • 内存不连续(contiguous:False),改变张量的 shape 和 strides
  • 改变张量维度的顺序
1
2
pos_embed.reshape(1, w0, h0, dim).permute(0, 3, 1, 2),  # → [1, dim, w0, h0]
pos_embed = pos_embed.contiguous() # permute返回非连续张量

.transpose(dim0,dim1)

  • 内存不连续(contiguous:False),改变张量的 shape 和 strides
  • 仅能交换张量中的两个维度
1
2
q, k, v = qkv[0] * self.scale, qkv[1], qkv[2]
attn = q @ k.transpose(-2, -1) # 交换最后两个维度(-2表示倒数第2维,-1表示最后1维)

.contiguous()

  • 内存连续化,但如果原 tensor 已经是连续的,不会新分配内存
1
y = x.transpose(0,1).contiguous()

torch.stack(tensors, dim=0) 往上放一层

  • 将多个张量沿着一个新维度拼接,增加一个新的维度
  • 会新建内存,stack 后 tensor 是连续的
1
2
3
4
5
6
7
a = torch.tensor([1, 2]) #(2,)
b = torch.tensor([3, 4])
c = torch.tensor([5, 6])

torch.stack([a, b, c], dim=0) # (3,2) tensor([[1, 2],[3, 4],[5, 6]])
torch.stack([a, b, c], dim=1) # (2,3) tensor([[1, 3, 5],[2, 4, 6]]) 新增的维度在第1维

torch.cat(tensors, dim=0) 连接起来

  • 沿已有维度拼接 tensor,不增加新维度。
  • 内存可能连续也可能不连续,cat 会返回新 tensor(通常连续)
1
2
3
a = torch.ones(2,3)
b = torch.zeros(2,3)
y = torch.cat([a,b], dim=0) # (4,3)

.expand(x.shape[0], -1, -1)

  • x.shape[0] 替换了第一个维度(原来必须是1),“复制”维度为1的数据,扩展成 batch size。
  • -1 表示保持原维度大小不变(第二和第三维不变)。
  • 不复制数据(共享内存),只是逻辑上重复元素。

slice / narrow()

  • contiguous 可能变成 False,offset 会改变
1
2
3
4
5
x = torch.arange(12).reshape(3,4)
y1 = x[:, 1:3] # slice x[start:end] (3,2)
y2 = x[:, 0] # slice (3,)
y3 = x.narrow(1, 1, 2) # narrow x.narrow(dim, start, length)(3,2)
y4 = x[x > 8] #输出是 1D tensor:tensor([9,10,11])

张量广播(broadcasting)

  • 自动逻辑扩展张量维度,使操作兼容。只能广播维度为 1或相等的情况

  • 不会复制内存,只是让操作时逻辑上看成扩展后的 shape,stride 不会增加

  • 使用 .expand() 可以显式广播。
1
2
3
4
5
6
7
8
9
10
x = [[0],
[1],
[2]] shape (3,1)
y = [[1,1,1,1], [1,1,1,1], [1,1,1,1]]

x broadcast → (3,4)
[[0,0,0,0],
[1,1,1,1],
[2,2,2,2]]
z = x + y → [[1,1,1,1], [2,2,2,2], [3,3,3,3]]

自动梯度(autograd)

  • PyTorch 自动跟踪 tensor 操作,计算梯度。
  • x = torch.randn(1, requires_grad=True) 时,会记录操作历史生成计算图。
  • .backward() 自动计算梯度。

注:
1、view / slice / narrow 等操作生成的新 tensor 保留计算图
2、广播也会被记录
3、.detach() 可以切断计算图
4、在 GPU / CPU 上都支持

针对ndarray

1
2
3
4
5
6
7
8
9
10
# 将one-hot编码的标签y转换为类别索引标签
y = np.argmax(y, axis=1) # 对每一行找出最大值的索引,得到类别标签的整数形式
# example:
y_train = np.array([
[0, 0, 1], # 类别 2
[1, 0, 0], # 类别 0
[0, 1, 0], # 类别 1
])

y_train = np.argmax(y_train, axis=1) # array([2, 0, 1])

ML model input

one-hot 接受情况

模型类别 模型名称/框架 是否接受 One-Hot 标签 正确标签格式
传统机器学习模型 LogisticRegression (sklearn) ❌ 不接受 整数类别标签(如 0,1,2)
SVM / SVC ❌ 不接受 整数类别标签
RandomForestClassifier ❌ 不接受 整数类别标签
KNeighborsClassifier ❌ 不接受 整数类别标签
DecisionTreeClassifier ❌ 不接受 整数类别标签
GaussianNB ❌ 不接受 整数类别标签
神经网络框架 PyTorch(自定义分类网络) ❌ 通常不接受 整数标签,用 CrossEntropyLoss
TensorFlow / Keras + SparseCategoricalCrossentropy ❌ 不接受 整数标签(如 0,1,2)
TensorFlow / Keras + CategoricalCrossentropy ✅ 接受 One-hot 标签
框架 常用损失函数 标签格式要求
PyTorch CrossEntropyLoss 整数标签([1, 2, 0, ...]
Keras SparseCategoricalCrossentropy 整数标签
Scikit-learn 所有分类模型 整数标签

浮点数精度

浮点数精度问题的本质

计算机通常使用 IEEE 754 标准表示浮点数(如 floatdouble):

  • 十进制小数 → 二进制往往是无限循环
  • 存储时必须截断或舍入 → 产生误差(rounding error)

例如:

1
0.1 (十进制) ≈ 0.0001100110011... (二进制无限循环)

常见精度问题

  • 表示误差(Representation Error)
1
0.1 + 0.20.3 #实际结果0.30000000000000004
  • 累积误差(Accumulation Error)
1
2
3
sum = 0
for i in range(1000000):
sum += 0.1 #结果 ≠ 100000
  • 大数吃小数(Catastrophic Cancellation)
1
2
3
a = 1.0000001
b = 1.0000000
a - b #当两个接近的大数相减,有效位数严重丢失,灾难性消减
  • 比较错误(Equality Comparison Problem)
1
2
if (a == b) #浮点数通常不应该直接比较相等
abs(a - b) < ε #正确做法:误差范围比较 ε = 1e-6 或 1e-9
  • 舍入误差(Rounding Error)

​ 在有限精度下:向最近值舍入,不同语言/硬件策略略有差异

  • 下溢 / 上溢(Underflow / Overflow)

    • 上溢:数太大 → ∞

    • 下溢:数太小 → 0 或非正规数

精度类型

类型 位宽 有效数字(十进制) 数值范围 优点 缺点 典型用途
FP16(半精度) 16-bit ~3–4位 较小 显存占用低、计算快 易下溢、数值不稳定 混合精度训练
FP32(float) 32-bit ~7位 ~10⁻³⁸ ~ 10³⁸ 稳定、通用性强 比FP16慢、占内存 默认训练精度
FP64(double) 64-bit ~15–16位 极大 高精度 非常慢、占用大 科学计算
BF16(Brain Float) 16-bit ~2–3位 ≈ FP32 范围大、稳定性好 精度较低 大模型训练
TF32(Tensor Float) 19-bit(存储为32) ~6–7位 ≈ FP32 速度快(GPU优化) 精度略低于FP32 GPU训练(如NVIDIA)
INT8(量化) 8-bit 整数(无小数) 很小 极省内存、推理快 精度下降明显 模型推理/部署

浮点数位宽组成

类型 总位宽 符号位(Sign) 指数位(Exponent)-决定“范围” 尾数位(Fraction)-决定“精度” 特点
FP16 16 1 5 10 精度低、范围小
FP32 32 1 8 23 平衡(默认)
FP64 64 1 11 52 高精度
BF16 16 1 8 7 范围≈FP32,但精度低
TF32 19(计算格式) 1 8 10 GPU内部格式
INT8 8 整数,无浮点结构

不同阶段推荐精度

  1. 当前主流

    👉 使用:混合精度训练(Mixed Precision)在 PyTorch 和 TensorFlow 中都是默认推荐方案。

    • 前向 / 反向:FP16 或 BF16

    • 参数更新:FP32

    FP16 计算得到的梯度通常是近似值,虽然精度有限,但通过转换为 FP32 进行累加和更新,可以避免这些小梯度在后续计算中被舍弃或变为 0。

    优点:显存 ↓(约一半),速度 ↑(GPU加速),精度基本不变

  2. 大模型训练(LLM / Transformer)

    👉 使用:BF16

    原因:指数位多 → 不容易梯度爆炸/消失,比 FP16 更稳定

  3. GPU特定优化

    👉 使用:TF32(自动)

    在 NVIDIA GPU 上:FP32 计算常自动用 TF32 加速,用户通常不用手动管

模型训练相关

.detach() 作用

1
2
3
4
5
6
7
8
9
10
11
features = encoder(x)          # encoder前向,生成特征
output = task(features) # task用特征做预测
loss = loss_fn(output, target) # 计算损失
loss.backward() # 反向传播,默认梯度会传到encoder和task参数

# 但如果
features = encoder(x).detach() # 断开连接
output = task(features)
loss = loss_fn(output, target)
loss.backward()
# 梯度只会传到task参数,encoder参数不变

hydra

配置组合(Configuration Composition)

Hydra 允许你将多个配置文件(YAML)组合起来,这种方式称为配置组合(config composition)。你可以用模块化的方式组织配置,从而提高可维护性和重用性。

1
2
3
4
5
6
7
8
# config.yaml
defaults:
- model: resnet
- dataset: imagenet

# config/model/resnet.yaml
hidden_size: 256
layers: 50

命令行覆盖配置

Hydra 允许你在命令行中覆盖配置参数,无需修改代码或配置文件本身。

1
python train.py model.hidden_size=512 dataset=mnist

动态“添加”一个新配置组

1
2
python demo_forcefield.py \
+experiment=downstream_task/forcefield/gelsight_dino \ # 这个参数会被Hydra当作一个配置 override, 动态“添加”一个配置组项,名为 experiment(在 defaults: 中没有声明过)

多运行支持(Multirun)

通过 -m 选项,Hydra 可以自动运行多个配置组合,用于超参数搜索等任务。

1
python train.py -m model.hidden_size=128,256,512

💡使用示例

1
2
3
4
5
6
7
8
9
10
import hydra
from omegaconf import DictConfig

# @hydra.main(config_path="config", config_name="default")
@hydra.main(version_base="1.3", config_path="config") # 1.3以上,如果主配置目录中只有一个.yaml文件(比如 default.yaml),可以省略 config_name,Hydra 会自动使用它。
def my_app(cfg: DictConfig):
print(cfg)

if __name__ == "__main__":
my_app()
1
2
3
4
5
6
7
8
9
配置文件结构:
config/
├── default.yaml
├── model/
│ ├── resnet.yaml
│ └── vgg.yaml
└── dataset/
├── mnist.yaml
└── imagenet.yaml

loss变成Nan一般会是什么原因?

1️⃣ 学习率过大

现象:前几步正常,突然 loss = NaN 或一路飙升然后 NaN

原因:参数更新一步跨太大,直接数值溢出

解决

  • lr除以 10 或 100
  • warmup
  • 梯度累计(缓解「小 batch + 大 lr」导致的 NaN)
  • Adam/AdamW 比 SGD 稳定
1
optimizer = AdamW(model.parameters(), lr=1e-4)

2️⃣ 梯度爆炸

现象:loss 先变大,再 NaN(RNN / Transformer / 深层网络更常见)

解决

  • 梯度裁剪(强烈推荐)
1
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
  • 减小 batch size
  • 用 LayerNorm / BatchNorm

3️⃣ 数值非法操作(log / div / sqrt)

典型雷点

1
2
3
4
torch.log(0)
torch.sqrt(负数)
x / 0
0^gamma

解决

  • epsilon
1
2
eps = 1e-8
loss = torch.log(x + eps)
  • 用 PyTorch 官方 loss(更稳)
1
nn.CrossEntropyLoss()  # 比自己写 softmax + log 稳

4️⃣ 输入数据本身有 NaN / Inf

排查

1
2
torch.isnan(x).any()
torch.isinf(x).any()

解决

  • 数据标准化前先清洗
  • 避免除 0
  • 缺失值填充

5️⃣ FP16 / 混合精度溢出

现象:用 AMP 时突然 NaN

解决

  • GradScaler
  • 或临时关掉 FP16 验证
1
scaler = torch.cuda.amp.GradScaler()

6️⃣ 初始化不当

现象:第一步 loss 就是 NaN

解决:用官方初始化

1
2
nn.init.xavier_uniform_(weight)
nn.init.kaiming_normal_(weight)

7️⃣ Loss 特有坑

  • CrossEntropyLoss:不要自己先 softmax
1
2
3
4
5
# ❌ 错误
loss = CE(softmax(logits), target)

# ✅ 正确
loss = CE(logits, target)
  • BCEWithLogitsLoss:输入必须是 logits,label ∈ {0,1}

8️⃣ 回归任务

  • target 量级过大 → MSE 爆炸先 → normalize target

SOMESOME

  1. Dropout调整:合理位置添加并调整Drop Rate,可有效提升模型性能。
  2. Softmax温度:在注意力、交叉熵分类、对比学习等涉及Softmax的场景中引入温度参数。
  3. Normalization:深度学习核心是表征学习,需通过归一化(如LLM的Pre-norm/Post-norm、多模态系统设计)确保分布对齐,避免深层网络或模块组合时的表征偏移。
  4. Batch Size与Learning Rate同步调整:Batch Size影响梯度估计准确性(类比下山方向),LR影响更新步长(下山速度),方向准确时可加快步长;微调预训练模型时可采用极低LR(如e-7)配合多轮训练。
  5. 学习率策略:采用Warmup+Cosine Decay组合。
  6. 正则化:基础正则化如weight decay可显著提升泛化能力。
  7. Label SmoothingHinge Loss:推荐使用,OAI深度学习的weak-to-strong generation基础实现可视为进阶Label Smoothing。
  8. 数据采样:排序任务中调整负样本采样策略是常用手段。
  9. 特征融合:优先用哈达马积而非拼接/相加,其能引入更多非线性并实现滤波效果。以上均有PyTorch原生API支持,实现简单。
  10. 表征分布:通过triplet loss、对比学习等提升内部表征质量,高质量表征可增强模型鲁棒性与泛化性,需针对模型特点关键位置表征。
  11. 加权平均设计:SE Layer等加权机制在多领域(不限于CV)效果显著。
  12. 多头结构扩展:借鉴多头注意力经验,扩展至其他模块可有效利用算力提升性能。
  13. Gating操作:受RWKV中token shift启发,非关键路径的Sigmoid门控有助于建模复杂依赖关系。

总结:深度学习核心tricks聚焦于表征分布与梯度流通,二者是模型的关键。

消融实验的一些小tips分享

第一步:优先尝试轻量改动,提升效果 先从 LayerScale、Stochastic Depth 和 Warmup Cosine 这三个方法入手。它们的改动幅度小,操作起来简单,而且效果稳定显著,基本都能带来性能提升,是性价比很高的优先选项。

第二步:进阶优化,补足效果 如果第一步的优化后效果还未达到预期,可以再加入 EMA进一步提升模型稳定性。要是还有提升空间,还可以尝试分层权重衰减,针对性地优化参数更新,让模型训练更精准。

第三步:最后调整,尝试换优化器 前面的方法都试过之后,如果效果还是不理想,最后再考虑更换优化器。这种调整相对复杂,建议放在最后尝试,避免前期因改动过大导致训练不稳定