TensorFlow2 结构化数据建模全流程范例:基于 titanic 生存预测的实战教程

原创2026-09-23 12:21:591,621 阅读
文章标签:教程深度学习机器学习

TensorFlow2 结构化数据建模全流程范例:基于 titanic 生存预测的实战教程

本文以《30天吃掉那只TensorFlow2》第一章的第一个实战范例为蓝本,完整演示使用 TensorFlow2 + Keras 对**结构化数据(表格数据)**进行建模的六步标准流程:准备数据、定义模型、训练模型、评估模型、使用模型、保存模型。读者学完后将掌握 Pandas 特征工程、Sequential 模型构建、compile/fit 训练、指标评估以及 Keras 与 SavedModel 两种模型保存方案,可直接迁移到自己的表格类机器学习项目中。

结构化数据是工业界最常见的数据形态(如 CRM 表、订单表、风控特征表),而 Titanic 生存预测正是这类问题最经典的入门案例:根据乘客的个人信息预测其在泰坦尼克号撞上冰山沉没后能否幸存。本项目完整的六步建模流程定义可见 一、TensorFlow的建模流程,本范例的配套数据存放在 data/titanic/ 目录(train.csv 共 712 条训练样本,test.csv 共 179 条测试样本,含表头各多一行)。

一,准备数据:DataFrame 读取与探索性分析

结构化数据一般使用 Pandas 中的 DataFrame 进行预处理,这也是全流程中对新手来说最困难、最需要打磨的环节。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import tensorflow as tf
from tensorflow.keras import models, layers

dftrain_raw = pd.read_csv('./data/titanic/train.csv')
dftest_raw = pd.read_csv('./data/titanic/test.csv')
dftrain_raw.head(10)

titanic 训练数据集前 10 行展示,包含 Pclass、Sex、Age、SibSp、Parch、Ticket、Fare、Cabin、Embarked 等原始字段

字段含义与处理方案

原数据共 11 个字段,它们的类型与建模策略如下,这也是整个特征工程的蓝图:

字段 含义 类型 处理方案
Survived 0 代表死亡,1 代表存活 y 标签 直接作为监督信号
Pclass 乘客所持票类 类别 三种取值(1,2,3),转换成 onehot 编码
Name 乘客姓名 文本 舍去
Sex 乘客性别 类别 转换成 bool 特征
Age 乘客年龄 数值 有缺失;作为数值特征,并添加"年龄是否缺失"辅助特征
SibSp 乘客兄弟姐妹/配偶个数 数值 整数值特征
Parch 乘客父母/孩子个数 数值 整数值特征
Ticket 票号 文本 舍去
Fare 票价 数值 浮点数,0-500 不等
Cabin 所在船舱 类别 有缺失;添加"所在船舱是否缺失"辅助特征
Embarked 登船港口 类别 取值 S、C、Q,有缺失;转成四维度 onehot(S,C,Q,nan)

从字段方案可以看出结构化建模的核心思想:类别字段 onehot 化、数值字段直接保留、低信息量的文本字段丢弃、缺失信息本身也编码成特征(Age_null、Cabin_null),避免在填充或丢弃时丢失"是否缺失"这一信息。

EDA:探索性数据分析

利用 Pandas 自带的可视化能力,可以快速完成 EDA(Exploratory Data Analysis),对标签分布、特征分布、特征与标签的关系建立直觉。

1)label 分布情况

%matplotlib inline
%config InlineBackend.figure_format = 'png'
ax = dftrain_raw['Survived'].value_counts().plot(kind = 'bar',
     figsize = (12,8),fontsize=15,rot = 0)
ax.set_ylabel('Counts',fontsize = 15)
ax.set_xlabel('Survived',fontsize = 15)
plt.show()

2)年龄分布情况

%matplotlib inline
%config InlineBackend.figure_format = 'png'
ax = dftrain_raw['Age'].plot(kind = 'hist',bins = 20,color= 'purple',
                    figsize = (12,8),fontsize=15)
ax.set_ylabel('Frequency',fontsize = 15)
ax.set_xlabel('Age',fontsize = 15)
plt.show()

titanic 乘客年龄直方图与 Survived 标签柱状图等 EDA 可视化输出

3)年龄和 label 的相关性

%matplotlib inline
%config InlineBackend.figure_format = 'png'
ax = dftrain_raw.query('Survived == 0')['Age'].plot(kind = 'density',
                      figsize = (12,8),fontsize=15)
dftrain_raw.query('Survived == 1')['Age'].plot(kind = 'density',
                      figsize = (12,8),fontsize=15)
ax.legend(['Survived==0','Survived==1'],fontsize = 12)
ax.set_ylabel('Density',fontsize = 15)
ax.set_xlabel('Age',fontsize = 15)
plt.show()

按 Survived 分组后的年龄核密度曲线对比,可观察存活者与未存活者的年龄分布差异

通过密度曲线叠加,可以直观观察到不同年龄段的存活差异,为后续建模提供先验判断。

正式数据预处理:特征工程函数

把字段处理方案固化为一个 preprocessing 函数,对训练集和测试集施加同一套转换,避免训练/测试不一致:

def preprocessing(dfdata):

    dfresult = pd.DataFrame()

    #Pclass
    dfPclass = pd.get_dummies(dfdata['Pclass'])
    dfPclass.columns = ['Pclass_' +str(x) for x in dfPclass.columns ]
    dfresult = pd.concat([dfresult,dfPclass],axis = 1)

    #Sex
    dfSex = pd.get_dummies(dfdata['Sex'])
    dfresult = pd.concat([dfresult,dfSex],axis = 1)

    #Age
    dfresult['Age'] = dfdata['Age'].fillna(0)
    dfresult['Age_null'] = pd.isna(dfdata['Age']).astype('int32')

    #SibSp,Parch,Fare
    dfresult['SibSp'] = dfdata['SibSp']
    dfresult['Parch'] = dfdata['Parch']
    dfresult['Fare'] = dfdata['Fare']

    #Cabin
    dfresult['Cabin_null'] =  pd.isna(dfdata['Cabin']).astype('int32')

    #Embarked
    dfEmbarked = pd.get_dummies(dfdata['Embarked'],dummy_na=True)
    dfEmbarked.columns = ['Embarked_' + str(x) for x in dfEmbarked.columns]
    dfresult = pd.concat([dfresult,dfEmbarked],axis = 1)

    return(dfresult)

x_train = preprocessing(dftrain_raw)
y_train = dftrain_raw['Survived'].values

x_test = preprocessing(dftest_raw)
y_test = dftest_raw['Survived'].values

print("x_train.shape =", x_train.shape )
print("x_test.shape =", x_test.shape )
x_train.shape = (712, 15)
x_test.shape = (179, 15)

预处理后每行样本被转换为 15 维特征向量:Pclass 3 维 onehot + Sex 2 维 onehot + Age 数值 + Age_null 缺失标志 + SibSp + Parch + Fare + Cabin_null 缺失标志 + Embarked 4 维 onehot(含 nan 维),合计 3+2+1+1+1+1+1+1+4 = 15。注意 pd.get_dummies(..., dummy_na=True) 会把缺失值单独编成一维,这正是 Embarked 输出四维的原因。

二,定义模型:Sequential 按层顺序构建

使用 Keras 接口有以下 3 种方式构建模型:

  1. 使用 Sequential 按层顺序构建模型;
  2. 使用函数式 API 构建任意结构模型;
  3. 继承 Model 基类构建自定义模型。

三种方式的完整对比与适用场景可参考仓库章节 6-1,构建模型的3种方法:顺序结构优先用 Sequential;多输入/多输出、共享权重、残差连接等非顺序结构推荐函数式 API;Model 子类化灵活但出错概率更高。本例网络结构是简单的三层全连接,因此选择最简单的 Sequential:

tf.keras.backend.clear_session()

model = models.Sequential()
model.add(layers.Dense(20,activation = 'relu',input_shape=(15,)))
model.add(layers.Dense(10,activation = 'relu' ))
model.add(layers.Dense(1,activation = 'sigmoid' ))

model.summary()
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
dense (Dense)                (None, 20)                320
_________________________________________________________________
dense_1 (Dense)              (None, 10)                210
_________________________________________________________________
dense_2 (Dense)              (None, 1)                 11
=================================================================
Total params: 541
Trainable params: 541
Non-trainable params: 0
_________________________________________________________________

关键设计要点:

  • 输入维度:input_shape=(15,) 与预处理后的特征维度严格对应;
  • 隐藏层:20 与 10 个神经元,relu 激活缓解梯度消失;
  • 输出层:单个神经元 + sigmoid 激活,输出 (0,1) 区间概率,适配二分类;
  • 参数量自洽:第一层 15×20+20=320,第二层 20×10+10=210,输出层 10×1+1=11,合计 541 个可训练参数;
  • tf.keras.backend.clear_session() 用于清除之前的模型/层命名残留,保证每次运行命名空间干净,在 notebook 反复执行时尤其重要。

三,训练模型:内置 fit 方法

训练模型通常有 3 种方法:内置 fit 方法、内置 train_on_batch 方法、自定义训练循环,详见仓库章节 6-2,训练模型的3种方法。其中 fit 方法功能最强大,支持 numpy array、tf.data.Dataset 与 Python generator 三种数据源,且可通过回调函数实现复杂训练控制。本例选择最常用也最简单的内置 fit 方法:

# 二分类问题选择二元交叉熵损失函数
model.compile(optimizer='adam',
            loss='binary_crossentropy',
            metrics=['AUC'])

history = model.fit(x_train,y_train,
                    batch_size= 64,
                    epochs= 30,
                    validation_split=0.2 #分割一部分训练数据用于验证
                   )
Train on 569 samples, validate on 143 samples
Epoch 1/30
569/569 [==============================] - 1s 2ms/sample - loss: 3.5841 - AUC: 0.4079 - val_loss: 3.4429 - val_AUC: 0.4129
...
Epoch 30/30
569/569 [==============================] - 0s 80us/sample - loss: 0.5071 - AUC: 0.8349 - val_loss: 0.5816 - val_AUC: 0.7605

对核心配置的解读:

  • optimizer='adam':自适应学习率优化器,无需手动调节学习率,是表格数据建模的默认选择;
  • loss='binary_crossentropy':二分类问题的标准损失函数,配合输出层 sigmoid 使用;
  • metrics=['AUC']:以 AUC 作为监控指标。相比 accuracy,AUC 对类别不平衡更稳健,是二分类排序类任务(如风控、营销响应)的主流评估指标;
  • validation_split=0.2:从 712 条训练数据中自动切出 20% 作为验证集,即 train 569 / validate 143;
  • batch_size=64、epochs=30:每个 epoch 分 9 个 batch 左右完成前向-反向传播,重复 30 轮。

从训练日志可以观察到标准的学习曲线形态:训练 loss 从 3.58 稳步下降到 0.51,训练 AUC 从 0.41 上升到 0.83;验证 AUC 同步爬升到 0.76 附近后趋于平缓,说明模型在前 30 轮内已完成有效拟合且没有明显过拟合。

四,评估模型:训练曲线与测试集指标

首先评估模型在训练集和验证集上的动态效果。model.fit 返回的 history.history 记录了每个 epoch 的 loss 与 AUC,封装一个通用绘图函数即可观察训练曲线:

%matplotlib inline
%config InlineBackend.figure_format = 'svg'

import matplotlib.pyplot as plt

def plot_metric(history, metric):
    train_metrics = history.history[metric]
    val_metrics = history.history['val_'+metric]
    epochs = range(1, len(train_metrics) + 1)
    plt.plot(epochs, train_metrics, 'bo--')
    plt.plot(epochs, val_metrics, 'ro-')
    plt.title('Training and validation '+ metric)
    plt.xlabel("Epochs")
    plt.ylabel(metric)
    plt.legend(["train_"+metric, 'val_'+metric])
    plt.show()

plot_metric(history,"loss")

训练集与验证集 loss 随 epoch 变化的双曲线对比图

plot_metric(history,"AUC")

训练集与验证集 AUC 随 epoch 变化的双曲线对比图

两幅曲线图清晰展示:loss 单调下降、AUC 单调上升,且训练曲线与验证曲线间距不大,说明模型泛化良好。再进一步在测试集上评估(测试集不参与训练,是最可信的泛化指标):

model.evaluate(x = x_test,y = y_test)
[0.5191367897907448, 0.8122605]

返回值依次为测试集 loss 与 AUC:loss≈0.519,AUC≈0.812。测试集 AUC 达到 0.81 以上,对于仅 15 维特征、三层全连接的小模型而言,已能较好刻画乘客存活规律。

五,使用模型:概率预测与类别预测

训练好的模型可用于两种粒度的预测:输出连续的存活概率,或直接输出离散的类别。

# 预测概率
model.predict(x_test[0:10])
# model(tf.constant(x_test[0:10].values,dtype = tf.float32))  # 等价写法
array([[0.26501188],
       [0.40970832],
       [0.44285864],
       [0.78408605],
       [0.47650957],
       [0.43849158],
       [0.27426785],
       [0.5962582 ],
       [0.59476686],
       [0.17882936]], dtype=float32)
# 预测类别
model.predict_classes(x_test[0:10])
array([[0],
       [0],
       [0],
       [1],
       [0],
       [0],
       [0],
       [1],
       [1],
       [0]], dtype=int32)

注意 model(tf.constant(...)) 是调用模型对象的等价写法,因为 Keras 模型本身可调用(callable)。概率预测适用于需要排序或设置自定义阈值的场景(如风控评分卡),类别预测则直接给出 0/1 判定。

六,保存模型:Keras 方式与 TensorFlow 原生方式

模型保存有两种路线:Keras 方式和 TensorFlow 原生方式。前者仅适合在 Python 环境中恢复模型;后者保存的是标准 SavedModel 格式,可跨平台部署。官方推荐使用后一种。

1,Keras 方式保存

(1)保存模型结构及权重(单文件 .h5)

# 保存模型结构及权重
model.save('./data/keras_model.h5')

del model  #删除现有模型

# identical to the previous one
model = models.load_model('./data/keras_model.h5')
model.evaluate(x_test,y_test)
[0.5191367897907448, 0.8122605]

save + load_model 一步到位,恢复后的模型评估结果与保存前完全一致([0.519..., 0.812...]),证明结构、权重、编译配置均被完整保留。仓库 data/ 目录下的 keras_model.h5 即本步骤的产物。

(2)分别保存模型结构与权重

# 保存模型结构
json_str = model.to_json()

# 恢复模型结构
model_json = models.model_from_json(json_str)
# 保存模型权重
model.save_weights('./data/keras_model_weight.h5')

# 恢复模型结构
model_json = models.model_from_json(json_str)
model_json.compile(
        optimizer='adam',
        loss='binary_crossentropy',
        metrics=['AUC']
    )

# 加载权重
model_json.load_weights('./data/keras_model_weight.h5')
model_json.evaluate(x_test,y_test)
[0.5191367897907448, 0.8122605]

to_json() 只保存网络拓扑结构,save_weights 只保存权重张量,二者需配合使用:先用 model_from_json 恢复结构,再 compile(注意权重恢复后必须重新 compile 才能 evaluate),最后 load_weights 载入参数。仓库中的 data/keras_model_weight.h5 即权重文件产物。

2,TensorFlow 原生方式保存

(1)仅保存权重(Checkpoint 格式)

# 保存权重,该方式仅仅保存权重张量
model.save_weights('./data/tf_model_weights.ckpt',save_format = "tf")

该方式只保存权重张量,不包含模型结构,恢复时需要先重建结构再载入权重。仓库 data/ 下的 tf_model_weights.ckpt.data-00000-of-00001 与 tf_model_weights.ckpt.index 即其产物。

(2)保存完整模型(SavedModel 格式,跨平台部署)

# 保存模型结构与模型参数到文件,该方式保存的模型具有跨平台性便于部署
model.save('./data/tf_model_savedmodel', save_format="tf")
print('export saved model.')

model_loaded = tf.keras.models.load_model('./data/tf_model_savedmodel')
model_loaded.evaluate(x_test,y_test)
[0.5191365896656527, 0.8122605]

SavedModel 是 TensorFlow 的官方可移植格式,目录结构为 data/tf_model_savedmodel/:saved_model.pb 存放图结构,variables/ 目录存放权重分片(本仓库产物中含多个 variables.data-* 分片与 variables.index),assets/ 存放辅助资源(本例为 saved_model.json)。恢复后的评估结果为 [0.5191365896656527, 0.8122605],与保存前仅有浮点尾数级差异。

SavedModel 的价值在于跨平台:它不依赖 Python 运行时,可被 TensorFlow Serving(参见仓库章节 6-6,使用tensorflow-serving部署模型)、TensorFlow Lite、TensorFlow.js 以及 spark-scala(6-7,使用spark-scala调用tensorflow模型)等加载,是实现工业级模型落地的关键一步。

小结:结构化数据建模的通用范式

回顾全流程,本范例将结构化数据建模抽象为一条可复用的流水线:

  1. 准备数据:DataFrame 读取 → EDA 可视化洞察 → 统一预处理函数(类别 onehot、缺失标志、数值归一/保留)→ 得到数值矩阵;
  2. 定义模型:根据数据维度与任务类型选择 Sequential/函数式 API/Model 子类化;
  3. 训练模型:compile 配置优化器-损失-指标,fit 指定 batch、epochs、验证集划分;
  4. 评估模型:绘制训练曲线判断拟合状态,用 evaluate 在测试集上确认泛化指标;
  5. 使用模型:predict 输出概率,predict_classes 输出类别;
  6. 保存模型:开发期用 .h5 快速存取,上线期用 SavedModel 跨平台部署。

该流程与本仓库其余三个建模范例(图片数据见 1-2,图片数据建模流程范例、文本数据见 1-3,文本数据建模流程范例、时间序列数据见 1-4,时间序列数据建模流程范例)共同构成 TensorFlow2 建模方法论的第一章;后续张量、计算图、自动微分等底层概念可继续学习 二、TensorFlow的核心概念。本仓库全部代码基于 TensorFlow 2.1 版本在 Jupyter 环境测试通过,可直接按 README.md 的学习方案在本地或云端运行复现。

登录后查看全文
eat_tensorflow2_in_30_days