跳到主要内容
SecondHome

深度学习基础

约 23 分钟原创LLM 基础#tutorial#pytorch#deep-learning#llm

前言:此部分文章并非系统性的教学文章,网络上已经很多非常优秀的教学课程,顶尖且免费。比如,哔哩哔哩上李沐大神的《动手学深度学习 v2》,Andrej Karpathy 在 YouTube 上教程,以及 Standford CS224N 课程。 这里主要记录作者的一些理解,有意思的知识点,或者豁然开朗的乐趣,希望你也能喜欢!

机器学习基础中讲解了机器学习的基本流程和方法。本部分我们来介绍机器学习其中一个分支深度学习,它让神经网络算法焕发了第二春。

依旧采用 Pytorch 官方文档自带的深度学习示例代码。深度学习作为机器学习的一个分支,其示例代码同样包含了机器学习基础涉及的 5 个步骤:

  1. 数据准备
  2. 模型定义
  3. 损失函数
  4. 优化算法
  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 时,需要注意以下几点:

  1. 必须继承自 nn.Module。
  2. 在构造函数 __init__ 中,必须首先调用 super.__init__ 方法,进行模块初始化。
  3. 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)
	# ......

因篇幅原因,上述源代码做了裁剪,保留了核心代码,核心逻辑如下:

  1. Module 会用以下几个 dict 来维护三种类型的属性,
    • _parameters,用于维护 Parameter,其实是一类 Tensor,后续会详细分析。
    • _modules,用于维护子 Module,形成 Module 的嵌套结构。
    • _buffers,用于维护 Tensor,这部分 Tensor (s) 并不是模型定义的 Parameter,往往是计算过程中的中间变量。用户可以在 register_buffer 时,通过 persistent 入参来控制其可持久性。
  2. 如果是这三种类型,会被设置到对应的 dict 中。
  3. 如果这三种类型的变量已经存在,那么对于的 value 必须是已存在变量对应的类型,或者为 None。
  4. 如果不是这三种类型中的一种,那么就如普通赋值语句一样,调用 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。但它们之间具有一一对应关系:

  1. Sequential — 容纳 Module 的 tuple。
  2. ModuleList — 容纳 Module 的 list。
  3. ModuleDict — 容纳 Module 的 dict。
  4. ParameterList — 容纳 Parameter 的 list。
  5. ParameterDict — 容纳 Parameter 的 dict。

示例代码中将几个线性层与 ReLU 操作定义成一个 Sequential,进行前向推理调用 forward 方法时,就只需要调用这个 Sequential 即可,计算自动会在容器内流转。

预置模型

除了核心抽象 Module 和 Parameter,以及它们对应的容器外,Pytorch 框架为神经网络算法预置了大量通用模块,放在 torch.nn 包空间中。简单分类如下(相见 TORCH.NN):

  1. 线性层,Linear Layers,是神经网络算法中最基本的结构,输入层、输出层、隐藏层都是指此结构。
  2. CNN(卷积神经网络相关),包括 Convolution Layers、Pooling layers、Padding Layers,主要用于处理图片、视频等。
  3. RNN(循环神经网络相关),包括 Recurrent Layers、Transformer Layers,前期主要用于处理自然语言,现在 Transformer 结构已经成了一个事实的多模态模型标准。
  4. 归一化算法,Normalization Layers,主要有 LayerNorm 和 BatchNorm,以及现在大语言模型中使用较多的 RMSNorm。RMSNorm 是 LayerNorm 的一个变种算法,Pytorch 暂未实现。它是一类正则化技术,降低对数据分布的敏感性,提升模型稳定性,也有加速计算、缓解过拟合的作用。
  5. Embedding 层,Sparse Layers,Pytorch 把 Embedding 归类到稀疏矩阵。对于稀疏矩阵只是个数据表示的概念问题,不多做介绍。Embedding 会将自然语言 tokenizer 后的 token(一般是一个数字)映射成高纬度的 Tensor。在模型训练完后 token 到 Tensor 的映射关系固定下来,因此 Embedding 可以单独使用,可以用来做文档内容相似度的匹配。
  6. 激活层,Non-linear Activations,常用的有 ReLU、SiLU、Tanh 等非线性激活函数。因为作为神经网络的主体的线性层,它们往往通过全连接的形式交织在一起,都是线性的。因此需要通过非线性的激活层引入神经网络模型的非线性。
  7. Dropout 层,和 normalization 一样,也是一类正则化技术。主要用于模型训练过程,随机废弃一部分神经网络节点(一般是输入层和隐藏层),从而提高模型的泛化能力,避免过拟合。

优化算法

优化算法独立于神经网络模型结构存在,放在 torch.optim 包空间下,用于训练阶段找到一组最优的模型参数,使得模型在给定数据集上最小化预测误差,达到最好的模型性能。

优化算法的核心是梯度下降,在此基础上演化出很多算法,Pytorch 都有相关实现,现在使用最广的还属 Adam 及其变种 Adamw。以下对主要几个算法做个简单总结,

  • 随机梯度下降和小批量随机梯度下降,解决计算复杂度以及与现代 GPU 硬件的配合问题,提升计算效率。
  • Momentum、RMSProp 和 AdaGrad 算法,从不同方面对学习率进行调节。
  • Adam 则将上述算法技术汇总到一个算法中,也是现今使用最广泛的优化算法。

Pytorch 框架为深度学习封装、抽象了基本的 Tensor、Module、Parameter 及其相关容器外,提供了深度学习的常用组件实现。比如损失函数,CrossEntropyLoss;优化算法,SGD 和 Adam 等;激活函数,ReLU;线性层,Linear。

相关阅读

持续耕种,欢迎订阅 🌱

RSS全站 ·原创 ·策展 ·雷达;公众号「炼金术」同步更新。