Kaggle竞赛-OttoGroup产品分类

刘二大人 第09讲 课后练习

题目地址:Otto Group Product Classification Challenge | Kaggle

Pandas基本操作参考本网站另一篇博文:Pandas快速入门指南:CSV操作与数据清洗核心 - GuTaicheng’s Blog

没毛病兄弟们,注释写的很详细,方便自己后续CV。

主要学到的几点:

  • 处理训练数据集,不能简单地直接全作为输出,需要计算均值和标准差,即规范化修正
  • 处理测试数据集,不能用测试集的数据计算均值和标准差,而是要用训练集
  • CrossEntropyLoss对最后输出的格式要求,向量,且格式为int64
  • 使用 model.train() 规范:设置训练模式
  • 测试集最后收集结果时的处理方式,已经处理过程中数据维度的变化,注释中解释的很清楚了
  • 一些优化方式,代码中含有【优化】的即Gemini给出的简单优化方案

代码:

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
import numpy as np
import torch
from torch import nn, optim
from torch.utils.data import Dataset, DataLoader
import pandas as pd
# 【优化新增】: 导入学习率调度器
from torch.optim.lr_scheduler import StepLR

# 全局变量:用于存储训练集计算得到的均值和标准差,避免数据泄露
GLOBAL_MEAN = None
GLOBAL_STD = None
CLASS_LABELS = ['Class_1', 'Class_2', 'Class_3',
'Class_4', 'Class_5', 'Class_6',
'Class_7', 'Class_8', 'Class_9']

class TrainDataset(Dataset):
def __init__(self, filepath):
global GLOBAL_MEAN, GLOBAL_STD

# 1. 加载数据
df = pd.read_csv(filepath)

# 2. 准备输入特征矩阵
# 取所有行,第1列到倒数第2列
x_numpy = df.iloc[:, 1:-1].values.astype(np.float32)
# 计算均值和标准差,并存储为全局变量
GLOBAL_MEAN = x_numpy.mean(axis=0)
GLOBAL_STD = x_numpy.std(axis=0)
# 避免除以零(如果某些特征的标准差为 0)
GLOBAL_STD[GLOBAL_STD == 0] = 1
# 标准化
x_numpy = (x_numpy - GLOBAL_MEAN) / GLOBAL_STD
self.x_data = torch.from_numpy(x_numpy)

# 3. 准备输出目标矩阵
# 设置标签编码
df['ClassId'] = pd.Categorical(df['target'], categories=CLASS_LABELS, ordered=True)
# nn.CrossEntropyLoss 强制要求类别索引是 torch.long 64 位整数
df['ClassId'] = df['ClassId'].cat.codes.astype(np.int64)
# 这里不需要特地加 .unsqueeze(dim=1) 维持成矩阵,因为CrossEntropyLoss要求是向量
y_numpy = df['ClassId'].values
self.y_data = torch.from_numpy(y_numpy)

# 设置长度
self.len = self.x_data.shape[0]

def __getitem__(self, index):
return self.x_data[index], self.y_data[index]

def __len__(self):
return self.len

class TestDataset(Dataset):
def __init__(self, filepath):
# 必须使用训练集计算的 GLOBAL_MEAN 和 GLOBAL_STD
global GLOBAL_MEAN, GLOBAL_STD

# 1. 加载数据
df = pd.read_csv(filepath)

# 2. 准备输入特征矩阵
# 取所有行,第1列到最后一列
x_numpy = df.iloc[:, 1:].values.astype(np.float32)

# 规范化修正:应用训练集计算的均值和标准差进行标准化
# 切记不能用测试集进行计算均值和标准化
x_numpy = (x_numpy - GLOBAL_MEAN) / GLOBAL_STD
self.x_data = torch.from_numpy(x_numpy)

# 3. 设置产品ID
self.PId = df['id'].values

# 4. 设置长度
self.len = self.x_data.shape[0]

def __getitem__(self, index):
return self.x_data[index], self.PId[index]

def __len__(self):
return self.len

class OttoGroupNet(nn.Module):
def __init__(self):
super(OttoGroupNet, self).__init__()
# 【架构优化】: 增加网络深度和宽度,提升容量
# 原: 93->64->32->9
# 新: 93->128->64->64->9
self.l1 = nn.Linear(93, 128)
self.l2 = nn.Linear(128, 64)
self.l3 = nn.Linear(64, 64) # 【架构优化】新增隐藏层
self.l4 = nn.Linear(64, 9) # 最终输出层
self.relu = nn.ReLU()

# 【正则化优化】: 在更多层后应用 Dropout
self.dropout1 = nn.Dropout(0.3)
self.dropout2 = nn.Dropout(0.3)

def forward(self, x):
# 第一层后 Dropout
x = self.dropout1(self.relu(self.l1(x)))
# 第二层后 Dropout
x = self.dropout2(self.relu(self.l2(x)))
# 第三层
x = self.relu(self.l3(x))
return self.l4(x) # 输出 Logits

def epoch_train(train_loader, model, criterion, optimizer):
model.train() # 规范:设置训练模式
total_loss = 0.0 # 新增:用于累积整个 Epoch 的总损失
total_batches = 0 # 新增:用于计数总批次数量
for batch_idx, data in enumerate(train_loader):
optimizer.zero_grad()
inputs, target = data
target_pred = model(inputs)
loss = criterion(target_pred, target)
loss.backward()
optimizer.step()
total_loss += loss.item() # 累加到总损失
total_batches += 1 # 计数批次

if total_batches > 0:
epoch_avg_loss = total_loss / total_batches
else:
epoch_avg_loss = 0.0
return epoch_avg_loss # 返回当前 Epoch 的平均损失

def test_inference(my_model, test_loader):
"""【关键修正】: 在测试集上进行推理,生成 Softmax 概率矩阵用于 Kaggle 提交。"""
my_model.eval() # 切换到评估模式
all_predictions = []
all_ids = []

print("\n--- 开始在测试集上进行推理 ---")

with torch.no_grad(): # 推理时禁用梯度
for x_data, PId in test_loader:
# 1. 前向传播:模型输出 Logits
# 并不是Logistic层输出的东西
# 而是尚未经过 Softmax 或 Sigmoid 激活函数处理的原始数值输出
outputs = my_model(x_data)

# 应用 Softmax,将 Logits 转换为概率
# dim=1 确保是对 9 个类别进行 Softmax
# 确保 全部正值,归一化
# probabilities 是 64*9的矩阵
probabilities = torch.softmax(outputs, dim=1)

# 2. 收集结果
# all_predictions 最后是一个列表,里面是总批次数量N个 64*9的矩阵,最后一批不一定
all_predictions.append(probabilities.cpu().numpy())
# all_ids 最后是一个列表,里面是总批次数量N个,64* 的一维向量,
# 因为在数据集定义中没有加.unsqueeze(dim=1)维持是矩阵,那么pytorch默认对单独一列转为向量
all_ids.append(PId.cpu().numpy())

# 3. 整合结果并构建提交 DataFrame
# 使用 np.concatenate 将所有 Batch 的结果堆叠成一个大矩阵
# np.vstack(), 将列表垂直叠加
# predictions_matrix 最后是 total_samples * 9 的大矩阵
# predictions_matrix = np.concatenate(all_ids, axis=0) 同样的效果
predictions_matrix = np.vstack(all_predictions)

submission_df = pd.DataFrame(predictions_matrix, columns=CLASS_LABELS)
# 使用 np.concatenate 确保 ID 也是一个连续的数组
# 使用 np.concatenate 将列表沿着制定维度拼接,
# axis = 0, 默认值, 垂直方向,对二维矩阵而言相当于np.vstack
# axis=1: 沿着水平轴(列方向)连接
# 但是当 列表内的元素只有一个维度时,也就是向量时,就是首尾相连,也相当于垂直,因为向量就是竖着写的
# 但是如果对 向量 使用 vstack 则会变成(1 * 64)的矩阵, 再垂直叠加
# 最后是 total_samples* 的一维向量
submission_df.insert(0, 'id', np.concatenate(all_ids))

print("--- 推理完成 ---")
print(f"生成的预测总数:{len(submission_df)}")

return submission_df

if __name__ == '__main__':
# 1. 初始化数据集和加载器 (TrainDataset 必须先运行,以设置 GLOBAL_MEAN/STD)
train_dataset = TrainDataset(filepath='DataSet/ottoGroup/train.csv')
train_loader = DataLoader(dataset=train_dataset, batch_size=64, shuffle=True, num_workers=0)

test_dataset = TestDataset(filepath='DataSet/ottoGroup/test.csv')
test_loader = DataLoader(dataset=test_dataset, batch_size=64, shuffle=False, num_workers=0)

NUM_EPOCHS = 100

my_model = OttoGroupNet()
criterion = nn.CrossEntropyLoss()
# 【正则化优化】: 添加 Weight Decay (L2 正则化),有助于降低 Log Loss
optimizer = optim.Adam(my_model.parameters(), lr=0.001, weight_decay=1e-5) # Adam优化器似乎比SGD更好
# optimizer = optim.SGD(my_model.parameters(), lr=0.01, momentum=0.5)
print("--- 初始化完成,开始训练 ---")

# 【超参数优化】: 初始化 StepLR 调度器 (每 3 个 Epoch 学习率衰减 90%)
scheduler = StepLR(optimizer, step_size=5, gamma=0.5)

for epoch_num in range(NUM_EPOCHS):
# 将参数显式传递给 train 函数
train_loss = epoch_train(train_loader, my_model, criterion, optimizer)
print(f"Epoch {epoch_num+1} 平均训练损失: {train_loss:.4f}")

# 【调度器优化】: 每训练完一个 Epoch 步进学习率
scheduler.step()

# 2. 调用测试方法进行推理
submission = test_inference(my_model, test_loader)

# 3. 可选:将结果保存为 CSV 文件
submission.to_csv('DataSet/ottoGroup/submission.csv', index=False)
print("\n预测结果已保存至 'DataSet/ottoGroup/submission.csv'")

优化前后


Kaggle竞赛-OttoGroup产品分类
https://blog.gutaicheng.top/2025/10/22/Kaggle竞赛-OttoGroup产品分类/
作者
GuTaicheng
发布于
2025年10月22日
许可协议