docker 开机自启动

在运行docker容器时可以加如下参数来保证每次docker服务重启后容器也自动重启:

1
docker run --restart=always

如果已经启动了则可以使用如下命令:

1
docker update --restart=always <CONTAINER ID>

postgres及数据持久化

1
docker run --name docker_postgres -d -p 5432:5432 -v /Users/hongjunyao/Desktop/postgres:/var/lib/postgresql/data -e POSTGRES_DB=baohan -e POSTGRES_USER=honng -e POSTGRES_PASSWORD='hong' postgres:9.6
持久化

mysql及数据持久化

1
docker run -p 3306:3306 --name docker_mysql -e TZ=Asia/Shanghai -v /Users/hongjunyao/Desktop/mysql/conf:/etc/mysql/conf.d -v /Users/hongjunyao/Desktop/mysql/logs:/logs -v /Users/hongjunyao/Desktop/mysql/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=hong -d mysql:5.7.23
持久化

redis及数据持久化

1
2
3
4
5
6
7
8

docker run \
-p 6379:6379 \
-v /Users/hongjunyao/Desktop/redis/data:/data \
-v /Users/hongjunyao/Desktop/redis/conf/redis.conf:/etc/redis/redis.conf \
--privileged=true \
--name docker_redis \
-d redis redis-server /etc/redis/redis.conf
持久化

想实现这个效果,就需要将 Puppeteer 的 headless 选项设为 false,并将 slowMo 设为 20-100 中的某个值,前者使得所有浏览器自动化操作可见,后者控制了动作之间的间隔,使其变慢,从而通过人眼可以看清每步操作。示例代码:

1
2
3
4
browser = await puppeteer.launch({
headless: false,
slowMo: 20
});

导航到某个页面
这个操作太常用了!第一步是启动浏览器,那么第二步就是导航到某个页面,代码示例:

1
2
page = await browser.newPage();
await page.goto('https://baidu.com');

上述代码会开启一个新页面,并将其导航到 https://baidu.com。

等待某个 DOM 节点出现
在进行某些页面操作前,我们必须要等待指定的 DOM 加载完成后才能操作,比如,一个 Input 没有加载出来时,你是无法在里面输入字符的等等。在 Puppeteer 中,你可以使用 page.waitForSelector 和选择器来等待某个 DOM 节点出现:

1
await page.waitForSelector('#loginForm');

上述代码会等待 ID 为 loginForm 的节点出现。

等待几毫秒
有时候,你找不到某个特定的时刻,只能通过时间间隔来确定,那么此时你可以使用 page.waitFor(number) 来实现:

1
await page.waitFor(500);

上述代码会等待 500 毫秒。

等待某个 JavaScript 函数返回 true
有时候,你需要等待某个复杂的时刻,这个时刻只能通过一些复杂的 JavaScript 函数来判断,那么此时你可以使用 page.waitFor(Function) 来实现:

1
await page.waitFor(() => !document.querySelector('.ant-spin.ant-spin-spinning'));

上述代码会等待 Antd 中的旋转图标消失。

向某个 Input 中输入字符
为了模拟用户登陆或仅仅就是输入某个表单,我们经常会向某个 Input 中输入字符,那么我们可以使用这个方法:

1
await page.type('#username', 'lewis');

上述代码向 ID 为 username 的 Input 中输入了 lewis。值得一提的是,该方法还会触发 Input 的 keydown、keypress, 和 keyup 事件,所以如果你有该事件的相关功能,也会被测试到哦,是不是很强大?

点击某个节点
在 Puppeteer 中模拟点击某个节点,非常简单,只需要:

1
await page.click('#btn-submit');

上述代码点击了 ID 为 btn-submit 的节点。

在浏览器中执行一段 JavaScript 代码
有时候我们需要在浏览器中执行一段 JavaScript 代码,此时你可以这样写:

1
page.evaluate(() => alert('1'));

上述代码会在浏览器执行 alert(‘1’)。

获取某一个节点的某个属性
有时候我们需要获取某个 Input 的 value,某个链接的 href,某个节点的文本 textContent,或者 outerHTML,那么你可以使用这个方法:

1
2
3
4
const searchValue = await page.$eval('#search', el => el.value);
const preloadHref = await page.$eval('link[rel=preload]', el => el.href);
const text = await page.$eval('.text', el => el.textContent);
const html = await page.$eval('.main-container', e => e.outerHTML);

获取某一类节点的某个属性集合
有时候我们需要获取某一类节点的某个属性集合,那么你可以这么写:

1
const textArray = await page.$$eval('.text', els => Array.from(els).map(el => el.textContent));

上述代码将页面中所有类为 text 的节点中的文本拼装为数组放到了 textArray 中。

postgres的copy to

PostgreSQL 的 COPY TO 直接可以干这个事情,而且导出速度是非常快的。下面例子是把 products 表导出成 CSV :

1
2
3
COPY products
TO '/path/to/output.csv'
WITH csv;

可以导出指定的属性:

1
2
3
COPY products (name, price)
TO '/path/to/output.csv'
WITH csv;

也可以配合查询语句,比如最常见的 SELECT :

1
2
3
4
5
6
7
COPY (
SELECT name, category_name
FROM products
LEFT JOIN categories ON categories.id = products.category_id
)
TO '/path/to/output.csv'
WITH csv;

导入 CSV
跟上面的导出差不多,只是把 TO 换成 FROM ,举例:

1
2
3
COPY products
FROM '/path/to/input.csv'
WITH csv;

这个命令做导入是非常高效的,在开头那篇博客作者的测试中,COPY 只花了 INSERT 方案 1/3 的时间,而后者还用 prepare statement 优化过。

示例

示例1.将整张表拷贝至标准输出
1
2
3
test=# copy tbl_test1 to stdout;
1 HA 12
2 ha 543
示例2.将表的部分字段拷贝至标准输出,并输出字段名称,字段间使用’,’分隔
1
2
3
4
test=# copy tbl_test1(a,b) to stdout delimiter ',' csv header;
a,b
1,HA
2,ha
示例3.将查询结果拷贝至标准输出
1
2
3
test=# copy (select a,b from tbl_test1 except select e,f from tbl_test2 ) to stdout delimiter ',' quote '"' csv header;
a,b
2,ha

将标准输入拷贝至表中需要注意几点

1.字段间分隔符默认使用【Tab】键
2.换行使用回车键
3.结束使用反斜线+英文据点(.)
4.最好指定字段顺序,要不然可能会错位赋值

示例4.将标准输入拷贝至表中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
test=# copy tbl_test1(a,b,c) from stdin;
Enter data to be copied followed by a newline.
End with a backslash and a period on a line by itself.
>> 1 公举 公主
>> 2 万岁 万万岁
>> \.
COPY 2
test=# select * from tbl_test1 ;
a | b | c
---+------+--------
1 | HA | 12
2 | ha | 543
1 | 公举 | 公主
2 | 万岁 | 万万岁
(4 rows)
示例5.从标准输入拷贝至表中,并将标准输入第一行作为字段名(和表中不符也没关系,copy会自动忽略第一行),字段分隔符为’,’
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
test=# copy tbl_test1(a,b,c) from stdin delimiter ',' csv header;
Enter data to be copied followed by a newline.
End with a backslash and a period on a line by itself.
>> a,b,c
>> 3,你好,hello
>> 4,超人,super
>> \.
COPY 2
test=# select * from tbl_test1 ;
a | b | c
---+------+--------
1 | HA | 12
2 | ha | 543
1 | 公举 | 公主
2 | 万岁 | 万万岁
3 | 你好 | hello
4 | 超人 | super
(6 rows)

以上是表与标准输出和标准输入间的相互拷贝,表与文件的拷贝和以上完全相同,只是将标准输出和标准输入换成文件。需要注意的是:

1.数据库用户必须有文件所在的路径的写权限。
2.如果表存在中文字符,导出至csv文件时需要设置编码为GBK,否则使用excel打开是中文显示乱码。
3.将文件导入表中时仍要考虑编码问题

示例6.将表拷贝至csv文件中
1
2
test=# copy tbl_test1 to '/tmp/tbl_test1.csv' delimiter ',' csv header;
COPY 6

使用excel打开文件,中文显示为乱码

示例7. 将表以GBK编码拷贝至csv文件中
1
2
test=# copy tbl_test1 to '/tmp/tbl_test1.csv' delimiter ',' csv header encoding 'GBK';
COPY 6

使用excel打开,中文显示正常

示例8.将刚才导出的文件再次拷贝至表中,使用默认编码UTF8
1
2
3
test=# copy tbl_test1(a,b,c) from '/tmp/tbl_test1.csv' delimiter ',' csv header;
ERROR: invalid byte sequence for encoding "UTF8": 0xb9
CONTEXT: COPY tbl_test1, line 4

示例9.将刚才导出的文件再次拷贝至表中,使用GBK编码

1
2
test=# copy tbl_test1(a,b,c) from '/tmp/tbl_test1.csv' delimiter ',' csv header encoding 'GBK';
COPY 6

说明:3Blue1Brown视频的关键点的记录,用来当笔记随时查阅和再学习的。

一、向量的本质

三个视角:

  • 物理专业学生:一定方向和长度的箭头,可以自由移动一个向量保持它不变。
  • 计算机专业学生:向量是有序的数字列表。
  • 数学专业学生:向量可以是任何事物,只要保证两个向量相加以及数字与向量相乘是有意义的即可。

加法的意义:如[a,b]表示点从原点出发沿x轴正轴移动a,沿y轴正轴移动b。那么[a,b]+[c,d]可以先考虑完x轴移动再考虑y轴移动,于是我们得到[a+c,b+d]
数乘的意义:缩放。

二、线性组合、张成的空间与基

基向量:[a,b]可以理解为将基向量$\vec{i}$,$\vec{j}$分别缩放a,b倍以后相加的结果。
线性组合:其实是向量之间的线性组合,其主体是向量,线性组合是一个操作,将各个向量缩放之后,相加在一起,就得到了参与操作的向量之间的线性组合。
张成的空间:v与w全部的线性组合所构成向量集合被称为张成的空间。

三、矩阵与线性变换

变换其实也是一种函数,我们有一个输入向量,然后经过变换之后,得到一个输出向量。整个过程,可以看作是输入的向量移动到了输出的输出的位置。考虑整个平面上的向量,在经过变换之后,得到了一个最新的位置。

四、矩阵乘法与线性变换复合

两个22矩阵a和b相乘,可以看作是对原始空间连续做了两次线性变换,而得到的计算结果c也是一个22的矩阵。使用c对原始空间进行一次线性变换,和连续使用a和b对原始空间进行两次线性变换的效果相同。

矩阵相乘的几何意义是将两次单独的变换变为一次组合变换即可。

该结论到三维空间中也是同样成立的。

五、行列式

如果在二维空间中,我们画出相对应的网格,那么线性变换,就是对这些网格做了拉伸,收缩或者反转。那么如何来定义这种变换的程度呢?就需要用到行列式determinant的概念了。
线性变换后的图形面积缩放比例,正负号与定向有关(右手法则)
在进行线性变换后,原来一个面积为1的单位方格,变成了面积为6的矩形。可以说,线性变换将原空间放大了6倍。
我们知道,行列式的值是有正有负的,那么怎么判断是负数呢?我们可以通过变换后的基向量i和j的方向来判定。

在变换之前,j是在i的左侧的:

如果经过线性变换后,j变成了在i的右侧,那么得到的行列式的值是负的:

那么到三维空间中,行列式的值就告诉我们经过线性变换后,单位体积变化的程度,而行列式的值可以通过右手定则来判定:

行列式这个“怪物”定义初看很奇怪,一堆逆序数什么的让人不免觉得恐惧,但其实它是有实际得不能更实际的物理意义的,理解只需要三步
1,行列式det(A)是针对一个n$\times$n的矩阵A而言的。A表示一个n维空间到n维空间的线性变换。那么什么是线性变换呢?无非是一个压缩或拉伸啊。假想原来空间中有一个n维的立方体(随便什么形状),其中立方体内的每一个点都经过这个线性变换,变成n维空间中的一个新立方体。
2,原来立方体有一个体积$V_{1}$,新的立方体也有一个体积$V_{2}$ 。
3,行列式det(A)是一个数对不对?这个数其实就是 $V_{2} \div V_{1}$ ,结束了。
行列式的计算:
二阶行列式的计算方法是“对角线法则”:

主对角线元素积与副对角线元素积的差

二阶行列式的法则并不适用三阶行列式。三阶行列式的计算方法如下

这个依然叫“对角线法则”,不过是复杂版的:主对角线乘完以后元素位置要平移一下继续相乘,直到x、y、z分别开过头以后,再分别减去x、y、z开头的副对角线乘积。

六、逆矩阵、列空间与零空间

逆矩阵

如果一个变换AA将空间压缩到了一条线上,那么就说AA的秩是1。如果压缩成一个平面,就是2。 所以,”秩“代表着变换后空间的维数。
比如对于2×22×2的矩阵,秩最大就是2,意味着基向量仍旧能张成整个二维空间。但对于3×33×3矩阵,秩为2意味着空间被压缩了。

列空间

列空间有两种解释:
1)假设矩阵A代表一个矩阵变换,原始空间中所有的向量,在经由矩阵A的变换之后,所得到的所有新向量的集合
2)由矩阵A的列向量所长成的空间
比如下面的例子,[[2,-2],[1,-1]]这个矩阵,将二维空间变换为一条直线,那么这条直线就是矩阵的列空间。

零空间

如果某个向量空间在线性变换之后,存在降维,那么就会有一系列原来不是零向量的向量落到了零向量的位置,所有这些向量的集合构成了零空间。

概述

本文将初步介绍如何使用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之间通信出了问题

基本概念

0%