Kaggle平台Titanic项目完整实战

Titanic 问题是Kaggle 平台上的一个练手题目,要求使用数据分析的方法预测泰坦尼克号的幸存者名单。虽然这并不是一个有奖金的正式题目,仅供大家尝试和学习,但是由于数据简单整洁,又是典型的二分类问题,作为第一次实践是非常不错的题目。

1. Kaggle 平台介绍

Kaggle平台是著名的数据分析竞赛在线平台, 创始于2010 年。在Kaggle 平台上有各种有趣的数据挖掘挑战题目, 包括分类、预测、推荐等多种类型。全世界的科学家和数据分析师可以以组队或者solo (单挑) 的形式参与竞赛。

参赛者可以使用任何方法对题目给出的数据进行分析,并对测试集数据进行预测, 最后平台以预测准确率为标准判定参赛者的成绩。一些知名企业和机构也会与Kaggle平台合作, 将业务中的一些难题提出为数据挖掘任务, 并设立高额奖金, 让更多的专家参与解决, 比如针对高清卫星图的物体识别、产品推荐、超声图像识别、鱼类检测识别, 等等。

Kaggle 作为竞赛平台, 拥有一套比较完备的评分系统。参赛者的成绩一般以预测的准确率排名, 预测结果越准确则排名越高。参赛者每天的排名设立了公开排行榜( public leaderboard )和非公开排行榜( private leaderboard )。

在公开排行榜中,所有人随时都可以查看预测准确率和当前排名,不过这里对于准确率的计算并不是以全部测试数据作为基准,而只是测试集中的一部分数据,或者说可以认为是平台的验证集数据。公开榜可以让参赛者对自己算法的表现有一个初步评估,通过与他人的对比来看是否还有提升空间。为了防止参赛者利用多次提交的方法来试探测试集数据的结果,人肉对测试数据“过拟合”,竞赛最终成绩由另一份测试数据,也就是非公开排行榜的排名所决定的。

除了举办各种比赛之外, Kaggle 平台上还积累了各种有趣的数据集。其中被关注较多的有欧洲足球赛对战数据,在这份数据中囊括了超过25000 场比赛和超过10000 名球员的数据,其中被详尽记录(进球、角球、越位、红黄牌等)的比赛就超过1 万场,甚至有10 家博彩机构给出的投注赔率。通过这样的大数据不仅可以对各个球员或球队的历史进行多维度分析, 计算各种单项和综合实力,更能对未来的表现进行预测。这样丰富全面数据确实不可多得。

Kaggle 平台是一个可以让人们充分交流的地方,参赛者们不仅比拼技术,更能交流经验。即使只是翻看各个题目的讨论帖,也会觉得受益匪浅,例如题目会有数据图形化讨论专区,在里面会从各种维度给出数据的可视化视图。这对于初学者来说是非常好的学习途径。

2. Titanic 题目介绍

泰坦尼克号轮船(Titanic)的沉没是历史上最著名的海难事故之一。作为当时世界上最大的客运轮船, 它在处女航中由于撞上冰山导致轮船断裂,并最终沉入大西洋。当时船上共有2224 名乘客和船员, 其中1502 名丧生。在回顾这样一场浩劫的过程中, 分析发现造成如此巨大损失的原因之一是当时轮船上并没有准备足够的救生艇。在这起事件发生后, 除了推动建立了更合理的航行安全规范外,人们还分析了当时对于仅有的救生艇位置的分配过程, 虽然最后幸存是有一定的运气成分, 但一些乘客确实比另外一些更容易获救, 比如妇女、儿童,以及高级舱位的乘客。

本题目是希望重新站在客观的视角来重新回顾这一事件。通过分析事发时的数据使用机器学习的方法来预测哪些乘客最终能在这次灾难中幸存下来。更具体来说,是通过乘客的各项信息,如姓名、性别、年龄、乘船客舱等级等信息,尝试预测每位乘客幸存的概率。

既然是要预测每位乘客是否可以幸存,那么可以认为这是一个二分类问题, 即一类标签为存活,另一类标签为丧生。我们最终需要训练一个分类器,可以是SVM、神经网络、随机森林等模型,来判定样本所属的类别。

对于此类数据挖掘题目,对数据进行-些简单的分析和处理是必要的第一步。从Kaggle 网站下载本问题的数据集(https://www.kaggle.com/c/titanic/data)

可以看到,数据分为训练集和测试集两类,均以csv格式存储。训练集数据文件train.csv 大小为59.76KB,包含891 条数据,测试集数据文件test.csv 的大小为27.96KB,包含418 条数据。在数据文件中,每一行是一个样本,代表一名乘客的信息,包含以下12个数据集字段说明

  • PassengerId 乘客ID
  • Survived 是否幸存
  • Pclass 客舱客级
  • Name 乘客姓名
  • Sex 乘客性别
  • Age 乘客年龄
  • SibSp 兄弟姐妹和配偶在船数量
  • ParCh 父母孩子在船数量
  • Ticket 船票号
  • Fare船票价格
  • Cabin客舱位置
  • Embarked 登船港口的编号
    其中,“Survived” 字段代表该名乘客最终是评生还,是样本的标签字段。

展示了前10 条训练、数据的内容:

最后需要输出的结果包含两列,分别为测试集乘客ID 和对应乘客是否幸存预测。

3. 对数据进行-些初步推断

通过观察题目给定的条件,我们可以根据-些生活常识和对Titanic 事件的了解,对数据进行-些初步推断。

比如我们看过《泰坦尼克号》电影的都知道,事故发生在寒冷的海上冰山,如果在轮船沉没前登上了为数不多的救生船,则基本可以幸存,否则落入寒冷的大西洋就-定是凶多吉少,就像电影中的男主人公Jack 一样。
那么在这样一种情况下,都有哪些人登上了救生船呢?

  • 首先是妇女和儿童,轮船上大部分人为英国人,英国是讲究绅士风度的国家,不管是船员还是乘客都会一致认为应该让妇女和儿童先登上救生船, 也因此妇女儿童幸存的比例会比成年男性要高。
  • 其次高等客舱的乘客基本是贵族、军官等有身份地位的人, 他们在事故发生后也会得到额外照顾,获得优先登上救生船的资格。
  • 剩下的其他人就没有那么幸运了, 幸存概率比较低。这些先验知识, 在后续的判定中将起到非常大的作用。

对于我们认为相关性较高的字段, 比如客舱等级“Pclass”、乘客性别“Sex”、乘客年龄“Age”, 将在之后作为主要的特征字段, 需要经过正规化( Normalization )处理后转换为数值形式。
在本例中, 正规化主要有三个方面,

  • 将字符串字段转换为数值化的表达方式. 比如将性别字段原本取值“male” 和“female” 分别转换为0 和l ,这样才能作为分类器的输入;
  • 可以将数值都归-化到[O, 1] 的取值范围内。比如年龄字段原本的值域是 (O, 100), 归-化过程可以是将每个值都除以100 ;
  • 补齐缺失数据。

除了我们认为的关键字段以外,其他字段可能也包含很多有用的信息,部分可以经过挖掘转换而作为特征使用。对于一些现实生活中的复杂问题.人类的认知也可能是片面的、不准确的,真实的相关性可能隐藏在其他数据中,所以抓取更多数据是惯用的思路,但同时会随之出现维度灾难( curse of dimensionality )的问题,也就是维度过多、无效的噪声过多,导致无法有效地从中过滤出真正有用的信息。在本例中,如姓名 “Name” 、父母子女在船舱数量”ParCh”、客舱位置“Cabin”、登船码头编号”Embarded”等几个字段,猛一看似乎与能否逃生并没有点大的直接关系,但若经过挖掘也许能有意外的收获。

3.1. 解决 Titanic 问题的主要思路

先采用正规化操作等手段对原始数掘进行预处理,然后挑选特征向量的维度,并以此训练一个分类器,最终使用训练好的分类器来预测测试集数据的结果。

接下来,就可以进入真正的编码解题阶段,作为首次尝试,无须追求一步到位地做到最好,而应该先以简单规则得出基线版本,然后不断优化以得到更好的结果。

因此,我们首先尝试用 TensorFlow 训练一个逻辑回归分类器来看看效果如何。

整个代码可以分为:数据读入及预处理,构建计算图,构建训练迭代过程,执行训练、存储模型,预测测试数据结果几个部分。

4. 数据读入及预处理

对数据进行预处理会使用到 pandas 和 scikit-learn 库所提供的一些功能。

首先,使用 pandas 内置的数据文件解析器读入数据。pandas 内置了多种解析器,可以直接处理csv 、pickle 、json 、excel 、html等常用的数据文件格式,甚至还可以从 MySQL 数据库或者操作系统的剪切板中读入数据。
读入操作非常简单,只需要用 read_csv() 函数读取 train.csv 文件,读入的数据为一个 DataFrame 类型的对象。

import pandas as pd
# 从csv 文件读入数据
data = pd.read_csv('data/train.csv')

4.1. 查看数据的概况

DataFrame 是-个类似于电子表格的二维数据姑构,长度可变,维度可变,类型亦可变。 在 DataFrame中,行列都经过排序编号。

可以通过 DataFrame.info() 方法查看数据的概况:

data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 891 entries, 0 to 890
Data columns (total 12 columns):
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
dtypes: float64(2), int64(5), object(5)
memory usage: 83.6+ KB

可以看到:

  • 数据包含 891 条记录,下标编号为 0 到 890。
  • 包含12个列
  • PassangerId、Survived、Pclass、Age、SibSp、Parch、Fare 几个字段为数值类型(64 位整形或64 位浮点型)
  • Name 、Sex 、Ticket 、Cabin 、Embarked 字段是对象,其实也就是字符串类型。
  • Age、Cabin、Embarked 二个字段存在缺失的情况, Age字段只有 714 个有效值,缺失177 个; Embarked 有889个值,缺失2个; Cabin 宇段仅有204 个值,缺失达到687 个。

4.2. 正规化处理

为了方便处理,我们仅保留Sex, Age、Pclass、SibSp 、Parch、Fare 这 6 个字段。

然后对各字段做正规化处理。具体来说,要将 Sex 字段的字符串进行转换:将 “male” 替换为 0 ,将“female” 替换为1,同时,将Age 字段有缺失的部分统一赋值为O。

# fill nan values with 0
data = data.fillna(0)
# convert ['male', 'female'] values of Sex to [1, 0]
data['Sex'] = data['Sex'].apply(lambda s: 1 if s == 'male' else 0)
# select features and labels for training
dataset_X = data[['Sex', 'Age', 'Pclass', 'SibSp', 'Parch', 'Fare']].as_matrix()

4.3. 独热编码

独热编码(one-hot encoding )又称为一位有效编码,是分类标签常用的编码方式,具体来说,就是使用 N 位布尔型的状态标识来对 N 种状态进行编码,任意时刻编码中只有一位有效,即取值为 True

在本例中,Survived 是幸存一类的标签,新建一个 Deceased(死亡) 字段来表示乘客是否死亡,其取值为Survived 取非。这样 Survived 与 Deceased 两个字段一起相当于构成了一组one-hot 编码。

# 'Survived' is the label of one class,
# add 'Deceased' as the other class
data['Deceased'] = data['Survived'].apply(lambda s: 1 - s)
dataset_Y = data[['Deceased', 'Survived']].as_matrix()

4.4. 切分数据集

为了防止训练过拟舍,我们将仅有的标记数据分成训练数据集(training dataset)和验证数据集(validation dataset)两类。验证数据不参与模型训练,样本数占全部标记数据的20%。

scikit-learn 库中提供了用于切分数据集的工具函数 train_ test_ split,随机打乱数据后按比例拆分数据集。

from sklearn.cross_validation import train_test_split
# split training data and validation set data
X_train, X_val, y_train, y_val = train_test_split(dataset_X, dataset_Y,
                                                  test_size=0.2,
                                                  random_state=42)

5. 构建计算图

5.1. 逻辑回归

逻辑回归是形式最简单,并且最容易理解的分类器之一。从数学上逻辑回归的预测函数可以表示为如下公式:

其中 x 为输入向量,是大小为 d×1 的列向量,
d 是特征数,
W 是大小为 c × d 的 权重矩阵,
c 是分类 类别数目,
b 是偏值向量,为c × 1 列向量。

softmax 指一种归-化指数函数,它将一个 k 维的向量 z 按照公式

将向量中的元素转换为(0,1)的区间,常使用这种方法将类似判别函数的置信度值转换为概率形式(如判别超平面的距离等)。

softmax函数常用于输出层,用于指定唯一的分类输出。

权重矩阵 W 和偏置向量 b 是模型中的参数,也就是要通过训练来求得的部分。

5.2. TensorFlow 构建步骤

5.3. 使用 placeholder 声明输入占位符

TensorFlow 设计了数据 Feed 机制,也就是说计算程序并不会直接交互执行,而是在声明过程只做计算图的构建。真正计算时,才用数据替换占位符。

声明占位符 placeholder 需要给定三个参数,分别是输入数据的元素类型 dtype、维度形状 shape 和占位符名称标识 name。

  • TensorFlow内置的所有标准数据类型包括 int16 、uint16 、int32、uint32 、floatl6、float32、string 等,在指定元素类型时可以直接使用。
  • 维度形状使用数组指定,若 shape 不指定,默认为None ,表示任意形状。需要注意是,通过 mini-batch 批量训练是如今常用优化技巧,通常能在更短时间内得到更好的拟合效果。所以在定义输入形状时, 一般将第一个维度作为 mini-batch 维度,而从第二个维度开始才是 特征维度。
  • 占位符名称 name 用于区分计图里的各个节点,不管在查找节点,还是在可视化方面,设置容易识别的名称都非常有帮助。名称参数 name 默认也为None ,系统会自功将节点设置为类似 “Placeholder:O ” 这样的名称。

本例中, 对于模型来说有两个输入数据,一个为特征数据X, 由 Sex、Age、Pclass、SibSp、Parch、Fare 这6 个字段的值组成,另一个为标记值 y ,由 Deceased、Survived两个字段组成。

# create symbolic variables
X = tf.placeholder(tf.float32, shape=[None, 6])
y = tf.placeholder(tf.float32, shape=[None, 2])

5.4. . 声明参数变量

逻辑回归模型中包含两个参数:- 权重矩阵 W 和偏置向量 b。TensorFlow 使用变量( Variable)来存储和更新这些参数的值。

  • 变量的声明方式是 直接定义 tf.Variable()对象。

  • 初始化变量对象有两种方式, 一种是通过参数指定初始值,一种是从protocol buffer 结构 VariableDef 中反序列化。

  • 初始值必须是一个tensor对象, 或是可以通过 convert_to_tensor方法 把 Python 对象转换成tensor。

  • TensorFlow 提供了多种构造随机tensor 的方法,可以构造全零tensor 、随机正态分布tensor 等。定义变量会保留初始值的维度形状。

值得注意的是,变量Variable 的构造函数同样也没有做数据的初始化,而是在计算图中加入了一个variable 算子和其对应的 assign 算子。

# weights and bias are the variables to be trained
weights = tf.Variable(tf.random_normal([6, 2]), name='weights')
bias = tf.Variable(tf.zeros([2]), name='bias')

5.5. 构造前向传播计算图

所谓前向传播就是网络正向计算,由输入计算出标签的过程。

逻辑回归的公式用 TensorFlow 表示只需要一行代码:

y_pred = tf.nn.softmax(tf.matmul(X, weights) + bias)

其中, tf.matmul 是矩阵乘法算子,
tf.nn.softmax()是softmax 函数,
对于偏置向量 bias,可以直接用加号“+”完成矩阵相加操作

5.6. 声明代价函数

机器学习算法的优化需要靠代价函数来评估优化方向。本例的二分类问题一般使用交叉熵 (cross entropy)作为代价函数。
交叉熵计算公式为:

程序代码为:

# Minimise cost using cross entropy
# NOTE: add a epsilon(1e-10) when calculate log(y_pred),
# otherwise the result will be -inf
cross_entropy = - tf.reduce_sum(y * tf.log(y_pred + 1e-10),
                                reduction_indices=1)
cost = tf.reduce_mean(cross_entropy)

值得注意的是,在计算交叉熵的时候,对模型输出值 y_pred 加上了一个很小的误差值(在上面程序中是 1e-10),
这是因为当 y_pred 十分接近真值 y_true 的时候,也就是y_pred 的值非常接近 0 或1 时, 计算log(0)会得到负无穷 -inf,
从而导致输出非法,全部都是 nan ,并进一步导致无法计算梯度, 迭代陷入崩溃。

要解决这个问题有三种办法:

  • 在计算 log() 时, 直接加入一个极小的误差值,使计算合法。这样可以避免计算log(0),但存在的问题是,加入误差后相当于y_pred 的值会突破 1。在示例代码中使用了这种方案;
  • 使用 clip() 函数, 当 y_pred 接近 0 时,将其赋值成为极小误差值。也就足将 y_pred 的取值范围限定在[10^-10, 1] 的范围内;
  • 当计算交叉熵的计算出现 nan 值时, 显式地将cost 设置为0。这种方式回避了log() 函数计算的问题,而是在最终的代价函数上进行容错处理。

5.7. 加入优化算法

TensorFlow 内置了多种经典的优化算法,如随机梯度下降算法(Stochastic Gradient Descent, SGD )、动量算法(Momentum)、Adagrad 算法、ADAM 算法、RMSProp算法,
另外还有在线学习算法 FTRL。

优化器内部会自动构建梯度计算和反向传播部分的计算图。

一般对于优化算法,最关键的参数是学习率( learning rate ),对于学习率的设置是一门技术。
同时,不同优化界法在不同问题上可能会有不同的收敛速度,在解决实际问题时可以做多种尝试。

# use gradient descent optimizer to minimize cost
train_op = tf.train.GradientDescentOptimizer(0.001).minimize(cost)

完成了计算图的定义,再接下来要构建训练迭代。

6. 构建训练迭代过程

在 TensorFlow 的设计中,前端编程语言要触发后端执行引擎开始计算,必须通过 Session 接口完成。
Session 对象负责将运行环境打包,并且管理运行时需要处理的变量、队列(queues) 、读取器(readers)等资源。

6.1. Session关闭方式

由于Session 中管理了上下文的各种资源,所以在计算执行结束后一定要关闭,以释放对资源的占用。

一般有两种使用方式:

  • 1.在声明后手动调用 Session.close()方法来关闭,一般会在大型应用中使用。
  • 2.Session 类重载了_enter()__exit_()两个方法,
    所以可以用Python的 with 语句将Session 作为上下文管理器( Context Manager )来操作,退出作用域时自动关闭对象。

6.2. Session.run

Session 启动后就正式进入了训练过程。

首先要做的是使用 tf.global_variables_initializer().run() 方法初始化所有的变量。

接下来就是用一个循环将训练数据反复带入计算图来执行迭代。在循环内,Session.run()是触发后端执行的入口。

Session.run()有两个关键的参数, fetchesfeed_dict, 其中,

  • fetches 指定需要被计算的节点,可以用数组同时制定多个节点。
    节点可以是算子 op,比如前文程序中的优化算子train_op ,也可以是tensor,比如代表代价函数值的 cost。-

执行会从输入节点什始, 按照节点的依赖关系,也就是有向图的拓扑序,依次计算目标节点所在的子图中的所有节点。

  • 计算所需要的输入数据则 由feed_dict 代入。feed_dict 需要传入一个字典,字典的 key 是输入占位符placeholder , value 为真实的输入数据。

6.3. 返回结果

Session.run()执行完计算后,会返回计算节点的结果。
若节点为算子,则没有返回值,
若节点是tensor , 则返回当前的值。

比如最常见的优化算子和代价函数tensor的组合,而优化算子的执行结果其实是通过梯度下降算法更新所有参数变量,不需要返回值,而在前向传播计算得到的代价值将会返回。

通过将每一轮迭代的代价值打印出来,可以监控训练的收敛情况。

with tf.Session() as sess:
    # variables have to be initialized at the first place
    tf.global_variables_initializer().run()

    # training loop
    for epoch in range(10):
        total_loss = 0.
        for i in range(len(X_train)):
            # prepare feed data and run
            feed_dict = {X: [X_train[i]], y: [y_train[i]]}
            _, loss = sess.run([train_op, cost], feed_dict=feed_dict)
            total_loss += loss
        # display loss per epoch
        print('Epoch: %04d, total loss=%.9f' % (epoch + 1, total_loss))

6.4. 评估模型

训练迭代循环执行结束后,用验证数据集评估模型的表现。

# calculate accuracy
correct_pred = tf.equal(tf.argmax(y, 1), tf.argmax(y_pred, 1))
acc_op = tf.reduce_mean(tf.cast(correct_pred, tf.float32))

# Accuracy calculated by TensorFlow
    accuracy = sess.run(acc_op, feed_dict={X: X_val, y: y_val})
    print("Accuracy on validation set: %.9f" % accuracy)

    # Accuracy calculated by NumPy
    pred = sess.run(y_pred, feed_dict={X: X_val})
    correct = np.equal(np.argmax(pred, 1), np.argmax(y_val, 1))
    numpy_accuracy = np.mean(correct.astype(np.float32))
    print("Accuracy on validation set (numpy): %.9f" % numpy_accuracy)

从上面代码中可以看到,若只计算模型预测值 y_pred, 则只需要传入占位符 X 所对应的数据即可,不需要指定另一个占位符 y。

TensorFlow 在计算时会进行图的剪枝优化,只会计算 fetches 指定的子图,因此也只需要代入子图包含的占位符。在这一点上,TensorFlow 保证了编程的灵前性和执行效率, 在使用时不用担心会执行不必要的计算。

7. 执行训练

代码保存为 Titanic_basic.py 文件, 通过命令行运行程序即可看到的输出。

Epoch: 0001, total loss=5404.497672466
Epoch: 0002, total loss=4828.810187879
Epoch: 0003, total loss=1947.525306458
Epoch: 0004, total loss=1252.168087578
Epoch: 0005, total loss=1539.618039026
Epoch: 0006, total loss=1487.082866493
Epoch: 0007, total loss=1442.661438188
Epoch: 0008, total loss=1160.806707235
Epoch: 0009, total loss=1306.904193033
Epoch: 0010, total loss=1151.198464708
Accuracy on validation set: 0.581005573
Accuracy on validation set (numpy): 0.581005573

可以看到,通过 10 轮迭代的训练, 代价值从5404下降到 1151 ,这代表模型对训练数据的拟合越来越好, 误差越来越小。
同时还可以看到, 在验证数据集上,本次训练的预测正确率为58%。

8. 执行程序 常见的错误

8.1. 变量未初始化

开启session 后的第一步就需要初始化所有变量, 否则在执行其他操作时将会产生如下错误:

FailedPreconditionError: Attempting to use uninitialized value variable

解决方法是在 Session 对象初始化后,紧接着执行 tf.global_variables_initializer().run()

8.2. 真实数据与占位符形状不匹配

如果在执行时出现类似下面这样的错误:

ValueError : Cannot feed value of shape (2,) for Tensor u'Placeholder_l:O', which has shape '(?, 2)'

那一般是从feed_dict 代入的真实数据的数组形状与占位符不匹配导致的。在确认传入数据无误的情况下, 可以使用numpy.reshape()将数据调整成需要的形状后再代入计算。

8.3. Session 己关闭

若在Session 作用域之外执行运算,就会报出如下错误:

RuntimeError: Attempted to use a closed Session.

这时候就要检查确认Session.run()语句是在Session 对象生命周期作用域内执行的。

9. 存储和加载模型参数

训练是一件十分费时耗力的事情,如果每次预测之前都还要训练,那显然是不现实的。正确的姿势是在训练得到一组优秀的参数时将其保存下来, 预测时直接加载到模型中使用。

TensorFlow 当然也满足了这一需求,使用的是 tf.train.Savercheckpoint机制。

变量的存储和读取是通过 tf.train.Saver 类来完成的。Saver 对象在初始化时,为计算图加入了用于存储和加载变量的算子, 并可以通过参数指定是要存储哪些变量。

Saver 对象的 save()restore()方法是触发图中算子的入口。

Checkpoints 是用于存储变量的二进制文件,在其内部使用字典结构存储变量,键为变量名字,即Variable.name成员的值。

TesnorFlow 代码中提供了一个工具程序可以用于查看 checkpoint 文件的内容,代码在 tensorflow /python/tools/inspect_checkpoint.py

Saver 最简单的用法就如下面代码所示。

vl = tf.Variable(tf.zeros([200]))
saver = tf.train.Saver()
# 在Saver 之后声明的变量将不会被Saver 处理
v2 = tf.Variable(tf.ones[lOO]))
# 训练Session 创建参数存档
with tf.Session() as sessl:
  # 完成模型训练过程

  # 持久化存储变罩
  save_path = saver.save(sessl, "model.ckpt")

# 在新Session 加载存档
with tf.Session() as sess2:
  # 加载变量
  saver.restore(sess2,"model.ckpt")
  # 判别预测,或继续训练

需要注意的是, Saver 对象在初始化时,若不指定变量列表,默认只会自动收集其声明之前的所有变量,在Saver 对象初始化后的所有变量将不被记录,
上面代码中的 v2 变量就不会在存取的范围内。

这样的机制在迁移学习的应用中非常有用,例如要将一个基于ImageNet 数据训练好的CNN 应用在新类型图片识别上, 只需要加载模型卷积部分的参数,重新训练最后的全连接网络即可。

9.1. 存储操作会生成4 个文件

Saver.save() 触发的存储操作会生成4 个文件:

  • 第一个是名为 “model.ckpt” 的文件,这个文件是真实存储变量及其取值的文件。
  • 第二个是名为 “model.ckpt.meta” 的描述文件,在这个文件存储的是 MetaGraphDef 结构的对象经过二进制序列化后的内容。MetaGraphDef 结构由 Protocol buffer 定义, 其中包含了整个计算图的描述、各个变量定义的声明、输入管道的形式,以及其他相关信息。
  • meta文件可以在没有计算图声明代码的情况载入模型,而若在应用时还有原始的Python程序代码,程序就已经可以重新构建计算图的基本信息,则加载只需要 “model.ckpt”一个文件即可,

  • 第三个文件是以 “model.ckpt.index”为名称的文件.存储了变量在checkpoint 文件中的位置索引。

  • 最后-个是名为“checkpoint”的文件,这个文件存储了最新存档的文件路径。

模型存档有两种存取模式,除了上面示例展示的一次性存储之外,还有一种方式是通过引入迭代计数器的方式,按训练迭代轮次存储。
使用这种方式时,需要在save() 方法中指定当前迭代轮次,然后系统会自动生成带有测试的轮次和版本号的checkpoint 文件。基本使用方式如下面代码所示。

with tf.Session() as sess:
  for step in range(max_step):
    # 执行迭代计算
    # 下面命令将生成以’ logistic.ckpt ???'为文件名的checkpoint
    saver.save(sess, ckpt_dir + '/logistic.ckpt', global_step=global_step)

由于每一轮选代都会生成一组checkpoint,在执行上万次迭代的训练过程中很可能把硬盘存储空间耗尽。为了防止这种情况发生, Saver 提供了几种有效的防范措施。

  • 第-种是设置 max_to_keep 参数,此参数指定存储操作以更迭的方式只保留最后几个版本的checkpoint。默认情况下,只保留最后5个版本的存档。
  • 第二种是设置 keep_checkpoint_every_n_hours 参数,这种方式以时间为单位,每 n 个小时存储一个checkpoint。该参数默认值是10000 ,也就是每一万小时生成一个checkpoint。

对于带有版本的checkpoint 的加载有两种方法,

  • 一种是与之前的例子一样,直接指定名称前缀, 加载一个特定版本的checkpoint。
  • 另一种方式是利用名为“checkpoint”的文件,找到最新版本存档。
ckpt = tf.train.get_checkpoint_state(ckpt_dir)
if ckpt and ckpt.model_checkpoint_path:
    print('Restoring from checkpoint: %s' % ckpt.model_checkpoint_path)
    saver.restore(sess, ckpt.model_checkpoint_path)

10. 预测测试数据结果

作为最终的检验,训练完的模型需要对测试数据集进行预测,并提交至 Kaggle 平台验证预测准确率。

加载测试数据后执一遍正向传播计算:

从程序可以看到,执行正向传播计算,不需要代入标签数据,仅提供特征数据即可。
最终的结果按题目要求存储为csv 格式。在kaggle 提交后,可以看到平台给出的成绩:

考虑Titanic 问题本身的数据样本实在太少,逻辑回归模型又是最简单的模型, 得到这样的准确率可以说已经是不错的结果了。

# predict on test data
testdata = pd.read_csv('data/test.csv')
testdata = testdata.fillna(0)
# convert ['male', 'female'] values of Sex to [1, 0]
testdata['Sex'] = testdata['Sex'].apply(lambda s: 1 if s == 'male' else 0)
X_test = testdata[['Sex', 'Age', 'Pclass', 'SibSp', 'Parch', 'Fare']]

# predict on test set
predictions = np.argmax(sess.run(y_pred, feed_dict={X: X_test}), 1)
submission = pd.DataFrame({
    "PassengerId": testdata["PassengerId"],
    "Survived": predictions
})

submission.to_csv("titanic-submission.csv", index=False)

11. TensorBoard 可视化

从工程实践角度来说,往往逻辑越复杂的时候,代码写得就越长,其中产生bug的概率就越高,所以通常以“千行代码bug率”这-指标来衡量程序员的专业程度。

一般使用TensorFlow 的场景是做深度学习算法的开发和应用,而深度学习算法大多是非常复杂又难以理解的,往往涉及许多生涩难懂的数学公式,这就使得开发难度变得很高。
不仅如此,机器学习算法普遍是以概率为基础的,存在一些不确定性, 所以每一次训练的结果都可能存在一定的波动。
当结果与预期存在一定偏差的时候,往往不能立刻定位是程序出错还是由于算法本身的随机性导致的,甚至有些错误代码还会引发正则化的效果,导致无法通过输出判断程序是否有bug,
这就进一步增大了程序调试的难度。

值得庆幸的是, TensorFlow 系统的开发者们也希望解决这个问题。现在他们可以借助TensorBoard 这一强大的可视化工具来帮助理解复杂的模型和检查实现中的错误。
相比其他深度学习系统而言,TensorFlow 在这一方面做得最为先进和人性化。

目前TensorBoard 可以展示几种数据:标量指标、因片、音频、计算图的有向图、参数变量的分布和直方图,可以用曲线图的方式显示损失代价等量化指标的变化过程,还可以展示必要的图片和音颇数据。

11.1. 记录事件数据

TensorBoard 的工作方式是启动-个Web 服务, 该服务进程从TensorFlow 程序执行所得的事件日志文件中读取概要数据,然后将数据在网页中绘制成可视化的图表,
概要数据主要包括以下几种类别:

  1. 标量数据、如准确率、代价损失值,使用 tf.summary.scalar 加入记录算子;
  2. 参数数据,如参数矩阵 weights 、偏偏置矩阵 bias, -般使用 `tf.summary.histogram 记录;
  3. 图像数据,用 tf.summary.image 加入记录算子;
  4. 音频数据,用 tf.summary.audio 加入记录算子;
  5. 计算图结构,在定义 tf.summary.FileWriter('./logs', sess.graph) 对象时自动记录

与其他算子一样,记录概要的节点由于没有被任何计算节点所依赖,所以并不会自动执行,需要手动通过 Session.run() 接口触发。
为了写起来更简便,tf.summary.merge_all 可以将所有概要操作合并成一个算子, 其执行的结果是经过protocol buffer 序列化后的tf.Surnmary 对象。

writer = tf.summary.FileWriter('./logs', sess.graph)
merged = tf.summary.merge_all()
sess.run([merged, acc_op],.....)
writer.add_summary(summary, epoch)  # Write summary

11.2. 启动TensorBorad 服务

TensorBoard 是一个完整的Python 应用,通过命令行启动Web。

tensorboard --logdir=./logs

命令行的 ‘logdir’ 参数是 tf.summary.FileWriter 写入事件日志文件的目录路径。注意这里指定的是目录,而不是文件。

TensorBoard 在加载数据时会按顺序读取一个目录下的所有TensorFlow 的事件文件, 这主要是因为当训练过程因为某些错误意外终止又被重新执行时,会生成多个事件文件,
直接读取一个目录就能将多个文件的记录过程不间断地展示出来。
另外, 如果指定的目录下有多个子目录的话, TensorBoard会把它们当作多次执行的记录,可以并行展示出来。这一般是在多次运行,比较不同参数的效果时使用。

TensorBoard 命令行中其他常用的参数边有:
–port 设置服务端口. 默认端口 6006
–event file 指定某-个特定的事件日志文件,
–reload interval 服务后台重新加载数据的间隔,默认为每120 秒。

本地服务启动后,可以在浏览器中打开 http://localhost:6006/ 进行访问。如果是长时间训练的话,一直保持浏览器页面打开,就能够持续看到程序执行的进程。

12. 本文对应的完整代码:

import os
import numpy as np
import pandas as pd
import tensorflow as tf
from sklearn.cross_validation import train_test_split

################
# Preparing Data
################

# read data from file
data = pd.read_csv('data/train.csv')

# fill nan values with 0
data = data.fillna(0)
# convert ['male', 'female'] values of Sex to [1, 0]
data['Sex'] = data['Sex'].apply(lambda s: 1 if s == 'male' else 0)
# 'Survived' is the label of one class,
# add 'Deceased' as the other class
data['Deceased'] = data['Survived'].apply(lambda s: 1 - s)

# select features and labels for training
dataset_X = data[['Sex', 'Age', 'Pclass', 'SibSp', 'Parch', 'Fare']].as_matrix()
dataset_Y = data[['Deceased', 'Survived']].as_matrix()

# split training data and validation set data
X_train, X_val, y_train, y_val = train_test_split(dataset_X, dataset_Y,
                                                  test_size=0.2,
                                                  random_state=42)

################
# Constructing Dataflow Graph
################

# arguments that can be set in command line
tf.app.flags.DEFINE_integer('epochs', 10, 'Training epochs')
tf.app.flags.DEFINE_integer('batch_size', 10, 'size of mini-batch')
FLAGS = tf.app.flags.FLAGS

with tf.name_scope('input'):
    # create symbolic variables
    X = tf.placeholder(tf.float32, shape=[None, 6])
    y_true = tf.placeholder(tf.float32, shape=[None, 2])

with tf.name_scope('classifier'):
    # weights and bias are the variables to be trained
    weights = tf.Variable(tf.random_normal([6, 2]))
    bias = tf.Variable(tf.zeros([2]))
    y_pred = tf.nn.softmax(tf.matmul(X, weights) + bias)

    # add histogram summaries for weights, view on tensorboard
    tf.summary.histogram('weights', weights)
    tf.summary.histogram('bias', bias)

# Minimise cost using cross entropy
# NOTE: add a epsilon(1e-10) when calculate log(y_pred),
# otherwise the result will be -inf
with tf.name_scope('cost'):
    cross_entropy = - tf.reduce_sum(y_true * tf.log(y_pred + 1e-10),
                                    reduction_indices=1)
    cost = tf.reduce_mean(cross_entropy)
    tf.summary.scalar('loss', cost)

# use gradient descent optimizer to minimize cost
train_op = tf.train.GradientDescentOptimizer(0.001).minimize(cost)

with tf.name_scope('accuracy'):
    correct_pred = tf.equal(tf.argmax(y_true, 1), tf.argmax(y_pred, 1))
    acc_op = tf.reduce_mean(tf.cast(correct_pred, tf.float32))
    # Add scalar summary for accuracy
    tf.summary.scalar('accuracy', acc_op)

global_step = tf.Variable(0, name='global_step', trainable=False)
# use saver to save and restore model
saver = tf.train.Saver()

# this variable won't be stored, since it is declared after tf.train.Saver()
non_storable_variable = tf.Variable(777)

ckpt_dir = './ckpt_dir'
if not os.path.exists(ckpt_dir):
    os.makedirs(ckpt_dir)

################
# Training the model
################

# use session to run the calculation
with tf.Session() as sess:
    # create a log writer. run 'tensorboard --logdir=./logs'
    writer = tf.summary.FileWriter('./logs', sess.graph)
    merged = tf.summary.merge_all()

    # variables have to be initialized at the first place
    tf.global_variables_initializer().run()

    # restore variables from checkpoint if exists
    ckpt = tf.train.get_checkpoint_state(ckpt_dir)
    if ckpt and ckpt.model_checkpoint_path:
        print('Restoring from checkpoint: %s' % ckpt.model_checkpoint_path)
        saver.restore(sess, ckpt.model_checkpoint_path)

    start = global_step.eval()
    # training loop
    for epoch in range(start, start + FLAGS.epochs):
        total_loss = 0.
        for i in range(0, len(X_train), FLAGS.batch_size):
            # train with mini-batch
            feed_dict = {
                X: X_train[i: i + FLAGS.batch_size],
                y_true: y_train[i: i + FLAGS.batch_size]
            }
            _, loss = sess.run([train_op, cost], feed_dict=feed_dict)
            total_loss += loss
        # display loss per epoch
        print('Epoch: %04d, loss=%.9f' % (epoch + 1, total_loss))

        summary, accuracy = sess.run([merged, acc_op],
                                     feed_dict={X: X_val, y_true: y_val})
        writer.add_summary(summary, epoch)  # Write summary
        print('Accuracy on validation set: %.9f' % accuracy)

        # set and update(eval) global_step with epoch
        global_step.assign(epoch).eval()
        saver.save(sess, ckpt_dir + '/logistic.ckpt',
                   global_step=global_step)
    print('Training complete!')

################
# Evaluating on the test set
################

# restore variables and run prediction in another session
with tf.Session() as sess:
    # restore variables from checkpoint if exists
    ckpt = tf.train.get_checkpoint_state(ckpt_dir)
    if ckpt and ckpt.model_checkpoint_path:
        print('Restoring from checkpoint: %s' % ckpt.model_checkpoint_path)
        saver.restore(sess, ckpt.model_checkpoint_path)

    # predict on test data
    testdata = pd.read_csv('data/test.csv')
    testdata = testdata.fillna(0)
    # convert ['male', 'female'] values of Sex to [1, 0]
    testdata['Sex'] = testdata['Sex'].apply(lambda s: 1 if s == 'male' else 0)
    X_test = testdata[['Sex', 'Age', 'Pclass', 'SibSp', 'Parch', 'Fare']]

    # predict on test set
    predictions = np.argmax(sess.run(y_pred, feed_dict={X: X_test}), 1)
    submission = pd.DataFrame({
        "PassengerId": testdata["PassengerId"],
        "Survived": predictions
    })

    submission.to_csv("titanic-submission.csv", index=False)

13. 参考文献


技术交流学习,请加QQ微信:631531977
目录