# 线性回归的从零实现
主要内容:基于 3.2 介绍的面向对象设计,手动实现线性回归,并重新介绍 3.2 中定义的各种模块以加深对面向对象设计的理解
# 定义模型
定义一个继承自 d2l.Module 的类作为模型,__init__方法中保存模型的参数
1 2 3 4 5 6 7
| class LinearRegressionScratch(d2l.Module): """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) def forward(self, X): return torch.matmul(X, self.w) + self.b
|
向 LinearRegressionScratch 类中加入 forward 方法
forward 方法是神经网络中前向传播的实现,用于从输入数据得到输出结果
# 损失函数
1 2 3 4
| @d2l.add_to_class(LinearRegressionScratch) 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): """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
1 2 3
| @d2l.add_to_class(LinearRegressionScratch) def configure_optimizers(self): return SGD([self.w, self.b], self.lr)
|
LinearRegressionScratch 继承自 d2l.Module,后者有 configure_optimizers 方法,用于返回一个优化器实例
# 训练
大致过程:(以线性回归为例)
1 2 3 4 5 6 7
| @d2l.add_to_class(d2l.Trainer) 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) def fit_epoch(self): self.model.train() for batch in self.train_dataloader: loss = self.model.training_step(self.prepare_batch(batch)) self.optim.zero_grad() 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)
|
- 将 data(class DataModule)的两种 dataloader 登记到内部属性,方便训练使用
- 计算数据集的批次数量
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
|
- 向模型中添加 trainer 属性,并赋值为自身的实例
- 登记 board.xlim (x 的取值范围),board 是一个用于训练过程中动态绘制(如 Loss 关于 epoch 图像)的实例
- 登记模型为内部属性
# 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
- 创建一个 Trainer 实例,传入训练轮次(max_epochs)
- 调用 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
- 创建一个继承自 DataModule 的子类
- 完成 get_dataloader () 重载
- 加入生成数据或获取数据的方法(可在__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]) ''' 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]):
1 2
| def configure_optimizers(self): raise NotImplementedError
|
待实现,返回一个优化器,如 SGD
# 如何使用 Module
- 创建一个继承自 Module 的子类
- 在__init__中保存模型参数
- 重载 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)