# 线性回归的从零实现

主要内容:基于 3.2 介绍的面向对象设计,手动实现线性回归,并重新介绍 3.2 中定义的各种模块以加深对面向对象设计的理解

# 定义模型

定义一个继承自 d2l.Module 的类作为模型,__init__方法中保存模型的参数

1
2
3
4
5
6
7
class LinearRegressionScratch(d2l.Module):  #@save
"""The linear regression model implemented from scratch."""
def __init__(self, num_inputs, lr, sigma=0.01):
super().__init__()
self.save_hyperparameters()
self.w = torch.normal(0, sigma, (num_inputs, 1), requires_grad=True)
self.b = torch.zeros(1, requires_grad=True)

requires_grad 表示需要计算梯度,而后可以通过 backward () 方法自动计算梯度

w 是一个 [num_inputs, 1] 形状的 0 为均值,sigma 为方差的正态分布取样

b -> 0

1
2
3
@d2l.add_to_class(LinearRegressionScratch)  #@save
def forward(self, X):
return torch.matmul(X, self.w) + self.b

向 LinearRegressionScratch 类中加入 forward 方法

forward 方法是神经网络中前向传播的实现,用于从输入数据得到输出结果

# 损失函数

1
2
3
4
@d2l.add_to_class(LinearRegressionScratch)  #@save
def loss(self, y_hat, y):
l = (y_hat - y) ** 2 / 2
return l.mean()

定义平方损失并加入类

# 优化算法

# SGD

1
2
3
4
5
6
7
8
9
10
11
12
13
class SGD(d2l.HyperParameters):  #@save
"""Minibatch stochastic gradient descent."""
def __init__(self, params, lr):
self.save_hyperparameters()

def step(self):
for param in self.params:
param -= self.lr * param.grad

def zero_grad(self):
for param in self.params:
if param.grad is not None:
param.grad.zero_()

step 根据梯度(grad)与学习率(lr)对参数进行一步更新

zero_grad 将梯度设置为 0

# 重载 configure_optimizers

1
2
3
@d2l.add_to_class(LinearRegressionScratch)  #@save
def configure_optimizers(self):
return SGD([self.w, self.b], self.lr)

LinearRegressionScratch 继承自 d2l.Module,后者有 configure_optimizers 方法,用于返回一个优化器实例

# 训练

大致过程:(以线性回归为例)

  • 初始化参数 (w,b\mathbf w,b

  • 重复

    • 计算梯度
    • 更新参数

1
2
3
4
5
6
7
@d2l.add_to_class(d2l.Trainer)  #@save
def prepare_batch(self, batch):
'''
这段代码看似没什么用,但是提供了一个灵活的扩展点
允许用户自定义或覆盖默认行为
'''
return batch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@d2l.add_to_class(d2l.Trainer)  #@save
def fit_epoch(self):
self.model.train() # torch.nn.moudle 定义的一个方法,用于将模式设置为训练模式
for batch in self.train_dataloader:
loss = self.model.training_step(self.prepare_batch(batch))
self.optim.zero_grad() # 将参数的梯度设为0,便于新一批的梯度计算
with torch.no_grad(): # 不纳入梯度计算图
loss.backward() # 反向传播计算梯度
if self.gradient_clip_val > 0:
'''
梯度裁剪技术,防止梯度爆炸(梯度过大),
导致一次权值更新过大,影响训练效果
'''
self.clip_gradients(self.gradient_clip_val, self.model)
self.optim.step() # 更新参数
self.train_batch_idx += 1
# 以下代码用于加载验证集
if self.val_dataloader is None:
return
self.model.eval()
for batch in self.val_dataloader:
with torch.no_grad():
self.model.validation_step(self.prepare_batch(batch))
self.val_batch_idx += 1

# 执行

1
2
3
4
model = LinearRegressionScratch(2, lr=0.03)
data = d2l.SyntheticRegressionData(w=torch.tensor([2, -3.4]), b=4.2)
trainer = d2l.Trainer(max_epochs=3)
trainer.fit(model, data)

1
2
3
with torch.no_grad():
print(f'error in estimating w: {data.w - model.w.reshape(data.w.shape)}')
print(f'error in estimating b: {data.b - model.b}')

# 加深对 d2l 面向对象设计的理解

这里重新理解一下 d2l 面向对象的设计,以及各个类与方法的联系

# class Trainer

# __init__

1
2
3
def __init__(self, max_epochs, num_gpus=0, gradient_clip_val=0):
self.save_hyperparameters()
assert num_gpus == 0, 'No GPU support yet'

max_epochs 训练轮数

# prepare_data

1
2
3
4
5
6
def prepare_data(self, data):
self.train_dataloader = data.train_dataloader()
self.val_dataloader = data.val_dataloader()
self.num_train_batches = len(self.train_dataloader)
self.num_val_batches = (len(self.val_dataloader)
if self.val_dataloader is not None else 0)

  1. 将 data(class DataModule)的两种 dataloader 登记到内部属性,方便训练使用
  2. 计算数据集的批次数量

p.s. dataloader 一般是一个可以迭代数据的 generator,示例参见 3.2 人造数据

# prepare_model

1
2
3
4
def prepare_model(self, model):
model.trainer = self
model.board.xlim = [0, self.max_epochs]
self.model = model

  1. 向模型中添加 trainer 属性,并赋值为自身的实例
  2. 登记 board.xlim (x 的取值范围),board 是一个用于训练过程中动态绘制(如 Loss 关于 epoch 图像)的实例
  3. 登记模型为内部属性

# fit

1
2
3
4
5
6
7
8
9
def fit(self, model, data):
self.prepare_data(data)
self.prepare_model(model)
self.optim = model.configure_optimizers()
self.epoch = 0
self.train_batch_idx = 0
self.val_batch_idx = 0
for self.epoch in range(self.max_epochs):
self.fit_epoch()

训练的核心部分 拟合 fit

首先调用 prepare_data, prepare_model 方法,将传入的模型与数据集作预处理

optim 获取模型的优化器

idx 表示 训练 / 验证轮次的下标

循环 max_epochs 次,调用 fit_epoch () 对所有数据进行一轮的拟合(训练 + 验证)

# 如何使用 Trainer

  1. 创建一个 Trainer 实例,传入训练轮次(max_epochs)
  2. 调用 fit 方法,传入 模型与数据集,并进行训练

# class DataModule

# __init__

1
2
def __init__(self, root='../data', num_workers=4):
self.save_hyperparameters()

root:数据存储路径

num_workers:指定子进程数量

# get_dataloader

1
2
def get_dataloader(self, train):
raise NotImplementedError

加载数据的方法,一般为一个 generator,需要自己实现,然后通过 @d2l.add_to_class (ClassName) 重载,示例参见 3.2 人造数据

# xxx_dataloader

1
2
3
4
5
def train_dataloader(self):
return self.get_dataloader(train=True)

def val_dataloader(self):
return self.get_dataloader(train=False)

分别获得训练用和验证用的数据集

# 如何使用 DataModule

  1. 创建一个继承自 DataModule 的子类
  2. 完成 get_dataloader () 重载
  3. 加入生成数据或获取数据的方法(可在__init__中实现)

# class Module

# __init__

1
2
3
4
def __init__(self, plot_train_per_epoch=2, plot_valid_per_epoch=1):
super().__init__()
self.save_hyperparameters()
self.board = ProgressBoard()

plot_x_per_epoch: 每轮训练 / 验证绘制次数

board:用于训练过程中动态绘制数据点的实例

# loss

1
2
def loss(self, y_hat, y):
raise NotImplementedError

损失函数,需要自己实现,然后通过 @d2l.add_to_class (ClassName) 重载

# forward

1
2
3
def forward(self, X):
assert hasattr(self, 'net'), 'Neural network is defined'
return self.net(X)

前向传播,传入输入数据,返回输出,中间可能是一个神经网络(计算图)

[assert hasattr (self, 'net'), 'Neural network is defined' 一句有些奇怪,貌似逻辑反了]

手动实现线性回归中,我们自己实现并重载了 forward 方法(具体地,是一个矩阵向量乘法)

# plot

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def plot(self, key, value, train):
"""Plot a point in animation."""
assert hasattr(self, 'trainer'), 'Trainer is not inited'
self.board.xlabel = 'epoch'
'''
以下分支语句,分别针对 训练与验证 两种情况,
计算对应的X坐标
'''
if train:
x = self.trainer.train_batch_idx / \
self.trainer.num_train_batches
n = self.trainer.num_train_batches / \
self.plot_train_per_epoch
else:
x = self.trainer.epoch + 1
n = self.trainer.num_val_batches / \
self.plot_valid_per_epoch
self.board.draw(x, value.to(d2l.cpu()).detach().numpy(),
('train_' if train else 'val_') + key,
every_n=int(n))

动态绘图方法

args:key 标签,value 打印的值,train 是否在训练过程中

# xxx_step

1
2
3
4
5
6
7
8
9
10
11
12
 def training_step(self, batch):
l = self.loss(self(*batch[:-1]), batch[-1]) # self()是什么
'''
batch[:-1] 包含 features,即输入/自变量
batch[-1] 对应 labels,即输出/预期结果
'''
self.plot('loss', l, train=True)
return l

def validation_step(self, batch):
l = self.loss(self(*batch[:-1]), batch[-1])
self.plot('loss', l, train=False)

进行一步的训练 / 验证(计算),并打印实时的 loss 值

关于 self (*batch [:-1]):

  • *batch [:-1] 将输入数据解包

  • 调用 self () 大致等同于调用 forward (),后者被写在内置函数__call__() 中

  • 综上:相当于用 输入数据(features) 在当前的神经网络上运算了一遍,并返回结果

# configure_optimizers

1
2
def configure_optimizers(self):
raise NotImplementedError

待实现,返回一个优化器,如 SGD

# 如何使用 Module

  1. 创建一个继承自 Module 的子类
  2. 在__init__中保存模型参数
  3. 重载 loss(损失函数), forward(前向传播), configure_optimizers(优化器)

[end]

2024/2/2

mofianger

代码引自 en.d2l.ai,部分注释为本人添加

参考:

3.2. Object-Oriented Design for Implementation — Dive into Deep Learning 1.0.3 documentation (d2l.ai)

3.3. Synthetic Regression Data — Dive into Deep Learning 1.0.3 documentation (d2l.ai)

3.4. Linear Regression Implementation from Scratch — Dive into Deep Learning 1.0.3 documentation (d2l.ai)