基于沐神part15课程学习、复现、理解预测房价算法实例
文章目录
沐神在part15课程中,引入了一个“预测房价”的深度学习实例(其实也算不上深度学习,毕竟就简单的单隐藏层线性模型)。
- 待解决问题
🌟实例中要解决的目标问题为:给定一个来源于kaggle网站的训练数据集(train_data)+测试数据集(test_data),基于train_data训练模型,应用与test_data以预测test_data上房价。
- 数据集说明
① train_data:其包含待预测房价的79项特征+房价1项特征,经过独热码编码后变为330项特征+房价1项特征。
② test_data:其包含待预测房价的79项特征,经过独热码编码后变为330项特征。
二、算法实例
2.1实例代码
以免自己对代码中哪一行操作不理解,将每一行的操作进行了详细备注
import numpy as np
import pandas as pd
import torch
from torch import nn
from d2l import torch as d2l
train_data = pd.read_csv('../data/kaggle_house_pred_train.csv') #训练集数据
test_data = pd.read_csv('../data/kaggle_house_pred_test.csv') #测试集数据
'''找出在test_data,但不在train_data的列
train_data比test_data多一个“SalePrice”列,即房价
'''
elements_in_train_not_in_test = train_data.columns.difference(test_data.columns)
print(train_data.shape)
print(test_data.shape)
print(train_data.iloc[0:4, [0, 1, 2, 3, -3, -2, -1]])
#将id列删除,取train_data所有行,1列到-1列,test_data所有行,1列至最后一列
all_features = pd.concat((train_data.iloc[:, 1:-1], test_data.iloc[:, 1:]))
'''处理缺失值,替换为对应的特征的平均值'''
numeric_features = all_features.dtypes[all_features.dtypes != 'object'].index #用来从一个DataFrame中提取非对象类型(即非字符串类型)的列名
# 通过将特征重新缩放到零均值和单位方差来标准化数据,对每列进行了标准化处理,每列的均值为0,标准差为1的标准正态分布。
# =============================================================================
# 标准化不会改变数据之间的相对关系。
# 标准化不会改变数据的分布形状。如果数据原来是正态分布的,标准化后仍然是正态分布;如果数据原来是偏态分布的,标准化后仍然是偏态分布。
# 对于某些机器学习算法(如线性回归、支持向量机、神经网络等),标准化可以提高模型的收敛速度和性能,因为这些算法对输入数据的尺度敏感。
# =============================================================================
all_features[numeric_features] = all_features[numeric_features].apply(
lambda x: (x - x.mean()) / (x.std()))
# 将所有缺失值替换为均值,即0
all_features[numeric_features] = all_features[numeric_features].fillna(0)
'''处理离散值。 我们用一次独热编码(one-hot)替换它们'''
all_features = pd.get_dummies(all_features, dummy_na=True)
all_features.shape
'''从pandas格式中提取NumPy格式,并将其转换为张量表示,训练集,验证集,训练标签'''
n_train = train_data.shape[0]
train_features = torch.tensor(all_features[:n_train].values.astype(float),
dtype=torch.float32) #train_features取all_features前0至n_train行数据
test_features = torch.tensor(all_features[n_train:].values.astype(float),
dtype=torch.float32) #test_features取all_features前n_train至最后一行数据
train_labels = torch.tensor(train_data.SalePrice.values.reshape(-1, 1).astype(float),
dtype=torch.float32) #.reshape(-1, 1) 方法将一维数组转换为二维数组,其中 -1 表示自动计算该维度的大小,1 表示第二个维度的大小为1。这通常用于将一维数组转换为列向量。
loss = nn.MSELoss() #MSELoss 是均方误差损失函数,预测值与真实值之间的平方差的平均值。
in_features = train_features.shape[1]
'''采用单层的线性回归'''
def get_net():
net = nn.Sequential(nn.Linear(in_features, 1)) #用于创建一个简单的线性回归模型,Y=WX+b,W、b在创建网络时会随机赋初值
return net
'''用相对误差来估计,因为房价的偏差还要考虑房子的状态异同,例如大小、装修风格等'''
def log_rmse(net, features, labels):
clipped_preds = torch.clamp(net(features), 1, float('inf')) #torch.clamp 是 PyTorch 中用于将张量中的值限制在一个指定范围内的重要函数,这里用于将 net(features) 的输出值限制在1到正无穷之间,在这里net输出的负值就直接改为1,这样不会带来误差吗?
rmse = torch.sqrt(loss(torch.log(clipped_preds), torch.log(labels)))
return rmse.item() #将一个标量(scalar)张量转换为一个 Python 标准数据类型的值
'''用于打印net网络模型的参数,weight,bias'''
def outputPara_net(net):
# 输出模型的结构
print("Model Structure:")
print(net)
# 输出模型的参数
print("\nModel Parameters:")
for name, param in net.named_parameters():
print(f"Parameter Name: {name}")
print(f"Parameter Values: {param}")
print(f"Parameter Shape: {param.shape}")
print("-" * 50)
'''我们的训练函数将借助Adam优化器,理解为平滑的SGD,优势是对于学习率不敏感'''
def train(net, train_features, train_labels, test_features, test_labels,
num_epochs, learning_rate, weight_decay, batch_size):
train_ls, test_ls = [], []
train_iter = d2l.load_array((train_features, train_labels), batch_size) #构造样本迭代获取器
optimizer = torch.optim.Adam(net.parameters(), lr=learning_rate,
weight_decay=weight_decay)
for epoch in range(num_epochs): #一个 epoch 指的是在整个训练数据集上完成一次完整的前向传播和反向传播的过程
for X, y in train_iter: #每次不重复地随机获取batch_size个训练样本
optimizer.zero_grad() #清零梯度
l = loss(net(X), y) #计算均方误差
l.backward() #反向传播
optimizer.step() #更新参数,这一步之后模型就已经能够根据给定输入进行输出了
train_ls.append(log_rmse(net, train_features, train_labels))
if test_labels is not None:
test_ls.append(log_rmse(net, test_features, test_labels))
#测试模型训练情况
# aaa_t = net(test_features).detach().numpy()
return train_ls, test_ls
'''获取K折训练、验证'''
def get_k_fold_data(k, i, X, y):
assert k > 1 #assert 语句在 Python 中用于判断某个条件为真,如果条件为假,则会引发 AssertionError 异常。
fold_size = X.shape[0] // k #这里的 // 是Python中的整数除法运算符,它返回除法的整数部分,舍去小数部分。
X_train, y_train = None, None
for j in range(k):
idx = slice(j * fold_size, (j + 1) * fold_size) #slice用于生成一个切片对象,该切片对象可以用于从数组或列表中选择特定范围的元素
X_part, y_part = X[idx, :], y[idx] #取Xidx行,所有列,y的idx行
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) #torch.cat 是 PyTorch 中用于沿指定维度连接张量的函数,0 表示沿行方向(即第一个维度)进行连接。
y_train = torch.cat([y_train, y_part], 0)
return X_train, y_train, X_valid, y_valid #X_train、y_train训练集取的是除i个slice外的数据,X_valid、y_valid取的是第i个slice的数据
'''k折数据交叉验证,返回训练和验证误差的平均值'''
def k_fold(k, X_train, y_train, num_epochs, learning_rate, weight_decay,
batch_size):
train_l_sum, valid_l_sum = 0, 0
for i in range(k):
data = get_k_fold_data(k, i, X_train, y_train) #获取k折数据,分为训练集和验证集
net = get_net() #这里是创建一个新的net,因此k折交叉验证是分别训练了5个net
# print("=" * 50)
# print(f'Before:第{i+1}折训练前net参数:\n')
# outputPara_net(net)
train_ls, valid_ls = train(net, *data, num_epochs, learning_rate,
weight_decay, batch_size) #开始训练,train_ls, valid_ls元素个数为100,是因为num_epochs=100,一个训练数据集通过train_iter完整训练了100次
# print("=" * 50)
# print(f'After:第{i+1}折训练后net参数:\n')
# outputPara_net(net)
train_l_sum += train_ls[-1] #‘-1’代表累加至列表中的最后一个元素
valid_l_sum += valid_ls[-1]
if i == 0:
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'fold {i + 1}, train log rmse {float(train_ls[-1]):f}, '
f'valid log rmse {float(valid_ls[-1]):f}')
return train_l_sum / k, valid_l_sum / k #为什么除以k?是因为k折交叉验证,loss是反复是原始数据反复训练了k次的累加结果,k次中训练集和验证集都不一样,这也说明了k折交叉验证有充分利用数据的作用
'''模型选择,k折、epoch、学习率、权重衰减系数、批量大小'''
#需要不断调整下述参数,以保证验证误差达到想要的标准
k, num_epochs, lr, weight_decay, batch_size = 5, 100, 5, 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}')
'''在完整的训练集上进行训练,在完整的训练集上作训练一次应该是为了做对比'''
def train_and_pred(train_features, test_feature, train_labels, test_data,
num_epochs, lr, weight_decay, 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'train log rmse {float(train_ls[-1]):f}')
# 采用测试集进行验证,即预测一个房子的房价
preds = net(test_features).detach().numpy()
test_data['SalePrice'] = pd.Series(preds.reshape(1, -1)[0]) #reshape(1, -1) 是一种常见的用法,用于将一个一维张量转换为二维张量,其中第一维的大小为1,第二维的大小根据数据自动确定。
submission = pd.concat([test_data['Id'], test_data['SalePrice']], axis=1)
submission.to_csv('../data/submission.csv', index=False)
train_and_pred(train_features, test_features, train_labels, test_data,
num_epochs, lr, weight_decay, batch_size)
2.2实例结果
- k折交叉验证(k=5)的训练误差及验证误差统计:
fold 1, train log rmse 0.170902, valid log rmse 0.156652
fold 2, train log rmse 0.162263, valid log rmse 0.191383
fold 3, train log rmse 0.164044, valid log rmse 0.168348
fold 4, train log rmse 0.168424, valid log rmse 0.154911
fold 5, train log rmse 0.162952, valid log rmse 0.182984
5-折验证: 平均训练log rmse: 0.165717, 平均验证log rmse: 0.170856
由于k折交叉验证过程中是训练了5个网络模型,并且通过train_iter分batch获取样本数据训练时,采样是随机的,因此每一次训练出来的网络模型存在差异,训练误差、验证误差会存在不一致的情况。
- 第1折交叉验证时,训练误差、验证误差随epoch变化情况统计:
- 在完整数据集上训练,训练误差统计(无):
train log rmse 0.162365
2.3补充说明
(1)什么是独热码?
独热编码(One-Hot Encoding)是一种将分类变量(类别特征)转换为二进制向量的编码方法。这种方法常用于机器学习和数据分析中,以便将非数值的类别数据转换为数值形式,从而可以被机器学习算法所处理。
- 直接引入一段代码来解释
import pandas as pd
import numpy as np
# 创建一个示例的DataFrame
data = {
'col1': [1, 2, 3, 4, 5],
'col2': ['A', 'B', 'A', 'C', np.nan],
'col3': [True, False, True, False, True]
}
all_features = pd.DataFrame(data)
# 打印原始数据
print("Original Data:")
print(all_features)
# 使用pd.get_dummies进行one-hot编码,包括处理缺失值
all_features_encoded = pd.get_dummies(all_features, dummy_na=True)
# 打印编码后的数据
print("\nEncoded Data:")
print(all_features_encoded)
- 进行独热码编码后结果如下所示:
Original Data:
col1 col2 col3
0 1 A True
1 2 B False
2 3 A True
3 4 C False
4 5 NaN True
Encoded Data:
col1 col2_A col2_B col2_C col2_nan col3_False col3_True
0 1 1 0 0 0 0 1
1 2 0 1 0 0 1 0
2 3 1 0 0 0 0 1
3 4 0 0 1 0 1 0
4 5 0 0 0 1 0 1
- 解释:
原始数据中,col1全为数值,col2中有‘A’、‘B’、‘C’三类元素值并且还存在np.nan空值,col3中存在‘True’、‘Fals’两类值。
因此col1不用额外编码,而col2将编码为4列,如col2_A列中‘1’代表原始数据中该行col2列中元素为‘A’,‘0’则代表不为‘A’,以此类推col2_B、col2_C、col2-nan、col3_False 、col3_True。
(2)模型中哪些参数可调?
k, num_epochs, lr, weight_decay, batch_size = 5, 100, 5, 0, 64
源码中上述参数可调,含义分别为:
①k折交叉验证中,数据集划分k个slice
②轮次/周期:一个 epoch 指的是在整个训练数据集上完成一次完整的前向传播和反向传播的过程
③学习率:用于调节优化器(源码用的Adam,典型的还有SGD)求解的逼近速度。
④权重衰减系数:和优化器Adam有关
⑤批量大小:用于调节每次训练的样本数量大小
模型可以不断调节这些参数,以达到更小的验证集误差值,即更优的模型。
(3)train_iter是如何取batch数据的?
- 获取样本过程
源码中train_iter作用为样本数据获取迭代器,每次获取batch_size的批量数据,如果是训练集的话,还会再获取样本前将原有数据打乱,然后再进行采样,但是不会重复采样,即一个训练样本仅会被获取一次,若最优一批次不足batch_size个数据时,就获取所有剩余数据。源码为:
train_iter = d2l.load_array((train_features, train_labels), batch_size)
其本质是调用了
torch.utils.data.DataLoader((dataset, batch_size=batch_size, shuffle=True))
- 解释:
dataset一般为训练数据集和训练样本集的tensor张量组;
当 shuffle=True
时,DataLoader
会在每个 epoch 开始时对数据进行随机打乱。这意味着每个 epoch 中的数据顺序是不同的,但不会重复获取数据。
当 shuffle=False
时,DataLoader
会按照数据集中的顺序依次获取数据。这意味着每个 epoch 中的数据顺序是固定的,也不会重复获取数据。
- 示例
可以通过以下示例来检测DataLoader
的取值过程:
import torch
from torch.utils.data import Dataset, DataLoader
# 定义一个简单的数据集类
class SimpleDataset(Dataset):
def __init__(self, data, labels):
self.data = data
self.labels = labels
def __len__(self):
return len(self.data)
def __getitem__(self, idx):
return self.data[idx], self.labels[idx]
# 创建示例数据
data = torch.arange(40) # 100个样本
labels = torch.arange(40) # 100个样本的标签
# 创建数据集实例
dataset = SimpleDataset(data, labels)
# 定义批量大小
batch_size = 10
# 创建DataLoader,shuffle=True
dataloader_shuffle = DataLoader(dataset, batch_size=batch_size, shuffle=True)
# 创建DataLoader,shuffle=False
dataloader_no_shuffle = DataLoader(dataset, batch_size=batch_size, shuffle=False)
# 遍历DataLoader (shuffle=True)
print("Shuffle=True:")
for epoch in range(2): # 2个epoch
print(f"Epoch {epoch + 1}")
for batch_idx, (data_batch, labels_batch) in enumerate(dataloader_shuffle):
print(f"Batch {batch_idx + 1}")
print("Data Batch:", data_batch)
print("Labels Batch:", labels_batch)
print("-" * 50)
# 遍历DataLoader (shuffle=False)
print("\nShuffle=False:")
for epoch in range(2): # 2个epoch
print(f"Epoch {epoch + 1}")
for batch_idx, (data_batch, labels_batch) in enumerate(dataloader_no_shuffle):
print(f"Batch {batch_idx + 1}")
print("Data Batch:", data_batch)
print("Labels Batch:", labels_batch)
print("-" * 50)
结果输出:
Shuffle=True:
Epoch 1
Batch 1
Data Batch: tensor([27, 24, 2, 12, 26, 22, 15, 3, 20, 8])
Labels Batch: tensor([27, 24, 2, 12, 26, 22, 15, 3, 20, 8])
--------------------------------------------------
Batch 2
Data Batch: tensor([33, 29, 13, 4, 23, 36, 28, 14, 17, 39])
Labels Batch: tensor([33, 29, 13, 4, 23, 36, 28, 14, 17, 39])
--------------------------------------------------
Batch 3
Data Batch: tensor([ 5, 35, 10, 18, 0, 37, 21, 16, 30, 34])
Labels Batch: tensor([ 5, 35, 10, 18, 0, 37, 21, 16, 30, 34])
--------------------------------------------------
Batch 4
Data Batch: tensor([19, 31, 25, 6, 11, 7, 32, 9, 1, 38])
Labels Batch: tensor([19, 31, 25, 6, 11, 7, 32, 9, 1, 38])
--------------------------------------------------
Epoch 2
Batch 1
Data Batch: tensor([ 4, 19, 31, 20, 34, 18, 6, 26, 2, 0])
Labels Batch: tensor([ 4, 19, 31, 20, 34, 18, 6, 26, 2, 0])
--------------------------------------------------
Batch 2
Data Batch: tensor([17, 14, 21, 25, 28, 7, 15, 9, 27, 11])
Labels Batch: tensor([17, 14, 21, 25, 28, 7, 15, 9, 27, 11])
--------------------------------------------------
Batch 3
Data Batch: tensor([12, 10, 24, 30, 29, 16, 13, 35, 22, 39])
Labels Batch: tensor([12, 10, 24, 30, 29, 16, 13, 35, 22, 39])
--------------------------------------------------
Batch 4
Data Batch: tensor([32, 23, 37, 36, 3, 8, 5, 38, 1, 33])
Labels Batch: tensor([32, 23, 37, 36, 3, 8, 5, 38, 1, 33])
--------------------------------------------------
Shuffle=False:
Epoch 1
Batch 1
Data Batch: tensor([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
Labels Batch: tensor([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
--------------------------------------------------
Batch 2
Data Batch: tensor([10, 11, 12, 13, 14, 15, 16, 17, 18, 19])
Labels Batch: tensor([10, 11, 12, 13, 14, 15, 16, 17, 18, 19])
--------------------------------------------------
Batch 3
Data Batch: tensor([20, 21, 22, 23, 24, 25, 26, 27, 28, 29])
Labels Batch: tensor([20, 21, 22, 23, 24, 25, 26, 27, 28, 29])
--------------------------------------------------
Batch 4
Data Batch: tensor([30, 31, 32, 33, 34, 35, 36, 37, 38, 39])
Labels Batch: tensor([30, 31, 32, 33, 34, 35, 36, 37, 38, 39])
--------------------------------------------------
Epoch 2
Batch 1
Data Batch: tensor([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
Labels Batch: tensor([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
--------------------------------------------------
Batch 2
Data Batch: tensor([10, 11, 12, 13, 14, 15, 16, 17, 18, 19])
Labels Batch: tensor([10, 11, 12, 13, 14, 15, 16, 17, 18, 19])
--------------------------------------------------
Batch 3
Data Batch: tensor([20, 21, 22, 23, 24, 25, 26, 27, 28, 29])
Labels Batch: tensor([20, 21, 22, 23, 24, 25, 26, 27, 28, 29])
--------------------------------------------------
Batch 4
Data Batch: tensor([30, 31, 32, 33, 34, 35, 36, 37, 38, 39])
Labels Batch: tensor([30, 31, 32, 33, 34, 35, 36, 37, 38, 39])
--------------------------------------------------
(3)什么是k折交叉验证?
k折交叉验证(k-Fold Cross-Validation)是一种常用的评估机器学习模型性能的技术。它的主要目的是通过将数据集划分为 k 个子集(或称为“折”),来更可靠地评估模型的泛化能力。这种方法可以有效地减少模型评估中的方差,提高评估结果的稳定性。
(4)为什么k折交叉验证后训练、验证误差要除以k?
return train_l_sum / k, valid_l_sum / k
因为k折交叉验证,train_l_sum和valid_l_sum是训练出的5个网络误差的累加结果,k次中训练集和验证集都不一样,这也说明了k折交叉验证有充分利用数据的作用。
(5)为什么划分训练集、验证集、测试集?
可以从3者关系说起:
①训练集:用于指导模型训练的数据集。
②验证集:是用来评估模型好坏的数据集,一般而言从训练数据集中划分而来(k折),其与训练数据集不存在交集,它的作用本来不是用于指导训练,因此模型调整超参数时,基于验证数据集得到的均方误差值、相对误差值等是很好的参考。
③测试集:其应用在模型训练好以后,将测试数据集应用在模型上以得到对应输出,一般而言对应待预测数据、待分类数据等。
三、疑问
(1)torch.clamp(net(features), 1, float('inf'))不会使得结果不精确吗?
答:源码中统计时采用log_rmse作为训练集、验证集误差的输出方式,但在实际训练过程中还是采用了均方误差作为损失函数(loss = nn.MSELoss()),因此不影响模型训练。
(2)采用k折交叉验证的方式训练模型,模型如何应用呢?
详细描述:按源码中使用方式而言,采用k折交叉验证方式训练模型,将会得到k个网络模型,并统计k次训练的总平均训练误差和验证误差,以评估模型的好坏程度。
❓但在实际应用当中,是选取k个网络模型中最小训练误差+验证误差的模型进行应用吗?
四、小结
基于此次算法实例,小结一下深度学习的建模、应用过程:
①数据集预处理:包含缺失值处理(标准化+均值替代)、离散值处理(one-hot编码)
②网络模型构建:梳理网络模型输入输出维度(判断数据集特征数+判断待解决问题是预测、分类),根据问题复杂度制定网络层数、模型参数(简单线性模型or其它)
③实现训练过程:选择训练样本(k折or完整数据集、batch批量大小等)、选择优化器(SGD、Adam等)、模型更新(梯度清零、计算损失、反向传播、参数更新)、统计误差
④模型调参:不断更换模型参数(k折、训练周期数、学习率、btach大小等),以实现更小的训练/验证误差
⑤模型应用:将测试集数据输入至模型,得到对应输出。