-
Notifications
You must be signed in to change notification settings - Fork 0
/
search.xml
487 lines (234 loc) · 362 KB
/
search.xml
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
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title>关于状态机的思考</title>
<link href="/posts/3b7dc41d/"/>
<url>/posts/3b7dc41d/</url>
<content type="html"><![CDATA[<p>LeetCode 上有这样一道题目:<a href="https://leetcode.cn/problems/biao-shi-shu-zhi-de-zi-fu-chuan-lcof/">表示数值的字符串</a>。题目看似简单,但其最佳解法却蕴含着一种绝妙的工程实践——状态机。</p><p>本文从这道编程题目出发,思考了一些状态机的使用场景,记录下脑暴过程。</p><span id="more"></span><p>题目描述如下:<br><img src="https://fastly.jsdelivr.net/gh/hzhu212/image-store@master/blog/16697365428841669736542461.png" alt=""></p><h1>什么是状态机</h1><p>状态机的全称为有限状态自动机(Finite-State Machine,FSM)。状态机由『状态』和『动作』两部分组成,一个状态经过某个动作之后可以转移到下一个状态。</p><p>例如,一扇门可以通过状态机来描述。</p><p>门的状态包括:</p><ul><li>开/open</li><li>闭/closed</li></ul><p>可以施加的动作包括:</p><ul><li>推/push</li><li>拉/pull</li></ul><p><img src="https://fastly.jsdelivr.net/gh/hzhu212/image-store@master/blog/16697367398861669736739326.png" alt=""></p><h1>状态机的表示</h1><p>由上面的描述可以知道,状态机本质上等效于一个有向图(可以有环):</p><ul><li>状态 <–> 图的顶点</li><li>动作 <–> 图的边</li></ul><p>众所周知,图有多种表示方式,除了画图外,还有邻接表、邻接矩阵等。</p><p>上例中的状态机以邻接表的形式表示如下:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">{</span><br><span class="line"> <span class="attr">"open"</span>: {</span><br><span class="line"> <span class="attr">"pull"</span>: <span class="string">"open"</span>,</span><br><span class="line"> <span class="attr">"push"</span>: <span class="string">"closed"</span></span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"closed"</span>: {</span><br><span class="line"> <span class="attr">"pull"</span>: <span class="string">"open"</span>,</span><br><span class="line"> <span class="attr">"push"</span>: <span class="string">"closed"</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>以邻接矩阵的形式表示如下:</p><p><img src="https://fastly.jsdelivr.net/gh/hzhu212/image-store@master/blog/16697370998991669737099567.png" alt=""></p><p>PS:这只是个特例,状态图一般是非常稀疏的。</p><h1>状态机怎么用</h1><p>以上述 LeetCode 问题的解答为例逐步引出状态机的妙用。</p><p>PS:以下解答摘自官方题解和评论区。</p><h2 id="解法-1-暴力穷举">解法 1. 暴力穷举</h2><p>思路是通过 <code>if...else</code> 逻辑穷举所有可能的情况:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br><span class="line">177</span><br><span class="line">178</span><br><span class="line">179</span><br><span class="line">180</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Solution</span> </span>{</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line"></span><br><span class="line"> <span class="function">bool <span class="title">is_digit</span><span class="params">(<span class="keyword">char</span> a)</span> </span>{ <span class="comment">//判断是否为数字return a >= '0' && a <= '9';</span></span><br><span class="line"> }</span><br><span class="line"> <span class="function">bool <span class="title">is_e</span><span class="params">(<span class="keyword">char</span> a)</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> a == <span class="string">'e'</span> || a == <span class="string">'E'</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="function">bool <span class="title">check</span><span class="params">(string st)</span> </span>{</span><br><span class="line"> <span class="keyword">int</span> count = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">char</span> i : st) {</span><br><span class="line"> <span class="keyword">if</span> (i == <span class="string">'.'</span>) count++;</span><br><span class="line"> <span class="keyword">if</span> (count > <span class="number">1</span>) <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="function">bool <span class="title">isNumber</span><span class="params">(string s)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (check(s) == <span class="keyword">false</span>) <span class="keyword">return</span> <span class="keyword">false</span>;<span class="comment">//确保最多只有一个小数点//前后去空格</span></span><br><span class="line"> <span class="keyword">int</span> i = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">while</span> (i < s.size() && s[i] == <span class="string">' '</span>) {</span><br><span class="line"> i++;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">int</span> end = s.size() - <span class="number">1</span>;</span><br><span class="line"> <span class="keyword">while</span> (end >= <span class="number">0</span> && s[end] == <span class="string">' '</span>) {</span><br><span class="line"> s.erase(s.begin() + end);</span><br><span class="line"> end--;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">int</span> len = s.size();</span><br><span class="line"> <span class="keyword">if</span> (s[i] == <span class="string">'+'</span> || s[i] == <span class="string">'-'</span>) {</span><br><span class="line"> i++;</span><br><span class="line"> <span class="keyword">if</span> (i >= len) <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> <span class="keyword">if</span> (s[i] == <span class="string">'.'</span>) {</span><br><span class="line"> i++;</span><br><span class="line"> <span class="keyword">if</span> (i >= len) <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> <span class="keyword">if</span> (is_digit(s[i])) {</span><br><span class="line"> <span class="keyword">while</span> (i < len && is_digit(s[i])) { i++; }</span><br><span class="line"> <span class="keyword">if</span> (i >= len) <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line"> <span class="keyword">if</span> (is_e(s[i])) {</span><br><span class="line"> i++;</span><br><span class="line"> <span class="keyword">if</span> (i >= len) <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> <span class="keyword">if</span> (s[i] == <span class="string">'-'</span> || s[i] == <span class="string">'+'</span>) i++;</span><br><span class="line"> <span class="keyword">if</span> (i >= len) <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> <span class="keyword">while</span> (i < len && is_digit(s[i])) { i++; }</span><br><span class="line"> <span class="keyword">if</span> (i >= len) <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (is_digit(s[i])) {</span><br><span class="line"> <span class="keyword">while</span> (i < len && is_digit(s[i])) { i++; }</span><br><span class="line"> <span class="keyword">if</span> (i >= len)<span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line"> <span class="keyword">if</span> (is_e(s[i])) {</span><br><span class="line"> i++;</span><br><span class="line"> <span class="keyword">if</span> (i >= len) <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> <span class="keyword">if</span> (s[i] == <span class="string">'-'</span> || s[i] == <span class="string">'+'</span>) i++;</span><br><span class="line"> <span class="keyword">if</span> (i >= len) <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> <span class="keyword">while</span> (i < len && is_digit(s[i])) { i++; }</span><br><span class="line"> <span class="keyword">if</span> (i >= len) <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (s[i] == <span class="string">'.'</span> ) {</span><br><span class="line"> i++;</span><br><span class="line"> <span class="keyword">if</span> (i >= len) <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line"> <span class="keyword">if</span> (is_digit(s[i])) {</span><br><span class="line"> <span class="keyword">while</span> (i < len && is_digit(s[i])) { i++; }</span><br><span class="line"> <span class="keyword">if</span> (i >= len) <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line"> <span class="keyword">if</span> (is_e(s[i])) {</span><br><span class="line"> i++;</span><br><span class="line"> <span class="keyword">if</span> (i >= len) <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> <span class="keyword">if</span> (s[i] == <span class="string">'-'</span> || s[i] == <span class="string">'+'</span>) i++;</span><br><span class="line"> <span class="keyword">if</span> (i >= len) <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> <span class="keyword">while</span> (i < len && is_digit(s[i])) { i++; }</span><br><span class="line"> <span class="keyword">if</span> (i >= len) <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (is_e(s[i])) {</span><br><span class="line"> i++;</span><br><span class="line"> <span class="keyword">if</span> (i >= len) <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> <span class="keyword">if</span> (s[i] == <span class="string">'-'</span> || s[i] == <span class="string">'+'</span>) i++;</span><br><span class="line"> <span class="keyword">if</span> (i >= len) <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> <span class="keyword">while</span> (i < len && is_digit(s[i])) { i++; }</span><br><span class="line"> <span class="keyword">if</span> (i >= len) <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (s[i] == <span class="string">'.'</span>) {</span><br><span class="line"> i++;</span><br><span class="line"> <span class="keyword">if</span> (i >= len) <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> <span class="keyword">if</span> (is_digit(s[i])) {</span><br><span class="line"> <span class="keyword">while</span> (i < len && is_digit(s[i])) { i++; }</span><br><span class="line"> <span class="keyword">if</span> (i >= len) <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line"> <span class="keyword">if</span> (is_e(s[i])) {</span><br><span class="line"> i++;</span><br><span class="line"> <span class="keyword">if</span> (i >= len) <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> <span class="keyword">if</span> (s[i] == <span class="string">'-'</span> || s[i] == <span class="string">'+'</span>) i++;</span><br><span class="line"> <span class="keyword">if</span> (i >= len) <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> <span class="keyword">while</span> (i < len && is_digit(s[i])) { i++; }</span><br><span class="line"> <span class="keyword">if</span> (i >= len) <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (is_digit(s[i])) {</span><br><span class="line"> <span class="keyword">while</span> (i < len && is_digit(s[i])) { i++; }</span><br><span class="line"> <span class="keyword">if</span> (i >= len) <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line"> <span class="keyword">if</span> (s[i] == <span class="string">'.'</span>) {</span><br><span class="line"> i++;</span><br><span class="line"> <span class="keyword">if</span> (i >= len) <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line"> <span class="keyword">if</span> (is_digit(s[i])) {</span><br><span class="line"> <span class="keyword">while</span> (i < len && is_digit(s[i])) { i++; }</span><br><span class="line"> <span class="keyword">if</span> (i >= len) <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (is_e(s[i])) {</span><br><span class="line"> i++;</span><br><span class="line"> <span class="keyword">if</span> (i >= len) <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> <span class="keyword">if</span> (s[i] == <span class="string">'-'</span> || s[i] == <span class="string">'+'</span>) i++;</span><br><span class="line"> <span class="keyword">if</span> (i >= len) <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> <span class="keyword">while</span> (i < len && is_digit(s[i])) { i++; }</span><br><span class="line"> <span class="keyword">if</span> (i >= len) <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (is_e(s[i])) {</span><br><span class="line"> i++;</span><br><span class="line"> <span class="keyword">if</span> (i >= len) <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> <span class="keyword">if</span> (s[i] == <span class="string">'-'</span> || s[i] == <span class="string">'+'</span>) i++;</span><br><span class="line"> <span class="keyword">if</span> (i >= len) <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> <span class="keyword">while</span> (i < len && is_digit(s[i])) { i++; }</span><br><span class="line"> <span class="keyword">if</span> (i >= len) <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (is_e(s[i])) {</span><br><span class="line"> i++;</span><br><span class="line"> <span class="keyword">if</span> (i >= len) <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> <span class="keyword">if</span> (s[i] == <span class="string">'-'</span> || s[i] == <span class="string">'+'</span>) i++;</span><br><span class="line"> <span class="keyword">if</span> (i >= len) <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> <span class="keyword">while</span> (i < len && is_digit(s[i])) { i++; }</span><br><span class="line"> <span class="keyword">if</span> (i >= len) <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> }</span><br><span class="line">};</span><br></pre></td></tr></table></figure><p>不足:</p><ul><li>冗长,可读性差</li><li>难以确认代码逻辑的正确性</li></ul><h2 id="解法-2-状态编程">解法 2. 状态编程</h2><p>比起暴力穷举,通过声明一些全局变量来保存状态,可复用一部分代码逻辑。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Solution</span> {</span></span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line"> <span class="function"><span class="keyword">bool</span> <span class="title">isNumber</span><span class="params">(<span class="built_in">string</span> s)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span>(s.size() == <span class="number">0</span>) <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">//跳过首尾空格int left = 0, right = s.length() - 1;</span></span><br><span class="line"> <span class="keyword">while</span>(left <= right && s[left] == <span class="string">' '</span>){</span><br><span class="line"> left++;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span>(left > right) <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> <span class="keyword">while</span>(left < right && s[right] == <span class="string">' '</span>){</span><br><span class="line"> right--;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">bool</span> isNum = <span class="literal">false</span>;</span><br><span class="line"> <span class="keyword">bool</span> isDot = <span class="literal">false</span>;</span><br><span class="line"> <span class="keyword">bool</span> isEe = <span class="literal">false</span>;</span><br><span class="line"> <span class="keyword">bool</span> isSign = <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">for</span>(<span class="keyword">int</span> i = left; i <= right; i++){</span><br><span class="line"> <span class="keyword">if</span>(s[i] >= <span class="string">'0'</span> && s[i] <= <span class="string">'9'</span>){</span><br><span class="line"> isNum = <span class="literal">true</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 一个'.';e/E后面跟一个整数(不能有'.')</span></span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span>(s[i] == <span class="string">'.'</span> && !isDot && !isEe){</span><br><span class="line"> isDot = <span class="literal">true</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 一个'E'或'e';前面需要出现过数字</span></span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span>((s[i] == <span class="string">'E'</span> || s[i] == <span class="string">'e'</span>) && isNum && !isEe){</span><br><span class="line"> isEe = <span class="literal">true</span>;</span><br><span class="line"> <span class="comment">//// 避免e结尾的情况 e后面得跟一个整数</span></span><br><span class="line"> isNum = <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// '+''-'只能出现在开头或者'E'或'e'的后一位</span></span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span>((s[i] == <span class="string">'+'</span> || s[i] == <span class="string">'-'</span>) && (i == left || s[i - <span class="number">1</span>] == <span class="string">'E'</span> || s[i - <span class="number">1</span>] == <span class="string">'e'</span>)){</span><br><span class="line"> isSign = <span class="literal">true</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 必须以数字结尾</span></span><br><span class="line"> <span class="keyword">return</span> isNum;</span><br><span class="line"> }</span><br><span class="line">};</span><br></pre></td></tr></table></figure><p>不足:</p><ul><li>状态变量多,不容易读懂</li><li>难以确认代码逻辑的正确性</li></ul><h2 id="解法-3-状态机">解法 3. 状态机</h2><p>在解法 2 的基础上进一步抽象,整理出各种状态以及可能遇到的字符的类型。</p><p>可以发现状态和类型都是有限的,并且它们之间的转移关系是确定的。如下图:</p><p><img src="https://fastly.jsdelivr.net/gh/hzhu212/image-store@master/blog/16697375479121669737547501.png" alt=""></p><p>邻接矩阵:</p><p><img src="https://fastly.jsdelivr.net/gh/hzhu212/image-store@master/blog/16697376719151669737671226.png" alt=""></p><p>邻接表:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br></pre></td><td class="code"><pre><span class="line">{</span><br><span class="line"> <span class="attr">"state_initial"</span>: {</span><br><span class="line"> <span class="attr">"char_space"</span>: <span class="string">"state_initial"</span>,</span><br><span class="line"> <span class="attr">"char_number"</span>: <span class="string">"state_integer"</span>,</span><br><span class="line"> <span class="attr">"char_point"</span>: <span class="string">"state_point_without_int"</span>,</span><br><span class="line"> <span class="attr">"char_sign"</span>: <span class="string">"state_int_sign"</span></span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"state_int_sign"</span>: {</span><br><span class="line"> <span class="attr">"char_number"</span>: <span class="string">"state_integer"</span>,</span><br><span class="line"> <span class="attr">"char_point"</span>: <span class="string">"state_point_without_int"</span></span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"state_integer"</span>: {</span><br><span class="line"> <span class="attr">"char_number"</span>: <span class="string">"state_integer"</span>,</span><br><span class="line"> <span class="attr">"char_exp"</span>: <span class="string">"state_exp"</span>,</span><br><span class="line"> <span class="attr">"char_point"</span>: <span class="string">"state_point"</span>,</span><br><span class="line"> <span class="attr">"char_space"</span>: <span class="string">"state_end"</span></span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"state_point"</span>: {</span><br><span class="line"> <span class="attr">"char_number"</span>: <span class="string">"state_fraction"</span>,</span><br><span class="line"> <span class="attr">"char_exp"</span>: <span class="string">"state_exp"</span>,</span><br><span class="line"> <span class="attr">"char_space"</span>: <span class="string">"state_end"</span></span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"state_point_without_int"</span>: {</span><br><span class="line"> <span class="attr">"char_number"</span>: <span class="string">"state_fraction"</span></span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"state_fraction"</span>: {</span><br><span class="line"> <span class="attr">"char_number"</span>: <span class="string">"state_fraction"</span>,</span><br><span class="line"> <span class="attr">"char_exp"</span>: <span class="string">"state_exp"</span>,</span><br><span class="line"> <span class="attr">"char_space"</span>: <span class="string">"state_end"</span></span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"state_exp"</span>: {</span><br><span class="line"> <span class="attr">"char_number"</span>: <span class="string">"state_exp_number"</span>,</span><br><span class="line"> <span class="attr">"char_sign"</span>: <span class="string">"state_exp_sign"</span></span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"state_exp_sign"</span>: {</span><br><span class="line"> <span class="attr">"char_number"</span>: <span class="string">"state_exp_number"</span></span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"state_exp_number"</span>: {</span><br><span class="line"> <span class="attr">"char_number"</span>: <span class="string">"state_exp_number"</span>,</span><br><span class="line"> <span class="attr">"char_space"</span>: <span class="string">"state_end"</span></span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"state_end"</span>: {</span><br><span class="line"> <span class="attr">"char_space"</span>: <span class="string">"state_end"</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>代码实现:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> enum <span class="keyword">import</span> Enum</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Solution</span>:</span></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">isNumber</span>(<span class="params">self, s: <span class="built_in">str</span></span>) -> bool:</span></span><br><span class="line"> <span class="comment"># 定义所有状态</span></span><br><span class="line"> State = Enum(<span class="string">"State"</span>, [</span><br><span class="line"> <span class="string">"STATE_INITIAL"</span>,</span><br><span class="line"> <span class="string">"STATE_INT_SIGN"</span>,</span><br><span class="line"> <span class="string">"STATE_INTEGER"</span>,</span><br><span class="line"> <span class="string">"STATE_POINT"</span>,</span><br><span class="line"> <span class="string">"STATE_POINT_WITHOUT_INT"</span>,</span><br><span class="line"> <span class="string">"STATE_FRACTION"</span>,</span><br><span class="line"> <span class="string">"STATE_EXP"</span>,</span><br><span class="line"> <span class="string">"STATE_EXP_SIGN"</span>,</span><br><span class="line"> <span class="string">"STATE_EXP_NUMBER"</span>,</span><br><span class="line"> <span class="string">"STATE_END"</span></span><br><span class="line"> ])</span><br><span class="line"> <span class="comment"># 定义所有动作</span></span><br><span class="line"> Chartype = Enum(<span class="string">"Chartype"</span>, [</span><br><span class="line"> <span class="string">"CHAR_NUMBER"</span>,</span><br><span class="line"> <span class="string">"CHAR_EXP"</span>,</span><br><span class="line"> <span class="string">"CHAR_POINT"</span>,</span><br><span class="line"> <span class="string">"CHAR_SIGN"</span>,</span><br><span class="line"> <span class="string">"CHAR_SPACE"</span>,</span><br><span class="line"> <span class="string">"CHAR_ILLEGAL"</span></span><br><span class="line"> ])</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">toChartype</span>(<span class="params">ch: <span class="built_in">str</span></span>) -> Chartype:</span></span><br><span class="line"> <span class="keyword">if</span> ch.isdigit():</span><br><span class="line"> <span class="keyword">return</span> Chartype.CHAR_NUMBER</span><br><span class="line"> <span class="keyword">elif</span> ch.lower() == <span class="string">"e"</span>:</span><br><span class="line"> <span class="keyword">return</span> Chartype.CHAR_EXP</span><br><span class="line"> <span class="keyword">elif</span> ch == <span class="string">"."</span>:</span><br><span class="line"> <span class="keyword">return</span> Chartype.CHAR_POINT</span><br><span class="line"> <span class="keyword">elif</span> ch == <span class="string">"+"</span> <span class="keyword">or</span> ch == <span class="string">"-"</span>:</span><br><span class="line"> <span class="keyword">return</span> Chartype.CHAR_SIGN</span><br><span class="line"> <span class="keyword">elif</span> ch == <span class="string">" "</span>:</span><br><span class="line"> <span class="keyword">return</span> Chartype.CHAR_SPACE</span><br><span class="line"> <span class="keyword">else</span>:</span><br><span class="line"> <span class="keyword">return</span> Chartype.CHAR_ILLEGAL</span><br><span class="line"></span><br><span class="line"> <span class="comment"># 定义状态机:邻接表表示法</span></span><br><span class="line"> transfer = {</span><br><span class="line"> State.STATE_INITIAL: {</span><br><span class="line"> Chartype.CHAR_SPACE: State.STATE_INITIAL,</span><br><span class="line"> Chartype.CHAR_NUMBER: State.STATE_INTEGER,</span><br><span class="line"> Chartype.CHAR_POINT: State.STATE_POINT_WITHOUT_INT,</span><br><span class="line"> Chartype.CHAR_SIGN: State.STATE_INT_SIGN</span><br><span class="line"> },</span><br><span class="line"> State.STATE_INT_SIGN: {</span><br><span class="line"> Chartype.CHAR_NUMBER: State.STATE_INTEGER,</span><br><span class="line"> Chartype.CHAR_POINT: State.STATE_POINT_WITHOUT_INT</span><br><span class="line"> },</span><br><span class="line"> State.STATE_INTEGER: {</span><br><span class="line"> Chartype.CHAR_NUMBER: State.STATE_INTEGER,</span><br><span class="line"> Chartype.CHAR_EXP: State.STATE_EXP,</span><br><span class="line"> Chartype.CHAR_POINT: State.STATE_POINT,</span><br><span class="line"> Chartype.CHAR_SPACE: State.STATE_END</span><br><span class="line"> },</span><br><span class="line"> State.STATE_POINT: {</span><br><span class="line"> Chartype.CHAR_NUMBER: State.STATE_FRACTION,</span><br><span class="line"> Chartype.CHAR_EXP: State.STATE_EXP,</span><br><span class="line"> Chartype.CHAR_SPACE: State.STATE_END</span><br><span class="line"> },</span><br><span class="line"> State.STATE_POINT_WITHOUT_INT: {</span><br><span class="line"> Chartype.CHAR_NUMBER: State.STATE_FRACTION</span><br><span class="line"> },</span><br><span class="line"> State.STATE_FRACTION: {</span><br><span class="line"> Chartype.CHAR_NUMBER: State.STATE_FRACTION,</span><br><span class="line"> Chartype.CHAR_EXP: State.STATE_EXP,</span><br><span class="line"> Chartype.CHAR_SPACE: State.STATE_END</span><br><span class="line"> },</span><br><span class="line"> State.STATE_EXP: {</span><br><span class="line"> Chartype.CHAR_NUMBER: State.STATE_EXP_NUMBER,</span><br><span class="line"> Chartype.CHAR_SIGN: State.STATE_EXP_SIGN</span><br><span class="line"> },</span><br><span class="line"> State.STATE_EXP_SIGN: {</span><br><span class="line"> Chartype.CHAR_NUMBER: State.STATE_EXP_NUMBER</span><br><span class="line"> },</span><br><span class="line"> State.STATE_EXP_NUMBER: {</span><br><span class="line"> Chartype.CHAR_NUMBER: State.STATE_EXP_NUMBER,</span><br><span class="line"> Chartype.CHAR_SPACE: State.STATE_END</span><br><span class="line"> },</span><br><span class="line"> State.STATE_END: {</span><br><span class="line"> Chartype.CHAR_SPACE: State.STATE_END</span><br><span class="line"> },</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment"># 程序主体</span></span><br><span class="line"> st = State.STATE_INITIAL <span class="comment"># 进入初始状态</span></span><br><span class="line"> <span class="keyword">for</span> ch <span class="keyword">in</span> s: <span class="comment"># 遍历动作</span></span><br><span class="line"> typ = toChartype(ch)</span><br><span class="line"> <span class="keyword">if</span> typ <span class="keyword">not</span> <span class="keyword">in</span> transfer[st]: <span class="comment"># 如果动作无法使状态流转,则宣告失败</span></span><br><span class="line"> <span class="keyword">return</span> <span class="literal">False</span></span><br><span class="line"> st = transfer[st][typ] <span class="comment"># 流转到下一个状态</span></span><br><span class="line"></span><br><span class="line"> <span class="comment"># 判断是否到达完成状态</span></span><br><span class="line"> <span class="keyword">return</span> st <span class="keyword">in</span> [State.STATE_INTEGER, State.STATE_POINT, State.STATE_FRACTION, State.STATE_EXP_NUMBER, State.STATE_END]</span><br></pre></td></tr></table></figure><p>优势:</p><ul><li>状态表的含义一目了然,含义清晰</li><li>可以遍历状态转移关系确认代码的正确性</li><li>代码量少,逻辑简单</li></ul><h1>为什么要使用状态机</h1><p><strong>状态机不是一个良好的算法,而是一个优秀的工程实践!</strong> 因为状态机本质上还是穷举,既没有降低时间复杂度,也没有降低空间复杂度。</p><p>使用状态机的好处:</p><ul><li>强制理清思路,不容易出错。</li><li>方便修改和扩展。尤其是在工程上线后,不需要修改代码,调整邻接表配置即可。</li><li>高度抽象。将繁琐的 <code>if...else</code> 逻辑抽象成一套公共库,节约开发成本。在 Java、Python 等热门语言中都有大量的状态机库可供调用。</li></ul><h1>状态机的应用</h1><p>有『状态』和『变化』的地方就可以有状态机。</p><ul><li>编译器/解释器<ul><li>如何识别语法错误?例如括号不闭合、<code>int a = /2</code>。</li></ul></li><li>正则表达式<ul><li><code>\d+</code> 与 <code>1</code>,<code>123a</code> match 不上,中间发生了什么?</li></ul></li><li>网络协议,如 TCP<ul><li>握手阶段在等待 ACK 的过程中不接受 SYN、不接受数据。</li></ul></li><li>订单系统<ul><li>付款、取消、等待、投诉等动作会对订单状态产生什么影响?</li></ul></li><li>游戏任务设计<ul><li>和 NPC 对话后再做某项任务与直接做某项任务效果不同。</li></ul></li><li>一种全面、系统的思考问题的方式,可用于做问题建模。例如:<ul><li>Airflow 中的任务状态建模:<ul><li>任务 pending 状态发起重试会发生什么?</li><li>任务运行中状态置成功会发生什么?</li><li>对 pending 任务发起回溯会发生什么?</li><li>对 pending 任务修改调度周期会发生什么?</li></ul></li><li>一些权限系统中的糟糕设计:<ul><li>自己创建的资源,默认没权限,需要自己给自己申请权限</li><li>管理员可以移除任何人的权限,但把自己的权限移除了</li></ul></li></ul></li></ul>]]></content>
<categories>
<category> 技术 </category>
</categories>
<tags>
<tag> 算法 </tag>
</tags>
</entry>
<entry>
<title>如何调优一个大型 Flink 任务</title>
<link href="/posts/80f3ba16/"/>
<url>/posts/80f3ba16/</url>
<content type="html"><![CDATA[<blockquote><p>本文<a href="https://developer.volcengine.com/articles/7065250720647708703">首发于火山引擎开发者社区</a>,获得当期社区征文活动一等奖。</p></blockquote><h1>本文目标</h1><p>随着实时计算的应用越来越广泛,同时实时数仓的概念逐渐深入人心,Flink 作为实时计算领域当之无愧的最优秀框架,其使用范围飞速扩张。对于一个优秀的大数据开发工程师来说,非常有必要熟练掌握 Flink 框架的使用和运维。</p><span id="more"></span><p>本文不会涉及对 Flink 框架的技术剖析,而是侧重于工程实践,力求实用。笔者会结合自己运维多个大型 Flink 任务的经验,对于『如何系统化地调优 Flink 任务、提升性能』给出一套完整的方法论。</p><h1>如何发现性能问题?</h1><p>解决问题的前提是发现问题。那么如何知道一个 Flink 任务是否存在性能问题呢?</p><p>Flink 作业性能不佳时一般有以下一些表现,可根据业务情况综合判断:</p><ul><li>上游 Kafka Topic 出现堆积。正常运行的任务,其上游 Kafka Topic 的 Lag Size 通常为零。如果发现数据持续堆积,说明处理速度跟不上流入速度,可能存在性能问题。但这种情况在数据高峰期也可能发生,可根据业务对延迟的要求决定是否需要优化。</li><li>QPS 曲线抖动。正常运行的任务,其 QPS 曲线一般平滑且稳定,有时也会随着输入 QPS 周期性波动。当发生性能问题时,往往会看到 QPS 曲线有明显抖动。有时 QPS 曲线并未抖动,但仍然出现堆积,同样说明性能不足。</li><li>算子反压。如果任务性能不佳,几乎必定对应着某些算子上发生了反压。可以在 Flink UI 上查看每一个算子的反压情况。某个算子 A 出现反压,意味着这个算子的输出被阻塞,说明下游算子有性能问题,但并不一定是直接下游,因为反压是会连续向上游传导的。从上到下找到第一个没有反压的算子,通常就是性能瓶颈所在的算子。<br><img src="https://fastly.jsdelivr.net/gh/hzhu212/image-store@master/blog/16697355828531669735582444.png" alt=""></li><li>CPU 占用率高且伴随抖动。正常运行的任务,其 CPU 占用率应稳定在较低水平。当占用率过高时(例如 >75%),往往会出现性能问题,此时 CPU 占用率曲线也通常会出现抖动。</li></ul><p>有时候不出现这些现象也不代表任务的性能没问题,因为任务平稳运行可能是靠堆资源堆出来的。本着追求极致的精神,我们应该力求把资源利用率优化到最好。当把计算资源压缩到尽可能低时,此时出现的性能问题才是我们调优和解决的对象。</p><p>那么到底分配多少资源才算合适呢?这里提供一些 QPS per CPU 的经验数据供参考:</p><ul><li>有状态计算:3000 QPS/CPU</li><li>无状态计算:10000 QPS/CPU</li></ul><blockquote><p>有状态处理是指多条数据之间需要维护上下文信息,例如涉及 GROUP BY 语义时,需要使用 Flink 的窗口函数,而窗口中就维护了状态信息。这类处理通常对 CPU 和内存都会造成压力,且窗口越长压力越大。</p></blockquote><p>注意:这里给出的仅仅是粗略的经验值,由于业务情况不同,例如数据是否压缩、序列化格式、是否需要复杂计算等,均会造成一定偏差。另外,CPU 硬件本身的优劣也会造成一定影响。</p><h1>如何拆解性能问题?</h1><p>网上有大量的 Flink 性能调优案例分析,但实际上我们每次遇到性能问题时往往还是无从下手,这是因为没有从案例中总结出系统化的方法论。下面就来解决这个方法论的问题。</p><p>笔者在日常实践中发现,Flink 的性能问题几乎全都可以归结到以下 3 种原因。最妙的是,这 3 种原因是正交的,定位性能问题时不会因为各个因素互相耦合而把脑子搞乱:</p><blockquote><p>经过上一步『问题发现』环节,假设我们已经通过反压找到了性能瓶颈所在的具体算子。</p></blockquote><h3 id="1-算子延迟高">1. 算子延迟高</h3><p>算子延迟高的原因多种多样,例如业务逻辑的复杂度太高、有频繁的磁盘或网络 IO、内存不足频繁 GC。这种情况下增大并行度可能有一定效果,但无法解决根本问题。</p><p>这种情况可以类比为:流水线上每个工人都很生疏,此时扩增人手也许能带来一定的速度提升,但也会带来很大的管理开销,根本的解决办法是提高每个工人的熟练度。</p><h3 id="2-并行度不足">2. 并行度不足</h3><p>有时候即使每个算子上的业务逻辑和算法都已经优化到无懈可击,但由于并行度太低,例如 10 个并行度消费 1000 个 partition,还是会造成作业整体性能不足。</p><p>这种情况可以类比为:流水线上每个工人都很熟练,但人手不够,因此造成生产速度不足,此时应该增加人手,扩大生产线。</p><h3 id="3-数据倾斜">3. 数据倾斜</h3><p>某个算子被分配了过多的数据消费不过来,而其他算子则有闲置的情况。由于作业中往往存在 shuffle 操作,那么此时发生堆积的算子就会成为整个作业的瓶颈。即使不存在 shuffle 操作,数据倾斜的坏处依然存在,一个显著的问题是会造成堆积算子与其余算子之间出现更大的数据乱序。这时无论是增大并行度还是调优算子的延迟都很难奏效,只能去消除数据倾斜。</p><p>这种情况可以类比为:流水线上某些工人被分配了过多的工作量,而其他工人虽然有空闲但却不能到别人的流水线上去帮忙,这种情况只能从工作量的分配上进行改善。</p><p>为了方便理解,列出这 3 种性能原因的类比表:</p><table><thead><tr><th>Flink 任务</th><th>类比为:工厂生产线</th></tr></thead><tbody><tr><td>算子延迟高</td><td>工人不够熟练</td></tr><tr><td>并行度不足</td><td>每个工人都很熟练,但人手太少</td></tr><tr><td>数据倾斜</td><td>每个工人都很熟练,人手也足够,但工作量分配不均匀,工作最多的人拖满了整体进度</td></tr></tbody></table><p>从以上的分析可以看出,这种拆解符合 MECE 原则(不重不漏),即:</p><ul><li>这 3 类原因是正交的,可以独立、互不影响地出现在性能问题中,这意味着『解决一类问题不影响其他问题继续成为性能瓶颈』。</li><li>这 3 类原因是互补的,并且不存在除了这 3 类原因之外的其他原因(后者不能证明,但目前也没有想到反例)。</li></ul><h1>如何优化性能问题?</h1><p>上述 3 类原因虽然全面,但过于粗糙,每种原因背后都存在多重多样的情况,我们优化性能问题的时候需要结合具体情况来分析决策。</p><p>下面简要地给出这 3 类原因的排查方向和优化思路:</p><h3 id="1-算子延迟高-2">1. 算子延迟高</h3><p>算子延迟高的问题可以通过观察算子延迟曲线进行判定,通常在 Flink 的 metrics 指标组中可以找到。</p><p>对于一个良好的实时任务,其各个算子延迟都应该稳定在 10ms 以内,因为磁盘 IO 或同机房内网络 IO 也只是达到这个量级,纯粹的计算更没有理由比这些操作更慢。</p><p>具体来说,算子延迟高的常见原因有:</p><ul><li>业务逻辑复杂,耗 CPU 较多(一般是压缩/解压、序列化/反序列化等造成的);</li><li>内存不足,导致 JVM 频繁发生 GC;</li><li>有较多/较慢的磁盘或网络 IO。</li></ul><p>针对几类原因的分析思路如下:</p><p>1 . 如果怀疑算子的业务逻辑复杂,耗 CPU 较多,那么可以通过 CPU 火焰图定位具体问题。CPU 火焰图可以分析一个进程一段时间内的 CPU 耗时分配在各个函数调用栈上的比例,由此可以定位到业务逻辑中最耗 CPU 的部分。</p><p><img src="https://fastly.jsdelivr.net/gh/hzhu212/image-store@master/blog/16697359190821669735918953.png" alt=""></p><p>2 . JVM GC 问题导致算子延迟高是非常常见的。Flink 任务的 metrics group 中一般都配有 GC 监控。理想的情况下,JobManager 和 TaskManager 应当从不发生 Full GC,如果频繁发生就说明内存管理有问题。</p><p>排除掉 Full GC 之后,算子的 GC 耗时就取决于 Young GC 了,后者的平均耗时一般应该在 100ms 以内,对于不涉及大内存操作(10GB 量级)的任务应该在 10ms 以内。GC 耗时高通常预示着内存不足,但未必是因为分配的内存不够,也可能是 GC 策略不合适导致内存使用效率低,或存在内存泄露等,需要进一步定位。</p><p>3 . 如果怀疑延迟是由于磁盘 IO 造成的,那么可以找到某些 Task Manager 查看其单机磁盘监控,是否有磁盘 IO 次数过高,或者数据 size 过大。</p><p>如果怀疑延迟是由于网络 IO 造成的,那么可以查看对应 API 提供方的延迟数据。例如任务访问了 Redis、HBase 等外部资源,那么这些基础设施本身都会有相应的延迟监控,可以从中判定延迟的来源。</p><h3 id="2-并行度不足-2">2. 并行度不足</h3><p>并行度不足的问题比较容易发现,一般可以观察任务总体的 CPU 占用,以及各个 Task Manager/Container 的 CPU 占用。如果 CPU 占用率一直接近 100%,甚至处于超发状态,且排除了算子延迟高的问题,那么通常就是并行度不足造成的。</p><p>并行度不足的解决方法很简单,就是增加资源。一般是增加 Task Manager 的个数,从而扩大并行度。</p><h3 id="3-数据倾斜-2">3. 数据倾斜</h3><p>数据倾斜的问题一般出现在发生 shuffle 操作的任务中,典型的就是 GROUP BY 语义。</p><p>可以通过观察算子反压现象加以定位,如果在某个算子的所有实例中只有部分实例出现反压现象,那么这些实例很可能遇到了数据倾斜。但也有可能是这些实例对应的节点负载过高,被动造成了性能问题,这种情况只需要简单排查一下这个节点的资源监控即可。</p><p>也可以在 Flink UI 中单独查看每一个 SubTask 的 Records Sent 和 Bytes Sent 值,观察是否有部分 SubTask 吞吐量明显更大的情况。</p><p>如果有分 Task Manager 的 CPU 监控的话,也可以作为参考,看是否存在个别 Task Manager 资源占用明显高于其他的情况,如果有则很可能这些 Task Manager 上发生了数据倾斜。</p><p>数据倾斜的治理思路大家并不陌生,大致跟离线数据倾斜相同,有以下几个解决方案:</p><ul><li>观察倾斜的数据是否为脏数据,如果是,则在 shuffle 操作之前将倾斜的数据清洗掉;</li><li>将引起倾斜的 hot key 附加一个随机数,在 shuffle 之前将其打散,待处理完成之后再清洗恢复;</li><li>将引起倾斜的 hot key 单独起一个任务进行处理,待处理完成后再将所有结果合并起来。</li></ul><hr><p>以上优化思路看起来简单直接,但事实上,上述 3 大类原因中的每一种细分情况都需要大量的知识和经验来辅助判断,需要多从实践中总结和学习。</p><h1>一些实用的『反向操作』</h1><p>Flink 官方文档中推荐了一些最佳实践,但许多并不适用于大型 Flink 任务(并行度 > 1000),这里总结为『反向操作』(即不符合官方推荐做法的意思)。</p><p>这个列表不宜直接遵守,其目的是,当你在运维大型 Flink 任务时,如果发现了无论如何也解决不了的性能/稳定性问题,可以参考一下,思考是否掉进了『最佳实践』的陷阱里。</p><h3 id="1-慎用-CheckPoint">1. 慎用 CheckPoint</h3><p>Flink 的 CheckPoint 是一个非常有用的功能,可以在任务失败之后完全恢复到最近一次 CheckPoint 的状态,用于实现 end to end 的 exactly once 语义。</p><p>但在一些大型 Flink 任务中,有时候维护的 state 会非常重,导致每次 CheckPoint 都需要将百 GB 甚至 TB 量级的数据写入到磁盘中,任务性能被严重拖慢,且 CheckPoint 容易生成失败或超时。</p><p>需要知道的是,开启 CheckPoint 并不一定能达成端到端的 exactly once 语义,这取决于下游的接收方是不是幂等的。如果不是,当任务失败重启时,CheckPoint 反而会导致数据重复消费。对于某些业务,这并没有比数据丢失好到哪儿去。</p><p>所以,当你的 Flink 任务不 care 偶然且少量的数据丢失时,关闭 CheckPoint 不仅没有坏处,反而可以提升作业性能。</p><h3 id="2-慎用-EventTime">2. 慎用 EventTime</h3><p>对于 Flink 的 Timing System,我们一般选择 ProcessingTime 或 EventTime 的其中一个。具体如何选择通常基于业务来判断,例如你需要按照用户下单的时间来处理数据,那么毫无疑问应当采用 EventTime 配合 Watermark。否则,出于性能考虑默认采用 ProcessingTime。</p><p>Flink 官方文档对 EventTime 做了浓墨重彩的介绍,但却没有强调一个重要的点,那就是 EventTime 对作业性能有着严重的损耗,尤其是对于存在 shuffle 操作的大型 Flink 任务。这是因为 EventTime 的乱序以及 Watermark 的传导和对齐机制会导致数据在 shuffle 操作两端出现严重的等待、滞后、进而拥堵。这种情况下,如果任务用到了窗口状态,那么内存占用会持续上涨,最终崩溃。</p><p><img src="https://fastly.jsdelivr.net/gh/hzhu212/image-store@master/blog/16697360948681669736094280.png" alt=""></p><p>如果你已经遇到了这个问题,那么应该把 EventTime 换成 ProcessingTime。虽然这不太符合业务要求,但至少能够让任务平稳运行。为了满足业务需求,你可能需要基于 Flink 输出的数据的时间字段进行额外的处理。</p><h1>总结</h1><p>本文介绍了大型 Flink 任务的运维和调优经验,立足实践,力求实用。</p><p>本文精华部分在于如何拆解 Flink 的性能问题,具体从以下三个正交的角度入手:</p><ul><li>算子延迟高</li><li>并行度不足</li><li>数据倾斜</li></ul><p>以及对这三类问题的具体优化思路。</p><p>最后介绍了一些不符合最佳实践的『反向操作』,展示了工程实践中有时不得不『屈服于现实』的无奈。</p>]]></content>
<categories>
<category> 技术 </category>
</categories>
<tags>
<tag> 大数据 </tag>
<tag> Flink </tag>
<tag> 实时数仓 </tag>
</tags>
</entry>
<entry>
<title>一文理解 HyperLogLog(HLL) 算法</title>
<link href="/posts/365f8e92/"/>
<url>/posts/365f8e92/</url>
<content type="html"><![CDATA[<p>HyperLogLog(HLL) 算法是一种估算海量数据基数的方法,被广泛用于各个数据库产品中。</p><p>与精确的基数统计算法相比,HLL 具备可合并性 (mergeability) ,因而可以方便地对海量数据进行并行计算,被广泛地用于大数据多维分析场景中。例如分别统计一款 APP 每个小时的 UV 以及全天的 UV,这类问题就非常适合使用 HLL 算法。</p><p>本文将会由浅入深,从基本概念讲起,引导读者从直观上理解 HLL 算法背后蕴含的基本思想。</p><span id="more"></span><h1>基数统计</h1><p>基数 (Cardinality) 是指一个字段所包含的不同取值的个数,有时候也称为 Distinct Values,简写为 DV。</p><p>举个例子:</p><ul><li>序列 [1, 2, 3, 4] 的基数为 4,因为包含 4 个不同的取值。</li><li>序列 [1, 2, 3, 1, 2] 的基数为 3,虽然包含 5 个元素,但其中的 1, 2 分别重复了一次。</li></ul><p>最直观的基数统计方法是利用 HashSet:将序列中的所有值依次添加到 HashSet 中,最后统计 HashSet 中值的个数即可。用 Python 代码实现如下:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">get_dv</span>(<span class="params">stream</span>):</span></span><br><span class="line"> s = <span class="built_in">set</span>()</span><br><span class="line"> <span class="keyword">for</span> value <span class="keyword">in</span> stream:</span><br><span class="line"> s.add(value)</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">len</span>(s)</span><br></pre></td></tr></table></figure><p>既然如此,为什么我们不使用 HashSet 来计算基数呢?</p><p>原因在于计算成本。当要统计的数据非常多时,HashSet 将会占用很大的内存,以至于资源耗尽也无法完成计算,这种情况在大数据场景下非常常见。在 HashSet 的基础上,有一个可以节省资源的改进方案,就是采用 bitmap,但 bitmap 只是把问题延缓了,仍然没有根本性地解决问题。</p><p>事实上,我们统计基数时往往并不要求分毫不差,只需要给出一个具有误差边界的粗略值即可。那么在这种前提下能否节省计算资源呢?</p><p>HyperLogLog(HLL) 就是这样一种算法,可以在计算结果的精确程度和资源占用之间取得一种平衡。</p><p>让我们从一些浅显的问题着手,逐步揭开 HLL 算法的神秘面纱。</p><h1>从概率视角看计数方法</h1><p>常规的计数方法会维护一个列表,每到来一条数据记录一下。这种计数是精确的,但代价是必须维护一个越来越长的列表。</p><p>概率论为我们提供了另外一种看待计数的视角:</p><p>$$ 观测到小概率事件发生(概率 p) → 类似的事情重复过很多次了(次数 N)$$</p><p>其中蕴含着一个粗略的定量关系:</p><p>$$N = 1/p$$</p><p>举个例子:</p><blockquote><p>在摇骰子猜大小的游戏中,三个骰子同时为 6 点的概率很小,为 1/(6^3)。假如在某场游戏中摇出了三个 6 点,猜猜一共摇了几次?</p><p>答:大概 6^3=216 次</p></blockquote><p>更进一步的例子:</p><blockquote><p>有一个抛硬币游戏,规则如下:玩家每次抛掷一枚均匀的硬币,正面与反面朝上的概率均> 为 1/2,每次抛掷都记一分。如果正面朝上,则继续抛掷;如果反面朝上,则游戏结> 束。</p><p>问1:在一局游戏中得 5 分的概率是多少?<br>答1:得 5 分意味着抛掷序列为「正正正正反」,概率为 (1/2)^5 = 1/32</p><p>问2:在一组游戏中,最高得分是 5 分,请问玩了多少局?<br>答2:大概 32 局</p></blockquote><p>如果把硬币的正反两面分别记作 0、1 的话,那么 HyperLogLog 的计数原理就呼之欲出了:</p><p>对于每一条待统计的数据(例如 user_id),计算其 hash 值并写成二进制形式(0-1 串),然后将其看作一局抛硬币游戏的记录,其中:</p><ul><li>0 代表硬币正面朝上。</li><li>1 代表硬币反面朝上。</li></ul><p>例如 hash(uid_345678)=00010010,意味着这局抛硬币游戏出现连续 3 次正面朝上,第 4 次反面朝上,游戏结束,最终得分为 4。第 4 位以后的序列不影响得分,可以忽略。</p><p>按照上述步骤,计算每条数据对应的「得分」,并找出其中的最高分 μ。那么这组数据的基数的期望为:</p><p>$$N = 2^μ$$</p><p>这就是利用概率论来估算基数所依据的基本原理。</p><p>在上述过程中涉及了一个重要步骤,就是将每个待观察的数据进行 hash 操作。为什么需要 hash 操作,而不是直接观察数据本身对应的二进制串呢?</p><p>这是因为游戏要求每次取 0 或 1 的概率是均等的,都是 0.5(这样整局游戏是一个<a href="https://en.wikipedia.org/wiki/Bernoulli_process">伯努利过程</a>)。换言之,要确保观察的 0-1 串足够随机才行。如果不做 hash 的话则无法保证随机性,例如对于 int 类型的数据,较小的值如 0、1、2 的二进制串中包含很长的连续 0,导致得分很高,这显然是错误的。</p><p>HLL 中实际使用的 hash 算法为 <a href="https://en.wikipedia.org/wiki/MurmurHash">MurmurHash</a>,其主要优势是随机性强和快速。</p><p>此外,比特币中使用 hash 值的前导零的个数来定义挖矿时的难度值 <a href="https://en.bitcoin.it/wiki/Difficulty">Difficulty</a>,其蕴含的思想是完全相同的。前导零个数越多,意味着要尝试的 hash 计算次数越多,对应着基数越大,其工作量/难度也越高。</p><h1>MVP 版基数估计算法</h1><p>根据上面的讨论,实现一个简单的基数估计算法如下:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">get_dv</span>(<span class="params">stream</span>):</span></span><br><span class="line"> max_z = <span class="number">0</span></span><br><span class="line"> <span class="keyword">for</span> value <span class="keyword">in</span> stream:</span><br><span class="line"> h = murmur_hash(value)</span><br><span class="line"> z = leading_zeros_count(h)</span><br><span class="line"> <span class="keyword">if</span> z > max_z:</span><br><span class="line"> max_z = z</span><br><span class="line"> <span class="keyword">return</span> <span class="number">1</span> << (max_z + <span class="number">1</span>)</span><br></pre></td></tr></table></figure><p>这个算法足够简单,而且理论上是没问题的。但实际上在数据量较小的情况下误差非常大,因为前导零个数的偶然性太大了。就好比抛硬币总是容易出现某一次运气爆棚的情况,严重拉高总体的估算值。</p><p>设想一下,假设仅输入一条数据,这个数据刚好有 1 个前导零,那么最终估计出的基数为 2^(1+1)=4,如果刚巧遇到更多的前导零,那么偏差会更大。最关键的是,这些偏差无法靠算法本身来控制,准确度全靠运气。</p><h1>LogLog 算法</h1><p>为了解决 MVP 算法不稳定、运气成分大的问题,一种最简单的思路就是「分拆计算求平均值」,也就是把输入数据均分为 $m$ 份(称为桶),每一个桶分别应用 MVP 算法,最终得分 <strong>$\bar{\mu}$ 为各桶得分的平均值</strong>。这就是 LogLog 算法所采用的思路,LogLog 是早于 HyperLogLog 诞生的一种算法。</p><p>LogLog 算法的计算公式可表示为:</p><p>$$N=\alpha \cdot m \cdot 2^{\bar{\mu}}$$</p><p>其中,$m$ 为分桶个数,$\bar{\mu}$ 为各桶最高得分的平均值,$\alpha$ 为修正系数,用于修正算术平均数带来的系统偏差。$\alpha$ 的计算规则如下:</p><p><img src="https://fastly.jsdelivr.net/gh/hzhu212/image-store@master/blog/16697329140171669732913934.png" alt=""></p><!-- $$\alpha_m=\left( m\int_0^\infty \left(log_2{\frac{2+u}{1+u}} \right)^m \text{d}u \right)^{-1} ≈ \lbrace{}$$ --><p>根据算法的特点,通常将分桶数 $m$ 设为 2 的整数次幂。例如 $m=64=2^6$,此时可以通过 hash 值的前 6 个 bit 来表示桶编号。从第 7 个 bit 开始统计前导零个数。</p><h1>HyperLogLog 算法</h1><p>LogLog 算法通过「分桶求平均值」的方式提高了估算结果的稳定性,使得算法更能抵御偶然性带来的影响。</p><p>但这么做仍然不够,因为算术平均数有一个天然的缺陷,就是容易受到极大/极小值的影响,一个离群点可能把最终结果严重带偏。例如老板月入 100000 元,9 个员工均月入 3000 元,那么平均收入就是 (100000+3000*9)/10=12700 元,这距离群里中的大多数(即 9 个员工)相差甚远,不能反映普遍情况。</p><p>如果将算术平均数改为调和平均数就可以解决这个问题,调和平均数的计算公式如下:</p><p>$$\bar{x}=\frac{1}{\frac{1}{n}\sum_n\frac{1}{x_i}} = \frac{n}{\sum_n\frac{1}{x_i}}$$</p><p>使用调和平均数计算出的平均收入为 10/(1/100000+9/3000)=3322,比较接近群体中的普遍情况。</p><p>HyperLogLog 算法对于 LogLog 算法的重要改进就是把算术平均数改成了调和平均数。同时,HLL 不是先求平均得分,再计算指数(因为这会导致离群点的效应指数级放大),而是先计算出每个桶的基数,然后求调和平均数。</p><p>HLL 统计基数的公式如下:</p><p>$$ N=\alpha\cdot m \cdot \frac{m}{\sum_{i=1}^m \frac{1}{2^{\mu_i}}} $$</p><p>在实际使用中,为了提高小样本的准确度,HLL 在上述公式计算结果的基础上还进行了一次修正。完整计算流程参见下图:</p><p><img src="https://fastly.jsdelivr.net/gh/hzhu212/image-store@master/blog/16697331667431669733165968.png" alt=""></p><p>前面提到过,分桶数越多越能抵御偶然效应带来的影响,使得基数估计的结果更准确。那么可以想到,HLL 算法的估算精度(用<a href="https://en.wikipedia.org/wiki/Coefficient_of_variation">相对标准误差 RSD</a> 来表示)与分桶数 m 之间存在负相关关系。其定量关系如下:</p><p>$$RSD = \frac{1.04}{\sqrt{m}}$$</p><p>有了这个关系,我们可以轻易地通过想要达到的误差精度来决定分桶的个数。</p><h2 id="合并">合并</h2><p>HLL 算法的一个重要特点是可合并性,使其能够预先统计各个子集的基数,然后汇总得到总体基数,极大地提高了统计效率。</p><p>HLL 结构体的合并过程非常简单,这是因为每个 HLL 结构体本质上就是一个桶数组。假设要将桶数组 a 和 b 合并成桶数组 c,只需要从 a、b 的对应位置取最大值即可,使用 Python 代码描述如下:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">hll_merge</span>(<span class="params">a, b</span>):</span></span><br><span class="line"> m = <span class="built_in">len</span>(a)</span><br><span class="line"> c = [<span class="number">0</span>]*m</span><br><span class="line"> <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(m):</span><br><span class="line"> c[i] = <span class="built_in">max</span>(a[i], b[i])</span><br><span class="line"> <span class="keyword">return</span> c</span><br></pre></td></tr></table></figure><p>合并后的桶数组按照上文中的公式估算基数即可。</p><h1>示例</h1><p>Rob Grzywinski 创建了一个 HyperLogLog 算法的 <a href="http://content.research.neustar.biz/blog/hll.html">demo</a> 网站,可以直观地理解算法的计算过程。</p><p>下图中显示,输入数据 value=8188163,其 MurmurHash 值为 84796297,二进制串见图。此外,图中 m=64 代表有 64 个桶,每个桶中的最高得分维护在一个表格中。</p><p>二进制串的最后 6 bit 用于表示桶 (Register) 编号,即 <code>001001</code>(9),所以当前数据划到第 9 桶。为什么用 6 bit 表示桶编号?因为这样刚好足够区分 64 个桶。如果要求桶数更多,则相应地需要更多 bit。</p><p>紧接着的 15 bit 用于统计得分(从右端开始),本例中得分为 2,因此第 9 个桶中记录 2。为什么只统计 15 个 bit 呢?因为工程实现中 register 结构体是有空间限制的,此处每个 register 占用 4 bit,记录范围为 0~15,所以能容纳的最大数字就是 15,如果得分大于 15 就记录不上了。</p><p>可以想象,每个桶占用 4 bit 的话能够统计的数据量非常有限,当所有桶的得分都为 15 时达到上限,约为 0.709*64*2^15=1486880 个。但考虑到整个 HLL 结构体仅占用了 64*4bit=32byte 的存储,空间效率是非常惊人的。</p><p><img src="https://fastly.jsdelivr.net/gh/hzhu212/image-store@master/blog/16697333217441669733321293.png" alt=""></p><p>从上面的推导中可以看到,一个 HLL 结构体所能统计的基数范围与它占用的空间存在两重 Log 关系,这也是算法被称为 LogLog 的原因。</p><p>回到上面的例子就是,基数容量 $1486880 ∝ 2^{2^4}$,其中的"4"代表每个桶占用 4bit 空间。</p><h1>业界案例</h1><p>在工程实践中,HLL 结构体的桶数和容量通常是可调参数,当数据量增大或者要求更高的精确度时,可以调高容量。</p><p>下面列出一些业界使用 HLL 算法的例子:</p><p>1 . <a href="https://datasketches.apache.org/">Apache DataSketch</a></p><p>Apache DataSketch 算法族中包含 HyperLogLog 的实现,该算法族被广泛用于许多大数据基础组件中,用于支持基数、分位数等的快速计算。例如:</p><ul><li>Hive/Spark 通过官方 <a href="https://github.com/apache/datasketches-hive">UDXF</a> 的方式使用 DataSketch;</li><li>Apache Druid 通过<a href="https://druid.apache.org/docs/latest/development/extensions-core/datasketches-extension.html">官方插件</a>的形式引入 DataSketch 扩展;</li><li>PostgreSQL 也通过<a href="https://github.com/apache/datasketches-postgresql">插件</a>形式引入 DataSketch 算法。</li></ul><p>2 . Redis</p><p>Redis 中使用 <code>PFCOUNT</code> 命令来调用 HLL 算法。</p><p>其 HLL 结构使用了 2^14=16384 个桶,hash 值采用 64bit 表示,除了桶编号之外剩余的 50 bit (64-14=50) 全部用于统计得分。为了确保桶中记录的分数最大范围高于 50,每个桶需要占用 6 bit 空间(2^6>50)。这样,总体的空间占用为 16384*6bit=12KB。</p><p>当数据量很少时会存在大量的空桶,此时出于优化目的,可以借助稀疏存储的表示方法来压缩空间,能够取得数倍到上千倍的压缩率。</p><p>3 . ClickHouse</p><p>ClickHouse 中的 <a href="https://clickhouse.com/docs/zh/sql-reference/aggregate-functions/reference/uniq/#agg_function-uniq">uniq</a> 函数背后采用的是 HLL 算法。</p><p>4 . Doris<br>Doris 中 <a href="https://cloud.baidu.com/doc/DORIS/s/Ikmealrom#approx_count_distinct">approx_count_distinct</a> 函数背后采用的也是 HLL 算法。原理类似,不再赘述。</p><h1>延伸阅读</h1><ul><li>HLL 算法中每个桶仅记录该桶中最大的得分,而忽略其他得分,因此绝大多数数据完全没有留下任何痕迹就被丢弃了,但最终估计出来的结果却能体现出这些数据的存在,有些匪夷所思。这一点其实符合信息论的思想,即概率越小的事件所携带的信息量越大($S=-log_2p$)。由于得分最高的数据出现的概率最低,因此携带的信息量最大,意味着我们仅捕获海量数据中携带最大信息量的数据,而丢弃其他信息量较少的数据。增加分桶个数可以让捕获到的信息量线性增长,因此能够提高最终的精度。</li><li>通过概率论来计数的基本思想是根据「实验观察」与「概率理论」反推出「背后的事实」,而不是直接研究「背后的事实」,这种思想被广泛用于除 HLL 之外的很多地方,例如:<a href="https://en.wikipedia.org/wiki/Monte_Carlo_method#Overview">利用蒙特卡洛方法估算圆周率</a>、<a href="https://en.wikipedia.org/wiki/Sunrise_problem">太阳升起问题</a>。</li></ul><h1>参考</h1><ul><li>论文 <a href="http://algo.inria.fr/flajolet/Publications/FlFuGaMe07.pdf">HyperLogLog: the analysis of a near-optimal cardinality estimation algorithm</a></li><li><a href="http://dqyuan.top/2018/08/22/hyperloglog.html">探索HyperLogLog算法(含Java实现)</a></li><li><a href="http://content.research.neustar.biz/blog/hll.html">Sketch of the Day: HyperLogLog — Cornerstone of a Big Data Infrastructure</a></li></ul>]]></content>
<categories>
<category> 技术 </category>
</categories>
<tags>
<tag> 数据结构 </tag>
<tag> 数据库 </tag>
<tag> 大数据 </tag>
</tags>
</entry>
<entry>
<title>DataSketches 算法概述</title>
<link href="/posts/b4f47739/"/>
<url>/posts/b4f47739/</url>
<content type="html"><![CDATA[<p>在数据领域,有几类经典的查询场景,这些查询在小数据量下很容易做到,但一旦数据量扩大传统思路将变得不可行,必须采用特定的数据结构与算法来支持,这就是今天要讨论的 DataSketches 算法族。</p><span id="more"></span><h1>背景</h1><p>这几类经典的查询场景包括:</p><ol><li>基数统计 // Unique User or Count Distinct</li></ol><ul><li>统计某段时间内访问某网站的 UV 数</li><li>统计某段时间内既访问了页面 A 又访问了页面 B 的 UV 数</li><li>统计某段时间内访问了页面 A 但未访问页面 B 的 UV 数</li></ul><ol start="2"><li>分位数 // Quantile & Histogram</li></ol><ul><li>统计某段时间内访问页面 A 与页面 B 各自的等待时长 95 分位数,以及整体等待时长 95 分位数</li></ul><ol start="3"><li>TopN 统计 // Most Frequent Items</li></ol><ul><li>统计某段时间内播放量最多的 10 个视频(热榜列表)</li></ul><ol start="4"><li>随机采样 // Sampling</li></ol><ul><li>从一个数据流中构造一个大小为 k 的随机样本,无论数据流有多长。</li></ul><p>这几类问题在数据量不大的情况下都是非常容易处理的。但一旦数据到达 Billion 量级,常规算法可能要花费数小时甚至数天的时间,并且即使提供充足的计算资源也于事无补,因为这几类问题都难以并行化处理。</p><p><a href="https://datasketches.apache.org/">DataSketches</a> 就是为了解决大数据场景下的这几类典型问题而诞生的一组算法,最初由雅虎开源。DataSketches 算法以牺牲查询结果的精确性为代价,可以在极小的空间内并行、快速地解决上述几类问题。</p><h1>核心思想</h1><p>DataSketches 的字面含义为『数据草图』,其基本思想是把一个源源不断的数据流汇总成一个数据结构,也就是 sketch(草图),之后可以从 sketch 中估算 (evaluate) 出需要的统计信息。</p><p>sketch 一般具有以下几个特征:</p><ol><li><p>Single-Pass / One-Touch<br>可以把 sketch 理解为一个状态存储器,它时刻承载着数据流迄今为止的所有历史信息,因此 sketch 通常是 single-pass 的,只需要遍历一遍数据即可取得所需的统计信息。</p></li><li><p>占用空间小<br>传统的统计方式需要维护一个巨大的数据列表,且随着数据的输入越来越大。sketch 可以在很小的常量空间内摄入海量的数据,通常在 KB 量级。这使得 sketch 在海量数据的统计中非常有优势。<br><img src="https://fastly.jsdelivr.net/gh/hzhu212/image-store@master/blog/16697312156751669731215351.png" alt=""></p></li><li><p>可合并性 (mergeability)<br>这使得 sketch 可以自由地分布式并行处理大量数据,因此具有快速、高效的优势。例如,在统计基数 (Distinct Value) 时,sketch 可以轻易地将局部统计结果合并为全局统计结果,而直接计数则做不到这一点:<br>$$DV(uid | city=北京or上海) ≠ DV(uid | city=北京) + DV(uid | city=上海)$$<br>$$sketch(uid | city=北京or上海) = sketch(uid | city=北京) + sketch(uid | city=上海)$$<br>PS: 第二个式子中的加号代表 sketch 的合并操作。<br><img src="https://fastly.jsdelivr.net/gh/hzhu212/image-store@master/blog/16697313486761669731347974.png" alt=""></p></li><li><p>不提供精确的统计值,但可设定误差范围<br>sketch 为了节省空间必然会丢失一部分信息,因此统计结果不可能是完全精确的。但在现实中,许多分析和决策也并不要求数据是绝对精确的,有时候知道某个统计数据在 1% 的误差范围内往往跟精确的答案一样有效。sketch 可以在计算复杂度与误差之间进行权衡,足以满足大数据场景下大部分的统计需求。</p><p>一个 sketch 算法的使用流程通常如下(以 HLL 为例):<br><img src="https://fastly.jsdelivr.net/gh/hzhu212/image-store@master/blog/16697313906761669731390065.png" alt=""></p></li></ol><h1>算法一览</h1><p>基数统计 // Unique User or Count Distinct:</p><ul><li><a href="https://datasketches.apache.org/docs/HLL/HLL.html">HLL Sketch</a>(详情参考下一篇博文)</li><li><a href="https://datasketches.apache.org/docs/CPC/CPC.html">CPC Sketch</a></li><li><a href="https://datasketches.apache.org/docs/Theta/ThetaSketchFramework.html">Theta Sketch</a></li><li><a href="https://datasketches.apache.org/docs/Tuple/TupleOverview.html">Tuple Sketch</a></li></ul><p>分位数 // Quantile & Histogram:</p><ul><li><a href="https://datasketches.apache.org/docs/Quantiles/OrigQuantilesSketch.html">Original QuantilesSketch</a> + <a href="https://datasketches.apache.org/docs/KLL/KLLSketch.html">KLL Floats Sketch</a></li><li><a href="https://arxiv.org/pdf/1902.04023.pdf">T-Digest</a></li></ul><p>TopN 统计 // Most Frequent Items:</p><ul><li><a href="https://datasketches.apache.org/docs/Frequency/FrequentDistinctTuplesSketch.html">Items Sketch</a>(基于 Misra-Gries 算法)</li></ul><p>随机采样 // Sampling:</p><ul><li><a href="https://datasketches.apache.org/docs/Sampling/ReservoirSampling.html">Reservoir Sampling</a></li></ul><h1>实际应用</h1><p>直接使用:</p><ul><li>Hive/Spark (<a href="https://github.com/apache/datasketches-hive">UDXF</a>)</li><li>Druid (<a href="https://druid.apache.org/docs/latest/development/extensions-core/datasketches-extension.html">Druid DataSketches Extension</a>)</li><li>PostgreSQL (<a href="https://github.com/apache/datasketches-postgresql">extension</a>)</li></ul><p>使用相关算法,但未引用代码库(删除线表示未使用 DataSketch 相关算法):</p><ul><li>ClickHouse (<a href="https://clickhouse.com/docs/zh/sql-reference/aggregate-functions/reference/uniq/#agg_function-uniq">uniq</a>: HLL; <a href="https://clickhouse.com/docs/zh/sql-reference/aggregate-functions/reference/quantile/">quantile</a>: Reservoir Sampling; <s>topK: Space-Saving</s>)</li><li>Doris (<a href="https://cloud.baidu.com/doc/DORIS/s/Ikmealrom#approx_count_distinct">approx_count_distinct</a>: HLL; <s><a href="https://cloud.baidu.com/doc/DORIS/s/Ikmealrom#percentile_approx">percentile_approx</a>: T-Digest; <a href="https://cloud.baidu.com/doc/DORIS/s/Ikmealrom#topn">topn</a>: Space-Saving</s>)</li><li>Redis (<s>基数统计:1. SCARD-基于集合;2. BITCOUNT-基于bitmap;</s> 3. PFCOUNT-HLL。分位数:<s>TDIGEST-基于 T-Digest</s>)</li></ul>]]></content>
<categories>
<category> 技术 </category>
</categories>
<tags>
<tag> 数据结构 </tag>
<tag> 数据库 </tag>
<tag> 大数据 </tag>
</tags>
</entry>
<entry>
<title>一文理解Python导入机制</title>
<link href="/posts/b9859a94/"/>
<url>/posts/b9859a94/</url>
<content type="html"><![CDATA[<p>Python 的 import 机制是最令用户困惑的地方之一,在实践中非常容易出错,相信被 <code>ImportError</code> 和 <code>ModuleNotFoundError</code> 折磨过的同学都对此深有体会。本文完整地梳理 Python 的各种导入逻辑,力求在实践中避坑并提出一些最佳实践。</p><span id="more"></span><p>PS:本文中的导入语句均以如下所示的目录结构为例进行演示:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">package/</span><br><span class="line"> __init__.py</span><br><span class="line"> subpackage1/</span><br><span class="line"> __init__.py</span><br><span class="line"> moduleX.py</span><br><span class="line"> moduleY.py</span><br><span class="line"> subpackage2/</span><br><span class="line"> __init__.py</span><br><span class="line"> moduleZ.py</span><br><span class="line"> moduleA.py</span><br></pre></td></tr></table></figure><p>Python 的导入行为可以分为绝对导入与相对导入两类:</p><h2 id="绝对导入">绝对导入</h2><p>绝对导入即指定 package 或 module 的绝对名称或路径,经常用于导入内置库或第三方库,例如:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> os <span class="comment"># 导入内置库</span></span><br><span class="line"><span class="keyword">import</span> requests <span class="comment"># 导入第三方库</span></span><br></pre></td></tr></table></figure><p>事实上,绝对导入是通过依次搜索 <code>sys.path</code> 列表中的所有路径来完成的,这一点类似于操作系统的 <code>PATH</code> 环境变量。一个目录只要加入到了 <code>sys.path</code> 中,那么其中直接包含的任意 package 或 module 均可实行绝对导入。例如:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> sys</span><br><span class="line">path = <span class="string">'/path/to/package'</span></span><br><span class="line"><span class="keyword">if</span> path <span class="keyword">not</span> <span class="keyword">in</span> sys.path:</span><br><span class="line"> sys.path.insert(<span class="number">0</span>, path)</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> moduleA</span><br><span class="line"><span class="keyword">import</span> subpackage1.moduleX</span><br><span class="line"><span class="keyword">from</span> subpackage1 <span class="keyword">import</span> moduleY</span><br><span class="line"><span class="keyword">import</span> subpackage2</span><br></pre></td></tr></table></figure><p>除了在代码运行时动态添加 <code>sys.path</code> 外,还有一个环境变量可以在 Python 进程启动时设定 <code>sys.path</code> 的初始值,即 <code>PYTHONPATH</code>。一些需要经常引用的本地目录可以加入 <code>PYTHONPATH</code> 中,这样就不用每次都在代码中修改 <code>sys.path</code> 了。</p><p>此外,通过 Python 命令启动脚本或模块时会把父进程(通常是命令行)的当前目录加入 <code>sys.path</code> 中,因此当前目录下的任意 package 或 module 也可以直接进行绝对导入。例如在 shell 中执行如下命令调用 Python 脚本:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> /path/to/package</span><br><span class="line">python moduleA.py <span class="comment"># or python -m moduleA</span></span><br></pre></td></tr></table></figure><p>那么在 <a href="http://moduleA.py">moduleA.py</a> 中可以进行以下绝对导入:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> subpackage1.moduleX</span><br><span class="line"><span class="keyword">from</span> subpackage1 <span class="keyword">import</span> moduleY</span><br><span class="line"><span class="keyword">import</span> subpackage2</span><br></pre></td></tr></table></figure><h2 id="相对导入">相对导入</h2><p>相对导入是指<strong>同一个顶层 package 内部不同 module 之间的导入行为</strong>,这是大前提。很多文章包括<a href="https://docs.python.org/3/reference/import.html#package-relative-imports">官方文档</a>在讲解相对导入时往往没有强调这个前提,导致大量的误解。</p><p>相对导入包含一个或多个前导的 <code>.</code>,其格式为 <code>from .xxx import yyy</code>,例如:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> . <span class="keyword">import</span> moduleA</span><br><span class="line"><span class="keyword">from</span> .subpackage1 <span class="keyword">import</span> moduleX</span><br></pre></td></tr></table></figure><p>其中,<code>.</code> 代表当前 package,<code>..</code> 代表上层 package,<code>...</code> 代表上上层 package,以此类推。</p><p>此外,还有一种称为“隐式相对导入”的方式,其导入语句格式与绝对导入完全一样。例如在 moduleX 中引用 moduleY:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> moduleY <span class="comment"># 隐式相对导入</span></span><br><span class="line"><span class="keyword">from</span> . <span class="keyword">import</span> moduleY <span class="comment"># 显式相对导入</span></span><br></pre></td></tr></table></figure><p>隐式相对导入容易与绝对导入混淆,非常不推荐,已被 Python3 废弃。如果希望在使用 Python2 时也废弃这种语法,可以在代码中加上以下语句:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> __future__ <span class="keyword">import</span> absolute_import</span><br></pre></td></tr></table></figure><p>后文中所提到的相对导入如无特殊说明均指显式相对导入。</p><p>在相对导入中,当目录结构与导入语句均确定时,能否断定一个绝对导入/相对导入一定正确或错误的?</p><p>答案是不能,需要视情况而定。根据程序调起的方式不同,同样的 import 语句有时候是正确的,有时候会报错。这就是相对导入最让人困惑的地方。</p><p>对此,我们需要了解 Python 程序的不同调起方式。</p><h3 id="Python-程序的三种调起方式">Python 程序的三种调起方式</h3><ol><li>作为脚本直接运行:<code>python package/subpackage1/moduleX.py</code></li><li>作为模块直接运行:<code>python -m package.subpackage1.moduleX</code></li><li>从别的模块中导入:<code>import package.subpackage1.moduleX</code></li></ol><p>Python 的导入机制依赖 <code>sys.path</code>、<code>__package__</code> 和 <code>__name__</code> 三个变量。以上三种调用方式会对这三个变量产生不同的作用。当执行到 <code>moduleX.py</code> 内部时,有:</p><ol><li>方式 1:<code>__package__</code> 为 <code>None</code>; <code>__name__</code> 为 <code>'__main__'</code>; <strong>当前目录和脚本所在目录</strong>被加入 <code>sys.path</code>。</li><li>方式 2:<code>__package__</code> 为 <code>'package'</code>; <code>__name__</code> 为 <code>'__main__'</code>; <strong>当前目录</strong>被加入 <code>sys.path</code>。</li><li>方式 3:<code>__package__</code> 为 <code>'package'</code>; <code>__name__</code> 为 <code>'moduleA'</code>。<code>sys.path</code> 中具体加入了什么路径,要看程序入口是怎么调起的。</li></ol><p><strong>所谓相对导入,相对的就是 <code>__package__</code> 所代表的包名</strong>。当执行 <code>from .moduleY import func</code> 时,实际上相当于解析 <code>from __package__.moduleY import func</code>。如果 <code>__package__</code> 为 <code>None</code>,则会解析 <code>from __name__.moduleY import func</code>(后文讨论 <code>__package__</code> 的取值时,均已包含该降级逻辑)。</p><p>相对导入可能抛出的错误包括以下几种:</p><ul><li><p>如果 <code>__package__</code> 为 <code>''</code>(一般出现在类似 <code>python -m moduleX</code> 这样的调用方式中),会抛出 <code>ImportError: attempted relative import with no known parent package</code> 错误。</p></li><li><p>如果 <code>__package__</code> 不为空,但在 <code>sys.path</code> 的所有路径中均未搜索到 <code>__package__</code> 所代表的包名,会抛出类似 <code>ModuleNotFoundError: No module named 'xxxpackage.moduleY'; 'xxxpackage' is not a package</code> 的错误。</p></li><li><p>如果 <code>__package__</code> 不为空且存在对应的包,但其中没有 <code>moduleY</code> 模块,会抛出类似 <code>ModuleNotFoundError: No module named 'xxxpackage.moduleY'</code> 的错误。</p></li><li><p>如果试图从上级 package 中进行相对导入,例如 <code>from ..moduleA import func</code>,那么必须确保 <code>__package__</code> 是多级 package,例如 <code>__package__ = 'package.subpackage1'</code>。如果 package 级别数小于上溯的级别数,例如 <code>__package__ = 'subpackage1'</code>,将会抛出 <code>ValueError: attempted relative import beyond top-level package</code> 错误。</p></li></ul><p>由此可见,相对导入必须确保 <code>__package__</code> 有合适的取值,也就是只能用于上述第 2、3 种调起方式。尽管第 1 种调起方式是最常用的,但不幸的是在这种方式下只能使用绝对导入,不能使用相对导入。</p><h2 id="绝对导入与相对导入对比">绝对导入与相对导入对比</h2><p>绝对导入由于其含义非常明确,且在任何调起方式中均可以使用,因而被 PEP8 所推荐。</p><p>绝对导入唯一的缺点是将 package 名称硬编码到了代码中,会带来维护问题。例如修改了某一顶层包名之后,那么其内部的所有绝对导入代码都需要相应修改。</p><p>而相对导入就可以避免这种维护问题,当包名修改之后内部代码无需做任何改动。但相对导入的解析机制更加复杂,容易因为使用不当而报错。并且使用了相对导入的 py 文件无法再作为脚本直接运行。</p><h2 id="最佳实践">最佳实践</h2><p>结合绝对导入与相对导入二者的优缺点,推荐一种关于绝对导入与相对导入的最佳实践:</p><ol><li>一般情况下使用绝对导入。</li><li>如果要构建一个 package 供外部调用,例如给其他脚本调用或发布到 <a href="https://pypi.org/">PYPI</a>,则在该 package 内部使用相对导入。</li><li>对于使用了相对导入的脚本,如果想直接运行其中的 <code>if __name__ == '__main__':</code> 代码块(通常用于简单测试当前 module 的功能),可以使用 <code>python -m package.module</code> 的方式调起,避免使用 <code>python package/module.py</code>。</li></ol>]]></content>
</entry>
<entry>
<title>LSM Tree:一种支持高效读写的存储引擎</title>
<link href="/posts/2d7c5edb/"/>
<url>/posts/2d7c5edb/</url>
<content type="html"><![CDATA[<p><strong>LSM tree (log-structured merge-tree)</strong> 是一种对频繁写操作非常友好的数据结构,同时兼顾了查询效率。LSM tree 是许多 key-value 型或日志型数据库所依赖的核心数据结构,例如 <a href="https://cloud.google.com/bigtable">BigTable</a>、<a href="https://hbase.apache.org/">HBase</a>、<a href="https://cassandra.apache.org/">Cassandra</a>、<a href="https://github.com/google/leveldb">LevelDB</a>、<a href="https://www.sqlite.org/">SQLite</a>、<a href="https://www.scylladb.com/">Scylla</a>、<a href="https://rocksdb.org/">RocksDB</a> 等。</p><span id="more"></span><p>LSM tree 之所以有效是基于以下事实:磁盘或内存的连续读写性能远高于随机读写性能,有时候这种差距可以达到三个数量级之高。这种现象不仅对传统的机械硬盘成立,对 SSD 硬盘也同样成立。如下图:</p><p><img src="https://cdn.jsdelivr.net/gh/hzhu212/image-store@master/blog/1612419000098-1612419000090.png" alt="硬盘和内存的随机读写与连续读写性能对比"></p><p>LSM tree 在工作过程中尽可能避免随机读写,充分发挥了磁盘连续读写的性能优势。</p><h2 id="SSTable">SSTable</h2><p>LSM tree 持久化到硬盘上之后的结构称为 <strong>Sorted Strings Table (SSTable)</strong>。顾名思义,SSTable 保存了<strong>排序</strong>后的数据(实际上是按照 key 排序的 key-value 对)。每个 SSTable 可以包含多个存储数据的文件,称为 segment,每个 segment 内部都是有序的,但不同 segment 之间没有顺序关系。一个 segment 一旦生成便不再修改(immutable)。一个 SSTable 的示例如下:</p><p><img src="https://cdn.jsdelivr.net/gh/hzhu212/image-store@master/blog/1612358955833-1612358955820.png" alt="SSTable"></p><p>可以看到,每个 segment 内部的数据都是按照 key 排序的。下面我们来介绍每个 segment 是如何生成的。</p><h2 id="写入数据">写入数据</h2><p>LSM tree 的所有写操作均为<strong>连续写</strong>,因此效率非常高。但由于外部数据是无序到来的,如果无脑连续写入到 segment,显然是不能保证顺序的。对此,LSM tree 会在内存中构造一个有序数据结构(称为 memtable),例如红黑树。每条新到达的数据都插入到该红黑树中,从而始终保持数据有序。当写入的数据量达到一定阈值时,将触发红黑树的 flush 操作,把所有排好序的数据一次性写入到硬盘中(该过程为连续写),生成一个新的 segment。而之后红黑树便从零开始下一轮积攒数据的过程。</p><p><img src="https://cdn.jsdelivr.net/gh/hzhu212/image-store@master/blog/1612360358135-1612360358128.png" alt="红黑树被一次性写入一个新的 segment"></p><h2 id="读取-查询数据">读取/查询数据</h2><p>如何从 SSTable 中查询一条特定的数据呢?一个最简单直接的办法是扫描所有的 segment,直到找到所查询的 key 为止。通常应该从最新的 segment 扫描,依次到最老的 segment,这是因为<strong>越是最近的数据越可能被用户查询</strong>,把最近的数据优先扫描能够提高平均查询速度。</p><p>当扫描某个特定的 segment 时,由于该 segment 内部的数据是有序的,因此可以使用二分查找的方式,在 $O(\log n)$ 的时间内得到查询结果。但对于二分查找来说,要么一次性把数据全部读入内存,要么在每次二分时都消耗一次磁盘 IO,当 segment 非常大时(这种情况在大数据场景下司空见惯),这两种情况的代价都非常高。一个简单的优化策略是,在内存中维护一个<strong>稀疏索引(sparse index)</strong>,其结构如下图:</p><p><img src="https://cdn.jsdelivr.net/gh/hzhu212/image-store@master/blog/1612408728966-1612408728945.png" alt="一个 segment 的稀疏索引"></p><blockquote><p>稀疏索引是指将有序数据切分成(固定大小的)块,仅对各个块开头的一条数据做索引。与之相对的是全量索引(dense index),即对全部数据编制索引,其中的任意一条数据发生增删均需要更新索引。两者相比,全量索引的查询效率更高,达到了理论极限值 $O(\log n)$,但写入和删除效率更低,因为每次数据增删时均需要因为更新索引而消耗一次 IO 操作。通常的关系型数据库,例如 MySQL 等,其内部采用 B tree 作为索引结构,这便是一种全量索引。</p></blockquote><p>有了稀疏索引之后,可以先在索引表中使用二分查找快速定位某个 key 位于哪一小块数据中,然后仅从磁盘中读取这一块数据即可获得最终查询结果,此时加载的数据量仅仅是整个 segment 的一小部分,因此 IO 代价较小。以上图为例,假设我们要查询 <code>dollar</code> 所对应的 value。首先在稀疏索引表中进行二分查找,定位到 <code>dollar</code> 应该位于 <code>dog</code> 和 <code>downgrade</code> 之间,对应的 offset 为 17208~19504。之后去磁盘中读取该范围内的全部数据,然后再次进行二分查找即可找到结果,或确定结果不存在。</p><p>稀疏索引极大地提高了查询性能,然而有一种极端情况却会造成查询性能骤降:当要查询的结果在 SSTable 中不存在时,我们将不得不依次扫描完所有的 segment,这是最差的一种情况。有一种称为**布隆过滤器(bloom filter)**的数据结构天然适合解决该问题。布隆过滤器是一种空间效率极高的算法,能够快速地检测一条数据是否在数据集中存在。我们只需要在写入每条数据之前先在布隆过滤器中登记一下,在查询时即可断定某条数据是否缺失。</p><blockquote><p>布隆过滤器的内部依赖于哈希算法,当检测某一条数据是否见过时,有一定概率出现假阳性(False Positive),但一定不会出现假阴性(False Negative)。也就是说,当<strong>布隆过滤器认为一条数据出现过,那么该条数据很可能出现过;但如果布隆过滤器认为一条数据没出现过,那么该条数据一定没出现过</strong>。这种特性刚好与此处的需求相契合,即检验某条数据是否缺失。</p></blockquote><h2 id="文件合并(Compaction)">文件合并(Compaction)</h2><p>随着数据的不断积累,SSTable 将会产生越来越多的 segment,导致查询时扫描文件的 IO 次数增多,效率降低,因此需要有一种机制来控制 segment 的数量。对此,LSM tree 会定期执行文件合并(compaction)操作,将多个 segment 合并成一个较大的 segment,随后将旧的 segment 清理掉。由于每个 segment 内部的数据都是有序的,合并过程类似于归并排序,效率很高,只需要 $O(n)$ 的时间复杂度。</p><p><img src="https://cdn.jsdelivr.net/gh/hzhu212/image-store@master/blog/1612415672645-1612415672637.png" alt="segment compaction"></p><p>在上图的示例中,segment 1 和 2 中都存在 key 为 <code>dog</code> 的数据,这时应该以最新的 segment 为准,因此合并后的值取 84 而不是 52,这实现了类似于字典/HashMap 中“覆盖写”的语义。</p><h2 id="删除数据">删除数据</h2><p>现在你已经了解了 LSM tree 读写数据的方式,那么如何删除数据呢?如果是在内存中,删除某块数据通常是将它的引用指向 NULL,那么这块内存就会被回收。但现在的情况是,数据已经存储在硬盘中,要从一个 segment 文件中间抹除一段数据必须要覆写其之后的所有内容,这个成本非常高。LSM tree 所采用的做法是设计一个特殊的标志位,称为 <em>tombstone(墓碑)</em>,删除一条数据就是把它的 value 置为墓碑,如下图所示:</p><p><img src="https://cdn.jsdelivr.net/gh/hzhu212/image-store@master/blog/1612416928355-1612416928350.png" alt="删除数据"></p><p>这个例子展示了删除 segment 2 中的 <code>dog</code> 之后的效果。注意,此时 segment 1 中仍然保留着 <code>dog</code> 的旧数据,如果我们查询 <code>dog</code>,那么应该返回空,而不是 52。因此,<strong>删除操作的本质是覆盖写,而不是清除一条数据</strong>,这一点初看起来不太符合常识。墓碑会在 compact 操作中被清理掉,于是置为墓碑的数据在新的 segment 中将不复存在。</p><h2 id="LSM-tree-与-B-tree-的对比">LSM tree 与 B tree 的对比</h2><p>主流的关系型数据库均以 B/B+ tree 作为其构建索引的数据结构,这是因为 B tree 提供了理论上最高的查询效率 - $O(\log n)$。但对查询性能的追求也造成了 B tree 的相应缺点,即每次插入或删除一条数据时,均需要更新索引,从而造成一次磁盘 IO。这种特性决定了 B tree 只适用于频繁读、较少写的场景。如果在频繁写的场景下,将造成大量的磁盘 IO,从而导致性能骤降。这种应用场景在传统的关系型数据库中比较常见。</p><p>而 LSM tree 则避免了频繁写场景下的磁盘 IO 开销,尽管其查询效率无法达到理想的 $O(\log n)$,但依然非常快,可以接受。所以从本质上来说,LSM tree 相当于牺牲了一部分查询性能,换取了可观的写入性能。这对于 key-value 型或日志型数据库是非常重要的。</p><h2 id="总结">总结</h2><p>LSM tree 存储引擎的工作原理包含以下几个要点:</p><ol><li>写数据时,首先将数据缓存到内存中的一个有序树结构中(称为 memtable)。同时触发相关结构的更新,例如布隆过滤器、稀疏索引。</li><li>当 memtable 积累到足够大时,会一次性写入磁盘中,生成一个内部有序的 segment 文件。该过程为连续写,因此效率极高。</li><li>进行查询时,首先检查布隆过滤器。如果布隆过滤器报告数据不存在,则直接返回不存在。否则,按照从新到老的顺序依次查询每个 segment。</li><li>在查询每个 segment 时,首先使用二分搜索检索对应的稀疏索引,找到数据所在的 offset 范围。然后读取磁盘上该范围内的数据,再次进行二分查找并获得结果。</li><li>对于大量的 segment 文件,定期在后台执行 compaction 操作,将多个文件合并为更大的文件,以保证查询效率不衰减。</li></ol><h2 id="参考:">参考:</h2><ul><li><a href="https://yetanotherdevblog.com/lsm/">Understanding LSM Trees: What Powers Write-Heavy Databases. Braden Groom</a></li><li><a href="http://www.benstopford.com/2015/02/14/log-structured-merge-trees/">Log Structured Merge Trees. Ben Stopford</a></li><li><a href="https://queue.acm.org/detail.cfm?id=1563874">The Pathologies of Big Data. ACM</a></li><li><a href="http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.44.2782&rep=rep1&type=pdf">原始论文</a>(比较晦涩,不建议)</li></ul>]]></content>
<categories>
<category> 技术 </category>
</categories>
<tags>
<tag> 数据结构 </tag>
<tag> 数据库 </tag>
<tag> 大数据 </tag>
</tags>
</entry>
<entry>
<title>shell命令的标准输入(stdin)</title>
<link href="/posts/2c63bd16/"/>
<url>/posts/2c63bd16/</url>
<content type="html"><![CDATA[<p>在 shell 命令中,有多种方式可以灵活地控制命令的标准输入(stdin),熟练掌握这些技巧有时会起到事半功倍的效果。</p><span id="more"></span><h2 id="管道符号">管道符号(<code>|</code>)</h2><p>管道操作可以将一个命令的输出重定向到另一个命令的输入:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">echo</span> <span class="string">"<span class="variable">$string</span>"</span> | <span class="built_in">command</span></span><br></pre></td></tr></table></figure><p>这是最方便,也最为广泛使用的一种方式。但这种方式有个缺陷,即管道符之后的命令是在一个子 shell 进程中运行的,它的运行效果无法作用到当前 shell 进程。</p><p>考虑以下命令:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">echo</span> <span class="string">"hello world"</span> | <span class="built_in">read</span> first second</span><br><span class="line"><span class="built_in">echo</span> <span class="variable">$second</span> <span class="variable">$first</span> <span class="comment"># will output nothing</span></span><br></pre></td></tr></table></figure><p>会发现第二条 <code>echo</code> 命令的输出为空,并非预期中的 <code>world hello</code>(注意,一些特殊的 shell 如 zsh 不存在该问题)。事实上,<code>read</code> 命令确实正确地读取了两个变量,但之后该 shell 子进程结束,控制权回到主进程,变量又被丢弃了。</p><p>为了避免这种情况,可以使用花括号把 shell 子进程中的所有命令括起来:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">echo</span> <span class="string">"hello world"</span> | {</span><br><span class="line"> <span class="built_in">read</span> first second</span><br><span class="line"> <span class="built_in">echo</span> <span class="variable">$second</span> <span class="variable">$first</span> <span class="comment"># will output "world hello"</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这使得第二个 <code>echo</code> 命令能够正常输出 <code>world hello</code>,但仍然无法把 <code>first</code> 和 <code>second</code> 两个变量带到主进程中来。</p><h2 id="here-string">here-string(<code><<<</code>)</h2><p>继续上面的例子,除了管道符之外,我们有没有办法把字符串<code>"hello world"</code>以标准输入的形式传递给 <code>read</code> 命令呢?</p><p>有。一个非常方便的办法是使用“here-string”:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">read</span> first second <<< <span class="string">"hello world"</span></span><br><span class="line"><span class="built_in">echo</span> <span class="variable">$second</span> <span class="variable">$first</span> <span class="comment"># will output "world hello"</span></span><br></pre></td></tr></table></figure><p>上述命令不仅能够符合预期地输出 <code>world hello</code>,而且 <code>first</code> 和 <code>second</code> 两个变量也被保存到了当前 shell 中,随时可用。</p><h2 id="here-document">here-document(<code><<</code>)</h2><p>here-document 可以认为是 here-string 的高阶形式,它支持多行输入,适合输入大段文本。</p><p>在 shell 命令行中使用方式如下:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">$ cat <<<span class="string">EOF</span></span><br><span class="line"><span class="string">> hi</span></span><br><span class="line"><span class="string">> there</span></span><br><span class="line"><span class="string">> EOF</span></span><br><span class="line">hi</span><br><span class="line">there</span><br></pre></td></tr></table></figure><p>注意:其中的 <code>$</code> 表示命令提示符,<code>></code> 表示换行,均由 shell 提供,并非用户输入。<code>EOF</code> 为标志字符串,代表输入的开头和结尾,可以替换为其他任意字符串。</p><p>here-documnet 在 shell 脚本中使用的例子如下:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">cat > out.txt <<<span class="string">FILE</span></span><br><span class="line"><span class="string">foo</span></span><br><span class="line"><span class="string">bar</span></span><br><span class="line"><span class="string">bar bar</span></span><br><span class="line"><span class="string">foo foo</span></span><br><span class="line"><span class="string">FILE</span></span><br></pre></td></tr></table></figure><p>这段脚本会将两个 <code>FILE</code> 之间的那段文本写入 <code>out.txt</code> 文件中。</p><h2 id="输入重定向">输入重定向(<code><</code>)</h2><p>输入重定向符号(<code><</code>)后需要接一个文件路径,表示将文件的内容重定向到标准输入。例如:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">echo</span> <span class="string">"hello world"</span> > tmp.txt</span><br><span class="line">cat tmp.txt <span class="comment"># will output "hello world"</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">read</span> first second < tmp.txt</span><br><span class="line"><span class="built_in">echo</span> <span class="variable">$second</span> <span class="variable">$first</span> <span class="comment"># will output "world hello"</span></span><br></pre></td></tr></table></figure><h2 id="参考">参考</h2><ul><li><a href="https://unix.stackexchange.com/questions/80362/what-does-mean">https://unix.stackexchange.com/questions/80362/what-does-mean</a></li></ul>]]></content>
<categories>
<category> 技术 </category>
</categories>
<tags>
<tag> Linux </tag>
<tag> shell </tag>
</tags>
</entry>
<entry>
<title>推荐一些Windows下的生产力工具</title>
<link href="/posts/28fcbc62/"/>
<url>/posts/28fcbc62/</url>
<content type="html"><![CDATA[<blockquote class="blockquote-center"><p>工欲善其事,必先利其器。</p></blockquote><p>Windows 系统拥有 MacOS 和 Linux 无法比拟的软件生态,但可惜的是,大多数用户并没有真正发掘出 Windows 系统的生产力。本文推荐一些“小而美”的 Windows 软件,包括本地软件与浏览器插件,能够极大地提升工作效率。</p><span id="more"></span><h2 id="本地软件">本地软件</h2><h3 id="Launchy-启动器">Launchy - 启动器</h3><p><a href="https://www.launchy.net/">https://www.launchy.net/</a></p><p>Launchy 是一款免费、小巧、快捷的启动器,安装包仅4M,检索速度极高,而且支持插件、皮肤等扩展功能,是一款难得的提升效率的软件。</p><p><img src="https://raw.githubusercontent.com/hzhu212/image-store/master/blog/20191201151423.png" alt=""></p><p>Windows 上的启动器软件不在少数,Launchy 有很多替代品可选,例如:</p><ul><li><a href="https://www.listary.com/">Listary</a></li><li><a href="http://www.wox.one/">Wox</a></li><li><a href="https://u.tools/">uTools</a></li></ul><p>它们或有更华丽的界面,或有丰富的功能,但笔者最爱 Launchy,因为它将软件的体积、功能和速度都做到了极致。</p><h3 id="Everything-文件搜索">Everything - 文件搜索</h3><p><a href="https://www.voidtools.com/zh-cn/">https://www.voidtools.com/zh-cn/</a></p><p>Everything 是 Windows 下的一款文件搜索引擎,能够基于文件名快速定文件和文件夹位置。其搜索速度比 Windows 资源管理器的默认搜索快几个数量级,并且支持通配符、正则表达式等复杂搜索功能。并且,Everything 是完全免费的。</p><p>很多启动器软件需要配合 Everything 使用,例如 Listary、uTools 等。</p><p><img src="https://www.voidtools.com/zh-cn/support/everything/Everything.Search.Window.png" alt=""></p><h3 id="FastStone-Capture-截图录屏">FastStone Capture - 截图录屏</h3><p><a href="https://www.faststone.org/FSCaptureDetail.htm">https://www.faststone.org/FSCaptureDetail.htm</a></p><p>一款功能丰富的截图工具包,功能包括:</p><ul><li>截图</li><li>图片标注</li><li>图片背景去除</li><li>图片像素级编辑</li><li>屏幕录制</li><li>取色器</li><li>屏幕标尺</li></ul><p>截图方式也非常丰富,包括:</p><ul><li>截取区域</li><li>截取形状</li><li>截取窗口或窗口组件</li><li>滚动截图</li></ul><p>在拥有如此强大功能的情况下,软件体积小到令人难以置信,仅有 4M。</p><p>唯一的遗憾是,该软件是收费的,然而……</p><p><img src="https://raw.githubusercontent.com/hzhu212/image-store/master/blog/20191201151618.png" alt=""></p><h3 id="QTranslate-翻译">QTranslate - 翻译</h3><p><a href="https://quest-app.appspot.com/home">https://quest-app.appspot.com/home</a></p><p>一款小巧而强大的桌面翻译软件,支持谷歌翻译、必应翻译、有道翻译、百度翻译等十多个翻译接口,七十多种语言,并具有划词翻译、OCR 取词翻译、长段文本翻译等高级功能。</p><p>QTranslate 的体积不足 1M,小到令人难以置信。其原因是,QTranslate 的所有功能都是通过调用外部 API 接口来实现的,只提供一个本地界面而已,软件本身是一组 API 接口的粘合剂。优点是用户可以体验到最新的翻译技术、最好的翻译质量,缺点是需要网络甚至梯子。</p><p><img src="https://raw.githubusercontent.com/hzhu212/image-store/master/blog/20191201151639.png" alt=""></p><h3 id="坚果云-云盘同步">坚果云 - 云盘同步</h3><p><a href="https://www.jianguoyun.com/">https://www.jianguoyun.com/</a></p><p>一款小巧好用的云盘软件,主打自动同步功能。</p><p>坚果云可长期驻留后台,资源占用极小,用户几乎察觉不到同步的存在。免费方案也很良心,用户每月可以获得 1GB 上传流量和 3GB 下载流量,只要不用来大量备份图片和视频,完全够用了,最重要的是没有限速。</p><p><img src="https://raw.githubusercontent.com/hzhu212/image-store/master/blog/20191201151659.png" alt=""></p><h3 id="Cmder-ConEmu-命令行">Cmder/ConEmu - 命令行</h3><p><a href="https://cmder.net/">https://cmder.net/</a> | <a href="https://conemu.github.io/">https://conemu.github.io/</a></p><p>一款 Windows 下的命令行工具,致力于解决 Windows 平台对开发者不友好的问题,让用户在 Windows 上流畅地使用 Linux Shell。Cmder 基于 ConEmu 开发,提供了更好的皮肤、交互等。两者均为免费、开源软件。</p><p>ConEmu 支持多种不同的 Shell,包括 Cmd、PowerShell、Git Bash 等。在 Windows 10 推出 Linux Subsystem 后,也第一时间进行了支持。</p><p><img src="https://raw.githubusercontent.com/hzhu212/image-store/master/blog/20191201151713.png" alt=""></p><h3 id="ScreenToGif-动图制作">ScreenToGif - 动图制作</h3><p><a href="https://www.screentogif.com/?l=zh_cn">https://www.screentogif.com/?l=zh_cn</a></p><p>一款免费小巧的 gif 制作工具,支持从屏幕录制 gif 或导入图片制作 gif,可对 gif 内容做后期编辑。堪称体积小巧(2M),功能强大。</p><p><img src="https://raw.githubusercontent.com/hzhu212/image-store/master/blog/20191201151731.png" alt=""></p><h3 id="PicGo-图床">PicGo - 图床</h3><p><a href="https://github.com/Molunerfinn/PicGo">https://github.com/Molunerfinn/PicGo</a></p><p>一款简单小巧、功能强大的免费开源图床工具。聚合了</p><ul><li>七牛云图床</li><li>腾讯云图床</li><li>Github 图床</li><li><a href="http://SM.MS">SM.MS</a> 图床</li></ul><p>等主流图床接口。支持拖拽上传、剪贴板上传、悬浮窗口等多种便捷的交互形式,居家写博客必不可少。</p><p><img src="https://raw.githubusercontent.com/hzhu212/image-store/master/blog/20191201151744.png" alt=""></p><h3 id="Sublime-Text-3-文本编辑">Sublime Text 3 - 文本编辑</h3><p><a href="https://www.sublimetext.com/">https://www.sublimetext.com/</a></p><p>一款小巧轻便、无所不能的文本编辑器。在一众流行的文本编辑器中间(Notepad++、UltraEdit、VSCode、Atom 等), Sublime Text 3 做到了体积最小、速度最快、资源最省,同时保证了界面华丽、功能强大、插件系统完善。</p><p>Sublime Text 是免费软件,但如果不付费,偶尔会有弹窗提醒,不影响使用。</p><p>最大的槽点是插件系统偶尔会被墙,不过这对于一名合格的软件工程师来说应该不算问题。</p><p><img src="https://raw.githubusercontent.com/hzhu212/image-store/master/blog/20191201151800.png" alt=""></p><h3 id="Internet-Download-Manager-IDM-下载器">Internet Download Manager (IDM) - 下载器</h3><p><a href="https://www.internetdownloadmanager.com/">https://www.internetdownloadmanager.com/</a></p><p>Windows 下最优秀的下载软件,没有之一。IDM 能够最大限度地利用用户的网络带宽,其加速技术是其他同类下载软件无法匹敌的。丰富的设置、浏览器插件、视频嗅探等功能也让 IDM 的易用性远超同行。遗憾的是,IDM 是收费软件,然而……</p><p>如果不想折腾的话,IDM 还有一些免费的替代品,例如 <a href="http://xdman.sourceforge.net/">XDM</a>、<a href="https://www.freedownloadmanager.org/zh/">FDM</a>、<a href="https://xdown.org/">xDown</a> 等,虽然效果不如 IDM,但能保证免费可用。</p><p><img src="https://www.internetdownloadmanager.com/home/idm.png" alt=""></p><h3 id="AnyDesk-远程桌面">AnyDesk - 远程桌面</h3><p><a href="https://anydesk.com/zhs">https://anydesk.com/zhs</a></p><p>AnyDesk 是一款轻量(仅3MB)、快速、免费、跨平台的远程桌面软件。</p><p>在 Windows 系统下,使用自带的“远程桌面连接”即可满足绝大多数的日常需求。AnyDesk 相比于 Windows 系统自带的远程桌面连接具有以下几个优势:</p><ul><li>联网即可用。不依赖 IP 地址,只要连接双方均联网,即可互达。</li><li>跨平台。AnyDesk 支持四大桌面操作系统(Windows、MacOS、Linux、ChromeOS)以及两大移动端操作系统(Android、iOS)。</li><li>非互斥。AnyDesk 支持多个用户同时连接到一个桌面,不会导致相互注销。</li></ul><p>AnyDesk 连接是默认是需要被连接方确认的,但被连接方可设置访问密码,这样就能做到无人值守的远程连接了。</p><p><img src="https://anydesk.com/_static/img/screenshots/multiplatform-6df87f.png" alt="AnyDesk"></p><h2 id="浏览器插件">浏览器插件</h2><p>推荐一些非常好用的浏览器插件,以下均支持 Firefox,一般也支持 Chrome。</p><h3 id="Tampermonkey(油猴)"><a href="https://addons.mozilla.org/zh-CN/firefox/addon/tampermonkey/">Tampermonkey(油猴)</a></h3><p>浏览器脚本管理插件。</p><p>油猴本身没有什么特殊之处,好用的是海量的脚本!安装完成之后,在“获取脚本”页面自己发掘吧~</p><h3 id="Proxy-SwitchyOmega"><a href="https://addons.mozilla.org/zh-CN/firefox/addon/switchyomega/">Proxy SwitchyOmega</a></h3><p>代理管理和切换工具。</p><p>注意,本插件不负责梯子,只是方便管理和自动切换代理。当使用 Shadowsocks、v2ray 等代理工具上网时,此插件必不可少。</p><h3 id="Infinity-新标签页"><a href="https://addons.mozilla.org/zh-CN/firefox/addon/infinity-new-tab-pro-firefox/">Infinity 新标签页</a></h3><p>华丽、易用的新标签页工具。</p><p>不仅美观,还集成了必应壁纸、书签、TODO list、笔记等方便的小工具,注册帐号后可以在不同平台和浏览器之间同步新标签页设置。</p><h3 id="Bitwarden"><a href="https://addons.mozilla.org/zh-CN/firefox/addon/bitwarden-password-manager/">Bitwarden</a></h3><p>免费密码管理器。</p><p>帐号、密码多得记不过来已经是生活的常态,针对这个需求诞生了许多密码管理工具,比较著名的包括 Lastpass、Bitwarden 等。Bitwarden 比起 Lastpass 更加易用,除了插件之外还有桌面版软件,最重要的是免费!</p><h3 id="Print-Edit-WE"><a href="https://addons.mozilla.org/zh-CN/firefox/addon/print-edit-we/">Print Edit WE</a></h3><p>在保存或打印网页之前先编辑一下。</p><p>网页文档打印保存为 PDF 文件是一个常规操作,但有时候网页上难免有干扰元素(导航栏、侧边栏、广告位等),这款插件能够将任意网页元素隐藏或删除,让打印出来的文档更纯净。</p><h3 id="Video-DownloadHelper"><a href="https://addons.mozilla.org/zh-CN/firefox/addon/video-downloadhelper/">Video DownloadHelper</a></h3><p>网页视频捕获(下载)工具。</p><p>很多视频网站与直播平台只提供视频供用户观看,但不允许下载。该插件能够捕获网页中传输的流媒体,将其转换为完整的视频并下载。让视频“所见即所得”。</p><h3 id="Octotree"><a href="https://addons.mozilla.org/zh-CN/firefox/addon/octotree/">Octotree</a></h3><p><em>PS:主要针对软件工程师群体</em></p><p>在 Github 项目页面显示目录与文件的侧边栏,就像本地编辑器一样。</p>]]></content>
<categories>
<category> 技术 </category>
</categories>
<tags>
<tag> Windows </tag>
<tag> 工具 </tag>
</tags>
</entry>
<entry>
<title>Python从零实现计算图和自动求导</title>
<link href="/posts/7a426523/"/>
<url>/posts/7a426523/</url>
<content type="html"><![CDATA[<p>计算图是现代深度学习框架如 Tensorflow、PyTorch 等的核心概念,其中涉及的所有计算几乎都依赖于计算图提供的自动求导功能,因此研究计算图对深入理解反向传播等深度学习的底层算法大有帮助。</p><span id="more"></span><h2 id="手工求导">手工求导</h2><p>求导在数学上非常容易实现,例如以下函数:</p><p>$$ f(x) = \sin(e^{x^2}) $$</p><p>我们能够轻易地求得其导函数为:</p><p>$$ f’(x) = \cos(e^{x^2}) \cdot e^{x^2} \cdot (2x) $$</p><p>那么能否通过编程语言实现该函数及其导函数?答案是可以,而且非常容易,只需要把式子逐项翻译即可:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> numpy <span class="keyword">as</span> np</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">f</span>(<span class="params">x</span>):</span></span><br><span class="line"> <span class="keyword">return</span> np.sin(np.exp(np.power(x, <span class="number">2</span>)))</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">f_prime</span>(<span class="params">x</span>):</span></span><br><span class="line"> t = np.exp(np.power(x, <span class="number">2</span>))</span><br><span class="line"> <span class="keyword">return</span> np.cos(t) * t * (<span class="number">2</span>*x)</span><br></pre></td></tr></table></figure><p>到目前为止,求导对于编程语言来说似乎没什么难的,但是不要忘记,我们这里研究的函数是一个具体的实例。在实际应用中,我们用到的函数将会非常丰富,它们的组合方式更是千变万化,光是上面这个简单的例子就会有无数种变形!例如:</p><p>$$ \begin{gathered}<br>f_2(x) = \cos(e^{x^2}) \\<br>f_3(x) = \sin(2^{x^2}) \\<br>f_4(x) = \sin(e^{x^3}) \\<br>\cdots<br>\end{gathered} $$</p><p>更不要提各式各样的其他复杂函数:</p><p>$$ \begin{gathered}<br>g(x) = -y \ln(\frac{1}{1 + e^{-wx}}) - (1-y)\ln(1 - \frac{1}{1 + e^{-wx}}) \\<br>h(x) = (w_n\cdot(\cdots(\mathrm{relu}(w_2\cdot(\mathrm{relu}(w_1\cdot x))))) - y) ^ 2 \\<br>\cdots<br>\end{gathered} $$</p><p>如果坚持手工求导的话,我们不仅需要无数次地推导公式,而且对于某些复杂的函数,求导公式并不简单,显然不可能完成。</p><h2 id="链式法则">链式法则</h2><p>因此,我们需要一套抽象的求导规则,使得无论函数的具体形式如何,都能自动对其求导。也就是实现如下抽象函数的求导法则:</p><p>$$ f(x) = g(h(k(\cdots(x))) $$</p><p>尽管这个问题听上去要比具体函数的求导困难得多,但它依然有章可循。回想我们求导的一般过程,不过是运用了以下两点技术而已:</p><ol><li><strong>基本函数的求导法则</strong>。包括三角函数、指数函数、幂函数等。</li><li><strong>链式法则</strong>。</li></ol><p>链式法则使得我们可以对复合函数进行求导。针对上面的例子,为了显式地调用链式法则,我们可以引入如下中间变量:</p><p>$$ \begin{aligned}<br>u &= x^2 \\<br>v &= e^u \\<br>w &= sin(v)<br>\end{aligned} $$</p><p>使用链式法则描述的求导过程如下:</p><p>$$ \frac{\mathrm{d}y}{\mathrm{d}x} = \frac{\mathrm{d}y}{\mathrm{d}w} \cdot \frac{\mathrm{d}w}{\mathrm{d}v} \cdot \frac{\mathrm{d}v}{\mathrm{d}u} \cdot \frac{\mathrm{d}u}{\mathrm{d}x} $$</p><p>有了链式法则,我们就能够“机械”地搬运任意基本函数的导函数,从而对非常复杂的复合函数求导。</p><h2 id="计算图">计算图</h2><p>由上述分析可知,一旦我们实现了(1)基本函数的求导法则以及(2)链式法则,就能够让程序模仿我们手工求导的过程,从而做到“以不变应万变”。计算图非常适合用来描述这两个法则。</p><p>计算图在数据结构上属于<strong>有向图</strong>(Directed Graph),图的每个节点对应一个“基本函数”,而节点之间的有向边则可用于描述链式法则。</p><p>上面的例子使用计算图描述如下:</p><p>$$ x \to (\cdot)^2 \to e^{(\cdot)} \to \sin(\cdot) \to y $$</p><p>计算图能够非常清晰地展现数据的流动过程。从输入 $x$ 开始,中间依次经过平方、自然指数、正弦函数三个基本运算依次作用,最终得到输出 $y$。</p><blockquote><p>注意:这个例子并非典型的计算图,因为其中所涉及的运算都是<strong>一元运算</strong>,导致图结构是线性的,没有分支,更像是<strong>链表</strong>。</p><p>这种线性结构的计算图<strong>无法描述加法、乘法等多元运算</strong>,例如 $x + sin(x)$、$x\sin(x)$。但它的好处是非常简单,便于理解和实现,因此我们将继续使用这种线性结构完成演示。</p></blockquote><p>计算图的每一个节点都包含一个基本函数,并且其导函数是已知的。节点在进行一次“前向计算”时,除了要根据输入值计算输出值之外,还要调用导函数计算梯度值,并缓存在节点中。最终,我们将所有节点的梯度值相乘(链式法则)即可得到整个计算流程的总梯度。</p><h2 id="代码实现">代码实现</h2><p>在实现代码之前,我们首先要明确接口的设计,即假想用户将会如何调用计算图,这是一个非常重要的工程原则。</p><p>我们期望用户以如下方式调用计算图:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">>>> </span><span class="keyword">import</span> compute_graph <span class="keyword">as</span> cg</span><br><span class="line"><span class="meta">>>> </span>inp = cg.Input()</span><br><span class="line"><span class="meta">>>> </span>out = cg.power(inp, <span class="number">2</span>)</span><br><span class="line"><span class="meta">>>> </span>out = cg.exp(out)</span><br><span class="line"><span class="meta">>>> </span>out = cg.sin(out)</span><br><span class="line"><span class="meta">>>> </span>graph = ComputeGraph(inp, out)</span><br><span class="line">>>></span><br><span class="line"><span class="meta">>>> </span><span class="keyword">import</span> numpy <span class="keyword">as</span> np</span><br><span class="line"><span class="meta">>>> </span>x = np.linspace(<span class="number">0</span>, <span class="number">1</span>, <span class="number">5</span>)</span><br><span class="line"><span class="meta">>>> </span>graph.forward(x)</span><br><span class="line">array([<span class="number">0.84147098</span>, <span class="number">0.87454388</span>, <span class="number">0.95916224</span>, <span class="number">0.98307241</span>, <span class="number">0.41078129</span>])</span><br><span class="line"><span class="meta">>>> </span>graph.grad</span><br><span class="line">array([ <span class="number">0.</span> , <span class="number">0.25811137</span>, <span class="number">0.36319491</span>, -<span class="number">0.48233501</span>, -<span class="number">4.95669947</span>])</span><br></pre></td></tr></table></figure><p>这种 API 风格与 <a href="https://keras.io/zh/">Keras</a> 非常接近,符合一般用户的使用习惯。</p><p>下面我们开始着手实施我们的想法。我们计划为计算图、计算图节点分别设计一个类。</p><h3 id="图节点类">图节点类</h3><p>首先定义所有图节点的基类,代表节点的通用结构。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Node</span>(<span class="params"><span class="built_in">object</span></span>):</span></span><br><span class="line"> <span class="string">"""Node of compute graph"""</span></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">__init__</span>(<span class="params">self, x, *args, **kw</span>):</span></span><br><span class="line"> <span class="comment"># 我们已经假定计算图为线性结构,因此只需要连接图中的前一个节点 x。</span></span><br><span class="line"> <span class="comment"># 如果考虑一般的图结构,则需要更为复杂的设计,此处不予讨论。</span></span><br><span class="line"> <span class="keyword">if</span> <span class="keyword">not</span> <span class="built_in">isinstance</span>(x, Node):</span><br><span class="line"> <span class="keyword">raise</span> ValueError(<span class="string">'the input should be a compute graph Node object'</span>)</span><br><span class="line"> x.<span class="built_in">next</span> = self</span><br><span class="line"> self.<span class="built_in">next</span> = <span class="literal">None</span></span><br><span class="line"></span><br><span class="line"> <span class="comment"># 使用一个变量缓存计算的梯度值</span></span><br><span class="line"> self.grad = <span class="literal">None</span></span><br><span class="line"></span><br><span class="line"> <span class="comment"># 其余非通用参数单独初始化</span></span><br><span class="line"> self.init(*args, **kw)</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">init</span>(<span class="params">self, *args, **kw</span>):</span></span><br><span class="line"> <span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">fun</span>(<span class="params">self, x</span>):</span></span><br><span class="line"> <span class="string">"""节点中保存的基本函数"""</span></span><br><span class="line"> <span class="keyword">raise</span> NotImplementedError()</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">fun_grad</span>(<span class="params">self, x, out</span>):</span></span><br><span class="line"> <span class="string">"""基本函数的导函数,用于计算梯度。</span></span><br><span class="line"><span class="string"> x, out 分别是 self.fun 的输入和输出。</span></span><br><span class="line"><span class="string"> 理论上只需要 x 即可计算出梯度,但很多函数的导函数会引用自身,例如指数函数。</span></span><br><span class="line"><span class="string"> 引入 out 作为参数可避免计算梯度时重复计算自身。</span></span><br><span class="line"><span class="string"> """</span></span><br><span class="line"> <span class="keyword">raise</span> NotImplementedError()</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">forward</span>(<span class="params">self, x</span>):</span></span><br><span class="line"> <span class="string">"""计算输出,同时缓存梯度"""</span></span><br><span class="line"> out = self.fun(x)</span><br><span class="line"> self.grad = self.fun_grad(x, out)</span><br><span class="line"> <span class="keyword">return</span> out</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">__str__</span>(<span class="params">self</span>):</span></span><br><span class="line"> <span class="keyword">return</span> self.__class__.__name__</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">__repr__</span>(<span class="params">self</span>):</span></span><br><span class="line"> <span class="keyword">return</span> <span class="string">'<"{}" node of compute graph>'</span>.<span class="built_in">format</span>(<span class="built_in">str</span>(self))</span><br></pre></td></tr></table></figure><p>一般的计算节点只需要继承节点基类,并实现 <code>fun</code> 和 <code>fun_grad</code> 两个方法即可。</p><h4 id="正弦函数节点">正弦函数节点</h4><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">sin</span>(<span class="params">Node</span>):</span></span><br><span class="line"> <span class="string">"""Node of sin function"""</span></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">fun</span>(<span class="params">self, x</span>):</span></span><br><span class="line"> <span class="keyword">return</span> np.sin(x)</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">fun_grad</span>(<span class="params">self, x, out</span>):</span></span><br><span class="line"> <span class="keyword">return</span> np.cos(x)</span><br></pre></td></tr></table></figure><h4 id="指数函数节点">指数函数节点</h4><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">exp</span>(<span class="params">Node</span>):</span></span><br><span class="line"> <span class="string">"""Node of exp function"""</span></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">fun</span>(<span class="params">self, x</span>):</span></span><br><span class="line"> <span class="keyword">return</span> np.exp(x)</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">fun_grad</span>(<span class="params">self, x, out</span>):</span></span><br><span class="line"> <span class="keyword">return</span> out</span><br></pre></td></tr></table></figure><h4 id="幂函数节点">幂函数节点</h4><p>注意,幂函数需要在初始化时传入额外的参数,即幂指数。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">power</span>(<span class="params">Node</span>):</span></span><br><span class="line"> <span class="string">"""Node of power function"""</span></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">init</span>(<span class="params">self, p</span>):</span></span><br><span class="line"> <span class="comment"># 从参数中接收幂指数</span></span><br><span class="line"> self.p = p</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">fun</span>(<span class="params">self, x</span>):</span></span><br><span class="line"> <span class="keyword">return</span> np.power(x, self.p)</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">fun_grad</span>(<span class="params">self, x, out</span>):</span></span><br><span class="line"> <span class="keyword">return</span> self.p * np.power(x, self.p - <span class="number">1</span>)</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">__str__</span>(<span class="params">self</span>):</span></span><br><span class="line"> <span class="keyword">return</span> <span class="string">'{}(., {})'</span>.<span class="built_in">format</span>(self.__class__.__name__, self.p)</span><br></pre></td></tr></table></figure><h4 id="输入节点">输入节点</h4><p>与普通节点不同,输入节点没有前驱节点,也不需要对数据进行加工和求导,因此需要单独进行定义。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Input</span>(<span class="params">Node</span>):</span></span><br><span class="line"> <span class="string">"""Input Node"""</span></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">__init__</span>(<span class="params">self</span>):</span></span><br><span class="line"> self.<span class="built_in">next</span> = <span class="literal">None</span></span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">fun</span>(<span class="params">self, x</span>):</span></span><br><span class="line"> <span class="keyword">return</span> x</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">fun_grad</span>(<span class="params">self, x, out</span>):</span></span><br><span class="line"> <span class="keyword">return</span> <span class="number">1</span></span><br></pre></td></tr></table></figure><h3 id="计算图类">计算图类</h3><p>我们已经把主要的计算过程定义在了图节点类中,因此计算图类的任务就非常轻松了,只需要整合图节点的计算结果即可。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">ComputeGraph</span>(<span class="params"><span class="built_in">object</span></span>):</span></span><br><span class="line"> <span class="string">"""Compute Graph"""</span></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">__init__</span>(<span class="params">self, inp, out</span>):</span></span><br><span class="line"> self.head = inp</span><br><span class="line"> self.tail = out</span><br><span class="line"> self.grad = <span class="literal">None</span></span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">forward</span>(<span class="params">self, x</span>):</span></span><br><span class="line"> <span class="keyword">if</span> self.head <span class="keyword">is</span> <span class="literal">None</span>:</span><br><span class="line"> <span class="keyword">raise</span> ValueError(<span class="string">'the graph is empty'</span>)</span><br><span class="line"> out = x</span><br><span class="line"> grad = <span class="number">1.0</span></span><br><span class="line"> node = self.head</span><br><span class="line"> <span class="keyword">while</span> node:</span><br><span class="line"> out = node.forward(out)</span><br><span class="line"> grad *= node.grad</span><br><span class="line"> node = node.<span class="built_in">next</span></span><br><span class="line"> self.grad = grad</span><br><span class="line"> <span class="keyword">return</span> out</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">__str__</span>(<span class="params">self</span>):</span></span><br><span class="line"> node = self.head</span><br><span class="line"> desc = []</span><br><span class="line"> <span class="keyword">while</span> node:</span><br><span class="line"> desc.append(<span class="built_in">str</span>(node))</span><br><span class="line"> node = node.<span class="built_in">next</span></span><br><span class="line"> <span class="keyword">return</span> <span class="string">' --> '</span>.join(desc)</span><br></pre></td></tr></table></figure><p>到此为止,我们的代码已经全部完成了,是不是简单地出乎意料?</p><h4 id="验证代码">验证代码</h4><p>在进行接口设计时,我们给出了一段样板代码,现在我们可以用它来验证我们的程序。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">>>> </span><span class="keyword">import</span> compute_graph <span class="keyword">as</span> cg</span><br><span class="line"><span class="meta">>>> </span>inp = cg.Input()</span><br><span class="line"><span class="meta">>>> </span>out = cg.power(inp, <span class="number">2</span>)</span><br><span class="line"><span class="meta">>>> </span>out</span><br><span class="line"><<span class="string">"power(., 2)"</span> node of compute graph></span><br><span class="line"><span class="meta">>>> </span>out = cg.exp(out)</span><br><span class="line"><span class="meta">>>> </span>out = cg.sin(out)</span><br><span class="line"><span class="meta">>>> </span>graph = ComputeGraph(inp, out)</span><br><span class="line"><span class="meta">>>> </span>print(graph)</span><br><span class="line">Input --> power(., 2) --> exp --> sin</span><br><span class="line">>>></span><br><span class="line"><span class="meta">>>> </span><span class="keyword">import</span> numpy <span class="keyword">as</span> np</span><br><span class="line"><span class="meta">>>> </span>x = np.linspace(<span class="number">0</span>, <span class="number">1</span>, <span class="number">5</span>)</span><br><span class="line"><span class="meta">>>> </span>graph.forward(x)</span><br><span class="line">array([<span class="number">0.84147098</span>, <span class="number">0.87454388</span>, <span class="number">0.95916224</span>, <span class="number">0.98307241</span>, <span class="number">0.41078129</span>])</span><br><span class="line"><span class="meta">>>> </span>graph.grad</span><br><span class="line">array([ <span class="number">0.</span> , <span class="number">0.25811137</span>, <span class="number">0.36319491</span>, -<span class="number">0.48233501</span>, -<span class="number">4.95669947</span>])</span><br></pre></td></tr></table></figure><p>代码无误,且输出完全符合预期。</p><p>但我们还未考察计算结果是否正确无误,毕竟这才是最重要的。我们可以通过之前手动推导的公式对计算结果加以验证,函数的定义如下:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">f</span>(<span class="params">x</span>):</span></span><br><span class="line"> <span class="keyword">return</span> np.sin(np.exp(np.power(x, <span class="number">2</span>)))</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">f_prime</span>(<span class="params">x</span>):</span></span><br><span class="line"> t = np.exp(np.power(x, <span class="number">2</span>))</span><br><span class="line"> <span class="keyword">return</span> <span class="number">2</span> * x * np.cos(t) * t</span><br></pre></td></tr></table></figure><p>我们进行如下验证:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">>>> </span>f(x)</span><br><span class="line">array([<span class="number">0.84147098</span>, <span class="number">0.87454388</span>, <span class="number">0.95916224</span>, <span class="number">0.98307241</span>, <span class="number">0.41078129</span>])</span><br><span class="line"><span class="meta">>>> </span>f_prime(x)</span><br><span class="line">array([ <span class="number">0.</span> , <span class="number">0.25811137</span>, <span class="number">0.36319491</span>, -<span class="number">0.48233501</span>, -<span class="number">4.95669947</span>])</span><br><span class="line"><span class="meta">>>> </span>np.<span class="built_in">all</span>(f(x) == graph.forward(x))</span><br><span class="line"><span class="literal">True</span></span><br><span class="line"><span class="meta">>>> </span>np.<span class="built_in">all</span>(f_prime(x) == graph.grad)</span><br><span class="line"><span class="literal">True</span></span><br></pre></td></tr></table></figure><p>说明计算图的计算结果和梯度值均准确无误。</p><p>上述代码在一些细节问题上可能有所欠缺,但足以从宏观上理解计算图的实现原理。</p>]]></content>
<categories>
<category> 技术 </category>
</categories>
<tags>
<tag> 机器学习 </tag>
<tag> 深度学习 </tag>
</tags>
</entry>
<entry>
<title>线性代数的本质</title>
<link href="/posts/66517499/"/>
<url>/posts/66517499/</url>
<content type="html"><![CDATA[<p>本文为 <a href="https://www.bilibili.com/video/av6731067/?p=1">3Blue1Brown-线性代数的本质</a> 系列视频的笔记。</p><span id="more"></span><p>该系列视频专注于建立线性代数中的<strong>几何直觉</strong>,而对基本定义、计算方法、公式定理等不做详细介绍,关于这部分内容可以参考普通线性代数教材。</p><h2 id="向量究竟是什么">向量究竟是什么</h2><p>对于向量,有三个不同的理解视角:</p><ol><li>物理学视角:向量是空间中的一个箭头,具有一定的<strong>长度</strong>和<strong>方向</strong>。在空间中平移向量不会改变其值。</li><li>计算机视角:向量是一个有序数字列表,例如 $\begin{bmatrix} 2 \cr 1 \end{bmatrix}$。</li><li>数学视角:向量用符号表示为 $\mathbf{v}$、$\mathbf{w}$,它可以代表任何东西,只要定义了合理的<strong>加法</strong>和<strong>数乘</strong>运算。</li></ol><p>通过引入坐标系,我们可以整合前两种观点,获得新的向量视角:</p><p><strong>向量是坐标系中的一个箭头,起点固定在坐标原点,终点落在某个坐标上。向量的终点坐标就是向量的数值表示。</strong></p><p><img src="https://raw.githubusercontent.com/hzhu212/image-store/master/blog/20190622142401.png" alt=""></p><h3 id="向量的加法">向量的加法</h3><p>从数值角度看,向量加法的定义非常直接,即<strong>把两个向量的对应坐标分别相加</strong>。但这样的定义隐含着怎样的含义呢?为什么要这么定义向量加法?</p><p>从几何角度看,向量 $\mathbf{v} + \mathbf{w}$,相当于把 $\mathbf{w}$ 的起点移动到 $\mathbf{v}$ 的终点后,从 $\mathbf{v}$ 的起点指向 $\mathbf{w}$ 的终点的向量。</p><p><img src="https://raw.githubusercontent.com/hzhu212/image-store/master/blog/20190622142718.png" alt=""></p><p>问题是为什么向量的加法对应着这种几何形式,而不是别的几何形式?比如这样:</p><p><img src="https://raw.githubusercontent.com/hzhu212/image-store/master/blog/20190622142736.png" alt=""></p><p>有一种思维方式可以很好地解释这个问题,即把向量想象成某种“运动”,那么向量的加法就会非常直观:一个物体先沿着向量 $\mathbf{v}$ 运动,再沿着向量 $\mathbf{w}$ 运动,其“合运动”就是从 $\mathbf{v}$ 的起点到 $\mathbf{w}$ 的终点。</p><p><img src="https://raw.githubusercontent.com/hzhu212/image-store/master/blog/20190622121011862_26397.gif" alt=""></p><h3 id="向量的数乘">向量的数乘</h3><p>向量的数乘就是使用一个实数乘以一个向量。从数值角度看,向量数乘的定义非常直接,即把向量的所有坐标分别乘以该实数。</p><p>从几何角度看,向量数乘就是把向量的长度<strong>缩放</strong>(Scaling)某个倍数,同时方向保持不变(如果缩放倍数为负,则向量反向)。缩放的倍数被称为<strong>标量(Scalar)</strong>。</p><p>在线性代数中,<strong>一个标量几乎总是对应着一种缩放作用</strong>。</p><h2 id="线性组合、张成的空间与基">线性组合、张成的空间与基</h2><p>我们介绍一种新的思维方式来看待向量的坐标:</p><p>对于向量 $\begin{bmatrix} 3 \cr -2 \end{bmatrix}$,我们可以把它的两个坐标分别看作两个标量,即 $3$ 和 $-2$。根据上一节的内容,这两个标量分别代表着对两个向量的缩放作用,这两个向量隐含在坐标系中,分别为 $x$ 轴方向和 $y$ 轴方向的<strong>单位向量</strong>,记作 $\boldsymbol{\hat{\imath}}$,$\boldsymbol{\hat{\jmath}}$。</p><p>那么向量的坐标表示可以自然地转化为<strong>对单位向量缩放然后求和</strong>:</p><p>$$ \begin{bmatrix}<br>3 \cr -2<br>\end{bmatrix} = (3)\boldsymbol{\hat{\imath}} + (-2)\boldsymbol{\hat{\jmath}} $$</p><p><img src="https://raw.githubusercontent.com/hzhu212/image-store/master/blog/20190622142821.png" alt=""></p><p>由于单位向量的特殊性,我们将 $\boldsymbol{\hat{\imath}}$ 与 $\boldsymbol{\hat{\jmath}}$ 称作 $xy$ 坐标系的<strong>基向量</strong>。</p><p>基向可以任意选取,不限于 $\boldsymbol{\hat{\imath}}$ 和 $\boldsymbol{\hat{\jmath}}$。空间中的同一个向量在两组不同的基向量下,其坐标值是不同的,因此,<strong>每当我们用数字描述向量时,它总是依赖于我们正在使用的基</strong>。</p><p>把两个向量分别做数乘然后求和,称为这两个向量的<strong>线性组合</strong>,记作:</p><p>$$ a \mathbf{v} + b \mathbf{w} $$</p><p>所有可以表示为给定向量线性组合的向量的集合,被称为给定向量<strong>张成(span)的空间</strong>。对应到上述表达式中,也就是 $a$,$b$ 在实数范围内自由变动时,$a \mathbf{v} + b \mathbf{w}$ 得到的所有向量的集合。</p><blockquote><p>向量的坐标对应着对基向量 $\boldsymbol{\hat{\imath}}$ 和 $\boldsymbol{\hat{\jmath}}$ 的线性组合。</p></blockquote><p>向量的加法和数乘运算是整个线性代数的基石。<strong>两个向量张成的空间实际上就是在说,仅通过向量加法和向量数乘这两种基础运算,能够获得的所有可能的向量的集合</strong>。</p><p>大部分情况下,一对向量张成的空间对应整个平面:</p><p><img src="https://raw.githubusercontent.com/hzhu212/image-store/master/blog/20190622142841.png" alt=""></p><p>然而,当两个初始向量共线时,它们张成的空间就只能是它们所在的直线。更糟糕的情况是,如果两个初始向量都是零向量,那么它们张成的空间只包含一个点,也就是坐标原点。</p><p>后两种情况在三维或更高维空间同样存在。在这类情况下,我们的原始向量组合中至少存在一个向量没有对张成的空间作出任何贡献。此时,我们称原始向量组合是<strong>线性相关</strong>的。</p><p>在一组线性相关的向量组合中,某些向量可以通过其余向量的线性组合表示出来,因而去掉这些向量不会影响张成的空间。反之,如果在一组向量中,任意一个向量都不能通过其余向量的线性组合表示出来,那么称这组向量是<strong>线性无关</strong>的。</p><p>在此基础上,我们可以给出<strong>基</strong>的严格定义:<strong>向量空间的一组基是张成该空间的一组线性无关的向量</strong>。</p><h2 id="矩阵与线性变换">矩阵与线性变换</h2><p>变换(transformation)本质上是一种函数(function),它接受一个向量作为输入,然后输出一个新的向量。</p><p>之所以称之为变换,而不是简单地称作函数,是因为变换隐含着“运动”的含义,而这种运动是能够可视化的:</p><p><img src="https://raw.githubusercontent.com/hzhu212/image-store/master/blog/20190622110726823_15747.gif" alt=""></p><p>一些变换可能非常复杂,使空间发生卷曲、缠绕:</p><p><img src="https://raw.githubusercontent.com/hzhu212/image-store/master/blog/20190622110752737_23282.gif" alt=""></p><p><img src="https://raw.githubusercontent.com/hzhu212/image-store/master/blog/20190622110803267_8515.gif" alt=""></p><p>但幸运的是,<strong>线性变换</strong>是一类特殊的、简单的变换,其对空间造成的变形作用从直觉上容易理解。线性变换需要满足如下两条性质:</p><ol><li>空间中所有的直线在变换后仍然保持直线,不能发生弯曲。</li><li>原点必须保持固定。</li></ol><p>简而言之,<strong>线性变换是保持空间网格线平行且等距分布的变换</strong>,正如上述第一张动图所展示的那样。</p><p>有了直觉上的认识,那么我们如何用数值来描述线性变换呢?为此,我们需要先了解一个非常关键的推论:<strong>线性变换能够维持向量加法和数乘(或者说维持线性组合关系)</strong>。</p><p>假设向量 $\mathbf{v}=\begin{bmatrix} x \cr y \end{bmatrix}$,则 $\mathbf{v}$ 能够表示成基向量 $\boldsymbol{\hat{\imath}}$ 和 $\boldsymbol{\hat{\jmath}}$ 的线性组合,即 $\mathbf{v} = x\boldsymbol{\hat{\imath}} + y\boldsymbol{\hat{\jmath}}$。经历任意线性变换后,该组合关系总是保持不变,即 $\mathbf{v}’ = x\boldsymbol{\hat{\imath}}’ + y\boldsymbol{\hat{\jmath}}'$。</p><p>也就是说,<strong>在线性变换前后,向量本身虽然发生了变化,但它对当前基向量的组合关系总是保持不变</strong>。之所以如此,是因为基向量也同步发生了变换,这个变换巧妙地维持了所有的线性组合关系,而这正是“保持空间网格线平行且等距分布”的自然结果。</p><p>上述推论已经给出了描述一个线性变换的全部信息,即:</p><p>$$ \mathbf{v}’ = x\boldsymbol{\hat{\imath}}’ + y\boldsymbol{\hat{\jmath}}’ $$</p><p>我们只需要追踪变换后的基向量 $\boldsymbol{\hat{\imath}}‘$ 和 $\boldsymbol{\hat{\jmath}}’$,就能得到任意一个变换后的向量 $\mathbf{v}'$。</p><blockquote><p>注意:当讨论 $\mathbf{v}'$ 的坐标时,我们所关注的是它在原始坐标系下的坐标表述,而不是在变换后坐标系下的坐标表述。后者实际上就是向量对当前基的组合关系,总是保持不变,是无意义的。<br>另一方面,作为观察者,我们总是要基于一个固定不变的参考系(即原始坐标系),不管是变换前还是变换后。如果切换坐标系,那么所有的向量坐标就失去可比性了。</p></blockquote><p>让我们以公式的形式更明确地描述一下这个过程。</p><p>假设在某个线性变换中,基向量发生了如下变换:</p><p>$$ \boldsymbol{\hat{\imath}} \rightarrow \boldsymbol{\hat{\imath}}’ = \color{green}{\begin{bmatrix} 1 \cr -2 \end{bmatrix}}<br>\qquad<br>\boldsymbol{\hat{\jmath}} \rightarrow \boldsymbol{\hat{\jmath}}’ = \color{red}{\begin{bmatrix} 3 \cr 0 \end{bmatrix}}<br>$$</p><p>那么对于任意一个向量 $\mathbf{v}$,它将发生如下变换:</p><p>$$ \begin{bmatrix} x \cr y \end{bmatrix} \rightarrow<br>x \boldsymbol{\hat{\imath}}’ + y \boldsymbol{\hat{\jmath}}’ =<br>x \color{green}{\begin{bmatrix} 1 \cr -2 \end{bmatrix}}\color{black}{} +<br>y \color{red}{\begin{bmatrix} 3 \cr 0 \end{bmatrix}}\color{black}{} =<br>\begin{bmatrix}<br>\color{green}{1}\color{black}{} x + \color{red}{3}\color{black}{} y \cr<br>\color{green}{-2}\color{black}{} x + \color{red}{0}\color{black}{} y<br>\end{bmatrix} $$</p><p>由以上的推导可以看出,一个线性变换完全由变换后的基向量 $\boldsymbol{\hat{\imath}}‘$ 和 $\boldsymbol{\hat{\jmath}}’$ 所确定。假设它们的坐标分别为 $\color{green}{\begin{bmatrix} a \cr c \end{bmatrix}}\color{black}{}$ 和 $\color{red}{\begin{bmatrix} b \cr d \end{bmatrix}}\color{black}{}$,我们可以将它们写在一起:</p><p>$$ \begin{bmatrix}<br>\color{green}{a}\color{black}{} & \color{red}{b}\color{black}{} \cr<br>\color{green}{c}\color{black}{} & \color{red}{d}\color{black}{}<br>\end{bmatrix} $$</p><p>这就得到了一个 $2 \times 2$ 的<strong>矩阵</strong>,它携带了当前线性变换的全部信息,因此我们可以认为该<strong>矩阵就是线性变换的数值表示</strong>。</p><p>我们将该线性变换作用于任意向量 $\begin{bmatrix} x \cr y \end{bmatrix}$,可以得到:</p><p>$$ x \color{green}{\begin{bmatrix} a \cr c \end{bmatrix}}\color{black}{} +<br>y \color{red}{\begin{bmatrix} b \cr d \end{bmatrix}}\color{black}{} =<br>\begin{bmatrix}<br>\color{green}{a}\color{black}{} x + \color{red}{b}\color{black}{} y \cr<br>\color{green}{c}\color{black}{} x + \color{red}{d}\color{black}{} y<br>\end{bmatrix} $$</p><p>自然地,我们可以将这个运算定义成<strong>矩阵对向量的乘法</strong>:</p><p>$$ \begin{bmatrix}<br>\color{green}{a}\color{black}{} & \color{red}{b}\color{black}{} \cr<br>\color{green}{c}\color{black}{} & \color{red}{d}\color{black}{}<br>\end{bmatrix}<br>\begin{bmatrix} x \cr y \end{bmatrix} \overset{def}{=}<br>x \color{green}{\begin{bmatrix} a \cr c \end{bmatrix}}\color{black}{} +<br>y \color{red}{\begin{bmatrix} b \cr d \end{bmatrix}}\color{black}{} =<br>\begin{bmatrix}<br>\color{green}{a}\color{black}{} x + \color{red}{b}\color{black}{} y \cr<br>\color{green}{c}\color{black}{} x + \color{red}{d}\color{black}{} y<br>\end{bmatrix} $$</p><p>在这个表达式中,矩阵放在向量左侧,其作用类似于一个函数。矩阵与向量相乘,就是对向量施加一个线性变换(也就是一种特定的函数)。而矩阵的列向量就是变换后的基向量,因此当我们看到一个矩阵时,很容易想象出对应的线性变换。反过来,当我们看到一个线性变换的可视化表示时,也很容易构造出相应的矩阵。</p><p>以下是两个例子,帮助你练习如何从一个直觉化的线性变换出发,构造出一个矩阵:</p><ol><li><p>给出线性变换“逆时针旋转 90°”的矩阵形式。</p><p>逆时针旋转 90° 后,$\boldsymbol{\hat{\imath}}$ 落在坐标 $\begin{bmatrix} 0 \cr 1 \end{bmatrix}$ 处,$\boldsymbol{\hat{\jmath}}$ 落在坐标 $\begin{bmatrix} -1 \cr 0 \end{bmatrix}$ 处。因此,该线性变换的矩阵形式为:</p><p>$$ \begin{bmatrix}<br>0 & -1 \cr<br>1 & 0<br>\end{bmatrix} $$</p></li><li><p>给出线性变换“向右剪切 1 个单位”的矩阵形式。</p><p>向右剪切 1 个单位后,$\boldsymbol{\hat{\imath}}$ 不变,落在坐标 $\begin{bmatrix} 1 \cr 0 \end{bmatrix}$ 处,$\boldsymbol{\hat{\jmath}}$ 向右移动一个单位,落在坐标 $\begin{bmatrix} 1 \cr 1 \end{bmatrix}$ 处。因此,该线性变换的矩阵形式为:</p><p>$$ \begin{bmatrix}<br>1 & 1 \cr<br>0 & 1<br>\end{bmatrix} $$</p></li></ol><p>现在反向思考,如何从一个矩阵出发,获得它所代表的线性变换的几何直觉?例如矩阵:</p><p>$$ \begin{bmatrix}<br>1 & 3 \cr<br>2 & 1<br>\end{bmatrix} $$</p><p>我们只需要在保持“空间网格线平行且等距分布”的情况下,将 $\boldsymbol{\hat{\imath}}$ 拉伸到 $\begin{bmatrix} 1 \cr 2 \end{bmatrix}$,$\boldsymbol{\hat{\jmath}}$ 拉伸到 $\begin{bmatrix} 3 \cr 1 \end{bmatrix}$ 即可:</p><p><img src="https://raw.githubusercontent.com/hzhu212/image-store/master/blog/20190622115306991_9892.gif" alt=""></p><p>最后我们给出“线性”的严格定义:</p><p>若一个函数或变换 $L$ 满足以下两条性质:</p><ol><li><p><strong>可加性</strong><br>$$ L(\mathbf{v} + \mathbf{w}) = L(\mathbf{v}) + L(\mathbf{w}) $$</p></li><li><p><strong>成比例</strong>(一阶齐次)<br>$$ L(c \mathbf{v}) = c L(\mathbf{v}) $$</p></li></ol><p>则称该函数或变换满足线性。</p><h2 id="矩阵乘法与线性变换复合">矩阵乘法与线性变换复合</h2><p>很多时候,我们需要描述两个或多个线性变换的相继作用。</p><p>例如,我们想要描述这个变换:先逆时针旋转 90°,然后向右剪切一个单位。</p><p><img src="https://raw.githubusercontent.com/hzhu212/image-store/master/blog/20190622120022752_12538.gif" alt=""></p><p>由于每个线性变换都会保持网格线平行且等距分布,因此最终叠加的结果仍然是一个线性变换,我们通常把这个变换称作前两个独立变换的<strong>复合变换</strong>。并且,复合变换与多个独立变换应当是等价的,因为它们最终的变换效果完全相同:</p><p><img src="https://raw.githubusercontent.com/hzhu212/image-store/master/blog/20190622120256034_15588.gif" alt=""></p><p>因此我们可以得到下面的等式:</p><p><img src="https://raw.githubusercontent.com/hzhu212/image-store/master/blog/20190622143035.png" alt=""></p><p>这个等式表明,两个矩阵对向量的相继作用等同于一个复合矩阵对该向量的作用。我们在此基础上更进一步,写成:</p><p>$$ \begin{bmatrix} 1 & 1 \cr 0 & 1 \end{bmatrix}<br>\begin{bmatrix} 0 & -1 \cr 1 & 0 \end{bmatrix} =<br>\begin{bmatrix} 1 & -1 \cr 1 & 0 \end{bmatrix} $$</p><p>一种自然且合理的想法是,<strong>我们可以把这种运算定义为矩阵乘法,它的几何意义就是两个线性变换的相继作用</strong>。</p><p><strong>矩阵乘法需要从右向左读,因为我们是先应用右侧的线性变换,然后应用左侧的线性变换</strong>。这种顺序看似反向,但如果从矩阵的函数特性出发,把矩阵看作一种函数记号,那么自右向左的顺序就很容易理解,毕竟我们研究复合函数时(例如 $f(g(x))$),也是从右向左读的,不是吗?</p><p>以上,我们从几何直觉出发,建立了矩阵乘法的定义。</p><p>从数值计算角度来看,矩阵乘法的计算规则非常奇怪,为什么要定义成这样?</p><p><img src="https://raw.githubusercontent.com/hzhu212/image-store/master/blog/20190622123359671_25740.gif" alt=""></p><p>实际上,我们可以从几何直觉出发,推导出矩阵乘法的计算规则。</p><p>考虑以下普适的矩阵乘法形式:</p><p>$$ \overbrace{\begin{bmatrix} a & b \cr c & d \end{bmatrix}}^{M_2}<br>\overbrace{\begin{bmatrix} \color{green}{e}\color{black}{} & \color{red}{f}\color{black}{} \cr \color{green}{g}\color{black}{} & \color{red}{h}\color{black}{} \end{bmatrix}}^{M_1} =<br>\overbrace{\begin{bmatrix} ? & ? \cr ? & ? \end{bmatrix}}^{M} $$</p><p>根据我们对矩阵和线性变换的定义,要得到矩阵 $M$,我们只需要追踪基向量 $\boldsymbol{\hat{\imath}}$ 和 $\boldsymbol{\hat{\jmath}}$ 的最终去向即可。由矩阵 $M_1$ 可知,经过第一次线性变换后,$\boldsymbol{\hat{\imath}}$ 变换到 $\color{green}{\begin{bmatrix} e \cr g \end{bmatrix}}\color{black}{}$。随后进行第二次变换,我们使用矩阵对向量的乘法得到:</p><p>$$ \begin{bmatrix} a & b \cr c & d \end{bmatrix}<br>\color{green}{\begin{bmatrix} e \cr g \end{bmatrix}}\color{black}{} =<br>\begin{bmatrix} ae+bg \cr ce+dg \end{bmatrix} $$</p><p>这就是最终的 $\boldsymbol{\hat{\imath}}$ 坐标,也就是 $M$ 的第一列。同样的方式可以求得 $\boldsymbol{\hat{\jmath}}$,即 $M$ 的第二列。最终得到:</p><p>$$ \begin{bmatrix} a & b \cr c & d \end{bmatrix}<br>\begin{bmatrix} e & f \cr g & h \end{bmatrix} =<br>\begin{bmatrix} ae+bg & af+bh \cr ce+dg & cf+dh \end{bmatrix} $$</p><p>这就是矩阵乘法的计算公式。</p><h2 id="行列式">行列式</h2><p>矩阵对应着一个线性变换,而矩阵的行列式则表征着该线性变换<strong>将空间缩放的比例</strong>。对于二维空间,行列式是面积缩放的比例。对于三维空间,行列式是体积缩放的比例。高维空间以此类推。</p><p><img src="https://raw.githubusercontent.com/hzhu212/image-store/master/blog/20190622143112.png" alt=""></p><p>如果线性变换将空间的维度降低,例如将二维平面压缩到一条直线上,那么行列式的值为零。</p><p>如果线性变换使得空间发生反转,例如由右手坐标系变换到左手坐标系,那么行列式的值为负。</p><p>二阶行列式的计算公式如下:</p><p>$$ \det\left(\begin{bmatrix} a & b \cr c & d \end{bmatrix}\right) = ad - bc $$</p><p>通过下图,很容易从几何角度给出其推导过程:</p><p><img src="https://raw.githubusercontent.com/hzhu212/image-store/master/blog/20190622143123.png" alt=""></p><p>$$ \det\left(\begin{bmatrix} a & b \cr c & d \end{bmatrix}\right) =<br>(a+b)(c+d) - ac -bd -2bc = ad - bc $$</p><p>三阶行列式的计算可以转化为多个二阶行列式:</p><p>$$ \det\left( \begin{bmatrix} a & b & c \cr d & e & f \cr g & h & i \end{bmatrix} \right) =<br>a \det\left( \begin{bmatrix} e & f \cr h & i \end{bmatrix} \right) -<br>b \det\left( \begin{bmatrix} d & f \cr g & i \end{bmatrix} \right) +<br>c \det\left( \begin{bmatrix} d & e \cr g & h \end{bmatrix} \right) $$</p><p>更高阶的行列式可类比这种处理方法,依次降阶。</p><h2 id="逆矩阵、列空间与零空间">逆矩阵、列空间与零空间</h2><p>矩阵的一个重要应用是求解<strong>线性方程组</strong>。</p><p>线性方程组是类似于这样的一个方程组:</p><p>$$ \begin{aligned}<br>2x + 5y + 3z &= -3 \cr<br>4x + 0y + 8z &= 0 \cr<br>1x + 3y + 0z &= 2<br>\end{aligned} $$</p><p>它满足以下两个条件:</p><ul><li>所有的未知量都是一阶的,不能出现高阶未知量或更复杂的形式,如 $x^2$,$xy$,$sin(x)$ 等。</li><li>未知量的系数都是常数。必要的时候需要补全未知项,即认为缺失项的系数为零。</li></ul><p>线性方程组在形式上与矩阵对向量的乘法非常相似,因此我们可以把上述方程组写作一个向量方程:</p><p>$$ \begin{bmatrix} 2 & 5 & 3 \cr 4 & 0 & 8 \cr 1 & 3 & 0 \end{bmatrix}<br>\begin{bmatrix} x \cr y \cr z \end{bmatrix} =<br>\begin{bmatrix} -3 \cr 0 \cr 2 \end{bmatrix} $$</p><p>如果我们用 $A$ 代表系数矩阵,用 $\mathbf{x}$ 代表未知项向量,用 $\mathbf{v}$ 代表常数项向量,就能得到更普遍的形式:</p><p>$$ A \mathbf{x} = \mathbf{v} $$</p><p>这个表达式具有明显的几何意义,即在线性变换 $A$ 中,向量 $\mathbf{x}$ 被变换为向量 $\mathbf{v}$。</p><p>与通常的线性变换表达式相比,这个等式有些特殊,等号右侧的向量 $\mathbf{v}$ 是已知的,而等号左侧的向量 $\mathbf{x}$ 却是未知的。我们需要解决的问题是,什么样的向量 $\mathbf{x}$ 能够在施加线性变换 $A$ 之后,落在向量 $\mathbf{v}$ 处?</p><p>为了找到 $\mathbf{x}$,我们需要找到线性变换 $A$ 的逆过程,对向量 $\mathbf{v}$ 施加逆向变换就能回到 $\mathbf{x}$。我们把这个逆过程称作 $A$ 的<strong>逆变换</strong>,记作 $A^{-1}$。</p><p>对一个空间先施加变换 $A$,然后施加逆变换 $A^{-1}$,空间会恢复原状,整个过程相当于对空间施加了一个“什么都不做”的变换:</p><p><img src="https://raw.githubusercontent.com/hzhu212/image-store/master/blog/20190620183057962_10964.gif" alt=""></p><p>我们用矩阵 $I$ 来表示“什么都不做”的线性变换,那么上述过程可以通过矩阵乘法来描述:</p><p>$$ A^{-1}A = I $$</p><p>式中的 $I$ 称为<strong>单位矩阵</strong>,对应的线性变换称为<strong>恒等变换(identity transformation)</strong>。单位矩阵实际上就是以单位向量 $\boldsymbol{\hat{\imath}}$、$\boldsymbol{\hat{\jmath}}$ … 作为列的矩阵。作为示例,以下是 3 阶单位矩阵:</p><p>$$ \begin{bmatrix}<br>1 & 0 & 0 \cr 0 & 1 & 0 \cr 0 & 0 & 1<br>\end{bmatrix} $$</p><p>在实践中,逆矩阵可以通过高斯消元法求得,也可以通过计算机计算。</p><p>一旦获得了逆矩阵 $A^{-1}$,我们就可以对前面的等式做如下变形:</p><p>$$ \begin{aligned}<br>& A \mathbf{x} = \mathbf{v} \cr<br>\Longleftrightarrow & A^{-1}A \mathbf{x} = A^{-1}\mathbf{v} \cr<br>\Longleftrightarrow & \mathbf{x} = A^{-1}\mathbf{v} \cr<br>\end{aligned} $$</p><p>上述结果意味着,对向量 $\mathbf{v}$ 施加逆变换 $A^{-1}$ 就能得到向量 $\mathbf{x}$。</p><p>假如随机取一个方阵 $A$,那么其逆矩阵 $A^{-1}$ 几乎总是存在的,但也有例外。$A^{-1}$ 存在的前提条件是,$A$ 没有将空间变换到更低的维度上,也即 $A$ 的行列式不为零:</p><p>$$ A^{-1} \text{ exists} \Longleftrightarrow \det(A) \neq 0 $$</p><blockquote><p>从直觉上考虑,这个条件是必要的。一旦空间被变换到更低的维度上,就意味着丢失了一部分坐标信息,因此不可能再还原回去,也就不存在逆变换。</p></blockquote><p>当 $A^{-1}$ 不存在时,线性方程组要么无解,要么有无数多个解。</p><p>$\det(A)=0$ 对应着线性变换后的空间维度降低,例如三维空间可能被压缩到一个平面、一条直线,甚至原点。</p><p>为了描述线性变换中空间的维度变化,我们引入一个新的术语<strong>秩(Rank)</strong>。<strong>矩阵的秩就是线性变换后的空间的维度</strong>。如果矩阵 $A$ 对应的线性变换没有导致空间降维,那么称矩阵 $A$ 为<strong>满秩矩阵</strong>。</p><p>我们从另一个角度考虑变换后的空间,由于矩阵 $A$ 的列向量就是变换后的空间的基,因此变换后的空间是由矩阵 $A$ 的列向量张成的,我们把这个空间称为 $A$ 的<strong>列空间</strong>。</p><p>从上述推理可知,矩阵的秩与列空间有着明显的联系,即<strong>矩阵的秩就是其列空间的维度</strong>。</p><p>所有矩阵的列空间均包含零向量,这是因为线性变换总是保持原点固定,而零向量变换后还是零向量。</p><p>对于一个满秩矩阵,唯一能在变换后落在原点的向量只有零向量自身。但对于一个非满秩矩阵来说,它将空间压缩到一个更低的维度上,因此会有一系列向量在变换后成为零向量:</p><p><img src="https://raw.githubusercontent.com/hzhu212/image-store/master/blog/20190620193603570_21726.gif" alt=""></p><p>变换后落在原点的向量的集合构成了一个空间,称作<strong>零空间(Null space)</strong>,或者<strong>核空间(Kernal space)</strong>。我们可以通过如下方程计算矩阵 $A$ 的零空间:</p><p>$$ A \mathbf{x} = \mathbf{0} $$</p><p>式中的 $\mathbf{0}$ 代表零向量,即所有元素均为零的向量。</p><p>显然,满秩矩阵的零空间只包含一个零向量。反之,如果一个线性变换的零空间包含除零向量以外的向量,那么可以断定该线性变换对应的矩阵为非满秩矩阵。</p><h2 id="非方阵">非方阵</h2><p>到此为止,我们讨论线性变换时都只考虑了方阵,因为方阵所代表的线性变换具有非常直观的几何意义。</p><p>相比之下,非方阵的几何含义则不那么容易解释。例如一个 $3\times 2$ 的矩阵:</p><p>$$ \begin{bmatrix}<br>3 & 1 \cr 4 & 1 \cr 5 & 9<br>\end{bmatrix} $$</p><p>它所代表的线性变换是怎样的呢?</p><p>首先,我们可以找到变换后的 $\boldsymbol{\hat{\imath}}$ 和 $\boldsymbol{\hat{\jmath}}$,即:</p><p>$$ \boldsymbol{\hat{\imath}} = \begin{bmatrix} 3 \cr 4 \cr 5 \end{bmatrix} \qquad<br>\boldsymbol{\hat{\jmath}} = \begin{bmatrix} 1 \cr 1 \cr 9 \end{bmatrix} $$</p><p>只有两个基向量,表明输入向量位于二维空间中。而 $\boldsymbol{\hat{\imath}}$ 和 $\boldsymbol{\hat{\jmath}}$ 具有三个分量,表明输出向量位于三维空间中。很容易验证,该矩阵的列空间是二维的,因此变换后的空间是三维空间中的一个平面。</p><p>需要注意的是,该矩阵仍是满秩的,因为它没有降低空间的维度(从二维变换到二维)。</p><p>类似地,一个 $2 \times 3$ 的矩阵:</p><p>$$ \begin{bmatrix}<br>3 & 1 & 4 \cr 1 & 5 & 9<br>\end{bmatrix} $$</p><p>代表怎样的线性变换呢?</p><p>通过类似的分析过程,我们知道该矩阵的输入空间为三维,输出空间则是二维,显然是非满秩的。虽然有三个基向量,但每个基向量都是二维的,因此最多张成一个二维空间。</p><p>有一类特殊的非方阵是 $1\times n$ 矩阵,也被称作行向量。它们将 $n$ 维空间变换到一维空间,也就是实数轴,这类变换与向量的点积运算具有奇妙的对应关系。</p><h2 id="点积与对偶性">点积与对偶性</h2><p>从数值计算角度,向量点积的定义是这样的:两个维数相同的向量,将它们的对应坐标分别相乘,然后求和。例如:</p><p>$$ \begin{bmatrix} 2 \cr 7 \cr 1 \end{bmatrix} \cdot<br>\begin{bmatrix} 8 \cr 2 \cr 8 \end{bmatrix} =<br>2\cdot 8 + 7 \cdot 2 + 1\cdot 8 = 38 $$</p><p>从数学角度看,向量点积的定义是这样的:</p><p>$$ \mathbf{v} \cdot \mathbf{w} = \Vert \mathbf{v} \Vert \cdot \Vert \mathbf{w} \Vert \cdot \cos\theta $$</p><p>其中,符号 $\Vert\cdot\Vert$ 代表向量的模量,也就是向量的长度。$\theta$ 代表向量 $\mathbf{v}$ 和 $\mathbf{w}$ 之间的夹角。</p><p>从几何角度看,两个向量的点积就是其中一个向量在另一个向量上的投影长度,乘以另一个向量的长度:</p><p><img src="https://raw.githubusercontent.com/hzhu212/image-store/master/blog/20190622143220.png" alt=""></p><p>事实上,这里的投影长度也就是上面公式中的 $\Vert \mathbf{v} \Vert \cdot \cos\theta$ 或 $\Vert \mathbf{w} \Vert \cdot \cos\theta$。</p><p>现在的问题是,数值计算角度的定义与几何角度的定义为什么是等效的?对应坐标相乘并求和,为什么与投影有所关联?</p><p>我们考虑最简单的二维单位向量,例如向量 $\mathbf{u} = \begin{bmatrix} u_x \cr u_y \end{bmatrix}$,将它与另一个未知向量 $\mathbf{x} = \begin{bmatrix} x \cr y \end{bmatrix}$ 做点乘:</p><p>$$ \mathbf{u} \cdot \mathbf{x} $$</p><p>这个表达式看上去有点像函数,$\mathbf{u}\cdot$ 就像一个函数作用在向量 $\mathbf{x}$ 上。这个关系非常类似矩阵之于线性变换,我们不妨将 $\mathbf{u}$ 转置一下,得到一个 $1\times 2$ 矩阵 $M=\begin{bmatrix} u_x & u_y \end{bmatrix}$:</p><p>$$ \begin{bmatrix} u_x & u_y \end{bmatrix} \begin{bmatrix} x \cr y \end{bmatrix} $$</p><p>从计算角度看,这个表达式刚好等同于向量点乘。</p><p>从几何角度看,这个表达式相当于对二维空间施加一个线性变换 $\begin{bmatrix} u_x & u_y \end{bmatrix}$,输出一维空间(实数轴)。变换后的 $\boldsymbol{\hat{\imath}}$ 和 $\boldsymbol{\hat{\jmath}}$ 分别落在点 $u_x$,$u_y$ 处。另一方面,$u_x$,$u_y$ 分别是向量 $\mathbf{u}$ 在 $\boldsymbol{\hat{\imath}}$ 和 $\boldsymbol{\hat{\jmath}}$ 上的投影,由于 $\mathbf{u}$ 是单位向量,该投影关系是对称的,即 $u_x$,$u_y$ 分别是 $\boldsymbol{\hat{\imath}}$ 和 $\boldsymbol{\hat{\jmath}}$ 在向量 $\mathbf{u}$ 上的投影。也就是说,这个线性变换就是把空间中的所有点向向量 $\mathbf{u}$ 做投影。</p><p>当 $\mathbf{u}$ 不是单位向量时,我们可以将它缩放成单位向量,同时获得一个标量系数 $\Vert \mathbf{u} \Vert$。</p><p>最终,任意向量 $\mathbf{u} = \begin{bmatrix} u_x \cr u_y \end{bmatrix}$ 对应的矩阵 $M=\begin{bmatrix} u_x & u_y \end{bmatrix}$ 对应的线性变换相当于<strong>把空间中的所有点向向量 $\mathbf{u}$ 做投影,然后把投影长度缩放 $\Vert \mathbf{u} \Vert$ 倍</strong>。</p><p>以上,我们通过构造一个单行矩阵,将向量点乘的数值计算与几何意义联系到了一起。</p><p>向量与线性变换之间有着出乎意料的联系,这是数学中<strong>对偶性(duality)</strong> 的一种体现。对偶性在数学中普遍存在,但却难以定义,粗略地说,对偶性就是<strong>两种数学事物之间自然而又出乎意料的对应关系</strong>。在上述推导中:</p><ul><li>一个向量的对偶是由它定义的线性变换。</li><li>一个多维空间到一维空间的线性变换的对偶是多维空间中的某个特定向量。</li></ul><blockquote><p>一般来说,提到线性变换我们会联想到一个矩阵,而不是一个向量。</p></blockquote><h2 id="基变换">基变换</h2><p>一个坐标系由坐标系的基向量完全确定。基向量可以随意选取,但我们通常使用正交的 $\boldsymbol{\hat{\imath}}$ 和 $\boldsymbol{\hat{\jmath}}$。</p><p>现假设有一个坐标系以 $\boldsymbol{\hat{\imath}}$ 和 $\boldsymbol{\hat{\jmath}}$ 为基向量,我们称它为原始坐标系 $S_0$。同时有另一组基向量 $\mathbf{b}_1$ 和 $\mathbf{b}_2$,它们确定了一个新的坐标系 $S_1$。为了方便说明,我们给出 $\mathbf{b}_1$,$\mathbf{b}_2$ 在 $S_0$ 下的坐标表示:</p><p>$$ \mathbf{b}_1 = \begin{bmatrix} 2 \cr 1 \end{bmatrix} \qquad \mathbf{b}_2 = \begin{bmatrix} -1 \cr 1 \end{bmatrix} $$</p><p>在两个不同的坐标系下,空间中的同一个向量的坐标表述将会有所不同。例如一个向量在原始坐标系中的坐标为 $\begin{bmatrix} 3 \cr 2 \end{bmatrix}$,但在新坐标系下的坐标却是 $\begin{bmatrix} (5/3) \cr (1/3) \end{bmatrix}$。</p><p>反过来,两个坐标系中的相同坐标对应着空间中不同的向量。这是因为向量的坐标就是对基向量进行线性组合的系数,基向量不同,那么线性组合得到的最终向量也不同。</p><p>这就好比在不同的语言中描述同一件事物,我们需要一种映射,来把一种描述“翻译”成另一种描述。</p><p>具体来说就是,我们该如何描述 $S_1$ 与 $S_0$ 的关系?$S_1$ 中的某个向量 $\mathbf{v}= \begin{bmatrix} x’ \cr y’ \end{bmatrix}$ 如何在 $S_0$ 下表述?</p><p>要回答这个问题,我们只需要追踪关键性的 $\mathbf{b}_1$ 和 $\mathbf{b}_2$ 即可,因为 $S_1$ 中的任意向量 $\mathbf{v}= \begin{bmatrix} x’ \cr y’ \end{bmatrix}$ 都可以表示为:</p><p>$$ x’ \mathbf{b}_1 + y’ \mathbf{b}_2 $$</p><p>这个表达式正是矩阵对向量的乘法,我们代入 $\mathbf{b}_1$ 和 $\mathbf{b}_2$ 的坐标,得到:</p><p>$$ \begin{bmatrix} 2 & -1 \cr 1 & 1 \end{bmatrix}<br>\begin{bmatrix} x’ \cr y’ \end{bmatrix} $$</p><p>自然而然地,我们可以从线性变换的角度理解这个表达式。左侧的 $2\times 2$ 矩阵代表一个线性变换,它将原始坐标系中的 $\boldsymbol{\hat{\imath}}$ 与 $\boldsymbol{\hat{\jmath}}$ 变换到 $\mathbf{b}_1$ 与 $\mathbf{b}_2$,也就是将坐标系 $S_0$ 变换到 $S_1$。对于右侧的向量 $\begin{bmatrix} x’ \cr y’ \end{bmatrix}$,我们不把它看作坐标,而是看作基向量的一种特定线性组合,那么上述表达式的结果,正是向量 $\mathbf{v}$ 在坐标系 $S_0$ 中的坐标表述,即:</p><p>$$ \begin{bmatrix} x \cr y \end{bmatrix} =<br>\begin{bmatrix} 2 & -1 \cr 1 & 1 \end{bmatrix}<br>\begin{bmatrix} x’ \cr y’ \end{bmatrix} $$</p><p>如果反过来考虑,原始坐标系中的任意向量在新坐标系 $S_1$ 下应该怎样表述呢?很显然,我们只需要施加线性变换的逆变换即可:</p><p>$$ \begin{bmatrix} x’ \cr y’ \end{bmatrix} =<br>\begin{bmatrix} 2 & -1 \cr 1 & 1 \end{bmatrix}^{-1}<br>\begin{bmatrix} x \cr y \end{bmatrix} $$</p><p>以上就是对于向量的基变换。考虑到矩阵也是一组数字坐标,同样会存在基变换问题。</p><p>依然沿用上述坐标系的例子,假设原始坐标系 $S_0$ 中存在一个线性变换 $M=\begin{bmatrix} 0 & -1 \cr 1 & 0 \end{bmatrix}$,代表“逆时针旋转 90°”,如何把同样的线性变换施加到新坐标系 $S_1$ 中呢?</p><p>首先,使用矩阵 $M$ 直接乘以 $S_1$ 下的向量肯定是行不通的,矩阵 $\begin{bmatrix} 0 & -1 \cr 1 & 0 \end{bmatrix}$ 之所以能够代表“逆时针旋转 90°”这个线性变换,是因为它记录了 $S_0$ 中变换后的基向量的去向。如果要在 $S_1$ 中构造同样的变换,我们就需要记录 $\mathbf{b}_1$ 和 $\mathbf{b}_2$ 的去向。</p><p>我们可以通过以下过程来解决这个问题:</p><ul><li>对于 $S_1$ 下的任意向量 $\begin{bmatrix} x’ \cr y’ \end{bmatrix}$,我们首先对其施加基变换 $A$,将该向量转化为 $S_0$ 下的表述。</li><li>然后施加 $S_0$ 下的线性变换 $M$,获得变换后的向量在 $S_0$ 下的表述。</li><li>最后施加逆向基变换 $A^{-1}$,获得变换后的向量在 $S_1$ 下的表述,也就是最终结果。</li></ul><p>在上述过程中,我们通过相继施加三个线性变换,构造了一个由 $S_1$ 所表述的复合变换 $M’$,它对空间的变换效果等同于由 $S_0$ 所表述的线性变换 $M$。写作矩阵乘法的形式如下:</p><p>$$ M’ = A^{-1} M A $$</p><h2 id="特征值与特征向量">特征值与特征向量</h2><p>在一个线性变换中,大多数的向量会脱离自己张成的空间:</p><p><img src="https://raw.githubusercontent.com/hzhu212/image-store/master/blog/20190621160814579_20337.gif" alt=""></p><p>但也存在一些特殊的向量,它们保持在自己张成的空间中,仅仅发生了伸缩变形:</p><p><img src="https://raw.githubusercontent.com/hzhu212/image-store/master/blog/20190621160826995_7527.gif" alt=""></p><p>对于示例中的线性变换 $\begin{bmatrix} 3 & 1 \cr 0 & 2 \end{bmatrix}$ 来说,存在两个这样的特殊方向,一个被拉伸 2 倍,另一个被拉伸 3 倍。这种在线性变换中不发生偏转的向量就称为<strong>特征向量(Eigenvector)</strong>,它们的缩放系数称为<strong>特征值(Eigenvalue)</strong>。如下图所示:</p><p><img src="https://raw.githubusercontent.com/hzhu212/image-store/master/blog/20190622143311.png" alt=""></p><p>当特征向量的方向发生倒转时,特征值为负数。</p><blockquote><p>特征向量有什么用处?</p><p>举一个关于三维变换的例子:对三维空间中的一个物体做旋转变换,可以使用一个 $3\times 3$ 的矩阵来描述。现在考虑这个矩阵的特征向量,是在变换过程中没有发生方向偏转的向量,这正是旋转轴所在的方向!使用特征向量(旋转轴)来描述旋转变换往往更加简单和直观。</p></blockquote><p>特征值与特征向量一般基于如下方程进行定义:</p><p>$$ A \mathbf{v} = \lambda \mathbf{v} $$</p><p>式中的 $\mathbf{v}$ 为特征向量,$\lambda$ 为特征值。这个方程的含义就是,对向量 $\mathbf{v}$ 施加线性变换 $A$,其结果相当于使用一个标量 $\lambda$ 缩放 $\mathbf{v}$,这正是我们前面用图形化的方式给出的定义。</p><p>方程的左侧为矩阵对向量的乘法,而右侧却是向量的数乘。为了求解这个方程,我们需要在等号右侧引入单位矩阵 $I$,然后作如下变形:</p><p>$$ \begin{aligned}<br>& A \mathbf{v} = (\lambda I) \mathbf{v} \cr<br>\Longleftrightarrow & A \mathbf{v} - (\lambda I) \mathbf{v} = \mathbf{0} \cr<br>\Longleftrightarrow & (A - \lambda I) \mathbf{v} = \mathbf{0}<br>\end{aligned} $$</p><p>我们得到一个新的矩阵 $A - \lambda I$,类似于这种形式:</p><p>$$ \begin{bmatrix}<br>3 - \lambda & 1 & 4 \cr<br>1 & 5 - \lambda & 9 \cr<br>2 & 6 & 5 - \lambda<br>\end{bmatrix} $$</p><p>矩阵 $A - \lambda I$ 将一个非零向量 $\mathbf{v}$ 变换到零向量,这说明它压缩了空间的维度,必定是非满秩的。于是:</p><p>$$ \det(A - \lambda I) = 0 $$</p><p>我们可以据此求解 $\lambda$,然后将 $\lambda$ 代入矩阵,求出 $\mathbf{v}$,这样就求出了所有的特征值与特征向量。</p><blockquote><p>注意,特征向量 $\mathbf{v}$ 代表某个方向上的一簇向量,而不是某一个特定的向量。</p></blockquote><p>二维线性变换不一定有特征向量,例如变换“逆时针旋转 90°”,所有向量的方向都会发生偏转。也可能只有一个特征向量,例如变换“向右剪切一个单位”,$x$ 轴方向是唯一不发生偏转的向量。除此之外,还存在另一种特殊情况,即一个特征值对应的特征向量不止在一条直线上,例如变换“将空间沿所有轴拉伸两倍”,唯一的特征值是 $2$,并且空间中的所有向量都是这个特征值对应的特征向量。</p><p>特征向量如此特殊,那么如果选取特征向量作为坐标系的基向量将会怎样呢?</p><p>当特征向量刚好为基向量时,我们称之为<strong>特征基</strong>。先考虑一个最特殊的情况,纯拉伸变换:</p><p>$$ \begin{bmatrix}<br>3 & 0 \cr 0 & 2<br>\end{bmatrix} $$</p><p>其列向量,也就是变换后的基向量,刚好也是特征向量。这个矩阵有一个明显的特征,就是除了对角线元素之外,其他所有元素均为零。这样的矩阵被称为<strong>对角矩阵</strong>。</p><p>对角矩阵有一个良好的性质,与普通矩阵相比,它的幂运算非常容易计算:</p><p>$$ \begin{bmatrix} 3 & 0 \cr 0 & 2 \end{bmatrix}^n =<br>\begin{bmatrix} 3^n & 0 \cr 0 & 2^n \end{bmatrix} $$</p><p>上述情况过于特殊,通常情况下特征向量并不刚好是基向量。但如果特征向量足够多,足以张成整个空间,那么我们可以通过基变换,人为地构造出特征基。</p><p>例如矩阵:</p><p>$$ \begin{bmatrix}<br>3 & 1 \cr 0 & 2<br>\end{bmatrix} $$</p><p>其特征向量有两个:$\begin{bmatrix} 1 \cr 0 \end{bmatrix}$、$\begin{bmatrix} -1 \cr 1 \end{bmatrix}$。我们把特征向量写成基变换矩阵:</p><p>$$ \begin{bmatrix}<br>1 & -1 \cr 0 & 1<br>\end{bmatrix} $$</p><p>应用基变换规则,得到:</p><p>$$ \begin{bmatrix} 1 & -1 \cr 0 & 1 \end{bmatrix}^{-1}<br>\begin{bmatrix} 3 & 1 \cr 0 & 2 \end{bmatrix}<br>\begin{bmatrix} 1 & -1 \cr 0 & 1 \end{bmatrix} =<br>\begin{bmatrix} 3 & 0 \cr 0 & 2 \end{bmatrix} $$</p><p><strong>特征基变换的结果必然会得到一个对角矩阵</strong>,这个变换的过程被称为<strong>对角化</strong>。</p><blockquote><p>当我们要对一个普通矩阵进行幂运算时,可以先将它对角化,然后进行幂运算,最后再应用逆向特征基变换即可。</p></blockquote><p>并非所有的矩阵都能对角化,矩阵能够对角化的充要条件是特征向量的个数等于矩阵的阶数。</p>]]></content>
<categories>
<category> 笔记 </category>
</categories>
<tags>
<tag> 线性代数 </tag>
</tags>
</entry>
<entry>
<title>概率统计笔记</title>
<link href="/posts/b59aba05/"/>
<url>/posts/b59aba05/</url>
<content type="html"><![CDATA[<p>本文为 <a href="https://book.douban.com/subject/1748397/">概率统计讲义</a> 一书的笔记。</p><span id="more"></span><h2 id="第一章-随机事件与概率">第一章 随机事件与概率</h2><h3 id="频率">频率</h3><p>$$ 频率=\frac{频数}{试验次数} $$</p><h3 id="概率">概率</h3><p><strong>定义</strong>:频率具有稳定性的事件叫作随机事件,频率的稳定值叫作该随机事件的概率。</p><p>随机事件 $A$ 在条件 $S$ 下发生的概率为 $p$,记作:</p><p>$$ P(A)=p $$</p><h4 id="等概完备事件组">等概完备事件组</h4><p><strong>定义</strong>:称一个事件组 $A_1, A_2, A_3, \cdots, A_n$ 为一个<strong>等概完备事件组</strong>,如果它具有下列三条性质:</p><ol><li><strong>等可能性</strong>:$A_1, A_2, A_3, \cdots, A_n$ 发生的机会相同</li><li><strong>完备性</strong>:在人一次试验中,$A_1, A_2, A_3, \cdots, A_n$ 至少有一个发生(也就是所谓的“除此之外,不可能有别的结果”)</li><li><strong>互不相容性</strong>:在任一次试验中,$A_1, A_2, A_3, \cdots, A_n$ 至多有一个发生(也就是所谓“他们是互相排斥的”)</li></ol><p>等概完备事件组又称等概基本事件组,其中的任意事件 $A_i(i=1,2,\cdots,n)$ 称为<strong>基本事件</strong>。</p><p>对于只满足条件 2、3 的事件组,称为<strong>完备事件组</strong>。</p><h4 id="事件的运算">事件的运算</h4><ol><li><p>必然事件表示为 $U$,不可能事件表示为 $V$。</p></li><li><p>包含:如果事件 $A$ 发生,那么 $B$ 必发生,就称事件 $B$ 包含事件 $A$,记作<br>$$ A \subset B $$</p></li><li><p>相等:如果事件 $A$ 包含事件 $B$,同时事件 $B$ 包含事件 $A$,那么就称事件 $A$ 与 $B$ 相等或等价,记作<br>$$ A=B $$</p></li><li><p>并:事件“$A$ 或 $B$”称为事件 $A$ 与事件 $B$ 的并,记作<br>$$ A \cup B \quad 或 \quad A+B $$</p></li><li><p>交:事件“$A$ 且 $B$”称为事件 $A$ 和事件 $B$ 的交,记作<br>$$ A \cap B \quad 或 \quad AB \quad 或 \quad A \cdot B $$</p></li><li><p>对立事件:事件“非$A$”称为 $A$ 的对立事件,记作 $\overline{A}$,有<br>$$ A \cap \overline{A} = V $$<br>$$ A \cup \overline{A} = U $$</p></li><li><p>事件的差:事件 $A$ 同 $B$ 的差表示 $A$ 发生而 $B$ 不发生的事件,记作 $A \backslash B$,由定义可知<br>$$ A \backslash B = A \cap \overline{B} $$</p></li></ol><h4 id="事件的互不相容性">事件的互不相容性</h4><p>如果事件 $A$ 与事件 $B$ 不能同时发生,即:</p><p>$$ AB = V(不可能事件) $$</p><p>那么,称 $A$ 与 $B$ 是互不相容事件。</p><h4 id="概率的加法公式">概率的加法公式</h4><p>如果事件 $A$,$B$ 互不相容,则</p><p>$$ P(A \cup B) = P(A) + P(B) $$</p><h4 id="条件概率">条件概率</h4><p>如果 $A$,$B$ 是条件 $S$ 下的两个随机事件,$P(A) \neq 0$,则称在 $A$ 发生的前提下 $B$ 发生的概率为<strong>条件概率</strong>,记作 $P(B \mid A)$</p><h4 id="概率的乘法公式">概率的乘法公式</h4><p>$$ P(AB) = P(A) P(B \mid A) $$</p><p>进一步有</p><p>$$ P(A) P(B \mid A) = P(B) P(A \mid B) $$</p><h4 id="事件的独立性">事件的独立性</h4><p>事件 $A$ 的发生并不影响事件 $B$ 的发生,即:</p><p>$$ P(B \mid A) = P(B) $$</p><p>称两个事件 $A$,$B$ 是<strong>相互独立</strong>的。此时有:</p><p>$$ P(AB) = P(A) P(B) $$</p><h4 id="全概公式">全概公式</h4><p>设事件组 $A_1, A_2, A_3, \cdots, A_n$ 为完备事件组,则对任意一个事件 $B$ 有:</p><p>$$ P(B) = \sum_{i=1}^{n} P(B \mid A_i) P(A_i) $$</p><p>考虑 $n=2$ 时的简化情况,有:</p><p>$$ P(B) = P(B \mid A) P(A) + P(B \mid \overline{A}) P(\overline{A}) $$</p><h4 id="逆概公式">逆概公式</h4><p>设事件组 $A_1, A_2, A_3, \cdots, A_n$ 为完备事件组,则对任意一个事件 $B$ 有:</p><p>$$ P(A_j \mid B) = \frac{P(B \mid A_j) P(A_j)}{\sum_{i=1}^{n} P(B \mid A_i) P(A_i)} \; (j=1,\cdots,n) $$</p><p>逆概公式也称为<strong>贝叶斯公式</strong>,本质上是乘法公式与全概公式的结合,即:</p><p>$$ P(A_j \mid B) = \frac{P(A_j B)}{P(B)} = \frac{P(B \mid A_j) P(A_j)}{\sum_{i=1}^{n} P(B \mid A_i) P(A_i)} \; (j=1,\cdots,n) $$</p><h4 id="独立试验序列概型">独立试验序列概型</h4><p>设每次射击打中目标的概率为 $p$,连续射击 $n$ 次,求恰好打中 $k$ 次的概率。</p><p>设单次试验中,事件 $A$ 发生的概率为 $p(0 \lt p \lt 1)$,则在 $n$ 次重复实验中:</p><p>$$ P(A发生k次) = C_n^k p^k q^{n-k} \quad (q=1-p; k=0,1,2,\cdots,n) $$</p><h2 id="第二章-随机变量与概率分布">第二章 随机变量与概率分布</h2><h3 id="随机变量">随机变量</h3><p><strong>定义</strong>:对于条件组 $S$ 下的每一个可能结果 $\omega$ 都唯一的对应到一个实数值 $X(\omega)$,则称实值变量 $X(\omega)$ 为一个随机变量,简记为 $X$。</p><p>举个例子:设盒中有 5 个球,其中 2 个白球、3 个黑球,从中随便取 3 个球。则“抽得的白球数”$X$ 是一个随机变量。</p><p>随机变量分为<strong>离散型随机变量</strong>和<strong>连续型随机变量</strong>。</p><h3 id="一、离散型随机变量">一、离散型随机变量</h3><p>将随机变量 $X$ 的所有可能取值到其相应概率的映射称为 $X$ 的概率分布,记为:</p><p>$$ p_k = P\{X=x_k\} \quad (k=1,2,\cdots) $$</p><h4 id="常用的离散型随机变量的概率分布">常用的离散型随机变量的概率分布</h4><ol><li><p>两点分布<br>随机变量 $X$ 仅取两个值:0 或 1,即</p><p>$$ \begin{aligned}<br>& P\{X=1\}=p \quad (0 \lt p \lt 1) \\<br>& P\{X=0\}=q \quad (q=1-p)<br>\end{aligned} $$</p></li><li><p>二项分布<br>$$ P\{X=k\} = C_n^k p^k q^{n-k} \quad (k=0,1,2,\cdots,n;\; 0 \lt p \lt 1;\;q=1-p) $$</p><p>随机变量 $X$ 满足二项分布可简记为:$X \sim B(n,p)$</p></li><li><p>泊松分布<br>$$ P\{X=k\} = \frac{\lambda^k}{k!} e^{-\lambda} \quad (k=0,1,2,\cdots,n) $$</p><p>当 $\displaystyle \lim_{n \to \infty} np = \lambda$ 时,泊松分布等同于二项分布。</p></li><li><p>超几何分布<br>$$ P\{X=m\} = \frac{C_M^m C_{N-M}^{n-m}}{C_N^n} \quad (m=0,1,2,\cdots,l;\; 其中 l=\min(M,n)) $$</p><p>示例:设一堆同类产品共 $N$ 个,其中有 $M$ 个次品。现从中任取 $n$ 个(假定 $n \le N-M$),则这 $n$ 个样品中所含次品个数 $X$ 是一个离散型随机变量,其概率分布为超几何分布。</p></li></ol><h3 id="二、连续型随机变量">二、连续型随机变量</h3><h4 id="概率密度函数">概率密度函数</h4><p><strong>定义</strong>:对于随机变量 $X$,如果存在非负可积函数 $p(x)(-\infty \lt x \lt \infty)$,使对任意的 $a,b(a \lt b)$ 都有:</p><p>$$ P\{a \lt X \lt b\} = \int_a^b p(x) \mathrm{d}x $$</p><p>则称 $X$ 为<strong>连续性随机变量</strong>;称 $p(x)$ 为 $X$ 的<strong>概率密度函数</strong>,简称概率密度或密度。</p><p>与离散型随机变量类比:将离散型随机变量 $X$ 的离散值无限细分,则 $X$ 的概率分布将变为概率密度函数。</p><p>显然,概率密度函数满足以下两条性质:</p><ol><li><p>对任何实数 $a$,有<br>$$ P\{X=a\} = 0 $$</p></li><li><p>概率密度在整个实数轴上的积分为 1<br>$$ \int_{-\infty}^{\infty} p(x) \mathrm{d}x = 1 $$</p></li></ol><h4 id="常见概率密度函数">常见概率密度函数</h4><ol><li><p>均匀分布<br>如果随机变量 $X$ 的概率密度为</p><p>$$ p(x) = \begin{cases}<br>\lambda \qquad 当 a \le x \le b \\<br>0 \qquad 其他 \end{cases} \quad (a \lt b) $$</p><p>则称 $X$ 服从 $[a,b]$ 区间上的均匀分布</p></li><li><p>指数分布</p><p>$$ p(x) = \begin{cases}<br>\lambda e^{-\lambda x} & 当 x \ge 0 \\<br>0 & 当 x \lt 0 \end{cases} \quad (\lambda \gt 0) $$</p></li><li><p>正态分布</p><p>$$ p(x) = \frac{1}{\sqrt{2 \pi} \sigma} e^{-\frac{1}{2 \sigma^2}(x-\mu)^2} \quad (-\infty \lt x \lt \infty,\;\sigma \gt 0) $$</p><p>变量 $X$ 服从正态分布 $N(\mu,\sigma^2)$ 可简记为 $X \sim N(\mu,\sigma^2)$。</p><p><strong>标准正态分布</strong>:参数 $\mu=0, \sigma=1$ 时的正态分布,即 $N(0,1)$。它的密度函数为</p><p>$$ p(x) = \frac{1}{\sqrt{2 \pi}} e^{-\frac{x^2}{2}} $$</p><p><strong>一个重要的积分</strong>:</p><p>$$ \int_{-\infty}^{\infty} \frac{1}{\sqrt{2 \pi}} e^{-\frac{x^2}{2}} \mathrm{d}x = 1 $$</p><p>通过正态分布的密度函数求某个区间的概率时,需要计算密度函数的积分,这种计算非常复杂,因此我们通过已经计算好数值的 $\Phi$ 函数来帮助求解:</p><p>$$ \Phi(x) = \int_{-\infty}^{x} \frac{1}{\sqrt{2 \pi}} e^{-\frac{t^2}{2}} \mathrm{d}t $$</p><p>那么对于标准正态分布,有</p><p>$$ P\{a \lt X \lt b\} = \Phi(b) - \Phi(a) $$</p><p>对于一般正态分布 $N(\mu,\sigma^2)$,常常使用<strong>变量替换法</strong>将其转化为标准正态分布,即令</p><p>$$ t = \frac{x-\mu}{\sigma} $$</p><p>这时,$X \sim N(\mu,\sigma) \rightarrow T \sim N(0,1)$。这样,对于一般正态分布也能轻易地计算其积分了。</p></li><li><p>$\Gamma$ 分布</p><p>$$ p(x) = \begin{cases}<br>\frac{\beta^\alpha}{\Gamma(\alpha)} x^{\alpha-1} e^{-\beta x} & x \gt 0 \\<br>0 & x \le 0 \end{cases} \quad (\alpha \gt 0, \beta \gt 0) $$</p><p>其中</p><p>$$ \Gamma(\alpha) = \int_0^{\infty} x^{\alpha-1} e^{-x} \mathrm{d}x $$</p><p>变量 $X$ 服从 $\Gamma$ 分布可简记为 $X \sim \Gamma(\alpha, \beta)$</p></li><li><p>韦布尔分布</p><p>$$ p(x) = \begin{cases}<br>m \frac{x^{m-1}}{\eta^m} e^{-(\frac{x}{\eta})^m} & x \gt 0 \\<br>0 & x \le 0 \end{cases} $$</p></li></ol><h4 id="分布函数">分布函数</h4><p><strong>定义</strong>:设 $X$ 是一个随机变量(可以是连续型的,也可以是离散型的,甚至更一般的),称函数</p><p>$$ F(x) = P(X \le x) \quad (-\infty \lt x \lt +\infty) $$ 为 $X$ 的分布函数。</p><p>连续型随机变量的分布函数事实上是其概率密度函数在区间 $(-\infty, x]$ 上的不定上限积分。</p><h4 id="随机变量函数的分布">随机变量函数的分布</h4><p><strong>随机变量函数</strong>:随机变量 $X$ 的函数也是一个随机变量,记作</p><p>$$ Y = f(X) $$</p><p>满足当 $X$ 取值为 $x$ 时,$y$ 取值为 $f(x)$。</p><p>举个例子:设 $X$ 是分子的速率,而 $Y$ 是分子的动能,则 $Y$ 是 $X$ 的函数:$Y=\frac{1}{2}mX^2$($m$ 为分子质量)。</p><p>我们的目的是,根据已知的 $X$ 的分布来寻求 $Y=f(X)$ 的分布。</p><h5 id="离散型随机变量函数的分布">离散型随机变量函数的分布</h5><p>假设离散型随机变量 $X, Y$ 有如下关系:$Y=f(X)$。要得到 $P\{Y=y_i\}$,只需求出 $Y=y_i$ 时对应的 $x_i$(可能有 0 个或多个对应值),将这些 $x_i$ 对应的概率相加即可。</p><h5 id="连续型随机变量函数的分布">连续型随机变量函数的分布</h5><p><strong>分布函数法</strong>:已知 $X$ 的分布,通过建立 $Y$ 与 $X$ 的分布函数之间的关系来求得 $Y$ 的分布。</p><p>举个例子:已知 $X \sim N(\mu,\sigma^2)$,求 $Y=\frac{X-\mu}{\sigma}$ 的概率密度。</p><p>解:设 $Y$ 的分布函数为 $F_Y(y)$,于是</p><p>$$ \begin{aligned}<br>F_Y(y) & = P(Y \le y) \quad (分布函数的定义) \\<br>& = P(\frac{X-\mu}{\sigma} \le y) \quad (Y=\frac{X-\mu}{\sigma}) \\<br>& = P(X \le \sigma y + \mu) \quad (不等式变形) \\<br>& = F_X(\sigma y + \mu) \quad (分布函数的定义)<br>\end{aligned} $$</p><p>其中 $F_X(x)$ 为 $X$ 的分布函数。那么,我们有</p><p>$$ F_Y(y) = F_X(\sigma y + \mu) $$</p><p>将上式两边对 $y$ 求微分,利用<strong>密度函数是分布函数的导数</strong>的关系,我们得到</p><p>$$ p_Y(y) = p_X(\sigma y + \mu) \sigma $$</p><p>再将</p><p>$$ p_X(x) = \frac{1}{\sqrt{2 \pi} \sigma} e^{-\frac{1}{2 \sigma^2}(x-\mu)^2} $$</p><p>代入,有</p><p>$$ p_Y(y) = \frac{1}{\sqrt{2 \pi}} e^{-\frac{y^2}{2}} $$</p><p>这表明 $Y \sim N(0,1)$。</p><h2 id="第三章-随机变量的数字特征">第三章 随机变量的数字特征</h2><h3 id="随机变量的期望">随机变量的期望</h3><p>随机变量的期望 $E(X)$ 是一个实数,它形式上是 $X$ 所有可能取值的加权平均,代表了随机变量 $X$ 的平均值。因此,也称期望为<strong>均值</strong>或<strong>分布的均值</strong>。</p><h4 id="离散型随机变量的期望">离散型随机变量的期望</h4><p>$$ E(X) = \sum_k x_k p_k \quad (=x_1p_1+x_2p_2+\cdots+x_kp_k+\cdots) $$</p><h5 id="几个常用分布的期望">几个常用分布的期望</h5><ol><li><p>两点分布<br>$$ E(X) = 1 \cdot p + 0 \cdot q = p $$</p></li><li><p>二项分布<br>$$ E(X) = \sum_{k=1}^n k C_n^k p^k q^{n-k} = np $$</p></li><li><p>泊松分布</p><p>$$ \begin{aligned}<br>E(X) & = \sum_{k=0}^\infty k \cdot \frac{\lambda^k}{k!} e^{-\lambda} \\<br>& = \lambda e^{-\lambda} \sum_{m=0}^\infty \frac{\lambda^m}{m!} \quad (令m=k-1) \\<br>& = \lambda e^{-\lambda} e^{\lambda} \quad (泊松分布的密度之和为 1) \\<br>& = \lambda<br>\end{aligned} $$</p></li><li><p>超几何分布<br>$$ E(X) = \frac{nM}{N} $$</p></li></ol><h4 id="连续型随机变量的期望">连续型随机变量的期望</h4><p><strong>定义</strong>:设连续型随机变量$X$的密度函数为 $p(x)$,称</p><p>$$ \int_{-\infty}^{+\infty} xp(x) \mathrm{d}x $$</p><p>为 $X$ 的<strong>期望</strong>(或均值),记作 $E(X)$。</p><p>本定义要求 $\displaystyle \int_{-\infty}^{+\infty} \vert x \vert p(x) \mathrm{d}x$ 收敛</p><h5 id="几个常用分布的期望-2">几个常用分布的期望</h5><ol><li><p>均匀分布<br>$$ E(X) = \frac{1}{2}(b+a) $$</p></li><li><p>指数分布</p><p>$$ \begin{aligned}<br>E(X) & = \int_{-\infty}^{+\infty} xp(x) \mathrm{d}x \\<br>& = \int_{0}^{+\infty} \lambda x e^{-\lambda x} \mathrm{d}x \\<br>& = \frac{1}{\lambda} \int_0^{+\infty} te^{-t} \mathrm{d}t \quad (令t=\lambda x) \\<br>& = -\frac{1}{\lambda} \int_0^{+\infty} t \mathrm{d}e^{-t} \\<br>& = -\frac{1}{\lambda}\left[(te^{-t}) \Big|_0^{+\infty}-\int_0^{+\infty}e^{-t}\mathrm{d}t \right] \\<br>& = \frac{1}{\lambda}<br>\end{aligned} $$</p></li><li><p>正态分布<br>$$ E(X) = \mu $$</p><p>证明略。正态分布密度函数以 $x=\mu$ 为对称轴,这就是其含义所在。</p></li></ol><h4 id="期望的简单性质">期望的简单性质</h4><p>$$ \begin{aligned}<br>E© &= c \\<br>E(kX) &= kE(X) \\<br>E(X+b) &= E(X) + b \\<br>E(kX+b) &= kE(X) + b<br>\end{aligned} $$</p><p>一言以蔽之,<strong>期望是线性的</strong>。</p><h4 id="随机变量函数的期望">随机变量函数的期望</h4><p>对于离散型随机变量有</p><p>$$ E\left[f(X)\right] = \sum_i f(x_i)p_i $$</p><p>对于连续型随机变量有</p><p>$$ E\left[f(X)\right] = \int_{-\infty}^{+\infty} f(x)p(x) \mathrm{d}x $$</p><p><strong>求随机变量函数的期望</strong>有如下两种方法:</p><ol><li>利用上述随机变量函数的期望公式直接求解;</li><li>首先通过 $X$ 的分布推出 $f(X)$ 的分布,然后通过期望的定义求出 $f(X)$ 的期望。</li></ol><p>一般来说,第一种方法较为简单,是我们的首选方法。</p><h3 id="随机变量的方差">随机变量的方差</h3><p><strong>定义</strong>:</p><p>$$ D(X) = E \left\{ [X-E(X)]^2 \right\} $$ 这表明 $X$ 的方差,就是随机变量 $[X-E(X)]^2$ 的期望。</p><blockquote><p>💡 定性认识,$D(X)$ 越小,则 $X$ 取值越集中在 $E(X)$ 附近。方差刻画了随机变量取值的分散程度。</p></blockquote><p><strong>方差简化计算公式</strong>:</p><p>$$ D(X) = E(X^2) - E^2(X) $$</p><p>推导如下:</p><p>$$ \begin{aligned}<br>D(X) &= \int_{-\infty}^{+\infty} \left[x-E(X) \right]^2 p(x) \mathrm{d}x \\<br>&= \int_{-\infty}^{+\infty} \left[x^2-2xE(X)+E^2(X) \right] p(x) \mathrm{d}x \\<br>&= \int_{-\infty}^{+\infty}x^2p(x)\mathrm{d}x - 2E(X)\int_{-\infty}^{+\infty}xp(x)\mathrm{d}x + E^2(X)\int_{-\infty}^{+\infty}p(x)\mathrm{d}x \\<br>&= E(X^2) - 2E(X)\cdot E(X) + E^2(X)\cdot 1 \\<br>&= E(X^2) - E^2(X)<br>\end{aligned} $$</p><h4 id="离散型随机变量的方差">离散型随机变量的方差</h4><p><strong>定义</strong>:设离散型随机变量的概率分布为</p><p>$$ P(X=x_k) = P_k \quad (k=1,2,\cdots) $$</p><p>则称和数</p><p>$$ \sum_k \left[ x_k-E(X) \right]^2 p_k $$</p><p>为 $X$ 的方差,记作 $D(X)$。</p><h4 id="连续型随机变量的方差">连续型随机变量的方差</h4><p><strong>定义</strong>:设连续型随机变量的密度为 $p(x)$,则称</p><p>$$ \int_{-\infty}^{+\infty} \left[ x-E(X) \right]^2 p(x) \mathrm{d}x $$</p><p>为 $X$ 的方差,记作 $D(X)$。</p><h4 id="常用分布的方差">常用分布的方差</h4><ol><li><p>两点分布</p><p>$$ \begin{aligned}<br>D(X) &= E(X^2) - E^2(X) \\<br>&= (1^2 \cdot p + 0^2\cdot q) - p^2 \\<br>&= pq<br>\end{aligned} $$</p></li><li><p>二项分布<br>$$ D(X) = npq $$</p></li><li><p>泊松分布<br>已知 $E(X)=\lambda$,</p><p>$$ \begin{aligned}<br>E(X^2) &= \sum_{k=0}^{\infty} K^2 \cdot \frac{\lambda^k}{k!} e^{-\lambda} \\<br>&= \sum_{k=1}^{\infty} (k-1+1) \frac{\lambda^k}{(k-1)!} e^{-\lambda} \\<br>&= \lambda^2 \cdot \sum_{k=2}^{\infty} \frac{\lambda^{k-2}}{(k-2)!}e^{-\lambda} + \lambda \cdot \sum_{k=1}^{\infty} \frac{\lambda^{k-1}}{(k-1)!}e^{-\lambda} \\<br>&= \lambda^2 + \lambda<br>\end{aligned} $$</p><p>则</p><p>$$ D(X) = (\lambda^2 + \lambda) - \lambda^2 = \lambda $$</p></li><li><p>均匀分布<br>$$ D(X) = \frac{1}{12}(b-a)^2 $$</p></li><li><p>指数分布<br>$$ D(X) = \frac{1}{\lambda^2} $$</p></li><li><p>正态分布<br>$$ D(X) = \sigma^2 $$</p></li></ol><h4 id="方差的简单性质">方差的简单性质</h4><p>$$ \begin{aligned}<br>D© &= 0 \\<br>D(kX) &= k^2 D(X) \\<br>D(X+b) &= D(X) \\<br>D(kX+b) &= k^2 D(X)<br>\end{aligned} $$</p><h4 id="切比雪夫不等式">切比雪夫不等式</h4><p>$$ P\{ \vert X-E(X) \vert \ge \varepsilon \} \le \frac{D(X)}{\varepsilon^2} $$</p><h2 id="第四章-随机向量">第四章 随机向量</h2><p><strong>定义</strong>:我们称 $n$ 个随机变量 $X_1,X_2,\cdots,X_n$ 的整体 $\xi = (X_1,X_2,\cdots,X_n)$ 为 $n$ 维随机向量。</p><p>我们重点研究二维随机向量。</p><h3 id="二维随机向量的联合分布与边缘分布">二维随机向量的联合分布与边缘分布</h3><h4 id="离散型随机向量的概率分布">离散型随机向量的概率分布</h4><p>$\xi = (X,Y)$ 为二维离散型随机向量,当且仅当 $X,Y$ 都是离散型随机变量。</p><p>一般称</p><p>$$ P\{(X,Y)=(x_i,y_j)\} = p_{ij} \quad (i=1,2,\cdots ;j=1,2,\cdots) $$</p><p>为 $\xi=(X,Y)$ 的概率分布,也称为 $(X,Y)$ 的<strong>联合分布</strong>。常采用<strong>概率分布表</strong>来表示离散型随机向量的概率分布。这些 $p_{ij}$ 具有 2 条基本性质:</p><ol><li><p>非负:<br>$$ p_{ij} \ge 0 $$</p></li><li><p>概率总和为 1:<br>$$ \sum_i \sum_j p_{ij} = 1 $$</p></li></ol><p><strong>三项分布</strong>:</p><p>$$ P\{(X,Y)=(k_1,k_2)\} = \frac{n!}{k_1!k_2!(n-k_1-k_2)!}p_1^{k_1}p_2^{k_2}(1-p_1-p_2)^{n-k_1-k_2} $$</p><h4 id="离散型随机向量的边缘分布与联合分布">离散型随机向量的边缘分布与联合分布</h4><p><strong>边缘分布</strong>:对于二维随机向量 $(X,Y)$,分量 $X$ 的概率分布称为 $(X,Y)$ 的关于 $X$ 的边缘分布。</p><p>$$ P\{ X=x_i \} = \sum_j p_{ij} $$ $$ P\{ Y=y_j \} = \sum_i p_{ij} $$</p><p>如果将 $(X,Y)$ 的概率分布写在概率分布表中($i$ 为行数,$j$ 为列数),则关于 $X$ 的边缘分布为“将每行加和得到的一列”;关于 $Y$ 的边缘分布为“将每列加和得到的一行”。</p><h4 id="连续型随机向量的联合分布">连续型随机向量的联合分布</h4><p><strong>概念</strong>:对于二维随机向量 $\xi=(X,Y)$,如果存在非负函数 $p(x,y)\;(x,y \in \mathbb{R})$,使对于任意一个邻边分别平行于坐标轴的矩形区域 $D$(即由不等式 $a\lt x\lt b,c\lt y\lt d$ 确定的区域),有</p><p>$$ P\{ (X,Y) \in D \} = \iint\limits_{D} p(x,y)\mathrm{d}x\mathrm{d}y $$</p><p>则称随机向量 $\xi=(X,Y)$ 为<strong>连续型</strong>的,并称 $p(x,y)$ 为 $\xi$ 的<strong>分布密度</strong>,也称 $p(x,y)$ 为 $(X,Y)$ 的<strong>联合分布密度</strong>。</p><p>由定义式容易得到</p><p>$$ \int_{-\infty}^{+\infty}\int_{-\infty}^{+\infty}p(x,y)\mathrm{d}x\mathrm{d}y = 1 $$</p><blockquote><p>💡 二维随机向量 $(X,Y)$ 落在平面上任意区域 $D$ 的概率,就等于联合密度 $p(x,y)$ 在 $D$ 上的积分,这就把概率的计算转化为一个二重积分的计算。<br>💡 几何意义:$\{(X,Y)\in D\}$ 的概率,数值上就等于以曲面 $z=p(x,y)$ 为顶、以平面区域 $D$ 为底的曲顶柱体的体积。</p></blockquote><h4 id="连续型随机向量的边缘分布">连续型随机向量的边缘分布</h4><p><strong>定义</strong>:对于随机向量 $(X,Y)$,作为其分量的随机变量 $X$(或 $Y$)的密度函数 $p_X(x)$(或 $p_Y(y)$),称为 $(X,Y)$ 的关于 $X$(或 $Y$)的<strong>边缘分布密度</strong>。</p><p>当 $(X,Y)$ 的联合密度 $p(x,y)$ 已知时,可通过以下方法求得边缘密度</p><p>$$ \begin{aligned}<br>p_X(x) &= \int_{-\infty}^{+\infty}p(x,y)\mathrm{d}y \\<br>p_Y(y) &= \int_{-\infty}^{+\infty}p(x,y)\mathrm{d}x<br>\end{aligned} $$</p><h4 id="随机变量的独立性">随机变量的独立性</h4><p><strong>定义</strong>:设 $X,Y$ 是两个随机变量,如果对任意的 $a\lt b,c\lt d$,事件 $\{a\lt X\lt b\}$ 与 $\{c\lt Y\lt d\}$ 相互独立,则称 $X$ 与 $Y$ 是<strong>相互独立</strong>的。</p><p><strong>重要定理</strong>:设 $X,Y$ 分别有分布密度 $p_X(x),p_Y(y)$,则 $X$ 与 $Y$ 相互独立的<strong>充要条件</strong>是:二元函数</p><p>$$ p_X(x)p_Y(y) $$</p><p>是随机向量 $(X,Y)$ 的联合密度。</p><h4 id="二维正态分布">二维正态分布</h4><p>$$ p(x,y) = \frac{1}{2\pi \sigma_1\sigma_2\sqrt{1-\rho^2}}e^{-\frac{1}{2(1-\rho^2)}\left[\left(\frac{x-\mu_1}{\sigma_1}\right)^2 - \frac{2\rho(x-\mu_1)(y-\mu_2)}{\sigma_1\sigma_2} + \left(\frac{y-\mu_2}{\sigma_2}\right)^2\right]} $$</p><p>两个边缘密度分别是两个一维正态分布:</p><p>$$ \begin{aligned}<br>P_X(x)=\frac{1}{\sqrt{2\pi}\sigma_1}e^{-\frac{(x-\mu_1)^2}{2\sigma_1^2}} \\<br>P_Y(y)=\frac{1}{\sqrt{2\pi}\sigma_2}e^{-\frac{(y-\mu_2)^2}{2\sigma_2^2}}<br>\end{aligned} $$</p><p>对于二维正态分布,<strong>两个分量 $X$ 与 $Y$ 独立</strong>的充要条件是 $\rho=0$。</p><h4 id="二维随机向量的分布函数">二维随机向量的分布函数</h4><p><strong>定义</strong>:设 $\xi=(X,Y)$ 是二维随机向量,称函数</p><p>$$ F(x,y) = P\{ X \le x, Y \le y \} $$</p><p>为它的<strong>分布函数</strong>。</p><p>若 $\xi=(X,Y)$ 的分布函数有二阶连续偏微商,则</p><p>$$ \frac{\partial^2 F(x,y)}{\partial x \partial y} $$</p><p>就是 $\xi$ 的<strong>分布密度</strong>。</p><h3 id="两个随机变量的函数的分布">两个随机变量的函数的分布</h3><table><thead><tr><th>问题</th><th>描述</th><th>求解</th></tr></thead><tbody><tr><td>1 个随机变量的函数的分布</td><td>已知 $X$ 的分布,求 $X$ 的函数 $Y=f(X)$ 的分布</td><td><strong>分布函数法</strong></td></tr><tr><td>2 个随机变量的函数的分布</td><td>已知 $(X,Y)$ 的联合密度,求 $Z=(X,Y)$ 的密度函数</td><td><strong>分布函数法</strong></td></tr></tbody></table><p>对于两个随机变量的函数的分布,我们同样采用<strong>分布函数法</strong>求解,包括如下 2 步:</p><ol><li><p>为求随机变量 $f(X,Y)$ 的密度,先求它的分布,即<br>$$ P\{f(X,Y) \le z\} $$</p></li><li><p>在求 $P\{f(X,Y) \le z\}$ 的过程中,用到下列等式<br>$$ P\{f(X,Y) \le z\} = \iint\limits_{f(X,Y)\le z} p(x,y) \mathrm{d}x\mathrm{d}y $$</p></li></ol><p>举个例子:设 $X,Y$ 相互独立且服从相同的分布 $N(0,1)$,求 $\sqrt{X^2+Y^2}$ 的密度。</p><p><strong>解</strong>:$(X,Y)$ 的联合密度为</p><p>$$ \begin{aligned}<br>p(x,y) &= \frac{1}{\sqrt{2\pi}} e^{-\frac{x^2}{2}} \cdot \frac{1}{\sqrt{2\pi}} e^{-\frac{x^2}{2}} \\<br>&= \frac{1}{2\pi} e^{-\frac{x^2+y^2}{2}}<br>\end{aligned} $$</p><p>记 $Z=\sqrt{X^2+Y^2}$ 的分布函数为 $F_Z(z)$,则</p><p>$$ \begin{aligned}<br>F_Z(x) &= P\{Z \le z\} \\<br>&= P\{\sqrt{X^2+Y^2} \le z\} \\<br>&= \iint\limits_{\sqrt{x^2+y^2} \le z} p(x,y) \mathrm{d}x\mathrm{d}y \\<br>&= \iint\limits_{\sqrt{x^2+y^2} \le z} \frac{1}{2\pi} e^{-\frac{x^2+y^2}{2}} \mathrm{d}x\mathrm{d}y \\<br>&= \int_0^{2\pi} \mathrm{d}\theta \int_0^z \frac{1}{2\pi} e^{-\frac{1}{2}r^2}r \mathrm{d}r \quad (极坐标变换: x=r\cos\theta,y=r\sin\theta) \\<br>&= \int_0^z r e^{-\frac{1}{2} r^2} \mathrm{d}r<br>\end{aligned} $$</p><p>当 $z\le 0$ 时 $F_Z(z)=0$。于是 $Z$ 的密度 $p(z)$ 为</p><p>$$ p(z) = \begin{cases}<br>z e^{-\frac{1}{2} z^2} & z \gt 0 \\<br>0 & z \le 0<br>\end{cases} $$</p><p>这就是所谓的<strong>瑞利(Rayleigh)分布</strong>。</p><h4 id="随机变量函数的联合密度">随机变量函数的联合密度</h4><p><strong>问题描述</strong>:已知 $(X,Y)$ 的联合密度为 $p(x,y)$,而</p><p>$$ \begin{cases}<br>u = f(x,y) \\<br>v = g(x,y)<br>\end{cases} $$</p><p>如何求出 $(U,V)$ 的联合密度?</p><p><strong>step1</strong>:假设 $(X,Y)$ 的联合密度 $p(x,y)$ 所在的平面区域为 $A$(可以是全平面),即 $P\{(X,Y)\in A\}=1$,我们可以得到 $(U,V)$ 的联合密度所在的区域 $G$:</p><p>$$ G = \{ (u,v) \mid u=f(x,y),v=g(x,y),(x,y)\in A \} $$</p><p><strong>step2</strong>: 根据 $u=f(x,y),v=g(x,y)$ 我们用 $u,v$表示出 $x,y$:</p><p>$$ x = x(u,v), \; y = y(u,v) $$</p><p><strong>step3</strong>:$(U,V)$ 的联合密度如下:</p><p>$$ q(u,v) = \begin{cases}<br>p\left[ x(u,v),y(u,v) \right] \left| \frac{\partial(x,y)}{\partial(u,v)} \right| & 当(u,v) \in G \\<br>0 & 当(u,v) \not\in G<br>\end{cases} $$</p><p>其中,$\left| \frac{\partial(x,y)}{\partial(u,v)} \right|$ 是函数 $x(u,v),y(u,v)$ 的雅可比行列式的<strong>绝对值</strong>。</p><p>举个例子:设 $X,Y$ 相互独立,都服从 $N(0,1)$,</p><p>$$ \begin{aligned}<br>X &= R \cos \Theta \\<br>Y &= R \sin \Theta<br>\end{aligned}<br>\left( R \ge 0, \; 0 \le \Theta \le 2\pi \right) $$</p><p>求 $(R,\Theta)$ 的联合密度与边缘密度。</p><p><strong>解</strong>:由于 $X,Y$ 相互独立,则</p><p>$$ p(x,y) = \frac{1}{\sqrt{2\pi}} e^{-\frac{x^2}{2}} \cdot \frac{1}{\sqrt{2\pi}} e^{-\frac{y^2}{2}} = \frac{1}{2\pi} e^{-\frac{x^2+y^2}{2}} $$</p><p>雅可比行列式</p><p>$$ J = \left| \frac{\partial(x,y)}{\partial(r,\theta)} \right| = \left| \begin{array}{cc} \cos\theta & -r\sin\theta \\ \sin\theta & r\cos\theta \end{array} \right| = r $$</p><p>则 $(R,\Theta)$ 的联合密度为</p><p>$$ q(r,\theta) = \begin{cases}<br>\frac{1}{2\pi} r e^{-\frac{r^2}{2}} & r \gt 0,\; 0 \lt \theta \lt 2\pi \\<br>0 & 其他<br>\end{cases} $$</p><p>当 $r \gt 0$ 时,$R$ 的边缘密度为</p><p>$$ f® = \int_0^{2\pi} q(r,\theta) \mathrm{d}\theta = r e^{-\frac{r^2}{2}} $$</p><p>当 $0 \lt \theta \lt 2\pi$ 时,$\Theta$ 的边缘密度为</p><p>$$ g(\theta) = \int_0^{+\infty} q(r,\theta) \mathrm{d}r = \frac{1}{2\pi} $$</p><p>综上:</p><p>$$ f® = \begin{cases}<br>r e^{-\frac{r^2}{2}} & r \gt 0 \\<br>0 & 其他<br>\end{cases} $$</p><p>$$ g(\theta) = \begin{cases}<br>\frac{1}{2\pi} & 0 \lt \theta \lt 2\pi \\<br>0 & 其他<br>\end{cases} $$</p><h3 id="随机向量的数字特征">随机向量的数字特征</h3><h4 id="两个随机变量的均值公式">两个随机变量的均值公式</h4><p>设 $(X,Y)$ 的联合密度为 $p(x,y)$,令 $Z=f(X,Y)$,则有:</p><p>$$ E(Z) = E \left[ f(X,Y) \right] = \int_{-\infty}^{+\infty} \int_{-\infty}^{+\infty} f(x,y)p(x,y) \mathrm{d}x \mathrm{d}y $$</p><p>另外,也可以根据 $Z=f(x,y)$ 先求出 $Z$ 的密度 $p_Z(z)$ 然后再根据单个随机变量的均值公式</p><p>$$ E(Z) = \int_{-\infty}^{+\infty} z p_Z(z) \mathrm{d}z $$</p><p>求出 $Z$ 的均值。</p><h4 id="两个随机向量均值和方差的性质">两个随机向量均值和方差的性质</h4><p>设 $(X,Y)$ 的联合密度为 $p(x,y)$ ,$X,Y$ 的边缘密度分别为 $p_X(x), p_Y(y)$,由前面的知识我们已经知道,随机变量的均值和方差满足以下性质:</p><p>$$ \begin{aligned}<br>E(X) &= \int_{-\infty}^{+\infty} x p_X(x) \mathrm{d}x \\<br>E(Y) &= \int_{-\infty}^{+\infty} y p_Y(y) \mathrm{d}y \\<br>D(X) &= E \left( \left[ X-E(X) \right]^2 \right) = \int_{-\infty}^{+\infty} \left[ x-E(X) \right]^2 p_X(x) \mathrm{d}x \\<br>D(Y) &= E \left( \left[ Y-E(Y) \right]^2 \right) = \int_{-\infty}^{+\infty} \left[ y-E(Y) \right]^2 p_Y(y) \mathrm{d}y<br>\end{aligned} $$</p><p>另一套由联合密度 $p(x,y)$ 给出的计算公式与上述公式形式上非常相近,只是一重积分变成了二重积分:</p><p>$$ \begin{aligned}<br>E(X) &= \int_{-\infty}^{+\infty}\int_{-\infty}^{+\infty} x p(x,y) \mathrm{d}x\mathrm{d}y \\<br>E(Y) &= \int_{-\infty}^{+\infty}\int_{-\infty}^{+\infty} y p(x,y) \mathrm{d}x\mathrm{d}y \\<br>D(X) &= \int_{-\infty}^{+\infty}\int_{-\infty}^{+\infty} \left[ x-E(X) \right]^2 p(x,y) \mathrm{d}x\mathrm{d}y \\<br>D(Y) &= \int_{-\infty}^{+\infty}\int_{-\infty}^{+\infty} \left[ y-E(Y) \right]^2 p(x,y) \mathrm{d}x\mathrm{d}y<br>\end{aligned} $$</p><p>这几个公式的成立很容易证明,此处略去。</p><h4 id="两个随机变量的和的均值与方差">两个随机变量的和的均值与方差</h4><p>$$ E(X+Y) = E(X) + E(Y) \tag{1} $$</p><p>$$ D(X+Y) = D(X) + D(Y) + 2E \left( \left[X-E(X)\right] \left[Y-E(Y)\right] \right) \tag{2} $$</p><p>当 $X,Y$ 独立时,有</p><p>$$ E(X \cdot Y) = E(X) \cdot E(Y) \tag{3} $$</p><p>$$ D(X+Y) = D(X) + D(Y) \tag{4} $$</p><p>式 $(1)$ 容易证明,略去。</p><p>证明 $(2)$ 式:</p><p>$$ \begin{aligned}<br>D(X+Y) &= E \left( \left[ (X+Y)-E(X+Y) \right]^2 \right) \\<br>&= E \left( \left[ \left[X-E(X)\right] + \left[Y-E(Y)\right] \right]^2 \right) \\<br>&= E \left( \left[X-E(X)\right]^2 + \left[Y-E(Y)\right]^2 + 2\left[X-E(X)\right]\left[Y-E(Y)\right] \right) \\<br>&= E \left( \left[X-E(X)\right]^2 \right) + E \left( \left[Y-E(Y)\right]^2 \right) + E \left( 2\left[X-E(X)\right]\left[Y-E(Y)\right] \right) \\<br>&= D(X) + D(Y) + 2E \left( \left[X-E(X)\right] \left[Y-E(Y)\right] \right)<br>\end{aligned} $$</p><p>证明 $(3)$ 式:</p><p>$$ \begin{aligned}<br>E(X \cdot Y) &= \int_{-\infty}^{+\infty} \int_{-\infty}^{+\infty} xy p(x,y) \mathrm{d}x \mathrm{d}y \\<br>&= \int_{-\infty}^{+\infty} \int_{-\infty}^{+\infty} xy p_X(x) p_Y(y) \mathrm{d}x \mathrm{d}y \quad (由于X,Y相互独立) \\<br>&= \int_{-\infty}^{+\infty} x p_X(x) \mathrm{d}x \int_{-\infty}^{+\infty} y p_Y(y) \mathrm{d}y \\<br>&= E(X) \cdot E(Y)<br>\end{aligned} $$</p><p>证明 $(4)$ 式:</p><p>$$ \begin{aligned}<br>& E \left\{ \left[ X - E(X) \right] \left[ Y - E(Y) \right] \right\} \\<br>&= E \left\{ XY - X E(Y) - Y E(X) + E(X)E(Y) \right\} \\<br>&= E(XY) - E(X)E(Y) - E(X)E(Y) + E(X)E(Y) \\<br>&= E(XY) - E(X)E(Y) = 0<br>\end{aligned} $$</p><h4 id="随机向量的均值和协方差">随机向量的均值和协方差</h4><p>称向量 $(E(X),E(Y))$ 为随机向量 $(X,Y)$ 的均值,称数值 $E \left\{ \left[ X- E(X) \right] \left[ Y - E(Y) \right] \right\}$ 为 $X,Y$ 的<strong>协方差</strong>。</p><p>协方差(斜方差)是二维随机向量 $(X,Y)$ 的重要数字特征,它刻画了 $X,Y$ 取值间的相互联系,通常采用记号:</p><p>$$ cov(X,Y) \overset{\mathrm{def}}{=} E \left\{ \left[ X- E(X) \right] \left[ Y - E(Y) \right] \right\} $$</p><p>或</p><p>$$ \sigma_{XY} \overset{\mathrm{def}}{=} E \left\{ \left[ X- E(X) \right] \left[ Y - E(Y) \right] \right\} $$</p><p>由前面的讨论可知:</p><p>$$ \begin{aligned}<br>\sigma_{XY} &= cov(X,Y) \\<br>&= \int_{-\infty}^{+\infty} \int_{-\infty}^{+\infty} \left[ X- E(X) \right] \left[ Y - E(Y) \right] p(x,y) \mathrm{d}x \mathrm{d}y<br>\end{aligned} $$</p><p>当 $X,Y$ 相互独立时,协方差 $\sigma_{XY} = 0$。随机变量独立是协方差为0的<strong>充分不必要条件</strong>。</p><p>与记号 $\sigma_{XY}$ 相对应,$D(X),D(Y)$ 也可分别记为 $\sigma_{XX},\sigma_{YY}$。</p><h4 id="随机向量的相关系数">随机向量的相关系数</h4><p><strong>定义</strong>:称</p><p>$$ \rho_{XY} = \frac{\sigma_{XY}}{\sqrt{\sigma_{XX}}\sqrt{\sigma_{YY}}} $$</p><p>为 $X,Y$ 的<strong>相关系数</strong>,在不引起混淆的情况下,简记为 $\rho$。</p><p>事实上,二维正态分布中的第五个参数 $\rho$ 就是 $\rho_{XY}$。</p><p>相关系数满足以下性质:</p><p>$$ \left| \rho \right| \le 1 $$</p><blockquote><p>💡 相关系数 $\rho$ 的实际意义是:它刻画了 $X,Y$ 之间的线性关系的近似程度。一般来说,$\left| \rho \right|$ 越接近 1,$X$ 与 $Y$ 越接近地有线性关系。<br>要注意的是,$\rho$ 只刻画 $X$ 与 $Y$ 之间的线性关系,当 $X,Y$ 之间有很密切的曲线关系时,$\left| \rho \right|$ 的数值可能接近 1,也可能接近 0。</p></blockquote><h3 id="多维随机向量">多维随机向量</h3><p>对于一般的 $n$ 维随机向量,可仿照二维随机向量的情形进行讨论。</p><h4 id="联合密度与边缘密度">联合密度与边缘密度</h4><p>对于 $n$ 维随机向量 $\xi = ( X_1,X_2,\cdots,X_n )$ ,如果存在非负函数 $p(x_1,x_2,\cdots,x_n)$ ,使对于任意 $n$ 维长方体 $D = \left\{ (x_1,x_2,\cdots,x_n) \mid a_1 \lt x_1 \lt b_1,a_2 \lt x_2 \lt b_2,\cdots,a_n \lt x_n \lt b_n \right\}$ 均有:</p><p>$$ P \left\{ \xi \in D \right\} = \iint\limits_{D}\cdots \int p(x_1,x_2,\cdots,x_n) \mathrm{d}x_1 \mathrm{d}x_2 \cdots \mathrm{d}x_n $$</p><p>则称 $\xi = (X_1,X_2,\cdots,X_n)$ 是连续型的,并称 $p(x_1,x_2,\cdots,x_n)$ 为 $(X_1,X_2,\cdots,X_n)$ 的联合密度。</p><p>称 $(X_1,X_2,\cdots,X_n)$ 的一部分分量构成的向量——如 $(X_1,X_2)$ 的分布密度为边缘密度。特别地,每个分量 $X_i$的分布密度 $p_i(x_i)$ 当然也是边缘密度,称它们为<strong>单个密度</strong>。</p><p>$X_1$ 的单个密度可如下求得:</p><p>$$ p_1(x_1) = \int_{-\infty}^{+\infty} \int_{-\infty}^{+\infty} \cdots \int_{-\infty}^{+\infty} p(x_1,x_2,\cdots,x_n)\mathrm{d}x_2 \mathrm{d}x_3 \cdots \mathrm{d}x_n $$</p><p>$(X_1,X_2)$ 的边缘密度可如下求得:</p><p>$$ p_{12}(x_1,x_2) = \int_{-\infty}^{+\infty} \int_{-\infty}^{+\infty} \cdots \int_{-\infty}^{+\infty} p(x_1,x_2,\cdots,x_n)\mathrm{d}x_3 \mathrm{d}x_4 \cdots \mathrm{d}x_n $$</p><h4 id="独立性">独立性</h4><p>设 $X_1,X_2,\cdots,X_n$ 是 $n$ 个随机变量,如果对任意的 $a_i \lt b_i(i=1,2,\cdots,n)$ ,事件 $\left\{ a_1 \lt X_1 \lt b_1 \right\}, \left\{ a_2 \lt X_2 \lt b_2 \right\}, \cdots, \left\{ a_n \lt X_n \lt b_n \right\}$ 相互独立,则称 $X_1,X_2,\cdots,X_n$ 是<strong>相互独立</strong>的</p><p><strong>定理</strong>:设 $X_1,X_2,\cdots,X_n$ 的分布密度分别是 $p_1(x_1),p_2(x_2),\cdots,p_n(x_n)$ ,则 $X_1,X_2,\cdots,X_n$ 相互独立的<strong>充要条件</strong>是:$n$ 元函数</p><p>$$ p_1(x_1)p_2(x_2)\cdots p_n(x_n) $$</p><p>是 $(X_1,X_2,\cdots,X_n)$ 的联合密度。</p><h4 id="n-个随机变量的函数的分布">$n$ 个随机变量的函数的分布</h4><p>仍然采用<strong>分布函数法</strong>。设 $Z = f(X_1,X_2,\cdots,X_n)$ ,则 $Z$ 的分布为:</p><p>$$ \begin{aligned}<br>F_Z(z) &= P \left\{ f(X_1,X_2,\cdots,X_n) \le z \right\} \\<br>&= \iiint\limits_{f(x_1,x_2,\cdots,x_n) \lt z} p(x_1,x_2,\cdots,x_n) \mathrm{d}x_1 \mathrm{d}x_2 \cdots \mathrm{d}x_n<br>\end{aligned} $$</p><p>$Z$ 的分布函数 $F_Z(z)$ 对 $z$ 求微分可以进一步求出 $Z$ 的密度函数 $p_Z(z)$。</p><h4 id="数字特征">数字特征</h4><h5 id="均值公式">均值公式</h5><p>$$ E \left[ f(X_1,X_2,\cdots,X_n) \right] = \int_{-\infty}^{+\infty} \int_{-\infty}^{+\infty} \cdots \int_{-\infty}^{+\infty} f(x_1,x_2,\cdots,x_n) p(x_1,x_2,\cdots,x_n) \mathrm{d}x_1 \mathrm{d}x_2 \cdots \mathrm{d}x_n $$</p><p>其中 $p(x_1,x_2,\cdots,x_n)$ 是 $(X_1,X_2,\cdots,X_n)$ 的联合密度。本公式要求右端的积分绝对收敛。</p><h5 id="均值与方差的性质">均值与方差的性质</h5><p>$$ E(X_1+X_2+\cdots+X_n) = E(X_1) + E(X_2) + \cdots + E(X_n) $$</p><p>当 $X_1,X_2,\cdots,X_n$ 相互独立时,有:</p><p>$$ \begin{aligned}<br>E(X_1 X_2 \cdots X_n) &= E(X_1) E(X_2) E(X_n) \\<br>D(X_1+X_2+\cdots+x_n) &= D(X_1) + D(X_2) + \cdots + D(X_n)<br>\end{aligned} $$</p><h5 id="协方差与协差阵">协方差与协差阵</h5><p>对于 $i \neq j$ ,$\sigma_{ij}$ 是第 $i$ 个分量 $X_i$ 与第 $j$ 个分量 $X_j$ 的协方差;而 $\sigma_{ii}$ 是第 $i$ 个分量 $X_i$ 的方差。称矩阵:</p><p>$$ \begin{bmatrix}<br>\sigma_{11} & \sigma_{12} & \cdots & \sigma_{1n} \\<br>\sigma_{21} & \sigma_{22} & \cdots & \sigma_{2n} \\<br>\vdots & \vdots & \ddots & \vdots \\<br>\sigma_{n1} & \sigma_{n2} & \cdots & \sigma_{nn} \\<br>\end{bmatrix} $$</p><p>为 $(X_1,X_2,\cdots,X_n)$ 的协差阵,记为 $\mathbf{\Sigma}$。$\mathbf{\Sigma}$ 显然是对称矩阵,且可以验证 $\mathbf{\Sigma}$ 是非负定的。</p><h5 id="相关系数与相关阵">相关系数与相关阵</h5><p>$$ \rho_{ij} = \frac{\sigma_{ij}}{\sqrt{\sigma_{ii}}\sqrt{\sigma_{jj}}} \quad (i=1,2,\cdots,n; \; j=1,2,\cdots,n) $$</p><p>对于 $i \neq j$ ,$\rho_{ij}$ 是 $X_i,X_j$ 的相关系数。同时有 $\rho_{ii}=1$。称矩阵</p><p>$$ \begin{bmatrix}<br>\rho_{11} & \rho_{12} & \cdots & \rho_{1n} \\<br>\rho_{21} & \rho_{22} & \cdots & \rho_{2n} \\<br>\vdots & \vdots & \ddots & \vdots \\<br>\rho_{n1} & \rho_{n2} & \cdots & \rho_{nn} \\<br>\end{bmatrix} $$</p><p>为 $(X_1,X_2,\cdots,X_n)$ 的相关阵,记为 $\mathbf{R}$。显然,$\mathbf{R}$ 是对称矩阵。</p><h5 id="n-维分布函数">$n$ 维分布函数</h5><p><strong>定义</strong>:设 $\xi = (X_1,X_2,\cdots,X_n)$ 是 $n$ 维随机向量,称 $n$ 维函数 $F(x_1,x_2,\cdots,x_n)=P \left\{ X_1\le x_1,X_2\le x_2,\cdots,X_n\le x_n \right\}$ 为 $\xi$ 的<strong>分布函数</strong>。</p><p>如果 $\xi$ 的分布密度为 $p(x_1,x_2,\cdots,x_n)$ ,则有:</p><p>$$ F(x_1,x_2,\cdots,x_n) = \int_{-\infty}^{x_1} \int_{-\infty}^{x_2} \cdots \int_{-\infty}^{x_n} p(u_1,u_2,\cdots,u_n) \mathrm{d}u_1 \mathrm{d}u_2 \cdots \mathrm{d}u_n $$</p><h3 id="大数定律和中心极限定理">大数定律和中心极限定理</h3><h4 id="大数定律">大数定律</h4><p>设 $X_1,X_2,\cdots,X_n,\cdots$ 是独立同分布的随机变量列,且 $E(X_1),D(X_1)$ 存在,则对任意的 $\varepsilon \gt 0$,有:</p><p>$$ \lim_{n \to \infty}P \left\{ \left| \frac{S_n}{n} - E(X_1) \right| \ge \varepsilon \right\} = 0 $$</p><p>这说明,<strong>只要 $n$ 足够大,算术平均值 $\frac{1}{n} (X_1+X_2+\cdots+X_n)$ 将无限接近于期望</strong>。这是整个概率论所基于的基本定理。</p><h4 id="强大数定律">强大数定律</h4><p>经过细致的研究发现,只要 $E(X_1)$ 存在,不管 $D(X_1)$ 是否存在,大数定律依然成立,而且可以得到更强的结论:</p><p>$$ P \left\{ \lim_{n\to\infty} \frac{S_n}{n} =E(X_1) \right\} = 1 $$</p><p>将该式称为强大数定律。</p><h4 id="中心极限定理">中心极限定理</h4><p>设 $X_1,X_2,\cdots,X_n,\cdots$ 是独立同分布的随机变量列,且 $E(X_1),D(X_1)$ 存在,$D(X_1) \neq 1$,则对一切实数 $a \lt b$,有:</p><p>$$ \lim_{n\to\infty}P \left\{ a \lt \frac{S_n-n E(X_1)}{\sqrt{n D(X_1)}} \lt b \right\} = \int_{a}^{b} \frac{1}{\sqrt{2\pi}} e^{-\frac{u^2}{2}} \mathrm{d}u $$</p><p>这里,$S_n = X_1+X_2+\cdots+X_n$</p><p>如果记 $\overline{X} = \frac{1}{n}(X_1+X_2+\cdots+X_n)$,上式也可写成:</p><p>$$ \lim_{n\to\infty} P \left\{ a \lt \frac{\overline{X}-E(X_1)}{\sqrt{D(X_1)/n}} \lt b \right\} = \int_{a}^{b} \frac{1}{\sqrt{2\pi}} e^{-\frac{u^2}{2}} \mathrm{d}u $$</p><p>这表明,只要 $n$ 足够大,随机变量 $\frac{\overline{X}-E(X_1)}{\sqrt{D(X_1)/n}}$ 就近似地服从标准正态分布,从而 $\overline{X}$ 近似地服从正态分布。故<strong>中心极限定理表达了正态分布在概率论中的特殊地位</strong>,尽管 $X_1$ 的分布是任意的,但只要 $n$ 充分大,算数平均值 $\overline{X}$ 的分布却是近似正态的。</p><h2 id="第五章-统计估值">第五章 统计估值</h2><h3 id="总体与样本">总体与样本</h3><p><strong>样本定义</strong>:称随机变量 $X_1,X_2,\cdots,X_n$ 为来自总体 $X$ 的容量为 $n$ 的样本,如果 $X_1,X_2,\cdots,X_n$ <strong>相互独立</strong>,而且每个 $X_i$ 与 $X$ 有相同的概率分布。这时,若 $X$ 有分布密度 $p(x)$ ,则常简称 $X_1,X_2,\cdots,X_n$ 是来自总体 $p(x)$ 的样本。</p><p><strong>定理</strong>:若 $X_1,X_2,\cdots,X_n$ 是来自总体的 $p(x)$ 的样本,则 $(X_1,X_2,\cdots,X_n)$ 有联合密度 $p(x_1)p(x_2)\cdots p(x_n)$ 。</p><h3 id="分布函数与分布密度的估计">分布函数与分布密度的估计</h3><h4 id="经验分布函数">经验分布函数</h4><p>设 $X$ 是一个随机变量,具有一系列样本值 $x_1,x_2,\cdots,x_n$ ,称函数</p><p>$$ F_n(x) = \frac{v_n}{n} $$</p><p>为 $X$ 的经验分布函数。其中,$v_n$ 为 $x_1,x_2,\cdots,x_n$ 中不超过 $x$ 的个数。</p><h4 id="经验分布密度">经验分布密度</h4><p>经验分布密度可采用经验分布函数进行估计。</p><p>当 $h$ 足够小时,易知</p><p>$$ p(x)=\frac{F(x+h)-F(x-h)}{2h} $$</p><p>对应地,可以得到:</p><p>$$ \hat{p_n}(x)=\frac{F_n(x+h)-F_n(x-h)}{2h} $$</p><p>具体方法包括:</p><h5 id="1-直方图法">(1) 直方图法</h5><p>作直方图,当分组数足够大,分组间距足够小时,所有小矩形顶端的连线近似刻画了分布密度函数</p><h5 id="2-核估计法">(2) 核估计法</h5><p><strong>核函数定义</strong>:设 $K(x)$ 是非负函数且 $\int_{-\infty}^{+\infty}K(x)\mathrm{d}x = 1$ ,则称 $K(x)$ 是核函数。核函数有很大的选择自由,例如:</p><p>$$ K_0(x) = \begin{cases}<br>1/2 \quad & -1\le x\lt 1 \\<br>0 \quad & \text{其他}<br>\end{cases} $$</p><p>$$ K_1(x) = \begin{cases}<br>1 \quad & -1/2 \le x \lt 1/2 \\<br>0 \quad & \text{其他}<br>\end{cases} $$</p><p>$$ K_2(x) = \frac{1}{\sqrt{2\pi}}e^{-x^2/2} $$</p><p>$$ K_3(x) = \frac{1}{\pi(1+x^2)} $$</p><p>$$ K_4(x) = \frac{1}{2\pi}\left( \frac{\sin(x/2)}{x/2} \right)^2 $$</p><p><strong>核估计</strong>:称函数</p><p>$$ \hat{p_n}(x) = \frac{1}{nh}\sum_{i=1}^{n}K \left( \frac{x-x_i}{h} \right) $$</p><p>为 $p(x)$ 的核估计。其中,$h$ 为一个较小的常数(参考直方图法中的分组宽度),$x_i$ 为样本值。</p><blockquote><p>可以这样理解核估计中核函数 $K \left( \frac{x-x_i}{h} \right)$ 的作用:<br>随机变量 $X$ 在 $x$ 处的概率由核函数确定,核函数将散落在 $x$ 附近一定范围内(若干单位个 $h$ 值)的所有样本点 $x_i$ 作为 $P\{X=x\}$ 的一部分权重。而 $\displaystyle \sum_{i=1}^{n}K \left( \frac{x-x_i}{h} \right)$ 即为所有样本点对 $P\{X=x\}$ 贡献权重的总和。</p></blockquote><h5 id="3-最近邻估计法">(3) 最近邻估计法</h5><h3 id="最大似然估计">最大似然估计</h3><p><strong>适用情况</strong>:已知随机变量的分布类型,但不知道参数的值,在此种情况下要得到分布密度可采用最大似然估计法。</p><p>例如:已知随机变量 $X$ 满足正态分布,但不知道 $\mu,\sigma^2$ 的值,此时可采用最大似然估计法。</p><p><strong>似然函数</strong>:假设已知随机变量 $X$ 的分布密度为 $p(x;\theta_1,\theta_2,\cdots,\theta_m)$ ,但不知道其中的参数 $\theta_1,\theta_2,\cdots,\theta_m$ ,现给定样本值 $x_1,x_2,\cdots,x_n$ ,称函数</p><p>$$ L_n(x_1,x_2,\cdots,x_n;\theta_1,\theta_2,\cdots,\theta_m)=\prod_{i=1}^{n}p(x_i;\theta_1,\theta_2,\cdots,\theta_m)$$</p><p>为样本 $x_1,x_2,\cdots,x_n$ 的似然函数。</p><p><strong>最大似然估计</strong>:如果 $L_n(x_1,x_2,\cdots,x_n;\theta_1,\theta_2,\cdots,\theta_m)$ 在 $\hat{\theta}_1,\hat{\theta}_2,\cdots,\hat{\theta}_m$ 达到最大值,则称 $\hat{\theta}_1,\hat{\theta}_2,\cdots,\hat{\theta}_m$ 分别是 $\theta_1,\theta_2,\cdots,\theta_m$ 的最大似然估计。</p><p>由于 $\ln L_n$ 与 $L_n$ 同时达到最大值,为了简化计算,常常采用 $\ln L_n$ 来描述。那么如何才能使得 $\ln L_n$ 达到最大值呢?可以利用“最大值点的一阶偏微分为0”这一性质,列出<strong>似然方程组</strong>:</p><p>$$ \left\{ \begin{aligned}<br>\frac{\partial\ln L_n}{\partial \theta_1} &= 0 \\<br>\frac{\partial\ln L_n}{\partial \theta_2} &= 0 \\<br>\cdots \cdots \\<br>\frac{\partial\ln L_n}{\partial \theta_m} &= 0 \\<br>\end{aligned} \right. $$</p><p>如此便可解得 $\hat{\theta}_1,\hat{\theta}_2,\cdots,\hat{\theta}_n$ 。</p><h3 id="期望和方差的点估计">期望和方差的点估计</h3><p>有时并不需要求得密度函数,而只需获得某些数字特征,这类估计称作点估计。</p><h4 id="期望的点估计">期望的点估计</h4><p>利用 $\displaystyle \overline{X}=\frac{X_1+X_2+\cdots+X_n}{n}$ 来估计期望 $E(x)$ <strong>不存在系统偏差</strong>。即:</p><p>$$ E(\overline{X})=E(X) $$</p><p>证明:</p><p>$$ \begin{aligned}<br>E(\overline{X}) &= E \left( \frac{X_1+X_2+\cdots+X_n}{n} \right) \\<br>&= \frac{1}{n}\left[ E(X_1)+E(X_2)+\cdots+E(X_n) \right] \\<br>&= E(X)<br>\end{aligned} $$</p><p>同理还可以得到:</p><p>$$ D(\overline{X})=\frac{D(X)}{n} $$</p><p>这说明,样本数量 $n$ 越大,用 $\overline{X}$ 来估计 $E(X)$ 的波动越小,即估计越优良。</p><h4 id="方差的点估计">方差的点估计</h4><p>利用 $\displaystyle S^2=\frac{1}{n-1}\sum_{i=1}^{n}(x_i-\overline{x})^2$ 来估计方差 $D(X)$ 不存在系统偏差。即:</p><p>$$ E(S^2) = D(X) $$</p><p>需要注意,我们习惯使用的 $\displaystyle \frac{1}{n}\sum_{i=1}^{n}(x_i-\overline{x})^2$ 并不是方差的无偏估计量。</p><h3 id="期望的置信区间">期望的置信区间</h3><p>期望的点估计只是得到了期望的一个近似值,那么该近似值 $\overline{X}$ 与真实值 $E(X)$ 到低相差多少呢?这就涉及到<strong>区间估计问题</strong>。</p><h4 id="已知方差,对期望进行区间估计">已知方差,对期望进行区间估计</h4><p>对于任意随机变量 $X$ ,根据中心极限定理可知,随机变量</p><p>$$ \eta = \frac{\overline{X}-E(X)}{\sqrt{\frac{D(X)}{n}}} $$</p><p>是服从标准正态分布的。查表可以得到</p><p>$$ P \left\{ \left| \eta \right|\le 1.96 \right\}=0.95 $$</p><p>也即 $E(X)$ 落在区间</p><p>$$ \left[ \overline{X}-1.96 \sqrt{\frac{D(X)}{n}},\;\overline{X}+1.96 \sqrt{\frac{D(X)}{n}} \right] $$</p><p>以内的概率为 $95%$ 。</p><p>这就是 $E(X)$ 的<strong>置信区间</strong>,<strong>置信度</strong>为 $95%$ 。</p><h4 id="未知方差,对期望进行区间估计">未知方差,对期望进行区间估计</h4><p>未知方差时,不能使用上述的置信区间公式,但我们自然会想到利用方差的无偏估计量 $S^2$ 来替代方差,即研究随机变量</p><p>$$ T = \frac{\overline{X}-E(X)}{\sqrt{S^2/n}} $$</p><p>的分布。经过复杂的推导发现,随机变量 $T$ 服从 $n-1$ 个自由度的 $t$ 分布:</p><p>$$ p_n(t)=\frac{\Gamma(n/2)}{\sqrt{(n-1)\pi}\Gamma((n-1)/2)}\left( 1+\frac{t^2}{n-1} \right)^{-n/2} $$</p><p>这样就得到了 $E(X)$ 的置信区间,如下:</p><p>$$ \left[ \overline{X}-\lambda \sqrt{\frac{S^2}{n}},\;\overline{X}+\lambda \sqrt{\frac{S^2}{n}} \right] $$</p><p>其中 $\lambda$ 可以通过查找 $t$ 分布的<strong>临界值表</strong>获得。</p><h3 id="方差的置信区间">方差的置信区间</h3><p>以下讨论只适用于<strong>服从正态分布</strong>的随机变量。</p><p>从计算期望的置信区间中我们受到如下启发:</p><blockquote><p>要求某个量的置信区间,我们首先通过该量构造一个特殊的随机变量 $\eta$,使得 $\eta$ 的分布与所研究的随机变量 $X$ 无关,而只与样本容量 $n$ 有关。然后通过给定的置信度从 $\eta$ 的分布的临界值表中反解出置信区间。</p></blockquote><p>我们构造随机变量 $\displaystyle \eta=\frac{(n-1)S^2}{\sigma^2}$ ,得出其分布为 $n-1$ 个自由度的 $\chi^2$ 分布,即:</p><p>$$ p(u)=\begin{cases}<br>\frac{1}{2^{\frac{n-1}{2}}\Gamma(\frac{n-1}{2})} u^{(n-3)/2} e^{-u/2} \quad & u\gt 0 \\<br>0 & u\le 0\\<br>\end{cases} $$</p><p>进而得出 $\sigma^2$ 的置信区间为:</p><p>$$ \left[ \frac{(n-1)S^2}{\lambda_2},\;\frac{(n-1)S^2}{\lambda_1} \right] $$</p><p>也即:</p><p>$$ \left[ \frac{\sum_{i=1}^{n}(X_i- \overline{X})^2}{\lambda_2},\; \frac{\sum_{i=1}^{n}(X_i- \overline{X})^2}{\lambda_1} \right] $$</p><p>式中 $\lambda_1,\lambda_2$ 可以通过查找 $\chi^2$ 分布的临界值表得到。</p><h2 id="第六章-假设检验">第六章 假设检验</h2><h3 id="问题的提法">问题的提法</h3><p><strong>例 1</strong>:某厂有一批产品,共 200 件,须经检验合格才能出厂,按国家标准,次品率不得超过 1% ,今在其中任意抽取 5 件,发现这 5 件含有次品。问这批产品能否出厂?</p><p>从直觉上看,这批产品当然是不能出厂的,但为什么呢?</p><p><strong>例 2</strong>:怎样根据一个随机变量的样本值,判断该随机变量是否服从正态分布 $N(\mu,\sigma^2)$?</p><p><strong>假设检验问题</strong>:这类问题中都隐含着一种“假设”或“看法”,例 1 中的假设是:次品率 $p \le 0.01$,例 2 中的假设是:该随机变量服从正态分布 $N(\mu,\sigma^2)$ ,现在我们要检验这些假设是否正确,这类问题称为<strong>假设检验问题</strong>。</p><p>回到例 1:要检验的假设是 $p\le 0.01$ ,如果假设成立,我们看看会出现什么后果。此时,假设有 200 件样品,那么其中最多有 2 件次品,任意抽取 5 件,我们来求 5 件中无次品的概率:</p><p>$$ P \left\{ \text{无次品} \right\} \ge \frac{C_{198}^5}{C_{200}^5} \ge 0.95 $$</p><p>于是,任抽 5 件,出现次品的概率 $\le 1-0.95=0.05$ 。这说明,如果次品率 $\le 0.01$ ,那么抽取 5 件样品,出现次品的机会是很小的,平均在 100 次抽样中,出现不到 5 次。而现在的事实是,在一次抽样实践中,竟然就发生了这种小概率事件,这是不合理的!因此假设 $p\le 0.01$ 是不能接受的。</p><blockquote><p>注:通常把概率不超过 0.05 的事件当做“小概率事件”,有时也把概率不超过 0.01 的事件当做小概率事件。</p></blockquote><p>以上分析过程可概括为<strong>概率性质的反证法</strong>。</p><h3 id="一个正态总体的假设检验">一个正态总体的假设检验</h3><p>设 $X \sim N(\mu,\sigma^2)$ ,关于它的假设检验问题,主要是下列四种:</p><ol><li>已知方差 $\sigma^2$ ,检验假设 $H_0: \mu = \mu_0$ ($\mu_0$ 是已知数)。</li><li>未知方差 $\sigma^2$ ,检验假设 $H_0: \mu = \mu_0$ ($\mu_0$ 是已知数)。</li><li>未知期望 $\mu$ ,检验假设 $H_0: \sigma^2 = \sigma_0^2$ ($\sigma_0$ 是已知数)。</li><li>未知期望 $\mu$ ,检验假设 $H_0: \sigma^2 \le \sigma_0^2$ ($\sigma_0$ 是已知数)。</li></ol><p>以下分别介绍。</p><h4 id="1-已知方差,检验期望">1. 已知方差,检验期望</h4><p>我们首先假设 $H_0$ 成立,看在该条件下会不会产生不合理的现象。</p><p>在 $\mu=\mu_0$ 的条件下,有 $X \sim N(\mu_0,\sigma^2)$ ,假设有样品 $X_1,X_2,\cdots,X_n$ ,由中心极限定理可知:</p><p>$$ U = \frac{\overline{X}-\mu_0}{\sqrt{\sigma^2/n}} \sim N(0,1) $$</p><p>查正态分布表可知:</p><p>$$ P \left\{ \left| \frac{\overline{X}-\mu_0}{\sqrt{\sigma^2/n}} \right| \gt 1.96 \right\} = 0.05 $$</p><p>该式描述了一个小概率事件,也就是说,如果我们用样本 $X_1,X_2,\cdots,X_n$ 实际计算出来的 $\overline{X}$ 满足该式,那么假设 $H_0$ 就是不合理的,则假设不成立,也称为<strong>假设不相容</strong>。</p><blockquote><p>事实上,以上计算过程完全<strong>等效于求置信区间问题</strong>。其等效解法为:先根据 $\sigma^2$ 和样本 $X_1,X_2,\cdots,X_n$ 求出 $\mu$ 的置信区间,如果 $\mu_0$ 在该区间内,则认为假设 $H_0$ 成立,否则认为假设不成立。</p></blockquote><p><strong>两类错误</strong>:从以上的分析过程中我们可以看到,当一个事件为小概率事件时,我们就认为它绝对不可能发生,这显然是不合理的,有时会造成错误:</p><p>当一个假设实际上是成立的,我们根据对样本的计算却判定其不成立,即犯了“以真为假”的错误,这种错误称为<strong>第一类错误</strong>。</p><p>反之,当一个假设实际上是不成立的,我们根据对样本的计算判定其成立,即犯了“以假为真”的错误,这种错误称为<strong>第二类错误</strong>。</p><h4 id="2-未知方差,检验期望">2. 未知方差,检验期望</h4><p>可转化为求置信区间问题,我们前面已经讲述过了,此处不再赘述。关键点是:构造随机变量</p><p>$$ T = \frac{\overline{X}-\mu}{\sqrt{S^2/n}} $$</p><p>$T$ 应符合 $n-1$ 个自由度的 $t$ 分布。</p><h4 id="3-未知期望,检验方差">3. 未知期望,检验方差</h4><h4 id="4-未知期望,检验方差的上限">4. 未知期望,检验方差的上限</h4><p>同样采用求置信区间的思路,关键点是:构造随机变量</p><p>$$ W = \frac{(n-1)S^2}{\sigma^2} $$</p><p>$W$ 应符合 $n-1$ 个自由度的 $\chi^2$ 分布。</p><h3 id="两个正态总体的假设检验">两个正态总体的假设检验</h3><p>在实际问题中,除了遇到一个总体的检验问题,还常遇到两个总体的比较问题。</p><p>设 $X \sim N(\mu_1,\sigma_1^2)$ ,$Y \sim N(\mu_2,\sigma_2^2)$ ,且 $X, Y$ 相互独立,主要研究以下四类问题:</p><ol><li>未知 $\sigma_1^2,\sigma_2^2$,但知道 $\sigma_1^2=\sigma_2^2$ ,检验假设 $H_0:\mu_1=\mu_2$</li><li>未知 $\mu_1,\mu_2$,检验假设 $H_0:\sigma_1^2 = \sigma_2^2$</li><li>未知 $\mu_1,\mu_2$,检验假设 $H_0:\sigma_1^2 \le \sigma_2^2$</li><li>未知 $\sigma_1^2,\sigma_2^2$,但知道 $\sigma_1^2 \ne \sigma_2^2$ ,检验假设 $H_0:\mu_1=\mu_2$</li></ol><p>以下分别讨论。</p><p><strong>1. 未知 $\sigma_1^2,\sigma_2^2$ ,但知道 $\sigma_1^2=\sigma_2^2$ ,检验假设 $H_0:\mu_1=\mu_2$</strong></p><p>设 $X_1,X_2,\cdots,X_{n_1}$ 来自总体 $N(\mu_1,\sigma_1^2)$,$Y_1,Y_2,\cdots,Y_{n_2}$ 来自总体 $N(\mu_2,\sigma_2^2)$,且 $X,Y$ 间相互独立。现已知 $\sigma_1^2=\sigma_2^2$,如何检验假设 $H_0:\mu_1=\mu_2$?</p><p>类比前面的研究方法,我们构造一个特殊的统计量:</p><p>$$ \widetilde{T} = \frac{(\overline{X}-\overline{Y})-(\mu_1-\mu_2)}{\sqrt{(n_1-1)S_1^2+(n_2-1)s_2^2}} \cdot \sqrt{\frac{n_1 n_2 (n_1+n_2-2)}{n_1+n_2}} $$</p><p>数学上可以证明 $\widetilde{T}$ 服从 $n_1+n_2-2$ 个自由度的 $t$ 分布。</p><p><strong>2. 未知 $\mu_1,\mu_2$ ,检验假设 $H_0:\sigma_1^2 = \sigma_2^2$</strong></p><p>构造特殊的统计量:</p><p>$$ \widetilde{F} = \frac{S_1^2/\sigma_1^2}{S_2^2/\sigma_2^2} $$</p><p>数学上可以证明 $\widetilde{F}$ 服从自由度为 $n_1-1, n_2-1$ 的 $F$ 分布,其中,$n_1-1,n_2-1$ 分别称为<strong>第一自由度</strong>和<strong>第二自由度</strong>。</p><p><strong>3. 未知 $\mu_1,\mu_2$ ,检验假设 $H_0:\sigma_1^2 \le \sigma_2^2$</strong></p><p>同 2.</p><p><strong>4. 未知 $\sigma_1^2,\sigma_2^2$ ,但知道 $\sigma_1^2 \ne \sigma_2^2$ ,检验假设 $H_0:\mu_1=\mu_2$</strong></p><p>这是著名的 Behrens-Fisher 问题。其解决方法如下:</p><p>设 $X_1,X_2,\cdots,X_{n_1}$ 来自总体 $N(\mu_1,\sigma_1^2)$ ,$Y_1,Y_2,\cdots,Y_{n_2}$ 来自总体 $N(\mu_2,\sigma_2^2)$ ,且 $X,Y$ 间相互独立。</p><p>$\overline{X}, \overline{Y}, S_1^2, S_2^2$ 分别表示样本 1、2 的均值,样本 1、2 的方差。易知:</p><p>$$ \overline{X}-\overline{Y} \sim N \left( \mu_1-\mu_2,\frac{\sigma_1^2}{n_1}+\frac{\sigma_2^2}{n_2} \right) $$</p><p>于是:</p><p>$$ \frac{\overline{X}-\overline{Y}-(\mu_1-\mu_2)}{\sqrt{\frac{\sigma_1^2}{n_1}+\frac{\sigma_2^2}{n_2}}} \sim N(0,1) $$</p><p>在零假设 $H_0:\mu_1=\mu_2$ 下</p><p>$$ \xi \triangleq \frac{\overline{X}-\overline{Y}}{\sqrt{\frac{\sigma_1^2}{n_1}+\frac{\sigma_2^2}{n_2}}} \sim N(0,1) $$</p><p>可见 $\left| \xi \right|$ 值太大时应拒绝 $H_0$ ,但由于 $\sigma_1^2, \sigma_2^2$ 是未知的,自然想到用 $S_1^2, S_2^2$ 分别代替,得到统计量:</p><p>$$ T = \frac{\overline{X}-\overline{Y}}{\sqrt{\frac{S_1^2}{n_1}+\frac{S_2^2}{n_2}}} $$</p><p>然而,$T$ 的精确分布依然相当复杂,且依赖于比值 $\frac{\sigma_1^2}{\sigma_2^2}$ 。幸运的是,数学上可以证明,统计量 $T$ 近似服从 $m$ 个自由度的 $t$ 分布,这个 $m$ 乃是与以下 $m^\ast$ 最接近的整数:</p><p>$$ m^\ast = \frac{\left( \frac{1}{n_1}S_1^2+\frac{1}{n_2}S_2^2 \right)^2}{\frac{1}{n_1-1}\left( \frac{S_1^2}{n_1} \right)^2 + \frac{1}{n_2-1}\left( \frac{S_2^2}{n_2} \right)^2} $$</p><p>利用 $t$ 分布表,找临界值 $\lambda$ 满足 $P(|T|>\lambda)=a$ ,于是当且仅当 $|T|>\lambda$ 时拒绝 $H_0: \mu_1=\mu_2$</p><h2 id="第七章-回归分析">第七章 回归分析</h2><p>回归分析是用来处理多个变量之间<strong>相关关系</strong>的一种数学方法。<strong>相关关系</strong>不同于<strong>函数关系</strong>,在相关关系中,多个变量之间明显相关,但并不具有完全确定性的关系,例如人的身高和体重,虽然凭借身高并不能精确确定体重,但总体来说有“身高者,体也重”的关系。</p><h3 id="一元线性回归">一元线性回归</h3><h4 id="经验公式与最小二乘法">经验公式与最小二乘法</h4><p>对于有一定关系的两个变量 $X,Y$ ,在观测中得到若干组数据 $(x_1,y_1),(x_2,y_2),\cdots,(x_n,y_n)$,我们怎样获取 $X,Y$ 之间的经验公式呢?</p><p><strong>step 1</strong>:作出<strong>散点图</strong>,大致确定经验公式的形式。若散点图大致为线性关系,那么我们可以得到如下经验公式:</p><p>$$ \hat{y} = a + bx $$</p><p>这里,在 $y$ 上方加“$\hat{}$”,是为了区别于 $Y$ 的实际值 $y$,因为 $y$ 代表着其与 $x$ 之间的函数关系,而观测值一般不具有严格的函数关系。</p><p><strong>step 2</strong>:求出参数 $a,b$</p><p>上述关系式:</p><p>$$ \hat{y} = a + bx $$</p><p>称为<strong>回归方程</strong>。我们的目的是要找到合适的参数 $a,b$ 使得<strong>回归方程所代表的直线总体最接近所有的散点</strong>。</p><p>我们如何来刻画一条直线与所有散点之间的总体接近程度呢?可以通过以下统计量:</p><p>$$ \sum_{i=1}^{n} \left[ y_i - (a + b x_i) \right]^2 $$</p><blockquote><p>该统计量的几何意义是点 $(x_i,y_i)$ 沿着 $y$ 轴的方向到直线的距离,而不是到直线的垂直距离!</p></blockquote><p>上述统计量随着 $a,b$ 的变化而变化,是关于 $a,b$ 的二元函数,记为 $Q(a,b)$:</p><p>$$ Q(a,b) = \sum_{i=1}^{n} \left[ y_i - (a + b x_i) \right]^2 $$</p><p>我们的目的是找到两个数 $\hat{a},\hat{b}$,使二元函数 $Q(a,b)$ 在 $a = \hat{a},b=\hat{b}$ 处达到最小</p><p>由于 $Q(a,b)$ 是 $n$ 个平方之和,所以使 $Q(a,b)$ 最小的原则称为<strong>平方和最小原则</strong>,习惯上称为<strong>最小二乘原则</strong>。$a,b$ 的值可以通过以下方程组求得:</p><p>$$ \left\{<br>\begin{aligned}<br>\frac{\partial Q}{\partial a} &= -2 \sum_{i=1}^{n} \left[ y_i - (a + b x_i) \right] = 0 \\<br>\frac{\partial Q}{\partial b} &= -2 \sum_{i=1}^{n} \left[ y_i - (a + b x_i) \right] \cdot x_i = 0<br>\end{aligned}<br>\right. $$</p><p>解得:</p><p>$$ \left\{<br>\begin{aligned}<br>b &= \frac{\sum\limits_{i=1}^{n}(x_i-\bar{x})(y_i-\bar{y})}{\sum\limits_{i=1}^{n}(x_i-\bar{x})^2} \\<br>a &= \bar{y} - b \bar{x}<br>\end{aligned}<br>\right. $$</p><h5 id="当相关关系不是线性关系时如何使用最小二乘法?">当相关关系不是线性关系时如何使用最小二乘法?</h5><p>采用适当的转化,构造原变量的生成变量,使得生成变量之间具有线性关系。</p><p>例如:变量 $X,Y$ 有如下相关关系:</p><p>$$ y = A e^{-B/x} $$</p><p>显然 $y$ 与 $x$ 之间的关系不是线性的。我们对等式两边取自然对数:</p><p>$$ \ln y = \ln A - \frac{B}{x} $$</p><p>令</p><p>$$ \begin{aligned}<br>y^\ast &= \ln y \\<br>x^\ast &= \frac{1}{x}<br>\end{aligned} $$</p><p>则两个新变量 $y^\ast,x^\ast$ 之间的关系便是线性的了,我们将 $x,y$ 的观测数值转化为这两种形式即可。</p>]]></content>
<categories>
<category> 笔记 </category>
</categories>
<tags>
<tag> 概率统计 </tag>
</tags>
</entry>
<entry>
<title>矩阵求导术</title>
<link href="/posts/20d9a268/"/>
<url>/posts/20d9a268/</url>
<content type="html"><![CDATA[<p>本文为知乎上的一篇文章 <a href="https://zhuanlan.zhihu.com/p/24709748">矩阵求导术</a> 的笔记。</p><span id="more"></span><p>符号约定:以下使用小写字母如 $x$ 表示标量,粗体小写字母如 $\boldsymbol{a}$ 表示向量,大写字母如 $A$ 表示矩阵。为保持一致性,向量如不进行特殊说明,均为<strong>列向量</strong>,行向量可通过列向量的转置来表示,如 $\boldsymbol{a}^T$。</p><p>矩阵对标量的求导即逐个元素对标量求导,没什么值得讨论的。我们以下主要介绍标量对矩阵的求导,然后延伸到矩阵对矩阵的求导。</p><h2 id="标量对矩阵的导数">标量对矩阵的导数</h2><h3 id="定义">定义</h3><p>首先明确一下标量对矩阵求导的定义:</p><p>$$ \frac{\partial f}{\partial X} = \left[\ddots, \frac{\partial f}{\partial X_{ij}}, \ddots \right] $$</p><p>即 $f$ 对 $X$ 逐元素求导,并排列成与 $X$ 形状相同的矩阵。</p><p>该定义在形式上很容易理解,但在实际计算中却难以使用,因为它破坏了矩阵的<strong>整体性</strong>。在工程实践中(比如 Matlab、numpy 等),我们倾向于把矩阵看作一个整体,从整体上对其进行的加减乘除等运算,像普通的标量一样。因此,我们应当考虑如何将求导作用于整个矩阵上,而不是作用于矩阵的各个元素上。</p><p>我们不妨从最简单的标量函数入手,构建起一套易于掌握的矩阵求导技巧。</p><p>导数与微分之间存在着密切的联系:</p><p>$$ \mathrm{d}y = f’(x)\mathrm{d}x = \frac{\mathrm{d}y}{\mathrm{d}x}\mathrm{d}x $$</p><p>即:全微分 $\mathrm{d}y$ 是导数 $\displaystyle \frac{\mathrm{d}y}{\mathrm{d}x}$ 与微分变量 $\mathrm{d}x$ 的积。<strong>(推论1)</strong></p><p>明确了上述重要定义之后,我们再来看看多元函数的情形。设 $f(x_1, x_2, x_3, \dots, x_n)$ 为多元函数,我们依然从全微分的定义入手:</p><p>$$ \mathrm{d}f = \sum_{i=1}^{n} \frac{\partial f}{\partial x_i}\mathrm{d}x_i $$</p><p>为了得到一个满足“整体性”的式子,我们需要去掉求和符号。令向量 $\boldsymbol{x}^T=[x_1, x_2, x_3, \dots, x_n]$,得到:</p><p>$$ \mathrm{d}f = \frac{\partial f}{\partial \boldsymbol{x}} \cdot \mathrm{d}\boldsymbol{x} $$</p><p>其中,$\displaystyle \frac{\partial f}{\partial \boldsymbol{x}}$ 代表 $f$ 对 $\boldsymbol{x}$ 的所有项的偏导数组成的向量。运算符“$\cdot$”代表<strong>点乘</strong>运算,其运算结果称为<strong>内积</strong>。</p><p>于是,我们得到了如下推论:</p><p>多元函数的全微分 $\mathrm{d}f$ 是导数向量 $\displaystyle \frac{\partial f}{\partial \boldsymbol{x}}$ 与微分变量 $\mathrm{d}\boldsymbol{x}$ 的内积。<strong>(推论2)</strong></p><p>与推论1进行比较可以发现,二者在形式上是完全相同的,唯一的差别在于末尾的“积”和“内积”。然而,标量的积完全可以看作向量内积的一种特殊情况,也就是说,推论2可以涵盖推论1。</p><p>现在,我们已经把全微分与导数之间的关系式推广到了<strong>关于向量的函数</strong>(即多元函数),如果把向量看作一种特殊的矩阵,那么这一推论也很容易推广到<strong>关于矩阵的函数</strong>:</p><p>$$ \mathrm{d}f = \frac{\partial f}{\partial X}\cdot \mathrm{d}X \tag{1} $$</p><p>即:关于矩阵的函数的全微分 $\mathrm{d}f$ 是导数矩阵 $\displaystyle \frac{\partial f}{\partial X}$ 与微分变量 $\mathrm{d}X$ 的内积。<strong>(推论3)</strong></p><blockquote><p>注:这里将内积的定义从向量推广到了矩阵,即先对做逐元素相乘,然后将所有乘积求和。</p></blockquote><p>由于标量和向量都可以看作是矩阵的特殊情况,因此推论3涵盖了推论 1、2。至此,我们得到了通用表达式 $(1)$。</p><h3 id="运算规则">运算规则</h3><p>有了定义,还需要一套完整的运算规则才能实际应用。</p><p>我们依然从最简单的标量函数中汲取灵感。例如,对于函数 $\displaystyle f(x)=x^2 sin(e^x)$,我如何求导呢?我们通常不是直接从导数的定义出发,而是先建立了初等函数的导数和四则运算、复合等法则,然后运用这些法则求导。因此,我们也需要建立矩阵微分的运算规则。</p><h4 id="矩阵的微分运算">矩阵的微分运算</h4><ol><li><p>加(减)法:</p><p>$$ \mathrm{d}(A\pm B)=\mathrm{d}A\pm\mathrm{d}B $$</p></li><li><p>乘法:</p><p>$$ \mathrm{d}(AB)=\mathrm{d}AB+A \mathrm{d}B $$</p></li><li><p>逆:</p><p>$$ \mathrm{d}A^{-1}=-A^{-1}\mathrm{d}A A^{-1} $$</p></li><li><p>转置:</p><p>$$ \mathrm{d}A^T=(\mathrm{d}A)^T $$</p></li><li><p>迹(trace):</p><p>$$ \mathrm{d}tr(A)=tr(\mathrm{d}A) $$</p></li><li><p>行列式:</p><p>$$ \mathrm{d}|A|=tr(A^*\mathrm{d}A) $$</p><p>其中 $A^*$ 表示 $A$ 的伴随矩阵。如果 $A$ 可逆,则又有:</p><p>$$ \mathrm{d}|A|=|A|tr(A^{-1}\mathrm{d}A) $$</p></li><li><p>逐元素(element-wise)乘法:</p><p>$$ \mathrm{d}(A\odot B)=\mathrm{d}A\odot B+A\odot \mathrm{d}B $$</p></li><li><p>逐元素函数:</p><p>$$ \mathrm{d}h(A)=h’(A)\odot \mathrm{d}A $$</p><p>其中,$h(X)$ 为对矩阵 $X$ 进行逐元素运算的标量函数。</p></li></ol><h4 id="矩阵的点乘运算">矩阵的点乘运算</h4><p>考虑到点乘运算是公式 $(1)$ 中的核心操作,因此我们还需要一些关于点乘的运算规则(注意:点乘要求参加运算的两个矩阵形状相同):</p><ol><li><p>定义式:</p><p>$$ A\cdot B=tr(A^TB) $$</p><p>对于向量来说,该式可以进一步简化:$\displaystyle \boldsymbol{a}\cdot \boldsymbol{b}=\boldsymbol{a}^T \boldsymbol{b}$</p></li><li><p>交换律:</p><p>$$ A\cdot B = B\cdot A $$</p></li><li><p>加法分配律:</p><p>$$ A\cdot(B+C)=A\cdot B + A\cdot C $$</p></li><li><p>与逐元素乘法的结合律:</p><p>$$ A\cdot(B\odot C)=(A\odot B)\cdot C $$</p></li><li><p>转置的交换:</p><p>$$ A^T\cdot B = A\cdot B^T $$</p></li></ol><h4 id="矩阵的迹运算">矩阵的迹运算</h4><p>以上公式中,多处涉及到矩阵的迹(trace)运算,这里提供一些迹技巧(注意:迹运算的对象必须为方阵):</p><ol><li><p>标量的迹等于其自身:</p><p>$$ tr(x) = x $$</p></li><li><p>线性:</p><p>$$ tr(A\pm B)=tr(A)\pm tr(B) $$</p></li><li><p>乘法可交换性:</p><p>$$ tr(AB) = tr(BA) $$</p><p>要求 $A$ 和 $B^T$ 的形状相同,这样才能保证 $AB$ 为方阵。</p></li><li><p>转置迹不变:</p><p>$$ tr(A^T)=tr(A) $$</p></li><li><p><strong>迹与点乘的转换</strong>:</p><p>$$ tr(AB) = A^T\cdot B $$</p></li></ol><h4 id="矩阵的其他相关运算">矩阵的其他相关运算</h4><p>最后,为了方便查阅,附上矩阵的转置、逆、行列式等相关运算规则:</p><p>$$ \begin{aligned}<br>(A^T)^T &= A \\<br>(AB)^T &= B^T A^T \\<br>(A^{-1})^{-1} &= A \\<br>AA^{-1} &= A^{-1}A = I \\<br>(AB)^{-1} &= B^{-1}A^{-1} \\<br>(A^T)^{-1} &= (A^{-1})^T \\<br>|A^{-1}| &= |A|^{-1} \\<br>(kA)^{-1} &= k^{-1}A^{-1}<br>\end{aligned}$$</p><p>有了上述这些运算规则,<strong>只要矩阵的函数 $f$ 是由矩阵 $X$ 经过加、减、乘、转置、逆、迹、行列式、逐元素乘法、逐元素函数等运算及其复合运算构成的,我们都能利用上述运算规则求得 $\displaystyle \frac{\partial f}{\partial X}$。其基本思路为:对 $f$ 的表达式求全微分,并设法将其转化为 $\mathrm{d}f = \mathrm{expr} \cdot \mathrm{d}X$ 的形式,那么 $expr$ 即为待求导数的表达式</strong>。</p><h3 id="算例演示">算例演示</h3><h4 id="例-1">例 1</h4><p>设 $\displaystyle y=\boldsymbol{a}^T X \boldsymbol{b}$,求 $\displaystyle \frac{\partial y}{\partial X}$。其中 $\boldsymbol{a}$ 为 $m\times 1$ 向量,$X$ 为 $m\times n$ 矩阵,$\boldsymbol{b}$ 为 $n\times 1$ 向量,$y$ 为标量。</p><p>解:</p><p>$$ \begin{aligned}<br>\mathrm{d}y &= \boldsymbol{a}^T \mathrm{d}X \boldsymbol{b} \\<br>&= tr(\boldsymbol{a}^T \mathrm{d}X \boldsymbol{b}) \\<br>&= tr(\boldsymbol{b} \boldsymbol{a}^T \mathrm{d}X) \\<br>&= tr((\boldsymbol{a} \boldsymbol{b}^T)^T \mathrm{d}X) \\<br>&= \boldsymbol{a} \boldsymbol{b}^T \cdot \mathrm{d}X<br>\end{aligned} $$</p><blockquote><p>说明:</p><ul><li>第0步:对 $y$ 求全微分;</li><li>第1步:标量套上迹;</li><li>第2步:迹运算内交换 $\boldsymbol{a}^T \mathrm{d}X$ 与 $\boldsymbol{b}$;</li><li>第3步:矩阵乘法的转置;</li><li>第4步:转化为点乘形式。</li></ul></blockquote><p>根据公式 $(1)$ 得到:</p><p>$$ \frac{\partial y}{\partial X}=\boldsymbol{a} \boldsymbol{b}^T $$</p><h4 id="例-2:线性回归">例 2:线性回归</h4><p>线性回归的损失函数定义为 $\displaystyle l=||X \boldsymbol{w}- \boldsymbol{y}||_2^2$,求 $\boldsymbol{w}$ 的最小二乘估计,即 $\boldsymbol{w}$ 为何值时 $l$ 可取得最小值。其中 $\boldsymbol{y}$ 为 $m\times 1$ 列向量,$X$ 为 $m\times n$ 矩阵,$\boldsymbol{w}$ 为 $n\times 1$ 向量,$l$ 为标量。</p><p>解:要求极小值,只需找到 $\displaystyle \frac{\partial l}{\partial \boldsymbol{w}}$ 的零点。</p><p>我们的运算规则中并未定义二阶范数的微分,但根据向量范数的定义,我们可以将它表示成内积的形式:</p><p>$$ l=(X \boldsymbol{w}- \boldsymbol{y})\cdot(X \boldsymbol{w}- \boldsymbol{y})=(X \boldsymbol{w}- \boldsymbol{y})^T(X \boldsymbol{w}- \boldsymbol{y}) $$</p><p>接下来,求 $l$ 对 $\boldsymbol{w}$ 的微分:</p><p>$$ \begin{aligned}<br>\mathrm{d}l &= \mathrm{d}(X \boldsymbol{w}- \boldsymbol{y})^T(X \boldsymbol{w}- \boldsymbol{y}) + (X \boldsymbol{w}- \boldsymbol{y})^T \mathrm{d}(X \boldsymbol{w}- \boldsymbol{y}) \\<br>&= (X \mathrm{d}\boldsymbol{w})^T(X \boldsymbol{w}- \boldsymbol{y})+(X \boldsymbol{w}- \boldsymbol{y})^T(X \mathrm{d}\boldsymbol{w}) \\<br>&= 2(X \boldsymbol{w}- \boldsymbol{y})^T X \mathrm{d}\boldsymbol{w} \\<br>&= 2X^T(X \boldsymbol{w}- \boldsymbol{y})\cdot \mathrm{d}\boldsymbol{w}<br>\end{aligned} $$</p><blockquote><p>说明:</p><ul><li>第2步:加号前后两项均为标量,所以对第一项加上转置,结果不变。</li><li>第3步:标量套上迹运算,然后转化为点积。</li></ul></blockquote><p>根据公式 $(1)$ 可得:</p><p>$$ \frac{\partial l}{\partial \boldsymbol{w}}=2X^T(X \boldsymbol{w}- \boldsymbol{y}) $$</p><p>令 $\displaystyle \frac{\partial l}{\partial \boldsymbol{w}}=\boldsymbol{0}$ 得(加粗的 $\boldsymbol{0}$ 代表零向量,其形状与 $\boldsymbol{w}$ 相同):</p><p>$$ \boldsymbol{w}=(X^T X)^{-1}X^T \boldsymbol{y} $$</p><h4 id="例-3:多元逻辑回归">例 3:多元逻辑回归</h4><p>多元逻辑回归的损失函数为 $\displaystyle l=-\boldsymbol{y}^T \log (\mathrm{softmax}(W \boldsymbol{x}))$,求 $\displaystyle \frac{\partial l}{\partial W}$。其中,$\boldsymbol{y}$ 为 $m\times 1$ 的 one-hot 向量(即除一个元素为 1 外,其他元素均为 0),$W$ 为 $m\times n$ 矩阵,$\boldsymbol{x}$ 为 $n\times 1$ 向量,$l$ 为标量。$\displaystyle \mathrm{softmax}(a)=\frac{\mathrm{exp}(\boldsymbol{a})}{\boldsymbol{1}^T\mathrm{exp}(\boldsymbol{a})}$,其中 $\displaystyle \mathrm{exp}(\boldsymbol{\cdot})$ 表示逐元素求指数,$\boldsymbol{1}$ 代表全 1 向量。</p><p>解:首先将 $\mathrm{softmax}$ 的表达式代入:</p><p>$$ l=-\boldsymbol{y}^T(\log(\mathrm{exp}(W\boldsymbol{x}))- \boldsymbol{1}\log(\boldsymbol{1}^T\mathrm{exp}(W \boldsymbol{x})))=-\boldsymbol{y}^T W \boldsymbol{x}+\log(\boldsymbol{1}^T\mathrm{exp}(W \boldsymbol{x})) $$</p><p>这里用到了 2 个等式:</p><p>$$ \log(\frac{\boldsymbol{v}}{c})=\log(\boldsymbol{v})- \boldsymbol{1}\log© $$ $$ \boldsymbol{y}^T \boldsymbol{1}=1 $$</p><p>接下来求 $l$ 对 $W$ 的全微分:</p><p>$$ \begin{aligned}<br>\mathrm{d}l &= -\boldsymbol{y}^T\mathrm{d}W \boldsymbol{x}+\frac{\boldsymbol{1}^T(\mathrm{exp}(W \boldsymbol{x})\odot(\mathrm{d}W \boldsymbol{x}))}{\boldsymbol{1}^T\mathrm{exp}(W \boldsymbol{x})}<br>\end{aligned} $$</p><blockquote><p>说明:注意逐元素函数 $\mathrm{exp}(\cdot)$ 的微分变换</p></blockquote><p>由于</p><p>$$ \begin{aligned}<br>\boldsymbol{1}^T(\mathrm{exp}(W \boldsymbol{x})\odot(\mathrm{d}W \boldsymbol{x})) &= \boldsymbol{1}\cdot(\mathrm{exp}(W \boldsymbol{x})\odot(\mathrm{d}W \boldsymbol{x})) \\<br>&= (\boldsymbol{1}\odot\mathrm{exp}(W \boldsymbol{x}))\cdot \mathrm{d}W \boldsymbol{x} \\<br>&= \mathrm{exp}(W \boldsymbol{x})\cdot \mathrm{d}W \boldsymbol{x} \\<br>&= \mathrm{exp}(W \boldsymbol{x})^T \mathrm{d}W \boldsymbol{x}<br>\end{aligned} $$</p><blockquote><p>说明:利用点乘与逐元素乘法的结合律</p></blockquote><p>故</p><p>$$ \begin{aligned}<br>\mathrm{d}l &= -\boldsymbol{y}^T\mathrm{d}W \boldsymbol{x}+\frac{\mathrm{exp}(W \boldsymbol{x})^T \mathrm{d}W \boldsymbol{x}}{\boldsymbol{1}^T\mathrm{exp}(W \boldsymbol{x})} \\<br>&= (-\boldsymbol{y}^T+\mathrm{softmax}(W \boldsymbol{x})^T)\mathrm{d}W \boldsymbol{x} \\<br>&= tr((\mathrm{softmax}(W \boldsymbol{x})-\boldsymbol{y})^T \mathrm{d}W\boldsymbol{x}) \\<br>&= tr(\boldsymbol{x}(\mathrm{softmax}(W \boldsymbol{x})-\boldsymbol{y})^T \mathrm{d}W) \\<br>&= (\mathrm{softmax}(W \boldsymbol{x})-\boldsymbol{y})\boldsymbol{x}^T \cdot \mathrm{d}W<br>\end{aligned} $$</p><p>所以:</p><p>$$ \frac{\partial l}{\partial W}=(\mathrm{softmax}(W \boldsymbol{x})-\boldsymbol{y})\boldsymbol{x}^T $$</p><h2 id="矩阵对矩阵的导数">矩阵对矩阵的导数</h2><p>未完待续。</p>]]></content>
<categories>
<category> 笔记 </category>
</categories>
<tags>
<tag> 线性代数 </tag>
</tags>
</entry>
<entry>
<title>远程工作流</title>
<link href="/posts/8247595/"/>
<url>/posts/8247595/</url>
<content type="html"><![CDATA[<p>远程工作的需求无非两个:</p><ol><li>远程执行命令。</li><li>在远程与本地之间进行文件的双向传输。</li></ol><p>所有的远程工作流都需要考虑如何提高这两个操作的效率。</p><span id="more"></span><h2 id="1-一般远程工作流">1. 一般远程工作流</h2><p>对于一般的远程机器,我们可以通过 ssh 进行连接和操作,并使用 <code>scp</code> 命令或 SFTP 客户端实现双向文件交换:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 登录开发机</span></span><br><span class="line">ssh dest_host</span><br><span class="line"></span><br><span class="line"><span class="comment"># 上传文件</span></span><br><span class="line">scp path/to/local_file dest_host:path/to/remote_file</span><br><span class="line"></span><br><span class="line"><span class="comment"># 下载文件</span></span><br><span class="line">scp dest_host:path/to/remote_file path/to/local_file</span><br></pre></td></tr></table></figure><p>有很多可视化的 SFTP 工具能够代替 <code>scp</code> 命令完成文件双向传输,非常方便,例如 WinSCP、Xmanager 等。</p><p>主流的文本编辑器也都提供了对 SFTP 的插件支持,例如:</p><ul><li>Sublime Text: <a href="https://wbond.net/sublime_packages/sftp">SFTP</a>。</li><li>VSCode: <a href="https://marketplace.visualstudio.com/items?itemName=liximomo.sftp">sftp</a>。</li></ul><p>可以很方便地上传、下载、删除、编辑远程文件。</p><h2 id="2-中继机下的远程工作流">2. 中继机下的远程工作流</h2><p>考虑到安全原因,公司常常会对远程开发机的入口做限制,想要连接开发机必须以一个中继机器作为中介。这时候我们需要两次 ssh 连接:首先连接到中继机,然后从中继机连接到开发机。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 登录到中继机</span></span><br><span class="line">ssh relay_host</span><br><span class="line"></span><br><span class="line"><span class="comment"># 现在我们已经在中继机上…</span></span><br><span class="line"><span class="comment"># 登录开发机</span></span><br><span class="line">ssh dest_host</span><br></pre></td></tr></table></figure><p>中继机的存在除了造成登录更复杂之外,也使得文件传输更难实现,因为绝大多数 SFTP 工具都没有中继功能,在这种情况下无法使用。</p><p>按照传统的方案,我们需要两次 <code>scp</code> 命令来完成一次文件上传操作:首先从本地上传到中继机,然后从中继机上传到开发机。但是这种方式显然太复杂了,而且大部分中继机对安全的要求非常苛刻,只允许使用 <code>ssh</code> 命令,导致 <code>scp</code> 方式失效。</p><p>此时我们有以下三种方案来实现文件传输功能:</p><h3 id="2-1-SSH-隧道">2.1 SSH 隧道</h3><p>为了避免重复性的中继命令,我们可以借助 <code>ssh</code> 提供的“隧道”功能。该功能可以透过任意数量的中继机,在本地机器与目标机器之间建立一条“隧道”。因此,文件的上传、下载依然可以通过本地机器上的一条命令完成。</p><p>同时,SSH 隧道有一个巨大的好处:所有的 SFTP 工具依然可用,因为隧道对外部是透明的。</p><blockquote><p>关于隧道功能,可以查看 <code>ssh</code> 帮助手册中的 <code>-ProxyJump</code>、<code>-ProxyCommand</code>、<code>-R</code>、<code>-L</code> 等参数的用法。</p></blockquote><p>不过使用该方案有两个前提:</p><ol><li>ssh 版本必须足够新,以支持隧道功能(OpenSSH 7.3 +)。</li><li>中继机必须允许 TCP 转发。</li></ol><p>但不幸的是,很多中继机对安全的要求非常苛刻,不允许 TCP 转发。在这种情况下,该方案无法工作。</p><h3 id="2-2-szrz-工具">2.2 szrz 工具</h3><p>szrz 是一个轻量、便捷的解决方案,其内部采用的 ZModem 协议非常底层,对中继机是透明的。因此 szrz 是中继机环境下文件传输问题的天然解决方案。</p><p>szrz 容易使用,配置完成后,在<strong>开发机</strong>的控制台中使用 <code>sz</code> 命令下载文件,<code>rz</code> 命令上传文件,同样可以透过任意数量的中继机。</p><p>但 szrz 有两个严重弊端:</p><ol><li>不支持大文件传输(超过 30MB 的文件就会把控制台卡死)。</li><li>丧失了 SSH 协议的一切好处,比如安全性、丰富的 SFTP 工具等。</li></ol><h3 id="2-3-不使用-SSH,而是借助-FTP、HTTP-等其他协议">2.3 不使用 SSH,而是借助 FTP、HTTP 等其他协议</h3><p>既然 SSH 协议被中继机限制了,那我们不如另立门户,借助 FTP、HTTP 等其他协议来实现文件传输。考虑到 FTP 天然适合静态文件服务,比 HTTP 高效很多,所以我们一般选择搭建 FTP 服务。</p><p>FTP、HTTP 服务器是非常成熟、稳定的技术,基本上随意挑选一款工具都可以满足我们的需求。我们以 <a href="https://github.com/giampaolo/pyftpdlib">pyftpdlib</a> 为例:</p><ul><li>在开发机上安装 FTP 工具:</li></ul><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pip install pyftpdlib</span><br></pre></td></tr></table></figure><ul><li>启动 FTP 服务</li></ul><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">python -m pyftpdlib <span class="comment"># and many other options ...</span></span><br></pre></td></tr></table></figure><p>我们实际在用的启动命令:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 考虑安全因素,可设置用户名和密码</span></span><br><span class="line">python -m pyftpdlib --directory /home/work/ --port 8888 -r 8000-9000 --user username --password ****** --write</span><br></pre></td></tr></table></figure><p>FTP 服务非常稳定,极少出错,因此你可以把上述命令放到后台执行,并且丢弃其日志:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">nohup python -m pyftpdlib ...blahblah... >/dev/null 2>&1 &</span><br></pre></td></tr></table></figure><p>开发机配置完成之后,我们就可以在本地进行文件上传、下载操作了:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 下载文件</span></span><br><span class="line">curl ftp://dest_host:8888/tmp/test.tar.gz -o test.tar.gz -u username:******</span><br><span class="line"></span><br><span class="line"><span class="comment"># 上传文件</span></span><br><span class="line">curl -T test.tar.gz ftp://dest_host:8888/tmp/test.tar.gz -u username:******</span><br></pre></td></tr></table></figure><p>FTP 解决方案的优势如下:</p><ol><li>支持大文件,并且传输速度是三种方案中最快的。</li><li>天然支持可视化,可直接把 ftp 地址输入浏览器查看、下载文件。</li><li>安全性:比 SSH 稍弱,但考虑到开发机仅公司内网可见,因此并不需要过度担忧。</li><li>FTP 与 SFTP 一样,有着丰富的第三方工具可用。</li></ol><h2 id="3-终极远程工作流:实现远程实时编辑">3. 终极远程工作流:实现远程实时编辑</h2><p>文件的上传下载其实是个低频需求,远程实时编辑开发机上的文件才是真正的痛点,这样就实现了远程工作空间(remote workspace)。在这个需求上,SSH 隧道和 FTP 解决方案更能充分提现其优越性。</p><p>目前主流的文本编辑器都支持 SFTP、FTP 等插件,比如:</p><ul><li>Sublime Text: <a href="https://wbond.net/sublime_packages/sftp">SFTP</a>。</li><li>VSCode: <a href="https://marketplace.visualstudio.com/items?itemName=liximomo.sftp">sftp</a>、<a href="https://marketplace.visualstudio.com/items?itemName=lukasz-wronski.ftp-sync">ftp-sync</a>。</li></ul><p>这些插件可以在本地目录和远程 SFTP/FTP 目录之间建立映射,从而实现远程实时编辑开发机上的代码(以下图片引自 Sublime Text SFTP):</p><p><img src="https://raw.githubusercontent.com/hzhu212/image-store/master/blog/20190921224609.png" alt="Mapping a Folder to a Remote"></p><p>当然,你也可以不做实时映射,而是使用手动方式同步本地和远程的代码:</p><p><img src="https://raw.githubusercontent.com/hzhu212/image-store/master/blog/20190921224708.png" alt="Editor Menu"></p><p>在这种工作流下,编辑远程机器上的代码与编辑本地代码没有任何差别。</p><p>相比如下两种工作流:</p><ul><li>本地编辑代码->上传到开发机->发现问题->重复以上三步(或使用 vim 做简单修改)</li><li>登录开发机->使用 vim 编辑代码(这种方式的弊端是不够工程化,大代码量难以管控)</li></ul><p>remote workspace 工作流有着可视化编辑器的加成,在实践中可以大大提高开发效率。</p>]]></content>
<categories>
<category> 技术 </category>
</categories>
<tags>
<tag> 远程 </tag>
<tag> ssh </tag>
<tag> szrz </tag>
</tags>
</entry>
<entry>
<title>了不起的盖茨比</title>
<link href="/posts/3941b47a/"/>
<url>/posts/3941b47a/</url>
<content type="html"><![CDATA[<p><img src="https://raw.githubusercontent.com/hzhu212/image-store/master/blog/20190614162338.png" alt="He seemed to be reaching toward something out there in the dark"></p><blockquote><p>He had come such a long way, and his dream must have seemed so close, that he could hardly fail to grasp it. But he did not know that it was already behind him.</p></blockquote><span id="more"></span><p>1922 年的纽约,经济野蛮式增长,股票一路飘红,一派繁荣的景象。各路金融大亨齐聚华尔街,摩天大楼里坐满了各处涌来的野心家。奢华的派对、气派的演出比比皆是,物质和欲望包围着醉生梦死的人们。历史总是惊人地相似,经济的繁荣似乎总会带来社会的疯狂和人性的冷漠。物质的世界里纸醉金迷,精神的世界里却乌烟瘴气,这就是盖茨比所生活的时代。</p><p>跟随着时间轴,电影的调子从开头的满怀期待,逐渐过渡到甜蜜温暖,继而走向困扰、纠结,然后是失望、心碎,最终的结局只剩下冰冷与残酷。盖茨比所日夜追寻的心爱之人黛西,也从开始的梦幻、不平凡逐渐走向现实、走向世俗,直至最终融入这个冰冷的世界。盖茨比一次又一次地用自己的真情与乐观面对世界的冰冷与丑恶,得到的却只有一次又一次的失望。整部电影感人至深而又发人深省,哀叹之余,也让人对现实、人性乃至宿命产生思考。</p><p>一个人愿意花数年时间奋斗、成长,从一无所有的贫寒士兵成为人尽皆知的大亨,只为将来可以更好地爱自己心中的姑娘。当结局事与愿违时,他愿意花五年时间等待,在靠近她的地方安顿下来,追随着那一小束承载着他全部希望的微弱的绿光。他精心设计属于她房子,精心挑选属于她的衣服,精心保留所有关于她的记忆,散尽千金举行无数场豪华派对,只期待有一天她的身影能够出现。在她面前,他会由一个自信成功的男子瞬间变成一个手足无措的少年,会不顾自己的身家前途,不惜一切代价,只为唤醒她心中的热情。这就是盖茨比,为了纯真的感情他一次次地改变自己的人生轨迹,只为追求一份幻想中的完美爱情。</p><p>然而现实是冰冷的,人性是冷漠的,盖茨比幻想中的美好姑娘并不如他想象的那般勇敢、纯真和执着。当梦幻与现实两条轨迹交汇时,有些东西势必不能两全。盖茨比渴求完美的爱情,但黛西却无法抛却旧的生活,局面开始变得纠结、无奈。正如一切美好的事物遇上现实都会变得索然无味一样,此时的黛西在“我”的心中已经由天仙变成了凡人。虽然盖茨比仍然满怀希望地等待,等待曾经的那段美好时光重新上演,但悲剧的味道已经越来越浓。当一颗子弹穿过盖茨比的胸膛时,一切温暖和美好的想象瞬间全部熄灭,但这还远不是悲剧的顶点。昔日宾朋无数的盖茨比逝去了,但他的葬礼上却寂寥无人,黛西的悄然离去仿佛给“我”的胸中浇上了一盆冰水。曾经的豪华派对与如今的冷清葬礼形成了强烈的对比,这种对比带给“我”的除了反感、恶心,还有失望、心碎。从此,纽约——这个曾经承载了“我”的野心和梦想的地方——再也没有了任何温度。</p><p>在某种程度上,盖茨比就像来自 B612 星球的小王子,无意降临到这个肮脏冷漠的世界上。他拥有纯洁的灵魂、美好的希望,但他在这个污浊的世界上追寻自己的梦想,结局注定是悲伤的。盖茨比的伟大之处,既不是因为他的财富,也不是因为他的才华,而在于他的心灵之纯洁、爱情之热烈,以及对美好纯真的执着追求。在浮华的世界中保持一颗纯洁的心灵,倾尽一生献出自己所有的爱,这是许多人终其一生都难以一遇的伟大品质。只是,盖茨比将纯洁的心灵献给了冰冷的世界,将伟大的爱情献给了不理解他的人。盖茨比的死令人惋惜,却又令人欣慰,因为这个世界不值得他留下,离开是最好的结局。盖茨比的死是一种回归,就像小王子最终回到了自己的星球。</p><p>盖茨比代表了极少数没有在物质和金钱中迷失自己的人,他们在追求物质财富的过程中,没有像大多数人一样变得自私、冷漠、世故,而是始终保持着对于美好纯真事物的向往。盖茨比用尽后半生去追忆曾经的那段美好时光,心中既遗憾挣扎又满怀希望,直至死去却也只得到了一片幻想。比起盖茨比的伟大,我们当然都是平凡人,但我们同样逃脱不了和他一样的宿命——追求而不可得的宿命。但是我们应该因此而放弃追求吗?不!我们应该永远向往那盏绿灯,正如影片最后一段话所说:我们奋力前行,逆流勇进,直到回到美好纯真的往昔岁月,找回曾经稍纵即逝的美好时光。</p><blockquote><p>Gatsby believed in the green light, the orgastic future that year by year recedes before us. It eluded us then, but that’s no matter. Tomorrow, we will run faster, stretch out our arms farther. And one fine morning… So we beat on, boats against the current, borne back, ceaselessly, into the past.</p></blockquote><p>希望我们永远不要迷失在物质和现实中,与@白袍少年共勉。</p>]]></content>
<categories>
<category> 生活 </category>
<category> 电影 </category>
</categories>
<tags>
<tag> 情感 </tag>
</tags>
</entry>
<entry>
<title>编码与乱码之追根溯源</title>
<link href="/posts/b2d70b72/"/>
<url>/posts/b2d70b72/</url>
<content type="html"><![CDATA[<p>乱码问题是不但是新手程序员之痛,也常常让许多资深 coder 束手无策。本文探讨编码的概念、乱码的原理,以及乱码问题的分析与解决。</p><span id="more"></span><blockquote><p>注:本文的所有截图和实验均以 Sublime Text 为例,其他编辑器或 IDE 在原理上类似。</p></blockquote><h2 id="一、什么是编码?">一、什么是编码?</h2><p>什么是编码?这要从「文件」的概念说起。根据呈现形式,文件可分为两种类型:「文本文件」和「二进制文件」。</p><p>二者的区别非常明显,文本文件中保存的是各种字符,包括英文字母如 <code>abc</code>、汉字如 <code>你好</code>、日文如 <code>こんにちは</code> 等;而二进制文件中保存的则是 <code>0101</code> 等二进制数值。如果你用 Sublime Text 分别打开文本文件和二进制文件,那么它们呈现的样子大致如下:</p><p><img src="https://upload-images.jianshu.io/upload_images/3310969-265e9519743cafd6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="文本文件与二进制文件"></p><blockquote><p>注:我们习惯采用十六进制的方式简化二进制数据的显示,这样对人类用户稍微友好一些,避免了过长的 <code>0-1</code> 串使得人们眼花缭乱。</p></blockquote><p>为什么会产生这两种类型的文件呢?一个非常直接的原因是,文本文件主要是给人类用户看的,例如我们常使用的 txt、markdown 文件,各种代码文件如 <code>.cpp</code>、<code>.java</code>、<code>.py</code>、<code>.js</code> 等,以及各种配置文件如 <code>.ini</code>、<code>.json</code> 等;而二进制文件则是给操作系统或应用程序看的,如 <code>.exe</code> 交给 Windows 系统执行、Word 文档交给 Office Word 软件打开、<code>.class</code> 文件交给 java 虚拟机执行,许多应用程序都会设计自己专用的二进制文件格式。</p><p>尽管我们把文件分为文本文件和二进制文件两种类型,但从计算机硬件层面上来看,它只能存储 <code>0101</code> 这样的二进制数据,不可能直接存储 <code>abc</code> 这样的字符。那么该如何解释文本文件的存在呢?</p><p>事实上,从存储方式上来看,文件确实只有一种类型,那就是二进制文件。至于文本文件,它只是二进制文件的一种特殊情况。在计算机最初发明的时候,确实只有二进制文件,那时的人们通过「打孔的纸带」作为存储程序的载体,而纸带上小孔的有无就代表二进制的 1 和 0。那时候的计算机根本没有字符的概念,更不要说文本文件。</p><p>后来,人们为了方便就制定了一套规则,规定二进制数值 <code>01100001</code> 代表字符 <code>a</code>、<code>01100010</code> 代表字符 <code>b</code>、……、<code>01111010</code> 代表字符 <code>z</code>。于是,最早的编码「ASCII 编码」就产生了。现在,如果我在一个文件中写入二进制数据 <code>011000010110001001100011</code>,从表面上看,它就是一个常规的二进制文件,没有任何特殊之处,但如果我用 ASCII 编码的规则去解释它,就会看到一串字符 <code>abc</code>。这时候,我们就可以认为这个文件是文本文件。</p><p>从上面的描述中,你应该已经发现:</p><ul><li><strong>所谓的「编码」就是一种规则,它规定了二进制数值与字符之间的映射关系</strong>;</li><li><strong>所谓的「文本文件」就是一种二进制文件,只不过能用某种编码解释得通</strong>。</li></ul><p>说回到 ASCII 编码,它使用 8 个二进制位——也就是 1 个字节来映射一个字符,这意味着它最多只能映射 <code>2^8=256</code> 个字符。256 个字符对于纯英文来说已经足够了,但世界上的语言太多了,要囊括英文、德文、法文、中文、日文、韩文、阿拉伯文、希伯来文等所有语言文字,至少需要十几万的字符量。随着各种文字不断被引入计算机,字符编码的长度也不断扩张,从 1 个字节逐渐增加到 2 个、3 个、4 个字节。同时,各个组织、各个国家都在制定自己的编码体系,形成了错综复杂的编码“方言”。最终,到了 1994 年,人们终于制定出了一套统一的、无所不包的编码——Unicode 编码,成为编码界的“世界语”,因此也被称为万国码。</p><p>Unicode 编码使用 4 个字节来保存字符映射关系,因此共支持 <code>2^(4*8)=4294967296</code> 个字符,远远超出了地球上所有文字的总量。这彻底解决了字符数量不够用的担忧,但也带来了存储空间的浪费:即使仅仅保存一个简单的英文字母 <code>a</code>,Unicode 编码也需要 4 个字节,但事实上只需要 1 个字节(ASCII 编码)。如果一个文本文件中绝大部分字符都是英文字母,那么 Unicode 就浪费了 75% 的存储空间。鉴于上述问题,人们又制定了一系列“改良版”的 Unicode 编码,包括 UTF-8、UTF-16、UTF-32 等,它们同样能够编码所有已知的字符,但占用更少的空间。</p><p>以 UTF-8 为例,对于常见的英文字符,它采用 1 个字节编码,常见的中文、日文等字符采用 2 个字节,不常见的中文字符等采用 3 到 4 个字节,对于极不常见的字符,它会采用 6 个字节进行编码。因此,在通常情况下,UTF-8 编码要比 Unicode 编码节省超过一半的空间。UTF-8 编码无所不包、节省空间,且具有良好的跨平台性,因此推荐一切文本文件都使用 UTF-8 编码。目前,主流的文本编辑器都把 UTF-8 作为默认编码方式。</p><p>最后解释一下所谓的「ANSI 编码」。ANSI 编码常被称为标准编码,但它并不是指某种明确的编码方式。为了更容易地理解 ANSI 编码,我们不妨把它与「官方语言」的概念做类比。正如中国的官方语言是汉语,日本的官方语言是日语一样,中文 Windows 系统的 ANSI 编码为 GBK 编码,而日文 Windows 系统的 ANSI 编码为 Shift_JIS 编码。正如「官方语言」不是某种语言,「ANSI 编码」也不是某种编码,它是另一个维度的概念,与国家和地区有关,不同国家和地区的 ANSI 编码是不兼容的。可想而知,如果都采用 ANSI 编码,那么不同国家的开发者在互相交换代码时将非常糟糕。因此,不推荐以 ANSI 作为 coding 编码。</p><h2 id="二、什么是乱码?">二、什么是乱码?</h2><p>什么是乱码?用某种编码方式去解读一个文件,得到了无意义的字符,这就是乱码。打个通俗的比方:我写了一段英文,你非要把它当作拼音来读,那么得到的解释就是无意义的,就相当于乱码;反过来,我写了一段拼音,你非要用英语的语法去解释它,也是解释不通的。</p><p>举几个实际的例子:</p><ul><li>用 UTF-8 编码打开一个二进制文件会出现乱码:</li></ul><p><img src="https://upload-images.jianshu.io/upload_images/3310969-6a9b408e15b2a0c7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="用 UTF-8 编码打开一个二进制文件"></p><ul><li>用 UTF-8 编码打开一个 GBK 编码的文本文件会出现乱码:</li></ul><p><img src="https://upload-images.jianshu.io/upload_images/3310969-fe3372bf0f3ae8b8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="用 UTF-8 编码打开一个 GBK 编码的文本文件"></p><ul><li>用 UTF-8 编码打开一个 UTF-8 编码的文本文件<strong>不会</strong>乱码:</li></ul><p><img src="https://upload-images.jianshu.io/upload_images/3310969-cd751f681341f26c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="用 UTF-8 编码打开一个 UTF-8 编码的文本文件"></p><p>综上,乱码的根源就是<strong>编码与解码用的不是同一套规则</strong>。 但不管文件是否乱码,它里面保存的二进制数据总是不变的。<strong>通常情况下,乱码并不是文件本身有问题,而是打开方式(解码方式)不正确</strong>。</p><h2 id="三、编程中出现乱码的原因与类型">三、编程中出现乱码的原因与类型</h2><p>我们在日常使用文本编辑器、IDE、命令行等编写和执行程序的过程中,常常会遇到乱码现象,而出现乱码的原因是多种多样的。这里试图从根源上理解乱码,并将其归类。</p><p>一般,我们编写和执行程序的流程如下:</p><ol><li>编写代码并保存;</li><li>调用编译器编译代码,并执行程序;</li><li>查看输出结果。</li></ol><p>在这短短的三步操作中,隐含着两次编码和解码过程,也就是下图中的过程 1 和过程 2:</p><p><img src="https://upload-images.jianshu.io/upload_images/3310969-1c2dadb42f018c37.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="代码编写和执行过程中的编码和解码"></p><p>在过程 1 和过程 2 中,任意一个过程两端的编码方式都必须一致,否则就会出现乱码。其中,对于「代码文件的编码」以及「展示器的编码」,我们可以在编辑器和控制台中进行设置。最不可控的是编译器的输入编码和输出编码,常见编译器/解释器的默认输入输出编码如下表所示:</p><table><thead><tr><th>编译器/解释器</th><th>默认输入编码</th><th>默认输出编码</th><th>设置输入编码</th><th>设置输出编码</th></tr></thead><tbody><tr><td>python</td><td>UTF-8</td><td>ANSI</td><td><code># coding=xxx</code></td><td>环境变量 <code>PYTHONIOENCODING</code></td></tr><tr><td>gcc/g++</td><td>UTF-8</td><td>UTF-8</td><td>未知</td><td>未知</td></tr><tr><td>javac</td><td>ANSI</td><td>ANSI</td><td>加 <code>-encoding</code> 参数</td><td>未知</td></tr><tr><td>matlab</td><td>ANSI</td><td>ANSI</td><td>修改配置文件</td><td>未知</td></tr></tbody></table><blockquote><p>注:该结果是笔者在自己的 Windows 10 家庭中文版上测试得到的,不同的平台可能有差异。</p></blockquote><hr><p>接下来,我们将以 Sublime Text 执行一段 Python 脚本为例来展示这 2 种乱码,通过设置编译器输入编码、输出编码、展示器编码来探究乱码产生的不同原因。</p><p>这段 Python 脚本非常简单,只有一句话:<code>print('你好')</code>,以 UTF-8 编码保存。正常执行的结果如下:</p><p><img src="https://upload-images.jianshu.io/upload_images/3310969-dfceb9f34cbf044c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="正常无乱码"></p><p>从上上图中不难看出,过程 1 和过程 2 均能导致乱码,其组合可形成如下三种乱码类型:</p><h3 id="过程-1-乱码">过程 1 乱码</h3><p>我们在 Python 脚本头部添加一行 <code># -*- coding: gbk -*-</code>,即把 Python 解释器的输入编码指定为 GBK,但脚本的编码保持 UTF-8 不变。执行结果将发生乱码,如下:</p><p><img src="https://upload-images.jianshu.io/upload_images/3310969-7933afa816af4af4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="乱码类型 1"></p><p>从这里我们也可以看出,Python 解释器的默认输入编码为 UTF-8。</p><h3 id="过程-2-乱码。">过程 2 乱码。</h3><p>这里又分为两种情况,一是编译器的输出编码错误;二是展示器的输入编码错误:</p><h4 id="编译器输出编码不当。">编译器输出编码不当。</h4><p>打开 <code>Python.sublime-build</code> 文件(可借助 PackageResourceViewer 插件),其初始内容如下:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">{</span><br><span class="line"> <span class="attr">"shell_cmd"</span>: <span class="string">"python -u \"$file\""</span>,</span><br><span class="line"> <span class="attr">"file_regex"</span>: <span class="string">"^[ ]*File \"(...*?)\", line ([0-9]*)"</span>,</span><br><span class="line"> <span class="attr">"selector"</span>: <span class="string">"source.python"</span>,</span><br><span class="line"> <span class="attr">"env"</span>: {<span class="attr">"PYTHONIOENCODING"</span>: <span class="string">"utf-8"</span>},</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>我们把末尾的行改为 <code>"env": {"PYTHONIOENCODING": "gbk"},</code>,即把 Python 解释器的输出编码设为 UTF-8。执行脚本,再次得到乱码,如下:</p><p><img src="https://upload-images.jianshu.io/upload_images/3310969-5376b7daa408a89d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="乱码类型 2-1"></p><p>注意:这里虽然也是乱码,但与类型 1 不同。</p><h4 id="展示器输入编码不当。">展示器输入编码不当。</h4><p>我们首先撤销对 <code>Python.sublime-build</code> 的所有更改,然后在其末尾增加一行内容 <code>"encoding": "gbk",</code>,即把 Sublime Text 控制台的编码设为 GBK。此时 <code>Python.sublime-build</code> 配置如下:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">{</span><br><span class="line"> <span class="attr">"shell_cmd"</span>: <span class="string">"python -u \"$file\""</span>,</span><br><span class="line"> <span class="attr">"file_regex"</span>: <span class="string">"^[ ]*File \"(...*?)\", line ([0-9]*)"</span>,</span><br><span class="line"> <span class="attr">"selector"</span>: <span class="string">"source.python"</span>,</span><br><span class="line"> <span class="attr">"env"</span>: {<span class="attr">"PYTHONIOENCODING"</span>: <span class="string">"utf-8"</span>},</span><br><span class="line"> <span class="attr">"encoding"</span>: <span class="string">"gbk"</span>,</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>执行脚本,得到乱码,如下:</p><p><img src="https://upload-images.jianshu.io/upload_images/3310969-5cf008dd6a9f634c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="乱码类型 2-2"></p><p>注意:这里的乱码与类型 1 相同,都是用 GBK 编码解释 UTF-8 字符串造成的。</p><h3 id="过程-1-与过程-2-同时乱码。">过程 1 与过程 2 同时乱码。</h3><p>乱码是可以叠加的,即乱码后的字符串可以再次被乱码,得到的乱码与叠加前的乱码均不同。</p><p>我们让 <code>Python.sublime-build</code> 文件保持上一步的状态,然后在 Python 脚本的开头重新加上一行 <code># -*- coding: gbk -*-</code>。执行脚本,会得到前两种完全不同的乱码,如下:</p><p><img src="https://upload-images.jianshu.io/upload_images/3310969-a7829d767073e968.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="乱码类型 3"></p><p>以上就是编程中出现乱码的 3 种典型情况。需要指出的是,以上采用 Sublime Text 的控制台作为展示器,其编码可以通过 Build System 中的 <code>encoding</code> 参数进行设置。如果你直接使用命令行如 cmd、bash、cmder 等来编译和运行程序,那就完全省去这些麻烦了,<strong>命令行一般会自动识别你的输出编码,因此总能使用正确解码方式,基本不会出现类型 2 乱码,但无法避免类型 1 乱码</strong>。</p><p>希望本文对你有所启发,如果你在编程中遇到了乱码,不妨对下图中的 2 个过程进行控制变量式的排除,如果能够解决你的问题,那便是本文最大的成功。</p><p><img src="https://upload-images.jianshu.io/upload_images/3310969-1c2dadb42f018c37.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="代码编写和执行过程中的编码和解码"></p>]]></content>
<categories>
<category> 技术 </category>
<category> 原理 </category>
</categories>
<tags>
<tag> 编码 </tag>
<tag> 乱码 </tag>
<tag> Sublime Text </tag>
</tags>
</entry>
<entry>
<title>Python与C++混合编程——Boost.python的基本使用</title>
<link href="/posts/923b21ed/"/>
<url>/posts/923b21ed/</url>
<content type="html"><![CDATA[<p>Boost.python 是 Python 与 C++ 混合编程的利器,本文探讨 Boost.python 的安装和基本使用。</p><span id="more"></span><blockquote><p>Boost库是一个可移植、提供源代码的C++库,作为标准库的后备,是C++标准化进程的开发引擎之一。Boost库由C++标准委员会库工作组成员发起,其中有些内容有望成为下一代C++标准库内容。在C++社区中影响甚大,是不折不扣的“准”标准库。</p><p>Boost由于其对跨平台的强调,对标准C++的强调,与编写平台无关。大部分boost库功能的使用只需包括相应头文件即可,少数(如正则表达式库,文件系统库等)需要链接库。但Boost中也有很多是实验性质的东西,在实际的开发中使用需要谨慎。</p><p>Boost库是为C++语言标准库提供扩展的一些C++程序库的总称。</p><p>——百度百科</p></blockquote><p>简单来说,Boost 是一系列通用的 C++ 扩展库的集合。而 Boost.python 则是这众多扩展库中的其中一个,它基于 C++ 代码提供了一套 Python 接口,可作为 C++ 与 Python 混合编程的桥梁。</p><h2 id="Boost-库的安装">Boost 库的安装</h2><h3 id="方式一:Boost-源码包">方式一:Boost 源码包</h3><p>首先去 Boost 官网下载 Boost 库:<a href="https://www.boost.org/users/download/">下载地址</a>。</p><p>官方默认只提供 Boost 的源码包。大多数情况下(纯 C/C++ 开发),源码包就足够了,我们只需要在编译的时候引入相应的头文件即可。但如果你的程序需要以静态/动态链接库的形式引入某些包,就需要自己编译了。恰好 Boost.python 就是这样的需求,毕竟,你总不能指望直接在 python 代码中引用 C++ 源码吧!</p><p>我们一般会把 python 代码之外的所有 C/C++ 外挂编译成一个 <code>.pyd</code> 文件,这样就能直接在 python 代码中调用了。事实上,<code>.pyd</code> 文件就是一个拥有 python 接口的 <code>.dll</code> 文件。</p><p>Boost 源码包的目录结构如下:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">boost_1_68_0</span><br><span class="line">+---boost</span><br><span class="line">+---doc</span><br><span class="line">+---libs</span><br><span class="line">+---more</span><br><span class="line">+---status</span><br><span class="line">+---tools</span><br><span class="line">+---INSTALL</span><br><span class="line">+---Jamroot</span><br><span class="line">+---LICENSE_1_0.txt</span><br><span class="line">+---boost.css</span><br><span class="line">+---boost.png</span><br><span class="line">+---boost-build.jam</span><br><span class="line">+---boostcpp.jam</span><br><span class="line">+---bootstrap.bat</span><br><span class="line">+---bootstrap.sh</span><br><span class="line">+---index.htm</span><br><span class="line">+---index.html</span><br><span class="line">+---rst.css</span><br></pre></td></tr></table></figure><p>其中,<code>boost</code> 目录下是所有库的头文件(<code>.hpp</code>文件),<code>libs</code> 目录下则是所有库的具体实现(<code>.cpp</code>文件)。</p><h3 id="方式二:Boost-预编译包">方式二:Boost 预编译包</h3><p>Boost 源码包如果要在本地完全编译,可能需要几个小时的时间。因此,为了方便使用,Boost 也推出了 Windows 下的 <a href="https://sourceforge.net/projects/boost/files/boost-binaries/">预编译版本</a>。预编译包比起源码包多出一个子目录,用于存放已经编译好的静态库和动态库。</p><p>安装预编译版本 <code>boost_1_68_0-msvc-14.0-32.exe</code> 之后的目录结构如下:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line">boost_1_68_0</span><br><span class="line">+---boost</span><br><span class="line">+---doc</span><br><span class="line">+---libs</span><br><span class="line">+---lib32-msvc-14.0 # new</span><br><span class="line">+---more</span><br><span class="line">+---status</span><br><span class="line">+---tools</span><br><span class="line">+---INSTALL</span><br><span class="line">+---Jamroot</span><br><span class="line">+---LICENSE_1_0.txt</span><br><span class="line">+---b2.exe # new</span><br><span class="line">+---bjam.exe # new</span><br><span class="line">+---boost.css</span><br><span class="line">+---boost.png</span><br><span class="line">+---boost-build.jam</span><br><span class="line">+---boostcpp.jam</span><br><span class="line">+---bootstrap.bat</span><br><span class="line">+---bootstrap.log # new</span><br><span class="line">+---bootstrap.sh</span><br><span class="line">+---index.htm</span><br><span class="line">+---index.html</span><br><span class="line">+---project-config.jam # new</span><br><span class="line">+---rst.css</span><br></pre></td></tr></table></figure><p>其中,<code>lib32-msvc-14.0</code> 目录保存了所有编译好的静态库和动态库。需要注意的是,预编译版本对编译器种类、版本和目标位数(32/64)都有要求。在上述例子中,要使用这些库,必须用 msvc-14.0(即 Visual Studio 2015)且设置目标位数为 32 位。</p><p>不同版本的预编译包可以安装在同一个目录下,以支持不同的编译环境,这样就比较方便了。比如在上述例子中,如果还要支持 64 位,可以再下载一个 <code>boost_1_68_0-msvc-14.0-64.exe</code>,同样安装在当前安装目录下。这样安装目录下将多出一个 <code>lib64-msvc-14.0</code> 子目录,存放编译好的 64 位的库,其余目录和文件均不受影响。</p><p>预编译版本满足了偷懒的需求,但很难满足所有要求,因为有些库必须配合本地环境才能使用。比如 Boost.python 就必须配合本地的 Python 版本才能使用,因此本地编译 Boost 库这个基本技能还是必要的。下面我们还是以 Boost 源码包的使用为例进行介绍。</p><h3 id="本地编译-Boost-python">本地编译 Boost.python</h3><p>在编译之前,需要确保本机已经安装了 Visual Studio 和 Python。</p><p>首先,我们使用命令行进入 Boost 源码包的安装目录,执行 <code>bootstrap.bat</code> 脚本,将会在当前目录下生成 <code>b2.exe</code>、<code>bjam.exe</code>、<code>project-config.jam</code>、<code>bootstrap.log</code> 四个文件。其中,<code>b2.exe</code>、<code>bjam.exe</code> 就是我们编译时要用到的命令了。这两个命令的作用是一样的,<code>bjam</code> 是老版本,<code>b2</code> 是升级版本。</p><p>下面就可以开始编译 Boost.python 了,笔者在本机所使用的命令如下:</p><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">b2.exe --with-python stage --stagedir="./bin/lib32-msvc-<span class="number">14</span>.<span class="number">0</span>" link=static address-model=<span class="number">32</span></span><br></pre></td></tr></table></figure><p>编译完成后,将在 <code>./bin/lib32-msvc-14.0/lib</code> 目录下产生 4 个文件:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">libboost_numpy36-vc140-mt-gd-x32-1_68.lib</span><br><span class="line">libboost_numpy36-vc140-mt-x32-1_68.lib</span><br><span class="line">libboost_python36-vc140-mt-gd-x32-1_68.lib</span><br><span class="line">libboost_python36-vc140-mt-x32-1_68.lib</span><br></pre></td></tr></table></figure><p>其中,python 和 numpy 各 2 个,带 <code>gd</code> 的对应 debug 版本,反之对应 release 版本。</p><p>默认情况下,编译时调用的编译器和 Python 版本是 b2/bjam 自动搜索的。如果要指定不同的 Python 版本,就需要在你的 home 目录下新建一个配置文件 <code>user-config.jam</code>(路径为 <code>C:\Users\xxx\user-config.jam</code>)。推荐直接使用 Boost 根目录下的 <code>tools/build/example/user-config.jam</code> 作为模板,稍加修改即可。例如,笔者的 <code>user-config.jam</code> 中对 Python 的配置如下:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">using python</span><br><span class="line"> : 3.5</span><br><span class="line"> : "D:/App/Python35/python.exe"</span><br><span class="line"> : "D:/App/Python35/include"</span><br><span class="line"> : "D:/App/Python35/libs"</span><br><span class="line"> : <define>BOOST_ALL_NO_LIB=1</span><br><span class="line"> ;</span><br></pre></td></tr></table></figure><h4 id="b2-bjam-参数说明:">b2/bjam 参数说明:</h4><p><code>b2</code> 命令的功能强大,用起来也比较复杂,因此在使用之前,最好先查看一下该命令的帮助:</p><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">b2.exe --<span class="built_in">help</span></span><br></pre></td></tr></table></figure><p>以下是一些比较重要的参数说明:</p><ul><li><p>stage/install:</p><p>stage 表示只生成库(dll 和 lib),install 还会生成包含头文件的 include 目录。推荐使用 stage,因为 install 生成的 include 目录实际就是源码包下的 boost 目录,需要 include 的时候可以直接使用,不需要再次生成,这样可以节省大量的编译时间。</p></li><li><p>toolset:</p><p>指定编译器,可选的如 borland、gcc、msvc-14.0(VS2015)等。如果不指定,会自动搜索本地可用的编译器(可查看 <code>./project-config.jam</code> 文件以确认)。</p></li><li><p>without/with:</p><p>选择不编译/编译哪些库(类似于黑名单/白名单)。<code>--with-python</code> 的含义是仅编译 python,其他的都不编译。反过来,如果用 <code>--without-python</code>,意思就是除了 python, 其他的都编译。with/without 参数可以多次出现,以限定多个库。如果不设置 with/without 参数,默认全部编译,可能需要几个小时的时间!</p><p>需要注意,编译 Boost.python 需要确保本地安装了 Python,并且 python 命令已加入环境变量。</p><p>要查看 Boost 包含的所有库,可使用以下命令:</p> <figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">b2.exe --show-libraries</span><br></pre></td></tr></table></figure></li><li><p>stagedir/prefix:</p><p>stage 时使用 stagedir,install 时使用 prefix,表示编译生成文件的路径。推荐给不同的编译环境指定不同的目录,如 Visual Studio 2015 的 x86 应用对应的是 <code>bin/lib32-msvc-14.0</code>,x64 应用对应的是 <code>bin/lib64-msvc-14.0</code>。如果都生成到一个目录下,将没有任何益处,徒增管理难度。如果使用了 install 参数,那么还将在上述指定的目录下生成 <code>include</code> 目录,用于保存头文件。</p></li><li><p>build-dir:</p><p>编译生成的中间文件的路径,默认是 Boost 根目录下的 <code>bin.v2</code> 目录,一般无需设置。</p></li><li><p>link:</p><p>指定生成动态链接库还是静态链接库,取值为 <code>static|shared</code>。生成静态链接库使用 static,生成动态链接库需使用 shared。如不指定,默认使用 static。静态库的缺点是占用空间比较大,优点是程序发布的时候无需附带 Boost 库的 dll,比较整洁。推荐使用静态库的方式编译 Boost.python,这样发布程序的时候就不用 Boost 的 dll 了,并且也多占用不了太多空间。</p></li><li><p>runtime-link:</p><p>指定运行时是动态还是静态链接其他库。同样有 shared 和 static 两种方式。如果不指定,默认是 shared,一般无需设置。</p></li><li><p>threading:</p><p>要编译的库是单线程还是多线程,可取值 <code>single|multi</code>。如果不指定,默认是 multi,一般无需设置。</p></li><li><p>variant</p><p><code>debug|release</code>,编译 debug 版本还是 release 版本。一般与最终发布的程序是 debug 还是 release 版相对应。如果不指定,默认两个都编译,一般无需设置。</p></li><li><p>address-model</p><p>编译成 32 位版本还是 64 位版本,可取值 <code>32|64</code>。如果不指定,默认两个版本都编译。如果是编译 Boost.python,该参数就要与本地安装的 Python 位数相对应,否则编译会出错,因此最好设置一下。</p></li></ul><h4 id="Boost-静态库-动态库的命名规则">Boost 静态库/动态库的命名规则</h4><p>以 Boost.python 为例,如果编译的是静态库(<code>link=static</code>),将会生成单个 <code>.lib</code> 文件:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">libboost_python36-vc140-mt-gd-x32-1_68.lib</span><br></pre></td></tr></table></figure><p>而如果编译的是动态库(<code>link=shared</code>),将会生成两个文件(<code>.lib</code> 和 <code>.dll</code>):</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">boost_python36-vc140-mt-gd-x32-1_68.lib</span><br><span class="line">boost_python36-vc140-mt-gd-x32-1_68.dll</span><br></pre></td></tr></table></figure><p>动态库虽然也生成 <code>.lib</code> 文件,但它与静态库的 <code>.lib</code> 文件差别很大。动态库的 <code>.lib</code> 更像是对 <code>.dll</code> 的声明,二者的关系类似于 <code>.h</code> 与 <code>.cpp</code> 的关系。因此,动态库中的 <code>.lib</code> 文件要比静态库的 <code>.lib</code> 文件小得多。</p><p>下面以静态库的命名规则为例进行分析:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">libboost_python36-vc140-mt-sgd-x32-1_68.lib</span><br><span class="line">| || | | | | | || ||| | | | |</span><br><span class="line"> - --- ------ --- -- - - - --</span><br><span class="line"> 1 2 3 4 5 6 7 8 9</span><br></pre></td></tr></table></figure><ol><li>静态库以 <code>lib</code> 开头,动态库开头没有 <code>lib</code>。</li><li>所有的库都含有 <code>boost</code> 前缀。</li><li>Boost 库名称,本例中为 <code>python36</code>。</li><li>编译器名称及其版本,<code>vc140</code> 指的是 msvc-14.0,对应 Visual Studio 2015。</li><li>有 <code>mt</code> 代表 <code>threading=multi</code>,没有则代表 <code>threading=single</code>。</li><li>有 <code>s</code> 代表 <code>runtime-link=static</code>,没有则代表 <code>runtime-link=shared</code>。</li><li>有 <code>gd</code> 代表 debug 版本,没有则代表 release 版本。</li><li>目标位数,<code>x32</code> 代表 32 位,<code>x64</code> 代表 64 位。</li><li>Boost 库的版本号,<code>1_68</code> 代表 Boost 1.68 版本。</li></ol><h2 id="Boost-库的使用">Boost 库的使用</h2><p>如果要在项目中使用 Boost 库,需要做以下两项配置:</p><ol><li>包含 Boost 头文件(本例中为:<code>D:\ProgramFiles\boost_1_68_0</code>);</li><li>链接 Boost 库文件(本例中为:<code>D:\ProgramFiles\boost_1_68_0\bin\lib32-msvc-14.0\lib</code>)。</li></ol><p>Boost 官方推荐使用 <code>b2/bjam</code> 命令进行自动化编译、链接,只需要编写一个 <code>.jam</code> 配置文件,这种方式类似于 <code>make</code> 和 <code>Makefile</code>。但考虑到还要学习 <code>jam</code> 语法,暂时还是用 Visual Studio 手动编译吧。</p><p>在 Visual Studio 中的具体操作如下:</p><ol><li>选中当前项目,点击属性按钮,依次选择“配置属性->C/C+±>常规->附加包含目录”,编辑并添加一条路径:<code>D:\ProgramFiles\boost_1_68_0</code>;</li><li>选中当前项目,点击属性按钮,依次选择“配置属性->链接器->常规->附加库目录”,编辑并添加一条路径:<code>D:\ProgramFiles\boost_1_68_0\bin\lib32-msvc-14.0\lib</code>。</li></ol><h3 id="Boost-python-的使用">Boost.python 的使用</h3><p>要使用 Boost.python,除了上述两项配置之外,还需要再添加两项配置:</p><ol><li>包含 Python 头文件(本例中为:<code>D:\Program Files (x86)\Python36-32\include</code>);</li><li>包含 Python 静态库文件(本例中为:<code>D:\Program Files (x86)\Python36-32\libs\python36.lib</code>)。</li></ol><p>在 Visual Studio 中的具体操作如下:</p><ol><li>选中当前项目,点击属性按钮,依次选择“配置属性->C/C+±>常规->附加包含目录”,编辑并添加一条路径:<code>D:\Program Files (x86)\Python36-32\include</code>;</li><li>选中当前项目,点击属性按钮,依次选择“配置属性->链接器->输入->附加依赖项”,编辑并添加一个文件:<code>"D:\Program Files (x86)\Python36-32\libs\python36.lib"</code>,注意要有双引号,否则可能识别不正确。</li></ol><h2 id="测试">测试</h2><h3 id="准备-hello-world-代码">准备 hello world 代码</h3><p>使用官网给出的 hello world 示例,代码文件位于 Boost 安装目录下:<code>D:\ProgramFiles\boost_1_68_0\libs\python\example\tutorial</code>,包括 <code>hello.cpp</code> 与 <code>hello.py</code> 两个文件,如下:</p><p><code>hello.cpp</code>:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="comment">// 当引入 #include <boost/python/xxx> 时,Boost 会默认链接 boost_python 动态链接库,</span></span><br><span class="line"><span class="comment">// 如果我们想要链接静态链接库,就需要在 include 之前加上 #define BOOST_PYTHON_STATIC_LIB</span></span><br><span class="line">#<span class="meta">#<span class="meta-keyword">define</span> BOOST_PYTHON_STATIC_LIB</span></span><br><span class="line"></span><br><span class="line">#<span class="meta">#<span class="meta-keyword">include</span> <span class="meta-string"><boost/python/module.hpp></span></span></span><br><span class="line">#<span class="meta">#<span class="meta-keyword">include</span> <span class="meta-string"><boost/python/def.hpp></span></span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">char</span> <span class="keyword">const</span>* <span class="title">greet</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"hello, world"</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">BOOST_PYTHON_MODULE(hello_ext)</span><br><span class="line">{</span><br><span class="line"> <span class="keyword">using</span> <span class="keyword">namespace</span> boost::python;</span><br><span class="line"> def(<span class="string">"greet"</span>, greet);</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><code>hello.py</code>:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> hello_ext</span><br><span class="line">print(hello_ext.greet())</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>我们的目的是将 <code>hello.cpp</code> 编译成一个 <code>.pyd</code> 文件(其实就是一个包含 Python 接口的 <code>.dll</code> 文件),然后用 <code>hello.py</code> 调用它。</p><p><strong>注意</strong>:<code>hello.cpp</code> 中的宏定义 <code>#define BOOST_PYTHON_STATIC_LIB</code> 非常重要,它可以指定链接 boost_python 的静态库,而不是默认的动态库。</p><p>如果你在编译过程中遇到了这样的错误:<code>LINK : fatal error LNK1104: cannot open file "boost_python36-vc140-mt-x32-1_68.lib"</code>,但是你明明已经引入了静态库:<code>libboost_python36-vc140-mt-x32-1_68.lib</code>,你可能会纳闷:我明明已经导入了静态库,为什么还找我要动态库?那么最可能的原因是,你的 C++ 代码中漏掉了这句宏定义。</p><h3 id="使用-Visual-Studio-构建-hello-world">使用 Visual Studio 构建 hello world</h3><p>Boost 官方推荐使用 <code>b2/bjam</code> 命令进行自动化编译、链接,只需要编写一个 <code>.jam</code> 配置文件,这种方式类似于 <code>make</code> 和 <code>Makefile</code>。但考虑到还要学习 <code>jam</code> 语法,暂时还是用 Visual Studio 手动编译吧。</p><ol><li>首先使用 Visual Studio 创建一个空项目。</li><li>打开项目属性,在“配置属性->常规”中,进行以下修改:<ul><li>配置类型设为:“动态库(.dll)”。</li><li>目标文件名设为:<code>hello_ext</code>。<br><strong>注意</strong>:<code>.pyd</code> 文件的名称必须与要导出的 python module 名称一致,否则 python 的 import 语句将会报错:<code>ImportError: dynamic module does not define module export function</code>。</li><li>目标文件扩展名设为:<code>.pyd</code>。</li></ul></li><li>将 <code>hello.cpp</code> 文件导入项目。</li><li>参照上一节 [Boost 库的使用] 中的说明,将 Boost 头文件目录、Boost 库文件目录、Python 头文件目录、Python 静态库文件都导入到项目中。</li><li>选择菜单“生成->生成 Project”,如果一切无误,将会生成一个 <code>hello_ext.pyd</code> 文件。由于生成器的默认配置是 <code>Debug-x86</code>,因此生成目录为项目下的 Debug 目录。</li><li>将 <code>hello_ext.pyd</code> 与 <code>hello.py</code> 放在同一目录下,执行 <code>hello.py</code>,将会看到输出:<code>hello, world</code>。</li></ol><h2 id="参考">参考</h2><ol><li><a href="https://baike.baidu.com/item/boost/69144">Boost 百度百科</a></li><li><a href="https://www.cnblogs.com/zhcncn/p/3950477.html">boost 1.56.0 编译及使用</a></li><li><a href="https://www.boost.org/doc/libs/1_65_1/libs/python/doc/html/tutorial/index.html">Boost.python QuickStart</a></li><li><a href="https://github.com/boostorg/build">Boost.build</a></li></ol>]]></content>
<categories>
<category> 技术 </category>
</categories>
<tags>
<tag> python </tag>
<tag> C/C++ </tag>
<tag> Boost </tag>
</tags>
</entry>
<entry>
<title>读《小王子》感悟——谈谈成长与爱</title>
<link href="/posts/d8e04a55/"/>
<url>/posts/d8e04a55/</url>
<content type="html"><![CDATA[<p>爱是一个宏大的主题,大到贯穿整个人类几千年的文明史。尽管如此经年累月,但历史上又有多少人能真正把她说清楚呢?</p><span id="more"></span><p>《小王子》一书从一个独特的角度让我们窥见了这个伟大主题的一角。小说的主线叙述了小王子——一个纯真无邪、让人心生怜爱的小男孩的成长故事。这是怎样一个纯真无邪的灵魂呢?在小王子的眼里,大人们终其一生孜孜以求的权利、名誉、财富、甚至规则都是极其无聊、难以理解的。比起这些,他的绵羊、小玫瑰、日出日落,甚至猴面包树才更值得关注。用小王子的话说,“grown-ups are very strange”,他们总是忙于自己那无聊的“matters of consequence”,他们从不关注真正有意义的事物,比如一朵花、一颗星星。</p><p>小王子的成长过程也是对爱不断体悟的过程。</p><p>在一个美妙的早晨,当一朵他从未见过的、美丽的、独一无二的玫瑰花终于从花苞中探出娇艳的小脸儿时,小王子顿时被她的美丽所打动,这大概是小王子心中第一次萌生爱的种子。</p><p>但不料这朵小玫瑰却表现得相当傲慢,她作出骄傲却又娇弱的样子:“I am not at all afraid of tigers, but I have a horror of drafts.”。她不停地用自己的虚荣心把小王子呼来唤去,但又有谁知道她拙劣的伎俩背后隐藏着的热烈的爱意?当然,小王子对这种莫名而来的折磨无法理解,矛盾逐渐萌生。最终,小王子决定离开他的小玫瑰,离开他的B612小行星,独自进行一趟旅行。</p><p>在即将离别时,小王子似乎已经感受到了些许伤感和不舍,但在骄傲的、故作坚强的小玫瑰的催促下,他还是离开了。从小王子的身影中,我们或可窥见孩提时的自己,那个同样幼稚、同样会负气出走的自己。</p><p>在旅行中,小王子遇到了形形色色的成年人。在小王子看来,他们头脑中所想的事情都甚为奇怪,甚至无聊、可笑。直到有一天,一个地理学家告诉小王子"flowers are ephemeral(花儿是转瞬即逝的)"。小王子如同惊梦一般,回想起了自己的小玫瑰,“My flower is ephemeral, and she has only four thorns to defend herself against the world. And I have left her on my planet, all alone!”,他开始为离开自己的小玫瑰感到内疚。</p><p>这真是一段漫长的旅行,在经过了六个星球之后,小王子最终来到了地球。那是一个孤独的夜晚,闪烁的星光让小王子想起了自己的家:“Look at my planet. It is right there above us. But how far away it is!”。当然,他还想起了那朵小玫瑰:“I have been having some trouble with a flower”。我想,小王子说出这句话的时候一定感到后悔和无奈,他已经在不知不觉中成长了很多。</p><p>小王子在沙漠中行走了很久,一天,他遇到了一个不可思议的壮观的花园,足足有五千多玫瑰花,全都和他的小玫瑰一模一样。这一幕对小王子造成了莫大的打击,因为他想起了小玫瑰的话,他曾经相信自己的那朵小玫瑰是全宇宙独一无二的。</p><p>正在小王子伤心不已的时刻,他遇到了那只在他生命中独一无二的狐狸,正如他独一无二的小玫瑰一样。狐狸用“驯化”的理论向小王子揭开了爱的真实含义:“To me, you are still nothing more than a little boy who is just like a hundred thousand other little boys. And I have no need of you. And you, on your part, have no need of me. To you, I am nothing more than a fox like a hundred thousand other foxes. But if you tame me, then we shall need each other. To me, you will be unique in all the world. To you, I shall be unique in all the world…”。驯化就像一种纽带,一旦建立,纽带两端的人便从千千万万相同的物种中脱颖而出,成为了彼此心中独一无二的精神牵绊。小王子终于明白了他对那朵小玫瑰的奇妙感觉:“There is a flower… I think that she has tamed me…”,他和小玫瑰一定是相互驯化了彼此。</p><p>在与作者相遇后,小王子曾无数次地提起他的玫瑰花,并且开始直言不讳地表达出自己的爱意:“If someone loves a flower, of which just one single blossom grows in all the millions and millions of stars, it is enough to make him happy just to look at the stars. He can say to himself, ‘Somewhere, my flower is there…’ But if the sheep eats the flower, in one moment all his stars will be darkened.”。如果一个人爱上了某个星星上的一朵花,那么当他望着满天繁星的时候便会感到幸福,因为他知道,自己所爱的那朵花就在那里。而一旦他失去了这朵花,他眼中所有的星星便会随之熄灭、黯淡。</p><p>在这场旅行中,小王子对小玫瑰的态度经历了从不解、到内疚、再到思念,以至于表达出真正的爱意。小王子成长的过程其实也是发现爱、表达爱的过程。最后,小王子终于彻底理解了小玫瑰的心思,他不无悔恨地说:“The fact is that I did not know how to understand anything! I ought to have judged by deeds and not by words. She cast her fragrance and her radiance over me. I ought never to have run away from her… I ought to have guessed all the affection that lay behind her poor little stratagems. Flowers are so inconsistent!”。当小王子说出"But I was too young to know how to love her.“时,其实他已经长大了。他发现了小玫瑰对他的爱意,也熟知小玫瑰每一条细枝末节的缺点,但他依然珍视着她的独一无二。小王子已经在旅行和成长中获得了爱的力量,他明白了"I am responsable for her.”。</p><p>生活中的我们虽然看起来比小王子成熟太多,但是正如作者所说的,我们其实都是讨人厌的 grown-ups。我们要么是忙于自己的事情,不懂得什么是爱,要么只是嘴上说着爱,以为努力工作、承担责任就是爱。但对于我们的亲人、朋友来说,我们也许一点都不了解他们。对于我们的所爱之人来说,我们也许充其量可算作一个忠实的生活伴侣,距离真正恩爱的精神伴侣实在相差太远。我们需要在生活中体会那句"I am responsable for her"。</p><p>真正的爱需要思考,需要理解对方的精神世界。在忙碌的生活中,我们不妨时常反思一下,我们真正了解身边的那个人吗?像小王子一样明白地知道对方的心中所想吗?生活之于我们就像旅行之于小王子,当我们忙于应付眼前的苟且、对付形形色色的人时,我们的心中也会自发地涌起小王子那样的思索吗?小王子的成长过程,也许能为我们的成长带来些许阳光、些许启发。</p><p>作为结束,我不想像 grown-ups 一样,对抽象的爱大书特书。小王子教给了我们一个最重要的品质,那就是纯真。假作者还在,他大概会如此阐述爱:</p><p>在某个遥远的地方,谁也不知道究竟在何处,有一只小绵羊,它可能吃掉了一朵玫瑰,也可能没有。在认识小王子之前,这么一个小小的可能性,在与我相关的亿万个人物、时间、场景之中,它显然微不足道,犹如一粒尘埃之于撒哈拉沙漠。但在认识小王子之后,关于那朵玫瑰的一毫一发,竟也时时牵动着我的内心。</p>]]></content>
<categories>
<category> 生活 </category>
<category> 读书 </category>
</categories>
<tags>
<tag> 成长 </tag>
</tags>
</entry>
<entry>
<title>Hexo + Github 搭建个人博客</title>
<link href="/posts/4d5f7337/"/>
<url>/posts/4d5f7337/</url>
<content type="html"><![CDATA[<p>按照以下步骤,你可以搭建一个与本站点同样简洁漂亮的个人博客,完全自由免费。</p><span id="more"></span><p>前提条件:</p><ul><li>熟悉 git + Github</li><li>熟悉 Nodejs</li><li>写博客当然还少不了 Markdown 基础</li></ul><h2 id="创建-Github-仓库">创建 Github 仓库</h2><p>在 Github 中创建一个仓库,名为 <code>xxx.github.io</code></p><blockquote><p>注:<code>xxx</code> 必须是你自己的 Github 用户名,否则博客网站将无法打开。例如,我的仓库命名为 <code>hzhu212.github.io</code></p></blockquote><h2 id="安装-Hexo">安装 Hexo</h2><p>要求已安装 <a href="https://nodejs.org/en/">Nodejs</a></p><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install -g hexo-cli</span><br></pre></td></tr></table></figure><p>考虑到网络问题,可能需要几分钟的时间。<br>如果由于网络问题无法安装,你可能需要使用 <a href="http://npm.taobao.org/">cnpm</a> 或 <a href="https://github.com/Pana/nrm">nrm</a></p><h2 id="创建-blog-项目">创建 blog 项目</h2><p>命令行定位到需要存放博客的目录,以下以 Windows 平台为例:</p><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> /d C:\users\xxx</span><br></pre></td></tr></table></figure><p>执行以下命令:</p><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">hexo init blog</span><br></pre></td></tr></table></figure><p>将在 <code>C:\users\xxx</code> 目录下 <strong>新建</strong> 一个子目录 <code>blog</code>,作为博客项目的根目录。</p><p>接下来需要安装依赖库:</p><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> blog</span><br><span class="line">npm install</span><br></pre></td></tr></table></figure><h2 id="本地调试与发布">本地调试与发布</h2><p>连续执行以下 2 条命令:</p><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">hexo g</span><br><span class="line">hexo s</span><br></pre></td></tr></table></figure><p>当命令运行完成后,打开浏览器访问 <code>http://localhost:4000/</code>。</p><p>Boom!~</p><p>你的博客站点新鲜出炉了~,还附带了第一篇 <strong>Hello World</strong> 博客。</p><blockquote><p>注:上面的 2 条命令其实是简写:<br><code>hexo g</code> = <code>hexo generate</code>,生成静态文件<br><code>hexo s</code> = <code>hexo server</code>,开启站点服务器</p></blockquote><h2 id="开始写作吧">开始写作吧</h2><p><code>ctrl+c</code> 退出 hexo server,然后执行:</p><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">hexo n "my first blog"</span><br></pre></td></tr></table></figure><p>将创建一篇新的博客,标题为 【<em>my first blog</em>】</p><blockquote><p>注:每篇博客都是一个 Markdown 文件,默认放在 <code>/blog/source/_posts/</code> 目录下,可直接编辑</p></blockquote><p>再次执行</p><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">hexo s</span><br></pre></td></tr></table></figure><p>访问 <code>http://localhost:4000/</code>,你的第一篇博客大功告成了!</p><blockquote><p>注:hexo server 支持热更新,编辑完 Markdown 文件后,只需刷新页面就能立即更新博客</p></blockquote><h2 id="发布网站">发布网站</h2><p>目前的你的博客网站还只支持本地访问,为了让 13 亿人都能够欣赏你的大作,你需要把它推送到 Github 上。</p><p>hexo 发布网站非常简单,只需要一条命令:</p><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">hexo d</span><br></pre></td></tr></table></figure><p><strong>但万事总有个 BUT,在此之前,你还需要一些准备工作:</strong></p><h3 id="安装-Github-部署插件">安装 Github 部署插件</h3><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install --save hexo-deployer-git</span><br></pre></td></tr></table></figure><h3 id="告诉-hexo-你要把网站部署到何处">告诉 hexo 你要把网站部署到何处</h3><p>进入 <code>/blog</code> 目录,找到 <code>_config.yml</code> 文件,这是博客项目的全局配置文件。打开它并翻到末尾,看到 <code>deploy</code> 选项,我们把它修改为:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">deploy:</span></span><br><span class="line"> <span class="attr">type:</span> <span class="string">git</span></span><br><span class="line"> <span class="attr">repo:</span> <span class="string">[email protected]:xxx/xxx.github.io.git</span> <span class="comment"># 这里改成你第一步创建的 git 仓库地址</span></span><br><span class="line"> <span class="attr">branch:</span> <span class="string">master</span></span><br></pre></td></tr></table></figure><p>保存配置文件。然后依次执行以下 3 条命令:</p><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">hexo clean</span><br><span class="line">hexo g</span><br><span class="line">hexo d</span><br></pre></td></tr></table></figure><blockquote><p>注:<code>hexo d</code> = <code>hexo deploy</code>,部署</p></blockquote><p>Bingo~</p><p>你的个人博客网站 <code>https://xxx.github.io</code> 建成了,不费吹灰之力!</p><p>悄悄夹带上我的私货:<a href="https://hzhu212.github.io/">hzhu212.github.io</a></p><h2 id="美化你的博客网站">美化你的博客网站</h2><p>网站是建成了,然而现在的你并没有一丝喜悦,我知道你一定是被 Hexo 的默认主题美哭了!<br>不过别失望,Hexo 为我们提供了 <a href="https://hexo.io/themes/">海量的主题库</a></p><p>选择一款自我感觉良好的主题,在介绍页面上找到它的 <strong>Github 仓库地址</strong>,然后把它下载下来。以 <strong>Anisina</strong> 主题为例:</p><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git clone https://github.com/Haojen/hexo-theme-Anisina themes/Anisina</span><br></pre></td></tr></table></figure><p>再次打开全局配置文件 <code>blog/_config.yml</code>,翻到末尾,找到 <code>theme</code> 配置项,将其修改为:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">theme:</span> <span class="string">Anisina</span></span><br></pre></td></tr></table></figure><p>重新部署,你的网站焕然一新了!</p><blockquote><p>注:有些主题会内置多种 <strong>样式(scheme)</strong>,例如我使用的 <a href="https://github.com/iissnan/hexo-theme-next">next主题</a>,定制性更强。<br>用户可进入 <code>/blog/themes/next/</code> 目录,打开主题配置文件 <code>_config.yml</code> ,在其中的 <code>scheme</code> 字段下选择一种样式。</p></blockquote><p>关于 Hexo 更多信息请访问 <a href="https://hexo.io/">Hexo 官网</a></p>]]></content>
<categories>
<category> 技术 </category>
</categories>
<tags>
<tag> web </tag>
<tag> Hexo </tag>
</tags>
</entry>
</search>