直交表の数理 ~ 有限射影幾何を用いた直交表の構築編 ~
前回の記事で有限射影空間の理論を用いた行列の構築について説明した。本稿では前回説明した手法で正しく直交表が構築できることを証明し、実際に直交表を構築するプログラム例を示したいと思う。
有限射影幾何を用いた直交表構築手法の証明
おさらい
を因子数、を直交表の強さ、を素数冪とする。行列の行列があって、その任意の () 行は上の次元ベクトル空間上で一次独立であるとする。このとき、行列の行列を用いて、直交表はという行列積により得られるとのことだった(詳細は前回の記事を参照)。以下ではこの事実の証明を考えていく。
事前に断らせて頂くと、証明はほぼ全て私が考えたものだが、有限体上のベクトル空間の扱いにあまり自信がない。有限体上のベクトル空間では、例えば内積が0でもベクトルが直交しないようなケースがあるなど、標数0の体で使える議論が使えないケースがある[1]。もしそういった点で誤りがあれば、Twitterやブログコメントにてご指摘頂けるとありがたい。
証明
を上の次元ベクトル空間上の行ベクトルの集合とし、これらのうち任意の個を選べばそれらは一次独立になっているとする。これを用いては以下のように書ける。
同様にを上の列ベクトルの集合とし、これらは相異なるとする。ベクトルの数がであるため、これらのベクトルはのベクトル全体の集合となる。これを用いては以下のように書ける。
これより、行列積は以下のようになる。
示したいことは、の任意の行を選んだとき、それらの各列の値から順序集合を形成したとき、同じ順序集合がちょうど回ずつ現れることである。というのも、列は全部で個あり、可能な順序集合のパターンは全部で通りとなるため、個の値の組がそれぞれ回ずつ出現すれば直交表としての性質を満たすためである。
以下、選ばれた個の行の番号をとする。
まず、全ての順序集合が回未満しか出現しないと仮定する。このとき、可能な順序集合の組み合わせが通りしかないため、最大でも個の列しか存在できなくなってしまう。そのため仮定は誤りで、少なくとも1つの順序集合は回以上出現する。
一番出現回数が多い順序集合について、それが出現する列がちょうど個であることを示す。そのために、このような列が個あると仮定して矛盾を導く。今、という番号の列について、行目の値から成る順序集合が一致したと仮定する。この時、について以下の式が成り立つ。
ただし、である。
についても同様に以下の式が得られる。
この関係を線形写像の観点から整理してみる。そのために以下のような行列を考える。
は一次独立なので、である。そのため、次元定理よりとなる。なので、はにおいて全て同一の次元空間に含まれる。そこで、のベクトル空間としての基底をとすると、は全て以下の形式で表すことができる。
ただし、である。これらの係数はの通りの値を取るため、この形式で表されるベクトルは全部で本となる。これはが相異なる個のベクトルであることに矛盾する。
よって仮定は誤りで、一番出現回数が多い列は個または個以上ということになる。後者は個の場合と同様の理由でありえないため、結局列数は個ということになる。
今考えた一番出現回数が多い順序集合が出現する列を除いて、他の列について同様の議論を繰り返すことで、結局全ての順序集合の出現回数は回と言える。よっては直交表になっている。
計算してみよう
証明ができて気分がすっきりしたところで、実際に計算してみよう。前回の記事で作成したgmat.pyをimportして直交表を構築するプログラム例を以下に示す。
#!/usr/bin/env sage import gmat import sys from sage.all import * def calcThetaMatrix(dim, order): K = GF(order, name="a", modulus="primitive") gamma = K.gen() Theta = calcThetaMatrixHelper(dim, order, gamma, 0, [0 for d in range(dim)]) Theta = Matrix(Theta) return Theta.transpose() def calcThetaMatrixHelper(dim, order, gamma, pos, currentVec): if pos == dim: return [currentVec.copy()] currentVec[pos] = 0 Theta = calcThetaMatrixHelper(dim, order, gamma, pos+1, currentVec) for q in range(1, order): currentVec[pos] = gamma**q Theta += calcThetaMatrixHelper(dim, order, gamma, pos+1, currentVec) return Theta def convertToInteger(oa, order): gf = GF(order, name="a", modulus="primitive") gamma = gf.gen() orderedGf = [0] + [gamma**(val) for val in range(order-1)] # Hasmap to convert elements of GF(q) to corresponding integers gfToInt = {} for i, val in enumerate(orderedGf): gfToInt[val] = i+1 intOa = [] for i in range(oa.nrows()): row = [] for j in range(oa.ncols()): row.append(gfToInt[oa[i][j]]) intOa.append(row) return Matrix(intOa) def isCorrectOA(oa, strength): vectorIndexSet = gmat.genVectorIndex([i for i in range(len(oa[0]))], strength) for vis in vectorIndexSet: tupleCount = {} for row in range(oa.nrows()): rowTuple = tuple([oa[row][val] for val in vis]) if rowTuple in tupleCount: tupleCount[rowTuple] += 1 else: tupleCount[rowTuple] = 1 count = 0 for v in tupleCount.values(): if count == 0: count = v elif count != v: return False return True def main(argc, argv): if argc != 4: print("usage: " + argv[0] + " dim order strength") return dim = int(argv[1]) order = int(argv[2]) strength = int(argv[3]) if order < 2 or dim < 1 or strength < 2: print("error: invalid parameter:") print(" dim={}, order={}, strength={}".format(dim, order, strength)) return if dim + 1 < strength: print("error: dim+1 should not be less than strength.") return G = gmat.calcGMatrix(dim, order, strength) print("G matrix:") print(G) Theta = calcThetaMatrix(dim+1, order) print("Theta matrix:") print(Theta) oa = G * Theta oa = oa.transpose() intOa = convertToInteger(oa, order) intOa = Matrix(sorted(intOa)) print("Resulting OA:") print(intOa) print("=== Sanity check ===") if isCorrectOA(intOa, strength): print("Correct OA.") else: print("Invalid OA.") if __name__ == "__main__": main(len(sys.argv), sys.argv)
を求めるところ(calcThetaMatrix関数)がややこしいが、単純なbacktrackアルゴリズムで生成している。
以下に実行例を示す。
shinya@DESKTOP-Q1NSEEU:~/programs/blog/2021% ./pg.py usage: ./pg.py dim order strength shinya@DESKTOP-Q1NSEEU:~/programs/blog/2021% ./pg.py 2 2 2 G matrix: [0 0 1] [1 0 1] [0 1 1] [1 1 1] [0 1 0] [1 1 0] [1 0 0] Theta matrix: [0 0 0 0 1 1 1 1] [0 0 1 1 0 0 1 1] [0 1 0 1 0 1 0 1] Resulting OA: [1 1 1 1 1 1 1] [1 1 2 2 2 2 1] [1 2 1 2 1 2 2] [1 2 2 1 2 1 2] [2 1 1 2 2 1 2] [2 1 2 1 1 2 2] [2 2 1 1 2 2 1] [2 2 2 2 1 1 1] === Sanity check === Correct OA. shinya@DESKTOP-Q1NSEEU:~/programs/blog/2021% ./pg.py 2 2 3 G matrix: [0 0 1] [1 0 1] [0 1 1] [1 1 1] Theta matrix: [0 0 0 0 1 1 1 1] [0 0 1 1 0 0 1 1] [0 1 0 1 0 1 0 1] Resulting OA: [1 1 1 1] [1 1 2 2] [1 2 1 2] [1 2 2 1] [2 1 1 2] [2 1 2 1] [2 2 1 1] [2 2 2 2] === Sanity check === Correct OA. shinya@DESKTOP-Q1NSEEU:~/programs/blog/2021% ./pg.py 3 2 2 G matrix: [0 0 0 1] [1 0 0 1] [0 1 0 1] [1 1 0 1] [0 0 1 1] [1 0 1 1] [0 1 1 1] [1 1 1 1] [0 0 1 0] [1 0 1 0] [0 1 1 0] [1 1 1 0] [0 1 0 0] [1 1 0 0] [1 0 0 0] Theta matrix: [0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1] [0 0 0 0 1 1 1 1 0 0 0 0 1 1 1 1] [0 0 1 1 0 0 1 1 0 0 1 1 0 0 1 1] [0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1] Resulting OA: [1 1 1 1 1 1 1 1 1 1 1 1 1 1 1] [1 1 1 1 2 2 2 2 2 2 2 2 1 1 1] [1 1 2 2 1 1 2 2 1 1 2 2 2 2 1] [1 1 2 2 2 2 1 1 2 2 1 1 2 2 1] [1 2 1 2 1 2 1 2 1 2 1 2 1 2 2] [1 2 1 2 2 1 2 1 2 1 2 1 1 2 2] [1 2 2 1 1 2 2 1 1 2 2 1 2 1 2] [1 2 2 1 2 1 1 2 2 1 1 2 2 1 2] [2 1 1 2 1 2 2 1 2 1 1 2 2 1 2] [2 1 1 2 2 1 1 2 1 2 2 1 2 1 2] [2 1 2 1 1 2 1 2 2 1 2 1 1 2 2] [2 1 2 1 2 1 2 1 1 2 1 2 1 2 2] [2 2 1 1 1 1 2 2 2 2 1 1 2 2 1] [2 2 1 1 2 2 1 1 1 1 2 2 2 2 1] [2 2 2 2 1 1 1 1 2 2 2 2 1 1 1] [2 2 2 2 2 2 2 2 1 1 1 1 1 1 1] === Sanity check === Correct OA. shinya@DESKTOP-Q1NSEEU:~/programs/blog/2021% ./pg.py 2 4 3 G matrix: [ 0 0 1] [ a 0 1] [ 0 a 1] [ a a 1] [ a 1 0] [a + 1 1 0] Theta matrix: [ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 a a a a a a a a a a a a a a a a a + 1 a + 1 a + 1 a + 1 a + 1 a + 1 a + 1 a + 1 a + 1 a + 1 a + 1 a + 1 a + 1 a + 1 a + 1 a + 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1] [ 0 0 0 0 a a a a a + 1 a + 1 a + 1 a + 1 1 1 1 1 0 0 0 0 a a a a a + 1 a + 1 a + 1 a + 1 1 1 1 1 0 0 0 0 a a a a a + 1 a + 1 a + 1 a + 1 1 1 1 1 0 0 0 0 a a a a a + 1 a + 1 a + 1 a + 1 1 1 1 1] [ 0 a a + 1 1 0 a a + 1 1 0 a a + 1 1 0 a a + 1 1 0 a a + 1 1 0 a a + 1 1 0 a a + 1 1 0 a a + 1 1 0 a a + 1 1 0 a a + 1 1 0 a a + 1 1 0 a a + 1 1 0 a a + 1 1 0 a a + 1 1 0 a a + 1 1 0 a a + 1 1] Resulting OA: [1 1 1 1 1 1] [1 1 2 2 4 4] [1 1 3 3 2 2] [1 1 4 4 3 3] [1 2 1 2 2 3] [1 2 2 1 3 2] [1 2 3 4 1 4] [1 2 4 3 4 1] [1 3 1 3 3 4] [1 3 2 4 2 1] [1 3 3 1 4 3] [1 3 4 2 1 2] [1 4 1 4 4 2] [1 4 2 3 1 3] [1 4 3 2 3 1] [1 4 4 1 2 4] [2 1 1 2 3 2] [2 1 2 1 2 3] [2 1 3 4 4 1] [2 1 4 3 1 4] [2 2 1 1 4 4] [2 2 2 2 1 1] [2 2 3 3 3 3] [2 2 4 4 2 2] [2 3 1 4 1 3] [2 3 2 3 4 2] [2 3 3 2 2 4] [2 3 4 1 3 1] [2 4 1 3 2 1] [2 4 2 4 3 4] [2 4 3 1 1 2] [2 4 4 2 4 3] [3 1 1 3 4 3] [3 1 2 4 1 2] [3 1 3 1 3 4] [3 1 4 2 2 1] [3 2 1 4 3 1] [3 2 2 3 2 4] [3 2 3 2 4 2] [3 2 4 1 1 3] [3 3 1 1 2 2] [3 3 2 2 3 3] [3 3 3 3 1 1] [3 3 4 4 4 4] [3 4 1 2 1 4] [3 4 2 1 4 1] [3 4 3 4 2 3] [3 4 4 3 3 2] [4 1 1 4 2 4] [4 1 2 3 3 1] [4 1 3 2 1 3] [4 1 4 1 4 2] [4 2 1 3 1 2] [4 2 2 4 4 3] [4 2 3 1 2 1] [4 2 4 2 3 4] [4 3 1 2 4 1] [4 3 2 1 1 4] [4 3 3 4 3 2] [4 3 4 3 2 3] [4 4 1 1 3 3] [4 4 2 2 2 2] [4 4 3 3 4 4] [4 4 4 4 1 1] === Sanity check === Correct OA.
上記のプログラムではを転置しての計算結果を出力している点、及び入力する次元の値は射影空間の次元の値としている (つまりの値を入力する仕様である) 点に注意されたい。
また、有限体の元をそのまま出力すると分かりづらいので、以前の記事と同じ方法で整数値に変換している。その後、行を昇順にsortしたものを最終出力としている。
今回も最後に正しく直交表が生成できているかチェックしているが、全てパスしている。
ちなみに
最近気づいたのだが、実はsageには直交表を直接的に求めるための関数が存在している[2]。「なんだ、こんな便利な関数があるのか」とガックリしたのだが、少し触ってみたところ、指数2以上の直交表は構築できないような挙動をしている。単なる私の理解不足かもしれないが、なんだかいまいちな仕様であり、本稿で作ったプログラムも無意味ではなかったかなと思っている。
以下に実行例を示す。第1引数が因子数、第2引数が水準数で、というパラメータに強さの値を与えればよい。
sage: for row in designs.orthogonal_arrays.build(2, 2, t=2): ....: print(row) ....: [0, 0] [0, 1] [1, 0] [1, 1] sage: for row in designs.orthogonal_arrays.build(3, 2, t=2): ....: print(row) ....: [0, 0, 0] [0, 1, 1] [1, 0, 1] [1, 1, 0] sage: for row in designs.orthogonal_arrays.build(4, 2, t=2): ....: print(row) ....: --------------------------------------------------------------------------- EmptySetError Traceback (most recent call last) <ipython-input-156-349912d12039> in <module> ----> 1 for row in designs.orthogonal_arrays.build(Integer(4), Integer(2), t=Integer(2)): 2 print(row) 3 /usr/lib/python3/dist-packages/sage/combinat/designs/orthogonal_arrays.py in build(k, n, t, resolvable) 2142 2143 """ -> 2144 return orthogonal_array(k,n,t,resolvable=resolvable) 2145 2146 @staticmethod /usr/lib/python3/dist-packages/sage/combinat/designs/orthogonal_arrays.py in orthogonal_array(k, n, t, resolvable, check, existence, explain_construction) 891 if explain_construction: 892 return msg --> 893 raise EmptySetError(msg) 894 895 elif k <= t: EmptySetError: There exists no OA(4,2) as k(=4)>n+t-1=3
まとめ
本稿では有限射影幾何の理論を用いた直交表構築手法の証明を行った。その後、実際に直交表が構築できることをsageを使って確かめた。
もともとは単に直交表の生成アルゴリズムを調べていただけなのだが、有限幾何という非常に面白い分野に出くわしたのは幸運であった。せっかくなので、もう少し有限幾何で遊んでみたいと思う。
また、改めてsageの凄さを思い知らされた。計算機上で高度な数学的な計算が実行できるというのは、確実に数学の研究を加速させる有用なテクノロジーだと思う。こういった分野の歴史や手法について調べてみるのも面白いかもしれない。
参考
直交表の数理 ~ 有限射影幾何を用いたG行列の構築編 ~
前回の記事ではMOLSを用いた直交表の構築方法について説明した。この方法では構築できる直交表のパターンに限界があるため、他のより優れた方法について調べてみた。
直交表の構築方法にはいろいろあるようだが、本稿では私が特に数学的に面白そうだと感じた有限射影幾何を用いた方法について説明したいと思う。ただし、有限射影幾何とはどういったものかという説明だけでもなかなかの文章量になりそうなので、やはり記事をいくつかに分けて説明したいと思う。その走りとして、本稿では手法の概要と有限射影幾何の基礎について述べた後、直交表の構築に必要な行列というものを構築する方法について説明したいと思う。
行列を用いた直交表の構築手法
手法の概要
論文[1]によると、強さの直交表を作るためには、ある性質を持つ行列があればよいらしい。その性質とは「任意の行を選んだとき、それらが一次独立になっている」ということである。もしそのような行列が得られれば、以下のようにして直交表が生成できるとのことだ。
ただし、を因子数とすると、は行列の行列、は行列の行列である。特に、は上の次元ベクトル空間の相異なる元を列ベクトルとして本並べることで得られる行列である。は次元のパラメータであるが、詳細は後述する。
生成される行列は行列となる。この行列を転置すれば、前回の記事と同様のフォーマットの直交表が得られる。すなわち、が求める直交表となる。
疑問点のオンパレードと解決の筋道
さて、これだけ見ると疑問点だらけでわけが分からない。少なくとも、以下のような疑問が湧いてくる。
- 行列はどのようにして求めればよいのか?
- というパラメータがいきなり出てきたが、これは何に基づいてどういう値に設定すべきものなのか?
- なぜが直交表になると言えるのか?
- 構築される直交表はどういうパターンになるか?
これらの疑問について、まずは現時点で私が理解していることの概要について述べる。
まず1点目について、これこそがまさに有限射影幾何が威力を発揮するところである。具体的には、有限体上の射影空間の点を求めて、それらから必要な部分集合を抜き出すことで解決できる。本稿の主題であり、難しいポイントなので詳細は後述する。
2点目について、dは因子数や水準数に依存して決まる。詳細は後述する。
3点目について、これは現時点で証明が見つけられていない。論文[1]で引用されている書籍に説明があるようだが、手元にないため参照できない。というわけで、今回も証明を自力で考えてみようと思うが、現時点での見通しはなく証明できるか分からない。少なくとも、これについては次回以降に回す。
4点目について、まず強さは2~4までいけるようである。5以上でも同様の理屈で行けそうだが、強さに応じて1点目で述べた「射影空間の点から必要な部分集合」なるものを抜き出すステップの計算量が爆発していくのと、実用上あまり必要になるとは思えないため、強さ5以上は考えないことにする。
有限体を利用している関係上、水準数は相変わらず素数冪に限られる。しかし、因子数は自由に選べるというのがこの手法の利点となる。
結局、構築できる直交表の型は (, は素数冪, となる。は他のパラメータに応じて自動的に決まるため、無駄に大きくすることはできるが入力としての自由度は基本的にないものと考えてよい。
有限体上の射影空間
では、行列を求めるための有限射影幾何について考えていこう。具体的には有限体上の射影空間を考えるのだが、先に射影空間の一般論から説明する。
射影空間
射影空間の定義はいろいろなバリエーションがあるが、今回使用するものをWikipedia[2]から引用する。
つまり射影空間とは、互いにスカラー倍の関係にあるベクトル同士を同一視して、それらを改めて点と捉えた空間のことである。元のベクトル空間の次元がなのに対して、そこから得られる射影空間の次元はであることに注意されたい。
また、射影空間における直線というものも考えることができる。まず、元の空間において原点を通る2つの異なる直線があったとして、それらを両方とも含む平面を考える。射影空間上ではは点になるが、この平面が射影空間上では点を結ぶ直線となる。
もとのベクトル空間上での直線を点、平面を直線と捉えるため、少々ややこしい空間であるが、こういうのは慣れるしかない。
有限体の場合
さて、射影空間を得るためにはまずベクトル空間を考える必要があるが、体としては何を選んでもよい。つまり、有限体上のベクトル空間であっても良いわけである。有限体上の射影空間のことを有限射影空間と呼ぶ。
以下では (は素数冪) 上の次元ベクトル空間から得られる次元射影空間をと表記する。
有限射影空間の完全代表系
有限射影空間上の全ての点から完全代表系を求め、それを再び元のベクトル空間の元と考えると、任意の2点から得られる上のベクトルは一次独立となる。これは有限射影空間の定義を考えれば当然である。
有限射影空間上の点の数
上の次元ベクトル空間における点 (ベクトル) の数は個に限られる。さらに、の場合は互いにスカラー倍の関係にある点は同一視されるので、さらに点の数が減ることになる。これは具体的にはいくつになるのだろうか?
次数が2の場合、つまり上の点の数は個になるようである[3]。ちなみに、2次元の射影空間のことを特別に射影平面と呼ぶ。
次数が3以上の場合はどうなるのだろうか?これはGaussian binomial coefficientと呼ばれるものを使って以下のように算出できる[4][5]。
私自身、Gaussian binomial coefficientなどというものは初めて見たのだが、計算の詳細は本稿の主眼ではないため、[5]などを参照されたい。ここではいくつかの具体的なについて、上の点の数がどうなるかをsageで計算してみよう。
sage: for dim in range(2,8): ....: for order in range(2,8): ....: if(is_prime_power(order)): ....: print("The number of points in PG({}, {}) = {}".format(dim, order, q_binomial(dim+1, 1, q=order))) ....: The number of points in PG(2, 2) = 7 The number of points in PG(2, 3) = 13 The number of points in PG(2, 4) = 21 The number of points in PG(2, 5) = 31 The number of points in PG(2, 7) = 57 The number of points in PG(3, 2) = 15 The number of points in PG(3, 3) = 40 The number of points in PG(3, 4) = 85 The number of points in PG(3, 5) = 156 The number of points in PG(3, 7) = 400 The number of points in PG(4, 2) = 31 The number of points in PG(4, 3) = 121 The number of points in PG(4, 4) = 341 The number of points in PG(4, 5) = 781 The number of points in PG(4, 7) = 2801 The number of points in PG(5, 2) = 63 The number of points in PG(5, 3) = 364 The number of points in PG(5, 4) = 1365 The number of points in PG(5, 5) = 3906 The number of points in PG(5, 7) = 19608 The number of points in PG(6, 2) = 127 The number of points in PG(6, 3) = 1093 The number of points in PG(6, 4) = 5461 The number of points in PG(6, 5) = 19531 The number of points in PG(6, 7) = 137257 The number of points in PG(7, 2) = 255 The number of points in PG(7, 3) = 3280 The number of points in PG(7, 4) = 21845 The number of points in PG(7, 5) = 97656 The number of points in PG(7, 7) = 960800
さらっと計算したが、sageは本当になんでも計算できてすごい。上記の結果から、のいずれが増加しても点の数が爆発的に増加する様子が見て取れる。
有限射影空間と行列
行列の求め方
強さ2の場合
さて、話を直交表に戻そう。有限射影空間を用いることで、直ちに強さ2の直交表を求めることができる。というのも、前述の通り射影空間の完全代表系を元のベクトル空間に戻してやれば、そのうちの任意の2ベクトルは互いに一次独立となっており、これはまさに行列の要件を満たすからである。
つまり、射影空間の完全代表系を元のベクトル空間に戻したものを行ベクトルとして縦に並べれば、それが行列となる。
強さ3以上の場合
強さ3以上の場合は、まず強さ2の行列を構築する。その上で、そこから任意の行を選んだ時に、それらが一次独立になるように一生懸命探索して部分集合を選別するしかない。これをまじめにやると計算量が指数オーダーになってしまうので、以下のようなgreedyなアルゴリズムを使うなど、計算の工夫が必要となる。
- 選抜したベクトルの集合を空集合で初期化する。
- 強さ2の行列の行を順に選んで以下を実行する。
- 今注目しているベクトルと現時点で選別されたベクトルの集合を合わせた集合について、そこから任意の個のベクトルを選ぶと一次独立になるかを調べる。YESであれば今のベクトルを選別したベクトルの集合に加える。
ただし、これだと行列の行数を最大化できないということが普通に起こりえる。また、それでなくとも完全代表系の選び方によっては行列の行数を最大化できないケースがあるのではないのか?という疑問もある。しかし、実用上はの行数は多ければ多いほどよいが、必ずしも最大の行数となる必要はない。そのため、手法や計算の容易性のために行列が多少小さくなることは許容できるものと考えている。
計算してみよう
射影空間上の点、及びその完全代表系はsageを使って計算が可能である。また、ベクトルの集合が一次独立であるかどうかもsageを使えば簡単に確かめられる。以下に行列を求めるプログラムを示す。
#!/usr/bin/env sage import sys from sage.all import * def getProjectiveSpacePoints(dim, order): PG = ProjectiveSpace(dim, GF(order, name="a", modulus="primitive")) return PG.rational_points() def calcGMatrix(dim, order, strength): points = getProjectiveSpacePoints(dim, order) # Convert points in PG(dim, order) to vectors in vector space over F_{order} vectors = [vector(p) for p in points] if strength == 2: return Matrix(vectors) else: return Matrix(getLinearIndependentVectors(vectors, order, strength)) def getLinearIndependentVectors(vectors, order, strength): V = VectorSpace(GF(order, name="a", modulus="primitive"), len(vectors[0])) currentSet = [] for v in vectors: if isRowIndependent(Matrix([v] + currentSet), order, strength): currentSet += [v] return currentSet def genVectorIndex(index, numVecs): vectorsSet = [] if len(index) == numVecs: vectorsSet.append([ind for ind in index]) else: for i in range(len(index)-numVecs+1): if numVecs == 1: vectorsSet += [[index[i]]] else: subVectorsSet = genVectorIndex(index[i+1:], numVecs-1) for j in range(len(subVectorsSet)): subVectorsSet[j] = [index[i]] + subVectorsSet[j] vectorsSet += subVectorsSet return vectorsSet def isRowIndependent(G, order, strength): V = VectorSpace(GF(order, name="a", modulus="primitive"), len(G[0])) # If the number of rows of G matrix is relatively small, # all we have to do is just to check the linear independency of all rows of G. if G.nrows() <= strength: if V.linear_dependence([v for v in G]) != []: return False else: # If the dimension of the vector space is too small, # it is impossible to choose linear independent vector set. if G.ncols() < strength: return False vectorIndexSet = genVectorIndex([i for i in range(G.nrows())], strength) for vis in vectorIndexSet: vs = [G[i] for i in vis] if V.linear_dependence(vs) != []: return False return True def main(argc, argv): if argc != 4: print("usage: " + argv[0] + " dim order strength") return dim = int(argv[1]) order = int(argv[2]) strength = int(argv[3]) if order < 2 or dim < 1 or strength < 2: print("error: invalid parameter:") print(" dim={}, order={}, strength={}".format(dim, order, strength)) return if dim + 1 < strength: print("error: dim+1 should not be less than strength.") return G = calcGMatrix(dim, order, strength) print("G matrix:") print(G) print("=== Sanity check ===") if isRowIndependent(G, order, strength): print("Correct G matrix.") else: print("Invalid G matrix.") if __name__ == "__main__": main(len(sys.argv), sys.argv)
以下に実行例を示す。
shinya@DESKTOP-Q1NSEEU:~/programs/blog/2021% ./gmat.py usage: ./gmat.py dim order strength shinya@DESKTOP-Q1NSEEU:~/programs/blog/2021% ./gmat.py 2 2 2 G matrix: [0 0 1] [1 0 1] [0 1 1] [1 1 1] [0 1 0] [1 1 0] [1 0 0] === Sanity check === Correct G matrix. shinya@DESKTOP-Q1NSEEU:~/programs/blog/2021% ./gmat.py 2 2 3 G matrix: [0 0 1] [1 0 1] [0 1 1] [1 1 1] === Sanity check === Correct G matrix. shinya@DESKTOP-Q1NSEEU:~/programs/blog/2021% ./gmat.py 2 2 4 error: dim+1 should not be less than strength. shinya@DESKTOP-Q1NSEEU:~/programs/blog/2021% ./gmat.py 3 3 4 G matrix: [0 0 0 1] [1 0 0 1] [0 1 0 1] [0 0 1 1] [1 1 1 1] === Sanity check === Correct G matrix.
実行時引数として受け取っている次元の値は射影空間の次元としている。これは前述のパラメータよりも1小さい値となっているので注意されたい。
得られた行列は最後に任意の行が一次独立になるかのチェックを行っているが、それは全てパスしている。
次元の決め方
前述した疑問点の2つ目についてここで詳細に考えてみよう。直交表を作るにあたり、各因子は行列の行に対応することになる。行を削る分には構わないが、行が足りないとなった場合には、次元を上げて行数を増やす必要がある。そのため、の値をいろいろと変えたときに行列の行数がどうなるかをあらかじめ調べておき、テスト要件として求められる因子数を満たすような値を指定すればよい。
例として、因子数6、水準数2、強さ3の直交表を構築したいとする。先ほど示したプログラムで計算すると、の行列の行数は4、の行列の行数は8となる。そのため、次数として2では足りず、3であればOKということになる。
まとめ
本稿では有限体上の射影空間の理論を用いた直交表の構築手法について説明した。特に、直交表を構築する上で最も重要となる行列の構築方法にフォーカスした。
本稿で述べた方法によりなぜ直交表が構築できるのかという部分について疑問が残っているため、それについてはできる限り次回に解決するようにしたい。また、最終的に直交表を構築するところまでプログラムを紹介できなかったため、それも次回に説明したいと思う。
参考
[1] 須田 健二, 五味 弘, 有限射影幾何を用いたソフトウェアテスト向けの直交表自動生成プログラムの開発とその応用, 2016, https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=&cad=rja&uact=8&ved=2ahUKEwiAtemPvonwAhVKxYsBHa7IDUwQFjABegQIAxAD&url=https%3A%2F%2Fipsj.ixsq.nii.ac.jp%2Fej%2F%3Faction%3Drepository_uri%26item_id%3D174252%26file_id%3D1%26file_no%3D1&usg=AOvVaw2fjazMjJ9ADLdgt_Y1nTAs
[2] Projective space - Wikipedia
[3] Projective plane - Wikipedia
[4] Finite geometry - Wikipedia
[5] Gaussian binomial coefficient - Wikipedia
直交表の数理 ~ MOLSを用いた直交表の構築編 ~
前回の記事ではmutually orthogonal Latin squares (MOLS) というテーブル群を構築する方法について説明した。本稿ではMOLSを用いて直交表を構築する方法について説明する。
直交表の構築
基本の構築方法
を素数、を1以上の整数、とする。このとき、MOLS()からは () というタイプの直交表が得られる。以下ではMOLS(3)から直交表を得るケースを例にして、直交表の構築方法を説明する。全体的に英語版Wikipedia [1]を参考にした。
まず、直交表の最初の2列を順序対が辞書順になるように埋める。
# | 因子1 | 因子2 | 因子3 | 因子4 |
---|---|---|---|---|
1 | 1 | 1 | ||
2 | 1 | 2 | ||
3 | 1 | 3 | ||
4 | 2 | 1 | ||
5 | 2 | 2 | ||
6 | 2 | 3 | ||
7 | 3 | 1 | ||
8 | 3 | 2 | ||
9 | 3 | 3 |
次に、今埋めた2列の値をそれぞれLatin Squareの行・列と見なして、MOLSに属する各表から値を取り出す。今は例としてMOLS(3)を考えているので、これに属するLatin squareはの2つである。そのため、3列目と4列目をそれぞれの値で埋めればよい。
分かりやすさのためにを具体的に示しておく。
1 | 2 | 3 |
2 | 3 | 1 |
3 | 1 | 2 |
1 | 2 | 3 |
3 | 1 | 2 |
2 | 3 | 1 |
これより、以下のように直交表が得られる。
# | 因子1 | 因子2 | 因子3 | 因子4 |
---|---|---|---|---|
1 | 1 | 1 | 1 | 1 |
2 | 1 | 2 | 2 | 2 |
3 | 1 | 3 | 3 | 3 |
4 | 2 | 1 | 2 | 3 |
5 | 2 | 2 | 3 | 1 |
6 | 2 | 3 | 1 | 2 |
7 | 3 | 1 | 3 | 2 |
8 | 3 | 2 | 1 | 3 |
9 | 3 | 3 | 2 | 1 |
直交表が得られていることの証明
なんだかよくわからないうちに直交表が構築できてしまったように見えるのだが、これは本当に直交表になっているのだろうか?相変わらずWikipedia[1]には証明がないので、自分で考えてみた。なお、記述をシンプルにするため、これ以降はLatin Squareや直交表の行・列の番号は0始まりとする。
上記の方法によって得られるテーブルの行数はとなる。そのため、(強さ2で考えているので)任意の2列を選んで各行から順序対を形成したときに、それらがすべて異なることを示せばよい。そうすれば、ちょうど異なる個の順序対が出尽くすことになるため、指数1の直交表が得られたことになる。
0列目、1列目、及び2列目以降で性質が異なるので、以下の組み合わせのパターンで場合分けをする。
- 0列目と1列目
- 0列目と2列目以降
- 1列目と2列目以降
- 2列目以降同士
準備
各ケースでの証明を始める前に、それぞれの列の行目の値がどのように書き表せるかを最初にまとめておこう。有限体の元は前回の記事に倣ってと書く。
- 0列目:+1
- 1列目:+1
- () 列目:
0列目と1列目
このケースは順序対が辞書順になるように値を決めているので自明である。
0列目と2列目以降
1列目と () 列目について順序対を形成したとき、行目と行目の順序対が一致したと仮定する。すると、以下の2つの等式が成立する。
1つ目の式を2つ目の式に代入して整理すると以下のようになる。
先ほどの1つ目の式よりとなるため、となるためにはとなる他ない。
1列目と2列目以降
2列目と () 列目について順序対を形成したとき、行目と行目の順序対が一致したと仮定する。すると、以下の2つの等式が成立する。
1つ目の式を2つ目の式に代入して整理すると以下のようになる。
2つ目の等号ではよりであることを用いた。
先ほどの1つ目の式よりはの倍数となるため、となるためにはとなる他ない。
2列目以降同士
2列目以降同士の組み合わせであれば、MOLSの定義より互いに直交するLatin squareから順序対を作ることになるため、それらは当然すべて異なる。
以上により、前述の方法で直交表が構築できることが分かった。
MOLSの限界とその他の手法
因子数の減らし方
前述した直交表の構築方法では、MOLSの完全集合の要素をすべて使用した。この場合、因子数がに限定されてしまい、生成できる直交表のパターンがあまりにも限定的である。
実は、因子数を以上に上げることは難しいが、下げることは割と簡単である。単にMOLSの要素から必要な分だけ排除してやれば良いだけである。例えば、先ほど直交表を構築したが、これをにしたければ単にを無視して最終列を削れば良いだけである。
ただし、この場合でも行数を削ることはできない。これは、因子数が3だろうが4だろうが、例えば最初の2行の順序対のパターンを出し尽くすのに必要な行数は確保する必要があるためである。
MOLSの限界
結局、MOLS()から生成できる直交表のパターンは前述のとおり ()となる。これがMOLSを用いた直交表構築法の限界である。具体的には、以下のような直交表は生成することができない。
- 強さが3以上
- 水準数が素数冪でない
- 因子数が以上
直交表を構築してみよう
では、実際に直交表を構築するプログラムを書いてみよう。前回の記事で作成したmols.pyを部品として使用し、直交表を構築するプログラム例を以下に示す。
#!/usr/bin/python3 import mols import sys def calcOrthogonalArray(ml, factor): order = len(ml[0]) height = order**2 oa = [[0 for i in range(factor)] for j in range(height)] for i in range(height): for j in range(factor): if j == 0: oa[i][j] = int(i/order) + 1 elif j == 1: oa[i][j] = i%order + 1 else: latSq = ml[j-2] oa[i][j] = latSq[int(i/order)][i%order] return oa def main(argc, argv): if argc != 3: print("usage: " + argv[0] + " factor level") return factor = int(argv[1]) level = int(argv[2]) oa = [] if factor == 1: # This is a special case oa = [[i+1] for i in range(level)] elif factor > level+1: print("error: factor should be less than or equal to level+1.") else: ml = mols.calcMOLS(level) oa = calcOrthogonalArray(ml, factor) for line in oa: print(line) if __name__ == "__main__": main(len(sys.argv), sys.argv)
このプログラムの実行例を以下に示す。
shinya@DESKTOP-Q1NSEEU:~/programs/blog/2021% ./oa.py 1 1 [1] shinya@DESKTOP-Q1NSEEU:~/programs/blog/2021% ./oa.py 1 3 [1] [2] [3] shinya@DESKTOP-Q1NSEEU:~/programs/blog/2021% ./oa.py 2 3 [1, 1] [1, 2] [1, 3] [2, 1] [2, 2] [2, 3] [3, 1] [3, 2] [3, 3] shinya@DESKTOP-Q1NSEEU:~/programs/blog/2021% ./oa.py 3 3 [1, 1, 1] [1, 2, 2] [1, 3, 3] [2, 1, 2] [2, 2, 3] [2, 3, 1] [3, 1, 3] [3, 2, 1] [3, 3, 2] shinya@DESKTOP-Q1NSEEU:~/programs/blog/2021% ./oa.py 4 3 [1, 1, 1, 1] [1, 2, 2, 2] [1, 3, 3, 3] [2, 1, 2, 3] [2, 2, 3, 1] [2, 3, 1, 2] [3, 1, 3, 2] [3, 2, 1, 3] [3, 3, 2, 1] shinya@DESKTOP-Q1NSEEU:~/programs/blog/2021% ./oa.py 5 3 error: factor should be less than or equal to level+1. shinya@DESKTOP-Q1NSEEU:~/programs/blog/2021% ./oa.py 6 5 [1, 1, 1, 1, 1, 1] [1, 2, 2, 2, 2, 2] [1, 3, 3, 3, 3, 3] [1, 4, 4, 4, 4, 4] [1, 5, 5, 5, 5, 5] [2, 1, 2, 3, 4, 5] [2, 2, 3, 5, 1, 4] [2, 3, 5, 4, 2, 1] [2, 4, 1, 2, 5, 3] [2, 5, 4, 1, 3, 2] [3, 1, 3, 4, 5, 2] [3, 2, 5, 1, 4, 3] [3, 3, 4, 2, 1, 5] [3, 4, 2, 5, 3, 1] [3, 5, 1, 3, 2, 4] [4, 1, 4, 5, 2, 3] [4, 2, 1, 4, 3, 5] [4, 3, 2, 1, 5, 4] [4, 4, 5, 3, 1, 2] [4, 5, 3, 2, 4, 1] [5, 1, 5, 2, 3, 4] [5, 2, 4, 3, 5, 1] [5, 3, 1, 5, 4, 2] [5, 4, 3, 1, 2, 5] [5, 5, 2, 4, 1, 3] shinya@DESKTOP-Q1NSEEU:~/programs/blog/2021% ./oa.py 6 6 /usr/lib/python3/dist-packages/apport/report.py:13: DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses import fnmatch, glob, traceback, errno, sys, atexit, locale, imp, stat Traceback (most recent call last): File "./oa.py", line 42, in <module> main(len(sys.argv), sys.argv) File "./oa.py", line 35, in main ml = mols.calcMOLS(level) File "/mnt/c/Users/shiny/Dropbox/programs/blog/2021/mols.py", line 17, in calcMOLS gf = GF(q, name="a", modulus="primitive") File "sage/structure/factory.pyx", line 367, in sage.structure.factory.UniqueFactory.__call__ (build/cythonized/sage/structure/factory.c:2162) File "/usr/lib/python3/dist-packages/sage/rings/finite_rings/finite_field_constructor.py", line 566, in create_key_and_extra_args raise ValueError("the order of a finite field must be a prime power") ValueError: the order of a finite field must be a prime power
第1引数が因子数、第2引数が水準数である。因子数が以下でなければならなかったり、水準数が素数冪でなければならない制限が見て取れる*1。
まとめ
本稿ではMOLSから直交表を構築する方法について説明した。また、この方法で生成できる直交表のバリエーションの限界について述べた。
現時点では良く分かっていないのだが、MOLSは有限射影幾何なるものと関連があるらしい。次回はそれについて調べてまとめてみたいと思う。ただ、見通しはないのでどうなるかは分からない。調べた結果、私が理解できて、かつ面白いと思ったら記事を書こうと思う。
直交表の数理 ~ MOLSの構築編 ~
世の中には実験計画法というものがある。実験計画法そのものに対して私は特段知見があるわけではないが、これがソフトウェア等のテストパターンを効率的に削減するのに役立つということは以前からなんとなく知っていた。考え方としては、全部のテスト条件の組み合わせをテストするのはパターンが多すぎてやりきれないので、その中から可能な限り互いに異なるものを選別することで、テストのバリエーションを確保しつつ項目数を削減しようというわけだ。
その選別方法にはいくつか手法があるが、その1つが直交表を用いた方法である。直交表の詳細な定義は後述するが、これはある種の制約を持ったテーブルであり、その制約を満たすパターンを見つけるのが難しい。私は当初この生成方法に興味を持ってなんとなく調べていたのだが、その背後には組み合わせ数学、代数学、幾何学など様々な数学との関連があることが分かった。
しかし、実験計画法というのがどちらかといえば実用寄りの話であり、(単なる推測だが)数学的な話題が主眼ではないためか、あまり直交表の数理的側面について詳しく解説された日本語の記事*1をインターネット上で見つけることができなかった。
そこで、本稿から続くいくつかの記事の中で、直交表の数理的側面について自分が理解したことをまとめてみようと思う。手始めに、本稿では直交表の基本的な説明を行った後、直交表を生成するために必要なmutually orthogonal Latin squares (MOLS) と呼ばれるテーブル群の生成方法について説明する。
直交表とは
定義
直交表の定義を日本語版のWIkipedia[1]から引用する。
これはちょっと定義がいまいちである。というのも、列数を2つに限定しているが、実際には3つ以上ということもあり得るからだ。英語版のWikipedia[2]ではこの点も含めた定義になっているので、そちらも引用する。
直交表を決めるパラメータ
直交表にはいくつかパターンがある。そのパターンを決めるためのパラメータが複数あるのだが、それを明示するために直交表をという記号で表す。これは英語版Wikipedia[2]の流儀であり、他にも記法は存在するが、本稿ではこれを採用する。
それぞれのパラメータの意味を以下に示す。
- : 水準数
- : 因子数
- : 強さ
- : 指数
このとき、 テストケース数はとなる。つまり、直交表は行列のテーブルとなる。
これだけだと分かりづらいので例を挙げよう。あるソフトウェアのテストをすることを考える。テスト条件は以下のようであったとする。
ここではテスト条件の項目が4つあるが、これが因子数である。また、各因子について選択肢が3つずつあるが、これが水準数となる。また、4つある因子の各水準の組み合わせとして、例えば異なる2つの組み合わせを網羅したいのであれば、強さは2となる。さらに、直交表ではそれらの組み合わせは必ず同じ回数ずつ出現するが、その回数を表すのが指数となる。例えば各組み合わせを1回ずつテストしたいとすると、上記のテストのための直交表はとなる。
実際の直交表がどうなるかも見ておこう。以下の例はTech Note[3]というWebサイトを参考に作成してみた。
# | OS | CPU | データベース | ブラウザ |
---|---|---|---|---|
1 | 1 | 1 | 1 | 1 |
2 | 1 | 2 | 2 | 2 |
3 | 1 | 3 | 3 | 3 |
4 | 2 | 1 | 2 | 3 |
5 | 2 | 2 | 3 | 1 |
6 | 2 | 3 | 1 | 2 |
7 | 3 | 1 | 3 | 2 |
8 | 3 | 2 | 1 | 3 |
9 | 3 | 3 | 2 | 1 |
ただし、各水準に適当に1~3の数値を割り当てて考えるものとする。例えば、WIndows: 1、Linux: 2、MacOS: 3という感じで考えればよい。
直交表を用いない場合、テスト条件の組み合わせ数は通りとなる。それに比べると大幅にテストケース数を削減できていることが分かるだろう。
強さのパラメータは直交表構築手法に重大な影響を与える。強さ3以上の直交表の構築方法は現時点では難しくて分からないので、しばらくは強さ2で考えることにする。
また、指数は増やすことはできても、ある一定の値以下にすることが不可能な場合があり、実質的には調整可能なパラメータにならないように思われる。例えば[3]で例示されている (本稿の記法では) については、因子数が多いのでどう足掻いてもにすることはできない。
Mutually orthogonal Latin squares (MOLS)
強さ2の直交表を構築するために、まずはmutually orthogonal Latin squares (MOLS) というものを構築する必要がある。以下ではこれについて説明する。
定義
MOLSを理解するためには、先にLatin squareとは何かを知っておく必要がある。Latin squareの定義を英語版Wikipedia[4]より引用する。
これは例を見たほうが早い。以下に次数3のLatin squareの例を示す。
1 | 2 | 3 |
2 | 3 | 1 |
3 | 1 | 2 |
これを見ると、任意の行・任意の列について、1~3の数字がちょうど一回ずつ表れていることが分かる。
Latin squareは同じ次数に対して複数存在する。その中である条件を満たしたものを集めた集合がMOLSである。MOLSの定義を英語版Wikipedia[5]より引用する。
要するに互いに「直交する」Latin squareの集合がMOLSである。引用した定義の中に記載されているが、Latin squre同士が直交するというのは独特の用語であり、ベクトルの直交なんかとはあまり関連がないので注意されたい。
以下に次数4のMOLSの例を示す。これをMOLS(4) のように表記する。
1 | 2 | 3 | 4 |
2 | 1 | 4 | 3 |
3 | 4 | 1 | 2 |
4 | 3 | 2 | 1 |
1 | 2 | 3 | 4 |
4 | 3 | 2 | 1 |
2 | 1 | 4 | 3 |
3 | 4 | 1 | 2 |
1 | 2 | 3 | 4 |
3 | 4 | 1 | 2 |
4 | 3 | 2 | 1 |
2 | 1 | 4 | 3 |
これらのうち2つを選ぶと、それらは互いに「直交」している。例えば、1つ目と2つ目の表について、同じセル同士の数字で順序対を形成すると、各セルに対応する順序対は全て異なる。具体的には以下のようになる。
(1, 1) | (2, 2) | (3, 3) | (4, 4) |
(2, 4) | (1, 3) | (4, 2) | (3, 1) |
(3, 2) | (4, 1) | (1, 4) | (2, 3) |
(4, 3) | (3, 4) | (2, 1) | (1, 2) |
標準形
MOLSにはある種の自由度がある。例えば、属する全ての表について、行や列の入れ替えを行ってもやはりMOLSとなる。このような本質的でない自由度を排除するため、MOLSの標準形というものを考える[5]。
MOLSの標準形の定義を以下に示す。
要するに、標準形とは全ての表の最初の行が自然な順序で並んでおり、かつある1つの表については最初の列もその順序で並んでいるようなMOLSのことである。先ほど示したMOLS(4)の例は標準形になっている。
MOLS()の要素数
MOLS()の標準形の要素数はどうなるだろうか?の場合は自明なので、として考える。このとき、(2, 1)成分の値を考えると、標準形であることから1にはなり得ない。そのため、2〜の通りの値が考えられる。
ここで、(2, 1)成分の値がになるようなLatin squareが2つ以上あると仮定する。すると、それらのうちの2つの表から得られる(2, 1)成分の順序対はとなる。しかし、これは成分の順序対と一致しているため、MOLSであるということに矛盾する。
そのため仮定は誤りで、(2, 1)成分の値がになるようなLatin squareは高々1つ存在する。すなわち、MOLS()の要素数は以下となる。
MOLSの構築方法
準備が整ったので、いよいよMOLSの構築方法について説明する。本稿では次数が素数冪の場合にMOLSの完全集合を求める方法について説明する。内容は[5]を参考にした。なお、記述をシンプルにするため、これ以降は行・列の番号は0始まりとする。
を素数、を1以上の整数、とする。このとき、MOLS()を求める。有限体の乗法群は巡回群であるため (例えば青雪江[6]の命題1.11.38に証明がある) 、ある生成元が存在して、の元は以下のように書ける。
なお、である。
ここで、MOLSの要素に番号をつけてと表す。また、の成分をと表記する。このとき、を以下のようにしてやればMOLSの完全集合が得られる。
正しくMOLSが構築できることの証明
上で示した方法はあまりにもシンプルなのだが、本当に正しいのだろうか?残念ながら英語版Wikipedia[5]に証明がないので、自力で考えてみた。
MOLSの定義に従い、以下を示せばよい。
- 任意のについてはLatin squareであること
- 相異なるを任意に固定したとき、任意のについてとの各セルからなる順序対は互いに異なること
1点目について、まず行を固定する。あるについてであるとすると、両辺からを引いてとなる。よってとなる他ない。
次に列を固定する。あるについてであるとすると、両辺からを引いてとなる。なのでさらに両辺をで割るととなる。よってとなる他ない。
以上により、同一行・同一列の値はすべて異なることが分かったので、はLatin squareである。
2点目について、任意のに対して順序対がすべて異なっていればよい。そこで、ある, ()について2つの順序対およびが等しいと仮定して矛盾を導く。仮定より以下の2つの等式が成立する。
上の式から下の式を引いて整理すると以下のようになる。
もしとすると、同じ行に同じ値が2つ現れることになるため、これはがLatin squareであることに反する。よってであり、これよりが分かる。は体なので零元以外での除算を考えることができる。そこで、上式の両辺をで割るとが得られる。これはに矛盾する。よって順序対およびは異なる。
以上により、前述の方法で得られるテーブル群はMOLSになっていることが分かった。
MOLSを構築してみよう
MOLSの構築方法が分かったので、実際に構築してみよう。以下に本手法のsageによる実装例を示す。
#!/usr/bin/env sage import sys from sage.all import * def isOrthogonal(ls1, ls2): orderedPairs = set() for row in range(len(ls1)): for col in range(len(ls1)): orderedPair = (ls1[row][col], ls2[row][col]) if orderedPair in orderedPairs: return False orderedPairs.add(orderedPair) return True def calcMOLS(q): gf = GF(q, name="a", modulus="primitive") gamma = gf.gen() orderedGf = [0] + [gamma**(val) for val in range(q-1)] mols = [] for r in range(1, q): latinSquare = [[0 for i in range(q)] for j in range(q)] for row in range(q): for col in range(q): latinSquare[row][col] = orderedGf[col] + orderedGf[r] * orderedGf[row] mols.append(latinSquare) # Hasmap to convert elements of GF(q) to corresponding integers gfToInt = {} for i, val in enumerate(orderedGf): gfToInt[val] = i+1 for i, latSq in enumerate(mols): for j, line in enumerate(latSq): mols[i][j] = [gfToInt[val] for val in line] return mols def main(argc, argv): if argc != 2: print("usage: " + argv[0] + " order") return q = int(argv[1]) print("=== Calc MOLS ===") mols = calcMOLS(q) for i, latSq in enumerate(mols): print("Latin square#{}:".format(i+1)) for line in latSq: print(line) print("=== Sanity check ===") for r in range(len(mols)-1): for s in range(r+1, len(mols)): if not isOrthogonal(mols[r], mols[s]): print("The result is wrong.") return print("Correct MOLS.") if __name__ == "__main__": main(len(sys.argv), sys.argv)
実行例を以下に示す。
shinya@DESKTOP-Q1NSEEU:~/programs/blog/2021% ./mols.py 2 === Calc MOLS === Latin square#1: [1, 2] [2, 1] === Sanity check === Correct MOLS. shinya@DESKTOP-Q1NSEEU:~/programs/blog/2021% ./mols.py 3 === Calc MOLS === Latin square#1: [1, 2, 3] [2, 3, 1] [3, 1, 2] Latin square#2: [1, 2, 3] [3, 1, 2] [2, 3, 1] === Sanity check === Correct MOLS. shinya@DESKTOP-Q1NSEEU:~/programs/blog/2021% ./mols.py 4 === Calc MOLS === Latin square#1: [1, 2, 3, 4] [2, 1, 4, 3] [3, 4, 1, 2] [4, 3, 2, 1] Latin square#2: [1, 2, 3, 4] [3, 4, 1, 2] [4, 3, 2, 1] [2, 1, 4, 3] Latin square#3: [1, 2, 3, 4] [4, 3, 2, 1] [2, 1, 4, 3] [3, 4, 1, 2] === Sanity check === Correct MOLS. shinya@DESKTOP-Q1NSEEU:~/programs/blog/2021% ./mols.py 5 === Calc MOLS === Latin square#1: [1, 2, 3, 4, 5] [2, 3, 5, 1, 4] [3, 5, 4, 2, 1] [4, 1, 2, 5, 3] [5, 4, 1, 3, 2] Latin square#2: [1, 2, 3, 4, 5] [3, 5, 4, 2, 1] [4, 1, 2, 5, 3] [5, 4, 1, 3, 2] [2, 3, 5, 1, 4] Latin square#3: [1, 2, 3, 4, 5] [4, 1, 2, 5, 3] [5, 4, 1, 3, 2] [2, 3, 5, 1, 4] [3, 5, 4, 2, 1] Latin square#4: [1, 2, 3, 4, 5] [5, 4, 1, 3, 2] [2, 3, 5, 1, 4] [3, 5, 4, 2, 1] [4, 1, 2, 5, 3] === Sanity check === Correct MOLS. shinya@DESKTOP-Q1NSEEU:~/programs/blog/2021% ./mols.py 6 === Calc MOLS === /usr/lib/python3/dist-packages/apport/report.py:13: DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses import fnmatch, glob, traceback, errno, sys, atexit, locale, imp, stat Traceback (most recent call last): File "./mols.py", line 63, in <module> main(len(sys.argv), sys.argv) File "./mols.py", line 48, in main mols = calcMOLS(q) File "./mols.py", line 17, in calcMOLS gf = GF(q, name="a", modulus="primitive") File "sage/structure/factory.pyx", line 367, in sage.structure.factory.UniqueFactory.__call__ (build/cythonized/sage/structure/factory.c:2162) File "/usr/lib/python3/dist-packages/sage/rings/finite_rings/finite_field_constructor.py", line 566, in create_key_and_extra_args raise ValueError("the order of a finite field must be a prime power") ValueError: the order of a finite field must be a prime power
コマンドライン引数で次数を受け取っているが、確かにサイズのMOLSが構築できた。次数6のケースで異常終了しているが、6は素数冪ではないためこれは期待通りである。
余談だが、sageは本当に万能で惚れ惚れする。今回は自分のPC (Windows10のWSL2で動作するUbuntu20.04) にインストールしたsageを使用したが、最近ではそんなことしなくてもブラウザ上で実行できたりするようである[7]。興味のある方は試してみるとよい。
まとめ
本稿では直交表の基本的な事項について説明した後、直交表の生成に必要となるMOLSの構築方法について述べた。具体的には有限体の性質をうまく使ってMOLSが構築できることを説明した。また、説明した手法を実際にsageで動かして正しさを実証した。
次回はMOLSを用いて直交表を生成する方法について書きたいと思う。恐らく今回ほどのボリュームにはならないだろう。さらにその次は有限射影幾何との関連についても考えてみたいが、こちらは現時点ではさっぱり見通しがない。頑張って勉強しようと思う。
参考
[1] 直交表 - Wikipedia
[2] Orthogonal array - Wikipedia
[3] www.ipros.jp
[4] Latin square - Wikipedia
[5] Mutually orthogonal Latin squares - Wikipedia
[6]
- 作者:雪江 明彦
- 発売日: 2010/12/07
- メディア: 単行本(ソフトカバー)
その仮説検定、本当に大丈夫ですか? 〜 2つの誤りをコントロールしよう
統計検定2級に合格しました
最近、統計検定2級に合格した。素直に嬉しい。2級は大学教養レベルの内容ではあるが、それらを良く理解していないと合格できないので、良い勉強になった。
余談だが、以下のサイトは2級合格に必要な情報が網羅されており、大変参考になった。
統計検定2級では、とにかく区間推定や仮説検定の問題がたくさん出る。その勉強の中で、仮説検定について私が今までよく理解出来ていなかった点があることに気づいたため、本稿ではその気付きについて紹介したい。
棄却できない帰無仮説の問題
状況設定
ある正規母集団から個のサンプルを抽出し、標本平均の値を元に母平均の値について仮説検定をすることを考えてみよう。帰無仮説は「」とし、対立仮説は「」とする。これには両側検定を用いるのが適切である。
以後、有意水準はすべて5%固定で考える。また、議論をメインの話題にフォーカスするため、母分散は既知とする。つまり、t分布は使わない。さらに、簡単のための場合のみを考えることにする*1。
もし帰無仮説が正しい場合、は正規分布に従うはずである。そのため、帰無仮説の採択域は以下のようになる。
ただし、は標準正規分布の上側%点を表す。
もしサンプル数が少なく、例えばだった場合、採択域は以下のようになる。
ここで、実はこの正規母集団の真の分布がだったとしよう。つまり、帰無仮説は誤りだったというわけである。このとき、母平均を中心にが分布するわけだが、のケースだとが帰無仮説の採択域の中に入ってしまっているので、これだと結構な確率で帰無仮説が棄却できないことになる。つまり、本当は棄却しなければならない帰無仮説を棄却できず、判断を誤ることになる。
何が問題か?
どうしてこのようなことになってしまうのだろうか?清く正しく仮説検定をやったはずなのに、どうして判断を誤ってしまうのだろうか?それはずばり、仮定した分布と真の分布の切り分けを付けるための分解能が足りなかったことが原因である。
仮定した母平均に対して真の母平均がある程度近くにある場合、サンプル数が足りないとの分散が大きくなってしまい、2つの確率分布は大きくオーバーラップする。その結果、帰無仮説が容易には棄却できないということになってしまうのである。サンプル数が多くなればの分散が小さくなっていくため、仮定した分布と真の分布がはっきりと別れるようになり、仮説検定により区別が付くようになる。
第一種の誤りと第二種の誤り
実は、このような概念は統計学の世界においては普通に知られていることである。それが仮説検定における「第一種の誤り」と「第二種の誤り」というものである。それぞれ以下のような意味で使われる。
今回のケースで言うと、サンプル数が少ない場合、第二種の誤りを起こす確率が高くなってしまうということである。以下では第一種・第二種の誤りを起こす確率をそれぞれと表記する。
仮説検定では、第一種の誤りの許容範囲については、有意水準という形で明示的に指定する。今回は有意水準5%としているので、5%の確率で第一種の誤りが起こることは許容していることになる。それに対して、第二種の誤りについては明示的に述べられることがあまりないように思う。しかし、第二種の誤りの存在も認識して仮説検定のパラメータを選ばないと、仮説検定から有益な結果が得られなくなるので、どちらも意識してやる必要がある。
2つの誤りの関係
第一種・第二種の誤りをどちらも小さくすることが理想であるが、両者は一般的にトレードオフの関係にある。なぜなら、第一種の誤りを減らすということは「間違って帰無仮説を棄却したくない」ということになるのに対して、第二種の誤りを減らすということは「間違って帰無仮説を採択したくない」ということに等しく、両者が相反するからである。
では、全く打つ手が無いのかというと、そうではない。大切なのは、まず第一種の誤りと第二種の誤りの存在を正しく認識し、それらがどういうパラメータに影響を受けるかを知った上で、自分の考えている問題設定において、2つの誤りが許容できる範囲内に収まるようにコントロールすることである。これにより、仮説検定の有益性を格段に向上することができる。
2つの誤りの可視化
ここまで文章だけで説明してしまったので、第一種の誤りと第二種の誤りを可視化してみよう。可視化にはDesmosというサービスを利用した。状況設定として、仮説検定でを仮定したが、真の分布はだったというケースについてグラフを描いた。
赤線が仮定した分布、青線が真の分布である。また、赤色部分の面積が、青色部分の面積がの値を表している。は固定で5%としている。これを見ると、サンプル数を増やすことでグラフがシャープになって2つの分布のオーバーラップが小さくなり、が減っていく様子が見て取れる。
俺の考えた最強の仮説検定
以上を踏まえて、正しい仮説検定のステップについて考えてみたいと思う。
それぞれの誤り確率を左右する要因を押さえる
第一種・第二種の誤りの発生確率を左右する要因として代表的なものを以下にまとめる。
要因 | 第一種の誤りへの影響 | 第二種の誤りへの影響 |
---|---|---|
有意水準 | ↑ | ↓ |
サンプル数 | 影響なし | ↓ |
母分散 | 影響なし | ↑ |
表の見方について補足する。要因の列に示した値が増加したときに、第一種・第二種の誤りが増えるか減るかを上下の矢印で示した。例えば、有意水準が大きくなれば、許容する第一種の誤り確率が大きくなるため、第一種の誤りは増加する。
許容される誤り確率を設定しよう
次に、第一種・第二種の誤りがどれくらいの確率で発生することを許容できるかを考えよう。一般的にはくらいが目安のようだが、これはもちろん仮説検定毎に異なる。例えば、第一種の誤りを起こすことが人命に関わるようなケースでは、第一種の誤りを極力起こしたくないというようなことがあるだろう。
真の分布を推定しよう
これが仮説検定の泣き所である。実は、当然だが第二種の誤りは真の分布が分からないと分からない。しかし、そもそも真の分布が分からないから仮説検定をやっているわけなので、これは矛盾している。しかし、仮説検定は残念ながらこの矛盾を常に孕んだ状態で行わなければならない宿命にある。
この問題に対する私の考えを書いておく。真の分布は究極的には分からないので、何らかの推定をするしかない。これにはいろいろなやり方があるはずである。
例えば、製造したビールの容量が本当に500mlと言えるかを仮説検定するような場合、過去に誤って480mlだったことがあるというデータがあれば、平均が480mlの正規分布を真の分布としてを求める手はあるだろう。
他には、例えば学習塾のとある講座を受けることで試験の点が上がったかどうかを仮説検定する場合について、保護者の皆様が5点以上の点数向上を求めているという期待される効果量があったとする。
このケースでは、「実は講座の効果として真に点数が2点向上する効果があった」というような場合、第二種の誤りを犯しても犯さなくても、保護者の期待を裏切っていることには変わりない。そのため、2点差を切り分けられる分解能を得るためにサンプルをたくさん用意することには、コストがかかるだけで意味がない。
一方、本当は点数が5点向上する効果があったのに第二種の誤りを犯すということは避けたい。そのため、受講者の平均点が5点向上した場合を元に真の分布を仮定し、を算出するのが良いだろう。
このように、過去のデータ、求められている効果、及び専門家の知見などから、真の分布を仮定してやるのがよいと考えられる。
まとめ
本稿では仮説検定における第一種の誤りと第二種の誤りについて述べた。仮説検定では第一種の誤りだけでなく第二種の誤りも存在することを正しく認識し、それらをどのような値に納めたいかを検討した上で各種パラメータを決定する必要があるということを説明した。
統計学は非常に面白いので統計検定1級まで取りたい気持ちもありつつ、近頃は人生の時間の有限性に思いを馳せる日々である。自分と家族の幸せのために何をすべきか、よく考えていきたい。
*1:このように状況を限定しなくても同様の議論ができるのだが、面倒なので簡単な状況に限定した。
クーポンコレクター問題の拡張とその確率分布
以前、クーポンコレクター問題の確率分布について調べたことがある (詳細はこちら) 。これはこれで大きな成果だったのだが、いざこれを実用しようと思うと、少々物足りないことに気づいた。
例えば、6種類のクーポンを全て集めることを考える。以前求めた確率分布では、クーポンを集め始める前の時点における確率を考えることはできる。しかし、実際にクーポンが揃っていく速さはその時々であり、例えば途中で早々に2種類集まった場合、そこからどれくらいの確率で残り4つのクーポンが集まるのかを知りたくなる。
つまり、知りたいのは種類のクーポンのうちすでに種類のクーポンを取得している状況で、残りの種類のクーポンを集めるために必要な試行回数の確率分布はどうなるのか?ということである。本稿ではこの疑問に対する回答について述べる。
考え方の方針
回の試行で出現し得るクーポンの出方の総数はとなる。そのため、すでに種類のクーポンを持っている状態から始めて、ちょうど回で全てのクーポンが揃うパターンの総数が分かれば確率が分かる。
考えを整理するために、いくつか場合分けをしてパターンを数え上げてみよう。以下ではクーポンは全部で種類あり、そのうちすでに種類のクーポンを持っているとする。
のとき
残りのクーポンの数より小さい試行回数でクーポンが集まることはあり得ないので、このケースは0通りである。
のとき
それぞれの試行で未取得のクーポンが毎回ダブリなく出る必要がある。このケースは残りのクーポンの並び替えの総数と等しく、通りとなる。
のとき
ここが山場である。このパターンでちょうど回目にクーポンが全て集まるためには、回目までに個のクーポンが出尽くしている必要がある。試行回数が残りのクーポンの数よりも多いため、最初の時点で持っているクーポンが出ることもあり得る。それも加味すると、回目までに出るクーポンは種類以上、種類以下となる。
最初に持っている種類のクーポンのうち種類のクーポンが出るとすると、回目までには種類のクーポンが出ることになる。
それぞれの試行に番号を付けて、これを個の区別された箱に分類しているのだと考えると、このパターンの総数は以下のように第二種スターリング数を用いて書ける。
第二種スターリング数自体は区別のない箱への分類の総数を表しているので、を掛けていることに注意されたい。また、すでに持っているクーポンから種類のクーポンを選ぶ方法には自由度がある。同様に、未取得のクーポンから種類のクーポンを選ぶ方法にも自由度がある。そのため、さらにを掛けている。
より、による総和を取れば、求めるパターンの総数は以下のようになる。
クーポンの出方の例:
この場合、である。それぞれのについてクーポンの出方の具体例を見てみよう。
ここでは6種類のクーポンをAからFのアルファベットで表す。また、最初の時点ですでにA, B, Cの3つのクーポンを持っているとする。
・
すでに持っているクーポンは1種類も出ないため、以下のようなクーポンの出方となる。
D F F F E
・
すでに持っているクーポンは1種類だけ出るため、以下のようなクーポンの出方となる。
D A F A E
・
すでに持っているクーポンは2種類だけ出るため、以下のようなクーポンの出方となる。
B F A D E
のとき
これは1つ前のケースとほぼ同様である。ただし、回目までに出るクーポンは種類以上、種類以下となる。ゆえにとなる。これより、このパターンの総数は以下のようになる。
クーポンの出方の例:
この場合、である。それぞれのについてクーポンの出方の具体例を見てみよう。
ここでも6種類のクーポンをAからFのアルファベットで表し、最初からA, B, Cの3つのクーポンを持っているとする。
・
すでに持っているクーポンは1種類も出ないため、以下のようなクーポンの出方となる。
D F F F D D F E
・
すでに持っているクーポンは1種類だけ出るため、以下のようなクーポンの出方となる。
D A F A A F D E
・
すでに持っているクーポンは2種類だけ出るため、以下のようなクーポンの出方となる。
B F F A D A B E
・
すでに持っているクーポンは3種類全て出るため、以下のようなクーポンの出方となる。
B F F C D A B E
4つのケースをまとめる
実は前述の4つのケースは以下のように一本の式にまとめられる。
これをで割れば求める確率分布が得られる。
ただし、はクーポンが全て集まるまでに必要な試行回数を表す確率変数であるとする。
計算してみよう
確率分布
前回同様、今回もpythonを使用して確率分布を計算してグラフを描いてみた。パラメータが多いのでの値は6固定とした。ソースコードを以下に示す。
#!/usr/bin/python3.6 from sympy.functions.combinatorial.numbers import stirling from sympy.functions.combinatorial.numbers import nC import matplotlib.pyplot as plt import math def coupon(k, l, n): loopNum = min(n-k+l, l)+1 numPattern = 0 for i in range(loopNum): stir = stirling(n-1, k-l-1+i, kind=2) numPattern += (k-l) * nC(l, i) * math.factorial(k-l-1+i) * stir return float(numPattern) / k**n def main(): numCouponKind = 6 numTrial = 30 for l in range(0, numCouponKind): x = [] y = [] for n in range(1, numTrial+1): prob = coupon(numCouponKind, l, n) x.append(n) y.append(prob) plt.plot(x, y, label='l = {}'.format(l)) plt.legend() plt.grid() plt.xlabel("n") plt.ylabel("probability") plt.savefig('coupon_lx_k6.png') #plt.show() if __name__ == "__main__": main()
得られたグラフを以下に示す。
が大きいほど分布が左に偏っており (つまり右に歪んでおり) 、最初に持っているクーポンが多ければ多いほど少ない試行回数でクーポンを全て収集できることが分かる。
累積分布
これまでの議論で確率分布は分かった。しかし、実際に自分がどれくらい試行を重ねればクーポンを揃えられるかというのは、累積分布を見た方が分かりやすいだろう。
私が日常生活で良く考えたくなるのはのケースなので*1、これらについて累積分布関数の値がそれぞれ0.5, 0.9, 0.95以上になるために必要な試行回数をの値毎に示す。
・
l | 累積0.5 | 累積0.9 | 累積0.95 |
---|---|---|---|
0 | 5 | 9 | 11 |
1 | 4 | 8 | 10 |
2 | 2 | 6 | 8 |
・
l | 累積0.5 | 累積0.9 | 累積0.95 |
---|---|---|---|
0 | 13 | 23 | 27 |
1 | 12 | 22 | 26 |
2 | 11 | 21 | 24 |
3 | 10 | 19 | 23 |
4 | 7 | 17 | 21 |
5 | 4 | 13 | 17 |
クーポンを確実に集めるためには、思ったより多くの試行が必要という印象を受けた。クーポン集めは最後の方が異常にしんどいので、が多少大きくても焼け石に水という感じだということも分かった。
まとめ
本稿ではクーポンコレクター問題の拡張として、最初からいくつかのクーポンを持っている状態でクーポンを全て集める際の試行回数について考えた。クーポンを確実に集めるために必要な試行回数は思いのほか大きく、クーポンを真面目に集めることがいかに厳しいことであるかを肌で感じた。
今後の人生で何かを集めたくなったら、大人しくメル○リを利用した方が良いかもしれない。