パターン指向リファクタリング入門を読んでみた

古いですが"パターン指向リファクタリング入門~ソフトウエア設計を改善する27の作法"を読んでみたので感想など

□生成

Creation Methodによるコンストラクタの置き換え

JavaC++ではコンストラクタがクラス名と一致するためどんなインスタンスが作られるのかが把握しづらくなる場合がある。
例えばローンを表すクラスがあってそれに対して支払い期日までに払わなければいけないタームローンや融資枠や有効期限が定められる回転融資など7種類のコンストラクタがある場合は、それぞれのインスタンスを作成すCreateionメソッドを用意した方がコードの見通しが良くなる。 コンストラクタをCreation Methodに置き換えた場合は以下のようになりどんなインスタンスが作られるのかがわかりやすくなる。

public class CapitalCalculation {
  public static Loan createTermLoan(double commitment, int riskRating, Date maturity)  
    return new Load(commitment, riskRating, maturity);

Factoryクラスによるインスタンス生成

Creation Methodの数が多い場合はインスタンスを生成するためだけのファクトリークラスを作ることで見通しが良くなる。 例えばローンなどインスタンスをどう生成するかで気をつけなければいけない場合は、一つのFactoryクラスでインスタンス生成を行うようにした方が良い。インスタンス生成のためのパラメータが後から増え他としてもFactoryクラスがあればどこにインスタンス生成のためのロジックがまとめられるのかがわかりやすくなる。

Factoryによるインスタンス生成を強制

Factoryによるインスタンスを強制する場合は、デフォルトのコンストラクタをprivateにする。

public class CapitalCalculation {
  private CapitalCalculation(,,,){
    ,,,
  }

javajava.util.Collectionsクラスはクラスをカプセル化する良い例らしい。javaのコレクション型ではListについてはUnmodifiableListやSynchronizedListなどあるが直接newするのではなくファクトリーメソッドを使うようにすることで利用者側でインスタンス生成について気をつけることが減っている。
開発時も同様で利用者側での負担を減らすためにファクトリーメソッドを強制して内部クラスを隠蔽する必要がある場合があるかもしれない。

BuilderによるCompositeの隠蔽

Compositeパターンに対してFactoryクラス一つですべてのインスタンス生成を行うのは無理があるのでインスタンス生成の責務をうまく分けた方が良いということか。流行っているDDDであればちゃんと集約をしておいて対応のfactoryでどこのインスタンスを生成するのかの責務をわかるようにしておけというのと同じ内容のはず。この辺りはどうモデリングして集約させるかに慣れたら自然と出来るようになる気がする。

Singoletonのインライン化

singletonのパターンは設計が楽になるし特に組み込み系などであれば全体の状態を管理するために必須となるしその為にもスレッドセーフなsingletonで実装を行わなければならない。ただ不必要にsingletonを使っている場合があってその場合は問題が起きる原因となりうるので回避出来るようにしたい。 スレッドセーフとかは別にしてシングルトンオプジェクトの取り扱う際に利用側からしたらオブジェクトの値を直接変更するよりも、参照、更新するための関数には決まったものを使うようにしたい。その為に方法として以下がある。 - Singletonの機能を1つのクラスに移し、そのオブジェクトを格納してアクセス手段を提供する。

以下にConsoleオブジェクトをクラスに変更してそれにアクセスするためのBlackjackクラスを利用するサンプルを示す。

public class Console {
  privae static HitStayResponse hitStayResponse = new HitStayResponse();

  private console() {
    super();
  }

  public static HitStayResponsne
    obtainHitStayResponse(BufferedReader input) {
      hitStayResponse.readFrom(input);
      return hitStayResponse;
    }

  public static void
    setPlayerResponse(HitStayResponse newHitStayResponse){
      hitStayResponse = newHitStayResponse;
    }
}

public class Blackjack...
  public HitStayResponse obtainHitStayResponse(BufferedReader input) {
    return Conseole.obtainHitStayResponse(input);
  }
  public void setPlayResponse(HitStayResponse newHitStayResponse){
    Console.setPlayerResponse(newHitStayResponse);
  }

singletonのstatic変数へのアクセスを提供するConsoleクラスおよびそれを利用するBlackjackで専用のメソッドは準備するとして、重要なのはConsole、Blackjackのそれぞれの粒度で利用側が使いやすくなるよううまく隠蔽を行うことのはず。

□単純化

メソッドの構造化

判定処理とかは一目で何をやっているのか分かるようにまとめる。

public void add(Object element) {
  if(!readOnly){
    int newSize = size+1;
    if(newSize > elements.length){
      Object[] newElements = new Object[Elements.length + 10];
      for(int i=0; i<size; i++){
        newElements[i] = elements[i];
      }
      elements = newElements;
    }
    elements[size++] = element;
  }
}

リファクタリング

public void add(Object element) {
  if(readOnly) return;
  if(atCapacity()){
    grow();
  }
  addElement(element);
}

これはリファクタリング後の方が圧倒的に見やすくなる。一目で分かる粒度のメソッドに分けることをComposed Methodというらしい。

Strategyによる条件判断の置き換え

例えばローンクラスに10種類のインスタンス生成パターンがあって3種類の料金計算Strategyがあるとしたらローンクラスのインスタンス生成は専用のファクトリークラスで行い、料金計算のためのStrategy生成はファクトリークラスがインスタンス生成の為にローンクラスのメソッドを呼び出すタイミングで行うようにする。Strategy生成の責務について3種類ぐらいであればローンクラスにもたせておいて複雑化してきたら別でstrategyを生成するためのfactoryを用意したら良いのか。

Decoratorによる拡張機能の書き換え

たまに使う場合がある機能を拡張機能として外に出すこと

public class StringNode extends AbstractNode...
  public satic Node createStringNode(StringBuffer textBuffer, int textBegin, int textEnd, boolean shouldDecode) {

    if(shouldDecode)
      return new DocodeingNode(
        new StringNode(textBuffer, textBegin, textEnd);
      )
    return new String Node(textBuffer, textBegin, textEnd);
  }

  public String toPlainTextString() {
    return textBuffer.toString();
  }
...

public class DecodingNode implements Node...
  private Node delegate;

  public DecodingNode(Node newDelegate){
    delegate = newDelegate;
  }

  public String toPlainTextString() {
    return Translate.decode(delegate.toPlainTextString());
  }

上記サンプルではStringNodeにshouldDecodeのオプションがあった場合はDecodingNodeのインスタンスが作られていてtoPlainTextString呼び出した際にはNodeクラスにロジックを利用した上でTranslate.decodeの追加処理を行っている
scalaのtraitをmix-inするのと同様か

Stateによる状態変化のための条件判断の置き換え

状態によって異なる初期があった場合はstateパターンによって、状態毎に生成するインスタンスを切り替えて利用する。似た処理を行う状態が増えることが予想される場合は使っても良いかも。

Compositeによる暗黙的なツリー構造の置き換え

データ型がツリー構造や再帰構造だったらcompositeを使えというもので、サンプルがxmlのパーサだったのでそういった時にはぴったしだと思う。

Commandによる条件付きディスパッチャの置き換え

特にwebアプリで使われるデザインパターン。クライアントからのリクエストをコマンドに投げて必要な処理を行う。最近はリアクティブが注目されているのでその場合はCRUDよりもCQRSの粒度でコマンドを作りたい。

□汎用化

Template Methodの形成

よく使われるので特に意識する必要はないと思う。サブクラスで共通で必要になるメソッドをスーパークラス側に持っていくというもの

Compositeの抽出

似通った特性を持つ2つ以上のクラスがあれば共通の特性をスーパークラスに移動して共通化させる。こうすることでサブクラスがぐっとシンプルになることがある。

Compositeによる単数・複数別の処理の置き換え

Compositeの抽出を繰り返す

Observerによるハードコードされた通知の置き換え

通知を受け取るためのインターフェイスを統一させて監視を行いやすくするというものでマルチスレッドでプログラムを動かし通知があったら逐次処理を反映していくとか用件があったら必要なんだと思う。

Adapterによるインタフェースの統合

利用側が違いを意識せず使えるようにするため必要

Adapterの抽出

利用側がAdapterの振る舞いにアクセスする必要がある。例えば使用するDBのバージョンによって使うクラスを切り替える場合などあり、その場合はクラスの生成側にインスタンス生成用のロジックを持たせる。

Interpreterによる暗黙的な言語処理の置き換え

例えばリストの検索を行う際などに絞り込みを行うためのインターフェースを用意しておいてそれを実装したクラスによるリストをiterateに反映することで期待した連続処理を行えるようにする。

□全体的な感想

既存の処理に対してデザインパターンをどう適用するかの本になっています。実践でデザインパターンをどう適用するのかわからない人は読む価値があるのかと思います。リファクタリングを行うとしたら上流のモデリングなどが必要になると思いますがこの本が扱っているのは上流の分析が済んだ上で、それをどうオブジェクト指向で実装するかというものになっています。レガシーで開発も行われるプログラムにどうリファクタリングで立ち向かうとかといったら別でこんなのを読んだ方が良いでしょうか。

ロジスティック回帰について調べてみた

ロジスティック回帰

いろいろ間違っているところはあると思いますが、ロジスティック回帰について調べた内容をまとめてみました。
数式は分からないけどChainerとかのオープンソースフレームワークでどんなことすれば良いのかイメージが付くぐらいになるのを目指したいと思います。

概要

ベルヌーイ分布に従う変数の統計的回帰モデルの一種である。

ベルヌーイ分布とは

取りうる値が2つであるような確率変数1つで記述する分布、例えばある人が"YES"と答える確率がxで"NO"と答える確率が1-xであるような確率分布を表すのに使われる。この場合に3回質問して3回とも"YES"と答えるならx3で、2回YESで1回NOなら3(1 - x)x2と言ったように表すことができる。自然言語処理等で使われる場合は1つの確率変数で分析を行うには無理があるのでは多変数ベルヌーイモデルが使われているはず。

多変数ベルヌーイモデル

多変数ベルヌーイモデルの場合複数の独立した確率した確率変数で確率分布を表します。例えば語彙にgood, bad, exciting, boringの4つがあり、4つの語彙限定でそれぞれの単語の発言確率をpGood, pBad, pExciting, pBoringとした場合(bGood + bBad + bExciting + bBoring = 1)に"good bad booring exciting boring good boring"の順番で発言する確率を求める場合に以下のようになる。

順番が決まっている場合は
pGood^2 * bBad^1 * bExciting^1 * bBoring^3となる。
単語の順番を気にしないのであれば以下のように求められる。
       7!            
----------------- * pGood^2 * bBad^1 * bExciting^1 * bBoring^3
2! * 1! * 1! * 3!           

ここでポジティブな人の各語彙の発言確率を以下のようになっており
(pGood, pBad, pExciting, pBoring) = (0.4, 0.1, 0.4, 0.1)
でネガティブの各語彙の発言確率が以下になっている場合
(pGood, pBad, pExciting, pBoring) = (0.3, 0.2, 0.3, 0.2)
その場合の"good bad booring exciting boring good boring"と発言した場合その人がポジティブかネガティブかの確率は以下のように求めることができる。

ポジティブな人の場合の確率
        7!            
----------------- * 0.4^2 * 0.1^1 * 0.4^1 * 0.1^3 = 0.002688
2! * 1! * 1! * 3!           

ネガティブな人の場合の確率
        7!            
----------------- * 0.3^2 * 0.1^2 * 0.3^1 * 0.2^3 = 0.018144
2! * 1! * 1! * 3!           

となりネガティブな人が発言する方が確率が高いため、おそらくネガティブな人だろうと判定できます。

改めてロジスティック回帰

ベルヌーイ分布とは何ぞやの部分がなんとなくわかったところでロジスティック回帰の流れを確認してみたいと思います。やりたいこととしては学習用のドキュメントデータとそれに対する分類結果のデータセットをもらった後に各分類ごとの最適な確率変数を求めて、それぞれの分類に対しての確率を求められるようになることです。

機械学習を始めたばかりですと最適化の流れがどうなっているのか把握できないと思うのですが、大体こんな感じになっていると思います。

1.学習データの読み込み

 ↓

2.確率変数のパラメータ初期化

 ↓

3.確率的勾配降下法により最適なパラメータを求める。
 3.1一度の学習で使うデータ数はどうするか?バッチ学習か?ミニバッチか?
 3.2各層の出力を決める活性化関数には何を使うか?ソフトマックス関数か?
 3.3誤差関数には何を使うか?交差エントロピーか?
 3.4誤差逆伝搬によりパラメータを更新する方向を決める?
 3.4更新するパラメータの大きさはどう決めるか?AdaGradか?それとも、、、

 ↓

4.学習にして最適化されたパラメータにより入力データに対して各分類結果ごとの確率を算出して分類を行う

すごいザックリまとめるとこんな感じになると思いまして確率的勾配降下法によるパラメータ最適化の工程について簡単に説明したいと思います。

ミニバッチ

機械学習では大量の学習データを扱います。そのためすべてのデータを読み込んでからパラメータを更新するのであれば計算コストが大きくなってしまいます。それを防ぐために学習データからサンプルを抽出します。抽出したサンプルをミニバッチと呼びそれに対して学習を行っていきます。サンプルの抽出方法についてもランダムに取ったりループの中のインデックスから定数取ってくるかとかいろいろあるようです。

活性化関数

ロジスティック回帰は多クラス分類のためソフトマックス関数が使われます。簡単に説明すると各分類結果に対する数値を合計すると1になるというものです。例えばポジティブ、ネガティブに分類する場合であれば、ポジティブに分類する数値とネガティブに分類する数値の合計がいっつも1になるようにするものです。

誤差関数

多クラス分類で使われる誤差関数は交差エントロピーです。ロジスティック回帰では抽出したミニバッチとその時点でのパラメータで計算(順伝播)し、それと正解データを比較してどれだけづれているか計算するのに交差エントロピーを使います。

誤差逆伝搬

どの方向にパラメータを更新すれば良いのかを求めるために更新パラメータの差分と誤差関数で微分を行います。誤差逆伝搬と言われておりましてどんな計算しているのか気になる場合は以下を参照したら良さそうです。 https://www.slideshare.net/piroyoung/ss-50433746?next_slideshow=1  

パラメータ更新

各パラメータの更新方向は求められたので、最適化アルゴリズムによりどれだけパラメータが更新するかを求められるます。例えばAdagradではまれに現れるパラメータは大きく更新し、よく出るパラメータは小さく更新するなど最適化アルゴリズムごとで更新方法が異なっています。詳しくは以下を見たら良いと思います。
http://postd.cc/optimizing-gradient-descent/#gradientdescentoptimizationalgorithms

回帰分析

確率的勾配効果により各パラメータが最適化されたらそれを使って分類を行います。

Pythonオープンソース機械学習フレームワークのChainerであれば簡単にロジスティック回帰行えます。以前試した時のを以下にまとめています。 http://steavevaivai.hatenablog.com/entry/2017/03/20/232339

GraphXを試してみた

GraphXとは

Apache Sparkのライブラリの一つでグラフの並列分散処理を行うことができます。Sparkの基本APIや他のライブラリの機能と合わせて利用することができるため、入力データの事前処理からグラフの生成、分析まで一貫して行えるのが強みとなっています。用途としてはtwitterなどの影響力や、レコメンドなどに利用できるのかと思います。

実際に動かしてみる

では実際に動かして試してみたいと思います。ここチュートリアルを試してみたいと思います。

事前準備

build.sbtのlibraryDependenciesに以下を追加しておいてください。

“org.apache.spark” % “spark-graphx_2.11” % sparkVersion,

GraphXについて

GraphXではノードとエッジで構成される以下のGraphクラスを使用して分析を行います。

class Graph[VD, ED] {
  val vertices: VertexRDD[VD]
  val edges: EdgeRDD[ED]
}

例えば、ノードにユーザ、エッジにユーザ間の関連を表す場合は以下のようにGraphを表すことができます。

val users: RDD[(VertexId, (String, String))] =
  sc.parallelize(Array((3L, ("rxin", "student")), (7L, ("jgonzal", "postdoc")),
                       (5L, ("franklin", "prof")), (2L, ("istoica", "prof"))))

 // Create an RDD for edges
 val relationships: RDD[Edge[String]] =
   sc.parallelize(Array(Edge(3L, 7L, "collab"),    Edge(5L, 3L, "advisor"),
                        Edge(2L, 5L, "colleague"), Edge(5L, 7L, "pi")))
 // Define a default user in case there are relationship with missing user
 val defaultUser = ("John Doe", "Missing")
 // Build the initial Graph
 val graph = Graph(users, relationships, defaultUser)

Graphに対して3つめの引数を渡しておりますが、これは関連情報はあるけど実際の人物が見つからなかった時に使用するデフォルトユーザになります。

グラフのトリプレット情報を出力する

先ほどのユーザと関連で構成されるグラフの情報を出力する場合はtripletsを使用します。tripletsはノードとエッジを組み合わせた情報になりまして今回のサンプルであればfranklinはrxinのadvisorと言った情報を表しています。

// トリプレットでグラフの情報を表示
println("show graph")
val facts: RDD[String] =
  graph.triplets.map(triplet =>
    triplet.srcAttr._1 + " is the " + triplet.attr + " of " + triplet.dstAttr._1)
facts.collect.foreach(println(_))

上記処理で以下のトリプレット情報が出力されます。

rxin is the collab of jgonzal
franklin is the advisor of rxin
istoica is the colleague of franklin
franklin is the pi of jgonzal

簡単なフィルター処理を行ってみる

グラフのトリプレットについてはフィルター処理を簡単に行うことができまして、例えばadvisorの関係のみを出力したい場合は以下のようになります。

val facts2: RDD[String] =
  graph.triplets.filter(t => t.attr equals  "advisor").map(triplet =>
    triplet.srcAttr._1 + " is the " + triplet.attr + " of " + triplet.dstAttr._1)
facts2.collect.foreach(println(_))

それからユーザ指定で誰かに対する関係の情報のみを出したい場合は以下のようになります。

println("show filterd by node graph")
val facts3: RDD[String] =
  graph.triplets.filter(t => t.srcAttr._1 equals  "franklin").map(triplet =>
    triplet.srcAttr._1 + " is the " + triplet.attr + " of " + triplet.dstAttr._1)
facts3.collect.foreach(println(_))

中心性分析

GraphXではどれだけ中心に位置するか、SNSであればどれだけ影響力があるかを調べるための中心性分析も行うことができます。ここではGraphXのリポジトリにあるサンプルデータを利用して影響力のある人物を調べてみたいと思います。使用するのは以下のuser情報とフォロー情報になります。
◯users.text

1,BarackObama,Barack Obama
2,ladygaga,Goddess of Love
3,jeresig,John Resig
4,justinbieber,Justin Bieber
6,matei_zaharia,Matei Zaharia
7,odersky,Martin Odersky
8,anonsys

◯followers.text

2 1
4 1
1 2
6 3
7 3
7 6
6 7
3 7

エッジ情報のファイルを読み込んでから中心性を分析は以下の処理だけで行えます。手軽そうです。

// Load the edges as a graph
val graphRank = GraphLoader.edgeListFile(sc, "data/followers.txt")
// Run PageRank
val ranks = graphRank.pageRank(0.0001).vertices

それから、ユーザ情報を読み込んで中心性解析結果を付与するのは以下になります。

val usersRank = sc.textFile("data/users.txt").map { line =>
  val fields = line.split(",")
  (fields(0).toLong, fields(1))
}
val ranksByUsername = usersRank.join(ranks).map {
  case (id, (username, rank)) => (username, rank)
}

あとは中心性情報を付与したユーザ情報を一覧出力します。

println(ranksByUsername.collect().mkString("\n"))

そしたらこんな感じの結果が得られると思います。

(justinbieber,0.15)
(matei_zaharia,0.7013599933629602)
(ladygaga,1.390049198216498)
(BarackObama,1.4588814096664682)
(jeresig,0.9993442038507723)
(odersky,1.2973176314422592)

これをみると誰からもフォローされていなjustinbieberが低くなっていて、2名からフォローされているBarackObama,odersky,jeresigの影響力が高くなっていることが確認できます。気になったのが2名にフォローされているjeresigよりもladygagaの方が数値が高い点です。graphRank.pageRankに渡した引数も影響している気もするのですが、2名にフォローされているBarackObamaに唯一フォローされているということで影響力が高いと判定されているのでしょうか。
試しにfollowers.txtを以下のように修正してみると

2 1
4 1
1 2
6 3
7 3
1 4
7 6
6 7
3 7

以下のような結果が得られladygagaの影響力が下がってjustinbieberの影響力が上がっているのか確認できました。想定通りです。

(justinbieber,0.7697996414951923)
(matei_zaharia,0.7013599933629602)
(ladygaga,0.7697996414951923)
(BarackObama,1.4583520976357462)
(jeresig,0.9993442038507723)
(odersky,1.2973176314422592)

使ってみた感想

簡単な分析を行う分には手軽で良さそうな気がしました。

Vue.jsからcytoscapeを使ってみる

Vue.jsの単一ファイルコンポーネントからCytoscapeを利用してみました。気をつける点としてはコンポーネントがマウントされるタイミングより前にCytoscapeの初期化処理を行ってはいけないぐらいですが、念のためメモしておきます。

事前作業

vue-initなどでプロジェクトを作成した後にnpm installでcytoscapeをダウンロードしておきます。

npm install –save cytoscape

単一ファイルコンポーネントスクリプトcytoscapeをrequireする

var cytoscape = require(‘cytoscape’)

初期化スクリプトの作成

cytoscape初期化のスクリプトを作成します。

methods: {
  view_init: function () {
    this.cy = cytoscape(
      {
        container: document.getElementById('cy'),
        boxSelectionEnabled: false,
        autounselectify: true,
        style: cytoscape.stylesheet()
            .selector('node')
            .css({
              'height': 80,
              'width': 80,
              'background-fit': 'cover',
              'border-color': '#000',
              'border-width': 3,
              'border-opacity': 0.5,
              'content': 'data(name)',
              'text-valign': 'center'
            })
            .selector('edge')
            .css({
              'width': 6,
              'target-arrow-shape': 'triangle',
              'line-color': '#ffaaaa',
              'target-arrow-color': '#ffaaaa',
              'curve-style': 'bezier'
            }),
        elements: {
          nodes: [
            { data: { id: 'cat' } },
            { data: { id: 'bird' } },
            { data: { id: 'ladybug' } },
            { data: { id: 'aphid' } },
            { data: { id: 'rose' } },
            { data: { id: 'grasshopper' } },
            { data: { id: 'plant' } },
            { data: { id: 'wheat' } }
          ],
          edges: [
            { data: { source: 'cat', target: 'bird' } },
            { data: { source: 'bird', target: 'ladybug' } },
            { data: { source: 'bird', target: 'grasshopper' } },
            { data: { source: 'grasshopper', target: 'plant' } },
            { data: { source: 'grasshopper', target: 'wheat' } },
            { data: { source: 'ladybug', target: 'aphid' } },
            { data: { source: 'aphid', target: 'rose' } }
          ]
        },
        layout: {
          name: 'breadthfirst',
          directed: true,
          padding: 10
        }
      }
    )
  }
},

コンポーネントマウント時にcytoscape初期化スクリプト呼び出し

mounted: function () {
  this.view_init()
}

これだけで、Vue.jsでもCytoscapeのグラフが表示さえるはずです。

ボタンクリックでノードを追加できるようにする

add_node: function (newId, targetId) {
  this.cy.add([
    { 'group': 'nodes', data: { 'id': newId }, position: { x: 300, y: 200 } },
    {'group': 'edges', data: {'id': 'edge' + this.count, 'source': newId, 'target': targetId}}
  ])
}

それとcytoscapeで動的にノードを足す場合はこんな感じでノードとエッジを追加してあげれば大丈夫です。レイアウトを初期化するのでなければノードの位置を指定しなければいけないです。 最終的にこんなコンポーネントを作成しました。

<template>
  <div id="view">
    <button v-on:click="add_node">push</button>
    <div id="cy"></div>
  </div>
</template>

<script>
var cytoscape = require('cytoscape')

export default {
  name: 'Cytoscape',
  components: {},
  created: function () {
  },
  data: function () {
    return {
      input: '',
      output: '',
      msg: 'vue to cytoscape',
      count: 0
    }
  },
  methods: {
    add_node: function () {
      console.info(this.cy)
      this.cy.add([
        { 'group': 'nodes', data: { 'id': 'node' + this.count }, position: { x: 300, y: 200 } },
        {'group': 'edges', data: {'id': 'edge' + this.count, 'source': 'node' + this.count, 'target': 'cat'}}
      ])
    },
    view_init: function () {
      this.cy = cytoscape(
        {
          container: document.getElementById('cy'),
          boxSelectionEnabled: false,
          autounselectify: true,
          style: cytoscape.stylesheet()
              .selector('node')
              .css({
                'height': 80,
                'width': 80,
                'background-fit': 'cover',
                'border-color': '#000',
                'border-width': 3,
                'border-opacity': 0.5,
                'content': 'data(name)',
                'text-valign': 'center'
              })
              .selector('edge')
              .css({
                'width': 6,
                'target-arrow-shape': 'triangle',
                'line-color': '#ffaaaa',
                'target-arrow-color': '#ffaaaa',
                'curve-style': 'bezier'
              }),
          elements: {
            nodes: [
              { data: { id: 'cat' } },
              { data: { id: 'bird' } },
              { data: { id: 'ladybug' } },
              { data: { id: 'aphid' } },
              { data: { id: 'rose' } },
              { data: { id: 'grasshopper' } },
              { data: { id: 'plant' } },
              { data: { id: 'wheat' } }
            ],
            edges: [
              { data: { source: 'cat', target: 'bird' } },
              { data: { source: 'bird', target: 'ladybug' } },
              { data: { source: 'bird', target: 'grasshopper' } },
              { data: { source: 'grasshopper', target: 'plant' } },
              { data: { source: 'grasshopper', target: 'wheat' } },
              { data: { source: 'ladybug', target: 'aphid' } },
              { data: { source: 'aphid', target: 'rose' } }
            ]
          },
          layout: {
            name: 'breadthfirst',
            directed: true,
            padding: 10
          }
        }
      )
    }
  },
  computed: {
  },
  mounted: function () {
    this.view_init()
  }
}

</script>
<style scoped>
#cy {
    width: 100%;
    height: 80%;
    position: absolute;
    top: 50px;
    left: 0px;
    text-align: left;
}

body {
  font: 14px helvetica neue, helvetica, arial, sans-serif;
}
</style>

cytoscapeが便利そうです。元々はネットワーク分析の可視化ツールですが、これがあればマインドマップとかだいぶ楽に作れそうだと思いました。その場合は、エッジ、ノードのデータを別のコンポーネントに渡せるようにした方が良いんだろうな。

Cytoscape.jsを試してみた

Cytoscape.jsとは

もともとはクライアントアプリとして欧米の研究機関によって開発されているオープンソースのネットワーク可視化ソフトウェアプラットフォームであるCytoscapeが10年ほど前に作られ現在も継続して開発が行われています。Cytoscapeはデータの読み込み、分析、可視化といった一連の処理を行うことができますが、分析は別のソフト例えばRなどを使い、それから分析結果をエクスポートしてCytoscapeで可視化と言った使われ方もあるようです。クライアントアプリでは分析結果の共有に不便であるためカナダのトロント大学Cytoscape.jsのを開発を始めたようです。
以下のデモを見るのが一番わかりやすいと思います。 js.cytoscape.org

Cytoscapeでの処理フローについて

下記URLのCytoscapeの開発者の記事によりますと以下のような処理フローになるようです。
http://qiita.com/keiono/items/0db6495c4d8bc215de67

    ネットワークデータを読み込む
    アトリビュート(データテーブル)を読み込む
        ソーシャルネットワークの例で言えば
            人の名前、年齢、職業、性別などがノードアトリビュート
            婚姻関係、友人関係、メールの交換された数などといった関係性に関する情報がエッジアトリビュート
    統合されたデータを使って、統計的な解析やフィルタリングなどを行い分析する
    自動レイアウト機能を使って、ノードを見やすく配置する
    ノード、エッジに付随する各種データに基づきマッピングを設定し、描画を行う
    PDFやJavaScriptウィジェットに出力する

Cytoscape.jsを試してみる

では実際にCytoscape.jsを試しに動かしてみたいと思います。
ここのチュートリアルを試してます。

1.ソース取得
ソースををgithubからダウンロードしておきます。
https://github.com/cytoscape/cytoscape.js

2.html作成
以下のhtmlを記述します。srcのパスはダウンロードしたCytoscape.jsのdist/cytoscape.min.jsを参照するように修正しておいてください。

<!doctype html>

<html>

<head>
    <title>Tutorial 1: Getting Started</title>
    <script src="cytoscape.js"></script>
</head>

<style>
    #cy {
        width: 100%;
        height: 100%;
        position: absolute;
        top: 0px;
        left: 0px;
    }
</style>

<body>
    <div id="cy"></div>
</body>
</html>

divタグに対してcanvasを埋め込んでグラフの描画を行いまして、cssで表示領域を設定しています。

3.グラフインスタンスの作成

var cy = cytoscape({
  container: document.getElementById('cy'),
  elements: [
    { data: { id: 'a' } },
    { data: { id: 'b' } },
    {
      data: {
        id: 'ab',
        source: 'a',
        target: 'b'
      }
    }]
});

上記スクリプトで描画するノードとエッジを指定しています。data: { id: ‘a’ } 、data: { id: ‘b’ } とありますので2つのノードが描画されます。それから、data: {id: ‘ab’,source: ‘a’,target: ‘b’}とありますので2つのノードを結んだエッジも表示されます。
先ほど作成したhtmlにスクリプトを記述して開いてみるとグラフが表示されたことが確認できます。cytoscape.jsには自動レイアウト機能がありますのでウィンドウのサイズを変更して開き直すとノードが縦に並んだり、横に並んだりして自動でレイアウトして描画されることが確認できます。

4.グラフの描画設定 3で描画したスクリプトのelementsに続けて以下のstyleを追記してください。

style: [
    {
        selector: 'node',
        style: {
            shape: 'hexagon',
            'background-color': 'red',
            label: 'data(id)'
        }
    }]

それから開き直すとノードが赤色六角形のID付きで表示されることが確認できたかと思います。
styleについて他にどんな設定があるかは以下を見たら良いと思います。
https://github.com/cytoscape/cytoscape.js/blob/master/documentation/md/style.md

5.ノードの配置設定
ノードをどのように配置するかはlayoutで指定できるようになっています。例えばノードを円状に並べて描画したい場合は、以下を追記したら行えます。

cy.layout({
    name: 'circle'
});

円状に並べたのをわかりやすくするためhtmlを編集しノードを増やしてみます。

<!doctype html>

<html>

<head>
    <title>Tutorial 1: Getting Started</title>
    <script src="../cytoscape.js/dist/cytoscape.min.js"></script>
</head>

<style>
    #cy {
        width: 100%;
        height: 100%;
        position: absolute;
        top: 0px;
        left: 0px;
    }
</style>

<body>
    <div id="cy"></div>
    <script>
      var cy = cytoscape({
        container: document.getElementById('cy'),
        elements: [
          { data: { id: 'a' } },
          { data: { id: 'b' } },
          {
            data: {
              id: 'ab',
              source: 'a',
              target: 'b'
            }
          }],
        style: [
        {
            selector: 'node',
            style: {
                shape: 'hexagon',
                'background-color': 'red',
                label: 'data(id)'
            }
        }]
      });
      for (var i = 0; i < 10; i++) {
          cy.add({
              data: { id: 'node' + i }
              }
          );
          var source = 'node' + i;
          cy.add({
              data: {
                  id: 'edge' + i,
                  source: source,
                  target: (i % 2 == 0 ? 'a' : 'b')
              }
          });
      }
      cy.layout({
        name: 'circle'
      });
    </script>
</body>
</html>

これで開きなおしてみると以下のように円状に並んで表示されることが確認できます。 f:id:steavevaivai:20170325065549p:plain

レイアウトの描画設定は自分で行うこともできここにまとめられているのですがcircleに配置するのであれば、以下の設定でも同様となっております。

var options = {
  name: 'circle',

  fit: true, // whether to fit the viewport to the graph
  padding: 30, // the padding on fit
  boundingBox: undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
  avoidOverlap: true, // prevents node overlap, may overflow boundingBox and radius if not enough space
  radius: undefined, // the radius of the circle
  startAngle: 3 / 2 * Math.PI, // where nodes start in radians
  sweep: undefined, // how many radians should be between the first and last node (defaults to full circle)
  clockwise: true, // whether the layout should go clockwise (true) or counterclockwise/anticlockwise (false)
  sort: undefined, // a sorting function to order the nodes; e.g. function(a, b){ return a.data('weight') - b.data('weight') }
  animate: false, // whether to transition the node positions
  animationDuration: 500, // duration of animation in ms if enabled
  animationEasing: undefined, // easing of animation if enabled
  ready: undefined, // callback on layoutready
  stop: undefined // callback on layoutstop
};

cy.layout( options );

6.ノードごとでスタイルを変えてみる
ノードごとでスタイルを変更したい場合があると思うのですが、その場合はノードに付与したIDを指定してstyle属性に設定を追加してあげれば行えます。
例えばID=node1に対してスタイルを設定したい場合は以下のようになります。

{
    selector: '#node1',
    style: {
        'background-image': 'https://farm8.staticflickr.com/7272/7633179468_3e19e45a0c_b.jpg',
        'height': 80,
        'width': 80,
        'background-fit': 'cover',
        'border-color': '#000',
        'border-width': 3,
        'border-opacity': 0.5
}

結果、このようになりスタイルが適用されているのが確認できます。 f:id:steavevaivai:20170325065606p:plain

触ってみた感想として表示するデータに合わせて柔軟にレイアウトを調整するというよりかは、事前に表示するデータを想定した上でそれに合わせてレイアウトをどう調整するのかが重要そうな気がしました。他の分析ソフトからエッジとノードあとアトリビュートをエクスポートできたら分析結果の共有に便利そうです。

Rでネットワーク分析をしてみる

ちょっと興味が湧いたのでigraphを使ってネットワーク分析を行ってみたいと思います。

igraph

igraphとは?
ネットワーク解析周りの関数が多数登録されており、簡単にネットワーク解析を行うことができる。最近ではグラフデータベースのneo4jなどでネットワーク分析をするのが流行ってそうですが、簡単に試す場合はRとかapach sparkを使う方が楽そうな気がします。

インストール 

install.packages(“igraph”)

バージョン確認

packageVersion(“igraph”)

パッケージの読みこみ

library(igraph)

サンプルデータで動かしてみる

以下のサイトから"Zachary’s karate club"をダウンロードする。データの中身は大学の空手部の交友関係情報らしいです。
http://www-personal.umich.edu/~mejn/netdata/

igraphを使う

install.packages(“igraph”)
library(“igraph”)

データを読み込む

“Zachary’s karate club"のデータをダウンロード後解答したkarate.gmlがグラフの情報を表すデータになります。試しに開いてみると以下のようにノードとエッジの情報を持った有向グラフを表していることがわかります。

Creator "Mark Newman on Fri Jul 21 12:39:27 2006"
graph
[
  node
  [
    id 1
  ]
  node
  [
    id 2
  ]

~~~

edge
[
  source 34
  target 32
]
edge
[
  source 34
  target 33
]
]

今回はノードにラベルの情報が含まれていないため、グラフ表示する際に名前の情報は含まれません。ノードにラベルを含む場合は以下のようにnodeの要素にlabelの項目を追加する形式で保存されます。

Creator "Mark Newman on Sat Jul 22 05:32:16 2006"
graph
[
  directed 0
  node
  [
    id 0
    label "BrighamYoung"
    value 7
  ]
  node
  [
    id 1
    label "FloridaState"
    value 0
  ]

~~~

ちなみにsourceとtargetが同一のエッジが複数あった場合強いつながりとして処理してくれてそうだった。
gml形式のデータについて詳しくは下記参照
http://graphml.graphdrawing.org/

今回はすでにノードとエッジの情報は分析済みのgml形式のデータでRStudioから表示させる動きを試してみたいと思います。 グラフデータの読み込みはread.graphを使います。

karate_gh <- read.graph(“./karate.gml”, format=“gml”)

データの可視化

plot関数で読み込んだグラフを可視化します。

plot(karate_gh, vertex.size=4, edge.arrow.size=0.2, layout=layout.fruchterman.reingold)

コミュニティ分析

leading.eigenvector.community関数によりグラフのデータからコミュニティの情報を得ます。得たデータをplot関数に利用することでコミュニティごとで色分けした表示が行えます。

karate_com <- leading.eigenvector.community(karate_gh)
plot(karate_gh, vertex.size=4, edge.arrow.size=0.2, vertex.color=karate_com$membership, layout=layout.fruchterman.reingold)

中心性解析

page.rank関数によりどれだけコミュニティの中心に位置していかを数値化できるので、これをプロット時のノードのサイズとして表すことで中心性解析の可視化が行える。

karate_pr <- page.rank(karate_gh, directed=TRUE)
plot(karate_gh, vertex.size=karate_pr$vector*200, vertex.color=karate_com$membership, edge.arrow.size=0.2, layout=layout.fruchterman.reingold)

f:id:steavevaivai:20170322064058p:plain

とりあえずノードとエッジがはっきりしたgml形式のデータがあればRで簡単に可視化できることがわかった。ブラウザ上でデータをみたい場合は、以下のossが使えそう。
js.cytoscape.org

Chainerによるロジスティック回帰

PythonディープラーニングフレームワークであるChainerを使って簡単なセンチメント分析を行ってみたいと思います。

必要なモジュールのインポート

# import chainer module
import numpy as np
import chainer
from chainer import cuda, Function, gradient_check, Variable
from chainer import optimizers, serializers, utils
from chainer import Link, Chain, ChainList
import chainer.functions as F
import chainer.links as L

# import mecab module
import re
import MeCab
import json

それからロジスティック回帰用のモデルを定義します。レイヤーは一つで入力が単語の種類数で、出力がクラスの数で今回はポジティブ、ネガティブの2つになるように引数で渡します。単語の種類数は学習データを読み無彩にカウントします。

# ロジスティック回帰を行います
class MyRogistic(Chain):
    # パラメータ数に語彙の数, 分類結果数を受け取ります。
    def __init__(self, vocab_count, class_count):
        # モデルを定義しています。中間層もない1層ネットワークです。
        super(MyRogistic, self).__init__(
            l1=L.Linear(vocab_count,class_count),
        )

    def __call__(self,x,y):
        return F.mean_squared_error(self.fwd(x), y)

    # 誤差関数はソフトマックスです
    def fwd(self,x):
        return F.softmax(self.l1(x))

次に入力データから単語の頻出数をカウントできるようにするためのメソッドを準備しておきます。判定に関係なさそうな単語は除外するようにします。

# 形態素解析を行うための関数を定義しておきます
def _mecab_parse_feat(feat):
    return dict(zip(_mecab_feat_labels, feat.split(',')))


def _mecab_node2seq(node, decode_surface=True, feat_dict=True,
                    mecab_encoding='utf-8'):
    # MeCab.Nodeはattributeを変更できない。
    while node:
        error_count = 0
        try:
            if decode_surface:
                node._surface = node.surface
            if feat_dict:  # 品詞の情報をdictで保存
                node.feat_dict = _mecab_parse_feat(
                node.feature
                )
            yield node
        except:
            error_count += 1
        node = node.next

#回帰の邪魔になるストップワードは除外するようにします。
def is_stopword(n):  # <- mecab node
    if len(n._surface) == 0:
        return True
    elif re.search(u'^[\s!-@\[-`\{-~ 、-〜!-@[-`]+$', n._surface):
        return True
    elif re.search(u'^(接尾|非自立)', n.feat_dict['cat1']):
        return True
    elif u'サ変・スル' == n.feat_dict['conj'] or u'ある' == n.feat_dict['orig']:
        return True
    elif re.search(u'^(名詞|動詞|形容詞)', n.feat_dict['pos']):
        return False
    else:
        return True


def not_stopword(n):  # <- mecab node
    return not is_stopword(n)


def node2word(n):  # <- mecab node
    return n._surface


def node2norm_word(n):  # mecab node
    if n.feat_dict['orig'] != '*':
        return n.feat_dict['orig']
    else:
        return n._surface


def word_segmenter_ja(sent, node_filter=not_stopword,
                      node2word=node2norm_word, mecab_encoding='utf-8'):
    if type(sent) == "unicode":
        sent = sent.encode(mecab_encoding)

    nodes = list(
        _mecab_node2seq(_mecab.parseToNode(sent), mecab_encoding=mecab_encoding)
    )
    if node_filter:
        nodes = [n for n in nodes if node_filter(n)]
    words = [node2word(n) for n in nodes]

    return words

日本語の構文解析に使うmecabを初期化します。辞書にはneologdを使用しています。

# 形態素解析の辞書にneologdを使用します
tagger =  MeCab.Tagger(' -Ochasen -d /usr/local/lib/mecab/dic/mecab-ipadic-neologd')
_mecab = MeCab.Tagger()
# 品詞,品詞細分類1,品詞細分類2,品詞細分類3,活用形,活用型,原形,読み,発音
_mecab_feat_labels = 'pos cat1 cat2 cat3 conj conj_t orig read pron'.split(' ')

学習データを初期化します。本番ではDBに保存されているデータをここで読み込むようにします。

# 学習用データ準備
text1 = """今までにない臨場感ですごい!効果ですけど値段に見合うだけの価値があります。"""

text2 = """ 昔見た近未来に一つ近づいたと思います。"""

text3 = """ 評価は高いようですが簡単に手を出せる価格ではありません。これを買うくらいなら別のVR製品を買ってもおまけが来ます。
正直信者が騒いでいるだけに思います。
"""

text4 = """値段は下がるはずなので今は待ちだと思います。遊ぶにもソフトが少ないですし。
"""

learn_docs = [
    word_segmenter_ja(text1),
    word_segmenter_ja(text2),
    word_segmenter_ja(text3),
    word_segmenter_ja(text4)
]

# 学習データをポジティブ0, ネガティブ1の何方かに分けるようにします
ans_index = [0,0,1,1]

# 語彙の数を求めるのに使う
def word_vocabularies(data):
    vocabularies = {}
    word_id = 0
    for doc in data:
        for word in doc:
            if word not in vocabularies:
                vocabularies[word] = word_id
                word_id += 1
    return vocabularies

# データ数
data_num = len(learn_docs)
# 分類するクラス数 今回はポジティブ、ネガティブの2値分類です
class_num = 2
word_vocab = word_vocabularies(learn_docs)
vocab_count = len(word_vocab)
learn_data = np.zeros(vocab_count * data_num).reshape(data_num, vocab_count).astype(np.float32)

# 学習時に使う入力データを初期化します
for i in range(len(learn_docs)):
    for word in learn_docs[i]:
        learn_data[i, word_vocab[word]] += 1.0

# 学習時に使う答えのデータを初期化します
learn_ans = np.zeros(class_num * data_num).reshape(data_num,class_num).astype(np.float32)
for i in range(data_num):
    learn_ans[i,np.int(ans_index[i])] = 1.0

ロジスティック回帰のモデルを初期化し、学習を行います。

# モデルを初期化します。
model = MyRogistic(vocab_count, class_num)
# パラメータの最適化アルゴリズムにAdamを使います
optimizer = optimizers.Adam()
optimizer.setup(model)

#学習します
# 学習データをランダムにサンプリングするために使います。
n = 4
# バッチサイズです。今回は学習データが4つしかないのでほとんど意味ないです
bs = 25
for j in range(5000):
    # インデックスをランダムにします
    sffindx = np.random.permutation(n)
    accum_loss = None
    for i in range(0, n, bs):
        # ミニバッチを取得
        x = Variable(learn_data[sffindx[i:(i+bs) if (i+bs) < n else n]])
        y = Variable(learn_ans[sffindx[i:(i+bs) if (i+bs) < n else n]])
        # 勾配を初期化
        model.zerograds()
        # 順方向に計算して誤差を取得
        loss = model(x,y)
        # 逆伝搬を行います
        loss.backward()
        # パラメータを更新します。今回はAdamを使っています。
        optimizer.update()
        # accum_loss = loss if accum_loss is None else accum_loss + loss バッチサイズで誤差を累計
    # accum_loss.backward() 逆伝搬
    # optimizer.update() パラメータ更新

学習によりモデルが最適化されたので、それを使って計算を行い正しい結果が得られるか確認します。(今回は学習データが4つなのであんまり意味ないと思いますが)

# パラメータ更新後に回帰を行ってみる
xt = Variable(learn_data, volatile='on')
# 準伝搬での計算結果を取得
yy = model.fwd(xt)

# 答え合わせ用のデータを初期化
ans = yy.data
nrow, ncol = ans.shape
ok = 0
ans_word=['posi', 'nega']
for i in range(nrow):
    # 分類結果の取得
    cls = np.argmax(ans[i,:])
    print(ans_word[cls])
    if learn_ans[i][cls] == 1.0:
        ok += 1
#正解数の表示
print (ok, "/", nrow, " = ", (ok * 1.0)/nrow)

学習をしたあとはモデル、辞書を保存します。

# 学習データの保存

# モデルの保存
# Save the model and the optimizer
print('save the model')
serializers.save_npz('sentiment.model', model)
print('save the optimizer')
serializers.save_npz('sentiment.state', optimizer)

# 辞書の保存
with open(  'vocab.dump', "w") as vocab_f:
    json.dump(str(word_vocab), vocab_f)

保存した辞書を読み込んでみます。

# 学習データの読み込み
load_vocab = {}
with open(  'vocab.dump', "r") as vocab_f:
    for data in json.load(vocab_f)[1:-1].split(","):
        pare_data = data.replace("'", "").split(":")
        load_vocab[pare_data[0].strip()] = pare_data[1].strip()

vocab_count = len(load_vocab)
class_count = 2
# load the model and the optimizer
load_model = MyRogistic(vocab_count, class_count)
load_optimizer = optimizers.Adam()
load_optimizer.setup(load_model)

print('load the model')
serializers.load_npz('sentiment.model',  load_model)
print('load the optimizer')
serializers.load_npz('sentiment.state', load_optimizer)

きちんと読み込まれているか試しに判定してみます。

# テストデータ
test_text1 = """今までにない臨場感ですごい!効果ですけど値段に見合うだけの価値があります。"""

test_text2=""" 評価は高いようですが簡単に手を出せる価格ではありません。これを買うくらいなら別のVR製品を買ってもおまけが来ます。
正直信者が騒いでいるだけに思います。
"""

test_doc = [
    word_segmenter_ja(test_text1),
    word_segmenter_ja(test_text2)
]

test_data = np.zeros(vocab_count *2 ).reshape(2, vocab_count).astype(np.float32)
for i in range(len(test_doc)):
    for word in test_doc[i]:
        if word in load_vocab:
            test_data[i,np.int(load_vocab[word])] += 1.0

xt = Variable(test_data, volatile='on')
yy = load_model.fwd(xt)

ans = yy.data
nrow, ncol = ans.shape
ans_word=['posi', 'nega']
for i in range(nrow):
    cls = np.argmax(ans[i,:])
    print(ans_word[cls])

chainerなら基本的なことは簡単に試せそうです。