(本稿はKLab Advent Calendar 2016 の1日目の記事になります)
今年もアドベントカレンダーの季節がやって来ました。お祭り騒ぎと年末進行の勢いで、普段は書かないような変なネタも含めてKLabエンジニア陣が多彩なエントリーをお届けします。
最初は昨年に引き続きKLabアドベントカレンダーの旗振り役をさせていただいてますoho-sからです。 よろしくお願いします。
皆さんは不老不死に興味はありますか? 古今東西、人々は伝説に宗教にそして現実にそれを追い求めて来ました。 でも、そもそも「生きている」ってどういうことなのでしょう。 現代の生物学においても、その定義は非常に曖昧です。 特に急進的な主張としては、一個体の生物を構成する物理化学生物的な現象を全て完全に正確に模倣したら、それは生きているということではないか、というものです。 そして、その主張にのっとり、近年のSFで科学的に現実的? な方法としてよく見られるのが、脳のコンピュータへのアップロードです。
これは、人間の脳を構成するあらゆる情報をデータ化したうえで、ものすごい性能のコンピュータにシミュレーションさせることで、人間がコンピュータ上で生きていけるのではないか、という考え方です。 そうすれば、コンピュータ上のあらゆるデータと同じように、バックアップもとれるし、コピーもできるし、電源がある限り動き続けられるし、これ、不老不死じゃん! ということです。
もちろん、人間の神経細胞は140億個以上あるといわれ、それら同士の結合はさらに大変な数になります。当然現在の最高性能のコンピュータをもってしても、シミュレーションは不可能です。 さらに、神経細胞同士の接続の性質も完全にわかっているとはいいがたい状況です。
では、一方で、「その人」を維持するに足る正確さで各神経細胞や、その相互の接続の情報を取得することができるのでしょうか?
生物学の世界では、生物の全遺伝情報、いわゆるゲノムがデータ化できた現在、次のターゲットは、脳の神経細胞とその全結合情報のデータ化となっています。 電子顕微鏡やその他の観測手段を用いてデータ化していくのです。 そして、一個体の全神経細胞とその全接続の情報のことを「コネクトーム」と呼び、一部の生物のコネクトームはインターネットで公開さえされているのです。
つまり、自分自身のコネクトームを作成し、それをシミュレーションできたら不老不死になれそうです。 そう、SFの登場人物たちはきっとこうやって、いとも簡単に脳をコンピュータにアップロードし、コピーし転送しバックアップしアップグレードし、そして進化しているのでしょう。
しかし、ちょっと待ってください。エンジニアなら「それどんなミドルウェア使ってるの?」って気になりませんか? RDBMSのバックアップさえ一仕事なのに、一貫性や同一性やその他諸々は本当に大丈夫なのか。むしろどんなデータ構造なのか。 気になるところはいくらでも出て来ます。 そこで、GraphDBです。
まずはコネクトームとはどのようなデータになるのかを具体的にお話ししましょう。 現在、公開されているコネクトームとしては、線虫( C. Elegans )のコネクトームがあります。 線虫は体長1mmほど、全細胞数およそ1000個、神経細胞は300個くらいです。 コネクトームの実態は、神経細胞をノード、それらの接続をエッジとしたグラフデータです。エッジの属性として、電気的・化学的結合の強さが格納されます。 つまり、このようなデータを永続化したり神経活動をシミュレーションしたりなどの様々な処理をするには、グラフデータを扱うのに適したデータ管理システム、そう、GraphDBがあればいいということになります。
GraphDBとは、その名の通りグラフデータ構造を格納して処理するためのDBMSです。 有名なものとしては、Neo4jなどがあります。 ノードとエッジからなるネットワークのようなデータ構造を、テーブルからなるデータ構造を管理するRDBMSのように、管理するものです。 RDBMSでグラフ構造を扱ったことのある人は、その煩雑さと低性能に悩まされたことがあるかもしれません。RDBMSでグラフ構造を扱うのは非常に難しいと言われています。GraphDBは最初からグラフデータ構造を扱うために設計されているために、はるかに速く安全にグラフ構造を管理できるのです。
GraphDBや、グラフデータを扱う仕組みは、例えばSNSのユーザー間の関係性であったり、検索対象ドキュメントの間の関係性であったりと、様々な対象が現実にあり、近年利用が進んでいる分野です。
ということで、本稿では、Go言語でグラフデータを格納し永続化できる簡単な仕組みを実際に作成し、そこに線虫のコネクトームを格納して、その線虫を不老不死にしてみましょう。
まずはバックエンドのデータストアを決めます。もちろんここでグラフに向いた独自データストアを作り始めてもいいのですが、今回はLevelDBを使います。GoでLevelDBを使うに当たっては、公式ドキュメントと、こちらを参考にしました。 LevelDBはいわゆるKVSなので、グラフデータに適したキーを設計しなければいけません。今回は、以下のようなキーの構造にしました。
ノード
Name | Size | Description |
---|---|---|
NodePrefix=0x01 | 1byte | プリフィックス |
type | 2byte | タイプ |
ID | 4byte | ID |
エッジ
Name | Size | Description |
---|---|---|
EdgePrefix=0x02 | 1byte | プリフィックス |
FromID | 4byte | 始点ノードのID |
Direction | 1Byte | エッジの方向 |
Type | 2Byte | タイプ |
ToID | 4byte | 終点ノードのID |
LevelDBはバイト列の前方一致による検索が可能なので、これで、
ができます。 一方で、このキー構造だと、終点から始点への検索ができないので、エッジを格納するときは、始点から終点までの前向きエッジと終点から始点までの後ろ向きエッジを同時に格納することで解決しました。
こちらが、実際に作ってみたリポジトリです。
以下にノードとエッジのコードを提示します。
また、DBのOpenや検索等のためのコードが以下になります。
公開されている多くのGraphDBには、ロックやトランザクションや各種制約、クエリ言語やサーバーとして動かすための仕組みなどがありますが、今回はLevelDBにグラフ構造を永続化して、簡単なノードの検索とエッジの検索ができるようになったということで、ここまでにしておきます。
線虫のコネクトームデータはこちらのものを利用しました。オリジナルのデータはdoi: 10.1126/science.1221762.を参照してください。dot形式のデータで取得してテキストエディタ等で置換とマクロでTSV形式の単純なデータに変換しました。
実際のデータはこのようなものです。
これを作成したGraphDBに読み込みます。
読み込みは以下のようなコードを書きました。
// InputFromTSV read two tsv files (node and edge) and insert node and edge into DB
func inputFromTSV(db *graphdb.GraphDB, nodefilename string, nodetype int16, edgefilename string, edgetype int16) {
nodefile, err := os.Open(nodefilename)
if err != nil {
panic(err)
}
defer nodefile.Close()
nodereader := csv.NewReader(nodefile)
nodereader.Comma = '\t'
for {
record, err := nodereader.Read()
if err == io.EOF {
break
} else if err != nil {
panic(err)
}
node := &graphdb.Node{
Nodetype: nodetype,
ID: no2ID(record[0]),
Value: []byte(record[1]),
}
db.AddNode(node)
}
// ==========================================================================
edgefile, err := os.Open(edgefilename)
if err != nil {
panic(err)
}
defer edgefile.Close()
edgereader := csv.NewReader(edgefile)
edgereader.Comma = '\t'
for {
record, err := edgereader.Read()
if err == io.EOF {
break
} else if err != nil {
panic(err)
}
db.AddEdge(no2ID(record[0]), no2ID(record[1]), edgetype, []byte(record[2]+","+record[3]))
}
}
func no2ID(no string) []byte {
noint, _ := strconv.Atoi(no)
id := make([]byte, 4)
binary.LittleEndian.PutUint32(id, uint32(noint))
return id
}
そして、まずは永続化した線虫のコネクトームをDOT形式で書き出し、データの確認をします。
func main() {
db, _ := graphdb.Open("celegans.db")
inputFromTSV(db, "c.elegans_neural.male_node.tsv", NEURON, "c.elegans_neural.male_edge.tsv", CONNECTION)
db.PrintGraph2DOT()
}
以下が出力したdot形式ファイルをGraphVizで書き出したものです。
次に、ノードに接続するエッジを検索してみましょう。 コードはこんな感じになります。
func main() {
db, _ := graphdb.Open("celegans.db")
start := db.GetNode(NEURON, no2ID("0"))
edges := db.GetNodesEdge(start)
for _, edge := range edges {
fmt.Println(graphdb.Byte2string(edge.To))
}
}
さらに、こうすると、Hop数を変えてノードを羅列できます。
func main() {
db, _ := graphdb.Open("celegans.db")
start := db.GetNode(NEURON, no2ID("0"))
fmt.Println(graphdb.Byte2string(start.ID))
rec(db, start, 0, 4)
}
func rec(db *graphdb.GraphDB, node *graphdb.Node, curdepth int, maxdepth int) {
curdepth++
if curdepth == maxdepth {
return
}
edges := db.GetNodesEdge(node)
for _, edge := range edges {
fmt.Println(strings.Repeat(" ", curdepth) + graphdb.Byte2string(edge.To))
next := db.GetNode(NEURON, edge.To)
rec(db, next, curdepth, maxdepth)
}
}
いかがでしょう。これで僕のPC上で線虫が不老不死となりました。
・・・そんなわけない!
今回はあくまでも実生物の神経ネットワークを使いやすい形で静的に永続化したにすぎず、生物のダイナミックな「生きている」という性質は、シミュレーションを動かしたりしなければ得られません。不老不死ははるか遠い。
今回、なぜこのような題材を選んだかということを最後に説明しないと意味不明になりそうなので、説明しておきます。
まず、GraphDBに興味があったこと、特に、今あるものを利用するのではなく、どう実現されているのかに興味があったので実際に作ってみようと考えました。 また既存のGraphDBはサーバーを立てて使うものが多いのですが、プログラム組み込みで使うものが欲しかったというのもあります。 と言っているところで、EliasDBというものを見つけました。 今回は、それRDBMSでもいいじゃんというレベルしか実装できませんでしたが、GraphDBはとても面白いので、今後も勉強していきたいと思います。
そして、GraphDBを使った題材を考えていて、コネクトームというものを知り、これだ!と思ったわけです。 コネクトームに関しては、ロボットに線虫のコネクトームを元にしたシミュレーションシステムを接続し、電子線虫サイボーグを作る研究など、とてつもなく未来な研究が実際に行われています。例えば、ここです。興味のある方は調べてみると面白いと思います。
それでは、引き続きこの後のKLabアドベントカレンダーをお楽しみください。
KLabのゲーム開発・運用で培われた技術や挑戦とそのノウハウを発信します。
合わせて読みたい
KLabのゲーム開発・運用で培われた技術や挑戦とそのノウハウを発信します。