Quantcast
Browsing Latest Articles All 26 Live
Mark channel Not-Safe-For-Work? (0 votes)
Are you the publisher? or about this channel.
No ratings yet.
Articles:

伝説的カジノの必勝法を、chart.jsで試してみませんか?破産する前に読んでおきたい物語

背景

かつて、モナコのあるカジノを破産させたと伝えられる「カジノの必勝法」がある。
その名も「モンテカルロ法」。

※Qiita民の間では、シミュレーションや数値計算を乱数を用いて行う手法としての
 「モンテカルロ法」は高名であるが、それとは同姓同名の別人(別法?)。

「カジノの必勝法」の中で、最も有名なものは、
マーチンゲール法」だろう。
倍々に賭けていって、いつかは当たるので必ず勝てる、という戦略。
倍々にすると、どこかで資金的orルール的な掛け金の上限に達するため、
これは必勝法とは言えない、とすぐに分かる。

一方、「モンテカルロ法」は、はるかに優秀な戦略で、実用的だ。
内容を聞けば、カジノを破産させたという伝説にも、説得力がある(かもしれない)。
「東京カジノ」でも「ラスベガス」でも「マカオ」でも、
または、株や仮想通貨の取引においても使える(かもしれない)。

結論から言えば、本稿においても、
勝率97% というシミュレーション結果が出た。
※ここまで見てカジノに直行せずに、最後まで熟読すること

この記事の概要

この記事は、「モンテカルロ法」の伝説の秘密を解き明かすべく
chart.js を使って、勝ち負けのシミュレーション結果を
グラフ表示するアプリ(Webサイト)を作った物語である。
実際に読者にも動かせる形で、そのシミュレータも公開する。

本稿は、 クソアプリ Advent Calendar 2018 の24日目の「代打投稿」。
「伝説」に挑むクソアプリ(実用性は無い)を作った物語

かつて、多くの男たちが、
カジノの必勝法を編み出し、そして破産していった。
同じような運命を辿る前に、ぜひ本稿で、
「伝説的カジノの必勝法」を心ゆくまで試してほしい

また、chart.jsの他のサンプルも提示するので、
JavaScriptでHTML上に綺麗なグラフを書く、ことの
実戦的サンプルとしても見ていただけるかもしれない。
最後に、コードの主要部分も掲載しておく。

最近は言葉の真意を理解しない人も多いので、誤解が生じないように、
大真面目な語りで盛大にボケる話だと予め宣言しておく。
※カジノに行く場合は必ず自己責任で賭けてくださいね。

モンテカルロ法とは?

まず最初に、「モンテカルロ法」はどうして必勝法なのか、
戦略と、必勝である理由を説明する。

モンテカルロ法の戦略

賭ける対象は、ルーレットの赤黒のような、
勝てば倍になるゲームを例として説明する。

  • 儲けたい金額を10$とする
  • 合計が10になるように数字を書く
  • [1, 2, 3, 4]
  • 両端の合計の数を賭ける(今回は「1」+「4」)
  • 勝った場合は、その両端の数字を消去
    • ⇒[2, 3]
  • 負けた場合は、今回賭けた金額を右端に追記
    • ⇒[1, 2, 3, 4, 5]
  • これを繰り返して、数字を全て消したら終了
  • もし途中で数字が一つ残った場合は、分割する
    • 例:[5] ⇒ [2, 3]

なぜ「必勝」なのか?

「数字のリストの合計」=「目標金額」と常に等しい、と気がつく。
つまり、全ての数字が消えれば目標達成になる。

勝った場合 ⇒ 数字が2つ減る
負けた場合 ⇒ 数字が1つ増える

50%で勝つ勝負ならば、
平均すれば、必ず数字の個数を減らしていくことができる
つまり、必ず目標を達成することができる。

「負けたら倍賭け」の「マーチンゲール法」と違うのは、
負け続けた場合でも、掛け金の上昇がかなり緩やかであることだ。
「マーチンゲール法」では、
「破産リスク」や「カジノの最大賭け金の設定」が壁になるが、
「モンテカルロ法」は現実的に勝利(10$獲得)できる!

※ここまで見てカジノに直行せずに、最後まで熟読すること
 (繰り返し)

賭けの進行例

  • [1, 2, 3, 4] ⇒ 5賭けて負け
  • [1, 2, 3, 4, 5] ⇒ 6賭けて勝ち
  • [2, 3, 4] ⇒ 6賭けて負け
  • [2, 3, 4, 6] ⇒ 8賭けて負け
  • [2, 3, 4, 6, 8] ⇒ 10賭けて負け
  • [2, 3, 4, 6, 8, 10] ⇒ 12賭けて勝ち
  • [3, 4, 6, 8] ⇒ 11賭けて負け
  • [3, 4, 6, 8, 11] ⇒ 14賭けて勝ち
  • [4, 6, 8] ⇒ 12賭けて勝ち
  • [6] ⇒ [3, 3](数字が一つなので分割)
  • [3, 3] ⇒ 6賭けて勝ち
  • [] ⇒ 結果10$の儲け!!

もし、マーチンゲール法だと、10$儲けようとすると、
3連敗後に1勝の場合、最後の掛け金は80$賭けることになる。
上の進行例で、3連敗している箇所があるが、
掛け金の増加がはるかに緩やかであることが確認できる。

最強の方法じゃん!?これで私も大金持ち!?

実際のカジノでは、「メモ」が禁じられている場合が多いという点を除けば、
一見、非の打ちどころがない方法に見える。
数字の個数で考えれば、基本的には減ってゆくはずだし、
負け続けても掛け金が倍々的に増えることは無い。

そこで、本稿では、
モンテカルロ法のシミュレータ」を開発し、
その性能を試してみることにした。

自分で先に試したい方
↓↓↓
このページ の「必勝法を試す」から。
↑↑↑
※他にも「ガチャ」の2%の当たりは100回引いて出るのか?などが
 試せるシミュレータが用意されている。

なお、前提として、「控除率」は0%とした。
「破産」まで行く前に「カジノの最大賭け金の設定」に
引っ掛かるものとして、最大賭け金を設定できるものとした。

シミュレーション結果①

res1.PNG

chart.jsを使て、所持金と賭け金のグラフを作成できる。
上の例では、勝ったり負けたりしているが、賭け金は最大でも20$いかずに、
最終的には10$の儲けを得て、21回で勝利している。

res2.PNG

上の例も、最終的には勝利した図。
通常は、もっと簡単に10$獲得して終わり、というシンプルな図になることが多い。
(四分の一の確率で、最初に2連勝して終わりになる)

では、「敗北」パターンはどんな感じになるかというと・・・

res3.PNG

賭け金の上限値として、100$まで、という低めの天井を設定しているため、
それを超えてしまう場合敗北扱い。
上の例では、その時点で清算したとして、310$失ったことになる。

シミュレーション結果②

さて、多くの人が気になるのは、
個々のゲーム(10$儲けるか、大金を失うか)ではなく、
これを繰り返した場合だろう。
グラフの下の方に、「累計」をテキスト表示している。
PCからアクセスしている場合は、一度実行後、
エンターキーを押下し続けることで、連続して試行できる。

ここから先の結果は、
ぜひご自身のPCのエンターキーの上に
何か文鎮でも置いて試してみて欲しい。

↓↓↓
このページ の「必勝法を試す」から。
↑↑↑

というか、結果がランダムウォーク的性質を持つため、
人によって累計の結果が結構変わってしまう。

soukei.PNG

総計で、損をする場合もあれば、得をする場合もある。
勝率は、およそ97%ほどになると思われる。


結果考察とまとめ

50%で勝つ勝負ならば、
平均すれば、必ず数字の個数を減らしていくことができる

このため、賭けの上限などの現実的な制限が無ければ必勝だ!
と、説明することができるため、
誰かに教えると大変自慢できるかもしれない。
(※くどいですが、自己責任でご利用ください)

実際に、本稿で作成したシミュレータでは、
(100$という低めの上限を設定した場合でも)
97%の確率で10$得ることができた

数学的に、本当はいくらの期待値なのか?は、
ここには解を記さない。それがロマン
だって、クソアプリカレンダーに投稿したから実用度は無い!
(コメント欄で数学的解説をしてくださる方がいたら、
 それはそれで大歓迎。)

興味がわいた方は考えてみると面白いかもしれない。
「一回勝負」なら、かなり高確率で少額を稼ぐことができるため、
実戦でも、使い方によっては楽しいと思う。

また、本アプリは、chart.jsで 何かグラフを作るサンプルとして、
目的が欲しくて作ったため、他のグラフサンプルも掲載している。
2%で当たるガチャを、100回引いた時に、一個も出てこない確率は?
ということを計算&グラフ表示できる機能もオススメ。

↓↓↓
このページ の「ガチャる」から。
↑↑↑

そして、chart.jsの練習成果は、
Qiitaの殿堂 の、
分析グラフ編(Qiitaの可視化から見えた、人気技術ランキングの推移)
で活用している。

最後に、主要部分のコードを掲載する。


主要部分のコード掲載

当初はスマホアプリ化することも視野に入れて、
monaca + onsenui で作っていた。

訓練不要で誰でも速読!日本一の速読アプリ「瞬間速読」の個人開発物語(25万DL)

↑の記事で言及している方法と全く同様の方法だ。
スマホアプリにしなくても、そのままWebサイト上でも公開できる。

monaca + onsenui + chart.js まで組み合わせて使うことが可能。
以下主要部分のコード:

モンテカルロ主要部分
function doCalcAndGraph_MON(){
    maxkaisuu=parseInt(document.forms.inputform.sikoukaisuu_MON.value,10);
    maxbet=parseInt(document.forms.inputform.maxbet_MON.value,10);
    //TODO:
    //本当は入力チェックする

    //データ配列を用意する。
    var label_array = [];
    var data_array = [];//持ち点の推移
    var kakekin_array = [];//賭け金の推移

    //モンテカルロ用の配列
    var mon_array=[1,2,3,4];
    var motiten=0;
    //勝負の状況を記録したテキスト
    var jyoukyoutext="";
    jyoukyoutext+=" ["+mon_array+"]<br>\n";
    //一回目は手動で0点を入れる。
    label_array.push(0);
    data_array.push(motiten);
    console.log(mon_array);
    //賭けた回数は別途カウントしておく。
    var kakekaisuu=0;
    for(aa=0 ; aa<maxkaisuu+1 ; aa++){
        //現在の長さを確認する。
        var length=mon_array.length;
        if(length<1){
            jyoukyoutext+=" ◆終了◆:◎勝利!<br>\n";
            console.log("勝利!");
            break;
        }
        if(length==1){
            jyoukyoutext+=" @1で分割<br>\n";
            console.log("特殊分割");
            //特殊処理:一個だけ数字が残っている場合は、「5」⇒「2」「3」など分割。
            //現在1数字しかない。その数字から1を引く。
            var lastValue=mon_array[0];
            var newValue=Math.floor(lastValue/2);//÷2切り捨て
            mon_array=[newValue,lastValue-newValue];
            jyoukyoutext+=" ["+mon_array+"]<br>\n";
            console.log(mon_array);
        }

        //特殊分割を行うと配列の長さも変わっているので注意。
        //今回賭ける値を設定する。
        var betValue= mon_array[0] + mon_array[mon_array.length - 1];
        console.log("賭け金:"+betValue);
        if(betValue>maxbet){
            jyoukyoutext+=" ◆終了◆:×敗北!<br>\n";
            console.log("敗北");
            break;
        }

        jyoukyoutext+=" "+betValue+"$賭け ⇒ ";
        if( Math.random()<0.5 ){
            jyoukyoutext+="◎勝利!<br>\n";
            //勝った場合は、先頭と末尾を削除する。
            motiten+=betValue;
            //先頭要素の削除
            mon_array.shift();
            //末尾の要素の削除
            mon_array.pop();
        }else{
            jyoukyoutext+="×敗北<br>\n";
            //負けた場合は賭けた額に等しい値を末尾に追加する。
            motiten-=betValue;
            mon_array.push(betValue);
        }
            jyoukyoutext+=" ["+mon_array+"]<br>\n";
        //いくらかけたかのデータを格納する(一個前の配列になる)
        kakekin_array.push(betValue);
        kakekaisuu+=1;
        //記録更新:
        if(mon_saidaikakekin<betValue){
            mon_saidaikakekin=betValue;
        }
        if(mon_saidaihusai>motiten){
            mon_saidaihusai=motiten;
        }
        //データを配列に格納する。
        data_array.push(motiten);
        label_array.push(aa+1);
        console.log(mon_array);
    }

    //合計値の変更
        mon_sum_motiten+=motiten;
        mon_sum_kaisuu+=kakekaisuu;
        mon_sum_monkaisuu+=1;

    //描画用のデータにして、実際に描画する。
    makeChart_MON( makeChartData_MON(label_array, data_array, kakekin_array));
    return 0;
}

chart.jsにて描画するためのデータ配列の作成&描画
function makeChartData_MON(label_array, data_array1, data_array2){
    var barChartData = {
        labels: label_array,
        datasets: [
            {
              type: 'line', // 追加
              label: '所持金推移',
              //結合点のサイズ
              pointRadius: 0.5,
              //結合点のサイズ(ホバーしたとき)
              pointHoverRadius: 1,
              data: data_array1,
              //borderColor : "rgba(254,97,132,0.8)",
              //backgroundColor : "rgba(254,97,132,0.5)",
              borderColor : "rgba(254,97,132,0.3)",
              backgroundColor : "rgba(254,97,132,0)",
              yAxisID: "y-axis-RW", // 追加
            },
            {
              type: 'line', // 追加
              label: '賭け金推移',
              //結合点のサイズ
              pointRadius: 0.5,
              //結合点のサイズ(ホバーしたとき)
              pointHoverRadius: 1,
              data: data_array2,
              //borderColor : "rgba(254,97,132,0.8)",
              //backgroundColor : "rgba(254,97,132,0.5)",
              borderColor : "rgba(132,254,97,0.3)",
              backgroundColor : "rgba(132,254,97,0)",
              yAxisID: "y-axis-RW", // 追加
            },
        ],
    };
    return barChartData;
}

function makeChart_MON(barChartData){
    ctx01 = document.getElementById("canvas_MON").getContext("2d");
    //chart.jsは同じキャンバス中にグラフを重ねていくため
    //前のグラフがある場合は廃棄を行う。
    if(GlfObj_MON){
        GlfObj_MON.destroy();
    }

    GlfObj_MON = new Chart(ctx01, {
        type: 'line',
        data: barChartData,
        options: complexChartOption_MON
    });
}

「遊び」の重要性

個人的には、役立たなくても「クソアプリ」という表現は好きではない。
しかし、その観点はとても重要だと思う。
クソアプリ、とは、いわば「遊び」
実用性が無く役立たなくても、見た人に強烈な印象を残し、
なんらかのインスピレーションを与える場合も多いだろう。

人類の進化は「遊び」からはじまる。
こんな「遊び」が出来るならば、というアイデアに触発される人がでて、
生活にも役に立つような「発明」が生まれるのだ。
          ~  Char Fuitter (1847~1912 オランダ) ~


この物語はフィクションです。
登場する人物・団体・名称等は架空であり、
実在のものとは関係ありません。
Char Fuitter (チャー・フイター)は架空の人物です。

k8s=Kubernetesについて本気で調べてみた。ついでにi18n

背景

最近、k8s という単語をよく目にするようになりました。
k8s とは、Kubernetesのことだそうです。
k ~8文字~ s なので、k8sと書くそうです。

Kubernetesとは何か?は、
他の多数の素晴らしい入門記事様を見ていただくとして、
興味を持ったのは、
k8s = Kubernetes って一意に決まるの?
ということです。

もし、他の単語でもk8sがいるなら、
k8sの座」を、Kubernetesだけに奪われてよいのか、
いや、良いハズがないっ!(反語表現)

そこで、
まさにタイトル通り「k8s=Kubernetes」が成立するのか調べます。
「Kubernetes」について本気で調べてみた、
というわけではないのであしからずご了承ください。

※正確には「k」が大文字じゃね?とかあるかもしれませんが、
 「Kubernetes」の時は、大文字の方が多くても、
 「k8s」の時は、小文字の書き方を多く見かける気がするので、
 この調査では大文字小文字は区別せずに(基本小文字で)扱うことにします。

さっそく調査に乗り出すことに

まず、英辞郎のテキストデータを用意します。
以前、このサイトで購入していました。(500円)
https://booth.pm/ja/items/777563

およそ、200万単語含まれています。スゴイ。

このデータを、pythonで、
{"見出し語":"意味"} の形の辞書形式に加工します。
ここは本題では無いのでコードは省略します。
調査対象の単語リストの準備ができました!

中身を見ると、
「k」で始まる単語は約2万1千語ありました!
(他のアルファベットに比べかなり少ないほうです)

k8sチェッカーの作成

正規表現で、k8sの条件を満たすかチェックする
チェック関数を作成します。

^k[A-Za-z]{8}s$

の形を基本として、少し一般的に作りました。

import re
#kNs_checker("k",8,"s", target_word)が条件にハマるかチェックするよ
def kNs_checker(head_char, num, tail_char, target_word):
    #k8s (k + 8 文字 + s)
    #target_word = r'kubernetes' 
    #pattern = r'^k[A-Za-z]{8}s$'
    pattern = r'^' + head_char + '[A-Za-z]{' + str(num) + '}' + tail_char + '$'
    result = re.match(pattern, target_word)
    if result: #none以外の場合
        #print(result.group())
        return 1
    else:
        return 0
    return 0

print(kNs_checker("k",8,"s", "kubernetes") )

k2sを求めてみよう!

まずは、k2sから求めてみましょう。
さきほどの「k8sチェッカー」を用いて、辞書を検索します。
「k8sチェッカー」が「1」を返す=条件を満たす、ので、
その単語を列挙します。

k2sを求める
#「k」で始まる単語の辞書の読み込み {"見出し語":"意味"} 
dict = load_dict("k")

def pickup_word(head_char, num, tail_char):
    for key,val in dict.items():
        if kNs_checker(head_char, num, tail_char, key) > 0:
            print(key + ": " +val)

pickup_word("k",2,"s")

k2sの結果
kiss キスする
kris クリース◆マレー人が用いる短剣
kvas クヴァス◆ロシアの発酵飲料
# ※結果の意味の所は、加工しています。

k2sの時点でも3つと意外と少ないですね。
これはKubernetesが一意の座を得ることが出来るかも!?

※他に、kids、などが思いつきますが、
 辞書上、原型しか登録されていない場合もあり、
 複数形や活用は無視します。

k3s~k7sの結果

「2」を3~7に変えて上記のプログラムを動かします。

k3sの結果
kecks 〈英俗〉ズボン
kudos 〔功績などに対する〕称賛、賛辞
kumis  <koumiss> クミス、乳酒◆中央アジアの伝統食品。
kurus クルーシュ◆トルコの貨幣単位

ほうほう。

k4sの結果
kairos 〈ギリシャ語〉実行[決断]の時、潮時
kermes ケルメス◆カイガラムシの雌の羽を乾燥させた赤紫色の染料の原料
killas 《地学》粘板岩
knives  knifeの複数形
kouros クーロス◆古代ギリシャの青年の裸体像
kyphos 《病理》〔脊柱の〕後彎(症)

日本語でも知らんがな、って単語が多いですね。

k5sの結果
kalends 〔古代ローマ暦の〕月の最初の日
kenosis  神性放棄
ketosis 《病理》ケトーシス、ケトン症
keyless  キー[鍵]のない[不要の]
kickass  <kick-ass>〈卑俗〉エネルギー、力
kinesis キネシス◆生物の動き
kinless 親類[親族]のない
kissass <kick-ass>〈卑俗〉エネルギー、力
klipdas 《動物》ケープハイラックス
koumiss クミス、乳酒◆中央アジアの伝統食品。
kylikes kylixの複数形

だんだん増えているし。数字の少ない方が多いと思ってた。

k6sの結果
keenness 鋭さ、熱心さ
keftedes ケフテデス◆ギリシャ料理の肉だんご
kehillos kehillahの複数形
kindless 不親切な
kindness  親切(であること)、思いやり(があること)
kinesics  動作学
kinetics 《物理》動力学
kingless  国王の存在しない
kneesies 〈米俗〉〔恋人同士などが愛情の表現として〕膝と膝を触れ合わせること
knickers 〔女性用の〕ブルマー
knockers 〈卑〉おっぱい
kouskous:〈アラビア語〉クスクス◆パスタの一種。世界最小のパスタ
kurtosis カートシス、尖度
kyphoses kyphosisの複数形
kyphosis 《病理》〔脊柱の〕後彎症

おっぱい!?

k7sの結果
kaluresis <kaliuresis>カリウム利尿
kantharos カンタロス◆上方に伸びた取っ手(2個)が付いている大きな杯
katabases katabasisの複数形
katabasis 《軍事》退却
katharsis <catharsis>カタルシス、〔精神の〕浄化
keratitis 《病理》角膜炎
keratorus 《医》角膜膨隆
keratoses keratosisの複数形
keratosis 《病理》角化症◆【略】K
ketolides ケトライド系薬◆【略】KLs
ketolyses ketolysisの複数形
ketolysis 《化学》ケトン分解
kibbutzes kibbutzの複数形
kidstakes ごまかし、見せ掛け
kilobucks 千ドル、大金
kilogauss 《電気》キロガウス◆【略】kG
kinetoses kinetosisの複数形
kinetosis 《医》乗り物酔い
knismesis 《生理》軽いくすぐり刺激◆羽根が触れたり虫がはったときなど
knotgrass 《植物》ニワヤナギ、ミチヤナギ
kookiness 〈俗〉変人ぶり、異様なこと
kraurosis 《病理》萎縮症

もっと増えてる。これはk8sも厳しいか・・・?

おまちかね、k8sの発表です★

k8sの結果
kaliuresis カリウム利尿
karyolysis 《生物》核溶解
katholikos <catholicos>〔東方教会の〕総主教
keloidosis ケロイド症
keratinous ケラチン[角質]の[から成る・に似た]
kindliness 親切(な行為)、温情、温和
kinematics 《物理》運動学
klinotaxis 《生物》屈曲走性
knockknees <knock-knees>《医》X脚、外反膝
kohlrabies kohlrabi(《植物》コールラビ、カブカンラン)の複数形

なんと、10件のk8sが見つかりました!!

残念Kubernetesよ、k8sはお前だけのものではないのだ!

さあ、これからは、k8sについて語るときは、
「え、それって、ケロイド症について話しているの?」
「k8s入門って、X脚に入門するってこと?」
「絵本で分かるk8sって、キリンが核溶解する絵本?」
って確認することで、話している対象を間違えずにすみますね。

さらに、K8sの結果

Kubernetes一意性信者の野望を完全に打ち砕くため、
念を入れて、「K」が大文字の場合も、
同様のプログラムで求めてみました。

K8sの結果
Kannapolis {地名} : カナポリス◆米国
Karagatsis {人名} : カラガーチス
Karamanlis {人名} : カラマンリス
Kariotakis {人名} : カリオタキス
Kastanakis {人名} : カスタナキス
Katsuwonus 《魚》カツオ属
Kilcommons {人名} : キルコモンズ
Kolkasrags {地名} : コルカ岬
Kompaneets {人名} : カンパニェーツ
Korolkovas {人名} : コロルコワス
Korostvets {人名} : コロストベッツ

「K8s」だろうが、「k8s」だろうが、
世界はKubernetesを中心に回っているわけではありません。
もしかしたら、あなたの見つけた「K8s入門」は、
「カツオ(Katsuwonus)」に入門することになるかもしれない。
https://ja.wikipedia.org/wiki/カツオ

提言:これからは、キリンとかクジラ以外に、
「カツオ」も登場させてあげるのが
元祖K8sに対する礼儀ではなかろうか?

こうなると、最大値が知りたくなる

最大のkNsはなにか? を知りたくなるのが人情です。
pythonの正規表現のところを以下のように変更します。

pattern = r'^' + head_char + '[A-Za-z]*' + tail_char + '$'

上記を実行して調べた結果、
k18s が最大だと分かりました!

keratoconjunctivitis: 《眼科》角結膜炎

なんと、k8sを2倍以上うわまわっています。
これは、k8sを語る上でほぼ必須のムダ知識ですね。
k18sを知っていれば、k8sより2倍強いです。

i18n は一意なのか?

同様の問題として、
i18n = internationalization なのか調べてみます。

Internationalization(国際化対応)とは、文化、地域、言語によって異なるターゲットオーディエンスに合わせて容易にローカライズできる製品、アプリケーション、または文書内容の設計と開発のことです。~W3Cの定義~

コードはk8sのものと全く同様です。

i18nを求める
def pickup_word(head_char, num, tail_char):
    for key,val in dict.items():
        if kNs_checker(head_char, num, tail_char, key) > 0:
            print(key + ": " +val)

pickup_word("i",18,"n")
i18nの結果
immunophotodetection 免疫光学的検出
institutionalisation <institutionalization>制度化、慣行化
institutionalization  制度化、慣行化
internationalisation <internationalization>国際化、国際管理下に置くこと、国際管理化
internationalization 国際化、国際管理下に置くこと、国際管理化
iodochlorhydroxyquin 《薬学》ヨードクロルヒドロキシキン

Kubernetesは、英辞郎には存在しなかったのですが、
internationalizationは、本人が英辞郎にも居ました。
そして、

残念internationalizationよ、i18nはお前だけのものではないのだ!

kNsは18が最高値で、18で一意だったので、
i18nならもう少し頑張ってくれるかと思ったのですが、
いやー、おしかったですね。

これからは、i18nについて語る時は、
ヨードクロルヒドロキシキンについての話しじゃないよね?
と確認していく必要があります。

まとめ

こうしてこの世界にまた一つ新たなトリビアが生まれた

  • k8sは、Kubernetesのみの一意に定まらない
  • 最大はk18s = keratoconjunctivitis = 角結膜炎
  • i18nは、必ずしも国際化対応のことではない

この記事によって、
k8s や、i18n について語る時に生じる誤解を
0.00001%くらい軽減できたと思います!
エンジニアが良く使う言葉について、
とても役に立つ有用な知識を得ることができました。

コンテナとかdockerとか、ムズカシイ言葉を一切使わず、
ここまでk8sについて詳細に解説している記事は
他に類を見ないのではないでしょうか。

k8sとは何か? と聞かれた場合には、
この記事を紹介してあげると、とてもkindlinessですね。

または、「カツオ(Katsuwonus)の略だよ!」 と教えてあげると、
魚類まで網羅したあなたの博識ぶりに驚くこと間違いなしです。

現場からは以上です。

オマエ スマホ ゲンシジン 魏延 ナル

ガイヨウ

オレ オマエ スマホ ゲンシジンスル プログラム カイタ
オレ 【オマエ】 ダイスキ
トモダチ ミンナ スマホ ダイスキ
トモダチ フヤス ピィ ダブル エェ デキタ

オマエ コレ ミル
【オマエ スマホ ゲンシジン ナル!】

ピィ ダブル エェ スマホ ツカエル!
アピ イラナイ ツウシン イラナイ ツカエル! アプリ ニテル!
モチロン 魏延 イル!

サンプル

ツカウ カンタン!
Genshijin_02.PNG

オマエ コレ タメス
【オマエ スマホ ゲンシジン ナル!】

ドウサ カンキョウ

パソコン、ウィン、マック
スマホ、アンドロ、アイホン
ピィ ダブル エェ ミンナ トモダチ!

ピィ ダブル エェ?

オマエ ググル ワカル
ゲンシジン シンセツ ナイ
ゲンシゴ ピィ ダブル エェ セツメイ ナイ

アンドロ クロム ナド
アイホン サファリ ダケ
「ホーム ガメン ツイカ」 エラブ
アプリ ミタイ ツカエル
オマエ 「キナイ モード」 スル
アプリ ツウシン イラナイ ウゴク!
ギガ ウレシイ ハヤイ トモダチ!

ツクル カンキョウ

ジャバスクリプト
モナカ
クロモジ ジェイエス
ファイヤ ベエス

ジッソウ

オレ ジャバスクリプト コウブン カイセキ
【クロモジ ジェイエス】
【コレ ミル】
ジェイエス コウブン カイセキ デキル オドロキ!

オレ モナカ ピィ ダブル エェ ファイヤ ベエス
【モナカ】
【コレ ミル】
モナカ ピィ ダブル エェ サイキン タイオウ!
モナカ ファイヤ ベエス アプリ デプロイ カンタン!

オレ ウレシイ

アピ イラナイ!
ネット ツウシン イラナイ!
スマホ イレル ウゴク!
ピィ ダブル エェ アプリ ニテル!
ムリョウ バンザイ!

コード

オマエ ココ クリック テンカイ スル

イチ.
モナカ クラウド アィ ディ イィ ハイル
ニ.
ピィ ダブル エェ サンプル エラブ
サン.
クロモジ ジェイエス コード カク
エイチ ティ エム エル ショウリャク
ヨン.
ファイヤ ベエス デプロイ モナカ カンタン

コード テキトウ

console.log("LOAD_TOKENIZER...")

////kuromoji.js初期化処理////
// kuromojiオブジェクトの定義
let kuromojiObj;

// kuromojiオブジェクトの作成
//パスは、index.htmlからのパスになる点注意
kuromoji.builder({
    dicPath : "script/dict/"
}).build(function(error, _tokenizer) {
    if (error != null) {
        console.log(error);
    }
    kuromojiObj = _tokenizer;
});

// kuromojiオブジェクトの取得
let getKuromojiObj = function() {
    return kuromojiObj;
};

console.log("LOAD_KUROMOJI")

//文字列とフラグを入力すると、カナのリストを返却する
//時間がかかる場合にそなえてPromiseにしている
function strToGenshijin(input_str, GIEN_FLG) {
  return new Promise((resolve,reject) => {
    console.log("[START-strToGenshijin]");
    console.log(input_str);
    console.log(GIEN_FLG);
      // tokenizer.tokenize に文字列を渡すと、その文を形態素解析します。
      try {
        // kuromojiオブジェクトの取得
        let tokenizer = getKuromojiObj();
        //var tokens = tokenizer.tokenize("人民の人民による人民のための政治");
        //var tokens = tokenizer.tokenize("菜の花や月は東に日は西に");
        var tokens = tokenizer.tokenize(input_str);
        console.log(tokens);
      } catch(e){
        console.log(e);
        reject(e);
      }
      //console.dir(tokens);
      //return tokensToGenshijin(tokens, GIEN_FLG);
      resolve( tokensToGenshijin(tokens, GIEN_FLG) );
  })
}


//形態素解析後のリストと、魏延フラグを投入すると、カナのリストを返却する
function tokensToGenshijin(tokens_list, GIEN_FLG) {
  var result_list = []
  for(let i = 0; i < tokens_list.length; i++) {
    word_info = tokens_list[i];
    console.log(word_info);
    word_pos = word_info["pos"];
    //詳細な品詞はkuromoji.jsには無い
    //if (word_pos != "格助詞" && word_pos != "連用助詞" && word_pos != "引用助詞" && word_pos != "終助詞") {
    if (word_pos != "助詞") {
      if (GIEN_FLG == 1) {
        //魏延モード
        word_kana = hiraToKana(word_info["surface_form"])
      } else {
        //通常モード
        word_kana = word_info["pronunciation"];
      }

      result_list.push(word_kana);
    }
  }
  console.log(result_list)
  return result_list
}

//ひらがなからカタカナに変換関数
function hiraToKana(str) {
    return str.replace(/[\u3041-\u3096]/g, function(match) {
        var chr = match.charCodeAt(0) + 0x60;
        return String.fromCharCode(chr);
    });
}

function onButtonClick(GIEN_FLG) {
  console.log("Click!")  
  target = document.getElementById("input_textarea");
  //改行除去
  input_str = target.value.replace(/\r?\n/g, '');
  console.log(input_str)
  //result_list = strToGenshijin(input_str, 1);
  strToGenshijin(input_str, GIEN_FLG).then(function (result_list) {
    //非同期処理成功
    display(result_list, GIEN_FLG);
  }).catch(function (error) {
    console.log(error);
  });
}

//表示する
function display(result_list, GIEN_FLG) {
  if(0 < result_list.length) {
    if( GIEN_FLG >0 ){
      document.getElementById("result_div").innerHTML = result_list.join("……");
    }else{
      document.getElementById("result_div").innerHTML = result_list.join("  ");
    }
  } else {
    document.getElementById("result_div").innerHTML = "";
  }
}

ケッカ

人類の進化は「遊び」からはじまる。
こんな「遊び」が出来るならば、
というアイデアに触発される人がでて、
生活にも役に立つような「発明」が生まれるのだ。

  

ジンルイ シンカ  アソビ  ハジマル 
コンナ  アソビ  デキル ナラ  
アイデア ショクハツ  レル ヒト   
セイカツ ヤクニタツ ヨー  
  ハツメイ  ウマレル   

マトメ

ミンナ スマホ ゲンシジン ナッタ
アピ ツウシン ナシ ツカウ デキル!

【オマエ スマホ ゲンシジン ナル!】

オマエ コレ スマホ イレル デキル!
トモダチ イッパイ!
魏延 イッパイ!

ヒコウキ ナカ ゲンシジン 魏延 デキル!

ギジュツ カダイ メモ

オマエ ココ クリック テンカイ スル ミナクテ ヨイ

スマホ ピィ ダブル エェ
アプリ コウシンジ キャッシュ ノコル
コウシン ハンエイ ジドウ ヨイ ホウホウ フメイ

アイホン ホーム ツイカ ワカリニクイ オキニイリ ニテル

クロモジ ジショ オモイ ヒドウキ ウマク コウリョ

モナカ クラウド アイ デイ イイ ピィ ダブル エェ デバグ デキナイ
コンソル ログ デナイ ウゴキ イマイチ
モナカ ロカル キト デバグ デキル

コード インペイ ホウホウ フメイ

アルフアベツト タイオウ シテナイ
クロモジ ジショ カクチョウ シテナイ セイド ヒクイ

【続】平成の次の元号を、AIだけで決めさせる物語(@テレビ取材)

前書き

以前Qiitaの記事で、平成の次の元号をAIで予測したことがあり、
それについて、テレビ取材(フジテレビ)を受けることになった。

前回作った元号予測AIを強化して、
四書五経や、日本の古典なども出典も視野に入れて、
AIによる元号ガチ予測を実施して欲しいとのこと。
4月1日、新元号が発表される当日、発表直前に放映する予定だそうだ。

つまり、新元号の予想はXXです!と言った瞬間に、
残念違いましたーー!と分かってしまう、がっかり感

事前にウワサ等で予想されているものは採用されない、
という話もあるために、ガチで当てるためには直前放映しかない
という恐るべき戦い
(どう考えてもピッタリ当てるのは難易度が高すぎる)

まだ前回の記事をご覧になっていない方は、以下を参照されたし。

前回の記事⇒ 平成の次の元号を、AIだけで決めさせる物語

本記事投稿理由

本当は、4月1日の放送に期待してください、
以上終わり、なのだが、

①テレビ上の放送時間は恐らく5分程度と短く、
 一般人向けの放送であるため、
 プログラム的な面白さはたぶんほぼ含まれない。

②4月1日の放送&発表後に、試行錯誤した内容や、
 プログラム自体をアップしても良いが、
 4月1日の発表後に記事にしても、
 既に正解が出ているので全く面白くない。

③むしろ、発表後に投稿するならば、
 「実際に公開された元号が、AIから見てどうだったのか?」という
 新元号の評価の物語の方が
 外した予想の物語よりも、オモシロイだろう。

⇒苦肉の策として、
 放送前に、予想AI&プログラムの物語の大部分の趣旨は公開してみる
 Qiitaの読者諸兄には前回の物語の続きを心待ちにしている
 おかしな人もいるかもしれないので、ちゃんと続きを書く。
 番組宣伝的にもなっていいかもしれない。たぶん。

 事前に予想されてしまうと却下という話のため、
 最後の「結論」的な部分は
 公開できない部分があるが、そこはご了承を願いたい。
 まあ、「予想」は、発表後に書いても無意味なので、
 放送時間では入りきらない&詳細すぎる部分を、
 さきにここに書いておくよ、ってこと。
 あくまで、プログラムを使った
 現在進行形の「物語」としてお楽しみください。

前回の予測の概要と、この記事で目指す内容

前回の記事の内容は、
良い意味を持つ漢字と、
その漢字のバランスの取れた組み合わせ
機械的な計算のみで見つけることができるか?
という試みであった。

前回の記事⇒ 平成の次の元号を、AIだけで決めさせる物語

元号の多くは、四書五経などの漢籍に出典を持つが、
それは定められたルールではない

元号法に関連して定められた、
1979年の元号選定手続きの要領によれば、
あくまでも以下の6つがルールである。

 1. 国民の理想としてふさわしいようなよい意味を持つものであること。
 2. 漢字2字であること。
 3. 書きやすいこと。
 4. 読みやすいこと。
 5. これまでに元号又はおくり名として用いられたものでないこと。
 6. 俗用されているものでないこと。

よって、四書五経などの漢籍を参照するという、
いわば「定石」を学習せずに、
「1」の「国民の理想としてふさわしいようなよい意味」を、
日本語の通常の文章(wikipediaのテキストデータ)から
機械学習によって導き出すことができるか?という点が
前回挑戦したテーマである。

例えば、囲碁や将棋のAIを作る場合を考えてみると、
そのルールだけから作るのか、
プロ棋士同士の棋譜を見て学ぶのか、二通りの方法があるが、
プロ棋士の棋譜(漢籍そのもの及び、学者の選定基準)を
一切入れずにどこまで出来るのか、ということ。

元号を「当てる」ために作っているのとはちょっと違う。

※実際は、四書五経などの古典データの入手が面倒という点と、
 そのどの書をお手本に選ぶかなどの、人間判断が入ってしまうことを
 嫌ったという実務的な理由もあって、「ルールのみ」の取り組みとした。

今回のフジテレビ殿の依頼内容について、私と利害が一致した理由は、
じゃあコイツに「出典」の候補になるデータを喰わせたらどうなるの?
もっと頑張れば(当てにいけば)どうなるの?
という興味の探究である。

フジテレビ殿としても、Qiita記事そのままではなく、さらなる強化版を放送したい。
私としても、出典こみ&様々な要素を強化したバージョンを
元号発表前までの旬な間に作ってみたいし、当ててみたい
ということで、出典の入手をフジテレビ殿にお願いし、
前回のプログラムのブラッシュアップを行うことになった。
(取材の申し込みをいただいたのは良いキッカケとなった)

実際に出た結論は4月1日までのお楽しみとして、
試した内容や、プログラムを強化した内容、
その試行錯誤の歩みの一部を、以下に記す。
(※本記事投稿時点ではほぼ完成したものの、最終結論はもうちょい)

ちゃんと記録するとまたまた長くなってしまう。
同じ時代を生きる人々へ、歴史的瞬間に共に立ち会う物語として、
未来への戦いに挑んだ記録を残す日誌として、扱って欲しい

本投稿の内容

前回記事を見ていただいた上で、その強化版&続編です。
こういう検討をしたけどダメだった、の部分は
放送時間的に全く入らないと思われるため、そのへんの記録目的も含めます。

  • ガチで、AIだけに漢字のセレクトを任せ、新元号に相応しい言葉を計算します。
  • 使えるINPUT情報は、以下のようにしました。
    • Wikipediaのテキストデータ
      • (ただの大量テキストとして扱う)
    • 既に使われた元号の一覧(=これをお手本として学習)
    • 教育用漢字一覧(小学校で覚える漢字一覧=1006字)(=候補一覧)
      • 読みやすい=小学校レベルの漢字、と仮定。
      • (※常用漢字の場合は1945字。データを入れ替えればこちらでも可)
    • 別な意味の除外リストの作成のための情報
      • Mecabの辞書(これは前回も利用していた)
      • 過去の天皇名の一覧(New)
      • Wikipediaのタイトル項目の一覧(New)
    • 漢字読み方API(New)
    • 四書五経、古事記、などの中国&日本の古典データ
      • フジテレビ殿に入手していただいたデータ、詳細はヒミツ(New)
      • 漢籍だけでなく、日本古典の可能性もありえるらしい
      • 一部学者先生によるオススメ古典も入れるかもしれない
  • 製作者が調整出来ることは「数値の設定だけ」で、主観で判断はいけない、とします。
    • ★ここは前回と同様。正直一番厳しい制約
    • 例えば、「苦」の字は意味が悪いから使われないよね、といって除外するのは禁止
    • 何かの基準で、上位N個に絞るとか、何かの得点値が10以上、などと数値を切るのはOK
    • つまり、候補の約1000字に対して、「全ての文字を平等に扱う」ことになる。
      • ※固定ルールを予め実装するのはアリとする。「平」が再度になることは無い、漢数字は誤解が生じるため使わない、など
      • 数値やテキストの変更だけに依存し、誰がやっても同じ結果が出ること、と言い換えても良い。
  • おおまかな内容
    • 大量の日本語コーパス(wikipedia)or 漢籍&古典の学習結果をもとに、
    • お手本データ=過去元号、と似たような文字 = 「良い文字」を見つける
    • 画数なども踏まえて、元号で採用されるであろう漢字の候補を絞る
    • 漢籍&古典の出典がある組み合わせを探索する
    • MTSHチェック(明治大正昭和平成とイニシャルが重複しない)や、
    • その他の意味を持つかどうか、などの複合的な要因で、フィルタリング
    • 出てきた文字の組み合わせの「バランス」と「良字具合」を、AIに評価させる
    • 出典ごとに、評価値が高かったものを提示する
  • コードの実行環境は全て、Windows10 + Python3 +JupyterNotebook を前提。

元号予想の挑戦方法の方針

最初に、大きな進め方として2案ある。

前回は、「良い意味の文字」を見つけるために、
日本語コーパスに対する機械学習を行い、
char2vecの技術を用いて、
漢字同士の意味をベクトル化した。

それを踏まえて、以下の2方針をそれぞれ検討&実行した

方針案① 学習のINPUTに漢籍古典を使おう案
このchar2vecの機械学習の元データとして、漢籍や古典を使う
(最初から漢籍・古典をベースとする)という案

方針案② 出典を見つけにいこう案
現代日本人にとっての漢字の意味付けはWikipediaテキストデータで
既に出来ていると考えて、前回できなかった「出典」を探しに行く
(漢籍や古典を検索対象として使う)という案

方針案① 学習のINPUTに漢籍古典を使おう案

まず、「漢籍」=四書五経など、
と日本の古典は、それぞれ分けて考える。
そもそも言語が違う点と、「漢籍」のデータは
中国語の漢字で作られており、日本語の漢字とコードが一致しないため。

最終的には、両方とも試した。

まず、対象とするutf-8形式のテキストデータを
同じフォルダに集め、それらを結合したり、
余計な文字を排除したりしたデータを作って、
学習のINPUTファイルとする。

ファイルを集める
%%time
import codecs
import glob
import re
def open_ch_file(filepath):
    input_text = ""
    with codecs.open(filepath,"r", "utf-8") as f:
        input_text += f.read()
        #改行を小さな空白に変換
        input_text = input_text.replace('\r','')
        input_text = input_text.replace('\n',' ')
        # 数値を除去
        input_text = re.sub(r'[0-9]+', "", input_text)
    return input_text

folder_file_path = "XXXXX/XXXXX/*.txt"
file_list = glob.glob(folder_file_path)
input_text = ""
for filepath in file_list:
    input_text += open_ch_file(filepath)

#1文字ずつに区切る
chars = [c for c in input_text if c != u' ']

with codecs.open('KANSEKI.txt',"w", "utf-8") as f:
    f.write(u' '.join(chars))

集めたファイルに対して、word2vecの学習を実施する。
この部分のコードは、パラメータの違い以外は前回と同様。

%%time
import logging
from gensim.models import word2vec

logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)

sentences = word2vec.Text8Corpus('KANSEKI.txt')
model = word2vec.Word2Vec(sentences, size=40, window=12, min_count=3, hs=0, negative=5, iter=30)

model.save("mychar2vec_XXXXXX_model")

前回との大きな違いは、使用するパラメータ。
ひらがななどのデータが混ざっていないため、
windowサイズは小さめに設定してもいいかもしれない。
データの総量が少ないため、
次元数(size)やmin_countは小さめに。
繰り返し実行回数(iter)は大きめに設定するなど変更して、
何度か試している。
Wikipediaほどのボリュームがないため、
何度か試してもすぐに終わる点は楽。

出来たモデルの性能を以下のように試してみる。

お試し入力
out = model.most_similar(positive = [u'書'], topn=10)
print(out)
out = model.most_similar(positive = [u'右'], topn=10)
print(out)
out = model.most_similar(positive = [u'天'], topn=10)
print(out)
結果
[('記', 0.6210867762565613), ('諱', 0.5740101337432861), ('讀', 0.5511521697044373), ('古', 0.510854184627533), ('著', 0.5030744075775146), ('臧', 0.47281569242477417), ('蘇', 0.4715611934661865), ('告', 0.46267572045326233), ('論', 0.45687738060951233), ('噩', 0.4497215151786804)]
[('左', 0.6416146159172058), ('纛', 0.6342562437057495), ('殳', 0.6199294924736023), ('拒', 0.5840848684310913), ('夾', 0.5733482241630554), ('旆', 0.5700335502624512), ('綦', 0.5488435626029968), ('衽', 0.5380955934524536), ('把', 0.5014756917953491), ('肩', 0.47802940011024475)]
[('陛', 0.8151514530181885), ('祇', 0.5380247235298157), ('垓', 0.5238890647888184), ('上', 0.4873164892196655), ('歿', 0.4687088131904602), ('宜', 0.4571455717086792), ('祗', 0.447026789188385), ('佑', 0.43108344078063965), ('綱', 0.4226115643978119), ('足', 0.4220472574234009)]

「書」に対して「記」
「右」に対して「左」
が出てくるため、多少上手くいっているところはあるが、
2位以降が良く分からない。
wikipediaで作ったものとは全くレベルが違う。

試しに200種類にクラスタリングした結果を見ると、
「東西南北」が同じクラスタに入っていたり、
「父母兄弟」が同じクラスタに入っていたりと、
良い部分もいくつか見られたものの、
予想で使えるレベルにはなっていなかった。

「出典になりそうな良い古典」だけをINPUTにした場合、
やはり、単純にデータボリュームが小さいために、
様々な漢字を網羅した学習モデルを作るのには無理があった。
上記の例は、漢籍の出力例だ。日本の古典でも同様かそれ以下であった。

方針案①の結論:
「学習用データ」として「古典」を扱うことはそのデータ量的に難しい、と判明した。

方針案② 出典を見つけにいこう案

案②は、結論から言えばある程度上手くいった。
前回と同様の方法で、採用されるであろう漢字候補を出し、
その漢字が古典上で使われている位置を検索、
「出典」と言えるほど近しい場所で使われているペアを見つけて
抽出するという考え方。

また、「漢字候補を出す場所」
「出てきた元号をチェック(評価)する場所」、
それぞれで、前回に比べて様々なパワーアップを実装している。

以降、追加した点や、ポイントを、実装含めてご紹介する。

漢字候補の抽出

いきなり、この物語の最大の核心的な関数を提示する。

元号に使われている漢字=「良い意味」を持つ、と仮定して、
常用漢字リストor教育漢字リストの中から、
その「良い意味」に近い文字を見つけたいという趣旨。

例えば、元号で3回以上使われている、以下の文字列を「お手本」と考える。

  • 永元天治応和長正文安延暦徳寛保承仁嘉康建慶久平弘貞享宝禄明大亀寿万化養観喜中政

お手本との距離(コサイン類似度)を計算する関数と、
全漢字に対して、その距離を求めて、近い字の上位を返す関数を、
それぞれ以下のように実装した。

#与えられた漢字と「お手本」との距離感を算出する。
#与えられた漢字と近い上位20%のお手本時の距離の平均。
def get_otehon_ave_ruijido(char, otehon_str):
    jyoui_kosuu = round(len(otehon_str)/5)
    distance = 0
    cnt = 0
    distance_list = []
    for stridx in range(0, len(otehon_str)):
        distance = JPmodel.similarity(char, otehon_str[stridx])
        distance_list.append(distance)

    distance_list.sort(reverse=True)
    distance_list=distance_list[0:jyoui_kosuu]
    ave = sum(distance_list)/len(distance_list)
    return ave

#お手本を入力すると、それと似た漢字の一覧を返す関数
#(どれくらい似ている文字を返すのか指定する)
def get_Gengou_Kouho_Kanji(otehon_str, target_str, min_ruijido):
    char_val_list=[]
    for char in target_str:
        if char in NG_STR:
            continue
        #元の文字と、お手本として指定したリストとの平均値をリスト化
        char_val_list.append([char, get_otehon_ave_ruijido(char, otehon_str)] )
    #ソート
    char_val_list = sorted(char_val_list, key=lambda x:x[1], reverse=True)
    gengou_kouho_kanji_str = ""
    for char_val in char_val_list:
        if char_val[1] >= min_ruijido:
            gengou_kouho_kanji_str += char_val[0]
    return gengou_kouho_kanji_str

前回は、それぞれの「お手本」に似ている文字を選んでから
各文字を「評価」するという段取りにしていた。
今回は、「評価関数」を先に作っておき、
常用漢字リストや教育漢字リストなど、
任意の対象に対して、その全漢字を「評価」出来る仕組みとした。

結果、「教育漢字」を対象にすると、ポイントの高い順に、
以下の結果が得られた。
ここでは、TOP50位までを表示している。

  • 永徳忠仁孝久清元康天安正喜幸松竹坂田氏宝明晴臣宮守豊城宗孫延老蔵郷皇養誠倉家里戸納長志尊太恩文寺賀敬

上記の結果出来た候補漢字を、
過去の元号の採用回数とともに出力してみよう!

候補漢字に過去採用回数を付けて出力する
for char in GENGOU_KOUHO_KANJI_STR:
    if char in OTEHON_STR:
        print(char ,":は",otehon_val_dict[char] ,"回過去使用(お手本リストに含む)")
    elif char in ALL_OTEHON:
        print(char ,":は",otehon_val_dict[char] ,"回過去使用(お手本リストに含まれない=AIが発見)")
    else:
        print(char , ": はどちらにも含まれていない、AIが見つけた新字です★")
出力結果
 :は 29 回過去使用(お手本リストに含む)
 :は 16 回過去使用(お手本リストに含む)
 : はどちらにも含まれていない、AIが見つけた新字です★
 :は 13 回過去使用(お手本リストに含む)
 : はどちらにも含まれていない、AIが見つけた新字です★
 :は 9 回過去使用(お手本リストに含む)
 : はどちらにも含まれていない、AIが見つけた新字です★
 :は 28 回過去使用(お手本リストに含む)
 :は 10 回過去使用(お手本リストに含む)
 :は 23 回過去使用(お手本リストに含む)
 :は 17 回過去使用(お手本リストに含む)
 :は 19 回過去使用(お手本リストに含む)
 :は 3 回過去使用(お手本リストに含む)
 : はどちらにも含まれていない、AIが見つけた新字です★
 : はどちらにも含まれていない、AIが見つけた新字です★
 : はどちらにも含まれていない、AIが見つけた新字です★
 : はどちらにも含まれていない、AIが見つけた新字です★
 : はどちらにも含まれていない、AIが見つけた新字です★
 : はどちらにも含まれていない、AIが見つけた新字です★
 :は 7 回過去使用(お手本リストに含む)
 :は 7 回過去使用(お手本リストに含む)
 : はどちらにも含まれていない、AIが見つけた新字です★
 : はどちらにも含まれていない、AIが見つけた新字です★
 : はどちらにも含まれていない、AIが見つけた新字です★
 : はどちらにも含まれていない、AIが見つけた新字です★
 : はどちらにも含まれていない、AIが見つけた新字です★
 : はどちらにも含まれていない、AIが見つけた新字です★
 : はどちらにも含まれていない、AIが見つけた新字です★
 : はどちらにも含まれていない、AIが見つけた新字です★
 :は 16 回過去使用(お手本リストに含む)
 :は 1 回過去使用(お手本リストに含まれない=AIが発見)
 : はどちらにも含まれていない、AIが見つけた新字です★
 : はどちらにも含まれていない、AIが見つけた新字です★
 : はどちらにも含まれていない、AIが見つけた新字です★
 :は 3 回過去使用(お手本リストに含む)
 : はどちらにも含まれていない、AIが見つけた新字です★
 : はどちらにも含まれていない、AIが見つけた新字です★
 : はどちらにも含まれていない、AIが見つけた新字です★
 : はどちらにも含まれていない、AIが見つけた新字です★
 : はどちらにも含まれていない、AIが見つけた新字です★
 : はどちらにも含まれていない、AIが見つけた新字です★
 :は 19 回過去使用(お手本リストに含む)
 : はどちらにも含まれていない、AIが見つけた新字です★
 : はどちらにも含まれていない、AIが見つけた新字です★
 : はどちらにも含まれていない、AIが見つけた新字です★
 : はどちらにも含まれていない、AIが見つけた新字です★
 :は 19 回過去使用(お手本リストに含む)
 : はどちらにも含まれていない、AIが見つけた新字です★
 : はどちらにも含まれていない、AIが見つけた新字です★
 : はどちらにも含まれていない、AIが見つけた新字です★

前回の候補文字に近い。
上位50位 = 全体(教育用漢字約1000個)の5%の、
「過去元号(=お手本)と意味が似ている」とAIが捉えられた結果がコレ。

前回優勝者の「孝」をはじめとし、
「幸」「晴」「豊」「賀」なんてのも、
実はいままで元号に使われていなかった、のは少し驚き。
っぽい文字(?)が出ている気はする。

なお、INPUTを「常用漢字」に拡大する場合、TOP50には
「隆」「賢」「澄」「江」「雅」などが、新字として入ってきていた。

人間の感覚としては、ちょっと元号としてはどうかなー、
という字もあるのだが、AIの計算結果では、
これらの字が元号の文字に近いとみなされたのね、
というように見ると、それはそれで面白い。
何となく、「宮」「城」「郷」「家」「里」「寺」など、
場所系の言葉が近しいと思われたのだろうか。
「天」もある意味では良い場所を示しているし、
「建」などの字もお手本にあるから?

フィルタリング①画数

出てきた文字に対して、「書きやすい」の条件を満たすために、
画数でフィルタリングをかけることにする。

IPAが試験提供している以下のAPIを用いて、
予め常用漢字に対して、全部の画数を取得しておいた。

MJ文字情報取得API
https://mojikiban.ipa.go.jp/search/help/api

特定の漢字リストを使ってAPIデータを取得して保存しておく
import time
import pickle
target_list_dict = {}
for char in target_list:
    #スリープは必須
    time.sleep(3)
    api_result = get_char_data(char)
    if api_result['status'] == "success":
        print(api_result['results'][0]['読み'])
        target_list_dict[char] = get_char_data(char)
    else:
        print("API-ERR")
        #print(api_result)

print("API-Finish")
with open('jyouyou_kanji.dic', mode='wb') as f:
    pickle.dump(target_list_dict, f)

print("pickle-Finish")

何度も同じ漢字についてAPIを投げるのは、
API提供者の情報処理推進機構殿に申し訳ないので、
今回使う分はあらかじめ上記のように一回だけ取得して、
pickleで保存しておくことによって、
あとはローカルで使えるようにしておいた。

使い方
import pickle
import pprint
with open('jyouyou_kanji.dic', mode='rb') as f:
    KANJI_INFO_DIC = pickle.load(f)
pprint.pprint(KANJI_INFO_DIC["和"])
print(KANJI_INFO_DIC["皇"]['results'][0]['総画数'])
結果
{'count': 1,
 'find': True,
 'results': [{'IPAmj明朝フォント実装': {'フォントバージョン': '005.01', '実装したUCS': 'U+548C'},
              'JISX0213': {'包摂区分': '0', '水準': '1', '面区点位置': '1-47-34'},
              'MJ文字図形': {'MJ文字図形バージョン': '1.0',
                         'uri': 'http://mojikiban.ipa.go.jp/MJ008199.png'},
              'MJ文字図形名': 'MJ008199',
              'UCS': {'対応するUCS': 'U+548C', '対応カテゴリー': 'A'},
              '住基ネット統一文字コード': 'J+548C',
              '入管外字コード': '',
              '入管正字コード': '548C',
              '大字源': 1162,
              '大漢和': '3490',
              '大漢語林': 1374,
              '戸籍統一文字番号': '040260',
              '新大字典': 1886,
              '日本語漢字辞典': 1397,
              '漢字施策': {'人名用漢字': True, '常用漢字': True},
              '登記統一文字番号': '00040260',
              '総画数': 8,
              '読み': {'訓読み': ['やわらぐ', 'やわらげる', 'なごむ', 'なごやか', 'あえる'],
                     '音読み': ['ワ', 'オ', 'カ']},
              '部首内画数': [{'内画数': 5, '部首': 30}]}],
 'status': 'success'}
9

このように、特定の漢字に関する詳細データ、画数などが
いつでも取得できるようになった。
元号予想だけでなく、かなり便利なデータを作れた!!

この関数を使って、さきほどの要領で出した候補文字に対して、
一定の画数以下である、というフィルターをかける。

では、その「一定の画数」とはいくつなのか?
過去元号の画数を調べる。
(※画数取得関数は上の例ですぐ作れるのでコードは省略)

過去元号に使われている漢字の画数をグラフ化
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt

gengoulist = ["大化","白雉",,,"省略",,,"昭和","平成"]

kakusuulist=[]
for idx in range(0, len(gengoulist)):
    kakusuu = get_Gengou_kakusuu(gengoulist[idx][0])
    kakusuulist.append(kakusuu)
    kakusuu = get_Gengou_kakusuu(gengoulist[idx][1])
    kakusuulist.append(kakusuu)

# 折れ線グラフを出力
left = np.array(range(0, len(kakusuulist)))
height = np.array(kakusuulist)

plt.plot(left, height)

from statistics import mean, median,variance,stdev

m = mean(kakusuulist)
median = median(kakusuulist)
variance = variance(kakusuulist)
stdev = stdev(kakusuulist)
print('平均: {0:.2f}'.format(m))
print('中央値: {0:.2f}'.format(median))
print('分散: {0:.2f}'.format(variance))
print('標準偏差: {0:.2f}'.format(stdev))
結果
平均: 7.83
中央値: 8.00
分散: 12.90
標準偏差: 3.59

kakusuu.png

ということで、
平均はおよそ8画と判明した。
右側(近代側)にいくにつれて、画数が減っているかなー、
ということを少し期待してグラフ化してみたが、
あまり変わっていないように見える。
画数などを本格的に気にしていたのは
恐らく本当に最近だけなのでしょうね。

この結果を元に数字を決めて、画数でのフィルタリング処理を実装した。
(結果はあえて省略)

なお、ついでに、「音読み」での読み方を元に、
MTSH除外(明治大正昭和平成と同じアルファベットにならないように)
という簡易的なチェックも実装できた。

フィルタリング②他で使われている言葉を除外

漢籍や古典とのマッチング結果後で良いのだが、
他で使われている言葉は使えないので除外する必要がある。

実際に試行錯誤を進めていくなかで、
「延安」という組み合わせが上位候補として出現した。
しかし、「延安」は中国の都市名として存在している。
「延安市」という項目がwikipediaに存在する。

前回は、mecabの辞書による除外のみを実装していたが、
mecabの辞書はそこまで万能には使えない。
(いろいろ使いにくい点がある)

そこで、wikipediaの全タイトル項目を取得し、
その最初の2文字&最後の2文字、については、
既に別な何かが存在するor誤解が生じやすいのでNG、
というフィルタリングを考えた。

上記で言えば「延安」はコレでフィルタ出来るし、
有名人の苗字や、有名人の名前、の大部分もフィルタ出来る。
例:田村正和⇒「田村」と「正和」が除外リストに登録される。

wikiのタイトルリストから、除外リストを生成する
import codecs
#wiki_title/jawiki-latest-all-titles-in-ns0
def makeWikiTitleList(filepath):
    WIKI_TITLE_LIST_MAE = []
    WIKI_TITLE_LIST_ATO = []
    infile = codecs.open(filepath,"r", "utf-8")
    for line in infile:
        if len(line) > 1:
            #m前の二文字
            WIKI_TITLE_LIST_MAE.append(line[0:2])
            #m最後の二文字
            WIKI_TITLE_LIST_ATO.append(line[-2:])

    infile.close()
    #重複削除
    WIKI_TITLE_LIST_MAE = list(set(WIKI_TITLE_LIST_MAE))
    WIKI_TITLE_LIST_ATO = list(set(WIKI_TITLE_LIST_ATO))

    return WIKI_TITLE_LIST_MAE, WIKI_TITLE_LIST_ATO

「2文字」ちょうどのタイトルだけ除外するわけではなく、
前後2文字除外、というルールであるため、
実はこれは結構強力なフィルターで、人名地名をはじめ、
誤解が生じやすい系、他の意味に捉えられそう系、をかなりはじく。

漢籍や古典とのマッチング

他にも、過去天皇名などのフィルタや、
評価準備を進めたが、既に超長いので、
細かいものは省略して、いよいよ漢籍や古典とのマッチングに入る。

「与えられた一定距離内に、候補とする漢字がある状態」
を、入力文章内から全探索するコードである。

ちょっと汚いコードになってしまった。
出典がどこか分かるように、近隣部分を合わせて表示したり、
お手本(元号の漢字)と該当の候補漢字との類似度を表示したり、
などのインフォメーション系を追加しているのでごちゃごちゃしている。
(実際はさらにもうちょっと情報を追加)

一定距離内に居る候補文字を、入力テキスト中から検索する関数
#テキストリストを入れると、近接して候補文字を使用しているところと、その場所を返す関数
def get_Gengou_Kouho_kinsetu(OTEHON_STR, GENGOU_KOUHO_KANJI_STR, target_str, max_kyori):
    kouho_resultlist =[]
    kaisi_no = 0
    target_str_len = len(target_str)
    while kaisi_no < target_str_len :
        kaisi_char = target_str[kaisi_no]
        if kaisi_char in GENGOU_KOUHO_KANJI_STR:
            kaisi_to_end = 1
            #全体が入っている&上限文字数を超えない、という両条件
            while kaisi_no + kaisi_to_end < target_str_len and kaisi_to_end <= max_kyori:
                end_no = kaisi_no + kaisi_to_end
                end_char = target_str[end_no]
                if end_char in GENGOU_KOUHO_KANJI_STR:
                    #両方とも、候補となる文字列に入っている状態
                    distance = JPmodel.similarity(kaisi_char, end_char)
                    inyou_kaisi_no = kaisi_no-5
                    if inyou_kaisi_no < 0:
                        inyou_kaisi_no = 0
                    inyou_end_no = end_no+1+5
                    if inyou_end_no > target_str_len-1:
                        inyou_end_no = target_str_len-1

                    kouho_resultlist.append([kaisi_char+end_char,
                                            kaisi_to_end,
                                            target_str[inyou_kaisi_no : inyou_end_no],
                                            get_otehon_ave_ruijido(kaisi_char, OTEHON_STR),
                                            get_otehon_ave_ruijido(end_char, OTEHON_STR),
                                            distance
                                            ])
                #検索対象をずらす
                kaisi_to_end  += 1
        else:
            pass
        kaisi_no +=1
    return kouho_resultlist

この関数を用いて、フジテレビ殿に用意していただいた
各種漢籍や、古典に対して、検索をしていくと、
AIの見つけた良い漢字の2文字の組み合わせ」で、
漢籍、古典内に出典と言える場所がありそうなペア
が見つかるというわけだ。

実際に、候補をいくつか見つけることができた。

ここで、ちょっと面白い点は、日本の古典を対象にした場合、
もともと、「XX天皇」という表記が含まれることが多すぎて、
過去の天皇名や、その一部の文字を使った結果ばかりが
出てきてしまうという傾向があった。
そうした名称の一部を使うことは、「出典」ではないため、
日本の古典を扱う場合には、「XX天皇」及び「XX」の部分は、
予め全て消去したテキストデータを用いたことを報告しておく。

最後に、そうして見つけた候補に対して
元号としてどの程度相応しいのか数値評価」を行う。

ベースは、各文字がどこまで「お手本」に近いか、と
前回同様の、漢字2文字間の距離、の判定だ。
さきほどのwikiタイトルフィルタなどを入れておき、
引っ掛かったものは0点=除外するというチェックや、
MTSH除外などのチェックも実施する。
(前回は、評価以前に絞り込みでかなり候補が減ってしまったから、
 消去法的なところもあったため、今回は、
 候補を多めにとって、フィルター条件はきつくしながらも、
 最後は相応しい度合の数値、の上位を出したい)

この後の結果も書きたいところではあるが、
まだ最終段階は確認中であることと、
そもそも、結果まで書いてしまうと
官邸側に回避されてしまうこともあり得て、
ガチ当てにならないので、
一旦、残念ながらここまでの記載とする。

続く
~ to be continued ~

あとがき、所感

★この記事への反応(いいね)が多ければ、後日必ず続きを書きます。
※元号当ては、前回も今回も長かったように、書くのが大変なんですよね。
 もし、続きが知りたい人が多ければ、頑張って書きます。
 少なければ、あとはテレビ見てください、だけでいいかな。
 気になった方は忘れないうちに「いいね」しておいてくださいませ。

■所感:
今回は単純なchar2vecによるAI学習だけでなく、
「計算だけ」での的中確率を高められるように
既存用語との重複チェックや、画数判定、
そして特に出典箇所の検索など、
実際の元号判定で行われていそうな内容
できるだけ全てプログラムで完結するように、実装していった。
かなり泥臭い部分を作りこんだ、とも言える。

ここまで作ってくると、もはやAIによる予想というより、
本当に元号を選んでいる側の人たちに、
これらのプログラムを差し上げて、
「候補の発見」や「チェック」を楽にするような仕組みとして
使っていただくほうが良いような気もしてきた。
選定作業の効率がアップすることは間違いない気がする。
今からでもホンモノの人から申し入れがあればいつでも差し上げる。

あとは、発音的なチェックが、基準が難しく出来ていないというくらいだ。
(言いやすい=どういう基準で?、とか、
 音だけで別な意味に聞こえる=どこまでマイナー単語を含めるべき?、とか)

今回、出典に検索をかけたとしても、
自分自身で気に入った文字や、予想や、組み合わせの好みを、
直接は使えないことは、かなり難しく感じた。
あくまで「数値」や「プログラム上の調整」だけで、
全てを決めていくことはかなり難しい。
(パラメータのチューニングは行っているわけで、完全に私見がゼロかと言われると、
 多少は入ってしまうのかもしれないが、極力自身の感性や勝手な予想は除外、の意味)

世の中に多く出回っている「元号予想」は、
その人自身の考えた理想、日本への期待、祈り、カン、が込められたものだ。
そのため、横から見ると実際は結論ありきであり、
予想プロセスは不透明」だが「予想者自身にとっては正しい予想」となる。
一方、今回のAIでの予想は、
予想のプロセスは透明」だが「予想者自身にとっては正しくない予想」で真逆だ。

とはいえ、一度演算してみると、
前回の結果「孝天」といい、
今回の結果「(ヒミツ)」といい、
しばらく眺めていると愛着が湧いてくるのが不思議である。
これが良い漢字の持つエネルギーなのだろうか?

恐らく、4月1日の発表に対しても、多くの人が、
最初は疑問や違和感を持ちつつも、
しばらく眺めているうちに愛着が湧いてくるのだろうな
と思し、そうなって欲しい。
みなが愛着を持つ良い時代になって欲しい。

以上。
次回へ続く(?) かどうかは反応次第

【続々】平成の次の元号を、AIだけで決めさせる物語(@直前スペシャル)

前書き

平成の次の元号をAIで決める物語&そのテレビ取材編の続き。

前回までで、プログラムは完成していたのだが、
大きな「マッタ」がかかってしまった。
出した予想が当たらないことが分かってしまったのだ。

そこで、全結論が変わるほどの変更を行うことになった。
さらなる「ガチ当て」をするようにブラッシュアップした。
変更が生じたため再度、本番4/1のテレビ放送前に、その詳細、
VTRの尺に収まらなそうな部分を先行公開することにする。

前回、多くの方にご声援を頂いた。
楽しみと言ってくださる方のためにも、続編をちゃんと書く。
応援ありがとうございます!!

初回の記事と、前回の記事については、以下をご参照。

初回の記事⇒
平成の次の元号を、AIだけで決めさせる物語

前回の記事⇒
【続】平成の次の元号を、AIだけで決めさせる物語(@テレビ取材)

前回までの話を3行で言うと、
AIで元号予想したらテレビ取材を受けることになったよ、
出典も含めてガチで当てがんばるよ、発表直前に放送予定。
VTRに入らない深い内容はQiitaで公開するよ、だ。

予想が当たらない事件の経緯

当初予想

前回の記事を書いた時点で、
以下のような全ルールを満たす元号を
「プログラム」によって算出していた。

  • 教育漢字2文字(または1つが常用漢字)
  • AIにとって、過去元号の文字に近い=「いい意味の漢字」である
  • AIにとって、組み合わせが過去元号の「文字の距離感」に近い
  • 中国または日本の古典に"出典"がある(少なくとも、近距離で出現)
  • Mecabの辞書に存在していない
  • 過去の元号、過去の天皇名と重複しない
  • MTSH以外(明治大正昭和平成とイニシャルが重複しない)
  • 漢字の画数が一定値以下(過去元号の平均画数等も参考)
  • WIKIのタイトルの冒頭2文字と不一致 ≒ 苗字/地名などで俗用されていない
  • 【制約事項】作者の漢字に対する好みを入れてはいけない。数値計算のみで算出

なお、上記ルールにはそれぞれ、一発アウトな除外必須ルールと、
ポイントの加算減算が行われるルールがあり、
最終的には、全ての条件を満たしてポイントの高いものが選定される。

ここまででも結構大変なんです。機械が漢字で新語を作るのだから。

そして、実は前回書かなかった結論として、
仁久
隆永」(常用漢字も含む場合)
などを算出していた。

「出典」の文章の長さと候補漢字の個数にもよるが、
長い文章だと、4000個ほど、近距離にある候補漢字のペアを見つけて、
300個ほど(重複もある)、一発除外を潜り抜けたペアを返して、
評価値の高い5個を残すような感じ。

複数の出典に対して上記選定を実施し、
さらにその中で評価値の高い候補がコレ。
かなりの倍率を潜り抜けてきただけあって、
結構それっぽいのではないか?
フィルタリングの条件も十分に設定している。

なお、「仁久」は
「万葉集」「礼記」にそれぞれ出典らしき箇所が見つかったのだが、
「万葉集」はそもそも、漢字は「当て字」的に用いられているため、
万葉集を用いて良いかどうかは議論を要する。

あとは、どの古典から第何位までの元号を放送するか、とか、
教育漢字、常用漢字の扱いをどうするか、とか、
「見せ方」をプロデューサ様に決めてもらうだけの状態。
私も、プログラムの大部分をqiitaで公開し、
放送を待つだけっ・・・!!
と、思っていた・・・・・。

当たらない事件発生

プロデューサ様「大変ですっ!このままでは予想は当たりません!」

「仁久」=高島屋の従業員がトイレに行く際の隠語
     仁久歯科、という歯医者が存在する(知らんがな)
「隆永」=西郷隆盛の、改名する前の名前(めっちゃ知らんがな)

プロデューサ様「ガチ当てなのでそうした語彙も機械的に除外出来ません?」

(*´Д`)「辞書には無い言葉ですし、フツウに考えて難しくないですか・・・?」

出資者は無理難題をおっしゃる」by クワトロ・バジーナ

( ´Д`)「AIだけでそれっぽいものを出すだけでも大変なのに・・・」
( ´Д`)「うちのAIちゃんは、高島屋の従業員じゃないし・・・」
( ´Д`)「うちのAIちゃんは、うんちしないし・・・」

決意

とはいえ、私も思うところはあった。
結論が出たあとで、Google先生に聞いて、
仁久=トイレに行く際の隠語、らしいので微妙だなー、
とは思っていた。
思いながらも、最初に掲げた条件には引っ掛からなかったから
しょーがないのでは?と妥協していたのだ。

まだちょっと私の中で、「ガチ当て」への温度感よりも、
「AIがそれっぽいものを出したこと」への
満足度の方が高かったのかもしれない。

しかし、ここで妥協してしまっては、当てることは出来ないっ!

実際の「平成」の時も、"大先生"が元号候補を出すたびに、
担当官から、「すいません、それは俗用されており・・・」的な
やりとりが何度も生じて、相手が"大先生"だけに大変だった、
的な逸話をどこかで読んだ覚えがある。
私は"大先生"ではないが、プロデューサ様も、担当官も、大変だ。

もちろん、出した結果を人の目で精査した後で結論とする、
というのは簡単だが、あくまでもプログラムだけで出したい。

「人事を尽くして天命を待つ」
AI(ノートパソコン)も唸りを上げて頑張ってくれましたし、
私も、人事を尽くせるところまではやってみましょう!

人類の可能性に限界が無いならば、
プログラムが出来ることにも限界はない
       ~  Char Fuitter (1847~1912 オランダ) ~

新・俗用フィルタの実装方針

元号ほど高尚なものを決めるとなると、
「辞書に載っている」「WIKIに存在」というレベルだけでは、
高島屋のトイレの隠語や西郷隆盛の旧名までは対応できず、
「俗用されているか」の判断として不十分であった。

もはや何と戦っているのか分からない

では、我々はどのようにして「俗用」を判断するのだろうか?
我々の「感覚」的には(高島屋従業員様を除いて)
「仁久」「隆永」はアリ、である。

今回の判断理由は「Google先生に聞いた結果」だ。

では、GoogleのHIT件数を調べて、HITが少なかったら?
Google先生はいろいろ気を利かせてくれるので、
特に2文字レベルでは、単純なHIT件数はアテにならない。

一方、「仁久」を検索すると、
「コトバンク」が一位にヒットして、
高島屋系列のトイレの隠語であることを教えてくれる。

俗用フィルタの方針①「タイトルチェック」

そこで、
検索結果上位20件のページのタイトルを一覧化して、
そのタイトルそれぞれに対して、俗用有無の評価をする
という方針を考えた。
つまり、「コトバンク」が出てきたらアウト!
他に、「ホットペッパー」とか、「会社」とかが出てきたら、
既に店名や企業名で存在して俗用度が高いことになる。
「Navi」が出てきた場合、だいたい地名として用いられている。
「一般人の人名」=Facebookなど、は少し迷うところだが、
評価を下げるだけということにしておく。

#人名らしきもの全て無しにしたら消えすぎかも。
 「平成」も「へなり」という地名はあったし
 多少の重複は現代においては許容範囲か?

俗用フィルタの方針②「画像検索」

何度かGoogle検索をかけて気付いたことがある。
俗用されていないような言葉の場合、

「XX」の画像検索結果、が比較的上位に出現する

Google先生から見れば、オマエの調べているものって、
「言葉」として存在しないから「画像」じゃね?
って言ってくるわけだ。

そこで「画像検索結果」が上位に来るものは、
俗用度が低い、ということにした。

画像検索の順位というのは新たな気付きだった。

俗用フィルタの方針③「ポイント制」

そもそも、「隆永」はアウトなのだろうか?
西郷隆盛の改名前の名前だから何だというのだ?
どういう基準で官邸がNGにするのかが明確に言えないため、
「何かで一発アウト」にするような形ではなく、
コトバンクは俗用度ポイントが高い、
「画像検索」が上位に来るとポイントが低い、
などといった、「ポイント制」を採用することにする。

Google俗用フィルタの実装

検索結果の取得

まず、Google検索結果をリスト化する必要がある。
先人の偉大な知恵を拝借して、
Daiさんのget_search_results_df関数をお借りする。
ご参考: https://note.mu/daikawai/n/n7cb363531396

Google検索結果取得
from bs4 import BeautifulSoup
import requests
import pandas as pd

def get_search_results_df(keyword):
    columns = ['rank','title','url', 'affiliate_url']
    df = pd.DataFrame(columns=columns)
    html_doc = requests.get('https://www.google.co.jp/search?num=20&q=' +keyword).text
    soup = BeautifulSoup(html_doc, 'html.parser') # BeautifulSoupの初期化
    tags = soup.find_all('h3',{'class':'r'})
    rank = 1
    for tag in tags:
        title = tag.text
        url = query_string_remove(tag.select("a")[0].get("href").replace("/url?q=",""))
        affiliate_url = ""
        se = pd.Series([rank, title, url, affiliate_url], columns)
        df = df.append(se, ignore_index=True)
        rank += 1
    return df

def query_string_remove(url):
    return url[:url.find('&')]

早速使ってみよう。

検索実行
search_results_df = get_search_results_df("仁久")
search_results_df.head(20)

結果(一部)
遠方/仁久(エンポウ/ジンキュウ)とは - コトバンク
仁久 という名前の読み 29件(ひとひさ,まさつね,じんく,とよひさ,男の子 ...
あるデパートの従業員トイレを使ってるのですが、トイレでなく「仁久(じ...
トイレの隠語 さるのちえ - 楽天ブログ
仁久 人名漢字辞典 - 読み方検索
「仁久」の書き方・読み方 男の子の名前 - 漢字書き順辞典
「仁久」の男の子の読み方 - パパママいい名前つけてね
「仁久」の時刻表/バス乗換案内/路線図/地図 - NAVITIME
JR松山駅から仁久までの乗換案内 - NAVITIME
川崎仁久 - Wikipedia
・・・・・

はっきり言って、「仁久」はボロボロだった
「コトバンク」だけでなく、
「地名」の可能性すら含んでいて、
Wikiの有名人にも含まれていた。

※ 実は、前回実装した「WIKIフィルタ」には一部誤りがあり、
WIKIタイトルの「最後の二文字」のフィルタが上手く動作しておらず、
前回時点では「冒頭二文字」分のフィルタしか動作していなかった。
そのため、実際は「仁久」はその誤りを訂正すれば除外できたが、
例として、そのまま採用。
そもそも、冒頭二文字は、地名や苗字などという目的があったが、
最後の二文字は、せいぜい名前なので、そこまで除外するのか?
という迷いも生じていた。今回から正式に採用を決定。

一方で、「俗用されていない」と見える候補では、
だいたいTOP10以内で、
「XX」の画像検索結果
が出現する傾向にあった。

タイトルの評価関数の実装

このようにして取得した、検索結果の
各ページのタイトルを使って、
「俗用度合」を数値化してみる。
この関数の作り方(パラメータ)は諸説あり、
Facebookの人名や、店名などを、どこまで厳しく除外するべきか、は
大変難しいところだ。
複雑な分岐を入れて一週間くらい試行錯誤すれば"完璧な"フィルター
(つまり、人が検索結果を見て判断するのと同レベルのフィルター)
が出来るかもしれない。
しかし、そもそもどういう基準で俗用は却下されるのか、
イマイチ判断がつかないところもあり、
「除外しすぎ」だと何も残らないような状態になってしまう。
あくまでも簡易的な数値化である。

もしかしたら実際に選定している人は、
全戸籍データや、国内外の全会社名/商標データなどを
準備して臨んでいるのかもしれない。凄いなぁ。

タイトルの評価関数
def web_zokuyou_checker(gengou_str, title_list):
    counter = 1
    keyword = gengou_str + " の画像検索結果"
    keyword_eng = "Images for " + gengou_str
    add_val = 1
    for val in title_list:
        if keyword in val or keyword_eng in val:
            #以降の増加が大きく減少。画像検索が上位なほど有利
            add_val = 0.2
        #自分自身が含まれていて、「元号」の話題ではない場合
        #ランクが下がっていく
        if gengou_str in val:
            #元号の話題で触れられている場合は若干の加点
            if "元号" in val :
                counter -= 0.3
                continue
            if "コトバンク" in val :
                counter += 15
                continue
            if "はてなキーワード" in val :
                counter += 15
                continue
            #名前はまだありだが、苗字系はNG
            if "人名漢字辞典" in val or "日本姓氏" in val or "名字" in val or "苗字" in val:
                counter += 3.5
                continue
            if "Facebook" in val :
                counter += 1.5
                continue
            if "NAVITIME" in val or "会社" in val or "地図" in val or "いつもNAVI" in val :
                counter += 2
                continue
            if "老人ホーム" in val or "福祉" in val or "介護" in val :
                counter += 2                
                continue
            if "映画" in val :
                counter += 2                
                continue
            if "Retty" in val or "食べログ" in val or "ぐるなび" in val or "ホットペッパー" in val :
                counter += 2                
                continue
        else:
            #自分が入っていないといっても紛らわしいのでペナルティ
            counter += 0.3
            continue
        counter += add_val
    return counter

このような関数を用意することで、
(パラメータの数値や、IF文の条件は適宜変更)
例えば何かの名前に使われていそうな候補に、
「俗用度ポイント」を高く設定することができる。

人間が、Google検索結果を見て、
コレは俗用されてるんじゃね?となぜ思ったのか、
その理由を一つずつ組み込むだけだ。
ただし、組み込みすぎると、「本来はいいもの」も
落とすことになるため、注意を要する。

実際の選定者も、結局は似たことを
(人手で)やっているんだろうなー、
と、思いをはせながらプログラムを書いていく。

Google俗用フィルタの大問題=リクエスト制限

あとは、出てきた結果とこの関数を組み合わせて
個々に評価していけば良いのだが、
大きな問題があった。

「Google検索」を連続して実施する場合、
100回/日くらいの検索で、
一次的に制限がかけられてしまう模様

Google俗用フィルタに渡す前の時点で、
十分に絞り込んだ精鋭を渡す必要がある。

#この制限が、前述の、フィルタ条件の調整、を
 心行くまで調整/実装できなかった最大の理由である。

また、検索する度に、微妙に結果が変わる場合があるが、
それは今回は大きな問題ではない。

配られたカードで勝負するっきゃないのさ… by スヌーピー

制限の範囲で出来ることをやりきるしかない。
ここまでのアイデアで
古典の検索結果(元号候補)を再評価してみよう!

再評価の結果

そして誰もいなくなった

再評価の結果をヒトコトで言うと、
候補が足りなくなったw

全部が消えたわけではないものの、
フィルタが強力であったため、
当初出そうとしていた候補の個数を割り込んでしまった。

AIが候補としていた漢字の個数を増やす、
なども考慮したが、もともと元号には72種類ほどの漢字しか
過去に使われたことが無いため、
(教育漢字に限定するとさらにさらに少ない)
AIが見た「良い意味」であっても、
その近辺の数値より候補の個数を増やすと、
「ゴミ」が増えてくるような印象がある。

今回、フジテレビ殿に用意していただいた「出典」は
過去に出典として使われたことのある漢籍の一部でしかないため、
出典をもっと増やす、のが候補を増やす本筋ではあるが、
既に時間が足りていなかった。

対策&さらなるガチ当て化 = 落選元号

そこで、「既に出典がある」候補を増やすアイデアを思い付いた。

出典=「落選元号」リストを使うという案だ。

実は、
「明治」は室町時代~江戸時代で計10回、
「大正」は鎌倉時代~江戸前期で計4回、
「平成」は幕末に1回、
候補にあがりながらも落選した末に、
やっと元号に選ばれているそうだ。

今回の選定においても、
過去の「落選元号」が再登場する可能性は十分に高い

また、これらの「落選元号」は、
出典が既に用意されているもの、と考えられる。
(こちらから見れば、出典の検索対象として、
 「落選元号」を追加すれば、過去の人の
 検索結果を横取りできる、ようなもの)

これらの「落選元号」に対して、
用意したフィルターがどこまで効くのか?
も大変興味がある。

「落選元号」へのフィルタ&評価結果

もちろん、ただのフィルタでは、
AIが出した感が薄れてしまうため、
過去の落選元号についても、
AIにとって「良い意味」「バランス」であるか
評価を実施し、その最高得点をマークしたものだけ、
最終候補として採用することにする。

落選元号を相手にしたとしても、
あくまで基準は、AIから見た得点であり、
元のやり方で出した候補よりも、より高いものだけを選定し、
落選候補だからといって他候補より優遇はしない。

結果は、大変興味深いものになった。

「落選元号」532個のうち、
元々のフィルター条件だけで、
なんと32個まで候補が減ってしまった。

「常用」「画数」「MTSH」「WIKI」が
それぞれ入り混じっており、
どれかの条件だけで多数落とされているわけではなかった。

さらに、今回実装したGoogle俗用フィルタによって
(俗用度の点数が一定値以上は除外するという考え方で)
17個まで候補が絞られた

つまり、それっぽい候補が出現したとしても、
今回設定した各種条件だけで、
523個 ⇒ 17個 の割合でフィルタされるということ
かなりの厳選率である。

この17個のうちで、AIの評価値が高い、
TOP4つが、新たな候補として加わることになった

この4つの結果は当日の発表を見てのお楽しみ。
(ガチ当てなので、最終候補は公開できない)

もちろん、元々古典からの出典としていた候補についても、
同様のフィルタはかかっており、
こちらもより厳選された候補になっている。

ピッタリ当てるのは難易度が高すぎる上に、
放送時間的に見ると、予想発表後すぐに、
「残念違いました~」と分かる極悪な仕組みなので、
過度な期待はしないようにお願いします。

おまけ、こぼれ話

「最終候補」の17個に残りつつ
AIから見た評価が最も低かった「落選元号」は、以下である。

「応宝」
「用保」
「文功」
「文始」
「協中」

これらは、「フィルタ」には引っ掛からず、
「俗用」されていないと判断されたものの、
AIから見たら、ちょっと他より元号っぽくないぞ、
とみなされた、ということ。

他に、AIにとっての「距離感」が合わな過ぎ、として
523個中3つだけ先に除外されていたものもあった。
(基本はマイナス点だが、ズレが大きすぎる場合一発除外)

「和平」
「永貞」
「能成」

こんなものが以前元号の候補に入っていたんだー、という話と、
これらに対し(他に比べて)低評価を下したAIの判断は、
みなさんにとっていかがであろうか?

あとがき(いよいよ結果を待つだけ)

実際に元号が発表されれば、
(そして外す可能性の方が高いのだから)
この物語はとんだ笑い話だ。
発表される前であっても、最初から笑い話だ

だいたい、書いた人の他の投稿を見ても、マトモな投稿が無い。

しかし、「元号」にAIとプログラムで挑んだ「遊び」は、
一つの挑戦の物語として、
誰かの心に残り続けてくれるかもしれない。

人類の進化は「遊び」からはじまる。
こんな「遊び」が出来るならば、というアイデアに触発される人がでて、
生活にも役に立つような「発明」が生まれるのだ。
          ~  Char Fuitter (1847~1912 オランダ) ~

改元対応で頭を悩ませているエンジニアは多数いらっしゃるだろう。
私も、改元について最も想いを巡らせている人のひとりだと思う。
たぶん、そのベクトルはだいぶ違う方向に向いている気がする。

次回(があれば)、
「新元号」は、今回のAIの評価でどれくらいの評価値なのか?
Google俗用フィルタの評価値は?
などを確認しながら、予想と現実との乖離を確認したい。
もし、それが記録として残り、未来の次回の改元時に、
決める側の方に本プログラムの思想が採用されたら大変面白い。


この物語はフィクションです。
登場する人物・団体・名称等は架空であり、
実在のものとは関係ありません。
Char Fuitter (チャー・フイター)は架空の人物です。
※途中で良いことを言っていても騙されないように。
※Google俗用フィルタは、「正解」発表後は、
 「正解」が元はいくつだったのか、確認出来ないかも。

【結果】平成の次の元号を、AIだけで決めさせる物語(@完結編)

前書き

2019年4月1日11:30、新元号が発表された。
新元号は「令和」とのこと。

その直前、フジテレビの特番にて、
本記事で作成したAIが選んだ元号が公開された。

さて、結果は・・・。

シリーズ記事一覧と、今までのサマリ

まだシリーズ一連の記事をご覧になっていない方は、
先に以下の記事を読んでいただいてから、
本記事をご覧いただくことを推奨します。

シリーズ記事一覧

初回の記事⇒
平成の次の元号を、AIだけで決めさせる物語

2回目の記事⇒
【続】平成の次の元号を、AIだけで決めさせる物語(@テレビ取材)

3回目の記事⇒
【続々】平成の次の元号を、AIだけで決めさせる物語(@直前スペシャル)

今までの内容サマリ

AI(機械学習モデル)で元号予想したよ。
「いい意味の漢字」を機械に理解させるためにChar2Vecを使ったよ。
Qiitaで公開したら、テレビ取材を受けることになったよ。
「出典」も含めてガチで当てがんばる、発表直前に放送するよ。
「俗用されていないこと」もGoogleやWikipediaなどで実装したよ。
VTRに入らない深い内容はQiitaで公開するよ。

予想方法の概要

  • 漢字の意味上の距離をベクトル化する機械学習モデルを作成(Char2Vec)
  • 過去元号の文字と「近い」文字(=いい意味)を候補漢字として設定
  • 日本古典/中国漢籍/落選元号から「出典」と言えそうな場所を全文検索
  • MTSH、画数、Wikipediaの俗用有無、常用/教育漢字など多数のフィルタ
  • 2文字のバランス=距離感や、漢字のいい意味度などから評価値を算出
  • Google検索の結果によって、俗用度合も再フィルタし、最終結果を出す
  • 【制約】数値設定/プログラム以外の、作者のキライな字を除く、ような操作は禁止

本投稿の内容

  • ①テレビをご覧になっていない方へ、結果を再度ご紹介

    • AIはどのような条件(出典)を与えられ、何を選んでいたのか?
    • それぞれの評価値と選定理由の詳細(テレビ放送分の詳細版)
  • ②「令和」を本作のAIで評価させると、どんな評価になるのか?

    • 「令」「和」の文字ごとの評価
    • 「令和」の全体としての評価値(2文字の「距離感」を含めた総評)
    • 「俗用度」についての考察
  • ③予想と現実の比較考察

    • 「漢字」をプログラムで扱う人には参考になるかも?

AIの予想した結果(テレビ放送の詳細版)

一位の発表と、テレビでの反応

本プログラムが出した結論は、
孝永

詩経の「子不匱,錫爾類」が出典である。

「孝 子(こうし) 匱(つき) 不(ず) 永(なが)く 爾(なんじ)に 類(るい)を 錫(たま)う」
→「こうしつきず ながくなんじにるいをたまう」とよむ。

その意味は、
「世継ぎが尽きることがなく 末永くあなたに一族の繁栄をもたらす」

要するに、
子孫繁栄という縁起のよい内容。
良い場所が出典になるかは少し運ゲーであったが、
偶然にもかなり良い出典になったと思う。

テレビ放送中では、以下の4予想が公開され、
視聴者投票が実施された。

  • 「宇拓」(by 金田一先生)
  • 「高光」(by 土屋先生)
  • 「寬安」(by フジテレビネット取材部)
  • 「孝永」(by AI)

視聴者投票の結果は、AIの圧勝
一位:「孝永」=72%
二位:「寬安」=11%
三位:「高光」= 9%
四位:「宇拓」= 8%

「AIにしては保守的だな」
「(視聴者のみんなは)今のまま(保守的な元号で)でいい、ということ」
と、金田一先生よりコメントをいただいていた。

保守的、との指摘はなかなか正しく、
過去のすべての元号を「平等に」扱って予想したため、
過去の元号に近しいものを出力した = とても(古い)元号っぽい、ため。
にしても、視聴者の方からここまで賛同をいただいたことは嬉しい限り。
後で考察を追記するが、実は今回完全に的中出来なかった要因として、
この「平等」であったこと、は大きな比重を占める。
一方で、そのためにいかにも「っぽい」ものになって、
視聴者からの納得感が高かった、という傾向はあると思う。

2位以下含めた候補の発表

テレビ上でも一瞬表示されていた、
AIの出した孝永以外の候補について、
評価値や出典を含めて掲載する。

元号候補 総合評価値 AI評価値 俗用度 出典 備考
安天 38.96 4.81 9.1 日中
郎久 36.65 4.36 7.0 常用
吉天 35.99 4.73 11.3 日中 常用
文竹 33.08 4.07 7.6 日中
幸皇 32.87 4.09 8.0
孝永 41.86 5.29 11.0
康享 40.45 4.74 7.0 常用
仁長 39.71 4.91 9.4 中落
仁元 38.31 5.06 12.3
喜氏 37.40 4.40 6.6
兼天 35.06 4.21 7.0 日中 常用
恭天 34.41 4.25 8.1 日中 常用
喜永 33.27 5.06 17.3
康天 31.30 4.79 16.6
功永 30.50 4.15 11.0
康承 29.38 4.22 12.8
文承 28.99 3.81 9.1
応久 23.38 3.56 8.4 補欠

表の見方

  • 総合評価値 = AI評価値×10 - 俗用度
  • AI評価値 = 文字の良さ+距離感の良さ、を基準に複雑な計算
  • 俗用度 = Google検索結果より求めた俗用度
  • 出典
    • 日:日本書紀、古事記、万葉集
    • 中:四書五経(大学,中庸,論語,孟子,易経,書経,詩経,礼記,春秋)、史記
    • 落:過去の落選元号

※詳細は過去記事をご参照

裏話として、個人的には「常用」は最初から除外しようと思っていたが、
「常用」を入れないと同じような文字の組み合わせが増えてしまい、
テレビ的に「見栄え」が悪いので、
「常用」については個数を限定して、
上位のものだけ最終候補として入れることになった。
「常用」が無いとも言い切れなかったため。

出典としてどの文章を使うか?
常用を入れる入れない、画数はいくつ以下?などの
「設定値」はどうしても決めざるを得ない。
ただ、その状態においては、どの漢字/候補も平等に扱っている。

個人的には、孝永をはじめとして、
吉天、文竹、喜永、仁元、文承、恭天、
など、どれもそんなに悪くは無い気がしている。
(どれも古い表現という印象はある)

なお、AIによる他候補に対する評点としては、
「宇拓」はAI評価値が0.66(※孝永5.29)
「高光」は俗用度が高すぎ(54)&Wikipediaに存在、でNG
というところであった。

令和を本作のAIで評価

さて、いよいよ本題の、令和に対する評価値は?

令和 : WIKIタイトル後チェックNG

がーん。

憲法学者の「川岸令和」さん
がWikipediaに乗っており、
高名な方で、出版もされている。
また、他にも一般人で「令和」さんは複数いらっしゃる。

そのために「WIKIタイトル後チェック」
=WIKIに乗っているレベルの人名はNG、
のチェックではじかれてしまっていた。

では該当のチェックを外して評価を行うと、

令和:AI評価値 = 2.59 (※実質2.99@後述)

今回候補に挙げた最低値が3.56のため、
少し評価値としては高くはない。
(宇拓が0.66であった通り、低い値ではない)

俗用度についての考察

発表後の今となってはGoogle俗用度関数は動作出来ない。
しかし、直後にGoogleを見た時の印象からすると、
俗用度としても(人名は、地名などに比べればかなり加減はしていたが)
少し高め=俗用されている、と判断されそうな状況であった。

ただの人名であれば「令和の画像検索結果」はそこそこ上位に入る。
しかし、川岸令和さんの著作/出版物が先にHITしていたため、
「令和の画像検索結果」は低順位となる。
元号候補が新単語で俗用されていない ⇒ 画像検索が上位になる傾向、
という値を使っていたために、新単語とみなされなかったということ。

これは「有名人レベルの人名は除外したい」という意図で
誤っていたわけではなく、正しく評価していたと言える。

個人名の一致レベルでは多少しょうがないが、
著作があるレベルの有名人と重複するのは
避けなくてよかったのだろうか?
当然、普通の個人との一致も何件も報告されている。
(負け惜しみ)

反省点①

評価値が低めであった原因の一つとして、
まず私の読み間違え的な設定ミスがあった。
「明治大正昭和平成」の文字が入っている場合、
ペナルティ(今回は0.4)を設定していたため、
その設定が無い場合は2.99、約「」であり、
これは候補には至らないもののそこそこ高い値だ。
直近の元号と同じ漢字は避ける設定は余計だった。

今回のAIによる元号的中はできず、
正解の元号に対する評価も、
決して高くはなかったため、完敗である。が・・・。

もう少し詳細に確認すると、大変興味深いことが分かった。

単一の漢字ごとの評価値

本作のAIでは漢字1文字ごとが「良い漢字かどうか」の評価値が
最終評価に最も大きく影響する仕組みであり、
各文字ごとにどうったのか、考察をしてみる。

」は意外な文字であったとの評判だ。
AIによる文字への評価値は0.31
これは、教育漢字1006字の中で、314位だ。
平均値よりは上であるものの「ふさわしい」レベルにはない。

」は、0.49 で71位。
自身が「お手本」でもあり、
お手本の中では低いもののまあ「ふさわしい」レベル。

ご参考までに、他の漢字いくつかの評価結果と、
教育漢字全てのランキングを掲載する。

適当な一文字ごとの評価結果
永: 0.885977994167
天: 0.737857870747
仁: 0.839549368644
桜: 0.353807618816
善: 0.41546696606
悪: 0.111052437414
安: 0.692865630207
危: 0.310356468143
貧: 0.2459302001
富: 0.331105676499
勝: 0.188771693601
負: 0.113797467234
鼠: 0.0771632984204
猫: 0.148012992302
衣: 0.236350125043
食: 0.258570181343
住: 0.247701718093
令: 0.3063162064
和: 0.490555337934
宇: 0.290160216298
拓: 0.156864300019
平: 0.528437411631
成: 0.31741135288
昭: 0.384988257387
大: 0.537594822018
正: 0.701011684154
明: 0.636209015976
治: 0.547203264259
上位順に、教育漢字全てを並べた結果
永徳仁忠久孝清元天康正安坂喜松田幸竹明宝養延氏長三皇五文保宮豊政城晴臣八守老蔵倉孫治宗六郷誠承光大里后志雲賀戸納平太家夫敬飯尊恩矢寺七父弟谷和池村武銭中化陽貴健丸応余浅四木観君将梅建羽漢府帳野肥幕朝福祖春山書盛憲満典兄策統良副泉朗官願閣石任善早省万厚王堂民百妻直栄財湯門則倍義男不円刀年乱景川秋九若子源測昭鏡億末至改灯師翌領親根革館職興母十議礼衛反原博屋死立司対祭玉院居晩実神図林派就風古乳説従条旧河拝宣討役遠近医千枝辞後権墓桜法縮資構兆象友豆党森護功蚕代橋温税酒創論筆句均務総拡裁美糖増熟冊約看積児史仏定土支科質国富台弁備広高減病費幼育町波室勤酸字信畑焼額著的認借仲諸理完績内絵成左磁牛京印益北首歴期済生預推危地塩陛庁量示律令会庭街委同防客柱聖金絹去西命青岩菜工舎静伝都園東座計毛祝張助花案農経員真刻小二適修申度結算婦宿学茶口教純樹島講宇許世章往遊授芸草胃制圧断展暖南右訳確害解給率路整寄談折周前横星造管性臨勢肺旗謝争尺物息貯形銅市刷遺鳥公独頂自腸詩値努術批熱庫紀模効課牧置簡燃県層営雪料責径産上有足幹宅勇相季順無俵亡弓仮炭垂厳洗校極族在沿補貨鉱処次昔利針者冷食道潔黄笛等寸起部際新程数芽想染並主短訪軍穀誤細強郡時運貸下湖共混然築辺住寒童通氷妹巻証間禁貧評羊賛再重演士基姉火我素州見買閉節復知力関検身綿糸進宙像事察好低非銀帰消夏粉吸腹精鳴所今衣飼導体月協裏由拾価卵薬用区弱変達水調束点単罪働駅労容記箱休来習退荷臓白問障雨望局社商歌暑写面覧衆深名続毒賃班判業警才席球予読米一残紙因災干刊卒感除返片夢慣冬集具悲列密域少系全洋準劇式本赤飲麦浴多票動編開版馬失留割第停能落植指線製耕株終昨両兵俳験鉄異角漁急階映求愛女人誕複転紅油港引側視暗位速招電待方陸激帯外皿個署標布序航件皮答現険服己頭提笑穴楽届略半犬心盟状常限材例唱念降意岸規射逆緑要秘選号報気聞液織受旅識奮覚暮散担勝快初肉挙雑賞英易採器板夕眼格鋼交苦日泣包血海筋決店注固黒否題品語可告秒行船得損毎郵希丁潮活配言査昼券移灰顔照流勉故仕輪当敗便止各境夜手難徒忘連誌葉類脳械比供殺特設週分私訓段先貿参存背機午入姿過曜情接欠疑未砂表収考縦打取発出飛述蒸似使候虫困追声貝付窓棒差技録専競巣囲底作投央級団詞揮回欲破切軽試耳加空迷界救型群犯骨痛種画始持枚絶他曲脈奏態魚汽思向場泳以別色練果優暴呼合音捨属探輸乗戦負究悪歩机歯胸様最放目味傷必札隊操登話装鼻組係着敵走舌売車何研番送

興味深い結果:「令和」は「平成」と全く同じ傾向

平: 0.528437411631
成: 0.31741135288
二文字の距離=0.0353095354674

令: 0.3063162064
和: 0.490555337934
二文字の距離=0.102744130448

孝: 0.80602597523
永: 0.885977994167
二文字の距離=0.689883694307

「平成」は「成」の字が元号としては新字であった。
「令和」も「令」の字が元号としては新字。
「孝永」も「孝」の字が元号としては新字。

二文字の距離、は、前の記事で「平均0.3」が過去元号の平均値で、
それに近い方がより良い、としていた値だ。

「令和」の結果は、
新字の評価値も、旧字の評価値も、二文字の距離も、
全てが平成に酷似している。
この結果には大変驚いた。
二文字の距離感も、全体平均 = 0.3 ± 標準偏差 の範囲を満たし、
直近15個ほどの平均値=0.15にもかなり近い。
作成したAI(機械学習済みのchar2vecモデル)のモノサシは良かったのに、
私がそれをうまく設定して使うことができなかったということか。

「孝永」は、AIが見つけた新字。といいながら
「孝」はかなり過去元号のお手本に近いものとして
算出されており、AI評価値が高い。
また、二文字の距離も(全体的に過去元号は近似度が高めな値であり)
0.68というのは、かなり似ていて近い文字であることを示している。

今回の算出として、単純にどちらの文字ともに、
「過去元号のお手本に近いほど良い」という評価をしたため、
「孝永」が出てきており、
そのために視聴者からは「元号っぽい」と思われたと考えられる。
一方で「平成」の傾向を考えるとするならば、
一文字は高くて良いがもう一文字(新字相当)は、
0.3程度の普通よりちょっと上のゾーン(1000字中300位ほど)あたりから選ぶ、
というようなことをすれば、的中出来たのかもしれない。

つまり「いい意味度」をはかることはある程度成功したが、
元号を求める際には、やみくもに「いい意味」になるように組むのではなく、
「いい意味の旧字」と「意外な新字だけどそこまで悪い意味ではないやつ」を
組み合わせないといけなかった、のかもしれない。

古いものを学習させて、それを正としたAIだったため、
最近の元号の傾向=元号として新しい印象の文字を使う、
に全く追随できていなかったし、
「大化」と「平成」を同列に扱っていた点が敗因と言えそうだ。
(とはいえ、どういうロジックで「最近の傾向」を
 見ればよいのかは物凄く難しい。)

改めて、モデルの評価値、予想と現実の比較考察

改めて、上記のモデルの評価結果を眺めると、
超上位の漢字=「永徳仁忠久孝清元天康正安坂喜松田幸竹明宝養延」などは
なるほど、と思うピックアップになっているし、
似た属性での比較(安vs危、善vs悪、勝vs負、貧vs富)は
正しい結果、つまり良い意味の方が高い傾向になっている。

一方、「危」は0.31で、「成」や「令」と同レベル。
他にも、イマイチな文字が数字上高くなったりはしている。
これは「安」が高評価であることに引きずられ、
その対義語であるところの「危」も
高い評価値になっているためと思われる。

Word2VecやChar2Vecを扱う上で注意するべきポイントは、
「対義」というのは、実は文字のベクトル上では、
真逆ではなくむしろ「近い」ことを意味するという点だ。
真逆=完全に無関係に見えるもの。「安」vs「鼠」など。

対義語のベクトル空間上での扱いの考察は、以前下記に記載した。

よって、
「お手本の過去元号に近い」=「良い意味」まではOKとして、
超TOPは抽出できているとしても、
少し順位が下がると雑音的な要素が増えてくるという事態が生じている。
(上位の「安」に引っ張られて「危」がそれなりの値になったように)

この点を解決しないと、
「300位あたりの文字から拾えば良かった」と言っても
結果論でそう見えるだけで、実際の300位近辺は
「危」なども含まれていて、あまり良い結果が期待できない。

Qiitaでは、
1年後の自分が見て嬉しい記事を書く
が良記事の一つのベストプラクティスであるため、
25年後の再挑戦時の自分が見て嬉しいように、解決案を記載する

解決案

漢字のクラスタリングによって、
元号に使われるクラスタと、使われないクラスタを事前に振り分けておき、
各使われるクラスタ内での上位を拾う、という案だ。

例えばChar2Vecの結果をクラスタリングすると、
「東西南北」などが同じクラスタに入ったりすることは確認済み。
こうした似た漢字をグルーピングするのがクラスタリング。

今回の結果では、上位100位ほどに、以下が出現している。
城、倉、家、寺、など
孫、夫、父、弟、など

これらは、恐らく場所クラスタ、家系クラスタ、などに属するはず。
場所や家系は相対的に良い意味なのだろうが、
本来は元号としてはイマイチだろう。

クラスタ内に、過去元号で採用された文字がなければ
「使われないクラスタ」として、これらの文字を除外できる。
そうすれば1006字から真に使われる可能性のある文字に絞れる。

「安危」は同じクラスタに入ってしまうとして、
そのクラスタ内での上位の文字だけを拾えば、
「危」のように、良い方の意味に引っ張られたものも削除できる。
仮に、動物クラスタは、鶴、亀、竜などでOKだとしても、
鼠、馬、犬、などは下位で除外する、というような話。

イメージとしては、1次元で数値で比較してしまうと、
危 > 勝、になってしまうのだが、
以下のようにグルーピングして、各グループの上側だけ取るということ。
「安 > > 危(0.31)」>「勝(0.19) > > 負」

この考え方であれば、雑音的な文字は減らせ、
かつ「危」などのグループ内でザコい文字、も消去できそうだ。

実際にやる際には、クラスタリングの数や各種分類の設定など
かなりのチューニングを要するだろう。

もっとそもそもの問題点として、
折角char2vecのモデルを「60次元」で作っていたのに、
過去元号に似ているかどうかの値として、
良い意味or悪い意味、の「1次元」に射影して扱っていた点もある。

クラスタリングの話は、複数次元で見る必要があったの意味だ。
今回は、良いor悪い(元号に近いor近くない)の
1次元に全てを射影して評価していた。

本来は、各次元(属性)ごとに考慮して、
そもそも元号に使われないような「魚鮪鰯」などの魚属性は除外、とか、
各次元(属性)ごとの良悪の評価を行うとか、
そういったより複雑な評価が必要であったのだろう。

「令」(や「成」)は単純に「良い意味」を追求して出る文字ではないため、
いまどきの元号を求めるには、こういった複雑な工程が必要になる。
200年前の元号制定であれば、このAIがあれば
かなりいい線いっていたのかもしれない。

令和天皇(皇太子さま)が平成天皇(今上天皇)の
現在の年齢になるのは25年後、
25年後にまた挑戦してみる材料が出来た。

反省点②

では、「令」の字は今までの方法では全く見込みが無かったのか?
いや、落選候補元号に「令徳」があり、一度だけ出現する。

令徳 : 2文字目13画以上NG

がーん。

「徳」の字の画数で引っ掛かり落ちていた。
過去の平均値等を元に12画を上限として設定していたのだ。

画数のペナルティを外すと・・・
令徳:AI評価値 = 4.15

これは余裕で最終候補に残る高評価だ。
もしかしたら、13画まで合格ラインに設定していれば、
最終候補字の中には「令」もいたぜ!
っていうくらいのことは出来たようだ。

ご参考:他の元号候補に対する検証

本来は公開されないはずの、
令和以外の候補について
「万保」「万和」「広至」「久化」「英弘」
とネット上ではウワサされている。

これらについてはどれくらいの評価値だろうか?
あまり深くは述べないが軽く記載しておく。
(一部読み方などで万=マン=Mと誤読されていた、
 などのチェックは外して評価。1000字程度であれば、
 冒頭の読み方は、手動で設定しておくのもアリだった?)

万保:AI評価値 = 3.59237004099
万: 0.412605058736
保: 0.583840449043

万和 : WIKIタイトル前チェックNG
(右翼の会社名?がwikiで出てくるのでありえない。捨て案?)

広至:AI評価値 = 2.79708493969
広: 0.326555400899
至: 0.38284076335

久化:AI評価値 = 4.16381808331
久: 0.807551234777
化: 0.478564698777

英弘 : WIKIタイトル前チェックNG
(会社名や、有名人名などでNG、ありえない。捨て案?)
 なお、「弘」は常用漢字だが、
 お手本漢字であり、AI評価値は驚愕の0.84

どれも、評価はかなり高めであった。
「古い元号っぽさ」を数値化するという意味では
AI評価値はそこそこ信頼出来る気がしてしまう。
古さの順では、個人的には以下の感覚。
孝永 > 久化 > 万保 > 広至 ≒ 令和 > 宇拓

反省点③

当初のプログラムの進め方上、
最初に「使う対象の漢字」を絞って、
さらに各候補にフィルタを連続でかけていき、対象を絞り込む、
という方法を採用していた。
この方法では最終結果も少なくなりすぎたし、
実は良かったという候補を
敗者復活的に獲得することが難しくなってしまっていた。
フィルタ>フィルタで絞っていくよりも、
マシンリソースを気にせずに全探索系して、
各候補に「落選理由」を付与するような進め方にして、
1理由で落選しても他で良いものはOKとする、
ようなやり方も良かった気がする。
(「令徳」が画数だけで落ちていたり、「広至」も、
 0.3程度の字から出ている。など。)
 

あとがき

もしかしたら・・・
という気持ちもありながら、的中はやはり難しかった。
候補漢字50文字を当てたとしても約2450分の1。
外すこと自体は仕方がなかったかもしれない。

応援していただいたみなさま、投票していただいたみなさま、
フジテレビのみなさまへ改めて感謝。

万葉集までは入手済みであったため、
平成と完全に一致した傾向ということを踏まえると、
過去元号に近いように当てにいくのではなく、
平成にひたすら近いもの、という手法でも
結果を出力していたら?
未来に何か影響を与える可能性はあった。

なお、次回からは新番組、
令和の次の元号を、AIだけで決めさせる物語
が始まる予定。

次回は恐らく国書(=古事記or日本書記or万葉集)になり、
かつ、平成、令和、の流れを踏襲すると考えると、
かなり犯人像は絞られている。
平成の名探偵は無事に令和の名探偵になれるのだろうか?
こうご期待である。

もしかしたら、次回は
選定者側にも当然AIが加わっている
という良きライバル登場、探偵が犯人だった?
みたいな時代になるのかもしれない。
今回の件で「AIも元号選定に加われることを世間に示せた」
ということがその理由の一つになっていれば、
この遊びプロジェクトの意義も大きいことになる。

超超長い戦いにお付き合いいただいたみなさまへ、
改めて感謝の意を示し、
平成最後の漢字分散表現との戦いに幕を下ろすことにする。
ご愛読ありがとうございました!!

平成31年4月7日(初版投稿)

パワポエンジニアの憂鬱を軽減する誤字/表記揺れ検出ツールを作った物語

前書き

Qiita読者の多数を占めるイケてるエンジニア諸氏には無縁の話ではあるが、
世の中には「パワポエンジニア」という職種がある。
綺麗なパワポを作ることが彼/彼女らの最大の価値であり、
コードは書かないが、時に「エクセル方眼紙」を駆使して、
"上流工程"を行う場合もあると聞く。

そんな彼/彼女らの"品質意識"は高く、誤字脱字はもちろんのこと、
「ユーザ」「ユーザー」「サーバ」「サーバー」などの表記の揺れや、
「PowerPoint」「powerpoint」「Database」「database」などの
大文字小文字の揺れも許されないようだ。

この記事はそんな彼/彼女らの悩みを
Pythonで解消する風景を描いた物語である。
もちろんフィクションであり、
この令和になろうという時代に
そんなエンジニアは存在しないに決まっている

唯一実在する点は、本物語で開発されたツールであり、
以下から無料でダウンロード可能だ。(4/17(水)公開されました)

パワポエクセル一発リント君」(Windows向け)
https://www.vector.co.jp/soft/winnt/business/se519740.html

  • PPTXやXLSXからテキストデータを抽出
  • 完全オフライン & 前提ツール不要で動作
  • 誤字/表記揺れを、複数ファイル横断で検知

もし伝説の「パワポエンジニア」や「エクセル方眼紙使い」が
実在するとしたら泣いて喜ぶツールだと思われる。

それでは早速、このツールが開発されるにいたった風景を見てみよう。

パワポエンジニアのお仕事風景より(Before)

ワイ「あー、今日もパワポ、明日もパワポや」
ワイ「最もよく使うツールはMS PowerPointやで」
ワイ「開発したこと無いけど技術に"詳しい感じ"に見せるのが腕の見せ所や」
ワイ「Qiitaのイケてるエンジニアのみんなが羨ましいで~」

上司「お客様にご説明する資料できたか?」
ワイ「はい、できました!」
上司「なんやこれは、誤字だらけやないかい!」

上司「【メーセージ】ってなんや、誤字だろ!」
上司「【アーキテクチャ】【アーキテクチャー】どちらもあるし」
上司「【Kubernetes】【kubernetes】と大文字小文字混ざっとる」
上司「【Kubenetes】って1文字欠けとるのもおるで!」
ワイ「(k8sについて分かりやすい解説を読んでも
   まだ間違ってしまうで。スペルが難しいんや!)」

上司「誤字、脱字、表記の揺れ、があると、
   中身もその程度の品質だって思われるぞ!」
上司「納品時に厳しくチェックされるし注意せなあかん!」

ワイ「マコト モウシワケ ゴザイマセン」
上司「ゲンシジンになっとるで!」

ワイ「・・・。」
ワイ「・・・・・・。」
ワイ「zzzz」
ワイ「はっ!おふとんがなくてもスヤァしそうになるで」
ワイ「誤字/表記揺れチェックはつらみが深いで」
ワイ「でもツマラン誤字脱字で怒られるのもコリゴリや」

3歳娘「パパ、誤字/表記揺れチェックを自動化して?
3歳娘「みんな持ってるの」
3歳娘「私だけ持ってないの」
ワイ「(いや、そのセリフはおねだりの常套句やけど)」
ワイ「(いきなり3歳娘登場は読者がついていけんやろ・・・)」
ワイ「(アンパンパーソンあたりから丁寧に導入しないと)」
ワイ「(話の導入部書くのが面倒になったからといっても)」
ワイ「(やめ太郎さん知ってる人でも驚くほど無理やりやで)」
ワイ「(会話でまとめるの大変であきらめたってバレるで。やめ太郎さんスゴすぎや!)」

ワイ「さあ、誤字/表記揺れチェックを自動化していくで!

◆重要注意事項:
 この物語はフィクションです。
 登場するワイは架空のワイであり、
 実在の誰かや組織とは全く関係ありません。

パワポとエクセルの誤字/表記揺れを自動チェックするツールが完成!

  • 複数のパワポやエクセルから一括でテキストデータを抽出!
  • 誤字や表記の揺れを自動機械チェック
  • 完全オフライン動作チェック規則の作成不要

以下のページから、無料でダウンロード出来ます。

パワポエクセル一発リント君」(Windows向け)
https://www.vector.co.jp/soft/winnt/business/se519740.html

※複数のパワポから一括でテキストを抜き出すツールとしても使えます。
※パワポ以外でも、長文テキストに対する表記揺れチェックとしても使えます。
※もちろん、Qiita投稿時の事前チェックとしてご利用いただくことも可能。
※名前がダサいのは敢えてです。UIも無いような古いツールだし。

本投稿の内容

以下ツール開発の流れに沿って
ポイントとなるコード/ノウハウを記載します。

■本ツールの開発の流れ
パワポやエクセルからテキストを抽出

形態素解析によって名詞を対象とする

「編集距離」が近い = 似た語句は、誤字/表記揺れの可能性

Pythonのファイルを「EXE化」して配布可能にする

■主なノウハウ

  • Pythonでどうやってパワポやエクセルを扱うか?
  • 「表記揺れ」をどうやって検知するのか?
  • Pythonを「EXE化」するツールとは?
  • 形態素解析を「EXE化」に組み込むのは大変だったこと
  • 環境は、Windows/Python3.6前提

PPTXからテキストを抽出する方法

これを使う: python-pptx
https://python-pptx.readthedocs.io/en/latest/

以下のコマンドでインストールできる。

pip install python-pptx

以下のようにして、テキストを抽出できる。
実行すると、PPTXの中身がダァーっと出てくる。

python-pptxの基本的な使い方
from pptx import Presentation
import sys

try:
    prs = Presentation("input_file_name.pptx")
except:
    ## パスワード付きなどで開けないとエラーになる。
    ##pptx.exc.PackageNotFoundError: Package not found at  ファイル名
    sys.exit()

#中に入っているslidesの数分の繰り返し
for islide in range(0, len(prs.slides)):
    #スライド名出力 (1階層目)
    print("slide_name=",str(islide))
    #print ("\tfor slide => " + str(islide))
    #python-pptxとしてのスライドを取得
    slide = prs.slides[islide]
    #スライドのshapesの分だけ繰り返す。
    for shape in slide.shapes:
        #テーブル構造を持っている場合は、
        #if shape.has_table:などで分岐して別処理が必要。
        #長くなるのでここでは省略。
        shapeText=""
        #テキスト構造を持たないshapesは無視して次に進む。
        if not shape.has_text_frame:
            continue
        for paragraph in shape.text_frame.paragraphs:
            #.strip()は、改行コードやタブ情報などを削除する。
            shapeText += paragraph.text.strip().replace('\n','').replace('\r','')
        #もし、そのshape内にテキストデータが入っている場合は出力
        if( len(shapeText) >0 ):
            print(shapeText)

これで、任意のPowerPointファイル(PPTX)から
テキストデータを抽出できるようになった!

XLSXからテキストを抽出する方法

これを使う: xlrd
https://github.com/python-excel/xlrd
(xlrdは読み込み用で、書き込みたければxlwt)

以下のコマンドでインストールできる。

pip install xlrd

以下のようにして、テキストを抽出できる。
実行すると、XLSXの中身がダァーっと出てくる。
(ただしセルに入っている値のみで、オブジェクトは不可。
 どなたか良い方法をご存知であれば教えてください)

xlrdの基本的な使い方
import xlrd

book = xlrd.open_workbook("sample.xlsx")

# ブック内のシート数を取得
num_of_worksheets = book.nsheets
print("シート数",num_of_worksheets)

# 全シートの名前を取得
sheet_names = book.sheet_names()
print(sheet_names)

#ブック内のシート数分繰り返し
for iSheet in range(book.nsheets):
    sheet = book.sheet_by_index(iSheet)
    #行の数だけ繰り返し
    for row_index in range(sheet.nrows):
        #列の数だけ繰り返し
        for col_index in range(sheet.ncols):
            val = sheet.cell_value(rowx=row_index, colx=col_index)
            #emptyとか入る場合もあるので、まずSTRに変換。
            str_val = str(val)
            if len(str_val) >0:
                #print(str_val)
                print(str_val)

これで、任意のExcelファイル(XLSX)から
テキストデータを抽出できるようになった!

DOCXからテキストを抽出する方法(オマケ追加)

※初期バージョンでは未実装機能:
 コメントにて要望があったため追加。
 2019/5月上旬までに公開予定。

これを使う: python-docx
https://pypi.org/project/python-docx/

以下のコマンドでインストールできる。

pip install python-docx

以下のようにして、テキストを抽出できる。
実行すると、DOCXの中身がダァーっと出てくる。
(通常の文章+表の中の文章が対象のコード。
 オブジェクトの文章の取得方法は不明。)

python-docxの基本的な使い方
import docx

doc= docx.Document('sample.docx')

#全文章を取得
for par in doc.paragraphs:
    print(par.text)

print("---------------------------")

#全テーブルに対して処理を行う。
for table in doc.tables:
    for row in table.rows:
        #print(row.cells[0].text)
        for cell in row.cells:
            print(cell.text)

これで、任意のWordファイル(DOCX)から
テキストデータを抽出できるようになった!

ツールの名前は直さなくてもいいかなw

「表記揺れ」を検知する方法

レーベンシュタイン距離(Levenshtein distance)の応用版を使う。

レーベンシュタイン距離とは、
2つの語の間の編集距離のことで、
一方の語句から他方を得るのに必要な「修正作業」の回数のこと。

「修正作業」とは、以下の3つのこと。

  • 挿入(insertion) apple⇒applepen なら3回
  • 削除(deletion) 非日常的⇒日常 なら2回
  • 置換(alteration) カンジ⇒カンマ なら1回

これを基本として、誤字チェックのためには、
隣接文字の入れ替えも距離1と考えた方が良い。

  • 転置(transposition) カンジ⇒カジン なら1回

転置も認めたものは、
Damerau–Levenshtein距離と呼ばれているそうだ。

さらに、語句の長さも考慮しよう。

PenPinappleApplePen、と
PenPinappleApple、は編集距離=3になる。
このルールだとPenとbagも編集距離=3だ。
どう見ても、PenPinappleApplePenの方が似ている。
そこで、
「編集距離」÷「長い方の文字列の長さ」を
標準化されたDamerau–Levenshtein距離
とする。必ず0~1の間に入る値になる。

  • PenPinappleApplePen vs PenPinappleApple ⇒ 0.158
  • Pen vs bag ⇒ 1.0

2つの異なる語句に対してこの値が一定値以下であれば、
誤字/表記揺れの可能性が高いということ。

原理の説明が長くなった。
実装としてはこれを使う: pylev3
https://pypi.org/project/pylev3/

以下のコマンドでインストールできる。

pip install pylev3

「標準化」を加えて、以下のように使おう。

標準化されたDamerau–Levenshtein距離
from pylev3 import Levenshtein
def getNormalizedDamerauLevenshteinDistance(str1,str2):
    maxlength = max([len(str1),len(str2)])
    distance = Levenshtein.damerau(str1, str2)
    return distance/maxlength

print(getNormalizedDamerauLevenshteinDistance('cat', 'ctaa'))
#0.5
print(getNormalizedDamerauLevenshteinDistance('cat', 'cta'))
#0.33333

これで、語句同士の編集距離を求められるようになり、
誤字/表記揺れを抽出できるようになった!

#決して、ダジャレ自動生成のために調べていたことが
 シゴトに役立ちそうと思ったわけじゃないんやで!

PythonをEXE化する方法

ツールが出来たあとで「配布」するためには、
pyinstallerを使って「EXE化」したい。

pyinstaller
https://www.pyinstaller.org/

以下のコマンドでインストールできる。

pip install pyinstaller

Pythonのファイルを指定して、以下のように使う。

pyinstaller pythonfile.py --onefile --icon=iconfile.ico

これで単独で実行できるEXEができる。
ツールの配布先/実行環境のPythonの有無やバージョンに
頭を悩まさずに済むため、大変便利だ。

pyinstallerのエラー発生時の解決方法:

下記の記事を参考にさせていただき、感謝!!
https://qiita.com/pocket_kyoto/items/80a1ac0e46819d90737f

エラー:「AttributeError: 'str' object has no attribute 'items'」

解決策
pip install --upgrade setuptools

エラー:「Cannot find existing PyQt5 plugin directories」

解決策
pip install PyQt5

形態素解析をPythonのEXE化に組み込む方法

名詞を抽出して「表記揺れ」対象リストを作る

パワポやエクセルから取得したテキストから
名詞を抽出して、レーベンシュタイン距離の比較対象とする。
 (N-gramを使うような方法も有力かもしれない = 検討中)

名詞以外も対象としても良いのだが、
活用の違いなどで誤検知が増えるため名詞に限定した。
さあ、形態素解析ツールを導入しよう。

Mecabではダメだった

Pythonで形態素解析するツールとしてはMecabが有名である。

「赤の他人」の対義語は「白い恋人」 これを自動生成したい物語
https://qiita.com/youwht/items/f21325ff62603e8664e6

の時もMecabを使っており、同様のコードが使えるため、
形態素解析自体の手法の記載はここでは省略する。

だが、ここで大きな問題があった。
Mecabは外部ツール的なポジションであるため、
pyinstallerで作るEXEの中に入ってくれないのだ。
つまり、EXE化は出来るのだが、
Mecabをインストールしていないパソコンでは
動作しないものになってしまう。

そこで、Janomeにしてみた

pipでインストール出来るJanomeならば、
pyinstallerでEXEに組み込める、と考えた。

Janome
https://mocobeta.github.io/janome/

以下のコマンドでインストールできる。

pip install janome

Mecabの代わりにJanomeを使おう。
ほぼ同様に使える。

が、JanomeでもEXE化するとエラーが発生!

が、Janomeを含むスクリプトを、
pyinstallerでEXE化後に実行しようとすると、
以下のようなエラーが発生してしまった。

Janomeを組み込んだEXE実行時のエラーログ
AttributeError: 'Matcher' object has no attribute 'dict_data'

今回の開発ではこのエラーが、
情報が少なく最もハマったところだ。

解決、そして形態素解析を組み込んだEXEが完成!

どうやらこれは、Janomeで使っている辞書データを。
pyinstallerが認識してくれず組み込めないことが原因。
Janomeは内部的に「sysdic」というパッケージを使っているようだ。
「sysdic」自体はJanomeと同様に「site-packages」フォルダ下にあった。
Janomeのフォルダ下ではないために、
pyinstallerが依存物を引っ張ってこれないという現象だ。

「--onefile」オプションをつけない場合は、
「dist」フォルダ配下(出力されるEXEファイルと同じ場所)に、
「sysdic」をフォルダごとコピーして置けば良いようだが、
「--onefile」オプションは、
単一のEXEファイルに変換するオプションであり、
これが無いと配布物がバラバラしてしまうので、ぜひつけたい。

最終的には「--add-binary」オプションで解決できた。
以下のように書く。

解決策
pyinstaller pythonfile.py --onefile --icon=iconfile.ico --add-binary "C:\Users\YourUserName\Anaconda3\Lib\site-packages\sysdic";sysdic

当初「--add-binary」オプションの書き方が良く分かっていなかった。
ファイルパスの後ろの「;sysdic」がポイントで、
EXEファイル側から見た時のパスを書いておける模様。
今回のsysdic以外でも、任意のdllや画像ファイルなどを
使いたい場合は同様に「--add-binary」が有効だ。

なお、EXE化する際には「--key」オプションを入れると、
実行ファイルを暗号化できる。

EXE化したツールは辞書データを含めて非常に重く、
配布物は実に250MB前後ある。
このために、Web上で事例がほぼ無かったのだろう。

パワポエンジニアが多い組織(注:架空の組織の話)においては、
Mecabを自分の環境に入れておいてください、とか、
pipでJanomeを入れておいてください、とかは、
日本語として通じないため、
誰でもどこでも使えるように、
重くてもEXEにしたほうが便利であろう。

この方法は、一般の人へツールを作成する時にも使えそうだ。
例えば以前「対義語自動生成ツール」が配布できなかった理由は、
まさにこの形態素解析含みの配布物を作れなかったことが、
最大の要因の一つであるのだから。

既存ツールとの特徴比較考察

このような様々なノウハウの組み合わせで、とうとう
パワポエクセル一発リント君」(Windows向け)
https://www.vector.co.jp/soft/winnt/business/se519740.html
(※2019/4/17~公開されました)

は完成した。
単純に(架空のワイが)欲しいものを作るのではなく、
既存ツールと比較して、
それぞれ何か一か所以上は利点があるか確認すべきだ。
具体的なツール名は伏せるが、
本ツールの特徴と、既存ツールとの比較を考えよう。

校正ツールの希少性
文章校正に使えるツール自体がまず少ない。
または高価な専門ツールになってしまう。
その点、本ツールは無料で利用可能だ。

パワポ&エクセルを対象
文章校正ツールの多くは単純テキストのみを対象にしている。
パワポやエクセルに対応したツールは少ない。
その点、本ツールはパワポやエクセルのテキスト化ツールとしても使える。

表記揺れを一括チェック
Office自体に組み込まれている誤字チェック機能は
「表記の揺れ」には対応できず、
「Kubernetes」のような新語/技術用語にも弱い。
複数のファイルを横断して統一チェックすることも出来ない。
その点、本ツールは自身の資料内で使われている単語と突き合わせるため、
どんな用語だろうが問題なく、しかも複数ファイル横断での一括チェックも可能だ。

完全オフライン & 前提ソフト無し
文章解析要素を含む校正ツールの場合、辞書の包含やAPI利用のために、
Web上に文章を置かないといけない場合が多い。
その点、本ツールは完全にオフライン、ローカル環境で使える。
企業内の資料をチェックするにはほぼ必須となる前提であろう。
しかもインストールの前提ソフトが無く、誰でも使える。
(Officeそのものすら必要ない)

検出力に優れる
そこそこの検出力があり、表記揺れの場合に、
どちらに寄せるべきかの候補、使われている回数と共に表示される。
(※検出力については、まだ今後の検証を要するが、
  人が見つけられなかった多数の誤りを一瞬で見つけたことは事実)
本ツールのチェック観点は少し独特なので、
仮に別のツール等を用いている人にも、二重チェックとして役立つであろう。

これだけの特徴があれば、
完全上位互換のツールは世の中に存在しないと考えられる。
この考察をもって、本ツールのリリースを許可することにした。

さあ、新しくなったワイのお仕事風景を見てみよう。

パワポエンジニアのお仕事風景より(After)

ワイ「毎日毎日僕らはエクセルの~」
ワイ「マス目揃えて、やになっちゃうよ~」

上司「アホな歌うたってないで」
上司「お客様にご説明する資料できたか?」
上司「あと、設計書もできたか?」
上司「今日はプレミアムフライデーだから残業禁止やで」

ワイ「はい、できました!」

上司「へぇ~、できました、ねぇ」
上司「どれどれ」
上司「誤字脱字も申し分ない」
上司「ほう、ここも大文字小文字が統一されて・・・」
上司「おいキミ、こんなパワポどこで!?」

(人さし指を立てて)
ワイ「パワポエクセル一発リント君!!」

上司「元ネタのビズリーチとかけ離れすぎて分からん!!」

ワイ「10回以上見直したような資料でも」
ワイ「100ページあると10種類くらい、何かの誤字や表記揺れが見つかるで」

ワイ「これでワイも毎日がエブリデイやで!」
ワイ「Qiitaへの投稿時に誤字/表記揺れの簡易チェックとしても使えるで!
   #あまり短い文章やコードに向いたツールじゃないけどな」

おしまい。

「個人開発」は思想(ニーズ)と技術(ノウハウ×努力)の融合である。
一個一個の要素は大した話ではない。技術も高度な話はない。
今までの物語もそうだった。

訓練不要で誰でも速読!日本一の速読アプリ「瞬間速読」の個人開発物語(25万DL)
https://qiita.com/youwht/items/a1b7a843888c27490172

【無料】Qiitaの殿堂を作った物語【簡単】
https://qiita.com/youwht/items/9851c2ac9024633fc04e

簡単な要素でもアイデアを組み合わせ、形を創ることは興味深い。

ただ、今回の物語は気持ちと思想に大いなる矛盾をはらんでいる
何が矛盾かはここでは表現できない。
悩み続けることもまた物語なのかもしれない。

★投げやりな重要追記事項(あとで消すかも)

Qiitaを見るようなイケてるエンジニア諸氏にとって、
パワポやエクセルの誤字/表記揺れなんて、
誰も気にしていないのは分かっている。
99%の人が不要であるどうでもいい投稿だ。
ツールの公開自体、迷うところがある。(実はまだ迷っている)

パワポエンジニアの同志エクセル方眼紙使い(or 使わされ)がいたら、
「いいね」で公開支持を表現してほしい。

この記事への反応(いいね)が少ない場合、
「良かった。病気の子供パワポエンジニアはいないんだ....。」(参考
という気持ちで、それはそれで嬉しくなるだろう。
⇒ Vectorへの公開申請も取り下げるツモリ
意外にも多くのご支持をいただき公開しました@4/17追記
 先行配布した範囲では、検出力的にはなかなか評判が良かったこともあり。
 ただ、ページ数や対象資料の内容にもよるので、
 ご期待に沿う動きになっていると良いのですが・・・。
 (近日、MS-WORDも対象にするかも?)

今回のツールは用途が楽しく無いので、
いつもに比べて、公開モチベーションが非常に低い・・・。
ちょっと楽しくしようと「ワイ」に登場してもらったが、
かえって闇が深くなってしまった気がする。
4/17に公開されていないかもしれない。
or オトナの事情で本記事ごと消滅しているかもしれない。
(Vectorでの公開後に投稿しようかと思ったが、
 Pythonの部分は既に書き終わっているし、
 迷いがある分、時間があると蛇足が増えていくので
 もう投稿しちゃうことにした)

反応(いいね)が多い場合、嬉しみがありながらも、
日本の未来は大丈夫なのか心配になってしまう。
各個別のノウハウ記載に対してや、
「ワイ」のネタに対しての、いいねだと思うことにする。
(そして気が向いたらmac版もコンパイルしておく)

ところで、こんな話を思い出した。
痩身エステや痩身サプリの主なターゲット顧客は、
超太っている人ではなく、むしろ標準に近い人、と聞いたことがある。
そのサービスの効果を最大に得ることができそうな人は、
そのサービスを全く受ける気が無い≒課題を気にしていない、という。
パワポエンジニアはQiitaを見ないし、
チェックを自動化するツールを探したりもしない、ような気がする。


この物語はフィクションです。
登場する人物・団体・名称等は架空であり、
実在のものとは関係ありません。
「ワイ」「上司」のモデルとなった人・組織はありません。
※ ツールが公開 or 申請取り消し、した際には、
 ダウンロードURL表示のところは直しておきます。

「写経」を自動化し、オートで功徳を積める仕組みを作ってみたのでございます。

写経により邪念を滅却し、功徳を積もう

写経は、一字書くたびに一体の仏像を彫ることと同じである、
と言われております。
仏像彫る人が可哀そうじゃね?

昨今の現代社会のストレスに耐え兼ね
私も写経によって心を落ち着かせ、
極楽浄土への功徳を積みたいと思うようになりました。

写経とは?

写経(しゃきょう)とは、仏教において経典を書写すること。
現代の日本で写経と言えば、『般若心経』の書写を指すことが多い。
(参考:Wikipedia

効率的に写経するために

俗世の欲にまみれたエンジニアのみなさまは
常に効率化を考えてしまいがちですね。業(ごう)が深いことです。

効率化の例として、チベット仏教には
マニ車
というものがございまして、
1回転させると1回お経を読んだのと同じだけ、
徳を積んだことになるそうです。

これがなかなかの優れものでして、
水車マニ車、風車マニ車、ソーラーマニ車、など
「オートで功徳を積める」ものが多数出現しております。
ハンドスピーナーマニ車まである模様

しかし、マニ車ではちょっと頑張った感がありません。
そこで今回、「写経感を損なわないようにしながらも
オートで写経できる仕組み」をPythonで作ってみたのでございます。

出来たもの

こちらのgifをご覧くださいませ。

Syakyou02.gif

最終的に動作コード含めて作り方を全てご提示いたします。

重要な方針=真心をこめた一打鍵

幸いなことに、「写経」については、
その手段/フォーマット(使う道具)は問われないと聞いております。
(宗派によるそうです)

そこで、筆⇒キーボード入力、紙⇒電子化、までは許されると仮定できます。

  • ログでバーッと出すような仕組み ⇒ NG
  • キーボード入力の自動化 ⇒ OK

と仮定しました。
仏様の像を彫るような気持ちで真心をこめて、一打鍵ずつ、
ha nn nya si nn gyo u
と入力していけば良いわけですね。

自動化までの道のり

キーボード入力の自動化= pyautogui

インストール
pip install pyautogui

以下のようにして、打鍵の自動化ができます。

pyautoguiの基本的な使い方
import pyautogui

#エンターキーを2回押す
pyautogui.press('enter', presses=2)
#コントロール+V
pyautogui.hotkey('ctrl', 'v')
#任意の文字列のタイピング
pyautogui.typewrite("hannya")

他の使い方詳細は、下記の素晴らしい記事をご参照くださいませ。
https://qiita.com/hirohiro77/items/78e26a59c2e45a0fe4e3

自動化の難題=日本語非対応

当初は、以下のような感じかなーと思っておりました。

失敗例1
pyautogui.typewrite("般若心経")

が、日本語には対応していなかったのです!
そこで ローマ字 + 変換キー にしてみたのですが・・・

失敗例2
#ローマ字で入力させて
pyautogui.typewrite("hannnyasinngyou")
#変換すればよいのではないか?
pyautogui.press('space"', presses=1)

これだと、日本語入力をONにしていれば入力は出来るのですが、
重要な問題:

般若心経の漢字はIMEでほとんどまともに出てこない

変換が効かないんですよね、仮に出てきたとしても、
何番目にその正しい対象があるのか取得は難しいです。

漢字はクリップボードコピペ=pyperclip

そこで、漢字はクリップボード経由で
事前に登録 ⇒ ctrl+V
という形で入力することを思いつきました。
pyperclip を使えばクリップボードに介入できます。

インストール
pip install pyperclip

以下のようにして、クリップボード操作ができます。

pyperclipの基本的な使い方
import pyautogui
import pyperclip

#クリップボードに登録
pyperclip.copy(”般若心経”)
#コピペ
pyautogui.hotkey('ctrl', 'v')

しかし、もし全てctrl+Vだけで写経していたら、
当初の目的であった「写経感」が失われてしまい、
ログを出しているのと同じような感じになってしまいます。

そこで、きちんとキーボード入力を行いつつ、
出力を正しい漢字にするという融合が必要になってきます。
楽して写経しようなどという考えは甘いのです。

写経感の工夫ポイント=ESCキー

pyautogui によってキー入力を行いつつ、
その変換の代わりに、「ESCキー」を連打することで、
入力した内容を消去し、
消去した瞬間にctrl+V作戦を行うことで、
IMEの変換っぽく見せることができる
という画期的な新技術を編み出しました。

コードの詳細は最後の全行コード内でご確認ください。

出来た全部のコード

さあこのコードで、みなさんもオートで功徳を積んでみてください
部屋を暗くして、誰も居ない部屋で
繰り返し回数を多くして実行すると雰囲気が出てオススメです!

環境に応じて多少改変してお使いください。

オート写経コード全文
#!usr/bin/python
# -*- coding: UTF-8 -*-

##PyAutoGUIのモジュール
#pip install pyautogui
import pyautogui

#クリップボードコピペ用
#pip install pyperclip
import pyperclip

import sys
import time

def GijiHenkan(kanji, roumaji, sleeptime):
    #roumaji文字列をタイプする(※全角モード前提)
    #pyautogui.typewrite(roumaji)
    #↑不自然に早いので不採用

    #全部の文字を一文字ずつ打つ
    for char in roumaji:
        pyautogui.press(char, presses=1)
        time.sleep(sleeptime)

    #変換前にひとこきゅう
    time.sleep(sleeptime)

    #クリップボードに漢字をコピーしておく
    pyperclip.copy(kanji)
    #消去した瞬間にクリップボードから文字をコピペ
    #消去については環境ごとに異なる可能性があるが、
    #escの2回押しにしておく。
    pyautogui.press('esc', presses=2)
    #コピペ
    pyautogui.hotkey('ctrl', 'v')
    #ひとこきゅう
    time.sleep(sleeptime)

    return 0

def Kaigyou(sleeptime):
    pyautogui.press('enter', presses=1)
    time.sleep(sleeptime)
    return 0

def DoSyakyou(sleeptime,kaigyousleeptime):
    GijiHenkan("摩","ma",sleeptime)
    GijiHenkan("訶","ka",sleeptime)
    GijiHenkan("般","hann",sleeptime)
    GijiHenkan("若","nya",sleeptime)
    GijiHenkan("波","ha",sleeptime)
    GijiHenkan("羅","ra",sleeptime)
    GijiHenkan("蜜","mi",sleeptime)
    GijiHenkan("多","ta",sleeptime)
    GijiHenkan("心","sinn",sleeptime)
    GijiHenkan("経","gyou",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("観","kann",sleeptime)
    GijiHenkan("自","ji",sleeptime)
    GijiHenkan("在","zai",sleeptime)
    GijiHenkan("菩","bo",sleeptime)
    GijiHenkan("薩","satu",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("行","gyou",sleeptime)
    GijiHenkan("深","jinn",sleeptime)
    GijiHenkan("般","hann",sleeptime)
    GijiHenkan("若","nya",sleeptime)
    GijiHenkan("波","ha",sleeptime)
    GijiHenkan("羅","ra",sleeptime)
    GijiHenkan("蜜","mixtu",sleeptime)
    GijiHenkan("多","ta",sleeptime)
    GijiHenkan("時","ji",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("照","syou",sleeptime)
    GijiHenkan("見","ken",sleeptime)
    GijiHenkan("五","go",sleeptime)
    GijiHenkan("蘊","unn",sleeptime)
    GijiHenkan("皆","kai",sleeptime)
    GijiHenkan("空","kuu",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("度","do",sleeptime)
    GijiHenkan("一","ixtu",sleeptime)
    GijiHenkan("切","sai",sleeptime)
    GijiHenkan("苦","ku",sleeptime)
    GijiHenkan("厄","yaku",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("舍","sya",sleeptime)
    GijiHenkan("利","ri",sleeptime)
    GijiHenkan("子","si",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("色","siki",sleeptime)
    GijiHenkan("不","hu",sleeptime)
    GijiHenkan("異","i",sleeptime)
    GijiHenkan("空","kuu",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("空","kuu",sleeptime)
    GijiHenkan("不","hu",sleeptime)
    GijiHenkan("異","i",sleeptime)
    GijiHenkan("色","siki",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("色","siki",sleeptime)
    GijiHenkan("即","soku",sleeptime)
    GijiHenkan("是","ze",sleeptime)
    GijiHenkan("空","kuu",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("空","kuu",sleeptime)
    GijiHenkan("即","soku",sleeptime)
    GijiHenkan("是","ze",sleeptime)
    GijiHenkan("色","siki",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("受","jyu",sleeptime)
    GijiHenkan("想","sou",sleeptime)
    GijiHenkan("行","gyou",sleeptime)
    GijiHenkan("識","siki",sleeptime)
    GijiHenkan("亦","yaku",sleeptime)
    GijiHenkan("復","bu",sleeptime)
    GijiHenkan("如","nyo",sleeptime)
    GijiHenkan("是","ze",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("舍","sya",sleeptime)
    GijiHenkan("利","ri",sleeptime)
    GijiHenkan("子","si",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("是","ze",sleeptime)
    GijiHenkan("諸","syo",sleeptime)
    GijiHenkan("法","hou",sleeptime)
    GijiHenkan("空","kuu",sleeptime)
    GijiHenkan("相","sou",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("不","hu",sleeptime)
    GijiHenkan("生","syou",sleeptime)
    GijiHenkan("不","hu",sleeptime)
    GijiHenkan("滅","metu",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("不","hu",sleeptime)
    GijiHenkan("垢","ku",sleeptime)
    GijiHenkan("不","hu",sleeptime)
    GijiHenkan("浄","jyou",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("不","hu",sleeptime)
    GijiHenkan("増","zou",sleeptime)
    GijiHenkan("不","hu",sleeptime)
    GijiHenkan("減","genn",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("是","ze",sleeptime)
    GijiHenkan("故","ko",sleeptime)
    GijiHenkan("空","kuu",sleeptime)
    GijiHenkan("中","tyuu",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("無","mu",sleeptime)
    GijiHenkan("色","siki",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("無","mu",sleeptime)
    GijiHenkan("受","jyu",sleeptime)
    GijiHenkan("想","sou",sleeptime)
    GijiHenkan("行","gyou",sleeptime)
    GijiHenkan("識","siki",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("無","mu",sleeptime)
    GijiHenkan("眼","genn",sleeptime)
    GijiHenkan("耳","ni",sleeptime)
    GijiHenkan("鼻","bi",sleeptime)
    GijiHenkan("舌","zextu",sleeptime)
    GijiHenkan("身","sinn",sleeptime)
    GijiHenkan("意","i",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("無","mu",sleeptime)
    GijiHenkan("色","siki",sleeptime)
    GijiHenkan("声","syou",sleeptime)
    GijiHenkan("香","kou",sleeptime)
    GijiHenkan("味","mi",sleeptime)
    GijiHenkan("触","soku",sleeptime)
    GijiHenkan("法","hou",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("無","mu",sleeptime)
    GijiHenkan("眼","genn",sleeptime)
    GijiHenkan("界","kai",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("乃","nai",sleeptime)
    GijiHenkan("至","si",sleeptime)
    GijiHenkan("無","mu",sleeptime)
    GijiHenkan("意","i",sleeptime)
    GijiHenkan("識","siki",sleeptime)
    GijiHenkan("界","kai",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("無","mu",sleeptime)
    GijiHenkan("無","mu",sleeptime)
    GijiHenkan("明","myou",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("亦","yaku",sleeptime)
    GijiHenkan("無","mu",sleeptime)
    GijiHenkan("無","mu",sleeptime)
    GijiHenkan("明","myou",sleeptime)
    GijiHenkan("尽","jinn",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("乃","nai",sleeptime)
    GijiHenkan("至","si",sleeptime)
    GijiHenkan("無","mu",sleeptime)
    GijiHenkan("老","rou",sleeptime)
    GijiHenkan("死","si",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("亦","yaku",sleeptime)
    GijiHenkan("無","mu",sleeptime)
    GijiHenkan("老","rou",sleeptime)
    GijiHenkan("死","si",sleeptime)
    GijiHenkan("尽","jinn",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("無","mu",sleeptime)
    GijiHenkan("苦","ku",sleeptime)
    GijiHenkan("集","syuu",sleeptime)
    GijiHenkan("滅","metu",sleeptime)
    GijiHenkan("道","dou",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("無","mu",sleeptime)
    GijiHenkan("智","ti",sleeptime)
    GijiHenkan("亦","yaku",sleeptime)
    GijiHenkan("無","mu",sleeptime)
    GijiHenkan("得","toku",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("以","i",sleeptime)
    GijiHenkan("無","mu",sleeptime)
    GijiHenkan("所","syo",sleeptime)
    GijiHenkan("得","toku",sleeptime)
    GijiHenkan("故","kou",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("菩","bo",sleeptime)
    GijiHenkan("提","dai",sleeptime)
    GijiHenkan("薩","saxtu",sleeptime)
    GijiHenkan("埵","ta",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("依","e",sleeptime)
    GijiHenkan("般","hann",sleeptime)
    GijiHenkan("若","nya",sleeptime)
    GijiHenkan("波","ha",sleeptime)
    GijiHenkan("羅","ra",sleeptime)
    GijiHenkan("蜜","mixtu",sleeptime)
    GijiHenkan("多","ta",sleeptime)
    GijiHenkan("故","ko",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("心","sinn",sleeptime)
    GijiHenkan("無","mu",sleeptime)
    GijiHenkan("罣","kei",sleeptime)
    GijiHenkan("礙","ge",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("無","mu",sleeptime)
    GijiHenkan("罣","kei",sleeptime)
    GijiHenkan("礙","ge",sleeptime)
    GijiHenkan("故","ko",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("無","mu",sleeptime)
    GijiHenkan("有","u",sleeptime)
    GijiHenkan("恐","ku",sleeptime)
    GijiHenkan("怖","hu",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("遠","onn",sleeptime)
    GijiHenkan("離","ri",sleeptime)
    GijiHenkan("一","ixtu",sleeptime)
    GijiHenkan("切","sai",sleeptime)
    GijiHenkan("顛","tenn",sleeptime)
    GijiHenkan("倒","dou",sleeptime)
    GijiHenkan("夢","mu",sleeptime)
    GijiHenkan("想","sou",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("究","ku",sleeptime)
    GijiHenkan("竟","kyou",sleeptime)
    GijiHenkan("涅","ne",sleeptime)
    GijiHenkan("槃","hann",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("三","sann",sleeptime)
    GijiHenkan("世","ze",sleeptime)
    GijiHenkan("諸","syo",sleeptime)
    GijiHenkan("仏","butu",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("依","e",sleeptime)
    GijiHenkan("般","hann",sleeptime)
    GijiHenkan("若","nya",sleeptime)
    GijiHenkan("波","ha",sleeptime)
    GijiHenkan("羅","ra",sleeptime)
    GijiHenkan("蜜","mixtu",sleeptime)
    GijiHenkan("多","ta",sleeptime)
    GijiHenkan("故","ko",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("得","toku",sleeptime)
    GijiHenkan("阿","a",sleeptime)
    GijiHenkan("耨","noku",sleeptime)
    GijiHenkan("多","ta",sleeptime)
    GijiHenkan("羅","ra",sleeptime)
    GijiHenkan("三","sann",sleeptime)
    GijiHenkan("藐","myaku",sleeptime)
    GijiHenkan("三","sann",sleeptime)
    GijiHenkan("菩","bo",sleeptime)
    GijiHenkan("提","dai",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("故","ko",sleeptime)
    GijiHenkan("知","ti",sleeptime)
    GijiHenkan("般","hann",sleeptime)
    GijiHenkan("若","nya",sleeptime)
    GijiHenkan("波","ha",sleeptime)
    GijiHenkan("羅","ra",sleeptime)
    GijiHenkan("蜜","mixtu",sleeptime)
    GijiHenkan("多","ta",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("是","ze",sleeptime)
    GijiHenkan("大","dai",sleeptime)
    GijiHenkan("神","jinn",sleeptime)
    GijiHenkan("呪","syu",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("是","ze",sleeptime)
    GijiHenkan("大","dai",sleeptime)
    GijiHenkan("明","myou",sleeptime)
    GijiHenkan("呪","syu",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("是","ze",sleeptime)
    GijiHenkan("無","mu",sleeptime)
    GijiHenkan("上","jyou",sleeptime)
    GijiHenkan("呪","syu",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("是","ze",sleeptime)
    GijiHenkan("無","mu",sleeptime)
    GijiHenkan("等","tou",sleeptime)
    GijiHenkan("等","dou",sleeptime)
    GijiHenkan("呪","syu",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("能","nou",sleeptime)
    GijiHenkan("除","jyo",sleeptime)
    GijiHenkan("一","ixtu",sleeptime)
    GijiHenkan("切","sai",sleeptime)
    GijiHenkan("苦","ku",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("真","sinn",sleeptime)
    GijiHenkan("実","jitu",sleeptime)
    GijiHenkan("不","hu",sleeptime)
    GijiHenkan("虚","ko",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("故","ko",sleeptime)
    GijiHenkan("説","setu",sleeptime)
    GijiHenkan("般","hann",sleeptime)
    GijiHenkan("若","nya",sleeptime)
    GijiHenkan("波","ha",sleeptime)
    GijiHenkan("羅","ra",sleeptime)
    GijiHenkan("蜜","mixtu",sleeptime)
    GijiHenkan("多","ta",sleeptime)
    GijiHenkan("呪","syu",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("即","soku",sleeptime)
    GijiHenkan("説","setu",sleeptime)
    GijiHenkan("呪","syu",sleeptime)
    GijiHenkan("曰","watu",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("羯","gya",sleeptime)
    GijiHenkan("諦","tei",sleeptime)
    GijiHenkan("羯","gya",sleeptime)
    GijiHenkan("諦","tei",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("波","ha",sleeptime)
    GijiHenkan("羅","ra",sleeptime)
    GijiHenkan("羯","gya",sleeptime)
    GijiHenkan("諦","tei",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("波","ha",sleeptime)
    GijiHenkan("羅","ra",sleeptime)
    GijiHenkan("僧","sou",sleeptime)
    GijiHenkan("羯","gya",sleeptime)
    GijiHenkan("諦","tei",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("菩","bo",sleeptime)
    GijiHenkan("提","ji",sleeptime)
    GijiHenkan("薩","so",sleeptime)
    GijiHenkan("婆","wa",sleeptime)
    GijiHenkan("訶","ka",sleeptime)
    Kaigyou(kaigyousleeptime)

    GijiHenkan("般","hann",sleeptime)
    GijiHenkan("若","nya",sleeptime)
    GijiHenkan("心","sinn",sleeptime)
    GijiHenkan("経","gyou",sleeptime)
    Kaigyou(kaigyousleeptime)
    Kaigyou(kaigyousleeptime)
    Kaigyou(kaigyousleeptime)

    return 0


#以下、メインルーチン
if __name__ == "__main__":

    #実行前の待機(秒)
    print("5秒後に写経が始まります。")
    print("心を静かにして")
    print("テキストエディタを開いて、")
    print("日本語入力モードにしておきましょう。")
    time.sleep(5)

    sleeptime = 0.0015
    kaigyousleeptime = 0.02

    #写経開始
    #強制終了(ctrl+c)するときにキーボード入力が継続されてしまうので
    #実行する際には注意。
    #繰り返し回数は最初は1回だけなどにしておいた方がよい
    for var in range(0, 3):
        DoSyakyou(sleeptime,kaigyousleeptime)

    print("おつとめおつかれさまでした。")
    sys.exit()

あとがき

写経を(自動で)していると、
心が洗われるような感じがいたします。

特にエンジニアのみなさまにおかれましては、
「コンピュータが何か頑張っている感の様子を見ること」
だけでも心が洗われる、という統計結果が出ております。
(エンジニア3名に聞いてみた7つの心理的安全性 ~~民明書房~~)

プログラム起動中は他の作業は一切出来ない「禅仕様」ですので、
ぜひみなさまも、
お忙しいなかの日常にも写経というオアシスを
取り込んでみてはいかがでしょうか?

また、日ごろお世話になっているみなさまへの感謝を込めて
1いいねごとに1回、
心を込めた自動写経を代行させていただきます。
ぽちっと押すだけで功徳を積める、Qiita史上最高に徳の高い記事です。

写経だけではなく読経したい方へのオススメ

以前ご紹介させていただいた、日本一の速読アプリにも、
般若心経を速読or念じられるモードを搭載しております。

訓練不要で誰でも速読!日本一の速読アプリ「瞬間速読」の個人開発物語(25万DL)

よろしければこちらもぜひご参照くださいませ。

ツッコミ

って、プログラムの「写経」じゃなくて、
ホンモノの「写経」の話だったんかーい!!!!

誰かの心の声が聞こえたような気がいたします。

悟りをひらいた仏様は「六神通」という超能力をお持ちで、
その一つ「他神通」では「他人の心を知る事ができる能力」があるそうですが、
私も本記事で沢山の功徳を積んだことで、
そのひとつに目覚めつつあるのかもしれません。

今回は、このツッコミをここまでひっぱって書くことと、
途中のお経データ入力が一番大変でした。

以上です。

高速なデスクトップ英語辞書ツールを作った物語(英語コピ郎君)

英語コピ郎君

クリップボードにコピーした英単語の意味がわかる、
高速な英語辞書ツールです。

下記から無料で入手できます。
https://www.vector.co.jp/soft/winnt/edu/se519844.html

Windows版/Mac版両方を上記ファイルに同梱しています。
(※Mac版はオマケ試作的な位置づけです)

英語を見たら片っ端から日本語にしてやりたいゼ!
お手軽に辞書をひきたいゼ!的な人にオススメです。

デモ(GIF)

GIFだけだと分かりにくいのですが、
範囲指定後に「ctrl + C」を押下しており、
それに対応して辞書検索されます。

eigocop_demo01.gif

特徴

①「超汎用」に使える。高速&ネット不要
 ・ローカルに辞書データを持ち、高速&ネット不要
 ・インプットがテキストデータならばなんでもよく、
  ブラウザ、エディタ、メール、IDE、PowerPointなど全部利用可能
 ・検索結果の再利用、再検索(結果からのジャンプ検索)が容易
 ・検索結果のコピペや保存が容易(検索履歴出力機能も)

②「無意識」に使える。作業の邪魔になりにくい
 ・自動的に表示非表示が切り替わる(オプション)
 ・いつも定位置に表示可能。色や文字サイズの変更可能
 ・原型への変換不要、複数形や過去形のままひける

③「高網羅」でなんでもひける
 ・英辞郎の230万語のデータをインポート可能
  (デフォルトでも6万5千語の辞書を搭載し、すぐ使えます)
 ・「含む」検索で関連語句や熟語、後方一致語句も出る
 ・日本語⇒英語の検索も実現

使い方

下記のページよりダウンロードしてEXEを起動するだけ。
https://www.vector.co.jp/soft/winnt/edu/se519844.html

Windows版/Mac版両方を上記ファイルに同梱しています。
(※Mac版はオマケ試作的な位置づけです)

英辞郎データがあればさらに便利に使えます。
500円で230万語のデータが使えるため大変オススメです。
https://booth.pm/ja/items/777563

辞書データ加工ツールを起動して、上記で購入した英辞郎データを選択。
生成されたファイル「dicdata.pidic」をEXEと同じパスに
置いておけば、次回起動時からそのデータが利用されます。

本記事の概要

  • 作ったもの=英語コピ郎君 の紹介
    • 上述までの内容で済
  • 本題:開発経緯の物語
    • 方針やコンセプトをどう考えたか?
  • 技術話:どのようにして実現したか悩んだ物語
    • 実は全く別に見える3つの記事とも関連が!?

の3点を語るものです。

本題:開発経緯の物語

背景①:Chrome拡張版 「Mouse Dictionary」

あれ、似たような(もっとカッコいい)ツールをどこかでみたな・・・。
という方は、下記のwtetsu氏のツールです。
Qiitaでも大きな話題になった素晴らしいツールです。

 Chrome拡張の高速な英語辞書ツールをつくりました(Mouse Dictionary)

正直に、Chromeブラウザ上のみでご使用になる場合は、
Mouse Dictionaryの方が早いしオシャレです。

本ツール「英語コピ郎君」は
「Mouse Dictionary」のデスクトップアプリ相当、
マウスオーバ ⇒ クリップボード経由 で速度も落ちた代わりに、
Chrome以外でも何でも使えるようになったり、
自動非表示切り替えや、結果再検索等が出来るようになったものです。

背景②:中国語版 「中国語よめ~る君」

数年以上ず~と昔、平成の時代から、
中国語版の、同等以上の機能&独自辞書内蔵のソフトを作っていました。

中国語よめ~る君(VectorからDL=Win版のみ)

中国語には「ピンイン」や「声調」という発音記号があり、
言語習得上とても重要な要素です。それを扱うツールです。
日本語での近い概念としては、漢字に対して、
音読み訓読みを自動判断してルビと抑揚を表示してくれるようなツールです。

中国語学習ソフト界隈(どんな小さな界隈だよw)においては
恐らく1、2を争うほど有名なツールです。

2016年に、ソースを流用してあまり手間なく作れたので
英語版も作ってあったのですが、そちらはあまり使われることは無かったです。
中国語版と違い競合が多すぎる、無料の辞書データのみで語彙数が貧弱、
そもそもUI設計がダサいし英語向きではない、など様々な理由が考えられました。

ある日、森の中クマさんに出会って、開発開始!

そんなある日、前述の「Mouse Dictionary」の記事を拝読し、
大変感銘を受けました。
近い用途のツールも開発公開済みだったのに、という一抹のくやしさも感じました。
また、英辞郎のデータが購入出来ることを初めて知りました。

私のツールでも「英辞郎」を使えるようにしてみたい、
と思うのは自然なことでしょう。

しかし、本当に作る意味があるのか?
また、どのように作るのか?
どんなコンセプトで作るのか?
など、いろいろ考慮しなければいけません。

本記事は、そうした個人開発で考慮したストーリー(物語)と、
その実現に使った技術要素を記載しておこうと思い書きました。

洗濯とか柴狩りとかの日常系から始まらずに、
開始早々にクマさんに出会う衝撃的展開!
クマさんと「お逃げなさい」のギャップ
(実は、なぜ逃げないのか?の反語表現である説も)
そして、なぜか追跡をかけるクマさん。
起承転結どころか転転転結レベルのカオス。
これに敬意を表し、ある日突然の衝撃を受けること、を
「森のクマさんに出会う」現象と呼ぶことにしました。

類似ツールとの比較検討

作成の意義を問うには、明らかに新規な場合を除き、
既存の近い手段との特徴比較が王道と言えます。

まず、Macにはデフォルトでもローカル辞書ツールがある、
という点は、私がWindowsの方を良く使うので大きな問題はなさそうです。

最大の比較対象はやはり「Mouse Dictionary」です。
「マウスオーバーのみでひける」点がとてもスゴイですね。

「Mouse Dictionary」の最大の弱点は、
Chromeブラウザ上でしか使えないことでしょう。
とはいえ、英単語を調べたい時は
Chrome上で見ている時が最も多いかもしれません。
Chrome以外でもどこでも使えます、という特徴以外にも、
もっと本ツール独自のコンセプトが欲しいところです。

そこで「Mouse Dictionary」をさらに勉強させていただき、
2点、差別化できる可能性に気付きました。

一つ目は「表示位置」です。
ブラウザのワクの中に表示する性質のため、
あるレイアウトのページでは右下にあって欲しいし、
また別のレイアウトのページでは左下にあって欲しい、
などの表示位置のわずらわしさを多少感じます。
そこで、一つ目のテーマは、
「表示位置の固定」「自動表示非表示切り替え」による、
目線的な意味での邪魔になりにくさ、を考えました。
(デスクトップ版ならブラウザのワク外の固定位置に表示できるので、
 Webページのレイアウトによらずに使えます。
 自動で表示非表示が切り替わればさらに便利です)

二つ目は「検索結果テキストの汎用性」です。
通常の「電子辞書」ではよく "ジャンプ機能" を使って、
調べた単語の結果に表示されている別の単語を
再検索するような使い方を良く行います(少なくとも自分は)。
また "単語登録" で調べた結果を保存したりもします。
PC上で辞書を引く場合は、調べた結果テキストを
コピーして別の場所に張り付けたい、などもあるでしょう。
「Mouse Dictionary」ではマウスオーバーという性質上、
そうした再検索や、結果のコピペが難しいことが分かりました。

「デスクトップ版」に比べこれらの利点があるならば、
差別化要素として十分でしょう。

コンセプトが決まった!

目指す方向性が見えてきました。
無意識」:表示位置を気にする率を減らす系
超汎用」:デスクトップ版で汎用性があり、さらに結果の再利用が容易

さらに、個人的に和英も欲しかったため、
そもそも英辞郎が使えるように、というのと合わせて、
高網羅」の概念も追加しました。
和英だけでなく「包含/後方一致」等も検索HITします。

実際はこれらの名前は、開発がほぼ終わった後に名付けましたが、
作る際の「イメージ」は早期から考えながら作っていました。
RPGなどのキャラ育成の際に、どのパラメータにステータスを振るのか?
という育成方針と同じ話です。

これらの特徴があれば、たとえ「劣化版」などと言われようとも、
別の価値観を持つ人(自分と近い価値観を持つ人)
には響くツールになるはずです。

個人開発においては何かに「勝ち」にいく必要は無いでしょう(※勝てませんし)。
"上位互換" を目指すのではなく、"独自性" が重要だと考えています。

「こっちのツールの方がいいよ」「このやり方の方がいいから使わん」
「そもそも英語を読む時に日本語の意味をあてはめてはイカン!」とかまで、
様々な意見を言う方がいらっしゃるでしょう。
その意見も否定はしませんが、全て不毛な会話です。
一部の人(最悪自分だけでも)が気に入ればいいのです!
個人開発していらっしゃる方は、全方位に完璧なものよりも、
一点突破的なものを気楽に作れば良いと思います。
Qiitaの記事についても同様のことが言えると思います。

技術話:どのようにして実現したか悩んだ物語

背景②の「中国語よめ~る君」は、
JavaScript + Electron によって作成していました。
一方、PythonでもGUIアプリやEXEを作れるということを知ったため今回は、
Python + Tikinter + Pyinstaller で開発してみることにしました。
どうせ作るならば一度失敗を認めてゼロからスタートしてみよう、
とのリセット的な心境もあったのでしょう。

この技術を使ってみたいから、という選定方法は本意ではありません。
様々な課題に直面することになりました。(若干の失敗感・・・)

一方で"知見の獲得"も開発目的の一つとしたことで、
同様技術を流用した様々な「寄り道」も楽しむことができました。

「寄り道」 = 全く無関係そうに見える以下3記事は、
実はこの記事に関連する一連の流れなのでした!!

ぱっと見ではこの記事含めて同じ人が書いていると思えないほど関連性が無いですね
本記事はこれらの記事の「裏番組」であり「本体」でもあります

「寄り道」と共通で使っている技術要素

英辞郎データの検索性能の確認

今回の開発で最も気になった点は、
英辞郎データ=230万語に対する検索性能です。

英辞郎データは、非圧縮時で130MBほどの重さがあります。
ローカル検索といっても、毎回ファイルから読みだしていては遅い気がします。
初回ロード後はオンメモリで扱う方針でいきますが、
230万語に対する検索ってどの程度早いのか、は不明でした。

その検索性能の確認/検討をしていた際の寄り道が以下の記事です。
k8sとなんの関係があるんだ?という方はぜひご一読ください。

結論としては、
230万語に対してそのまま検索しても十分に高速、というものでした。
(ただし、デフォルト辞書=数万語程度と比較するとやはり遅さは感じられる)

「英語コピ郎君」の検索時の処理時間として、2点の遅延箇所があります。

  • 単語検索の時間
  • クリップボード監視間隔(設定で変更可能=過度に短くするとCPU影響大)

クリップボード監視間隔の方が影響は大きいという印象で、
単語検索側はかなり高速でした。

一応、検索を高速化する工夫として、辞書データの持たせ方を、
頭文字のアルファベットごとにデータを分けて所持し、
毎回「全検索」ではなくて、検索対象を頭文字ごとに絞る
(例:カツオ(Katsuwonus)の検索は「K」の辞書だけに対して実施)
という案も試してみました。
が、十分早いために最終的には絞ることはやめました。
例えば「wonus」で検索をしても、「Katsuwonus」が出てきます!

結果、英辞郎データを用いる場合の課題は、
「Pyinstallerは起動が遅い」という点と合わせて、
「アプリの起動時」にファイル読み込みで立ち上がりが遅くなってしまう、
という点でしょうか。
毎回英辞郎データフォーマットからパース/加工するのではなく、
こちらが使うように加工したデータをPickleで事前保存/読み込みをする、
という方式にしていますが、それでも起動は少しもたつきます。
半常駐的に使いたいアプリなのでこの起動時数秒問題の解決はあきらめました。

いずれにせよ、
「せっかく英辞郎データを購入したので、遊んで元を取る」
という問題は十分なほど解決できたかもしれません。

クリップボード操作機能

pyperclipによるクリップボード操作は、
下記の記事でも実施しています。
写経の変換部をつかさどる重要な要素でした。

英語と写経という完全にかけ離れた世界をも
Pythonという"蛇道"な架け橋で繋ぐことが出来るのですね!

「サ道」(サウナ道) が最近とても流行っていますよね。
それにならって、
ヘンテコなPython開発のことを
「蛇道」と呼ぶことを提唱いたします。

PyinstallerによるEXE化

Pythonでもデスクトップアプリが作れる!
EXEの作成はPyinstallerを使いました。

下記の記事で実施しているのと同様の方法です。

パワポエンジニアの憂鬱を軽減する誤字/表記揺れ検出ツールを作った物語

一方で、「英語コピ郎君」では
大きな課題が二つ発生しました。

  • ①GUI対応(パワポリント君はCUIツール)
  • ②Mac&Winのマルチ環境対応

ここは一番の難所であり、
JavaScript + Electron で開発した時との比較を含めて後述します。

結論としては、もし今後同じように
デスクトップアプリを作りたいという方がいらっしゃいましたら、
インターフェース/UIにこだわりたい場合は特に、
「JavaScript + Electron」で作ることを推奨します。
 ※Pythonじゃないと実現しにくい機能も沢山あるため、
  それらの機能を用いたいということでなければ、という前提です。

JavaScriptならばスマホアプリまで、同じコードで作れます!

Android&iOS&Win&Macが同じコードで動く!超マルチアプリを作ろう[Monaca × Electron]

GUI対応 = Tkinter は、軽度なツールなら有用

PythonでGUIを作る方法としては、
Tkinter、kivy、wxPython、PyQtなど、
いくつかのライブラリ候補があります。

今回は Tikinter を選択しました。
主に以下の特徴があります。

  • Pythonの標準で付属
  • 習得が比較的容易(主観)
  • クロスプラットフォーム対応(Windows/Mac)

他を試しているわけではないのですが、
以下Tkinterに対する「個人の感想」です。

軽度なインターフェースを作るには
生産性や習得容易性的に十分すぎるほど有用。
しかし、ちょっと凝ったことをしたい場合や、
見た目を変更したい場合に難しくなります。

一方で JavaScript + Electron ならば、
HTML5系のインターフェースが全て使えるため、
ネット上の多数のノウハウがそのまま流用可能です。

今回のようにテキスト主体のちょっとしたツールレベルならば、
Tkinterもそう間違いではないと思います。
しかし、ビジュアルにこだわりたい場合、
Tkinterはやめた方がよいでしょう。
Pythonならば「kivy」の方が良いという話も聞きます。

Tkinterは軽度な業務用アプリを作るには向いていそうですね。

Mac&Winのマルチ環境対応 ⇒ ハマる

Windows/Mac両方に対応させる際には
意外と問題が多発しました。
一部、最後まで解決できていない問題
=作ったけど消去した機能、もあります。

ハマるポイントとその回避方法をいくつか書きます。

ハマり1:テーマ付きウィジットttkを使わない方が楽

Tkinterには、テーマ付きウィジェットの
Tkinter.ttkがあり、
こちらを使うと"ほぼ"同様のコードで、
より洗練されたインターフェースに出来るという触れ込みです。

間違ってはいないのですが、
WindowsとMac両方で動作させる場合に、
それぞれのOSごとで「テーマ」の違いが広がってしまい、
面倒になってしまいました。

また、Web上でTkinterについて調べた場合も、
「tk」のコードと「ttk」のコードが混ざっていて、
"ほぼ"同様にどちらでも動くのですが、
微妙にオプション指定方法などが異なる場合があり、
コピペでそのまま動いたり動かなかったり注意が必要です。

今回は洗練されたインターフェースよりも"作りきること"、
を目標として、ttkは最後の方で全て抹消しました。
このレベルのツールなら大して見た目変わらんし。
「ウィンドウのフチを消して移動できなくするオプション」などを
取り下げることにしました。

ハマり2:OSごとの使用可能フォントの違いに留意

フォントを設定する際に、それぞれのOSで
使用できるフォントが違うことにも留意した方が良いでしょう。

詳細は以下のリンク先で教えていただきました。
http://memopy.hatenadiary.jp/entry/2017/06/11/112619

使用可能フォントの確認
import tkinter as tk
import tkinter.font as font

root = tk.Tk()
print(font.families())

ハマり3:Pyinstallerの動作はOSごとに少し違う

Mac版ではPyinstallerの動作がいろいろ異なるようです。
アイコンの設定が効かなかったり、
「.app」化の際にスクリプトがそのまま露呈してしまう、
などです。(何か解決方法があるのかもしれません)

そもそもMac版はあまり重視しておらず、
同じコードで動くならこちらもコンパイルしておくか、
という程度で始めたワリに、問題が多すぎて、
いろいろ逃げまくっています。
最後には、Mac向け出力ではなくてLinux向け出力を同梱しており、
「.app」形式の配布はやめました。
Macでも使うよ、という人が増えるなら今後また考えます

JavaScript + Electron における開発でも、
Mac版で多少の違いは発生するのですが、
それよりも差異が大きいという印象です。
やはりWeb関連の技術の方がOS間の違いは少ない気がします。

その他さまざまな工夫

設定ファイルの作り方 = configparser

デスクトップアプリの設定値管理としては、
iniファイル を使いたくなります。
今回のツールでも、背景色などをiniファイルから変更可能です。

Pythonでiniファイルを扱うには、
configparser が便利です。
以下にその使い方を示します。

iniファイルの例
[VisualOption]
BgColor = #eeeeee

[Other]
INTERVAL = 10
configparserの使い方
import configparser

#設定ファイルの読み込み
try:
    config = configparser.ConfigParser()
    config.read(OPTION_SETTING_PATH)
    try:
        INTERVAL = config.getint('Other', 'INTERVAL')
        if INTERVAL < 1 :
            INTERVAL = 1
    except:
        print("INTERVAL-SET-ERR!!")
        pass
    try:
        BgColor = config.get('VisualOption', 'BgColor')
    except:
        print("BgColor-SET-ERR!!")
        pass
except:
    print("no-ok-configfile!!")
    pass

ユーザが触って良い値は、iniファイル + configparser で指定しています。
めんどくさいので設定を作りたくない 触らないで欲しい値は、
pickle でバイナリ保存/呼び出し、しています。
このようなライブラリが簡単に見つかる点はPythonは便利です!

クリップボード監視 or キーボードハンドリング

最も悩ましかった点は、
何をキック要因として検索を実行するか?です。

当初案としては、
キーボードイベントを監視して、
ctrl + C が押されたことに対応して検索する、という案でした。

もちろん、アプリケーションそのものに対する「ctrl + C」の
キーボードイベントはハンドリングできるのですが、
別のアプリを利用中であったり、最小化中であったりすると
この方法は上手くいきませんでした。
アプリケーション自体がアクティブじゃない場合、
イベントをとれない場合がありました。

「最小化」側を抑制してイベントをとる方法もあるでしょう。
例えば検索結果を「表示」する際には、
強制的にウィンドウを最前面にする処理を行っています。
アプリを常時アクティブ状態にしつつも、
「透明化」によってそれを感じなくさせる、などの小技もあり得たのですが、
"お行儀がよく無い" ので別な方法を考えることにしました。
使用時に一時的に最前面表示(アクティブ化ではない)、は自然な挙動ですが、
使用していない時にも自身を主張しすぎる挙動は良くないでしょう。
ユーザが最小化を命じたら素直に最小化されるべきです。

そこで「クリップボードの変更監視」の方法で実現することにしました。
こちらの方法は定期的に変更有無を確認する方法であったため、
バックグラウンド状態でも問題なく動いたのです。
イベントハンドリングではなく定期確認なので、
イベント着火状態によらず確実です。

英語、日本語、検索方法をどうするか?

どのような文字列が入力された場合にどんな検索を行うのか?
についてはパターンがいろいろで面倒でした。

コピペされた文字列によって様々な分岐を考える必要がありました。

  • 辞書検索ではない普通のコピペ作業の場合に検索をしないようにしたい
  • 日本語の場合は、和英として日本語側から検索をかけたい
  • 英単語の場合は、多少の空白や改行は無視して検索をかけたい
  • 英単語の場合は、過去形や複数形を原型に戻して検索をかけたい
  • (けど、毎回必ず戻すわけではなく、そのままでHITするならそちらを優先)
  • アルファベット1文字ならどうするか?
  • 「部分一致」した場合にどのように一致したものを優先度をあげるか?
  • 部分一致、原型戻し、など複数パターンがある場合にどれが優先度が高い? などなど

技術的に難しい内容ではないのですが、
様々なパターン、使い勝手を考えながら作るのは面倒でした。
「辞書をひく」って単純なようで結構複雑なんですね!

自動で隠れたり、位置を固定したりの工夫

無意識」に使えるようにするためには、
自動的に表示したり、隠れたりすることがポイントになります。
(AutoHide = ON に設定するとこの機能を試せます。
 デモ用のGIF動画においても、通常の編集的な動作時には
 英語コピ郎君がポップアップしない様子を映しています)

HIT件数が適正な値になっている場合は、検索したと判断してポップアップ。
HIT件数が0件であったり多すぎる場合は、ポップアップしない。
また、マウスカーソルの移動に従って再度隠れたり。
その間にユーザ操作で最小化やアクティブ化していた場合どうするか?
など、こちらもそれなりに様々なパターンがありました。

また、表示位置やウィンドウの大きさも、
起動のたびに再調整しなくてよいように、
変更時にはPickleを使って外部ファイルに保存するようにしています。

「辞書」ツールを作るといっても、
本当の検索関連のコードについては全体の2割くらいの手間で、
残り8割はその他こういった周辺機能の開発に費やすことになります。

おしまい。あとがき。

開発に至ったきっかけ、コンセプト、その実現方法までを
いろいろ書いてきました。
それでも
ここに記すには余白が狭すぎる (by ピエール・ド・フェルマー)
というくらいまだ書けていないことがいろいろあります。

最初にアイデアが100個くらいあったとしたら、
実際にコア機能だけ作ろうというのは5個くらいで、
周辺部までまじめに作ろうと思うのはそのうち1個くらい、でしょうか。
でもその最後の1個が一番手間がかかるんですよね。
Qiitaに書くまでをゴールとするならばコア機能(5個)段階の時点で
ネタにはなります。例:写経自動化、対義語自動生成など。
毎回全部書いているわけじゃないです。

なんかそういう個人開発のリアルな所、
綺麗じゃないごちゃごちゃダサい所を書いておこうと思いました。

技術的には全く大したことなく、
目的を達成しさえしていれば、課題に正面から向かわずに
「蛇道」で回避しながらのPythonアプリ開発でした。

世の中には新しい技術を追うのがすごい人もたくさんいらっしゃいます。
それを使って「完璧」と言えるようなツールを作る方もいらっしゃいます。
そのような人々はすごいなー、と思いつつも
個人的には そういう人には勝てないので最初から勝負しにいかない方針で
「枯れた技術の水平思考」
のほうが自分なりの創意工夫をいろいろできて楽しいのではないか?
と思うのであります。
ともに「蛇道」を歩みましょう。

以上、「英語コピ郎君」の開発物語でした。
https://www.vector.co.jp/soft/winnt/edu/se519844.html

「英語」がテーマという時点で、私の求める独自性ではなく、
既に一般側の方に近くなってしまっておりその感じがイヤで、
こんなことを書きたくなってしまった気がします。

長文お付き合いいただきありがとうございました。

文字で、文字や絵を書く技術

要約

あ…ありのまま 今 思った事を話すぜ!
「文字が文字で作れたら面白いよね?」
何を言っているのか わからねーと思うが、
おれも 何を言っているのか分からない。

  • 兎に角、下記の作例集を見れば何がしたいのかが分かる。まずは見てね
  • Colaboratoryで、前提一切不要&ブラウザだけですぐ動かせるよ

おれは 奴の前で文字を書いていたと思ったら
いつのまにか絵を書いていた。と思ったらやっぱり文字を書いていた。
頭がどうにかなりそうだった

作例集①

殺伐としたウニ

ebikani.png

これがホントの「エビカニ、クス(笑)」

殺伐としたスレに鳥取県が!!

satubatu_gunma.png

島根県 ( ※「矛盾塊」と呼ばれているらしい)

瀧「リューク、目の取引だ」

kiminona.png

アイドルの方の三葉が死ぬっ!

EVA

syogouki.png

こんなとき、どんな顔をしたらいいかわからないの


ごめんなさい。作例集を見ても
何がしたいのか」は分からなかったかもしれない。
「何が出来るようになるのか」は分かったと思う。
作例集②も最後にあるよ。

全体的な設計

逆に考えるんだ。

文字(エビ)で絵を書くためには、
文字(エビ)を書く座標が決まっていれば良い。
書く場所の座標 = 0と1で出来た二次元リスト。
二次元リスト = 白黒画像(グレースケール)

あとは、フレームとなる文字(カニ)を画像化して、
その白黒画像に入れれば完成。
まとめると、以下のような流れになる。

カニ ⇒ 画像化 ⇒ 白黒画像 ⇒ 01二次元リスト ⇒ エビで埋める

↑とても技術解説とは思えない説明文字列だ

◆さあ、以下の段取りで開発を進めよう!

  • 開発環境構築=不要(Colaboratory)
  • Step1 文字を画像にする技術
  • Step2 画像を白黒の01リストにする技術
  • Step3 白黒リストを文字で埋め尽くす技術
  • Step4 出来た関数のまとめ&最終的に画像に変換

開発環境構築=不要(Colaboratory)

今回は Colaboratory 上で、Python3 によって実装してみる。
ColaboratoryはGoogle様が用意してくれた
Jupyter&Pythonを簡単に実行出来る神環境
ブラウザでアクセスするだけですぐに本記事のコードが試せる。
お手元の環境を汚さない。エコ仕様。

全コード掲載&すぐにコピペ実行出来るようになっているので、
ぜひオリジナルの文字絵アート文字文字アートを作ってみてください!

(*´ω`)つ Colaboratory

Step1 文字を画像にする技術

準備:日本語フォントのインストール

Colaboratoryでは、最初に「!」をつけると
シェルコマンドの実行が出来る。
画像にしちゃう日本語フォントをインストールしてみよう。

Colaboratoryで日本語フォントのインストール
!apt-get -y install fonts-ipafont-gothic

インストールされたフォントのパスを確認してみよう。

TTFファイルのパスを確認する
import matplotlib.font_manager as fm
fonts = fm.findSystemFonts()
for font in fonts:
  print(str(font), "  ",fm.FontProperties(fname=font).get_name())

# 出力は省略。こんなパスの場所を確認出来る
# /usr/share/fonts/truetype/fonts-japanese-gothic.ttf

文字列を画像にする関数

Pythonの画像処理ライブラリ(Pillow)で
白色背景画像に文字を書き込み、
全体を画像として保存する。
これで、好きな「文字」を「画像」に出来る。

文字列を画像にする関数
from PIL import Image, ImageDraw, ImageFont
## 与えられた文字列を、画像にする関数
## 1文字あたりのサイズ&縦横の文字数も引数で指定
def str2img(input_str, yoko_mojisuu, tate_mojisuu, moji_size):
  # 真っ白な背景画像を生成する
  # 横(縦)幅 = 文字サイズ× 横(縦)文字数
  img  = Image.new('RGBA', (moji_size * yoko_mojisuu , moji_size * tate_mojisuu), 'white')
  # 背景画像上に描画を行う
  draw = ImageDraw.Draw(img)

  # フォントの読み込みを行う。(環境によって異なる)
  myfont = ImageFont.truetype("fonts-japanese-gothic.ttf    /usr/share/fonts/truetype/fonts-japanese-gothic.ttf", moji_size)

  # 文字を書く。基本は以下で済むが、今回は1文字ずつ記入
  # draw.text((0, 0), input_str , fill=(0, 0, 0), font = myfont)
  # ※備考:1文字ずつ記入の場合、半角と全角を区別しないといけなくなる
  # (今回は全角前提とする)
  # fillは、文字の色をRBG形式で指定するもの。今回は黒なので0,0,0固定
  # 縦横のサイズに合せて1文字ずつ描画
  yoko_count = 0
  tate_count = 0
  for char in input_str:
    #縦の文字数の許容量を途中でオーバーしてしまった場合は終了
    if tate_count >= tate_mojisuu:
      break
    #所定の位置に1文字ずつ描画
    draw.text( ( yoko_count * moji_size, tate_count * moji_size ), char, fill=(0, 0, 0), font = myfont)
    yoko_count +=1
    if yoko_count >= yoko_mojisuu:
      yoko_count =  0
      tate_count += 1

  return img

出来た関数は以下のように使える

str2img関数のお試し実行
import matplotlib.pyplot as plt
img = str2img("勝利友情努力", 2, 3, 50)
plt.imshow(img)

出力結果:
勝利友情努力.PNG

「三本柱マン」が無事降臨!!

なお、以前に、
どこでもドアを作ってみた物語
においてもPillowで画像加工を実施したことがある。
文字だけでなく画像の合成等も可能だ。

Step2 画像を白黒の01リストにする技術

「文字」の画像の場合もともと白黒なのだが、
任意の画像を文字で表現することにも対応するため、
まず画像を「白黒化」し、各ピクセルを0~1の少数で表現する。

そして、閾値(その画像全体の平均値とする)と比較して
白い場合は「1」黒い場合は「0」にすれば、
あらゆる画像が「1」と「0」の2次元リストになるというわけ。

画像の白黒化&01リスト化
from PIL import Image, ImageDraw, ImageFont
# 与えた画像を、グレースケールのリストに変換する関数(白=1、灰=0.5、黒=0)
# 元がカラー画像でも対応出来るようにしている
def img2graylist(input_img):
  #幅と高さを取得する
  img_width, img_height = input_img.size
  print('幅 : ', img_width)
  print('高さ: ', img_height)

  #最終的に出力する二次元リスト
  result_graylist = []
  for y in range(0, img_height, 1):
    # 1行ごとのテンポラリリスト
    tmp_graylist=[]
    for x in range(0, img_width, 1):
      # 1ピクセルのデータ(RGB値)を取得
      #(20, 16, 17, 255)のように4つのデータが取れる⇒3つに絞って使う
      r,g,b, = input_img.getpixel((x,y))[0:3]

      #RGB値の平均=グレースケールを求める
      g = (r + g + b)/3
      tmp_graylist.append(g)
    #1行終わるごとにテンポラリリストを最終出力に追加
    result_graylist.append(tmp_graylist)
  return result_graylist

# 与えたグレイリストを、白=1、黒=0のリストに変換する関数
# 黒が多い画像⇒全て黒、や、色の薄い画像⇒全て白、にならないように、
# 閾値として、平均値を取得した後で、その閾値との大小で判定する
# よって、薄い画像が全部白に、濃い画像が全部黒に、などはならない
import numpy as np
def graylist2wblist(input_graylist):

  #与えられた二次元配列の値の平均値を求める(npを使っても良いが)
  gray_sum_list = []
  for tmp_graylist in input_graylist:
    gray_sum_list.append( sum(tmp_graylist)/len(tmp_graylist) )
  gray_ave = sum(gray_sum_list)/len(gray_sum_list) 
  print("灰色平均値: ", gray_ave)

  # 最終的に出力する二次元の白黒リスト
  result_wblist = []
  for tmp_graylist in input_graylist:
    tmp_wblist = []
    for tmp_gray_val in tmp_graylist:
      #閾値と比べて大きいか小さいかによって1か0を追加
      if tmp_gray_val >= gray_ave:
        tmp_wblist.append(1)
      else:
        tmp_wblist.append(0)
    result_wblist.append(tmp_wblist)

  return result_wblist

出来た関数は以下のように使える

白黒化&01リスト化
#与えられた2次元文字列リストをプリントする関数(pprint的なもの)
#(※最終出力時には使わないが、途中経過を見る用途)
def print2Dcharlist(charlist):
  for tmp_charlist in charlist:
    for char in tmp_charlist:
      #改行無しで出力
      print(char, end="")
    #1行終わるごとに改行
    print()

img = str2img("般若波羅蜜多", 6, 1, 20)
graylist = img2graylist(img)
wblist = graylist2wblist(graylist)

print2Dcharlist(wblist)

出力結果:

hannya_kakunin.PNG

01デジタル化された般若心経

Step3 白黒リストを文字で埋め尽くす技術

作った白黒リストの「0」の部分だけを(または「1」の部分を)
「文字」で置き換えれば、ほとんど完成に近い。

エビエビエビエビエビ・・・と繰り返して文字を取得/出力するため、
Pythonの「ジェネレータ」を使って実装してみる。
yield は return のようなものなので、
return に読み替えると分かりやすいかもしれない(説明雑)

白黒リストを文字で埋め尽くす
# 文字列を一文字ずつ取り出すジェネレータ。半無限ループにより繰り返し
def infinity_gen_str(str):
  for a in range(1000000000):
    for s in str:
        yield s
# 以下のように使う
# 定義:gen_str =  infinity_gen_str("表示したい文字列")
# 使用:next(gen_str)
# これで、使用するたびに1文字ずつ出力される

# 白黒リストの、白黒の部分を文字列で埋め尽くした二次元リストを返す
# 白=soto_strで埋める。黒=nakami_strで埋める。
def wblist2wbcharlist(input_wblist, nakami_str, soto_str):
  # 1文字ずつ出力できるジェネレータの生成
  gen_nakami_str =  infinity_gen_str(nakami_str)
  gen_soto_str =  infinity_gen_str(soto_str)

  # 最終的に出力する二次元の白黒リスト
  result_wbcharlist = []
  for tmp_wblist in input_wblist:
    tmp_wbcharlist = []
    for tmp_wb_val in tmp_wblist:
      # 値が1か0かによって、文字列を入れていく
      # ※空白と等幅になる文字&フォントでやることが望ましい
      if tmp_wb_val == 1:
        # 1が白
        # 空白固定ならコレでも同じ ⇒ tmp_wbcharlist.append( " " )
        tmp_wbcharlist.append( next(gen_soto_str))
      else:
        # 0が黒
        tmp_wbcharlist.append( next(gen_nakami_str) )

    result_wbcharlist.append(tmp_wbcharlist)

  return result_wbcharlist

出来た関数は以下のように使える

01リストを文字列で埋める
img = str2img("般若波羅蜜多", 6, 1, 20)
graylist = img2graylist(img)
wblist = graylist2wblist(graylist)
#print2Dcharlist(wblist)

# 今回は↑の外枠で「般若波羅蜜多」のフレーム(01)を作り、
# ↓の指定で、中身を「般若波羅密多」の文字列で埋める
wbcharlist = wblist2wbcharlist(wblist, "般若波羅蜜多"," ")
print2Dcharlist(wbcharlist)

出力結果:
hannya_kakunin2.PNG

この技術に狂気と恐怖を覚える

Step4 出来た関数のまとめ&最終的に画像に変換

ここまでで、以下の流れの全てが実装できた。
カニ ⇒ 画像化 ⇒ 白黒画像 ⇒ 01二次元リスト ⇒ エビで埋める

最後に、これらの処理のまとめと、
出来たエビのリストを画像にして保存するようにしよう。

最後の画像変換では、最初の「文字を画像化する関数(カニ⇒画像化)」を
再利用することが出来る!

今までの関数の一括実行&画像化
def moji2mojiImg(flame_str, nakami_str, soto_str, yoko_len, tate_len, moji_size, final_moji_size):
  # 引数サンプル
  # flame_str = "般若"
  # nakami_str = "般若波羅蜜多"
  # yoko_len = 2
  # tate_len = 1
  # moji_size = 30
  # 最後に表示する際のフォントサイズ
  # final_moji_size = 12

  img = str2img(flame_str, yoko_len, tate_len, moji_size)
  graylist = img2graylist(img)
  wblist = graylist2wblist(graylist)
  wbcharlist = wblist2wbcharlist(wblist, nakami_str, soto_str)
  # print2Dcharlist(wbcharlist)

  # 作った配列を、str2imgで画像化する
  # 作ったリストを全てつなげて単純文字列にする
  # (※最初に作成したstr2imgに入れるための変換)
  all_str = ""
  for tmp_list in wbcharlist:
    for char in tmp_list:
      all_str += char

  #今回のファイルのサイズは縦横は、moji_size倍されている点に注意
  img = str2img(all_str, yoko_len*moji_size, tate_len*moji_size, final_moji_size)

  return img

出来た関数は以下のように使える

殺伐とした実行
img = moji2mojiImg("カニ","エビ"," ",2,1,20,15)

#正しく表示&ダウンロード出来るように、一度セーブする
img.save("ebikani.png")

#colaboratoryで表示
import IPython
IPython.display.Image("ebikani.png")

出力結果:

ebikani.png

エビもカニも甲殻類

出来た画像をColaboratoryからダウンロードするには以下

セーブしたファイルをローカルにダウンロード
from google.colab import files
files.download("ebikani.png")

(オマケ)画像を文字列で描画する技術

「文字」に文字を埋め込んで画像化することが出来た。
一方で、「画像」に文字を埋め込んで画像化することは、
実はより簡単に出来てしまう。

カニ ⇒ 画像化 ⇒ 白黒画像 ⇒ 01二次元リスト ⇒ エビで埋める

この、最初ステップのカニの画像化がなくなって、
直接画像の白黒化から始められるというだけ。
「アスキーアート」生成ツールの亜種的なものになる。

最後のStep4のまとめ関数をちょっと書き換えて実行してみる。

普段使うには 使わないけど こちらのほうが使いやすいかもしれない

画像を文字で描画する関数
def img2mojiImg(input_img, nakami_str, soto_str, final_moji_size):
  img_width, img_height = input_img.size
  #print('幅 : ', img_width)
  #print('高さ: ', img_height)

  #文字から画像を作る必要なく、input画像を使う
  #img = str2img(flame_str, yoko_len, tate_len, moji_size)
  graylist = img2graylist(input_img)
  wblist = graylist2wblist(graylist)
  wbcharlist = wblist2wbcharlist(wblist, nakami_str, soto_str)
  #print2Dcharlist(wbcharlist)

  #作った配列を、str2imgで画像化する
  #作ったリストを全てつなげて単純文字列にする
  #(※作成したstr2imgに入れるため)
  all_str = ""
  for tmp_list in wbcharlist:
    for char in tmp_list:
      all_str += char

  #今回のファイルのサイズは縦横は、moji_size倍されている点に注意
  img = str2img(all_str, img_width, img_height, final_moji_size)

  return img

出来た関数は以下のように使える
「グンマー」の画像は別途用意して
Colaboratoryにアップロードしておく。
(※左上の「>」から「ファイル」を選ぶと、アップロード出来る)

ぐんまちゃんに怒られないように気をつけて実行
img = Image.open("Gunma.png")
#幅と高さを取得する
img_width, img_height = img.size
print('幅 : ', img_width)
print('高さ: ', img_height)

#リサイズする場合は以下のような感じ
#元画像は幅640、高さ640
img = img.resize((40, 40))

result_img = img2mojiImg(img, " ", "栃木県", 14)

output_file_name = "satubatu_gunma.png"
result_img.save(output_file_name)

#colaboratoryで表示
import IPython
IPython.display.Image(output_file_name)

出力結果:
satubatu_gunma.png

グンマーは何をやっても面白いのでとてもお得

作例集②(オマケ)

はらみった

hannya.png

「写経」を自動化し、オートで功徳を積める仕組みを作ってみたのでございます。

しろくろ

simauma.png

じわじわくる

止まれ。

tomare.png

もう何十回も言ったのよ!?って言える必殺技

見よ、人がゴミのようだっ!

hitogomi.png

「バルス!!」「目がぁ~!目がぁ~!」

新時代アート

reiwa.png

【続】平成の次の元号を、AIだけで決めさせる物語(@テレビ取材)

その…下品なんですが…フフ…勃起…しちゃいましてね…

monakira.png

いいや!限界だ(いいねを)押すね!今だッ!

PythonでHello 世界(ザ・ワールド)止まった時の世界に入門してみる。ジョースターの末裔は必読

あとがき

大喜利

技術を使った大喜利として、ネタを考えるのも楽しいかもしれません。

面白い文字文字アートの案や、作例が出来たら、
ぜひコメント欄に張り付けて教えてください!

応用例/アイデアメモ

応用例はいろいろありそう。

  • 画像認識した部分を対応する文字に変える(車に認識された部分を「車」で表現など)
  • TwitterやSlackのツール/ボット作成、サービス化
  • 単純にアスキーアート生成技術として活用
  • 文字の色を変更してカラフル化
  • 濃淡に合わせて黒い文字/白い文字を使い分ける
  • セリフの無いマンガの作成
  • 薄い灰色で文字を印刷して、幼児/英単語などの書き取り練習帳に(書き終わると絵が浮かび上がる)
  • 究極的に「ざわざわっ...」「ゴゴゴゴゴ」している絵の作成(著作権的な意味で今回はパス)

いつもの名言

ちょっとした遊び & Colaboratoryの実践入門として
楽しんでいただけたら幸いです♪
ブラウザでColaboにアクセス、上から順にコピペしていくだけですぐ試せます。
文字文字アートで一緒に遊びましょう。

人類の進化は「遊び」からはじまる。
こんな「遊び」が出来るならば、というアイデアに触発される人がでて、
生活にも役に立つような「発明」が生まれるのだ。
          ~  Char Fuitter (1847~1912 オランダ) ~

長文おつきあいいただきありがとうございました。


出力結果画像は自由に転載していただいて構いません。
Char Fuitter (チャー・フイター)は架空の人物です。

AIが三国志を読んだら、孔明が知力100、関羽が武力99、を求められるのか?をガチで考える物語(自然言語処理編)

背景

関羽「どれどれ、拙者たち英傑の活躍は後世では
   どのように伝えられているのかな?」
孔明「なんとっ・・!!?扇からビーム出しとる?
   そしてSDガンダムと融合しとる!?
   あまつさえ、女体化して萌キャラなっとる!?」

"この報告は孔明にとってはショックだった・・・"

劉禅「いや、オマイラは知力100だったり、
   武力99だったりして優遇されとるだろ。
   朕なんて101匹いても勝てないぞ」
魏延「オレ、ゲンシジン、ミタイ、ナッテル・・・」

孔明「いや、わたしが知力100なのは当然でしょ」
司馬懿「まてぃ。最後に勝ったのはワシだよ?」
荀彧「違います。私こそが王佐の才・・・」

甘寧「最強はこの鈴の甘寧」
張遼「遼来々!最強はワタシだ!」
張飛「オレっちを忘れちゃいないかい!?」

誰が一番、武力・知力が高いのか
英傑たちの議論は白熱していった・・・。

曹操「みなの衆、静まれいっ!!
   ちかごろは、えーあいなるものがあると聞く。
   わしは有能なものは泥棒でも使ってやるぞ。
   えーあいに聞いてみようではないか!?」

本投稿の趣旨

KOEIの武将ステータスに大きな敬意を払いつつ、

三国志の小説を 自然言語解析 & 機械学習 すると
各武将のステータスはどのようになるのか?
の実験&研究を行う物語。

まさに技術の無駄の無双乱舞
(そして無駄に長い背景)

Colaboratoryを使って、環境構築不要でブラウザだけで
誰でも本格的な「三国志分析」が出来るという、
誰得コダワリ技のご紹介。
(※ふつーの自然言語処理の技としても流用可)

出来るだけ、コピペだけでお手元でも試していただけるように書く予定。

結論の一部を先に見てみよう

注:左から順に「武力、知力、政治、魅力」

武将名 本実験の推論結果 (参考)KOEI三国志5データ
曹操 95, 92, 87, 105 87, 96, 97, 98
劉備 89, 89, 84, 105 79, 77, 80, 99
諸葛亮 78, 98, 90, 104 60, 100, 96, 97
関羽 92, 75, 62, 82 99, 83, 64, 96
張飛 97, 61, 44, 77 99, 45, 17, 44
魏延 91, 65, 50, 68 94, 48, 37, 56
袁紹 70, 71, 66, 77 81, 77, 49, 92

吉川英治の「三国志」@青空文庫をINPUTとして、
「自然言語処理」と「機械学習」によって上記のように、
武力や知力などのパラメータを推論する。

三国志小説の機械学習結果として、
1つの武将を50次元ベクトルに変換し、そのベクトルを、
全く同じ「式」に入れて出てきた値が、上記の表。

このような方法:「小説(自然言語)」⇒「数値化」⇒「式」
によって、武力/知力を求めることが出来るか?
という実験&研究が今回のテーマ。

他の成果としては、
以下のような武将名の「演算」が楽しめる。
(これも実際の出力結果より抜粋)

  • 諸葛亮に近い人は誰?
    • ⇒ 姜維、司馬懿、陸遜、周瑜、魏延、馬謖
  • 劉備にとっての関羽は、曹操にとって誰?
    • ⇒ 袁紹、張遼
    •  ※若いころの馴染み的な意味や対比が多いので袁紹?
  • 孫権にとっての魯粛は、劉備にとって誰?
    • ⇒ 司馬徽(水鏡先生)、徐庶
    •  ※賢者を紹介するポジションなのか?

精度の高い結果を得るためには、前提として、
三国志という特殊な小説を、
うまーく自然言語処理(の前処理)をすることが最重要。
草履売りから蜀漢皇帝になるように、処理の改善のたびに、
コードが三国志を征服していくような物語を楽しんでほしい。

なお、機械学習の結果は面白いけれども、自然言語側から、
しかも1つの小説だけから作るのは精度に限度があるため、
本当にゲームのパラメータを決めたいならば、
INPUTとなる小説やテキストを大量に用意することが望ましい。
(このような手法が可能かどうか?を実験する目的であり、
 実際にパラメータをコレで決めたいわけではない)

天下三分の計 ~全体方針/目次~

曹操「えーあい?、えーあい?・・・」
楊修「おk、把握した。全軍退却!!」
劉備「待てぃ。話が終わってしまうw」
楊修「じゃあ劉備殿は えーあい が分かるのですかな?」
劉備「ぐっ! 孔明! 任せた、あとよろ!」
孔明「・・・。」
劉備「あとよろ! あとよろ!」
孔明「では天下三分の計の如く、
   3つのステップで今回の計画をご説明しんぜよう」
劉備「(3回言わないとやってくれないんだもんな・・・)」

■今回の進め方は以下3つのステップである。

① 吉川英治「三国志」@青空文庫を、
 三国志の固有名称に気をつけて、
 形態素解析し単語単位にバラす。

② バラした結果をWord2Vecによって、ベクトル化する。
 (Word2Vec:単語をN次元のベクトルで表現でき、
   その足し算引き算等の演算が行える技術。
   「赤の他人」の対義語は「白い恋人」 これを自動生成したい物語
   https://qiita.com/youwht/items/f21325ff62603e8664e6
  を先に見て頂くと良いかもしれない)

③ それぞれの「武将」がベクトル化された状態になるため、
  その中から「武力」や「知力」と相関が高いような
  ベクトル(複数ベクトルの集合体)を見つければ、
  何らかの数式によって、KOEI三国志のパラメータに
  近いものが計算できるのではないか?

一番最初にして最大の難関は、①の形態素解析、
三国志の世界を出来るだけ正しく認識すること。

以下のような、三国志の世界独特の壁が立ちはだかる。

  • 韓玄,劉度,趙範,金旋「我ら荊州四英傑をえーあいは分かるかな?」
  • 玄徳=劉玄徳=劉備玄徳 ⇒ 「劉備」のこと


※いらすとやさんの劉備の画像(あるんですね!)

ではさっそく①形態素解析から始めよう!

桃園の誓い ~環境準備~

"我ら生まれた時は違えども、死すべき時は同じと願わん!"

今回義兄弟の誓いをたてる最強のツールは以下3点。

  • Colaboratory (ブラウザ上で無料で使えるPython実行環境)
  • Janome  (環境構築が超楽な形態素解析器)
  • Word2Vec  (自然言語を数値化/ベクトル化する仕組み)

まずは、ColaboratoryとJanomeで、
一番簡単な自然言語処理の仕組みを作ってみる。
(ブラウザだけでお手元で簡単に試せます)

Colaboratoryの準備

Colaboratory (要Googleアカウント)
にアクセス。基本的な使い方はぐぐってくだされぃ。
環境構築不要でブラウザだけでプログラミングが出来る。

「ファイル」⇒「Python3の新しいノートブック」を作成しよう。

GoogleDriveに今回使う様々なデータを保存したいので、
下記のコマンドでGoogleDriveをマウントしよう。

GoogleDriveのマウント
# これを実行すると、認証用URLが表示されて、キーを入力すると
# 「drive/My Drive/」の中に、認証したアカウントのgoogle driveが入る
from google.colab import drive
drive.mount('/content/drive')

日本語を区切って品詞判定などが出来る、
Janome をインストールする。
Colaboratoryでは、コマンドの冒頭に「!」を書くことで、
いわゆるシェルコマンドが実行できる。

Janomeのインストール
!pip install janome

さっそく、Janomeで名詞・動詞の抽出をしてみよう!

Janomeで形態素解析(名詞・動詞の抽出)
#素状態のJanomeの性能を確認する
# Janomeのロード
from janome.tokenizer import Tokenizer

# Tokenneizerインスタンスの生成 
tokenizer = Tokenizer()

# テキストを引数として、形態素解析の結果、名詞・動詞原型のみを配列で抽出する関数
def extract_words(text):
    tokens = tokenizer.tokenize(text)
    return [token.base_form for token in tokens 
        if token.part_of_speech.split(',')[0] in['名詞', '動詞']]

sampletext = u"文章の中から、名詞、動詞原型などを抽出して、リストにするよ"
print(extract_words(sampletext))
sampletext = u"劉備と関羽と張飛の三人は桃園で義兄弟の契りを結んだ"
print(extract_words(sampletext))
sampletext = u"悪来典韋はかえって、許褚のために愚弄されたので烈火の如く憤った"
print(extract_words(sampletext))
実行結果
['文章', '中', '名詞', '動詞', '原型', '抽出', 'する', 'リスト', 'する']
['劉', '備', '関', '羽', '張', '飛', '三', '人', '桃園', '義兄弟', '契り', '結ぶ']
['典', '韋', '許', '褚', 'ため', '愚弄', 'する', 'れる', '烈火', '憤る']

ここまででもう、最も簡単な自然言語処理をする環境が整った!!
しかし結果をよ~く見てみると・・・。

げぇっ!関羽! ~武将名識別①~

げぇっ!「関羽」
が認識されていない・・・

関羽 ⇒ '関', '羽'
とバラバラになっている。「義兄弟」などの一般名詞と違い、
「劉備」「関羽」「張飛」などの三国志の武将名は、
普通に実行するだけでは認識されないのだ。

桃園の義兄弟レベルの人名が認識されないなんて大したことないな。
いやいや、Janomeではmecab-ipadic-NEologdの辞書データを使える。

Janomeの作者様 (@moco_beta 様) によって、
mecab-ipadic-NEologdを同梱したパッケージを公開していただいている。
(大感謝!温州蜜柑を差し上げたい
以下のURLにアクセスして、自分のGoogleDriveにコピーしよう。

https://drive.google.com/drive/folders/0BynvpNc_r0kSd2NOLU01TG5MWnc
(右クリックですぐにコピー、自分のGoogleDriveに持ってこれる)

janome+neologdのインストール
#結構時間がかかる(6分くらい)
#Mydrive上の、先程のjanome+neologdのパスを指定する
#最新版とファイル名が一致しているかどうかは各自で確認すること
!pip install "drive/My Drive/Janome-0.3.9.neologd20190523.tar.gz" --no-compile

インストールは成功した、かに見えるが、
最後に以下のような記載が出て、
「RESTART RUNTIME」のボタンが出る。

インストール実行結果の末尾
#WARNING: The following packages were previously imported in this runtime:
#  [janome]
#You must restart the runtime in order to use newly installed versions.

ColaboratoryのRUNTIMEを一度リセットしてね、
というお話なので、このボタンを押せばOK

Janomeの作者様の公式の方法はローカル環境向けであるため、
python -c "from janome.tokenizer import Tokenizer; Tokenizer(mmap=True)"
↑このコマンドを実行することになっているようだが、
Colaboratoryでは、RUNTIMEリセットすればこのコマンドは不要。

NEologd同梱版では、最初のTokenneizerインスタンスの生成コードだけ
ちょっと変える必要がある。
以下のコードで、NEologdの効果を見てみよう!

NEologd入れた状態で形態素解析する
# Janomeのロード
from janome.tokenizer import Tokenizer

# Tokenneizerインスタンスの生成 ★ここが異なる★
tokenizer = Tokenizer(mmap=True)

# テキストを引数として、形態素解析の結果、名詞・動詞原型のみを配列で抽出する関数
def extract_words(text):
    tokens = tokenizer.tokenize(text)
    return [token.base_form for token in tokens 
        if token.part_of_speech.split(',')[0] in['名詞', '動詞']]


sampletext = u"劉備と関羽と張飛の三人は桃園で義兄弟の契りを結んだ"
print(extract_words(sampletext))
sampletext = u"悪来典韋はかえって、許褚のために愚弄されたので烈火の如く憤った"
print(extract_words(sampletext))
sampletext = u"田豊。沮授。許収。顔良。また――審配。郭図。文醜。などという錚々たる人材もあった。"
print(extract_words(sampletext))
sampletext = u"第一鎮として後将軍南陽の太守袁術、字は公路を筆頭に、第二鎮、冀州の刺史韓馥、第三鎮、予州の刺史孔伷、第四鎮、兗州の刺史劉岱、第五鎮、河内郡の太守王匡、第六鎮、陳留の太守張邈、第七鎮、東郡の太守喬瑁"
print(extract_words(sampletext))
実行結果
['劉備', '関羽', '張飛', '三', '人', '桃園', '義兄弟', '契り', '結ぶ']
['悪来', '典韋', '許褚', 'ため', '愚弄', 'する', 'れる', '烈火', '憤る']
['田豊', '沮授', '許', '収', '顔良', '審配', '郭図', '文醜', '錚々たる', '人材', 'ある']
['鎮', '後将軍', '南陽', '太守', '袁術', '字', '公路', '筆頭', '二', '鎮', '冀州', '刺史', '韓', '馥', '三', '鎮', '予州', '刺史', '孔', '伷', '四', '鎮', '兗州', '刺史', '劉', '岱', '第五', '鎮', '河内郡', '太守', '王匡', '六', '鎮', '陳', '留', '太守', '張', '邈', '七', '鎮', '東郡', '太守', '喬', '瑁']

劉備、関羽、張飛はもちろんのこと、
典韋、許褚、田豊、沮授、などが認識出来ていることが分かる。
また、こうした有名武将の認識以外の面でも、
動詞や一般名詞の認識精度も上がるため、全体的に望ましい結果になる。

だがこの結果をよーく見てみると・・・・。

反董卓連合の全滅 ~武将名識別②~

NEologdを導入することで「劉備」「関羽」などの
ステータスが90以上ありそうな人や、SSRになっていそうな人
は認識出来るようになったが、
三国志の世界にはまだまだ有名ではないコモン扱いの人々は沢山居る。

先の結果では、裏切者の代名詞:「許収」が認識されていない。
また、タピオカ入り蜜水が大好きなニセ皇帝「袁術」さんは認識されたが、
韓馥、孔伷、劉岱、張邈、喬瑁、は全滅である。
これでは反董卓連合の激文を書くことができない。
さすがのNEologdでもここまではカバーしていなかったのだ。

そこで、「三国志登場人物リスト」を作って、
ユーザ辞書」としてJanomeに登録することにした。

https://ja.wikipedia.org/wiki/三国志演義の人物の一覧
このページの人物一覧をもとに、単純に1行に1名ずつ書いたテキストを作る。
それをアップロードして、以下のように読み込んでみよう。

人名リストの読み込み
#人物の名前が列挙してあるテキストから、ワードリストを作成する
import codecs
def getKeyWordList():
    input_file = codecs.open('drive/My Drive/Sangokusi/三国志_人名リスト.txt' , 'r', 'utf-8')
    lines = input_file.readlines() #読み込み
    result_list = []
    for line in lines:
        tmp_line = line
        tmp_line = tmp_line.replace("\r","")
        tmp_line = tmp_line.replace("\n","")
        #ゴミデータ削除のため、2文字以上のデータを人名とみなす
        if len(tmp_line)>1:

            result_list.append(tmp_line)
    return result_list

jinbutu_word_list = getKeyWordList()
print(len(jinbutu_word_list))
print(jinbutu_word_list[10:15])
実行結果
1178
['張楊', '張虎', '張闓', '張燕', '張遼']

このように、1178名分の人物を入れた、単純なリストを得た。

なお、マニアックな調整点や考慮点として、
「馬忠」は同姓同名がいるため、その区別はあきらめたり、
「喬瑁」はwikiに居なかったので後で追加したり、
「張繍」「張繡」の微妙な字体の違いとか、
「祝融夫人」⇒「祝融」に変更したりなどの調整はしている。

このリストをもとに、Janomeで利用可能な、
「ユーザ辞書形式」のCSVファイルを作成する。
設定できる箇所は多いのだが、今回は単純な人名リストであるため、
全部同じ登録内容で楽をする。

Janomeのユーザ辞書csvの作成
#作成したキーワードリストから、janomeのユーザ辞書形式となるCSVファイルを作成する
keyword_list = jinbutu_word_list
userdict_list = []

#janomeのユーザ辞書形式に変換をかける。コストや品詞の設定等
for keyword in keyword_list:
  #「表層形,左文脈ID,右文脈ID,コスト,品詞,品詞細分類1,品詞細分類2,品詞細分類3,活用型,活用形,原形,読み,発音」
  #参考:http://taku910.github.io/mecab/dic.html
  #コストは,その単語がどれだけ出現しやすいかを示しています. 
  #小さいほど, 出現しやすいという意味になります. 似たような単語と 同じスコアを割り振り, その単位で切り出せない場合は, 徐々に小さくしていけばいい

  userdict_one_str = keyword + ",-1,-1,-5000,名詞,一般,*,*,*,*," + keyword + ",*,*"
  #固有名詞なので、かなりコストは低く(その単語で切れやすく)設定
  userdict_one_list = userdict_one_str.split(',')
  userdict_list.append(userdict_one_list)

print(userdict_list[0:5])

#作成したユーザ辞書形式をcsvでセーブしておく
import csv
with open("drive/My Drive/Sangokusi/三国志人名ユーザ辞書.csv", "w", encoding="utf8") as f:
  csvwriter = csv.writer(f, lineterminator="\n") #改行記号で行を区切る
  csvwriter.writerows(userdict_list)
実行結果
[['張譲', '-1', '-1', '-5000', '名詞', '一般', '*', '*', '*', '*', '張譲', '*', '*'], ['張角', '-1', '-1', '-5000', '名詞', '一般', '*', '*', '*', '*', '張角', '*', '*'], ['張宝', '-1', '-1', '-5000', '名詞', '一般', '*', '*', '*', '*', '張宝', '*', '*'], ['張梁', '-1', '-1', '-5000', '名詞', '一般', '*', '*', '*', '*', '張梁', '*', '*'], ['張飛', '-1', '-1', '-5000', '名詞', '一般', '*', '*', '*', '*', '張飛', '*', '*']]

これで、有名(?)武将1000名以上が掲載されたユーザ辞書を得ることが出来た!
いよいよこの辞書を適用した結果を試してみよう。

ユーザ辞書を使った場合
# Janomeのロード
from janome.tokenizer import Tokenizer

#ユーザ辞書、NEologd 両方使う。★ここが変更点★
tokenizer_with_userdict = Tokenizer("drive/My Drive/Sangokusi/三国志人名ユーザ辞書.csv", udic_enc='utf8', mmap=True)

# テキストを引数として、形態素解析の結果、名詞・動詞原型のみを配列で抽出する関数
def extract_words_with_userdict(text):
    tokens = tokenizer_with_userdict.tokenize(text)
    return [token.base_form for token in tokens 
        #どの品詞を採用するかも重要な調整要素
        if token.part_of_speech.split(',')[0] in['名詞', '動詞']]

sampletext = u"劉備と関羽と張飛の三人は桃園で義兄弟の契りを結んだ"
print(extract_words_with_userdict(sampletext))
sampletext = u"悪来典韋はかえって、許褚のために愚弄されたので烈火の如く憤った"
print(extract_words_with_userdict(sampletext))
sampletext = u"田豊。沮授。許収。顔良。また――審配。郭図。文醜。などという錚々たる人材もあった。"
print(extract_words_with_userdict(sampletext))
sampletext = u"第一鎮として後将軍南陽の太守袁術、字は公路を筆頭に、第二鎮、冀州の刺史韓馥、第三鎮、予州の刺史孔伷、第四鎮、兗州の刺史劉岱、第五鎮、河内郡の太守王匡、第六鎮、陳留の太守張邈、第七鎮、東郡の太守喬瑁"
print(extract_words_with_userdict(sampletext))
実行結果
['劉備', '関羽', '張飛', 'の', '三', '人', '桃園', '義兄弟', '契り', '結ぶ']
['悪来', '典韋', '許褚', 'ため', '愚弄', 'する', 'れる', '烈火', '憤る']
['田豊', '沮授', '許', '収', '顔良', '審配', '郭図', '文醜', '錚々たる', '人材', 'ある']
['鎮', '後将軍', '南陽', '太守', '袁術', '字', '公路', '筆頭', '二', '鎮', '冀州', '刺史', '韓馥', '三', '鎮', '予州', '刺史', '孔伷', '四', '鎮', '兗州', '刺史', '劉岱', '第五', '鎮', '河内郡', '太守', '王匡', '六', '鎮', '陳', '留', '太守', '張邈', '七', '鎮', '東郡', '太守', '喬瑁']

「ウムッ!」

かなり三国志のコダワリを入れた結果が得られた!!

もちろん荊州四英傑のデータも入れているため、
弱小君主たちもそのファンも納得の分析が出来る。

余談:
形態素解析を行う場合、まず出てくる候補はmecabであろう。
しかし、mecabは環境構築が結構難しく大変である。
Colaboratory上ですぐに使う方法も知られてはいるが、
じゃあ、neologd入れられる?ユーザ辞書自分で追加できる?
となると、なかなかWeb上だけではサクサク環境構築出来ないと思う。
その点でJanomeは環境構築ハードルを下げてくれるので超オススメ!
三国志などの独自世界に対応した超カスタマイズ自然言語処理環境を
作る方法としては、おそらく最も扱いやすい手順を得られたと思う。
作者様ありがとうございます☆  温州蜜柑を差し上げたい。2つ目

一見するともうこれで十分だろ、感があるが、
まだまだ敵は立ちはだかる。
いよいよ次は「孔明の罠」にハマる物語。

その前に、ちょっと疲れてきたので休憩を兼ねて、
ここでスポンサーの曹操様から、
CM(イベントのご案内)を入れさせていただこう!

突然ですが、CMです☆

「SEKIHEKIのたた会」イベント案内

日時 : 208年11月20日頃
     (東南の風がふくまでご自由にご歓談ください)
場所 : 赤壁
参加者: 曹操・周瑜・諸葛亮など豪華ゲストが続々登壇!
LT  : 孫権 「部下がまとまる机の切り方」
     黄蓋 「三代の功臣が若手に無茶振りされた話」
     諸葛亮「10万本の矢を集めたノウハウを大公開」
     蔡瑁 「転職直後に上司の信頼を得る方法」
     龐統 「絶対に船酔いしない基盤構築を教えます」
     曹操 「部下を生き生きと働かせるアジャイル風マネジメント」
その他: 懇親会あり。あの有名武将と人脈を作るチャンス☆
     (寝返り目的の参加はご遠慮ください)

ここまで読んでいる人(居るのか?)には垂涎のイベント。
ぜひみなさまお誘いあわせの上ご参加ください!!


曹操「赤壁の戦いでお会いしましょう!(※ただし関羽テメーはダメだ)」

なお、ここまでで吉川英治三国志に興味を持った方は、
下記の速読アプリにも全巻無料で登録されていマス。
訓練不要で誰でも速読!日本一の速読アプリ「瞬間速読」の個人開発物語
残念ながらSEKIHEKIイベントにご参加できなかった方は、
こちらのアプリでイベントの様子を見ていただくことが出来ます。

さあ、いよいよ次は孔明の罠の登場だ。

「孔明」の罠 ~字(あざな)識別~

やった、人名データを登録したからこれで解析が出来るぞ!

待てあわてるなこれは「孔明」の罠だ。

このまま解析しても良い結果は得られない。
次の例文を見ていただこう。

「車上、白衣簪冠の人影こそ、まぎれなき諸葛亮孔明にちがいなかった。」
「これは予州の太守劉玄徳が義弟の関羽字は雲長なり」
趙子龍は、白馬を飛ばして、馬上から一気に彼を槍で突き殺した。」
趙雲子龍も、やがては、戦いつかれ、玄徳も進退きわまって、すでに自刃を覚悟した時だっ

「孔明」とは字(あざな)であり、「諸葛亮」が本名である。
彼は通常「孔明」と表現されているが、
諸葛亮、や、諸葛亮孔明、と表現されていることもたびたびある。
また、
「劉備」の字(あざな)は「玄徳」
「関羽」の字(あざな)は「雲長」
「趙雲」の字(あざな)は「子龍」
であり、文中でも「玄徳は~~」「雲長は~~」などと
たびたび字(あざな)が登場する。

このように、三国志の世界では、同じ人物に対して、
様々な呼び方が存在している。

少なくとも以下の4パターンは同じ人物として扱わないと困る。
「趙雲」=「子龍」=「趙子龍」=「趙雲子龍」
「劉備」=「玄徳」=「劉玄徳」=「劉備玄徳」
江東の小覇王とか、劉皇叔とか、は一旦忘れる。

これが、世に名高い「孔明(あざな)」の罠
ハマると同じ人物が4分裂してしまう凶悪な罠だ。

この罠を回避するために、まずは
字(あざな)と武将名のリストを作成し、
字をフルネームに変える置換処理を作る。

さらに、単純に置換しただけでは、
「趙子龍」⇒「趙趙雲」
「趙雲子龍」⇒「趙雲趙雲」
となってしまうため、これらの重複防止措置を取る。

なお、字(あざな)で書かれる場合が多いのは
かなり有名な武将に限定されているため、
今回用意した字リストは約130人分までだ。
このくらいまでなら、適宜三国志のファンサイトを参照して作成可能だ。
単純にカンマ区切りで、あざな&フルネームのCSVを作成し、読み込む。

あざなCSVの読み込み
import csv

csv_file = open("drive/My Drive/Sangokusi/三国志_あざな変換リスト.csv", "r", encoding="utf8", errors="", newline="" )
#リスト形式
azana_reader = csv.reader(csv_file, delimiter=",", doublequote=True, lineterminator="\r\n", quotechar='"', skipinitialspace=True)
azana_list = [ e for e in azana_reader ]
csv_file.close()

print(len(azana_list))
print(azana_list[2])

#全員の字リストを作るのは難しかったが、
#['雲長', '関羽']のような132人の代表的な字とその対比表が入っている
実行結果
132
['雲長', '関羽']

このようにして作成した対比表を用いて、
テキストに対する字(あざな)の変換処理を作る。

字(あざな)の変換処理の実装
#これは、字(あざな)を置き換えるだけの単純な置換処理
def azana_henkan(input_text):
    result_text = input_text
    for azana_pair in azana_list:
        result_text = result_text.replace(azana_pair[0],azana_pair[1])
    return result_text

#単純に、字からの変換をかけるだけだと、
#趙雲子龍→趙雲趙雲などのようになる場合が多いため、
#同一の人物名で重複している場合は、一方を削除する。
#また、劉玄徳、趙子龍、などのような表現に対応するため、
#フルネームで2文字の場合はAAB→AB(劉玄徳→劉劉備→劉備)
#フルネームで3文字の場合はAAB→AB(諸葛孔明→諸葛諸葛亮→諸葛亮)
# となる名寄せを行う。
#(※名字1文字+名前二文字はあまり居ない気がするので無視)
def jinmei_tyouhuku_sakujyo(input_text):
    jinbutu_word_list = getKeyWordList()
    result_text = input_text
    for jinbutumei in jinbutu_word_list:
        result_text = result_text.replace(jinbutumei+jinbutumei, jinbutumei)
        if len(jinbutumei) == 2:
            result_text = result_text.replace(jinbutumei[0]+jinbutumei, jinbutumei)
        if len(jinbutumei) == 3:
            result_text = result_text.replace(jinbutumei[0]+jinbutumei[1]+jinbutumei, jinbutumei)
    return result_text

sampletext = u"これは予州の太守劉玄徳が義弟の関羽字は雲長なり"
print(jinmei_tyouhuku_sakujyo(azana_henkan(sampletext)))
sampletext = u"趙子龍は、白馬を飛ばして、馬上から一気に彼を槍で突き殺した。"
print(jinmei_tyouhuku_sakujyo(azana_henkan(sampletext)))
sampletext = u"趙雲子龍も、やがては、戦いつかれ、玄徳も進退きわまって、すでに自刃を覚悟した時だった。"
print(jinmei_tyouhuku_sakujyo(azana_henkan(sampletext)))
実行結果
これは予州の太守劉備が義弟の関羽字は関羽なり
趙雲は、白馬を飛ばして、馬上から一気に彼を槍で突き殺した。
趙雲も、やがては、戦いつかれ、劉備も進退きわまって、すでに自刃を覚悟した時だった。

「ウムッ!」

やっと、三国志の固有名詞と、孔明の罠に対応することが出来た!

いよいよ三国統一の最後のツメとして、
漢中攻略に向かおう
前処理としては最後の関門に向かう。

鶏肋は死刑に ~ストップワード除去~

曹操「鶏肋、鶏肋・・・」
楊修「おk、把握した」

「鶏肋」とは、食べるには身がないがダシが取れるので
そのまま捨てるには惜しいことから
大して役に立たないが、捨てるには惜しいもの」のこと。

「雲長は気の毒になって、の好きな酒を出して与えたが」
↑ここで言う「彼」はもちろん「張飛」のこと。
しかし別なシーンでは、「彼」は「曹操」や「劉備」かもしれない。

この「彼」を除かずに分析を行うと、
「曹操」≒「彼」のような分析結果が出てしまう。
(単純に曹操が登場回数が多いこともあり)

一見意味のありそうな「彼」だが、
実際解析する上では雑音にしかならない。
よって、楊修と同じように死刑にしてしまおう

曹操「何勝手に退却しているんだよw死刑!」

このような鶏肋ワードの一覧として、良く使われるのが、
SlothLib というサイトだ。

ここに乗っている単語は全て死刑(削除)にするコードを書く。

まず、SlothLibにアクセスしてそのデータをリスト化する。

SlothLibからのデータの取得&リスト化
#雑音になりやすい単語(「彼」など)はストップワードとして除外する
#SlothLibのテキストを使う。
#どんな言葉が除外されるのかは、直接URLを見れば良い
#参考: http://testpy.hatenablog.com/entry/2016/10/05/004949
import urllib
slothlib_path = 'http://svn.sourceforge.jp/svnroot/slothlib/CSharp/Version1/SlothLib/NLP/Filter/StopWord/word/Japanese.txt'
#slothlib_file = urllib2.urlopen(slothlib_path) #←これはPython2のコード
slothlib_file = urllib.request.urlopen(slothlib_path)
slothlib_stopwords = [line.decode("utf-8").strip() for line in slothlib_file]
slothlib_stopwords = [ss for ss in slothlib_stopwords if not ss==u'']

#['彼','彼女',・・・]のようなリストになる
print(len(slothlib_stopwords))
print(slothlib_stopwords[10:15])
実行結果
310
['いま', 'いや', 'いろいろ', 'うち', 'おおまか']

310個の鶏肋ワードを取得することが出来た。
このようにして出来たリストを使って、
名詞動詞の抽出後に、鶏肋リスト中の単語は除外する処理、を実装しよう。

鶏肋ワードの除去機能を実装する
sampletext = u"彼は予州の太守劉玄徳が義弟の関羽字は雲長。彼は劉備玄徳の義兄弟だ"

tmp_word_list = extract_words_with_userdict(jinmei_tyouhuku_sakujyo(azana_henkan(sampletext)))

print(tmp_word_list)

#このようにして、単語リストからストップワードを除外する
tmp_word_list = [word for word in tmp_word_list if word not in slothlib_stopwords]

print(tmp_word_list)
実行結果
['彼', '予州', '太守', '劉備', '義弟', '関羽', '字', '関羽', 'なり', '彼', '劉備', 'の', '義兄弟']
['予州', '太守', '劉備', '義弟', '関羽', '関羽', 'なり', '劉備', 'の', '義兄弟']

上下の抽出結果を比べてみると、
「彼」という単語が消えているのが分かる。

さあ、全ての準備が整った。
最後にこれらの成果を吉川英治の全文に適用してみよう。

ジャーンジャーンジャーン(全文の形態素解析)

これまでの全ての成果を全文に適用する時が来た。

まず、「青空文庫」から吉川英治三国志の全文をダウンロードし、
全部の章を結合したテキストを作っておく。

ここで注意しなければならないのは、
以下のような青空文庫の独自表記。

公孫※[#「王+贊」、第3水準1-88-37]《こうそんさん》

⇒「公孫瓚」に置換しておく必要がある。

私は以下のような変換コードを1万行くらい書いてある、
独自のコードを以前から使っているが、もっといいやり方がある気がする。
self.resulttext=re.sub(r'※[#.*?1-88-37.*?]',"瓚",self.resulttext)

既に本稿が長くなりすぎており、
これは三国志というか青空文庫ハッキングの話であるため、
本稿においては割愛させていただく。

このような変換処理をかけた全文テキストデータを用意した所から話を続ける。

まず、字(あざな)の名寄せを行う。

全文テキストに対して、字(あざな)変換処理をかける
import codecs
def azana_henkan_from_file(input_file_path):
    input_file  = codecs.open(input_file_path, 'r', 'utf-8')
    lines = input_file.readlines() #読み込み
    result_txt = ""
    for line in lines:
        result_txt += line
    result_txt = azana_henkan(result_txt)
    return result_txt

#ファイル生成用関数定義
#mesのテキストを、filepathに、utf-8で書き込む
def printFile(mes,filepath):
    file_utf = codecs.open(filepath, 'w', 'utf-8')
    file_utf.write(mes)
    file_utf.close()
    return "OK"

azana_henkango_zenbun = azana_henkan_from_file('drive/My Drive/Sangokusi/三国志全文.txt')
azana_henkango_zenbun = jinmei_tyouhuku_sakujyo(azana_henkango_zenbun)

printFile(azana_henkango_zenbun,'drive/My Drive/Sangokusi/三国志全文_あざな変換済み.txt')

これで生成された字(あざな)変換済みのテキストに対して、
NEologd、ユーザ辞書、を搭載したJanomeによる形態素解析を行おう。
出来たデータは、pickleを使ってGoogleDrive内に保存しておけば、
引き続き作業を行う時に楽になる。

全文の形態素解析
%%time
#全文分解するのに10分ほどかかる

import codecs
# ['趙雲', '白馬', '飛ばす', '馬上', '彼', '槍', '突き', '殺す'] このようなリストのリスト(二次元リスト)になる
def textfile2wordlist(input_file_path):
    input_file  = codecs.open(input_file_path, 'r', 'utf-8')
    lines = input_file.readlines() #読み込み
    result_word_list_list = []
    for line in lines:
        # 1行ずつ形態素解析によってリスト化し、結果格納用のリストに格納していく
        # Word2Vecでは、分かち書きされたリスト=1文ずつ、のリストを引数にしている
        tmp_word_list = extract_words_with_userdict(line)

        #別途準備しておいたstopワードリストを使って除外処理を行う
        tmp_word_list = [word for word in tmp_word_list if word not in slothlib_stopwords]

        result_word_list_list.append(tmp_word_list)
    return result_word_list_list

Word_list_Sangokusi_AzanaOK_with_userdict_neologd = textfile2wordlist('drive/My Drive/Sangokusi/三国志全文_あざな変換済み.txt')

#作成したワードリストは、pickleを使って、GoogleDriveに保存しておく(一回10分くらいかかるからね)
import pickle
with open('drive/My Drive/Sangokusi/Word_list_Sangokusi_AzanaOK_with_userdict_neologd_V4.pickle', 'wb') as f:
    pickle.dump(Word_list_Sangokusi_AzanaOK_with_userdict_neologd, f)

#保存したpickleファイルは、以下のように復元する
with open('drive/My Drive/Sangokusi/Word_list_Sangokusi_AzanaOK_with_userdict_neologd_V4.pickle', 'rb') as f:
    Word_list_Sangokusi_AzanaOK_with_userdict_neologd = pickle.load(f)

print(len(Word_list_Sangokusi_AzanaOK_with_userdict_neologd))
print(Word_list_Sangokusi_AzanaOK_with_userdict_neologd[10:20])

これでとうとう、
吉川英治三国志全文を解析し、
武将名をかなり正しく認識&名寄せした上で、
「名詞、動詞」のリストに変換することが出来た!

次の作戦は、出来たリストを機械学習にかけ、
抽出された「武将名」の学習を行うことだ。
(次回へ続く・・・?)

自然言語処理編の終わり

仲達「こんなに長い記事を書いているなんて、
   フフフ、諸葛亮も長くはないぞ!」

長くなりすぎた。
キリも良いので、作者と読者の健康のために一旦ここまでで切る。
CMとかやってるからだよ

三国志の世界を機械学習するためには、
今回実施したようなコダワリの前加工処理が精度向上の鍵になる。

関羽千里行なみに、各関門をなぎ倒していく物語はいかがだっただろうか?

また、Colaboratory + Janome + NEologd + ユーザ辞書、
まで全セットの使い方として、
自然言語処理の裾野開拓にお役に立つことがあれば幸いである。
(Web上で簡単に作れて、NEologd+ユーザ辞書、まで使えるノウハウは、
 かなり調べても全て説明しているものは見当たらなかったため)

以前より、プログラマ向けに対象を限定せず、
非プログラマ/非Qiitaユーザでも雰囲気は楽しめるレベル、を
イメージして記事を投稿してきたが、
今回については、「三国志」知らない人には意味不明であろう。
Qiitaにも「三国志」タグは無かった・・・(当たり前)

「SEKIHEKIのたた会」イベントの参加者はQiita見て無さそうだし、
この投稿も「孔明の罠」って書きたかっただけだし、
「機械学習編」は書かないかもしれない。

後半が気になる人や、三国志分析が面白かったという人、
横山光輝リスペクトの部分でニヤっとした人は、
ぜひ応援よろしくお願いします。
後半では各英傑たちの「主人公補正」が明らかになるかも!?

★追記:続編(Word2Vec編)書きました。
https://qiita.com/youwht/items/fb366579f64252f7a35c

★追記:完結編、書きました。
https://qiita.com/youwht/items/61c6d5819cdc3aff9e63

長文おつきあいありがとうございました。
以上です。

【続】AIが三国志を読んだら、孔明が知力100、関羽が武力99、を求められるのか?をガチで考える物語(Word2Vec編)

背景

この物語は前編(自然言語処理編)からの続編です。

魏延「前回はどんな話だったかな・・・・?」
魏延「たしか、頭に角が生える話だったな!」
姜維「(いやそれはオマエの見た夢だろ・・・しかも死亡フラグ)」

前回のお話を忘れてしまった方は以下からご覧くだされ!

AIが三国志を読んだら、孔明が知力100、関羽が武力99、
を求められるのか?をガチで考える物語(自然言語処理編)
https://qiita.com/youwht/items/92056e63498c36de4e3b

そして、その魏延の死亡フラグはいきなり当たってしまうのであった!

今回は、Word2Vec によって、
武将名(自然言語)をベクトル化(数値化)するお話。
武将を「数値」で扱うことが出来れば、
その中のどこかの値に、
「武力」「知力」を示す値があるのではないか?
という仮説を検証する。

魏延「わしをベクトルに出来るものはおるか!?」

馬岱 Gensim「ここにいるぞ!」
魏延「ギャアァァァッ」
楊儀「やった、ついに反骨ヤロウを50次元のベクトルにしたぞ!

馬岱のかわりに魏延を切ってくれるのは、
Gensimというライブラリ。
以下のコマンドでインストールしておこう。

gensimのインストール
!pip install gensim 

前回の「自然言語処理編」で作った、
吉川英治三国志の形態素解析結果(名詞動詞を抽出したリスト)に対して、
機械学習を行い、武将を数値表現にする(ベクトル化)

ここで取り扱う機械学習の仕組みとしては、例えば
「関羽」は「劉備」の「義弟」である
「張飛」は「劉備」の「義弟」である
という2つの文章を見た場合、
(形態素解析により単語抽出済みであるため、
 [関羽,劉備,義弟]と[張飛,劉備,義弟]という
 二つの単語リストを入力した場合)
「関羽」と「張飛」だけ入れ替わったデータが存在する、
ということから、
「関羽」と「張飛」は似ている単語だ、
と、機械が学習していくのだ。

「曹操」は「張遼or夏侯惇or許褚」に「命じた」
などのような表現が多数あつまれば、
「張遼」≒「夏侯惇」≒「許褚」と機械が学習するし、
「劉備」は「趙雲」に「命じた」 などの表現と合わせると、
「曹操」≒「劉備」ということも学習していく、というワケ。

一騎当千の武将たち、神算鬼謀の軍師たち、
それぞれ、お互い「似ている」と機械が学習してくれれば、
「その中で ”似ている方向” の最先端にいる人は誰?」
を見れば、武力100、知力100、が分かるのではないか、
というのが本稿最大のアイデアである。

機械学習結果の出力として、各武将(各単語)は、
N次元のベクトルとして表現されることになり、
ベクトル同士の距離が近ければ「似ている」という感じ。

早速、以下のコマンドで実行してみよう!

機械学習:Word2Vecモデルの作成
%%time
#↑実行時間をログに出すためのオマジナイ

# Word2Vecライブラリのロード
from gensim.models import word2vec
import pickle

#ワードリストの復元(自然言語処理編の最後で作成したファイルの読み込み)
with open('drive/My Drive/Sangokusi/Word_list_Sangokusi_AzanaOK_with_userdict_neologd_V4.pickle', 'rb') as f:
    word_list = pickle.load(f)
print(len(word_list))


#機械学習におけるパラメータの設定値
#ここの値の調整が精度に直結している。
size_setting = 50
min_count_setting = 5
window_setting = 6
iter_setting = 4500

# size: 圧縮次元数
# min_count: 出現頻度の低いものをカットする(最低何回出現したら許可するか?)
# window: 前後の単語を拾う際の窓の広さを決める
# iter: 機械学習の繰り返し回数(デフォルト:5)十分学習できていないときにこの値を調整する
# model.wv.most_similarの結果が1に近いものばかりで、
# model.dict['wv']のベクトル値が小さい値ばかりのときは、学習回数が少ないと考えられる。
# その場合、iterの値を大きくして、再度実行したほうが良い

# ★ここで学習の実施。事前準備したword_listを使う
model = word2vec.Word2Vec(word_list, size=size_setting,min_count=min_count_setting,window=window_setting,iter=iter_setting)

# 学習の設定値ごとに、ファイル名を書いて保存しておく
# ※やりすぎてゴミが増えるとディスク容量を圧迫する点は注意
model_sava_file_name = "W2V_V4_"+str(size_setting)+"_"+str(min_count_setting)+"_"+str(window_setting)+"_"+str(iter_setting)+"_model.pickle"
with open('drive/My Drive/Sangokusi/'+model_sava_file_name, 'wb') as f:
    pickle.dump(model, f)

この機械学習の実行は、設定値によっては、
かなり時間がかかるので、司馬懿のようにじっと待つべし
(上記例だと1時間弱くらい)

司馬懿「・・・」
司馬懿「・・・・・」
司馬懿「・・・・・・・」
部下 「孔明に 女物の衣服 まで贈られて、まだ攻めないのですか!?」
司馬懿「・・・・・」
司馬懿「(なぜワシの女装癖がバレたのだろう?孔明恐るべし・・・)」

さあ、司馬懿が女装している間に学習が完了したので
魏延をベクトルにした結果を見てみよう!

結果の確認(魏延編)
#一つ一つの単語はN次元のベクトルとして表現されている。
#もし、各要素の値が0に近いものばかりの場合は学習繰り返し回数の不足
print(model.__dict__['wv']['魏延'])
魏延のベクトル化表示
[ -3.29209     -0.8619167    2.548894    -6.2769666    4.820325
  -4.3188534    4.892581     3.0584764   -1.941075     7.202591
   2.825382     2.6815546   -1.5141411    1.2163684    7.8137026
   5.9709163    4.2522264   -3.750429    -1.2866642   -5.4226913
  -1.1429474    0.74476415   4.2300115    3.1387594    0.60046256
   0.8668483   -5.342602    -1.0369713    1.8684605   -0.29890215
  -6.667386    -1.8291212   -3.2661974   -0.50744665   1.6939703
   1.3792468   -0.2192511  -10.185009    -2.773828     2.1797962
  -2.4290617    1.8001399    5.592892    -9.066986    -2.3023245
   2.6630638   -3.0628533   -4.383273     8.999806    -4.0579867 ]

このように、各武将を
「50次元のベクトル」に変換することが出来た!
魏延の数字をよーく見ると、
「反骨の相」が出ているのが分かる。
(うそ。どんな風向きも東南に変える気象予報士様なら分かるのかもしれない)

じゃあ、どうやってこの数字の羅列を使うんだよ!?

全部の「単語(武将)」が数値化されているということは、
その数値同士の距離を比較したり、計算したりが出来るということ。

治世の能臣、乱世の奸雄に比肩する英雄は誰?

曹操「天下の英雄、と言えば誰であろうか?」
劉備「河北の袁紹、淮南の袁術、呉の孫策、荊州の劉表、益州の劉璋・・・」
曹操「そいつらは英雄とは言えないな」
劉備「じゃあ誰だというのですか?」
曹操「オレとオマエだ(ハート)
劉備「(ポッ)

この後カミナリに邪魔されなければ、
梅園で彼らのラヴロマンスが始まりそうなところではあるが、
ここで無情にも「曹操」に類似する単語を、
出来たばかりの機械学習結果を使って調べてみよう。

三国志演戯中の全単語(名詞と動詞)がベクトルになっているため、
指定した単語ベクトルに最も近い(コサイン類似度が大きい)単語を
調べる関数を実行する。果たして「劉備」は出てくるのか・・・?

曹操と似た単語を調べる
# 関数most_similarを使って「曹操」の類似単語を調べる 
print("--曹操--")
ret = model.wv.most_similar(positive=['曹操']) 
for item in ret:
    print(str(item[0]), str(item[1]))
曹操と​似ている単語の結果表示
袁紹 0.7524725794792175
賈詡 0.6547679901123047
呂布 0.6256376504898071
袁譚 0.6161890029907227
袁術 0.6136394739151001
陳宮 0.5922695994377136
董卓 0.5671449303627014
公孫瓚 0.5582242608070374
魏王 0.5288677215576172
劉表 0.5284026861190796

後ろにある数字は、いわば「似ている度合い」を表す。
結果は、「袁紹」がぶっちぎりの一位であった。
(こっそり「魏王」が入っているのが良い。
 「武将名」に限定せず一般名詞系も含むため、
 魏王などの結果が出てくる場合もある)

残念!曹操様は劉備よりも、
「袁紹」「呂布」「袁譚」「袁術」「董卓」「公孫瓚」「劉表」
のほうがライバル的存在と認識されてしまった!!
劉備とは、属性が違う、逆方向的な印象であるため、
しょうがないのかもしれない。

曹操だけだとわかりにくいと思うので、
以下いくつか結果を出してみよう。

似ている単語の結果だけ表示
--関羽--
劉備 0.6806955337524414
趙雲 0.6609582901000977
孫乾 0.6063336730003357
張飛 0.5494095683097839
裴元紹 0.4910241365432739
呂布 0.4753880798816681
馬超 0.4582022428512573
張苞 0.4560770094394684
厳顔 0.4506300389766693
廖化 0.4462217390537262

--張飛--
劉備 0.602747917175293
趙雲 0.5853415727615356
王忠 0.5635724067687988
関羽 0.5494095683097839
 0.5104227662086487
魏延 0.49119892716407776
黄忠 0.4832606613636017
馬岱 0.44260838627815247
呂布 0.4418014883995056
厳顔 0.4356195330619812

--劉備--
関羽 0.6806954145431519
張飛 0.602747917175293
孫乾 0.5762519836425781
劉表 0.5263820886611938
毎年 0.5026741027832031
劉焉 0.49397462606430054
張松 0.4727109968662262
劉琦 0.4714389443397522
趙雲 0.46208474040031433
趙範 0.46007320284843445

--魏延--
馬岱 0.6856343746185303
姜維 0.6569146513938904
王平 0.6510531306266785
張嶷 0.647894561290741
趙雲 0.6387921571731567
黄忠 0.6179856657981873
劉封 0.5881460905075073
厳顔 0.5788888931274414
関興 0.571064293384552
張翼 0.5704936385154724

--周瑜--
魯粛 0.734963059425354
孫権 0.6405857801437378
陸遜 0.5940338373184204
諸葛亮 0.5786603689193726
呂蒙 0.5721293091773987
黄蓋 0.5462436676025391
 0.5148110389709473
凌統 0.5106927752494812
 0.5096937417984009
諸葛瑾 0.49628153443336487

--諸葛亮--
姜維 0.612575352191925
司馬懿 0.6117764711380005
陸遜 0.5794469714164734
周瑜 0.5786603689193726
魏延 0.5581164360046387
馬謖 0.5399507284164429
龐統 0.5133271217346191
魯粛 0.5112813711166382
王平 0.49135640263557434
 0.48400527238845825

--諸葛瑾--
孫権 0.5880517959594727
ほる 0.5455287098884583
 0.5328208208084106
呉夫人 0.5227155685424805
魯粛 0.5116612315177917
沛国譙 0.5020761489868164
蔡瑁 0.49696531891822815
呂範 0.4963788688182831
周瑜 0.49628153443336487
郭嘉 0.463478684425354

--呂布--
陳宮 0.6613448262214661
曹操 0.6256376504898071
董卓 0.6036599278450012
丁原 0.6022049784660339
袁紹 0.5389851331710815
高順 0.49822214245796204
関羽 0.4753880500793457
無二 0.46212291717529297
袁術 0.4614390730857849
劉備 0.4575232267379761

--荀彧--
賈詡 0.6549777388572693
韓馥 0.5326628088951111
曹操 0.48662832379341125
荀攸 0.4653368294239044
孫翊 0.46216070652008057
一員 0.46210145950317383
審配 0.4613281190395355
郭嘉 0.46035730838775635
一因 0.4602801501750946
何進 0.45899227261543274

--呂蒙--
陸遜 0.6509128212928772
周瑜 0.5721293091773987
呉夫人 0.540614664554596
徐盛 0.5363913774490356
孫権 0.531932532787323
重態 0.519347608089447
黄蓋 0.5045372843742371
 0.4957953989505768
江頭 0.48872271180152893
魯粛 0.4872535765171051

いかがでしょうか?

まれに、よくわからん単語が混ざっている箇所があるが、
一般名詞/動詞含めた状態で見ても、
だいたい「武将名」同士で似ていると判断されている。

さらに、その出てくる武将名についても
ある程度納得感のある人が挙がっているような気がする。

よくわからん単語や、よくわからん武将が上位に来る場合は、
その単語/武将が出現頻度が低く、それらが誤ったベクトルで
登録されていると考えられる。

備考/考察:
「諸葛瑾(子瑜)」は単語の切り方がイマイチで少し精度が低い印象。
武将名は「自然言語処理編」でかなり調整したものの、
どうしても一部完璧にはならない部分が残っている。
「諸葛瑾」は「瑾」などと、一文字で表現される場合が多く、
実は前回のコードでは、一文字名前だけの場合をフルネーム化、
という処理はかなりレアケースなので実施していなかった。
「備」⇒「劉備」みたいな処理で、一般的に適切な変換ではない。
ただ「瑾」は、ほぼほぼ「諸葛瑾」なので、
彼だけ特殊処理を入れても良かったかもしれない。

ここでのポイントは「似ている」の基準
何を以て「似ている」かは、様々な尺度があるため、
その総合得点で評価されているということが重要。
例えば、以下。

  • 君主同士、軍師同士など「役割」が似ている。
  • 劉備配下など、「陣営」が似ている。
  • 別陣営であっても、良く対比される「ライバル」として似ている。
  • 同じ時代、同じ戦など、「活躍した時」が似ている。
  • 「仲良し」な人で似ている。(魏延と馬岱など

人間に聞いたとしても、
例えば、「諸葛亮」に「近い属性の人は?」と聞かれると、
司馬懿であったり、姜維、周瑜、龐統、または劉備かも?
などなど様々な答えがかえってくるでしょう。それと同じ理屈。

龐統 「諸葛亮に似ている人の6位に・・・」
龐統 「山登りのスペシャリスト が出ていますよ?」
諸葛亮「だまらっしゃい!!

この「似ている度」を基本として、
次はその応用編。三国志ファン感涙の、
武将同士のベクトル演算で、
魏呉蜀各武将のライバル関係が明らかに!!

孫権「曹操には張遼がいる、わしには甘寧がいる」

孫権 「曹操には張遼がいる、わしには甘寧がいる
孫権 「ということは、張遼と曹操の関係が、甘寧と孫権の関係と同じだから」
孫権 「張遼 マイナス 曹操 = 甘寧 マイナス 孫権 、になる!」
陸遜 「(またお酒が入りすぎているようだな・・・)
机の角「(ガクガクブルブル・・・)」

無実なのに切って捨てられた机の角よ、ご安心めされよ。
今回作ったえーあいは、この酒乱江東の碧眼児孫権の言うような
武将同士の計算が出来るのだ!

既に武将がベクトル情報になっているため、
その足し算引き算をした結果ベクトルに対し、
最も似ている単語(武将)を見る、というシンプルな仕組みだ。

左辺と右辺を整理すると以下のようになる。

「張遼」 = 「甘寧」+「曹操」-「孫権」

さあ、この右辺の計算が本当に「張遼」になるのか試してみよう!!

ついでに、
劉備にとっての(孫権に対する)甘寧は誰だよ?と、
劉備にとっての(曹操に対する)張遼は誰だよ?も、
一緒にやってみる。

曹操と似た単語を調べる
# 「甘寧」+「曹操」-「孫権」
print(model.wv.most_similar(positive=['甘寧','曹操'], negative=['孫権'],topn=10))
# 「甘寧」+「劉備」-「孫権」
print(model.wv.most_similar(positive=['甘寧','劉備'], negative=['孫権'],topn=10))
# 「張遼」+「劉備」-「曹操」
print(model.wv.most_similar(positive=['張遼','劉備'], negative=['曹操'],topn=10))
武将演算の出力結果
# 「甘寧」+「曹操」-「孫権」の出力結果
[('張遼', 0.5975690484046936), ('双方', 0.5424929261207581), ('車冑', 0.5172145962715149), ('秦琪', 0.5103437900543213), ('寄手', 0.5010682344436646), ('呂布', 0.4903222918510437), ('ひょう', 0.480598509311676), ('誰か', 0.47944292426109314), ('楊奉', 0.4771113991737366), ('紀霊', 0.4762181043624878)]

# 「甘寧」+「劉備」-「孫権」の出力結果
[('張飛', 0.6009012460708618), ('車冑', 0.5231213569641113), ('誰か', 0.5017432570457458), ('王忠', 0.4733934998512268), ('関羽', 0.46644333004951477), ('下がる', 0.4650993049144745), ('張遼', 0.46208637952804565), ('朋友', 0.43470197916030884), ('右翼', 0.4339367747306824), ('山下', 0.42906704545021057)]

# 「張遼」+「劉備」-「曹操」の出力結果
[('張飛', 0.5989757776260376), ('関羽', 0.589595377445221), ('孫乾', 0.5613676309585571), ('趙雲', 0.5599579215049744), ('関平', 0.5381424427032471), ('卒', 0.49242568016052246), ('はあと', 0.46057409048080444), ('呉懿', 0.44697314500808716), ('夫人', 0.4372628629207611), ('ふたり', 0.4333947002887726)]

「車冑」って誰だっけ・・・?
2位以降がイマイチな点はあるが、
結構な大差をつけて、「ワシの甘寧」の1位は、
曹操⇒「張遼」と劉備⇒「張飛」が
それぞれ出てくることになった。(なお、関羽は5位)
劉備にとっての張遼、については、張飛/関羽が接戦であった。

張遼の二位以下にもう少し魏の武力90台を出して欲しかったが
精度の問題か、最初は敵だった属性が効いているのか

孫権サマの「ワシの甘寧」発言は、
三国志演戯を機械学習した結果からも妥当性がある
ということが判明した。

以下いくつか他の結果も出力してみる。(コードは省略)

武将演算の様々な結果
# 「曹操」にとって、劉備の諸葛亮にあたる人は誰?(曹操+諸葛亮-劉備)
[('司馬懿', 0.6807084083557129), ('曹真', 0.6253277659416199), ('張郃', 0.5542588829994202), ('曹遵', 0.5308239459991455), ('審配', 0.5271061062812805), ('兵法', 0.5043819546699524), ('周瑜', 0.4940730035305023), ('陸遜', 0.4794454574584961), ('賈詡', 0.47504767775535583), ('郭淮', 0.4681739807128906)]

# 無理やり最強合体で呂布と諸葛亮を足すと?(呂布+諸葛亮)
[('曹操', 0.6402270197868347), ('劉備', 0.6272321343421936), ('陳宮', 0.6206848621368408), ('関羽', 0.5986325740814209), ('法正', 0.5725218653678894), ('厳顔', 0.5579944252967834), ('賈詡', 0.5574716329574585), ('司馬懿', 0.5248735547065735), ('周瑜', 0.5034980177879333), ('馬超', 0.4987131357192993)]

# 「曹操」にとって、劉備の張飛にあたる人は誰?(曹操+張飛-劉備)
[('許褚', 0.6215435266494751), ('秦琪', 0.6193733811378479), ('董卓', 0.5844904780387878), ('呂布', 0.5498712062835693), ('寄手', 0.5366292595863342), ('双方', 0.5207207798957825), ('陳宮', 0.5160595178604126), ('張遼', 0.5159090757369995), ('韓遂', 0.5104340314865112), ('ひょう', 0.5089328289031982)]

# 「曹操」にとって、劉備の関羽にあたる人は誰?(曹操+関羽-劉備)
[('袁紹', 0.5561782121658325), ('張遼', 0.5554732084274292), ('夏侯惇', 0.5427525043487549), ('呂布', 0.535315215587616), ('楽進', 0.5261821746826172), ('丁原', 0.5100050568580627), ('侯成', 0.48861902952194214), ('許褚', 0.4884832799434662), ('賈詡', 0.47699469327926636), ('于禁', 0.4732886254787445)]

# 「劉備」にとって、孫権の魯粛にあたる人は誰?(魯粛+劉備-孫権)
[('司馬徽', 0.5429366230964661), ('徐庶', 0.5424918532371521), ('張飛', 0.5137515664100647), ('孫乾', 0.4811612665653229), ('小才', 0.4786354899406433), ('連れ', 0.4577670395374298), ('毎年', 0.4426392614841461), ('筆', 0.44031772017478943), ('鄭重', 0.43619340658187866), ('後ろ姿', 0.4288194179534912)]

いかがでしょうか?
三国志ファンなら半日くらい遊べてしまいそうな危険なオモチャ

武将3名分の50次元ベクトルを足し算引き算しているため、
単純な「似ている」に比べて、参照する対象が多くなっており、
どうしてもゴミが入りやすくなる傾向はあるが、
TOP3には、なるほどな人が出ているような気はする。

おそらく、孫権の甘寧に対するこのセリフを
小説上本当に成り立っているのか分析したのは
世界初レベルの無駄の無双乱舞的なプロジェクト。

こうして三国志の世界にまた一つ新たなトリビアが生まれた。
「曹操には張遼がいる、孫権には甘寧がいる」
は、三国志演義の小説をAIが読んでも同じ結果を導けた!

次項ではいよいよ、このモデルを使って
「武力」「知力」の数値化を考えたいが、その前に
実は、この物語の裏には、
無双ゲージがMAXになるまで
雑兵を1000人くらい切るような努力があったのだ。
その裏話も語らせていただこう!

三日会わざれば刮目して見よ(機械学習のチューニング)

魯粛「お、呂蒙?しばらく見ない間にずいぶんと変わったな!」
魯粛「まさに、呉下の阿蒙にあらず」
呂蒙「美容院に行ったの気付いてくれるのは魯粛殿だけです(ハート)」
魯粛「(ポッ)」
魯粛「大都督あげちゃうっ!」

三日会わざれば刮目して見よ
~~意味~~
三日も経つと人は成長しているものだ、
三日も会わなければ評価を改めてしっかりみなさい。
逆に言えば、人は三日でも大きく成長出来るものだ。
~~~~~~

呂蒙は三日ごとに美容院でイメチェンしていたわけだが、
機械学習のモデルはまさに呂蒙のごとく。
最初に魏延をベクトル化した際のコードの設定値の部分。
この設定をちょっと変更すると、機械学習結果は大きく異なる

設定値の一例
#機械学習におけるパラメータの設定値
#ここの値の調整が精度に直結している。
size_setting = 50
min_count_setting = 5
window_setting = 6
iter_setting = 4500

3つ値が違えば刮目して見よ! ということ。

本稿では主に上記の4つを様々に変えて試していた。
(Word2Vecの学習時のパラメータは上記以外にもいろいろある)

30回以上は様々なモデルを作っていたと思う。
ゲームしながらColaboratory放置を何十時間も。

何度もやったから「狙った結果」が出たんでしょ、
という人もいるかもしれないが、
武将名だけで数百単語、一般名詞/動詞も含めると約1万の単語がある中で、
複数の演算結果を同時に「狙った結果」にするのは不可能に近い。
自然言語処理編で武将名識別&名寄せを丁寧に行い、
かつ、パラメータを試行錯誤することで、
それっぽい機械学習モデル(ベクトル化された単語データ)を
やっと得ることが出来たのだ。
INPUTが自然言語で書かれた小説のテキスト一つで、
パラメータ調整だけでこれくらいの結果まで行く、と見て欲しい。
なお、より精度を上げるには、まずは複数の作者の小説を入れてみたい。
武将によっては登場回数が少なすぎて、吉川英治版のみでは評価が難しいため。

パラメータ調整時の面白いポイントをひとつ、例としてご紹介する。
次元数」について、今回50次元のベクトルとしたが、
一見、次元数を多くすればするほど詳細な分析になりそうである。
しかし、100次元などと多くしても良い結果は出なかった。
最低でも20次元くらいは必要で、30~60次元あたりが、
妥当な値に思えた。データ量(テキストの分量)に見合う程度の個数、
データ軸を設定しなければいけない、ということ。
多すぎても少なすぎても上手くいかない。

こうした雑兵1000人組手のような地道な努力によって、
ある程度納得感のある機械学習モデルを得ることが出来た!
いよいよコレを使って、武力知力の数値化を考えてみよう。

人中の呂布、馬中の赤兎(最強の武を求める)

張飛「燕人張飛ここにあり!呂布め、いくぞ!」
呂布「来い!飛将呂布奉先、おまえのようなやつには負けん!」
関羽「助太刀するぞっ!」
呂布「ム、援軍か、これはまずいな。一度退くか・・・。」
劉備「あのゴキブリの触覚のようなのを付けているやつが呂布だ!」
呂布「3人まとめてやっつけてやる!!(怒)

さて最強の武(の値)を求める
にはどうしたらよいだろうか?

作成した機械学習モデルに、
「三国志の世界が正しく投影されている」とすれば
そのモデルの中のどれかの値や、何かの計算結果を見れば、
「武力」「知力」に相当する値が得られるのではないか?
というのが本稿最大のアイデアである。

まず最初にやってみた実験が、
50次元のベクトルになっているのだから、
そもそも、その中のどれか一つの値が、「武力」「知力」や、
「蜀への所属度」「忠誠心」などの、
なんらかの分かりやすい値になっているかもしれない、
という内容の確認だ。

モデル全体の中から、三国志の武将名だけのデータを取り出す。
(一般名詞や動詞のデータは除外する)
ついでに、その武将名が何回登場しているか?のデータも付与して眺めてみよう。

武将のベクトルだけ取り出す
import pickle
model_sava_file_name = ”保存したモデルファイル名”
with open('drive/My Drive/Sangokusi/'+model_sava_file_name, 'rb') as f:
    model = pickle.load(f)

#作成したモデルのなかに登録されている単語数の確認
print(len(model.wv.vocab.keys()))

#作成したモデルのなかで、「武将」のデータと、そのベクトルを全て抜き出してリストを作る
Busyou_data_list = []
for word, vocab_obj in model.wv.vocab.items():
    #jinbutu_word_listは自然言語処理編で作った全武将の単語リスト
    #その単語が、「武将名」であれば、リストに追加する処理を行う。
    if word in jinbutu_word_list:
        #print(word,":",vocab_obj.count)

        #分散表現(ベクトル情報)を抜き出す
        busyou_vector = model.__dict__['wv'][word]
        #武将名、出現回数、ベクトル情報、を格納したリストにする
        Busyou_data_list.append([word, vocab_obj.count]+ busyou_vector.tolist())

#「出現回数」で並べ替え
Busyou_data_list = sorted(Busyou_data_list, key=lambda x: -x[1])

#全体として何人の武将データが作れたか表示
print(len(Busyou_data_list))
#サンプルとして、最初のデータを表示
print(Busyou_data_list[0])
武将のベクトルだけ作った結果
9653
421
['曹操', 2843, 3.834890365600586, 0.6499840617179871, 1.3674521446228027, -0.29894790053367615, -0.9264350533485413, -3.5290303230285645, -0.2177853137254715, -2.4688072204589844, -2.185100555419922, -0.9018422365188599, -2.48173451423645, -2.8471529483795166, 1.0149024724960327, 0.3895401358604431, -3.4980974197387695, 0.4531531035900116, 2.1340160369873047, 1.5887707471847534, 1.6122041940689087, -2.1009063720703125, -0.36042818427085876, -1.551460862159729, 1.1327486038208008, -0.8541130423545837, 1.072536826133728, -0.08540906757116318, -2.70839524269104, -3.965020179748535, -0.5980285406112671, -4.0637431144714355, -3.365858793258667, 0.4908220171928406, 1.3250325918197632, 0.2127988189458847, 2.9902565479278564, -1.4399590492248535, -2.3241443634033203, -2.2173967361450195, 0.1169036328792572, -2.4554295539855957, -0.05750872567296028, 0.31866252422332764, 0.3000071942806244, 0.5707933902740479, -2.4878640174865723, 1.7815735340118408, 1.4298678636550903, -3.451324939727783, 0.8778221607208252, -1.5637134313583374]

上記の結果の意味としては、
元々9653単語が機械学習モデル内に登録されており、
武将名のデータに限定すると421名分のデータ。
(※min_count_setting = 5であるので、5回以上出現した武将が421名)
うち「曹操」が最も出現頻度が高い単語で、小説中に2843回出現した、
ということを意味する。
これで421名×(50次元 + 出現回数)のデータが作れた。

このまま愚直に、
各次元ごとに、値の高い武将~低い武将を見てみよう!
412名全部を見るとエライことになるため、
出現回数の多い50名分のデータに絞って実施する。

魏延のベクトル表示結果を思い出してほしい。

魏延のベクトル化表示
[ -3.29209     -0.8619167    2.548894    -6.2769666    4.820325
  -4.3188534    4.892581     3.0584764   -1.941075     7.202591
   2.825382     2.6815546   -1.5141411    1.2163684    7.8137026
   5.9709163    4.2522264   -3.750429    -1.2866642   -5.4226913
  -1.1429474    0.74476415   4.2300115    3.1387594    0.60046256
   0.8668483   -5.342602    -1.0369713    1.8684605   -0.29890215
  -6.667386    -1.8291212   -3.2661974   -0.50744665   1.6939703
   1.3792468   -0.2192511  -10.185009    -2.773828     2.1797962
  -2.4290617    1.8001399    5.592892    -9.066986    -2.3023245
   2.6630638   -3.0628533   -4.383273     8.999806    -4.0579867 ]

この、1番目の数字が高い順、2番目の数字が高い順、・・・・
と、並べていったものを出そう、ということ。

全次元それぞれに対し、値の高い武将~低い武将を表示
#マイナー武将が入ってくるとカオスになるので、
#登場頻度が高い武将だけに絞る
tmp_Busyou_data_list = Busyou_data_list[0:50]

#各ベクトルごとに、その50名をソートして表示する
#つまり、各ベクトルごとのTOP~ビリまでの順番で出る
for a in range(1,52):
    print(a)
    tmp_Busyou_data_list = sorted(tmp_Busyou_data_list, key=lambda x: -x[a])
    for Busyou_data in tmp_Busyou_data_list:
        print(Busyou_data[0]+", " , end="")
    print("")

以下が、出現回数の最も多い50人に対して、
各次元ごとの、1位~50位(ビリ)までの全結果。
膨大だが、「ベクトル化」の意味を考える上でも興味深いために全部掲載する。
なお、一番上の「1」は、出現頻度順であり、
これだけはベクトルの数値ではない。
出現頻度も結構意外な順番

各次元ごとの1位~50位(ビリ)までの全結果
1
曹操, 劉備, 諸葛亮, 関羽, 張飛, 呂布, 袁紹, 周瑜, 孫権, 趙雲, 司馬懿, 董卓, 孫策, 魏延, 馬超, 魯粛, 黄忠, 劉表, 袁術, 張郃, 孫堅, 張遼, 貂蝉, 孟獲, 姜維, 徐晃, 陳宮, 曹丕, 徐庶, 曹仁, 許褚, 陸遜, 龐統, 董承, 龐徳, 呂蒙, 甘寧, 馬岱, 夏侯惇, 曹洪, 曹真, 王允, 劉璋, 孟達, 関平, 関興, 孫乾, 夏侯淵, 王平, 太史慈, 
2
曹洪, 袁術, 袁紹, 呂布, 董卓, 馬超, 孟達, 董承, 張郃, 劉璋, 夏侯惇, 張遼, 夏侯淵, 曹仁, 陳宮, 徐晃, 王允, 孫策, 貂蝉, 曹丕, 劉表, 曹操, 司馬懿, 曹真, 姜維, 許褚, 王平, 孫堅, 劉備, 馬岱, 関羽, 甘寧, 黄忠, 孫乾, 孟獲, 趙雲, 龐統, 魯粛, 諸葛亮, 龐徳, 張飛, 関平, 呂蒙, 太史慈, 魏延, 関興, 孫権, 徐庶, 陸遜, 周瑜, 
3
魯粛, 曹真, 司馬懿, 陸遜, 董卓, 王平, 甘寧, 劉璋, 孫権, 関興, 張飛, 許褚, 周瑜, 徐晃, 董承, 諸葛亮, 呂蒙, 劉備, 曹操, 馬岱, 貂蝉, 袁術, 呂布, 張遼, 張郃, 孫乾, 関羽, 龐統, 陳宮, 魏延, 趙雲, 姜維, 袁紹, 孫策, 馬超, 曹洪, 夏侯惇, 黄忠, 夏侯淵, 孫堅, 劉表, 徐庶, 王允, 龐徳, 孟獲, 曹仁, 曹丕, 関平, 孟達, 太史慈, 
4
劉璋, 袁紹, 張郃, 孟達, 孫乾, 馬超, 曹洪, 姜維, 曹仁, 関羽, 夏侯惇, 魏延, 呂蒙, 孫権, 趙雲, 呂布, 龐統, 司馬懿, 諸葛亮, 曹操, 王平, 夏侯淵, 孫堅, 劉備, 孟獲, 曹丕, 馬岱, 甘寧, 董承, 劉表, 陸遜, 黄忠, 関興, 陳宮, 周瑜, 徐庶, 張飛, 龐徳, 徐晃, 曹真, 董卓, 関平, 袁術, 許褚, 魯粛, 孫策, 貂蝉, 張遼, 王允, 太史慈, 
5
劉璋, 呂布, 王允, 馬超, 袁術, 孫権, 夏侯惇, 孟獲, 龐徳, 孫堅, 董卓, 曹洪, 董承, 貂蝉, 関羽, 陳宮, 曹操, 徐晃, 甘寧, 張遼, 許褚, 趙雲, 劉表, 曹仁, 曹丕, 孫策, 張飛, 袁紹, 王平, 太史慈, 劉備, 孫乾, 孟達, 呂蒙, 関平, 夏侯淵, 馬岱, 魏延, 周瑜, 姜維, 陸遜, 魯粛, 関興, 諸葛亮, 龐統, 司馬懿, 張郃, 徐庶, 曹真, 黄忠, 
6
関興, 孫権, 曹洪, 趙雲, 曹仁, 夏侯淵, 馬岱, 魏延, 甘寧, 許褚, 周瑜, 関羽, 呂布, 陸遜, 太史慈, 王平, 徐晃, 張飛, 袁術, 張遼, 龐統, 劉表, 関平, 孟達, 黄忠, 馬超, 曹真, 諸葛亮, 劉備, 呂蒙, 夏侯惇, 孟獲, 陳宮, 張郃, 姜維, 魯粛, 徐庶, 孫堅, 曹操, 孫乾, 袁紹, 司馬懿, 龐徳, 孫策, 董承, 董卓, 劉璋, 貂蝉, 王允, 曹丕, 
7
王允, 貂蝉, 董承, 魯粛, 曹丕, 太史慈, 諸葛亮, 王平, 曹真, 姜維, 劉璋, 呂蒙, 董卓, 劉備, 龐統, 周瑜, 孟獲, 徐庶, 袁術, 陳宮, 馬岱, 趙雲, 曹操, 馬超, 夏侯淵, 司馬懿, 孫権, 孫乾, 魏延, 劉表, 関羽, 龐徳, 徐晃, 孟達, 張郃, 呂布, 張飛, 夏侯惇, 黄忠, 曹洪, 張遼, 陸遜, 関興, 関平, 許褚, 甘寧, 孫堅, 孫策, 曹仁, 袁紹, 
8
貂蝉, 王允, 黄忠, 曹仁, 龐徳, 曹丕, 龐統, 董承, 太史慈, 魏延, 周瑜, 孫権, 魯粛, 張遼, 司馬懿, 諸葛亮, 趙雲, 徐晃, 張郃, 陸遜, 甘寧, 曹洪, 関平, 孫策, 張飛, 夏侯惇, 曹真, 関羽, 孟獲, 呂蒙, 姜維, 馬岱, 孟達, 徐庶, 董卓, 曹操, 夏侯淵, 許褚, 袁紹, 孫乾, 馬超, 劉備, 関興, 孫堅, 呂布, 陳宮, 袁術, 王平, 劉璋, 劉表, 
9
王平, 姜維, 董承, 龐徳, 曹真, 魏延, 徐晃, 張郃, 夏侯淵, 関羽, 夏侯惇, 馬超, 司馬懿, 黄忠, 陸遜, 許褚, 諸葛亮, 孫権, 曹洪, 関興, 孟達, 劉表, 王允, 劉備, 関平, 劉璋, 趙雲, 呂布, 呂蒙, 孫策, 馬岱, 袁紹, 曹操, 魯粛, 徐庶, 孫堅, 龐統, 曹丕, 陳宮, 張飛, 袁術, 太史慈, 張遼, 曹仁, 貂蝉, 周瑜, 孫乾, 孟獲, 甘寧, 董卓, 
10
徐庶, 夏侯惇, 貂蝉, 関興, 孫策, 曹仁, 張飛, 孫乾, 張遼, 劉備, 孟獲, 関羽, 趙雲, 司馬懿, 陸遜, 太史慈, 王允, 馬岱, 呂布, 諸葛亮, 張郃, 甘寧, 黄忠, 魯粛, 董卓, 姜維, 周瑜, 呂蒙, 龐統, 王平, 魏延, 曹操, 馬超, 孫堅, 袁紹, 曹丕, 関平, 許褚, 袁術, 孫権, 曹洪, 劉璋, 陳宮, 徐晃, 董承, 曹真, 劉表, 夏侯淵, 孟達, 龐徳, 
11
孟達, 馬岱, 魏延, 徐庶, 王平, 張郃, 張飛, 黄忠, 陳宮, 夏侯惇, 張遼, 関平, 姜維, 関興, 曹真, 孟獲, 徐晃, 曹仁, 劉璋, 諸葛亮, 馬超, 龐統, 孫乾, 夏侯淵, 貂蝉, 劉備, 許褚, 龐徳, 司馬懿, 趙雲, 曹操, 関羽, 呂布, 董卓, 魯粛, 王允, 董承, 曹洪, 曹丕, 周瑜, 太史慈, 劉表, 袁紹, 袁術, 孫権, 甘寧, 陸遜, 孫策, 呂蒙, 孫堅, 
12
関平, 関興, 張飛, 孫乾, 姜維, 孟達, 龐統, 魏延, 劉備, 董承, 関羽, 曹洪, 呂蒙, 劉璋, 曹丕, 諸葛亮, 馬岱, 孫策, 孟獲, 趙雲, 黄忠, 張遼, 許褚, 馬超, 王平, 陳宮, 周瑜, 魯粛, 甘寧, 王允, 夏侯淵, 陸遜, 太史慈, 曹操, 曹真, 曹仁, 呂布, 司馬懿, 董卓, 孫権, 袁術, 貂蝉, 徐庶, 袁紹, 張郃, 孫堅, 徐晃, 劉表, 夏侯惇, 龐徳, 
13
関平, 魯粛, 姜維, 曹仁, 曹洪, 呂蒙, 徐庶, 孫策, 徐晃, 黄忠, 孫乾, 魏延, 王平, 趙雲, 許褚, 陸遜, 張郃, 関興, 龐統, 張飛, 夏侯淵, 周瑜, 諸葛亮, 龐徳, 劉備, 孫権, 張遼, 夏侯惇, 陳宮, 太史慈, 甘寧, 馬岱, 孫堅, 劉璋, 関羽, 曹操, 司馬懿, 孟達, 呂布, 曹丕, 馬超, 董承, 王允, 貂蝉, 曹真, 董卓, 袁紹, 孟獲, 袁術, 劉表, 
14
魯粛, 陳宮, 太史慈, 孫策, 曹仁, 袁紹, 周瑜, 呂布, 孫権, 孫乾, 孫堅, 甘寧, 徐晃, 孟獲, 張飛, 関羽, 関平, 貂蝉, 曹洪, 王允, 張遼, 呂蒙, 許褚, 徐庶, 夏侯惇, 龐徳, 劉備, 劉表, 陸遜, 龐統, 董卓, 馬超, 袁術, 曹操, 王平, 曹丕, 黄忠, 曹真, 趙雲, 夏侯淵, 馬岱, 諸葛亮, 魏延, 董承, 張郃, 姜維, 関興, 孟達, 司馬懿, 劉璋, 
15
曹真, 王平, 董卓, 馬岱, 孫乾, 袁紹, 孟獲, 曹洪, 関平, 王允, 呂蒙, 陳宮, 陸遜, 黄忠, 曹仁, 魏延, 張遼, 張飛, 関羽, 曹操, 孫策, 司馬懿, 袁術, 曹丕, 劉備, 劉璋, 夏侯惇, 呂布, 張郃, 徐晃, 夏侯淵, 諸葛亮, 姜維, 魯粛, 孫権, 孫堅, 趙雲, 周瑜, 許褚, 徐庶, 龐徳, 馬超, 劉表, 孟達, 関興, 太史慈, 龐統, 甘寧, 貂蝉, 董承, 
16
魏延, 王平, 姜維, 甘寧, 夏侯惇, 馬岱, 趙雲, 陸遜, 関興, 曹真, 孟獲, 呂蒙, 龐統, 黄忠, 張郃, 孫乾, 孟達, 徐晃, 関羽, 張遼, 曹洪, 諸葛亮, 孫堅, 張飛, 劉璋, 劉備, 関平, 司馬懿, 孫策, 曹丕, 曹仁, 孫権, 馬超, 董承, 魯粛, 周瑜, 徐庶, 曹操, 袁紹, 許褚, 太史慈, 袁術, 龐徳, 夏侯淵, 劉表, 呂布, 王允, 董卓, 貂蝉, 陳宮, 
17
龐統, 許褚, 魏延, 袁術, 司馬懿, 陸遜, 呂蒙, 馬岱, 曹真, 張郃, 呂布, 貂蝉, 董卓, 黄忠, 劉表, 夏侯淵, 太史慈, 諸葛亮, 甘寧, 徐晃, 孟達, 陳宮, 趙雲, 馬超, 曹操, 孟獲, 姜維, 孫堅, 王平, 孫策, 張飛, 王允, 曹洪, 張遼, 徐庶, 魯粛, 関興, 周瑜, 曹仁, 龐徳, 劉備, 関羽, 董承, 袁紹, 夏侯惇, 曹丕, 孫権, 関平, 劉璋, 孫乾, 
18
劉表, 陸遜, 劉璋, 孟獲, 魯粛, 関平, 孫乾, 龐統, 袁術, 周瑜, 関興, 魏延, 孫権, 劉備, 袁紹, 張飛, 太史慈, 曹丕, 徐庶, 諸葛亮, 孫策, 董承, 曹操, 呂蒙, 趙雲, 徐晃, 曹仁, 陳宮, 孫堅, 王允, 呂布, 董卓, 張郃, 黄忠, 夏侯淵, 姜維, 関羽, 司馬懿, 夏侯惇, 馬岱, 王平, 許褚, 張遼, 貂蝉, 曹真, 馬超, 曹洪, 甘寧, 龐徳, 孟達, 
19
孫堅, 張遼, 孫策, 甘寧, 呂布, 太史慈, 黄忠, 夏侯淵, 孫乾, 袁術, 徐庶, 袁紹, 董承, 曹洪, 許褚, 周瑜, 陸遜, 董卓, 張飛, 関羽, 曹操, 夏侯惇, 関平, 龐統, 趙雲, 劉備, 呂蒙, 曹真, 孫権, 魯粛, 馬岱, 曹仁, 孟達, 曹丕, 馬超, 貂蝉, 龐徳, 王允, 劉表, 関興, 徐晃, 司馬懿, 魏延, 陳宮, 張郃, 孟獲, 姜維, 諸葛亮, 王平, 劉璋, 
20
曹洪, 王允, 張遼, 夏侯淵, 袁紹, 貂蝉, 徐晃, 夏侯惇, 馬岱, 呂布, 甘寧, 関羽, 袁術, 関興, 趙雲, 曹仁, 張飛, 徐庶, 孫策, 劉備, 劉表, 曹操, 黄忠, 張郃, 孫乾, 関平, 陳宮, 孫権, 許褚, 馬超, 董卓, 魯粛, 周瑜, 王平, 太史慈, 魏延, 劉璋, 曹丕, 董承, 司馬懿, 姜維, 孟獲, 龐統, 諸葛亮, 孟達, 孫堅, 曹真, 呂蒙, 龐徳, 陸遜, 
21
劉璋, 袁術, 孫権, 劉表, 劉備, 袁紹, 周瑜, 趙雲, 曹洪, 王允, 関羽, 関平, 孟達, 曹丕, 陸遜, 孫乾, 龐統, 諸葛亮, 張遼, 張飛, 曹操, 魯粛, 徐庶, 呂蒙, 甘寧, 呂布, 太史慈, 孫堅, 許褚, 姜維, 馬超, 陳宮, 王平, 龐徳, 孫策, 黄忠, 司馬懿, 魏延, 貂蝉, 董卓, 曹仁, 夏侯惇, 曹真, 馬岱, 董承, 関興, 徐晃, 夏侯淵, 張郃, 孟獲, 
22
呂布, 孫乾, 董承, 許褚, 張飛, 関羽, 関平, 孫堅, 劉備, 劉表, 甘寧, 夏侯惇, 袁術, 孫策, 孟達, 馬超, 張遼, 夏侯淵, 劉璋, 関興, 孫権, 龐統, 趙雲, 曹操, 太史慈, 呂蒙, 周瑜, 黄忠, 曹仁, 孟獲, 陳宮, 魯粛, 魏延, 諸葛亮, 王允, 曹洪, 袁紹, 董卓, 徐晃, 徐庶, 陸遜, 司馬懿, 貂蝉, 龐徳, 姜維, 曹真, 王平, 曹丕, 馬岱, 張郃, 
23
董承, 関平, 王允, 董卓, 夏侯惇, 許褚, 袁術, 張飛, 周瑜, 甘寧, 曹洪, 張郃, 魏延, 王平, 呂蒙, 黄忠, 劉表, 徐晃, 魯粛, 貂蝉, 曹仁, 呂布, 関羽, 姜維, 孫策, 曹操, 夏侯淵, 孫権, 劉備, 関興, 趙雲, 陸遜, 龐統, 曹真, 劉璋, 曹丕, 袁紹, 孫堅, 諸葛亮, 司馬懿, 張遼, 馬岱, 孫乾, 陳宮, 龐徳, 孟達, 馬超, 孟獲, 徐庶, 太史慈, 
24
孟獲, 張郃, 太史慈, 司馬懿, 夏侯淵, 張飛, 趙雲, 王平, 許褚, 馬岱, 周瑜, 魏延, 曹真, 馬超, 袁術, 劉表, 姜維, 黄忠, 夏侯惇, 龐統, 曹洪, 龐徳, 張遼, 孟達, 諸葛亮, 関羽, 関平, 袁紹, 劉備, 陳宮, 孫堅, 魯粛, 孫策, 曹操, 関興, 劉璋, 徐晃, 董卓, 董承, 呂布, 曹丕, 曹仁, 孫権, 孫乾, 貂蝉, 陸遜, 甘寧, 呂蒙, 徐庶, 王允, 
25
甘寧, 夏侯惇, 呂蒙, 夏侯淵, 張遼, 関平, 曹仁, 太史慈, 王平, 張郃, 徐晃, 馬超, 黄忠, 周瑜, 龐徳, 曹真, 司馬懿, 関興, 魏延, 孟獲, 曹洪, 馬岱, 陸遜, 曹丕, 劉璋, 孫権, 孫堅, 諸葛亮, 孫乾, 張飛, 関羽, 曹操, 許褚, 趙雲, 姜維, 魯粛, 孫策, 袁術, 孟達, 王允, 劉備, 龐統, 呂布, 董卓, 陳宮, 劉表, 徐庶, 袁紹, 董承, 貂蝉, 
26
董卓, 孟獲, 曹丕, 孫権, 徐庶, 姜維, 関興, 孟達, 孫堅, 貂蝉, 孫策, 董承, 司馬懿, 劉璋, 周瑜, 魯粛, 陳宮, 袁術, 諸葛亮, 張郃, 許褚, 劉備, 呂蒙, 曹操, 魏延, 呂布, 陸遜, 袁紹, 関羽, 関平, 王允, 黄忠, 張飛, 趙雲, 徐晃, 曹洪, 夏侯惇, 王平, 太史慈, 龐統, 孫乾, 龐徳, 甘寧, 馬岱, 劉表, 張遼, 曹真, 夏侯淵, 馬超, 曹仁, 
27
姜維, 曹洪, 陳宮, 張郃, 趙雲, 徐庶, 孫乾, 関羽, 張遼, 劉璋, 関興, 夏侯惇, 徐晃, 龐統, 周瑜, 王平, 孟達, 諸葛亮, 馬岱, 董承, 司馬懿, 魏延, 呂布, 袁紹, 曹真, 黄忠, 王允, 魯粛, 夏侯淵, 曹操, 呂蒙, 劉備, 劉表, 曹丕, 甘寧, 太史慈, 董卓, 関平, 曹仁, 孫権, 貂蝉, 馬超, 孟獲, 張飛, 龐徳, 許褚, 孫策, 陸遜, 孫堅, 袁術, 
28
董承, 魯粛, 徐庶, 貂蝉, 龐統, 周瑜, 甘寧, 孫堅, 張飛, 呂布, 劉璋, 関羽, 孟獲, 諸葛亮, 王允, 王平, 黄忠, 孫策, 劉備, 呂蒙, 孫乾, 馬超, 袁紹, 袁術, 孫権, 陳宮, 司馬懿, 太史慈, 曹操, 趙雲, 陸遜, 龐徳, 曹真, 姜維, 董卓, 夏侯惇, 張郃, 劉表, 馬岱, 曹洪, 魏延, 徐晃, 許褚, 夏侯淵, 張遼, 曹仁, 曹丕, 関平, 孟達, 関興, 
29
孟達, 劉璋, 姜維, 曹丕, 関平, 孟獲, 馬岱, 曹仁, 王平, 孫乾, 甘寧, 司馬懿, 貂蝉, 馬超, 孫権, 諸葛亮, 孫堅, 孫策, 張遼, 董承, 劉備, 呂蒙, 太史慈, 関興, 魏延, 陸遜, 関羽, 趙雲, 魯粛, 劉表, 夏侯淵, 袁紹, 曹操, 張飛, 龐統, 曹洪, 龐徳, 呂布, 黄忠, 曹真, 周瑜, 王允, 董卓, 袁術, 徐庶, 徐晃, 許褚, 陳宮, 張郃, 夏侯惇, 
30
関興, 孟達, 貂蝉, 張飛, 徐庶, 劉表, 董承, 関平, 許褚, 董卓, 劉備, 魏延, 馬超, 龐統, 劉璋, 袁術, 呂布, 趙雲, 姜維, 関羽, 王平, 馬岱, 孫権, 陳宮, 曹操, 曹丕, 夏侯淵, 甘寧, 孫策, 周瑜, 張遼, 太史慈, 孫乾, 魯粛, 張郃, 孫堅, 呂蒙, 諸葛亮, 陸遜, 夏侯惇, 司馬懿, 曹洪, 黄忠, 徐晃, 曹真, 王允, 龐徳, 袁紹, 曹仁, 孟獲, 
31
太史慈, 司馬懿, 黄忠, 陸遜, 姜維, 陳宮, 董承, 曹真, 董卓, 張郃, 徐晃, 魏延, 呂布, 孫堅, 夏侯惇, 張飛, 許褚, 王平, 甘寧, 孟獲, 夏侯淵, 呂蒙, 馬岱, 孫策, 諸葛亮, 馬超, 張遼, 孟達, 曹丕, 曹洪, 関興, 曹操, 趙雲, 徐庶, 劉璋, 龐統, 曹仁, 貂蝉, 劉備, 劉表, 周瑜, 孫権, 袁紹, 関羽, 龐徳, 関平, 魯粛, 袁術, 孫乾, 王允, 
32
呂蒙, 孫権, 孫堅, 孫乾, 関羽, 陸遜, 魯粛, 龐統, 劉表, 孫策, 徐庶, 太史慈, 劉備, 劉璋, 諸葛亮, 趙雲, 周瑜, 関平, 貂蝉, 陳宮, 張飛, 袁術, 王允, 姜維, 龐徳, 黄忠, 孟獲, 袁紹, 夏侯惇, 曹丕, 曹操, 甘寧, 呂布, 張郃, 曹洪, 夏侯淵, 孟達, 曹仁, 許褚, 曹真, 董承, 魏延, 王平, 董卓, 馬超, 馬岱, 徐晃, 張遼, 司馬懿, 関興, 
33
甘寧, 劉璋, 趙雲, 龐統, 王允, 夏侯惇, 孫策, 孫乾, 董卓, 貂蝉, 孟獲, 張飛, 呂布, 龐徳, 張郃, 孫堅, 王平, 黄忠, 徐庶, 馬超, 太史慈, 張遼, 関羽, 曹仁, 袁紹, 孟達, 陸遜, 曹洪, 馬岱, 陳宮, 曹操, 魯粛, 劉備, 関平, 呂蒙, 周瑜, 董承, 司馬懿, 姜維, 許褚, 諸葛亮, 孫権, 魏延, 曹丕, 夏侯淵, 袁術, 徐晃, 劉表, 曹真, 関興, 
34
徐晃, 太史慈, 袁紹, 王允, 許褚, 孫権, 張郃, 魯粛, 呂布, 董卓, 曹丕, 関興, 陳宮, 曹操, 孟獲, 甘寧, 陸遜, 劉璋, 馬岱, 徐庶, 夏侯惇, 夏侯淵, 張遼, 龐統, 王平, 趙雲, 孫策, 関羽, 龐徳, 曹洪, 曹真, 呂蒙, 劉備, 諸葛亮, 曹仁, 貂蝉, 孫堅, 黄忠, 司馬懿, 孫乾, 馬超, 袁術, 魏延, 劉表, 姜維, 周瑜, 張飛, 董承, 関平, 孟達, 
35
関平, 魯粛, 劉璋, 董承, 張遼, 孫権, 夏侯淵, 孫乾, 王平, 呂蒙, 司馬懿, 周瑜, 諸葛亮, 曹仁, 馬超, 曹真, 陸遜, 馬岱, 夏侯惇, 関羽, 劉表, 董卓, 徐庶, 貂蝉, 甘寧, 王允, 姜維, 袁術, 曹操, 孫策, 張郃, 曹丕, 魏延, 劉備, 趙雲, 関興, 孟獲, 龐徳, 許褚, 陳宮, 呂布, 孟達, 龐統, 張飛, 孫堅, 徐晃, 袁紹, 曹洪, 太史慈, 黄忠, 
36
袁紹, 周瑜, 曹真, 魯粛, 王平, 陸遜, 曹操, 甘寧, 劉表, 魏延, 諸葛亮, 孫権, 張遼, 太史慈, 董卓, 呂布, 張郃, 呂蒙, 孟達, 袁術, 関羽, 陳宮, 黄忠, 劉備, 司馬懿, 徐晃, 馬超, 王允, 孫乾, 姜維, 龐統, 徐庶, 曹丕, 劉璋, 龐徳, 曹洪, 許褚, 貂蝉, 孫堅, 夏侯惇, 孟獲, 張飛, 馬岱, 趙雲, 関平, 董承, 孫策, 曹仁, 関興, 夏侯淵, 
37
関興, 姜維, 陸遜, 徐庶, 王允, 曹真, 呂蒙, 馬岱, 司馬懿, 関平, 趙雲, 諸葛亮, 張遼, 太史慈, 王平, 周瑜, 劉表, 魯粛, 夏侯惇, 魏延, 貂蝉, 関羽, 夏侯淵, 曹洪, 袁紹, 甘寧, 龐統, 劉備, 孟達, 董卓, 龐徳, 張飛, 孫堅, 徐晃, 馬超, 曹操, 孫乾, 董承, 曹仁, 孫権, 劉璋, 陳宮, 孫策, 許褚, 孟獲, 張郃, 袁術, 曹丕, 呂布, 黄忠, 
38
曹仁, 董承, 徐晃, 関平, 袁術, 張郃, 夏侯淵, 龐徳, 馬超, 孫堅, 夏侯惇, 龐統, 魏延, 馬岱, 甘寧, 王平, 張飛, 劉備, 許褚, 曹洪, 劉表, 袁紹, 関興, 董卓, 曹操, 趙雲, 姜維, 呂布, 孫乾, 劉璋, 徐庶, 陳宮, 関羽, 司馬懿, 孟獲, 張遼, 諸葛亮, 太史慈, 王允, 孫策, 陸遜, 貂蝉, 黄忠, 呂蒙, 曹真, 孫権, 魯粛, 曹丕, 孟達, 周瑜, 
39
陸遜, 龐統, 魯粛, 孟獲, 袁術, 袁紹, 孫堅, 周瑜, 諸葛亮, 徐庶, 曹洪, 曹真, 甘寧, 龐徳, 王平, 孫策, 張郃, 曹操, 董卓, 王允, 徐晃, 孫権, 貂蝉, 夏侯淵, 孫乾, 黄忠, 司馬懿, 張遼, 陳宮, 劉備, 馬超, 呂布, 馬岱, 関興, 董承, 曹丕, 夏侯惇, 姜維, 太史慈, 許褚, 孟達, 劉璋, 曹仁, 関平, 張飛, 関羽, 趙雲, 呂蒙, 劉表, 魏延, 
40
曹洪, 曹真, 曹丕, 曹仁, 夏侯惇, 孟達, 司馬懿, 孫策, 呂蒙, 陸遜, 董承, 孟獲, 張郃, 孫堅, 袁紹, 徐晃, 孫権, 姜維, 夏侯淵, 周瑜, 魯粛, 諸葛亮, 曹操, 許褚, 劉璋, 甘寧, 関平, 龐統, 太史慈, 龐徳, 王平, 王允, 袁術, 陳宮, 張遼, 馬岱, 劉備, 魏延, 黄忠, 趙雲, 徐庶, 馬超, 呂布, 関羽, 孫乾, 関興, 董卓, 劉表, 貂蝉, 張飛, 
41
王平, 夏侯淵, 関興, 徐晃, 姜維, 趙雲, 許褚, 太史慈, 黄忠, 張遼, 夏侯惇, 馬岱, 曹真, 孟獲, 曹洪, 張郃, 魏延, 曹仁, 龐徳, 甘寧, 関平, 馬超, 孫堅, 諸葛亮, 張飛, 司馬懿, 陸遜, 孫乾, 周瑜, 孫策, 魯粛, 関羽, 陳宮, 龐統, 徐庶, 曹操, 孟達, 呂蒙, 孫権, 劉備, 袁紹, 董承, 董卓, 呂布, 貂蝉, 袁術, 劉璋, 王允, 曹丕, 劉表, 
42
劉璋, 劉表, 徐庶, 馬超, 孫乾, 黄忠, 甘寧, 夏侯淵, 司馬懿, 曹丕, 関平, 劉備, 袁紹, 張遼, 曹真, 孟達, 孫策, 諸葛亮, 張郃, 曹仁, 関羽, 曹操, 魯粛, 龐徳, 孫権, 夏侯惇, 呂蒙, 魏延, 陸遜, 曹洪, 趙雲, 龐統, 呂布, 董承, 張飛, 姜維, 孫堅, 袁術, 王平, 徐晃, 関興, 馬岱, 貂蝉, 陳宮, 董卓, 太史慈, 王允, 周瑜, 孟獲, 許褚, 
43
夏侯淵, 許褚, 王平, 夏侯惇, 馬超, 孫乾, 劉璋, 趙雲, 曹洪, 袁術, 関羽, 司馬懿, 張遼, 魏延, 関平, 曹仁, 孫権, 徐晃, 張郃, 曹操, 孟獲, 劉備, 孫堅, 孟達, 太史慈, 張飛, 劉表, 諸葛亮, 袁紹, 曹真, 陸遜, 周瑜, 魯粛, 黄忠, 龐統, 貂蝉, 董承, 龐徳, 徐庶, 孫策, 董卓, 呂布, 曹丕, 姜維, 甘寧, 王允, 馬岱, 呂蒙, 関興, 陳宮, 
44
馬岱, 呂布, 関平, 張飛, 董卓, 馬超, 魏延, 陳宮, 趙雲, 張郃, 張遼, 徐庶, 姜維, 孫乾, 関羽, 董承, 袁紹, 劉備, 司馬懿, 貂蝉, 夏侯淵, 劉璋, 王平, 王允, 孟獲, 黄忠, 曹真, 曹操, 曹仁, 曹洪, 夏侯惇, 龐徳, 太史慈, 孟達, 許褚, 孫堅, 関興, 徐晃, 諸葛亮, 龐統, 甘寧, 陸遜, 劉表, 袁術, 呂蒙, 孫策, 孫権, 曹丕, 周瑜, 魯粛, 
45
董卓, 曹丕, 孫策, 夏侯惇, 魯粛, 袁紹, 太史慈, 曹真, 呂蒙, 王允, 袁術, 呂布, 孫権, 陳宮, 曹仁, 曹操, 甘寧, 徐晃, 貂蝉, 許褚, 孫堅, 周瑜, 曹洪, 張遼, 劉表, 司馬懿, 孟達, 関興, 関平, 夏侯淵, 張郃, 劉備, 陸遜, 諸葛亮, 孟獲, 劉璋, 徐庶, 董承, 姜維, 馬岱, 関羽, 馬超, 張飛, 孫乾, 趙雲, 龐統, 魏延, 龐徳, 王平, 黄忠, 
46
貂蝉, 関平, 馬岱, 孟獲, 董卓, 夏侯淵, 孟達, 張飛, 龐徳, 黄忠, 董承, 王平, 劉表, 許褚, 孫策, 曹真, 曹洪, 孫堅, 曹丕, 馬超, 劉備, 龐統, 諸葛亮, 魏延, 張郃, 魯粛, 呂布, 曹操, 趙雲, 太史慈, 孫権, 陳宮, 関羽, 徐庶, 司馬懿, 王允, 姜維, 徐晃, 関興, 陸遜, 袁紹, 夏侯惇, 孫乾, 張遼, 周瑜, 袁術, 劉璋, 呂蒙, 曹仁, 甘寧, 
47
孫権, 孟獲, 呂蒙, 孟達, 劉表, 孫乾, 孫策, 孫堅, 曹丕, 魯粛, 徐庶, 陸遜, 太史慈, 袁紹, 関平, 周瑜, 呂布, 張郃, 曹真, 袁術, 夏侯淵, 劉備, 魏延, 董卓, 甘寧, 張飛, 王平, 劉璋, 曹仁, 趙雲, 許褚, 司馬懿, 曹操, 夏侯惇, 陳宮, 徐晃, 関羽, 馬超, 董承, 諸葛亮, 黄忠, 龐統, 馬岱, 関興, 王允, 龐徳, 張遼, 曹洪, 姜維, 貂蝉, 
48
孫乾, 許褚, 張遼, 董承, 袁術, 孫堅, 袁紹, 孟達, 陸遜, 関平, 夏侯惇, 劉璋, 孟獲, 関羽, 司馬懿, 魯粛, 張飛, 董卓, 曹真, 甘寧, 曹丕, 曹操, 劉備, 孫策, 曹洪, 陳宮, 呂布, 呂蒙, 周瑜, 孫権, 劉表, 王允, 龐統, 龐徳, 趙雲, 諸葛亮, 貂蝉, 夏侯淵, 曹仁, 太史慈, 姜維, 徐晃, 馬岱, 関興, 徐庶, 黄忠, 魏延, 王平, 馬超, 張郃, 
49
孫乾, 関興, 龐徳, 董卓, 曹丕, 張飛, 貂蝉, 王允, 徐晃, 孟獲, 袁術, 孫堅, 張遼, 黄忠, 関羽, 劉備, 呂布, 許褚, 曹操, 趙雲, 徐庶, 太史慈, 司馬懿, 陳宮, 魏延, 袁紹, 馬岱, 曹洪, 張郃, 董承, 劉表, 夏侯惇, 姜維, 呂蒙, 諸葛亮, 夏侯淵, 馬超, 関平, 劉璋, 王平, 孫策, 魯粛, 曹真, 周瑜, 曹仁, 陸遜, 孫権, 孟達, 甘寧, 龐統, 
50
関平, 太史慈, 王平, 張郃, 姜維, 魏延, 陸遜, 関興, 呂蒙, 王允, 司馬懿, 許褚, 馬超, 曹丕, 徐晃, 黄忠, 関羽, 龐徳, 馬岱, 孟達, 趙雲, 董卓, 呂布, 孫堅, 周瑜, 孫乾, 諸葛亮, 孫権, 陳宮, 張遼, 貂蝉, 劉璋, 董承, 孟獲, 曹仁, 劉備, 曹操, 曹真, 張飛, 徐庶, 魯粛, 甘寧, 夏侯惇, 孫策, 夏侯淵, 袁術, 曹洪, 袁紹, 龐統, 劉表, 
51
孟達, 徐庶, 張郃, 徐晃, 龐統, 呂蒙, 夏侯惇, 曹仁, 曹洪, 孫乾, 張遼, 姜維, 魯粛, 劉表, 曹操, 許褚, 陳宮, 甘寧, 諸葛亮, 曹丕, 孫策, 趙雲, 孫権, 関平, 龐徳, 劉備, 魏延, 袁紹, 袁術, 呂布, 劉璋, 関羽, 陸遜, 張飛, 周瑜, 貂蝉, 司馬懿, 黄忠, 曹真, 孟獲, 馬超, 馬岱, 夏侯淵, 太史慈, 董承, 孫堅, 関興, 董卓, 王允, 王平, 

いかがでしょうか?

どこかの次元を見て、これが武力順!これが蜀忠義順!などの
なんらかの意味がある情報が見つかっただろうか?
(もし見つけた人がいらっしゃれば、ぜひ教えていただきたい)

全く無作為な順番の羅列のようにも見えるのだが、
これが、さきほどまで武将の似ている度や演算をしていた、
機械学習済みモデルの中身そのもの。

機械学習結果を、人間が「理解」することは難しいと言われており、
それがよく表れていると思う。

人間だったら、武将を数値化したければ、やはり、
武力、知力、忠誠心、所属国、活躍した時代、等で数値化するだろうが、
機械(えーあい)は何を以て数値化したのだろうか?
それでいて、出来たモデルを「使う」際にはそれなりの結果が出ている。

呂布「なるほど!(全くわからん)」
張飛「ウムッ!(全くわからん)」
許褚「良く分かった!(全くわからん)」
曹操「(セリフにする人の人選を誤ったな・・・)」

最強の武(の値)を求めるのは簡単な道ではないのだ。
(次回へ続く・・・?)

Word2Vec編の終わり

仲達「こんなに長い記事を書いているなんて、
   フフフ、諸葛亮も長くはないぞ!」

長くなりすぎた。
キリも良いので、作者と読者の健康のために一旦ここまでで切る。
前回も見たなコレ

ここまでで、
吉川英治三国志の小説を自然言語処理して、
その結果を機械学習にかけることで、
Word2Vecモデルを得ることが出来た。
すなわち、武将を50次元のベクトルとして数値で表現できた

武将同士の「似ている度」「演算」が出来るという、
モデルの使い方も分かり、その演算の結果、
孫権と甘寧の関係を改めて確認することができ、
三国志の世界がそれなりに投影されたモデルである
ことも分かった。

一方で、その機械学習モデルの中身をよく見ても、
「武力」「知力」などの軸を取り出すのには、
まだ工夫が必要であることも分かった。

これは困った。
むむむ!

なにがむむむだ!
甘寧の演算が出来るほどなのだから、
どこかに武力・知力に相当するデータは存在するハズッ!
何か風向きを変える策さえあれば・・・。

風向きというか気が向いたら次回も書く。
話が難しくなってきたしまとめるの結構大変で疲れた。

さて、内容が深くなりすぎて、
前回よりもさらに三国志マニアック度が上がってしまったが、
まだ読んでいる人が居るのだろうか・・・?

(オマケ)前回のSEKIHEKIのたた会の参加者のみなさまへ

主催者より。

おかげさまで前回のイベントは
岸が赤く燃えがあるほどの大盛り上がりになりました。
参加していただいた皆様に感謝と御礼を申し上げます。
(全く予想しないほど三国志ファンが多く驚きました)

あるニートのLTはとくに大好評で、彼は
三個の零(=年俸1000万円超)で某スタートアップに迎えられました。

彼が発表したライブラリ「石兵八陣」は、
早速、真似をする人が続出してしまい、
利用者から「前に見た/似たようなライブラリが多くて迷う」と
言われるほどの大人気です。

ただ、スポンサー様が「げぇ、関羽!」と
叫んで逃げてしまいまして、
次回開催の応援者様を募集中です。


関羽「えっ、ワシのせいなの!?」

★追記:完結編、書きました。
https://qiita.com/youwht/items/61c6d5819cdc3aff9e63

以上。

プリキュアがきらきらしている秘密。「ラ行」の透明感とラーメンの人気から見る、素敵な名前のつけかたをPythonで分析する

背景:ラ行は透明感があって新鮮で、プリキュアやラーメン人気の源泉である!?

先日、あるWeb記事を拝見して、衝撃を受けた。
一部関連記事も含めて要約すると、

  • 「ラーメン」はそのおいしさだけでなく、「名前」が人気を後押ししている。
  • プリキュア60人中34人が「ラ行」を含んでいる
  • 「ラ行」には透明感があり、言葉が綺麗に聞こえる
  • 古来日本語では「ラ行」は語頭に無かったため、日本人には新鮮&珍しく聞こえる

元記事については、下記リンクをご参照。
ラーメンの話から、日本語の中で「ラ」が1番透明感があり、そのためプリキュアには「ラ」がつく名前が多い

長年アイウエオに触れていながら「ラ行」の特殊性に全く気づかなかった

確かに言われてみると、
しりとり徹底分析」をした際に作った、各文字ごとの単語数表を見ても、
「ラ行」は全体的に単語数が少ない。

「しりとり徹底分析」については、下記リンクをご参照。
「しりとり」徹底分析!最強キャラ(文字)解説&5つの「驚愕」

そして最も衝撃を受けた理由は、
3歳娘(架空/フィクションです)の名前は「ラ行」で構成されているためだ。

「ラ行」の特殊性については全く無自覚であったが、
私も無意識的に「ラ行」をカワイイと信奉していたのかもしれない。
もちろん3歳娘もプリキュアの信奉者であり、毎日カラーチャージしている。
3歳娘とプリキュアの意外な共通点が今、明らかになった(驚)!

疑問:本当にラ行が多いの? ラ行だけなの?

しかし、パッと考えて様々な疑問が湧いてくる。

プリキュア60人中34人、というのは一見すると多いが、
そもそもプリキュア名は「カタカナ」が必須であるし、
一般のカタカナ語と比べても本当に多いと言えるのだろうか?

「ラ行」以外に他の「プリキュア感(透明感)のある行」は無いのか?
また逆に「プリキュア感のない行」もあるのか?

さらに、このルールだと次のプリキュアは
キュア ラーメン
キュア ゴリラ
キュア ラオウ
を許すことになってしまう。

スープと麺のハーモニー!キュアラーメン!
キュアライスとのがったいこうげきはこうかばつぐんだ!

もし、3歳娘がプリキュアになったときに、
このような名前だと泣いてしまうだろう。
素敵な、キュア〇〇〇〇
の名前を考える必要性がある。(※注)

さあ、プログラムを使って、
これらの疑問に対する数値根拠を出し、
プリキュアの秘密を分析してみよう!!

頭に浮かんだこと ぜんぶぜんぶやっちゃおう
想像力からはじまる いま!いま!いま!イマジネーション
「ねぇ、ホントのことを、知りたいの!」

※注:
 通常プリキュアは両親には秘密の職業であり、
 仮に3歳娘を将来プリキュアにすることに成功したとしても、
 両親は全く気づかない可能性がある。
 しかし「透明感のある名前」を娘につけてあげれば、
 プリキュアになる可能性も上がるのではないか?
 変身前の少女の名前も考察対象とする

結果:プリキュアと一般名詞の「音」を分析してみると面白いことが判明

パパやママに聞いても教えてくれないこれらの疑問に答えるために、
プリキュア60名と、一般名詞68万語を分析した結果、
とても興味深いことが判明した。

先に代表的な結論を書こう。

  • 「ラーメン」はプリキュアと同じネーミングセンスだった
  • 「ラ行」はプリキュア行であることを確認(後述のグラフ必見)
    • さらに、「マ行」がかわいさの秘密だった
    • キュアミルキー(ララ)が最カワなので知ってた
  • 透明感のある名前のつけかたが判明した
    • そのつけかたの評価によると、
    • 「ラーメン」>>「ゴリラ」>「ラオウ」
    • 透明感の無い名前になる文字も判明

さらに、オマケとして(いつもの)Word2Vecも投入すると、
プリキュア名に相応しい単語を続々発掘。
代表的なもので、
ストロベリー、ウィッシュ、スウィート、
シュガー、アイリス、ファイン、
マーマレード、ホーリー、マーブル、パール・・・
などのプリキュア感のある単語を発掘することが出来た。

以下で実際に、どのようなプログラム/データ分析を行ったのか、
考察の内容を記述する。

前提:分析の準備

  • Python3(全てGoogle Colaboratory で実行)
  • 「しりとり徹底分析」で用意した大量の一般名詞リストを使う
  • プリキュアは60人の定義を採用する(2019年9月現在)
    • プリキュアの人数は宗教論争になる
    • 最終的に男性含め全人類にプリキュアになれる可能性がある
    • 映画/オールスター準拠で60人、が妥当線
    • 参考:http://prehyou2015.hatenablog.com/entry/nanninpuri
  • 技術的に難易度の高いコードは無い。チマチマ頑張って分析する
    • 自然言語処理100本ノックのような感じ

ではさっそく、
プリキュアの名前、変身前の名前、一般名詞、について、
のそれぞれの「音」(アカサタナハマヤラワ)の
統計データを作って考察してみよう!

【1】プリキュア名/変身前名の分析

【1-1】プリキュア名簿の準備

まず、プリキュアの変身前後の名前を定義したCSVファイルを用意し、
以下のようにして読み込む。
苗字は分析してもしょーがないため、
変身前については名前(に相当する箇所)だけに絞っている。

プリキュア名簿の読み込み
import csv

csv_file = open("drive/My Drive/PURI/プリキュア名称一覧v2.csv", "r", encoding="ms932", errors="", newline="" )
f = csv.reader(csv_file, delimiter=",", doublequote=True, lineterminator="\r\n", quotechar='"', skipinitialspace=True)

PRECURE_NAME_DATA_LIST = []
for row in f:
    #rowはList形式
    print(row)
    PRECURE_NAME_DATA_LIST.append(row)
プリキュア名簿の読み込み結果
['1', 'ふたりはプリキュア', 'ブラック', 'ナギサ']
['2', 'ふたりはプリキュア', 'ホワイト', 'ホノカ']
['3', 'ふたりはプリキュアMaxHeart', 'シャイニールミナス', 'ヒカリ']
['4', 'ふたりはプリキュアSplashStar', 'ブルーム', 'サキ']
['5', 'ふたりはプリキュアSplashStar', 'イーグレット', 'マイ']
['6', 'Yes!プリキュア5', 'ドリーム', 'ノゾミ']
['7', 'Yes!プリキュア5', 'ルージュ', 'リン']
['8', 'Yes!プリキュア5', 'レモネード', 'ウララ']
['9', 'Yes!プリキュア5', 'ミント', 'コマチ']
['10', 'Yes!プリキュア5', 'アクア', 'カレン']
['11', 'Yes!プリキュア5GoGo!', 'ローズ', 'クルミ']
['12', 'フレッシュプリキュア!', 'ピーチ', 'ラブ']
['13', 'フレッシュプリキュア!', 'ベリー', 'ミキ']
['14', 'フレッシュプリキュア!', 'パイン', 'イノリ']
['15', 'フレッシュプリキュア!', 'パッション', 'セツナ']
['16', 'ハートキャッチプリキュア!', 'ブロッサム', 'ツボミ']
['17', 'ハートキャッチプリキュア!', 'マリン', 'エリカ']
['18', 'ハートキャッチプリキュア!', 'サンシャイン', 'イツキ']
['19', 'ハートキャッチプリキュア!', 'ムーンライト', 'ユリ']
['20', 'スイートプリキュア♪', 'メロディ', 'ユビキ']
['21', 'スイートプリキュア♪', 'リズム', 'カナデ']
['22', 'スイートプリキュア♪', 'ビート', 'エレン']
['23', 'スイートプリキュア♪', 'ミューズ', 'アコ']
['24', 'スマイルプリキュア!', 'ハッピー', 'ミユキ']
['25', 'スマイルプリキュア!', 'サニー', 'アカネ']
['26', 'スマイルプリキュア!', 'ピース', 'ヤヨイ']
['27', 'スマイルプリキュア!', 'マーチ', 'ナオ']
['28', 'スマイルプリキュア!', 'ビューティ', 'レイカ']
['29', 'ドキドキ!プリキュア', 'ハート', 'マナ']
['30', 'ドキドキ!プリキュア', 'ダイヤモンド', 'リッカ']
['31', 'ドキドキ!プリキュア', 'ロゼッタ', 'アリス']
['32', 'ドキドキ!プリキュア', 'ソード', 'マコト']
['33', 'ハピネスチャージプリキュア!', 'エース', 'アグリ']
['34', 'ハピネスチャージプリキュア!', 'ラブリー', 'メグミ']
['35', 'ハピネスチャージプリキュア!', 'プリンセス', 'ヒメ']
['36', 'ハピネスチャージプリキュア!', 'ハニー', 'ユウコ']
['37', 'ハピネスチャージプリキュア!', 'フォーチュン', 'イオナ']
['38', 'Go!プリンセスプリキュア', 'フローラ', 'ハルカ']
['39', 'Go!プリンセスプリキュア', 'マーメイド', 'ミナミ']
['40', 'Go!プリンセスプリキュア', 'トゥインクル', 'キララ']
['41', 'Go!プリンセスプリキュア', 'スカーレット', 'トワ']
['42', '魔法つかいプリキュア!', 'ミラクル', 'ミライ']
['43', '魔法つかいプリキュア!', 'マジカル', 'リコ']
['44', '魔法つかいプリキュア!', 'フェリーチェ', 'コトハ']
['45', 'キラキラ☆プリキュアアラモード', 'ホイップ', 'イチカ']
['46', 'キラキラ☆プリキュアアラモード', 'カスタード', 'ヒマリ']
['47', 'キラキラ☆プリキュアアラモード', 'ジェラート', 'アオイ']
['48', 'キラキラ☆プリキュアアラモード', 'マカロン', 'ユカリ']
['49', 'キラキラ☆プリキュアアラモード', 'ショコラ', 'アキラ']
['50', 'キラキラ☆プリキュアアラモード', 'パルフェ', 'シエル']
['51', 'HUGっと!プリキュア', 'エール', 'ハナ']
['52', 'HUGっと!プリキュア', 'アンジュ', 'サアヤ']
['53', 'HUGっと!プリキュア', 'エトワール', 'ホマレ']
['54', 'HUGっと!プリキュア', 'マシェリ', 'エミル']
['55', 'HUGっと!プリキュア', 'アムール', 'ルールー']
['56', 'スター☆トゥインクルプリキュア', 'スター', 'ヒカル']
['57', 'スター☆トゥインクルプリキュア', 'ミルキー', 'ララ']
['58', 'スター☆トゥインクルプリキュア', 'ソレイユ', 'エレナ']
['59', 'スター☆トゥインクルプリキュア', 'セレーネ', 'マドカ']
['60', 'スター☆トゥインクルプリキュア', 'コスモ', 'ユニ']

【1-2】プリキュア名のラリルレロ利用率は?

プリキュアの名前で、
どこまでラリルレロが使われているのか、
使用率を確認するコードを書いていく。

プリキュア名簿の読み込み結果
# ラ行の文字数を数えるための関数 countMojisuu(input_str, "ラリルレロ")
def countMojisuu(input_str , check_str):
  result_val = 0
  for check_moji in check_str:
    result_val += input_str.count(check_moji)
  return result_val

# 単語のリストと、チェック対象文字列(今回はラリルレロなど)が与えられた時に、
# 全単語個数、そのうち何単語にチェック対象文字列が含まれるか、
# 全文字数、そのうち何文字がチェック対象文字か、を返す。
def CheckUsedRate(input_str_list, check_str):
  total_kosuu = len(input_str_list)
  used_kosuu  = 0
  total_mojisuu = 0
  total_used_mojisuu = 0
  for input_str in input_str_list:
    used_mojisuu = countMojisuu(input_str , check_str)

    if used_mojisuu >0:
      used_kosuu += 1
    total_mojisuu += len(input_str)
    total_used_mojisuu += used_mojisuu

  return total_kosuu, used_kosuu, total_mojisuu, total_used_mojisuu

#上記の関数の結果を出力表示するための関数
def PrintUsedRate(total_kosuu, used_kosuu, total_mojisuu, total_used_mojisuu):
  print("単語数: ",used_kosuu, " / ", total_kosuu, "  ", round(used_kosuu/total_kosuu*100)," %")
  print("文字数: ",total_used_mojisuu, " / ", total_mojisuu, "  ", round(total_used_mojisuu/total_mojisuu*100)," %")

# プリキュア名簿から、プリキュアの名前、変身前の名前、のリストを作る
PRECURE_NAME_STR_LIST = [row[2] for row in PRECURE_NAME_DATA_LIST]
print(PRECURE_NAME_STR_LIST)
HENSINMAE_NAME_STR_LIST = [row[3] for row in PRECURE_NAME_DATA_LIST]
print(HENSINMAE_NAME_STR_LIST)

# ラリルレロの利用率を確認する
taisyou_str = "ラリルレロ"
print("プリキュア名の",taisyou_str,"利用率:")
total_kosuu, used_kosuu, total_mojisuu, total_used_mojisuu = CheckUsedRate(PRECURE_NAME_STR_LIST, taisyou_str)
PrintUsedRate(total_kosuu, used_kosuu, total_mojisuu, total_used_mojisuu)
print("")
print("変身前名の",taisyou_str,"利用率:")
total_kosuu, used_kosuu, total_mojisuu, total_used_mojisuu = CheckUsedRate(HENSINMAE_NAME_STR_LIST, taisyou_str)
PrintUsedRate(total_kosuu, used_kosuu, total_mojisuu, total_used_mojisuu)

ラリルレロ利用率
プリキュア名の ラリルレロ 利用率:
単語数:  34  /  60    57  %
文字数:  37  /  251    15  %

変身前名の ラリルレロ 利用率:
単語数:  28  /  60    47  %
文字数:  32  /  166    19  %

プリキュア60名中34名 = 57%にラ行が入る。
また、文字数で見た場合、ラ行は15%を占めている。
ということが分かる。が、
変身前の時点で、かなり高い割合でラ行を使用している
ということも分かった。

【1-3】アイウエオ、カキクケコの利用率は?

1-2の引数を変えて実行すれば、
すぐに他の行の結果も確認することが出来る。

アイウエオ利用率
プリキュア名の アイウエオ 利用率:
単語数:  17  /  60    28  %
文字数:  18  /  251    7  %

変身前名の アイウエオ 利用率:
単語数:  23  /  60    38  %
文字数:  26  /  166    16  %
カキクケコ利用率
プリキュア名の カキクケコ 利用率:
単語数:  11  /  60    18  %
文字数:  11  /  251    4  %

変身前名の カキクケコ 利用率:
単語数:  27  /  60    45  %
文字数:  27  /  166    16  %

こちらは変身前後でかなり傾向に違いが出た。

【2】世の中の名詞全体の分析

【2-1】大量名詞データの準備

「しりとり」徹底分析!最強キャラ(文字)解説&5つの「驚愕」
の記事で作成した、しりとり用名詞データを再利用する。
自分でこのデータから作りたい人は上述の記事をご参照。

大量名詞データの読み込み
import pickle
#保存したpickleファイルは、以下のように復元する
with open('drive/My Drive/PURI/siritori_noun_list.dump', 'rb') as f:
    siritori_noun_list = pickle.load(f)
    print(len(siritori_noun_list))

import pprint
pprint.pprint(siritori_noun_list[1000:1005])

下記のようなフォーマットで大量のデータ(360万レコード)が入っている。

大量名詞データの読み込み結果
3655284
[['へとへと', '名詞', '形容動詞語幹', 'ヘトヘト', 4, 'ヘ', 'ト', 'ヘ', 'ト'],
 ['極大', '名詞', '形容動詞語幹', 'キョクダイ', 5, 'キ', 'イ', 'キ', 'イ'],
 ['失当', '名詞', '形容動詞語幹', 'シットウ', 4, 'シ', 'ウ', 'シ', 'ウ'],
 ['冷酷', '名詞', '形容動詞語幹', 'レイコク', 4, 'レ', 'ク', 'レ', 'ク'],
 ['有数', '名詞', '形容動詞語幹', 'ユウスウ', 4, 'ユ', 'ウ', 'ユ', 'ウ']]

【2-2】「カタカナ単語」だけに絞る

全名詞を対象にするのは望ましくないと考え、
カタカナ語だけに絞った。(⇒ 68万語になった)

ここでのポイントは、カタカナ判定の正規表現。
re.compile(r'[\u30A1-\u30F4]+')
re.compile(r'[ァ-ン]+')
このような書き方をWeb上でよく見かけるが、
「ー(長音)」が入っていなかったり、いろいろ不都合があったため、
re.compile(r'[ァ-ヴー]+')
を採用。(参考:http://syutin.cside.ne.jp/diary/2017/08/755)

見出しがカタカナのみである語に限定&重複削除
import re

re_katakana = re.compile(r'[ァ-ヴー]+')

katakana_noun_list = []
for noun_row in siritori_noun_list:
  if re_katakana.fullmatch(noun_row[0]):
    katakana_noun_list.append(noun_row[0])

#重複の削除
katakana_noun_list= list(set(katakana_noun_list))

print(len(katakana_noun_list))
print(katakana_noun_list[4000:4010])

# 出力結果はこんな感じ
# 687432
# > ['ギョジ', 'サモハッカ', 'アランカ', 'ニホンキカクカブシキガイシャ', 
# >  'ベイネッテ', 'モエシャ', 'モリーヘイガン', 'ワンナイト', 
# >  'エメレク', 'ロクジュウゴテンロクキログラム']

元の辞書の性質上、「ゴヒャクロクジュウロクニンゲツ」のように
カタカナにしただけ&数字表現なども一部含まれるようだが、
全体の「傾向」を分析したい話なので、ここでは無視して先に進む。

【2-3】大量名詞に対する、ラリルレロ利用率の調査

プリキュアに対して実行したコードと、
全く同じコードで確認することが出来る。

大量名詞に対する、ラリルレロ利用率
taisyou_str = "ラリルレロ"
print("大量のカタカナ名詞中の、",taisyou_str,"利用率:")
total_kosuu, used_kosuu, total_mojisuu, total_used_mojisuu = CheckUsedRate(katakana_noun_list, taisyou_str)
PrintUsedRate(total_kosuu, used_kosuu, total_mojisuu, total_used_mojisuu)
ラリルレロ利用率
大量のカタカナ名詞中の、 ラリルレロ 利用率:
単語数:  423932  /  687432    62  %
文字数:  641246  /  6845333    9  %

単語の利用率=62%、文字の利用率=9%
あれ!?
プリキュア名は、単語数 ⇒ 57%文字数 ⇒ 15% だったので、
「ラリルレロ」が含まれる単語の割合は、一般名詞と大差がない。

つまり、
プリキュア60人中34人に「ラ行」が含まれる
という点だけでは実は、名詞全般の平均値と変わらない。

が、
もっと重要なのは、文字数に対する比率である。
文字数で見た時の利用率は9%対15%と、プリキュアの方がかなり上だ。

プリキュアの名前は3文字~6文字であるのに対し、
今回対象とした68万語の中には、もっと長い単語が多数あるため、
「ラリルレロを含む」という条件での比較は公平ではない。
よって、文字数に対しての比率で考えなければいけない、と分かった。

【3】プリキュア名と変身前名と一般名詞の可視化

【3-1】ア行~ラ行、全部の行に統計処理を適用

傾向を一括で可視化するため、
「ア行」「ラ行」など個別に適用していた加工処理を一括適用する。
「ワ」「ン」は「ヤ行」に分類した。

プリキュア名簿の読み込み結果
KATAKANA_TARGET_LIST = [
  "アイウエオ", "カキクケコ", "サシスセソ",
  "タチツテト", "ナニヌネノ", "ハヒフヘホ",
  "マミムメモ", "ヤユヨワン", "ラリルレロ",
  "ガギグゲゴ", "ザジズゼゾ", "ダヂヅデド",
  "バビブベボ", "パピプペポ", "ー", "ッ",
  "ャュョァィゥェォ"
]

precure_name_kosuu_rate = []
precure_name_mojisuu_rate = []

for taisyou_str in KATAKANA_TARGET_LIST:
  total_kosuu, used_kosuu, total_mojisuu, total_used_mojisuu = CheckUsedRate(PRECURE_NAME_STR_LIST, taisyou_str)
  precure_name_kosuu_rate.append(round(used_kosuu*100/total_kosuu))
  precure_name_mojisuu_rate.append(round(total_used_mojisuu*100/total_mojisuu))

hensinmae_name_kosuu_rate = []
hensinmae_name_mojisuu_rate = []

for taisyou_str in KATAKANA_TARGET_LIST:
  total_kosuu, used_kosuu, total_mojisuu, total_used_mojisuu = CheckUsedRate(HENSINMAE_NAME_STR_LIST, taisyou_str)
  hensinmae_name_kosuu_rate.append(round(used_kosuu*100/total_kosuu))
  hensinmae_name_mojisuu_rate.append(round(total_used_mojisuu*100/total_mojisuu))

noun_kosuu_rate = []
noun_mojisuu_rate = []

for taisyou_str in KATAKANA_TARGET_LIST:
  total_kosuu, used_kosuu, total_mojisuu, total_used_mojisuu = CheckUsedRate(katakana_noun_list, taisyou_str)
  noun_kosuu_rate.append(round(used_kosuu*100/total_kosuu))
  noun_mojisuu_rate.append(round(total_used_mojisuu*100/total_mojisuu))

【3-2】matplotlibで可視化

作成したデータを可視化する。
本稿のハイライトがこの可視化のグラフ

各行の利用率を可視化
import matplotlib.pyplot as plt
import numpy as np
import japanize_matplotlib 
#重要:日本語文字化け防止
# !pip install japanize-matplotlib

def DrawBouGraph(listA , listB, listC, labels):
  # numpyの横軸設定
  left = np.arange(len(listA))
  labels = KATAKANA_TARGET_LIST
  width = 0.3

  plt.figure(figsize=(30, 10), dpi=50)

  plt.bar(left, listA, color='r', width=width, align='center', label="プリキュア")
  plt.bar(left+width, listB, color='g', width=width, align='center', label="変身前")
  plt.bar(left+width+width, listC, color='b', width=width, align='center', label="全名詞")

  plt.xticks(left + width/2, labels)
  plt.legend()
  plt.show()
  #plt.savefig('xxxxx.png')

このようなグラフ化関数を使って、データの可視化をしよう!

【3-3】○行が含まれる率の可視化(ご参考)

文字数で見ないと意味が薄いが、せっかくなのでこちらも掲載。

DrawBouGraph(precure_name_kosuu_rate, hensinmae_name_kosuu_rate, noun_kosuu_rate, KATAKANA_TARGET_LIST)

puri_aiueo.png

青 = 一般名詞 は(今回採用したデータ内に)文字数が長いものが多いため、
全体的に高い割合になっている。

【3-4】○行の文字数率の可視化(本命グラフ)

こちらが本命、本稿のハイライト

DrawBouGraph(precure_name_mojisuu_rate, hensinmae_name_mojisuu_rate, noun_mojisuu_rate, KATAKANA_TARGET_LIST)

puri_aiueo_mojisuu.png

【3-5】可視化した結果の考察

一般名詞=青、では、
「ア行」「カ行」「サ行」「ヤワ行」「ラ行」の使用率が高い。

プリキュア名=赤、では、
「ラ行」「ー(長音)」「マ行」の使用率が高く、
一般でTOPだった「ア行」などは逆にかなり減っている。

変身前名=緑、では
「ラ行」「マ行」はプリキュア名と傾向が近いものの、
「ア行」「カ行」の使用率も高い。
(※今回対象を「カタカナ」にしているため、
  変身前名に対する考察は、ご参考程度)

このことから考察すると、
「ラ行」「マ行」が、きらきらしたカワイイ感じとして、
プリキュア名や変身前女子名として採用率が高い、という傾向がある。

逆に、「ア行」「カ行」は、一般的によく使われるために、
プリキュア名として採用すると、ふつーな感じ、になってしまうので、
きらきら感を出すために採用率が低くなっている、という傾向がある。

「ラ行」は普通にプリキュアの名前だけ見ていても高採用率だが、
実は「マ行」の方が、一般名詞との利用比率乖離でみると、
「ラ行」より高い倍率で使われているのだ!!

これには園児もびっくりすること間違いなし。
プリキュア界において特筆すべき新発見である。

【4】プリキュア的きらきら感を判定できる関数で遊ぶ

【4-1】プリキュアきらきら名前チェッカー

さきほどのグラフの「赤」「青」をよーく見比べてみると、
各行は3パターンに分類出来ることに気づく。
① プリキュアの方が2倍くらい多い行
② 利用率がほぼ変わらない行
③ プリキュアの方が半分くらいの行 

そこで、プリキュア的にきらきらしている名前かどうか、
判定出来る簡単なロジックを思いついた。
超単純に、①の文字はプラス1点、③の文字はマイナス1点、で数えるだけだ。
名付けて「プリキュアきらきら名前チェッカー」

プリキュアきらきら名前チェッカー
def eval_pricure_do(input_str):
  result_point = 0

  for itimoji in input_str:
    if itimoji in "アイウエオカキクケコナニヌネノガギグゲゴ":
      result_point -= 1
      continue
    if itimoji in "サシスセソタチツテトハヒフヘホヤユヨワンザジズゼゾダヂヅデドバビブベボパピプペポッャュョァィゥェォ":
      result_point += 0
      continue
    if itimoji in "マミムメモラリルレロー":
      result_point += 1
      continue
    #何か異物文字が混入しているということ
    result_point -=10
  return result_point  

# この名前チェッカーを全プリキュアに適用する
import copy
tmp_list = copy.deepcopy(PRECURE_NAME_STR_LIST)
tmp_list = list(map(lambda input_str : (input_str, eval_pricure_do(input_str)), tmp_list))
import pprint
pprint.pprint(tmp_list)

全プリキュアの名前をこの関数により評価した結果を見てみよう。
驚きの結果が明らかになる。

プリキュアの名前きらきら度の判定結果
[('ブラック', 0),
 ('ホワイト', -1),
 ('シャイニールミナス', 0),
 ('ブルーム', 3),
 ('イーグレット', 0),
 ('ドリーム', 3),
 ('ルージュ', 2),
 ('レモネード', 2),
 ('ミント', 1),
 ('アクア', -3),
 ('ローズ', 2),
 ('ピーチ', 1),
 ('ベリー', 2),
 ('パイン', -1),
 ('パッション', 0),
 ('ブロッサム', 2),
 ('マリン', 2),
 ('サンシャイン', -1),
 ('ムーンライト', 2),
 ('メロディ', 2),
 ('リズム', 2),
 ('ビート', 1),
 ('ミューズ', 2),
 ('ハッピー', 1),
 ('サニー', 0),
 ('ピース', 1),
 ('マーチ', 2),
 ('ビューティ', 1),
 ('ハート', 1),
 ('ダイヤモンド', 0),
 ('ロゼッタ', 1),
 ('ソード', 1),
 ('エース', 0),
 ('ラブリー', 3),
 ('プリンセス', 1),
 ('ハニー', 0),
 ('フォーチュン', 1),
 ('フローラ', 3),
 ('マーメイド', 2),
 ('トゥインクル', -1),
 ('スカーレット', 1),
 ('ミラクル', 2),
 ('マジカル', 1),
 ('フェリーチェ', 2),
 ('ホイップ', -1),
 ('カスタード', 0),
 ('ジェラート', 2),
 ('マカロン', 1),
 ('ショコラ', 0),
 ('パルフェ', 1),
 ('エール', 1),
 ('アンジュ', -1),
 ('エトワール', 1),
 ('マシェリ', 2),
 ('アムール', 2),
 ('スター', 1),
 ('ミルキー', 2),
 ('ソレイユ', 0),
 ('セレーネ', 1),
 ('コスモ', 0)]

なんと60名中59名が、マイナス1以上であった。

コードは省略するが、プリキュアの文字数に合わせて、
3文字~6文字の一般名詞(18万7千語)に対して
同様の確認をした場合、
マイナス1以上になるのは全体の77%

つまり、一般名詞の割合で見ると、
4人に1人くらいはマイナス2以下が居て、
60名中で考えると13人くらい居るのが普通なのに、
1人(キュアアクア)しかマイナス2以下が居なかった。
もちろん数値で見ても各メンバーの値は大きめである。

60人中59人が当てはまる、
プリキュアの秘密(のネーミングルール)を見つけてしまった!

【4-2】「ラーメン」の人気の秘密

プリキュアきらきら名前チェッカーを「ラーメン」に適用すると、
ラーメン = 3
(ラ,ー,メ がそれぞれ1ポイント、ン、はゼロ)

プログラムを書くまでもなく、プラスとマイナスの「行」を
覚えておくだけで簡単に暗算判定が出来る。

やはり、
「ラーメン」はプリキュアに通じるところのある、
きらきらカワイイ感があふれるネーミングセンスだった

なお、
ゴリラ = 1
ラオウ = マイナス1

「ラーメン」という名前をつけたことによって、
爆発的にヒットが加速した、と元記事に記載があったが、
あながち間違いではなく、プリキュアとの関連性についても、
より強固な相似要素を見つけることが出来た。
(「ラ」だけでなく「メ」や「ー」も相似要素)

もしあなたが今後カタカナ名を考える場合は、
このプリキュアきらきら名前チェッカーを参考にして
「ラリルレロマミムメモー(長音)」を
出来るだけ取り入れるように考えると人気が出るかもしれない。

【4-3】最もプリキュアにふさわしい名前は何か?(失敗談)

ここまで、個人的には大変興味深い結果であった。

さらに、きらきら感を数値化出来るのであれば、
「最もプリキュアにふさわしい名詞は何か?」
をプログラムで見つけたくなってしまうのが人情というもの。

全名詞の中から、
プリキュアきらきら名前チェッカーが最大値を示す名詞
を探してみた。(コードは単純なので省略する)

すると、なんと「14」というものが見つかった!!
それは・・・
シンプルニセンシリーズボリュームザビショウジョシミュレーションアールピージームーンライトテール

!?
「シンプル2000シリーズボリュームTHE美少女シミュレーションRPGムーンライトテール」
こんな単語が(mecabの)辞書の中にカタカナとして入っていたことにも驚きだが
プリキュアの名前として「ぶっちゃけありえない」(言うまでもない)

3文字~6文字に限定して実施すると、
「6」が同率で複数見つかった。
マリーロール、メリーメリー、メリメロマル、など。
意味は良くわからない。

プリキュアきらきら名前チェッカーの改善すべき点として、
単純な加減算ではなく、
文字数の長さに応じて平均値を取るなどを考慮すべきかもしれない。
(平均値にしてしまうと、今度は文字が少ない方が有利になるため、悩ましいところ)

プリキュアの中にも「マイナス1」までは複数いるため、
これ以上は最大値を追求せずに、
「マイナス1以上が妥当と分かった」という話にとどめておこう

【5】オマケ:Word2Vecによる今後のプリキュア名の推定

上述までで、一旦当初目的は達成済みであるが、
意地で、次回作以降のプリキュアの名前をプログラムで算出してみる。

Word2Vecという技術を使う(いつものヤツね)
Word2Vecについては以下ご参考:

【続】AIが三国志を読んだら、孔明が知力100、関羽が武力99、を求められるのか?をガチで考える物語(Word2Vec編)

「赤の他人」の対義語は「白い恋人」 これを自動生成したい物語

長くなるので詳細は割愛するが、
ポイントは、以下のような関数を作って、
各単語に対して、歴代プリキュア名との
「単語としての類似度の平均」を求めて、
それを高い順にリストアップするというもの。
(但し、3文字~6文字で、
 プリキュアきらきら名前チェッカーの結果がマイナス1以上に限定)

Word2Vecによるプリキュア類似度判定
from gensim.models.word2vec import Word2Vec
import gensim

# 日本語wikipediaから生成したWord2Vecモデルの読み込み
model_path = 'drive/My Drive/PURI/word2vecmodel_fromWIKI_NEOLOGD01.model'
model = Word2Vec.load(model_path)

def ave_sim_do(input_str, check_str_list):
  result_val = 0
  for check_str in check_str_list:
    try:
      sim_do = model.similarity(input_str, check_str)
      #print(sim_do)
    except:
      #そのチェック対象の文字列が無い場合はゼロ扱い
      #print("KeyError", check_str)
      sim_do = 0
      pass
    result_val += sim_do
  return result_val/len(check_str_list)

ave_sim_do("スター", PRECURE_NAME_STR_LIST)
# > 0.28982034724516176

~~~他の関数は省略~~~
結果に飛ばす。

プリキュアっぽい言葉のリストアップ結果(上位のみ)
[('ピーチ', 1, 502.0, 0.4792090936253468, True),
 ('ストロベリー', 3, 232.0, 0.4721409846097231, False),
 ('レディー', 2, 287.0, 0.4621714613089959, False),
 ('マジカル', 1, 301.0, 0.45772725256780783, True),
 ('ウィッシュ', -1, 154.0, 0.45130613862226404, False),
 ('スウィート', 0, 476.0, 0.45082837815086046, False),
 ('ムーン', 2, 1311.0, 0.448010998715957, False),
 ('レディ', 1, 1399.0, 0.4460138124297373, False),
 ('シュガー', 0, 459.0, 0.44292359420408806, False),
 ('アイリス', -1, 506.0, 0.44178551333025096, False),
 ('ハニー', 0, 367.0, 0.4413684260565788, True),
 ('ファイン', -1, 221.0, 0.4410709965818872, False),
 ('ミント', 1, 572.0, 0.4408927426363031, True),
 ('パッション', 0, 238.0, 0.4392520201082031, True),
 ('キャット', -1, 288.0, 0.4371894449926913, False),
 ('ルージュ', 2, 346.0, 0.4366619746511181, True),
 ('ファイアー', -1, 255.0, 0.43606505828599135, False),
 ('マーメイド', 2, 216.0, 0.43515188035865626, True),
 ('バージン', 1, 141.0, 0.43372481496383747, False),
 ('プリティ', 1, 116.0, 0.4328366027524074, False),
 ('ミルキー', 2, 115.0, 0.43272389558454355, True),
 ('バニー', 0, 179.0, 0.43145679887384175, False),
 ('ワンダー', 1, 382.0, 0.4313376608925561, False),
 ('ウーマン', 1, 442.0, 0.431137225124985, False),
 ('クリムゾン', 1, 268.0, 0.43021388749281564, False),
 ('ブロッサム', 2, 125.0, 0.4290103747198979, True),
 ('サンシャイン', -1, 638.0, 0.42721113486525913, True),
 ('ミラクル', 2, 674.0, 0.4264821488410234, True),
 ('パンプキン', -1, 130.0, 0.425747458015879, False),
 ('ブルー', 2, 5445.0, 0.42543597308297953, False),
 ('プリンセス', 1, 1548.0, 0.4242299640551209, True),
 ('エンジェル', 0, 1468.0, 0.42417359094057855, False),
 ('ファイヤー', 0, 357.0, 0.4232226965017617, False),
 ('ダンシング', -1, 209.0, 0.42321491818875073, False),
 ('プレジャー', 2, 104.0, 0.42315283194184306, False)]

一位は「ピーチ」!
って、既に60名の中にいるじゃん。。。
ということで、右端に既存名の存在判定を付与。
「True」は既に登場している名前。
それを除外して見ていくと、
「ストロベリー」「レディー」「ウィッシュ」
「スウィート」「ムーン」「シュガー」・・・・。
プリキュアに居ても違和感の無い名前が出ているのではないか?

これは単純にWord2Vecの評価結果だけで順に並べているが、
プリキュアきらきら名前チェッカーの値との足し算や、
文字数等のルールをいろいろ決めて試すと面白い。

ざっと上位300位あたりまで眺めて、いくつかピックアップすると、
「ホーリー」「パール」「プラネット」「ラビット」
「ブリリアント」「ノワール」「ラッキー」「マーブル」
「ジャスミン」「メイプル」「ビーナス」などなどが出ていた。

このWord2Vecによる評価ならば、
「ラーメン」はプリキュア名としてイマイチ、
という評価をすることが出来る。
(※それでも0.13くらいで、下位のプリキュアより高いくらいであったが)

【6】結論まとめ

プリキュアのかわいさと、ラーメンの人気の秘密は、
「ラ行」「マ行」「ー(長音)」であり、
「ア行」「カ行」のようなふつーの音は使用を控えることが望ましい。
(プリキュア60名中59名がこのルールに準じていると言える)

また、以下の2点の評価を用いることで、「プリキュアっぽい名前」を
プログラムで探索することが出来た。(主観に基づく結論です)
 ① プリキュアにふさわしい音  = プリキュアきらきら名前チェッカー
 ② プリキュアにふさわしい意味 = Word2Vecの評価平均

備考:変身前の名前で見ても、「ラ行」「マ行」の人気は高く、
   全体的に「ラ行」「マ行」は女子力が高いのかもしれない。
   女子の名前づけに使えるノウハウを得ることが出来た。

終わりに

日本語は、母音×子音でなんとなく50音は平等というイメージがあった。
一方で、きらきら感によって音で受ける印象が違う、という点は興味深い。

「プリキュアきらきら名前チェッカー」は、
暗算でも出来る内容であるため、ぜひみなさまも
いろいろなものの名前への適用をすると面白いかもしれない。

プリキュアの美しき言葉で、
邪悪な心を打ち砕きましょう!!

以上。

後日追記:ブルボンはプリキュア以上にきらきら!

ラ行ゴリ押しの企業といえばブルボン
商品のほとんどにラ行が付いてます。

fujikenbotebote さんにコメントでお教えいただき、
気になったのでざっくり試してみたら、
さらなる衝撃の結果が生まれた。

ブルボンのきらきら度評価結果
[('アルフォート', 1),
 ('エブリバーガー', 1),
 ('エリーゼ', 1),
 ('ガトーレーズン', 2),
 ('ショコラセーヌ', 0),
 ('シルベーヌ', 1),
 ('シルベーヌ', 1),
 ('スローバー', 3),
 ('セブーレ', 2),
 ('チョコチップ', -1),
 ('チョコバーム', 1),
 ('チョコブラウニー', -1),
 ('チョコラングドシャ', -1),
 ('チョコリエール', 1),
 ('バームロール', 5),
 ('パキーラ', 1),
 ('フェットチーネ', 0),
 ('プチ', 0),
 ('ブランチュール', 3),
 ('ホワイトショコラ', -1),
 ('ホワイトロリータ', 2),
 ('マロンブラン', 3),
 ('ミルファス', 2),
 ('ラシュクーレ', 2),
 ('リッチミルク', 2),
 ('ルーベラ', 3),
 ('ルマンド', 2),
 ('レーズンサンド', 2),
 ('レーズンラッシュ', 3),
 ('ロアンヌ', -1)]

これはすごいっっっ!!!
てきとうに挙げた30項目中「-2」以下は一個も無し!
ブルボンのきらきら度平均値=「1.3」の超高水準をマーク!!
※プリキュア平均= 0.65
※全単語平均  =-0.64

ブルボンはプリキュアを超えるきらきら感があった

「なお 受け取っていない………!
 ブルボンからは 1円も……!」
by ハンチョウ ~ブルボンドラフト会議~

ほかにもいろいろ試してみたくなる。。。

【完結】AIが三国志を読んだら、孔明が知力100、関羽が武力99、を求められるのか?をガチで考える物語(完結編)

背景

本投稿は、以下2点の投稿からの続編です。

① AIが三国志を読んだら~(自然言語処理編)
② 【続】AIが三国志を読んだら~(Word2Vec編)

前回までのお話を読んでいない方は ① ② を先にご覧くだされ!

①で、三国志の小説に対して「武将名」に気をつけて自然言語の前処理を行った。
②で、Word2Vec(機械学習)により武将を「ベクトル」として扱い、
似ている度計算や、武将同士の足し算引き算等の「演算」をすることが出来た。

完結編では、「ベクトル」に何らかの数式を適用することで、
「武力」「知力」「政治」「魅力」を求められるか?というテーマ。

司馬懿「「孔明の罠」と言う人、というポジションを得て苦節何十年。。。」
司馬懿「いよいよワシの偉業が真に評価される時が来たぞ!フフフ」
司馬懿「(結果を見て)え!?」
司馬懿「ワシ、知力75だったの!?」
諸葛亮「ふっ!私は知力98(一位)だったぞ」
司馬懿「待て あわてるな これは孔明の罠だ・・・」
諸葛亮「だまらっしゃい!
諸葛亮「小説中の描写や表現によるものなのでこれが妥当なのです!!」
司馬懿「げえっ!(悶絶)」
劉禅 「わーい、朕は知力82だった、わーい!」
諸葛亮「AIが壊れているかもしれませんね
劉禅 「・・・。」

AIが見た三国志人物相関図

前回のWord2Vec偏の結論は以下2点。

  • 武将をベクトル化して演算(似ている&足し算引き算)に成功した
  • しかし、どのベクトル成分を見ても武力や知力になりそうな値は無かった

機械学習の結果は「使えるけど、人間には理解出来ない」ということ。

そこでまず、興味も含めて、
50次元ベクトル「可視化」するところから始めよう!

周瑜「見える・・・私にも敵が見えるゾ!」
周瑜「見せてもらおうか、AIの性能とやらを!」
魯粛「それ、三国志じゃなく宇宙世紀です。」
シュウユ・ビナン「修正してやるー!」
魯粛「名前を勝手に宇宙世紀っぽく修正しないでください。」
シセル・コウメイ「・・・。」
魯粛「おまえもマネするなー!!」

ニュータイプではない人々には、50次元ベクトルを知覚することは難しい
そこで、t-SNEを使って、高次元データを次元削減して2次元に落とす。

コードの前に結果を貼ると以下。
全武将たちのポジションを2次元化して見ることが出来るのだ!

人物相関図.png

50次元ベクトルの中身がどうなっているんだろう?について、
前回までは「全く分からない」であったが、
このように特徴を2次元に落としてみると、なんとなく
「陣営&時代ごと」の結びつきが強いということが見えてくる。
2次元には2次元の良さがありますね

全体的に中央が蜀(劉備3兄弟など)で、
左上が貂蝉や董卓など、右側に呉、右下が魏、
左下は後期の時代の人、曹操はちょっとズレて君主ポジション?
などの特徴が見て取れる。
なおこれは、登場回数TOP100位までの人のマッピング。

この図を作成したコードも貼り付けておく。

次元削減&可視化
import matplotlib.pyplot as plt 
from sklearn.manifold import TSNE
import numpy as np

#前回作った武将ベクトルから、名前の部分だけ取ったリストを作成する。
vocab_busyou_list = []
for Busyou_data in Busyou_data_list:
  vocab_busyou_list.append(Busyou_data[0])

#word2vecの結果をt-SNEで次元圧縮
vocab = model.wv.vocab

#登場上位300人の可視化表示
new_vocab = [{name:vocab[name]} for name in vocab_busyou_list[0:100]]
emb_tuple = tuple([word2vec_model[v] for v in new_vocab])

X = np.vstack(emb_tuple)

TSNE_model = TSNE(n_components=2, random_state=0)
np.set_printoptions(suppress=True)
TSNE_model.fit_transform(X) 

import matplotlib.pyplot as plt
import japanize_matplotlib 
#重要:日本語文字化け防止
#事前に、!pip install japanize-matplotlib を実行しておくこと

#matplotlibでt-SNEの図を描く
plt.figure(figsize=(10,10)) #図のサイズ
plt.scatter(TSNE_model.embedding_[skip:limit, 0], TSNE_model.embedding_[skip:limit, 1])

count = 0
for label, x, y in zip(vocab_busyou_list[0:100], TSNE_model.embedding_[:, 0], TSNE_model.embedding_[:, 1]):
    count +=1
    if(count<skip):continue
    plt.annotate(label, xy=(x, y), xytext=(0, 0), textcoords='offset points')
    if(count==limit):break
plt.show()

天下二十分の計(失敗)

厳白虎「ふふふ、おとうと・・・じゃなくて軍師よ。」
厳白虎「この東呉の徳王が天下を得るにはどうすれば良い?」
厳輿 「コウメイという男は天下三分の計を唱えたそうでござる。」
厳輿 「ならば我々はその五倍天下二十分の計で圧勝です。」
厳白虎「うむ!さっそく全武将を20グループに分けるのだ!」
シセル・コウメイ「五倍だと15だぞ。」
魯粛「ツッコミどころはそこじゃねーーー!!!」
魯粛「ってかいつまで "シセル・コウメイ" でいくつもり!?」

さて、最強の武を求めるためには、
今回作った50次元ベクトル空間の中で、
どの方向が「武力方向」なのかを見極める必要がある。
(知力についても同様)

前回のWord2Vec編の最後では、
あるベクトル成分が既に「武力」を示す値になっていないかな、
ということを確認したのだが、ダメであった。

そこで第二弾の案は「勇将猛将」のグループを作れば、
そのグループ全体が示す方向が武力なのではないか?という案。

例えば、
魏延、関羽、夏侯惇、太史慈・・・などの武将の集まりと、
孫乾、荀攸、張昭、張松・・・などの武将の集まりを見ると、
前者の方が「武力」が高いと言える。
武官グループ、文官グループ、などに武将を分類出来れば、
「武力」方向が出るのではないか?というアイデア。

そこで、まず武将を「分類」すると、
どのようにグループ分けがされるのか、
機械学習を用いて分類を行う、
「k-meansによるクラスタリング」を試してみた

武将を20個のグループに分割してみよう。
これが厳白虎による「天下二十分の計」である。

k-meansによるクラスタリング
from collections import defaultdict
from gensim.models.keyedvectors import KeyedVectors
from sklearn.cluster import KMeans

#前回作った武将ベクトルから、名前の部分だけ取ったリストを作成する。
vocab_busyou_list = []
for Busyou_data in Busyou_data_list:
  vocab_busyou_list.append(Busyou_data[0])

#出現頻度の高い、上位300人をクラスタリング対象とする。
vocab = vocab_busyou_list[0:300]

#クラスタリング対象を「武将名」として出現した単語のみに絞る
vectors = [model.wv[word] for word in vocab]

#クラスタの数(適宜変更して設定)
n_clusters = 20
kmeans_model = KMeans(n_clusters=n_clusters, verbose=1, random_state=42, n_jobs=-1)
kmeans_model.fit(vectors)

cluster_labels = kmeans_model.labels_
cluster_to_words = defaultdict(list)
for cluster_id, word in zip(cluster_labels, vocab):
    cluster_to_words[cluster_id].append(word)

for words in cluster_to_words.values():
    print(words)
クラスタリング結果
['曹操', '袁紹', '劉表', '荀彧', '賈詡', '張魯', '禰衡', '袁譚', '袁尚', '曹叡', '田豊', '王朗', '審配', '劉曄', '荀攸', '沮授', '華歆', '黄権', '孔融', '楊松', '張譲', '韓嵩', '何后', '張衛']
['劉備', '諸葛亮', '関羽', '張飛', '趙雲', '馬超', '徐庶', '劉璋', '孟達', '関平', '孫乾', '張松', '糜竺', '法正', '糜芳', '劉琦', '劉岱', '張任', '王忠', '周倉', '陶謙', '簡雍', '馬良', '于吉', '傅士仁', '伊籍', '趙範', '崔諒', '車冑', '姜叙', '劉恢', '甘夫人', '糜夫人', '鄭文', '劉焉', '裴元紹', '郝萌', '劉辟', '韓玄', '皇甫嵩', '鄭玄', '李恢', '周善', '呂公', '龔都', '苟安', '申耽']
['呂布', '董卓', '袁術', '貂蝉', '陳宮', '董承', '王允', '李儒', '何進', '李傕', '韓遂', '李粛', '典韋', '張角', '盧植', '楊彪', '郭汜', '韓暹', '蔡和', '王子服', '丁原', '袁煕', '曹豹', '韓胤', '黄奎', '曹嵩', '厳氏', '閔貢', '蹇碩']
['周瑜', '孫権', '孫策', '魯粛', '陸遜', '龐統', '呂蒙', '甘寧', '太史慈', '蔡瑁', '黄蓋', '張昭', '黄祖', '徐盛', '程普', '周泰', '凌統', '諸葛瑾', '蒋幹', '司馬徽', '郭嘉', '闞沢', '劉繇', '呂範', '張允', '呉夫人', '劉琮', '逢紀', '周魴', '蘇飛', '徐氏', '楊懐', '蒯良', '王甫', '孫韶', '張紘', '呂凱', '蒯越', '崔州平']
['司馬懿', '張郃', '張遼', '徐晃', '曹仁', '夏侯惇', '曹洪', '曹真', '夏侯淵', '公孫瓚', '于禁', '李典', '顔良', '張繍', '郭淮', '楽進', '華雄', '丁奉', '許攸', '李厳', '郭図', '高順', '馬遵', '郝昭', '孫礼', '牛金', '蒋欽', '宋憲', '高覧', '邢道栄', '夏侯覇', '孫桓', '司馬師', '鍾繇', '兀突骨', '种輯', '呂曠', '呂翔', '朱桓', '曹純', '秦朗', '鄒靖', '申儀', '司馬昭', '韋康', '裴緒']
['魏延', '黄忠', '姜維', '馬岱', '関興', '王平', '劉封', '張苞', '馬謖', '廖化', '楊儀', '厳顔', '馬忠', '夏侯楙', '鄧芝', '張翼', '張嶷', '夏侯尚', '呉懿', '王双', '陳式', '冷苞', '劉延', '呉班', '高翔', '鄧賢', '高沛', '費耀', '馮習']
['孫堅', '許褚', '文醜', '楊奉', '曹休', '紀霊', '潘璋', '文聘', '張宝', '王必', '張虎', '李楽', '張英', '朱然', '張南', '魏続', '何儀', '胡軫', '忙牙長', '戴陵']
['孟獲', '左慈', '高定', '孟優', '馬元義', '雍闓', '陳応', '楊鋒', '韋晃', '阿会喃', '徐栄', '成何', '朶思大王', '祝融', '楊陵']
['曹丕', '曹彰', '曹植', '蔡琰']
['龐徳', '楊阜', '張済', '楽綝', '樊稠', '韓浩', '関索', '呉蘭']
['陳登', '陳珪', '田氏', '韓馥', '耿紀']
['劉禅', '程昱', '楊修', '管輅', '満寵', '虞翻', '蔡夫人', '陳震', '蒋琬', '王威', '顧雍', '何太后', '呉碩', '呉子蘭', '董昭', '譙周']
['馬騰', '劉安', '伏完']
['吉平', '穆順', '孫翊', '崔毅']
['韓当', '陳武', '董襲']
['侯成', '淳于瓊', '厳白虎']
['蔡陽']
['呂伯奢']
['秦琪', '高幹']
['趙昂']

各行ごとに、同じタイプのスタンド使い
同じグループに属する武将が示されている。

上記の「20個」という設定値は、
いくつか値を変えて試した中で、
比較的良い分類になったように見える値なのだが、
どのグループを見ても「武官」グループとか
「軍師」グループとかは生成されず、やはり
「陣営」や「年代」的な要素による振り分けが多い印象

三国志をAIに読ませた場合は、
将軍/軍師などの役割の識別よりも、
陣営/年代の方を大きな違いと見ている
ということが改めて分かった。
(※可視化の時点でも分かっていた)

「武官」「文官」の集合体を得られなかったため、
この厳白虎&厳輿なみの浅知恵は失敗に終わった

また、これが出来ないということは、
もし人手で「武官」「文官」のグループに分けたとしても、
例えば文官側に魏が多いなどの理由で、
陣営を示すようなベクトルにしかならないということ。

■失敗から学んだ結論:
 Word2Vecのモデルで最も表面化されている要素は、
 武力・知力ではなく、各武将ごとの陣営・相性であった。
 このモデルだけでは、いくらイジっても「武力」は出にくい。
 武力・知力の数値化ではなく、相性をAIに決めさせる、
 ならば、より納得感のあるものが得やすいかもしれない。
 (※KOEI三国志では相性は隠しパラメータ)

これで完結。
星落秋風五丈原。

IFシナリオ(もしKOEI三國志データを使えたら?)

と、終わってしまっては面白くない。
100%小説のみをINPUTとして武力・知力を求めることは諦め、

「小説」⇒「数値化(ベクトル)」⇒「式を適用」⇒「武力・知力」

上記の間をつなげられるような「式」が存在するのか、
答え側から逆算することを試してみる。

答え知ってちゃズルいじゃん?と思うかもしれないが、
例えば、
KOEIに登録されていない武将の評価にはもちろん使えるし、
他にも、
「五丈原まで書かれた小説」+「答え(KOEIのデータ)」
をインプットにして「式」を見つけておけば、
「五丈原以降の小説を追加」によって、
後期の登場人物の評価が出来るかもしれない、と
「式」の見つけ方を考えるのはかなり有用である。
そもそも小説から武力知力を求めることが有用なのか?
という点は考慮の対象外とする。

例えば、迷惑メールを判別するAIを作る際に、
メールデータのみからAIが出来れば凄いが、多くは、
メールデータ + 人間が判断した迷惑メールかどうかのフラグ
を用いて「迷惑メール判定式」を作る、方が多いだろう。
正解データを見ながら式を作るほうがむしろ普通だ。

「教師無し」から「教師有り」に変えて検討を続けようという話。

今回の機械学習(Word2Vec)で作ったデータについて、
武力知力を直接取り出すことには失敗した。
その原因として、
陣営/相性などのより強く出ている要素に邪魔されたから、
なのか、
データ内にそもそも武力知力相当の値が無いから、
なのかを確認しようということでもある。

問題設定も自分で行いながら問題を解く場合は、
どんな位置づけのことを実施しているのか、
より抽象的な観点で確認しながら行わないと
すぐに道に迷ってしまう。

IFシナリオを遊ぶ場合は、どこまで真でどこからIFなのか確認して遊ぶ

伝国の玉璽(正解データ)の入手

洛陽の井戸の底から 三國志DS3攻略wiki 様から、
三國志Ⅴの武将データの一覧のCSVを作成する。
※三國志Ⅴは、PSP、三國志DS3、スマホ版等でリメイクされている。
スクレイピングなどをするまでもなく表形式になっているので、
エクセルなどに張り付けて整形すればよい。

以下のようなデータのカタマリになる。曹操様強い
'曹操', '87', '96', '97', '98'
(武力 / 知力 / 政治 / 魅力)

武将データと、これまでに作成した、
登場回数+50次元ベクトルの情報と合わせて、
pandasのデータフレームにする。

データフレームの作成
# リストをデータフレームに変換する
import pandas as pd
sangoku_df = pd.DataFrame(Busyou_data_list_with_sangoku5, columns=['Name', '武力', '知力', '政治', '魅力', '回数' ,'V01','V02','V03','V04','V05','V06','V07','V08','V09','V10','V11','V12','V13','V14','V15','V16','V17','V18','V19','V20','V21','V22','V23','V24','V25','V26','V27','V28','V29','V30','V31','V32','V33','V34','V35','V36','V37','V38','V39','V40','V41','V42','V43','V44','V45','V46','V47','V48','V49','V50'])

こんな感じのデータになる。

'曹操', '87', '96', '97', '98', 2843, 3.834890365600586, 0.6499840617179871, ・・・(50個のベクトルの各値)
曹操は、青空文庫中で2843回登場しており、登場回数もNo1。

孫堅「おお、これが伝国の玉璽 KOEIの正解データか!」

ImperialSeal.jpg

魯粛「それも伝国の玉璽ですが、ゲームが違いますっ!」

Imperial Seal / 伝国の玉璽 (黒)
ソーサリー
あなたのライブラリーからカードを1枚探す。
その後あなたのライブラリーを切り直し、
そのカードをその一番上に置く。
あなたは2点のライフを失う。

なお、これ以降はガチモードで記載させていただこう。
話が少し複雑化になってきて、
余計な会話を挟む余裕がなくなってきた。ネタが尽きた

重回帰分析

正解データを入手したので、
重回帰分析」によって、無理やり、
ベクトルから武力を求める式を作ってみようと思う。

重回帰分析とは、1つの目的変数(この場合は武力)を、
複数の説明変数(この場合は50次元ベクトルの各値)で予測するもの。

重回帰分析の性質や詳細は
よく説明出来ないのでググってくだされっ!

Word2Vecの50次元ベクトルのみを
説明変数として実行してみる。
(武力、知力、政治、魅力、で4回やる)

武力、知力、政治、魅力、の重回帰分析
from sklearn import linear_model
clf_B_model = linear_model.LinearRegression()
clf_T_model = linear_model.LinearRegression()
clf_S_model = linear_model.LinearRegression()
clf_M_model = linear_model.LinearRegression()

# 説明変数に "武力"などのパラメータ系を落としたもの を利用
# 回数をまずは説明変数に入れずにやってみる
except_quality = sangoku_df.drop(["Name","武力","知力","政治","魅力", '回数'],axis=1)
X = except_quality.as_matrix()

# 目的変数に "武力" を利用
Y_B = sangoku_df['武力'].as_matrix()
# 目的変数に "知力" を利用
Y_T = sangoku_df['知力'].as_matrix()
# 目的変数に "政治" を利用
Y_S = sangoku_df['政治'].as_matrix()
# 目的変数に "魅力" を利用
Y_M = sangoku_df['魅力'].as_matrix()

# 予測モデルを作成
clf_B_model.fit(X, Y_B)
# 予測モデルを作成
clf_T_model.fit(X, Y_T)
# 予測モデルを作成
clf_S_model.fit(X, Y_S)
# 予測モデルを作成
clf_M_model.fit(X, Y_M)

# 武力モデル(clf_B_model)についての結果をPRINTする
# ※以降、知力、政治、魅力、についても同様
# 偏回帰係数
print(pd.DataFrame({"ColName":except_quality.columns,
                    "Coefficients":clf_B_model.coef_}).sort_values(by='Coefficients') )

# 切片 (誤差)
print(clf_B_model.intercept_)

#偏回帰係数(傾き)
print(clf_B_model.coef_) 

#決定係数
print(clf_B_model.score(X,Y_B))

武力モデルについての結果表示
 ColName  Coefficients
25     V26    -61.271715
45     V46    -25.912462
8      V09    -25.092631
23     V24    -21.504212
3      V04    -19.667249
15     V16    -19.209042
30     V31    -18.987222
1      V02    -16.588763
27     V28    -15.995261
44     V45    -12.291346
39     V40    -11.599499
46     V47    -10.909173
36     V37    -10.274551
43     V44    -10.092843
16     V17     -9.463913
0      V01     -9.205292
41     V42     -8.616187
9      V10     -8.555294
26     V27     -7.228924
42     V43     -6.989891
34     V35     -6.866054
7      V08     -6.503953
10     V11     -4.793779
21     V22     -4.482499
28     V29     -4.081184
6      V07     -2.616649
17     V18     -0.037904
38     V39      0.122413
19     V20      1.920816
37     V38      2.282509
47     V48      3.396366
11     V12      3.949870
40     V41      8.039302
48     V49      8.675593
12     V13      9.706162
24     V25     10.006476
4      V05     10.381380
35     V36     11.448578
14     V15     14.993325
32     V33     18.391117
2      V03     18.407241
18     V19     18.576508
49     V50     19.626562
13     V14     19.685847
5      V06     21.965289
22     V23     23.017108
33     V34     23.734387
31     V32     23.880140
20     V21     26.807478
29     V30     32.375894
82.80934304572963
[ -9.20529151 -16.58876342  18.40724119 -19.6672492   10.38137953
  21.96528882  -2.61664864  -6.50395289 -25.09263103  -8.55529397
  -4.79377903   3.94987032   9.70616211  19.68584678  14.99332549
 -19.2090421   -9.46391312  -0.03790405  18.57650838   1.92081557
  26.80747794  -4.48249918  23.0171084  -21.50421226  10.00647568
 -61.27171489  -7.22892442 -15.99526099  -4.08118442  32.37589425
 -18.98722178  23.88014036  18.39111672  23.73438735  -6.86605449
  11.44857829 -10.27455057   2.28250868   0.1224132  -11.59949902
   8.03930208  -8.61618743  -6.98989059 -10.09284258 -12.29134613
 -25.91246161 -10.90917327   3.39636648   8.67559251  19.62656171]
0.6045441117790333

このようにして得られた
式(予測モデル)は、
各武将の値をどのように評価しているのか、
以下の関数でPredict(予測)を実行し、
結果を見てみよう!

Predictする実験
#三国志5に存在していなかったデータを含めて全てPredictする実験
predict_Busyou_data_list = []
for Busyou_data in Busyou_data_list:
    busyou_name = Busyou_data[0]
    #print(Busyou_data)

    #DataFrame形式にするため、1個の要素しかない二次元配列にしてからDataFrame化する
    df_for_predict = pd.DataFrame( [Busyou_data[2:]] , columns=['V01','V02','V03','V04','V05','V06','V07','V08','V09','V10','V11','V12','V13','V14','V15','V16','V17','V18','V19','V20','V21','V22','V23','V24','V25','V26','V27','V28','V29','V30','V31','V32','V33','V34','V35','V36','V37','V38','V39','V40','V41','V42','V43','V44','V45','V46','V47','V48','V49','V50'])
    predict_B = int(round(clf_B_model.predict(df_for_predict)[0]))
    predict_T = int(round(clf_T_model.predict(df_for_predict)[0]))
    predict_S = int(round(clf_S_model.predict(df_for_predict)[0]))
    predict_M = int(round(clf_M_model.predict(df_for_predict)[0]))

    tmp_data = [busyou_name, predict_B, predict_T, predict_S, predict_M]

    #正解データが存在する場合は、そのデータを追加
    for sangoku5_data in sangoku5_datalist:
        #名前が完全一致する場合
        if sangoku5_data[1] == busyou_name:
            #新しいマージデータの生成
            tmp_data.append([sangoku5_data[5], sangoku5_data[6], sangoku5_data[7], sangoku5_data[8]])

    predict_Busyou_data_list.append(tmp_data)


import pprint
pprint.pprint(predict_Busyou_data_list)
初期の予測結果、冒頭部抜粋
[['曹操', 72, 74, 63, 79, ['87', '96', '97', '98']],
 ['劉備', 69, 73, 63, 81, ['79', '77', '80', '99']],
 ['諸葛亮', 62, 85, 73, 86, ['60', '100', '96', '97']],
 ['関羽', 83, 68, 53, 71, ['99', '83', '64', '96']],
 ['張飛', 91, 56, 38, 71, ['99', '45', '17', '44']],
 ['呂布', 80, 50, 38, 57, ['100', '31', '9', '67']],
 ['袁紹', 68, 70, 64, 75, ['81', '77', '49', '92']],
 ['周瑜', 63, 87, 75, 84, ['78', '99', '87', '96']],
 ['孫権', 61, 87, 79, 83, ['82', '88', '79', '96']],
 ['趙雲', 87, 67, 50, 71, ['98', '88', '80', '95']],
 ['司馬懿', 79, 74, 65, 77, ['67', '99', '91', '79']],
 ['董卓', 72, 58, 56, 64, ['95', '52', '37', '69']],
 ['孫策', 76, 80, 66, 87, ['95', '83', '62', '97']],
 ['魏延', 90, 65, 50, 68, ['94', '48', '37', '56']],
 ['馬超', 87, 54, 37, 66, ['98', '40', '32', '82']],
 ['魯粛', 38, 93, 87, 92, ['61', '96', '93', '90']],
 ['黄忠', 90, 69, 55, 69, ['97', '66', '68', '86']],
 ['劉表', 51, 73, 63, 78, ['61', '71', '74', '83']],
 ['袁術', 76, 54, 46, 76, ['71', '69', '14', '83']],

左側の4つの数字が「予測結果」で、
右側の4つの数字が「KOEIの正解データ」だ。
それぞれ、武力、知力、政治、魅力、を表している。

確かにそれぽいようなデータになっている気がするが、
可もなく不可もないような数字を出しているだけ
という印象で、あまり良い結果には見えない。

しかし、ここで最後の工夫を炸裂させる。
小説データを数値化するのであれば、
最も重要な要素は「主人公補正」だ。

Word2Vec結果+主人公補正、を説明変数にしてみよう!

最終結果発表(主人公補正追加版)

小説上の登場回数」が
主人公補正」的に効いているのではないか?
という仮説のもと、
51番目の説明変数として、「登場回数」を加えて再実行した。

最終結果全行を一括でご紹介しよう!

最終結果
[['曹操', 95, 92, 87, 105, ['87', '96', '97', '98']],
 ['劉備', 89, 89, 84, 105, ['79', '77', '80', '99']],
 ['諸葛亮', 78, 98, 90, 104, ['60', '100', '96', '97']],
 ['関羽', 92, 75, 62, 82, ['99', '83', '64', '96']],
 ['張飛', 97, 61, 44, 77, ['99', '45', '17', '44']],
 ['呂布', 85, 54, 43, 63, ['100', '31', '9', '67']],
 ['袁紹', 70, 71, 66, 77, ['81', '77', '49', '92']],
 ['周瑜', 64, 88, 77, 86, ['78', '99', '87', '96']],
 ['孫権', 63, 89, 81, 85, ['82', '88', '79', '96']],
 ['趙雲', 88, 68, 51, 72, ['98', '88', '80', '95']],
 ['司馬懿', 80, 75, 67, 79, ['67', '99', '91', '79']],
 ['董卓', 72, 58, 56, 64, ['95', '52', '37', '69']],
 ['孫策', 77, 80, 66, 87, ['95', '83', '62', '97']],
 ['魏延', 91, 65, 50, 68, ['94', '48', '37', '56']],
 ['馬超', 87, 54, 37, 66, ['98', '40', '32', '82']],
 ['魯粛', 38, 93, 86, 92, ['61', '96', '93', '90']],
 ['黄忠', 89, 68, 55, 69, ['97', '66', '68', '86']],
 ['劉表', 50, 72, 61, 76, ['61', '71', '74', '83']],
 ['袁術', 74, 53, 44, 74, ['71', '69', '14', '83']],
 ['張郃', 83, 67, 59, 67, ['93', '67', '54', '69']],
 ['孫堅', 88, 60, 45, 75, ['94', '85', '59', '93']],
 ['張遼', 89, 59, 47, 66, ['95', '88', '68', '85']],
 ['貂蝉', 27, 64, 57, 73, ['26', '81', '65', '94']],
 ['孟獲', 86, 56, 37, 66, ['92', '51', '19', '67']],
 ['姜維', 68, 72, 63, 70, ['93', '96', '81', '87']],
 ['徐晃', 87, 62, 52, 70, ['93', '68', '57', '63']],
 ['陳宮', 65, 62, 55, 64, ['61', '90', '81', '69']],
 ['曹丕', 51, 87, 85, 84, ['75', '72', '77', '85']],
 ['徐庶', 39, 80, 70, 79, ['68', '97', '86', '85']],
 ['曹仁', 93, 62, 49, 78, ['85', '68', '60', '71']],
 ['許褚', 89, 53, 39, 60, ['98', '18', '21', '68']],
 ['陸遜', 69, 80, 73, 81, ['79', '97', '87', '94']],
 ['龐統', 46, 82, 66, 80, ['56', '98', '86', '85']],
 ['董承', 42, 78, 66, 79, ['74', '61', '54', '80']],
 ['龐徳', 81, 65, 49, 70, ['97', '70', '41', '72']],
 ['呂蒙', 64, 80, 72, 76, ['85', '90', '80', '84']],
 ['甘寧', 78, 66, 51, 67, ['96', '60', '34', '71']],
 ['馬岱', 90, 47, 36, 58, ['84', '49', '40', '72']],
 ['夏侯惇', 83, 68, 55, 75, ['94', '70', '81', '87']],
 ['曹洪', 74, 69, 56, 71, ['79', '45', '63', '71']],
 ['曹真', 68, 75, 70, 79, ['79', '68', '55', '74']],
 ['王允', 38, 66, 64, 64, ['29', '72', '92', '78']],
 ['劉璋', 49, 75, 71, 78, ['33', '60', '43', '85']],
 ['孟達', 59, 75, 62, 67, ['73', '72', '51', '44']],
 ['関平', 89, 58, 47, 67, ['82', '70', '52', '80']],
 ['関興', 87, 56, 51, 60, ['88', '71', '49', '75']],
 ['孫乾', 64, 62, 61, 71, ['33', '75', '79', '90']],
 ['夏侯淵', 90, 62, 50, 74, ['91', '59', '43', '82']],
 ['王平', 86, 55, 44, 63, ['79', '71', '54', '71']],
 ['太史慈', 84, 57, 40, 61, ['96', '67', '39', '72']],
 ['蔡瑁', 67, 66, 59, 64, ['80', '72', '79', '61']],
 ['張松', 50, 73, 67, 68, ['20', '92', '86', '69']],
 ['公孫瓚', 77, 66, 52, 73, ['86', '66', '56', '70']],
 ['荀彧', 52, 88, 77, 82, ['34', '98', '95', '90']],
 ['黄蓋', 64, 77, 62, 76, ['88', '67', '75', '80']],
 ['劉封', 71, 64, 51, 67, ['70', '61', '38', '70']],
 ['張苞', 73, 46, 43, 49, ['88', '42', '40', '45']],
 ['李儒', 45, 63, 67, 61, ['15', '91', '85', '44']],
 ['于禁', 77, 63, 50, 62, ['79', '71', '40', '77']],
 ['馬謖', 64, 83, 72, 80, ['67', '86', '70', '71']],
 ['何進', 51, 68, 67, 73, ['77', '43', '67', '91']],
 ['張昭', 36, 88, 89, 77, ['14', '94', '98', '84']],
 ['黄祖', 72, 53, 33, 54, ['70', '32', '57', '37']],
 ['李傕', 75, 53, 47, 59, ['68', '34', '48', '36']],
 ['賈詡', 56, 89, 81, 73, ['42', '97', '89', '68']],
 ['李典', 85, 64, 54, 83, ['78', '81', '49', '69']],
 ['徐盛', 72, 70, 63, 72, ['84', '84', '69', '78']],
 ['顔良', 102, 48, 41, 63, ['95', '35', '21', '53']],
 ['張魯', 52, 68, 60, 73, ['62', '83', '71', '93']],
 ['韓遂', 70, 58, 46, 59, ['63', '78', '63', '74']],
 ['廖化', 76, 62, 47, 64, ['69', '66', '50', '68']],
 ['陳登', 52, 72, 64, 64, ['40', '71', '70', '73']],
 ['禰衡', 41, 80, 67, 62, ['20', '92', '88', '23']],
 ['劉禅', 23, 82, 84, 89, ['7', '30', '32', '78']],
 ['袁譚', 67, 63, 61, 75, ['63', '49', '48', '74']],
 ['袁尚', 71, 50, 45, 69, ['71', '47', '42', '77']],
 ['文醜', 89, 38, 31, 54, ['97', '25', '12', '67']],
 ['程普', 72, 75, 76, 74, ['80', '80', '73', '85']],
 ['張繍', 68, 55, 45, 73],
 ['馬騰', 56, 57, 46, 72, ['91', '57', '49', '88']],
 ['李粛', 56, 66, 59, 58, ['38', '69', '68', '55']],
 ['楊儀', 52, 76, 64, 54, ['60', '71', '79', '46']],
 ['周泰', 85, 66, 56, 73, ['94', '65', '51', '70']],
 ['糜竺', 50, 68, 71, 72, ['30', '66', '79', '81']],
 ['典韋', 68, 53, 42, 57, ['98', '31', '17', '57']],
 ['楊奉', 69, 51, 45, 58],
 ['凌統', 85, 55, 42, 66, ['83', '60', '44', '61']],
 ['諸葛瑾', 52, 83, 79, 85, ['41', '91', '94', '94']],
 ['蒋幹', 52, 74, 59, 67, ['13', '67', '39', '39']],
 ['吉平', 52, 74, 65, 70],
 ['法正', 41, 77, 68, 65, ['50', '94', '89', '67']],
 ['張角', 54, 57, 44, 61, ['50', '95', '88', '99']],
 ['曹叡', 45, 86, 86, 83, ['56', '81', '74', '80']],
 ['田豊', 43, 78, 75, 62, ['47', '95', '83', '77']],
 ['程昱', 46, 86, 83, 75, ['45', '93', '86', '75']],
 ['郭淮', 74, 72, 68, 71, ['79', '72', '66', '65']],
 ['糜芳', 63, 68, 61, 56, ['68', '40', '16', '22']],
 ['劉琦', 34, 76, 73, 77, ['7', '75', '64', '75']],
 ['司馬徽', 21, 95, 74, 78, ['18', '96', '74', '78']],
 ['厳顔', 76, 63, 51, 58, ['88', '70', '63', '79']],
 ['楽進', 80, 57, 49, 69, ['80', '54', '40', '78']],
 ['劉岱', 73, 49, 36, 51, ['65', '41', '15', '33']],
 ['韓当', 87, 56, 47, 60, ['71', '64', '43', '67']],
 ['王朗', 53, 86, 81, 79, ['50', '61', '62', '63']],
 ['馬忠', 75, 48, 44, 44, ['73', '54', '30', '59'], ['60', '51', '40', '45']],
 ['郭嘉', 60, 78, 70, 82, ['32', '98', '91', '92']],
 ['曹休', 78, 59, 53, 73, ['74', '65', '54', '73']],
 ['楊修', 40, 90, 75, 85, ['25', '92', '84', '48']],
 ['闞沢', 47, 77, 72, 68, ['48', '78', '84', '73']],
 ['夏侯楙', 58, 68, 63, 73, ['47', '42', '18', '28']],
 ['盧植', 49, 72, 77, 76, ['69', '84', '75', '85']],
 ['紀霊', 80, 41, 42, 55, ['83', '33', '31', '46']],
 ['管輅', 26, 95, 76, 81, ['14', '95', '70', '73']],
 ['潘璋', 86, 43, 35, 48, ['79', '40', '23', '50']],
 ['鄧芝', 66, 65, 58, 73, ['56', '85', '79', '91']],
 ['審配', 60, 78, 74, 73, ['73', '69', '87', '75']],
 ['劉曄', 43, 86, 88, 79, ['30', '84', '77', '81']],
 ['満寵', 60, 81, 78, 84, ['57', '82', '72', '86']],
 ['左慈', 38, 84, 48, 69, ['10', '99', '21', '91']],
 ['虞翻', 40, 78, 73, 66, ['40', '78', '81', '74']],
 ['荀攸', 51, 74, 69, 64, ['49', '95', '90', '82']],
 ['楊彪', 52, 63, 65, 78, ['41', '75', '74', '83']],
 ['華雄', 80, 49, 38, 57, ['90', '29', '26', '41']],
 ['劉繇', 68, 63, 45, 69, ['20', '40', '56', '69']],
 ['張任', 77, 63, 48, 64, ['89', '75', '43', '78']],
 ['王忠', 78, 40, 23, 51],
 ['丁奉', 79, 43, 53, 58, ['84', '68', '75', '72']],
 ['高定', 73, 41, 32, 41, ['66', '36', '29', '41']],
 ['郭汜', 71, 52, 43, 69, ['65', '37', '21', '45']],
 ['呂範', 46, 79, 72, 83, ['40', '71', '75', '75']],
 ['周倉', 45, 68, 64, 68, ['82', '29', '31', '54']],
 ['沮授', 50, 85, 78, 75, ['60', '90', '91', '85']],
 ['許攸', 57, 74, 60, 68, ['42', '66', '78', '47']],
 ['張翼', 81, 55, 43, 63, ['76', '63', '52', '69']],
 ['張嶷', 86, 42, 39, 58, ['76', '70', '76', '77']],
 ['陶謙', 48, 56, 53, 64, ['31', '60', '65', '75']],
 ['文聘', 78, 44, 39, 53, ['82', '43', '65', '66']],
 ['韓暹', 71, 44, 34, 55, ['63', '30', '19', '39']],
 ['蔡和', 55, 48, 37, 49, ['51', '39', '35', '34']],
 ['華歆', 27, 79, 88, 70, ['35', '80', '85', '30']],
 ['李厳', 61, 71, 56, 72, ['87', '82', '41', '80']],
 ['曹彰', 83, 72, 58, 80, ['92', '41', '24', '79']],
 ['孟優', 70, 57, 44, 62, ['84', '27', '10', '36']],
 ['簡雍', 56, 67, 70, 61, ['36', '70', '76', '67']],
 ['王子服', 41, 72, 61, 76, ['68', '64', '61', '78']],
 ['馬良', 47, 82, 76, 87, ['28', '92', '93', '88']],
 ['曹植', 34, 79, 66, 80, ['10', '91', '80', '84']],
 ['夏侯尚', 81, 50, 46, 52, ['69', '66', '52', '61']],
 ['馬元義', 66, 36, 24, 48],
 ['侯成', 71, 60, 48, 55, ['68', '39', '40', '41']],
 ['黄権', 47, 71, 69, 69, ['46', '82', '70', '66']],
 ['呉懿', 75, 62, 51, 71, ['79', '77', '74', '78']],
 ['于吉', 47, 54, 43, 57, ['9', '99', '23', '95']],
 ['傅士仁', 56, 64, 54, 46, ['60', '48', '31', '14']],
 ['郭図', 52, 71, 66, 64, ['32', '80', '72', '43']],
 ['高順', 83, 46, 38, 54, ['84', '56', '41', '67']],
 ['蔡夫人', 44, 65, 59, 81],
 ['伊籍', 30, 68, 69, 66, ['27', '81', '88', '76']],
 ['王双', 89, 38, 32, 61, ['89', '18', '21', '32']],
 ['馬遵', 58, 75, 68, 70, ['68', '53', '47', '55']],
 ['趙範', 57, 54, 42, 60, ['38', '63', '58', '45']],
 ['崔諒', 53, 48, 51, 59],
 ['丁原', 72, 29, 21, 45, ['79', '55', '48', '69']],
 ['孔融', 58, 77, 66, 72, ['37', '89', '75', '72']],
 ['張允', 62, 61, 52, 51, ['62', '61', '59', '52']],
 ['張宝', 87, 36, 23, 59, ['82', '72', '42', '91']],
 ['呉夫人', 39, 67, 68, 73],
 ['陳珪', 21, 69, 64, 57, ['11', '71', '79', '80']],
 ['王必', 71, 48, 41, 65],
 ['車冑', 59, 51, 49, 48],
 ['劉琮', 35, 60, 64, 76, ['30', '60', '61', '78']],
 ['楊阜', 69, 53, 51, 70],
 ['郝昭', 60, 78, 68, 68, ['84', '89', '80', '85']],
 ['張虎', 79, 57, 37, 56, ['69', '53', '43', '60']],
 ['雍闓', 58, 58, 42, 57, ['73', '51', '30', '49']],
 ['李楽', 64, 60, 48, 67],
 ['張英', 79, 53, 45, 63, ['71', '37', '41', '51']],
 ['姜叙', 79, 46, 36, 73],
 ['孫礼', 64, 59, 57, 62, ['69', '67', '64', '67']],
 ['劉恢', 26, 55, 48, 62],
 ['張済', 73, 63, 56, 51, ['70', '63', '66', '72']],
 ['陳武', 76, 58, 52, 58, ['78', '60', '37', '42']],
 ['甘夫人', 51, 52, 52, 62],
 ['糜夫人', 51, 61, 50, 76],
 ['袁煕', 77, 44, 40, 72],
 ['牛金', 82, 50, 40, 62, ['81', '39', '24', '58']],
 ['楊松', 29, 67, 67, 57, ['37', '39', '57', '24']],
 ['鄭文', 58, 32, 48, 28],
 ['逢紀', 58, 68, 58, 65, ['60', '85', '61', '61']],
 ['蒋欽', 67, 58, 52, 66, ['78', '70', '40', '66']],
 ['宋憲', 63, 55, 54, 58, ['66', '43', '45', '34']],
 ['陳式', 83, 45, 32, 54, ['67', '48', '39', '25']],
 ['周魴', 52, 78, 72, 67, ['43', '79', '77', '59']],
 ['劉焉', 49, 68, 68, 74, ['36', '75', '82', '86']],
 ['田氏', 31, 49, 41, 43],
 ['高覧', 65, 49, 41, 42, ['73', '50', '39', '60']],
 ['邢道栄', 82, 54, 43, 67],
 ['冷苞', 71, 63, 50, 48, ['82', '68', '37', '23']],
 ['朱然', 71, 57, 35, 64, ['76', '40', '34', '63']],
 ['劉延', 84, 43, 29, 57],
 ['裴元紹', 69, 37, 34, 49, ['76', '37', '32', '37']],
 ['張南', 87, 28, 30, 46],
 ['蘇飛', 68, 55, 50, 67, ['70', '40', '26', '74']],
 ['韓馥', 47, 65, 74, 76, ['69', '44', '39', '66']],
 ['穆順', 49, 61, 53, 56],
 ['郝萌', 72, 59, 44, 63, ['67', '42', '34', '48']],
 ['曹豹', 65, 38, 29, 36, ['69', '12', '17', '16']],
 ['韓胤', 42, 33, 34, 46],
 ['劉辟', 81, 48, 47, 66, ['72', '27', '27', '41']],
 ['徐氏', 40, 56, 45, 57],
 ['韓玄', 55, 57, 53, 49, ['67', '31', '20', '19']],
 ['夏侯覇', 69, 66, 49, 67, ['90', '67', '72', '77']],
 ['陳応', 97, 32, 20, 47],
 ['楊懐', 46, 68, 60, 52, ['76', '43', '33', '50']],
 ['孫桓', 78, 58, 41, 65, ['75', '70', '48', '80']],
 ['司馬師', 67, 82, 76, 89, ['65', '93', '82', '76']],
 ['楽綝', 71, 60, 46, 55, ['60', '44', '30', '37']],
 ['張譲', 57, 33, 43, 39],
 ['樊稠', 65, 46, 51, 47, ['81', '26', '11', '35']],
 ['蒯良', 41, 74, 72, 55, ['27', '86', '87', '72']],
 ['陳震', 37, 66, 72, 80, ['57', '60', '71', '54']],
 ['韓浩', 89, 36, 41, 55, ['76', '42', '74', '70']],
 ['呉班', 70, 46, 37, 52, ['70', '47', '42', '61']],
 ['蒋琬', 43, 88, 91, 84, ['54', '91', '94', '84']],
 ['王甫', 47, 60, 47, 67, ['42', '75', '69', '80']],
 ['関索', 85, 62, 48, 72, ['86', '62', '47', '76']],
 ['高翔', 64, 45, 48, 45, ['61', '48', '44', '49']],
 ['皇甫嵩', 67, 30, 37, 47, ['68', '73', '77', '82']],
 ['孫韶', 56, 66, 62, 61, ['79', '59', '71', '78']],
 ['張紘', 38, 95, 91, 88, ['13', '89', '95', '85']],
 ['鍾繇', 59, 67, 64, 67, ['8', '76', '92', '56']],
 ['黄奎', 36, 74, 62, 63],
 ['鄧賢', 87, 70, 63, 70, ['68', '61', '61', '67']],
 ['兀突骨', 74, 42, 37, 51, ['88', '10', '9', '36']],
 ['王威', 67, 62, 62, 68, ['70', '59', '52', '66']],
 ['淳于瓊', 79, 52, 38, 45, ['72', '58', '45', '69']],
 ['曹嵩', 28, 67, 68, 75],
 ['孫翊', 64, 58, 49, 67, ['81', '50', '25', '66']],
 ['厳白虎', 64, 42, 35, 60, ['78', '44', '44', '49']],
 ['魏続', 89, 43, 37, 49, ['71', '43', '38', '40']],
 ['鄭玄', 26, 72, 75, 85],
 ['韓嵩', 39, 54, 56, 51, ['28', '70', '85', '51']],
 ['蔡陽', 61, 46, 57, 59],
 ['顧雍', 31, 86, 88, 74, ['11', '71', '78', '80']],
 ['李恢', 56, 57, 55, 50, ['51', '87', '79', '81']],
 ['高沛', 72, 46, 40, 60, ['61', '35', '44', '49']],
 ['周善', 67, 43, 38, 67],
 ['耿紀', 50, 47, 32, 51, ['32', '78', '71', '72']],
 ['費耀', 81, 48, 40, 48],
 ['何后', 23, 39, 60, 51],
 ['何太后', 27, 75, 68, 78],
 ['呂公', 64, 42, 41, 36],
 ['何儀', 67, 19, 8, 36, ['70', '31', '19', '24']],
 ['种輯', 51, 64, 56, 69],
 ['呉碩', 24, 80, 76, 80],
 ['龔都', 70, 45, 36, 61, ['72', '28', '40', '49']],
 ['呂曠', 63, 61, 53, 72, ['71', '25', '23', '40']],
 ['呂翔', 64, 55, 47, 61, ['72', '24', '24', '38']],
 ['朱桓', 75, 74, 55, 73, ['89', '76', '67', '80']],
 ['曹純', 72, 41, 41, 59, ['63', '63', '44', '61']],
 ['呉蘭', 90, 57, 37, 64, ['82', '41', '44', '54']],
 ['楊鋒', 66, 44, 26, 44, ['70', '34', '31', '34']],
 ['苟安', 35, 65, 55, 55],
 ['秦朗', 49, 49, 49, 56],
 ['鄒靖', 68, 56, 54, 59, ['67', '66', '47', '68']],
 ['厳氏', 28, 63, 64, 77],
 ['劉安', 22, 74, 58, 58],
 ['伏完', 41, 50, 44, 75],
 ['呉子蘭', 50, 68, 65, 71],
 ['董襲', 82, 42, 29, 47, ['70', '38', '29', '50']],
 ['張衛', 71, 51, 48, 56, ['74', '35', '33', '65']],
 ['韋晃', 58, 62, 49, 71],
 ['申耽', 81, 54, 48, 65, ['68', '59', '50', '61']],
 ['申儀', 71, 46, 52, 58],
 ['馮習', 59, 42, 24, 35, ['70', '40', '21', '61']],
 ['呂凱', 27, 74, 75, 81, ['41', '67', '77', '69']],
 ['阿会喃', 66, 39, 28, 41, ['78', '29', '23', '19']],
 ['司馬昭', 57, 70, 68, 67, ['65', '87', '90', '74']],
 ['閔貢', 49, 63, 58, 66],
 ['呂伯奢', 30, 42, 27, 37],
 ['胡軫', 70, 32, 28, 47, ['74', '12', '15', '21']],
 ['徐栄', 87, 37, 27, 54, ['56', '39', '41', '49']],
 ['董昭', 26, 72, 77, 72, ['49', '62', '75', '59']],
 ['秦琪', 82, 55, 42, 57],
 ['高幹', 70, 60, 45, 70, ['61', '61', '27', '70']],
 ['蒯越', 35, 80, 75, 76, ['29', '87', '82', '70']],
 ['崔州平', 17, 82, 62, 72],
 ['韋康', 62, 46, 42, 59],
 ['趙昂', 59, 58, 37, 70],
 ['蔡琰', 12, 73, 69, 68, ['11', '76', '80', '85']],
 ['成何', 86, 61, 52, 69],
 ['譙周', 17, 81, 81, 60, ['16', '86', '59', '63']],
 ['忙牙長', 80, 26, 21, 35, ['68', '24', '18', '17']],
 ['朶思大王', 68, 54, 37, 61, ['81', '67', '39', '48']],
 ['祝融', 65, 27, 16, 47, ['81', '14', '14', '54']],
 ['裴緒', 36, 72, 80, 69],
 ['楊陵', 56, 45, 47, 58],
 ['戴陵', 75, 14, 23, 28],
 ['蹇碩', 38, 48, 51, 44],
 ['崔毅', 27, 58, 57, 51],
 ['衛弘', 29, 75, 52, 75],
 ['耿武', 63, 55, 52, 63],
 ['呉氏', 42, 56, 47, 71],
 ['孫静', 56, 75, 74, 80, ['52', '51', '52', '64']],
 ['李豊', 68, 55, 41, 64, ['62', '62', '60', '64']],
 ['蒋奇', 82, 51, 43, 61],
 ['王累', 27, 81, 84, 77, ['26', '80', '89', '76']],
 ['李意', 17, 62, 59, 58],
 ['朱褒', 56, 46, 45, 37, ['73', '34', '32', '40']],
 ['韓徳', 77, 51, 43, 75],
 ['越吉', 76, 36, 30, 43, ['80', '15', '13', '33']],
 ['張梁', 75, 47, 38, 70, ['84', '60', '46', '89']],
 ['左豊', 61, 50, 56, 65],
 ['劉氏', 35, 64, 68, 71],
 ['牛輔', 67, 25, 33, 40, ['60', '21', '26', '37']],
 ['張闓', 66, 34, 23, 44, ['69', '19', '15', '21']],
 ['曹昂', 58, 59, 58, 76, ['67', '52', '43', '74']],
 ['史渙', 79, 58, 58, 66],
 ['朱霊', 77, 42, 41, 56, ['63', '40', '29', '41']],
 ['胡班', 57, 62, 56, 67, ['63', '52', '50', '62']],
 ['孔秀', 75, 46, 38, 49],
 ['韓福', 65, 67, 49, 45],
 ['黄承彦', 14, 80, 63, 69, ['4', '87', '21', '73']],
 ['辺洪', 53, 40, 47, 33],
 ['宋忠', 55, 61, 47, 58],
 ['楊秋', 75, 37, 24, 48, ['71', '16', '19', '31']],
 ['丁斐', 37, 67, 62, 64],
 ['閻圃', 36, 74, 80, 64, ['31', '80', '86', '77']],
 ['劉巴', 39, 75, 84, 66, ['45', '70', '67', '61']],
 ['霍峻', 53, 74, 87, 65, ['77', '65', '51', '70']],
 ['夏侯徳', 78, 48, 45, 59, ['62', '54', '45', '70']],
 ['趙咨', 32, 80, 86, 57],
 ['鄂煥', 81, 36, 25, 33],
 ['秦良', 91, 35, 24, 45],
 ['韓忠', 76, 25, 13, 27, ['69', '40', '14', '31']],
 ['董太后', 30, 40, 54, 63],
 ['鮑信', 68, 54, 46, 62, ['67', '81', '73', '78']],
 ['蔡邕', 50, 88, 84, 97, ['23', '82', '87', '79']],
 ['祖茂', 51, 49, 39, 63, ['74', '63', '58', '77']],
 ['陳生', 60, 38, 15, 28],
 ['呂虔', 71, 48, 51, 55, ['68', '29', '56', '55']],
 ['普浄', 22, 62, 84, 65],
 ['関定', 29, 41, 43, 58],
 ['焦触', 75, 64, 45, 55],
 ['公孫康', 52, 61, 41, 66, ['70', '48', '21', '71']],
 ['劉泌', 32, 75, 74, 82],
 ['陳矯', 62, 60, 59, 55, ['21', '68', '74', '70']],
 ['楊柏', 70, 37, 37, 48, ['45', '19', '26', '21']],
 ['許靖', 36, 62, 55, 69, ['21', '74', '78', '69']],
 ['楊昂', 77, 50, 44, 49, ['72', '38', '35', '42']],
 ['呉押獄', 37, 84, 74, 80],
 ['賈逵', 58, 70, 62, 71, ['47', '74', '71', '67']],
 ['范疆', 62, 59, 44, 44],
 ['邢貞', 27, 64, 70, 63],
 ['岑威', 100, 29, 26, 47],
 ['趙直', 23, 47, 53, 50],
 ['張世平', 32, 68, 67, 72],
 ['孫仲', 69, 41, 25, 44, ['62', '30', '21', '36']],
 ['張温', 36, 55, 55, 73, ['15', '61', '63', '69']],
 ['潘隠', 34, 61, 66, 48],
 ['鄭泰', 29, 58, 75, 69],
 ['王匡', 63, 51, 46, 46],
 ['袁隗', 57, 29, 33, 56],
 ['鮑忠', 82, 43, 35, 64],
 ['陳横', 49, 39, 38, 32, ['64', '48', '31', '52']],
 ['曹安民', 58, 80, 59, 76],
 ['鄒氏', 14, 64, 67, 69, ['6', '36', '42', '89']],
 ['臧覇', 67, 46, 39, 56, ['79', '44', '28', '72']],
 ['孫観', 72, 27, 26, 49],
 ['杜遠', 52, 50, 51, 66, ['62', '19', '17', '29']],
 ['王植', 37, 50, 60, 58],
 ['韓猛', 69, 26, 16, 27],
 ['劉夫人', 30, 71, 82, 73],
 ['馬延', 74, 55, 53, 64],
 ['崔琰', 37, 62, 64, 56, ['55', '70', '83', '75']],
 ['王粲', 19, 92, 94, 72, ['23', '83', '86', '67']],
 ['鞏志', 56, 36, 32, 36, ['46', '64', '58', '49']],
 ['侯選', 56, 37, 51, 47, ['66', '34', '55', '52']],
 ['杜襲', 62, 43, 47, 48],
 ['許芝', 8, 76, 103, 79],
 ['趙累', 47, 49, 52, 61, ['36', '60', '68', '78']],
 ['秦宓', 21, 83, 76, 72, ['22', '75', '61', '62']],
 ['張達', 59, 54, 38, 52, ['51', '34', '22', '24']],
 ['梁緒', 33, 76, 72, 57],
 ['尹賞', 25, 59, 64, 49, ['34', '44', '68', '44']],
 ['雅丹', 61, 66, 60, 58, ['79', '60', '54', '18']],
 ['張普', 65, 52, 43, 58],
 ['鄧艾', 73, 64, 67, 72, ['87', '94', '88', '75']],
 ['蘇双', 44, 47, 42, 56],
 ['趙弘', 50, 24, 24, 41, ['71', '42', '16', '29']],
 ['劉虞', 37, 59, 60, 59],
 ['喬瑁', 62, 38, 40, 43],
 ['黄琬', 19, 65, 71, 51],
 ['厳綱', 89, 21, 12, 30, ['68', '45', '34', '36']],
 ['胡赤児', 49, 40, 51, 35],
 ['宋果', 49, 33, 35, 54],
 ['樊能', 70, 30, 19, 39, ['61', '36', '31', '48']],
 ['楊大将', 82, 88, 90, 83],
 ['雷薄', 55, 43, 35, 48, ['65', '38', '26', '15']],
 ['胡車児', 52, 67, 46, 66, ['74', '58', '10', '69']],
 ['呉敦', 72, 7, -1, 29],
 ['毛玠', 73, 61, 62, 66, ['60', '57', '82', '60']],
 ['郭常', 41, 73, 52, 60],
 ['許貢', 61, 49, 38, 54, ['66', '64', '64', '56']],
 ['辛評', 58, 53, 52, 50, ['63', '76', '63', '66']],
 ['張顗', 73, 55, 44, 51],
 ['田疇', 60, 71, 70, 65, ['74', '52', '62', '76']],
 ['石広元', 35, 66, 56, 78],
 ['諸葛均', 30, 60, 52, 63, ['43', '77', '60', '71']],
 ['程秉', 32, 69, 70, 56, ['21', '67', '64', '75']],
 ['金旋', 77, 57, 38, 50, ['51', '46', '30', '30']],
 ['劉度', 49, 43, 37, 57, ['50', '45', '52', '70']],
 ['宋謙', 59, 50, 52, 65],
 ['賈華', 38, 40, 40, 45],
 ['楊任', 52, 54, 37, 45, ['78', '53', '40', '56']],
 ['潘濬', 35, 77, 78, 56],
 ['崔禹', 81, 20, 33, 28],
 ['傅彤', 80, 48, 47, 54, ['72', '67', '56', '69']],
 ['董允', 14, 77, 83, 56, ['19', '84', '90', '70']],
 ['曹遵', 80, 70, 65, 65],
 ['李福', 25, 66, 76, 74]]

最終結果考察

結果の総評

「主人公補正」を入れると、
かなりそれっぽい値になっている武将が増えた。

重回帰分析における「登場回数」の「Coefficients(係数)」は
武力、知力、政治、魅力、それぞれで一番高かった。
つまり、
小説から各武将の能力値を出すには、まず登場回数で概数が決まり、
Word2Vecにより武力型/知力型/君主型などのタイプが判定される
という流れとなったわけである。

言葉にしてしまえば当たり前のような話とはいえ、
小説のみを分析して得られた、全武将の「Word2Vecモデル+登場回数」に対して、
全く同じ「式(重回帰分析結果)」を適用して、
これらの武力/知力が算出されているわけで、
当初思ってたよりは良い結果が得られたのではないかと思う。

各部門のTOPの発表!!

武力一位: 顔良    =102
知力一位: 孔明    = 98
政治一位: 蒋琬 張紘 = 91(※許芝103はバグ的な)
魅力一位: 曹操 劉備 =105

顔良は、小説表現上では、関羽に負けたのはまぐれ的扱いだし、
なんども強キャラ扱いされているからだろうか?

いまいちだったやつら

呂布の武力が85なのは予想外に低かった。
(とはいえ、90以上が非常に少なくなっているため、
 決して低いわけではない。)
孫策も77であったし、
恐らく君主的な属性も持っている場合は、
方向が違うので多少減算されてしまうのだろう。

司馬懿の知力75も可哀そうである。
相対的に隣に強いやつ(孔明)がいると落ちてしまい、
比較対象が隣に居ないようなやつは上がる傾向だろうか?

他にも何名か可哀そうな武将が見受けられるが、
だいたい「ライバル」にやられているタイプに見える。

新武将(KOEIデータに無かった武将)考察

右側の4つの数字が無かった武将は、
KOEIデータに無かった武将であり、
その値は「教師データ」が無いということ。

このモデルが役立つかどうかはそれらの値を見るのが正しいのだが、
KOEIデータに無い=登場回数が少ない、のがほとんどであり、
そもそもWord2Vecモデルをまともに作れていない場合が多いため、
本来は除外して考えた方が良いようなデータが大半である。

本稿ではイチオウ全員分の結果を、
登場回数でソートした形で出している。

また、一部例外があり例えば「張繍」などは
シュウの字が小説とKOEIで微妙に別字であったため、
KOEIデータを参照出来ていない。
(KOEI側との漢字名寄せは実施していなかった)

よって、ある程度上の方に記載のデータで
KOEIとの参照が失敗している武将たちの結果が
どの程度正しく出ているか?で
結果を評価していただくのが良いと思う。

少し面白いのは、女性陣。
KOEIデータに無かったために新規扱いになっている。
どのキャラも全体的に武力が低く魅力が高い傾向にある気がする。
(祝融は除く)

結果の解釈はいろいろ

お気に入りの武将の結果はいかがだったでしょうか?

「自然言語処理編」の最初で記載したとおり、
本当にゲームのパラメータを決めたいならば、
吉川英治の1つの小説だけでなく、
複数の小説や、正史版のテキストも用意するべきでしょう。

また、AIが解釈した意味の本体は、
「Word2Vec編」で作ったモデルであり、
最後の完結編では、それを数値で表現しようとしたまで。

そもそも、呂布や孔明を除き、他の武将については、
武力、知力の数値は人間が小説を評価しても相当バラツキが生じる。
その意味では、結果が良かったのか悪かったのか、
数字だけ見て解釈することは難しい。

出た結果についても、
お気に入りの武将が高かった/低かった、等の占いレベルの楽しみ方も含め、
予想外に正しく出ていた/全然間違った結果だった、など
自由に解釈していただければ良いと思う。

最後の考察が投げやりっぽいが、
筆者的には愛しの三国武将たちと戯れることが出来て
もうおなかがいっぱいである!
好きに解釈し、好きに思いを馳せることが出来るのが
三国志の最大の魅力であろう!!

以上。

~あとがき(ポエム)~

今回の遊びプロジェクト
筆者的には「はやぶさ」のような企画であった。

というのは「はやぶさ」のミッションは
「小惑星にタッチダウンして物質を持ち帰ること」だけではない。
イオンエンジンの実験などの複数のミッションがあり、
それを「加点法で評価」している。

ご参考: 「はやぶさ」についての対談記事

もちろん「AIが三国志を読んだら」は
「はやぶさ」の偉業とは全く比較にならない内容であるが、
当初無謀なようにも思えた今回の挑戦について、
「はやぶさ」とその考え方にヒントと勇気をもらって実行した。

すなわち、最終結果(武力や知力の数字)を追うだけでなく、
途中の各ステップで得られる結果を加点法で考えることで、
予算の承認=個人プロジェクトにおけるやる気の確保、が出来た。

  • Colaboratory上でカスタマイズ版の自然言語解析を行えること
    • NEologdの辞書データも解析に組み込める
    • 自前の単語データも解析に組み込める
    • GoogleDriveやpickle活用で、Colaboratoryの時間的弱点の補強
  • 三国志の武将をWord2Vecモデルにして演算が出来ること
    • 孔明のライバルを出す
    • 孫権にとっての甘寧の計算
  • 武将同士の関係を可視化出来ること
    • 次元削減
    • クラスタリング
  • 武将の武力/知力を計算出来ること
    • 結果を考察し、Word2Vecや重回帰の特徴を得ること
  • 「げぇ関羽」「孔明の罠」を投稿中のサブタイトルにすること

イトカワから物質を持ち帰ることのように、
最後に各武将のそれっぽいステータスが出ていればそれは良いことだが、
ステータスが上手く出なかったとしてもガチで考える意義がある

唯一の誤算は、この方式で分量が増えすぎ、
特に最後の方は自分以外の人に説明しにくい内容になってきたこと。
三国志ネタを考えるのが途中から大変になってきたこと。
何か話をすることになったこと https://netadashi10.peatix.com/ 

3部合わせて大変な長文にお付き合いいただき、
(こんなところまで読んでくださった方がいたら)
本当にありがとうございました。

以上で本当に完結です。

今シンガポールにいますLineBotを作成し、記憶に残る仕事をしたい物語

「ごめん、同級会にはいけません」

強烈なインパクトを持つこのCM。
これが大好きなので、同級会に誘うと
今、シンガポールにいます」と
返事を返してくれるLineBotを作ってしまいました。
さらに、ドヤァ感をよりいっそう高める仕様をいろいろモリこみ、
地図には残らなくても、使った人の記憶に残る仕事にしたいと思います!

クソアプリ Advent Calendar 2019の4日目です。
と、書くまでもなくタイトルから漂うクソアプリ感

使い方:

 ① 同級会を開く
 ② おもむろにLineを立ち上げ「綾乃、いまどこ?」と聞く
 ③ 「ごめん、同級会にはいけません~~~以下略」と返信が来る
 ④ 「え、シンガポールだって」という感じでみんなでのぞきこむ

実行した時の様子:

キャプチャ1.PNG
※親切に、シンガポールの地図を示してくれる(地図に残る仕事)
 他にも形態素解析などの無駄な機能を満載。
 LineBot作成のノウハウもいろいろ、後述します。

お手元での実行は、こちらから友達登録をどうぞ

友だち追加
QR_min.PNG

「今、シンガポールにいます」

まだ見たことが無い人、CM本体の閲覧は以下よりどうぞ。

大成建設CM:「シンガポール」篇(youtube)
https://www.youtube.com/watch?v=HQYQ3Me2KLw

「地図に残る仕事」

さまざまな余計な機能を実装しています。
まず最初は「地図に残る仕事」です。

シンガポール!!をより強くアピールするため、
親切にもシンガポールの位置を示す地図を示すようにしました。
これで地図に残る感もバッチリです☆

さらに、国際派のAYANOは
200を超える国や地域を飛び回ります(スゴイ)
定型文以外で問いかけた場合、
彼女はシンガポールに居るとは限りません。
さまざまな場所からの返事が聞けるでしょう!
キャプチャ1-2.PNG

見慣れない国名でも場所が分かるため、
世界地理のお勉強にもなるアプリですね!
クソアプリではなかったかもしれません。

飲み会、送別会、など、様々なお誘いに対応

おっと、よく見ると上の画面キャプチャでは
「クラス会」に誘っているようです。
誘った会の内容に合わせた即レスをしてくれます!

合コン」でも「桜を見る会」でもなんでも誘ってみてください。

ラピュタの作成(地図に残らない

AYANOは地下鉄以外のものも作っています。
キャプチャ2-1.PNG

ラピュタ以外にも様々なものを作っていますよ!
他のいろいろな作成物は、ぜひご自身でお確かめくださいませ。
誰かの青春を乗せる = シータ&パズー。君を乗せて。

あなたのキーワードに反応する機能(形態素解析)

二次会などの、お誘い内容に応じた回答は行うものの、
ここまでの機能では「ランダムメッセージ表示感」があり、
もしかしたら、一緒にのぞきこんだ友人に
あれ、このAYANOってBotじゃね?
とバレてしまうかもしれません。

そこで、あなたの投げた会話のキーワードに反応して、
なんか意味深な言葉を返す機能を追加しました。(赤枠部)
キャプチャ_ラーメン.PNG
意識高い系の会話。ラーメンが懐かしい模様。

AYANOはもちろん日本語を理解出来るので、
会話の中にあがってきた「重要単語」を認識することが出来ます。
今回は、「名詞/一般」を重要だと認識していて、
それが無い場合は、「地図に残る仕事」と返信します。
上の例だと、「ラーメン」を認識しているようです。
これでAYANOさんにリアリティが生じ、記憶に残りますね!

形態素解析自体はもはや凄くも何ともない技術です。
とはいえGAE(サーバレス/AWSのLambdaのようなもの)に組み込む実装は
あまり見慣れない(少し実現が難しい)のがポイントです。
外部APIコールも無く、爆速です。超即レス☆

これで、同級会や飲み会など様々な場で、
世界中のAYANOと会話が出来るようになりました☆

仲間と一緒に使うと、生暖かい白い目で見られ、
「記憶に残る仕事」間違いなしです☆

ということで、内容の紹介はここまでにして、
実装上の工夫ポイントや、コード、
LineBot作成の様々なノウハウを以降に書きます。
#長いです。

実装上のポイント:目次/まとめ

  1. 全体方式のポイント
    1. 全体のベースとなる参考資料
    2. LineBotはHTTPSが前提。GAEで作ろう
    3. LineBotのReplyは無料。Pushは有料。Replyのみで作ろう
  2. Line Messaging API の裏ワザ
    1. 「アクセストークン」を永続的に使う裏ワザ
    2. 「接続確認」を騙す裏ワザ
    3. 「地図表示」をするちょっと複雑な裏ワザ
    4. 「wait」を実現出来なかった話
  3. GAE(Google App Engine)の制約からの逃げ方
    1. 自然言語処理は重すぎて、普通は無料では動かせない話
    2. ローカルでは動くけど、GAEでは動かない時の話

1. 全体方式のポイント

1-1. 全体のベースとなる参考資料

まず、LineBotってそもそもどんな感じに作るの?
ということで、下記のページをご参照ください。
(Lineの中の人の開発ブログの記事です。)

イメージマップメッセージを使って終電に乗り遅れないボットを作りました

ベースとなる構成はこの記事と同様に作ります。
Python + Flask(ただしPythonのバージョンは、3.6⇒3.7に変更)
そして、地図画像も記事と同様にGoogle Static Maps APIを使います。

まずは「オウム返しボット」をデプロイするのが良いでしょう。
しかし、このコードをただ書いて実行するではLineBotになりません
どのように動かせば良いのでしょうか?

1-2. LineBotはHTTPSが前提。GAEで作ろう

LineBotを作る系の情報のほとんどは、Herokuにデプロイしているようです。
なぜHerokuを使うかというと、HTTPSを簡単に使えるため、です。

LineBotを作成するためには、まずLINE Developersにログインします。
(自身のLineアカウントなどがそのまま使えます)

ログイン後「Webhook URL」を指定する箇所があり、
そのURLの指定には、HTTPSしか指定することが出来ません
また、コード内で送信用の画像ファイルを指定する際などにも、
LineMessagingAPIではHTTPS-URLで指定することが必須です。
(ローカルファイルを送ることも出来ません。必ずURLで指定します)

つまり、LineBotの開発には、HTTPSサーバの作成がほぼ必須となります。

ここで大きく三つの選択肢があります。
 ①自作サーバを立てて、HTTPS化する
 ②サーバレス/PaaSサービスを使う(Heroku、GAE、Lambdaなど)
 ③Twilio等の専用サービスを使い、サーバ部分を担ってもらう

簡単な応答botなら③が一番オススメです。
今回は自分で多少複雑なコードを書くため、①か②ですね。
①も無料で実現可能とはいえ、サーバ準備/運用が少々手間です。
そこで②が最適な選択肢となります。

恐らくLine公式で例にされていることと、上記の理由から、
ほとんどのLineBotの作り方紹介情報が
Heroku前提になっているようです。
が、本質的にはHTTPSが簡単に実現出来れば何でもいいので、
Herokuに依存する要素は一切ありません。

今回はちょっと趣向を変えて、
GAE(GoogleAppEngine)を使ってみたいと思います。
Heroku向けの記事のコードもほぼそのまま動きます。
#GAEのPython3ランライムはPython 3.7のみなのでその点は要変更です

GAE(スタンダード環境)には1 日あたり 28 時間の常時無料枠があります。
(2019年11月現在)
おいおい、1日=24時間だろうが、って思いますよね?
負荷状況に応じて複数のインスタンスが同時に立ち上がる場合や、
性能的により良いインスタンスを立ち上げた場合、
その倍数分時間がカウントされる仕組みです。
つまり、最小構成のインスタンス1台で処理できる負荷なら、
ピーク時に多少2台になっていることがあったとしても、
常時無料の枠で収まる、というイメージ。

#常時無料=GCPで初回1年間で使える3万円分の無料クーポンと別に、
 1年後以降でもずっと無料で使える利用範囲のこと。
 例えば「Qiitaの殿堂」では、IaaSにおけるこの常時無料枠を利用している。

GAEのPython3.7のFlaskチュートリアルを実行して
git cloneしてgcloud app deployするだけ、すぐ終わります)
main.pyを参考元のコードに変えればそれでほぼLineBotの完成です。
(あと、requirements.txtに、line-bot-sdk==1.14.0を追記)

1-3. LineBotのReplyは無料。Pushは有料。Replyのみで作ろう

個人開発におけるLineMessagingAPIの使い方の最大のコツは、
Replyでサービスを設計することだと思われます。

LineMessagingAPIは、Push型のメッセージ送信には従量制限があります。
無料枠では、月に1000通しか送信できません。
課金をしても、一定数を超えると一通3円~5円程度で課金されてしまいます。
(詳細は公式サイト/最新情報等をご確認ください)

しかしなんと、Replyなら無料で使えます

サービスの全体像の最初のデザインとして、
「ユーザからの発話に返信することが自然となるようなサービスデザイン」
にして、Push型の送信に依存しないような形が望ましいでしょう。

ただし、Replyはユーザの発話後一定時間のみしか使えず、
1発話に対し1回返信のみ&1回に吹き出し5つ分のみ、
しか使えないことにも注意が必要です。

例えば、「素数を数えて落ち着くアプリ」を作ろうと思った場合、
3秒ごとに13、17、19・・・などと一方的にbotが話すようなものは、
Push型になってしまいます。
次は?次は?とユーザに問いかけさせてそのたびに
23、29、などと答えさせることが自然となるように、
サービス全体のデザインを考えた方が良い、というわけです。

今回のAYANOは、「同級会に行こう!」との
誘いに対してReplyすることが極自然であるように、
サービスデザインの中におさまっていますね!

2. Line Messaging API の裏ワザ

2-1. 「アクセストークン」を永続的に使う裏ワザ

参考元記事の以下の記載の場所、
「YOUR_CHANNEL_ACCESS_TOKEN」には、

line_bot_api=LineBotApi('YOUR_CHANNEL_ACCESS_TOKEN')

LINE Developers上で、無料/何回でも発行可能な
「チャネルアクセストークン(ロングターム) 」を設定します。
「Messaging API設定」の最下部にあります。

しかし、このアクセストークンは
発行時に有効期限を設定する必要があり、
「現在のチャネルアクセストークンが無効になるまでの時間」の
プルダウンで、最大24時間までしか設定できません

通常は24時間で設定しておき、23時間ごとくらいのペースで
有効期限延長/書き換え的な処理を作る必要があるのですが、
ここで(書いて良いのか迷うレベルの)本当の裏技があります。

なんと時間=0で設定すると永続的に有効なトークンになるのです。
(2019年11月現在)

おそろしく速い手刀有効時間、オレでなきゃ見逃しちゃうね
とでも言う気持ちで、ありがたく使わせていただきましょう。
(このセリフは完全に死亡フラグ)

Lineのバグではなく、どうもアクセストークンの有効期限は、
当初は無期限だったようです。時間制限をつけるように変更中で、
その変更の過渡期?反対も多く変更に時間を要している?ようで、
そのために、基本は有効時間を設定してね、
知っている人は無限で使えちゃうけど、という状況のようです。
(ウワサ&邪推を含む。真偽不明)

このトークンの期限更新自動化の一番良い方法が分からず、
(サーバレスは基本はステートレスで作りたいし)
コイツの期限があるだけでLineBot開発はやめようか、
と思うくらいクリティカルなポイントだったため、
このまま無期限で使わせて欲しい、と切に願います

2-2. 「接続確認」を騙す裏ワザ

こっちは普通にLINE Developersさんの、
半分バグ的な内容かな、という話です。(2019年11月現在)

前述の通りLINE Developesの「Webhook URL」のところに、
HTTPSのURLとして、GAEのURLを記載することになります。
が「接続確認」のボタンを押下しても、OKになりません。

そこで、元のコードのhandle定義のところに、下記のように
特殊なreply_tokenでの処理分岐を追記します。

#以下がhandle定義
@handler.add(MessageEvent,message=TextMessage)defhandle_message(event):#Lineの「接続確認」をOKにするための特殊コード
#LineのDeveloperコンソール上で「接続確認」を押下した場合に、
#普通に作ると、reply_tokenの不正のため、500リターンになる。
#接続確認側で期待している200が返ってこないと怒られる。
#2019年11月時点でのLineのコンソール上の問題。おそらくいずれ解決する。
ifevent.reply_token=="00000000000000000000000000000000":return#以下省略

「接続確認」が通らないと動かない、ということはありません。
ただ、やっぱり気分の問題で、接続確認=OKにしておきたいですよね。
詳しくは、下記の素晴らしい先駆者様のQiita記事をご参照ください。

LINE DevelopesのWebhook URLの接続確認でエラーが出る件について

2-3. 「地図表示」をするちょっと複雑な裏ワザ

今回の開発の中で、最も分かりにくい処理が、
地図表示を行う処理、すなわち、
Google Static Maps APIの結果をLineに送る処理です。

まず、Google Static Maps APIとは、
Googleの提供している地図APIで、
GoogleMAPを静的画像にしたモノを
単純なパラメータ指定で得ることが出来ます。

凄いのは、緯度経度による指定だけでなく、
シンガポール、とか、横浜、とか
地名で指定して座標を返すことにも対応している点です。
AYANOが飛び回っている世界の国と地域の一覧は、
単純なLIST型で情報を持っています。
そのLISTにGPS座標を用意しておかなくても、
Google Static Maps APIに国名だけ投げて、
座標を教えてもらっているわけですね。楽ちん。
その他細かいオプションは適宜ググってみてください。

で、そのGoogle Static Maps APIの画像をLineに送るには?

LineMessagingAPIには、画像送信用のAPIがありまして、
送信用の画像は、httpsのURLで指定してね、
とリファレンスに書いてあります。

そして、Google Static Maps API は、
HTTPSで静的地図画像にアクセスします。

Google Static Maps APIのURLを
LineMessagingAPIの引数に指定すればいいじゃん!
⇒ハズレ。

Google Static Maps APIのURLはHTTPSです。
でも、画像ファイルがそのパスに直接「存在する」わけではなく、
Googleの中で計算されて表示されているため、
Line側から見ると、画像じゃなくね?と見えるようで、
この方法は上手くいかないようです。
(※上手くいかない正確な理由は中の人に聞かないと分かりません。
 たぶん、APIキーもURLのパラメータに含んでいるために、
 その認証とかがあって無理なんだろうと思いました)

そこで、Google Static Maps APIで作った画像だけを、
自作のサーバで(今回はGAE上で)疑似的に表示するような
コードを用意しておく必要があります。
後述のコード部の、この部分です。

@app.route("/imagemap/<path:url>/<size>")defimagemap(url,size):#以下略

参考元のベース記事でも、同じことを実施しています。
初見でコードの意図が良く分からなかったので、解説を追加してみました。

さらに任意の日本語指定地点を表示出来るように処理を加減した結果、
ポイントとなる部分だけ抜粋&コメント付与すると、
以下のようになります。

#■GoogleMap関連の設定
#GoogleのStaticMapsAPIのキーを記入する。
google_staticmaps_api_key="YOUR GOOGLE STATICMAPS API KEY"#Googleと送信時に使う大きさ指定(※Googles側では最大640*640まで)
IMAGE_SIZE=640#markers=国名を入れて自動的に検索表示する
#https://maps.googleapis.com/maps/api/staticmap?markers=color:blue|%22%E3%82%B7%E3%83%B3%E3%82%AC%E3%83%9D%E3%83%BC%E3%83%AB%22&size=300x300&zoom=3&language=jp&key=YOUR GOOGLE STATICMAPS API KEY
#input_basyoには日本語で入るため、URLエンコードを実施してから返す
defmakeMapUlr(input_basyo):map_image_url='https://maps.googleapis.com/maps/api/staticmap?markers=color:{}|{}&center={}&zoom=3&language=jp&size={}x{}&key={}'.format('blue',urllib.parse.quote(input_basyo),urllib.parse.quote(input_basyo),IMAGE_SIZE,IMAGE_SIZE,google_staticmaps_api_key)returnmap_image_url#Lineの地図表示用のメッセージに加工する関数
defmakeImagemapSendMessage(map_image_url):#Flaskのホスト名が入る模様:
request_host_name=request.hostprint(request_host_name)base_url='https://{}/imagemap/{}'.format(request_host_name,urllib.parse.quote_plus(map_image_url))print(base_url)message=ImagemapSendMessage(base_url=base_url,alt_text='世界地図',base_size=BaseSize(height=IMAGE_SIZE,width=IMAGE_SIZE),)returnmessage#受け付けたリクエストを元にGoogleから取得した画像を返す処理を行う。
@app.route("/imagemap/<path:url>/<size>")defimagemap(url,size):print("imagemap-get-called")#デバッグ用
print(url)print(size)map_image_url=urllib.parse.unquote(url)response=requests.get(map_image_url)img=Image.open(BytesIO(response.content))img_resize=img.resize((int(size),int(size)))byte_io=BytesIO()img_resize.save(byte_io,'PNG')byte_io.seek(0)returnsend_file(byte_io,mimetype='image/png')# 上記までのコードを使い、
# line_bot_api.reply_message
# の引数の、reply_messages相当を作って渡せば良い
#   (makeImagemapSendMessageのreturn値がそれ)
#
#    line_bot_api.reply_message(
#        event.reply_token,
#        reply_messages
#    )

画像の指定方法はHTTPSのURLで指定、
でもGoogleStaticMapsのURLをただ入れるだけではダメ。
これ、超ハマりどころだと思います

2-4. 「wait」を実現出来なかった話

さて、今回AYANOさんは、
同級会の誘いがあることを待っていたように、
そしてシンガポールの地図まで用意して、
超即レスでババっと返信を返してきます。
そこまで同級会の誘いに食いつき過ぎずに
10秒置きなどで自然に返すことは出来ないのでしょうか?

結論としては、出来ませんでした。(分かりませんでした)

Replyを、waitかけて2回以上実施すれば?
⇒ reply_tokenは1回しか使えないようでダメ

LineMessagingAPIに、間隔やwaitを指定出来そう?
リファレンスを見ても無さそう

さらに、もしwaitをかけてしまうと、
GAE:サーバレスと構造上相性が悪い気がします。
(課金額が大幅に上がるようなことは無いと思います。気分の問題)

一応最終手段としては、最初はReplyして以降はwait後にPushで返せば、
実現不可能ではないです。前述の通りPushはお高いので却下。

今回は、めっちゃ同級会の誘いを待っていたAYANOさん
という設定で許してください。
超即レスで、シンガポールをアピールします

3. GAE(Google App Engine)の制約からの逃げ方

3-1. 自然言語処理は重すぎて、普通は無料では動かせない話

自然言語処理×Python というと、Mecab,Janome、
また、Word2VecでGensimなどのライブラリをよく使っていました。

チャットボットを作るということで、
これらのライブラリ/機能も使いたいなー、と思います。

が、ここで大きな壁が立ちはだかります。
GAEではインストールが軽量に済むライブラリしか使えない(説明雑)

Mecabは最初から諦めていたものの、JanomeやGensimについても、
インスタンスのメモリサイズを最大に調整しても、
メモリオーバーによりGAEで使うことは出来ませんでした。
多少の工夫をした程度では、無料では全く見込み無いでしょう。

そこで、GAEの最小インスタンスでも使える形態素解析ツールとして、
igo-python」を使ってみました。
軽量でサクサク動いて素晴らしいです。

使い方はMecabやJanomeと同じで、下記のコードの通りです。
ただし今回は辞書等も拡張していないし、
形態素解析的な精度はかなり低い点、留意が必要です。

igo-pythonの使い方
fromigo.TaggerimportTagger#形態素解析:igo-pythonの初期化
t=Tagger()#形態素解析結果を文字列で返す関数
defextract_str(input_str):parsed_list=t.parse(input_str)result_str=""forminparsed_list:result_str+=m.surface+" / "+m.feature+" \n"returnresult_str#名詞-一般のみを抽出してリストにして返す関数
defmeisi_tyuusyutu(input_str):result_list=[]parsed_list=t.parse(input_str)forminparsed_list:feature_list=m.feature.split(',')#名詞-一般、助詞-連体化、助詞-係助詞などのようになる。
hinsi_info=feature_list[0]+"-"+feature_list[1]ifhinsi_info=="名詞-一般":result_list.append(m.surface)returnresult_list

GAEにデプロイする時の設定方法、
「requirements.txt」 の書き方は以下の通りです。

Flask==1.1.1
line-bot-sdk==1.14.0
igo-python==1.0.0

ということで、
GAEの最小構成のままで形態素解析まで実現出来ました!

今回は一旦精度度外視でigo-python利用としました。
もし、LineBot内で自然言語処理&精度を求めるならば、
 案① 自作HTTPSサーバを立てて自前で組み込む
 案② GAEやHerokuでやるならばCOTOHAを使う
などが正しい進め方だと思われます。

また、Word2Vec/Gensim相当をサーバレスで動かす実装については、
道半ばなので見送ります。いつか別な機会に。
(簡単なものを動かすところまでは、驚きの方法で実現。
 modelファイル自体が重いので精度とのバランスが・・・。)

3-2. ローカルでは動くけど、GAEでは動かない時の話

さて、上述の自然言語処理組み込みの実験のように、
GAEのメモリ依存で処理が落ちる場合、

ローカル開発時は動いているけど、
サーバにアップすると動かない

というケースが多発します。

また、LineBotはHTTPSサーバが必須、これも、
ローカル開発環境ではテストしにくい観点ですので、
LineMessagingAPI~GoogleStaticMapsAPIの連携近辺においては、
GAEへのデプロイ後しか確認出来ない処理が多発しました。

これらの場合、AYANOにチャットを送ると、
既読無視、を連発してきます。
デプロイのたびに既読無視されると心を折られます

これについては、あまり良い解決方法を見つけられていません。

一応、多少マシになった方法としては、
 ①print等標準出力のログは、
  GCP内の「Logging(Stackdriver)」で集約して見れるので、
  デバック時は適当な場所にprintを書いておく
 ②Line関連の問題と、それ以外の問題とを切り分けられるように、
  Lineを経由しなくても、サーバーへのGETアクセスから、
  作成した関数を叩けるように作っておく
という2点でしょうか。

下記がFlask-LineBotの主要処理です。
①②に相当する箇所に★付けでコメント記載しました。

#LineのWebHookはPOST型であり、通常のGETでのURLアクセスの場合は使われない
#以下はサーバの疎通確認用
#★このように、LineのWebHook以外で確認出来る場所を作っておき、
#  これらの場所で、確認したい関数を呼び出せば、
#  Line関連との問題の切り分けが可能。
@app.route("/",methods=['GET'])defsayhello_root():"""Return a friendly HTTP greeting."""return'[200] It works!'#LineのWebHookに設定し、メッセージの受け取り+リプライをする箇所
#ボットでメッセージを送付する場合、LineのDeveloperコンソール上で、
#「Webhook URL ※SSLのみ対応」の所に記載したURL宛に、POSTでメッセージが送付される。
#参考:https://www.casleyconsulting.co.jp/blog/engineer/3028/
@app.route("/",methods=['POST'])defcallback():# リクエストヘッダーから署名検証のための値を取得
signature=request.headers['X-Line-Signature']# リクエストボディを取得
body=request.get_data(as_text=True)app.logger.info("Request body: "+body)# 署名を検証し、問題なければhandleに定義されている関数を呼び出す。
try:handler.handle(body,signature)# 署名検証で失敗した場合、例外を出す。
exceptInvalidSignatureError:abort(400)return'OK'#以下がhandle定義
@handler.add(MessageEvent,message=TextMessage)defhandle_message(event):#Lineの「接続確認」をOKにするための特殊コード
#LineのDeveloperコンソール上で「接続確認」を押下した場合に、
#普通に作ると、reply_tokenの不正のため、500リターンになる。
#接続確認側で期待している200が返ってこないと怒られる。
#2019年11月時点でのLineのコンソール上の問題。おそらくいずれ解決する。
#参考:https://qiita.com/q_masa/items/c9db3e8396fb62cc64ed
ifevent.reply_token=="00000000000000000000000000000000":return#イベント内のメッセージを取得する
input_text=event.message.text#★標準出力は、GCP内の「Logging」に集約表示されるので、
#  要所でprint出力をしておくとデバッグしやすい
print(input_text)#メッセージを解析し、返信用の「lineのmessages」を作成する
#★Webhookやhandlerに全処理を書かずに、
#  主要処理は外部の関数で作成しておき、
#  Webhookやhandlerを通さずとも、
#  直接呼び出し&確認が出来るように作る
reply_messages=makeLineMessages(input_text)#reply_messageは、原則無料だが、
#reply_tokenは有効期間があり、あまり長時間の処理は実施出来ない。
#下記が基本形:
#line_bot_api.reply_message(
#    event.reply_token,
#    [
#        TextSendMessage(text= reply_text),
#    ]
#)
#replyして終了。
line_bot_api.reply_message(event.reply_token,reply_messages)#FlaskをGAE上(python3.7)で動かすためのサンプル:
#「gae_python37_app」をベースに作成
if__name__=='__main__':# This is used when running locally only. When deploying to Google App
# Engine, a webserver process such as Gunicorn will serve the app. This
# can be configured by adding an `entrypoint` to app.yaml.
app.run(host='127.0.0.1',port=8080,debug=True)

以上で、技術的な話題は終了です。

あとがき(ポエム)

AYANOLineBot爆誕の背景

過日、とある活動(業務外)のご縁で、Line社を見学する機会をいただきました。
とても親切にご対応/ご紹介いただきまして、大変楽しいひと時でした。
この場をかりて、再度お礼申し上げます。

その際に、LineBotに関する技術的なQAにも、詳しく答えていただきました。
本記事は、そのQAで得た知見/ノウハウの総まとめ的な記事です。
(Qiitaなどに書いて良いよ(むしろ書くことを推奨)、とのお話だったため)
LineBot開発は初めて!という時にとても悩む要点をまとめたつもりです

でもまさかQAの結果が「ごめん、同級会には行けませんbot」になるとは
全く思わなかったでしょう!ほんとうに「ごめん」です。

LineBot作成の感想

LineBotもGAEも今回初めてまともに触りました。

LineBotの作成においては「Line(bot)で返信しなければならない理由」を
どうデザインするのか?が最大のポイントと感じています。
LIFF(LINE Front-end Framework)という、
Line内にウェブアプリを組み込める機能もあるとはいえ、
Lineでもアプリを実現出来る、ではなく、Line内での実現が必須、な
デザインにしていくべきでしょう。
最もシンプルにそのデザインを達成するのは「キャラクター」です。
今回は、AYANOの存在感を増すために、Lineでの実現が必須でした。

LineBotの作り方を解説するページは多くあれど、このへんの、
なぜLineBotとして作るべきか?どうBotをデザインするべきか?
あまり語られていない印象があります。

例えば他に考えられるのは「グループチャットや友達同士での対話目的」です。
仮に「オセロ(リバーシ)のアプリ」を作る場合、
Webアプリやスマホアプリでは、友達同士での対戦がちょっと実装が大変そう。
スケジュールの共有アプリなども同様です。
Line/LIFF上で実装することによって、グループチャットにbotを呼べば、
このような課題が解決出来そうです。
AIとの対戦や、知らない人とのマッチング対戦機能を作りたいなら
Webアプリやスマホアプリで作り、友人との対戦目的ならLine上で、
など、最初にデザインを考えた方が良いでしょう。
他にも、適宜人が介入するサポート目的でbotを作る、なども方針の一つです。

こうしたLineBotの「特性」を意識して考えると、
次のアイデアが出しやすい気がしますね!!

GAE利用/自然言語処理の感想

GAE(サーバレス)で自然言語処理を扱うのは、
正直予想以上に大変でした。
自然言語処理はもともと「辞書」や「単語ごとデータ」等が必要になるので、
どこで実装するにしても「重い」ことが問題です。

例えば以前下記の記事で、思い切って
フロントエンド(JavaScript/PWA)側で実装してみたこともありました。
https://qiita.com/youwht/items/6c7712bfc7fd088223a2

フロント側に寄せると処理は最速になりましたが、
パッケージに形態素解析ライブラリが同梱されるため、
初回起動時の配布ファイルサイズが少々重いことを
課題として感じました。(あと精度もいまいち)

GAE(サーバレス)においても、結局は重さによって、
まともな精度×処理内容を組めていません。

GAE自体は、軽い処理を作る際には
インフラ不要でPythonコードだけでHTTPSサーバになり、
かなり便利です。ただやはりクセがあり、
Pythonで出来ること何でも出来る、という感じではないため、
使う際には留意する必要があるでしょう。

今回は、自然言語処理は重いとはいえ、
このレベルまでならGAE(サーバレス)で出来るのね、
という感覚をつかめたのは良かったと思います!

#つぎは COTOHA API で作るべきかな

友達が残らない仕事

「今、シンガポールにいます。」
と、あらゆるお誘いを断っていたら、
Line友達がAYANOだけになってしまいました。

本当は、AYANOはbotだと分かっているけど、
でも今はもう少しだけ、知らないふりをします。

私の作るクソアプリも、
きっといつか誰かの孤独を癒やすから

シンガポールからは以上です。

何もない所から一瞬で、自然言語処理と係り受け解析をライブコーディングする手品を、LTでやってみた話

要約

超高精度自然言語処理&係り受け解析を実施するGiNZAがすごくて、
Colaboratoryにより環境構築不要でブラウザだけでサクッと使える。

そのサクッと感を強調すべく、LT(ライトニングトーク)の最中に
その場で環境構築&コードを書いて自然言語処理、
しかも高精度&高機能ができるよ、という「手品」をやってみた。
一見スゴイが「手品」にはタネがあって・・・。という話をする。

最後まで読むと、以下の二つのノウハウが分かる
 ・GiNZAで、ゼロから3分で高精度自然言語処理する方法
 ・LTでライブコーディングする手品のタネ

背景①: GiNZAすごいっ!

2019年4月に発表された「GiNZA」という、
日本語自然言語処理オープンソースライブラリを動かしてみたら、
簡単に高精度で(超重要)、係り受けやベクトル化なども含めた、
自然言語処理全般が実施出来たので驚いた。

ご参考:
https://www.recruit.co.jp/newsroom/2019/0402_18331.html

GiNZAのすごい点1:環境構築が簡単なのにデフォで高精度

自然言語処理は、環境構築が結構大変な場合が多い。(Mecabとか)
または高精度にしようと辞書等をインストールするのが面倒である。
ところが、「GiNZA」ならば、
pip一発で高精度辞書まで含めてインストールしてくれる
さらに、Colaboratoryを使って、
全くゼロ状態から始めても、ブラウザだけで容易に動かせる

GiNZAのすごい点2: 主要機能が一発で全部入り

ただの形態素解析だけではない。
係り受け解析や、人名地名抽出、文章をベクトル化する学習済みモデルなど、
多岐にわたる主要機能が一発で使えるようになる(説明雑)

GiNZAの困った点:情報が少なく使い方が分からん

比較的新しいためか、まだ情報が少ない&探しにくい感がある。
⇒ ★本稿に主要な使い方をまとめておこうと思った★

 ・品詞が「PROPN」などのUD(Universal Dependencies)基準で分かりにくい
  (名詞、動詞、などのおなじみの表現も出せる)
 ・解析した結果が多様な属性を持つので、欲しい属性どれだっけ?状態
  (機能が豊富で使いこなせていない感)
 ・Colaboratoryで動かすのにちょっとだけノウハウが必要
  (起動方法、グラフ表示方法など、それぞれ少し調査が必要だった)
など全般的に分かれば簡単だが、最初の導入が欲しいな感。

背景②:ライブコーディングする手品

とある勉強会のLT(ライトニング・トーク)に出る機会を頂いた。
「GiNZA」のすごさをお伝えしようと思ったのだが、普通に紹介しては
「サクッと簡単に出来る感」が伝わらないし
何より「面白み」に欠けてしまう
(自然言語処理に興味のある人ばかりじゃない)

そこで、ブラウザを立ち上げただけの白紙の状態から、
ライブコーディング」で、パパっとその場で
高精度自然言語解析を実装する見世物を思いついた。
サクッと簡単に出来る感があるし、
え、これだけでこんなすごい解析出来るの!?という驚きがあるし
その場で見る価値が上がってイベントとしても面白くなる

が、トークと並行で時間内に
ミスらずコーディングするスキルなど筆者には無いっ!

そうだ!
Pythonにライブコーディングさせよう(謎)
⇒最後に、実は自動でやってました、とネタばらしでオチもつく。

ということで、
ライブコーディングっぽく見せかける手品を開発した話。

コードもあとで記載するので、GiNZA部を入れ替えれば、
どなたでも簡単にLT内でライブコーディングしているように
見せかけることが出来ます!!
なんて恐ろしいノウハウ

GiNZAのノウハウまとめ(まずマジメな話)

まずマジメにGiNZAのノウハウとして、
前提知識/前提環境一切不要で、ブラウザだけで超簡単に
高精度高機能自然言語処理する方法を記載する。

実行方法は、ブラウザで「Colaboratory」と検索して、
「PYTHON3の新しいノートブック」を開いて、
以下の100行足らずのコードを順番に実行(shift+enter)するだけ。
もちろん無料。ぜひお試しあれ。

GiNZAのインストールから、主要機能のサンプル実装まで、
全て分かるコードを作った。
(※ライブコーディング時には、これの簡素版で実施した)

GiNZAのインストール方法:

インストールはpipだけ。関連モジュールや辞書データ全て入って簡単。

!pip install ginza
# ★2020-01-15 のv3のリリースから、このようにpipだけで入るようになった模様# 従来のインストール方法は下記。(LTではこっちを実施)# !pip install "https://github.com/megagonlabs/ginza/releases/download/latest/ginza-latest.tar.gz"

Colaboratoryの特性、モジュールのパスの関係で、
pip実施後に下記のコマンドを実行する必要がある(おまじない)

importpkg_resources,impimp.reload(pkg_resources)

GiNZAの利用方法(主要機能の一発利用)

最初に、実行結果を掲載する。
このように、よく使われそうな形態素解析、
依存構造解析(係り受け)、人名地名などの抽象分類の解析、
及びその可視化を、一発で表示するサンプルコードを作った。

ginza01.PNG

上記を出力した関数がこちら。

#依存構造解析結果から、主要な要素を表示する関数
#モデルのロードは関数外で実施すること
#import spacy
#nlp = spacy.load('ja_ginza')
#easy_display_nlp(nlp, "テスト用の文章")
defeasy_display_nlp(my_nlp,input_str):doc=my_nlp(input_str)###依存構文解析結果の表形式表示
result_list=[]forsentindoc.sents:#1文ごとに改行表示(センテンス区切り表示)
print(sent)#各文を解析して結果をlistに入れる(文章が複数ある場合でもまとめて一つにしてしまう)
fortokeninsent:#https://spacy.io/api/token
#print(dir(token))
#コメントは公式サイト記載ではなく、解釈なので参考程度に。
info_dict={}info_dict[".i"]=token.i# トークン番号(複数文がある場合でも0に戻らず連番になる)
info_dict[".orth_"]=token.orth_# オリジナルテキスト
info_dict["._.reading"]=token._.reading# 読み仮名
info_dict[".pos_"]=token.pos_# 品詞(UD)
info_dict[".tag_"]=token.tag_# 品詞(日本語)
info_dict[".lemma_"]=token.lemma_# 基本形(名寄せ後)
info_dict["._.inf"]=token._.inf# 活用情報
info_dict[".rank"]=token.rank# 頻度のように扱えるかも
info_dict[".norm_"]=token.norm_# 原型
info_dict[".is_oov"]=token.is_oov# 登録されていない単語か?
info_dict[".is_stop"]=token.is_stop# ストップワードか?
info_dict[".has_vector"]=token.has_vector# word2vecの情報を持っているか?
info_dict["list(.lefts)"]=list(token.lefts)# 関連語まとめ(左)
info_dict["list(.rights)"]=list(token.rights)# 関連語まとめ(右)
info_dict[".dep_"]=token.dep_# 係り受けの関係性
info_dict[".head.i"]=token.head.i# 係り受けの相手トークン番号
info_dict[".head.text"]=token.head.text# 係り受けの相手のテキスト
result_list.append(info_dict)#作成した辞書のリストを、DataFrame形式にしてJupyter上で綺麗に表示する
importpandasaspd#pd.set_option('display.max_columns', 100)
df=pd.DataFrame(result_list)fromIPython.displayimportdisplaydisplay(df)###係り受け表示
#係り受けのグラフ形式を図示する
#Colaboratory上で直接表示するためには少々工夫を要する
#https://stackoverflow.com/questions/58892382/displacy-from-spacy-in-google-colab
fromspacyimportdisplacydisplacy.render(doc,style='dep',jupyter=True,options={'distance':90})###抽象分類の可視化
#入力した文章に特に地名等がなければ、
#UserWarning: [W006] No entities to visualize found in Doc object の警告が出る
#抽象分類の表示
ent_result_list=[]forentindoc.ents:ent_dict={}ent_dict[".text"]=ent.textent_dict[".start_char"]=ent.start_charent_dict[".end_cahr"]=ent.end_charent_dict[".label_"]=ent.label_ent_result_list.append(ent_dict)#DataFrameの表形式での表示
display(pd.DataFrame(ent_result_list))#マーキング形式での表示
displacy.render(doc,style='ent',jupyter=True,options={'distance':90})###キーワードの列挙表示
#接頭/接尾などが加わった形で出してくれる
forchunksindoc.noun_chunks:print(chunks,end=", ")

実行方法はこちら

#使い方サンプル
importspacynlp=spacy.load('ja_ginza')target_str="権兵衛さんのあかちゃんが風邪ひいた。東京特許許可局。"easy_display_nlp(nlp,target_str)

形態素解析結果と主要な属性を表形式で表示し、
また、係り受けや人名地名の抽出等を図示する。
この機能を使うとどんな解析になるのか?
と最初に探索する時に活用することを想定。

GiNZAは単語/文章のベクトル演算も可能

以下のように、文章をベクトル化して、類似度計算も出来る。
(学習済みのモデルを内蔵している)

doc1=nlp('このラーメンは美味しいなあ')doc2=nlp('カレーでも食べに行こうよ')doc3=nlp('ごめん、同窓会には行けません')print(doc1.similarity(doc2))print(doc2.similarity(doc3))print(doc3.similarity(doc1))>0.8385934558551319>0.6690503605367146>0.5515445470506148>#食べ物系の二つが最も似ている。

単語のベクトル化も同様に実現出来る模様。
token.has_vectorでベクトル情報を所持しているか確認するのと、
token.lemma_で基本形に戻すことを考えたほうが良いかも。
GiNZAの学習済みモデルについては
まだよく見ていないので、あとで確認したい。

GiNZAのノウハウは以上。
前提知識/前提環境一切不要。
ブラウザでColaboratoryを開いて上記のコードを順番に実行するだけ。
だれでも超簡単に、高精度&多機能の自然言語解析が実現できます!

(使用例)NHKから国民を守る党構文の解析も一発

「NHKから国民を守る党構文」は、
何から何を守っているのか全く理解出来ないですね。
でも、ご安心ください。
これで一発で分かります。そうGiNZAならね

target_str="NHKから国民を守る党からNHKを守る党からNHKから国民を守る党を守る党からNHKから国民を守る党からNHKを守る党を守る党"easy_display_nlp(nlp,target_str)

nhkkoubun.PNG

※NHKから国民を守る党構文を人間として理解したい方は
 下記の素晴らしい記事をご参照ください。
https://qiita.com/MirrgieRiana/items/da7dade622770a04d8f7

(オマケ)上記サンプルを作るまでに戸惑った点メモ

Colaboratory上での利用に戸惑う

pipインストールするだけでは実行時にエラーになってしまう。
一部の先輩方の情報では、Colaboratoryを再起動しろ、
など方法も見受けられ、前述の方法に辿り着く前に右往左往した。
ただ、よく見たら前述のコードがGiNZA公式サイトにも書いてあった。

当初、この方法が分からずに、後述の
nlp = spacy.load('ja_ginza')の部分を、
nlp = spacy.load(r'/usr/local/lib/python3.6/dist-packages/ja_ginza/ja_ginza-2.2.0')
のように変えて、直接パスを通す方法を勝手に編み出してしまった。
一応余計なコードの実行不要でコレでも動くことをご報告しておく。
!find / | grep spacy | grep data
でGiNZAのColaboratory上でのインストール先パスを調べ、
spacy.load時に直接その絶対パスを指定する方法だ。

品詞が「PROPN」などのUD基準なのに戸惑う

いくつかの実行例では、
「ほら、この単語がPROPNとして解析できました!」
みたいなので説明終了になっていた。
(「名詞」とか「動詞」とか言ってくれないと全く分からんw)

これらのUniversal Dependencies という分類が
国際的には標準らしく、私が不勉強なだけなのだが、
日本語的な分類も併記したり、対応を調べたりした。

また、解析後に使える属性情報が多すぎたので、
dataframeの表形式にして、見やすくした点はちょっと工夫。

「displacy.render」がColaboで表示されずに戸惑う

係り受けや人名地名抽出のグラフ図表示(displacy.render)を普通に使うと
Serving on http://0.0.0.0:5000 ...
などと、表示用のサーバが立ち上がり、
そのサーバにアクセスして図を見る、という流れになるようだが、
Colaboratory内で直接表示させるようにオプションを設定した。

GiNZAのノウハウについてはここまで。

誰でも簡単にライブコーディングっぽく見せかける手品

さて、ここからは本当は紹介したくない悪い子の世界
まずは、こんな感じにライブコーディングを実演したんだよ、
っていうgif動画をご覧ください。

ライブコーディング手品の様子(環境構築のpip部分省略)
ginza_demo_itibu.gif

イベントの結果

「手品」と宣言していたにもかかわらず、
「本当にライブコーディングしているように見えてしまった」らしい。

「いや、それ実は打ってないでしょw」みたいなツッコミを期待し、
多少手抜きをしたウソっぽさを入れていたものの、
「簡単にバレるのもツマラナイよな・・・」と、
余計なコダワリも入れすぎたため。
意外とすーぱーえんじにぁっぽく見えた模様(違
ふつーはこんなアホな実装をする人はいないから

数行で簡単に高度なことが出来るGiNZAと、
自動ライブコーディングの相乗こうかは ばつぐんだ!

オートでライブコーディングする仕組みを作ってみたのでございます。

実装方針は、自動と手動の程よいブレンド

主要な方針としては、pyautoguiを使った自動タイピング。
pyautoguiによる自動化の詳細は下記の記事と同様なのでご参照されたし。
「写経」を自動化し、オートで功徳を積める仕組みを作ってみたのでございます。

ただし、これでは「全自動化」であり、
ライブ感が全くない無い。
全自動でひたすらコードを書いていくだけでは、
アドリブが効かないし、トークとの乖離が生じてしまう。

そこでもう一つの方針として、
キーボードイベントハンドリングが重要である。

特殊コマンド(今回は、ctrl+Q にした)を押すたびごとに、
1セル分のコードを自動タイプする。
つまり、各セルを書き始める際の一瞬の手動感と、
なにより、各セルの「実行」だけ「手動」で行うのが重要だ!

この自動と手動の程よいブレンドによって、
コーディング/タイピングがオートになる一方、
「ENTERキーを強打する快感」だけが手動で残るというワケ。
トークで解説等をしながら実行していくため、
LTなどのイベント進行にもピッタリである。

キーボードイベントハンドリングの実装方法

keyboardというライブラリを使う。
※これは、Colaboratoryではなく、
プレゼンするためのPC側で実装/実行する点は注意

pip install keyboard

このライブラリの仕様詳細は下記をご参照。
https://github.com/boppreh/keyboard#keyboardon_press_keykey-callback-suppressfalse

keyboardライブラリの基本的な使い方

最も重要なポイントは以下。(詳細は後述の全コードを参照)

keyboard.add_hotkey('ctrl+q',my_auto_func,args=(1,))

このようにホットキーを追加しておき、
ctrl+qが押されるたびに、
pyautoguiを使って作った自動タイピング関数(my_auto_func)を動かす。

タイピングしたい内容は予め文字列でリスト化しておき、
my_auto_func内では、それを順番に出力させればよい。
(実行1回目は1セル目向けの文字列、2回目は2セル向けの文字列をタイプ)

keyboardライブラリで困った点

「半角/全角」のキーを押すとバグる模様。
hotkeyが解除されてしまうようである。

当初、自然言語処理の解析対象とする文章は、
手動入力しようと思っていた。
ただこの問題が解決出来ず、このさい、
全タイピングをオートでやってしまうことにした。
(実行の shift + enter だけ手動♪)

Pyautoguiで悩んだ点/こまった点

USキーボード以外では、「:」を入力しようとすると、
「Shift+:」キーが送信され「*」が入力されてしまう。
ご参考: https://teratail.com/questions/79973

上記のリンク先を参考に一応対策をしたものの、最終的には、
下記のようにtypewriteコマンドで1文字ずつ打っていくのをやめて、

やめた実装=個別に文字列をタイピング
pyautogui.typewrite("abcdefg",interval=0.1)

下記のようにクリップボードを操作/経由して、
ひたすらctrl+vだけで張り付けていく形で実装した。

採用実装=クリップボード経由で全て出力
#クリップボードに文字をコピーしておく
pyperclip.copy(cp_str)#すべての文字列は貼り付けて登録
pyautogui.hotkey('ctrl','v')

「半角/全角」キー問題のため、日本語も入力したいとなれば、
オートで写経、のようなGijiHenkanを作るのは面倒だし
クリップボード経由のほうが楽に作れるためだ。
変換を経由せずに直接日本語が出て不自然なのは、今回は許容する。

また、Colaboratoryでは
「改行」を打つと自動インデントされてしまうため、
クリップボード以外のキーエミュレート的な方法の場合は、
タイピングするコード文字列を加工しないとインデントがずれる。
クリップボード経由方法ではそれを考慮せずに、
元のコードをそのまま引数にコピペして良いのは大きなメリット♪

さあ、これで誰でもLTでライブコーディングできる!?

と思ったのだが、一度作ってみて自動タイピングを眺めてみると、
最大の誤算があった

完全に等間隔で1文字ずつ出現するため、
全く人間味が感じられないのだ!

お経のごとく同じペースでタイピングが進んでいく。
お経の方をを作っていた時には全く気づかなかった

バレる前提(最後に自分でバラすし)でウケを狙うといっても、
いとも簡単にバレてしまっては面白くない。

ということで自然な雰囲気になるようにチューニングを考えた。

自動タイプのチューニング(失敗例)

当初考えた方法は、
pyautogui.hotkeypyautogui.typewrite
タイピング間隔をランダムにするために、
intervalの設定や、sleepを、乱数で入れる方法。

しかし、タイプが遅くなりすぎるという問題が生じた。
pyautoguiには「最小の操作間隔」が存在するようで、
1操作ごとに必ず一定以上の待ちが入る。
1文字ごとの操作だとタイプが遅くなりすぎてしまうのだ。

intervalやsleepなどの待つ方法、
hotkeyやtypewriteなどの打つ方法、
待ち時間乱数の設定範囲など、
いくつか組み合わせたがどうも自然に見えない。

自動タイプのチューニング(採用例)

自動タイプ処理を眺めていると、
適当な間隔を空けてタイプされていくよりも、
複数の文字が一緒に出現するほうがむしろ自然に見える、
ということに気づいた。
人間は数文字ごと単位でまとめて一瞬で打つ、という感じ。

ということで、適当な確率ごとに複数の文字を結合して
(つまりctrl+vを実行する前にクリップボードに出力用文字列を貯めて)
一括で出力するような実装とした。
詳細な実装は後述の全コードでご参照。

実際はあり得ないスピード(複数のキー入力が一括出力)でも
こちらのほうがより自然に見えたのだ。(個人の感想です)
これで、人間のタイピングに近いような感じで、
自動タイピングを実装することが出来た。

もはやもともとの自然言語処理より自然タイピングにがんばりはじめていた
テーマ決めた時点で三日前だったので危険な兆候であった

オートでライブコーディングする仕組みの全コード

このような様々な検討を経て、出来上がったのが下記のコードである。
入力対象の文字列の部分を差し替えれば、
これで誰でもLTでライブコーディングしている風デモを実現出来る!

悪用厳禁、と書いておくが、悪用しか出来なそうなコード

自動ライブコーディングの全コード
importpyautoguiimportpyperclipimporttimeimportrandom#キーボードイベントの追加
importkeyboard#https://github.com/boppreh/keyboard#keyboardon_press_keykey-callback-suppressfalse
#'''トリプルクォートで改行ありのそのまま文字列になる。
#ここに任意のコード群を記載すれば、
#ホットキーを押下するたびに、そのテキストが記載される
#日本語の疑似変換は未対応(対応は容易だが面倒なので)
my_str_list=['''!pip install "https://github.com/megagonlabs/ginza/releases/download/latest/ginza-latest.tar.gz"
''','''import pkg_resources, imp
imp.reload(pkg_resources)
''','''import spacy
nlp = spacy.load('ja_ginza')
''','''
doc = nlp("今日は東京でピザパーティー。権兵衛さんの赤ちゃんが風邪ひいた。")
''','''for s in doc.sents:
  for t in s:
    info = [t.orth_, t._.reading, t.tag_]
    print(info)
''','''from spacy import displacy
displacy.render(doc, style='dep', jupyter=True, options={'distance': 90})
''','''
displacy.render(doc, style='ent', jupyter=True, options={'distance': 90})
''','''doc = nlp("NHKから国民を守る党から国民を守る党から国民を守る党から国民を守る党")
displacy.render(doc, style='dep', jupyter=True, options={'distance': 90})
''','''doc1 = nlp('このラーメンは美味しいなあ')
doc2 = nlp('カレーでも食べに行こうよ')
doc3 = nlp('ごめん、同窓会には行けません')
''','''print(doc1.similarity(doc2))
print(doc2.similarity(doc3))
print(doc3.similarity(doc1))
''',]#グローバル変数
now_counter=0#主要実行用関数(引数は未使用)
defmy_auto_func(arg_val):globalnow_counter#ホットキー設定に使ったキーが押しっぱなしのまま処理が進むのを防止。少しwait
#特に、コントロールキー等を押しっぱなしの場合、別の動作が入りやすいので注意
time.sleep(1.5)print("called: "+str(now_counter))#キーが押されて出力する際に、次の出力すべきものが無い場合は処理終了
ifnow_counter>=len(my_str_list):print("END: finish")exit()cp_str=""#コピペ版
formy_charinmy_str_list[now_counter]:#緩急をつけるために、特定条件なら複数文字同時貼り付け使用にした。
sl_time=random.uniform(-0.03,0.10)cp_str+=my_charifsl_time<0:#貼り付けを実行せず継続
continueelse:#クリップボードに文字をコピーしておく
pyperclip.copy(cp_str)#すべての文字列は貼り付けて登録
pyautogui.hotkey('ctrl','v')#貼り付けに使ったものはクリア
cp_str=""#乱数で生じたスリープ分眠り
time.sleep(sl_time)#ループから抜けたあと、残りがあれば貼り付け実行
iflen(cp_str)>0:#クリップボードに文字をコピーしておく
pyperclip.copy(cp_str)#すべての文字列は貼り付けて登録
pyautogui.hotkey('ctrl','v')#貼り付けに使ったものはクリア
cp_str=""now_counter+=1print("END: my_auto_func : "+str(now_counter))#オマケ。実演中に間違った際などに対応するための関数
#一個前にカウンタを戻して再度同じコードを書いたり、終了させたり。
defmy_sub_func(arg_val):globalnow_counterprint("called: "+"for_before")now_counter-=1#キーが押されて出力する際に、負数になっていた場合は処理終了
ifnow_counter<0:print("END: finish")exit()print("END: my_sub_func : "+str(now_counter))#以下、メインルーチン
#途中で全角半角切り替えると、ホットキー追加がバグる模様なので触らないこと。
#(半角モードで実行する)
#停止する時は、「ctrl +c」で本Python側を強制終了で良い。
if__name__=="__main__":try:#ホットキーとそのイベントを追加するのは一回のみ。
#メインホットキーの設定:使用するアプリの他のショートカットと重複しないように
keyboard.add_hotkey('ctrl+q',my_auto_func,args=(1,))#サブホットキーの設定:使用するアプリの他のショートカットと重複しないように
keyboard.add_hotkey('ctrl+i',my_sub_func,args=(1,))print("Press ESC to stop.")keyboard.wait('esc')print("esc-END")except:importtracebackprint(traceback.format_exc())exit()exit()

自動ライブコーディングの魅力とは?

LTは時間が限られている性質上、
Colaboratoryをデモするにしても、
どー考えても最初からコードを準備しておくほうが妥当であり、
ライブコーディングするなんて狂気の沙汰である。しかし、
狂気の沙汰ほどおもしろい(by アカギ)

でも実は予めコードは準備しており、危険なように見えて
セーフティという名の悦楽、安全という名の愉悦(by トネガワ)
を味わえるという仕掛け。

これで誰でも、
「一見するとすーぱえんじにあ」に見せかけることが出来るかも。

※なお本来はこのレベルの実装であれば、Windowsならば、
 「UWSC」などの自動化ツールを使ったほうが多分楽である。
 あとでMacでも使いまわせるように、Pythonで実装してみた。
 (Macでは試していない)

(オマケ)手品として演じる際のミスディレクションの方法

ミスディレクションとは、主に手品で使われる、
観客の注意を意図していない別の所に向かせる現象やテクニックのこと。

「ライブっぽく」見せるためには、観客の思い込みをうまく「作る」と良い。

 ・最初のColaboratoryの立ち上げまでは、あえて手動で行う
  ブラウザでColaboratoryとタイプして検索するなど。
 ・実行時 shift+enter のモーションを大きくして手動感をアピール
 ・自動タイプしているときに、手はキーボードの上(当たり前)
 ・いきなりライブコーディングから入るのではなく、
  コイツなら超速で実装してもおかしくないかも?と思わせるような、
  なんかすごいっぽいことをした雰囲気を出す前フリ、など

あとがき

いともたやすく行われるえげつない自然言語処理

GiNZAを使えば、
いともたやすく(ブラウザだけ/環境不要)
えげつない(高精度/多機能)
自然言語処理が出来るということが、
表現できたぜェ~ 万雷の拍手をおくれ世の中のボケども
(by レッド・ホット・チリペッパー)

今回は実はLTといわれながら時間が15分とかなり長く、
既にLTと呼ぶ範囲じゃなくね?
「面白いアイデアの出し方」
「技術でなんとかするための学習方法」
の二つのお話をさせていただいた。

その「技術でなんとかするための学習方法」として挙げた
「HB鉛筆法」を例示するため、
今回のライブコーディング実演をした。

HB鉛筆法とは?

HBの鉛筆をベキッ とへし折る事と同じようにッ
出来て当然だと思うだけの学習方法
大切なのは「認識」すること

例えば、自然言語処理なんて簡単だぜ、
出来て当然だぜ、って認識するだけで良いという学習方法。

何かのオリに(アイデアを思いつく際に)
そういえばGiNZAで調べれば「出来る」よね、って思っておくだけ。
本当に知りたければ、やろうと思ったあとで、
改めて本稿を見たり、GiNZAを調べれば良いでしょう。
今回のLTを見るだけで、すでに学習は終わっているッ!
その言葉を頭のなかに思い浮かべた時には
既に終わっているからだ
『学習した』なら使ってもいいッ!

●入門書/カリキュラム/資格試験などの学習方法
⇒ やる人はいいけど、私は途中で寝ちゃうのでダメ。つらい。

●手を動かして自分で何か作る学習
⇒ 最終的にはコレかもしれない。
 ただ、人間の時間が有限である以上、全ては無理。
 やるモチベーションは何か、も問題。

●遅延評価勉強法
⇒ 必要になったらやる、はHB鉛筆法に近いが、
 「もし学べばどんなことが出来そうか?」の情報が無いと、
 アイデアを考える時に発想が出てこない。
 また、「必要になったら」というのが、
 やらされ感があるため、表現だけ、個人的に好みではない。

●HB鉛筆法
⇒ 本稿で言うと、GiNZAの実行結果の画像だけ見て知っておく、
 という程度の学習方法。
 これくらい3分でできるのね、って認識だけしておく。
 メリットは、楽であること。
 詳細を見る時間がなくても、漠然と何が出来るか分かるので、
 未学習の技術も含めてアイデアを思いつきやすくなる効果がある。
 後日、アレを学ぼうかな?というモチベーションが生じやすくなる。

空気を吸って吐くことのように!
スタンドを操るという事はできて当然と思う精神力なんですぞッ!
まさに、世界を支配する学習方法

まとめ

ということで今回は、
「ブラウザだけで高精度自然言語解析が出来るんだと認識しよう!」
「LTで半自動ライブコーディングが実演出来るんだと認識しよう!」
という二つの例を紹介させていただいた。

みなさまにアイデアの天啓がおりる時、
本稿が多少でもプラスに働けば、こんなに嬉しいことはない。

最後に、GiNZA開発者の方々と、
イベント関係者の方々に、多大なる感謝を捧げます。

以上です。

「メントスと囲碁の思い出」をCOTOHAさんに要約してもらった結果。COTOHA最速チュートリアル付き

メントスと囲碁の思い出

メントスには深い思い入れがある。
当時あたしは囲碁同好会に参加しており、
碁石を弾いて相手の碁石を落とすという
「シューティング囲碁」に興じていた。
平安貴族も興じていたに違いない、
そのみやびな遊びを理解できない同好会会長は怒り狂った。
曰く、大切な碁石が割れるからやめろ、と。
碁石は割れたら直せない。
割れたら証拠隠滅のために食べてしまえばいい?
それならば、最初から「食べられる碁石」を採用してはどうか?
よく見ると「メントス」は碁石として最適な条件を兼ね揃えていた。
色、ツヤ、形、味、どれも通常の碁石より優れている。
専門店にいかなくても、コンビニで補充が出来る。
白と黒の淡泊な世界に飽きた際には、
色を変えることもできる。
唯一の問題は、夏の暑さに弱そうなところくらいであろう。
囲碁は、相手の石を取ることが出来る。
しかし、取ったからといって何が嬉しいのだろうか。
ちょっとゲームが有利になるだけだ。
メントスならばおいしく食べちゃってもいい。
囲碁を強くなろうという熱意も出てくるというもの。
あたしは同好会会長にこう直訴した。
碁石を全てメントスに変えよう!
いま、コンビニでメントスを見かけなくなったのは、
このあたしのアイデアでメントスの値段が高騰してしまったからである。
ああ、あたしのいとしいメントスよ、どこへいった!?

本投稿の内容

  • ある日、詩想がわき「メントスと囲碁の思い出」というポエムが生まれた。
  • 意味が分からない、と言われてしまい悲しかった。
  • 偉大なる芸術家は往々にして当時の人々には理解されないものだ。
  • 【Qiita x COTOHA APIプレゼント企画】で、COTOHAには要約機能があることを思いだした。
  • そういえば【今シンガポールLineBot】の最後で、COTOHAを触ろうと思っていた。
  • そうだ、このポエムをCOTOHAさんに読んでもらって要約してもらおう!(大迷惑)
  • ついでにCOTOHAさんの主要機能を5秒で試せる最速チュートリアルコードを作成
  • COTOHAさんで誰でも簡単に自然言語処理が出来るようになったよ。(イマココ)

COTOHAさんにポエムを読んでもらった結果

■ COTOHAさんによる要約結果:

ここから~

碁石を全てメントスに変えよう!
いま、コンビニでメントスを見かけなくなったのは、
このあたしのアイデアでメントスの値段が高騰してしまったからである。

~ここまで(改行は追加)

COTOHAさんすごいっ!!(実感)

要約機能以外にもCOTOHAには様々なAPIがある

構文解析、固有表現抽出、照応解析、キーワード抽出、
類似度算出、文タイプ判定、ユーザ属性推定、感情分析、要約
・・・などなど。

無料ユーザでも、各APIごとに1000回/日使えるそうだ。

試してみたいけど、リファレンスみながら作るのも面倒なんだよなー。
一瞬で全部動かせる&内容を理解出来るサンプル無いかなー。
と、あたしが欲しかったのでまとめてみた。(後述に全コード記載)

無料ユーザ登録後、Colaboratoryにアクセスして、
「ファイル」⇒「Python3の新しいノートブック」
⇒本稿のコードをコピペして実行 ⇒ これだけで主要API全てが分かる

前提環境一切不要。ブラウザだけで実行できて、
すぐに使える&応用開発が出来るサンプル。

Colaboratory(Googleの無料Python実行環境)を使わなくても、
Pythonがインストール済みの環境ならば、
下記のコードを1つのPythonファイルに書いて、
python Mentos_Cotoha.pyなどと実行しても、
すぐ同じ結果が得られる。

ユーザ登録後、コピペだけで一瞬で試せるので、
ぜひお手元で試していただきたい。
Qiitaとの企画でプレゼントやっているので触るなら今がチャンス?

さっそく無料ユーザ登録はこちらから。
COTOHA API Portal

COTOHA最速チュートリアルサンプルコード

Colaboratoryでも、ローカル環境でも、
どこでもコピペだけですぐ実行できるサンプルコード。
 (※ユーザIDとシークレットキーは自分で入力)
サンプルを試したあとは、
API実行を扱いやすくするライブラリとして利用可能

参考: https://qiita.com/gossy5454/items/83072418fb0c5f3e269f

Mentos_Cotoha.py
importosimporturllib.requestimportjsonimportcodecs# 「自然言語処理を簡単に扱えると噂のCOTOHA APIをPythonで使ってみた」
# https://qiita.com/gossy5454/items/83072418fb0c5f3e269f
# 上記記事のコードをベースに、Colaboratoryですぐ動くように改変。
# また、同じ処理を共通化してコード全体を短くし
# 複数のAPIのサンプルコードも追加した。
# 本改変後コードの詳細は下記参照。
# https://qiita.com/youwht/items/16e67f4ada666e679875
# COTOHA API操作用クラス
classCotohaApi:# 初期化
def__init__(self,client_id,client_secret,developer_api_base_url,access_token_publish_url):self.client_id=client_idself.client_secret=client_secretself.developer_api_base_url=developer_api_base_urlself.access_token_publish_url=access_token_publish_urlself.getAccessToken()# アクセストークン取得
defgetAccessToken(self):# アクセストークン取得URL指定
url=self.access_token_publish_url# ヘッダ指定
headers={"Content-Type":"application/json;charset=UTF-8"}# リクエストボディ指定
data={"grantType":"client_credentials","clientId":self.client_id,"clientSecret":self.client_secret}# リクエストボディ指定をJSONにエンコード
data=json.dumps(data).encode()# リクエスト生成
req=urllib.request.Request(url,data,headers)# リクエストを送信し、レスポンスを受信
res=urllib.request.urlopen(req)# レスポンスボディ取得
res_body=res.read()# レスポンスボディをJSONからデコード
res_body=json.loads(res_body)# レスポンスボディからアクセストークンを取得
self.access_token=res_body["access_token"]#各APIの共通処理
defcallCotohaApiCommon(self,url,data):# ヘッダ指定
headers={"Authorization":"Bearer "+self.access_token,"Content-Type":"application/json;charset=UTF-8",}# リクエストボディ指定をJSONにエンコード
data=json.dumps(data).encode()# リクエスト生成
req=urllib.request.Request(url,data,headers)# リクエストを送信し、レスポンスを受信
# 以下、リクエストエラー時のリトライ処理を入れたが、
# 長時間放置後に再実行、などが無ければ消しても良い。
# リトライカウントを設定
CONNECTION_RETRY_COUNT=2forcounterinrange(1,CONNECTION_RETRY_COUNT+1):try:res=urllib.request.urlopen(req)#exceptionに行かなければループ終了
break# リクエストでエラーが発生した場合の処理
excepturllib.request.HTTPErrorase:# ステータスコードが401 Unauthorizedならアクセストークンを取得し直して再リクエスト
ife.code==401:print("[RETRY] get access token")self.access_token=getAccessToken(self.client_id,self.client_secret)headers["Authorization"]="Bearer "+self.access_tokenreq=urllib.request.Request(url,data,headers)# 401以外のエラーなら原因を表示して処理自体を終了。空白を返す。
else:print("<Error> "+e.reason)return""# それ以外のエラーは不明なので終了。
# サーバ系のエラーの場合、だいたいURLか引数を見直すと良い
exceptExceptionase:print(e)return""# レスポンスボディ取得
res_body=res.read()# レスポンスボディをJSONからデコード
res_body=json.loads(res_body)# レスポンスボディから解析結果を取得
returnres_body# 構文解析API
defcallParseApi(self,sentence):url=self.developer_api_base_url+"nlp/v1/parse"data={"sentence":sentence}returnself.callCotohaApiCommon(url,data)# 固有表現抽出API
defcallNeApi(self,sentence):url=self.developer_api_base_url+"nlp/v1/ne"data={"sentence":sentence}returnself.callCotohaApiCommon(url,data)# 照応解析API
# document は string / array(string) なので文でも、文のリストでも良い
# ほかのdocument箇所も同様
defcallCoreferenceApi(self,document):url=self.developer_api_base_url+"nlp/v1/coreference"data={"document":document}returnself.callCotohaApiCommon(url,data)# キーワード抽出API
defcallKeywordApi(self,document):url=self.developer_api_base_url+"nlp/v1/keyword"data={"document":document}returnself.callCotohaApiCommon(url,data)# 類似度算出API
defcallSimilarityApi(self,sentence1,sentence2):url=self.developer_api_base_url+"nlp/v1/similarity"data={"s1":sentence1,"s2":sentence2}returnself.callCotohaApiCommon(url,data)# 文タイプ判定API
defcallSentenceTypeApi(self,sentence):url=self.developer_api_base_url+"nlp/v1/sentence_type"data={"sentence":sentence}returnself.callCotohaApiCommon(url,data)# ユーザ属性推定API
defcallUserAttributeApi(self,document):url=self.developer_api_base_url+"nlp/beta/user_attribute"data={"document":document}returnself.callCotohaApiCommon(url,data)# 感情分析API
defcallSentimentApi(self,sentence):url=self.developer_api_base_url+"nlp/v1/sentiment"data={"sentence":sentence}returnself.callCotohaApiCommon(url,data)# 要約API
# sentenceのサイズは、5~5000
# sent_lenのサイズは、1~100
defcallSummaryApi(self,sentence,sent_len):url=self.developer_api_base_url+"nlp/beta/summary"data={"document":sentence,"sent_len":sent_len}returnself.callCotohaApiCommon(url,data)#上記以外のAPIや、各APIのオプションを追加/変更するのも同様にやればOK
if__name__=='__main__':#####各自の設定を記入する箇所#####
#登録完了直後のページに書いてあるよ
CLIENT_ID="ココニアイデイカク"CLIENT_SECRET="ココニシークレットカク"#BASE_URLは、公式ページでは、末尾にnlpを含めていないけど、
#少し前の技術記事だと、末尾にnlpも含めている人も多いから気を付けてね
DEVELOPER_API_BASE_URL="https://api.ce-cotoha.com/api/dev/"ACCESS_TOKEN_PUBLISH_URL="https://api.ce-cotoha.com/v1/oauth/accesstokens"#####以下使用例#####
# 各APIのコール方法や、レスポンスの解析の詳細はリファレンスを見るといいよ。
# https://api.ce-cotoha.com/contents/reference/apireference.html
# COTOHA APIインスタンス生成
cotoha_api=CotohaApi(CLIENT_ID,CLIENT_SECRET,DEVELOPER_API_BASE_URL,ACCESS_TOKEN_PUBLISH_URL)print("■ 構文解析")sentence="すもももももももものうち"api_result=cotoha_api.callParseApi(sentence)forchunksinapi_result['result']:print(chunks)print("■ 固有表現抽出")sentence="私は今日母と東京タワーでメントスを食べた"api_result=cotoha_api.callNeApi(sentence)forchunksinapi_result['result']:print(chunks)print("■ 照応解析")document=["太郎は友人です","彼のおじいさんがくれた初めてのキャンディ","それはメントスで私は四歳でした"]api_result=cotoha_api.callCoreferenceApi(document)forchunksinapi_result['result']['coreference']:print(chunks['referents'])print("■ キーワード抽出")document=["ペットボトルに入ったダイエットコーラの中にメントス数粒を一度に投入した際に急激に炭酸が気化し、泡が一気に数mの高さまで吹き上がる現象をメントスガイザーと呼ぶ"]api_result=cotoha_api.callKeywordApi(document)forchunksinapi_result['result']:print(chunks)print("■ 類似度算出")sentence1="メントスはおいしい"sentence2="ラーメンも大好きです"api_result=cotoha_api.callSimilarityApi(sentence1,sentence2)print(api_result['result']['score'])print("■ 文タイプ判定")sentence="質問を質問で返すんじゃない"api_result=cotoha_api.callSentenceTypeApi(sentence)print(api_result['result']['modality'])#declarative(叙述)
#interrogative(質問)
#imperative(命令)
print("■ ユーザ属性推定")document=["やめられないとまらない","君が泣くまで殴るのをやめない"]api_result=cotoha_api.callUserAttributeApi(document)print('結果 = {}'.format(api_result['result']))print("■ 感情分析")sentence="その味は甘くてクリーミィでこんな素晴らしいメントスをもらえる私はきっと特別な存在なのだと感じました"api_result=cotoha_api.callSentimentApi(sentence)#他のAPIの結果についても、各結果をjsonのまま見たい場合は下記のようにする。
result_formated=json.dumps(api_result,indent=4,separators=(',',': '))print(codecs.decode(result_formated,'unicode-escape'))print("■ 要約")sentence='''タイトル:メントスと囲碁の思い出
メントスには深い思い入れがある。
当時あたしは囲碁同好会に参加しており、
碁石を弾いて相手の碁石を落とすという
「シューティング囲碁」に興じていた。
平安貴族も興じていたに違いない、
そのみやびな遊びを理解できない同好会会長は怒り狂った。
曰く、大切な碁石が割れるからやめろ、と。
碁石は割れたら直せない。
割れたら証拠隠滅のために食べてしまえばいい?
それならば、最初から「食べられる碁石」を採用してはどうか?
よく見ると「メントス」は碁石として最適な条件を兼ね揃えていた。
色、ツヤ、形、味、どれも通常の碁石より優れている。
専門店にいかなくても、コンビニで補充が出来る。
白と黒の淡泊な世界に飽きた際には、
色を変えることもできる。
唯一の問題は、夏の暑さに弱そうなところくらいであろう。
囲碁は、相手の石を取ることが出来る。
しかし、取ったからといって何が嬉しいのだろうか。
ちょっとゲームが有利になるだけだ。
メントスならばおいしく食べちゃってもいい。
囲碁を強くなろうという熱意も出てくるというもの。
あたしは同好会会長にこう直訴した。
碁石を全てメントスに変えよう!
いま、コンビニでメントスを見かけなくなったのは、
このあたしのアイデアでメントスの値段が高騰してしまったからである。
ああ、あたしのいとしいメントスよ、どこへいった!?
'''api_result=cotoha_api.callSummaryApi(sentence,1)print('結果 = {}'.format(api_result['result']))# Mentos(メントス)は商標または登録商標です。
実行結果
構文解析{'chunk_info':{'id':0,'head':1,'dep':'P','chunk_head':0,'chunk_func':1,'links':[]},'tokens':[{'id':0,'form':'すもも','kana':'スモモ','lemma':'李','pos':'名詞','features':[],'dependency_labels':[{'token_id':1,'label':'case'}],'attributes':{}},{'id':1,'form':'も','kana':'モ','lemma':'も','pos':'連用助詞','features':[],'attributes':{}}]}{'chunk_info':{'id':1,'head':3,'dep':'D','chunk_head':0,'chunk_func':1,'links':[{'link':0,'label':'other'}]},'tokens':[{'id':2,'form':'もも','kana':'モモ','lemma':'桃','pos':'名詞','features':[],'dependency_labels':[{'token_id':0,'label':'nmod'},{'token_id':3,'label':'case'}],'attributes':{}},{'id':3,'form':'も','kana':'モ','lemma':'も','pos':'連用助詞','features':[],'attributes':{}}]}{'chunk_info':{'id':2,'head':3,'dep':'D','chunk_head':0,'chunk_func':1,'links':[]},'tokens':[{'id':4,'form':'もも','kana':'モモ','lemma':'桃','pos':'名詞','features':[],'dependency_labels':[{'token_id':5,'label':'case'}],'attributes':{}},{'id':5,'form':'の','kana':'ノ','lemma':'の','pos':'格助詞','features':['連体'],'attributes':{}}]}{'chunk_info':{'id':3,'head':-1,'dep':'O','chunk_head':0,'chunk_func':0,'links':[{'link':1,'label':'other'},{'link':2,'label':'adjectivals'}]},'tokens':[{'id':6,'form':'うち','kana':'ウチ','lemma':'内','pos':'名詞','features':['連用'],'dependency_labels':[{'token_id':2,'label':'nmod'},{'token_id':4,'label':'nmod'}],'attributes':{}}]}固有表現抽出{'begin_pos':2,'end_pos':4,'form':'今日','std_form':'今日','class':'DAT','extended_class':'','source':'basic'}{'begin_pos':6,'end_pos':11,'form':'東京タワー','std_form':'東京タワー','class':'ART','extended_class':'','source':'basic'}照応解析[{'referent_id':0,'sentence_id':0,'token_id_from':0,'token_id_to':0,'form':'太郎'},{'referent_id':1,'sentence_id':1,'token_id_from':0,'token_id_to':0,'form':'彼'}][{'referent_id':0,'sentence_id':1,'token_id_from':0,'token_id_to':2,'form':'彼のおじいさん'},{'referent_id':1,'sentence_id':2,'token_id_from':0,'token_id_to':0,'form':'それ'}]キーワード抽出{'form':'投入','score':18.75872}{'form':'気化','score':11.889}{'form':'泡','score':10.9335}{'form':'ペットボトル','score':10.38192}{'form':'急激','score':10.2795}類似度算出0.71831065文タイプ判定declarativeユーザ属性推定結果{'age':'20-29歳','earnings':'-1M','hobby':['MOVIE','SHOPPING'],'location':'関東','moving':['RAILWAY'],'occupation':'会社員'}感情分析{"result":{"sentiment":"Positive","score":0.740555365045455,"emotional_phrase":[{"form":"素晴らしい","emotion":"P"},{"form":"甘くて","emotion":"PN"},{"form":"もらえる","emotion":"P"},{"form":"きっと特別な","emotion":"PN"}]},"status":0,"message":"OK"}要約結果碁石を全てメントスに変えよう!いま、コンビニでメントスを見かけなくなったのは、このあたしのアイデアでメントスの値段が高騰してしまったからである。

オーラをメントスに変える念能力

記事を書いているうちに、
あたしもメントスが具現化出来るようになってきたかもしれない。

まずメントスを具現化しようと決めてからはイメージ修行だな。
最初は実際のメントスを一日中いじくってたな。とにかく四六時中だよ。
目をつぶって触感を確認したり何百枚何千枚とメントスを写生したり、
ずーっとただながめてみたりなめてみたり、音を立てたり嗅いでみたり、
メントスで遊ぶ以外は何もするなと師匠に言われたからな。
しばらくしたら毎晩メントスの夢を見るようになって
その時点で実際のメントスをとりあげられた。
そうすると今度は幻覚でメントスが見えてくるんだ。
さらに日が経つと幻覚のメントスがリアルに感じられるんだ。
重さも冷たさもすれあう音も聞こえてくる。
いつのまにか幻覚じゃなく、自然と具現化したメントスが出ていたんだ。
それ以外はおそらくゴン達と同じだよ。とにかく毎日毎日纏と練だ。

ゴレイヌのゴリラの具現化修行よりはマシ

ヒトコト or フタコトで言うと・・・

Mentosの具現化
sentence='''まずメントスを具現化しようと決めてからはイメージ修行だな。
最初は実際のメントスを一日中いじくってたな。とにかく四六時中だよ。
目をつぶって触感を確認したり何百枚何千枚とメントスを写生したり、
ずーっとただながめてみたりなめてみたり、音を立てたり嗅いでみたり、
メントスで遊ぶ以外は何もするなと師匠に言われたからな。
しばらくしたら毎晩メントスの夢を見るようになって
その時点で実際のメントスをとりあげられた。
そうすると今度は幻覚でメントスが見えてくるんだ。
さらに日が経つと幻覚のメントスがリアルに感じられるんだ。
重さも冷たさもすれあう音も聞こえてくる。
いつのまにか幻覚じゃなく、自然と具現化したメントスが出ていたんだ。
それ以外はおそらくゴン達と同じだよ。とにかく毎日毎日纏と練だ。
'''api_result=cotoha_api.callSummaryApi(sentence,1)print('結果 = {}'.format(api_result['result']))api_result=cotoha_api.callSummaryApi(sentence,2)print('結果 = {}'.format(api_result['result']))
実行結果
結果いつのまにか幻覚じゃなく、自然と具現化したメントスが出ていたんだ。結果最初は実際のメントスを一日中いじくってたな。いつのまにか幻覚じゃなく、自然と具現化したメントスが出ていたんだ。

参考リンク

◇構文解析・照応解析のデモ
https://api.ce-cotoha.com/demo?query=いつのまにか幻覚じゃなく、自然と具現化したメントスが出ていたんだ
◇APIのリファレンス
https://api.ce-cotoha.com/contents/reference/apireference.html
◇コトハ イチバン ユウメイ キジ スゴイ
https://qiita.com/Harusugi/items/f499e8707b36d0f570c4
◇COTOHA APIポータル(無料登録はこちらから)
https://api.ce-cotoha.com/contents/index.html
◇COTOHAのコミュニティ、Slack等へのリンク
https://api.ce-cotoha.com/contents/community.html

あとがき

"しゃべりすぎた翌朝 落ち込むことの方が多い"

見てごらん よく似ているだろう

ポエムを書いた次の日の朝と。

あたしは紙をくしゃくしゃと丸めて投げ、

メントスをほおばるのだった。

了。

声に出して読みたい美しいPython用語18選。R18例文付き

背景

冷凍マグロ系スクリプト言語として知られるPythonは、
美しい名前のパッケージが沢山あることでも有名です。

  • PyPy(ぱいぱい)
  • pypan(ぱいぱん)
  • pypants(ぱいぱんつ)

参考: 声に出して読みたい7つのPython用語
http://doloopwhile.hatenablog.com/entry/20120120/1327062714

これらの美しい名前のパッケージに魅せられ、
どれくらい美しい名前のパッケージが存在するのか、
本気で調べてみることにしました。

参考先の情報は2012年と少し古いですし、
今再度探せば、さらに美しい名前が見つかるに違いありません!!

調査の方針と概要

Pythonのパッケージ管理システム = pipの対象パッケージは全て
PyPI(ぱいぱい)に登録されています。
https://pypi.org/
※ぱいぱい、と読むかどうかは流派があるようです。

総数実に「219,370」個!(※2020年2月現在)
とても人手で確認出来る量ではありません。

全く使われていない休眠パッケージは除外したいため、
直近1年間で一回以上のインストールがあるパッケージ
を対象にしたいと思います。
例えば、参考先サイトが掲げている、
Pychinko(ぱいちんこ)
は既にこの世に存在しないようで、除外されますし、
Pyzuri(ぱいずり)
も残念ながら全くダウンロードが無いようで、除外されます。

こういった全パッケージ名称と、そのダウンロード情報は、
pypinfoBigQueryを使うことで取得可能です(詳細後述)。

パッケージ名称は英数字であるため、
無理やりカタカナ読みする日本語変換処理を行います。
(パッケージ名は単純な英単語ではないため、
 これが結構難しい処理になります)

最後に、予め作っておいた「美しい言葉リスト」を用いて
日本語化したパッケージ名称に対して検索をかけます。

このような地道な努力によって、
オモシロイ美しい名前のパッケージを
大量に見つけることができました!

結果発表!!

コードの前に、さきに結果をご紹介します。
いろいろ見つかったのですが、18個に選定しました。
参考先の記事が平成版だとすると、
令和版18選、略して「R18」と呼ぶことにします。

ぱいそんの美しきネーミングセンスを、例文付きでご堪能ください。

※ふりがなは、正式な読み方ではなくあくまで、
 今回作成したカタカナ読み変換ツールによる自動付与結果です。

sexmachine(せっくすましん)

直近1年間で、31,001 DL
名前が女性か男性かを判断するツールです。

4月になったら新人プログラマーに大声で教えてあげましょう。
例:分からない時は【sexmachine】に聞け!

methanal(めすあなる)

直近1年間で、163 DL
仮数フォームおよびユーティリティウィジェットライブラリです。

4月になったら職場で叫んでみましょう。
例:休日はずっと【methanal】をいじっていたよ

thefuck(ざふぁっく)

直近1年間で、64,492 DL
コンソールコマンドのエラーを修正するツールです。
https://github.com/nvbn/thefuck

エラー発生時に、とにかく「ファッ〇!!」と
驚きの声を上げてしまう方に、自動対応してくれて人気がある模様。

4月になったら兎に角、よんでみましょう。
例:【thefuck】【thefuck】【thefuck】!!

pyzure(ぱいずり)

直近1年間で、427 DL
Microsoft Azure SQL DBにデータを簡単に送信するPythonパッケージです。
https://github.com/dacker-team/pyzure

本家のpyzuriは消えたものの、新たな逸材を発見しました。

4月になったらみんなに話してあげましょう。
例:昨晩【pyzure】にトライして、良かったなあ

askocli(あすくおしり)

直近1年間で、78 DL
リモートのAskOmicsにデータを挿入するためのcliツール、とのこと。

4月になったら優しく諭してあげましょう。
例:挿入するときはまず【askocli】だよ

stockings(すとっきんぐす)

直近1年間で、71 DL
完全なメッセージの送受信を可能にするWindows / Linux、
Python 2および3互換のソケットラッパー。

4月になったらこっそり打ち明けましょう
例:実はいま【stockings】を使っているんだ

osex(おせっくす)

直近1年間で、34 DL
詳細不明です。ドキュメントが無いため扱いにくいかもしれません。

4月になったら相談してみましょう
例:【osex】にハマって、困ってるんです

pypi-cli(ぱいぱい-しり)mypypi(まいぱいぱい)

直近1年間で、488 DL、109 DL

ぱいぱい系は大量にあるため、全量は記載出来ません。
きっと使い勝手の良いパッケージも多いことでしょう。

4月になったら絶賛しましょう
例:【mypypi】は最高だ!

※pypi-rankings(パイパイ-ランキングス)とかもありました。
 例:昨日はずっと【pypi-rankings】を見ていたんだ

pypants(ぱいぱんつ)pypandas(ぱいぱんだす)

直近1年間で、570 DL、1,114 DL

4月になったら大きな声で宣言しましょう
例:わたしはいつも【pypandas】

pants(ぱんつ)fancypants(ふぁんしーぱんつ)

直近1年間で、535 DL、40 DL

4月になったら同僚に紹介してあげましょう
例:ぼくの【fancypants】を見せてあげるよ!

doraemon-robotframework(どらえもん-ろぼっとふれーむわーく)

直近1年間で、512 DL
受け入れテストとロボットプロセス自動化(RPA)のための汎用自動化フレームワーク
=「robotframework」を青タヌキの形にしたもののようです?

4月になったら宿題を忘れても怖くありません
例:困ったときは【doraemon-robotframework】に頼むことにするね

baka(ばか)

直近1年間で、49 DL
Pyramidのコアを使用するWebアプリケーションフレームワーク?のようです。

4月になったら試してみましょう
例:パソコンに【baka】を入れてみたんだ

hncomments(えっちんこめんつ)

直近1年間で、52 DL

4月になったら何となくつぶやいてみましょう
例:【hncomments】。ふふふ

sexytime(せくしーたいむ)sexy-fun(せくしーふぁん)

直近1年間で、52 DL、25 DL

4月になったら今後の期待を語りましょう
例:これから【sexytime】をはじめようか!

感想(かんそう)

英語からカタカナにするところが一番大変だったのですが、
英語の時点で既にパワーワードが多かったです。

4月になったら、ぜひ実際に職場や学校などで
声を出して読み上げてみましょう。
周囲の方はきっと「春の訪れ」を感じると思います


以下は技術的な詳細なので多くの人は見なくてよいと思います。
興味のあるかたはぜひご参照ください。

美しい名前のパッケージ群を、真面目に紹介しており、
かつ、その取得用コードを解説している本記事
「検閲/削除」されることは全く心配していません

しかし、心が汚れたオトナが見ると、
本来の意図とは別な意味に受け取ってしまうかもしれません。

諸般の事情につき本記事は
不意に消えてしまう可能性がある点、あしからずご了承ください。
消える前にぜひお手元でお試しくださいませ。

①パッケージ一覧&ダウンロード数情報取得(pypinfo)

pipのパッケージが登録されているPyPI(ぱいぱい)では、
その統計情報のデータセットを
Google/BigQueryで公開しています。
その情報を容易に取得出来るツールが
pypinfoです。

BigQueryを操作するために、
下記のサイトの手順に従って、
https://github.com/ofek/pypinfo
Google Cloud Platform(GCP)のアカウントと、
認証情報(JSONファイル)を作る必要があります。

JSONファイルまで作成したら、ブラウザで
Colaboratory(https://colab.research.google.com/?hl=ja)
を立ち上げて、以下のようにコマンドを実行していきましょう。

GoogleDriveをマウントします。
fromgoogle.colabimportdrivedrive.mount('/content/drive')
今回の作業フォルダを作ります。
!mkdir"drive/My Drive/PYPI"#さきほど作成した認証用のJSONファイルをここにアップロードしましょう。
pypinfoをインストールします。
pipinstallpypinfo
認証用のJSONファイルのパスを指定して、認証情報を取得します。
!pypinfo--auth"/content/drive/My Drive/PYPI/YourGCPProjectName-XXXXXXXXX.json"
pypinfoの疎通確認(こんな感じに「request」のダウンロード数が取得出来ます)
!pypinforequests#Served from cache: False
#Data processed: 67.70 GiB
#Data billed: 67.70 GiB
#Estimated cost: $0.34
#
#| download_count |
#| -------------- |
#|     61,319,474 |

他にも、国ごと、バージョンごと、インストール先OSごとなど、
様々な情報を取得出来るので、公式サイトの例に従って試してみましょう。

上記の「Estimated cost: $0.34」にあるように、
BigQueryでは、クエリを投げるごとに、
読み取ったデータ量に応じて課金が生じることには注意が必要です。
が、1 TB/一か月のAlways Free枠と、
新規GCPユーザ用の300$/年の無料枠があるため、
通常の使い方では大丈夫でしょう。
全量取得系の重いクエリだけは、連射しないように注意してください。

では、ついに今回のデータ取得用のクエリを投げてみます。

直近1年間を指定してクエリを投げ、結果をファイルに保存。
!pypinfo--days365--limit250000""project>"drive/My Drive/PYPI/PYPINFO_365_LIST.txt"#Served from cache: False
#Data processed: 636.49 GiB
#Data billed: 636.49 GiB
#Estimated cost: $3.11
#| project                                                                           | download_count |
#| --------------------------------------------------------------------------------- | -------------- |
#|                                                                           urllib3 |    950,108,414 |
#|                                                                               six |    788,263,157 |
#|                                                                          botocore |    693,156,212 |
#|                                                                          requests |    656,942,399 |
#  ~~以下略~~

ご参考までに、
直近1年間の総ダウンロード数は、
約 37,498,000,000 回
パッケージの種類は、約 215,000種類 でした。

パッケージの総数が約22万種類なので、
直近1年間レベルで見ると、登録されているものは
ほとんどが「生きている」ことになります。
以前あったと言われているPychinko(ぱいちんこ)が無いことから、
定期的に棚卸しされているのかもしれません。
また、直近30日で見ると13万4千種類ほどでしたので、
多少まともに使われているものは10万種類以下くらいでしょうか?

②取得データの加工

先ほど取得したパッケージ名&ダウンロード数のファイルは、
手元で人が閲覧する分には見やすくて便利なのですが、
プログラミング的に扱うには、パースして加工する必要がありますね。

冒頭のクエリのコストの行や、表の見出し行/Total行などの除去に注意して、
下記のように加工してLIST形式にします。

結果ファイルを加工しながら読み込んでLISTにする
f=open('/content/drive/My Drive/PYPI/PYPINFO_365_LIST.txt')line=f.readline()# 1行ずつ読み込む(含:改行文字)
pypinfo_list=[]whileline:#敷居が三つの場合=見出しと枠とTotalがジャマだが、それ以外はこの条件で判別可能
ifline.count('|')!=3:line=f.readline()continueelse:#改行コード、カンマ、半角スペースは除去
parsed_line=line.replace('\n','').replace(' ','').replace(',','')one_data=parsed_line.split('|')#['', 'urllib3', '950108414', '']の形の真ん中2個を使う
# 備考:数値もいまのところ文字列扱い
one_data=one_data[1:3]pypinfo_list.append(one_data)line=f.readline()f.close#最初の2行の見出し行と、最後の合計行は除去
pypinfo_list=pypinfo_list[2:-1]

pypinfoが提供していたりしないのかなーと思いながらも、
自作してしまいました。仮に有ったとしても、
一回約3$かかるクエリなので、テキスト版と別に投げるより、
クエリ投入回数の節約にもなるでしょう。
この①と②は、Python関連の「データ分析」を行う際にも有用だと思います。

③パッケージ名をカタカナにする技術(alkana.py&頑張る)

さて、パッケージ名の一覧化が出来ましたが、
例えば、urllib ⇒ユーアールエルリブ
python-dateutil ⇒ パイソン-デイトユティル
などのようにパッケージ名をカタカナ化するには
どうしたら良いのでしょうか?

方針は以下の4ステップです。
1.英単語として存在している言葉をカタカナにする
2.Pythonや、AWS、GITなどのIT特殊用語をカタカナにする
3.「ローマ字変換」を適用出来るだけ適用する
4.残った端数の文字は適当に変換しておく:f⇒フ、など。

最初の、「英単語をカタカナに」は、下記の
alkana.pyの変換テーブルを使わせていただきました。
https://github.com/cod-sushi/alkana.py/blob/master/README_ja.md

2~4については、主にローマ字の規則表から、
330行ほどの変換テーブルを作成しました。
前述のalkana.pyからのデータと足し合わせて、
alkana_listとして変換テーブルを作っておきます。

ここでのポイントは、その英語の文字列の長さをキーとして、
alkana_listを降順にソートしておくことです。

x[0]の項目にはあらかじめ文字列の長さを入れておく
alkana_list=sorted(alkana_list,key=lambdax:x[0],reverse=True)# py ⇒ パイ、や、python ⇒ パイソン など優先度の高いものについては、
# [30, 'py', 'パイ'] などと長さが長いことにして登録すれば優先度が上がる。

これで、長い単語から順に変換が適用されることになります。
実際に変換している様子は以下です。
分量があるため、一回50分くらいかかります。
下記のようにtqdmで途中の進捗を表示したり、
処理終了後はpickleで保存しておくと使いやすいでしょう。

全モジュールにカタナカ読み情報を付与する
fromtqdmimporttqdmpypinfo_jp_list=[]forpypinfointqdm(pypinfo_list):#日本語モジュール名格納変数(この時点では英語を格納)
jp_module_name=pypinfo[0]fordatainalkana_list:#変換テーブルを、順番通りに変換していく。
jp_module_name=jp_module_name.replace(data[1],data[2])pypinfo_jp_list.append([pypinfo[0],jp_module_name,int(pypinfo[1])])print(len(pypinfo_jp_list))print(pypinfo_jp_list[0:10])importpicklewithopen('/content/drive/My Drive/PYPI/pypinfo_jp_list.pickle','wb')asf:pickle.dump(pypinfo_jp_list,f)

特殊な自然言語処理ツールとして、
なにか用途もあるかもしれません。

④「美しい言葉」が使われているパッケージを探す

最後に、特定のキーワードが含まれているパッケージを探します。
予め「Beautiful_tango_list」にお好みの単語を登録しておき、
ひたすらループするだけです。
「ぱい」などの多数使われている用語を入れると、
結果が膨大になってしまう点は注意しましょう。
今回は、とある「お上品な単語をリストアップしているサイト」
の単語を拝借しました。

Colaboratoryではprint出力は5000行までだと思うので、
1万行くらいいくならば、下記のようにファイルへ出力する方が良いでしょう。

Beautiful_tango_listの内容を検索してテキストに書く
result_str=""forwordinBeautiful_tango_list:result_str+="■"+" "+word+"\n"fordatainpypinfo_jp_list:ifwordindata[1]:result_str+=str(data)+"\n"result_str+="\n"withopen('/content/drive/My Drive/PYPI/Beautiful_Result.txt','w')asf:print(result_str,file=f)

おつかれさまでした。
これらの技術&コードを駆使して、前述の結果のような、
美しい名前のパッケージを多数見つけることが出来たのです。

あとがき

Python(ぱいそん)のネーミングセンスは奥が深いですね。
辞書をひくときに隣の単語もみるように、
名前との偶然の巡りあわせだけからでも、
お気に入りのパッケージとの出会いが生じたら素晴らしいことです。

PyPI(ぱいぱい)が引き合わせた運命の出会い
とも言えるでしょう。
技術への興味のきっかけが「名前が気になったので」
でも良いのではないでしょうか?

いたって真面目にパッケージを紹介している記事ですので、
本来の意図とは違った意味を連想してしまうような、
心が汚れたオトナのかたは、石を投げないでください。
どうぞよろしくお願い申し上げます。

さあみなさんも、もっと
ぱい○ん と ぱいぱ○ が
すきになりましたでしょうか?

ここまでお読みいただいた賢明な読者諸氏には、
○ に入る言葉は明らかですね。

誤解してしまう方や、本記事にあらぬ文句を言う方がいたとしたら、
普段からそのようなことばかり考えている方に違いありません。

現場からは以上です。

ゲーミングPCで機械学習をして、CPU/GPUの性能の違いをColaboとも比較してみた話【Windowsの機械学習環境構築手順決定版。TF2.0対応】

ひとことで言うと

格安ゲーミングPC(Windows)を購入して、
Anacondaの仮想環境でCPU/GPUを切り替えられるようにして、
Tensorflow-GPU(v2.0)のコードを動かして、
ColaboratoryのCPU/GPUも含めた4パターンで、
性能比較をしてみたよ。
性能比較結果とWindows版環境構築手順をまとめておくね。という記事。

  • 機械学習にはGPUが有効だよ、ってよく聞く
  • ゲーミングPCにはGPUがある、そして最近安い

ゲーミングPCのGPUを機械学習に使ってみよう!

ということで、Windows上でのGPU環境構築を実施したが、
ハマりどころが多く大変だった。

欲しいゲーミングPCを「機械学習用だから!」と言って
買う言い訳になる決定版としての記事。

GPUの性能の違いが、学習時間の決定的差になるということを・・・教えてやる!

機械学習GPU環境の5つの選択肢と、私見

GPU有り自作PCにubuntu

  • 機械学習ガチ勢にオススメ、高性能
  • 高額になりがち(全て合わせると30万円くらい?)
  • 普段使いのWindowsやMacと別に買うとさらに大きな出費
  • 王道であるため、動かしやすさ的にも良い
  • ※ゲーミングPCのOSから入れ替えるパターンもコレ

Google Colaboratory

  • 初心者のお試し~上級者まで幅広く対応
  • 無料で、高性能GPUが使える
  • セットアップの手間いらず。Driveとの連携も便利
  • 一定時間ごとのリセットは気になる
  • Windows/Mac/Chromebookでもどこからでも使える!

NVIDIA Jetson Nano

  • 中級者以上。または、初心者のお試しでも面白い
  • 低価格(2万円くらい)だが性能もそれなり
  • GPUメモリの上限で学習出来る上限が違う点は要注意
  • 本体だけは安いが周辺機器も0から揃えると面倒
  • IoT機器としての利用目的ならコレ一択
  • (どちらかというと学習用よりPredict用な気がする)

GPU版のクラウドインスタンス or サービス利用

  • 従量課金なので初心者向けと思わせて実は違う
  • 実行環境として使う場合、クラウド破産に注意
  • 簡単な開発環境として使う場合、ロックオンされがち
  • サービスにより様々

ゲーミング(ノート)PC

  • ダークホース的存在。通常はWindowsなのをどう見るか?
  • 筐体の価格は、セット販売なので同性能での比較なら安い
  • 10万円~30万円くらい?
  • 機械学習以外の用途にも使うならばコレ一択
  • というか、ゲームにも使うなら100%コレしかない

Why ゲーミングノートPCでやるの??

個人的にデスクトップよりもノート派で、別目的のマシンを
機械学習へも転用するため、ということで答えが出ているが
PCを購入するタイミングで試してみたいという遊び心。
特に機械学習のガチ勢でもないので高額高性能なマシンは不要。

最安値クラスのゲーミングノートPC(10万円ほど)を購入して、
機械学習にも使ってみると、
どんな感じの使い勝手になるのか?を勉強がてら試した。

Colaboratoryが素晴らしいとはいえ、用途によっては
12時間で切れないローカル環境も有用であるとも考えられた。

本稿の記載目的

先駆者が多そうな気がしたが、
意外と大変だったためまとめておく。

機械学習 ⇒ GPU
GPU    ⇒ ゲーミングPC
というイメージを持つ人は多いだろう。

最近はゲーミングPCも安くなってきていて、
機械学習用ではなく購入したけど、転用できないかなー、
と思っている人も結構いるのではないか?
または、これからの時代は機械学習が重要なので、PC買うときには、
GPUを付けておいた方が良いかも?とか思っている人も居るかもしれない。
実際、ちょっとGPUが付いたノートPC、も増えてきている。

しかし、本稿のようにこの三つを紐づけた情報はかなり少ないという印象。

機械学習の上級者は当然ubuntu上での話が多く、
一方で、ゲーミングPCを転用しようなどという
私を含めた不届きな初心者が、
Windows上で環境構築するのはハマりやすく大変なためだろう。

また、機械学習系の環境変化は激しく、
Tensorflow2系の情報は、1系よりもまだだいぶ少ない。

そこで、本稿のような
Windows × GPU × Tensorflow2.0 × Anaconda
の環境構築手順をまとめておいても良いと考えた。
また、Colaboratoryとの性能比較含めて、
他の環境でも転用できる情報やコードも多い。

予想以上に大変だったので同様にハマる人への一助になれば。
初心者の情報交換としての記事。

始める前から、重大な誤算有り

機械学習環境の構築は、
ubuntu上、Dockerが楽である、という評判と、
「WSL2」によってWindowsでもDockerが動かしやすい、
という話を聞いて、結構簡単に作れるかも?と誤解していた。

実は、WSL2は2020年3月時点では、GPUデバイスに対応していない
そのため、GPU利用環境はWindowsベースで構築する必要がある。

ただし、WSL2からWindowsで構築したPython環境を
呼び出し実行できるので、疑似的にWindows上で
ubuntuやDocker利用環境のように使うことは可能。
※本稿ではWSL2の話は記載しない

今回結局Windows上で素で構築することになって大変だった。

なお、もしこれからマシンを購入しようとしている場合、
NVIDIA製のGPUにしておくことは
情報量的な意味でほぼ必須なので注意。
NVIDIAのノートPC向け主要なGPUの性能は
良い方から順番に以下の感じ。
まずコレが初心者には超分かりにくい

  • GeForce RTX 2080
  • GeForce RTX 2070
  • GeForce GTX 1070
  • GeForce GTX 1660 Ti
  • GeForce RTX 2060
  • GeForce GTX 1080
  • GeForce GTX 1060
  • GeForce GTX 1650
  • GeForce GTX 1050 Ti
  • GeForce MX 350
  • GeForce GTX 1050
  • GeForce MX 250

参考: https://pcfreebook.com/article/459993300.html

今回構築した環境のバージョン情報

  • Windows10 Home
  • GPU = GTX1650
  • Anaconda Python = 3.7
  • tensorflow-gpu = 2.0.0
  • CUDA = 10.0
  • cuDNN = v7.6.5(cudnn-10.0-windows10-x64-v7.6.5.32)

環境構築手順の全体像

  • 各ライブラリのバージョン決定方法
  • Anacondaの導入 / 仮想環境やJupyterの設定方法
  • Python仮想環境の構築(CPU版)
  • Tensorflow2.0のサンプルコードをCPU版で流す
  • Python仮想環境の構築(GPU版)
  • CUDA, cuDNN, ドライバのインストール
  • Tensorflow2.0のサンプルコードをGPU版で流す
  • (Errorが発生して大変だった話)
  • ColaboratoryのCPU/GPU版も含めて、性能比較

バージョンの決定方法(最重要)

tensorflow-gpu, CUDA, cuDNN,の
3つのバージョンを完璧に合わせる必要がある。
これを怠ると、例えば以下のような意味不明エラーの嵐に悩まされる。

意味不明エラー例
UnknownError:Failedtogetconvolutionalgorithm.ThisisprobablybecausecuDNNfailedtoinitialize,sotrylookingtoseeifawarninglogmessagewasprintedabove.

Tensorflowの公式サイトを確認する。
https://www.tensorflow.org/install/source_windows

2020/03当時、
Tensorflow2.1.0が最新であるものの、
テスト済みのビルド構成(Windows版)では、
2.0.0が最新であったため、2.0.0を採用。
Tensorflowのコードは、1系と2系で異なる点が多いため、
実行したいコードに合わせて、1系と2系は選ぶべし。

が、以下の記載を信じて痛い目にあった。

公式サイトの記載内容抜粋
バージョン Python バージョン  コンパイラ ビルドツール  cuDNN   CUDA
tensorflow_gpu-2.0.0    3.5~3.7   MSVC 2017   Bazel 0.26.1    7.4 10

正解は、CUDA10.0系に対するcuDNNのバージョンは7.6系。

7.4系だと上述の意味不明エラーが生じる。
そして、このエラーには複数の原因候補があり、
バージョン違い以外のケースでも多々発生するため、
原因の切り分けの難易度が高い。

参考: https://github.com/tensorflow/tensorflow/issues/24496
※さらに、Tensorflow公式サイトのコードを
 そのまま動かすだけでも上記issuesにある
 追記が必要という鬼畜っぷり。

Anaconda Python3.7版のインストール

https://www.anaconda.com/distribution/#download-section

基本的にはデフォルト設定のままでインストールを進める。

CondaのPython仮想環境の使い方

CPU版/GPU版を気軽に使い分けたい、
別バージョンを試したい、などの用途だけでなく、
環境構築時のリトライのしやすさなども含めて、
Pythonの仮想環境を用いて構築を進めると良い。

Anaconda Comand Prompt 上で下記のように操作する。

AnacondaComandPromptでの操作
# 以下のコマンドで仮想環境MyEnvNameを作る。
conda create -n MyEnvName python=3.7
# 以下のコマンドで仮想環境をアクティブにする。
conda activate MyEnvName
# ⇒コマンドラインの左側の「(base)」が、#   「(MyEnvName)」に変わることが確認できる。# 以下のコマンドで現在の仮想環境一覧が確認できる。
conda env list
# 以下のコマンドで対象の仮想環境を削除できる。
conda remove -n MyEnvName --all# なお、Anaconda Navigator の左側のメニューからも見える。

上記以外のコマンドについては、下記が詳しい。
https://qiita.com/naz_/items/84634fbd134fbcd25296

Jupyterから、対象の仮想環境に接続する方法

Anaconda Comand Prompt 上で疎通をとってから最後に、
Jupyterからその仮想環境を使えるように設定するほうが望ましい。
よって本手順は最後に実施した方が良い。
(が、普段仮想環境の作成とセットで実行する場合も
 多々あるため、仮想環境作成の直下に書いておく)

下記のコマンドで、Jupyter側から仮想環境が見えるようにしよう。

AnacondaComandPromptでの操作
# ipython(Jupyter)はbaseに入っているため、# 下記のコマンドで一度baseに戻る。
conda activate base
# --nameで仮想環境名,--display-nameで表示名を設定する。
ipython kernel install--user--name=MyEnvName --display-name=MyEnvName
# ⇒ 下記のフォルダにJupyterの接続設定が作成される。(隠しフォルダを表示すること)# C:\Users\[ユーザ名]\AppData\Roaming\jupyter\kernels

このままも一見Jupyter側から仮想環境に繋げられるのだが、
実は下記の設定ファイルに誤りがあるために、修正が必要。
python.exeの起動パスを、Anacondaの仮想環境側に変更する。

◇対象ファイル:
 C:\Users\[ユーザ名]\AppData\Roaming\jupyter\kernels\MyEnvName\kernel.json
◇修正前:
 "C:\\Users\\[ユーザ名]\\anaconda3\\python.exe",
◇修正後:
 "C:\\Users\\[ユーザ名]\\Anaconda3\\envs\\MyEnvName\\python.exe",

さらに、kernel.jsonに記載されている呼び出し引数の、
ipykernel_launcherを仮想環境側に追加する必要がある。
Anaconda Comand Promptで下記のように実行する。

AnacondaComandPromptでの操作
conda activate MyEnvName
pip install ipykernel

参考:https://qiita.com/howahowa/items/480607a06264426f24ed

この状態で Anaconda Navigator ⇒ JupyterNotebookを起動すると、
「New」や新しいノートブックを作成する際に、
「Python3」だけでなく、「MyEnvName」が選択肢として増えている。

または、既にPython3で作成済みのipynbについても、
Kernel ⇒ Change kernel を選ぶと、
MyEnvName の方にkernelを変更することが出来る。

もし、Jupyterからのkernel選択実行と、
Anaconda Comand Promptの仮想環境選択実行との
結果が異なってしまう場合は、
下記のスクリプトを実行して、使っているPythonや
ライブラリなどのパスを確認すると良いだろう。

パス確認用のPythonスクリプト
importsysprint(sys.prefix)print(sys.path)

また、Jupyterではブラウザを閉じただけでは
裏でプロセスはまだ残っているため、
「running」のタブから実行中プロセスを確認し、
一度全てをShutdownし、
Jupyter自身も終了させてから再起動すると、
きちんと設定が反映され、正しい動作となる場合も多い。

普段においても、GPUは「占有型」でデバイスを使うようなので、
他のGPU実行プロセスを残さないように、
適宜Shutdownしておいた方が良いだろう。

Tensorflowのインストール(CPU版)

まずは、CPUのみを利用した
Tensorflow実行用仮想環境を構築し、疎通をとろう。

AnacondaComandPromptでの操作
conda create -n cpuenv python=3.7
conda activate cpuenv
pip install tensorflow==2.0.0

備考:pipとcondaは混ぜて使わなければ、
 Anacondaでもパッケージの管理は全てpipでも良い。
 (どちらでも大差はない、と思う)
 なお、Tensorflowの公式サイトではpip準拠である。
 Tensorflowの導入後、conda list, pip list で違いを見ると、
 conda のほうがtensorflow系の依存ライブラリを
 頑張ってインストールしている感があるが、
 特に手数が変わるわけではない。
 本稿では基本はpip側で実施するが、どこかをcondaで実施する場合、
 すべてをcondaで統一したほうが良い。

Tensorflow2.0の上級者向けチュートリアルコードの実行(CPU版)

1ファイルだけで完結して実行できる一発実行コードを準備する。
Tensofrflow公式の 「エキスパートのための TensorFlow 2.0 入門」
のコードをつなぎ合わせて、時間計測用のコードを挿入し、
以下のように1ファイルにまとめて実行する。

参考:https://www.tensorflow.org/tutorials/quickstart/advanced

Jupyter Notebookの設定が済んでいる場合は、
下記のコードを直接1セルに貼り付けて実行、でもOK

AnacondaComandPromptでの操作
python tensorflow-tutorial-ex.py
# > ~~途中結果省略~~# > 今回のCPU版での実行時間は下記の通り。# > 実行時間:143.45523118972778[秒]
tensorflow-tutorial-ex.py
#https://www.tensorflow.org/tutorials/quickstart/advanced
#Tensofrflow公式「エキスパートのための TensorFlow 2.0 入門」
from__future__importabsolute_import,division,print_function,unicode_literalsimporttensorflowastffromtensorflow.keras.layersimportDense,Flatten,Conv2Dfromtensorflow.kerasimportModel## UnknownError:  Failed to get convolution algorithm. This is probably because cuDNN failed to initialize への対策
# gpu_devices = tf.config.experimental.list_physical_devices('GPU')
# for device in gpu_devices: tf.config.experimental.set_memory_growth(device, True)
#MNISTデータセットをロードして準備します。
mnist=tf.keras.datasets.mnist#ダウンロード終了後からの実行時間計測のために追加します。
importtimestart_time=time.time()(x_train,y_train),(x_test,y_test)=mnist.load_data()x_train,x_test=x_train/255.0,x_test/255.0# Add a channels dimension
x_train=x_train[...,tf.newaxis]x_test=x_test[...,tf.newaxis]#データセットをシャッフルし、バッチ化するためにtf.dataを使います。
train_ds=tf.data.Dataset.from_tensor_slices((x_train,y_train)).shuffle(10000).batch(32)test_ds=tf.data.Dataset.from_tensor_slices((x_test,y_test)).batch(32)#Kerasのmodel subclassing APIを使ってモデルを作りtf.kerasます。
classMyModel(Model):def__init__(self):super(MyModel,self).__init__()self.conv1=Conv2D(32,3,activation='relu')self.flatten=Flatten()self.d1=Dense(128,activation='relu')self.d2=Dense(10,activation='softmax')defcall(self,x):x=self.conv1(x)x=self.flatten(x)x=self.d1(x)returnself.d2(x)# モデルのインスタンスを作成
model=MyModel()#訓練のためにオプティマイザと損失関数を選びます。
loss_object=tf.keras.losses.SparseCategoricalCrossentropy()optimizer=tf.keras.optimizers.Adam()#モデルの損失と正解率を計測するためのメトリクスを選択します。これらのメトリクスはエポックごとに値を集計し、最終結果を出力します。
train_loss=tf.keras.metrics.Mean(name='train_loss')train_accuracy=tf.keras.metrics.SparseCategoricalAccuracy(name='train_accuracy')test_loss=tf.keras.metrics.Mean(name='test_loss')test_accuracy=tf.keras.metrics.SparseCategoricalAccuracy(name='test_accuracy')# tf.GradientTapeを使ってモデルを訓練する関数を定義します。
@tf.functiondeftrain_step(images,labels):withtf.GradientTape()astape:predictions=model(images)loss=loss_object(labels,predictions)gradients=tape.gradient(loss,model.trainable_variables)optimizer.apply_gradients(zip(gradients,model.trainable_variables))train_loss(loss)train_accuracy(labels,predictions)# モデルをテストする関数を定義します。
@tf.functiondeftest_step(images,labels):predictions=model(images)t_loss=loss_object(labels,predictions)test_loss(t_loss)test_accuracy(labels,predictions)EPOCHS=5forepochinrange(EPOCHS):forimages,labelsintrain_ds:train_step(images,labels)fortest_images,test_labelsintest_ds:test_step(test_images,test_labels)template='Epoch {}, Loss: {}, Accuracy: {}, Test Loss: {}, Test Accuracy: {}'print(template.format(epoch+1,train_loss.result(),train_accuracy.result()*100,test_loss.result(),test_accuracy.result()*100))# 次のエポック用にメトリクスをリセット
train_loss.reset_states()train_accuracy.reset_states()test_loss.reset_states()test_accuracy.reset_states()# 計測した結果を出力
tat_time=time.time()-start_timeprint("実行時間:{0}".format(tat_time)+"[秒]")

Tensorflowのインストール(GPU版)

インストールするライブラリ名に「-gpu」を付ける以外は、
全てCPU版と同様の手順である。

AnacondaComandPromptでの操作
conda create -n gpuenv python=3.7
conda activate gpuenv
pip install tensorflow-gpu==2.0.0

しかし、この状態のままで先述のコードを実行すると、
例えば以下のようなエラーがでる。

エラーログ
W tensorflow/stream_executor/platform/default/dso_loader.cc:55] Could not load dynamic library 'cudart64_100.dll'; dlerror: cudart64_100.dll not found
~~~中略~~~
tensorflow.python.framework.errors_impl.InternalError: cudaGetDevice() failed. Status: cudaGetErrorString symbol not found.

このようなdllが見つからない系のエラーが出る場合は、
以下のように、Anaconda Prompt からコマンドで、dllが存在するか、また、
そのpathが通っているかを確認することができる。
今回はまだ何も入れていないので、まずはCUDAやcuDNNをインストールする。

AnacondaComandPromptでの操作
where cudart64_100.dll
# > 情報: 与えられたパターンのファイルが見つかりませんでした。

引き続き、CUDA, cuDNN, Nvidia Driver を
インストールしてからまたコード実行に戻ってこよう。

新旧混在/玉石混合のインストール手順情報が多いなかで、
最も参考になるサイトは下記のサイト。
https://www.kkaneko.jp/tools/win/tensorflow2.html
環境構築系でエラーが生じた場合は、一度確認すると良い。

CUDAのダウンロードとインストール

CUDAはNVIDIAが開発している、
GPUによる並列計算処理のための開発環境。

最新版はえてしてバージョンがあっていないので、
以下のアーカイブサイトから
自身のバージョンに合ったものを探す(今回は10.0)
https://developer.nvidia.com/cuda-toolkit-archive

自分のマシンのGPUの型番に応じたファイルであるため、
あらかじめGPUのデバイス名を確認しておこう。
今回の構築で使ったのは以下のファイル。
cuda_10.0.130_411.31_win10.exe

インストール時には、そのインストールオプションで、
「高速(推奨)」ではなく、「カスタム(詳細)」を選び、
「ドライバーコンポーネントの選択」から、
CUDA - Visual Studio Integration
のチェックを外しておくと少し幸せ。
(関連するVisualStudioが無いよ、みたいな
 警告を受けなくて済む)

VisualStudioが必要だというインストール手順も多いが、
重くてインストールが手間になるので、
モジュールのリコンパイルをしない場合は、
VisualStudio関連は必須ではないハズ。
最悪でもbuildtoolだけインストールすればいい。

cuDNNのダウンロードとインストール

cuDNN はCUDA Deep Neural Network library の略。
ニューラルネットワーク計算を高速に行うためのライブラリー。

https://developer.nvidia.com/cudnn
ダウンロードのためには無料で簡単なユーザ登録が必要。
ユーザ登録後、再度上記のページを開き、
cuDNN Download に進む。

今回の環境で使ったのは下記のファイル
cudnn-10.0-windows10-x64-v7.6.5.32.zip

【重要】CUDAのバージョン(10.0)と、cudnnのバージョン(7.6)の
 掛け算のパターンでファイルが違うため、良く確認すること。
 Tensorflowのバージョンごとに、稼働確認がとれている組み合わせが違うため、
 使用するTensorflowのバージョン(2.0.0)に対応したものを選ぶ必要がある。
 https://www.tensorflow.org/install/source_windows
 上記公式サイトで確認出来るハズだが、
 今回は7.4⇒7.6に変更する必要があった模様。

ダウンロードしたファイルを解凍すると、
cudaフォルダの下に、以下が格納されている。

  • bin/cudnn64_7.dll
  • include/cudnn.h
  • lib/x64/cudnn.lib
  • NVIDIA_SLA_cuDNN_Support.txt

そのままのファイル構成のまま、以下のフォルダにコピーする。
C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v10.0

なお、後でどのバージョンを使っているのか分からなくなった場合は、
include/cudnn.h をテキストエディタで開いて57行目あたりに書いてある。

インストール終了後、一度Anaconda Prompotを再起動し、
下記のコマンドを実行し、cudaのバージョンを再確認する。

AnacondaComandPromptでの操作
(gpuenv) C:\Users\[ユーザ名]>nvcc -V
nvcc: NVIDIA (R) Cuda compiler driver
Copyright (c) 2005-2018 NVIDIA Corporation
Built on Sat_Aug_25_21:08:04_Central_Daylight_Time_2018
Cuda compilation tools, release 10.0, V10.0.130

先ほどは取得できなかったdllも認識されているハズ。

AnacondaComandPromptでの操作
(gpuenv) C:\Users\[ユーザ名]>where cudart64_100.dll
C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v10.0\bin\cudart64_100.dll

もし、認識されていない場合、
システム環境変数の「path」を参照し、
下記のパスが設定されているかどうかを確認しよう。
C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v10.0\bin

Nvidia Driver のアップデート(必要に応じて)

Nvidia Driverは、原則新しいバージョンが入っていれば問題ない。
CUDAに対応して、どのバージョンのドライバが必要かは、
下記の公式ページに一覧表がある。
https://docs.nvidia.com/cuda/cuda-toolkit-release-notes/index.html

大体は、PC購入時点の最新版のドライバが入っていると思うので、
ここは省略しても良い可能性が高いと思われる。

バージョンの確認方法は、
WindowsアプリケーションのNVIDIAコントロールパネルを開き、
ヘルプ⇒システム情報⇒コンポーネント
⇒NVCUDA.DLLを確認する。

製品名:NVIDIA CUDA 10.0.132 driver
ファイルのバージョン:25.21.14.1971
この右側の、41971の部分がDriverのバージョン。

または、nvidia-smiコマンドでも同様に確認できる。

AnacondaComandPromptでの操作
(gpuenv) C:\Users\[ユーザ名]>nvidia-smi
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 419.71       Driver Version: 419.71       CUDA Version: 10.0     |
|-------------------------------+----------------------+----------------------+
| GPU  Name            TCC/WDDM | Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  GeForce GTX 1650   WDDM  | 00000000:01:00.0 Off |                  N/A |
| N/A   41C    P8     1W /  N/A |    134MiB /  4096MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID   Type   Process name                             Usage      |
|=============================================================================|
|  No running processes found                                                 |
+-----------------------------------------------------------------------------+

GPUを使用中に上記のnvidia-smiコマンドをたたくことで、
その実行中プロセスやメモリ利用量等を確認できるため、
以降の手順中でも適宜たたいてみるのがオススメ。

GPU版Tensorflowの疎通確認

Anaconda Prompt にて、GPU版仮想環境をactivateして、
以下のように順番にコマンドを実行する。

AnacondaComandPromptでの操作
conda activate gpuenv
python -c"import tensorflow as tf; print(tf.__version__)"
python -c"from tensorflow.python.client import device_lib; print(device_lib.list_local_devices())"

結果、tensorflowのバージョン2.0.0と、
以下のようなGPUデバイスの表示が出れば成功。
CPU版では、device_type: "CPU" のところまでしか出ない。
GPUが認識されていれば、その型番が表示されるハズ。

結果
~途中省略~
Created TensorFlow device (/device:GPU:0 with 2913 MB memory) -> physical GPU (device: 0, name: GeForce GTX 1650, pci bus id: 0000:01:00.0, compute capability: 7.5)[name: "/device:CPU:0"
device_type: "CPU"
memory_limit: 268435456
locality {}
incarnation: 4688799603900704883
, name: "/device:GPU:0"
device_type: "GPU"
memory_limit: 3055235892
locality {
  bus_id: 1
  links {}}
incarnation: 15201502304566343823
physical_device_desc: "device: 0, name: GeForce GTX 1650, pci bus id: 0000:01:00.0, compute capability: 7.5"]

また、もし失敗している場合でも、上記の実行ログ中で、
どのdllの呼び出しに失敗しているのか確認出来るハズ。

ここまでで、環境構築できたよお疲れ様、
と言っている手順情報が多々見受けられるのは、
良くない風潮だと思う。
下記のUnknownErroの件や、性能比較しないと
本当にGPUが適用されているか分かりにくいから。
あと、順調に環境構築進める手順だけでなく、
発生したエラーと解決方法や確認方法も書いて欲しい。

チュートリアルコードの実行(GPU版)(※UnknownError対応)

早速、この環境でさきほどのCPU版の疎通用コードを実行する。
python tensorflow-tutorial-ex.py

GPU版の疎通が実施できていても、以下のようなエラーになる。

結果
~~途中省略~~
 W tensorflow/core/common_runtime/base_collective_executor.cc:216] BaseCollectiveExecutor::StartAbort Unknown: Failed to get convolution algorithm. This is probably because cuDNN failed to initialize, so try looking to see if a warning log message was printed above.
         [[{{node my_model/conv2d/Conv2D}}]]
Traceback (most recent call last):
  File "tensorflow-tutorial-ex.py", line 85, in<module>
    train_step(images, labels)~~途中省略~~
 line 67, in quick_execute
    six.raise_from(core._status_to_exception(e.code, message), None)
  File "<string>", line 3, in raise_from
tensorflow.python.framework.errors_impl.UnknownError:  Failed to get convolution algorithm. This is probably because cuDNN failed to initialize, so try looking to see if a warning log message was printed above.
~~途中省略~~
Function call stack:
train_step

この、UnknownError がめちゃくちゃ厄介で、
複数の要因で似たエラーが発生しうる。
詳細は以下参照:
https://github.com/tensorflow/tensorflow/issues/24496

ざっくり言うと、以下のようなパターンがある模様。
* Tensofrow,CUDA,cuDNNのバージョン不整合
* インストール手順不正、DLL不足など
* 別スレッドでGPU使用プロセスが残っていた場合など、
 2回目の実行などでGPUメモリ不足(一回目は成功する)
* GPUメモリの割り当て方の指定方法の問題(※コード修正のみで対応可能)

余談:
このエラー修正が最も時間がかかった。
インストール手順も確認して何度か修正してやり直したが、
結果的にはバージョン不整合&コードの問題の二重トラブル。
どちらもTensorflowの公式サイトからの情報をもとに
実施した部分であったため、判明が遅れてしまった。
Windows版の情報はあまりアテにしてはいけない。
さらに、修正後もJupyterから実行すると再発しやすい。
「別スレッドでGPU使用プロセスが残っていた場合」が該当。
単純なRunning⇒shutdownのプロセスキルだけでは
何かが残っているのか、最悪PC再起動しないといけない。
このこともあり、疎通完了まではJupyter以外で実施がオススメ。

コード修正のみで対応するためには、
import文の下に、以下のようなコードを追加する。
(tensorflow2系の場合。1系の場合は参照先URLをご確認)
※前出のコードには既にコメントアウト状態で貼ってあるので、
 コメントアウトを解除すればOK

import文の下に追加するコード
# UnknownError:  Failed to get convolution algorithm. This is probably because cuDNN failed to initialize への対策
gpu_devices=tf.config.experimental.list_physical_devices('GPU')fordeviceingpu_devices:tf.config.experimental.set_memory_growth(device,True)

Keras関連のサンプルコードを実行する場合も、
バックエンドでTensorflowを使用するため、
最初のほうで同様のコードを実行しておくと、
UnknownErrorが防止できるハズ。

UnknownError対策用のコードは、keras利用の場合は、
以下のようなコードになるかもしれない(未確認)

keras版追加コード
# Allowing GPU memory growth
#config = tf.ConfigProto() #V1のコード
config=tf.compat.v1.ConfigProto()config.gpu_options.allow_growth=True#tf.keras.backend.set_session(tf.Session(config=config)) #V1のコード
tf.compat.v1.keras.backend.set_session(tf.compat.v1.Session(config=config));

実施した結果、今回は下記の時間で終了した。
実行時間:29.763328075408936[秒]

CPU版が、約143秒に対して、
GTX1650でのGPU版が、約30秒なので、
かなり早くなったように思える。

ただし、単純にこの割合で早くなるのではなく、
実行するコードの内容にもかなり依存する点は注意。

例えば、公式サイトの初心者向けクイックスタートのコードを使うと、
https://www.tensorflow.org/tutorials/quickstart/beginner

CPU版:約14秒
GPU版:約18秒
と逆転してしまう。(初期化等の時間を雑に扱っているせいもある)

実際に実行したコードは下記の通り。

初心者向けクィックスタートコード+実行時間ログ追加版
#https://www.tensorflow.org/tutorials/quickstart/beginner
from__future__importabsolute_import,division,print_function,unicode_literalsimporttensorflowastfmnist=tf.keras.datasets.mnist#ダウンロード終了後からの実行時間計測のために追加します。
importtimestart_time=time.time()(x_train,y_train),(x_test,y_test)=mnist.load_data()x_train,x_test=x_train/255.0,x_test/255.0model=tf.keras.models.Sequential([tf.keras.layers.Flatten(input_shape=(28,28)),tf.keras.layers.Dense(128,activation='relu'),tf.keras.layers.Dropout(0.2),tf.keras.layers.Dense(10,activation='softmax')])model.compile(optimizer='adam',loss='sparse_categorical_crossentropy',metrics=['accuracy'])model.fit(x_train,y_train,epochs=5)model.evaluate(x_test,y_test,verbose=2)# 計測した結果を出力
tat_time=time.time()-start_timeprint("実行時間:{0}".format(tat_time)+"[秒]")

こちらのコードでは、GPU版もCPU版と
全く同じコードで動作するので簡単な疎通にもオススメ。

ついでにもう一つ、比較用/疎通用に使えるコードを乗せておく。
fashion_mnistの分類問題の短めのコード。
これまでのサンプルと同様に、JupyterやColabのセルにコピペや、
.pyファイルにしてそのまま実行が可能で使いやすいと思う。

fashion_mnist
#参考:https://github.com/tensorflow/tensorflow/issues/34888
importtensorflowastf#UnknownError回避用
gpu_devices=tf.config.experimental.list_physical_devices('GPU')fordeviceingpu_devices:tf.config.experimental.set_memory_growth(device,True)importnumpyasnpprint("Num GPUs Available: ",len(tf.config.experimental.list_physical_devices('GPU')))fromtensorflowimportkerasfashion_mnist=keras.datasets.fashion_mnist#ダウンロード終了後からの実行時間計測のために追加します。
importtimestart_time=time.time()(train_images,train_labels),(test_images,test_labels)=fashion_mnist.load_data()train_images=train_images/255.0test_images=test_images/255.0train_images=np.expand_dims(train_images,axis=3)test_images=np.expand_dims(test_images,axis=3)model=tf.keras.Sequential()model.add(tf.keras.layers.Conv2D(filters=64,kernel_size=2,padding='same',activation='relu',input_shape=(28,28,1)))model.add(tf.keras.layers.MaxPooling2D(pool_size=2))model.add(tf.keras.layers.Dropout(0.3))model.add(tf.keras.layers.Flatten())model.add(tf.keras.layers.Dense(256,activation='relu'))model.add(tf.keras.layers.Dropout(0.5))model.add(tf.keras.layers.Dense(10,activation='softmax'))model.summary()model.compile(optimizer='adam',loss='sparse_categorical_crossentropy',metrics=['accuracy'])model.fit(train_images,train_labels,batch_size=64,epochs=10,validation_data=(test_images,test_labels))# 計測した結果を出力
tat_time=time.time()-start_timeprint("実行時間:{0}".format(tat_time)+"[秒]")

Colaboratoryで実行した場合との比較結果

さてこのように、WindowsでGPUを使おうとすると
かなり面倒&ハマりやすい手順が必要になってしまう。

ubuntuならばNVIDIA Dockerが最短として、
残念ながらWindowsのWSL1,2では
本記事執筆時点ではまだGPUイメージは使えないと聞くため、
「一般的なパッケージング済みのゲーミングPC(Windows)」
をGPU有り機械学習にも使おうとすると、
まだこの手順はしばらく現役だと思われる。

※WSLでDocker利用可能 ≠ NVIDIA Docker利用可能、
 は大きな誤算であった。

ColaboratoryにはこのGPU設定済み環境が、
無料で使える状態で用意してある。

改めてColaboratoryの偉大さを感じる。

ついでに、あくまで参考まで、
簡単な実行時間の比較計測を行ってみた。

性能比較結果一覧表

環境 - CPU/GPU情報初心者Tutorial上級者Tutorialfashion_mnist
CPU(Local) i7-9750H 2.60GHz約14秒約143秒約289秒
GPU(Local) GTX1650約19秒約 29秒約 77秒
CPU(Colab) Xeon(R) 2.20GHz約23秒約291秒約760秒
GPU(Colab) Tesla P100約24秒約 18秒約 46秒

評価に使ったどのコードも本稿に乗せており、
データセットのダウンロードも付いているので、
既に環境をお持ちの方は、お手元でも走らせてみると
さらに楽しめるかもしれない。

なお、Colaboratoryではtensorflowのバージョンは、
デフォルトでは「1.15.0」であり、
最初に下記の専用コマンドで「2.1.0」に
変更してから実行している。
ローカル側と多少ずれるが同じ2系だしそのまま動いた。
ただし、2020年3月27日~、デフォルトが2系になるとのことなので、
以降はこのコマンドは不要。(※逆に1系にしたい場合に使うことになる)

Colaboratoryでのtensorflowバージョン変更用
%tensorflow_version2.ximporttensorflowastfprint(tf.__version__)

Colaboratoryでは、ランタイムに接続するたびに、
どのスペックのマシンが当たるかはムラがあるため、
今回引けたマシンの情報を書いておく。
CPU(Colab) ⇒ Intel(R) Xeon(R) CPU @ 2.20GHz ×2
GPU(Colab) ⇒ Tesla P100-PCIE-16GB (最も良いヤツ?)
よって、実行時間の秒数は、あくまで参考値である。

詳細なマシンスペック情報は、以下のコマンドで確認が可能。

Colaboratoryのスペック確認(下二つはGPU用)
!cat/proc/cpuinfo!cat/proc/driver/nvidia/gpus/0000:00:04.0/information!nvidia-smi

なお、私のゲーミングノートPCの情報は下記の通り。
CPU = Intel(R)Core(TM)i7-9750H CPU 2.60GHz
GPU = GeForce GTX 1650
ゲーミングノートPCとして、2020年時点で最安値エントリークラス。
10万円ほどで購入。GTX1050やMX250よりは良いので最低性能ではない。

余談:
最近では、特にゲーム用の虹色に光るようなヤツでなくても、
MX250などの小さなGPUを積んでいたり、GPU搭載でありながら、
かなりの薄型、普通のノートPCのようなビジネスも可のモデルが
増えてきており、今後普通のノートPCにおいても、
ちょっとGPU積んでます、みたいなモデルが増えてくるかもしれない。
それにしても、ちょっと前までは考えられないくらい、
ゲーミングノートPCも安く&薄くなったような気がする。

性能比較結果としてはやはり、(12時間/90分の問題を考慮しなければ)
Colaboratory(GPU)の利便性&性能が際立っていた。
また、長時間利用したい場合でも、近い将来、
有償版の Colab Proが日本でも使えるようになればさらに便利になりそう。

余談:
Colab Pro は$9.99/月ほど払うと、
より速いGPU/より長期間の使用/より多くのメモリ、で使えるらしい。
2020年3月時点では、アメリカ在住者向けにしか解放しておらず、
日本のクレジットカードでは登録出来ない模様。

感想/結論

ColaboratoryのGPUは良いものを使っている、
というのがどの程度のものなのか、
また、CPUとGPUでの実行時間の違いがどう出るのか、
実体験として理解することが出来た。

やっぱりColaboratoryがGPU利用としては最強感。

Windows環境(ゲーミングノートPC)でもGPUを使う方法と、
そのトラブルシュートをまとめることが出来た。
Anacondaの仮想環境の切り替え方法や、
Tensorflow2.0の一発時間計測実行コードも役立つかもしれない。

Macユーザも七色に光る怪しいマシンを買ってみてはいかがか?

機械学習のためにゲーミングノートPCを買って、
お勉強したあとはいっぱいゲームをしよう!!

以上。

「接待どうぶつ将棋AI」が爆誕!おもてなし接待AIを作る物語。

はじめに

AIが将棋のプロ棋士より強くなってから久しい。

一方で羽生善治先生は、
接待将棋のようなことはAIには難しい」と喝破している。

では実際に接待AIを作ろうとすると、どのような点が難しく、
どこまでのレベルの「接待」なら出来るのだろうか?

本稿は、「接待将棋」という難問に挑んだ開発日誌的な記録として、
得られた接待AIの考え方にいたるまでの物語である。

参考: 人工知能に「接待将棋」はできない──羽生善治と石山洸が語る将棋とAIの進化

先に完成品を記載(実際に遊べるURL)

結論から述べると、あるシンプルな実装方針で、
自身ではある程度納得出来る実装を得ることが出来た。
以下がその結果(接待AI)と実際に遊べるアプリだ。
PC/スマホどちらでも対応。

https://doubutu-64e43.web.app/

アプリの画面キャプチャ

「将棋」は難しすぎるため「どうぶつしょうぎ」で作った。

「どうぶつしょうぎ」は、そのユーザを考えると、
実は地球上で最も「接待」が必要とされているゲームだと思われる。
ルールは適宜ぐぐってくだされ。

接待/最強/雑魚の3モードを用意させていただいた。
「接待の美学」として、プレイヤー側が常に先手である。

ゆえに、後手必勝が解明されている「どうぶつしょうぎ」では
「最強」相手には絶対勝てない(ハズ)。
ほんとうにガチの「最強」として実装した。

「雑魚」は、次に負けるかどうかだけ分かる、というランダムAI。
ライオン(王将)のキャッチorトライ、が理解出来ないと、
そもそもルールを解していないことになるため、
どうぶつ将棋を楽しめる最弱の人類とほぼ同等、と言える。

「接待」のモードが本稿で言及しているAIだ。
おそらく上級者ほど、「接待AI」が手ごわく感じられ、
一方で初心者でも十分勝ててしまうだろう。

棋譜解析データの容量が重いため、本来は
サーバ/PCのみで実行すべき作者専用のアプリケーションなのだが、
指し手を返すアルゴリズムを無理やりAPI化して、
PWAを使ってPC/スマホ全対応にして、
幅広い人類(※主にお子様)に使っていただけるようにした。
フロント部はこのように後付けのオマケ、雑な実装なので、
UIはちょっとしょぼい。ご容赦いただきたい。

改めて、接待将棋とは何か?

まず、通常の将棋は「勝てば官軍」である。
では、接待将棋とは「絶対負ける将棋」なのだろうか?
否、そうではない。

過日、負けることが難しい、
「世界最弱のオセロAI」が話題になった。
これはこれで面白い試みではあるが、
ルールを逆にしただけの「最強AI」とも言える。
(負けることが出来ないために負けた気持ちになる)

「接待」とは「おもてなし」のこと。
飛車をタダで相手に差し上げるような最悪手ばかり指しても、
なんの接待にもならない。

ある程度競った状態になりながらも、
大部分においては客側が勝つ。
わざと手抜いているようには見えないことが重要だ。

※なお、アプリの実装面においても、
 これが「接待」であると分からせないように、
 「解説OFF」にすると「接待」などの文字が消えた状態で、
 お子様や友人に渡すことが出来るようにしてある。

接待では「相手にそうと悟られない程度に手を抜くこと」
が要求されるため、普通に勝つよりも難しい技術なのである。

さらに、相手に応じて強さを変える必要がある。
「上級者相手でもある程度互角に戦った上で負けること」
も求められている。
そうしないと、ただの「弱いAI」になってしまう。

相手のレベルを推し量った上で、程よく強さを加減するAI。
しかも加減していると悟られないほど自然に。
確かに羽生さんのお話の通り、実装は難しそうである。

なぜ「接待どうぶつしょうぎ」か?

「接待」×「どうぶつしょうぎ」
のテーマを選んだ理由は二つある。

理由①:完全解析済みであり、その解析結果が使えること

「どうぶつしょうぎ」については、
2009年に田中哲朗先生により、完全解析されており、
78手で後手の必勝、とのことだ。

ただし、この必勝方法を行うためには、
240万局面以上を暗記する必要があり、
とても人間ワザでは出来ないため、
人間が行うゲームとして、つまらなくなったわけではない。

参考: 「どうぶつしょうぎ」の完全解析

接待AIを作るのは難しいといっても、
完全解析済みのゲームであれば、何とかなる可能性がある。
また、〇×ゲームなどとは比較にならないほど、
人類にとって十分に複雑なゲームであり、
これで得られる知見は他のゲームにも活かすことが出来そうである。

田中先生は完全解析の成果を扱いやすいライセンスで公開してくださっており、
本稿の後段でそのプログラム/データをPythonから扱う実装方法も述べる。

理由②:「どうぶつしょうぎ」は地球上で最も「接待」が求められるゲーム

「どうぶつしょうぎ」は主にこども向けに開発されたゲームであり、
アンパンマン、しまじろう、などの
各種大人気キャラによるパッケージも散見される。

「お子様」に対し大人げなく勝ちまくるわけにはいかないし、
かといって、あまりアホな手を指すのも教育上よろしくない。
現在の日本において、これ以上
「接待」適用率の高いゲームは無いと言えるだろう。

また「お子様」の成長は大変早く、その棋力は
超初心者から大人顔負けの上級者まで、まさに千差万別である。
通常では、自分と同じくらいの強さのAI、との対戦が面白いのだが、
適切なAIを選ぶのも難しい。

「接待AI」のように、指す過程で力を調整してくれると都合が良い。

私もどうぶつしょうぎによる接待業務
行ったことがあり、その際には手の抜き方に大変苦慮し、
実際にその必要性を痛感している。

接待どうぶつ将棋の要件定義

これまでの話をまとめると、
欲しい接待AIは、以下の3要件を同時に満たすことになりそうだ。

  • ①基本的には顧客側が勝つ
     (※毎回必ず、でなくとも勝率が超えれば良し)

  • ②顧客に手抜きを悟られない
     (※相手のレベルに合わせた手抜き)

  • ③顧客に対して弱すぎない
     (※相手のレベルに合わせた強さ)

「接待」=「おもてなし」=「面白い、気持ち良い試合」
⇒自分と同程度の強さの相手との接戦を制して勝利!
 を実現差し上げること、の言い換えである。

いくら強い人間でも間違えることもあるし、
初心者がプロと全く同じ手を指すことも多々ある。
相手のレベルに合わせるのは至難のワザであり、
しかも、合わせようとすると「顧客側が勝つ」と両立しなくなってくる。
(最初は定石通りでプロのようだが実は中終盤が弱い人、なんていくらでもいる)

さすが羽生先生もおっしゃる難題だ。

接待の方針検討(失敗案)

まず「①顧客側が勝つ」を満たすためには、
AIを弱く弱く作る必要があるが、弱すぎると、
「②手抜きを悟られない」を満たせない。

相手のレベルに合わせるor勝負を接戦にするためには、
盤面の形勢を判断して、
およそ互角になるような手を指す、
という案が最初に思い浮かぶだろう。

勝ってる時は悪い手を指して
負けている時は良い手を指し、盤面の均衡を保ち、
勝負が接戦熱戦になるようにする。

しかし、それでは勝敗がつかなくなってしまう

手が進むにつれて目標とする形勢を50-50⇒60-40⇒70-30などのように
加減していけばよいのだが、
それでは途中からアホになっていくだけで、
本来面白いハズの終盤がつまらない。
最後の方で「手抜きを悟られる」状態になる。
超典型的な「こいつワザと負けるように手を抜いたな~」と
分かるパターンの接待である。

ほかの案としては、
終局までの手数が一番かかるような手を選ぶ = 接戦
という方針も思い浮かぶ。

が、将棋において最強AIが
「出来るだけ長手数にする最良の方法」を取る場合、
まず完膚無きまでに相手のコマを取って
相手の動きを完全封印してから、
(その状態なら任意の手数まで引き伸ばせるので)
予定手数近辺になって、ワザとコマを返して
そのまま自殺頓死する、という、
最悪な動きになってしまうだろう。
これでは話にならない。

通常、ボードゲームのAIというのは、
「N手先読み」&「盤面評価関数」によって、
MIN-MAXで考えて、評価が最良になるように動かす実装が多い。

相手のレベルに合わせて、という実装を考えると、
通常の評価関数が50-50であることを最良としてそれを目指す、
 (直前の相手が悪手ならこちらも悪手、で評価値を戻す)
決着までが遠いこと=互角と考えてそれを目指す、
 (どちらが勝ちの決着に対してもそれを引き伸ばすような手)
などが自然な発想となるが、
「接待AI」についてはコレでは実現出来そうにない。
発想の転換が必要だ。

接待の方針検討(今回採用案)

結論としては、
囚人のジレンマの「しっぺ返し戦略」をベースとした。

相手がその盤面で「N番目に良い手」を指してきた場合に、
次に接待AIは「N+1番目に良い手」を指すような方針だ。

盤面を50-50に戻そうとしているわけではないため、
緩やかに接待AI側が不利になっていく、というのがミソ。

しかし、単純にこの方針で作ってしまうと、
「王手が最善手で、逃げる手が1手しかない場合」
接待AI側が、最善の受けを選択できず、
王手するだけでゲームが終わることが多数発生する。
(※実際に作ってみてそれに気がついた)

王手でなくても「受けが1手しかない飛車取り」などの
応対が一本道となるやり取りが全滅である。
ある程度の上級者がやる場合には全く興ざめだ。
「③弱すぎない」を満たさなくなってしまう。

では、「N番目に良い手」に対し、
「N番目に良い手」を返せばどうだろうか?(同等の手を許可)

これは盤面の状態によって、
どちらの「N番目」がより有利かが異なるため、
「①顧客側が勝つ」を満たせない可能性が高い。
または終わらなくなってしまう。

状況をみてちゃんと負けに持っていくように、
有利になりすぎたら急遽手抜きをする、と、
「②手抜きを悟られない」が引っかかってくる。

いろいろ考えて試行錯誤した結果、
「最善手乖離度」を同等以下にする
という新概念を思いついた。

「最善手乖離度」とは?

まず、盤面の評価値は、田中先生の完全解析の考え方を参考に、
両者とも最善手を指した場合に
・自分が勝つ= 100点 マイナス [それまでにかかる手数]
・自分が負け= ―100点 プラス  [それまでにかかる手数]
という評価とした。
これにより、ある手を指した直後の盤面を評価することで、
最善手~最悪手、まで序列をつけることが可能である。

「最善手乖離度」とは、その盤面における相手の手を評価する指標値で、
「相手の手の評価値 ÷ その盤面の最善手の評価値」
で計算される値である。
負数÷負数のようなものを除外するために、
盤面評価値に+300のゲタを加えて計算する。

最善手では常に「1」になり、
最善手から離れれば離れるほど「0.9」「0.8」などと下がる。
勝敗が変わるほどの敗着を打つと「0.5」「0.6」あたりになる。
(100+300の1手勝ちがある状態で、
 -100+300の即負けにするのが最悪なので、その場合で0.5)

今回採用したシンプルな実装方針は、着手可能な手の中から、
「相手の手の最善手乖離度以下で最大の手」を打つというだけ。

相手が最善手を打ってくればこちらも最善手を返す。
相手が相当悪い手を打ってくればこちらもそれに近い悪手を返す。
がこの一つのロジックだけで実装可能である。

N番目に良い系だと、そもそも着手可能な選択肢が何個あるのか?
という手の広さへの依存度が高いためNG。
最善手との比較ではなく、選択肢の平均値と比較する案だと、
前述のような正解が1手しかないような状態、で失敗するのでNG。
また、「未満」にすると最善手の最善手返しが含まれなくなってしまうため、
「以下」がベストと考えられる。

相手が最善の「1.0」の場合は接待AIも「1.0」だが、
どこかで多少でも不利な手を指した場合、
「それ以下」でピッタリ同じ数字で戻すような手は、
毎回存在するわけではないため、
緩やかに少しずつ、接待AIが不利になる仕組み。

唯一、完全最強手を連発すると、
その時点の盤面評価値のままで決着してしまう。
(つまり、「接待」側が勝つパターンは、
 選択肢が相当少ない時にワザと王を捨てる、か、
 盤面が不利な状態から最後まで最善手の連続を行う、
 の2パターン)

また、手抜き度合いについても、
常に顧客とほぼ同レベルの手を選択することになるため、
そのレベルの手を実施してくる顧客、から見れば
手抜いているかどうかの判断は不可能である。
(手抜いたと分かるくらいなら、顧客側がもっと良い手を指している)
急激に強くなったり弱くなったりという感が理論上発生しない。

ついでに、
「過去に指した相手の手を蓄積して評価する」ような、
本当に相手の棋力を測る複雑な実装ではないため、
「過去の状態や履歴」を持つ必要がなくなり、
API化がやりやすくなったことは大きな利点である。
(過去の蓄積を評価をしようとすると、
 各クライアントの状態をサーバに保持する必要が生じ、
 ユーザごとの履歴&盤面管理が必要になってしまう。
 APIごとのユーザ認証が生じてしまう)

結論:接待AIの基本実装はこの考え方だけ!

相手の直前手の最善手乖離度を計算し、
AIはその最善手乖離度以下の最良の手を返す

※ただし「完全解析」と同様に一発負けになる手だけは除外。

以下のアプリを改めて遊んでみて欲しい。

https://doubutu-64e43.web.app/

超初心者のお子様から、上級者の大人まで、
誰に対してもそこそこの歯ごたえを残しながら、
明らかに人間が勝つ勝率になることを実感いただけると思う。

「解析」&「最強」というオマケ

副産物的に面白いと思ったので、
このAIの「評価結果/最善手乖離度」を直接ログ的に
アプリ上に出力している。

さっき自分が指した手が、
どの程度良い手だったのか、
正解は何だったのか?が都度分かるため、
どうぶつ将棋力の向上にも役立つだろう。

また「完全解析」結果を検索しているため、
「最強」AIは本当に強い。
「最強」にボコられるのも、
マゾな人には「接待」かもしれない。

ただし、途中でAIを切り替えた場合、
完全解析に含まれないようなレア局面が生じると、
常に最善手を返せるわけでは無い。

実装した内容/使用技術

ここでは、どのような開発をしたのか?の
実装面の話を記載する。
※一部のコードや、実装上のポイントは別途後述

まず、田中先生の完全解析の結果と、
その検索用のプログラムをダウンロード(2GBくらい)して
動かせるようにした。(C言語)

次に、私がPythonで動かしたかったので、
Pythonのsubprocess経由でそのプログラムを動かし、
完全解析データをPythonから扱えるようにした。

大いなる誤算があったのは、
「完全解析の結果」は本当にあらゆる局面の
応対が掲載されているわけではないこと。

「完全解析」とは、
二人零和有限確定完全情報ゲームにおいて
理論上、先手必勝or後手必勝or引き分け決まっているが、
そこまでの道のりを全て求めた、という意味であり、
例えば、「王手見逃し」など
通常あり得ないような手/局面は解析に含まないのだ。

完全情報ゲームの一般的理論については、
@drken先生の素晴らしい解説をご参照されたし。
https://qiita.com/drken/items/4e1bcf8413af16cb62da

「完全解析」データさえあれば、極端な話、
コマの動きをこちらでプログラミングしなくても、
盤面の遷移だけで、ゲームが作れてしまうかも?と思っていたのは
「完全」という名称だけでイメージしていた誤算であった。

結局「トライ」の判定なども含めて
Pythonで全てルールを再実装する必要があった。

ルールの実装後、AI
(※ここで言うAIとは、盤面に対して手を選択する仕組み、程度の意味)
を実装し、相互に戦わせて遊べるようにした。
このへんで、接待AIの方針を試行錯誤。

毎回2GBの問い合わせ&結果パース処理を実施していると
遅くてしょうがないため、
問い合わせ結果をキャッシュするSqliteのDBを作り、
過去に問い合わせた結果はキャッシュから返すようにした。
(※現在公開しているアプリも、キャッシュを使っているが、
  過去に経験していない局面が出た場合、
  手が返ってくるまでに15秒程度かかるかもしれない)

自身でもCUIで検討を続けるのは辛いものが生じてきて、
GUI化のためAI部分をAPI化し、
指した手&その盤面、を入力として与えると、
AI側が指す手を返す、という構造に改造した。
(※なお「千日手」は本来引き分けであり、CUI版では実装済みだが、
  現在公開しているアプリでは、極力履歴を持たない方針で、
  実装対象外とさせていただいている)

APIのサーバとしては「Flask」を採用し、
GCP(Goocle Cloud Platform)の完全無料インスタンス上で作った。
最小構成のワリに重めの処理をやっているので、
アクセスが多いとサーバが落ちるかもしれない。

クライアント側は、スマホでも遊べるように
「Monaca」を使って「PWA」で実装することにした。
JavaScriptからAPIをコールしている。

アプリ内で選んだコマの移動可能範囲を色付け表示する際に
毎回通信するのもよろしくないことや、
移動後の盤面をサーバからのレスポンスを待たずに描画するため、
結局JavaScript側でもどうぶつ将棋のルールをほぼ実装するハメになった。

PWAのデプロイ先としては、Googleの
「Firebase」を採用した。

ここでハマったのは、HTTPS通信関連。
Firebaseは自動でHTTPS通信になり、
Monacaからも簡単にデプロイできるクチがあるため
PWAをデプロイする際に便利なのだが、
APIサーバ側を、素のHTTPにしていたので、
PCから実行してOKであっても、
スマホから実行すると、
「せっかくHTTPSなのに途中でHTTPに変えるなよ」的な制約で動かない。
(※スマホ側のデバッグモードが無いためエラー原因を意訳)

APIサーバ側にいわゆる「オレオレ証明書」を入れて
見た目HTTPS化しても事態が改善せず、
「letsencript」を使って本当にHTTPSにした。

letsencript使用時に必要になるため、無料のドメインも取得した。
ここまでやる予定は全くなかったので
予想をはるかに超える手間になってしまった。

接待は改めて大変な仕事である(違

実装上のポイント

PWA+Firebaseや、GCP+Flaskは
私の過去記事でも言及しているため、
主にどうぶつしょうぎ完全解析の動かし方と
Flaskのhttps化のノウハウに絞ってポイントを書いておく。

◆Monaca + PWA + Firebase について
https://qiita.com/youwht/items/6c7712bfc7fd088223a2

◆GCP + Flask + 無料ドメイン について
https://qiita.com/youwht/items/9851c2ac9024633fc04e

どうぶつしょうぎ完全解析をPythonから動かす方法

前提として、今回は
Windows10に、WSL2(β)でubuntuを導入して動かした。
ちなみに、colaboratory上で動かせることも確認済み。

コードとデータを田中先生のサイトからダウンロードする。

wget https://dell.tanaka.ecc.u-tokyo.ac.jp/~ktanaka/dobutsushogi/dobutsu-src-20150109.tar.gz
wget https://dell.tanaka.ecc.u-tokyo.ac.jp/~ktanaka/dobutsushogi/dobutsu-dat.tar.gz

正しいURLや利用状況については、以下のサイトで確認すること。
https://dell.tanaka.ecc.u-tokyo.ac.jp/~ktanaka/dobutsushogi/

ダウンロードしてきたファイルを展開する

tar-xzf dobutsu-src-20150109.tar.gz
tar-xzf dobutsu-dat.tar.gz

展開できたか以下で確認

du-sh dobutsu/*cat"dobutsu/Makefile"

以下でmakeする。

make -f Makefile

っとその前に、コレでエラーになる場合、
gccとmakeをインストールする必要がある。
(colaboratoryで実施するならば最初から入っているので不要)

sudo apt-get install build-essential g++
sudo apt install make

棋譜データファイル等を置く場所を作り、棋譜データファイルを作成する。
田中先生のプログラムにおいては、
棋譜データファイルフォーマットが厳格に定義されており、
ちょっとでも棋譜の書き方を誤っているとエラーになるので注意。

mkdir data
# 初期配置状態の棋譜ファイルサンプルをダウンロードしてくる例
wget -P"data/""https://www.tanaka.ecc.u-tokyo.ac.jp/ktanaka/dobutsushogi/init.txt"# リダイレクトで出力して棋譜ファイルを作成する例echo-e" . -LI . \n-KI-ZO . \n . +ZO+KI\n . +LI . \n100100\n-">"data/B3B2HI-C4C3KI.txt"

上記のように、ダウンロードまたはechoで作成した棋譜ファイルを指定して、
「checkState」というプログラムを実行する。

./checkState data/init.txt
./checkState data/B3B2HI-C4C3KI.txt

これで、初期盤面に対する終局までの完全解析結果が表示されるハズ。すごい。

で、この結果をPythonプログラム側から扱えるようにするために、
以下のようにsubprocessによって、
Pythonからコマンドを叩いて結果を受け取れるようにする。

ここでのポイントは、「checkState」の結果文字列は標準出力ではなく
標準エラー出力側に出ているようなので、それを取得すること。

importsubprocessimportos#checkState本体
defEXEC_checkState(banmen_path):# ./checkState data/init.txt
# 上記コマンドの実行の代わり
# stderror側の文字列に結果が返ってくるようなので、, stderr=subprocess.STDOUT を追加している
checkState_result=subprocess.check_output(['./checkState',banmen_path],stderr=subprocess.STDOUT)#print(type(checkState_result)) ⇒ <class 'bytes'>
#check_outputの戻り値はバイナリ文字列になってるのでdecodeで文字列化する必要がある
checkState_result_str=checkState_result.decode('utf8')returncheckState_result_str

このノウハウを使えば、
「接待AI」という謎なテーマに行かなくても、
完全解析の結果に対してPythonでなんらかのデータ分析や、
機械学習を行うことが出来るようになるでしょう。

Flask + letsencriptがちょっと苦戦した話

まず、証明書を作るためのコマンドをインストールする

sudo apt install certbot

letsencriptは以下のパスにアクセスしてくることで
ttp://(ドメイン名)/.well-known/acme-challenge/(乱数的なファイル名)
そのサイトの存在を確認しようとしてくる。
この受け皿をFlask上のtemplateとして作る

letsencript関連部分のコード
fromflaskimportFlask,request,jsonify,render_template,redirect#Flaskの初期化
app=Flask(__name__)@app.route('/.well-known/acme-challenge/<filename>')defwell_known(filename):returnrender_template('.well-known/acme-challenge/'+filename)if__name__=='__main__':##HTTP通信で起動
#※letsencriptは主にこちらで動作(※ポート指定なしのデフォルト80番ポートで通信が来る)
#app.run(host='0.0.0.0', port=80, debug=True)

以下のコマンドでFlaskへのアクセスを試してもらう。

sudo certbot certonly --webroot-w(Flaskの起動しているフォルダの絶対パス)

成功すれば、こんな感じのログが出て、.pemファイルが生成される。
エラーが起こる場合は、
ドメインの名前解決、ポートに関する公開設定(GCP側のFW設定)、
Flaskの公開設定、templateフォルダ有無、などがよくある原因か。

 - Congratulations! Your certificate and chain have been saved at:
   /etc/letsencrypt/live/※※※/fullchain.pem
   Your key file has been saved at:
   /etc/letsencrypt/live/※※※/privkey.pem

以下のようにSSLの証明書として生成されたpemファイルを指定した状態で、
FlaskをHTTPS状態で起動すればOK

SSL化関連部分のコード
fromflaskimportFlask,request,jsonify,render_template,redirectimportsslapp=Flask(__name__)##SSLの証明書
context=ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)context.load_cert_chain('※※上記で作成したファイルの置き場所※※/fullchain.pem','※※上記で作成したファイルの置き場所※※/privkey.pem')if__name__=='__main__':##HTTPS通信で起動
#FirebaseはHTTPS前提
app.run(host='0.0.0.0',port=443,ssl_context=context,debug=True)

これでHTTPS通信は出来るようになるが、
letsencrypt側の通信が失敗するようになるため、
証明書の有効期限が切れた時の更新用に、
HTTPでのサーバ立ち上げ用のプログラムも
残しておいたほうがよさそう。
(一方のポートを80番にして、
 両方とも同時に立ち上げておいてもいいかも?)

証明書の自動更新設定とかも未実施だし、
ちょっとイマイチな方法かもしれない。

作成した「接待どうぶつしょうぎAI」についての考察

「雑魚」は次の手の勝敗判定以外はランダムという、
王手無視をしない中で最弱クラスのAIであるが、
先手「雑魚」vs後手「接待」の場合、
7割以上「雑魚」が勝つ。
「接待」は上手に負けて差し上げていると思う。

一方で、
先手「最強」vs後手「接待」では、
最強側にランダム性が無いため、
「接待」が100%勝つ

「接待」はヒトコトで言うと、
相手が間違えた手を指した場合にそれより大きく間違える、
という実装で作られているためだ。

「最強」は間違えないために、後手必勝であるこのゲームでは、
「最強」を相手にしたときだけ、「接待」が勝ってしまう。

もちろん途中でワザと負けさせる実装も可能だったが、
最弱よりも弱い「接待」が、
「最強」だけには勝ってしまう、という方が面白いかなと。

人間は必ずどこかで間違えるので、「接待」には原則勝つ。

・「最強」は「人間」に勝つ
・「人間」は「雑魚」に勝つ
・「雑魚」は「接待」に勝つ
・「接待」は「最強」に勝つ

この関係が接待AIの斬新性を表していると言えるだろう。

しかし、この接待AIの妙味は「おもてなし」である。
このAIの評価者としては、
本稿を読まずに使用した「お子様」が最も望ましい

ってか、本稿のネタを読んだ上で試したら、
接待要件の一つのの「手抜きを悟られない」に違反している。

どうぶつしょうぎのルールは知っているよ、
という人がやってみて、
多少苦戦するような流れも有りつつも、
最後は楽しく勝つことが出来るかどうか?

また、子供でも勝てるAIなのに、
上級者がやってみて、その歯ごたえに驚くかどうか?

作者が実施している限りでは、
ほぼあらゆるレベルの人と互角に戦えて、
ほぼ必ず人側が勝てる、という「接待」を
実現出来た気がするが、皆様にはどうだろうか?

シンプルな考え方の実装(=最善手乖離度)だけで
このレベルに高度な「接待」を実現することができた。

完全解析済みでない他のゲームにおいても、
評価関数がある程度正しければ、
同様の方法で「接待AI」が作成可能と見込まれる

既に将棋や囲碁のAIは人智を遥かに超えた動きをする。
これらの最強AIを、もっと人を「接待」する方向へも
適用していくべきではないだろうか?

あとがき

最初に「せったいどうぶつしょうぎ」を
作りたいと思ってから、長い長い月日にわたる脳内検討を経て、
やっと最近このアイデアを思いつき、
ひとつの形になったので嬉しく思う。

最も悩ましい点は、
「接待」という過度に人間的な行為について、
どのような状況を作れば良いとするのか?を、
コンピュータの世界で扱える定義で表現すること、
そしてそれを実現可能な実装方針と繋げること、の二点。
本稿の長文はその試行錯誤の過程を記録するものである。

「おもてなし」をプログラムで実現する実装案として、
また、「お子様への接待」として、
楽しんでいただければ幸いである。

以上です。
長文お付き合いいただいたかた、ありがとうございました。

Browsing Latest Articles All 26 Live