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! ε=\_○ノ イヤッホーゥ!