精华内容
下载资源
问答
  • TensorFlow入门

    千次阅读 多人点赞 2019-04-23 10:09:29
    在实现上, TensorFlow 将图形定义转换成分布式执行的操作, 以充分利用可用的计算资源(如 CPU或 GPU). 一般你不需要显式指定使用 CPU 还是 GPU, TensorFlow 能自动检测. 如果检测到 GPU, TensorFlow 会尽可能地利用...

    TensorFlow入门

    参考资料:

    说明:以下代码示例基于Python3.7和TensorFlow1.13.1

    简介

    TensorFlow 是一个采用数据流图(data flow graphs),用于数值计算的开源软件库。节点(Nodes)在图中表示数学操作,图中的线(edges)则表示在节点间相互联系的多维数据数组,即张量(tensor)。它灵活的架构让你可以在多种平台上展开计算,例如台式计算机中的一个或多个CPU(或GPU),服务器,移动设备等等。

    什么是数据流图(Data Flow Graph)?

    数据流图用“结点”(nodes)和“线”(edges)的有向图来描述数学计算。“节点”一般用来表示施加的数学操作,但也可以表示数据输入(feed in)的起点/输出(push out)的终点,或者是读取/写入持久变量(persistent variable)的终点。“线”表示“节点”之间的输入/输出关系。这些数据“线”可以输运“size可动态调整”的多维数据数组,即“张量”(tensor)。张量从图中流过的直观图像是这个工具取名为“Tensorflow”的原因。一旦输入端的所有张量准备好,节点将被分配到各种计算设备完成异步并行地执行运算。

    为什么Tensorflow要使用图模型?图模型有什么优势呢?

    首先,图模型的最大好处是节约系统开销,提高资源的利用率,可以更加高效的进行运算。因为我们在图的执行阶段,只需要运行我们需要的op,这样就大大的提高了资源的利用率;其次,这种结构有利于我们提取中间某些节点的结果,方便以后利用中间的节点去进行其它运算;还有就是这种结构对分布式运算更加友好,运算的过程可以分配给多个CPU或是GPU同时进行,提高运算效率;最后,因为图模型把运算分解成了很多个子环节,所以这种结构也让我们的求导变得更加方便。

    在Anaconda中查找tensorflow,勾选安装即可安装TensorFlow成功。

    基本用法

    使用 TensorFlow, 你必须明白 TensorFlow:

    • 使用图 (graph) 来表示计算任务.
    • 在被称之为 会话 (Session) 的上下文 (context) 中执行图.
    • 使用 tensor 表示数据.
    • 通过 变量 (Variable) 维护状态.
    • 使用 feed 和 fetch 可以为任意的操作(arbitrary operation)赋值或者从其中获取数据.

    综述

    TensorFlow 是一个编程系统, 使用图来表示计算任务. 图中的节点被称之为 op (operation 的缩写). 一个 op 获得 0 个或多个 Tensor, 执行计算, 产生 0 个或多个 Tensor. 每个 Tensor 是一个类型化的多维数组. 例如,你可以将一小组图像集表示为一个四维浮点数数组, 这四个维度分别是 [batch, height, width, channels].

    一个 TensorFlow 图_描述_了计算的过程. 为了进行计算, 图必须在 会话 里被启动.
    会话 将图的 op 分发到诸如 CPU 或 GPU 之类的 设备 上, 同时提供执行 op 的方法.
    这些方法执行后, 将产生的 tensor 返回. 在 Python 语言中, 返回的 tensor 是
    numpy ndarray 对象.

    CPU和GPU之间的区别是什么?

    CPU和GPU是嵌入式和电子系统的基本设备,但它们都可以用于不同的目的。CPU是用于根据操作(例如算术,逻辑,控制和输入 - 输出)执行程序给出的指令的微处理器。相反,GPU最初被设计为在计算机游戏中渲染图像。CPU强调低延迟,而在GPU中,重要性是高吞吐量。

    比较的项目CPUGPU
    代表中央处理器图形处理单元
    专注于低延迟高吞吐量
    擅长处理串行指令处理并行指令
    包含更少的强大核心很多较弱的核心
    特征无序和推测执行的控制逻辑。架构可以容忍内存延迟
    速度有效可以高于CPU的
    内存消耗

    CPU的定义

    **CPU(中央处理器)**是一种主要充当每个嵌入式系统的大脑的设备。它由用于临时存储数据和执行计算的ALU(算术逻辑单元)和执行指令排序和分支的CU(控制单元)组成。它还与计算机的其他单元(例如存储器,输入和输出)交互,用于执行来自存储器的指令,这是接口也是CPU的关键部分的原因。I / O接口有时包含在控制单元中。

    它提供地址、数据和控制信号,同时接收在系统总线的帮助下处理的指令、数据、状态信号和中断。系统总线是一组各种总线,例如地址、控制和数据总线。与GPU不同,CPU为快速缓存分配更多硬件单元,而计算则分配的少。

    GPU的定义

    **GPU(图形处理单元)**是专门用于计算图形显示设计的处理器。它通常与CPU结合用于与CPU共享RAM,这对于大多数计算任务都是有益的。它是高端图形密集处理所必需的。独立GPU单元包含自己的RAM,称为VRAM,用于视频RAM。先进的GPU系统与多核CPU协同工作。起初,图形单元是由英特尔和IBM在20世纪80年代引入的。这些卡具有简单的功能,如区域填充,简单图像的更改,形状绘制等。

    现代图形能够执行研究和分析任务,由于其极端的并行处理,通常超过CPU。在GPU中,几个处理单元被剥离在一起,其中不存在高速缓存一致性。

    计算图

    TensorFlow 程序通常被组织成一个构建阶段和一个执行阶段. 在构建阶段, op 的执行步骤被描述成一个图. 在执行阶段, 使用会话执行图中的 op.

    例如, 通常在构建阶段创建一个图来表示和训练神经网络, 然后在执行阶段反复执行图中的训练 op.

    TensorFlow 支持 C, C++, Python 编程语言. 目前, TensorFlow 的 Python库更加易用, 它提供了大量的辅助函数来简化构建图的工作, 这些函数尚未被 C 和 C++ 库支持.

    三种语言的会话库 (session libraries) 是一致的.

    构建图

    构建图的第一步, 是创建源 op (source op). 源 op 不需要任何输入, 例如 常量 (Constant). 源 op 的输出被传递给其它 op 做运算.

    Python 库中, op 构造器的返回值代表被构造出的 op 的输出, 这些返回值可以传递给其它 op 构造器作为输入.

    TensorFlow Python 库有一个_默认图 (default graph)_, op 构造器可以为其增加节点. 这个默认图对许多程序来说已经足够用了.

    import tensorflow as tf
    
    # 创建一个常量 op, 产生一个 1x2 矩阵. 这个 op 被作为一个节点
    # 加到默认图中.
    #
    # 构造器的返回值代表该常量 op 的返回值.
    matrix1 = tf.constant([[3., 3.]])
    
    # 创建另外一个常量 op, 产生一个 2x1 矩阵.
    matrix2 = tf.constant([[2.],[2.]])
    
    # 创建一个矩阵乘法 matmul op , 把 'matrix1' 和 'matrix2' 作为输入.
    # 返回值 'product' 代表矩阵乘法的结果.
    product = tf.matmul(matrix1, matrix2)
    

    默认图现在有三个节点, 两个 constant() op, 和一个matmul() op. 为了真正进行矩阵相乘运算, 并得到矩阵乘法的结果, 你必须在会话里启动这个图.

    在一个会话中启动图

    构造阶段完成后, 才能启动图. 启动图的第一步是创建一个 Session 对象, 如果无任何创建参数, 会话构造器将启动默认图.

    # 启动默认图.
    sess = tf.Session()
    
    # 调用 sess 的 'run()' 方法来执行矩阵乘法 op, 传入 'product' 作为该方法的参数. 
    # 上面提到, 'product' 代表了矩阵乘法 op 的输出, 传入它是向方法表明, 我们希望取回
    # 矩阵乘法 op 的输出.
    #
    # 整个执行过程是自动化的, 会话负责传递 op 所需的全部输入. op 通常是并发执行的.
    # 
    # 函数调用 'run(product)' 触发了图中三个 op (两个常量 op 和一个矩阵乘法 op) 的执行.
    #
    # 返回值 'result' 是一个 numpy `ndarray` 对象.
    result = sess.run(product)
    print(result)
    # ==> [[ 12.]]
    
    # 任务完成, 关闭会话.
    sess.close()
    

    Session 对象在使用完后需要关闭以释放资源. 除了显式调用 close 外, 也可以使用 “with” 代码块来自动完成关闭动作.

    with tf.Session() as sess:
      result = sess.run([product])
      print result
    

    在实现上, TensorFlow 将图形定义转换成分布式执行的操作, 以充分利用可用的计算资源(如 CPU或 GPU). 一般你不需要显式指定使用 CPU 还是 GPU, TensorFlow 能自动检测. 如果检测到 GPU, TensorFlow 会尽可能地利用找到的第一个 GPU 来执行操作.如果你的系统里有多个 GPU, 那么 ID 最小的 GPU 会默认使用。

    指定设备

    如果你想要手动指派设备, 你可以用 with tf.device 创建一个设备环境, 这个环境下的 operation 都统一运行在环境指定的设备上.

    # 新建一个graph.
    with tf.device('/cpu:0'):
      a = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], shape=[2, 3], name='a')
      b = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], shape=[3, 2], name='b')
    c = tf.matmul(a, b)
    # 新建session with log_device_placement并设置为True.
    sess = tf.Session(config=tf.ConfigProto(allow_soft_placement=True, log_device_placement=True))
    # 运行这个op.
    print sess.run(c)
    

    如果你指定的设备不存在, 你会收到 InvalidArgumentError 错误提示。为了避免出现你指定的设备不存在这种情况, 你可以在创建的 session 里把参数 allow_soft_placement 设置为 True, 这样 tensorFlow 会自动选择一个存在并且支持的设备来运行 operation.

    Tensor

    TensorFlow 程序使用 tensor 数据结构来代表所有的数据, 计算图中, 操作间传递的数据都是 tensor.

    你可以把 TensorFlow tensor 看作是一个 n 维的数组或列表. 其中零维张量表示的是一个标量,也就是一个数;一维张量表示的是一个向量,也可以看作是一个一维数组;二维张量表示的是一个矩阵;同理,N维张量也就是N维矩阵。

    # 导入tensorflow模块
    import tensorflow as tf
    
    a = tf.constant([[2.0, 3.0]], name = "a")
    b = tf.constant([[1.0], [4.0]], name = "b")
    result = tf.matmul(a, b, name = "mul")
    print(result)
    
    # 输出
    # Tensor("mul_3:0", shape=(1, 1), dtype=float32)
    

    上述程序的输出结果表明:构建图的运算过程输出的结果是一个Tensor,且其主要由三个属性构成:Name、Shape和Type。Name代表的是张量的名字,也是张量的唯一标识符,我们可以在每个op上添加name属性来对节点进行命名,Name的值表示的是该张量来自于第几个输出结果(编号从0开始),上例中的“mul_3:0”说明是第一个结果的输出。Shape代表的是张量的维度,上例中shape的输出结果(1,1)说明该张量result是一个二维数组,且每个维度数组的长度是1。最后一个属性表示的是张量的类型,每个张量都会有唯一的类型,常见的张量类型如下图所示。

    常用的张量类型

    我们需要注意的是要保证参与运算的张量类型相一致,否则会出现类型不匹配的错误。如下面程序所示,当参与运算的张量类型不同时,Tensorflow会报类型不匹配的错误:

    import tensorflow as tf
    m1 = tf.constant([5, 1])
    m2 = tf.constant([2.0, 4.0])
    result = tf.add(m1, m2)
    
    TypeError: Input 'y' of 'Add' Op has type float32 that does not match type int32 of argument 'x'.
    

    正如程序的报错所示:m1是int32的数据类型,而m2是float32的数据类型,两者的数据类型不匹配,所以发生了错误。所以我们在实际编程时,一定注意参与运算的张量数据类型要相同。

    变量

    变量Variables维护图执行过程中的状态信息.

    下面的例子演示了如何使用变量实现一个简单的计数器.

    # 创建一个变量, 初始化为标量 0.
    state = tf.Variable(0, name="counter")
    
    # 创建一个 op, 其作用是使 state 增加 1
    one = tf.constant(1)
    new_value = tf.add(state, one)
    update = tf.assign(state, new_value)
    
    # 启动图后, 变量必须先经过`初始化` (init) op 初始化,
    # 首先必须增加一个`初始化` op 到图中.
    init_op = tf.global_variables_initializer()
    
    # 启动图, 运行 op
    with tf.Session() as sess:
      # 运行 'init' op
      sess.run(init_op)
      # 打印 'state' 的初始值
      print(sess.run(state))
      # 运行 op, 更新 'state', 并打印 'state'
      for _ in range(3):
        sess.run(update)
        print(sess.run(state))
    
    # 输出:
    
    # 0
    # 1
    # 2
    # 3
    

    代码中 assign() 操作是图所描绘的表达式的一部分, 正如 add() 操作一样. 所以在调用 run() 执行表达式之前, 它并不会真正执行赋值操作.

    通常会将一个统计模型中的参数表示为一组变量. 例如, 你可以将一个神经网络的权重作为某个变量存储在一个 tensor 中. 在训练过程中, 通过重复运行训练图, 更新这个 tensor.

    变量的初始化

    当我们完成了变量的创建,接下来,我们要对变量进行初始化。变量在使用前一定要进行初始化,且变量的初始化必须在模型的其它操作运行之前完成。通常,变量的初始化有三种方式,如下所示:

    # 创建两个变量, 初始化为标量 0.
    W = tf.Variable(0, name="W")
    b = tf.Variable(0, name="b")
    
    # 初始化全部变量
    init = tf.global_variables_initializer()
    with tf.Session() as sess:
      sess.run(init)
    
    # 初始化变量的子集
    init_subset = tf.variables_initializer([W, b], name = "init_subset")
    with tf.Session() as sess:
      sess.run(init_subset)
    
    # 初始化单个变量
    init_var = tf.Variable(tf.zeros([2,5]))
    with tf.Session() as sess:
      sess.run(init_var.initializer)
    

    上述程序说明了初始化变量的三种方式:初始化全部变量、初始化变量的子集以及初始化单个变量。首先,global_variables_initializer()方法是不管全局有多少个变量,全部进行初始化,是最简单也是最常用的一种方式;variables_initializer()是初始化变量的子集,相比于全部初始化化的方式更加节约内存;Variable()是初始化单个变量,函数的参数便是要初始化的变量内容。通过上述的三种方式,我们便可以实现变量的初始化,放心的使用变量了。

    但有时一个变量的初始化依赖于其他变量的初始化,为了确保初始化顺序不会错,可以使用initialized_value()来获取初始化变量的值。你应该使用tf.Variable.initialized_value()而不是变量本身来初始化另一个变量,其值取决于此变量的值。

    # Initialize 'v' with a random tensor.
    v = tf.Variable(tf.truncated_normal([10, 40]))
    # Use `initialized_value` to guarantee that `v` has been
    # initialized before its value is used to initialize `w`.
    # The random values are picked only once.
    w = tf.Variable(v.initialized_value() * 2.0)
    

    变量的保存和恢复

    我们经常在训练模型后,希望保存训练的结果,以便下次再使用或是方便日后查看,这时就用到了Tensorflow变量的保存。变量的保存是通过tf.train.Saver()方法创建一个Saver管理器,来保存计算图模型中的所有变量。具体代码如下:

    var1 = tf.Variable([1,3], name="v1")
    var2 = tf.Variable([2,4], name="v2")
    # 初始化全部变量
    init = tf.global_variables_initializer()
    # 调用Saver()存储器方法
    saver = tf.train.Saver()
    # 启动图
    with tf.Session() as sess:
      sess.run(init)
      # 设置存储路径
      save_path = saver.save(sess, "test/save.ckpt")  
    

    我们要注意,我们的存储文件save.ckpt是一个二进制文件,Saver存储器提供了向该二进制文件保存变量和恢复变量的方法。保存变量的方法就是程序中的save()方法,保存的内容是从变量名到tensor值的映射关系。完成该存储操作后,会在对应目录下生成如下图所示的文件:

    保存变量生成的相应文件

    Saver提供了一个内置的计数器自动为checkpoint文件编号。这就支持训练模型在任意步骤多次保存。此外,还可以通过global_step参数自行对保存文件进行编号,例如:global_step=2,则保存变量的文件夹为model.ckpt-2。

    那如何才能恢复变量呢?首先,我们要知道一定要用和保存变量相同的Saver对象来恢复变量。其次,不需要事先对变量进行初始化。具体代码如下所示:

    # 保存后模型恢复出来用于测试报错:NotFoundError: Key Variable_1 not found in checkpoint
    # 原因:如果模型训练完保存后直接加载,相当于变量在前后定义了两次,
    # 第一次创建的变量name="v1",加载时创建的变量虽然name="v1",
    # 但是实际上name会变成"v1_1"(v1_n-1),
    # 我们在保存的checkpoint中搜索的就是v1_n-1,因为搜索不到所以会报错,提示
    # Key v1_1 not found in checkpoint
    # 解决方法:
    # (1)保存模型后,restart kernel后,再加载测试,就不会出错。
    # (2)在加载过程中,定义 name 相同的变量前面加 tf.reset_default_graph() 
    # 清除默认图的堆栈,并设置全局图为默认图;
    
    # 清除默认图的堆栈
    tf.reset_default_graph() 
    
    var1 = tf.Variable([0,0], name="v1")
    var2 = tf.Variable([0,0], name="v2")
    # 调用Saver()存储器方法
    saver = tf.train.Saver()
    # 读取checkpoint文件
    module_file = tf.train.latest_checkpoint("test/")
    print(module_file)
    # 启动图
    with tf.Session() as sess:
      saver.restore(sess, module_file)
      # 打印变量的值
      # evel()方法用于在session中计算并返回变量的值, 不传参数的话,则使用的是默认的session  
      print(var1.eval())
      print(var2.eval())
    
    # 输出
    # test/save.ckpt
    # INFO:tensorflow:Restoring parameters from test/save.ckpt
    # [1 3]
    # [2 4]  
    

    本程序示例中,我们要注意:变量的获取是通过restore()方法,该方法有两个参数,分别是session和获取变量文件的位置。我们还可以通过latest_checkpoint()方法,获取到该目录下最近一次保存的模型。

    变量作用域

    在深度学习中,你可能需要用到大量的变量集,而且这些变量集可能在多处都要用到。例如,训练模型时,训练参数如权重(weights)、偏置(biases)等已经定下来,要拿到验证集去验证,我们自然希望这些参数是同一组。以往写简单的程序,可能使用全局限量就可以了,但在深度学习中,这显然是不行的,一方面不便管理,另外这样一来代码的封装性受到极大影响。因此,TensorFlow提供了一种变量管理方法:变量作用域机制,以此解决上面出现的问题。

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

    • tf.get_variable(<name>, <shape>, <initializer>):
      通过所给的名字创建或是返回一个变量.
    • tf.variable_scope(<scope_name>): 通过 tf.get_variable()为变量名指定命名空间.

    方法 tf.get_variable() 用来获取或创建一个变量,而不是直接调用tf.Variable.它采用的不是像tf.Variable这样直接获取值来初始化的方法.它的特殊之处在于,他还会搜索是否有同名的变量。一个初始化就是一个方法,创建其形状并且为这个形状提供一个张量.这里有一些在TensorFlow中使用的初始化变量:

    • tf.constant_initializer(value) 初始化一切所提供的值,
    • tf.random_uniform_initializer(a, b)从a到b均匀初始化,
    • tf.random_normal_initializer(mean, stddev) 用所给平均值和标准差初始化均匀分布.

    创建变量作用域用法如下:

    with tf.variable_scope("foo"):
        with tf.variable_scope("bar"):
            v = tf.get_variable("v", [1])
            assert v.name == "foo/bar/v:0"
    

    方法tf.variable_scope(scope_name),它会管理在名为scope_name的域(scope)下传递给tf.get_variable的所有变量名(组成了一个变量空间),根据规则确定这些变量是否进行复用。这个方法最重要的参数是reuse,有None,tf.AUTO_REUSE与True三个选项。具体用法如下:

    1. reuse的默认选项是None,此时会继承父scope的reuse标志。

    2. 自动复用(设置reuse为tf.AUTO_REUSE),如果变量存在则复用,不存在则创建。这是最安全的用法,在使用新推出的EagerMode时reuse将被强制为tf.AUTO_REUSE选项。用法如下:

      def foo():
        with tf.variable_scope("foo", reuse=tf.AUTO_REUSE):
          v = tf.get_variable("v", [1])
        return v
      
      v1 = foo()  # Creates v.
      v2 = foo()  # Gets the same, existing v.
      assert v1 == v2
      
    3. 复用(设置reuse=True):

      with tf.variable_scope("foo"):
        v = tf.get_variable("v", [1])
      with tf.variable_scope("foo", reuse=True):
        v1 = tf.get_variable("v", [1])
      assert v1 == v
      
    4. 捕获某一域并设置复用(scope.reuse_variables()):

      with tf.variable_scope("foo") as scope:
        v = tf.get_variable("v", [1])
        scope.reuse_variables()
        v1 = tf.get_variable("v", [1])
      assert v1 == v
      

      1)非复用的scope下再次定义已存在的变量;或2)定义了复用但无法找到已定义的变量,TensorFlow都会抛出错误,具体如下:

      with tf.variable_scope("foo"):
          v = tf.get_variable("v", [1])
          v1 = tf.get_variable("v", [1])
          #  Raises ValueError("... v already exists ...").
      
      with tf.variable_scope("foo", reuse=True):
          v = tf.get_variable("v", [1])
          #  Raises ValueError("... v does not exists ...").
      

    Fetch

    为了取回操作的输出内容, 可以在使用 Session 对象的 run() 调用 执行图时, 传入一些 tensor, 这些 tensor 会帮助你取回结果. 在之前的例子里, 我们只取回了单个节点 state, 但是你也可以取回多个tensor:

    input1 = tf.constant(3.0)
    input2 = tf.constant(2.0)
    input3 = tf.constant(5.0)
    intermed = tf.add(input2, input3)
    mul = tf.multiply(input1, intermed)
    
    with tf.Session() as sess:
      result = sess.run([mul, intermed])
      print(result)
    
    # 输出:
    # [21.0, 7.0]
    

    需要获取的多个 tensor 值,在 op 的一次运行中一起获得(而不是逐个去获取 tensor)。

    Feed

    上述示例在计算图中引入了 tensor, 以常量或变量的形式存储. TensorFlow 还提供了 feed 机制, 该机制可以临时替代图中的任意操作中的 tensor, 可以对图中任何操作提交补丁, 直接插入一个 tensor.

    feed 使用一个 tensor 值临时替换一个操作的输出结果. 你可以提供 feed 数据作为 run() 调用的参数.

    feed 只在调用它的方法内有效, 方法结束, feed 就会消失. 最常见的用例是将某些特殊的操作指定为 “feed” 操作, 标记的方法是使用 tf.placeholder() 为这些操作创建占位符.

    placeholder是一个数据初始化的容器,它与变量最大的不同在于placeholder定义的是一个模板,这样我们就可以session运行阶段,利用feed_dict的字典结构给placeholder填充具体的内容,而无需每次都提前定义好变量的值,大大提高了代码的利用率。

    input1 = tf.placeholder(tf.float32)
    input2 = tf.placeholder(tf.float32)
    output = tf.multiply(input1, input2)
    
    with tf.Session() as sess:
      print(sess.run([output], feed_dict={input1:[7.], input2:[2.]}))
    
    # 输出:
    # [array([ 14.], dtype=float32)]
    

    如果没有正确提供 feed, placeholder() 操作将会产生错误.

    TensorBoard

    1. Tensorboard简介

    对大部分人而言,深度神经网络就像一个黑盒子,其内部的组织、结构、以及其训练过程很难理清楚,这给深度神经网络原理的理解和工程化带来了很大的挑战。为了解决这个问题,tensorboard应运而生。Tensorboard是tensorflow内置的一个可视化工具,它通过将tensorflow程序输出的日志文件的信息可视化使得tensorflow程序的理解、调试和优化更加简单高效。Tensorboard的可视化依赖于tensorflow程序运行输出的日志文件,因而tensorboard和tensorflow程序在不同的进程中运行。

    那如何启动tensorboard呢?下面代码定义了一个简单的用于实现向量加法的计算图。

    import tensorflow as tf  
    # 定义一个计算图,实现两个向量的减法操作  
    # 定义两个输入,a为常量,b为变量  
    a=tf.constant([10.0, 20.0, 40.0], name='a')  
    b=tf.Variable(tf.random_uniform([3]), name='b')  
    output=tf.add_n([a,b], name='add')  
    # 生成一个具有写权限的日志文件操作对象,将当前命名空间的计算图写进日志中  
    writer=tf.summary.FileWriter('/path/to/logs', tf.get_default_graph())  
    writer.close()
    

    在上面程序的8、9行中,创建一个writer,将tensorboard summary写入文件夹/path/to/logs,然后运行上面的程序,在程序定义的日志文件夹/path/to/logs目录下,生成了一个新的日志文件events.out.tfevents.1524711020.bdi-172,如下图1所示。当然,这里的日志文件夹也可以由读者自行指定,但是要确保文件夹存在。如果使用的tensorboard版本比较低,那么直接运行上面的代码可能会报错,此时,可以尝试将第8行代码改为file_writer=tf.train.SummaryWriter(‘/path/to/logs’, sess.graph)

    图1 日志目录下生成的events文件路径

    接着运行如图2所示命令tensorboard --logdir /path/to/logs来启动服务。

    图2 linux下启动tensorboard服务的命令

    注意,当系统报错,找不到tensorboard命令时,则需要使用绝对路径调用tensorboard,例如下面的命令形式:

    python tensorflow/tensorboard/tensorboard.py --logdir=path/to/log-directory

    图3 tensorflow向量相加程序的计算图的可视化结果

    启动tensorboard服务后,在本地浏览器中输入http://188.88.88.88:6006,会看到如上图3所示的界面。注意,由于本节程序是在Linux服务器上运行的,所以需要输入该服务器完整的IP地址(http://188.88.88.88:6006指本实验所使用的服务器IP地址,实际操作时需要修改成实际使用的服务器IP),若tensorflow程序是在本机上运行,则需将上述IP地址http://188.88.88.88:6006替换成localhost。

    根据上述内容描述,tensorboard的启动过程可以概括为以下几步:

    1.创建writer,写日志文件
    writer=tf.summary.FileWriter('/path/to/logs', tf.get_default_graph())
    2.保存日志文件
    writer.close()
    3.运行可视化命令,启动服务
    tensorboard --logdir /path/to/logs

    4.打开可视化界面

    通过浏览器打开服务器访问端口http://xxx.xxx.xxx.xxx:6006

    注意:tensorboard兼容Google浏览器或Firefox浏览器,对其他浏览器的兼容性较差,可能会提示bug或出现其他性能上的问题。

    图4 tensorboard各栏目的默认界面

    在这里使用tensorboard1.13.1,较以往版本有很多不同。首先从界面上,此版本的tensorboard导航栏中只显示有内容的栏目,如GRAPHS,其他没有相关数据的子栏目都隐藏在INACTIVE栏目中,点击这些子栏目则会显示一条如图4所示的提示信息,指示使用者如何序列化相关数据。除此之外,在栏目的数量上也有增加,新增了DISTRIBUTIONS、PROJECTOR、TEXT、PR CURVES、PROFILE五个栏目。

    Tensorboard的可视化功能很丰富。SCALARS栏目展示各标量在训练过程中的变化趋势,如accuracy、cross entropy、learning_rate、网络各层的bias和weights等标量。如果输入数据中存在图片、视频,那么在IMAGES栏目和AUDIO栏目下可以看到对应格式的输入数据。在GRAPHS栏目中可以看到整个模型计算图结构。在HISTOGRAM栏目中可以看到各变量(如:activations、gradients,weights 等变量)随着训练轮数的数值分布,横轴上越靠前就是越新的轮数的结果。DISTRIBUTIONS和HISTOGRAM是两种不同形式的直方图,通过这些直方图可以看到数据整体的状况。PROJECTOR栏目中默认使用PCA分析方法,将高维数据投影到3D空间,从而显示数据之间的关系。

    2. Tensorflow数据流图

    从tensorboard中我们可以获取更多,远远不止图3所展示的。这一小节将从计算图结构和结点信息两方面详细介绍如何理解tensorboard中的计算图,以及从计算图中我们能获取哪些信息。

    2.1 Tensorflow的计算图结构

    如上图3展示的是一个简单的计算图,图结构中主要包含了以下几种元素:

    : Namespace,表示命名空间

    :OpNode,操作结点

    :Constant,常量

    :Dataflow edge,数据流向边,显示两个操作之间的tensor流程

    :Control dependency edge,控制依赖边

    :Reference edge,参考边

    除此之外,还有Unconnected series、Connected series、Summary等元素。

    断线节点序列
    :彼此之间不连接的有限个节点序列。这个结构上的简化法叫做序列折叠(series collapsing)。 序列基序(Sequential motifs)是拥有相同结构并且其名称结尾的数字不同的节点,它们被折叠进一个单独的节点块(stack)中。对长序列网络来说,序列折叠极大地简化了视图,对于已层叠的节点,双击会展开序列。

    相连节点序列
    :彼此之间相连的有限个节点序列

    摘要节点:摘要节点

    引用边:引用边,表示出度操作节点可以使入度tensor发生变化。

    这些元素构成的计算图能够让我们对输入数据的流向,各个操作之间的关系等有一个清晰的认识。

    图5 初始的计算图结构

    如上图5,是一个简单的两层全连接神经网络的计算图。仅仅从图5,我们很难快速了解该神经网络的主体数据流关系,因为太多的细节信息堆积在了一起。这还只是一个两层的简单神经网络,如果是多层的深度神经网络,其标量的声明,常量、变量的初始化都会产生新的计算结点,这么多的结点在一个页面上,那其对应的计算图的复杂性,排列的混乱性难以想象。所以我们需要对计算图进行整理,避免主要的计算节点淹没在大量的信息量较小的节点中,让我们能够更好的快速抓住主要信息。通过定义子命名空间,可以达到整理节点、让可视化效果更加清晰的目的。

    图6 整理后的计算图结构

    如上图6,就是通过定义子命名空间整理结点后的效果。该计算图只显示了最顶层的各命名空间之间的数据流关系,其细节信息被隐藏起来了,这样便于把握主要信息。

    图7为加入子命名空间后的部分代码截图。代码中,将输入数据都放在了input命名空间中,还使用了perdition、moving_averages、loss、train等命名空间去整理对应的操作过程。

    图7 用命名空间整理计算图的代码截图

    图8 手动将节点从主图中移除

    除此之外,我们还可以通过手动将不重要的节点从主图中移除来简化计算图,如上图8,右键点击想要移除的节点,会出现“Remove from main graph”按钮,点击该按钮,就可以移除对应节点了。

    2.2 结点的信息

    Tensorboard除了可以展示整体的计算图结构之外,还可以展示很多细节信息,如结点的基本信息、运行时间、运行时消耗的内存、各结点的运行设备(GPU或者CPU)等。

    2.2.1 基本信息

    前面的部分介绍了如何将计算图的细节信息隐藏起来,但是有的时候,我们需要查看部分重要命名空间下的节点信息,那这些细节信息如何查看呢?对于节点信息,双击图8中的任意一个命名空间,就会展开对应命名空间的细节图(再次双击就可以收起细节图)。

    图9 展开input命名空间节点信息图

    上图9是input命名空间的展开图,展开图中包含了两个操作节点(x_input和y_input)。除了了解具体包含的操作节点以及其他元素外,我们还可以获取粒度更小的信息。

    图10 input命名空间的放大的细节图

    图11 命名空间的节点信息

    图12 计算节点的基本信息

    上图10所示为图9中input命名空间展开图的放大图。观察图10,我们可以了解到输入数据x、y的维度,图中x的向量维度为784维,y为10维,?表示样本数量。本节演示中使用的是mnist数据集,mnist数据集是一个针对图片的10分类任务,输入向量维度是784,这说明可以通过计算图上这些信息,来校验输入数据是否正确。通过左键单击命名空间或者操作节点,屏幕的右上角会显示对应的具体信息。

    如上图11中,右上角绿色框标注的部分为命名空间layer2的具体信息。如上图12中,右上角绿色框标注的部分为节点x_input的具体信息。

    2.2.2 其他信息

    除了节点的基本信息之外,tensorboard还可以展示每个节点运行时消耗的时间、空间、运行的机器(GPU或者CPU)等信息。本小节将详细讲解如何使用tensorboard展示这些信息。这些信息有助于快速获取时间、空间复杂度较大的节点,从而指导后面的程序优化。

    将2.1节中图7所展示的代码的session部分改成如下所示的程序,就可以将程序运行过程中不同迭代轮数中tensorflow各节点消耗的时间和空间等信息写入日志文件中,然后通过读取日志文件将这些信息用tensorboard展示出来。

    #创建writer对象  
    writer=tf.summary.FileWriter("/path/to/metadata_logs",tf.get_default_graph())  
    with tf.Session() as sess:  
        tf.global_variables_initializer().run()  
        for i in range(TRAINING_STEPS):  
            x_batch, y_batch=mnist.train.next_batch(BATCH_SIZE)  
            if i%1000==0:  
                #这里通过trace_level参数配置运行时需要记录的信息,  
                # tf.RunOptions.FULL_TRACE代表所有的信息  
                run_options = tf.RunOptions(trace_level=tf.RunOptions.FULL_TRACE)  
                #运行时记录运行信息的proto,pb是用来序列化数据的  
                run_metadata = tf.RunMetadata()  
                #将配置信息和记录运行信息的proto传入运行的过程,从而记录运行时每一个节点的时间、空间开销信息  
                _, loss_value, step = sess.run([train_op, loss, global_step], feed_dict={x: x_batch, y_: y_batch}, options=run_options, run_metadata=run_metadata)  
                #将节点在运行时的信息写入日志文件  
                writer.add_run_metadata(run_metadata, 'step %03d' % i)  
            else:  
                _, loss_value, step = sess.run([train_op, loss, global_step], feed_dict={x: xs, y_: ys})  
        writer.close()
    

    运行上面的程序,生成日志文件存储在/path/to/metadata_logs/目录下,启动tensorboard服务,读取日志文件信息,将每一个节点在不同迭代轮数消耗的时间、空间等信息展示出来。

    图13 选择迭代轮数对应记录页面

    如上图13所示,在浏览器中打开可视化界面,进入GRAPHS子栏目,点击Session runs选框,会出现一个下拉菜单,这个菜单中展示了所有日志文件中记录的运行数据所对应的迭代轮数。任意选择一个迭代轮数,页面右边的区域会显示对应的运行数据。

    图14 第9000轮迭代时不同计算节点消耗时间的可视化效果图

    图15 第9000轮迭代时不同计算节点占有存储的可视化效果图

    如上图14所示,选择了第9000轮的运行数据,然后选择Color栏目下的Compute time选项,GRAPHS栏目下就会显示tensorflow程序每个计算节点的运行时间。图中使用颜色的深浅来表示运行时间的长短,颜色深浅对应的具体运行时间可以从页面左侧的颜色条看出。由图14可知,train命名空间运行时所消耗的时间最长,Variable命名空间所消耗的时间比较短,无色表示不消耗时间。

    如上图15展示了tensorflow各个节点所占用的空间大小。与衡量运行时所消耗的时间方法类似,使用颜色的深浅来标识所占用内存的大小。颜色条上的数字说明,占用的最大空间为677MB,最小空间为0B。train命名空间占用的存储空间最大。

    除了时间和空间指标,tensorboard还可以展示各节点的运行设备(GPU还是CPU)、XLA Cluster、TPU Compatibility等,这些全部都在Color栏目下作为选项供选择。这些指标都是将节点染色,通过不同颜色以及颜色深浅来标识结果的。如下图16,是TPU Compatibility展示图。

    图16 第9000轮迭代时不同计算节点的TPU Compatibility效果展示图

    3. Tensorflow监控指标可视化

    除了GRAPHS栏目外,tensorboard还有IMAGES、AUDIO、SCALARS、HISTOGRAMS、DISTRIBUTIONS、FROJECTOR、TEXT、PR CURVES、PROFILE九个栏目,本小节将详细介绍这些子栏目各自的特点和用法。

    3.1 IMAGES

    图像仪表盘,可以显示通过tf.summary.image()函数来保存的png图片文件。

    # 指定图片的数据源为输入数据x,展示的相对位置为[-1,28,28,1]
    image_shape=tf.reshape(x, [-1, 28, 28,1])  
    # 将input命名空间下的图片放到summary中,一次展示10张  
    tf.summary.image('input', image_shape, 10) 
    

    如上面代码,将输入数据中的png图片放到summary中,准备后面写入日志文件。运行程序,生成日志文件,然后在tensorboard的IMAGES栏目下就会出现如下图17所示的内容(实验用的是mnist数据集)。仪表盘设置为每行对应不同的标签,每列对应一个运行。图像仪表盘仅支持png图片格式,可以使用它将自定义生成的可视化图像(例如matplotlib散点图)嵌入到tensorboard中。该仪表盘始终显示每个标签的最新图像。

    图17 tensorboard中的IMAGES栏目内容展开界面

    3.2 AUDIO

    音频仪表盘,可嵌入音频的小部件,用于播放通过tf.summary.audio()函数保存的音频。

    tf.summary.audio('audio', audio, sampling_frequency)
    

    audio是一个三维或者二维tensor,含义是[音频数, 每个音频的帧数, 每个音频的通道数]或者[音频数, 每个音频的帧数]。

    sampling_frequency是音频的采样率。

    仪表盘设置为每行对应不同的标签,每列对应一个运行。该仪表盘始终嵌入每个标签的最新音频。

    3.3 SCALARS

    Tensorboard 的标量仪表盘,统计tensorflow中的标量(如:学习率、模型的总损失)随着迭代轮数的变化情况。如下图18所示,SCALARS栏目显示通过函数tf.summary.scalar()记录的数据的变化趋势。如下所示代码可添加到程序中,用于记录学习率的变化情况。

    # 在learning_rate附近添加,用于记录learning_rate  
    tf.summary.scalar('learning_rate', learning_rate)  
    

    Scalars栏目能进行的交互操作有:

    • 点击每个图表左下角的蓝色小图标将展开图表
    • 拖动图表上的矩形区域将放大
    • 双击图表将缩小
    • 鼠标悬停在图表上会产生十字线,数据值记录在左侧的运行选择器中。

    图18 tensorboard中的SCALARS栏目内容展开界面

    此外,读者可通过在仪表盘左侧的输入框中,编写正则表达式来创建新文件夹,从而组织标签。

    3.4 HISTOGRAMS

    Tensorboard的张量仪表盘,统计tensorflow中的张量随着迭代轮数的变化情况。它用于展示通过tf.summary.histogram记录的数据的变化趋势。如下代码所示:

    tf.summary.histogram(weights, 'weights')

    上述代码将神经网络中某一层的权重weight加入到日志文件中,运行程序生成日志后,启动tensorboard就可以在HISTOGRAMS栏目下看到对应的展开图像,如下图19所示。每个图表显示数据的时间“切片”,其中每个切片是给定步骤处张量的直方图。它依据的是最古老的时间步原理,当前最近的时间步在最前面。通过将直方图模式从“偏移”更改为“叠加”,如果是透视图就将其旋转,以便每个直方图切片都呈现为一条相互重叠的线。

    图19 tensorboard中的HISTOGRAMS栏目内容展开界面

    3.5 DISTRIBUTIONS

    Tensorboard的张量仪表盘,相较于HISTOGRAMS,用另一种直方图展示从tf.summary.histogram()函数记录的数据的规律。它显示了一些分发的高级统计信息。

    如下图20所示,图表上的每条线表示数据分布的百分位数,例如,底线显示最小值随时间的变化趋势,中间的线显示中值变化的方式。从上至下看时,各行具有以下含义:[最大值,93%,84%,69%,50%,31%,16%,7%,最小值]。这些百分位数也可以看作标准偏差的正态分布:[最大值,μ+1.5σ,μ+σ,μ+0.5σ,μ,μ-0.5σ,μ-σ,μ-1.5σ,最小值],使得从内侧读到外侧的着色区域分别具有宽度[σ,2σ,3σ]。

    图20 tensorboard中的DISTRIBUTIONS栏目内容展开界面

    3.6 PROJECTOR

    嵌入式投影仪表盘,全称Embedding Projector,是一个交互式的可视化工具,通过数据可视化来分析高维数据。例如,读者可在模型运行过程中,将高维向量输入,通过embedding projector投影到3D空间,即可查看该高维向量的形式,并执行相关的校验操作。Embedding projector的建立主要分为以下几个步骤:

    1)建立embedding tensor

    #1. 建立 embeddings  
    embedding_var = tf.Variable(batch_xs, name="mnist_embedding")  
    summary_writer = tf.summary.FileWriter(LOG_DIR) 
    

    2)建立embedding projector 并配置

    config = projector.ProjectorConfig()  
    embedding = config.embeddings.add()  
    embedding.tensor_name = embedding_var.name  
    embedding.metadata_path = path_for_mnist_metadata   #'metadata.tsv'  
    embedding.sprite.image_path = path_for_mnist_sprites  #'mnistdigits.png'  
    embedding.sprite.single_image_dim.extend([28,28])  
    projector.visualize_embeddings(summary_writer, config)
    

    3)将高维变量保存到日志目录下的checkpoint文件中

    sess = tf.InteractiveSession()  
    sess.run(tf.global_variables_initializer())  
    saver = tf.train.Saver()  
    saver.save(sess, os.path.join(LOG_DIR, "model.ckpt"), 1) 
    

    4)将metadata与embedding联系起来,将 vector 转换为 images,反转灰度,创建并保存 sprite image

    to_visualise = batch_xs  
    to_visualise = vector_to_matrix_mnist(to_visualise)  
    to_visualise = invert_grayscale(to_visualise)  
    sprite_image = create_sprite_image(to_visualise)  
    plt.imsave(path_for_mnist_sprites,sprite_image,cmap='gray') 
    

    5)运行程序,生成日志文件,启动服务,tensorboard中的PROJECTOR栏将展示投影后的数据的动态图,如下图21所示。

    图21 tensorboard中的PROJECTOR栏目内容展开界面

    Embedding Projector从模型运行过程中保存的checkpoint文件中读取数据,默认使用主成分分析法(PCA)将高维数据投影到3D空间中,也可以设置选择另外一种投影方法,T-SNE。除此之外,也可以使用其他元数据进行配置,如词汇文件或sprite图片。

    3.7 TEXT

    文本仪表盘,显示通过tf.summary.text()函数保存的文本片段,包括超链接、列表和表格在内的Markdown功能均支持。

    3.8 PR CURVES

    PR CURVES仪表盘显示的是随时间变化的PR曲线,其中precision为横坐标,recall为纵坐标。如下代码创建了一个用于记录PR曲线的summary。

    # labels为输入的y, predition为预测的y值  
    # num_thresholds为多分类的类别数量  
    tensorboard.summary.pr_curve(name='foo',  
                         predictions=predictions,  
                         labels=labels,  
                         num_thresholds=11)  
    

    图22 tensorboard中的PR CURVES栏目内容展开界面

    上图22为tensorboard上PR CURVES栏目在有内容时的首页,没有内容时就隐藏在INACTIVE栏目下。

    训练模型时,经常需要在查准率和查全率之间权衡,PR曲线能够帮助我们找到这个权衡点。每条曲线都对应一个二分类问题,所以,针对多分类问题,每一个类都会生成一条对应的PR曲线。

    3.9 PROFILE

    Tensorboard的配置文件仪表盘,该仪表盘上包含了一套TPU工具,可以帮助我们了解,调试,优化tensorflow程序,使其在TPU上更好的运行。

    但并不是所有人都可以使用该仪表盘,只有在Google Cloud TPU上有访问权限的人才能使用配置文件仪表盘上的工具。而且,该仪表盘与其他仪表盘一样,都需要在模型运行时捕获相关变量的跟踪信息,存入日志,方可用于展示。

    在PROFILE仪表盘的首页上,显示的是程序在TPU上运行的工作负载性能,它主要分为五个部分:Performance Summary、Step-time Graph、Top 10 Tensorflow operations executed on TPU、Run Environment和Recommendation for Next Step。如下图23所示:

    图23 tensorboard中的PROFILE栏目内容展开界面

    其中,Performance Summary包括以下四项:

    1)所有采样步骤的平均步长时间

    2)主机空闲时间百分比

    3)TPU空闲时间百分比

    4)TPU矩阵单元的利用率

    Run Environment(运行环境)包括以下五方面:

    1)使用的主机数量

    2)使用的TPU类型

    3)TPU内核的数量

    4)训练批次的大小(batch size)

    5)作业信息(构建命令和运行命令)

    4. 总结

    本节主要介绍了tensorflow中一个非常重要的工具——tensorboard。Tensorboard是一个可视化工具,它能够以直方图、折线图等形式展示程序运行过程中各标量、张量随迭代轮数的变化趋势,它也可以显示高维度的向量、文本、图片和音频等形式的输入数据,用于对输入数据的校验。Tensorflow函数与tensorboard栏目的对应关系如下表所示。

    Tensorboard栏目tensorflow日志生成函数内容
    GRAPHS默认保存显示tensorflow计算图
    SCALARStf.summary.scalar显示tensorflow中的张量随迭代轮数的变化趋势
    DISTRIBUTIONStf.summary.histogram显示tensorflow中张量的直方图
    HISTOGRAMStf.summary.histogram显示tensorflow中张量的直方图(以另一种方式)
    IMAGEStf.summary.image显示tensorflow中使用的图片
    AUDIOtf.summary.audio显示tensorflow中使用的音频
    TEXTtf.summary.text显示tensor flow中使用的文本
    PROJECTOR通过读取checkpoint文件可视化高维数据

    Tensorboard的可视化功能对于tensorflow程序的训练非常重要,使用tensorboard进行调参主要分为以下几步:

    1)校验输入数据

    如果输入数据的格式是图片、音频、文本的话,可以校验一下格式是否正确。如果是处理好的低维向量的话,就不需要通过tensorboard校验。

    2)查看graph结构

    查看各个节点之间的数据流关系是否正确,再查看各个节点所消耗的时间和空间,分析程序优化的瓶颈。

    3)查看各变量的变化趋势

    在SCALAR、HISTOGRAMS、DISTRIBUTIONS等栏目下查看accuracy、weights、biases等变量的变化趋势,分析模型的性能

    4)修改code

    根据3)和4)的分析结果,优化代码。

    5)选择最优模型

    6)用Embedding Projector进一步查看error出处

    Tensorboard虽然只是tensorflow的一个附加工具,但熟练掌握tensorboard的使用,对每一个需要对tensorflow程序调优的人都非常重要,它可以显著提高调参工作的效率,帮助我们更快速地找到最优模型。

    读取数据

    1. tensorflow 的数据读取机制

    以图像数据为例,数据读取过程如下所示:

    假设我们的硬盘中有一个图片数据集0001.jpg,0002.jpg,0003.jpg……我们只需要把它们读取到内存中,然后提供给GPU或是CPU进行计算就可以了。这听起来很容易,但事实远没有那么简单。事实上,我们必须要把数据先读入后才能进行计算,假设读入用时0.1s,计算用时0.9s,那么就意味着每过1s,GPU都会有0.1s无事可做,这就大大降低了运算的效率。

    如何解决这个问题?方法就是将读入数据和计算分别放在两个线程中,将数据读入内存的一个队列,如下图所示:

    读取线程源源不断地将文件系统中的图片读入到内存队列中,而负责计算的是另一个线程,计算需要数据时,直接从内存队列中取就可以了。这样就可以解决GPU因为IO而空闲的问题!

    而在tensorflow中,为了方便管理,在内存队列前又添加了一层所谓的**“文件名队列”**。

    为什么要添加这一层文件名队列?首先得了解机器学习中的一个概念:epoch(迭代)。对于一个数据集来讲,运行一个epoch就是将这个数据集中的图片全部计算一遍。如一个数据集中有三张图片A.jpg、B.jpg、C.jpg,那么跑一个epoch就是指对A、B、C三张图片都计算了一遍。两个epoch就是指先对A、B、C各计算一遍,然后再全部计算一遍,也就是说每张图片都计算了两遍。

    tensorflow使用文件名队列+内存队列双队列的形式读入文件,可以很好地管理epoch。下面用图片的形式来说明这个机制的运行方式。还是以数据集A.jpg, B.jpg, C.jpg为例,假定我们要跑一个epoch,那么就在文件名队列中把A、B、C各放入一次,并在之后标注队列结束,如下图。

    程序运行后,内存队列首先读入A(此时A从文件名队列中出队),然后再读取B和C。

    此时,如果再尝试读入,系统由于检测到了“结束”,就会自动抛出一个异常(OutOfRange)。外部捕捉到这个异常后就可以结束程序了。这就是tensorflow中读取数据的基本机制。如果我们要跑2个epoch而不是1个epoch,那只要在文件名队列中将A、B、C依次放入两次再标记结束就可以了。

    2. TensorFlow数据读取机制对应的函数

    如何在TensorFlow中创建这两个内存?

    • 创建文件名队列 - tf.train.string_input_producer 阻塞态 + tf.train.start_queue_runners 激活态

    把输入的数据按照要求排序成一个队列。最常见的是把一堆文件名整理成一个队列。如下操作:

    filenames = [os.path.join(data_dir,'data_batch%d.bin' % i ) for i in xrange(1,6)]
    filename_queue = tf.train.string_input_producer(filenames)
    

    tf.train.string_input_producer有两个重要的参数,一个是num_epochs,它就是上文中提到的epoch数。另一个是shuffle,shuffle是指在epoch内文件顺序是否被打乱。若设置shuffle=False,如下图,每个epoch内,数据还是按照A、B、C的顺序进入文件名队列,这个顺序不会改变。如果设置shuffle=True,那么在epoch内,数据的前后顺序就会被打乱,具体如下图所示。

    **其实,仅仅应用tf.train.string_input_producer构建的文件名队列是处于阻塞态的,并没有真正的将文件名读入到相应的文件名队列内存中,如下左图所示。为了完成在文件名队列内存中构建文件名队列(也就是我们说的读入数据),我们还需要tf.train.start_queue_runners进行启动,**如下右图所示

    我们通常也把tf.train.start_queue_runners叫做‘入栈线程启动器’,使用tf.train.start_queue_runners之后,才会真正启动填充队列的线程,这时系统就不再“阻塞”。此后计算单元就可以拿到数据并进行计算,整个程序也就跑起来了。

    • 创建数据内存序列

    在tensorflow中,数据内存队列不需要自己建立,我们只需要使用reader对象从文件名队列中读取数据就可以了。所以TensorFlow高效读取数据机制中,最重要的是完成文件名队列的设计。

    3. 为什么要使用TFRecords来进行文件的读写?

    在tensorflow中数据的传入方式主要包含以下几种:

    • 供给数据(feed): 在tensorflow程序运行的每一步, 让Python代码来供给数据。
    • 从文件读取数据: 在tensorflow graph的起始, 让一个输入pipeline从文件中读取数据。
    • 预加载数据: 在tensorflow graph中定义常量或变量来保存所有数据(仅适用于数据量比较小的情况)。

    当我们遇到数据集比较大的情况时,第一种和最后一种方法会极其占内存,效率很差。那么为什么使用TFRecords会比较快?在于其使用二进制存储文件,也就是将数据存储在一个内存块中,相比其它文件格式要快很多,特别是如果你使用hdd(Hard Disk Drive)而不是ssd(Solid State Disk),因为它涉及移动磁盘阅读器头并且需要相当长的时间。总体而言,通过使用二进制文件,可以更轻松地分发数据,使数据更好地对齐,以实现高效的读取。

    整个过程分两部分,一是使用tf.train.Example协议流将文件保存成TFRecords格式的.tfrecords文件,这里主要涉及到使用tf.python_io.TFRecordWriter(“train.tfrecords”)tf.train.Example以及tf.train.Features三个函数,第一个是生成需要对应格式的文件,后面两个函数主要是将我们要传入的数据按照一定的格式进行规范化。

    另一部分就是在训练模型时将我们生成的.tfrecords文件读入并传到模型中进行使用。这部分主要涉及到使用tf.TFRecordReader(“train.tfrecords”)tf.parse_single_example两个函数。第一个函数是将我们的二进制文件读入,第二个则是进行解析然后得到我们想要的数据。

    #### 生成train.tfrecords文件 ####
    import os
    import tensorflow as tf 
    from PIL import Image
     
    cwd = os.getcwd()
     
    ''' 数据目录
    -- img1.jpg
         img2.jpg
         img3.jpg
         ...
    -- img1.jpg
         img2.jpg
         ...
    -- ...
    '''
    writer = tf.python_io.TFRecordWriter("train.tfrecords") # 定义train.tfrecords文件
    for index, name in enumerate(classes): # 遍历每一个文件夹
        class_path = cwd + name + "/"      # 每一个文件夹的路径
        for img_name in os.listdir(class_path):  # 遍历每个文件夹中所有的图像
            img_path = class_path + img_name  # 每一张图像的路径
            img = Image.open(img_path)    # 打开图像
            img = img.resize((224, 224))  # 图像裁剪
            img_raw = img.tobytes()       # 将图像转化为bytes
     
            # 调用Example 和 Feature函数将数据格式化保存起来
            # 注意:Features 传入参数为一个字典,方便后续读取数据时的操作
            example = tf.train.Example(features=tf.train.Features(feature={
                "label": tf.train.Feature(int64_list=tf.train.Int64List(value=[index])),
                'img_raw': tf.train.Feature(bytes_list=tf.train.BytesList(value=[img_raw]))
            }))
            #序列化为字符串,并写入数据
            writer.write(example.SerializeToString())  
    writer.close()
    

    基本的,一个Example中包含Features,Features里包含Feature(这里没s)的字典。最后,Feature里包含有一个 FloatList,或者ByteList,或者Int64List

    就这样,我们把相关的信息都存到了一个文件中,不用单独的label文件,读取也很方便。

    # 从tfrecords文件中读取记录的迭代器
    for serialized_example in tf.python_io.tf_record_iterator("train.tfrecords"):
        example = tf.train.Example()
        example.ParseFromString(serialized_example)
     
        image = example.features.feature['image'].bytes_list.value
        label = example.features.feature['label'].int64_list.value
        # 可以做一些预处理之类的
        print(image, label)
    

    4. 使用队列读取tfrecords数据

    从TFRecords文件中读取数据, 首先需要用tf.train.string_input_producer生成一个解析队列。之后调用tf.TFRecordReader的tf.parse_single_example解析器。其原理如下图:

    解析器首先读取解析队列,返回serialized_example对象,之后调用tf.parse_single_example操作将Example协议缓冲区(protocol buffer)解析为张量。

    def read_and_decode(filename):
        # 根据文件名生成文件名队列
        filename_queue = tf.train.string_input_producer([filename])
        # 定义reader
        reader = tf.TFRecordReader()
        # 返回文件名和文件
        _, serialized_example = reader.read(filename_queue) 
        # 将协议缓冲区Protocol Buffer解析为张量tensor
        # 注意到:我们写文件就是采用了字典的方式进行存储的,所以解析的时候依然用字典进行数据提取
        features = tf.parse_single_example(serialized_example,
                                           features={
                                               'label': tf.FixedLenFeature([], tf.int64),
                                               'img_raw' : tf.FixedLenFeature([], tf.string),
                                           })
        # 将编码为字符串的变量重新变回来,因为写进tfrecord里用to_bytes的形式,也就是字符串
        img = tf.decode_raw(features['img_raw'], tf.uint8)
        # 检查张量形状是否对齐
        img = tf.reshape(img, [224, 224, 3])
        # 图像数据格式化为tf.float32
        img = tf.cast(img, tf.float32) * (1. / 255) - 0.5
        # 标签数据格式化为tf.int32
        label = tf.cast(features['label'], tf.int32)
     
        return img, label
    

    之后,在训练模型过程中,我们就会很方便用这些数据了,例如:

    
    # 解析tfrecords文件的数据
    img, label = read_and_decode("train.tfrecords")
     
    # 通过随机打乱张量的顺序创建batch
    # capacity = ( min_after_dequeue + (num_threads + aSmallSafetyMargin * batch_size) )
    img_batch, label_batch = tf.train.shuffle_batch(
                               [img, label],  # 入队的张量列表
                               batch_size=30, # 进行一次批处理的tensor数
                               capacity=2000, # 队列中最大的元素数
                               min_after_dequeue=1000,# 一次出列操作完成后,队列中元素的最小数量
                               num_threads=4  #使用多个线程在tensor_list中读取文件
                               )
    init = tf.global_variables_initializer()
     
    with tf.Session() as sess:
        sess.run(init)
        # 队列-入栈线程启动器
        threads = tf.train.start_queue_runners(sess=sess)
        for i in range(3):
            val, loss= sess.run([img_batch, label_batch])
    

    三个要点作为总结:

    • tensorflow里的graph能够记住状态,这使得TFRecordReader能够记住tfrecord的位置,并且始终能返回下一个。而这就要求我们在使用之前,必须初始化整个graph,这里使用了函数tf.global_variables_initializer()来进行初始化
    • tensorflow中的队列和普通的队列差不多,不过它里面的operation和tensor都是符号型的,在调用sess.run()时才执行
    • TFRecordReader会一直弹出队列中文件的名字,直到队列为空

    线程和队列

    在使用TensorFlow进行异步计算时,队列是一种强大的机制。

    正如TensorFlow中的其他组件一样,队列就是TensorFlow图中的节点。这是一种有状态的节点,就像变量一样:其他节点可以修改它的内容。具体来说,其他节点可以把新元素插入到队列后端(rear),也可以把队列前端(front)的元素删除。

    为了感受一下队列,让我们来看一个简单的例子。我们先创建一个“先入先出”的队列(FIFOQueue),并将其内部所有元素初始化为零。然后,我们构建一个TensorFlow图,它从队列前端取走一个元素,加上1之后,放回队列的后端。慢慢地,队列的元素的值就会增加。

    EnqueueEnqueueManyDequeue都是特殊的节点。他们需要获取队列指针,而非普通的值,如此才能修改队列内容。我们建议您将它们看作队列的方法。事实上,在Python API中,它们就是队列对象的方法(例如q.enqueue(...))。

    队列使用概述

    队列,如FIFOQueueRandomShuffleQueue,在TensorFlow的张量异步计算时都非常重要。

    例如,一个典型的输入结构:是使用一个RandomShuffleQueue来作为模型训练的输入:

    • 多个线程准备训练样本,并且把这些样本推入队列。
    • 一个训练线程执行一个训练操作,此操作会从队列中移除最小批次的样本(mini-batches)。

    TensorFlow的Session对象是可以支持多线程的,因此多个线程可以很方便地使用同一个会话(Session)并且并行地执行操作。然而,在Python程序实现这样的并行运算却并不容易。所有线程都必须能被同步终止,异常必须能被正确捕获并报告,回话终止的时候, 队列必须能被正确地关闭。

    所幸TensorFlow提供了两个类来帮助多线程的实现:tf.Coordinator
    tf.QueueRunner。从设计上这两个类必须被一起使用。Coordinator类可以用来同时停止多个工作线程并且向那个在等待所有工作线程终止的程序报告异常。QueueRunner类用来协调多个工作线程同时将多个张量推入同一个队列中。

    创建队列

    操作队列的函数主要有:

    • FIFOQueue():创建一个先入先出(FIFO)的队列

    • RandomShuffleQueue():创建一个随机出队的队列

    • enqueue_many():初始化队列中的元素

    • dequeue():出队

    • enqueue():入队

    1、FIFOQueue

    FIFOQueue是先进先出队列,主要是针对一些序列样本。如:在使用循环神经网络的时候,需要处理语音、文字、视频等序列信息的时候,我们希望处理的时候能够按照顺序进行,这时候就需要使用FIFOQueue队列。

    #先入先出队列,初始化队列,设置队列大小5
    q = tf.FIFOQueue(5,"float")
    #入队操作
    init = q.enqueue_many(([1,2,3,4,5],))
    #定义出队操作
    x = q.dequeue()
    y = x + 1
    #将出队的元素加1,然后再加入到队列中
    q_in = q.enqueue([y])
    #创建会话
    with tf.Session() as sess:
        sess.run(init)
        #执行3次q_in操作
        for i in range(3):
            sess.run(q_in)
        #获取队列的长度
        que_len = sess.run(q.size())
        #将队列中的所有元素执行出队操作
        for i in range(que_len):
            print(sess.run(q.dequeue()))
    

    2、RandomShuffleQueue

    RandomShuffleQueue是随机队列,队列在执行出队操作的时候,是以随机的顺序进行的。随机队列一般应用在我们训练模型的时候,希望可以无序的获取样本来进行训练,如:在训练图像分类模型的时候,需要输入的样本是无序的,就可以利用多线程来读取样本,将样本放到随机队列中,然后再利用主线程每次从随机队列中获取一个batch进行模型的训练。

    #初始化一个随机队列,设置队列大小为10,最小长度为2
    q = tf.RandomShuffleQueue(capacity=10,min_after_dequeue=2,dtypes="float")
    #创建会话
    with tf.Session() as sess:
        #定义10次入队操作
        for i in range(10):
            sess.run(q.enqueue(i))
        #定义8次出队操作
        for i in range(8):
            print(sess.run(q.dequeue()))
    

    注意:在使用随机队列的时候,我们设置了队列的容量为10,最小长度为2。当队列的长度已经等于队列的容量(10)再执行入队操作, 或队列的长度已经等于最小长度(2)再执行出队操作时,程序会发生阻断,即程序在执行,但是没有任何输出,如下图:

    定义了10次出队操作,当队列出队8次之后,就被阻断了。我们可以通过设置会话在运行时的等待时间来解除阻断:

    #初始化一个随机队列,设置队列大小为10,最小长度为2
    q = tf.RandomShuffleQueue(capacity=10,min_after_dequeue=2,dtypes="float")
    #创建会话
    with tf.Session() as sess:
        #定义10次入队操作
        for i in range(10):
            sess.run(q.enqueue(i))
        #设置会话运行时等待时间,等待时长为5s
        run_options = tf.RunOptions(timeout_in_ms=5000)
        #定义10次出队操作
        for i in range(10):
            try:
                #当队列进入阻断之后,超时就抛出异常
                print(sess.run(q.dequeue(),options=run_options))
            except tf.errors.DeadlineExceededError:
                print("out of range")
                #退出循环
                break
    

    当队列出队第9次的时候,进入阻断状态时,我们可以通过DeadlineExceededError来捕获阻断信息。

    Coordinator

    Coordinator类用来帮助多个线程协同工作,多个线程同步终止。
    其主要方法有:

    • should_stop():如果线程应该停止则返回True。
    • request_stop(<exception>): 请求该线程停止。
    • join(<list of threads>):等待被指定的线程终止。

    首先创建一个Coordinator对象,然后建立一些使用Coordinator对象的线程。这些线程通常一直循环运行,一直到should_stop()返回True时停止。
    任何线程都可以决定计算什么时候应该停止。它只需要调用request_stop(),同时其他线程的should_stop()将会返回True,然后都停下来。

    # 线程体:循环执行,直到`Coordinator`收到了停止请求。
    # 如果某些条件为真,请求`Coordinator`去停止其他线程。
    def MyLoop(coord):
      while not coord.should_stop():
        ...do something...
        if ...some condition...:
          coord.request_stop()
    
    # Main code: create a coordinator.
    coord = Coordinator()
    
    # Create 10 threads that run 'MyLoop()'
    threads = [threading.Thread(target=MyLoop, args=(coord)) for i in xrange(10)]
    
    # Start the threads and wait for all of them to stop.
    for t in threads: t.start()
    coord.join(threads)
    

    QueueRunner

    QueueRunner类会创建一组线程, 这些线程可以重复的执行Enquene操作, 他们使用同一个Coordinator来处理线程同步终止。此外,一个QueueRunner会运行一个_closer thread_,当Coordinator收到异常报告时,这个_closer thread_会自动关闭队列。

    您可以使用一个queue runner,来实现上述结构。
    首先建立一个TensorFlow图表,这个图表使用队列来输入样本。增加处理样本并将样本推入队列中的操作。增加training操作来移除队列中的样本。

    example = ...ops to create one example...
    # Create a queue, and an op that enqueues examples one at a time in the queue.
    queue = tf.RandomShuffleQueue(...)
    enqueue_op = queue.enqueue(example)
    # Create a training graph that starts by dequeuing a batch of examples.
    inputs = queue.dequeue_many(batch_size)
    train_op = ...use 'inputs' to build the training part of the graph...
    

    在Python的训练程序中,创建一个QueueRunner来运行几个线程, 这几个线程处理样本,并且将样本推入队列。创建一个Coordinator,让queue runner使用Coordinator来启动这些线程,创建一个训练的循环, 并且使用Coordinator来控制QueueRunner的线程们的终止。

    # Create a queue runner that will run 4 threads in parallel to enqueue
    # examples.
    qr = tf.train.QueueRunner(queue, [enqueue_op] * 4)
    
    # Launch the graph.
    sess = tf.Session()
    # Create a coordinator, launch the queue runner threads.
    coord = tf.train.Coordinator()
    enqueue_threads = qr.create_threads(sess, coord=coord, start=True)
    # Run the training loop, controlling termination with the coordinator.
    for step in xrange(1000000):
        if coord.should_stop():
            break
        sess.run(train_op)
    # When done, ask the threads to stop.
    coord.request_stop()
    # And wait for them to actually do it.
    coord.join(threads)
    

    异常处理

    通过queue runners启动的线程不仅仅只处理推送样本到队列。他们还捕捉和处理由队列产生的异常,包括OutOfRangeError异常,这个异常是用于报告队列被关闭。
    使用Coordinator的训练程序在主循环中必须同时捕捉和报告异常。
    下面是对上面训练循环的改进版本。

    try:
        for step in xrange(1000000):
            if coord.should_stop():
                break
            sess.run(train_op)
    except Exception, e:
       # Report exceptions to the coordinator.
       coord.request_stop(e)
    
    # Terminate as usual.  It is innocuous to request stop twice.
    coord.request_stop()
    coord.join(threads)
    
    展开全文
  • 遗传算法

    万次阅读 多人点赞 2019-04-06 21:41:47
    文字述说终究还是不如图表好理解,因此还是看图吧(下图将本题与自然遗传联系了起来): 通过以上描述,我们不难看出,遗传算法不能保证一定能求得最优解,而只能以一定的概率求最优解。但是使用遗传算法时,我们...

    使用遗传算法求解多峰函数的最大值,是我的一项课程作业,做完之后,顺便把文档整理出来做个记录。全部内容如下:

    1、问题描述

    编程实现遗传算法,并求解多峰函数的最大值。多峰函数的表达式如下所示:
    在这里插入图片描述
    用MATLAB做出函数的图像如下:
    在这里插入图片描述

    2、算法描述及实现

    2.1、遗传算法概述

    遗传算法(GA,Genetic Algorithm),也称为进化算法。遗传算法是受达尔文的进化论的启发,借鉴生物进化过程而提出的一种启发式搜索算法。其主要特点是直接对结构对象进行操作,因此不同于其他求解最优解的算法,遗传算法不存在求导和对函数连续性的限定,采用概率化的寻优方法,不需要确定的规则就能自动获取和指导优化的搜索空间,自适应地调整搜索方向。

    以上是对遗传算法相对抽象的总结,为了更具体形象的解释遗传算法的一般原理,我们首先介绍一些生物学上的概念

    ①种群:不同生物个体形成的群体,生物的进化以群体的形式进行,这样的一个群体称为种群;

    ②个体:组成种群的单个生物;

    ③基因:带有遗传信息的DNA片段,可以通俗的将基因理解为一段信息,这段信息决定的生物个体的性状;

    ④表现型:根据基因形成的个体的外部表现;

    ⑤适应度:生物个体对于生存环境的适应程度,越适应那么其得以存活和繁衍的概率就越大;

    ⑥遗传:通过繁殖过程,子代将从父母双方各获取一部分基因,形成新的自己的基因,这个过程中,会发生基因的复制、交叉,也会以较低的概率发生基因突变;

    ⑦自然选择:物竞天择,适者生存的自然淘汰机制。具体为对环境适应度高的个体参与繁殖的机会比较多,后代就会越来越多。适应度低的个体参与繁殖的机会比较少,后代就会越来越少;

    ⑧进化:种群通过代际繁衍不断适应生存环境的过程,在这个过程中,以对外界环境的适应度为评判标准,生物的性状不断得到改良。

    了解了这些术语的含义,我们就可以进一步说说生物进化的过程了。由于自然选择是客观存在的,即生物只能改变自己去适应环境,那么在自然选择的过程中,适应度低的个体会被淘汰,适应度高的个体被保留,高适应度的父体与母体又有更高的概率繁衍出适应度高的子代,因此在一代又一代的繁衍之后,高适应度的个体在种群中所占的比例越来越大,种群就这样完成了进化。

    现在我们要参考生物进化的过程来设计算法解决求最优解的问题。对此,遗传算法的思路是,将要解决的问题模拟成一个生物进化的过程,通过进化来寻找最优解。以我们题目中寻找多峰函数的最大值这个问题为例

    将(x, y)这一可能的解作为一个个体;将多峰函数的函数值f(x, y)作为个体的适应度;对(x, y)进行编码作为个体的基因;以适应度为标准不断筛选生物个体;通过遗传算子(如复制、交叉、变异等)不断产生下一代。如此不断循环迭代,完成进化。最终,根据设定的迭代次数,可得到最后一代种群,该种群中的个体适应度都较高,而多峰函数的最大值就有比较大的概率存在于这一群解中,以种群中适应度最高的个体作为问题的解,则可以说该解有比较高的概率就是我们希望求得的最优解。

    文字述说终究还是不如图表好理解,因此还是看图吧(下图将本题与自然遗传联系了起来):
    在这里插入图片描述
    通过以上描述,我们不难看出,遗传算法不能保证一定能求得最优解,而只能以一定的概率求最优解。但是使用遗传算法时,我们可以不用关心具体如何去找最优解,要做的只是简单的否定一些表现不好的个体。这一优点也是遗传算法能够取得广泛应用的原因之一。

    2.2、算法的流程

    通过上文的阐述,对于如何模拟自然进化来求题中多峰函数的最优解已经比较明晰了。这里我将列出遗传算法的主要步骤,并一一解析:

    第一步:随机产生一个种群,作为问题的初代解(通常,初代解可能与最优解相差较大,这是可以容忍的,只要保证初代解是随机产生的,以确保个体基因的多样性即可);

    第二步:寻找一种合适的编码方案对种群中的个体进行编码,可以选择如浮点数编码或二进制编码等常用编码方案(需要指出的是,不同的编码方案直接影响后续遗传算子的实现细节);

    第三步:以多峰函数的函数值 作为个体的适应度,计算种群中每个个体的适应度(算出的适应度将为后续的个体选择提供依据);

    第四步:根据适应度的高低选择参与繁衍的父体与母体,选择的原则是适应度越高的个体越可能被选中(以此不断淘汰适应度低的个体);

    第五步:对被选出的父体与母体执行遗传操作,即复制父体与母体的基因,并采用交叉、变异等算子产生出子代(在较大程度保留优秀基因的基础上,变异增加了基因的多样性,从而提高找到最优解的概率);

    第六步:根据一定的准则判断是继续执行算法,还是找出所有子代中适应度最高个体作为解返回并结束程序(判断的准则可以是设定的解的阈值、指定的迭代次数等)。
    在这里插入图片描述

    2.3、算法的编码实现

    2.3.1、编码

    本文采用的是二进制编码方式,这种编码方式编解码过程简单易行,相应的交叉算子、变异算子等操作用位运算即可实现。当然,它也有一定的缺点,比如连续性不够强。为保证求解的精度,本文使用14个bit为 编码,使用11个bit为 编码,两者组合成25个bit的最终结果。不难算出,该方式的编码精度可达千分位。具体的编码操作可总结为,将 或 的取值区间映射到0~2n-1这一整数范围,其中n表示编码位数, 或 在其取值区间的某点,相应的映射到整数区间中的某点,改点即为 或 的基因编码。程序如下:

    /*
        基因编码
        gene1       输入型参数,待编码的基因段1
        gene2       输入型参数,待编码的基因段2
        gene_code   输出型参数,基因编码
    
        返回值:当输入的基因不符合要求时返回false,否则返回true
    */
    static bool gene_encode(const double gene1, const double gene2, unsigned int *gene_code)
    {
        /* 判断基因是否合法 */
        if (!is_gene_legal(gene1, gene2))
            return false;
    
        /* 若基因合法则对其进行编码 */
        unsigned int gene1_code = (gene1 - GENE1_RANGE_LEFT) * (GENE1_CODE_MAX - 1) / (GENE1_RANGE_RIGHT - GENE1_RANGE_LEFT);
        unsigned int gene2_code = (gene2 - GENE2_RANGE_LEFT) * (GENE2_CODE_MAX - 1) / (GENE2_RANGE_RIGHT - GENE2_RANGE_LEFT);
        
        /* 组合基因片段 */
        *gene_code = (gene1_code << 11) | gene2_code;
    
        return true;
    }
    

    2.3.2、解码

    解码是编码的逆过程,无需赘述,程序如下:

    /*
        基因解码
        gene_code   输入型参数,基因编码
        gene1       输出型参数,解码后的基因段1
        gene2       输出型参数,解码后的基因段2
    
        返回值:当输入的基因编码不符合要求时返回false,否则返回true
    */
    static bool gene_decode(const unsigned int gene_code, double *gene1, double *gene2)
    {
        /* 判断基因编码是否合法 */
        if (!is_gene_code_legal(gene_code))
            return false;
    
        /* 若基因编码合法则对其进行解码 */
        unsigned int gene1_code = GET_GENE1_CODE(gene_code);
        unsigned int gene2_code = GET_GENE2_CODE(gene_code);
    
        *gene1 = (double)gene1_code * (GENE1_RANGE_RIGHT - GENE1_RANGE_LEFT) / (GENE1_CODE_MAX - 1) + GENE1_RANGE_LEFT;
        *gene2 = (double)gene2_code * (GENE2_RANGE_RIGHT - GENE2_RANGE_LEFT) / (GENE2_CODE_MAX - 1) + GENE2_RANGE_LEFT;
    
        return true;
    }
    

    2.3.3、计算适应度

    适应度函数也称评价函数,通常用于区分群体中个体好坏的标准。适应度高的,也就是优秀的个体有更大的几率参与繁衍,遗传自己的基因。一般的,适应度函数根据目标函数来确定,有时候直接将目标函数值作为适应度。这里,考虑到待求解的多峰函数,尖峰分布密集而且峰的直径很窄,这不利于遗传算法的收敛,因此本文不直接将多峰函数值作为适应度,而是利用对数函数将多峰函数进行平缓,并将平缓后的函数值作为目标函数。具体做法是,将多峰函数进行两次求对数,因此,多峰函数与适应度的关系可如下表示:
    在这里插入图片描述
    用MATLAB做出适应度函数图像如下:
    在这里插入图片描述
    对比前文中的图不难看出,图像得到了有效的平缓,同时不同峰之间也保持着一定的高低之别。值得一提的是,这里更主要的是给出优化遗传算法的一个思路,即可以在适应度函数上做文章。本题的适应度函数只是对多峰函数本身做了一个简单的变换,读者不妨思考一下,就本题而言有没有什么非常好的适应度函数。

    据上文所述,适应度求值函数如下:

    /*
        多峰函数:z = 21.5 + x *sin(4 * 3.1415926 * x) + y * sin(20 * 3.1415926 * y)
        适 应 度:log(log(z))
        约    束:-3.0 <= x <= 12.1; 4.1 <= y <= 5.8
        精    度:精确到千分位
    */
    double get_fitness(const double x, const double y)
    {
        return log(log(21.5 + x * sin(4 * PI * x) + y * sin(20 * PI * y)));
    }
    

    2.3.4、选择算子

    本文的选择算法采用了非常常用的“轮盘赌算法”,赌盘算法的原理非常简单明了。创建赌盘时,我们将种群中所有个体的适应度求和,不妨将得到的结果称为总和适应度。然后,将每个个体的适应度除以总和适应度,然后将得到的商逐个累加,每加一次就得到赌盘的一个边界,累加完成后总和为1。如下的饼状图可以更形象的表明赌盘的原理:
    在这里插入图片描述
    由上文所述,赌盘创建函数可如下编写:

    /*
        创建赌盘
        ga      遗传算法器指针
    */
    static void create_roulette(GA *ga)
    {
        /* 计算赌盘中的概率 */
        ga->roulette[0] = ga->fitness[0] / ga->sum_fitness;
    
        for (int num = 1; num < ga->population_num - 1; num++)
        {
            ga->roulette[num] = ga->roulette[num - 1] + ga->fitness[num] / ga->sum_fitness;
        }
    
        ga->roulette[ga->population_num - 1] = 1.0;
    }
    

    再回到选择算子,选择算子需要赌盘作为基础,其运行时,会产生一个0到1的随机数,然后在赌盘中找到该数所在的区间,这个区间对应的个体即为被选中的个体。因此,适应度越高的个体被选中的几率越大,这是合理的。当然,也存在较小的概率选出适应度较低的个体,为了避免这种情况,本文引入了竞争机制,即一次选择的过程选出2个个体,再取其中适应度较高的那个个体,具体的程序如下:

    /*
        基因选择函数
        ga      遗传算法器指针
        返回值:返回使用轮盘赌的方式选出的个体(编号)
        说  明:选择策略为轮盘赌+随机竞争
    */
    static unsigned int select(GA *ga)
    {
        unsigned int index1 = 0, index2 = 0;
    
        /* 产生一个[0.0, 1.0]之间的浮点数 */
        double selector1 = rand() * 1.0 / RAND_MAX;
        double selector2 = rand() * 1.0 / RAND_MAX;
    
        /* 找出被选中的个体的索引 */
        for (; selector1 > ga->roulette[index1]; index1++);
        for (; selector2 > ga->roulette[index2]; index2++);
    
        return (ga->fitness[index1] > ga->fitness[index2] ? index1 : index2);
    }
    

    2.3.5、交叉算子

    遗传算法的交叉操作实质上是按某种方式交换父体和母体的部分基因,常见的交叉算子有单点交叉、两点交叉、多点交叉、均匀交叉及算术交叉等。本文选用两点交叉法,实现过程既不复杂,也有较好的随机性,该方法可由下图示意:
    在这里插入图片描述
    图中虚线指出的两个交叉点是随机产生的。具体程序如下:

    /*
        交叉函数
        ga          遗传算法器指针
        one         输出型参数,待交叉基因
        another     输出型参数,待交叉基因
        说明:
        1.对传入的基因编码执行两点交叉操作
    */
    static void cross(GA *ga, unsigned int *one, unsigned int *another)
    {
        /* 1.随机产生两个交叉点的位置 */
        unsigned char pos1 = rand() % GENE_CODE_LENGTH + 1;
        unsigned char pos2 = rand() % GENE_CODE_LENGTH + 1;
        unsigned char min_pos = min(pos1, pos2);
        unsigned char max_pos = max(pos1, pos2);
    
        /* 2.截出需要交换的基因段 */
        unsigned int one_gene_seg = get_bits(*one, min_pos, max_pos) << (min_pos - 1);
        unsigned int another_gene_seg = get_bits(*another, min_pos, max_pos) << (min_pos - 1);
        unsigned int mask = ~(get_bits(~(0U), min_pos, max_pos) << (min_pos - 1));
    
        /* 3.执行交叉操作 */
        *one = (*one & mask) | another_gene_seg;
        *another = (*another & mask) | one_gene_seg;
    }
    

    2.3.6、变异算子

    在自然界中,基因变异可以增加个体的多样性,这对于遗传算法来说是增加了个体的随机性,可以增加找到最优解的概率。本文采用的变异算子所做的操作是随机选择基因的某一位进行反转,程序如下:

    /*
        变异函数
        gene_code       输入型参数
        说明:
        1.对传入的基因编码执行变异操作
        2.随机选择基因编码中的一位做反转操作
    */
    static void mutate(unsigned int *gene_code)
    {
        unsigned int mutate_bit = 1 << (rand() % GENE_CODE_LENGTH);
        *gene_code ^= mutate_bit;
    }
    

    2.3.7、繁殖函数及进化函数

    遗传算法的主要算子都在上文中分析过了,下面要做的就是根据遗传算法的流程将这些算子整合起来以实现算法功能。在本文中,这其中涉及到两个关键的函数,即繁殖函数和进化函数。繁殖函数包括基因的复制、交叉及变异,同时本文还采用了子代竞争策略,即父代产生的两个子代个体仅保留适应度最高的,程序如下:

    /*
        繁殖函数
        ga       遗传算法器指针
        father   从种群中选出的父体
        mother   从种群中选出的母体
        返回值:  适应度最高的子代的基因编码
        说明: 
        1.一对父体与母体将繁殖出一对子代
        2.选择出适应性更好的子代返回
    */
    static unsigned int inherit(GA *ga, unsigned int father, unsigned int mother)
    {
        unsigned int son1 = ga->gene_code[father];
        unsigned int son2 = ga->gene_code[mother];
    
        /* 1.交叉 */
        cross(ga, &son1, &son2);
    
        /* 2.变异 */
        mutate(&son1);
        mutate(&son2);
    
        /* 3.子代竞争 */
        double son1_gene1, son1_gene2, son2_gene1, son2_gene2;
        gene_decode(son1, &son1_gene1, &son1_gene2);
        gene_decode(son2, &son2_gene1, &son2_gene2);
    
        return (ga->get_fitness(son1_gene1, son1_gene2) > ga->get_fitness(son2_gene1, son2_gene2)) ? son1 : son2;
    }
    

    进化函数则实现了遗传算法的一次完整的迭代过程,根据上文给出的遗传算法流程图,不难进行如下编码:

    /*
        进化函数
        ga      遗传算法器指针
    */
    static void evolve(GA *ga)
    {
        /* 1.申请暂存子代基因编码的内存 */
        unsigned int *descendants = (unsigned int *)calloc(ga->population_num, sizeof(unsigned int));
        
        /* 2.精英保留(将上一代中适应度最高的个体的基因编码保留) */
        descendants[0] = ga->gene_code[ga->best_individual];
        
        /* 3.选择合适的父体与母体 */
        unsigned int father = select(ga);
        unsigned int mother = select(ga);
    
        /* 4.繁殖(包含交叉与变异) */
        for (int num = 1; num < ga->population_num; num++)
            descendants[num] = inherit(ga, father, mother);
    
        /* 5.将子代记录到ga中并进行基因解码(使新一代的基因编码与基因对应) */
        for (int num = 0; num < ga->population_num; num++)
        {
            ga->gene_code[num] = descendants[num];
            gene_decode(ga->gene_code[num], &ga->gene[num].gene1, &ga->gene[num].gene2);
        }
        
        /* 5.更新种群适应度 */
        fit(ga);
        
        /* 6.更新赌盘 */
        create_roulette(ga);
    
        /* 7.释放之前申请的空间 */
        free(descendants);
    }
    

    3、运行结果及分析

    至此,本文已经给出了一个遗传算法的C语言实现的所有关键程序。下面就调用编写的遗传算法进行测试。本文将创建含有100个个体的种群,并进行100代迭代以求解多峰函数的最大值,一次完整的调用本文实现的遗传算法的程序如下所示:

    /* 创建遗传算法器 */
    GA *ga = create_ga(get_fitness, 100);
    
    /* 初始化遗传算法器 */
    ga->init(ga);
    
    /*迭代100代*/
    for (int i = 0; i < 100; i++)
    ga->evolve(ga);
    
    /*销毁遗传算法器*/
    delete_ga(ga);
    

    经多次调用测试,算法执行的结果较为稳定,所得的多峰函数最大值大多在38以上,多次运行结果中最好的解为38.849744,对应的坐标为(11.625331, 5.725256)。将迭代求得的最大值用MATLAB作图如下:
    在这里插入图片描述
    为验证是否找到了最优解,用MATLAB遍历求出该多峰函数在给定定义域内的最大值为38.8501,与本文求出的结果相差0.000356,可见本文实现的遗传算法表现还不算太差。

    文中给出的程序比较散,这里给出完整程序的下载链接

    展开全文
  • python画图

    千次阅读 2019-05-06 16:12:15
    y3) # 绘制曲线 y3 plt.show() subplot(numRows, numCols, plotNum) 图表的整个绘图区域被分成 numRows 行和 numCols 。 然后按照从左到右,从上到下的顺序对每个子区域进行编号,左上的子区域的编号为1 plotNum ...

    参考文献
    https://www.jianshu.com/p/78ba36dddad8
    首先了解下图上的标示代表什么
    在这里插入图片描述

    1.画一个图

    import matplotlib.pyplot as plt
    import numpy as np
    
    x = np.linspace(-2, 6, 50)
    y1 = x + 3      # 曲线 y1
    y2 = 3 - x      # 曲线 y2
    plt.figure()    # 定义一个图像窗口
    plt.plot(x, y1) # 绘制曲线 y1
    plt.plot(x, y2) # 绘制曲线 y2
    plt.show()
    
    

    在这里插入图片描述
    调用np.linspace是创建一个 numpy 数组,并记作 x。
    x 包含了从 -2 到 6 之间等间隔的 50 个值。
    y1 和 y2 则分别是这 50 个值对应曲线的函数值组成的 numpy 数组。
    前面的操作还处于设置属性的阶段,还没有开始绘制图形。
    plt.figure() 函数才意味着开始执行绘图操作。最后调用show()函数将图形呈现出来。

    2.在一个界面上画多个图

    参考
    https://www.cnblogs.com/xiaoboge/p/9683056.html

    import matplotlib.pyplot as plt
    import numpy as np
    
    if __name__ == '__main__' :
        x = np.linspace(-2, 6, 50)
        y1 = x + 3      # 曲线 y1
        y2 = 3 - x      # 曲线 y2
        y3 = 2*x + 3     # 曲线 y3
        plt.figure()    # 定义一个图像窗口
        plt.subplot(2,2,1)
        plt.plot(x, y1) # 绘制曲线 y1
        plt.subplot(2,2,2)
        plt.plot(x, y2) # 绘制曲线 y2
        plt.subplot(2,1,2)
        plt.plot(x, y3) # 绘制曲线 y3
        plt.show()
    
    

    subplot(numRows, numCols, plotNum)
    图表的整个绘图区域被分成 numRows 行和 numCols 列。
    然后按照从左到右,从上到下的顺序对每个子区域进行编号,左上的子区域的编号为1
    plotNum 参数指定创建的 Axes 对象所在的区域。
    subplot在 plotNum 指定的区域中创建一个轴对象. 如果新创建的轴和之前创建的轴重叠的话,之前的轴将被删除。
    结果如图
    在这里插入图片描述

    import pandas as pd
    import matplotlib.pyplot as plt
    
    #t = pd.DataFrame({"month":[1,2,3,4],"number":[1000,1300,1200,1300]})
    #t.to_csv('excel.csv', sep=',', index=False)
    
    df = pd.read_csv('excel.csv', sep=',')
    
    x=[]
    y=[]
    
    for i,row in df.iterrows():
        x.append(int(row.values[0]))
        y.append(int(row.values[1]))
    
    plt.plot(x,y,label='csv')
    plt.xlabel('month')
    plt.ylabel('number')
    plt.title('pls let this work')
    plt.legend()
    plt.show()
    

    3.python读取txt文件并画图

    import matplotlib.pyplot as plt
    import numpy as np
    
    data = np.loadtxt('path.txt')
    
    plt.plot(data[:,0],data[:,1])
    plt.xlabel("x")
    plt.ylabel("y")
    plt.show()
    

    python读取txt文件并画图
    参考https://blog.csdn.net/Astro_Dog/article/details/80700243

    展开全文
  • 【数据库学习】数据库总结

    万次阅读 多人点赞 2018-07-26 13:26:41
    实体化视图的结果会保存在一个普通的数据表中,在对实体化视图进行查询的时候不再会对创建实体化视图的基表进行查询,而是直接查询实体化视图对应的结果表,然后通过定期的刷新机制来更新实体化视图表中的数据。...

    1,概念

    1)数据库

    数据库是长期存储在计算机内、有组织的、可共享的大量数据的集合。
    数据库中存储的是数据及数据之间的关系。

    正常情况读写文件系统比数据库快一到两个数据级;
    数据库的查询,大量并发的时候可能最浪费时间的是connect和close。
    数据库的优势是体现的大量数据的查询、统计以及并发读写,不是在速度上。

    2)数据库数据特点

    永久存储、有组织、可共享。
    (数据的最小存取单位是数据项)

    3)数据库系统的特点

    ①数据结构化

    ②数据的共享性,冗余度,易扩充

    ③数据独立性高

    数据独立性包括:物理独立性和逻辑独立性
    a)物理独立性(外模式\模式映像):
    用户程序不需要了解,应用程序要处理的只是数据的逻辑结构,这样当数据的物理存储改变了,应用程序不用改变。
    b)逻辑独立性(模式\内模式映像):
    逻辑独立性是指用户的应用程序与数据库的逻辑结构是相互独立的,即,当数据的逻辑结构改变时,用户程序也可以不变。
    逻辑数据独立性(logical data independence)是指概念模式改变,外模式和应用程序不变。在逻辑数据独立性里,数据的逻辑结构发生改变或存储关系的选择发生改变时用户不会受到影响。改变概念模式,例如增加和删除实体、增加和删除属性、增加和删除联系,不需要改变现有的外模式或重写应用程序。在DBMS中只需要修改视图的定义和映像来支持逻辑数据独立性。对用户来说,不再关心所做的修改是非常重要的。换句话说,模式经过逻辑重构之后,根据外模式构建的应用程序还是和从前一样工作。

    4)概念模型(E-R模型)

    ①概念

    概念模型的一种表示方法:实体联系方法,用E-R方法(E-R模型)来描述。
    概念模型是用于信息世界的建模,是一种信息模型,与具体的DBMS无关。且能满足用户对数据的处理要求,易于修改。
    概念模型与具体数据模型无关且容易向数据库模型转化。

    实体:举行表示
    属性:椭圆表示,并用直线与实体连接
    联系:菱形表示,用直线与实体连接,同时在边上标上联系的类型(1:1,1:n,m:n)。
    

    一个联系转化为一个关系模式,与该联系相连的各实体的码以及联系的属性转化为关系的属性,该关系的码则有三种情况:
    若联系为1:1,则每个实体的码均是该关系的后选码。
    若联系为1:n,则关系的码为n端实体的码。
    若联系为m:n,则关系的码为诸实体码的组合。

    数据库模式定义语言DDL(Data Definition Language):是用于描述数据库中要存储的现实世界实体的语言。一个数据库模式包含该数据库中所有实体的描述定义。这些定义包括结构定义、操作方法定义等。

    数据库逻辑设计: 将概念设计所得到的概念模型转换为某一具体的数据模型(层次、网状、关系、面向对象).

    5)关系完整性

    在关系模型中,关系完整性主要是指以下三方面:

    实体完整性

    所谓的实体完整性就是指关系(所谓的关系就是表)的主码不能取空值;
    比如学生表的主码通常是取学号为主码

    参照完整性

    是指参照关系中每个元素的外码要么为空(NULL),要么等于被参照关系中某个元素的主码;
    参照关系也称为外键表,被参照关系也称为主键表。

    用户定义的完整性

    指对关系中每个属性的取值作一个限制(或称为约束)的具体定义。比如 性别属性只能取”男“或”女“,再就是年龄的取值范围,可以取值0-130 ,但不能取负数,因为年龄不可能是负数。

    6)关系数据库规范化

    目地:使结构更合理,消除存储异常,使数据冗余尽量小,便于插入、删除和更新。
    原则:遵从概念单一化“一事一地”原则,即一个关系模式描述一个实体或实体间的一种联系。
    规范的实质:概念的单一化。
    规范化的方法:将关系模式投影分解成两个或两个以上的关系模式。

    2,依赖和范式

    1)依赖

    ①部分函数依赖

    设X,Y是关系R的两个属性集合,存在X→Y,若X’是X的真子集,存在X’→Y,则称Y部分函数依赖于X。

        举个例子:通过AB能得出C,通过A也能得出C,通过B也能得出C,那么说C部分依赖于AB。
    

    ②完全函数依赖

    设X,Y是关系R的两个属性集合,X’是X的真子集,存在X→Y,但对每一个X’都有X’!→Y,则称Y完全函数依赖于X。

        举个例子:通过AB能得出C,但是AB单独得不出C,那么说C完全依赖于AB.
    

    ③传递函数依赖

    设X,Y,Z是关系R中互不相同的属性集合,存在X→Y(Y !→X),Y→Z,则称Z传递函数依赖于X。

        举个例子:通过A得到B,通过B得到C,但是C得不到B,B得不到A,那么成C传递依赖于A
    

    ④多值依赖

    设R(U)是属性集U上的一个关系模式。X,Y,Z是U的子集,并且Z=U-X-Y。关系模式R(U)中多值依赖X→→Y成立,当且仅当对R(U)的任一关系r,给定的一对(x,z)值有一组Y的值,这组值仅仅决定于x值而与z值无关。

    举例:
    有这样一个关系 <仓库管理员,仓库号,库存产品号> ,假设一个产品只能放到一个仓库中,但是一个仓库可以有若干管理员,那么对应于一个 <仓库管理员,库存产品号>有一个仓库号,而实际上,这个仓库号只与库存产品号有关,与管理员无关,就说这是多值依赖。

    2)范式

    各个范式联系:
    5NF⊂4NF⊂BCNF⊂3NF⊂2NF⊂1NF

    ①1NF(满足最低要求的范式:字段不可再分,原子性)

    如果一个关系模式R的所有属性都是不可分的基本数据项,则R∈1NF。
    自我理解1NF就是无重复的列。
    如:(X1,X2)→X3,X2→X3 其中x3对x2部分依赖
    如:(X1,X2)→X3,X2→X4 其中有非主属性X4部分依赖于候选键{X1,X2},所以这个关系模式不为第二范式;又因为范式之间的关系满足1NF⊇2NF⊇3NF ⊇ BCNF,所以是第一范式。

    ②2NF(消除部分子函数依赖:一个表只能说明一个事物)

    若R∈1NF,且每一个非主属性完全函数依赖于码,则R∈2NF。
    即要求数据库表中的每个实例或行必须可以被唯一地区分。

    ③3NF(消除传递依赖,即消除非主属性对键的传递依赖:每列都与主键有直接关系,不存在传递依赖。任何非主属性不依赖于其它非主属性。)

    若R∈3NF,则每一个非主属性既不部分依赖于码,也不传递依赖于码。
    自我理解是:表中所有的数据元素不但要能唯一地被主键所标识,而且他们之间还必须相互独立,不存在其他的函数关系。

    ④BCNF(修正第三范式、扩充第三范式 消除主属性对键的传递依赖)

    所有非主属性对每一个码都是完全函数依赖;
    所有主属性对每一个不包含它的码,也是完全函数依赖;
    没有任何属性完全函数依赖于非码的任何一组属性。

    ⑤4NF

    关系模式R<U,F>∈1NF,如果对于R的每个非平凡多值依赖X->->Y(Y∉X),X都含有码,则称R<U,F>∈4NF

    3,数据库平台

    数据库管理系统(DBMS):是系统软件,是数据库系统的核心。
    常见数据库管理系统有:Access、mysql、sql server

    4,数据库语句

    SQL 语言是非过程化的语言,易学习。
    SQL语言具有两种使用方式:一种是在终端交互方式下使用,称为交互式SQL; 另一种是嵌入在高级语言的程序中使用,称为嵌入式SQL,而这些高级语言可以是C、PASCAL、COBOL等,称为宿主语言。

    1)基本对象

    关系数据库系统支持 三级模式结构,其概念模式、外模式和内模式中的基本对象有表、视图和索引。
    三级模式结构有效地组织、管理数据,提高了数据库的逻辑独立性和物理独立性。使数据库达到了数据独立性。

    ①模式(schema,逻辑模式)

    A.概念

    是数据库中全体数据的逻辑结构和特征的描述,是所有用户的公共数据视图。是数据库系统模式结构的中间层,即不涉及数据的物理存储细节和硬件环境,也与具体的应用程序、开发工具及高级设计语言无关。
    模式是数据库数据在逻辑级上的视图,一个数据库只有一个模式。

    也用于区分一个 大项目中的各个小项目,这样若有相同名字的表的话, 不同模式不会发生冲突。相当于编程时的命名空间。
    如:
    一个公司的系统,分2个子系统,分别为财务系统和人力资源系统.
    这2个子系统, 共用一个数据库。
    那么 财务系统的表, 可以放在财务的模式(schema).
    人力资源系统的表,放在人力资源系统的模式里面。
    这2个子系统,能够互相访问对方的表。
    但是又不因为 表重名 的问题,影响对方。

    B.访问

    访问具体的一个表,可以由 4个部分组成
    分别为 服务器名, 数据库名,模式名,表名。

    对于访问本地的数据库:
    不指定模式名的话, 数据库默认使用dbo模式。
    (DBO是每个数据库的默认用户,具有所有者权限,即DbOwner )
    pg不指定模式的话默认使用public模式。

    C.操作

    --创建
    CREATE SCHEMA schema_name;
    

    ②外模式(子模式,用户模式)

    是数据库用户能够看见和使用的局部数据的逻辑结构和特征的描述,是数据库用户的数据视图,是与某一应用有关的数据的逻辑表示。
    外模式通常是模式的子集,一个数据库可以有多个外模式,但一个应用程序只能有一个外模式。
    外模式是保证数据库安全性的一个有力措施:用户只能访问外模式的数据,其余数据不可见。

    ③内模式(存储模式)

    一个数据库只有一个内模式。
    内模式是数据物理结构和存储方式的描述,是数据在数据库内部的表示方式。

    数据库管理系统在三级模式之间提供了两层映像:
    外模式/模式映像(保证数据的逻辑独立性)
    模式/内模式映像(保证了物理独立性)

    ④表

    表分为临时表和永久表。

    临时表

    临时表存储在tempdb中(如下),当不再使用时会自动删除。

    IF OBJECT_ID('tempdb..#ownerAnnouce') IS NOT NULL
    

    根据进程独立,只有进程的拥有者有表的访问权限,其它用户不能访问该表;
    不同的用户进程,创建的临时表虽然“名字”相同,但是这些表之间相互并不存在任何关系;在SQLSERVER中,通过特别的命名机制保证临时表的进程独立性。

    临时表有两种类型:本地和全局。

    A.本地临时表

    名称以单个数字符号 (#) 打头;它们仅对当前的用户连接是可见的;当用户从 SQL Server 实例断开连接时被删除。

    B.全局临时表

    名称以两个数字符号 (##) 打头,创建后对任何用户都是可见的,当所有引用该表的用户从 SQL Server 断开连接时被删除。

    临时表优点

    真正的临时表利用了数据库临时表空间,由数据库系统自动进行维护,因此节省了表空间。并且由于临时表空间一般利用虚拟内存,大大减少了硬盘的I/O次数,因此也提高了系统效率。

    临时表的创建

    A. create table #临时表名
    B.select * into #临时表名 from 表名(永久表或临时表)

    ⑤视图

    A.概念

    视图是一张虚拟表,视图的字段是自定义的,视图只支持查询,查询数据来源于实体表。

    一般视图是只读的,在pg中通过添加规则可以进行视图的更新。从pg9.1开始,用户可以通过INSTEAD OF的触发器来实现视图更新。

    B.优缺点

    • 优点
      视图可以将多个复杂关联表提取信息,方便查询,但不能优化查询速度(调用视图查询时才进行动态检索数据)。
      即,如果你认为一个sql查询非常慢,为了优化它的速度把它建立成视图,这是不可取的,视图是每次调用的时候生成,并不是数据源变化就刷新数据,并不能提高检索效率。
    • 缺点
      视图就是临时表,即调即用,如果数据源没有任何变化,在反复调用中,临时表会缓存到内存中(SHOW STATUS LIKE ‘Qcache%’;),视图中不能创建索引,但视图可以基于索引生成 。

    C.场景

    1. 重用SQL语句;
    2. 简化复杂SQL操作(生成视图),重用查询且不需要知道基本查询细节。
    3. 保护数据。用户有表的部分权限。
    4. 更改数据格式和表示。视图可返回与底层表不同的表示和格式。

    D.操作

    --创建视图
    CREATE OR REPLACE VIEW view_name(studentName, studentAge)  --(studentName, studentAge) 可以去掉,加上是重命名列名
    AS 
    SELECT user_info.name, user_info.age from user_info;
    
    --删除视图
    DROP VIEW view_name;
    

    ⑥实体视图

    相对于普通的视图来说,实体化视图的不同之处在于实体化视图管理存储数据,占据数据库的物理空间。

    实体化视图的结果会保存在一个普通的数据表中,在对实体化视图进行查询的时候不再会对创建实体化视图的基表进行查询,而是直接查询实体化视图对应的结果表,然后通过定期的刷新机制来更新实体化视图表中的数据。

    demo

    -- 创建物化视图
    CREATE MATERIALIZED VIEW MAX_ID_MVIEW 
    AS
      SELECT PART_ID, MAX(ID)  MAX_ID
      FROM PART_DETAIL GROUP BY PART_ID;
      
    -- 如果刷新时不带CONCURRENTLY则无需创建唯一索引
    CREATE UNIQUE INDEX IDX_MAX_ID ON MAX_ID_MVIEW(PART_ID);
    
    -- 利用watch命令每120s刷新一次物化视图
    REFRESH MATERIALIZED VIEW CONCURRENTLY MAX_ID_MVIEW; \watch 120
    

    作用

    1. 减轻网络负担:通过实体化视图将数据从一个数据库分发到多个不同的数据库上,通过对多个数据库访问来减轻对单个数据库的网络负担。
    2. 搭建分发环境:通过从一个中央数据库将数据分发到多个节点数据库,达到分发数据的目的。
    3. 复制数据子集:实体化视图可以进行行级/列级的筛选,这样可以复制需要的那一部分数据。
    4. 实体化视图是用于汇总,预计算,复制或分发数据的对象, 在大型的数据库中使用它可以提高涉及到的SUM,COUNT,AVG,MIN,MAX等的表的查询的速度。
    5. 物化视图的快速刷新采用了增量的机制,在刷新时,只针对基表上发生变化的数据进行刷新。因此快速刷新是物化视图刷新方式的首选。

    ⑦索引

    为了改变数据库的性能和可访问性所增加的一组辅助性数据。
    详细介绍见下文。

    2)数据结构创建及修改

    1>数据库操作

    --查看数据库
    show databases;
    --建库
    create database children;
    --删库
    drop database children;
    --调用数据库
    use children;
    

    2>表操作

    --pg建表
    CREATE TABLE if not exists public.stu_info(   --创建public模式下的表
        FOREIGN KEY (ID) REFERENCES people_info (ID),  --单个外键,一般情况下不建议增加这种强约束
        id int8 PRIMARY KEY,    --系统会自动为主键创建一个隐含的索引  primary key(Sno,Cno)组合主键
        address VARCHAR (255) UNIQUE NOT NULL,
        birthday TIMESTAMP NOT NULL,  
        age int default 15,  --默认值,影响后续插入值。但对旧数据没有影响。
       CONSTRAINT student2_pkey PRIMARY KEY (id),
       CONSTRAINT ck_age CHECK(age<18), --检查约束,约束某些字段需要满足的要求。NULL被认为满足条件。
       CONSTRAINT uk_tbl_unique_a_b unique(id ,address) --唯一约束。唯一键中可以写入任意多个NULL!即可以存在多组 1,null  
    )
    WITH (
      OIDS=FALSE
    );
    ALTER TABLE myschema.tb_test
      OWNER TO postgres;
    
    --重命名表
    alter table tableName RENAME TO newName;--pg
    

    i>指定默认值

    一般用于数据预置或create_time、update_time的自动录入。各个DBMS获得系统日期如下:

    DBMS函数/变量
    AccessNOW()
    DB2CURRENT_DATE
    MySQLCURRENT_DATE()
    OracleSYSDATE
    PostgreSQLCURRENT_DATE
    SQL ServerGETDATE()
    SQLitedate(‘now’)
    --修改默认值
    alter table tableName alter column age set DEFAULT 15;--pg
    --删除默认值
    alter table tableName alter column age drop DEFAULT 15;--pg
    

    ii>表约束

    表约束有:主键、外键、检查约束、唯一约束、非NULL约束。

    --添加主键(有些DBMS不允许在建表之后修改主键)
    ALTER TABLE tableName ADD PRIMARY KEY(fieldName) ; --fieldName在库中不能有重复数据
    --增加约束
    alter table tableName add check (age<16);--pg 增加检查约束,约束名为:tableName_age_check
    alter table tableName add constraint uk_tbl_unique_a_b unique (a,b);--pg 增加唯一约束
    alter table tableName alter column fieldName set NOT NULL;--pg 增加非空约束
    --删除约束
    alter table tableName drop constraint constraintName;--pg 根据约束名删除检查约束、唯一约束
    alter table tableName alter column fieldName drop NOT NULL;--pg 删除非空约束(非空约束没有约束名)
    

    iii>修改表字段

    --增加列\添加一个字段
    alter table tableName add column columnName varchar(30) default 'a' not null; --column 可加可不加
    --删除列(会连同字段上的约束一并删除)
    alter table tableName drop column columnName; --column 可加可不加
    --修改列名:
    alter table tableName rename column fieldName TO fieldNameNew;--pg、oracle中
    exec sp_rename '[表名].[列名]‘,’[表名].[新列名]'--在sqlserver
    ALTER TABLE 表名 CHANGE 列名 新列名 列类型--mysql
    
    --修改字段类型或长度:
    alter table tableName modify column 字段名 类型;
    alter table tableName alter column fieldName TYPE text;--pg修改字段数据类型。仅在当前数据都可以隐式转换为新类型时才可以执行成功
    --将NAME最大列宽增加到10个字符
    ALTER TABLE CARD ALTER COLUMN NAME varchar(10) 
    
    

    3)数据查询

    数据库处理一个查询的步骤:
    客户端连接->查询缓存->解析器->预处理器->查询优化器->查询执行引擎->数据

    1. 客户端发送一条查询给服务器;
    2. 服务器先会检查查询缓存query cache,如果命中了缓存,则立即返回存储在缓存中的结果。否则进入下一阶段;
    3. 服务器端进行SQL解析parsing、预处理transition,再由优化器optimization生成对应的执行计划;
    4. 根据优化器生成的执行计划,调用存储引擎的API来执行分布distribution查询;
    5. 将结果返回给客户端。
    

    1>简单查询

    select * from student;
    select 1+2; #当表达式与表列无关时,在pg和mysql中不适用“from tableName”
    

    拼接查询:
    Access和 SQL Server使用 + 号。DB2、Oracle、PostgreSQL、SQLite和Open Office Base 使用 ||。

    select label || '_' || id from user_info;  --结果:abc_1
    

    2>条件查询

    功能表达举例备注
    等于=
    不等于<>!=
    空值is null,is not nullselect * from student where class is not null;
    确定集合,ininnot inselect * from student where age not in(21,23);在sql标准中仅支持100个以内的占位符作为查询参数。根据数据库不同,对in的参数和长度有不同的限制,否则会直接报错。
    确定范围between and , not between and
    模糊查询like ,not likeselect * from student where name like '%丽%';’ %代表任意长度(可为0)的字符串;_(下划线):代表任意单个字符。(汉字代表2个字符,所以一个汉字用两个下划线);\为转义字符

    select出的别名是否可以作为where查询条件?不能,因为执行计划中where在selectz之前。如:select label a from asset_field where a = '分类'

    3>排序查询

    非排序查询的数据顺序:pg默认返回数据的顺序是插入表的数据顺序。

    # 单个排序:
    select name,age from student order by age desc; # 默认为asc:升序排列。desc:降序排序。
    
    #多重排序:
    order by 字段5,字段6 asc  //先按字段5排序,再按字段6排序
    

    4>case when then查询

    --简单case函数
    case sex
      when '1' then '男'
      when '2' then '女’
      else '其他' end
    --case搜索函数
    case when sex = '1' then ''
         when sex = '2' then ''
         else '其他' end  
    

    应用:

    select (case sex
    		  when '1' then '男'
    		  when '2' then '女’
    		  else '其他' end)sex from student where class = 11;
    

    5>where、group by、having

    大部分的where都可以背having代替,不同的是where过滤行,而having过滤分组,用在group by之后。(where在分组前过滤,having在分组后过滤)

    select class,avg(age) as age from student 
    group by class 
    having avg(age)>23 /*要求平均年龄大于23*/
    

    where肯定在group by 之前
    where后的条件表达式里不允许使用聚合函数,而having可以。

    6> 函数

    聚合函数

    avg平均数,同min(age)、max(age)、sum(age)

    select avg(age)  as age from student group by class order by age desc; 
    

    count

    select count(class)from student;
    /*数量 因为使用了92标准,所以null不计入count*/
    count(*) 跟count(1) 的结果一样,返回记录的总行数,都包括对NULL 的统计,
    count(column) 是不包括NULL 的统计。
    

    distinct

    select distinct(class)from student;/*去重复,出现所有不同的内容*/
    select count(distinct(class)) from student;
    

    其它

    LEFT(“123456789”,LEN(“数据库”))/*分两步运算,第一步是运算LEN函数,结果是3。第二步针对123456789这个字符从左边开始连续取三个数*/
    
    select top 100 * from student where no=11;/*显示前100行*/
    select isnull(name,'无') as name,age,class from student;/*isnull之后就无列名了 用as给列重命名*/
    select name,age,class,'the name is' + name as introduce from student;/*用加号形成一个自定义列*/
    

    7>SQL-92 规则

    是数据库的一个标准。以下代码 写在存储过程前面,表示遵从SQL-92 规则。
    SQL-92 标准要求在对空值进行等于 (=) 或不等于 (<) 比较时取值为 FALSE。

    SET ANSI_NULLS ON
    GO
    SET QUOTED_IDENTIFIER ON
    GO
    

    SET ANSI_NULLS ON
    即使 column_name 中包含空值,使用 WHERE column_name = NULL 的 SELECT 语句仍返回零行。
    即使 column_name 中包含非空值,使用 WHERE column_name < NULL 的 SELECT 语句仍会返回零行。

    SET QUOTED_IDENTIFIER ON
    为ON:标识符可以由双引号分隔,而文字必须由单引号分隔。
    为OFF:标识符不可加引号。

    8>多层查询 EXISTS

    如果内层查询语句查询到符合条件的记录,就返回一个真值(true),否则,将返回
    一个假值(false)。

    SELECT * FROM employee
    WHERE EXISTS
    (SELECT d_name FROM department WHERE d_id=1003);
    

    同理还有:NOT EXISTS。

    9>关联查询、联结(JOIN)表

    关系数据库设计中表的设计是把信息分解成多个表,一类数据一个表,各表通过某些共同的值相互关联。
    一般情况下我们不建议建立外键这种强关联的关联信息。

    可伸缩(scale)
    能够适应不断增加的工作量而不失败。关系数据库的可伸缩性远远优于非关系数据库。

    注意:

    1. 联结的表越多效率越低。
    2. SQL本身不限制联结表的数目,但DBMS有最大数目限制。
    3. 一般情况下,联结查询比子查询快,实际应用中应该尝试两种方法看哪种快。
    JSON类型说明备注
    JOIN如果表中有至少一个匹配,则返回行INNER已省略。外联结比内联结返回的行数多(还包括没有关联的行)
    LEFT JOIN即使右表中没有匹配,也从左表返回所有的行OUTER已省略
    RIGHT JOIN即使左表中没有匹配,也从右表返回所有的行OUTER已省略
    FULL JOIN只要其中一个表中存在匹配,就返回行OUTER已省略

    luo_persons表:

    id_plast_namefirst_nameaddresscity

    luo_orders表:

    id_oorder_noid_p

    要求输出:谁订购了产品,并且他们订购了什么产品?

    ①联表查询(等值联结,equijoin)

    SELECT
    	a.last_name, a.first_name, b.order_no
    FROM
    	luo_persons a,
    	luo_orders b 
    WHERE
    	a.id_p = b.id_p	
    

    ②join查询(内联结,inner join, 推荐)

    /*(推荐)等值联结明确指定联结类型可转换为inner join

    SELECT
    	last_name,
    	first_name,
    	order_no 
    FROM
    	luo_persons
    	INNER JOIN luo_orders ON luo_persons.id_p = luo_orders.id_p
    

    ③union查询(复合查询、并查询)

    UNION 操作符用于合并两个或多个 SELECT 语句的结果集。

    注意:

    1. UNION 内部的 SELECT 语句必须拥有相同数量的列、表达式或聚集函数。列也必须拥有相似的数据类型(可以不完全相同,但是可以互相转换)。同时,每条 SELECT 语句中的列的顺序必须相同。
    2. 默认地,UNION 操作符选取不同的值。如果允许重复的值,请使用 UNION ALL。
    3. UNION能组合的最大语句数目限制需要查询具体的DBMS文档。
    	select id_p from luo_persons 
    	union 
    	SELECT id_p from luo_orders
    

    某些DBMS中还支持其它类型的UNION:

    1. EXCEPT(或MINUS):检索在第一个表中存在而在第二个表中不存在的行;
    2. INTERSECT:检索两个表中都存在的行。

    4)数据更新

    ①数据插入

    i> insert

    insert into tableName(no,name) values'1','kate');
    --按表中列的顺序,但如果表结构发生了变化那么对应 sql也要改。不推荐
    insert into product values('001','001','N','N');
    

    有自增长主键(id)的插入:
    i>可以把id的值设置为null或者0,这样mysql会自己做处理
    ii>手动指定需要插入的列,不插入这一个字段的数据!

    ii> insert select

    将select结果插入表中,一般用于可重复执行的sql。
    注:
    1.insert select语句中,如果select返回多行,那么会insert多行数据。

    INSERT INTO "public"."vendors"("vend_name", "vend_id") select 'vend_name1', 1 
    WHERE NOT EXISTS (select 1  FROM "public"."vendors" WHERE vend_id = 1);
    

    iii> select into

    1. SELECT INTO 语句从一个表中选取数据,然后把数据插入另一个表中。
    2. SELECT INTO 语句常用于创建表的备份复件或者用于对记录进行存档。
    3. select into 可以从多个表中检索数据,但只能插入到一个表中。

    函数里面,把一个查询出来的值存入临时变量:

    SELECT LastName,FirstName
    INTO _lName,_fName  FROM Persons
    

    也可以存入临时表中:

    SELECT *
    INTO Persons_backup
    FROM Persons
    

    ②数据修改

    update tableName set name = 'Tom' where name='kate';
    update tableName set age = age + 1;
    

    5)数据删除

    删除表中几行:

    DELETE FROM Person WHERE LastName = 'Wilson' 
    

    删除表中所有行,保留表、不释放空间。所删除的每行记录都会进日志,可以回滚。

    DELETE FROM table_name
    

    删除表:删除内容和定义,释放空间

    drop table user;    
    DROP TABLE IF EXISTS "public"."role_relation"; 可重复执行sql
    

    删除表中所有数据,保留表、同时释放空间(速度比delete快,但是无法撤回,日志里面只记录页释放):

    truncate table book;
    

    truncate是DDL语句(Data Definition,数据定义语句),相当于用重新定义一个新表的方法把原表的内容直接丢弃了,所以执行起来很快。delete语句是DML语句(Data Manipulation,数据操作语句),把数据一条一条的删除,所以删除多行数据执行较慢。

    6)其他注意

    ①加中括号

    列名、表名、存储过程名、函数名等都可以按需要加中括号。防止某些关键字在应用中引起歧义。

    select [select] from 表名;
    

    7)数据库授权

    ①授权GRANT

        GRANT <权限>
        ON <对象类型>  <对象名>
        TO <用户>
        [WITH GRANT OPTION]  // 如果指定了WITH GRANT OPTION子句,则获得某种权限的用户还可以把这种权限再授予其他用户,允许用户传递权限,但是不允许循环授权。
    

    举例:

    例1:把查询Student表的权限授给用户U1
    GRANT SELECT
    ON TABLE Student
    TO U1;
    
    例2:把全部操作权限授予用户U2和U3
    GRANT ALL PRIVILEGES
    ON TABLE Student,Course
    TO U2,U3;
    
    例3:把查询权限授予所有用户
    GRANT SELECT
    ON TABLE SC
    TO PUBLIC;
    

    ③权限的收回 REVOKE

    REVOKE <权限>
    ON <对象类型>  <对象名>
    FROM <用户>
    

    举例:

    例6:收回所有用户对表sc的查询权限
    REVOKE SELECT
    ON TABLE SC
    FROM PUBLIC;
    

    ③对用户模式的授权

    由DBA(数据库管理员,Database Administrator,简称DBA)在创建用户时实现。

    CREATE USER <username>
    [WITH] [DBA|RESOURCE|CONNECT]
    

    只有系统的超级用户才有权创建一个新的数据库用户
    新创建的用户有三种权限:DB,|RESOURCE,CONNECT

    ④数据库角色创建及授权

    CREATE ROLE <角色名>
    

    给角色授权:

    GRANT <权限>
    ON <对象类型>  对象名
    TO <角色>
    

    将一个角色授予其他的角色或用户

    GRANT <角色1>
    TO <角色3>
    [WITH ADMIN OPTION]//如果指定了WITH ADMIN OPTION 子句,则获得某种权限的角色或用户还可以把这种权限再授予其他角色
    

    角色权限的收回

    REVOKE <权限>
    ON <对象类型>  <对象名>
    FROM <角色>
    

    ⑤DENY 拒绝账户访问

    在安全系统中创建一项,以拒绝给当前数据库内的安全帐户授予权限并防止安全帐户通过其组或角色成员资格继承权限。

    DENY { ALL | statement [ ,...n ] }
    TO security_account [ ,...n ]
    

    和授权区别:
    不授权是没有权限,但是如果这个用户属于某个角色,这个角色有了权限,那么这个用户可以从角色继承这个权限。如果选择了deny,即使这个用户属于某个具有权限的角色,他也没有权限。

    8)数据类型

    ①uniqueidentifier

    可存储16字节的二进制值,其作用与全局唯一标记符(GUID)一样。GUID是唯一的二进制数:世界上的任何两台计算机都不会生成重复的GUID值。GUID主要用于在用于多个节点,多台计算机的网络中,分配必须具有唯一性的标识符。

    9)函数

    ①OBJECT_ID

    A. 返回指定对象的对象 ID

    USE master;
    GO
    SELECT OBJECT_ID(N'AdventureWorks.Production.WorkOrder') AS 'Object ID';
    GO
    

    B. 验证对象是否存在

    USE AdventureWorks;
    GO
    IF OBJECT_ID (N'dbo.AWBuildVersion', N'U') IS NOT NULL
    DROP TABLE dbo.AWBuildVersion;
    GO
    

    N是显式的将非unicode字符转成unicode字符,它来自 SQL-92 标准中的 National(Unicode)数据类型,用于扩展和标准化,在这里可以不用,写作object_id(PerPersonData)。

    10)SQL中的借书经典案例

    ①问题描述

    本题用到下面三个关系表:
    CARD 借书卡。 CNO 卡号,NAME 姓名,CLASS 班级
    BOOKS 图书。 BNO 书号,BNAME 书名, AUTHOR 作者,PRICE 单价,QUANTITY 库存册数
    BORROW 借书记录。 CNO 借书卡号,BNO 书号,RDATE 还书日期

    备注:限定每人每种书只能借一本;库存册数随借书、还书而改变。

    要求1. 写出建立BORROW表的SQL语句,要求定义主码完整性约束和引用完整性约束。

    CREATE TABLE BORROW(
        CNO int FOREIGN KEY REFERENCES CARD(CNO),
        BNO int FOREIGN KEY REFERENCES BOOKS(BNO),
        RDATE datetime,
        PRIMARY KEY(CNO,BNO)) 
    

    要求2. 找出借书超过5本的读者,输出借书卡号及所借图书册数。

    SELECT CNO,借图书册数=COUNT(*)
    FROM BORROW
    GROUP BY CNO
    HAVING COUNT(*)>5
    

    要求3. 查询借阅了"水浒"一书的读者,输出姓名及班级

    CARD 借书卡。 CNO 卡号,NAME 姓名,CLASS 班级
    BOOKS 图书。 BNO 书号,BNAME 书名, AUTHOR 作者,PRICE 单价,QUANTITY 库存册数
    BORROW 借书记录。 CNO 借书卡号,BNO 书号,RDATE 还书日期

    SELECT * FROM CARD c
    WHERE EXISTS(
        SELECT * FROM BORROW a,BOOKS b 
        WHERE a.BNO=b.BNO
            AND b.BNAME=N'水浒'
            AND a.CNO=c.CNO) 
    

    要求4. 查询过期未还图书,输出借阅者(卡号)、书号及还书日期。

    SELECT * FROM BORROW 
    WHERE RDATE<GETDATE() 
    

    要求5. 查询书名包括"网络"关键词的图书,输出书号、书名、作者。

    SELECT BNO,BNAME,AUTHOR FROM BOOKS
    WHERE BNAME LIKE N'%网络%' 
    

    N’string’ 表示string是个Unicode字符串

    要求6. 查询现有图书中价格最高的图书,输出书名及作者。

    SELECT BNO,BNAME,AUTHOR FROM BOOKS
    WHERE PRICE=(
        SELECT MAX(PRICE) FROM BOOKS) 
    

    要求7. 查询当前借了"计算方法"但没有借"计算方法习题集"的读者,输出其借书卡号,并按卡号降序排序输出。

    SELECT a.CNO
    FROM BORROW a,BOOKS b
    WHERE a.BNO=b.BNO AND b.BNAME=N'计算方法'
        AND NOT EXISTS(
            SELECT * FROM BORROW aa,BOOKS bb
            WHERE aa.BNO=bb.BNO
                AND bb.BNAME=N'计算方法习题集'
                AND aa.CNO=a.CNO)
    ORDER BY a.CNO DESC 
    

    要求8. 将"C01"班同学所借图书的还期都延长一周。

    UPDATE b SET RDATE=DATEADD(Day,7,b.RDATE)
    FROM CARD a,BORROW b
    WHERE a.CNO=b.CNO
        AND a.CLASS=N'C01' 
    
    DATEADD(datepart,number,date)  
    date 参数是合法的日期表达式。number 是您希望添加的间隔数;对于未来的时间,此数是正数,对于过去的时间,此数是负数。
    

    要求9. 从BOOKS表中删除当前无人借阅的图书记录。

    DELETE FROM BOOKS a
    WHERE NOT EXISTS(
        SELECT * FROM BORROW
        WHERE BNO=a.BNO) 
    

    要求11.在BORROW表上建立一个触发器,完成如下功能:如果读者借阅的书名是"数据库技术及应用",就将该读者的借阅记录保存在BORROW_SAVE表中(注ORROW_SAVE表结构同BORROW表)。

    CREATE TRIGGER TR_SAVE ON BORROW
    FOR INSERT,UPDATE
    AS
    IF @@ROWCOUNT>0
    INSERT BORROW_SAVE SELECT i.*
    FROM INSERTED i,BOOKS b
    WHERE i.BNO=b.BNO
        AND b.BNAME=N'数据库技术及应用' 
    

    要求13.查询当前同时借有"计算方法"和"组合数学"两本书的读者,输出其借书卡号,并按卡号升序排序输出。

    SELECT a.CNO
    FROM BORROW a,BOOKS b
    WHERE a.BNO=b.BNO
        AND b.BNAME IN(N'计算方法',N'组合数学')
    GROUP BY a.CNO
    HAVING COUNT(*)=2
    ORDER BY a.CNO DESC
    

    5,索引

    6,关系运算

    1)集合运算符

    并(∪)、差(-)、交(∩)、笛卡尔积(×)

    笛卡尔积(直积):表示为X × Y,第一个对象是X的成员而第二个对象是Y的所有可能有序对的其中一个成员。
    例如,A={a,b}, B={0,1,2},则
    A×B={(a, 0), (a, 1), (a, 2), (b, 0), (b, 1), (b, 2)}
    

    2)专门的关系运算符

    ①选择(限制、σ)

    在关系R中选择满足给定条件的诸元组。

    ②投影(π)

    关系R上的投影是从R中选择出若干属性列组成新的关系。
    这里写图片描述
    投影之后可既改变行,又改变元组的数量。

    ③连接(θ连接、⋈)

    从两个关系的笛卡尔积中选取属性间满足一定条件的元组。(连接由乘积(笛卡尔积)、选择、投影组成)
    分为等值连接(=)、自然连接(要求比较的分量是相同的属性组,并在结果中把重复的属性列去掉)。
    这里写图片描述

    ④除运算(➗)

    RS÷S的意义就是:“在R和S的联系RS中,找出与S中所有的元组有关系的R元组”。

    3)算术比较符

    4)逻辑运算符

    非与或

    7,数据库完整性

    1)实体完整性

    主键唯一且不为空。

    2)参照完整性

    不允许修改外码
    级连操作:当删除或修改被参照表时,同时删除或修改参照表中的不一致元祖。

    3)用户定义的完整性

    4)触发器(Trigger)

    是用户定义在关系表上的一类由事件驱动的特殊过程。一旦定义,任何用户对标的增删改操作均由服务器自动激活相应触发器,在DBMS核心层进行集中的完整性控制。

    8,存储过程(Stored Procedure)

    1)概念

    存储过程是一组为了完成特定功能的SQL 语句集,存储在数据库中,经过第一次编译后再次调用不需要再次编译,用户通过指定存储过程的名字并给出参数(如果该存储过程带有参数)来执行它。

    2)优点

    ①执行效率高

    存储过程因为SQL 语句已经预编译过了,因此运行的速度比较快。

    ②降低了客户机和服务器之间的通信

    存储过程在服务器端运行,减少客户端的压力。
    减少网络流量,客户端调用存储过程只需要传存储过程名和相关参数即可,与传输SQL 语句相比自然数据量少了很多。

    ③方便实施企业规则(提高了可维护性、安全性)

    可以把企业规则的运算程序写成存储过程放入数据库服务器中,由RDBMS管理,既有利于集中控制,又能够方便地进行维护。
    当用户规则发生变化时,只要修改存储过程,无须修改其他应用程序。

    允许模块化程序设计,就是说只需要创建一次过程,以后在程序中就可以调用该过程任意次,类似方法的复用。
    增强了使用的安全性,充分利用系统管理员可以对执行的某一个存储过程进行权限限制,从而能够实现对某些数据访问的限制,避免非授权用户对数据的访问,保证数据的安全。程序员直接调用存储过程,根本不知道表结构是什么,有什么字段,没有直接暴露表名以及字段名给程序员。

    ④安全性高

    可设定只有某些用户才具有对指定存储过程的使用权。

    3)缺点

    调试麻烦(至少没有像开发程序那样容易),可移植性不灵活(因为存储过程是依赖于具体的数据库)。

    4)场景

    当一个事务涉及到多个SQL语句时或者涉及到对多个表的操作时就要考虑用存储过程;
    当在一个事务的完成需要很复杂的商业逻辑时(比如,对多个数据的操作,对多个状态的判断更改等)要考虑;还有就是比较复杂的统计和汇总也要考虑,但是过多的使用存储过程会降低系统的移植性。

    sql尽量放在存储过程中。
    面对大量数据,用orcle比sql server稳定。

    5)代码

    ①创建

    use test1
    set ansi_nulls on
    go
    set quoted_identifier on
    go
    create procedure procedure_student
    	-- add the parameters for the stored procedure here
    	@gradeid int,
    	@gradename varchar(10) --传入的参数
    as
    begin
    	--计算内容
    end
    go
    

    ②执行

    exec dbo.procedure_student 1,'g'
    

    9,数据库恢复技术

    1)事务

    10,并发控制

    为了保证事务的隔离性和一致性,DBMS需要对并发操作进行正确调度。

    1)并发操作带来的数据不一致性

    ①更新丢失

    ②读“脏”数据

    事务T1修改数据,T2读取数据,T1由于某种原因被撤销,则数据修改回原值,但T2读取的数据是之前修改的数据,即脏数据、不正确的数据。

    ③不可重复读

    事务T1读数据后,T2修改了数据,T1无法再现上一次读取的结果。

    ④幻读

    事务T1读数据后,T2新增或者删除了数据,T1无法再现上一次读取的结果。

    2)并发控制技术

    悲观锁:封锁
    乐观锁:版本号、时间戳

    3)封锁分类(悲观锁)

    ①共享锁(S锁、读锁)

    (读取)操作创建的锁。其他用户可以并发读取数据,但任何事物都不能获取数据上的排它锁,直到已释放所有共享锁。
    若事务T对数据对象A加上S锁,则事务T只能读A;其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这就保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。

    ②排它锁(X锁、写锁,eXclusive lock)

    若事物T对数据对象A加上X锁,则只允许T读取和修改A,其它任何事务都不能再对A加任何类型的锁,直到T释放A上的锁。它防止任何其它事务获取资源上的锁,直到在事务的末尾将资源上的原始锁释放为止。

    ③更新锁(U锁)

    用来预定要对此页施加X锁,它允许其他事务读,但不允许再施加U锁或X锁;当被读取的页将要被更新时,则升级为X锁;U锁一直到事务结束时才能被释放。

    4)封锁问题

    ①活锁

    i>饥饿

    考虑一台打印机分配的例子,当有多个进程需要打印文件时,系统按照短文件优先的策略排序,该策略具有平均等待时间短的优点,似乎非常合理,但当短文件打印任务源源不断时,长文件的打印任务将被无限期地推迟,导致饥饿以至饿死。

    ii>活锁概念

    与饥饿相关的另外一个概念称为活锁,在忙式等待条件下发生的饥饿,称为活锁。

    a)忙式等待:不进入等待状态的等待。
    b)阻塞式等待:进程得不到共享资源时将进入阻塞状态,让出CPU 给其他进程使用。
    c)忙等待和阻塞式等待的相同之处:
    在于进程都不具备继续向前推进的条件,不同之处在于处于忙等待的进程不主动放弃CPU,尽管CPU 可能被剥夺,因而是低效的;而处于阻塞状态的进程主动放弃CPU ,因而是高效的。

    iii>举例

    事务T1请求封锁R,T2请求封锁R,T3请求封锁R……
    T1释放R之后,系统批准了T3的请求,然后是T4……请求,T2可能永远等待下去。(在整个过程中,事务T2 在不断的重复尝试获取锁R)。

    iv>与死锁区别

    活锁的时候,进程是不会阻塞的,这会导致耗尽CPU 资源,这是与死锁最明显的区别。
    处于活锁的实体是在不断的改变状态,所谓的“活”, 而处于死锁的实体表现为等待;活锁有一定几率解开,而死锁是无法解开的。

    v>避免方式

    采用先来先服务策略。

    ②死锁

    i>概念

    是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去,此时称系统处于死锁状态或系统产生了死锁。

    ii>举例

    T1请求封锁R1,T2请求封锁R2,然后T1又请求封锁R2,T1一直等待T2释放R2,此时,T2请求封锁R1,T2将一直等待T1释放R1。

    iii>死锁原因

    在数据库中,产生死锁的原因主要是:
    两个或多个事务都已封锁了一些数据对象,然后又都请求其他事务已封锁的数据对象,从而出现死等待。

    产生死锁的四个必要条件:
    (1) 互斥条件:一个资源每次只能被一个进程使用。
    (2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
    (3) 不可剥夺条件: 进程已获得的资源,在末使用完之前,不能强行剥夺。
    (4) 环路等待条件: 若干进程之间形成一种头尾相接的循环等待资源关系。
    只要系统发生了死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死
    锁。

    iv>死锁预防

    预防死锁的发生只需破坏死锁产生的四个必要条件之一即可。

    1. 破坏互斥条件
      如果允许系统资源都能共享使用,则系统不会进入死锁状态。但有些资源根本不能同时访问,如打印机等临界资源只能互斥使用。所以,破坏互斥条件而预防死锁的方法不太可行,而且在有的场合应该保护这种互斥性。
    2. 破坏不剥夺条件
      当一个已保持了某些不可剥夺资源的进程,请求新的资源而得不到满足时,它必须释放已经保持的所有资源,待以后需要时再重新申请。这意味着,一个进程已占有的资源会被暂时释放,或者说是被剥夺了,或从而破坏了不可剥夺条件。
      该策略实现起来比较复杂,释放已获得的资源可能造成前一阶段工作的失效,反复地申请和释放资源会增加系统开销,降低系统吞吐量。这种方法常用于状态易于保存和恢复的资源,如CPU 的寄存器及内存资源,一般不能用于打印机之类的资源。
    3. 破坏请求和保持条件
      釆用预先静态分配方法,即进程在运行前一次申请完它所需要的全部资源,在它的资源未满足前,不把它投入运行。一旦投入运行后,这些资源就一直归它所有,也不再提出其他资源请求,这样就可以保证系统不会发生死锁。
      这种方式实现简单,但缺点也显而易见,系统资源被严重浪费,其中有些资源可能仅在运行初期或运行快结束时才使用,甚至根本不使用。而且还会导致“饥饿”现象,当由于个别资源长期被其他进程占用时,将致使等待该资源的进程迟迟不能开始运行。
    4. 破坏环路等待条件
      为了破坏循环等待条件,可釆用顺序资源分配法。首先给系统中的资源编号,规定每个进程,必须按编号递增的顺序请求资源,同类资源一次申请完。也就是说,只要进程提出申请分配资源Ri,则该进程在以后的资源申请中,只能申请编号大于Ri 的资源。
      这种方法存在的问题是,编号必须相对稳定,这就限制了新类型设备的增加;尽管在为资源编号时已考虑到大多数作业实际使用这些资源的顺序,但也经常会发生作业使甩资源的顺序与系统规定顺序不同的情况,造成资源的浪费;此外,这种按规定次序申请资源的方法,也必然会给用户的编程带来麻烦。

    都不好用,一般采用死锁的诊断和解除。

    v>死锁的诊断和解除

    a)超时法
    如果一个事务等待时间超时,则认为发生死锁。(可能误判)
    b)事务等待图法
    事务等待图是一个有向图,反映了事务的等待情况。如果图中出现回路,就表示出现了死锁。

    处理方案是:选择一个处理代价最小的事务,将其撤销并释放所有锁。
    a) 从死锁进程处剥夺资源
    b) 终止部分或全部进程

    5) 两段锁协议(Two-Phase Locking――2PL)

    两段锁协议规定所有的事务应遵守的规则:
    ① 在对任何数据进行读、写操作之前,首先要申请并获得对该数据的封锁。
    ② 在释放一个封锁之后,事务不再申请和获得其它任何封锁。
    即事务的执行分为两个阶段:
    第一阶段是获得封锁的阶段,称为扩展阶段。
    第二阶段是释放封锁的阶段,称为收缩阶段。

    定理:若所有事务均遵守两段锁协议,则这些事务的所有交叉调度都是可串行化的。
    对于遵守两段协议的事务,其交叉并发操作的执行结果一定是正确的。值得注意的是,上述定理是充分条件,不是必要条件。一个可串行化的并发调度的所有事务并不一定都符合两段锁协议,存在不全是2PL的事务的可串行化的并发调度。
    同时我们必须指出,遵循两段锁协议的事务有可能发生死锁。

    此时事务T1 、T2同时处于扩展阶段,两个事务都坚持请求加锁对方已经占有的数据,导致死锁。
    为此,又有了一次封锁法。一次封锁法要求事务必须一次性将所有要使用的数据全部加锁,否则就不能继续执行。因此,一次封锁法遵守两段锁协议,但两段锁并不要求事务必须一次性将所有要使用的数据全部加锁,这一点与一次性封锁不同,这就是遵守两段锁协议仍可能发生死锁的原因所在。

    11,常见图

    DFD 数据流图(Data Flow Diagram):
    这里写图片描述
    ER图 实体-联系图(Entity-Relationship Diagram)
    这里写图片描述

    12,数据库连接:JDBC与JdbcTemplate

    13,数据库安全

    1)SQL注入

    ①概念

    在SQL 语句在拼接的情况下,用户输入为一部分sql语句。

    ②解决方法

    i> 对特殊字符进行过滤、转义或者使用预编译的sql 语句绑定变量

    SQL执行时,2种方式:
    ①字符串处理(拼接),然后执行SQL
    用户输入的时候,可以通过输入sql语句来进行SQL注入。
    ②传参,执行SQL -->交给SQL引擎**(推荐)**
    用prepareStatement,参数用set 方法进行填装。

    String sql= "insert into userlogin values(?,?)";
    PreparedStatement ps=conn.prepareStatement(sql);
    for(int i=1;i<100;i++){
    ps.setInt(1, i);
    ps.setInt(2, 8888);
    ps.executeUpdate();
    ps.close();
    conn.close();
    

    ii> 当sql 语句运行出错时,不要把数据库返回的错误信息全部显示给用户,以防止泄漏服务器和数据库相关信息

    iii>检查变量的数据类型和格式

    只要是有固定格式的变量,在SQL 语句执行前,应该严格按照固定格式去检查,确保变量是我们预想的格式,这样很大程度上可以避免SQL 注入攻击。
    例如:对于where id={$id}这种形式,数据库里所有的id 都是数字,那么就应该在SQL 被执行前,检查确保变量id 是int 类型。

    iv>所有的SQL 语句都封装在存储过程中

    所有的SQL 语句都封装在存储过程中,这样不但可以避免SQL 注入,还能提高一些性能。

    14,分布式数据库

    1)概念

    分布式数据库是一个物理上分散的而逻辑上集中的数据集。
    它有三大特点: 数据分布性 逻辑关联性 站点自治性

    2)五个基本原则

    ①资源的重复性
    指分布式系统中硬件,软件以及数据的冗余配置。
    ②物理上的分布性
    从硬件,软件以及数据上看都是相互独立地分布。
    ③高层操作系统(或者分布式操作系统)
    高层操作系统负责对分布性的资源进行统一的控制,它使一个简单的硬件堆积转变为一个统一协调的工作系统。
    ④系统的透明性
    透明性是分布式系统的灵魂,实现不同层次的透明性是分布式系统必须解决的关键问题之一。
    ⑤协作的自治性
    每一节点都是一个完整的处理系统,同时又是合作的。 简而言之:分布式系统是一个多节点的,处理或数据分布的,在统一下提高综合处理能力的协作体。

    3)待解决问题

    不完整系统状态信息
    时间延迟
    通信的代价
    负载均衡

    4)分类(从控制方式角度)

    ①紧耦合式DDBMS

    全局控制信息放在一个称为中心站点的站点上。所有的全局访问都必须通过中心站点来确定远程数据片的位置。
    优点:容易实现数据的一致性和完整性。
    缺点:易产生访问瓶颈,系统效率不高,可靠性较差。

    ②联邦式DDBMS

    每个站点都包含全局控制信息的一个副本,都可以接受全局访问。任何对远程数据的请求,都可以通过广播方式传播到其他节点。
    优点:具有较好的可靠性和可用性,并行性好,更容易适应旧有的系统集成和异构分布式数据库系统的建立。
    缺点:保持数据的一致性很困难,实现难度大。

    ③组合式DDBMS

    是上述方案的折衷,它把站点分为两类,一类具有全局控制信息,称为主节点,可以接受全局任务,另一类没有全局信息,只能为主节点提供数据服务。
    优点:灵活性较好,易于实现层次控制结构。
    缺点:设计复杂。

    5)分布透明性

    即在分布式数据库系统中用户不必关心数据的分布情况。分为三个层次:

    ①分片透明性

    它是分布式数据库系统的最高透明性层次,它向用户完全屏蔽了DDB的分片信息。这样的透明性保持了高水平的数据独立性。

    ②位置透明性

    用户的应用程序不需要关心数据分片的具体存储站点,当数据库的数据片的存储站点发生改变时,只需改变对应的GRS/NRS映射就可以保持全局表示模式不发生改变

    ③数据模型透明性

    它向用户屏蔽的只是本站点的具体数据库存储及其管理情况。 在异构的情况下,这种透明性避免了用户对不同数据模型的转换的实现。
    本地透明性是3种透明方式中最低的。

    6)数据分割方法

    ①水平分割

    把全局关系的元组分割成一些子集,这些子集被称为数据分片或段(Fragment)。
    水平分割可以通过关系运算“选择”来定义。

    水平分片是对全局关系执行“选择”操作,把具有相同性质的元组进行分组,构成若干个不相交的子集.水平分片的方法可归为初级分片和导出分片两类。

    ②垂直分割

    把全局关系按照属性组(纵向)分隔成一些数据分片或段。
    垂直分割可以通过关系运算“投影”来定义。

    ③混合分割

    可把水平分割和垂直分割这两种方法结合起来使用,产生混合式数据分片。

    ④数据分片应遵循的原则

    若R={R1,R2,…,Rn}满足:
    1)完整性(completeness)条件:
    如果分片 a∈R,则必有a∈Ri,i=l,2,…,n
    2)可重构(reconstructed)条件:
    R=∪ Ri,(水平分片)或R=∞Ri,(垂直分片)
    3)不相交(disjoint)条件:
    Ri∩ Rj=φ,i≠j,I,j:=1,2,…,,n(水平 分片)
    Ri∩Rj=主键属性,I,j=1,2,…,n(垂直分片)

    7)分布式数据库和集中式区别

    分布式(distributed)是指在多台不同的服务器中部署不同的服务模块,通过远程调用协同工作,对外提供服务。
    集群(cluster)是指在多台不同的服务器中部署相同应用或服务模块,构成一个集群,通过负载均衡设备对外提供服务。

    15,数据库优化

    1)优化SQL 语句

    ①explain

    通过explain(查询优化神器)用来查看SQL 语句的执行效果,可以帮助选择更好的索引和优化查询语句,写出更好的优化语句。
    通常我们可以对比较复杂的尤其是涉及到多表的SELECT 语句,把关键字EXPLAIN 加到前面,查看执行计划。例如:explain select * from news;

    explain语法:

    explain select … from … [where ...] 
    

    ② 用具体的字段列表代替“*

    任何地方都不要使用select * from t ,不要返回用不到的任何字段。

    ③ 不在索引列做运算或者使用函数

    ④ 查询尽可能使用limit 减少返回的行数,减少数据传输时间和带宽浪费。

    2)优化表的数据类型

    ① 使用procedure analyse()函数对表进行分析

    该函数可以对表中列的数据类型提出优化建议。能小就用小。表数据类型第一个原则是:使用能正确的表示和存储数据的最短类型。这样可以减少对磁盘空间、内存、cpu 缓存的使用。
    使用方法:select * from 表名procedure analyse();

    ② 对表进行拆分

    通过拆分表可以提高表的访问效率。有2 种拆分方法:
    1.垂直拆分
    把主键和一些列放在一个表中,然后把主键和另外的列放在另一个表中。如果一个表中某些列常用,而另外一些不常用,则可以采用垂直拆分。
    2.水平拆分
    根据一列或者多列数据的值把数据行放到二个独立的表中。

    ③ 使用中间表来提高查询速度

    创建中间表,表结构和源表结构完全相同,转移要统计的数据到中间表,然后在中间表上进行统计,得出想要的结果。

    3)硬件优化

    ①CPU 的优化

    选择多核和主频高的CPU。

    ②内存的优化

    使用更大的内存。将尽量多的内存分配给MYSQL 做缓存。

    ③磁盘I/O 的优化

    i>使用磁盘阵列

    RAID 0 没有数据冗余,没有数据校验的磁盘陈列。实现RAID 0至少需要两块以上的硬盘,它将两块以上的硬盘合并成一块,数据连续地分割在每块盘上。
    RAID1 是将一个两块硬盘所构成RAID 磁盘阵列,其容量仅等于一块硬盘的容量,因为另一块只是当作数据“镜像”。
    使用RAID-0+1 磁盘阵列。RAID 0+1 是RAID 0 和RAID 1 的组合形式。它在提供与RAID 1 一样的数据安全保障的同时,也提供了与RAID 0 近似的存储性能。

    ii>调整磁盘调度算法

    选择合适的磁盘调度算法,可以减少磁盘的寻道时间。

    4)MySQL 自身的优化

    对MySQL 自身的优化主要是对其配置文件my.cnf 中的各项参数进行优化调整。如指定MySQL 查询缓冲区的大小,指定MySQL 允许的最大连接进程数等。

    5)应用优化

    ①使用数据库连接池

    ②使用查询缓存

    它的作用是存储select 查询的文本及其相应结果。如果随后收到一个相同的查询,服务器会从查询缓存中直接得到查询结果。查询缓存适用的对象是更新不频繁的表,当表中数据更改后,查询缓存中的相关条目就会被清空。

    6)大访问量的优化

    ①使用优化查询的方法

    (见上面)

    ②主从复制,读写分离

    i>主从复制(master,slave):

    通过配置两台(或多台)数据库的主从关系,可以将一台数据库服务器的数据更新同步到另一台服务器上。网站可以利用数据库的这一功能,实现数据库的读写分离,从而改善数据库的负载压力。一个系统的读操作远远多于写操作,因此写操作发向master,读操作发向slaves 进行操作(简单的轮循算法来决定使用哪个slave)。
    利用数据库的读写分离,Web 服务器在写数据的时候,访问主数据库(Master),主数据库通过主从复制机制将数据更新同步到从数据库(Slave),这样当Web 服务器读数据的时候,就可以通过从数据库获得数据。这一方案使得在大量读操作的Web 应用可以轻松地读取数据,而主数据库也只会承受少量的写入操作,还可以实现数据热备份,可谓是一举两得的方案。
    这里写图片描述

    负载均衡(Load Balance,简称LB)

    7)数据库分表、分区、分库

    分表见上面描述。
    分区就是把一张表的数据分成多个区块,这些区块可以在一个磁盘上,也可以在不同的磁盘上,分区后,表面上还是一张表,但数据散列在多个位置,这样一来,多块硬盘同时处理不同的请求,从而提高磁盘I/O 读写性能,实现比较简单。包括水平分区和垂直分区。
    分库是根据业务不同把相关的表切分到不同的数据库中,比如web、bbs、blog 等库。

    17,应用

    1)服务器与服务器之间传输文件夹下的文件,一个文件夹下有10 个文件,另一个文件夹下有100 个文件,两个文件夹大小相等,问,哪个传输更快?

    10 个文件更快。
    1)建立连接数更少,建立连接的开销比传输文件的开销大。
    2)文件写入磁盘,要计算文件的起始位置,文件数目少的话,这个开销就小了

    展开全文
  • 软件测试面试题汇总

    万次阅读 多人点赞 2018-09-27 12:31:09
    、阐述工作版本的定义? ............................................................................................................................ 8 22 、什么是桩模块?什么是驱动模块? ..............
  • PLC编程基础

    万次阅读 多人点赞 2018-05-17 17:20:00
     (2)定义工程的设备条目。 (3)保存工程当一个新的PLC被添加到工程中的时候,将创建以下空表:1)空的本地符号表;2)全局符号表;3)IO表;4)PLC内存数据;5)PLC设置数据。2.编写一个梯形图程序下面以一个...
  • HTML标签常用标签

    千次阅读 多人点赞 2017-02-23 11:33:58
    标签定义超链接,用于从一张页面链接到另一张页面。 元素最重要的属性是 href 属性,它指示链接的目标。 下面是指向 w3school 的超链接 <a href="http://www.w3school.com.cn">W3School ...
  • 数据变化 一.Excel组成元素 二.Excel图表类型与用途 三.Excel可视化大全-应用 进入正题之前,先纠正一种误解。 有人觉得,基本图表太简单、太原始,不高端,不大气,因此追求更复杂的图表。但是,越简单的图表...
  • 前端图表制作——HighCharts图表入门

    千次阅读 2018-10-12 12:45:56
    HighCharts图表入门 HighCharts的作用 ​ 在JavaEE企业级项目开发中,很多项目都会用到数据的统计和图表的展示功能,如:各种股票系统,银行的资金结算,公司的财务报表,等等。如何快速高效的开发这些图表是一件...
  • NBIOT模块学习总结

    万次阅读 多人点赞 2018-08-15 10:58:15
    AT+CGDCONT 定义一个 PDP 上下文 B350+ AT+CFUN 设置终端功能 B350+ AT+CMEE 报告移动终端错误 B600+ AT+CCLK 返回当前日期和时间 B656+ AT+CPSMS 省电模式设置 B657SP1+ AT+CEDRXS eDRX 设置 B657SP1+ ...
  • CAXA CAPP工艺图表2020中文版

    千次阅读 2021-04-16 15:14:20
    CAXA CAPP2020是一款工艺图表软件,包含了图文混排、知识重用、工艺知识库、典型工艺、汇总统计等功能于一体,为用户提供最全面的工艺图表信息,与CAD系统的完美结合,全面集成了 CAXA CAD 电子图板,可完全按电子...
  • 图表-源码

    2021-02-25 08:30:12
    TrueNAS SCALE图表还具有三个附加文件,即app-readme.md文件(可在TrueNAS SCALE UI中提供高层概览显示)和questions.yaml文件(用于定义提示用户的问题)以及item.yaml文件概述特定项目细节。 有2个目录charts和...
  • 软件测试入门知识了解

    万次阅读 多人点赞 2018-09-05 14:59:58
    需求评审和设计评审是验证软件产品的需求定义和设计实现,验证所定义的产品特性是否符合客户的期望、系统的设计是否合理、是否具有可测试性以及满足非功能质量特性的要求。这个阶段主要通过对需求文档、设计文档等...
  • 之前一篇讲过动态图表主要有三种形式:数据透视表+切片器,vlookup+match+数据验证,以及控件+offset,之前是觉得数据透视表是最简单灵活的,但是近几天深入研究了几个控件的案例,发现控件也是有其不可比拟的优势的...
  • 接着帆软(FineReport)报表学习——一个简单的报表,新做一个带有图表的决策报表。 一、定义数据查询 首先定义数据查询totaldata,用条形图显示卖场数据: select dept,sum(amount) as amount from salesdata ...
  • Android 图表开发开源库MPAndroidChart

    万次阅读 2018-02-23 10:09:47
    支持自定义字体,颜色,背景,手势,虚线等 以 柱状图举使用: xml中直接定义 Activity中 初始化 protected Typeface mTfLight; mTfLight = Typeface.createFromAsset(getActivity().getAssets(), "OpenSans-Light...
  • ECharts 图表插件使用整理 说明 ECharts 是一个 JavaScript 实现的开源可视化库,兼容当前绝大部分浏览器 IE 8 以上,是一个可高度个性化定制的数据可视化图表 ECharts 版本 –&gt; 4.0.2 这篇文章...
  • 本节包含的详细练习将引导您完成与在数据视图中生成一些常用图表类型有关的步骤。所有练习都使用 Tableau Desktop 附带的 “Sample - Superstore” 数据源。为堆叠条添加合计将合计添加到图表中条形的顶部有时就像...
  • VBA 图表的基本操作(三)

    千次阅读 2020-08-14 22:28:30
    今天继续讲讲VBA图表的相关内容 一、删除图表 ①假设sheet1上面有个图表,要删除的话,代码如下: Sub demo() Sheets("Sheet1").ChartObjects(1).Delete End Sub ②如果是单独的一张Chart,要删除的话直接...
  •  1.1.4 名称的定义及应用 5  1.2 Excel函数的基本操作 7  1.2.1 函数的输入技巧 7  1.2.2 函数的种类 9  1.2.3 函数的参数类型 10  1.3 公式与函数应用技巧 11  1.3.1 复制公式 11  1.3.2 把公式转换为值 12...
  • 中文版Excel.2007图表宝典 2/2

    热门讨论 2012-04-06 19:01:36
    1.4.2 切换行和的方向/12 1.4.3 改变图表类型/13 1.4.4 应用图表布局/14 1.4.5 应用图表样式/15 1.4.6 添加和删除图表元素/15 1.4.7 格式化图表元素/16 1.5 对图表进行操作/17 1.5.1 移动图表和改变图表的大小/17 ...
  • 中文版Excel.2007图表宝典 1/2

    热门讨论 2012-04-06 18:49:24
    1.4.2 切换行和的方向/12 1.4.3 改变图表类型/13 1.4.4 应用图表布局/14 1.4.5 应用图表样式/15 1.4.6 添加和删除图表元素/15 1.4.7 格式化图表元素/16 1.5 对图表进行操作/17 1.5.1 移动图表和改变图表的大小/17 ...
  • 《数据库系统概论》复习

    千次阅读 多人点赞 2019-05-27 12:13:27
    基本关系的6条性质:同质、异可同域、行列无序性、候选码唯一性、分量原子性 2、关系模式 关系模式:关系的描述称为模式,它的形式化表达为R(U,D,DOM,F),其中R为关系名,U为属性名集合,D为属性域,DOM为...
  • tableau具有非常强大的自定义筛选功能,使得动态图表相当地酷炫。那Excel可以做出一样酷炫的动态图表吗?答案必须是个大大的Yes! 本文总结三种常用的动态图表制作方法:数据透视表+切片器,vlookup+match+数据验证,...
  • 集成学习—随机森林原理(理解与论文研读)

    千次阅读 多人点赞 2019-08-21 17:44:40
    此外,文末出来的参考视频,如果读论文还是不太懂个人觉得很有必要看看,会加深理解,因为读完论文再看了一遍也让我对之前脑海中的袋外估计、特征重要性等内容有了推掉重来的印象。 第一部分:理解 随机森林 ...
  • excel 交互式图表 创建一个交互式Excel图表 (Create an Interactive Excel Chart)If you add a chart to a dashboard, it’s helpful to let people choose what they’d like to see in the chart. In this example...
  • 技巧78 活用视图定义多视角的图表 技巧79 永恒的区域与多变的图表 技巧80 利用数据透视图轻松地创建动态图 技巧81 动态图的法宝“控件”与“名称” 第3章 高级图表 技巧82 断层图 技巧83 细分柱形图 技巧84 不等宽...
  • 本指南中出的指标包括简短描述及其单位及其适用的CDH版本。大多数单位是不言自明的。单位“CPU秒/秒”定义为每秒使用的CPU秒数。例如,如果您有一个具有16个核心的1个主机群集,并且每个任务使用一个核心的16个...
  • Android强大的图表开源——MPAndroidChart

    万次阅读 多人点赞 2017-02-09 23:54:07
    介绍在APP开发中遇到图表的样式,一般我们要先查询GitHub上比较火的开源框架,这种图标应用广泛,统计、游戏统计、人际关系图等等,用到今天的这个框架MPAndroidChart 点击查看GitHub 一个可以拖动缩放的图表库,...
  • 0x01数据收集 这里使用python爬取招聘信息存到数据库。这个脚本只负责收集,收集之后保存到数据库 为了加快爬取速度这里采用... po = Pool(2) # 定义一个进程池,最大进程数2 for i in range(0,len(job_list)): ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 45,490
精华内容 18,196
关键字:

列图表的定义