关注

【从零开始大模型开发与微调:基于PyTorch与ChatGLM】(从数据管道到训练驾驶舱-PyTorch数据处理与模型可视化展示)

🔥承渊政道:个人主页

❄️个人专栏: 《C语言基础语法知识》 《数据结构与算法》 《C++知识内容》 《Linux系统知识》 《算法刷题指南》 《测评文章活动推广》 《大模型语言路线学习》 《MySQL数据库学习》 《Python知识内容》

✨逆境不吐心中苦,顺境不忘来时路!✨
🎬 博主简介:

前面带领大家完成了基于PyTorch 2.0模型与训练方面的学习,相信已经可以较好地完成一定基础的深度学习应用项目.大家也可能感觉到在前期的学习中,更多的是对PyTorch 2.0模型本身的了解,而对其他部分介绍较少.特别是数据处理部分,一直使用NumPy计算包对数据进行处理,因此缺乏一个贴合PyTorch自身的数据处理器.针对这个问题,PyTorch在2.0版本中为我们提供了专门的数据下载和数据处理包,集中在torch.utils.data这个工具包中,使用该包中的数据处理工具可以极大地提高开发效率及质量,帮助提高使用者在数据预处理,数据加载模块的边界与效率,如下图所示.可以看到,下图展示的是基于PyTorch 2.0的数据处理工具箱总体框架,主要由以下3部分构成:Dataset:一个抽象类,其他数据需要继承这个类,并且覆写其中的两个方法__getitem__和__len__.DataLoader:定义一个新的迭代器,实现批量读取、打乱数据以及提供并行加速等功能.Sample:提供多种采样方法的函数.下面我们将基于PyTorch 2.0的工具箱依次对其进行讲解.




1.用于自定义数据集的torch.utils.data工具箱使用详解

开头提到torch.utils.data工具箱中提供了3个类用于对数据进行处理和采样,但是Dataset在输出
时每次只能输出一个样本,而DataLoader可以弥补这一缺陷,实现批量乱序输出样本,如下图所示.

DataLoader

1.1使用torch.utils.data. Dataset封装自定义数据集

在PyTorch 2.0中,数据集的自定义使用需要继承torch.utils.data.Dataset类,之后实现其中的__getitem__、__len__方法.基本的Dataset类架构如下:

class Dataset():
    def __init__(self, transform=None): 
        super(Dataset, self).__init__()

    def __getitem__(self, index):
        pass

    def __len__(self):
        pass

可以很清楚地看到,Dataset除了基本的init函数外,还需要填充两个额外的函数:getitem__与__len.这是仿照Python中数据list的写法对其进行定义,其使用方法如下:

data = Customer(Dataset)[index]     #打印出index序号对应的数据
length = len(Customer(Dataset))     #打印出数据集总行数

下面以前面文章中一直使用的MNIST数据集为例进行介绍.
1.init的初始化方法
在对数据进行输出之前,首先将数据加载到Dataset这个类中,加载的方法直接按数据读取的方案使用NumPy进行载入.当然,也可以使用任何对数据读取的技术获取数据本身.在这里,所使用的数据读取代码如下:

def __init__(self, transform=None):      
     super(MNIST_Dataset, self).__init__()
     # 载入数据
     self.x_train = np.load("../dataset/mnist/x_train.npy")
     self.y_train_label = np.load("../dataset/mnist/y_train_label.npy")

2.__getitem__与__len__方法
首先是对数据的获取方式,__getitem__是Dataset父类中内置的数据迭代输出的方法.在这里,我们只需要显式地提供此方法的实现即可,代码如下:

def __getitem__(self, item):
     image =  (self.x_train[item])
     label =  (self.y_train_label[item])
     return image,label

而__len__方法用于获取数据的长度,在这里直接返回标签的长度即可,代码如下:

def __len__(self):
     return len(self.y_train_label)

完整的自定义MNIST_Dataset数据输出代码如下:

class MNIST_Dataset(torch.utils.data.Dataset):
    def __init__(self):
        super(MNIST_Dataset, self).__init__()
        # 载入数据
        self.x_train = np.load("../dataset/mnist/x_train.npy")
        self.y_train_label = np.load("../dataset/mnist/y_train_label.npy")

    def __getitem__(self, item):
        image = self.x_train[item]
        label = self.y_train_label[item]
        return image, label

    def __len__(self):
        return len(self.y_train_label)

大家可以将上面代码中定义的MNIST_Dataset类作为模板尝试更多的自定义数据集.

Dataset:给数据集建立一个清晰契约

Dataset 是 PyTorch 数据体系的底层抽象.它不关心你读取的是图片、文本、音频还是表格,只要求你回答两个问题:

  1. 这个数据集一共有多少条样本?
  2. 给我一个索引,我能拿到对应的样本吗?

这两个问题分别对应 __len____getitem__.

from torch.utils.data import Dataset


class MyDataset(Dataset):
    def __init__(self, samples, transform=None):
        self.samples = samples
        self.transform = transform

    def __len__(self):
        return len(self.samples)

    def __getitem__(self, index):
        sample = self.samples[index]
        if self.transform is not None:
            sample = self.transform(sample)
        return sample

这个形式看起来很简单,但它背后的工程意义很强:Dataset 把"数据从哪里来"和"模型怎么训练"解耦了.模型训练循环不需要知道数据在CSV、JSONL、数据库还是图片文件夹里,只要 Dataset 能稳定返回样本即可.

在MNIST 场景里,一个样本通常是"图片 + 标签".到了大模型微调里,一个样本可能变成:

{
    "input_ids": [...],
    "attention_mask": [...],
    "labels": [...]
}

也就是说,Dataset 的本质不是"读图片的类",而是"样本协议的封装器".你把协议设计清楚,后面的 transform、batch、训练循环才会清楚.


1.2改变数据类型的Dataset类中的transform的使用

我们获取的输入数据对于PyTorch 2.0来说并不能够直接使用,因此最少需要一种转换的方法,将初始化载入的数据转化成我们所需要的样式.

1.将自定义载入的参数转化为PyTorch 2.0专用的tensor类
这一步的编写方法很简单,我们只需要额外提供对于输入输出类的处理方法即可,代码如下:

class ToTensor:
    def __call__(self, inputs, targets):  # 可调用对象
        return torch.tensor(inputs), torch.tensor(targets)

这里我们所提供的ToTensor类的作用是对输入的数据进行调整.需要注意的是,这个类的输入输出数据结构和类型需要与自定义Dataset类中的def getitem()方法的数据结构和类型相一致.

2.新的自定义的Dataset类
对于原本的自定义数据Dataset类的定义,需要对其做出修正,新的数据读取类的定义如下:

class MNIST_Dataset(torch.utils.data.Dataset):
    def __init__(self, transform=None):  # 在定义时需要定义transform的参数
        super(MNIST_Dataset, self).__init__()
        # 载入数据
        self.x_train = np.load("../dataset/mnist/x_train.npy")
        self.y_train_label = np.load("../dataset/mnist/y_train_label.npy")

        self.transform = transform  # 需要通过地址模式提供transform类

    def __getitem__(self, index):
        image = self.x_train[index]
        label = self.y_train_label[index]

        # 通过判断transform类的存在对其进行调用
        if self.transform:
            image, label = self.transform(image, label)

        return image, label

    def __len__(self):
        return len(self.y_train_label)

在这里大家需要显式地在MNIST_Dataset类中提供transform的定义、具体使用位置和操作.因此,在这里特别注意,我们自己定义的transform类需要与getitem函数的输出结构相一致.
完整的带有transform的自定义MNIST_Dataset类使用如下:

import numpy as np
import torch


class ToTensor:
    def __call__(self, inputs, targets):  # 可调用对象
        return torch.tensor(inputs), torch.tensor(targets)


class MNIST_Dataset(torch.utils.data.Dataset):
    def __init__(self, transform=None):  # 在定义时需要定义transform的参数
        super(MNIST_Dataset, self).__init__()
        # 载入数据
        self.x_train = np.load("../dataset/mnist/x_train.npy")
        self.y_train_label = np.load("../dataset/mnist/y_train_label.npy")

        self.transform = transform  # 需要显式地提供transform类

    def __getitem__(self, index):
        image = self.x_train[index]
        label = self.y_train_label[index]

        # 通过判定transform类的存在对其进行调用
        if self.transform:
            image, label = self.transform(image, label)

        return image, label

    def __len__(self):
        return len(self.y_train_label)


mnist_dataset = MNIST_Dataset()
image, label = mnist_dataset[1024]
print(type(image), type(label))

print("----------------------------------")

mnist_dataset = MNIST_Dataset(transform=ToTensor())
image, label = mnist_dataset[1024]
print(type(image), type(label))

在这里我们做了尝试,对同一个MNIST_Dataset类做了无传入和有transform传入的比较,最终结果如下图所示.

无传入和有transform传入的比较

可以清楚地看到,对于传入后的数据,由于transform的存在,其数据结构有了很大的变化.

3.修正数据输出的维度
在transform类中,我们还可以进行更为复杂的操作,例如对维度进行转换,代码如下:

class ToTensor:
        def __call__(self, inputs, targets):   #可调用对象
           inputs = np.reshape(inputs,[28*28])
           return torch.tensor(inputs), torch.tensor(targets)

可以看到,我们根据输入大小的维度进行折叠操作,从而为后续的模型输出提供合适的数据维度格式.此时,大家可以使用如下方法打印出新的输出数据维度,代码如下:

mnist_dataset = MNIST_Dataset(transform=ToTensor())
     image,label = (mnist_dataset[1024])
     print(type(image), type(label))
     print(image.shape)

4.依旧无法使用自定义的数据对模型进行训练
当学到此部分时,一定信心满满地想将刚学习到的内容应用到我们的深度学习训练中.但是遗
憾的是,到目前为止,使用自定义数据集的模型还无法运行,这是由于PyTorch 2.0在效能方面以及损失函数的计算方式上对此进行了限制.大家可以先运行程序并参考本小节结尾的提示,尝试解决这个问题,我们在接下来也提供了一种PyTorch 2.0官方建议的解决方案.

# 注意下面这段代码无法正常使用,仅供演示
import numpy as np
import torch

# device = "cpu"       # PyTorch的特性,需要指定计算的硬件,如果没有GPU的存在,就使用CPU进行计算
device = "cuda"        # 在这里默认使用GPU,如果出现运行问题,可以将其改成CPU模式


class ToTensor:
    def __call__(self, inputs, targets):  # 可调用对象
        inputs = np.reshape(inputs, [1, -1])
        targets = np.reshape(targets, [1, -1])
        return torch.tensor(inputs), torch.tensor(targets)


# 注意下面这段代码无法正常使用,仅供演示
class MNIST_Dataset(torch.utils.data.Dataset):
    def __init__(self, transform=None):  # 在定义时需要定义transform的参数
        super(MNIST_Dataset, self).__init__()
        # 载入数据
        self.x_train = np.load("../dataset/mnist/x_train.npy")
        self.y_train_label = np.load("../dataset/mnist/y_train_label.npy")

        self.transform = transform  # 需要显式地提供transform类

    def __getitem__(self, index):
        image = self.x_train[index]
        label = self.y_train_label[index]

        # 通过判定transform类的存在对其进行调用
        if self.transform:
            image, label = self.transform(image, label)

        return image, label

    def __len__(self):
        return len(self.y_train_label)


# 注意下面这段代码无法正常使用,仅供演示
mnist_dataset = MNIST_Dataset(transform=ToTensor())

import os
os.environ["CUDA_VISIBLE_DEVICES"] = "0"  # 指定GPU编码
import torch
import numpy as np

batch_size = 320  # 设定每次训练的批次数
epochs = 1024     # 设定训练次数


# 设定的多层感知机网络模型
class NeuralNetwork(torch.nn.Module):
    def __init__(self):
        super(NeuralNetwork, self).__init__()
        self.flatten = torch.nn.Flatten()
        self.linear_relu_stack = torch.nn.Sequential(
            torch.nn.Linear(28 * 28, 312),
            torch.nn.ReLU(),
            torch.nn.Linear(312, 256),
            torch.nn.ReLU(),
            torch.nn.Linear(256, 10)
        )

    def forward(self, input):
        x = self.flatten(input)
        logits = self.linear_relu_stack(x)

        return logits


model = NeuralNetwork()
model = model.to(device)  # 将计算模型传入GPU硬件等待计算
torch.save(model, "./model.pth")
model = torch.compile(model)  # PyTorch 2.0的特性,加速计算速度
loss_fu = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=2e-5)  # 设定优化函数


# 注意下面这段代码无法正常使用,仅供演示
# 开始计算
for epoch in range(20):
    train_loss = 0

    for sample in mnist_dataset:
        image = sample[0]
        label = sample[1]

        train_image = image.to(device)
        train_label = label.to(device)

        pred = model(train_image)
        loss = loss_fu(pred, train_label)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        train_loss += loss.item()  # 记录每个批次的损失值

    # 计算并打印损失值
    train_loss /= len(mnist_dataset)
    print("epoch:", epoch, "train_loss:", round(train_loss, 2))

这段代码看起来没有问题,但是实际上在运行时会报错,这是由于数据在输出时是逐个输出的,模型在逐个数据计算损失函数时无法对其进行计算;同时,这样的计算方法会极大地限制PyTorch
2.0的计算性能.因此在此并不建议采用此方法直接对模型进行计算.

transform:把原始样本变成模型能理解的张量

一个关键细节:从文件或数组中读出来的数据,往往不能直接交给 PyTorch 模型.常见原因包括:

  • 类型不对:NumPy 数组、PIL 图片、Python list 需要转成 torch.Tensor;
  • 维度不对:图像可能需要从 [H, W] 变成 [C, H, W];
  • 数值范围不对:像素值可能需要归一化;
  • 语义结构不对:文本需要分词、截断、补齐、构造标签.

因此 transform 可以理解为数据进入模型前的"格式检查站".

import torch


class ToTensor:
    def __call__(self, sample):
        image, label = sample["image"], sample["label"]
        image = torch.tensor(image, dtype=torch.float32).unsqueeze(0) / 255.0
        label = torch.tensor(label, dtype=torch.long)
        return {"image": image, "label": label}

transform 的输入输出结构要与 __getitem__ 保持一致,这一点非常重要.如果 Dataset 返回 (image, label),transform 就应该处理这个结构;如果返回字典,transform 也应该返回训练循环能读懂的字典.

在大模型微调里,transform 往往就是 tokenizer 和标签构造逻辑:

class ChatTransform:
    def __init__(self, tokenizer, max_length=2048):
        self.tokenizer = tokenizer
        self.max_length = max_length

    def __call__(self, sample):
        prompt = sample["instruction"] + "\n" + sample["input"]
        answer = sample["output"]
        text = prompt + answer

        encoded = self.tokenizer(
            text,
            max_length=self.max_length,
            truncation=True,
            padding=False,
        )

        input_ids = encoded["input_ids"]
        attention_mask = encoded["attention_mask"]

        labels = input_ids.copy()
        prompt_len = len(self.tokenizer(prompt, truncation=True)["input_ids"])
        labels[:prompt_len] = [-100] * prompt_len

        return {
            "input_ids": input_ids,
            "attention_mask": attention_mask,
            "labels": labels,
        }

这里的 -100 是语言模型训练中常见的 ignore index,用来告诉损失函数:prompt 部分只是上下文,不应该作为答案被学习.这个例子说明,transform 不只是"类型转换",它也承载了训练目标的定义.


1.3批量输出数据的DataLoader类详解

下面讲解torch.utils.data工具箱中最后一个工具,即用于批量输出数据的DataLoader类.
首先需要说明的是,DataLoader可以解决使用Dataset自定义封装的数据时无法对数据进行批量化处理的问题,其用法非常简单,只需要将其包装在使用Dataset封装好的数据集外即可,代码如下:

mnist_dataset = MNIST_Dataset(transform=ToTensor())   #通过Dataset获取数据集
     from torch.utils.data import DataLoader               #导入DataLoader
     train_loader = DataLoader(mnist_dataset, batch_size=batch_size, shuffle=True) #包装已封装好的数据集

事实上使用起来就是这么简单,我们对DataLoader的使用,首先导入对应的包,然后用其包装封装好的数据即可.DataLoader的定义如下:

class DataLoader(object):
__initialized = False
        def __init__(self, dataset, batch_size=1, shuffle=False, sampler=None,
        def __setattr__(self, attr, val):
        def __iter__(self):
        def __len__(self):

与前面我们实现Dataset的不同之处在于:

  • 我们一般不需要自己实现DataLoader的方法,只需要在构造函数中指定相应的参数即可,比如常见的batch_size、shuffle等参数.所以使用DataLoader十分简洁方便,都是通过指定构造函数的参数来实现.
  • DataLoader实际上是一个较为高层的封装类,它的功能都是通过更底层的_DataLoader来完成的,这里就不再展开讲解了.DataLoaderIter就是_DataLoaderIter的一个框架,用来传给_DataLoaderIter一堆参数,并把自己装进DataLoaderIter中.

对于DataLoader的使用现在只介绍这么多.基于PyTorch2.0数据处理工具箱对数据进行识别和训练的完整代码如下:

import numpy as np
import torch

# device = "cpu"  # PyTorch的特性,需要指定计算的硬件,如果没有GPU的存在,就使用CPU进行计算
device = "cuda"   # 在这里默认使用GPU,如果出现运行问题,可以将其改成CPU模式


class ToTensor:
    def __call__(self, inputs, targets):  # 可调用对象
        inputs = np.reshape(inputs, [28 * 28])
        return torch.tensor(inputs), torch.tensor(targets)


class MNIST_Dataset(torch.utils.data.Dataset):
    def __init__(self, transform=None):  # 在定义时需要定义transform的参数
        super(MNIST_Dataset, self).__init__()

        # 载入数据
        self.x_train = np.load("../dataset/mnist/x_train.npy")
        self.y_train_label = np.load("../dataset/mnist/y_train_label.npy")

        self.transform = transform  # 需要显式地提供transform类

    def __getitem__(self, index):
        image = self.x_train[index]
        label = self.y_train_label[index]

        # 通过判定transform类的存在对其进行调用
        if self.transform:
            image, label = self.transform(image, label)

        return image, label

    def __len__(self):
        return len(self.y_train_label)


batch_size = 320  # 设定每次训练的批次数
epochs = 42       # 设定训练次数

mnist_dataset = MNIST_Dataset(transform=ToTensor())

from torch.utils.data import DataLoader

train_loader = DataLoader(mnist_dataset, batch_size=batch_size)


# 设定的多层感知机网络模型
class NeuralNetwork(torch.nn.Module):
    def __init__(self):
        super(NeuralNetwork, self).__init__()

        self.flatten = torch.nn.Flatten()
        self.linear_relu_stack = torch.nn.Sequential(
            torch.nn.Linear(28 * 28, 312),
            torch.nn.ReLU(),
            torch.nn.Linear(312, 256),
            torch.nn.ReLU(),
            torch.nn.Linear(256, 10)
        )

    def forward(self, input):
        x = self.flatten(input)
        logits = self.linear_relu_stack(x)
        return logits


model = NeuralNetwork()
model = model.to(device)  # 将计算模型传入GPU硬件等待计算

torch.save(model, "./model.pth")

model = torch.compile(model)  # PyTorch 2.0的特性,加速计算速度

loss_fu = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=2e-4)  # 设定优化函数


# 开始计算
for epoch in range(epochs):
    train_loss = 0

    for image, label in train_loader:
        train_image = image.to(device)
        train_label = label.to(device)

        pred = model(train_image)
        loss = loss_fu(pred, train_label)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        train_loss += loss.item()  # 记录每个批次的损失值

    # 计算并打印损失值
    train_loss = train_loss / batch_size
    print("epoch:", epoch, "train_loss:", round(train_loss, 2))


DataLoader:训练吞吐量的组织者

DataLoader 像一套批量装载系统,把零散样本打乱、分组,并尽可能并行送到计算设备.

单独使用 Dataset 每次只能取一个样本.这个设计适合描述数据集,却不适合直接训练.真实训练通常需要 mini-batch,因为GPU擅长并行矩阵计算,而不是一次处理一个样本.

DataLoader 的价值就在这里:它把样本组织成 batch,并提供打乱、并行加载、采样等能力.

from torch.utils.data import DataLoader


loader = DataLoader(
    dataset,
    batch_size=64,
    shuffle=True,
    num_workers=4,
    pin_memory=True,
)

for batch in loader:
    images = batch["image"].to(device)
    labels = batch["label"].to(device)
    logits = model(images)
    loss = criterion(logits, labels)

几个参数值得特别关注:

  • batch_size:每次送入模型的样本数量,影响显存、吞吐量和梯度估计稳定性.
  • shuffle:训练集通常需要打乱,验证集通常不打乱.
  • num_workers:使用多个子进程加载数据,减少 GPU 等数据的时间.
  • pin_memory:在使用 CUDA 时,固定页内存可以提升 CPU 到 GPU 的拷贝效率.
  • drop_last:当最后一个 batch 不满时是否丢弃,分布式训练中常会关注它.

大模型场景下,DataLoader 还有一个绕不开的角色:collate_fn.原因是文本样本长度不同,不能直接堆叠成张量,需要在 batch 级别动态 padding.

import torch


def collate_chat_batch(features, pad_token_id):
    max_len = max(len(item["input_ids"]) for item in features)

    input_ids, attention_mask, labels = [], [], []
    for item in features:
        pad_len = max_len - len(item["input_ids"])

        input_ids.append(item["input_ids"] + [pad_token_id] * pad_len)
        attention_mask.append(item["attention_mask"] + [0] * pad_len)
        labels.append(item["labels"] + [-100] * pad_len)

    return {
        "input_ids": torch.tensor(input_ids, dtype=torch.long),
        "attention_mask": torch.tensor(attention_mask, dtype=torch.long),
        "labels": torch.tensor(labels, dtype=torch.long),
    }

如果说 Dataset 定义了单个样本,collate_fn 就定义了"一批样本如何合并".这是很多 LLM 微调 bug 的高发区:padding token、attention mask、labels 三者只要有一个没对齐,loss 就可能异常.


2.实战:基于tensorboardX的训练可视化展示

前面带领大家完成了对于PyTorch 2.0中数据处理工具箱的使用,相信已经可以较好地对PyTorch2.0的数据进行处理.下面对PyTorch 2.0进行数据可视化.


2.1可视化组件tensorboardX的简介与安装

前面介绍了Netron的安装与使用,这是一种可视化PyTorch模型的方法,其优点是操作简单,可视性强.但是随之而来的是,Netron组件对模型的展示效果并不是非常准确,只能大致地展示出模型的组件与结构.tensorboardX就是专门为PyTorch 2.0进行模型展示与训练可视化的组件,可以记录模型训练过程的数字、图像等内容,以方便研究人员观察神经网络训练过程.
可以使用以下代码安装tensorboardX:

pip install tensorboardX

注意,这部分操作一定要在Anaconda或者Miniconda终端中进行,基于pip的安装和后续操作都是这样.


2.2tensorboardX可视化组件的使用

tensorboardX最重要的作用之一是对模型的展示,大家可以遵循以下步骤获得模型的展示效果.
1.存储模型的计算过程
首先使用tensorboardX模拟一次模型的运算过程,代码如下:

import torch
from tensorboardX import SummaryWriter


# 定义神经网络模型
class NeuralNetwork(torch.nn.Module):
    def __init__(self):
        super(NeuralNetwork, self).__init__()

        self.flatten = torch.nn.Flatten()

        self.linear_relu_stack = torch.nn.Sequential(
            torch.nn.Linear(28 * 28, 312),
            torch.nn.ReLU(),
            torch.nn.Linear(312, 256),
            torch.nn.ReLU(),
            torch.nn.Linear(256, 10)
        )

    def forward(self, input):
        x = self.flatten(input)
        logits = self.linear_relu_stack(x)
        return logits


# 创建模型
model = NeuralNetwork()

# 模拟输入数据
input_data = torch.rand(5, 784)

# 创建 TensorBoard 写入器
writer = SummaryWriter("./logs")

# 写入模型结构
writer.add_graph(model, input_data)

writer.close()

print("模型结构已写入 TensorBoard logs 文件夹")

可以看到,首先载入已设计好的模型,之后模拟输入数据,在载入tensorboardX并建立读写类之后,将模型带参数的运算过程加载到运行图中.

2.查看默认位置的logs文件夹
运行第1步的代码后,程序会在当前平行目录下生成一个新的logs目录s这是存储和记录模型展示的文件夹,如下图所示.

3.使用Anaconda或者Miniconda终端打开对应的目录
使用Anaconda或者Miniconda终端打开刚才生成的目录.
此时需要注意的是,我们在这里打开的是logs文件夹的上一级目录,而不是logs文件夹本身.
之后调用tensorboardX对模型进行展示,大家需要在刚才打开的文件夹中执行自己的命令.执行结果如下图所示.

可以看到,此时程序在执行,并提供了一个HTTP地址.至此,使用tensorboardX展示模型的步骤第
一阶段完成.

4.使用浏览器打开模型展示页面
查看模型展示页面,在这里使用google浏览器,大家也可以尝试不同的浏览器,这里只需要在地址栏中输入http://localhost:6006即可进入tensorboardX的本地展示页面,如下图所示.

可以看到,这是记录了模型的基本参数、输入输出以及基本模块的展示,之后大家可以双击模型主题部分,展开模型进行进一步的说明,如下图所示.更多操作建议自行尝试.


2.3tensorboardX对模型训练过程的展示

模型结构的展示是很重要的内容,而有的还希望了解模型在训练过程中出现的一些问题和参数
变化,tensorboardX同样提供了此功能,可以记录并展示模型在训练过程中损失值的变化,代码如下:

from tensorboardX import SummaryWriter
     writer = SummaryWriter()
     #开始计算
     for epoch in range(epochs):
     # 计算并打印损失值
     train_loss = train_loss/batch_size
     writer.add_scalars('evl', {'train_loss': train_loss}, epoch)
     writer.close()

这里可以看到,使用tensorboardX对训练过程的参数记录非常简单,直接记录损失过程即可,而epoch作为横坐标标记也被记录.完整的代码如下:

import numpy as np
import torch
from pathlib import Path
from torch.utils.data import DataLoader
from torchvision.datasets import MNIST
from tensorboardX import SummaryWriter


# 1. 设备设置
# Mac 先用 CPU,最稳定
device = torch.device("cpu")
print("当前使用设备:", device)


# 2. 数据路径
BASE_DIR = Path(__file__).resolve().parent
DATA_DIR = BASE_DIR / "dataset" / "mnist"

X_PATH = DATA_DIR / "x_train.npy"
Y_PATH = DATA_DIR / "y_train_label.npy"


def prepare_mnist_npy():
    DATA_DIR.mkdir(parents=True, exist_ok=True)

    if X_PATH.exists() and Y_PATH.exists():
        print("MNIST npy 文件已存在,直接加载。")
        return

    print("未找到 MNIST npy 文件,开始下载并创建...")

    mnist = MNIST(
        root=BASE_DIR / "dataset",
        train=True,
        download=True
    )

    x_train = mnist.data.numpy()
    y_train_label = mnist.targets.numpy()

    np.save(X_PATH, x_train)
    np.save(Y_PATH, y_train_label)

    print("MNIST npy 文件创建完成。")
    print("x_train.npy:", X_PATH)
    print("y_train_label.npy:", Y_PATH)


prepare_mnist_npy()


# 3. Transform
class ToTensor:
    def __call__(self, inputs, targets):
        inputs = np.reshape(inputs, [28 * 28])
        inputs = torch.tensor(inputs, dtype=torch.float32) / 255.0
        targets = torch.tensor(targets, dtype=torch.long)
        return inputs, targets


# 4. Dataset
class MNIST_Dataset(torch.utils.data.Dataset):
    def __init__(self, transform=None):
        super(MNIST_Dataset, self).__init__()

        self.x_train = np.load(X_PATH)
        self.y_train_label = np.load(Y_PATH)

        self.transform = transform

    def __getitem__(self, index):
        image = self.x_train[index]
        label = self.y_train_label[index]

        if self.transform:
            image, label = self.transform(image, label)

        return image, label

    def __len__(self):
        return len(self.y_train_label)


# 5. DataLoader
batch_size = 320
epochs = 320

mnist_dataset = MNIST_Dataset(transform=ToTensor())

train_loader = DataLoader(
    mnist_dataset,
    batch_size=batch_size,
    shuffle=True,
    num_workers=0
)


# 6. 模型
class NeuralNetwork(torch.nn.Module):
    def __init__(self):
        super(NeuralNetwork, self).__init__()

        self.flatten = torch.nn.Flatten()

        self.linear_relu_stack = torch.nn.Sequential(
            torch.nn.Linear(28 * 28, 312),
            torch.nn.ReLU(),
            torch.nn.Linear(312, 256),
            torch.nn.ReLU(),
            torch.nn.Linear(256, 10)
        )

    def forward(self, input):
        x = self.flatten(input)
        logits = self.linear_relu_stack(x)
        return logits


model = NeuralNetwork()
model = model.to(device)

loss_fu = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=2e-6)

writer = SummaryWriter("./logs")


# 7. 开始训练
for epoch in range(epochs):
    train_loss = 0

    for image, label in train_loader:
        train_image = image.to(device)
        train_label = label.to(device)

        pred = model(train_image)
        loss = loss_fu(pred, train_label)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        train_loss += loss.item()

    train_loss = train_loss / len(train_loader)

    print("epoch:", epoch, "train_loss:", round(train_loss, 4))

    writer.add_scalars("evl", {"train_loss": train_loss}, epoch)

writer.close()

torch.save(model.state_dict(), "./model.pth")
print("训练完成,模型已保存到 ./model.pth")

完成训练后,我们可以使用上一步的HTTP地址,此时单击TIMESERIES标签,对存储的模型变量进行验证,如下图所示.

这里记录了模型在训练过程中保存的损失值的变化,更多的模型训练过程参数值的展示请大家自行尝试.

tensorboardX:把训练过程从黑盒变成可观测系统

训练可视化的价值不是"好看",而是让我们及时发现 loss 发散、学习率异常、模型结构不符合预期等问题.

tensorboardX,它的核心作用是把训练过程写成事件日志,再通过浏览器查看.常见记录对象包括:

  • 模型计算图;
  • loss、accuracy、learning rate 等标量;
  • 图片、音频、文本等样本;
  • 参数分布和梯度分布;
  • embedding 投影.

基础用法如下:

from tensorboardX import SummaryWriter


writer = SummaryWriter(logdir="runs/exp_001")

for epoch in range(num_epochs):
    train_loss = train_one_epoch(model, train_loader)
    val_loss = evaluate(model, val_loader)

    writer.add_scalar("loss/train", train_loss, epoch)
    writer.add_scalar("loss/val", val_loss, epoch)

writer.close()

启动可视化服务:

tensorboard --logdir runs --port 6006

然后打开:

http://localhost:6006

我给大家演示了模型结构展示.这个能力对初学者尤其有用,因为它能帮助我们确认输入输出维度、模块连接关系是否符合预期.不过在复杂大模型中,完整计算图可能非常庞大,实践中更常记录 loss、learning rate、gradient norm、tokens per second、validation metric 等指标.


3.训练可视化应该看什么

只记录 loss 还不够.一个更工程化的训练面板可以包含:

指标作用异常信号
train/loss观察训练是否收敛长时间不降、突然爆炸
eval/loss观察泛化能力训练降而验证升,可能过拟合
learning_rate检查调度器warmup 或 decay 不符合预期
grad_norm观察梯度稳定性梯度爆炸或长期接近 0
tokens_per_second衡量吞吐量数据加载慢、GPU 利用率低
sample_length检查数据分布截断过多、padding 过多

对大模型微调来说,我建议至少记录这几类:

writer.add_scalar("train/loss", loss.item(), global_step)
writer.add_scalar("train/lr", scheduler.get_last_lr()[0], global_step)
writer.add_scalar("train/grad_norm", grad_norm, global_step)
writer.add_scalar("data/avg_seq_len", avg_seq_len, global_step)

很多训练问题表面上像"模型不行",实际上是数据管道问题.比如平均序列长度突然变短,可能说明数据清洗阶段截断过度;loss 一开始就很低,可能是 labels 大面积被 mask;吞吐量忽高忽低,可能是 DataLoader worker 或存储读取瓶颈.


4. 常见坑点与排查思路

4.1Dataset返回结构不稳定

错误示例:有的样本返回 tuple,有的样本返回 dict;有的样本缺少 label;有的样本字段名不同.这样会导致 DataLoader 在合并 batch 时失败.

建议:让 __getitem__ 永远返回同一种结构,并在初始化阶段做字段检查.


4.2transform 改了类型但没改维度

图像任务里常见 [H, W][C, H, W] 混淆;文本任务里常见 list 和 tensor 混用.类型转换不等于模型可用,还要确认维度语义.

建议:在训练前打印一个 batch 的 shape.

batch = next(iter(loader))
for key, value in batch.items():
    print(key, value.shape, value.dtype)

4.3单样本训练导致效率低或 loss 计算异常

特别说明了只靠 Dataset 逐个样本输出并不适合训练.深度学习训练通常需要 batch 化,否则性能差,也容易和损失函数期望的输入形状不一致.

建议:训练入口统一使用 DataLoader,即使实验很小也保留 batch 维度.


4.4文本padding和labels没对齐

LLM 微调里,input_idsattention_masklabels 必须同步 padding.padding 位置的 labels 通常应设为 -100,避免参与 loss.

建议:对一个 batch 解码前几条样本,人工检查 prompt、answer、label mask 是否符合预期.


4.5TensorBoard 日志目录混乱

如果多个实验都写到同一个目录,曲线会混在一起,难以分析.

建议:日志目录包含实验名、时间、关键超参.

writer = SummaryWriter("runs/chatglm_lora_bs8_lr2e-4_seed42")

5.总结

本文讲的不是一个孤立 API,而是一套 PyTorch 训练工程的基础范式:

  • Dataset 定义样本协议;
  • transform 完成类型、维度和训练目标转换;
  • DataLoader 完成 batch 化、打乱和并行加载;
  • tensorboardX 记录模型结构和训练过程;
  • 用可视化曲线把训练从黑盒变成可诊断系统.

如果把大模型训练比作一座工厂,模型结构只是核心机器,数据管道是原料供应链,TensorBoard 是控制室.供应链不稳定,机器再强也产不出可靠结果;控制室没有监控,训练过程出了问题也很难定位.

因此,学习 PyTorch 和 ChatGLM 微调时,建议先把这条数据与可视化链路吃透.它会直接决定后续实验是否可复现、训练是否高效、问题是否能被快速定位.


🚀真正的勇者不是流泪的人,而是含泪奔跑的人!

敬请期待下一篇文章内容


每日心灵鸡汤: 真正的魅力,是稳定、克制和清醒!

为什么人会瞬间失去魅力?因为真正的魅力,从来不是长相、口才或者表面的热情,而是一个人内在状态的稳定感.很多人一开始相处会让人觉得不错,可一旦深入接触,就会慢慢暴露出内在的不成熟.比如总喜欢诉苦,把别人当成情绪垃圾桶;过度解释自己,拼命在意别人怎么看;喜欢炫耀,本质上却是内心缺乏安全感;情绪一上来就失控,说话不过脑;喜欢插话、追问隐私、刚认识就把所有秘密全盘托出,这些其实都在暴露一个问题,是缺乏分寸感.而最容易让人失去好感的,还不是情绪,而是没有自己的判断.别人说什么就信什么,听风是风,听雨是雨,没有独立思考,也没有真正稳定的价值体系.所以很多时候,一个人失去魅力,并不是因为"不够优秀",而是因为内在太混乱.真正有魅力的人,往往都有一种共同点:情绪稳定,但不冷漠;有边界感,但不疏离;有表达欲,但不过度倾诉;有自信,但不需要靠炫耀证明自己.他们不一定最耀眼,却会让人相处得很舒服.因为人与人相处到最后,比的从来不是谁更会表现,而是谁更稳定、克制、清醒.

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/2401_87629362/article/details/162693156

文章来源crawl

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--