介绍
您可能曾经听说过,中奖的几率非常小。与所有与概率有关的事情一样,多次尝试可能会让结果对您有利。现在,如果您参加许多彩票,中奖的几率会更高一些,具体取决于您参加了多少次彩票。这仍然不能保证您最终会中奖,但通过均匀分布,并遵循大数定律(在这种情况下意味着大量彩票),我们可以得出相对更有可能的可能性。
重要的是要明白,每张新彩票都是相互独立的,同一个彩票“彩票号码”可能会赢得许多不同的彩票(遵循大数定律)。你也可能运气不好,每次彩票都选错号码,不管你试了多少次。你现在有两个选择:
每次你可以尝试一个随机数。
每次您都可以尝试同一个号码。
从理论上(和数学上)看,这两种情况发生的可能性是相等的。但是,情况 2 会给你一点优势。随着次数趋近于无穷大,每个数字最终都会被选中。问题在于,在情况 1 中,你需要尝试更多次,希望你当时选择的数字与获胜的数字相匹配。在情况 2 中,你确信随着尝试次数趋近于无穷大,你的数字最终会“获胜”。对于这篇博文,我们将使用情况 2。
那么,在我告诉你答案之前,你认为你能回答这个问题吗?
“如果你身边的所有彩票都为 100 万人提供中奖名额,而你为每一位中奖者都挑选了同一张彩票 [x],那么你需要参加多少次彩票抽奖才能最终成为中奖者?”(欢迎评论你最初的答案)
答案是...
约1440万次。
本博文的其余部分将介绍我如何得出该值、如何进行模拟以及一些注意事项。从这里开始,事情将变得更加技术化。
逻辑
100 万人的彩票号码范围是 1 - 1,000,000(或 0 - 999,999)。玩家每次彩票只能在这个范围内选择一个号码,中奖彩票也只能来自这个范围。本质上,我们可以说我们将有一组 100 万个号码。
考虑到用户可以选择该范围内的任何数字,我们需要满足集合中每个项目至少被选中一次的条件。这是因为如果每个数字至少被调用一次,它将涵盖玩家可能选择的任何可能的票号。这也意味着我们不关心每个数字运行的次数,使“集合”成为我们模拟的理想 Python 数据结构。我们将从一个空集开始,并在每次迭代时用随机生成的数字填充它,直到集合包含指定范围内的每个数字。由于 Python 集合不重复数字,我们不必担心确保唯一性。
def calculate_lottery_chances(lottery_players_count):
number_set = set()
count = 0
while len(number_set) < lottery_players_count:
gen_number = random.randint(1, lottery_players_count)
number_set.add(gen_number)
count += 1
return count
对于 1,000,000 人的彩票,函数调用将如下所示:calculate_lottery_chances(1000000),它将返回中奖前的彩票尝试次数。以这种方式安排代码使其非常易于扩展。

问题
简而言之,问题的根源是“变化”。我第一次运行该函数时,得到的值是“1310 万”。我重新运行它,得到的值大约是 1390 万。我又运行了几次,得到的答案却大相径庭——在某个时候,我得到的答案是 1500 万。很明显,我需要这样做并找到一个平均值。按照目前的模式,我认为随着求平均值的迭代次数趋于无穷大,我将更接近一个可靠的答案。需要某种可以做到这一点并快速完成的东西,这促使我编写了这个函数:
def average_over_n_times(function, function_arg, n):
"""
This returns the average of the returned value of a function
when it is called n times, with its (one) arg
"""
total = 0
for x in range(0, n):
total += function(function_arg)
return round(total/n)
随后,所有内容将被修补如下:
num_of_trials = average_over_n_times(calculate_lottery_chances, lottery_players_count, n)
其中“n”表示对结果求平均值的次数。然而,这又带来了另一个问题,将在下一节中讨论。
“n”应该是什么?
n 值越大,结果越接近“平均情况”。但是,考虑到仍然没有绝对或确定性,执行这一系列任务太多次就不再有成效。我这样说有以下原因:
时间不是无限的,我们无法无限地进行这些计算,这意味着每次运行时总会有变化(无论多小),从而违背了“绝对”的概念。
计算资源是有限的。
该实验的假设之一是,计算机产生的“随机性”可以准确地模仿现实。
就像算法运行时间一样,较小的量级不再像较大的量级那么重要。当处理大于 13,000,000 的值时,大约 100,000 的变化并不那么重要。
考虑到这些,我用以下值测试了“n”:10、20、30、50、100、1000 和 5000 次。
PyTorch 的作用是什么?
此时,您可能想知道为什么博客文章标题中甚至没有提到“PyTorch”这个词。好吧,虽然我提到使用不同的值测试 n,但我对所有测试使用的代码并不相同。
这些都是计算量很大的实验,我的 CPU 跟我讲了话。我之前分享的代码片段写在一个没有外部包依赖项的文件中,该文件在 bash shell 中运行,并time在前面加上命令来跟踪执行时间。以下是仅使用 CPU 时的执行时间:
| n | 时间(分和秒) |
|---|---|
| 10 | 1分34.494秒 |
| 20 | 3分2.591秒 |
| 三十 | 5分19.903秒 |
| 50 | 10分58.844秒 |
| 100 | 14分56.157秒 |
到了 1000,程序就无法运行了。我不确定它是不是在中途坏了,无法停止执行,但我在 4 小时 57 分钟后取消了它。我觉得有几个因素影响了这一点,我将在“注意事项”部分讨论。不管怎样,我的风扇噪音很大,我知道我可能把笔记本电脑功率适中的 CPU 用得太多了。我拒绝接受失败,在思考我能做些什么至少运行 4 位数的迭代时,我想起了一位使用 PyTorch 的朋友告诉我的一句话:
“GPU 在执行计算密集型任务时通常比 CPU 更高效”
PyTorch 使用 GPU,使其成为完成这项工作的完美工具。
重构
出于我们的目的,PyTorch 将用于计算,因此重构现有calculate_lottery_chances()代码意味着改变依赖于 CPU 的数值运算并切换到合适的 PyTorch 数据结构。简而言之:
Python
set()数据类型不再足够。Python
randint()函数将会被替换为其 PyTorch 等效函数。由于
set()数据类型不够用,因此会有一个开关来生成与大小匹配的零张量lottery_players_count,并使用布尔值来指示某个数字是否先前获胜。
重构calculate_lottery_chances如下:
import torch
def calculate_lottery_chances(lottery_players_count):
seen_numbers = torch.zeros(lottery_players_count, dtype=torch.bool, device='xpu')
batch_size = 10000
count = 0
while not torch.all(seen_numbers):
# Generate a batch of random numbers
random_numbers = torch.randint(
0, lottery_players_count,
(batch_size,),
device='xpu'
)
seen_numbers[random_numbers] = True
count += batch_size
return count
我将我的设备设置为“xpu”,因为我的计算机使用PyTorch 支持的英特尔图形 GPU 。
输出
为了确保我的 GPU 在执行过程中得到使用,我在运行之前打开了 Windows 任务管理器并导航到“性能”部分。运行时,我看到 GPU 资源使用率明显飙升。
为了便于理解,以下是前后对比:
前:
注意 GPU 使用率为 1%
后:
注意 GPU 使用率为 49%
对于不同 n 值的运行时间,GPU 的速度要快几倍。它能够在不到一分钟的时间内连续运行低于 100 的 n 值,并且能够计算5000(五千!)的 n 值
以下是使用 GPU 的运行时间表:
| n | 时间(分和秒) |
|---|---|
| 10 | 0分13.920秒 |
| 20 | 0分18.797秒 |
| 三十 | 0分24.749秒 |
| 50 | 0分34.076秒 |
| 100 | 1分12.726秒 |
| 1000 | 16分9.831秒 |
为了直观地了解本次实验中 GPU 和 CPU 操作之间的性能差距有多大,这里有一个数据可视化供您参考:
x 轴的上限为 100,因为我无法再从 CPU 获得实际的“及时”输出,因此没有空间与 GPU 进行比较。使用 1000 - 5000 范围内的数字进行实验,结果通常是“1440 万次”。这就是我之前得到答案的方式。
注意事项
这项实验做出了假设并依赖于某些做事方式。此外,我对 PyTorch 缺乏经验,这可能意味着可能存在更有效的方法。以下是一些可能影响我的发现准确性或执行时间的因素:
我做了一个巧妙的假设,即计算机生成的随机性模仿了现实生活中(物理世界)的随机性。
虽然我将一些逻辑改为使用 PyTorch,但其余代码仍然依赖于 CPU。例如,在函数中
average_over_n_times(),循环中的加法和平均可能都受益于 PyTorch 等效项。我怀疑性能会有所提升。我不确定
batch_size我所用的方法对准确性和性能有何影响。所有 CPU 和 GPU 测试都是在 PC 插电的情况下进行的,以使机器发挥最佳性能。使用电池供电的设备运行这些测试可能会延长运行时间。
PyTorch 的 CUDA 可能比“XPU”更有优势,但我的电脑不支持前者。
我在测试期间避免让 PC 进入“睡眠”状态。如果计算机进入睡眠状态,测试可能需要更长时间才能完成。
最后,我想指出的是,这是我第一次使用 PyTorch 做任何事情,它的性能给我留下了深刻的印象。
结论
当我深入研究这个技术时,我没想到性能会有如此大的提升。我了解了张量背后的概念,以及一些关于计算更复杂的任务背后的支持机制的知识。您可以随意使用、复制或修改代码片段。
感谢您的包容,希望您读得愉快。
直到下一次,
干杯。

