astamuse Lab

astamuse Labとは、アスタミューゼのエンジニアとデザイナーのブログです。アスタミューゼの事業・サービスを支えている知識と舞台裏の今を発信しています。

初公開!データエンジニアのデスクトップ。1/3日の順調な環境移行

こんにちは、福田です。

新緑が目を刺すGW明け、オフィスを引っ越しました。最近人が増えてきたため、開発・デザイン部は7Fから、8Fの新しいスペースへの移動です。なので、正確にはフロアを跨ぐ座席移動になります。私にとって、6回目の社内引越です。

f:id:astamuse:20180515180658j:plain

午前。ミーティングを終え、先陣を切って機材の移動を開始。台車に乗せ、エレベータで上階へ。チームメイトの朴さんが優しく手伝ってくれます。いつもありがとうございます。

lab.astamuse.co.jp

一番乗り、と勇み足で入室。しかしそこには人影が。仕事が速く正確なことで有名なtorigakiさんが既に移動を済ませ、軽快にMacBook Proを叩いていました。そのスピードに舌を巻きます。

lab.astamuse.co.jp

ひと通り自席の配線を済ませたところで、今回の引越しの裏方で皆を支えてくださったnishikawaさんとランチへ。175°DENOでの担々麺。花椒の香りに誘われて、会話に花が咲きます。

f:id:astamuse:20180515180828j:plain

lab.astamuse.co.jp

午後、マシンに火を入れます。お願い、動いてっ!  

f:id:astamuse:20180515180912j:plain

殺風景だった空間に華やぎと活気が。自分へのご褒美、グリーンスムージーで一息。不足しがちな食物繊維を補給します。デスク右手は愛用のプラズマクラスター加湿器。PCの筐体ではありません。

f:id:astamuse:20180515180945j:plain

足回りの古式ゆかしいマシン3台。OSは全てGentoo Linux。カーネルは最新です。メイン機に挿したNVIDIAのカードから3枚のディスプレイに出力。サブ機では、仮想環境で検証用のHadoopクラスタが動いていたり、コンテナの実行環境になっており、日々のR&DやPoCに重宝しています。

f:id:astamuse:20180515181018j:plain

グリーンも、滞りなくデプロイ完了です。手塩に掛けたパキラが高くそびえ立ちます。成長著しく伸びすぎた感があるので、ひと段落したら少し剪定を。

f:id:astamuse:20180515181046j:plain

さて、環境が整ったところで心機一転、仕事に戻りたいと思います。ありがとうございました。

アスタミューゼでは、エンジニア・デザイナーを募集しています。ご興味のある方はこちらからご応募ください。お待ちしております。

recruit.astamuse.co.jp

エンジニアとの遭遇~バックオフィス編

はじめまして。アスタミューゼのバックオフィス所属、「数字の国のヒト」と申します。どうぞ宜しくお願いいたします。 素人の私がなぜこのブログに...はい、開発・インフラ部長からご指名受けちゃったからですねー。 (デスクで圧倒的な存在感をかもし出す、通称「白い悪魔」ちゃん。)

f:id:astamuse:20180501103845j:plain

読者の皆様が毎度楽しみにしている技術的なお話は、今回全く登場しなくてスミマセン。バックオフィスから見た、アスタミューゼの開発・デザインチームの雰囲気が少しでも伝われば幸いです。

エンジニアとの遭遇~序章1:課金編

最初にエンジニアの方々と接点を持ったのは、米国系インターネットサービス会社の日本法人起ち上げでした。本国のサービスを日本で提供するにあたり、ローカライゼーションやQAのエンジニア達に囲まれる日々。 出社したら会議室から物音が...入ってみたら、エンジニア氏が寝袋で寝ていたなんて驚き現象も。

仕事上の主な接点は、

  • ユーザの課金情報登録
  • 売上数値の収集、帳簿への計上
  • 付随する費用の確認、計上

などなど、「数字の国」関連の内容を正確に反映してもらうべく、エンジニアの方々に仕組みを作成していただく事でした。「売上データ締まりましたか!?」「鋭意集計中です!!」の攻防が飛び交う日々。 f:id:astamuse:20180501110847j:plain

その後、英国系シネマコンプレックスの立ち上げ直後に参加した際も、インターネット予約システムから売上、ポイント等、やはりお金回りでの接点でした。「数字の国のヒト」継続です。

エンジニアとの遭遇~序章2 :採用編

次なるエンジニアの方々との接点は、米国系IT企業の日本法人でした。こちらも起ち上げフェーズ、ロードバランシングやリモートアクセスが当時のメイン製品でした。

f:id:astamuse:20180427181432j:plain

前職との違いは、「数字の国のヒト」に加えて、「人の国のヒト」の二刀流になった点です。

エンジニア経験皆無の私が、事業拡大に伴い採用業務も担うことに。TCP/IPとか、L7・L3・L2とか、Pingとか、Application Delivery Networkとか...初めて聞く言葉だらけ!目を回す日々の連続でしたが、現場のエンジニアさんやマネージャーさん達と面接を数多くこなすうちに、いつしかリクルーターや候補者の方々に製品の魅力を伝えたり、書類選考までこなせるようになったから不思議なものです。

この経験で、エンジニアの人達が持っている技術の幅広さ、奥深さを改めて感じる事ができたと同時に、技術について熱く語る彼ら彼女らの姿が今でも強く印象に残っています。

エンジニアとの遭遇~序章3 :ERP導入編

更に接点は続きます。本社向けの月次・四半期・年次決算報告、予実管理、着地予想等...「数字の国」に100%帰国した中での新しいチャレンジ、欧州系医療機器メーカーでのERP導入です。

f:id:astamuse:20180501105707j:plain

本国で使用しているERPをそのまま日本で導入、ローカライズする予算はとても限られている。その為には目の前に立ちふさがる

  • ダブルバイト問題(納品書とか請求書、日本語必須だし!)
  • 紙のサイズ違う問題(宛名表示の位置、おかしくない?)
  • 時差問題(日本その時間営業してるし、メンテとかやめて!)
  • 会計基準の違い問題 (日本基準と国際会計基準の2帳簿を保持しないとまずいでしょ?)

などなど...本社側の精通したエンジニアさん達と、製造元の日本側システムコンサルさんが、イチから一緒に取り組んでくれました。新年度、新しいERPに切り替えての運用が始まった時の達成感は、やはり大きかったです。

エンジニアとの遭遇~本題 :アスタミューゼ編

筆者の「エンジニア遭遇レベル:初級」がお分かりいただけたところで、やっと本題、アスタミューゼの登場です。

  • 新規事業開発/技術活用コンサルティング
  • 人材採用/キャリア支援
  • 知的情報Webプラットフォーム

3つの柱を軸に、自社でWebsite、データベースを開発・運用している会社での勤務経験は初めて。エンジニアの方々と「数字の国のヒト」とは、どんな接点があるのでしょうか。

過去の遭遇エピソードとの大きな違いの1つは、ERPや在庫管理等、経理システムとの連動が無いこと。「数字の国のヒト」との接点は非常に少なく...さ、淋しいっ!!そんな少ない接点でも力強く伝わってくるのは、チームワークの良さです。開発・デザイン本部内はもちろんなのですが、事業サイドとの連携も強化されています。運営しているサイトの改善、蓄積しているデータベースの整備・活用、新規事業起ち上げに向けての開発等...密に接している姿が多く見られます(う、羨ましい)。

f:id:astamuse:20180501114958j:plain

もう1つの大きな違いは、エンジニアの方々のワークライフバランスが良く取れている点です。「顧客のシステムメンテ対応のために休日出勤」とか、「顧客のサーバーダウンの為の緊急出動」とか「新製品リリース直前のローカライゼーションで超激務」など、今まで見てきたあるあるな状況は殆ど目にしていません。 オン・オフをしっかり切り替える、フレックス勤務が上手に活用されている良い環境だと感じます。イクメンも多数いらっしゃいます。

最近では、新人の方も増え、入社後から独り立ちまでをスムーズに繋げる研修プログラムも組み立てつつ、益々チームワークが強まってきている印象です。 シャイな印象の人が多い(あくまでも個人の感想です)のですが、お声をかけるとちゃんと答えてくださる。家電製品・植物・スマートフォン等...技術以外の話題ばかりで話しかけてしまう私にも、優しく対応してくださいます。ハッ、お仕事の邪魔をしているのでは(汗

そんなアスタミューゼでは、エンジニア・デザイナー絶賛募集中です。ご興味をお持ちくださった方、是非ご応募ください。遭遇エピソードへの新たな章を書き加えるべく、お待ちしております!

以上、「数字の国のヒト」でした。

CoreNLPを使ってみる(3) Spark編

山縣です。

前回に引き続き CoreNLP を触っていきたいと思います。 前回までに API の使い方を見てきたので、今回は Spark からの使い方を見ていきたいと思います。

spark-corenlp

セットアップ

spark からCoreNLPを容易に使用する方法として spark-corenlp パッケージがあります。 spark-corenlp は Spark Packages で公開されています。また CoreNLP も Maven Repository で公開されており、下記のように spark shell の引数 --packages で指定するだけで利用することができます。 (弊社では CDH の spark 2.2.0を scala 2.11で利用しています)

$ spark2-shell --packages databricks:spark-corenlp:0.2.0-s_2.11,edu.stanford.nlp:stanford-corenlp:3.6.0

ためしに下記のサンプルを実行してみます。

import org.apache.spark.sql.functions._
import com.databricks.spark.corenlp.functions._

import spark.implicits._

val input = Seq(
  (1, "Stanford University is located in California. It is a great university.")
).toDF("id", "text")

val output = input
  .select(explode(ssplit('text)).as('sen))
  .select('sen, tokenize('sen).as('words), ner('sen).as('nerTags), sentiment('sen).as('sentiment))

output.show(truncate = false)

pasteモードでコピペして Ctrl-D します。

scala> :pa
// Entering paste mode (ctrl-D to finish)

import org.apache.spark.sql.functions._
import com.databricks.spark.corenlp.functions._
...

// Exiting paste mode, now interpreting.

18/04/20 14:42:51 WARN scheduler.TaskSetManager: Lost task 0.0 in stage 0.0 (TID 0, cdh-dn03-dt.c.stg-astamuse-astamus
e.internal, executor 1): org.apache.spark.SparkException: Failed to execute user defined function(anonfun$ner$1: (stri
ng) => array<string>)
        at org.apache.spark.sql.catalyst.expressions.GeneratedClass$GeneratedIterator.processNext(Unknown Source)
...
Caused by: java.io.IOException: Unable to open "edu/stanford/nlp/models/pos-tagger/english-left3words/english-left3wor
ds-distsim.tagger" as class path, filename or URL
        at edu.stanford.nlp.io.IOUtils.getInputStreamFromURLOrClasspathOrFileSystem(IOUtils.java:485)

エラーになってしまいました。モデルデータが無いのが原因です。Maven にはモデルデータも上がっているのですが、 使用するためには classifier を指定する必要があります。しかし --packages でのclassifier の記述の仕方が不明でうまく指定することが出来ませんでした。仕方がないのでモデルファイルをダウンロードして直接 jar ファイルを指定することで解決しました。

$ spark2-shell --packages databricks:spark-corenlp:0.2.0-s_2.11,edu.stanford.nlp:stanford-corenlp:3.6.0 \
  --jars ./libs/stanford-corenlp-3.6.0-models-english.jar

上記のようにダウンロードした英語のモデルデータ stanford-corenlp-3.6.0-models-english.jar へのパスを --jars で指定します。

再度実行してみます。

scala> :pa
// Entering paste mode (ctrl-D to finish)

import org.apache.spark.sql.functions._
...
// Exiting paste mode, now interpreting.

+----------------------------------------------+----------------------------------------------------+--------------------------------------------------+---------+
|sen                                           |words                                                 |nerTags                                   |sentiment|
+----------------------------------------------+----------------------------------------------------+--------------------------------------------------+---------+
|Stanford University is located in California .|[Stanford, University, is, located, in, California, .]|[ORGANIZATION,ORGANIZATION, O, O, O, LOCATION, O]|1        ||It is a great university .                    |[It, is, a, great, university, .]                     |[O, O, O, O, O, O]                                |4        |
+----------------------------------------------+----------------------------------------------------+--------------------------------------------------+---------+

今度は実行されました。これで Spark から CoreNLPが使用できるようになりました。

spark-corenlp の概要

spark-corenlp パッケージは小さいパッケージで、コードは object com.databricks.spark.corenlp.functions があるだけです。このobject に定義されている UDF として CoreNLP の各機能を提供する形になっています。 UDF として以下のものが定義されています。

  • cleanxml
  • tokenize
  • ssplit
  • pos
  • lemma
  • ner
  • depparse
  • coref
  • natlog
  • openie
  • sentiment

ソースコードを見ると以下のように SimpleAPI を利用して実装されています。

functions.scala

...
  def tokenize = udf { sentence: String =>
    new Sentence(sentence).words().asScala
  }
...
  def pos = udf { sentence: String =>
    new Sentence(sentence).posTags().asScala
  }
...
  def lemma = udf { sentence: String =>
    new Sentence(sentence).lemmas().asScala
  }

なお sentiment だけ、対応する SimpleAPIがないようで StanfordCoreNLP インスタンスを利用していました。

自前でUDFを定義する

spark-corenlp パッケージを使わずに直接 CoreNLP を使用する場合、私は以下のように UDF を定義して使っています。

package test2 {

  import java.util.Properties

  import org.apache.spark.sql.functions.udf
  import edu.stanford.nlp.ling.CoreAnnotations.{SentencesAnnotation, TokensAnnotation}
  import edu.stanford.nlp.pipeline.{Annotation, StanfordCoreNLP}
  import org.apache.spark.sql.{DataFrame, SparkSession}

  import scala.collection.JavaConverters._

  case class ann(word:String, lemma:String, pos:String)
  object NLProc {
    val props = new Properties()
    props.setProperty("annotators", "tokenize,ssplit,pos,lemma")
    @transient val pipeline = new StanfordCoreNLP(props)

    val nlproc_f = (text: String) => {
      val document = new Annotation(text)
      pipeline.annotate(document)
      document.get(classOf[SentencesAnnotation]).asScala flatMap { s =>
        s.get(classOf[TokensAnnotation]).asScala map { t =>
          ann(t.word(), t.lemma(), t.tag())
        }
      }
    }:Seq[ann]

    val nlproc = udf(nlproc_f)
  }
}

paste -raw モードでコピペします。

scala> :pa -raw
// Entering paste mode (ctrl-D to finish)

package test2 {
...

// Exiting paste mode, now interpreting.

性能劣化を避けるために pipline を UDFの呼び出しの度に作らないようにしています。 以下のように使用します。

import test2._
val df = Seq(
  (1, "Stanford University is located in California. It is a great university.")
).toDF("id", "Text")
val df2 = df.select(NLProc.nlproc($"Text").as("nlp"))
df2.show(truncate=false)
scala> :pa
// Entering paste mode (ctrl-D to finish)

import test2._
...
// Exiting paste mode, now interpreting.

+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|nlp                                                                                                                 |
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|[[Stanford,Stanford,NNP], [University,University,NNP], [is,be,VBZ], [located,located,JJ], [in,in,IN], [California,California,NNP], [.,.,.], [It,it,PRP], [is,be,VBZ], [a,a,DT], [great,great,JJ], [university,university,NN], [.,.,.]]|
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+

spark-corenlp の問題点

spark-corenlp は手軽なので、私も当初使っていたのですが最近は上記のように自分でUDFを定義して使用しています。

理由のひとつは対応する CoreNLP のバージョンが古いままな事があげられます。現在の spark-corenlp が対応する CoreNLP のバージョンは 3.6.0 ですが、CoreNLP の最新バージョンは 3.9.1 まで上がっており、かなりバージョンの差が開いてしまっています。

またもう一つの理由は性能面で問題があることに気がついたからです。

性能面での問題を明らかにするため、spark-corenlp を使った場合と、上記のような UDF を定義した場合で処理時間を比較してみました。

spark-corenlp と 自前のUDFの処理時間の比較

データとして Kaggle の Amazon Fine Food Reviews を使ってみます。zip ファイルを展開して CSV ファイルを hdfs 上に上げて下記のように読み込みます。

import com.databricks.spark.corenlp.functions._

val df =  spark.read.format("com.databricks.spark.csv").option("inferSchema", "true").option("header", "true").
load("hdfs://nameservice1/user/y.yamagata/AmazonFineFoodReviews.csv").repartition(4096).persist
scala> df.count
res2: Long = 568454

scala> df.printSchema
root
 |-- Id: integer (nullable = true)
 |-- ProductId: string (nullable = true)
 |-- UserId: string (nullable = true)
 |-- ProfileName: string (nullable = true)
 |-- HelpfulnessNumerator: string (nullable = true)
 |-- HelpfulnessDenominator: string (nullable = true)
 |-- Score: string (nullable = true)
 |-- Time: string (nullable = true)
 |-- Summary: string (nullable = true)
 |-- Text: string (nullable = true)

件数は57万件弱、今回は Text カラムのみを利用します。

scala> df.select("Text").show(truncate=false)
...
|"This was a serious surprise to us... our cats have always refused wet cat food except for people quality tuna in the
 can. Soon as we opened these for them ( was a varity pack ) they scarfed one down and everyday wanted more .. it real
ly looks, smells like people food! The package says ""people food for cats"" and after reading the ingrediants I belie
ve it is!" |
|Not too over powering. Lots of powder and not too salty. Comes with pieces of dried veggies and very small beef piece
s but adding more beef is not a problem. lol
...

上記のようなテキストデータが入っています。 spark-corenlp で Text カラムのデータに以下のような処理を実行します。

    def run(df: DataFrame): DataFrame = {
      ...
      val df2 = df.select(tokenize($"Text").as("word"), lemma($"Text").as("lemma"), pos($"Text").as("pos")).persist
      df2.count
      df2.unpersist()

tokenize, lemma, pos の処理を呼び出します。 自前のUDFでは以下のような処理を実行します。

    def run(df:DataFrame):DataFrame = {
      val df2 = df.select(NLProc.nlproc($"Text")).as("nlp").persist()
      df2.count()
      df2.unpersist()

NLProc.nlproc は前に定義したものになり、こちらもtokenize, lemma, posを呼び出しています。

この2つの処理を3回ずつ実行してJob の実行時間をSparkUI から取得しました。 Spark の実行パラメータは、Executor数:4, Core数:4 Executorメモリ12GB となります。 結果は以下のとおりです。

spark-corenlp(分) 自前UDF(分)
1回目  9.0 1.9
2回目  8.9 1.8
3回目  9.0 1.9
平均  9.0 1.9

自前のUDFで実行したほうが4倍以上速くなりました。

このような差がついてしまう原因としては、spark-corenlp が tokenize, lemma, pos と各 UDF を呼ぶ毎に別々に CoreNLP の API を呼ぶためではないかと考えています。 たとえば lemma UDF は内部で lemma アノテータ を呼んでおり、lemma アノテータはアノテータの依存関係から tokenize, ssplit, pos を呼びます。そして pos UDF は内部で pos アノテータを呼び、posアノテータは tokenize, ssplit を呼びます。このようにそれぞれのUDF毎に別々に処理を実行すると同じ処理を何度も呼び出すことになり計算量が多くなっているのではと予想しています。実際、呼び出す UDFを lemma だけにすると処理時間は2.4分程度になり、自前UDFに近い処理時間となりました。

終わりに

以上、Spark から CoreNLP を使う方法を見てきました。spark-corenlp パッケージは便利ですが処理速度やCoreNLPのバージョンへの追随に問題があると思われます。 現時点では自前でUDFを使ったほうが良いのではないかと考えています。 Spark で使用する場合は大量のドキュメントを処理したいことが多いかと思います。NLP周りの処理は重く時間がかかることが多いので、性能が落ちないように処理するように注意したほうが良いと思います。

Copyright © astamuse company, ltd. all rights reserved.