いものやま。

雑多な知識の寄せ集め

ベイズ統計学を学んでみた。(その2)

前回はイントロで、今回から具体的な内容に入っていく。

確率論

ベイズ統計学を考えていくのに必要となるのが、確率論。 まずはこの確率論の基本的なところを押さえていきたい。

なお、高校数学の確率論だと、集合の要素数を使って確率を定義していく。 また、大学で扱うような公理的確率論だと、コルモゴロフの公理から確率を定義していく。

ただ、前者は連続変数を扱う場合には力不足だし、後者は直感から外れるところ(確率変数の定義とか)があって分かりにくい。

そこで、自分は「分布」を基礎として確率を定義していきたい。 このあとの議論をみれば分かるように、分布から確率を定義していくと、直感的で分かりやすく、それでいて連続変数を扱うことも可能になる。

確率変数

何かをやったときに起こりうる事象をすべて集めた集合を、確率変数と呼ぶことにする。 確率変数は集合なので、一般に大文字(たとえば X)で表記する。

なお、慣例にしたがって“変数”と呼んでいるけれど、実際には“集合”なので、普通の変数( xとか)とは違い、何か値が代入されたりするわけではない。

また、公理的確率論だと確率変数は関数として定義されるけど、ここではそう定義していないので、公理的確率論の確率変数とは厳密には別物なので注意。関数として定義してるのに X=2とか書いてる時点でおかしいんだけどね・・・ 公理的確率論では標本空間に相当する。

閑話休題

確率変数の一例は、サイコロ1個を振ったときの出目。 この確率変数を Xとすると、 X = \{1, 2, 3, 4, 5, 6\}となる。

あるいは、コイン投げの結果も確率変数の1つで、この確率変数を Yとすると、 Y = \{\text{表}, \text{裏}\}となる。 確率変数の要素は、別に数字じゃなくてもいい。

他にも、適当に誰か1人を選んだときのその人の身長とかも確率変数になる。 人間の身長だと小さくても30cmくらいから大きくても250cmくらいだと思うけど、それよりも小さかったり大きかったりというのは考えられる。 なので、取りうる値は0より大きい実数とし、この確率変数を Zとすると、 Z = \mathbb{R}_{> 0}となる。

 X Yのように要素が離散的な確率変数を離散確率変数と呼ぶ。 また、 Zのように要素が連続的な確率変数を連続確率変数と呼ぶ。

今日はここまで!

ベイズ統計学を学んでみた。(その1)

機械学習をやってると、なにかと耳にするのがベイズ統計学。 数理最適化をやってきた身としては、正直どうなの?と思うんだけど、理解もせずに批判するのもアレなんで、ちょっと勉強して自分なりにまとめていきたいと思う。

一応、参考にした本は以下:

しくみがわかるベイズ統計と機械学習

しくみがわかるベイズ統計と機械学習

  • 作者:手塚 太郎
  • 発売日: 2019/11/01
  • メディア: 単行本(ソフトカバー)

他、次のような本も眺めてる(※全部は読んでない):

ベイズモデリングの世界

ベイズモデリングの世界

  • 発売日: 2018/01/18
  • メディア: 単行本(ソフトカバー)

パターン認識と機械学習 上

パターン認識と機械学習 上

ただ、この本に限らず、ベイズ統計で使われる記法は雑すぎて理解に苦しむので、この一連の記事では独自の記法を使うことにする。 また、話や理論の流れについても、分かりやすくなるように独自で整理している。 これまでベイズ統計をやってきた人は違和感を覚えるかもしれないけど、普通に数学をやってきた人には逆に分かりやすいはず。

ここがダメだよベイズ統計

あらかじめ書いておくと、先にも述べたとおり、自分はベイズ統計については現時点でかなり懐疑的。

まず、なんかベイズ統計の人たち、狂信的なイメージあるのよね・・・

ベイズ統計はすごい! 最高! 他の方法はダメだ!」みたいなのを見ると、「うわぁ・・・」って正直思っちゃう。 まぁ、そういう人はかなり限られてるとは思うけど。 ただ、そこまでいかなくても、みんなベイズ統計はいいものだと思ってて、批判的な意見を見ることが少ない。

そして、「じゃあ、そんなにいいものなら、なんでみんな使わないの?」と聞くと、「数学が分かってないと使えないから」という答えが返ってくる。

いや、自分のような人間ならともかく、深層学習の最先端をやってるような研究者とかは数学分からないなんてことはないわけで。 だから、使われてない理由は単に「使い物にならない」からでしょ。 あと、ベイズ統計学での記法が雑すぎて、数学つよつよの人には逆によく分からないという可能性はあるけど。

結局、ベイズ統計では生成モデルを考えるけど、その想像力の限界がモデルの限界として現れてくることになる。 人間の理解の限界が、モデルの限界。 だから、出来たモデルはもちろん人間にも理解可能なものになるけど、人間の理解を超えた性能を出すことはできない。

一方、深層学習とかは、そのよく分からないものをよく分からないまま扱うので、出来たモデルが理解可能とは限らないけど、人間の理解を超えた性能を出すことができる。

もちろん、ベイズ統計の結果を使って深層学習などのモデルの説明をしようとしてたりするわけだけど、それって批評家があとから「それが上手くいったのはこれこれこういった理由なので当たり前ですよね」みたいにドヤる感じに似ていて、それなら「お前が最初にやれよ」って感じ。 それができないなら、後付けで説明が与えられても、正直どうなのとしか言えないと思う。

記法が雑?

ちなみに、これまで何度も「記法が雑」と書いてきたけど、これがベイズ統計を分かりにくくしている一番の原因だと思う。 雑だと思えない? そういう人は残念ながら数学やるのにあまり向いてない。

たとえば、当たり前のように P(X=2)みたいな書き方をするけど、冷静に考えると意味分からないよね。

「えっ、確率変数 Xの値が2であるような確率でしょ?」と思うかもしれないけど、それなら P(X)は? 確率変数 X確率密度関数(もしくは確率質量関数)?

じゃあ、 P(X \le 2)はどうかというと、確率変数 Xが2以下である確率だと思うけど、ひるがえって考えると P(X=2)は「確率密度関数 Xが2のときの値」なの? 「確率変数 Xが2のときの確率の値(確率変数が連続なら0になる)」なの?

そもそもの話をすると、Pの定義域ってどこ? 確率変数 Xと確率変数 Yが独立なら、 P(X, Y) = P(X)P(Y)って書いたりするけど、この式をじっと見ると Pって一体何なんだとなってくる。

本によっては、 Pは関数ではなくて確率を関数っぽく表現した記法なんて書いてあったりして、なんじゃそりゃって感じ。

この辺りのカオスな記法運用についても、整理していきたいと思う。

今日はここまで!

Googleスライド左下見えない問題に対処してみた。

Googleスライドはブラウザさえあれば使えるので、愛用してる人も多いはず。 自分も仕事でとてもお世話になっている。

ただ、一つ困ったことが。

そう、Googleスライド左下見えない問題』

プレゼンテーションをやっているとき、左下の方にカーソルを持っていくと、コントロールバーが表示されて左下のコンテンツが隠れてしまう。。。

f:id:yamaimo0625:20210307030225p:plain
左下の内容が・・・

f:id:yamaimo0625:20210307030401p:plain
見えなくなる。。。

これを解決する方法を見つけたので、紹介したい。

Invisible Google Slide Control Bar

方法は簡単で、Chrome拡張を入れるだけ。

このChrome拡張を入れてGoogleスライドを開くと、コントロールバーに「<」というボタンが追加される。

f:id:yamaimo0625:20210307032245p:plain
ボタンが追加される。

このボタンを押すと・・・コントロールバーが左にスライドして収納される!

f:id:yamaimo0625:20210307032503p:plain
左下が見える!

ちなみに、隠れたコントロールバーは「>」ボタンを押すとまた出てくる。

Chrome拡張が使えない場合

なお、Chrome以外のブラウザやセキュリティの関係で拡張が禁止されてる場合は、残念ながらこの拡張を使えない。

その場合、ちょっと手間がかかるけど、次の方法が使える。

  1. スライドのURL(https://docs.google.com/presentation/d/<スライド固有のID>/edit<オプションなど>)をコピーする。
  2. edit<オプションなど>の部分をpreviewに変える。

こうするとコントロールバーがステータスバーのように表示されるので、左下が隠されることがない。

f:id:yamaimo0625:20210307040814p:plain
見える!

今日はここまで!

2021年にやっていきたいこと。

f:id:yamaimo0625:20210101203756p:plain
初日の出@江戸川土手

あけましておめでとうございます! 今年もよろしくお願いします!

ということで、今年やっていきたいことのリストアップ。

仕事

仕事に関しては、数理最適化を使う案件がいろいろ入ってくると思うので、それらをこなして実績を積んでいきたい。 本当は、やったことを公表もできるといいんだけど。 まぁ、企業だとなかなか難しいところもあるので、それはワンチャンあればという感じかな。

そして、データ分析の仕事に関してはまだまだ経験値が足りてないと思うので、地力を伸ばしていきたい。 とくに、ベイズ推定に関して理解できてない部分が大きいので、ちょっと頑張ろうと思う。

他、数理最適化に関してはもうちょっと理論的な部分(凸解析とか)を勉強しなおしたいというのはあるけど、うーん、できるかな・・・

同人活動など

同人活動に関しては、3冊は難しいので、2冊くらい新刊を生やしたい。 コウモリ少女の続編を書きたいんだよなぁ。

あと、 \TeXバイバイするために、いろいろ調べたりアウトプットを出していければなと思ってる。 まぁ、優先度としては仕事に関する勉強(数理最適化や機械学習ベイズ統計)の方が高いので、やるやる詐欺で終わる可能性が高いけど。

体調管理

あとは、体調管理。

これは地道に続けていかないとなので、ふりかえりをしながら意識が持続するようにしていきたいと思う。

とりあえず、以下を続けていく予定:

  • 早寝早起き
  • ラジオ体操
  • 散歩
  • ストレッチ

上記に加えて、新型コロナのせいで運動する量が圧倒的に減っているので、もう少し何か運動できるといいな。 ロードバイクはおそらく乗らないけど(車を運転するようになって、ロードバイクがさらに怖く感じられるようになった・・・あと、落車のトラウマはやっぱり大きい)、普通の自転車に乗る機会はもうちょっと増やしたいところ。

ブログ

最後にブログに関して。

これはあまり気負わずに、ゆるくやっていけたらと思う。 アウトプットの方法はブログだけに限らないので。 同人誌でもいいし、何かOSSをやるでもいいし。

ただ、去年はアウトプットの量自体が減ってしまったので、今年はどういった形でもいいから少しずつアウトプットを積み重ねていくことは意識したいかな。

今日はここまで!

2020年をふりかえってみた。

毎年恒例のふりかえり。 (びばさんに倣って平仮名の「ふりかえり」でw)

去年のふりかえりは以下:

そして、今年やっていきたいと考えていたことは以下:

仕事

まずは仕事に関して。

新型コロナがあったりとかで転職活動も大変だったんだけど、7月から無事働き始めることができた。

新しい仕事は電力関係の仕事で、データ分析や数理最適化を業務で使うことができたので、とてもよかった。 データ分析に関してはまだまだ未熟なのでもっと地力をつけていかないとという感じだけど、数理最適化に関してはチームの中では比較的専門的な立場で成果を出すことができたと思う。 そして、数理最適化を使う案件が増えてきてる感じがあるので、今後も楽しみ。 もっと力をつけて貢献していきたいし、そういったことを通して社会での数理最適化の価値を高めて、自身の市場価値を上げていければなと。

ちなみに、仕事で実際に数理最適化を使ってて課題に思ったのは、不確実性への対処。 確率計画法を調べたりもしたけど、他にもロバスト最適化やオンライン最適化という枠組みもあったりするので、使える道具を増やしていきたい。

他にも、新しい職場ではGCPやGitを使って開発できてるんで、やっと普通の開発環境になったという感がある。

あと、基本的にリモートなのもすごく助かった。 満員電車は本当に疲弊させられるので・・・ 新型コロナが落ち着いたあともリモートは続いて欲しいなぁ。

体調管理

次に、体調管理に関して。

これは新型コロナでいろいろ変わったところがある。

まず、プールには全然行けなかった。 新型コロナで緊急事態宣言が出た頃は、プール自体が休館に。 そして、秋くらいになるとプール自体は再開したんだけど、換気のために窓は全開の状態で、水の中は暖かいけど外に出るとめっちゃ寒く、数回行ってこれは無理だなと諦めた。

あと、リモートワークで通勤がなくなったので、本当に運動量が落ちた。 通勤してた頃は1日に大体6,000歩くらいは歩いてたんだけど、新型コロナで引きこもってからは1,000歩も歩かない日がほとんどになったり。

これはよくないということで、今は昼に家の周りを20分くらい歩くようにした。 本当はもっと歩いた方がいいんだろうけど、これで2,000歩くらいは歩けてる感じ。

他、年初に挙げていたのは、早寝早起きとラジオ体操、それとストレッチ。

早寝早起きについては、転職してからはちょっとできてない感じ。 週末のふりかえりで少し意識すると数日は直るんだけど、どうしても寝るのが遅くなる。。。 何かいい方法があればいいんだけど、地道に改善していく感じか。

ラジオ体操はちゃんと続けられてる。 ストレッチの効果もあって、少しずつ体も柔らかくなってきてる気がする。

ストレッチについてはなかなかできてなかったんだけど、ストレッチ音声を作ったのがよかった。

ちなみに、上の記事だと40分弱の音声を作ったんだけど、40分だと長くて続かなかった。 なので、途中からは20分強に縮めたものを作ってやるようにした。

やってみて分かったのは、30分より短いかどうかって心理的にはけっこう重要だということ。 30分より短いと30分という単位で考えられるけど、30分より長いと1時間という単位で考えてしまうというか。

さらに、最近はアニメを見るついでにストレッチをやるようにしてる。 そうすると「ストレッチするぞ」っていうスイッチを入れる必要がないので、けっこう手軽にストレッチをすることができたり。 (そういう意味でも、30分より短いというのは重要)

まぁ、そんな感じで、完璧にできてたわけではないけど、ふりかえりをしながら少しずつ改善はできた。 まだまだ運動が足りてない感じはあるので、来年も改善を続けていきたい。

ブログ

ブログに関しては、ほとんど記事を書けなかった。 仕事をしてると、なかなか大変。。。

ちょっと試したこととしては、日記ブログがある。 まぁ、結論としてはダメな感じだったけど。

ただ、上記の記事でも書いた通り、得るものはあった。 もっとも、あまり記事は書けてないんだけど。

勉強

勉強に関しては、今年はPythonにだいぶ触れたなという感じがある。 まぁ、結局Pythonを好きになることはなかったけど。 この言語、ホントどうにかならないものか・・・ ただ、相変わらず嫌いではあるものの、とりあえず一通り使えるようにはなったと思う。

数理最適化に関しては、実務で使えてるというのが大きかったと思う。 理論的な部分ももっとやっていきたい感じはあるけど。

あと、今年は統計検定2級をとったりもした。

それに関連して、機械学習には数理最適化の文脈じゃなくてベイズ統計の文脈での研究もあって、そっちはまだキャッチアップできてないので、来年は少しキャッチアップしていきたいところ。

あとは \TeXに関して。 結局、ほとんど勉強しなかったんだけど、組版関連のいろいろな勉強会で話を聞いて思ったのは、やっぱり自分でどうにかしないとダメなのかなということ。  \TeXがツラいのは分かりきったこととして、SATySFiはベター \TeXの域を抜けれてないし、Vivliostyleはブラウザ側の実装の関係でツラい部分が残ってる感じ。 Re:VIEWやPandocはPDFを出力しようとすると結局 \TeXに頼ることになるし。 他には、JupyterでPDFを出力したりもできるみたいなんだけど、力不足感は否めない。。。

同人活動

同人活動に関しては、年初に挙げた目標は、新刊を3冊出すこと。

ただ、新型コロナの影響でリアルイベントがなくなったりで、けっこう影響が大きかった。

結果として、以下の2冊を出すことができた:

本当はもう1冊、コウモリ少女の続きを出したかったんだけど、調査が間に合わなかったりで無理だった。 残念。。。

このあたり、やっぱり仕事をしながらだと難しい感じがある。 なんとか執筆する時間と気力を生む方法を考えていきたいなぁ。

その他

あと、今年大きかったのは、今更ながら車の免許をとったこと。 これでだいぶ生活が変わった。

免許をとったのは今年の1月だったけど、運転の練習を兼ねて、できるだけ車で出かけるようにしてた。 そのおかげもあってか、やっと運転にも慣れてきた感じだけど、代わりに自転車に全然乗らなくなってしまった。 それで運動不足が余計に酷くなってる感じもあるので、来年はもう少しは自転車にも乗るようにしたい。


何はともあれ、転職して経済的に安定して、数理最適化を仕事で使えるようになったりと、いい感じな年だった。

一方で、アウトプットは減ってしまってる感じがあるので、来年はもう少し頑張りたい。

今日はここまで! よいお年を!

天体ショーを整数計画問題で解いてみた。

ペンシルパズルの「天体ショー」を整数計画問題として定式化してソルバーを使って解いてみた。

これは数理最適化 Advent Calendar 2020の19日目の記事です。

天体ショー

ペンシルパズルだとピクロス数独が有名だけど、天体ショーもその一つ。

いろいろなペンシルパズルがあるけど、自分はこの天体ショーが大好き。

  • 問題の図が星を散りばめたようになっていて、まさに「天体ショー」
  • ルールが「点対称」を使ったものになっていて、「点対称」と「天体ショー」を掛けているのが素敵
  • 解き終わったときにイラストが出てくるので、解く喜びが大きい
  • 序盤はサクサクとテンポよく進み、中盤で考えどころがあって、大きなブロックを切り出せると興奮する
  • 問題を作るのも比較的簡単(ドット絵を考えて、点対称な図形で区切っていけばいい)

そこで、天体ショーのソルバーを作ってみることを考えた。

人間が解くようにロジックを組んでいってもいいんだけど、整数計画問題として定式化さえできれば、汎用ソルバーで解くこともできる。 なので、今回は整数計画問題として定式化することを考えてみた。

ただ、検索してみると、すでに先人が。

鉛筆パズルの整数計画法による解法定式化集
(52番目に天体ショーがある)

なので、これを実装するだけかなと思ったんだけど、ちゃんと読んでみると問題があることが分かった。

連結制約

基本的な方針は、あるマスがある星に属するかどうかを0-1変数で表現する先人の方法でOK。 ただ、先人の定式化だと「ある星に属するマスがすべて連結している」という条件(以下、連結制約)をちゃんと表現できていないことに気がついた。

一応、「孤立したマスがない」「2マスだけが繋がったブロックがない」「L字型にだけ繋がったブロックがない」という条件は入っていて、これで大体OKだったらしい。

けど、次のような簡単な問題でも上記の制約だけではダメになる:

f:id:yamaimo0625:20201219224218p:plain

この問題の正しい答えは以下:

f:id:yamaimo0625:20201219225415p:plain

けど、先人の入れた制約だと、以下のような答えも正しいとなってしまう:

f:id:yamaimo0625:20201219230227p:plain

先人の制約はすべて守られているけど、明らかに上のブロックと下のブロックは真ん中のブロックと連結してないので、正しい答えになっていない。

じゃあ、どうすれば連結制約を正しく表現できるのか・・・?

これが想像以上に難しかった。 ああでもない、こうでもないと悩み続け、昨日やっと答えが分かった。

全域木

答えは「ブロックが全域木になっていることを表現する」というもの。

任意のブロックについて、マスを頂点、マスとマスの境界を辺と考えると、これはグラフになっていて、「ブロックが連結である」ことと「グラフに全域木が存在する」ことは同値になっている。 (※ここでは全域木の定義を「連結」で「閉路を持たない」グラフとする。単に「閉路を持たない」グラフは全域森)

そして、ブロックは星のあるマスから伸びていくと考えると、グラフに全域木が存在するなら、星のないマスはすべて、ただ1つの親を持つことになる。 (逆に、星のないマスがすべて、ただ1つの親を持つなら、グラフに全域木が存在する)

そこで、 D = \{\textrm{up}, \textrm{right}, \textrm{down}, \textrm{left}\}として、マス nに対して、方向 d \in Dのマスが親かどうかを、変数 p_{n, d} \in \{0, 1\}で表現することにする。 さらに、星のないマス nに対して、星のあるマスまでのパスの長さを変数 l_n \in \mathbb{Z}_{\ge 0}で表現することにする。

このとき、次の3つが制約として必要になる:

  1. 星のない任意のマスについて、親はただ1つ存在する
     \sum_{d\in D} p_{n, d} = 1
  2. 親のパスの長さは、子のパスの長さよりちょうど1小さい
     -M (1- p_{n, d}) \le l_{n} - l_{n'} - 1 \le M (1 - p_{n, d})
    (※  Mは十分に大きな正の整数で、子 nと親 n'は隣り合っていて、 dは適切な方向とする)
  3. 親は子と同じ星に属する
     p_{n, d} + x_{n, m} - 1 \le x_{n', m}
    (※ 親 n'は子 nに隣接していて、 dは適切な方向とし、変数 x_{n, m} \in \{0, 1\}はマス nが星 mに属するかどうかを表すとする)

2つめの制約はちょっとテクニカルで、 n'が親でないなら -M \le l_{n} - l_{n'} - 1 \le Mとなるので実質意味のない制約になり、逆に n'が親なら l_{n} - l_{n'} - 1 = 0なので子のパスの長さが親よりちょうど1大きいという制約になる。

また、最後の制約は、 p_{n, d} = 1(つまり n' nの親)かつ x_{n, m} = 1(つまり n mに属する)のときに限り、左辺は1となって(それ以外は0以下)、そのときに限り x_{n', m} = 1(つまり n' mに属する)となる(それ以外は右辺は0でも1でもいい)ので、ちゃんと条件を表現できていることが分かると思う。

(2020-12-26追記)
2.は元の制約だと閉路になる場合があったので、修正した。

PICOS

あとは他の制約なども数式で表現してソルバーに食わせればいいだけで、PythonからPICOS(+GLPK)を使って解いてみた。

ちなみに、PICOSはPuLPOR-Toolsのようなライブラリなんだけど、類似したライブラリの中で一番Pythonっぽいコードが書けるので、自分は気に入ってる。 ソルバーもデフォルトでCVXOPTがついてきて、他にもGLPKやSCIP、あと持ってればCPLEXやGurobiも使える。 さらにはLPやMIPだけじゃなくてSOCPやSDPも解ける優れもの。 (知名度だけは低い・・・)

まず、問題はテキストで以下のように書くことにした:

-----------
| | |o|*|o|
--o--------
| | | * | |
---------o-
|o|*|o| | |
-----------
| * | |*| |
-----------
| |o| | |o|
-----------

oが白い星で、*が黒い星。

これを読み込んで情報を保持するために、MarkerクラスとProblemクラスを定義:

from enum import Enum

class Marker:
    class Color(Enum):
        WHITE = 0
        BLACK = 1
    
    def __init__(self, color, row, col):
        self.color = color
        self.row = row
        self.col = col
class Problem:
    @classmethod
    def read_str(cls, contents):
        lines = contents.splitlines()
        row_size = len(lines) // 2
        col_size = len(lines[0]) // 2
        markers = []
        for r_idx, line in enumerate(lines[1:-1]):  # 最初と最後の行は冗長なので無視
            for c_idx, char in enumerate(line[1:-1]): # 最初と最後の列は冗長なので無視
                row = r_idx / 2
                col = c_idx / 2
                if char == 'o':
                    marker = Marker(Marker.Color.WHITE, row, col)
                    markers.append(marker)
                elif char == '*':
                    marker = Marker(Marker.Color.BLACK, row, col)
                    markers.append(marker)
        return cls(row_size, col_size, markers)
    
    @classmethod
    def read_file(cls, filepath):
        with open(filepath) as f:
            contents = f.read()
        return cls.read_str(contents)    
    
    def __init__(self, row_size, col_size, markers):
        self.row_size = row_size
        self.col_size = col_size
        self.markers = markers

そして、解く関数を以下のように定義:
※めっちゃ長い

import numpy as np
import math
import picos

# 方向
directions = ['up', 'right', 'down', 'left']

def solve_by_picos(problem, solver='glpk'):
    # 整数計画問題
    ip = picos.Problem()
    ip.set_objective('find')
    
    # 変数 ==========

    # (row,col)のマスがmarkerに属するかどうか
    belong = {}
    for row in range(problem.row_size):
        for col in range(problem.col_size):
            for i, marker in enumerate(problem.markers):
                belong[row,col,marker] = picos.BinaryVariable(f'belong_{row}_{col}_{i}')

    # (row,col)のマスについて、directionのマスが全域木で親かどうか
    parent = {}
    for row in range(problem.row_size):
        for col in range(problem.col_size):
            for direction in directions:
                parent[row,col,direction] = picos.BinaryVariable(f'parent_{row}_{col}_{direction}')
    # 上端、右端、下端、左端については、各方向が親になることはないので、変数を0に置き換える
    for row in range(problem.row_size):
        # 左端は左が親にならない
        parent[row,0,'left'] = 0
        # 右端は右が親にならない
        parent[row,problem.col_size-1,'right'] = 0
    for col in range(problem.col_size):
        # 上端は上が親にならない
        parent[0,col,'up'] = 0
        # 下端は下が親にならない
        parent[problem.row_size-1,col,'down'] = 0

    # (row,col)のマスの、全域木でのルートまでの距離(明確に属するマスをルートとする)
    distance = {}
    for row in range(problem.row_size):
        for col in range(problem.col_size):
            distance[row,col] = picos.IntegerVariable(f'distance_{row}_{col}', lower=0)

    # 制約 ==========
    
    # 各マーカーについて、明確に属するマスを制約として表現する
    # (距離が0になるという制約も入れる)
    # このとき、明確に属するマスは親が不要なので、変数を0で置き換える
    for marker in problem.markers:
        row_range = range(math.floor(marker.row), math.ceil(marker.row)+1)
        col_range = range(math.floor(marker.col), math.ceil(marker.col)+1)
        for row in row_range:
            for col in col_range:
                ip.add_constraint(belong[row,col,marker] == 1)
                ip.add_constraint(distance[row,col] == 0)
                for direction in directions:
                    parent[row,col,direction] = 0

    # 各マスはいずれかのマーカーに属する
    for row in range(problem.row_size):
        for col in range(problem.col_size):
            belong_sum = sum(belong[row,col,marker] for marker in problem.markers)
            ip.add_constraint(belong_sum == 1)

    # 点対称の位置にあるマスは同じマーカーに属する
    # NOTE:
    # 点対称なマスに関して、以下が成り立つ:
    #   (row + opposite_row) / 2 = marker.row, (col + opposite_col) / 2 = marker.col
    # なので、点対称なマスの位置は、以下で計算できる:
    #   opposite_row = 2*marker.row - row, opposite_col = 2*marker_col - col
    # まず、点対称なマスが枠外になる場合、そのマスがマーカーに属することはない。
    # そして、
    #   opposite_row < row
    # であれば、行についてマーカーを跨いで反対側に来ているので、すでに登録済み(再度登録はしない)であり、
    #   opposite_row == row かつ opposite_col <= col
    # のときも、マーカーのある行の列についてマーカーを跨いで反対側に来ているので、すでに登録済み(再度登録はしない)。
    for marker in problem.markers:
        for row in range(problem.row_size):
            opposite_row = int(2*marker.row - row)
            for col in range(problem.col_size):
                opposite_col = int(2*marker.col - col)
                
                opposite_is_out = (
                    (opposite_row < 0) or (problem.row_size <= opposite_row)
                    or (opposite_col < 0) or (problem.col_size <= opposite_col)
                )
                if opposite_is_out:
                    ip.add_constraint(belong[row,col,marker] == 0)
                else:
                    already_added = (
                        (opposite_row < row)
                        or ((opposite_row == row) and (opposite_col <= col))
                    )
                    if not already_added:
                        ip.add_constraint(belong[row,col,marker] == belong[opposite_row,opposite_col,marker])

    # 親が不要でないマスは、いずれか1つの方向が親になる
    for row in range(problem.row_size):
        for col in range(problem.col_size):
            parent_sum = sum(parent[row,col,direction] for direction in directions)
            if type(parent_sum) != int:
                ip.add_constraint(parent_sum == 1)

    # 親の距離は子よりちょうど1小さい
    # NOTE:
    # distance[row,col]とdistance[parent_row,parent_col]について、
    # a. 親になっているなら 0 <= distance[row,col] - distance[parent_row,parent_col] - 1 <= 0
    # b. 親になっていないなら -∞ <= distance[row,col] - distance[parent_row,parent_col] - 1 <= ∞
    # が成り立つので、十分大きなMに対して
    # -M*(1-parent[row,col,direction]) <= distance[row,col] - distance[parent_row,parent_col] - 1 <= M*(1-parent[row,col,direction])
    # という制約で表現できる。
    M = problem.row_size * problem.col_size
    for row in range(problem.row_size):
        for col in range(problem.col_size):
            for direction in directions:
                if type(parent[row,col,direction]) != int:
                    if direction == 'up':
                        parent_row = row - 1
                        parent_col = col
                    elif direction == 'right':
                        parent_row = row
                        parent_col = col + 1
                    elif direction == 'down':
                        parent_row = row + 1
                        parent_col = col
                    else:
                        parent_row = row
                        parent_col = col - 1
                    dist_expr = distance[row,col] - distance[parent_row,parent_col] - 1
                    ip.add_constraint(dist_expr >= -M * (1-parent[row,col,direction]))
                    ip.add_constraint(dist_expr <= M * (1-parent[row,col,direction]))

    # 親は子と同じマーカーに属する
    # NOTE:
    # directionの方向のマスが親(parent[row,col,direction] == 1)で、
    # 子のマスがmarkerに属する(belong[row,col,marker] == 1)ならば、
    # 親のマスもmarkerに属さなければならない(belong[parent_row,parent_col,marker] == 1)。
    # この論理は以下の制約で表現できる:
    #    parent[row,col,direction] + belong[row,col,marker] - 1 <= belong[parent_row,parent_col,marker]
    # (両方の条件が満たされたときだけ左辺は1になる(それ以外は0以下)ことに注意)
    for row in range(problem.row_size):
        for col in range(problem.col_size):
            for direction in directions:
                if type(parent[row,col,direction]) != int:
                    if direction == 'up':
                        parent_row = row - 1
                        parent_col = col
                    elif direction == 'right':
                        parent_row = row
                        parent_col = col + 1
                    elif direction == 'down':
                        parent_row = row + 1
                        parent_col = col
                    else:
                        parent_row = row
                        parent_col = col - 1
                    for marker in problem.markers:
                        ip.add_constraint(parent[row,col,direction] + belong[row,col,marker] - 1 <= belong[parent_row,parent_col,marker])

    ip.solve(solver=solver)
    
    # 答えの構築
    # 各マスの属するマーカーが入ったndarrayを返す
    answer = np.zeros((problem.row_size, problem.col_size), dtype=np.object)
    for row in range(problem.row_size):
        for col in range(problem.col_size):
            for i, marker in enumerate(problem.markers):
                if belong[row,col,marker].value == 1:
                    answer[row,col] = marker
                    break
    
    return answer

詳細はコメントなどを読んでもらえれば・・・

これでちゃんと連結制約を守って問題を解くことができた。


最後に、自分でも天体ショーの問題を作ってみたので、貼り付けてみる:

f:id:yamaimo0625:20201220002252p:plain

鉛筆片手に解いてもらってもいいし、プログラムで解いてもいいので、楽しんでもらえればと思う。


上記のコードや、問題や解答をPNGで出力するコードは、以下のリポジトリを参照:
※上の問題の答えも入ってるので注意


数理最適化をテーマにした同人誌を書いてるので、よろしければこちらもどうぞ:

今日はここまで!

ペンシルパズル本116 天体ショー1

ペンシルパズル本116 天体ショー1

  • 作者:ニコリ
  • 発売日: 2006/10/10
  • メディア: 単行本

ペンシルパズル本135 天体ショー2

ペンシルパズル本135 天体ショー2

  • 作者:ニコリ
  • 発売日: 2009/02/10
  • メディア: 単行本

未踏ジュニア2020年度最終成果報告会を見てみた。

去年の未踏ジュニアが面白かったので、今年も見てみた。

去年の記事は以下:

感想

去年に引き続き、今年の発表もとても面白かった。 オンラインということでいろいろ大変だったと思うけど、YouTubeTwitterのコメント、質問もちゃんと拾われていて、とてもよかった。

そして、今年もみんなレベルが高い・・・! 技術力もそうだけど、ユーザを思いやる気持ちやプロジェクトをしっかりとやりきる行動力などが本当に凄かった。

どの発表も面白かったんだけど、自分が特に印象に残った発表をいくつか紹介したい。

Mer

多機能電子リコーダー。

これが想像以上に凄かった。

電子リコーダーと聞いて、既存のウィンドシンセと何が違うんだろう?と思ってたんだけど、発表を聞いて納得。

  • 息を吹くだけでなく、吸っても音が出る
  • 管の下側を捻ることでピッチを調整できる

この2つが大きな違いで、とても面白いと思った。 実際に触ってみたい・・・

技術的にも丁寧な作りがされていて、たとえば最初はマイクで入力しようとしてたけど、それだと他の楽器の影響を受けてしまうので、気圧計を使って入力するように変更したとのこと。 さらに、気圧計も吹き込んだ息の湿度で壊れてしまうということがあったので、直接息がかからないように変更したらしい。 よく考えられてるなぁと感心。

発表ではデモも予定されてたようだけど、ちょっとアクシデントがあって残念ながらデモはなしに。 ハードウェア系はこういうことがよくあるから怖い・・・ ぜひともどこかで演奏も聞いてみたいなぁ。

ぶらっしゅとーく

小さい子でも使える筆談アプリ。

小さい頃の体験が動機となって、小さい子やお年寄りでも使える筆談アプリを作ったとのこと。

このアプリを作ったのは小6の子だったんだけど、本当にユーザのことを考えてプロダクトを作っていて、その姿勢がとても素晴らしいと思った。 大人のPdMでも、ここまで真剣にユーザのことを考えてる人はなかなかいないんじゃないかな。

そして、プレゼンがすごく堂々としているのがとても印象的だった。 焦ることなく、ゆっくりとしっかりした声で説明をしていて、これには舌を巻くばかり。 こんなに度胸の据わった小学生がいるのか・・・

spaghetian

リレーで作った自作CPU。

なんだろう、とにかくかっこよかった・・・!

CPUを作るという時点ですでに凄いけど、それを半導体使わずにリレーだけでやるというのが本当に凄かった。 命令セットも自分で考えたらしい。

そして、動作してるときの様子が本当にかっこいい。 カチャカチャとリレーが小気味よく動く音を鳴らしながら、LEDがピカピカと光って計算が進んでいく様子は、最高の一言。 いやー、これはいい・・・

さらにそれを3台繋いで通信までさせてピンポンゲームをやってみせたり、これも凄かった。


ということで、とりあえず3つだけ紹介したけど、他の発表もとても面白かった。

アーカイブも公開されてるので、興味ある人はぜひ見て欲しい。

今日はここまで!