Flask + SQLAlchemy + WSGIでちょっとはまったメモ

えがおと開発中に、自分のアホさでハマったので自戒をこめてメモ
# いくつか同様のネタがあるのでシリーズ化するかも(´・ω・`)

結論を先に書くと、DB接続セッションはちゃんとクローズしろ(#゚Д゚)ゴルァ!! ってことなんですが

構成

  • Apache + WSGIの構成で、Virtual Hostsによる複数ホストでWSGIdaemon modeで起動
    • 例えばa.hoge.comとb.hoge.comのWSGIアプリを別デーモンとして起動
  • a.hoge.comのFlaskアプリ(Aアプリ)とb.hoge.comのFlaskアプリ(Bアプリ) は同じDBを参照
  • DBの利用はSQLAlchemy(Flask-SQLAlchemy)を利用
    • DB定義のmodelはAアプリ、Bアプリとは別ファイルに記述したものを利用

現象

a.hoge.comで追加・変更したはずのデータがb.hoge.comに反映されない(´・ω・`)なぜ?ちなみにApacheを再起動すると反映される…なん、だ、と…( ゚д゚ )

原因と解決

Flaskの後処理プロセスの中でDBのセッションをクローズしていなかったのが原因みたいで、各アプリのafter_request(viewの後処理用プロセス)でdbのセッションをクローズするとOKだったみたい。

#####
# close db session
@app.after_request
def close_session(response):
    # DBのセッションをクローズ
    db_session.close()
    return response

...というより、マニュアルに書いてあるんだからちゃんとヤレ(#゚Д゚)ゴルァ!!ってことですね、ほんとゴメンナサイ


詳しい原因については、WSGIでセッションが永続化されたせいで、他セッションの変更状態を取得できなかったのが原因?もしやdb_session.commit()ではホントの意味でコミットされない?等という妄想どまできちんと理解出来ていないので、近日中にWSGIとかSQLAlchemyまわりのコードとドキュメント読まなきゃだめだな…と思っております。

あと、他のフレームワークでdbのクローズ処理をサボってもOKだったからといって、全ての環境でサボっていいわけではないのよってことも反省…etc

以上です

マニュアルをちゃんと読めばこんなミスをしないと思いますが、同様の現象でハマった人の一助になればこれ幸い

「えがおと」の人力フィルタリングについて解説するよ

"えがおと"の人力フィルタリングについて、id:hyukixに「受信メール手作業で登録してるの?(_´Д`)ノ~~オツカレー」という謂れのない激励を受けたのでちょっと解説してみますよ

フィルタリングの概要

今回実装した人力フィルタリングは、送られてきた写真・メッセージに不適切なものが含まれていないかどうか?という判定処理を人力で行っているだけで、各種メールデータのパースや地域データの取得については、ある程度の自動化をしています。

簡単に流れを書くとこんな感じです

1:メール受信 [自動]

MTAのプログラム連携機能を利用して、メールを処理モジュールに渡します

  • メールデータをパースして件名、本文、添付ファイル(画像)を取得
2:サムネイルを作成 [自動]

受信メールの添付ファイル(画像)を編集して、公開用の画像を作成します

  • 受信した画像ファイルを縮小して一覧表示用画像を作成
3:地域情報を取得 [自動]

Yahoo APIを利用して地域情報を取得します

  • 画像にExifデータ(GPSinfo)が含まれる場合は、Exifの緯度経度情報から逆ジオコーディング
  • 画像にExifデータが含まれない場合は、件名の文字列から地域情報を取得
4:各種データをDBに保存 [自動]

ここまで生成した各種データをDBに保存しますデス

5:保存された各写真を管理画面より確認して、公開・非公開を判定 ← 「ここが人力」
  • メッセージや画像回転などの、ちょっとした修正をする場合もあり
  • 公開処理時に地域情報を自動的に更新

…と、人力のフェーズは1箇所なので、徹夜で登録してるわけではないのです(´・ω・`)

地域情報の表記ゆれ対策

ちなみに、メール件名からの地域情報の取得は表記ゆれの対策として、次のようなAPI問い合わせを2度も行うというエコじゃない動きになっております…そのうち何らかの対処したいところですが…

  1. 件名の文字列(地域名)から緯度経度情報を取得(ジオコーディング)
  2. 取得した緯度経度から正式な地域名を取得(逆ジオコーディング)

お世話になっているAPI

地域情報取得の際に、都道府県データ自体はマップを作るっている手もありそうだったのですが、市町村その他の区域を検討するのが面倒だったので…とてもありがたく利用させてもらっています…デス

そんなわけで、えがおとをよろしくお願いします

上記のように半自動化された人力フィルタリングなので、お昼休みなんかのちょっとした時間で公開できるようになっておりますデス

そんなわけで、お送りいただいた写メールの公開はそんなにお待たせすることは無いはずです…ど忘れしないように頑張ります(´・ω・`)

Flaskでsjis対応のページを作成する方法

FlaskでUTF-8以外(例えばsjis)のサイトを作成するための自分メモを以下、羅列

  • after_request内でresponse.dataの内容を変換
  • response.headers.addでresponse_headerを適切なものに設定
  • sys.setdefaultencoding('sjis') でdefaultencodingをsjisに設定
    • スクリプト内でdefaultencodingを設定する場合はsysパッケージをreloadすること
    • あまり良くない方法みたいだけども(´・ω・`)
  • flaskファイル、templateともにutf-8でOK
例えばこんなふう?サンプル
@app.after_request
def convert_sjis(response):
    reload(sys)
    sys.setdefaultencoding('sjis')                                              
    response.headers.add('Content-Type', 'text/html; charset=shift_jis')
    response.data = response.data.decode('utf8').encode('sjis')
    return response

これで携帯サイトをFlaskで作れるようになった(`・ω・´)


参考:ぎじゅっやさん

「えがおと」を公開しました

3月11日の東北関東大震災で被災しましたが、比較的被害の少ない地域に住んでいたために無事でした。

被害が小さかったとはいえ会社は半壊し、被害の大きいフロアは立ち入り禁止になりました。また、電気・水道・ガスの各種ライフラインは数日にわたって断絶し、未だに復旧しきっておりません。

そんな中、各地よりたくさんの支援をしていただき、被害の小さい地域から徐々に復興し始めています。今回自分の周囲では被害が小さかったとはいえ、被災者となったことで自分はたくさんの人のおかげで生きていけているのだなぁ、とシミジミと思っております。

自分にできることは何かを考えました

その反面、支援をしてくれている皆様やより被害の大きい地域に対して、自分の立場から何か出来ないか?と真剣に考えてみました。

こういった立場から振り返ると、自分はプログラムが組める人間で、ソレを活用することでとりあえず何かできそうな気がしました。

ただ幸いなことに、安否確認や被害情報の共有、シェアハウスの斡旋などといったクリティカルなサービスは、震災直後からたくさんの人々が行なってくれています。

少しでも笑顔を広げたいと思いました

だから被災者という立場の自分は、被災地の復興状況の中でハッピーな気持ちになれるような風景を共有する仕組みを作ることにしました。

とてもひどい震災で、たくさんの悲惨な風景が連日報道されました。

こういった地震の恐ろしさを伝えることは重要だと思います。ただ、連日連夜悲惨なものを見せられれば、嫌でも気分が下向きになります。正直、明日への希望をブチおるような報道をいくつか見せられて、激昂したこともありました。

だから、もう少しハッピーを感じさせる風景を集めてみたいと思いました。

復興というプラスの気分になる風景を共有したい

今回実際に被災して、地震そのものの被害もそうですが復興するための日々がまた大変だということが身にしみました。その一方、例え順調にすすまなくても復興するための努力をする姿はとても素晴らしく、そしてハッピーな気分にさせるものだなと実感しました。

  • 例えば、数少ない在庫を全部ひらいて販売を再開するスーパー
  • 例えば、残り少ない食材やガスを使い切る勢いで限定メニューを提供する飲食店
  • 例えば、残りのプロパンガスを全て使ってシャンプーを提供する美容院

その他たくさんの人々が、今自分にできることを精一杯やっている姿はとてもありがたく、そしてハッピーな気分にさせてくれると素直に思いました。だから、そんな素晴らしい風景を共有できるサービス「えがおと」を作ってみました。

えがお

えがおと: http://egaoto.info/

使い方は、被災地の復興状況を感じさせるハッピーな風景を撮影して、egaoto@egaoto.infoに写メールするだけです。その際に件名に写真を撮影した地域名を入れてもらえると、分類できるのでちとありがたいです。

詳しい使い方は実際のページを御覧ください。ちょこちょこいじっているのでそちらが最新版になります。


正直、今すぐ必要なサービスではないでしょうが、笑顔が広がることが本当の復興につながることだと勝手に思っている、そんな自分にできることをやっていきたいと思ってはじめてみます。

Flask始めました

ここのところ忙しくてPythonScalaの両方を触る(主に心の)余裕がなさなさなので、スキマ時間を見つけてはPython製のマイクロフレームワークであるFlask触っておりますデス

Flaskを触るまで

当初はDjangoでイロイロとやろうとしてたのですが...

  • Djangoの枠組みに乗っかればいいところを無理やり俺俺ルール(User modelを自分で定義したいとか云々)でやろうとしたり...etc
  • そして当然のように発生した(自業自得的な)Djangoルール回避コードが増えてきたり...etc
  • なにかやるたびにDjangoの壁が立ちはだかるようになったり...etc

という、正直言ってDjangoを使いこなせていない…という状況が重たくなってきたところで、次のような記事を見かけて、これ幸い!とあっさりと乗り換えてしまったのです。

Flaskが好きな(暫定的で個人的な)6つの理由

以下、Flaskをお気に入りっている理由を羅列してみます、結論としてはコンパクトで必要十分ってことにまとまってしまうのですが…一応ソレっぽく書いてみますよ(`・ω・´)

独自ルールが少ない

乱暴に言ってしまえば、Flaskが提供するのはViewを呼び出すためのルールとその周辺ツールなので、ソレ以外に関するモロモロのルールは自分で定義することができます。

そのため、私のようなマイペース人間でも思いつきをフレームワークのルールによって遮られないので、実装のストレスがかなり少ないです…まあ、ここらへんは人間性にもヨルと思いますが…やりたいことを素直にやれないのはストレスになる性質なので(´・ω・`)

必要最低限のコンパクトなサイズ

マイクロフレームワークと銘うってあるとおり、Flask自身はとてもコンパクトサイズのため、必然的に覚える内容が少なくて済むのです。

また、これは個人的な相性の問題だと思うのですが、用意されている機能やツールが本当にかゆいところに手が届く親切設計だと思います…うん、Djangoにも同じような機能は用意されているから単純にDjangoを使いこなせていなかっただけかも知れないけども(´・ω・`)

ただ、この機能一覧をざらっと読む気になれる学習コストの低さはここしばらく求めていた手軽さだなぁ…と、一人で悦にはいっております。

拡張もできますよ

Flask自身はコンパクトでも追加モジュール的にExtensionsが多数用意されているので、足りない機能は補うことが出来ますデス(´・ω・`)

しかもPyPI対応しているものも多いのでsetuptoolsで楽ちんインストール出来ますね(`・ω・´)virtualenv使えば環境管理もお手軽ですし

オフィシャルをザザッと眺めると、たとえば次のようなExtensionsが用意されていますな

  • ORMapper : Flask-SQLAlchemy
  • Testing : Flask-Testing
  • Form : Flask-WTF
  • etc...

とりあえず上記のようなExtensionsを組み合わせるだけで、それなりに高機能な開発フレームワークにクラスチェンジ出来る感じです。

サイズが小さいのでソースコードを読みたくなる

まだ日本語を含めた情報が多くないので、つまずいたときには大元のソースコードにあたることが多いのですが、フレームワーク自体のサイズが大きくないので、余り苦にならないのデス

良いソースコードを読むのは勉強になるので、これはいい傾向ですな(`・ω・´)

それなりの規模にも対応できそう

機能のモジュール分割に関する仕組みも用意されているので、中規模程度のアプリケーションには十分対応できそうな感じです。設計次第では大規模アプリも作れるんじゃないかな?パフォーマンスでるのか試してみたいところですが(´・ω・`)

少なくてもモジュールに分割できるので、数人で共同開発なんていうことも十分可能な仕組みが提供されていますデス。

開発サーバ、スクリプト実行の機能もあるでよ

Djangoのように開発用の簡易サーバが組み込まれているので、ちょっとした動作試験が手元でできてしまうという親切設計

さらにはFlaskの仕組みをスクリプト実行するための仕組みも提供されているので、Webアプリとバッチ処理の組み合わせもスムーズに行えそうな感じです(`・ω・´)

こういう機能があるのは個人的にかなりありがたいです

そんな感じでちょこちょこいじっております

なかなか日本語の情報が見つからないですが、時には英語と格闘しつつ、時にはソースを読みつついじって楽しんでおりますデス

久しぶりに、チョットした時間ができたら無理やりにでもコード書いてる&思考の空き時間にFlask使ったコード設計を自然に行なっている、くらいお気に入りを見つけました(`・ω・´)

Flaskが気になった人は

Flaskオフィシャルの内容に沿ってHello Worldしてみてください、そしてQuick Startでお試しアプリを書いてみてください(`・ω・´)この手軽さはとても癖になりますよ(゚∀゚)

ちなみにドキュメントの日本語訳版はこちらにありました
http://a2c.bitbucket.org/flask/

個人的には素敵な相棒を見つけた気分

そんなわけで、ちょこちょこFlaskを使ったアプリを書いているところ(正確に言うとDjangoで作りかけてたものをFlaskに移植中)なので、進行形でボロボロでてきているちょっとしたつまずきや小ネタなんかを時間を見つけてブログっていきたいと思います(`・ω・´)

それではHave a nice Flask!

Scalaで集合知プログラミング その14


Scala de 集合知プログラミングの第3章を続けていきますよ(`・ω・´)

今回は階層的クラスタリングの結果をデンドログラムで表示するって課題をやってみますよ

デンドログラム

クラスタリングの結果を表現する樹形図(デンドログラム)を書いていきたいと思います(`・ω・´)

ちなみにデンドログラムはこんな感じの図になりますね
http://aoki2.si.gunma-u.ac.jp/lecture/misc/clustan.html

利用するのは前回生成したブログフィードによる階層的クラスタリングの結果です

それでは実際にデンドログラムを描画していってみますデス(´・ω・`)

図形描画の準備

デンドログラムを描画するための準備として、各クラスタの高さや幅を取得するためのメソッドを用意しますデス

まずはサンプルのPythonコードを写経してみますよ(`・ω・´)

# クラスタを再帰的にたどって高さを計算                                          
def getheight(clust):
  # 終端の場合は高さが1
  if clust.left == None and clust.right == None:
    return 1
  # 終端でない場合はソレゾレの枝の高さを再帰的に加算
  return getheight(clust.left) + getheight(clust.right)
         
# ルートノードへの距離としての階層の深さを計算
def getdepth(clust):
  # 終端の場合は距離は0
  if clust.left == None and clust.right == None:
    return 0
  # 終端でない場合は2つの枝の大きいほうに自身の距離である
  # 枝間の類似度を加える
  return max(getdepth(clust.left), getdepth(clust.right)) + clust.distance


そんなに複雑なものではないので、とりあえずScalaにサクっと翻訳してみます

// 縦の長さを計算
  def getheight(clust:Cluster):Double = clust match{
    // 終端の場合は1を返す
    case clust if clust.left == None && clust.right == None => 1
    // 終端以外の場合は枝分を合計した値を返す
    case clust => getheight(clust.left.get) + getheight(clust.right.get)
  }             
   
  // 階層クラスタの深さを計算
  def getdepth(clust:Cluster):Double = clust match{
    // 終端の場合は1を返す
    case clust if clust.left == None && clust.right == None => 1
    // 終端以外の場合は長い方の枝に自身の長さ(類似度)加えた長さを返す
    case clust => {
      (getdepth(clust.left.get) max getdepth(clust.right.get)) + clust.distance     }           
  }  

ここらへんのコードはだいぶ楽に翻訳できるようになってきましたな(´・ω・`)

背景の描画

次に実際にデンドログラムを描画する処理を記述します

描画の流れとしてはデンドログラム描画用の背景画像を作成→実際のノードを頭から再帰的に描画、という順序で進めていくことになりますデス

なので、まずは背景画像(白キャンバス)を作成してみます

まずはPythonコードの写経です(`・ω・´)

Pythonで図形の描画をしたり画像をゴニョゴニョするにはPILを使うのが一番楽にやれそうです

…ということでPILを使って白背景キャンバスを作ってみます

# PILをインポート
from PIL import Image, ImageDraw 

# デンドログラムを描画
def drawdendrogram(clust, labels, jpeg='cluster.jpg'):
  # 高さと幅を定義
  h = getheight(clust) * 20
  w = 1200
  # 階層の深さを定義
  depth = getdepth(clust)
         
  # 幅の縮尺を計算
  scaling = float(w - 150) / depth
         
  # 白を背景とした画像の台紙を作成
  img = Image.new('RGB', (w,h), (255,255,255))
  draw = ImageDraw.Draw(img)
  draw.line((0, h/2, 10, h/2), fill=(255, 0, 0))
         
  # ノードを描画(あとで実装)
  drawnode(draw, clust, 10, (h/2), scaling, labels)
  img.save(jpeg, 'JPEG')


Pythonコードで実際の描画の流れがわかったので、Scalaでも描画処理を書いてみます

Scalaで描画関連の処理をするにははGraphicsやGraphics2DなんかのJavaライブラリを使うのがヨサゲなので、そこら辺のお勉強がてらに頑張って書いてみますよ

とりあえず今回はより細かい画像操作が出来るよ!という触れ込みのGraphics2Dを使ってみました…でも、今回の内容だったらGraphicsでも十分だったと思います(´・ω・`)

ちなみに、各種使い方はここらへんを参考にしております

それではScala書いていきます

  // 必要なモジュールをインポート
  import java.awt.Image
  import java.awt.image.BufferedImage
  import java.awt.Graphics2D
  import javax.imageio.ImageIO
  import java.awt.Color
  import java.awt.geom._
  import java.io.File

  def drawdendrogram(clust:Cluster, labels:Option[List[String]],                
    jpeg:String="cluster.jpg"):Unit = {
    // 背景作成のための高さと幅を定義
    val h = getheight(clust) * 20
    val w = 1200
    // クラスタの深さを取得して縮尺を定義
    val depth = getdepth(clust)
    val scaling = (w - 150) / depth
                
    // 描画用のオブジェクトを作成
    val im = new BufferedImage(w.toInt, h.toInt, BufferedImage.TYPE_INT_RGB)
    var g = im.createGraphics()
    // 背景を描画
    g.setPaint(Color.white)
    g.fill(new Rectangle2D.Double(0, 0, w, h))
    g.drawImage(im, null, 0, 0)
                
    // ノードの描画を開始
    g = drawnode(g, clust, 10, (h / 2).toInt, scaling, labels)
                
    // ファイルに書き出し
    try {       
      ImageIO.write(im, "jpeg", new File(jpeg))
    }catch {    
      case e:Exception => println("image write error")
    }           
  }

うん、なんとか書き下せましたな(´・ω・`)俺がJavaドキュメントを漁る日が来るとは...

各ノードの書き出し

背景の書き出しができたので実際にクラスタノードを書き出す処理を書いてみます

流れとしては各クラスタの描画位置や高さ・幅を確認して、再帰的にラインを引いていく→終端に来たら名前(ラベル)を表示して終了、ってな感じで進みますね

それでは、まずはPython写経をしますよ(`・ω・´)

# クラスタノードの描画
def drawnode(draw, clust, x, y, scaling, labels):
  # 枝の場合はラインを描画
  if clust.id < 0:
    # 子クラスタの高さをゲット
    h1 = getheight(clust.left) * 20
    h2 = getheight(clust.right) * 20
    # 自分自身の位置情報を設定
    top = y - (h1 + h2) / 2
    bottom = y + (h1 + h2) / 2
    # 親クラスタまでの直線の長さを定義
    ll = clust.distance * scaling
         
    # クラスタから子に向けた垂直直線を描画
    draw.line((x, top + h1 / 2, x, bottom - h2 / 2), fill=(255, 0, 0))
    # 子クラスタに向けた水平直線を描画
    draw.line((x, bottom - h2 / 2, x + ll, bottom - h2 / 2), fill=(255, 0, 0))
    # 親クラスタに向けた水平直線を描画
    draw.line((x, top + h1 / 2, x + ll, top + h1 / 2), fill=(255, 0, 0))
         
    # 子クラスタの描画を再帰的に実行
    drawnode(draw, clust.left, x + ll, top + h1 / 2, scaling, labels)
    drawnode(draw, clust.right, x + ll, bottom - h2 / 2, scaling, labels)
         
  # 終端だった場合はラベルを描画
  else:  
    draw.text((x + 5, y - 7), labels[clust.id], (0, 0, 0))

うん、できました

それではこちらもScalaに翻訳してみますよ(`・ω・´)

  // クラスタの各ノードを描画する処理
  def drawnode(draw:Graphics2D, clust:Cluster, x:Int, y:Int,
    scaling:Double, labels:Option[List[String]]):Graphics2D = {
    // 枝の場合はラインを描画
    if(clust.id.get < 0){
      // 子クラスタの高さを取得
      val h1 = getheight(clust.left.get) * 20
      val h2 = getheight(clust.right.get) * 20
      // 自分の縦方向の位置を設定
      val top = y - (h1 + h2) / 2
      val bottom = y + (h1 + h2) / 2

      // 親クラスタまでの距離を設定
      val ll = clust.distance * scaling

      //// ノードの描画を開始
      draw.setPaint(Color.red)
      // クラスタから子に向けた垂直直線を描画
      draw.draw(new Line2D.Double(x, top + h1 / 2, x, bottom - h2 /2))
      // 子クラスタに向けた水平直線を描画
      draw.draw(new Line2D.Double(x, bottom - h2 / 2, x + ll, bottom - h2 /2))
      // 親クラスタに向けた水平直線を描画
      draw.draw(new Line2D.Double(x, top + h1 / 2, x + ll, top + h1 /2))

      // 子クラスタについて再帰的に実行
      drawnode(draw, clust.left.get, (x + ll).toInt, (top + h1 / 2).toInt, scaling, labels)
      drawnode(draw, clust.right.get, (x + ll).toInt, (bottom - h2 / 2).toInt, scaling, labels)

    // 終端の場合はラベルを描画
    }else{
      val label = if(labels != None) labels.get(clust.id.get) else clust.id.get.toString
      draw.setPaint(Color.black)
      draw.drawString(label, x, y)
    }
    return draw
  }

(´ε`;)ウーン…値の受け渡し方法がScala的に合っているのかわからんす

まあ、とりあえずできたってことで(´・ω・`)

実行して画像作成

それでは作成したコードでデンドログラムを描画してみます…が、描画結果の貼付けはめんどうなので省きます…ゴメンなさい(´・ω・`)

一応実行コマンドだけは載せておきますデス

  • Pythonでデンドログラムの描画
# クラスタリング結果を生成します
>>> import cluster
>>> b, w, d = cluster.readfile('blogdata.txt')
>>> clust = cluster.hcluster(d)

# デンドログラムを描画します
# 描画結果はcluster.jpgというファイルに出力します
>>> cluster.drawdendrogram(clust)
  • Scalaでデンドログラムを描画
// クラスタリングを行います 
scala> import org.plasticscafe.collective.cluster.Cluster._
scala> val (b, w, d) = readfile("blogdata.txt")  
scala> val clust = hcluster(d)      

// デンドログラムを描画します
// 描画結果はcluster.jpgというファイルに出力です
scala> drawdendrogram(clust, Option(b)) 

結果はjpegとして出力されますデス

まとめ

今回はクラスタリング結果をデンドログラムとして描画してみました(`・ω・´)

また、同時にPILとjava Graphics2Dをお勉強してみました(´・ω・`)

次回は列のクラスタリングとやらをやってみますが、今回とデータを転置するようなイメージですかね?まあ久方ぶりに頭を悩ませていますが、楽しんでやっていきますYO! ε=\_○ノ イヤッホーゥ!

Scalaで集合知プログラミング その13


Scala de 集合知プログラミングの第3章を続けていきますよ(´・ω・`)

今回は前回に引き続き階層的クラスタリングをやっていきますデス

階層的クラスタリング

前回までにはクラスタリングのための準備として以下の4項目を行って来ました

  • RSSを基にしたクラスタリング用のデータの作成
  • データファイルの読み込み処理
  • アイテム間類似度の定義と計算処理
  • クラスタ表現用クラスの定義

なので今回はこれらの定義を利用して実際のクラスタリングを行っていきたいと思います(`・ω・´)

それではPython de クラスタリング

まずはPythonコードを写経して、実際に階層的クラスタリングを行っていきます(`・ω・´)

具体的な手順としては全部の要素を比較→一番類似度の高いペアをグループ化、を延々と繰り返していくだけです…けっこう力技ですな(´・ω・`)

# clusters.pyに追記します

# クラスタのグループ化処理を定義します
def hcluster(rows, distance=pearson):
  distances = {}
  currentclustid = -1

  # 各行を初期クラスタとして定義
  clust = [bicluster(rows[i], id = i) for i in range(len(rows))]

  # クラスタリングの処理を開始
  while 1 < len(clust):
    # 探索条件の初期値として
    # クラスタ内の0番目と1番目が最も近いペアと仮定する
    lowestpair = (0, 1)
    closest = distance(clust[0].vec, clust[1].vec)

    # 全ての組をループして最も距離が近い組を探索
    for i in range(len(clust)):
      # 頭から2つ取り出して比較開始
      for j in range(i + 1, len(clust)):
        # 計算した距離をキャッシュして
        # 存在する場合はソレを使用する
        if (clust[i].id, clust[j].id) not in distances:
          distances[(clust[i].id, clust[j].id)] = distance(clust[i].vec, clust[j].vec)
        d = distances[(clust[i].id, clust[j].id)]

        # ピックアップした2つのペアの類似度が最も大きければ
        # 最近傍のペアとして保存
        if d < closest:
          closest = d
          lowestpair = (i, j)

    # 最近傍のペアの各ベクトル要素の平均値を取得
    mergevec = [
      (clust[lowestpair[0]].vec[i] + clust[lowestpair[1]].vec[i]) / 2.0
      for i in range(len(clust[0].vec))]

    # 新たなクラスたの作成
    newcluster = bicluster(mergevec, left=clust[lowestpair[0]],
                          right=clust[lowestpair[1]],
                          distance=closest, id=currentclustid)

    # 新しくつくられたクラスタのIDは負の数として管理する
    # カウントダウン(負の数的にインクリメント)
    currentclustid -= 1
    # 統合元のクラスタを計算用クラスタ群から削除
    del clust[lowestpair[1]]
    del clust[lowestpair[0]]
    # 統合先クラスタを計算用クラスタ群に追加
    clust.append(newcluster)

  return clust[0]

とりあえず実行してみましょうかね(´・ω・`)

# モジュールをインポートします
>>> import cluster
# データファイルを読み込んで各種データを取得します
>>> blognames, words, data = cluster.readfile('blogdata.txt')
# クラスタリングを行います
>>> clust = cluster.hcluster(data)
# とりあえず何かはできました
>>> clust
<cluster.bicluster instance at 0x9c8aecc>

とりあえずクラスタリングできたっぽい雰囲気です(´・ω・`)

Pythonクラスタリングの結果表示

計算されたクラスタ群はオブジェクトでしか確認できないので、生成されたクラスタ群を確認するための簡易表示メソッドを用意してやることにします(´・ω・`)

# クラスタリングの結果を確認表示する処理
# 再帰的に下にさかのぼって行きます
def printclust(clust, labels=None, n=0):
  # 階層表示するためのインデントを出力
  for i in range(n):
    # 改行しない場合は,を指定
    print ' ',
  
  # 負のidは作成したグループなので枝の途中
  if clust.id < 0:
    print '-'
  # 正のidは初期からあるアイテムなので末端として表示
  else:
    # ラベルの設定によって表示方法を変更
    if labels == None:
      print clust.id
    else:
      print labels[clust.id]
  
  # 右と左の枝を再帰的にたどって処理
  if clust.left != None:
    printclust(clust.left, labels=labels, n=n+1)
  if clust.right != None:
    printclust(clust.right, labels=labels, n=n+1)

それでは実際にクラスタリングの結果を表示してみますよ

>>> cluster.printclust(clust, labels=blognames)
-
  flagrantdisregard
  -
    -
      SpikedHumor - Today's Videos and Pictures
      -
        Hot Air  Top Picks
        Celebrity gossip juicy celebrity rumors Hollywood gossip blog from Perez Hilton
<以下省略>

うん、階層構造にクラスタリングできたっぽい雰囲気です(´・ω・`)

Scala de クラスタリング

それでは本題のScalaによるクラスタリングをしていきマス(`・ω・´)

とりあえずこんな感じで書きなおしてみました…再帰が最適化されているか不安ですが(´・ω・`)

  import scala.collection.mutable                                                           
  // クラスタリング処理を実行
  def hcluster(rows:List[List[Double]],
    distance:(List[Double], List[Double]) => Double = pearson):Cluster = {
    // アイテムを初期クラスタとして登録
    val clust = (for(i <- Range(0, rows.size)) yield { 
      Cluster(rows(i), id = Some(i)) }).toList
    // 類似度のキャッシュ用変数を準備
    val distances = mutable.Map.empty[(Int, Int), Double] 
    // 再帰的ループでクラスタリングを実行
    def clustring(clust:List[Cluster], cid:Int = -1):List[Cluster] = {
      // クラスタが一つになった時点で終了
      val length = clust.size
      if(length <= 1){ clust
      } else {
        // 一番類似度の高いペアを取得
        val closest = (for(i <- Range(0, length)) yield {
          clust.takeRight(length - i).map(x =>{ 
            if(!distances.contains((clust(i).id.get, x.id.get)))            
              distances((clust(i).id.get, x.id.get)) = distance(clust(i).vec, x.vec)
            (distances(clust(i).id.get, x.id.get), clust(i), x)
          })
        }).flatMap(x => x).filter(x => x._2 != x._3 ).toList.sort(_._1 < _._1).head
        // 類似度の高いペアをマージしてグループを作成
        // 初期アイテムと区別するためにIdは負の数
        val c1 = closest._2
        val c2 = closest._3
        val mergevec = (for(i <- Range(0, c1.vec.size)) yield {
          (c1.vec(i) + c2.vec(i)) / 2}).toList
        val c0 = Cluster(mergevec, Some(c1), Some(c2), closest._1, Some(cid))
        // 再帰処理をしますが、処理が長いので進捗表示つけます
        println(closest._2.id.get + " & " + closest._3.id.get + " is deleted")
        println(clust.size)
        clustring(c0 :: clust - closest._2 - closest._3, cid -1)
      }
    }
   // 処理を開始してルートクラスタを返す
    clustring(clust)(0)
  }

(´ε`;)ウーン…まだまだ効率化出来ていない気がする上に…きちんと動くのか不安です(´・ω・`)

とりあえず処理が出来るか確認したいので、表示用処理もScalaで書いてみますよ(`・ω・´)

// クラスタ表示用の処理です
  def printclust(clust:Cluster, labels:Option[List[String]], n:Int=0):Unit = {
    // 階層をインデントスペースで表現します
    for(i <- Range(0, n)) print(" ")
    // IDが負の数の場合はまとめたグループなので枝です
    if(clust.id.get < 0){
      println("-")
    // IDが正の場合はアイテムなので情報を表示します
    } else {
      // ラベルが存在する場合で表示を変更します
      if(labels == None) println(clust.id.get)
      else println(labels.get(clust.id.get))
    }
    // 左右の枝がある場合は再帰的に表示していきます
    if(clust.left != None) printclust(clust.left.get, labels=labels, n=n+1)
    if(clust.right != None) printclust(clust.right.get, labels=labels, n=n+1)
  }

んじゃ、実行してみます(´・ω・`)

// インポートしてみます
scala> import org.plasticscafe.collective.cluster._ 
// データソースを読み込みます 
scala> val (b,w,d) = Cluster.readfile("blogdata.txt")
b: List[String] = List(Joystiq, Neil Gaiman's Journal, Search Engine Watch Blog, The Superficial - Because You're Ugly, Online Marketing Report, TreeHugger, Joh
<省略>
// 階層的クラスタリングを実行します
scala> val clust = Cluster.hcluster(d)               
26 & 64 is deleted
91
2 & 27 is deleted
90
<省略>
// クラスタリングの結果を表示します(´・ω・`)
scala> Cluster.printclust(clust, Some(b))            
-
 -
  -
   -
    -
     ThinkProgress
     The Official Google Blog
    kottke.org
   -
    The Viral Garden
    A Consuming Experience (full feed)
  Google Operating System
 -

あれ?Pythonのときと結果が違うのはなんででしょ?

いくつかの処理ポイントがあるから、どこでひっかかってるのか(´・ω・`)わからん

...と、いうことで時間あるときに治す方向で一端ペンディングしますデス

追記

途中途中の中間データは同様の結果が出ているので、リストからの値の取り出し順あたりに差が出てきている雰囲気が…

根本的なリストの使い方に原因がありそうなので、少し時間をかけて見ていきたいと思います(´・ω・`)

まとめ

今回までで半ば無理矢理に階層的クラスタリングを試してみました

次回はこのクラスタリングの結果からデンドログラムを書きたいと思います

…が、頑張ります(´・ω・`)