かもブログ

かもかも(@kam0_2)の雑記。掃き溜め。備忘録。

Googleに依存したオタクの末路はなんですか?

 朝起きたら衝撃的なメールが来てた.この記事はGoogleフォトに関するお気持ち表明である.
 巷で話題のように,今日Googleフォトは,そのサービスの根幹をひっくり返すような告知をした. 有料化だとかサブスク化だとか言われているが,簡潔に要約すると以下の一文になる.

  • 来年の6月以降,Googleフォトにバックアップした画像や動画は,あなたのGoogleドライブ容量を消費する.

Googleフォトの 思い出

 

f:id:two_headed_duck:20201112184101p:plain
今日公式ページで撮ったスクショ
 Googleフォトは2015年に始まったサービスで,筆者は多分リリース時から愛用していた.筆者は全般的にGoogleのサービスが大好きなGoogle依存であるのだが, その中でもかなり重宝していた.
 サービスのウリは「思い出を無制限に」,無制限に画像をアップロードでき,スマホで取った写真を自動バックアップし,バックアップ済の写真をスマホから消せば. ストレージ容量が少ないスマホでも安心!そんなサービスであった.リリース以来Androidの標準的なライブラリアプリになっていたことも書かなければならないだろう.
 「無制限にアップロードできる」にはもちろん制限があり,画像は動画はGoogleによって非可逆圧縮される.皮肉にもこれは「高画質モード」といわれており,元の画質でアップロードする「元のサイズモード」ではGoogleドライブ容量を消費した.
 昔はGoogleフォトにバックアップした写真はGoogleドライブからも見れたのだが,何年か前に両者は分離され*1,再びくっつくことはないかのように思えた.

来年の6月に何が起きるか

  • 来年の6月以降,Googleフォトにバックアップした画像や動画は,あなたのGoogleドライブ容量を消費する.

 では,この文の意味することを考えよう.Googleドライブは無料容量が15GBである.よって来年からは15GB以上は課金が必要になる.これが多くの人の解釈かもしれない.しかし,この15GBには他のGoogleサービスも同居する.その最もたる例はGmailである.
 GmailIMAPのメールサービスである.IMAPとはサーバにすべてのメールデータを保存し,各メールソフトが必要に応じてデータをサーバとやり取りする形式である.(対して,メールソフト側にメールデータを保存し,サーバにはメーラーに保存済みのメールを残さない方式をPOPという.)とどのつまり,Googleドライブの容量がなくなると,Gmailの送受信はできなくなる.
 携帯電話網や固定電話がインフラならメールもインフラであるはずで,インフラを人質に取られているようなものだ.AndroidにおいてGoogleフォトは標準ライブラリアプリの位置づけであり,多くの人が自動バックアップを有効化していると思われる.あくる日突然「写真かメールを消さないとメールは使えないぞ」と言われるのである.
 メールがインフラならインフラに金払えよ,まあそれはわからなくもない.でも,「無料で」「無制限に」を売りにしていたのはGoogleである.Googleフォトの存在はストレージ容量の少ないスマホの人気を高めたし,それはAndroidを選ぶ人を増やしたはずだ.Android Oneとかありましたよね?

f:id:two_headed_duck:20201112190247p:plain
Android Oneの公式サイトより.Android Oneは廉価な端末シリーズでストレージ容量は32GBしかない.しかしフォトがあればその心配はない.それは素晴らしい!!!!
 それが突然すべてが嘘になった.無制限でもなければ無料でもない神聖Google帝国は,今日突然Googleドライブを買えと言ってきたのだ.それに怒るのだ.悲しくなるのだ.裏切られたから.自分で宣伝して広めたくせに「現在 Google フォトには、お客様の数々の思い出も含めて 4 兆以上の写真や動画が保存されており、毎週 280 億もの写真と動画が新たにアップロードされています。」じゃねーんだよ.*2
 しかも買いきりで容量が増やせるわけではない.サブスクリプション,月額250円で100GB.単純計算で年間3000円であるが,無制限でないのだからもっとかかる可能性もある.

GAFAMに依存したバカ

 予兆はあった.GSuiteではGoogleドライブの容量が無限のプランがあったが,去年の10月にサ終した.*3
 同じことがGoogleフォトリリース直後の15年にMicrosoftのOneDriveでもあった.*4
 OneDriveといえば,無料容量が15GBから5GBに減ることになり,さらに既存の無料ユーザの容量も5GBに減らすと発表して大炎上したのが懐かしい.結局,既存ユーザは申請すれば元の容量を維持できたが.申請すれば,の時点でおかしな話だ.これも15年の話であった. *5
 思えば,当時OneDriveに写真をバックアップしていたが,上記の事件に絶望してGoogleフォトに感激し,乗り換えたのだ.以降クラウドストレージもGoogleを使うようになった.多くのユーザが同じことをしたと思う.このExodusはとんだ皮肉を迎える.Microsoftに依存した結果裏切られ,今度はGoogleに騙された.
 おれはGAFAMに依存したバカなのだ.無料のランチはない.甘い文句に騙されて怒ってクソエントリを書くのだ.歴史から何も学ばなかったのだ.
 Googleは平気でサ終をする.Google+Picasaを生贄にしてGoogleフォトは生まれた.Hangoutも消える.サ終とは違うが,ドライブにエロ画像を保存してたら勝手に消された人もいた.*6凍結されたら?他のサイトのパスワードも2FAも何もかも帝国に預けてしまっているのだ.*7詰み.こうならないように気をつけてほしい.
  自分に「邪悪になるな」と言い聞かせるIT・ギガントを信用しすぎた.気づけたはずなのに.(もう「邪悪になるな」というのはやめたらしいが.)
 巷ではAmazonPrimeに移行すれば無制限という言説が蔓延している.OneDriveにキレてGoogleに行ったおれと同じ目にあうと思うよ.AmazonはGAFAMの中でも一番黒いとされる.各国で配送員を酷使してるとか,電子書籍を勝手に値引きされるとかね.アップロードした写真は機械学習カンディルの餌になったあと適当に破棄されるのだと思う.Googleくんもフォトでいっぱい機械学習できてよかったね.
 Appleも最近この手の商売を始めてきた.iOS 12.4からはクイックスタートという機能でPCでバックアップを取る作業無しでデータを移行できる.そのデータを保存できる容量のiCloud空き容量があればという条件付きで.Apple製品には詳しくないのでそんなに言及しないが,Gmailと同じようにicloud.comのメールはicloud容量を消費するし同じ臭いがする.移行のためにデータ契約を迫られる未来というのは現実になりつつある.しかも,移行だから一時的にデータを置くだけでいいのに契約しないといけないのだから.
 要するに巨大IT企業に依存しすぎている.明日からはこのハイテク人工呼吸機は年1億払わんと使えなくなるらしい.そういう世界になっていく.

Google依存,やめたいな.

この記事へのご意見はコメント欄にどうぞ.間違った内容等あると思います.

*1:19年の7月に分離された.参考文献:https://jetstream.bz/archives/85484

*2:Googleの社員はみな頭がいいからこうなる事はわかっていたはずである.同情の余地はない.

*3:参考文献:

gigazine.net

*4:参考文献:

gigazine.net

*5:参考文献:

gigazine.net

*6:参考文献:https://anond.hatelabo.jp/20191113003537

*7:Googleパスワードマネージャと認証システムのこと.筆者はGoogle大好きなので.

メモ:久留里線の乗り方

久留里線の乗り方わからなくないですか,私はわかりません.
記事の内容には万全を期しておりませんが,間違いがありましたらお知らせください.

切符の買い方/乗るまでに必要なこと

まず,久留里線ではICカードは使えない.

木更津駅

久留里線の駅で唯一自動改札がある.普通に券売機で切符を買って乗るだけ.

他駅から来て木更津で乗り換える場合

切符で来た場合

手続き不用.乗るだけ.

ICカードで来た場合

駅員さんに事前精算をお願いして,切符ライクな紙切れをもらう必要がある.

有人駅(横田・久留里*1

券売機や駅員さんから切符を買って乗る.

無人

乗車駅証明書(オレンジの機械)から乗車駅証明書を発券し懐に収めます. 乗車駅証明書はその駅から乗ったことを証明する.バスの整理券みたいなもの.基本的に着駅精算になる.

乗り方

ワンマンなのか否か

久留里線ワンマン運転とそうでないものに分かれる.

ワンマン

ホームにある「ワンマン乗車口」以外のドアは開かない!「久留里上総亀山方面」「木更津方面」それぞれあるのでそこで待ち,来たらドア開きボタンを押して乗る.

ワンマンでない場合

すべてのドアが開く. ドア開きボタンを押して乗る.乗ったら閉める.(今はコロナで自動開閉されてるような

木更津駅(例外)

すべてのドアが開くので,ドア開きボタンを押して乗る.

降り方/料金精算

木更津駅

特になし.すべてのドアが開く.乗車駅証明書の場合は有人改札を通る.普通のきっぷなら自動改札を通れる.乗車駅証明書はICでも頼めば精算できます.

木更津から更に乗り換える場合

着駅で木更津と同じように対処すれば良い.遠くの駅で乗車駅証明書で改札しようとすると駅員さんが必死に路線図を眺めだす. この場合,ICで精算するとIC運賃が適用されて高くなったり安くなったりするようだ.

有人駅

すべてのドアが開く.ただし,3両以上の場合は下郡以東は前方2両しか開かない! 駅員さんが有人改札してくれるので,切符を渡すか,乗車駅証明書を見せて運賃精算する. ICカードで乗ってしまった場合どうなるかは知らない.(高い運賃取られる?

無人

めんどい.

ワンマン

先頭車両の運転席のそばのドアしか開かない.運転席の前にバスのような機械がおいてある. 切符の場合は改札をする運転手に切符を見せて,運賃箱にポイしてから降りる.(定期券・周遊券は見せるだけ) 乗車駅証明書の場合は,バスのような運賃表を見て,運賃と乗車駅証明書を運賃箱に入れる. バスのような両替機もついていて千円札を崩せる.両替機は走行中も利用可能. 精算が済んだらドア開きボタンを押して降りる.

ワンマンでない

すべてのドアが開く.ただし,3両以上の場合は下郡以東は前方2両しか開かない!

切符

何もせずに降りる.駅のホームあたりに乗車券回収箱があるのでそこに使い終わった切符を入れる.

乗車駅証明書

これが一番難しい.巡回している車掌に申し出て,車内で精算を行い切符をもらう必要がある.精算はおつりもでるし,ICカードはでもできるらしい? 必ずしも巡回してくれるとは限らないので列車後方の車掌のいる場所に赴く必要もある.駅や時間によっては車掌がホームに出て改札することもあるようだ. 降りたらもらった切符を回収箱に入れる.

*1:一部シーズンの一部時間帯は馬来田駅も有人らしい.また夜中はこれらの駅でも無人かもしれない.

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
という関係がコサイン類似度とユークリッド距離で成り立つ。

実装

2021/12/11 追記
ベクトルが正規化済みなら内積を求めるだけだろ どう考えても. この記事を書いたときの俺はバカなのか?
以下読む価値なし.

ユークリッド距離を求めてから、コサイン類似度を求めたほうが早い気がするのでそうする。
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()などを使わなければ、限りなくいい方法だと思います。よんでくれてありがとね。

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

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

 昔からしりとりが好きだった。しりとりは一見すると、勝ち負けもないようなただの言葉の言い合いに過ぎない。しかし、しりとりには明らかに「強い人」「弱い人」が存在する。どうすれば強くなれるのだろうか。
f:id:two_headed_duck:20200401191434p:plain  Googleで適当に検索してみると、色々出てくる。「る攻め」がいいとか「ぷ攻め」がいいとか。僕は昔「む攻め」を極めたし、友達には「や攻め」使いがいた。しかし、これらの言説に総じて言えるのは、「論拠の不足」であろう。感覚的に「〇〇が強い」というのは、信用に値するのであろうか。そんなことを日々思っていたときに、ある本に出会った。とつげき東北氏の「科学する麻雀」である。麻雀は「ツキと流れ」が支配する感覚のゲームと長い間思われていた。しかし、統計学やコンピュータシミュレーションを用いることで「麻雀必勝法」を科学的に導出できることをこの本は示している。しりとりの話から逸れてしまったが、僕はしりとりでも同じことが研究できるはずだと思ったのである。
 そこで、僕の稚拙なプログラミング能力を生かしてしりとりを科学的に極めてやろうと思ったのだ。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

まとめ

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

本当にそれだけ?

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

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

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

今後の展望

 今後があるかもわかっていない。考えているのは、人名や外来語を禁止したときにどうなるのかといったルールを制限した場合である。そのような際には、「る攻め」神話が大きく崩れ、ほんとうの意味での「しりとり」が見えてくるかもしれない。

謝辞

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

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

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

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

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

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

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

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