简单记录下这个过程。最后再输出完整过程。

发现 9.23

偶然机会,我到专壹自习室进行了体验,发现专壹自习室内的是由全自动化的设备进行无人化运营的。勾起了我的兴趣。打算把家里改造成智能化
以此记录整个俊瑶的智能家的始末。

cgUJfv

调研-立项-立flag 9.25

再次调研专壹自习室、考拉自习室、一隅自习室。立项,我要把家里改造成智能家居,并确认一下自己提出的几个要求

1.尽可能能接入各个厂商的设备
2.网关协议尽可能的自主可控
3.外网可控。

立个最终效果flag,以此作为目标

o80tAU 1q64kZ QxC8wg

技术初步调研及尝试

首先买个小米多模网关。结论-坑,只兼容小米的部分zigbee设备,对别的设备不兼容。放弃
WzSFZr

技术最终选型

  1. home assistant+群晖(树莓派也可)vuMYFd WfX0H5
  2. zigbee2mqtt9gQejz 3.网关板子 有线版 mFPTeZ 无线版 mFWj6b

初步原理:将所有设备连如网关,再由网关上云,通过云来控制网关下发控制子设备。

进度:

1.接入米家台灯

发现家里的小米台灯是yeelight,立刻接入尝试。

2.接入智能插座wifi版+智能插座蓝牙网关版

pTP7Zz

3.接入人体红外

5P5g7Z

4.接入落地灯

HjxUy6

5.接入慧作吸顶风扇灯 ✖️2

6I5MiW

6.接入无线开关六键版

jAVKcO

激活函数是深度学习,亦或者说人工神经网络中一个十分重要的组成部分,它可以对神经元的接收信息进行非线性变换,将变换后的信息输出到下一层神经元。激活函数作用方式如下公式所示:

01d9Nn

其中,𝐴𝑐𝑡𝑖𝑣𝑎𝑡𝑖𝑜𝑛()就是激活函数。

为什么要使用激活函数呢?当我们不用激活函数时,网络中各层只会根据权重𝑤和偏差𝑏只会进行线性变换,就算有多层网络,也只是相当于多个线性方程的组合,依然只是相当于一个线性回归模型,解决复杂问题的能力有限。我们希望我们的神经网络能够处理复杂任务,如语言翻译和图像分类等,线性变换永远无法执行这样的任务。激活函数得加入能对输入进行非线性变换,使其能够学习和执行更复杂的任务。

另外,激活函数使反向传播成为可能,因为激活函数的误差梯度可以用来调整权重和偏差。如果没有可微的非线性函数,这就不可能实现。

总之,激活函数的作用是能够给神经网络加入一些非线性因素,使得神经网络可以更好地解决较为复杂的问题。

常用激活函数

1、sigmoid函数

sigmoid函数可以将整个实数范围的的任意值映射到[0,1]范围内,当当输入值较大时,sigmoid将返回一个接近于1的值,而当输入值较小时,返回值将接近于0。sigmoid函数数学公式和函数图像如下所示:

m9JssL

感受一下TensorFlow中的sigmoid函数:

1
2
3
import tensorflow as tf
x = tf.linspace(-5., 5.,6)
x

<tf.Tensor: id=3, shape=(6,), dtype=float32, numpy=array([-5., -3., -1., 1., 3., 5.], dtype=float32)>

有两种方式可以调用sigmoid函数:
方式一:

1
tf.keras.activations.sigmoid(x)
1
2
<tf.Tensor: id=4, shape=(6,), dtype=float32, numpy=
array([0.00669285, 0.04742587, 0.26894143, 0.7310586 , 0.95257413 , 0.9933072 ], dtype=float32)>

方式二:

1
tf.sigmoid(x)
1
2
<tf.Tensor: id=5, shape=(6,), dtype=float32, numpy=
array([0.00669285, 0.04742587, 0.26894143, 0.7310586 , 0.95257413 , 0.9933072 ], dtype=float32)>

看,𝑥中所有值都映射到了[0,1]范围内。

sigmoid优缺点总结:

  • 优点:输出的映射区间(0,1)内单调连续,非常适合用作输出层,并且比较容易求导。
  • 缺点:具有软饱和性,即当输入x趋向于无穷的时候,它的导数会趋于0,导致很容易产生梯度消失。
2、relu函数

Relu(Rectified Linear Units修正线性单元),是目前被使用最为频繁得激活函数,relu函数在x<0时,输出始终为0。由于x>0时,relu函数的导数为1,即保持输出为x,所以relu函数能够在x>0时保持梯度不断衰减,从而缓解梯度消失的问题,还能加快收敛速度,还能是神经网络具有稀疏性表达能力,这也是relu激活函数能够被使用在深层神经网络中的原因。由于当x<0时,relu函数的导数为0,导致对应的权重无法更新,这样的神经元被称为”神经元死亡”。

relu函数公式和图像如下:
fMAhbi
在TensorFlow中,relu函数的参数情况比sigmoid复杂,我们先来看一下:

1
tf.keras.activations.relu( x, alpha=0.0, max_value=None, threshold=0 )
  • x:输入的变量
  • alpha:上图中左半边部分图像的斜率,也就是x值为负数(准确说应该是小于threshold)部分的斜率,默认为0
  • max_value:最大值,当x大于max_value时,输出值为max_value
  • threshold:起始点,也就是上面图中拐点处x轴的值
    1
    2
    3
    x = tf.linspace(-5., 5.,6)
    x
    <tf.Tensor: id=9, shape=(6,), dtype=float32, numpy=array([-5., -3., -1., 1., 3., 5.], dtype=float32)>
    1
    2
    tf.keras.activations.relu(x)
    <tf.Tensor: id=10, shape=(6,), dtype=float32, numpy=array([0., 0., 0., 1., 3., 5.], dtype=float32)>
    1
    2
    tf.keras.activations.relu(x,alpha=2.)
    <tf.Tensor: id=11, shape=(6,), dtype=float32, numpy=array([-10., -6., -2., 1., 3., 5.], dtype=float32)>
    1
    2
    tf.keras.activations.relu(x,max_value=2.)  # 大于2部分都将输出为2.
    <tf.Tensor: id=16, shape=(6,), dtype=float32, numpy=array([0., 0., 0., 1., 2., 2.], dtype=float32)>
    1
    2
    tf.keras.activations.relu(x,alpha=2., threshold=3.5)  # 小于3.5的值按照alpha * (x - threshold)计算
    <tf.Tensor: id=27, shape=(6,), dtype=float32, numpy=array([-17., -13., -9., -5., -1., 5.], dtype=float32)>
3、softmax函数

softmax函数是sigmoid函数的进化,在处理分类问题是很方便,它可以将所有输出映射到成概率的形式,即值在[0,1]范围且总和为1。例如输出变量为[1.5,4.4,2.0],经过softmax函数激活后,输出为[0.04802413, 0.87279755, 0.0791784 ],分别对应属于1、2、3类的概率。softmax函数数学公式如下:
mgdmhg

1
2
3
tf.nn.softmax(tf.constant([[1.5,4.4,2.0]]))
<tf.Tensor: id=29, shape=(1, 3), dtype=float32, numpy=
array([[0.04802413, 0.87279755, 0.0791784 ]], dtype=float32)>
1
2
3
tf.keras.activations.softmax(tf.constant([[1.5,4.4,2.0]]))
<tf.Tensor: id=31, shape=(1, 3), dtype=float32, numpy=
array([[0.04802413, 0.87279755, 0.0791784 ]], dtype=float32)>
1
2
x = tf.random.uniform([1,5],minval=-2,maxval=2)
x
1
2
<tf.Tensor: id=38, shape=(1, 5), dtype=float32, numpy=
array([[ 1.9715171 , 0.49954653, -0.37836075, 1.6178164 , 0.80509186]] , dtype=float32)>
1
2
3
tf.keras.activations.softmax(x)
<tf.Tensor: id=39, shape=(1, 5), dtype=float32, numpy=
array([[0.42763966, 0.09813169, 0.04078862, 0.30023944, 0.13320053]] , dtype=float32)>
4、tanh函数

tanh函数无论是功能还是函数图像上斗鱼sigmoid函数十分相似,所以两者的优缺点也一样,区别在于tanh函数将值映射到[-1,1]范围,其数学公式和函数图像如下:

bu58hv

1
2
3
x = tf.linspace(-5., 5.,6)
x
<tf.Tensor: id=43, shape=(6,), dtype=float32, numpy=array([-5., -3., -1., 1., 3., 5.], dtype=float32)>
1
2
3
4
tf.keras.activations.tanh(x)
<tf.Tensor: id=44, shape=(6,), dtype=float32, numpy=
array([-0.99990916, -0.9950547 , -0.7615942 , 0.7615942 , 0.9950547 ,
0.99990916], dtype=float32)>

总结

神经网络中,隐藏层之间的输出大多需要通过激活函数来映射(当然,也可以不用,没有使用激活函数的层一般称为logits层),在构建模型是,需要根据实际数据情况选择激活函数。TensorFlow中的激活函数可不止这4个,本文只是介绍最常用的4个,当然,其他激活函数大多是这几个激活函数的变种。

模型构建

1
2
3
4
5
6
7
8
9
10
11
12
import tensorflow as tf
from tensorflow.keras import layers
print(tf.__version__)
print(tf.keras.__version__)

model = tf.keras.Sequential()
# 往模型中添加一个有64个神经元组成的层,激活函数为relu:
model.add(layers.Dense(64, activation='relu'))
# 再添加一个:
model.add(layers.Dense(64, activation='relu'))
# 添加一个有10个神经元的softmax层作为输出层:
model.add(layers.Dense(10, activation='softmax'))

等价于:

1
2
3
4
5
model = tf.keras.Sequential([
layers.Dense(64, activation='relu'),
layers.Dense(64, activation='relu'),
layers.Dense(10, activation='softmax')]
)

定义神经网络层通过tf.keras.layers模块中的Dense类实现,Dense类构造参数如下:

  • units:指定神经元个数,必须是一个正整数。
  • activation:激活函数,可以是可以是一个可调用对象或标识一个对象的字符串
  • use_bias:布尔型,是否使用是否使用偏置项
  • kernel_initializer和bias_initializer:权值、偏置初始化方法,可以是一个可调用对象或标识一个对象的字符串
  • kernel_regularizer和bias_regularizer:对权值、偏置进行正则化的方法,可以是一个可调用对象或标识一个对象的字符串
  • activity_regularizer:对层的输出进行正则化的方法,可以是一个可调用对象或标识一个对象的字符串
  • kernel_constraint和bias_constraint:对权值矩阵、偏置矩阵的约束方法,可以是一个可调用对象或标识一个对象的字符串

训练模型

建立好模型之后,接下来当然是要进行训练模型了。不过,在训练前还需要做一些配置工作,例如指定优化器、损失函数、评估指标等,这些配置参数的过程一般通过tf.keras.Model.compile方法进行,先来熟悉一下tf.keras.Model.compile方法的三个常用参数:

  • optimizer:tf.keras.optimizers模块中的优化器实例化对象,例如 tf.keras.optimizers.Adam或 tf.keras.optimizers.SGD的实例化对象,当然也可以使用字符串来指代优化器,例如’adam’和’sgd’。
  • loss:损失函数,例如交叉熵、均方差等,通常是tf.keras.losses模块中定义的可调用对象,也可以用用于指代损失函数的字符串。
  • metrics:元素为评估方法的list,通常是定义在tf.keras.metrics模块中定义的可调用对象,也可以用于指代评估方法的字符串。

在知道怎么配置模型训练参数后,就可以根据实际应用情况合理选择优化器、损失函数、评估方法等:

1
2
3
4
5
6
7
8
9
# 回归模型
model.compile(optimizer=tf.keras.optimizers.Adam(0.01), # 指定优化器,学习率为0.01
loss='mse', # 指定均方差作为损失函数
metrics=['mae']) # 添加绝对值误差作为评估方法

# 分类模型
model.compile(optimizer=tf.keras.optimizers.RMSprop(0.01),
loss=tf.keras.losses.CategoricalCrossentropy(), # 分类模型多用交叉熵作为损失函数
metrics=[tf.keras.metrics.CategoricalAccuracy()])

通过compile()配置好模型后,就可以开始训练了。tf.keras中提供了fit()方法对模型进行训练,先来看看fit()方法的主要参数:

  • x和y:训练数据和目标数据
  • epochs:训练周期数,每一个周期都是对训练数据集的一次完整迭代
  • batch_size:簇的大小,一般在数据集是numpy数组类型时使用
  • validation_data:验证数据集,模型训练时,如果你想通过一个额外的验证数据集来监测模型的性能变换,就可以通过这个参数传入验证数据集
  • verbose:日志显示方式,verbose=0为不在标准输出流输出日志信息,verbose=1为输出进度条记录,verbose=2为每个epoch输出一行记录
  • callbacks:回调方法组成的列表,一般是定义在tf.keras.callbacks中的方法
  • validation_split:从训练数据集抽取部分数据作为验证数据集的比例,是一个0到1之间的浮点数。这一参数在输入数据为dataset对象、生成器、keras.utils.Sequence对象是无效。
  • shuffle:是否在每一个周期开始前打乱数据

下面分别说说如何使用fit()方法结合numpy数据和tf.data.Dataset数据进行模型训练。

1
2
3
4
5
6
import numpy as np

data = np.random.random((1000, 32))
labels = np.random.random((1000, 10))

model.fit(data, labels, epochs=10, batch_size=32)

评估与预测

训练好的模型性能如何,评估测试一下就知道了。可以使用模型自带的evaluate()方法和predict()方法对模型进行评估和预测。

1
2
3
4
5
# 如果是numpy数据,可以这么使用
data = np.random.random((1000, 32))
labels = np.random.random((1000, 10))

model.evaluate(data, labels, batch_size=32)
1
2
3
4
5
# 如果数Dataset对象,可以这么使用
dataset = tf.data.Dataset.from_tensor_slices((data, labels))
dataset = dataset.batch(32)

model.evaluate(dataset)

使用predict()方法进行预测:

1
2
3
# numpy数据
result = model.predict(data, batch_size=32)
print(result.shape)
1
2
3
# dataset数据
result = model.predict(dataset)
print(result.shape)

traefik之docker-compose.yml

traefik 一般需要一个配置文件来管理路由,服务,证书等。我们可以通过 docker 启动 traefik 时来挂载配置文件,docker-compose.yaml 文件如下
traefik 默认有一个 dashboard,通过 :8080 端口暴露出去。我们可以在浏览器中直接通过 :8080 访问,但是

使用 IP 地址肯定不是特别方便,此时我们可以配置 Host
在公网环境下访问有安全性问题,此时可以配置 basicAuth,digestAuth,IpWhiteList 或者 openVPN

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
version: '3'

services:
reverse-proxy:
image: traefik
ports:
- "52080:80"
- "52081:8080"
volumes:
- ./traefik.toml:/etc/traefik/traefik.toml
- /var/run/docker.sock:/var/run/docker.sock
container_name: traefik
labels:
- "traefik.http.routers.api.rule=Host(`www.hong.local`)"
- "traefik.http.routers.api.service=api@internal"

traefik.toml

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
## Static configuration
[global]
checkNewVersion = true
sendAnonymousUsage = false

[entryPoints]
[entryPoints.http]
address = ":80"

[entryPoints.traefik]
address = ":8080"

[api]
insecure = true
dashboard = true
#debug = true


[providers]
[providers.docker]
endpoint = "unix:///var/run/docker.sock"
defaultRule = "Host(`{{ normalize .Name }}.docker.localhost`)"
# 限制服务发现范围
# 如果设置为 false, 则没有 traefik.enable=true 标签的容器将从生成的路由配置中忽略
exposedByDefault = false
#[providers.file]
# filename = "dynamic_conf.toml"
# watch = true
[metrics]
[metrics.prometheus]

whoami之docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
version: '3'

services:
whoami:
image: containous/whoami
labels:
# 声明公开此容器访问
- traefik.enable=true
# 服务将响应的域
- traefik.http.routers.whoami.rule=Host(`www.jun.local`)
networks:
default:
external:
name: traefik_default

那 whoami 这个 http 服务做了什么事情呢

暴露了一个 http 服务,主要提供一些 header 以及 ip 信息
配置了容器的 labels,设置该服务的 Host 为 whoami.docker.localhost,给 traefik 提供标记

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数字识别

0%