Apache Sparkのアプリをデバッグする

sparkアプリケーションのデバッグ

1.sbt assemblyでjarファイルを生成しspark-submitコマンド実行サーバにアップロードする

2.spark-submitコマンド実行サーバにポートフォワードの設定付きでssh接続する
とりあえず5039ポートを使ってみる

ssh -L 5039:remote:5039 target

3.spark-submitコマンド実行

spark-submit --master local[*] \    
--driver-java-options -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5039 \    
--class 実行対象クラス \    
--name アプリケーション名 jarファイル アプリの引数    

4.ローカルの開発環境でリモートデバッグ

pycharmを使ってpysparkの開発を行った際に"from pyspark.sql.functions import lit"でエラーがでたのを調べて見た

pysparkの開発を行った際に"from pyspark.sql.functions import lit"でimportできないとエラーが出たのを確認した時のメモ 実際は以下のようにpyspark.sql.functions.py内で以下のようにして動的にメソッドを追加している。

def _create_function(name, doc=""):
    """ Create a function for aggregator by name"""
    def _(col):
        sc = SparkContext._active_spark_context
        jc = getattr(sc._jvm.functions, name)(col._jc if isinstance(col, Column) else col)
        return Column(jc)
    _.__name__ = name
    _.__doc__ = doc
    return _


_functions = {
    'lit': _lit_doc,
    'col': 'Returns a :class:`Column` based on the given column name.',
    'column': 'Returns a :class:`Column` based on the given column name.',
    'asc': 'Returns a sort expression based on the ascending order of the given column name.',
    'desc': 'Returns a sort expression based on the descending order of the given column name.',

    'upper': 'Converts a string expression to upper case.',
    'lower': 'Converts a string expression to upper case.',
    'sqrt': 'Computes the square root of the specified float value.',
    'abs': 'Computes the absolute value.',

    'max': 'Aggregate function: returns the maximum value of the expression in a group.',
    'min': 'Aggregate function: returns the minimum value of the expression in a group.',
    'count': 'Aggregate function: returns the number of items in a group.',
    'sum': 'Aggregate function: returns the sum of all values in the expression.',
    'avg': 'Aggregate function: returns the average of the values in a group.',
'mean': 'Aggregate function: returns the average of the values in a group.',
    'sumDistinct': 'Aggregate function: returns the sum of distinct values in the expression.',
}

for _name, _doc in _functions.items():
    globals()[_name] = since(1.3)(_create_function(_name, _doc))

ここではcreate_functionでメソッドを生成し、globals()[name]にてname(col)で関数を呼び出せるようにしている。getattrでは"sc.jvm.functions"のnameで指定した関数を呼び出せるようにしており、ここでjvm場で動いているsparkを呼び出すようにしている。pysparkではpythonのコードがjvm場で動くという分けではなくpy4jにより連携するようになっていて、その連携部分が"getattr(sc.jvm.functions, name)(col.jc if isinstance(col, Column) else col)“のようでpyspark自体についてももうちょっと調べたいと思います。

pysparkでの開発時に気になった点のメモでした。

Sparkで単体テストをしてみる

Apache Sparkで単体テストをしてみる

Intelij IDEAでsparkの単体テストを書いてみたのでメモ

build.sbtの設定を変更

まず、build.sbtに以下の設定を追加する。

parallelExecution in Test := false

“build sbt"で複数のテストが同時に動いた場合に発生するSparkContext周りのエラーを防ぐのに必要なようである。

テストを書いてみる

まず、以下のようにcsvをDataFrameとして読み込んでデータを取得するclassのテストを書く場合

package intoroduction.spark.dataframe

import org.apache.spark.sql.SQLContext
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.sql.hive.HiveContext
import org.apache.spark.sql.types.DataType._
import org.apache.spark.sql.types.IntegerType

case class Dessert(menuId: String, name: String, price: Int, kcal: Int)

class DesertFrame(sc: SparkContext, sqlContext: SQLContext, filePath: String) {
  import sqlContext.implicits._
  lazy val dessertDF = {
    val dessertRDD = sc.textFile(filePath)
    sc.textFile(filePath)
    // データフレームとして読み込む
    dessertRDD.map { record =>
      val splitRecord = record.split(",")
      val menuId = splitRecord(0)
      val name = splitRecord(1)
      val price = splitRecord(2).toInt
      val kcal = splitRecord(3).toInt
      Dessert(menuId, name, price, kcal)
    }.toDF
  }
  dessertDF.createOrReplaceTempView("desert_table")


  def findByMenuId(menuId: String) = {
    dessertDF.where(dessertDF("menuId") === menuId)
  }
}


object DesertFrame {

  def main(args: Array[String]): Unit ={

    val conf = new SparkConf().setAppName("DesertFrame").setMaster("local[*]")
    val sc = new SparkContext(conf)
    val sqlContext = new SQLContext(sc)
    import sqlContext.implicits._

    val filePath = "src/test/resources/data/dessert-menu.csv"
    val desertFrame = new DesertFrame(sc, sqlContext, filePath)

    val d19DF = desertFrame.findByMenuId("D-19").head
    print(d19DF)

  }
}

上記のDesertFrameのテストを書く場合は以下のようになる。

package intoroduction.spark.dataframe

import org.apache.spark.sql.SQLContext
import org.apache.spark.{SparkConf, SparkContext}
import org.scalatest.{BeforeAndAfterAll, FunSuite}

class DessertFrameTest extends FunSuite with BeforeAndAfterAll{
  private var sparkConf: SparkConf = _
  private var sc: SparkContext = _
  private var sqlContext: SQLContext = _

  override def beforeAll() {
    print("before...")
    sparkConf = new SparkConf().setAppName("DessertFrameTest").setMaster("local")
    sc = new SparkContext(sparkConf)
    sqlContext = new SQLContext(sc)
  }

  override def afterAll() {
    sc.stop()
    print("after...")
  }

  test("dessert_frame"){
    val filePath = "src/test/resources/data/dessert-menu.csv"
    val desertFrame = new DesertFrame(sc, sqlContext, filePath)

    val d19DF = desertFrame.findByMenuId("D-19").head
    assert(d19DF.get(0) == "D-19")
    assert(d19DF.get(1) == "キャラメルロール")
    assert(d19DF.get(2) == 370)
    assert(d19DF.get(3) == 230)
  }
}

ここでは"SparkConf().setAppName(“DessertFrameTest”).setMaster(“local”)“と指定しており、ローカルの環境で動かすことができるようになりテストで使うデータを"src/test/resources/data/dessert-menu.csv"にしているのでテストデータもそのままgitで管理できるようになっている。

テスト実行

あとは"sbt test"か"sbt test:testOnly クラス指定"でテストを実行できるはずである。

Hadoopについて調べてクラスタを構築してみた

並列分散処理入門

Hadoop,Spark周りについて調べたことをまとめてみる

並列分散処理とは

並列分散処理とは複数のサーバを同時に動かしてデータを処理することである。ビッグデータと呼ばれるような大量のデータを扱う場合はたくさんのサーバを使ってクラスターを構成し、多ければ数千台のサーバを使うということもある。

並列分散処理ツール登場の背景

既存のRDBMSがある中でHadoopなどの代表的な並列分散処理ツールが登場した背景として、RDBMSではトランザクションやACIDなど複雑なデータに不整合が起きないような設計によりそのまま複数台の端末で動かそうとするのであればディスクI/Oの方がボトルネックとなりCPU資源を使い切れず速度が出し切れなくなる問題があった。この問題を解決するためHadoopなどの並列分散処理ツールではRDBMSにあったデータに不整合が発生しないように気にする機能を除外し、Mapreduceという仕組みにより複数台の端末で同時に処理してもディスクI/Oがボトルネックとならないように作られている。

Hadoopについて

概要

Hadoopとは代表的な並列分散処理ツールでGoogleのDoug cuttingにより作られた。Hadoopの名前の由来はDougの子供が持っていたゾウのぬいぐるみ。HadoopではHDFS(Hadoop Distributed File System)により複数のサーバにリソースを分散させて、Mapreduceにより複数の端末で処理を実行するということができるようになっている。
オープンソースで開発が行われておりチケット管理にJiraを使用している。
https://issues.apache.org/jira/projects/HADOOP/summary

HDFS

HDFSとは巨大なデータを扱うとことに特化した分散ファイルシステム。データサイズとして想定しているのは1ファイルが数GB以上に及ぶようなファイルであり、そのような大きいファイルを高速に扱うことに特化している。HDFS自体にレプリケーションの対障害設定があり、基本的にはHDFSの役割を果たすノードにRAID構成は不要とされている。HDFSバッチ処理に的するように作られておりデータの書き込みは一度だけで、それ以降は読み込み及び新しい書き込みが行われ、データの一部更新はできないようになっている。

Mapreduce

多数のサーバを利用して巨大なデータを分散処理するためのフレームワークMapreduceではクラスター上の各ノードに以下の役割がある。
○NameNode
 データがどこに配置されているかなどのメタデータを管理する。各ノードに対してハートビートで死活監視を行う。NameNode自体が死んだ時のためにSecondaryNameNodeを設定することもできる。
○JobTracker
MapReduceのマスターサーバで1つのジョブをタスクと呼ばれる複数の処理に分割し、各スレーブにタスクを割り振る
○DataNode
データを保存する
○TaskTracker
タスクを処理する

既存のMapreduceでは5000ノードからなる40000タスクを実行した時にJobTrackerがボトルネックとなることが確認されており、これは単一のJobTrackerに次の2つの異なる責任が課せられているためとされている。
クラスターでの計算リソースの管理
クラスター上で実行されるすべてのタスクの調整

この問題を解決するために単一のJobTrackerを使用するのではなく、クラスター・マネージャーを導入するようになった。クラスター・マネージャーの役割はクラスター内のライブ・ノードと使用可能なリソースを追跡して、タスクに割り当てることである。スレーブ側のTaskTracker側で短時間存続する JobTrackerが開始されるようになったが、これによりジョブのライフサイクルの調整は、クラスター内で使用可能なすべてのマシンに分散されさらに多くのタスクを分散処理できるようになった。この流れでYARNが登場した。

YARN

JobTrackerが担っていた2つの役割のを分割し、スレーブ側でタスクの調整が行えるよう疎結合になったことでhadoop自体の分散処理エンジン以外を使うことができるようになりSparkで処理を実行することができるようになっている。Mapreduceの時に使っていた用語は以下のように変わっている。

クラスター・マネージャー → ResourceManager
短時間存続する専用の JobTracker → ApplicationMaster
TaskTracker → NodeManager
ジョブ → 分散アプリケーション

YARNの登場により従来のMapreduceはMRv1、YARNがMRv2という扱いになった。Hadoopはversion2からYARNが使えるようになった。

Sparkについて

概要

scalaで書かれた並列分散処理のエンジン部分。HDFS上のデータにアクセスしてデータを処理することができる。Hadoopとの違いとして、Hadoopの分散処理エンジンでは計算結果をメモリ上には保存せずディスクに保存する作りになっている。それに比べてSparkでは計算結果をメモリ上に保存して再利用するという仕組みがあるためHadoopの分散処理エンジンと比べるとディスクI/Oの影響で数倍早いと言われている。このように仕組みが異なっている原因としてはHadoopとSparkの開発時期の違いが考えられ、Hadoopの開発が始まった頃はサーバ1台あたりのメモリを8GBくらいで想定していたのに対しSparkの開発時にはサーバ1台でメモリ100GB以上とか積んでいるのも珍しくないような時代背景の違いが考えられる。

用途

Sparkは以下のような用途で使われている。
バッチ処理
単純にhadoopよりも早いためバッチ処理もsparkが使われることが多いきがする
機械学習処理
機械学習のライブラリ(MLib)がありhadoopで使っていたMahoutより高速化できたという事例がある
○ストリーム処理
HDFS以外でもKafkaから送られてくるストリーミングデータを処理することができ、これによりバッチ処理、ストリーミング処理の両面でリアルタイムな機械学習でレコメンドするといったことが可能になっている

hadoopクラスタ管理

hadoopクラスタでは複数のサーバを扱わなければいけないので管理の面で気をつけなければいけないが、Clouderaが出しているマネジメントツールにより各ノードの管理が行いやすくなるらしい。clouderaのマネジメントツールを使わないにしてもディストリビューションで管理している対象ツールのバージョンを確認することでHadoopクラスタ構築時にどのバージョンを入れれば良いのかの参考になりそう(Hadoop, Spark, Hiveなど)

Hadoopクラスタ構築

実際にHadoopクラスタを構築してみたいと思います。
1.javaのインストール
hadoopはopenJdkではなくOraclejdkを推奨している
2.プログラムのダウンロード
今回はHadoopとSpark,Hiveをインストールする

wget http://ftp.jaist.ac.jp/pub/apache/hadoop/common/hadoop-2.6.5/hadoop-2.6.5.tar.gz
wget https://d3kbcqa49mib13.cloudfront.net/spark-1.6.0-bin-hadoop2.6.tgz
wget https://archive.apache.org/dist/hive/hive-0.12.0/hive-0.12.0-bin.tar.gz    

3.プログラムの展開及び配置
4.環境変数を追加
vi ~/.bashrc 以下を追加

# Hadoop
export HADOOP_HOME=HADOOPの展開先
export HADOOP_CONF_DIR=$HADOOP_HOME/etc/hadoop
export PATH=$HADOOP_HOME/bin:$HADOOP_HOME/sbin:$PATH

# spark
export SPARK_HOME=Sparkの展開先
export PATH=$SPARK_HOME/bin:$PATH

# hive
export HIVE_HOME=Hiveの展開先
export PATH=$HIVE_HOME/bin:$PATH

source ~/.bashrc

HADOOP_CONF_DIRを環境変数に追加しているが、これはSparkで使用している。

5.設定ファイルの編集
HADOOP_CONF_DIRのディレクトリの以下の設定ファイルを編集します。

core-site.xml : マスターノードの設定
hdfs-site.xml : データ保存の設定、マスター側とスレーブで設定は異なる
hive-site.xml : hive展開時のconfディレクトリにあったhive-default.xml.templateをリネーム
mapred-site.xml : Mapreduceの設定 yarnを使う場合はここで指定する
yarn-site.xml : リソースマネージャの設定

上記設定ファイルでサーバを指定する場合はIPアドレスではなくFQDNで指定しなければいけないので注意が必要
○core-site.xml

<configuration>
  <property>
    <name>fs.defaultFS</name>
    <value>hdfs://ネームノードのFQDN:9000</value>
  </property>
</configuration>

hdfs-site.xml(NameNodeの場合)

<configuration>
  <property>
    <name>dfs.replication</name>
    <value>3</value>
  </property>
  <property>
    <name>dfs.namenode.name.dir</name>
    <value>/data/1/dfs/nn</value>
  </property>
</configuration>

hdfs-site.xml(DataNodeの場合)

<configuration>
  <property>
    <name>dfs.replication</name>
    <value>3</value>
  </property>
  <property>
    <name>dfs.datanode.data.dir</name>
    <value>/data/1/dfs/dn,/data/2/dfs/dn,/data/3/dfs/dn</value>
  </property>
</configuration>

○mapred-site.xml

<configuration>
  <property>
    <name>mapreduce.framework.name</name>
    <value>yarn</value>
  </property>
</configuration>

○yarn-site.xml

<configuration>
  <!-- Site specific YARN configuration properties -->
  <property>
    <name>yarn.nodemanager.aux-services</name>
    <value>mapreduce_shuffle</value>
  </property>
  <property>
    <name>yarn.resourcemanager.hostname</name>
    <value>リソースマネージャのFQDN</value>
  </property>
  <property>
    <name>yarn.web-proxy.address</name>
    <value>WebAppProxyのFQDN:9046</value>
  </property>
</configuration>

7.サービスの起動
設定が終わったらサービスを起動させます。NameNodeリソースマネージャが同一で、それ以外にDataノード兼TaskTrackerのスレーブが連なる場合は、以下のようになります。

  • マスターノード
# $HADOOP_HOME/sbin/start-all.sh
# yarn-daemon.sh start proxyserver
# start-yarn.sh
# mr-jobhistory-daemon.sh start historyserver
  • スレーブノード
# $HADOOP_HOME/sbin/start-all.sh
# start-yarn.sh

サービス起動後は各ノードでjpsを実行しサービスが立ち上がっているか確認します。それから以下URLにアクセスします。
http://ネームノードのFQDN:50070/dfshealth.html#tab-datanode
http://ノードマネージャのFQDN:8088/cluster/nodes

8.hdfsコマンドの確認 基本的なhdfsコマンドを実行しファイルのアップロードと参照が可能か確認します。

hdfs dfs -ls /
hdfs dfs -mkdir /input
echo "upload test" > test.txt
hdfs dfs -put test.txt /input
hdfs dfs -cat /input/test.txt

上記実行ご別のノードでもアップロードしたファイルが参照できるか確認してみます。

Hadoopクラスタ上でSparkのプロジェクトを動かしてみる

Sparkの簡単なプロジェクトを作成してHadoopクラスタ上で動かしてみたいと思います。
1.プロジェクト作成
まずsbt newかintelijの新規プロジェクト作成でプロジェクトを作成します。

2.build.sbtの設定
今回はspark-coreだけで十分のはずですが一応spark-sqlとかもlibraryDependenciesに追加しています。今回はjarを生成してsparkのコマンド経由で実行するのですが、jarを生成できるようにassemblyのプラグインを追加しています。またlibraryDependenciesでspark関連をprovidedで指定することも注意してください。

name := "StudySpark"

version := "1.0"

scalaVersion := "2.11.8"
val sparkVersion = "2.1.0"

// Intelijより直接実行する場合はspark関係はcompileにする、sbt assemblyでjarを出力する場合はprovidedにしておく
libraryDependencies ++= Seq(
  "org.apache.spark" % "spark-core_2.11" % sparkVersion % "provided",
  "org.apache.spark" % "spark-sql_2.11" % sparkVersion % "provided",
  "org.apache.spark" % "spark-mllib_2.11" % sparkVersion % "provided",
  "org.apache.spark" % "spark-graphx_2.11" % sparkVersion % "provided",
  "org.apache.spark" % "spark-hive_2.11" % sparkVersion % "provided",
  "joda-time" % "joda-time" % "2.9.7"
)

// assembly settings
assemblyJarName in assembly := "studyspark.jar"
assemblyMergeStrategy in assembly := {
  case PathList("javax", "servlet", xs @ _*)         => MergeStrategy.first
  case PathList(ps @ _*) if ps.last endsWith ".html" => MergeStrategy.first
  case "application.conf"                            => MergeStrategy.concat
  case "unwanted.txt"                                => MergeStrategy.discard
  case x =>
    val oldStrategy = (assemblyMergeStrategy in assembly).value
    oldStrategy(x)
}

3.サンプルのプログラム作成
よく使われるワードカウントのプログラムを作ります。

package intoroduction.spark

import org.apache.spark.{SparkConf, SparkContext}


/**
  * 実行コマンド
  * spark-submit --master yarn \
  * --class intoroduction.spark.WordCount \
  * --name WordCount target/scala-2.11/studyspark.jar \
  * /input/test.txt
  *
  */
object WordCount {

  def main(args: Array[String]) = {
    require(args.length >= 1, "ファイルを指定してください")

    val conf = new SparkConf().setAppName("WordCount").setMaster("local[*]")
    val sc = new SparkContext(conf)

    try {
      val filePath = args(0)
      val wordAndCountRDD = sc.textFile(filePath)
                .flatMap(_.split("[ ,.]"))
                .filter(_.matches("""\p{Alnum}+"""))
                .map((_, 1)).reduceByKey(_ + _)

      wordAndCountRDD.collect.foreach(println)

    } finally {
      sc.stop()
    }
  }
}

4.jar生成 以下のコマンドでjarを生成します。
sbt assembly

5.プログラム実行
それから以下のコマンドでプログラムを実行します。

spark-submit --master yarn \
--class intoroduction.spark.rdd.WordCount \
--name WordCount target/scala-2.11/studyspark.jar \
/input/test.txt

うまくいけばコンソールに実行結果が出力されるはずです。 実行ステータスはノードマネージャのweb画面から見ることもできます。 http://ノードマネージャのFQDN:8088/cluster/nodes

デバック実行したい場合は事前に以下の環境変数を追加しておく必要があります。
export SPARK_SUBMIT_OPTS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=7777
上記環境変数追加後はport=7777でリモートデバックできます。

Angularでカスタムディレクティブへの双方向バインドに対して調べてみた

Angularでカスタムディレクティブへの双方向バインドに対して調べてみたときのメモ

コンポーネント内のメンバ変数の変更を監視する

例えばコンポーネント内に以下のメンバ変数があったとします。

@Input() textInput: String = "";

このメンバ変数は親のコンポーネントから直接値を変更されることがあるため、値が変更されたタイミングに実行したメソッド等ある場合は監視が必要になりそうですが、以下のように@Inputで監視対象の変数を指定することで変更された際に何をするか指定することができます。

@Input('textInput')
set updateInternalVal(externalVal) {
  this.textInput = externalVal;
  this.onEditChange();
}

自作のカスタムディレクティブに対して双方向バインディングしてみる

inputタグなど元からhtmlに存在しているタグであればng-modelでバインドすることでinputへの入力がバインドしている変数に直接反映されるし、バインドしている変数自体を変更することでバインド先へのinputタグの表示が切り替わる双方向バインドが働いてますが、Angular2以降では自作のディレクティブに対しての双方向バインドが廃止されたようでちょっと工夫が必要なようでした。
以下自分が試した方法になります。

Outputを受け取る際に変数を変更する

例えば以下のコンポーネントがあったとします。 myText.html

<input [(ngModel)]="textInput" (ngModelChange)="onEditChange()" placeholder="{{ placeHolder }}"/> {{ textLength }}

myText.ts

import { Component, Input, Output, EventEmitter } from '@angular/core';
import { Router }            from '@angular/router';

import { Observable }        from 'rxjs/Observable';
import { Subject }           from 'rxjs/Subject';


@Component({
  selector: 'my-text',
  templateUrl: './myText.html',
  styleUrls: [ './myText.css' ]
})
export class MyTextComponent {
  @Input() textInput: String = "";
  @Input() placeHolder: String = "";

  textLength: Number = 0;

  @Output() textChange = new EventEmitter<String>();

  // 親コンポーネントからの値の変更時に実行
  @Input('textInput')
  set updateInternalVal(externalVal) {
    this.textInput = externalVal;
    this.onEditChange();
  }

  onEditChange(): void {
    if(this.textInput !== void 0 && this.textInput.length !== void 0){
      this.textLength = this.textInput.length;
    }
    this.textChange.emit(this.textInput);
  }
}

このコンポーネントはメンバ変数textInputに親からバインドしているものがセットされているのですが、コンポーネント内で変数の値が変更されたことを親へ伝えるのには以下のようにカスタムのイベントを発行しています。

this.textChange.emit(this.textInput);

その場合、呼び出し元の親コンポーネントは以下のようにtextChangeのイベントを受け取った際にバインド先の変数を変更することで双方向バインドの動きになります。

<my-text [(textInput)]="hero.name" (textChange)="hero.name = $event" placeHolder="name"></my-text><br />

Outputのカスタムイベント名を"メンバ変数 + Change"にすることで自動で双方向バインドする

さっきの方法だと親コンポーネント側で双方向バインドするか選べましたが、こっちの方は子コンポーネントの方で直接双方向バインドするように指定できます。 まず子コンポーネント側でカスタムイベントの名称を"メンバ変数 + Change"に指定し、値に変更があったらemitするようにします。 myText.ts

import { Component, Input, Output, EventEmitter } from '@angular/core';
import { Router }            from '@angular/router';

import { Observable }        from 'rxjs/Observable';
import { Subject }           from 'rxjs/Subject';


@Component({
  selector: 'my-text',
  templateUrl: './myText.html',
  styleUrls: [ './myText.css' ]
})
export class MyTextComponent {
  @Input() textInput: String = "";
  @Input() placeHolder: String = "";

  textLength: Number = 0;

  @Output() textInputChange = new EventEmitter<String>();

  // 親コンポーネントからの値の変更時に実行
  @Input('textInput')
  set updateInternalVal(externalVal) {
    this.textInput = externalVal;
    this.onEditChange();
  }

  onEditChange(): void {
    if(this.textInput !== void 0 && this.textInput.length !== void 0){
      this.textLength = this.textInput.length;
    }
    this.textInputChange.emit(this.textInput);
  }
}

この場合は、イベントが発行された時に何をするとか特に指定しなくても双方向バインドが実現されます。以下呼び出し元のサンプルになります。

<my-text [(textInput)]="hero.name" placeHolder="name"></my-text><br />

深層学習(青本) 1章. はじめに

深層学習の1章読書まとめ

研究の歴史

多層ニューラルネットへの期待と失望

ニューラルネットワークはこれまでに2度のブームが訪れていた

1回目のブーム
 1960年代〜1970年代
 パーセプトロン(1層ニューラルネット)

2回目のブーム
 80年代半ばから90年代前半前半
 誤差逆伝搬の誕生(多層ニューラルネット)

パーセプトロンの欠点

 ノイズに弱い
 収束が遅く、学習効率が悪い(誤差逆伝搬でいう損失関数の微分を効率的に使ったパラメータ更新が行えない)  n次元の入力に対して重み付けを行った結果がある値より大きいかどうかで2つのクラスに分類する
 y=WtX (Wt:学習済みの重み、 X:入力、 y:2つのクラスを分類する境界)
 線形分離可能な問題にしかできない(多層化できれば解決できる)
 多層化できない(中間層を出力することができない)

誤差逆伝搬(多層ニューラルネット)の欠点

 入力にノイズがあったら過学習は発生する
 2層程度であれば期待通りだが、それより増やしていくと勾配が急速にに小さくなったり大きくなったりする勾配消失問題が発生する
 ※ニューラルネットの層を増やす動きがあるが、これは中間層を用意することで柔軟なネットワークを構築でき学習精度を高めることができるからである、誤差逆伝搬により多層化は行えるが層が増えると勾配が消失しあまり精度が上がらなくなるという問題がある
 ※この頃はニューラルネットの層数やユニット数(中間層の主力)の設定に対しての理論がなかった

畳み込みニューラルネットの誕生

1980年代後半画像を対象としていた畳み込みニューラルネットについてはこの時点で5層からなる多層ネットワークの学習に成功していた
※前の層の畳み込みの出力を粗くリサンプリングするようなイメージとなっており、これにより画像の多少のずれによる見え方の違いを吸収することが可能となり勾配消失問題が発生しなかった
1980年代半ばからのブームの多層ニューラルネットワークでは各層の出力全てを次の層に入力していた(全結合)
多層ニューラルネットワーク自体の研究低下とともに畳み込みニューラルネットの関心も小さくなっていった

多層ネットワークの事前学習

1990年代〜2000年代前半(ニューラルネットの関心が低い時期)
ディープビリーフネットワーク(DBN)の研究
 一つの層を貪欲アルゴリズムで訓練できることでブレークスルーとなった
制約ボルツマンマシン(RBM)の誕生

その後、自己符号化の登場

1990年代〜2000年代前半では事前学習によりノイズ除去などで良い入力を得られるようにする研究が行われた

特徴量の学習

画像や音声などの自然界のデータなど同じものでもちょっとの違いでデータに差が出るものをネットワークの多層構造に取り込むために、どのように特徴量を抽出するか
 例えば猫の画像でも明るさが違ったり、向きが違ったりするだけでデータとして差が出る
 音声の場合は白色ノイズなど、扱うデータの種類によりノイズの種類も様々
 正規化することでそれらの違いを吸収した特徴量の情報が欲しい

画像や音声については事前学習ではなく事前処理による特徴量抽出で良い入力を得られるようにしていた

深層学習の隆盛

音声認識、画像認識のベンチマークで多層ニューラルネットワークの有効性が認められるようになり深層学習の有効性が広く認知されるようになった
 一重に深層学習といっても扱う対象によって学習方法が異なる
 ・音声認識の場合は層間ユニット全結合で事前学習が一般的に行われる
 ・画像認識では畳込みニューラルネットが主流で事前学習は不要
 ・自然言語処理音声認識の特定のタスクでは再帰ニューラルネット

それぞれ学習方法が違う多層ニューラルネットワークで性能を把握できるようになった
 →共通の理由は計算機の計算能力の飛躍的な功により扱えるデータが増えた

GPU Technology ConferenceとかみるとGPUの発達が深層学習に与える影響が大きそうに感じる

計算機の処理能力向上によりニューラルネットワークの多層化をしたら思わぬ結果が出た

AnularJSのチュートリアルをやってみた

Angularのチュートリアルを通して使い方を覚えたいと思います。
今回学習用で作成したプロジェクトは以下からとってこれるようにしています。
GitHub - teruuuuuu/angular_handson

開発環境構築

angular-cliを使えるようにする

1.angular-cliをグローバルインストールする

npm install -g @angular/cli

2.プロジェクトを作ってみる

ng new my-app

3.アプリを起動する

cd my-app
ng serve –open

“ng serve"で開発用のサーバを起動しファイルに対して変更があればすぐに修正を反映する動きをします。–openのオプションをつけることでコマンド実行時にブラウザを自動で開きます。

Atomで開発

Atomで開発する場合はプラグインを探して入れておけば良さそうです。 atom-typescriptをとりあえず入れておいた

開発

angular-cliでのアプリ開発について

.angular-cli.json

開発環境の設定は.angular-cli.jsonで行なっており、最初に読み込みhtmlやスクリプト、ルートディレクトリやビルド時の出力先もここで設定している。アプリ全体でスタイルやスクリプトを適用したいという場合はここのscriptとstylesや、デフォルトのstyles.cssを修正する。アプリのルートディレクトリはsrcに設定してある。

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "project": {
    "name": "my-app"
  },
  "apps": [
    {
      "root": "src",
      "outDir": "dist",
      "assets": [
        "assets",
        "favicon.ico"
      ],
      "index": "index.html",
      "main": "main.ts",
      "polyfills": "polyfills.ts",
      "test": "test.ts",
      "tsconfig": "tsconfig.app.json",
      "testTsconfig": "tsconfig.spec.json",
      "prefix": "app",
      "styles": [
        "styles.css"
      ],
      "scripts": [],
      "environmentSource": "environments/environment.ts",
      "environments": {
        "dev": "environments/environment.ts",
        "prod": "environments/environment.prod.ts"
      }
    }
  ],
  "e2e": {
    "protractor": {
      "config": "./protractor.conf.js"
    }
  },
  "lint": [
    {
      "project": "src/tsconfig.app.json"
    },
    {
      "project": "src/tsconfig.spec.json"
    },
    {
      "project": "e2e/tsconfig.e2e.json"
    }
  ],
  "test": {
    "karma": {
      "config": "./karma.conf.js"
    }
  },
  "defaults": {
    "styleExt": "css",
    "component": {}
  }
}

index.html

angular-cliで作成したプロジェクトのindex.htmlは以下のようになっている。app-rootという独自のタグを読み込んでいます。コンポーネントが初期化されるまではタグの中身の"Loading…“が表示されます。

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>AngularHandson</title>
  <base href="/">

  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
  <app-root>Loading...</app-root>
</body>
</html>

main.ts

main.tsではアプリに必要なモジュールを読み込んでいます。環境変数などはJsonを読み込ませるかプロパティに直接定義するなどしてenvironmentで読み込めるようにしておくと良さそうです。リリース時はenableProdModeが有効になるようですが、angularの開発モードが無効になるなどの違いがあるようです。

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './main.module';
import { environment } from './environments/environment';

if (environment.production) {
  enableProdMode();
}

platformBrowserDynamic().bootstrapModule(AppModule);

main.module.ts

main.tsで読み込んでいるモジュール。NgModuleでページの単位となるコンポーネント一覧を読み込んでいるのが確認できる。bootstrapはエントリポイントを指定していて、providersにはデータ共有に使うサービスとかを指定して、importsでは外部のモジュールを読み込んだりしていて、declarationsではディレクティブとパイプを読み込ませるらしい。ディレクティブとは先ほどのapp-rootなど独自に定義したタグのことを言っており、パイプについてはangularのhtml内で条件指定でフィルターかけるのに使う。BrowserModuleはブラウザの情報を取ってくるので必要になるっぽい。

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

index.component.ts

Componentアノテーションのselectorではディレクティブと使用するタグ名、templeteUrlでテンプレートhtml、styleUrlsでcssを指定してからclassないでスクリプトを記述している。titleはクラス変数でapp.component.htmlの表示で使用している。TypeScriptのクラス変数ではconstやletを付与しようとしたらコンパイラに怒られたので普遍にする場合はstatic readonlyとかつけたら良さそうと思ったけどそうしたらbindできなくなる動きをしていた。

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './index.component.html',
  styleUrls: ['./index.component.css']
})
export class AppComponent {}

index.component.html

index.component.tsのhtml表示用のテンプレートは最初は以下のようになっています。

<!--The whole content below can be removed with the new code.-->
<div style="text-align:center">
  <h1>
  </h1>
</div>

htmlとコンポーネントで2wayバインディングさせてみる

まずmain.module.tsで2wayバインディングに必要となるFormModuleを読み込ませます main.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms'; //テンプレートでバインディングしたり、validationするのに必要

import { AppComponent } from './index.component';

@NgModule({
  imports: [
    BrowserModule,
    FormsModule
  ],
  declarations: [
    AppComponent
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

それからindex.component.tsとindex.component.htmlでコンポーネント内のクラス変数をbindさせてみます。 index.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './index.component.html',
  styleUrls: ['./index.component.css']
})
export class AppComponent {
  textInput = "input test";
}

index.component.html

<!--The whole content below can be removed with the new code.-->
<div style="text-align:center">
  <h1>
    <input [(ngModel)]="textInput"><br />
    {{ textInput }}
  </h1>
</div>

これで実際に動かしてみると画面にコンポーネントのクラス変数が表示され、またinputタグへ入力を行いbindしている変数を変更すると値自体が変更され表示にも反映されるのが確認できます。

ルータとサービスを使ってみる

angularのルータとサービスを利用してみたいと思います。angularのルータにより表示するコンポーネントを切り替えたりURLパラメータを受け取った処理ができるようになります。サービスはAPIを投げてデータを取得したりとか、コンポーネント間で共有するデータとかデータに関する操作とかを記述します。 まずルータを足してみます。 app/router/app.router.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { HeroDetailComponent } from 'app/component/heroDetail/hero.detail.component';

// コンポーネントとURLを関連づける
const routes: Routes = [
  { path: '', redirectTo: '/detail/1', pathMatch: 'full' },
  // URLパラメータをコンポーネントに渡すようにしている
  { path: 'detail/:id', component: HeroDetailComponent },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

ルータではURLとコンポーネントマッピングを行います。

app/service/hero.service.ts

import { Injectable } from '@angular/core';

import { Hero } from 'app/model/Hero';
import { HEROES } from 'app/mock/heros.mock';



@Injectable()
export class HeroService {

  getHeroes(): Promise<Hero[]> {
    console.log("hero service getHeros");
    console.info(HEROES); // 2wayバインドによりmockオブジェクト自体が変更されていることが確認できる
    return Promise.resolve(HEROES);
  }

  getHeroesSlowly(): Promise<Hero[]> {
    return new Promise(resolve => {
      // Simulate server latency with 2 second delay
      setTimeout(() => resolve(this.getHeroes()), 2000);
    });
  }

  // id指定でデータ取得
  getHeroById(id: number): Promise<Hero> {
    return this.getHeroes()
      .then(heroes => heroes.find(hero => hero.id === id));
  }
}

サービスはインポートしたHEROESオブジェクトをPromiseにより非同期でレスポンスとして返す処理を行います。今回使用しているモック用のオブジェクトは以下のようになっています。 app/mock/heros.mock.ts

import { Hero } from 'app/model/Hero';

export const HEROES: Hero[] = [
  { id: 1, name: 'Mr. Nick' },
  { id: 11, name: 'Mr. Nice' },
  { id: 12, name: 'Narco' },
  { id: 13, name: 'Bombasto' },
  { id: 14, name: 'Celeritas' },
  { id: 15, name: 'Magneta' },
  { id: 16, name: 'RubberMan' },
  { id: 17, name: 'Dynama' },
  { id: 18, name: 'Dr IQ' },
  { id: 19, name: 'Magma' },
  { id: 20, name: 'Tornado' }
];

それと、Heroの型は以下のようになっています。 app/model/Hero.ts

export class Hero {
  id: number;
  name: string;
}

次にルータとサービスをモジュールとして使えるようにコンポーネントに読み込ませます。 main.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms'; //テンプレートでバインディングしたり、validationするのに必要

import { AppComponent } from 'index.component';
import { HeroDetailComponent } from 'app/component/heroDetail/hero.detail.component';

import { HeroService } from 'app/service/hero.service';
import { AppRoutingModule } from 'app/router/app.router';

@NgModule({
  imports: [
    AppRoutingModule, // 注意 ルータはdeclationではなくimportsにたす
    BrowserModule,
    FormsModule
  ],
  declarations: [
    AppComponent,
    HeroDetailComponent
  ],
  providers: [
    HeroService // サービスはprovidersに追加する
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

次にルータとサービスで使用するHeroDetailComponentを追加してみたいと思います。
app/component/heroDetail/hero.detail.component.ts

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';

import { Hero } from 'app/model/Hero';
import { HeroService } from 'app/service/hero.service';


@Component({
  selector: 'hero-detail', //ディレクティブのタグ名
  templateUrl: './hero.detail.component.html' //htmlテンプレートの読み込み
})
export class HeroDetailComponent implements OnInit {
  // テンプレートhtmlにbindして使用するクラス変数
  title = 'HeroDetail';
  hero: Hero = new Hero();

  // コンポーネントを使用する側で用途を決めれるようにする
  isSearchMode: Boolean = true;


  constructor(
    private heroService: HeroService,
    // urlパラメータを取得するのに必要
    private route: ActivatedRoute) {
  }

  ngOnInit(): void {
    if(this.isSearchMode){
      // ルータからパラメータ取得
      this.route.params.forEach((params: Params) => {
        console.log("hero detail component ngOnInit");
        console.info(params);
        if (params['id'] !== undefined) {
          const id = +params['id'];
          this.heroService.getHeroById(id)
              .then(hero => this.hero = hero);
        }
      });
    }
  }
}

app/component/heroDetail/hero.detail.component.html

<!-- heroが見つかった時のみこの部分を表示する-->
<div *ngIf="hero">
  <h1>{{title}}</h1>
  <h2>{{hero.name}} details!</h2>
  <div><label>id: </label>{{hero.id}}</div>
  <div>
    <label>name: </label>
    <!-- 注意 ngMoelを使う場合はNgModuleでFormsModuleをインポートしないといけない-->
    <input [(ngModel)]="hero.name" placeholder="name">
  </div>
</div>

<!-- heroが見つからなかった場合の処理 -->
<div *ngIf="!hero">
  hero not found.
</div>

app/component/heroDetail/hero.detail.component.css

h1 {
  color: #369;
  font-family: Arial, Helvetica, sans-serif;
  font-size: 250%;
}

それから、ルータのディレクティブをhtmlテンプレートに追加して使用できるようにします。 index.component.html

<!--The whole content below can be removed with the new code.-->
<div style="text-align:center">
  <h1>
    <input [(ngModel)]="textInput"><br />
    {{ textInput }}<br />
</div>
<!-- ルータ配置用のテンプレート -->
<router-outlet></router-outlet>

これで動かしてみると'http://localhost:4200'のアクセスは'http://localhost:4200/detail/1'にリダイレクトされid=1のユーザが表示されるはずです。idの部分のパラメータを変更することで表示するユーザが切り替わることが確認できます。

コンポーネントでも同一のサービスを使ってみる

コンポーネントで同一のサービスを利用し2way-bindingによりサービス館でデータが共有されていることを確認します。 まず今回使用するサービスに以下のメソッドを追加します。先に追加している非同期の処理でも大丈夫ですが今回は既にサービスコンポーネントで保有されているデータを返す処理を追加したく非同期である必要はなさそうなのでそれ用のメソッドを追加しています。

// データ共有をするだけの用途とかでPromiseを使わないこともできる
getSyncHero(id: number): Hero {
  return HEROES.find(hero => hero.id === id);
}

それから、以下のように既存のindex.component.tsでサービスを利用するように変更します。 index.component.ts

import { Component, OnInit } from '@angular/core';

import { Hero } from 'app/model/Hero';
import { HeroService } from 'app/service/hero.service';

@Component({
  selector: 'app-root',
  templateUrl: './index.component.html',
  styleUrls: ['./index.component.css']
})
export class AppComponent implements OnInit {
  textInput = "input test";
  hero: Hero = new Hero();

  constructor(
    private heroService: HeroService) {
  }

  ngOnInit(): void {
    this.hero = this.heroService.getSyncHero(1);
  }
}

index.component.html

<!--The whole content below can be removed with the new code.-->
<div style="text-align:center">
  <h1>
    <input [(ngModel)]="textInput"><br />
    {{ textInput }}<br />
    <input [(ngModel)]="hero.name" placeholder="name"><br />
    {{ hero.name }}
  </h1>
</div>
<!-- ルータ配置用のテンプレート -->
<router-outlet></router-outlet>

これでindex.componentにもHeroの情報が表示され、またそれぞれのinputに対して入力すると即時で反映されることが確認できます。

htmlテンプレートでループ処理をする

次にhtmlテンプレート内でループを回して描画を行ってみたいと思います。リストの情報を取得する以下のコンポーネントを追加します。
app/component/heroList/hero.list.component.ts

import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';

import { Hero } from 'app/model/Hero';
import { HeroService } from 'app/service/hero.service';


@Component({
  selector: 'hero-list',
  templateUrl: './hero.list.component.html',
  styleUrls: ['./hero.list.component.css']
})
export class HeroListComponent implements OnInit {
  heroes: Hero[] = [];
  title = 'HeroesList';
  selectedHero: Hero;

  // サービスはconstructorに足しておく
  constructor(
    private router: Router,
    private heroService: HeroService) {
  }

  ngOnInit(): void {
    // 再描画のたびに呼ばれるので、ここでメンバ変数を初期化
    console.log("HeroListComponent ngOnInit")
    this.heroService.getHeroes()
      .then(heroes => this.heroes = heroes);
    /*
    this.heroService.getHeroesSlowly()
      .then(heroes => this.heroes = heroes);
      */
  }

  onSelect(hero: Hero): void {
    this.selectedHero = hero;
  }
}

それから、htmlテンプレートを作成します。

<h1>{{title}}</h1>
<h2>My Heroes</h2>
<ul class="heroes">
  <li *ngFor="let hero of heroes" (click)="onSelect(hero)" [class.selected]="hero === selectedHero">
    <span class="hero-element">
      <span class="badge">{{hero.id}}</span> {{hero.name}}</span>
  </li>
</ul>

上記の<li *ngFor=“let hero of heroes” ~の部分がコンポーネント内のメンバ変数であるheroesをループさせて描画処理を 行っています。hero === selectedHeroの条件が一致している場合はタグのクラスに"selected"を追加します。
それとcssも作成しておきます。

.selected {
  background-color: #CFD8DC !important;
  background-color: rgb(0,120,215) !important;
  color: white;
}
.heroes {
  margin: 0 0 2em 0;
  list-style-type: none;
  padding: 0;
  width: 15em;
}
.heroes li {
  cursor: pointer;
  position: relative;
  left: 0;
  background-color: #EEE;
  margin: .5em;
  padding: .5em;
  height: 1.6em;
  border-radius: 4px;
}
.heroes li:hover {
  color: #607D8B;
  color: rgb(0,120,215);
  background-color: #DDD;
  left: .1em;
}
.heroes li.selected:hover {
  /*background-color: #BBD8DC !important;*/
  color: white;
}
.heroes .text {
  position: relative;
  top: -3px;
}
.heroes .badge {
  display: inline-block;
  font-size: small;
  color: white;
  padding: 0.8em 0.7em 0 0.7em;
  background-color: #607D8B;
  background-color: rgb(0,120,215);
  line-height: 1em;
  position: relative;
  left: -1px;
  top: -4px;
  height: 1.8em;
  margin-right: .8em;
  border-radius: 4px 0 0 4px;
}
button {
  font-family: Arial;
  background-color: #eee;
  border: none;
  padding: 5px 10px;
  border-radius: 4px;
  cursor: pointer;
  cursor: hand;
}
button:hover {
  background-color: #cfd8dc;
}
.error {color:red;}
button.delete-button{
  float:right;
  background-color: gray !important;
  background-color: rgb(216,59,1) !important;
  color:white;
}

あとは、main.moduleに今回のモジュールを追加して、ルータでURLとコンポーネントを関連づけることで表示が行えます。 main.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms'; //テンプレートでバインディングしたり、validationするのに必要

import { AppComponent } from 'index.component';
import { HeroDetailComponent } from 'app/component/heroDetail/hero.detail.component';
import { HeroListComponent } from 'app/component/heroList/hero.list.component';

import { HeroService } from 'app/service/hero.service';
import { AppRoutingModule } from 'app/router/app.router';

@NgModule({
  imports: [
    AppRoutingModule, // 注意 ルータはdeclationではなくimportsにたす
    BrowserModule,
    FormsModule
  ],
  declarations: [
    AppComponent,
    HeroDetailComponent,
    HeroListComponent
  ],
  providers: [
    HeroService
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

app/router/app.router.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { HeroDetailComponent } from 'app/component/heroDetail/hero.detail.component';
import { HeroListComponent } from 'app/component/heroList/hero.list.component';

// コンポーネントとURLを関連づける
const routes: Routes = [
  { path: '', redirectTo: '/list', pathMatch: 'full' },
  { path: 'detail/:id', component: HeroDetailComponent },
  { path: 'list', component: HeroListComponent }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

コンポーネントにデータを渡してみる

サービスを使ってコンポーネント間でデータが共有できるのは確認できましたので、次はサービスを使わずに直接コンポーネントに対してデータが渡せる確認してみたいと思います。
まず、app/component/heroList/hero.list.component.htmlに以下を追加します。

<!-- コンポーネントのメンバ変数を[]で囲ったものに対して選択したheroを渡す -->
<hero-detail [hero]="selectedHero" [isSearchMode]="false"></hero-detail>

それからapp/component/heroDetail/hero.detail.component.tsのメンバ変数に@Input()を付与することで親コンポーネントからデータを受け取ることができるようになります。 app/component/heroDetail/hero.detail.component.ts

import { Component, Input, OnInit } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';

import { Hero } from 'app/model/Hero';
import { HeroService } from 'app/service/hero.service';


@Component({
  selector: 'hero-detail', //ディレクティブのタグ名
  templateUrl: './hero.detail.component.html' //htmlテンプレートの読み込み
})
export class HeroDetailComponent implements OnInit {
  // テンプレートhtmlにbindして使用するクラス変数
  title = 'HeroDetail';
  @Input()  hero: Hero = new Hero();

  // コンポーネントを使用する側で用途を決めれるようにする
  @Input()  isSearchMode: Boolean = true;


  constructor(
    private heroService: HeroService,
    // urlパラメータを取得するのに必要
    private route: ActivatedRoute) {
  }

  ngOnInit(): void {
    if(this.isSearchMode){
      // ルータからパラメータ取得
      this.route.params.forEach((params: Params) => {
        console.log("hero detail component ngOnInit");
        console.info(params);
        if (params['id'] !== undefined) {
          const id = +params['id'];
          this.heroService.getHeroById(id)
              .then(hero => this.hero = hero);
        }
      });
    }
  }
}

コンポーネント間で画面遷移してみる

コンポーネント間で遷移できるようにするためまずapp/component/heroList/hero.list.component.tsに以下のメソッドを追加します。

gotoDetail(): void {
  this.router.navigate(['/detail', this.selectedHero.id]);
}

それから、app/component/heroList/hero.list.component.htmlからgotoDetailを呼び出せるようにするため、以下のように修正します。

<h1>{{title}}</h1>
<h2>My Heroes</h2>
<ul class="heroes">
  <li *ngFor="let hero of heroes" (click)="onSelect(hero)" [class.selected]="hero === selectedHero">
    <span class="hero-element">
      <span class="badge">{{hero.id}}</span> {{hero.name}}</span>
  </li>
</ul>

<!-- コンポーネントのメンバ変数を[]で囲ったものに対して選択したheroを渡す -->
<!--
<hero-detail [hero]="selectedHero" [isSearchMode]="false"></hero-detail>
-->

<div *ngIf="selectedHero">
  <h2>
    {{selectedHero.name | uppercase}} is my hero
  </h2>
  <button (click)="gotoDetail()">View Details</button>
</div>

次にapp/compnent/heroDetail/hero.detail.compnentでは遷移元に戻れるように以下のメソッドを追加します。

goBack(savedHero: Hero = null): void {
  window.history.back();
}

それからapp/compnent/heroDetail/hero.detail.compnent.htmlから呼び出せるように以下のように修正します。

<!-- heroが見つかった時のみこの部分を表示する-->
<div *ngIf="hero">
  <h1>{{title}}</h1>
  <h2>{{hero.name}} details!</h2>
  <div><label>id: </label>{{hero.id}}</div>
  <div>
    <label>name: </label>
    <!-- 注意 ngMoelを使う場合はNgModuleでFormsModuleをインポートしないといけない-->
    <input [(ngModel)]="hero.name" placeholder="name"><br />
    <button (click)="goBack()">Back</button>
  </div>
</div>

<!-- heroが見つからなかった場合の処理 -->
<div *ngIf="!hero">
  hero not found.
</div>

これでコンポーネント間での画面繊維が確認できたかと思います。

もう一つコンポーネントを追加してみる

次にダッシュボードコンポーネントを追加して、こちらからもhero.detail.compnentに遷移できるようにしたいと思います。 以下のapp/component/dashboard/dashboard.component.tsを作成します。

import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';

import { Hero } from 'app/model/Hero';
import { HeroService } from 'app/service/hero.service';

@Component({
  selector: 'my-dashboard',
  templateUrl: './dashboard.component.html',
  styleUrls: ['./dashboard.component.css']
})
export class DashboardComponent implements OnInit {
  heroes: Hero[] = [];

  constructor(
    private router: Router,
    private heroService: HeroService) {
  }

  ngOnInit(): void {
    this.heroService.getHeroes()
      .then(heroes => this.heroes = heroes.slice(1, 6));
  }

  gotoDetail(hero: Hero): void {
    const link = ['/detail', hero.id];
    this.router.navigate(link);
  }
}

それからhtmlテンプレートを作成します。

<div class="grid grid-pad">
  <h3>Top Heroes</h3>
  <div *ngFor="let hero of heroes" (click)="gotoDetail(hero)"  class="col-1-4">
    <div class="module hero">
      <h4>{{hero.name}}</h4>
    </div>
  </div>
</div>

あとはmain.module.tsでDashboardComponentを読み込むようにし、ルータに追加しておくと画面が表示されるようになります。

httpリクエストを投げれるようにしてみる

次にhttpリクエストを投げれるようにしてみます。リクエストを受けるWEBサーバを準備するのは面倒なのでangularのモックを利用します。そのためにはmain.module.tsで以下のモジュールをインポートするようにします。

import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; // npm install --save angular-in-memory-web-api

“angular-in-memory-web-api"はangular本体に組み込まれていないので以下のコマンドでインストールしておきます。

npm install –save angular-in-memory-web-api

それから、レスポンスとして返すデータを定義するapp/service/in-memory-data.service.tsを作成します。

// angular-in-memory-web-apiで使うモックのapiの初期データ
export class InMemoryDataService {
  createDb() {
    const heroes = [
      { id: 1, name: 'one' },
      { id: 11, name: 'Mr. Nice' },
      { id: 12, name: 'Narco' },
      { id: 13, name: 'Bombasto' },
      { id: 14, name: 'Celeritas' },
      { id: 15, name: 'Magneta' },
      { id: 16, name: 'RubberMan' },
      { id: 17, name: 'Dynama' },
      { id: 18, name: 'Dr IQ' },
      { id: 19, name: 'Magma' },
      { id: 20, name: 'Tornado' }
    ];
    return { heroes };
  }
}

ここまで済んだらmain.module.tsを以下のように修正しWebAPIのモックを使用できるようにします。

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms'; //テンプレートでバインディングしたり、validationするのに必要
import { HttpModule }    from '@angular/http'; // httpサービスを利用するのに必要

// 今回はWebAPIのモックを使用する
import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; // npm install --save angular-in-memory-web-api
import { InMemoryDataService }  from 'app/service/in-memory-data.service';

import { AppComponent } from 'index.component';
import { HeroDetailComponent } from 'app/component/heroDetail/hero.detail.component';
import { HeroListComponent } from 'app/component/heroList/hero.list.component';
import { DashboardComponent } from 'app/component/dashboard/dashboard.component';

import { HeroService } from 'app/service/hero.service';
import { AppRoutingModule } from 'app/router/app.router';

@NgModule({
  imports: [
    AppRoutingModule, // 注意 ルータはdeclationではなくimportsにたす
    BrowserModule,
    FormsModule,
    HttpModule,
    InMemoryWebApiModule.forRoot(InMemoryDataService)
  ],
  declarations: [
    AppComponent,
    HeroDetailComponent,
    HeroListComponent,
    DashboardComponent
  ],
  providers: [
    HeroService
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

次にapp/service/hero.service.tsを修正しWebAPI経由でデータを取得するようにします。まず"angular/http"モジュールとrxjsのtoPromiseをインポートします。 angular/httpのレスポンスはrxjsのtoPromiseで非同期で扱うのでrxjsのインポートも必要になります。

import { Headers, Http, Response } from '@angular/http';
import 'rxjs/add/operator/toPromise';

それからコンポーネント内のメッソッドを以下のように修正しWebAPI経由でデータを取得するように変更します。

private heroesUrl = 'api/heroes';  // URL to web api
private headers = new Headers({ 'Content-Type': 'application/json' });

constructor(private http: Http) { }

getHeroes(): Promise<Hero[]> {
  console.log("hero service getHeros");
  return this.http.get(this.heroesUrl)
    .toPromise()
    // jsonのレスポンスを受け取ってHero型の配列に変換する
    .then(response => response.json().data as Hero[])
    .catch(this.handleError);
}

getHeroById(id: number): Promise<Hero> {
  const url = `${this.heroesUrl}/${id}`;
  return this.http.get(url)
    .toPromise()
    .then(response => response.json().data as Hero)
    .catch(this.handleError);
}

// httpリクエスト失敗時の処理
private handleError(error: any): Promise<any> {
  console.error('An error occurred', error);
  return Promise.reject(error.message || error);
}

今回はrxjsを使用していますがレスポンスのjsonをHero型の配列に変換するだけなので

.toPromise()
.then(response => response.json().data as Hero)

のようになっています。

動かしてみると一覧表示をする際に毎回データを撮り直しているため、heroの名前を変更して一覧に戻ると変更が反映されないというのが確認できるかと思います。これまではサービスをコンポーネント間でのデータの共有として使っていたのですが、今回の修正でサービスをWebサーバに対してサービスを投げる用途で使うようにしたのでその違いはわかるようにしておきたいです。例えば共通のAPIで取得した結果を複数のコンポーネントで使うという必要があるのでしたら、サービスコンポーネント内にデータ保有用の変数を用意しておきWebAPIを呼び出した後はその変数を変更するようにする必要があるかと思います。

追加、更新、削除のリクエストを投げれるようにしてみる

サービス側に追加、更新、削除のリクエストを投げるメソッドを追加します。

// angular-in-memory-web-apiのcreateApi呼び出し
create(name: string): Promise<Hero> {
  return this.http
    .post(this.heroesUrl, JSON.stringify({ name: name }), { headers: this.headers })
    .toPromise()
    .then(res => res.json().data as Hero)
    .catch(this.handleError);
}

// angular-in-memory-web-apiのupdateApi呼び出し
update(hero: Hero): Promise<Hero> {
  const url = `${this.heroesUrl}/${hero.id}`;
  return this.http
    .put(url, JSON.stringify(hero), { headers: this.headers })
    .toPromise()
    .then(() => hero)
    .catch(this.handleError);
}

delete(id: number): Promise<void> {
  const url = `${this.heroesUrl}/${id}`;
  return this.http.delete(url, { headers: this.headers })
    .toPromise()
    .then(() => null)
    .catch(this.handleError);
}

httpモジュールのpost、put、deleteを使い分けていますが違いはここのstackoverflowを確認するのが良さそうです。

あとは各コンポーネントからサービスを利用するようにしたら、heroの名前変更が一覧に反映されたり、登録、削除が確認できるかと思います。

検索を行ってみる

サービスに検索用のリクエストを投げるメソッドを追加します。

search(term: string): Observable<Hero[]> {
  return this.http
    .get(`app/heroes/?name=${term}`)
    .map(response => response.json().data as Hero[]);
}

レスポンスをそのままHero型の配列にセットするだけなので今までと同様にtoPromise()~で大丈夫かと思ったのですが、getリクエストの場合はtoPromiseが使えないようです。 この辺りはrxjsとhttpモジュール周りの学習が必要になりそうです。 それから以下のapp/component/heroSearch/hero.search.componentモジュールを追加してdashboardコンポーネントに配置すると検索コンポーネントが使えるようになります。 app/component/search/hero.search.component.ts

import { Component, OnInit } from '@angular/core';
import { Router }            from '@angular/router';

import { Observable }        from 'rxjs/Observable';
import { Subject }           from 'rxjs/Subject';

// Observable class extensions
import 'rxjs/add/observable/of';

// Observable operators
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
import 'rxjs/add/operator/switchMap';

import { HeroService } from 'app/service/hero.service';
import { Hero } from 'app/model/Hero';

@Component({
  selector: 'hero-search',
  templateUrl: './hero.search.component.html',
  styleUrls: [ './hero.search.component.css' ],
  providers: [HeroService]
})
export class HeroSearchComponent implements OnInit {
  heroes: Observable<Hero[]>;
  private searchTerms = new Subject<string>();

  constructor(
    private heroService: HeroService,
    private router: Router) {}

  // Push a search term into the observable stream.
  search(term: string): void {
    this.searchTerms.next(term);
  }

  ngOnInit(): void {
    this.heroes = this.searchTerms
      .debounceTime(300)        // wait 300ms after each keystroke before considering the term
      .distinctUntilChanged()   // ignore if next search term is same as previous
      .switchMap(term => term   // switch to new observable each time the term changes
        // return the http search observable
        ? this.heroService.search(term)
        // or the observable of empty heroes if there was no search term
        : Observable.of<Hero[]>([]))
      .catch(error => {
        // TODO: add real error handling
        console.log(error);
        return Observable.of<Hero[]>([]);
      });
  }

  gotoDetail(hero: Hero): void {
    let link = ['/detail', hero.id];
    this.router.navigate(link);
  }
}

app/component/heroSearch/hero.search.comonent.html

<div id="search-component">
  <h4>Hero Search</h4>
  <input #searchBox id="search-box" (keyup)="search(searchBox.value)" />
  <div>
    <div *ngFor="let hero of heroes | async"
         (click)="gotoDetail(hero)" class="search-result" >
      {{hero.name}}
    </div>
  </div>
</div>

app/component/heroSearch/hero.search.component.css

.search-result{
  border-bottom: 1px solid gray;
  border-left: 1px solid gray;
  border-right: 1px solid gray;
  width:195px;
  height: 16px;
  padding: 5px;
  background-color: white;
  cursor: pointer;
}

.search-result:hover {
  color: #eee;
  background-color: #607D8B;
}

#search-box{
  width: 200px;
  height: 20px;
}

検索用のコンポーネントでは入力に変更があったらrxjsでサービスを呼び出すようにしています。angularのチュートリアルでrxjsが使われているので、angularをやるならrxjsを覚えていた方が良さそうに思いました。 あとは今までと同様main.module.tsでモジュールを読み込むようにしダッシュボードコンポーネントに表示するようにして動作は確認できるかと思います。

 触ってみた感想

Angularの1は触ったことがあったのですが、それに比べてだいぶ分かりやすくて扱いやすくなったと思います。特にルータ周りはReactと比べて優位に立ってそうな気がしました。