为什么选择TensorFlow?

  自从12年AlexNet获得ImageNet大赛的冠军后,深度学习开始流行起来,也因为硬件的快速发展GPU并行计算配合易用的API,让深度学习以及神经网络大方光彩。

  深度学习的框架其实有很多,目前来说最火的还要数PyTorch,TensorFlow以及Keras。其中Pytorch比较适合学术研究,自己搞着玩,如果工业实践就不太适合了。TensorFlow由于时间比较久,学起来比较困难,不过有完整的开发,部署方案,还有大量的GitHub项目可供参考。Keras则是TensorFlow的一个高级API,同类的还有TensorFlow的TFLearn等等。

  总结来说,如果是学生的话,只是为了论文或者学习,那么推荐Pytorch;如果是公司的开发者,想要在业务员中使用深度学习,推荐直接使用TensorFlow,如果使用最新的1.14,那么官网的示例里面就已经是Keras了;如果是从GitHub上面下载了源码想要学习,那就得去学习对应版本的TensorFlow API了。

  在总结一下TensorFlow的优点:

  • 易用性:有对应Python的API
  • 可移植性:一套代码就可以适应单个或者多个CPU、GPU、移动设备等
  • 灵活性:可以部署在树莓派、安卓、windows、ios、linux等上
  • 可视化:有tensorboard提供开发的可视化界面,方便跟踪调参
  • 检查点:可以通过检查点记录保存实验数据
  • 自动微积分:自动求解梯度
  • 庞大的社区:一年内拥有10000+的开发者,3000+的项目
  • 大量基于TensorFlow的项目代码

  在使用TensorFlow的公司包括:Google ,OpenAI,DeepMind,SnapChat,Airbus,eBay等。

  下面就来学习下TensorFlow的基础知识,TensorFlow不仅提供了基础的语法,还提供了一些简化的API:

  • TF Learn,tf.contrib.learn,基于scikit-learn风格的API
  • TF Slim,tf.contrib.slim,轻量级的tf构建API,可以自动配置默认值,简化使用
  • Keras,更高级更抽象的API,使用Keras之后,就像叠积木一样创建模型,不过对于背后的原理隐藏的太深太深

  TensorFlow的名字中已经说明了它最重要的两个概念——Tensor和Flow。Tensor就是张量,张量这个概念在数学或者物理学中可以有不同的解释,但是这里我们不强调它本身的含义。在TensorFlow中,张量可以被简单地理解为多维数组,Flow翻译成中文就是“流”,它直观的表述计算的编程系统。TensorFlow中的每一个计算都是计算图上的一个节点,而节点之间的边描述了计算之间的依赖关系。

TensorFlow计算模型——计算图的概念

  计算图是TensorFlow中最基本的一个概念,TensorFlow中所有计算都会被转化为计算图上的一个节点。

  在TensorFlow程序中,所有的数据都通过张量的形式来表示。从功能的角度上看,张量可以被简单理解为多为数组。其中零阶张量表示标量(scalar),也即是一个数(张量的类型也可以是字符串)。第一阶张量为向量(vector),也就是一个一维数组;第 n 阶张量可以理解为一个 n 维数组。但是张量在TensorFlow中的实现并不是直接采用数组的形式,它只是对TensorFlow中运算结果的引用。在张量中并没有真正保存数字,它保存的是如何得到这些数字的计算过程。以向量加法为例,当运行如下代码的时候,得到的不是加法的结果,而是对结果的一个引用。

#_*_coding:utf-8_*_
import tensorflow as tf # tf.constant 是一个计算,这个计算的结果为一个张量,保存在变量a中
a = tf.constant([1.0, 2.0], name='a')
b = tf.constant([2.0, 3.0], name='b') result = a + b
# print(result) # Tensor("add:0", shape=(2,), dtype=float32)

  从上面的结果来看,TensorFlow的张量和Numpy的数组不同,他计算的结果不是一个具体的数字,而是一个张量的结构。从上面结果来看,一个张量主要保存了三个属性,名字(name),维度(shape)和类型(type)。

  张量的第一个属性名字不仅是一个张量的唯一标识符,它同样也给出了这个张量是如何计算的,TensorFlow的计算都可以通过计算图的模型来建立,而计算图上的每一个节点代表一个计算,计算的结果就保存在张量之中。所以张量和计算图上节点所代表的计算结果是对应的。所以张量的命名就可以通过“node : src_output”的形式来给出。其中node为节点的名称,src_output 表示当前张量来自节点的第几个输出。比如上面的“add:0” 就说明了result这个张量是计算节点“add” 输出的第一个结果(编号从0 开始)。

  张量的第二个属性是张量的维度。这个属性描述了一个张量的维度信息,比如上面样例中 shape = (2, ) 说明了张量 result 是一个一维数组,这个数组的长度为2。维度是张量一个很重要的属性,围绕张量的维度TensorFlow也给出了很多有用的运算。

  张量的第三个属性就是类型(type),每一个张量会有一个唯一的类型。TensorFlow 会对参与运算的所有张量进行类型的检查,当发现类型不匹配的时候会报错,比如下面的代码就会得到类型不匹配的错误:

#_*_coding:utf-8_*_
import tensorflow as tf # tf.constant 是一个计算,这个计算的结果为一个张量,保存在变量a中
a = tf.constant([1, 2], name='a')
b = tf.constant([2.0, 3.0], name='b') result = a + b

  这段代码和上面例子基本一模一样,唯一不同就是把其中一个加数的小数点去掉了。这会使得加数 a 的类型为整数而加数 b 的类型为实数,这样程序就会报类型不匹配的错误:

ValueError: Tensor conversion requested dtype int32 for Tensor with
dtype float32: 'Tensor("b:0", shape=(2,), dtype=float32)'

  如果将第一个加数指定成实数类型 ,如下:

a = tf.constant([1, 2], name='a', dtype=tf.float32)

  那么两个加数的类型相同,就不会报错了。如果不指定类型,则会默认为 int32,而带小数的则会默认为float32.所以一般建议通过指定dtype来明确指出变量或者常量的类型。

tensorflow中的数据类型列表

张量的使用

  和TensorFlow的计算模型相比,TensorFlow的数据模型相比较简单。张量使用主要可以总结为两大类。

  第一类用途是对中间计算结果的引用。当一个计算包含很多中间结果时,使用张量可以大大提高代码的可读性。比如上面的例子。

  第二类是当计算图构造完成之后,张量可以用来获得计算结果,也就是得到真实的数字,虽然张量本身没有存储具体的数字,但是通过下面的Session就可以得到具体的数字。

  

会话(TensorFlow运行模型——Session)

  下面学习如何使用会话(session)来执行定义好的运算,会话拥有并管理TensorFlow程序运行时的所有资源。当所有计算完成之后需要关闭会话来帮助系统回收资源,否则就可能出现资源泄露的问题。TensorFlow中使用会话的模式一般有两种,第一种模式需要明确调用会话生成函数和关闭会话函数,这么模式如下:

# 创建一个会话
sess = tf.Session()
# 使用这个创建好的会话来得到关心的运算结果
# 比如可以调用 sess.run(result) 来得到张量计算的结果
sess.run(...)
# 关闭会话使得本次运算中使用到的资源可以被释放
sess.close()

  使用这种模式的时候,在所有计算完成之后,需要明确调用Session.close 函数来关闭会话并释放资源。然而,当程序因为异常而退出时,关闭会话的函数可能就不会被执行而导致资源泄露。为了解决异常退出时资源释放的问题,TensorFlow可以通过Python的上下文管理器来使用会话,也就是可以利用 with 代码块生成Session,限制作用域,代码如下:

# 创建一个会话,并通过python中的上下文管理器来管理这个会话
with tf.Session() as sess:
# 使用这创建好的会话来计算关心的结果
  sess.run(...)
# 不需要再调用“Session.close()” 函数来关闭会话
# 当上下文退出时会话关闭和资源释放也自动完成了。

  通过Python上下文管理器的机制,只要将所有的计算放在'with' 的内部就可以。当上下文管理器退出时候会自动释放所有资源。这样即解决了因为异常退出时资源释放的问题,同时也解决了忘记调用Session.close 函数而产生的资源泄露问题。

  Session 函数中没有传入参数,表明该代码将会依附于(如果还没有创建会话,则会创建新的会话)默认的本地会话。生成会话之后,所有的 tf.Variable 实例都会通过调用各自初始化操作的 sess.run() 函数进行初始化。

init = tf.initialize_all_variables()
sess.run(init)

  在通过initializer给变量赋值固然可行,但是当变量的数据增多后,或者变量之间存在依赖关系时,单个调用的方案就比较麻烦了。所以使用上述代码更加便捷。

  sess.run() 方法将会运行图表中与作为参数传入的操作相对应的完整子集。在初始调用时, init操作只包含了变量初始化程序 tf.group。图标的其他部分不会再这里,而是在下面的训练训练运行。

  在交互式环境中(比如Python脚本或者Jupyter的编译器下),通过设置默认会话的方式来获得张量的取值更加方便。所有TensorFlow提供了一种在交互式环境下直接构建默认会话的函数,这和函数就是tf.InteractiveSession.使用这个函数会自动将生成的会话注册为默认会话。下面代码展示了tf.InteractiveSession 函数的用法:

sess = tf.InteractiveSession()
print(result.eval())
# 其实 sess.run(result) 和 result.eval(session=sess)) 功能相同
sess.close()

  通过tf.InteractiveSession 函数可以省去将产生的会话注册为默认会话的过程。

  变量在被使用前,需要通过会话(session)运行其初始化方法完成初始化赋值。

sess.run(tf.global_variables_initializer)

  注意:在新版本的tensorflow中,使用下面代码替换上面代码,不然会报 Warning。

sess.run(tf.global_variables_initializer)

神经网络参数与TensorFlow变量tf.Variable()

  神经网络中的参数是神经网络实现分类或者回归问题中重要的部分。在TensorFlow中变量(tf.Variable()的作用就是保存和更新神经网络中的参数),下面学习一下变量的定义:

def __init__(self,
initial_value=None,
trainable=True,
collections=None,
validate_shape=True,
caching_device=None,
name=None,
variable_def=None,
dtype=None,
expected_shape=None,
import_scope=None):
  • initial_value:初始化的值,可以是随机数,常数或者通过其他变量的初始值得到的。
  • trainable:标记是否加入GraphKeys.TRAINABLE_VARIABLES集合
  • validate_shape:如果为False则可以更改shape
  • dtype:变量的类型,不可改变

  下面代码给出了一种在TensorFlow中声明一个2*3的矩阵变量的方法:

w1 = tf.Variable(tf.random_normal([2, 3], stddev=1, seed=1))

  首先它调用了TensorFlow变量的声明函数tf.Variable。在变量声明函数中给出了初始化这个变量的方法。TensorFlow中变量的初始值可以设置成随机数,常数或者是同其他变量的初始值计算得到。在上面的样例中,tf.reandom_normal([2, 3], stddev=2)会产生一个2*3的矩阵,矩阵的元素均值为0,标准差为2 的随机数。tf.random_normal函数可以通过参数mean来指定平均值,在没有指定时默认为0,通过满足正态分布的随机数来初始化神经网络中的参数是一个非常有用的方法,除了正态分布的随机数,TensorFlow还提供了一些其他的随机数生成器,下图列出了TensorFlow目前支持的所有随机数生成器

  TensorFlow也支持通过常数来初始化一个变量,下图给出了TensorFlow中常用的常量声明方法:

  在神经网络中,偏置项(bias)通常会使用常数来设置初始值,下面代码给出了一个例子:

# 下面会产生一个初始值为0且长度为3 的变量
biases = tf.Variable(tf.zeros([3]))

  当然,TensorFlow也支持通过其他变量的初始值来初始化新的变量,下面给出了具体的方法:

# 声明w1 w2两个变量
w1 = tf.Variable(weights.initialized_value())
w2 = tf.Variable(weights.initialized_value() * 2.0)

  以上代码中,w1的初始值被设置成了与weights变量相同,w2的初始值则是weights初始值的两倍。在TensorFlow中,一个变量的初始化过程需要被明确的调用。

  类似于张量,维度(shape)和类型(type)也是变量最重用的两个属性,和大部分程序语言类似,变量的类型是不可改变的。一个变量在被构建之后,它的类型就不能再改变量。

  如下代码会报出类型不匹配的错误:

# 声明w1 w2两个变量
# 定义神经网络的参数
w1 = tf.Variable(tf.random_normal([2, 3], stddev=1), name='w1')
w2 = tf.Variable(tf.random_normal([2, 3], dtype=tf.float64, stddev=1), name='w2') w1.assign(w2)
'''
程序会报错:
TypeError: Input 'value' of 'Assign' Op has type float64 that does not
match type float32 of argument 'ref'.
'''

  维度是另外一个重要的属性,和类型不大一样的是,维度在程序运行中是有可能改变的,但是需要设置参数,如下:

# 声明w1 w2两个变量
# 定义神经网络的参数
w1 = tf.Variable(tf.random_normal([2, 3], stddev=1), name='w1')
w2 = tf.Variable(tf.random_normal([2, 3], stddev=1), name='w2') w1.assign(w1, w2)
'''
程序会报错(维度不匹配的错误):
TypeError: Expected bool for argument 'use_locking' not
<tf.Variable 'w2:0' shape=(2, 3) dtype=float32_ref>.
'''
# 下面代码可以被成功执行
tf.assign(w1, w2, validate_shape=False)

  虽然TensorFlow支持更改变量的维度,但是这种做法比较罕见。

常用Tensorflow函数笔记

  下面是几个在Tensorflow中常用的函数

(1)tf.argmax(input, axis=None, name=None, dimension=None)
此函数是对矩阵按行或列计算最大值
参数
input:输入Tensor
axis:0表示按列,1表示按行
name:名称
dimension:和axis功能一样,默认axis取值优先。新加的字段
返回:Tensor 行或列的最大值下标向量 (2)tf.equal(a, b)
此函数比较等维度的a, b矩阵相应位置的元素是否相等,相等返回True,否则为False
返回:同维度的矩阵,元素值为True或False (3)tf.cast(x, dtype, name=None)
将x的数据格式转化成dtype.例如,原来x的数据格式是bool,
那么将其转化成float以后,就能够将其转化成0和1的序列。反之也可以 (4)tf.reduce_max(input_tensor, reduction_indices=None,
keep_dims=False, name=None)
功能:求某维度的最大值 (5)tf.reduce_mean(input_tensor, reduction_indices=None,
keep_dims=False, name=None)
功能:求某维度的均值
参数1--input_tensor:待求值的tensor。
参数2--reduction_indices:在哪一维上求解。0表示按列,1表示按行
参数(3)(4)可忽略
例:x = [ 1, 2
3, 4]
x = tf.constant([[1,2],[3,4]], "float")
tf.reduce_mean(x) = 2.5
tf.reduce_mean(x, 0) = [2, 3]
tf.reduce_mean(x, 1) = [1.5, 3.5] (6)tf.truncated_normal(shape, mean=0.0, stddev=1.0,
dtype=tf.float32, seed=None, name=None)
从截断的正态分布中输出随机值
shape: 输出的张量的维度尺寸。
mean: 正态分布的均值。
stddev: 正态分布的标准差。
dtype: 输出的类型。
seed: 一个整数,当设置之后,每次生成的随机数都一样。
name: 操作的名字。 (7)tf.random_normal(shape, mean=0.0, stddev=1.0,
dtype=tf.float32, seed=None, name=None)
从标准正态分布中输出随机值 (8) tf.nn.conv2d(input, filter, strides, padding,
use_cudnn_on_gpu=None, data_format=None, name=None)
在给定的4D input与 filter下计算2D卷积
1,输入shape为 [batch, height, width, in_channels]: batch为图片数量,in_channels为图片通道数
2,第二个参数filter:相当于CNN中的卷积核,它要求是一个Tensor,
具有[filter_height, filter_width, in_channels, out_channels]这样的shape,
具体含义是[卷积核的高度,卷积核的宽度,图像通道数,卷积核个数],要求类型与
参数input相同,有一个地方需要注意,第三维in_channels,就是参数input的第四维
3,第三个参数strides:卷积时在图像每一维的步长,这是一个一维的向量,长度4
4,第四个参数padding:string类型的量,只能是"SAME","VALID"其中之一,
这个值决定了不同的卷积方式(后面会介绍)
5,第五个参数:use_cudnn_on_gpu:bool类型,是否使用cudnn加速,默认为true
结果返回一个Tensor,这个输出,就是我们常说的feature map,shape仍然是
[batch, height, width, channels]这种形式。 (9)tf.nn.max_pool(value, ksize, strides, padding, name=None)
参数是四个,和卷积很类似:
第一个参数value:需要池化的输入,一般池化层接在卷积层后面,所以输入
通常是feature map,依然是[batch, height, width, channels]这样的shape
第二个参数ksize:池化窗口的大小,取一个四维向量,一般是[1, height, width, 1],
因为我们不想在batch和channels上做池化,所以这两个维度设为了1
第三个参数strides:和卷积类似,窗口在每一个维度上滑动的步长,一般也是[1, stride,stride, 1]
第四个参数padding:和卷积类似,可以取'VALID' 或者'SAME' 返回一个Tensor,类型不变,
shape仍然是[batch, height, width, channels]这种形式 (10) tf.reshape(tensor, shape, name=None)
函数的作用是将tensor变换为参数shape的形式。
其中shape为一个列表形式,特殊的一点是列表中可以存在-1。-1代表的含义是不用我
们自己指定这一维的大小,函数会自动计算,但列表中只能存在一个-1。(当然如果存
在多个-1,就是一个存在多解的方程了) (11)tf.nn.dropout(x, keep_prob, noise_shape=None, seed=None,name=None)
为了减少过拟合,随机扔掉一些神经元,这些神经元不参与权重的更新和运算
参数:
x : 输入tensor
keep_prob : float类型,每个元素被保留下来的概率
noise_shape : 一个1维的int32张量,代表了随机产生“保留/丢弃”标志的shape。
seed : 整形变量,随机数种子。
name : 名字,没啥用。

  

通过TensorFlow游乐场了解神经网络

   首先我们通过TensorFlow游乐场来快速了解神经网络的主要功能。TensorFlow游乐场是一个通过网页浏览器就可以训练的简单神经网络并实现了可视化训练过程的工具。

  TensorFlow游乐场的地址:http://playground.tensorflow.org/

  从上图中可以看出,TensorFlow的左侧提供四个不同的数据集来测试神经网络。默认的数据为左上角被框出来的那个。被选中的数据也会显示在上面最右边的“OUTPUT”栏目下。在这个数据中,可以看到一个二维平面上有蓝色或者橙色的点,每一个小点都代表了一个样例,而点的颜色代表了样例的标签。因为点的颜色只有两种,所有这是一个二分类问题。在这里举这么一个例子来说明这个数据可以代表的实际问题。假设需要判断某工厂生产的零件是否合格,那么蓝色的点可以表示所有合格的零件,而橙色代表不合格的零件。这样判断一个零件是否合格就变成了区分点的颜色。

  为了将一个实际问题对应到屏幕上不同颜色点的划分,还需要将实际问题中的实体,比如上述例子中的零件,变成屏幕上的一个点。这就是特征提取解决的问题。还是以零件为例,可以用零件的长度和质量来大致描述一个零件。这样一个物理意义上的零件就可以被转化成长度和质量这两个数字。在机器学习中,所有用于描述实体的数字的组合就是一个实体的特征向量(feature vector)。而特征向量的提取对机器学习的效果至关重要,通过特征提取就可以将实际问题中的实体转化为空间中的点。假设使用长度和质量作为一个零件的特征向量,那么每个零件就是二维平面上的一个点。TensorFlow游乐园中Features一栏对应了特征向量。

  特征向量是神经网络的输入,神经网络的主体结构显示了在上图的中间位置。目前主流的神经网络都是分层的结构,第一层是输入层,代表特征向量中每一个特征的取值。比如如果一个零件的长度是0.5,那么x1的值就是0.5。同一层的节点不会相互连接,而且每一层只和下一层连接,直到最后一层作为输出层得到计算的结果。在二分类问题中,比如判断零件是否合格,神经网络的输出层往往只包含一个节点。在二分类问题中,比如判断零件是否合格,神经网络的输出层往往只包含一个节点,而这个节点会输出一个实数值。通过这个输出值和一个事先设定的阈值,就可以判断结果是零件合格,反之则零件不合格,一般可以认为当输出值离阈值越远得到的答案越可靠。

  在输入和输出层之间的神经网络叫做隐藏层,一般一个神经网络的隐藏层越多,这个神经网络就越“深”。而所谓深度学习中的这个“深度”和神经网络的层数也是密切相关的。在TensorFlow游乐场中可以通过点击加或者减来增加或者减少神经网络隐藏层的数量。处理可以选择深刻网络的深度,TensorFlow游乐场也支持选择神经网络每一层的节点数以及学习率(learning rate),激活函数(activation),正则化(regularization)。

  所以通过神经网络解决分类问题主要可以分为以下四个步骤:

  1,提取问题中实体的特征向量作为神经网络的输入。不同的实体可以提取不同的特征向量。

  2,定义神经网络的结构,并定义如何从神经网络的输入得到输出。这个过程可以是神经网络的前向传播算法

  3,通过训练数据来调整神经网络中参数的取值,这就是训练神经网络的过程。

  4,使用训练好的神经网络来预测未知的数据。

前向传播算法

  下面学习一下最简单的全连接网络结构的前向传播算法,并且将展示如何通过TensorFlow来实现这个算法。

  下面首先了解神经元的结构,神经元是一个神经网络的最小单位,下面显示一个最简单的神经元结构:

  从上图可以看出,一个神经元有多个输入和一个输出。每个神经元的输入既可以是其他神经元的输出,也可以是整个神经网络的输入。所谓神经网络的结构就是指的不同神经元之间的连接结构。一个最简单的神经元结构的输出就是所有输入的加权和,而不同输入的权重就是神经元的参数。神经网络的优化过程就是优化神经元中的参数取值的过程。

  下图给出了一个简单的判断零件是否合格的三层全连接神经网络,之所以称为全连接神经网络是因为相邻两层之间任意两个节点之间都有连接。

  下图展示一个判断零件是否合格的三层神经网络结构图:

  计算神经网络的前向传播结构需要三部分信息。第一个部分是神经网络的输入,这个输入就是从实体中提取的特征向量。比如上面有两个输入,一个是零件的长度x1,一个是零件的质量 x2,第二个部分为神经网络的连接结构。神经网络是由神经元构成的,神经网络的结构给出不同神经元之间输入输出的连接关系。神经网络中的神经元也可以称为节点。在上图中 a11节点有两个输入 ,分别是x1 和 x2的输出。而 a11 的输出则是节点 y 的输入。最后一个部分是每个神经元中的采纳数。我们用W来表示神经元中的参数。W的上标表名了神经网络的层数,比如W(1) 表示第一层节点的参数,而W(2) 表示第二层节点的参数。W的下标表明了连接节点编号,比如W(1) 1,2 表示连接 x1 和 a12节点的边上的权重。这里我们假设权重是已知的。

  当我们给定神经网络的输入,神经网络的结构以及边上权重,就可以通过前向传播算法来计算出神经网络的输出,下图展示了这个神经网络前向传播的过程:

  上图给出来输入层的取值,从输入层开始一层一层地使用前向传播算法,首先隐藏层中有三个节点,每一个节点的取值都是输入层取值的加权和。当求出输出值的阈值,判断是否大于0,这样就可以判断是否合格。上面整个过程就是前向传播的算法。

  当然前向传播的算法可以表示为矩阵乘法,将输入 x1  x2 组织成一个1*2 的矩阵x = [x1, x2],而W(1) 组织成一个2*3 的矩阵:

  这样通过矩阵乘法可以得到隐藏层三个节点所组成的向量取值:

  类似的输出层可以表示为:

  这样就可以将前向传播算法通过矩阵乘法的方式表达出来了。在TensorFlow中矩阵政法是非常容易实现的。以下代码实现了神经网络的前向传播过程:

# 定义神经网络前向传播的过程
a = tf.matmul(x, w1)
y = tf.matmul(a, w2)

  其中 tf.matmul 实现了矩阵乘法的功能。

  以下样例介绍了如何通过遍历实现神经网络的参数并实现前向传播的过程:

#_*_coding:utf-8_*_
import tensorflow as tf # 定义神经网络的参数
# 声明w1 w2两个变量,这里还通过seed设定了随机种子,这样可以保证运行结果一样
w1 = tf.Variable(tf.random_normal([2, 3], stddev=1, seed=1))
w2 = tf.Variable(tf.random_normal([3, 1], stddev=1, seed=1)) # 暂时将输出的特征向量定义为一个常量,注意这里x是一个1*2的矩阵
x = tf.constant([[0.7, 0.9]]) # 定义神经网络前向传播的过程
a = tf.matmul(x, w1)
y = tf.matmul(a, w2) sess = tf.Session()
# 这里不能直接通过sess.run(y)来获取y的取值
# 因为w1和w2 都还没有运行初始化过程,下面分别初始化两个变量
sess.run(w1.initializer)
sess.run(w2.initializer)
print(sess.run(y)) # 输出[[3.957578]]
sess.close()

  从代码中可以看出,当声明了变量w1 w2之后,可以通过w1  w2来定义神经网络的前向传播过程并得到中间结果 a 和最后答案 y 。但是这些被定义的计算在这一步中并不是真正的运算,当需要运行这些计算并得到具体的数字的时候,需要进入TensorFlow程序第二步。

  在第二步,我们会声明一个会话(session),然后通过会话计算结果。

通过TensorFlow训练神经网络模型

  使用监督学习的方式设置神经网络参数需要有一个标注好的训练数据集。以判断零件是否合格为例,这个标注好的训练数据集就是手机的一批合格零件和一批不合格零件。监督学习最重要的思想就是在已知答案的标注数据集上,模型给出的预测结果要尽量接近真实的答案。通过调整神经网络中的参数对训练数据进行拟合,可以使得模型对未知的样本提供预测的能力。

  在神经网络优化算法中,最常用的方法是反向传播算法(backpropagation),下图展示了使用反向传播算法训练神经网络的流程图:

  从上图可以看出,反向传播算法实现了一个迭代的过程。在每次迭代的开始,首先需要选取一小部分训练数据,这一小部分数据叫做一个batch。然后这个batch的样例会通过前向传播算法得到神经网络模型的额预测结果。因为训练数据都是由正确答案标注的,所以可以计算出当前神经网络模型的预测答案与正确答案之间的差距。最后,基于这预测值和真实值之间的差距,反向传播算法会相应的更新神经参数的取值,使得在这个batch上神经网络模型的预测结果和真实答案更加接近。

  通过TensorFlow实现反向传播算法的第一步是使用TensorFlow表达一个batch的数据,在之前我们使用常量来表达,但是如果每轮迭代中选取的数据都要通过常量来表示,那么TensorFlow都会在计算图中增加一个节点。一般来说,一个神经网络的训练过程会需要经过几百万轮甚至几亿轮的迭代,这样计算图就会非常大,而且利用率很低。为了避免这个问题,TensorFlow提供了placeholder机制用于提供输入数据。placeholder相当于定义了一个位置,这个位置中的数据在程序运行时再指定。这样在程序中就不需要生成大量常量来提供输入数据,而只需要将数据通过placeholder传入TensorFlow计算图。在placeholder定义时,这个位置上的数据类型是需要指定的。和其他张量一样,placeholder的类型也是不可以改变的。placeholder中数据的维度信息是可以根据提供的数据推导出来,所以不一定给出。

  下面给出了通过placeholder实现前向传播算法:

import tensorflow as tf

# 定义神经网络的参数
# 声明w1 w2两个变量,这里还通过seed设定了随机种子,这样可以保证运行结果一样
w1 = tf.Variable(tf.random_normal([2, 3], stddev=1, seed=1))
w2 = tf.Variable(tf.random_normal([3, 1], stddev=1, seed=1)) # 定义placeholder作为存放输入数据的地方,这里维度也不一定要定义
# 但是如果维度是确定的,那么给出维度可以降低出错的概率
x = tf.placeholder(tf.float32, shape=(1, 2), name='input') # 定义神经网络前向传播的过程
a = tf.matmul(x, w1)
y = tf.matmul(a, w2) sess = tf.Session()
# 这里不能直接通过sess.run(y)来获取y的取值
# 因为w1和w2 都还没有运行初始化过程,下面分别初始化两个变量
init_op = tf.global_variables_initializer()
sess.run(init_op)
'''
下面一行将报错:
InvalidArgumentError (see above for traceback): You must feed a value
for placeholder tensor 'input' with dtype float and shape [1,2]
'''
# print(sess.run(y)) # 下面一行将会得到之前一样的输出结果
print(sess.run(y, feed_dict={x: [[0.7, 0.9]]})) # [[3.957578]]
sess.close()

  在这段程序中替换了原来通过常量定义的输入 x ,在新的程序中计算前向传播结果时,需要提供一个feed_dict 来指定 x 的取值。 feed_dict 是一个字典(map),在字典中需要给出每个用到的placeholder的取值,如果某个需要的placeholder没有被指定取值,那么在程序运行时候会报错。

  在上面的样例程序中,如果将输入的1*2 矩阵改为 n*2 的矩阵,那么就可以得到 n 个样例的前向传播结果了。其中 n*2 的矩阵的每一行为一个样例数据。这样前向传播的结果为 n*1 的矩阵,这个矩阵的每一行就代表了一个样例的前向传播结果,下面程序给出一个实例:

import tensorflow as tf

# 定义神经网络的参数
# 声明w1 w2两个变量,这里还通过seed设定了随机种子,这样可以保证运行结果一样
w1 = tf.Variable(tf.random_normal([2, 3], stddev=1, seed=1))
w2 = tf.Variable(tf.random_normal([3, 1], stddev=1, seed=1)) # 定义placeholder作为存放输入数据的地方,这里维度也不一定要定义
# 但是如果维度是确定的,那么给出维度可以降低出错的概率
# x = tf.placeholder(tf.float32, shape=(1, 2), name='input')
x =tf.placeholder(tf.float32, shape=(3, 2), name='input') # 定义神经网络前向传播的过程
a = tf.matmul(x, w1)
y = tf.matmul(a, w2) sess = tf.Session()
# 这里不能直接通过sess.run(y)来获取y的取值
# 因为w1和w2 都还没有运行初始化过程,下面分别初始化两个变量
init_op = tf.global_variables_initializer()
sess.run(init_op) # 因为x 在定义时指定了 n 为3,所以在运行前向传播过程时需要提供三个样例数据 print(sess.run(y, feed_dict={x: [[0.7, 0.9], [0.1, 0.4], [0.5, 0.8]]}))
'''
输出结果为:
[[3.957578 ]
[1.1537654]
[3.1674924]]
'''
sess.close()

  上面的样例中展示了一次性计算多个样例的前向传播结果。在运行时,需要将3个样例组成一个3*2的矩阵传入placeholder。计算得到的结果为3*1 的矩阵。

  在得到一个batch的前向传播结果之后,需要定义一个损失函数来刻画当前的预测值和真实答案之间的差距。然后通过反向传播算法来调整神经网络参数的取值使得差距可以被缩小。下面定义一个简单的额损失函数,并通过TensorFlow定义反向传播的算法。

# 定义损失函数来刻画预测值与真实值的差距
cross_entropy = -tf.reduce_mean(
y_ * tf.log(tf.clip_by_value(y, le-10, 1.0))
)
# 定义学习率
learning_rate = 0.001
# 定义反向传播算法来优化神经网络中的采纳数
train_step = tf.train.AdamOptimizer(learning_rate).minimize(cross_entropy)

  在上面代码中,cross_entropy 定义了真实值和预测值之间的交叉熵(cross entropy),这是分类问题中一个常用的损失函数,第二行 train_step 定义了反向传播的优化方法。目前TensorFlow支持7种不同的优化器,比较常用的优化方法有三种:

tf.train.GradientDescentOptimizer
tf.train.AdamOptimizer
tf.train.MomentumOptimizer

  

完整神经网络样例程序

  代码如下:

# _*_coding:utf-8_*_
import tensorflow as tf # Numpy 是一个科学计算的工具包,这里通过Numpy工具包生成模拟数据集
from numpy.random import RandomState # 定义训练数据batch的大小
batch_size = 8 # 定义神经网络的参数
w1 = tf.Variable(tf.random_normal([2, 3], stddev=1, seed=1))
w2 = tf.Variable(tf.random_normal([3, 1], stddev=1, seed=1)) # 在shape的一个维度上使用None可以方便的表示使用不大的batch大小,
# 在训练时需要把数据分成比较小的batch,在测试的时候,可以一次性的使用全部的数据
# 但是数据集比较大的是,将大量数据放入一个batch可能会导致内存溢出。
x = tf.placeholder(tf.float32, shape=(None, 2), name='x-input')
y_ = tf.placeholder(tf.float32, shape=(None, 1), name='y-input') # 定义神经网络前向传播的过程
a = tf.matmul(x, w1)
y = tf.matmul(a, w2) # 定义损失函数来刻画预测值与真实值的差距
cross_entropy = -tf.reduce_mean(
y_ * tf.log(tf.clip_by_value(y, 1e-10, 1.0))
)
# 定义学习率
learning_rate = 0.001
# 定义反向传播算法来优化神经网络中的采纳数
train_step = tf.train.AdamOptimizer(learning_rate).minimize(cross_entropy) # 通过随机数生成一个模拟数据集
rdm = RandomState(1)
dataset_size = 128
X = rdm.rand(dataset_size, 2) # 定义规则来给出样本的标签,在这里所有x1+x2<1 的样例都被认为是正样本(比如零件合格)
# 而其他为负样本(比如零件不合格)和TensorFlow游乐场中的表示法不大一样的地方式
# 这里使用0表示负样本,1来表示正样本,大部分解决分类问题的神经网络都会采用0和1的表示方法
Y = [[int(x1 + x2 < 1)] for (x1, x2) in X] # 创建一个会话来运行TensorFlow
with tf.Session() as sess:
init_op = tf.global_variables_initializer()
# 初始化变量
sess.run(init_op)
print(sess.run(w1))
print(sess.run(w2))
'''
在训练之前神经网络参数的值
w1 = [[-0.8113182 1.4845988 0.06532937]
[-2.4427042 0.0992484 0.5912243 ]] w2 = [[-0.8113182 ]
[ 1.4845988 ]
[ 0.06532937]]
'''
# 设定训练的轮数
STEPS = 5000
for i in range(STEPS):
# 每次选取batch_size 个样本进行训练
start = (i * batch_size) % dataset_size
end = min(start + batch_size, dataset_size) # 通过选取的样本训练神经网络并更新参数
sess.run(train_step,
feed_dict={x: X[start:end], y_: Y[start: end]})
if i % 1000 == 0:
# 每隔一段时间计算在所有数据上的交叉熵并输出
total_cross_entropy = sess.run(cross_entropy,
feed_dict={x: X, y_: Y})
print("After %d training step(s), cross_entropy on all data is %g" % (i, total_cross_entropy))
'''
输出结果:
After 0 training step(s), cross_entropy on all data is 0.0674925
After 1000 training step(s), cross_entropy on all data is 0.0163385
After 2000 training step(s), cross_entropy on all data is 0.00907547
After 3000 training step(s), cross_entropy on all data is 0.00714436
After 4000 training step(s), cross_entropy on all data is 0.00578471 通过这个结果可以发现随着训练的进行,交叉熵是逐渐变小的
交叉熵越小说明预测的结果和真实的结果差距越小
'''
print(sess.run(w1))
print(sess.run(w2))
'''
在训练之后神经网络参数的值
w1 = [[-1.9618275 2.582354 1.6820377]
[-3.4681718 1.0698231 2.11789 ]]
w2 = [[-1.824715 ]
[ 2.6854665]
[ 1.418195 ]] 从和开始的神经网络参数值对比,我们发现这两个参数的取值是已经发生变化
这个变化就是训练的结果,它使得这个神经网络能更好的拟合提供的训练数据集
'''

  上面的程序实现了训练神经网络的全部过从,从这段程序中可以总结出训练神经网络的过程分为以下三个步骤:

  • 1,定义神经网络的结构和前向传播的输出结果
  • 2,定义损失函数以及选择反向传播优化的算法
  • 3,生成会话(tf.Session)并且在训练数据上反复进行反向传播优化算法

无论神经网络的结构如何变化,这三个步骤是不变的。

tf.Variable()  & tf.get_variable()

  tf.Variable() 和 tf.get_variable() 都可以用来创建变量,但是前者会自动保证唯一性,而后者不能保证唯一性。

  我们可以对比两个函数:

# 新建一个变量,变量值是 initial_value
Variable(initial_value=None, trainable=True,
collections=None, validate_shape=True,
caching_device=None,name=None,
expected_shape=None, import_scope=None,
constraint=None) # 获取具有这些参数的现有变量或者创建一个新变量。(可以创建共享变量)
# 如果该name的变量还未定义,则新创建一个,如果依据定义了,则直接获取该变量
get_variable(name, shape=None, dtype=None,
initializer=None, regularizer=None,
trainable=True, collections=None,
caching_device=None, partitioner=None,
validate_shape=True, use_resouce=None,
constraint=None)

  下面举个例子来说明二者的不同之处:

#_*_coding:utf-8_*_
'''
下面例子来说明 tf.Variable() 和 tf.get_variable() 的不同之处
''' import tensorflow as tf with tf.variable_scope('scope1'):
w1 = tf.Variable(1, name='w1')
w2 = tf.get_variable(name='w2', initializer=2.) with tf.variable_scope('scope1', reuse=True):
w1_p = tf.Variable(1, name='w1')
w2_p = tf.get_variable(name='w2', initializer=3.) print('w1', w1)
print('w1_p', w1_p)
# w1 <tf.Variable 'scope1/w1:0' shape=() dtype=int32_ref>
# w1_p <tf.Variable 'scope1_1/w1:0' shape=() dtype=int32_ref> print('w2', w2)
print('w2_p', w2_p)
# w2 <tf.Variable 'scope1/w2:0' shape=() dtype=float32_ref>
# w2_p <tf.Variable 'scope1/w2:0' shape=() dtype=float32_ref> print(w1 is w1_p, w2 is w2_p)
# False True

  我们可以看出, tf.Variable()会自动处理冲突问题,如上面代码所示。而tf.get_variable()会判断是否已经存在该name的变量,如果有,且该变量空间的reuse=True,那么就可以直接共享之前的值,如果没有,则重新创建。(注意:如果没有将reuse设置为True,则会提示冲突发生)。错误如下:

ValueError: Variable scope1/w2 already exists, disallowed. Did you
mean to set reuse=True in VarScope? Originally defined at:

  因为代码的最后一句语句是是判断上述变量是否相等,可以看出,通过get_variable()定义的变量是完全等价的,即使后一句 get_variable 是将 initializer 设为3,但是由于 name='w2' 的变量已经存在,并且 reuse=True,则直接引用之前定义的,这样就可以用 get_variable() 来定义共享变量。

  在生成上下文管理器时,若设置reuse=True,tf.variable_scope将只能获取已经创建过的变量,如果空间中没有变量则会报错。如果reuse=False 或者 reuse=None,tf.get_variable将创建新的变量。而且同名变量已经存在,会报错。

tf.get_variable  & tf.variable_scope

  tf.get_variable 函数可以用来创建或者获取变量,当创建变量时,与 tf.Variable是一样的。

  tf.variable_scope 函数生成一个上下文管理器,用于控制 tf.get_variable。

  这里,我们会发现, tf.get_variable() 在使用时,一般会和 tf.varibale_scope() 配套使用,需要指定它的作用域空间,这样在引用的使用的使用就可以通过设置指定的scope的 reuse=True进行引用。

#_*_coding:utf-8_*_
'''
变量生成之 tf.get_variable 与 tf.variable_scope reuse参数
'''
import tensorflow as tf with tf.variable_scope('a'):
v1 = tf.get_variable("v", [1], initializer=tf.constant_initializer(1.0)) # with tf.variable_scope("a"):
# 报错 ValueError: Variable a/v already exists,
# v2 = tf.get_variable("v", [1]) with tf.variable_scope("a", reuse=True):
v3 = tf.get_variable("v", [1])
print(v3 == v1) # True with tf.variable_scope("b", reuse=True):
# 报错 ValueError: Variable b/v does not exist, or was not created with tf.get_variable().
v4 = tf.get_variable("v",[1])

变量作用域:tf.variable_scope  & tf.name_scope

  在TensorFlow中有两个作用域(scope),分别是 tf.variable_scope()  和 tf.name_scope()。 其中name_scope() 是给 op_name加前缀,指定op的作用域空间 ,op是指操作。而variable_scope() 是给get_variable() 创建的变量的名字加前缀,表明作用域空间,也可以用于处理命名冲突。

variable_scope 示例

  variable_scope 变量作用域机制在TensorFlow中主要由两部分组成:

  当 tf.get_variable_scope().reuse == False时,varibale_scope 作用域只能用来创建新变量:

  上面程序会抛出 ValueError 错误,因为  v  这个变量已经被定义过了,但 tf.get_variable_scope().reuse默认为FALSE,所以不能重复使用。

  当 tf.get_variable_scope().reuse ==True 时,作用域可以共享变量:

  1,获取变量作用域

  可以直接通过 tf.varibale_scope()  来获取变量作用域:

  如果在开启的一个变量作用域里使用预先定义的一个作用域,则会跳过当前变量的作用域,保持预先存在的作用域不变。

  2,变量作用域的初始化

  变量作用域可以默认携带一个初始化器,在这个作用域中的子作用域或变量都可以继承或者重写父作用域初始化器中的值,方法如下:

  上面讲的是 variable_name,那么对于 op_name呢?在varibale_scope 作用域下的操作,也会被加上前缀:

  varibale_scope 主要用在循环神经网络(RNN)的操作中,其中需要大量的共享变量。

name_scope 示例

  TensorFlow中常常有数以千计的节点,在可视化的过程中很难一下展示出来,因此用 name_scope为变量划分范围,在可视化中,这表示在计算图中的一个层级。name_scope会影响 op_name,不会影响用 get_varibale()创建的变量,而会影响通过Varibale() 创建的变量,因此:

  可以看出,tf.name_scope() 返回的是一个字符串,如上述的“bar”。name_scope对用 get_varibale()创建的变量的名字不会有任何影响,而Varibale() 创建的操作会被加上前缀,并且会给操作机上名字前缀。

tf.cast() 数据类型转换

  tf.cast() 函数的作用是执行 tensorflow中张量数据类型转换,比如读入的图片如果是 int8 类型的,一般在训练前把图像的数据格式转换为float32。

  cast()定义:

cast(x, dtype, name=None)

  第一个参数 x:待转换的数据(张量)

  第二个参数dtype:目标数据类型

  第三个参数name:可选参数,定义操作的名称

int32转换为float32 代码:

#_*_coding:utf-8_*_
import tensorflow as tf t1 = tf.Variable([1, 2, 3, 4, 5])
t2 = tf.cast(t1, dtype=tf.float32) print('t1: {}'.format(t1))
print('t2:{}'.format(t2)) with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
sess.run(t2)
print(t2.eval())
print(sess.run(t2))

  输出如下:

t1: <tf.Variable 'Variable:0' shape=(5,) dtype=int32_ref>
t2:Tensor("Cast:0", shape=(5,), dtype=float32)
[1. 2. 3. 4. 5.]
[1. 2. 3. 4. 5.]

tf.argmax的使用

  tf.argmax(vector, 1):返回的是vector中的最大值的索引号,如果vector是一个向量,那就返回一个值,如果是一个矩阵,那就返回一个向量,这个向量的每一个维度都是相对应矩阵行的最大值元素的索引号。

import tensorflow as tf
import numpy as np A = [[1,3,4,5,6]]
B = [[1,3,4], [2,4,1]] with tf.Session() as sess:
print(sess.run(tf.argmax(A, 1)))
print(sess.run(tf.argmax(B, 1))) --------------------- 输出:
[4]
[2 1]

  

参考文献:

https://blog.csdn.net/uestc_c2_403/article/details/72232807

  此文是自己的学习笔记总结,学习于《TensorFlow深度学习框架》,俗话说,好记性不如烂笔头,写写总是好的,所以若侵权,请联系我,谢谢。

tensorflow学习笔记——常见概念的整理的更多相关文章

  1. TensorFlow学习笔记——深层神经网络的整理

    维基百科对深度学习的精确定义为“一类通过多层非线性变换对高复杂性数据建模算法的合集”.因为深层神经网络是实现“多层非线性变换”最常用的一种方法,所以在实际中可以认为深度学习就是深度神经网络的代名词.从 ...

  2. tensorflow学习笔记——自编码器及多层感知器

    1,自编码器简介 传统机器学习任务很大程度上依赖于好的特征工程,比如对数值型,日期时间型,种类型等特征的提取.特征工程往往是非常耗时耗力的,在图像,语音和视频中提取到有效的特征就更难了,工程师必须在这 ...

  3. Tensorflow学习笔记No.4.1

    使用CNN卷积神经网络(1) 简单介绍CNN卷积神经网络的概念和原理. 已经了解的小伙伴可以跳转到Tensorflow学习笔记No.4.2学习如和用Tensorflow实现简单的卷积神经网络. 1.C ...

  4. tensorflow学习笔记——使用TensorFlow操作MNIST数据(2)

    tensorflow学习笔记——使用TensorFlow操作MNIST数据(1) 一:神经网络知识点整理 1.1,多层:使用多层权重,例如多层全连接方式 以下定义了三个隐藏层的全连接方式的神经网络样例 ...

  5. tensorflow学习笔记——使用TensorFlow操作MNIST数据(1)

    续集请点击我:tensorflow学习笔记——使用TensorFlow操作MNIST数据(2) 本节开始学习使用tensorflow教程,当然从最简单的MNIST开始.这怎么说呢,就好比编程入门有He ...

  6. TensorFlow学习笔记10-卷积网络

    卷积网络 卷积神经网络(Convolutional Neural Network,CNN)专门处理具有类似网格结构的数据的神经网络.如: 时间序列数据(在时间轴上有规律地采样形成的一维网格): 图像数 ...

  7. TensorFlow学习笔记5-概率与信息论

    TensorFlow学习笔记5-概率与信息论 本笔记内容为"概率与信息论的基础知识".内容主要参考<Deep Learning>中文版. \(X\)表示训练集的设计矩阵 ...

  8. TensorFlow学习笔记6-数值计算基础

    TensorFlow学习笔记6-数值计算 本笔记内容为"数值计算的基础知识".内容主要参考<Deep Learning>中文版. \(X\)表示训练集的矩阵,其大小为m ...

  9. TensorFlow学习笔记4-线性代数基础

    TensorFlow学习笔记4-线性代数基础 本笔记内容为"AI深度学习".内容主要参考<Deep Learning>中文版. \(X\)表示训练集的设计矩阵,其大小为 ...

随机推荐

  1. linux 下 设置 MySQL8 表名大小写不敏感方法,解决设置后无法启动 MySQL 服务的问题

    在安装完成之后,初始化数据库之前,修改 my.cnf 打开mysql配置文件 vim /etc/my.cnf 在尾部追加一行 lower_case_table_names=1 并保存,然后再初始化数据 ...

  2. 【设计模式】行为型03观察者模式(Observer Pattern)

    记得16年初第一次学习了23种设计模式,但是除了少数几个简单的外,其他的很多都是学了个似懂非懂,以至于有人问起甚至说不上来,现在想想,其实就是没看懂而已.例如观察者模式,其实原理很简单,但是当时并没有 ...

  3. Requests方法 -- 重定向操作

    一.重定向1. (Redirect)就是通过各种方法将各种网络请求重新定个方向转到其它位置,从地址A跳转到地址 B 了.2.重定向状态码:--301 redirect: 301 代表永久性转移(Per ...

  4. .NET中生成动态验证码

    .NET中生成动态验证码 验证码是图片上写上几个字,然后对这几个字做特殊处理,如扭曲.旋转.修改文字位置,然后加入一些线条,或加入一些特殊效果,使这些在人类能正常识别的同时,机器却很难识别出来,以达到 ...

  5. WeUI Picker组件 源代码分析

    前言 由于最近做的一个移动端项目需要使用到类似 WeUI Picker组件 的选择效果,  所以在这里来分析下 WeUI Picker 的实现逻辑.(weui.js项目地址) 之前也做过类似的组件, ...

  6. 2019 Java 全栈工程师进阶路线图,一定要收藏

    技术更新日新月异,对于初入职场的同学来说,经常会困惑该往那个方向发展,这一点松哥是深有体会的. 我刚开始学习 Java 那会,最大的问题就是不知道该学什么,以及学习的顺序,我相信这也是很多初学者经常面 ...

  7. JAVA包装类解析和面试陷阱分析

    包装类 什么是包装类 虽然 Java 语言是典型的面向对象编程语言,但其中的八种基本数据类型并不支持面向对象编程,基本类型的数据不具备“对象”的特性——不携带属性.没有方法可调用. 沿用它们只是为了迎 ...

  8. Bzoj 2013 [Ceoi2010] A huge tower 题解

    2013: [Ceoi2010]A huge tower Time Limit: 10 Sec  Memory Limit: 259 MBSubmit: 471  Solved: 321[Submit ...

  9. C语言字符型数据的ASCII码值为何是负数?

    有如下一段C语言程序: #include "stdio.h" int main(void) { char a = 0xC8; printf ("字符a的ASCII码值的1 ...

  10. 利用Docker搭建Redis集群

    Redis集群搭建 运行Redis镜像 分别使用以下命令启动3个Redis docker run --name redis-6379 -p 6379:6379 -d hub.c.163.com/lib ...