不同进程间的内存是互相独立的,没办法直接互相操作对方内的数据,而共享内存则是靠操作系统提供的内存映射机制,让不同进程的一块地址空间映射到同一个虚拟内存区域上,使不同的进程可以操作到一块共用的内存块。共享内存是效率最高的进程间通讯机制,因为数据不需要在内核和程序之间复制。

共享内存用到的是系统提供的mmap函数,它可以将一个文件映射到虚拟内存的一个区域中,程序使用指针引用这个区域,对这个内存区域的操作会被回写到文件上,其函数原型如下:

void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);
  • 参数fd为即将映射到进程空间的文件描述字,一般由open()返回,同时,fd可以指定为-1,此时须指定flags参数中的MAP_ANON,表明进行的是匿名映射(不涉及具体的文件名,避免了文件的创建及打开,很显然只能用于具有亲缘关系的进程间通信)。
  • len是映射到调用进程地址空间的字节数,它从被映射文件开头offset个字节开始算起。
  • prot参数指定共享内存的访问权限。可取如下几个值的或:PROT_READ(可读),PROT_WRITE(可写),PROT_EXEC(可执行),PROT_NONE(不可访问)。
  • flags由以下几个常值指定:MAP_SHARED, MAP_PRIVATE, MAP_FIXED。其中,MAP_SHARED,MAP_PRIVATE必选其一,而MAP_FIXED则不推荐使用。 如果指定为MAP_SHARED,则对映射的内存所做的修改同样影响到文件。如果是MAP_PRIVATE,则对映射的内存所做的修改仅对该进程可见,对文件没有影响。
  • offset参数一般设为0,表示从文件头开始映射。
  • 参数addr指定文件应被映射到进程空间的起始地址,一般被指定一个空指针,此时选择起始地址的任务留给内核来完成。函数的返回值为最后文件映射到进程空间的地址,进程可直接操作起始地址为该值的有效地址。

顺带介绍一下shm_open和shm_unlink两个函数:

shm_open()函数
功能:    打开或创建一个共享内存区
头文件:    #include <sys/mman.h>
函数原形:    int shm_open(const char *name,int oflag,mode_t mode);
返回值:    成功返回0,出错返回-1
参数:
name    共享内存区的名字
oflag    标志位
mode    权限位
参数解释:oflag参数必须含有O_RDONLY和O_RDWR标志,还可以指定如下标志:O_CREAT,O_EXCL或O_TRUNC.mode参数指定权限位,
它指定O_CREAT标志的前提下使用。shm_open的返回值是一个整数描述字,它随后用作mmap的第五个参数。

shm_unlink()函数
功能:    删除一个共享内存区
头文件:    #include <sys/mman.h>
函数原形:    int shm_unlink(const char *name);
参数:     name    共享内存区的名字
返回值:    成功返回0,出错返回-1
shm_unlink函数删除一个共享内存区对象的名字,删除一个名字仅仅防止后续的open,mq_open或sem_open调用取得成功。

可以参考此文章的介绍来进一步了解mmap等函数:http://www.cnblogs.com/polestar/archive/2012/04/23/2466022.html

可以利用golang调用cgo的方法实现c中的mmap。实验分为读和写两个程序,这样我们可以观察到读进程可以读到写进程写入共享内存的信息。

shm_writer.go代码示例:

package main

/*
#cgo linux LDFLAGS: -lrt

#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>

#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)

int my_shm_new(char *name) {
    shm_unlink(name);
    return shm_open(name, O_RDWR|O_CREAT|O_EXCL, FILE_MODE);
}
*/
import "C"
import (
    "fmt"
    "unsafe"
)

const SHM_NAME = "my_shm"
const SHM_SIZE = 4 * 1000 * 1000 * 1000

type MyData struct {
    Col1 int
    Col2 int
    Col3 int
}

func main() {
    fd, err := C.my_shm_new(C.CString(SHM_NAME))
    if err != nil {
        fmt.Println(err)
        return
    }

    C.ftruncate(fd, SHM_SIZE)

    ptr, err := C.mmap(nil, SHM_SIZE, C.PROT_READ|C.PROT_WRITE, C.MAP_SHARED, fd, 0)
    if err != nil {
        fmt.Println(err)
        return
    }
    C.close(fd)

    data := (*MyData)(unsafe.Pointer(ptr))

    data.Col1 = 100
    data.Col2 = 876
    data.Col3 = 8021
}

shm_reader.go代码示例:

package main

/*
#cgo linux LDFLAGS: -lrt

#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>

#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)

int my_shm_open(char *name) {
    return shm_open(name, O_RDWR, FILE_MODE);
}
*/
import "C"
import (
    "fmt"
    "unsafe"
)

const SHM_NAME = "my_shm"
const SHM_SIZE = 4 * 1000 * 1000 * 1000

type MyData struct {
    Col1 int
    Col2 int
    Col3 int
}

func main() {
    fd, err := C.my_shm_open(C.CString(SHM_NAME))
    if err != nil {
        fmt.Println(err)
        return
    }

    ptr, err := C.mmap(nil, SHM_SIZE, C.PROT_READ|C.PROT_WRITE, C.MAP_SHARED, fd, 0)
    if err != nil {
        fmt.Println(err)
        return
    }
    C.close(fd)

    data := (*MyData)(unsafe.Pointer(ptr))

    fmt.Println(data)
}

上面的程序映射了一块4G的虚拟内存,用来证明mmap没有实际占用4G内存,而是用到了虚拟内存。shm_writer创建好共享内存以后,往内存区域写入了一个结构体,shm_reader则读出一个结构体。

上面代码中还用到一个cgo的技巧,像shm_open和mmap函数在错误时会返回errno,如果我们在go中使用多返回值语法,cgo会自己把错误码转换成错误信息,很方便的功能。

http://colobu.com/2015/10/12/create-minimal-golang-docker-images/

Docker是PaaS供应商dotCloud开源的一个基于LXC 的高级容器引擎,源代码托管在 GitHub 上, 基于Go语言开发并遵从Apache 2.0协议开源。正如DockerPool在免费Docker电子书Docker —— 从入门到实践中这样提到的:

作为一种新兴的虚拟化方式,Docker 跟传统的虚拟化方式相比具有众多的优势。

首先,Docker 容器的启动可以在秒级实现,这相比传统的虚拟机方式要快得多。 其次,Docker 对系统资源的利用率很高,一台主机上可以同时运行数千个 Docker 容器。

容器除了运行其中应用外,基本不消耗额外的系统资源,使得应用的性能很高,同时系统的开销尽量小。传统虚拟机方式运行 10 个不同的应用就要起 10 个虚拟机,而Docker 只需要启动 10 个隔离的应用即可。

Docker让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。容器是完全使用沙箱机制,相互之间不会有任何接口(类似 iPhone 的 app)。几乎没有性能开销,可以很容易地在机器和数据中心中运行。最重要的是,他们不依赖于任何语言、框架包括系统。

本文不会介绍Docker原理和操作,而是介绍如何使用Docker创建一个Golang应用程序的镜像,这样我们就可以在其它机器上运行这个镜像。
本文参考了很多的文章,这些文章列在了本文的底部。

编写一个Golang服务器

这里我在研究endless库的时候写了一个测试程序,就用它来测试一下docker镜像的创建。
endless可以允许我们在重启网络服务器的时候零时间宕机, 英语是graceful restart,我称之为无缝重启。
服务器监听4242端口,顺便使用raymond模版引擎替换golang自带的模版引擎,采用bone这个高性能的mux库。
代码如下:

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
68
package main
import (
“flag"
“log"
“net/http"
“os"
“syscall"
“github.com/aymerick/raymond"
“github.com/fvbock/endless"
“github.com/go-zoo/bone"
)
var (
//homeTpl, _ = raymond.ParseFile(“home.hbs")
homeTpl = raymond.MustParse(`<html>
<head>
<title>test</title>
</head>
</body>
<div class="entry">
<h1></h1>
<div class="body">
</div>
</div>
</body>
</html>
`)
)
func homeHandler(rw http.ResponseWriter, req *http.Request) {
ctx := map[string]string{“greet": “hello", “name": “world"}
result := homeTpl.MustExec(ctx)
rw.Write([]byte(result))
}
func varHandler(rw http.ResponseWriter, req *http.Request) {
varr := bone.GetValue(req, “var")
test := bone.GetValue(req, “test")
rw.Write([]byte(varr + " “ + test))
}
func Handler404(rw http.ResponseWriter, req *http.Request) {
rw.Write([]byte(“These are not resources you’re looking for …"))
}
func restartHandler(rw http.ResponseWriter, req *http.Request) {
syscall.Kill(syscall.Getppid(), syscall.SIGHUP)
rw.Write([]byte(“restarted"))
}
func main() {
flag.Parse()
mux := bone.New()
// Custom 404
mux.NotFoundFunc(Handler404)
// Handle with any http method, Handle takes http.Handler as argument.
mux.Handle(“/index", http.HandlerFunc(homeHandler))
mux.Handle(“/index/:var/info/:test", http.HandlerFunc(varHandler))
// Get, Post etc… takes http.HandlerFunc as argument.
mux.Post(“/home", http.HandlerFunc(homeHandler))
mux.Get(“/home/:var", http.HandlerFunc(varHandler))
mux.GetFunc(“/test/*", func(rw http.ResponseWriter, req *http.Request) {
rw.Write([]byte(req.RequestURI))
})
mux.Get(“/restart", http.HandlerFunc(restartHandler))
err := endless.ListenAndServe(“:4242″, mux)
if err != nil {
log.Fatalln(err)
}
log.Println(“Server on 4242 stopped")
os.Exit(0)
}

Golang镜像

Docker官方提供了Golang各版本的镜像: Official Repository – golang.
它包含了Golang的编译和运行时环境。最简单的使用方法就是在你的Dockerfile文件中加入

1
FROM golang:1.3-onbuild

这个镜像包含了多个ONBUILD触发器。你可以编译和运行你的镜像:

1
2
$ docker build -t my-golang-app .
$ docker run -it –rm –name my-running-app my-golang-app

为编译好的Golang应用创建小的镜像

上面的Golang容器相当的大,因为它包含了Golang的编译和运行环境。
官方网站上列出了镜像的大小:

golang:1.5.1-onbuild

$ docker pull library/golang@sha256:f938465579d1cde302a447fef237a5a45d7e96609b97c83b9144446615ad9e72

Total Virtual Size: 709.5 MB (709470237 bytes)
Total v2 Content-Length: 247.0 MB (246986021 bytes)

实际上我们并不需要那么多的软件,因为我们的Golang应用程序是预先编译好的,而不是在Golang容器中现场编译运行,因此我们不需要Golang的编译环境等。如果你查看golang:1.5的Dockerfile,会发现它基于buildpack-deps:jessie-scm,会安装GCC及一堆的build工具,下载Go的发布文件并安装。基本上这些对于我们来说并不需要。我们需要的是:

一个可以运行我们编译好的Golang应用的镜像。

我们可以从scratch镜像创建。
scratch镜像是一个空的镜像文件,特别适合创建超级小的镜像。
Dockerfile文件如下:

1
2
3
FROM scratch
ADD main /
CMD [“/main"]

运行
输出如下

1
2
3
4
5
6
7
8
9
10
11
12
13
# docker build -t example-scratch .
Sending build context to Docker daemon 8.054 MB
Step 0 : FROM scratch
–>
Step 1 : ADD main /
–> 4ad02fa47a7d
Removing intermediate container d64080c4b42f
Step 2 : CMD /main
–> Running in 5d9a08c3a20e
–> 5c29c8249678
Removing intermediate container 5d9a08c3a20e
Successfully built 5c29c8249678

这样镜像就创建成功了,查看一下:

1
2
3
[root@localhost work]# docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
example-scratch latest 5c29c8249678 3 minutes ago 8.052 MB

只有8M左右,非常的小。

但是运行这个镜像,容器无法创建:

1
2
3
# docker run -it -p 4242:4242 example-scratch
no such file or directory
Error response from daemon: Cannot start container 79bb9fb62788b4a8c1487695a3219ddf3aa85bde2bc44473838f6f4d1583a204: [8] System error: no such file or directory

原因是我们的main文件生成的时候依赖的一些库如libc还是动态链接的,但是scratch 镜像完全是空的,什么东西也不包含,所以生成main时候要按照下面的方式生成,使生成的main静态链接所有的库:

1
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .

然后重新生成镜像并运行:

1
2
# docker build -t example-scratch .
# docker run -it -p 4242:4242 example-scratch

容器运行成功,在浏览器中访问http://宿主IP:4242/index成功返回结果

发布

可以方便的将刚才的镜像发布到docker.io上。
首先将刚才的镜像打tag:

1
2
3
4
5
6
7
8
# docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
example-scratch latest 2ea4bbfd67dc 10 minutes ago 8.01 MB
# docker tag 2ea4bbfd67dc smallnest/example-scratch
# docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
smallnest/example-scratch latest 2ea4bbfd67dc 10 minutes ago 8.01 MB
example-scratch latest 2ea4bbfd67dc 10 minutes ago 8.01 MB

运行docker login登录,然后运行下面的命令push到docker.io上。

1
docker push smallnest/example-scratch

访问 https://hub.docker.com/r/smallnest/example-scratch/ 可以看到刚刚push的这个镜像,这样我们就可以pull到其它机器上运行了。

参考文档

  1. https://blog.golang.org/docker
  2. https://hub.docker.com/_/golang/
  3. https://blog.codeship.com/building-minimal-docker-containers-for-go-applications/
  4. https://medium.com/@kelseyhightower/optimizing-docker-images-for-static-binaries-b5696e26eb07
  5. http://www.iron.io/blog/2015/07/an-easier-way-to-create-tiny-golang-docker-images.html
  6. https://labs.ctl.io/small-docker-images-for-go-apps/
  7. http://dockerpool.com/static/books/docker_practice/introduction/why.html
  8. https://docs.docker.com/installation/centos/
  9. http://segmentfault.com/a/1190000002766882

https://segmentfault.com/a/1190000000628247

注:本文由 Adriaan de Jonge 编写,本文的原文地址为 Create The Smallest Possible Docker Container

当我们在使用 Docker 的时候,你会很快注意到你正在下载很多 MB 作为你的预先配置的容器。一个简单的 Ubuntu 容器很容易超过 200 MB,并且随着在上面安装软件,尺寸在逐渐增大。在某些情况下,你不需要任何事情都使用 Ubuntu 。例如,如果你只是简单的想运行一个 web 服务,使用 GO 编写的,没有必要围绕它使用任何工具。

我一直在寻找尽可能小的容器入手,并且发现了一个:

docker pull scratch

scratch 镜像是完美的,真正的完美!它简洁,小巧以及快速。它不包含任何 bug,安全泄漏,慢的代码或是技术债务。这是因为它是一个空的镜像。除了一点由 Docker 加入的元数据。事实上,你可以使用如下命令按照 Docker 文档描述的那样创建一个自己的 scratch 镜像。

tar cv --files-from /dev/null | docker import - scratch

所以这可能就是最小的 Docker 镜像。

或者我们可以说说关于这个的更多东西?比如,你怎样使用 scratch 镜像。这给自己带来了一些挑战。

为 scratch 镜像创建内容

我们可以在一个空镜像中运行什么?一个没有依赖的可执行程序。你是否有没有依赖的可执行程序?

我过去常常使用 Python,Java 和 Javascript 编写代码。每一个这样的语言/平台都需要一个运行时的安装。最近,我开始涉及 Go(或是 golang 如果你喜欢)平台。看起来 Go 是静态连接的。因此我尝试编译一个简单的 web 服务输出 Hello World 并且运行在 scratch 容器中。下面是这个 Hello World web 服务的代码:

package main

import (
    "fmt"
    "net/http"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Hello World from Go in minimal Docker container")
}

func main() {
    http.HandleFunc("/", helloHandler)

    fmt.Println("Started, serving at 8080")
    err := http.ListenAndServe(":8080", nil)
    if err != nil {
        panic("ListenAndServe: " + err.Error())
    }
}

明显地,我不能在 scratch 容器中编译我的 web 服务,因为容器中没有 Go 编译器。正如我在 Mac 上工作,我也无法编译 Linux 的二进制文件一样(实际上,是可以在不同的平台上交叉编译 Go 的源码的,但这会在另外一篇博客中介绍)。

因此,我首先需要一个有 Go 编译器的 Docker 容器。让我们开始:

docker run -ti google/golang /bin/bash

在这个容器里面,我可以构建一个 Web 服务,通过我已经提交到一个 GitHub 仓库的代码。

go get github.com/adriaandejonge/helloworld

go get 命令是 go build 命令的变种,运行获取和构建远程的依赖。你可以运行可执行的结果:

$GOPATH/bin/helloworld

它工作了,但是这不是我们想要的。我们需要 hello world 容器运行在 scratch 容器里面。因此,实际上,我们需要一个 Dockerfile :

FROM scratch
ADD bin/helloworld /helloworld
CMD ["/helloworld"]

然后启动它,不幸的是,我们开始 google/golang 容器的这个方法, 没有办法构建这个 Dockerfile 。因此,首先,我们需要一种方法从这个容器内部访问到 Docker。

从 Docker 内部调用 Docker

当你使用 Dokcer 时,你迟早会遇到需要从 Docker 内部访问 Docker。可以有多种方法实现它。你可以使用递归和在 Docker 中运行 Docker。尽管如此,这样看起来会很复杂并且导致容器很大。你还可以使用一些额外的命令选项在实例外访问 Docker 服务器:

docker run -v /var/run/docker.sock:/var/run/docker.sock -v $(which docker):$(which docker) -ti google/golang /bin/bash

在你继续前,你重新运行 Go 编译器,由于在重启动过程中 Docker 忘记了我们以前编译过。

go get github.com/adriaandejonge/helloworld

当我们启动这个容器, -v 参数在 Docker 容器中创建一个卷并且允许你从 Docker 的机器提供一个文件作为输入。/var/run/docker.sock 是 UNIX socket,通过这个允许你访问 Docker 服务。 (which docker) 部分是一个非常聪明的方法,它提供了一个在 容器中的 Docker 可执行文件的路径,而不是硬编码。尽管如此,当你在 Mac 上通过 boot2docker 使用这个命令的时候需要小心。如果 Docker 的可执行文件与 boot2docker 虚拟机的在不同的位置,将导致不匹配。因此,你或许想使用/usr/local/bin/docker 硬编码的方式替换 $(which docker),如果你运行在不同的系统,/var/run/docker.sock 有在不同位置的机会,你需要做相应的调整。

现在你可以在 google/golang 容器的 $GOPATH 目录使用 Dockerfile ,在这个示例中指向 /gopath。实际上,我已经在 github 上检查过了这个 Dockerfile,因此,你可以从 Go build 目录复制它到所需的位置,像这样:

cp $GOPATH/src/github.com/adriaandejonge/helloworld/Dockerfile $GOPATH

你需要复制这个作为二进制的编译文件,现在位于 $GOPATH/bin,并且它不可能从父目录包含文件当构建一个 Dockerfile 的时候。因此复制后,下一步是:

docker build -t adejonge/helloworld $GOPATH

所有的都完成以后, Docker 给出如下响应:

Successfully built 6ff3fd5a381d

允许你运行这个容器:

docker run -ti --name hellobroken adejonge/helloworld

但是不幸的是, Docker 这次响应如下:

2014/07/02 17:06:48 no such file or directory

那么到底是怎么回事?我们在 scratch 容器中有可执行的静态链接。难道我们犯了一个错误?

事实证明,Go 不是静态链接库。或者至少不是所有的库。在 Linux 下,我们可以使用 ldd 命令来看到动态链接库:

ldd $GOPATH/bin/helloworld 

得到如下响应:

linux-vdso.so.1 => (0x00007fff039fe000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f61df30f000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f61def84000)
/lib64/ld-linux-x86-64.so.2 (0x00007f61df530000)

因此,在我们运行我们的 web 服务之前,我需要告诉 go 编译器实际的静态链接。

创建在 Go 中的可执行静态链接

为了创建可执行的静态链接,我们需要告诉 Go 使用 cgo 编译器而不是 go 编译器。命令如下:

CGO_ENABLED=0 go get -a -ldflags '-s' github.com/adriaandejonge/helloworld

CGO_ENABLED 环境变量告诉 Go 使用 cgo 编译器而不是 go 编译器。-a 参数告诉 GO 重薪构建所有的依赖。否则的话你将以动态链接依赖结束。最后的 -ldflags '-s' 参数是一个非常好的扩展。它大概降低了可执行文件 50% 的文件大小。你也可以不通过 cgo 使用这个。尺寸缩小是去除了调试信息的结果。

为了确定,运行 ldd 命令:

ldd $GOPATH/bin/helloworld 

返回是:

not a dynamic executable

你也可以重新运行步骤,围绕着从 scratch 创建 Docker 容器的可执行文件。

docker build -t adejonge/helloworld $GOPATH

如果一切顺利,Docker 将响应如下:

Successfully built 6ff3fd5a381d

允许你运行这个容器:

docker run -ti --name helloworld adejonge/helloworld

响应如下:

Started, serving at 8080

到目前为止,有许多手动的步骤和很多错误的地方。让我们退出 google/golang 容器并且从周边服务器继续:

<Press Ctrl-C>
exit

你可以检查 Docker 容器和镜像存在不存在:

docker ps -a
docker images -a

你可以使用如下命令清理:

docker rm -f helloworld
docker rmi -f adejonge/helloworld

创建一个 Docker 容器来创建一个 Docker 容器

目前为止,我们花了那么多步骤,我们还可以记录在 Dockerfile 中并且 Docker 会为我们做这些工作:

FROM google/golang
RUN CGO_ENABLED=0 go get -a -ldflags '-s' github.com/adriaandejonge/helloworld
RUN cp /gopath/src/github.com/adriaandejonge/helloworld/Dockerfile /gopath
CMD docker build -t adejonge/helloworld gopath

我在 一个单独的称为 adriaandejonge/hellobuild 的 GitHub 仓库检查了 Dockerfile。它可以使用下面的命令构建:

docker build -t adejonge/hellobuild github.com/adriaandejonge/hellobuild

提供 -t 参数命名 adejonge/hellobuild 镜像并且它的最新的隐式的标签。这些名字让你以后更容易去除镜像。下一步,你可以使用就像我们在这篇文章前面看到的那样提供一个参数从这个镜像中创建一个容器:

docker run -v /var/run/docker.sock:/var/run/docker.sock -v $(which docker):$(which docker) -ti --name hellobuild adejonge/hellobuild

提供 --name hellobuild 参数使得在运行后更容易移除容器。事实上,你可以这样做,因为运行这个命令后,你已经创建了一个 adejonge/helloworld 镜像:

docker rm -f hellobuild
docker rmi -f adejonge/hellobuild

现在你可以创建一个基于 adejonge/helloworld 镜像的名为 helloworld 的新容器,就像你以前做的那样:

docker run -ti --name helloworld adejonge/helloworld

因为所有的这些步骤都是从相同的命令中运行,不需要在 Docker 中打开一个 bash shell 。你可以把这些步骤添加进一个 bash 脚本,自动运行它,为了使你方便,我已经把这些脚本加入了 hellobuild GitHub 仓库

另外,如果你想尝试一个尽可能小的容器,但是又不想遵循博客中的步骤,你可以使用我检入进 Docker Hub repository 的预先构建好的镜像。

docker pull adejonge/helloworld

使用 docker images -a ,你可以看到大小是 3.6MB。当然,如果你成功创建一个比我使用 Go 编写的 web 服务还小的可执行文件,你可以使得它更小。使用 C 语言或者是汇编,你可以这样做到。尽管如此,你不可能使得它比 scratch 镜像还小

扩展阅读

https://samkuo.me/post/2015/09/tiny-docker-image-with-ubuntu-bash/

前言

圍繞著 Docker 容器 (container) 技術所引發的風潮 , 一個越來越成熟的生態系正在形成 。 相信許多人都在關注它的發展 , 也可能有諸多的疑問 。 對於真正想把 Docker 應用在生產環境的人而言 , 其中一個疑問可能是 :Docker 的映像 (image) 非得動則幾十 , 上百甚至上千 MB 嗎 ? 的確 , 頻寬跟儲存空間不再像過去那樣高不可攀 , 但太大的檔案或多或少還是會影響部署時間 。 本文山姆鍋示範其中一種製作小型 Docker 映像的方法 。

對於想直接知道結果的讀者 , 可以參考本文所使用的 原始碼

為什麼需要在意映像檔大小

Docker 所提供的容器技術 , 另一種稱呼為 : 軟體容器 (Software Container), 是一種 作業系統層虛擬化 技術 。 在不同的作業系統有不同的方法跟名稱 , 例如 :Solaris 的 zones, FreeBSD 的 jail, Linux 的 LXC 等都是同樣概念的東西 。 表面上 , 容器跟虛擬機 (virtual machine) 有許多相似之處 , 同樣可以區隔不同的應用與服務 , 避免干擾彼此的執行 。 所以 , 您會看到許多人試圖在一個容器內同時執行多個服務或應用 , 需要用到排程 , 所以 cron 服務要啟動 ; 需要資料庫 , 所以資料庫服務器也一起執行 。 嚴格來講 , 沒有人可以說這樣做不對 。 但是 , 容器不是虛擬機 ! 這樣的做法並沒有發揮容器技術的潛力 。 山姆鍋認同的觀點是 : 一個容器應該只包含一個應用或服務剛好所需 (just enough) 的檔案 。 例如 : 一個 web 應用 , 除了應用本身執行檔外 , 可能需要第三方的套件 , 也許還需要一些系統套件才能運作 , 除此之外都是多餘 。 簡單說 , 容器包含的東西要盡可能的精簡 , 自然表示映像檔要盡量的小 。

為什麼不需要在意映像檔大小

什麼 ? 不是才說要在意映像檔大小 , 轉眼又說不用在意 ? 是的沒錯 , 山姆鍋不是頭腦不清楚 。 對於多數的使用情況 , 您不需要太在意一個映像檔大小 。 透過 Docker 這樣的產品 , 由於映像檔採用層層堆疊的方式組成 , 組成映像檔底層的資料通常會被重複使用 。 如此 , 在部署下載映像檔時 , 並不會因為小小修改就需要重新下載整個映像檔 。 但這個前提是 : 那個映像檔之前已經被下載並快取在該主機中 !

小型映像檔的需求

山姆鍋先說明對這個小型映像檔的需求 :

  1. 要跟 Ubuntu 14.04 二進位相容

    對於網路服務或應用 , 山姆鍋選擇的作業系統就是 Ubuntu, 所以 , 跟 Ubuntu 相容是必要條件 。

  2. 要支援 Bash

    雖然有 Busybox 提供大多數工具程式 , 但還是希望使用的 Shell 是 Bash。

  3. 要有一個啟動器

    需要一個類似 Linux init 功能 , 但是精簡的服務 。 山姆鍋這裏使用的是 runit , 使用 runit 主要目的在利用它所提供的 log 以及改變執行身份的功能 。

  4. 建構映像檔需要的時間必需短

    使用 buildroot 這樣的工具雖然可以建構出小型映像檔 , 但編譯需要的時間實在太久了 。

製作小型 Docker 映像檔的方法

不管您對於該不該在意映像檔大小的結論是什麼 , 看到這裡 , 山姆鍋假設您還是想知道如何製作小型映像檔的方法 。 山姆鍋採用的方法分成兩階段 , 第一階段產生一個根檔案 (root filesystem) 系統 ; 第二階段將這個根檔案系統包裝成 Docker 映像檔 。

先說說比較容易理解的第二階段 , 底下是第二階段所使用的 Dockerfile:

FROM scratch                #1

ADD overlayfs.tar /         #2
ADD run.sh /run.sh          #3
RUN chmod a+x /run.sh

ENV HOME /
WORKDIR /
CMD ["/run.sh"]
  1. 從標記 1, 這個映像以 scratch 這個映像為基礎 。

  2. 標記 2 的將代表根檔案系統內容的 ‘overlayfs.tar’ 展開並複製到映像檔的根目錄 。

    這裏稍微補充一下 :ADD 指令在遇到 .tar 這種壓縮檔會自動解開 。

  3. run.sh 這個腳本負責啟動 runit 服務 。

run.sh 的內容 :

#!/bin/sh

export > /etc/envvars       #1
/usr/sbin/runsvdir-start

runit 有個問題是不會傳遞環境變數給它啟動的服務或應用 , 標號 1 的敘述就是將當下環境變數存到 /etc/envvars這個檔案 , 後續可以 使用 source /etc/envvars 重新讀回 。

第一階段唯一目的就是製作第二階段所需的根檔案系統 tar 檔 。 第二階段本身也是透過 Docker 來執行 , Dockerfile 的大致步驟說明如下 :

  1. 安裝必要以及基本系統套件
apt-get update -q && apt-get install -qy busybox-static runit bash pwgen
  1. 建立基本根檔案系統結構 ,
RUN mkdir -p bin etc etc/service dev dev/pts lib proc sys tmp var var/lib var/run var/log \
    usr usr/sbin usr/bin usr/lib root
  1. 建立 Docker 映像檔所需的系統檔案 , 如 /etc/resolv.conf, 並建立 root 以及 ava 帳戶 。

    ava 這個身份用來執行應用 , 根據安全建議 , 您應該避免使用 root 來執行 。

chmod a+w tmp &&\
    touch etc/resolv.conf &&\
    cp /etc/nsswitch.conf etc/nsswitch.conf &&\
    echo root:x:0:0:root:/root:/bin/sh > etc/passwd &&\
    echo root:x:0: > etc/group &&\
    echo ava:x:1000:1000::/home/ava:/bin/bash >> etc/passwd &&\
    echo ava:x:1000: >> etc/group &&\
    echo ava:x:1000:1000::/home/ava:/bin/bash >> /etc/passwd &&\
    echo ava:x:1000: >> /etc/group
  1. 複製 Busybox, Bash, Runit, pwgen 及必要程式庫到適當路徑 。
  2. 使用上列步驟建立的根檔案結構 , 建立第二階段所需要的 overlayfs.tar 檔案 , 並放置在所產生的映像檔的根目錄中 。

整個建構流程便是利用第一階段的 Dockerfile 建立一個映像檔 , 其中 /overlayfs.tar 檔案是第二階段需要的根檔案系統 。 最後 , 使用第二階段的 Dockerfile 建立真正要發行的映像檔 。 有提供一個 Makefile:

GROUP=eavatar
NAME=basebox
VERSION=0.1.5


all: build tag

build: Dockerfile overlayfs.tar
    docker build  -t $(GROUP)/$(NAME):$(VERSION) .

overlayfs.tar:
    cd overlayfs && docker build  -t $(NAME)-builder .
    docker run  $(NAME)-builder cat /overlayfs.tar > overlayfs.tar
    #       docker rmi $(NAME)-builder

tag:
    @if ! docker images $(GROUP)/$(NAME) | awk '{ print $$2 }' | grep -q -F $(VERSION); then echo "$(NAME) version $(VERSION) is not yet built. Please run 'make build'"; false; fi
    docker tag $(GROUP)/$(NAME):$(VERSION) $(GROUP)/$(NAME):latest

clean:
    rm -f overlayfs.tar

這個方法的優點在於適用所有可以在 Ubuntu 上執行的應用 , 缺點是必須找出應用相依的系統程式庫 (libraries)。

結語

為什麼不在容器內支援套件管理功能 , 如 apt, opkg? 因為直接修改容器內的檔案會造成版本控管困難 , 導致難以理解系統目前狀態 。 Docker 容器技術是達成持續交付 (continuous delivery) 的重要工具 , 而要達成持續交付的目的 , 使用相同的二進位套件 (binary packages) 是必要手段 。 也就是說 : 容器映像檔本身直接被當作是應用或服務發行套件 。 山姆鍋認為在使用任何一種容器技術時 , 釐清 「 容器不是虛擬機 」 這個概念 , 對於整體架構設計至關重要 。

 (注:本文译自一篇博客,作者行文较随意,我尽量按原意翻译,但作者所介绍的知识还是非常好的,包括例子的选择、理论的介绍都很到位,由浅入深,源文地址

  近些年来,人工智能领域又活跃起来,除了传统了学术圈外,Google、Microsoft、facebook等工业界优秀企业也纷纷成立相关研究团队,并取得了很多令人瞩目的成果。这要归功于社交网络用户产生的大量数据,这些数据大都是原始数据,需要被进一步分析处理;还要归功于廉价而又强大的计算资源的出现,比如GPGPU的快速发展。

除去这些因素,AI尤其是机器学习领域出现的一股新潮流很大程度上推动了这次复兴——深度学习。本文中我将介绍深度学习背后的关键概念及算法,从最简单的元素开始并以此为基础进行下一步构建。

(本文作者也是Java deep learning library的作者,可以从此处获得,本文中的例子就是使用这个库实现的。如果你喜欢,可以在Github上给个星~。用法介绍也可以从此处获得)

机器学习基础

如果你不太熟悉相关知识,通常的机器学习过程如下:

1、机器学习算法需要输入少量标记好的样本,比如10张小狗的照片,其中1张标记为1(意为狗)其它的标记为0(意为不是狗)——本文主要使用监督式、二叉分类。

2、这些算法“学习”怎么样正确将狗的图片分类,然后再输入一个新的图片时,可以期望算法输出正确的图片标记(如输入一张小狗图片,输出1;否则输出0)。

这通常是难以置信的:你的数据可能是模糊的,标记也可能出错;或者你的数据是手写字母的图片,用其实际表示的字母来标记它。

感知机

感知机是最早的监督式训练算法,是神经网络构建的基础。

假如平面中存在 个点,并被分别标记为“0”和“1”。此时加入一个新的点,如果我们想知道这个点的标记是什么(和之前提到的小狗图片的辨别同理),我们要怎么做呢?

一种很简单的方法是查找离这个点最近的点是什么,然后返回和这个点一样的标记。而一种稍微“智能”的办法则是去找出平面上的一条线来将不同标记的数据点分开,并用这条线作为“分类器”来区分新数据点的标记。

线性分类器

在本例中,每一个输入数据都可以表示为一个向量 x = (x_1, x_2) ,而我们的函数则是要实现“如果线以下,输出0;线以上,输出1”。

用数学方法表示,定义一个表示权重的向量 w 和一个垂直偏移量 b。然后,我们将输入、权重和偏移结合可以得到如下传递函数:

映射变换函数

  这个传递函数的结果将被输入到一个激活函数中以产生标记。在上面的例子中,我们的激活函数是一个门限截止函数(即大于某个阈值后输出1):

                     

训练

感知机的训练包括多训练样本的输入及计算每个样本的输出。在每一次计算以后,权重 w 都要调整以最小化输出误差,这个误差由输入样本的标记值与实际计算得出值的差得出。还有其它的误差计算方法,如均方差等,但基本的原则是一样的。

缺陷

这种简单的感知机有一个明显缺陷:只能学习线性可分函数。这个缺陷重要吗?比如 XOR,这么简单的函数,都不能被线性分类器分类(如下图所示,分隔两类点失败):

为了解决这个问题,我们要使用一种多层感知机,也就是——前馈神经网络:事实上,我们将要组合一群这样的感知机来创建出一个更强大的学习机器。

前馈神经网络

神经网络实际上就是将大量之前讲到的感知机进行组合,用不同的方法进行连接并作用在不同的激活函数上。

前馈神经网络示意图

我们简单介绍下前向神经网络,其具有以下属性:

  • 一个输入层,一个输出层,一个或多个隐含层。上图所示的神经网络中有一个三神经元的输入层、一个四神经元的隐含层、一个二神经元的输出层。
  • 每一个神经元都是一个上文提到的感知机。
  • 输入层的神经元作为隐含层的输入,同时隐含层的神经元也是输出层神经元的输入。
  • 每条建立在神经元之间的连接都有一个权重 w (与感知机中提到的权重类似)。
  • 在 t 层的每个神经元通常与前一层( t – 1层)中的每个神经元都有连接(但你可以通过将这条连接的权重设为0来断开这条连接)。
  • 为了处理输入数据,将输入向量赋到输入层中。在上例中,这个网络可以计算一个3维输入向量(由于只有3个输入层神经元)。假如输入向量是 [7, 1, 2],你将第一个输入神经元输入7,中间的输入1,第三个输入2。这些值将被传播到隐含层,通过加权传递函数传给每一个隐含层神经元(这就是前向传播),隐含层神经元再计算输出(激活函数)。
  • 输出层和隐含层一样进行计算,输出层的计算结果就是整个神经网络的输出。

超线性

如果每一个感知机都只能使用一个线性激活函数会怎么样?整个网络的最终输出也仍然是将输入数据通过一些线性函数计算过一遍,只是用一些在网络中收集的不同权值调整了一下。换名话说,再多线性函数的组合还是线性函数。如果我们限定只能使用线性激活函数的话,前馈神经网络其实比一个感知机强大不到哪里去,无论网络有多少层。

正是这个原因,大多数神经网络都是使用的非线性激活函数,如对数函数、双曲正切函数、阶跃函数、整流函数等。不用这些非线性函数的神经网络只能学习输入数据的线性组合。

训练

大多数常见的应用在多层感知机的监督式训练的算法都是反向传播算法。基本的流程如下:

1、将训练样本通过神经网络进行前向传播计算。

2、计算输出误差,常用均方差:

其中 t 是目标值, y 是实际的神经网络计算输出。其它的误差计算方法也可以,但MSE(均方差)通常是一种较好的选择。

3、网络误差通过随机梯度下降的方法来最小化。

梯度下降很常用,但在神经网络中,输入参数是一个训练误差的曲线。每个权重的最佳值应该是误差曲线中的全局最小值(上图中的 global minimum)。在训练过程中,权重以非常小的步幅改变(在每个样本或每小组样本训练完成后)以找到全局最小值,但这可不容易,训练通常会结束在局部最小值上(上图中的local minima)。如例子中的,如果当前权重值为0.6,那么要向0.4方向移动。

这个图表示的是最简单的情况,误差只依赖于单个参数。但是,网络误差依赖于每一个网络权重,误差函数非常、非常复杂。

好消息是反向传播算法提供了一种通过利用输出误差来修正两个神经元之间权重的方法。关系本身十分复杂,但对于一个给定结点的权重修正按如下方法(简单):

其中 E 是输出误差, w_i 是输入 i 的权重。

实质上这么做的目的是利用权重 i 来修正梯度的方向。关键的地方在于误差的导数的使用,这可不一定好计算:你怎么样能给一个大型网络中随机一个结点中的随机一个权重求导数呢?

答案是:通过反向传播。误差的首次计算很简单(只要对预期值和实际值做差即可),然后通过一种巧妙的方法反向传回网络,让我们有效的在训练过程中修正权重并(期望)达到一个最小值。

隐含层

隐含层十分有趣。根据普适逼近原理,一个具有有限数目神经元的隐含层可以被训练成可逼近任意随机函数。换句话说,一层隐含层就强大到可以学习任何函数了。这说明我们在多隐含层(如深度网络)的实践中可以得到更好的结果。

隐含层存储了训练数据的内在抽象表示,和人类大脑(简化的类比)保存有对真实世界的抽象一样。接下来,我们将用各种方法来搞一下这个隐含层。

一个网络的例子

可以看一下这个通过 testMLPSigmoidBP 方法用Java实现的简单(4-2-3)前馈神经网络,它将 IRIS 数据集进行了分类。这个数据集中包含了三类鸢尾属植物,特征包括花萼长度,花瓣长度等等。每一类提供50个样本给这个神经网络训练。特征被赋给输入神经元,每一个输出神经元代表一类数据集(“1/0/0” 表示这个植物是Setosa,“0/1/0”表示 Versicolour,而“0/0/1”表示 Virginica)。分类的错误率是2/150(即每分类150个,错2个)。

大规模网络中的难题

神经网络中可以有多个隐含层:这样,在更高的隐含层里可以对其之前的隐含层构建新的抽象。而且像之前也提到的,这样可以更好的学习大规模网络。增加隐含层的层数通常会导致两个问题:

1、梯度消失:随着我们添加越来越多的隐含层,反向传播传递给较低层的信息会越来越少。实际上,由于信息向前反馈,不同层次间的梯度开始消失,对网络中权重的影响也会变小。

2、过度拟合:也许这是机器学习的核心难题。简要来说,过度拟合指的是对训练数据有着过于好的识别效果,这时导至模型非常复杂。这样的结果会导致对训练数据有非常好的识别较果,而对真实样本的识别效果非常差。

下面我们来看看一些深度学习的算法是如何面对这些难题的。

自编码器

  大多数的机器学习入门课程都会让你放弃前馈神经网络。但是实际上这里面大有可为——请接着看。

自编码器就是一个典型的前馈神经网络,它的目标就是学习一种对数据集的压缩且分布式的表示方法(编码思想)。

从概念上讲,神经网络的目的是要训练去“重新建立”输入数据,好像输入和目标输出数据是一样的。换句话说:你正在让神经网络的输出与输入是同一样东西,只是经过了压缩。这还是不好理解,先来看一个例子。

压缩输入数据:灰度图像

这里有一个由28×28像素的灰度图像组成的训练集,且每一个像素的值都作为一个输入层神经元的输入(这时输入层就会有784个神经元)。输出层神经元要有相同的数目(784),且每一个输出神经元的输出值和输入图像的对应像素灰度值相同。

在这样的算法架构背后,神经网络学习到的实际上并不是一个训练数据到标记的“映射”,而是去学习数据本身的内在结构和特征(也正是因为这,隐含层也被称作特征探测器(feature detector))。通常隐含层中的神经元数目要比输入/输入层的少,这是为了使神经网络只去学习最重要的特征并实现特征的降维。

我们想在中间层用很少的结点去在概念层上学习数据、产生一个紧致的表示方法。

流行感冒

为了更好的描述自编码器,再看一个应用。

这次我们使用一个简单的数据集,其中包括一些感冒的症状。如果感兴趣,这个例子的源码发布在这里

数据结构如下:

    • 输入数据一共六个二进制位
    • 前三位是病的证状。例如,1 0 0 0 0 0 代表病人发烧;0 1 0 0 0 0 代表咳嗽;1 1 0 0 0 0 代表即咳嗽又发烧等等。
    • 后三位表示抵抗能力,如果一个病人有这个,代表他/她不太可能患此病。例如,0 0 0 1 0 0 代表病人接种过流感疫苗。一个可能的组合是:0 1 0 1 0 0 ,这代表着一个接种过流感疫苗的咳嗽病人,等等。

当一个病人同时拥用前三位中的两位时,我们认为他生病了;如果至少拥用后三位中的两位,那么他是健康的,如:

    • 111000, 101000, 110000, 011000, 011100 = 生病
    • 000111, 001110, 000101, 000011, 000110 = 健康

我们来训练一个自编码器(使用反向传播),六个输入、六个输出神经元,而只有两个隐含神经元。

在经过几百次迭代以后,我们发现,每当一个“生病”的样本输入时,两个隐含层神经元中的一个(对于生病的样本总是这个)总是显示出更高的激活值。而如果输入一个“健康”样本时,另一个隐含层则会显示更高的激活值。

再看学习

本质上来说,这两个隐含神经元从数据集中学习到了流感症状的一种紧致表示方法。为了检验它是不是真的实现了学习,我们再看下过度拟合的问题。通过训练我们的神经网络学习到的是一个紧致的简单的,而不是一个高度复杂且对数据集过度拟合的表示方法。

某种程度上来讲,与其说在找一种简单的表示方法,我们更是在尝试从“感觉”上去学习数据。

受限波尔兹曼机

下一步来看下受限波尔兹曼机(Restricted Boltzmann machines RBM),一种可以在输入数据集上学习概率分布的生成随机神经网络。

RBM由隐含层、可见层、偏置层组成。和前馈神经网络不同,可见层和隐含层之间的连接是无方向性(值可以从可见层->隐含层或隐含层->可见层任意传输)且全连接的(每一个当前层的神经元与下一层的每个神经元都有连接——如果允许任意层的任意神经元连接到任意层去,我们就得到了一个波尔兹曼机(非受限的))。

标准的RBM中,隐含和可见层的神经元都是二态的(即神经元的激活值只能是服从伯努力分布的0或1),不过也存在其它非线性的变种。

虽然学者们已经研究RBM很长时间了,最近出现的对比差异无监督训练算法使这个领域复兴。

对比差异

单步对比差异算法原理:

1、正向过程:

    • 输入样本 v 输入至输入层中。
    • v 通过一种与前馈网络相似的方法传播到隐含层中,隐含层的激活值为 h

2、反向过程:

    • 将 h 传回可见层得到 v’ (可见层和隐含层的连接是无方向的,可以这样传)。
    • 再将 v’ 传到隐含层中,得到 h’

3、权重更新:

其中 a 是学习速率, vv’hh’ 和 都是向量。

算法的思想就是在正向过程中影响了网络的内部对于真实数据的表示。同时,反向过程中尝试通过这个被影响过的表示方法重建数据。主要目的是可以使生成的数据与原数据尽可能相似,这个差异影响了权重更新。

换句话说,这样的网络具有了感知对输入数据表示的程度的能力,而且尝试通过这个感知能力重建数据。如果重建出来的数据与原数据差异很大,那么进行调整并再次重建。

再看流行感冒的例子

为了说明对比差异,我们使用与上例相同的流感症状的数据集。测试网络是一个包含6个可见层神经元、2个隐含层神经元的RBM。我们用对比差异的方法对网络进行训练,将症状 v 赋到可见层中。在测试中,这些症状值被重新传到可见层;然后再被传到隐含层。隐含层的神经元表示健康/生病的状态,与自编码器相似。

在进行过几百次迭代后,我们得到了与自编码器相同的结果:输入一个生病样本,其中一个隐含层神经元具有更高激活值;输入健康的样本,则另一个神经元更兴奋。

例子的代码在这里

深度网络

到现在为止,我们已经学习了隐含层中强大的特征探测器——自编码器和RBM,但现在还没有办法有效的去利用这些功能。实际上,上面所用到的这些数据集都是特定的。而我们要找到一些方法来间接的使用这些探测出的特征。

好消息是,已经发现这些结构可以通过栈式叠加来实现深度网络。这些网络可以通过贪心法的思想训练,每次训练一层,以克服之前提到在反向传播中梯度消失及过度拟合的问题。

这样的算法架构十分强大,可以产生很好的结果。如Google著名的“猫”识别,在实验中通过使用特定的深度自编码器,在无标记的图片库中学习到人和猫脸的识别。

下面我们将更深入。

栈式自编码器

和名字一样,这种网络由多个栈式结合的自编码器组成。

 

自编码器的隐含层 t 会作为 t + 1 层的输入层。第一个输入层就是整个网络的输入层。利用贪心法训练每一层的步骤如下:

1、通过反向传播的方法利用所有数据对第一层的自编码器进行训练(t=1,上图中的红色连接部分)。

2、训练第二层的自编码器 t=2 (绿色连接部分)。由于 t=2 的输入层是 t=1 的隐含层,我们已经不再关心 t=1 的输入层,可以从整个网络中移除。整个训练开始于将输入样本数据赋到 t=1 的输入层,通过前向传播至 t = 2 的输出层。下面t = 2的权重(输入->隐含和隐含->输出)使用反向传播的方法进行更新。t = 2的层和 t=1 的层一样,都要通过所有样本的训练。

3、对所有层重复步骤1-2(即移除前面自编码器的输出层,用另一个自编码器替代,再用反向传播进行训练)。

4、步骤1-3被称为预训练,这将网络里的权重值初始化至一个合适的位置。但是通过这个训练并没有得到一个输入数据到输出标记的映射。例如,一个网络的目标是被训练用来识别手写数字,经过这样的训练后还不能将最后的特征探测器的输出(即隐含层中最后的自编码器)对应到图片的标记上去。这样,一个通常的办法是在网络的最后一层(即蓝色连接部分)后面再加一个或多个全连接层。整个网络可以被看作是一个多层的感知机,并使用反向传播的方法进行训练(这步也被称为微调)。

栈式自编码器,提供了一种有效的预训练方法来初始化网络的权重,这样你得到了一个可以用来训练的复杂、多层的感知机。

深度信度网络

和自编码器一样,我也可以将波尔兹曼机进行栈式叠加来构建深度信度网络(DBN)。

 

在本例中,隐含层 RBM 可以看作是 RBM t+1 的可见层。第一个RBM的输入层即是整个网络的输入层,层间贪心式的预训练的工作模式如下:

1. 通过对比差异法对所有训练样本训练第一个RBM t=1

2. 训练第二个RBM t=1。由于 t=2 的可见层是 t=1 的隐含层,训练开始于将数据赋至 t=1 的可见层,通过前向传播的方法传至 t=1 的隐含层。然后作为 t=2 的对比差异训练的初始数据。

3. 对所有层重复前面的过程。

4. 和栈式自编码器一样,通过预训练后,网络可以通过连接到一个或多个层间全连接的 RBM 隐含层进行扩展。这构成了一个可以通过反向传僠进行微调的多层感知机。

本过程和栈式自编码器很相似,只是用RBM将自编码器进行替换,并用对比差异算法将反向传播进行替换。

  (注: 例中的源码可以从 此处获得.)

卷积网络

这个是本文最后一个软件架构——卷积网络,一类特殊的对图像识别非常有效的前馈网络。

在我们深入看实际的卷积网络之臆,我们先定义一个图像滤波器,或者称为一个赋有相关权重的方阵。一个滤波器可以应用到整个图片上,通常可以应用多个滤波器。比如,你可以应用四个6×6的滤波器在一张图片上。然后,输出中坐标(1,1)的像素值就是输入图像左上角一个6×6区域的加权和,其它像素也是如此。

有了上面的基础,我们来介绍定义出卷积网络的属性:

  • 卷积层  对输入数据应用若干滤波器。比如图像的第一卷积层使用4个6×6滤波器。对图像应用一个滤波器之后的得到的结果被称为特征图谱(feature map, FM),特征图谱的数目和滤波器的数目相等。如果前驱层也是一个卷积层,那么滤波器应用在FM上,相当于输入一个FM,输出另外一个FM。从直觉上来讲,如果将一个权重分布到整个图像上后,那么这个特征就和位置无关了,同时多个滤波器可以分别探测出不同的特征。
  • 下采样层 缩减输入数据的规模。例如输入一个32×32的图像,并且通过一个2×2的下采样,那么可以得到一个16×16的输出图像,这意味着原图像上的四个像素合并成为输出图像中的一个像素。实现下采样的方法有很多种,最常见的是最大值合并、平均值合并以及随机合并。
  • 最后一个下采样层(或卷积层)通常连接到一个或多个全连层,全连层的输出就是最终的输出。
  • 训练过程通过改进的反向传播实现,将下采样层作为考虑的因素并基于所有值来更新卷积滤波器的权重。

可以在这看几个应用在 MNIST 数据集上的卷积网络的例子,在这还有一个用JavaScript实现的一个可视的类似网络。

实现

目前为止,我们已经学会了常见神经网络中最主要的元素了,但是我只写了很少的在实现过程中所遇到的挑战。

概括来讲,我的目标是实现一个深度学习的库,即一个基于神经网络且满足如下条件的框架:

    • 一个可以表示多种模型的通用架构(比如所有上文提到的神经网络中的元素)
    • 可以使用多种训练算法(反向传播,对比差异等等)。
    • 体面的性能

为了满足这些要求,我在软件的设计中使用了分层的思想。

结构

我们从如下的基础部分开始:

    • NeuralNetworkImpl 是所有神经网络模型实现的基类。
    • 每个网络都包含有一个 layer 的集合。
    • 每一层中有一个 connections 的链表, connection 指的是两个层之间的连接,将整个网络构成一个有向无环图。

这个结构对于经典的反馈网络、RBM 及更复杂的如 ImageNet 都已经足够灵活。

这个结构也允许一个 layer 成为多个网络的元素。比如,在 Deep Belief Network(深度信度网络)中的layer也可以用在其 RBM 中。

另外,通过这个架构可以将DBN的预训练阶段显示为一个栈式RBM的列表,微调阶段显示为一个前馈网络,这些都非常直观而且程序实现的很好。

数据流

下个部分介绍网络中的数据流,一个两步过程:

  1. 定义出层间的序列。例如,为了得到一个多层感知机的结果,输入数据被赋到输入层(因此,这也是首先被计算的层),然后再将数据通过不同的方法流向输出层。为了在反向传播中更新权重,输出的误差通过广度优先的方法从输出层传回每一层。这部分通过 LayerOrderStrategy 进行实现,应用到了网络图结构的优势,使用了不同的图遍历方法。其中一些样例包含了 广度优先策略 和 定位到一个指定的层。层的序列实际上由层间的连接进行决定,所以策略部分都是返回一个连接的有序列表。
  2. 计算激活值。每一层都有一个关联的 ConnectionCalculator,包含有连接的列表(从上一步得来)和输入值(从其它层得到)并计算得到结果的激活值。例如,在一个简单的S形前馈网络中,隐含层的  ConnectionCalculator 接受输入层和偏置层的值(分别为输入值和一个值全为1的数组)和神经元之间的权重值(如果是全连接层,权重值实际上以一个矩阵的形式存储在一个 FullyConnected 结构中,计算加权和,然后将结果传给S函数。ConnectionCalculator 中实现了一些转移函数(如加权求和、卷积)和激活函数(如对应多层感知机的对数函数和双曲正切函数,对应RBM的二态函数)。其中的大部分都可以通过 Aparapi 在GPU上进行计算,可以利用迷你批次训练。

通过 Aparapi 进行 GPU 计算

像我之前提到的,神经网络在近些年复兴的一个重要原因是其训练的方法可以高度并行化,允许我们通过GPGPU高效的加速训练。本文中,我选择 Aparapi 库来进行GPU的支持。

Aparapi 在连接计算上强加了一些重要的限制:

  • 只允许使用原始数据类型的一维数组(变量)。
  • 在GPU上运行的程序只能调用 Aparapi Kernel 类本身的成员函数。

这样,大部分的数据(权重、输入和输出数据)都要保存在 Matrix 实例里面,其内部是一个一维浮点数组。所有Aparapi 连接计算都是使用 AparapiWeightedSum (应用在全连接层和加权求和函数上)、 AparapiSubsampling2D (应用在下采样层)或 AparapiConv2D (应用在卷积层)。这些限制可以通过 Heterogeneous System Architecture 里介绍的内容解决一些。而且Aparapi 允许相同的代码运行在CPU和GPU上。

训练

training 的模块实现了多种训练算法。这个模块依赖于上文提到的两个模块。比如,BackPropagationTrainer (所有的训练算法都以 Trainer 为基类)在前馈阶段使用前馈层计算,在误差传播和权重更新时使用特殊的广度优先层计算。

我最新的工作是在Java8环境下开发,其它一些更新的功能可以在这个branch 下获得,这部分的工作很快会merge到主干上。

结论

本文的目标是提供一个深度学习算法领域的一个简明介绍,由最基本的组成元素开始(感知机)并逐渐深入到多种当前流行且有效的架构上,比如受限波尔兹曼机。

神经网络的思想已经出现了很长时间,但是今天,你如果身处机器学习领域而不知道深度学习或其它相关知识是不应该的。不应该过度宣传,但不可否认随着GPGPU提供的计算能力、包括Geoffrey Hinton, Yoshua Bengio, Yann LeCun and Andrew Ng在内的研究学者们提出的高效算法,这个领域已经表现出了很大的希望。现在正是最佳的时间深入这些方面的学习。

附录:相关资源

如果你想更深入的学习,下面的这些资源在我的工作当中都起过重要的作用:

在听到人们谈论机器学习的时候,你是不是对它的涵义只有几个模糊的认识呢?你是不是已经厌倦了在和同事交谈时只能一直点头?让我们改变一下吧!

本指南的读者对象是所有对机器学习有求知欲但却不知道如何开头的朋友。我猜很多人已经读过了“机器学习”的维基百科词条,倍感挫折,以为没人能给出一个高层次的解释。本文就是你们想要的东西。

本文目标在于平易近人,这意味着文中有大量的概括。但是谁在乎这些呢?只要能让读者对于ML更感兴趣,任务也就完成了。

何为机器学习?

机器学习这个概念认为,对于待解问题,你无需编写任何专门的程序代码,遗传算法(generic algorithms)能够在数据集上为你得出有趣的答案。对于遗传算法,不用编码,而是将数据输入,它将在数据之上建立起它自己的逻辑。

举个例子,有一类算法称为分类算法,它可以将数据划分为不同的组别。一个用来识别手写数字的分类算法,不用修改一行代码,就可以用来将电子邮件分为垃圾邮件和普通邮件。算法没变,但是输入的训练数据变了,因此它得出了不同的分类逻辑。

机器学习算法是个黑盒,可以重用来解决很多不同的分类问题。

“机器学习”是一个涵盖性术语,覆盖了大量类似的遗传算法。

 

两类机器学习算法

你可以认为机器学习算法分为两大类:监督式学习(Supervised Learning)监督式学习(Unsupervised Learning)。两者区别很简单,但却非常重要。

监督式学习

假设你是一名房产经纪,生意越做越大,因此你雇了一批实习生来帮你。但是问题来了——你可以看一眼房子就知道它到底值多少钱,实习生没有经验,不知道如何估价。

为了帮助你的实习生(也许是为了解放你自己去度个假),你决定写个小软件,可以根据房屋大小、地段以及类似房屋的成交价等因素来评估你所在地区房屋的价值。

你把3个月来城里每笔房屋交易都写了下来,每一单你都记录了一长串的细节——卧室数量、房屋大小、地段等等。但最重要的是,你写下了最终的成交价:

这是我们的“训练数据”。

我们要利用这些训练数据来编写一个程序来估算该地区其他房屋的价值:

这就称为监督式学习。你已经知道每一栋房屋的售价,换句话说,你知道问题的答案,并可以反向找出解题的逻辑。

为了编写软件,你将包含每一套房产的训练数据输入你的机器学习算法。算法尝试找出应该使用何种运算来得出价格数字。

这就像是算术练习题,算式中的运算符号都被擦去了: 

天哪!一个阴险的学生将老师答案上的算术符号全擦去了。

看了这些题,你能明白这些测验里面是什么样的数学问题吗?你知道,你应该对算式左边的数字“做些什么”以得出算式右边的答案。

在监督式学习中,你是让计算机为你算出数字间的关系。而一旦你知道了解决这类特定问题所需要的数学方法后,你就可以解答同类的其它问题了。

非监督式学习

 

让我们回到开头那个房地产经纪的例子。要是你不知道每栋房子的售价怎么办?即使你所知道的只是房屋的大小、位置等信息,你也可以搞出很酷的花样。这就是所谓的非监督式学习

 
即使你不是想去预测未知的数据(如价格),你也可以运用机器学习完成一些有意思的事。

这就有点像有人给你一张纸,上面列出了很多数字,然后对你说:“我不知道这些数字有什么意义,也许你能从中找出规律或是能将它们分类,或是其它什么-祝你好运!”

你该怎么处理这些数据呢?首先,你可以用个算法自动地从数据中划分出不同的细分市场。也许你会发现大学附近的买房者喜欢户型小但卧室多的房子,而郊区的买房者偏好三卧室的大户型。这些信息可以直接帮助你的营销。

你还可以作件很酷的事,自动找出房价的离群数据,即与其它数据迥异的值。这些鹤立鸡群的房产也许是高楼大厦,而你可以将最优秀的推销员集中在这些地区,因为他们的佣金更高。

本文余下部分我们主要讨论监督式学习,但这并不是因为非监督式学习用处不大或是索然无味。实际上,随着算法改良,不用将数据和正确答案联系在一起,因此非监督式学习正变得越来越重要。

老学究请看:还有很多其它种类的机器学习算法。但初学时这样理解不错了。

 

太酷了,但是评估房价真能被看作“学习”吗?

作为人类的一员,你的大脑可以应付绝大多数情况,并且没有任何明确指令也能够学习如何处理这些情况。如果你做房产经纪时间很长,你对于房产的合适定价、它的最佳营销方式以及哪些客户会感兴趣等等都会有一种本能般的“感觉”。强人工智能(Strong AI)研究的目标就是要能够用计算机复制这种能力。

但是目前的机器学习算法还没有那么好——它们只能专注于非常特定的、有限的问题。也许在这种情况下,“学习”更贴切的定义是“在少量范例数据的基础上找出一个等式来解决特定的问题”。

不幸的是,“机器在少量范例数据的基础上找出一个等式来解决特定的问题”这个名字太烂了。所以最后我们用“机器学习”取而代之。

当然,要是你是在50年之后来读这篇文章,那时我们已经得出了强人工智能算法,而本文看起来就像个老古董。未来的人类,你还是别读了,叫你的机器仆人给你做份三明治吧。

让我们写代码吧!

前面例子中评估房价的程序,你打算怎么写呢?往下看之前,先思考一下吧。

如果你对机器学习一无所知,很有可能你会尝试写出一些基本规则来评估房价,如下:

假如你像这样瞎忙几个小时,也许会取得一点成效,但是你的程序永不会完美,而且当价格变化时很难维护。

如果能让计算机找出实现上述函数功能的办法,这样岂不更好?只要返回的房价数字正确,谁会在乎函数具体干了些什么呢?

考虑这个问题的一种角度是将房价看做一碗美味的汤,而汤中成分就是卧室数、面积和地段。如果你能算出每种成分对最终的价格有多大影响,也许就能得到各种成分混合起来形成最终价格的具体比例。

这样可以将你最初的程序(全是疯狂的if else语句)简化成类似如下的样子:

请注意那些用粗体标注的神奇数字——.841231951398213, 1231.1231231,2.3242341421, 201.23432095。它们称为权重。如果我们能找出对每栋房子都适用的完美权重,我们的函数就能预测所有的房价!

找出最佳权重的一种笨办法如下所示:

步骤1:

首先,将每个权重都设为1.0:

步骤2:

将每栋房产带入你的函数运算,检验估算值与正确价格的偏离程度:

运用你的程序预测房屋价格。

 

例如:上表中第一套房产实际成交价为25万美元,你的函数估价为17.8万,这一套房产你就差了7.2万。

再将你的数据集中的每套房产估价偏离值平方后求和。假设数据集中有500套房产交易,估价偏离值平方求和总计为86,123,373美元。这就反映了你的函数现在的“正确”程度。

现在,将总计值除以500,得到每套房产的估价偏离平均值。将这个平均误差值称为你函数的代价

如果你能调整权重使得这个代价变为0,你的函数就完美了。它意味着,根据输入的数据,你的程序对每一笔房产交易的估价都是分毫不差。而这就是我们的目标——尝试不同的权重值以使代价尽可能的低。

步骤3:

不断重复步骤2,尝试所有可能的权重值组合。哪一个组合使得代价最接近于0,它就是你要使用的,你只要找到了这样的组合,问题就得到了解决!

思想扰动时间

这太简单了,对吧?想一想刚才你做了些什么。你取得了一些数据,将它们输入至三个通用的简单步骤中,最后你得到了一个可以对你所在区域的房屋进行估价的函数。房价网,要当心咯!
但是下面的事实可能会扰乱你的思想:

1.过去40年来,很多领域(如语言学/翻译学)的研究表明,这种通用的“搅动数据汤”(我编造的词)式的学习算法已经胜过了需要利用真人明确规则的方法。机器学习的“笨”办法最终打败了人类专家。

2.你最后写出的函数真是笨,它甚至不知道什么是“面积”和“卧室数”。它知道的只是搅动,改变数字来得到正确的答案。

3.很可能你都不知道为何一组特殊的权重值能起效。所以你只是写出了一个你实际上并不理解却能证明的函数。
4.试想一下,你的程序里没有类似“面积”和“卧室数”这样的参数,而是接受了一组数字。假设每个数字代表了你车顶安装的摄像头捕捉的画面中的一个像素,再将预测的输出不称为“价格”而是叫做“方向盘转动度数”,这样你就得到了一个程序可以自动操纵你的汽车了!

太疯狂了,对吧?

 

步骤3中的“尝试每个数字”怎么回事?

好吧,当然你不可能尝试所有可能的权重值来找到效果最好的组合。那可真要花很长时间,因为要尝试的数字可能无穷无尽。
为避免这种情况,数学家们找到了很多聪明的办法来快速找到优秀的权重值,而不需要尝试过多。下面是其中一种:
首先,写出一个简单的等式表示前述步骤2:

这是你的代价函数

接着,让我们将这同一个等式用机器学习的数学术语(现在你可以忽略它们)进行重写:

 
θ表示当前的权重值。 J(θ) 意为“当前权重值对应的代价”。

这个等式表示我们的估价程序在当前权重值下偏离程度的大小。
如果将所有赋给卧室数和面积的可能权重值以图形形式显示,我们会得到类似下图的图表:

代价函数的图形像一支碗。纵轴表示代价。

图中蓝色的最低点就是代价最低的地方——即我们的程序偏离最小。最高点意味着偏离最大。所以,如果我们能找到一组权重值带领我们到达图中的最低点,我们就找到了答案!

因此,我们只需要调整权重值使我们在图上能向着最低点“走下坡路”。如果对于权重的细小调节能一直使我们保持向最低点移动,那么最终我们不用尝试太多权重值就能到达那里。

如果你还记得一点微积分的话,你也许记得如果你对一个函数求导,结果会告诉你函数在任一点的斜率。换句话说,对于图上给定一点,它告诉我们那条路是下坡路。我们可以利用这一点朝底部进发。

所以,如果我们对代价函数关于每一个权重求偏导,那么我们就可以从每一个权重中减去该值。这样可以让我们更加接近山底。一直这样做,最终我们将到达底部,得到权重的最优值。(读不懂?不用担心,接着往下读)。

这种找出最佳权重的办法被称为批量梯度下降,上面是对它的高度概括。如果想搞懂细节,不要害怕,继续深入下去吧。

当你使用机器学习算法库来解决实际问题,所有这些都已经为你准备好了。但明白一些具体细节总是有用的。

 

还有什么你随便就略过了?

上面我描述的三步算法被称为多元线性回归。你估算等式是在求一条能够拟合所有房价数据点的直线。然后,你再根据房价在你的直线上可能出现的位置用这个等式来估算从未见过的房屋的价格。这个想法威力强大,可以用它来解决“实际”问题。

但是,我为你展示的这种方法可能在简单的情况下有效,它不会在所有情况下都有用。原因之一是因为房价不会一直那么简单地跟随一条连续直线。

但是,幸运的是,有很多办法来处理这种情况。对于非线性数据,很多其他类型的机器学习算法可以处理(如神经网络或有核向量机)。还有很多方法运用线性回归更灵活,想到了用更复杂的线条来拟合。在所有的情况中,寻找最优权重值这一基本思路依然适用。

还有,我忽略了过拟合的概念。很容易碰上这样一组权重值,它们对于你原始数据集中的房价都能完美预测,但对于原始数据集之外的任何新房屋都预测不准。这种情况的解决之道也有不少(如正则化以及使用交叉验证数据集)。学会如何处理这一问题对于顺利应用机器学习至关重要。

换言之,基本概念非常简单,要想运用机器学习得到有用的结果还需要一些技巧和经验。但是,这是每个开发者都能学会的技巧。

 

机器学习法力无边吗?

一旦你开始明白机器学习技术很容易应用于解决貌似很困难的问题(如手写识别),你心中会有一种感觉,只要有足够的数据,你就能够用机器学习解决任何问题。只需要将数据输入进去,就能看到计算机变戏法一样找出拟合数据的等式。

但是很重要的一点你要记住,机器学习只能对用你占有的数据实际可解的问题才适用。

例如,如果你建立了一个模型来根据每套房屋内盆栽数量来预测房价,它就永远不会成功。房屋内盆栽数量和房价之间没有任何的关系。所以,无论它怎么去尝试,计算机也推导不出两者之间的关系。

你只能对实际存在的关系建模。

怎样深入学习机器学习

我认为,当前机器学习的最大问题是它主要活跃于学术界和商业研究组织中。对于圈外想要有个大体了解而不是想成为专家的人们,简单易懂的学习资料不多。但是这一情况每一天都在改善。

吴恩达教授(Andrew Ng)在Coursera上的机器学习免费课程非常不错。我强烈建议由此入门。任何拥有计算机科学学位、还能记住一点点数学的人应该都能理解。

另外,你还可以下载安装SciKit-Learn,用它来试验成千上万的机器学习算法。它是一个python框架,对于所有的标准算法都有“黑盒”版本。

在做分類時常常需要估算不同樣本之間的相似性度量(Similarity Measurement),這時通常採用的方法就是計算樣本間的「距離」(Distance)。採用什麼樣的方法計算距離是很講究,甚至關係到分類的正確與否。

本文的目的就是對常用的相似性度量作一個總結。

本文目錄:

1. 歐氏距離

2. 曼哈頓距離

3. 切比雪夫距離

4. 閔可夫斯基距離

5. 標準化歐氏距離

6. 馬氏距離

7. 夾角餘弦

8. 漢明距離

9. 傑卡德距離 & 傑卡德相似係數

10. 相關係數 & 相關距離

11. 信息熵

1. 歐氏距離(Euclidean Distance)

歐氏距離是最易於理解的一種距離計算方法,源自歐氏空間中兩點間的距離公式。

(1)二維平面上兩點a(x1,y1)與b(x2,y2)間的歐氏距離:

(2)三維空間兩點a(x1,y1,z1)與b(x2,y2,z2)間的歐氏距離:

(3)兩個n維向量a(x11,x12,…,x1n)與 b(x21,x22,…,x2n)間的歐氏距離:

也可以用表示成向量運算的形式:

(4)Matlab計算歐氏距離

Matlab計算距離主要使用pdist函數。若X是一個M×N的矩陣,則pdist(X)將X矩陣M行的每一行作為一個N維向量,然後計算這M個向量兩兩間的距離。

例子:計算向量(0,0)、(1,0)、(0,2)兩兩間的歐式距離

X = [0 0 ; 1 0 ; 0 2]

D = pdist(X,’euclidean’)

結果:

D =

1.0000    2.0000    2.2361

 

2. 曼哈頓距離(Manhattan Distance)

從名字就可以猜出這種距離的計算方法了。想像你在曼哈頓要從一個十字路口開車到另外一個十字路口,駕駛距離是兩點間的直線距離嗎?顯然不是,除非你能穿越大樓。實際駕駛距離就是這個「曼哈頓距離」。而這也是曼哈頓距離名稱的來源, 曼哈頓距離也稱為城市街區距離(City Block distance)

(1)二維平面兩點a(x1,y1)與b(x2,y2)間的曼哈頓距離

(2)兩個n維向量a(x11,x12,…,x1n)與 b(x21,x22,…,x2n)間的曼哈頓距離

(3) Matlab計算曼哈頓距離

例子:計算向量(0,0)、(1,0)、(0,2)兩兩間的曼哈頓距離

X = [0 0 ; 1 0 ; 0 2]

D = pdist(X, ‘cityblock’)

結果:

D =

1     2     3

3. 切比雪夫距離 ( Chebyshev Distance )

國際象棋玩過麼?國王走一步能夠移動到相鄰的8個方格中的任意一個。那麼國王從格子(x1,y1)走到格子(x2,y2)最少需要多少步?自己走走試試。你會發現最少步數總是max( | x2-x1 | , | y2-y1 | ) 步 。有一種類似的一種距離度量方法叫切比雪夫距離。

(1)二維平面兩點a(x1,y1)與b(x2,y2)間的切比雪夫距離

(2)兩個n維向量a(x11,x12,…,x1n)與 b(x21,x22,…,x2n)間的切比雪夫距離

這個公式的另一種等價形式是

看不出兩個公式是等價的?提示一下:試試用放縮法和夾逼法則來證明。

(3)Matlab計算切比雪夫距離

例子:計算向量(0,0)、(1,0)、(0,2)兩兩間的切比雪夫距離

X = [0 0 ; 1 0 ; 0 2]

D = pdist(X, ‘chebychev’)

結果:

D =

1     2     2

 

4. 閔可夫斯基距離(Minkowski Distance)

閔氏距離不是一種距離,而是一組距離的定義。

(1) 閔氏距離的定義

兩個n維變量a(x11,x12,…,x1n)與 b(x21,x22,…,x2n)間的閔可夫斯基距離定義為:

其中p是一個變參數。

當p=1時,就是曼哈頓距離

當p=2時,就是歐氏距離

當p→∞時,就是切比雪夫距離

根據變參數的不同,閔氏距離可以表示一類的距離。

(2)閔氏距離的缺點

閔氏距離,包括曼哈頓距離、歐氏距離和切比雪夫距離都存在明顯的缺點。

舉個例子:二維樣本(身高,體重),其中身高範圍是150~190,體重範圍是50~60,有三個樣本:a(180,50),b(190,50),c(180,60)。那麼a與b之間的閔氏距離(無論是曼哈頓距離、歐氏距離或切比雪夫距離)等於a與c之間的閔氏距離,但是身高的10cm真的等價於體重的10kg麼?因此用閔氏距離來衡量這些樣本間的相似度很有問題。

簡單說來,閔氏距離的缺點主要有兩個:(1)將各個份量的量綱(scale),也就是「單位」當作相同的看待了。(2)沒有考慮各個份量的分佈(期望,方差等)可能是不同的。

(3)Matlab計算閔氏距離

例子:計算向量(0,0)、(1,0)、(0,2)兩兩間的閔氏距離(以變參數為2的歐氏距離為例)

X = [0 0 ; 1 0 ; 0 2]

D = pdist(X,’minkowski’,2)

結果:

D =

1.0000    2.0000    2.2361

5. 標準化歐氏距離 (Standardized Euclidean distance )

(1)標準歐氏距離的定義

標準化歐氏距離是針對簡單歐氏距離的缺點而作的一種改進方案。標準歐氏距離的思路:既然數據各維份量的分佈不一樣,好吧!那我先將各個份量都「標準化」到均值、方差相等吧。均值和方差標準化到多少呢?這裡先複習點統計學知識吧,假設樣本集X的均值(mean)為m,標準差(standard deviation)為s,那麼X的「標準化變量」表示為:

而且標準化變量的數學期望為0,方差為1。因此樣本集的標準化過程(standardization)用公式描述就是:

標準化後的值 =  ( 標準化前的值  - 份量的均值 ) /份量的標準差

經過簡單的推導就可以得到兩個n維向量a(x11,x12,…,x1n)與 b(x21,x22,…,x2n)間的標準化歐氏距離的公式:

如果將方差的倒數看成是一個權重,這個公式可以看成是一種加權歐氏距離(Weighted Euclidean distance)

(2)Matlab計算標準化歐氏距離

例子:計算向量(0,0)、(1,0)、(0,2)兩兩間的標準化歐氏距離 (假設兩個份量的標準差分別為0.5和1)

X = [0 0 ; 1 0 ; 0 2]

D = pdist(X, ‘seuclidean’,[0.5,1])

結果:

D =

2.0000    2.0000    2.8284

 

6. 馬氏距離(Mahalanobis Distance)

(1)馬氏距離定義

有M個樣本向量X1~Xm,協方差矩陣記為S,均值記為向量μ,則其中樣本向量X到u的馬氏距離表示為:

 

而其中向量Xi與Xj之間的馬氏距離定義為:

若協方差矩陣是單位矩陣(各個樣本向量之間獨立同分佈),則公式就成了:

也就是歐氏距離了。

若協方差矩陣是對角矩陣,公式變成了標準化歐氏距離。

(2)馬氏距離的優缺點:量綱無關,排除變量之間的相關性的干擾。

(3) Matlab計算(1 2),( 1 3),( 2 2),( 3 1)兩兩之間的馬氏距離

X = [1 2; 1 3; 2 2; 3 1]

Y = pdist(X,’mahalanobis’)

結果:

Y =

2.3452    2.0000    2.3452    1.2247    2.4495    1.2247

 

7. 夾角餘弦(Cosine)

有沒有搞錯,又不是學幾何,怎麼扯到夾角餘弦了?各位看官稍安勿躁。幾何中夾角餘弦可用來衡量兩個向量方向的差異,機器學習中借用這一概念來衡量樣本向量之間的差異。

(1)在二維空間中向量A(x1,y1)與向量B(x2,y2)的夾角餘弦公式:

(2) 兩個n維樣本點a(x11,x12,…,x1n)和b(x21,x22,…,x2n)的夾角餘弦

類似的,對於兩個n維樣本點a(x11,x12,…,x1n)和b(x21,x22,…,x2n),可以使用類似於夾角餘弦的概念來衡量它們間的相似程度。

即:

夾角餘弦取值範圍為[-1,1]。夾角餘弦越大表示兩個向量的夾角越小,夾角餘弦越小表示兩向量的夾角越大。當兩個向量的方向重合時夾角餘弦取最大值1,當兩個向量的方向完全相反夾角餘弦取最小值-1。

夾角餘弦的具體應用可以參閱參考文獻[1]。

(3)Matlab計算夾角餘弦

例子:計算(1,0)、( 1,1.732)、( -1,0)兩兩間的夾角餘弦

X = [1 0 ; 1 1.732 ; -1 0]

D = 1- pdist(X, ‘cosine’)  % Matlab中的pdist(X, ‘cosine’)得到的是1減夾角餘弦的值

結果:

D =

0.5000   -1.0000   -0.5000

 

8. 漢明距離(Hamming distance)

(1)漢明距離的定義

兩個等長字符串s1與s2之間的漢明距離定義為將其中一個變為另外一個所需要作的最小替換次數。例如字符串「1111」與「1001」之間的漢明距離為2。

應用:信息編碼(為了增強容錯性,應使得編碼間的最小漢明距離儘可能大)。

(2)Matlab計算漢明距離

Matlab中2個向量之間的漢明距離的定義為2個向量不同的份量所佔的百分比。

例子:計算向量(0,0)、(1,0)、(0,2)兩兩間的漢明距離

X = [0 0 ; 1 0 ; 0 2];

D = PDIST(X, ‘hamming’)

結果:

D =

0.5000    0.5000    1.0000

 

9. 傑卡德相似係數(Jaccard similarity coefficient)

(1) 傑卡德相似係數

兩個集合A和B的交集元素在A,B的並集中所佔的比例,稱為兩個集合的傑卡德相似係數,用符號J(A,B)表示。

傑卡德相似係數是衡量兩個集合的相似度一種指標。

(2) 傑卡德距離

與傑卡德相似係數相反的概念是傑卡德距離(Jaccard distance)。傑卡德距離可用如下公式表示:

傑卡德距離用兩個集合中不同元素佔所有元素的比例來衡量兩個集合的區分度。

(3) 傑卡德相似係數與傑卡德距離的應用

可將傑卡德相似係數用在衡量樣本的相似度上。

樣本A與樣本B是兩個n維向量,而且所有維度的取值都是0或1。例如:A(0111)和B(1011)。我們將樣本看成是一個集合,1表示集合包含該元素,0表示集合不包含該元素。

p :樣本A與B都是1的維度的個數

q :樣本A是1,樣本B是0的維度的個數

r :樣本A是0,樣本B是1的維度的個數

s :樣本A與B都是0的維度的個數

那麼樣本A與B的傑卡德相似係數可以表示為:

這裡p+q+r可理解為A與B的並集的元素個數,而p是A與B的交集的元素個數。

而樣本A與B的傑卡德距離表示為:

(4)Matlab 計算傑卡德距離

Matlab的pdist函數定義的傑卡德距離跟我這裡的定義有一些差別,Matlab中將其定義為不同的維度的個數佔「非全零維度」的比例。

例子:計算(1,1,0)、(1,-1,0)、(-1,1,0)兩兩之間的傑卡德距離

X = [1 1 0; 1 -1 0; -1 1 0]

D = pdist( X , ‘jaccard’)

結果

D =

0.5000    0.5000    1.0000

 

10. 相關係數 ( Correlation coefficient )與相關距離(Correlation distance)

(1) 相關係數的定義

相關係數是衡量隨機變量X與Y相關程度的一種方法,相關係數的取值範圍是[-1,1]。相關係數的絕對值越大,則表明X與Y相關度越高。當X與Y線性相關時,相關係數取值為1(正線性相關)或-1(負線性相關)。

(2)相關距離的定義

(3)Matlab計算(1, 2 ,3 ,4 )與( 3 ,8 ,7 ,6 )之間的相關係數與相關距離

X = [1 2 3 4 ; 3 8 7 6]

C = corrcoef( X’ )   %將返回相關係數矩陣

D = pdist( X , ‘correlation’)

結果:

C =

1.0000    0.4781

0.4781    1.0000

D =

0.5219

其中0.4781就是相關係數,0.5219是相關距離。

11. 信息熵(Information Entropy)

信息熵並不屬於一種相似性度量。那為什麼放在這篇文章中啊?這個。。。我也不知道。 (╯▽╰)

信息熵是衡量分佈的混亂程度或分散程度的一種度量。分佈越分散(或者說分佈越平均),信息熵就越大。分佈越有序(或者說分佈越集中),信息熵就越小。

計算給定的樣本集X的信息熵的公式:

參數的含義:

n:樣本集X的分類數

pi:X中第i類元素出現的概率

信息熵越大表明樣本集S分類越分散,信息熵越小則表明樣本集X分類越集中。。當S中n個分類出現的概率一樣大時(都是1/n),信息熵取最大值log2(n)。當X只有一個分類時,信息熵取最小值0

參考資料: 

[1]吳軍. 數學之美 系列 12 – 餘弦定理和新聞的分類.

http://www.google.com.hk/ggblog/googlechinablog/2006/07/12_4010.html

[2] Wikipedia. Jaccard index.

http://en.wikipedia.org/wiki/Jaccard_index

[3] Wikipedia. Hamming distance

http://en.wikipedia.org/wiki/Hamming_distance

[4] 求馬氏距離(Mahalanobis distance )matlab版

http://junjun0595.blog.163.com/blog/static/969561420100633351210/

[5] Pearson product-moment correlation coefficient

http://en.wikipedia.org/wiki/Pearson_product-moment_correlation_coefficient

舊的方法原本是以下這樣,後來我裝新的XP系統時發現無法成功

首先從 http://www.indyproject.org/downloads/10/indy10.0.52_source.zip下載壓縮檔。
(或是到這個網頁找http://www.indyproject.org/sockets/download/files/indy10.en.aspx )

Indy 10 安裝步驟:

移除舊版 Indy 9
  1. 進 Delphi 後,選擇 Component –> Install Packages…
  2. 在 Design Packages 找有 Indy 字樣的選項後,Remove 後離開。
  3. 在 Delphi 裡,選擇 Tools –> Environment Options,打開 Library 標籤,刪除有Indy 的路徑。
  4. 在系統資料夾找所有 Indy 開頭的 Indy*.bpl 和 Indy*.dcp 檔案,並刪除。
開始安裝:
  1. 解壓縮剛下載的 Indy10.0.52_source.zip 檔至 您放置 Lib 的目錄 (例:C:\Program Files (x86)\Borland\Delphi7\Source\Indy10),並將底下四個目錄加入 Delphi 內的 Library 路徑。
  2. 打開 \System\IndySystem70.dpk        ,Compile
  3. 打開 \Core\IndyCore70.dpk               ,Compile
  4. 打開 \Protocols\IndyProtocols70.dpk   ,Compile
  5. 打開 \SuperCore\IndySuperCore70.dpk,Compile
  6. 打開 \Core\dclIndyCore70.dpk            ,Install
  7. 打開 \Protocols\dclIndyProtocols70.dpk,Install
  8. 打開 \SuperCore\dclIndySuperCore70.dpk,Install

 

新的方式(成功率大幅提高)

http://indy.fulgan.com/ZIP/ 下載開發中的Indy10,(2014.3.3)今日的版本是Indy10_5099.zip
(下載舊方法的連結 indy10.0.52_source.zip 應該也可以,但我沒試過)

開始進行安裝

先移除原安裝的 Indy (Delphi7之後好像都會自動裝Indy9,大致和舊方式相同,這邊再寫詳細一點):

  1. 進 Delphi 後,選擇上方功能選項 「Component」 –> 「Install Packages…」
  2. 在 「Design Packages」框框裡找有「Indy」字樣的選項,點選後按「Remove」移除(如有安裝過或Delphi7之後版本應該有1,2個)
  3. 全部移乾淨後,按 「OK」離開
  4. 再選擇上方功能選項「Tools」 –> 「Environment Options」,再點開「Library」標籤
  5. 在「Directories」框框裡,找「Library path」的最後面,點「…」按鈕
  6. 尋找列表內,有任何「Indy」字樣的路徑,點選後按「Delete」刪除掉
  7. 刪除完後,點選「OK」離開,關掉 Delphi
  8. 再從檔案總管中,搜尋「系統資料夾」(就是Windows目錄)內所有的Indy開頭的檔案,應該都會在 windows/system32 裡面

確定以上步驟都完成,或是您確定沒安裝任何版本的Indy ,就可以開始進行安裝

  1. 解壓縮剛下載的壓縮檔到你想參照的目錄位置,我是放在 C:\Program Files (x86)\Borland\Delphi7\Source\Indy10
  2. 進入 \Lib 目錄,找Fulld_7.bat,點選執行 (不同版本請自行變通,這步不做應該也可以,10.0.52版沒有這檔案,請自行嘗試)
  3. 點擊打開 \System\IndySystem70.dpk ,然後點 Compile
  4. 點擊打開 \Core\IndyCore70.dpk ,然後點 Compile
  5. 點擊打開 \Protocols\IndyProtocols70.dpk ,然後點 Compile
  6. 在 Delphi 上面功能列選擇「Component」 –> 「Install Packages…」 (用點擊檔案打開後Install的方式經常失敗)
  7. 點「Add…」按鈕,選擇 \Core\dclIndyCore70.dpk,然後「開啟舊檔」
  8. 再點「Add…」按鈕,選擇 \Protocols\dclIndyProtocols70.dpk,然後「開啟舊檔」
  9. P.S. 「SuperCore」可以完全忽視,不用安裝

我照這個步驟的成功率目前是百分百,提供給大家試試看

參照網址http://www.indyproject.org/Sockets/Docs/Indy10Installation.EN.aspx

許多初學者在使用SQL Server時都會遇到使用SQL Server Management Studio無法連接遠端資料庫實例的問題,大致的錯誤描述如下:

An error has occurred while establishing a connection to the server.

(provider: Named Pipes Provider, error: 40 – Could not open a connection to SQL Server) (Microsoft SQL Server, Error: 5)

An error has occurred while establishing a connection to the server.  When connecting to SQL Server 2005, this failure may be caused by the fact that under the default settings SQL Server does not allow remote connections. (provider: Named Pipes Provider, error: 40 – Could not open a connection to SQL Server) (Microsoft SQL Server, Error: 1326)

意思是說不能在資料庫之間建立一個連接,原因是具名管道提供者出現錯誤。其實這是一個比較典型的資料庫伺服器設置問題,在局域網或廣域網路中都可能會遇到,我們只需要對資料庫伺服器進行一些配置便可以解決這個問題,來看看具體的步驟。

 

確保伺服器端資料庫服務已經啟動
開始->所有程式->Microsoft SQL Server 2008->Configutation Tools,打開SQL Server Configuration Manager,點擊SQL Server Services,查看資料庫服務是否已經啟動,如果服務未開啟,手動啟動它。當然,你還可以通過點擊Windows中的開始->控制台->管理者工具->服務,來查看相應的資料庫服務是否啟動。或者如果伺服器和你的機器在同一網路,你還可以通過命令「sqlcmd -L」(注意L要大寫)去查看該網路內所有可用的SQL Server伺服器。

103A24507-0

在SQL Server Configuration中啟用TCP/IP
多個SQL Server伺服器之間通過網路相互通信是需要TCP/IP支援的,為使SQL Server伺服器能被遠端連線必須確保TCP/IP已經啟用。按照前面介紹的步驟打開SQL Server Configuration Manager,然後打開SQL Server Network Configuration,選擇你要設置的資料庫,然後點擊TCP/IP,右鍵啟用。如果有必要,你還可以啟用Named Pipes。記住,所有的修改都必須在重啟SQL Server服務之後才能生效!

103A25093-1

在Windows防火牆中打開SQL Server的埠號
很多時候我們在對資料庫伺服器本身做了很多次設置後仍然無法成功建立遠端連線,這時就要考慮是否是防火牆在作怪。預設情況下,許多埠號和服務都會被防火牆所禁止而不能遠端存取或執行,SQL Server預設的埠號也不例外。我們應該重新設置Windows防火牆給SQL Server添加例外。除非人為修改,預設情況下SQL Server的埠號是1433,將該埠號添加到Windows防火牆中。如果SQL Server存在命名實例,那麼也應該將SQL Server browser添加到Windows防火牆中。(有關SQL Server的命名實例將在後面介紹)
打開Windows控制台,選擇Windows防火牆->Change Settings->Exceptions->Add Port

103A24096-2

103A2A29-3

點擊Add port…在彈出的對話方塊中填入:
Name: SQL
Port Number: 1433
Protocol: Select TCP

103A21P3-4
103A260Z-5

在SQL Server管理器中啟用遠端連線
這一步通常會被資料庫管理員忽略,如果未啟用資料庫遠端連線,資料庫實例只允許在本地進行連接而不能被遠端連線,啟用遠端連線同樣非常重要。預設設置中遠端連線是被禁止的。如下圖,打開SQL Server Management Studio,右鍵點擊資料庫實例然後選擇屬性功能表。

103A22331-6

在打開的視窗中,左側選擇Connections,然後勾選"Allow remote connections to this server"。

103A220K-7

啟用SQL Server Browser服務
如果SQL Server在安裝時不是用的預設實例名而是自訂的實例名,並且沒有配置特定的TCP/IP埠號,那麼按照我們前面的描述SQL Server仍然不能支援遠端連線。但如果你啟用的SQL Server Browser服務,便可以通過動態TCP/IP埠號進行遠端SQL Server連接。啟用SQL Server Browser服務非常簡單,與啟用SQL Server類似,在SQL Server Configuration Manager中右鍵點擊SQL Server Browser,然後選擇啟用。啟用該服務將會影響到伺服器上所有已安裝的SQL Server實例。

103A22541-8

在防火牆中為sqlbrowser.exe應用程式創建例外
我們在前面已經提到了,自訂命名的SQL Server實例要支援遠端連線需要啟用sqlbrowser服務,Windows防火牆可能會阻止該服務執行。因此,我們必須在Windows防火牆中給sqlbrowser服務添加例外。
首先找到伺服器上安裝sqlbrowser.exe程式的路徑,如C:\Program Files\Microsoft SQL Server\90\Shared\sqlbrowser.exe。如果不確定SQL Server安裝在什麼地方,你可以在Windows搜索一下檔案名。與我們在前面介紹的在防火牆中添加SQL TCP/IP埠號的方法類似,給sqlbrowser.exe應用程式添加防火牆例外。

 

重新創建資料庫別名
創建SQL Server別名並在應用程式中使用它很常見。使用資料庫別名可以確保一旦資料庫的位置發生了變化,如更換了新的伺服器,IP位址發生了變化等,應用程式中的資料庫連接字串不用修改。否則你更換了資料庫的位置,你還要通知所有使用該資料庫的應用程式修改原始程式碼或設定檔中的連接字串,這恐怕是不可能的。所以,使用資料庫別名來配置連接字串是一個非常明智的選擇。另外,你還可以使用相同的別名來指向不同的資料庫實例,當修改別名參數時,可以馬上實現資料庫之間的切換。創建資料庫別名非常簡單,在SQL Server Configuration Manager中選擇Aliases進行創建。
103A23S8-9

CompareDate
函数 比较两个日期时间值日期部分的大小 
CompareDateTime
函数 比较两个日期时间值的大小 
CompareTime
函数 比较两个日期时间值时间部分的大小 
DateOf
函数 去除日期时间值的时间部分 
DateTimeToJulianDate
函数 转换日期时间值为儒略日 
DateTimeToModifiedJulianDate
函数 转换日期时间值为改进的儒略日 
DateTimeToUnix
函数 转换日期时间值为Unix/Linus日期时间值 
Day of week 函数 常量 *ISO 8601标准中一周各天顺序的 常量 
DayOf
函数 返回一个日期时间值的天 
DayOfTheMonth
函数 返回一个日期时间值的天 
DayOfTheWeek
函数 返回一个日期时间值是那星期的第几天 
DayOfTheYear
函数 返回一个日期时间值是那年的第多少天 
DaysBetween
函数 返回两个日期时间值之间相差的整数天数 
DaysInAMonth
函数 返回指定年、月的天数 
DaysInAYear
函数 返回指定年的天数 
DaysInMonth
函数 返回一个日期时间值的那个月的天数 
DaysInYear
函数 返回一个日期时间值的那一年的天数 
DaySpan
函数 返回两个日期时间值之间相差的小数天数 
DecodeDateDay 过程 返回一个日期时间值的年份和是一年的第多少天 
DecodeDateMonthWeek 过程 返回一个日期时间值的年、月、那个月的第几周、那周的第几天 
DecodeDateTime 过程 返回一个日期时间值的年、月、日、时、分、秒、毫秒 
DecodeDateWeek 过程 返回一个日期时间值的年、一年的第多少周、一周的第几天 
HourODecodeDayOfWeekInMonth 过程 返回一个日期时间值的年、月、一周的第几天、那个月的第几个星期几 
EncodeDateDay
函数 返回指定年和一年的第多少天的日期时间值 
EncodeDateMonthWeek
函数 返回指定年、月、那个月的第几周、那周的第几天的日期时间值 
EncodeDateTime
函数 返回指定年、月、日、时、分、秒,毫秒返的日期时间值 
EncodeDateWeek
函数 返回指定年、那年的第多少周、那周的第几天的日期时间值 
EncodeDayOfWeekInMonth
函数 返回指定年、月、那个月的第几个星期几的日期时间值 
EndOfADay
函数 返回指定年、那年第多少天的最后一秒的日期时间值 
EndOfAMonth
函数 返回指定年、月的最后一天最后一秒的日期时间值 
EndOfAWeek
函数 返回指定年、那年第多少周、那周第几天的最后一秒的日期时间值 
EndOfAYear
函数 返回指定年的最后一天最后一秒的日期时间值 
EndOfTheDay
函数 返回指定日期时间值的那一天最后一秒的日期时间值 
EndOfTheMonth
函数 返回指定日期时间值的那个月的最后一天最后一秒的日期时间值 
EndOfTheWeek
函数 返回指定日期时间值的那一周的最后一天最后一秒的日期时间值 
EndOfTheYear
函数 返回指定日期时间值的那一年最后一天最后一秒的日期时间值 
HourOf
函数 返回指定日期时间值的小时部分 
HourOfTheDay
函数 返回指定日期时间值的小时部分. 
HourOfTheMonth
函数 返回从指定日期时间值的那个月的第一天0点到指定日期的小时已经度过的小时数 
HourOfTheWeek
函数 返回从指定日期时间值中那一周第一天0点到指定日期的那个小时已经度过的小时数 
fTheYear
函数 返回从指定日期时间值中那一年第一天0点到指定日期的那个小时已经度过的小时数 
HoursBetween
函数 返回两个指定日期时间值之间相差的小时数 
HourSpan
函数 返回两个指定日期时间值之间相差的小时数(包括小数部分) 
IncDay
函数 返回日期时间值向后推移指定天数后的值 
IncHour
函数 返回日期时间值向后推移指定小时数的值 
IncMilliSecond
函数 返回日期时间值向后推移指定毫秒数的值 
IncMinute
函数 返回日期时间值向后推移指定分钟数的值 
IncSecond
函数 返回日期时间值向后推移指定秒数的值 
IncWeek
函数 返回日期时间值向后推移指定星期数的值 
IncYear
函数 返回日期时间值向后推移指定星期数的值 
IsInLeapYear
函数 判断指定的日期时间值的年份是否为闰年 
IsPM
函数 判断指定的日期时间值的时间是否是中午12:0:0之后 
IsSameDay
函数 判断一个日期时间值与标准日期时间值是否是同一天 
IsToday
函数 判断一个日期时间值是否是当天 
IsValidDate
函数 判断指定的年、月、日是否是有效的日期 
IsValidDateDay
函数 判断指定的年、该年的天数是否是该年有效的天数 
IsValidDateMonthWeek
函数 判断指定的年、月、该月的第几周、该周的第几天是否是有效的日期 
IsValidDateTime
函数 判断指定的年、月、日、时、分、秒、毫秒是否是有效的日期时间值 
IsValidDateWeek
函数 判断指定的年、该年的第多少周、该周第几天是否是有效的日期 
IsValidTime
函数 判断指定的时、分、秒、毫秒是否是有效的时间 
JulianDateToDateTime
函数 转换儒略日期为日期时间值 
MilliSecondOf
函数 返回指定日期时间值的毫秒部分 
MilliSecondOfTheDay
函数 返回指定日期时间值的那天0时0分0秒0毫秒开始到其指定时间的毫秒数 
MilliSecondOfTheHour
函数 返回指定日期时间值的那一小时0分0秒0毫秒开始到其指定时间的毫秒数 
MilliSecondOfTheMinute
函数 返回指定日期时间值的那一分钟0秒0毫秒开始到其指定时间的毫秒数 
MilliSecondOfTheMonth
函数 返回指定日期时间值的那个月1日分钟0秒0毫秒开始到其指定时间的毫秒数 
MilliSecondOfTheSecond
函数 返回指定日期时间值的毫秒部分 
MilliSecondOfTheWeek
函数 返回指定日期时间值的那周星期一0时0分0秒0毫秒到其指定时间的毫秒数 
MilliSecondOfTheYear
函数 返回指定日期时间值的那年1月1日0时0分0秒0毫秒到其指定时间的毫秒数 
MilliSecondsBetween
函数 返回两个指定日期时间值之间相差的毫秒数(整数) 
MilliSecondSpan
函数 返回两个指定日期时间值 之间相差的毫秒数(小数) 
MinuteOf
函数 返回指定日期时间值 分钟部分 
MinuteOfTheDay
函数 返回指定日期时间值的那天0时0分开始到其指定时间的分钟数 
MinuteOfTheHour
函数 返回指定日期时间值的分钟部分 
MinuteOfTheMonth
函数 返回指定日期时间值的那个月1日0时0分开始到其指定时间的分钟数 
MinuteOfTheWeek
函数 返回指定日期时间值的那周第1天0时0分开始到其指定时间的分钟数 
MinuteOfTheYear
函数 返回指定日期时间值的那年1月1日0时0分开始到其指定时间的分钟数 
MinutesBetween
函数 返回两个指定日期时间值之间相差的分钟数(整数) 
MinuteSpan
函数 返回两个指定日期时间值之间相差的分钟数(包含小数) 
ModifiedJulianDateToDateTime
函数 转换修正的儒略日为日期时间值 
MonthOf
函数 返回指定日期时间值的月份部分 
MonthOfTheYear
函数 返回指定日期时间值的月份部分 
MonthsBetween
函数 返回两个指定日期时间值之间相差的月份(整数) 
MonthSpan
函数 返回两个指定日期时间值之间相差的月份(包含小数) 
NthDayOfWeek
函数 返回指定日期时间值该月的第几个星期几 
OneHour 常量 Delphi与时间成反比的常量 
OneMillisecond 常量 Delphi与时间成反比的常量 
OneMinute 常量 Delphi与时间成反比的常量 
OneSecond
常量 Delphi与时间成反比的常量 
RecodeDate
函数 替换指定日期时间值的日期部分 
RecodeDateTime
函数 选择替换指定日期时间值 
RecodeDay
函数 替换指定日期时间值 的日部分 
RecodeHour
函数 替换指定日期时间值 的小时部分 
RecodeMilliSecond
函数 替换指定日期时间值的毫秒部分 
RecodeMinute
函数 替换指定日期时间值的分钟部分 
RecodeMonth
函数 替换指定日期时间值的月份部分 
RecodeSecond
函数 替换指定日期时间值的秒部分 
RecodeTime
函数 替换指定日期时间值的时间部分 
RecodeYear
函数 替换指定日期时间值的年份部分 
SameDate
函数 判断两个日期时间值的年、月、日部分是否相同 
SameDateTime
函数 判断两个日期时间值的年、月、日、时、分、秒、毫秒是否相同 
SameTime
函数 判断两个日期时间值的时、分、秒、毫秒部分是否相同 
SecondOf
函数 返回指定日期时间值的秒部分 
SecondOfTheDay
函数 返回从指定日期时间值那天0时0分0秒到其指定时间的秒数 
SecondOfTheHour
函数 返回从指定日期时间值那小时0分0秒到其指定时间的秒数 
SecondOfTheMinute
函数 返回从指定日期时间值那分钟0秒到其指定时间的秒数 
SecondOfTheMonth
函数 返回从指定日期时间值那个月1日0时0分0秒到其指定时间的秒数 
SecondOfTheWeek
函数 返回从指定日期时间值所在周的星期一0时0分0秒到其指定时间的秒数 
SecondOfTheYear
函数 返回从指定日期时间值那年的1月1日0时0分0秒到其指定时间的秒数 
SecondsBetween
函数 返回两个指定日期时间值之间相差的秒数(整数) 
SecondSpan
函数 返回两个指定日期时间值之间相差的秒数(包含小数) 
StartOfADay
函数 返回指定那天开始(0时0分0秒0毫秒)的日期时间值 
StartOfAMonth
函数 返回指定年、月的第一天开始(0时0分0秒0毫秒)的日期时间值 
StartOfAWeek
函数 返回指定年、第多少周、第几天开始(0时0分0秒0毫秒)的日期时间值 
StartOfAYear
函数 返回指定年开始(1月1日0时0分0秒0毫秒)的日期时间值 
StartOfTheDay
函数 返回指定日期时间值那天开始(0时0分0秒0毫秒)的日期时间值 
StartOfTheMonth
函数 返回指定日期时间值那个月开始(1日0时0分0秒0毫秒)的日期时间值 
StartOfTheWeek
函数 返回指定日期时间值那周开始(第一天0时0分0秒0毫秒)的日期时间值 
StartOfTheYear
函数 返回指定日期时间值那年开始(1月1日0时0分0秒0毫秒)的日期时间值 
TimeOf
函数 返回指定日期时间值的时间部分 
Today
函数 返回当天的日期 
Tomorrow
函数 返回下一天的日期 
TryEncodeDateDay
函数 计算指定年、该年第多少天的日期时间值 
TryEncodeDateMonthWeek
函数 计算指定年、月、该月第几周、该周第几天的日期时间值 
TryEncodeDateTime
函数 转换指定年、月、日、时、分、秒、毫秒为日期时间值 
TryEncodeDateWeek
函数 转换指定年、该第多少周、该周第几天为日期时间值 
TryEncodeDayOfWeekInMonth
函数 转换指定年、月、该月第几个星期几为日期时间值 
TryJulianDateToDateTime
函数 转换指定儒略日为日期时间值 
TryModifiedJulianDateToDateTime
函数 转换指定修正儒略日为日期时间值 
TryRecodeDateTime
函数 选择替换指定日期时间值的某些部分 
UnixToDateTime
函数 转换Unix或Linux日期、时间值为Delphi日期时间值 
WeekOf
函数 返回指定日期时间值是某年的第多少周 
WeekOfTheMonth
函数 返回指定日期时间值是某月的第 几周 
WeekOfTheYear
函数 返回指定日期时间值是某年的第多少周 
WeeksBetween
函数 返回两个指定日期时间值 之间相差多少周(整数) 
WeeksInAYear
函数 返回指定的年有多少周 
WeeksInYear
函数 返回指定日期时间值的那年有多少周 
WeekSpan
函数 返回两个指定日期时间值之间相差多少周(包含小数) 
WithinPastDays
函数 判断两个日期之间相差 是否在指定天数的范围内 
WithinPastHours
函数 判断两个日期时间值之间相差是否在指定小时的范围内 
WithinPastMilliSeconds
函数 判断两个日期时间值之间相差是否在指定毫秒的范围内 
WithinPastMinutes
函数 判断两个日期时间值之间相差是否在指定分钟的范围内 
WithinPastMonths
函数 判断两个日期时间值之间相差是否在指定月份的范围内 
WithinPastSeconds
函数 判断两个日期时间值之间相差是否在指定秒数的范围内 
WithinPastWeeks
函数 判断两个日期时间值之间相差是否在指定星期数的范围内 
WithinPastYears
函数 判断两个日期时间值之间相差是否在指定年数的范围内 
YearOf
函数 返回指定日期时间值中年份部分 
YearsBetween
函数 返回两个指定日期时间值之间相差的年份数(整数) 
YearSpan
函数 返回两个指定日期时间值之间相差的年份数(包含小数) 
Yesterday
函数 返回当前日期之前一天(昨天)的日期