当前位置: 首页 > news >正文

Kaggle 经典竞赛泰坦尼克号:超级无敌爆炸详细基础逐行讲解Pytorch实现代码,看完保证你也会!!!

讲解代码分为3个步骤:有什么用,为什么需要他,如何使用

保证大家耐心看完一定大有裨益!如果有懂的可以跳过,不过建议可以看完,查漏补缺嘛。

现在开始吧!

项目目标

我们的目标是根据泰坦尼克号乘客的个人信息(如年龄、性别、船舱等级等),预测他是否能在海难中幸存下来。这是一个典型的二元分类问题。现在我们开始吧,这是原版测试集和训练集的下载地址:
链接: https://pan.baidu.com/s/1zc1dpc6Xm-BzHbeMrp4oSw?pwd=6688 提取码: 6688 

由于原版训练测试集有很多数据遗漏,所以我们要对其梳理一下。

第一步,导入必要库与加载数据

import pandas as pd
import numpy as np# 加载数据
train_df = pd.read_csv('train.csv')
test_df = pd.read_csv('test.csv')# 打印数据信息,快速了解数据
print("训练数据信息:")
train_df.info()
print("\n测试数据信息:")
test_df.info()print("\n训练数据前5行:")
print(train_df.head())

1.pd.read_csv('train.csv'):

有什么用:

它的作用是读取一个CSV(逗号分隔值)文件,并将其内容加载到一个 pandas DataFrame 对象中。在这里,代码分别读取了 train.csvtest.csv 两个文件,并把它们存储在名为 train_dftest_df 的变量里。dfDataFrame 的常用缩写。

为什么要用它:

这是数据加载的核心步骤。没有这一步,我们的程序就无法访问和使用存储在文件中的训练数据和测试数据。在机器学习中,我们通常会将数据分成“训练集”(用于训练模型)和“测试集”(用于评估模型在未见过数据上的表现),所以这里会加载两个文件。

使用语法:

  • pandas.read_csv(filepath_or_buffer)

  • 最主要的参数就是文件的路径。这里 'train.csv' 表示文件就在当前代码运行的目录下。

  • 这个函数还有很多可选参数,例如,如果你的数据不是用逗号分隔的,可以用 sep 参数指定分隔符,如 pd.read_csv('data.txt', sep='\t') 用于读取用Tab分隔的文件。

2.train_df.info():

有什么用:

这是一个非常方便的函数,用于快速获取 DataFrame 的一个简洁摘要。

为什么需要:

在真正开始处理数据前,我们需要对数据有一个宏观的了解。.info() 方法能立刻告诉我们以下关键信息:

  1. 数据有多少行、多少列:了解数据规模。

  2. 每一列的名称是什么

  3. 每一列有多少个非空值:这是发现“缺失数据”最快的方法。如果“非空值”数量少于总行数,就说明这一列有数据缺失。

  4. 每一列的数据类型Dtype):比如是数字(int64, float64)、文本(object)还是其他类型。这对于后续的数据预处理至关重要。

  • 使用语法

    • 它是一个 DataFrame 对象的方法,直接在变量名后调用即可,不需要参数。

    • dataframe_variable.info()

3.train_df.head():

有什么用:

这个函数用于查看 DataFrame 的前几行数据,默认是前5行。

为什么需要它?

.info() 给了我们数据的“骨架”信息,而 .head() 则让我们能亲眼看到数据的内容长什么样。通过查看真实的数据样本,我们可以直观地了解每一列的数值范围、文本格式等,对数据建立一个具象的认识。如果数据集有几百万行,你不可能把它全部打印出来看,所以看头几行和尾几行(用.tail())是最高效的方式。

使用语法

  • dataframe_variable.head(n)

  • n 是你想查看的行数,是可选参数。如果省略,默认为5。

  • 例如,train_df.head(10) 就会显示前10行数据。

运行结果:

Age, Cabin 有大量缺失值。Embarked 在训练集中有少量缺失。

Sex, Embarked 是文本类别,需要转换为数字。

PassengerId, Name, Ticket 对预测可能用处不大或者处理起来太复杂,我们初期可以先舍弃。Cabin 缺失太多,也先舍弃。

第二步,数据梳理准备

# 为了方便,我们将训练集和测试集合并处理,最后再分开
# 保存测试集的PassengerId用于最后提交
test_passenger_ids = test_df['PassengerId']# 我们只挑选部分特征开始
features = ['Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare', 'Embarked']# 合并数据
all_df = pd.concat([train_df[features], test_df[features]], axis=0).reset_index(drop=True)# 1. 处理缺失值
# Age: 使用中位数填充,因为它对异常值不敏感
all_df['Age'] = all_df['Age'].fillna(all_df['Age'].median())
# Fare: 同样使用中位数填充
all_df['Fare'] = all_df['Fare'].fillna(all_df['Fare'].median())
# Embarked: 使用众数(出现次数最多的值)填充
all_df['Embarked'] = all_df['Embarked'].fillna(all_df['Embarked'].mode()[0])
print("\n处理完缺失值后的数据信息:")
all_df.info()

1. test_passenger_ids = test_df['PassengerId']

  • 有什么用?

    • 这行代码从测试集 test_df 中提取出 'PassengerId' 这一列,并将其保存到一个新的变量 test_passenger_ids 中。

  • 为什么需要它?

    • 在很多机器学习任务(尤其是像Kaggle竞赛)中,PassengerId 这种ID列是用来唯一标识每一行数据的,但它本身不包含任何可供模型学习的“特征”信息(乘客的ID号与他能否生还无关)。因此,我们不应该把它作为特征喂给模型去训练。

    • 但它又非常重要。当我们用模型对测试集 test_df 做出预测后,我们需要将预测结果和每个乘客对应起来,才能生成最终的提交文件。所以,我们在这里提前把它单独存起来,等所有处理和预测都完成后,再用它来和预测结果配对。

  • 使用语法

    • dataframe['column_name']

    • 这是 pandas 中选择单个列的标准方法。test_df['PassengerId'] 会返回一个 pandas Series 对象,里面包含了所有测试集乘客的ID。

2. features = ['Pclass', 'Sex', 'Age', ...]

  • 有什么用?

    • 这行代码创建了一个Python列表(list),其中包含了我们初步决定要用来训练模型的特征列的名称。这个过程叫做特征选择(Feature Selection)

  • 为什么需要它?

    • 一个原始数据表里可能有很多列,但并非所有列都对我们的预测目标(比如,预测乘客是否生还)有用。

    1. 排除无关特征:像 PassengerIdName (乘客姓名)通常被认为是无关特征。

    2. 简化模型:选择一个特征子集可以使我们的代码更清晰,模型更简单,训练速度更快。

    3. 方便管理:把所有要用的特征名放在一个列表里,后续可以很方便地从DataFrame中一次性把它们都选出来,也便于未来增删特征做实验。

  • 使用语法

    • 这是标准的Python列表定义语法:list_name = [element1, element2, ...]

3. all_df = pd.concat([...]).reset_index(drop=True)

这一行做了两件大事:concatreset_index。我们分开看。

pd.concat([train_df[features], test_df[features]], axis=0)

有什么用?

  • 将训练集和测试集中我们选定的特征列(features 列表中的那些列)合并成一个大的DataFrame all_df

为什么需要它?

  • 为了确保预处理的一致性。训练集和测试集常常需要进行相同的预处理操作,比如:

    • 填充缺失值Age 列可能有缺失值。我们通常会用所有数据(训练集+测试集)的年龄平均值或中位数来填充,这样更准确。

    • 编码分类变量Sex 列是'male'/'female'这样的文本,需要转换成0/1这样的数字。Embarked 列也是同理。我们必须保证在训练集和测试集中,相同的文本被转换成相同的数字。

  • 将它们合并后,我们只需要对 all_df 这一个DataFrame进行预处理,就可以保证操作的统一性,避免了对训练集和测试集重复写同样的代码,减少了出错的可能。处理完毕后,我们再将它们拆分回训练集和测试集。

使用语法

  • pd.concat(objs, axis=0)

  • objs: 一个列表,包含了要合并的DataFrame对象。这里是 [train_df[features], test_df[features]]。注意 train_df[features] 表示从train_df中只选择features列表里包含的那些列。

  • axis=0: 这是关键参数axis=0 表示纵向合并(按行合并),也就是把第二个DataFrame接到第一个的下面,行数相加。这正是我们合并训练/测试集时想要的。如果用 axis=1,则是横向合并(按列合并)。

4.reset_index(drop=True)

  • 有什么用?

    • 为合并后的新DataFrame all_df 创建一个全新的、连续的索引。

  • 为什么需要它?

    • 当你用pd.concat合并两个DataFrame时,默认会保留它们各自原来的索引。比如,训练集索引是 0 到 890,测试集索引是 0 到 417。合并后,新的 all_df 的索引就会是 0, 1, ..., 890, 0, 1, ..., 417,存在大量重复。这会给后续的数据筛选和处理带来麻烦。

    • reset_index() 会生成一个从0开始的全新连续索引 (0, 1, 2, ... , 1307)。

    • 参数 drop=True 的作用是丢弃原来的旧索引。如果不加这个参数,原来的旧索引会被当作一个新列(列名为'index')保留下来,但我们通常不需要它,所以直接丢弃。

5.all_df['Age'] = all_df['Age'].fillna(all_df['Age'].median())

有什么用?

  • 这两行代码分别找到了Age(年龄)列和Fare(票价)列中所有的缺失值,并用这两列各自的中位数(median)来填充它们。

为什么需要它?(特别是,为什么用中位数?)

  • 必要性AgeFare是重要的数值特征,但它们存在缺失数据,必须填充。

  • 策略选择:对于数值型数据,常见的填充策略有使用平均值(mean)、中位数(median)或众数(mode)。

    • 平均值:计算简单,但容易被“异常值”(outliers)影响。例如,有几个乘客买了天价船票,这会把平均票价拉得很高,用这个偏高的平均值去填充缺失票价可能就不太合理。

    • 中位数:将所有数据排序后取中间的那个数。它最大的优点是对异常值不敏感(鲁棒性强)。即使有天价船票,中位数也不会受其影响,因此它往往能更好地代表数据的“一般水平”。正如代码注释所说,这是一个更稳妥的选择。

使用语法

  • 代码结构为:df['列名'] = df['列名'].fillna(要填充的值)

  • all_df['Age']: 选中Age这一列数据(这是一个Pandas Series)。

  • .median(): 这是Pandas Series的一个方法,调用它会计算出该列的中位数,返回一个数字。

  • .fillna(value): 找到该列中所有的NaN,并将它们替换为括号中提供的value

  • 整个流程是:先计算出Age列的中位数,然后用这个中位数去填充Age列中的所有NaN,最后将填充好的新列数据覆盖掉原来的Age列。Fare列同理。

6.all_df['Embarked'] = all_df['Embarked'].fillna(all_df['Embarked'].mode()[0])

有什么用?

  • 这行代码找到了Embarked(登船港口)列中的所有缺失值,并用该列的众数(mode)来填充。众数就是数据中出现次数最多的那个值。

为什么需要它?(特别是,为什么用众数?)

  • Embarked列是分类特征,它的值是文本(如'S', 'C', 'Q'),而不是数字。对于这种数据,我们无法计算平均值或中位数。

  • 最合乎逻辑的填充方法,就是用出现频率最高的值来填充。我们推断,缺失的值有最大的可能性是那个最常见的值。

使用语法

  • 语法结构和上面类似,但有一个关键点 [0]

  • .mode(): 这个方法计算列的众数。

  • 为什么是 .mode()[0] 因为一列数据的众数可能不止一个(比如'S'和'C'的出现次数完全相同且都是最高)。所以.mode()方法总是返回一个Pandas Series,里面包含一个或多个众数值。即便只有一个众数,它也依然被包在一个Series里。我们需要通过索引[0]来把第一个(通常也是唯一一个)众数值取出来,作为一个单独的值去填充。

经过这步处理后,all_df中这三列的“窟窿”就被补上了,数据变得更加完整,可以用于下一步的转换和建模。

最后的运行结果为:

# 2. 将分类特征转换为数值特征
# Sex: Male -> 0, Female -> 1
all_df['Sex'] = all_df['Sex'].map({'male': 0, 'female': 1})
# Embarked: 使用独热编码 (One-Hot Encoding)
all_df = pd.get_dummies(all_df, columns=['Embarked'], prefix='Embarked')print("\n转换分类特征后的数据前5行:")
print(all_df.head())

1.all_df['Sex'] = all_df['Sex'].map({'male': 0, 'female': 1})

有什么用?

  • 这行代码将Sex列中的文本值 'male' 替换为数字 0,'female' 替换为数字 1。这是一种手动实现的标签编码(Label Encoding)

为什么需要它?

  • Sex列是一个典型的二元分类特征(只有两种可能的类别)。对于这种情况,最简单直接的方法就是将两个类别分别映射到一个数字。这样,'male'和'female'这两个字符串就被转换成了模型可以处理的0和1。

使用语法

  • Series.map(字典)

  • all_df['Sex'] 选中 Sex 这一列数据。

  • .map() 是Pandas Series的一个方法,它可以接受一个字典作为参数。

  • {'male': 0, 'female': 1} 就是这个映射字典。.map方法会遍历Sex列中的每一个元素,如果在字典的“键”(key)中找到了该元素(比如'male'),就把它替换成字典里对应的“值”(value)(也就是0)。

  • 最后,将这个转换后的新Series赋值回all_df['Sex']列。

2.all_df = pd.get_dummies(all_df, columns=['Embarked'], prefix='Embarked')

有什么用?

  • 这行代码对Embarked(登船港口)列进行了独热编码(One-Hot Encoding)。它会做两件事:

1.移除原来的Embarked列。

2.根据Embarked列中所有不重复的值(比如'S', 'C', 'Q'),创建出对应数量的新列(Embarked_S, Embarked_C, Embarked_Q)。

对于每一行数据,如果原来的Embarked值是'S',那么在新列Embarked_S中,该行的值就为1,而在Embarked_CEmbarked_Q中则为0

为什么需要它?(特别是,为什么不用map方法映射成0, 1, 2?)

  • Embarked列是多元(无序)分类特征。它有三个类别'S', 'C', 'Q'。

  • 陷阱:如果我们像处理Sex列一样,简单地把它们映射成S=0, C=1, Q=2,模型可能会错误地理解它们之间存在某种顺序关系大小关系(比如认为 Q > C > S)。但实际上,这三个港口只是地点不同,没有顺序或大小之分。这种错误的假设会干扰模型的学习。

  • 独热编码的优势:它通过将一个特征拆分成多个“是/否”的二元特征来完美地解决了这个问题。每个新列代表“是否是这个港口?”。这样,特征之间就变得相互独立,模型不会再做出错误的顺序假设。这是处理无序多元分类特征的标准做法。

  • 使用语法

    • pd.get_dummies(data, columns, prefix)

    • data: 需要进行独热编码的整个DataFrame,这里是all_df

    • columns=['Embarked']: 一个列表,指定要对哪些列进行独热编码。

    • prefix='Embarked': 为新生成的列添加前缀。如果不指定,新列名可能就是'S', 'C', 'Q',可读性较差。加上前缀后,新列名会变成Embarked_S, Embarked_C, Embarked_Q,非常清晰。

    • 这个函数会返回一个全新的、已经完成了编码的DataFrame,我们再把它赋值给all_df

输出结果:

  • Sex列已经变成了0和1。

  • 原来的Embarked列消失了。

  • 数据表的末尾多了几个Embarked_开头的新列。

# 3. 特征缩放 (Feature Scaling)
# 神经网络对特征的尺度很敏感,我们使用标准化(Standardization)
from sklearn.preprocessing import StandardScaler# 需要缩放的列
cols_to_scale = ['Age', 'Fare', 'Pclass', 'SibSp', 'Parch']
scaler = StandardScaler()
all_df[cols_to_scale] = scaler.fit_transform(all_df[cols_to_scale])print("\n特征缩放后的数据前5行:")
print(all_df.head())

有什么用?

  • 这段代码的核心作用是对指定的数值特征列进行标准化(Standardization)

  • 标准化处理后,这些列的数据都会被转换成平均值为0,标准差为1的新数据。

为什么需要它?(这可能是最重要的一步)

  1. 不同特征的“量纲”差异巨大:观察我们的数据,Age的范围可能是0-80,Fare的范围可能是0-500+,而Pclass(舱位等级)只有1, 2, 3。这些特征的数值尺度(量纲)相差悬殊。

  2. 对神经网络训练的影响:神经网络的权重更新依赖于梯度下降算法。如果特征尺度差异很大:

    • 收敛速度变慢:尺度大的特征(如Fare)在计算损失和梯度时会占据主导地位,导致损失函数的“等高线图”变成一个又扁又长的椭圆形。模型优化的路径会像在一个狭长的山谷中来回震荡,而不是平稳地走向最低点,这会大大减慢模型的训练收敛速度。

    • 权重学习不公平:模型可能会错误地认为,数值范围更大的特征就更重要。通过缩放,我们将所有特征拉到了一个可比较的起跑线上,让模型能够根据特征真正的预测能力来学习其权重,而不是被它们的原始尺度所迷惑。

  3. 标准化 vs. 归一化

    • 标准化 (Standardization) (我们用的这种): 将数据处理成均值为0,标准差为1。它不把数据限制在特定范围内,对异常值的敏感度较低,是神经网络中最常用的缩放方法之一。

    • 归一化 (Normalization / Min-Max Scaling): 将数据缩放到一个固定的区间,通常是[0, 1]。

使用语法

  • cols_to_scale = [...]: 定义一个列表,清晰地指明哪些列需要被缩放。这里包含了我们所有的数值型特征(包括Pclass这种有序分类特征,我们也可以当数值特征来处理和缩放)。

  • scaler = StandardScaler(): 创建一个StandardScaler实例(对象)。此时,这个scaler对象还是“空的”,它不了解我们的数据。

  • scaler.fit_transform(...): 这是最核心的一步,它是一个复合操作:

    • fit() (拟合)scaler首先会“学习”我们提供的数据(all_df[cols_to_scale])。具体来说,它会计算出Age, Fare等每一列的平均值标准差,并把这些统计值保存在scaler对象内部。

    • transform() (转换):然后,scaler会使用刚刚学到的平均值和标准差,对每一列的每一个数据点应用标准化公式:新值 = (原值 - 平均值) / 标准差

    • fit_transform 将这两个步骤合并为一步,代码更简洁高效。它会返回一个包含所有转换后数据的新数组。

  • all_df[cols_to_scale] = ...: 将fit_transform返回的包含新值的数组,赋值回all_df中对应的列,完成对原始数据的替换。

输出结果:

# 4. 分离回训练集和测试集
train_processed_df = all_df[:len(train_df)]
test_processed_df = all_df[len(train_df):]
target = 'Survived'
y_train = train_df[target] # 训练集的标签
  • 有什么用?

    • 这两行代码将我们之前合并并处理好的 all_df,重新精确地拆分回处理后的训练集 train_processed_df 和处理后的测试集 test_processed_df

  • 为什么需要它?

    • 我们的策略是“合并处理,分离建模”。

    • 合并是为了确保对训练集和测试集应用完全一致的预处理规则(比如用同一个中位数填充,用同一个缩放器scaler)。

    • 分离是机器学习的基本原则。我们必须用训练集 (train_processed_df和其对应的标签y_train) 来训练模型,然后用训练好的模型去对测试集 (test_processed_df) 进行预测。测试集是模拟的“未来未知数据”,在训练过程中绝对不能让模型看到测试集的数据(除了在预处理阶段为了更准确的统计而“借用”了一下),否则就是“作弊”,会导致模型评估结果过于乐观且不真实。

  • 使用语法

    • 这里利用了Pandas DataFrame对Python切片(slicing)语法的支持,非常巧妙。

    • len(train_df): 我们获取了原始训练集train_df的行数。假设它有891行。

    • all_df[:len(train_df)]: 这句代码的意思是“从all_df的第0行开始,取到第891行(不包括第891行)”。因为我们当初是先把train_df放在前面进行合并的,所以这部分数据不多不少,正好就是属于原来训练集的那部分数据。

    • all_df[len(train_df):]: 这句代码的意思是“从all_df的第891行开始,一直取到最后一行”。这部分数据正好就是属于原来测试集的那部分。

结果:

现在,我们拥有了:

  • train_processed_df (X_train): 处理好的训练特征

  • y_train (y_train): 训练标签

  • test_processed_df (X_test): 处理好的测试特征

“万事俱备,只欠东风”,下一步就是将它们转换成PyTorch Tensor,搭建神经网络这个“东风”,来完成最终的训练和预测任务了!

我们先看看我们的训练集:

import torch
from torch.utils.data import Dataset, DataLoader, TensorDatasetprint(train_processed_df.info())

第三步,数据打包与搭建神经网络

# 直接转换为NumPy浮点数组 -> 再转PyTorch张量
X_np = train_processed_df.to_numpy(dtype=np.float32)  # 强制指定float32
X_train_tensor = torch.from_numpy(X_np)  # NumPy->PyTorch自动类型匹配# 标签处理(注意保持维度一致)
y_np = y_train.to_numpy(dtype=np.float32)
y_train_tensor = torch.from_numpy(y_np).view(-1, 1)  # 相当于unsqueeze(1)

1.train_processed_df.to_numpy(dtype=np.float32)

  • 有什么用?

    • 将我们处理好的train_processed_df这个Pandas DataFrame,转换成一个NumPy数组 X_np

  • 为什么需要它?

    1. PyTorch的亲密伙伴:PyTorch与NumPy的集成是无缝的。将数据从Pandas转换到PyTorch时,NumPy是最理想、最高效的中间桥梁。

    2. 强制指定np.float32:这是一个非常关键的细节。默认情况下,NumPy可能会创建float64(双精度)的数组。但在深度学习中,几乎所有的计算都是在float32(单精度)下进行的。这能在保证足够精度的情况下,将内存占用和计算量减半,尤其是在使用GPU时,性能提升非常明显。所以我们在这里强制指定数据类型为np.float32

  • 使用语法

    • .to_numpy(): 这是Pandas DataFrame或Series对象的方法,用于将其转换为NumPy数组。

    • dtype=np.float32: 是一个可选参数,用来指定转换后数组中元素的数据类型。

2.torch.from_numpy(X_np)

  • 有什么用?

    • 从NumPy数组 X_np 创建一个PyTorch张量 X_train_tensor。这正是我们的最终目的。

  • 为什么需要它?

    • PyTorch框架中的所有运算,无论是模型的前向传播还是反向传播,都必须在torch.Tensor对象上进行。没有这一步,数据就无法进入PyTorch的生态系统。

    • 一个重要的特性torch.from_numpy() 创建的张量和原始的NumPy数组共享内存。这意味着它们指向计算机内存中的同一块数据。这样做的好处是效率极高,因为它避免了复制数据的开销。但也要注意,如果后续修改了NumPy数组,那么对应的PyTorch张量也会跟着改变,反之亦然。在这个场景下,这完全没问题,因为我们只是做最后的转换。

  • 使用语法

    • torch.from_numpy(numpy_array): PyTorch的函数,输入一个NumPy数组,输出一个PyTorch张量。输出张量的数据类型会自动与输入的NumPy数组匹配(这也是我们上一步要指定float32的原因)。

3.torch.from_numpy(y_np).view(-1, 1):

  • 有什么用?

    • 将标签NumPy数组y_np转换为PyTorch张量,并且调整其形状(维度)

  • 为什么需要 .view(-1, 1)

    1. 原始形状torch.from_numpy(y_np)执行后,得到的张量是一个一维向量,形状是 [891] (假设有891个样本)。

    2. 模型输出的形状:我们的模型在进行二分类任务时,最后一层的输出通常是一个形状为 [批量大小, 1] 的二维张量。对于整个训练集,就是[891, 1]

    3. 维度匹配:在计算损失时,PyTorch需要模型输出和标签张量的形状能够匹配。如果一个是 [891, 1],另一个是 [891],可能会导致广播(broadcasting)错误或潜在的bug。

    4. 解决方案.view(-1, 1)的作用就是将形状从[891]强制“重塑”为[891, 1]。它把一个一维的“列表”变成了一个N行1列的“列向量”。这样就确保了我们的标签张量和模型输出张量的维度是完全一致的,可以让损失计算准确无误地进行。

  • 使用语法

    • tensor.view(shape): 是一个改变张量形状的方法。

    • view(-1, 1):这里的-1是一个占位符,意思是PyTorch根据总元素数量和另一个维度(这里是1)自动计算出这个维度的大小。所以 view(-1, 1) 就是告诉PyTorch:我们想要1列,行数你帮我算好。

    • 对于一维向量来说,.view(-1, 1).unsqueeze(1) 的效果是完全一样的,都是在最后增加一个维度。

现在,我们已经成功地将数据从最开始的CSV文件,一路过关斩将,变成了PyTorch可以直接使用的、格式干净、维度正确的张量:

  • X_train_tensor: 训练特征张量,形状为[样本数, 特征数]

  • y_train_tensor: 训练标签张量,形状为[样本数, 1]

这两份张量已经准备好,可以打包成DatasetDataLoader

# 2. 创建TensorDataset
# TensorDataset可以把特征和标签打包在一起
train_dataset = TensorDataset(X_train_tensor, y_train_tensor)# 3. 创建DataLoader
# DataLoader可以帮我们打乱数据、划分批次
BATCH_SIZE = 64
train_loader = DataLoader(dataset=train_dataset, batch_size=BATCH_SIZE, shuffle=True)

1.TensorDataset:将特征和标签打包

有什么用?

  • TensorDataset 是一个PyTorch提供的工具类,它的作用就像一个拉链,将我们的特征张量 X_train_tensor 和标签张量 y_train_tensor 配对打包在一起。

  • 它能确保数据和标签一一对应。当你从train_dataset中取第i个元素时,你会同时得到X_train_tensor的第i行和y_train_tensor的第i行。

为i什么需要它?

  1. 数据与标签的绑定:在训练时,模型需要同时拿到一条数据和它对应的正确答案。TensorDataset就是实现这种绑定的最简单直接的方式。

  2. 遵循标准接口:它创建了一个符合PyTorch标准的Dataset对象。所有Dataset对象都有共同的行为(比如可以用len()获取总长度,可以用[]索引来获取数据),这使得它可以无缝地被下一步的DataLoader所使用。当你的数据已经全部在内存中并是Tensor格式时,TensorDataset是创建数据集的首选。

使用语法

  • torch.utils.data.TensorDataset(*tensors)

  • 你需要传入一个或多个张量作为参数。

  • 关键要求:所有传入的张量的第一个维度(也就是样本数量)必须相等。在我们的例子中,X_train_tensor(形状 [891, 特征数])和 y_train_tensor(形状 [891, 1])的第一个维度都是891,所以满足这个要求。

2. DataLoader:自动化数据加载器

有什么用?

  • DataLoader 是PyTorch中最核心的数据加载工具。它接收一个Dataset对象(比如我们刚创建的train_dataset),并把它包装成一个强大的Python迭代器(iterator)

  • 在我们训练模型时,可以直接遍历这个train_loader,它会自动地、高效地为我们提供一个个小批量(mini-batch) 的数据。

为什么需要它?(这是PyTorch训练流程的精髓)

  1. 实现小批量梯度下降:我们不可能一次性把整个数据集(891条数据)都扔进模型去训练,这会占用巨大内存且效率低下。标准的做法是“小批量梯度下降(Mini-batch Gradient Descent)”,即每次只用一小部分数据(比如64条)来计算损失、更新模型权重,然后重复这个过程。DataLoader的核心工作就是帮我们自动完成这个分批操作。

  2. 数据打乱(shuffle=True:这是防止模型过拟合、提升泛化能力的关键。设置为True后,DataLoader会在每个训练周期(epoch)开始前,都重新随机打乱数据的顺序。这样可以确保模型不会学到数据的排列顺序,而是学习数据本身内在的模式。在训练时,这个参数几乎必须设为True

  3. 内存管理与效率:通过分批加载,我们无需一次性将所有数据载入到昂贵的GPU显存中,使得训练大数据集成为可能。DataLoader还支持多线程预加载数据(通过num_workers参数),可以实现CPU加载数据和GPU计算的并行,大大提升训练效率。

使用语法

  • torch.utils.data.DataLoader(dataset, batch_size=1, shuffle=False, ...)

  • dataset: 我们要加载的Dataset对象,这里是train_dataset

  • batch_size: 每个批次包含的样本数量。64是一个非常常见的大小,通常设为2的幂次方以提高硬件效率。最后一批的数量可能会小于batch_size

  • shuffle=True: 一个布尔值。True表示在每个epoch开始时打乱数据顺序。对于训练集,我们用True;对于验证集和测试集,我们通常用False,因为评估时我们希望顺序是固定的,便于比较结果。

搭建网络

import torch.nn as nn
import torch.nn.functional as Fclass TitanicNet(nn.Module):def __init__(self, input_features):super(TitanicNet, self).__init__()# 定义网络层self.fc1 = nn.Linear(input_features, 64) # 输入层 -> 第一个隐藏层self.fc2 = nn.Linear(64, 32)             # 第一个隐藏层 -> 第二个隐藏层self.fc3 = nn.Linear(32, 1)              # 第二个隐藏层 -> 输出层self.dropout = nn.Dropout(0.2)           # Dropout层,防止过拟合def forward(self, x):# 定义数据在网络中的流向(前向传播)x = F.relu(self.fc1(x)) # 使用ReLU激活函数x = self.dropout(x)     # 应用dropoutx = F.relu(self.fc2(x)) # 使用ReLU激活函数x = self.dropout(x)     # 应用dropoutx = self.fc3(x)         # 输出层不需要激活函数,因为我们将使用BCEWithLogitsLossreturn x

1. 整体框架: class TitanicNet(nn.Module)

  • 有什么用?

    • 这段代码定义了一个名为 TitanicNet 的Python类,它就是我们神经网络的模型骨架。

    • 所有在PyTorch中自定义的模型,都应该**继承(inherit)**自torch.nn.Module这个基类。

  • 为什么需要它?

    • 继承 nn.Module 后,我们的TitanicNet类就能自动获得PyTorch提供的各种强大功能,比如:

      • 自动追踪模型中所有可学习的参数(权重和偏置)。

      • 方便地将整个模型移动到GPU上(用 .to(device))。

      • 轻松地在训练模式 (.train()) 和评估模式 (.eval()) 之间切换(这对于Dropout和BatchNorm等层至关重要)。

      • 加载和保存整个模型的状态。

    • 这是一种约定,是PyTorch约定俗成的东西。

2. __init__ 方法:定义模型的网络层

有什么用?

  • __init__方法是这个类的构造函数。在PyTorch模型中,它的核心任务是定义并初始化模型需要用到的所有网络层

  • 这些网络层被定义好后,会作为类的属性(比如self.fc1)存储起来,等待在forward方法中被调用。

为什么需要它?

  • 这里是“蓝图规划”阶段。我们在这里一次性把所有需要的“砖瓦”都准备好,但先不关心它们怎么连接。这让模型的结构非常清晰。

语法详解

  • super(TitanicNet, self).__init__(): 必须调用!这行代码的作用是调用父类nn.Module的构造函数,完成一些必要的内部初始化,否则模型无法正常工作。

  • self.fc1 = nn.Linear(input_features, 64):

    • nn.Linear(in_features, out_features): 定义一个全连接层(Fully Connected Layer),也叫线性层。它会对输入数据做一次线性变换

  • in_features: 输入特征的数量。input_features就是我们处理好的数据X_train_tensor的列数。
    • out_features: 输出特征的数量,也就是该层神经元的个数。这里是64。

  • self.fc2 = nn.Linear(64, 32): 定义第二个全连接层。注意,它的输入维度64必须和上一层fc1的输出维度64完全一致。

  • self.fc3 = nn.Linear(32, 1): 定义输出层。它的输出维度是1,因为我们要做的是二分类任务(生还 vs. 未生还),最终只需要一个数值来代表预测结果。

  • self.dropout = nn.Dropout(0.2):

    • nn.Dropout(p): 定义一个Dropout层。它是一种非常有效的正则化手段,用于防止模型过拟合

    • p=0.2: 表示在训练过程中,每次数据流过这个层时,都有20%的神经元会被随机“丢弃”(暂时使其输出为0)。这强迫网络不能过度依赖任何一个神经元,从而学习到更鲁棒的特征。

    • 重要:Dropout只在训练模式(.train())下生效,在评估模式(.eval())下会自动失效.

3. forward 方法:连接网络层,定义数据流

语法详解

  • x = F.relu(self.fc1(x)):

    • self.fc1(x): 首先,输入数据x流过第一个全连接层。

    • F.relu(...): relu修正线性单元(Rectified Linear Unit),是一种激活函数。它的公式是 f(x)=max(0,x)。

    • 为什么需要激活函数? 如果没有像ReLU这样的非线性激活函数,那么无论我们堆叠多少个线性层,整个网络本质上还是一个线性模型,无法学习复杂的非线性规律。ReLU是目前最常用、最高效的激活函数之一。

  • x = self.dropout(x): 在激活之后,应用dropout。

  • x = self.fc3(x): 数据流过最后的输出层。

  • 为什么最后一层没有激活函数? 因为我们计划使用的损失函数是BCEWithLogitsLoss。这个损失函数内部已经集成了Sigmoid激活函数,并且这么做在数值上更稳定。所以,它期望的输入是未经激活的原始输出值,这些原始值被称为 logits

# 确定输入特征的数量
input_dims = X_train_tensor.shape[1]
model = TitanicNet(input_features=input_dims)
print(model)

1. input_dims = X_train_tensor.shape[1]

  • 有什么用?

    • 这行代码的作用是动态地获取我们训练数据 X_train_tensor特征数量(也就是数据的列数),并将其存储在变量 input_dims 中。

  • 为什么需要它?

    1. 模型的“入口”尺寸必须匹配:回忆一下我们定义的 TitanicNet 模型,它的 __init__ 方法需要一个参数 input_features,这个参数决定了模型第一层 nn.Linear(input_features, 64) 的输入神经元数量。这个数量必须和我们喂给它的数据的特征数完全一样,否则数据就塞不进模型的入口,程序会报错。

    2. 代码的灵活性:我们当然可以手动数一下有几个特征,然后写一个固定的数字(比如input_features=10)。但这是非常不好的编程习惯。如果我们将来调整了数据预处理步骤,增删了某个特征,就必须手动回来修改这个数字,很容易忘记而出错。像现在这样,直接从数据 X_train_tensor 的形状中获取特征数,意味着无论我们的数据有多少列,代码都能自动适应,无需任何修改。

  • 使用语法

    • tensor.shape: 这是PyTorch张量的一个属性,它返回一个元组(tuple),表示张量在各个维度上的大小。对于X_train_tensor,它的形状是 [样本数量, 特征数量]

    • .shape[1]: 我们通过索引 [1] 来获取这个元组中的第二个元素,也就是特征数量

2. model = TitanicNet(input_features=input_dims)

  • 有什么用?

    • 这行代码根据我们之前定义的 TitanicNet 类(蓝图),创建了一个具体、可用的模型实例(对象),并赋值给变量 model

  • 为什么需要它?

    • class TitanicNet(...) 只是它定义了模型应该长什么样。而 model = TitanicNet(...) 这一步,才建造出一个实际的模型。

    • 这个 model 对象现在是一个包含了我们所有网络层(fc1, fc2, fc3, dropout)的集合体。PyTorch已经为这些层自动初始化了随机的权重(weights)和偏置(biases)。这个 model 就是我们接下来要进行训练、评估和预测的主体。

  • 使用语法

    • 这是标准的Python类实例化语法。

    • TitanicNet(input_features=input_dims): 调用类的名字,并传入构造函数__init__所需要的参数。这里,我们把刚刚动态获取的特征数 input_dims 传递给了 input_features 参数。

# 定义损失函数
# BCEWithLogitsLoss 结合了 Sigmoid 和 BCELoss,数值上更稳定
criterion = nn.BCEWithLogitsLoss()# 定义优化器
# Adam 是一种常用的自适应学习率优化算法
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)# 设置训练参数
EPOCHS = 100
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")
model.to(device) # 将模型移动到GPU(如果可用)print(torch.__version__)
print(torch.version.cuda)  # 应该显示 CUDA 版本

1. criterion = nn.BCEWithLogitsLoss():定义损失函数

  • 有什么用?

    • 这行代码定义了我们的损失函数(Loss Function),也叫代价函数(Cost Function)评判标准(Criterion)

    • 它的核心任务是衡量模型的预测结果真实标签之间的差距。差距越大,损失值就越高,说明模型预测得越“差”。

  • 为什么需要它?

    1. 为训练提供目标:整个神经网络的训练过程,就是一个想方设法最小化损失函数值的过程。损失函数为模型的学习指明了方向和目标。

    2. 选择BCEWithLogitsLoss的原因

      • BCE二元交叉熵(Binary Cross-Entropy)的缩写,这是解决二分类问题(如我们的“生还/未生还”)的标准损失函数。

      • WithLogits是它的精髓所在。正如我们之前在定义模型时讨论的,我们的模型最后一层输出的是未经激活的原始数值(logits)。BCEWithLogitsLoss这个损失函数会在内部自动帮我们完成Sigmoid激活,然后再计算二元交叉熵损失。

      • 数值稳定性:您的注释非常正确,将Sigmoid和BCE合并在一个函数里计算,可以避免在某些情况下(比如logits的绝对值很大时)出现浮点数溢出问题,比我们自己手动加Sigmoid层再用BCELoss要更稳定、更安全。

  • 使用语法

    • criterion = nn.损失函数类(): 我们通过实例化一个损失函数类来创建一个可用的损失函数对象。对于BCEWithLogitsLoss的基础用法,不需要传入任何参数。

2. optimizer = torch.optim.Adam(...):定义优化器

  • 有什么用?

    • 这行代码定义了我们的优化器(Optimizer)。如果说损失函数是指出“错在哪里”,那么优化器就是负责“如何改正”的工具。

    • 它根据损失函数计算出的梯度(指明了让损失变小的方向),来更新模型中所有的权重和偏置,从而让模型在下一次预测时表现得更好。

  • 为什么需要它?

    1. 驱动模型学习:优化器是模型学习的引擎。在训练循环中,我们喊一声optimizer.step(),模型的所有参数就会根据计算好的梯度进行一次微调。没有优化器,模型就只是一个静态的结构,无法学习。

    2. 选择Adam的原因Adam(自适应矩估计)是目前最流行、最通用的优化器之一。相比传统的随机梯度下降(SGD),Adam能够为模型中的每一个参数自适应地调整学习率。在绝大多数任务中,它都能快速、稳定地收敛,是一个非常优秀的“万金油”选择,非常适合作为入门和首选。

  • 使用语法

    • optimizer = torch.optim.优化器类(params, ...)

    • params = model.parameters(): 这是最重要的参数。我们必须明确告诉优化器,它需要更新哪些参数。model.parameters()nn.Module提供的一个便捷方法,它会自动返回我们模型中所有需要学习的参数(所有nn.Linear等层里的权重和偏置)。

    • lr=0.001: lr代表学习率(Learning Rate)。它控制了每次更新参数时的“步子”大小。这是一个至关重要的超参数。0.001是Adam优化器一个非常常用且效果不错的初始值。

3. device = ...model.to(device):配置计算设备

  • 有什么用?

    • 这几行代码的作用是自动检测当前环境是否有可用的NVIDIA GPU(以及CUDA环境),并将我们的模型迁移到指定的计算设备上

  • 为什么需要它?

    1. 大幅加速训练:神经网络的计算核心是海量的矩阵运算。GPU(图形处理器)的设计天生就擅长这种并行计算,其速度比CPU快几十甚至上百倍。对于任何稍具规模的模型,使用GPU训练都是必须的,否则可能需要花费数小时甚至数天的时间。

    2. 统一设备:在PyTorch中,要进行计算,模型数据必须在同一个设备上。我们在这里先把模型用.to(device)放到了GPU上,之后在训练循环中,我们还需要把每一批次的数据也放到同一个device上。

  • 使用语法

    • EPOCHS = 100: 定义训练的总轮数,即我们要把整个训练数据集从头到尾过多少遍。

    • torch.cuda.is_available(): 返回TrueFalse,判断CUDA环境是否可用。

    • device = torch.device(...): 创建一个设备对象。"cuda"代表GPU,"cpu"代表CPU。通过一个三元表达式,我们的代码实现了自动选择。

    • model.to(device): 这是nn.Module的方法,它会将模型内部所有的参数和缓冲区都移动到指定的device上。

第四步,开始训练

# 训练循环
for epoch in range(EPOCHS):model.train() # 将模型设置为训练模式epoch_loss = 0.0for inputs, labels in train_loader:inputs, labels = inputs.to(device), labels.to(device)# 1. 梯度清零optimizer.zero_grad()# 2. 前向传播outputs = model(inputs)# 3. 计算损失loss = criterion(outputs, labels)# 4. 反向传播loss.backward()# 5. 更新权重optimizer.step()epoch_loss += loss.item()if (epoch + 1) % 10 == 0:print(f'Epoch [{epoch+1}/{EPOCHS}], Loss: {epoch_loss/len(train_loader):.4f}')

整体结构:Epoch 与 Batch 的双重循环

整个训练循环是一个双层for循环:

  • 外层循环 (for epoch in range(EPOCHS):):遍历世代(Epoch)。一个Epoch代表我们的模型已经完整地看过一遍所有的训练数据。我们让模型反复看很多遍(这里是100遍),以期它能充分学习到数据中的规律。

  • 内层循环 (for inputs, labels in train_loader:):遍历批次(Batch)。在每一个Epoch中,我们不是一次性处理所有数据,而是通过train_loader,一小批一小批地处理数据。

训练循环详解

我们来逐步分析循环内部的每一个动作:

model.train()

  • 有什么用?:在每个Epoch开始时,调用这行代码来明确地告诉PyTorch:“现在开始是训练模式了!”

  • 为什么需要它?:这是非常关键的一步。它会“激活”模型中只在训练时才使用的层,比如我们之前定义的Dropout层。反之,在评估模型时,我们会调用model.eval()来“关闭”这些层,以保证评估结果的确定性。

inputs, labels = inputs.to(device), labels.to(device)

  • 有什么用?:将当前批次的数据(inputs)和标签(labels)一起发送到我们之前设定的计算设备上(cpucuda)。

  • 为什么需要它?:为了进行计算,模型和它要处理的数据必须位于同一个设备上。我们之前已经把model放到了device上,所以现在也必须把每一批数据放上去。

训练的核心五步

这五步是所有PyTorch标准训练流程,顺序几乎是固定的。

1. optimizer.zero_grad() - 梯度清零

  • 有什么用?:清除上一个批次计算出的所有参数的梯度。

  • 为什么需要它?:PyTorch的梯度在反向传播时是累加的。如果我们不清零,当前批次计算出的梯度就会被加到上一个批次的梯度上,这会导致完全错误的更新方向。所以,在为新的一批数据计算梯度之前,必须“清扫”一下,确保每次更新都只基于当前批次的数据。

2. outputs = model(inputs) - 前向传播

  • 有什么用?:将输入数据inputs喂给模型,得到模型的预测输出outputs(也就是logits)。

  • 为什么需要它?:这是模型进行“预测”的过程。数据按照我们在forward方法中定义的路径,在网络中走了一遍,得出了一个初步的答案。

3. loss = criterion(outputs, labels) - 计算损失

  • 有什么用?:用我们定义的损失函数criterion,来比较模型的预测outputs和真实标签labels之间的差距。

  • 为什么需要它?:这是为了量化模型“错得有多离谱”。得到的loss是一个单独的数值,这个数值就是我们接下来要努力减小的目标。

4. loss.backward() - 反向传播

  • 有什么用?:PyTorch的autograd(自动微分)引擎会根据loss,自动计算出模型中每一个可学习参数(权重和偏置)的梯度。

  • 为什么需要它?:梯度指明了能让损失函数值上升最快的方向。因此,梯度的反方向就是我们调整参数、降低损失的最佳方向。这一步就是为了找到这个“方向图”。

5. optimizer.step() - 更新权重

  • 有什么用?:命令我们的优化器optimizer,根据上一步计算出的梯度,来更新模型的所有参数。

  • 为什么需要它?:这是真正发生“学习”的一步。loss.backward()只负责计算方向,optimizer.step()则负责真正地朝着这个正确的方向“迈出一步”,对模型的权重进行微调。

epoch_loss += loss.item()

  • 有什么用?loss本身是一个带计算图的张量。.item()方法可以从中提取出纯粹的Python数字。我们把每个batch的loss累加起来,是为了计算整个epoch的平均loss。

  • 为什么需要它?:这是为了监控训练过程。通过观察每个epoch的平均loss是否在稳步下降,我们就能判断模型是否在有效地学习。

print(f'Epoch ...')

  • 有什么用?:每隔10个epoch,打印一次当前的epoch数和这个epoch的平均损失。

  • 为什么需要它?:这里的epoch_loss/len(train_loader)就是用总损失除以总批次数,得到平均损失。

第五步,评估模型并且生成测试结果文件

X_np1 = test_processed_df.to_numpy(dtype=np.float32)  # 强制指定float32
X_test_tensor = torch.from_numpy(X_np1)  # NumPy->PyTorch自动类型匹配# 1. 进行预测
model.eval() # 将模型设置为评估模式,这会禁用Dropout
with torch.no_grad(): # 在这个代码块中,不计算梯度,以节省计算资源X_test_tensor = X_test_tensor.to(device)test_outputs = model(X_test_tensor)# test_outputs是logits,需要通过sigmoid转换为概率test_probs = torch.sigmoid(test_outputs).cpu()# 根据概率(阈值为0.5)转换为0或1的预测结果test_preds = (test_probs > 0.5).int().squeeze()# 2. 创建提交文件
submission_df = pd.DataFrame({'PassengerId': test_passenger_ids,'Survived': test_preds.numpy()
})submission_df.to_csv('submission_pytorch.csv', index=False)print("\n提交文件 'submission_pytorch.csv' 已生成!")
print(submission_df.head())

X_np1 = test_processed_df.to_numpy(dtype=np.float32)  # 强制指定float32
X_test_tensor = torch.from_numpy(X_np1)  # NumPy->PyTorch自动类型匹配

将我们预处理好的测试集特征test_processed_df,转换成PyTorch模型能够接收的Tensor格式。

进行预测(模型推理 )

这一部分是模型应用的核心,包含了几个非常关键的步骤。

model.eval()

  • 有什么用?:将模型切换到评估模式

  • 为什么需要它?:这是model.train()的对应操作,至关重要。在评估模式下,PyTorch会自动关闭Dropout层(因为预测时我们希望使用整个网络的能力,而不是随机丢弃神经元)和BatchNorm层(如果模型中有的话)。这能保证我们的预测结果是确定性的、可复现的。

with torch.no_grad():

  • 有什么用?:创建一个上下文管理器,临时关闭所有梯度的计算

  • 为什么需要它?:在预测阶段,我们只是单纯地让数据通过模型得到结果,完全不需要计算梯度(梯度是训练时用来更新权重的)。关闭梯度计算可以带来两大好处:

    1. 节省内存:不需要为反向传播保存中间状态。

    2. 加快速度:跳过了梯度计算的开销,让前向传播更快。

    • 这是所有模型推理代码的标准优化操作。

预测流程详解

1.X_test_tensor.to(device): 将测试数据张量也放到与模型相同的设备上。

2.test_outputs = model(X_test_tensor): 进行预测。将测试数据喂给模型,得到模型的原始输出(logits)。

3.test_probs = torch.sigmoid(test_outputs).cpu(): 转换成概率

  • torch.sigmoid(...): 模型的输出是logits,为了将其解释为“生还的概率”(一个0到1之间的数),我们必须用sigmoid函数对其进行激活。

  • .cpu(): 模型的计算可能在GPU上完成,得到的test_outputs也在GPU上。为了方便后续使用NumPy或Pandas处理,我们通常会用.cpu()方法将结果数据拷回CPU内存。

4.test_preds = (test_probs > 0.5).int().squeeze(): 得出最终类别

  • (test_probs > 0.5): 以0.5为阈值,将概率转换为布尔值(True/False)。概率大于0.5的被认为是True(预测为生还)。

  • .int(): 将布尔值True/False转换为整数1/0,得到我们最终的预测类别。

  • .squeeze(): 此时张量的形状是[样本数, 1].squeeze()会移除所有大小为1的维度,将其变成[样本数],这更方便我们后续创建DataFrame。

submission_df = pd.DataFrame

({ 'PassengerId': test_passenger_ids, 'Survived': test_preds.numpy() })

submission_df.to_csv('submission_pytorch.csv', index=False)

  • 有什么用?:将我们的预测结果与乘客ID配对,生成一个符合Kaggle等竞赛平台要求的CSV文件。

  • 为什么需要它?:这是将我们的模型成果转化为最终交付物的步骤。

  • 使用语法

    • pd.DataFrame({...}): 用一个字典来创建一个Pandas DataFrame。字典的键成为列名。

    • 'PassengerId': test_passenger_ids: 还记得我们最开始就保存下来的测试集乘客ID吗?现在它派上了用场,确保每个预测结果都能和正确的乘客对应上。

    • 'Survived': test_preds.numpy(): 为了将PyTorch张量放入DataFrame,需要先用.numpy()方法将其转换回NumPy数组。

    • .to_csv('...', index=False): 将DataFrame保存为CSV文件。index=False是一个非常重要的参数,它告诉Pandas不要将DataFrame自身的行索引(0, 1, 2...)写入到文件中,避免提交格式错误。

最后,关于提高准确率

以上就是我们的整个竞赛流程,但是如果安装标准流程,实际上准确率并不够高,接下来我从几个方向帮助大家后续提高准确率

1. 特征工程 

对于像泰坦尼克号这样的表格类数据问题,特征工程往往是提升模型表现最有效的方法。我们的模型能学到的上限,很大程度上取决于我们喂给它的数据质量。

创造更有信息的特征:创建FamilySize (家庭大小): SibSp (兄弟姐妹/配偶数) + Parch (父母/子女数) + 1 (自己) = FamilySize。家庭大小可能和生还率有关(例如,独自一人 vs. 小家庭 vs. 大家庭)。

2. 模型结构 

增加或减少层的深度和宽度:

更宽: 可以尝试增加每层神经元的数量,比如 128 -> 64

更深: 可以再增加一个隐藏层,比如 128 -> 64 -> 32

注意: 更大更深的网络不一定更好,它会增加过拟合的风险,需要更强的正则化手段来配合。

使用批量归一化:

在全连接层之后、激活函数之前加入nn.BatchNorm1d层。可以加速模型收敛,稳定训练过程,并在一定程度上起到正则化作用。

修改后的forward可能像这样: x = self.batchnorm1(F.relu(self.fc1(x)))

3. 训练过程

  • 调整超参数 :

    • 学习率 : 0.001 是一个很好的起点,但可以尝试更小(如0.0005)或更大(如0.005)的值。

    • 优化器 (Optimizer): Adam很棒,但也可以试试AdamW(Adam的改进版,带有权重衰减)或者RMSprop

    • 训练轮数 (Epochs): 训练更多轮可能会有提升,但要小心过拟合。

  • 使用学习率调度器 :

    • 在训练过程中动态地调整学习率,通常是逐渐降低。比如,使用torch.optim.lr_scheduler.ReduceLROnPlateau,当模型性能在几轮内没有提升时自动降低学习率。这在训练后期非常有帮助。

4. 更高级的策略

  • 交叉验证 (Cross-Validation):

    • 之前的做法只是简单地将原始训练集用于训练。更可靠的做法是使用K折交叉验证

    • 例如,进行5折交叉验证:将训练集分成5份,轮流用其中4份进行训练,剩下1份用于验证。这样我们就训练了5个模型。最终提交时,可以用这5个模型对测试集预测结果的平均值或投票,这通常会比单一模型的结果要稳定和准确得多。

  • 模型集成 (Ensembling):

    • 不要只依赖一个神经网络。可以训练几个不同结构的模型,或者完全不同类型的模型(比如经典的梯度提升树XGBoost, LightGBM,或随机森林),然后将它们的预测结果融合起来。模型集成是竞赛中刷榜常用方式。

http://www.lryc.cn/news/611518.html

相关文章:

  • 霍尔传感器
  • 碰撞问题的分析
  • 什么是CDN, 它为什么更快
  • 《算法导论》第 7 章 - 快速排序
  • 概率/期望 DP Jon and Orbs
  • 机器学习④【算法详解:从决策树到随机森林】
  • 一周学会Matplotlib3 Python 数据可视化-图形的组成部分
  • 场外期权的卖方是什么策略?
  • Python包管理新利器:uv全面解析与Conda对比指南
  • 从 LinkedIn 到 Apache:Kafka 的架构设计与应用场景
  • KafKa 项目 -- GitHub 学习
  • 【第6话:相机模型2】相机标定在自动驾驶中的作用、相机标定方法详解及代码说明
  • 在Word和WPS文字中如何输入汉字的偏旁部首
  • SELinux加固Linux安全2
  • docker安装FFmpeg
  • SmartMediaKit 模块化音视频框架实战指南:场景链路 + 能力矩阵全解析
  • Flink CDC如何保障数据的一致性?
  • 力扣经典算法篇-44-组合总和(回溯问题)
  • Ubuntu20.04 离线安装 FFmpeg 静态编译包
  • 【unity实战】用unity实现一个3D俯视角暗杀潜行恐怖类游戏,主要是实现视野范围可视化效果
  • X86-ubuntu22.04远程桌面只有1/4无法正常操作
  • 问题定位排查手记1 | 从Windows端快速检查连接状态
  • 分布式文件系统07-小文件系统的请求异步化高并发性能优化
  • ubuntu 22.04 中安装python3.11 和 3.11 的 pip
  • STM32U5 外部中断不响应问题分析
  • Ubuntu设置
  • DevOps时代的知识基座革命:Gitee Wiki如何重构研发协作范式
  • 基于51单片机的温控风扇Protues仿真设计
  • 【面试场景题】电商秒杀系统的库存管理设计实战
  • Python高级排序技术:非原生可比对象的自定义排序策略详解