n-gramとマルコフ連鎖の理論と実装、もちろんScalaを使って。
n-gramとは
n-gramとは形態素解析のように辞書を使って文章を品詞分解していくのではなく「文字単位で文章を分割していく」手法のこと。
なので、形態素解析のように辞書を必要とするわけではない。
またn-gramのnが何を指すかというとn文字で分割するという意味である。
1-gramは1文字ずつ分割する。
2-gramは2文字ずつ分割する。
3-gramは3文字ずつ分割する。
…
みたいに。
実際には次のように分割していく。
文章:今日は雨のようだ。 条件:n = 2とする 分割: 今日 日は は雨 雨の のよ よう うだ だ。
ただここで問題なのがnが小さすぎるとその自然言語にもよるが不適切な結果を生むことに成る。
文章:東京都庁 条件:n = 2 分割: 東京 京都 都庁
n-gramを使って文字列分割を行うと本来想定している最適解とは異なる場合がある。
今回で言えば、「東京都庁」という意味を持つのに本来の意味とかけ離れた「京都」が出てしまうなど。
そこで日本語の文章を分割するときはn=3を想定するっぽい。
簡単故にこれを実際の会話で使う文章生成に使うと割と精度が下がる。
n-gramのサンプル
今回は、「青空文庫」さんから夏目漱石の「こころ」をお借りしました。
事前にutf-8にしてあれこれ邪魔な記述を抜かして使用します。
これに対してn = 3の時のn-gram、つまりtri-gramで分割しました。
ソースコードは次の通り。
import scala.io.Source import scala.io.BufferedSource import scala.collection.mutable.Map object Ngram{ //3文字ずつ分割していく private [this] val n:Int = 3 def main(args:Array[String]):Unit = { //文字列とその出現頻度を格納 var ngramMap:Map[String,Int] = Map[String,Int]() //ファイルを読み込む val source = Source.fromFile("kokoro.dat","utf-8") //1行ずつ読み込む val lines:Iterator[String] = source.getLines //ngramで分割し格納 for(line <- lines){ var tmp_line:String = line while(tmp_line.length >= this.n){ var subString:String = tmp_line.take(this.n) //切り出した文字列が今までに出てないなら追加 //出現しているなら出現回数の更新 if(!ngramMap.contains(subString)){ ngramMap += (subString -> 1) }else{ var count:Int = ngramMap(subString) + 1 ngramMap.update(subString, count) } //先頭文字の除去 tmp_line = tmp_line.tail } } val result_key:Iterable[String] = ngramMap.keys for(key <- result_key){ println(s"${key} ${ngramMap(key)}") } } }
これをコンパイルして次のように実行する。
$ scala Ngram | sort -r -nk2 | head
そうすると、よく使われる部分文字列ベスト10が切り出せた。
部分文字列 | 出現回数 |
した。 | 1464 |
ました | 1026 |
った。 | 856 |
です。 | 789 |
のです | 776 |
かった | 593 |
たので | 562 |
なかっ | 470 |
ってい | 397 |
奥さん | 386 |
語尾とか接続詞的なあれこれは頻度高いのわかるが「奥さん」…。
マルコフ連鎖とは
マルコフ連鎖とは、ある事象がその直近の事象のみに影響を受けて確率的に変化するという考えかた。
これにはマルコフ性という、次に起こる事象が過去に起きた事象の影響を受けないという性質がなければならない。
もっと言えば、マルコフ性を備えた確率過程のことをマルコフ過程といい、その中でも状態空間が連続ではなく離散的である場合を特にマルコフ連鎖という。
身近な例を上げるなら、すごろくや人生ゲームといったものが一種のマルコフ連鎖の例と言える。
n-gramとマルコフ連鎖を使った文章生成
本来は確率を使ってどうこうするけど、Scalaでそれをやってしまうと処理速度的に心配なので次に出てくる文字列をひたすら追加しそこからランダムで選択することにした。
それが次のコード
import scala.io.{Source,BufferedSource} import scala.collection.mutable.{Map,ArrayBuffer} import scala.util.Random object Markov{ //tri-gramを想定 private [this] val n:Int = 3 def main(args:Array[String]):Unit = { //ngramの分割結果と次に続く文字列を格納する var ngramMap:Map[String,ArrayBuffer[String]] = Map[String,ArrayBuffer[String]]() //ファイルから一行ずつ読み込む val source:BufferedSource = Source.fromFile("kokoro.dat","utf-8") val lines:Iterator[String] = source.getLines //ngramで分割 for(line <- lines){ var tmp_line:String = line var split_str:String = tmp_line.take(this.n) //先頭文字列だけ独立して追加 if(!ngramMap.contains(tmp_line.take(this.n))) ngramMap += ( split_str -> ArrayBuffer.empty[String] ) tmp_line = tmp_line.tail while(tmp_line.length >= this.n){ var ngramArray:ArrayBuffer[String] = ngramMap(split_str) ngramArray += tmp_line.take(this.n) ngramMap.update(split_str,ngramArray) if( !ngramMap.contains(tmp_line.take(this.n)) ) ngramMap += (tmp_line.take(this.n) -> ArrayBuffer.empty[String]) split_str = tmp_line.take(this.n) tmp_line = tmp_line.tail } } //先頭文字列選択を行う val rnd:Random = new Random var result_str:String = ngramMap.keys.toList(rnd.nextInt(ngramMap.size)) var tmp_str:String = result_str while(!result_str.contains("。")){ var next_wordArray:ArrayBuffer[String] = ngramMap(tmp_str) tmp_str = next_wordArray(rnd.nextInt(next_wordArray.size)) result_str += tmp_str.last } println(result_str) } }
これを実行することによって得たいくつかの文章は次の通り
ひどい文もできていたので比較的ましなものを選択した。
叔父だの叔母だの、その性質としてよかった。 耳に異様な調子をもっと底の方に沈んだ心を大事そうに聞いた。 端書を受けない事はできないかと疑ってみました。 かも彼らは、退屈のために箒で背中を向けた。 今東京へ出て来たといって褒めるのですから、私には分りませんでした。 生と異なる点を明白に述べなければ純白でなくっちゃなるまですでに妻を迎えたらしい。 父に万事をやっ付けて離さない悠長な田舎にありませんでした。 思うかと、奥さんをどうしてもいいました 酸にも聞こえる試しはまるで頼りにするのは定めていた私が、それは迷惑そうな顔をちょっと顔だけ向け直して来て、いつの間にか暮れて春になりました。 まだ起きているのであった。
ngramだと単語が意味を持たないので、計算コストは低いけど出来上がる文が時折変な文になってしまう。
マルコフ連鎖を採用しているのも原因の一つであるけど。
Oracle Code Japan Tour in Sapporoに行ってきた
はじめに
Oracle Code Japan TourがSapporoにくるとのことで初めて情報系のイベントに参加してみた。
というわけで実際に得た情報をあれこれまとめてみた。
すごいメモのような感じになっているので間違ってたらコメントなどくれると嬉しい。
Feed Back from Java Day Tokyo 2017
Java + Dockerが現在利用できるようになっている。
javaだけでなくoracleの製品も利用できるようになった。
java ee8修正案としてhttp2の実装がありになったりリアクティブプログラミングができるようになったりしていくとのこと
非同期と同期を組み合わせて高速化を図る。
javaの最新情報は以下にある
builder.japan.zdnet.com
Raspberry Pi with Java 9
jlinkというコマンドの話
必要なモジュールなどのみでjreを作成できるコマンド
ラズパイみたいにリソースが限られている時に有効
java se9ではhttp2のあれこれが乗っかる。
se9ではarm v5とarm v6(raspberry pi a,b,b+,zero)には未対応
そこでjavaを使いたいなら8以下のものを使うひつようが出てくる。
javafxをiphone,androidで使いたくなったらgluonが開発してるヤツを使える。
gluonhq.com
javaでラズパイのあれこれを動かすためのプロジェクトがあるためハードウェア制御などをjavaでも行うことができる。
ここではそのデモをみて大半が終わってしまった。
ただラズパイ上でjavaを動かすのも楽しそうだと思った。
Cloud Native Java EE
ここはjava ee使わんしネットワークとかあれこれやってないから普通に話聞いて面白いなぐらいしか感想がない。
ただdockerと組み合わせるのが面白かった。
あと
JOnsen – Java Unconference at a Japanese Onsen
っていうアンカンファレンスの話聞けてよかった。
Java SE 9のススメ
ここではjava9での互換性、新機能、改善点などについての話だった
互換性
java9で互換性がなくなったものがいくつかある
言語仕様とライブラリ
java se8ではアンダースコアを1文字変数名として使用できたが警告がでる状況だったが、se9からはアンダースコア1文字だけではコンパイルエラーとなる。
@deprecatedの付いているメソッドについて、今まではずっと残ってきたものでも本当になくなる可能性があるとのこと。
特にGUI関連のものや、OSに比較的依存したものがJigsawプロジェクトによりブラックオックス化される可能性が高いそう。
保守、運用
virtual VM,hprof,jhatなどなどのツールがno moreとなってしまう。
デフォルトのGCが現行のものだとParallel GCなところがG1GCに変更になる。
またそれ以前のCMSはdeprecatedになり今後のアップデートで消える可能性がある。
CMSとG1GCでは使えないのでゴリゴリチューニングしている人は注意が必要。
ただしメモリサイズが小さいマシンなどではparallel GCを使うのがよい。
新機能
今回は大きく分けて3つ
- Project Jigsaw
- JShell
- Reactive Streams
が紹介された。
Project Jigsaw
背景:大規模システムになるとクラスパスが300個近くになることがあるが、実際バージョン違いだけだったりするのにこれはどうなのという問題
俗に言うjar hellに陥る問題。
ライブラリ更新で動作不良を起こしたり、publicがpublicすぎる問題などなど。
これらを辿って行くと依存性の欠如であったり、公開範囲を適切に設定できないなどなど。
これをモジュールにして解決しようというのがProject Jigsaw
モジュールに関して
現行のjarはほぼそのまま使える。
jarファイル中にメタ情報を付加する。
それらを管理するmodule-info.javaの有無が現行とse9のおおきな違い。
クラスパスが消えてモジュールパスに統一される可能性がある。
その他としてjava.baseにjava.langやjava.utilなどが統一されScalaっぽくimportしなくても使えるみたい
依存性の調査は
jdeps -s ファイル名.jar
でそのファイルが使ってるモジュールを表示してくれる。
それをもとにモジュールのあれこれを作成できるようになる。
JShell
javaのreplツール
jshellコマンドを打つと使用できるようになる。
ちなみに行末の;を省略してもいい
同じ変数名で別のデータ型の変数を宣言すると一番最後に変更したものに置換される。
Reactive Streams
非同期処理のためのインターフェース
http://www.reactice-streams.org
back pressureがあるタイプのPublish-Subscribeモデル。
subscriberがpublisherに要求するとsubscriptionが独自に生成されデータがどれだけ通過するか制限できるだか把握できるだか
ココらへんはあまり実感がわかなかった
改善
project coin
stream,collection
Stream
ファクトリメソッドとして
- ofNullable
- iterate
が追加される。
そのほかのメソッドでは
- take/dropWhile
^ Collectors.flatMapping
- Collectors.filtering
が追加され関数型のあれこれが割と使えるようになる。
Optionalはstream,flatMapと併用を前提にした設計になる。
ifPresentOrElseが追加され、値がなかった時(Else)にどうするか記述できるようになる。
orの中にoptionalを書けるようになる。
ofメソッドも追加され
List<Integer> list = List.of(1,2,3)
と実装できる。
これはイミュータブルとして生成される。
その他
javadocがhtml5に対応。
検索窓が追加されるがデフォルトで検索窓にフォーカスがあてられるためスペースでしたにいけなくなった。
リリースは2017/09/21に変更になった。
JDK 9 Early-Access Builds
ここで最新情報をあされる。
日本語版NetBeans8.2をUbuntuにいれてみた
なぜかNetBeansをいれることになった
友達曰く、「NetBeansが英語なのとプロジェクトを開けない」とのことで実際に検証してみた。
普段はVSCodeかVimとsbtでScala,Javaしか書かない人間のUbuntuにはそんな便利なもの入ってるわけもなく
実際に導入しながらそれらを試してみた。
NetBeans日本語化できないのか?
最初から日本語化されてるのあったっぽい
インストールした
次のコマンド打ったらこんな出力でた
$ sh netbeans-8.2-javase-linux.sh インストーラを構成しています... システムでJVMを検索しています... インストール・データを抽出しています... インストーラ・ウィザードを実行中...
これでインストーラが起動したので
どんどん次へなり、進むなりを押していく
すると最高に嫌な予感がする。
こんなのが出てきた。
これ無視し続けて、なんとかインストールすると
起動はするが、プロジェクト開くときにこんなのがでた
まぁ開けないわな。
開き方を変えた
$ cd netbeans-8.2
そこでさらにbinディレクトリに移動する。
~/netbeans-8.2$ cd bin ~/netbeans-8.2/bin$
ここまできたら、次のコマンドを実行してjdkとやらの場所を確認する
~/netbeans-8.2/bin$/usr/sbin/update-java-alternatives -l java-1.8.0-openjdk-amd64 1081 /usr/lib/jvm/java-1.8.0-openjdk-amd64 java-8-oracle 1077 /usr/lib/jvm/java-8-oracle
みたいなことになるから、jdkのほうをコピペして次のコマンドで実行する
~/netbeans-8.2/bin$./netbeans --jdkhome /usr/lib/jvm/java-1.8.0-openjdk-amd64
これで使えるようになった(はず)。
最後に
一回使ってみたけど、やっぱりNetBeans使いにくい。
【メモ】sbtで自作ライブラリを使えるようにした話
とりあえず公開したかった。
なんか公開したかった。
sbtで使えるようにしたかった。
ライブラリをビルドする。
とりあえず、sbtプロジェクトに作成してるであろうBuild.scala,build.sbt的なあれこれに次を追加する。
Build.scalaならsettingsの中に突っ込んでください。
それ以外のname,version,scalaVersionはもうすでに設定したものとする。
publishTo := Some(Resolver.file("名前",file("jar,pomなどを作成するディレクトリを指定"))(Patterns(true, Resolver.mavenStyleBasePattern)))
ここで一度プロジェクトをコンパイルする。
$ sbt compile
これでokなら次を実行する
$ sbt publish
するとさっき指定したディレクトリに「名前」のディレクトリとその直下に「名前_ScalaVersion」のディレクトリが生成されている
github pagesを作成しそこにビルドしたやつをぶん投げる
公開したいライブラリがgithubのレポジトリにあるとして、そのレポジトリにgh-pagesというブランチを作る。
とりあえずそこにindex.htmlを投げる。
そしていつもどおりgh-pagesにpushする。
その勢いでさっきビルドしたあれこれをぶん投げる。
手順はこれで終わり。
wgetでテストする
次のコマンドを走らせてdlできればok
wget https://[username].github.io/reponame/projectname/projectname_scalaVersion/version/projectname_scalaVersion-version.jar
これでダウンロードできないなら何かしら失敗しているか、gh-pagesの反映に時間がかかることがあるので最初にリンクを張ったサイトをみてどうにかしてみる。
sbtで使えるかテストする
適当にプロジェクト作るかどうにかして別のBuild.scalaで使うには次のようにする
//ライブラリを追加するところに "projectName" % "projectName_scalaVersion" % "Version" //resolverを作って次を記述 resolvers += "Maven Repo on github" at "https://[username].github.io/projectName/"
これでダウンロードできる(はず)
Newton-Raphson法と単位円内接正多角形を使った円周率近似をScalaでやってみた
こんなやつをコードにした
まずNewton-Raphson法は次の式におけるxの値を求める
このあと登場するけど、単位円内接正多角形を使う近似では漸化式をつかっていくわけだけども
その中で平方根を計算しないといけないのでCを任意実数定数として関数g(x)を次のようにする
すると次の式におけるxを近似的に求めることになる
Newton-Raphson法ではこれを次の式を満たす様に微分を使いながら近似していく
ここまでNewton-Raphson法
次に単位円を用いて円周率を求める。
しかし、円周がわからない。
これは円周率が不明であるためである。
そこで単位円内に内接正多角形を作って円周とみなす。
これは漸化式で表現でき正n角形のnが大きくなればなるほどその周の長さが円周の長さに近づく。
その内接正多角形の一辺の長さが次の式となる
というわけでコードにしてみた
実際に書いたコード
object calcPI{ val max = 20 def main(args:Array[String]):Unit = { var Ln = newtonRaphson(2)//L_1 var n = 0 var pow = 2 while( n < max ){ println( pow * Ln ) Ln = newtonRaphson(2 - newtonRaphson(4-Ln*Ln)) n += 1 pow *= 2 } } def newtonRaphson(c:BigDecimal):BigDecimal = { var x:BigDecimal = c var dx:BigDecimal = -func(x,c) / dfunc(x) while( 1.0e-16 < dx.abs ){ x += dx dx = -func(x,c) / dfunc(x) } return x } def func(x:BigDecimal, c:BigDecimal):BigDecimal = x*x - c def dfunc(x:BigDecimal):BigDecimal = 2.0*x }
これを実行して、出力結果をdatファイルにおとしてgnuplotでグラフにするとこんな感じになる
BigDecimal使ってはいるけど精度的にはまだまだかなと思うけどある程度できた。
Newton-Raphson法を用いた円周率近似
Newton-Paphson法の基礎原理
Newton-Raphson法とは次の式を満たすxの値を見つけるアルゴリズム
ここではg(x)を任意の微分可能な関数としてCを実数定数とする。
つまりNewton-Rapshon法は次の式を満たすxの値を見つけるアルゴリズムとなる。
つまり以下の様にg,Cが与えられた時は2の平方根を求めることになる。
実際の計算
基礎原理はうえで紹介したので実際に解を近似して求める。というのも上記の式を満たすxがすぐに正確に見つかるわけではないことによる。
そこで与えられたf(x)を一回微分して接線の方程式を求め、その接線とx-軸の接点におけるx座標で再びf(x)に戻り微分し接点のx座標を求める…。
これを再帰的に繰り返すことで求めたいxに近似していくこととなる。
その漸化式は次のようになる。
コード化する
ひとまず次のプログラムをベースにする。
#include<stdio.h> int main(){ return 0; }
ここで関数fとそれを一回微分したdfを関数として作る。
#include<stdio.h> #include<stdlib.h> #include<math.h> //プロトタイプ宣言 double Func(double x,double C);//変数xとCが必要となる double dFunc(double x);//fを微分しているのでxのみ必要 int main(){ return 0; } double Func(double x, double C){ return x*x - C; //x^2-Cを計算して返す } double dFunc(double x){ return 2.0*x; //2xを返す }
これで準備は出来た。
次に微分して…。となるがCで微分を扱うのは難しいので数学的に考えてfとxのみの式に直す。
それは以下になる。
というわけでNewton-Raphson法を用いて平方根を求める。ここでは誤差がになるまで続けることにする。
#include<stdio.h> #include<stdlib.h> #include<math.h> //追加した #define EPS 10e-6 //プロトタイプ宣言 double Func(double x,double C);//変数xとCが必要となる double dFunc(double x);//fを微分しているのでxのみ必要 double Newton(double C);//平方根を求めたい値を実数で受け取る int main(){ printf("%12.10f\n",Newton(2) ); return 0; } //Newton-Rapshon法を用いてcの平方根を求める。 double Newton(double C){ double x = C; double dx = -Func(x,C)/dFunc(x); while( fabs(dx) > EPS){ x += dx; printf("%12.10f\n",x); dx = -Func(x,C)/dFunc(x); } return x; } double Func(double x, double C){ return x*x - C; //x^2-Cを計算して返す } double dFunc(double x){ return 2.0*x; //2xを返す }
これで平方根をNewton-Raphson法を使って求めることができる。
ただし、誤差があることに注意。
円周率近似
円周率近似も漸化式を用いて行う。
http://oto-suu.seesaa.net/article/291513400.html
上記のサイトがそれを紹介しているが半径がなので、半径を単位円において考えることにする。
基本的には正n角形の周を求め円周に近似していくことになる。
その場合、漸化式は次のようになる。
ソースコードにする
これをソースコードにするとこうなる
#include<stdio.h> #include<stdlib.h> #include<math.h> #define EPS 1.0e-8 #define POW 2 double Newton(double); double Func(double, double); double dFunc(double); int main(){ double Ln = Newton(2.0);//L_1 = sqrt(2) printf("%12.10f\n",Ln); int n = 1; int pow_base = 2; int n_max = 20; while( n < n_max ){ printf( "%12.10f\n", pow_base * Ln ); Ln = Newton( 2-Newton(4 - pow(Ln, 2)) ); n += 1; pow_base *= POW; } return 0; } //平方根を求める double Newton(double C){ double x = C; double dx = -Func(x, C) / dFunc(x); while( fabs(dx) > EPS ){ x += dx; dx = -Func(x, C) / dFunc(x); } return x; } double Func(double x, double C){ return x*x-C; } double dFunc(double x){ return 2.0 * x; }
これで円周率の近似値が求められた。
githubでコミットしても芝生が生えなかったわけ
昨日、githubの自分のレポジトリにコミットしてたら唐突に芝生が生えなくなった。
それで色々ググった結果
- masterブランチじゃないとだめ
- ローカルレポジトリの登録アドレスがgithubアカウントのメールアドレスになっていないとだめ
- forkしたやつもだめ
みたいなあれこれが書いてあって、それ以外の原因は見つからなかった。
がしかし、自分の環境ではmasterブランチにコミットしていたし、メアドも合ってるし、自分で作成したレポジトリだしってことで
沼に陥ったので、github supportにこれらの問題をぶん投げた。
すると返信があって
「ラグってるだけだと思うから、24時間以上して芝生生えなかったらまたメールしてね」
と来たので今朝見たら、本当にラグってただけだった。
なんか芝生もすぐに反映されなくて、数時間ラグることもあるらしい。