概述

本文将初步介绍如何使用TensorFlow进行编程。在阅读之前请先 安装TensorFlow,此外为了能够更好的理解本文的内容,阅读之前需要了解一点以下知识:

python基本编程。能看得懂python代码,最好能使用脚本工具或pycharm之类的IDC编写代码。
至少有一点数组的概念。
最理想的状态是具备机器学习的基础知识。不过如果在阅读之前没有了解过任何机器学习相关的知也无大碍,可以把本文作为了解机器学习的开端。后面会另开一篇用MNIST了解机器学习的基础知识。
TensorFlow提供种类繁多的API接口,其中TensorFlow Core是最低层级的接口,为开发TensorFlow提供基础支持。官方推荐把TensorFlow Core用作机器学习研究以及相关的数据建模。除了TensorFlow Core之外还有更高抽象的API接口,这些API接口比TensorFlow Core更易于使用、更易于快速实现业务需求。例如 tf.contrib.learn 接口,它提供管理数据集合、进行数据评估、训练、推演等功能。在使用TensorFlow开发的过程中需要特别注意,以 contrib 开头的API接口依然还在不断完善中,很有可能在未来某个发行版本中进行调整或者直接取消。

本文首先介绍TensorFlow Core,然后会演示如何使用 tf.contrib.learn 实现简单的建模。了解TensorFlow Core是为了让开发者理解在使用抽象接口时底层是如何工作的,以便于在训练数据时创建更合适的模型。

TensorFlow

TensorFlow的基础数据单元是张量(tensor)。一个张量认为是一组向量的集合,从数据结构的角度来理解这个集合等价于一组数值存储在1到多个队列中(张量没办法几句话说得清楚,想要了解去谷哥或者度妞搜索“张量分析”,可以简单想象成一个多维度的数组)。一个张量的阶表示了张量的维度,下面是一些张量的例子:

1
2
3
4
3 # 0阶张量,可以用图形[]来表示
[1. ,2., 3.] # 1阶张量,是一个图形为[3]的向量
[[1., 2., 3.], [4., 5., 6.]] # 2阶张量,是一个图形为[2,3]的矩阵
[[[1., 2., 3.]], [[7., 8., 9.]]] # 图形为[2,1,3]的三阶张量

TensorFlow Core教程

导入TensorFlow

下面是导入TensorFlow包的标准方式:

1
import tensorflow as tf

通过python的方式导入之后, tf 提供了访问所有TensorFlow类、方法和符号的入口。

图计算(Computational Graph)

TensorFlow Core的编程开发可以看就做2个事:

构建计算图。(建模)
运行计算图。(执行)
图(graph,也可以叫连接图)表示由多个点链接而成的图。本文中的图指的是TensorFlow建模后运算的路径,可以使用TensorBoard看到图的整个形态。

节点(node)表示图中每一个点,这些点都代表了一项计算任务。

所以简而言之:编程 TensorFlow Core 就是事先安排好一系列节点的计算任务,然后运行这些任务。

下面我们先构建一个简单的图,图中的节点(node)有0或多个张量作为输入,并产生一个张量作为输出。一个典型的节点是“常量”(constant)。TensorFlow的常量在构建计算模型时就已经存在,在运行计算时并不需要任何输入。下面的代码创建了2个浮点常量值常量 node1 和 node2:

1
2
3
node1 = tf.constant(3.0, tf.float32)
node2 = tf.constant(4.0) # also tf.float32 implicitly
print(node1, node2)

运行后会打印输出:

1
Tensor("Const:0", shape=(), dtype=float32) Tensor("Const_1:0", shape=(), dtype=float32)

观察这个打印的结果会发现,它并不是按照预想的那样输出 3.0 或 4.0 的值。这里输出的是一个节点的对象信息。因为到这里还没有执行第二项工作——运行计算模型图。只有在运行时,才会使用到节点真实的值 3.0 和4.0。为了进行图运算需要创建一个会话(session),一个会话封装了TensorFlow运行库的各种控制方法和状态量(context)。

下面的代码会创建一个会话(session)对象实例,然后执行 run 方法来进行模型计算:

1
2
sess = tf.Session()
print(sess.run([node1, node2]))

运行后我们会发现,打印的结果是3.0和4.0:

1
[3.0, 4.0]

然后,对 node1 和 node2 进行和运算,这个和运算就是图中的运算模型。下面的代码是构建一个 node1 、 node2 进行和运算, node3 代表和运算的模型,构建完毕后使用 sess.run 运行:

1
2
3
node3 = tf.add(node1, node2)
print("node3: ", node3)
print("sess.run(node3): ",sess.run(node3))

运行后会输出了以下内容:

1
2
node3:  Tensor("Add_2:0", shape=(), dtype=float32)
sess.run(node3): 7.0

到此,完成了TensorFlow创建图和执行图的过程。

前面提到TensorFlow提供了一个名为TensorBoard的工具,这个工具能够显示图运算的节点。下面是一个TensorBoard可视化看到计算图的例子:

这样的常量运算结果并没有什么价值,因为他总是恒定的产生固定的结果。图中的节点能够以参数的方式接受外部输入——比如使用占位符。占位符可以等到模型运行时再使用动态计算的数值:

1
2
3
a = tf.placeholder(tf.float32)
b = tf.placeholder(tf.float32)
adder_node = a + b # + 可以代替tf.add(a, b)构建模型

上面这3行代码有点像用一个function或者一个lambda表达式来获取参数输入。我们可以在运行时输入各种各样的参数到图中进行计算:

1
2
print(sess.run(adder_node, {a: 3, b:4.5}))
print(sess.run(adder_node, {a: [1,3], b: [2, 4]}))

输出结果为:

1
2
7.5
[ 3. 7.]

TensorBoard中,显示的计算图为:

我们可以使用更复杂的表达式来增加计算的内容:

1
2
add_and_triple = adder_node * 3.
print(sess.run(add_and_triple, {a: 3, b:4.5}))

计算输出:

1
22.5

TensorBoard中的显示:

在机器学习中一个模型通常需要接收各种类型的数据作为输入。为了使得模型可以不断的训练通常需要能够针对相同的输入修改图的模型以获取新的输出。变量(Variables)可以增加可训练的参数到图中,他们由指定一个初始类型和初始值来创建:

1
2
3
4
W = tf.Variable([.3], tf.float32)
b = tf.Variable([-.3], tf.float32)
x = tf.placeholder(tf.float32)
linear_model = W * x + b

前面已经提到在调用 tf.constant 时会初始化不可变更的常量。 而这里通过调用 tf.Variable 创建的变量不会被初始化,为了在TensorFlow运行之前(sess.run执行模型运算之前)初始化所有的变量,需要增加一步 init 操作:

1
2
init = tf.global_variables_initializer()
sess.run(init)

可以通过重载 init 方式来全局初始化所有TensorFlow图中的变量。在上面的代码中,在我们调用 sess.run 之前,所有的变量都没有初始化。

下面的 x 是一个占位符,{x:[1,2,3,4]} 表示在运算中把x的值替换为[1,2,3,4]:

1
print(sess.run(linear_model, {x:[1,2,3,4]}))

输出:

1
[ 0.          0.30000001  0.60000002  0.90000004]

现在已经创建了一个计算模型,但是并不清晰是否足够有效,为了让他越来越有效,需要对这个模型进行数据训练。下面的代码定义名为 y 的占位符来提供所需的值,然后编写一个“损益功能”(loss function)。

一个“损益功能”是用来衡量当前的模型对于想达到的输出目标还有多少距离的工具。下面的例子使用线性回归作为损益模型。回归的过程是:计算模型的输出和损益变量(y)的差值,然后再对这个差值进行平方运算(方差),然后再把方差的结果向量进行和运算。下面的代码中, linear_model - y 创建了一个向量,向量中的每一个值表示对应的错误增量。然后调用 tf.square 对错误增量进行平方运算。最后将所有的方差结果相加创建一个数值的标量来抽象的表示错误差异,使用 tf.reduce_sum来完成这一步工作。如下列代码:

1
2
3
4
5
6
7
8
# 定义占位符
y = tf.placeholder(tf.float32)
# 方差运算
squared_deltas = tf.square(linear_model - y)
# 定义损益模型
loss = tf.reduce_sum(squared_deltas)
# 输出损益计算结果
print(sess.run(loss, {x:[1,2,3,4], y:[0,-1,-2,-3]}))

运算之后的差异值是:

1
23.66

可以通过手动将 W 和 b 的值修改为-1和1降低差异结果。TensorFlow中使用 tf.Variable 创建变量,使用 tf.assign 修改变量。例如 W=-1 、b=1 才是当前模型最佳的值,可以像下面这样修改他们的值:

1
2
3
4
fixW = tf.assign(W, [-1.])
fixb = tf.assign(b, [1.])
sess.run([fixW, fixb])
print(sess.run(loss, {x:[1,2,3,4], y:[0,-1,-2,-3]}))

修改之后的最终输出结果为:

1
0.0
tf.train 接口

机器学习的完整过程超出了本文的范围,这里仅说明训练的过程。TensorFlow提供了很多优化器来逐渐(迭代或循环)调整每一个参数,最终实现损益值尽可能的小。最简单的优化器之一是“梯度递减”(gradient descent),它会对损益计算模型求导,然后根据求导的结果调整输入变量的值(W和b),最终目的让求导的结果逐渐趋向于0。手工进行编写求导运算非常冗长且容易出错,TensorFlow还提供了函数 tf.gradients 实现自动求导过程。下面的例子展示了使用梯度递减训练样本的过程:

1
2
3
4
5
6
7
8
# 设定优化器,这里的0.01表示训练时的步进值
optimizer = tf.train.GradientDescentOptimizer(0.01)
train = optimizer.minimize(loss)
sess.run(init) # 初始化变量值.
for i in range(1000): # 遍历1000次训练数据,每次都重新设置新的W和b值
sess.run(train, {x:[1,2,3,4], y:[0,-1,-2,-3]})

print(sess.run([W, b]))

这个模式的运算结果是:

1
[array([-0.9999969], dtype=float32), array([ 0.99999082], dtype=float32)]

现在我们已经完成机器学习的整个过程。虽然进行简单的线性回归计算并不需要用到太多的TensorFlow代码,但是这仅仅是一个用于实例的案例,在实际应用中往往需要编写更多的代码实现复杂的模型匹配运算。TensorFlow为常见的模式、结构和功能提供了更高级别的抽象接口。

一个完整的训练过程

下面是根据前文的描述,编写的完整线性回归模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import numpy as np
import tensorflow as tf

# 模型参数
W = tf.Variable([.3], tf.float32)
b = tf.Variable([-.3], tf.float32)
# 模型输入
x = tf.placeholder(tf.float32)
# 模型输出
linear_model = W * x + b
# 损益评估参数
y = tf.placeholder(tf.float32)
# 损益模式
loss = tf.reduce_sum(tf.square(linear_model - y)) # 方差和
# 优化器
optimizer = tf.train.GradientDescentOptimizer(0.01)
train = optimizer.minimize(loss)
# 训练数据
x_train = [1,2,3,4]
y_train = [0,-1,-2,-3]
# 定义训练的循环
init = tf.global_variables_initializer()
sess = tf.Session()
sess.run(init) # reset values to wrong
for i in range(1000):
sess.run(train, {x:x_train, y:y_train})

# 评估训练结果的精确性
curr_W, curr_b, curr_loss = sess.run([W, b, loss], {x:x_train, y:y_train})
print("W: %s b: %s loss: %s"%(curr_W, curr_b, curr_loss))

运行后会输出:

1
W: [-0.9999969] b: [ 0.99999082] loss: 5.69997e-11

这个复杂的程序仍然可以在TensorBoard中可视化呈现:

tf.contrib.learn

前面已经提到,TensorFlow除了TensorFlow Core之外,为了便于业务开发还提供了很多更抽象的接口。tf.contrib.learn 是TensorFlow的一个高级库,他提供了更加简化的机器学习机制,包括:

运行训练循环
运行评估循环
管理数据集合
管理训练数据
tf.contrib.learn 定义了一些通用模块。

基本用法

先看看使用 tf.contrib.learn 来实现线性回归的方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import tensorflow as tf
# NumPy常用语加载、操作、预处理数据.
import numpy as np

# 定义一个特性列表features。
# 这里仅仅使用了real-valued特性。还有其他丰富的特性功能
features = [tf.contrib.layers.real_valued_column("x", dimension=1)]

# 一个评估者(estimator)是训练(fitting)与评估(inference)的开端。
# 这里预定于了许多类型的训练评估方式,比如线性回归(linear regression)、
# 逻辑回归(logistic regression)、线性分类(linear classification)和回归(regressors)
# 这里的estimator提供了线性回归的功能
estimator = tf.contrib.learn.LinearRegressor(feature_columns=features)

# TensorFlow提供了许多帮助类来读取和设置数据集合
# 这里使用了‘numpy_input_fn’。
# 我们必须告诉方法我们许多多少批次的数据,以及每次批次的规模有多大。
x = np.array([1., 2., 3., 4.])
y = np.array([0., -1., -2., -3.])
input_fn = tf.contrib.learn.io.numpy_input_fn({"x":x}, y, batch_size=4,
num_epochs=1000)

# ‘fit’方法通过指定steps的值来告知方法要训练多少次数据
estimator.fit(input_fn=input_fn, steps=1000)

# 最后我们评估我们的模型价值。在一个实例中,我们希望使用单独的验证和测试数据集来避免过度拟合。
estimator.evaluate(input_fn=input_fn)

运行后输出:

1
{'global_step': 1000, 'loss': 1.9650059e-11}
自定义模型

tf.contrib.learn 并不限定只能使用它预设的模型。假设现在需要创建一个未预设到TensorFlow中的模型。我们依然可以使用tf.contrib.learn保留数据集合、训练数据、训练过程的高度抽象。我们将使用我们对较低级别TensorFlow API的了解,展示如何使用LinearRegressor实现自己的等效模型。

使用 tf.contrib.learn 创建一个自定义模型需要用到它的子类 tf.contrib.learn.Estimator 。而 tf.contrib.learn.LinearRegressortf.contrib.learn.Estimator 的子类。下面的代码中为 Estimator 新增了一个 model_fn 功能,这个功能将告诉 tf.contrib.learn 如何进行评估、训练以及损益计算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import numpy as np
import tensorflow as tf
# 定义一个特征数组,这里仅提供实数特征
def model(features, labels, mode):
# 构建线性模型和预设值
W = tf.get_variable("W", [1], dtype=tf.float64)
b = tf.get_variable("b", [1], dtype=tf.float64)
y = W*features['x'] + b
# 损益子图
loss = tf.reduce_sum(tf.square(y - labels))
# 训练子图
global_step = tf.train.get_global_step()
optimizer = tf.train.GradientDescentOptimizer(0.01)
train = tf.group(optimizer.minimize(loss),
tf.assign_add(global_step, 1))
# ModelFnOps方法将创建我们自定义的一个抽象模型。
return tf.contrib.learn.ModelFnOps(
mode=mode, predictions=y,
loss=loss,
train_op=train)

estimator = tf.contrib.learn.Estimator(model_fn=model)
# 定义数据集
x = np.array([1., 2., 3., 4.])
y = np.array([0., -1., -2., -3.])
input_fn = tf.contrib.learn.io.numpy_input_fn({"x": x}, y, 4, num_epochs=1000)

# 训练数据
estimator.fit(input_fn=input_fn, steps=1000)
# 评估模型
print(estimator.evaluate(input_fn=input_fn, steps=10))

运行后输出:

1
{'loss': 5.9819476e-11, 'global_step': 1000}

下篇将讲下经典的MNIST数字识别

梯度下降法是一个一阶最优化算法,通常也称为最速下降法。要使用梯度下降法找到一个函数的局部极小值,必须向函数上当前点对于梯度(或者是近似梯度)的反方向的规定步长距离点进行迭代搜索。所以梯度下降法可以帮助我们求解某个函数的极小值或者最小值。对于n维问题就最优解,梯度下降法是最常用的方法之一。下面通过梯度下降法的前生今世来进行详细推导说明。

梯度下降法的前世

首先从简单的开始,看下面的一维函数:

1
f(x) = x^3 + 2 * x - 3

在数学中如果我们要求f(x) = 0处的解,我们可以通过如下误差等式来求得:

1
error = (f(x) - 0)^2

当error趋近于最小值时,也就是f(x) = 0处x的解,我们也可以通过图来观察:

通过这函数图,我们可以非常直观的发现,要想求得该函数的最小值,只要将x指定为函数图的最低谷。这在高中我们就已经掌握了该函数的最小值解法。我们可以通过对该函数进行求导(即斜率):

1
derivative(x) = 6 * x^5 + 16 * x^3 - 18 * x^2 + 8 * x - 12

如果要得到最小值,只需令derivative(x) = 0,即x = 1。同时我们结合图与导函数可以知道:

当x < 1时,derivative < 0,斜率为负的;
当x > 1时,derivative > 0,斜率为正的;
当x 无限接近 1时,derivative也就无限=0,斜率为零。
通过上面的结论,我们可以使用如下表达式来代替x在函数中的移动

x = x - reate * derivative

当斜率为负的时候,x增大,当斜率为正的时候,x减小;因此x总是会向着低谷移动,使得error最小,从而求得 f(x) = 0处的解。其中的rate代表x逆着导数方向移动的距离,rate越大,x每次就移动的越多。反之移动的越少。
这是针对简单的函数,我们可以非常直观的求得它的导函数。为了应对复杂的函数,我们可以通过使用求导函数的定义来表达导函数:若函数f(x)在点x0处可导,那么有如下定义:

上面是都是公式推导,下面通过代码来实现,下面的代码都是使用python进行实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> def f(x):
... return x**3 + 2 * x - 3
...
>>> def error(x):
... return (f(x) - 0)**2
...
>>> def gradient_descent(x):
... delta = 0.00000001
... derivative = (error(x + delta) - error(x)) / delta
... rate = 0.01
... return x - rate * derivative
...
>>> x = 0.8
>>> for i in range(50):
... x = gradient_descent(x)
... print('x = {:6f}, f(x) = {:6f}'.format(x, f(x)))
...

执行上面程序,我们就能得到如下结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
x = 0.869619, f(x) = -0.603123
x = 0.921110, f(x) = -0.376268
x = 0.955316, f(x) = -0.217521
x = 0.975927, f(x) = -0.118638
x = 0.987453, f(x) = -0.062266
x = 0.993586, f(x) = -0.031946
x = 0.996756, f(x) = -0.016187
x = 0.998369, f(x) = -0.008149
x = 0.999182, f(x) = -0.004088
x = 0.999590, f(x) = -0.002048
x = 0.999795, f(x) = -0.001025
x = 0.999897, f(x) = -0.000513
x = 0.999949, f(x) = -0.000256
x = 0.999974, f(x) = -0.000128
x = 0.999987, f(x) = -0.000064
x = 0.999994, f(x) = -0.000032
x = 0.999997, f(x) = -0.000016
x = 0.999998, f(x) = -0.000008
x = 0.999999, f(x) = -0.000004
x = 1.000000, f(x) = -0.000002
x = 1.000000, f(x) = -0.000001
x = 1.000000, f(x) = -0.000001
x = 1.000000, f(x) = -0.000000
x = 1.000000, f(x) = -0.000000
x = 1.000000, f(x) = -0.000000

通过上面的结果,也验证了我们最初的结论。x = 1时,f(x) = 0。
所以通过该方法,只要步数足够多,就能得到非常精确的值。

梯度下降法的今生

上面是对一维函数进行求解,那么对于多维函数又要如何求呢?我们接着看下面的函数,你会发现对于多维函数也是那么的简单。

1
f(x) = x[0] + 2 * x[1] + 4

同样的如果我们要求f(x) = 0处,x[0]与x[1]的值,也可以通过求error函数的最小值来间接求f(x)的解。跟一维函数唯一不同的是,要分别对x[0]与x[1]进行求导。在数学上叫做偏导数:

保持x[1]不变,对x[0]进行求导,即f(x)对x[0]的偏导数
保持x[0]不变,对x[1]进行求导,即f(x)对x[1]的偏导数
有了上面的理解基础,我们定义的gradient_descent如下:

1
2
3
4
5
6
7
8
9
>>> def gradient_descent(x):
... delta = 0.00000001
... derivative_x0 = (error([x[0] + delta, x[1]]) - error([x[0], x[1]])) / delta
... derivative_x1 = (error([x[0], x[1] + delta]) - error([x[0], x[1]])) / delta
... rate = 0.01
... x[0] = x[0] - rate * derivative_x0
... x[1] = x[1] - rate * derivative_x1
... return [x[0], x[1]]
...

rate的作用不变,唯一的区别就是分别获取最新的x[0]与x[1]。下面是整个代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
>>> def f(x):
... return x[0] + 2 * x[1] + 4
...
>>> def error(x):
... return (f(x) - 0)**2
...
>>> def gradient_descent(x):
... delta = 0.00000001
... derivative_x0 = (error([x[0] + delta, x[1]]) - error([x[0], x[1]])) / delta
... derivative_x1 = (error([x[0], x[1] + delta]) - error([x[0], x[1]])) / delta
... rate = 0.02
... x[0] = x[0] - rate * derivative_x0
... x[1] = x[1] - rate * derivative_x1
... return [x[0], x[1]]
...
>>> x = [-0.5, -1.0]
>>> for i in range(100):
... x = gradient_descent(x)
... print('x = {:6f},{:6f}, f(x) = {:6f}'.format(x[0],x[1],f(x)))
...

输出结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
x = -0.560000,-1.120000, f(x) = 1.200000
x = -0.608000,-1.216000, f(x) = 0.960000
x = -0.646400,-1.292800, f(x) = 0.768000
x = -0.677120,-1.354240, f(x) = 0.614400
x = -0.701696,-1.403392, f(x) = 0.491520
x = -0.721357,-1.442714, f(x) = 0.393216
x = -0.737085,-1.474171, f(x) = 0.314573
x = -0.749668,-1.499337, f(x) = 0.251658
x = -0.759735,-1.519469, f(x) = 0.201327
x = -0.767788,-1.535575, f(x) = 0.161061
x = -0.774230,-1.548460, f(x) = 0.128849
x = -0.779384,-1.558768, f(x) = 0.103079
x = -0.783507,-1.567015, f(x) = 0.082463
x = -0.786806,-1.573612, f(x) = 0.065971
x = -0.789445,-1.578889, f(x) = 0.052777
x = -0.791556,-1.583112, f(x) = 0.042221
x = -0.793245,-1.586489, f(x) = 0.033777
x = -0.794596,-1.589191, f(x) = 0.027022
x = -0.795677,-1.591353, f(x) = 0.021617
x = -0.796541,-1.593082, f(x) = 0.017294
x = -0.797233,-1.594466, f(x) = 0.013835
x = -0.797786,-1.595573, f(x) = 0.011068
x = -0.798229,-1.596458, f(x) = 0.008854
x = -0.798583,-1.597167, f(x) = 0.007084
x = -0.798867,-1.597733, f(x) = 0.005667
x = -0.799093,-1.598187, f(x) = 0.004533
x = -0.799275,-1.598549, f(x) = 0.003627
x = -0.799420,-1.598839, f(x) = 0.002901
x = -0.799536,-1.599072, f(x) = 0.002321
x = -0.799629,-1.599257, f(x) = 0.001857
x = -0.799703,-1.599406, f(x) = 0.001486
x = -0.799762,-1.599525, f(x) = 0.001188
x = -0.799810,-1.599620, f(x) = 0.000951
x = -0.799848,-1.599696, f(x) = 0.000761
x = -0.799878,-1.599757, f(x) = 0.000608
x = -0.799903,-1.599805, f(x) = 0.000487
x = -0.799922,-1.599844, f(x) = 0.000389
x = -0.799938,-1.599875, f(x) = 0.000312
x = -0.799950,-1.599900, f(x) = 0.000249
x = -0.799960,-1.599920, f(x) = 0.000199
x = -0.799968,-1.599936, f(x) = 0.000159
x = -0.799974,-1.599949, f(x) = 0.000128
x = -0.799980,-1.599959, f(x) = 0.000102
x = -0.799984,-1.599967, f(x) = 0.000082
x = -0.799987,-1.599974, f(x) = 0.000065
x = -0.799990,-1.599979, f(x) = 0.000052
x = -0.799992,-1.599983, f(x) = 0.000042
x = -0.799993,-1.599987, f(x) = 0.000033
x = -0.799995,-1.599989, f(x) = 0.000027
x = -0.799996,-1.599991, f(x) = 0.000021
x = -0.799997,-1.599993, f(x) = 0.000017
x = -0.799997,-1.599995, f(x) = 0.000014
x = -0.799998,-1.599996, f(x) = 0.000011
x = -0.799998,-1.599997, f(x) = 0.000009
x = -0.799999,-1.599997, f(x) = 0.000007
x = -0.799999,-1.599998, f(x) = 0.000006
x = -0.799999,-1.599998, f(x) = 0.000004
x = -0.799999,-1.599999, f(x) = 0.000004
x = -0.799999,-1.599999, f(x) = 0.000003
x = -0.800000,-1.599999, f(x) = 0.000002
x = -0.800000,-1.599999, f(x) = 0.000002
x = -0.800000,-1.599999, f(x) = 0.000001
x = -0.800000,-1.600000, f(x) = 0.000001
x = -0.800000,-1.600000, f(x) = 0.000001
x = -0.800000,-1.600000, f(x) = 0.000001
x = -0.800000,-1.600000, f(x) = 0.000001
x = -0.800000,-1.600000, f(x) = 0.000000

细心的你可能会发现,f(x) = 0不止这一个解还可以是x = -2, -1。这是因为梯度下降法只是对当前所处的凹谷进行梯度下降求解,对于error函数并不代表只有一个f(x) = 0的凹谷。所以梯度下降法只能求得局部解,但不一定能求得全部的解。当然如果对于非常复杂的函数,能够求得局部解也是非常不错的。

tensorflow中的运用

通过上面的示例,相信对梯度下降也有了一个基本的认识。现在我们回到最开始的地方,在tensorflow中使用gradientDescent。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import tensorflow as tf

# Model parameters
W = tf.Variable([.3], dtype=tf.float32)
b = tf.Variable([-.3], dtype=tf.float32)
# Model input and output
x = tf.placeholder(tf.float32)
linear_model = W*x + b
y = tf.placeholder(tf.float32)

# loss
loss = tf.reduce_sum(tf.square(linear_model - y)) # sum of the squares
# optimizer
optimizer = tf.train.GradientDescentOptimizer(0.01)
train = optimizer.minimize(loss)

# training data
x_train = [1, 2, 3, 4]
y_train = [0, -1, -2, -3]
# training loop
init = tf.global_variables_initializer()
sess = tf.Session()
sess.run(init) # reset values to wrong
for i in range(1000):
sess.run(train, {x: x_train, y: y_train})

# evaluate training accuracy
curr_W, curr_b, curr_loss = sess.run([W, b, loss], {x: x_train, y: y_train})
print("W: %s b: %s loss: %s"%(curr_W, curr_b, curr_loss))

上面的是tensorflow的官网示例,上面代码定义了函数linear_model = W * x + b,其中的error函数为linear_model - y。目的是对一组x_train与y_train进行简单的训练求解W与b。为了求得这一组数据的最优解,将每一组的error相加从而得到loss,最后再对loss进行梯度下降求解最优值。

1
2
optimizer = tf.train.GradientDescentOptimizer(0.01)
train = optimizer.minimize(loss)

在这里rate为0.01,因为这个示例也是多维函数,所以也要用到偏导数来进行逐步向最优解靠近。

1
2
for i in range(1000):
sess.run(train, {x: x_train, y: y_train})

最后使用梯度下降进行循环推导,下面给出一些推导过程中的相关结果

1
2
3
4
5
6
7
8
9
10
11
12
13
W: [-0.21999997] b: [-0.456] loss: 4.01814
W: [-0.39679998] b: [-0.49552] loss: 1.81987
W: [-0.45961601] b: [-0.4965184] loss: 1.54482
W: [-0.48454273] b: [-0.48487374] loss: 1.48251
W: [-0.49684232] b: [-0.46917531] loss: 1.4444
W: [-0.50490189] b: [-0.45227283] loss: 1.4097
W: [-0.5115062] b: [-0.43511063] loss: 1.3761
....
....
....
W: [-0.99999678] b: [ 0.99999058] loss: 5.84635e-11
W: [-0.99999684] b: [ 0.9999907] loss: 5.77707e-11
W: [-0.9999969] b: [ 0.99999082] loss: 5.69997e-11

这里就不推理验证了,如果看了上面的梯度下降的前世今生,相信能够自主的推导出来。那么我们直接看最后的结果,可以估算为W = -1.0与b = 1.0,将他们带入上面的loss得到的结果为0.0,即误差损失值最小,所以W = -1.0与b = 1.0就是x_train与y_train这组数据的最优解。

来个彻底了解虚拟机的网络吧,👋

Parallels

共享网络

这是虚拟机建议的网络类型,因为它现成可用,无需任何特定配置。使用此网络模式时,您的 Mac 充当虚拟机的路由器。结果是:

Parallels Desktop 创建了独立的虚拟子网,它自己的虚拟 DHCP 服务器在 macOS 中运行。
虚拟机属于具有其自己的 IP 范围的虚拟子网。
虚拟机在 Mac 所属的真实子网中不可见。
虚拟机可以 ping 真实子网中的计算机。

从上图中可以看出,您的 Mac 将有两个 IP 地址。这是因为 Mac 将同时属于两个子网:真实子网(例如,您的家庭网络)和由 Parallels Desktop 创建的虚拟子网。可从 Parallels Desktop 偏好设置 > 网络中访问此虚拟子网的设置。

若要使用共享网络模式,请转到虚拟机的配置 > 硬件 > 网络 1 > 源,并选择共享网络。

桥接网络


使用此网络模式时,虚拟机的网卡使用名为“桥接”的技术建立与您的 Mac 计算机网卡的直接连接。结果是:

虚拟机显示为独立的物理计算机,该计算机与它运行在其上的 Mac 属于同一子网。
DHCP 服务器(例如,您的路由器)为虚拟机提供一个 IP 地址,该 IP 地址与同一子网中的其他计算机处于相同的 IP 范围内。
虚拟机可以 ping 和查看子网中的所有计算机。
其他计算机可以 ping 和查看虚拟机。
若要使用桥接网络模式,请转到虚拟机的配置 > 硬件 > 网络 1 > 源,并根据需要选择桥接:默认适配器、以太网或 Wi-Fi。

以下内容介绍每种类型的桥接模式是如何与 Mac 网络设置对应的:

桥接:以太网对应于您的 Mac 以太网适配器
桥接:Wi-Fi 对应于您的 Mac Wi-Fi 适配器
桥接:默认适配器对应于在 Mac 上被选为默认网络适配器(系统偏好设置 > 网络中列出的第一个网络适配器)的那个适配器。

Host-Only 网络


此模式与共享模式类似,只不过由 Parallels Desktop (10.37.129.x) 创建的虚拟子网是与外界隔离的。结果是,以 Host-Only 模式运行的虚拟机只能查看和 ping 它运行在其上的 Mac。

VirtualBox

VirtualBox的提供了四种网络接入模式,它们分别是:
1、NAT 网络地址转换模式(NAT,Network Address Translation)
2、Bridged Adapter 桥接模式
3、Internal 内部网络模式
4、Host-only Adapter 主机模式

1、NAT

NAT:Network Address Translation,网络地址转换
NAT模式是最简单的实现虚拟机上网的方式,你可以这样理解:
Guest访问网络的所有数据都是由主机提供的,Guest并不真实存在于网络中,主机与网络中的任何机器都不能查看和访问到Guest的存在。
Guest可以访问主机能访问到的所有网络,但是对于主机以及主机网络上的其他机器,Guest又是不可见的,甚至主机也访问不到Guest。

虚拟机与主机的关系:只能单向访问,虚拟机可以通过网络访问到主机,主机无法通过网络访问到虚拟机。
虚拟机与网络中其他主机的关系:只能单向访问,虚拟机可以访问到网络中其他主机,其他主机不能通过网络访问到虚拟机。
虚拟机与虚拟机的关系:相互不能访问,虚拟机与虚拟机各自完全独立,相互间无法通过网络访问彼此。

IP:10.0.2.15 网关:10.0.2.2 DNS:10.0.2.3 一台虚拟机的多个网卡可以被设定使用 NAT,
第一个网卡连接了到专用网 10.0.2.0,第二个网卡连接到专用网络 10.0.3.0,等等。默认得到的客户端ip(IP
Address)是10.0.2.15,网关(Gateway)是10.0.2.2,域名服务器(DNS)是10.0.2.3,可以手动参考这个进行修
改。 NAT方案优缺点:
笔记本已插网线时:虚拟机可以访问主机,虚拟机可以访问互联网,在做了端口映射后(最后有说明),主机可以访问虚拟机上的服务(如数据库)。
笔记本没插网线时:主机的“本地连接”有红叉的,虚拟机可以访问主机,虚拟机不可以访问互联网,在做了端口映射后,主机可以访问虚拟机上的服务(如数据库)

2、Bridged Adapter(网桥模式)

网桥模式,你可以这样理解:

它是通过主机网卡,架设了一条桥,直接连入到网络中了。因此,它使得虚拟机能被分配到一个网络中独立的IP,所有网络功能完全和在网络中的真实机器一样。
网桥模式下的虚拟机,你把它认为是真实计算机就行了。

虚拟机与主机的关系:可以相互访问,因为虚拟机在真实网络段中有独立IP,主机与虚拟机处于同一网络段中,彼此可以通过各自IP相互访问。
虚拟机于网络中其他主机的关系:可以相互访问,同样因为虚拟机在真实网络段中有独立IP,虚拟机与所有网络其他主机处于同一网络段中,彼此可以通过各自IP相互访问。
虚拟机与虚拟机的关系:可以相互访问,原因同上。

IP:一般是DHCP分配的,与主机的“本地连接”的IP 是同一网段的。虚拟机就能与主机互相通信。

笔记本已插网线时:(若网络中有DHCP服务器)主机与虚拟机会通过DHCP分别得到一个IP,这两个IP在同一网段。 主机与虚拟机可以ping通,虚拟机可以上互联网。
笔记本没插网线时:主机与虚拟机不能通信。主机的“本地连接”有红叉,就不能手工指定IP。虚拟机也不能通过DHCP得到IP地址,手工指定IP后,也无法与主机通信,因为主机无IP。这时主机的VirtualBox Host-Only Network 网卡是有ip的,192.168.56.1。虚拟机就算手工指定了IP192.168.56.*,也ping不能主机。

3、Internal(内网模式)

内网模式,顾名思义就是内部网络模式:
虚拟机与外网完全断开,只实现虚拟机于虚拟机之间的内部网络模式。

虚拟机与主机的关系:不能相互访问,彼此不属于同一个网络,无法相互访问。 虚拟机与网络中其他主机的关系:不能相互访问,理由同上。
虚拟机与虚拟机的关系:可以相互访问,前提是在设置网络时,两台虚拟机设置同一网络名称。如上配置图中,名称为intnet。

IP:VirtualBox的DHCP服务器会为它分配IP ,一般得到的是192.168.56.101,因为是从101起分的,也可手工指定192.168.56.*。
笔记本已插网线时:虚拟机可以与主机的VirtualBox Host-Only Network 网卡通信这种方案不受主机本地连接(网卡)是否有红叉的影响。

4、Host-only Adapter(主机模式)

主机模式,这是一种比较复杂的模式,需要有比较扎实的网络基础知识才能玩转。可以说前面几种模式所实现的功能,在这种模式下,通过虚拟机及网卡的设置都可以被实现。

我们可以理解为Guest在主机中模拟出一张专供虚拟机使用的网卡,所有虚拟机都是连接到该网卡上的,我们可以通过设置这张网卡来实现上网及其他很多功能,比如(网卡共享、网卡桥接等)。

虚拟机与主机的关系:默认不能相互访问,双方不属于同一IP段,host-only网卡默认IP段为192.168.56.X
子网掩码为255.255.255.0,后面的虚拟机被分配到的也都是这个网段。通过网卡共享、网卡桥接等,可以实现虚拟机于主机相互访问。

虚拟机与网络主机的关系:默认不能相互访问,原因同上,通过设置,可以实现相互访问。
虚拟机与虚拟机的关系:默认可以相互访问,都是同处于一个网段。

虚拟机访问主机,用的是主机的VirtualBox Host-Only Network网卡的IP:192.168.56.1 ,不管主机“本地连接”有无红叉,永远通。
主机访问虚拟机,用是的虚拟机的网卡3的IP: 192.168.56.101 ,不管主机“本地连接”有无红叉,永远通。
虚拟机访问互联网,用的是自己的网卡2, 这时主机要能通过“本地连接”有线上网,(无线网卡不行)

1
$ nano hostname_and_ip.sh

and insert the script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/sh

hostname=$1
ip=$2 # should be of format: 192.168.1.100
dns=$3 # should be of format: 192.168.1.1

# Change the hostname
sudo hostnamectl --transient set-hostname $hostname
sudo hostnamectl --static set-hostname $hostname
sudo hostnamectl --pretty set-hostname $hostname
sudo sed -i s/raspberrypi/$hostname/g /etc/hosts

# Set the static ip

sudo cat <<EOT >> /etc/dhcpcd.conf
interface eth0
static ip_address=$ip/24
static routers=$dns
static domain_name_servers=$dns
EOT

Now run the script, an example of my naming and ip convention can be seen below. But adapt to your liking.

First argument: the new hostname
Second argument: the new static IP
Third argument: the IP of your Router

master: 192.168.1.100

1
$ sh hostname_and_ip.sh k8s-master 192.168.1.100 192.168.1.1

worker-02: 192.168.1.102

1
$ sh hostname_and_ip.sh k8s-worker-02 192.168.1.102 192.168.1.1

worker-03: 192.168.1.103

1
$ sh hostname_and_ip.sh k8s-worker-03 192.168.1.103 192.168.1.1

Now, reboot the Pi. You should be able to access the Pi over SSH as follows:

1
$ ssh pi@k8s-master.local (or k8s-worker-01.local etc.)

Verify that your Pi now also has a new static IP by running ifconfig.

Installing the prerequisites

Now, that the static networking and naming is in place, we need to install some software on the Raspberry Pi.

Therefore, create a new file on the Raspberry Pi:

1
$ nano install.sh

Copy and insert the following script.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/sh

# Install Docker
curl -sSL get.docker.com | sh && \
sudo usermod pi -aG docker

# Disable Swap
sudo dphys-swapfile swapoff && \
sudo dphys-swapfile uninstall && \
sudo update-rc.d dphys-swapfile remove
echo Adding " cgroup_enable=cpuset cgroup_enable=memory" to /boot/cmdline.txt
sudo cp /boot/cmdline.txt /boot/cmdline_backup.txt
orig="$(head -n1 /boot/cmdline.txt) cgroup_enable=cpuset cgroup_enable=memory"
echo $orig | sudo tee /boot/cmdline.txt

# Add repo list and install kubeadm
curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add - && \
echo "deb http://apt.kubernetes.io/ kubernetes-xenial main" | sudo tee /etc/apt/sources.list.d/kubernetes.list && \
sudo apt-get update -q && \
sudo apt-get install -qy kubeadm

Execute the script

1
$ sh install.sh

This will install and configure docker, disable swap and install kubeadm.

Reboot the machine, and repeat this process for all your Raspberry Pis.

Initialize the Kubernetes master

So, we are now ready to set up Kubernetes. To do this, we are going to use the awesome tool called, kubeadm. This makes it pretty easy to spin up a Kubernetes cluster by, basically, running kubeadm init on the master node and kubeadm join on the worker nodes.

One of the purposes of this cluster is going to be demoing Kubernetes stuff. One example could be to pull out the network cable of one of the worker nodes and demoing how Kubernetes deals with this situation by rescheduling the pods from the lost node.

Therefore, we would like to change one of the arguments to the kube-controller-manager, namely, pod-eviction-timeout which defaults to 5 minutes. That’s a long time to wait in a presentation. Instead, we want to change this to 10s. You may also want to change the time that Kubernetes allows the node to be unresponsive. It defaults to 40 seconds. To change this, add the following argument to the master configuration : node-monitor-grace-period: 10s.

Changing arguments passed to the different Kubernetes core components by kubeadm is pretty simple. We just have to pass a YAML configuration file specifying the arguments we want to change. Let’s do that.

Create the configuration file:

1
$ nano kubeadm_conf.yaml

Copy and insert the following

1
2
3
4
5
apiVersion: kubeadm.k8s.io/v1alpha1
kind: MasterConfiguration
controllerManagerExtraArgs:
pod-eviction-timeout: 10s
node-monitor-grace-period: 10s

Save and run

1
$ sudo kubeadm init --config kubeadm_conf.yaml

This takes a couple of minutes. Once the process finishes you should see something similar to:

1
2
3
4
5
6
7
8
9
10
11
12
...
Your Kubernetes master has initialized successfully!
To start using your cluster, you need to run the following as a regular user:
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
You should now deploy a pod network to the cluster.
Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:
https://kubernetes.io/docs/concepts/cluster-administration/addons/
You can now join any number of machines by running the following on each node
as root:
kubeadm join --token TOKEN 192.168.1.100:6443 --discovery-token-ca-cert-hash HASH

Follow the instructions in the output:

1
2
3
$ mkdir -p $HOME/.kube
$ sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
$ sudo chown $(id -u):$(id -g) $HOME/.kube/config

You can verify that your master node is up and running.

1
2
3
pi@master:~ $ kubectl get nodes
NAME STATUS ROLES AGE VERSION
master NotReady master 3m v1.9.0

Don’t mind the status being NotReady. In order for the master node to become ready, we need to install a container network. But before we do that, let’s add some more nodes to the cluster first.

Setting up the worker nodes

Alright, next up we need to spin up some workers for the cluster to be complete.

Assuming you have already set up the prerequisites mentioned above, we basically only need to run the kubeadm join on each of your worker nodes. As shown above, kubeadm outputs the command that you need to run on all your worker nodes.

1
$ sudo kubeadm join --token TOKEN 192.168.1.100:6443 --discovery-token-ca-cert-hash HASH

Repeat for every node.

Set up weave as the container network

Nodes are not able to communicate without a container network, which is something you have to provide. Therefore, the final piece of the puzzle is to add one. We will be using weave-net for this. On the master node, run the following command:

1
$ kubectl apply -f “https://cloud.weave.works/k8s/net?k8s-version=$(kubectl version | base64 | tr -d ‘\n’)

All set and done…

That was it. You should now have a fully functioning Raspberry Pi Kubernetes cluster. Verify your setup by running

1
2
3
4
5
6
pi@k8s-master:~ $ kubectl get nodes
NAME STATUS ROLES AGE VERSION
k8s-master Ready master 17m v1.11.2
k8s-worker-01 Ready <none> 10m v1.11.2
k8s-worker-02 Ready <none> 10m v1.11.2
k8s-worker-03 Ready <none> 6m v1.11.2

Pod实现原理

Pod只是一个逻辑概念,Kubernetes真正处理的还是宿主机操作系统上Linux容器的Namespace和Cgroups,而并不存在一个所谓的Pod边界或隔离环境
Pod其实是一组共享了某些资源的容器,Pod里的所有容器,共享的是同一个Network Namespace,并且可以声明共享同一个Volume

在Kubernetes项目里,Pod的实现需要使用一个中间容器,这个容器叫做Infra容器(Infra容器占用极少的资源,它的镜像时用汇编语言编写的,永远处于“暂停”状态的容器)。在Pod中,Infra容器永远都是第一个被创建的容器,而其他用户定义的容器,则通过join Network Namespace的方式,与Infra容器关联在一起,如下图所示,对于同一个Pod里面的所有用户容器,它们的进出流量都是通过Infra容器完成的。


在Infra容器Hold住Network Namespace后,用户容器就可以加入到Infra容器的Network Namespace中。对于Pod里容器A和容器B来说

  • 它们可以直接使用localhost进行通信
  • 它们看到的网络设备跟infra容器看到的完全一样
  • 一个Pod只有一个IP地址(即这个Pod的Network Namespace对应的IP地址)。其他所有网络资源,都是一个Pod一份,并且被该Pod中的所有容器共享
  • Pod的生命周期只跟Infra容器一致,而与容器A和容器B无关

pod是一个小家庭,它把密不可分的家庭成员(container)聚在一起,Infra container则是家长,掌管家中共通资源,家庭成员通过sidecar方式互帮互助,其乐融融~

Pod的生命周期

  • Pending:Pod的YAML文件已经提交给了Kubernetes,API对象已经被创建并保存在Etcd当中,但是这个Pod里有些容器因为某种原因而不能被顺利创建,比如调度不成功
  • Running:Pod已经调度成功,跟一个具体的节点绑定,它包含的容器都已经创建成功,并且至少有一个正在运行中
  • Succeeded: Pod里所有容器都正常运行完毕,并且已经退出了,一次性任务最常见
  • Failed:Pod里至少有一个容器以不正常的状态(非0的返回码)退出
  • Unknown:Pod状态不能持续地被kubelet汇报给kub-apiserver,很可能是Master和Kubelet之间通信出了问题

基本概念

最近总是需要kubernetes通过ubuntu下载境外镜像,通过自己搭建的ss走代理
如何在linux像mac客户端一样通过终端代理起来,备忘一下

1
2
# apt-get install python-pip
# pip install shadowsocks

编辑文件/etc/shadowsocks.json,填写代理信息:

1
2
3
4
5
6
7
8
9
{ 
"server":"代理服务器ip”,
"server_port":代理服务器端口,
"password":"代理服务器访问密码”,
"local_address":"127.0.0.1”,
"local_port":1080,
"timeout":600,
"method":"aes-256-cfb" // 代理服务器访问数据加密方式,根据自己配置ss 服务端时的配置自行填写
}

开启 关闭

1
2
sudo sslocal -c /etc/shadowsocks.json -d start
sudo sslocal -c /etc/shadowsocks.json -d stop

本地代理跑起来后,我们来配置通过privoxy 8118走1080代理

1
sudo apt-get install privoxy

编辑配置文件/etc/privoxy/config,加入下面两行内容。

1
2
listen-address 127.0.0.1:8118
forward-socks5 / 127.0.0.1:1080 .

开启关闭重启

1
2
3
sudo service privoxy restart
sudo service privoxy stop
sudo service privoxy start

校验

1
2
3
export http_proxy="127.0.0.1:8118"
export https_proxy="127.0.0.1:8118"
$ curl ip.cip.cc


最近沉迷在 Kubernetes部署中不能自拔,作为初学者,基本上把可能踩的坑都踩了一遍,先分享一下怎么部署 Kubernetes 集群
首先,我们要知道 Kubernetes 是什么:

Kubernetes简称为k8s,它是 Google 开源的容器集群管理系统。在 Docker 技术的基础上,为容器化的应用提供部署运行、资源调度、服务发现和动态伸缩等一系列完整功能,提高了大规模容器集群管理的便捷性。

K8s 是一个完备的分布式系统支撑平台,具有完备的集群管理能力,多层次的安全防护和准入机制、多租户应用支撑能力、透明的服务注册和发现机制、內建负载均衡器、强大的故障发现和自我修复能力、服务滚动升级和在线扩容能力、可扩展的资源自动调度机制以及多粒度的资源配额管理能力。同时 K8s 提供完善的管理工具,涵盖了包括开发、部署测试、运维监控在内的各个环节。

安装

这个教程是对三台机器进行 k8s 部署,系统是Ubuntu 16.04.4 LTS。其中一台是 master ,其他两台是 worker。

安装 Docker

添加使用 HTTPS 传输的软件包以及 CA 证书

1
2
3
4
5
6
$ sudo apt-get update
$ sudo apt-get install \
apt-transport-https \
ca-certificates \
curl \
software-properties-common

添加软件源的 GPG 密钥

1
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

向 source.list 中添加 Docker 软件源

1
2
3
4
5
$ sudo add-apt-repository \
"deb [arch=amd64] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) \
stable"
$ sudo apt-get update

安装指定版本

1
2
$ apt-cache madison docker-ce
$ sudo apt-get install docker-ce=<VERSION>

在测试或开发环境中 Docker 官方为了简化安装流程,提供了一套便捷的安装脚本,Ubuntu 系统上可以使用这套脚本安装:

1
2
$ curl -fsSL get.docker.com -o get-docker.sh
$ sudo sh get-docker.sh --mirror Aliyun
安装 kubeadm, kubelet 和 kubectl

官方源

1
2
3
4
5
6
7
8
apt-get update && apt-get install -y apt-transport-https curl
curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
cat <<EOF >/etc/apt/sources.list.d/kubernetes.list
deb https://apt.kubernetes.io/ kubernetes-xenial main
EOF
apt-get update
apt-get install -y kubelet kubeadm kubectl
apt-mark hold kubelet kubeadm kubectl

因为某些你懂的原因,要更换阿里源,并安装kubelet kubeadm kubectl:

1
2
3
4
5
6
7
apt-get update && apt-get install -y apt-transport-https
curl https://mirrors.aliyun.com/kubernetes/apt/doc/apt-key.gpg | apt-key add -
cat <<EOF >/etc/apt/sources.list.d/kubernetes.list
deb https://mirrors.aliyun.com/kubernetes/apt/ kubernetes-xenial main
EOF
apt-get update
apt-get install -y kubelet kubeadm kubectl
1
2
关闭swap
$ sudo swapoff -a
使用kubeadm创建一个集群

使用 kubeadm 创建 k8s 集群其实还蛮简单,最大的困难是那堵墙,当我费了一整天把那堵墙问题解决后,发现 1.13.0 版本居然提供了中国特供的一个功能,所以把两种方法都写出来,供大家参考。

1.13.0 版本之前

查看kubeadm 会用到哪几个镜像:

1
$ kubeadm config images list

把得到的

1
2
3
4
5
6
7
k8s.gcr.io/kube-apiserver:v1.13.0
k8s.gcr.io/kube-controller-manager:v1.13.0
k8s.gcr.io/kube-scheduler:v1.13.0
k8s.gcr.io/kube-proxy:v1.13.0
k8s.gcr.io/pause:3.1
k8s.gcr.io/etcd:3.2.24
k8s.gcr.io/coredns:1.2.6

写个批量脚本获取替换成阿里云镜像地址拉取images并打回标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
images=(
kube-apiserver:v1.13.0
kube-controller-manager:v1.13.0
kube-scheduler:v1.13.0
kube-proxy:v1.13.0
pause:3.1
etcd:3.2.24
coredns:1.2.6
)
//pull镜像重新标记tag
for imageName in ${images[@]} ; do
docker pull registry.cn-hangzhou.aliyuncs.com/google_containers/$imageName
docker tag registry.cn-hangzhou.aliyuncs.com/google_containers/$imageName k8s.gcr.io/$imageName
docker rmi registry.cn-hangzhou.aliyuncs.com/google_containers/$imageName
done

拉取完后
kubeadm init
就可以完成 Kubernetes Master 的部署了

1.13.0版本之后

Kubenetes默认Registries地址是k8s.gcr.io,很明显,在国内并不能访问gcr.io,因此在kubeadm v1.13之前的版本,安装起来非常麻烦,但是在1.13版本中终于解决了国内的痛点,其增加了一个–image-repository参数,默认值是k8s.gcr.io,我们将其指定为国内镜像地址:registry.aliyuncs.com/google_containers,其它的就可以完全按照官方文档来愉快的玩耍了。

1
2
3
4
5
$ kubeadm init \
--image-repository registry.aliyuncs.com/google_containers \
--pod-network-cidr=192.168.0.0/16 \
--ignore-preflight-errors=cri \
--kubernetes-version=1.13.1

kubeadm 会生成一行指令:

1
kubeadm join 10.168.0.2:6443 --token 00bwbx.uvnaa2ewjflwu1ry --discovery-token-ca-cert-hash sha256:00eb62a2a6020f94132e3fe1ab721349bbcd3e9b94da9654cfe15f2985ebd711

并提示如下命令执行确保授权配置

1
2
3
4
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

安装完后,
kubectl get 命令来查看当前唯一一个节点的状态了

1
2
3
4
$ kubectl get nodes

NAME STATUS ROLES AGE VERSION
iz94t4csjq4z NotReady master 82m v1.13.0

其中STATUS 是NotReady
我们通过kubectl describe 来查看这个节点(Node)对象的详细信息、状态和事件(Event)信息

1
2
3
4
5
6
7
8
9
kubectl describe node {NAME}
//$ kubectl describe node iz94t4csjq4z

...
Conditions:
...

Ready False ... KubeletNotReady runtime network not ready: NetworkReady=false reason:NetworkPluginNotReady message:docker: network plugin is not ready: cni config uninitialized

通过 kubectl describe 指令的输出,我们可以看到 NodeNotReady 的原因在于,我们尚未部署任何网络插件。
后面我们将专门讲关于部署插件。

另外,我们还可以通过 kubectl 检查这个节点上各个系统 Pod 的状态,其中,kube-system 是 Kubernetes
项目预留的系统 Pod 的工作空间(Namepsace,注意它并不是 Linux Namespace,它只是 Kubernetes 划分
不同工作空间的单位):

1
2
3
4
5
6
7
8
9
10
11
$ kubectl get pods -n kube-system

NAME READY STATUS RESTARTS AGE
coredns-78fcdf6894-j9s52 0/1 Pending 0 1h
coredns-78fcdf6894-jm4wf 0/1 Pending 0 1h
etcd-master 1/1 Running 0 2s
kube-apiserver-master 1/1 Running 0 1s
kube-controller-manager-master 0/1 Pending 0 1s
kube-proxy-xbd47 1/1 NodeLost 0 1h
kube-scheduler-master 1/1 Running 0 1s

可以看到,CoreDNS、kube-controller-manager 等依赖于网络的 Pod 都处于 Pending 状态,即调度失败。这当然是符合预期的:因为这个 Master 节点的网络尚未就绪。

命令

获取端部节点

1
2
3
4
$ kubectl get nodes

NAME STATUS ROLES AGE VERSION
iz94t4csjq4z Ready master 3h57m v1.13.0

查看node详细

1
$ kubectl describe node iz94t4csjq4z

查看全部pod

1
$ kubectl get pods --all-namespaces

通过namespaces获取pod

1
2
3
4
5
6
7
8
9
10
11
12
$ kubectl get pods -n kube-system

NAME READY STATUS RESTARTS AGE
coredns-86c58d9df4-qqddh 1/1 Running 0 3h58m
coredns-86c58d9df4-xxccw 1/1 Running 0 3h58m
etcd-iz94t4csjq4z 1/1 Running 0 3h57m
kube-apiserver-iz94t4csjq4z 1/1 Running 0 3h57m
kube-controller-manager-iz94t4csjq4z 1/1 Running 1 3h57m
kube-proxy-k4zkp 1/1 Running 0 3h58m
kube-scheduler-iz94t4csjq4z 1/1 Running 1 3h57m
kubernetes-dashboard-79ff88449c-dxbsb 1/1 Running 0 3h27m
weave-net-c7dvl 2/2 Running 0 3h49m

查看pod

1
$ kubectl describe pod -n kube-system

查看pod具体详情

1
$ kubectl describe pod kubernetes-dashboard-767dc7d4d-mg5gw -n kube-system
1
2
$ kubectl edit cm coredns -n kube-system
$ kubectl -n kube-system edit configmap coredns

单节点配置( Master 隔离)

默认情况下 Master 节点是不允许运行用户 Pod 的,而 Kubernetes 做到这一点,依靠的是 是 Kubernetes 的 Taint/Toleration 机制。一旦某个节点被加上了一个 Taint,即被“打上了污点”那么所有 Pod 就都不能在这个节点上运行,因为 Kubernetes 的 Pod 都有“洁癖”。
如果你通过 kubectl describe 检查一下 Master 节点的 Taint 字段,就会有所发现了:

1
2
3
4
5
6
$ kubectl describe node master

Name: master
Roles: master
Taints: node-role.kubernetes.io/master:NoSchedule

可以看到,Master 节点默认被加上了node-role.kubernetes.io/master:NoSchedule这样一个“污点”,其中“键”是node-role.kubernetes.io/master,而没有提供“值”。

如果你就是想要一个单节点的 Kubernetes,删除个 Taint 才是正确的选择:

1
$ kubectl taint nodes --all node-role.kubernetes.io/master-

如上所示,我们在“node-role.io/master”这个键后面加上了一个短横线“-”,这个格式就意味着移除所有以“node-role.kubernetes.io/master”为键的 Taint。

这个步骤的配置最终使Master节点能允许运行用户pod,也是确保下面插件部署能正确运行。

到了这一步,一个基本完整的 Kubernetes 集群就完毕了。

部署插件

部署 Dashboard 可视化插件

kubernetes-dashboard先从国内镜像拉下来:

1
2
3
$ docker pull registry.cn-hangzhou.aliyuncs.com/google_containers/kubernetes-dashboard-amd64:v1.10.0
$ docker tag registry.cn-hangzhou.aliyuncs.com/google_containers/kubernetes-dashboard-amd64:v1.10.0 k8s.gcr.io/kubernetes-dashboard-amd64:v1.10.0
$ docker rmi registry.cn-hangzhou.aliyuncs.com/google_containers/kubernetes-dashboard-amd64:v1.10.0

打上kis.gcr.io的tag,这样执行Dashboard拉取的时候就直接本地拿pull下来的直接安装。

1
2
3
4
安装
$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/master/aio/deploy/recommended/kubernetes-dashboard.yaml
删除
$ kubectl delete -f https://raw.githubusercontent.com/kubernetes/dashboard/master/aio/deploy/recommended/kubernetes-dashboard.yaml

安装后

1
$ kubectl describe pod kubernetes-dashboard -n kube-system 

可查看Dashboard状态

部署网络插件

部署网络插件非常简单
因为这里的镜像来源不是kis.gcr.io,所以我们就不先拉取镜像下来.

1
2
3
4
安装
$ kubectl apply -f https://git.io/weave-kube-1.6
删除
$ kubectl delete -f https://git.io/weave-kube-1.6

部署完成后,我们可以通过 kubectl get 重新检查Pod的状态:

1
2
3
4
5
6
7
8
9
10
11
12
$ kubectl get pods -n kube-system

NAME READY STATUS RESTARTS AGE
coredns-78fcdf6894-j9s52 1/1 Running 0 1d
coredns-78fcdf6894-jm4wf 1/1 Running 0 1d
etcd-master 1/1 Running 0 9s
kube-apiserver-master 1/1 Running 0 9s
kube-controller-manager-master 1/1 Running 0 9s
kube-proxy-xbd47 1/1 Running 0 1d
kube-scheduler-master 1/1 Running 0 9s
weave-net-cmk27 2/2 Running 0 19s

部署容器存储插件
1
2
3
4
5
6
7
8
9
10
11
12
安装
$ kubectl apply -f https://raw.githubusercontent.com/rook/rook/master/cluster/examples/kubernetes/ceph/operator.yaml
$ kubectl apply -f https://raw.githubusercontent.com/rook/rook/master/cluster/examples/kubernetes/ceph/cluster.yaml


删除
$ kubectl delete -f https://raw.githubusercontent.com/rook/rook/master/cluster/examples/kubernetes/ceph/operator.yaml
$ kubectl delete -f https://raw.githubusercontent.com/rook/rook/master/cluster/examples/kubernetes/ceph/cluster.yaml

查看安装情况
$ kubectl get pods -n rook-ceph-system
$ kubectl get pods -n rook-ceph

加入工作节点

SSH到其他机器上,成为 root 用户(如:sudo su -),安装 kubeadm, kubelet and kubectl。

然后复制上面的运行kubeadm init命令时最后一句输出,并运行它的:

1
$ kubeadm join --token <token> <master-ip>:<master-port> --discovery-token-ca-cert-hash sha256:<hash>

这时候回到master 节点服务器,运行下面命令查看节点状态:

1
$ kubectl get nodes

NAME STATUS ROLES AGE VERSION
izuf6e4bl8eavupeu7q9a0z Ready 98s v1.13.0
izuf6e4bl8eavupeu7q9a1z Ready master 75m v1.13.0
如果我们忘记了Master节点的加入token,可以使用如下命令来查看:

1
$ kubeadm token list

默认情况下,token的有效期是24小时,如果我们的token已经过期的话,可以使用以下命令重新生成:

1
$ kubeadm token create

如果我们也没有–discovery-token-ca-cert-hash的值,可以使用以下命令生成:

1
$ openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | openssl rsa -pubin -outform der 2>/dev/null | openssl dgst -sha256 -hex | sed 's/^.* //'

至此一个简单的 k8s 集群部署就差不多了。

常见问题

Q1:

1
2
2018/11/05 04:04:18 [INFO] plugin/reload: Running configuration MD5 = f65c4821c8a9b7b5eb30fa4fbc167769
2018/11/05 04:04:24 [FATAL] plugin/loop: Seen "HINFO IN 6900627972087569316.7905576541070882081." more than twice, loop detected
1
2
3
4
5
6
7
8
9
配置:--resolv-conf
/etc/systemd/system/kubelet.service.d/10-kubeadm.conf
https://stackoverflow.com/questions/53075796/coredns-pods-have-crashloopbackoff-or-error-state/53414041#53414041
https://stackoverflow.com/questions/52645473/coredns-fails-to-run-in-kubernetes-cluster
https://www.jianshu.com/p/e4dcd56fad38

systemctl daemon-reload && systemctl restart kubelet


Q2:

1
Unable to connect to the server: dial tcp 192.168.1.169:6443: connect: network is unreachable

mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

sudo swapoff -a

Q3:

1
2
3
4
5
 	[ERROR DirAvailable--etc-kubernetes-manifests]: /etc/kubernetes/manifests is not empty
[ERROR FileAvailable--etc-kubernetes-kubelet.conf]: /etc/kubernetes/kubelet.conf already exists
[ERROR Port-10250]: Port 10250 is in use
[ERROR FileAvailable--etc-kubernetes-pki-ca.crt]: /etc/kubernetes/pki/ca.crt already exists

首先我们需要了解什么是“MNIST”?

每当我们学习一门新的语言时,所有的入门教程官方都会提供一个典型的例子——“Hello World”。而在机器学习中,入门的例子称之为MNIST。

MNIST是一个简单的视觉计算数据集,它是像下面这样手写的数字图片:

每张图片还额外有一个标签记录了图片上数字是几,例如上面几张图的标签就是:5、0、4、1。

本文将会展现如何训练一个模型来识别这些图片,最终实现模型对图片上的数字进行预测。

首先要明确,我们的目标并不是要训练一个能在实际应用中使用的模型,而是通过这个过程了解如何使用TensorFlow完成整个机器学习的过程。我们会从一个非常简单的模型开始——Softmax回归。

然后要明白,例子对应的源代码非常简单,所有值得关注的信息仅仅在三行代码中。然而,这对于理解TensorFlow如何工作以及机器学习的核心概念非常重要,因此本文除了说明原理还会详细介绍每一行代码的功用。

模型创建

MINIST数据

MINIST数据集的官网是 Yann LeCun’s website。下面这2行代码的作用是从MINIST官网自动下载并读取数据:

1
2
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)

MINIST的数据分为2个部分:55000份训练数据(mnist.train)和10000份测试数据(mnist.test)。这个划分有重要的象征意义,他展示了在机器学习中如何使用数据。在训练的过程中,我们必须单独保留一份没有用于机器训练的数据作为验证的数据,这才能确保训练的结果是可以在所有范围内推广的(可泛化)。

前面已经提到,每一份MINIST数据都由图片以及标签组成。我们将图片命名为“x”,将标记数字的标签命名为“y”。训练数据集和测试数据集都是同样的结构,例如:训练的图片名为 mnist.train.images 而训练的标签名为 mnist.train.labels

Kafka

概念名词

kafka [消息列队]
Kafka™ 是一个分布式流处理系统,这是什么意思呢?
我们认为一个流数据平台具有三个主要功能

1.它允许您发布和订阅流记录。在这方面,它类似于一个消息队列或企业消息传递系统。
2.它能让你以容错方式进行流数据的存储。
3.数据产生时你就可以进行流数据处理。

Kafka擅长哪些地方?

它被用于两大类别的应用程序

1.建立实时流数据通道,这个通道可以可靠的获取在系统或应用间的数据。

2.建立实时流媒体应用来转换流数据或对流数据做出反应

首先是几个概念:

kafka作为集群运行在一台或多台服务器。

Kafka群集存储流记录的类别称为主题(topics)

Kafka的每条记录包含一个键,一个值和一个时间戳。

Kafka 有个核心API:

Producer API 允许应用推送流记录到一个或多个Kafka主题上。

Consumer API 允许应用程序订阅一个或多个主题并且并处理产生的流记录

Streams API 允许应用程序作为一个流处理器,从一个或多个主题获取流数据,然后输出流数据到一个或多个主题,有效地将输入流转换为输出流。

Connector API 允许构建和运行可重用的生产者(Producer)
或消费者(Consumer)连接Kafka与现有应用程序或数据系统。例如,一个连接器(connector)在关系数据库中可能获取每个变化的表。

实践

配置
1
2
3
4
5
6
7
8
9
10
11
12
exports.kafka = {
host: 'localhost:9092',
producerConfig: {
// Partitioner type (default = 0, random = 1, cyclic = 2, keyed = 3, custom = 4), default 0
partitionerType: 1,
},
consumerTopics: [{
topic: 'test',
partition: 0,
},
],
};
订阅消费
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
'use strict'

const kafka = require('kafka-node')
module.exports = async app => {
app.beforeStart(async () => {
const ctx = app.createAnonymousContext()
const Producer = kafka.Producer
const client = new kafka.KafkaClient({
kafkaHost: app.config.kafka.host,
})
const producer = new Producer(client, app.config.kafka.producerConfig)
producer.on('error', function(err) {
console.error('ERROR: [Producer] ' + err)
})
producer.on('ready', function() {
app.logger.warn('【启动订阅者成功】')
})
app.producer = producer

const consumer = new kafka.Consumer(client, app.config.kafka.consumerTopics, {
autoCommit: true,
})
consumer.on('message', async function(message) {
app.logger.warn('【收到消息】:', message.value)
try {
// 处理接收到逻辑
// ctx.runInBackground(async () => {
// await ctx.service.log.insert(JSON.parse(message.value))
// })
consumer.commit(true, (err, data) => {
if (err) {
console.error('commit>>err:', err)
} else {
// console.log('commit>>data:', data)
}
})
} catch (error) {
console.error('ERROR: [GetMessage] ', message, error)
}
})
consumer.on('error', function(err) {
console.error('ERROR: [Consumer] ' + err)
})
})
}

发送消息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
async sendKafka() {
const {
ctx,
app,
service,
} = this;
console.log('发送消息队列->');
const payloads = [{
topic: 'test',
messages: '2 ' + new Date(),
partition: 0,
},
{
topic: 'test',
messages: [ '3 ' + new Date(), '4 ' + new Date() ],
},
];
app.producer.send(payloads, function(err, data) {
console.log('data', data);
if (!err) {
ctx.helper.success({
ctx,
res: data,
});
}
});
}

基本命令

安装:
kafka依赖java环境,因此你可能需要先安装好java环境。

1
2
3
4
5
// mac 环境使用brew直接安装kafka
brew install kafka

// 卸载
brew uninstall kafka

mac 安装kafka过程中会自动的安装好zookeeper。

启动:

1
2
3
4
5
6
7
// 启动
brew services start kafka
brew services start zookeeper

// 重启
brew services restart kafka
brew services restart zookeeper

查看kafaka版本:

1
2
3
4
5
6
7
cd kafka

# 执行以下命令
find ./libs/ -name \*kafka_\* | head -1 | grep -o '\kafka[^\n]*'

drwxrwxr-x. 6 root root 117 May 18 2016 kafka_2.11-0.10.0.0
获得了版本为2.11-0.10.0.0。

前台启动Zookeeper 和kafka

1
2
3
bin/zookeeper-server-start.sh config/zookeeper.properties

bin/kafka-server-start.sh config/server.properties

后台启动Zookeeper 和kafka

1
2
3
4
5
6
7
bin/zookeeper-server-start.sh config/zookeeper.properties 1>/dev/null  2>&1  &

bin/kafka-server-start.sh config/server.properties 1>/dev/null 2>&1 &

bin/zookeeper-server-start.sh -daemon config/zookeeper.properties

bin/kafka-server-start.sh -daemon config/server.properties

其中1>/dev/null 2>&1 是将命令产生的输入和错误都输入到空设备,也就是不输出的意思。/dev/null代表空设备。

关闭命令

1
2
3
bin/zookeeper-server-stop.sh config/zookeeper.properties

bin/kafka-server-stop.sh config/server.properties

kafka配置说明:
配置文件server.properties

1
2
3
4
5
// mac 电脑配置文件地址
vim /usr/local/etc/kafka/server.properties

// Linux 配置文件地址
vim /usr/src/kafka/config/server.properties

备注: 尽量配置host.name, 例如本地的配置

1
2
host.name = 127.0.0.1
port=9092

常用基本命令:
创建一个主题(topic)

1
2
3
4
5
// mac
kafka-topics --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic test

// linux
bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic test

删除topic

1
2
3
4
5
// mac 
kafka-topics --delete --zookeeper localhost:2181 --topic 【topic name】

// linux
bin/kafka-topics.sh --delete --zookeeper localhost:2181 --topic 【topic name】

查看创建的topic list

1
2
3
4
5
// mac
kafka-topics --list --zookeeper localhost:2181

// linux
bin/kafka-topics.sh --list --zookeeper localhost:2181

查看详情

1
2
// linux
bin/kafka-topics.sh --describe --zookeeper localhost:2181 --topic

生产消息

1
2
3
4
5
// mac 
kafka-console-producer --broker-list localhost:9092 --topic test

// linux
bin/kafka-console-producer.sh --broker-list localhost:9092 --topic test

消费消息

1
2
3
4
5
// mac 
kafka-console-consumer --bootstrap-server localhost:9092 --topic test --from-beginning

// linux
bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test --from-beginning

zookeeper

安装
1
sudo apt-get install zookeeper

默认信息

1
2
3
4
#安装路径
/usr/share/zookeeper
#配置文件
/etc/zookeeper/conf/zoo.cfg
卸载
1
2
3
4
sudo apt-get remove zookeeper
sudo apt-get remove --auto-remove zookeeper
sudo apt-get purge zookeeper
sudo apt-get purge --auto-remove zookeeper
0%