株式会社ホクソエムのブログ

R, Python, データ分析, 機械学習

学習済み日本語word2vecとその評価について

ホクソエムサポーターの白井です。 今回は日本語の word2vec に着目し、日本語の学習済み word2vec の評価方法について紹介します。

自然言語は非構造化データであるため、単語や文章を計算機で扱いやすい表現に変換する必要があります。 そのための方法の1つに word2vec があり、Bag of Words (BoW)tf-idf とならんでよく用いられます。

一般に、word2vec は Mikolovが提案した手法 (CBOW, Skip-gram) をはじめ、 GloVefastText など、単語をベクトルで表現する単語分散表現のことを指します。

word2vec は教師なし学習のため、コーパスさえ準備できれば誰でも新しい単語分散表現を学習することができます。 しかし、実際に word2vec を使う際に、どのように評価すれば良いのかがよく分からず、配布されている学習済みモデルを適当に選んで使ってしまうことも多いかと思います。

そこで、本記事では、日本語 word2vec の評価方法を紹介し、実際に日本語の学習済み word2vec を評価してみます。 基本的な評価方法は英語の場合と同様ですが、日本語では前処理として分かち書きが必要となるため、単語の分割単位を考慮する必要があります。

今回評価するにあたって書いたコードはGithubで公開しています。

github.com

1. word2vec の評価方法

ここでは、学習済み word2vec モデルの評価方法を2つ紹介します。 その他にも、固有表現抽出や文書分類などの、実際の解きたいタスクに適用した結果で word2vec を評価する方法もありますが、今回は word2vec そのものを評価する方法にフォーカスします。

1つ目の評価方法は、単語同士の 類似度・関連性 を測る方法です。2つの単語が意味的に似ているかどうかを判定することで、学習された表現を評価します。

これを行うためのデータセットとして、英語の場合、WordSim353 が有名です。 このデータセットは353の単語ペアと、その単語ペアの類似度(1~10)で構成されています。


実際のデータ

データセットにおける単語ペアの類似度と、学習済みモデルにおける単語ペアの類似度のスピアマンの順位相関係数を算出し、評価指標とします。

2つ目の評価方法は、Mikolovの論文 で紹介されている、単語を類推する アナロジータスク です。

アナロジータスクとは、例えば、king - man + woman = queen のように、king と man の関係が queen における woman であることを予測するタスクです。

これを行うためのデータセットは、 Google_analogy_test_set_(State_of_the_art) から入手できます (論文に記載されているリンクは切れているので注意してください)。 このデータセットには次の表のようなデータが含まれています。

table1 from Efficient Estimation of Word Representations in Vector Space

2. 日本語の評価データセット

上記では英語の評価用データセットを紹介しましたが、日本語の word2vec モデルを評価するには、日本語のデータセットを使う必要があります。

ここでは、日本語 word2vec の評価に使えるデータセットを紹介します。

日本語類似度・関連度データセット (JWSAN)

  • Data: http://www.utm.inf.uec.ac.jp/JWSAN/
  • 名詞・動詞・形容詞の 類似度・関連度 データセット
  • 類似度と関連度がそれぞれ1~7の間の値で付与されている
  • すべての単語ペア2145組のデータセット (jwsan-2145) と、分散表現に適したデータに厳選した1400組のデータセット (jwsan-1400) が存在

日本語単語類似度データセット (JapaneseWordSimilarityDataset)

The Revised Similarity Dataset for Japanese (jSIM)

  • Data: https://vecto.space/projects/jSIM/
  • 上記JapaneseWordSimilarityDatasetを追加・修正した 類似度 データセット
    • full version: 品詞カテゴリを修正
    • tokenized version: full versionをmecabで分かち書き
    • Unambiguous: tokenized versionから分かち書きに曖昧性がない単語のみ選出

The Japanese Bigger Analogy Test Set (jBATS)

3. 日本語学習済み word2vec

上記で紹介した評価用データセットを使って、日本語の学習済み word2vec を評価することが本記事の目的です。 本記事では、新たに word2vec を学習することは避け、Web上から入手可能な日本語の学習済み word2vec で評価を行います。 ここでは、誰でも利用可能な日本語学習済み word2vec をまとめます。

日本語学習済み word2vec には、エンティティベクトルや白ヤギコーポレーション(以下白ヤギ)のように、日本語だけ作成・公開している場合と、 fastTextのように多言語対応を目的として日本語のモデルを公開している場合があります。

これらのモデルはそれぞれ、学習に利用するデータ・ツール・学習方法が異なります。 特に日本語の場合、文字を分かち書きする前処理が必要なため、どの方法・どの辞書を用いて分かち書きを行ったかが、モデルの大きな違いになってきます。

エンティティベクトル (WikiEntVec)

Wikipediaで学習されたモデルです。分かち書きにはmecab (neologd) が利用されています。

Wikipediaが日々更新されているためか、定期的に新しいモデルがリリースされているようです。 https://github.com/singletongue/WikiEntVec/releases で最新のモデルが確認可能です。

白ヤギ (Japanese Word2Vec Model Builder)

こちらもmecab (neologd) による分かち書きで、Wikipediaで学習されたモデルです。 50次元のみ公開されています。

chiVe

Sudachiの開発元であるWorks applications が公開しているモデルです。 こちらは 国立国語研究所の日本語ウェブコーパス(NWJC)で学習されたモデルです。また、分かち書きにはSudachiを使用しています。

fastText

fastText実装で学習されたモデルです。多言語モデルの実装中に日本語が含まれています。分かち書きにはmecabが用いられているという説明があります。 どの辞書を利用したかの記載がないのですが、おそらくデフォルトのipadicであると考えられます。

3.1 日本語学習済み word2vec まとめ

上記モデルを含め、公開されているword2vecについて、個人的に見つけた結果を以下にまとめました。

Name Model Data Dim Tokenizer Dict
WikiEntVec Skip-gram? Wikipedia 100,200,300 mecab mecab-ipadic-NEologd
白ヤギ CBOW? Wikipedia 50 mecab mecab-ipadic-NEologd
chiVe Skip-gram NWJC 300 Sudachi
bizreach Skip-gram 求人データ 100, 200 mecab ipadic
dependency-based-japanese-word-embeddings Dependency-Based Word Embeddings Wikipedia 100, 200, 300 Ginza
hottoSNS-w2v
(※要問い合わせ)
CBOW ブログ, Twitter 200 Juman, mecab mecab-ipadic-NEologd
朝日新聞単語ベクトル
(※要問い合わせ)
Skip-gram, CBOW, Glove 朝日新聞 300 mecab ipadic
fastText CBOW Common Crawl, Wikipedia 300 mecab ?
wikipedia2vec Skip-gram Wikipedia 100, 300 mecab ?
wordvectors Skip-gram?, fastText Wikipedia 300 mecab ?

学習方法 (Skip-gram or CBOW) について、READMEなどのドキュメントに明記されておらず、学習コードのパラメーターから判断したモデルに関しては ? をつけています。 (具体的にはgensim.models.word2vec.Word2Vecのパラメータ sg で判断しています)

4. 日本語 word2vec の評価

学習済み word2vec に対して、単語類似度の評価データセットを使ってスコアを算出し、比較してみます。

今回比較に利用するモデルとデータセットは以下の通りです。モデルの次元数が一致していないことには注意が必要です。 (次元数が大きいほど表現力が高くなるため)

word2vecモデル (カッコ内は 次元数 × 語彙数 )

  • WikiEntVec (200 × 1,015,474)
    • HPで公開されている2017年のモデル
  • 白ヤギ (50 × 335,476)
  • chiVe (300 × 3,644,628)
  • fastText (300 × 2,000,000)
    • gensimで読み込むためtxt のデータ

評価データセット

  • JWSAN
    • 2145 (jwsan-2145)・ 1400 (jwsan-1400)
  • JapaneseWordSimilarityDataset
    • adv (副詞)・verb (動詞)・noun (名詞) ・adj (形容詞)

また、JWSANに評価用スクリプトがなかったため、評価コードを実装し、公開しています (Github)。 複数のデータセットで評価するにあたって、スピアマンの順位相関係数はSciPyで実装されているspearmanr で統一しました。 未知語については評価から取り除いています。

実験結果 (スピアマンの順位相関係数) は以下のとおりです。太字がデータセットごとで最も良いスコアを示しています。 chiVeとfastTextが比較的良いスコアを出していることがわかります。

JapaneseWordSimilarityDataset JWSAN (similarity)
adv verb noun adj 2145 1400
WikiEntVec 0.250 0.334 0.292 0.231 0.643 0.499
白ヤギ 0.214 0.299 0.243 0.231 0.581 0.416
chiVe 0.394 0.326 0.361 0.475 0.701 0.541
fastText 0.350 0.386 0.357 0.459 0.737 0.610

また、実験における、未知語の割合は以下のとおりです。JWSDはJapaneseWordSimilarityDatasetを示しています。

oov

4.1 分かち書き

上記の結果で、未知語の割合が非常に高いのが気になります。 未知語とは辞書中に存在しない単語1 ですが、 word2vec においてはモデルの語彙に含まれない単語のことを意味します。しかしながら、膨大なデータから学習したモデルが、評価データセットに含まれる単語をほとんど学習していないというのはあり得ない気がします。

未知語扱いになってしまう原因として、分かち書きが考えられます。 jSIMの紹介でも述べましたが、評価データセットには、分かち書きすると複数の単語に分かれるタイプの単語 (複合語・派生語) が含まれています。

例えば、JapaneseWordSimilarityDatasetに含まれる動詞「掴んだ」「寂れた」はmecabの解析結果で以下のように、動詞と助動詞に分割されます。

掴んだ
掴ん  動詞,自立,*,*,五段・マ行,連用タ接続,掴む,ツカン,ツカン
だ 助動詞,*,*,*,特殊・タ,基本形,だ,ダ,ダ

寂れた
寂れ  動詞,自立,*,*,一段,連用形,寂れる,サビレ,サビレ
た 助動詞,*,*,*,特殊・タ,基本形,た,タ,タ

また、「議論した」「ディスカッションする」のような単語は、以下のように、名詞と動詞(と助動詞)に分割されます。

議論した
議論  名詞,サ変接続,*,*,*,*,議論,ギロン,ギロン
し 動詞,自立,*,*,サ変・スル,連用形,する,シ,シ
た 助動詞,*,*,*,特殊・タ,基本形,た,タ,タ

ディスカッションする
ディスカッション    名詞,サ変接続,*,*,*,*,ディスカッション,ディスカッション,ディスカッション
する  動詞,自立,*,*,サ変・スル,基本形,する,スル,スル

このような、評価データとモデルの分割単位の不一致の問題点を解消するため、今回は分かち書きに対応した類似度も算出しました。 具体的には、モデルが未知語だった単語について、分かち書きをし、複数単語の和をその単語のベクトルとして扱います。 「議論した」であれば、「議論」「し」「た」のそれぞれのベクトルの和を「議論した」ベクトルとみなします。 (評価コードの get_divided_wv がその実装に当たります)

学習済みword2vecの分かち書き手法に合わせた未知語の割合は以下グラフのとおりです。どのモデルについても、未知語が減ったことがわかります。 特にchiVeは全てのデータセットにおいて未知語が0になりました。語彙数が4つの中で最も多く、また、Sudachiの3つの分割方法を考慮したモデルのため、柔軟に対応できていることが理由として考えられます。

compare

分かち書きを利用した場合のスピアマンの順位相関係数は以下のようになりました。 評価するデータが増えていることもあり、スコア自体は下がっています。

JapaneseWordSimilarityDataset JWSAN (similarity)
adv verb noun adj 2145 1400
WikiEntVec 0.182 0.149 0.248 0.158 0.733 0.610
白ヤギ 0.155 0.223 0.202 0.257 0.580 0.416
chiVe 0.255 0.260 0.310 0.404 0.701 0.541
fasttext 0.301 0.181 0.293 0.336 0.733 0.610

(参考) 分かち書きありなしのスコアの比較表 compare_table

4.2 ケーススタディ

実際に算出された類似度をみてみます。

未知語が0になったchiVeモデルの、jwsan-1400の結果に注目します。

類似度が高いと出力した単語ペアをみると、 「高校」「中学」や「裁判」「訴訟」のように、 意味的に似ている単語が上位になっています。 しかし、「動詞」と「主語」のような (どちらも文の構成要素の一つではあるものの) 対義的な意味である単語ペアも似ている扱いになっていました。

word1 word2 正解 予測
高校 中学 2.54 0.851
書店 本屋 5.45 0.836
裁判 訴訟 3.5 0.814
出版 刊行 4.01 0.792
動詞 主語 1.38 0.791

一方で、以下の「写し」「複製」のように、 正解データは類似度が高いにも関わらず、モデルは類似度が低いと出力したペアもありました。

word1 word2 正解 予測
写し 複製 4.73 0.283
支度 用意 4.71 0.339
決まり 規律 4.64 0.167
焼く 燃やす 4.36 0.363

5. まとめ

日本語学習済み word2vec とその評価方法について紹介しました。

今回全体的に精度が良かった chiVe ですが、モデルサイズが12.5GB程度あるので、実際に利用する場合はメモリ等の環境を気にする必要がありそうです。 (fastTextが4.5GB、WikiEntVecが2.0GB程度ということも考慮すると、かなり大きいことがわかるかと思います。)

白ヤギは未知語が多く精度が低かったものの、今回扱った4つのモデルの中では最もモデルサイズが小さい (24MB程度) です。

今回紹介した類似度の精度だけでなく、環境や状況に応じて学習済み word2vec を使い分けることが必要だと思います。

形態素解析の理論と実装 (実践・自然言語処理シリーズ)

形態素解析の理論と実装 (実践・自然言語処理シリーズ)

  • 作者:工藤 拓
  • 出版社/メーカー: 近代科学社
  • 発売日: 2018/10/04
  • メディア: 単行本

GitHub Actions実行時に依存するRパッケージのインストールをキャッシュ化する

ホクソエムの u_ribo です。漫画「ブリーチ」の石田雨竜に親近感を感じます。仕事はシュッと終わらせて趣味の時間を増やしたいですよね。

要約

  • GitHub Actionsに対してrenvを使ったキャッシュ機能を活用。依存するRパッケージのインストール時間を短縮する
    • パッケージのインストールに要する時間を1/25に短縮
    • renvのキャッシュはOSによりパスが異なるため、GitHub Actionsを実行するOSに応じて変更が必要になる
    • キャッシュ機能はpipでも使えるため、Pythonによる処理を適用するときも便利
  • GitHub Actionsでrenvのキャッシュを利用するサンプル

はじめに

GitHub上でビルド、テスト、デプロイ等の作業を自動的に行えるGitHub Actionsが便利ですね。RやPythonも実行可能なため、データ分析の作業を補助する機能を持たせることもできるかと思います。例えば、リポジトリ上のデータが更新されたタイミングで分析コードを走らせてレポートを作成するといった具合です。このブログでも id:shinichi-takayanagi さんが記事を書かれています。

blog.hoxo-m.com

そんなGitHub Actionsですが、RやPythonのコードを実行する際にパッケージのインストールが必要になる場合があります。パッケージの追加もコマンドで行えるため、それ自体は問題になりません。しかし処理時間に関してはどうでしょう。パッケージのインストールはGitHub Actionsが動作するたびに実行されます。依存パッケージが多い・頻繁に実行されるジョブでは、ここでの作業がジョブ全体に要する時間の大部分を占める恐れがあります。

そこで、ジョブの過程で取得した依存パッケージを次回の実行時に再利用できるようにする、キャッシュ機能を活用します。これにより実行時間の短縮が期待できます。

公式のExampleを見ると、Pythonであればpip、Nodeはnpmやyarnを利用したパッケージ管理ツールを利用する方法が書かれています。ではRの場合はどうするの?が本記事の話題です。ここではRパッケージのインストール結果をキャッシュ化する例として、Rのパッケージ管理にrenvを利用して、pkgdownでのウェブサイトの構築を行うワークフローに導入することとします。

pkgdownでのウェブサイトの構築を行うワークフローについては id:yutannihilation さんの下記の記事をご覧ください。

notchained.hatenablog.com

この記事を参考に、まずはキャッシュ機能を使わないGitHub Actionsの設定を済ませます。本記事では、ここで用意したyamlファイルを編集します。

renvでパッケージ管理

renvはRStudioにより開発されているパッケージ管理のためのパッケージです。プロジェクトで利用されるパッケージの依存関係を明らかにし、再現可能な形で環境を構築します。具体的にはプロジェクトで使われるRパッケージとそのバージョン、インストール元の情報等を renv.lock ファイルに記録します。

speakerdeck.com

用意したプロジェクトに対してrenvによる管理を有効化しましょう。renv::init() を実行すると renv/フォルダ、renv.lockファイルが生成されます(.Rprofileがない場合はこれも)。

この時、すでにRコードが存在する場合、利用するパッケージおよびその依存パッケージがrenvによりインストールされ、その情報がrenv.lockに記録されます。パッケージのインストール先は従来Rが利用する環境(/usr/local/lib/R/site-library//Library/Frameworks/R.framework/Versions/{Rバージョン}/Resources/library)とは異なる環境となります。それはホームディレクトリに近い場所とrenvを有効化したプロジェクトの中です。

最初の「ホームディレクトリに近い場所」はOSごとに異なります。具体的には以下のとおりです。

プラットフォーム 場所
Linix ~/.local/share/renv
macOS ~/Library/Application Support/renv
Windows %LOCALAPPDATA%/renv

renvを使ったプロジェクトでパッケージをインストールするとこのディレクトリにファイルが保存されます(renvのキャッシュを無効化した場合はプロジェクトの中に直接保存されます)。そのため、他のRプロジェクトでパッケージのアップデートを行ってもその影響を受けません。また、依存関係も記述されているので再インストールも安全に行えます。

renv.lockに書かれたパッケージを復元するにはrenv::restore()を実行します。一度インストールされたパッケージであればキャッシュからインストールが行われるため、ファイルのダウンロード、ビルドの作業が省略されます。

またプロジェクトで利用するパッケージに変更(追加や更新、削除)があった際は renv::status() で確認、必要に応じて renv::snapshot()renv::lock を更新しましょう。

詳しい利用方法はここでは省略します。興味のある方は

qiita.com

speakerdeck.com

をご覧ください。

GitHub Actionsにrenvを導入する

続いてGitHub Actionsにrenvを導入する方法です。pkgdownによるウェブサイトのビルドを行うActionsではmacOS上で動作します。そこでrenvのキャッシュもmacOS仕様にする必要があります。

.github/workflows/ にある pkgdownのウェブページをビルドするYAMLファイルにある以下の箇所を変更します。

      - name: Install dependencies
        run: |
          Rscript -e 'install.packages("remotes")' \
                  -e 'remotes::install_deps(dependencies = TRUE)' \
                  -e 'remotes::install_github("jimhester/pkgdown@github-actions-deploy")'
  1. renvをインストール
  2. renv::restore()で必要なパッケージを復元する
    - name: Install dependencies
      run: |-
        Rscript -e "install.packages('renv')" \
                -e "renv::restore(confirm = FALSE)"

YAMLを書き換えたら、手元のRコンソールでrenv::install("jimhester/pkgdown@github-actions-deploy")renv::snapshot()を実行してrenv.lockを更新します。これは元のYAMLに書かれているremotes::install_github("jimhester/pkgdown@github-actions-deploy")の代わりに必要な処理です。

続いてキャッシュの指定です。今回はmacOSで動作させているので、キャッシュのpathもmacOSの~/Library/Application Support/renvとします。

    - uses: actions/cache@v1
      if: startsWith(runner.os, 'macOS')
      with:
        path: ~/Library/Application Support/renv
        key: ${{ runner.os }}-renv-${{ hashFiles('**/renv.lock') }}
        restore-keys: |
          ${{ runner.os }}-renv-

こちらが編集後のyamlファイルです。

それではキャッシュ化の効果を見てみましょう。依存パッケージのインストールにかかった時間 (Install Package Dependenciesの部分)を見ます。最初の処理で4分38秒だったのに対し、キャッシュが働く2回目はわずか11秒で完了しています。1/25の差は大きいですね。出力を見ても、きちんとキャッシュを利用しているのがわかります。

f:id:u_ribo:20200123174147p:plain

任意のOS、Rコードの実行に必要な依存パッケージを扱う例

renvのキャッシュはOSごとに異なることは述べたとおりです。OSごとのキャッシュ先の指定方法は、PRを出してマージされたのでGitHub Actionsのcacheリポジトリに記載されています。

https://github.com/actions/cache/blob/master/examples.md#r---renv

また、今回解説したウェブサイト構築以外の用途で利用する際のサンプルとして、以下のリポジトリへのリンクを示しておきます。

GitHub - uribo/renv_ghaction: for demonstration

投げていたジョブが完了しました。お先に失礼します! Enjoy!

Rと3Dプリンターで八ヶ岳のミニチュアを作る。

この記事について

この記事はR Advent Calendar 2019の19日目の記事です。

はじめに

ホクソエムサポーターの輿石です。最近3Dプリンターを買いました。遠い世界のガジェットのように思っていましたが、家庭用であれば3万円前後で買えてしまうんですね。
3Dプリンターの使い方としてCADで自分の作りたいものを設計していくのが一般的かと思いますが、Rで3Dのプロットを作成することで、データから立体物を作ることが可能です。
この記事では、3Dのプロットを作成できるrayshaderパッケージと、基盤地図情報の地形図データをRに読み込むことができるfgdrパッケージを使って、故郷八ヶ岳周辺のミニチュアを作ってみます。

データの取得

国土地理院の基盤地図情報サイトからデータをダウンロードします。 今回は数値標高モデルの10mメッシュのデータを使います。八ヶ岳周辺のメッシュコードは533862、533863、533872、533873でした。地名検索機能が付いた地図のUIが用意されており、クリックだけで欲しい場所のデータを指定することができました。

データの読み込み

まずは今回必要になるパッケージを読み込みます。

library(fgdr)
library(rayshader)
library(raster)
library(tidyverse)

基盤地図情報からダウンロードしたxml形式のデータをfgdrパッケージのread_fgd_dem関数を使ってraster形式で読み込みます。広い範囲(複数のメッシュコードの範囲)を対象にしたい場合には複数のxmlファイル結合する必要があるので、読み込んでからraster::margeで結合します。

files <-
  list.files("data/", full.names = T, pattern = "dem10b")   
files
[1] "data/FG-GML-5338-62-dem10b-20161001.xml" "data/FG-GML-5338-63-dem10b-20161001.xml" "data/FG-GML-5338-72-dem10b-20161001.xml"
[4] "data/FG-GML-5338-73-dem10b-20161001.xml"
raster_list <-
  files %>%
  map(~fgdr::read_fgd_dem(., resolution = 10, return_class = "raster"))

r <- purrr::reduce(raster_list, raster::merge,  tolerance = 0.2)

rayshaderを使った3D出力

rayshaderは公式のドキュメントが充実しているので、詳細はこちらが役に立つと思います。
rayshaderで扱えるようにrasterをmatrixに変換してからプロットします。

elmat <- rayshader::raster_to_matrix(r)
elmat %>%
  rayshader::sphere_shade(texture = "desert") %>%
  rayshader::plot_map()

f:id:kosshi08:20191216015641p:plain:w500
基盤数値情報DEMデータを元に作成

elmat %>%
  rayshader::sphere_shade(texture = "desert") %>%
  rayshader::plot_3d(elmat, zscale = 10, fov = 0, theta = 135, zoom = 0.75, phi = 45, windowsize = c(1000, 800))

rayshader::render_snapshot(clear=FALSE)

f:id:kosshi08:20191216020508p:plain
基盤数値情報DEMデータを元に作成

3Dプリンターで印刷する

rayshaderパッケージのsave_3dprint関数を使うことで、3Dプリンターでは一般的なstlというフォーマットでデータを出力することができます。stlは造形物を三角形の集合体で表現するファイル形式なようです。
今回私が購入した熱溶解積層方式の3DプリンターはstlファイルをG-Codeというファイル形式に変換して読み込む必要があるので、3Dプリンターに付属しているソフトで変換します。(stlファイルがあればほとんどの3Dプリンターで何らかの方法で出力できるのではと思います。)

rayshader::save_3dprint("Yatsugatake.stl")
# このあと3Dプリンター付属のファイルでG-coede形式に変換

G-code形式のファイルを3Dプリンターに送って出力します。こんな感じで線を描き少しずつ重ねていって八ヶ岳を出力していきます。

f:id:kosshi08:20191218010817j:plain:w500

私が使っている3Dプリンターでは、15cm四方の大きさで8時間で出力できました。(7cm四方の大きさで約1時間弱、25cm四方だと6日間弱かかります!)

f:id:kosshi08:20191218011238j:plain:w500
基盤数値情報DEMデータを元に作成

八ヶ岳の裾野までプリントしたため山の部分が小さくなってしまいました。標高1600m以上の部分に絞ってプリントしてみます。

values(r)[values(r) < 1600] <- NA
elmat = rayshader::raster_to_matrix(r)
elmat %>%
  sphere_shade(texture = "desert") %>%
  plot_3d(elmat, zscale = 10, fov = 0, theta = 135, zoom = 0.5, phi = 45, windowsize = c(1000, 800))
save_3dprint("Yatsugatake_1600m.stl")

f:id:kosshi08:20191218010614j:plain:w500
基盤数値情報DEMデータを元に作成

山の稜線がはっきり認識できるミニチュアができました。

3Dプリンターについて

3Dプリンターにもいくつか種類があり、家庭用では主にプラスチックを熱で溶かして少しずつ積層していく熱溶解積層方式と、光をレジンに当てて硬化させていく光造形方式の二つが主流になっています。
私の購入した3DプリンターはANYCUBICというメーカーの熱溶解積層方式のエントリーモデルで、Amazonでもベストセラーになっている定番のものです。 光造形方式の方がきれいに出力できるようですが、レジンの取り扱いが難しかったり、造形時のレジンの臭いが気になる人もいるようなので、熱溶解積層方式を選びました。エントリーには熱溶解積層方式が良いのではと思います。

おわりに

この記事ではRとfgdrとrayshaderを使って八ヶ岳を3Dで可視化し、3Dプリンターで出力してみました。rayshaderではggplot2のオブジェクトも3Dで表示できるので、一般的なプロットも3Dプリンターで印刷することができます(使いどころは??)。3Dプリンターがあると可視化の幅が広がりますね。2020年、一家に1台3Dプリンターはいかがでしょうか。

Enjoy!

参考

GitHub Actions でRのパッケージの継続的インテグレーション(CI)を行う

本記事について

R Advent Calendar 2019 1103日目の記事です。 空きがなかったので適当に書きます。

1103->11月03日は”いいおっさん”の日です、各位、よろしくお願いいたします。

はじめに

株式会社ホクソエムの高柳です。

この記事ではGitHub ActionsとR、特にRのパッケージ開発と組み合わせて使う方法を書きたいと思います。 GitHub Actionsとは”コードをビルド、テスト、パッケージング、リリース、デプロイするためのプロセスの集合”であるワークフローを、GitHub リポジトリに直接作成することができる仕組みです。 詳しくは この辺なんかを読むとよいでしょう。

R言語への適用例としては、丁度R Advent Calendar 2019 8日目:GitHub Actions で R の環境ごとのベンチマークをとったにも記事があります。

なぜGitHub Actionsを使うのか?

R言語のパッケージのCIツールとしてはTravis CIがほぼデファクトスタンダードかなと思うのですが、これをPrivate repositoryや会社で使おうと思うと費用がかかってしまいます。 もちろん会社・個人として応援できる場合は全然課金すれば良いと思うのですが、そうではない場合には何某かの代替手段が欲しいところです。

そこでGitHub Actionsが使えるかどうか調べてみた、というお話です。

Rパッケージ開発での使い方

サッとググって調べたところ、

github.com

が見つかるのですが、これを素直に使うよりも usethis パッケージにスッと導入された関数を使う方が簡単そうなので、今回はこれを使うことにします。 もっといいやり方があったら教えてください!

まず、CIしたいパッケージはRStudioやusethisから適当に作っておくとして、usethisパッケージをgithubからインストールしておきます。 これは、上述した機能がまだCRAN版にないための対応です。

devtools::install_github("r-lib/usethis")

usethisをインストール or 更新したのち、Rのコンソールから

usethis::use_github_actions()

を叩くと、開発しているパッケージのディレクトリに .github/workflows/R-CMD-check.yaml というファイルができています。

$ cat .github/workflows/R-CMD-check.yaml
on: [push, pull_request]

name: R-CMD-check

jobs:
  R-CMD-check:
    runs-on: macOS-latest
    steps:
      - uses: actions/checkout@v1
      - uses: r-lib/actions/setup-r@master
      - name: Install dependencies
        run: Rscript -e "install.packages(c('remotes', 'rcmdcheck'))" -e "remotes::install_deps(dependencies = TRUE)"
      - name: Check
        run: Rscript -e "rcmdcheck::rcmdcheck(args = '--no-manual', error_on = 'error')"

このファイルをgithubにアップロードするだけです、簡単ですね???

なお、中で使われているr-lib/actionsやこの機能自体は、最近だと glueパッケージで(個人的にはよくお世話になる)有名なRStudioの Jim Hester 氏がメインで開発しているので安心・安全ですね!

README.mdにStatusバッジを張り付けたい!というオシャレさんは以下のようにするとバッチが張り付けられます。

[![R build status](https://github.com/shinichi-takayanagi/ghactiontest/workflows/R-CMD-check/badge.svg)](https://github.com/shinichi-takayanagi/ghactiontest)

この辺のもう少し詳しい使い方や解説については英語での解説記事があるので、こちらも参考にするとよいでしょう。 - Github actions with R

サンプルコード

ここでの手順を踏まえた上で、適当に作ったパッケージにPull Requestをこんな感じで出してみました。

Update README.md by shinichi-takayanagi · Pull Request #1 · shinichi-takayanagi/ghactiontest · GitHub

勝手に単体テスト(正確にはR CMD check相当の処理)を行ってくれます。 これで安心してRのパッケージ開発に専念できますね!

Enjoy!

sqlparse 入門 - 字句解析編 -

本記事はPythonその2 Advent Calendar 2019に参加しています。

1. はじめに

こんにちは。ホクソエムサポーターの藤岡です。 データアナリストらしいですが、分析そっちのけでPySparkと戯れてます。

メソッドチェインを積み上げていくスタイルで最初はちょっと使いづらいなと思ったのですが、 DataFrameが思いのほか使いやすくて、 気がつくとPySpark無しでは生きられない身体になってしまいました......。

さて、今回紹介するライブラリはsqlparseです。

sqlparseは、SQLエンジンを一切使わずにSQLを解析し、そこから種々の情報を得ることができる非常に頼もしいライブラリです。 例えば、SQLの山の中から欲しいテーブルのDDLを簡単に検索できるようにしたり、 さらにそこからカラムの情報を抜き出してきたり、業務で大変お世話になっております。

ただ、パーサー自体が情報分野にいないと馴染みがないものであり、どう使っていいのか分かりづらい一面があります。 加えて、トークン周りの実装にクセがあるため正直馴染みづらいです。

例えば、トークンの種別を表すsqlparse.tokens.Tokenというオブジェクトがあるのですが、実体はtupleのサブクラスのインスタンスです。 isinstance(token, Token)をやると怒られます*1。 ついでに、sqlparse.sql.Tokenというオブジェクトもあるんですが、こっちはクラスです。なんで?

というわけで、前置きもそこそこに解説に入りたいと思います! 色々と書いていたら内容が重くなってしまったので、字句解析編と狭義の構文解析編の二つに分けています。

2. 注意

  • 本記事に書かれた内容は自分の理解に基づいたものであり、誤りが含まれている可能性がありますが、ご了承ください。
  • もし本記事の不備にお気付きの際には、コメントでご指摘いただければ幸いです。
  • また、以下の解説ではSQLが何度か登場しますが、すべてHiveQLです。
  • 今回のサンプルプログラムは説明用に作成したものであり、実際の業務での使用は一切想定していないことをご留意ください。
  • 本記事のプログラムは全て以下の環境で動作させています。     - Python:   3.6.4     - sqlparse: 0.3.0 (2019/12/07現在の最新ver)

3. サンプルコードについて

本記事のサンプルプログラムはリポジトリに完成品がありますので、ぜひ遊んでみてください。

github.com

4. 導入

Python3.4以上*2がインストールされている環境で、$ pip install sqlparseしてください。

5. sqlparseについて

sqlparseは、作者によればA non-validating sql parser module for Python、つまり、Pythonの非検証のSQLパーサーモジュールです。 といっても、計算機科学でもやってなければ、なんのこっちゃと戸惑うかと思います。

そこで、本章ではパーサーと、その重要な機能の一つである字句解析について簡単に解説します。

少し小難しい話かもしれませんが、 sqlparseを使うときには、ただ使い方を知るだけでなく、 そのコアとなるパーサーについて簡単に知っておくことが重要です。 それにより機能を正しく、かつ十全に活用することができるので、 ぜひ本章を読んでみていただければと思います。

5.1. パーサー (parser) について

パーサーとは、テキストを一定の文法にしたがって解析するアルゴリズム、もしくはその実装のことです。

パーサーが行う処理を広義の構文解析と呼びます。

例えば、SQLやPythonのようなプログラミング言語で書かれた命令を実行するためには、 そこに書かれた個々の命令やその実行フローなどを計算機が読み取る必要があります。 それを実現するにはパーサーによるテキスト解析が欠かせません。

さらに、広義の構文解析は字句解析狭義の構文解析の2ステップに分けることができます。 直感的なイメージとしては、私たちが英語を読解する場合と照らし合わせると、

  • 字句解析: アルファベットの並びを単語・熟語として認識すること
  • 狭義の構文解析:  文法の適用

に近い処理です。

こうした解析の結果として得られる情報は計算機だけでなくそれを扱う人間にとっても有用であり、 私たちがsqlparseを活用する意味もそこにあります。

5.2. 非検証パーサーについて

パーサーが処理するテキストの妥当性をチェックすることを検証と言います。 そして、検証を行うパーサーを検証パーサー、検証を行わないパーサーを非検証パーサーと言います。

ただ、これは一般的にXMLパーサー等で使われる言葉であり、SQLパーサーの文脈ではあまり見ない言葉です。 なので、ここではエラーがあっても無視して解析を進める、くらいの認識でいいかと思います。

5.3. 字句解析についてもう少し詳しく

5.1節で軽く触れた字句解析について、sqlparseを使いながらもう少し詳細に見ていきます。 なお、ここで使うコードについては後ほど解説しますので、まずは結果に注目していただければと思います。

さて、5.1節の説明では字句解析について、アルファベットの並びを単語・熟語として認識することと表現しましたが、 より正確には、文字列を入力としてその文字列を表すトークン列を得る処理のことです。

例えば、以下のようなSQLをトークン列に変換していくことを考えます。​

SELECT
    id,
    age,
    gender
FROM
    hoge_table 
WHERE
    age > 10;

このsql (sql_1とします) を入力として以下のコードを実行してみます。

import sqlparse

sql_1 = "SELECT id, age, gender FROM hoge_table WHERE age > 10;"
parsed_queries = sqlparse.parse(sql_1)
parsed_query = parsed_queries[0]
list(parsed_query.flatten())

すると、以下のようなトークンオブジェクトのリストが得られます。

[<DML 'SELECT' at 0x107D69A08>,
 <Whitespace ' ' at 0x107D841C8>,
 <Name 'id' at 0x107D84348>,
 <Punctuation ',' at 0x107D84228>,
 <Whitespace ' ' at 0x107D842E8>,
 <Name 'age' at 0x107D843A8>,
...

一つ一つのトークンオブジェクトは、<トークン '値' at アドレス>という表記になっています。 ここで注意なのですが、構文解析におけるトークンとは"SELECT"のような文字列ではなく、 その種別の名称であるDMLの部分です。 また、空白があっても必ずしもそこを基準に分割される訳ではありません。 例えば、sqlparseは"LEFT JOIN"を"LEFT"と"JOIN"には分割しません。

最初のトークンはDML、次のトークンはWhitespace、さらにName,Punctuation,...と続いていっています。 それぞれのトークンは、sql_1の文字列の分割である、"SELECT", " ", "id", ", ", ...といった値 (文字列) と対応しています。

なお、上で紹介したトークンはsqlparseによるトークン分割で得られる最小単位のトークン列であり、より大きな単位でトークンに分割することもできます。 この部分には狭義の構文解析が深く関わってくるので、詳しくは次回の記事で触れます。

5.4. 基本的な使用方法

5.3節で使用したサンプルコードについて簡単に解説しながら、 sqlparseの使い方の基本について触れていきます。

まず、sqlparse.parse関数にSQLの文字列を渡すと広義の構文解析が行われ、その結果が返されます。

import sqlparse

sql_1 = "SELECT id, age, gender FROM hoge_table WHERE age > 10;"
parsed_queries = sqlparse.parse(sql_1)

結果はタプルとして返されるので、ここで渡すsqlにはクエリが複数含まれていても大丈夫です。 ただし、今回のように一つしかクエリが入っていない場合も結果はタプルとして返ってくるので、 クエリを取り出すにはインデックスを指定する必要があります。

parsed_query = parsed_queries[0]

sqlparse.parse()で得られる各パース結果は、 一つのクエリのパース結果が一つのトークンオブジェクト*3として得られるようになっています。

例えば、

-- hoge
SELECT hoge FROM t1;
-- fuga
SELECT fuga FROM t2;
-- piyo
SELECT piyo FROM t3;

を渡すと、

(<Statement ' -- ho...' at 0x107D86D68>,
 <Statement ' -- fu...' at 0x107D90048>,
 <Statement ' -- pi...' at 0x107D86DE0>)

が返ってきます。

最後に、今回は字句解析の結果(最小分割単位のトークン分割)だけを得たいので、flattenメソッドで狭義の構文解析の結果を捨てます。

list(parsed_query.flatten())

こうして、5.3節で得られたトークン列が得られます。

[<DML 'SELECT' at 0x107D69A08>,
 <Whitespace ' ' at 0x107D841C8>,
 <Name 'id' at 0x107D84348>,
 <Punctuation ',' at 0x107D84228>,
 <Whitespace ' ' at 0x107D842E8>,
 <Name 'age' at 0x107D843A8>,
...

以後、特に断りがない場合は、この最小分割単位のトークン列のことを、たんにトークン列と呼ぶこととします。

6. 使用例: CREATE TABLE DDL Finder

6.1 概要

ある案件で、テーブルの定義のDDLが書かれた大量のsqlファイルがフォルダに格納されている現場に遭遇しました。 その中からいちいち目視で必要なファイルを探すのは面倒だった上に、ファイル名が日本語だったり、コメント中に別のテーブル名が入っていたりして、findコマンドでも探せない......。 自分はそんなとき、テーブル名を入力としてそのDDLを実行してCREATE TABLE & MSCK REPAIRをする関数を作成しました。

そのキモとなるのがSQLの解析部分です。当時は正規表現を使ってサクッと作ったのですが *4、 今回はそれをsqlparseを使って実装してみます。

想定する要件は以下の通りです。

  • 多数の.sqlファイルが存在するフォルダが探索対象。
  • それぞれの.sqlファイルにはCREATE TABLE文だけでなく、MSCK REPAIR TABLE文のような別のクエリが書かれている。
  • テーブル名は探索範囲内でユニーク。ただし、当該ファイル以外のファイルのコメント等には出現しうる。
  • ファイル名はテーブル名とは無関係。
  • テーブル名はコメントやlocation等にも出現。
  • 入力はテーブル名とフォルダのパス、出力は当該SQLファイルのパス。
  • 見つからなければFileNotFoundErrorをraiseさせる。

6.2. 実装

まず、sqlparseが関わる部分以外をさっくりと作ると、以下のような感じになります。

from glob import iglob
from pathlib import Path

def find_table_definition(
        sql: str, table: str, db: Optional[str] = None) -> Optional[TokenType]:   
    """引数で指定したテーブルの定義を文字列`sql`中から探し出す関数。"""
    <<メイン部分>>

def find_hive_ddl(directory: str, table: str, db: Optional[str] = None):
    """引数で指定したテーブルの定義スクリプトをフォルダから探し出す関数"""
    for p in iglob(directory, recursive=True):
        path = Path(p)
        if not path.is_file():
            continue
        with p.open() as f:
            if find_table_definition(f.read(), table, db):
                return p
    raise FileNotFoundError(
        "DDL file of {table} not found.".format(table=table)
    )

次に、上記のメイン部分find_table_definitionを作成していきます。

処理の流れとしては以下の通りです。

  1. SQLテキストからCREATE TABLEクエリを抽出
  2. クエリ中からテーブル名のトークンオブジェクトを抽出。
  3. テーブル名を取得。
  4. 一致判定の結果を返す。

HiveQLの場合、以下の条件を満たすのならばCREATE TABLEのDDLです。

  • コメントを除けば、DDLトークン"CREATE"から始まる。
  • さらにTABLEトークンが続く。ただ、Keywordトークンの"TEMPORARY"と"EXTERNAL"を挟むことがある。

上記の条件を満足しているかを判定する部分のコードは以下の通りです。

from typing import Optional, List, Sequence
from sqlparse.tokens import Keyword, DDL, Token, Name, Punctuation
from sqlparse.sql import Statement
import sqlparse


TokenType = Token.__class__
DEFAULT_DB = "default"


def find_table_definition(
        sql: str, table: str, db: Optional[str] = None) -> Optional[TokenType]:
    """`table`で指定したテーブルの定義を文字列`sql`中から見つける"""
    db = db or DEFAULT_DB
    # 1. パースしてトークン列に分割
    for query in sqlparse.parse(sql):
        tokens = [
            t for t in query.flatten()
            if not (t.is_whitespace or is_comment(t))
        ]
        # 2. <DDL CREATE>を検証
        if not match_consume(tokens, (DDL, "CREATE")):
            continue
        # 3. <Keyword TEMPORARY> <Keyword EXTERNAL> をスキップ
        match_consume(tokens, (Keyword, "TEMPORARY"))
        match_consume(tokens, (Keyword, "EXTERNAL"))
        # 4. <Keyword TABLE>を検証
        if not match_consume(tokens, (Keyword, "TABLE")):
            continue
        # 5. <Keyword IF>, <Keyword NOT>, <Keyword EXISTS>をスキップ
        if match_consume(tokens, (Keyword, "IF")):
            if not match_consume(tokens, (Keyword, "NOT")):
                continue
            if not match_consume(tokens, (Keyword, "EXISTS")):
                continue
        # 6. テーブル名が一致したら返す。
        if db == DEFAULT_DB and match_consume(tokens, (Name, table)):
            return query
        if (match_consume(tokens, (Name, db))
                and match_consume(tokens, (Punctuation, "."))
                and match_consume(tokens, (Name, table))
        ):
            return query


def is_comment(token: TokenType) -> bool:
    return token.ttype in sqlparse.tokens.Comment


def match_consume(tokens: List[TokenType], match_args: Iterable) -> bool:
    if tokens[0].match(*match_args):
        tokens.pop(0)
        return True
    return False

試してみると、うまく動くことが確かめられます。 以下の例はサンプルコードリポジトリのSQLで試した場合です。

find_hive_ddl("./ddls/*.sql", "apachelog")

> './ddls/apachelog_ddl.sql'

6.3. 実装詳細

ここでは、find_table_definition()の実装について、ポイントとなる箇所を解説していきます。 まず、文字列sqlをパースして、そのflattenでトークン列に変換してから、空白文字とコメントを取り除いています。

for query in sqlparse.parse(sql):
    tokens = [t for t in query.flatten() if not (t.is_whitespace or is_comment(t))]

空白文字の除去は簡単で、is_whitespace属性で判定できます。

ですが、コメントは少し特殊で、 is_commentという以下の関数を呼び出して除いています。

def is_comment(token: TokenType) -> bool:
    return token.ttype in sqlparse.tokens.Comment

トークンの種別を表すオブジェクト(Comment, DDL, Statement等)はttype属性に格納されています。 ttypeはsqlparse.tokens*5に定義されているのですが、 少し特殊な実装になっていて、sqlparse内で定義されている関数やメソッドに頼らずに扱おうとすると予期しない動作を引き起こす可能性があります。

詳細については今回は省略しますが、面白い実装なのでソースコードを読んでみることをオススメします! そのうち自分のブログの方でも取り上げようと思います。

話を戻すと、コメントの判定でtoken.ttypesqlparse.tokens.Commentをinで比較しているのは、 コメントのttypeはCommentトークンだけではなく、 一行コメントトークンSingleや複数行コメントトークンMultilineとなる場合があるからです。

先ほどの条件文は「トークンオブジェクトtokenがCommentトークンもしくはそのサブグループに属するSingle, Multilineのいずれかに該当する場合にのみ真」というものなので、これらを全て扱うことができます。

邪魔なトークンを取り除けたら、CREATE TABLE文かどうかの判定を行います。 ここまできたら正規表現でもいい気がしますが、 トークンオブジェクトのmatchメソッドを使って、個々のトークンを調べていきます。

まず、先頭にCREATEトークンが来ていることを確認します。 もしもCREATEトークンが来ていれば、そのトークンの次のトークンの判定に移り、 そうでなければ、そのクエリの解析を終えます。 この処理をコード化すると以下の通りになります。

if not tokens[0].match(DDL, "CREATE"):
    continue
tokens.pop(0)

matchにttypeと値を渡すことでトークンのマッチングができます。 regex引数にTrueを渡すと、値を正規表現として処理することもできます。

このトークンマッチ->トークンを進めるという処理を繰り返していくことになるので、 match_consume関数にまとめてしまいます。

def match_consume(tokens: List[TokenType], match_args: Sequence) -> bool:
    if tokens[0].match(*match_args):
        tokens.pop(0)
        return True
    return False

この関数を作ることで、以下のように処理が簡略化されます。

if not match_consume(tokens, (DDL, "CREATE")):
    continue

これを使って、以降の処理も同様に書くことができます。

# 3. <Keyword TEMPORARY> <Keyword EXTERNAL> をスキップ
match_consume(tokens, (Keyword, "TEMPORARY"))
match_consume(tokens, (Keyword, "EXTERNAL"))
# 4. <Keyword TABLE>を検証
if not match_consume(tokens, (Keyword, "TABLE")):
    continue
# 5. <Keyword IF>, <Keyword NOT>, <Keyword EXISTS>をスキップ
if match_consume(tokens, (Keyword, "IF")):
    if not match_consume(tokens, (Keyword, "NOT")):
        continue
    if not match_consume(tokens, (Keyword, "EXISTS")):
        continue
# 6. テーブル名が一致したら返す。
if db == DEFAULT_DB and match_consume(tokens, (Name, table)):
    return query
if (match_consume(tokens, (Name, db))
        and match_consume(tokens, (Punctuation, "."))
        and match_consume(tokens, (Name, table))
    ):
    return query

実装は以上です。

もちろん、文法の細かいチェックなどは入っていないですが、 それでも着目している部分に関してはかなり正確なチェックをしつつ、 目的のDDL探索ができるようなスクリプトが簡単に書けることが分かります。

7. 次回予告

ここまで字句解析とその結果得られるトークン列の活用方法を紹介してきましたが、もっと高度なことをやろうと思うと色々と不便です。 例えば、SELECT文からサブクエリを抽出しようとすると、SELECT文の中の複雑な規則を条件分岐で表現することになり骨が折れます。

そこで、次回は今回紹介しなかった狭義の構文解析について簡単に解説し、 SELECT文中のテーブル間の依存関係抽出のスクリプト作成に挑戦しながら、 少しだけ高度なsqlparseの活用方法について紹介したいと思います。

8. おわりに

sqlparse.parseに色々と投げ込んでみると発見があって面白いので、ぜひぜひ試してみてください!

9. 補足

CREATE TABLE文の判定条件の詳細

HiveQLのCREATE TABLE文*6は、公式によると以下の通りです。

CREATE [TEMPORARY] [EXTERNAL] TABLE [IF NOT EXISTS] [db_name.]table_name    -- (Note: TEMPORARY available in Hive 0.14.0 and later)
  [(col_name data_type [column_constraint_specification] [COMMENT col_comment], ... [constraint_specification])]
  [COMMENT table_comment]
  [PARTITIONED BY (col_name data_type [COMMENT col_comment], ...)]
  [CLUSTERED BY (col_name, col_name, ...) [SORTED BY (col_name [ASC|DESC], ...)] INTO num_buckets BUCKETS]
  [SKEWED BY (col_name, col_name, ...)                  -- (Note: Available in Hive 0.10.0 and later)]
     ON ((col_value, col_value, ...), (col_value, col_value, ...), ...)
     [STORED AS DIRECTORIES]
  [
   [ROW FORMAT row_format] 
   [STORED AS file_format]
     | STORED BY 'storage.handler.class.name' [WITH SERDEPROPERTIES (...)]  -- (Note: Available in Hive 0.6.0 and later)
  ]
  [LOCATION hdfs_path]
  [TBLPROPERTIES (property_name=property_value, ...)]   -- (Note: Available in Hive 0.6.0 and later)
  [AS select_statement];   -- (Note: Available in Hive 0.5.0 and later; not supported for external tables)

大事なのは最初の1行CREATE [TEMPORARY] [EXTERNAL] TABLEです。 HiveQLには、CREATE DATABASE文のような<DDL CREATE>から始まる別のタイプのSQLがあるため、 それを弾くためにTABLEまでを解析させる必要があるわけです。

10. References

GitHub - andialbrecht/sqlparse: A non-validating SQL parser module for Python LanguageManual - Apache Hive - Apache Software Foundation

ビッグデータ分析・活用のためのSQLレシピ

ビッグデータ分析・活用のためのSQLレシピ

*1:余談ですが、queue.Queueオブジェクトなんかも、ぱっと見クラスなのに実はただのファクトリ関数です。継承しようとするまで気づきませんでした......。

*2:2.7にも対応していますが、すぐにサポート対象外になるそうです。

*3:構文木の根に当たるトークンオブジェクトです。

*4:DDLの書き方が全部統一されていたのでそれを満たす正規表現を採用したのですが、別のDDLファイル群に転用できなくて泣いた記憶があります。

*5:ここで定義されているのはトークンであり、トークンオブジェクトはsqlparse.sqlに定義されていますので、ご注意ください。

*6:LIKEによる定義コピーはここでは省略しています。

gganimateでバーチャートレースを作って競争心を煽る

ホクソエムサポーターの輿石です。普段はデータ分析会社で分析業務や社内Rパッケージ開発をはじめ分析環境を整備する仕事をしています。

最近WEB系のメディアで「バーチャートレース(bar chart race )」と呼ばれるぬるぬる動く棒グラフを見ることが増えてきました。興味を惹くという点で優れた面白い可視化だと思います。Rではgganimateパッケージを使うことで簡単にggplot2のグラフをアニメーションにできたので、作成方法を細かい部分含めて紹介します。

なお、本記事ではggplot2の詳細には触れていませんが、詳細は「Rグラフィックスクックブック」がオススメです。なんと本記事が投稿された2019年11月21日は約6年ぶりの改版となる第2版の発売日なようです!6年前は少し面倒だったことも今ではより簡単にできるようになっていたりするので、すでにggplot2が使えるあなたも是非ッ。

Rグラフィックスクックブック ―ggplot2によるグラフ作成のレシピ集

Rグラフィックスクックブック ―ggplot2によるグラフ作成のレシピ集

1.可視化のテーマ【qiitaのイイネ数を競う】

可視化にしろレースにしろ、自分の興味・関心のある事柄の方が面白いと思います。自分自身と関わりのあるテーマが良いなと思っていたところ、所属組織のqiitaのイイネ数をバーチャートレースで可視化するという記事が面白かったので、同じことをRからやってみます。
この記事では筆者が所属するorganizationをピックアップしてアニメーションを作成します。qiitaからデータを取得するコードも公開するので、是非ご自身の組織のデータで可視化してみてください。

最終的に作成したいアニメーションはこちらです。

2.データの取得

qiitaではデータ取得のためのAPIが提供されています。主にRからAPIを叩くqiitrパッケージを使い、一部APIが対応していない部分はスクレイピングでデータを取得します。API利用のために事前にユーザの管理画面からtokenを取得してください。
なお、qiitaのAPIには1時間に1000回のリクエスト制限があります。コード自体は放っておけば良い感じに休止してデータを最後まで取ってくれるよう記述していますが、イイネ数が多い組織で試す場合は時間に余裕をもってお試しください。参考までに、イイネ累計獲得数1位のメルカリさんの場合は2000強のリクエストだったので、2時間ちょっとかかります。

まずはこの記事で必要になるパッケージをまとめて読み込みます。

library(tidyverse)
library(gganimate)
library(gifski)
library(qiitr)
library(rvest)

定数を定義します。ぜひ自分の所属組織に変えて実行してみてください!

target_organization <- "valuesccg"
qiitr::qiita_set_accesstoken()

一部の処理を関数にまとめていきます。

qiita_get_organization_member <- function(organization) {
  target_url <- str_glue("https://qiita.com/organizations/{organization}/members")
  sess <- html_session(target_url)
  
  member_ids <- c()
  while (!is.null(sess)) {
    ids <- sess %>% 
      html_nodes(xpath = "//*[@class='od-MemberCardHeaderIdentities_userid']") %>%
      html_text() %>%
      str_remove("@")

    member_ids <- append(member_ids, ids)
    
    sess <- tryCatch(
      sess %>% follow_link(xpath = "//*[@class='st-Pager_next']/a"), 
      error = function(e) return(NULL)
    )
  }
  
  member_ids
}


qiita_get_likes_date <- function(item_id, page_limit = 100){  
  res <- try(qiita_api("GET", path = sprintf("/api/v2/items/%s/likes", item_id), page_limit = page_limit))
  while(is.null(res) | (class(res) == "try-error" && str_detect(res, "Rate limit exceeded"))){
    Sys.sleep(600)
    res <- try(qiita_api("GET", path = sprintf("/api/v2/items/%s/likes", item_id), page_limit = page_limit))
  }
  
  lubridate::ymd_hms(map_chr(res, "created_at"))
}

実際にデータを取得し、tidyに整形していきます。

# organization所属メンバー一覧の取得
organization_members <- qiita_get_organization_member(organization = target_organization)

# メンバーの投稿記事一覧の取得
possibly_qiita_get_items <- possibly(qiita_get_items, otherwise = NULL)
item_data <-
  organization_members %>%
  tibble(user_id = .) %>%
  mutate(res = map(user_id, ~possibly_qiita_get_items(user_id = ., page_limit = 100L)),
         item_id = map(res, pluck, "id"), 
         likes = map(res, pluck, "likes_count")) %>%
  select(-res) %>%
  unnest(cols = c(item_id, likes)) %>%
  unnest(cols = c(item_id, likes)) %>%
  filter(likes > 0) %>%
  mutate(query_num = ceiling(likes/100))

sum(item_data$query_num)

# 記事ごとにイイネが付いた日時を取得
likes_data <-
  item_data %>%
  mutate(likes_date = map(item_id, qiita_get_likes_date)) %>%
  unnest(cols = likes_date)

3.可視化のためのデータ加工

データを取得できたので、gganimateでの可視化に適した形にデータフレームを加工していきましょう。アニメーションの1コマ1コマを作成するためのデータフレームを作成し、1コマを識別できるidを付けてすべてを1つにまとめたデータフレームを作成します。
ここでは、直近1年のデータを対象にイイネ累計獲得数のTOP10を週ごとに集計し、52週間分をまとめたデータフレームを作成していきます。

likes_data_sum <-
  likes_data %>%
  filter(likes_date >= lubridate::today() - lubridate::dyears(1)) %>%
  mutate(weeks = paste0(lubridate::year(likes_date), "_w", str_pad(lubridate::week(likes_date), width = 2, pad = "0"))) %>%
  arrange(user_id, weeks) %>%
  group_by(user_id, weeks) %>%
  summarise(likes = n()) %>%
  mutate(score_sum = cumsum(likes)) %>%
  ungroup()

likes_data_sum <-
  likes_data_sum %>%
  select(-likes) %>%
  pivot_wider(names_from = weeks, values_from = score_sum) %>%
  pivot_longer(-user_id, names_to = "weeks", values_to = "score_sum") %>%
  arrange(user_id, weeks) %>%
  group_by(user_id) %>%
  fill(score_sum) %>%
  ungroup() %>%
  mutate(score_sum = if_else(is.na(score_sum), 0L, score_sum)) %>%
  group_by(weeks) %>%
  mutate(ranking = row_number(-score_sum)) %>%
  filter(ranking <= 10) %>%
  ungroup()

下記の形のデータフレームができました。

head(likes_data_sum) 
# A tibble: 6 x 4
  user_id weeks    score_sum ranking
  <chr>   <chr>        <int>   <int>
1 accakr  2018_w47         0       3
2 accakr  2018_w48         1       4
3 accakr  2018_w49         1       4
4 accakr  2018_w50         1       5
5 accakr  2018_w51         1       5
6 accakr  2018_w52         1       5

4.アニメーションの作成①

バーチャートレースを作成する場合は、geom_bar()ではなく、geom_tile()を使うことがポイントです。
1つのコマに絞ってデータを可視化する要領で記述したコードに、transition_states()関数を追加しコマを識別する列を指定するだけでアニメーションが作成できます。(facet_wrap()の代わりにtransition_states()を使うイメージ。)

p <-
  likes_data_sum %>%
  ggplot(aes(x = ranking, group = user_id)) +
  geom_tile(aes(y = score_sum/2,
                height = score_sum,
                fill = user_id,
                width = 0.9)) +
  geom_text(aes(y = 0, label = paste(user_id, " ")), vjust = -1, hjust = -0.1) +
  geom_text(aes(y = score_sum, label = paste0(" ", score_sum), vjust = 1, hjust = 0)) +
  scale_x_reverse() +
  coord_flip() +
  theme_light()

p +
  transition_states(weeks)

tmp_race_animation

(なんだかカクカクしていますが、)ひとまずアニメーションを作ることができました!

5.アニメーションのカスタマイズ

ここではgganimateパッケージの関数について詳細をまとめます。アニメーションに関する設定を理解し、より質の高いアニメーションを作りましょう。

i. transition_xxx()

「transition_」から始まる関数で1コマ1コマの移り変わりの軸を何にするか、移り変わりの大まかな関係をどうするか、を決めます。アニメーション作成に最低限必要な関数です。沢山ありますが、以下の3つを覚えておけばいいでしょう。

a. transition_states()

カテゴリ変数・離散変数間の移り変わりをアニメーションにしたいときに使います。
引数transition_lengthとstate_lengthで1回のコマ遷移でアニメーションが動く時間と止まる時間の割合を決めることができます。
また、wrap=T(デフォルト)の場合はアニメーションが最後のコマになった際に、最初のコマに戻るところまでがアニメーションになるようです。

b. transition_time()

日時を表す変数を軸に、時間の移り変わりをアニメーションにしたいときに使います。
時間の間隔とアニメーションの移り変わりを合わせてくれるようで、例えばc(2015, 2016, 2018, 2019)のように一部期間のデータがかけていても時間間隔が合うように補完されたアニメーションが出力されます。実際の時間間隔に合うようになっているので、transition_state()のようにアニメーションが止まる時間を作ることはできず、常にぬるぬると動くアニメーションになります。

#2017年がないデータ
test_data <- data_frame(timing = c(2015, 2016, 2018, 2019), id = "1", y = c(0, 1, 2, 3))

test_data %>%
  ggplot() +
  geom_bar(aes(x = id, y = y), stat = "identity", width = 0.5) +
  coord_flip() +
  transition_time(timing) +
  labs(title = "[{as.integer(frame_time)}] : {frame_time}")

c. transition_reveal()

主に折れ線グラフを作成する際など、徐々にデータを追加しながらplotしなければならないケースで使うようです。月ごとの折れ線グラフの場合は、1コマ目は1月目のデータ、2コマ目は1~~2ヶ月目までのデータ、という形でplotされ、アニメーションになります。

なお、各transitionで「今何を描画しているか」などを表す変数が用意されており、glueの形式でタイトルなどテキストに挿入することが可能です。transition_states()ならば{closest_state}や{previous_state}、transition_time()ならば{frame_time}と、transitionごとに異なるので関数のヘルプで確認するようにしましょう。

ii. view_follow()

軸のメモリ範囲をコマごとのデータ範囲によって可変にしたいときに使います。
ただし、今回のgeom_tile()を使ったチャートでは可変にするとaxis.titleやaxis.textなど軸系のラベルがうまく表示できないようです。themaの設定で非表示にしています。

iii. ease_aes()

コマ間の移り変わりの際に、「最初ゆっくり動いて早くなってまたゆっくり動く」など変化に抑揚を付けたいときに使います。"cubic-in-out", "sine-in-out"などの文字列を指定するのですが、これらはCSSやjQueryのアニメーションに緩急をつけるプロパティ「イージング」の用語なようです。このサイトを見ると"cubic"や"sine"の違いや他の選択肢が分かります。ease_aes()を指定しない場合と、"cubic-in-out"を指定した場合の動きの違いを見てみましょう。

test_data <- data_frame(timing = c(1, 2, 3), id = "1", y = c(0, 1, 2))
p <-
  test_data %>%
  ggplot() +
  geom_bar(aes(x = id, y = y), stat = "identity", width = 0.5) +
  coord_flip() +
  transition_states(timing, transition_length = 6, state_length = 1, wrap = FALSE)

animate(p, nframes = 100, fps = 10, width = 800, height = 100)  
animate(p + ease_aes("cubic-in-out"), nframes = 100, fps = 10, width = 800, height = 100)  

ease_aesを指定しない場合(一定の速度で増加) ease_aes_normal ease_aesに"cubic-in-out"を指定(増加に緩急が付く) ease_aes_cubic

iv. enter_xxx/fade_xxx

新しいオブジェクト(棒や点など)の描画のされ方や、プロット内のオブジェクトが画面から消える際の消え方を指定できます。enter_fade()やexit_shrink()などがあります。

v. animate()

アニメーションはanimate()関数でレンダリングすることで最終的なアウトプットとなります。ここでは主に下記の項目を決めます。

  • アニメーションの時間の長さとフレーム数
  • 出力形式
  • アニメーションの縦横のサイズ

a. アニメーションの時間の長さとフレーム数

引数3つでduration = nframe ÷ fpsの関係が成り立ちます。

  • nframe:フレームの数(default = 100)
  • fps:フレームレート。1秒当たりのフレーム数(default = 10)
  • duration:アニメーション全体の時間の長さ(デフォルトは100/10で10秒)

一般にfpsが高い方がデータが重くなり、映像が滑らかになります。データのプロットは10fpsもあれば十分なケースが多いでしょう。(テレビアニメは24fpsです)
アニメーションがカクカクしている!もっと滑らかにしたい!という場合はfpsはそのままで、durationを長くしてみることをオススメします。fpsよりもコマ(今回のケースでは52週間分52コマ)を何秒に収めるかの方が全体的な滑らかさを決めるポイントになるように思います。

b. 出力形式

xxx_renderer()関数で出力形式を指定できます。デフォルトはgifski_renderer()でgifが出力されます。

c. アニメーションの縦横のサイズ

widthとheightで指定しましょう。

d. (おまけ)start_pause、end_pause引数でアニメーションを最初か最後で静止させる

start_pauseもしくはend_pause引数で最初か最後のフレームでアニメーションを一時停止させることができます。指定しないと最後に到達した瞬間に最初に戻るので、「最後どうなった!?」とツッコみたくなります。指定でちょっとハマったのでまとめました。

  • frame数で指定する。
    「1秒間静止させるframe数=fps」です。値をあらかじめ確認して指定しましょう。
  • 静止させた分duration(アニメーションの時間)を長くする。
    静止させた分も含めてdurationなので、例えば5秒で最後まで動かして1秒静止させたいという場合はdurationは6にしましょう。
  • transition_states()を使い最後で静止させる場合は、wrap=Fにする。
    デフォルトのwrap=Tの場合最後のコマから最初のコマに戻るところまでがアニメーションになります。最後のframeは最初のコマに戻る寸前のframeであり、中途半端ななんとも言えないタイミングで静止してしまいます。transition_states(wrap=F)で最後でぴったり終わるようにしましょう。

6.アニメーションの作成②

それでは最終的なアニメーションを出力したいと思います。

p <-
  p +
  theme(axis.line=element_blank(),
        axis.title.x=element_blank(),
        axis.title.y=element_blank(),
        axis.text.x=element_blank(),
        axis.text.y=element_blank(),
        legend.position="none",
        panel.border=element_blank(),
        panel.grid.major.y=element_blank(),
        panel.grid.minor.y=element_blank(),
        plot.title=element_text(size=25, hjust=0.5, face="bold", vjust=-1, lineheight = 1),
        plot.subtitle=element_text(size=18, hjust=0.5)) +
  labs(title = "直近1年間のqiita累計イイネ獲得数ランキング : {closest_state}",
       subtitle = paste0("organization:", target_organization)) + 
  transition_states(weeks, transition_length = 6, state_length = 1, wrap = F) +
  view_follow() +
  enter_fade() +
  ease_aes("cubic-in-out")

final_animation <- animate(p, fps = 10, duration = 32, end_pause = 20, width = 800, height = 400)

# 保存するときはanim_save()
dir.create("images")
anim_save("images/race_bar_chart.gif", final_animation)

良い感じにぬるぬる動くアニメーションになりました!
(私の順位は下から3番目なようです。 )

ggplot2が書ければアニメーションも作れてしまいますね。R凄い。

Enjoy!

参考文献

EMNLP2019の気になった論文を紹介

ホクソエムサポーターの白井です。

EMNLP-IJCNLP 2019 (以降 EMNLP) が先日、香港で開催されました。 EMNLPは Empirical Methods in Natural Language Processing の略称で、ACLやNAACLと並ぶ、計算機科学のTop conferenceと言われてます*1。 今年採択されたEMNLPの論文は682本 (+システム/デモ論文45本) です。 (年々増えています。)

今回は、EMNLP2019の論文から、いくつか気になったものを紹介します。 前回に引き続き、検証系の論文とデータ構築についての論文をメインに扱います。

以降、記載する図表は、明記しない限り、論文から引用しています。

1. ner and pos when nothing is capitalized

※タイトルはtypoではないです

英語において、大文字・小文字の使い分けにはルールがあります。例えば、文の最初や固有名詞の最初の文字は大文字を使います (e.g. "For these language, ..." , Hong Kong)。 これらの大文字の情報は、NER (Named Entity Recognition: 固有表現抽出) やPOS (Part of Speech Tagging: 品詞タグ付け) タスクにおいて重要な情報となり得ます。

ちなみに、NERは人物、場所、組織といった固有名詞を抽出するタスクです。下の例であれば、 AllenNLPPyTorchAllen Institute for Artificial IntelligenceORG (組織)、SeattleLOC (場所) だとモデルが予測しています。

from https://demo.allennlp.org/named-entity-recognition/ のelmo-nerによる予測結果(文はサイトの例文より引用)

一方、POSは単語に対して動詞や名詞などの品詞タグを付与するタスクです。下の Stanford CoreNLPによる結果では、 Penn Tree Bankの定義にしたがって単語にタグ付けされています。 例えば、 wePRP (Personal pronoun: 人称代名詞)、performVBP (Verb, non-3rd person singular present: 動詞, 三単現でない) のタグが付与されています。

from http://nlp.stanford.edu:8080/corenlp/process の予測結果 (例文は本論文のアブストラクトより引用)

これらのNERやPOSは、自然言語処理において、基礎となる重要な処理です。

しかしながら、SNSの投稿のようなくだけたテキスト、音声認識の出力などでは、ルール通りに大文字が使われていないこともあります。その場合、大文字を学習に利用したモデルでは、精度が落ちることが示されています。

(Casedが大文字のテキスト、Uncasedが小文字化したテキストでのタスクの精度。Uncasedのほうが精度が低いことがわかる。)

先行研究ではTruecasing (大文字を含む文に復元する方法) が試みられていますが、特有のcasingやtruecasingの間違いによってエラーが引き起こされるため、課題があるように思えます。

本論文では、NERとPOSタスクにおいて、cased text (大文字を含むテキスト) と lower-cased/uncased text (小文字のみのテキスト) での結果を比較し、最も適切な方法について検証しています。

検証した実験設定は以下のとおりです。

  1. Train on cased: 学習データもテストデータもそのまま
  2. Train on uncased: 学習データもテストデータも小文字化する
  3. Train on cased+uncased: そのままのデータと小文字化したデータどちらも学習データに利用
    • Half Mixed: 学習データの50%のみ小文字化して利用
  4. Train on cased, test on truecased: 学習データはそのまま使い、テストデータをtruecasingする
  5. Truecase train and test: 学習データもテストデータもtruecasingする

結果としては、3. Train on cased+uncased がいずれのタスクにおいても良いパフォーマンスであることが示されています。

学習に用いるデータ量が単純に2倍になっていることが要因の一つだと考えられます。

当たり前の結果といえばそうなのですが、汎用的に利用できるモデルを考える上では、小文字化したデータも利用して学習が必要な可能性を示唆していて興味深いと思いました。

日本語には英語における大文字小文字に対応するルールはありませんが、カタカナ・ひらがな・漢字のように複数の表記方法があるので、表記方法の違いについて検証する必要があるのかもしれません。

2. A Little Annotation does a Lot of Good: A Study in Bootstrapping Low-resource Named Entity Recognizers

上記論文の結果からも分かるとおり、ニューラルネットワークを用いたNER (固有表現抽出) モデルにおいて、重要なのはラベル付き学習データ (教師データ) です。英語では、そのようなデータが比較的豊富に存在しますが、他の言語の場合は、十分なデータ資源がないというのが現状です。

しかし、ラベルつき (アノテーション) データを作成するのは大変で、コストもかかるため、最低限の資源で実装したいところです。

本論文では、課題が多いLow-resourceな言語でのNERに対して、有効な "recipe" を紹介しています。流れとしては大きく3つ、cross-lingual transfer learning (言語を横断する転移学習)、active learning、fine-tunningです。

  • Cross-Lingual Transfer Learning
    • 2つの単語分散表現を用意し、bilingual word embeddings (BWE)を学習する
    • BWEを利用して、the cross-domain similarity local scaling (CSLS) metric (Github)によって単語間の辞書を作る
  • Entity-Targeted Active Learning
    • ラベルの付いていないsequenceから、最も有益なスパン (テキストの範囲) を選択する
    • アノテーターの作業は、entity typeを選ぶか、スパンの修正だけですむ
    • 図のETALが提案方法、SALが全てのシークエンスにアノテーションする方法
  • Fine-tunning
    • a BiLSTM-CNN-CRF model (Ma and Hovy (2016))を用いる
    • Active Learningではアノテーションが一部しかついていないため、Partical-CRFを利用

実験では、ドイツ語・スペイン語・オランダ語・ヒンディー語・インドネシア語について検証しています。

データ資源が少ないと、データを増やす方法を考えがちですが、必要最小限のリソースでタスクを解く工夫を提案している点が面白いと感じました。

3. Are We Modeling the Task or the Annotator? An Investigation of Annotator Bias in Natural Language Understanding Datasets

上記論文でも言及したように、人手によるアノテーションつきデータは自然言語処理の研究において重要であるものの、作業コストなどの課題が多く存在します。

膨大なテキストに対してアノテーションする場合、多くはAmazon Mechanical Turk (AMT) などのクラウドソーシングサービスを利用し、クラウドワーカー(アノテーター)に作成を依頼しています。近年の研究では、高い質のデータを作成できる少人数が膨大なデータを生成する方法を選んでいるようです。

しかしながら、Natural Language Understanding (NLU) タスクなどで利用されるフリー記述の場合、少人数が作成したデータは、データの多様性やモデルの生成能力に影響する可能性があります。

本論文では、NLUタスクにおける、アノテーターによる影響 annotator bias を調査しています。

  • 以下の、アノテーター情報が含まれるNLUデータセットについて調査
    • MNLI: テキストの含意を推論するデータセット
    • OpenBookQA: multi-hop (多段階で推論が必要) に着目した多項式のQAデータセット
    • CommonsenseQA: 常識に着目した多項式のQAデータセット
  • BERTモデルに、入力文 x 正解ラベル y に加えてアノテーター情報 z を含む、 ((z, x),y) を与えて学習を行う
    • 結果、アノテーター情報を含むとモデルの精度が向上した
    • With ID がアノテーター情報を含むモデル
  • 同様に、BERTをfine-tuningしてアノテーターの識別をしたところ、データを作成した割合が高いアノテーターほど、識別が容易であることがわかった

これらの結果から、アノテーターによって学習データとテストデータのアノテーターは分離させるべきであることが論文中で提案されています。

データを扱う側としても、アノテーションに何人関わっているのか、アノテーターによって偏りがないかどうか、確認する必要があると考えさせられました。

  • 参考

4. (Male, Bachelor) and (Female, Ph.D) have different connotations: Parallelly Annotated Stylistic Language Dataset with Multiple Personas

残りの2つはデータがメインの論文を紹介します。

先ほどの annotation bias のように、実際の人間の扱う言葉には、スタイル (≒文体) の違いがつきものです。性別・年齢による違いはもちろん、同じ人物でも状況に合わせて、言葉遣いが変化します。

このような、文章のスタイルの違いに着目し、文章を目的のスタイルに変換する研究は Style transfer と言います。Computer Visionでは、○○っぽい画風に変換する研究で有名かと思います。

f:id:sh111h:20191203222634p:plain from https://research.preferred.jp/2015/09/chainer-gogh/

NLPにおけるStyle transferも、文のスタイル変換や、著者のプロファイルなどに応用されています。 しかしながら、これらの研究ではパラレルコーパス (翻訳における複数の言語で文同士の対応が付いたコーパス *2 。Style transferにおいては 複数のスタイルで 同じ意味 のコーパス) を利用していないものが多いです。それにより、複数のスタイルのバリエーションを学習したり、評価することは困難です。

そこで、本論文は、スタイル、特にペルソナ情報についてのパラレルコーパス、PASTEL (the parallel and annotated stylistic language dataset) について紹介しています。

  • データ構築には Visual Story Tellingを利用
    • 元々の意味を保持しつつ、自身のスタイルで文書を作成してもらう
    • どの情報を与えればスタイルが多様になるか予備実験
    • 最も良かった Story (images + local keywords) (画像とキーワードを与える) 方法でアノテーションを行う

  • アノテーターにはペルソナ情報を入力してもらう
    • 性別・年齢・民族・出身・学歴・政治的立場

このパラレルコーパスによって、教師ありのスタイル変換が可能になります。

from Github

また、このパラレルコーパスを用いた応用方法として、controlled style classification を提案しています。 これは、他のスタイル情報を固定した上で、一つのスタイルについて分類するタスクです。例えば、与えられた文章の年齢や性別を当てることで、テキストにおける年齢・性別の違いを表現する特徴量がわかります。

画像とは異なり、言語には様々なスタイルが絡んでくるため、明示的にスタイル情報が示されているパラレルコーパスが提案されるのは画期的だと思いました。

5. Open Domain Web Keyphrase Extraction Beyond Language Modeling

Keyphrase Extraction (KPE: キーフレーズ抽出) は文書から特徴的なフレーズを抽出するタスクです。 先行研究では、学術論文のドメインにおいて、効果的なモデルが提案されていますが、これは、論文中に論文の著者が作成したキーワードが含まれていることがあり、ドメインの学習データが比較的豊富なことが要因です。

しかし、実応用においては、学術ドメインに限らず、スパースなデータ量の、様々なドメインにおいてキーフレーズ抽出ができることが求められます。

本論文では、オープンドメインのキーフレーズ抽出タスクに着目したデータセット OpenKP について紹介しています。

  • 文書はBing Search Engineからサンプリングされたウェブページ (詳細は http://www.msmarco.org)
    • ドメインや文書の種類で制限はしていないため、ニュース記事・動画ページ・リンクまとめページも含んでいる
    • トピックもばらついており、最も多いトピック healthcare でも全体の3.7%程度
  • 1つの文書につき1~3のキーフレーズを人手でアノテーション
    • 複数人の一致率は高くない
    • ランダムに選択した50文書において、5人の上位3つのキーフレーズで、完全一致したのは43%

また、このデータセットのタスクを解くためのモデル BLING-KPE (Beyond Language UnderstandING KeyPhrase Extraction) を提案しています。アーキテクチャの大きな枠組みはCNNとTransformerですが、タイトルの Beyond Language Understanding を目的とした2つの特徴があります。

  • visual feature (画像の特徴量)
    • データがウェブページであるため、画面の表示情報を特徴量として入力する
    • 具体的には、フォント、テキストサイズ、表示位置、DOM
  • weak supervision (弱い教師)
    • 検索クエリからそのドキュメントのページがクリックされたかどうかを識別する Query Prediction タスクを、モデルの事前学習に利用
    • クエリはBing search logから取得

OpenKPでの実験の結果、上記2つの特徴量は有意な特徴量であることが示されています。

赤枠がvisual featureなし、緑枠がvisual featureありの予測結果。 この結果から、フォントサイズやテキストサイズなどの visual feature が有効であることがわかる。

おわりに

EMNLP2019の論文を5つ紹介しました。

その他、本会議中開催されたTutorialの資料は多くの場合公開されています。 そのトピックについて包括的に知るのには良い資料だと思うので、気になるトピックがあれば読んでみるのもいいかもしれません。

参考: Tutorialのリンク集

ゼロから作るDeep Learning ❷ ―自然言語処理編

ゼロから作るDeep Learning ❷ ―自然言語処理編

  • 作者:斎藤 康毅
  • 出版社/メーカー: オライリージャパン
  • 発売日: 2018/07/21
  • メディア: 単行本(ソフトカバー)