かもブログ

かもかも(@kam0_2)の雑記。地理やITに関するにわか稚拙センテンスの掃き溜め。

Windowsに社会的距離を強制させるアプリを作った話とその他

Windowsに社会的距離を強制させるアプリを作った話

 流行りに乗って、こんなものを作ってみた。

f:id:two_headed_duck:20200507222242p:plain
このようなタイトルバーが。。。

f:id:two_headed_duck:20200507222335p:plain
ソーシャルディスタンスを保つ!!

実行しているすべてのアプリケーションのタイトルバーに社会的距離を維持させるジョークソフトです。

遊び方

  1. ここからアプリケーションをダウンロードして起動します。
    f:id:two_headed_duck:20200507222517p:plain
    真ん中のボタンでOn/Offを切り替えられます。
  2. 真ん中のボタンを押すと動作します。実行されているアプリケーション、これから実行するアプリケーションの全てに影響を及ぼします。
  3. アンインストールはアプリケーションを削除するだけで終了します。

注意点

  • Windowsが不安定になることがあります。
  • 一部のアプリケーション(e.g. メモ帳)ではタイトルバーが変わりません。
  • タイトルバー以外のボタンなども変わることがあるようです。
  • ライセンスはMITです。使用にあたって発生した損害の責任を作者は負いかねます。

ソース

github.com

その他

どうでもいいい話

f:id:two_headed_duck:20200507223324p:plain
伺か(Materia-B)
 伺か(Materia-B)には「キャプションいじり」という機能があるんですね。これを始めてみたときはおったまげたわけですよ。任意のアプリケーションのタイトルバーが改変されて、面白おかしい文章になってしまうんですね。
 んで、そもそもどんな技術でそんなことができんのか気になったわけです。それでやってみました。難しくないし簡単でびっくりしちゃった。

Win32API

 大昔、C言語Windowsフォームを作っていた頃(今も現役かもしれないが)はWin32APIと呼ばれるものを呼び出して、ウィンドウやらボタンやらを作っていたわけです。(Win32API自体はグラフィックのみならず、I/O等様々な機能がありますが、)これらは"C:\Windows\System32\"下にある”***32.dll”がその正体で、Cの場合はwindows.hというラッパーを通じて、GUIを作ることになります。C#などで.NETを用いてGUIを作る場合も.NETがWin32APIのラッパーをしているだけであり、根底ではすべてWin32APIがGUIを構成しています。はい。
 で、このWin32API、ウインドウ1つに対して固有の「ウインドウハンドル(hwnd)」という値を割り振るんです。APIの関数に対して、このハンドルを渡して、ボタンを置きたいウインドウを指定したりします。多分。詳しく知りません。*1
 このウインドウハンドルさえあれば、他のアプリケーションのウインドウを好き放題操作可能なんです。AアプリのウインドウにBアプリからタイトルを書き換えることができてしまう。で、ハンドルは簡単に取得できます。ハンドルの一覧を取得するAPIがあります。
 どうやら昔から、他のアプリをいじるはマナー違反ということで、「禁止」ではなかったようなのです。とどのつまり、Windowsってよくわかんないねってお話でした。

 C#からWin32APIを呼ぶ場合はDLLをインポートしてラッパーメソッドを自力実装する必要があるのですが、ネットには神サイトが有りすでにラッパーメソッドが公開されています。

https://www.pinvoke.net/

神です。

*1:ウインドウメッセージとかもあるし本当に詳しく知りません

10万円をネットで申請した話。(with マイナンバーカード&ICカードリーダ)

特別定額給付金の申請が始まりました。

f:id:two_headed_duck:20200501170327p:plain  今日、5月1日から一部自治体で、10万円の特別定額給付金のネット申請が可能になったのでさっそくレビューしてみました。(アホ)

必要なものなど

 まず、申請を行うためには、

  • 世帯主であること
  • お住まいの自治体がネット申請を受付開始していること

が必要です。人口規模の多い自治体は遅くなりそうです。筆者の自治体は13万人ぐらいでしたが。
 さらに、

が必要です。今回は「ICカードリーダライタ*1」と「パソコン」を使ってやりました。ブラウザは「Google Chrome」です。動作環境については、マイナポータルをご覧ください。👉 https://img.myna.go.jp/html/dousakankyou.html

マイナポータルへ

myna.go.jp  初めての場合は「申請はこちら」を押す前に、「ICカードリーダでログイン」を押して、ログインできるか確かめます。Google Chromeの場合は拡張機能とアプリケーションのインストールが必要です。「ICカードリーダを初めてパソコンに繋いだ」場合はうまく行かないことがあります。再起動をすると直りますので諦めないようにしましょう。*2カードを差し込んだ状態で、ログインボタンを押すのがコツです。ログインには4桁のパスコードがいります。マイナンバーカード作成時に設定したやつです。
 一度ログインに成功したら、「申請はこちら」を押します。すると、郵便番号を入力を促されます。お住まいの自治体が申請を受け付けているか調べられます。 f:id:two_headed_duck:20200501172254p:plain ウィザードを進めていくと、上のような画面になり申請が始まります。

申請レビュー

f:id:two_headed_duck:20200501172513p:plain  メールアドレスと電話番号の登録から始まります。その後、自身の住所(申請者情報)などを入力していきます。住所などはカードを読み取ることで、自動入力してくれる機能があります。しかし、郵便番号は自動入力されなかったり、そこそこドジな機能でした。
 次に、「申請情報」です。申請者含めて受給する人の情報や、振込先の口座情報を入力します。銀行以外の信金とかでもいいみたいです。
f:id:two_headed_duck:20200501172956p:plain
 そしたら、その口座を所有していることを確認する書類を画像やPDFで提出します。ハイテクだぁ。申請者本人名義の口座でないといけません。筆者はネット銀行なのですが、どうやらネット銀行のスクショでもいいらしいです。寛大ですね。いくらでも捏造できるけどな。でも助かります。通帳ないし、キャッシュカードはクレジット付きで晒したくないので。
 最後に電子署名をします。電子署名のパスワードが必要です。電子署名時はマイナンバーカードをICカードリーダに挿します。
 そんなこんなで手続きが完了すると、申請したことの控えがもらえます。指定したメルアドに送信してもらうこともできます。ただし控えはZIPで来るので、スマホだとみづらいかもしれません。
 控えはPDFとCSVがあります。CSVはなんと文字コードUTF-8Excelで開くと文字化けしますが、UTF-8とは感動モノですね。日本国政府と一緒にShift-JISを滅ぼしましょう。

感想

 すげえよ。めちゃハイテクじゃん。Chromeでも動くし。IEでしか動かない&Java or ActiveXゴリゴリかと思ってました。*3この場で謝罪いたします。このまま手書きとはんこを抹殺してください。よろしくお願い申し上げます。
 TLは技術好きが多いのでレビュー記事出るかなと思ったんですけど、みんなツイートすらしないので記事書いちゃいました。みんな都会に住んでんのか。そもそも世帯主じゃないか。

5/8追記

f:id:two_headed_duck:20200508213713p:plain
こんなメールが来ました。マイナポータルに登録した情報が自治体に流れたようです。なるほど。

5/26追記

f:id:two_headed_duck:20200526223638p:plain
振り込まれた!
ちなみに、口座確認書類はネットバンキングのスクショっていういくらでも改ざんできるガバガバ画像でしたが普通に振り込まれました。悪いナレッジを置いておきますね。

*1:ちなみにこのためだけにメルカリで380円で買った。公式にはマイナンバーカード非対応・住基カードe-Tax対応だったが使えました。

*2:ICカードを読み書きするサービスが起動していないことに起因。一度ICカードリードライタをつなぐと、以降はサービスが常駐するようになるらしい。

*3:いろいろあったらしい。👉https://twitter.com/masanork/status/1253995609977057280

木更津のミステリー・サークル(浸透実験池跡)

木更津市の地図を眺めていると

f:id:two_headed_duck:20200407214609p:plain
ミステリー・サークルだ!!!
 奇妙な地形を見つけられます。これはいったい?調査してきました。*1

アクセス

 
公共交通機関の場合は、「三井アウレットパーク木更津」をまず目指します。バスが出ています。そこから歩く感じです。

小櫃川オビツガワ)河口干潟/三角州 へ

f:id:two_headed_duck:20200407215209p:plain まずは、上の地図の①の地点から小櫃川河口干潟に入ります。上の地図は入り口にあります。車は入れません。干潟ですので、潮の満ち引きに注意してください。満潮時は避けましょう。

砂利道を道なりに進みます。

f:id:two_headed_duck:20200407215504p:plain 自然豊かで、鳥などもたくさんいます。ただ、写真にも写っているように近くにある基地からの軍用ヘリの騒音がすごいときも多々あります。自転車でもいけます。

道の終わりには砂浜があります

f:id:two_headed_duck:20200407215649p:plain さて、この浜は地図の⑩か⑬なのですが、⑥ミステリー・サークル方面への道がありません。

道なき道をゆけ

f:id:two_headed_duck:20200407215915p:plain このように道が途切れているのですが、獣道のような人一人通れる道が探すとあります。
f:id:two_headed_duck:20200407220016p:plain こういう感じです。

到着

f:id:two_headed_duck:20200407220201p:plain
 到着です。池は水深10mあります。落ちると危険です。宇宙人はいませんでした。左奥に見えるのはホテル三~日~月~です。 f:id:two_headed_duck:20200407220348p:plain
f:id:two_headed_duck:20200407220415p:plain 春だったので、きれいな花が咲いていました。

これは何

 これはミステリー・サークルではなく、日本製鐵(旧:八幡製鐵)が1960年代に作った「浸透実験池」です。どんな実験かというと、「淡水の工業用水を海辺で確保する」ために河口湖を作るというものでした。
 浸透圧とは、「濃度の濃い液体と薄い液体が半透膜によって仕切られている時、濃い液体の方に薄い液体の水分が流入する」という見かけ上の圧力です。(??筆者は頭悪い??)半透膜とは、液体に溶けている物質(溶質)は通さず、水(溶媒)だけを通す膜のことです。「青菜に塩」や「ナメクジに塩」は浸透圧で説明できます。
 ここで、河口湖を作った場合、海水が土壌を介して淡水に侵入してしまいます。そこで、淡水の河口湖を覆うように、ドーナツ状の淡水の湖をもう一個つくります。すると、「侵入してきた海水」と「外側の湖」で浸透現象が起こり、外側の湖から水分が土壌の海水に流れることで、中央の内側の湖は海水から守られる、という目論見です。
 しかし、土壌の質などを変えて3年間実験したものの、うまい具合にはいかず、オイルショックもあり、実験は中止、池は放棄されたのでした。
 以下のサイトが詳しいです。

www.sugolog.jp

 

*1:コロナについては、近所を散歩していただけなので不要不急ですが、問題ではないと思います。

低メモリ(RAM)環境で、gensimのword2vecモデルを使うテクニック(備忘録)

この記事について

 Python 3.x で自然言語処理ライブラリgensimでword2vecを使います。学習済みのモデルデータを読み込んで、各単語のベクトルをえられるようにしたいとき、低メモリ(RAM)環境では、うまく行かないことがあります。この記事では、低メモリ環境で、学習済みword2vecモデルから各単語ベクトルを抽出する方法を紹介します。

問題の根源

 gensimのword2vecモデルはmost_similar()メソッドのように、「すべてのベクトルをメモリ上に展開していないと使えないメソッド」を含んでいます。そのため、モデルをロードする際に、モデルのサイズ以上の空きメモリがないと、読み込みに失敗します。自分の手元には日本語版Wikipediaから作ったモデルがありますが、モデルと付随するファイル含めて合計で1.3GBあります。一般的なパソコンでは問題ないですが、VPSなどの極めてメモリが少ない環境では問題になります。

解決法1 KeyedVectorsのみにする

 gensimのword2vecモデルは単語のベクトル情報以外にも様々な情報を持っています。これらを切り落として、単純なベクトルのみにします。

f:id:two_headed_duck:20200405150704p:plain
https://radimrehurek.com/gensim/models/keyedvectors.html より引用
再学習はできなくなりますが、オブジェクトを軽量化できます。また、オリジナルのword2vec形式での保存/読み込みが可能になります。これは後に重要になります。

from gensim.models import Word2Vec
model = Word2Vec.load("jawiki.model") # gensim形式のモデルをロードします
model.wv.save("jawiki.kv")  # KeyedVectorsのみを保存します。

当方では、1.3GBから732MBに減量することができました。これを読み込むには以下のようにします。

from gensim.models import KeyedVectors
kvs = KeyedVectors.load("jawiki.kv")
for l in kvs.most_similar(positive=["女性", "皇帝"]):
    print(l)

most_similar()similarity()などがそのまま使えます。kvs["安倍晋三"]のようにしてここのベクトルが得られます。

解決法2 正規化して元データを消す

 正規化とは「すべての単語ベクトルを単位ベクトルにする」ことです。単語ベクトルを扱う場合は、ベクトルの長さを無視して考えたほうが効果的であることが知られています。出現頻度や単語の文字長にベクトルの長さは影響されるため、単語同士の類似度を考える上では余計です。また、similarity()で利用されるコサイン類似度は比較対象のベクトルの角度差のみで計算されます。事前に正規化することで、後の計算処理を減らすことができます。
 事前正規化を行っている場合、元のオリジナルベクトルを消しておくことで、メモリー消費量を減らせます。半分にできるはずです。事前正規化はinit_sims()メソッドで行いますが、init_sims(replace=True)とすると、オリジナルのベクトルを忘れます。これを行うと、再学習はできなくなります。

kvs.init_sims(replace=True)
kvs.save("jawiki_normalized.kv")

解決法3 読み込む単語数を絞り込む

 word2vecのオリジナル形式からモデルを読み込む際は読み込む単語数を絞り込むことができます。まずは、gensim形式のモデルをword2vec形式に変換します。

kvs.save_word2vec_format("jawiki_normalized.kv.bin", binary=True)

バイナリ形式で保存することで、若干のファイル軽量化効果があります。50MBぐらい小さくなりました。また、単一のファイルになります。読み込む場合はこうします。

kvs = KeyedVectors.load_word2vec_format(
        "jawiki_normalized.kv.bin",
        binary=True,
        limit=10000)
print(len(kvs.vocab))  # -> 10000

limit=*を書き換えることで任意の数の単語のみを読み込みます。どの単語が選ばれるかなどはよく調べてません。

最終解決法

 今までの方法は結局の所、モデルをすべてメモリにロードする必要がありました。最終解決法ではgensimを使うのをやめて、必要な単語のベクトルだけをメモリにロードできるようにします。most_similar()などは使えなくなりますが、similarity()などは自分で実装することで、使えるようになります。
 まずは、gensimからモデルをテキスト形式で吐かせます。

kvs = KeyedVectors.load("jawiki_normalized.kv")
kvs.save_word2vec_format("jawiki_normalized.kv.txt", binary=False)

headしてみるとこんなデータができます

879000 200
の -0.18367043 -0.029534407 -0.04568029 0.059862584 -0.07997447 -0.046858925 0.14023784 0.07425413 -0.008315135 -0.12953435 -0.026671728 0.0070677847 0.16410053 -0.018117158 0.010087145 0.01466953 0.041376486 -0.1008434 -0.06180911 -0.058874626 0.008909045 0.044234663 0.014539371 -0.0028310632 0.018776815 0.049506992 0.073248215 -3.157805e-05 0.10673941 0.03583274 -0.10652217 -0.059396442 -0.0261147 -0.03284311 0.081868224 -0.012962754 -0.034510043 -0.10235525 -0.0769274 0.082873024 -0.012097447 0.0727865 0.014861626 0.03009095 -0.030071974 0.027952265 -0.025505234 -0.05018914 0.0417648 -0.02850465 0.07273282 -0.09483565 -0.04345289 0.15165983 -0.031632055 0.17059058 -0.044084977 0.0052842456 -0.028103009 -0.04878716 -0.024238652 0.054972656 -0.03674599 -0.040995907 -0.0050267056 -0.055824015 -0.062449194 0.13537866 0.07244164 -0.0714956 -0.019936046 -0.16671564 -0.100499384 0.007035132 -0.11507068 0.04457269 -0.13684298 -0.0803739 -0.017586963 0.08030843 0.033362087 -0.108916104 0.039713085 0.04878837 -0.04515106 -0.018243527 0.12831812 -0.017795842 0.04866434 -0.060816325 -0.050964024 -0.01270505 -0.033732776 -0.08854351 0.06676082 -0.05967011 0.07151932 0.061369702 -0.044702165 0.05061344 0.14359577 -0.051136304 0.0615069 -0.022738786 0.051260248 -0.07742282 0.02800144 0.03599446 0.06877219 -0.01719892 0.06675788 0.06307474 -0.036053922 -0.22192973 -0.048423205 -0.04822691 0.059284963 0.052187417 -0.07442425 -0.07336827 -0.118471846 -0.08167546 -0.013464366 0.006391276 0.0012051899 -0.114997625 -0.055676162 -0.053286873 0.047492016 -0.053404514 -0.042560868 0.039681405 0.11049425 0.07539424 -0.08105653 0.06514334 0.116133265 0.0396685 0.050337143 -0.055580735 -0.12020915 0.010553302 0.100789204 0.057277914 -0.01211393 -0.0009179714 0.08036089 0.0055075907 -0.17496698 0.13960186 -0.016151525 -0.062465787 0.038296852 0.017309219 0.012427443 -0.02194943 -0.09682389 -0.06353024 -0.060949646 0.020672444 0.13299905 0.028146993 0.07839865 0.022320326 -0.047637332 0.0005690668 0.07873213 -0.02366066 0.053062834 -0.011705997 0.056978896 -0.13023455 -0.048329093 -0.012450362 0.08268035 0.027270395 -0.09271215 0.014355482 0.11091846 -0.016312167 -0.06322232 0.024058044 -0.15635346 -0.060165983 -0.06801029 0.053233985 -0.091366336 -0.04244707 0.03506038 0.010074944 -0.039310183 0.003523075 0.026648186 -0.09815444 0.0450355 0.0151189305 -0.09069526 -0.015519027 0.06754098 -0.021642119
(略)

先頭行に単語数 ベクトル次元数があり、以降の行に単語 ベクトル...となっています。必要な単語だけこのファイルからベクトルを読み込むようにすれば、低メモリ環境でも何十万というヴォキャブラリを活かすことができます。

SQLiteのDBにしてしまう。

 個人的なおすすめはこのファイルをさらにデータベースにしてしまうことです。CSVなどより検索が早くなります。クソみたいなコードを貼ります。

import sqlite3
import logging

WORD2VEC_TEXT = "jawiki_normalized.kv.txt"
DB_PATH = "jawiki_normalized.kv.db"

def return_column_names(n):
    if n >= 0:
        r = "keyword TEXT PRIMARY KEY,"
        for i in range(n):
            r += f" vec{i} integer,"
    else:
        r = "?,"
        for i in range(-1*n):
            r += "?,"
    return r[0:-1]

if __name__=="__main__":

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

    con = sqlite3.connect(DB_PATH)
    c = con.cursor()

    with open(WORD2VEC_TEXT, "r") as f:
        fl = f.readline()
        logging.info("Start: " + fl)
        count = int(fl.split(" ")[0])
        size = int(fl.split(" ")[1])
        c.execute(f"create table kv( {return_column_names(size)} );")
        for i in range(2, count+2):
            line = f.readline()
            row = line.split(" ")
            if len(row) != (size+1):
                logging.warn(f"Index error: line: {i} text: {line}")
                continue
            c.execute(f"insert into kv values ({return_column_names(-1*size)});", row)
            if i % 10**4 == 0:
                logging.info(f"{i} / {count} done. {i/count*100} percent.")
                con.commit()
    logging.info("DONE!!!")
    con.commit()
    con.close()

DBができたら、インデックスを作っておきましょう。

$ sqlite3 jawiki_normalized.kv.db
SQLite version 3.22.0 2018-01-22 18:45:57
Enter ".help" for usage hints.
sqlite> create index keyword_index on kv(keyword);
sqlite> .exit

DBのCursorを受け取って、ベクトルを返す関数は以下のようになります。

def get_vec(c_kv, surface):
    c_kv.execute("select * from kv where keyword=?;", (surface,))
    r = c_kv.fetchall()[0]
    return np.array(r[1:])

c_kvがDBのCursorで、surfaceが単語、単語が見つからない場合はIndexErrorになります。

コサイン類似度の算出

 別にユークリッド距離でも構わないと思いますが、ユークリッド距離は「短いほど近く」コサイン類似度は「大きいほど近く」なります。正規化を行っている場合は、ユークリッド距離は2の時が一番遠くなります。コサイン類似度では-1が一番遠く、1が一番近くなります。コサイン類似度はsimilarity()などで使われています。gensimを使わないので自分で実装する必要があります。

コサイン類似度ってなんやねん

コサイン類似度は2つのベクトルabの間の角度をθとしたときのcosθのこと
(やたら難しい概念かと思ったら高校1年生でもわかりそうなものでびっくりしちゃった。)具体的にどう求めるかというと、ベクトルの内角の公式より、

f:id:two_headed_duck:20200405164656p:plain

そんだけ。ちなみに、abユークリッド距離をdとおくと、
f:id:two_headed_duck:20200405171204p:plain
となり、ここで、abは正規化されていて単位ベクトルなので、
f:id:two_headed_duck:20200405171937p:plain
という関係がコサイン類似度とユークリッド距離で成り立つ。

実装

ユークリッド距離を求めてから、コサイン類似度を求めたほうが早い気がするのでそうする。
f:id:two_headed_duck:20200405172324p:plain より、

def get_similarity(vec1, vec2):
    # vec1, vec2は事前正規化済みとして
    # ユークリッド距離
    diff = (vec1 - vec2) ** 2
    d_2 = diff.sum()    # ユークリッド距離の二乗
    # コサイン類似度に
    cos_sim = (2 - d_2) / 2
    return cos_sim

とすると求められる。簡単じゃん。

その他

 最終的解決法をつかうとモデルをメモリにロードする必要がなくなるので、ロード時間を大幅に減らせます。most_similar()などを使わなければ、限りなくいい方法だと思います。よんでくれてありがとね。

しりとりを科学する。 ~論理に基づいた最強のしりとり戦法を求めて~

長い文章を読みたくない人へ


このしりとりCPU・TwitterBotと戦ってみてね。

しりとりに必勝法はあるのか(ポエム)

 昔からしりとりが好きだった。しりとりは一見すると、勝ち負けもないようなただの言葉の言い合いに過ぎない。しかし、しりとりには明らかに「強い人」「弱い人」が存在する。どうすれば強くなれるのだろうか。
f:id:two_headed_duck:20200401191434p:plain  Googleで適当に検索してみると、色々出てくる。「る攻め」がいいとか「ぷ攻め」がいいとか。僕は昔「む攻め」を極めたし、友達には「や攻め」使いがいた。しかし、これらの言説に総じて言えるのは、「論拠の不足」であろう。感覚的に「〇〇が強い」というのは、信用に値するのであろうか。そんなことを日々思っていたときに、ある本に出会った。とつげき東北氏の「科学する麻雀」である。麻雀は「ツキと流れ」が支配する感覚のゲームと長い間思われていた。しかし、統計学やコンピュータシミュレーションを用いることで「麻雀必勝法」を科学的に導出できることをこの本は示している。しりとりの話から逸れてしまったが、僕はしりとりでも同じことが研究できるはずだと思ったのである。
 そこで、僕の稚拙なプログラミング能力を生かしてしりとりを科学的に極めてやろうと思ったのだ。僕はこの計画に「Ngok」という名前をつけた。ンゴックとはアフリカ、南スーダン共和国遊牧民族の名前であり、また、「Ngok is the Gear Of Kotoba-asobi」の頭文字である。GitHubにリポジトリを公開しているので、暇だったら覗いてほしい。とはいえ、人に見せるものではないような気もする。
 この記事はそんなくだらない遊びの中間報告である。書いている人の頭は良くない。

一般化してみる

 しりとりとはなんだろうか。流れを一般化してみよう。
 はじめに、単語というものを定義する。単語は「サーフェス」と「ヨミガナ」という属性を持つ。「サーフェス」というのは分かりづらい言い方だが、"見た目"のことだ。*1
 しりとりのルールを一般化しよう。しかし、ここで一旦制約をつける。しりとりは1人以上いれば遊べるゲームではあるが、今回の研究では2人でプレイするものだとする。3人以上になると、麻雀で言うところの「コンビ打ち」のように、誰かを蹴落とすための戦略性が生じてくるが、今回はそれを抜きにして、シンプルに2人対戦で考える。
 ここで、終端文字取得関数E(w)を考える。E(w)はルールによって定義が異なる。E(w)は単語を引数に取り、その単語の「終端文字」を返す。「終端文字」が1文字とは限らないことに留意しよう。次に敗北判定関数L(c)を定義しよう。L(c)は「終端文字」を引数に取り、これが敗北する終端文字であれば、敗北の判定を返す。*2
 次に、単語を記憶するリストWLを定義する。このリスト末尾に単語を追加する関数P(w)と、与えたられた単語(のヨミガナ)がリストの中に存在しないか、確認する関数C(w)も定義できる。C(w)はルールによって定義が異なる。「サーフェス」で区別するのか、「ヨミガナ」で区別するのかという問題である。そして、WLの末尾要素を取得する関数S()を定義する。
 最後に、プレイヤーが単語を思いつく関数G(c)を定義する。G(c)は終端文字を引数に持ち、その終端文字で始まる単語を返す。ただし、返す単語はユニーク(一意)であるとする。*3もし、返せないときは敗北とする。
 これまでの定義から、しりとりの1ターン、プレイヤーが自分の番にやるべきことは以下のように一般化される。

  1. x <- S() # 前回言われた単語を参照する
  2. c <- E(w) # その単語の終端を得る
  3. if L(c) { # ここでL(c)が真なら勝利する }
  4. w <- G(c) # 単語を思いつく、思いつかない場合は負けとするが割愛
  5. if !C(w) { P(w) } else { Goto 4 } # すでに言われた語でなければ、WTに追加する。そうでない場合は、考え直す。

1~5が完了すると、同じことをもうひとりのプレイヤーも行う。これを延々と繰り返すのがしりとりであると定義できる。このように一般化することで、しりとりシミュレータを作ることもできる。

単語の獲得

 しりとりをコンピュータ・シミュレーションする際に問題となるのは、G(c)の実装である。G(c)は「知っている単語の中でcから始まる単語を返す」部分と、「返された単語の中で何で答えるのがいいのかを戦略的に選ぶ」部分に分けることができる。ここでは前者、「知っている単語の中で~」すなわち、コンピュータにどのようにして単語を覚えさせるのかを検討する。
 今回の研究では、mecab-ipadic-NEologdという自然言語処理に使われるOSSの辞書を利用した。IPA辞書と呼ばれる基礎的な日本語の単語に加えて、mecab-ipadic-NEologdはインターネット上から最新の単語を取得し、辞書に追加するシステムを備えているため、最新のトレンド用語やマイナーな学術用語もカヴァーしている。オタク用語もバッチリである。
 しかし、問題もある。この辞書は形態素解析向けの辞書であるため、「表記ゆれ」の語が多く存在する。例えば、「鴨そば」も「鴨蕎麦」も「かも蕎麦」も「かもそば」もそれぞれ別単語として登録されているイメージである。これらを別単語と考えるのは無理がある。そこで、今回の研究では「ヨミガナユニークルール」を導入する。同じヨミガナの語は1度しか使えないというルールである。同音異義語は認めない。こうすることで、「カモソバ」1単語と捉えることができる。同音異義語を認めるルールの場合、同音異義語か否かをコンピュータが判定することは難しく、*4このルールはコンピュータを扱う上で合理的だと思われる。
 また、一般的(世間的)に、しりとりは名詞のみという原則がある。そこで、mecab-ipadic-NEologdから名詞に関わる単語を抽出した結果、2020/3/15の最終更新データでは1,982,873単語となった。広辞苑は約25万単語と言われており、約8倍の単語数である。(広辞苑は動詞とかも含んでいるが)ここまで多い単語数であれば、このデータは「日本語の単語母集団」に対する「十分な数の標本」と見ることができる。(筆者は統計に理解はない)この単語データはシミュレーションのコンピュータの記憶に使えるだけでなく、「日本語の単語全体」の傾向を掴むのにも使えるはずだ。
 「知っている単語の中でcから始まる単語を返す」はこれでもう実装できる。単語データが有れば、コンピュータにはあとは朝飯前である。筆者はSQLiteのデータベースに落とし込むことで、単語検索に十分なスピードが出るようにしている。返された単語の中で何で答えるのがいいのかを戦略的に選ぶ」に関しては、後述する。

○攻めについて

 しりとりの戦術といえば、「○攻め」である。巷にはさまざまな「○攻め」言説が溢れている。本当に強いのは何攻めだろうか。調べてみよう。

その前に、ルール定義が必要だ

 しりとりには様々なルールが有る。Ngokのプログラムは様々なルールに対応した各種関数を持っている。それらにはしりとりのルールを素数の積で指定する。

# モードは各オプションの積で指定します。
   # 何も指定しない場合、厳密に一致する必要があります。    
   # 相互交換可能オプション
   # 2 濁点<=>濁点              e.g.)帝国=>軍事法廷
   # 3 半濁点<=>濁点            e.g.)法被=>広島
   # 小文字に関するオプションは併用不可です。
   # 何も指定しない場合は、自動車=>社宅のようになります。
   # 5 小文字<=>標準文字        e.g.)自動車=>ヤクザ               
   # 7 小文字を除く             e.g.)自動車=>指紋
   # 11 小文字は負け            e.g.)自動車(負け)
   # 13 (予約)
   # 長音のルールも併用不可です。何も指定しないと、マヨラー=>ラーメンのようになります。
   # 17 長音は省く              e.g.)メーデー=>デビルマン
   # 19 長音は母音を取る        e.g.)メーデー=>エンブレム
   # 23 長音は負け              e.g.)メーデー(負け)
   # 29 (予約)
   # その他のルール
   # 31 「ン」で負けにしない。  e.g.)食パン=>ンゴック族
   # 37 ジ<=>ヂ                 e.g.)鼻血=>ジルコニウム
   # 41 ズ<=>ヅ                 e.g.)木更津=>ずんだ餅
   # 43 ヲ<=>オ                 e.g.)みつを。=>折り紙
# 推奨は2*3*5*17*43です。

今回は筆者の偏見で最も一般的と思われる2*3*5*17*43=21930で見てみよう。簡単に言えば「濁点半濁点無視」「小文字は大文字」「長音無視」「を=お」である。しりとりのルールは簡単なようで難しい。今回最も苦労したのは、あまたのルールに対応するE(w)を作ることであった。

Xで終わりYで始まる単語

https://github.com/comradeKamoKamo/Ngok/raw/dev/output_21930/cm.png?raw=true

 この図はX(横軸)で始まり、Y(縦軸)で終わる単語の数をヒートマップで表したものである。注意点としては、カラーバーが5000で頭打ちであるが、実際には5000以上の単語数を持つXYの組み合わせも多く存在する。
 これを見ると、日本語の単語には偏りがあることがわかる。よく言語学の領域では「日本語の単語にはら行から始まる単語は少ない」という説がある。すでに証明されてるだろうが、この図をつかってもたやすく証明することができる。逆に「な行で終わる言葉は少ない」という事実もわかる。
 ここで、二次元配列CMを定義する。CM[Y,X]でYで始まり、Xで終わる単語の数を返すものとする。これは以降の議論で頻繁に使用する。
 図を見ると、何攻めにしたらいいかのヒントが見つかるかもしれない、がもっと簡単な方法がある。「単語終始比率R」というパラメータを導入する。ある文字cについての単語終始比率R(c)は

R(c) = cで終わる単語の数の合計 / cで始まる単語の数の合計

とする。R(c)が高い値の時、その文字は「攻めやすく」相手が「返しづらい」ということである。これを各文字ごとに取得し、グラフにしてみよう。

https://github.com/comradeKamoKamo/Ngok/blob/dev/output_21930/rate.png?raw=true

これを見ると、一目瞭然、「る攻め」最強説誕生である。群を抜いている。次点が「う攻め」でこれは正直以外だった。普段から「う」で攻めていた人の話は聞いたことがなかった。僕の「む攻め」、友達の「や攻め」は悪くないが良くもない。

番外編:「ぷ攻め」

「ぷ攻め」が強いかどうか、21930のルールでは半濁点を無視するため、わからない。別途検証してみよう。ルールは5*17*37*41*43= 5544635 である。 https://github.com/comradeKamoKamo/Ngok/blob/twi_bot/output_5544635/rate.png?raw=true ぷが強いとか言ってた人はしりとりやめたほうがいいんじゃないかなと思える結果である。(別に悪くはない。)濁点半濁点無視をしない場合は、「る攻め」よりも「ず攻め」が有利である。これは僕の父親がよく使っていた戦法であり感慨深い。「ず攻め」の強さは圧倒的で、濁点半濁点無視でない場合はず攻めするしかない。が、基本は21930のルールで話をすすめる。

先行研究者様がいました。(追記)

単語分析に関して、先行研究者様がいました・・・。

active-galactic.hatenablog.com

qiita.com

驚くほどやり方が似てますけど本当に偶然です。データの扱いが違うのか、ルールが違うのか、やや結果が異なっていますね。

戦略

 しりとりは攻めるだけだろうか。その前提から検証してみることにする。まずは、戦術を実装しよう。

戦略論の前にガバイ理論

 「攻めるか」「守るか」これが基本である。ここで、「攻め方」「守り方」は極力シンプルに実装する。このような「攻守」の戦略を立てる際は、シンプルな戦術が有利であるということが一般的な気がするからだ。*5冒頭のポエムで科学がどうこう言っていたのに、この根拠もないガバイ理論を出して申し訳ない。
 要するに、各戦術は極力単純化して考えシミュレーションしてみようということである。しかし、各戦術が有効な戦術かどうかをしっかりと検証できる手法はない。「この戦術のが強い」という物があれば教えてほしい。

攻撃的戦術

 攻撃的戦術は「cで始まり、R(c)が最大となる文字で終わる単語を返す」ものである。それだけ。

防御的戦術

 防御的戦術は「cで始まり、CM[x,c]が最小となるような文字xで終わる単語を返す」とする。つまり、相手にまたcで攻められることを防ぐ。

中間戦術

 中間戦術は、

  1. rate = WLにおけるcで終わる単語数 / WLにおける総単語数 とし、
  2. 一様乱数randを0.0~1.0で発生させ、
  3. rate >= rand なら防御し、 そうでなければ、攻撃する。

というルーチンを取る。c攻めされている割合で防御する。

ランダム戦術

 ランダムは「cで始まる単語の中から、ランダムで1つ返す」という戦術である。

シミュレーション

 先程のしりとり一般化を用いて、シミュレーションができる。十分な回数のシミュレーションを行った。*6
 結果はどうやっても、攻撃的戦術が勝利した。防御的戦術は攻撃的戦術に対して逆効果だったのだ。(防御的戦術はランダムよりは圧倒的に強いため、効果がないわけではない。)この時点で、中間戦術は意味がない。中間戦術は防御に勝ち、攻撃に負けるからである。ランダムはどの戦術よりも悪かった。
 防御的戦術の失敗は、結局の所、防御といっても「○攻め」になってしまうからであろう。実際には「る攻め」に対して「ぬ攻め」で返していた。防御のためとはいっても、効率の悪い攻め方であったら負けて当然なのである。
 以上の結果から、しりとりにおいては「攻め」は圧倒的な王道戦術と言える。また、ランダム同士の対戦シミレーションから「先攻」「後攻」に特に優劣はないこともわかった。*7

まとめ

 以上のことから、しりとりの最強戦術はここで述べた「攻撃的戦術」である。つまり、「○攻め」であり、その攻め文字は、ここで示された単語終始比率が高い順に行えばいい。あとは、単語を覚えるだけと言える。それだけである。つまらないゲームですね。
 しりとりは突き詰めると面白くなくなるという実感が以前からあったが、真実だったようだ。

本当にそれだけ?

 本当にそれだけだろうか。コンピュータは単語の意味を理解できないため、上のような結果になっている。しかし、実際には、ある人はずっと同じジャンルの言葉で返す、とかそういうのがあるはずである。ここからは、科学的に検証できないが、筆者が強いと思う戦い方である。つまり、ネット上の根拠のないしりとり戦術言説であり読まなくても良い。

  • 返す単語は相手の単語と関連した単語のがよい。相手の語彙を減らせる。
  • 関連語のないときはなるべく短い単語が良い。短い単語は相手も思いつきやすい。

根拠がないので、本当かわからない。

最強?のしりとりCPU「ンゴック君」

以上を踏まえて、作成されたしりとりCPUがンゴック君である。これと対戦して、強さを体感してみてほしいと思う。「本当にそれだけ?」の作戦も盛り込まれている。word2vecという機械学習アルゴリズムは、コンピュータに「関連語」を理解させる手助けになる。これを利用し、可能であれば関連語を返してくれる。ぜひ遊んでみてほしい。

今後の展望

 今後があるかもわかっていない。考えているのは、人名や外来語を禁止したときにどうなるのかといったルールを制限した場合である。そのような際には、「る攻め」神話が大きく崩れ、ほんとうの意味での「しりとり」が見えてくるかもしれない。
 ンゴック君に関しては、word2vecよりも強力な機械学習モデルを導入するなどしてみたいと思っている。BERTの話です。

謝辞

 ここまで読んでくれてありがとね!

*1:見た目とは、例えば、漢字で表記したときの単語の見た目である。しかし、口頭でしりとりを行う際はそれはアクセントや声量を絡めた発音方式を含むかもしれない。後述するが、今回の研究では「ユニークヨミガナルール」を採用しているため、サーフェスはさほど重要でない。アクセントや単語の意味によって、同音異義語を容認するルールの場合は重要である。

*2:通常は「ン」であるが、ルールによっては異なる。長音や「ヂ」を敗北とする場合など。

*3:細かいことはそんなに気にしないでほしい。こうしないと4-5で無限ループして兵庫県警に捕まってしまう。

*4:「君」と「黄身」はサーフェスで区別できるが、例えば、地名としての「四日市」と市としての「四日市」は区別が難しい。あまりいい例ではないかも。

*5:リチャード・ドーキンスの「利己的な遺伝子」でそんなのを読んだ気がする。

*6:ただし、コンピュータにとってはサーフェスはどうでもいいため、単語を使わずに、配列CMの数字を引き算するだけで実際はシミュレーションする。ヨミガナユニークルールの利点である。また、次の注で述べるとおり、中間戦術とランダム戦術が絡まないシミレーションは回数を重ねる必要はない。

*7:攻撃的戦術同士を戦わせると、必ず後攻が勝つ。しかし、これは意味のない結果で、攻撃的戦術や防御的戦術は乱数要素を含まないため、勝敗は単語データの単語数の運命で決まるからである。

4月2日 緊急事態宣言 不思議なことはすぐ気になる。

4月2日 緊急事態宣言 で検索しても何もヒットしないらしい。

f:id:two_headed_duck:20200330232950p:plain 4/2や4月3日、1日ではヒットする。何がおかしい。英語でも出ない。
海外サーバだと検索できるみたい...魚拓👉https://archive.is/sWdEh
国内サーバから魚拓を取れた。目的外使用だが、👉https://securl.nu/saved.php?key=20200330582f07358d39568e613d36a2f6384ec8
「非常事態宣言」でも検索できない模様。「4月」と「2日」を区切ると検索可能になる。

そんだけ

個人的にこういうやり方は嫌い。百度で「天安門事件」を検索してヒットしないのはおかしい。それと同じだ。Googleくんさぁ。

追記

朝日新聞の取材によると、Google曰く「不具合」。

www.asahi.com

グーグル日本法人にこの検索結果について取材すると、「ご指摘の問題について把握しており、現在調査対応しています」と説明。「情報にアクセスし使えるようにすることはグーグルの使命です。私たちのシステムは、アルゴリズムにより自動的に公平な検索結果を生成するように設計されており、これにより特定の個人・団体の偏見や意図に影響されることを防いでいます」とコメントし、検索結果の不具合で意図的なものではないことを強調した。

本当か?そうであるなら不具合の原因を開示してほしい。状況に応じて追記します。

ごみ

4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言 4月2日 緊急事態宣言

東京湾観音に行ってきた(不要不急)

東京湾観音!

f:id:two_headed_duck:20200322212135p:plain
東京湾観音
 恥ずかしながら、東京湾観音の存在を全く知らなかったんです。こんなにでかい観音像が東京湾にあったなんて、ということで紹介します。

アクセス

f:id:two_headed_duck:20200322212321p:plain
※話題の香川は関係ありません
 最寄り駅はJR内房線「佐貫町駅」。東京湾といっても房総半島側にあります。電車の本数は1時間に1本程度。東京からは総武線快速内房線直通「君津」行で、君津で乗り換えることで簡単に行けます。駅前はカフェ以外なにもないです。コンビニもありません。
 昔は、駅から観音像までのバスがあったようですが、今は廃止されたようです。駅を出たら、左に進んでいきます。
f:id:two_headed_duck:20200322212816p:plain
表参道(ただの道)
 進むと表参道への分岐があります。表参道をひたすら登っていきます。歩行者に優しい道ではありません。車に注意です。
f:id:two_headed_duck:20200322212946p:plain
こういう、うっそうとした道を約20分登ります
 進んでいくと、老人ホームがあります。するとすぐそこに巨大な観音様が見えるはずです。観音様しかないような場所に老人ホームを立てるセンスには敬服します。

東京湾観音について

 f:id:two_headed_duck:20200322213401p:plain
 東京湾観音は戦後に世界平和を目的に建てられた、全長56mの救世観音像です。新しい観音様の部類でしょうか。拝観料は大人500円で、観音様の胎内に入ることができます。

東京湾観音は、東京湾を一望できる南房総国定公園(大坪山)に建つ高さ56mの救世観音です。 本像は昭和36年に宇佐美政衛氏が世界平和の理念の元に建立されました。 そのお姿は平和な世の中を祈願した珠を懐き、常に我々の幸せを願う安らかなお顔です。 また、美術作品としても有名な東京湾観音の原型の作者は、国際グランプリを受賞した彫刻家の長谷川昴(こう)氏です。観音様の胎内にも長谷川氏の作品が多数あり>ます。天上界までの324段の道のりをゆっくりと拝観しながら歩いて頂ければ、きっと心が満たされてゆくことでしょう。 天上界(宝冠の高さ)から一望できる東京湾は絶景で、天候に恵まれた日には富士山や夕焼けも美しく見えます。
東京湾観音 / 東京湾観音についてより引用。

f:id:two_headed_duck:20200322213905p:plain
 こんなものもありました。僕の曽祖父はシベリア抑留で帰ってこなかったらしいので。。。

胎内

 20階建て構造で、各階には七福神を始めとしていろいろな像が置かれています。螺旋階段を登って上に上がります。結構大変で20分ぐらいかかります。 f:id:two_headed_duck:20200322214443p:plain
f:id:two_headed_duck:20200322214527p:plain
f:id:two_headed_duck:20200322214513p:plain
f:id:two_headed_duck:20200322214604p:plain
鼻の穴から観音様の大きさがわかります。上の方には展望台?的なものがあります。僕が行った日は超強風の日で、高いのでそれがさらに強風になっており大変でした。。。 f:id:two_headed_duck:20200322214730p:plain
f:id:two_headed_duck:20200322214804p:plain やっとこさ天上界につきましたと。 f:id:two_headed_duck:20200322214844p:plain
登ってきたご褒美にお守りがもらえます。あと、願い事を書くノートなんかもありました。南無大師遍照金剛。

まとめ

 お土産屋とプチ資料館もあります。甘酒を買って飲んでました。東京湾観音は知名度が低めだと思いますが、珍しく楽しい観音様なのでみんな行きましょう。徒歩でのアクセスは最悪ですが。

おまけ

f:id:two_headed_duck:20200322214955p:plain
この書き方かっこいいですよね
f:id:two_headed_duck:20200322215041p:plain
採光窓があるので、後ろ姿はやや面白いです