jenkinsのインストールからGradleの手動ビルドまで
jenkins
Javaで書かれたオープンソース継続的インテグレーションツール
いろいろ自動化しなきゃと思ったのでちょっと触ったときのメモ
インストール
事前にjavaがインストールされているか確認、入ってなかったら7以上のjavaを入れておく
jenkinsのインストール
sudo wget -O /etc/yum.repos.d/jenkins.repo http://pkg.jenkins-ci.org/redhat-stable/jenkins.repo sudo rpm --import https://jenkins-ci.org/redhat/jenkins-ci.org.key sudo yum install jenkins
jenkinsの起動
CentOS7も起動スクリプトから起動
service jenkins start
jenkinsの自動起動設定
chkconfig jenkins on
外部からの通信許可
firewall-cmd --zone=public --add-port=8080/tcp --permanent firewall-cmd --zone=public --add-service=http --permanent firewall-cmd --reload
初期設定
“http://IPアドレス:8080/"でJenkinsにアクセスログイン画面では”/var/lib/jenkins/secrets/initialAdminPassword"に初期パスワードの設定があるとの表示があるのでそれに従いパスワードを入力してログイン
標準的なプラグインをインストールするかどうか選べるのでインストールするで次に進む
プラグインインストール後はユーザを作成してダッシュボード画面に遷移
秘密鍵を追加
“/var/lib/jenkins/.ssh"に秘密鍵を追加する
mkdir /var/lib/jenkins/.ssh chown jenkns:jenkins /var/lib/jenkins/.ssh [root@localhost .ssh]# ssh-keygen -t rsa Generating public/private rsa key pair. Enter file in which to save the key (/root/.ssh/id_rsa): /var/lib/jenkins/.ssh/id_rsa Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in /var/lib/jenkins/.ssh/id_rsa. Your public key has been saved in /var/lib/jenkins/.ssh/id_rsa.pub. The key fingerprint is: xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx xxxx@xxxxxxxxxx [root@localhost jenkins]# chown -R jenkins:jenkins .ssh/var/lib/jenkins/.ssh/ [root@localhost jenkins]# chmod 600 .ssh/id_rsa [root@localhost jenkins]# chmod 644 .ssh/id_rsa.pub [root@localhost jenkins]# chmod 644 .ssh/known_hosts # sudo -u jenkins ssh -T git@bitbucket.org
Gradleをビルドしてみる
新規ジョブ作成から"フリースタイル・プロジェクトのビルド"を選択
ソースコード管理ではgitを選択してリポジトリURLにはgitプロトコルのURLを入力する。認証情報の追加から事前に準備していた鍵を指定する。
ビルドトリガも選びたいけど今回は手動で動かせば良いので未設定、ビルド環境も今回は簡単な動作確認が目的のため未設定
ビルド手順の追加では"Invoke Gradle Script"でも良いのかもしれないけど今回はコミット前に"gradle wrapper"を準備しておいてGradleをインストールしていなくても
ビルドを実行できる状態にしておいたので"シェルの実行"を選択して"./gradlew build"を指定しておく
それから手動で実行をしたら
Could not find tools.jar. Please check that /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.121-0.b13.el7_3.x86_64/jre contains a valid JDK installation.
再度手動で実行してビルド成功を確認!
便利そうなのでgitのイベントをフックして自動でビルド、デプロイまで出来るようにしたいと思います。
SpringBootをGradleでビルド
SpringBootのプロジェクトを作ってGradleでビルドしてみる
作り方は公式のサイトにのっています。プロジェクトの作成方法としてSTSを使った方法とサンプルのソースをダウンロードしてきて使う方法があります。開発環境としてEclipseではなくInteliJを使いたいので、サンプルのソースを参考にspring bootのプロジェクトを作成して見たいと思います。
公式の手順ではこちらのソースをダウンロードした後にInteliJでcompleteフォルダをgradleプロジェクトとして読み込み、後はコントローラにリクエストを飛ばしてspring bootが動いていることを確認しています。
サンプルのソースを参考にspring bootのプロジェクトを作ってみる
InteliJでgradleのプロジェクトを作成後、build.gradleの設定を以下のようにします。
buildscript { repositories { mavenCentral() } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:2.0.3.RELEASE") } } apply plugin: 'java' apply plugin: 'idea' apply plugin: 'eclipse' apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' bootJar { baseName = 'spring_test' version = '0.1.0' } repositories { jcenter() } group 'teruuuu' version '1.0-SNAPSHOT' sourceCompatibility = 1.8 targetCompatibility = 1.8 dependencies { compile("org.springframework.boot:spring-boot-starter-web") // tag::actuator[] compile("org.springframework.boot:spring-boot-starter-actuator") // end::actuator[] // tag::tests[] testCompile("org.springframework.boot:spring-boot-starter-test") // end::tests[] }
baseNameには任意のプロジェクト名を指定します。それからInteliJのツールウィンドウからGradleプロジェクトの同期を実行します。これでspring bootのjarが依存ライブラリとしてプロジェクトに含まれるはずです。
次に公式のサンプルソースと同様のコントローラを作成して見ます。
└── src └── main └── java ├── Apprication.java └── HelloController.java
Apprication.javaではspring bootを起動します。
package hello; import java.util.Arrays; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } @Bean public CommandLineRunner commandLineRunner(ApplicationContext ctx) { return args -> { System.out.println("Let's inspect the beans provided by Spring Boot:"); String[] beanNames = ctx.getBeanDefinitionNames(); Arrays.sort(beanNames); for (String beanName : beanNames) { System.out.println(beanName); } }; } }
次にHelloController.javaでリクエストを受け取れるようにします。
package hello; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RequestMapping; @RestController public class HelloController { @Autowired AppConfig appConfig; @RequestMapping("/") public String index() { return "Greetings from Spring Boot!"; } }
後はspringを起動した後にリクエストを投げて動作確認ができます。
./gradlew build && java -jar build/libs/spring_test-0.1.0.jar
後はブラウザでlocalhost:8080にアクセスするかcurlを投げると"Greetings from Spring Boot!"と表示されます。
% curl localhost:8080 Greetings from Spring Boot!
gradleでビルド時に環境ごとのリソースフォルダを切り替えれるようにする
build.gradleでパラメータを受け取り、その値によりsourceSets.main.resources.srcDirs
を変更するようにします。
buildscript { repositories { mavenCentral() } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:2.0.3.RELEASE") } } apply plugin: 'java' apply plugin: 'idea' apply plugin: 'eclipse' apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' bootJar { baseName = 'spring_test' version = '0.1.0' } repositories { jcenter() // mavenCentral() } group 'teruuuu' version '1.0-SNAPSHOT' sourceCompatibility = 1.8 targetCompatibility = 1.8 dependencies { compile("org.springframework.boot:spring-boot-starter-web") // tag::actuator[] compile("org.springframework.boot:spring-boot-starter-actuator") // end::actuator[] // tag::tests[] testCompile("org.springframework.boot:spring-boot-starter-test") // end::tests[] } def environment = project.hasProperty( 'env' ) ? env : 'dev' if( environment.equals( 'prod' )) sourceSets.main.resources.srcDirs 'src/main/resources/prod' else sourceSets.main.resources.srcDirs 'src/main/resources/dev'
動作確認のために以下のリソースファイルを作成して見ます。
src/main/resource/dev/application.yml
app: env: dev
src/main/resource/prod/application.yml
app: env: prod
それから、設定内容をセットするbeanを作成します。
AppConfig.java
package hello; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.PropertySource; import org.springframework.stereotype.Component; @Component @ConfigurationProperties(prefix="app") @PropertySource(value = "application.yml") public class AppConfig { private String env; public String getEnv() { return env; } public void setEnv(String env) { this.env = env; } }
後はコントローラを修正し、AppConfigにセットされた値を表示するメソッドを追加します。 HelloController.Java
package hello; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RequestMapping; @RestController public class HelloController { @Autowired AppConfig appConfig; @RequestMapping("/") public String index() { return "Greetings from Spring Boot!"; } @RequestMapping("/env") public String env() { return "env:" + appConfig.getEnv(); } }
それから、以下のようにビルド時にパラメータを渡せばリソースフォルダが切り替わります。
./gradlew clean build -Penv=prod && java -jar build/libs/spring_test-0.1.0.jar
% curl localhost:8080/env env:prod%
Springはドキュメントが充実していると思いますが、Spring Boot Gradle Pluginの説明はこちらになります。
レガシーソフトウェア改善ガイドの感想
書簡
最近起こった出来事もあり特にレガシープロジェクトの健康状態をどうすれば良いか考えていた時にちょうど良い本がありました。
書籍の中で述べられている方針自体は真新しいということはないのですがクリスさんの経験に基づく話もあってプロジェクトの品質を良くするためには何をすれば良いのか綴られており、この本を読むたくさんの人は共感が得られるのではないのかと思います。
この本の中ではレガシープロジェクトを"保守または拡張が困難な既存のプロジェクト"と定義しております。レガシープロジェクトが生まれる原因としては例えばコミュニケーションの不足であったり、ドキュメントが不足、その上にプロジェクトの担当者自体が単独でコミュニケーションを取れる相手がいないなどありまして、そのようなハードな環境で働いているエンジニアは世界中にもたくさんいるのではないでしょうか。
そのような環境に置かれると現状の維持だけしか考えなかったりとか良くあると思います。自分もとりあえずコードの可読性やロジックの簡略化にさえ気をつけておけば良いという、いわゆるコード見りゃわかるからの考えが昔はあったのですがそれは間違いでした。ソースコードのライフサイクルは開発時以外にも運用やカスタマイズの時まで意識する必要があります。そのためのコミュニケーションが不足していると自分の管理すべきコードに対して次第に責任を持てなくなります。
プロジェクトを健康に保つためのコミュニケーションが行われる環境やインフラストラクチャの自動化についての実例を元にしたサンプルがあったのでまずはそこを意識して自分が担当しているプロジェクトにも取り入れていきたいと思います。
この本は最後に"自分が書いたコードにはプライドを持とう。別の誰かが書いたコードを渡されたら先人の人のためにも後から来る人々のためにもコードに敬意を払い育成しよう。自分たちが残すべき遺産は誇らしい遺産にしよう。"と述べられています。エンジニアとして楽しく働いていくためにもこの考えは徹底していきたいと思います。
今後の改善
まずは自動化について以下について以下のものに取り組んでいくのが良さそうだと思いました。
- ビルドツールをGradleにする
- 環境ごとにビルドを行えるようにする
- テスト環境をAnsibleで構築できるようにする
- Jenkinsを利用する
- gitにPush, Mergeされたタイミングで自動でビルドして結果を見れるようにする
- ビルド時にテストスクリプトを自動で実行できるようにする
- ビルド時にテスト環境に自動でデプロイをこな得るようにする
- Fulentdでログを集計後Elasticsearchに突っ込んづkibanaで可視化する
もしも自社がクラウドサービスを運用しているのでしたら、セキュリティの面で自信を持ってサービスを提供するためにもこの辺りは行えなきゃいけないのかと思います。よく研究されたフレームワークはいつ攻撃を受けてもおかしくありませんので、脆弱性が発表された当日、翌日ですぐに対応版をリリースできるようにするためにもこの辺りは徹底していきたいです。
DDDの境界付けられたコンテキストについて調べてみた
エリック・エヴァンスのDDDの14章.“モデルの整合性を維持する"を読んでみたのでそのメモなど
戦略的設計
戦略的設計の原則は設計上の意思決定を導いて、各部分の相互依存関係を減らし、設計をより明確にしながら、蹴って的に重要の相互運用性と相乗効果を失わないようにするものである。
そのためには境界付けられたこんんテキストを定義してモデルの適用範囲を内部に限定したり、蒸留によって混ざりってしまったコンポーネントを分離したり、システム全体に対して統一感を持たせたりして、ドメイン駆動設計ではこれを戦略的設計として表される。
モデルの整合性を維持する
システムを一つのモデルで統一することは以下のリスクがある。 - 1.一度にあまりにも多くのレガシーシステムを置き換えようとしてしまうかもしれない. - 2.調整にかかるオーバーヘッドが能力の限界を超えてしまうために、巨大なプロジェクトは動きが取れなくなってしまうかもしれない。 - 3.特殊の要件を伴うアプリケーションは、その要求を完全には満たしていないモデルを使わなければならないかもしれず、そのせいで振る舞いをどこか別の場所に入れなければならなくなる。 - 4.逆に単一のモデルを使って万人を満足させようとすれば選択肢が複雑になり、モデルを使うのが難しくなる。
モデルについてどこを共通して使うか、それとも分けて使うかというのは常に認識を合わせておなければ混乱や破損が生じる原因となる、認識を合わせるためにも境界付けられたコンテキストで各モデルの適用範囲を定義し、コンテキストマップによってプロジェクト全体のコンテキストとコンテキスト間の関係を可視化させておきたい。
境界付けられたコンテキスト
似たようなモデルであっても使用するコンテキストによって動きが変わってくることもあるし、それを境界付けることもなく使用していたらバグの温床となりうる。そうならないためにもモデルをどのコンテキスト内で使用すべきなのかをはっきりとさせておく必要がある。
継続的な統合
コンテキストやモデルの粒度にブレがないように継続的にコミュニケーションをとって認識合わせをしておかなければならない。
コンテキストマップ
境界付けたコンテキストを見える化してそれぞれのコンテキスト、モデルに対して名前を付けることでそれがユビキタス言語として使えるようになる。コンテキストマップを作成することで問題が起きた時の修正箇所は明白にすることができる。
境界付けられたコンテキスト間の関係
境界付けられたコンテキスト間でやり取りをする場合には以下のような方法がある。 - 共有カーネルを利用する - 顧客/供給者の関係で利用する - レガシーシステムとかの連携でも使われる腐敗防止層を使う - 外部サービスとの連携であれば公開ホストサービスを利用する
共有カーネル
境界付けられたコンテキスト間で共有して使いたいモデルがある場合は、コンテキスト毎にモデルを作成するのではなく一つのモデルをコンテキスト間で共有させて方が良い場合もある。目標は重複を減らして2つのサブシステム間の統合を容易にすることである。
顧客/供給者の開発チーム
コンテキスト毎で顧客/供給者といった下流、上流の関係をはっきりとさせておく。期待されるインターフェースは検証を行い下流への副作用を心配せずに上流チームが自由に変更を行えるようにする。参照だけするのと、登録・更新、削除、参照ができるのを分けるということか。
腐敗防止層
レガシープロジェクトのモデルに対して機能追加するときなどでは、新システムと旧システムの橋渡しとして使われる腐敗防止層がある。コンテキスト間でも同様に腐敗防止層をインタフェースとして利用することで別コンテキストの機能を利用する際に気をつけなければいけないことを隠蔽することができる。
公開ホストサービス
一つのコンテキスト内のモデルを複数のコンテキストで利用することが考えれる場合は公開ホストサービスとして他コンテキストから利用できるようにする。
パターン指向リファクタリング入門を読んでみた
古いですが"パターン指向リファクタリング入門~ソフトウエア設計を改善する27の作法"を読んでみたので感想など
□生成
Creation Methodによるコンストラクタの置き換え
JavaやC++ではコンストラクタがクラス名と一致するためどんなインスタンスが作られるのかが把握しづらくなる場合がある。
例えばローンを表すクラスがあってそれに対して支払い期日までに払わなければいけないタームローンや融資枠や有効期限が定められる回転融資など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(,,,){ ,,, }
javaのjava.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)
使ってみた感想
簡単な分析を行う分には手軽で良さそうな気がしました。