HyperLogLog
概要
HyperLogLog は多重集合 (multiset) における異なりの数問題 (distinct-count problem) を概算するための確率的アルゴリズム。つまり同じ値が複数存在するデータセットから値の種類の数を概算する。異なりの数 (distinct count) とは SQL で COUNT(DISTINCT item) に相当する数であり、例えば集合
元の Flajolet らの論文 [2] では、多重集合における異なりの数を指してカーディナリティ (cardinality) と言葉を使用しているが、集合論の文脈でのカーディナリティ (濃度) とは重複分を含む要素数を意味している。例えば多重集合 {a,a,b,c,c,c} の濃度は 6 だが異なりの数は 3 である。混乱を避けるためこの記事ではカーディナリティという言葉は使用しない。
重複値を持つデータセットから正確な異なりの数
HyperLogLog は特に大規模データセットでの集計や分散ストリーミング分析に適していることから、Redis, Spark, Redshift といった分散システムで利用することができる。例えば Redshift では SELECT APPROXIMATE COUNT(item) ... とすることで HyperLogLog を使った集計処理を実行する。
Table of Contents
アルゴリズム
HyperLogLog の考え方の基礎となる確率カウント法、確率的平均化、LogLog について順番に説明する。
確率的カウント法
確率的カウント法 (probabilistic counting) または Flajolet-Martine アルゴリズム (1985) [1] は大規模データセットにおいて異なりの数を効率的に推定するためのアルゴリズムである。データセット (多重集合, データストリーム) に含まれている各要素をハッシュ化し、そのハッシュ値のビット列で最初に 1 が出現する位置の最も大きなものから、データセットに含まれている異なりの数を推定する。この考え方は HyperLogLog の基礎となっている。
入力
データセット
さて、ハッシュ値が均一に分散しているということは、ハッシュ値の各ビットは 0 と 1 が等しい確率で出現するということと等価である。つまり、ハッシュ値の最下位ビットパターンが 1 となる確率は
前出の
単純に考えると「確率
ただし現実的には
確率的カウント法のアプローチ (省略された説明)
確率カウント法に基づいて推定アルゴリズムを組み立てると以下のようになる。
長さ
のビット列 を用意する。初期状態はすべてのビットが 0 である。 データセット
に含まれるすべての要素 に対して、 の 番目のビットを 1 に設定する; つまり とする。
を となる最も小さい とする (例えば であれば )。このとき の異なりの数はおおよそ である。ここで とする。
データセット
たまたま
しかしそれでも確率的カウント法は外れ値の影響を受けやすく、実際には結果の分散が大きくなることが問題となる。例えば以下の実装で N を様々な値に変えてみると、誤差 5% 程度となることもあれば 60% や 80% を超えることもある。
import scala.util.hashing.MurmurHash3
// y の k ビット目を返す
def bit(y: Long, k: Int): Int = ((y >> k) & 1).toInt
// 最下位ビットから連続する 0 ビットの数を返す
def ρ(y: Long): Int = (0 to 31).find(i => bit(y, i) == 1).getOrElse(31)
// 31 ビット幅のハッシュ関数
def h(x: Int): Int = MurmurHash3.bytesHash(BigInt(x).toByteArray) & 0x7FFFFFFF
val N = 100000 // 異なりの数
val M = 0 until N // 重複集合 (実際には重複していないが)
val z = M.foldLeft(0L) { case (z, x) => z | (1 << ρ(h(x))) }
val r = (0 to 31).find(i => bit(z, i) == 0).getOrElse(-1)
val n = math.pow(2, r) / 0.77351
val err = math.abs(n - N) / N
System.out.printf("z=%s, r=%d, n=%f, err=%f\n", z.toBinaryString, r, n, err)
z=1111111111111111, r=16, n=84725.472198, err=0.152745 このような外れ値の影響を軽減するために LogLog や HyperLogLog では部分集合に分けて独立して算出した値の調和平均を取る方法などが検討される。
LogLog
確率的カウント法は
まずデータセット
次に各部分集合の
この LogLog の標準誤差 (相対誤差) は
HyperLogLog
HyperLogLog アルゴリズムでは幾何平均ではなく調和平均 (harmonic mean) を使用することで LogLog から外れ値の問題をさらに改善している。各部分集合の推定値は調和平均を用いて次のように表される。
直感的には、各部分集合に
HyperLogLog の現実の適用ではさらに補正が必要である。例えばクーポンコレクター問題
から
誤差と容量の見積もり
HyperLogLog の標準誤差は
異なりの数の最大値を
例えば非常に大きな異なりの数の最大値として
実験的洞察から LogLog や HyperLogLog の分布は正規分布に似た形になっていることが分っている [3]。これらは標準誤差
和集合の推定
HyperLogLog を使用して集合
例えばある駅の改札を IC 乗車券で通過したユニーク旅客数を 1 日ごとに HyperLogLog で推定していたとする。このとき、推定に使用した各レジスタ
例1. での HyperLogLog での推定値の算出
アルゴリズムを図で説明する。Fig 1 は異なりの数 20 を持つ集合
例2. での HyperLogLog での推定値の算出
HyperLogLog のアルゴリズムをプログラムにすると以下のようになる (論文にあるような最後の補正は行っていない)。このコードでは異なりの数
import scala.util.hashing.MurmurHash3
object HyperLogLogExample extends App {
val b = 14
val m = 1 << b
val alpha14 = 0.721
val M = Array.fill(m)(0) // 各サブストリームの ρ の最大値を保持するレジスタ
def hash(value: String): Int = MurmurHash3.stringHash(value)
def leadingZeros(x: Int): Int = {
val binary = x.toBinaryString.reverse.padTo(32, '0').reverse
val i = binary.indexOf('1')
return if (i >= 0) i else binary.length
}
// データストリームの例 (実際の異なりの数は 100000)
val n = 100000
val dataStream = (0 until n).map(_.toString)
// 各要素を HyperLogLog に追加
for (v <- dataStream) {
val h = hash(v)
val j = (h >> (32 - b)) & (m - 1) // 上位 b ビットを取得
val r = leadingZeros(h << b) + 1
M(j) = math.max(M(j), r)
}
// 異なりの数を推定
val Z = 1.0 / M.map(count => 1.0 / (1 << count)).sum
val E = alpha14 * m * m * Z
println(s"Estimated number of distinct elements: $E")
}
Estimated number of distinct elements: 100129.57268650533
参考文献
- Philippe Flajolet, G. Nigel Martine. Probabilistic Counting Algorithms for Data Base Applications. Journal of Computer and System Sciences. 31 (2): 182–209. 1985.
- Philippe Flajolet, Éric Fusy, Olivier Gandouet, Frédéric Meunier. HyperLogLog: the analysis of a near-optimal cardinality estimation algorithm. AofA: Analysis of Algorithms, Jun 2007, Juan les Pins, France. pp.137-156, ⟨10.46298/dmtcs.3545⟩. ⟨hal-00406166v2⟩
- Dzejla Medjedovic, Emin Tahirovic, Ines Dedovic. 大規模データセットのためのアルゴリズムとデータ構造. マイナビ出版 (2024)
