回归是指研究一组随机变量和另一组变量之间关系的统计分析方法,又称多重回归分析。在机器学习课程中,我们已经初步了解了线性回归,即使用一个线性模型(一个通过属性的线性组合来进行预测的函数,一般写为 f ( x ) = x A T + b f(x)=xA^T+b f(x)=xAT+b)尽可能准确地预测实值输出标记。通过使用pytorch实现线性回归,我们将
本实验使用Jupyter Notebook,事先引用以下库:
import torch
import numpy as np
import matplotlib.pyplot as plt
回归的数据集中所有的离散点,其分布需要近似为线性函数,因此需要我们设置好一个现存的线性函数作为基础,在此之上添加噪声,由此构建数据集的代码如下(以一元线性函数 f ( x ) = 114 x + 514 f(x)=114x + 514 f(x)=114x+514为例),生成150个带噪音的样本:
eps = torch.normal(0.,0.1,size=(150,2)) # 服从N(0,0.01) 的随机噪声
x = torch.hstack((torch.randn(150,1),torch.ones(150).reshape(-1,1))) # 随机的150个二元值
w = torch.Tensor([[114,514]]) # 系数矩阵
y = torch.mm(w,x.T).reshape(-1,1) # 计算y并进行计算得到函数值
dataset = torch.hstack(((x[:,0] + eps[:,0]).reshape(-1,1),y + eps[:,1])) # 构建数据集
plt.scatter(dataset[:,0],dataset[:,1])
得到150个线性函数分布的离散点:
接下来进行训练集和测试集的划分,由于已经是随机的乱序点,不需要进行shuffle操作:
dataset_train, dataset_test = dataset[:100],dataset[-50:]
p_tr = plt.scatter(dataset_train[:,0],dataset_train[:,1],c='r')
p_te = plt.scatter(dataset_test[:,0],dataset_test[:,1],c='g')
plt.legend((p_tr,p_te),('train','test'))
显而易见,模型参数由一个系数、一个常数项构成,输出为一个值,这边可以使用torch.nn.Linear。由此可得pytorch模型构成如下(两种形式等价):
class Func(torch.nn.Module):
def __init__(self):
super(Func,self).__init__()
self.linear = torch.tensor(torch.randn(1),requires_grad=True)
self.b = torch.tensor(torch.randn(1),requires_grad=True)
def forward(self,x):
return torch.mm(x,self.linear.T) + self.b
或
class Func(torch.nn.Module):
def __init__(self):
super(Func,self).__init__()
self.linear = torch.nn.Linear(1,1)
def forward(self,x):
return self.linear(x)
注意,第一种代码需要手动添加追踪变量并重写parameters函数。
传送门:torch.nn.Linear
线性回归中常用均方误差(mean squad error,MSE)来计算损失,其公式非常简单:
m s e ( y ; y ^ ) = 1 M ∑ i = 1 M ( y − y ^ ) 2 mse(y;\hat{y}) = \frac{1}{M}\sum_{i=1}^{M}(y-\hat{y})^2 mse(y;y^)=M1i=1∑M(y−y^)2
在实际使用中,我们使用torch.nn.MSELoss即可。下面两种代码均可:
def loss_func(y,y_hat):
M = y.size()[0]
tsum = 0.
for i in range(M):
tsum += (y[i] - y_hat[i]) ** 2
return (1./M) * tsum
或
loss_func = torch.nn.MSELoss()
经验风险最小化(empirical risk minimization,ERM)是统计学理论中的一个原则,其认为经验风险最小的模型是最优的模型,即
min f ∈ F 1 N ∑ i = 1 N L ( y i , f ( x i ) ) \min_{f∈F}\frac{1}{N}\sum_{i=1}^{N}L(y_i,f(x_i)) f∈FminN1i=1∑NL(yi,f(xi))
由公式显然可知,样本容量足够大时,经验风险最小化能够保证有很好的学习效果,但是样本容量过小时,容易导致过拟合。
模型训练的循环,本质就是通过上述的经验风险最小化来训练模型。
对于误差的优化,我们采用随机梯度下降(stochastic gradient descent,SGD),每一个数据计算一次损失函数后使用梯度更新参数,公式如下:
θ i = W i − 1 − η ∇ f θ ( θ i − 1 ) \theta_{i}=W_{i-1}-\eta\nabla f_{\theta}(\theta_{i-1}) θi=Wi−1−η∇fθ(θi−1)
这里我们不展开描述,直接使用torch自带的SGD定义优化函数。注意,使用前需要实例化线性模型类。
func = Func()
optimizer = torch.optim.SGD(func.parameters(),0.01)
# 将参数加入梯度监控表中便于计算梯度,学习率设置为0.01
经过前面的铺垫,我们只需要循环进行参数优化即可。代码如下:
EPOCHS = 800
for i in range(EPOCHS):
y = func(dataset_train[:,0].reshape(-1,1))
loss = loss_func(y,dataset_train[:,1].reshape(-1,1))
optimizer.zero_grad()
loss.backward()
optimizer.step()
if (i+1) % 100 == 0 or i + 1 == EPOCHS:
print('epoch {} / {} : MSE Loss = {}'.format(i+1,EPOCHS,loss))
结果如下:
在训练完成后,我们能够得到所需的相关系数和偏置值:
print('f(x) = {:.2f}x + {:.2f}'.format(float(func.linear.weight[0,0]),float(func.linear.bias[0])))
结果如下,和原函数非常接近:
使用预测好的模型在测试集上运行,得到结果。
代码如下:
ys = func(dataset_test[:,0].reshape(-1,1))
loss = loss_func(y,dataset_train[:,1].reshape(-1,1))
print('loss={}'.format(float(loss)))
p_te = plt.scatter(dataset_test[:,0],dataset_test[:,1],c='g')
p_pr = plt.scatter(dataset_test[:,0],ys.detach(),c='b')
plt.legend((p_te,p_pr),('actual','pred'))
输出和图像如下:
可见线性回归达到了一个比较好的效果,但是如果线性模型比较简单,且受到噪声影响,则效果优势不明显。
正则化是为了降低过拟合风险而加入的一种计算方式,一般有L1和L2正则化。由于 L1 正则化最后得到 w 向量中将存在大量的 0,使模型变得稀疏化,因此 L2 正则化更加常用。其定义为:
W = W − α ( ∂ L ∂ w + λ m W ) W = W - \alpha(\frac{\partial L}{\partial w}+\frac{\lambda}{m}W) W=W−α(∂w∂L+mλW)
也称之为权重衰减(weights decay)
修改代码权重衰减需要在优化器中进行设置,修改的代码如下:
optimizer = torch.optim.SGD(func.parameters(),0.01,weight_decay=0.1)
分别尝试0.1,0.01和0.001进行测试,结果如下:
weights_decay=0.1
在线性回归中,随着正则化系数不断变小,损失越来越小。
多项式不同于前面的线性回归,其定义式存在高次项:
f ( x ; w ) = w 1 x + w 2 x 2 + ⋯ + w M x M + b = w T ϕ ( x ) + b f(x;w)=w_1x+w_2x^2+\dots+w_Mx^M+b =w^T\phi(x) + b f(x;w)=w1x+w2x2+⋯+wMxM+b=wTϕ(x)+b
数据集构建方法和前面比较类似,再次不多赘述,代码如下:
import torch
import numpy as np
import matplotlib.pyplot as plt
eps = torch.randn(25,2) * 0.01 # torch.randn 为方差1的高斯噪声
xs = torch.rand(25,1).reshape(-1,1)
xs = torch.hstack((xs,xs ** 2,xs ** 3)) # 定义每项次数
w = torch.tensor([[11.,45.,14.]]).reshape(-1,1)
ys = torch.matmul(xs,w) + 191
dataset = torch.hstack((xs,ys)) # 保存x,x^2,x^3用于后面使用nn.Linear,拟合每个次数前的系数w
dataset_train,dataset_test = dataset[:15],dataset[-10:]
p_tr = plt.scatter(dataset_train[:,0],dataset_train[:,-1],c='r')
p_te = plt.scatter(dataset_test[:,0],dataset_test[:,-1],c='g')
plt.legend((p_tr,p_te),('train','test'))
基于预处理好的输入数据,可以直接使用nn.Linear,因为:
原本的nn.Linear(M,1)为
f ( x ) = ∑ i = 1 M w i x i + b f(x)=\sum_{i=1}^{M}w_ix_i +b f(x)=i=1∑Mwixi+b
经过预处理,公式更新为:
f ( x ) = ∑ i = 1 M w i x i + b f(x)=\sum_{i=1}^{M}w_ix^i+b f(x)=i=1∑Mwixi+b
模型定义、损失函数和优化器代码如下:
class PolyFunc(torch.nn.Module):
def __init__(self):
super(PolyFunc,self).__init__()
self.linear = torch.nn.Linear(3,1)
def forward(self,x):
return self.linear(x)
func = PolyFunc()
loss_func = torch.nn.MSELoss()
optimizer = torch.optim.SGD(func.parameters(),0.01)
类比前面线性模型,很容易得到模型训练的代码:
EPOCHS = 100000
for i in range(EPOCHS):
y = func(dataset_train[:,:-1])
loss = loss_func(y,dataset_train[:,-1].reshape(-1,1))
optimizer.zero_grad()
loss.backward()
optimizer.step()
if (i+1) % 10000 == 0 or i + 1 == EPOCHS:
print('epoch {} / {} : MSE Loss = {}'.format(i+1,EPOCHS,loss))
print('f(x) = {}x + {}x^2 + {}x^3 + {}'.format(
float(func.linear.weight[0,0]),
float(func.linear.weight[0,1]),
float(func.linear.weight[0,2]),
float(func.linear.bias[0])
))
训练时误差已经体现,下面代码用于体现测试时的误差:
ys = func(dataset_test[:,:-1])
loss = loss_func(y,dataset_train[:,-1].reshape(-1,1))
print('loss={}'.format(float(loss)))
p_te = plt.scatter(dataset_test[:,0],dataset_test[:,-1],c='g')
p_pr = plt.scatter(dataset_test[:,0],ys.detach(),c='b')
plt.legend((p_te,p_pr),('actual','pred'))
该实验内容,因为近似二次函数,将前面代码进行微调即可。这里不再赘述,结果如下:
数据集分布
Runner类的成员函数定义如下:
此部分为前面各部分的综合运用,本文仅实现最简单的Runner类。代码经过整理如下:
class Runner():
def __init__(self, model, loss_fn, optimizer,eval=None):
self.model = model
self.loss_fn = loss_fn
self.optimizer = optimizer
if eval is None:
self.eval = loss_fn
else:
self.eval = eval
def train(self,train_x, train_y,epochs):
losses = []
from rich.progress import track
for i in track(range(epochs),description='training...'):
pred = self.model(train_x)
self.loss = self.loss_fn(train_y,pred.reshape(-1,1))
self.optimizer.zero_grad()
self.loss.backward()
self.optimizer.step()
return self.loss
def eval(self,eval_x,eval_y):
pred = self.model(eval_x)
eval_val = self.eval(pred.reshape(-1,1),eval_y)
return eval_val
def predict(self,x):
pred = self.model(x)
return pred
def save_model(self, path):
torch.save(self.model.state_dict(),path)
def load_model(self,path):
self.model = torch.load(path)
该类的使用将在下一部分给出。
引入需要使用的库:
import torch
import numpy as np
import pandas as pd
from sklearn.datasets import load_boston # 波士顿房价数据集
import matplotlib.pyplot as plt
from sklearn.preprocessing import MinMaxScaler # 数据归一化处理
from random import shuffle # 用于打乱数据集
首先我们通过sklearn的内置数据集读取波士顿房价数据,检查是否存在缺失值或异常值:
df = load_boston()
dataset = torch.hstack((torch.tensor(df.data),torch.tensor(df.target).reshape(-1,1)))
df = pd.DataFrame(dataset.numpy())
print(df[df.isnull().T.any()])
可知数据一切正常。
通过查阅资料可知,波士顿房价数据中,最后一列为房价,前面的内容均为参数,也即data属性为参数,target属性为结果,由此可构建数据集:
df = load_boston()
dataset = torch.hstack((torch.tensor(df.data),torch.tensor(df.target).reshape(-1,1)))
df = pd.DataFrame(dataset.numpy())
print(df[df.isnull().T.any()])
shuffle(dataset)
scaler = MinMaxScaler()
dataset = scaler.fit_transform(dataset) # 归一化处理
dataset_train, dataset_test = torch.tensor(dataset[:-50]),torch.tensor(dataset[-50:]) # 使用前456个数据进行训练,后50个数据用于测试
注意,由于数据范围跨度较大,在训练中可能出现梯度消失的问题,因此需要使用归一化来防止数据过拟合。
通过数据集可知,如果使用线性回归,则需要回归12个参数的相关系数和偏置值,由此易得模型定义代码:
class Model(torch.nn.Module):
def __init__(self):
super(Model, self).__init__()
self.linear = torch.nn.Linear(12,1)
def forward(self,x):
return self.linear(x)
Runner类定义和前面的定义相同,在此不多赘述,下面展示Runner类的初始化过程:
model = Model()
runner = Runner(model,torch.nn.MSELoss(),torch.optim.SGD(model.parameters(),0.01))
由于使用Runner类,模型训练非常简单,代码如下:
loss_last = runner.train(dataset_train[:,:-1],dataset_train[:,-1].reshape(-1,1),1000)
print('loss=%f'%float(loss_last))
模型测试使用测试集进行运算,并计算损失:
loss = runner.eval(dataset_test[:,:-1],dataset_test[:,-1].reshape(-1,1))
print('eval loss=%f'%loss)
输出结果如下:
模型预测相对来说比较简单,这里使用整个数据集数据进行测试。代码如下:
plt.plot(dataset[:,-1],label='actual')
plt.plot(runner.predict(dataset[:,:-1]).detach().numpy(),label='pred')
plt.legend()
这次实验我们了解到了回归及其基本方法。回归还有逻辑回归等众多内容等待我们进行学习。本次实验对于项目中代码的复用、封装和面向对象特点的认识均有帮助,也指导我们如何进行数据处理以及如何将回归这一数学概念应用于现实生活。