用到的库
import random
import matplotlib.pyplot as plt
import torch
import torch.nn
from torch import optim
import numpy as np
构造一个小的回归数据集:
生成 150 个带噪音的样本,其中 100 个训练样本,50 个测试样本,并打印出训练数据的可视化分布。
def add_gauss(data,mu,sigma):
'''添加高斯模糊'''
for i in range(len(data)):
data[i][0]+=random.gauss(mu,sigma=sigma)
data[i][1] += random.gauss(mu,sigma=sigma)
return data
def init_data(len,rate,t_w,t_b):
'''数据集数目,训练集占比、真实的w,真实的b '''
data = []
for i in range(len):
x = random.uniform(0, 50)
y = t_w * x + t_b
if [x, y] not in data:
data.append([x, y])
data = add_gauss(data, 0, 0.5)
train_data = random.sample(data, int(rate*len))
test_data = []
for i in data:
if i not in train_data:
test_data.append(i)
for i in data:
if i not in train_data:
test_data.append(i)
plt.figure()
'''转换为tensor'''
train_data = torch.Tensor(train_data)
test_data = torch.Tensor(test_data)
datax = []
datay = []
for i in train_data:
datax.append([i[0]])
datay.append([i[1]])
train_data = [datax, datay]
datax = []
datay = []
for i in test_data:
datax.append([i[0]])
datay.append([i[1]])
test_data= [datax, datay]
return torch.tensor(train_data),torch.tensor(test_data)
if __name__=='__main__':
train_data,test_data=init_data(150,2/3,0.5,1)
class LM(torch.nn.Module):
'''线性模型'''
def __init__(self):
super(LM,self).__init__()
self.linear=torch.nn.Linear(1,1)#输入和输出数目
def forward(self,x):
y_pred=self.linear(x)
return y_pred
回归任务中常用的评估指标是均方误差
均方误差(mean-square error, MSE)是反映估计量与被估计量之间差异程度的一种度量。
loss=torch.nn.MSELoss()
【注意:代码实现中没有除2】思考:没有除2合理么?谈谈自己的看法,写到实验报告。
是合理的,除二与不除二的唯一区别在于精确度,不除二时数量级偏大,反而更容易比较损失的大小。
经验风险 ( Empirical Risk ),即在训练集上的平均损失。
optimizer = optim.SGD(model.parameters(), lr=0.001,weight_decay=1e-5)
'''其中model.parameters()初始化随机的参数w,b
lr学习率
weight_decay 是L2正则化系数,为防止过拟合'''
思考1. 为什么省略了1/N不影响效果?
和上一问一样,都是数量级的问题,去掉不去掉只影响了数据的灵敏度,不影响数值的比较的结果。
思考 2. 什么是最小二乘法 ( Least Square Method , LSM )
回答以上问题,写到实验报告。
就是对LSM的损失函数的各个w和b求偏导,零偏导等于零,所得到的解就是下一次迭代需要的的可行解,用于更新数据。
2.2.5 模型训练
在准备了数据、模型、损失函数和参数学习的实现之后,开始模型的训练。
在回归任务中,模型的评价指标和损失函数一致,都为均方误差。
通过上文实现的线性回归类来拟合训练数据,并输出模型在训练集上的损失。
if __name__=='__main__':
train_data,test_data=init_data(150,2/3,0.5,1)
model=LM()
loss=torch.nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.001,weight_decay=1e-5)
X=train_data[0]
y=train_data[1]
num_epochs = 20
for epoch in range(num_epochs):
pre_y = model(X)
l = loss(pre_y, y)
optimizer.zero_grad() # 梯度清零
l.backward()
optimizer.step()
# 输出权重和偏置
print('w = ', model.linear.weight.item())
print('b = ', model.linear.bias.item())
print('epoch %d, loss: %f' % (epoch, l.item()))
w = 1.2275058031082153
b = 1.0194733142852783
epoch 0, loss: 783.282349
w = -0.06824445724487305
b = 0.9822576642036438
epoch 1, loss: 473.114380
…
w = 0.4923485517501831
b = 0.9970325827598572
epoch 19, loss: 0.382204
用训练好的模型预测一下测试集的标签,并计算在测试集上的损失。
x=test_data[0]
y=test_data[1]
l=loss(model(x),y)
print('测试集loss:',l.item())
测试集loss: 0.4034019112586975
if __name__=='__main__':
train_data,test_data=init_data(5000,2/3,0.5,1)
w = 0.6784240007400513
b = -0.625267744064331
epoch 0, loss: 37.602818
w = 0.47063958644866943
b = -0.6307486295700073
epoch 1, loss: 14.453145
…
w = 0.548579216003418
b = -0.6132088899612427
epoch 19, loss: 0.991307
测试集loss: 1.0176087617874146
数据量不是越大越好,这种情况下,图像是一条巨粗的线,很难拟合出原本的直线。当然这是高斯模糊的结果,数据量巨大的同时图像也展示出了高斯模糊的大致范围。
对主函数略作调整
if __name__=='__main__':
train_data,test_data=init_data(150,2/3,0.5,1)
for i in [1e-8,1e-4,1e-2,1]:
model = LM()
loss = torch.nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.001,weight_decay=i)
X=train_data[0]
y=train_data[1]
num_epochs = 20
for epoch in range(num_epochs):
pre_y = model(X)
l = loss(pre_y, y)
optimizer.zero_grad() # 梯度清零
l.backward()
optimizer.step()
# 输出权重和偏置
'''print('w = ', model.linear.weight.item())
print('b = ', model.linear.bias.item())'''
print('epoch %d, loss: %f' % (epoch, l.item()))
x=test_data[0]
y=test_data[1]
l=loss(model(x),y)
print('测试集loss:',l.item())
x=np.linspace(0,50,10)
plt.plot(x,x*model.linear.weight.item()+model.linear.bias.item(),label='weight_decay='+str(i))
plt.legend()
plt.show()
数据创建函数也有所调整,为了作图。
def init_data(len,rate,t_w,t_b):
data = []
for i in range(len):
x = random.uniform(0, 50)
y = t_w * x + t_b
if [x, y] not in data:
data.append([x, y])
data = add_gauss(data, 0, 0.5)
train_data = random.sample(data, int(rate*len))
test_data = []
for i in data:
if i not in train_data:
test_data.append(i)
plt.figure()
for i in train_data:
plt.scatter(i[0],i[1],c='r')
for i in test_data:
plt.scatter(i[0], i[1], c='g')
'''转换为tensor'''
train_data = torch.Tensor(train_data)
test_data = torch.Tensor(test_data)
datax = []
datay = []
for i in train_data:
datax.append([i[0]])
datay.append([i[1]])
train_data = [datax, datay]
datax = []
datay = []
for i in test_data:
datax.append([i[0]])
datay.append([i[1]])
test_data= [datax, datay]
return torch.tensor(train_data),torch.tensor(test_data)
epoch 19, loss: 0.793325
测试集loss: 0.7044939994812012
epoch 19, loss: 0.805041
测试集loss: 0.7179186940193176
epoch 19, loss: 1.028256
测试集loss: 0.9195249676704407
epoch 19, loss: 0.320158
其中红色圆点为训练集,测试圆点为测试集。
学到这里不得不回忆起第一次做一元LSM回归,当时是直接套用了一次LSM的w、b的公式,直接算出来了。但是要改成二元的就很麻烦,直接加了一个维度,代码得从头到尾的改。而这次用的torch就把模型改一下就行了。
构建训练和测试数据,其中:
训练数样本 15 个,测试样本 10 个,高斯噪声标准差为 0.1,自变量范围为 (0,1)。
def init_data(begin,stop,num,rate,t_w,t_b,mu,sigma):
'''begin:数据起点,
stop:数据集终点,
num:生成数目,
rate:训练集占比,
t_w:真实的w,
t_b:真实的b,
mu,sigma:高斯模糊的均值和方差'''
data = []
for i in range(num):
x = [random.uniform(begin, stop)+random.gauss(mu,sigma=sigma),random.uniform(begin, stop)+random.gauss(mu,sigma=sigma),random.uniform(begin, stop)+random.gauss(mu,sigma=sigma)]
y = x[0]*t_w[0]+x[1]*t_w[1]+x[2]*t_w[2]+t_b+random.gauss(mu,sigma=sigma)
data.append([x, y])
train_data = random.sample(data, int(rate*num))
test_data = []
for i in data:
if i not in train_data:
test_data.append(i)
'''转换为tensor'''
datax = []
datay = []
for i in train_data:
datax.append(i[0])
datay.append([i[1]])
train_data = [torch.tensor(datax), torch.tensor(datay)]
datax = []
datay = []
for i in test_data:
datax.append(i[0])
datay.append([i[1]])
test_data= [torch.tensor(datax), torch.tensor(datay)]
return train_data,test_data
套用求解线性回归参数的方法来求解多项式回归参数
class PolyLM(torch.nn.Module):
'''线性模型'''
def __init__(self):
super(PolyLM,self).__init__()
self.linear=torch.nn.Linear(3,1)#输入和输出数目
def forward(self,x):
y_pred=self.linear(x)
return y_pred
对于多项式回归,我们可以同样使用前面线性回归中定义的LinearRegression算子、训练函数train、均方误差函数mean_squared_error。
if __name__=='__main__':
train_data,test_data=init_data(0,1,25,3/5,[0.5,0.4,0.3],2,0,0.1)
model = PolyLM()
loss = torch.nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.01,weight_decay=1e-3)
X=train_data[0]
y=train_data[1]
num_epochs = 30
for epoch in range(num_epochs):
pre_y = model(X)
l = loss(pre_y, y)
optimizer.zero_grad() # 梯度清零
l.backward()
optimizer.step()
# 输出权重和偏置
'''print('w = ', model.linear.weight.item())
print('b = ', model.linear.bias.item())'''
print('num_epoch %d, train_loss: %f' % (epoch, l.item()))
num_epoch 0, train_loss: 4.053303
num_epoch 1, train_loss: 3.736230
num_epoch 2, train_loss: 3.444098
…
num_epoch 26, train_loss: 0.501111
num_epoch 27, train_loss: 0.463407
num_epoch 28, train_loss: 0.428665
num_epoch 29, train_loss: 0.396653
通过均方误差来衡量训练误差、测试误差以及在没有噪音的加入下sin函数值与多项式回归值之间的误差,更加真实地反映拟合结果。多项式分布阶数从0到8进行遍历。
'''测试模型'''
x=test_data[0]
y=test_data[1]
l=loss(model(x),y)
print('测试集loss:',l.item())
测试集loss: 0.37486714124679565
对于模型过拟合的情况,可以引入正则化方法,通过向误差函数中添加一个惩罚项来避免系数倾向于较大的取值。
if __name__=='__main__':
train_data,test_data=init_data(0,1,25,3/5,[0.5,0.4,0.3],2,0,0.1)
for i in [1e-8, 1e-4, 1e-2, 1]:
model = LM()
loss = torch.nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.01,weight_decay=i)
X=train_data[0]
y=train_data[1]
num_epochs = 30
for epoch in range(num_epochs):
pre_y = model(X)
l = loss(pre_y, y)
optimizer.zero_grad() # 梯度清零
l.backward()
optimizer.step()
# 输出权重和偏置
'''print('w = ', model.linear.weight.item())
print('b = ', model.linear.bias.item())'''
print('weight_decay:{}, train_loss: {}'.format(i, l.item()))
'''测试模型'''
x=test_data[0]
y=test_data[1]
l=loss(model(x),y)
print('测试集loss:',l.item())
weight_decay:1e-08, train_loss: 0.5947156548500061
测试集loss: 0.6567635536193848
weight_decay:0.0001, train_loss: 1.588309407234192
测试集loss: 1.6525604724884033
weight_decay:0.01, train_loss: 0.9914471507072449
测试集loss: 1.1179535388946533
weight_decay:1, train_loss: 1.3105849027633667
测试集loss: 1.3865985870361328
机器学习方法流程包括数据集构建、模型构建、损失函数定义、优化器、模型训练、模型评价、模型预测等环节。
为了更方便地将上述环节规范化,我们将机器学习模型的基本要素封装成一个Runner类。
除上述提到的要素外,再加上模型保存、模型加载等功能。
Runner类的成员函数定义如下:
__init__函数:实例化Runner类,需要传入模型、损失函数、优化器和评价指标等;
train函数:模型训练,指定模型训练需要的训练集和验证集;
evaluate函数:通过对训练好的模型进行评价,在验证集或测试集上查看模型训练效果;
predict函数:选取一条数据对训练好的模型进行预测;
save_model函数:模型在训练过程和训练结束后需要进行保存;
load_model函数:调用加载之前保存的模型。
import torch
import torch.nn
from torch import optim
class Runner():
'''
Runner类的成员函数定义如下:
__init__函数:实例化Runner类,需要传入模型、损失函数、优化器和评价指标等;
train函数:模型训练,指定模型训练需要的训练集和验证集;
evaluate函数:通过对训练好的模型进行评价,在验证集或测试集上查看模型训练效果;
predict函数:选取一条数据对训练好的模型进行预测;
save_model函数:模型在训练过程和训练结束后需要进行保存;
load_model函数:调用加载之前保存的模型。
'''
def __init__(self,model,loss,optim,eval_loss):
'''传入模型、损失函数、优化器和评价指标'''
self.model=model
self.loss=loss
self.optim=optim
self.eval_loss=eval_loss
def train(self,train_data):
'''train_data:列表类型,两个元素为tensor类型,第一个是x,第二个是y'''
model=self.model
loss = torch.nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.01, weight_decay=1e-3)
X = train_data[0]
y = train_data[1]
num_epochs = 30
for epoch in range(num_epochs):
pre_y = model(X)
l = loss(pre_y, y)
optimizer.zero_grad() # 梯度清零
l.backward()
optimizer.step()
print('epoch %d, loss: %f' % (epoch, l.item()))
self.__save_model(model)
def evaluate(self,test_data):
'''测试模型
test_data:列表类型,两个元素为tensor类型,第一个是x,第二个是y'''
x = test_data[0]
y = test_data[1]
l = self.loss(self.model(x), y)
print('测试集loss:', l.item())
def predict(self,X):
'''预测数据'''
return self.model(X)
def __save_model(self,model):
'''内部调用,保存模型'''
self.model=model
def load_model(self,model):
'''外部调用,读取模型'''
self.model=model
封装好Runner类后,直接调用类函数就行了,十分的简洁。
这里的__save_model()是内部隐藏类,外部是看不到的,也无法调用,就算添了’__'再调用也不行。
主函数如下:
if __name__=='__main__':
model = PolyLM()
loss = torch.nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.01, weight_decay=1e-3)
runner=Runner(model=model,loss=loss,optim=optimizer,eval_loss=loss)
train_data, test_data = init_data(0, 1, 25, 3 / 5, [0.5, 0.4, 0.3], 2, 0, 0.1)
runner.train(train_data)
runner.evaluate(test_data)
运行结果:
epoch 0, loss: 7.497038
epoch 1, loss: 6.954211
epoch 2, loss: 6.450910
…
epoch 27, loss: 1.009143
epoch 28, loss: 0.938562
epoch 29, loss: 0.873111
测试集loss: 1.039759635925293
使用线性回归来对马萨诸塞州波士顿郊区的房屋进行预测。
实验流程主要包含如下5个步骤:
数据处理:包括数据清洗(缺失值和异常值处理)、数据集划分,以便数据可以被模型正常读取,并具有良好的泛化性;
模型构建:定义线性回归模型类;
训练配置:训练相关的一些配置,如:优化算法、评价指标等;
组装训练框架Runner:Runner用于管理模型训练和测试过程;
模型训练和测试:利用Runner进行模型训练和测试。
本次实验不需要数据清晰
用到的库
import numpy as np
import pandas as pd
import torch.nn
import torch.nn as nn
from Runner import *
import torch
def init_boston(path,rate):
#波士顿标签
labels = ["CRIM", "ZN", "INDUS", "CHAS", "NOX", "RM", "AGE", "DIS", "RAD", "TAX", "PTRATIO", "B", "LSTAT", "MEDV"]
#读取csv
boston = pd.read_csv(path,dtype=float)
#数据集划分
train_df = boston.sample(int(len(labels) * rate))
test_df = boston.drop(index=train_df.index)
#转化为tensor
train_X= torch.from_numpy(train_df.drop('MEDV', axis=1).to_numpy().astype(np.float32))
train_y=torch.from_numpy(train_df['MEDV'].to_numpy().astype(np.float32)*1000).reshape(len(train_df['MEDV']),1)
test_X= torch.from_numpy(test_df.drop('MEDV', axis=1).to_numpy().astype(np.float32))
test_y= torch.from_numpy(test_df['MEDV'].to_numpy().astype(np.float32)*1000).reshape(len(test_df['MEDV'].to_numpy()),1)
return train_X,train_y,test_X,test_y
class BostonLM(nn.Module):
def __init__(self):
super(BostonLM, self).__init__()
self.linear = torch.nn.Linear(13, 100) # 输入和输出数目
self.hide=torch.nn.Linear(100,1)
def forward(self,X):
y = self.linear(X)
y2=self.hide(y)
return y2
# coding:utf-8
import torch
import torch.nn as nn
from torch import optim
import random
class Runner():
def __init__(self,model,loss,optim):
'''传入模型、损失函数、优化器和评价指标'''
self.model=model
self.loss=loss
self.optim=optim
def LSM_train(self,X,y,epoches=300):
'''train_data:列表类型,两个元素为tensor类型,第一个是x,第二个是y'''
print('start training....')
loss = self.loss
optimizer = self.optim
train_X, train_y, = X,y
for i in range(epoches):
pre_y =self.model(train_X)
l = loss(pre_y, train_y)
optimizer.zero_grad()
l.backward()
optimizer.step()
if i % 100 == 0:
print("epoch:{}, loss in train data:{}".format(i, l))
print('training ended.')
def LSM_evaluate(self,x,y):
'''测试模型
test_data:列表类型,两个元素为tensor类型,第一个是x,第二个是y'''
l = self.loss(self.model(x), y)
print('loss in test data:', l.item())
def predict(self,X):
'''预测数据'''
return self.model(X)
def save_model(self, save_path):
''''.pt'文件'''
torch.save(self, save_path)
def read_model(self, path):
''''.pt'文件'''
torch.load(path)
if __name__=='__main__':
net = BostonLM() # 初始化网络模型
loss = torch.nn.MSELoss() # 均方损失
optimizer = torch.optim.Adam(net.parameters(), lr=0.01)
train_X, train_y, test_X, test_y = init_boston('boston_house_prices.csv', rate=4 / 5)
runner=Runner(net,loss=loss,optim=optimizer)
runner.LSM_train(train_X,train_y,epoches=30000)
test_loss=runner.LSM_evaluate(test_X,test_y)
test_loss=runner.LSM_evaluate(test_X,test_y)
prey=runner.predict(input)
print('24 predicted:',prey.item())
运行结果:
…
epoch:29600, loss in train data:0.7043233513832092
epoch:29700, loss in train data:14840.9619140625
epoch:29800, loss in train data:1.225315809249878
epoch:29900, loss in train data:0.6267527937889099
training ended.
loss in test data: 184685824.0
24 predicted:43825.0977
【注意】例程2.5中有:
from nndl.op import Linear
from nndl.opitimizer import optimizer_lsm
这两个python文件位置如下:
文件使用了paddle,改写成pytorch,并在后续工作中使用即可。
问题1:使用类实现机器学习模型的基本要素有什么优点?
调用函数更方便,代码更简洁,减少不必要的重复代码。
问题2:算子op、优化器opitimizer放在单独的文件中,主程序在使用时调用该文件。这样做有什么优点?
避免重复编写相同的代码。
问题3:线性回归通常使用平方损失函数,能否使用交叉熵损失函数?为什么?
交叉熵损失函数多用于线性分类问题,用于表示类别之间的相关性,进而判断分类的效果。
总结:花了大量时间查阅相关资料,编代码的过程中也遇到了很多错误,真的让人很崩溃,但是最后程序运行起来的时候心情会大好,看大自己努力的结果是一件让人开心的事。这次实验学习了线性回归、多元线性回归的相关知识,同时尝试了Runner类的封装和基于线性回归的波士顿房价预测,其中波士顿预测还不太通透,需要进一步学习。总的来说工程量不小,收获也是蛮大的。