知识如何迁移?从用 Pytorch 变成用 Tensorflow。

本文是一个记录,用于记录自己如何快速上手 TF 的经历。

PS: 本文中的大部分概念基于莫烦的 MorvanZhou/Tensorflow-Tutorial

一、什么是 session

seesion 就是 TF 中的启动器,我们需要通过调用 session 来将数据传入整个计算图中,得到想要的结果,没有 session 就没有结果。

比如我们要计算:

$\begin{bmatrix}

  1. & 1. & 1. \
  2. & 1. & 1. \
    \end{bmatrix} +
    \begin{bmatrix}
  3. & 2. & 3. \
  4. & 5. & 6. \
    \end{bmatrix} =
    \begin{bmatrix}
  5. & 3. & 4. \
  6. & 6. & 7. \
    \end{bmatrix}$
1
2
3
4
5
6
7
import tensorflow as tf
import numpy as np

input1 = tf.constant(1.0, shape=[2, 3])
input2 = tf.constant(np.reshape(np.arange(1.0, 7.0, dtype=np.float32), (2, 3)))
add_output = tf.add(input1, input2)
print(add_output)

output:

Tensor(“Add_2:0”, shape=(2, 3), dtype=float32)

为什么没有得到我们想要的结果?

因为在我们写完上述代码之后,我们完成的是 “定义了一个可以运行的计算图”。如果我们需要启动这个计算图,我们还需要使用 tf.Session()。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import tensorflow as tf
import numpy as np

input1 = tf.constant(1.0, shape=[2, 3])
input2 = tf.constant(np.reshape(np.arange(1.0, 7.0, dtype=np.float32), (2, 3)))
add_output = tf.add(input1, input2)

# 使用 session() 有两种方式 (就像使用 open 打开文件一样)

# 第一种:
sess = tf.Session()
result1 = sess.run(add_output)
print(result1)
sess.close()

# 第二种:
# 推荐使用这种,不会漏泄 sess.close()
with tf.Session() as sess:
result2 = sess.run(dot_operation)
print(result2)

二、什么是 variable

variable 就是变量的含义,通俗一点说就是“模型中的参数”,比如我们要训练一个线性模型 $y = k x + b$,那么 $x$ 和 $b$ 就是我们的 variable,值得注意的是,一旦定义了变量,在运行之前,要对变量进行初始化

接下来,我们会定义一个变量 $x$,初始时候 $x=0$,之后每次运行都让 $x = x + 1$

1
2
3
4
5
6
7
8
9
10
11
12
13
x = tf.Variable(0, name="x") 
add_one = tf.add(x, 1)
update_x = tf.assign(x, add_one) # 将 add_one 的结果赋值给 x

with tf.Session() as sess:
# 请注意: 一旦我们定义了变量,我们需要将他们初始化,可以使用如下语句:
sess.run(tf.global_variables_initializer())
# 开始时候的x
print(sess.run(x))
# 我们运行三次
for _ in range(3):
sess.run(update_x)
print(sess.run(x))

三、什么是 placeholder

placeholder 翻译过来是占位符,通俗一点说就是“每次训练的过程中会输入到模型的数据”。我们在定义 placeholder 的时候需要将它的数据类型和数据格式一起传入。

比如下面会看到的 tf.float32 表示的就是每次训练扔进模型中的数据类型是 tf.float32。 而数据格式默认是 None 就是一维值也可以是多维,比如 [2,3] 表示2行、3列的数据格式。而 [None, 3] 表示列为3,行不定。

使用占位符的关键就是在 sess.run() 的时候使用 feed_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
import tensorflow as tf

# 第一个操作
x1 = tf.placeholder(dtype=tf.float32, shape=None)
y1 = tf.placeholder(dtype=tf.float32, shape=None)
z1 = x1 + y1

# 第二个操作
x2 = tf.placeholder(dtype=tf.float32, shape=[2, 1])
y2 = tf.placeholder(dtype=tf.float32, shape=[1, 2])
z2 = tf.matmul(x2, y2)

# 开始运行
with tf.Session() as sess:
# 当我们只运行一个操作的时候
z1_value = sess.run(z1, feed_dict={x1: 1, y1: 2})

# 当我们运行多个操作的时候
z1_value, z2_value = sess.run(
[z1, z2], # 同时运行 z1 和 z2
feed_dict={
x1: 1, y1: 2,
x2: [[2], [2]], y2: [[3, 3]]
})
print(z1_value)
print(z2_value)

四、用 Neural Network 来拟合一个曲线(回归模型)

这里,我们想要做的是用 TF 来拟合一个非常简单的类似于 $y=x^2$ 的曲线。

用实现这个拟合的目标,我们会使用一个简单的两层神经网络来完成。这时候,我们会用到 tf.layers 这个模块。如果用 Pytorch 来类比,这就是基础版的 torch.nn,之所叫基础版本,是因为里面只给你简单的封装了一些常用的 layers, 不过也勉强够用了。

再介绍一下求梯度的过程, pytorch 中求梯度有两种方式 1: 用 backward() 求出梯度之后,手动对梯度进行处理;2:backward() 求出梯度之后,使用 step() 来自动为我们更新所有的梯度。

tf 中也是这样的,我们有两种方式: 1:optimizer.compute_gradients(loss) + optimizer.apply_gradients(); 2: optimizer.minimize(loss) 。

如果你不需要用 graident 做什么事,推荐使用第二种。

模拟数据如下图所示:

regression picture

导入包加使用模拟数据

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
import tensorflow as tf
import matplotlib.pyplot as plt
import numpy as np

tf.set_random_seed(1)
np.random.seed(1)

# 生成模拟数据
x = np.linspace(-1, 1, 100).reshape(-1, 1) # shape (100, 1)
noise = np.random.normal(0, 0.1, size=x.shape)
y = np.power(x, 2) + noise # shape (100, 1) + some noise

# 画出来的杂音图,如上图所示。
plt.scatter(x, y)
plt.show()

# 定义占位符,用来表示要扔进模型中的数据
input_data_x = tf.placeholder(tf.float32, x.shape) # input x
input_data_y = tf.placeholder(tf.float32, y.shape) # input y

# 定义模型: 我们用一个两层的神经网络来进行拟合
# 第一层:线性层, inputs 表示输入数据, units 表示隐含层大小, activation 表示激活函数
linear_layer = tf.layers.dense(inputs=input_data_x, units=10, activation=tf.nn.relu) # 隐含层
# 第二层:输出层
output_layer = tf.layers.dense(inputs=linear_layer, units=1) # output layer

# 定义损失函数、优化器
# 损失函数:因为是拟合,最好用的就是最小二乘法的。
loss = tf.losses.mean_squared_error(input_data_y, output_layer)
# 优化器:我们使用的是梯度下降
optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.5)
# 第一种更新梯度的方法:
train_op = optimizer.minimize(loss)

# 第二种更新梯度的办法:
# get_gradient = optimizer.compute_gradients(loss)
# update_parameters = optimizer.apply_gradients(get_gradient)


# 定义 Session()
sess = tf.Session()
# 初始化计算图中的变量
sess.run(tf.global_variables_initializer())

# 开始训练
for step in range(100):
# 运行流程一共有3个输出, train_op 会输出; loss 会输出; 拟合的结果也会输出。
_, l, pred = sess.run([train_op, loss, output_layer], {input_data_x: x, input_data_y: y})
if step % 10 == 0:
print(l)
# 画出最后的拟合结果图。
plt.cla()
plt.scatter(x, y)
plt.plot(x, pred, 'r-', lw=5)
plt.text(0.5, 0, 'Loss=%.4f' % l, fontdict={'size': 20, 'color': 'red'})
plt.pause(0.1)
plt.show()

五、模型的存储和重载

很多时候,我们会需要使用预训练的模型,这样我们需要把模型重载。但是重载之前,我们得先学会如何保存模型。

tf 中模型的保存也比较简单,讲起来两步就能搞定。

1.实例化 Saver 类 (Saver 类是 tf 中用来存储和重载的类)

2.保存模型数据

代码形式如下:

1
2
3
4
5
6
7
8
9
# 定义一个 Saver 类
saver = tf.train.Saver()

# 训练模型的过程
....

# 模型训练完了, 将模型保存到 save/model.ckpt 文件中,
# write_meta_graph=False 表示不存储网络结构,只存储网络中的参数。 如同 pytorch 中的 torch.save(model.state_dict(), "save/model.ckpt")
saver.save(sess, "./model_params", write_meta_graph=False)

我们保存完模型之后,如果下次要用这个模型,该怎么办呢? 同样使用 Saver 类就行了,代码如下:

1
2
3
4
5
6
# 模型重新建立的过程
...

# 这次我们不需要初始化,直接将存储好的模型进行加载
saver = tf.train.Saver()
saver.restore(sess, "./model_params")

六、数据处理和分批

用过 Pytorch 的人都知道 Dataset 和 Dataloader 合起来用起来有多爽。那么 tf 有没有这么爽的东西呢?

有的,而且功能也差不多… 叫 tf.data.Dataset 和 tf.data.Iterator

先从一个简单的例子来看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 创建一个Dataset对象
dataset = tf.data.Dataset.from_tensor_slices([1,2,3,4,5,6,7,8,9])
# 创建一个迭代器
iterator = dataset.make_one_shot_iterator()
# get_next()函数可以帮助我们从迭代器中获取元素
element = iterator.get_next()

# 遍历迭代器,获取所有元素
with tf.Session() as sess:
for i in range(9):
print(sess.run(element))

# 打印结果
# 1 2 3 4 5 6 7 8 9

当我们需要封装的数据是比较简单的数据的时候,我们可以直接使用 tf.data.Dataset.from_tensor_slices() 将数据包装成 Dataset 类。然后调用 make_one_shot_iterator() 变成一个可以迭代器,最后通过 get_next() 帮助我们从迭代器中获取元素。

再从一个复杂一些的例子来看,比如现在我们需要从硬盘中读取图片信息。假设,我们知道文件夹下面放着的图片的信息,那么我们可以这样做。

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
# 先建立一个常量保存所有图片的路径
filenames = tf.constant(["/var/data/image1.jpg", "/var/data/image2.jpg", ...])

# 再理想一些,每个图片对应的类别我们也清楚
labels = tf.constant([0, 37, ...])

# 构建一个用于解析图片数据的函数
def _parse_images(filename, label):
image_string = tf.read_file(filename)
# 当这个报错了,可以查看 API 文档,选择对应的图片格式进行解析,
# 比如 tf.image.decode_jpg(image_string) 用来处理 jpg 格式的图片
image_decoded=tf.image.decode_image(image_string)
# 调整图片大小,并且选用 method,使用最邻近插值,返回的结果仍为图像数据,使用其他方法则返回float数据
image_resized = tf.image.resize_images(image_decoded, [28, 28],method=1)

return image_resized, label

# 构建 dataset
dataset = tf.data.Dataset.from_tensor_slices((filenames, labels))
# 用 _parse_images 函数来处理对应的 dataset
dataset = dataset.map(_parse_function)
# 使用 batch = 64
batched_dataset = dataset.batch(64)


# 构建迭代器
iterator=batched_dataset.make_one_shot_iterator()
# 构建 next element
next_element = iterator.get_next()
# 得到数据和标签
features, label=sess.run(next_element)

最后再补充一些理论知识,看得懂就看,看不懂就随意吧…

tf.data.Dataset:表示一串元素(elements,tfrecors中的example),其中每个元素包含了一或多个Tensor对象。例如:在一个图片pipeline中,一个元素可以是单个训练样本,它们带有一个表示图片数据的tensors和一个label组成的pair。对于datasets其实理解为一个数据堆就行,我们可以在这个数据堆上进行多种操作,预处理、排序、batching等等。有两种不同的方式创建一个dataset

  • 创建一个source (例如:Dataset.from_tensor_slices()), 从一或多个tf.Tensor对象中构建一个dataset
  • 应用一个transformation(例如:Dataset.batch()),从一或多个tf.data.Dataset对象上构建一个dataset

tf.data.Iterator:它提供了主要的方式来从一个dataset中抽取元素。通过Iterator.get_next() 返回的该操作会yields出Datasets中的下一个元素,作为输入pipeline和模型间的接口使用。

其实数据处理的过程有时候才是最花时间的过程,这里仅仅只是蜻蜓点水一般的略过了一下,还是得多看 API 文档

七、CNN 和 MNIST

终于到了用 CNN 来处理 MNIST 的环节了….