开发过程中有些细节容易被忽略,今天挑几个重点聊一聊。
🔥承渊政道:个人主页
❄️个人专栏: 《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工具箱使用详解
- 1.1使用torch.utils.data. Dataset封装自定义数据集
- 1.2改变数据类型的Dataset类中的transform的使用
- 1.3批量输出数据的DataLoader类详解
- 4.1Dataset返回结构不稳定
- 4.2transform 改了类型但没改维度
- 4.3单样本训练导致效率低或 loss 计算异常
- 4.4文本padding和labels没对齐
- 4.5TensorBoard 日志目录混乱
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中.
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 像一套批量装载系统,把零散样本打乱、分组,并尽可能并行送到计算设备.
单独使用 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观察梯度稳定性梯度爆炸或长期接近 0tokens_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_ids、attention_mask、labels 必须同步 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记录模型结构和训练过程; - 用可视化曲线把训练从黑盒变成可诊断系统.
因此,学习 PyTorch 和 ChatGLM 微调时,建议先把这条数据与可视化链路吃透.它会直接决定后续实验是否可复现、训练是否高效、问题是否能被快速定位.
🚀真正的勇者不是流泪的人,而是含泪奔跑的人!
敬请期待下一篇文章内容
每日心灵鸡汤: 真正的魅力,是稳定、克制和清醒!
为什么人会瞬间失去魅力?因为真正的魅力,从来不是长相、口才或者表面的热情,而是一个人内在状态的稳定感.很多人一开始相处会让人觉得不错,可一旦深入接触,就会慢慢暴露出内在的不成熟.比如总喜欢诉苦,把别人当成情绪垃圾桶;过度解释自己,拼命在意别人怎么看;喜欢炫耀,本质上却是内心缺乏安全感;情绪一上来就失控,说话不过脑;喜欢插话、追问隐私、刚认识就把所有秘密全盘托出,这些其实都在暴露一个问题,是缺乏分寸感.而最容易让人失去好感的,还不是情绪,而是没有自己的判断.别人说什么就信什么,听风是风,听雨是雨,没有独立思考,也没有真正稳定的价值体系.所以很多时候,一个人失去魅力,并不是因为"不够优秀",而是因为内在太混乱.真正有魅力的人,往往都有一种共同点:情绪稳定,但不冷漠;有边界感,但不疏离;有表达欲,但不过度倾诉;有自信,但不需要靠炫耀证明自己.他们不一定最耀眼,却会让人相处得很舒服.因为人与人相处到最后,比的从来不是谁更会表现,而是谁更稳定、克制、清醒.
今天的内容大概就这些,实际开发中大家还会遇到更多细节,欢迎留言分享自己的经验。
评论 (0)
暂无评论