代码走读 - 百度开源智能问答框架 AnyQ

面向对象:想搭建智能问答系统、深度语义匹配的nlp选手。在自己亲手搭建一个之前,学习和走读优秀的框架代码是个不会错的选择。

AnyQ(ANswer Your Questions) :百度QA开源项目,主要包含面向FAQ集合的问答系统框架、文本语义匹配工具SimNet。

框架目录

本文重点走读SimNet框架的代码。开源代码地址,点这里。TensorFlow版SimNet的结构如下:(自动屏蔽名字带 ‘pairwise’ 的文件,稍后解释)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
simnet
|-tf
|- date //示例数据,tsv格式,没有表头
|- train_pointwise_data //训练集数据
|- test_pointwise_data //测试集数据

|- examples //示例配置文件,以模型种类命名,里面的参数需熟知
|- bow-pointwise.json
|- cnn-pointwise.json
|- knrm-pointwise.json
|- lstm-pointwise.json
|- mmdnn-pointwise.json
|- mvlstm-pointwise.json
|- pyramid-pointwise.json

|- layers //网络中使用操作层的实现
|- tf_layers.py

|- losses //损失函数实现,可放置各种损失函数class
|- simnet_loss.py

|- nets //网络结构实现,由tf_layers.py不同组合实现
|- bow.py
|- knrm.py
|- lstm.py
|- matchpyramid.py
|- mlpcnn.py
|- mm_dnn.py
|- mvlstm.py

|- tools //数据转化及评价工具
|- evaluate.py
|- tf_record_reader.py
|- tf_record_writer.py

|- util //工具类
|- controler.py
|- converter.py # 数据转换
|- datafeeds.py # 读取数据
|- utility.py

|- README.md //请仔细反复读,

|- run_infer.sh //运行predict任务
|- run_train.sh //运行train任务

|- tf_simnet.py //主运行文件

读框架代码的工具,相较于jupyter,spyder,推荐Pycharm

运行环境

  • linux,其它系统推荐docker
  • python 2.7
  • tensorflow 1.7.0

数据类型

解释为何屏蔽名字带 ‘pairwise’ 的文件:

语义匹配网络SimNet可以使用Pointwise与Pairwise两种类型的数据进行训练。

Pointwise训练及测试数据格式

  • 训练数据格式:训练数据包含三列,依次为Query1的ID序列(ID间使用空格分割),Query2的ID序列(ID间使用空格分割),Label,每列间使用TAB分割,例如;
1
2
3
1 1 1 1 1   2 2 2 2 2   0
1 1 1 1 1 1 1 1 1 1 1
...
  • 测试数据格式:Pointwise测试数据格式与训练数据格式相同。

Pairwise训练及测试数据格式

  • 训练数据格式:训练数据包含三列,依次为Query1的ID序列(ID间使用空格分割),Positive Query2的ID序列(ID间使用空格分割),Negative Query3的ID序列(ID间使用空格分割),每列间使用TAB分割,例如;
1
2
3
1 1 1 1 1   1 1 1 1 1   2 2 2 2 2   
1 1 1 1 1 1 1 1 1 1 3 3 3 3 3
...
  • 测试数据格式:测试数据格式包含三列,依次为Query1的ID序列(ID间使用空格分割),Query2的ID序列(ID间使用空格分割),Label,每列间使用TAB分割,例如;
1
2
3
4
1 1 1 1 1   1 1 1 1 1   1
1 1 1 1 1 2 2 2 2 2 0
3 3 3 3 3 3 3 3 3 3 1
...

由于使用的数据集是Quora数据集,为 [问句1,问句2,label] 的Pointwise格式数据集,因此名字带 ‘pairwise’ 的文件暂时都用不上。若是数据集为pairwise,就能用的上了。

.json 配置文件走读

准备完数据文件之后,观察配置文件,以cnn-pointwise.json为例。 通过配置文件可以灵活的选择网络类型,数据类型,损失函数以及其他超参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
{
"train_data":{
"train_file": "data/convert_train_pointwise_data", //训练文件路径
"data_size": 400, //训练集大小,根据不同的训练数据文件需做改动
"left_slots" : [["left",32]], //left slot的名字及最大长度
"right_slots" : [["right",32]] //right slot的名字及最大长度
},

"model":{
"net_py": "./nets/mlpcnn", //网络对应模块路径
"net_class": "MLPCnn", //网络对应类名
"vocabulary_size": 3, //词典大小,根据词嵌入得出的字典大小需做改动

# 不同的网络net有不同的参数,这些是cnn的参数
"embedding_dim": 128, // Embedding嵌入层维度
"num_filters": 256, //卷机核数量
"hidden_size": 128, //隐藏层大小
"window_size": 3, //卷机核大小

# 损失函数参数
"loss_py": "./losses/simnet_loss", //损失对应模块路径,内有各种损失函数
"loss_class": "SoftmaxWithLoss" //损失对应类名,可选择其它损失函数
},

"global":{
"training_mode": "pointwise", //训练模式,也是数据格式
"n_class": 2, //类别数目,2为二分类
"max_len_left": 32, //Left slot的最大长度
"max_len_right": 32 //Right slot的最大长度
},

"setting":{
"batch_size": 64, // Batch Size,每一步跑的样本数
"num_epochs": 1, // Number of Epochs,重复全体样本的倍数
"thread_num": 6, //线程数
"print_iter": 100, //显示间隔,每100步显示一个loss
"model_path": "model/pointwise", //模型保存路径,在框架目录里没有,需要自己新建
"model_prefix": "cnn", //模型保存名前缀
"learning_rate": 0.001, //学习率
"shuffle": 1 //是否打乱数据
},

"test_data":{
"test_file": "data/convert_test_pointwise_data", //测试数据路径
"test_model_file": "model/pointwise/cnn.epoch1", //测试使用模型,要先跑train任务后才有模型文件保存下来,才能做预测
"test_result": "result_cnn_pointwise" //测试结果文件
},

"graph":{
"graph_path": "graph", //freeze任务文件保存路径
"graph_name": "model_cnn_pairwise.protxt" //freeze任务结果文件
}
}

参数说明:

  • 假设训练集的 data_size = 1000时,运行模型训练train任务时,设置了num_epochs = 5,batch_size = 50,shuffle = 1,print_iter = 10,那么训练集最终的样本量 = data_size num_epochs =5000,重复了5倍原有样本量,shuffle = 1表示打乱样本数据,而每步step跑 batch_size 条样本,一共能跑 data_size num_epochs / batch_size = 5000/50 = 100 步,而每print_iter步报一次loss,那么一共报 100/print_iter = 10次loss。

    在代码中还有个参数为 epoch_iter,为保存模型而设置,意为每epoch_iter步时,保存一次模型文件。epoch_iter = data_size / batch_size = 1000/50 = 20,共100步,即跑完train后共保存 100/epoch_iter =100/20 =5个模型文件。若shuffle = 0,epoch_iter 意为跑完一次原data_size 需要的步数,若shuffle = 1,epoch_iter 意为跑完一次与原data_size一样大的数据集需要的步数,这样的话,最终保存的模型数量 = num_epochs 。

  • 假设测试集的 data_size = 200时,运行模型预测predict任务时,经常设置num_epochs = 1,因为在测试时没必要重复测试样本,是否打乱数据影响也不大,当设置batch_size = 50,测试数据总样本量 = data_size *num_epochs =200仍然是原测试数据集,每步跑batch_size = 50条数据,共200/50=4steps跑完,最终会打印一个accuracy数值。

  • 这些参数只对模型训练有效,模型预测的predict任务里的这些参数需要在tf_simnet.py内的predict函数内修改!!!(为自己修改代码把配置参数提到配置文件里埋下伏笔😂)

其它说明:

  • 保存模型文件的路径需要自己手动添加,在目录上新建model和pointwise文件夹:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    simnet
    |-tf
    |- date
    |- examples
    |- layers
    |- losses
    |- nets
    |- tools
    |- util

    # 新建下面的文件夹
    |- model
    |- pointwise

.sh 任务文件走读

数据准备完毕,配置文件修改完成后,可在Linux执行.sh脚本文件来实现 train / predict / freeze / …等任务。

run_train.sh

通过执行脚本run_train.sh可以启动训练任务,打开run_train.sh:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
set -e # set -o errexit
set -u # set -o nounset
set -o pipefail

# 以下命令用来做训练集和测试集数据转换,转换一次形成convert文件便可以,不需要重复转换
#-----------------------------------------------------------------------
# 将train_pointwise_data转成convert_train_pointwise_data
echo "convert train data"
python ./tools/tf_record_writer.py pointwise ./data/train_pointwise_data ./data/convert_train_pointwise_data 0 32
#-----------------------------------------------------------------------
# 将test_pointwise_data转成convert_test_pointwise_data
echo "convert test data"
python ./tools/tf_record_writer.py pointwise ./data/test_pointwise_data ./data/convert_test_pointwise_data 0 32
echo "convert data finish"
#-----------------------------------------------------------------------

in_task_type='train' # 输入任务类型,可选择 train/predict/freeze/convert
in_task_conf='./examples/cnn-pointwise.json' # 输入配置文件地址

#-----------------------------------------------------------------------
# 以下命令运行tf_simnet.py文件,执行train任务,深度语义匹配使用为cnn网络
python tf_simnet.py \
--task $in_task_type \
--task_conf $in_task_conf

也可以通过如下方式启动自定义训练,效果与上面的.sh文件是一样的:

1
2
3
python tf_simnet.py
--task train
--task_conf examples/cnn_pointwise.json

执行完run_train.sh后,在model文件夹内会自动保存各个epoch_iter和final的模型文件。

run_infer.sh

通过执行脚本run_infer.sh可以启动预测任务,可以得到模型预测结果或得分,打开.sh文件:

1
2
3
4
5
6
7
8
9
set -e # set -o errexit
set -u # set -o nounset
set -o pipefail

in_task_type='predict' # 选择了predict任务
in_task_conf='./examples/cnn-pointwise.json' # 仍然是cnn配置文件路径
python tf_simnet.py \
--task $in_task_type \
--task_conf $in_task_conf

也可以通过如下方式启动自定义训练:

1
2
3
python tf_simnet.py
--task predict
--task_conf examples/cnn_pointwise.json

执行完run_infer.sh之后,会自动在路径上生成result文件,可打开查看。

自定义.sh任务文件

据观察,.sh文件主要运行tf_simnet.py文件,有两个参数可以自定义。

参数说明:

  • task: 任务类型 ,可选择 train/predict/freeze/convert 。
  • task_conf: 使用配置文件地址

接下来尝试生成自定义的.sh任务文件:

  • 可以把转换数据的命令抽取出来,形成 run_convert_data.sh文件:

    执行完run_convert_data.sh后,在data文件夹里会自动生成convert前缀的数据文件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    set -e # set -o errexit
    set -u # set -o nounset
    set -o pipefail

    # 将具体的文件名把下面命令里的中文字替换掉即可
    echo "convert train data"
    python ./tools/tf_record_writer.py pointwise ./data/待转化的训练数据文件名 ./data/已转化的训练数据文件名 0 32
    echo "convert test data"
    python ./tools/tf_record_writer.py pointwise ./data/待转化的测试数据文件名 ./data/已转化的测试数据文件名 0 32
    echo "convert data finish"
  • 定义一个cnn配置文件的执行freeze任务的run_freeze_cnn.sh

    执行完run_freeze_cnn.sh后,根据配置文件里“graph”的参数设置,在路径上会自动生成graph文件夹,里面有model_cnn_pairwise.protxt文件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    set -e # set -o errexit
    set -u # set -o nounset
    set -o pipefail

    in_task_type='freeze' # 输入任务类型,可选择 train/predict/freeze/convert
    in_task_conf='./examples/cnn-pointwise.json'
    python tf_simnet.py \
    --task $in_task_type \
    --task_conf $in_task_conf
  • 定义一个使用lstm网络的训练任务 run_train_lstm.sh,同时一定要记得修改配置文件!!:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    set -e # set -o errexit
    set -u # set -o nounset
    set -o pipefail

    in_task_type='train'
    in_task_conf='./examples/lstm-pointwise.json' # 修改了配置文件路径,配置文件内参数也得修改好
    python tf_simnet.py \
    --task $in_task_type \
    --task_conf $in_task_conf
  • 当lstm的train任务跑完后,利用其保存的模型文件进行predict任务,新建run_predict_lstm.sh

    1
    2
    3
    4
    5
    6
    7
    8
    9
    set -e # set -o errexit
    set -u # set -o nounset
    set -o pipefail

    in_task_type='predict' # 修改了任务类型
    in_task_conf='./examples/lstm-pointwise.json' # 确认lstm配置文件路径
    python tf_simnet.py \
    --task $in_task_type \
    --task_conf $in_task_conf

.py 文件走读

tf_simnet.py

tf_simnet.py是整个深度语义匹配框架的主运行py文件,打开如下,已对主要代码进行一行行的注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
#coding=utf-8

# Copyright (c) 2018 Baidu, Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import argparse
import logging
import json
import sys
import os

import tensorflow as tf

from utils import datafeeds
from utils import controler
from utils import utility
from utils import converter

_WORK_DIR = os.path.split(os.path.realpath(__file__))[0]
sys.path.append(os.path.join(_WORK_DIR, '../../../common'))
import log

# 加载配置文件信息,形成conf配置字典
def load_config(config_file):
"""
load config
"""
with open(config_file, "r") as f:
try:
conf = json.load(f)
except Exception:
logging.error("load json file %s error" % config_file)
conf_dict = {}
unused = [conf_dict.update(conf[k]) for k in conf]
logging.debug("\n".join(
["%s=%s" % (u, conf_dict[u]) for u in conf_dict]))
return conf_dict


def train(conf_dict):
"""
train
"""
# 根据配置文件先判断data文件是 pairwise 还是 pointwise
training_mode = conf_dict["training_mode"]

# 根据配置文件输入net网络文件路径 + 网络类型class
net = utility.import_object(
conf_dict["net_py"], conf_dict["net_class"])(conf_dict)

if training_mode == "pointwise":

# 喂数据,从train_file, batch_size, num_epochs, shuffle等配置信息确定数据队列长度和秩序
datafeed = datafeeds.TFPointwisePaddingData(conf_dict)

# 从转换后的数据中拿出一组组 input_l, input_r, label_y, 一步步的拿进来训练
input_l, input_r, label_y = datafeed.ops()

# 做语义匹配预测
pred = net.predict(input_l, input_r)

# 根据配置文件设置loss函数路径 + loss种类
loss_layer = utility.import_object(
conf_dict["loss_py"], conf_dict["loss_class"])()
loss = loss_layer.ops(pred, label_y)

# pairwise 先忽略
elif training_mode == "pairwise":
datafeed = datafeeds.TFPairwisePaddingData(conf_dict)
input_l, input_r, neg_input = datafeed.ops()
pos_score = net.predict(input_l, input_r)
neg_score = net.predict(input_l, neg_input)
loss_layer = utility.import_object(
conf_dict["loss_py"], conf_dict["loss_class"])(conf_dict)
loss = loss_layer.ops(pos_score, neg_score)
else:
print >> sys.stderr, "training mode not supported"
sys.exit(1)


# --------------------
# define optimizer
# --------------------
# 超参数 学习速率的设置
lr = float(conf_dict["learning_rate"])
optimizer = tf.train.AdamOptimizer(learning_rate=lr).minimize(loss)

# 运行 controler 的 run_trainer 函数
controler.run_trainer(loss, optimizer, conf_dict)


def predict(conf_dict):
"""
predict
"""

# 根据配置文件输入net网络文件路径 + 网络类型class
net = utility.import_object(
conf_dict["net_py"], conf_dict["net_class"])(conf_dict)

# 更新/覆盖 conf_dict配置文件 的配置参数 (这里需要手动调整)
conf_dict.update({"num_epochs": "1", "batch_size": "1",
"shuffle": "0", "train_file": conf_dict["test_file"]})
# num_epochs = 1,数据集为样本全量的1倍,仍为原测试样本
# batch_size = 1,一次只读一条样本,覆盖掉配置文件batch_size的值
# shuffle = 0 /1,样本数据是否随机读取,覆盖掉配置文件shuffle的值
# train_file 为 测试集样本路径,覆盖掉配置文件的训练集data路径

# 喂数据,从train_file, batch_size, num_epochs, shuffle等配置信息确定数据队列长度和秩序
test_datafeed = datafeeds.TFPointwisePaddingData(conf_dict)

# 从转换后的数据中拿出 test_l, test_r, test_y
test_l, test_r, test_y = test_datafeed.ops()

# test network

# 做语义匹配预测
pred = net.predict(test_l, test_r)
# 运行 controler 的 run_predict 函数
controler.run_predict(pred, test_y, conf_dict) # run_predict(pred, label, config)


def freeze(conf_dict):
"""
freeze net for c api predict
"""

# 网络文件路径 + 网络类型class
net = utility.import_object(
conf_dict["net_py"], conf_dict["net_class"])(conf_dict)

test_l = dict([(u, tf.placeholder(tf.int32, [None, v], name=u))
for (u, v) in dict(conf_dict["left_slots"]).iteritems()])
test_r = dict([(u, tf.placeholder(tf.int32, [None, v], name=u))
for (u, v) in dict(conf_dict["right_slots"]).iteritems()])
pred = net.predict(test_l, test_r)
controler.graph_save(pred, conf_dict)


def convert(conf_dict):
"""
convert
"""
converter.run_convert(conf_dict)


if __name__ == "__main__":
# 在tf目录下自动新增log文件夹
log.init_log("./log/tensorflow")

# 命令解析
parser = argparse.ArgumentParser()
# 增加task命令: 命令选项为 train // predict // freeze // convert
parser.add_argument('--task', default='train',
help='task: train/predict/freeze/convert, the default value is train.')
# 增加task_conf命令:命令选项为examples目录下的json文件
parser.add_argument('--task_conf', default='./examples/cnn-pointwise.json',
help='task_conf: config file for this task')
args = parser.parse_args()
task_conf = args.task_conf

# 加载配置文件,config
config = load_config(task_conf)
task = args.task

# 判断任务类型
if args.task == 'train':
train(config)
elif args.task == 'predict':
predict(config)
elif args.task == 'freeze':
freeze(config)
elif args.task == 'convert':
convert(config)
else:
print >> sys.stderr, 'task type error.'
  • 可直接先看最下面的if __name__ == "__main__":,两个parser.add_argument分别对应.sh文件的两个task和task_conf参数。选择好任务和配置文件后就运行任务,假如 args.task == ‘train’,那么运行train函数。

  • 再跳到def train(conf_dict):这行,看train函数如何运行。前面几行还是比较好理解,如有疑问可发私信问我,得到loss,optimizer和配置conf_dict之后,最后一行又运行了一个大函数:

    1
    2
    # 运行 controler 的 run_trainer 函数
    controler.run_trainer(loss, optimizer, conf_dict)

    那么就再跳到controler这个文件,继续往下看。

需要特殊说明的是,当模型预测执行predict任务运行predict函数时,num_epochs/batch_size/shuffle/…等参数需要在代码里面调整,而配置文件里的参数设置对模型预测不起作用,在tf_simnet.py的predict函数找到如下代码进行设置:

1
2
3
4
5
6
7
# 更新测试集的 conf_dict配置文件 的配置参数 (这里需要手动调整)
conf_dict.update({"num_epochs": "1", "batch_size": "1",
"shuffle": "0", "train_file": conf_dict["test_file"]})
# num_epochs = 1,数据集为样本全量的1倍,仍为原测试样本
# batch_size = 1,一次只读一条样本,覆盖掉原有batch_size的值
# shuffle = 0 /1,样本数据是否随机读取,覆盖掉原有shuffle的值
# train_file 为 测试集样本路径,覆盖掉原先的训练集data路径

controler.py

打开controler.py可以看见,里面有与tf_simnet.py里的train/predict/freeze函数一一对应的run_train/run_predict/graph_save函数,对重要代码已做注释:

继续上面的思路,直接看run_trainer(loss, optimizer, conf_dict)函数。同样,如有疑问可留言或私信我

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
#coding=utf-8

# Copyright (c) 2018 Baidu, Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import sys

import tensorflow as tf


def run_predict(pred, label, config):
"""
run classification predict function handle
"""
mean_acc = 0.0

# 执行模型保存任务,需要创建一个Saver对象
saver = tf.train.Saver()

mode = config["training_mode"]

# label输入值为二维的0/1向量,类似将原先的label值onehot化,pred同理为二维向量
# label = pred = 形如([0,1],[1,0],[1,0],[0,1],...)

# 找出数值为1/或者说数值最大的index值,因为是2维,index取值在[0,1]之间,(,1)代表行(,0)代表列
label_index = tf.argmax(label, 1) # label_index为向量,形如([1],[0],[0],[1],...)
if mode == "pointwise":
pred_prob = tf.nn.softmax(pred, -1) # 向量, 将pred变成二分类的概率小数向量,形如([0.73,0.27],[0.1,0.9],[1.0,0],[0.87,0.13],...)
score = tf.reduce_max(pred_prob, -1) # 向量, 取向量里的最大概率值,而pred结果为大概率值的输出,(,-1)为去掉最后一个向量,形如([0.73],[0.9],[1.0],[0.87],...)
pred_index = tf.argmax(pred_prob, 1) # 向量, 同理label_index,形如([0],[1],[0],[0],...)
correct_pred = tf.equal(pred_index, label_index) # 向量, 判断label_index与pred_index是否一致,一致为1,不一致为0
acc = tf.reduce_mean(tf.cast(correct_pred, "float")) # 数字,array([0,0,0,1,1])与array([1,0,1,0,1]), 求得correct_pred = 2,acc=2/5= 0.4=求猜对的期望值

# 'pairwise'先忽略
elif mode == "pairwise":
score = pred
pred_index = tf.argmax(pred, 1)
acc = tf.constant([0.0])

# 找到模型文件路径
modelfile = config["test_model_file"]
#新建result文件,保存预测结果
result_file = file(config["test_result"], "w")
step = 0
init = tf.group(tf.global_variables_initializer(),
tf.local_variables_initializer())
with tf.Session(config=tf.ConfigProto(intra_op_parallelism_threads=1)) \
as sess:
sess.run(init) #初始化设置
saver.restore(sess, modelfile) # 加载模型文件
coord = tf.train.Coordinator()
read_thread = tf.train.start_queue_runners(sess=sess, coord=coord)
while not coord.should_stop():
step += 1
try:
ground, pi, a, prob = sess.run([label_index, pred_index, acc, score]) # 用二维向量来表示label和pred,用数值1在二维向量中的位置是否一致判断label与pred结果是否一致
mean_acc += a
for i in range(len(prob)):

result_file.write("%d\t%d\t%f\n" % (ground[i], pi[i], prob[i]))
# 一步步写result文件,共3列,分别是ground[i], pi[i], prob[i]
# ground[i]是测试集label的值,0或1
# pi[i]是预测的值,0或1
# prob[i]是预测结果的概率

except tf.errors.OutOfRangeError:
coord.request_stop()
coord.join(read_thread)
sess.close()
result_file.close()
if mode == "pointwise":
mean_acc = mean_acc / step
print >> sys.stderr, "accuracy: %4.2f" % (mean_acc * 100) # 输出accuracy


def run_trainer(loss, optimizer, config):
"""
run classification training function handle
"""
thread_num = int(config["thread_num"])
model_path = config["model_path"] # 模型文件的存储路径
model_file = config["model_prefix"] # 模型文件名的前缀
print_iter = int(config["print_iter"])
data_size = int(config["data_size"])
batch_size = int(config["batch_size"])
epoch_iter = int(data_size / batch_size)
avg_cost = 0.0

# 执行模型保存任务,需要创建一个Saver对象
saver = tf.train.Saver(max_to_keep=None)
# 初始化 global variables
init = tf.group(tf.global_variables_initializer(),
tf.local_variables_initializer())
with tf.Session(config=tf.ConfigProto(intra_op_parallelism_threads=thread_num,
inter_op_parallelism_threads=thread_num)) \
as sess:
sess.run(init)
# 创建一个线程管理器(协调器)对象
coord = tf.train.Coordinator()
# 把tensor推入内存序列中,供计算单元调用
read_thread = tf.train.start_queue_runners(sess=sess, coord=coord) # 启动入队线程,由多个或单个线程
step = 0
epoch_num = 1
while not coord.should_stop(): # coord.should_stop() 查询是否应该终止所有线程
try:
step += 1
c, _= sess.run([loss, optimizer])
avg_cost += c

#-----------------------------------------------------------
# 当step步数是print_iter的倍数时,即每print_iter步时,打印一个loss
if step % print_iter == 0:
print("loss: %f" % ((avg_cost / print_iter)))
avg_cost = 0.0

#-----------------------------------------------------------
# 当step步数是epoch_iter的倍数时,即每epoch_iter步时,保存一个model文件
if step % epoch_iter == 0:
print("save model epoch%d" % (epoch_num))
save_path = saver.save(sess,
"%s/%s.epoch%d" % (model_path, model_file, epoch_num))
epoch_num += 1

except tf.errors.OutOfRangeError:
# 全部步数走完后,保存一个final的模型文件
save_path = saver.save(sess, "%s/%s.final" % (model_path, model_file))
coord.request_stop() # 发出终止所有线程的命令
coord.join(read_thread) # 把线程加入主线程,等待threads结束
sess.close()


def graph_save(pred, config):
"""
run classify predict
"""
graph_path=config["graph_path"]
graph_name=config["graph_name"]
mode = config["training_mode"]
if mode == "pointwise":
pred_prob = tf.nn.softmax(pred, -1, name="output_prob")
elif mode == "pairwise":
pred_prob = tf.identity(pred, name="output_prob")
saver = tf.train.Saver()
step = 0
init = tf.group(tf.global_variables_initializer(),
tf.local_variables_initializer())
with tf.Session(config=tf.ConfigProto(intra_op_parallelism_threads=1)) \
as sess:
sess.run(init)
tf.train.write_graph(sess.graph_def, graph_path, graph_name, as_text=True)
sess.close()

跑完run_trainer(loss, optimizer, config),就自动保存了模型文件,供模型预测使用。

同理按照这个思路,回到tf_simnet.py可以看predict任务或者freeze(graph)任务的代码流程。同样也会再看到controler.py里面的函数是如何执行任务的。

写在最后

  • 不同的.sh文件有不同的task和task_conf参数值,对应的是运行tf_simnet.py和controler.py里不同的函数。
  • 其它的.py文件点开来应该不太难理解,如有疑问可留言或私信我
  • 每个模型文件比较大,我跑出来的每个有1.5G左右大小。
  • 若想观察每一步的数据形状或者变换情况,可自行加入print命令打印出来看看。
  • 每个任务完成后有自动生成的文件,可以研究看看,比如model文件,result文件,graph文件,log文件。
  • 代码走读只是理解的第一步,若要扎实掌握还需自己改动代码,再调试调试。
  • 欢迎打赏😘
would you buy me a coffee☕~
0%