Kaggle:House Prices

发布时间 2023-11-23 17:22:47作者: newbe3three

Kaggle:House Prices

数据处理

首先是处理数据,导入相应的包,使用pandas读取csv文件,并指定Id列为index,本身Id这一列也不携带预测信息。同时将训练数据和测试数据拼接在一起以便后续的处理。

train_data = pd.read_csv("dataset/train.csv", index_col="Id") # 
test_data = pd.read_csv("dataset/test.csv", index_col="Id")
all_features = pd.concat((train_data.iloc[:, :-1], test_data)) #

将数据粗略地分为两类,一类是连续的数值类型,一类是离散的类型。

对于数值类型的数据,进行零均值和单位方差的标准化处理,同时把缺失值NaN置为0。

numeric_features = all_features.dtypes[all_features.dtypes != 'object'].index
all_features[numeric_features] = all_features[numeric_features].apply(
    lambda x: (x - x.mean()) / (x.std()))
all_features[numeric_features] = all_features[numeric_features].fillna(0)

对于离散数据,使用get_dummies()函数,将它们转换成对应的独热编码。

all_features = pd.get_dummies(all_features, dummy_na=True)

最后,提取出训练特征、训练标签和测试特征集,转换成Tensor对象,并使用Dataloader进行batch。

num_train = train_data.shape[0]
train_features = torch.tensor(all_features[:num_train].values, dtype=torch.float32)
test_features = torch.tensor(all_features[num_train:].values, dtype=torch.float32)
train_labels = torch.tensor(train_data.SalePrice.values.reshape(-1,1), dtype=torch.float32)
dataset = torch.utils.data.TensorDataset(train_features, train_labels)
loader = DataLoader(dataset=dataset, batch_size=32, shuffle=True)

定义模型、损失和优化

定义一个简单的线性模型作为基线(baseline)模型,可以用以衡量之后较复杂模型的性能。

loss = nn.MSELoss()
in_features = train_features.shape[1] # 输入特征的数量

# 定义一个简单的线性模型作为基线(baseline)模型 
def get_net():
    net = nn.Sequential(nn.Linear(in_features, 1))
    return net 

对于房价的预测的误差,我们更关心相对误差而不是绝对误差。因为不同房价的价格尺度可能很大。比如对于12w的价格我们预测偏差为10w,那么模型的表现就很糟糕;如果对于500w的价格偏差为10w,那么这样的偏差是可以接受的。

因此这里,将使用对数尺度来衡量误差(可以想象以下对数函数的图像)。损失函数为:

\[\sqrt{\frac{1}{n}\sum_{i=1}^n(\log(y_i)-\log(\hat y_i))^2} \]

def log_rmse(net, features, labels):
    # 为了在取对数时进一步稳定该值,将小于1的值设置为1
    clipped_preds = torch.clamp(net(features), 1, float('inf'))
    rmse = torch.sqrt(loss(torch.log(clipped_preds), torch.log(labels)))
    return rmse.item()

训练和预测

通过迭代构建训练函数

def train(net, train_features, train_labels, test_features, test_labels,
           num_epochs, learning_rate, weight_decay):
    train_ls, test_ls = [], []

    optimizer = torch.optim.Adam(net.parameters(), 
                                 lr=learning_rate, weight_decay=weight_decay)
    for epoch in range(num_epochs):
        for X, y in loader:
            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))
    return train_ls, test_ls

由于数据量也比较小,还需要剥离一部分数据作为开发集进行验证,可以采用K交叉验证。具体实现如下:

# 将数据分为k-1份和 1份两部分
def get_k_fold_data(k, i, X, y):
    assert k > 1
    fold_size = X.shape[0] // k
    X_train, y_train = None, None
    for j in range(k):
        idx = slice(j * fold_size, (j + 1) * fold_size)
        X_part, y_part = X[idx, :], 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)
            y_train = torch.cat([y_train, y_part], 0)
    return X_train, y_train, X_valid, y_valid

def k_fold(k, X_train, y_train, num_epochs, learning_rate, weight_decay):
    train_l_sum, valid_l_sum = 0, 0
    for i in range(k):
        data = get_k_fold_data(k, i, X_train, y_train)
        net = get_net()
        train_ls, valid_ls = train(net, *data, num_epochs, learning_rate, weight_decay)
        train_l_sum += train_ls[-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'折{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, num_epochs, lr, weight_decay = 10, 100, 5, 0
train_l, valid_l = k_fold(k, train_features, train_labels, num_epochs, lr,
                          weight_decay)
print(f'{k}-折验证: 平均训练log rmse: {float(train_l):f}, '
      f'平均验证log rmse: {float(valid_l):f}')

image

训练模型后,输出预测信息并保存到csv文件中,用以提交。

def train_and_pred(train_freatures, test_features, train_labels, test_data, 
                   num_epoches, lr, weight_decay):
        net = get_net()
        train_ls, _ = train(net,train_freatures, train_labels,None, None, num_epochs, lr, weight_decay)
        d2l.plot(np.arange(1, num_epoches+1), [train_ls], xlabel='epoch', ylabel='log rmse', xlim=[1, num_epochs], legend=['train', 'valid'], yscale='log')
        print(f'训练log rmse:{float(train_ls[-1]):f}')
        preds = net(test_features).detach().numpy()
        x = pd.Series(preds.reshape(1, -1)[0])
        print(x.values)
        # 通过values获得预测值这一列,并保存到test_data中
        test_data['SalePrice'] = pd.Series(preds.reshape(1, -1)[0]).values
        #submission = pd.concat([test_data['Id'], test_data['SalePrice']], axis=1)
        submission = test_data['SalePrice']
        submission.to_csv('submission.csv', index=True)
       
train_and_pred(train_features, test_features, train_labels, test_data, num_epochs, lr, weight_decay)

运用DenseNet的思想

运用DenseNet的思想设计多层感知机模型,看看效果如何。

DenseNet主要由两部分组成dense block和transition layer,前者用于定义如何连接输入和输出,后者用于控制通道数。

由于在本例中,每个特征样本都是一个列向量,所以也就用不到transition layer。按照dense的理念,我们需要构建一个简单的dense block,能将线性层的输入和输出连接起来作为下一层的输入即可。

下面的代码定义了由四个dense block构成的模型。

def linear_block(input_features, out_features):
    return nn.Sequential(
        nn.BatchNorm1d(input_features), nn.ReLU(),
        nn.Linear(input_features, out_features)
    )
# 定义稠密块
class DenseBlock(nn.Module):
    def __init__(self, num_linear, input_features, num_features):
        super(DenseBlock, self).__init__()
        layer = []
        for i in range(num_linear):
            layer.append(linear_block(i * num_features + input_features, num_features))
        self.net = nn.Sequential(*layer)
    def forward(self, X):
        for blk in self.net:
            y = blk(X)
            X = torch.cat((X, y), 1)
        return X
def get_dense_linear_net():
    linear_prev = nn.Sequential(
        nn.Linear(in_features, 64),
        nn.BatchNorm1d(64), nn.ReLU(),
    )
    num_features, growth_rate = 64, 32
    num_linear_in_dense_blocks = [4, 4, 4, 4]
    blks = []
    for i, num_linear in enumerate(num_linear_in_dense_blocks):
        blks.append(DenseBlock(num_linear, num_features, growth_rate))
        num_features += num_linear * growth_rate
    net = nn.Sequential(
        linear_prev, *blks,
        nn.Dropout(0.2),
        nn.Linear(num_features, 1))
    return net
    

在k折交叉验证函数这块,调用上述构建的含有dense block的网络即可。其余部分都一样。

def k_fold_dense(...)
	...
	net = get_dense_linear_net()
    ...

训练并输出预测

k, num_epochs, lr, weight_decay = 10, 100, 0.01, 0
train_l, valid_l = k_fold_dense(k, train_features, train_labels, num_epochs, lr,
                          weight_decay)
print(f'{k}-折验证: 平均训练log rmse: {float(train_l):f}, '
      f'平均验证log rmse: {float(valid_l):f}')

image

总结

  1. 通过本次练习,熟悉了对于一个案例从数据处理到构建模型进行预测的流程。学到一些案例处理的思路,比如对于损失函数的考虑。还有就是数据处理很关键。
  2. 学习到一些对数据操作的函数以及k折交叉验证的实现,
    • DataFrame.apply() 对数据对象进行批量处理,很多pandas的数据对象都可以使用apply()来调用函数。,如 Dataframe、Series、分组对象、各种时间序列等。
    • pandas.get_dummies() 是 pandas 库中的一个函数,用于对整个数据集中的离散变量执行独热编码(One-Hot Encoding)。
    • torch.clamp(input,min,max)将输入的张量中的数据压缩在[min,max]之间。 float('inf')代表无穷大
    • Series([data, index, dtype, name, copy, …]) 将一维的列向量数据换成Series的结构,通过values提取其值,可以通过DataFrame中某一行或者某一列创建序列。
  3. 含有dense block的模型,是一个比较复杂的模型,这就导致了其过拟合了训练集,而在测试集上表现就比较差了(最后提交kaggle结果0.14607,只比简单模型好了一点点点点点点)。也尝试加上dropout、权重衰减等方式,结果并没由得到比较好的优化,可能需要进一步调参。