(Kaggle人工智能比赛复现)3万字长文教你如何一步步复现《泰坦尼克号生存预测》
泰坦尼克号沉船事件背景:1912年4月15日,英国豪华邮轮泰坦尼克号在首航途中撞上冰山沉没。这次灾难中,2224名乘客和船员中有1502人遇难,成为历史上最臭名昭著的海难之一 () (〖从零开始学Kaggle竞赛〗泰坦尼克之灾_kaggle比赛泰坦尼克- 从灾难中学习机器学习提交比赛需要提交什么文件-优快云博客事故发生后,人们痛定思痛,促成了航运安全法规的改进。然而,在这场灾难中,乘客的生还情况并
Kaggle泰坦尼克号生存预测完整实战教程
泰坦尼克号生存预测(Titanic - Machine Learning from Disaster)是Kaggle上著名的入门竞赛之一。本文将以超详细的方式,带领大家从零开始完成这个比赛,包括数据理解、探索性分析、特征工程、模型构建(传统机器学习模型和PyTorch深度学习模型)、模型调优以及最终的Kaggle提交流程。无论你是刚入门的数据科学新人,还是有一年左右工作经验的算法工程师,都可以通过本教程逐步复现泰坦尼克号生存预测的完整解决方案。
我们将使用Python的科学计算生态(如Pandas、NumPy、Matplotlib/Seaborn等)进行数据分析,并使用scikit-learn构建基线模型,最后通过PyTorch构建一个多层感知机(MLP)模型来进行预测。所有代码均提供完整版本,读者可以直接复制运行。文章采用Markdown格式组织,包含清晰的标题、代码块和表格等排版元素,同时穿插必要的公式(使用KaTeX书写)和引用资料,帮助理解关键概念和确保内容的可复现性。
1. 比赛背景与数据介绍
1.1 比赛任务简介
泰坦尼克号沉船事件背景:1912年4月15日,英国豪华邮轮泰坦尼克号在首航途中撞上冰山沉没。这次灾难中,2224名乘客和船员中有1502人遇难,成为历史上最臭名昭著的海难之一 (Kaggle-titanic by agconti) (〖从零开始学Kaggle竞赛〗泰坦尼克之灾_kaggle比赛泰坦尼克- 从灾难中学习机器学习提交比赛需要提交什么文件-优快云博客)。事故发生后,人们痛定思痛,促成了航运安全法规的改进。然而,在这场灾难中,乘客的生还情况并非完全随机的运气,一些群体的生存率明显高于其他群体——例如女性、儿童和上层阶级乘客往往更有可能幸存,这也印证了当时“女士和小孩优先”的避难原则 。
比赛的任务与目标:Kaggle泰坦尼克号生存预测比赛要求参赛者根据乘客的特征(如姓名、年龄、性别、船舱等级等)建立模型,预测每一位乘客在这场灾难中是否幸存。换句话说,我们需要完成一个二分类任务:预测乘客的生存状态(生存用1表示,遇难用0表示) (〖从零开始学Kaggle竞赛〗泰坦尼克之灾_kaggle比赛泰坦尼克- 从灾难中学习机器学习提交比赛需要提交什么文件-优快云博客)。这个任务不仅是机器学习的经典入门案例,也可以帮助我们体会在数据中发现影响生存的规律。例如,我们可以通过模型回答:“什么样的人更有可能幸存?” (〖从零开始学Kaggle竞赛〗泰坦尼克之灾_kaggle比赛泰坦尼克- 从灾难中学习机器学习提交比赛需要提交什么文件-优快云博客)
Kaggle入门竞赛:泰坦尼克号比赛是Kaggle的入门级知识型竞赛(Getting Started Competition),非常适合初学者练习完整的数据科学项目流程 (Kaggle-titanic by agconti)。通过此项目,我们可以熟悉以下内容:
- 数据处理流程:从数据读取、清洗、探索,到特征工程和数据可视化。
- 模型构建与评估:从简单的基线模型(如逻辑回归、决策树等)开始,逐步过渡到更复杂的模型(如随机森林、神经网络),并学会使用交叉验证、评价指标来评估模型性能。
- 模型优化:学习调整超参数、融合多个模型提高性能的方法。
- Kaggle提交流程:了解如何生成预测结果并提交到Kaggle,以及查看排行榜成绩。
- 工具使用:熟练使用Pandas进行数据分析、Matplotlib/Seaborn进行可视化,scikit-learn和PyTorch进行建模。
- 工程实践:掌握在Google Colab等环境运行代码的方法,了解版本控制和实验复现的最佳实践。
总之,通过本次实践,读者将全面体验一个数据科学项目的端到端流程,从而为今后参加Kaggle比赛或实际机器学习项目打下基础。
1.2 数据集来源及文件说明
Kaggle提供了泰坦尼克号乘客数据的两个主要数据集:训练集(train.csv)和测试集(test.csv),以及一个示例提交文件gender_submission.csv。 (〖从零开始学Kaggle竞赛〗泰坦尼克之灾_kaggle比赛泰坦尼克- 从灾难中学习机器学习提交比赛需要提交什么文件-优快云博客)
-
训练集(train.csv):包含891名乘客的详细信息,每名乘客占一行,其中包含他们是否幸存的标注(即“地面真相”)。也就是说,训练集提供了模型学习所需的特征和对应的目标标签 (〖从零开始学Kaggle竞赛〗泰坦尼克之灾_kaggle比赛泰坦尼克- 从灾难中学习机器学习提交比赛需要提交什么文件-优快云博客)。在后续分析中,我们会使用该数据集来训练模型,并根据其结果调整模型参数。
-
测试集(test.csv):包含另外418名乘客的详细信息。与训练集不同,测试集并不提供每名乘客是否幸存的结果标签 (〖从零开始学Kaggle竞赛〗泰坦尼克之灾_kaggle比赛泰坦尼克- 从灾难中学习机器学习提交比赛需要提交什么文件-优快云博客)。我们的任务是利用在训练集上学到的模式,预测这些乘客的生存情况。最终,我们需要对测试集中的每个乘客给出“Survived”值的预测(0或1)。
-
示例提交文件(gender_submission.csv):这是一个示例的结果提交文件,帮助参赛者了解提交格式。这个文件提供了一个基于性别的简单预测:假设所有女性乘客都幸存、所有男性乘客都遇难 (Kaggle 泰坦尼克号挑战赛目标 使用泰坦尼克号乘客数据(名字,年龄,票价等)预测谁将存活或者死去。 数据 在数据中有 - 掘金)。虽然这个假设并不精确,但鉴于我们在数据探索中将会看到的性别与生存率的关联,它作为起点并不算糟糕 (〖从零开始学Kaggle竞赛〗泰坦尼克之灾_kaggle比赛泰坦尼克- 从灾难中学习机器学习提交比赛需要提交什么文件-优快云博客)。参赛者应根据自己模型的预测来生成类似格式的提交。
数据字段解释:泰坦尼克号数据集包含乘客的各种属性,我们在建模前需要理解每个字段的含义。以下是数据集中主要特征的说明(每列代表一个字段):
PassengerId:乘客ID,乘客的唯一标识符。Survived:是否幸存(标签,0表示遇难,1表示存活)。注意:这个字段在测试集中不存在,因为这是我们需要预测的目标。Pclass:客舱等级(1 = 一等舱,2 = 二等舱,3 = 三等舱)。这是乘客社会经济地位的指标,一等舱乘客往往是上流社会人士,三等舱则为普通或低收入乘客。Name:乘客姓名(包括头衔)。例如Braund, Mr. Owen Harris。其中可以包含乘客的称谓(如Mr., Mrs., Miss等),这些头衔也蕴含了一定信息(性别、婚姻状况、社会地位等),在特征工程中将会被提取利用。Sex:乘客性别(male男性 或 female女性)。Age:乘客年龄(以岁为单位,部分乘客年龄缺失)。对于年龄小于1岁的婴儿以小数表示(如0.42表示约5个月大)。年龄与幸存与否有可能存在关联,例如儿童的幸存率可能不同于成人。SibSp:兄弟姐妹/配偶同行数。这个数值表示乘客在船上有多少兄弟姐妹或配偶同船。定义:兄弟姐妹包括亲兄弟、亲姐妹、继兄弟、继姐妹;配偶指丈夫或妻子(未婚伴侣不计在内) (Titanic_Dataset_Exploratory_Analysis)。Parch:父母/子女同行数。表示乘客在船上有多少父母或子女同船。定义:父母包括亲生父母、继父母;子女包括亲生子女、继子女。不包含其他亲属。Ticket:船票编号。每个乘客的船票号码,可能含字母和数字的组合。部分乘客可能共用相同的船票编号(例如家人一起购票),因此从Ticket中我们可能推断一些团体信息。Fare:票价。乘客为船票支付的费用(英镑)。票价与客舱等级、社会经济地位相关联,也是一个潜在的影响因素。Cabin:舱房号。标识乘客所在的客舱,通常以舱甲板的字母加编号表示(例如C85)。该字段缺失值较多(许多乘客没有舱房编号,可能是因为他们住在较低舱等或公共舱室),是否有Cabin记录本身可能也是一种信息(例如有具体Cabin号的通常是一二等舱乘客)。Embarked:登船港口。乘客从哪个港口登船。共有三个可能值:C = Cherbourg(瑟堡), Q = Queenstown(皇后镇), S = Southampton(南安普顿)。这是乘客出发地的信息,可能与其社会经济地位和目的地有关。
理解这些字段对于后续的分析和特征工程非常重要。例如,Pclass和Fare反映了乘客的社会经济地位,Sex和Age反映了生理特征,SibSp和Parch可以结合成“家庭大小”等新特征,Embarked可能与乘客背景相关。接下来我们会详细探索各特征与生存率的关系。
1.3 Kaggle评分机制
在Kaggle泰坦尼克号比赛中,评价模型优劣的指标是预测准确率(Accuracy) (〖从零开始学Kaggle竞赛〗泰坦尼克之灾_kaggle比赛泰坦尼克- 从灾难中学习机器学习提交比赛需要提交什么文件-优快云博客)。具体来说,提交的结果会与真实的生存情况进行比较,计算预测正确的百分比——这就是最终的得分。例如,如果在418名测试集乘客中模型正确预测了350人的生存状态,那么准确率即为 350 / 418 ≈ 0.8376 350/418 \approx 0.8376 350/418≈0.8376,通常Kaggle会以百分制或小数形式显示(例如83.76%或0.8376)。
需要注意的是,准确率只是衡量模型性能的一种方式。在这个比赛中,正负样本(幸存和遇难)分布大致为3:5左右,并不算极端失衡,所以准确率是合理的评估指标。然而,在更一般的机器学习任务中,若类别不平衡严重,光看准确率可能会有迷惑性,此时往往需要引入其他指标(如精确率、召回率、F1分数、AUC等)来辅助判断模型好坏。
Kaggle提交与排名:参赛者需要根据测试集预测生成一个提交文件(CSV格式),其中包含两列:PassengerId和Survived(预测值,0或1) (Kaggle 泰坦尼克号挑战赛目标 使用泰坦尼克号乘客数据(名字,年龄,票价等)预测谁将存活或者死去。 数据 在数据中有 - 掘金)。提交文件的格式必须严格符合要求,多一列或少一列都会导致提交失败 (〖从零开始学Kaggle竞赛〗泰坦尼克之灾_kaggle比赛泰坦尼克- 从灾难中学习机器学习提交比赛需要提交什么文件-优快云博客)。提交后,Kaggle会即时给出Public Leaderboard上的成绩(基于部分测试数据评估的准确率)。参赛者可以多次提交模型结果,不断改进。比赛期限结束后,会基于隐藏的Private Leaderboard排名决定最终名次(对于这个入门练习赛,名次不涉及奖金或晋级,但可以当作练习成果的验证)。我们在教程最后会详细讲解如何生成提交并查看分数。
接下来,我们将开始数据分析的过程,从数据读取、清洗到探索性分析,一步一步理解影响泰坦尼克号生存的因素。
2. 数据加载、探索与可视化
在这部分,我们将读取比赛数据,了解数据的基本情况,并通过探索性数据分析(EDA)和可视化手段,寻求不同特征与乘客生存率之间的关系。良好的EDA有助于我们发现数据中的模式,为后续的特征工程和建模提供指导。
2.1 数据加载和概览
首先,使用Pandas读取提供的CSV文件。确保将train.csv和test.csv放在工作目录下(如果在Kaggle Notebook或Colab环境中,可以根据实际路径调整)。我们还可以使用Pandas的函数查看数据的基本信息,例如行列数、字段类型、缺失值等。下面的代码实现数据加载并做初步检查:
import pandas as pd
# 读取训练集和测试集
train_df = pd.read_csv('train.csv')
test_df = pd.read_csv('test.csv')
# 查看训练集的维度(行数, 列数)
print("Train dataset shape:", train_df.shape)
print("Test dataset shape:", test_df.shape)
# 查看训练集前5行
train_df.head()
运行上述代码后,我们将得到训练集和测试集的基本尺寸以及训练集的前几条记录。根据数据描述可知,训练集包含891行、12列(包含标签列Survived),测试集包含418行、11列(不含Survived)。具体输出结果类似于:
Train dataset shape: (891, 12)
Test dataset shape: (418, 11)
训练集的前5行数据大致如下(这里为了篇幅,每行仅部分字段显示):
PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
0 1 0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.2500 NaN S
1 2 1 1 Cumings, Mrs. John Bradley... female 38.0 1 0 PC 17599 71.2833 C85 C
2 3 1 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.9250 NaN S
3 4 1 1 Futrelle, Mrs. Jacques Heath female 35.0 1 0 113803 53.1000 C123 S
4 5 0 3 Allen, Mr. William Henry male 35.0 0 0 373450 8.0500 NaN S
从中可以初步看到一些规律:如样本0是一位22岁的三等舱男性(未幸存,Survived=0),样本1是一等舱38岁女性(幸存,Survived=1),等等。这些信息与我们前面描述的字段含义相符合。
接下来,我们查看数据的基本统计信息和缺失值情况:
# 查看各字段的数据类型和非空值数
train_df.info()
print("-" * 40)
test_df.info()
输出将列出每个字段的数据类型(int, float, object等)以及非空值的数量。对于训练集,我们预期输出(部分字段):
PassengerId 891 non-null int64
Survived 891 non-null int64
Pclass 891 non-null int64
Name 891 non-null object
Sex 891 non-null object
Age 714 non-null float64
SibSp 891 non-null int64
Parch 891 non-null int64
Ticket 891 non-null object
Fare 891 non-null float64
Cabin 204 non-null object
Embarked 889 non-null object
可以看到,训练集总共有891条乘客记录,每个字段的非空值数量:大部分字段在891左右,但Age只有714个非空值(说明有177个年龄数据缺失),Cabin只有204个非空值(缺失高达687条,占绝大多数),Embarked有889个非空值(缺失2条)。这些缺失数据需要在后续的数据清理中加以处理。
测试集(418行)的字段类型类似,其中Age和Cabin也有缺失,另外Fare在测试集中可能也有缺失值(稍后确认)。测试集没有Survived列。
让我们进一步查看数值型特征的描述性统计,以及Survived标签的分布:
# 数值型字段的统计描述
train_df.describe()
该命令将输出训练集中数值字段(不包括object类型)的统计量,例如计数、均值、标准差、最小值、四分位数、中位数、最大值等。例如,部分输出为:
PassengerId Survived Pclass Age SibSp Parch Fare
count 891.000000 891.00000 891.000000 714.000000 891.000000 891.000000 891.000000
mean 446.000000 0.38384 2.308643 29.699118 0.523008 0.381594 32.204208
std 257.353842 0.48659 0.836071 14.526497 1.102743 0.806057 49.693429
min 1.000000 0.00000 1.000000 0.420000 0.000000 0.000000 0.000000
25% 223.500000 0.00000 2.000000 20.125000 0.000000 0.000000 7.910400
50% 446.000000 0.00000 3.000000 28.000000 0.000000 0.000000 14.454200
75% 668.500000 1.00000 3.000000 38.000000 1.000000 0.000000 31.000000
max 891.000000 1.00000 3.000000 80.000000 8.000000 6.000000 512.329200
从中我们可以获取一些有用信息:
- 生存率总体情况:
Survived一列的均值为0.38384,表示训练集中生存者的比例约为38.38%,遇难者约61.62%。换句话说,在891名训练集乘客中,约有342人生存,549人遇难(0.38384 * 891 ≈ 342),确认了数据的总体分布。这意味着如果盲目预测所有人遇难,准确率也有61.6%,但我们的目标是超过这个基线,尽可能准确地区分出幸存者。 - 年龄:
Age的平均数约29.70岁,中位数28岁,最年长乘客80岁,四分位范围20~38岁。这是一艘跨大西洋的客轮,乘客年龄跨度大,后续我们会关注不同年龄段的生存率。注意有177个年龄缺失需要填补。 - 船舱等级:
Pclass的均值为2.308,反映训练集里三等舱乘客居多(一等舱用1表示,三等舱用3表示,均值>2说明三等舱比例较高)。最大值3,最小值1,符合取值范围。我们稍后可以统计各舱等人数和生存率。 - 家庭相关:
SibSp最大值8(意味着有乘客在船上有8个兄弟姐妹或配偶,确实有家庭可能非常庞大),Parch最大6。四分位数表明多数乘客没有携带直系家庭成员上船(75%分位处SibSp=1, Parch=0)。 - 票价:
Fare分布跨度大,从0到512不等。平均票价32.20英镑,中位数14.45,表明票价分布可能偏态分布(有少数乘客支付了极高票价——512.33是船上最贵的票,应是豪华套房)。票价与舱等高度相关,一等舱票价通常昂贵,三等舱便宜甚至免费(可能船员或者特殊情况)。
最后,我们查看一下训练集中各字段缺失值数量,以便制定缺失值处理策略:
# 统计训练集每列缺失值数量
train_df.isnull().sum()
结果会显示每个字段缺失值的个数,例如:
PassengerId 0
Survived 0
Pclass 0
Name 0
Sex 0
Age 177
SibSp 0
Parch 0
Ticket 0
Fare 0
Cabin 687
Embarked 2
dtype: int64
这证实了我们之前通过info推断的缺失情况:Age缺失177条,Cabin缺失687条,Embarked缺失2条,其余字段无缺失。测试集的缺失值情况也类似,我们稍后在特征工程环节再统一处理。
2.2 目标变量分析:幸存者 vs 遇难者
在深入研究特征之前,先快速了解一下目标变量Survived本身的分布情况以及基础统计。
首先,可以计算幸存者(1)和遇难者(0)的数量:
# 计算幸存者和遇难者的人数
survived_count = train_df['Survived'].sum()
died_count = len(train_df) - survived_count
survival_rate = survived_count / len(train_df)
print(f"总乘客数: {len(train_df)}")
print(f"幸存者人数: {survived_count}, 遇难者人数: {died_count}")
print(f"幸存率: {survival_rate:.2%}")
输出大致为:
总乘客数: 891
幸存者人数: 342, 遇难者人数: 549
幸存率: 38.38%
如前所述,生存率约为38%,遇难率约62%。这意味着数据是偏向遇难者居多的。虽然幸存和遇难都占了相当比例,但遇难是多数类。在建模时,我们需要关注模型在这两类上的表现,而不仅仅追求整体准确率。
我们可以使用图形来更直观地看幸存与否的分布。例如,绘制一个简单的计数图:
import seaborn as sns
import matplotlib.pyplot as plt
# 绘制Survived的计数图
sns.countplot(x='Survived', data=train_df)
plt.xticks([0, 1], ['Died (0)', 'Survived (1)'])
plt.title("Count of Survival Status")
plt.show()
(如果绘图环境不可用,可忽略显示图形的代码,但建议在本地或Colab上运行查看图像。)
该图将显示Survived=0和Survived=1的条形高度对比,可以直观看出遇难人数多于幸存人数。之后的分析我们会将Survived与其他特征组合,看看不同人群的生存率差异。
2.3 特征与生存率关系分析
现在,我们逐个探究各个特征对生存率的影响。常识和历史告诉我们,一些因素(如性别、年龄、舱等)应该与泰坦尼克号乘客的幸存概率密切相关,我们将在数据中验证这些假设。
2.3.1 性别(Sex)与生存率
首先考察性别。众所周知,泰坦尼克沉没时实行了“女士优先”的救生原则,因此我们预计女性乘客的生存率会显著高于男性。
让我们计算不同性别的生存率:
# 按性别计算生存率
survival_by_sex = train_df.groupby('Sex')'Survived'.mean()
print(survival_by_sex)
输出应为类似:
Sex
female 0.742038
male 0.188908
Name: Survived, dtype: float64
即女性乘客的生存率约为0.742(74.2%),男性仅为0.189(18.9%)。这验证了我们的预期:几乎四分之三的女性幸存,而只有不到五分之一的男性幸存 (〖从零开始学Kaggle竞赛〗泰坦尼克之灾_kaggle比赛泰坦尼克- 从灾难中学习机器学习提交比赛需要提交什么文件-优快云博客)。这是一条非常强的信息,对预测有直接的意义。也难怪Kaggle提供的示例提交(gender_submission.csv)直接以性别为依据猜测生还情况,虽然简单,但女性=生存、男性=遇难的规则已经能达到约78%多的准确率(因为男性居多且大部分男性遇难了)。
我们也可以通过可视化更直观地呈现这一差异。例如使用条形图或分组柱状图:
# 绘制不同性别的生存率柱状图
sns.barplot(x='Sex', y='Survived', data=train_df)
plt.title("Survival Rate by Sex")
plt.ylabel("Survival Rate")
plt.show()
这个图会显示男性和女性对应的生存率柱状高度(带置信区间)。可以清楚看到女性柱的高度远高于男性柱,印证女性生存率更高。
结论:性别是影响生存的一个极其显著的因素。在后续建模时,我们必须将性别作为一个重要特征纳入模型。同时,性别特征的表现也为特征工程提供思路,例如我们可能不需要对这个特征做太复杂的转换,因为它本身就是很强的信号。
2.3.2 客舱等级(Pclass)与生存率
泰坦尼克号按照舱等划分乘客社会阶层:一等舱乘客多为富裕的上层阶级,住在上层甲板且靠近甲板,脱险可能性更高;三等舱多为底层甲板的普通乘客。我们预计舱等越高(数值越小的一等舱),生存率越高。
计算不同Pclass的生存率:
# 按客舱等级计算生存率
survival_by_class = train_df.groupby('Pclass')'Survived'.mean()
print(survival_by_class)
可能输出:
Pclass
1 0.629630
2 0.472826
3 0.242363
Name: Survived, dtype: float64
这表示:一等舱乘客的生存率约为62.96%,二等舱约为47.28%,而三等舱只有约24.24%。差异显著:一等舱幸存率大概是三等舱的2.6倍 (Titanic_Dataset_Exploratory_Analysis)。这与我们关于社会阶层和逃生机会的推测一致——上层阶级(Pclass=1)由于更靠近甲板、获得救生艇的优先权,生还机会更大,而三等舱乘客处境最为不利。
用图表展示更直观:
# 绘制不同舱等的生存率
sns.barplot(x='Pclass', y='Survived', data=train_df)
plt.title("Survival Rate by Passenger Class")
plt.ylabel("Survival Rate")
plt.show()
图中会显示1、2、3等舱的平均生存率条形。可以看到,舱等与生存率大致呈正相关:1等舱最高,3等舱最低。
进一步,我们还可以观察性别和舱等的交互作用。例如,一等舱的女性、生存率也许接近100%;三等舱男性生存率可能更低于平均。这种组合信息可以通过透视表或分组进一步分析:
# 组合Sex和Pclass,计算各组生存率
survival_by_sex_class = train_df.groupby(['Sex', 'Pclass'])'Survived'.mean()
print(survival_by_sex_class)
输出示例:
Sex Pclass
female 1 0.968085
2 0.921053
3 0.500000
male 1 0.368852
2 0.157407
3 0.135447
Name: Survived, dtype: float64
这表格显示:
- 女性在1等舱生存率高达96.8%,在2等舱也有92.1%,即使在3等舱也有50%。
- 男性在1等舱生存率约36.9%,2等舱15.7%,3等舱仅13.5%。
由此可见,如果你是三等舱男性,生还可能性非常渺茫;而一二等舱女性几乎都得救了。这种显著差异后面也许可以作为新特征,例如组合Sex和Pclass。
2.3.3 登船港口(Embarked)与生存率
泰坦尼克号从英国南安普顿(S)出发,中途停靠法国瑟堡(C)和爱尔兰皇后镇(Q)接乘客,然后驶向纽约。因此登船港口Embarked某种程度和乘客社会背景有关(例如在瑟堡登船的多数是一等舱富人,在南安普顿登船的三等舱乘客更多 (Titanic_Dataset_Exploratory_Analysis))。我们来看不同登船港口乘客的生存率:
# 按登船港口计算生存率
survival_by_embark = train_df.groupby('Embarked')'Survived'.mean()
print(survival_by_embark)
假设输出:
Embarked
C 0.553571
Q 0.389610
S 0.336957
Name: Survived, dtype: float64
即:
- 从Cherbourg(瑟堡)登船的乘客生存率最高,约55.4%;
- Queenstown(皇后镇)登船的约38.96%;
- Southampton(南安普顿)登船的最低,约33.7%。
这表明登船港口似乎确实与生存率有关 (Titanic_Dataset_Exploratory_Analysis)。一开始看可能令人奇怪:为什么瑟堡登船的生存率更高?结合背景分析,原因可能是从瑟堡登船的乘客中一等舱比例高(大量富人从法国上船),而从南安普顿上的多为三等舱移民,因此整体生存率受到舱等结构影响 (Titanic_Dataset_Exploratory_Analysis)。事实上,数据分析发现瑟堡登船的乘客有50%以上是一等舱,而南安普顿登船的一等舱不到20% (Titanic_Dataset_Exploratory_Analysis)。所以港口本身并非直接影响生存率,而是因为它与乘客群体构成相关。这提示我们:Embarked可以作为特征,但它可能主要是间接反映了社会阶层。在特征工程时,我们可以保留Embarked,也可能将其与Pclass交叉考虑。
用柱状图展示:
# 绘制不同登船港口的生存率
sns.barplot(x='Embarked', y='Survived', data=train_df)
plt.title("Survival Rate by Embarked Port")
plt.ylabel("Survival Rate")
plt.show()
图上C港口柱子最高,S最低,中间是Q。这与我们上面的数字一致:瑟堡登船的幸存率最高。
2.3.4 年龄(Age)与生存率
年龄对生存率的影响需要一点更细致的分析。当时救生原则除了“女士优先”还有“儿童优先”。直觉上,儿童更容易获救,而老年人和青壮年的生存率可能较低。不过需要结合性别与舱等,因为一等舱儿童和三等舱儿童的命运也可能不同。
首先简单地看一下幸存者和遇难者的年龄分布差异。我们可以比较生存者的年龄平均值或中位数,或者绘制年龄分布图(例如直方图或核密度图):
# 计算幸存者和遇难者的平均年龄(忽略缺失值)
avg_age_survived = train_df[train_df['Survived']==1]['Age'].mean()
avg_age_died = train_dftrain_df['Survived'==0]'Age'.mean()
median_age_survived = train_dftrain_df['Survived'==1]'Age'.median()
median_age_died = train_dftrain_df['Survived'==0]'Age'.median()
print(f"幸存者平均年龄: {avg_age_survived:.1f}, 中位数: {median_age_survived}")
print(f"遇难者平均年龄: {avg_age_died:.1f}, 中位数: {median_age_died}")
根据数据,可能得到:
幸存者平均年龄: ~28.3岁, 中位数: 28.0
遇难者平均年龄: ~30.6岁, 中位数: 28.0
平均而言幸存者略年轻一些,但中位数都是28岁。差异不是非常显著。为了更深入地看年龄影响,我们可以将年龄按区间分组或者绘制生存与否的年龄分布图。例如,把乘客分为儿童(<=12岁)、青少年(13-18)、青年(19-30)、中年(31-50)、老年(>50) 等等,看各组生存率;或者直接绘图。
用直方图/密度图比较:
# 绘制年龄分布直方图,按生存与否分色
plt.figure(figsize=(8,4))
train_dftrain_df['Survived'==1]'Age'.hist(bins=20, alpha=0.5, color='green', label='Survived')
train_dftrain_df['Survived'==0]'Age'.hist(bins=20, alpha=0.5, color='red', label='Died')
plt.xlabel("Age")
plt.ylabel("Count")
plt.legend()
plt.title("Age distribution: Survived vs Died")
plt.show()
这幅图会将幸存者和遇难者的年龄分布重叠展示(绿色表示幸存者,红色为遇难者)。通过观察,可以发现儿童(低龄)部分绿色比例较高,说明儿童幸存率高;在20-40岁成年区间,红色(遇难者)较多;老年乘客(60岁以上)数量本就不多,幸存者也很少。
我们也可以计算某个年龄阈值以下乘客的生存率。例如,求出16岁以下儿童的生存率,与成人对比:
# 增加一个表示儿童的字段,定义年龄<16为儿童
train_df['IsChild'] = (train_df'Age' < 16).astype(int)
child_survival_rate = train_df.groupby('IsChild')'Survived'.mean()
print(child_survival_rate)
输出类似:
IsChild
0 0.361233
1 0.550000
Name: Survived, dtype: float64
表示:16岁及以上乘客生存率约36.1%,而16岁以下乘客生存率为55%。儿童确实高出不少。不过这里没有考虑性别因素,例如儿童里女孩更多幸存等。总体上,可以说年龄小是生存的优势。
接下来我们将年龄作为连续变量,可能在模型中用或做适当转换。可以考虑将年龄分段作为分类特征,比如婴幼儿(0-5)、少年(6-12)、青年(13-18)、成年(19-35)、中年(36-60)、老年(60+)等,从而捕捉非线性关系。不过由于我们之后会尝试神经网络模型,它可以一定程度上自动学习非线性关系,所以未必需要手工分段。我们会在特征工程中再决定。
2.3.5 家庭大小(SibSp, Parch)与生存率
直观想法:独自旅行的人和大家庭可能在灾难中处境不同。有人推测太大的家庭不容易整体幸存(因为人多可能行动更慢,且不可能都上同一条救生艇),而完全独自的人也许在混乱中没人照应,生存率可能也低于有一两个亲友一起的人。让我们验证一下。
可以用SibSp和Parch组合构造“家庭大小”特征:FamilySize = SibSp + Parch + 1(+1把自己也算进去)。在特征工程部分我们会正式创建这个特征。这里先做一个简单分析:
# 计算家庭大小并统计不同大小的生存率
train_df['FamilySize'] = train_df['SibSp'] + train_df['Parch'] + 1
survival_by_family = train_df.groupby('FamilySize')'Survived'.mean()
print(survival_by_family)
输出将列出家庭成员数量从1到11的平均生存率。例如:
FamilySize
1 0.30
2 0.52
3 0.58
4 0.72
5 0.20
6 0.13
7 0.33
8 0.00
11 0.00
Name: Survived, dtype: float64
这是假设输出,用来说明趋势:
- 独自一人(FamilySize=1)的生存率约30%(低于整体平均38%)。
- 小家庭(2~4人)的生存率较高:例如2人52%,3人58%,4人72%(这个高可能因为4人中很多是二等舱家庭?)。
- 大家庭(5人及以上)生存率急剧下降:5人20%,6人13%,7人33%(样本可能很少),8人以上几乎为0(比如有一家8口和一家11口全部遇难)。
从这些数字看,适中的家庭规模(2-4人)生存率最高,而单身乘客和大家庭(5+)生存率明显较低 (Titanic Survival Analysis. Introduction | by Ejike Uchenna Splendor) (Titanic Passenger Survival Analysis - Data Visualization - RPubs)。这可能是因为:
- 独自旅行时,遇难时没人帮助照料(例如无人带你上救生艇)。
- 家庭太大时,可能为了彼此不分开而错失逃生机会,或者一家占据多个名额困难。
可视化方面,可以绘制家庭大小 vs 生存率的曲线/柱状图来更清楚地展示这一趋势,但考虑到有些家庭大小样本很少(比如8、11),曲线末尾可能波动。我们在特征工程时会决定如何处理这个特征,例如可以进一步衍生一个IsAlone特征表示是否独自,或者对FamilySize做截断/分桶。
2.3.6 其他特征:姓名和客舱
姓名(Name):Name本身作为字符串无法直接用于模型,但其中**头衔(Title)**是很有价值的信息。比如Name中“Mr.”、“Mrs.”、“Miss.”、“Master.”等可以提取出来,代表性别、婚姻状况和年龄段(Master一般指未成年男性)。在EDA中我们可以粗略看一下各种头衔的人数和幸存率,为之后的特征工程做准备。
# 提取称呼Title并看幸存率(简略分析)
train_df['Title'] = train_df['Name'].str.extract('.*, (\w+\.)') # 从Name中提取逗号后第一个单词加点
print(train_df'Title'.value_counts())
print(train_df.groupby('Title')'Survived'.mean())
(提取Title的正则或方法有多种,这里简化处理,后面特征工程会更严谨。)
可能输出:
Mr. 517
Miss. 182
Mrs. 125
Master. 40
Dr. 7
Rev. 6
Col. 2
Major. 2
Mlle. 2
Lady. 1
Countess. 1
Jonkheer. 1
Mme. 1
Capt. 1
Sir. 1
Don. 1
Name: Title, dtype: int64
Title
Capt. 0.0
Col. 0.0
Countess. 1.0
Don. 0.0
Dr. 0.4
Jonkheer. 0.0
Lady. 1.0
Major. 0.5
Master. 0.575
Miss. 0.697
Mlle. 0.5
Mme. 1.0
Mr. 0.156
Mrs. 0.793
Rev. 0.0
Sir. 1.0
Name: Survived, dtype: float64
可以看到:
- 常见头衔:Mr.(男性)、Miss.(未婚女性)、Mrs.(已婚女性)、Master.(少年男孩)占绝大多数。
- 少见头衔如
Dr.、Rev.(牧师)、Col.(上校)、Major.、贵族头衔Sir.、Lady.、Countess.、外语称谓Don.(西班牙语贵族称呼)、Mme.(法语女士)、Mlle.(法语小姐)、Jonkheer.(荷兰贵族)等。由于样本极少,可以合并为“Rare”类别。 - 生存率:
Mr.很低(15.6%,男性整体低),Mrs.高(79.3%,女性已婚多为成人女性,生存率高),Miss.也高(69.7%),Master.约57.5%(较高,很多Master是小男孩,确实有救),Dr.约40%(有男有女Doctor),Rev.牧师全遇难(0%),一些贵族称谓Lady/Countess/Sir/Mme等生存率100%(但样本1-2个,几乎都是上层女性和男性贵族被重点照顾了),Col/Major军官等也幸存率低,等等。
头衔提供了有趣的信息,将作为后续特征工程的重要一环。EDA让我们确定需要把Title提取出来,并把稀有头衔合并处理。
客舱号(Cabin):由于Cabin缺失太多(77%缺失),完全缺失的样本我们可能无法利用具体舱号。但我们可以提取Cabin的首字母(甲板号)作为特征,因为甲板位置可能和逃生有关系。然而,由于缺失非常多,我们很可能只用一个是否有Cabin的指示(有Cabin通常是一二等舱,有自己的房间;没Cabin多数三等舱或船员舱位)。在EDA中,我们可以看一下有无Cabin记录的生存率:
# 是否有客舱号
train_df['HasCabin'] = train_df['Cabin'].notna().astype(int)
survival_by_cabin = train_df.groupby('HasCabin')'Survived'.mean()
print(survival_by_cabin)
输出:
HasCabin
0 0.299854
1 0.666667
Name: Survived, dtype: float64
即没有Cabin信息的人(多数是三等舱)生存率约29.9%,有Cabin号的人生存率66.7%(注意:后者包括船员和妇孺?). 这个巨大的差异也与舱等/经济地位息息相关。但它提示HasCabin可以作为一个强特征使用。
Cabin首字母(Deck)可以尝试提取,但鉴于缺失如此多,模型训练时能用这个信息的样本有限。我们会考虑是否值得使用。在EDA中我们不深入细化Cabin了。
通过以上探索,我们得到一些明确的结论:
- 性别:女性生存率远高于男性,是最强预测因素之一。
- 舱等:一等舱生存率 > 二等舱 > 三等舱,反映社会阶层影响 (Titanic_Dataset_Exploratory_Analysis)。
- 港口:瑟堡登船乘客生存率最高,南安普顿最低,但港口影响实为其与舱等关联所致 (Titanic_Dataset_Exploratory_Analysis)。
- 年龄:总体上幸存者略年轻,但显著的是儿童优先原则,16岁以下儿童生存率高于平均。需要在模型中体现儿童与成年人的区别。
- 家庭:有1-3名亲友同行的乘客生存率比独身或大家庭更高,适度的家庭规模似乎有利 (Titanic Survival Analysis. Introduction | by Ejike Uchenna Splendor)。我们应引入
FamilySize和衍生特征如是否独自,以捕捉这一模式。 - 头衔:乘客姓名中的称谓Title蕴含丰富信息,如性别、年龄段、社会地位等,与生存率密切相关(如Mr低, Mrs/Miss高, Master较高, 少数贵族头衔高等)。需要将其提取并编码。
- 客舱信息:大量缺失,但是否有Cabin号可以区分一部分高存活率群体。
这些发现将直接指导我们的数据预处理与特征工程:我们将针对上述重要因素创建和转换特征,以最大限度地让模型利用这些信息。
在进入下一步之前,我们先删除在探索中临时添加的列(如IsChild, FamilySize, Title, HasCabin),以免干扰后续正式的特征工程流程:
# 删除EDA时临时添加的列,清理数据框
train_df.drop(['IsChild', 'FamilySize', 'Title', 'HasCabin'], axis=1, inplace=True, errors='ignore')
现在,我们准备开始对数据进行预处理和特征工程了。
3. 数据预处理与特征工程
通过EDA我们发现了一些数据质量问题(缺失值)以及潜在有用的新特征。本节将对训练集和测试集同时进行清洗和特征工程转换,确保最终得到适合模型输入的干净数据集。
主要工作包括:
- 处理缺失值(Age、Embarked、Fare、Cabin等)。
- 提取并转换特征(如Name中的Title, SibSp+Parch形成FamilySize, 是否独自IsAlone, 是否有Cabin)。
- 将类别变量编码成数值形式(独热编码或标签编码等)。
- 特征缩放(Normalization/Standardization)使数值特征在相似尺度上。
- 确保对训练集和测试集做一致的变换,避免数据泄漏和维度不匹配。
为了方便对两份数据做相同处理,我们可以将训练和测试数据合并处理某些步骤(尤其是特征提取和编码部分)。不过需注意避免在合并处理中使用测试集的任何标签信息(好在测试集没有Survived列)。对缺失值的填补,应尽量使用训练集的信息,以防数据泄漏。在合并的过程中,我们可以加入一个标识区分训练和测试,以便最后再拆分。
让我们开始逐步实现:
3.1 合并数据集以统一处理
# 添加一个标识列,1表示训练集,0表示测试集
train_df['isTrain'] = 1
test_df'isTrain' = 0
# 在测试集添加Survived列占位(值全为None或nan),以便与训练集对齐列
test_df'Survived' = None
# 合并训练和测试数据
full_df = pd.concat([train_df, test_df], ignore_index=True)
print("Combined data shape:", full_df.shape)
我们给训练集打上标签isTrain=1,测试集isTrain=0,并给测试集加了空的Survived列以对齐(也可以不这么做,直接在concat时忽略),然后垂直拼接。full_df现在包含了891+418 = 1309条乘客记录,列涵盖了所有原始特征(和Survived标签、isTrain标识)。注意,这里Survived列对测试部分是NaN,我们不会去碰它。
3.2 缺失值处理
根据之前的缺失统计,主要需要处理:
Age(年龄)缺失177条(训练)+ 测试中也有缺失。Embarked(登船港)缺失2条(训练)。Fare(票价)测试集中缺失1条(训练集无缺失)。Cabin大量缺失,我们另行处理。
Embarked 缺失填充
Embarked在训练集有两条缺失。通常用出现频率最高的港口来填充即可。我们先看哪个港口最多:
# 找出Embarked的众数(不含缺失)
embarked_mode = full_df.loc[full_df['isTrain']==1, 'Embarked'].mode()[0]
print("Embarked mode (most frequent):", embarked_mode)
训练集中Embarked众数是**‘S’**(南安普顿),因为绝大多数人在南安普顿登船。我们就用’S’填补缺失值 (Kaggle 泰坦尼克号挑战赛目标 使用泰坦尼克号乘客数据(名字,年龄,票价等)预测谁将存活或者死去。 数据 在数据中有 - 掘金):
# 填充Embarked缺失值为众数 'S'
full_df['Embarked'].fillna(embarked_mode, inplace=True)
(注:在一些分析中,也有人根据缺失Embarked乘客的票价和舱等推测其实他们登船港口为C,因为票价较高且是1等舱。不过这种推测对整体结果影响不大,我们采取简单众数法。)
Fare 缺失填充
测试集有1个乘客票价缺失。我们可以用该乘客所属的舱等的票价中位数或均值来填充。首先找出这个缺失项是谁:
# 找到Fare缺失的样本信息
missing_fare = full_df[full_df['Fare'].isna()]
print(missing_fare[['PassengerId','Pclass','Embarked','Age']])
假设输出类似:
PassengerId Pclass Embarked Age
1043 3 S 60.5
也就是测试集中PassengerId 1044(因为训练891,加上去编号应该1044)的乘客,三等舱,从S港口登船,年龄60.5岁,票价缺失。
我们用三等舱、S港口乘客的票价中位数来填充比较合理,因为票价与舱等和登船港相关。计算这个中位数:
# 计算同舱等同登船港口乘客的票价中位数(基于训练集)
med_fare_3S = full_df[(full_df['isTrain']==1) & (full_df['Pclass']==3) & (full_df'Embarked'=='S')]['Fare'].median()
print("Median fare for 3rd class from S:", med_fare_3S)
# 填充缺失Fare
full_df'Fare'.fillna(med_fare_3S, inplace=True)
假定计算得到的3等舱S港口票价中位数为7.925(数据中常见值),缺失Fare即填充为7.925。这样处理基于同类群体典型值。
(另一种简单方法:直接用训练集Fare总体中位数填充,也无不可,但我们用了细分类别的中位数更贴切一些。)
Age 缺失填充
年龄是更复杂的缺失情况。简单填充如使用平均数或中位数,会忽略年龄与其他特征的关联,可能引入偏差。我们希望更智能地填补年龄,比如利用乘客的头衔Title来推断。通常,不同头衔的人年龄分布不同,例如:
Master(未成年男性)的年龄普遍小(孩子),可能集中在0-14岁范围。Miss(年轻女性)分布可能从小女孩到年轻女性,大概在少女到年轻成人阶段平均20多岁。Mrs(已婚女性)多为成人或者中年女性,年龄平均在30岁上下。Mr(成年男性)分布较广但主要集中在成年、中年男性。- 稀有头衔如
Dr,Col,Major往往属于中年或老年男性;Countess,Lady是贵妇,可能中老年。 Rev牧师一般也年龄较大。
利用头衔分类计算各类的年龄中位数可能是个不错的方法,然后据此填补相应类别缺失年龄 (Titanic Survival Analysis. Introduction | by Ejike Uchenna Splendor)。
我们已经提取过一次Title,在正式处理中我们要规范地提取并归类:
# 从Name提取Title(称谓)
full_df'Title' = full_df'Name'.str.extract('*, (\w+\.)') # 提取逗号后首个单词+点
full_df'Title'.value_counts()
我们再看一次所有头衔种类及频数(full_df里包含测试集,所以可能出现训练集没有的Title,比如"Dona."在测试集中出现)。上面输出基本跟之前看到的差不多,但可能多了一个Dona.:
Mr. 757
Miss. 260
Mrs. 197
Master. 61
Dr. 8
Rev. 6
Col. 4
Major. 2
Mlle. 2
Ms. 2
Lady. 1
Countess. 1
Jonkheer. 1
Don. 1
Dona. 1
Mme. 1
Capt. 1
Sir. 1
我们需要将这些Title进行归类简化:
- 常见主要类别:
Mr.,Mrs.,Miss.,Master.(保留) - 少量与这些等价的:
Ms.一般视为Miss.,Mlle.(法语小姐)视为Miss.,Mme.(法语夫人)视为Mrs.。 - 其余稀有头衔全部归为一类,比如
Rare。
具体映射规则可以如下:
# 先统一一些同义称呼
full_df'Title'.replace({'Mlle.': 'Miss.',
'Ms.': 'Miss.',
'Mme.': 'Mrs.',
'Dona.': 'Rare', # Dona是西班牙语女贵族,归为Rare
}, inplace=True)
# 稀有头衔列表
rare_titles = ['Dr.', 'Rev.', 'Col.', 'Major.', 'Lady.', 'Countess.',
'Jonkheer.', 'Don.', 'Capt.', 'Sir.']
full_df'Title' = full_df'Title'.replace(rare_titles, 'Rare')
full_df'Title'.value_counts()
现在Title列大概剩下这些类别:Mr., Miss., Mrs., Master., Rare五种。
接下来,我们计算训练集中每个Title的年龄中位数,用于填充年龄缺失:
# 计算各Title组的年龄中位数(仅用训练集部分)
title_median_age = full_dffull_df['isTrain'==1].groupby('Title')'Age'.median()
print(title_median_age)
假设输出(基于常识近似):
Title
Master. 4.0
Miss. 22.0
Mr. 30.0
Mrs. 35.0
Rare 46.5
Name: Age, dtype: float64
这些中位数年龄符合直觉:Master(小男孩)~4岁,Miss(年轻女性)22岁,Mr(成年男性)30岁,Mrs(已婚女性)35岁,Rare(特殊称号,多为年长者)46.5岁左右。
现在用这个mapping填充Age缺失:
# 定义一个函数,根据Title填充Age缺失值
def fill_age_by_title(row):
if pd.isna(row'Age'):
return title_median_age[row['Title']]
else:
return row'Age'
full_df'Age' = full_df.apply(fill_age_by_title, axis=1)
# 再确认是否还有缺失
print("Remaining missing Age:", full_df'Age'.isna().sum())
我们逐行检查,如果Age为空,就查找该行Title对应的中位数年龄赋值。这就完成了Age的填充。顺带确保Title提取中,如果有缺失Title(极少见,Name都有格式),可以考虑额外逻辑。但Name应该都有头衔。
检查结果应该显示 Age 缺失为0,表示所有年龄都填补完毕。
Cabin处理
由于Cabin缺失非常多,我们不会试图填补具体Cabin值。但我们可以提取有无Cabin或Cabin的首字母。这里建议:
- 增加特征
HasCabin表示是否有客舱号。 - 提取
Deck= Cabin首字母,对于有Cabin的样本。
看看Cabin首字母的分布:
# 增加HasCabin特征
full_df'HasCabin' = full_df'Cabin'.notna().astype(int)
# 提取Cabin的首字母作为Deck
full_df['Deck'] = full_df'Cabin'.str0 # 取Cabin字符串第一个字母
full_df'Deck'.value_counts(dropna=False)
可能出现Deck类别:A, B, C, D, E, F, G, T 和 NaN(NaN代表原先Cabin缺失的,HasCabin=0的)。其中T是一个特殊情况(实际数据里只有一个乘客Cabin=‘T’,属异常值或特殊,我们可以将其归为另外或当做Rare deck)。
鉴于NaN太多,我们不直接用Deck作为模型特征,因为80%乘客没Deck值。但HasCabin=0/1本身就是一个重要二值特征,可以保留。另外,也可以将NaN视为一类Deck ‘U’(Unknown)然后做one-hot,尽管大量’Unknown’类别可能不太有信息除非我们通过HasCabin已经捕获了。这里可以二选一:
- 要么用HasCabin二值
- 要么用Deck多分类(包含U)
我们可以两个都生成,但最终可能选其一。随机森林这种模型可以从HasCabin提取信息,神经网络也可以。使用Deck one-hot也能表达类似信息但稍微丰富一点(不同有Cabin的舱层也许有些区别,一等舱多是A B C D E decks,三等舱可能在F G decks,可能进一步细分幸存率)。
为了尽可能保留信息,我们先将NaN填为’U’(Unknown deck),然后使用Deck:
# 将缺失的Deck(原Cabin缺失)标记为 'U'
full_df'Deck' = full_df'Deck'.fillna('U')
full_df'Deck'.replace('T', 'U', inplace=True) # 把唯一的异常Deck T归为U
full_df'Deck'.value_counts()
Deck现在类别有A, B, C, D, E, F, G, U。我们可以保留Deck供模型使用,也保留HasCabin。不过注意二者高度相关(Deck=U <-> HasCabin=0),在某些模型中可能重复。我们后续可能只选择其中之一(比如对于树模型用HasCabin就够,对于神经网络one-hot Deck也行)。
先保留,建模时可以实验。
检查全部缺失处理完毕
# 再次检查缺失值情况
full_df.isnull().sum()
应该除了测试集Survived(NaN)外,其它特征都无缺失了。如果看到有任何特征仍有缺失,需要重新处理。若都为0,则表明数据清洗完成。
3.3 特征工程:新增与转换特征
在清洗完基本数据后,我们构造前面讨论的新特征,并将一些需要的特征转为模型可用的形式。
3.3.1 特征:FamilySize 和 IsAlone
根据SibSp和Parch计算家庭大小:
# 构造FamilySize特征
full_df'FamilySize' = full_df'SibSp' + full_df'Parch' + 1
# 构造IsAlone特征:是否独自出行(家庭大小为1则独自)
full_df['IsAlone'] = (full_df'FamilySize' == 1).astype(int)
FamilySize为具体人数(1~11),IsAlone为0或1。
我们可以检查一下FamilySize的分布以及IsAlone在数据中的比例,以确认正确:
print(full_df'FamilySize'.value_counts())
print(full_df'IsAlone'.value_counts())
3.3.2 特征:优化Title(称谓)
我们之前提取了Title并分组填充年龄,现在Title本身也是很好的分类特征,应当纳入模型。现在full_df’Title’已经只有5类:Mr, Miss, Mrs, Master, Rare。我们可以将其转为数值编码或后面直接做独热编码。
3.3.3 删除无用特征
有些原始特征对预测没有直接帮助或者和我们新特征重复冗余,考虑删除:
PassengerId:只是识别ID,对预测无意义。但在生成提交文件时需要保留ID,所以训练时删除,最终输出再用test数据保存的ID。Name:我们已经提取了Title,Name本身不需要了。Ticket:船票号码,比较杂乱。虽然一些高级特征工程会从Ticket提取信息(如票号是否有字母前缀、共同票号的人数),但出于简洁我们暂不使用Ticket。可以考虑Ticket团体特征(同票号的人数, 这其实和Family有重叠但也包含非亲属团体),这里我们不深入。Cabin:我们已经提取了Deck和HasCabin,原始Cabin字符串可以舍弃。
Embarked我们保留,Sex保留,Pclass保留,Age、Fare这些数值保留。
所以我们现在drop字段:
full_df.drop(['PassengerId','Name','Ticket','Cabin'], axis=1, inplace=True)
注意,此时full_df仍包含训练+测试合并的数据,但我们drop的PassengerId要小心:测试集的ID我们还要用于提交,因此我们需要在删除前先存储一下测试集的PassengerId以备后用:
# 备份测试集的PassengerId,以便提交
test_ids = test_df['PassengerId'].copy()
# 现在删除不需要的列
full_df.drop('PassengerId','Name','Ticket','Cabin', axis=1, inplace=True)
3.3.4 编码分类特征
目前在full_df中,仍有一些特征是类别型数据,需要转换成数字形式:
- Sex(当前为"male"/"female"字符串)
- Embarked(“C”,“Q”,“S”)
- Title(“Mr”,“Miss”,“Mrs”,“Master”,“Rare”)
- Deck(“A”,“B”,…,“U”)
- Pclass目前是整数1/2/3,但其实是有序类别,也可以直接当作数值或做one-hot都行。
- HasCabin, IsAlone 目前已经是0/1,不需处理。
- FamilySize是整数,可以当做数值特征保留或进行离散化。考虑到FamilySize范围1-11,不算大,我们也可以保持数值形式(或者转成类别one-hot也可)。简单起见我们暂当作数值连续特征。
对于类别数据,一种常用方法是独热编码(One-Hot Encoding),将每个类别转换为一个独立的二元特征。例如Sex可以转换成一个Sex_female二值列(male则为0, female为1),或者两列(female和male列,用0/1表示,但male列其实冗余,多出一列需要避免共线可以少一列)。Pandas可以用get_dummies方便地对DataFrame做这件事。
也可以LabelEncode(把类别转成数字编码,如 male=0,female=1),对于有序类别Pclass来说直接用数字1,2,3表示也是OK的。但对于无序类别(Embarked, Title等),简单label编码会让算法误以为有顺序,例如Embarked编码C=0,Q=1,S=2可能引入不存在的大小关系。所以更安全的是使用独热编码。
为演示清晰,我们对Sex, Embarked, Title, Deck这些做独热编码,对Pclass由于它其实有大小顺序关系,可直接用数值1/2/3也可以用独热。这里我们统一用独热编码处理,反正三个舱等独热也只是多两列而已(独热编码会n类产生n列或n-1列,我们可以让Pandas产生n列然后可以视情况去掉一列避免完全共线,但对决策树和神经网络来说保留全部dummy问题不大,线性模型的话要注意共线性,不过线性模型一般有正则化也问题不大)。
# 对分类变量进行独热编码
categorical_features = ['Sex','Embarked','Title','Deck','Pclass']
full_df = pd.get_dummies(full_df, columns=categorical_features, drop_first=False)
print("Data shape after one-hot encoding:", full_df.shape)
这里我们没有drop_first,让每个类别保留一列(包括一个基准类别),以便完整表示信息。对于一些模型(如非正则化的线性模型)这样会有共线性,但我们之后主要模型不是简单线性回归所以无妨。
现在full_df中的数据都是数值型的列了。我们可以看一下经过编码后的列,比如原来Sex变成了Sex_female和Sex_male两列(如果drop_first=False会两个都在,但其实一个可以删除以避免冗余);Embarked变成Embarked_C, Embarked_Q, Embarked_S三列;Title变成Title_Master, Title_Miss, Title_Mr, Title_Mrs, Title_Rare等;Deck变成Deck_A,…Deck_U;Pclass变成Pclass_1, Pclass_2, Pclass_3等。
可能很多列,我们打印shape看看。原始full_df(合并集)有 isTrain, Survived, Sex, Embarked, Age, SibSp, Parch, Fare, Title, HasCabin, Deck, FamilySize, IsAlone, Pclass 这么多列,编码后去掉这些换成数列:
- Sex:2列
- Embarked:3列
- Title:5列
- Deck:8列
- Pclass:3列
- isTrain, Survived, Age, SibSp, Parch, Fare, HasCabin, FamilySize, IsAlone remain (Age, Fare, SibSp, Parch, FamilySize都作为连续变量保留)
- isTrain和Survived其中isTrain我们可保留或去掉,因为我们之后要拆分,Survived对训练有用,对测试为空。
编码后列数 = 2+3+5+8+3 + (原连续: Age,SibSp,Parch,Fare,HasCabin,FamilySize,IsAlone, Survived,isTrain) = 2+3+5+8+3 + 9 = 30 + 9 = 39列左右。shape显示1309 x 39之类的。不过,isTrain和Survived在最终建模features里不是真的特征:我们需要把isTrain去掉,并把Survived作为标签y。SibSp和Parch其实已经包含进FamilySize,但也可以保留原始SibSp,Parch作为模型输入也无伤,不过FamilySize已经综合了它俩,而且IsAlone又是由FamilySize衍生,所以SibSp,Parch可能有冗余。可以考虑是否删除SibSp,Parch防止多重共线。对于树模型不严重,对于神经网络可能也不严重但有重复信息。
为了简洁,我们可以删掉SibSp和Parch,因为FamilySize和IsAlone已包含这方面信息:
full_df.drop(['SibSp','Parch'], axis=1, inplace=True)
同时isTrain列也可以删掉(在我们拆分前可以利用它分开训练和测试,但我们也可以在拆分前就删掉,保留其他)。
我们准备将full_df拆回训练和测试:
# 拆分回训练集和测试集部分
train_processed = full_dffull_df['isTrain'==1].copy()
test_processed = full_dffull_df['isTrain'==0].copy()
# 删除辅助列isTrain和标签列Survived(在train_processed中分离出来,在test_processed中没意义)
train_y = train_processed'Survived'.astype(int).copy() # 提取训练集标签
train_X = train_processed.drop(['Survived','isTrain'], axis=1).copy()
test_X = test_processed.drop('Survived','isTrain', axis=1).copy()
print("train_X shape:", train_X.shape)
print("test_X shape:", test_X.shape)
这里:
train_X是训练特征,train_y是训练标签。test_X是测试特征。- 我们确认train_X有891行,test_X有418行,列数应该一致,且不包括标签。
特征缩放(标准化):对于像Age和Fare这样的连续值,以及FamilySize,这些特征取值范围差异较大。例如Fare在0500,Age在080,FamilySize在1~11。某些模型(尤其是梯度下降优化的模型如神经网络、逻辑回归)对不同尺度的特征比较敏感,因此通常要做标准化(Standardization)或归一化(Normalization)。标准化指减均值除以标准差,使特征变成均值0方差1。归一化通常指缩放到0-1之间。
对于树模型(决策树、随机森林、XGBoost等)来说,不需要标准化,因为决策树不受特征尺度影响。但对逻辑回归、神经网络来说,标准化会帮助训练更快收敛、结果更稳定。
因此,我们将对连续数值列进行标准化。哪些是连续列?可以认为Age, Fare, FamilySize是连续变量(SibSp,Parch如果保留也是,不过我们去掉了)。HasCabin, IsAlone本来就是0/1不需要缩放(也可以当作类别)。Pclass现在独热编码了,也不需要。One-hot列都是0/1无需缩放。Title, Sex, Embarked, Deck都变成了0/1列。
所以我们仅对Age, Fare, FamilySize三列进行标准化。使用sklearn的StandardScaler:
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
# 注意:要用训练集的分布来fit scaler,然后应用到训练和测试
numeric_cols = ['Age','Fare','FamilySize']
train_X[numeric_cols] = scaler.fit_transform(train_Xnumeric_cols)
test_Xnumeric_cols = scaler.transform(test_Xnumeric_cols)
这样处理后,train_X和test_X中的Age, Fare, FamilySize列会变为均值0方差1的值(测试集也是根据训练集的均值方差变换)。这一步防止数据泄漏(只用训练集的统计)。
现在数据已经准备好了,我们可以查看最终特征列名及数量:
print("Final feature columns:", list(train_X.columns))
print("Number of features:", train_X.shape[1])
预计列包括:Age, Fare, FamilySize, IsAlone, HasCabin, 以及独热编码的 Sex_female, Sex_male, Embarked_C, Embarked_Q, Embarked_S, Title_Master., Title_Miss., Title_Mr., Title_Mrs., Title_Rare, Deck_A, Deck_B, Deck_C, Deck_D, Deck_E, Deck_F, Deck_G, Deck_U, Pclass_1, Pclass_2, Pclass_3. 加起来大概20多个特征。由于我们drop_first=False,Sex有male和female,其实其中一个是冗余。我们可以手动删掉Sex_male和Pclass_3等做drop_first。比如删除Sex_male, Embarked_S, Title_Rare? 这样每组dummy去掉一个。但其实有正则化或者树模型不怕,而且这样更直观。我们可忽略优化。
最后,数据处理总结:
- 填充了Embarked缺失为S,Fare缺失以同舱等中位数,Age缺失根据Title中位数。
- 新增特征FamilySize, IsAlone, Title, Deck, HasCabin。
- 删除无关特征PassengerId, Name, Ticket, Cabin(已提取部分)。
- 对类别变量做One-Hot编码,得到纯数值矩阵。
- 对Age, Fare, FamilySize标准化处理。
- 得到训练特征矩阵train_X (891xN)和标签train_y (891,), 以及测试特征test_X (418xN)。
到这里,我们可以进入模型构建阶段了。
4. 基线模型构建(传统机器学习)
在着手训练复杂的神经网络之前,先用几种传统的机器学习算法建立基线模型非常有益。基线模型可以帮助我们快速评估特征和数据的有效性,以及为后续更复杂模型提供参考指标。
我们将尝试以下几种经典的监督学习算法:
- 逻辑回归(Logistic Regression):线性模型,适用于二分类问题,可输出概率,解释性好。
- 决策树(Decision Tree):基于树结构的非线性模型,可捕捉特征交互但易过拟合。
- 随机森林(Random Forest):集成多棵决策树的算法,通过Bagging减少过拟合,通常效果比单棵树好。
当然还有其他模型如支持向量机、K近邻、朴素贝叶斯等,但鉴于任务性质和流行程度,我们重点试上述三种。通过比较它们在验证集上的性能,我们可以对问题的难度和特征有效性有大致了解。
4.1 划分训练集和验证集
由于我们最终要在Kaggle提交测试结果,但测试集没有真实标签,无法直接评估模型在测试集的表现,所以我们需要在训练集上进行模型验证。常用的方法有:
- 将训练集再划分出一部分作为验证集(hold-out validation)。
- 使用交叉验证(cross-validation)在训练集上多次验证以获得稳定的评估。
这里我们采用简单的hold-out方法,留取训练集的20%作为本地验证集(validation set),用80%数据训练模型,然后在这20%上评估性能。这模拟模型对未知数据的预测效果。
注:由于训练样本只有891条,这样切分可能略少,不过因为这是教程范例,我们先这样。如果想更稳健,也可以使用KFold交叉验证,比如5折交叉验证获取平均分数。另外,也可以利用全部训练集训练最终模型后再在Kaggle测试集上看排名。但为了教学清晰,我们使用固定划分便于计算混淆矩阵等具体指标。
划分操作可以用sklearn的train_test_split函数:
from sklearn.model_selection import train_test_split
# 将训练数据划分为训练子集和验证子集
X_train_sub, X_val, y_train_sub, y_val = train_test_split(train_X, train_y, test_size=0.2, random_state=42, stratify=train_y)
print("Training subset shape:", X_train_sub.shape, "Validation subset shape:", X_val.shape)
我们设置stratify=train_y以保证划分后生存与遇难的比例大致和原训练集相同,避免划分导致分布失衡。
假设划分完毕:
- X_train_sub (approx 712 x N)
- X_val (approx 179 x N)
- y_train_sub (712,), y_val(179,)
4.2 训练基线模型并评估
我们定义一个函数来评估模型在验证集的表现,计算一些常用指标:
- 准确率 (Accuracy):预测正确的比例,即 ( T P + T N ) / ( T P + T N + F P + F N ) (TP+TN)/(TP+TN+FP+FN) (TP+TN)/(TP+TN+FP+FN)。
- 精确率 (Precision):预测为正类中真正为正的比例,即 T P / ( T P + F P ) TP/(TP+FP) TP/(TP+FP),针对我们问题就是预测幸存的人中实际幸存比例。
- 召回率 (Recall):真正类被预测正确的比例,即 T P / ( T P + F N ) TP/(TP+FN) TP/(TP+FN),在我们问题就是实际幸存者中被模型预测出来的比例(也称灵敏度)。
- F1分数:精确率和召回率的调和平均 F 1 = 2 P r e c i s i o n ∗ R e c a l l P r e c i s i o n + R e c a l l F1 = 2 \frac{Precision * Recall}{Precision + Recall} F1=2Precision+RecallPrecision∗Recall,综合考虑错误代价平衡。
- AUC (ROC曲线下的面积):衡量模型区分正负类的能力,对阈值变化稳定,对类不平衡较鲁棒。AUC越接近1越好,0.5表示随机猜测水平。
此外我们也可以输出混淆矩阵来了解分类错误的具体情况。混淆矩阵四格:
预测负类(0) 预测正类(1)
实际负类(0) TN FP
实际正类(1) FN TP
这样我们能知道有多少幸存者被错判(FN)或多少遇难者被错判(FP)。
使用sklearn的accuracy_score, precision_score, recall_score, f1_score, roc_auc_score, confusion_matrix, classification_report等函数可以方便地得到这些指标。
让我们编写评估代码:
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score, confusion_matrix, classification_report
def evaluate_model(model, X_val, y_val):
"""对给定模型在验证集上计算各项指标并输出结果。"""
val_pred = model.predict(X_val) # 模型预测的类别
val_proba = None
# 如果模型有predict_proba方法,则计算正类概率用于AUC
if hasattr(model, "predict_proba"):
val_proba = model.predict_proba(X_val)[:, 1]
else:
# 有些模型没有predict_proba,比如有decision_function的SVM
if hasattr(model, "decision_function"):
# 对于逻辑回归这种,decision_function等价于logit,可用sigmoid转概率,但AUC可直接用decision values
val_proba = model.decision_function(X_val)
acc = accuracy_score(y_val, val_pred)
prec = precision_score(y_val, val_pred)
rec = recall_score(y_val, val_pred)
f1 = f1_score(y_val, val_pred)
auc = roc_auc_score(y_val, val_proba) if val_proba is not None else None
print("Accuracy: {:.4f}".format(acc))
print("Precision: {:.4f}".format(prec))
print("Recall: {:.4f}".format(rec))
print("F1-score: {:.4f}".format(f1))
if auc is not None:
print("ROC AUC: {:.4f}".format(auc))
print("Confusion Matrix:")
print(confusion_matrix(y_val, val_pred))
# 可以打印分类报告
print("Classification Report:")
print(classification_report(y_val, val_pred, digits=4))
这个函数将打印主要指标。特别地,accuracy我们期望至少比0.616(全猜遇难)要好,Precision/Recall要兼顾,但因为幸存是少数类,所以Precision低于Recall或者反之要看模型倾向,多指标可以全面看模型行为。AUC则提供阈值无关的性能度量。
4.2.1 逻辑回归
逻辑回归适合先验特征较线性可分的情况。虽然我们特征中有些非线性关系(比如FamilySize和Age等),但通过One-Hot其实引入了一些非线性能力(例如Pclass3 vs Pclass1,2,Sex etc都是离散组合)。
使用sklearn的LogisticRegression:
from sklearn.linear_model import LogisticRegression
log_reg = LogisticRegression(max_iter=500, solver='lbfgs') # 增大max_iter以确保收敛
log_reg.fit(X_train_sub, y_train_sub)
print("Logistic Regression performance on validation set:")
evaluate_model(log_reg, X_val, y_val)
我们设max_iter=500防止默认100迭代不够,solver用lbfgs(默认)即可,数据不大足够快。训练完成后,输出验证集结果,例如可能得到:
Logistic Regression performance on validation set:
Accuracy: 0.8101
Precision: 0.8056
Recall: 0.7083
F1-score: 0.7536
ROC AUC: 0.8615
Confusion Matrix:
[[95 8]
[26 50]]
Classification Report:
precision recall f1-score support
0 0.7851 0.9223 0.8485 103
1 0.8621 0.6579 0.7477 76
accuracy 0.8101 179
macro avg 0.8236 0.7901 0.7981 179
weighted avg 0.8196 0.8101 0.8054 179
(此为示例,并非真实输出。真实输出需看运行结果。)
解释:
- Accuracy ~0.81,逻辑回归已经达到81%准确率,算不错。
- Precision(针对幸存预测) ~0.8621表示模型预测为幸存的乘客中86.2%确实幸存(有些FP)。
- Recall ~0.6579表示实际幸存者中65.8%被模型识别出来,漏掉约34%。
- F1 ~0.7477综合衡量较好。
- AUC ~0.86说明模型有较强区分能力。
- 混淆矩阵:真负103,假正8,假负26,真正50。即8人实际上遇难却预测成幸存(FP=8),26人实际上幸存却预测成遇难(FN=26)。对于希望尽可能找出幸存者的目标,我们FN有26,还可改进Recall。但Precision不错。
- 分类报告也列出了0类(遇难者)和1类(幸存者)的各自指标。遇难者(0)Precision 78.5%, Recall 92.2% - 说明模型对遇难者判别很准,很多幸存者被错划为遇难(对应Recall 65.8% for class 1)。
总的来说,逻辑回归基线已经达到~0.80-0.82的水平(与很多Kaggle Titanic指南相符,常见逻辑回归能达到0.78-0.80+)。
4.2.2 决策树
使用sklearn的DecisionTreeClassifier,先不剪枝让它自由长深看看(可能会过拟合训练)。
from sklearn.tree import DecisionTreeClassifier
tree_clf = DecisionTreeClassifier(random_state=42)
tree_clf.fit(X_train_sub, y_train_sub)
print("Decision Tree performance on validation set:")
evaluate_model(tree_clf, X_val, y_val)
可能输出:
Decision Tree performance on validation set:
Accuracy: 0.7486
Precision: 0.6875
Recall: 0.6842
F1-score: 0.6858
ROC AUC: 0.7304
Confusion Matrix:
[[85 18]
[24 52]]
...
准确率可能略低(比如74.9%),Precision/Recall大概在0.68左右均衡。决策树如果不限制深度,完全可能在训练子集上100%准确但验证效果一般。我们可以打印一下训练精度:
train_acc = accuracy_score(y_train_sub, tree_clf.predict(X_train_sub))
print("Decision Tree training accuracy:", train_acc)
很可能训练上达到100%。说明过拟合严重。可以尝试剪枝参数如max_depth或min_samples_leaf等来限制复杂度,提高泛化:
tree_clf_pruned = DecisionTreeClassifier(max_depth=5, min_samples_leaf=5, random_state=42)
tree_clf_pruned.fit(X_train_sub, y_train_sub)
print("Pruned Decision Tree performance on validation set:")
evaluate_model(tree_clf_pruned, X_val, y_val)
假设得到:
Pruned Decision Tree performance on validation set:
Accuracy: 0.8045
Precision: 0.7937
Recall: 0.7105
F1-score: 0.7500
ROC AUC: 0.8153
...
修剪后,决策树性能提高到类似80.4%准确率,与逻辑回归差不多,并缓解过拟合。实际调参可通过grid search更系统完成。
4.2.3 随机森林
随机森林结合多棵树,往往能取得比单树更好的效果且不易过拟合太严重。我们直接训练一个默认100棵树的RandomForestClassifier:
from sklearn.ensemble import RandomForestClassifier
rf_clf = RandomForestClassifier(n_estimators=100, random_state=42)
rf_clf.fit(X_train_sub, y_train_sub)
print("Random Forest performance on validation set:")
evaluate_model(rf_clf, X_val, y_val)
示例输出:
Random Forest performance on validation set:
Accuracy: 0.8156
Precision: 0.8205
Recall: 0.7105
F1-score: 0.7619
ROC AUC: 0.8484
Confusion Matrix:
[[96 7]
[22 54]]
...
准确率约81.6%,略优于逻辑回归。Precision 82.0%, Recall 71.1%表明森林对幸存者也抓得不少,FP和FN都相对平衡降低了点。这个结果已经相当不错。随机森林一般能在Titanic上达到0.80-0.83不等看参数。
我们可以通过特征重要度看随机森林认为哪些特征最重要:
import numpy as np
feature_importances = rf_clf.feature_importances_
for col, imp in sorted(zip(train_X.columns, feature_importances), key=lambda x: x1, reverse=True)[:10]:
print(f"{col}: {imp:.4f}")
这将输出最重要的10个特征及其重要度。例如可能看到:
Title_Mr.: 0.1503
Sex_male: 0.1407
Fare: 0.1185
Age: 0.1004
Pclass_3: 0.0732
Title_Miss.: 0.0601
FamilySize: 0.0550
Pclass_1: 0.0476
Title_Master.: 0.0440
...
这表明模型高度依赖于Title_Mr(是否是Mr)、Sex_male、Fare、Age等,符合我们预期:性别、头衔、票价这些与生存强相关。
4.3 模型评估总结
从以上结果看:
- 逻辑回归和随机森林在验证集上达到约0.81左右准确率,表现较好。
- 决策树未剪枝时过拟合,剪枝后能达到与逻辑回归近似的80%水平。
- 模型均远超基准(全预测遇难61.6%),也超过性别单一规则(78%准确率左右)。
- 随机森林略胜一筹,Precision/Recall均衡,F1最高。
- AUC方面,逻辑回归0.86最高,随机森林0.85,决策树0.73-0.81(剪枝后0.81)。
- 就Interpretability,逻辑回归容易解释coeff,但我们这里更关注预测准确就不展开。
因此,作为基线,我们可以将随机森林或逻辑回归作为当前最好。若追求极致,可试试梯度提升决策树(如XGBoost/LightGBM),通常Titanic可以到0.82-0.84。但我们不在此赘述。
基线模型给我们一个大致期望:准确率在0.8上下。我们希望深度学习模型至少达到这个范围,不然就不如简单模型。
接下来,我们将切换到使用PyTorch构建一个深度学习模型(MLP)来尝试解这个分类问题。
5. 使用 PyTorch 构建深度学习模型
深度学习在结构化小数据(如本例891条)上不一定比传统方法更有优势,但作为练习,我们构建一个多层感知机(MLP)模型来进行泰坦尼克幸存预测。这可以帮助我们了解如何用PyTorch完成从数据到模型训练再到预测的全过程。
5.1 准备数据张量和数据加载器
PyTorch需要将数据转换为Tensor格式。我们已经有pandas DataFrame/numpy形式的train_X, train_y, X_val, y_val, 以及最终测试的test_X。我们将其转换成PyTorch的张量。
注意:PyTorch的tensor默认是float32 for floats,int64 for ints。我们的特征主要是float (标准化之后的Age, Fare等)或0/1 (也可视作float), 标签y是0/1 int。我们需要把标签转换为LongTensor(int64)以用于损失函数。
import torch
# 将数据转成tensor
X_train_tensor = torch.tensor(X_train_sub.values, dtype=torch.float32)
y_train_tensor = torch.tensor(y_train_sub.values, dtype=torch.long)
X_val_tensor = torch.tensor(X_val.values, dtype=torch.float32)
y_val_tensor = torch.tensor(y_val.values, dtype=torch.long)
这里我们用 .values 转换成 numpy array,然后再tensor。也可以用torch.from_numpy(np.array())再指定类型。
接下来,我们创建数据集 (Dataset) 和数据加载器 (DataLoader) 用于批训练:
from torch.utils.data import TensorDataset, DataLoader
# 创建TensorDataset
train_dataset = TensorDataset(X_train_tensor, y_train_tensor)
val_dataset = TensorDataset(X_val_tensor, y_val_tensor)
# 创建DataLoader
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=64, shuffle=False)
我们使用batch_size=32对训练数据做shuffle随机打乱后分批;验证集载入时不需要shuffle(不影响评估结果但避免顺序变化)。
5.2 定义网络结构
现在我们定义多层感知机模型。我们需要先知道输入维度(即特征数)。可以通过 train_X.shape1 获取。
input_size = train_X.shape1
print("Input feature dimension:", input_size)
假设输出 Input feature dimension: 29(根据之前推测特征数,大概二三十)。
我们设计一个包含两个隐藏层的全连接网络,并使用ReLU激活函数,以及Dropout层来缓解过拟合。架构比如:
- 输入层 -> 隐藏层1(例如64个神经元) -> ReLU -> Dropout(0.5)
- 隐藏层2(例如32个神经元) -> ReLU -> Dropout(0.5)
- 输出层 -> 2个输出(或者1个输出)
这里有两种实现终层的思路:
- 输出单个神经元,用Sigmoid激活,直接表示生存概率。然后用二元交叉熵损失(BCE)。
- 输出2个神经元,分别表示类0和类1的原始得分,使用softmax变为概率,再用交叉熵损失(CrossEntropyLoss)。
第二种在PyTorch中可以用CrossEntropyLoss直接处理,它内部会对未softmax的logits计算softmax并算交叉熵损失。CrossEntropyLoss要求模型输出没有Sigmoid/Softmax的logits,标签用LongTensor表示类别索引。
我们采取第二种方式(输出2节点,用CrossEntropyLoss),因为这样实现简单并且自然可以扩展为多分类(虽然当前二分类,用CrossEntropyLoss也很合适)。
定义模型类:
import torch.nn as nn
class TitanicMLP(nn.Module):
def __init__(self, input_dim):
super(TitanicMLP, self).__init__()
self.net = nn.Sequential(
nn.Linear(input_dim, 64),
nn.BatchNorm1d(64),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(64, 32),
nn.BatchNorm1d(32),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(32, 2)
)
def forward(self, x):
return self.net(x)
这里我们使用了nn.Sequential快速搭建顺序网络,包含:
- 线性层64神经元 (Linear(input_dim,64))
- 批标准化 (BatchNorm1d) 对线性层输出做标准化,可稳定训练
- ReLU 激活
- Dropout(0.5) 以50%概率随机失活神经元,防止过拟合
- 线性层32神经元
- 批标准化
- ReLU
- Dropout(0.5)
- 输出层线性映射到2维(没有激活,在CrossEntropyLoss里会做Softmax)
BatchNorm在小批上可能效果有限,但也算正则手段之一,不妨一试。
注意:BatchNorm在很小批量时效果可能不稳定,不过32批应该尚可。若有问题可以去掉BN,依靠Dropout和早停防止过拟合。
创建模型实例和定义损失函数、优化器:
input_dim = X_train_sub.shape1
model = TitanicMLP(input_dim)
# 定义损失函数为交叉熵损失
criterion = nn.CrossEntropyLoss()
# 定义优化器,比如Adam
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
我们选择Adam优化器,学习率设为0.001(这是常用初始值)。我们可以后续尝试不同lr或调Scheduler。
5.3 训练循环
现在编写训练和验证循环。我们将进行若干个epoch的训练,每个epoch遍历训练集所有批次,然后计算在验证集上的指标以跟踪模型性能。
可以选择在CPU或GPU上训练。Titanic数据规模很小,CPU就够。但如果想利用GPU,PyTorch需要先model.to(device)和tensor.to(device)。我们可检查有没有GPU:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)
并在训练循环将tensor .to(device)。由于本例很小,我们可以都在CPU跑,也就无需写to(device)太繁琐。但我们写上结构以示规范:
# 将数据移到device (CPU/GPU)
X_train_tensor = X_train_tensor.to(device)
y_train_tensor = y_train_tensor.to(device)
X_val_tensor = X_val_tensor.to(device)
y_val_tensor = y_val_tensor.to(device)
model.to(device)
若上面使用DataLoader,就需要在迭代时对批数据x,y也to(device),见下。
实现训练epochs:
num_epochs = 100
best_val_acc = 0.0
best_model_state = None
for epoch in range(1, num_epochs+1):
model.train()
running_loss = 0.0
for batch_X, batch_y in train_loader:
batch_X, batch_y = batch_X.to(device), batch_y.to(device)
optimizer.zero_grad()
# 正向传播
logits = model(batch_X) # shape (batch_size, 2)
loss = criterion(logits, batch_y) # 计算损失,CrossEntropyLoss会内部做softmax
# 反向传播和优化
loss.backward()
optimizer.step()
running_loss += loss.item()
# 计算一个epoch的平均loss
epoch_loss = running_loss / len(train_loader)
# 每个epoch在验证集上评估
model.eval()
# 关闭梯度以加速
with torch.no_grad():
# 模型输出logits,取max得到预测
val_logits = model(X_val_tensor.to(device))
val_pred = torch.argmax(val_logits, dim=1)
val_loss = criterion(val_logits, y_val_tensor.to(device)).item()
# 计算准确率
val_acc = (val_pred.cpu() == y_val_tensor.cpu()).float().mean().item()
# 保存最佳模型
if val_acc > best_val_acc:
best_val_acc = val_acc
best_model_state = model.state_dict()
if epoch % 10 == 0 or epoch == 1:
print(f"Epoch {epoch:03d}: Train Loss = {epoch_loss:.4f}, Val Loss = {val_loss:.4f}, Val Acc = {val_acc:.4f}")
我们设训练100轮(epoch)。每轮:
- model.train()开启训练模式(启用Dropout/BN)。
- 遍历train_loader,小批训练,累计loss。
- model.eval()切换评估模式(关闭Dropout/BN的统计),计算验证集loss和accuracy。
- 记录并打印。
- 保存当前最佳模型参数。
在Titanic数据上,很可能不到100 epoch就会收敛甚至过拟合,所以我们应该关注val表现变化。
我们也可以加入早停(early stopping)机制:例如连续N个epoch验证集没提升就停止。不过由于epoch不多,我们可以手动看然后停止,这里简化不实现。
输出训练过程例如:
Epoch 001: Train Loss = 0.6205, Val Loss = 0.4830, Val Acc = 0.8101
Epoch 010: Train Loss = 0.4512, Val Loss = 0.4517, Val Acc = 0.8212
Epoch 020: Train Loss = 0.4250, Val Loss = 0.4470, Val Acc = 0.8268
Epoch 030: Train Loss = 0.4123, Val Loss = 0.4405, Val Acc = 0.8324
Epoch 040: Train Loss = 0.4030, Val Loss = 0.4386, Val Acc = 0.8324
Epoch 050: Train Loss = 0.3965, Val Loss = 0.4380, Val Acc = 0.8268
Epoch 060: Train Loss = 0.3891, Val Loss = 0.4405, Val Acc = 0.8268
...
Epoch 100: Train Loss = 0.3648, Val Loss = 0.4602, Val Acc = 0.8101
(该输出为虚构示例,但大致可能的情况。)
可以看到:
- Epoch1后Val Acc=0.8101,已经不错(由于初始网络有随机性能会抖动)。
- 在epoch30左右Val Acc达到0.8324最高,之后没提升甚至略降。
- Train Loss持续下降,但Val Loss最低在epoch40附近,然后略上升,Val Acc停滞/下降。这说明模型开始过拟合训练集。
- 所以训练到epoch30-40即可停止以免过拟合严重。
最佳Val Acc在epoch30/40约0.8324,对应Val Loss ~0.438。已经比逻辑回归/随机森林的0.8156还高了一点。这个结果说明我们的神经网络学到了类似或更好的模式。当然,每次训练结果会有些波动,可多尝试随机种子或调网络结构。
早停:我们可以根据best_val_acc记录,在适当epoch就停止训练以节省时间。比如如果10个epoch没提升,可以break。这里不深入,只选最终best_model_state用了哪个epoch的参数。
5.4 模型评估与过拟合讨论
让我们加载最佳模型参数并在验证集评估其最终指标:
# 加载最佳模型参数
if best_model_state is not None:
model.load_state_dict(best_model_state)
model.eval()
with torch.no_grad():
val_logits = model(X_val_tensor.to(device))
val_pred = torch.argmax(val_logits, dim=1).cpu().numpy()
y_val_np = y_val_tensor.cpu().numpy()
计算准确率、混淆矩阵、分类报告:
from sklearn.metrics import accuracy_score, confusion_matrix, classification_report
val_accuracy = accuracy_score(y_val_np, val_pred)
print("Final Validation Accuracy: {:.4f}".format(val_accuracy))
print("Confusion Matrix:")
print(confusion_matrix(y_val_np, val_pred))
print("Classification Report:")
print(classification_report(y_val_np, val_pred, digits=4))
假设输出:
Final Validation Accuracy: 0.8324
Confusion Matrix:
[95 8
22 54]
Classification Report:
precision recall f1-score support
0 0.8125 0.9223 0.8644 103
1 0.8709 0.7105 0.7826 76
accuracy 0.8324 179
macro avg 0.8417 0.8164 0.8235 179
weighted avg 0.8372 0.8324 0.8295 179
这里,Val准确率83.24%,高于之前随机森林81.56%。Precision(幸存预测的准确率)87.09%,Recall(幸存实际识别率)71.05%,比逻辑回归(Precision 86.21, Recall 65.79)更好,在Precision略增同时Recall提高5个百分点,F1提升。说明我们的MLP模型性能略胜基线模型,至少在验证集如此。
混淆矩阵对比如逻辑回归(之前: FP=8,FN=26):
- 现在FP=8 (遇难错判幸存8人)跟逻辑回归一样;
- FN=22 (幸存错判遇难22人)少于逻辑回归的26,说明召回提高;
- 所以总体准确率提升。
过拟合:从训练loss一直降而验证loss后来回升可以看出一定过拟合。我们已经用Dropout(0.5)和早停来应对。还可以减小网络容量(比如64-32改成32-16)或者增加正则(L2 weight_decay)。Given结果已达到83%+还可以接受,不做更多调整。如果训练更久,网络可能对训练集学到100%而验证集不增反降。
调整网络和超参数:如果上述结果不理想,可以:
- 改网络大小(更多/更少层和神经元)。
- 更改Dropout比例或去除BatchNorm。
- 调学习率:过大不稳定,过小收敛慢,可尝试0.01, 0.0005等。
- 使用学习率调度(learning rate scheduler)让lr逐步降低。
- 增加训练epoch,但要配合早停。
- 甚至尝试更高级的网络结构/激活等,不过对于表格数据,MLP已经够用。
由于训练集小,我们需要避免过度拟合,在验证集达到最佳就停止,已经做到。注意Kaggle上的评测是对测试集,与验证集分布相同但不同具体样本,所以一个泛化良好的模型应该在测试集也有类似表现。我们期待我们选的最佳模型对测试集有约0.82左右准确率。
6. 模型优化与调优
上一节我们构建了多个模型,并比较性能。随机森林和神经网络表现较好。理论上,我们可以通过超参数调优和模型融合进一步提升成绩。
6.1 超参数调优
超参数是指模型训练前需设定的参数,如:
- 对于逻辑回归:正则化强度C、迭代收敛准则等。
- 对于决策树:最大深度、最小样本叶节点数、分裂标准等。
- 对于随机森林:树的数量n_estimators、每棵树的深度、特征采样比例等。
- 对于神经网络:网络结构(层数、每层神经元数)、学习率、正则项(如weight decay)、激活函数、优化算法等等。
调参方法:
- Grid Search(网格搜索):设置若干备选值组合,遍历尝试每种组合,用交叉验证选出最佳。但组合多时计算成本高。
- Random Search(随机搜索):在参数空间中随机采样若干组合测试,比grid更高效在高维空间。
- Bayesian Optimization(贝叶斯优化)或其他AutoML方法:更智能地探索参数空间,收敛更快。
- 人工调试:根据验证集表现、原理和经验逐步调整。
在Titanic这个任务上,参数空间不算大,可以使用GridSearchCV对比如随机森林的参数调一调。如我们可以调max_depth和n_estimators:
from sklearn.model_selection import GridSearchCV
param_grid = {
'n_estimators': [50, 100, 200],
'max_depth': [3, 5, 7, None],
'min_samples_leaf': [1, 3, 5]
}
grid_search = GridSearchCV(RandomForestClassifier(random_state=42), param_grid, cv=5, scoring='accuracy')
grid_search.fit(train_X, train_y)
print("Best params:", grid_search.best_params_)
print("Best CV score:", grid_search.best_score_)
这会5折交叉验证不同参数组合。比如可能输出:
Best params: {'max_depth': 5, 'min_samples_leaf': 3, 'n_estimators': 100}
Best CV score: 0.8301
表示5折CV平均准确率83.01%在参数max_depth=5, min_samples_leaf=3, n_estimators=100达到最大。可以据此训练最终RF模型。
对于神经网络,调参可以手动尝试不同结构。AutoML调神经网络结构比较复杂且耗时,这里不深入。我们可以靠验证集观察选择稍好结构/epoch。
6.2 交叉验证与模型融合
交叉验证:上面GridSearchCV就是一种k折交叉验证。交叉验证的好处是用数据更充分(每个样本都作为验证一次),评估更稳定。缺点是需要训练k次模型。
在我们最后做预测前,也可以考虑用K折交叉验证训练多个模型然后融合:
- 将训练集分成k折,每折轮流做验证训练k个模型,然后对测试集取平均或投票。这会利用训练集更多信息,且缓解单一模型偶然波动,提高鲁棒性。
- 例如我们可以训练5个神经网络(或随机森林),每个用4/5数据训练,然后对测试集每个样本预测5次取平均(probability)然后0.5阈值分类或多数投票。
模型融合:融合不同模型的思路:
- 简单平均/投票:对多个模型的预测概率平均,再决定最终类别;或者直接投票看哪个类别票多。适合性能相近的模型。
- 加权融合:给较优模型赋予更大权重。
- 堆叠 (Stacking):利用一级模型的预测作为新特征,再训练一个二级模型(比如逻辑回归)进行最终预测。这种经常能提高性能,但实现稍复杂,要注意避免信息泄漏(需要多折生成一级模型预测)。
对于Titanic这样的小数据,融合能提高一些但是幅度有限。也要防止融合过多复杂模型反而过拟合。作为展示,我们可以简单平均两个模型:随机森林和神经网络的预测,以期望综合它们各自的长处。
举例:
# 让rf_clf和model对验证集和测试集都预测概率,然后平均
rf_val_proba = rf_clf.predict_proba(X_val)[:,1]
nn_val_proba = torch.softmax(model(X_val_tensor.to(device)), dim=1):,1.cpu().numpy()
ensemble_val_proba = (rf_val_proba + nn_val_proba) / 2
ensemble_val_pred = (ensemble_val_proba >= 0.5).astype(int)
print("Ensemble Val Acc:", accuracy_score(y_val, ensemble_val_pred))
print("Ensemble Confusion:\n", confusion_matrix(y_val, ensemble_val_pred))
类似可以对test:
rf_test_proba = rf_clf.predict_proba(test_X):,1
nn_test_proba = torch.softmax(model(torch.tensor(test_X.values, dtype=torch.float32).to(device)), dim=1):,1.cpu().numpy()
ensemble_test_pred = (rf_test_proba + nn_test_proba) / 2
ensemble_test_pred = (ensemble_test_pred >= 0.5).astype(int)
融合通常可以略提高准确率或平衡Precision/Recall。比如Val上rf81.56%acc, nn83.24%acc, 融合可能达到84%(纯猜测)。但是需要注意,如果一个模型犯的错误另一个也犯,那融合无益。还需看具体互补性。
总结:通过超参数调优和模型融合,我们可以将模型性能进一步逼近其上限。在Kaggle Titanic这个入门题上,最高分一般在0.84左右徘徊(因为数据本身信息有限,无法100%准确)。使用比如集成多个模型(RF+GBDT+NN)并叠加一些复杂特征,可以达到0.83~0.85不等。作为入门练习,我们得到80%+已经很好了。
7. Kaggle 提交流程
现在我们选择一个最终模型来对测试集的418名乘客进行生存预测,并按照Kaggle要求生成提交文件(CSV)。
我们可以选用上面表现较好的神经网络模型作为最终模型。由于其结果略优,我们就用它来预测test。也可以考虑将训练集全部数据来重新训练模型,以充分利用数据提高效果。然而,由于模型可能过拟合891个样本且没有额外验证数据防止,这有点冒险。但通常最终提交都会用全训练数据建模,因为测试集真正结果未知,倾向用更多数据提升泛化。
折中:我们可以将之前train_sub+val一起用来训练一个新的模型(相当于用891全训练),训练epoch数目通过前面经验减少以防过拟合太多。或者直接用我们已经训练的模型(因为已经用到验证了,但也OK)。
为了稳妥,我们在原训练集(891条)上重新训练我们的MLP模型若干epoch,但通过观察防止过拟合严重。如果怕过拟合,也可以不要太多epoch或稍调dropout强一点。
也可以简单地:使用之前保存的最佳模型(它在712子集上训练,179验证未参与训练,所以没有见过那179样本的标签,不过那179样本其实我们有标签,可以用… 这里不好说最佳,还是全数据重训吧)。
让我们重训网络:
# 用全部训练数据(891)重新训练模型若干epoch
full_X_tensor = torch.tensor(train_X.values, dtype=torch.float32)
full_y_tensor = torch.tensor(train_y.values, dtype=torch.long)
full_dataset = TensorDataset(full_X_tensor, full_y_tensor)
full_loader = DataLoader(full_dataset, batch_size=32, shuffle=True)
model_full = TitanicMLP(input_dim)
model_full.to(device)
optimizer_full = torch.optim.Adam(model_full.parameters(), lr=0.001)
num_epochs_full = 30 # 训练30轮观察
model_full.train()
for epoch in range(1, num_epochs_full+1):
for batch_X, batch_y in full_loader:
batch_X, batch_y = batch_X.to(device), batch_y.to(device)
optimizer_full.zero_grad()
logits = model_full(batch_X)
loss = criterion(logits, batch_y)
loss.backward()
optimizer_full.step()
# 简单来看一下loss下降
if epoch % 10 == 0 or epoch == 1:
with torch.no_grad():
train_loss = criterion(model_full(full_X_tensor.to(device)), full_y_tensor.to(device)).item()
print(f"Epoch {epoch}: Full train loss = {train_loss:.4f}")
(我们不划验证,所以只能监控训练loss。)
假设loss在30 epoch后下降稳定。避免过拟合一般可以少跑些,或拉大dropout。由于我们无法验证,只能凭经验。我们观察loss几乎到底没有大变化就可以停止。比如看到Epoch30 loss=0.38,相比之前712训练loss0.36略高,还行。
接下来,用这个模型对test集预测:
model_full.eval()
with torch.no_grad():
test_tensor = torch.tensor(test_X.values, dtype=torch.float32).to(device)
test_logits = model_full(test_tensor)
test_pred = torch.argmax(test_logits, dim=1).cpu().numpy()
现在test_pred是0/1数组(长度418)对应每个测试乘客的预测生存状况。
生成提交文件:
Kaggle要求两列:PassengerId 和 Survived。
我们之前保存了test_ids (418个ID对应test.csv顺序),利用它和预测:
submission_df = pd.DataFrame({
'PassengerId': test_ids,
'Survived': test_pred
})
submission_df.to_csv('submission.csv', index=False)
print("Submission file saved!")
这样就会在当前目录生成submission.csv。打开看下前几行:
PassengerId,Survived
892,0
893,1
894,0
... (共418行)
这文件即可用于提交。要提交的话,在Kaggle比赛Titanic页面,进入Submit Predictions页面上传这个CSV即可。Kaggle系统会评分并给出Public Leaderboard上的准确率。根据我们模型表现,期望得分大概在0.80~0.83之间。
7.1 提交结果与查看分数
假如我们提交并得到准确率,比如0.83253(这相当不错)。当然每次略有波动,低的话可能0.79, 高可能0.84。这个score就是模型在测试集的准确率百分比。Public LB显示大约前几百名能到0.83-0.85,最高可能0.89(有人用集成和泄漏技巧达到,但超过0.88很少见)。
在Leaderboard上,我们可以看到自己的名次。Titanic是知识竞赛,没有奖金,但上榜可以作为学习成果。
值得注意的是:Kaggle通常会将测试集分为Public和Private两部分,在比赛结束前你的成绩排名用Public部分计算,最终排名看Private。但因为Titanic长期开放,通常显示Public成绩足矣。
7.2 结果分析和可能改进
我们模型得分若在0.83左右,已算相当不错,进入Top10%没问题(Titanic有大量提交很多人78%上下)。
如果想进一步提升,可以考虑:
- 更多特征工程:Titanic有不少小trick特征,比如:
- 从Ticket推导家庭或团体:若同票号多人也许生存命运关联。可提取每张票的同组人数作为特征。
- 从姓名进一步提取姓氏:家庭姓氏相同的人可能命运相近?或者有些姓氏整个家族都没幸存说明大灾。
- 船舱号Deck我们没深入利用,或许不同甲板A/B/C vs D/E vs F/G幸存率不同,可单独建模。其实我们有Deck one-hot,但数据少信息有限。
- 年龄和性别交互:比如构造一个特征
Child*Female看看是否小女孩特别容易幸存?事实上儿童中性别不大区分,皆优待。 - 船员与乘客:乘客ID有些范围或Ticket带字符串可以区分船员?Titanic数据未明确给出船员,只乘客表,所以不考虑。
- 尝试XGBoost / LightGBM模型:这些提升树模型常取得比RF更好的效果。很多Kaggleer在Titanic用XGBoost能达0.84左右。
- 模型Stacking:用逻辑回归+RF+GBDT+NN的预测作为输入,再训练元模型。可能涨一点。
但是作为入门项目,以上我们做的已经覆盖了典型流程,大部分初赛选手也会采用类似方案。进一步提升虽然有意义,但边际收益变小。
8. 拓展内容
本节讨论如何在不同环境下运行以上代码,以及确保项目的复现性和代码管理。
8.1 使用 Google Colab 运行代码
Google Colab是谷歌提供的免费云端Jupyter笔记本环境,带有免费GPU,加之方便访问Google Drive,非常适合尝试本项目,尤其神经网络训练部分可以用GPU加速(不过我们的MLP很小,用CPU也无碍)。
要在Colab上运行本项目,可按照以下步骤:
- 打开Colab并创建一个新Notebook。
- 准备数据:可以通过几种方式把Kaggle的train.csv和test.csv弄到Colab环境:
- 最方便:使用Kaggle API下载。需要先在Kaggle账户设置里创建API Token,把kaggle.json上传到Colab,然后运行
!pip install kaggle和使用!kaggle competitions download -c titanic命令下载数据集ZIP,再!unzip解压出train.csv等。 - 或者:手动从Kaggle下载csv文件到本地,再在Colab左侧栏>文件,上传train.csv和test.csv文件(注意每次启动需要重新上传,或挂载Google Drive存储)。
- 还可以使用Google Drive:将数据存到自己云盘,然后在Colab用
from google.colab import drive; drive.mount('/content/drive')挂载,再读文件路径'/content/drive/MyDrive/...'。
- 最方便:使用Kaggle API下载。需要先在Kaggle账户设置里创建API Token,把kaggle.json上传到Colab,然后运行
- 安装需要的库:Colab预装了numpy, pandas, sklearn, pytorch等常见库,一般可直接使用。如没有则用
!pip install 包名。 - 将我们本文包含的代码按顺序分成几个单元格复制粘贴到Colab中执行。比如一节节执行数据加载/EDA/预处理/建模部分。注意不要一次性跑完,按逻辑分块便于调试。
- 训练PyTorch模型时,可以利用GPU:在Colab菜单 Runtime > Change runtime type > Hardware accelerator选GPU,然后我们的代码
device = 'cuda' if available就会用GPU跑。对于MLP训练100个epoch在这么小数据上CPU几十毫秒,GPU其实意义不大,不过习惯上可以练习一下 GPU 使用。 - 监控输出结果:Colab会显示print输出,可以根据我们在代码中的print调参。如果需要,可插入更多print或progress输出了解训练动态。
- 生成submission.csv:可以用
files.download('submission.csv')在Colab下载文件到本地,或保存到Google Drive以备提交。 - 将模型训练过程和结果记录下来,以便写报告或分享。
Colab提供了无配置环境和免费算力,但要注意:
- 会话存储:Colab的VM不是持久的,90分钟不活动或12小时强制重启,一些文件会丢失。如果需要长时间/多日工作,记得保存notebook到Google Drive,并定期备份重要输出(如模型、数据)到Drive。
- 版本:Colab的PyTorch版本可能更新,如果本文代码有任何由于版本导致的问题(比如某些API变化),可切换PyTorch版本或调整代码。
- 依赖:确保运行前import所需的库都安装available,否则pip安装。
8.2 版本控制与复现最佳实践
在真实的工作中,良好的版本控制和实验记录对团队合作和项目复现非常重要。以下是一些建议:
- 版本控制(Git):使用Git管理代码。可以把本项目整理为一个Git仓库,commit每次关键修改(比如数据处理改变、模型结构改变)并写清commit message。这样可以回溯任意版本,比较不同尝试的效果。
- 代码组织:将不同功能模块化,例如:data_processing.py(数据读取与预处理函数)、model.py(模型定义)、train.py(训练和验证循环)、inference.py(预测生成提交)等。主程序可调用这些模块,使代码更简洁易懂,也方便分别调试。
- 配置文件:用配置文件或命令行参数管理超参数,而不是把数字写死在代码里。比如使用
argparse或创建一个 dict/namespace 存储配置(learning_rate, batch_size, hidden_size等)。这样尝试不同超参数只需改配置,不改代码逻辑,且可以保存不同配置对应的结果记录。 - 实验记录:每次跑实验,记下日期、使用的参数、模型、结果。可以简单写在README或使用专门工具(如Weights & Biases, MLflow)跟踪。尤其当实验多了容易混淆,通过记录可以快速查询先前结果避免重复工作。
- 随机种子:设置随机种子保证可重复性。我们在sklearn和torch中都可以设置seed。例如:
这样每次运行能得到一致结果(不过涉及Dropout、GPU并行等结果也可能有微小差异或需额外设置CUDNN deterministic)。np.random.seed(42) torch.manual_seed(42) random.seed(42) - 依赖环境:记录所用的Python和库版本。例如将
pip freeze输出保存,或用conda导出环境.yml文件。以免日后版本升级导致代码不兼容。复现实验需要一致环境。 - 持续集成:如果项目较大,可以考虑加一些测试用例,确保数据处理和模型预测部分输出维度等符合预期,防止改动引入bug。也可使用GitHub Actions等设置持续集成,在推送代码时跑一下关键流程。
- 模型和结果保存:训练好的模型(比如PyTorch state_dict)和产生的关键中间结果(如特征文件、处理后的数据)可以保存下来(如保存在 disk 或云存储),以便后续直接加载使用,而不必每次重头处理和训练。这在数据或模型大的情况下尤其重要(本项目数据小无所谓)。
- 文档:给代码写适当注释和文档说明,让团队其他人或将来的自己容易理解思路和流程。比如README描述如何运行项目,预期输入输出是什么。
- 隐私:注意不要把像kaggle API token、个人秘钥等写入代码或提交repo。这些最好通过环境变量或配置文件读取,且配置文件不要纳入版本控制。
通过这些实践,我们可以更方便地分享和复现本项目。例如,把代码和说明上传到GitHub,别人可以按照README一步步安装环境、运行脚本,再现我们的结果,这也是Kaggle社区常见的知识分享方式(很多优秀的Titanic解决方案都在GitHub或Kaggle Notebook公开)。
总结:
本教程从比赛背景、数据理解开始,带领大家经历了完整的Titanic幸存预测项目流程。我们进行了详尽的探索性数据分析,挖掘出关键特征(性别、舱等、年龄、家庭、头衔等)对生存率的影响;通过数据清洗和特征工程,将原始数据转化为适合建模的格式,包括填补缺失值、创造新特征以及标准化处理。然后,我们构建了多个基线模型,比较了它们的性能,进而使用PyTorch搭建了深度学习模型来尝试提升预测准确率。我们探讨了模型调优和融合的方式来进一步改进成绩,并最终生成了可以在Kaggle提交的预测结果文件。整个过程中强调了代码可复现和良好实践,包括如何使用Colab运行、如何进行版本控制和实验管理等。
通过这个项目,相信读者不仅对泰坦尼克号数据有了深入了解,更掌握了机器学习项目的一般套路:从理解问题->探索数据->特征工程->模型训练->调优->预测部署的闭环流程。在实际工作中,这样的套路同样适用。希望您能将所学应用到其他数据科学任务中,持续优化,不断挑战更复杂的比赛和问题。祝您在Kaggle竞赛和机器学习学习之路上乘风破浪、再创佳绩!
更多推荐



所有评论(0)