Transformer Learning —- Training and Regularization

实验环境:

  • 操作系统: Linux Ubuntu 24.04
  • nogpu

这一章主要讲模型训练时会遇到的问题, 以及常见的处理方法.

核心问题是: 模型不能只在训练集上表现好, 模型还要在没见过的新数据上表现好.

所以这一章会包含两类内容:

  1. 训练机制: 前向传播, 反向传播, 计算图, 参数初始化

  2. 正则化方法: 权重衰减, 暂退法, 防止过拟合

最后用 Kaggle 房价预测作为一个综合实战例子.

1 模型的正则化方法

1.1 模型选择, 欠拟合和过拟合

训练模型时, 我们通常会看两个指标:

  1. 训练误差: 模型在训练集上的误差

  2. 验证误差: 模型在没参与训练的数据上的误差

如果模型连训练集都学不好, 这叫欠拟合. 欠拟合通常说明模型太简单, 表达能力不够.

如果模型在训练集上很好, 但在验证集上很差, 这叫过拟合. 过拟合通常说明模型太复杂, 或者数据太少, 模型把训练集里的噪声也记住了.

可以这样理解:

  • 欠拟合: 题都没学会
  • 过拟合: 只背了练习题答案
  • 泛化好: 真正理解了规律

模型选择就是在不同模型和不同超参数之间做选择, 目标是找到验证集表现最好的方案.

1.2 权重衰减

权重衰减(weight decay), 也叫 L2 正则化.

它的核心思想是: 不要让模型的权重太大

普通训练只优化预测损失(loss). 加入权重衰减后, 目标变成:

1
loss + lambda * 权重平方和

代码形式是:

1
l = loss(net(X), y) + lambd * l2_penalty(w)

其中 lambda 控制惩罚强度:

  • lambda = 0, 不使用权重衰减

  • lambda 越大, 对大权重的惩罚越强

为什么这样能减少过拟合?

如果模型使用很大的权重, 它可能是在强行记住训练集中的细节和噪声. 权重衰减会把权重往 0 拉, 让模型更平滑, 更稳定, 不那么容易记住训练集.

一个典型的例子: https://zh.d2l.ai/chapter_multilayer-perceptrons/weight-decay.html#id2, 样本数为 20 的训练集, 但是特征点却有 200, 这样模型很容易就找到一些偶然规律进而展现出错误的判断.

PyTorch 简洁写法是:

1
2
3
4
5
optimizer = torch.optim.Adam(
net.parameters(),
lr=learning_rate,
weight_decay=weight_decay,
)

1.3 暂退法

暂退法(Dropout), 是神经网络中常见的正则化方法.

它的核心思想是: 训练时随机关闭一部分神经元

比如隐藏层输出原来是:

1
[h1, h2, h3, h4, h5]

使用 Dropout 后可能变成:

1
[h1, 0, h3, 0, h5]

这样可以逼模型不要过度依赖某几个神经元.

PyTorch 写法是:

1
2
3
4
5
6
net = nn.Sequential(
nn.Linear(784, 256),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(256, 10),
)

Notice:

训练时, Dropout 生效

测试时, Dropout 关闭

这是显而易见的.

在 PyTorch 中:

1
2
net.train()  # 训练模式, Dropout 生效
net.eval() # 评估模式, Dropout 关闭

2 模型调优

2.1 前向传播

前向传播就是模型从输入算到输出的过程.

比如一个 MLP:

1
2
3
4
5
6
输入 X
-> 隐藏层
-> 激活函数
-> 输出层
-> 损失函数
-> loss

代码形式是:

1
2
y_hat = net(X)
loss = loss_fn(y_hat, y)

前向传播回答的问题是: 模型现在预测得怎么样?

它会计算预测结果和损失, 同时保存中间变量, 给反向传播使用.

2.2 反向传播

反向传播就是从 loss 开始, 反过来计算每个参数的梯度.

它的方向是:

1
loss -> 输出层 -> 隐藏层 -> 输入附近的层

代码形式是:

1
loss.backward()

执行完以后, 每个可训练参数都会得到 .grad:

1
2
3
4
W1.grad
b1.grad
W2.grad
b2.grad

反向传播回答的问题是: 为了让 loss 变小, 每个参数应该怎么改?

优化器根据梯度更新参数:

1
optimizer.step()

完整训练流程是:

1
2
3
4
5
6
y_hat = net(X)              # 前向传播
loss = loss_fn(y_hat, y) # 计算损失

optimizer.zero_grad() # 清空旧梯度
loss.backward() # 反向传播
optimizer.step() # 更新参数

2.3 计算图

计算图是 PyTorch 自动记录的一张计算关系图.

比如:

1
X -> Linear -> ReLU -> Linear -> loss

PyTorch 会记录每一步操作, 所以当我们调用:

1
loss.backward()

它就能沿着计算图反方向计算梯度.

训练时比预测更占内存, 因为训练不仅要做前向传播, 还要保存中间变量给反向传播使用.

Notice:

预测: 只需要前向传播

训练: 前向传播 + 保存中间变量 + 反向传播

2.4 参数初始化

神经网络的权重不能随便初始化.

如果权重太小, 梯度可能越来越小, 这叫梯度消失.

1
梯度太小 -> 参数几乎不更新 -> 模型学不动

如果权重太大, 梯度可能越来越大, 这叫梯度爆炸.

1
梯度太大 -> 参数更新过猛 -> loss 乱跳甚至 NaN

还有一个问题叫打破对称性.

如果同一层的神经元初始化成完全一样, 它们会得到一样的输出, 一样的梯度, 最后永远学一样的东西.

所以权重通常要随机初始化.

常见初始化方法是 Xavier 初始化:

1
nn.init.xavier_uniform_(layer.weight)

它的目标是: 让每一层的输出不要越来越大, 也不要越来越小, 让训练更稳定.

常见搭配是:

1
2
sigmoid/tanh -> Xavier 初始化
ReLU -> Kaiming/He 初始化

3 预测房价实例

这一节用 Kaggle 房价预测做一个综合实战.

这个任务是:

  • input: 房子的各种属性
  • output: 预测房价 SalePrice

本样例运行成功后会生成:

1
submission.csv

3.1 代码拆解

3.1.1 下载数据

代码里先定义数据地址和校验值:

1
2
3
4
5
DATA_URL = "http://d2l-data.s3-accelerate.amazonaws.com/"
DATA_HUB = {
"kaggle_house_train": (...),
"kaggle_house_test": (...),
}

download 函数负责下载 CSV, 并且会检查本地缓存.

1
2
train_data = pd.read_csv(download("kaggle_house_train"))
test_data = pd.read_csv(download("kaggle_house_test"))

训练集有房价标签 SalePrice, 测试集没有.

3.1.2 合并训练集和测试集特征

1
all_features = pd.concat((train_data.iloc[:, 1:-1], test_data.iloc[:, 1:]))

这行做了三件事:

1
2
3
1. 去掉训练集的 Id
2. 去掉训练集最后一列 SalePrice
3. 去掉测试集的 Id

然后把训练集特征和测试集特征拼在一起, 统一做预处理.

为什么要一起处理?

因为类别特征要做独热编码(One-Hot), 如果训练集和测试集分开处理, 可能会产生不一致的列.

One-Hot description from https://cloud.tencent.com/developer/article/1688022

3.1.3 数值特征标准化

新版 pandas 里, 字符串列可能不是 object 类型, 所以这里不用 dtype != "object", 而是直接选择数值列:

1
numeric_features = all_features.select_dtypes(include=["number"]).columns

然后标准化:

1
2
3
all_features[numeric_features] = all_features[numeric_features].apply(
lambda x: (x - x.mean()) / x.std()
)

标准化后: 均值约等于 0, 标准差约等于 1

缺失值填 0:

1
all_features[numeric_features] = all_features[numeric_features].fillna(0)

因为标准化之后, 0 表示这个特征的平均水平.

3.1.4 类别特征独热编码

房价数据里有很多字符串特征, 比如:

1
2
3
4
MSZoning
RoofStyle
SaleType
SaleCondition

模型不能直接处理字符串, 所以要转成 0/1 特征:

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

dummy_na=True 的意思是, 缺失值也单独作为一种类别.

比如,

1
2
3
4
Id    MSZoning
1 RL
2 NaN
3 RM

独热编码后,

1
2
3
4
Id    MSZoning_RL    MSZoning_RM    MSZoning_nan
1 1 0 0
2 0 0 1
3 0 1 0

所以原来 1 列 MSZoning

会被拆成多列,

1
2
3
4
5
MSZoning_RL
MSZoning_RM
MSZoning_C
MSZoning_FV
MSZoning_nan

每一列都是 0 或 1, 这样模型就能处理了, 最后转成 float32:

1
all_features = all_features.astype("float32")

这一步很重要, 因为 PyTorch 训练时需要数值张量.

3.1.5 转成 PyTorch 张量

1
2
3
4
5
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
)

运行后实际形状是:

1
2
train_features: (1460, 330)
test_features: (1459, 330)

也就是:

1
2
3
训练集 1460 个样本
测试集 1459 个样本
每个样本 330 个处理后的特征 # 本来是 79 个特征值, 在进行独热编码之后变成了 330 个

3.1.6 DataLoader

这里自己实现了一个简单的 make_data_iter.

1
2
3
def make_data_iter(features, labels, batch_size, shuffle=True):
dataset = TensorDataset(features, labels)
return DataLoader(dataset, batch_size=batch_size, shuffle=shuffle)

作用是把特征和标签打包成小批量数据.

训练时每次取一小批样本, 而不是一次性把全部数据喂给模型.

3.1.7 模型

模型是一个线性回归 baseline:

1
2
def get_net(in_features):
return nn.Sequential(nn.Linear(in_features, 1))

结构是:

1
330 个输入特征 -> 1 个房价输出

3.1.8 评价指标 log RMSE

房价预测不用普通均方根误差(Root Mean Squared Error, RMSE), 而是使用对数均方根误差(log RMSE).

Notice: 这里因为使用了平方, RMSE 处理之后会对极大的误差非常敏感, 取对数之后就能缩短对极端数值的影响, 预测差 10 万, 对 12 万的房子很严重, 对 400 万的房子影响没那么大.

代码是:

1
2
3
4
5
6
def log_rmse(net, features, labels):
net.eval()
with torch.no_grad():
clipped_preds = torch.clamp(net(features), 1, float("inf"))
rmse = torch.sqrt(loss(torch.log(clipped_preds), torch.log(labels)))
return rmse.item()

torch.clamp 是为了防止预测值小于 1, 因为房价要取 log, 小于等于 0 会出问题.

3.1.9 训练函数

训练流程是标准 PyTorch 写法:

1
2
3
4
optimizer.zero_grad() # 清空旧梯度
l = loss(net(X), y) # 前向传播, 计算损失
l.backward() # 反向传播
optimizer.step() # 更新参数

优化器使用 Adam:

1
2
3
optimizer = torch.optim.Adam(
net.parameters(), lr=learning_rate, weight_decay=weight_decay
)

这里的 weight_decay 就是前面讲过的权重衰减.

3.1.10 K 折交叉验证

from https://zh.d2l.ai/chapter_multilayer-perceptrons/underfit-overfit.html#k

当训练数据稀缺时,我们甚至可能无法提供足够的数据来构成一个合适的验证集。 这个问题的一个流行的解决方案是采用折交叉验证。 这里,原始训练数据被分成个不重叠的子集。 然后执行次模型训练和验证,每次在个子集上进行训练, 并在剩余的一个子集(在该轮中没有用于训练的子集)上进行验证。 最后,通过对次实验的结果取平均来估计训练和验证误差。

K 折交叉验证用来更稳定地评估模型.

如果 k = 5, 就把训练集分成 5 份:

1
2
3
4
第 1 次: 第 1 份验证, 其余 4 份训练
第 2 次: 第 2 份验证, 其余 4 份训练
...
第 5 次: 第 5 份验证, 其余 4 份训练

最后取 5 次验证误差的平均值.

代码核心是:

1
2
3
data = get_k_fold_data(k, i, X_train, y_train)
net = get_net(in_features)
train_ls, valid_ls = train(net, *data, ...)

每一折都会重新创建一个模型, 避免上一折训练结果影响下一折.

3.1.11 生成提交文件

交叉验证只是用来选模型和超参数.

选好之后, 再用全部训练集训练一个最终模型:

1
train_and_pred(...)

然后对测试集预测:

1
preds = net(test_features).detach().numpy()

最后生成 Kaggle 需要的 CSV:

1
2
3
4
submission = pd.DataFrame(
{"Id": test_data["Id"], "SalePrice": preds.reshape(-1)}
)
submission.to_csv(output_file, index=False)

输出文件是:

1
submission.csv

3.2 完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
import hashlib
import os

import pandas as pd
import requests
import torch
from torch import nn
from torch.utils.data import DataLoader, TensorDataset


# Kaggle 房价预测的本地 PyTorch API 版本.

# D2L 托管的房价预测数据地址.
DATA_URL = "http://d2l-data.s3-accelerate.amazonaws.com/"

# DATA_HUB 保存数据集名称, 下载地址, SHA-1 校验值.
# SHA-1 用来判断本地缓存文件是否完整, 避免重复下载或使用损坏文件.
DATA_HUB = {
"kaggle_house_train": (
DATA_URL + "kaggle_house_pred_train.csv",
"585e9cc93e70b39160e7921475f9bcd7d31219ce",
),
"kaggle_house_test": (
DATA_URL + "kaggle_house_pred_test.csv",
"fa19780a7b011d9b009e8bff8e99922a8ee2eb90",
),
}


def download(name, cache_dir="data"):
"""下载数据文件, 如果本地缓存存在且校验通过, 就直接复用缓存."""
if name not in DATA_HUB:
raise KeyError(f"{name} is not registered in DATA_HUB")

url, sha1_hash = DATA_HUB[name]
os.makedirs(cache_dir, exist_ok=True)
fname = os.path.join(cache_dir, url.split("/")[-1])

# 如果文件已经存在, 先计算它的 SHA-1.
# 校验一致说明文件完整, 可以直接返回, 不需要重新下载.
if os.path.exists(fname):
sha1 = hashlib.sha1()
with open(fname, "rb") as f:
for data in iter(lambda: f.read(1048576), b""):
sha1.update(data)
if sha1.hexdigest() == sha1_hash:
return fname

# 本地没有缓存, 或缓存校验失败, 就从网络下载.
print(f"Downloading {url} -> {fname}")
response = requests.get(url, timeout=30)
response.raise_for_status()
with open(fname, "wb") as f:
f.write(response.content)
return fname


def load_house_data():
"""读取房价数据, 完成预处理, 并转换成 PyTorch 张量."""
# train_data 有 SalePrice 标签, test_data 没有 SalePrice 标签.
train_data = pd.read_csv(download("kaggle_house_train"))
test_data = pd.read_csv(download("kaggle_house_test"))

# 去掉 Id, 因为 Id 只是编号, 对预测房价没有实际意义.
# train_data.iloc[:, 1:-1]: 去掉第一列 Id 和最后一列 SalePrice.
# test_data.iloc[:, 1:]: 去掉第一列 Id.
# 把训练集和测试集特征拼在一起, 是为了统一做标准化和独热编码.
all_features = pd.concat((train_data.iloc[:, 1:-1], test_data.iloc[:, 1:]))

# 选择所有数值列.
numeric_features = all_features.select_dtypes(include=["number"]).columns

# 数值特征标准化: 变成均值约为 0, 标准差约为 1.
# 这样不同量纲的特征会处在相近尺度, 训练更稳定.
all_features[numeric_features] = all_features[numeric_features].apply(
lambda x: (x - x.mean()) / x.std()
)

# 标准化后, 数值特征的均值是 0, 所以缺失值可以填 0.
# 这相当于把缺失值放在该特征的平均水平上.
all_features[numeric_features] = all_features[numeric_features].fillna(0)

# 类别特征不能直接喂给线性层, 需要独热编码成 0/1 特征.
# dummy_na=True 表示缺失类别也会单独生成一列, 例如 Feature_nan.
all_features = pd.get_dummies(all_features, dummy_na=True)

# get_dummies 后可能混有 bool/int/float, 统一转成 float32, 方便转 torch tensor.
all_features = all_features.astype("float32")

# 前 n_train 行对应训练集, 后面对应测试集.
n_train = train_data.shape[0]
train_features = torch.tensor(all_features[:n_train].values, dtype=torch.float32)
test_features = torch.tensor(all_features[n_train:].values, dtype=torch.float32)

# SalePrice 是训练标签, reshape(-1, 1) 让标签形状变成 [样本数, 1].
train_labels = torch.tensor(
train_data["SalePrice"].values.reshape(-1, 1), dtype=torch.float32
)
return train_features, test_features, train_labels, test_data


def make_data_iter(features, labels, batch_size, shuffle=True):
"""把特征和标签打包成 DataLoader, 用于小批量训练."""
dataset = TensorDataset(features, labels)
return DataLoader(dataset, batch_size=batch_size, shuffle=shuffle)


def get_net(in_features):
"""创建线性回归模型: 多个房屋特征 -> 1 个房价预测值."""
return nn.Sequential(nn.Linear(in_features, 1))


# 均方误差损失, 回归任务常用.
loss = nn.MSELoss()


def log_rmse(net, features, labels):
"""计算 Kaggle 房价预测常用指标: log RMSE."""
# 评估阶段要关闭 Dropout/BatchNorm 等训练行为.
# 虽然这里的线性模型没有这些层, 但保留这个习惯是正确的.
net.eval()
with torch.no_grad():
# 房价要取 log, 所以预测值不能小于等于 0.
# clamp 把小于 1 的预测值截断为 1, 避免 log 出现非法值.
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,
valid_features,
valid_labels,
num_epochs,
learning_rate,
weight_decay,
batch_size,
):
"""训练一个模型, 并返回每轮的训练 log RMSE 和验证 log RMSE."""
train_ls, valid_ls = [], []
train_iter = make_data_iter(train_features, train_labels, batch_size)

# Adam 是常用优化器, 对学习率相对不那么敏感.
# weight_decay 就是权重衰减, 用于抑制过拟合.
optimizer = torch.optim.Adam(
net.parameters(), lr=learning_rate, weight_decay=weight_decay
)

for _ in range(num_epochs):
# 训练模式, 开启训练相关行为.
net.train()
for X, y in train_iter:
# 清空上一批数据留下的梯度.
optimizer.zero_grad()

# 前向传播: 根据当前参数预测房价, 并计算 MSE loss.
l = loss(net(X), y)

# 反向传播: 计算每个参数对 loss 的梯度.
l.backward()

# 参数更新: Adam 根据梯度更新模型参数.
optimizer.step()

# 每个 epoch 结束后, 记录训练集 log RMSE.
train_ls.append(log_rmse(net, train_features, train_labels))

# 如果传入了验证集, 就记录验证集 log RMSE.
# 最终提交测试集时没有 valid_labels, 所以这里需要判断 None.
if valid_labels is not None:
valid_ls.append(log_rmse(net, valid_features, valid_labels))

return train_ls, valid_ls


def get_k_fold_data(k, i, X, y):
"""返回第 i 折交叉验证需要的训练集和验证集."""
if k <= 1:
raise ValueError("k must be greater than 1")

fold_size = X.shape[0] // k
X_train, y_train = None, None
X_valid, y_valid = 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:
# 第 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], dim=0)
y_train = torch.cat([y_train, y_part], dim=0)

return X_train, y_train, X_valid, y_valid


def k_fold(
k,
X_train,
y_train,
num_epochs,
learning_rate,
weight_decay,
batch_size,
):
"""执行 K 折交叉验证, 返回平均训练误差和平均验证误差."""
train_l_sum, valid_l_sum = 0.0, 0.0
in_features = X_train.shape[1]

for i in range(k):
# 每一折都重新切分数据, 并重新创建一个新模型.
# 这样每一折之间不会互相污染训练结果.
data = get_k_fold_data(k, i, X_train, y_train)
net = get_net(in_features)
train_ls, valid_ls = train(
net, *data, num_epochs, learning_rate, weight_decay, batch_size
)

# 只累加最后一个 epoch 的误差, 用来计算平均表现.
train_l_sum += train_ls[-1]
valid_l_sum += valid_ls[-1]
print(
f"fold {i + 1}, "
f"train log rmse {train_ls[-1]:.6f}, "
f"valid log rmse {valid_ls[-1]:.6f}"
)

return train_l_sum / k, valid_l_sum / k


def train_and_pred(
train_features,
test_features,
train_labels,
test_data,
num_epochs,
learning_rate,
weight_decay,
batch_size,
output_file="submission.csv",
):
"""使用全部训练数据训练最终模型, 并生成 Kaggle 提交文件."""
net = get_net(train_features.shape[1])

# 这里没有验证集, 因为超参数已经通过 K 折交叉验证选好了.
# 最终模型应该使用全部训练数据来训练.
train_ls, _ = train(
net,
train_features,
train_labels,
None,
None,
num_epochs,
learning_rate,
weight_decay,
batch_size,
)
print(f"final train log rmse {train_ls[-1]:.6f}")

# 对 Kaggle 测试集预测房价.
net.eval()
with torch.no_grad():
preds = net(test_features).detach().numpy()

# Kaggle 提交文件只需要两列: Id 和 SalePrice.
submission = pd.DataFrame(
{"Id": test_data["Id"], "SalePrice": preds.reshape(-1)}
)
submission.to_csv(output_file, index=False)
print(f"saved {output_file}")


def main():
# 固定随机种子, 让每次运行的结果尽量可复现.
torch.manual_seed(0)

# 读取并预处理数据.
train_features, test_features, train_labels, test_data = load_house_data()
print(f"train_features: {tuple(train_features.shape)}")
print(f"test_features: {tuple(test_features.shape)}")

# 超参数设置.
# 这些值来自 D2L 示例, 可以继续通过验证集或 K 折交叉验证调优.
k = 5
num_epochs = 100
learning_rate = 5
weight_decay = 0
batch_size = 64

# 先用 K 折交叉验证评估当前超参数组合.
train_l, valid_l = k_fold(
k,
train_features,
train_labels,
num_epochs,
learning_rate,
weight_decay,
batch_size,
)
print(
f"{k}-fold validation, "
f"avg train log rmse {train_l:.6f}, "
f"avg valid log rmse {valid_l:.6f}"
)

# 交叉验证完成后, 用全部训练数据训练最终模型并生成 submission.csv.
train_and_pred(
train_features,
test_features,
train_labels,
test_data,
num_epochs,
learning_rate,
weight_decay,
batch_size,
)


if __name__ == "__main__":
main()

输出结果是:

1
2
3
4
5
6
7
8
9
10
train_features: (1460, 330)
test_features: (1459, 330)
fold 1, train log rmse 0.170119, valid log rmse 0.156695
fold 2, train log rmse 0.162237, valid log rmse 0.190499
fold 3, train log rmse 0.163671, valid log rmse 0.168297
fold 4, train log rmse 0.167846, valid log rmse 0.154611
fold 5, train log rmse 0.163780, valid log rmse 0.183211
5-fold validation, avg train log rmse 0.165530, avg valid log rmse 0.170663
final train log rmse 0.162681
saved submission.csv

同时生成了:

1
submission.csv