LoginSignup
12
13

More than 5 years have passed since last update.

「日本語ボキャブラリーテスト」のアルゴリズム

Last updated at Posted at 2016-08-19

語彙力診断の点数分布 - Qiita を読んでなんとなくやってみたくなったので。当該診断のアルゴリズムを読んでいきます。

日本語ボキャブラリーテストのソースコードを見ると結果表示スクリプトが

function resultParse() {
  $("#collection, #progress").hide();$("#result_details, #emoji, .result_link, .footer-share").show();
  iq_value = getVocSizeByAnswers(voc_answers);
  prestige = "私の語彙力は・・・【" + parseInt(iq_value * lang_ratio) + "】です!あなたは?";
  ...
}

というものであるというのがわかります。iq_value及びlang_ratioは初期化部分っぽいところで

<script>var lang_ratio = 1.2;
var iq_value = 0;
...

のように設定されており、lang_ratioは中身がよくわからないが(他の言語のバージョンもあるので、言語別のアジャスタブルパラメータなのかもしれない)以後他の値が代入されることもなく固定値のようです。

iq_valueの方は getVocSizeByAnswers(voc_answers) の戻り値との事なので同関数を検索するとその中身は

function getVocSizeByAnswers(arr){
  var size = 0;
  var H=0,L=0,M=0;
  var F = [];
  F.push([18,20,18,20,0,10,'20000+(H*.05+M*.05+L*.1)*3500']);
  F.push([18,20,14,17,0,10,'15000+(H*.05+M*.05+L*.1)*3000']);
  F.push([18,20,10,13,0,10,'8000+(H*.05+M*.05+L*.1)*1000']);
  F.push([18,20,5,9,0,10,'5000+(H*.05+M*.05+L*.1)*1000']);
  F.push([18,20,0,4,0,10,'4000+(H*.05+M*.05+L*.1)*1000']);
  F.push([15,17,18,20,0,10,'15000+(H*.05+M*.05+L*.1)*3000']);
  F.push([15,17,14,17,0,10,'15000+(H*.05+M*.05+L*.1)*1000']);
  F.push([15,17,10,13,0,10,'5000+(H*.05+M*.05+L*.1)*800']);
  F.push([15,17,5,9,0,10,'4000+(H*.05+M*.05+L*.1)*1000']);
  F.push([15,17,0,4,0,10,'3000+(H*.05+M*.05+L*.1)*1000']);
  F.push([10,14,18,20,0,10,'6000+(H*.05+M*.05+L*.1)*1000']);
  F.push([10,14,14,17,0,10,'5000+(H*.05+M*.05+L*.1)*1000']);
  F.push([10,14,10,13,0,10,'4000+(H*.05+M*.05+L*.1)*1000']);
  F.push([10,14,5,9,0,10,'4000+(H*.05+M*.05+L*.1)*800']);
  F.push([10,14,0,4,0,10,'4000+(H*.05+M*.05+L*.1)*500']);
  F.push([5,9,18,20,0,10,'10000+(H*.05+M*.05+L*.1)*1000']);
  F.push([5,9,14,17,0,10,'8000+(H*.05+M*.05+L*.1)*1000']);
  F.push([5,9,10,13,0,10,'4000+(H*.05+M*.05+L*.1)*1000']);
  F.push([5,9,5,9,0,10,'4000+(H*.05+M*.05+L*.1)*800']);
  F.push([5,9,0,4,0,10,'4000+(H*.05+M*.05+L*.1)*500']);
  F.push([0,4,18,20,0,10,'5000+(H*.05+M*.05+L*.1)*1000']);
  F.push([0,4,14,17,0,10,'4000+(H*.05+M*.05+L*.1)*1000']);
  F.push([0,4,10,13,0,10,'4000+(H*.05+M*.05+L*.1)*500']);
  F.push([0,4,5,9,0,10,'2000+(H*.05+M*.05+L*.1)*500']);
  F.push([0,4,0,4,0,10,'(H*.05+M*.05+L*.1)*500']);
  for (var i = 0; i<50; i++){
    var score = arr[i];
    if(score>0){
      if (i<20){
        H++;
      }
      if (i<40 && i>19){
        M++;
      }
      if (i>39){
        L++;
      }
    }
  }
  for (var j = 0;j<F.length; j++){
    var rule = F[j];
    var H1 = rule[0];
    var H2 = rule[1];
    var M1 = rule[2];
    var M2 = rule[3];
    var L1 = rule[4];
    var L2 = rule[5];
    var ruleQuery = rule[6];
    if (H>=H1 && H<=H2 && M>=M1 && M<=M2 && L>=L1 && L<=L2){
      size = eval(ruleQuery);
    }
  }
  size = Math.round(size);
  return size;
}

となっています。arr[] = voc_answers[] の中身がわからないと何をしているのかよくわからないので検索すると

function updateValue(v) {
  if (v == 'M' || v == 'F' || v == 'N') {
    gender = v;
  } else {
    voc_answers.push(Number(v));
  }
}

という関数があり、これで更新しているようです。

updateValueを呼び出している箇所を探すと

$('.answer').click(function(){
    var clickDiff = (new Date()).getTime() - lastClick;
    lastClick = (new Date()).getTime();
    if (clickDiff < LOCK_TIME && !isDebug) {
      alert('よく問題を読んでから答えて下さい!');
      return;
    }
    var $this = $(this);
    var val = $this.attr('value');
    _gaq.push(['_trackPageview', 'q-'+counter+'/'+$this.index()]);
    var a_j = val.split('to,')[1];
    var is_j = (Number(a_j) === parseInt(a_j, 10));
    var skipAll = false;
    if(is_j){val = val.split('to,')[0]}
    if(val){if(is_j){val = val.split(',').join('')}updateValue(val)}

    ......

というふうになっており、問題に答えたタイミングでセットされるようです。
結局valは選択肢パネルのvalue属性であるということがわかります。

そしてこのvalueの値なのですがおそらくその選択肢が正解か否かを表す二値が入っているのでしょう。

それに関しては推論のつながりを重視するのならこの要素がいかにして現れるかを追っていく方法がありますが、おそらくは問題文や選択肢もベタ書きのような気配なので選択肢の一つで検索してみると、

var data = "ならう_SYN\n(0)つかむ\n(0)上げる\n(0)取る\n(1)学ぶ\n|\nまじめ_SYN\n(0)本当\n(1)本気\n(0)本来\n(0)本心\n| ... \nたをやめ_ANT\n(0)たまゆら\n(0)ありてい\n(0)あまつさえ\n(1)ますらを"; ...

という感じで問題の情報がひとつの変数にパイプ区切りで突っ込んであるところが見つかります。この選択肢の前の括弧に入っている数値が非常に怪しいので、Chrome のディベロッパーツールを使って先ほどのanswerクラスdiv要素のvalue属性を調べるとこの括弧の中身と一致しました。

したがってここまでの流れをまとめると、「正解不正解情報を 0/1 [int] の配列で保有して getVocSizeByAnswersに渡している。」というものになります。

getVocSizeByAnswers自体を見ます。此処からPython様の擬似言語になります。

  size = 0; H=0,L=0,M=0; F = [];
  F.append([18,20,18,20,0,10,'20000+(H*.05+M*.05+L*.1)*3500']);
  ...
  H = sum(arr[0:20]);
  M = sum(arr[20:40]);
  L = sum(arr[40:50]);

  for rule in F:
    H1, H2, M1, M2, L1, L2 = rule[0:6];
    ruleQuery = rule[6];
    if H1 <= H <=H2 and M1 <= M <=M2 and L1 <= L <=L2:
      size = eval(ruleQuery);
    # if in the case of rule = [18,20,18,20,0,10,'20000+(H*.05+M*.05+L*.1)*3500']
    # size calculated if H in [18, 19, 20] and M in [18, 19, 20] and L in 0 .. 10 (always true)
    # according to the formula: size = 20000 + (H * 0.05 + M * 0.05 + L * 0.1) * 3500
  }

のようになっています。
何をしているかというと

  1. 各問題番号正解数から H(序盤の問題正解数)、M(中盤の問題正解数)、L(終盤の問題正解数)を算出
  2. F の中にあるルールを前から順番に適用していく。
  3. ルールは (H,M,L) の取りうる範囲とスコア計算式からなる
  4. F を上から眺めていくと
    1. 序盤と中盤に関して9割以上の正解を要求するルールから始まり徐々に条件を下げていく。
    2. 終盤に関しては全ルールにわたって正解数を問わない

というようになっています。

分析としてはやや尻切れトンボなきがしますがとりあえずはどのようなアルゴリズムかということはわかったのでこの辺で筆を置きます。

感想レベルの話

  • H * 0.05 + M * 0.05 + L * 0.1 で一見 L に重みがかかっているように見えるがこれは問題数の配分が 20, 20, 10 であることの補正なので特に後半に荷重がかけられているということはない。

  • グラフではないがせめてルール番号、中央値、幅、幅と中央値の比を表だけでも

    • (H * .05 + M * .05 + L *.1 ) は [0., 3.] を動く、したがって幅は 係数×3. である。
    • 底上げ + 幅 /2 が中央値になると仮定し(序盤・中盤・終盤の各々のセクターに含まれる各問題の難易度は同じくらいと仮定)
    • 実際に表示される数値との比較を用意にするために全体を lang_ratio (=1.2)

※)初稿でlang_ratioをかけていなかったのを修正

rule num center width ratio
0 30300 12600 0.415842
1 23400 10800 0.461538
2 11400 3600 0.315789
3 7800 3600 0.461538
4 6600 3600 0.545455
5 23400 10800 0.461538
6 19800 3600 0.181818
7 7440 2880 0.387097
8 6600 3600 0.545455
9 5400 3600 0.666667
10 9000 3600 0.400000
11 7800 3600 0.461538
12 6600 3600 0.545455
13 6240 2880 0.461538
14 5700 1800 0.315789
15 13800 3600 0.260870
16 11400 3600 0.315789
17 6600 3600 0.545455
18 6240 2880 0.461538
19 5700 1800 0.315789
20 7800 3600 0.461538
21 6600 3600 0.545455
22 5700 1800 0.315789
23 3300 1800 0.545455
24 900 1800 2.000000

のようになる。

  • 追加でやるとよかったかもしれなかったこと
    • 一つの可能性としてこのアルゴリズムがやっていることがIQ 算出関数(誤差関数の逆関数になる?)の得点区間別線形近似である可能性があるのでその可能性について検討 ... 結構めんどくさそうですね。
12
13
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
13