K162

Starwalker, Stardust.

0%

Recently, I try to manage the documents, papers and references with Zotero. To keep my libraries updated between my devices and multi platforms, I decide to set up my Raspberry Pi (using Raspberry Pi OS) as an WebDAV server.

If you are in a Linux environment such as Ubuntu, the steps for configuration are similar.

Step 1: Install Apache2 and enable WebDAV module

Firstly, we need to install Apache Web server to enable WebDAV file sharing:

1
sudo apt install apache2

And enable WebDAV module with the directive following:

1
2
sudo a2enmod dav
sudo a2enmod dav_fs

Step 2: Create the directory for WebDAV

We will organize the documents files within the folder we created:

1
sudo mkdir /var/www/webdav

Remember to set the owner and group of the directory to apache’s user and group, granting permission to www-data:

1
sudo chown www-data:www-data /var/www/webdav

Step 3: Configure Apache Host

Add the following directives inside the <VirtualHost> tags in the file /etc/apache2/sites-available/000-default.conf:

1
2
3
4
5
6
7
8
Alias /webdav /var/www/webdav
<Location /webdav>
DAV On
AuthType Basic
AuthName "WebDAV"
AuthUserFile /etc/apache2/webdav.users
Require valid-user
</Location>

Step 4: Create user for WebDAV

Use the htpasswd utility to create a user for WebDAV accessing:

1
sudo htpasswd -c /etc/apache2/webdav.users your_username

Step 5: Restart Apache

Restart apache2 to apply the change:

1
sudo systemctl restart apache2

If you encounter some error message like below when restarting apache.service:

1
Address already in use: AH00072: make_sock: could not bind to address...

Please check the processes or services listening on the port that Apache is configured to use.

We can open Web browser and type http:/server_ip_or_domain/webdav to check the connection and WebDAV availability. Enter the user and password in the pop-up window, then you will access the directory we created.

Finally, Zotero could synchronize the documents with the local server at a fast speed. (树莓派吃灰属性 -1 😆)

When I install FreePBX in Vmare Workstation Player with the ISO image STABLE SNG7-PBX-64bit-2203-2 downloaded from FreePBX Official website, it raised an error message like screenshot below.

kernel-devel error

You have specified that the package ‘kernel-devel’ should be installed. This package does not exist. Would you like to ignore this package and continue with installation?

Regardless of whether Yes or No is clicked, the installer will be terminated, entering a black screen with a blinking cursor.

Solution

FreePBX ISO should not be setup with Easy Install in Vmware Workstation Player. So please select the option I will install the operating system later in the wizard.

Do Not Select Easy Install

Vmware will detect CentOS 7 in the image. After that, you should be able to install FreePBX properly.

我习惯在 Terminal 中直接使用某软件的 core,接着手动在 系统偏好设置->网络->高级->代理 打开/关闭代理配置。但苦于要一次次在系统偏好设置中来回切换,实在觉得麻烦。

最近发现,Apple 在 Remote Desktop 支持上,提供了一个命令 networksetup,用来快速配置客户端的网络设置。1

例如,可以使用它快速获取当前硬件端口上所有的网络服务:

1
2
3
4
$ networksetup -listallnetworkservices
USB 10/100/1000 LAN
Wi-Fi
Thunderbolt Bridge

或是借助下面的命令,快速配置设备的网络代理。更多用法可以通过 networksetup -help 查看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Web Proxy (HTTP)
networksetup -getwebproxy <networkservice>
networksetup -setwebproxy <networkservice> <domain> <port number> <authenticated> <username> <password>
networksetup -setwebproxystate <networkservice> <on off>

# Secure Web Proxy (HTTPS)
networksetup -getsecurewebproxy <networkservice>
networksetup -setsecurewebproxy <networkservice> <domain> <port number> <authenticated> <username> <password>
networksetup -setsecurewebproxystate <networkservice> <on off>

# SOCKS Proxy
networksetup -getsocksfirewallproxy <networkservice>
networksetup -setsocksfirewallproxy <networkservice> <domain> <port number> <authenticated> <username> <password>
networksetup -setsocksfirewallproxystate <networkservice> <on off>

开心!那我岂不是可以在 捷径(Shortcuts) 中创建一个 shortcut,然后丢 menu bar,就能一键切换代理啦!

shortcut detail shortcut on menu bar

本来还尝试过,想让 Shortcut 打开 Terminal 直接执行命令,或通过捷径让 Terminal 去打开一个 .command 文件,但可惜,都不允许呢~可不管怎样,有这个命令后,真的比以前方便很多了!

参考链接

[1] About networksetup in Remote Desktop

更改 Active Directory 域控制器的计算机名称,一般不应该从 Windows 的设置应用中,直接使用”重命名这台电脑“按钮进行修改。也不应该从”系统属性“面板中,点击”更改“按钮来重命名计算机。因为这可能会造成各种问题,例如造成计算机属性和 AD 数据库中的信息不一致,导致在下次登录服务器时:

The SAM database on the windows server does not have a computer account for this workstation trust relationship.

服务器上的安全数据库没有此工作站信任关系的计算机帐户。

使用 Netdom 修改域控制器计算机名称

所以,建议使用 Netdom 来重命名域控制器的计算机名称。

这里假设我的域控制器计算机名称为 WIN-BLAHBLAH,想修改为 ADS01,域名为 mikumoe.com

操作步骤

0x01 运行 Powershell

以管理员身份运行 Powershell。

在 Powershell 中,你可以通过 /ENUMerate 来获取当前服务器或计算机可用的 FQDN,例如:

1
2
3
4
5
PS C:\Windows\system32> netdom computername WIN-BLAHBLAH.mikumoe.com /enumerate
计算机的所有名称是:

WIN-BLAHBLAH.mikumoe.com
命令成功完成。

在这里,我们只有一条记录。

0x02 新建新计算机名 FQDN

在域控制器上,新建一条新计算机名的 FQDN(Fully Qualified Domain Name)记录。这会在 Active Directory 数据库中新增一条 SPN 记录的同时,也会在 DNS 服务器中新增一条 SRV 记录:

1
netdom computername <当前计算机名称> /add:<新的计算机名称>

例如:

1
2
3
4
5
PS C:\Windows\system32> netdom computername WIN-BLAHBLAH.mikumoe.com /add:ADS01.mikumoe.com
已成功将 ADS01.mikumoe.com
添加为计算机的替换名称。

命令成功完成。

此时,再次通过 /ENUmerate 查看计算机可用的 FQDN,就会发现有了 2 条记录:

1
2
3
4
5
6
PS C:\Windows\system32> netdom computername WIN-BLAHBLAH.mikumoe.com /ENUMerate
计算机的所有名称是:

WIN-BLAHBLAH.mikumoe.com
ADS01.mikumoe.com
命令成功完成。

0x03 设置主名称

使用 /MakePrimary 把新的计算机名称,设置为主名称:

1
netdom computername <当前计算机名称> /makeprimary:<新的计算机名称>

例如:

1
2
3
4
5
6
7
8
9
10
PS C:\Windows\system32> netdom computername WIN-BLAHBLAH.mikumoe.com /makeprimary:ADS01.mikumoe.com
已成功将 ADS01.mikumoe.com 设为计算机的主名称。
必须重新启动计算机才能使此名称更改生效。
在此之前,此计算机可能无法验证
用户和其他计算机,并且无法被林中的其他计算机验证。
指定的新名称已从计算机替换名称列表中删除。
在重新启动后,
主计算机名将被设置为指定的新名称。

命令成功完成。

随后,根据提示,需要重启服务器

0x04 移除旧的计算机名 FQDN

重启完毕后,使用以管理员身份运行 Powershell,移除旧的域控制器计算机名称:

1
netdom computername <新的计算机名称> /remove:<当前计算机名称>

例如:

1
2
3
4
5
PS C:\Windows\system32> netdom computername ADS01.mikumoe.com /remove:WIN-BLAHBLAH.mikumoe.com
已作为计算机的
替换名称成功删除 WIN-BLAHBLAH.mikumoe.com。

命令成功完成。

再次查看计算机可用的 FQDN,此时就只有一条记录了:

1
2
3
4
5
PS C:\Windows\system32> netdom computername ADS01.mikumoe.com /ENUMerate
计算机的所有名称是:

ADS01.mikumoe.com
命令成功完成。

0x05 检查一下

此时,在服务器管理器的面板中,应该已经能看到新的计算机名,显示 ADS01

为确认新的计算机名称已被正常应用,我们还可以打开 DNS 管理器进行确认。

例如,我在正向查找区域 mikumoe.com 中,可以看到新的 FQDN ADS01.mikumoe.com 已经被添加到 DNS 中。

但同时发现,旧的 FQDN WIN-BLAHBLAH.mikumoe.com 依然存在于 DNS 记录里,因此可以右键手动删除

包括在 mikumoe.com/_sites/Default-First-Site-Name/_tcp 目录中的相关 SRV 记录,也更新为了 ADS01.mikumoe.com

最后,还可以使用以下命令验证 FSMO 信息:

1
netdom query fsmo

例如:

1
2
3
4
5
6
7
PS C:\Windows\system32> netdom query fsmo
架构主机 ADS01.mikumoe.com
域命名主机 ADS01.mikumoe.com
PDC ADS01.mikumoe.com
RID 池管理器 ADS01.mikumoe.com
结构主机 ADS01.mikumoe.com
命令成功完成。

Wow, nice!

参考连接

[1] Microsoft Docs, Rename a Domain Controller Using Netdom

关于 BFC

根据 Mozilla 的 MDN 文档1中关于 BFC(Box Formatting Content,块级格式化上下文)的说明:

A block formatting context is a part of a visual CSS rendering of a web page. It’s the region in which the layout of block boxes occurs and in which floats interact with other elements.

Formatting contexts affect layout, but typically, we create a new block formatting context for the positioning and clearing floats rather than changing the layout, because an element that establishes a new block formatting context will:

  • contain internal floats.
  • exclude external floats.
  • suppress margin collapsing.

讲简单点,BFC 是页面上的一个独立容器,内部的元素不会影响外部的元素,反之亦然。

而盒子是否形成 BFC,对元素的 CSS 浮动有重要影响。同时,BFC 还能取消 margin 塌陷(margin collapsing),以及阻止元素被 float 的元素覆盖。

举个栗子

一个没有形成 BFC 的例子:

1
2
3
4
5
6
<div>	<!-- 没有设置 height -->
<p></p>
<p></p>
</div>

<!-- p 标签设置了 height 和 float -->

如果一个父元素(例子中的 div 标签)不设置 height,而内部的子元素都为 float 时,就无法撑起自身:

noBFC

创建 BFC

创建 BFC 的方法有很多,从文档的根元素 <html> 标签,到设置 column-span: all; ,具体可参考 Mozilla 的 MDN 文档1中的详细描述。

但是,针对上述例子中的场景,创建 BFC 有 4 种典型的方法:

  1. 父元素 添加属性 overflow: hidden; (推荐)
  2. 父元素 position 属性的值不是 staticrelative (推荐)
  3. 父元素 float 属性的值不是 none
  4. 父元素 display 的值是 inline-block, flexinline-flex

效果如下:

BFC

清除浮动

浮动一定要封闭到一个盒子中,否则会对页面其他元素产生影响。

举个栗子

一个没有清除浮动的例子:

1
2
3
4
5
6
7
8
9
10
<div>
<p></p>
<p></p>
</div>
<div>
<p></p>
<p></p>
</div>

<!-- p 标签设置了 float -->

原因就在于 div 没有形成 BFC,导致子元素无法形成两行:

noClear

清除浮动的方法

如果父元素的高度固定,那么直接设置 height 属性即可。

但是,父元素高度如果不固定(如,元素内容是动态的),那么就不能设置固定的 height 值。此时,也有几个典型的方法:

1. 父元素形成 BFC

让父元素形成 BFC,如:设置 overflow: hidden;

2. 设置 clear:both; 属性

后面的父元素设置 clear:both;clear 属性用来清除浮动对自己的影响,both 参数表示左右浮动都清除。

但由于前面的父元素依旧没有 height,可能会在后续元素定位上产生些问题。

3. 使用 ::after 伪元素(推荐)

使用 ::after 伪元素,给父元素最后添加一个子元素,并且给 ::after 设置 clear:both

1
2
3
4
5
.clearfix::after {
content: '';
clear: both;
display: block; /* 别忘记设置 */
}

效果如下:

Clear

参考资料

[1] MDN contributors, Block formatting context

对图进行遍历时,我们往往会建立一个数组 vis[],用来记录是否访问过某一结点,或(在广度优先遍历时)建立一个数组 inq[] ,用来记录结点是否进入过队列。

但如果需要获得遍历过程中,从起始结点到各个结点的路径,可以通过记录结点的前驱来实现。

PHP 关联数组

在 PHP 的实现中,可以直接将数组 vis[] (或是 inq[]) 换作一个关联数组,用来记录结点前驱。其中,下一个结点 $next 作为关联数组的键,$node 当前结点为值:

1
2
3
4
$inq[ $next ] = $node;
// $inq 关联数组
// $next 下一个结点
// $node 当前结点,即作为下一个结点的前驱

遍历过程中,如果要确认某个结点是否被访问过(或是否进入过队列),可使用 array_key_exists(key, array) 函数,检查某一个数组中是否存在某一个键名。例如,若能在 $inq 中找到名为 $next 的键(即返回 true),那就说明 $next 已经进入过队列:

1
2
3
array_key_exists($next, $inq);
// $next 下一个结点,即关联数组的键
// $inq 关联数组

获得路径

建立一个数组 $path,用来保存结点路径, $i 为数组 $path 下标。

如果需要获得从起始结点 $start 到某一结点 $node 的路径,可以通过循环,不断寻找当前结点的前驱,同时记录到数组 $path 中:

1
2
3
4
5
6
$i = 0;
while( $node != $start ){ // 如果没有到达起始结点
$path[ $i ] = $node; // 记录到路径数组中
$node = $inq[ $node ]; // 变成前驱
$i++; // 下标 +1
}

此时数组 $path 中,起始结点在 $path 最后一位。如果需要正向序列,只要逆向输出即可!

在 C/C++ 中的实现

在 C++ 的 STL 容器中有一种 map,也应该可以实现。

map 可以将任何基本类型(包括 STL 容器),映射到任何基本类型(包括 STL 容器):

1
map<typename1, typename2> mp;

当然,在一些简单场景下,前驱的记录可以直接单纯地用一个一维的数值数组实现,类似于数组实现的静态链表。

错误的定义

起因是我在 main() 函数外定义了一个全局变量和数组:

1
2
int n = 10;
int array[n];

然后,编译器就不留情面地报错了:

[Error] array bound is not an integer constant before ‘]’ token

如果是 C,对数组进行类似错误的定义,会出现如下的报错:

[Error] variably modified ‘array’ at file scope

加上 const 限定符

一般而言,C/C++ 的编译器在编译时,一个数组的长度往往要求是静态已知的。因此,如果数组 array[n] 长度是借助一个变量 n 来确定,那么可以加上 const 限定符:

1
2
int const n = 10;	// 加上 const
int array[n];

或者换种方式:

1
2
#define size 10	// 宏定义
int array[size];

《C Primer Plus》中有阐述道,C90 新增的 const 关键字是 用于限定一个变量为只读。同时中文版的译者还在其中注解道:在 C 语言中,用 const 类型限定符声明的是变量,不是常量。1

变长数组(variable-length array, VLA)

在 C 的 C99 标准中,引入了变长数组的特性,即允许使用变量表示数组的维度:

1
2
int n = 10;
int array[n]; // 一个变长数组

但最终,C11 把变长数组变成了一个可选特性。其实,变长数组的“变”也不过是在创建数组时,可以使用变量指定数组的维度。至于想修改已创建的数组长度——tan90!(不存在的!)

变长数组相对而言是新特性,所以目前完全支持这一特性的编译器也不多。

遵守规范

有人在 StackOverflow2 上指出,如果把最上面对变量和数组的错误定义,放在 main() 函数里头,虽然没有报错(这也恰恰符合我的印象),但也是不合规范的:

Both examples are ill-formed in C++. If a compiler does not diagnose the latter, then it does not conform to the standard.

You use a language extension that allows runtime length automatic arrays. But does not allow runtime length static arrays. Global arrays have static storage.

参考资料

[1] Stephen Prata.C Primer Plus[M].北京:人民邮电出版社,2019:78.
[2] eerorika, Why should global array size be an integer constant?

又入手了一台 Surface Pro。然而 DPST(Display Power Saving Technology,显示器节能技术)对我而言,必须得关!

早前有一篇日志已经讨论过 Surface Pro 4 和 The New Surface Pro(5th Gen)关闭 DPST 的方法1。Surface Pro 7 的操作方法也类似,只不过项目在注册表中的位置和数值稍有变化。

Surface Pro 7 注册表中关于 DPST 的设置位于

1
[HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}\0000]

定位到 FeatureTestControl 项,其默认值为 200

之前的日志里有提到:

FeatureTestControl 的字段中,每个值都代表了一个功能特性。其中 0 表示启用某个特性,而 1 则表示禁用。Intel 的显卡驱动正是通过读取该注册表项中的值来决定是否启用相关特性。而 DPST 的值,就在从右往左数第五位。

十六进制 200 的转换成二进制为 0010 0000 0000,把从右往左数的第五位改成 1,即关闭 DPST,那么值为 0010 0001 0000,换算回十六进制为 210

只要把 FeatureTestControl 的值改成 210 即可关闭 DPST。

修改完毕后,重启系统。

参考链接

[1] 新 Surface Pro 的 DPST 设置

环境配置

我习惯使用 Anaconda,这应该是配置开发环境最简单且最方便的方法之一了。环境可以在 Environments 里直接新建一个,接着安装相应的包,安装过程中,请留意选择 GPU 的版本,例如:

  • tensorflow-gpu
  • keras-gpu

设备检测

安装完成后进入环境,尝试打印出设备看看,检查环境是否顺利配置:

1
2
gpus = tf.config.experimental.list_physical_devices('GPU')
print(gpus)

如果顺利,应该会打印类似如下的信息:

[PhysicalDevice(name='/physical_device:GPU:0', device_type='GPU')]

可以看到,Tensorflow 已经能检测到 GPU 了,比如我就只有一块。

开启训练

我随便找了个 CNN 的网络,打算做个简单的测试。但在开启模型训练之后,程序就突然报错:

UnknownError:   Failed to get convolution algorithm. This is probably because cuDNN failed to initialize, so try looking to see if a warning log message was printed above.     [[node sequential/conv2d/Conv2D (defined at C:\path_of_envs\lib\site-packages\tensorflow_core\python\framework\ops.py:1751) ]] [Op:__inference_distributed_function_985]Function call stack:distributed_function

在这过程中,可以通过任务管理器留意到,当程序执行到定义网络结构的部分时,显存会突然开始飙升,直至几乎吃满。怀疑是不是显存炸了所以无法进行训练,毕竟已经有数据顺利跑了一部分了。

plot-regression

根据 Tensorflow 的官方文档1,它是这样描述的:

By default, TensorFlow maps nearly all of the GPU memory of all GPUs (subject to CUDA_VISIBLE_DEVICES) visible to the process. This is done to more efficiently use the relatively precious GPU memory resources on the devices by reducing memory fragmentation.
……
In some cases it is desirable for the process to only allocate a subset of the available memory, or to only grow the memory usage as is needed by the process. TensorFlow provides two methods to control this.

The first option is to turn on memory growth by calling tf.config.experimental.set_memory_growth, which attempts to allocate only as much GPU memory as needed for the runtime allocations
……

也就是说,默认情况下,Tensorflow 会把所有显卡的显存都吃掉,减少碎片从而提高效率。显然我的鸡踢叉 1080 根本经不起这样折腾,所以得靠 tf.config.experimental.set_memory_growth 进行按需分配:

1
2
3
4
5
6
gpus = tf.config.experimental.list_physical_devices('GPU')
try:
for gpu in gpus:
tf.config.experimental.set_memory_growth(gpu, True)
except RuntimeError as ex:
print(ex)

可以把上面这段代码放在 import 部分的下面。如果你对显存大小的分配有更严格的要求,可以使用:

1
2
3
4
5
tf.config.experimental.set_virtual_device_configuration(
# 例如指明第一块显卡
gpus[0],
# 设置分配的显存大小
[tf.config.experimental.VirtualDeviceConfiguration(memory_limit=1024)])

配置完成后,尝试运行~

[============================================================================================================>哇~~~~~~速度简直了!

参考链接

[1] Google, TensorFlow API: Use a GPU.

Tensorflow 从 2.0 版本开始,Keras 就被深度集成在了其中1。同时,Tensorflow 2.0 相较于 1.x 版本,在很多方面都做出了改变。这里我尝试使用 tf.keras 针对 Regression 和 Classification 搭建简单的神经网络,记录一下过程,并进行简单的梳理。

Import

复制粘贴,一次引入,快捷高效,简单粗暴:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import tensorflow as tf
from tensorflow import keras

import matplotlib as mpl
import matplotlib.pyplot as plt

# 为了能在 Jupyter Notebook 中显示图片
%matplotlib inline

import numpy as np
import pandas as pd
import sklearn
import os
import sys
import time

# 打印各个版本信息
print(sys.version_info)
for module in tf, keras, mpl, np, pd, sklearn:
print(module.__name__, module.__version__)

在这,也记录一下我当前的版本信息:

sys.version_info(major=3, minor=7, micro=6, releaselevel='final', serial=0)tensorflow 2.0.0tensorflow_core.keras 2.2.4-tfmatplotlib 3.1.1numpy 1.18.1pandas 0.25.3sklearn 0.22.1

Regression

Dataset

回归问题的数据集,使用的是 sklearn 中的“加州房价”:

1
2
3
from sklearn.datasets import fetch_california_housing

housing = fetch_california_housing()

Data Split

按默认 3:1 的比例拆分数据(即 test_size=0.25),选取出训练集、验证集、测试集。

1
2
3
4
5
6
7
8
9
10
from sklearn.model_selection import train_test_split

x_train_all, x_test, y_train_all, y_test = train_test_split(
housing.data, housing.target, random_state = 7)
x_train, x_valid, y_train, y_valid = train_test_split(
x_train_all, y_train_all, random_state = 11)

print(x_train.shape, y_train.shape) # (11610, 8) (11610,)
print(x_valid.shape, y_valid.shape) # (3870, 8) (3870,)
print(x_test.shape, y_test.shape) # (5160, 8) (5160,)

Normalization

一言不合正则化:

1
2
3
4
5
6
from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
x_train_scaled = scaler.fit_transform(x_train)
x_valid_scaled = scaler.transform(x_valid)
x_test_scaled = scaler.transform(x_test)

这里我想简单说一说 fit_transform()transform() 的区别,因为这个细节我之前也琢磨了一段时间,这里,还得提到另外一个函数,就是 fit()。官方的 API 文档中2,对这三个函数是这样解释的:

fit(self, X[, y])
Compute the mean and std to be used for later scaling.

transform(self, X[, copy])
Perform standardization by centering and scaling.

fit_transform(self, X[, y])
Fit to data, then transform it.

简单而言,fit_transform() 就是整合了 fit()transform() 的功能。当我在训练集上用 fit_transform() 做 scale 的时候,它会先计算均值和方差,并记录下来,这就是 fit() 的功能。然后,再对数据执行归一化,也就是 transform()

而验证集和测试集,都是用的训练集的均值和方差。也就是说,在训练集有过一次拟合后,验证集和测试集就可以直接根据训练集的均值和方差,调用 transform() 执行归一化。

Model

在网络搭建的过程中,相比 1.0 版本,Tensorflow 2.0 的确是引起舒♂适!

1
2
3
4
5
6
7
8
9
10
11
12
# 搭建网络
model = keras.models.Sequential([
keras.layers.Dense(30, activation='relu', input_shape=x_train.shape[1:]),
keras.layers.Dense(1),
])

# 编译模型
model.compile(loss="mean_squared_error",
optimizer = keras.optimizers.SGD(0.001))

callbacks = [keras.callbacks.EarlyStopping(
patience=5, min_delta=1e-2)]

通过 model.summary() 可以看到网络层次,就两个 dense layer,一个输入,一个输出:

Model: "sequential_1"_________________________________________________________________Layer (type)                 Output Shape              Param #   =================================================================dense_1 (Dense)              (None, 30)                270       _________________________________________________________________dense_2 (Dense)              (None, 1)                 31        =================================================================Total params: 301Trainable params: 301Non-trainable params: 0

Train

使用 model.fit() 开启训练。with tf.Session() as sess: 什么的,甚至 tf.global_variables_initializer() 什么的,这里不需要了:

1
2
3
4
history = model.fit(x_train_scaled, y_train,
validation_data = (x_valid_scaled, y_valid),
epochs = 100,
callbacks = callbacks)

Plot

1
2
3
4
5
6
7
def plot_learning_curves(history):
pd.DataFrame(history.history).plot(figsize=(8,5))
plt.grid(True)
plt.gca().set_ylim(0,1)
plt.show()

plot_learning_curves(history)
plot-regression

Evaluate

1
model.evaluate(x_test_scaled, y_test, verbose=0)

看一下测试集的结果:

0.3782297415326732

Classification

Dataset & Data Split

分类问题的数据集使用的是 Keras 的 fashion_mnist

1
2
3
4
5
6
7
8
9
10
11
12
13
# 使用 fashion_mnist 数据集
fashion_mnist = keras.datasets.fashion_mnist

#导入训练集和测试集
(x_train_all, y_train_all), (x_test, y_test) = fashion_mnist.load_data()

# 拆分训练集和验证集
x_valid, x_train = x_train_all[0:5000], x_train_all[5000:]
y_valid, y_train = y_train_all[0:5000], y_train_all[5000:]

print(x_valid.shape, y_valid.shape) # (5000, 28, 28) (5000,)
print(x_train.shape, y_train.shape) # (55000, 28, 28) (55000,)
print(x_test.shape, y_test.shape) # (10000, 28, 28) (10000,)

Normalization

由于 fit_transform() 要求输入的参数是一个二维矩阵,因此 x_train 的结构就要从 [None, 28, 28] 转换成 [None, 784],接着再转回来,所以要 reshape() 两次:

1
2
3
4
5
6
7
8
9
10
from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()

x_train_scaled = scaler.fit_transform(
x_train.astype(np.float32).reshape(-1, 1)).reshape(-1, 28, 28)
x_valid_scaled = scaler.transform(
x_valid.astype(np.float32).reshape(-1, 1)).reshape(-1, 28, 28)
x_test_scaled = scaler.transform(
x_test.astype(np.float32).reshape(-1, 1)).reshape(-1, 28, 28)

Model

再次引起舒♂适。借助 tf.keras 的 API,搭建网络添加 layers 的时候,真的十分方便。方法也不止一种。例如,上面已经提到过的,通过给 Sequential() 传递一个 list 的形式:

1
2
3
4
5
6
7
8
9
10
model = keras.models.Sequential([
keras.layers.Flatten(input_shape=[28, 28]),
keras.layers.Dense(300, activation="relu"),
keras.layers.Dense(100, activation="relu"),
keras.layers.Dense(10, activation="softmax")
])

model.compile(loss="sparse_categorical_crossentropy",
optimizer = keras.optimizers.SGD(0.001),
metrics = ["accuracy"])

这里的 loss 函数用的是 sparse_categorical_crossentropy,因为数据集中的 y,都是一个个标量,例如 y_train[0] 的值是 4。而要计算 loss,就需要是向量,因此需要经过 One-Hot,转成向量才可以。

另外,这里还可以通过调用 add() 函数来添加 layers:

1
2
3
4
5
model = keras.models.Sequential()
model.add(keras.layers.Flatten(input_shape=[28, 28]))
model.add(keras.layers.Dense(300, activation="relu"))
model.add(keras.layers.Dense(100, activation="relu"))
model.add(keras.layers.Dense(10, activation="softmax"))

所以,这样一来,对于某些深度神经网络,就可以直接用 for 循环搭建,例如:

1
2
for _ in range(20):
model.add(keras.layers.Dense(100, activation='selu'))

后面要是有别的 layer,直接跟上就可以了。

Train

1
2
3
history = model.fit(x_train_scaled, y_train,
epochs=10,
validation_data=(x_valid_scaled, y_valid))

Plot

1
2
3
4
5
6
7
def plot_learning_curves(history):
pd.DataFrame(history.history).plot(figsize=(8, 5))
plt.grid(True)
plt.gca().set_ylim(0, 1)
plt.show()

plot_learning_curves(history)
plot-classification

Evaluate

结果大概看一下:

1
model.evaluate(x_test_scaled, y_test, verbose=0)
[0.4324875540494919, 0.8463]

关于 Callback 输出路径

模型训练的过程中,有遇到过一个 callback 输出路径的问题,应该算是 Tensorflow 的一个 bug。比如,当我想保存模型文件的时候,我会先建立一个路径:

1
2
3
4
5
6
7
8
9
10
logdir = "./callbacks"

if not os.path.exists(logdir):
os.mkdir(logdir)
output_model_file = os.path.join(logdir, "blahblah_model.h5")

callbacks = [
keras.callbacks.ModelCheckpoint(output_model_file,
save_best_only = True),
]

由于最近我使用的环境是 Windows,所以就遇到了这样的报错:

ProfilerNotRunningError: Cannot stop profiling. No profiler is running.

说白了,就是不能有 /。这个问题在 Github 的一个 Issue3 上也有讨论。

因此在 Mac 以及 Linux 环境下:

1
logdir = "./callbacks"

而在 Windows 环境下请使用:

1
logdir = "callbacks"

最后不得不说:

Eager Mode, YES!

参考链接

[1] Google, TensorFlow API Module: tf.keras Overview

[2] scikit-learn API: sklearn.preprocessing.StandardScaler

[3] Github Issue, TensorFlow keras callback using tensorboard, “ProfilerNotRunningError: Cannot stop profiling. No profiler is running.” #2279