ml-tf-1x-merge-1

66 阅读51分钟

TensorFlow 1.x 机器学习(二)

原文:annas-archive.org/md5/1386ae5de8c0086da5a8cba927050ae7

译者:飞龙

协议:CC BY-NC-SA 4.0

第七章:利用机器学习赚钱

到目前为止,我们主要使用 TensorFlow 进行图像处理,并在较小程度上进行文本序列处理。在本章中,我们将处理一种特定类型的表格数据:时间序列数据。

时间序列数据来自许多领域,通常有一个共同点——唯一不断变化的字段是时间或序列字段。这在许多领域中都很常见,尤其是在经济学、金融、健康、医学、环境工程和控制工程中。我们将在本章中通过例子来深入探讨,但关键点是要记住顺序很重要。与前几章我们可以自由打乱数据不同,时间序列数据如果被随意打乱就会失去意义。一个额外的复杂性是数据本身的可获取性;如果我们拥有的数据仅限于当前时刻且没有进一步的历史数据可供获取,那么再多的数据收集也无法生成更多数据——你只能受到基于时间的可用性限制。

幸运的是,我们将深入探讨一个数据量庞大的领域:金融世界。我们将探索一些对冲基金和其他复杂投资者可能使用时间序列数据的方式。

本章将涵盖以下主题:

  • 什么是时间序列数据及其特殊属性

  • 投资公司在其定量和机器学习驱动的投资努力中可能使用的输入类型和方法

  • 金融时间序列数据及其获取方式;我们还将获取一些实时金融数据

  • 修改后的卷积神经网络在金融中的应用

输入和方法

投资公司的内部专有交易团队采用多种手段进行投资、交易和赚钱。相对不受监管的对冲基金使用更加广泛、更有趣和更复杂的投资手段。有些投资基于直觉或大量思考,另一些则主要基于过滤、算法或信号。两种方法都可以,但我们当然会关注后一种方法。

在定量方法中,有许多技术;其中一些如下:

  • 基于估值

  • 基于异常和信号

  • 基于外部信号

  • 基于过滤和分段的队列分析

其中一些方法将使用传统的机器学习技术,如 K-近邻算法、朴素贝叶斯和支持向量机。特别是队列分析,几乎非常适合 KNN 类型的方法。

另一个流行的技术是情感分析和基于群众情绪的信号。我们在上一章中讨论了这一点,当时我们通过分析文本情感,并将段落分类为基本类别:正面、负面、开心、生气等等。想象一下,如果我们收集了更多的数据,并过滤出所有涉及特定股票的数据,我们就能得到股票的情绪倾向。现在,想象一下如果我们拥有一种广泛的(可能是全球性的)、大流量且高速度的文本来源——其实你不需要想象,这一切在过去十年已经成为现实。Twitter 通过 API 提供他们的firehose数据,Facebook 也是,其他一些社交媒体平台也同样如此。事实上,一些对冲基金会消耗整个 Twitter 和 Facebook 的数据流,并试图从中提取关于股票、市场板块、商品等的公众情绪。然而,这是一种外部的基于自然语言处理(NLP)信号的投资策略,实践者们用它来预测时间序列的方向性和/或强度。

在本章中,我们将使用内部指标,利用时间序列本身来预测时间序列中的未来数据。预测实际的未来数据实际上是一项非常困难的任务,结果发现,这并不是完全必要的。通常,只需一个方向性的观点就足够了。方向性的观点与运动的强度结合起来会更好。

对于许多类型的投资,甚至连观点本身可能也不能给你完全的保证,平均来看,做得比错得稍多一些就足够了。想象一下每次投掷硬币下注一分钱——如果你能 51%的时间猜对,并且能够玩成千上万次,这可能足以让你盈利,因为你赚得比亏得多。

这一切对基于机器学习的努力来说是一个好兆头,虽然我们可能对我们的答案没有 100%的信心,但从统计上来看,我们可能具有很强的预测能力。归根结底,我们希望领先一步,因为即使是微小的优势,也能通过成千上万次的循环被放大,从而带来可观的利润。

获取数据

让我们首先获取一些数据。为了本章的目的,我们将使用 Quandl 的数据,Quandl 一直是技术精通的独立投资者的长期最爱。Quandl 通过多种机制提供许多股票的数据。一个简单的机制是通过 URL API。要获取例如 Google 股票的数据,我们可以点击www.quandl.com/api/v3/datasets/WIKI/GOOG/data.json。类似地,我们可以将GOOG替换为其他指数代码,以获取其他股票的数据。

通过 Python 自动化这个过程是相当容易的;我们将使用以下代码来实现:

import requests 

API_KEY = '<your_api_key>' 

start_date = '2010-01-01' 
end_date = '2015-01-01' 
order = 'asc' 
column_index = 4 

stock_exchange = 'WIKI' 
index = 'GOOG' 

data_specs = 'start_date={}&end_date={}&order={}&column_index={}&api_key={}' 
   .format(start_date, end_date, order, column_index, API_KEY) 
base_url = "https://www.quandl.com/api/v3/datasets/{}/{}/data.json?" + data_specs 
stock_data = requests.get(base_url.format(stock_exchange, index)).json()

因此,在这里,stock_data变量中将包含从 WIKI/GOOG 获取的股票数据,数据来源于格式化的 URL,日期范围为2010-01-012015-01-01column_index = 4变量告诉服务器仅获取历史数据中的收盘值。

请注意,你可以在你的 GitHub 仓库中找到本章的代码—(github.com/saifrahmed/MLwithTF/tree/master/book_code/chapter_07)。

那么,什么是这些收盘值呢?股票价格每天都会波动。它们以某个特定值开盘,在一天内达到一定的最高值和最低值,最终在一天结束时以某个特定值收盘。下图展示了股票价格在一天内的变化:

所以,在股票开盘后,你可以投资它们并购买股票。到一天结束时,你将根据所买股票的收盘值获得利润或亏损。投资者使用不同的技术来预测哪些股票在特定的日子里有上涨潜力,并根据他们的分析进行投资。

接近问题

在本章中,我们将研究股票价格是否会根据其他时区市场的涨跌而涨跌(这些市场的收盘时间比我们想投资的股票早)。我们将分析来自欧洲市场的数据,这些市场的收盘时间比美国股市早大约 3 或 4 小时。我们将从 Quandl 获取以下欧洲市场的数据:

  • WSE/OPONEO_PL

  • WSE/VINDEXUS

  • WSE/WAWEL

  • WSE/WIELTON

我们将预测接下来美国市场的收盘涨跌:WIKI/SNPS。

我们将下载所有市场数据,查看下载的市场收盘值图表,并修改数据以便可以在我们的网络上进行训练。然后,我们将看到我们的网络在假设下的表现。

本章中使用的代码和分析技术灵感来源于 Google Cloud Datalab 笔记本,地址为github.com/googledatalab/notebooks/blob/master/samples/TensorFlow/Machine%20Learning%20with%20Financial%20Data.ipynbhere

步骤如下:

  1. 下载所需数据并进行修改。

  2. 查看原始数据和修改后的数据。

  3. 从修改后的数据中提取特征。

  4. 准备训练并测试网络。

  5. 构建网络。

  6. 训练。

  7. 测试。

下载和修改数据

在这里,我们将从codes变量中提到的来源下载数据,并将其放入我们的closings数据框中。我们将存储原始数据、scaled数据和log_return

codes = ["WSE/OPONEO_PL", "WSE/VINDEXUS", "WSE/WAWEL", "WSE/WIELTON", "WIKI/SNPS"] 
closings = pd.DataFrame() 
for code in codes: 
    code_splits = code.split("/") 
    stock_exchange = code_splits[0] 
    index = code_splits[1] 
    stock_data = requests.get(base_url.format(stock_exchange,  
    index)).json() 
    dataset_data = stock_data['dataset_data'] 
    data = np.array(dataset_data['data']) 
    closings[index] = pd.Series(data[:, 1].astype(float)) 
    closings[index + "_scaled"] = closings[index] / 
     max(closings[index]) 
    closings[index + "_log_return"] = np.log(closings[index] / closings[index].shift()) 
closings = closings.fillna(method='ffill')  # Fill the gaps in data 

我们将数据缩放,使得股票值保持在01之间;这有助于与其他股票值进行最小化比较。它将帮助我们看到股票相对于其他市场的趋势,并使得视觉分析更为简便。

对数回报帮助我们获取市场涨跌图,相较于前一天的数据。

现在,让我们看看我们的数据长什么样。

查看数据

以下代码片段将绘制我们下载并处理的数据:

def show_plot(key="", show=True): 
    fig = plt.figure() 
    fig.set_figwidth(20) 
    fig.set_figheight(15) 
    for code in codes: 
        index = code.split("/")[1] 
        if key and len(key) > 0: 
            label = "{}_{}".format(index, key) 
        else: 
            label = index 
        _ = plt.plot(closings[label], label=label) 

    _ = plt.legend(loc='upper right') 
    if show: 
        plt.show() 

show = True 
show_plot("", show=show) 
show_plot("scaled", show=show) 
show_plot("log_return", show=show) 

原始市场数据转换为收盘值。正如你在这里看到的,WAWEL的值比其他市场大几个数量级:

WAWEL 的收盘值在视觉上减少了其他市场数据的趋势。我们将缩放这些数据,这样我们可以更清楚地看到。请看以下截图:

缩放后的市场值帮助我们更好地可视化趋势。现在,让我们看看log_return的样子:

对数回报是市场的收盘值

提取特征

现在,我们将提取所需的特征来训练和测试我们的数据:

feature_columns = ['SNPS_log_return_positive', 'SNPS_log_return_negative'] 
for i in range(len(codes)): 
    index = codes[i].split("/")[1] 
    feature_columns.extend([ 
        '{}_log_return_1'.format(index), 
        '{}_log_return_2'.format(index), 
        '{}_log_return_3'.format(index) 
    ]) 
features_and_labels = pd.DataFrame(columns=feature_columns) 
closings['SNPS_log_return_positive'] = 0 
closings.ix[closings['SNPS_log_return'] >= 0, 'SNPS_log_return_positive'] = 1 
closings['SNPS_log_return_negative'] = 0 
closings.ix[closings['SNPS_log_return'] < 0, 'SNPS_log_return_negative'] = 1 
for i in range(7, len(closings)): 
    feed_dict = {'SNPS_log_return_positive': closings['SNPS_log_return_positive'].ix[i], 
        'SNPS_log_return_negative': closings['SNPS_log_return_negative'].ix[i]} 
    for j in range(len(codes)): 
        index = codes[j].split("/")[1] 
        k = 1 if j == len(codes) - 1 else 0 
        feed_dict.update({'{}_log_return_1'.format(index): closings['{}_log_return'.format(index)].ix[i - k], 
                '{}_log_return_2'.format(index): closings['{}_log_return'.format(index)].ix[i - 1 - k], 
                '{}_log_return_3'.format(index): closings['{}_log_return'.format(index)].ix[i - 2 - k]}) 
    features_and_labels = features_and_labels.append(feed_dict, ignore_index=True) 

我们将所有特征和标签存储在features_and_label变量中。SNPS_log_return_positiveSNPS_log_return_negative键分别存储 SNPS 的对数回报为正和负的点。如果为真,则为1,如果为假,则为0。这两个键将作为网络的标签。

其他键用于存储过去三天的其他市场值(对于 SNPS,由于今天的值无法获取,我们还需要存储前 3 天的数据)。

为训练和测试做准备

现在,我们将把特征分成traintest子集。我们不会随机化我们的数据,因为在金融市场的时间序列中,数据是以规律的方式每天提供的,我们必须按原样使用它。你不能通过训练未来的数据来预测过去的行为,因为那样毫无意义。我们总是对股票市场的未来行为感兴趣:

features = features_and_labels[features_and_labels.columns[2:]] 
labels = features_and_labels[features_and_labels.columns[:2]] 
train_size = int(len(features_and_labels) * train_test_split) 
test_size = len(features_and_labels) - train_size 
train_features = features[:train_size] 
train_labels = labels[:train_size] 
test_features = features[train_size:] 
test_labels = labels[train_size:]

构建网络

用于训练我们时间序列的网络模型如下所示:

sess = tf.Session() 
num_predictors = len(train_features.columns) 
num_classes = len(train_labels.columns) 
feature_data = tf.placeholder("float", [None, num_predictors]) 
actual_classes = tf.placeholder("float", [None, 2]) 
weights1 = tf.Variable(tf.truncated_normal([len(codes) * 3, 50], stddev=0.0001)) 
biases1 = tf.Variable(tf.ones([50])) 
weights2 = tf.Variable(tf.truncated_normal([50, 25], stddev=0.0001)) 
biases2 = tf.Variable(tf.ones([25])) 
weights3 = tf.Variable(tf.truncated_normal([25, 2], stddev=0.0001)) 
biases3 = tf.Variable(tf.ones([2])) 
hidden_layer_1 = tf.nn.relu(tf.matmul(feature_data, weights1) + biases1) 
hidden_layer_2 = tf.nn.relu(tf.matmul(hidden_layer_1, weights2) + biases2) 
model = tf.nn.softmax(tf.matmul(hidden_layer_2, weights3) + biases3) 
cost = -tf.reduce_sum(actual_classes * tf.log(model)) 
train_op1 = tf.train.AdamOptimizer(learning_rate=0.0001).minimize(cost) 
init = tf.initialize_all_variables() 
sess.run(init) 
correct_prediction = tf.equal(tf.argmax(model, 1), tf.argmax(actual_classes, 1)) 
accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float")) 

这只是一个简单的网络,包含两个隐藏层。

训练

现在,让我们来训练我们的网络:

for i in range(1, 30001): 
    sess.run(train_op1, feed_dict={feature_data: train_features.values, 
            actual_classes: train_labels.values.reshape(len(train_labels.values), 2)}) 
    if i % 5000 == 0: 
        print(i, sess.run(accuracy, feed_dict={feature_data: train_features.values, 
                actual_classes: train_labels.values.reshape(len(train_labels.values), 2)})) 

测试

我们的网络测试结果如下所示:

feed_dict = { 
    feature_data: test_features.values, 
    actual_classes: test_labels.values.reshape(len(test_labels.values), 2) 
} 
tf_confusion_metrics(model, actual_classes, sess, feed_dict) 

进一步探讨

假设你刚刚训练了一个优秀的分类器,展示了对市场的某些预测能力,那么你应该开始交易吗?就像我们迄今为止做的其他机器学习项目一样,你需要在独立的测试集上进行测试。在过去,我们通常会将数据分成以下三个集合:

  • 训练集

  • 开发集,也叫验证集

  • 测试集

我们可以做类似于当前工作的事情,但金融市场为我们提供了一个额外的资源——持续的数据流!

我们可以使用早期获取的数据源继续拉取更多的数据;从本质上来说,我们拥有一个不断扩展、看不见的数据集!当然,这也取决于我们使用的数据频率——如果我们使用的是日数据,那么这需要一段时间才能完成。如果使用的是小时数据或每分钟数据,就会更容易,因为我们会更快地获得更多的数据。基于报价量的逐笔数据通常会更好。

由于可能涉及真实的资金,大多数人通常会进行模拟交易——基本上是几乎实时地运行系统,但实际上并不花费任何资金,而只是跟踪系统在真实环境下的表现。如果这一方法有效,下一步将是进行真实交易,也就是说,使用真实的资金(通常是小额资金用于测试系统)。

个人的实际考虑

假设你训练了一个不错的分类器,并且在盲测或实时数据集上也展示了良好的结果,那么现在应该开始交易吗?虽然理论上是可能的,但并不那么简单。以下是一些原因:

  • 历史数据分析与实时数据流:历史数据通常是经过清洗的,接近完美,但实时数据则没有这样的优势。你将需要编写代码来评估数据流并剔除可能不可靠的数据。

  • 买卖价差:这是新手面临的最大惊讶。市场上实际上有两个价格:一个是你可以买入的价格,另一个是你可以卖出的价格。你并不是在看到的典型市场价格下同时进行买卖(那只是买卖双方的最后碰撞点,称为最后成交价)。买入持仓后立即卖出会因为这个差价而亏损,所以从净利润来看,你已经处于亏损状态。

  • 交易成本:这可以低至每笔交易 1 美元,但它仍然是一个障碍,需要在策略能盈利之前克服。

  • 税务问题:这通常被忽视,可能是因为税务意味着净收益,而这通常是一件好事。

  • 退出能力:仅仅因为理论上可以卖出,并不意味着实际上有市场可以买入你的持仓,而且即便有市场,也可能无法一次性卖出全部持仓。猜猜看?还需要更多的编码。这一次,你需要查看买盘价格、这些价格的成交量以及订单簿的深度。

  • 成交量和流动性:仅仅因为信号告诉你买入,并不意味着市场上有足够的成交量可供购买;你可能只看到订单簿顶部的价格,而底下实际的成交量很少。更多的编码仍然是必要的!

  • 与交易 API 的集成:调用库很简单,但涉及资金时就不那么容易了。你需要交易协议、API 协议等。然而,成千上万的个人已经做过了,Interactive Brokers 是寻求 API 进行买卖持仓的最受欢迎的经纪商。方便的是,他们也提供一个 API 来提供市场数据。

学到的技能

在本章中,你应该已经学习了以下技能:

  • 理解时间序列数据

  • 为时间序列数据设置管道

  • 集成原始数据

  • 创建训练集和测试集

  • 实际考虑因素

总结

对金融数据的机器学习与我们使用的其他数据并无不同,事实上,我们使用的网络与处理其他数据集时相同。我们还有其他可用的选项,但总体方法保持不变。特别是在进行资金交易时,我们会发现,周围的代码相对于实际的机器学习代码部分会变得更大。

在下一章,我们将探讨如何将机器学习应用于医学领域。

第八章:现在医生来接诊

到目前为止,我们已经使用深度网络处理了图像、文本和时间序列数据。虽然大多数示例都很有趣且相关,但它们并不具备企业级的标准。现在,我们将挑战一个企业级问题——医学诊断。我们之所以称之为企业级问题,是因为医学数据具有一些在大型企业外部不常见的属性,比如专有数据格式、大规模原生数据、不方便的类别数据和非典型特征。

本章将涵盖以下主题:

  • 医学影像文件及其特性

  • 处理大规模图像文件

  • 从典型医学文件中提取类别数据

  • 使用“预训练”的网络进行非医学数据的应用

  • 扩展训练以适应医学数据的规模

获取医学数据本身就是一项挑战,因此我们将依赖于一个受欢迎的网站,所有读者都应该熟悉——Kaggle。虽然有许多医学数据集可以免费访问,但大多数都需要繁琐的注册过程才能访问它们。许多数据集仅在医学影像处理领域的特定子社区中公开,而且大多数都需要特定的提交流程。Kaggle 可能是最规范化的医学影像数据集来源,同时也有非医学数据集供你尝试。我们将特别关注 Kaggle 的糖尿病视网膜病变检测挑战。

数据集包含训练集和盲测集。训练集用于训练我们的网络,测试集则用于在 Kaggle 网站上提交我们的网络结果。

由于数据量相当大(训练集为 32GB,测试集为 49GB),它们都被划分成多个约 8GB 的 ZIP 文件。

这里的测试集是盲测集——我们不知道它们的标签。这是为了确保我们的网络训练结果在提交测试集时具有公平性。

就训练集而言,其标签存储在trainLabels.csv文件中。

挑战

在我们深入代码之前,请记住,大多数机器学习的工作都有两个简单的目标之一——分类或排序。在许多情况下,分类本身也是一种排序,因为我们最终选择排名最高的分类(通常是概率)。我们在医学影像方面的探索也不例外——我们将把图像分类为以下两个二元类别之一:

  • 疾病状态/阳性

  • 正常状态/阴性

或者,我们将把它们分类为多个类别,或者对它们进行排序。在糖尿病视网膜病变的情况下,我们将按以下方式对其进行排名:

  • 类别 0: 无糖尿病视网膜病变

  • 类别 1: 轻度

  • 类别 2: 中度

  • 类别 3: 严重

  • 类别 4: 广泛的糖尿病视网膜病变

通常,这被称为评分。Kaggle 友好地为参与者提供了超过 32 GB 的训练数据,其中包含超过 35,000 张图片。测试数据集甚至更大——达到了 49 GB。目标是使用已知评分对这 35,000+ 张图像进行训练,并为测试集提出评分。训练标签看起来是这样的:

图像级别
10_left0
10_right0
13_left0
13_right0
15_left1
15_right2
16_left4
16_right4
17_left0
17_right1

这里有些背景——糖尿病视网膜病变是一种眼内视网膜的疾病,因此我们有左眼和右眼的评分。我们可以将它们视为独立的训练数据,或者我们可以稍后发挥创意,将它们考虑在一个更大的单一患者的背景下。让我们从简单的开始并逐步迭代。

到现在为止,你可能已经熟悉了将一组数据划分为训练集、验证集和测试集的过程。这对我们使用过的一些标准数据集来说效果不错,但这个数据集是一个竞赛的一部分,并且是公开审计的,所以我们不知道答案!这很好地反映了现实生活。有一个小问题——大多数 Kaggle 竞赛允许你提出答案并告诉你你的总评分,这有助于学习和设定方向。它也有助于他们和社区了解哪些用户表现良好。

由于测试标签是盲测的,我们需要改变之前做过的两件事:

  • 我们将需要一个用于内部开发和迭代的流程(我们可能会将训练集分成训练集、验证集和测试集)。我们将需要另一个用于外部测试的流程(我们可能会找到一个有效的设置,运行它在盲测集上,或者首先重新训练整个训练集)。

  • 我们需要以非常特定的格式提交正式提案,将其提交给独立审计员(在此案例中为 Kaggle),并根据进展情况进行评估。以下是一个示例提交的样式:

图像级别
44342_left0
44342_right1
44344_left2
44344_right2
44345_left0
44345_right0
44346_left4
44346_right3
44350_left1
44350_right1
44351_left4
44351_right4

不出所料,它看起来和训练标签文件非常相似。你可以在这里提交你的内容:

www.kaggle.com/c/diabetic-retinopathy-detection/submithttps://www.kaggle.com/c/diabetic-retinopathy-detection/submit

你需要登录才能打开上述链接。

数据

让我们开始看看数据。打开一些示例文件并准备好迎接惊讶——这些既不是 28x28 的手写字块,也不是带有猫脸的 64x64 图标。这是一个来自现实世界的真实数据集。事实上,图像的大小甚至不一致。欢迎来到现实世界。

你会发现图像的大小从每边 2,000 像素到接近 5,000 像素不等!这引出了我们第一个实际任务——创建一个训练管道。管道将是一组步骤,抽象掉生活中的艰难现实,并生成一组干净、一致的数据。

管道

我们将智能地进行处理。Google 使用其 TensorFlow 库中的不同网络制作了许多管道模型结构。我们要做的是从这些模型结构和网络中选择一个,并根据我们的需求修改代码。

这很好,因为我们不会浪费时间从零开始构建管道,也不必担心集成 TensorBoard 可视化工具,因为它已经包含在 Google 的管道模型中。

我们将从这里使用一个管道模型:

github.com/tensorflow/models/

如你所见,这个仓库中有许多由 TensorFlow 制作的不同模型。你可以深入了解一些与自然语言处理(NLP)、递归神经网络及其他主题相关的模型。如果你想理解复杂的模型,这是一个非常好的起点。

对于本章,我们将使用Tensorflow-Slim 图像分类模型库。你可以在这里找到这个库:

github.com/tensorflow/models/tree/master/research/slim

网站上已经有很多详细信息,解释了如何使用这个库。他们还告诉你如何在分布式环境中使用该库,并且如何利用多 GPU 来加速训练时间,甚至部署到生产环境中。

使用这个的最佳之处在于,他们提供了预训练的模型快照,你可以利用它显著减少训练网络的时间。因此,即使你有较慢的 GPU,也不必花费数周的时间来训练这么大的网络,便可达到一个合理的训练水平。

这叫做模型的微调,你只需要提供一个不同的数据集,并告诉网络重新初始化网络的最终层以便重新训练它们。此外,你还需要告诉它数据集中有多少个输出标签类。在我们的案例中,有五个独特的类别,用于识别不同等级的糖尿病视网膜病变DR)。

预训练的快照可以在这里找到:

github.com/tensorflow/models/tree/master/research/slim#Pretrained

如你在上面的链接中所见,他们提供了许多可以利用的预训练模型。他们使用了ImageNet数据集来训练这些模型。ImageNet是一个标准数据集,包含 1,000 个类别,数据集大小接近 500 GB。你可以在这里了解更多:

image-net.org/

理解管道

首先,开始将models仓库克隆到你的计算机中:

git clone https://github.com/tensorflow/models/

现在,让我们深入了解从 Google 模型仓库中获得的管道。

如果你查看仓库中这个路径前缀(models/research/slim)的文件夹,你会看到名为datasetsdeploymentnetspreprocessingscripts的文件夹;这些文件涉及生成模型、训练和测试管道,以及与训练ImageNet数据集相关的文件,还有一个名为flowers的数据集

我们将使用download_and_convert_data.py来构建我们的 DR 数据集。这个图像分类模型库是基于slim库构建的。在这一章中,我们将微调在nets/inception_v3.py中定义的 Inception 网络(稍后我们会详细讨论网络的规格和概念),其中包括计算损失函数、添加不同的操作、构建网络等内容。最后,train_image_classifier.pyeval_image_classifier.py文件包含了为我们的网络创建训练和测试管道的通用程序。

对于这一章,由于网络的复杂性,我们使用基于 GPU 的管道来训练网络。如果你想了解如何在你的机器上安装适用于 GPU 的 TensorFlow,请参考本书中的附录 A,高级安装部分。此外,你的机器中应该有大约120 GB的空间才能运行此代码。你可以在本书代码文件的Chapter 8文件夹中找到最终的代码文件。

准备数据集

现在,让我们开始准备网络的数据集。

对于这个 Inception 网络,我们将使用TFRecord类来管理我们的数据集。经过预处理后的输出数据集文件将是 proto 文件,TFRecord可以读取这些文件,它只是以序列化格式存储的我们的数据,以便更快的读取速度。每个 proto 文件内包含一些信息,如图像的大小和格式。

我们这样做的原因是,数据集的大小太大,我们不能将整个数据集加载到内存(RAM)中,因为它会占用大量空间。因此,为了高效使用内存,我们必须分批加载图像,并删除当前没有使用的已经加载的图像。

网络将接收的输入大小是 299x299。因此,我们将找到一种方法,首先将图像大小缩小到 299x299,以便得到一致的图像数据集。

在减少图像大小后,我们将制作 proto 文件,稍后可以将这些文件输入到我们的网络中,网络将对我们的数据集进行训练。

你需要首先从这里下载五个训练 ZIP 文件和标签文件:

www.kaggle.com/c/diabetic-retinopathy-detection/data

不幸的是,Kaggle 仅允许通过账户下载训练的 ZIP 文件,因此无法像之前章节那样自动化下载数据集文件的过程。

现在,假设你已经下载了所有五个训练 ZIP 文件和标签文件,并将它们存储在名为 diabetic 的文件夹中。diabetic 文件夹的结构将如下所示:

  • diabetic

    • train.zip.001

    • train.zip.002

    • train.zip.003

    • train.zip.004

    • train.zip.005

    • trainLabels.csv.zip

为了简化项目,我们将手动使用压缩软件进行解压。解压完成后,diabetic 文件夹的结构将如下所示:

  • diabetic

    • train

    • 10_left.jpeg

    • 10_right.jpeg

    • ...

    • trainLabels.csv

    • train.zip.001

    • train.zip.002

    • train.zip.003

    • train.zip.004

    • train.zip.005

    • trainLabels.csv.zip

在这种情况下,train 文件夹包含所有 .zip 文件中的图像,而 trainLabels.csv 文件包含每张图像的真实标签。

模型库的作者提供了一些示例代码,用于处理一些流行的图像分类数据集。我们的糖尿病问题也可以用相同的方法来解决。因此,我们可以遵循处理其他数据集(如 flowerMNIST 数据集)的代码。我们已经在本书的 github.com/mlwithtf/mlwithtf/ 库中提供了修改代码,便于处理糖尿病数据集。

你需要克隆仓库并导航到 chapter_08 文件夹。你可以按照以下方式运行 download_and_convert_data.py 文件:

python download_and_convert_data.py --dataset_name diabetic --dataset_dir D:\\datasets\\diabetic

在这种情况下,我们将使用 dataset_namediabetic,而 dataset_dir 是包含 trainLabels.csvtrain 文件夹的文件夹。

它应该能够顺利运行,开始将数据集预处理成适合的 (299x299) 格式,并在新创建的 tfrecords 文件夹中生成一些 TFRecord 文件。下图展示了 tfrecords 文件夹的内容:

解释数据准备过程

现在,让我们开始编写数据预处理的代码。从现在开始,我们将展示我们对 tensorflow/models 原始库所做的修改。基本上,我们将处理 flowers 数据集的代码作为起点,并对其进行修改以满足我们的需求。

download_and_convert_data.py 文件中,我们在文件开头添加了一行新的代码:

from datasets import download_and_convert_diabetic 
and a new else-if clause to process the dataset_name "diabetic" at line 69: 
  elif FLAGS.dataset_name == 'diabetic': 
      download_and_convert_diabetic.run(FLAGS.dataset_dir)

使用这段代码,我们可以调用 datasets 文件夹中的 download_and_convert_diabetic.py 文件中的 run 方法。这是一种非常简单的方法,用于分离多个数据集的预处理代码,但我们仍然可以利用 image classification 库的其他部分。

download_and_convert_diabetic.py 文件是对 download_and_convert_flowers.py 文件的复制,并对其进行了修改,以准备我们的糖尿病数据集。

download_and_convert_diabetic.py 文件的 run 方法中,我们做了如下更改:

  def run(dataset_dir): 
    """Runs the download and conversion operation. 

    Args: 
      dataset_dir: The dataset directory where the dataset is stored. 
    """ 
    if not tf.gfile.Exists(dataset_dir): 
        tf.gfile.MakeDirs(dataset_dir) 

    if _dataset_exists(dataset_dir): 
        print('Dataset files already exist. Exiting without re-creating   
        them.') 
        return 

    # Pre-processing the images. 
    data_utils.prepare_dr_dataset(dataset_dir) 
    training_filenames, validation_filenames, class_names =   
    _get_filenames_and_classes(dataset_dir) 
    class_names_to_ids = dict(zip(class_names,    
    range(len(class_names)))) 

    # Convert the training and validation sets. 
    _convert_dataset('train', training_filenames, class_names_to_ids,   
    dataset_dir) 
    _convert_dataset('validation', validation_filenames,    
    class_names_to_ids, dataset_dir) 

    # Finally, write the labels file: 
    labels_to_class_names = dict(zip(range(len(class_names)),    
    class_names)) 
    dataset_utils.write_label_file(labels_to_class_names, dataset_dir) 

    print('\nFinished converting the Diabetic dataset!')

在这段代码中,我们使用了来自 data_utils 包的 prepare_dr_dataset,该包在本书仓库的根目录中准备好。稍后我们会看这个方法。然后,我们修改了 _get_filenames_and_classes 方法,以返回 trainingvalidation 的文件名。最后几行与 flowers 数据集示例相同:

  def _get_filenames_and_classes(dataset_dir): 
    train_root = os.path.join(dataset_dir, 'processed_images', 'train') 
    validation_root = os.path.join(dataset_dir, 'processed_images',   
    'validation') 
    class_names = [] 
    for filename in os.listdir(train_root): 
        path = os.path.join(train_root, filename) 
        if os.path.isdir(path): 
            class_names.append(filename) 

    train_filenames = [] 
    directories = [os.path.join(train_root, name) for name in    
    class_names] 
    for directory in directories: 
        for filename in os.listdir(directory): 
            path = os.path.join(directory, filename) 
            train_filenames.append(path) 

    validation_filenames = [] 
    directories = [os.path.join(validation_root, name) for name in    
    class_names] 
    for directory in directories: 
        for filename in os.listdir(directory): 
            path = os.path.join(directory, filename) 
            validation_filenames.append(path) 
    return train_filenames, validation_filenames, sorted(class_names) 

在前面的这个方法中,我们查找了 processed_images/trainprocessed/validation 文件夹中的所有文件名,这些文件夹包含了在 data_utils.prepare_dr_dataset 方法中预处理过的图像。

data_utils.py 文件中,我们编写了 prepare_dr_dataset(dataset_dir) 函数,负责整个数据的预处理工作。

让我们首先定义必要的变量来链接到我们的数据:

num_of_processing_threads = 16 
dr_dataset_base_path = os.path.realpath(dataset_dir) 
unique_labels_file_path = os.path.join(dr_dataset_base_path, "unique_labels_file.txt") 
processed_images_folder = os.path.join(dr_dataset_base_path, "processed_images") 
num_of_processed_images = 35126 
train_processed_images_folder = os.path.join(processed_images_folder, "train") 
validation_processed_images_folder = os.path.join(processed_images_folder, "validation") 
num_of_training_images = 30000 
raw_images_folder = os.path.join(dr_dataset_base_path, "train") 
train_labels_csv_path = os.path.join(dr_dataset_base_path, "trainLabels.csv")

num_of_processing_threads 变量用于指定在预处理数据集时我们希望使用的线程数,正如你可能已经猜到的那样。我们将使用多线程环境来加速数据预处理。随后,我们指定了一些目录路径,用于在预处理时将数据存放在不同的文件夹中。

我们将提取原始形式的图像,然后对它们进行预处理,将其转换为适当的一致格式和大小,之后,我们将使用 download_and_convert_diabetic.py 文件中的 _convert_dataset 方法从处理过的图像生成 tfrecords 文件。之后,我们将这些 tfrecords 文件输入到训练和测试网络中。

正如我们在前一部分所说的,我们已经提取了 dataset 文件和标签文件。现在,既然我们已经提取了所有数据并将其存储在机器中,我们将开始处理图像。来自 DR 数据集的典型图像如下所示:

我们想要做的是去除这些多余的黑色空间,因为它对我们的网络来说并不必要。这将减少图像中的无关信息。之后,我们会将这张图像缩放成一个 299x299 的 JPG 图像文件。

我们将对所有训练数据集重复此过程。

剪裁黑色图像边框的函数如下所示:

  def crop_black_borders(image, threshold=0):
     """Crops any edges below or equal to threshold

     Crops blank image to 1x1.

     Returns cropped image.

     """
     if len(image.shape) == 3:
         flatImage = np.max(image, 2)
     else:
         flatImage = image
     assert len(flatImage.shape) == 2

     rows = np.where(np.max(flatImage, 0) > threshold)[0]
     if rows.size:
         cols = np.where(np.max(flatImage, 1) > threshold)[0]
         image = image[cols[0]: cols[-1] + 1, rows[0]: rows[-1] + 1]
     else:
         image = image[:1, :1]

     return image 

这个函数接收图像和一个灰度阈值,低于此值时,它会去除图像周围的黑色边框。

由于我们在多线程环境中执行所有这些处理,我们将按批次处理图像。要处理一个图像批次,我们将使用以下函数:

  def process_images_batch(thread_index, files, labels, subset):

     num_of_files = len(files)

     for index, file_and_label in enumerate(zip(files, labels)):
         file = file_and_label[0] + '.jpeg'
         label = file_and_label[1]

         input_file = os.path.join(raw_images_folder, file)
         output_file = os.path.join(processed_images_folder, subset,   
         str(label), file)

         image = ndimage.imread(input_file)
         cropped_image = crop_black_borders(image, 10)
         resized_cropped_image = imresize(cropped_image, (299, 299, 3),   
         interp="bicubic")
         imsave(output_file, resized_cropped_image)

         if index % 10 == 0:
             print("(Thread {}): Files processed {} out of  
             {}".format(thread_index, index, num_of_files)) 

thread_index 告诉我们调用该函数的线程 ID。处理图像批次的多线程环境在以下函数中定义:

   def process_images(files, labels, subset):

     # Break all images into batches with a [ranges[i][0], ranges[i] 
     [1]].
     spacing = np.linspace(0, len(files), num_of_processing_threads +  
     1).astype(np.int)
     ranges = []
     for i in xrange(len(spacing) - 1):
         ranges.append([spacing[i], spacing[i + 1]])

     # Create a mechanism for monitoring when all threads are finished.
     coord = tf.train.Coordinator()

     threads = []
     for thread_index in xrange(len(ranges)):
         args = (thread_index, files[ranges[thread_index] 
         [0]:ranges[thread_index][1]],
                 labels[ranges[thread_index][0]:ranges[thread_index] 
                 [1]],
                 subset)
         t = threading.Thread(target=process_images_batch, args=args)
         t.start()
         threads.append(t)

     # Wait for all the threads to terminate.
     coord.join(threads) 

为了从所有线程中获取最终结果,我们使用一个 TensorFlow 类,tf.train.Coordinator(),它的 join 函数负责处理所有线程的最终处理点。

对于线程处理,我们使用threading.Thread,其中target参数指定要调用的函数,args参数指定目标函数的参数。

现在,我们将处理训练图像。训练数据集分为训练集(30,000 张图像)和验证集(5,126 张图像)。

总的预处理过程如下所示:

def process_training_and_validation_images():
     train_files = []
     train_labels = []

     validation_files = []
     validation_labels = []

     with open(train_labels_csv_path) as csvfile:
         reader = csv.DictReader(csvfile)
         for index, row in enumerate(reader):
             if index < num_of_training_images:
                 train_files.extend([row['image'].strip()])
                 train_labels.extend([int(row['level'].strip())])
             else:
                 validation_files.extend([row['image'].strip()])

   validation_labels.extend([int(row['level'].strip())])

     if not os.path.isdir(processed_images_folder):
         os.mkdir(processed_images_folder)

     if not os.path.isdir(train_processed_images_folder):
         os.mkdir(train_processed_images_folder)

     if not os.path.isdir(validation_processed_images_folder):
         os.mkdir(validation_processed_images_folder)

     for directory_index in range(5):
         train_directory_path =   
    os.path.join(train_processed_images_folder,   
    str(directory_index))
         valid_directory_path =   
   os.path.join(validation_processed_images_folder,  
   str(directory_index))

         if not os.path.isdir(train_directory_path):
             os.mkdir(train_directory_path)

         if not os.path.isdir(valid_directory_path):
             os.mkdir(valid_directory_path)

     print("Processing training files...")
     process_images(train_files, train_labels, "train")
     print("Done!")

     print("Processing validation files...")
     process_images(validation_files, validation_labels,  
     "validation")
     print("Done!")

     print("Making unique labels file...")
     with open(unique_labels_file_path, 'w') as unique_labels_file:
         unique_labels = ""
         for index in range(5):
             unique_labels += "{}\n".format(index)
         unique_labels_file.write(unique_labels)

     status = check_folder_status(processed_images_folder, 
     num_of_processed_images,
     "All processed images are present in place",
     "Couldn't complete the image processing of training and  
     validation files.")

     return status 

现在,我们将查看准备数据集的最后一个方法,即在download_and_convert_diabetic.py文件中调用的_convert_dataset方法:

def _get_dataset_filename(dataset_dir, split_name, shard_id): 
    output_filename = 'diabetic_%s_%05d-of-%05d.tfrecord' % ( 
        split_name, shard_id, _NUM_SHARDS) 
    return os.path.join(dataset_dir, output_filename) 
def _convert_dataset(split_name, filenames, class_names_to_ids, dataset_dir): 
    """Converts the given filenames to a TFRecord dataset. 

    Args: 
      split_name: The name of the dataset, either 'train' or  
     'validation'. 
      filenames: A list of absolute paths to png or jpg images. 
      class_names_to_ids: A dictionary from class names (strings) to  
      ids 
        (integers). 
      dataset_dir: The directory where the converted datasets are  
     stored. 
    """ 
    assert split_name in ['train', 'validation'] 

    num_per_shard = int(math.ceil(len(filenames) /  
    float(_NUM_SHARDS))) 

    with tf.Graph().as_default(): 
        image_reader = ImageReader() 

        with tf.Session('') as sess: 

            for shard_id in range(_NUM_SHARDS): 
                output_filename = _get_dataset_filename( 
                    dataset_dir, split_name, shard_id) 

                with tf.python_io.TFRecordWriter(output_filename)
                as   
                tfrecord_writer: 
                    start_ndx = shard_id * num_per_shard 
                    end_ndx = min((shard_id + 1) * num_per_shard,  
                    len(filenames)) 
                    for i in range(start_ndx, end_ndx): 
                        sys.stdout.write('\r>> Converting image  
                         %d/%d shard %d' % ( 
                            i + 1, len(filenames), shard_id)) 
                        sys.stdout.flush() 

                        # Read the filename: 
                        image_data =  
                    tf.gfile.FastGFile(filenames[i], 'rb').read() 
                        height, width =          
                    image_reader.read_image_dims(sess, image_data) 

                        class_name =  
                     os.path.basename(os.path.dirname(filenames[i])) 
                        class_id = class_names_to_ids[class_name] 

                        example = dataset_utils.image_to_tfexample( 
                            image_data, b'jpg', height, width,   
                             class_id) 

                 tfrecord_writer.write(example.SerializeToString()) 

                  sys.stdout.write('\n') 
                  sys.stdout.flush() 

在前面的函数中,我们将获取图像文件名,并将它们存储在tfrecord文件中。我们还会将trainvalidation文件拆分为多个tfrecord文件,而不是只使用一个文件来存储每个分割数据集。

现在,数据处理已经完成,我们将正式将数据集形式化为slim.dataset的实例。数据集来自Tensorflow Slim。在datasets/diabetic.py文件中,你将看到一个名为get_split的方法,如下所示:

_FILE_PATTERN = 'diabetic_%s_*.tfrecord' 
SPLITS_TO_SIZES = {'train': 30000, 'validation': 5126} 
_NUM_CLASSES = 5 
_ITEMS_TO_DESCRIPTIONS = { 
    'image': 'A color image of varying size.', 
    'label': 'A single integer between 0 and 4', 
} 
def get_split(split_name, dataset_dir, file_pattern=None, reader=None): 
  """Gets a dataset tuple with instructions for reading flowers. 
  Args: 
    split_name: A train/validation split name. 
    dataset_dir: The base directory of the dataset sources. 
    file_pattern: The file pattern to use when matching the dataset sources. 
      It is assumed that the pattern contains a '%s' string so that the split 
      name can be inserted. 
    reader: The TensorFlow reader type. 
  Returns: 
    A `Dataset` namedtuple. 
  Raises: 
    ValueError: if `split_name` is not a valid train/validation split. 
  """ 
  if split_name not in SPLITS_TO_SIZES: 
    raise ValueError('split name %s was not recognized.' % split_name) 

  if not file_pattern: 
    file_pattern = _FILE_PATTERN 
  file_pattern = os.path.join(dataset_dir, file_pattern % split_name) 

  # Allowing None in the signature so that dataset_factory can use the default. 
  if reader is None: 
    reader = tf.TFRecordReader 

  keys_to_features = { 
      'image/encoded': tf.FixedLenFeature((), tf.string, default_value=''), 
      'image/format': tf.FixedLenFeature((), tf.string, default_value='png'), 
      'image/class/label': tf.FixedLenFeature( 
          [], tf.int64, default_value=tf.zeros([], dtype=tf.int64)), 
  } 
  items_to_handlers = { 
      'image': slim.tfexample_decoder.Image(), 
      'label': slim.tfexample_decoder.Tensor('image/class/label'), 
  } 
  decoder = slim.tfexample_decoder.TFExampleDecoder( 
      keys_to_features, items_to_handlers) 

  labels_to_names = None 
  if dataset_utils.has_labels(dataset_dir): 
    labels_to_names = dataset_utils.read_label_file(dataset_dir) 

  return slim.dataset.Dataset( 
      data_sources=file_pattern, 
      reader=reader, 
      decoder=decoder, 
      num_samples=SPLITS_TO_SIZES[split_name], 
      items_to_descriptions=_ITEMS_TO_DESCRIPTIONS, 
      num_classes=_NUM_CLASSES, 
      labels_to_names=labels_to_names) 

之前的方法将在训练和评估过程中被调用。我们将创建一个slim.dataset的实例,包含关于我们的tfrecord文件的信息,以便它可以自动解析二进制文件。此外,我们还可以使用slim.dataset.Dataset,结合DatasetDataProvider,通过 Tensorflow Slim 来并行读取数据集,从而提高训练和评估的效率。

在开始训练之前,我们需要从Tensorflow Slim 图像分类库中下载 Inception V3 的预训练模型,这样我们就可以利用 Inception V3 的性能,而无需从头开始训练。

预训练快照可以在这里找到:

github.com/tensorflow/models/tree/master/research/slim#Pretrained

在本章中,我们将使用 Inception V3,因此我们需要下载inception_v3_2016_08_28.tar.gz文件,并解压缩它以获得名为inception_v3.ckpt的检查点文件。

训练过程

现在,让我们继续进行模型的训练和评估。

训练脚本位于train_image_classifer.py文件中。由于我们遵循了该库的工作流程,因此可以保持该文件不变,并使用以下命令运行训练过程:

python train_image_classifier.py --train_dir=D:\datasets\diabetic\checkpoints --dataset_name=diabetic --dataset_split_name=train --dataset_dir=D:\datasets\diabetic\tfrecords --model_name=inception_v3 --checkpoint_path=D:\datasets\diabetic\checkpoints\inception_v3\inception_v3.ckpt --checkpoint_exclude_scopes=InceptionV3/Logits,InceptionV3/AuxLogits --trainable_scopes=InceptionV3/Logits,InceptionV3/AuxLogits --learning_rate=0.000001 --learning_rate_decay_type=exponential 

在我们的设置中,我们已经让训练过程运行了一整夜。现在,我们将运行训练好的模型,通过验证过程来查看其效果。

验证过程

你可以使用以下命令运行验证过程:

python eval_image_classifier.py --alsologtostderr --checkpoint_path=D:\datasets\diabetic\checkpoints\model.ckpt-92462 --dataset_name=diabetic --dataset_split_name=validation --dataset_dir=D:\datasets\diabetic\tfrecords --model_name=inception_v3

如你所见,当前的准确率大约是 75%。在进一步探索部分,我们将给出一些提高准确率的建议。

现在,我们将查看 TensorBoard,来可视化训练过程。

使用 TensorBoard 可视化输出

现在,我们将使用 TensorBoard 来可视化训练结果。

首先,你需要将command-line目录更改为包含检查点的文件夹。在我们的例子中,它是上一条命令中的train_dir参数,D:\datasets\diabetic\checkpoints。然后,你应该运行以下命令:

tensorboard -logdir .

以下是我们运行 TensorBoard 时的输出:

前面的图像显示了包含 RMSprop 优化器的节点,用于训练网络以及它所包含的用于 DR 分类输出的一些 logits。下一张截图展示了作为输入的图像及其预处理和修改:

在此截图中,你可以看到训练过程中网络输出的图形:

这张截图显示了训练过程中网络的总原始损失:

Inception 网络

Inception 网络的主要概念是将不同的卷积操作结合在同一层中。通过将 7x7、5x5、3x3 和 1x1 的卷积组合在一起,传递给下一层。通过这种方式,网络可以提取更多的特征,从而提高准确性。以下是 Google Inception V3 网络的示意图。你可以尝试访问chapter_08/nets/inception_v3.py中的代码。

该图像来自github.com/tensorflow/models/blob/master/research/inception/g3doc/inception_v3_architecture.png

继续深入

我们从运行该网络中得到的结果是在验证集上的准确率为 75%。这并不算很好,因为该网络的使用非常关键。在医学中,错误的余地非常小,因为人的健康状况直接关系到生命。

为了提高准确性,我们需要定义不同的评估标准。你可以在这里阅读更多内容:

en.wikipedia.org/wiki/Confusion_matrix

同时,你可以平衡数据集。现在的数据集是不平衡的,病人数量远少于正常患者。因此,网络对正常患者特征更加敏感,而对病人特征较不敏感。

为了修复这个问题,我们可以对数据集进行 SMOTE 处理。SMOTE 基本上是通过复制较少频繁类别的数据(例如水平或垂直翻转图像、改变饱和度等)来创建一个平衡的数据集。SMOTE 代表合成少数类过采样技术

这是一本关于该主题的优秀读物:

www.jair.org/media/953/live-953-2037-jair.pdf

其他医学数据挑战

可以理解的是,医疗数据不像其他数据集那样容易发布,因此公开领域的数据集要少得多。这一情况正在缓慢改变,但与此同时,以下是一些你可以尝试的公开数据集和相关挑战。需要注意的是,许多挑战已经被克服,但幸运的是,它们仍然继续发布数据集。

ISBI 大奖挑战

ISBI 是国际生物医学影像学大会,这是一个推动本章中所述工作的受欢迎的会议场所。他们的年度会议通常会向学术界提出多个挑战。2016 年他们提出了几个挑战。

一个受欢迎的挑战是 AIDA-E:内窥镜图像分析检测异常。挑战网站是isbi-aida.grand-challenge.org/

另一个受欢迎的挑战是淋巴结中的癌症转移检测,涉及病理数据。挑战网站是camelyon16.grand-challenge.org/

在放射学方面,2016 年一个受欢迎的挑战是数据科学碗挑战赛,聚焦心脏病诊断。该挑战名为转变我们诊断心脏病的方式,目标是对心脏磁共振成像(MRI)数据的部分进行分割,以衡量心脏泵血量,这一数据被用作心脏健康的代理指标。挑战网站及数据集为www.datasciencebowl.com/competitions/transforming-how-we-diagnose-heart-disease/

另一个受欢迎的放射学数据集是 Lung Image Database Consortium(LIDC)中的计算机断层扫描CT)数据,属于 LIDC-IDRI 图像集。这是一个诊断和肺癌筛查胸部 CT 扫描的数据集。有趣的是,除了图像级别的类别外,该数据集还标注了病变的实际位置。

这两个放射学竞赛还有两个有趣的原因:

  • 它们包括三维体积数据,本质上是由二维图像组成的有序堆叠,这些图像共同构成了一个实际的空间。

  • 它们包括分割任务,在这些任务中,你需要将图像或体积的某些部分分类到特定类别。这是一个常见的分类挑战,不同之处在于我们还尝试对图像中的特征进行定位。在一种情况下,我们尝试定位特征并指出它的位置(而不是对整个图像进行分类),在另一种情况下,我们则尝试将一个区域进行分类,以此来衡量一个区域的大小。

我们稍后会更多地讨论如何处理体积数据,但目前你已经有了一些非常有趣和多样化的数据集可以使用。

阅读医疗数据

尽管存在挑战,但糖尿病视网膜病变挑战并不像想象中那么复杂。实际图像是以 JPEG 格式提供的,但大多数医学数据并非 JPEG 格式。它们通常是容器格式,如 DICOM。DICOM 代表 医学中的数字成像与通信,并且有多个版本和变体。它包含医学图像,但也有头数据。头数据通常包括一般的病人和研究数据,但它还可以包含其他几十个自定义字段。如果你幸运的话,它也会包含诊断信息,你可以将其作为标签。

DICOM 数据为我们之前讨论的流程增加了一个步骤,因为现在我们需要读取 DICOM 文件,提取头信息(并希望包括类/标签数据),并提取底层图像。DICOM 并不像 JPEG 或 PNG 那么容易使用,但也不算太复杂。它需要一些额外的包。

由于我们几乎所有的工作都是用 Python 完成的,因此让我们使用一个用于 DICOM 处理的 Python 库。最流行的是 pydicom,可以在github.com/darcymason/pydicom找到。

文档可以在pydicom.readthedocs.io/en/stable/getting_started.html获取。

应注意,pip 安装当前存在问题,因此必须从源代码仓库克隆并通过设置脚本进行安装,才能使用。

来自文档的一个简短摘录将有助于我们理解如何处理 DICOM 文件:

>>> import dicom 
>>> plan = dicom.read_file("rtplan.dcm") 
>>> plan.PatientName 
'Last^First^mid^pre' 
>>> plan.dir("setup")    # get a list of tags with "setup" somewhere in the name 
['PatientSetupSequence'] 
>>> plan.PatientSetupSequence[0] 
(0018, 5100) Patient Position                    CS: 'HFS' 
(300a, 0182) Patient Setup Number                IS: '1' 
(300a, 01b2) Setup Technique Description         ST: '' 

这看起来可能有些凌乱,但这正是你在处理医学数据时应该预期的交互方式。更糟糕的是,每个供应商通常将相同的数据,甚至是基本数据,放入略有不同的标签中。行业中的典型做法就是“到处看看!”我们通过以下方式转储整个标签集来做到这一点:

>>> ds 
(0008, 0012) Instance Creation Date              DA: '20030903' 
(0008, 0013) Instance Creation Time              TM: '150031' 
(0008, 0016) SOP Class UID                       UI: RT Plan Storage 
(0008, 0018) Diagnosis                        UI: Positive  
(0008, 0020) Study Date                          DA: '20030716' 
(0008, 0030) Study Time                          TM: '153557' 
(0008, 0050) Accession Number                    SH: '' 
(0008, 0060) Modality                            CS: 'RTPLAN'

假设我们在寻找诊断信息。我们会查看几个标签文件,尝试看看诊断是否始终出现在标签(0008, 0018) Diagnosis下,如果是,我们通过从大部分训练集中提取这个字段来验证我们的假设,看它是否始终被填充。如果是的话,我们就可以进入下一步。如果不是,我们需要重新开始并查看其他字段。从理论上讲,数据提供者、经纪人或供应商可以提供这些信息,但从实际情况来看,这并不那么简单。

下一步是查看值域。这一点非常重要,因为我们希望看到我们的类的表现。理想情况下,我们会得到一个干净的值集合,例如{Negative, Positive},但实际上,我们通常会得到一条长尾的脏值。所以,典型的做法是遍历每一张图片,并统计每个遇到的唯一值域值,具体如下:

>>> import dicom, glob, os 
>>> os.chdir("/some/medical/data/dir") 
>>> domains={} 
>>> for file in glob.glob("*.dcm"): 
>>>    aMedFile = dicom.read_file(file) 
>>>    theVal=aMedFile.ds[0x10,0x10].value 
>>>    if domains[theVal]>0: 
>>>       domains[theVal]= domains[theVal]+1 
>>>    else: 
>>>       domains[theVal]=1 

在这一点上,最常见的发现是,99%的领域值存在于少数几个领域值之间(如正向负向),而剩下的 1%的领域值是脏数据(如正向,但待审阅@#Q#%@#%,或已发送重新阅读)。最简单的方法是丢弃这些长尾数据——只保留好的数据。如果有足够的训练数据,这尤其容易做到。

好的,我们已经提取了类信息,但我们仍然需要提取实际的图像。我们可以按照以下步骤进行:

>>> import dicom 
>>> ds=dicom.read_file("MR_small.dcm") 
>>> ds.pixel_array 
array([[ 905, 1019, 1227, ...,  302,  304,  328], 
       [ 628,  770,  907, ...,  298,  331,  355], 
       [ 498,  566,  706, ...,  280,  285,  320], 
       ..., 
       [ 334,  400,  431, ..., 1094, 1068, 1083], 
       [ 339,  377,  413, ..., 1318, 1346, 1336], 
       [ 378,  374,  422, ..., 1369, 1129,  862]], dtype=int16) 
>>> ds.pixel_array.shape 
(64, 64)

不幸的是,这仅仅给了我们一个原始的像素值矩阵。我们仍然需要将其转换为可读的格式(理想情况下是 JPEG 或 PNG)。我们将按以下步骤进行下一步操作:

接下来,我们将把图像缩放到我们需要的比特长度,并使用另一个库将矩阵写入文件,该库专门用于将数据写入目标格式。在我们的例子中,我们将使用 PNG 输出格式,并使用png库将其写入。这意味着需要额外的导入:

import os 
from pydicom import dicomio 
import png 
import errno 
import fnmatch

我们将这样导出:

学到的技能

你应该在本章中学到这些技能:

  • 处理晦涩难懂且专有的医学影像格式

  • 处理大型图像文件,这是医学图像的一个常见特征

  • 从医疗文件中提取类数据

  • 扩展我们现有的管道以处理异构数据输入

  • 应用在非医学数据上预训练的网络

  • 扩展训练以适应新数据集。

总结

在本章中,我们为医学诊断这一企业级问题创建了一个深度神经网络,用于图像分类问题。此外,我们还引导你完成了读取 DICOM 数字医学影像数据的过程,为进一步研究做准备。在下一章,我们将构建一个可以通过学习用户反馈自我改进的生产系统。

第九章:自适应巡航控制 - 自动化

在本章中,我们将创建一个生产系统,从训练到提供模型服务。我们的系统将能够区分 37 种不同的狗和猫品种。用户可以向我们的系统上传图像并接收结果。系统还可以从用户那里接收反馈,并每天自动进行训练以改善结果。

本章将重点讲解以下几个方面:

  • 如何将迁移学习应用到新数据集

  • 如何使用 TensorFlow Serving 提供生产模型服务

  • 创建一个通过众包标注数据集并在用户数据上自动微调模型的系统

系统概览

以下图表提供了我们系统的概述:

在这个系统中,我们将使用一个初始数据集在训练服务器上训练一个卷积神经网络模型。然后,模型将在生产服务器上通过 TensorFlow Serving 提供服务。在生产服务器上,会有一个 Flask 服务器,允许用户上传新图像并在模型出现错误时修正标签。在一天的某个特定时间,训练服务器将会将所有用户标记过的图像与当前数据集合并,以自动微调模型并将其发送到生产服务器。以下是允许用户上传并接收结果的网页界面框架:

设置项目

在本章中,我们将微调一个已经在 ImageNet 数据上训练过的 VGG 模型,该数据集有 1,000 个类别。我们已经提供了一个包含预训练 VGG 模型和一些实用文件的初始项目。你可以去下载 github.com/mlwithtf/mlwithtf/tree/master/chapter_09 上的代码。

chapter-09 文件夹中,你将看到以下结构:

- data
--VGG16.npz
- samples_data
- production
- utils
--__init__.py
--debug_print.py
- README.md

有两个文件是你需要理解的:

  • VGG16.npz 是从 Caffe 模型导出的预训练模型。第十一章,深入学习 - 21 个问题 将展示如何从 Caffe 模型创建这个文件。在本章中,我们将把它作为模型的初始值。你可以从 chapter_09 文件夹中的 README.md 下载此文件。

  • production 是我们创建的 Flask 服务器,用作用户上传和修正模型的网页接口。

  • debug_print.py 包含了一些我们将在本章中使用的方法,用于理解网络结构。

  • samples_data 包含了一些我们将在本章中使用的猫、狗和汽车的图像。

加载预训练模型以加速训练

在这一节中,我们将专注于在 TensorFlow 中加载预训练模型。我们将使用由牛津大学的 K. Simonyan 和 A. Zisserman 提出的 VGG-16 模型。

VGG-16 是一个非常深的神经网络,具有许多卷积层,后面接着最大池化层和全连接层。在ImageNet挑战中,VGG-16 模型在 1000 类图像的验证集上的 Top-5 分类错误率为 8.1%(单尺度方法):

首先,在project目录中创建一个名为nets.py的文件。以下代码定义了 VGG-16 模型的图:

    import tensorflow as tf 
    import numpy as np 

    def inference(images): 
    with tf.name_scope("preprocess"): 
        mean = tf.constant([123.68, 116.779, 103.939],  
    dtype=tf.float32, shape=[1, 1, 1, 3], name='img_mean') 
        input_images = images - mean 
    conv1_1 = _conv2d(input_images, 3, 3, 64, 1, 1,   
    name="conv1_1") 
    conv1_2 = _conv2d(conv1_1, 3, 3, 64, 1, 1, name="conv1_2") 
    pool1 = _max_pool(conv1_2, 2, 2, 2, 2, name="pool1") 

    conv2_1 = _conv2d(pool1, 3, 3, 128, 1, 1, name="conv2_1") 
    conv2_2 = _conv2d(conv2_1, 3, 3, 128, 1, 1, name="conv2_2") 
    pool2 = _max_pool(conv2_2, 2, 2, 2, 2, name="pool2") 

    conv3_1 = _conv2d(pool2, 3, 3, 256, 1, 1, name="conv3_1") 
    conv3_2 = _conv2d(conv3_1, 3, 3, 256, 1, 1, name="conv3_2") 
    conv3_3 = _conv2d(conv3_2, 3, 3, 256, 1, 1, name="conv3_3") 
    pool3 = _max_pool(conv3_3, 2, 2, 2, 2, name="pool3") 

    conv4_1 = _conv2d(pool3, 3, 3, 512, 1, 1, name="conv4_1") 
    conv4_2 = _conv2d(conv4_1, 3, 3, 512, 1, 1, name="conv4_2") 
    conv4_3 = _conv2d(conv4_2, 3, 3, 512, 1, 1, name="conv4_3") 
    pool4 = _max_pool(conv4_3, 2, 2, 2, 2, name="pool4") 

    conv5_1 = _conv2d(pool4, 3, 3, 512, 1, 1, name="conv5_1") 
    conv5_2 = _conv2d(conv5_1, 3, 3, 512, 1, 1, name="conv5_2") 
    conv5_3 = _conv2d(conv5_2, 3, 3, 512, 1, 1, name="conv5_3") 
    pool5 = _max_pool(conv5_3, 2, 2, 2, 2, name="pool5") 

    fc6 = _fully_connected(pool5, 4096, name="fc6") 
    fc7 = _fully_connected(fc6, 4096, name="fc7") 
    fc8 = _fully_connected(fc7, 1000, name='fc8', relu=False) 
    outputs = _softmax(fc8, name="output") 
    return outputs 

在上述代码中,有一些事项需要注意:

  • _conv2d_max_pool_fully_connected_softmax是分别定义卷积层、最大池化层、全连接层和 softmax 层的方法。我们稍后将实现这些方法。

  • preprocess命名空间中,我们定义了一个常量张量mean,它会从输入图像中减去。这是 VGG-16 模型训练时所用的均值向量,用于将图像的均值调整为零。

  • 接下来,我们使用这些参数定义卷积层、最大池化层和全连接层。

  • fc8层中,我们不对输出应用 ReLU 激活,而是将输出送入softmax层,以计算 1000 个类别的概率。

现在,我们将在nets.py文件中实现_conv2d_max_pool_fully_connected_softmax方法。

以下是_conv2d_max_pool方法的代码:

 def _conv2d(input_data, k_h, k_w, c_o, s_h, s_w, name, relu=True,  
 padding="SAME"): 
    c_i = input_data.get_shape()[-1].value 
    convolve = lambda i, k: tf.nn.conv2d(i, k, [1, s_h, s_w, 1],  
 padding=padding) 
    with tf.variable_scope(name) as scope: 
        weights = tf.get_variable(name="kernel", shape=[k_h, k_w,  
 c_i, c_o], 

 initializer=tf.truncated_normal_initializer(stddev=1e-1,  
 dtype=tf.float32)) 
        conv = convolve(input_data, weights) 
        biases = tf.get_variable(name="bias", shape=[c_o],  
 dtype=tf.float32, 

 initializer=tf.constant_initializer(value=0.0)) 
        output = tf.nn.bias_add(conv, biases) 
        if relu: 
            output = tf.nn.relu(output, name=scope.name) 
        return output 
 def _max_pool(input_data, k_h, k_w, s_h, s_w, name,  
 padding="SAME"): 
    return tf.nn.max_pool(input_data, ksize=[1, k_h, k_w, 1], 
                          strides=[1, s_h, s_w, 1], padding=padding,  
 name=name) 

如果你阅读过第四章,猫和狗,大部分上面的代码是自解释的,但仍有一些行需要稍作解释:

  • k_hk_w是卷积核的高度和宽度。

  • c_o表示通道输出,即卷积层特征图的数量。

  • s_hs_wtf.nn.conv2dtf.nn.max_pool层的步幅参数。

  • 使用tf.get_variable代替tf.Variable,因为在加载预训练权重时我们还需要再次使用get_variable

实现fully_connected层和softmax层非常简单:

 def _fully_connected(input_data, num_output, name, relu=True): 
    with tf.variable_scope(name) as scope: 
        input_shape = input_data.get_shape() 
        if input_shape.ndims == 4: 
            dim = 1 
            for d in input_shape[1:].as_list(): 
                dim *= d 
            feed_in = tf.reshape(input_data, [-1, dim]) 
        else: 
            feed_in, dim = (input_data, input_shape[-1].value) 
        weights = tf.get_variable(name="kernel", shape=[dim,  
 num_output], 

 initializer=tf.truncated_normal_initializer(stddev=1e-1,  
 dtype=tf.float32)) 
        biases = tf.get_variable(name="bias", shape=[num_output], 
 dtype=tf.float32, 

 initializer=tf.constant_initializer(value=0.0)) 
        op = tf.nn.relu_layer if relu else tf.nn.xw_plus_b 
        output = op(feed_in, weights, biases, name=scope.name) 
        return output 
 def _softmax(input_data, name): 
    return tf.nn.softmax(input_data, name=name) 

使用_fully_connected方法时,我们首先检查输入数据的维度,以便将输入数据重塑为正确的形状。然后,使用get_variable方法创建weightsbiases变量。最后,我们检查relu参数,决定是否应使用tf.nn.relu_layertf.nn.xw_plus_b对输出应用relutf.nn.relu_layer将计算relu(matmul(x, weights) + biases),而tf.nn.xw_plus_b则只计算matmul(x, weights) + biases

本节的最后一个方法用于将预训练的caffe权重加载到已定义的变量中:

   def load_caffe_weights(path, sess, ignore_missing=False): 
    print("Load caffe weights from ", path) 
    data_dict = np.load(path).item() 
    for op_name in data_dict: 
        with tf.variable_scope(op_name, reuse=True): 
            for param_name, data in   
    data_dict[op_name].iteritems(): 
                try: 
                    var = tf.get_variable(param_name) 
                    sess.run(var.assign(data)) 
                except ValueError as e: 
                    if not ignore_missing: 
                        print(e) 
                        raise e 

为了理解这个方法,我们必须了解数据是如何存储在预训练模型VGG16.npz中的。我们创建了一段简单的代码来打印预训练模型中的所有变量。你可以将以下代码放在nets.py的末尾,并使用 Python nets.py运行:

    if __name__ == "__main__": 
    path = "data/VGG16.npz" 
    data_dict = np.load(path).item() 
    for op_name in data_dict: 
        print(op_name) 
        for param_name, data in     ].iteritems(): 
            print("\t" + param_name + "\t" + str(data.shape)) 

这里是一些结果的代码行:

conv1_1
    weights (3, 3, 3, 64)
    biases  (64,)
conv1_2
    weights (3, 3, 64, 64)
    biases  (64,)

如你所见,op_name是层的名称,我们可以通过data_dict[op_name]访问每层的weightsbiases

让我们来看一下load_caffe_weights

  • 我们使用tf.variable_scopereuse=True参数,以便我们能获取图中定义的weightsbiases的准确变量。之后,我们运行 assign 方法为每个变量设置数据。

  • 如果变量名称未定义,get_variable方法将会抛出ValueError。因此,我们将使用ignore_missing变量来决定是否抛出错误。

测试预训练模型

我们已经创建了一个 VGG16 神经网络。在这一部分,我们将尝试使用预训练模型对汽车、猫和狗进行分类,以检查模型是否已成功加载。

nets.py文件中,我们需要将当前的__main__代码替换为以下代码:

    import os 
    from utils import debug_print 
    from scipy.misc import imread, imresize 

    if __name__ == "__main__": 
    SAMPLES_FOLDER = "samples_data" 
    with open('%s/imagenet-classes.txt' % SAMPLES_FOLDER, 'rb') as   
    infile: 
     class_labels = map(str.strip, infile.readlines()) 

    inputs = tf.placeholder(tf.float32, [None, 224, 224, 3],   
    name="inputs") 
    outputs = inference(inputs) 

    debug_print.print_variables(tf.global_variables()) 
    debug_print.print_variables([inputs, outputs]) 

    with tf.Session() as sess: 
     load_caffe_weights("data/VGG16.npz", sess,   
    ignore_missing=False) 

        files = os.listdir(SAMPLES_FOLDER) 
        for file_name in files: 
            if not file_name.endswith(".jpg"): 
                continue 
            print("=== Predict %s ==== " % file_name) 
            img = imread(os.path.join(SAMPLES_FOLDER, file_name),  
            mode="RGB") 
            img = imresize(img, (224, 224)) 

            prob = sess.run(outputs, feed_dict={inputs: [img]})[0] 
            preds = (np.argsort(prob)[::-1])[0:3] 

            for p in preds: 
                print class_labels[p], prob[p]

在前面的代码中,有几件事需要注意:

  • 我们使用debug_print.print_variables辅助方法,通过打印变量名称和形状来可视化所有变量。

  • 我们定义了一个名为inputs的占位符,形状为[None, 224, 224, 3],这是 VGG16 模型所需的输入大小:

      We get the model graph with outputs = inference(inputs). 
  • tf.Session()中,我们调用load_caffe_weights方法并设置ignore_missing=False,以确保能够加载预训练模型的所有权重和偏置。

  • 图片使用scipy中的imreadimresize方法加载和调整大小。然后,我们使用带有feed_dict字典的 sess.run方法,并接收预测结果。

  • 以下是我们在章节开始时提供的samples_datacar.jpgcat.jpgdog.jpg的预测结果:

    == Predict car.jpg ==== 
    racer, race car, racing car 0.666172
    sports car, sport car 0.315847
    car wheel 0.0117961
    === Predict cat.jpg ==== 
    Persian cat 0.762223
    tabby, tabby cat 0.0647032
    lynx, catamount 0.0371023
    === Predict dog.jpg ==== 
    Border collie 0.562288
    collie 0.239735
    Appenzeller 0.0186233

上述结果是这些图像的准确标签。这意味着我们已经成功加载了在 TensorFlow 中的预训练 VGG16 模型。在下一部分,我们将向你展示如何在我们的数据集上微调模型。

为我们的数据集训练模型

在这一部分,我们将演示如何创建数据集、微调模型以及导出模型以供生产使用。

Oxford-IIIT Pet 数据集简介

Oxford-IIIT Pet 数据集包含 37 种犬类和猫类,每个类别有 200 张图像,图像的尺度、姿势和光照变化较大。标注数据包含物种、头部位置和每张图片的像素分割。在我们的应用中,我们只使用物种名称作为模型的类别名称:

数据集统计

以下是狗狗和猫咪品种的数据集:

  1. 狗狗品种:
品种总数
美国斗牛犬200
美国比特犬200
巴吉犬200
小猎兔犬200
拳师犬199
吉娃娃200
英国可卡犬196
英国猎狐犬200
德国短毛指示犬200
大比利犬200
哈瓦那犬200
日本狆犬200
凯斯犬199
莱昂贝格犬200
迷你雪达犬200
纽芬兰犬196
博美犬200
哈巴狗200
圣伯纳犬200
萨摩耶犬200
苏格兰梗199
柴犬200
斯塔福郡斗牛梗189
小麦梗200
约克夏梗200
总计4978
  1. 猫品种:
品种数量
阿比西尼亚猫198
孟加拉猫200
伯曼猫200
孟买猫184
英国短毛猫200
埃及猫190
缅因猫200
波斯猫200
拉格多尔猫200
俄罗斯蓝猫200
暹罗猫199
无毛猫200
总计2371
  1. 宠物总数:
家族数量
2371
4978
总计7349

下载数据集

我们可以从牛津大学的网站www.robots.ox.ac.uk/~vgg/data/pets/获取数据集。我们需要下载数据集和真实标签数据,分别命名为images.tar.gzannotations.tar.gz。我们将 TAR 文件存储在data/datasets文件夹中,并解压所有.tar文件。确保data文件夹具有以下结构:

- data
-- VGG16.npz
-- datasets
---- annotations
------ trainval.txt
---- images
------ *.jpg

准备数据

在开始训练过程之前,我们需要将数据集预处理为更简单的格式,以便在进一步的自动微调中使用。

首先,我们在project文件夹中创建一个名为scripts的 Python 包。然后,我们创建一个名为convert_oxford_data.py的 Python 文件,并添加以下代码:

    import os 
    import tensorflow as tf 
    from tqdm import tqdm 
    from scipy.misc import imread, imsave 

    FLAGS = tf.app.flags.FLAGS 

    tf.app.flags.DEFINE_string( 
    'dataset_dir', 'data/datasets', 
    'The location of Oxford IIIT Pet Dataset which contains    
     annotations and images folders' 
    ) 

    tf.app.flags.DEFINE_string( 
    'target_dir', 'data/train_data', 
    'The location where all the images will be stored' 
    ) 

    def ensure_folder_exists(folder_path): 
    if not os.path.exists(folder_path): 
        os.mkdir(folder_path) 
    return folder_path 

    def read_image(image_path): 
    try: 
        image = imread(image_path) 
        return image 
    except IOError: 
        print(image_path, "not readable") 
    return None 

在这段代码中,我们使用tf.app.flags.FLAGS来解析参数,以便轻松定制脚本。我们还创建了两个helper方法来创建目录和读取图像。

接下来,我们添加以下代码,将 Oxford 数据集转换为我们喜欢的格式:

 def convert_data(split_name, save_label=False): 
    if split_name not in ["trainval", "test"]: 
    raise ValueError("split_name is not recognized!") 
    target_split_path =  
    ensure_folder_exists(os.path.join(FLAGS.target_dir, split_name)) 
    output_file = open(os.path.join(FLAGS.target_dir, split_name +  
    ".txt"), "w") 

    image_folder = os.path.join(FLAGS.dataset_dir, "images") 
    anno_folder = os.path.join(FLAGS.dataset_dir, "annotations") 

    list_data = [line.strip() for line in open(anno_folder + "/" +  
    split_name + ".txt")] 

    class_name_idx_map = dict() 
    for data in tqdm(list_data, desc=split_name): 
      file_name,class_index,species,breed_id = data.split(" ") 
      file_label = int(class_index) - 1 

      class_name = "_".join(file_name.split("_")[0:-1]) 
      class_name_idx_map[class_name] = file_label 

      image_path = os.path.join(image_folder, file_name + ".jpg") 
      image = read_image(image_path) 
      if image is not None: 
      target_class_dir =  
       ensure_folder_exists(os.path.join(target_split_path,    
       class_name)) 
      target_image_path = os.path.join(target_class_dir,  
       file_name + ".jpg") 
            imsave(target_image_path, image) 
            output_file.write("%s %s\n" % (file_label,  
            target_image_path)) 

    if save_label: 
        label_file = open(os.path.join(FLAGS.target_dir,  
        "labels.txt"), "w") 
        for class_name in sorted(class_name_idx_map,  
        key=class_name_idx_map.get): 
        label_file.write("%s\n" % class_name) 

 def main(_): 
    if not FLAGS.dataset_dir: 
    raise ValueError("You must supply the dataset directory with  
    --dataset_dir") 

    ensure_folder_exists(FLAGS.target_dir) 
    convert_data("trainval", save_label=True) 
    convert_data("test") 

 if __name__ == "__main__": 
    tf.app.run() 

现在,我们可以使用以下代码运行scripts

python scripts/convert_oxford_data.py --dataset_dir data/datasets/ --target_dir data/train_data.

脚本读取 Oxford-IIIT 数据集的真实标签data,并在data/train_data中创建一个新的dataset,其结构如下:

- train_data
-- trainval.txt
-- test.txt
-- labels.txt
-- trainval
---- Abyssinian
---- ...
-- test
---- Abyssinian
---- ...

让我们稍微讨论一下这些内容:

  • labels.txt包含了我们数据集中 37 个物种的列表。

  • trainval.txt包含了我们将在训练过程中使用的图像列表,格式为<class_id> <image_path>

  • test.txt包含了我们将用来检验模型准确度的图像列表。test.txt的格式与trainval.txt相同。

  • trainvaltest文件夹包含 37 个子文件夹,这些文件夹分别是每个类别的名称,且每个类别的所有图像都包含在相应的文件夹中。

设置训练和测试的输入管道

TensorFlow 允许我们创建一个可靠的输入管道,方便快速的训练。在这一部分中,我们将实现tf.TextLineReader来读取训练和测试文本文件。我们将使用tf.train.batch来并行读取和预处理图像。

首先,我们需要在project目录中创建一个名为datasets.py的新 Python 文件,并添加以下代码:

    import tensorflow as tf 
    import os 

    def load_files(filenames): 
    filename_queue = tf.train.string_input_producer(filenames) 
    line_reader = tf.TextLineReader() 
    key, line = line_reader.read(filename_queue) 
    label, image_path = tf.decode_csv(records=line, 

    record_defaults=[tf.constant([], dtype=tf.int32),   
    tf.constant([], dtype=tf.string)], 
                                      field_delim=' ') 
    file_contents = tf.read_file(image_path) 
    image = tf.image.decode_jpeg(file_contents, channels=3) 

    return image, label 

load_files方法中,我们使用tf.TextLineReader读取文本文件的每一行,例如trainval.txttest.txttf.TextLineReader需要一个字符串队列来读取数据,因此我们使用tf.train.string_input_producer来存储文件名。之后,我们将行变量传入tf.decode_cvs,以便获取labelfilename。图片可以通过tf.image.decode_jpeg轻松读取。

现在我们已经能够加载图片,我们可以继续进行,创建image批次和label批次用于training

datasets.py中,我们需要添加一个新方法:

 def input_pipeline(dataset_dir, batch_size, num_threads=8,   
    is_training=True, shuffle=True): 
    if is_training: 
        file_names = [os.path.join(dataset_dir, "trainval.txt")] 
    else: 
        file_names = [os.path.join(dataset_dir, "test.txt")] 
    image, label = load_files(file_names) 

    image = preprocessing(image, is_training) 

    min_after_dequeue = 1000 
    capacity = min_after_dequeue + 3 * batch_size 
    if shuffle: 
     image_batch, label_batch = tf.train.shuffle_batch( 
     [image, label], batch_size, capacity,  
     min_after_dequeue, num_threads 
      ) 
    else: 
        image_batch, label_batch = tf.train.batch( 
            [image, label], batch_size, num_threads, capacity 
            ) 
    return image_batch, label_batch

我们首先使用load_files方法加载imagelabel。然后,我们通过一个新的预处理方法处理图片,该方法稍后将实现。最后,我们将imagelabel传入tf.train.shuffle_batch进行训练,并通过tf.train.batch进行测试:

 def preprocessing(image, is_training=True, image_size=224,  
 resize_side_min=256, resize_side_max=312): 
    image = tf.cast(image, tf.float32) 

    if is_training: 
        resize_side = tf.random_uniform([], minval=resize_side_min,  
        maxval=resize_side_max+1, dtype=tf.int32) 
        resized_image = _aspect_preserving_resize(image,  
        resize_side) 

        distorted_image = tf.random_crop(resized_image, [image_size,  
        image_size, 3]) 

        distorted_image =  
        tf.image.random_flip_left_right(distorted_image) 

        distorted_image =  
        tf.image.random_brightness(distorted_image, max_delta=50) 

        distorted_image = tf.image.random_contrast(distorted_image,  
        lower=0.2, upper=2.0) 

        return distorted_image 
    else: 
        resized_image = _aspect_preserving_resize(image, image_size) 
        return tf.image.resize_image_with_crop_or_pad(resized_image,  
        image_size, image_size)

在训练和测试中,预处理有两种不同的方法。在训练中,我们需要增强数据,以从当前数据集中创建更多的训练数据。预处理方法中使用了几种技术:

  • 数据集中的图片可能有不同的分辨率,但我们只需要 224x224 的图片。因此,在执行random_crop之前,我们需要将图像调整为合适的大小。下图描述了裁剪是如何工作的。_aspect_preserving_resize方法将稍后实现:

  • 在裁剪图片后,我们通过tf.image.random_flip_left_righttf.image.random_brightnesstf.image.random_contrast对图片进行失真处理,从而生成新的训练样本。

  • 在测试过程中,我们只需要通过_aspect_preserving_resizetf.image.resize_image_with_crop_or_pad来调整图片大小。tf.image.resize_image_with_crop_or_pad允许我们对图像进行中心裁剪或填充,以匹配目标的widthheight

现在,我们需要将最后两个方法添加到datasets.py中,如下所示:

    def _smallest_size_at_least(height, width, smallest_side): 
      smallest_side = tf.convert_to_tensor(smallest_side,   
      dtype=tf.int32) 

      height = tf.to_float(height) 
      width = tf.to_float(width) 
      smallest_side = tf.to_float(smallest_side) 

      scale = tf.cond(tf.greater(height, width), 
                    lambda: smallest_side / width, 
                    lambda: smallest_side / height) 
      new_height = tf.to_int32(height * scale) 
      new_width = tf.to_int32(width * scale) 
      return new_height, new_width 

    def _aspect_preserving_resize(image, smallest_side): 
      smallest_side = tf.convert_to_tensor(smallest_side,   
      dtype=tf.int32) 
      shape = tf.shape(image) 
      height = shape[0] 
      width = shape[1] 
      new_height, new_width = _smallest_size_at_least(height, width,   
      smallest_side) 
      image = tf.expand_dims(image, 0) 
      resized_image = tf.image.resize_bilinear(image, [new_height,   
      new_width], align_corners=False) 
      resized_image = tf.squeeze(resized_image) 
      resized_image.set_shape([None, None, 3]) 
      return resized_image 

到目前为止,我们已经做了很多工作来准备datasetinput管道。在接下来的部分中,我们将为我们的datasetlossaccuracytraining操作定义模型,以执行training过程。

定义模型

我们的应用程序需要对37类狗和猫进行分类。VGG16 模型支持 1,000 种不同的类别。在我们的应用中,我们将重用所有层直到fc7层,并从头开始训练最后一层。为了使模型输出37个类别,我们需要修改nets.py中的推理方法,如下所示:

    def inference(images, is_training=False): 
    # 
    # All the code before fc7 are not modified. 
    # 
    fc7 = _fully_connected(fc6, 4096, name="fc7") 
    if is_training: 
        fc7 = tf.nn.dropout(fc7, keep_prob=0.5) 
    fc8 = _fully_connected(fc7, 37, name='fc8-pets', relu=False) 
    return fc8
  • 我们向方法中添加了一个新参数is_training。在fc7层之后,如果推理是训练过程,我们将添加一个tf.nn.dropout层。这个 dropout 层有助于模型在面对未见过的数据时更好地正则化,并避免过拟合。

  • fc8层的输出数量从 1,000 改为 37。此外,fc8层的名称必须更改为另一个名称;在这种情况下,我们选择fc8-pets。如果不更改fc8层的名称,load_caffe_weights仍然会找到新层并分配原始权重,而这些权重的大小与我们新的fc8层不同。

  • 推理方法最后的softmax层也被移除了,因为我们稍后将使用的loss函数只需要未经归一化的输出。

定义训练操作

我们将在一个新的 Python 文件models.py中定义所有操作。首先,让我们创建一些操作来计算lossaccuracy

 def compute_loss(logits, labels): 
   labels = tf.squeeze(tf.cast(labels, tf.int32)) 

   cross_entropy =   
   tf.nn.sparse_softmax_cross_entropy_with_logits(logits=logits,    
   labels=labels) 
   cross_entropy_mean = tf.reduce_mean(cross_entropy) 
   tf.add_to_collection('losses', cross_entropy_mean) 

   return tf.add_n(tf.get_collection('losses'),    
   name='total_loss') 

 def compute_accuracy(logits, labels): 
   labels = tf.squeeze(tf.cast(labels, tf.int32)) 
   batch_predictions = tf.cast(tf.argmax(logits, 1), tf.int32) 
   predicted_correctly = tf.equal(batch_predictions, labels) 
   accuracy = tf.reduce_mean(tf.cast(predicted_correctly,   
   tf.float32)) 
   return accuracy

在这些方法中,logits是模型的输出,labels是来自dataset的真实数据。在compute_loss方法中,我们使用tf.nn.sparse_softmax_cross_entropy_with_logits,因此我们不需要通过softmax方法对logits进行归一化。此外,我们也不需要将labels转换为一个热编码向量。在compute_accuracy方法中,我们通过tf.argmaxlogits中的最大值与labels进行比较,从而得到accuracy

接下来,我们将定义learning_rateoptimizer的操作:

 def get_learning_rate(global_step, initial_value, decay_steps,          
   decay_rate): 
   learning_rate = tf.train.exponential_decay(initial_value,   
   global_step, decay_steps, decay_rate, staircase=True) 
   return learning_rate 

 def train(total_loss, learning_rate, global_step, train_vars): 

   optimizer = tf.train.AdamOptimizer(learning_rate) 

   train_variables = train_vars.split(",") 

   grads = optimizer.compute_gradients( 
       total_loss, 
       [v for v in tf.trainable_variables() if v.name in   
       train_variables] 
       ) 
   train_op = optimizer.apply_gradients(grads,   
   global_step=global_step) 
   return train_op 

train方法中,我们配置了optimizer仅对train_vars字符串中定义的某些变量进行compute和应用gradients。这样,我们只更新最后一层fc8weightsbiases,而冻结其他层。train_vars是一个包含通过逗号分隔的变量列表的字符串,例如models/fc8-pets/weights:0,models/fc8-pets/biases:0

执行训练过程

现在我们已经准备好训练模型了。让我们在scripts文件夹中创建一个名为train.py的 Python 文件。首先,我们需要为training过程定义一些参数:

 import tensorflow as tf 
 import os 
 from datetime import datetime 
 from tqdm import tqdm 

 import nets, models, datasets 

 # Dataset 
 dataset_dir = "data/train_data" 
 batch_size = 64 
 image_size = 224 

 # Learning rate 
 initial_learning_rate = 0.001 
 decay_steps = 250 
 decay_rate = 0.9 

 # Validation 
 output_steps = 10  # Number of steps to print output 
 eval_steps = 20  # Number of steps to perform evaluations 

 # Training 
 max_steps = 3000  # Number of steps to perform training 
 save_steps = 200  # Number of steps to perform saving checkpoints 
 num_tests = 5  # Number of times to test for test accuracy 
 max_checkpoints_to_keep = 3 
 save_dir = "data/checkpoints" 
 train_vars = 'models/fc8-pets/weights:0,models/fc8-pets/biases:0' 

 # Export 
 export_dir = "/tmp/export/" 
 export_name = "pet-model" 
 export_version = 2 

这些变量是不言自明的。接下来,我们需要定义一些用于training的操作,如下所示:

 images, labels = datasets.input_pipeline(dataset_dir, batch_size,   
 is_training=True) 
 test_images, test_labels = datasets.input_pipeline(dataset_dir,  
 batch_size, is_training=False) 

 with tf.variable_scope("models") as scope: 
    logits = nets.inference(images, is_training=True) 
    scope.reuse_variables() 
    test_logits = nets.inference(test_images, is_training=False) 

 total_loss = models.compute_loss(logits, labels) 
 train_accuracy = models.compute_accuracy(logits, labels) 
 test_accuracy = models.compute_accuracy(test_logits, test_labels) 

 global_step = tf.Variable(0, trainable=False) 
 learning_rate = models.get_learning_rate(global_step,  
 initial_learning_rate, decay_steps, decay_rate) 
 train_op = models.train(total_loss, learning_rate, global_step,  
 train_vars) 

 saver = tf.train.Saver(max_to_keep=max_checkpoints_to_keep) 
 checkpoints_dir = os.path.join(save_dir,  
 datetime.now().strftime("%Y-%m-%d_%H-%M-%S")) 
 if not os.path.exists(save_dir): 
    os.mkdir(save_dir) 
 if not os.path.exists(checkpoints_dir): 
    os.mkdir(checkpoints_dir) 

这些操作是通过调用我们在datasets.pynets.pymodels.py中定义的方法创建的。在这段代码中,我们为训练创建了一个输入管道,并为测试创建了另一个管道。之后,我们创建了一个新的variable_scope,命名为models,并通过nets.inference方法创建了logitstest_logits。你必须确保添加了scope.reuse_variables,因为我们希望在测试中重用训练中的weightsbiases。最后,我们创建了一个saver和一些目录,以便每隔save_steps保存检查点。

training过程的最后一部分是training循环:

 with tf.Session() as sess: 
    sess.run(tf.global_variables_initializer()) 
    coords = tf.train.Coordinator() 
    threads = tf.train.start_queue_runners(sess=sess, coord=coords) 

    with tf.variable_scope("models"): 
       nets.load_caffe_weights("data/VGG16.npz", sess,  
       ignore_missing=True) 

    last_saved_test_accuracy = 0 
    for i in tqdm(range(max_steps), desc="training"): 
                  _, loss_value, lr_value = sess.run([train_op,    
                  total_loss,  learning_rate]) 

      if (i + 1) % output_steps == 0: 
          print("Steps {}: Loss = {:.5f} Learning Rate =  
          {}".format(i + 1, loss_value, lr_value)) 

      if (i + 1) % eval_steps == 0: 
          test_acc, train_acc, loss_value =  
          sess.run([test_accuracy, train_accuracy, total_loss]) 
          print("Test accuracy {} Train accuracy {} : Loss =  
          {:.5f}".format(test_acc, train_acc, loss_value)) 

      if (i + 1) % save_steps == 0 or i == max_steps - 1: 
          test_acc = 0 
          for i in range(num_tests): 
              test_acc += sess.run(test_accuracy) 
          test_acc /= num_tests 
      if test_acc > last_saved_test_accuracy: 
            print("Save steps: Test Accuracy {} is higher than  
            {}".format(test_acc, last_saved_test_accuracy)) 
             last_saved_test_accuracy = test_acc 
             saved_file = saver.save(sess, 

     os.path.join(checkpoints_dir, 'model.ckpt'), 
                  global_step=global_step) 
          print("Save steps: Save to file %s " % saved_file) 
      else: 
          print("Save steps: Test Accuracy {} is not higher  
                than {}".format(test_acc, last_saved_test_accuracy)) 

    models.export_model(checkpoints_dir, export_dir, export_name,  
    export_version) 

    coords.request_stop() 
    coords.join(threads) 

training循环很容易理解。首先,我们加载预训练的VGG16模型,并将ignore_missing设置为True,因为我们之前更改了fc8层的名称。然后,我们循环max_steps步,在每output_steps步时打印loss,每eval_steps步时打印test_accuracy。每save_steps步,我们检查并保存检查点,如果当前的测试准确率高于之前的准确率。我们仍然需要创建models.export_model,以便在training之后导出模型供服务使用。不过,在继续之前,你可能想要先检查一下training过程是否正常工作。让我们注释掉以下这一行:

    models.export_model(checkpoints_dir, export_dir, export_name,  
    export_version) 

然后,使用以下命令运行training脚本:

python scripts/train.py

这是控制台中的一些输出。首先,我们的脚本加载了预训练的模型。然后,它会输出loss

('Load caffe weights from ', 'data/VGG16.npz')
training:   0%|▏                | 9/3000 [00:05<24:59,  1.99it/s]
Steps 10: Loss = 31.10747 Learning Rate = 0.0010000000475
training:   1%|▎                | 19/3000 [00:09<19:19,  2.57it/s]
Steps 20: Loss = 34.43741 Learning Rate = 0.0010000000475
Test accuracy 0.296875 Train accuracy 0.0 : Loss = 31.28600
training:   1%|▍                | 29/3000 [00:14<20:01,  2.47it/s]
Steps 30: Loss = 15.81103 Learning Rate = 0.0010000000475
training:   1%|▌                | 39/3000 [00:18<19:42,  2.50it/s]
Steps 40: Loss = 14.07709 Learning Rate = 0.0010000000475
Test accuracy 0.53125 Train accuracy 0.03125 : Loss = 20.65380  

现在,让我们停止training并取消注释export_model方法。我们需要models.export_model方法将最新的模型(具有最高测试准确率)导出到名为export_name、版本为export_versionexport_dir文件夹中。

为生产环境导出模型

 def export_model(checkpoint_dir, export_dir, export_name,  
 export_version): 
    graph = tf.Graph() 
    with graph.as_default(): 
        image = tf.placeholder(tf.float32, shape=[None, None, 3]) 
        processed_image = datasets.preprocessing(image,  
        is_training=False) 
        with tf.variable_scope("models"): 
         logits = nets.inference(images=processed_image,  
          is_training=False) 

        model_checkpoint_path =  
        get_model_path_from_ckpt(checkpoint_dir) 
        saver = tf.train.Saver() 

        config = tf.ConfigProto() 
        config.gpu_options.allow_growth = True 
        config.gpu_options.per_process_gpu_memory_fraction = 0.7 

        with tf.Session(graph=graph) as sess: 
            saver.restore(sess, model_checkpoint_path) 
            export_path = os.path.join(export_dir, export_name,  
            str(export_version)) 
            export_saved_model(sess, export_path, image, logits) 
            print("Exported model at", export_path)

export_model方法中,我们需要创建一个新的图表来在生产中运行。在生产中,我们不需要像training那样的所有变量,也不需要输入管道。然而,我们需要使用export_saved_model方法导出模型,具体如下:

 def export_saved_model(sess, export_path, input_tensor,  
 output_tensor): 
    from tensorflow.python.saved_model import builder as  
 saved_model_builder 
    from tensorflow.python.saved_model import signature_constants 
    from tensorflow.python.saved_model import signature_def_utils 
    from tensorflow.python.saved_model import tag_constants 
    from tensorflow.python.saved_model import utils 
    builder = saved_model_builder.SavedModelBuilder(export_path) 

    prediction_signature = signature_def_utils.build_signature_def( 
        inputs={'images': utils.build_tensor_info(input_tensor)}, 
        outputs={ 
            'scores': utils.build_tensor_info(output_tensor) 
        }, 
        method_name=signature_constants.PREDICT_METHOD_NAME) 

    legacy_init_op = tf.group( 
        tf.tables_initializer(), name='legacy_init_op') 
    builder.add_meta_graph_and_variables( 
        sess, [tag_constants.SERVING], 
        signature_def_map={ 
          'predict_images': 
           prediction_signature, 
        }, 
        legacy_init_op=legacy_init_op) 

    builder.save() 

通过这种方法,我们可以为生产环境创建一个模型的元图。我们将在后面的部分介绍如何在生产中提供模型服务。现在,让我们运行scripts,在 3000 步后自动训练并导出:

python scripts/train.py

在我们的系统中,使用 Core i7-4790 CPU 和一块 TITAN-X GPU,训练过程需要 20 分钟才能完成。以下是我们控制台中的最后几条输出:

Steps 3000: Loss = 0.59160 Learning Rate = 0.000313810509397
Test accuracy 0.659375 Train accuracy 0.853125: Loss = 0.25782
Save steps: Test Accuracy 0.859375 is not higher than 0.921875
training: 100%|██████████████████| 3000/3000 [23:40<00:00,  1.27it/s]
    I tensorflow/core/common_runtime/gpu/gpu_device.cc:975] Creating TensorFlow device (/gpu:0) -> (device: 0, name: GeForce GTX TITAN X, pci bus id: 0000:01:00.0)
    ('Exported model at', '/home/ubuntu/models/pet-model/1')

很棒!我们有一个具有 92.18%测试准确率的模型。我们还得到了一个导出的模型文件(.pb 格式)。export_dir文件夹将具有以下结构:

- /home/ubuntu/models/
-- pet_model
---- 1
------ saved_model.pb
------ variables

在生产环境中提供模型服务

在生产中,我们需要创建一个端点,用户可以通过该端点发送图像并接收结果。在 TensorFlow 中,我们可以轻松地使用 TensorFlow Serving 来提供我们的模型服务。在本节中,我们将安装 TensorFlow Serving,并创建一个 Flask 应用,允许用户通过 Web 界面上传他们的图像。

设置 TensorFlow Serving

在你的生产服务器中,你需要安装 TensorFlow Serving 及其前提条件。你可以访问 TensorFlow Serving 的官方网站:tensorflow.github.io/serving/setup。接下来,我们将使用 TensorFlow Serving 提供的标准 TensorFlow 模型服务器来提供模型服务。首先,我们需要使用以下命令构建tensorflow_model_server

bazel build   
//tensorflow_serving/model_servers:tensorflow_model_server

将训练服务器中的/home/ubuntu/models/pet_model文件夹中的所有文件复制到生产服务器中。在我们的设置中,我们选择/home/ubuntu/productions作为存放所有生产模型的文件夹。productions文件夹将具有以下结构:

- /home/ubuntu/productions/
-- 1
---- saved_model.pb
---- variables

我们将使用tmux来保持模型服务器的运行。让我们通过以下命令安装tmux

sudo apt-get install tmux

使用以下命令运行tmux会话:

tmux new -s serving

tmux会话中,让我们切换到tensorflow_serving目录并运行以下命令:

    bazel-bin/tensorflow_serving/model_servers/tensorflow_model_server --port=9000 --model_name=pet-model --model_base_path=/home/ubuntu/productions

控制台的输出应如下所示:

    2017-05-29 13:44:32.203153: I external/org_tensorflow/tensorflow/cc/saved_model/loader.cc:274] Loading SavedModel: success. Took 537318 microseconds.
    2017-05-29 13:44:32.203243: I tensorflow_serving/core/loader_harness.cc:86] Successfully loaded servable version {name: pet-model version: 1}
    2017-05-29 13:44:32.205543: I tensorflow_serving/model_servers/main.cc:298] Running ModelServer at 0.0.0.0:9000 ...  

如你所见,模型在主机0.0.0.0和端口9000上运行。在下一节中,我们将创建一个简单的 Python 客户端,通过 gRPC 将图像发送到该服务器。

你还应该注意,当前在生产服务器上使用的是 CPU 进行服务。使用 GPU 构建 TensorFlow Serving 超出了本章的范围。如果你更倾向于使用 GPU 进行服务,可以阅读附录 A*,高级安装*,它解释了如何构建支持 GPU 的 TensorFlow 和 TensorFlow Serving。

运行和测试模型

在项目仓库中,我们已经提供了一个名为production的包。在这个包中,我们需要将labels.txt文件复制到我们的dataset中,创建一个新的 Python 文件client.py,并添加以下代码:

    import tensorflow as tf 
    import numpy as np 
    from tensorflow_serving.apis import prediction_service_pb2,     
    predict_pb2 
    from grpc.beta import implementations 
    from scipy.misc import imread 
    from datetime import datetime 

    class Output: 
    def __init__(self, score, label): 
        self.score = score 
        self.label = label 

    def __repr__(self): 
        return "Label: %s Score: %.2f" % (self.label, self.score) 

    def softmax(x): 
    return np.exp(x) / np.sum(np.exp(x), axis=0) 

    def process_image(path, label_data, top_k=3): 
    start_time = datetime.now() 
    img = imread(path) 

    host, port = "0.0.0.0:9000".split(":") 
    channel = implementations.insecure_channel(host, int(port)) 
    stub =  
    prediction_service_pb2.beta_create_PredictionService_stub(channel) 

    request = predict_pb2.PredictRequest() 
    request.model_spec.name = "pet-model" 
    request.model_spec.signature_name = "predict_images" 

    request.inputs["images"].CopyFrom( 
        tf.contrib.util.make_tensor_proto( 
            img.astype(dtype=float), 
            shape=img.shape, dtype=tf.float32 
        ) 
    ) 

    result = stub.Predict(request, 20.) 
    scores =    
    tf.contrib.util.make_ndarray(result.outputs["scores"])[0] 
    probs = softmax(scores) 
    index = sorted(range(len(probs)), key=lambda x: probs[x],  
    reverse=True) 

    outputs = [] 
    for i in range(top_k): 
        outputs.append(Output(score=float(probs[index[i]]),  
        label=label_data[index[i]])) 

    print(outputs) 
    print("total time", (datetime.now() -   
    start_time).total_seconds()) 
    return outputs 

    if __name__ == "__main__": 
    label_data = [line.strip() for line in   
    open("production/labels.txt", 'r')] 
    process_image("samples_data/dog.jpg", label_data) 
    process_image("samples_data/cat.jpg", label_data) 

在此代码中,我们创建了一个process_image方法,该方法将从图片路径读取图像,并使用一些 TensorFlow 方法创建张量,然后通过 gRPC 将其发送到模型服务器。我们还创建了一个Output类,以便我们可以轻松地将其返回给caller方法。在方法结束时,我们打印输出和总时间,以便我们可以更轻松地调试。我们可以运行此 Python 文件,看看process_image是否有效:

python production/client.py

输出应如下所示:

    [Label: saint_bernard Score: 0.78, Label: american_bulldog Score: 0.21, Label: staffordshire_bull_terrier Score: 0.00]
    ('total time', 14.943942)
    [Label: Maine_Coon Score: 1.00, Label: Ragdoll Score: 0.00, Label: Bengal Score: 0.00]
    ('total time', 14.918235)

我们得到了正确的结果。然而,每张图片的处理时间几乎是 15 秒。原因是我们正在使用 CPU 模式的 TensorFlow Serving。如前所述,你可以在附录 A 中构建支持 GPU 的 TensorFlow Serving,高级安装。如果你跟随那个教程,你将得到以下结果:

    [Label: saint_bernard Score: 0.78, Label: american_bulldog Score: 0.21, Label: staffordshire_bull_terrier Score: 0.00]
    ('total time', 0.493618)
    [Label: Maine_Coon Score: 1.00, Label: Ragdoll Score: 0.00, Label: Bengal Score: 0.00]
    ('total time', 0.023753)

第一次调用时的处理时间是 493 毫秒。然而,之后的调用时间将只有大约 23 毫秒,比 CPU 版本快得多。

设计 Web 服务器

在本节中,我们将设置一个 Flask 服务器,允许用户上传图片,并在模型出错时设置正确的标签。我们已经在生产包中提供了所需的代码。实现带有数据库支持的 Flask 服务器超出了本章的范围。在本节中,我们将描述 Flask 的所有要点,以便你能更好地跟随和理解。

允许用户上传和修正标签的主要流程可以通过以下线框图进行描述。

该流程通过以下路由实现:

路由方法描述
/GET此路由返回一个网页表单,供用户上传图片。
/upload_imagePOST这个路由从 POST 数据中获取图像,将其保存到上传目录,并调用 client.py 中的 process_image 来识别图像并将结果保存到数据库。
/results<result_id>GET这个路由返回数据库中对应行的结果。
/results<result_id>POST这个路由将用户的标签保存到数据库,以便我们可以稍后微调模型。
/user-labelsGET这个路由返回所有用户标注图像的列表。在微调过程中,我们会调用此路由获取标注图像的列表。
/modelPOST这个路由允许从训练服务器启动微调过程,提供一个新的训练模型。此路由接收压缩模型的链接、版本号、检查点名称以及模型名称。
/modelGET这个路由返回数据库中最新的模型。微调过程将调用此路由来获取最新的模型并从中进行微调。

我们应该在 tmux 会话中运行此服务器,使用以下命令:

tmux new -s "flask"
python production/server.py

测试系统

现在,我们可以通过 http://0.0.0.0:5000 访问服务器。

首先,你会看到一个表单,用来选择并提交一张图像。

网站会被重定向到 /results 页面,显示对应的图像及其结果。用户标签字段为空。页面底部也有一个简短的表单,你可以提交模型的更正标签。

在生产环境中进行自动微调

在运行系统一段时间后,我们将拥有一些用户标注的图像。我们将创建一个微调过程,使其每天自动运行,并使用新数据微调最新的模型。

让我们在 scripts 文件夹中创建一个名为 finetune.py 的文件。

加载用户标注的数据

首先,我们将添加代码以从生产服务器下载所有用户标注的图像:

    import tensorflow as tf 
    import os 
    import json 
    import random 
    import requests 
    import shutil 
    from scipy.misc import imread, imsave 
    from datetime import datetime 
    from tqdm import tqdm 

    import nets, models, datasets 

    def ensure_folder_exists(folder_path): 
    if not os.path.exists(folder_path): 
        os.mkdir(folder_path) 
    return folder_path 

    def download_user_data(url, user_dir, train_ratio=0.8): 
    response = requests.get("%s/user-labels" % url) 
    data = json.loads(response.text) 

    if not os.path.exists(user_dir): 
        os.mkdir(user_dir) 
    user_dir = ensure_folder_exists(user_dir) 
    train_folder = ensure_folder_exists(os.path.join(user_dir,   
    "trainval")) 
    test_folder = ensure_folder_exists(os.path.join(user_dir,   
    "test")) 

    train_file = open(os.path.join(user_dir, 'trainval.txt'), 'w') 
    test_file = open(os.path.join(user_dir, 'test.txt'), 'w') 

    for image in data: 
        is_train = random.random() < train_ratio 
        image_url = image["url"] 
        file_name = image_url.split("/")[-1] 
        label = image["label"] 
        name = image["name"] 

        if is_train: 
          target_folder =  
          ensure_folder_exists(os.path.join(train_folder, name)) 
        else: 
          target_folder =   
          ensure_folder_exists(os.path.join(test_folder, name)) 

        target_file = os.path.join(target_folder, file_name) +   
        ".jpg" 

        if not os.path.exists(target_file): 
            response = requests.get("%s%s" % (url, image_url)) 
            temp_file_path = "/tmp/%s" % file_name 
            with open(temp_file_path, 'wb') as f: 
                for chunk in response: 
                    f.write(chunk) 

            image = imread(temp_file_path) 
            imsave(target_file, image) 
            os.remove(temp_file_path) 
            print("Save file: %s" % target_file) 

        label_path = "%s %s\n" % (label, target_file) 
        if is_train: 
            train_file.write(label_path) 
        else: 
            test_file.write(label_path) 

download_user_data 中,我们调用 /user-labels 端点获取用户标注图像的列表。JSON 的格式如下:

   [ 
    { 
     "id": 1,  
     "label": 0,  
     "name": "Abyssinian",  
     "url": "/uploads/2017-05-23_14-56-45_Abyssinian-cat.jpeg" 
    },  
    { 
     "id": 2,  
      "label": 32,  
      "name": "Siamese",  
     "url": "/uploads/2017-05-23_14-57-33_fat-Siamese-cat.jpeg" 
    } 
   ] 

在这个 JSON 中,label 是用户选择的标签,URL 是用来下载图像的链接。对于每一张图像,我们会将其下载到 tmp 文件夹,并使用 scipy 中的 imreadimsave 来确保图像是 JPEG 格式。我们还会创建 trainval.txttest.txt 文件,和训练数据集中的文件一样。

对模型进行微调

为了微调模型,我们需要知道哪个是最新的模型及其对应的检查点,以恢复 weightsbiases。因此,我们调用 /model 端点来获取检查点名称和版本号:

    def get_latest_model(url): 
    response = requests.get("%s/model" % url) 
    data = json.loads(response.text) 
    print(data) 
    return data["ckpt_name"], int(data["version"]) 

响应的 JSON 应该是这样的:

    { 
     "ckpt_name": "2017-05-26_02-12-49",  
     "id": 10,  
     "link": "http://1.53.110.161:8181/pet-model/8.zip",  
     "name": "pet-model",  
     "version": 8 
    } 

现在,我们将实现微调模型的代码。让我们从一些参数开始:

    # Server info 
    URL = "http://localhost:5000" 
    dest_api = URL + "/model" 

    # Server Endpoints 
    source_api = "http://1.53.110.161:8181" 

    # Dataset 
    dataset_dir = "data/train_data" 
    user_dir = "data/user_data" 
    batch_size = 64 
    image_size = 224 

    # Learning rate 
    initial_learning_rate = 0.0001 
    decay_steps = 250 
    decay_rate = 0.9 

    # Validation 
    output_steps = 10  # Number of steps to print output 
    eval_steps = 20  # Number of steps to perform evaluations 

    # Training 
    max_steps = 3000  # Number of steps to perform training 
    save_steps = 200  # Number of steps to perform saving    
    checkpoints 
    num_tests = 5  # Number of times to test for test accuracy 
    max_checkpoints_to_keep = 1 
    save_dir = "data/checkpoints" 
    train_vars = 'models/fc8-pets/weights:0,models/fc8- 
    pets/biases:0' 

    # Get the latest model 
    last_checkpoint_name, last_version = get_latest_model(URL) 
    last_checkpoint_dir = os.path.join(save_dir,   
    last_checkpoint_name) 

    # Export 
    export_dir = "/home/ubuntu/models/" 
    export_name = "pet-model" 
    export_version = last_version + 1 

然后,我们将实现微调循环。在以下代码中,我们调用 download_user_data 来下载所有用户标注的图像,并将 user_dir 传递给 input_pipeline,使其能够加载新图像:

    # Download user-labels data 
    download_user_data(URL, user_dir) 

    images, labels = datasets.input_pipeline(dataset_dir,     
    batch_size, is_training=True, user_dir=user_dir) 
    test_images, test_labels =    
    datasets.input_pipeline(dataset_dir, batch_size,    
    is_training=False, user_dir=user_dir) 

     with tf.variable_scope("models") as scope: 
     logits = nets.inference(images, is_training=True) 
     scope.reuse_variables() 
     test_logits = nets.inference(test_images, is_training=False) 

    total_loss = models.compute_loss(logits, labels) 
    train_accuracy = models.compute_accuracy(logits, labels) 
    test_accuracy = models.compute_accuracy(test_logits,  
    test_labels) 

    global_step = tf.Variable(0, trainable=False) 
    learning_rate = models.get_learning_rate(global_step,      
    initial_learning_rate, decay_steps, decay_rate) 
    train_op = models.train(total_loss, learning_rate,  
    global_step, train_vars) 

    saver = tf.train.Saver(max_to_keep=max_checkpoints_to_keep) 
    checkpoint_name = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") 
    checkpoints_dir = os.path.join(save_dir, checkpoint_name) 
    if not os.path.exists(save_dir): 
      os.mkdir(save_dir) 
    if not os.path.exists(checkpoints_dir): 
      os.mkdir(checkpoints_dir) 

    with tf.Session() as sess: 
      sess.run(tf.global_variables_initializer()) 
      coords = tf.train.Coordinator() 
      threads = tf.train.start_queue_runners(sess=sess,   
      coord=coords) 

    saver.restore(sess,  
    models.get_model_path_from_ckpt(last_checkpoint_dir)) 
    sess.run(global_step.assign(0)) 

    last_saved_test_accuracy = 0 
    for i in range(num_tests): 
        last_saved_test_accuracy += sess.run(test_accuracy) 
    last_saved_test_accuracy /= num_tests 
    should_export = False 
    print("Last model test accuracy    
    {}".format(last_saved_test_accuracy)) 
    for i in tqdm(range(max_steps), desc="training"): 
        _, loss_value, lr_value = sess.run([train_op, total_loss,   
        learning_rate]) 

     if (i + 1) % output_steps == 0: 
       print("Steps {}: Loss = {:.5f} Learning Rate =   
       {}".format(i + 1, loss_value, lr_value)) 

        if (i + 1) % eval_steps == 0: 
          test_acc, train_acc, loss_value =  
          sess.run([test_accuracy, train_accuracy, total_loss]) 
            print("Test accuracy {} Train accuracy {} : Loss =  
            {:.5f}".format(test_acc, train_acc, loss_value)) 

        if (i + 1) % save_steps == 0 or i == max_steps - 1: 
          test_acc = 0 
          for i in range(num_tests): 
            test_acc += sess.run(test_accuracy) 
            test_acc /= num_tests 

        if test_acc > last_saved_test_accuracy: 
          print("Save steps: Test Accuracy {} is higher than  
          {}".format(test_acc, last_saved_test_accuracy)) 
          last_saved_test_accuracy = test_acc 
          saved_file = saver.save(sess, 

        os.path.join(checkpoints_dir, 'model.ckpt'), 
                                        global_step=global_step) 
                should_export = True 
                print("Save steps: Save to file %s " % saved_file) 
            else: 
                print("Save steps: Test Accuracy {} is not higher  
       than {}".format(test_acc, last_saved_test_accuracy)) 

    if should_export: 
        print("Export model with accuracy ",  
        last_saved_test_accuracy) 
        models.export_model(checkpoints_dir, export_dir,   
        export_name, export_version) 
        archive_and_send_file(source_api, dest_api,  
        checkpoint_name, export_dir, export_name, export_version) 
      coords.request_stop() 
      coords.join(threads)

其他部分与训练循环非常相似。但是,我们不是从caffe模型中加载权重,而是使用最新模型的检查点,并运行测试多次以获取其测试准确性。

在微调循环的末尾,我们需要一个名为archive_and_send_file的新方法来从exported模型创建归档,并将链接发送到生产服务器:

    def make_archive(dir_path): 
    return shutil.make_archive(dir_path, 'zip', dir_path) 

    def archive_and_send_file(source_api, dest_api, ckpt_name,    
    export_dir, export_name, export_version): 
    model_dir = os.path.join(export_dir, export_name,    
    str(export_version)) 
    file_path = make_archive(model_dir) 
    print("Zip model: ", file_path) 

    data = { 
        "link": "{}/{}/{}".format(source_api, export_name,  
     str(export_version) + ".zip"), 
        "ckpt_name": ckpt_name, 
        "version": export_version, 
        "name": export_name, 
    } 
     r = requests.post(dest_api, data=data) 
    print("send_file", r.text) 

您应该注意,我们创建了一个带有source_api参数的链接,这是指向训练服务器的链接,http://1.53.110.161:8181。我们将设置一个简单的 Apache 服务器来支持此功能。但是,在现实中,我们建议您将归档模型上传到云存储,如 Amazon S3。现在,我们将展示使用 Apache 的最简单方法。

我们需要使用以下命令安装 Apache:

sudo apt-get install apache2

现在,在/etc/apache2/ports.conf中,第 6 行,我们需要添加此代码以使apache2监听端口8181

    Listen 8181 

然后,在/etc/apache2/sites-available/000-default.conf的开头添加以下代码以支持从/home/ubuntu/models目录下载:

    <VirtualHost *:8181> 
      DocumentRoot "/home/ubuntu/models" 
      <Directory /> 
        Require all granted 
      </Directory> 
    </VirtualHost> 

最后,我们需要重新启动apache2服务器:

sudo service apache2 restart

到目前为止,我们已经设置了所有执行微调所需的代码。在第一次运行微调之前,我们需要向/model端点发送POST请求,以获取关于我们第一个模型的信息,因为我们已经将模型复制到生产服务器。

project代码库中,让我们运行finetune脚本:

python scripts/finetune.py

控制台中的最后几行将如下所示:

    Save steps: Test Accuracy 0.84 is higher than 0.916875
    Save steps: Save to file data/checkpoints/2017-05-29_18-46-43/model.ckpt-2000
    ('Export model with accuracy ', 0.916875000000004)
    2017-05-29 18:47:31.642729: I tensorflow/core/common_runtime/gpu/gpu_device.cc:977] Creating TensorFlow device (/gpu:0) -> (device: 0, name: GeForce GTX TITAN X, pci bus id: 0000:01:00.0)
    ('Exported model at', '/home/ubuntu/models/pet-model/2')
    ('Zip model: ', '/home/ubuntu/models/pet-model/2.zip')
    ('send_file', u'{\n  "ckpt_name": "2017-05-29_18-46-43", \n  "id": 2, \n  "link": "http://1.53.110.161:8181/pet-model/2.zip", \n  "name": "pet-model", \n  "version": 2\n}\n')

正如您所见,新模型的测试准确率为 91%。该模型也被导出并存档到/home/ubuntu/models/pet-model/2.zip。代码还在调用/model端点将链接发布到生产服务器。在生产服务器的 Flask 应用日志中,我们将得到以下结果:

('Start downloading', u'http://1.53.110.161:8181/pet-model/2.zip')
('Downloaded file at', u'/tmp/2.zip')
('Extracted at', u'/home/ubuntu/productions/2')
127.0.0.1 - - [29/May/2017 18:49:05] "POST /model HTTP/1.1" 200 -

这意味着我们的 Flask 应用程序已从训练服务器下载了2.zip文件,并将其内容提取到/home/ubuntu/productions/2。在 TensorFlow Serving 的 tmux 会话中,您还将获得以下结果:

    2017-05-29 18:49:06.234808: I tensorflow_serving/core/loader_harness.cc:86] Successfully loaded servable version {name: pet-model version: 2}
    2017-05-29 18:49:06.234840: I tensorflow_serving/core/loader_harness.cc:137] Quiescing servable version {name: pet-model version: 1}
    2017-05-29 18:49:06.234848: I tensorflow_serving/core/loader_harness.cc:144] Done quiescing servable version {name: pet-model version: 1}
    2017-05-29 18:49:06.234853: I tensorflow_serving/core/loader_harness.cc:119] Unloading servable version {name: pet-model version: 1}
    2017-05-29 18:49:06.240118: I ./tensorflow_serving/core/simple_loader.h:226] Calling MallocExtension_ReleaseToSystem() with 645327546
    2017-05-29 18:49:06.240155: I tensorflow_serving/core/loader_harness.cc:127] Done unloading servable version {name: pet-model version: 1}

此输出表明 TensorFlow 模型服务器已成功加载pet-modelversion 2并卸载了version 1。这也意味着我们已经为在训练服务器上训练并通过/model端点发送到生产服务器的新模型提供了服务。

设置每天运行的 cron 任务

最后,我们需要设置每天运行的微调,并自动将新模型上传到服务器。我们可以通过在训练服务器上创建crontab轻松实现这一点。

首先,我们需要运行crontab命令:

crontab -e

然后,我们只需添加以下行来定义我们希望finetune.py运行的时间:

0 3 * * * python /home/ubuntu/project/scripts/finetune.py

正如我们所定义的,Python 命令将每天凌晨 3 点运行。

总结

在本章中,我们实现了一个完整的真实生产环境,从训练到服务深度学习模型。我们还在 Flask 应用中创建了一个 web 界面,用户可以上传他们的图像并获得结果。我们的模型可以每天自动进行微调,以提高系统的质量。以下是一些你可以考虑的改进方案:

  • 模型和检查点应保存在云存储中。

  • Flask 应用和 TensorFlow Serving 应由另一个更好的进程管理系统来管理,例如 Supervisor。

  • 应该有一个 web 界面,供团队审批用户选择的标签。我们不应完全依赖用户来决定训练集。

  • TensorFlow Serving 应该构建为支持 GPU 以实现最佳性能。