中野智文のブログ

データ・マエショリストのメモ

シンギュラリティは永久機関のようなもの

背景

社員旅行の夜、シンギュラリティについて質問された。 それまであまりよく考えたことがなかったけど、このときに自分の中でかなり整理された。

無限に賢くなる?

シンギュラリティの主張は、

  • AIがもっと賢いAIを作る。
  • これを繰り返す。
  • 最終的に圧倒的に人より賢いAIが作られる。

確かにこれを聞けばすごくAIは賢くなる気がする。

賢さとは何か?

賢さとは何なのか。考えてみる。

  • 計算スピード。すでにコンピュータのほうが早い。でも人間より賢いわけではない。
  • 記憶力。コンピュータの方が多く記憶できるし、早く記憶できる。でも人間より賢いわけではない。

賢さの要素となるものは既に人間より上回っているにも関わらず、賢いとはいえないわけである。

学習の性能とその上限

今シンギュラリティが話題となっている背景に、深層学習のような機械学習のブレイクスルーがあるので、(機械)学習の枠組みで賢さを考える。 (機械)学習の評価尺度に正解率というものがある。特に重要なのが、汎化性能とよばれる未知の問題に対する正解率である。

この未知の問題に対する正解率には実は上限がある。どんなに優れた機械学習手法においてもこの上限を超えることはできない。

例えば、

  • サイコロの目の数の予測

であるが、どのように優れた機械学習の手法を用いても正解率は6分の1を超えることはできない。

無限に賢くなる手法というのが提案されたとしても(事実Boostingは「訓練事例に対しては」無限に正解率が上がる)、アキレスと亀のように上限を超えることはできない。

賢い人が教えたらもっと賢い人になるか?

機械学習は新しいアルゴリズムを生成するものではない。だから、機械学習によって直にもっと賢い機械学習ができるわけではない。 ただし、機械学習のパラメータを効率よくチューニングするような話(メタ学習とよばれる)もないわけではない。 また機械学習アルゴリズムを一つの推論とみなすとき、強化学習のような方法で効率よく学習が出来るようになるかもしれない。 ところが、強化学習やメタ学習による機械学習手法が代表的な一手法として確立してない現状をみると、 この強化学習やメタ学習にブレイクスルーがなければ、将来的にもその可能性は低いと考えられる。

そもそも賢い人が教えたらもっと賢い人になるという前提自体が根拠が不明である。賢い人自身がもっと賢くなることはあっても。

計算複雑性理論は越えられない

また例えそうだったとしても、計算複雑性理論の上限を超えることは出来ないだろう。もちろん量子コンピュータなどの新しい可能性もあるが、賢さのみでハードウェアを構築することは出来ないし、 人間もそうだが、どんなに賢くても無限のリソースが手に入るわけでもない。物理的な制約まで賢さが克服する根拠はない。

結局、シンギュラリティは計算機科学における永久機関

結局、シンギュラリティに到達するには様々な制約があり、それが克服される技術的・科学的な根拠はまだない。 よって、個人的には永久機関のようなものと考えている。

json ぽいデータを引き抜くクソクエリ

背景

json に対応していない mysql 5.7 未満のシステムに、json で値が格納されている。

対応

クソにはクソで。 LOCATE と、SUBSTRING を駆使し、抽出を試みる。 ここに書かれているは方法は、「一般的に」JSONをパースして抽出する方法でなく、利用範囲が限られるアドホックな方法であること(だからクソ)に注意されたい。

戦略

SUBSTR は、

SUBSTRING(文字列, 開始位置, 文字数)

で開始位置から文字数分、文字列を抽出できる。よって、 抽出したい文字列の開始位置抽出したい文字列の文字数 が分かればいいことになる。

多くの場合は、抽出対象となる文字列は ..., <key>: <value>, ... みたいな形式になっており、抽出したい文字列自体(ここでは、<value> とする)は不定で、<key> をヒントに、「<key> の後のコロンの後に続く文字列」であることが多い。ここでは、これを例として話をすすめる。

抽出したい文字列の開始位置

とりあえず、 の開始位置であるが、

LOCATE("<key>:", 非抽出カラム名)

で取れる。よってこれに <key>: の長さを追加すれば、「の後のコロンの後に続く文字列」の開始位置が取れることなる。 この長さは、 丁寧にSQLLENGTH("<key>:") と計算してもいいし、自分で数えても良い。 空白の有無はそこはデータ依存によるが、後で TRIM を使うのであれば、空白が含まれていても問題ないだろう。

よって抽出したい文字列の開始位置は、

LOCATE("<key>:", 被抽出カラム名) + LENGTH("<key>:")

となる。

抽出したい文字列の文字数

抽出したい文字列の文字数は次のように求められる。

抽出したい文字列の文字数 = (抽出したい文字列の先頭からの文字列における)抽出したい文字列の次の文字列の位置 -1

である。ここで、 抽出したい文字列の先頭からの文字列 は次のようして求まる。

SUBSTRING(被抽出カラム名, LOCATE("<key>:", 被抽出カラム名) + LENGTH("<key>:")) 

よって、LOCATEを使い、 (抽出したい文字列の先頭からの文字列における)抽出したい文字列の次の文字列の位置 は、

LOCATE(",", SUBSTRING(被抽出カラム名, LOCATE("<key>:", 被抽出カラム名) + LENGTH("<key>:")))

となる。

まとめると

最後にすべてまとめると、

SUBSTRING(被抽出カラム名, 抽出したい文字列の開始位置, 抽出したい文字列の文字数)

に、抽出したい文字列の開始位置 を代入すると、

SUBSTRING(被抽出カラム名, LOCATE("<key>:", 被抽出カラム名) + LENGTH("<key>:"), 抽出したい文字列の文字数)

となり、ここに、抽出したい文字列の文字数 を代入すると、

SUBSTRING(被抽出カラム名, LOCATE("<key>:", 被抽出カラム名) + LENGTH("<key>:"), LOCATE(",", SUBSTRING(被抽出カラム名, LOCATE("<key>:", 被抽出カラム名) + LENGTH("<key>:")))-1)

となる。クソクエリの完成である。

まとめ

テンプレートを作った。

SUBSTRING(column,LOCATE(prefix, column)+LENGTH(prefix),LOCATE(suffix,SUBSTRING(column,LOCATE(prefix, column)+LENGTH(prefix)))-1)

ここで、column: 被抽出カラム名、prefix: 抽出したい文字列の前に来る文字列、suffix: 抽出したい文字列の次に来る文字列 である。

ただし、これは抽出したい文字列の次に来る文字列が固定されている場合に限定されるクエリである。最後にカンマがあったりなかったりするとうまくいかない。クソクエリだからである。

luigi で パラメータで与えられた日付をtimedelta を使って増減させたい

背景

luigi にて、パラメータで与えられた日付に対して、固定日分前(例えば一週間など)を求めたい。

from datetime import timedelta
import luigi


class MyTask(luigi.Task):
    date = luigi.DateParameter()
    start_date = date - timedelta(days=7)

と書くと、次のようなエラー。

unsupported operand type(s) for -: 'DateParameter' and 'datetime.timedelta'

確かに、date はDateParameterなので、エラーの通りなのだが。

stackoverflow.com

での、回答者のコメントでは、 “DateParameter returns a value that is a python date. ” といってるので、半分信じてしまった。 ちなみにこのQAは役立たない。なぜならデフォルト値の計算をしたいわけではないから。

クラス変数とインスタンス変数の違い

関数の中の、self.date はクラス変数と思っていたが、間違いだ。これはインスタンス変数だ。

厄介なことに、 self.date の表記は python 的にはクラス変数でもインスタンス変数にもなるらしい。 インスタンス変数として「初期化」すれば、クラス変数は隠蔽され以後インスタンス変数として扱われる。 ただ参照するだけならば、クラス変数として扱われる。

d.hatena.ne.jp

結局、self.dateインスタンス変数ということは、親のクラスのインスタンス初期化の処理などで、クラス変数の設定通りの動作が行われ、self.date というインスタンス変数に値がセットされたのだろう。

解決

from datetime import timedelta
import luigi


class MyTask(luigi.Task):
    date = luigi.DateParameter()

    def requires(self):
        start_date = self.date - timedelta(days=7)    

もし、固定の日でなく、パラメータで与えたいのであれば、TimeDeltaParameter を使おう。

http://luigi.readthedocs.io/en/stable/api/luigi.parameter.html#luigi.parameter.TimeDeltaParameter

luigi の RangeDaily をコードで使う

背景

範囲日の繰り返し DateIntervalParameter

指定した範囲の日の処理を指定したい場合は、チュートリアルには次のようなコード例がある。

http://luigi.readthedocs.io/en/stable/example_top_artists.html#step-1b-running-this-in-hadoop

    def requires(self):
        return [StreamsHdfs(date) for date in self.date_interval]

この date_interval はこれ以降説明など出てこない。もっと細かい仕様を調べたい場合は、 DateIntervalParameter のドキュメントを読む必要がある。

http://luigi.readthedocs.io/en/stable/api/luigi.parameter.html#luigi.parameter.DateIntervalParameter

何かちょっとドキュメントとして読みにくい。また DateIntervalParameter はタスクのパラメータであるので、 本当にほしいのは、渡された別のパラメータから日付の範囲を生成する DateInterval のような気がするのだが、

http://luigi.readthedocs.io/en/stable/api/luigi.date_interval.html#luigi.date_interval.DateInterval

ドキュメントは徐々に何も書かれていなくなってくる。

範囲日の繰り返し RangeDaily

ところで、luigi コマンド上で上記のような範囲指定を行いたい場合コードが書けないので、タスクのコードをDateParameterから、DateIntervalParameterに変えるとか、 それを呼び出す範囲日指定のラッパータスクを作成しなければならない。 そこでコマンドレベルでも特にコードを書かずに上記のような範囲の実行を行う一般的なラッパータスク RangeDaily が用意されている。

Luigi Patterns — Luigi 2.6.2 documentation

luigi --module all_reports RangeDaily --of AllReportsV2 --start 2014-10-31 --stop 2014-12-25

本題

RangeDaily はコードでも使えるよ、という話。RangeDaily であればドキュメントは割とあるのと、広範囲を誤って指定しても実行されないような仕組みがあるので、 そういった意味で比較的安全に範囲日を指定できるというメリットがある。

コード例

import luigi
from luigi.tools.range import RangeDaily
from my_sub_task import MySubTask

class MyTask(luigi.Task):
    date = luigi.DateParameter()
    start_date = luigi.DateParameter()
    opt1 = luigi.Parameter()
    opt2 = luigi.Parameter()

    def requires(self):
        return [RangeDaily(of=MySubTask,
                           start=self.start_date,
                           stop=self.date,
                           task_limit=100,
                           days_back=200,
                           of_params={"opt1": self.opt1,
                                      "opt2": self.opt2})]

関数の outputrun は必要。

task_limit days_back は安全のためのオプションで、それぞれデフォルトでは 50 と100 に設定されている。 詳しくは次で確認できる。

http://luigi.readthedocs.io/en/stable/api/luigi.tools.range.html#luigi.tools.range.RangeBase

http://luigi.readthedocs.io/en/stable/api/luigi.tools.range.html#luigi.tools.range.RangeDailyBase

of_paramsdate 以外のパラメータを渡すための、dict。 注意しなくてはいけないのは、 date型などを直接この op_params の value に入れると、JSON 化できないというエラーとなる。 文字列に変換してから入れる必要があるが、文字列に変換すると今度は DateParameter の方が日付型に変換してくれないというオチ。 (そんなときは無理してこの RagenDaily を使う必要はない)

コマンドライン

コマンドラインで呼び出す場合は次のようになる。

luigi --module my_task MyTask --start-date 2017-07-22  --date 2017-07-25 --opt1=aaa opt2=bbb --workers=5

--workers のオプションはここに来る。(RangeDailyのオプションではないので)。 また 今回のコードにする話に限ったことではないが、--stop はその日を含まないので注意。

(おまけ)その日を含むコードを実行

require にその日を追加する方法。

    def requires(self):
        return [MySubTask(date=self.date,
                          opt1=self.opt1,
                          opt2=self.opt2),
                RangeDaily(of=MySubTask,
                           start=self.start_date,
                           stop=self.date,
                           task_limit=100,
                           days_back=200,
                           of_params={"opt1": self.opt1,
                                      "opt2": self.opt2})]

stop に一日後を指定する方法。

timedelta を、 import する必要がある。

from datetime import timedelta # (←追加)
...
(中略)
...

    def requires(self):
        stop_date = self.date + timedelta(days=1)   
        return [RangeDaily(of=MySubTask,
                           start=self.start_date,
                           stop=stop_date,
                           task_limit=100,
                           days_back=200,
                           of_params={"opt1": self.opt1,
                                      "opt2": self.opt2})]

Julia の GaussianProcesses を jupyter notebook で表示する

背景

そろそろ jupyter notebook を使いたくなってきた。

前回、Gaussian Processes のライブラリを入れるところまではできたので(下記参照) nakano-tomofumi.hatenablog.com

今回は、jupyter notebook 上でグラフを表示するところまでやってみる。

IJulia インストー

申し訳ない。既にインストール済みだったのか、さっくりできた。

julia> Pkg.add("IJulia")
julia> Pkg.update()
julia> using IJulia
julia> notebook()

jupyter 上で GaussianProcesses ライブラリを使う。

以下は jupyter notebook 上での実行の話。

using GaussianProcesses

INFO: Recompiling stale cache file /Users/nakanotomofumi/.julia/lib/v0.5/Optim.ji for module Optim.

LoadError: ArgumentError: Module Klara not found in current path.
Run `Pkg.add("Klara")` to install the Klara package.
while loading /Users/nakanotomofumi/.julia/v0.5/GaussianProcesses/src/GaussianProcesses.jl, in expression starting on line 2

もしかしてパスの問題かもしれないが、素直に入れてみる。

Pkg.add("Klara")

問題なく入った。

本家のReadme.md が変更されている。

よく読むと、jupyter notebook へ移ったとある。つい最近変更したようだ。 なるほど、前回の例は、何が実行コマンドで何が出力か分かりにくかった。 しかしjupyter notebookへのリンクをクリックしても、当該ファイルへは飛ばない…

どうやら、下記らしい。

github.com

あとはこの通りに実行して…。

pyplot の場所でエラーが…。あれ?本家の jupyter notebookもエラーだし…。

エラーでなくなってからマージして欲しい…。

BigQuery のスキーマを bq コマンドで使う形式で表示する jq

背景

ある日の空テーブルを作成しようとしたが、スキーマの文字列が必要になった。手作業だとミスが起こるから、コマンドが欲しい。

jq コマンド

スキーマ文字列を取得

$ bq show --format prettyjson <既存のBQのテーブル名> | jq -r '.schema[] | map(.name+":"+.type) | join(",")

一気に空テーブル作成

$ bq mk --schema `bq show --format prettyjson <既存のBQのテーブル名> | jq -r '.schema[] | map(.name+":"+.type) | join(",")'` -t <空のBQのテーブル名>

Julia の GaussianProcesses のライブラリを入れようとする (2)

前回はこちら

nakano-tomofumi.hatenablog.com

Optim の仕様変更への対応

問題のエラーは、次のようなもの

julia> optimize!(gp)
ERROR: MethodError: no method matching set_params!(::GaussianProcesses.GP, ::Float64; noise=true, mean=true, kern=true)
Closest candidates are:
  set_params!(::GaussianProcesses.GP, ::Array{Float64,1}; noise, mean, kern) at /Users/xxxxx/.julia/v0.5/GaussianProcesses/src/GP.jl:275
  set_params!{K<:GaussianProcesses.Kernel}(::GaussianProcesses.Masked{K<:GaussianProcesses.Kernel}, ::Any) at /Users/xxxxxx/.julia/v0.5/GaussianProcesses/src/kernels/masked_kernel.jl:55 got unsupported keyword arguments "noise", "mean", "kern"

これに対して、既に修正が行われている模様:

github.com

そして実行。

julia> Pkg.checkout("GaussianProcesses")
INFO: Checking out GaussianProcesses master...
INFO: Pulling GaussianProcesses latest master...
INFO: No packages to install, update or remove

おや?変化なし。

パッケージが最新版を見ていない模様。

julia> Pkg.status()
4 required packages:
 - GaussianProcesses             0.4.0+             master

statusを見ると、master を見ていることになっている。再コンパイルが必要?

最新のブランチを反映させる方法を知らないため、一旦 exit() し、再び julia を起動し、using で呼び出し。

次は、plot(gp) でエラーだ。

なので、 一旦、Pkg.free("GaussianProcesses") で元に戻す。

そして、再び exit して、その後、Pkg.checkout("GaussianProcesses") を実行。

plot(gp) でエラー出ず。

f:id:nakano-tomofumi:20170710191518p:plain

そして、

julia> optimize!(gp)
Results of Optimization Algorithm
 * Algorithm: Conjugate Gradient
 * Starting Point: [-1.0,0.0,0.0,0.0]
 * Minimizer: [-3.506712993556627,-0.080606913136668, ...]
 * Minimum: -3.600592e+00
 * Iterations: 19
 * Convergence: false
   * |x - x'| < 1.0e-32: false
   * |f(x) - f(x')| / |f(x)| < 1.0e-32: false
   * |g(x)| < 1.0e-08: false
   * f(x) > f(x'): true
   * Reached Maximum Number of Iterations: false
 * Objective Function Calls: 69
 * Gradient Calls: 50

キタ━━━━(゚∀゚)━━━━!!

f:id:nakano-tomofumi:20170710191527p:plain

まとめ

一応、Juliaのライブラリ GaussianProcesses で超パラメータの最適化は出来た。 しかし、他のライブラリ(Optim)の勝手なバージョンアップ等もあって、うまく動くことは保証されない。 今後も勝手なバージョンアップがありそうだから、ビジネス用途には全く向いていない。あくまで遊び(教育用途)レベルにとどめておくべき。