『ゼロから作るDeep Learning』をScalaで実装する(その1) - Breezeで行列演算

Deep Learningを業務でがっつり使うことは(少なくとも当分は)ないんだけど、さすがに知らないでいろいろ語るのはマズいので、巷で良書と評判の『ゼロから作るDeep Learning』をやろうと思います。

とはいえ今更Pythonでやるのもつまらないので、せっかくだから新しく覚える言語でやろう、ということでScalaで書くことに。

本書の目標とするところは、ゼロからディープラーニングを実装することでした。そのため、外部のライブラリは極力使用しないというのが方針ですが、次の2つのライブラリは例外として用いることにします。ひとつはNumpy、もうひとつはMatplotlibというライブラリです。 (p.2-3)

Scalaで可視化やるつもりはないのでmatplotlibのところは飛ばせばいいんですが、numpy相当の行列演算をスクラッチで書くのはつらいのでScalaの行列演算(以外もあるけど)ライブラリであるところのBreezeを使うことにします。

以下、1章のnumpyの部分をBreezeで書き直します。

1.5.0 インストール

とりあえずREPLを使います。REPLで外部ライブラリ使うにはこうすればいいらしい。

$ sbt
set libraryDependencies += "org.scalanlp" % "breeze_2.11" % "0.12"
set resolvers += "Sonatype Releases" at "https://oss.sonatype.org/content/repositories/releases/"
set scalaVersion := "2.11.6"
console

https://github.com/scalanlp/breeze/wiki#fast-installation

1.5.1 インポート

scala> import breeze.linalg._

1.5.2 配列(ベクトル)の生成

scala> val x = DenseVector(1.0, 2.0, 3.0)
scala> println(x)
DenseVector(1.0, 2.0, 3.0)

わざわざDenseと明示してるってことはSparseもあるに違いない。

1.5.3 算術演算

scala> val x = DenseVector(1.0, 2.0, 3.0)
scala> val y = DenseVector(2.0, 4.0, 6.0)
scala> x + y
res30: breeze.linalg.DenseVector[Double] = DenseVector(3.0, 6.0, 9.0)
scala> x - y
res31: breeze.linalg.DenseVector[Double] = DenseVector(-1.0, -2.0, -3.0)
scala> x :* y
res32: breeze.linalg.DenseVector[Double] = DenseVector(2.0, 8.0, 18.0)
scala> x :/ y
res33: breeze.linalg.DenseVector[Double] = DenseVector(0.5, 0.5, 0.5)
scala> x / 2.0
res34: breeze.linalg.DenseVector[Double] = DenseVector(0.5, 1.0, 1.5)

要素ごとの操作については演算子を変えてますね(:*:/)。そもそもベクトル同士の(内積ではない)積とか商とかってなんやねんって話なのでこういう方針は好き。

1.5.4 N次元配列

scala> val A = DenseMatrix((1, 2), (3, 4))
scala> println(A)
1  2  
3  4  
scala> (A.rows, A.cols)  // A.shapeがやりたい
res40: (Int, Int) = (2,2)
scala> A.getClass  // A.dtypeがやりたい
res41: Class[_ <: breeze.linalg.DenseMatrix[Int]] = class breeze.linalg.DenseMatrix$mcI$sp

numpyのA.shapeA.dtypeに直接相当するメソッドはない模様。

scala> val B = DenseMatrix((3, 0), (0, 6))
scala> A + B
res42: breeze.linalg.DenseMatrix[Int] =
4  2
3  10
scala> A :* B
res44: breeze.linalg.DenseMatrix[Int] =
3  0
0  24

numpyのA * B(要素ごとの積)に相当するのはA :* Bということに注意。

A * Bすると普通に行列同士の積(numpyでいうところのA.dot(B))になりnumpyよりこっちの方が明らかに正しいだろ感。

scala> println(A)
1  2  
3  4  
scala> A * 10
res46: breeze.linalg.DenseMatrix[Int] =
10  20
30  40

1.5.5 ブロードキャスト

scala> val A = DenseMatrix((1, 2), (3, 4))
scala> val B = DenseVector(10, 20)
scala> A(*, ::) :* B
res49: breeze.linalg.DenseMatrix[Int] =
10  40
30  80

Breezeではブロードキャストは暗黙には行われません。「行方向のブロードキャストである」ことを明示するためにA(*, ::)とします。

他にも、BはMatrixじゃなくてVectorとして定義する必要があったり、最終的にはベクトルとベクトルの要素ごとの積になるので:*にする必要があったりといろいろ注意が必要。

1.5.6 要素へのアクセス

scala> val X = DenseMatrix((51, 55), (14, 19), (0, 4))
scala> println(X)
51  55  
14  19  
0   4   
scala> X(0)
<console>:12: error: could not find implicit value for parameter canSlice: breeze.linalg.support.CanSlice[breeze.linalg.DenseMatrix[Int],Int,Result]
              X(0)
               ^
scala> X(0, ::)
res58: breeze.linalg.Transpose[breeze.linalg.DenseVector[Int]] = Transpose(DenseVector(51, 55))
scala> X(0, 1)
res59: Int = 55

X(0)はできない模様。

scala> for (row <- X) println(row)
<console>:12: error: value foreach is not a member of breeze.linalg.DenseMatrix[Int]
              for (row <- X) println(row)
                          ^
scala> for (i <- 0 until X.rows) println(X(i, ::))
Transpose(DenseVector(51, 55))
Transpose(DenseVector(14, 19))
Transpose(DenseVector(0, 4))

Breezeではベクトルはすべて縦ベクトルなので横ベクトルはTranspose(DenseVector(51, 55))のように表現される。

scala> val X1 = X.toDenseVector
scala> println(X1)
DenseVector(51, 14, 0, 55, 19, 4)
scala> val X1 = X.t.toDenseVector
scala> println(X1)
DenseVector(51, 55, 14, 19, 0, 4)
scala> X1(0, 2, 4).toDenseVector  // X1(0, 2, 4)の返り値はbreeze.linalg.SliceVector型
res11: breeze.linalg.DenseVector[Int] = DenseVector(51, 14, 0)

toDenseVectorでは列方向にベクトル化される。なのでnumpyと同じように行方向でやりたかったらX.tで転置しておけばよい。

scala> DenseVector(X1.toArray.filter(_ > 15))
res23: breeze.linalg.DenseVector[Int] = DenseVector(51, 55, 19)

Booleanを使った要素の抜き出しはできなそう。なので一回Arrayにしてfilterして(何故かDenseVectorにはfilterメソッドがない)またDenseVectorに戻せばよい(これが効率のいいやり方かどうかは知らない)。

以上。次はこれを使ってパーセプトロンを実装する。

生存時間解析のチュートリアル論文を読む(その3)

3. SIMPLE DESCRIPTION OF OUTCOMES AT A FIXED TIME INTERVAL

  • 一番わかりやすいやり方は、あるイベントの後、ある一定期間の間に結果が「起こった/起こらなかった」というものだ
    • 例えば、「手術後の早期死亡率」みないなのがわかりやすい

3.1. Analysis specification

3.1.1. Inclusion criteria

  • こうした分析の対象者として含めるには、次のような条件が必要だ
    • 患者が、該当するイベント(手術とか)を経験していること
    • 一定期間の間追跡されていること
  • 本文の例だと、少なくとも1年はfollow-upされていてる、とか、手術を受けている、とかだね

3.1.2. Outcomes (also known as events, responses or dependent variables)

  • 死だったり、あるいはdefinitive surgeryのような中間的なイベントだったり
  • 本文の例だと、死とか、発病後1年以内の死とか

3.2. Worked example: proportion dying within one year of presentation

  • 具体例として、複雑性肺動脈閉鎖の発病から1年以内に死んだ人の割合を見てみよう
  • p = (死んだ患者数 / 総患者数) は0から1の値を取るよ
  • オッズで表現すると p / (1 - p) = (死んだ患者数 / 生き残った患者数)
    • これは見慣れない表現かも知れないけど、オッズは後で説明するように、複数の説明変数を同時に扱う時の基礎になるんだ
  • この例だと、真の死亡率の95%信頼区間は標準正規分布への近似で p ± 1.96√{p(1-p)/n で計算できる
    • もっとも、これだけnが小さい時には他の推定法の方が適切だけどね

3.3. Caveats and inferences

  • こうしたシンプルな分析結果を一般化できるかどうかは、コホートがどれだけ母集団(?)を代表しているかにかかってるよ
    • 例えば、こんな素朴なやり方で出した死亡率を、後で話すような調整手法も使わずに病院間の比較に使ったりするのはだめよ

生存時間解析のチュートリアル論文を読む(その2)

生存時間解析のチュートリアル論文を読む(その1) - ふゆみけ〜おかわり〜 の続き

2. DATA

  • 臨床データの分析で一番大事なのでデータの integrity 、例えばデータの質、完全性、妥当性
    • これらが備わってないデータにどんなに高度な統計手法を使っても無駄だよね

2.1. General problems of bias

  • ここまで挙げた観察データ分析特有の問題に加えて、ランダム化実験とも共通したバイアスの問題があるよ
    • Bias which prejudices external generalizability
      • 入手可能な患者のサブセットから導き出された結論を、同じ条件の患者すべてに一般化したいけど、それって本当にできるんだっけ?
      • 典型的ではない患者群から得た結論を一般化したかったら、その典型的じゃなくさせている要素を分析の中でちゃんと考慮しないといけないよね
    • Ascertainment bias
      • 患者のある状態に関する情報の可用性が、その状態に依存する時に起こるよ
      • 「患者が死んだ」という情報が「生きている」という情報より報告されやすいとしたら、その追加情報はバイアスの原因になるよ

2.2. Illustrative data

  • 複雑性肺動脈閉鎖?の218人の患者から30人を抜き出したデータを使うよ
  • この病気の特徴としては、先天性の病気だけど medical attention が現れる年齢にばらつきがあることなんだ
  • 30人を抜き出した基準は説明に都合がいいからという理由なので、このデータから出た結論を一般化しちゃだめよ
  • (データの詳細は実論文を参照)

生存時間解析のチュートリアル論文を読む(その1)

生存時間解析なのか生存時間分析なのか、はたまたサバイバル分析なのか。 いまいち呼称が統一されていないSurvival Analysis。

医療統計や計量経済ではよく使われるはずなのですが、 日本語で読める定番の書籍があまりないことも呼称の乱立の原因なのかも知れません。

お仕事の関係で入門したいのですが、松戸のジュンク堂に行っても生存時間解析の本が一冊もない。

こりゃだめだ。ということでググってたら見つけたチュートリアル論文(英語)に手を付けることにします。

TUTORIAL IN BIOSTATISTICS - SURVIVAL ANALYSIS IN OBSERVATIONAL STUDIES

http://www.rni.helsinki.fi/~kja/event2010/Tutorial.pdf

要約

1. INTRODUCTION

1.1. Background

  • データベースを使った研究が医療分野でも増えてきたよ

1.2. Structure of this paper

  • 従来から主流だったランダム化比較実験と、観察データに基づく分析には重要な違いがあるんだ
    • ランダム化比較実験
      • 実験群の間で説明変数(観察不可能なものも含む)がバランスしているから、非説明変数の違いはほぼ全部介入の影響と考えていいよ
    • 観察データに基づく分析
      • 対象の選び方がランダムじゃないし、介入のタイミングもバラバラだから、介入の被説明変数への影響がtentativeだよ
  • ランダム化比較実験ができず、観察データに基づく分析に頼らざるを得ないような場面はある
  • そんな時には、観察データの質にかなり厳格な注意が必要だし、より洗練された統計分析が求められるんだ
  • この論文では、観察データからよりvalidな結論を導き出すための研究デザインや分析手法について説明するよ
  • 詳しくは第2節で説明するけど(たぶん)、2つのストーリーに沿って説明するよ
    • 1つ目は fixed interval でのイベント発生を対象とするものだ
      • 例えば「手術から1年以内の死」みたいに
    • 2つ目は、期間をまたいだイベント発生を分析するもの
      • 例えば「20歳になるまでの死亡率のパターン」
  • 重要な概念は2つ
    • 第4節で導入する late entry
    • 第9節で考える time-dependent variables
  • 詳しくはそれぞれの節で説明して、第10節ではこれらの概念を組合せて分析する方法を教えるよ
  • 最後にはこうした分析から導く結論には、どうしてもtentativeな性質があることを強調して終わるよ

所感

の前に、何故口語調で訳したんだ俺

  • 「実験できないから観察データに頼らざるを得ないんだけど、そうするとデータの統計的な扱いにはかなり気をつけて、工夫した手法を使わないといけないよね」という問題設定は、自然科学に対する計量経済学や計量社会学のアプローチに近い(というかまんま)
    • 医療統計全然知らなかったんだけど、自分のバックグランド(社会学)と実は近いのかも
  • 英国人の英語って読みづらくないですかね…ちょっと難しいし一文がやたら長いし…

第2節も近いうちに上げます。

私の情報収集術

諸事情により長い夏休みだったので、情報収集のやり方をちょびっと洗練してみた。

f:id:fuyumi3:20150816224631p:plain

目的

普段TwitterのTLを追ってるだけでも、Web業界/データ分析業界の動向はだいたい掴める。

それとはてブのトップ見てればだいたいの情報は入ってくるんだけど、いずれにせよ読みっぱなしだと「あれ、どっかで読んだ気がするけど思い出せない…」になりそうなので、役に立つ/面白いと思った情報はストックしておきたい。

プラスして、仕事に関係あること以外でも、政治経済社会スポーツエンタメその他広くアンテナ張っときたいよね。新聞とか取ってないし。

最後におまけで、読んだページをTwitterに流しておけばひょっとすると人の役に立つかも知れない。

情報収集源

  1. Presso
    • はてな村民なので、キュレーション系ではPressoが一番しっくりくる。
  2. RSS
    • 固定で追っかけたいブログやサイトはRSS登録

情報の集約

PressoとRSSリーダfeedly使ってる)個別で読んでると情報が分散されてしまう。やはり情報は一元管理したいよね。

ってことでPocketを使うことに。

Pressoの記事リスト画面にはワンタップで「あとで読む」タグをつけてブクマできる機能がるので、iftttを使ってこれをPocketに流すように設定。

feedlyiPhoneアプリには直接Pocketに送る機能があるので、これを使う。

こうすることで、電車の中とかでPressoとfeedlyのアプリを開いて、興味ある記事をぽちぽちっとするとPocketに保存できるように。

読む

Pocketで読む。ただひたすら読む。

出先でスマホで読んでもいいし、家でPCやタブレットでじっくり読んでもいい。クラウド万歳。

シェア()する

読んでこれ面白いなーと思ったら、Pocket上でスターを付ける。ここでまたiftttを使って、スターをつけた記事をTwitterに流すようにしておく。

これで人力キュレーションメディアの出来上がり。

まとめると冒頭の画像になる。

問題点

いろいろある

  • あとで読む」でブクマした記事がPocketに反映されるのに時間がかかる
  • あとで読む」だけのブクマが大量発生する。コメント付きのブクマがしづらい
  • Twitterに流れるのはタイトルとリンクだけで、自分のコメントとかはつけられない

問題はあるが、現状それなりに機能してるのでしばらくはこのままでいこうと思う。

Neo4jをはじめよう - (2) データを投入する

Neo4jをはじめよう - (1) インストール - ふゆみけ〜おかわり〜

の続き

前回の最後で「Twitterのデータを投入!」とか息巻いてましたが、API制限キツくて色々めんどくさいのでここはSNA(社会ネットワーク分析)用のサンプルデータを使うことにします。

「SNAって何やねん」って人はこのエントリでも見ればいいんじゃないでしょうか。

d.hatena.ne.jp

使用するデータ

SNABook/chapter3 at master · maksim2042/SNABook · GitHub

上述した勉強会で使った『オープンソースで学ぶ社会ネットワーク分析』という本の3章のデータを使います。

LiveJournal.comとかいうSNS(?)*1から、とあるユーザーの友人、友人の友人、友人の友人の友人……を深さ3で取得したデータ。

node数は87,250、edge数は143,120と、遊び用のデータとしてはなかなかいい感じな規模感。

サンプルデータをNeo4に投入

csvに変換

では、このデータセットをneo4jに投入してみましょう。サンプルデータはPajekというネットワーク分析ツールで使うデータ形式で保存されており、このままではロードできません。

neo4jで外部ソースからインポートするにはcsvファイルを使います。

12.8. Importing CSV files with Cypher - - The Neo4j Manual v2.2.1

そこで、今回はnetworkxというPythonのネットワーク分析用ライブラリを使って、Pajekのデータ形式をneo4jが読み込めるcsv形式に変換してやります。

gistcdd6b2ee69af071e76d9

nodeとedgeで別ファイルで、結果はこんな感じに。

nodes.csv

id,name
17872,zhuravl
19609,kir_a_m
51320,vizu_buduwee
21481,na_stasja
66039,aernmru
...

edges.csv

start,end,weight
zhuravl,milavalyacat,1.0
zhuravl,a_u,1.0
kir_a_m,cheger,1.0
vizu_buduwee,johnjthunderer,1.0
...

cypherで読み込み

あとはこいつをcypherを使って読み込みます。

前回のエントリ通りvagrant仮想マシンを立ち上げていれば、Vagrantfileと同じディレクトリにこのcsvファイルを設置すれば仮想マシン上からも/vagrantでアクセス可能ですね。

ブラウザ経由でアクセスして次のコマンドを叩いてやりましょう。

LOAD CSV WITH HEADERS FROM "file:///vagrant/nodes.csv" AS csvLine
CREATE (n:Person {id: toInt(csvLine.id), name: csvLine.name})
LOAD CSV WITH HEADERS FROM "file:///vagrant/edges.csv" AS csvLine
MATCH (m:Person), (n:Person)
WHERE m.name = csvLine.start AND n.name = csvLine.end
CREATE (m)-[r:FRIEND {weight: toFloat(csvLine.weight)}]->(n)

データのインポートには結構メモリを食うようで、vagrantのデフォルトで割り当てられているメモリ(300Mぐらいだったか?)だと「ヒープが溢れたよ」ってエラーになって成功しません。カッとなってVagrantfileを書き換えて2G割り当てたら成功しました。

正常に読み込めたか確認する

MATCH (n) RETURN COUNT(n)

f:id:fuyumi3:20150423175015p:plain

MATCH ()-[r]->() RETURN COUNT(r)

f:id:fuyumi3:20150423175126p:plain

せっかくだから次数中心性を出してみる

MATCH (node)-[r:FRIEND]-() RETURN node, count(r) AS degree ORDER BY degree DESC LIMIT 100

f:id:fuyumi3:20150423175758p:plain

次回

次数中心性だけじゃなくて近接中心性や媒介中心性も出してみる

*1:Wikipediaでは「インターネットユーザーがブログ、記事(ジャーナル)、日記を掲載し保持できる仮想共同体の一種」と説明されており、つまりどういうことだってばよ

Neo4jをはじめよう - (1) インストール

最近グラフDBが流行の気配を見せているような気がします。ということでNeo4jで遊んでみようと思いますが、まずは環境構築からですね。

今どき遊び用とは言えローカルマシンに素でDB入れるのは抵抗があるので、VagrantでUbuntu14の仮想マシンを作ってそこに入れます。

仮想マシンの構築

Vagrant自体のセットアップは省略します。Vagrantfileは最低限の設定のみで、こんな感じ。

# -*- mode: ruby -*-
# vi: set ft=ruby :

VAGRANTFILE_API_VERSION = "2"

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  config.vm.box = "ubuntu/trusty64"
  config.vm.network :private_network, ip:"192.168.33.11"
end

ネットワーク経由でNeo4jにアクセスするので、private_networkを設定する必要があります。アドレスは適当に。

Vagrantfileが書けたらうpしましょう。

$ vagrant up

Neo4jのインストール

まずはVMに入る。

$ vagrant ssh

ここから先はVM内で叩くコマンド。

# レポジトリを追加
$ wget -O - http://debian.neo4j.org/neotechnology.gpg.key | sudo apt-key add -
$ sudo sh -c "echo 'deb http://debian.neo4j.org/repo stable/' > /etc/apt/sources.list.d/neo4j.list"

# パッケージリストを更新してインストール
$ sudo apt-get update
$ sudo apt-get install neo4j

ホストOSからもアクセスできるように設定変更

これでNeo4jはインストールできましたが、デフォルトではlocalhostからのアクセスしか受け付けません。

VMではなくホストOSからブラウザや各言語のクライアントライブラリからアクセスするにはリモートからの接続を許可する必要があります。

当たり前ですが本番で何も考えずにリモートアクセスを許可すると大変なことになります。あくまでVMに入れて遊ぶ用の設定ということで。

$ sudo vim /etc/neo4j/neo4j-server.properties

# 下記の行をコメントアウト
org.neo4j.server.webserver.address=0.0.0.0

$ sudo service neo4j-service restart

アクセスできるか確認する

これで、VMに入らずとも手許のブラウザから接続できるようになりました。 お使いのブラウザを開いて http://192.168.33.11:7474/ と叩いてみましょう。

こんな画面が出れば成功です。

f:id:fuyumi3:20150324004650p:plain

次回

PythonクライアントからNeo4jにアクセスし、Twitterのデータを投入してみます。