确保 np.random.choice() 的列表内容总和为 1

Ensure that contents of list sums up to 1 for np.random.choice()

背景

在 Python 3.5 中,我正在制作一个函数来生成具有不同生物群落的地图 - 一个二维列表,第一层代表 Y 轴的线,项目代表沿线的项目X 轴。

Example:

[
["A1", "B1", "C1"],
["A2", "B2", "C2"],
["A3", "B3", "C3"]
] 

This displays as:

A1 B1 C1
A2 B2 C2
A3 B3 C3

目标

如果地图上的给定位置的邻居也是该生物群系,则该位置应该更有可能是该生物群系。所以,如果给定方格的邻居都是伍兹,那么几乎可以保证该方格是伍兹。


我的代码(到目前为止)

所有生物群落都由 类(woodsBiomedesertBiomefieldBiome)表示。它们都继承自 baseBiome,后者单独用于填充网格。

我的代码是函数形式的。它以最大的 X 和 Y 坐标作为参数。这是:

def generateMap(xMax, yMax):
    areaMap = []  # this will be the final result of a 2d list

    # first, fill the map with nothing to establish a blank grid
    xSampleData = []  # this will be cloned on the X axis for every Y-line
    for i in range(0, xMax):
        biomeInstance = baseBiome()
        xSampleData.append(biomeInstance)  # fill it with baseBiome for now, we will generate biomes later
    for i in range(0, yMax):
        areaMap.append(xSampleData)

    # now we generate biomes
    yCounter = yMax  # because of the way the larger program works. keeps track of the y-coordinate we're on
    for yi in areaMap:  # this increments for every Y-line
        xCounter = 0  # we use this to keep track of the x coordinate we're on
        for xi in yi:  # for every x position in the Y-line
            biomeList = [woodsBiome(), desertBiome(), fieldBiome()]
            biomeProbabilities = [0.0, 0.0, 0.0]
            # biggest bodge I have ever written
            if areaMap[yi-1][xi-1].isinstance(woodsBiome):
                biomeProbabilities[0] += 0.2
            if areaMap[yi+1][xi+1].isinstance(woodsBiome):
                biomeProbabilities[0] += 0.2
            if areaMap[yi-1][xi+1].isinstance(woodsBiome):
                biomeProbabilities[0] += 0.2
            if areaMap[yi+1][xi-1].isinstance(woodsBiome):
                biomeProbabilities[0] += 0.2
            if areaMap[yi-1][xi-1].isinstance(desertBiome):
                biomeProbabilities[1] += 0.2
            if areaMap[yi+1][xi+1].isinstance(desertBiome):
                biomeProbabilities[1] += 0.2
            if areaMap[yi-1][xi+1].isinstance(desertBiome):
                biomeProbabilities[1] += 0.2
            if areaMap[yi+1][xi-1].isinstance(desertBiome):
                biomeProbabilities[1] += 0.2
            if areaMap[yi-1][xi-1].isinstance(fieldBiome):
                biomeProbabilities[2] += 0.2
            if areaMap[yi+1][xi+1].isinstance(fieldBiome):
                biomeProbabilities[2] += 0.2
            if areaMap[yi-1][xi+1].isinstance(fieldBiome):
                biomeProbabilities[2] += 0.2
            if areaMap[yi+1][xi-1].isinstance(fieldBiome):
                biomeProbabilities[2] += 0.2
            choice = numpy.random.choice(biomeList, 4, p=biomeProbabilities)
            areaMap[yi][xi] = choice

    return areaMap

解释:

如您所见,我从一个空列表开始。我将 baseBiome 添加到它作为占位符(最多 xi == xMaxyi == 0)以生成一个我可以循环浏览的二维网格。

我创建了一个列表 biomeProbabilities,其中包含代表不同生物群落的不同索引。在循环浏览地图中的位置时,我检查所选位置的邻居并根据其生物群系调整 biomeProbabilities 中的值。

最后,我将 numpy.random.choice()biomeListbiomeProbabilities 结合使用,使用每个项目的给定概率从 biomeList 中做出选择。


我的问题

如何确保biomeProbabilities中每一项的总和都等于1(这样numpy.random.choice就可以随机选择)?我看到了两个合乎逻辑的解决方案:

a) 分配新的概率,使排名最高的生物群落 0.8,然后是第二个 0.4 和第三个 0.2

b) 每一项加减等量,直到总和 == 1

哪个选项(如果有)会更好,我将如何实施?

此外,有没有更好的方法来获得结果,而无需诉诸我在这里使用的无穷无尽的 if 语句?

这听起来像是解决问题的复杂方法。你很难让它以这种方式工作,因为你将自己限制在一次前向传球中。

你可以做到这一点的一种方法是选择一个随机位置来开始一个生物群落,然后 "expand" 它以一定的概率(比如 0.9)进入邻近的补丁。

(请注意,您的示例中存在代码错误,第 10 行——您必须复制内部列表)

import random
import sys


W = 78
H = 40

BIOMES = [
    ('#', 0.5, 5),
    ('.', 0.5, 5),
]

area_map = []

# Make empty map
inner_list = []
for i in range(W):
    inner_list.append(' ')
for i in range(H):
    area_map.append(list(inner_list))

def neighbors(x, y):
    if x > 0:
        yield x - 1, y
    if y > 0:
        yield x, y - 1
    if y < H - 1:
        yield x, y + 1
    if x < W - 1:
        yield x + 1, y

for biome, proba, locations in BIOMES:
    for _ in range(locations):
        # Random starting location
        x = int(random.uniform(0, W))
        y = int(random.uniform(0, H))

        # Remember the locations to be handled next
        open_locations = [(x, y)]
        while open_locations:
            x, y = open_locations.pop(0)

            # Probability to stop
            if random.random() >= proba:
                continue

            # Propagate to neighbors, adding them to the list to be handled next
            for x, y in neighbors(x, y):
                if area_map[y][x] == biome:
                    continue
                area_map[y][x] = biome
                open_locations.append((x, y))

for y in range(H):
    for x in range(W):
        sys.stdout.write(area_map[y][x])
    sys.stdout.write('\n')

当然,更好的方法是使用 Perlin noise 函数,通常用于此类任务(例如在 Minecraft 中)。如果特定区域的值高于某个阈值,则使用其他生物群系。优点是:

  • 惰性生成:你不需要提前生成整个区域地图,当你真正需要知道该区域时,你确定该区域是什么类型的生物群落
  • 看起来更逼真
  • Perlin 为您提供真实值作为输出,因此您可以将其用于更多事情,例如地形高度,或混合多个生物群落(或者您可以将其用于 "wetness",有 0-20%沙漠,20-60%是草,60-80%是沼泽,80-100%是水)
  • 您可以叠加多个 "sizes" 噪音,例如通过简单地将它们相乘来为您提供每个生物群落的细节

我提议:

biomeProbabilities = biomeProbabilities / biomeProbabilities.sum()

对于无穷无尽的 if 语句,我建议使用预先分配的方向数组,例如:

directions = [(-1, -1), (0, -1), (1, -1),
              (-1,  0),          (1,  0),
              (-1,  1), (0,  1), (1,  1)]

并用它来迭代,例如:

for tile_x, tile_y in tiles:
    for x, y in direction:
        neighbor = map[tile_x + x][tile_y + y]

@remram 很好地回答了您可能会或可能不会用来生成地形的算法,所以我不会讨论这个主题。