0%

我们来回顾一下Action-Value的定义:某个动作a的价值$q*$定义为采取该动作后收到的预期奖励:

由于$q*(a)$是未知的,所以我们需要预估它。

样本平均法

样本平均法通过计算动作奖励(reward)的均值来预估该动作的value:

说明:我们使用$t-1$是因为我们计算value的时间t是基于t时刻之前产生的动作来预估的。
另外如果我们尚未执行动作A,那么我们需要对该动作的value设置为一个默认值(比如0)

回到我们的医学实验的例子中,医生需要决定使用3种药物中的哪一种,如果经过治疗后,病人变得更好,医生会记录奖励值为1,否则记录奖励值为0。

随着试验的进行,我们估计的q值会逐渐接近真正的action value。

贪婪策略

有了对q值合理的估计后,医生就不会随机的给患者分配治疗方案了,相反,医生会开始选择目前为止认为最好的治疗方案给到患者。我们称这种选择action的方式为贪婪策略

贪婪策略选取动作的方案是选择当前具有最大估计值的动作。选择贪婪策略也就意味着agent正在利用其当前所学到的知识(agent正在努力获得最大的奖励)。

我们可以通过argmax来计算贪婪策略的行为。

探索开发困境

agent也可以选择非贪婪动作来做探索,此时,agent会牺牲掉暂时的奖励来换取更多的关于其他动作的信息。

agent不能同时选择探索(explore)和利用(exploit),这是强化学习的基本问题之一。这就是探索开发困境。

增量式预估Action Value

假设我们现在维护了一个每日都有百万访问量的网站。在这个网站下投放的广告可以想象成是一个k臂老虎机问题。我们希望找出产生收益最多的广告。如果我们把所有历史数据存储下来,采用样本平均法来预估广告的value固然可行,但在如此庞大的访问量下,数据的存储和计算可能是一个比较明显的问题。如何在不存储数据的情况下保持最新的action value的估算值呢?如何以增量的方式来预估action value是我们需要考虑的问题。

在样本平均法中,我们的q值更新规则可以以递归的方式来编写(这样一来,我们就可以避免存储历史数据):

由于

所以上面的式子可以改写为:

综上,我们可以得到:

这个表达式是一种形式,后面还将出现很多次。这种形式的抽象表述如下:

我们有必要对这其中的一些术语进行说明。

  • 估计值误差:指的是$(Target - OldEstimate)$,即旧估计值与新目标之间的差值。
  • 目标值(Target):指的是$R_n$,即新的奖励。
  • 步长(StepSize):指的是$\frac{1}{n}$的部分,即目标值朝着真实值的方向更新一步的步长大小。

我们用$α_n$来表示步长,其范围是$[0,1]$:

在均值采样的场景下,我们的步长为:

我们再回到医疗试验的例子中。如果随着试验的进行,某种药物的效果发生了变化,我们该如何应对?

假设我们让B药物现在的治愈率提升到0.9:

此时,这个问题就变成了非固定老虎机问题了,奖惩的分配随着时间的变化而发生了变化,而这一变化医生并不知道,但是我们希望算法能适应于这种变化。

解决这一问题的一种方式是使用固定步长大小

历史奖励衰减

假设$α_n=0.1$是一个恒定的值,那么最近的奖励对估计的影响将大于旧的奖励。

此图表显示了最近一次获得的奖励与T时间步长前收到的奖励的比例。加权随着时间的推移呈指数级淡化。

由于

所以

我们可以依次展开递归表达式,得到:

这里$Q1$是初始的action value,$Q{n+1}$最终等于初始的$Q_1$加上随着时间推移而变化的奖励加权总和。

这个等式告诉我们:

  • $Q_1$的贡献随着时间的推移呈指数级减少
  • 在过去的时间里,reward对总和的贡献是呈指数级变化的。

我们看到随着时间的推移,初始的Q值对结果的影响越来越小,最近的奖励对我们的下一次估计影响最大。

在强化学习中,代理人通过与世界互动来生成自己的训练数据。 代理人必须通过试验和错误了解自己行为的后果,而不是被告知正确的行动。

我们使用K臂老虎机(K-Armed Bandit)来描述强化学习中的基本概念,比如rewards、timesteps和values。

想象一下,有一名医生想要测量3种药物的效果:

医生会随机开始一次治疗,然后观测患者的反映情况。

过了一段时间后,医生发现其中某种药物似乎比其他药物效果要更好:

医生现在必须决定是否坚持最佳治疗继续进行随机研究。如果医生只使用一种药物治疗, 那么他们就不能再收集另外两种药物治疗的数据(也许其他治疗方法之一实际上是更好的, 只是由于偶然而变得更糟)。 但如果其他两种治疗方法更糟, 那么继续研究会危及其他患者的健康。

这种医疗试验的本质就是一个K臂老虎机的案例。

K臂老虎机

在K臂老虎机问题中,我们有一个决策者(代理)负责在k个不同的行动之间进行选择,并根据他选择的行动获得奖励。

医疗试验的例子中,代理人就是医生。医生必须在3种不同的治疗行动之间进行选择,选择不同的治疗会产生一些未知的回报,最后患者是否得到治疗就是医生获得的奖励。

Action-Values

为了让医生决定哪个动作是最好的,我们必须定义每个动作的价值(Action-Values)。

我们对动作价值的定义是奖励的期望

说明: 符号的意思是 定义为
上面公式的含义是:$q*(a)$被定义为$R_t$的期望,$R_t$是我们在选中某个动作的时候的奖惩值。

这个条件期望被定义为所有可能的奖励的总和。在该总和中,我们将可能的奖励($r$)乘以观察到该奖励的概率($p(r|a)$)。

代理(agent)的目标就是最大限度的提高预期的回报

代理选择最大的value的操作的过程称为argmax:

连续型的reward

在上面的例子中,我们把患者是否治愈作为reward,下面我们用一个更容易测量的指标来衡量患者的状态:接受治疗后血压的变化

在每种不同的药物治疗之后,患者的血压会呈现出不同的概率分布,也许是伯努利分布(结果1)、二项式分布(结果2)、均值分布中的某一种(结果3):

$q*(a)$是a动作的reward的均值

为什么要研究老虎机问题

现实生活中,我们随时都需要做出各种各样的决策,除了医生对患者选择最佳药物的例子外,我们决定去看什么电影、听什么歌曲、即便是在餐厅点菜 我们都在做着类似的选择。

老虎机是对未知环境下动作选择问题的简单抽象,在这种简单的问题下去思考算法的设计会使问题更加清晰。

隐马尔可夫模型

解决时序性的预测问题,我们通常会用到HMM模型(隐马尔科夫模型),但在开始介绍HMM模型之前,我们有必要先了解它的前置知识:

  • 马尔科夫链
  • 马尔科夫模型

马尔科夫链

现有状态只和上一个状态有关,而未来状态只与现有状态有关:

满足这样的一种结构,我们就称之为马尔科夫链

如果一个系统,在做不同状态之间转换的时候,当前状态只受到过的一个状态的影响,和其他状态都没有关系;换句话说,这个系统内的未来的一个状态只受到当前状态的影响,和其他状态都无关,满足这样性质的一个系统,我们就称之为是一个具有马尔科夫性的系统。

一个马尔科夫链,需要具备以下参数:

  • 初始分布
  • 转移概率和转移概率矩阵

初始分布

其中,初始分布如下:

这个公式的含义是在一个系统内,各个状态初始时的概率分布情况。其中$\pi$是概率分布的意思。举例说明:

例如:假设一个系统内只有1、2、3这三种状态,其中状态1出现的概率为0.2,状态2出现的概率是0.3,状态3出现的概率是0.5。这三个概率即初始概率分布。

转移概率和转移概率矩阵

继续上面的例子。当我们处于状态1时,下一个状态可能是状态2,可能是状态3,也可能是状态1。如果状态1变为状态2的概率是0.2,状态1变为状态3的概率是0.3,状态1变为状态1的概率是0.5:

这里的概率就是转移概率

对于某一个状态来说,具有的转移概率一共有3个,那么对于3种状态来说一共有3x3=9个转移概率。我们可以用一个3x3的矩阵来表示。这个矩阵就被称为转移概率矩阵


一个具体的例子,愚蠢的顾客

  • 某同类物品A、B、C的宣传力度不同,愚蠢的顾客在广告宣传的效应下,第一次尝试选择购买A、B、C的概率为0.2,0.4,0.4。经零售商统计,顾客的购买倾向为下表,尝试求某顾客第四次来购买各物品的概率:

在这个例子中,第一次购买A、B、C的概率0.2,0.4,0.4就是初始分布,上面的那个表就是状态转移矩阵

可观测的马尔科夫模型

上面的描述都是关于马尔科夫链的,那么什么是马尔科夫模型呢?

  • 对于一个问题而言,我们有初始分布$\pi$,转移概率矩阵A,在给定的任意一个时刻t,我们都有一个状态$q_t$,随着时间的变化,一个状态转移到另一个状态,我们便能得到一个观测序列,即为状态序列$O=[q_1,q_2,q_3,q_4,…,q_m]$。而且整个问题中一共有n个观测状态。

  • 出现这样的序列的概率为:

所以一个可观测的马尔科夫模型由一个三元组描述:$(A,\pi,n)$一般情况下简写为$(A,\pi)$。(因为观测状态的数量n可以从状态概率分布$\pi$得出)

这里的$A$就是转移概率矩阵,$\pi$就是状态初始分布,$n$就是观测状态的数量。

举个例子:

  • 有一个抽屉,抽屉里放有三种颜色的球,颜色分别为红蓝绿。某人随机的将球一个一个从抽屉中取出,球的颜色依次构成序列(C1,C2,C3,…)。如果红、蓝、绿三个状态的初始分布为$\pi=(0.5,0.2,0.3)$,转移概率矩阵:
  • 那么出现颜色序列为:红,红,绿,绿 的概率是多少?

解答:

初始概率分布$\pi=(0.5,0.2,0.3)$,可以看出,初始为红色的概率为0.5。

从状态转移矩阵可以看出,各个颜色变化的概率分布:

绿
0.4 0.3 0.3
0.2 0.6 0.2
绿 0.1 0.1 0.8

可以看出,红色转移到红色的概率为0.4,红色转移到绿色的概率是0.3,绿色转移到绿色的概率是0.8。所以最终出现序列红,红,绿,绿 的概率为:

所以上面的那个公式就是将所求的序列O之中的各个状态的转移概率连乘起来得到的最终概率:

问题来了

那么我们能不能在只知道观测序列的情况下,得知初始分布和转移概率矩阵呢?

解答:

如果我们穷举了所有的观测序列,那么:

具体实例,比如我们从抽屉中抽小球,四次的观测序列如下:

  • [红,红,红]
  • [红,红,蓝]
  • [红,蓝,红]
  • [蓝,红,红]

可以得出初始分布为:

转移概率为:

隐马尔科夫模型(Hidden Markov Model)

介绍完了上面的可观测的马尔科夫模型,接下来介绍隐马尔科夫模型

隐马尔科夫模型的基本想法是:系统的状态S无法观测,但我们可以观测到某个其他和状态关联的事物,这个事物出现是伴随系统状态而出现的。

为什么会有无法观测的情况呢?举个例子:观测天空是否在下雨这个现象可以通过观测苔藓的生长情况来判断。比如下雨天,苔藓生长比较茂盛。所以我们可以通过观察苔藓来判断下雨的概率是否大。

一个隐马尔科夫模型一般包含以下参数组成:

  • 观测集合:$R=\{R_1,R_2,R_3,R_4,…,R_m\}$
    • 代表我们能观测到的状态有哪些,比如抓小球的例子中就是红蓝绿三种颜色。
  • 观测序列:$O=[o_1,o_2,o_3,o_4,…,o_l]$
    • 代表我们能观测到的具体的观测序列
  • 状态集合:$S=\{S_1,S_2,S_3,S_4,…,S_n\}$
    • 代表状态的集合,比如上面的下雨天的例子中,状态就是晴天、雨天
  • 状态序列:$Q=[q_1,q_2,q_3,q_4,…,q_l]$
    • 就是出现某些状态的序列
  • 观测概率:$P\{o_i=R_k|q_t=S_j\}=b_j(i)$,记$B=[b_j(i)]$
    • 观测概率是隐马尔科夫模型特有的,在$t$时刻的时候,出现状态$q$,观测到状态$o_i$为指定状态$R_k$的概率。

所以,隐马尔可夫模型由一个五元组来描述$(A,B,\pi,R,S)$,一般情况下,可以简化为$(A,B,\pi)$。其中$A$是状态转移矩阵,$B$是观测概率,$\pi$是初始分布。

注意

  • 不同的状态序列可以产生相同的观测序列(以不同的概率产生)
  • 状态转移是随机的,系统在一个状态中产生的观测也是随机的
  • 可观测马尔科夫模型是隐马尔科夫模型的特例:当$m=n$,如果$i=j,b_j(i)=1$否则$b_j(i)=0$。
    • 即在马尔科夫模型下,状态序列和观测序列是一样的。

三个基本问题

隐马尔可夫模型一般可以用来解决三个基本问题:

  • (1)估计:已知模型$(A,B,\pi)$,求观测序列出现的概率
    • 解决方法:前后向算法
  • (2)预测:已知模型$(A,B,\pi)$和一个观测序列,求对应的不可观测的状态序列
    • 解决方法:Viterbi算法
  • (3)学习:已知一组观测序列,求模型$(A,B,\pi)$
    • 解决方法:Baum-Welch算法

下面用一个具体的例子来了解一下这三个基本问题是如何处理的。

股市预测

  • 如果股市只有三种状态:牛市、熊市、普通
  • 而且股票只有三种趋势:涨、跌、不变
  • 如何利用隐马尔可夫模型进行股市预测?

那么如果在股市预测问题中,应用隐马尔科夫模型,来解决上面的那三个对应的基本问题,分别如下:

  • (1)已知模型,求观测到连续一周出现涨势的概率
  • (2)已知模型,观察到一周的变化情况为:涨、不变、涨、不变、跌,问股市的状态变化情况?
  • (3)观察到股市一周的变化情况为:涨、不变、涨、不变、跌,求下周一开盘时的涨跌情况?

隐马尔科夫模型的代码如下hmm.py

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
from __future__ import division
import math
import random

class HMM:
"""Class to implement an HMM.
Defined by:
1. Hidden state transition probability matrix T
2. Observable emission probability matrix E
3. Prior probability matrix 'priors'
4. Vocabulary of possible hidden states M ('states')
5. Vocabulary of possible observable emissions V ('emissions')
"""
def __init__(self, states, emissions):
self._states = states
self._emissions = emissions
self._T = dict()
self._E = dict()
self._priors = dict()
#print "Don't forget to set: T, E, and priors..."

def set_T(self, new_T):
tf = True
for key in new_T:
if sorted(new_T[key].keys()) != sorted(self._states):
tf = False
if sorted(new_T.keys()) != sorted(self._states):
tf = False

if tf: self._T = new_T
else:
print """Unmatched key -- check dictionary!
T => T[to state][given state]
"""

def set_E(self, new_E):
tf = True
for key in new_E:
if sorted(new_E[key].keys()) != sorted(self._states):
tf = False
if sorted(new_E.keys()) != sorted(self._emissions):
tf = False

if tf: self._E = new_E
else:
print """Unmatched key -- check dictionary!
E => E[emission][given state]
"""

def set_priors(self, new_priors):
if sorted(new_priors.keys()) == sorted(self._states):
self._priors = new_priors
else:
print """Unmatched key -- check dictionary!
priors => priors[state]
"""

# **************************************************
# Functions that take an observation sequence and an HMM
# **************************************************

def forward(O, hmm):
"""Return trellis representing p(theta_t | O_1^t).
"""
# initialize local variables
n = len(O)
f = {state: list() for state in hmm._states}
for o in O:
for state in hmm._states:
f[state].append(0)

# construct forward trellis
for state in hmm._states:
f[state][0] = hmm._priors[state] * hmm._E[O[0]][state]
for t in range(1, n):
for j in hmm._states:
for i in hmm._states:
f[j][t] += f[i][t-1] * hmm._T[j][i] * hmm._E[O[t]][j]
return f

def backward(O, hmm):
"""Return trellis representing p(O_t+1^N | theta_t == i).
"""
# initialize local variables
n = len(O)
b = {state: list() for state in hmm._states}
for o in O:
for state in hmm._states:
b[state].append(0)

# construct backward trellis
for state in hmm._states:
b[state][n-1] = 1
for t in range(n-2, -1, -1):
for i in hmm._states:
for j in hmm._states:
b[i][t] += b[j][t+1] * hmm._T[j][i] * hmm._E[O[t+1]][j]
return b

def posterior(O, hmm):
"""Return trellis representing p(theta_t | O).
Posterior probabilities would be used to find the maximum likelihood
of a state at a given time step based on the observation sequence
O. The value returned by the forward algorithm is the O_prob
value returned here.
"""
# get n
n = len(O)
# initialize forward, backward, and posterior trellises
f = forward(O, hmm)
b = backward(O, hmm)
p = {state: list() for state in hmm._states}
for o in O:
for state in hmm._states:
p[state].append(0)

# total probability of sequence O
O_prob = math.fsum([f[state][n-1] for state in hmm._states])

# build posterior trellis
for state in hmm._states:
for t in range(n):
p[state][t] = (f[state][t] * b[state][t]) / O_prob

return p

def forward_algrithm(O, hmm):
f = forward(O, hmm)
prop = 0.0
for stat in hmm._T:
prop += f[stat][len(O)-1]
return

def viterbi_path(O, hmm):
"""Return most likely hidden state path given observation sequence O.
"""
n = len(O)
u = {state: list() for state in hmm._states}
v = {state: list() for state in hmm._states}
bt = list()
for o in O:
for state in hmm._states:
for t in (u, v):
u[state].append(0)
v[state].append(str())
bt.append(str())

for state in hmm._states:
u[state][0] = hmm._priors[state] * hmm._E[O[0]][state]
# v[state][0] not of interest
for t in range(1, n):
for j in hmm._states:
for i in hmm._states:
p = u[i][t-1] * hmm._T[j][i] * hmm._E[O[t]][j]
if p > u[j][t]:
u[j][t] = p
v[j][t] = i
p = 0
for state in hmm._states:
if u[state][n-1] > p:
p = u[state][n-1]
bt[n-1] = state
for t in range(n-2, -1, -1):
bt[t] = v[bt[t+1]][t+1]

return bt

def baum_welch(O, hmm):
"""Return new hmm from one iteration of re-estimation."""
n = len(O)
f = forward(O, hmm)
b = backward(O, hmm)
p = posterior(O, hmm)
E_prime = dict()
for emission in hmm._emissions: E_prime[emission] = dict()
T_prime = dict()
for state in hmm._states: T_prime[state] = dict()
priors_prime = dict()

# construct E_prime
for state in hmm._states:
den = math.fsum([p[state][t] for t in range(n)])
for emission in hmm._emissions:
v = 0
for t in range(n):
if O[t] == emission: v += p[state][t]
E_prime[emission][state] = v / den

# construct T_prime
p_O = math.fsum([f[s][n-1] for s in hmm._states])
for given in hmm._states:
den = math.fsum([p[given][t] for t in range(n)])
for to in hmm._states:
v = 0
for t in range(1, n):
v += ( f[given][t-1] *
b[to][t] *
hmm._T[to][given] *
hmm._E[O[t]][to]
) / p_O
T_prime[to][given] = v / den

# construct priors_prime
for state in hmm._states:
priors_prime[state] = p[state][0]

new_hmm = HMM(hmm._states, hmm._emissions)
new_hmm.set_E(E_prime)
new_hmm.set_T(T_prime)
new_hmm.set_priors(priors_prime)

return new_hmm

下面我们来用这个模型解决这三个问题。

估计问题:已知模型,求观测到连续一周出现涨势的概率

已知模型如下图所示:

某股民根据经验判断当前为牛市、熊市、普通的概率分别是0.4、0.3、0.3。

这个问题中,我们的观测状态集合为:牛市、熊市、普通

我们的发射状态集合为:涨、跌、不变

初始分布$\pi$为:$(0.4, 0.3, 0.3)$

转移概率矩阵A:$
A=\begin{pmatrix}
0.6 & 0.2 & 0.2 \\
0.5 & 0.3 & 0.2 \\
0.4 & 0.1 & 0.5
\end{pmatrix}
$

观测概率矩阵B:$
A=\begin{pmatrix}
0.7 & 0.1 & 0.2 \\
0.1 & 0.6 & 0.3 \\
0.3 & 0.3 & 0.4
\end{pmatrix}
$

求连续一周出现涨势的概率,我们应该使用前后向算法forward_algrithm()。改算法接收一个hmm模型。我们在初始化hmm模型的时候,需要设置观测状态states、发射状态emissions,转移概率矩阵set_T(),发射概率矩阵set_E(),以及初始分布set_priors()

我们初始化好这些参数:

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
 
def initTestModel():
model = hmm.HMM(['bull', 'bear', 'normal'], ['up', 'down', 'unchange'])
transition_matrix = {
'bull': {
'bull': 0.6,
'bear': 0.2,
'normal': 0.2
},
'bear': {
'bull': 0.5,
'bear': 0.3,
'normal': 0.2
},
'normal': {
'bull': 0.4,
'bear': 0.1,
'normal': 0.5
}
}
emission_matrix = {
'up': {
'bull': 0.7,
'bear': 0.1,
'normal': 0.3
},
'down': {
'bull': 0.1,
'bear': 0.6,
'normal': 0.3
},
'unchange': {
'bull': 0.2,
'bear': 0.3,
'normal': 0.4
}
}
p = {'bull':0.4, 'bear':0.3, 'normal':0.3}

model.set_T(transition_matrix)
model.set_E(emission_matrix)
model.set_priors(p)

return model

初始化模型完成之后,调用forward_algrithm()即可得出指定观测序列的预测结果:

1
2
3
4
def evaluate():
model = initTestModel()
prop = model.forward_algrithm(['up', 'up', 'up', 'up', 'up'], model)
print prop

输出结果为:

1
0.0232968298

下面的代码可以查看所有组合可能出现的概率:

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
def generate_weekly_list():
status = ['up','down','unchange']

i = 0
weekly_list = {}
for statu1 in status:
for statu2 in status:
for statu3 in status:
for statu4 in status:
for statu5 in status:
key = statu1 + ',' + statu2 + ',' + statu3 + ',' + statu4 + ',' + statu5
value = [statu1, statu2, statu3, statu4, statu5]
weekly_list[key] = value
return weekly_list


def evaluate():
model = initTestModel()

all_list = generate_weekly_list()
prop_list = {}
for key in all_list:
prop = hmm.forward_algrithm(all_list[key], model)
prop_list[key] = prop

sort_list = sorted(prop_list.items(), lambda x, y: cmp(x[1], y[1]), reverse=True)

for (key, value) in sort_list:
print ('%.6f:' % value) + key
预测问题:已知模型+股市变化,求股市状态

已知模型,观察到一周的变化情况为:涨、不变、涨、不变、跌,问股市的状态变化情况?

还是同样的模型:

观测状态集合为:牛市、熊市、普通

我们的发射状态集合为:涨、跌、不变

初始分布$\pi$为:$(0.4, 0.3, 0.3)$

转移概率矩阵A:$
A=\begin{pmatrix}
0.6 & 0.2 & 0.2 \\
0.5 & 0.3 & 0.2 \\
0.4 & 0.1 & 0.5
\end{pmatrix}
$

观测概率矩阵B:$
A=\begin{pmatrix}
0.7 & 0.1 & 0.2 \\
0.1 & 0.6 & 0.3 \\
0.3 & 0.3 & 0.4
\end{pmatrix}
$

代码实现如下:

1
2
3
4

def predict():
model = initTestModel()
print hmm.viterbi_path(['up', 'unchange', 'up', 'unchange', 'down'], model)

结果是:

1
['bull', 'bull', 'bull', 'bull', 'bear']

我们同样可以写出所有的组合的预测结果:

1
2
3
4
5
6
7

def predict():
model = initTestModel()
weekly_list = generate_weekly_list()
for key in weekly_list:
print key + ':'
print hmm.viterbi_path(weekly_list[key], model)
学习问题:已知一堆观测序列,求模型

某股民连续三周观测到股市的变动情况为:

  • 涨,不变,涨,跌,涨
  • 跌,涨,跌,涨,不变
  • 不变,不变,跌,涨,涨

问,下周的变化情况?

这个问题的思路为:

根据观测序列 -> 求出模型 -> 得到$A,B,\pi$ -> 预测当前状态 -> 利用转移矩阵预测下一个状态。

代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
def learn():
model = initTestModel()

o_list1 = ['up', 'unchange', 'up', 'down', 'up']
o_list2 = ['down', 'up', 'down', 'up', 'unchange']
o_list3 = ['unchange', 'unchange', 'down', 'up', 'up']

model = hmm.baum_welch(o_list1, model)
model = hmm.baum_welch(o_list2, model)
model = hmm.baum_welch(o_list3, model)

print hmm.viterbi_path(o_list3, model)

得出结果为:

1
['normal', 'bull', 'bear', 'bull', 'bull']

你也可以将转移概率矩阵打印出来:

1
print model._T

以上就是HMM模型的全部内容。

傅里叶级数

介绍和选择

基于傅里叶变换的方法几乎用于所有工程和科学领域,几乎所有工程师和科学家都使用。 比如对于以下领域的初学者:

  • 电路设计师
  • 光谱学家
  • 晶体学家
  • 从事信号处理和通信工作的任何人
  • 从事成像工作的人员

我期待课堂上会有很不同领域的同学,这对我们所有人来说都很重要。 随着兴趣和背景的多样性,并非所有的例子和应用都是大家所熟悉的,并且与所有人相关。 我们都必须互相削减一些内容,这是我们所有人分出的机会。 同样,你也应该意识到这是许多可能课程中傅立叶变换的一门课程。 无论是在数学上还是在应用范围内,主题的丰富性意味着我们将几乎不断地做出选择。 关于这个主题的书看起来不一样,和这份讲义也不太一样 - 甚至用于基本对象和操作的符号也因书而异。 我会试着带大家在合适的时机选择一个合适的方向作为切入点来带领大家了解这门学科,并且我也会说出其他切入点会是什么。


第一个选择是从哪里开始,我的选择是对傅立叶级数的简要处理。傅立叶分析最初涉及通过傅立叶级数表示和分析周期性现象,然后通过傅立叶将这些见解扩展到非周期性现象。 转变。 实际上,从傅立叶级数到傅立叶变换的一种方法是将非周期性现象(因此几乎任何一般函数)视为周期性现象的极限情况,因为周期趋于无穷大。 周期性情况下的一组离散频率在非周期情况下成为频率的连续体,频谱诞生了,随之而来的是该主题最重要的原则:

每个信号都有一个频谱,由频谱决定。 您可以在时域(或空间)或频域中分析信号。

我认为这条格言有资格成为宇宙组成的主要秘密之一。

课程视频地址

本质上,傅里叶级数可以看做是用数学的手段研究周期性现象的一门学科。

我们在初中高中学过cos和sin这些三角函数,我们是否可以使用这些三角函数来建模非常广泛的周期性现象呢?这正是这节课需要解决的问题。

如何使用简单的sin(t),cos(t)来建模复杂的周期性现象?

首先,我们这里说的复杂的周期性现象会普遍到什么程度呢?我们希望把这些方法应用在普遍的条件下,但是并不是所有的现象都是周期性的(其实甚至在一些周期性的现象中,这个假定也未必行得通)。所以,并不是所有的现象都试用这个方法

其实现实生活中的现象,最终都会结束,我们只观察某一个特定时间段的现象。而数学函数,比如正余弦函数,都是无始无终的。那么如何用它们来描述那些会结束的现象呢?

在面对实际的现象时,比如下面这种现象:

只存在于一个有限的时间段内,画出一个这样的信号。这并不是一个周期现象,但如果我们重复绘制这个图像,就可以强制使其成为周期性的函数了。

或许我们只是对其中的一部分感兴趣,但对于数学分析,如果使其具有周期性,就对所有的都适用了。这个过程叫做信号的周期化(periodization of a signal)。它可以用于研究非周期性信号。

信号周期化

我们通常把周期函数的周期设定为1,这样更加方便。因此函数$f(t)$需要满足:

  • 对任何$t$均有$f(t+1)=f(t)$,

因此我们的信号模型可以表示为:

以及

如果我们知道一个周期为1的周期性函数在任意一个单位为1的时间间隔内的形式,那么我们就可以知道整个函数了。

生成复杂的周期函数

那么我们如何用简单的sin和cos函数来表示各种复杂的周期现象呢?

事实上,我们可以通过对$sin(2πt)$和$cos(2πt)$进行变换和相加的方式来得到相当普遍的周期为1的周期函数。

对正余弦函数进行变换

下图是$sin(2πt)$的函数图像,其周期为1,频率为1:

经过变换之后,$sin(4πt)$的函数图像如下,其周期为$1/2$,频率为2:

其实你也可以说他的周期是1。因为一秒之内它经历了两个完整的周期,你可以把这两个完整的周期看做一个周期,图形在整个坐标轴上一直在重复这个原始信号。

再次变换之后,$sin(6πt)$的函数图像如下,其周期为$1/3$,频率为3:

同样,也可以把它看做周期是1。

对正余弦函数进行合并

现在把上面的三个函数合并:

其效果如下:

组合之后的函数周期为1,它由3个不同频率的周期函数组成,频率分布为1,2,3。但把它们组合起来之后,却只有一个周期:周期为1。

我们不仅可以改变频率,也可以单独改变幅度,并且可以改变其中每一个的相位。

表示一个复杂的周期函数的几种方式

最基本的形式

一个复杂的周期为1的信号,可以通过变换一系列的正余弦函数的频率幅度相位,然后将它们加起来,来得到。

这是表示一个复杂周期的最一般的形式。

利用和角公式来表示

正余弦和角公式:

因此我们可以将上面的公式$(1-3)$展开成以下形式:

这里的$a_k$和$b_k$是由A计算出的。

加一个常数项的形式(直流分量形式)

这里的$\frac{a_0}{2}$是一个常数项,电器工程师常称之为直流分量(dc component)。因为电器工程师在研究交流电或直流电的过程中,发现有一部分是不随着周期发生改变的,这一部分称之为直流分量

复指数的表示形式

上面几种都是比较常用的一些用于表示一个复杂信号的表示形式。但是迄今为止,最方便使用的还是用复指数的形式来表示:

在这里$i$的值为:

根据著名的欧拉公式,我们可以用复指数的形式来表示正余弦函数,其中$cos$是实部,$sin$是虚部:

你也可以将上面公式$(1-5)$的三角函数表示的和式的形式写成这种形式:

在这里$C_k$是复数。

共轭

对于复数$a + bi$来说,其共轭为$a - bi$。
复数$C_k$的共轭表示为$\bar{C_k}$

另外,如果一个复数等于其自身的共轭意味着什么呢?$C_0=\bar{C_0}$

这意味着这个数本身是实数。

如果你试着把余弦表示的方式全部转成了复指数的形式,你会发现$C_k$不仅仅是复数,它同时还满足对称性。并且由于它的对称性,所以$\sum_{k=-n}^n C_k $的总和为实数。

即:$ C_{-k} $等于$C_k$的共轭:

这是一条重要的性质。

反过来,如果和式的系数满足对称性,那么总和就应该是实数。这是因为可以把所有项分成正项和负项两组,并且由于$(1-11)$的对称关系,复数和复数的共轭的和结果是实数,可以得出这个结论。

我们生成的复杂的周期函数,其普遍性有多强?

$f(t)$是周期为1的周期函数,我们可以把$f(t)$写成$(1-10)$那种形式吗?

换句话说,一个周期为1的复杂的周期函数,我们能用正余弦函数通过叠加变化以及组合来生成出来吗?

假设我们能做到

假设我们可以做到用$(1-12)$来表示所有复杂的周期为1的周期函数,那么对于未知系数$C_k$我们如何求得呢?

以下是求$C_k$的过程:

两边同时乘以$e^{-2\pi imt}$:

然后对两边同时求积分,积分区间是0到1,因为我们的频率是1:

由于:

所以:

其中:

这里的:

是一个整数,就像$sin(2π)$乘以一个整数,这个整数是1,所以这里的结果是0。

所以我们的$C_m$可以得到下面的结果:

期初我们设定$f(t)$是已知的,所以我们就可以求得$C_m$了。


结论:给定周期为1的周期函数$f(t)$,如果能把$f(t)$写成和式的形式:$f(t)=\sum_{k=-n}^nC_ke^{2\pi ikt}$,那么溪水会按照这个公式给出,其中$C_k=\int_0^{1}e^{-2\pi ikt}f(t)dt$。

下节课我们将介绍,我们得到的这些参数,将带来什么意义。

快速入门教程

前提条件

在开始本教程之前,你需要有一定的Python基础。如果你想要回顾一下Python相关的知识点,你可以看一下这份教程

如果你希望运行本教程中的示例,那么需要在您机器上安装一些软件。有关说明,请参阅http://scipy.org/install.html

基础

NumPy的主要对象是齐次多维数组。它是一个元素的表(元素通常是数字),所有的元素拥有相同的类型,可以被一个正整数元组来索引。在NumPy中维度称之为axis(轴)

例如,在3D空间中的一个坐标点[1, 2, 1]拥有一个axis。这个axis拥有3个元素,所以我们说它的长度是3。在下面的例子中,有2个axis。第一个axis的长度是2,第二个axis的长度是3。

1
2
[[ 1., 0., 0.],
[ 0., 1., 2.]]
[[1.0, 0.0, 0.0], [0.0, 1.0, 2.0]]

NumPy的数组class称之为ndarray。它还有另外一个别名:array。注意numpy.array与标准Python库中的array.array不一样,标准库中的array只可以操作以为数组,并且只能提供少量的方法。ndarray更重要的一些属性如下:

ndarray.ndim

数组的axis(维度)数量

ndarray.shape

数组的维度。这是一个整数类型的元组,指示了数组在每个维度下的尺寸信息。对于一个n行m列的矩阵来说,它的`shape`是`(n,m)`。因此`shape`元组的长度,也是axis的数量,即`ndim`。

ndarray.size

数组的元素总数。值等于`shape`中的元素的乘积。

ndarray.dtype

一个描述数组中元素类型的对象。可以使用标准的Python类型创建或指定dtype。另外,也可以使用NumPy自己提供的一些类型。例如`numpy.int32`,`numpy.int16`和`numpy.float64`。

ndarray.itemsize

数组中每个元素占用的bytes大小。例如,一个数组的元素类型为`float64`,它的`itemsize`就是8(=64/8),另一个数组的元素类型为`complex32`的`itemsize`值为4(=32/8)。这个值相当于`ndarray.dtype.itemsize`。

ndarray.data

该缓冲区包含数组的实际元素。通常,我们不需要使用此属性,因为我们将使用索引来访问数组中的元素。

一个例子

1
2
3
import numpy as np
a = np.arange(15).reshape(3, 5)
a
array([[ 0,  1,  2,  3,  4],
       [ 5,  6,  7,  8,  9],
       [10, 11, 12, 13, 14]])
1
a.shape
(3, 5)
1
a.ndim
2
1
a.dtype.name
'int64'
1
a.itemsize
8
1
a.size
15
1
type(a)
numpy.ndarray
1
2
b = np.array([6, 7, 8])
b
array([6, 7, 8])
1
type(b)
numpy.ndarray

数组的创建

有几种可以创建数组的方式。

例如,你可以通过使用array方法,从一个标准的Python列表或元组来创建一个numpy数组。数组的类型由序列中元素的类型自动推导得出。

1
2
3
import numpy as np
a = np.array([2,3,4])
a
array([2, 3, 4])
1
a.dtype
dtype('int64')
1
2
b = np.array([1.2, 3.5, 5.1])
b.dtype
dtype('float64')

在调用array方法来创建数组时,有一种常见的错误,就是在方法中传入了多个数字,而不是通过传入一个包含一组数字的list作为参数。

1
2
a = np.array(1,2,3,4)   # WRONG
a = np.array([1,2,3,4]) # RIGHT

array函数将序列的序列转换为二维数组,将序列的序列的序列转换成3维数组,等等。

1
2
b = np.array([(1.5,2,3),(4,5,6)])
b
array([[ 1.5,  2. ,  3. ],
       [ 4. ,  5. ,  6. ]])

数组的类型也可以在创建的时候,显式的指定:

1
2
c = np.array([[1,2],[3,4]],dtype=complex)
c
array([[ 1.+0.j,  2.+0.j],
       [ 3.+0.j,  4.+0.j]])

通常,数组的元素在初始状态下是未知的,但尺寸已知。因此,NumPy提供了一些方法来创建以初始化占位符填充的数组。这最大限度地减少了增加数组的开销,这是一项昂贵的操作。

方法zeros创建一个全部由0填充的数组,方法ones创建一个全部由1填充的数组,方法empty创建了一个全部由随机的数字填充的数组,随机数的值取决于内存当前的状态。默认情况下,创建出来的数组类型为folat64

1
np.zeros((3,4))
array([[ 0.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  0.]])
1
np.ones((2,3,4), dtype=np.int16) # dtype可以被指定
array([[[1, 1, 1, 1],
        [1, 1, 1, 1],
        [1, 1, 1, 1]],

       [[1, 1, 1, 1],
        [1, 1, 1, 1],
        [1, 1, 1, 1]]], dtype=int16)
1
np.empty((2,3)) # 未初始化,输出可能不同
array([[ 0.,  0.,  0.],
       [ 0.,  0.,  0.]])

为了创建数字序列,NumPy提供了一个类似于range的返回数组而不是列表的函数。

1
np.arange(10, 30, 5)
array([10, 15, 20, 25])
1
np.arange(0, 2, 0.3) # 可以接受float类型的参数
array([ 0. ,  0.3,  0.6,  0.9,  1.2,  1.5,  1.8])

arange与浮点参数一起使用时,由于有限的浮点精度,通常不可能预测获得的元素数量。出于这个原因,通常最好使用函数linspace来接收我们想要的元素数量作为参数,而不是步长:

1
2
from numpy import pi
np.linspace(0, 2, 9) # 创建9个数字,均匀分布在0到2之间
array([ 0.  ,  0.25,  0.5 ,  0.75,  1.  ,  1.25,  1.5 ,  1.75,  2.  ])
1
2
x = np.linspace( 0, 2*pi, 100)
f = np.sin(x)

打印数组

当你打印一个数组时,NumPy以一种类似嵌套列表的形式来展示,同时具有以下布局:

  • 最后一个axis从左向右打印
  • 倒数第二个axis从上到下打印
  • 其余的也是从上到下打印的,每个切片与下一个由空行分开。

然后将一维数组打印为行,将二维数组作为矩阵,将三维数组作为矩阵列表。

1
2
a = np.arange(6)  # 一维数组
print(a)
[0 1 2 3 4 5]
1
2
b = np.arange(12).reshape(4,3)  # 二维数组
print(b)
[[ 0  1  2]
 [ 3  4  5]
 [ 6  7  8]
 [ 9 10 11]]
1
2
c = np.arange(24).reshape(2,3,4)  # 三维数组
print(c)
[[[ 0  1  2  3]
  [ 4  5  6  7]
  [ 8  9 10 11]]

 [[12 13 14 15]
  [16 17 18 19]
  [20 21 22 23]]]

如果数组太大而无法打印,NumPy将自动跳过数组的中心部分并仅打印角点:

1
print(np.arange(10000))
[   0    1    2 ..., 9997 9998 9999]
1
print(np.arange(10000).reshape(100,100))
[[   0    1    2 ...,   97   98   99]
 [ 100  101  102 ...,  197  198  199]
 [ 200  201  202 ...,  297  298  299]
 ..., 
 [9700 9701 9702 ..., 9797 9798 9799]
 [9800 9801 9802 ..., 9897 9898 9899]
 [9900 9901 9902 ..., 9997 9998 9999]]

要禁用此行为并强制NumPy打印整个数组,可以使用set_printoptions更改打印选项。

1
np.set_printoptions(threshold=np.nan)

基本操作

数组上的算术运算符应用于元素。一个新的数组被创建并填充结果。

1
a = np.array([20, 30, 40, 50])
1
2
b = np.arange(4)
b
array([0, 1, 2, 3])
1
2
c = a-b
c
array([20, 29, 38, 47])
1
b**2
array([0, 1, 4, 9])
1
10*np.sin(a)
array([ 9.12945251, -9.88031624,  7.4511316 , -2.62374854])
1
a<35
array([ True,  True, False, False], dtype=bool)

不像其他的矩阵语言那样,*操作符在NumPy中是元素间的乘法。矩阵乘法可以使用dot方法来实现:

1
2
3
A = np.array([[1,1],[0,1]])
B = np.array([[2,0],[3,4]])
A*B # 元素间的乘积
array([[2, 0],
       [0, 4]])
1
A.dot(B)   # 矩阵乘法
array([[5, 4],
       [3, 4]])
1
np.dot(A, B) # 矩阵乘法的另一种实现
array([[5, 4],
       [3, 4]])

一些例如+=-=的操作符,实现的方式是通过修改现有的矩阵而不是创建新的矩阵。

1
2
3
4
a = np.ones((2,3), dtype=int)
b = np.random.random((2,3))
a *= 3
a
array([[3, 3, 3],
       [3, 3, 3]])
1
2
b += a
b
array([[ 3.05432455,  3.59941571,  3.65058751],
       [ 3.85091779,  3.45890823,  3.55943444]])
1
a += b # b 不会自动的转型成为 integer 类型
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-87-3054fce39e6f> in <module>()
----> 1 a += b


TypeError: Cannot cast ufunc add output from dtype('float64') to dtype('int64') with casting rule 'same_kind'

在使用不同类型的数组时,结果数组的类型对应于更一般或精确的数组(称为向上转型)。

1
2
3
a = np.ones(3, dtype=np.int32)
b = np.linspace(0, pi, 3)
b.dtype.name
'float64'
1
2
c = a+b
c
array([ 1.        ,  2.57079633,  4.14159265])
1
c.dtype.name
'float64'
1
2
d = np.exp(c*1j)
d
array([ 0.54030231+0.84147098j, -0.84147098+0.54030231j,
       -0.54030231-0.84147098j])
1
d.dtype.name
'complex128'

许多一元运算,例如计算数组中所有元素的总和,都是作为ndarray类的方法来实现的。

1
2
a = np.random.random((2,3))
a
array([[ 0.48681264,  0.52685408,  0.53980305],
       [ 0.27958753,  0.55125855,  0.70834892]])
1
a.sum()
3.0926647737313067
1
a.min()
0.27958753466020847
1
a.max()
0.70834891569018965

默认情况下,这些操作适用于数组,就好像它是数字列表一样,无论其形状如何。但是,通过指定axis参数,可以沿着数组的指定轴(axis)应用操作:

1
2
b = np.arange(12).reshape(3,4)
b
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])
1
b.sum(axis=0)    # 每一列的和
array([12, 15, 18, 21])
1
b.min(axis=1)    # 每一行的最小值
array([0, 4, 8])
1
b.cumsum(axis=1)   # 每行的累加值
array([[ 0,  1,  3,  6],
       [ 4,  9, 15, 22],
       [ 8, 17, 27, 38]])

通用方法

NumPy提供了一些常见的数学运算方法,例如sin,cos和exp。在NumPy中,这些方法被称作”通用方法”(ufunc)。在NumPy中,这些方法操作在数组中的每个元素上,产生一个数组作为输出。

1
2
B = np.arange(3)
B
array([0, 1, 2])
1
np.exp(B)
array([ 1.        ,  2.71828183,  7.3890561 ])
1
np.sqrt(B)
array([ 0.        ,  1.        ,  1.41421356])
1
2
C = np.array([2., -1., 4.])
np.add(B, C)
array([ 2.,  0.,  6.])

索引,切片和迭代

一维数组可以像Python中的list或其他序列一样进行索引、切片和迭代操作。

1
2
a = np.arange(10)**3
a
array([  0,   1,   8,  27,  64, 125, 216, 343, 512, 729])
1
a[2]
8
1
a[2:5]
array([ 8, 27, 64])
1
a[:6:2] = -1000
1
a
array([-1000,     1, -1000,    27, -1000,   125,   216,   343,   512,   729])
1
a[::-1]
array([  729,   512,   343,   216,   125, -1000,    27, -1000,     1, -1000])
1
2
for i in a:
print(i**(1/3.))
nan
1.0
nan
3.0
nan
5.0
6.0
7.0
8.0
9.0


/usr/local/Homebrew/Cellar/python3/3.6.0/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/ipykernel_launcher.py:2: RuntimeWarning: invalid value encountered in power

多维数组每个轴(axis)都有一个索引。这些索引以逗号分隔的元组给出:

1
2
3
4
5
def f(x,y):
return 10*x + y

b = np.fromfunction(f,(5,4),dtype=int)
b
array([[ 0,  1,  2,  3],
       [10, 11, 12, 13],
       [20, 21, 22, 23],
       [30, 31, 32, 33],
       [40, 41, 42, 43]])
1
b[2,3]
23
1
b[0:5, 1] # 输出第2列的每一行的元素
array([ 1, 11, 21, 31, 41])
1
b[:, 1] # 与上一步操作等价
array([ 1, 11, 21, 31, 41])
1
b[1:3, :] # 输出第2和第3行的每个列元素
array([[10, 11, 12, 13],
       [20, 21, 22, 23]])

当提供的索引数量少于axis的数量时,缺失的索引被视为完整的切片:

1
b[-1] # 输出最后一行。相当于 b[-1,:]
array([40, 41, 42, 43])

表达式b[i]的这种表示形式,意味着在i后面还有多个::的数量取决于剩余的axis数量。NumPy也允许你使用...来表示这一形式:b[i,...]

(...)表示产生完整索引元组所需要的冒号。例如,如果x是一个5轴数组,那么:

  • x[1,2,...]等价于x[1,2,:,:,:]
  • x[...,3]等价于x[:,:,:,:,3]
  • x[4,...,5,:]等价于x[4,:,:,5,:]
1
2
3
4
5
6
7
# 一个3D数组(由两个2D数组粘贴而成)
c = np.array([[[0, 1, 2],
[10, 12, 13]],
[[100,101,102],
[110,112,113]]
])
c.shape
(2, 2, 3)
1
c[1,...]  # 相当于 c[1,:,:] 或 c[1]
array([[100, 101, 102],
       [110, 112, 113]])
1
c[...,2]  # 相当于 c[:,:,2]
array([[  2,  13],
       [102, 113]])

迭代多维数组是相对于第一个axis完成的:

1
2
for row in b:
print(row)
[0 1 2 3]
[10 11 12 13]
[20 21 22 23]
[30 31 32 33]
[40 41 42 43]

但是,如果想要对数组中的每个元素执行操作,可以使用flat属性,该属性是数组中所有元素的迭代器:

1
2
for element in b.flat:
print(element)
0
1
2
3
10
11
12
13
20
21
22
23
30
31
32
33
40
41
42
43

Shape操作

改变一个array的shape

一个数组的形状由这个数组每个轴上的元素数量给出:

1
2
a = np.floor(10*np.random.random((3,4)))
a
array([[ 8.,  3.,  6.,  4.],
       [ 7.,  5.,  7.,  7.],
       [ 3.,  1.,  8.,  8.]])
1
a.shape
(3, 4)

数组的形状可以通过各种命令进行更改。请注意,以下三个命令都返回一个修改后的数组,但都没有改变原数组:

1
a.ravel()  # 返回展开的数组
array([ 8.,  3.,  6.,  4.,  7.,  5.,  7.,  7.,  3.,  1.,  8.,  8.])
1
a.reshape(6,2)  # 返回一个改变了shape的数组
array([[ 8.,  3.],
       [ 6.,  4.],
       [ 7.,  5.],
       [ 7.,  7.],
       [ 3.,  1.],
       [ 8.,  8.]])
1
a.T  # 返回数组的转置
array([[ 8.,  7.,  3.],
       [ 3.,  5.,  1.],
       [ 6.,  7.,  8.],
       [ 4.,  7.,  8.]])
1
a.T.shape
(4, 3)
1
a.shape
(3, 4)

由ravel()产生的数组元素的顺序通常是”C-style”的,即最右边的索引“变化最快”,因此[0,0]之后的元素是[0,1]。如果一个数组变形为其他形状,数组再次被视为”C-style”。NumPy通常创建按次顺序存储的数组,因此ravel()通常不需要复制数组,但如果数组是通过对另一个数组进行切片操作,或者使用不寻常的方式创建的,则可能需要复制它。函数ravel()reshape()也可以通过使用可选参数来使用FORTRAN-style的数组,其中最左侧的索引更改速度最快。

reshape方法返回的结果是一个变形后的数组,而ndarray.resize方法会更改数组本身的形状:

1
a
array([[ 8.,  3.,  6.,  4.],
       [ 7.,  5.,  7.,  7.],
       [ 3.,  1.,  8.,  8.]])
1
2
a.resize((2,6))
a
array([[ 8.,  3.,  6.,  4.,  7.,  5.],
       [ 7.,  7.,  3.,  1.,  8.,  8.]])

如果在reshape操作中将尺寸参数传入-1,则会自动计算这一位置的尺寸:

1
a.reshape(3,-1)
array([[ 8.,  3.,  6.,  4.],
       [ 7.,  5.,  7.,  7.],
       [ 3.,  1.,  8.,  8.]])

将不同的数组粘贴起来

多个数组可以按照不同的axis来粘贴起来:

1
2
a = np.floor(10*np.random.random((2,2)))
a
array([[ 9.,  9.],
       [ 8.,  6.]])
1
2
b = np.floor(10*np.random.random((2,2)))
b
array([[ 5.,  3.],
       [ 0.,  4.]])
1
np.vstack((a,b))
array([[ 9.,  9.],
       [ 8.,  6.],
       [ 5.,  3.],
       [ 0.,  4.]])
1
np.hstack((a,b))
array([[ 9.,  9.,  5.,  3.],
       [ 8.,  6.,  0.,  4.]])

函数column_stack将1D数组作为列堆叠到2D数组中。它相当于仅用于2D数组的hstack操作。

1
2
from numpy import newaxis
np.column_stack((a,b)) # 仅作用于2D数组
array([[ 9.,  9.,  5.,  3.],
       [ 8.,  6.,  0.,  4.]])
1
2
3
a = np.array([4., 2.])
b = np.array([3., 8.])
np.column_stack((a,b)) # 返回一个2D数组
array([[ 4.,  3.],
       [ 2.,  8.]])
1
np.hstack((a,b))  # 得到不同的结果
array([ 4.,  2.,  3.,  8.])
1
a[:,newaxis]  # 这将得到一个2D列向量
array([[ 4.],
       [ 2.]])
1
np.column_stack((a[:,newaxis],b[:,newaxis]))
array([[ 4.,  3.],
       [ 2.,  8.]])
1
np.hstack((a[:,newaxis],b[:,newaxis]))  # 结果是一样的
array([[ 4.,  3.],
       [ 2.,  8.]])

另一方面,函数row_stack相当于对任何数组进行vstack操作。一般情况下,对于具有两个以上维度的数组,hstack操作沿着它的第二个axis进行堆叠,vstack沿着它的第一个axis堆叠,concatenate沿着指定axis的方向进度堆叠。

注意

在复杂的情况下,r_c_可用于通过沿着一个轴堆积数字来创建数组。他们允许使用表示范围的:操作符。

1
np.r_[1:4,0,4]
array([1, 2, 3, 0, 4])

当使用数组作为参数时,r_c_与默认行为的vstackhstack类似,可以通过可选参数指定所要连接的轴的序号。

将一个数组拆分成几个较小的数组

使用hsplit,可以沿着水平轴来切割数组,或者通过指定返回的数组的形状来切割数组,或者通过指定需要分割的列来分割数组。

1
2
a = np.floor(10*np.random.random((2,12)))
a
array([[ 4.,  3.,  3.,  1.,  2.,  5.,  2.,  5.,  5.,  8.,  2.,  2.],
       [ 5.,  1.,  1.,  2.,  9.,  6.,  5.,  5.,  0.,  8.,  8.,  7.]])
1
np.hsplit(a,3)  # 将a切分成3份
[array([[ 4.,  3.,  3.,  1.],
        [ 5.,  1.,  1.,  2.]]), array([[ 2.,  5.,  2.,  5.],
        [ 9.,  6.,  5.,  5.]]), array([[ 5.,  8.,  2.,  2.],
        [ 0.,  8.,  8.,  7.]])]
1
np.hsplit(a,(3,4)) # 沿着第3和第4列来切分数组
[array([[ 4.,  3.,  3.],
        [ 5.,  1.,  1.]]), array([[ 1.],
        [ 2.]]), array([[ 2.,  5.,  2.,  5.,  5.,  8.,  2.,  2.],
        [ 9.,  6.,  5.,  5.,  0.,  8.,  8.,  7.]])]

vspilt沿着垂直轴进行分割,array_split允许指定沿着那个轴来进行分割。

副本和视图

当操作一个数组时,它们的数据有时会被复制到一个新的数组中,有时则不会。这通常会让新手感到困惑。下面是3个例子:

完全没有复制

简单的赋值不会复制数组对象或数据。

1
2
3
a = np.arange(12)
b = a # 没有新的对象被创建
b is a # a 和 b 是同一个ndarray对象的两个名字
True
1
2
b.shape = 3,4  # 改变a的shape
a.shape
(3, 4)

Python将可变对象作为引用传递,所以函数调用不会执行复制操作。

1
2
3
4
def f(x):
print(id(x))

id(a) # id 是一个对象的唯一标识
4449897488
1
f(a)
4449897488

视图或浅拷贝

不同的数组对象可以共享相同的数据。view函数创建一个新的数组对象,但它和原数组持有相同的数据。

1
2
c = a.view()
c is a
False
1
c.base is a  # c 是 一个a数据所创建出来的视图
True
1
c.flags.owndata
False
1
2
c.shape = 2,6  # a的shape并不发生改变
a.shape
(3, 4)
1
2
c[0,4] = 1234  # a的数据发生改变
a
array([[   0,    1,    2,    3],
       [1234,    5,    6,    7],
       [   8,    9,   10,   11]])

对一个数组进行切片操作,返回它的一个视图:

1
2
3
s = a[ : , 1:3]  # 也可以被写作 s = a[:,1:3]
s[:] = 10 # s[:] 是一个s的视图。注意这里 s = 10 和 s[:] = 10 的区别
a
array([[   0,   10,   10,    3],
       [1234,   10,   10,    7],
       [   8,   10,   10,   11]])

深拷贝

copy方法可以构造数组以及数据的完整副本。

1
2
d = a.copy()    # 一个由新数据构成的新的数组对象被创建了
d is a
False
1
d.base is a     # d 与 a 不共享任何东西
False
1
2
d[0,0] = 9999
a
array([[   0,   10,   10,    3],
       [1234,   10,   10,    7],
       [   8,   10,   10,   11]])

方法预览

这里有一个NumPy中各种类型的比较有用的方法列表。

  • 数组创建

    arange,array,copy,empty,empty_like,eye,fromfile,fromfunction,identity,linspace,logspace,mgrid,ogrid,ones,ones_like,zeros,zeros_like

  • 转换

    ndarray.astype,atleast_1d,atleast_2d,atleast_3d,mat

  • 手法

    array_split, column_stack, concatenate, diagonal, dsplit, dstack, hsplit, hstack, ndarray.item, newaxis, ravel, repeat, reshape, resize, squeeze, swapaxes, take, transpose, vsplit, vstack

  • 问题

    all, any, nonzero, where

  • 排序

    argmax, argmin, argsort, max, min, ptp, searchsorted, sort

  • 操作

    choose, compress, cumprod, cumsum, inner, ndarray.fill, imag, prod, put, putmask, real, sum

  • 基本统计

    cov,mean,std,var

  • 基本线性代数

    cross,dot,outer,linalg.svd,vdot

进阶

广播机制

广播允许通用方法以有意义的方式处理形状不完全相同的输入。

广播第一法则是,如果所有的输入数组维度不都相同,一个“1”将被重复地添加在维度较小的数组上直至所有的数组拥有一样的维度。

广播第二法则确定长度为1的数组沿着特殊的方向表现地好像它有沿着那个方向最大形状的大小。对数组来说,沿着那个维度的数组元素的值理应相同。

应用广播法则之后,所有数组的大小必须匹配。更多细节可以从这个文档找到。

花哨的索引和索引技巧

NumPy提供比常规Python序列更多的索引功能。正如我们前面看到的,除了通过整数和切片进行索引之外,还可以使用整数和布尔数组数组对索引进行索引。

通过数组索引

1
2
3
a = np.arange(12)**2          # 前12个方格
i = np.array([1,1,3,8,5]) # 一个索引数组
a[i] # 一个在位置i的元素
array([ 1,  1,  9, 64, 25])
1
2
j = np.array([[ 3, 4], [ 9, 7]])  # 一个二维索引数组
a[j] # 与j的shape相同
array([[ 9, 16],
       [81, 49]])

当被索引数组a是多维的时,每一个唯一的索引数列指向a的第一维。以下示例通过将图片标签用调色版转换成色彩图像展示了这种行为。

1
2
3
4
5
6
7
8
9
10
11
palette = np.array([[0,0,0],           # 黑
[255,0,0], # 红
[0,255,0], # 绿
[0,0,255], # 蓝
[255,255,255] # 白
])
image = np.array([[0,1,2,0], # 每个值对应调色板中的颜色
[0,3,4,0]
])

palette[image]
array([[[  0,   0,   0],
        [255,   0,   0],
        [  0, 255,   0],
        [  0,   0,   0]],

       [[  0,   0,   0],
        [  0,   0, 255],
        [255, 255, 255],
        [  0,   0,   0]]])

我们也可以给出不不止一维的索引,每一维的索引数组必须有相同的形状。

1
2
a = np.arange(12).reshape(3,4)
a
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])
1
2
3
4
5
6
7
i = np.array([[0,1],    # indices for the first dim of a
[1,2]])

j = np.array([[2,1], # indices for the second dim
[3,3]])

a[i,j] # i 和 j必须拥有相同的shape
array([[ 2,  5],
       [ 7, 11]])
1
a[i,2]
array([[ 2,  6],
       [ 6, 10]])
1
a[:,j]
array([[[ 2,  1],
        [ 3,  3]],

       [[ 6,  5],
        [ 7,  7]],

       [[10,  9],
        [11, 11]]])

当然,我们可以将ij放入一个序列中(比如说一个列表),然后用列表进行索引。

1
2
l = [i,j]
a[l] # 相当于一个[i,j]
array([[ 2,  5],
       [ 7, 11]])

但是,我们不能将ij放进一个数组,因为这个数组将被解读为a的第一个维度的索引。

1
2
s = np.array([i,j])
a[s] # 结果不是我们想要的
---------------------------------------------------------------------------

IndexError                                Traceback (most recent call last)

<ipython-input-204-79ccae1d198c> in <module>()
      1 s = np.array([i,j])
----> 2 a[s]     # 结果不是我们想要的


IndexError: index 3 is out of bounds for axis 0 with size 3
1
a[tuple(s)]     # 与 a[i,j]相同
array([[ 2,  5],
       [ 7, 11]])

另一个常用的数组索引用法是搜索时间序列最大值。

1
2
3
time = np.linspace(20, 145, 5)    # 时间尺度
data = np.sin(np.arange(20)).reshape(5,4) # 4个时间依赖序列
time
array([  20.  ,   51.25,   82.5 ,  113.75,  145.  ])
1
data
array([[ 0.        ,  0.84147098,  0.90929743,  0.14112001],
       [-0.7568025 , -0.95892427, -0.2794155 ,  0.6569866 ],
       [ 0.98935825,  0.41211849, -0.54402111, -0.99999021],
       [-0.53657292,  0.42016704,  0.99060736,  0.65028784],
       [-0.28790332, -0.96139749, -0.75098725,  0.14987721]])
1
2
ind = data.argmax(axis=0)    # 每个序列的最大值的索引
ind
array([2, 0, 3, 1])
1
2
3
4
time_max = time[ind]         # 时间序列对应的最大值
data_max = data[ind, range(data.shape[1])] # => data[ind[0],0], data[ind[1],1]...

time_max
array([  82.5 ,   20.  ,  113.75,   51.25])
1
data_max
array([ 0.98935825,  0.84147098,  0.99060736,  0.6569866 ])
1
np.all(data_max == data.max(axis=0))
True

你也可以使用数组索引作为目标来赋值:

1
2
a = np.arange(5)
a
array([0, 1, 2, 3, 4])
1
2
a[[1,3,4]] = 0
a
array([0, 0, 2, 0, 0])

然而,当一个索引列表包含重复时,赋值被多次完成,保留最后一次的值:

1
2
3
a = np.arange(5)
a[[0,0,2]]=[1,2,3]
a
array([2, 1, 3, 3, 4])

这足够合理,但是小心如果你想用Python的+=结构,可能结果并非你所期望:

1
2
3
a = np.arange(5)
a[[0,0,2]] += 1
a
array([1, 1, 3, 3, 4])

即使0在索引列表中出现两次,索引为0的元素仅仅增加一次。这是因为Python要求a+=1a=a+1等同。

通过布尔数组索引

当我们使用整数数组索引数组时,我们提供一个索引列表去选择。通过布尔数组索引的方法是不同的我们显式地选择数组中我们想要和不想要的元素。

我们能想到的使用布尔数组的索引最自然方式就是使用和原数组一样形状的布尔数组。

1
2
3
a = np.arange(12).reshape(3,4)
b = a > 4
b # b 是一个和a形状相同的boolean数组
array([[False, False, False, False],
       [False,  True,  True,  True],
       [ True,  True,  True,  True]], dtype=bool)
1
a[b]                # 经过筛选后的1维数组
array([ 5,  6,  7,  8,  9, 10, 11])

这个属性在赋值时非常有用:

1
2
a[b] = 0            # 将a中所有比4大的元素赋值为0
a
array([[0, 1, 2, 3],
       [4, 0, 0, 0],
       [0, 0, 0, 0]])

你可以参考曼德博集合示例看看如何使用布尔索引来生成曼德博集合的图像。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import numpy as np
import matplotlib.pyplot as plt
def mandelbrot(h,w,maxit=20):
"""返回一个尺寸为(h,w)的曼德博分形图"""
y,x = np.ogrid[ -1.4:1.4:h*1j, -2:0.8:w*1j]
c = x+y * 1j
z = c
divtime = maxit + np.zeros(z.shape,dtype=int)

for i in range(maxit):
z = z**2 + c
diverge = z*np.conj(z) > 2**2 # who is diverging
div_now = diverge & (divtime == maxit) # who is diverging now
divtime[div_now] = i # note when
z[diverge] = 2 # avoid diverging too much

return divtime

plt.imshow(mandelbrot(400,400))
plt.show()

png

第二种通过布尔来索引的方法更近似于整数索引;对数组的每个维度我们给一个一维布尔数组来选择我们想要的切片。

1
2
3
4
5
a = np.arange(12).reshape(3,4)
b1 = np.array([False,True,True]) # 第一维的筛选
b2 = np.array([True,False,True,False]) # 第二维的筛选

a[b1,:] # 选择行
array([[ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])
1
a[b1]         # 和上面相同
array([[ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])
1
a[:,b2]       # 选择列
array([[ 0,  2],
       [ 4,  6],
       [ 8, 10]])
1
a[b1,b2]      # 一个奇怪的结果
array([ 4, 10])

注意一维数组的长度必须和你想要切片的维度或轴的长度一致,在之前的例子中,b1是一个秩为1长度为三的数组(a的行数),b2(长度为4)与a的第二秩(列)相一致。

ix_()函数

ix_函数可以为了获得多元组的结果而用来结合不同向量。例如,如果你想要用所有向量a、b和c元素组成的三元组来计算a+b*c:

1
2
3
4
5
a = np.array([2,3,4,5])
b = np.array([8,5,4])
c = np.array([5,4,6,8,3])
ax,bx,cx = np.ix_(a,b,c)
ax
array([[[2]],

       [[3]],

       [[4]],

       [[5]]])
1
bx
array([[[8],
        [5],
        [4]]])
1
cx
array([[[5, 4, 6, 8, 3]]])
1
ax.shape, bx.shape, cx.shape
((4, 1, 1), (1, 3, 1), (1, 1, 5))
1
2
result = ax + bx * cx
result
array([[[42, 34, 50, 66, 26],
        [27, 22, 32, 42, 17],
        [22, 18, 26, 34, 14]],

       [[43, 35, 51, 67, 27],
        [28, 23, 33, 43, 18],
        [23, 19, 27, 35, 15]],

       [[44, 36, 52, 68, 28],
        [29, 24, 34, 44, 19],
        [24, 20, 28, 36, 16]],

       [[45, 37, 53, 69, 29],
        [30, 25, 35, 45, 20],
        [25, 21, 29, 37, 17]]])
1
result[3,2,4]
17
1
a[3] + b[2] * c[4]
17

你也可以实行如下简化:

1
2
3
4
5
6
def ufunc_reduce(ufct, *vectors):
vs = np.ix_(*vectors)
r = ufct.identity
for v in vs:
r = ufct(r,v)
return r

然后这样使用它:

1
ufunc_reduce(np.add,a,b,c)
array([[[15, 14, 16, 18, 13],
        [12, 11, 13, 15, 10],
        [11, 10, 12, 14,  9]],

       [[16, 15, 17, 19, 14],
        [13, 12, 14, 16, 11],
        [12, 11, 13, 15, 10]],

       [[17, 16, 18, 20, 15],
        [14, 13, 15, 17, 12],
        [13, 12, 14, 16, 11]],

       [[18, 17, 19, 21, 16],
        [15, 14, 16, 18, 13],
        [14, 13, 15, 17, 12]]])

这个reduce与ufunc.reduce(比如说add.reduce)相比的优势在于它利用了广播法则,避免了创建一个输出大小乘以向量个数的参数数组。

用字符串索引

参加结构化数组

线性代数

继续前进,基本线性代数包含在这里。

简单数组运算

参考numpy文件夹中的linalg.py获得更多信息

1
2
3
import numpy as np
a = np.array([[1.0,2.0], [3.0, 4.0]])
print(a)
[[ 1.  2.]
 [ 3.  4.]]
1
a.transpose()
array([[ 1.,  3.],
       [ 2.,  4.]])
1
np.linalg.inv(a)
array([[-2. ,  1. ],
       [ 1.5, -0.5]])
1
2
u = np.eye(2) # unit 2x2 matrix; "eye" represents "I"  单位矩阵
u
array([[ 1.,  0.],
       [ 0.,  1.]])
1
2
j = np.array([[0.0, -1.0], [1.0, 0.0]])
np.dot(j, j) # 矩阵乘法
array([[-1.,  0.],
       [ 0., -1.]])
1
np.trace(u)    # trace
2.0
1
2
y = np.array([[5.],[7.]])
np.linalg.solve(a, y)
array([[-3.],
       [ 4.]])
1
np.linalg.eig(j)
(array([ 0.+1.j,  0.-1.j]),
 array([[ 0.70710678+0.j        ,  0.70710678-0.j        ],
        [ 0.00000000-0.70710678j,  0.00000000+0.70710678j]]))
1
2
3
4
5
6
7
Parameters:
square matrix
Returns
The eigenvalues, each repeated according to its multiplicity.
The normalized (unit "length") eigenvectors, such that the
column ``v[:,i]`` is the eigenvector corresponding to the
eigenvalue ``w[i]`` .

技巧和提示

下面我们给出简短和有用的提示。

“自动”改变形状

更改数组的维度,你可以省略一个尺寸,它将被自动推导出来。

1
2
3
a = np.arange(30)
a.shape = 2,-1,3 # -1 意味着 “无论需要什么”
a.shape
(2, 5, 3)
1
a
array([[[ 0,  1,  2],
        [ 3,  4,  5],
        [ 6,  7,  8],
        [ 9, 10, 11],
        [12, 13, 14]],

       [[15, 16, 17],
        [18, 19, 20],
        [21, 22, 23],
        [24, 25, 26],
        [27, 28, 29]]])

向量组合(stacking)

我们如何用两个相同尺寸的行向量列表构建一个二维数组?在MATLAB中这非常简单:如果xy是两个相同长度的向量,你仅仅需要做m=[x;y]。在NumPy中这个过程通过函数column_stackdstackhstackvstack来完成,取决于你想要在那个维度上组合。例如:

1
2
3
4
5
x = np.arange(0,10,2)      # x = ([0,2,4,6,8])
y = np.arange(5) # y = ([0,1,2,3,4])
m = np.vstack([x,y]) # m=([[0,2,4,6,8],
# [0,1,2,3,4]])
m
array([[0, 2, 4, 6, 8],
       [0, 1, 2, 3, 4]])
1
2
xy = np.hstack([x,y])      # xy = ([0,2,4,6,8,0,1,2,3,4])
xy
array([0, 2, 4, 6, 8, 0, 1, 2, 3, 4])

直方图(histogram)

NumPy中histogram函数应用到一个数组返回一对变量:直方图数组和箱式向量。注意:matplotlib也有一个用来建立直方图的函数(叫作hist,正如matlab中一样)与NumPy中的不同。主要的差别是pylab.hist自动绘制直方图,而numpy.histogram仅仅产生数据。

1
2
3
4
5
6
7
8
import numpy as np
import matplotlib.pyplot as plt
# 简历一个拥有10000个元素的正态分布的向量,方差为0.5^2,均值为2
mu, sigma = 2, 0.5
v = np.random.normal(mu,sigma,10000)
# 绘制分成50份的正态分布直方图
plt.hist(v, bins=50, normed=1)
plt.show()

png

1
2
3
4
# 用numpy计算直方图然后绘制它
(n, bins) = np.histogram(v, bins=50, normed=True) # NumPy version (no plot)
plt.plot(.5*(bins[1:]+bins[:-1]), n)
plt.show()

png

前提条件

  • 安装Docker 1.13或更高的版本
  • 按照Part3部分,获取Docker Compos
  • 按照Part4部分,获取Docker Machine
  • 阅读Part1。
  • 学习Part2中的如何创建容器。
  • 确保您的镜像作为一个发布容器在运行。运行这条插入了usernamerepotag信息的命令:docker run -p 80:80 username/repo:tag,然后访问http://localhost/
  • 获取到Part5中的最终版本的compose.yml文件。

介绍

您一直在为整个教程编辑相同的Compose文件。那么,我们有一个好消息,这个Compose文件在生产环境中的效果与你的计算机上的效果是相同的。在这里,我们通过一些选项来运行Docker化的程序。

发布

Docker社区版(云服务提供者)

如果您可以在生产环境中使用Docker社区版,那么你可以使用Docker Cloud来帮助您管理应用程序,例如Amazon Web Services,DigitalOcean,和Microsoft Azure等常用的服务提供商。

设置和部署:

  • 将Docker Cloud与您的首选提供商连接,授予Docker Cloud权限,以便为您自动配置以及为您”Docker化”VM。
  • 使用Docker Cloud创建您的计算资源并创建您的swarm。
  • 部署您的应用。

注意:我们没有链接到Docker Cloud文档。请务必在完成每个步骤后回到此页面。

连接Docker Cloud

你可以在标准模式swarm模式下运行Docker Cloud。

如果你正在标准模式下运行Docker Cloud,请按照以下说明将您的服务提供商链接到Docker Cloud。

如果您在Swarm模式下运行(推荐用于Amazon Web Services或Microsoft Azure),那么请跳至下一节关于如何创建swarm的部分。

创建你的swarm

准备好创建一个swarm了吗?

注意:如果您使用Docker云代理来自带主机,则此提供程序不支持swarm模式。您可以使用Docker Cloud注册您自己的现有的swarm

在云服务平台上部署你的应用程序

  • 1.通过Docker Cloud连接到你自己的swarm。有几种不同的连接方式:

    • 从Swarm模式的Docker Cloud Web界面中,选择页面顶部的Swarms,单击要连接的swarm,然后将给定的命令复制粘贴到命令行终端中。

      或者。。。

    • 在Docker for Mac或Docker for Windows上,您可以通过桌面应用菜单直接连接到swarm

      无论哪种方式,都将打开一个终端,其上下文是本地计算机,但其Docker命令会路由到云服务提供商上运行的swarm。您可以直接访问本地文件系统和远程swarm,从而启用纯粹的docker命令。

  • 2.运行docker stack deploy -c docker-compose.yml getstartedlab在云托管swarm上部署应用程序。

    1
    2
    3
    4
    5
    6
    docker stack deploy -c docker-compose.yml getstartedlab

    Creating network getstartedlab_webnet
    Creating service getstartedlab_web
    Creating service getstartedlab_visualizer
    Creating service getstartedlab_redis

    您的应用现在运行在了云服务平台上了。

运行一些swarm命令来验证部署:

你可以使用swarm命令行,就像你之前做的那样,浏览并管理你的swarm。这里有一些你比较熟悉的例子:

  • 使用docker node ls列出节点。
1
2
3
4
5
6
[getstartedlab] ~ $ docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS
9442yi1zie2l34lj01frj3lsn ip-172-31-5-208.us-west-1.compute.internal Ready Active
jr02vg153pfx6jr0j66624e8a ip-172-31-6-237.us-west-1.compute.internal Ready Active
thpgwmoz3qefdvfzp7d9wzfvi ip-172-31-18-121.us-west-1.compute.internal Ready Active
n2bsny0r2b8fey6013kwnom3m * ip-172-31-20-217.us-west-1.compute.internal Ready Active Leader
  • 使用docker service ls列出服务。
1
2
3
4
5
6
[getstartedlab] ~/sandbox/getstart $ docker service ls
ID NAME MODE REPLICAS IMAGE PORTS
x3jyx6uukog9 dockercloud-server-proxy global 1/1 dockercloud/server-proxy *:2376->2376/tcp
ioipby1vcxzm getstartedlab_redis replicated 0/1 redis:latest *:6379->6379/tcp
u5cxv7ppv5o0 getstartedlab_visualizer replicated 0/1 dockersamples/visualizer:stable *:8080->8080/tcp
vy7n2piyqrtr getstartedlab_web replicated 5/5 sam/getstarted:part6 *:80->80/tcp
  • 使用docker service ps <service>查看service的任务列表。
1
2
3
4
5
6
7
[getstartedlab] ~/sandbox/getstart $ docker service ps vy7n2piyqrtr
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
qrcd4a9lvjel getstartedlab_web.1 sam/getstarted:part6 ip-172-31-5-208.us-west-1.compute.internal Running Running 20 seconds ago
sknya8t4m51u getstartedlab_web.2 sam/getstarted:part6 ip-172-31-6-237.us-west-1.compute.internal Running Running 17 seconds ago
ia730lfnrslg getstartedlab_web.3 sam/getstarted:part6 ip-172-31-20-217.us-west-1.compute.internal Running Running 21 seconds ago
1edaa97h9u4k getstartedlab_web.4 sam/getstarted:part6 ip-172-31-18-121.us-west-1.compute.internal Running Running 21 seconds ago
uh64ez6ahuew getstartedlab_web.5 sam/getstarted:part6 ip-172-31-18-121.us-west-1.compute.internal Running Running 22 seconds ago

在云供应商机器上开放服务端口

此时,您的应用作为一个swarm部署在您的云提供商服务器上,正如刚刚运行的docker命令所证明的那样。但是,您仍然需要在云服务器上打开端口,以便:

  • 允许在工作节点上的redis服务和web服务之间进行通信
  • 允许入站流量通过worker节点上的web服务,以便可以在浏览器访问Hello World和Visualizer。
  • 允许运行manager的服务器上的入站SSH流量(这可能已在您的云提供商上设置)

这些是您需要为每项服务公开的端口:

Service 类型 协议 端口
web HTTP TCP 80
visualizer HTTP TCP 8080
redis TCP TCP 6379

具体的做法取决于云服务平台。

我们以Amazon Web Services(AWS)为例。

redis如何持久化数据?

为了使redis服务正常工作,在运行docker stack deploy之前,需要ssh进入manager运行的云服务器,并在/home/docker/中创建data/目录。另一种选择是将docker-stack.yml中的数据路径更改为manager服务器上已存在的一个路径。此示例不包含此步骤,因此示例输出中的redis服务未启动。

示例:AWS

  • 1.登录AWS控制台,转到EC2仪表板,然后单击进入Running Instances查看节点。
  • 2.在左侧的按钮,进入Network & Security > Security Groups

    请参阅getstartedlab-Manager-<xxx>, getstartedlab-Nodes-<xxx>, 和 getstartedlab-SwarmWide-<xxx>的与swarm相关的安全组。

  • 3.为swarm选择“节点”安全组。组名是这样的:getstartedlab-NodeVpcSG-9HV9SMHDZT8C

  • 4.为webvisualizerredis服务添加入站规则,为每个服务设置类型,协议和端口(如上表所示),然后单击保存以应用规则。

提示:当你保存新的规则时,会为IPv4和IPv6地址自动创建HTTP和TCP端口。

  • 5.进入Running Instances列表,获取其中一个worker的公共DNS名称,并将其粘贴到浏览器地址栏中。

就像本教程的前几部分一样,Hello World应用程序显示在端口80上,而Visualizer显示在端口8080上。

迭代和清理

从这里你可以完成你在教程前面部分学到的所有知识。

  • 通过修改docker-compose.yml文件并使用命令docker stack deploy重新发布来扩展你的应用程序。
  • 通过编辑代码更改应用程序行为,然后重新构建并推送新镜像。(要做到这一点,请按照之前用于构建应用程序发布镜像的相同步骤)。
  • 您可以使用docker stack rm命令来拆卸堆栈。例如:
1
docker stack rm getstartedlab

与在本地Docker机器虚拟机上运行swarm的场景不同,不管您是否关闭本地主机,您的swarm和部署在其上的任何应用程序都将继续在云服务器上运行。

前提条件

  • 安装Docker 1.13或更高的版本
  • 按照Part3部分,获取Docker Compos
  • 按照Part4部分,获取Docker Machine
  • 阅读Part1。
  • 学习Part2中的如何创建容器。
  • 确保您已发布了那个推送到仓库的friendlyhello镜像。我们在这里使用该共享镜像。
  • 确保你在part4中设置的机器处于运行状态。运行docker-machine ls来验证这一点。如果机器处于停止状态,运行docker-machine start myvm1来启动manager,然后执行docker-machine start myvm2来启动worker。
  • 让你在Part4创建的swarm处于运行状态并准备就绪。运行docker-machine ssh myvm1 "docker node ls"来验证这一点。如果swarm起来了,那么两个node的状态都是ready。如果不是这样,重新初始化swarm,并按照part4中的方式将worker加入到swarm中。

介绍

在part4中,你学到了如何设置一个swarm,这是一群运行Docker的机器,并为其部署了一个应用程序,其中容器在多台机器上运行。

在这里的Part5中,您将学习到分布式应用程序层次结构的顶部部分:堆栈(stack)。堆栈是一组相互关联的服务,它们可以共享依赖关系,并且可以进行协调和缩放。单个堆栈能够定义和协调整个应用程序的功能(尽管非常复杂的应用程序可能需要使用多个堆栈)。

一些好消息是,从Part3部分开始,在创建Compose文件并使用docker stack deploy时,从技术上讲,您其实一直都在使用堆栈。但这是在单个主机上运行的单个服务堆栈,通常不会发生在生产环境中。在这里,你可以把你学到的东西,使多个服务相互关联,并在多台机器上运行它们。

你做得很好,这就是你的主场!

添加一项新服务并重新部署

将服务添加到我们的docker-compose.yml文件很容易。首先,我们添加一个免费的可视化工具,让我们看看我们的swarm是如何安排容器的。

  • 1.打开docker-compose.yml文件,并用以下内容替换它。确保你的username/repo:tag是正确的:
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
version: "3"
services:
web:
# replace username/repo:tag with your name and image details
image: username/repo:tag
deploy:
replicas: 5
restart_policy:
condition: on-failure
resources:
limits:
cpus: "0.1"
memory: 50M
ports:
- "80:80"
networks:
- webnet
visualizer:
image: dockersamples/visualizer:stable
ports:
- "8080:8080"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock"
deploy:
placement:
constraints: [node.role == manager]
networks:
- webnet
networks:
webnet:

这里唯一新增的东西就是visualizer。注意到这里有两个新的东西:一个volumes键,让visualizer可以访问Docker主机的socket文件,这项服务职能在swarm manager上运行。这是因为这个容器是由Docker创建的一个开源项目构建的,它显示了一个图表中的swarm运行的Docker服务。

我们稍后会详细讨论放置约束和体积。

  • 2.确保你的shell被配置为与myvm1进行通信(完整的例子在这里)。

    • 运行docker-machine ls来列出机器,并确保您已连接到myvm1,如旁边的星号所示。
    • 如果需要,重新运行docker-machine env myvm1,然后运行给定的命令来配置shell。

      在Mac或者Linux上,命令如下:

      1
      eval $(docker-machine env myvm1)

      在Windows命令如下:

      1
      & "C:\Program Files\Docker\Docker\Resources\bin\docker-machine.exe" env myvm1 | Invoke-Expression
  • 3.在manager上重新运行docker stack deploy命令,并且需要更新的任何服务都会更新:

1
2
3
$ docker stack deploy -c docker-compose.yml getstartedlab
Updating service getstartedlab_web (id: angi1bf5e4to03qu9f93trnxm)
Creating service getstartedlab_visualizer (id: l9mnwkeq2jiononb5ihz9u7a4)
  • 4.看一下visualizer。

    你可以看到compose文件中的visualizer运行在了8080端口。通过运行docker-machine ls可以获取每个节点的IP地址信息。分别访问任意一个IP地址的8080端口,你可以看到visualizer的运行效果:

    visualizer的单个副本按照您的预期在manager上运行,并且web的5个实例遍布整个swarm。你可以通过运行docker stack ps <stack>来确认可视化的结果:

    1
    docker stack ps getstartedlab

    可视化器是一个独立的服务,可以在包含它的任何应用程序中运行。它不依赖于其他任何东西。现在让我们创建一个具有依赖关系的服务:提供访问者计数器的Redis服务。

数据持久化

让我们再次通过相同的工作流程来添加用于存储应用程序数据的Redis数据库。

  • 1.保存这个在最后位置添加Redis服务的新的docker-compose.yml文件。确保替换镜像详情部分的username/repo:tag
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
version: "3"
services:
web:
# replace username/repo:tag with your name and image details
image: username/repo:tag
deploy:
replicas: 5
restart_policy:
condition: on-failure
resources:
limits:
cpus: "0.1"
memory: 50M
ports:
- "80:80"
networks:
- webnet
visualizer:
image: dockersamples/visualizer:stable
ports:
- "8080:8080"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock"
deploy:
placement:
constraints: [node.role == manager]
networks:
- webnet
redis:
image: redis
ports:
- "6379:6379"
volumes:
- "/home/docker/data:/data"
deploy:
placement:
constraints: [node.role == manager]
command: redis-server --appendonly yes
networks:
- webnet
networks:
webnet:

Redis在Docker库中有一个官方镜像,并已被授予redis作为镜像的简称,所以在这里没有username/repo符号。Redis端口6379已经由Redis预配置为从容器暴露给主机,在我们的Compose文件中,我们将它从主机展示给全世界,因此,如果您愿意,您可以将任何节点的IP输入到Redis桌面管理器中,并管理此Redis实例。

最重要的是,redis规范中有几件事情使数据在这个堆栈的部署之间持续存在:

  • redis总是在manager上运行,所以它总是使用相同的文件系统。
  • redis在主机文件系统中访问任意目录作为容器内的/data,这是Redis存储数据的地方。

这就是在您的主机物理文件系统中为Redis数据创建“真相源”。如果没有这个,Redis会将其数据存储在容器文件系统中的/data中,如果该容器曾经被重新部署,该数据将被清除。

这个真相的来源有两个组成部分:

  • 放置在Redis服务上的放置约束,确保它始终使用相同的主机。
  • 您创建的容器,允许容器作为./data(位于Redis容器内)访问./data(在主机上)。在容器来来去去时,存储在指定主机上的./data文件仍然存在,从而保持连续性。

您已准备好部署新的供Redis使用的堆栈了。

  • 2.在manager上创建一个./data目录。
1
docker-machine ssh myvm1 "mkdir ./data"
  • 3.确保你的shell被配置为与myvm1进行通信(完整的例子在这里)。

    • 运行docker-machine ls列出机器,并确保你已经连接到了myvm1,由旁边的星号所指示。
    • 如果需要的话,重新运行docker-machine env myvm1,然后运行下面给出的命令来配置shell。

      在Mac或Linux上,命令如下:

      1
      eval $(docker-machine env myvm1)

      在Windows上命令如下:

      1
      & "C:\Program Files\Docker\Docker\Resources\bin\docker-machine.exe" env myvm1 | Invoke-Expression
  • 4.再次运行docker stack deploy
1
$ docker stack deploy -c docker-compose.yml getstartedlab
  • 5.运行docker service ls来验证三个服务处于运行状态:
1
2
3
4
5
$ docker service ls
ID NAME MODE REPLICAS IMAGE PORTS
x7uij6xb4foj getstartedlab_redis replicated 1/1 redis:latest *:6379->6379/tcp
n5rvhm52ykq7 getstartedlab_visualizer replicated 1/1 dockersamples/visualizer:stable *:8080->8080/tcp
mifd433bti1d getstartedlab_web replicated 5/5 orangesnap/getstarted:latest *:80->80/tcp
  • 6.检查位于你的某个节点的网页,例如http://192.168.99.101,然后看访问者计数器的结果,该计数器现在已经存在并将信息存储在Redis上。

另外,请检查任一节点IP地址的端口8080处的可视化工具,并注意查看随webvisualizer工具一起运行的redis服务。