【问题标题】:Zipf's Law in Java for text generation - too slowJava 中用于文本生成的 Zipf 定律 - 太慢了
【发布时间】:2014-11-24 13:10:54
【问题描述】:

嘿,我正在开发一个文本生成器,它应该会生成数百万个不同的文本。 为了使每个文本的内容逼真,我使用了 Zipf 定律 运行良好,字分布正确。

但是下面的next() 函数执行速度很慢,因为我想生成数百万篇文章,所以必须对其进行更改。 (while循环是慢的部分)

有人可以帮我解决这个问题吗?

我是这样实现的:

   public int next() {

    int rank;
    double frequency = 0;
    double dice;

    rank = rnd.nextInt(size);
    frequency = (1.0d / Math.pow(rank, this.skew)) / this.bottom;
    dice = rnd.nextDouble();


    while (!(dice < frequency) || (rank == 0)) {
        rank = rnd.nextInt(size);
        frequency = (1.0d / Math.pow(rank, this.skew)) / this.bottom;
        dice = rnd.nextDouble();
    }

    return rank;
}

编辑:我从http://diveintodata.org/2009/09/13/zipf-distribution-generator-in-java/获得代码

【问题讨论】:

  • 猜猜:你在 Linux 上运行 1.7 或更低的 JVM,而你的 rnd 是 SecureRandom?
  • @fge rnd 不是 SecureRandom
  • 发布完整的代码,这样人们就可以编译它而无需自己填写位。 while 循环只有 3 行。检查哪个函数需要多长时间才能执行。除非 java 的 RNG 有什么可疑之处,否则可能是数学运算需要很长时间。
  • 对于完整代码,您可以从我在 EDIT 上面发布的网站上获取代码。瓶颈是(内)while循环?我尝试使用 fastmath 而不是数学,但代码执行速度慢了 1/3..

标签: java performance power-law


【解决方案1】:

您复制的实现......有一些问题。有人可能会说这显然是错误的,因为它使用的是随机值,并且在像

这样的计算中
rank = rnd.nextInt(size);
friquency = (1.0d / Math.pow(rank, this.skew)) / this.bottom;

rank 的值是0,那么频率是Infinity,并弄乱了一些统计数据。

我试图纠正这些错误,但是没有分析了实现并且没有将其与 Zipf 分布函数的定义进行了比较.所以如果有人复制我的代码,他可能会发现它仍然“......有一些问题”


next 函数的实现,严格来说,不是“total correct”,因为它不一定终止。没有什么可以阻止循环永远运行。根据参数的不同,它可能或多或少需要一段时间才能终止。而且我认为这也是您的“性能”问题的主要原因之一:对于某些值,(dice &lt; frequency) 的条件不太可能发生......


不管怎样,您想要实现的目标可以更通用地表述:您有一定的概率分布。你想要一个“随机”函数,它根据这个分布返回随机值。

实现此目的的一种简单而通用的方法是使用NavigableMap 将(累积的)概率分布映射到目标值。然后可以使用此映射快速查找目标值,给定一个介于 0.0 和 1.0 之间的随机值,该值由 java.util.Random 实例提供。

对于特定情况可能有更有效的解决方案,但同样:这是非常通用和简单的(并且仍然相当有效)。


我在这里为 Zipf 发行版实现了这一点。同样,我没有详细验证所有内容,并且有一些 +1/-1 怪事(在第一段中提到),但它应该表明这个想法:FastZipfGenerator 填充了包含概率分布的地图,并且在next() 函数中,只需执行查找:

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Random;
import java.util.TreeMap;

public class ZipfGeneratorTest
{
    public static void main(String[] args) {

        int size = 10;
        double skew = 2.0;

        ZipfGenerator z0 = new ZipfGenerator(size, skew);
        FastZipfGenerator z1 = new FastZipfGenerator(size, skew);

        long before = 0;
        long after = 0;

        int n = 5000000;

        before = System.nanoTime();
        Map<Integer, Integer> counts0 = computeCounts(z0, size, n);
        after = System.nanoTime();
        System.out.println(counts0+", duration "+(after-before)/1e6);

        before = System.nanoTime();
        Map<Integer, Integer> counts1 = computeCounts(z1, size, n);
        after = System.nanoTime();
        System.out.println(counts1+", duration "+(after-before)/1e6);
    }

    private static Map<Integer, Integer> computeCounts(
        ZipfGenerator z, int size, int n)
    {
        Map<Integer, Integer> counts = new LinkedHashMap<Integer, Integer>();
        for (int i=1; i<=size; i++)
        {
            counts.put(i, 0);
        }
        for (int i=1; i<=n; i++)
        {
            int k = z.next();
            counts.put(k, counts.get(k)+1);
        }
        return counts;
    }

    private static Map<Integer, Integer> computeCounts(
        FastZipfGenerator z, int size, int n)
    {
        Map<Integer, Integer> counts = new LinkedHashMap<Integer, Integer>();
        for (int i=1; i<=size; i++)
        {
            counts.put(i, 0);
        }
        for (int i=1; i<=n; i++)
        {
            int k = z.next();
            counts.put(k, counts.get(k)+1);
        }
        return counts;
    }

}

// Based on http://diveintodata.org/tag/zipf/
class ZipfGenerator {
    private Random rnd = new Random(0);
    private int size;
    private double skew;
    private double bottom = 0;

    public ZipfGenerator(int size, double skew) {
        this.size = size;
        this.skew = skew;

        for(int i=1;i <=size; i++) {
            this.bottom += (1/Math.pow(i, this.skew));
        }
    }

    // the next() method returns an random rank id.
    // The frequency of returned rank ids are follows Zipf distribution.
    public int next() {
        int rank;
        double friquency = 0;
        double dice;

        rank = rnd.nextInt(size)+1;
        friquency = (1.0d / Math.pow(rank, this.skew)) / this.bottom;
        dice = rnd.nextDouble();

        while(!(dice < friquency)) {
            rank = rnd.nextInt(size)+1;
            friquency = (1.0d / Math.pow(rank, this.skew)) / this.bottom;
            dice = rnd.nextDouble();
        }

        return rank;
    }


    // This method returns a probability that the given rank occurs.
    public double getProbability(int rank) {
        return (1.0d / Math.pow(rank, this.skew)) / this.bottom;
    }
}



class FastZipfGenerator
{
    private Random random = new Random(0);
    private NavigableMap<Double, Integer> map;

    FastZipfGenerator(int size, double skew)
    {
        map = computeMap(size, skew);
    }

    private static NavigableMap<Double, Integer> computeMap(
        int size, double skew)
    {
        NavigableMap<Double, Integer> map = 
            new TreeMap<Double, Integer>();

        double div = 0;
        for (int i = 1; i <= size; i++)
        {
            div += (1 / Math.pow(i, skew));
        }

        double sum = 0;
        for(int i=1; i<=size; i++)
        {
            double p = (1.0d / Math.pow(i, skew)) / div;
            sum += p;
            map.put(sum,  i-1);
        }
        return map;
    }

    public int next()
    {
        double value = random.nextDouble();
        return map.ceilingEntry(value).getValue()+1;
    }

}

它会打印随机样本结果(基本上是“直方图”)和一些计时结果。计时结果类似于

duration 6221.835052
duration 304.761282

表明它很可能会更快(尽管这不应被视为“基准”......)

【讨论】:

  • 非常感谢您详细的回复和解释一切的努力!我明天试试,然后标记你的答案解决了!
  • 如果“排名值为 0,则频率为 Infinity”,但没有人应该关心,因为循环在 Infinity 可以使用之前终止。
  • @maaartinus 在我的特殊情况下,排名 0 的问题非常重要,因为它弄乱了我文本中的单词分布! - 我没有进一步分析它,但我最常用的词经常出现在生成的文本中——Marco13 再次感谢你,你很友善!在您的帮助下,我获得了巨大的性能提升并学到了一些东西!
  • @maaartinus 在我的测试中,结果分布(直方图)与概率分布的实际函数值不匹配,防止“无穷大”情况(只需加 1)解决了这个问题。但除此之外我还没有验证过其他方法。
  • @Marco13 但是加 1 不仅会阻止Inf,它还只是计算其他东西。这可能是正确的,我不知道。把原版和我的对比一下,也避免了Inf;它们必须以相同的方式工作。
【解决方案2】:

您从https://diveintodata.org/2009/09/13/zipf-distribution-generator-in-java/ 获得的源代码有一些错误。

这里有快速修复。 (1) 在构造函数 ZipfGeneator(int,double) 中,确保使用等号计算最大大小。

public ZipfGenerator(int size, double skew) {
  this.size = size;
  this.skew = skew;

  for(int i=1;i <= size; i++) {
  this.bottom += (1/Math.pow(i, this.skew));
  }
 }

(2) 替换

rank = rnd.nextInt(size); 

rank = rnd.nextInt(size)+1; 

这里是完整的源代码。

import java.util.Random;

//credit: https://diveintodata.org/2009/09/13/zipf-distribution-generator-in-java/ [Online; December 2017]

public class ZipfGenerator {
 private Random rnd = new Random(System.currentTimeMillis());
 private int size;
 private double skew;
 private double bottom = 0;

 public ZipfGenerator(int size, double skew) {
  this.size = size;
  this.skew = skew;

  for(int i=1;i <= size; i++) {
  this.bottom += (1/Math.pow(i, this.skew));
  }
 }

 // the next() method returns an random rank id.
 // The frequency of returned rank ids are follows Zipf distribution.
 public int next() {
   int rank;
   double friquency = 0;
   double dice;

   rank = rnd.nextInt(size)+1;
   friquency = (1.0d / Math.pow(rank, this.skew)) / this.bottom;
   dice = rnd.nextDouble();

   while(!(dice < friquency)) {
     rank = rnd.nextInt(size)+1;
     friquency = (1.0d / Math.pow(rank, this.skew)) / this.bottom;
     dice = rnd.nextDouble();
   }

   return rank;
 }

 // This method returns a probability that the given rank occurs.
 public double getProbability(int rank) {
   return (1.0d / Math.pow(rank, this.skew)) / this.bottom;
 }

 public static void main(String[] args) {
   if(args.length != 2) {
     System.out.println("usage: ./zipf size skew");
     System.exit(-1);
   }

   ZipfGenerator zipf = new ZipfGenerator(Integer.valueOf(args[0]),
   Double.valueOf(args[1]));
   for(int i= 1;i <= 10; i++) {
     System.out.println(i+" "+zipf.getProbability(i));
   }
   //use size = 10 and skew = 2 for testing below
   int hist [] = new int [12];
   for(int i=0;i<12;i++) {
       hist[i] = 0;
   }
   System.out.println("Testing the probability distribution:");
   int sum = 0;
    for(int i= 1;i <= 1000000; i++) {
        hist[zipf.next()]++; 
   }
   for(int i=0;i<12;i++)
     System.out.println(i+" "+hist[i]/1000000.0);
    }

}

结果:

Probability distribution from the formula:
1 0.6452579827864142
2 0.16131449569660355
3 0.07169533142071269
4 0.04032862392415089
5 0.02581031931145657
6 0.017923832855178172
7 0.013168530260947229
8 0.010082155981037722
9 0.007966147935634743
10 0.006452579827864143
Testing the probability distribution from sampling:
0 0.0
1 0.645813
2 0.160766
3 0.071527
4 0.040346
5 0.026039
6 0.01801
7 0.013215
8 0.009953
9 0.007863
10 0.006468
11 0.0

注意,0 和 11 的概率为 0,正如预期的那样。

【讨论】:

    【解决方案3】:

    您问的是速度,所以我提出了一个小的优化。首先,摆脱重复的东西,看看它的全部内容:

    public int next() {
        while (true) {
            int rank = rnd.nextInt(size);
            if (rank == 0) return return rank;
            double frequency = (1.0d / Math.pow(rank, this.skew)) / this.bottom;
            double dice = rnd.nextDouble();
            if (dice < frequency) return rank;
        }
    }
    

    到目前为止,它的工作方式应该完全相同(除非我忽略了某些东西)。我将rank 的测试向上移动,因为如果它为零,以下计算将毫无用处。现在有一条线我们可以加快速度

    double frequency = Math.pow(rank, -this.skew) * inverseBottom;
    

    实际上,由于舍入错误,这可能会稍微改变结果,但我怀疑你应该关心。如果rank 保持不变,您可以将pow 转换为exp 以使其更快,但事实并非如此。对于一个小的size,您可以预先计算一个ln(rank) 的表并像这样使用它

    double frequency = Math.exp(ln[rank] * -this.skew) * inverseBottom;
    

    一个更好的算法肯定会给你带来比这种低级优化更多的东西。

    【讨论】:

      猜你喜欢
      • 2015-04-07
      • 1970-01-01
      • 1970-01-01
      • 2017-05-17
      • 2018-05-03
      • 1970-01-01
      • 1970-01-01
      • 2019-02-12
      • 2013-08-24
      相关资源
      最近更新 更多