基于tensorflow的深层神经网络(三)如何用tensorflow优化神经网络

news2024/11/15 15:32:16

1、神经网络优化算法

梯度下降算法主要用户优化单个参数的取值,而反向传播算法给出了一个高效的方式在所有参数上使用梯度下降算法,从而使神经网络模型在训练数据上的损失函数尽可能小。反向传播算法是训练神经网络的核心算法,它可以根据定义好的损失函数优化神经网络中参数的取值,从而使神经网络在训练数据集上的损失函数达到一个最小值。神经网络模型中参数的优化过程直接决定了模型的质量,是使用神经网络时非常重要的一步。

假设用\theta表示神经网络中的参数,J(\theta)表示在给定参数取值下,训练数据集上损失函数的大小,那么整个优化过程可以抽象为寻找一个参数\theta,使得J(\theta)最小。因为没有一个通用方法可以对任意损失函数直接求解最佳的参数取值,所以在实践中,梯度下降算法是最常用的神经网络算法是最常用的神经网络优化方法。梯度下降算法会迭代更新参数\theta,不断沿着梯度的反方向让参数朝着总损失更小的方向更新,下图展示了梯度下降算法的原理。

                                                      

横轴表示参数\theta的取值,纵轴表示损失函数J(\theta)的值,上图表示了在参数\theta去不同值时,对应损失函数J(\theta)的大小。假设当前的参数和损失值对应上图箭头和曲线的交点,那么梯度下降算法会将参数向x轴左侧移动,从而使得损失值朝着箭头向方向移动。参数的梯度可以通过求偏导的方式计算,对于参数\theta,其梯度为\frac{\partial J(\theta)}{\partial \theta}。有了梯度,还需定义一个学习率\eta(learning rate)来定义每次参数更新的幅度。从直观上理解,可以认为学习率定义的就是每次参数移动的幅度。通过参数的梯度和学习率,参数更新公式为:

                                                                                      \theta_{n+1}=\theta_n-\eta \frac{\partial J(\theta_n)}{\partial \theta_n}

下面给出了一个具体的例子来说明梯度下降算法是如何工作的。假设要通过梯度下降算法来优化参数x,使得损失函数J(x)=x^2的值尽量小。梯度下降算法的第一步需要随机产生一个参数x的初始值,然后再通过梯度和学习率来更新参数x的取值。在这个样例中,参数x的梯度为\bigtriangledown = \frac{\partial J(x)}{\partial x}=2x,那么使用梯度下降算法每次对参数x的更新公式为x_{n+1}=x_n-\eta\bigtriangledown。假设参数的初始值为5,学习率为0.3,那么这个优化过程可以总结为下表,

轮数当前轮数值梯度\times学习率更新后参数值
152\times5\times0.3=35-3=2
222\times2\times0.3=1.22-1.2=0.8
30.82\times0.8\times0.3=0.480.8-0.48=0.32
40.322\times0.32\times0.3=0.1920.32-0.192=0.128
50.1282\times0.128\times0.3=0.07680.128-0.0768=0.0512

从上表中可以看出,经过5此迭代之后,参数x的指标成了0.0512,这个和参数是最优值0已经比较接近了。虽然这里给出的是一个非常简单的样例,但是神经网络的优化过程也可以类推的。神经网络的优化过程可以分为两个阶段:

  • 第一个阶段先通过前向传播算法计算得到预测值,并将预测值和真实值做对比得出两者之间的差距
  • 第二个阶段通过反向传播算法计算损失函数对每一个参数的梯度,再根据梯度和学习率使用梯度下降算法更新每一个参数

David Rumelhart、 Geoffrey Hinton和Ronald Williams教授发表的论文:Learning representations by back-propagating errors

梯度下降算法并不能保证被优化的函数达到全局最优解。如下图所示,图中给出的函数就有可能值能得到局部最优解而不是全局最优解。在圆球处,损失函数的偏导为0,于是参数就不会再进一步更新。在这个样例中,如果参数x的初始值落在右侧区间中,那么通过梯度下降得到的结果都会落在小球代表的局部最优解。只有当x的初始值落在左侧取件时梯度下降才能给出全局最优答案。由此可见在训练神经网络时,参数的初始值会很大程度影响最后得到的结果。只有当损失函数为凸函数时,梯度下降算法才能保证达到全局最优解。

                                     

除了不一定能达到全局最优,梯度下降算法的另一个问题就是计算时间太长。因为要在全部训练数据上最小化损失,所以损失函数J(\theta)是在所有训练数据上的损失和。这样在每轮迭代中都需要计算在全部训练数据上的损失函数。在海量的训练数据下,要计算所有训练数据的损失函数是非常耗时的。为了加速训练过程,可以使用随机梯度下降算法(stochastic gradient descent)。这个算法优化的不是全部训练数据上的损失函数,而是在每一轮迭代中,随机优化某一条训练数据上的损失函数。这样每一轮参数更新的速度就大大加快了。因为随机梯度下降算法每次优化的只是某一条数据上的损失函数,所以它的问题也非常明显:在某一条数据上损失函数更小并不代表在全部数据上损失函数更小,于是使用随机梯度下降优化得到的神经网络甚至可能无法达到局部最优。

为了综合梯度下降算法和随机梯度下降算法的优缺点,在实际应用中一般采用这两种算法的折中-------每次计算一小部分训练数据的损失函数。这一小部分数据被称之为一个batch。通过矩阵运算,每次在一个batch上优化神经网络的参数并不会比单个数据慢太多。另一方面,每次使用一个batch可以大大减少收敛所需要的迭代次数,同时可以使收敛到的结果更加接近梯度下降的效果。以下代码给出了tensorflow中如何实现神经网络的训练过程。

batch_size = n

# 每次读取读取一小部分数据作为当前的训练数据来执行反向传播算法
x = tf.placeholder(tf.float32, shape = (batch_size, 2), name = 'x-input')
y = tf.placeholder(tf.float32, shape = (batch_size, 1), name = 'y-input')

# 定义神经网络结构和优化算法
loss = ...
train_step = tf.train.Adamoptimizer(0.001).minimize(loss)

# 训练神经网络
with tf.Session( ) as sess:
   # 参数初始化
...

# 迭代的更新参数

for i in range(STEPS):
    # 准备batch_size个训练数据。一般将所有训练数据随机打乱之后再选取可以得到
    # 更好的优化过程
    current_x, current_y = ...
    sess.run(train_step, feed_dict = {x: current_X, y_: current_Y})

2、学习率设置

在训练神经网络时,需要设置学习率(learning rate)控制参数更新的速度。学习率决定了参数每次更新的幅度,如果幅度过大,那么可能导致参数在极优值的两侧开回移动当优化J(x)=x^2 函数的样例时,如果在优化中使用学习率为1,那么整个优化过程如下表

轮数当前轮参数值梯度\times学习率更新后参数值
152\times5\times1=105-10\approx-5
2-52\times(-5)\times1=-10-5-(-10)=5
352\times5\times1=105-19=-5

 从以上样例可以看出,无论进行多少轮迭代,参数将在5和-5之间摇摆,而不会收敛到一个极小值。相反,当学习率过小时,虽然能保证收敛性,但这会大大降低优化速度。我们会需要更多轮的迭代才能达到一个比较理想的优化效果。比如当学习率为0.001时,迭代5之后,x的值将为4.95。要将训练到0.05需要2300轮;二当学习率为0.3时,只需要5轮可以达到。综上所述,学习率既不能过大, 也不能过小。为了解决学习率的问题,TensorFlow提供了一种更加灵活的学习率设置方法------指数衰减法,tf.train.exponential_decay函数实现了指数衰减学习率。通过这个函数,可以使用较大的学习率快速得到一个比较优的解,然后随着迭代的继续逐步减少学习率,使得模型在训练后期更加稳定。exponential_decay函数会指数级地减少学习率,它实现了以下代码的功能:

decayed_learning_rate = \ learning_rate * decay_rate ^ (global_step / decay_steps)

decayed_learning_rate为每一轮优化时使用的学习率,learning_rate为事先设定的初始学习率,decay_rate为衰减系数,decay_steps为衰减速度。

tf.train.exponential_decay函数可以通过设置参数staircase选择不同的衰减方式。当staircase被设置为True时,global_step/decay_steps会被转化成整数。这使得学习率成为一个阶梯函数(staircase function)。

在这样的设置下,decay_steps通常代表了完整的使用一遍训练数据所需要的迭代轮数。这个迭代轮数也就是总训练样本数除以每一个batch中的训练样本数。这种设置的常用场景是每完整地过一遍训练数据,学习率就减少一次。这可以使得训练数据集中的所有数据对模型训练有相等的作用。当使用连续的指数衰减学习率时,不同训练数据有不同的学习率,而当学习率减少时,对应的训练数据对模型训练结果的影响也就小了。

global_step = tf.Variable(0)

# 通过exponential_decay函数生成学习率
learning_rate = tf.train.exponential_decay(0.1, global_step, 100, 0.96 , staircase=True)


# 使用指数衰减的学习率,在minimize函数中传入global_step将自动更新
# global_step参数,从而使学习率也得到相应更新
learning_step = tf.train.GradientDescentOptimizer(learning_rate)\
                .minimize(...my loss..., global_step = global_step)

上面这段代码中设定了初始学习率为0.1,因为指定了staircase=True,所以每训练100轮后学习率乘以0.96。一般来说初始学习率、衰减系数和衰减速度都是根据经验设置的,而且损失很熟下降的速度和迭代结束之后总损失的大小没有必然的联系。也就是说并不能通过前几轮损失函数下降的速度来比较不同神经网络的效果。

3、过拟合问题

在真实的应用中想要的并不是让模型尽量模拟训练数据的行为,而是希望通过训练出来的模型对未知的数据给出判断。模型在训练数据上的表现并不一定代表了它在未知数据上的表现。过拟合问题就是可以导致这个差距的一个很重要因素。所谓过拟合,指的是当一个模型过于复杂后,它可以很好地“记忆”每一个训练数据中随机噪声的部分而忘记了要去“学习”训练数据中通用的趋势。举一个极端例子,如果一个模型中的参数比训练数据的总数还要多,那么只要训练数据不冲突,这个模型完全可以记住所有训练数据的结果从而使得损失函数为0。可以直观地相像一个包含n个变量和n个等式的方程组,当方程不冲突时,这个方程组是可以通过数学的方法来求解的。然而,过度拟合训练数据中的随机噪声虽然可以得到非常小的损失函数,但是对于未知数据可能无法做出可靠的判断。

为了避免过拟合问题,一个非常有用的方法是正则化(regularization)。正则化的思想就是在损失函数中加入刻画模型复杂度程序的指标。假设用于刻画模型在训练数据上表现损失函数为J(\theta),那么在优化时不是直接优化J(\theta),而是优化J(\theta)+\lambda R(w)。其中R(w)刻画的是模型的复杂度,而\lambda表示模型复杂损失在总损失中的比例。注意这里\theta表示的是一个神经网络的所有参数,它包括边上的权重w和偏置b。一般来说模型复杂度只由权重w决定。常用的刻画模型复杂度的函数R(w)有两种:

一种是L1正则化,计算公式是:

                                                                    R(w)=\parallel w\parallel_1=\sum_i|w_i|

另一种是L2正则化,计算公式是

                                                                  R(w)=\parallel w\parallel_2^2=\sum_i|w_i^2|

无论是哪一种正则化方式,基本的思想都是希望通过限制权重的大小,使得模型不能任意拟合训练数据中的随机噪声。但这两种正则化的方法也有很大的区别。首先,L1正则化会让参数变得更稀疏,而L2正则化不会。之所以L2正则化不会让参数变得稀疏的原因是当参数很小时,比如0.001,这个参数的平方基本上就可以忽略了,于是模型不会进一步将这个参数调整为0。其次,L1正则化的计算公式不可导,而L2正则化公式可导。因为在优化时需要计算损失函数的偏导数,所以对含有L2正则化损失函数的优化要更加简洁。优化带L1正则化的损失函数要更加复杂,而且优化方法也有很多种。在实践中,也可以将L1正则化和L2正则化同时使用:

                                                                  R(w)=\sum_i(\alpha|w_i|+(1-\alpha)w_i^2)

以下代码给出了一个简单的带L2正则化的损失函数定义:

w = tf.Variable(tf.random_normal([2, 1], stddev = 1, seed = 1))
y = tf.matmul(x, w)

loss = tf.reduce_mean(tf.square(y_ - y )) + tf.contrib.layers.12_regularizer(lambda)(w)

在以上程序中,loss为定义的损失函数,它由两部分组成。第一个是均方误差损失函数,它刻画了模型在训练数据上的表现。第二个部分就是正则化,它防止模型过度拟合训练数据中的随机噪声。lambda参数表示了正则化项的权重,也就是公式J(\theta)+\lambda R(w)中的\lambda。w为需要计算正则化损失的函数。tensorflow提供了tf.contrib.layers.l2_regularizer函数,它可以返回一个函数,这个函数可以计算一个给定参数的L2正则化的值。类似的,tf.contrib.layer.l1_regularizer可以计算L1正则化的值。以下代码给出了使用这两个函数的样例:

weights = tf.constant([[1.0, -2.0], [-3.0, 4.0]])
with tf.Session( ) as sess:
   # 输出为(|1| + |-2| + |-3| + |4|) * 0.5=5,其中0.5为正则化项的权重。
   print sess.run(tf.contrib.layers.l1_regulrilzer(.5)(weights))
   # 输出为(1^2 +(-2)^2 + (-2)^2 + 4^2)/2 * 0.5 = 7.5
   print sess.run(tf.contrib.layers.l2_regularizer(.5)(weights))

在简单的神经网络中,这样的方式就可以很好的计算带正则化的损失函数了。但当神经网络的参数增多之后,这样的方式首先可能导致损失函数loss的定义很长,可读性差且容易出错但更主要的是,当神经网络结构复杂之后定义网络结构的部分和计算损失函数大的部分可能不在同一函数中,这样通过变量这种方式计算损失函数就不方便了。为了解决这个问题,可以使用tensorflow中给提供的集合(collection)。它可以在一个计算图(tf.Graph)中保存一组实体(比如张量)。以下代码给出了通过集合计算一个五层神经网络带L2正则化的损失函数的计算方法。

import tensorflow as tf


# 获取一层神经网络边上的权重,并将这个权重的L2正则化损失加入名称为'losses'的集合中
def get_weight(shape, lambda1):
     # 生成一个变量
     var = tf.Variable(tf.random_normal(shape), dtype = tf.float32)
     # add_to_collection函数将这个新生成变量的L2正则化损失项加入集合。
     # 这个函数的第一个参数'losses'是集合的名字,第二个参数是要加入这个集合的内容
     tf.add_to_collection(
         'losses', tf.contrib.layers.l2_regularizer(lambda1)(var))
     return var



x = tf.placeholder(tf.float32, shape=(None, 2))
y_ = tf.placeholder(tf.float32, shape=(None, 1))
batch_size = 8



# 定义了每一层网络中节点的个数
layer_dimension = [2, 10, 10, 10, 1]

# 神经网络的层数
n_layers = len(layer_dimension)


# 这个变量维护前向传播时最深层的节点,开始的时候就是输入层
cur_layer = x
# 当前层的节点个数
in_dimension = layer_dimension[0]


# 通过一个循环来生成5层全连接神经网络结构
for i in range(1, n_layers):
      # layer_dimension[i]为下一层的节点个数
      out_dimension = layer_dimension[i]
      # 生成当前层中权重的变量,并将这个变量的L2正则化损失加入到计算图上的集合。
      weight = get_weight([in_dimension, out_dimension], 0.001)
      bias = tf.Variable(tf.constant(0.1, shape=[out_dimension]))
      # 使用ReLU激活函数
      cur_layer = tf.nn.relu(tf.matmul(cur_layer, weight) + bias)
      # 进入下一层之前将下一层的节点个数更新为当前层节点个数
      in_dimension = layer_dimension[i]

# 在定义神经网络前向传播的同时已经将所有L2正则化损失加入了图上的集合
# 这里只需要计算刻画模型在训练数据上表现的损失函数
mse_loss = tf.reduce_mean(tf.square(y_ - cur_layer))

# 将均方误差损失函数加入损失集合
tf.add_to_collection('losses', mse_loss)

# get_collection返回一个列表,这个列表是所有这个集合中的元素。在这个样例中,
# 这些元素就是损失函数的不同部分,将它们加起来就可以得到最终的损失函数。
loss = tf.add_n(tf.get_collection('losses'))

从上面代码可以看出通过使用集合的方法在网络结构比较复杂的情况下可以使代码的可读性更高。以上代码给出的是一个只有5层的全连接网络,在更复杂的网络结构中,使用这样的方式来计算损失函数将大大增强代码的可读性。

4、滑动平均模型:

滑动平均模型可以使模型在测试数据上更健壮(robust)的方法------滑动平均模型。在采用随机梯度下降算法训练神经网络时,使用滑动平均模型在很多应用中都可以在一定程度提高最终模型在测试数据上的表现。

在tensorflow中提供了tf.train.ExponentialMovingAverage来实现滑动平均模型。在初始化ExponentialMovingAverage时,需要提供一个衰减率(decay)。这个衰减率将用于控制模型更新的速度。ExponentialMovingAverage对每一个变量会维护一个影子变量(shadow variable),这个影子变量的初始值就是响应变量的初始值,而每次运行变量更新时,影子变量的值会更新为:

                                      shadow_variable = decay*shadow_variable + (1 - decay)*variable

其中shadow_variable为了影子变量,variable为待更新的变量,decay为衰减率。从公式中可以看到,decay决定了模型更新的速度,decay越大模型越趋于稳定。在实际应用中,decay一般会设成非常接近1的数(比如0.999或0.9999)。为了使得模型在训练前期可以更新得更快,ExponentialMovingAverage还体用了num_unpdates参数来动态设置decay的大小。如果在ExponentialMovingAverage初始化时提供了num_updates参数,那么每次使用的衰减率将是:

                                                               min\{ decay, \frac{1+num\_updates}{10+num\_updates}\}

下面通过一段代码来解释ExponentialMovingAverage是如何被使用的。

import tensorflow as tf 


# 定义一个变量用于计算滑动平均,这个变量的初始值为0,注意这是手动指定了变量的
# 类型为tf.float32,因为所有需要计算滑动平均的变量必须是实数型。
v1 = tf.Variable(0, dtype = tf.float32)


# 这里step变量模拟神经网络中迭代的次数,可以用于动态控制衰减率
step = tf.Variable(0, trainable=False)



# 定义一个滑动平均的类(class)。初始化时给定了衰减率(0.99)和控制衰减率的变量step,
ema = tf.train.ExponentialMovingAverage(0.99, step)



# 定义一个更新变量滑动平均的操作。这里需要给定一个列表,每次执行这个操作时
# 这个列表中的变量都会被更新。
maintain_average_op = ema.apply([v1])



with tf.Session() as sess:
   # 初始化所有变量
   init_op = tf.global_variable_initializer()
   sess.run(init_op)
  
   # 通过ema.average()获取滑动平均之后变量的取值。在初始化之后变量v1的值和v1的
   # 滑动平均都为0。
   print sess.run([v1, ema.average(v1)])



   # 更新变量v1的值到5。
   sess.run(tf.assign(v1, 5))
   # 更新v1的滑动平均值。衰减率为min{0.99, (1+step)/(10+step) = 0.1}= 0.1,
   # 所以v1的滑动平均会被更新为0.1*0 + 0.9*5 = 4.5。
   see.run(maintain_average_op)
   print sess.run([v1, ema.avergae(v1)])
   #  输出[5.0, 4.5]


   # 更新step的值为1000。
   sess.run(tf.assign(step, 10000))

   # 更新v1的值为10。
   sess.run(tf.assign(v1 ,10))

   # 更新v1的滑动平均值。衰减率为min{0.99, (1 + step)/(10 + step)约等于0.999}=0.99,
   # 所以v1的滑动平均被更新为0.99*4.5 + 0.01*10 = 4.555
   sess.run(maintain_average_op)
   print sess.run([v1, ema.average(v1)])
   # 输出[10.0, 4.5549998]


   # 再次更新滑动平均值,得到的滑动平均值为0.99*4.555 + 0.01 * 10 = 4.60945
   sess.run(maintain_average_op)
   print sess.run([v1, ema.average(v1)])
   # 输出[10.0, 4.6094499]

以上代码给出了ExponentialMovingAverage的简单样例。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/78822.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

红黑树的插入过程

一棵红黑树是一种特殊的二叉查找树,具有以下性质: 每个节点要么是红色,要么是黑色。根节点是黑色。每个叶子节点(NIL)是黑色。如果一个节点是红色的,那么它的两个儿子都是黑色的。从任意一个节点到其每个叶…

71.qt quick-可伸展菜单-抽屉栏示例 通用QML界面(一键换肤)

在我们之前章节已经提供过了抽屉栏和菜单伸展栏: 63.qt quick-QML侧边滑动栏(不需要任何图片资源,支持自定义左右方向和大小)_诺谦的博客-CSDN博客_qml侧边栏68.qt quick-qml多级折叠下拉导航菜单 支持动态添加/卸载 支持qml/widget加载等_诺谦的博客-CSDN博客_qml下拉菜单 由…

三维家发生工商变更:注册资本减少46%,美凯龙、阿里等股东退出

近日,云工业软件服务商广东三维家信息科技有限公司(下称“三维家”)发生工商变更,注册资本由16.9254亿元变更为9亿元,同比减少46.83%。同时,包括红星美凯龙、阿里巴巴等多名股东退出,变更时间为…

01.Spring源码整体脉络介绍及源码编译——四

IOC是核心 IOC 容器加载过程【重要】:所有模块都依赖IOC,aop,循环依赖都依赖IOC IOC控制反转,控制理念,来解决层与层之间的耦合。DI注入实现 怎么讲Bean交给IOC容器来管理 配置类xml,注解 加载spring上下…

java计算机毕业设计ssm学院校友信息管理系统的设计与实现5yqhy(附源码、数据库)

java计算机毕业设计ssm学院校友信息管理系统的设计与实现5yqhy(附源码、数据库) 项目运行 环境配置: Jdk1.8 Tomcat8.5 Mysql HBuilderX(Webstorm也行) Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts…

【Java基础篇】基础知识易错集锦(二)

我们同样用几道例题来回顾相对应的基础知识; 解析: 首先我呢区分一下实例变量和局部变量; 局部变量:定义在方法内部的变量;实例变量:定义在类中但在任何方法之外,你也可以理解为全局变量&…

16.C预处理器和C库

文章目录C预处理器和C库16.1翻译程序的第一步16.2明示常量:#define16.3在#define中使用参数16.3.1用宏参数创建字符串:#运算符16.3.2预处理器黏合剂:##运算符16.3.3变参宏:...和__VA_ARGS__16.4宏和函数的选择16.5文件包含&#x…

NCTF2022 calc题目复现

calc(环境变量注入getshell) 经典计算器题目,看着有点眼熟,没错,就是buu三月赛的一道题目。由于那时候web可能都算不上入门,所以也就没有复现。比赛时就网上看了看三月赛的wp,但是没有什么用&a…

IEEE 二进制浮点数的表示

今天,我来将 IEEE 二进制浮点数的表示方式进行一个简单的介绍。 浮点数 在 C 语言中,有两种存储浮点数的方式,分别是 float 和 double ,当然了还有long double。这几种浮点型所容纳的长度不同,当然它们存储的精度也就…

[附源码]JAVA毕业设计新型药物临床信息管理系统(系统+LW)

[附源码]JAVA毕业设计新型药物临床信息管理系统(系统LW) 项目运行 环境项配置: Jdk1.8 Tomcat8.5 Mysql HBuilderX(Webstorm也行) Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。 …

p5.第一章 Python基础入门 -- 运算符、优先级和表达式 (五)

1.2.3.2.11 False等价 False等价布尔值,相当于bool(value) 空容器 空集合set空字典dict空列表list空元组tuple空字符串None0# bool(value)是布尔函数# In: bool(1), bool(0) # Out: (True

离散数学·支配集、覆盖集、独立集和匹配

支配集 简而言之——V-支配集后剩下的点,都能在支配集中找到相邻的点 支配数的符号是γ0(有关点的集,下标为0) 例 右下角相同颜色的为同一个支配集 要注意极小性 整个V就是支配集(所以说支配集找极大没有意义&#xf…

测试员凡尔赛,工作三年晒出11月工资条,直言加班太累了

最近有工作3年的测试员晒出自己11 月份的工资条,并直言加班太累了。 从工资条上可以看到,这个收入确实不算低,才3年时间,月工资就已经到了二万五了,这个工资已经可以击败绝大多数行业了。 不过二万五只是税前工资&am…

第二证券|系统性稳地产政策加力 租购并举制度加快建立

在房地产职业深度调整期,下一年方针走向备受关注。虽然日前召开的中心政治局会议没提及房地产,可是从其对经济方针的表述能够预见,作为经济支柱产业的房地产职业,下一年将在稳经济中发挥更重要的效果,国家将持续出台系…

[附源码]Python计算机毕业设计SSM基于的装修公司运营管理管理系统(程序+LW)

项目运行 环境配置: Jdk1.8 Tomcat7.0 Mysql HBuilderX(Webstorm也行) Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。 项目技术: SSM mybatis Maven Vue 等等组成,B/S模式 M…

一文彻底搞懂ssh的端口转发

文章目录背景什么是端口转发?本地端口转发本地端口转发的语法场景1场景二ssh -L参数解释ssh 远程端口的安全问题远程端口转发远程端口转发的语法场景一远程端口转发和本地端口转发要在哪台服务器上执行场景二ssh -R 参数解释端口转发的选项端口转发需要修改哪些ssh配…

fastdfs部署详解

fastdfs部署 官方github支持 官方解释:FastDFS 是一个开源的高性能分布式文件系统。它的主要功能包括:文件存储、文件同步和文件访问(文件上传和文件下载),它可以解决高容量和负载均衡问题。FastDFS应该能满足图片分享…

10 款开源工具

1. JIRA 2. Git 3. Jenkins 4. Selenium 5. Groovy 6、Spock 7. Maven 8. Gradle 9. Docker 10. Linux 本文主要介绍Java程序员应该在2019年学习的一些基本和高级工具。如果你是一位经验丰富的Java开发人员,你可能对这些工具很熟悉,但如果不是&…

Java 并发编程<13>-ThreadPoolExecutor的springboot应用

Java 并发编程<13>-ThreadPoolExecutor的springboot应用 Java并发编程<10>安全集合 ...... Java 并发编程<1>-线程实现的方式 线程池简介 a .为什么使用线程池 降低系统资源消耗&#xff0c;通过重用已存在的线程&#xff0c;降低线程创建和销毁造成的消…

基于Surf+GTM的图像配准和拼接算法matlab仿真

目录 1.算法描述 2.仿真效果预览 3.MATLAB核心程序 4.完整MATLAB 1.算法描述 SIFT采用的是DoG图像&#xff0c;而SURF采用的是Hessian矩阵&#xff08;SURF算法核心&#xff09;行列式近似值图像。在数学中&#xff0c;Hessian矩阵是一个自变量为向量的实值函数的二阶偏导数…