目录
一:回顾
上一篇我们了解了另外一种处理过拟合的方法,丢弃法(dropout),它通过以一定的概率将神经元的输出设置为0,从而强制网络中的每个神经元都不会过度依赖于其他神经元,进而减少了过拟合的风险,实践中,dropout概率通常在0.2到0.5之间,这样可以有效地减轻过拟合问题,提升模型的泛化性能。之后用代码实现了dropout的效果,之后介绍了前向后向传播。这节课将从0实现一个线性模型——房价预测
二:预测房价
此数据集由Bart de Cock于2011年收集 , 涵盖了2006-2010年期间亚利桑那州埃姆斯市的房价。
下载和缓存数据集
import hashlib
import os
import tarfile
import zipfile
import requests
#@save
DATA_HUB = dict()
DATA_URL = 'http://d2l-data.s3-accelerate.amazonaws.com/'
下面的download
函数用来下载数据集, 将数据集缓存在本地目录(默认情况下为../data
)中, 并返回下载文件的名称。 如果缓存目录中已经存在此数据集文件,并且其sha-1与存储在DATA_HUB
中的相匹配, 我们将使用缓存的文件,以避免重复的下载。
def download(name, cache_dir=os.path.join('..', 'data')): #@save
"""下载一个DATA_HUB中的文件,返回本地文件名"""
assert name in DATA_HUB, f"{name} 不存在于 {DATA_HUB}"
url, sha1_hash = DATA_HUB[name]
os.makedirs(cache_dir, exist_ok=True)
fname = os.path.join(cache_dir, url.split('/')[-1])
if os.path.exists(fname):
sha1 = hashlib.sha1()
with open(fname, 'rb') as f:
while True:
data = f.read(1048576)
if not data:
break
sha1.update(data)
if sha1.hexdigest() == sha1_hash:
return fname # 命中缓存
print(f'正在从{url}下载{fname}...')
r = requests.get(url, stream=True, verify=True)
with open(fname, 'wb') as f:
f.write(r.content)
return fname
我们还需实现两个实用函数: 一个将下载并解压缩一个zip或tar文件, 另一个是将使用的所有数据集从DATA_HUB
下载到缓存目录中。
def download_extract(name, folder=None): #@save
"""下载并解压zip/tar文件"""
fname = download(name)
base_dir = os.path.dirname(fname)
data_dir, ext = os.path.splitext(fname)
if ext == '.zip':
fp = zipfile.ZipFile(fname, 'r')
elif ext in ('.tar', '.gz'):
fp = tarfile.open(fname, 'r')
else:
assert False, '只有zip/tar文件可以被解压缩'
fp.extractall(base_dir)
return os.path.join(base_dir, folder) if folder else data_dir
def download_all(): #@save
"""下载DATA_HUB中的所有文件"""
for name in DATA_HUB:
download(name)
三: 访问和读取数据集
import numpy as np
import pandas as pd
import torch
from torch import nn
from d2l import torch as d2l
DATA_HUB['kaggle_house_train'] = ( # @save
DATA_URL + 'kaggle_house_pred_train.csv',
'585e9cc93e70b39160e7921475f9bcd7d31219ce')
DATA_HUB['kaggle_house_test'] = ( # @save
DATA_URL + 'kaggle_house_pred_test.csv',
'fa19780a7b011d9b009e8bff8e99922a8ee2eb90')
train_data = pd.read_csv(download('kaggle_house_train'))
test_data = pd.read_csv(download('kaggle_house_test'))
print(train_data.shape)
print(test_data.shape)
print(train_data.iloc[0:4, [0, 1, 2, 3, -3, -2, -1]])#输出前三行和倒数前三的数据
输出:让我们看看前四个和最后两个特征,以及相应标签(房价)。
我们可以看到,在每个样本中,第一个特征是ID, 这有助于模型识别每个训练样本。 虽然这很方便,但它不携带任何用于预测的信息。 因此,在将数据提供给模型之前,我们将其从数据集中删除。
# 【技巧1】把认为没关系的特征给去掉!
# 这个地方不仅删除了第0列(id),最后一列(salesprice)也删除了。train_data提取除了从第1列到倒数第一列的数据
# 这里第一列是ID,最后一列是要预测的价格,在训练数据的X也就是特征中都不需要,价格数据后面直接放到train_labels。
# train_data要干掉最后label列,test_data没有label列
# test_data也去ID号那一列,concat是先把训练集和测试机合起来做预处理,最后预测要用
a = train_data.iloc[:, 1:-1]
b = test_data.iloc[:, 1:]
all_features = pd.concat((train_data.iloc[:, 1:-1], test_data.iloc[:, 1:]))
print(all_features)
# 文本处理。若无法获得测试数据,则可根据训练数据计算均值和标准差 pandas的object是python里的str,不是object的话那么就是一个数值
numeric_features = all_features.dtypes[all_features.dtypes != 'object'].index
numeric_features1 = all_features.index
print(type(numeric_features))
四:数据预处理
如上所述,我们有各种各样的数据类型。 在开始建模之前,我们需要对数据进行预处理。 首先,我们将所有缺失的值替换为相应特征的平均值。然后,为了将所有特征放在一个共同的尺度上, 我们通过将特征重新缩放到零均值和单位方差来标准化数据:
我们标准化数据有两个原因: 首先,它方便优化。 其次,因为我们不知道哪些特征是相关的, 所以我们不想让惩罚分配给一个特征的系数比分配给其他任何特征的系数更大。
# 所有缺失的值替换为相应特征的平将均值,是的没有缺失值存在。方法是:通过将特征重新缩放到零均值和单位方差来标准化数据。这样就使均值为 0,标准差为 1
# 因为这里有训练集和测试集,所以算他们总的平均值,但实际应用中可能没有测试集,只能计算训练集的均值和方差
# 作用是把那些数值特征均值变成0,方差变成1
all_features[numeric_features] = all_features[numeric_features].apply(
lambda x: (x - x.mean()) / (x.std()))
# 在标准化数据之后,所有均值消失,因此我们可以将缺失值设置为0,有些可能没有采样到,那就把它设置为0(debug可以看得出)
all_features[numeric_features] = all_features[numeric_features].fillna(0)
# “Dummy_na=True”将“na”(缺失值)视为有效的特征值,并为其创建指示符特征
# 独热编码详见04_3.py 把特征数量增加了,实验证明,不是一行只有一个。而是看你特征怎么样,如果你特征只有两种的话,那么这一列分成两列的时候就是一行一个了,但是
#当数据大的时候,就会很多的1和0,很复杂,但是也不影响后面的计算
all_features = pd.get_dummies(all_features, dummy_na=True)
print(all_features)
用了 pd.get_dummies(all_features, dummy_na=True)方法后,将all_features的形状从79列变成了331列,将那些不是数字的值也当成一列。如,“MSZoning”包含值“RL”和“Rm”。 我们将创建两个新的指示器特征“MSZoning_RL”和“MSZoning_RM”,其值为0或1。 根据独热编码,如果“MSZoning”的原始值为“RL”, 则:“MSZoning_RL”为1,“MSZoning_RM”为0。 pandas
软件包会自动为我们实现这一点。
可以看到此转换会将特征的总数量从79个增加到331个。 最后,通过values
属性,我们可以 从pandas
格式中提取NumPy格式,并将其转换为张量表示用于训练。
# 得到行数
n_train = train_data.shape[0]
print(n_train)
# dtype默认是numpy是float64的,所以变成32的
# 显示数据预处理,转换均值方差来标准化数据,独热编码。然后就开始转换类型
train_features = torch.tensor(all_features[:n_train].values, dtype=torch.float32)
test_features = torch.tensor(all_features[n_train:].values, dtype=torch.float32) # 原来最后预测用的
# 把上面去掉的最后一列拿回来
train_labels = torch.tensor(
train_data.SalePrice.values.reshape(-1, 1), dtype=torch.float32)
# 这就是为什么要把train_features转换成numpy类型。因为torch无法处理其他类型
# 返回独热编码后的训练数据的,所有列
in_features = train_features.shape[1] # ([1460*331])取331
五:训练
首先,我们训练一个带有损失平方的线性模型。 显然线性模型很难让我们在竞赛中获胜,但线性模型提供了一种健全性检查, 以查看数据中是否存在有意义的信息。 如果我们在这里不能做得比随机猜测更好,那么我们很可能存在数据处理错误。 如果一切顺利,线性模型将作为基线(baseline)模型, 让我们直观地知道最好的模型有超出简单的模型多少。
房价就像股票价格一样,我们关心的是相对数量,而不是绝对数量。
例如,如果我们在俄亥俄州农村地区估计一栋房子的价格时, 假设我们的预测偏差了10万美元, 然而那里一栋典型的房子的价值是12.5万美元, 那么模型可能做得很糟糕。 另一方面,如果我们在加州豪宅区的预测出现同样的10万美元的偏差, (在那里,房价中位数超过400万美元) 这可能是一个不错的预测。
def get_net():
net = nn.Sequential(nn.Linear(in_features, 1)) # 只有价格这一个特征 # ([331*1])
return net # 是不是生成网络但是没有数据? 有了,在训练那把训练数据传给了这个网络了
loss = nn.MSELoss() # 然后把y-hat和y当作参数传给loss就能计算出loss了
# 因为房价的数值一般都比较大,所以预测估计时使用log,使用相对误差也就是百分比
def log_rmse(net, features, labels):
# 为了在取对数时进一步稳定该值,将小于1的值设置为1(最小范围控制在1)
clipped_preds = torch.clamp(net(features), 1, float('inf')) # 这里的话是把模型输出的值限制在1和inf之间
rmse = torch.sqrt(loss(torch.log(clipped_preds), # 做log后丢到mse里面去loss,再正常的做线性回归
torch.log(labels)))
return rmse.item()
与前面的部分不同,我们的训练函数将借助Adam优化器 (我们将在后面章节更详细地描述它)。 Adam优化器的主要吸引力在于它对初始学习率不那么敏感。
# 训练部分的总结:用(k-1)个切分的作为train,剩下的1个作为验证集(这样的操作会重复五次,这样每个切分的数据集都有机会作为验证集)。
# 然后用k-1个切分的数据集拿去训练,得出epoch轮后更新好的w和b(权重这次方法w和b是系统随机赋值,也可以根据自己需求修改参数自己设置),把样本集放进log_rmse里面
# 然后算出它的均值损失。 验证集的话是更新好的w和b直接与验证集的数据联合计算出test—hat,然后与test-labels计算出均值误差。一共有epoch个误差值
# Adam(比较平滑的SGD)优化器的主要吸引力在于它对初始学习率不那么敏感。以前一直用SGD,可能lr没调好就会出问题,这里的test就是验证集
def train(net, train_features, train_labels, test_features, test_labels,
num_epochs, learning_rate, weight_decay, batch_size):
train_ls, test_ls = [], []
# 同样是转换类型给torch用
# 1168/64=18.25,那么load_array就是19,每次遍历train_iter需要19次的64个batch—size,不过这个站在GPU速度才会明显,cpu上用不用batch_size计算时间都一样
train_iter = d2l.load_array((train_features, train_labels), batch_size=64) # 加入batch—size给linear网络
# # 这里使用的是Adam优化算法
# net.parameters():网络中的parameters包含的参数有网络连接权重W和偏置bias
optimizer = torch.optim.Adam(net.parameters(),
lr=learning_rate,
weight_decay=weight_decay)
for epoch in range(num_epochs):
for X, y in train_iter:
optimizer.zero_grad()
# 这里的y是真实的数据,要用net(x)(y^hat = wx+b)计算出y-hat,再与y计算loss
l = loss(net(X), y) # 把x的形状(batch_size,输入的特征数,这里的输入特征比较特殊,是头顶上的标号,平时看的不都是行数x嘛,其实道理都是一样的)
l.backward()
optimizer.step()
#每次dataloader把最后一次的w和b拿出来,再去做损失,整个算法最终目的是,先系统随机初始化w,b,拿x去算出一条曲线,优化w和b,尽量可以拟合x对于的y值!
train_ls.append(log_rmse(net, train_features, train_labels))
# 这么隐蔽,刚开始不知道为什么要写判断test——labels是否为空。原来是预测那里训练部分不给test集了
if test_labels is not None:
test_ls.append(log_rmse(net, test_features, test_labels))
return train_ls, test_ls
六:K折交叉验证
K折交叉验证, 它有助于模型选择和超参数调整。 我们首先需要定义一个函数,在k折交叉验证过程中返回第i折的数据。 具体地说,它选择第i个切片作为验证数据,其余部分作为训练数据。 注意,这并不是处理数据的最有效方法,如果我们的数据集大得多,会有其他解决办法。
# 折交叉验证A(就是拿出验证集和测试集,很简单的东西)
# 具体地说,它选择第k个切片作为验证数据,其余(k-1)部分作为训练数据。 注意,这并不是处理数据的最有效方法,如果我们的数据集大得多,会有其他解决办法。
def get_k_fold_data(k, i, X, y): # k是折数,i是当前第几折
assert k > 1
fold_size = X.shape[0] // k
X_train, y_train = None, None
# 切成k下
for j in range(k):
idx = slice(j * fold_size, (j + 1) * fold_size) # 一折的大小,100的话,5折就是20
X_part, y_part = X[idx, :], y[idx] # 对应的那一折的部分取出来(xpart行列都很多,y只有一列)
if j == i: # 作为验证集
X_valid, y_valid = X_part, y_part
elif X_train is None:
X_train, y_train = X_part, y_part
else:
X_train = torch.cat([X_train, X_part], 0)
y_train = torch.cat([y_train, y_part], 0)
return X_train, y_train, X_valid, y_valid # 返回当前的下训练集和测试集
当我们在k折交叉验证中训练k次后,返回训练和验证误差的平均值
def k_fold(k, train_features, train_labels, num_epochs, learning_rate, weight_decay,
batch_size):
train_l_sum, valid_l_sum = 0, 0
for i in range(1, k): # k是折数,i是当前第几折 (一共循环k折,每次拿出对应第k个切片当作验证机,其他当作数据集)
# data返回了x训练集,y—labels训练集,以及他们的验证集,下面加个*data就可以访问元组里面的4个东西了
data = get_k_fold_data(k, i, train_features, train_labels)
net = get_net() # 生成神经网络,待给入输入特征数量进行训练
# 丢进去训练,返回训练损失,和验证损失
train_ls, valid_ls = train(net, *data, num_epochs, learning_rate,
weight_decay, batch_size)
train_l_sum += train_ls[-1] # 由图可知,负-1表示最后一次的loos,因为每一层train都要循环,然后100次后append起来
valid_l_sum += valid_ls[-1]
if i == 1: # 为了方便查看,只画出k=1的图片,但是五次的平均loss都不好差太多,所以只看一个即可
# 为什么上面train不直接返回最后一个loss?这样不是更方便赋值吗?因为要看效果图,所以要用append得出所有epoch的loss
d2l.plot(list(range(1, num_epochs + 1)), [train_ls, valid_ls],
xlabel='epoch', ylabel='rmse', xlim=[1, num_epochs],
legend=['train', 'valid'], yscale='log')
print(f'折{i + 1},训练log rmse{float(train_ls[-1]):f}, '
f'验证log rmse{float(valid_ls[-1]):f}')
return train_l_sum / k, valid_l_sum / k # 求平均
七:模型选择
在本例中,我们选择了一组未调优的超参数,并将其留给读者来改进模型。 找到一组调优的超参数可能需要时间,这取决于一个人优化了多少变量。 有了足够大的数据集和合理设置的超参数,k折交叉验证往往对多次测试具有相当的稳定性。 然而,如果我们尝试了不合理的超参数,我们可能会发现验证效果不再代表真正的误差。
k, num_epochs, lr, weight_decay, batch_size = 5, 100, 4, 0, 64
train_l, valid_l = k_fold(k, train_features, train_labels, num_epochs, lr,
weight_decay, batch_size)
print(f'{k}-折验证: 平均训练log rmse: {float(train_l):f}, '
f'平均验证log rmse: {float(valid_l):f}')
请注意,有时一组超参数的训练误差可能非常低,但k折交叉验证的误差要高得多, 这表明模型过拟合了。 在整个训练过程中,我们希望监控训练误差和验证误差这两个数字。 较少的过拟合可能表明现有数据可以支撑一个更强大的模型, 较大的过拟合可能意味着我们可以通过正则化技术来获益。
八:预测
# 其实可以直接拿模型去训练加预测的,但是因为要看一下这个模型好不好,所有用了K-交叉验证特别的去训练看了一下效果。
def train_and_pred(train_features, test_features, train_labels, test_data,
num_epochs, lr, weight_decay, batch_size):#使用k则交叉后得出最优的weight_decay,num_epochs,batch_size这些超参数,再做预测
net = get_net()
train_ls, _ = train(net, train_features, train_labels, None, None,
num_epochs, lr, weight_decay, batch_size)
d2l.plot(np.arange(1, num_epochs + 1), [train_ls], xlabel='epoch',
ylabel='log rmse', xlim=[1, num_epochs], yscale='log')
# 这次就不做平均误差了,因为那是训练的时候才需要计算平均误差,以便于了解训练的模型到底好不好,
print(f'训练log rmse:{float(train_ls[-1]):f}')
# 将网络应用于测试集。net里面已经有了100轮之后的w和b了,传入test—feature给net就能算出test-hat了,也就是房价!!!!!!用.detach().numpy() 把它的梯度去掉变成常数输出来
preds = net(test_features).detach().numpy()
# 将其重新格式化以导出到Kaggle
# test_data['SalePrice']是把估计出来的房价加入test_data的列表的最后一列里去
# reshape(1, -1)是转换成
test_data['SalePrice'] = pd.Series(preds.reshape(1, -1)[0])
# 当axis = 1的时候,concat就是行对齐,然后将不同列名称的两张表合并
submission = pd.concat([test_data['Id'], test_data['SalePrice']], axis=1)
submission.to_csv('submission.csv', index=False)
train_and_pred(train_features, test_features, train_labels, test_data,
num_epochs, lr, weight_decay, batch_size)
d2l.plt.show()
九:总结:
-
真实数据通常混合了不同的数据类型,需要进行预处理。
-
常用的预处理方法:将实值数据重新缩放为零均值和单位方法;用均值替换缺失值。
-
将类别特征转化为指标特征,可以使我们把这个特征当作一个独热向量来对待。
-
我们可以使用K折交叉验证来选择模型并调整超参数。
-
对数对于相对误差很有用。
所有项目代码+UI界面
视频,笔记和代码,以及注释都已经上传网盘,放在主页置顶文章