負荷テストの学び(1)
負荷テストをしたことがないので、何回かにわけて負荷テストについて勉強します。
負荷テストとは
以下、IT用語辞典から引用したものです。
負荷テストとは、システムが実際の業務に耐えられる処理能力を持っているかどうかを検証するためのテストのことである。システムに対して擬似的に大量アクセスを発生させ、反応を検証することが負荷テストに当たる。
通常は何の異常もなく稼動しているシステムも、不意に膨大なトラフィックが舞い込んできたり、普段はないような負荷の高い処理がたくさん要求されたりして、システムに多大な負荷がかかると、システムは停滞するかハングアップするかして支障を来たすものがある。こうした異常の可能性は通常の運用では検証できないので、敢えて大量のアクセスをかけることによって検証される。負荷テストが行われることによって、アクセスがどれだけ増えるとレスポンスが低下したりシステムダウンしたりするのかを確認することができる。
負荷テストは、Webサーバーの公開前などの場合に行われることが多い。サーバーが充分なアクセスに耐えられないと判明された場合、メモリの増設やロードバランサーの設置などによって処理・通信の性能を向上が図られる。
サイトによって負荷テストの定義が違っていて、混乱ますね。
以下は、一番信頼できそうな(偏見)Oracleの資料を参考にしました。
負荷テストの目的
負荷テストは、システム開発プロセスにおける結合テストの最終フェーズで行われます。
「想定のユーザー数で耐えられるかどうか?」を調べたいならば、ユーザーのシナリオを作成して、同時実行し問題がないかどうか調べることになります。
しかし、ユーザーのシナリオを網羅することは不可能なため、要点を絞ってテストするようです。
4種類のアプローチ
性能テスト - スループット
一定時間内にどれくらいのユーザーがシナリオを実行することができるのか、または転送データ量のような数値的な指標を割り出すためのテスト。
性能テスト - 応答時間
通常時や高負荷時のシステムの応答性能に着目するテスト。
限界テスト
想像以上に処理限界に近いアクセスが発生した場合に、たとえ応答時間遅延や処理能力低下が起きたとしてもシステムが正常に動作できるかどうかを確認するためのテスト。
耐久テスト
長期間運用時に発生する問題(メモリリーク、ディスク容量不足)を、システムリソース監視を行って異常値を検出するテスト。
まとめ
Oracleの資料をもとに、負荷テストを学びました。
今まで、「負荷テスト = 性能テスト」だと思っていたので、とても勉強になりました。
次は具体的にどう実施するのか調べてみたいと思います。
ScalaNLPのchalk/topicsを眺めてみた
topicsには、LDA.scalaしかありません。
LDAとは
文書のトピックを決めるときに使われることがあります。
スマートニュースを想像してもらえばよくわかりますが、ニュースがスポーツだったり、テクノロジーだったりとトピックごとにわけられています。
LDAはこのようなシチュエーションで役に立ちます。
詳しくは以下のリンクを見ていただければと思います。
Latent Dirichlet allocation - Wikipedia, the free encyclopedia
Latent Dirichlet Allocation(LDA)を用いたニュース記事の分類
Wikipediaの最初の部分だけ和訳しておきます。
LDAでは、各文書が様々なトピックの混合として見なされます。これはpLSAと似ていますが、トピック分布はディリクレ事前分布を持っていることが想定されています。実際には、このことが文書内のトピックの混合をさらに合理的にしています。しかしながら、pLSAは一様なディリクレ事前分布のもとではLDAと等しくなります。
例えば、LDAモデルはCAT_relatedとDOG_relatedとして分類されるトピックを持っているとします。トピックはmilk, meow, kittenなど様々な語を生成する確率を持っています。これらの語はCAT_relatedと分類され、解釈されるかもしれません。当然、catという語自体はこのトピックに高い確率を持っています。同様に、DOG_relatedトピックはpuppy, bark, boneなどの語を生成する高い確率を持っています。
theのような機能語を除いた語は、クラス間で大体の確率を持っています。トピックは意味的にも認識的にもあまり強く定義されません。トピックは教師ラベルや共起の尤度に基づいたpuruningに基づいています。語はいくつかのトピックに異なる確率で現れます。
うーん、微妙ですが、文書がトピックを持っていて、語はトピックに紐付いていることがわかれば良いと思います。
LDA.scala
実際にコードを見てみます。
学習するための文書があるディレクトリ、トピックの数、LDAのハイパーパラメータを渡す必要があります。
case class Params(dir: File, numTopics: Int = 20, topicSmoothing: Double = .1, wordSmoothing: Double = 0.1) def main(args: Array[String]) = { val config = CommandLineParser.parseArguments(args)._1 val params = config.readIn[Params]("") ...
こんな感じ。
LDA.main(Array("--dir", getClass().getClassLoader().getResource("").getPath() + "com/hase1031/data/topics"))
内部では、まず初期化が行われ、イテレーションでパラメータを変更します。
イテレーションの回数はディフォルトで50となっています。
val lda = new LDA(params.numTopics, params.topicSmoothing, params.wordSmoothing) val model = lda.iterations(trainingData).tee(m => println(m.likelihood)).last
では、イテレーションで何をやっているか確認してみます。
最初に各単語をランダムに初期化しています。
その後、モデルを作って、そのモデルを利用してパラメータを更新します。
def iterations(data: IndexedSeq[SparseVector[Double]]): Iterator[Model] = { val numWords = data.head.size val termWeights = DenseMatrix.rand(numTopics, numWords) / numWords.toDouble Iterator.iterate(Model(termWeights, 0.0, numTopics, topicSmoothing, wordSmoothing)) { current => var ll = 0.0 ... } }.drop(1).take(numIterations)
モデルの定義は以下のようになっています。
さっきから出てきている、DenseMatrixやSparseVectorはBreezeで実装されているデータ構造です。
case class Model(termWeights: DenseMatrix[Double], likelihood: Double, numTopics: Int, topicSmoothing: Double, wordSmoothing: Double) { case class InferenceResult(topicLoadings: DenseVector[Double], wordLoadings: DenseMatrix[Double], ll: Double) def inference(doc: SparseVector[Double]) = { ... } }
このinferenceでは尤度を計算しているのですが、この尤度計算を本気で理解しようとするのは大変です。
ここではどんなことをしているのかを知るためにコードを載せるだけにします。
private def likelihood(doc: SparseVector[Double], theta: DenseVector[Double], gamma: DenseMatrix[Double]) = { val dig = digamma(theta) val digsum = digamma(sum(theta)) var ll = lgamma(topicSmoothing * numTopics) - numTopics * lgamma(topicSmoothing) - lgamma(sum(theta)) var k = 0 while(k < numTopics) { ll += (topicSmoothing - 1)*(dig(k) - digsum) + lgamma(theta(k)) - (theta(k) - 1)*(dig(k) - digsum) var i = 0 while(i < doc.activeSize) { val n = doc.indexAt(i) ll += doc.valueAt(i) * (gamma(k, i)*((dig(k) - digsum) - log(gamma(k, i)) + math.log(termWeights(k, n)))) i += 1 } k += 1 } ll }
そして、inference後にパラメータの更新を行います。
// m step: Beta = exp(digamma(counts) - digamma(\sum(counts)) counts += topicSmoothing val newCounts = digamma(counts) for(k <- 0 until numTopics) { newCounts(k, ::) -= digamma(sum(counts(k,::))) } // compute the rest of the likelihood (from the word counts) ll += numTopics * (lgamma(wordSmoothing * numWords) - numWords * lgamma(wordSmoothing)) ll += (wordSmoothing-1) * (newCounts.sum) ll -= ((counts - 1.0) :* (newCounts)).sum ll += lbeta(counts, Axis._1).sum exp.inPlace(newCounts) current.copy(newCounts, ll)
こんな感じです。数式が頭に入ってないと難しいですね。
数式は上で紹介したブログを見て理解していただければと思います。
このLDAはBLASを利用しているので、 実行するときは以下の引数を与えないと動かないことがあるのでご注意を。
sbt run -Dcom.github.fommil.netlib.BLAS=com.github.fommil.netlib.F2jBLAS
まとめ
LDAを理解するだけでも大変なのに、実装まで理解しようと思うのは大変ですね。
ですが、ブラックボックスで使わないように、最低限は理解したいですね。
ScalaNLPのchalk/text を眺めてみた
ScalaNLPのbreezeは以前紹介した通りですが、他のプロジェクトとしてchalkやnakがありました。
今回はchalkの中でもtext以下にあるプログラムがどんなことができるのか調べてみました。
ディレクトリを見ると、analyze、segment、tokenize、transformがあったので、1つずつ紹介します。
analyze
segment
JavaSentenceSegmenter
segmenterは、文分割してくれます。
Localeごとに区切り文字が違うらしく、EnglishとJapaneseで試してみました。
//segment val englishSegmenter = new JavaSentenceSegmenter(Locale.ENGLISH).apply("Hello, World. I'm Takayuki Hasegawa") for(sentence <- englishSegmenter) { println(sentence) } println("--") val japaneseSegmenter = new JavaSentenceSegmenter(Locale.JAPANESE).apply("こんにちわ。私の名前は長谷川です。") for(sentence <- japaneseSegmenter) { println(sentence) }
Hello, World. I'm Takayuki Hasegawa -- こんにちわ。 私の名前は長谷川です。
ちゃんとできてますねー。
tokenize
JavaWordTokenizer
Tokenizerは、文をトークンに分割します。
日本語の場合は単語分割でしょうか。
//tokenize val englishTokenizer = new JavaWordTokenizer(Locale.ENGLISH).apply("Hello, World. I'm Takayuki Hasegawa") for(sentence <- englishTokenizer) { println(sentence) }
Hello , World . I'm Takayuki Hasegawa -- こんにちわ 。 私 の 名前 は 長谷川 です 。
日本語でもできてますね。
WhitespaceTokenizer
文字通り空白区切りをしてくれます。
内部では、RegexSplitTokenizerに空白文字を指定することで実現しています。
こんな感じです。
class WhitespaceTokenizer() extends RegexSplitTokenizer("\\s+")
使うと以下のようになります。
val white = WhitespaceTokenizer("Hello, World. I'm Takayuki Hasegawa") for (w <- white) { println(w) }
Hello, World. I'm Takayuki Hasegawa
transform
RemoveRareFilter
レアワードフィルターは、閾値を決めて、レアかどうかを判断します。
class RemoveRareWords(threshold: Int = 10) { def apply[T, Obs <: Observation[Seq[T]]](data: Seq[Obs]) = { val c = Counter[T, Int]() for { d <- data w <- d.features.toSet[T] } { c(w) += 1 } for (d <- data) yield for (seq <- d) yield for (w <- seq if c(w) >= threshold) yield w }
まとめ
至る所でJavaの力を借りていて、あまりScalaならではって感じじゃなかった気がします。
単語分割は、品詞などを知りたいならば、MeCabを使わなければなりませんね。
一応、今回試してみたコードのレポジトリをあげておきます。hase1031/chalk-example
MeCabを使ってみた
以前、C++, PythonからMeCabを使ったことがありますが、JavaやScalaから使ったことがなかったので、使ってみました。
mecab-javaの利用
mecab本体のインストールは他の記事を参照してください。
当初、Sbtの設定に一行だけ加えるだけで使えるようになるんじゃないと思ってました。
こんな感じ。
libraryDependencies += "org.chasen.mecab" % "mecab-java" % "0.993"
しかし、以下のエラーが解決できず諦めました。
java.lang.UnsatisfiedLinkError: org.chasen.mecab.MeCabJNI.new_Tagger__SWIG_0(Lja va/lang/String;)J at org.chasen.mecab.MeCabJNI.new_Tagger__SWIG_0(Native Method) at org.chasen.mecab.Tagger.<init>(Tagger.java:140) at com.hase1031.MecabScalaUsingMecabJava$delayedInit$body.apply(MecabSca laUsingMecabJava.scala:7) at scala.Function0$class.apply$mcV$sp(Function0.scala:40) at scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala: 12) at scala.App$$anonfun$main$1.apply(App.scala:71) at scala.App$$anonfun$main$1.apply(App.scala:71) at scala.collection.immutable.List.foreach(List.scala:318) at scala.collection.generic.TraversableForwarder$class.foreach(Traversab leForwarder.scala:32) at scala.App$class.main(App.scala:71) at com.hase1031.MecabScalaUsingMecabJava$.main(MecabScalaUsingMecabJava. scala:5) at com.hase1031.MecabScalaUsingMecabJava.main(MecabScalaUsingMecabJava.s cala) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl. java:57) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAcces sorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:606) [trace] Stack trace suppressed: run last compile:run for the full output. java.lang.RuntimeException: Nonzero exit code: 1 at scala.sys.package$.error(package.scala:27) [trace] Stack trace suppressed: run last compile:run for the full output. [error] (compile:run) Nonzero exit code: 1 [error] Total time: 4 s, completed 2013/12/15 11:55:38
nativeメソッドを呼べなくて困っているらしいが、解決方法がわからず…
結局、mecab-javaをダウンロードしてきて、makeしたあとに、MeCab.jarとlibMeCab.soをlib以下に移動させることで利用しました。
hase1031/mecab-scala-using-mecab-java
実行するとこんな感じ。
すもも スモモ すもも 名詞-一般
も モ も 助詞-係助詞
もも モモ もも 名詞-一般
も モ も 助詞-係助詞
もも モモ もも 名詞-一般
の ノ の 助詞-連体化
うち ウチ うち 名詞-非自立-副詞可能
EOS
まとめ
やっぱり、Sbtの設定だけでどうにかしたかったなー。
もうちょっと知識があったらできたのかな?
Jubatusを使ってみた
前回、Scalaで機械学習などをサポートすることを目的としたScalaNLPを紹介しました。
しかし、ScalaにはJavaの資産を使えるというメリットがあるので、今回はJavaを利用して、PFIが開発しているリアルタイム解析基盤Jubatusを使ってみました。
Jubatusについて
Jubatusは「分散したデータ」を「常に素早く」「深く分析」することを狙った分散基盤技術です。
Jubatusの名前の由来は、俊敏な動物であるチータの学術名からの命名で、「ユバタス」と読みます。株式会社Preferred InfrastructureとNTTソフトウェアイノベーションセンタが共同開発した、日本発のオープンソースプロダクトです。
最終的に全ての人にスケーラブルなオンライン機械学習フレームワークを提供することがJubatusの目標です。
Jubatus は以下の特徴を持ったオンライン機械学習向け分散処理フレームワークです。
オンライン機械学習ライブラリ: 多値分類、線形回帰、推薦(近傍探索)、グラフマイニング、異常検知、クラスタリング
特徴ベクトル変換器 (fv_converter): データの前処理と特徴抽出
フォルトトレラントな分散機械学習のためのフレームワーク
Jubatusのインストール
私の環境はCentOSです。他の環境の人も公式を見れば簡単にインストールできると思います。
sudo rpm -Uvh http://download.jubat.us/yum/rhel/6/stable/x86_64/jubatus-release-6-1.el6.x86_64.rpm sudo yum install jubatus jubatus-client
Jubatusを利用するクライアントも提供されています。
C++, Python, Ruby, Javaが書かれています。
…
…
あれ、Scalaないな?
本来ならば、「Scalaクライアントを実装してみた」と言いたいところですが、それは大変なので、Javaクライアントを利用して使ってみます。
使ってみる
Sbtを利用してコンパイルするスケルトンがないので、ほぼJavaクライアントをパクって自分で用意しました。
hase1031/jubatus-scala-using-java-client
このクライアントの初期コードでは、ユーザーが3人いて、どのユーザーがどのユーザーに似ているかを判別するプログラムが用意されています。
// user01 val d1 = new Datum().addNumber("movie_A", 5).addNumber("movie_B", 2) .addNumber("movie_C", 3) r.updateRow("user01", d1) // user02 val d2 = new Datum().addNumber("movie_A", 2).addNumber("movie_B", 5) .addNumber("movie_C", 1) r.updateRow("user02", d2) // user03 val d3 = new Datum().addNumber("movie_A", 5).addNumber("movie_B", 1) .addNumber("movie_C", 4) r.updateRow("user03", d3)
コードを見てわかるように、映画Aと映画Cを高評化しているのは、ユーザー1とユーザー3。
逆に、ユーザ−2は映画Aと映画Cを低評価、映画Bを好評化しています。
Jubatusの起動
jubarecommender --configpath /usr/share/jubatus/example/config/recommender/lsh.json &
Configのパスは人によって異なります。
クライアントの実行
sbt run
user01 is similar to: user01 (1.0), user03 (0.921875), user02 (0.765625), user02 is similar to: user02 (1.0), user01 (0.765625), user03 (0.6875), user03 is similar to: user03 (1.0), user01 (0.921875), user02 (0.6875)
ユーザー1とユーザー3がよく似ているという結果がでました。
簡単!
まとめ
Jubatusは今回のリコメンド以外にも分類だったり、回帰だったりといろいろできるので試していきたい。
Javaの資産を使えるのはありがたいですね。
ScalaNLPのQuickStartをやってみた
ScalaNLPとは
ScalaNLPには、BreezeとEpicという2つのプロジェクトがある。
Pythonにはnumpyやscipyという便利なライブラリがあるが、Scalaでそれに匹敵するものを作ろうとしているっぽい。
Scala環境構築
Macを使っている人はhomebrewを使えばすぐにscalaとsbtをインストールできるだろうし、Windowsの人はmsiをダウンロードしてくるのがはやいと思う。
ちなみに私はWindowsのGitbashを使って進めていく。
breezeのルートディレクトリで、sbt console が動くようにしておくこと。
Linear algebra
まずは線形代数パッケージを使ってみる。
scala> import breeze.linalg._
ベクトル
scala> val x = DenseVector.zeros[Double](5) x: breeze.linalg.DenseVector[Double] = DenseVector(0.0, 0.0, 0.0, 0.0, 0.0)
ここで作られたベクトルは列ベクトルで、行ベクトルにするには.tをする必要がある。
scala> val y = DenseVector.ones[Int](5).t y: breeze.linalg.DenseMatrix[Int] = 1 1 1 1 1
もちろんSparseVectorもある。
scala> SparseVector(2, 0, 3, 2, -1) res2: breeze.linalg.SparseVector[Int] = SparseVector((0,2), (1,0), (2,3), (3,2), (4,-1))
アクセスはListとかと同じ。
scala> x(0) res3: Double = 0.0 scala> x(1) = 2 scala> x res5: breeze.linalg.DenseVector[Double] = DenseVector(0.0, 2.0, 0.0, 0.0, 0.0)
スライスして代入することもできる(Range型が速いって強調されてる)
scala> x(3 to 4) := .5 res6: breeze.linalg.DenseVector[Double] = DenseVector(0.5, 0.5) scala> x res7: breeze.linalg.DenseVector[Double] = DenseVector(0.0, 2.0, 0.0, 0.5, 0.5)
行列
scala> val m = DenseMatrix.zeros[Int](5,5) m: breeze.linalg.DenseMatrix[Int] = 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
scala> (m.rows, m.cols) res9: (Int, Int) = (5,5) scala> m(::,1) res10: breeze.linalg.DenseVector[Int] = DenseVector(0, 0, 0, 0, 0) scala> m(4,::) := DenseVector(1,2,3,4,5).t res11: breeze.linalg.DenseMatrix[Int] = 1 2 3 4 5 scala> m res12: breeze.linalg.DenseMatrix[Int] = 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 2 3 4 5
scala> m(0 to 1, 0 to 1) := DenseMatrix((3,1),(-1,-2)) res13: breeze.linalg.DenseMatrix[Int] = 3 1 -1 -2 scala> m res14: breeze.linalg.DenseMatrix[Int] = 3 1 0 0 0 -1 -2 0 0 0 0 0 0 0 0 0 0 0 0 0 1 2 3 4 5
他にもいろいろな操作方法がある。
scala> m + m res15: breeze.linalg.DenseMatrix[Int] = 6 2 0 0 0 -2 -4 0 0 0 0 0 0 0 0 0 0 0 0 0 2 4 6 8 10 scala> m :* m res16: breeze.linalg.DenseMatrix[Int] = 9 1 0 0 0 1 4 0 0 0 0 0 0 0 0 0 0 0 0 0 1 4 9 16 25 scala> m.sum res19: Int = 16 scala> m.max res20: Int = 5 scala> m.argmax res21: (Int, Int) = (4,4)
breeze.stats.distributions
Breezeは多くの確率分布を扱っている。
scala> import breeze.stats.distributions._
まずはポアソン分布。以下は平均が3.0となる。
val poi = new Poisson(3.0) poi: breeze.stats.distributions.Poisson = Poisson(3.0)
10個サンプリングしてみる。
scala> poi.sample(10) res24: IndexedSeq[Int] = Vector(3, 2, 6, 3, 3, 1, 4, 8, 2, 4) scala> res24 map { poi.probabilityOf(_) } res25: IndexedSeq[Double] = Vector(0.22404180765538775, 0.22404180765538775, 0.05040940672246224, 0.22404180765538775, 0.22404180765538775, 0.14936120510359185, 0.16803135574154085, 0.008101511794681432, 0.22404180765538775, 0.16803135574154085)
サンプルから平均と分散を出してみる。
scala> val doublePoi = for(x <- poi) yield x.toDouble doublePoi: breeze.stats.distributions.Rand[Double] = breeze.stats.distributions.Rand$$anon$11@9c5468 scala> breeze.stats.DescriptiveStats.meanAndVariance(doublePoi.samples.take(1000)) res26: (Double, Double) = (2.9630000000000036,2.862493493493491)
ちなみに本当の平均と分散は、
scala> (poi.mean, poi.variance) res30: (Double, Double) = (3.0,3.0)
breeze.optimize
最適化パッケージで、いくつかの凸最適化ルーチンと線形計画問題のソルバーがある。
import breeze.optimize._
DiffFunctionで放物線を定義できるらしいが、以下が何をやっているのかよくわからない。
あとで詳しくソース見る。
scala> val f = new DiffFunction[DenseVector[Double]] { | def calculate(x: DenseVector[Double]) = { | (norm((x - 3.) :^ 2.,1.),(x * 2.) - 6.) | } | } f: breeze.optimize.DiffFunction[breeze.linalg.DenseVector[Double]] = $anon$1@4d23bc scala> f.valueAt(DenseVector(0,0,0)) res32: Double = 27.0 scala> f.valueAt(DenseVector(3,3,3)) res35: Double = 0.0 scala> f.gradientAt(DenseVector(3,0,1)) res36: breeze.linalg.DenseVector[Double] = DenseVector(0.0, -6.0, -4.0) scala> f.calculate(DenseVector(0,0)) res37: (Double, breeze.linalg.DenseVector[Double]) = (18.0,DenseVector(-6.0, -6.0))
関数の導関数を近似することもできるらしい。
scala> def g(x: DenseVector[Double]) = (x - 3.0):^ 2.0 sum g: (x: breeze.linalg.DenseVector[Double])Double scala> g(DenseVector(0,0,0)) res39: Double = 27.0 scala> val diffg = new ApproximateGradientFunction(g) diffg: breeze.optimize.ApproximateGradientFunction[Int,breeze.linalg.DenseVector[Double]] = <function1> scala> diffg.gradientAt(DenseVector(3,0,1)) res40: breeze.linalg.DenseVector[Double] = DenseVector(1.000000082740371E-5, -5.999990000127297, -3.999990000025377)
他にもあるけど、あまり理解できなくなったので、省略。
まとめ
使いこなせればかなり便利!
しかし、ソースを見ると、関数型でかかれているところが少なかったりと、まだまだ開発途中のプロジェクトという印象を受けた。
もっと使って情報を発信していきたい。
さくらVPSにRedmine
チュートリアルに従っていけば基本問題なし.
一箇所だけエラーが出た.
jsonをインストールするときに以下のエラーが出た.
/usr/bin/ruby extconf.rb mkmf.rb can't find header files for ruby at /usr/lib/ruby/ruby.h
これはruby-develをインストールしていないせいなので
yum install ruby-devel
でOK!
あとgemパッケージのインストールが終わると<= 1.8.6 : unsupported
= 1.8.7 : gem install rdoc-data; rdoc-data --install
= 1.9.1 : gem install rdoc-data; rdoc-data --install
>= 1.9.2 : nothing to do! Yay!
って書いてあって、なごむ