PyTorch之泰坦尼克号—对Titanic数据进行训练代码分析和基础解释
文章目录
基础分析代码
from torch.utils.data import Dataset, DataLoader
import torch
import torch.nn as nn
import pandas as pd
class LogisticRegressionModel(nn.Module):
def __init__(self, input_dim):
super().__init__()
self.linear = nn.Linear(input_dim, 1) # nn.Linear也继承自nn.Module,输入为input_dim,输出一个值
def forward(self, x):
return torch.sigmoid(self.linear(x)) # Logistic Regression 输出概率
class TitanicDataset(Dataset):
def __init__(self, file_path):
self.file_path = file_path
self.mean = {
"Pclass": 2.236695,
"Age": 29.699118,
"SibSp": 0.512605,
"Parch": 0.431373,
"Fare": 34.694514,
"Sex_female": 0.365546,
"Sex_male": 0.634454,
"Embarked_C": 0.182073,
"Embarked_Q": 0.039216,
"Embarked_S": 0.775910
}
self.std = {
"Pclass": 0.838250,
"Age": 14.526497,
"SibSp": 0.929783,
"Parch": 0.853289,
"Fare": 52.918930,
"Sex_female": 0.481921,
"Sex_male": 0.481921,
"Embarked_C": 0.386175,
"Embarked_Q": 0.194244,
"Embarked_S": 0.417274
}
self.data = self._load_data()
self.feature_size = len(self.data.columns) - 1
def _load_data(self):
df = pd.read_csv(self.file_path)
df = df.drop(columns=["PassengerId", "Name", "Ticket", "Cabin"]) ##删除不用的列
df = df.dropna(subset=["Age"])##删除Age有缺失的行
df = pd.get_dummies(df, columns=["Sex", "Embarked"], dtype=int)##进行one-hot编码
##进行数据的标准化
base_features = ["Pclass", "Age", "SibSp", "Parch", "Fare"]
for i in range(len(base_features)):
df[base_features[i]] = (df[base_features[i]] - self.mean[base_features[i]]) / self.std[base_features[i]]
return df
def __len__(self):
return len(self.data)
def __getitem__(self, idx):
features = self.data.drop(columns=["Survived"]).iloc[idx].values
label = self.data["Survived"].iloc[idx]
return torch.tensor(features, dtype=torch.float32), torch.tensor(label, dtype=torch.float32)
train_dataset = TitanicDataset(r"E:\电子书\RethinkFun深度学习\data\titanic\train.csv")
validation_dataset = TitanicDataset(r"E:\电子书\RethinkFun深度学习\data\titanic\validation.csv")
model = LogisticRegressionModel(train_dataset.feature_size)
model.to("cuda")
model.train()
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)
epochs = 100
for epoch in range(epochs):
correct = 0
step = 0
total_loss = 0
for features, labels in DataLoader(train_dataset, batch_size=256, shuffle=True):
step += 1
features = features.to("cuda")
labels = labels.to("cuda")
optimizer.zero_grad()
outputs = model(features).squeeze()
correct += torch.sum(((outputs >= 0.5) == labels))
loss = torch.nn.functional.binary_cross_entropy(outputs, labels)
total_loss += loss.item()
loss.backward()
optimizer.step()
print(f'Epoch {epoch + 1}, Loss: {total_loss/step:.4f}')
print(f'Training Accuracy: {correct / len(train_dataset)}')
model.eval()
with torch.no_grad():
correct = 0
for features, labels in DataLoader(validation_dataset, batch_size=256):
features = features.to("cuda")
labels = labels.to("cuda")
outputs = model(features).squeeze()
correct += torch.sum(((outputs >= 0.5) == labels))
print(f'Validation Accuracy: {correct / len(validation_dataset)}')
从逻辑回归到pytorch基础训练
从“逻辑回归原理”到“用PyTorch实现”确实有一个思维跳跃。开始一步步搭建这个桥梁。
思维过渡:从数学公式到代码实现
第一步:回忆逻辑回归的数学本质
逻辑回归的核心公式是:
z = w1*x1 + w2*x2 + ... + wn*xn + b
p = sigmoid(z) # 把z压缩到0-1之间,得到概率
其中:
- w1, w2, …, wn 是权重参数
- b 是偏置参数
- x1, x2, …, xn 是输入特征
- 我们的目标是找到最好的w和b,让预测概率p尽量接近真实标签
第二步:PyTorch如何表示这个数学公式?
在PyTorch中,我们用一个类来表示这个公式:
class LogisticRegression(nn.Module):
def __init__(self, input_dim):
super().__init__()
# 这里定义了公式中的 w 和 b
self.linear = nn.Linear(input_dim, 1)
def forward(self, x):
# 这里实现了公式计算:z = wx + b,然后 p = sigmoid(z)
z = self.linear(x) # 计算 z = w1*x1 + w2*x2 + ... + b
p = torch.sigmoid(z) # 计算 p = sigmoid(z)
return p
关键理解:
- nn.Linear(input_dim, 1) 这一行代码,就自动创建了公式中所有的 w 和 b
- 如果 input_dim=8(8个特征),它就创建了 w1, w2, …, w8 和 b 共9个参数
- 这些参数一开始是随机值,我们需要通过训练找到它们的正确值
第三步:训练过程的核心思想
训练就是不断调整w和b的值,让模型预测更准。这个过程分三步:
- 前向传播(Forward)—— 用当前参数做预测
# 假设我们有8个特征
x = [年龄, 性别, 船票等级, ...] # 一个乘客的特征
# 用当前的w和b计算
z = w1*年龄 + w2*性别 + ... + w8*特征8 + b
p = sigmoid(z) # 得到预测概率
在代码中就是:predictions = model(batch_data)
2. 计算损失(Loss)—— 判断预测有多差
# 如果真实标签是1(幸存),但p=0.3(预测遇难)
# 说明预测得很差,损失应该很大
loss = 一些计算方式(p, 真实标签)
在代码中就是:loss = loss_function(predictions, labels)
3. 反向传播(Backward)—— 找出调整方向
这是最关键的一步! 我们需要知道:
如果增大w1,loss会变大还是变小?
如果减小b,loss会变大还是变小?
PyTorch通过 loss.backward() 自动计算每个参数的梯度:
梯度 > 0:增大这个参数会使loss增加(应该减小它)
梯度 < 0:增大这个参数会使loss减少(应该增大它)
4. 更新参数(Step)—— 实际调整参数
# 根据梯度方向和学习率调整
w1 = w1 - 学习率 * w1的梯度
w2 = w2 - 学习率 * w2的梯度
...
b = b - 学习率 * b的梯度
在代码中就是:optimizer.step()
第四步:完整训练循环的直观理解
想象你在教一个完全不懂的新手做选择题:
for epoch in range(100): # 教100轮
for batch in dataloader: # 每次给他5道题(batch_size=5)
# 1. 清空他上次的错误记忆
optimizer.zero_grad()
# 2. 让他做这5道题
predictions = model(batch.questions)
# 3. 批改,看他错了多少
loss = 计算错误数量(predictions, batch.answers)
# 4. 分析:每道题为什么错?哪个知识点薄弱?
loss.backward() # 自动分析!
# 分析结果:知识点A薄弱度=0.8,知识点B薄弱度=-0.3...
# (正数表示要加强,负数表示要减弱)
# 5. 针对性辅导:薄弱的知识点多讲,已经会的少讲
optimizer.step() # 根据分析调整教学重点
第五步:为什么需要optimizer.zero_grad()?
这是最容易困惑的点!看这个例子:
没有zero_grad的情况(错误):
# 第一批数据:梯度分析显示w1应该+0.1
loss1.backward() # w1.grad = +0.1
# 第二批数据:梯度分析显示w1应该-0.2
loss2.backward() # w1.grad = +0.1 + (-0.2) = -0.1
optimizer.step() # 实际调整:w1 = w1 - lr * (-0.1)
# 注意!这里用的是两批数据的混合梯度!
# 这不是我们想要的!
有zero_grad的情况(正确):
# 第一批数据
optimizer.zero_grad() # w1.grad = 0
loss1.backward() # w1.grad = +0.1
optimizer.step() # 用+0.1调整w1
# 第二批数据
optimizer.zero_grad() # w1.grad = 0 (清空!)
loss2.backward() # w1.grad = -0.2
optimizer.step() # 用-0.2调整w1
# 每批数据独立分析,独立调整!
第六步:从你的角度重新看代码
现在,用这个理解重新看训练代码:
# 1. 创建模型:定义了公式 z = wx + b
model = LogisticRegression(input_dim=8)
# 2. 创建优化器:告诉它要调整哪些参数(所有的w和b)
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)
# 翻译:创建一个调参师傅,专门调整model里的所有参数,每次调整幅度0.1
# 3. 训练循环
for epoch in range(100):
for batch_data, batch_labels in dataloader:
# 每次调整前,清空上次的分析结果
optimizer.zero_grad()
# 用当前参数做预测
predictions = model(batch_data)
# 计算预测有多差
loss = F.binary_cross_entropy(predictions, batch_labels)
# 自动分析:每个参数应该怎么调?
loss.backward()
# 实际调整参数
optimizer.step()
总结过渡
从逻辑回归到PyTorch实现的思维转变:
- 数学公式 → Python类:把 z = wx + b 写成一个类,让PyTorch管理w和b
- 手动计算梯度 → 自动计算:不用自己推导梯度公式,loss.backward() 自动搞定
- 手动更新参数 → 优化器更新:不用写 w = w - lr * grad,optimizer.step() 自动更新
- 单个样本 → 批量处理:PyTorch用DataLoader一次处理一批数据,效率更高
试着运行一次完整代码,在关键位置打印一些值看看:
print("模型参数数量:", len(list(model.parameters())))
print("第一个batch的预测:", predictions[:3])
print("损失值:", loss.item())
增加一些特征的二次项代码
class TitanicDataset(Dataset):
def __init__(self, file_path):
self.file_path = file_path
self.mean = {
"Pclass": 2.236695,
"Age": 29.699118,
"SibSp": 0.512605,
"Parch": 0.431373,
"Fare": 34.694514,
"Sex_female": 0.365546,
"Sex_male": 0.634454,
"Embarked_C": 0.182073,
"Embarked_Q": 0.039216,
"Embarked_S": 0.775910,
"Pclass_Pclass": 5.704482,
"Pclass_Age": 61.938151,
"Pclass_SibSp": 1.198880,
"Pclass_Parch": 0.983193,
"Pclass_Fare": 53.052327,
"Pclass_Sex_female": 0.754902,
"Age_Age": 1092.761169,
"Age_SibSp": 11.066415,
"Age_Parch": 10.470476,
"Age_Fare": 1104.142053,
"Age_Sex_female": 10.204482,
"SibSp_SibSp": 1.126050,
"SibSp_Parch": 0.525210,
"SibSp_Fare": 24.581262,
"SibSp_Sex_female": 0.233894,
"Parch_Parch": 0.913165,
"Parch_Fare": 24.215465,
"Parch_Sex_female": 0.259104,
"Fare_Fare": 4000.200255,
"Fare_Sex_female": 17.393698,
"Sex_female_Sex_female": 0.365546
}
self.std = {
"Pclass": 0.838250,
"Age": 14.526497,
"SibSp": 0.929783,
"Parch": 0.853289,
"Fare": 52.918930,
"Sex_female": 0.481921,
"Sex_male": 0.481921,
"Embarked_C": 0.386175,
"Embarked_Q": 0.194244,
"Embarked_S": 0.417274,
"Pclass_Pclass": 3.447593,
"Pclass_Age": 34.379609,
"Pclass_SibSp": 2.603741,
"Pclass_Parch": 2.236945,
"Pclass_Fare": 52.407209,
"Pclass_Sex_female": 1.118572,
"Age_Age": 991.079188,
"Age_SibSp": 19.093099,
"Age_Parch": 29.164503,
"Age_Fare": 1949.356185,
"Age_Sex_female": 15.924481,
"SibSp_SibSp": 3.428831,
"SibSp_Parch": 1.561298,
"SibSp_Fare": 70.185369,
"SibSp_Sex_female": 0.639885,
"Parch_Parch": 3.008314,
"Parch_Fare": 77.207321,
"Parch_Sex_female": 0.729143,
"Fare_Fare": 19105.110593,
"Fare_Sex_female": 43.568303,
"Sex_female_Sex_female": 0.481921
}
self.data = self._load_data()
self.feature_size = len(self.data.columns) - 1
def _load_data(self):
df = pd.read_csv(self.file_path)
df = df.drop(columns=["PassengerId", "Name", "Ticket", "Cabin"])
df = df.dropna(subset=["Age"])
df = pd.get_dummies(df, columns=["Sex", "Embarked"], dtype=int)
base_features = ["Pclass", "Age", "SibSp", "Parch", "Fare", "Sex_female"]
for i in range(len(base_features)):
for j in range(i, len(base_features)):
df[base_features[i] + "_" + base_features[j]] = ((df[base_features[i]] * df[base_features[j]]
- self.mean[
base_features[i] + "_" + base_features[j]])
/ self.std[base_features[i] + "_" + base_features[j]])
for i in range(len(base_features)):
df[base_features[i]] = (df[base_features[i]] - self.mean[base_features[i]]) / self.std[base_features[i]]
return df
def __len__(self):
return len(self.data)
def __getitem__(self, idx):
features = self.data.drop(columns=["Survived"]).iloc[idx].values
label = self.data["Survived"].iloc[idx]
return torch.tensor(features, dtype=torch.float32), torch.tensor(label, dtype=torch.float32)
核心代码解释
解释这三个关键代码段,帮你理解整个流程。
第一部分:训练过程中的关键代码
outputs = model(features).squeeze()
correct += torch.sum(((outputs >= 0.5) == labels))
loss = torch.nn.functional.binary_cross_entropy(outputs, labels)
total_loss += loss.item()
1. outputs = model(features).squeeze()
-
model(features):模型预测,得到形状为 [batch_size, 1] 的张量
- 例如:[[0.7], [0.3], [0.9], …],每个值在0-1之间,表示预测的幸存概率
-
.squeeze():去掉多余的维度,从 [batch_size, 1] 变成 [batch_size]
例如:[0.7, 0.3, 0.9, …]
为什么要这样做?因为后续计算需要一维数组
2. correct += torch.sum(((outputs >= 0.5) == labels))
这是计算正确预测数量的代码,分三步理解:
# 假设 outputs = [0.7, 0.3, 0.9, 0.2]
# 假设 labels = [1, 0, 1, 0] (1=幸存,0=遇难)
# 第一步:将概率转换为类别预测
predictions = outputs >= 0.5
# predictions = [True, False, True, False]
# 解释:0.7>0.5 → True(预测幸存), 0.3<0.5 → False(预测遇难)
# 第二步:比较预测和真实标签
comparison = predictions == labels
# comparison = [True == 1, False == 0, True == 1, False == 0]
# 在PyTorch中,True=1, False=0,所以:
# comparison = [True, True, True, True] # 因为1==1, 0==0, 1==1, 0==0
# 第三步:统计True的数量
correct_count = torch.sum(comparison)
# correct_count = 4 (这个batch全部预测正确)
3. loss = torch.nn.functional.binary_cross_entropy(outputs, labels)
这是计算损失函数,衡量预测有多差:
- 二元交叉熵:专门用于二分类问题(幸存/遇难)
- 数学意义:如果真实标签是1,预测概率越接近1,损失越小;预测概率越接近0,损失越大
- 例子:
- 真实标签=1,预测=0.9 → 损失小(-log(0.9)≈0.1)
- 真实标签=1,预测=0.1 → 损失大(-log(0.1)≈2.3)
4. total_loss += loss.item()
- loss 是一个PyTorch张量,包含计算图信息(用于反向传播)
- .item() 提取其中的数值(Python浮点数)
- 累加到 total_loss 中,用于后续计算平均损失
第二部分:预测/验证过程
with torch.no_grad():
correct = 0
for features, labels in DataLoader(validation_dataset, batch_size=256):
features = features.to("cuda")
labels = labels.to("cuda")
outputs = model(features).squeeze()
correct += torch.sum(((outputs >= 0.5) == labels))
print(f'Validation Accuracy: {correct / len(validation_dataset)}')
关键点1:with torch.no_grad():
这是最重要的区别!它告诉PyTorch:
- 不要记录计算图(不跟踪梯度)
- 不要计算梯度(节省内存和计算资源)
- 为什么:验证/预测时我们只需要前向传播,不需要反向传播更新参数
关键点2:验证流程 vs 训练流程
对比一下:
| 步骤 | 训练过程 | 验证过程 |
|---|---|---|
| 1 | 需要梯度计算 | 不需要梯度计算 (torch.no_grad()) |
| 2 | 计算损失 | 不计算损失(或只计算不反向传播) |
| 3 | loss.backward() | 没有反向传播 |
| 4 | optimizer.step() | 没有参数更新 |
| 5 | 目的是学习参数 | 目的是评估性能 |
完整验证流程:
- 关闭梯度计算:with torch.no_grad():
- 加载验证数据:从DataLoader获取批次
- 前向传播:outputs = model(features) 得到预测概率
- 计算正确率:比较 (outputs >= 0.5) 和真实标签
- 统计并输出:正确数 / 总样本数
第三部分:特征工程优化
for i in range(len(base_features)):
for j in range(i, len(base_features)):
df[base_features[i] + "_" + base_features[j]] = (
(df[base_features[i]] * df[base_features[j]] - self.mean[base_features[i] + "_" + base_features[j]])
/ self.std[base_features[i] + "_" + base_features[j]]
)
for i in range(len(base_features)):
df[base_features[i]] = (df[base_features[i]] - self.mean[base_features[i]]) / self.std[base_features[i]]
这是增加二次项特征的代码,分两部分:
第一部分:创建二次项特征(交互特征)
# 假设 base_features = ['年龄', '票价', '兄弟姐妹数']
# 双重循环创建所有两两组合的乘积特征:
# i=0, j=0: '年龄_年龄' = 年龄²
# i=0, j=1: '年龄_票价' = 年龄×票价
# i=0, j=2: '年龄_兄弟姐妹数' = 年龄×兄弟姐妹数
# i=1, j=1: '票价_票价' = 票价²
# i=1, j=2: '票价_兄弟姐妹数' = 票价×兄弟姐妹数
# i=2, j=2: '兄弟姐妹数_兄弟姐妹数' = 兄弟姐妹数²
# 标准化: (原始值 - 均值) / 标准差
# 例如:年龄_票价 = (年龄×票价 - 年龄×票价的均值) / 年龄×票价的标准差
为什么这样做?
- 原始模型只能学习线性关系:z = w1年龄 + w2票价 + …
- 增加二次项后,模型可以学习非线性关系:
z = w1*年龄 + w2*票价 + w3*年龄² + w4*票价² + w5*年龄×票价 + ... - 这能让模型捕捉更复杂的模式,比如:
- 年龄和生存率可能是U型关系(小孩和老人存活率高)
- 票价和年龄的交互效应(富有的小孩存活率特别高)
第二部分:标准化原始特征
# 对原始特征也进行标准化
# 例如:年龄 = (原始年龄 - 年龄均值) / 年龄标准差
为什么要标准化?
- 加速收敛:所有特征在相似尺度上,梯度下降更稳定
- 避免某些特征主导:如果不标准化,数值大的特征(如票价0-500)会比数值小的特征(如兄弟姐妹数0-8)对结果影响大得多
- 提高数值稳定性
优化效果对比
- 原始特征:训练集80%,验证集83%
- 增加二次项后:训练集82%,验证集87%
- 提升原因:模型有了更强的表达能力,能捕捉数据中更复杂的模式
总结:完整流程理解
- 训练时:计算损失 → 反向传播 → 更新参数
- 验证时:只做前向传播 → 计算准确率 → 不更新参数
- 特征工程:通过增加二次项,让简单模型也能学习复杂关系
关键区别记忆:
- 训练:要梯度,要更新参数,目的是学习
- 验证/预测:不要梯度,不更新参数,目的是评估
- torch.no_grad() 就是切换这两个模式的开关
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)