本代码基于《动手学深度学习》Pytorch版,第四章多层感知机,第四节模型选择、欠拟合和过拟合,第三部分多项式回归。对代码进行修改,增加注释,供学习使用。
导入相关库
import matplotlib_inline
import matplotlib.pyplot as plt
import IPython
import torch
from torch import nn
import numpy as np
import math
设置JupyterNotebook中Matplotlib默认图像显示格式为SVG,让图表更易嵌入网页,并可在各种分辨率和设备上呈现出高质量效果
# SVG(ScalableVectorGraphics,可缩放矢量图形)基于XML的矢量图形格式,可缩放和调整大小而不失真
# SVG图像具有更高的质量和可扩展性,在需更清晰的图像或需要缩放图像时使用
# 在JupyterNotebook中,默认Matplotlib生成的图像会以PNG格式显示
def use_svg_display():
matplotlib_inline.backend_inline.set_matplotlib_formats('svg')
# backend_inline是JupyterNotebook设置Matplotlib后端的模块,使生成的图像直接嵌入笔记本中,而不是在单独窗口中显示
# set_matplotlib_formats()设置Matplotlib图像显示格式,允许为Matplotlib生成的图像指定所需的输出格式
# 可接受其他参数,quality(设置JPEG输出格式图像质量),dpi(设置图像每英寸点数)等
设置Matplotlib图形的轴属性,并在轴上启用网格线
def set_axes(axes, xlabel, ylabel, xlim, ylim, xscale, yscale, legend):
axes.set_xlabel(xlabel)
axes.set_ylabel(ylabel)
axes.set_xscale(xscale)
axes.set_yscale(yscale)
axes.set_xlim(xlim)
axes.set_ylim(ylim)
# legend如果为None或空列表,则不显示
if legend:
axes.legend(legend)
axes.grid()
# grid(b = None, which = 'major', linestyle = '-', linewidth = 0.5, color = 'gray', alpha = 0.5)
# 创建网格线,可帮助在绘制图形时更好地定位数据点
# b可选,布尔值,是否显示网格线,默认为None,根据其他参数自动确定是否显示网格线
# which可选,指定要显示的网格线类型
# 可是major(主要网格线),minor(次要网格线),both(主要和次要网格线),默认为major
# linestyle可选,指定网格线线型,可是-(实线),--(虚线),-.(点划线),:(点线),默认为-
# linewidth可选,指定网格线线宽,默认为0.5
# color可选,指定网格线颜色,可是任何有效的Matplotlib颜色字符串或颜色代码,默认为gray
# alpha可选,指定网格线透明度,默认为0.5
在动画中绘制图表,在训练过程中实时显示数据,在动画中绘制图表指在计算机动画或交互式图形应用程序中实时显示数据变化过程
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)):
# xlabel:x轴标签
# ylabel:y轴标签
# legend图例,显示每条线的标签
# xlim:x轴范围
# ylim:y轴范围
# xscale:x轴刻度类型,默认为线性
# yscale:y轴刻度类型,默认为线性
# fmts绘制线条的格式
# nrows子图行数
# ncols子图列数
# figsize图形大小,包含两个元素的元组,表示整个图表的宽度和高度(以英寸为单位)
# 检查legend是否为None,如果是,则将其设置为空列表
if legend is None:
legend = []
# 设置图像显示格式
use_svg_display()
# 创建包含nrows行和ncols列的子图网格
self.fig, self.axes = plt.subplots(nrows, ncols, figsize = figsize)
# 如果只有一个子图,将其放入列表中
if nrows * ncols == 1:
self.axes = [self.axes, ]
# 将单一的Axes对象转换为包含该对象的列表
# 使用lambda函数配置子图的轴
self.config_axes = lambda: set_axes(self.axes[0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend)
self.X, self.Y, self.fmts = None, None, fmts
# 向图表中添加数据点,并在添加数据点后更新图形
def add(self, x, y):
# x:x轴的数据点
# y:y轴的数据点
# 检查变量是否具有__len__属性,如果有,说明变量是可迭代对象,如果没有,说明变量不是可迭代对象
# 将y转换为包含单个元素的列表,计算y的长度,即元素数量,将其存储在变量n中,将x转换为长度为n的列表
if not hasattr(y, '__len__'):
y = [y]
n = len(y)
if not hasattr(x, '__len__'):
x = [x] * n
# 检查变量是否为空,如果为空,将其初始化为包含n个空列表的列表
if not self.X:
# 检查是否为空,如果为空,则条件为真,执行if语句块中的代码,否则,条件为假,跳过if语句块
self.X = [[] for _ in range(n)]
# 使用列表推导式创建包含n个空列表的列表
if not self.Y:
self.Y = [[] for _ in range(n)]
# 遍历x和y,如果不为None,则添加到X和Y
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)
self.axes[0].cla()
for x, y, fmt in zip(self.X, self.Y, self.fmts):
self.axes[0].plot(x, y, fmt)
# 配置子图属性,并显示图形后清除当前输出区域
self.config_axes()
IPython.display.display(self.fig)
IPython.display.clear_output(wait = True)
累加数值
class Accumulator:
# 初始化长度为n的浮点数列表self.data存储累加的值,所有元素初始值为0.0
def __init__(self, n):
# n要累加的值的数量
self.data = [0.0] * n
# 接收任意数量的数值参数,并与self.data中的对应元素相加并存储
def add(self, *args):
# args可变数量参数
self.data = [a + float(b) for a, b in zip(self.data, args)]
# 重置累加器,将self.data中的所有元素重置为0.0
def reset(self):
self.data = [0.0] * len(self.data)
# 实现类实例的索引访问,允许使用索引操作符[]来访问Accumulator对象中的元素
def __getitem__(self, idx):
# __getitem__允许对象像字典或列表一样进行索引操作
# 当尝试使用索引访问对象时,Python会自动调用该对象的__getitem__方法
# __getitem__通常与__setitem__一起使用,以实现对象的索引操作
# __setitem__用于设置值,__getitem__用于获取值
# idx要访问的元素索引
return self.data[idx]
计算预测正确的数量
def accuracy(y_hat, y):
if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:
y_hat = y_hat.argmax(axis = 1)
# argmax()在NumPy数组和PyTorch张量上找到最大值的索引
cmp = y_hat.type(y.dtype) == y
return float(cmp.type(y.dtype).sum())
评估给定数据集上模型的损失
def evaluate_loss(net, data_iter, loss):
metric = Accumulator(2)
for x, y in data_iter:
out = net(x)
y = y.reshape(out.shape)
l = loss(out, y)
metric.add(l.sum(), l.numel())
return metric[0] / metric[1]
构建数据迭代器
def iterate(dataset, n, shuffle = True):
data = torch.utils.data.TensorDataset(*dataset)
# TensorDataset(*tensors)创建数据集,其中每个样本由一组张量表示
# tensors一系列张量,每个张量表示一个样本的一个特征,所有张量的长度(即第一个维度大小)必须相同,表示数据集中的样本数量
# TensorDataset类常用于将多个张量组合成一个数据集,以便在训练神经网络时使用
# 可与其他PyTorch数据加载器(如DataLoader)结合使用,以实现数据的批量加载和并行处理
# 在Python中,使用*作为前缀可将元组或列表中的元素解包并作为单独的参数传递给函数
return torch.utils.data.DataLoader(data, n, shuffle = shuffle)
# DataLoader(dataset, batch_size = 1, shuffle = False, sampler = None, batch_sampler = None, num_workers = 0,
# collate_fn = None, pin_memory = False, drop_last = False, timeout = 0, worker_init_fn = None)
# 创建数据加载器,数据加载器用于在训练神经网络时高效地加载和处理数据
# dataset要加载的数据集,可是PyTorch中定义的任何数据集类(如TensorDataset,ImageFolder等)的实例
# batch_size可选,每个批次的数据量大小,默认为1
# shuffle可选,布尔值,是否在每个epoch开始时打乱数据集,默认为False
# sampler可选,自定义采样器,决定从数据集中抽取哪些样本,如果提供了,则shuffle将被忽略
# batch_sampler可选,自定义批量采样器,决定从数据集中抽取哪些样本组成批次
# 如果提供了,则batch_size,shuffle,sampler将被忽略
# num_workers可选,加载数据的子进程数量,默认为0,使用主进程加载数据
# collate_fn可选,自定义函数,将一批样本组合成一个批次,默认为None,使用默认的组合方式
# pin_memory可选,布尔值,是否将数据加载到固定内存中,默认为False
# 如果设为True,则数据加载器会将数据加载到固定内存中,可提高数据传输到GPU的速度
# drop_last可选,布尔值,是否丢弃最后一个不完整的批次,默认为False
# timeout可选,等待子进程加载数据的超时时间(秒),默认为0,无限制等待
# worker_init_fn可选,自定义函数,初始化每个子进程,默认为None
训练一轮
def train_epoch(net, train, loss, updater):
# 将模型设置为训练模式
if isinstance(net, torch.nn.Module):
# isinstance(object, classinfo)检查对象是否为指定类的实例或者指定类的子类的实例,如果是,则返回True,否则返回False
# 还可检查对象是否为多个类型中的任意一个,如果对象是多个类型中的任意一个返回True,否则返回False
# object要检查的对象
# classinfo要比较的类,可是一个类或者一个类元组
# torch.nn.Module是PyTorch框架中的一个基类,用于表示神经网络模型
# 如果net是torch.nn.Module的实例,那么net就是一个有效的神经网络模型
net.train()
# train()模型方法,将模型设置为训练模式
# 当模型处于训练模式时,会启用dropout和batch normalization等技术,以便在训练过程中进行正则化和提高模型泛化能力
# 初始化累加器,用于存储训练损失总和,训练准确度总和,样本数
metric = Accumulator(3)
for x, y in train:
y_hat = net(x)
l = loss(y_hat, y)
# 计算梯度更新参数
if isinstance(updater, torch.optim.Optimizer):
# 使用PyTorch内置的优化器和损失函数
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 train(train_features, test_features, train_labels, test_labels, n_epochs = 400):
input = train_features.shape[-1]
net = nn.Sequential(nn.Linear(input, 1, bias = False))
# Sequential(*args)创建顺序模型,顺序模型是一种简单的神经网络模型,由多个层按照顺序堆叠而成
# args一系列要添加到模型中的层,可是PyTorch中定义的任何层
# 自动处理层之间的连接,不需手动连接层,支持通过索引访问模型中的层
# 顺序模型适用于简单的神经网络结构,但对于更复杂的网络结构,可能需要使用其他类型的模型,如nn.ModuleList或自定义模型类
# Linear(in_features, out_features, bias = True)
# 创建线性层(linearlayer),线性层对输入数据进行线性变换,输出数据形状与输入数据形状相同
# in_features输入特征的数量,即输入数据的最后一个维度大小
# out_features输出特征的数量,即输出数据的最后一个维度大小
# bias可选,布尔值,是否使用偏置项,默认为True,使用偏置项
# 线性层常用于实现全连接层(fully-connectedlayer),将输入数据与权重矩阵相乘,然后加上偏置项(如果启用),应用激活函数
loss = nn.MSELoss(reduction = 'none')
# MSELoss()计算均方误差损失(MeanSquaredErrorLoss)
# reduction可选,损失值的归约方式
# none不进行归约,返回每个样本的损失值
# mean默认,计算所有样本损失的平均值
# sum计算所有样本损失的总和
# 在计算损失值时会自动计算梯度信息,因此在调用backward()时,梯度信息将被更新
trainer = torch.optim.SGD(net.parameters(), lr = 0.01)
# SGD(params, lr = 0.01, momentum = 0, dampening = 0, weight_decay = 0, nesterov = False)
# 创建随机梯度下降(StochasticGradientDescent)优化器,随机梯度下降是一种常用的优化算法,用于在训练神经网络时更新模型参数
# params需要优化的模型参数集合,通常是包含模型参数的列表或字典
# lr可选,学习率,控制每次参数更新的步长,默认为0.01
# momentum可选,动量因子,加速梯度下降收敛,默认为0
# dampening可选,动量的阻尼系数,默认为0
# weight_decay可选,权重衰减(L2正则化),防止模型过拟合,默认为0
# nesterov可选,布尔值,是否使用Nesterov动量,默认为False
batch_size = min(10, train_labels.shape[0])
train_iter = iterate((train_features, train_labels.reshape(-1, 1)), batch_size, shuffle = False)
test_iter = iterate((test_features, test_labels.reshape(-1, 1)), batch_size, shuffle = False)
# !!!问题:当shuffle = True时,图像无法与书本吻合,图像并不圆润,震荡频繁,波动较大!!!
animator = Animator(xlabel = 'epoch', ylabel = 'loss', yscale= 'log', xlim = [1, n_epochs], ylim = [1e-3, 1e2],
legend = ['train', 'test'])
for epoch in range(n_epochs):
train_epoch(net, train_iter, loss, trainer)
if epoch == 0 or (epoch + 1) % 20 == 0:
animator.add(epoch + 1, (evaluate_loss(net, train_iter, loss), evaluate_loss(net, test_iter, loss)))
# !!!问题:图像不一定与书本吻合,随机性大
print('w:', net[0].weight.data.numpy())
实现
生成带有噪声的三阶多项式数据集
max_degree = 20
# 最大阶数
n_train, n_test = 100, 100
# n_train训练集数量
# n_test测试机数量
true_w = np.zeros(max_degree)
# zeros(shape, dtype = float, order = 'C')创建指定形状的全零数组
# shape整数或者整数元组,要创建的数组形状
# dtype可选,数组的数据类型,默认为float
# order可选,数组的内存布局,默认为C,按行优先顺序存储,F,按列优先顺序存储
true_w[0 : 4] = np.array([5, 1.2, -3.4, 5.6])
# array()创建NumPy数组
# NumPy数组是多维数组对象,可用来表示向量、矩阵和更高维度的张量
# object任何暴露数组接口的对象,如列表,元组或其他序列类型
# 如果传递的是一个序列,NumPy将创建一个一维数组,如果传递的是一个嵌套序列,NumPy将创建一个多维数组
# dtype可选,数组元素的数据类型,如不指定,将根据输入数据自动推断数据类型
# copy可选,布尔值,是否创建输入数据的副本,默认为True,创建副本,如果设为False,返回输入数据的视图,
# 视图和副本在修改数据时的行为是不同的
# order可选,数组的内存布局,可是C(行优先,C风格),F(列优先,Fortran风格),A(任意顺序),默认为C
# subok可选,布尔值,是否允许返回子类实例,默认为False,返回NumPy数组实例,如果设为True,返回子类实例
# ndmin可选,返回数组的最小维度,默认为0,返回标量或数组,如果设置大于为0的整数,将创建一个至少具有指定维度的数组
features = np.random.normal(size = (n_train + n_test, 1))
# print(features)
# normal(loc = 0.0, scale = 1.0, size = None)从正态分布中生成随机数,返回一个数组
# loc可选,正态分布的均值(平均值),默认为0.0
# scale可选,正态分布的标准差,默认为1.0,标准差越大,生成的随机数分布越分散
# size可选,输出数组的形状,整数,表示生成一维数组的长度,元组,表示生成多维数组的形状,如未指定,则返回一个标量值
# 生成的随机数是伪随机数,其随机性取决于随机数生成器种子,如需重现相同的随机数序列,可在调用normal()之前设置随机数生成器种子
np.random.shuffle(features)
# shuffle(x)对数组进行原地随机排列,直接修改输入数组,而不返回新的数组
# x要随机排列的数组,可是任何一维数组
# 只能用于一维数组,如需对多维数组的行或列进行随机排列,可使用np.random.permutation()
poly_features = np.power(features, np.arange(max_degree).reshape(1, -1))
# print(poly_features[: 20, : 4])
# power(a, b)计算数组中每个元素的指定次幂,返回新的NumPy数组,其中的元素是a中对应元素的b次幂
# a输入数组,可是任何数值类型的NumPy数组
# b指数数组,可是任何数值类型的NumPy数组,b的形状可与a相同,也可是一个标量值,如果是,那a中的每个元素都将被提升到这个标量次幂
# 在处理负数和零次幂时有一些特殊情况
# 如果a中包含负数,且b不是整数,那么结果将是复数
# 如果a中包含零,且b是负数,那么结果将是无穷大(inf)
# 如果a中包含零,且b是零,那么结果将是未定义(nan)
# arange([start, ]stop, [step, ]dtype=None)创建一个等差数列
# start可选,等差数列的起始值,如未指定,默认为0
# stop必须,等差数列的终止值(不包含)
# step可选,等差数列的公差,如未指定,默认为1
# dtype可选,数组元素数据类型,如不指定,将根据输入数据自动推断数据类型
# 参数必须是数值类型,不能是字符串或其他非数值类型
for i in range(max_degree):
poly_features[:, i] /= math.gamma(i + 1)
# gamma(x)计算伽马函数(Gamma function)的值
# 伽马函数是一种扩展了阶乘概念到实数和复数的特殊函数
# x输入值,可是实数或复数,不能为负整数或零,因为这些值会导致伽马函数未定义
# 仅适用于实数和复数的输入值。对于负整数和零的输入值,该函数将引发ValueError异常
# 由于math模块仅支持实数计算,因此无法直接计算复数的伽马函数值,如果需要计算复数的伽马函数值,可使用scipy.special.gamma()
labels = np.dot(poly_features, true_w)
# dot(a, b, out = None)计算两个数组的点积,返回新的NumPy数组
# a输入数组,可是1维数组(向量)或2维数组(矩阵)
# b输入数组,可是1维数组(向量)或2维数组(矩阵),如果a和b的维度不兼容,将引发错误
# out可选,输出数组,如果指定,结果将存储在此数组中,out的形状必须与结果数组相同,且数据类型兼容,如未指定,将创建新的数组存储
# 在不同维度的数组上具有不同的行为
# 当a和b都是一维数组(向量)时,计算点积(内积)
# 当a和b都是二维数组(矩阵)时,计算矩阵乘法
# 当a是一维数组,b是二维数组时,将a视为行向量,计算矩阵乘法
# 当a是二维数组,b是一维数组时,将b视为列向量,计算矩阵乘法
# 其他情况下,将引发错误
# 从NumPy1.20.0版本开始,对于矩阵乘法,建议使用@运算符或numpy.matmul(),因为提供了更好的性能和更清晰的代码
labels += np.random.normal(scale = 0.1, size = labels.shape)
true_w, features, poly_features, labels = [torch.tensor(x, dtype = torch.float32) for x in [true_w, features,
poly_features, labels]]
# tensor()创建张量
# data任何暴露数组接口的对象,如列表,元组或其他序列类型
# 如果传递的是一个序列,PyTorch将创建一个一维张量,如果传递的是一个嵌套序列,PyTorch将创建一个多维张量
# dtype可选,张量元素的数据类型,如不指定,将根据输入数据自动推断数据类型
# device可选,张量存储的设备,可是cpu或cuda(GPU),默认为cpu
# requires_grad可选,布尔值,是否需要计算梯度,默认为False,如果设为True,则在计算过程中会跟踪梯度信息,以便进行反向传播
# pin_memory可选,布尔值,是否将张量数据固定在内存中,默认为False,如果设为True,则固定,可提高数据传输到GPU的速度
# dtype可选,张量元素数据类型,如不指定,将根据输入数据自动推断数据类型
print(features[: 2])
print(poly_features[: 2, :])
print(labels[: 2])
运行结果
tensor([[-0.2119],
[ 0.2110]])
tensor([[ 1.0000e+00, -2.1192e-01, 2.2456e-02, -1.5863e-03, 8.4043e-05,
-3.5621e-06, 1.2582e-07, -3.8090e-09, 1.0090e-10, -2.3760e-12,
5.0352e-14, -9.7007e-16, 1.7132e-17, -2.7928e-19, 4.2275e-21,
-5.9727e-23, 7.9109e-25, -9.8618e-27, 1.1611e-28, -1.2950e-30],
[ 1.0000e+00, 2.1103e-01, 2.2267e-02, 1.5664e-03, 8.2640e-05,
3.4880e-06, 1.2268e-07, 3.6985e-09, 9.7563e-11, 2.2877e-12,
4.8277e-14, 9.2619e-16, 1.6288e-17, 2.6441e-19, 3.9856e-21,
5.6073e-23, 7.3958e-25, 9.1810e-27, 1.0764e-28, 1.1955e-30]])
tensor([4.6612, 5.2764])
正常
train(poly_features[: n_train, : 4], poly_features[n_train :, : 4], labels[: n_train], labels[n_train :])
运行结果
w: [[ 5.000563 1.2154133 -3.3958533 5.5772505]]
欠拟合
train(poly_features[: n_train, : 2], poly_features[n_train :, : 2], labels[: n_train], labels[n_train :])
运行结果
w: [[3.8899014 3.0845203]]
过拟合
train(poly_features[: n_train, :], poly_features[n_train :, :], labels[: n_train], labels[n_train :], n_epochs = 1500)
运行结果
w: [[ 4.9993348e+00 1.2700386e+00 -3.3290725e+00 5.2567663e+00
-4.0447375e-01 1.1487528e+00 8.3831310e-02 2.0189838e-01
2.2912174e-02 3.5644762e-02 2.0944776e-01 1.1871718e-01
-3.3130620e-02 -6.2206190e-02 -3.6891932e-03 -4.4839166e-02
1.6889639e-01 -1.4784461e-01 1.6560024e-01 5.2023530e-02]]