基础分析代码

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的值,让模型预测更准。这个过程分三步:

  1. 前向传播(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实现的思维转变:

  1. 数学公式 → Python类:把 z = wx + b 写成一个类,让PyTorch管理w和b
  2. 手动计算梯度 → 自动计算:不用自己推导梯度公式,loss.backward() 自动搞定
  3. 手动更新参数 → 优化器更新:不用写 w = w - lr * grad,optimizer.step() 自动更新
  4. 单个样本 → 批量处理: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 目的是学习参数 目的是评估性能

完整验证流程:

  1. 关闭梯度计算:with torch.no_grad():
  2. 加载验证数据:从DataLoader获取批次
  3. 前向传播:outputs = model(features) 得到预测概率
  4. 计算正确率:比较 (outputs >= 0.5) 和真实标签
  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型关系(小孩和老人存活率高)
    • 票价和年龄的交互效应(富有的小孩存活率特别高)

第二部分:标准化原始特征

# 对原始特征也进行标准化
# 例如:年龄 = (原始年龄 - 年龄均值) / 年龄标准差

为什么要标准化?

  1. 加速收敛:所有特征在相似尺度上,梯度下降更稳定
  2. 避免某些特征主导:如果不标准化,数值大的特征(如票价0-500)会比数值小的特征(如兄弟姐妹数0-8)对结果影响大得多
  3. 提高数值稳定性

优化效果对比

  • 原始特征:训练集80%,验证集83%
  • 增加二次项后:训练集82%,验证集87%
  • 提升原因:模型有了更强的表达能力,能捕捉数据中更复杂的模式

总结:完整流程理解

  • 训练时:计算损失 → 反向传播 → 更新参数
  • 验证时:只做前向传播 → 计算准确率 → 不更新参数
  • 特征工程:通过增加二次项,让简单模型也能学习复杂关系

关键区别记忆:

  • 训练:要梯度,要更新参数,目的是学习
  • 验证/预测:不要梯度,不更新参数,目的是评估
  • torch.no_grad() 就是切换这两个模式的开关
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐