Softmax回归:从理论到实践全解析(2)
1. 数据读取与可视化
import os
import time
import torch
import torchvision
from torch.utils import data
from torchvision import transforms
import matplotlib.pyplot as plt
from matplotlib_inline import backend_inline
# --- 解决环境问题的配置 ---
os.environ['KMP_DUPLICATE_LIB_OK'] = 'True'
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
# --- 类和函数的定义 ---
class Timer:
"""一个简单的计时器类"""
def __init__(self):
self.start_time = time.time()
def stop(self):
return time.time() - self.start_time
def use_svg_display():
"""使用svg格式在Jupyter中显示绘图。"""
backend_inline.set_matplotlib_formats('svg')
def get_fashion_mnist_labels(labels):
"""将数字标签转换为文本标签。"""
text_labels = ['T恤', '裤子', '套头衫', '连衣裙', '外套',
'凉鞋', '衬衫', '运动鞋', '包', '短靴']
return [text_labels[int(i)] for i in labels]
def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5):
"""在一个网格中绘制多张图像。"""
# ... (函数内部代码不变)
figsize = (num_cols * scale, num_rows * scale)
_, axes = plt.subplots(num_rows, num_cols, figsize=figsize)
axes = axes.flatten()
for i, (ax, img) in enumerate(zip(axes, imgs)):
if torch.is_tensor(img):
ax.imshow(img.numpy())
else:
ax.imshow(img)
ax.axes.get_xaxis().set_visible(False)
ax.axes.get_yaxis().set_visible(False)
if titles:
ax.set_title(titles[i])
return axes
def get_dataloader_workers():
"""子进程数量"""
return 0
def load_data_fashion_mnist(batch_size, resize=None): #@save
"""下载Fashion-MNIST数据集,然后将其加载到内存中"""
trans = [transforms.ToTensor()]
if resize:
trans.insert(0, transforms.Resize(resize))
trans = transforms.Compose(trans)
mnist_train = torchvision.datasets.FashionMNIST(
root="../data", train=True, transform=trans, download=True)
mnist_test = torchvision.datasets.FashionMNIST(
root="../data", train=False, transform=trans, download=True)
return (data.DataLoader(mnist_train, batch_size, shuffle=True,
num_workers=get_dataloader_workers()),
data.DataLoader(mnist_test, batch_size, shuffle=False,
num_workers=get_dataloader_workers()))
if __name__ == '__main__':
# 1. 加载数据集
trans = transforms.ToTensor()
mnist_train = torchvision.datasets.FashionMNIST(
root="../data", train=True, transform=trans, download=True)
mnist_test = torchvision.datasets.FashionMNIST(
root="../data", train=False, transform=trans, download=True)
# 2. 打印数据集信息 (现在它只会被主进程打印一次)
print(f"训练集样本数: {len(mnist_train)}")
print(f"测试集样本数: {len(mnist_test)}")
print(f"单张图片的张量形状: {mnist_train[0][0].shape}")
# 3. 可视化一小批图像
batch_size_vis = 18
vis_iter = data.DataLoader(mnist_train, batch_size=batch_size_vis, shuffle=True)
X_vis, y_vis = next(iter(vis_iter))
show_images(X_vis.reshape(18, 28, 28), 2, 9, titles=get_fashion_mnist_labels(y_vis))
plt.show()
# 4. 测试数据加载速度
batch_size_test = 256
num_workers = get_dataloader_workers()
train_iter = data.DataLoader(mnist_train, batch_size_test, shuffle=True,
num_workers=num_workers)
print("\n--- 开始测试数据加载速度 ---")
timer = Timer()
for X, y in train_iter:
continue
print(f'使用 {num_workers} 个进程加载 {len(mnist_train)} 张图片总共花费: {timer.stop():.2f} 秒')
-
trans = transforms.ToTensor(): 创建一个ToTensor转换器的实例。它的作用是:- 将输入的 PIL 图像(Python Imaging Library 格式)或 NumPy 数组转换为 PyTorch 张量 (
torch.Tensor)。 - 将图像的像素值从
[0, 255]的整数范围,自动缩放到[0.0, 1.0]的浮点数范围。这是通过将每个像素值除以 255 实现的。数据归一化是神经网络训练中一个常见的预处理步骤。
- 将输入的 PIL 图像(Python Imaging Library 格式)或 NumPy 数组转换为 PyTorch 张量 (
-
mnist_train = torchvision.datasets.FashionMNIST(...): 加载 Fashion-MNIST 的训练数据集
1.root="../data": 指定数据集存放的目录。如果该目录下没有数据集,PyTorch 会自动下载。
2.train=True: 表示我们要加载的是训练集。
3.transform=trans: 告诉数据集加载器,对每一张读入的图片都要应用我们前面定义的trans转换,也就是将它变成归一化后的张量。
4.download=True: 如果在root目录下找不到数据集文件,就自动从网上下载。 -
mnist_test = torchvision.datasets.FashionMNIST(...): 以同样的方式加载测试数据集,唯一的区别是train=False。
运行代码得到
训练集样本数: 60000
测试集样本数: 10000
单张图片的张量形状: torch.Size([1, 28, 28])
— 开始测试数据加载速度 —
使用 4 个进程加载 60000 张图片总共花费: 7.48 秒
2. softmax回归的从零开始实现
# softmax_complex.py
import os
os.environ['KMP_DUPLICATE_LIB_OK'] = 'True'
import torch
import time
from IPython import display
from torchvision import transforms
import torchvision
from torch.utils import data
import matplotlib.pyplot as plt
from matplotlib_inline import backend_inline
from IPython import display
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
def get_dataloader_workers():
"""子进程数量"""
return 0
def load_data_fashion_mnist(batch_size, resize=None): # @save
"""下载Fashion-MNIST数据集,然后将其加载到内存中"""
trans = [transforms.ToTensor()]
if resize:
trans.insert(0, transforms.Resize(resize))
trans = transforms.Compose(trans)
mnist_train = torchvision.datasets.FashionMNIST(
root="../data", train=True, transform=trans, download=True)
mnist_test = torchvision.datasets.FashionMNIST(
root="../data", train=False, transform=trans, download=True)
return (data.DataLoader(mnist_train, batch_size, shuffle=True,
num_workers=get_dataloader_workers()),
data.DataLoader(mnist_test, batch_size, shuffle=False,
num_workers=get_dataloader_workers()))
def softmax(X):
X_exp = torch.exp(X)
partition = X_exp.sum(1, keepdim=True)
return X_exp / partition # 这里应用了广播机制
class Accumulator: # @save
"""在n个变量上累加"""
def __init__(self, n):
self.data = [0.0] * n
def add(self, *args):
self.data = [a + float(b) for a, b in zip(self.data, args)]
def reset(self):
self.data = [0.0] * len(self.data)
def __getitem__(self, idx):
return self.data[idx]
def cross_entropy(y_hat, y):
return - torch.log(y_hat[range(len(y_hat)), y])
def accuracy(y_hat, y): # @save
"""计算预测正确的数量"""
if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:
y_hat = y_hat.argmax(axis=1)
cmp = y_hat.type(y.dtype) == y
return float(cmp.type(y.dtype).sum())
def evaluate_accuracy(net, data_iter, device=None):
"""计算模型精度 (修改版,支持device)"""
if isinstance(net, torch.nn.Module):
net.eval()
metric = Accumulator(2)
with torch.no_grad():
for X, y in data_iter:
if device:
X, y = X.to(device), y.to(device)
metric.add(accuracy(net(X), y), y.numel())
return metric[0] / metric[1]
class Animator:
"""在动画中绘制数据。"""
def __init__(self, xlabel=None, ylabel=None, legend=None, xlim=None,
ylim=None, xscale='linear', yscale='linear',
fmts=('-', 'm--', 'g-.', 'r:'), nrows=1, ncols=1,
figsize=(3.5, 2.5)):
# 1. 替换 d2l.use_svg_display()
def use_svg_display():
"""使用svg格式在Jupyter中显示绘图。"""
backend_inline.set_matplotlib_formats('svg')
use_svg_display()
# 2. 替换 d2l.plt.subplots()
self.fig, self.axes = plt.subplots(nrows, ncols, figsize=figsize)
if nrows * ncols == 1:
# 如果只有一个子图,plt.subplots返回的不是列表,我们把它统一成列表
self.axes = [self.axes, ]
# 3. 替换 d2l.set_axes()
# 我们将 set_axes 的逻辑直接封装在 config_axes 方法中
self.xlabel = xlabel
self.ylabel = ylabel
self.legend = legend if legend is not None else []
self.xlim = xlim
self.ylim = ylim
self.xscale = xscale
self.yscale = yscale
self.fmts = fmts
# 定义一个内部方法来配置坐标轴
self._config_axes()
# 初始化数据存储
self.X, self.Y = None, None
def _config_axes(self):
"""配置坐标轴。"""
# 我们只配置第一个子图,因为 d2l 的 Animator 也是这样做的
ax = self.axes[0]
if self.xlabel:
ax.set_xlabel(self.xlabel)
if self.ylabel:
ax.set_ylabel(self.ylabel)
if self.xlim:
ax.set_xlim(self.xlim)
if self.ylim:
ax.set_ylim(self.ylim)
ax.set_xscale(self.xscale)
ax.set_yscale(self.yscale)
if self.legend:
ax.legend(self.legend)
ax.grid()
def add(self, x, y):
"""向图表中添加多个数据点。"""
# 确保 y 是一个列表或元组
if not hasattr(y, "__len__"):
y = [y]
n = len(y)
# 确保 x 是一个列表或元组,且长度与 y 相同
if not hasattr(x, "__len__"):
x = [x] * n
# 初始化数据存储列表
if not self.X:
self.X = [[] for _ in range(n)]
if not self.Y:
self.Y = [[] for _ in range(n)]
# 添加新的数据点
for i, (a, b) in enumerate(zip(x, y)):
if a is not None and b is not None:
self.X[i].append(a)
self.Y[i].append(b)
# --- 绘图部分 ---
ax = self.axes[0]
# 1. 清除当前子图上的旧内容
ax.cla()
# 2. 绘制所有线条
for x_data, y_data, fmt in zip(self.X, self.Y, self.fmts):
ax.plot(x_data, y_data, fmt)
# 3. 重新配置坐标轴的标签、范围等
self._config_axes()
# 4. 在Jupyter中显示和刷新图像
display.display(self.fig)
display.clear_output(wait=True)
def train_epoch_ch3(net, train_iter, loss, updater, device=None):
"""训练模型一个迭代周期 (修改版,支持device)"""
if isinstance(net, torch.nn.Module):
net.train()
metric = Accumulator(3)
for X, y in train_iter:
# 将数据移动到指定的设备上
if device:
X, y = X.to(device), y.to(device)
y_hat = net(X)
l = loss(y_hat, y)
if isinstance(updater, torch.optim.Optimizer):
updater.zero_grad()
l.mean().backward()
updater.step()
else:
l.sum().backward()
updater(X.shape[0])
metric.add(float(l.sum()), accuracy(y_hat, y), y.numel())
return metric[0] / metric[2], metric[1] / metric[2]
def sgd(params, lr, batch_size):
"""手动实现的小批量随机梯度下降(原地操作)。"""
with torch.no_grad():
for param in params:
# 原地更新参数
param -= lr * param.grad / batch_size
# 原地清空梯度
param.grad.zero_()
def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater, device=None):
"""训练模型 (修改版,支持device)"""
animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9],
legend=['train loss', 'train acc', 'test acc'])
for epoch in range(num_epochs):
# 将 device 参数传递下去
train_metrics = train_epoch_ch3(net, train_iter, loss, updater, device)
# evaluate_accuracy 也需要 device 参数
test_acc = evaluate_accuracy(net, test_iter, device)
animator.add(epoch + 1, train_metrics + (test_acc,))
train_loss, train_acc = train_metrics
assert train_loss < 0.5, train_loss
assert train_acc <= 1 and train_acc > 0.7, train_acc
assert test_acc <= 1 and test_acc > 0.7, test_acc
def run(device=None):
"""
封装了完整的训练和测试流程,并指定在哪个设备上运行。
:param device: torch.device, 指定计算设备 (e.g., torch.device('cuda'))
"""
# 1. 设置超参数
lr = 0.1
batch_size = 512
num_epochs = 10
# 2. 加载数据
train_iter, test_iter = load_data_fashion_mnist(batch_size)
# 3. 初始化模型参数
num_inputs = 784
num_outputs = 10
# 将 W 和 b 直接创建在指定的设备上
W = torch.normal(0, 0.01, size=(num_inputs, num_outputs), requires_grad=True, device=device)
b = torch.zeros(num_outputs, requires_grad=True, device=device)
# 4. 定义模型和优化器
# 这个 net 函数现在会捕获在 run 函数作用域内的 W 和 b
def net(X):
# 注意:这里的 X 也需要被移动到和 W, b 相同的设备上
# 我们将在 train_epoch_ch3 中处理
return softmax(torch.matmul(X.reshape((-1, W.shape[0])), W) + b)
def updater(batch_size):
return sgd([W, b], lr, batch_size)
# 5. 开始训练并计时
print(f"--- 开始在 {device} 上训练 ---")
start_time = time.time()
train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, updater, device)
end_time = time.time()
print(f"--- 在 {device} 上的训练结束 ---")
print(f"总耗时: {end_time - start_time:.2f} 秒\n")
# 清除 Animator 的图像,为下一次运行做准备
if __name__ == '__main__':
# (此处省略了您已有的所有类和函数的定义代码)
# 确保您的脚本里包含了所有必要的定义
# 运行 CPU 版本
run(device=torch.device('cpu'))
# 检查 CUDA 是否可用,如果可用,则运行 GPU 版本
if torch.cuda.is_available():
run(device=torch.device('cuda'))
else:
print("CUDA (GPU) 在此设备上不可用。")
plt.show()
因为数据量比较小,所以用cpu与gpu没什么区别,主要的时间还是花在了数据加载上
— 在 cpu 上的训练结束 —
总耗时: 58.27 秒
— 在 cuda 上的训练结束 —
总耗时: 67.53 秒
3. softmax回归调包实现
import os
os.environ['KMP_DUPLICATE_LIB_OK'] = 'True'
import softmax_complex
from torch import nn
import torch
import time
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
def init_weights(m):
if type(m) == nn.Linear:
nn.init.normal_(m.weight, std=0.01)
batch_size = 256
train_iter, test_iter = softmax_complex.load_data_fashion_mnist(batch_size)
net = nn.Sequential(nn.Flatten(), nn.Linear(784, 10))
net.apply(init_weights)
loss = nn.CrossEntropyLoss(reduction='none')
trainer = torch.optim.SGD(net.parameters(), lr=0.1)
num_epochs = 10
start_time = time.time()
softmax_complex.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)
end_time = time.time()
print(f"总耗时: {end_time - start_time:.2f} 秒\n")
plt.show()
总耗时: 60.89 秒
鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。
更多推荐


所有评论(0)