第三节课课堂简介:
讲师:秦鹤亮、刘宇涵
日期:2024.7.25
内容:阿里云天池baseline讲解与提升
二手车价格预测
赛题理解
赛题以预测二手车的交易价格为任务,数据集报名后可见并可下载,该数据来自某交易平台的二手车交易记录,总数据量超过40w,包含31列变量信息,其中15列为匿名变量。为了保证比赛的公平性,将会从中抽取15万条作为训练集,5万条作为测试集A,5万条作为测试集B,同时会对name、model、brand和regionCode等信息进行脱敏。
以下为整理后的数据概况:
SaleID 交易ID,唯一编码
name 汽车交易名称,已脱敏 (意思是对敏感数据已经进行过变形处理了)
regDate 汽车注册日期,例如20160101,2016年01月01日
model 车型编码,已脱敏
brand 汽车品牌,已脱敏
bodyType 车身类型:豪华轿车:0,微型车:1,厢型车:2,大巴车:3,敞篷车:4,双门汽车:5,商务车:6,搅拌车:7
fuelType 燃油类型:汽油:0,柴油:1,液化石油气:2,天然气:3,混合动力:4,其他:5,电动:6
gearbox 变速箱:手动:0,自动:1
power 发动机功率:范围 [ 0, 600 ]
kilometer 汽车已行驶公里,单位万km
notRepairedDamage 汽车有尚未修复的损坏:是:0,否:1
regionCode 地区编码,已脱敏
seller 销售方:个体:0,非个体:1
offerType 报价类型:提供:0,请求:1
creatDate 汽车上线时间,即开始售卖时间
price 二手车交易价格(预测目标)
v系列特征 匿名特征,包含v0-14在内13个匿名特征。
所以我们可以知道:
1.此题为传统的数据挖掘问题,通过数据科学以及机器学习深度学习的办法来进行建模得到结果。
2.此题是一个典型的回归问题。
3.主要应用xgb、lgb、catboost,以及pandas、numpy、matplotlib、seabon、sklearn、keras等等4.数据挖掘常用库或者框架来进行数据挖掘任务。
5.通过EDA来挖掘数据的联系和自我熟悉数据。
数据分析
在机器学习中,数据分析过程是准备和优化数据以便进行有效建模的关键环节。对一些重要代码进行摘录讲解。
读取数据
test_data = pd.read_csv('/gemini/data-1/used_car_testB_20200421.csv', sep=' ')
test_data.shape
train_data = pd.read_csv('/gemini/data-1/used_car_train_20200313.csv', sep=' ')
train_data.shape
2. 数据合并与清洗
合并训练集和测试集以便统一处理。
data = pd.concat([train_data, test_data])
对于数据中的特殊符号(如'-'),将其替换为数值(如'-1'),以统一数据格式。data.replace('-', '-1')
:将数据中的 '-'
替换为 '-1'
。对训练集数据进行简略查看,发现训练集中['model','bodyType','fuelType',gearbox']这4列均有缺失值,并且'notRepairedDamage'列为字符串类型变量'object',其中”-“有缺失项,因此用‘-1’代替。这通常是为了处理缺失值或异常值,使得后续处理变得更一致。
data = data.replace('-', '-1')
处理缺失值,例如使用众数、中位数或特定值填充。
data['notRepairedDamage'] = data['notRepairedDamage'].astype('float32')
data['power'] = data['power'].apply(lambda x: x if x <= 600 else 600)
去除可能无关或冗余的列,如name和regionCode。
data.drop(['name', 'regionCode'], axis=1, inplace=True)
数字特征
#绘制所有变量的柱形图,查看数据
data.hist(bins=100,figsize=(15,15))
plt.cla() #清除axes
特征工程
特征处理(Feature Engineering)是指在机器学习模型开发过程中,对原始数据进行预处理,以提取更有价值的特征供模型使用的过程。特征处理的好坏直接影响到模型的性能。特征处理过程中通常会涉及到数据清洗、特征编码、特征缩放、特征选择、特征衍生、特征降维等操作。对一些重要代码进行摘录讲解。
config = {
'epoch': 10,
'batch_size': 512,
'learning_rate': 8e-3,
'device': 'cuda',
"num_cols": ['regDate', 'creatDate', 'power', 'kilometer', 'v_0', 'v_1', 'v_2', 'v_3', 'v_4', 'v_5', 'v_6', 'v_7', 'v_8', 'v_9', 'v_10',
'v_11', 'v_12', 'v_13', 'v_14'],
"cate_cols": ['model', 'brand', 'bodyType', 'fuelType', 'gearbox', 'seller', 'notRepairedDamage']
}
-
epoch
: 训练的轮数,这里设置为 10 轮。 -
batch_size
: 每批次处理的样本数量,你设置为 512。 -
learning_rate
: 学习率,控制模型权重更新的幅度,设置为 (8 \times 10^{-3})(即 0.008)。 -
device
: 计算设备,cuda
表示你将使用 NVIDIA GPU 进行训练。 -
num_cols
: 数值特征的列名列表,这些列在模型中会被视为连续变量。 -
cate_cols
: 类别特征的列名列表,这些列会被视为分类变量,可能需要编码或转换处理。
# 定义One-Hot编码函数
def oneHotEncode(df, colNames):
for col in colNames:
dummies = pd.get_dummies(df[col], prefix=col)
df = pd.concat([df, dummies],axis=1)
df.drop([col], axis=1, inplace=True)
return df
oneHotEncode
函数的作用是将数据中的分类变量(即类别特征)转换为One-Hot编码格式。One-Hot编码是处理分类数据时的一种常用方法,它将每个类别转换为一个二进制向量,使得机器学习模型可以有效地使用这些数据。
data.notRepairedDamage = data.notRepairedDamage.astype('float32')
# 处理离散数据
for col in config['cate_cols']:
data[col] = data[col].fillna('-1')
data = oneHotEncode(data, config['cate_cols'])
# 处理连续数据
for col in config['num_cols']:
data[col] = data[col].fillna(0)
data[col] = (data[col]-data[col].min()) / (data[col].max()-data[col].min())
-
data.notRepairedDamage = data.notRepairedDamage.astype('float32')
:将notRepairedDamage
列的数据类型转换为float32
。这是为了节省内存或符合模型的输入要求。 -
for col in config['cate_cols']: data[col] = data[col].fillna('-1')
:对config['cate_cols']
中的所有分类特征列,用'-1'
填充缺失值。这使得缺失的数据可以作为一个特殊类别来处理。 -
data = oneHotEncode(data, config['cate_cols'])
:使用前面定义的oneHotEncode
函数将这些分类特征列进行One-Hot编码,转换为模型可以处理的格式。 -
for col in config['num_cols']: data[col] = data[col].fillna(0)
:对config['num_cols']
中的所有连续特征列,用0填充缺失值。 -
data[col] = (data[col] - data[col].min()) / (data[col].max() - data[col].min())
:对连续特征进行归一化,将数据缩放到[0, 1]
的范围。这有助于使特征在相同的尺度上,从而提高模型的训练效果和收敛速度。
建模调参
建模前数据预处理
# 暂存处理后的test数据集
test_data = data[pd.isna(data.price)]
test_data.to_csv('./one_hot_testB.csv')
# 删除test数据(price is nan)
data.reset_index(inplace=True)
train_data = data
train_data = train_data.drop(data[pd.isna(data.price)].index)
train_data.shape
# 删除ID
train_data.drop(['SaleID'], axis=1, inplace=True)
# 打乱
train_data = train_data.sample(frac=1)
# 分离目标
train_target = train_data['price']
train_data.drop(['price', 'index'], axis=1, inplace=True)
# 分离出验证集,用于观察拟合情况
validation_data = train_data[:10000]
train_data = train_data[10000:]
validation_target = train_target[:10000]
train_target = train_target[10000:]
1.这行代码从原始数据 data
中筛选出价格 (price
) 为缺失值 (NaN
) 的记录,存储在 test_data
中,并将其保存为 CSV 文件 one_hot_testB.csv
。这通常是为了后续进行预测或模型评估。
2.reset_index
用于重置索引以确保索引顺序从零开始。然后,从 data
中删除价格为缺失值的记录,得到 train_data
。这样,train_data
只包含有价格信息的记录。
3.这行代码从 train_data
中删除 SaleID
列。通常,ID 列对模型训练没有帮助,可能只会增加噪声。
4.使用 sample(frac=1)
方法将数据打乱,以确保数据的随机性。这有助于提高模型训练的泛化能力。
5.将目标变量 price
从 train_data
中分离出来,存储在 train_target
中,然后从 train_data
中删除 price
和 index
列,留下特征列。
6.将 train_data
和 train_target
的前 10,000 条记录分离出来作为验证集,其余部分作为实际训练集。这种方法可以帮助在训练过程中监控模型在未见过的数据上的表现,从而避免过拟合。
建模指的是选择一个合适的模型,并用数据对其进行训练。常见的模型包括线性回归、决策树、支持向量机、神经网络等。我们需要考虑数据类型、任务类型、模型复杂度等因素来选择合适的模型。
调参(Parameter Tuning)是指调整模型的超参数,以获得最佳的模型性能。超参数是模型在训练过程中设置的参数,不会通过训练数据直接学习到。例如:
- 线性回归:正则化参数(L1、L2)。
- 决策树:树的深度、最小样本分裂数。
- 支持向量机:惩罚参数(C)、核函数的类型和参数。
- 神经网络:学习率、层数、每层的神经元数目、激活函数类型等。
调参的方法包括:
- 网格搜索(Grid Search):定义一个超参数的范围,系统地尝试所有可能的组合。
- 随机搜索(Random Search):在超参数的空间中随机选择组合进行测试,相比网格搜索更高效。
- 贝叶斯优化(Bayesian Optimization):通过建立超参数和模型性能之间的概率模型,逐步选择最有可能提升性能的超参数组合。
- 交叉验证(Cross-Validation):将数据集划分为多个子集,多次训练和验证模型,以评估模型的性能和稳定性。
调参的目标是找到一组超参数,使模型在验证集或测试集上的表现最优。有效的调参可以显著提高模型的预测性能。
我们选择了深度神经网络的模型。
# 定义网络结构
class Network(nn.Module):
def __init__(self, in_dim, hidden_1, hidden_2, hidden_3, hidden_4):
super().__init__()
self.layers = nn.Sequential(
nn.Linear(in_dim, hidden_1),
nn.BatchNorm1d(hidden_1),
nn.ReLU(),
nn.Linear(hidden_1, hidden_2),
nn.BatchNorm1d(hidden_2),
nn.ReLU(),
nn.Linear(hidden_2, hidden_3),
nn.BatchNorm1d(hidden_3),
nn.ReLU(),
nn.Linear(hidden_3, hidden_4),
nn.BatchNorm1d(hidden_4),
nn.ReLU(),
nn.Linear(hidden_4, 1)
)
def forward(self, x):
y = self.layers(x)
return y
# 定义网络
model = Network(train_data.shape[1], 256, 256, 256, 32)
model.to(config['device'])
# 使用Xavier初始化权重
for line in model.layers:
if type(line) == nn.Linear:
print(line)
nn.init.xavier_uniform_(line.weight)
__init__
方法:定义了网络的层结构。使用了nn.Sequential
来简化模型的创建流程,其中包括多个全连接层 (nn.Linear
)、批归一化层 (nn.BatchNorm1d
) 和激活函数 (nn.ReLU
)。forward
方法:定义了前向传播的过程,通过self.layers
处理输入数据x
,并返回网络输出。- 通过传入特征维度(
train_data.shape[1]
)和隐藏层的尺寸(256, 256, 256, 32),实例化Network
类。 model.to(config['device'])
将模型转移到指定的设备上(例如 GPU)。- 遍历模型中的每一层,如果该层是
nn.Linear
类型的线性层,则应用 Xavier 初始化 (nn.init.xavier_uniform_
) 对其权重进行初始化。Xavier 初始化有助于保持前向传播时的激活值和反向传播时的梯度稳定。
将数据转化为tensor,并移动到cpu或cuda上。
# 将数据转化为tensor,并移动到cpu或cuda上
train_features = torch.tensor(train_data.values, dtype=torch.float32, device=config['device'])
train_num = train_features.shape[0]
train_labels = torch.tensor(train_target.values, dtype=torch.float32, device=config['device'])
validation_features = torch.tensor(validation_data.values, dtype=torch.float32, device=config['device'])
validation_num = validation_features.shape[0]
validation_labels = torch.tensor(validation_target.values, dtype=torch.float32, device=config['device'])
定义损失函数和优化器。
# 定义损失函数和优化器
criterion = nn.MSELoss()
criterion.to(config['device'])
optimizer = optim.Adam(model.parameters(), lr=config['learning_rate'])
nn.MSELoss()
:这是均方误差损失函数(Mean Squared Error Loss),适用于回归任务。它计算预测值和真实值之间的均方误差。criterion.to(config['device'])
:将损失函数移动到指定的设备上(例如 GPU)。不过,实际上,PyTorch 中的损失函数通常不需要移动到 GPU,因为计算损失时,输入和目标张量都已经在相同的设备上,损失函数会自动处理。可以省略这一步。optim.Adam
:这是 Adam 优化器,一种常用的自适应学习率优化算法。Adam 在很多情况下表现得比传统的梯度下降优化器更好,因为它结合了动量和自适应学习率。model.parameters()
:这个方法返回模型中所有可学习的参数,这些参数将被 Adam 优化器更新。lr=config['learning_rate']
:设置优化器的学习率。config['learning_rate']
应该在配置文件中定义,控制学习过程的步长。
开始训练模型并评估
# 开始训练
mae_list = []
for epoch in range(config['epoch']):
losses = []
model.train()
for i in range(0, train_num, config['batch_size']):
end = i + config['batch_size']
if i + config['batch_size'] > train_num-1:
end = train_num-1
mini_batch = train_features[i: end]
mini_batch_label = train_labels[i: end]
pred = model(mini_batch)
pred = pred.squeeze()
loss = criterion(pred, mini_batch_label)
if torch.isnan(loss):
break
mae = torch.abs(mini_batch_label-pred).sum()/(end-i)
losses.append(mae.item())
optimizer.zero_grad()
loss.backward()
optimizer.step()
model.eval()
pred = model(validation_features)
validation_mae = torch.abs(validation_labels-pred.squeeze()).sum().item()/validation_num
mae_list.append((sum(losses)/len(losses), validation_mae))
print(f"epoch:{epoch + 1} MAE: {sum(losses)/len(losses)}, Validation_MAE: {validation_mae}")
torch.save(model, 'model.pth')
for epoch in range(config['epoch']):
:循环遍历每个训练周期,epoch
代表当前进行到第几轮训练。losses = []
:每一轮的每次迭代中,都会记录下损失。for i in range(0, train_num, config['batch_size'])
:遍历训练数据集,将数据划分成批次。mini_batch = train_features[i: end]
:根据i
到end
的索引切片,获取当前批次的数据特征。mini_batch_label = train_labels[i: end]
:获取该批次的标签。pred = model(mini_batch)
:将批次数据通过模型得到预测值。pred = pred.squeeze()
:如果有必要,去掉维度上多余的维度,例如如果模型输出是 (batch_size, 1),可以通过.squeeze()
变成 (batch_size)。loss = criterion(pred, mini_batch_label)
:计算当前批次的损失。if torch.isnan(loss): break
:检查损失是否是无穷大或NaN,这些情况可能表示模型陷入了数值问题,如梯度爆炸或消失。mae = torch.abs(mini_batch_label-pred).sum()/(end-i)
:计算当前批次的前向预测的平均绝对误差(MAE)。losses.append(mae.item())
:将当前批次的MAE保存到losses
列表中。optimizer.zero_grad()
:在每次迭代开始时,将梯度清零。loss.backward()
:计算损失函数相对于模型参数的梯度。optimizer.step()
:根据梯度更新模型参数。
评估模型
model.eval()
:将模型设置为评估模式,这可能会关闭如 dropout 等在评估时不使用的特性。pred = model(validation_features)
:用验证集数据对模型进行评估,得到预测值。validation_mae = torch.abs(validation_labels-pred.squeeze()).sum().item()/validation_num
:计算在验证集上的平均绝对误差。mae_list.append((sum(losses)/len(losses), validation_mae))
:将每轮的训练损失和验证集上的MAE保存到列表中,用于后续分析。
保存模型
torch.save(model, 'model.pth')
:将模型在当前最佳性能(比如最低的MAE值)时保存,可以用作后续的预测或验证。
输出回归模型
x = np.arange(0, config['epoch'])
y1, y2 = zip(*mae_list)
plt.plot(x, y1, label='train')
plt.plot(x, y2, label='valid')
plt.legend()
plt.show()
进行预测并导出csv文件
import pandas as pd
import torch
from torch import nn
from settings import config
class Network(nn.Module):
def __init__(self, in_dim, hidden_1, hidden_2, hidden_3, hidden_4):
super().__init__()
self.layers = nn.Sequential(
nn.Linear(in_dim, hidden_1),
nn.BatchNorm1d(hidden_1),
nn.ReLU(),
nn.Linear(hidden_1, hidden_2),
nn.BatchNorm1d(hidden_2),
nn.ReLU(),
nn.Linear(hidden_2, hidden_3),
nn.BatchNorm1d(hidden_3),
nn.ReLU(),
nn.Linear(hidden_3, hidden_4),
nn.BatchNorm1d(hidden_4),
nn.ReLU(),
nn.Linear(hidden_4, 1)
)
def forward(self, x):
y = self.layers(x)
return y
model = torch.load('model.pth', map_location=config['device'])
data = pd.read_csv('./one_hot_testB.csv')
data = data.drop(columns=['Unnamed: 0', 'price'])
test = data.drop(columns='SaleID')
test = torch.tensor(test.values, dtype=torch.float32)
pred = model(test)
price = pd.DataFrame(pred.detach().cpu().numpy(), columns=['price'])
res = pd.concat([data.SaleID, price], axis=1)
res.to_csv('output.csv')
两种不同提升方法
此外讲师还介绍了两种进阶方法,依靠这两种方法做出的预测模型更为精确,误差更小。现在在下面进行简要的介绍。
调库法
调库版是指使用现成的机器学习库和工具来构建和训练模型。这通常包括利用流行的机器学习框架(如 TensorFlow、PyTorch 或 scikit-learn)和一些数据预处理库(如 pandas 和 NumPy)来快速实现和调整模型。
特点:
- 高效性:利用成熟的库,能够快速搭建和训练模型。
- 可维护性:使用的库和工具有完善的文档和社区支持。
- 便捷性:现成的库通常提供了许多高效的算法实现和优化策略。
步骤:
- 数据预处理:使用 pandas 对数据进行清洗、处理缺失值和特征工程。
- 特征选择和缩放:利用 scikit-learn 提供的特征选择和缩放工具。
- 模型构建和训练:使用 TensorFlow 或 PyTorch 构建深度学习模型,或使用 scikit-learn 中的传统机器学习算法。
- 调参:利用库中的网格搜索(GridSearchCV)或随机搜索(RandomizedSearchCV)工具调整模型超参数。
- 评估和测试:使用库提供的评估函数计算模型的性能指标,如 MAE、RMSE 等。
手搓法
手搓版是指从头开始手动实现整个模型和训练过程。这通常涉及从基础开始,自己编写数据预处理、特征工程和模型训练代码。这种方式要求较高的技术能力,但可以提供更大的灵活性。
特点:
- 灵活性:可以完全控制每一个细节,进行自定义的优化。
- 学习机会:通过手动实现,可以深入理解机器学习和深度学习的工作原理。
- 挑战性:需要手动实现数据处理、模型构建和优化等多个环节。
步骤:
- 数据预处理:手动编写数据清洗和特征工程代码。
- 模型构建:从头实现机器学习模型或深度学习模型(例如用 NumPy 实现线性回归)。
- 训练过程:手动实现前向传播、损失计算和梯度更新等步骤。
- 评估和测试:计算模型的性能指标。
总结感想
学习机器学习的过程是一段充满挑战与乐趣的旅程。首先,机器学习让我深刻理解了数据在现代决策中的核心地位。通过系统学习,我掌握了从数据收集、清洗到建模和优化的全过程。这个过程不仅要求扎实的数学和统计学基础,还需要具备编程能力和对业务场景的深刻理解。
其次,机器学习的实践让我认识到,理论与实践的结合至关重要。尽管算法和模型在书本上看起来简单,但实际应用时面对的挑战往往复杂且多变。每一个数据集的特点都可能影响模型的效果,这使得特征工程和模型调优变得尤为重要。
此外,我还感受到了机器学习的巨大潜力和广泛应用。从图像识别到自然语言处理,再到推荐系统,机器学习正不断改变我们的生活和工作方式。每一个成功的模型背后,都凝聚着大量的数据和智慧,这不仅是技术的胜利,更是对科学和人类智能的深刻探索。
总的来说,学习机器学习不仅提高了我的技术能力,更让我对数据和智能有了全新的认识。这个领域的快速发展和应用前景,让我充满期待和信心,愿意不断探索和进步。