Tensorflow:LinearSVM(一)

浏览: 2077

(这里是最终成品的 GitHub 地址)

本文拟通过使用 Tensorflow 实现一个朴素的线性支持向量机(LinearSVM)的形式来作为 Tensorflow 的“应用式入门教程”。虽说用 mnist 做入门教程项目几乎是约定俗成的事了,但总感觉照搬这么个东西过来当专栏有些水……所以还是自己亲手写了个 LinearSVM ( σ'ω')σ

在实现之前,先简要介绍一下 LinearSVM 算法:

  • 输入:D=\{ (x_1,y_1),...,(x_N,y_N)\}
  • 损失函数:L=\sum_{i=1}^N\left| 1-y_i\left( w\cdot x_i+b\right)\right|_+ + \frac12\| w\|^2 ,其中\left| x\right|_+=\max{(x, 0)}
  • 输出:f(x)=w\cdot x+b

以及介绍一下 Tensorflow 的若干思想:

  • Tensorflow 的核心在于它能构建出一张“运算图(Graph)”,我们需要做的是往这张 Graph 里加入元素
  • 基本的元素有如下三种:常量(constant)、可训练的变量(Variable)和不可训练的变量(Variable(trainable=False))
  • 由于机器学习算法常常可以转化为最小化损失函数,Tensorflow 利用这一点、将“最小化损失”这一步进行了很好的封装。具体而言,你只需要在 Graph 里面将损失表达出来后再调用相应的函数、即可完成所有可训练的变量的更新

其中第三点我们会在实现 LinearSVM 时进行相应说明,这里则会把重点放在第二点上。首先来看一下应该如何定义三种基本元素以及相应的加、减、乘、除(值得一提的是,在 Tensorflow 里面、我们常常称处于 Graph 之中的 Tensorflow 变量为“Tensor”,于是 Tensorflow 就可以理解为“Tensor 的流动”)(注:Tensor 这玩意儿叫张量,数学上是挺有来头的东西;然而个人认为如果不是做研究的话就完全可以不管它数学内涵是啥、把它当成高维数组就好 ( σ'ω')σ):

import tensorflow as tf

# 定义常量、同时把数据类型定义为能够进行 GPU 计算的 tf.float32 类型
x = tf.constant(1, dtype=tf.float32)
# 定义可训练的变量
y = tf.Variable(2, dtype=tf.float32)
# 定义不可训练的变量
z = tf.Variable(3, dtype=tf.float32, trainable=False)
x_add_y = x + y
y_sub_z = y - z
x_times_z = x * z
z_div_x = z / x

此外,Tensorflow 基本支持所有 Numpy 中的方法、不过它留给我们的接口可能会稍微有些不一样。以“求和”操作为例:

# 用 Numpy 数组进行 Tensor 的初始化
x = tf.constant(np.array([[1, 2], [3, 4]]))
# Tensorflow 中对应于 np.sum 的方法
axis0 = tf.reduce_sum(x, axis=0) # 将会得到值为 [ 4 6 ] 的 Tensor
axis1 = tf.reduce_sum(x, axis=1) # 将会得到值为 [ 3 7 ] 的 Tensor

更多的操作方法可以参见这里

最后要特别指出的是,为了将 Graph 中的 Tensor 的值“提取”出来、我们需要定义一个 Session 来做相应的工作。可以这样理解 Graph 和 Session 的关系(注:该理解可能有误!如果我确实在瞎扯的话,欢迎观众老爷们指出 ( σ'ω')σ):

  • Graph 中定义的是一套“运算规则”
  • Session 则会“启动”这一套由 Graph 定义的运算规则,而在启动的过程中、Session 可能会额外做三件事:
    • 赋予“运算规则”中一些“占位符”以具体的值
    • 更新所有可训练的变量(如果启动的运算规则包括“更新参数”这一步的话)
    • 从运算规则中提取出想要的中间结果

其中“占位符”和“更新参数”的相关说明会放在后文进行,这里我们只说明“提取中间结果”是什么意思。比如现在 Graph 中有这么一套运算规则:x=1,\ \ y=x+1,\ \ z=y+1,而我只想要运算规则被启动之后、y 的运算结果。该需求的代码实现如下:

x = tf.constant(1)
y = x + 1
z = y + 1
print(tf.Session().run(y)) # 将会输出 2

如果我想同时获得 y 和 z 的运算结果的话,只需将第 4 行改为如下代码即可:

print(tf.Session().run([y, z]))   # 将会输出 [2, 3]

最后想要特别指出一个非常容易犯错的地方:当我们使用了 Variable 时,必须要先调用初始化的方法之后、才能利用 Session 将相应的值从 Graph 里面提取出来。比如说,下面这段代码是会报错的:

x = tf.Variable(1)
print(tf.Session().run(x)) # 报错!

应该改为:

x = tf.Variable(1)
with tf.Session().as_default() as sess:
sess.run(tf.global_variables_initializer())
print(sess.run(x))

其中 tf.global_variables_initializer() 的作用可由其名字直接得知:初始化所有 Variable

接下来就是 LinearSVM 的实现了,由前文的讨论可知,关键只在于把损失函数的形式表达出来(利用到了  ClassifierBase):

import tensorflow as tf
from Util.Bases import ClassifierBase

class TFLinearSVM(ClassifierBase):
def __init__(self):
super(TFLinearSVM, self).__init__()
self._w = self._b = None
# 使用 self._sess 属性来存储一个 Session 以方便调用
self._sess = tf.Session()

def fit(self, x, y, sample_weight=None, lr=0.001, epoch=10 ** 4, tol=1e-3):
# 将 sample_weight(样本权重)转换为 constant Tensor
if sample_weight is None:
sample_weight = tf.constant(
np.ones(len(y)), dtype=tf.float32, name="sample_weight")
else:
sample_weight = tf.constant(
np.array(sample_weight) * len(y), dtype=tf.float32, name="sample_weight")
# 将输入数据转换为 constant Tensor
x, y = tf.constant(x, dtype=tf.float32), tf.constant(y, dtype=tf.float32)
# 将需要训练的 w、b 定义为可训练 Variable
self._w = tf.Variable(np.zeros(x.shape[1]), dtype=tf.float32, name="w")
self._b = tf.Variable(0., dtype=tf.float32, name="b")
# ========== 接下来的步骤很重要!!! ==========
# 调用相应方法获得当前模型预测值
y_pred = self.predict(x, True, False)
# 利用相应函数计算出总损失:
# cost = ∑_(i=1)^N max⁡(1-y_i⋅(w⋅x_i+b),0)+1/2 + 0.5 * ‖w‖^2
cost = tf.reduce_sum(tf.maximum(
1 - y * y_pred, 0) * sample_weight) + tf.nn.l2_loss(self._w)
# 利用 Tensorflow 封装好的优化器定义“更新参数”步骤
# 该步骤会调用相应算法、以减少上述总损失为目的来进行参数的更新
train_step = tf.train.AdamOptimizer(learning_rate=lr).minimize(cost)
# 初始化所有 Variable
self._sess.run(tf.global_variables_initializer())
# 不断调用“更新参数”步骤;如果期间发现误差小于阈值的话就提前终止迭代
for _ in range(epoch):
# 这种写法是比较偷懒的写法,得到的 cost 将不太精确
if self._sess.run([cost, train_step])[0] < tol:
break

然后就要定义获取模型预测值的方法—— self.predict 了:

def predict(self, x, get_raw_results=False, out_of_sess=True):
# 利用 reduce_sum 方法算出预测向量
rs = tf.reduce_sum(self._w * x, axis=1) + self._b
if not get_raw_results:
rs = tf.sign(rs)
# 如果 out_of_sess 参数为 True、就要利用 Session 把具体数值算出来
if out_of_sess:
rs = self._sess.run(rs)
# 否则、直接把 Tensor 返回即可
return rs

之所以要额外用一个 out_of_sess 参数控制输出的原因如下:

  • Tensorflow 在内部进行 Graph 运算时是无需把具体数值算出来的、不如说使用原生态的 Tensor 进行运算反而会快很多
  • 当模型训练完毕后,在测试阶段我们希望得到的当然是具体数值而非 Tensor、此时就需要 Session 帮我们把中间结果提取出来了

以上就是 LinearSVM 的完整实现,可以看到还是相当简洁的

这里特别指出这么一点:利用 Session 来提取中间结果这个过程并非是没有损耗的;事实上,当 Graph 运算本身的计算量不大时,开启、关闭 Session 所造成的开销反而会占整体开销中的绝大部分。因此在我们编写 Tensorflow 程序时、要注意避免由于贪图方便而随意开启 Session。这也是为什么要用 out_of_sess 参数来控制输出的最重要的原因——我们要避免反复开启 Session

然而只做到这一点是不够的;事实上,如果在同一段代码中不断地调用参数 out_of_sess 为 True 的 predict 方法的话,会发现它的速度越来越慢。这是因为我们计算模型输出时用的语句:

rs = tf.reduce_sum(self._w * x, axis=1) + self._b

中,x 不是一个 Tensor 而是一个 numpy 数组;这就导致 Tensorflow 每次运行这个语句时,都会在 Graph 中重新写一遍相关的运算规则。久而久之,Graph 中就会堆积大量冗余的运算规则,从而导致程序越跑越慢。事实上这正是初学 Tensorflow 的大坑之一,观众老爷们需要格外注意 ( σ'ω')σ

那么解决方案是什么呢?就是前文说过的占位符(Placeholder)。具体而言:

  • Placeholder 是一个 Tensor,它是可以在 Graph 中写入相应的运算规则的
  • 当我想用这套运算规则算出某个具体的数值时,只需要将相应的输入数据“喂给”Placeholder 即可

所以我们就能通过“用 Placeholder 写出 predict 的运算规则 → 在真正 predict 时把数据喂给 Placeholder”这样的方式、来避免每次 predict 时都要重写运算规则。一般而言,几乎所有的 Tensorflow 程序都会有类似的步骤;如果没有的话、就要注意一下是否落入了坑中 ( σ'ω')σ

Placeholder 详细的应用会在下一篇文章中叙述,这里就再说一下其核心思想:将未能确定的信息以 Placeholder 的形式进行定义、在确实调用到的时候再赋予具体的数值,这样子能够避免重复向 Graph 中写入相同运算规则、从而避免了 Tensorflow 程序出现“越跑越慢”的情况

不过虽说有这么些不完美的地方,但以上的诸多实现就已经差不多构成 Tensorflow 的一个入门级教程了;虽然我是抱着“即使从来没用过 Tensorflow 也能看懂”的心去写的,但可能还是会有地方说得不够详细;若果真如此,还愿不吝指出 ( σ'ω')σ

下一篇文章在介绍 Placeholder 的同时,会介绍一些比较泛的东西(比如如何进行抽象、如何简单地考虑内存问题等),大概可以算是 Tensorflow 的一个基础级教程

至于进阶级……估计我能力就不够了(趴

(猛戳我进入下一章!( σ'ω')σ)

推荐 1
本文由 射命丸咲 创作,采用 知识共享署名-相同方式共享 3.0 中国大陆许可协议 进行许可。
转载、引用前需联系作者,并署名作者且注明文章出处。
本站文章版权归原作者及原出处所有 。内容为作者个人观点, 并不代表本站赞同其观点和对其真实性负责。本站是一个个人学习交流的平台,并不用于任何商业目的,如果有任何问题,请及时联系我们,我们将根据著作权人的要求,立即更正或者删除有关内容。本站拥有对此声明的最终解释权。

0 个评论

要回复文章请先登录注册