バイアスと戯れる

Rと言語処理と(Rによる言語処理100本ノック終了)

第51回R勉強会@東京(TokyoR)にてLT発表しました

概要

 10月10日に開催されたTokyoRのLTセッションにて、『Rでいろんな言語』というタイトルでRからPythonを呼ぶパッケージ{PythonInR}を紹介しました。発表の途中でスクリーンが砂嵐になるというトラブルに見舞われましたが、なんとか無事に時間内に収めることができました。
 また、公開したスライドに未掲載だった{PythonInR}を使ってChainerを呼び出したコードを手直ししたので、メモ書きとして残しておきます。

第51回R勉強会@東京(#TokyoR) : ATND

speakerdeck.com



発表について

 LT発表スライドの概要は次の通りです。

  • 2015年10月10日はTokyoRとPyConJPが同日に開催されました
  • とても素晴らしいPythonを脆弱なRから活用できたらRユーザーは幸せだけど、できないのだろうか? 
  • {PythonInR}というパッケージがあり、関数や環境が充実しているようで、試してみたら呼べてしまいました

 以上のような、開催日にちなんだ発表をさせていただきました。

 {PythonInR}はドキュメントが充実しており、そちらをまとめる&実行してみた内容がメインになっております。
 PythonInR Reference — PythonInR 1.0.0 documentation

 そして、PythonパッケージをRで使用する例として、スライドではgensimのword2vec()で単語の分散表現を学習していますが、paragraph2vecもgensimから呼び出せました(発表スライド中のコマンドとともにRPubsへ後日公開予定)。
 また、Chainerによるword2vecは自前のMac PCでも動作しましたが(複数インストールされているPythonで苦しみましたが)、GloVeのPython実装はLinux上でのみ確認しております(以前にCaffeのインストールで、gccをいろいろやったせいかも。こちらのスクリプトも調査して後日公開予定)。
 maciejkula/glove-python · GitHub




{PythonInR}を使ってChainerを動かす

 以下はLT発表後に手直ししたChainerを用いたword2vecを行うRコード(ほぼPython)です。
 各種PythonコードはChainerのexamples/word2vec/train_word2vec.pyを参考にさせていただいております。
 chainer/train_word2vec.py at master · pfnet/chainer · GitHub


Pythonコード(関数定義)

# def-chainer-word2vec-fun.py
def continuous_bow(dataset, position, window):

  h = None
  w = np.random.randint(window - 1) + 1
  for offset in range(-w, w + 1):
    if offset == 0:
      continue
    d = xp.asarray(dataset[position + offset])
    x = chainer.Variable(d)
    e = model.embed(x)
    h = h + e if h is not None else e
      
    d = xp.asarray(dataset[position])
    t = chainer.Variable(d)

  return loss_func(h, t)

def skip_gram(dataset, position, window):
  
  d = xp.asarray(dataset[position])
  t = chainer.Variable(d)
  
  w = np.random.randint(window - 1) + 1
  loss = None
  for offset in range(-w, w + 1):
    if offset == 0:
      continue
    d = xp.asarray(dataset[position + offset])
    x = chainer.Variable(d)
    e = model.embed(x)
  
    loss_i = loss_func(e, t)
    loss = loss_i if loss is None else loss + loss_i
  
  return loss


Rコード(準備)

# コサイン類似度
filterCosineSim <- function (
  seed_word_vector, target_word_vectors, 
  extract_rownames = NULL
) {
  word_vectors <- rbind(seed_word_vector, target_word_vectors)
  numerator <- crossprod(x = t(x = word_vectors))
  denominator <- diag(numerator)
  return((numerator / sqrt(outer(denominator, denominator)))[extract_rownames, ])
}

# Rパッケージ読み込み
library(PythonInR)


# Python側で使うライブラリ群の読み込み
PythonInR::pyImport(import = "time")
PythonInR::pyImport(import = "collections")

PythonInR::pyImport(import = "numpy", as = "np")

PythonInR::pyImport(import = "chainer")
PythonInR::pyImport(from = "chainer", import = "cuda")
PythonInR::pyImport(import = "chainer.functions", as = "F")
PythonInR::pyImport(import = "chainer.optimizers", as = "O")

PythonInR::pyExecp(code = 'xp = np')
PythonInR::pyExecfile(filename = "def-chainer-word2vec-fun.py")


# パラメータや引数の設定
## 明示的にas.integer()で整数にしないとfloatになる
PythonInR::pySet(key = "window", value = as.integer(5))
PythonInR::pySet(key = "unit", value = as.integer(200))
PythonInR::pySet(key = "batchsize", value = as.integer(100))
PythonInR::pySet(key = "set_epoch", value = as.integer(10))

# 入力ファイルもexamplesと同じものを使う
## https://raw.githubusercontent.com/tomsercu/lstm/master/data/ptb.train.txt
PythonInR::pySet(key = "train_file_name", value = "ptb.train.txt")
# データ準備(Rで書いてもよさそう)
read_file <- '
word2index = {}
index2word = {}
dataset = []
counts = collections.Counter()
with open(train_file_name) as f:
  for line in f:
    for word in line.split():
      if word not in word2index:
        ind = len(word2index)
        word2index[word] = ind
        index2word[ind] = word
        counts[word2index[word]] += 1
        dataset.append(word2index[word])

    n_vocab = len(word2index)
'
PythonInR::pyExec(code = read_file)

# R上でデータを確認(型キャストされる)
## http://pythoninr.bitbucket.org/TypeCasting.html#r-to-python-pyget
> PythonInR::pyGet(key = "index2word")[1:20]
             0              1              2              3              4              5 
         "aer"     "banknote"      "berlitz"     "calloway"     "centrust"       "cluett" 
             6              7              8              9             10             11 
   "fromstein"       "gitano"     "guterman" "hydro-quebec"          "ipo"          "kia" 
            12             13             14             15             16             17 
     "memotec"          "mlx"         "nahb"        "punts"         "rake"      "regatta" 
            18             19 
      "rubens"          "sim" 

# データ数
> PythonInR::pyExec(code = 'print(len(dataset))')
9999


Rコード(学習部)

# モデルと損失関数の定義
PythonInR::pyExecp(code = 'model = chainer.FunctionSet(embed = F.EmbedID(n_vocab, unit),)')

# (「Skip-gramモデルとSoftmax+CE」の組み合わせ。examplesのプログラムには他の計算方法もある)
PythonInR::pyExecp(code = 'train_model = skip_gram')
# PythonInR::pyExecp(code = 'train_model = continuous_bow')

def_loss_func <- '
model.l = F.Linear(unit, n_vocab)
loss_func = lambda h, t: F.softmax_cross_entropy(model.l(h), t)
'
PythonInR::pyExec(code = def_loss_func)
# 学習の実行
run_optimizer <- '
dataset = np.array(dataset, dtype = np.int32)

# 最適化手法にはAdamを使う
optimizer = O.Adam()
optimizer.setup(model)

word_count = 0
skip = (len(dataset) - window * 2) // batchsize

# 経過表示用
begin_time = time.time()
cur_at = begin_time
next_count = 100000

for epoch in range(set_epoch):
  accum_loss = 0
  indexes = np.random.permutation(skip)
  print("epoch: {0}".format(epoch))
  for i in indexes:
    # 経過表示
    if word_count >= next_count:
      now = time.time()
      duration = now - cur_at
      throuput = 100000. / (now - cur_at)
      print("{} words, {:.2f} sec, {:.2f} words/sec".format(word_count, duration, throuput))
      next_count += 100000
      cur_at = now

    position = np.array(range(0, batchsize)) * skip + (window + i)
    loss = train_model(dataset, position, window)

    accum_loss += loss.data
    word_count += batchsize

    optimizer.zero_grads()
    loss.backward()
    optimizer.update()

  print(accum_loss)
'

# 学習実行
> PythonInR::pyExec(code = run_optimizer)
epoch: 0
5075.76473427
epoch: 1
4424.79116631
epoch: 2
4015.50499153
epoch: 3
4136.20912743
epoch: 4
3990.47239399
epoch: 5
3487.66871929
epoch: 6
3219.87060261
epoch: 7
3205.53463268
epoch: 8
2983.06294155
epoch: 9
2466.28941822


Rコード(結果の取得)

PythonInR::pyExec(code = 'model.to_cpu()')
embed_word_res <- PythonInR::pyGet(key = 'model.embed.W')
rownames(embed_word_res) <- names(sort(PythonInR::pyGet(key = 'word2index')))

> t(embed_word_res[1:5, 20])
                 aer    banknote      berlitz     calloway     centrust
  [1,] -1.9210659266  2.66550684  0.319574237  0.381956309  0.577695131
  [2,] -1.3218621016 -2.49230599 -0.679477215  0.139727041  0.136762887
  [3,]  0.3553474247 -1.27228832 -0.015422379  0.154143527  0.941983998
  [4,]  0.5452865958 -0.57457387 -1.342149138  1.199438214 -0.491248012
  [5,] -1.4254230261 -0.46078750  0.217225134  0.746220648  1.651308060
  [6,] -0.2459839880 -1.02575839  0.861322761  1.275416017  0.504857481
  [7,]  0.6889060736  0.62762350  1.335024953 -1.237316370  0.422370821
  [8,]  0.4057052135  0.89054787 -2.274557590  1.760630727  1.280030966
  [9,] -0.5444009304  0.31793687  1.741585135  0.085238419 -1.072679877
 [10,]  0.0153995762 -1.40665817 -1.653285742 -0.471961915 -0.945782900
 [11,]  0.2586796284 -2.63728619  0.974781752 -1.091140628  0.711577356
 [12,]  1.2850890160  0.92642528  0.059416633  1.979779959  1.517618895
 [13,] -1.0132664442 -1.57576632 -0.084011830  1.128095150  1.733328104
 [14,]  0.6541242003 -0.28958291 -2.761154890  0.630589366 -0.688703120
 [15,] -0.4360258877 -1.11609161  0.149192870  0.148330510 -0.382760823
 [16,]  0.4184043705 -0.96482414 -0.007774435 -0.421285033 -0.534063876
 [17,] -0.9240003228  1.18594503  0.470627725  0.278243899  0.336607963
 [18,] -1.0976681709  0.59142280 -0.482638657 -1.455917716  0.520051181
 [19,]  1.2184268236 -0.32253051  1.953751206 -1.087785363 -0.101868734
 [20,]  0.6005632281  0.90789568  1.657281041  1.603208303 -0.275181383


Rコード(アナロジー)

# "sister" + "husband" - "brother"
seed_word_vector <- matrix(
  data = -embed_word_res["brother", ] + embed_word_res["sister", ] + embed_word_res["husband", ],
  nrow = 1, dimnames = list("-brother+sister+husband", NULL)
)
seed_word_cs <- filterCosineSim(
  seed_word_vector = seed_word_vector, 
  target_word_vectors = embed_word_res,
  extract_rownames = rownames(seed_word_vector)
)

# "wife"が出てきて欲しいが(TOP3は演算に使用しているので除外)
sort(
  x = seed_word_cs[setdiff(x = names(seed_word_cs), names(seed_word_vector))],
  decreasing = TRUE
)[1:5]
  
-brother+sister+husband                  husband                   sister                   unveil 
               1.0000000                0.5998873                0.4992677                0.2933514 
                    feb. 
               0.2673408 


まとめ

 {PythonInR}というRからPythonを呼ぶパッケージを使い、Chainerのword2vecのデモを動かすRコードを書きました。Chainerが動作したので、RユーザーでもDeep Learningの恩恵が享受しやすくなりました。また、Chainer以外のライブラリも挙動を確認しているので、RにはなくてPythonにあるというパッケージを試したいという方々は今まで以上に手を広げやすくなったかと思います。
 とはいえ、Pythonコードを書くシーンが多いので、これを機にPythonの勉強を始めるのもいいかもしませんね。
 あと、学習した分散表現を用いたアナロジーがあまりいい結果でなかったので、書き方ややり方を間違えていたかもしません。こちらに関しては、もう少し調査をしたいと思います。



参考



その他

 RでPythonを呼ぶ方法として、他にも{Rcpp}を使う例もあります。興味がある方は下記のリンクをご覧ください。
 Call Python from R through Rcpp

 LT発表後に{PythonInR}に興味持たれた@kohske 先生が .RmdファイルでPythonチャンクを書き、それをRチャンク内で呼び出す仕組みを試みております。

 RPubs - RMarkdown de PythonInR
 https://gist.githubusercontent.com/kohske/f4b8a828da5da7c74717/raw/f17248be43d3e61bf8d8de7e05bc1d17d2a070cb/pynR.Rmd

 そのTLを追って知った{runr}も興味深いですね。

 https://github.com/yihui/runr



実行環境

> devtools::session_info()
Session info -----------------------------------------------------------------------------------------
 setting  value                       
 version  R version 3.2.2 (2015-08-14)
 system   x86_64, darwin13.4.0        
 ui       RStudio (0.99.467)          
 language (EN)                        
 collate  ja_JP.UTF-8                 
 tz       Asia/Tokyo                  

Packages ---------------------------------------------------------------------------------------------
 package   * version date       source                        
 curl        0.9     2015-06-19 CRAN (R 3.2.0)                
 devtools    1.8.0   2015-05-09 CRAN (R 3.2.0)                
 digest      0.6.8   2014-12-31 CRAN (R 3.2.0)                
 git2r       0.10.1  2015-05-07 CRAN (R 3.2.0)                
 memoise     0.2.1   2014-04-22 CRAN (R 3.2.0)                
 pack        0.1-1   2015-04-21 local                         
 PythonInR * 0.1-1   2015-09-19 CRAN (R 3.2.0)                
 R6          2.1.1   2015-08-19 CRAN (R 3.2.0)                
 Rcpp        0.12.0  2015-07-26 Github (RcppCore/Rcpp@6ae91cc)
 rversions   1.0.1   2015-06-06 CRAN (R 3.2.0)                
 xml2        0.1.1   2015-06-02 CRAN (R 3.2.0)