Three.jsでオブジェクトが表示されない時のチェックリスト

Three.jsで何か作ってるとき、画面にオブジェクトが表示されなくてしばらく時間を費やしてしまうことがちょくちょくある。わかってみたら単純でアホみたいなミスばかりなんだけど、実際画面に何も表示されないって状況に直面すると「どこからチェックしよ…」ってなってしまうので、ありえる原因を書き出して今後の参考にしようと思う。

Canvas

renderer.domElementであるcanvasはちゃんと正しい場所に正しい大きさで存在してる?

ウェブページ自体の背景をsceneの背景色と同じにしてたら気づきにくいので、ちゃんとinspectorで見る。

Canvas element - Renderer - Scene - Cameraの繋がり

renderer.domElementは意図したcanvas elementになってる?

Renderer.render()に正しいsceneとcameraが渡されてる?

Material

オブジェクトのマテリアルはちゃんとsceneの背景色と識別できる色?

opacityが0になってたりせん?visibilityはtrue?

material.sideは意図した方向になってる?

Position

デフォルトではカメラは原点で-Zの方向を向いてるので、オブジェクトはそこから見えるところに置いといた方がいい。

カメラとオブジェクトが重なってたり、オブジェクトの内側にカメラがいたりしたら(materialを背面に設定していない限り)見えないから気をつけて。

Add

ちゃんとscene.add(object)した?sceneにちゃんと入ってるか、scene.childrenで確認して。

Render

ちゃんとrenderしてる?requestAnimationFrame()で毎フレームrenderしてる?

完全に静止してるやつやったら一回だけのrenderでもいいけど、それならオブジェクトのセッティングとか全部render前に済ませよう。texture画像の読み込みみたいな非同期処理はrenderが実行される前に終わらんこともあるし。

Fog

Fogが濃すぎてオブジェクト見えないとかない?とりあえずfog無効にしよ。

Camera depth

camera.nearとcamera.farは適切?範囲が狭すぎたらオブジェクトがクリップされてしまう。

Controls

FlyControlsとかがちゃんと動いてる前提でカーソルぐるぐる動かしてても、実はカメラ全然動いてないとかありえるから、camera.positionとかcamera.rotationが変動してるか確認しよ。

おまけ: tweenしても動かんとき

  • tween.startしてる?

  • tween.updateしてる?

  • update毎にちゃんと値を代入してる?

  • needsUpdateをtrueにしなければいけないとかない?ちゃんとドキュメント見て。

今思いつくんはこれくらいかな。何かめちゃくちゃうざいチェックリストになってしまった。

タイムゾーンを変換するLine botを作る

留学から帰ってから、現地にいる友達とビデオチャットする時間を設定するのが厄介になってしまった。「○○時は空いてる?」って一言聞くためにも、世界の時間一覧アプリを開けて当該時刻までスクロールして、対応する相手の時間を探して打ち込まなければならない…っていう一連の手間がめんどい。しかも私は「今ここは○○時やから、あっちは○○時かな」って計算できるほど頭がよくない。Daylight saving timeとか考えだしたら死ぬ。

だからそういうのを自動でしてくれるbotが欲しいと思った。

作ったbotはここにある↓

https://github.com/shio-yaamaa/timezone-line-bot

目標

私はAsia/Tokyo 18:00からできるよー

とか打ったらbot

私は
Asia/Tokyo 18:00
America/Los_Angeles 2:00
からできるよー

みたいな感じに変換してしゃべってほしい。シンプルな「JST」ではなく「Asia/Tokyo」とするのは、略称だと一意に決まらないことがあるみたいだから。それに、友達のいるところだと季節によってPSTになったりPDTになったりして頭おかしくなりそうだし。

けど毎回「Asia/Tokyo」とか書くなんてめんどくさすぎるので、チャットメンバーが「Asia/Tokyoのことは"jst"と呼ぶ」とでも決めておいて、

私はjst18:00からできるよーbotに反応してもらいたい。

できるようにすること

現在時刻

「今そっち何時?」「○○時だよ」って会話も頻発するので、

timezone nowって言ったら登録されてるタイムゾーン全ての現在時刻を表示するようにする。

全てのタイムゾーン

「Asia/Tokyo」みたいなやつ全部覚えてないので、timezone allって言ったら全てのタイムゾーンのリストを返すようにする。ただし、全部のタイムゾーンは数が多すぎるので、一覧ページへのリンクのみ返す。

タイムゾーンの登録

timezone add Asia/Tokyo as jstみたいな感じにする。いちいち「timezone」って打つのめんどくさいけど、「add」だけで反応するようにしたら要らんときまで反応してきそう。

asの後は、そのタイムゾーンエイリアスで、適当に指定できる。

消すときは、timezone delete jstで消える。

Typoがあればsuggestする機能つけたかったけど、とりあえず今は一部を入力したら全部を返す("london"って入れたら「もしかしてEurope/London?」って聞いてくる)くらいしかできてない。

時刻のparse

Javascriptの時刻parserはいろいろ見つかったけど、Chronoが一番すごいと思ったので使わせてもらうことにした。Datejsも「tomorrow」とかをparseできるのはすごいんだけど、「tomorrow 5:00」とかになるとChronoしか処理できなくなる。

他にChronoがすごいところ

  • 文字列が時刻以外の情報を含んでいても時刻だけを読み取り、どこから読み取ったかのインデックスを教えてくれる

  • 年や月などのコンポーネントがそれぞれspecifyされてるのかimplyされてるだけなのか教えてくれる

  • 期間のデータだったら始まりと終わりどちらも取ってくれる

  • デフォルトで多言語対応(さすがに英語の方が広く対応してるけど)

めっちゃありがたかった。

サーバーとか

Amazon Lambdaを使うことにした。データベースはDynamoDB。どっちもよくわかってないし、AWS自体が謎の塊だから、このチョイスで大丈夫なのか知らんけど。

一番躓いたところは、Lambdaでzipをアップロードして実行したときの「Cannot find module 'index'」っていうエラー。私の場合たぶん原因は二つあって、一つはzip圧縮するときにディレクトリの中身だけではなくディレクトリごと圧縮してしまったこと、もう一つはnode_modulesの中に必要なモジュールがちゃんと入ってなかった(npm iし忘れた)ことだった。

あと、LambdaからDynamoDBにアクセスできない!!と思ったら原因は全然違うところにあった。DBの読み込みとかが非同期なのに、それを無視してcontext.succeed()やっちゃってたから途中でDBの処理を中断させられてたみたい。

LambdaとDynamoDBしか使わんはずやったのに、いざやり始めたら他に必要なサービスがどんどん出てきて圧倒された。AWSってすごいな…。もうちょっと全体像を把握したい。


いつまでこのbotを使う用事があるかはわからんけど、ずっとLine botに興味あったから作れてよかった。

React+Reduxで脱出ゲームを作る

服部平次の部屋からの脱出ゲーム作りたい。今までの原作と映画でのトリックをうまく組み合わせて何かできひんかなって考えてる。それと、ちょうどReactをやってみたいと思ってたので、Reactで脱出ゲームがちゃんと作れるのか確かめるために小さいのを作ってみた。

GitHub Pagesで公開してる。

https://shio-yaamaa.github.io/react-escape-room/

ソース: https://github.com/shio-yaamaa/react-escape-room

作った感想とか、作り替えるときにどこを変更したらいいかとかメモしておく。

ゲームの中身

中学生の時くらいにNeutralのMyaさんに激しく憧れてて(今ももちろん憧れてるけど)、何でもいいから一個脱出ゲーム作りたい!ってなってシナリオ考えたんだった気がする。見返してみると、物理法則めっちゃ無視してるしアイテムの鍵率高すぎだし酷いけど、なんか頑張って考えたのは覚えている。当時は吉里吉里のKAGで作ろうとしたけど、書き方が悪かったのか超遅くなったので、諦めてしまったんだった。部屋とかアイテムはそのときにモデリング&レンダリングしたので、その画像は今回そのまま使えた。

Componentの構成

<Provider>
    <App>
        <ScreenSwitch container> manages which screen to display and calls AssetsLoader
            <StartScreen>
            <EndScreen>
            <LoadScreen>
            <div> shows fade effect when switching between screens

            <Game container> manages functions related to the whole game, like save, hint, and cursor
                <div> aligns MainScreen and Sidebar horizontally
                    <MainScreen container> handles click events on MainViewMap, ArrowArea, and ItemDetailWindow
                        <MainView> displays background images
                        <MainViewOverlay> additional images on the MainView like dial numbers
                        <MainViewMap> clickable map
                        <ArrowArea> (x4) fire events when they (not their children!) are clicked
                        <ItemDetailWindow> zoomed view of items
                            <ItemDetailView> displays item images
                            <ItemDetailViewMap> clickable map
                        <Sidebar>
                            <ItemFrameContainer container>
                                <ItemFrame> (x10)
                            <div> aligns SaveButton and HintButton horizontally
                                <SaveButton>
                                <HintButton>
                <Hint>
                <div> // shows fade effect when save button is clicked

どれをcontainerにするかの判断についてはあんまり自信がない。

ゲームごとに書き換えるところ

なるべく後で使えるように作ったつもりなので、他の脱出ゲームを作るときは下記を変更したらたぶん動く。

Assets

assets/images/

  • items: アイテム欄でのアイテムの画像

  • itemDetails: アイテム詳細ウィンドウでのアイテムの画像

  • itemDetailMaps: アイテム詳細ウィンドウのクリッカブルマップ

  • mainViews: 部屋の画像

  • mainViewOverlays: 部屋の画像の上に載せるやつ

  • mainViewMaps: 部屋の画像のクリッカブルマップ

  • endScreenBackground: ゲームクリア画面

assets/sounds/: 全般的に

Program

redux/modules/

  • items: ゲーム内のアイテム

  • itemStatus: アイテム詳細ウィンドウ内でのアイテムの状態

  • status: 部屋の状態

scenario/: stateから背景画像を選択したり、クリック場所から動作を決めたりするスクリプトが入ってるので、全部変える。

Reactを使ってよかったこと

JSXが好き。(けどたまにスタイルをJSXかCSSファイルどっちに書こうかめっちゃ迷う。pseudo classとかはCSSでしかできないみたいだから)

「これがあれやったらそれ表示して」って書いとくだけで初期化も再描画も勝手にやってくれるん嬉しい。

初めCanvasとどうやって折り合いをつけるかわからんくて戸惑ったけど、Componentのライフサイクルに合わせてちゃんと書いたらそこまで大変でもなかった。

アイテム欄みたいな同じコンポーネントいっぱい並べる所がすごい楽。

Reactを使って苦労したところ

  • Assetsを事前に全部requireしておく

    たぶん私がWebpackとかの技術に慣れてなさすぎるんだと思うけど、なんか難しかった。結局このページのmih-kopylovさんの最後のコメントを参考にしたらできた。

  • フェード

    普通は、actionをdispatchしたらstateが更新されて、即座に画面が更新される。だから、「画面を黒くフェードしていって、一番フェードが濃い状態の時に画面を切り替えて、それからフェードを徐々に消していく」みたいなやつはちょっとめんどくさかった。何かいい方法あるんかな。

Reduxを使ってよかったこと

データの矛盾が起きない感じがめっちゃいいし、一方的に決まった方法でしか状態を変更できないのもありがたい。

save/load機能を作るのがめちゃくちゃ簡単だった。saveはstateをlocalStorageにそのままばーんって入れて、loadではそれをまるごと取ってくればいいだけ。


服部の脱出ゲームにもほぼそのまま使えると思うんだけど、まだ部屋のモデリングとか全く進んでないからいつになるかわからなすぎる。

今のところはこんなん。

f:id:YaaMaa:20180609222004p:plain

実際作ってみると、紫だと思ってたベッドが実は彩度の低い赤だったとわかったり、ポスターのバイクの種類を特定できるようになってきたりと、服部の部屋についての学びが多い。

音響モデルから音素間の距離を求める

しばらく前に、「"工藤"を誤魔化すための単語」みたいなツイートをしたら、予想外に伸びた。

服部の「工藤の人」としての知名度ってすごいんやな。うれしい。

いろいろ指摘をもらったんだけど、その中でも「Juliusで提供されている音響モデル(たくさんの人の声を統計的に学習したもの)で音素間の距離を定義できるとPanPhonよりもよくなるかもしれない」っていうアドバイスをめっちゃ試したいと思ったので、やってみることにした。知らないことばかりだったので、やったことをメモする。

Juliusの音響モデル

dictation kitに入ってるhmmdefsって拡張子のやつが、モデルの定義ファイル。monophone用とtriphone用がある。

  • monophone用: "m", "n"みたいな音素一つ一つに対してモデルを定義したやつ

  • triphone用: "a-m+a", "a-n+a"みたいに3つ並んでる音素に対してモデルを定義したやつ

たぶんtriphoneの方がちゃんと音素環境を考慮してるからいいと思うし、アドバイスしてくれた方もtriphone勧めてくださってたけど、とりあえずわかりやすそうなmonophoneを使った。

HMM

「音響モデル」って聞いて、何となく音声ファイルみたいなのを想像してたんだけど、めちゃくちゃ見当外れだった。実際の音響モデルはHidden Markov Model (HMM)として定義されていた。HMMっていうのは、stateがある確率で変わっていって、変わるごとにある確率に基づいた何かを出力するってやつらしい。

音響モデルは後戻りしないので、HMMの中でもleft-to-rightのものとして分類される。確かに遷移確率見たら、リピートするか次に進むかの二択しかなかった。

hmmdefsの構造は、HTK Bookの7章読んだら何となくわかった。一つの音素に対して、

~h "音素"
<BEGINHMM>
    <NUMSTATES> stateの数
        <STATE> 2
            <NUMMIXES> mixtureの数
                <MIXTURE> 1 mixture1の重み
                    <MEAN> meanの数
                        meanが並んでる
                    <VARIANCE> varianceの数
                        varianceが並んでる
                    <GCONST>
                <MIXTURE> 2 mixture2の重み
                    (略)
        <STATE> 3
            (略)
        <STATE> 4
            (略)
    <TRANSP> stateの数
        stateの数×stateの数の行列
<ENDHMM>

って感じで定義されている。stateは5個、mixtureは16個、meanやvarianceは25個だった。

HMM同士の距離

どうやってHMM間の距離測るんやろって思っていろいろ読んだけど、mixtureを含んでいるためそんな単純に計算できない(たぶん)。Music Similarity Measuresによると、片方のHMMから出したサンプルがもう片方のHMMから出力される確率がどれくらいかを測って、それを二つのHMMの近さと見なしたらいいっぽい。どれくらいばらつきがあるかわからんけど、一つの組み合わせにつき1000回やって、その平均を取ることにした。

そのためには、HMMからサンプルを出す機能と、尤度を計算する機能が必要。hmmlearnがそういうことをやってくれるので、そのhmm.GMMHMMっていうモデルを使った。

GMM→GaussianMixture

GMMHMMのプロパティのgmms_には、scikit-learnのGMMっていうオブジェクトが入っている。それがもう古いらしくて、GMMの代わりに新しいGaussianMixtureを使えってwarningが出る。

github.com

hmmlearnのissueで話し合ってくれてはいるけど、途中で終わってる。なので自分で勝手にhmm.py内のGMMを全部GaussianMixtureに書き換えてしまった。それに伴って、sample()の引数とかscore()の戻り値の形式など変更があったところに対応できるようにちょっと直した。

GaussianMixtureのパラメータの設定

個々のMixtureの分のパラメータ(weights_とかmeans_)をGMMHMMから設定する方法がわからんかったので、gmms_に入ってるGaussianMixtureオブジェクトに対して直接_set_parameters()してしまった。そのときにprecisions_choleskyというものが欲しいみたいなんだけど、それが何なのか理解すらできなかったので、GaussianMixture._compute_precision_cholesky()を使って出してもらった。

調節とか

  • left-to-rightのモデルだから、最後のstateから行くとこがないので、遷移確率の行列の最後の行は全て0になる。でも、遷移確率の行ごとの合計が全て1にならないとscore()でエラーが出る。どうしたらいいかわからんけど、とりあえずリピートするところを1にした。

  • 遷移確率や分散に0があるとエラーになるので、めちゃくちゃ小さい値に置き換えた。

  • 普通のHMMは延々と続くので、sample()するときには何回繰り返すかを指定するんだけど、音響モデルはleft-to-rightだから最後のstateに辿り着いたら止めるようにしてほしい。なのでちょっと処理を書き換えた。

NotFittedError

パラメータをセットして、やっとsample()できる!と思ったら、NotFittedErrorとかいうものが出た。fitした後じゃないとsample出してくれへんらしい。

stackoverflow.com

↑初めにちっちゃいデータセットでっち上げてfitさせてからパラメータをセットし、その後sample()したらいいって回答がある(ちっちゃいって言っても、サンプルの数 >= Mixtureの数じゃないと無理だけど)。だから、適当にfitする→ちゃんとしたパラメータをセットする→sample()するっていう流れになった。

GMMHMMとGaussianMixtureでの引数名が指すもの

なんかcomponentとか微妙に意味が違うからややこしい。

GMMHMM GaussianMixture
the number of states n_components
the number of mixtures n_mix n_components
the number of features n_features

それぞれの音素がHMMとして定義されてて、そのHMMの中にstateがいくつかあって、それぞれのstateの出力確率がいくつかのmixtureから成ってて、それぞれのmixtureにfeatureが決められている。

log likelihoodはsampleの長さに影響される?

log likelihoodの行列(左がサンプルを出したもとのHMM、上がそのサンプルを出す尤度を測ったHMM)を可視化すると、以下のようになった。

f:id:YaaMaa:20171202193318p:plain

かろうじて対角線は見えてるけど、それより行ごとの違いが大きすぎる。左のHMMから取ったサンプルの長さの影響が大きいのかなと思って、x軸をサンプルの長さ、y軸をscore (= log likelihood)としたグラフを描いてみた。

f:id:YaaMaa:20171202032558p:plain

すごいあからさまに影響されてる。濃い青色のところは、行列では対角線上に当たる部分、つまり同じ音素同士を比べたときのスコア。これがだいたい横一直線になってもらわないといけないと思う。いろいろめちゃくちゃな方法で値を操作した結果、次のような行列になった。

f:id:YaaMaa:20171202200958p:plain

さっきに比べたらわりと納得できる感じになったと思う(適当)。

距離の可視化

ツイートに書いたのと同じ方法で、音素間の距離を可視化した。

f:id:YaaMaa:20171202201138p:plain

雑に分類してみると、

f:id:YaaMaa:20171202201245p:plain

何となく同じような属性を持つ音素同士で固まってるようなので満足!

あの意味わからん数字の羅列だったHMM定義ファイルの中身が、処理してみたらちゃんと人間の知覚に沿った感じになってるの、何かすごいな。

もうちょっとやりたいことがあった気がするけど、期末テストやってるうちに自分が何してたのか忘れてしまったので、終わる。

Pixivのスクレイピング

Pixivに投稿された小説の中で服部の瞳の色がどんな感じで描写されてるのか気になったので、調べることにした。そのために、「服部平次」タグのついた小説の本文から「瞳」を検索し、その前後10文字を取ってくるという作業をPythonで書いた。そのときにいろいろ試行錯誤したので、メモしとく。

まず、PixivのHTMLの構成を調べた(2017/7/22時点ではこんなんだったけど、いつ変わるかわからない)。

検索画面

タグ=服部平次, 順番=古い順で検索したときのURLは以下の通り。

https://www.pixiv.net/novel/tags.php?tag=%E6%9C%8D%E9%83%A8%E5%B9%B3%E6%AC%A1&order=date&p=ページ数

ページに作品があるときとないとき

あるとき(*‘▽’)

<dl class="column-related inline-list"></dl>
<ul class="novel-items autopagerize_page_element novel-list">
    <li>小説</li>
    <li>小説</li>
</ul>
<dl class="column-related inline-list"></dl>

novel-itemsの中にそのページの作品がリストで並んでて、上下のcolumn-relatedっていうのは関連タグ。

ないとき(._.)

<div class="_no-item">見つかりませんでした</div>

一つ一つの作品

検索ページでの作品一つ一つのHTMLの構成。さっき<li>小説</li>って略したところ。

<li>
  <section class="_novel-item list-b">
    <div class="novel-contents">
      <div class="imgbox">
        <a href="/novel/show.php?id=000000">
          <img class="cover ui-scroll-view" data-filter="thumbnail-filter lazy-image" data-tags="" data-type="novel" data-id="000000" data-user-id="00000" data-src-filtered="https://source.pixiv.net/www/images/filtered-100.png?1" src="https://source.pixiv.net/www/images/common/transparent.gif">
        </a>
        <div class="chars">0文字</div>
      </div>
      <div class="novel-right-contents">
        <div class="_ui-tooltip bookmark-box" data-tooltip="0件のブックマーク">
          <a href="/novel/bookmark_detail.php?id=000000" class="bookmark-count _ui-tooltip bookmark-box"><i class="_icon sprites-bookmark-badge"></i>0</a>
        </div>
        <div class="title-box">
          <h1 class="title"><a href="/novel/show.php?id=000000">Title</a></h1>
        </div>
        <ul class="data">
          <li class="author">by <a href="/novel/member.php?id=00000" class="user ui-profile-popup" data-user_id="00000" data-profile_img="" data-user_name="Name">Name</a></li>
        </ul>
        <ul class="tags">
          <li><a href="" class="tag-icon">c</a><a href="">Tag1</a></li>
          <li><a href="" class="tag-icon">c</a><a href="">Tag2</a></li>
        </ul>
        <p class="novel-caption">Caption</p>
        <div class="_one-click-bookmark js-click-trackable " data-click-category="abtest_www_one_click_bookmark" data-click-action="novel" data-click-label="000000" data-type="novel" data-id="000000"></div>
      </div>
    </div>
  </section>
</li>

だいたいこんな感じ。作品情報もユーザー情報もほとんどわかるが、投稿日時等は詳細画面に移らないとわからない。

小説の詳細画面

小説の情報

<ul class="meta">
  <li>2017年8月10日 08:10</li>
  <li>小説 2P</li>
</ul>

「3/4」って感じで現在位置とページの総数を教えてくれる表示もあるけど、そこはhtmlでは「読み込み中…」になってて後から書き換えられるタイプなので、スクレイプするのめんどくさそう。

本文

<textarea name="novel_text" id="novel_text">本文本文本文</textarea>

こんな感じで入ってる。このtextareaに全ページ分の文章が入ってて、ページ区切りは[newpage]で仕切られてる。

re.sub(r'\s|\n| ', '', 本文)

↑のように書いたらスペースとか改行取り除けた。

ログインする

初め、ログイン処理書かずにそのままスクレイプしたら、R-18小説とかが取得できない上に検索結果の10ページまでしか見れなかった。それでは困るので、まずPixivに自分のアカウントでログインする。

ログイン処理にはmechanizeがいいって聞いたけど、Python3をサポートしてくれてないので、RoboBrowserを使った。

from robobrowser import RoboBrowser

browser = RoboBrowser(parser='lxml', history=True)
browser.open('https://accounts.pixiv.net/login')

form = browser.get_forms('form', class_='')[0]
form['pixiv_id'] = email_address
form['password'] = password
browser.submit_form(form)

page = 100
browser.open('https://www.pixiv.net/novel/tags.php?tag=%E6%9C%8D%E9%83%A8%E5%B9%B3%E6%AC%A1&order=date&p=' + str(page))
novel_items = browser.find(class_='novel-items')
print(novel_items)

ログインして、試しに服部平次タグで検索した100ページ目にある小説一覧を取ってきた。

Pixivのログイン画面のformタグにはidも何もついてないのでどうしようかと思ったけど、class指定を空にしてget_formsしたやつにインデックス0を指定したら取得できた!

小説情報を取る

novel_items = browser.find(class_='novel-items')
    
if novel_items == None:
    break
    
# ページ内の小説をループ
for novel in novel_items.find_all(class_='_novel-item'):
        
    novel_url = pixiv_url + novel.find('h1').find('a')['href']
    browser.open(novel_url)
    
    # 小説の情報
    title = novel.find('h1').find('a').text
    author = novel.find('li', class_='author').find('a').text
    date = browser.find('ul', class_='meta').find('li').text.split(' ')[0]
    text = re.sub(r'\s|\n| ', '', browser.find('textarea', id='novel_text').text)

これで、タイトル・作者名・投稿日・本文を取得できる。

「瞳」という表現の前後それぞれ10文字を抜き出す

import re
for match in re.finditer('瞳', text):
    print(text[max(0, match.start()-10):min(len(text), match.start()+10+1)])

Wikipediaの文章に使ってみたら下のような感じで出てくる。

膜状組織であり中央に瞳孔と呼ばれる開口部を
後方奥は眼底と言い、瞳孔を通じて検視鏡で観
される。虹彩筋のうち瞳孔括約筋は動眼神経支
約筋は動眼神経支配、瞳孔散大筋は交感神経支
交感神経支配であり、瞳孔径の変化に寄与する
経となり、毛様体筋と瞳孔括約筋に分布する。

こんな感じで出てきたやつを集計した結果、以下のようになった。

あほらしい集計だけど、Pythonでのスクレイピングは初めてだったので楽しかった! 小説書く方たちの豊富な表現も見てて面白かった(*‘▽’)

服部平次の「おっさん」「おっちゃん」呼び分け

この前やっとCourseraのMachine Learningのコースを修了した。習ったことを何か日常で気になることに生かせないかなと思って、服部平次の「おっさん」「おっちゃん」の呼び分けの傾向を求めるのに使ってみることにした。

服部は中年男性のことを「おっさん」もしくは「おっちゃん」と呼ぶが、どうやって呼び分けてるんやろなって前から不思議に思っていた。

前提

  • ひらがなとカタカナの区別はつけない

  • binary classificationで、「おっちゃん: y = 1, おっさん: y = 0」とする

  • 92巻までのセリフを用いる(そこまでしかテキストデータ化終わってないから)

  • 小五郎は別格なので除外する

feature

どういうfeatureがあるのか考えた結果、「巻数」「親密さ」「疑惑度」「方向」の4つを作ることにした。

1. 巻数(= 時代)

初期の服部は割と雑な感じで「おっさん」って呼びがちで、最近の服部は人懐っこくて「おっちゃん」をよく使ってる気がする。

2. 親密さ

服部と相手がどれくらい親しいか。

馴染みの相手には「おっちゃん」を使う傾向が高そう。

目安
0 初対面・面識なし
1 面識あり・少し話したことがある
2 かなり話したことがある・親しい

3. 疑惑度

服部が相手に対してどれくらいの疑惑を抱いているか。胡散臭い相手に対しては「おっさん」の方が適している気がする。

目安
0 潔白もしくは被害者側
1 少し怪しい・容疑者の一人である
2 かなり怪しい
3 犯人だと確定している

被害者だとしても、胡散臭くて怪しい奴であれば、1とする(K3で殺された新聞記者とか)。

4. 方向

服部が相手に対してどういう方向で話しているか。面と向かっているときの方が「おっちゃん」を使いがちな気がする。

目安
0 相手に聞こえないところで言及している
1 相手に聞こえるところで言及している
2 相手に向かって話している

データ収集

服部のセリフデータから「おっさん」「オッサン」「おっちゃん」「オッチャン」で検索し、小五郎に言及しているもの以外を以下のようにまとめた。

f:id:YaaMaa:20170612022458p:plain

(一番右のカラムは、どれがどのシーンか思い出しやすいようにするためのメモ)

データ数は80個となった。少ないかな…。

赤馬のおっちゃんや郷司のおっさんなど途中で疑惑度が変化した人もいるので、同じ人を複数回呼んでるのも全部記録した。シンフォニー号の事件で助けてくれた漁師の方たちとか親密さがわからない人については、適当に予想した。

相関関係

ここで相関関係求めて意味があるのかはわからんけど、試しに出してみた。scipy.stats.pearsonrに計算してもらうと、以下のようになった。

yと○○の関係 相関係数 P値
巻数 0.6596983083448209 2.8327330071418733e-11
親しさ 0.29273833233028956 0.008410796649393136
疑惑度 -0.06611364854623174 0.5601156608149662
方向 -0.02280254169471079 0.8408797463324191

巻数とyは思ったより相関関係あるし、親しさとyも小さいけど一応あると言える。疑惑度と方向については、yとの相関関係はほとんど見られないみたい。

SVCで学習する

まずはscikit-learnのSVCで学習してもらってみる。カーネルは、一番定番と呼ばれててCourseraでも重点を置いて教えてくれたRBF kernelを使う。ハイパーパラメータについては、グリッドサーチしてF1 scoreが良さそうだった組み合わせ(C=5, gamma=0.5)にした。グリッドサーチしたって言っても、めっちゃシャッフルの影響受けて毎回最適値がコロコロ変わるから、キリのいいところで適当に決めた。

import numpy as np
from sklearn import preprocessing
from sklearn import svm
from sklearn.metrics import classification_report

y, volume, intimacy, suspicion, direction = np.loadtxt('ossan.csv', unpack=True, delimiter=',')

# create and preprocess feature matrix
X = np.c_[volume, intimacy, suspicion, direction]
X = preprocessing.scale(X)

# SVC with rbf kernel
clf = svm.SVC(kernel='rbf', C=5, gamma=0.5)
clf.fit(X, y)

# evaluate
print(classification_report(y, clf.predict(X), target_names=['おっさん', 'おっちゃん']))

訓練用データに対してのclassification_reportを出したら以下のような結果になった。

precision recall f1-score support
おっさん 0.94 0.97 0.96 33
おっちゃん 0.98 0.96 0.97 47
avg/total 0.96 0.96 0.96 80

訓練用データだから高いのは当たり前なんだけど、それでも予想外に高かった。SVMすご…。

けど、目的は呼び方を正しく予測することではなくて、だいたいの傾向を見出すことである。RBF kernel使ったら、精度はいいけど、もともとのfeatureがどう影響してどうなってるのか私には全くわからん。linear kernelを使ったらそれぞれのfeatureにかかる係数を見れるみたいなので、試してみた。

Linear kernelでのSVC

# データの用意などはさっきと一緒

# SVC with rbf kernel
clf = svm.SVC(kernel='linear', C=1)
clf.fit(X, y)

# show coefficients
for coef in zip(['volume', 'intimacy', 'suspicion', 'direction'], clf.coef_[0]):
    print(coef[0] + ': ' + str(coef[1]))

今回は、F1 scoreは0.89だった。線形だからもっと下がるかと思ってた(*‘▽’)

係数

係数が正であれば、そのfeatureの値が大きいほど「おっちゃん」と呼びやすい。負であれば、そのfeatureの値が大きいほど「おっさん」と呼びやすい。

予想した符号 実際の係数 予想が合ってるか
巻数 2.34837022019
親密さ 0.227720579735
疑惑度 -0.158394754511
方向 0.36036075643

符号的には予想と一致してるけど、巻数以外はほぼ影響してないように見える。

可視化

そのままだと次元数が多くてグラフにできないので、「巻数 + 別のfeature1個」でRBF kernel使って学習した結果をグラフにした。

赤が「おっちゃん」、青が「おっさん」。

f:id:YaaMaa:20170614005146p:plain

f:id:YaaMaa:20170614010317p:plain

上はpredict、下はdecision functionから出てきた値のグラフ化。

親しさについてはまあまあ傾向が感じられるけど、疑惑度と方向については何も読み取れない…。

結論

昔の服部は「おっさん」を使いがちで、近頃の服部は「おっちゃん」を使いがち。親しさはちょっと呼び方に関係するが、疑惑度と方向はほとんど関係なさそう? もしかしたら私がfeatureとして作れなかっただけで、もっとちゃんとした決め手があるのかもしれない。

初対面の人を「おっさん」呼ばわりする失礼な服部も、馴れ馴れしく「おっちゃん」って呼ぶ服部も、どっちも好き。

服部平次絶体絶命の暗号を作成するツール

名探偵コナンの第323~324話「服部平次 絶体絶命」に登場する暗号を自動で作れるサイトを作ってた。期末テストの息抜き程度に作る予定だったのに、いつの間にか春休みも半ばになってた(゜_゜)

hattori.cipher.jp

アニメ内では、服部平次は監禁された状態で提示された暗号を解いた上で、犯人を表す暗号に脳内で作り替えてコナンに送るというすごいことをやってのける。どういう思考回路してるのか全く理解できないんだけど、暗号を作成するプログラムを作ったら何かわかるかもしれないと思ったので、これを作った。

※ 暗号の解き方も書いちゃっているので、思いっきりネタバレです!

暗号の解き方

まず、楠川さん作の暗号を使って解読方法を復習する。

暗号は、以下のように7×7=49個の英数字からできている。前提知識としては、この暗号が7桁の数字を表しているということと、暗号の作成者が博打好きだということがある。

Q52A252
K332Q44
4KQQKJ6
AQJ96J3
355533Q
6246426
2KK53KA

1. アルファベットを数字に直す

暗号内のアルファベットがA, J, Q, Kであることから、これはトランプを表していると考え、それぞれ数字に直す。A→1, J→11, Q→12, K→13。

 12521252
 133321244
413121213116
 1121196113
  35553312
  6246426
2131353131

このようにアルファベットを数字に直す作業を「展開」と呼ぶことにする。また、その逆の操作を「短縮」とする。

展開の結果、暗号は64個の数字となった。

2. 仲間外れの9を除き、その9で残りの個数を割ると、答えの桁数である7になる

1~6の数字は多くあるのに、一つだけ9があることに注目する。仲間外れの9を除くと数字は64-1=63個となり、これを9で割ると答えの桁数である7となる。

125212521
333212444
131212131
161121161
133555331
262464262
131353131

↑9を除いた数字を9個ずつで改行したところ。数字が7行できていることがわかる。

この一行ずつが、答えの一桁ずつを表しているのではないか、と考える。

3. サイコロに当てはめる

1~6の数字があることと、暗号の作成者が博打好きであることから、サイコロを連想する。サイコロは正方形なので、9つの数字の組を一つのサイコロに見立てて、3×3の正方形に並べる。

125
212
521

333
212
444

131
212
131

(略)

4. それぞれの数字をサイコロに置き換える

f:id:YaaMaa:20170315175512p:plain

(4文字目以降は省略)

すると、赤い丸を持つ1だけが浮かび上がるので、3×3の枠全体を一つのサイコロと見ると、3156204(語呂合わせで「サイコロ振れよ」)の数字が浮かび上がる。これが暗号の答え。

※ コナンが作った暗号では、3×3の枠全体をサイコロと見なすのではなく、ただ枠に現れる模様を見る。すると、カタカナで「ソトニサソエ」と浮かび上がり、これが答えとなる。

解き方を知った上で暗号を解くと、

展開 → 9を除いた残りを3×3の枠に流し込む → 1が形成する模様を読み取る

という割とシンプルな方法で解ける。

暗号作成

こういう暗号を作るにあたって、二つの記号を定義しておく。

  • N: 2, 3, 4, 5, 6
  • R: Q, K

暗号では結局1以外の数字は「1でない」ことに意味があり、実際の数字なんて何でもいいので、2~6はNと抽象化しといて後で振り分けた方が楽。同様に、QとKも二桁目が2であるか3であるかの違いしかないので、「1以外は何でもOK」という考えの下ではそれほど違いがないため、まとめてRと置く。

ユーザーからの入力

ユーザーから暗号の答えを入力してもらう。

入力する方式は下の二つを用意しなければならない。

  • 模様: コナン作の暗号のように、3×3のマスの中の模様が表すものがそのまま暗号の答えになる場合

  • 数字: 楠川作の暗号のように、3×3のマス全体を一つのサイコロと見立てて数字を導き出す場合

入力されたら、1になるところをA、1以外になるところをNとしておく。

例えば、数字の入力欄に「810」と入力されたら

AAA ANA AAA  NNN NAN NNN  NNN NNN NNN

となるし(「8」はサイコロにない数字なので、勝手に模様作った)、模様の入力欄に

■□□
■□□
■■□

□■□
□□□
□■□

■□□
■■□
■■□

と入力されたら

ANN ANN AAN  NAN NNN NAN  ANN AAN AAN

となる。

短縮

数字をアルファベットに直す操作。短縮には二種類ある。

  • AA-J短縮: AAは「11」の意味なので、Jと短縮できる。

  • AN-R短縮: ANは「1, 1以外の数字」が並んだものなので、Nを2or3に限定することでRと短縮できる。どうせNは後から2~6のどれかの数字に決まることになるので、ここで限定しちゃってもOK

最低短縮数を設定する

J, Q, Kが揃ってないとトランプだとわかってもらうのは難しいので、それぞれ最低一つずつほしい。なので、

  • AA-J短縮の最低短縮数は1

  • AN-R短縮の最低短縮数は2

とする。これに従えない場合(以下で求める短縮可能数が十分でない場合とか)は妥協する。

短縮可能数を調べる

AA-J短縮とAN-R短縮がそれぞれ何回行えるかを調べる。

  • AA-J短縮: Aが連続して二つあり、且つ一つ目のAがそのAの連続内で奇数個目にあるときに行える

  • AN-R短縮: AとNがこの順で連続していて、且つAがそのAの連続内で奇数個目にあるときに行える

合計短縮数を決める

短縮を一度行うごとに、暗号は一文字ずつ短くなる。

つまり、合計短縮数は、展開後の数字の個数 - 展開前の英数字の個数である。作中に出てきた暗号の場合、82-72=15回。

展開後の数字の個数はユーザーからの入力によって自動で決まるけど、展開前の英数字の個数は特に決まっていない。とはいえ、以下のような制限がある。

  1. n2でなければならない

    展開前の英数字の並びが正方形であるというのには、服部曰く「サイコロを表す」という意味があるので、正方形にできるように自然数の二乗個でなければならない。

  2. n2であるとき、nは奇数でなければならない

    9を中央に配置するというのには、服部曰く「サイコロの1の目を表す」という意味があるので、9を中央に配置できるように一辺の個数は奇数でなければならない。

これらの条件を考慮して、また短縮可能数に収まる範囲内で、(そしてできれば最低短縮数よりは多く、)短縮合計数を決める。

ただし、上記の二つの条件には従えない場合がある。例えば暗号の答えが「0」だったとき、暗号は「NNN NNN NNN」のどこかに9を挿入したものとなる。すると数字が10個となり、短縮可能数も0なので、n2には絶対にできない。こういうときは妥協する。

短縮数を分配する

合計短縮数を、AA-J短縮とAN-R短縮に分配する。楠川さんたちが作った暗号での配分は以下の通り。

J + R J R
楠川作 15 3 12
服部作 15 4 11
コナン作 15 5 10

だいたいJの個数:Rの個数=1:2~4くらいが妥当なのかなと思う。

なので、配分の仕方は以下のようにした。

  • 合計短縮数 < 最低短縮数の合計になってしまうときは、それぞれの最低短縮数に比例するように短縮合計数を分配する

  • 最低短縮数の合計 < 合計短縮数の場合は、最低短縮数を満たせば、あとはなるべく上記の比率に従うようにランダムで分配する

置換/反転

ただ短縮しただけでは、文字の配置はワンパターンになってしまう。例えば、「AAAAAN」という並びは、「JJAN」「AJAR」などいろいろな方法で短縮できるのに、上記で行った短縮だけだと毎回同じパターンになってしまう。それを防ぐために、置換や反転を行う。

  • JNとARの置換: JNは、NをRに限定することでARと置き換えられる。特にRの数が足りないときに積極的に置換する

  • AとJの反転: AとJが隣接しているとき、それらはひっくり返しても大丈夫

具体化

抽象化してあったRとNを具体化する。

Rの具体化

RをQとKに具体化する。楠川さんたちが作った暗号では平等に割り振られているので、それに従う。

R Q K
楠川作 12 6 6
服部作 11 6 5
コナン作 10 5 5

Rが奇数個で余ってしまったら、服部作のに倣ってQに回せばいいかな。

Nの具体化

楠川さんたちの暗号での2~6の出現回数は以下の通り。

  • 楠川作
展開前 展開後
2の出現回数 7 13
3の出現回数 7 13
4の出現回数 5 5
5の出現回数 6 6
6の出現回数 5 5
  • 服部作
展開前 展開後
2の出現回数 5 11
3の出現回数 5 10
4の出現回数 6 6
5の出現回数 4 4
6の出現回数 6 6
  • コナン作
展開前 展開後
2の出現回数 3 8
3の出現回数 3 8
4の出現回数 4 4
5の出現回数 3 3
6の出現回数 4 4

こう見ると、展開後ではなく展開前の時点で大体均一に分配すればいいことがわかる。

これくらいのばらつきを保ちつつ、ほどよく均一に分配する方法を調べてたら、以下の記事がすごい参考になった。

Box-Muller法を用いて正規分布に従う分配を行う - Qiita

この方は、出てきた二数を$readyを使うことで効率的に使ってるけど、めんどくさいし、片方しか使ってない例(書いた方は「大丈夫なのか…」と不安げにしてはるけど、結果のグラフはいい感じに見える)もあるので、まあいっかと思って片方しか使わなかった。

標準偏差はどうするのかということだけど、こういう場合平均が上がるにつれて標準偏差も大きくなっていくのが自然だと思うので、標準偏差を平均で割ってみたらどんな値が出るか見てみた。

平均 標準偏差 標準偏差/平均
楠川さん作の暗号 6 0.89443 0.14907
服部作の暗号 5.2 0.74833 0.14391
コナン作の暗号 3.4 0.4899 0.14409

かなり一定になってる! なので、この平均をとって、標準偏差=平均*0.146にした。

(後から知ったけど、標準偏差/平均は「変動係数」と呼ばれるもので、ちゃんと意味のある数値らしい)

また暗号を解く際に服部が「1から6までは仰山あんのに、9は一個だけしかない」と言っていたことから、9を際立たせるために2~6は複数個、最低2個ずつないといけない。 更に、Nが極端に少ない場合、6を出現させるのを最優先する。「サイコロ」という発想を浮かばせるためには、「6までの数字がある」と認識させるのが大事だから。それに、2や3はRの展開で出てくる可能性もあるから、Nで無理に作る必要性はあんまりない。

なので、2~6までに2個ずつ配分できるほどNが多くない場合、つまりNの個数<(6-2+1)*2のときには、6→5→4→3→2→6→5→…の順で配分していく。

9の挿入

最後に、真ん中に「9」を挿入する。

これで暗号が完成!

できたもの

hattori.cipher.jp

↑のページで公開していますので、よかったら遊んでみてください。結構な頻度で「この暗号には問題があります」って出るので鬱陶しいと思います(*‘▽’)

ミニバードで無料ドメイン見てたら、「cipher.jp」っていうのがあったから、ぴったりな名前やなって思ってすぐ取得した。

サイト内で一番力を入れたのは、暗号解読画面の解説。なんとかアニメ放送された本家の画像をcanvasで再現しようと頑張った。

f:id:YaaMaa:20170315133906p:plain

f:id:YaaMaa:20170315133928p:plain

f:id:YaaMaa:20170315133940p:plain

f:id:YaaMaa:20170315180929p:plain

上が本家、下がcanvasで作ったやつ。よく見るとグラデーションの色合い・角度とか服部のツノ(一枚目の右上から生えてるやつ)のとんがり具合とかいろいろと違う(・_・`)

あと、楠川さんの暗号って3×3の枠に流し込んだときに2~6がすごい対称性のある綺麗な並びになるようにできてんねんかな。これのおかげで解読方法により確信が持てるようになるんだろうけど、私には真似できなかった…。服部とコナンも、(切羽詰まった状況で暗号作ったから当たり前だけど)そんなに対称性は持たせられてないので、まあしゃあないかと思って妥協してしまった。

服部が暗号を組み立てた手順

では、作中で服部がどんな感じであの暗号を組み立てたのか考えてみる。

まず、下3行を血で汚した理由だけど、あれは「3文字分で弁護士のことを表せるから、およそ3文字分に相当する3行分を汚そう」と思ったのか、「血の量的に汚すのは3行が限界だから3行分汚そう」となったのかどっちかはわからない。というか、手を使えない状況でどうやって暗号の紙を血で汚したんだろう。顎から一滴一滴したたらせてたんだったら死ぬほど時間かかるし、楠川さんに手伝ってもらったんかな。めっちゃ大変そう。

いずれにせよ、下3行分を見えなくした時点で、展開前の(7×3=)21文字が隠されたことになる。これは展開後の25文字分に相当する。3×3の枠で3文字を表そうと思ったら27個(=3×3×3)の数字が必要だが、最初の二つ(暗号全体の展開後でいうと後ろから26個目と27個目)を楠川さんのと共通にしとけばOK

そこを共通にするためには、犯人を示唆する文字列の一文字目はANから始まることになる。「Lib」ならその条件を満たしている。

Libを3×3の枠×3で表そうとすると、

(AN)N
ANN
AAN

NAN
NNN
NAN

ANN
AAN
AAN

上のように配置することになる。これを全部繋げると、下の通り。

(AN)NANNAANNANNNNNANANNAANAAN

※ 初めのANは既に固定されているので、()付きで表した。

このうち初めのANを除いた「NANNAANNANNNNNANANNAANAAN(25文字)」を短縮して21文字(7文字×3行)にすればいい。なので、短縮は25-21で4回行う。

血で汚れてないところにもJ, Q, Kはそれぞれトランプを連想するのに十分な回数出てるから、今からの短縮では特に文字の出現回数は考慮しなくて大丈夫。

短縮できるのは、0始まりで数えると、

  • 1-2個目: AN-R

  • 4-5個目: AA-J or 5-6個目: AN-R

  • 8-9個目: AN-R

  • 14-15個目: AN-R

  • 16-17個目: AN-R

  • 19-20個目: AA-J or 20-21個目: AN-R

  • 22-23個目: AA-J or 23-24個目: AN-R

これだけある。このうち、服部は4-5, 8-9, 20-21, 23-24個目を短縮した。

(私の方法でいったら、AANをARにするためにはまずAA-J短縮をしてからJN-AR置換をするけど、あれはプログラム書くときに楽な方を取っただけで、人間だったらAANからそのままAN-R短縮すると思う)

短縮が終わったら、下のようになった。

NANNJNNKNNNNANANNAQAK

最後に、Nに2~6までの適当な数字を分配する。

3A55J64K6243A6A64AQAK

これを上の4行と合わせたら、工藤に送る暗号の完成。

殺されそうな瀬戸際で、殴られまくって頭ガンガンしてるだろうに、血で汚れてない上4行を記憶しながらこんだけ考えられるなんて、服部ってすごい…(゜-゜)


USJでやってるコナンアトラクションの服部がめっちゃかっこいいらしい。何としてでも行かなければ(>_<)