深度学习基础
前言:此部分文章并非系统性的教学文章,网络上已经很多非常优秀的教学课程,顶尖且免费。比如,哔哩哔哩上李沐大神的《动手学深度学习 v2》,Andrej Karpathy 在 YouTube 上教程,以及 Standford CS224N 课程。 这里主要记录作者的一些理解,有意思的知识点,或者豁然开朗的乐趣,希望你也能喜欢!
机器学习基础中讲解了机器学习的基本流程和方法。本部分我们来介绍机器学习其中一个分支深度学习,它让神经网络算法焕发了第二春。
依旧采用 Pytorch 官方文档自带的深度学习示例代码。深度学习作为机器学习的一个分支,其示例代码同样包含了机器学习基础涉及的 5 个步骤:
- 数据准备
- 模型定义
- 损失函数
- 优化算法
- 模型训练
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets
from torchvision.transforms import ToTensor
# 超参数定义
learning_rate = 1e-3
batch_size = 64
epochs = 5
# 1. 数据准备,包括训练数据集和测试数据集
training_data = datasets.FashionMNIST(
root="data",
train=True,
download=True,
transform=ToTensor()
)
test_data = datasets.FashionMNIST(
root="data",
train=False,
download=True,
transform=ToTensor()
)
train_dataloader = DataLoader(training_data, batch_size=64)
test_dataloader = DataLoader(test_data, batch_size=64)
# 2. 模型定义
class NeuralNetwork(nn.Module):
def __init__(self):
super().__init__()
self.flatten = nn.Flatten()
self.linear_relu_stack = nn.Sequential(
nn.Linear(28*28, 512),
nn.ReLU(),
nn.Linear(512, 512),
nn.ReLU(),
nn.Linear(512, 10),
)
def forward(self, x):
x = self.flatten(x)
logits = self.linear_relu_stack(x)
return logits
model = NeuralNetwork()
# 3. 损失函数
# Initialize the loss function
loss_fn = nn.CrossEntropyLoss()
# 4. 优化算法
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)
# 5. 模型训练,包括测试检验
def train_loop(dataloader, model, loss_fn, optimizer):
size = len(dataloader.dataset)
# Set the model to training mode - important for batch normalization and dropout layers
# Unnecessary in this situation but added for best practices
model.train()
for batch, (X, y) in enumerate(dataloader):
# Compute prediction and loss
pred = model(X)
loss = loss_fn(pred, y)
# Backpropagation
loss.backward()
optimizer.step()
optimizer.zero_grad()
if batch % 100 == 0:
loss, current = loss.item(), batch * batch_size + len(X)
print(f"loss: {loss:>7f} [{current:>5d}/{size:>5d}]")
def test_loop(dataloader, model, loss_fn):
# Set the model to evaluation mode - important for batch normalization and dropout layers
# Unnecessary in this situation but added for best practices
model.eval()
size = len(dataloader.dataset)
num_batches = len(dataloader)
test_loss, correct = 0, 0
# Evaluating the model with torch.no_grad() ensures that no gradients are computed during test mode
# also serves to reduce unnecessary gradient computations and memory usage for tensors with requires_grad=True
with torch.no_grad():
for X, y in dataloader:
pred = model(X)
test_loss += loss_fn(pred, y).item()
correct += (pred.argmax(1) == y).type(torch.float).sum().item()
test_loss /= num_batches
correct /= size
print(f"Test Error: \n Accuracy: {(100*correct):>0.1f}%, Avg loss: {test_loss:>8f} \n")
for t in range(epochs):
print(f"Epoch {t+1}\n-------------------------------")
train_loop(train_dataloader, model, loss_fn, optimizer)
test_loop(test_dataloader, model, loss_fn)
print("Done!")
上述 5 个步骤已经通过代码注释的形式标注出来,本文将从另一个角度,借助 Pytorch 来介绍深度学习的相关概念和方法。
虽然从数据集的收集、清洗、整理,到数据集用于训练和测试的划分,以及小批量随机梯度下降算法中对批量大小的选择,不仅需要投入大量的工作和精力,也需要不同理论和算法的支撑。但这部分不是本文重点,感兴趣的同学可以找一本机器学习或者数据挖掘的书籍,一般而言书的前几章会对数据处理方法、理论做详细说明。
示例代码的数据准备部分,使用了 Pytorch datasets 模块提供的预置数据集,不过多展开。
接下来,我们重点关注模型定义模块,以及 Pytorch 框架的实现。
神经网络(pytorch.nn)
深度学习就是一种神经网络算法,只是第一个发表相关文章的作者取了一个好听的名字 — 深度学习。顾名思义,深度学习是网络层数很 “深” 的神经网络。
Pytorch 框架中神经网络相关的组件抽象放在 torch.nn 包中,nn 即 neural network。
Module & Parameter
Module 和 Parameter 是 torch.nn 包最基本,最核心的抽象。
Module
Pytorch 中所有神经网络的模块必须继承自 Module。示例代码定义的 NeuralNetwork 就是一个 Module。
Module 是一个嵌套结构,nn.Flatten 和 nn.Sequential 也是 pytorch 中预定义的通用组件,也叫子模块。既然是嵌套结构,自定义的 NeuralNetwork 也可以作为子模块供其他 Module 使用。
Pytorch 定义大量的通用组件,比如 Linear、Embedding、LayerNorm、ReLU 等。但对于当下有大量资金加持,并且潜力巨大的技术方向,各种理论和尝试更是层出不穷,日新月异。比如当下普遍采用的 RotaryEmbedding 和 RMSNorm 就需要自定义。
在自定义 Module 时,需要注意以下几点:
- 必须继承自 nn.Module。
- 在构造函数 __init__ 中,必须首先调用 super.__init__ 方法,进行模块初始化。
- forward 方法定义了模块中各组件的连接算法。Python 小知识:调用实例化的 python 对象,比如 model (X),实际调用的是 __call__ 方法,Module 实现了 __call__ 方法,会进一步调用用户实现的 forward 方法。 __init__ 完成模型构造,forward 方法定义算法逻辑。
进一步推进聚焦,分析 Module 类中的 __setattr__ 方法。如果你对 Python 有一定了解,就知道__setattr__ 是一个特殊的 Python 方法,它重载了赋值语句。例如,o.a = 1,等价于 o.__setattr__(‘a’, 1)。
def __setattr__(self, name: str, value: Union[Tensor, 'Module']) -> None:
# ...... 省略非关键代码
params = self.__dict__.get('_parameters')
if isinstance(value, Parameter):
# ...
remove_from(self.__dict__, self._buffers, self._modules, self._non_persistent_buffers_set)
self.register_parameter(name, value)
elif params is not None and name in params:
# ...
self.register_parameter(name, value)
else:
modules = self.__dict__.get('_modules')
if isinstance(value, Module):
# ...
remove_from(self.__dict__, self._parameters, self._buffers, self._non_persistent_buffers_set)
for hook in _global_module_registration_hooks.values():
output = hook(self, name, value)
if output is not None:
value = output
modules[name] = value
elif modules is not None and name in modules:
# ...
for hook in _global_module_registration_hooks.values():
output = hook(self, name, value)
if output is not None:
value = output
modules[name] = value
else:
buffers = self.__dict__.get('_buffers')
if buffers is not None and name in buffers:
for hook in _global_buffer_registration_hooks.values():
output = hook(self, name, value)
if output is not None:
value = output
buffers[name] = value
else:
super().__setattr__(name, value)
# ......
因篇幅原因,上述源代码做了裁剪,保留了核心代码,核心逻辑如下:
- Module 会用以下几个 dict 来维护三种类型的属性,
- _parameters,用于维护 Parameter,其实是一类 Tensor,后续会详细分析。
- _modules,用于维护子 Module,形成 Module 的嵌套结构。
- _buffers,用于维护 Tensor,这部分 Tensor (s) 并不是模型定义的 Parameter,往往是计算过程中的中间变量。用户可以在 register_buffer 时,通过 persistent 入参来控制其可持久性。
- 如果是这三种类型,会被设置到对应的 dict 中。
- 如果这三种类型的变量已经存在,那么对于的 value 必须是已存在变量对应的类型,或者为 None。
- 如果不是这三种类型中的一种,那么就如普通赋值语句一样,调用 super ().__setattr__ 方法。
理解这部分源码,对于我们后续看懂 Llama 等开源大语言模型非常有用。所谓深度学习,或者 Transformer 结构,它们的深度大部分通过重复同一个子模型结构达到。该重复的子模型,不能放在一个数组中,如此无法被上层的模型所感知,需要放到 Pytorch 特定的模型容器(比如 ModuleList)中才能达到相应的效果。上述代码就很好的帮我们回答了这个问题 。
Parameter
从 parameter.pyi 接口文件的定义可知,Parameter 继承自 Tensor,
class Parameter(Tensor):
def __init__(
self,
data: Tensor = ...,
requires_grad: builtins.bool = ...,
): ...
不直接使用 Tensor 的原因,正如 Module 一节提到的,用于区分模型结构(_parameters 维护)和中间过程(_buffers 维护)。
模型容器
在定义模型结构时,往往需要对某部分子模块做进一步的抽象封装,或者需要重复某个子模块,需要使用容器来承载。
而在 Module 类中,Pytorch 框架通过重写了 __setattr__ 方法来区分模型结构中的各个模块(Module)以及变量(Parameter)。因此不能使用 Python 原生的容器来封装 Module 和 Parameter。但它们之间具有一一对应关系:
- Sequential — 容纳 Module 的 tuple。
- ModuleList — 容纳 Module 的 list。
- ModuleDict — 容纳 Module 的 dict。
- ParameterList — 容纳 Parameter 的 list。
- ParameterDict — 容纳 Parameter 的 dict。
示例代码中将几个线性层与 ReLU 操作定义成一个 Sequential,进行前向推理调用 forward 方法时,就只需要调用这个 Sequential 即可,计算自动会在容器内流转。
预置模型
除了核心抽象 Module 和 Parameter,以及它们对应的容器外,Pytorch 框架为神经网络算法预置了大量通用模块,放在 torch.nn 包空间中。简单分类如下(相见 TORCH.NN):
- 线性层,Linear Layers,是神经网络算法中最基本的结构,输入层、输出层、隐藏层都是指此结构。
- CNN(卷积神经网络相关),包括 Convolution Layers、Pooling layers、Padding Layers,主要用于处理图片、视频等。
- RNN(循环神经网络相关),包括 Recurrent Layers、Transformer Layers,前期主要用于处理自然语言,现在 Transformer 结构已经成了一个事实的多模态模型标准。
- 归一化算法,Normalization Layers,主要有 LayerNorm 和 BatchNorm,以及现在大语言模型中使用较多的 RMSNorm。RMSNorm 是 LayerNorm 的一个变种算法,Pytorch 暂未实现。它是一类正则化技术,降低对数据分布的敏感性,提升模型稳定性,也有加速计算、缓解过拟合的作用。
- Embedding 层,Sparse Layers,Pytorch 把 Embedding 归类到稀疏矩阵。对于稀疏矩阵只是个数据表示的概念问题,不多做介绍。Embedding 会将自然语言 tokenizer 后的 token(一般是一个数字)映射成高纬度的 Tensor。在模型训练完后 token 到 Tensor 的映射关系固定下来,因此 Embedding 可以单独使用,可以用来做文档内容相似度的匹配。
- 激活层,Non-linear Activations,常用的有 ReLU、SiLU、Tanh 等非线性激活函数。因为作为神经网络的主体的线性层,它们往往通过全连接的形式交织在一起,都是线性的。因此需要通过非线性的激活层引入神经网络模型的非线性。
- Dropout 层,和 normalization 一样,也是一类正则化技术。主要用于模型训练过程,随机废弃一部分神经网络节点(一般是输入层和隐藏层),从而提高模型的泛化能力,避免过拟合。
优化算法
优化算法独立于神经网络模型结构存在,放在 torch.optim 包空间下,用于训练阶段找到一组最优的模型参数,使得模型在给定数据集上最小化预测误差,达到最好的模型性能。
优化算法的核心是梯度下降,在此基础上演化出很多算法,Pytorch 都有相关实现,现在使用最广的还属 Adam 及其变种 Adamw。以下对主要几个算法做个简单总结,
- 随机梯度下降和小批量随机梯度下降,解决计算复杂度以及与现代 GPU 硬件的配合问题,提升计算效率。
- Momentum、RMSProp 和 AdaGrad 算法,从不同方面对学习率进行调节。
- Adam 则将上述算法技术汇总到一个算法中,也是现今使用最广泛的优化算法。
Pytorch 框架为深度学习封装、抽象了基本的 Tensor、Module、Parameter 及其相关容器外,提供了深度学习的常用组件实现。比如损失函数,CrossEntropyLoss;优化算法,SGD 和 Adam 等;激活函数,ReLU;线性层,Linear。