普通视图

发现新文章,点击刷新页面。
昨天以前ControlNet Blog

异星工厂中的高品质产率分析

作者 ControlNet
2024年12月31日 21:22

异星工厂的品质扩展包给游戏带来了新的生产规划挑战,比起像以前只能横向扩张工厂的规模,现在可以通过使用高品质的工厂和插件,大幅增加产量。为了最大化如传奇品质的高品质物品的生产,我们需要对高品质产率的计算和规划进行一些分析。

高品质物品生产蓝图

一般来说生产高品质的物品有两种方法,生产出目标物品然后慢慢回收提升质量,或者是直接从源头生产高品质的原料,然后直接生产高品质的物品。

这里先初步的设计了两个蓝图,一个是用于电星蓝图的高品质原料生产,通过读取当前物流网络的信号,自动的将多余物品拿去回收,不断的生产高品质的原料。对于原料级别的物品(如铁板、铜板),比起直接放入回收机拿到同样的物品,将其放入到组装机里生产更高级别的物品,然后再回收回来,可以获得更高的品质。

例如下图里的:
blueprint1
Fig. 1. 回收原料生产高品质物品的例子.

这个蓝图设计成完全全自动的模式,能自动进行负载均衡,回收多余的物品。

回收机+组装机3:

1
0eNrVV1tv2zYU/i98XKnWki+NDWy/Ym+pIcgykxCRKJei3BmBAGdrkHZIU2xNt16yBtmwre2wG9AWa9LAP2aVpTztL+xQkmXZlmunAzYM8ANN8nzn9p1zqC3UsjzS4ZQJ1NhCbeKanHYEdRhqoPPDfvTD9tvT/eD1IDg6Ot+5Ex68DH96GA2eDPe/D0++DU8Gwa3fwtvPhi+3w7MvwtPD4OcH0Ysfr7FrLDi7N7z9LPj9fvjo5vDrV0H/4V9v9hLE8/6jaLAbHjxNIb7cC08HsEhwh7/effvH8+DO/WDnM5Cd0f04Ot6Ldp8Hnz+NXhxFZ2fBN7vR8fGf/U8RRtR0mIsaq1vIpevMsKRPzLAJOMOJ2TMtwpEP11ibfIIaqo8LLhquS+yWRdm6YhvmBmVEKeeENL+JEWGCCkoSVfGfns48uwXwDRVnSKagXaJ0uNOlbcIVc4O4AqzsOC5NQryFAFFZWblcxagHouXq5aovrZrC1MaYnIoNmwhqKqZjtygzhMOLMCs5RIzaFPxPzlUAgzAJ7lh6i2wYXQoAIDVG1uG4HaO58mCNclfoM3HqGBwWAvwqyfC4REpJUVcYkk0KBMLpEG6kbPoArHQ80fEWYflFESjjmUTOOl1XU6drky5DLKggdpIu2s4pvu4ZFqhRbKftWUTR4kQnV+Em0ynrgiEO7yWi438ACX6am6hRktYWnqhzT7S5J2W/6cNvNgCVLAAtz9pUKHMJF4VRyNGpNp38SgFyNUMeJW8Btcb4V2P8Ija5iUZ3ch3HMKk9DLyywIHp3QJG4FGaYJ853AbqSKW2vCNNbKAP4w1P0i4NelKrGIleR8J1KRdeLJfiJxRUPr4geKlUguw0Cylau3CR1ksTcfzXinTRtXzZXlm2bPGEDToj4obDN2PjOIGaE9wjGK1zQgB2zbBcMm3QrEx8LROSCMXd4Sp+Z/cu4HBtqkYg8rQz6ZOKRtt6AUX+9w1lZfmGUrlYQ6nnevV1D2beu2ZfeQq7iPUm5aZHhQ5BJbqzpuf4CR1jdJoVhE6Y0bIy/sTZje3Qs5YzpynJWM36o5Yyh9rAh3iULzV7ly7rFHaqpvP/Vpct8YnW9RGSr5WkfOe8iyZHb3FnU8d92fVaQKzYmwLHtWz+FqG8xytG+68b5ML54b//KwdScwM8kolZ1bCKwadyE8OyjIFycllNdtV4qeEa1tIVZEQua/I8uVrDFVyPN2FDlRfUJijIdCbpT92BlpV5Ejx+Mjx4BY/44N627GvtedNXyQIqfVo8NWMOpApTDuDRooHkKJ024vzBraD/BuXk8oaGpzej73aGhyfwMTD86vWsreo/sbWZ9GnZDrMvIowso0UsqfuXvvwiia0ECy5ltsCdLgQ3VlStafVKvV6tatVaWVN9/29ridSk

回收机:

1
0eNrFVt1u2zYUfhdeDlRnyVYSC1ifonepIcgS0xCRJZei3BmBAGdbkXZIW3RNsP6kDTJgW3/QrgPaYksa+GFWWfLVXmGHlCz/yXFbFJh9c8TD8/GcjzwfuY2abkjajHocGdvIIYHNaJtT30MGGh720t92Ppzejv/ux0dHw+u3kv23yYsHaf/J4PavyckvyUk/vvE6ufls8HYnObubnB7GL++nb36/7F324rN7g5vP4j8Pkoc/DH5+F/ce/Pt+L0Mc9h6m/d1k/2kO8dNectoHI8Md/HHnw1/P41sH8fXvIXZu7Ufp8V66+zz+8Wn65ig9O4sf76bHx//0vkMYUdv3AmSsb6OAXvEsV9TkWS0CxTBid22XMBTBNM8h3yJDjXDJRItRvtkinNqK7bea1LO4PxmlRQ2MiMcppyRbS350TS9sNQHfUHEBZXPaIUqb+R3qEKbYmyTgkGbbD2jG8TYCREVb1S/oGHXBVPXKBT0Sic2gavj8BMtQa5OYGDkUSMhmqAAHXHHmu2aTbFodChAQN8Y2we1IvEA4NigLuDlHVttiYHCorSIoCoiIWjrNbxNm5Yfsa8jcD3k7XI4+mYPpEX7NZ1syOUYcZHAWEoyuMEIAdsNyAzKb0HyMnFYECYQoKiO/+hnka/8T+aIFuCX6WVGnqf7qY6ku5aBWcDBaYRkDlelDXV50kLESTNvQVaMmFfvuQmKzoyWJY3Q1tFxIGsY9n7WgQrFoS8wRSRroGzkQCnZk9496GiPebQu4DmU8lHE5fsaUcunTwPUK/KIG/Muo1PGcKpXxt1awN31+YCcoJ62cEGdiF/MMlZbvhC5RNClb2VRRq0m9DqTis27OZfEFkLCl9hYyKhktJR51oUdb6KkKBholFKwUFDRDd0uhXkAYX8DDWBxr9dleqpVgr07QezUEyT1Xemuz6GWn1KbMDik3gVhi+hvmRFvBURx5i641iWc13UJhADTPxCzO8oLTLviar2itqMghdnaVnN961YWELRKfHHhGeSa/1j9WiKaa4iIS92UmOwuu5mntKW+Z+mcosPrJJHzp62+pqERfUqHVyniBsAkdKFFLmFmpj5VF7M41YEbszbqGVbyGqw0MFtgVrEmzhlelVQV3XbqruJpPrMkxVVpFyBoMgqk2ALzIO9v9nJI8ZzwyDCQlc0zR4OD1cPfO8P6NuPceiWLzQNC7Yk786Mlg/x28P+N7O+L15yy6D5RiNwUHy3VcsiKEU+hT8UDGyLWaBPYEJa964oEqlx8cnoCnA/VJeH1Fq9fqdV3X9JWqpkbRf5i4IY0=

另外一种模式是通过将低级的产物不断回收,不断循环直到生产出高品质的物品,如下所示。

blueprint2
Fig. 1. 回收产物生产高品质物品的例子.

回收机+组装机3:

1
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

回收机+电磁工厂:

1
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

回收机+铸造厂:

1
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

原理分析

如果是生产高品质的原料,比如说铜板,那么将铜板生产成铜线,就会考虑到底是使用产能插件好还是质量插件好。虽然质量插件提升了出现高品质物品的概率,但是产能插件大幅增加了产率,特别是高品质的高级产能插件,能提升相当大比例的产率,可能能生产出更多的高品质物品。

参考Figure 2,考虑以下的生产步骤:

flowchart LR    Input[(蓝箱输入)] --> Ass[组装机] --> |低品质| Rec[回收机]    Ass --> |高品质| Output[/红箱输出/]    Rec --> Ass

如果以上图表没有正确渲染,请刷新页面。

令每次的产物$X$都用百分比表示,其中1代表原料投入的数量,如果是0.1,则表示剩下了10%的物品。

$$ X = (x_{普通}, x_{罕见}, x_{稀有}, x_{史诗}, x_{传奇}) $$

而每次经过组装机或者回收机,则是将原料于一个转换矩阵$T$相乘,输出则是新的产物的数量。不过在生产出传奇物品之后,会将这部分收集起来,不参与矩阵乘法。

其中$T$可以表示为

$$
\begin{align*}
T &= \begin{pmatrix}
T_{普通到普通} & T_{普通到罕见} & T_{普通到稀有} & T_{普通到史诗} & T_{普通到传奇} \\
0 & T_{罕见到罕见} & T_{罕见到稀有} & T_{罕见到史诗} & T_{罕见到传奇} \\
0 & 0 & T_{稀有到稀有} & T_{稀有到史诗} & T_{稀有到传奇} \\
0 & 0 & 0 & T_{史诗到史诗} & T_{史诗到传奇} \\
0 & 0 & 0 & 0 & T_{传奇到传奇} \\
\end{pmatrix} \\
&= (1 + P)\begin{pmatrix}
Q_{普通到普通} & Q_{普通到罕见} & Q_{普通到稀有} & Q_{普通到史诗} & Q_{普通到传奇} \\
0 & Q_{罕见到罕见} & Q_{罕见到稀有} & Q_{罕见到史诗} & Q_{罕见到传奇} \\
0 & 0 & Q_{稀有到稀有} & Q_{稀有到史诗} & Q_{稀有到传奇} \\
0 & 0 & 0 & Q_{史诗到史诗} & Q_{史诗到传奇} \\
0 & 0 & 0 & 0 & Q_{传奇到传奇} \\
\end{pmatrix}
\end{align*}
$$
其中$P$是额外产率,比如说用了产能插件之后会增加,而对于回收机来说,应该取值$P=-0.75$。而矩阵中的$Q_{*}$是指在给定总共的质量加成$Q$的情况下,计算出的
每一个品级到另一个品级的转换率。这部分的计算可以参考官方wiki[1]

定义每次产物的的起始状态为

$$X_{0} = (1, 0, 0, 0, 0)$$

则之后的每一次的产物状态可以表示为

$$X_{t} = X_{t-1}T_{回收机}T_{组装机}$$

但是需要注意的是,每次进入到下个循环之前,需要移除掉传奇物品,然后将其加到最终的产物中。

代码实现

当流程确定之后,就可以通过Scallop[2]来实现这个过程。Scallop是一个用Rust实现的,基于符号推理的编程语言,可以用来解决这类问题。

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
type production_after_assembler(bound iter: i32, common: f32, uncommon: f32, rare: f32, epic: f32, legendary: f32)
type production_after_recycler(bound iter: i32, common: f32, uncommon: f32, rare: f32, epic: f32, legendary: f32)

type prod_rate(f32)
type qual_rate(f32)
type assembler_base_prod(f32)

type MachineType = ASSEMBLER | RECYCLER // 0: ASSEMBLER, 1: RECYCLER
type n_slots(f32, MachineType)
type n_qual(f32, MachineType)
type n_prod(f32, MachineType)
type base_prod(f32, MachineType)
type P(f32, MachineType)
type Q(f32, MachineType)
type T_self(f32, MachineType)
type T_c2l(f32, MachineType)
type T_c2e(f32, MachineType)
type T_c2r(f32, MachineType)
type T_c2u(f32, MachineType)
type T_u2l(f32, MachineType)
type T_u2e(f32, MachineType)
type T_u2r(f32, MachineType)
type T_r2l(f32, MachineType)
type T_r2e(f32, MachineType)
type T_e2l(f32, MachineType)

rel n_slots(4, RECYCLER)
rel base_prod(-0.75, RECYCLER) // only give 1/4 material back
rel base_prod(b_p, ASSEMBLER) = assembler_base_prod(b_p)
rel n_qual(4, RECYCLER)
rel n_prod(x, m) = n_qual(y, m) and x + y == n_s and n_slots(n_s, m)
// total production and quality rate for each machine
rel P(p_r * n_p + b_p, m) = n_prod(n_p, m) and base_prod(b_p, m) and prod_rate(p_r)
rel Q(q_r * n_q, m) = n_qual(n_q, m) and qual_rate(q_r)

// transform the quality to itself
rel T_self(1 - q, m) = Q(q, m)
// transform common to higher
rel T_c2l(q * 0.001, m) = Q(q, m)
rel T_c2e(x, m) = Q(q, m) and T_c2l(t, m) and q * 0.01 == x + t
rel T_c2r(x, m) = Q(q, m) and T_c2e(t1, m) and T_c2l(t2, m) and q * 0.1 == x + t1 + t2
rel T_c2u(x, m) = Q(q, m) and T_c2r(t1, m) and T_c2e(t2, m) and T_c2l(t3, m) and q == x + t1 + t2 + t3
// transform uncommon to higher
rel T_u2l(q * 0.01, m) = Q(q, m)
rel T_u2e(x, m) = Q(q, m) and T_u2l(t, m) and q * 0.1 == x + t
rel T_u2r(x, m) = Q(q, m) and T_u2e(t1, m) and T_u2l(t2, m) and q == x + t1 + t2
// transform rare to higher
rel T_r2l(q * 0.1, m) = Q(q, m)
rel T_r2e(x, m) = Q(q, m) and T_r2l(t, m) and q == x + t
// transform epic to higher
rel T_e2l(q, m) = Q(q, m)

rel production_after_recycler(0, 1.0, 0.0, 0.0, 0.0, 0.0) // initial state
rel production_after_assembler(
iter,
(1 + p) * (c * t_self),
(1 + p) * (c * t_c2u + u * t_self),
(1 + p) * (c * t_c2r + u * t_u2r + r * t_self),
(1 + p) * (c * t_c2e + u * t_u2e + r * t_r2e + e * t_self),
(1 + p) * (c * t_c2l + u * t_u2l + r * t_r2l + e * t_e2l) + l
) = production_after_recycler(iter, c, u, r, e, l) and T_self(t_self, m) and T_c2u(t_c2u, m)
and T_c2r(t_c2r, m) and T_c2e(t_c2e, m) and T_c2l(t_c2l, m)
and T_u2l(t_u2l, m) and T_u2e(t_u2e, m) and T_u2r(t_u2r, m)
and T_r2l(t_r2l, m) and T_r2e(t_r2e, m) and T_e2l(t_e2l, m)
and P(p, m) and m == ASSEMBLER and iter >= 0

rel production_after_recycler(
iter + 1,
(1 + p) * (c * t_self),
(1 + p) * (c * t_c2u + u * t_self),
(1 + p) * (c * t_c2r + u * t_u2r + r * t_self),
(1 + p) * (c * t_c2e + u * t_u2e + r * t_r2e + e * t_self),
(1 + p) * (c * t_c2l + u * t_u2l + r * t_r2l + e * t_e2l) + l
) = production_after_assembler(iter, c, u, r, e, l) and T_self(t_self, m) and T_c2u(t_c2u, m)
and T_c2r(t_c2r, m) and T_c2e(t_c2e, m) and T_c2l(t_c2l, m)
and T_u2l(t_u2l, m) and T_u2e(t_u2e, m) and T_u2r(t_u2r, m)
and T_r2l(t_r2l, m) and T_r2e(t_r2e, m) and T_e2l(t_e2l, m)
and P(p, m) and m == RECYCLER and iter >= 0

rel legendary(l, n_s, n_q, p_r, q_r, b_p) = production_after_recycler(20, c, u, r, e, l)
and n_slots(n_s, ASSEMBLER) and n_qual(n_q, ASSEMBLER) and prod_rate(p_r) and qual_rate(q_r) and assembler_base_prod(b_p)

注意并没有直接在scallop代码中定义输入的数据,而是通过在和Python API中进行实现,方便一次编译,多次循环调用。

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
from scallopy import ScallopContext
from scallopy.collection import ScallopCollection
import time
import pandas as pd

# number of module slots of the "assembler" machine
possible_n_slots = [4, 5, 6, 8]
# all possible productivity module rates
possible_prod_rate = sorted(list({
0.04, 0.05, 0.06, 0.07, 0.10, # tier 1
0.06, 0.07, 0.09, 0.11, 0.15, # tier 2
0.10, 0.13, 0.16, 0.19, 0.25, # tier 3
}))
# all possible quality module rates
possible_qual_rate = sorted(list({
0.01, 0.013, 0.016, 0.019, 0.025, # tier 1
0.02, 0.026, 0.032, 0.038, 0.05, # tier 2
0.025, 0.032, 0.04, 0.047, 0.062, # tier 3
}))
# possible assembler base productivity 0 or 50%
possible_assembler_base_prod = [0.0, 0.5]
# number of quality modules will be 0..n_slots

# build the inputs
inputs = {
"n_slots": [], "prod_rate": [], "qual_rate": [], "n_qual": [], "assembler_base_prod": []
}
for n_slots in possible_n_slots:
for prod_rate in possible_prod_rate:
for qual_rate in possible_qual_rate:
for n_qual in range(n_slots + 1):
for assembler_base_prod in possible_assembler_base_prod:
inputs["n_slots"].append([(n_slots, 0)])
inputs["prod_rate"].append([(prod_rate,)])
inputs["qual_rate"].append([(qual_rate,)])
inputs["n_qual"].append([(n_qual, 0)])
inputs["assembler_base_prod"].append([(assembler_base_prod,)])

print("Total number of inputs: ", len(inputs["n_slots"]))

ctx = ScallopContext()
ctx.import_file("model.scl")
t0 = time.time()
ctx.compile()
print("Time: ", (t1 := time.time()) - t0)

result = [
ScallopCollection(ctx.provenance, coll)
for coll in ctx._internal.run_batch([["legendary"]] * len(inputs["n_slots"]), inputs, parallel=True)
]

output = [list(*each) for each in result]
print("Time: ", (t2 := time.time()) - t1)

# save to csv
output = [list(each[0]) for each in output]
df = pd.DataFrame(output)
df.to_csv("output.csv", index=False, header=["output", "n_slots", "n_qual", "prod_rate", "qual_rate", "assembler_base_prod"])

在代码中,预先定义了每一种组合(不同的产能插件和品质插件的组合,还有是否有50%自带产能),然后编译Scallop模型,把每一种可能性都放进去模拟,计算出传奇物品的总产量。

结果分析

通过以上计算的结果,画了一系列热力图,来展示不同的组合下,为了最大化传奇物品的产量,应该选择用多少产能插件和品质插件。

heatmap_slots_4_base_prod_0.0
Fig. 3. 最优的品质插件数量,4插槽组装机,无基础产能加成 (组装机3型)

heatmap_slots_5_base_prod_0.5
Fig. 4. 最优的品质插件数量,5插槽组装机,50%基础产能加成 (电磁工厂)

heatmap_slots_8_base_prod_0.0
Fig. 5. 最优的品质插件数量,8插槽组装机,无基础产能加成 (低温工厂)

结论

从上图可见,最大化传奇物品的产量,并不是直接堆质量插件即可,而是需要根据产能插件和品质插件提供的加成来灵活选择,主要是考虑产能插件的数值。具体的最优化选择可以以上面几张图作为参考。

  • [1] "Quality", Factorio Wiki, 2024. https://wiki.factorio.com/Quality.
  • [2] J. Huang et al., "Scallop: From Probabilistic Deductive Databases to Scalable Differentiable Reasoning", in Advances in Neural Information Processing Systems, Curran Associates, Inc., 2021, pp. 25134–25145. [Online]. Available: https://proceedings.neurips.cc/paper_files/paper/2021/hash/d367eef13f90793bd8121e2f675f0dc2-Abstract.html

异星工厂中的高品质产率分析

作者 ControlNet
2025年12月31日 21:22

异星工厂的品质扩展包给游戏带来了新的生产规划挑战,比起像以前只能横向扩张工厂的规模,现在可以通过使用高品质的工厂和插件,大幅增加产量。为了最大化如传奇品质的高品质物品的生产,我们需要对高品质产率的计算和规划进行一些分析。

高品质物品生产蓝图

一般来说生产高品质的物品有两种方法,生产出目标物品然后慢慢回收提升质量,或者是直接从源头生产高品质的原料,然后直接生产高品质的物品。

这里先初步的设计了两个蓝图,一个是用于电星蓝图的高品质原料生产,通过读取当前物流网络的信号,自动的将多余物品拿去回收,不断的生产高品质的原料。对于原料级别的物品(如铁板、铜板),比起直接放入回收机拿到同样的物品,将其放入到组装机里生产更高级别的物品,然后再回收回来,可以获得更高的品质。

例如下图里的:
blueprint1
Fig. 1. 回收原料生产高品质物品的例子.

这个蓝图设计成完全全自动的模式,能自动进行负载均衡,回收多余的物品。

回收机+组装机3:

1
0eNrVV1tv2zYU/i98XKnWki+NDWy/Ym+pIcgykxCRKJei3BmBAGdrkHZIU2xNt16yBtmwre2wG9AWa9LAP2aVpTztL+xQkmXZlmunAzYM8ANN8nzn9p1zqC3UsjzS4ZQJ1NhCbeKanHYEdRhqoPPDfvTD9tvT/eD1IDg6Ot+5Ex68DH96GA2eDPe/D0++DU8Gwa3fwtvPhi+3w7MvwtPD4OcH0Ysfr7FrLDi7N7z9LPj9fvjo5vDrV0H/4V9v9hLE8/6jaLAbHjxNIb7cC08HsEhwh7/effvH8+DO/WDnM5Cd0f04Ot6Ldp8Hnz+NXhxFZ2fBN7vR8fGf/U8RRtR0mIsaq1vIpevMsKRPzLAJOMOJ2TMtwpEP11ibfIIaqo8LLhquS+yWRdm6YhvmBmVEKeeENL+JEWGCCkoSVfGfns48uwXwDRVnSKagXaJ0uNOlbcIVc4O4AqzsOC5NQryFAFFZWblcxagHouXq5aovrZrC1MaYnIoNmwhqKqZjtygzhMOLMCs5RIzaFPxPzlUAgzAJ7lh6i2wYXQoAIDVG1uG4HaO58mCNclfoM3HqGBwWAvwqyfC4REpJUVcYkk0KBMLpEG6kbPoArHQ80fEWYflFESjjmUTOOl1XU6drky5DLKggdpIu2s4pvu4ZFqhRbKftWUTR4kQnV+Em0ynrgiEO7yWi438ACX6am6hRktYWnqhzT7S5J2W/6cNvNgCVLAAtz9pUKHMJF4VRyNGpNp38SgFyNUMeJW8Btcb4V2P8Ija5iUZ3ch3HMKk9DLyywIHp3QJG4FGaYJ853AbqSKW2vCNNbKAP4w1P0i4NelKrGIleR8J1KRdeLJfiJxRUPr4geKlUguw0Cylau3CR1ksTcfzXinTRtXzZXlm2bPGEDToj4obDN2PjOIGaE9wjGK1zQgB2zbBcMm3QrEx8LROSCMXd4Sp+Z/cu4HBtqkYg8rQz6ZOKRtt6AUX+9w1lZfmGUrlYQ6nnevV1D2beu2ZfeQq7iPUm5aZHhQ5BJbqzpuf4CR1jdJoVhE6Y0bIy/sTZje3Qs5YzpynJWM36o5Yyh9rAh3iULzV7ly7rFHaqpvP/Vpct8YnW9RGSr5WkfOe8iyZHb3FnU8d92fVaQKzYmwLHtWz+FqG8xytG+68b5ML54b//KwdScwM8kolZ1bCKwadyE8OyjIFycllNdtV4qeEa1tIVZEQua/I8uVrDFVyPN2FDlRfUJijIdCbpT92BlpV5Ejx+Mjx4BY/44N627GvtedNXyQIqfVo8NWMOpApTDuDRooHkKJ024vzBraD/BuXk8oaGpzej73aGhyfwMTD86vWsreo/sbWZ9GnZDrMvIowso0UsqfuXvvwiia0ECy5ltsCdLgQ3VlStafVKvV6tatVaWVN9/29ridSk

回收机:

1
0eNrFVt1u2zYUfhdeDlRnyVYSC1ifonepIcgS0xCRJZei3BmBAGdbkXZIW3RNsP6kDTJgW3/QrgPaYksa+GFWWfLVXmGHlCz/yXFbFJh9c8TD8/GcjzwfuY2abkjajHocGdvIIYHNaJtT30MGGh720t92Ppzejv/ux0dHw+u3kv23yYsHaf/J4PavyckvyUk/vvE6ufls8HYnObubnB7GL++nb36/7F324rN7g5vP4j8Pkoc/DH5+F/ce/Pt+L0Mc9h6m/d1k/2kO8dNectoHI8Md/HHnw1/P41sH8fXvIXZu7Ufp8V66+zz+8Wn65ig9O4sf76bHx//0vkMYUdv3AmSsb6OAXvEsV9TkWS0CxTBid22XMBTBNM8h3yJDjXDJRItRvtkinNqK7bea1LO4PxmlRQ2MiMcppyRbS350TS9sNQHfUHEBZXPaIUqb+R3qEKbYmyTgkGbbD2jG8TYCREVb1S/oGHXBVPXKBT0Sic2gavj8BMtQa5OYGDkUSMhmqAAHXHHmu2aTbFodChAQN8Y2we1IvEA4NigLuDlHVttiYHCorSIoCoiIWjrNbxNm5Yfsa8jcD3k7XI4+mYPpEX7NZ1syOUYcZHAWEoyuMEIAdsNyAzKb0HyMnFYECYQoKiO/+hnka/8T+aIFuCX6WVGnqf7qY6ku5aBWcDBaYRkDlelDXV50kLESTNvQVaMmFfvuQmKzoyWJY3Q1tFxIGsY9n7WgQrFoS8wRSRroGzkQCnZk9496GiPebQu4DmU8lHE5fsaUcunTwPUK/KIG/Muo1PGcKpXxt1awN31+YCcoJ62cEGdiF/MMlZbvhC5RNClb2VRRq0m9DqTis27OZfEFkLCl9hYyKhktJR51oUdb6KkKBholFKwUFDRDd0uhXkAYX8DDWBxr9dleqpVgr07QezUEyT1Xemuz6GWn1KbMDik3gVhi+hvmRFvBURx5i641iWc13UJhADTPxCzO8oLTLviar2itqMghdnaVnN961YWELRKfHHhGeSa/1j9WiKaa4iIS92UmOwuu5mntKW+Z+mcosPrJJHzp62+pqERfUqHVyniBsAkdKFFLmFmpj5VF7M41YEbszbqGVbyGqw0MFtgVrEmzhlelVQV3XbqruJpPrMkxVVpFyBoMgqk2ALzIO9v9nJI8ZzwyDCQlc0zR4OD1cPfO8P6NuPceiWLzQNC7Yk786Mlg/x28P+N7O+L15yy6D5RiNwUHy3VcsiKEU+hT8UDGyLWaBPYEJa964oEqlx8cnoCnA/VJeH1Fq9fqdV3X9JWqpkbRf5i4IY0=

另外一种模式是通过将低级的产物不断回收,不断循环直到生产出高品质的物品,如下所示。

blueprint2
Fig. 1. 回收产物生产高品质物品的例子.

回收机+组装机3:

1
0eNrtXN1u68YRfhdeFquA+ytSQPsSuTwwBFpa28ShSIWknLoHBpoALdCLtBdBi1wmFwVaNEFboEVQoK9jJ32Lzu5KpGST4g5VnZzUB77wLrkzOzM78+3sD/UmuMw2el2meR3M3gTposirYPbqTVCl13mSmWd5stLBLEiqSq8uszS/nqySxU2a6wkP7kmQ5kv982BG70kH0VVS1ZO6TPJqXZT15FJn9R4N66Qp9eJukelyryE/bFjfrU3DjzZJltZ3AdlRZvpa58ukvNsjFfcXJNB5ndapdprZyt0836wuoZMZJceEJcG6qIC2yE3HRhT5gSTBXTBT9AN5b+R6wo+N5RcCPxIsU9DfvRcd3HnD3YxUneT1ZFGsLtM8qYuyg/t0X1piiOqyyOaX+ia5TYECmlWuv+qwDIbajSwJrtKs1uXTp1tB1kkJBXg/CUGA3aDMgrwoVzBeptOVaWMEnAU/tQ82xt3o/QX8ddlQILVUZ9Ry62y3aVlvrDpbyZw7Tj7E6cx6dZZIv5mi/EYhuSsU9+kh92oN5gBTdvCNm5F6wnVnftNw2J5AXGzq9aaeA3QVpWuc6SuDLs+ki5C6R/26d/vWIi0Xm7SelzpZzm+SfDk3rUAK8KW63GjStNg9d01XxVIbl+iQOW5kvtxkrydpXumy06KCDuARDQ/V34Bnl9cluOOyxwAiPAynQwNs4yHNwfpd1qYtMlS11tlkcaOr+pjgYY/gzNsGYe+IUdbFmeM8QvBjBnlLHkEFehyZxzi6MOocyBaREoiwG8D4dHEUhwXvHYVuM7V8jR2WaYPQV2lZ1fNnM34fCGtISAwmALAXztZ20rD4Xaw14IYVI/hJ0OAGkncnZFOFHhJ5Umh546wQOJzd5DCuK2hwGtLSyDtkJTJkY7Sppyd5Pwu9VVFDGaEvIqoBRGTMe/yR82yZlPrEsWdYUI3eAVBlAo1x0cvCOCZ940AOZSIMDZfypEyETf3iTg5lIswb1iQyE2ExLmjku5CJcHRGKU/LRDjFRql8YZkIZ+ghOSkT4dx3JpLITESv08WJMxEX3uGKzEK4RJv5tCyEK29VhrIQ7ouGQ1kIj9DR+MLmTI5F9SOpUNfuWOgdfMg0sN1APS0CBfV1WzWUNAjvXQCFnHsF9wsINZQeCDF2y5f3MJRjGbIehgobsgYtdgx/yJD1pvvQeuj/JKbJgczzXNcfF+Vrq0ypl7u06brUGvq5SrJKP5X3OY1t1hAZDt3QIfZQWmdg9KI8Pk5x60k+6eC+hbZx79zryobU9tF8CZFguoSGeETs0KqdMlZ6mW5WE6tbCU64LjJ9bJ+4z6NjtEc3IMsOLRX9QHPQ5LyTkGxnic6juyMbsvzIKYpZSSxSI5QLA7tYAM7g5Wm7qjDnb7Vebc9Slnunezung/XFJtsdIbqm5tQFWN0CmwKmIHcM09TAocFwi9fBLDTadr6hvW9Y7xtuzmMuugxIcaFopoEzhGJVbMqFnoO04Ip7hmy27O7PFraSIcO23XbsCVvJsU6p3jvlgQEF0injt+qUdh/xjA4psQ45NI9IhXRI+R4lDw2ITFjk20VJu51wRofEJjZyECFjrEO+R8jDmw4h0iHfLkIe3FE6k1cqivXKIZhUDOmV6j1MHhqQI4dEDQGF8t7lbLcH5FMfj7oYj959ED2SKuSattkzFffDMWgtP6nSX0Di0dX5FL1QVPvd//8vFBV6P3e36SDemf1ceV4LxXuXHasqvdWTdVncpktdDt9ukr34t3fw5kwy13lymTXbRV2iTEP8pSiJPCo0uNzchdwC8KbST589l81717e9mSS9rhf63wTj+1g0vAc8xV5a6IfSbv7IreF2Ae3JX47lL3ymginy2mh758ZT/CmSv0LyR179bJeLnvzjsXde/PhH4Vj5vYY38g/YCBdXEfIWvKRIwyDjVoZI/mLsvQxP/si4lQzJ3//cGImYETJkJRIxI2TISiRiRvFY/l4hFSMjViIRM0Z+riKRiBljAxeJmDEfezTuyV+Mld9veP1vwyERM0ZOtQqJaDEybhUSkWNk3CokosXIuFVIRKahd2atkJBJQ2TQKoGVnY3twMvradhGbak/2sCKp3/l024HqMGVj7lkOC+u5ntrN0qG10VkJ0W7JOn5qMtsx3TpI7Dn0U00T/u+spHk2QeMHWy2lnm+bP7R73DRUJ2wSlZ9ZvX/5KGJyg7rnvOyEw0j/LpX+ax7aRiPPoDt89O9z8GWeuEG59j5YZ+8fVs9W6ZP9nn2a69Gb/scjNLPAvNlrxump58s+231dH+HSSn6trGYIo30Y//uibLR57C9fsnRVo9fmtXRH6uYKywvy0Ry7JcCL8dE6LugUr40E01Hn+r3wls0+ki2l2U8+kjRsoTZ82MYSzN3vuKEEpjF6AV5JQgjQhEGxQieAi7TbREA1xRhfqSE0ba8e26YMNcc4pAZhsKVOWGC8F1ZKduGWT6uU2ZpHR9mJADkEq7MCaeW1jQl3LWHR9BG2DbmNYHM2rQxr4lwMsAjaONkAIWo6Ze7MsiwVda0l3RX5kSGrhwRSSSQ78pmyWArMRFERZYptGXEpAvGVoaJkYi6MnByUhgmBCJI7somqbMVbqm5oxaG2skkjc7S2cgQEhk5AlM2U6+tKEstHLXRQjnJpdXCWdIQEkUdgdUCPNtWYkstLTUQgj2YpTCNieK2kS0bx7UVZimcX5gGxPiffWM6VK5DFW0tIlwFtAqdgqYBMcmnaTYNtxXrEsayjVXs/0ZLy8oayToSZXs62KfWBO6dsDW5rUlbU9vadE8T+38rPYRA8+slLofcffi0bAHjP7/67PEP/4K6WZh1/9rJpIE5z5+vMPG87Wobz2RXmAWs7fvx668ePv/ku39//vDNFw9f/vrhd18+/v5vwVFq2VJ/95s/P/7zk+//+u3j3z99+OVnlvTCrR/NWqX54R8SZMmlzoyuf/kC+vv+H396+PaPD7/9FN7cgmWsXlKxWMSxlEwqzuj9/X8BDicdEA==

回收机+电磁工厂:

1
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

回收机+铸造厂:

1
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

原理分析

如果是生产高品质的原料,比如说铜板,那么将铜板生产成铜线,就会考虑到底是使用产能插件好还是质量插件好。虽然质量插件提升了出现高品质物品的概率,但是产能插件大幅增加了产率,特别是高品质的高级产能插件,能提升相当大比例的产率,可能能生产出更多的高品质物品。

参考Figure 2,考虑以下的生产步骤:

flowchart LR    Input[(蓝箱输入)] --> Ass[组装机] --> |低品质| Rec[回收机]    Ass --> |高品质| Output[/红箱输出/]    Rec --> Ass

如果以上图表没有正确渲染,请刷新页面。

令每次的产物$X$都用百分比表示,其中1代表原料投入的数量,如果是0.1,则表示剩下了10%的物品。

$$ X = (x_{普通}, x_{罕见}, x_{稀有}, x_{史诗}, x_{传奇}) $$

而每次经过组装机或者回收机,则是将原料于一个转换矩阵$T$相乘,输出则是新的产物的数量。不过在生产出传奇物品之后,会将这部分收集起来,不参与矩阵乘法。

其中$T$可以表示为

$$
\begin{align*}
T &= \begin{pmatrix}
T_{普通到普通} & T_{普通到罕见} & T_{普通到稀有} & T_{普通到史诗} & T_{普通到传奇} \\
0 & T_{罕见到罕见} & T_{罕见到稀有} & T_{罕见到史诗} & T_{罕见到传奇} \\
0 & 0 & T_{稀有到稀有} & T_{稀有到史诗} & T_{稀有到传奇} \\
0 & 0 & 0 & T_{史诗到史诗} & T_{史诗到传奇} \\
0 & 0 & 0 & 0 & T_{传奇到传奇} \\
\end{pmatrix} \\
&= (1 + P)\begin{pmatrix}
Q_{普通到普通} & Q_{普通到罕见} & Q_{普通到稀有} & Q_{普通到史诗} & Q_{普通到传奇} \\
0 & Q_{罕见到罕见} & Q_{罕见到稀有} & Q_{罕见到史诗} & Q_{罕见到传奇} \\
0 & 0 & Q_{稀有到稀有} & Q_{稀有到史诗} & Q_{稀有到传奇} \\
0 & 0 & 0 & Q_{史诗到史诗} & Q_{史诗到传奇} \\
0 & 0 & 0 & 0 & Q_{传奇到传奇} \\
\end{pmatrix}
\end{align*}
$$
其中$P$是额外产率,比如说用了产能插件之后会增加,而对于回收机来说,应该取值$P=-0.75$。而矩阵中的$Q_{*}$是指在给定总共的质量加成$Q$的情况下,计算出的
每一个品级到另一个品级的转换率。这部分的计算可以参考官方wiki[1]

定义每次产物的的起始状态为

$$X_{0} = (1, 0, 0, 0, 0)$$

则之后的每一次的产物状态可以表示为

$$X_{t} = T_{回收机}T_{组装机}X_{t-1}$$

但是需要注意的是,每次进入到下个循环之前,需要移除掉传奇物品,然后将其加到最终的产物中。

代码实现

当流程确定之后,就可以通过Scallop[2]来实现这个过程。Scallop是一个用Rust实现的,基于符号推理的编程语言,可以用来解决这类问题。

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
type production_after_assembler(bound iter: i32, common: f32, uncommon: f32, rare: f32, epic: f32, legendary: f32)
type production_after_recycler(bound iter: i32, common: f32, uncommon: f32, rare: f32, epic: f32, legendary: f32)

type prod_rate(f32)
type qual_rate(f32)
type assembler_base_prod(f32)

type MachineType = ASSEMBLER | RECYCLER // 0: ASSEMBLER, 1: RECYCLER
type n_slots(f32, MachineType)
type n_qual(f32, MachineType)
type n_prod(f32, MachineType)
type base_prod(f32, MachineType)
type P(f32, MachineType)
type Q(f32, MachineType)
type T_self(f32, MachineType)
type T_c2l(f32, MachineType)
type T_c2e(f32, MachineType)
type T_c2r(f32, MachineType)
type T_c2u(f32, MachineType)
type T_u2l(f32, MachineType)
type T_u2e(f32, MachineType)
type T_u2r(f32, MachineType)
type T_r2l(f32, MachineType)
type T_r2e(f32, MachineType)
type T_e2l(f32, MachineType)

rel n_slots(4, RECYCLER)
rel base_prod(-0.75, RECYCLER) // only give 1/4 material back
rel base_prod(b_p, ASSEMBLER) = assembler_base_prod(b_p)
rel n_qual(4, RECYCLER)
rel n_prod(x, m) = n_qual(y, m) and x + y == n_s and n_slots(n_s, m)
// total production and quality rate for each machine
rel P(p_r * n_p + b_p, m) = n_prod(n_p, m) and base_prod(b_p, m) and prod_rate(p_r)
rel Q(q_r * n_q, m) = n_qual(n_q, m) and qual_rate(q_r)

// transform the quality to itself
rel T_self(1 - q, m) = Q(q, m)
// transform common to higher
rel T_c2l(q * 0.001, m) = Q(q, m)
rel T_c2e(x, m) = Q(q, m) and T_c2l(t, m) and q * 0.01 == x + t
rel T_c2r(x, m) = Q(q, m) and T_c2e(t1, m) and T_c2l(t2, m) and q * 0.1 == x + t1 + t2
rel T_c2u(x, m) = Q(q, m) and T_c2r(t1, m) and T_c2e(t2, m) and T_c2l(t3, m) and q == x + t1 + t2 + t3
// transform uncommon to higher
rel T_u2l(q * 0.01, m) = Q(q, m)
rel T_u2e(x, m) = Q(q, m) and T_u2l(t, m) and q * 0.1 == x + t
rel T_u2r(x, m) = Q(q, m) and T_u2e(t1, m) and T_u2l(t2, m) and q == x + t1 + t2
// transform rare to higher
rel T_r2l(q * 0.1, m) = Q(q, m)
rel T_r2e(x, m) = Q(q, m) and T_r2l(t, m) and q == x + t
// transform epic to higher
rel T_e2l(q, m) = Q(q, m)

rel production_after_recycler(0, 1.0, 0.0, 0.0, 0.0, 0.0) // initial state
rel production_after_assembler(
iter,
(1 + p) * (c * t_self),
(1 + p) * (c * t_c2u + u * t_self),
(1 + p) * (c * t_c2r + u * t_u2r + r * t_self),
(1 + p) * (c * t_c2e + u * t_u2e + r * t_r2e + e * t_self),
(1 + p) * (c * t_c2l + u * t_u2l + r * t_r2l + e * t_e2l) + l
) = production_after_recycler(iter, c, u, r, e, l) and T_self(t_self, m) and T_c2u(t_c2u, m)
and T_c2r(t_c2r, m) and T_c2e(t_c2e, m) and T_c2l(t_c2l, m)
and T_u2l(t_u2l, m) and T_u2e(t_u2e, m) and T_u2r(t_u2r, m)
and T_r2l(t_r2l, m) and T_r2e(t_r2e, m) and T_e2l(t_e2l, m)
and P(p, m) and m == ASSEMBLER and iter >= 0

rel production_after_recycler(
iter + 1,
(1 + p) * (c * t_self),
(1 + p) * (c * t_c2u + u * t_self),
(1 + p) * (c * t_c2r + u * t_u2r + r * t_self),
(1 + p) * (c * t_c2e + u * t_u2e + r * t_r2e + e * t_self),
(1 + p) * (c * t_c2l + u * t_u2l + r * t_r2l + e * t_e2l) + l
) = production_after_assembler(iter, c, u, r, e, l) and T_self(t_self, m) and T_c2u(t_c2u, m)
and T_c2r(t_c2r, m) and T_c2e(t_c2e, m) and T_c2l(t_c2l, m)
and T_u2l(t_u2l, m) and T_u2e(t_u2e, m) and T_u2r(t_u2r, m)
and T_r2l(t_r2l, m) and T_r2e(t_r2e, m) and T_e2l(t_e2l, m)
and P(p, m) and m == RECYCLER and iter >= 0

rel legendary(l, n_s, n_q, p_r, q_r, b_p) = production_after_recycler(20, c, u, r, e, l)
and n_slots(n_s, ASSEMBLER) and n_qual(n_q, ASSEMBLER) and prod_rate(p_r) and qual_rate(q_r) and assembler_base_prod(b_p)

注意并没有直接在scallop代码中定义输入的数据,而是通过在和Python API中进行实现,方便一次编译,多次循环调用。

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
from scallopy import ScallopContext
from scallopy.collection import ScallopCollection
import time
import pandas as pd

# number of module slots of the "assembler" machine
possible_n_slots = [4, 5, 6, 8]
# all possible productivity module rates
possible_prod_rate = sorted(list({
0.04, 0.05, 0.06, 0.07, 0.10, # tier 1
0.06, 0.07, 0.09, 0.11, 0.15, # tier 2
0.10, 0.13, 0.16, 0.19, 0.25, # tier 3
}))
# all possible quality module rates
possible_qual_rate = sorted(list({
0.01, 0.013, 0.016, 0.019, 0.025, # tier 1
0.02, 0.026, 0.032, 0.038, 0.05, # tier 2
0.025, 0.032, 0.04, 0.047, 0.062, # tier 3
}))
# possible assembler base productivity 0 or 50%
possible_assembler_base_prod = [0.0, 0.5]
# number of quality modules will be 0..n_slots

# build the inputs
inputs = {
"n_slots": [], "prod_rate": [], "qual_rate": [], "n_qual": [], "assembler_base_prod": []
}
for n_slots in possible_n_slots:
for prod_rate in possible_prod_rate:
for qual_rate in possible_qual_rate:
for n_qual in range(n_slots + 1):
for assembler_base_prod in possible_assembler_base_prod:
inputs["n_slots"].append([(n_slots, 0)])
inputs["prod_rate"].append([(prod_rate,)])
inputs["qual_rate"].append([(qual_rate,)])
inputs["n_qual"].append([(n_qual, 0)])
inputs["assembler_base_prod"].append([(assembler_base_prod,)])

print("Total number of inputs: ", len(inputs["n_slots"]))

ctx = ScallopContext()
ctx.import_file("model.scl")
t0 = time.time()
ctx.compile()
print("Time: ", (t1 := time.time()) - t0)

result = [
ScallopCollection(ctx.provenance, coll)
for coll in ctx._internal.run_batch([["legendary"]] * len(inputs["n_slots"]), inputs, parallel=True)
]

output = [list(*each) for each in result]
print("Time: ", (t2 := time.time()) - t1)

# save to csv
output = [list(each[0]) for each in output]
df = pd.DataFrame(output)
df.to_csv("output.csv", index=False, header=["output", "n_slots", "n_qual", "prod_rate", "qual_rate", "assembler_base_prod"])

在代码中,预先定义了每一种组合(不同的产能插件和品质插件的组合,还有是否有50%自带产能),然后编译Scallop模型,把每一种可能性都放进去模拟,计算出传奇物品的总产量。

结果分析

通过以上计算的结果,画了一系列热力图,来展示不同的组合下,为了最大化传奇物品的产量,应该选择用多少产能插件和品质插件。

heatmap_slots_4_base_prod_0.0
Fig. 3. 最优的品质插件数量,4插槽组装机,无基础产能加成 (组装机3型)

heatmap_slots_5_base_prod_0.5
Fig. 4. 最优的品质插件数量,5插槽组装机,50%基础产能加成 (电磁工厂)

heatmap_slots_8_base_prod_0.0
Fig. 5. 最优的品质插件数量,8插槽组装机,无基础产能加成 (低温工厂)

结论

从上图可见,最大化传奇物品的产量,并不是直接堆质量插件即可,而是需要根据产能插件和品质插件提供的加成来灵活选择,主要是考虑产能插件的数值。具体的最优化选择可以以上面几张图作为参考。

  • [1] "Quality", Factorio Wiki, 2024. https://wiki.factorio.com/Quality.
  • [2] J. Huang et al., "Scallop: From Probabilistic Deductive Databases to Scalable Differentiable Reasoning", in Advances in Neural Information Processing Systems, Curran Associates, Inc., 2021, pp. 25134–25145. [Online]. Available: https://proceedings.neurips.cc/paper_files/paper/2021/hash/d367eef13f90793bd8121e2f675f0dc2-Abstract.html

Python编程基础06:Python的面对对象编程

作者 ControlNet
2021年9月8日 13:17

人生苦短,我用Python![1]

这次我们来学习一下Python的面向对象编程(Object-Oriented Programming),也被简写为OOP。OOP是一种编程思想,可以让程序有更好的扩展性,可读性和可维护性。

面对对象编程

自打程序设计这一概念出现以来,人们就一直致力于研究出可靠又易于上手易于理解的编程语言以及不同的范式,人们所最熟知的C语言便是过程式编程(Procedural Programming)[2]的典范。这种编程范式曾经在很长的一段时间里解决了大部分人所面对的痛点——对着机器语言发愣。然而渐渐开始发现,C语言并非完美无缺,在各行各业软件愈发复杂的大前提下,C语言在很多时候会有许多不便之处,比如难以代码复用,功能拓展,项目维护,等等。为了解决这些问题,人们开始转而考虑另外一种编程范式:面向对象编程(Object Oriented Programming,简称OOP)[2]

在了解何为OOP之前,我们首先需要明白,C语言所谓的“过程式编程”究竟是什么,过程式编程的重点是过程,也就是“procedure”,这一词在计算机领域有“例程”的意思,所谓例程,就是一系列指令的集合所构成的一个可供执行的单元。而在C语言中,源代码便是由这些例程所组成的,一个C语言项目会使用变量函数(也就是例程)来描述“该怎么实现程序员的意图”,而它能做到的的唯一的代码复用,也就是将重复的代码拆出来放进新的函数里面,成为一个新的“例程”以供调用。C语言之所以选择了这种编程范式是因为他是最接近机器语言的一种编程范式,计算机的机器语言本质上就是由一条条二进制的指令所组成并交给CPU执行,就像是C语言中的一个个语句一样。

正如上一节中提到的函数封装和过程分解,可以发现,实际上之前介绍的Python语法和编程思想依然基于从C语言发展而来的过程式编程。下图明确的展示了这个编程范式的结构。

function_flow
Fig. 1. 过程式编程的执行流程.

当我们提到“人”的时候,脑海中浮现出来的究竟是什么?我相信对于大部分“人”来说,这个问题的答案都是非常模糊的,我们知道人,但是仅仅是作为一个概念性的东西存在,我们需要更加详细的信息,才能对“人”这个字建立起一个更加精确地形象,比如姓名,年龄,外貌,性别等等等等,换句话说,“人”这个名词本身就像是一个模板,而这个世界上的每一个“人”,都是派生自这个模子的一个个体,这些个体补充上了前面说到的那些缺失的信息,才能让这个个体作为一个货真价实的“人”存在于这个世界上。

如果你能明白上面这段话的意思,OOP的概念对于你而言就已经熟悉了一半,上面的“人”就是一个(class)。一个类可以拥有自己的属性和行为,但是并不能被直接拿来使用,需要以这个类为模板创造(实例化)出一个具体的东西,才能够把它拿来使用,这个东西就是对象(object)。一个类可以拥有属性(attribute),比如性别,年龄,也可以拥有行为,也被成为方法(method),比如吃饭,上学。OOP中的类通过对属性和行为的抽象,极强的提升了自身的表现力和抽象能力,一切东西都可以用类抽象出来。

OOP的三大要素

OOP有三大要素,分别是:封装(Encapsulation),继承(Inheritance),多态(Polymorphism)[3]。它们解决了过程式编程的一些不足。

封装

让我们接着拿人作为例子,我们知道人可以吃饭,但是鲜有人知道食物在人体中消化的每一个细节,我们知道人需要呼吸,却鲜有人知道肺结构的每一部分。

封装,指的就是只对外暴露自己必要的属性和行为,而隐藏具体逻辑。一个“人”类可能可以“吃饭”,但是使用这个类不需要知道食物是如何被具体消化的,这个具体逻辑是在类的内部不可见的。简单而言:对外隐藏具体实现细节,而仅仅暴露接口

继承

生物学中有”界门纲目科属种”的分类学[4],不同的物种会因为一些相似的特征而分到同一个分类中。而分类又是带层级的,真核生物里面又有动物,动物又分脊椎动物和无脊椎动物。鸟和鱼在生存环境和呼吸方式上不同,可是共同点是他们都需要摄入食物。男人和女人在身体构造上不同,可是共同点是他们都拥有名字。

这样的场景在程序开发中实在是过于常见了,比如同一个网站不同类型的账户,有的是普通账户,有的是会员账户,如果用过程式编程,那就只能硬加条件判断,并分别去写对应的逻辑,很混乱。但是OOP的继承概念就解决了这个问题。继承,指的就是一个类可以指定另外一个或者多个作为自己的父类,这样自己就可以使用父类的字段和方法,同时也可以拥有属于自己的额外属性和方法;凡是需要父类的的地方,都可以使用该父类的子类去替换,这就是里氏替换原则(Liskov Substitution Principle)[5]

oop-concept-inheritance
Fig. 2. OOP的继承. Adapted from [6]

如上图所示,父类是“Person”而子类是“Programmer”, “Dancer”和“Singer”,所以它们也构成了IS-A关系,例如“Programmer”是“Person”,所以允许里氏替换。因为“Person”都有“name”、“designation”属性和“learn”、“walk”、“eat”方法,而“Dancer”是“Person”的子类,所以“Dancer”也可以直接用“Person”的共通方法,实现了代码复用。同时,“Dancer”及其他子类也可以拥有自己的独特属性和方法,比如“groupName”属性和“dancing”方法。

多态

继承的出现引入了新的问题:对于同一个行为,不同的子类可能拥有不同的逻辑。比如鸟类和鱼类的呼吸方式,虽然鸟类和鱼类都需要呼吸,但是鸟类呼吸是通过肺,而鱼类是通过鳃。针对这一问题,OOP通过多态解决,也就是父类同一行为在不同子类上可能有着不同的实现。同样的例子,让父类“动物”提供一个方法“呼吸”,在子类“鸟”中我们重写(override)这个方法,在方法的实现中让鸟类使用肺呼吸;而在子类“鱼”中我们选择让鱼用鳃呼吸。如果需要“动物”对象,由于里氏替换原则的存在,需要“动物”的地方都可以用“动物”的子类替换,所以可以提供一个“鸟”,也可以提供一个“鱼”,由于多态的存在,虽然调用的方法都是“呼吸”,但是由于不同类型的实现不同,所以效果也会不同。

oop-concept-polymorphism
Fig. 3. OOP的多态的另一个例子. Adapted from [6]

Python的对象

Python支持很多类型的数据,比如说1234(int),3.1415(float),"Python"(str),[1,2,3](list),等等。这里的每一个数据都是一个对象(object)。

每一个对象都有:

  • 一个类型(type)
  • 一个内部数据表示(原始,复合)
  • 一组和对象进行交互的方法

每一个对象都是一个类的实例(instance)。比如说:

  • 1234是一个int类型的实例
  • "Python"是一个str类型的实例

在Python中,一切皆对象。所以在Python中,

  • 对于某个类,可以创建新的对象
  • 可以操作对象
  • 可以销毁对象。比如说使用del关键字,或者直接不管它,Python会回收已销毁或者不可访问的对象,这也被称为垃圾回收(garbage collection)。

Python的类

Python中的类(class)是为了表示程序中的某一种概念而设计的,也可以被认为是定义了一种用于创建对象的模板/蓝图。在一个简单或复杂的程序中,我们需要创建很多类,每个类都有自己的职责和特性。

类的实例(instance)是从这个类中创建的对象。实例可以被赋值于一个变量,这样就可以通过这个变量来访问这个实例的内部值和相关的方法。

对于每个类,我们都会定义一些”实例变量”(instance variables) - 存储在一个实例中的内部值,和一些”方法”(methods) - 操作实例的函数。

创建一个类

创建一个类需要先指定类名,在Python中通过关键字class来定义类。比如说创建一个Point类,格式是这样的:

1
2
class Point:
# define attributes here

这里在关键字class后就跟一个想要定义的类名Point,然后跟着一个冒号,接下来缩进并且定义类的内容。

注意,在Python中,类名是采用大写驼峰命名法,例如Point,CapWord,UpperCamelCase

构造器

每一个Python中的类需要一个构造器(constructor)__init__(其中双下划线在两边)用于创建新的实例。

  • 构造器在类中是非常重要的一部分
  • 主要用于初始化实例的属性
  • 通过类名进行调用

比如说在这个Point类的例子中,我们需要一个xy分别代表一个点在平面直角坐标系的两个坐标。

1
2
3
4
class Point:
def __init__(self, x, y):
self.x = x
self.y = y

在以上例子中,__init__右边的self是代表这个实例本身,而xy是构造器的参数。然后下面的self.xself.y则是实例的属性,我们需要把传入的xy值赋值给这两个属性。这样一个Point实例就会带上两个属性xy了。

1
2
3
p = Point(1, 2)
print(p.x) # 1
print(p.y) # 2

这样我们就可以通过Point(...)和对应的xy值来创建一个新的Point实例。然后通过p.x就可以访问x属性,就是在类定义中self.x所对应的值。

定义方法

类定义中的函数我们一般称为方法(method)。它就像一个函数一样,但是只能用在类中。

1
2
3
4
5
6
7
8
9
10
class Point:
def __init__(self, x, y):
self.x = x
self.y = y

def distance(self, other):
dx = self.x - other.x
dy = self.y - other.y
distance = (dx**2 + dy**2) ** 0.5
return distance

上面这个例子中,我们定义了一个distance方法,它用于计算两个Point实例之间的距离。其中参数中的self代表的是这个实例本身,在使用的时候就不用再指定了。other则是另一个Point实例。

举个例子,如果要使用这个distance方法,我们可以这样写:

1
2
3
p = Point(3, 4)
origin = Point(0, 0)
print(p.distance(origin)) # 5

从上面的代码可以看到,distance中只需要传入一个参数other,而self则已经分配给了p了(写在这个方法名之前)。

但是我们还有另外一种调用方法的手段。对于同样的例子,可以这么写。

1
2
3
p = Point(3, 4)
origin = Point(0, 0)
print(Point.distance(p, origin)) # 5

在这个例子中,方法distance之前用的是Point类,而不是一个实例p,这样就可以完整的指定两个Point实例来计算了。

魔术方法

如果直接使用print来打印一个对象,会怎么样?

1
2
p = Point(3, 4)
print(p) # <__main__.Point object at 0x10c8b9e58>

会发现打印的结果是包含了一个类名和所在的内存地址,对于人类来说,这个结果不是很有用。于是我们可以定义一个__str__方法,来改变打印的结果。Python会自动使用__str__方法的返回值作为打印的结果。

这样一来,Point类就是这样定义的:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Point:
def __init__(self, x, y):
self.x = x
self.y = y

def distance(self, other):
dx = self.x - other.x
dy = self.y - other.y
distance = (dx**2 + dy**2) ** 0.5
return distance

def __str__(self):
return "(" + str(self.x) + "," + str(self.y) + ")"

这时候再print一个Point实例,就会变成这样:

1
2
p = Point(1, 2)
print(p) # (1,2)

就像这个例子中的__str__方法一样,Python中还有别的魔术方法可以用于一些特殊的目的。

比如说:

  • __add__(self, other) -> self + other
  • __sub__(self, other) -> self - other
  • __eq__(self, other) -> self == other
  • __lt__(self, other) -> self < other
  • __len__(self) -> len(self)
  • __str__(self) -> print(self)

还有很多,可以参考Python的文档[7]

self

类中的self参数,在方法中表示的是当前的实例本身。所以如果用一个实例来调用方法的话,就不需要再去指定self了。假设我们在point.py这个文件里定义了Point类,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Point:
def __init__(self, x=0, y=0):
self.x = x
self.y = y

def get_x(self):
return self.x

def get_y(self):
return self.y

def set_x(self, x):
self.x = x

def set_y(self, y):
self.y = y

def distance(self, other):
dx = self.x - other.x
dy = self.y - other.y
distance = (dx**2 + dy**2) ** 0.5
return distance

这样我们就可以在别的文件里调用这个Point类,如下所示:

1
2
3
4
5
6
7
from point import Point
point1 = Point()
point2 = Point(1, 2)
print(point1.get_x()) # 0
print(point1.get_y()) # 0
print(point2.get_x()) # 1
print(point2.get_y()) # 2

从这里可以看出,Point类的方法中的self参数,就是当前的实例本身。

继承和多态

正如上文中提到的OOP的三大要素,Python中也可以轻松使用继承和多态。

Python中的继承通过在类名后面加上一个括号来实现。举个例子,一个平面直角坐标系上的点实际上可以当成是一个广义上二维向量的特殊形式,这里的Point类可以当成是一个Vector类的子类。

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
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y

def get_x(self):
return self.x

def get_y(self):
return self.y

def set_x(self, x):
self.x = x

def set_y(self, y):
self.y = y

def __str__(self):
return "(" + str(self.x) + " " + str(self.y) + ")"

class Point(Vector):
def distance(self, other):
dx = self.x - other.x
dy = self.y - other.y
distance = (dx**2 + dy**2) ** 0.5
return distance
def __str__(self):
return "<" + str(self.x) + "," + str(self.y) + ">"

这样一来,Point类继承了Vector类,并且复用了xy属性,还有一堆getter和setter方法。而且,Point类还有一个distance方法,这是子类特有的。

而且,VectorPoint类都有__str__方法,但是它们的实现不一样,这样一来,在同样打印这些对象的时候,将会有不同的结果,这就是多态的体现。

1
2
print(Vector(1, 2))  # (1 2)
print(Point(1, 2)) # <1,2>

变量的作用域

作用域(scope)的相关概念在Python中是非常重要的。这里介绍其中的主要概念。

  • 作用域(scope)是指在一个程序中变量的访问范围。
  • 生命周期(lifecycle)是指变量在程序执行中的存在时间。
  • 全局变量(global variable)
    • 是指在整个程序中都可以访问的变量。
    • 只有在程序结束之后才会被销毁。
  • 局部变量(local variable)
    • 是指在一个函数中可以访问的变量。
    • 在函数执行结束之后,这个变量就会被销毁。
1
2
3
4
5
6
7
8
9
def f(x):
# ----local scope----
x = x + 1
print("f(x): x =", x)
return x
# ----local scope----

x = 3 # global scope
z = f(x) # global scope

举个例子来说的话,上面函数f中缩进的那一部分内容都是一个局部作用域,f(x)中的x是一个局部变量。而函数之外的顶层代码,比如x = 3z = f(x)中的xz都是全局变量。函数内的x和函数外的x是不一样的两个变量,需要分清楚。

类中的作用域

而在class中,作用域就更加复杂了。

  • 实例变量(instance variable)
    • 和单个实例相关,并且在那个实例中是唯一的。
    • 在类的内部是局部的,不能被外部直接访问(必须有实例才能访问)。
  • 类变量(class variable)
    • 在类的顶层定义的变量,和类相关,和实例无关。
    • 一定程度上是全局的,只要通过类即可直接访问(也可以通过实例访问)。
1
2
3
4
5
6
7
class Point:
counts = 0 # class variable

def __init__(self, x=0, y=0):
# x, y are local variables
self.x = x # self.x is an instance variable
self.y = y # self.y is an instance variable

访问这些变量:

1
2
3
4
5
print(Point.count)  # 0
print(Point.x) # error
p = Point(1, 2)
print(p.x) # 1
print(p.counts) # 0

实践试试

对于刚接触OOP的同学来说,这可能是非常困难的,就算是作者也花了起码半年的时间才慢慢理解,但是我们可以通过下面的例子先来试试。这里提供了两个部分的实践,一个是继续探索并且拓展正文中提到的Point类,另一个则是写一个井字棋游戏。

探索Point类

首先来看看正文中提到的Point类。

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
class Point:

def __init__(self, x, y):
self.x = x
self.y = y

def get_x(self):
return self.x

def get_y(self):
return self.y

def set_x(self, x):
self.x = x

def set_y(self, y):
self.y = y

def distance(self, other):
dx = self.x - other.x
dy = self.y - other.y
distance = (dx**2 + dy**2) ** 0.5
return distance

def __eq__(self, object):
return self.x == object.x and self.y == object.y

def __str__(self):
return str(self.x) + " , " + str(self.y)

可以先试试判断一下各个变量所处的定义域。

  • 类变量: 没有
  • 实例变量: self.x, self.y
  • 局部变量: dx, dy, distance

如果我们需要计算某个点到原点的距离,可以添加以下方法distance_to_origin

1
2
def distance_to_origin(self):
return self.distance(Point(0, 0))

这里我们使用了distance方法,这样就可以通过复用方法来让代码变得更加清晰简洁。

如果假设以原点为圆心,经过这个点画一个圆,计算这个圆的面积,就可以添加以下方法area_of_circle来实现。

1
2
3
import math
def area_of_circle(self):
return math.pi * self.distance_to_origin() ** 2

如果我们创建了很多个Point,想要一直追踪创建的Point的数量,可以通过添加一个类变量count来记录。

1
2
3
4
5
6
7
8
9
10
class Point:
count = 0

def __init__(self, x, y):
self.x = x
self.y = y
Point.count += 1

def get_count(self):
return Point.count

以上画的点是基于平面直角坐标系的,如果我们想要一个三维立体坐标的点,也可以定义一个类似的类Point3D来实现。

当然了,2D的Point中可以画圆来计算圆的面积,现在Point3D可以通过画球来计算球的体积。

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
class Point3D:
count = 0

def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
Point3D.count += 1

def get_count(self):
return Point3D.count

def distance(self, other):
dx = self.x - other.x
dy = self.y - other.y
dz = self.z - other.z
distance = (dx**2 + dy**2 + dz**2) ** 0.5
return distance

def distance_to_origin(self):
return self.distance(Point3D(0, 0, 0))

def volumn_of_sphere(self):
return 4/3 * math.pi * self.distance_to_origin() ** 3

def __str__(self):
return str(self.x) + " , " + str(self.y) + " , " + str(self.z)

def __eq__(self, object):
return self.x == object.x and self.y == object.y and self.z == object.z

如果更进一步,我们想要一个类PointND,它能代表任意维度的点,比如三维的点,或者五维的点,其实也可以通过非常类似的方式来实现。当然这里计算的就不是球的体积而是超球体的体积了。

其中超球体的体积公式[8]

$$V_{n}(R)={\frac {\pi ^{n/2}}{\Gamma \left({\frac {n}{2}}+1\right)}}R^{n}$$

其中$\Gamma(n)$代表的是Gamma函数[9],如果$n$是正整数,则$\Gamma(n)=(n-1)!$。而Gamma函数将$n$的定义域拓展至所有非负整数的复数域上,这样就可以计算任意维度的超球体的体积。当然我们不用管那么多,Python自带的math库就有一个函数gamma可以直接计算Gamma函数。

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
import math

class PointND:
count = 0
def __init__(self, *xs):
self.xs = xs
self.n = len(xs) # number of dimensions

def get_count(self):
return PointND.count

def distance(self, other):
assert self.n == other.n # dimensions of 2 points should be the same
distance_sq = 0 # sum up squares of differences for each dimension
for i in range(self.n):
dx = self.xs[i] - other.xs[i]
distance_sq += dx ** 2
distance = distance_sq ** 0.5
return distance

def distance_to_origin(self):
return self.distance(PointND(*([0] * self.n)))

def volumn_of_hypersphere(self):
r = self.distance_to_origin()
return math.pi ** (self.n / 2) / math.gamma((self.n / 2) + 1) * r ** self.n

def __str__(self):
return " , ".join(str(x) for x in self.xs)

def __eq__(self, object):
return self.xs == object.xs

井字棋游戏

井字棋(Tic-Tac-Toe)是一个两个玩家之间对战的游戏,游戏在一个3*3的棋盘上进行,每个棋子可以是X或O。如果一个玩家在棋盘上的某一行,某一列或者某一斜线上放置三个相同的棋子,则该玩家获胜。关于井字棋的更多信息可以参考Wiki[10]

在这个部分,我们将试试开发一个简单的井字棋游戏,游戏将在两个人类玩家之间进行。这个意思是在这个部分不需要写一个复杂的AI代码。

对于棋盘的表示,我们可以用一个二维列表来实现。然后对于将要定义的Game类,则需要以下这些方法:

  • start_game: 开始游戏
  • take_input: 接受玩家输入
  • check_for_win: 检查是否有玩家获胜
  • print_game: 打印棋盘状态

来试试看实现一下这个代码吧。

以下代码提供了其中一种解决思路。

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
class Game:

def __init__(self):

self.board = [['-','-','-'],['-','-','-'],['-','-','-']]
self.player_turn = 1
self.total_turns = 0

def start_game(self):
flag = True
while flag:
self.print_board()
self.take_input()
t = self.check_for_win()
if t == True:
self.print_board()
flag = False

def take_input(self):
x = int(input("Enter value for x. Value can be between 0, 1, 2: "))
y = int(input("Enter value for y. Value can be between 0, 1, 2: "))

if x >= 3 or y >= 3 or x < 0 or y < 0:
print("Invalid point!")
elif self.board[x][y] != '-':
print("Already occupied.")
else:
if self.player_turn == 1:
self.board[x][y] = 'x'
self.player_turn = 2
self.total_turns += 1
else:
self.board[x][y] = 'o'
self.player_turn = 1
self.total_turns += 1

def check_for_win(self):
win = False
if self.board[0][0] == 'x' and self.board[0][1] == 'x' and self.board[0][2] == 'x' or \
self.board[1][0] == 'x' and self.board[1][1] == 'x' and self.board[1][2] == 'x' or \
self.board[2][0] == 'x' and self.board[2][1] == 'x' and self.board[2][2] == 'x':
print("player 1 wins!")
win = True
elif self.board[0][0] == 'x' and self.board[1][0] == 'x' and self.board[2][0] == 'x' or \
self.board[0][1] == 'x' and self.board[1][1] == 'x' and self.board[2][1] == 'x' or \
self.board[0][2] == 'x' and self.board[1][2] == 'x' and self.board[2][2] == 'x':
print("player 1 wins!")
win = True
elif self.board[0][0] == 'x' and self.board[1][1] == 'x' and self.board[2][2] == 'x' or \
self.board[0][2] == 'x' and self.board[1][1] == 'x' and self.board[2][0] == 'x':
print("player 1 wins!")
win = True
elif self.board[0][0] == 'o' and self.board[0][1] == 'o' and self.board[0][2] == 'o' or \
self.board[1][0] == 'o' and self.board[1][1] == 'o' and self.board[1][2] == 'o' or \
self.board[2][0] == 'o' and self.board[2][1] == 'o' and self.board[2][2] == 'o':
print("player 2 wins!")
win = True
elif self.board[0][0] == 'o' and self.board[1][0] == 'o' and self.board[2][0] == 'o' or \
self.board[0][1] == 'o' and self.board[1][1] == 'o' and self.board[2][1] == 'o' or \
self.board[0][2] == 'o' and self.board[1][2] == 'o' and self.board[2][2] == 'o':
print("player 2 wins!")
win = True
elif self.board[0][0] == 'o' and self.board[1][1] == 'o' and self.board[2][2] == 'o' or \
self.board[0][2] == 'o' and self.board[1][1] == 'o' and self.board[2][0] == 'o':
print("player 2 wins!")
win = True
elif self.total_turns == 9:
print("It is a draw.")
win = True
return win

def print_board(self):
print(self.board[0])
print(self.board[1])
print(self.board[2])

参考文献

  • [1] B. Eckel, “sebsauvage.net - Python”, Sebsauvage.net, 2021. [Online]. Available: http://sebsauvage.net/python/.
  • [2] "paradigms", Cs.lmu.edu, 2021. [Online]. Available: https://cs.lmu.edu/~ray/notes/paradigms/.
  • [3] "Object-Oriented Principles", D.umn.edu, 2021. [Online]. Available: https://www.d.umn.edu/~gshute/softeng/presentations/oo-principles.xhtml.
  • [4] "What is Taxonomy?", Cbd.int, 2021. [Online]. Available: https://www.cbd.int/gti/taxonomy.shtml.
  • [5] B. Liskov and J. Wing, "A behavioral notion of subtyping", ACM Transactions on Programming Languages and Systems, vol. 16, no. 6, pp. 1811-1841, 1994. Available: 10.1145/197320.197383.
  • [6] "Java Tutorials - OOP Concepts | Encapsulation | Abstraction | Inheritance | Polymorphism", Btechsmartclass.com, 2021. [Online]. Available: http://www.btechsmartclass.com/java/java-oop-concepts.html.
  • [7] "3. Data model — Python 3.9.7 documentation", Docs.python.org, 2021. [Online]. Available: https://docs.python.org/3/reference/datamodel.html#basic-customization.
  • [8] "DLMF: 5.19 Mathematical Applications", Dlmf.nist.gov, 2021. [Online]. Available: https://dlmf.nist.gov/5.19#E4.
  • [9] P. Davis, "Leonhard Euler's Integral: A Historical Profile of the Gamma Function", The American Mathematical Monthly, vol. 66, no. 10, pp. 849-869, 1959. Available: 10.1080/00029890.1959.11989422.
  • [10] "Tic-tac-toe - Wikipedia", En.wikipedia.org, 2021. [Online]. Available: https://en.wikipedia.org/wiki/Tic-tac-toe.

Windows最佳动画观看环境配置指南(MPC-HC, madvr, SVP, Anime4K)

作者 ControlNet
2021年7月31日 23:09

在PC上最好的动画观看体验需要复杂的环境配置和硬件支持,本文介绍了这一套方案同时有极佳的上采样和去噪的画质,也有插帧带来流畅的拉镜头效果。如果你曾经只在线观看过动画的,请一定要试试。本文介绍的这一套方案使用MPC-HC播放器[1],madvr滤镜[2],SVP插件[3]和Anime4K着色器[10]

有人可能会说,网上随便下载的高压缩流媒体源的动画根本没必要用那么复杂的设置,其实正好相反,madvr和SVP的优势是把这些高压缩的充满噪点、色带和卡顿的画面进行处理,让视频的观感大幅提升。在提升幅度上是远远超出在蓝光原盘中使用madvr渲染的。所以画质越差,分辨率越低(比如720p),它的提升幅度越大。当然,能有高画质源当然是要选更好的。在本文的最后,将会有画质对比。

软件准备

首先先下载以下软件作为准备。

软件安装

madvr

同样可以从官网安装,也可以使用包管理器。

如果是从官网上安装,会相对来说麻烦一点,把madvr文件夹解压到你想保存的地方,并且运行install.bat注册madvr渲染器。然后这个文件夹不能删,它就是这个软件的自身。

SVP

SVP是一款补帧软件,可以把24帧的动画补帧到你想要的任何帧数(受限于硬件),对于动画而言,最重要的是可以把拉镜头的各种衔接变得流畅,而动作部分由于动画的制作流程问题(原画帧数极低),依然能保持原有的风味。最大的问题是果冻效应和鬼影,不过可以根据自己对比进行取舍。

SVP是一款商业软件,请大家支持正版。安装过程中会自动安装需要的ffdshow分离器解码器和MPC-HC播放器,所以基本上是非常方便了。

在第一步选择中,请只选择32位的(第一项),64位好像怎么测试都无法正常运行。

svp-1
Fig. 1. SVP安装第一步

第二步的时候,记得勾选第一项安装MPC-HC(32位)播放器,如果上步没有安装madvr也可以在这里安装。

svp-2
Fig. 2. SVP安装第二步

第三步,根据自己的勾选即可。MPC-HC是默认使用LAV音视频解码器[5],所以就不用再配置LAV了。如果使用potplayer这种,还需要自己配置LAV Filter。

svp-3
Fig. 3. SVP安装第三步

剩下的就等安装完毕了。

XySubFilter

在使用madvr和SVP渲染之后,使用XySubFilter作为字幕渲染器可以更好的还原字幕的显示效果。

在GitHubhttps://github.com/Cyberbeing/xy-VSFilter[4]中的Release下载解压,并且管理员运行Install_XySubFilter.bat即可。

MPC-HC配置

启动MPC-HC,打开设置,进入”回放 - 输出”,按照下图红框的指示,配置渲染器为madvr,字幕渲染器为XySubFilter。

mpchc-1
Fig. 4. MPC-HC配置madvr和XySubFilter

第二步进入到设置中的”扩展滤镜”中,选择添加滤镜,找到”ffdshow raw video filter”,如果没有找到说明其中一个并不是安装的64位版本,需要重新安装32位的MPCHC和SVP。然后根据红框位置,要设置为”首选”。只有分离器使用ffdshow,才能使用SVP。

mpchc-2
Fig. 5. MPC-HC配置ffdshow / SVP

然后可以测试一下,随便打开一个动画,注意观察任务栏右下角的图标,看看有没有出现”ffdshow”和”madvr”的图标,如下图红框中的两个所示。

runtime-icon
Fig. 6. 运行成功时右下角的图标

如果成功了,说明MPC-HC已经成功调用madvr和SVP了,这是一个好的开始,下一步就是配置这些工具的设置。

LAV解码器配置

在这一步参考了这篇攻略[6]

首先需要打开MPC-HC设置中的”内部滤镜”中的这几个,先从”音频解码器”开始。

lav-1
Fig. 7. LAV解码器设置的位置

音频解码器中进入”Mixing”页面,勾选”Enable Mixing”,这个设置可以将一些5.1,7.1这种多声道的音频混合输出到目前使用的声道,这样不会丢弃一些没有的声道的信息。下图是一个对于双声道的设置例子。如果是用单声道的,请不要勾选”Don’t mix Stereo sources”不然会丢失信息。需要按照自己的情况进行选择,如果是双声道的耳机/音响就选”Stereo”,如果是单声道的音响就选”Mono”,如果你用的已经是5.1甚至7.1声道的设备了,忽略本段,不使用”Mixer”设置。

lav-2
Fig. 8. LAV音频解码器的设置

下一步进入视频解码器设置,基本上保持默认即可。只要确定红框位置正确即可,硬件解码器如果有的话建议选择”DXD11(copy-back)”,没有的话就”DXVA2(copy-back)”,copy-back是无损的,并且可以允许后续处理。硬件选择是”Automatic”即可。

lav-3
Fig. 9. LAV视频解码器的设置

madvr配置

这一部分的设置可以参考这篇文章[7]

打开一个视频播放,右下角应该会出现madvr的图标,双击即可打开设置。

注意:madvr设置和SVP设置极大受到硬件性能影响,请根据CPU、GPU、内存的性能酌情调整。这里参考的配置是针对RTX3080Ti + i7 8700K + 32G 3200的硬件考虑的。

设备部分配置

首先需要进入下图所示的区域,在需要播放的显示器设备中,选择”properties”把RGB level设置为”PC levels (0-255)”。

madvr-1
madvr-2
Fig. 10. madvr设置显示器配置

去压缩效果

在面对高压缩的流媒体动画视频时,这个部分起到的观感效果极佳,下图是设置。

madvr-3
Fig. 11. madvr设置去压缩效果

compare-1
Fig. 12. 去压缩效果对比, Nekopara第三话. Adapted from [8]

上图可以看到效果开关的对比。关闭时,因为压缩造成的噪点在相对颜色单一的墙砖上清晰可见,这种在流媒体动画视频中很常见,开启就可以去噪。

锐化处理

在”image enhancements”页面,可以对线条和边缘的锐化设置进行调整,个人觉得意义不大,特别是”thin edges”它会将像素整个进行偏移填充,让画面中物体的结构比例都发生变化了,可以自行测试,个人不喜欢。而”crispen edges”会造成生硬的锐化,反而让观感变差了。所以只打开了”sharpen edges”和”enhance detail”。

madvr-4
Fig. 13. madvr设置锐化处理

这个页面带来的观感变化其实不大,故对比略去。

上采样处理

这一部分极为重要,它可以使用有效的实时上采样算法,将低分辨率的视频更好的展示在高分辨率的显示设备下。现在2K,4K分辨率的普及率很高,而动画主要还是1080P的,所以起到的效果很好。

首先是色度上采样,如下图所示。

madvr-5
Fig. 14. madvr色度上采样设置. Adapted from [7]

这里个人的选择是”NGU + Anti-Alias, very high quality”。低配电脑可以选择”super-xbr”。

然后是下采样,也就是高分辨率视频在低分辨率下播放时使用的算法。

madvr-6
Fig. 15. madvr下采样设置. Adapted from [7]

这里选择了和上图一样的配置,”SSIM + 1D 100% + scale in linear light”,感觉下采样一般用的不多无所谓。

然后图像上采样,这个就是所谓的把低分辨率提升到高分辨率的算法。

madvr-7
Fig. 16. madvr图像上采样设置. Adapted from [7]

这里个人的选择是”NGU Sharp + high + 剩下默认”,配置低一些的话可以使用”Jinc”或者”super-xbr”。

至此,基本上madvr的配置就差不多了,剩下的部分其实不是很重要,当然也可以自己去按需修改。

这里来一个简单的对比。

compare-2
Fig. 17. madvr开关对比, 赛马娘第二季第五话. Adapted from [9]

可以看到线条的清晰度有一个明显的改观,这样的提升不仅仅发生在局部,而是一个整体的观感提升。

以上配置在这台3080Ti的GPU下,从1080p升采样到4K,播放时GPU的占用大约为29%。不能把GPU吃的太满,还要为后面的SVP插帧预留性能。

SVP设置

首先需要应用4GB补丁,右击SVP任务栏图标,如下图所示。然后再选择MPC-HC软件的位置即可。

svp-4
Fig. 18. SVP应用4GB补丁.

双击打开SVP界面,如下图所示。手动选择你想要的帧数,一般感觉60就够了。优化选择”动画”。伪影去除和性能质量选择可以自己调整。

svp-5
Fig. 19. SVP设置.

然后再是禁止SVP使用黑边填充,因为这个东西在有些视频播放时,会突然卡住,然后切换过去,然后就没字幕了等等,总而言之特别的不好用,建议直接关闭。如下图所示。

svp-6
Fig. 20. SVP禁止黑边填充.

Anime4k配置

根据教程[10],首先将下载的Anime4K着色器HLSL文件解压到”C:\Users\<username>\AppData\Roaming\MPC-HC\Shaders”下。再打开MPC-HC的设置,进入”回放 - 着色器”,点击”添加着色器文件”,将这些HLSL文件全部添加。如下图所示。

anime4k-1
Fig. 21. Anime4k添加着色器.

再根据上图红框位置,将对应的着色器添加到”重绘尺寸后”。注意顺序很重要!如果显示器画面是1080p及以下,需要把”Anime4K_PushGrad”换成”Anime4K_PushGrad_Weak”。这样设置就完成了。在madvr的加持下,感觉提升幅度主要在线条锐化上,提升幅度有但是不是很大,还好Anime4K本身并不是很吃资源,所以还不错。

在以上设置下,GPU占用大约为70%,不丢帧(丢帧情况可以在MPC-HC中按Ctrl+4查看)。已经是极限了,如果再调高一点madvr的设置就会丢帧。

参考文献

  • [1] "MPC-BE", SourceForge, 2021. [Online]. Available: https://sourceforge.net/projects/mpcbe/.
  • [2] "madVR", Madvr.com, 2021. [Online]. Available: http://madvr.com/.
  • [3] "SVP – SmoothVideo Project – Real Time Video Frame Rate Conversion", Svp-team.com, 2021. [Online]. Available: https://www.svp-team.com/.
  • [4] "GitHub - Cyberbeing/xy-VSFilter: Official xy-VSFilter Repository", GitHub, 2021. [Online]. Available: https://github.com/Cyberbeing/xy-VSFilter.
  • [5] "GitHub - Nevcairiel/LAVFilters: LAV Filters - Open-Source DirectShow Media Splitter and Decoders", GitHub, 2021. [Online]. Available: https://github.com/Nevcairiel/LAVFilters.
  • [6] "​小科普 | 让画质更上一层楼!配置mad VR和LAV Filters(上)_服务软件_什么值得买", Post.smzdm.com, 2021. [Online]. Available: https://post.smzdm.com/p/apz39zr2/.
  • [7] "小科普 | 让画质更上一层楼!配置madVR和LAV Filters(下)_服务软件_什么值得买", Post.smzdm.com, 2021. [Online]. Available: https://post.smzdm.com/p/awx02qr2/.
  • [8] Nekopara Project, "NEKOPARA TVanime Official HP", NEKOPARA TVanime Official HP, 2021. [Online]. Available: https://nekopara-anime.com/en/.
  • [9] "TVアニメ『ウマ娘 プリティーダービー Season 2』公式サイト", TVアニメ『ウマ娘 プリティーダービー Season 2』公式サイト, 2021. [Online]. Available: https://anime-umamusume.jp/.
  • [10] "Anime4K", Anime4K, 2021. [Online]. Available: https://bloc97.github.io/Anime4K/.

计算机视觉中的Transformer续

作者 ControlNet
2021年7月27日 04:31

上一篇文章《计算机视觉中的Transformer》讲了计算机视觉中的Transformer结构[1],还有非常受欢迎的Vision Transformer(ViT)[2]。本篇文章将补上上一篇掠过的《Attention Augmented Convolutional Networks》[3]和《End-to-End Object Detection with Transformers》[4],同时也会介绍一下DeiT (Data-effieciency Image Transformer)[5]

Self-Attention回顾

Transformer的核心是Self-Attention。Self-Attention是基于特征向量对序列上token成对关系的表征学习(Representation Learning)。

self-attention
Fig. 1. Self Attention.

计算方式大致如下:

  • 序列: $X\in R^{n\times d}$
  • Query向量: $Q$有相对应的可学习权重$W^Q\in R^{n\times dq}$
  • Key向量: $K$也有相对应的可学习权重$W^K\in R^{n\times dk}$
  • Value向量: $V$也有相对应的可学习权重$W^V\in R^{n\times dv}$
  • $Q=XW^Q, K=XW^K, V=XW^V$
  • Self-Attention层: $Z=softmax(\frac{QK^T}{\sqrt{d_q}})V$
  • 若是Masked Self-Attention层,则需要增加一层mask: $softmax(\frac{QK^T}{\sqrt{d_q}}\circ M)$

self-attention-operation
Fig. 2. Self Attention运算过程. Adapted from [1]

这里展示的是一个简单的Self-Attention计算方式,对于[1]同时提出的Multi-Head Self-Attention,可以将上述过程并行的执行多次来模拟不同的head。

在2021年,计算机视觉领域中的Transformer和Self-Attentio的相关技术已经发展了很多。我们可以使用以下这张图来理解今天的Self-Attention相关技术的不同类型。
taxonomy-self-attention
Fig. 3. Self Attention不同类型. Adapted from [6]

在上图中能看到不同的Attention类型,比如Local Attention, Global Attention, Vectorized Attention。下图介绍了这些类型的大致计算流程。

self-attention-overview
Fig. 4. Self Attention的不同类型. Adapted from [6]

用Attention增强卷积

从之前我们可以了解到,Self-Attention可以很好的找到距离较远的token之间的关系,而普通的卷积层只能计算非常有限的范围。如果我们能够将这些距离较远的token之间的关系计算出来,那么就可以使用Self-Attention来弥补卷积层的这个缺陷,对图像分析是有帮助的。而且卷积层是等变的,在不同区域的卷积用的是同一个参数。所以在2019年的时候,这篇文章的作者看到Self-Attention已经被广泛运用于NLP领域,所以就想到了用Self-Attention来增强卷积,并且使用基于相对位置的Positional Encoding用于解决上述说的等变问题[3]

Flatten和Attention层

先介绍会在这个部分中使用的一些数学符号:

  • $H, W, F_in$是指特征图中高度、宽度、输入feature map的数量
  • $N_h, d_v, d_k$是指head的数量,value和key的维度。其中$N_h$是可以整除$d_v$和$d_k$的。
  • $d^h_v, d^h_k$是指每一个head中value和key的维度。

attention-augmented-convolution
Fig. 5. Attention Augmented Convolution的计算过程. Adapted from [3]

第一步是需要将输入的特征图进行flatten,从$(H,W,F_in)$变成一个新矩阵$X\in \mathbb{R}^{HW\times F_{in}}$。然后把它放进标准的Multi-head Attention(MHA)层中,于是输出它的输出则是,
$$O_h=Softmax(\frac{(XW_q)(XW_k)^T}{\sqrt{d^h_k}})(XW_v)$$

Positional Encoding

在这篇文章,作者使用了一个基于像素的相对位置的Positional Encoding,专门用于图像分析。其实这个相对位置的Positional Encoding之前已经提出,具体的可以参考这篇[7]

它的计算方式是

$$l_{i,j}=\frac{q_i^T}{\sqrt{d_k^h}}(k_j+r^W_{j_x-i_x}+r^H_{j_y-i_y})$$

其中$r$是Positional Encoding,$i$是当前token所代表的像素,$j$是计算Self-Attention时的目标像素,而$x$和$y$则是像素的位置。将这个计算向量化后,

$$O_h=Softmax(\frac{QK^T+S_H^{rel}+S_W^{rel}}{\sqrt{d^h_k}})V$$

这篇作者也提到,根据他们的实验,最好的效果是同时使用传统卷积层和Attention层的输出,所以以上的结果要通过一个简单的拼接层来获得。

$$AAConv(X)=Concat[Conv(X),MHA(X)]$$

实验结果

首先是每篇论文都会提到的自己的方法比别人强。

aa-result
Fig. 6. Attention Augmented用于分类的实验结果. Adapted from [3]

可见,用于一些传统的CNN模型,用Attention增强卷积的方法可以得到更好的结果。对于COCO数据集的对象检测也是一样的,这里就不再赘述了。

不过其中最有趣的还是Positional Encoding的对比。

pos-enc-result
Fig. 7. Positional Encoding是否使用的对比结果. Adapted from [3]

能看到这个Positional Encoding真的是很有用,所以在Transformer中的Self-Attention层中基本人人都会用它。

E2E的Transformer对象检测

在2020年的时候,一篇论文[4]提出了一个E2E的Transformer对象检测模型Detection Transformer (DETR),它非常有开创性的使用Transformer结构实现了一个完全端到端的训练方式。这有什么好处呢?首先之前的对象检测模型很多都是需要Proposal,Anchor,或者Window之类的容易出错,而且还需要一些人工处理,比如说非最大值抑制等等。这些东西在一定程度上会影响模型的性能。如果能使用E2E的模式避免人工处理的情况下训练,那就能解决这个问题。

首先来看一下它的结构。

detr
Fig. 8. DETR结构. Adapted from [4]

这个结构是由一个骨干网络,Positional Encoding,Transformer,以及一个FFN预测头组成的。让我们一个一个看。

骨干特征提取器

首先是第一步的backbone,这一部分在原文中是一个CNN的特征提取器,需要从图像中提取出高价值的特征图。在原文中对于一个图像为$x_{img}\in \mathbb{R}^{3\times H_0 \times W_0}$,在骨干网络后将会变成一个特征图$f\in \mathbb{R}^{C\times H\times W}$。他们设定的值是$C=2048$ $H,W=\frac{H_0}{32},\frac{W_0}{32}$ 。

Transformer部分

通过骨干网络得到的特征图会通过一个1x1卷积压缩通道数,得到$z_0\in\mathbb{R}^{d\times H\times W}$。因为这个Transformer编码器希望得到一个序列作为输入,所以不能用一个三维的矩阵,需要压平成二维。这个特征图将会被flatten到$(d\times HW)$。对于每一层Transformer编码器,都有一个Self-Attention层和FFN,就像原版Transformer一样。作者还特地说明了,对于每一层,都添加了一个固定的不可训练的Positional Encoding加到输入中去。对于Self-Attention层和Transformer编码器部分相信大家都很熟悉了,但是这个网络还使用了Transformer解码器部分,这需要好好看看。

detr-transformer
Fig. 9. DETR中的Transformer部分. Adapted from [4]

这里我们可以看到,它并没使用原版Transformer中的Masked Multi-Head Self-Attention层,可能是因为原版的是对于时间序列的预测所以需要遮盖未来时间的序列元素,但是这里是图像分析,则不需要这些。然后是对于编码器的Attention层,和解码器的第二个Attention层,都使用了Positional Encoding,而且是每一层都会使用。

这里的Object queries其实是一个可学习的参数,原文中设定的长度是$N=100$。剩下的都是一些相加融合,根据这张图应该是很容易理解的。

FFN预测头

在解码器的输出得到嵌入序列之后,对于序列中的每一个元素都通过两个FFN网络,一个用于预测类型,一个用于预测Box。

这些FFN在原文中使用的是一个3层MLP和ReLU。对于一个91个类的多分类问题,这里的类型预测FFN会输出一个92长度的向量,多出来的一个代表”没有对象($\phi$)”。这个预测Box会被编码成中心坐标(x, y)和宽度高度(w, h),也就是长度为4的向量。由于这个预测框的数量等于Object Queriesd的数量$N$且是固定且有限的,所以必须要远大于实际可能的预测框数量。

损失函数

因为预测出的是长度为$N$的一系列无序的box,而ground truth是一个长度远小于$N$的序列。所以在计算损失时,需要将预测的box和ground truth进行匹配。这里需要搜索到一个有最小损失值的box匹配,然后计算这个最小损失。这个匹配$\sigma$是:

$$\hat{\sigma}=\mathop{\arg\min}_{\sigma\in\mathfrak{S}_N}\sum^N_i \mathcal{L}_{match}(y_i,\hat{y}_{\sigma(i)})$$

对于这个问题,使用Hungarian算法可以有效的解决,找到ground truth和预测结果的最优匹配。这里的$\mathcal{L}_{match}$是一个二分图匹配的损失函数,其中$y_i$是ground truth的第i个box,$\hat{y}_{\sigma(i)}$是预测的第i个box。对于第i个预测$\sigma(i)$,定义类别$c_i$的概率是$\hat{p}_{\sigma(i)}(c_i)$,预测的box是$\hat{b}_{\sigma(i)}$。定义这个损失函数为:

$$\mathcal{L}_{match}=-\mathbb{1}_{\{c_i\neq\emptyset\}}\hat{p}_{\sigma(i)}(C_i)+\mathbb{1}_{\{c_i\neq\emptyset\}}\mathcal{L}_{box}(b_i,\hat{b}_{\sigma(i)})$$

与上面的等式结合,可以得到Hungarian损失函数,

$$\mathcal{L}_{Hungarian}(y,\hat{y})=\sum^N_{i=1}[-log\hat{p}_{\hat{\sigma}(i)}(C_i)+\mathbb{1}_{\{c_i\neq\emptyset\}}\mathcal{L}_{box}(b_i,\hat{b}_{\hat{\sigma}}(i))]$$

其中的$\hat{\sigma}$是上面找到的最佳匹配方式。

对于box损失,这里使用GIOU损失函数[8]

再对L1损失和IOU损失进行线性组合,得到最终的损失函数,

$$\mathcal{L}=\lambda_{iou}\mathcal{L}_{iou}(b_i,\hat{b}_{\sigma(i)})+\lambda_{L1}||b_i-\hat{b}_{\sigma(i)}||_1$$

其中的$\lambda_{iou}$和$\lambda_{L1}$是超参数。

然而除此之外,他们还使用了解码器辅助损失(Auxiliary decoding loss)。添加到每一个解码器层后面,预测class和box并计算上述损失$\mathcal{L}$。

推断代码

作者们另外提供了一个基于PyTorch的用于推断的代码,放在这里有助于理解。

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
import torch
from torch import nn
from torchvision import resnet50

class DETR(nn.Module):

def __init__(self, num_classes, hidden_dim, nheads, num_encoder_layers, num_decoder_layers):
super().__init__()
# We take only convolutional layers from ResNet-50 model
self.backbone = nn.Sequential(*list(resnet50(pretrained=True).children())[:-2])
self.conv = nn.Conv2d(2048, hidden_dim, 1)
self.transformer = Transformer(hidden_dim, nheads, num_encoder_layers, num_decoder_layers)
self.linear_class = nn.Linear(hidden_dim, num_classes + 1)
self.linear_bbox = nn.Linear(hidden_dim, 4)
self.query_pos = nn.Parameter(torch.randn(100, hidden_dim))
self.row_embed = nn.Parameter(torch.randn(50, hidden_dim // 2))
self.col_embed = nn.Parameter(torch.randn(50, hidden_dim // 2))

def forward(self, inputs):
x = self.backbone(inputs)
h = self.conv(x)
H, W = h.shape[-2:]
pos = torch.cat([
self.col_embed[:W].unsqueeze(0).repeat(H, 1, 1),
self.row_embed[:H].unsqueeze(1).repeat(1, W, 1)
], dim=-1).flatten(0, 1).unsqueeze(1)
h = self.transformer(pos + h.flatten(2).permute(2, 0, 1), self.query_pos.unsqueeze(1))
return self.linear_class(h), self.linear_bbox(h).sigmoid()

detr = DETR(num_classes=91, hidden_dim=256, nheads=8, num_encoder_layers=6,
num_decoder_layers=6
)
detr.eval()
inputs = torch.randn(1, 3, 800, 1200)
logits, bboxes = detr(inputs)

Listing 1: DETR PyTorch推断代码. Adapted from [4]

实验结果

他们使用了COCO数据集用于测试这个方法。

detr-result
Fig. 10. DETR实验结果. Adapted from [4]

一言以蔽之,相比于FasterRCNN,DETR更好。

然后他们也测试了一下编码器层的数量,解码器层的数量,辅助损失,FFN,Positional Encoding的重要性,发现都很重要,都是需要的。

slots-vis
Fig. 11. $N$个Object Queries的预测可视化. Adapted from [4]

通过上图可以看出,这些预测插槽也已经学习到了box的预测模式,每一个插槽是专注于其中的少数几种box的样式。

detr2
Fig. 12. DETR用于图像分割. Adapted from [4]

如上图所示,DETR架构也可以用于图像分割,并且也在实验中取得了比其他方法更好的结果。

Transformer和知识蒸馏

在2020年底,ViT发布两个月后,这篇论文《Training data-efficient image transformers & distillation through attention》[5]介绍了他们的新方法Data-efficient image Transformers (DeiT),这是一个用少量数据和时间训练,就可以达到SOTA水平的方法。对于一个差不多参数量的ViT,需要一个Cloud TPU v3训练83天。而对于DeiT,只需要4个GPU训练3天,可见效率提升很大。

此外,之前的ViT需要在谷歌未公开的数据集JFT-300M用3亿张图片进行训练,而这个只需要在公开的ImageNet上训练就已经能达到SOTA的水平,所以这个模型是data-efficient的。

它主要的方法是通过知识蒸馏(knowledge distillation)的方法,并且做了不少实验,对比了不同的实验设定有怎样的效果。

它的基本结构如下图所示。

deit
Fig. 13. DeiT架构. Adapted from [5]

知识蒸馏

知识蒸馏是一种知识训练方法,其中知识是由一个预训练的教师模型和一个需要训练的学生模型组成。知识蒸馏除了能有效的压缩模型,还能有效的提升学生模型的性能。

Fig. 13可以看出,相比于之前的ViT,它多了一个知识蒸馏的部分。首先是输入部分,除了左下角的class token之外,还增加了一个右下角的distillation token。这个distillation token经过Transformer会输出一个值用于计算蒸馏损失。

蒸馏损失

关于蒸馏损失,这篇文章给出了两种方法,分别是硬蒸馏和软蒸馏。

$$\mathcal{L}_{dis}^{hard}=\frac{1}{2}\mathcal{L}_{CE}(\psi(Z_s),y)+\frac{1}{2}\mathcal{L}_{CE}(\psi(Z_s),y_t)$$

$$\mathcal{L}_{dis}^{soft}=(1-\lambda)\mathcal{L}_{CE}(\psi(Z_s),y)+\lambda\tau^2KL(\psi(Z_s/\tau),\psi(Z_t/\tau))$$

其中,$Z_t$是教师模型的logits输出,$Z_s$是学生模型的logits输出,$y_t$是老师模型的label,$y$是ground truth。$\tau$是超参数蒸馏温度,$\lambda$是蒸馏损失和交叉熵损失的比例。另外,$\mathcal{L}_{CE}$指的是交叉熵损失,$\psi()$指的是sigmoid函数。

根据作者的实验结果,硬蒸馏的训练效果更好

教师模型的选择

teacher-model
Fig. 14. 教师模型的实验结果对比. Adapted from [5]

作者对不同的教师模型进行了横向对比,根据上图的实验结果,可以看出使用CNN的效果要比Transformer更好。

其他超参数

作者对于训练用的很多参数都做了实验对比测试,其中包括学习率,参数初始化方法,weight decay,dropout,还有数据增强,优化器和正则化等等。对于详细的内容请参考原文,因为仅仅是实验结果而已,这里就不多做展示了。

这篇文章给Transformer使用知识蒸馏提供了很多意见和想法,如果在未来需要用一些比较高效的方法来训练Transformer,可以参考这篇文章。

参考文献

  • [1] A. Vaswani et al., “Attention is all you need,” in Proceedings of the 31st International Conference on Neural Information Processing Systems, Red Hook, NY, USA, Dec. 2017, pp. 6000–6010.
  • [2] A. Dosovitskiy et al., “An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale,” arXiv:2010.11929 [cs], Oct. 2020, [Online]. Available: http://arxiv.org/abs/2010.11929.
  • [3] I. Bello, B. Zoph, A. Vaswani, J. Shlens, and Q. V. Le, “Attention Augmented Convolutional Networks,” in Proceedings of the IEEE/CVF International Conference on Computer Vision, 2019, pp. 3286–3295, [Online]. Available: https://openaccess.thecvf.com/content_ICCV_2019/html/Bello_Attention_Augmented_Convolutional_Networks_ICCV_2019_paper.html.
  • [4] N. Carion, F. Massa, G. Synnaeve, N. Usunier, A. Kirillov, and S. Zagoruyko, “End-to-End Object Detection with Transformers,” in Computer Vision – ECCV 2020, Cham, 2020, pp. 213–229, doi: 10.1007/978-3-030-58452-8_13.
  • [5] H. Touvron, M. Cord, M. Douze, F. Massa, A. Sablayrolles, and H. Jegou, “Training data-efficient image transformers & distillation through attention,” in International Conference on Machine Learning, Jul. 2021, pp. 10347–10357. Accessed: Jul. 22, 2021. [Online]. Available: http://proceedings.mlr.press/v139/touvron21a.html
  • [6] S. Khan, M. Naseer, M. Hayat, S. W. Zamir, F. S. Khan, and M. Shah, “Transformers in Vision: A Survey,” arXiv:2101.01169 [cs], Feb. 2021, Accessed: Apr. 23, 2021. [Online]. Available: http://arxiv.org/abs/2101.01169
  • [7] P. Shaw, J. Uszkoreit, and A. Vaswani, “Self-Attention with Relative Position Representations,” arXiv:1803.02155 [cs], Apr. 2018, Accessed: Jul. 25, 2021. [Online]. Available: http://arxiv.org/abs/1803.02155
  • [8] H. Rezatofighi, N. Tsoi, J. Gwak, A. Sadeghian, I. Reid, and S. Savarese, “Generalized Intersection Over Union: A Metric and a Loss for Bounding Box Regression,” in Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition, 2019, pp. 658–666. Accessed: Jul. 26, 2021. [Online]. Available: https://openaccess.thecvf.com/content_CVPR_2019/html/Rezatofighi_Generalized_Intersection_Over_Union_A_Metric_and_a_Loss_for_CVPR_2019_paper.html

在UEFI Secure Boot下的Linux安装N卡驱动

作者 ControlNet
2021年5月13日 09:22

最近在帮忙给一台单卡3090的工作站装Linux系统,结果踩了不少坑,折腾了一天多才成功装上。这篇文章就来分享一下,有哪些坑,然后自己是如何解决的。

当然其中最大的原因都是因为这台电脑是组织管理的,自己没有权限设置BIOS,并且强制启动Secure Boot,就算是有win10管理员账号也解决不了任何问题。

安装Linux系统

首先拿到手看看配置,是10900X和3090。有两块硬盘,一个500G左右的NVME SSD,一个2T的HDD。每个硬盘一个分区,一个受组织管理的Win10安装在SSD上。所以为了在SSD上安装Linux,先将SSD分成了两个分区,准备制作Ubuntu20.04的U盘启动盘。

ubuntu
Fig. 1. Ubuntu. Adapted from [13]

因为之前在家里台式机的win10上用过一个叫做Universal-USB-Installer-2.0.0.0.exe的软件挺好用,直接就复制到了自己Macbook的Win10虚拟机里,并且制作了Ubuntu启动盘。结果U盘插上启动的时候引导失败了,被Secure Boot阻止了,因为没有合格的签名,提示/casper/vmlinuz has invalid signature。后来向同学借用了win10电脑才解决问题。

所以使用虚拟机做U盘启动盘可能无法通过Secure Boot,看来虚拟机也并不是万能的。

不过最终也在Google和一些参考资料[1][2][3]的帮助下成功装上了Win10和Ubuntu双系统。

尝试安装GPU驱动

依然在很多搜索到的资料[4, 5]的帮助下,装上Ubuntu的时候第一件事就是安装N卡驱动。在N卡官网上,自动下载了.run后缀的文件。因为自己基本没怎么用过Linux,直接就双击打开了,安装速度极慢。放弃,进终端用sh运行.run文件才终于解决了。

nvidia-driver
Fig. 2. Nvidia driver download page. Adapted from [14]

然而安装的时候有很多选项并不是很能理解,虽然凭借着自己的直觉进行选择,但是不管试了几次,使用nvidia-smi依然看不到显卡,在Ubuntu的信息界面也只是显示了GPU是LLVM设备。这个时候就开始怀疑自己是不是安装的方法错误了。

这时候上网听说Linux Mint系统非常适合Linux小白[6],并且对各种第三方驱动都非常友好。Linux Mint的官网上可以选择三种GUI,Xfce, MATE, Cinnamon,根据官方的介绍[7],最终考虑到实用性和美观性选择了Cinnamon。在这个信息的驱使下,重新制作了Mint的启动盘,并且完全删除了Ubuntu安装了Mint。

mint
Fig. 3. Linux Mint. Adapted from [15]

一进系统,就开始进入Driver Manager去检查能否直接使用推荐的N卡驱动,的确有,并且安装上了,但是依然没有看到显卡。

然后尝试按照之前同样的方法,手动安装N卡驱动。这时留意到了”UEFI Secure Boot”的字样,感觉这其中有点关系。网上一查[8][9][12],居然是Secure Boot的锅,而绝大多数人的解决方案都是关闭Secure Boot。

在Secure Boot下安装GPU驱动

还好找到了这几篇文章[10][11],上面详细的介绍了如何自己生成签名,并且添加至Linux MOK,让第三方驱动在UEFI Secure Boot启动的情况下依然可以工作。

主要的安装过程是这样的。

1.禁用图形界面,并且安装一些前置库

按Ctrl+Alt+F2进入终端。

1
2
3
sudo su
service lightdm stop # stop X server
apt-get install gcc kernel-devel # some dependencies

2. 安装驱动并签名

1
sudo sh "NVIDIA-xxx.run"  # run the NVIDIA driver installer

在安装过程中根据自己的需要进行选择,不过需要在dkms安装的对话框中选择否。

在询问道是否需要签名时,选择生成密钥。程序将会生成一个私钥和公钥,保存在/usr/share/nvidia中,公钥是.key文件,私钥是.der。记录一下这两个文件的路径。这时候安装程序会提示是否删除私钥,选否。然后安装程序会提示运行失败,不过这个没有问题,等下一步将密钥添加到信任列表中就可以工作了。

3. 添加密钥到信任列表中

首先要将这个密钥添加到MOK,输入

1
sudo mokutil --import /usr/share/nvidia/xxx.der

然后输入一个密码两次。再直接重启电脑,这时候会进入Linux MOK Management界面,选择Enroll MOK, Continue boot再输入上一步设置的密码即可将密钥添加到信任列表中。

再启动系统输入nvidia-smi,发现显卡已经可以使用了。

nvidia-smi
Fig. 4. nvidia-smi screenshot.

安装Anaconda,一键装上PyTorch,测试能否使用CUDA。

pytorch
Fig. 5. Checking pytorch cuda availability screenshot.

终于大功告成了。

参考文献

  • [1] R. Smith and P. Vakalopoulos, "How to install ubuntu in UEFI mode", Ask Ubuntu, 2021. [Online]. Available: https://askubuntu.com/questions/927924/how-to-install-ubuntu-in-uefi-mode.
  • [2] ravery, "Installing Ubuntu on External Hard Drive - UEFI", Ask Ubuntu, 2021. [Online]. Available: https://askubuntu.com/questions/990790/installing-ubuntu-on-external-hard-drive-uefi.
  • [3] "详解安装Ubuntu Linux系统时硬盘分区最合理的方法 - 云+社区 - 腾讯云", Cloud.tencent.com, 2021. [Online]. Available: https://cloud.tencent.com/developer/article/1711884.
  • [4] "How to Install Nvidia Drivers on Ubuntu 20.04", Linuxize, 2021. [Online]. Available: https://linuxize.com/post/how-to-nvidia-drivers-on-ubuntu-20-04/.
  • [5] L. Rendek, "How to uninstall the NVIDIA drivers on Ubuntu 20.04 Focal Fossa Linux", linuxconfig, 2021. [Online]. Available: https://linuxconfig.org/how-to-uninstall-the-nvidia-drivers-on-ubuntu-20-04-focal-fossa-linux.
  • [6] "Ubuntu vs Mint:哪个更好?", 知乎专栏, 2021. [Online]. Available: https://zhuanlan.zhihu.com/p/52076038.
  • [7] "选择正确的版本 — Linux Mint Installation Guide 文档", Linuxmint-installation-guide.readthedocs.io, 2021. [Online]. Available: https://linuxmint-installation-guide.readthedocs.io/zh_CN/latest/choose.html.
  • [8] "NVIDIA driver is not loaded. Ubuntu 18.10", NVIDIA Developer Forums, 2021. [Online]. Available: https://forums.developer.nvidia.com/t/nvidia-driver-is-not-loaded-ubuntu-18-10/70495/4.
  • [9] "Ubuntu16.04安装nvidia显卡安转与Secure Boot开启的矛盾_caobo_0512的博客-CSDN博客", Blog.csdn.net, 2021. [Online]. Available: https://blog.csdn.net/caobo_0512/article/details/106246195.
  • [10] "Linux secure boot(安全启动)时添加Nvidia显卡驱动_天道酬勤-CSDN博客", Blog.csdn.net, 2021. [Online]. Available: https://blog.csdn.net/qq_38880380/article/details/78675202.
  • [11] "安装Ubuntu系统的一个坑(安全启动)_荒陌的博客-CSDN博客_ubuntu关闭安全启动", Blog.csdn.net, 2021. [Online]. Available: https://blog.csdn.net/sinat_38640606/article/details/82794511?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-9.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-9.control.
  • [12] "安装Ubuntu系统的一个坑(安全启动)_荒陌的博客-CSDN博客_ubuntu关闭安全启动", Blog.csdn.net, 2021. [Online]. Available: https://blog.csdn.net/sinat_38640606/article/details/82794511?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-9.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-9.control.
  • [13] "🐧Installing Ubuntu - A Step by Step guide 🐧", DEV Community, 2021. [Online]. Available: https://dev.to/kaiwalyakoparkar/installing-ubuntu-a-step-by-step-guide-2b29.
  • [14] "Download Drivers | NVIDIA", Nvidia.com, 2021. [Online]. Available: https://www.nvidia.com/Download/index.aspx.
  • [15] "Linux Mint 20.1 “Ulyssa” Cinnamon overview | Sleek, modern, innovative.", Youtube.com, 2021. [Online]. Available: https://www.youtube.com/watch?v=Cs09YF_YlMI.

计算机视觉中的Transformer

作者 ControlNet
2021年4月30日 23:56

深度学习中最一开始的Transformer是2017年推出的,非常强力[1]。可能当时作者觉得这个东西很强,所以才会赋予”变形金刚”的名字吧。而后来,Transformer也广泛的推广到了计算机视觉(CV)领域,从2020年开始,就有对Transformer在CV中的大量新研究发表。

本文主要会讲最初的Transformer,Vision Transformer(ViT)和Multi-scale Vision Transformer(MViT)。

最初的Transformer

解决并行问题

最初的Transformer是来源于这篇,”Attention is all you need”,用于自然语言处理(NLP)的机器翻译任务的。以前的RNN(包括LSTM[2], GRU[3])层计算并不能并行,因为一个序列中的某一个元素的计算是要基于别的元素的。如下图所示。

rnn-layer
Fig. 1. RNN layer. Adapted from [4]

其中$a^1$到$a^4$都是一个序列中的token,RNN层可以看到一个序列中的全部信息,包括位置信息。但是这里每一个token的计算需要其他token的计算结果,所以无法做到并行化提高效率。

cnn-layer
Fig. 2. Convolutional layer. Adapted from [4]

如上图所示, 使用卷积层[5]虽然可以并行计算,但是覆盖面积受到卷积核(kernel)的限制,无法在距离比较远的token中提取特征。

stacked-cnn-layers
Fig. 3. Stacked convolutional layer. Adapted from [4]

如上图所示,很多人会通过将卷积层堆叠起来以达到提取更多特征的目的,但是这个覆盖范围其实依然并不是很宽,而且也需要更多的计算。

我们需要的是一个有长时记忆,并且可以并行处理的结构。所以这篇论文提出了Self-attention机制和Multi-Head Attention Layer。

calculate-k-q
Fig. 4. Calculation of $K^i$ and $Q^i$ in self-attention layer. Adapted from [4]

首先,我们有一个输入序列,从$x^1$到$x^4$。在Embedding之后,会将每一个token转换成1-D vector,$a^1$到$a^4$。使用3个线性层分别对应Q,K,V的权重,则可以把$z^i$转换成Q (Query),K (Key),V (Value)的向量。这个计算方式如下所示。
$$
\begin{split}
q^i &= W^qa^i \\
k^i &= W^ka^i \\
v^i &= W^va^i
\end{split}
$$
我们以第一个token作为例子,需要用$q^1$与所有token的$k$相乘,从而求得$\alpha_{1,1}$到$\alpha_{1,4}$。之后,通过一个softmax进行标准化使得它们的和为1,得到$\hat{\alpha}_{1,1}$到$\hat{\alpha}_{1,4}$。

calculate-bi
Fig. 5. Calculation of $b^i$ in self-attention layer. Adapted from [4]

然后,再对计算出来的$\hat{\alpha}_{1,1}$到$\hat{\alpha}_{1,4}$和$v$相乘求和。以第一个token为例,它的attention层输出是,
$$
b^1 = \sum_i\hat{\alpha}_{1,i}v^i
$$

重复同样的步骤,则可以得到$b_1$到$b_4$。以上的步骤看起来计算相当的复杂,但是其实可以通过几个比较简单的矩阵运算即可完成,所以它是可以很容易通过GPU进行并行计算的。

matrix-multiplications
Fig. 6. Matrix multiplication form in self-attention layer. Adapted from [4]

左上角的$O$代表output,$I$代表input。而这个$I$则是将所有的输入$a_i$叠起来变成一个2D矩阵。而以上的全部需要一个一个迭代计算的过程都可以用矩阵乘法的方式实现,总的计算其实很方便。正因为这个容易进行并行计算,所以在运算速度上是快于RNN的。

Transformer结构

scaled-dot-product-attention
Fig. 7. Scaled dot product attention. Adapted from [1]

$$Attention(Q,K,V)=softmax(\frac{QK^T}{\sqrt{d_K}})V$$

如上所示[1],这个过程被称为Scaled Dot-Product Attention。你会注意到这个公式里除以一个$\sqrt{d_K}$。根据原文的说明,这个的目的是为了把值重新缩放回 Mean = 0, Variance = 1 的状态。

multi-head-attention
Fig. 8. Multi-head attention. Adapted from [4]

除此之外,他们还提出了一个Multi-Head Attention Layer,相比于普通的attention层,这个多头attention层有多个attention层互相并行。最后把多个并行的attention层拼接,再通过一个全连接层进行映射,把维度保持在和输入相同的状态。

transformer
Fig. 9. Transformer structure. Adapted from [1]

如上图所示,整个Transformer是一个encoder-decoder结构。如果这个Transformer的任务是把英文翻译成中文,那在encoder的inputs这里输入中文句子,在decoder的输入端输入英文句子,decoder的输出则是概率。从encoder开始,输入的token序列会先送入embedding层转换成向量,然后再和positional encoding相加。因为在attention层中输入token并不会知道其中的位置信息,所以需要加一个positional encoding。因为矩阵相加可以视为拼接的一种特殊情况,所以通过embedding和positional encoding相加,attention层可以同时获取两者的信息,提取的特征更有效率。这个positional encoding可以是自己手动设定的,也可以是通过学习的。在这篇文章下用的是手动设定的。相加之后,会有4条路线,1条是类似于ResNet[6]的恒等连接,而另外3条则是通过对应的全连接层得到Q,K,V,再输入进attention层。在结束了multi-head attention层的计算之后,接下来是一个Add & Norm层,这个层的过程就是将attention层的输入和输出相加,并且做一个layer normalization(LN)[7]

layer-normalization
Fig. 10. Layer normalization. Adapted from [4]

如上图所示[4],和常用的batch normalization(BN)[8]相比,主要是标准化的维度不同。LN是在一个输入中跨通道的标准化,BN是在一个batch中跨数据但是在同一个通道下进行标准化。在sequence数据中,一般使用LN而不是BN。

然后是decoder部分,这里的decoder的输入,也就是机器翻译的结果是需要右移一位的(shifted right),因为第一位是<BOS>标签,作为一个句子的开头。对比encoder和decoder,主要区别在于decoder的第一层是一个Masked Multi-Head Attention。因为对于一个基于时间序列的预测来说,当然是不能通过未来的信息去预测的,所以在这个Masked Multi-Head Attention层中,所有的token输入只能看到前面的信息,而后面的信息都会被隐藏。

在这个Masked Multi-Head Attention层之后,会有一个普通的Multi-Head Attention,但是其中的Q和K是来源于encoder计算的结果,只有V是来自于上一层的输出。个人理解是Q和K可以寻找不同token在这个sequence之间的相关性,所以encoder的输出要拿来给decoder使用。

transformer-inference
Fig. 11. Transformer inference. Adapted from [4]

如上图所示[4],在预测的时候,因为不知道翻译的结果,所以只能一个一个预测,将第一个预测出的词放到这个序列的第二个输入才能依次预测出整个句子。在训练的时候因为知道了全部内容,所以可以并行,但是在预测中是不行的。

尝试把Transformer用于CV

在Transformer发布之后,有很多研究在尝试把Transformer结果用于CV。

attention-augmented-convolutional-networks
Fig. 12. Attention augmented convolutional network. Adapted from [9]

比如这一篇[9],同时使用传统的卷积和新的Self-Attention机制用于计算机视觉相关的任务。

object-detection-transformer
Fig. 13. Object detection transformer. Adapted from [10]

而这一篇[10],则仅仅用CNN替代原始Transformer中的Embedding过程,剩下的都与原版[1]很像。同样是完整的Transformer encoder和decoder,还有positional encoding,最后生成的结果也是一个个box用于对象检测(object detection)。这个过程已经和原版很像了。

关于这两篇的详解,已经在续篇中详细介绍。

Vision Transformer

之前的研究很少有讲Transformer直接用在CV上的,而且用上了速度也很慢。在2020年年底,有一篇论文,”An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale”,提出了Vision Transformer (ViT)[11],提供了另一种方法让本来用于NLP的Transformer可以直接用在CV上。在ViT中,每一个Transformer的token其实是一个图片的patch,这也是标题这么起的原因。在这篇论文中,这个ViT主要是用于最基础的图像分类任务。

vision-transformer
Fig. 14. Vision transformer. Adapted from [11]

具体步骤

一开始我们有一个图片$x$,高度$H$,宽度$W$,通道数$C$。对于RGB图像来说$C=3$。
$$x\in H\times W\times C$$

正如之前所说,我们先需要讲这个图片分成一块一块patch,然后将一个patch中的所有数值全部暴力的压扁到一维向量。假设patch都是正方形,边长为$P$,一个图片中的patch数量为$N$。则,
$$x_p\in N \times (P^{2}C)$$
其中,
$$N=\frac{HW}{P^2}$$
所以我们是将一个patch本来为三维矩阵$(P\times P \times C)$直接摊平为一维向量,长度为$P^{2}C$。而因为有$N$个patch,所以最后的结果是$N \times (P^{2}C)$的二维矩阵。这个形状是符合Transformer输入的。

然而$P^{2}C$的长度可能太长了,于是我们可以使用一个线性映射将维度降低到$N\times D$,其中$D$是新的patch向量的长度。把降维之后的矩阵记为$z_p$。
$$z_p\in N\times D$$

然后,我们需要增加一个可训练的token,$z_{class}$,放到整个$Z$的第一位,并且加上一个可训练的positional encoding。
$$Z=[z_{class},z_p^1,z_p^2,z_p^3,\cdots,z_p^N]+E_{pos}$$
由这些新的向量组成的矩阵加上positional encoding $E_{pos}$,得到的结果的矩阵记为$Z$。

这些东西就可以直接放进一个Transformer encoder,得到的结果也是一个sequence,但是只取第一个,也就是$z_{class}$对应的输出。将这个向量放入一个MLP用于分类,得到分类结果$\hat{y}$,与label $y$一起计算loss并反向传播训练整个ViT。

Class Token

根据原文,这个是从BERT[12]的结果中拿过来的,因为使用一个可训练的token,会比使用别的token的结果用于分类更加公平,因为不会受到原先包含信息的影响。

bert
Fig. 15. BERT. Adapted from [12]

Positional Encoding

positional-encoding
Fig. 16. Positional encoding in ViT. Adapted from [11]

然后是关于positional encoding的。这张图展示了positional encoding的相似度,越亮说明越相似。我们可以看到,对于每一个patch,在相似行和列的patch的相似度是较高的,而那些距离比较远的相似度则较低。这说明一个可训练的positional encoding是可以学到其中的位置关系的。

attention-distance
Fig. 17. Attention distance. Adapted from [11]

这张图展示了attention距离和网络深度的关系。x轴是网络深度,y轴是平均attention的patch之间的距离,不同的点代表不同的head找到的attention。很显然,在网络很浅的时候,attention层依然能找到距离较远的关系。如果这是一个CNN网络,那这个结果就应该会分布在这张图中一堆散点的下边缘。所以不用特别深的网络,ViT依然能学到这张图的全局特征。

Multiscale Vision Transformer

在之前的ViT[11]中,实际上每一个attention层的输入输出维度都是一样的,所以attention层只是在一个scale上进行检测。这篇2021年的论文提出了Multiscale Vision Transformer (MViT)[13]。大家都知道,在CNN中经过了数层卷积层之后会有一个降采样层降低特征图的长宽,这样之后的卷积层将能提取到不同scale的特征。这篇论文想通过同样的方式来把这个multi-scale的想法用在ViT上。

multiscale-vision-transformer
Fig. 18. Multiscale vision transformer. Adapted from [13]

如上图所示[13],他们提出了Multi-Head Pooling Attention (MHPA)层。很显然,最大的区别就在于对于每一条路上都加了一个pooling层。在这些pooling层之后,那些向量的长度就会降低,这样后面的attention层就能检测到不同scale的特征了。

mvit-structures
Fig. 19. MViT structures. Adapted from [13]

在这篇文章里,他们提出两个MViT的结构,分别是MViT-B和MViT-S,用于视频分析的。他们把一整个网络分成了多个stage。在第一个stage中是读取数据,因为视频是用3D矩阵表示,切分patch也是3D,所以patch在这里被成为cube。然后在后面的stage中,cube的长度和宽度都不断降低,而通道数则不断增加。在最终的实验结果中,MViT的表现要优于之前的ViT模型,而且参数数量也更少了,运行速度也更快了。

推荐一个GitHub Repository

这里推荐一个GitHub repo,叫Awesome Visual-Transformer,里面收录了许多用于CV的Transformer论文和官方实现的code链接[14]
https://github.com/dk-liang/Awesome-Visual-Transformer

参考文献

  • [1] A. Vaswani et al., “Attention is all you need,” in Proceedings of the 31st International Conference on Neural Information Processing Systems, Red Hook, NY, USA, Dec. 2017, pp. 6000–6010.
  • [2] S. Hochreiter and J. Schmidhuber, “Long Short-Term Memory,” Neural Computation, vol. 9, no. 8, pp. 1735–1780, Nov. 1997, doi: 10.1162/neco.1997.9.8.1735.
  • [3] K. Cho et al., “Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation,” arXiv:1406.1078 [cs, stat], 2014.
  • [4] "Transformer & BERT", Speech.ee.ntu.edu.tw, 2021.
  • [5] Y. Lecun, L. Bottou, Y. Bengio, and P. Haffner, ‘Gradient-based learning applied to document recognition’, Proceedings of the IEEE, vol. 86, no. 11, pp. 2278–2324, Nov. 1998, doi: 10.1109/5.726791.
  • [6] K. He, X. Zhang, S. Ren, and J. Sun, “Deep Residual Learning for Image Recognition,” in Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition, 2016, pp. 770–778.
  • [7] J. L. Ba, J. R. Kiros, and G. E. Hinton, “Layer Normalization,” arXiv:1607.06450 [cs, stat], 2016.
  • [8] S. Ioffe and C. Szegedy, “Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift,” in International Conference on Machine Learning, Jun. 2015, pp. 448–456.
  • [9] I. Bello, B. Zoph, A. Vaswani, J. Shlens, and Q. V. Le, “Attention Augmented Convolutional Networks,” in Proceedings of the IEEE/CVF International Conference on Computer Vision, 2019, pp. 3286–3295.
  • [10] N. Carion, F. Massa, G. Synnaeve, N. Usunier, A. Kirillov, and S. Zagoruyko, “End-to-End Object Detection with Transformers,” in Computer Vision – ECCV 2020, Cham, 2020, pp. 213–229, doi: 10.1007/978-3-030-58452-8_13.
  • [11] A. Dosovitskiy et al., “An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale,” arXiv:2010.11929 [cs], Oct. 2020.
  • [12] J. Devlin, M.-W. Chang, K. Lee, and K. Toutanova, “BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding,” in Proceedings of the 2019 Conference of the North American Chapter of the Association for Computational Linguistics: Human Language Technologies, Volume 1 (Long and Short Papers), Minneapolis, Minnesota, Jun. 2019, pp. 4171–4186, doi: 10.18653/v1/N19-1423.
  • [13] H. Fan et al., “Multiscale Vision Transformers,” arXiv:2104.11227 [cs], Apr. 2021.
  • [14] "dk-liang/Awesome-Visual-Transformer", GitHub, 2021. [Online]. Available: https://github.com/dk-liang/Awesome-Visual-Transformer.
❌
❌