Edited at

reticulateパッケージでRからPythonを使う


概要


  • reticulateパッケージはPythonを活用するRのパッケージ

  • reticulateパッケージの使い方とライブラリ利用例を実行結果を提示して確認

  • 試したライブラリはSentence Piece, Pytorch, AllenNLPなどは動作している

 (本記事ではRを「パッケージ」とPythonを「ライブラリ」という風に書き分けています)


前書き

 有用なライブラリがPythonで開発されているため使いたくなりますが、Pythonはわからない・書けないというRユーザーは未だに多くいらっしゃいます。

 しかしながら、すでにあるPythonライブラリの関数にデータを適用した結果だけが欲しいのであれば、reticulateパッケージで事足りてしまうようです。

 同様のパッケージには以前に紹介したPythonInRやrPythonなどがありますが、reticulateがリリースされてからは私はこちらを用いています(kerasパッケージが公開前に行ったreticulateでKerasを使って画像生成する記事でも使用しています)。

 たまに「PythonInRパッケージの記事を参考にしました!」と言われて「すみません、私はもう使っていないです」と答えるのもとても心苦しいですので、reticulateパッケージの使い方や利用例などを実行して確認する形でまとめました(主に自分用)。

 reticulateパッケージは、RStudio社がRに対応していないTensorFlowをPythonから呼ぶためにtensorflowパッケージ内で実装していた変換部分(libpython)を中心に別パッケージにしたという認識です。tensorflow::importでRからgensimを使う記事を書いてもいますが、0.5から0.6になるタイミングでtensorflowパッケージのimportはreticulateに移行しています。

 RとPython間を扱うための様々な関数が用意しており、RからPythonを活用するためのパッケージの決定版と言えそうです。まだ不安定な部分や癖がありますが、手懐けられればできる幅は広がるでしょう。

 公式ページにはすでに有用な情報が豊富にあり、本記事でも参考にしていますのでreticulateパッケージに興味が出たらぜひともお読みください。


reticulateを使う

 Rユーザーがreticulateパッケージを使う目的は次の2種類に分類できるでしょう。


  • PythonをRで書きたい(RでPythonを扱う)

  • Pythonで書かれたライブラリをRで利用したい(すでにあるライブラリ・関数をRで利用する)

 主な用途は後者と想定されます。これは使いたいPythonライブラリをインポートして、すでに用意できたデータを関数に当てはめるだけならそれほど難しくありません。


基本手順

 あらかじめ準備が必要になりますので、後述の「事前準備」の章を参考に設定しておきましょう。

 事前準備を済ませてreticulateパッケージでPythonが呼び出せる状態にできたら、次のコードのようにインポートしたPythonオブジェクトのクラスやその中の関数などに$演算子を用いてアクセスします。これが基本的な手順になります。


reticulateパッケージの基本的な利用手順

library(tidyverse)

library(reticulate)

# 使用するPythonを指定
reticulate::use_python(
python = reticulate:::python_unix_binary(bin = "python3.6"),
required = TRUE
)

# Pythonライブラリのインポート
pd <- reticulate::import(module = "pandas")
datasets <- reticulate::import(module = "sklearn.datasets")

# scikit-learnのデータセットからirisデータをロード
sklearn_iris <- datasets$load_iris()

# データセットをPandasデータフレームに変換
# import関数のデフォルト設定では対応するRオブジェクト(この場合はデータフレーム)に自動的に変換
pd_sklearn_iris <- pd$DataFrame(
data = sklearn_iris$data, columns = sklearn_iris$feature_names
)
# Pandasデータフレームに変換し直してPandasのheadメソッドで一部表示
reticulate::r_to_py(x = pd_sklearn_iris)$head(n = 5L)
#> sepal length (cm) ... petal width (cm)
#> 0 5.1 ... 0.2
#> 1 4.9 ... 0.2
#> 2 4.7 ... 0.2
#> 3 4.6 ... 0.2
#> 4 5.0 ... 0.2
#>
#> [5 rows x 4 columns]


 このくらいの簡単な処理なら、RとPython間のデータ型の変換やreticulateパッケージで定義されている関数などを詳しく知らなくてもこなせるでしょう。

 しかしながら、適用したいPython関数の入力として適した形にデータを適宜変換したり、得られた結果のPythonオブジェクトをRに戻すという処理を繰り返すなら、RでPythonを扱う方法を知っておくことをおすすめします。

 

 以降の節では「RでPythonを扱う方法」と、「既存のPython関数・ライブラリをRで利用する」に分けて説明します。

 読み通すことを推奨しますが、長くなってしまったため必要と興味に応じて読み進めるといいでしょう。


  • 事前設定をしていなくてこれからするという方

     事前準備


  • Pythonライブラリの利用例から知りたい方

     ライブラリ利用例


  • 「変換や関数の使い方を知っておきたい・全体を通して見たい」という方

     そのまま進む


RでPythonを扱う

 PythonとRでは構文はもちろん、定義されているデータ構造や組み込み関数が違います。

 Rにはスカラー型はなく「長さが1のベクトル」と考えたり、Pythonでは行列やデータフレームはNumpyやPandasを用いて拡張したり、Rの「NULL」はPythonでは「None」に対応します。Pythonでよく用いられるイテレータ・ジェネレータも、Rではあまり使われません(iteratorsパッケージを用いたり、R6クラスによる定義などで実装は可能)。

 これらの違いをよしなに扱う処理や関数群がreticulateパッケージで提供されています。本節ではそれらの関数を対応するオブジェクトに実行した結果を提示しながら、挙動を確かめます。


reticulateにおけるPythonオブジェクトの扱い方

 reticulateパッケージでRオブジェクトを入力としてPythonを呼び出すと、入力されたRオブジェクトは次の表の通りに対応するPythonのデータ型・データ構造に自動的に変換されます(Python側からR側に値を返す際も同様。こちらの表は公式サイトより)。

R
Python
Examples

1要素のベクトル
スカラー(Scalar)

1, 1L, TRUE, "foo"

多要素のベクトル
リスト(List)

c(1.0, 2.0, 3.0), c(1L, 2L, 3L)

複数の型を含むリスト
タプル(Tuple)

(1L, TRUE, "foo"), tuple(1, 2)

名前付きリスト
辞書(Dict)

list(a = 1L, b = 2.0), dict(x = x_data)

行列/配列
Numpyの配列(ndarray)
matrix(c(1,2,3,4), nrow = 2, ncol = 2)

データフレーム
Pandasのデータフレーム
data.frame(x = c(1,2,3), y = c("a", "b", "c"))

関数
Pythonの関数
function(x) x + 1

バイト型(raw)
bytearray
as.raw(c(1:10))

NULL, TRUE, FALSE
None, True, False

NULL, TRUE, FALSE

 このようにR-Python間でオブジェクトは自動変換されますが、Python側に期待通りのオブジェクトを与えて処理させるためにR側で明示的に変換させたい場合もあります。そのような要求を満たすには、reticulateパッケージで定義されている関数を用いると良いでしょう。

 r_to_py()は「RオブジェクトをPythonへ」、py_to_r()は「PythonオブジェクトをRへ」、相互変換するreticulateパッケージの関数です。これらを用いて動作例を示します。


リストとタプル

 変換対応を示した表にある通り、Pythonにおける「リスト(List)」はRではベクトルに相当します。

 前述の通り、Rにはデータ型としてスカラーがないため、Pythonのリストを入力すべきPythonの関数に「長さが1のベクトル」を渡すとPython側でスカラーに変換され、リストを想定していた後続処理ができない場合もあります。


Pythonリスト型の変換について

# 1要素のベクトルはスカラーに変換

(py_scalar <- reticulate::r_to_py(x = c(2)))
#> 2.0
class(x = py_scalar)
#> [1] "python.builtin.float" "python.builtin.object"

# 多要素のベクトルはリストに変換
# Python側で整数値にしたい場合は入力数値に「L」を付けて明示的に整数型(baseのNumericConstantsのヘルプを参照のこと)
(py_list_1 <- reticulate::r_to_py(x = c(list(2), 2L, 5)))
#> [2.0, 2, 5.0]
class(x = py_list_1)
#> [1] "python.builtin.list" "python.builtin.object"

# 1要素のリストを渡すとリストに変換
(py_list_2 <- reticulate::r_to_py(x = list(2)))
#> [2.0]
class(x = py_list_2)
#> [1] "python.builtin.list" "python.builtin.object"

# py_lenはPythonオブジェクトの長さを返す関数(Pythonにおけるlen関数に相当)
# Pythonのリストが入力なら長さを返す
reticulate::py_len(x = py_list_1)
#> [1] 3
reticulate::py_len(x = py_list_2)
#> [1] 1

# Pythonのスカラーが入力だとエラーを返す
reticulate::py_len(x = py_scalar)
#> Error in py_get_attr_impl(x, name, silent): AttributeError: 'float' object has no attribute '__len__'

reticulate::py_has_attr(x = py_scalar, name = "__len__")
#> [1] FALSE
reticulate::py_has_attr(x = py_list_2, name = "__len__")
#> [1] TRUE

# Rのベクトルと同様に参照できますが、Rと異なり添字は0オリジン
print(py_list_1[0])
#> 2.0
print(py_list_1[3])
#> Error in py_call_impl(callable, dots$args, dots$keywords): IndexError: list index out of range

# py_[set|get|del]_itemで変更・参照・削除ができる
reticulate::py_set_item(x = py_list_1, name = 0L, value = 0L)
reticulate::py_get_item(x = py_list_1, key = 0L)
#> 0
reticulate::py_del_item(x = py_list_1, name = 0L)

# リストオブジェクトのメソッドで操作可能
reticulate::py_has_attr(x = py_list_1, name = "reverse")
#> [1] TRUE
print(py_list_1)
#> [2, 5.0]
py_list_1$reverse()
#> None
print(py_list_1)
#> [5.0, 2]

# py_unicode関数はPython3ではユニコードオブジェクト(Python2では8ビットの文字列オブジェクト)に変換
# 多要素の文字列ベクトルを渡すことでPythonのリストを生成できる
(py_uni_list_1 <- reticulate::py_unicode(str = c("あ", "あいう")))
#> ['あ', 'あいう']
print(py_uni_list_1[1][1])
#> い

# 1要素の文字列ベクトルはスカラーが生成
reticulate::py_unicode(str = c("あいう"))
#> あいう


 タプル(Tuple)は変更不可(immutable)なPythonのリストです。要素の追加・変更・削除ができないオブジェクトにできるため、定数定義や後述の辞書のキーなどに用いるそうです。

 公式ドキュメントでは「複数の型を含むリスト(List of multiple types)」が変換されるとありますが、Pythonのタプルを変換するとR側では「複数の型を含むリスト」になりますが、逆ができないようです。tuple()でこなせますので、現時点ではこちらを使うのが良いでしょう。


Pythonタプル型の変換について

# 「複数の型を含むリスト」を変換

(py_tuple_1 <- reticulate::r_to_py(x = list(1, "a", TRUE)))
#> [1.0, 'a', True]
class(x = py_tuple_1)
#> [1] "python.builtin.list" "python.builtin.object"

# tuple関数を利用して作成
(py_tuple_2 <- reticulate::tuple(1, "a", TRUE))
#> (1.0, 'a', True)
class(x = py_tuple_2)
#> [1] "python.builtin.tuple" "python.builtin.object"

# タプルをRオブジェクトに変換すると「複数の型を含むリスト」になる
reticulate::py_to_r(x = py_tuple_2)
#> [[1]]
#> [1] 1
#>
#> [[2]]
#> [1] "a"
#>
#> [[3]]
#> [1] TRUE

# Pythonのリストと同様に添字で参照できますが、変更・削除はできない
print(py_tuple_2[0])
#> 1.0
reticulate::py_get_item(x = py_tuple_2, key = 0L)
#> 1.0
reticulate::py_set_item(x = py_tuple_2, name = 0L, value = 0L)
#> Error: Python object has no '__setitem__' method
reticulate::py_del_item(x = py_tuple_2, name = 0L)
#> Error: Python object has no '__delitem__' method



辞書

 Pythonにおける「辞書」とはキーと値のペアからなるオブジェクトです(他の言語では「連想配列」や「ハッシュ」と呼ばれます)。Rの名前付きリストに相当し自動変換されますが、dict()で明示的に作成できます。

 Rでは馴染みがないかもしれませんが(下記リンクにRでの実現例)、単語の出現頻度を素性に用いる言語処理では単語と頻度を対にしたハッシュテーブルはたびたび用いられます。


Python辞書型の変換について

# 「名前付きリスト」の変換

(dict_a <- reticulate::r_to_py(x = list(a = 1, b = 2)))
#> {'a': 1.0, 'b': 2.0}

# dict関数を利用して作成(キーと値のペアを渡す)
(dict_b <- reticulate::dict(a = 1, b = 2))
#> {'a': 1.0, 'b': 2.0}
# py_dict関数を利用して作成(キーと値を個々のベクトルで渡す)
(dict_c <- reticulate::py_dict(key = c("a", "b"), values = c(1, 2)))
#> {'a': 1.0, 'b': 2.0}

class(x = dict_a)
#> [1] "python.builtin.dict" "python.builtin.object"

# Pythonのリスト同様、Pythonオブジェクトを受け取って結果を返す関数を適用できる
reticulate::py_len(x = dict_b)
#> [1] 2
reticulate::py_str(object = dict_b)
#> [1] "{'a': 1.0, 'b': 2.0}"

# 他のPythonオブジェクト同様に添字操作できる
print(dict_b$a)
#> 1.0
dict_b["a"] <- 3

# dictオブジェクトのメソッドからもアクセス可能
reticulate::py_has_attr(x = dict_b, name = "get")
#> [1] TRUE
dict_b$get("a")
#> 3.0

reticulate::py_has_attr(x = dict_b, name = "keys")
#> [1] TRUE
dict_b$keys()
#> ['a', 'b']



配列とデータフレーム

 Rにおける行列・配列はNumpyを用いたN次元配列が対応します。このとき、入力したRオブジェクトの次元数によってPythonにおける配列の形状が変わります。np_array()で明示的に作成でき、ベクトルに対してnp_array()で1次元配列に変換することも可能です。

 配列に関しては、RとPython間でデータの持たせ方や表示が異なって困惑する場合もあります。知っておいた方が良い点や注意事項などが公式ページで詳しく書いてありますので、読んでおくといいでしょう。


Numpy配列の変換について

print(d <- seq_len(length.out = 12))

#> [1] 1 2 3 4 5 6 7 8 9 10 11 12

# ベクトルはPythonのリストオブジェクトに変換
reticulate::r_to_py(x = d)
#> List (12 items)

# np_array関数にベクトルを入力すると1次元配列
(py_vec_arr <- reticulate::np_array(data = d))
#> [ 1 2 3 4 5 6 7 8 9 10 11 12]
reticulate::py_len(x = py_vec_arr)
#> [1] 12

# 添字操作
print(py_vec_arr[4])
#> 5
print(d[5])
#> [1] 5

# メソッド
print(py_vec_arr$shape)
#> (12,)
print(py_vec_arr$size)
#> 12

# Pythonの1次元配列はRのベクトルに戻る
reticulate::py_to_r(x = py_vec_arr)
#> [1] 1 2 3 4 5 6 7 8 9 10 11 12

# 行列はPython(Numpy)の2次元配列に変換
print(m <- matrix(data = d, nrow = 3, ncol = 4, byrow = TRUE))
#> [,1] [,2] [,3] [,4]
#> [1,] 1 2 3 4
#> [2,] 5 6 7 8
#> [3,] 9 10 11 12
reticulate::r_to_py(x = m)
#> [[ 1 2 3 4]
#> [ 5 6 7 8]
#> [ 9 10 11 12]]

# np_arrayに行列を入力しても2次元配列
(py_mat_arr <- reticulate::np_array(data = m))
#> [[ 1 2 3 4]
#> [ 5 6 7 8]
#> [ 9 10 11 12]]
reticulate::py_len(x = py_mat_arr)
#> [1] 3

# 添字操作
print(py_mat_arr[2][0])
#> 9
print(m[3, 1])
#> [1] 9

# Pythonにおけるスライス演算子(:)はRのコロン演算子(:)として用いられる
# R側でベクトル生成できない記述のスライシングは対応していない
# OK
print(py_mat_arr[0L:1])
#> [[1 2 3 4]
#> [5 6 7 8]]
# NG
# print(try(expr = py_mat_arr[0:], silent = TRUE))

# 後述するPython組み込み関数で試してみる
py_mat_arr$`__getitem__`(reticulate::import_builtins()$slice(0L, 2L, 1L))
#> [[1 2 3 4]
#> [5 6 7 8]]

# メソッド
print(py_mat_arr$shape)
#> (3, 4)
print(py_mat_arr$size)
#> 12

# Pythonの2次元配列はRの行列に戻る
reticulate::py_to_r(x = py_mat_arr)
#> [,1] [,2] [,3] [,4]
#> [1,] 1 2 3 4
#> [2,] 5 6 7 8
#> [3,] 9 10 11 12

# 配列はPython(Numpy)のRオブジェクトの次元数に応じたN次元配列に変換
print(arr_1 <- array(data = d, dim = c(3, 2, 2)))
#> , , 1
#>
#> [,1] [,2]
#> [1,] 1 4
#> [2,] 2 5
#> [3,] 3 6
#>
#> , , 2
#>
#> [,1] [,2]
#> [1,] 7 10
#> [2,] 8 11
#> [3,] 9 12
reticulate::r_to_py(x = arr_1)
#> [[[ 1 7]
#> [ 4 10]]
#>
#> [[ 2 8]
#> [ 5 11]]
#>
#> [[ 3 9]
#> [ 6 12]]]

# np_array関数に配列を入力しても同様
(py_arr1_arr <- reticulate::np_array(data = arr_1))
#> [[[ 1 7]
#> [ 4 10]]
#>
#> [[ 2 8]
#> [ 5 11]]
#>
#> [[ 3 9]
#> [ 6 12]]]
reticulate::py_len(x = py_arr1_arr)
#> [1] 3

# 添字操作
print(py_arr1_arr[0][0][1])
#> 7
print(arr_1[1, 1, 2])
#> [1] 7

# メソッド
print(py_arr1_arr$shape)
#> (3, 2, 2)
print(py_arr1_arr$size)
#> 12

# 入力Rオブジェクトの次元数にPython側の形状も対応
# 表示形式は異なるが同じデータ
print(arr_2 <- array(data = d, dim = c(2, 1, 3, 2)))
#> , , 1, 1
#>
#> [,1]
#> [1,] 1
#> [2,] 2
#>
#> , , 2, 1
#>
#> [,1]
#> [1,] 3
#> [2,] 4
#>
#> , , 3, 1
#>
#> [,1]
#> [1,] 5
#> [2,] 6
#>
#> , , 1, 2
#>
#> [,1]
#> [1,] 7
#> [2,] 8
#>
#> , , 2, 2
#>
#> [,1]
#> [1,] 9
#> [2,] 10
#>
#> , , 3, 2
#>
#> [,1]
#> [1,] 11
#> [2,] 12
(py_arr2_arr <- reticulate::np_array(data = arr_2))
#> [[[[ 1 7]
#> [ 3 9]
#> [ 5 11]]]
#>
#>
#> [[[ 2 8]
#> [ 4 10]
#> [ 6 12]]]]
print(py_arr2_arr$shape)
#> (2, 1, 3, 2)

# 添字操作で指定したデータは同じ
print(py_arr2_arr[0][0][1])
#> [3 9]
print(arr_2[1, 1, 2, , drop = TRUE])
#> [1] 3 9
# Rでは「drop = FALSE」の見え方がデフォルト
print(arr_2[1, 1, 2, , drop = FALSE])
#> , , 1, 1
#>
#> [,1]
#> [1,] 3
#>
#> , , 1, 2
#>
#> [,1]
#> [1,] 9

# array_reshape関数で形状を変えることもできる
# このときのデータを並べ直すときの方向は行(order = "C")がデフォルト
print(py_mat_arr)
#> [[ 1 2 3 4]
#> [ 5 6 7 8]
#> [ 9 10 11 12]]
reticulate::array_reshape(x = py_mat_arr, dim = c(4, 3))
#> [[ 1 2 3]
#> [ 4 5 6]
#> [ 7 8 9]
#> [10 11 12]]
reticulate::array_reshape(x = py_mat_arr, dim = c(4, 3), order = "F")
#> [[ 1 6 11]
#> [ 5 10 4]
#> [ 9 3 8]
#> [ 2 7 12]]


 RにおけるデータフレームはPandasを用いたデータフレームが対応します。このとき、数値や文字列などの型はNumpyの配列(ndarray)に変換するときと同じルールに基づいていますが、因子(factor)とPOSIXct型については以下の対応がされています。

R
Python

Factor
NumPyのCategory型

POSIXt
NumPyの配列(dtype = datetime64[ns])

 このように個別処理されますが、変換は現状あまりうまくできていないように思えます(こちらは今後に期待しましょう)。


Pandasデータフレームの変換について

N <- 3

dt <- seq.POSIXt(
from = lubridate::ymd_hms("2017-04-01 09:00:00", tz = "Asia/Tokyo"),
to = lubridate::ymd_hms("2017-04-30 09:00:00", tz = "Asia/Tokyo"),
by = "day"
)
df <- data.frame(d = sample(x = dt, size = N), x = seq_len(length.out = N)) %>%
dplyr::mutate(y = LETTERS[.$x], z = factor(x = letters[.$x])) %>%
print()
#> d x y z
#> 1 2017-04-12 09:00:00 1 A a
#> 2 2017-04-27 09:00:00 2 B b
#> 3 2017-04-06 09:00:00 3 C c

# データフレームはPandasのデータフレームに変換
(py_df <- reticulate::r_to_py(x = df))
class(x = py_df)
#> [1] "pandas.core.frame.DataFrame" "pandas.core.generic.NDFrame"
#> [3] "pandas.core.base.PandasObject" "pandas.core.base.StringMixin"
#> [5] "pandas.core.base.SelectionMixin" "python.builtin.object"

print(py_df["x"])
#> 0 1
#> 1 2
#> 2 3
#> Name: x, dtype: int32
print(py_df["y"])
#> 0 A
#> 1 B
#> 2 C
#> Name: y, dtype: object
print(py_df["z"])
#> 0 a
#> 1 b
#> 2 c
#> Name: z, dtype: category
#> Categories (3, object): [a, b, c]

# 元々あったタイムゾーンはなくなる
# https://github.com/rstudio/reticulate/issues/325
print(py_df["d"])
#> 0 2017-04-24
#> 1 2017-04-10
#> 2 2017-04-02
#> Name: d, dtype: datetime64[ns]

# Pandasのメソッドは実行できる
py_df$sample(n = 1L)
#> d x y z
#> 1 2017-04-10 2 B b

# PandasのデータフレームをR側に戻すと因子型の列は変換されないまま
(df_from_pydf <- reticulate::py_to_r(x = py_df))
#> d x y z
#> 1 2017-04-24 1 A <environment: 0x7fe8eb240c78>
#> 2 2017-04-10 2 B <NA>
#> 3 2017-04-02 3 C <NA>
#> 警告メッセージ:
#> format.data.frame(x, digits = digits, na.encode = FALSE) で:
#> 壊れたデータフレームです。列が切り詰められるか、または NA 値で埋められます
print(df_from_pydf$z)
#> [a, b, c]
#> Categories (3, object): [a, b, c]

# タイムゾーンはUTCに変更されている
print(df_from_pydf$d)
#> [1] "2017-04-24 UTC" "2017-04-10 UTC" "2017-04-02 UTC"



疎行列

 「文書を行」に「単語を列」に単語出現頻度を値とした文書単語行列(単語と文書を転置した単語文書行列でも)は言語処理で用いられますが、単語の出現頻度はドキュメントで偏りがあるために0が非常に多い行列になります(単語数だけの次元を確保するためにサイズが巨大な行列にもなりやすい)。

 このような値がなくてスカスカな行列は疎行列と呼ばれ、通常と同じくデータを持つと大量にメモリを消費する上に演算時間もかかります。そのため、疎行列を適した形に圧縮して扱う必要があります。

 

 Rで疎行列を扱うにはSparseM/Matrixパッケージがあります。このうちMatrixパッケージはR自体と合わせてインストールされます(ただし、baseやgraphicsなどと異なり使用時には呼び出す必要があります。このような「Recommended Packages」はR-3.5.1では

こちらのリンク先にあるパッケージが対象になります)。

 

 reticulateパッケージを用いると、Matrixパッケージによる疎行列がScipyライブラリによるCSC(Compressed Sparse Columns)形式の疎行列に変換されます。Scipyが扱う疎行列の内部構造に関しては以下のサイトなどをご参考ください。


疎行列の変換について

library(Matrix)

set.seed(seed = 123)
base_mat <- matrix(
data = sample(x = c(0, 1L), size = 50000, replace = TRUE, prob = c(0.9, 0.1)),
nrow = 50, ncol = 1000, byrow = TRUE
)
# 通常の行列を疎行列に変換
dgc_mat_1 <- Matrix::Matrix(data = base_mat)
class(x = dgc_mat_1)
#> [1] "dgCMatrix"
#> attr(,"package")
#> [1] "Matrix"

# 一致
all(base_mat == dgc_mat_1)
#> [1] TRUE

# オブジェクトサイズを比較
pryr::object_size(dgc_mat_1)
#> 64.1 kB
pryr::object_size(base_mat)
#> 400 kB

# 疎行列はScipyによるCSC形式の疎行列に変換
(py_csc_mat <- reticulate::r_to_py(x = dgc_mat_1))
#> (7, 0) 1.0
#> (18, 0) 1.0
#> (20, 0) 1.0
#> (38, 0) 1.0
#> (41, 0) 1.0
#> (3, 1) 1.0
#> (5, 1) 1.0
#> (40, 1) 1.0
#> (16, 2) 1.0
#> (25, 2) 1.0
#> (39, 2) 1.0
#> (43, 2) 1.0
#> (48, 2) 1.0
#> (11, 3) 1.0
#> (12, 3) 1.0
#> (15, 3) 1.0
#> (26, 3) 1.0
#> (31, 3) 1.0
#> (32, 3) 1.0
#> (37, 3) 1.0
#> (41, 3) 1.0
#> (44, 3) 1.0
#> (0, 4) 1.0
#> (9, 4) 1.0
#> (13, 4) 1.0
#> : :
#> (30, 994) 1.0
#> (46, 994) 1.0
#> (32, 995) 1.0
#> (4, 996) 1.0
#> (24, 996) 1.0
#> (39, 996) 1.0
#> (42, 996) 1.0
#> (47, 996) 1.0
#> (5, 997) 1.0
#> (8, 997) 1.0
#> (24, 997) 1.0
#> (30, 997) 1.0
#> (37, 997) 1.0
#> (44, 997) 1.0
#> (45, 997) 1.0
#> (49, 997) 1.0
#> (6, 998) 1.0
#> (45, 998) 1.0
#> (5, 999) 1.0
#> (9, 999) 1.0
#> (15, 999) 1.0
#> (16, 999) 1.0
#> (33, 999) 1.0
#> (40, 999) 1.0
#> (44, 999) 1.0
class(x = py_csc_mat)
#> [1] "scipy.sparse.csc.csc_matrix"
#> [2] "scipy.sparse.compressed._cs_matrix"
#> [3] "scipy.sparse.data._data_matrix"
#> [4] "scipy.sparse.base.spmatrix"
#> [5] "scipy.sparse.data._minmax_mixin"
#> [6] "scipy.sparse.sputils.IndexMixin"
#> [7] "python.builtin.object"

# Pythonオブジェクトから戻すと同じ結果になる
reticulate::py_to_r(x = py_csc_mat)
#> 50 x 1000 sparse Matrix of class "dgCMatrix"

#> [1,] . . . . 1 . . . . . 1 . . . . . . . . 1 . . . 1 . . . . . . 1 1 . . . . . . . . . . . . . . . . ......
#> [2,] . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ......
#> [3,] . . . . . . . . . . . . . . 1 . . . . . 1 . 1 . . . . . . . . . . . . . . 1 1 . . . . . 1 1 . . ......
#> [4,] . 1 . . . . . . . . 1 . . . . . . . . . . . . . . . . 1 . 1 . . . . . . . . . . . . . . . . . . ......
#> [5,] . . . . . . . . . 1 . 1 . . . 1 1 . . . . . . . . . . . . . . . . . . 1 . . . . . . . . 1 . . . ......
#> [6,] . 1 . . . . . . . . . 1 . 1 . 1 . . 1 . 1 1 . . . . . . . . . . . . . . . . . . . . 1 1 . . . . ......
#> [7,] . . . . . . . . 1 . . . . . . 1 . . . 1 1 . . . . 1 . . . . 1 . . . 1 . . . . . . . . . 1 . . . ......
#> [8,] 1 . . . . . . . . . . . . 1 . . . 1 1 1 . . . . . 1 . . . . . . . . . . . . . . . . . . . . 1 . ......
#> [9,] . . . . . . . . . . 1 . . . . . . . . . 1 1 1 . . . . . . . . 1 . . . . . . . 1 1 1 . . . . . . ......
#> [10,] . . . . 1 . . . . . . . . . . . . . . . . . . . . . . . 1 . . . . . . . . . . . . . . . . . . . ......

#> ......................................suppressing columns and rows in show(); maybe adjust 'options(max.print= *, width = #> *)'
#> ..............................

#> [41,] . 1 . . . . . . . . . . . . 1 . . . . . . . . . . . . . 1 . 1 . . 1 . . . . . . . . . . . . . . ......
#> [42,] 1 . . 1 . . . . . 1 . 1 . . 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 . . . 1 ......
#> [43,] . . . . . . . . . . . . . . . 1 . . . 1 . . . . . . . . . 1 . . . . . . . . . . . . . . . . . . ......
#> [44,] . . 1 . . . . . 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 . . . . . . . . ......
#> [45,] . . . 1 . . . . . . . . . . . . . . . 1 . . . . . . . . . . . . . 1 1 . . . . . . . 1 . . . . . ......
#> [46,] . . . . . . . . 1 . . 1 . . . . . . . . . . 1 1 . . . . . . . . . 1 . . . . . . . . . . . . 1 . ......
#> [47,] . . . . . . . . . . . . . . . . . . . 1 . . . . . . . . . . . . . . . . . . . . 1 . . . . . . . ......
#> [48,] . . . . . . . . . . . . . . . . . 1 . . . . . 1 . . . . . . . . . . . . . . . . . 1 . . . . . . ......
#> [49,] . . 1 . . . . . . . 1 . 1 . . . . . . . . . . . . . . . . . 1 . . . . . . . . . . . . . . 1 1 . ......
#> [50,] . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 . . . . . . . 1 . . . 1 . 1 ......

# 値がある行(i)と列(j)の位置を指定し、その位置にする値(x)を指定しても疎行列は作成できる
ROW_N <- 3
COL_N <- 10

i_idx <- sample(x = ROW_N, size = COL_N * ROW_N, replace = TRUE)
j_idx <- sample(x = COL_N, size = COL_N * ROW_N, replace = TRUE)
ij_vl <- runif(n = COL_N * ROW_N)
dgc_mat_2 <- Matrix::sparseMatrix(i = i_idx, j = j_idx, x = ij_vl)

# 指定番目のデータが、疎行列に対応しているか確認
varidate_idx <- 3

# 指定番目のデータ
i_idx[varidate_idx]
#> [1] 1
j_idx[varidate_idx]
#> [1] 4
ij_vl[varidate_idx]
#> [1] 0.2238414

# 疎行列で添字指定
dgc_mat_2[i_idx[varidate_idx], j_idx[varidate_idx]]
#> [1] 0.2238414

# Python側での行と列の添字指定はうまくいかない(やり方が悪いだけ?)
py_csc_mat_2 <- reticulate::r_to_py(x = dgc_mat_2)
py_csc_mat_2[i_idx[varidate_idx] - 1L, j_idx[varidate_idx] - 1]
#> Error in `[.python.builtin.object`(py_csc_mat_2, i_idx[varidate_idx] - : 使われていない引数 (j_idx[varidate_idx] - 1)

# Python側での行指定はうまくいく
py_csc_mat_2[i_idx[varidate_idx] - 1L]
#> (0, 2) 0.930494927103
#> (0, 3) 0.223841364961
#> (0, 4) 0.283874823246
#> (0, 5) 0.670918702846
#> (0, 6) 0.586554164998
#> (0, 9) 0.593989548041
# メソッドでの列指定は可能
py_csc_mat_2$getcol(i = j_idx[varidate_idx] - 1L)
#> (0, 0) 0.223841364961
#> (1, 0) 0.228133548284
#> (2, 0) 1.31150214281



関数

 Rで定義した関数をラップし、Python側で関数定義して実行できる形にするにはpy_func()を用います。このとき、定義するRの関数にはPythonと互換性がない構文は含められません。

 複雑な関数の定義は向かず、今のところ私は用途が思い当たりませんが、いいアイデアがあれば教えて欲しいです。

 なお、Python関数を事前に定義したファイルから読み込んだり自前で書いて実行したりして、Rで利用する方法は別節にて扱います。


関数の変換について

func <- function (a, b = 1.5) { a + b }

rapped_func <- reticulate::py_func(f = func)
class(x = rapped_func)
#> [1] "python.builtin.function" "python.builtin.object"

# 引数を与えて実行できる
rapped_func(a = 5)
#> [1] 6.5
# py_callでも呼び出し可能
reticulate::py_call(x = rapped_func, a = 1, b = 2)
#> 3.0



Pythonを書く

 R-Python間の変換ルールとオブジェクトの扱い方についてわかってきたと思いますが、イテレータやライブラリ固有の型など、Rにそのままでは変換できない結果をPython関数が返す場合は数多くあります。

 Python実行結果をRに変換できるオブジェクトにしていくためにも、Python組み込み関数やPythonでよく使われる技法を知っておくことは重要でしょう。


Python組み込み関数を使う

 Python組み込みの関数群にRからアクセスするにはimport_builtins()を使用します。import_builtins()の中では「"builtins"をimport()の引数に与えた処理」を行っているので、import()と同様に$演算子で関数が利用できます。実際にPython組み込み関数をいくつか用いてみましょう。


RからPython組み込み関数を使う

# 組み込み関数をロード

py_builtin <- reticulate::import_builtins()

# range関数を使う
py_range <- py_builtin$range(1L, 5L, 1L)

# メソッド呼び出し(呼び出し時に与えた引数を返す)
print(py_range$start)
#> [1] 1
print(py_range$stop)
#> [1] 5
print(py_range$step)
#> [1] 1

# Pythonのイテレータオブジェクトを扱わせる関数をRで定義
itPyBuiltin <- function (py_it_obj){
iter <- py_builtin$iter(py_it_obj)
while (TRUE) {
# iter_nextで次の要素を取り出す
obj <- reticulate::iter_next(it = iter)
if (is.null(x = obj)) {
break
}
print(obj)
}
}

itPyBuiltin(py_it_obj = py_range)
#> [1] 1
#> [1] 2
#> [1] 3
#> [1] 4

# 同種の関数は同様に呼び出せる
# Pythonの呼び出し可能な関数はreticulate::py_callでも呼び出せる
py_zip <- reticulate::py_call(x = py_builtin$zip, c(1, 2, 3), c("a", "b", "c"))
itPyBuiltin(py_it_obj = py_builtin$iter(py_zip))
#> [[1]]
#> [1] 1
#>
#> [[2]]
#> [1] "a"
#>
#> [[1]]
#> [1] 2
#>
#> [[2]]
#> [1] "b"
#>
#> [[1]]
#> [1] 3
#>
#> [[2]]
#> [1] "c"

py_enumerate <- reticulate::py_call(
x = py_builtin$enumerate,
reticulate::py_dict(key = c("a", "b"), values = c(1, 2))
)
# 組み込み関数のiterではなく「__iter__」でも可
itPyBuiltin(py_it_obj = py_enumerate$`__iter__`())
#> [[1]]
#> [1] 0
#>
#> [[2]]
#> [1] "a"
#>
#> [[1]]
#> [1] 1
#>
#> [[2]]
#> [1] "b"



With句

 Rの総称関数(generic function)のひとつであるwithを使うことで、Pythonと同じようにコンテキストマネージャオブジェクトを操作できます。こちらでもPythonの組み込み関数を組み合わせられます。


RでPythonのコンテキストマネージャを書く

output_file_name <- "output.txt"

with(
# `%as%`はreticulateパッケージで定義されている演算子
data = py_builtin$open(output_file_name, "w") %as% file,
expr = {
file$write("Hello, there!")
}
)
#> [1] 13

readr::read_file(file = "output.txt")
#> [1] "Hello, there!"



イテレータ

 先ほどのPython組み込み関数をRで利用するときに出てきましたが、Rではあまり使われないイテレータはPythonでしばしば用いられます。イテレータにより、あらかじめ大きいデータを生成せずにループ処理を実行したり、無限に呼び出せるオブジェクトを生成することなどができます。

 reticulateパッケージにはPythonイテレータオブジェクトを扱う関数も用意されており、先のように自前で定義しなくても良い場合もあります。ここではreticulateパッケージでPythonイテレータオブジェクトを処理する例と合わせ、R内で実現する例も提示します。


RでPythonイテレータを使う

# Pythonイテレータオブジェクトに対してiterateを適用すると、各要素に引数で指定したR関数が実行可能

reticulate::iterate(it = py_builtin$range(3L), f = print)
#> [1] 0
#> [1] 1
#> [1] 2

# 関数を指定しなければまとめられた結果を返す
iter_res_1 <- reticulate::iterate(it = py_builtin$range(3L)) %>% print()
#> [1] 0 1 2

# 自作関数も渡せる
iter_res_2 <- reticulate::iterate(
it = reticulate::py_call(x = py_builtin$zip, c(1, 2, 3), c(4, 5, 6)),
f = function (x) {
return(as.integer(x = (x[[1]] * x[[2]])))
}
) %>% print()
#> [1] 4 10 18

# iter_nextでそれぞれの要素を順々に取り出して処理できる
py_zip <- reticulate::py_call(x = py_builtin$zip, c(1, 2), c("a", "b"))
# Error: iterator function called with non-iterator argument
class(x = py_zip)
#> [1] "python.builtin.list" "python.builtin.object"
# NG
reticulate::iter_next(it = py_zip)
#> Error: iterator function called with non-iterator argument
# OK
class(x = py_builtin$iter(py_zip))
#> [1] "python.builtin.iterator" "python.builtin.listiterator"
#> [3] "python.builtin.object"
reticulate::iter_next(it = py_builtin$iter(py_zip))
#> [[1]]
#> [1] 1
#>
#> [[2]]
#> [1] "a"
reticulate::iter_next(it = py_builtin$iter(py_zip))
#> [[1]]
#> [1] 1
#>
#> [[2]]
#> [1] "a"

# 取り出し終えるとデフォルトではNULLを返す(completedで指定した値に変更可)
reticulate::iter_next(it = py_builtin$iter(py_zip))
#> [[1]]
#> [1] 1
#>
#> [[2]]
#> [1] "a"
reticulate::iter_next(it = py_builtin$iter(py_zip), completed = NA)
#> [[1]]
#> [1] 1
#>
#> [[2]]
#> [1] "a"

# 入力引数から始まるジェネレータ関数
defSeqGenerator <- function(start) {
value <- start
function() {
value <<- value + 1
return(value)
}
}

# Rで定義した自作関数をpy_iteratorによってイテレータオブジェクトとして扱える
seq_generator <- defSeqGenerator(start = 5)
py_seq_generator <- reticulate::py_iterator(fn = seq_generator)
class(x = py_seq_generator)
#> [1] "python.builtin.iterator" "rpytools.generator.RGenerator"
#> [3] "python.builtin.object"

reticulate::iter_next(it = py_seq_generator)
#> [1] 6
reticulate::iter_next(it = py_seq_generator)
#> [1] 7
# 元の関数を呼び出しても、py_iteratorでラップした関数もカウントアップする
seq_generator()
#> [1] 8
reticulate::iter_next(it = py_seq_generator)
#> [1] 9


 Rでイテレータを扱うにはiteratorsパッケージを活用するのがよいでしょう。こちらとforeachパッケージを組み合わせることで、大規模なループ処理を実行するのに用いられます。

 Rにおけるイテレータに関する話は下記資料などをご参考に(イテレータについてはP20以降)。

 なお、Microsoft R Openは「additionalPackages」にdoParallelパッケージが含まれており、依存パッケージであるiteratorsパッケージも一緒にインストールされます。


Rでイテレータを使う

# 指定数までカウントアップするイテレータオブジェクトを生成

r_iter <- iterators::icount(count = 3)

# nextElemで呼び出す度にカウントアップ
iterators::nextElem(obj = r_iter)
#> [1] 1
iterators::nextElem(obj = r_iter)
#> [1] 2
iterators::nextElem(obj = r_iter)
#> [1] 3

# 指定数になるとError: StopIteration
iterators::nextElem(obj = r_iter)
#> Error: StopIteration

# イテレータを用いることでループ実行時にあらかじめ大きいデータを生成しないで済む
# foreachパッケージと組み合わせて利用される
r_iter <- iterators::irnorm(n = 100, mean = 0, sd = 1, count = 500)
ires <- foreach::foreach(i = r_iter, .combine = c) %do% {
return(mean(i))
}
mean(ires)
#> [1] 0.002603129

# iteratorsパッケージで作成したイテレータオブジェクトにnextElemが生成されている場合は、
# py_iteratorに渡してPythonイテレータオブジェクトとして扱うことも可能
r_iter <- iterators::icount(count = 3)
py_it <- reticulate::py_iterator(fn = r_iter$nextElem)
class(x = py_it)
#> [1] "python.builtin.iterator" "rpytools.generator.RGenerator"
#> [3] "python.builtin.object"
# ただし、StopIterationを扱う処理がないためにエラーが発生する
reticulate::iterate(it = py_it, f = print)
#> [1] 1
#> [1] 2
#> [1] 3
#> Error occurred in generator: StopIteration

# iteratorsパッケージの乱数生成やsplitも対応
r_norm_iter <- iterators::irnorm(n = 10, mean = 0, sd = 1, count = 5)
py_norm_it <- reticulate::py_iterator(fn = r_norm_iter$nextElem)
reticulate::iter_next(it = py_norm_it)
#> [1] 2.551291335 0.008213067 -0.451395215 -0.709668545 -1.465336271
#> [6] 0.080361076 0.879780379 -0.118462566 -0.642798338 -0.213771716

# Iterator Factory Functionsは関数をラップして渡せる場合もある
wrapIterFactory <- function (it_obj){
return(
function () {
iterators::nextElem(obj = it_obj)
}
)
}

# OK
i1 <- iterators::iter(seq_len(length.out = 3))
it1 <- reticulate::py_iterator(fn = wrapIterFactory(it_obj = i1))
reticulate::iter_next(it = it1)
#> [1] 1

# NG
i3 <- iterators::iter(data.frame(x = seq_len(length.out = 3), y = 10), by = "row")
it3 <- reticulate::py_iterator(fn = wrapIterFactory(it_obj = i3))
reticulate::iter_next(it = it3)
#> Error in py_iter_next(it, completed): TypeError: Could not compare [None] with block values



保存とロード

 reticulateパッケージで処理・作成されたPythonオブジェクトの保存と読み込みを行いたいときは、py_save_object()py_load_object()が使えます。これらの関数ではPython標準ライブラリであるpickleモジュールを用いて、ローカルファイルに対してPythonオブジェクトの管理をします。


Pythonオブジェクトを保存・読み込む

pd <- reticulate::import(module = "pandas", convert = FALSE)

sklearn_iris <- reticulate::import(module = "sklearn.datasets")$load_iris()
pd_sklearn_iris <- pd$DataFrame(
data = sklearn_iris$data, columns = sklearn_iris$feature_names
)
class(x = pd_sklearn_iris)
#> [1] "pandas.core.frame.DataFrame" "pandas.core.generic.NDFrame"
#> [3] "pandas.core.base.PandasObject" "pandas.core.base.StringMixin"
#> [5] "pandas.core.base.SelectionMixin" "python.builtin.object"

# 保存
EXPORT_FILE_NAME <- "sklearn_iris.pkl"
reticulate::py_save_object(object = pd_sklearn_iris, filename = EXPORT_FILE_NAME)
rm(pd_sklearn_iris)

# 読み込み
print(pd_sklearn_iris)
#> Error in print(pd_sklearn_iris): オブジェクト 'pd_sklearn_iris' がありません
pd_sklearn_iris <- reticulate::py_load_object(filename = EXPORT_FILE_NAME)
# Rへ変換可能なオブジェクトは自動変換されてしまう
head(pd_sklearn_iris, n = 5)
#> sepal length (cm) sepal width (cm) petal length (cm) petal width (cm)
#> 1 5.1 3.5 1.4 0.2
#> 2 4.9 3.0 1.4 0.2
#> 3 4.7 3.2 1.3 0.2
#> 4 4.6 3.1 1.5 0.2
#> 5 5.0 3.6 1.4 0.2

# 自動変換させたくない場合はpy_load_object内でpickleをimportする処理を「convert = FALSE」にする
loadPyObj <- function (filename, pickle = "pickle", is_convert = TRUE) {
builtins <- reticulate::import_builtins()
pickle <- reticulate::import(module = pickle, convert = is_convert)
handle <- builtins$open(filename, "rb")
on.exit(handle$close(), add = TRUE)
pickle$load(handle)
}
loadPyObj(filename = EXPORT_FILE_NAME, is_convert = FALSE)$head(5L)
#> sepal length (cm) sepal width (cm) petal length (cm) petal width (cm)
#> 0 5.1 3.5 1.4 0.2
#> 1 4.9 3.0 1.4 0.2
#> 2 4.7 3.2 1.3 0.2
#> 3 4.6 3.1 1.5 0.2
#> 4 5.0 3.6 1.4 0.2


 保存したファイルはPythonからも読み込めます。

python3 -m pickle sklearn_iris.pkl | head -n 5

#> sepal length (cm) ... petal width (cm)
#> 0 5.1 ... 0.2
#> 1 4.9 ... 0.2
#> 2 4.7 ... 0.2
#> 3 4.6 ... 0.2


まとめ

 RとPython間のデータ型・データ構造などの違いを知り、reticulateパッケージで定義された各種関数で適宜扱うことで、RからPythonをインタラクティブに利用できるようになってきたと思います。

 しかしながら、変換対応していないオブジェクトへの四則演算(各演算子を関数に変換すれば対応できる場合も場合も)やPythonにおけるスライスの部分配列へのアクセス、Rが対応していない構文もあります(zeallotパッケージや演算子定義で対応可能なものもある)。

 もちろん知っておくべきこともまだありますが、実際に使って慣れていくのが良いでしょう。


既存のPython関数・ライブラリをRで活用する

 

 前節ではRでPythonを扱うためにreticulateパッケージの関数について挙動を試しましたが、元々の目的は「すでにあるPythonライブラリや関数などをRで利用すること」です。

 本節ではPythonコード・関数を評価・実行してR側で結果を得る方法に関して解説し、続いていくつかのライブラリについて簡単な利用例を記載することで活用手順を説明します。


Pythonコードの実行

 RでPythonコードを評価し結果を得るアプローチには、「文字列に書かれたPythonコードの実行」と「ファイルに書かれたPythonコードの実行」があります。前者は自前でPythonを書く機会は多くなるでしょうが、後者は使い方次第で結果だけを利用することも可能です。


文字列に書かれたPythonコードの実行

 Rの文字列として記述した簡素なPythonコードを評価し、結果を受け取るにはpy_eval()があります。この関数は書けるコード量が限られて変数定義もできないですが(あくまでも実行結果を受け取る用途と思われる)、同じように文字列をPythonコードとして実行するpy_run_string()と合わせたりすると有効に使えます(ただし、グローバル汚染には注意しましょう)。


RでPythonコードを評価して結果を得る

(a <- reticulate::py_eval(code = "1 + 5", convert = FALSE))

#> 6

# 定義されていない
reticulate::py_eval(code = "a", convert = FALSE)
#> Error in py_eval_impl(code, convert): NameError: name 'a' is not defined

# 変数代入もできない
reticulate::py_eval(code = "a = 1 + 5", convert = FALSE)
#> Error in py_eval_impl(code, convert): SyntaxError: invalid syntax (reticulate_eval, line 1)

# py_run_stringで「local = FALSE」を指定
main_syspath <- reticulate::py_run_string(
code = "import sys\nm_pth = sys.path", local = FALSE
)
class(x = main_syspath)
#> [1] "python.builtin.dict" "python.builtin.object"
# 結果をRからもpy_evalからも参照できる
stringr::str_subset(string = main_syspath$m_pth, "/usr/local/")
#> [1] "/usr/local/Cellar/python/3.6.5_1/Frameworks/Python.framework/Versions/3.6/bin"
#> [2] "/usr/local/opt/python/Frameworks/Python.framework/Versions/3.6/lib/python36.zip"
#> [3] "/usr/local/opt/python/Frameworks/Python.framework/Versions/3.6/lib/python3.6"
#> [4] "/usr/local/opt/python/Frameworks/Python.framework/Versions/3.6/lib/python3.6/lib-dynload"
#> [5] "/usr/local/opt/python/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages"
reticulate::py_eval(code = "m_pth[:2]", convert = FALSE)
#> ['/usr/bin', '/Library/Python/2.7/site-packages']

# py_evalやpy_run_string(local = FALSE)は「__main__」(Pythonのトップレベルのスクリプト環境)で実行しており、reticulateパッケージの名前空間にある「py」オブジェクトでアクセスできる
stringr::str_subset(string = reticulate::py$m_pth, "/usr/local/")
#> [1] "/usr/local/Cellar/python/3.6.5_1/Frameworks/Python.framework/Versions/3.6/bin"
#> [2] "/usr/local/opt/python/Frameworks/Python.framework/Versions/3.6/lib/python36.zip"
#> [3] "/usr/local/opt/python/Frameworks/Python.framework/Versions/3.6/lib/python3.6"
#> [4] "/usr/local/opt/python/Frameworks/Python.framework/Versions/3.6/lib/python3.6/lib-dynload"
#> [5] "/usr/local/opt/python/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages"

# pyに対して変数定義をするとpy_evalで取得できる
global_py <- reticulate::py
global_py$a <- 1 + 5
reticulate::py_eval(code = "a", convert = FALSE)
#> 6.0
reticulate::py_run_string(code = "a = a + 5", local = FALSE)
print(global_py$a)
#> [1] 11
print(reticulate::py$a)
#> [1] 11

# import_mainの環境でも同じ
py_main <- reticulate::import_main()
print(py_main$a)
#> [1] 11

# グローバル汚染を避けたい場合は「local = TRUE」を指定
local_syspath <- reticulate::py_run_string(
code = "import sys\nl_pth = sys.path", local = TRUE
)
# 結果をRで取得できるが、py_evalからは参照できない
stringr::str_subset(string = local_syspath$l_pth, "/usr/local/")
#> [1] "/usr/local/Cellar/python/3.6.5_1/Frameworks/Python.framework/Versions/3.6/bin"
#> [2] "/usr/local/opt/python/Frameworks/Python.framework/Versions/3.6/lib/python36.zip"
#> [3] "/usr/local/opt/python/Frameworks/Python.framework/Versions/3.6/lib/python3.6"
#> [4] "/usr/local/opt/python/Frameworks/Python.framework/Versions/3.6/lib/python3.6/lib-dynload"
#> [5] "/usr/local/opt/python/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages"

reticulate::py_eval(code = "l_pth", convert = FALSE)
#> Error in py_eval_impl(code, convert): NameError: name 'l_pth' is not defined
reticulate::py$l_pth
#> Error in py_get_attr_impl(x, name, silent): AttributeError: 'module' object has no attribute 'l_pth'



ファイルに書かれたPythonコードの実行

 さほど長くなくシンプルなPythonコードであれば、前述のpy_run_string()で文字列として与えて実行するだけでも許されるかもしれません。しかしながら、可読性とメンテナンス性などを考えたらPythonコードを別ファイルとして保存しておいた方が良いでしょう。

 別ファイルに定義されたPythonコードを実行してR上で利用できるようにする関数には、py_run_file()があります。こちらもpy_run_string()と同様にデフォルトでは「main」で実行されます。

 以下ではscikit-learnを用いたロジスティック回帰による分類モデルを学習するPythonスクリプトファイルを作成しておき、その実行結果に対してRでのデータ当てはめを試みます(目的は利用確認であるので、なんの工夫もなく全データで学習しています)。


iris_lr.py

# coding:utf-8

from sklearn import datasets
from sklearn import linear_model

sklearn_iris = datasets.load_iris()
(X, y) = sklearn_iris.data, sklearn_iris.target

# ロジスティック回帰で学習
clf = linear_model.LogisticRegression()
clf.fit(X, y)



ファイルに書かれたPythonコードをRで評価して結果を得る

# 上記のPythonファイルを「local = TRUE」で読み込んでRオブジェクトに代入

py_clf <- reticulate::py_run_file(file = "iris_lr.py", local = TRUE)
# 割り当てたRオブジェクトから学習結果にアクセスできる
class(x = py_clf$clf)
#> [1] "sklearn.linear_model.logistic.LogisticRegression" "sklearn.base.BaseEstimator"
#> [3] "sklearn.linear_model.base.LinearClassifierMixin" "sklearn.base.ClassifierMixin"
#> [5] "sklearn.linear_model.base.SparseCoefMixin" "python.builtin.object"
# 確認
py_clf$clf
#> LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
#> intercept_scaling=1, max_iter=100, multi_class='warn',
#> n_jobs=None, penalty='l2', random_state=None, solver='warn',
#> tol=0.0001, verbose=0, warm_start=False)

# 「local = TRUE」ではreticulate::pyに割り当てられない(Rオブジェクトに代入していないと呼び出せない)
reticulate::py$clf
#> Error in py_get_attr_impl(x, name, silent): AttributeError: module '__main__' has no attribute 'clf'

# datasetsパッケージのirisデータを適用すると結果が得られ、問題なく呼び出せている
tibble::tibble(
species = iris %>% dplyr::pull(Species),
predict = iris %>% dplyr::select(-Species) %>%
py_clf$clf$predict()
) %>%
dplyr::group_by(species, predict) %>% dplyr::tally()
#> # A tibble: 5 x 3
#> # Groups: species [?]
#> species predict n
#> <fct> <dbl> <int>
#> 1 setosa 0 50
#> 2 versicolor 1 45
#> 3 versicolor 2 5
#> 4 virginica 1 1
#> 5 virginica 2 49

# 上記のPythonファイルを「local = FALSE」で読み込んでPython環境に割り当て
reticulate::py_run_file(file = "iris_lr.py", local = FALSE)
# 「local = FALSE」ではreticulate::pyに割り当てられる
reticulate::py$clf
#> LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
#> intercept_scaling=1, max_iter=100, multi_class='warn',
#> n_jobs=None, penalty='l2', random_state=None, solver='warn',
#> tol=0.0001, verbose=0, warm_start=False)

# source_pythonでも同様にPythonコードが書かれたファイルが利用可能
# こちらでは環境を指定できる
py_lr <- new.env(parent = emptyenv())
reticulate::source_python(file = "iris_lr.py", envir = py_lr)
# グローバル環境にはなく、指定した環境内にある
exists(x = "clf", inherits = FALSE)
#> [1] FALSE
exists(x = "clf", where = py_lr, inherits = FALSE)
#> [1] TRUE
py_lr$clf
#> LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
#> intercept_scaling=1, max_iter=100, multi_class='warn',
#> n_jobs=None, penalty='l2', random_state=None, solver='warn',
#> tol=0.0001, verbose=0, warm_start=False)

# Python側のトップレベルのスクリプト環境がコピーされている挙動はよくわからない
py_lr$a
#> [1] 11

# source_pythonではURL指定もできる
reticulate::source_python(
file = "https://raw.githubusercontent.com/tensorflow/models/master/tutorials/rnn/ptb/reader.py",
envir = py_lr
)



Pythonライブラリの利用例

 前節にて文字列や別ファイルに書かれたPythonコードを評価して、R側から結果を得ることができました。これにより退屈なことはPythonにやらせたり、数多くいらっしゃるPython使いの方々が書いた綺麗なコードを流用できるようになりました。

 次はPythonライブラリを活用する手立てについてですが、こちらはクラスや関数定義が個々で異なるため、基本的な手順を示した後に実利用例ベースで触れたいと思います。

 例として挙げるPythonライブラリは「SpaCy, Sentence Piece, Pytorch, AllenNLP」です。これらの簡単な例を自前で書いたり、チュートリアル内容をreticulateパッケージを用いて書き直したりします。ライブラリの詳細については、それぞれの小節毎に記載のリンクをご参考ください。なお、例で用いたPythonライブラリはあらかじめインストールしております。


Pythonライブラリを利用する基本的な手順

 Pythonライブラリを利用するには、本章の最初にある「基本手順」で行ったようにimport()を用います。ライブラリを読み込んだ後は目的に応じて使いたい機能を$演算子で呼び出します。


ライブラリ利用と使いたい関数を見つける

# scikit-learnを使う

sklearn <- reticulate::import(module = "sklearn")

# py_helpでヘルプドキュメントが確認できる(RStudioでは別ファイルに開かれる)
# 使いたい関数を見つけたり、データ形式を調べる参考に
reticulate::py_help(object = sklearn)

# 対象ライブラリの階層を"$"で下る(Pythonでは'.')
class(x = sklearn)
#> [1] "python.builtin.module" "python.builtin.object"
reticulate::py_list_attributes(x = sklearn)
#> [1] "_ASSUME_FINITE" "__SKLEARN_SETUP__" "__all__"
#> [4] "__builtins__" "__check_build" "__doc__"
#> [7] "__file__" "__name__" "__package__"
#> [10] "__path__" "__version__" "_contextmanager"
#> [13] "base" "clone" "config_context"
#> [16] "exceptions" "externals" "get_config"
#> [19] "logger" "logging" "os"
#> [22] "re" "set_config" "setup_module"
#> [25] "sys" "utils" "warnings"

class(x = sklearn$svm)
#> [1] "python.builtin.module" "python.builtin.object"
reticulate::py_list_attributes(x = sklearn$svm)
#> [1] "LinearSVC" "LinearSVR" "NuSVC" "NuSVR"
#> [5] "OneClassSVM" "SVC" "SVR" "__all__"
#> [9] "__builtins__" "__doc__" "__file__" "__name__"
#> [13] "__package__" "__path__" "base" "bounds"
#> [17] "classes" "l1_min_c" "liblinear" "libsvm"
#> [21] "libsvm_sparse"

class(x = sklearn$svm$SVC)
#> [1] "abc.ABCMeta" "python.builtin.type" "python.builtin.object"
reticulate::py_list_attributes(x = sklearn$svm$SVC)
#> [1] "__abstractmethods__" "__class__"
#> [3] "__delattr__" "__dict__"
#> [5] "__doc__" "__format__"
#> [7] "__getattribute__" "__getstate__"
#> [9] "__hash__" "__init__"
#> [11] "__module__" "__new__"
#> [13] "__reduce__" "__reduce_ex__"
#> [15] "__repr__" "__setattr__"
#> [17] "__setstate__" "__sizeof__"
#> [19] "__str__" "__subclasshook__"
#> [21] "__weakref__" "_abc_cache"
#> [23] "_abc_negative_cache" "_abc_negative_cache_version"
#> [25] "_abc_registry" "_check_proba"
#> [27] "_compute_kernel" "_decision_function"
#> [29] "_dense_decision_function" "_dense_fit"
#> [31] "_dense_predict" "_dense_predict_proba"
#> [33] "_estimator_type" "_get_coef"
#> [35] "_get_param_names" "_pairwise"
#> [37] "_predict_log_proba" "_predict_proba"
#> [39] "_sparse_decision_function" "_sparse_fit"
#> [41] "_sparse_kernels" "_sparse_predict"
#> [43] "_sparse_predict_proba" "_validate_for_predict"
#> [45] "_validate_targets" "_warn_from_fit_status"
#> [47] "coef_" "decision_function"
#> [49] "fit" "get_params"
#> [51] "predict" "predict_log_proba"
#> [53] "predict_proba" "score"
#> [55] "set_params"

# python.builtin.functionに行き着く
class(x = sklearn$svm$SVC$fit)
#> [1] "python.builtin.instancemethod" "python.builtin.object"
reticulate::py_list_attributes(x = sklearn$svm$SVC$fit)
#> [1] "__call__" "__class__" "__cmp__"
#> [4] "__delattr__" "__doc__" "__format__"
#> [7] "__func__" "__get__" "__getattribute__"
#> [10] "__hash__" "__init__" "__new__"
#> [13] "__reduce__" "__reduce_ex__" "__repr__"
#> [16] "__self__" "__setattr__" "__sizeof__"
#> [19] "__str__" "__subclasshook__" "im_class"
#> [22] "im_func" "im_self"

# 使いたい関数の入力に合うようにデータを用意して使う
reticulate::py_help(object = sklearn$svm$SVC$fit)


 RStudioを利用している場合、Rパッケージ・関数名と同じように$の後に「タブ」を入力するとコード補完がされます(下図)。

rstudio_c.png

 また、View()を実行することでソースパネルの別タブにデータビューアが表示され、変換可能なオブジェクトの情報を見ることができます。


PythonオブジェクトにView関数を実行

View(py_lr)


rstudio_v.png

 さらにデータビューア上でカーソルを合わせ、右側に出てくる左矢印のポインタをクリックすると、下記のコマンドがコンソールにコマンドが打ち込まれて詳細が確認できます。


データビューアからの詳細表示

py_lr[["print_function"]]

#> _Feature((2, 6, 0, 'alpha', 2), (3, 0, 0, 'alpha', 0), 65536)

 

 こうして利用したいPython関数を見つけ、用途に合うかも併せてドキュメントで確認し、入力データを適した形で用意できれば、あとは実行するだけとなります。それでは先ほど名前を出したライブラリについて、次節から利用例として動作させた結果を記載します。


SpaCy

 SpaCyは自然言語処理のPythonライブラリです。日本語に対応したモデルは残念ながら公開されていないために本格的な解析は難しそうというのが個人の感想ですが、言語処理で使われる様々な機能が実装されています。

 SpaCyに関しては、すでにreticulateを用いたspacyrパッケージがCRAN公開されていますが(ただし、reticulateパッケージでconda環境を利用している必要があります)、実行例として公式を参考に簡単な英文解析を行います。


モデルファイルの事前準備

# モデルファイルを事前にダウンロードしておく

python3 -m spacy download en


SpaCyの関数適用

if (!reticulate::py_module_available(module = "spacy")) {

on.exit()
}

# ライブラリ読み込み
r_spacy <- reticulate::import(module = "spacy")
r_spacy$`__version__`
#> [1] "1.9.0"

# モデルのロード
spacy_en <- r_spacy$load("en")

# 簡単な解析結果例
doc_1 <- spacy_en("my fries were super gross")
reticulate::iterate(
it = reticulate::py_call(x = py_builtin$enumerate, doc_1),
f = function (x) {
tibble::data_frame(
token_id = x[[1]] + 1, token = x[[2]]$text,
pos = x[[2]]$pos_, tag = x[[2]]$tag_
) %>%
return()
}
) %>%
dplyr::bind_rows()
#> # A tibble: 5 x 4
#> token_id token pos tag
#> <dbl> <chr> <chr> <chr>
#> 1 1 my ADJ PRP$
#> 2 2 fries NOUN NNS
#> 3 3 were VERB VBD
#> 4 4 super ADV RB
#> 5 5 gross ADJ JJ

# 文間の類似度
doc_1$similarity(spacy_en("such disgusting fries"))
#> [1] 0.7139702



Sentence Piece

 Sentence Pieceは「ニューラル言語処理向けのトークナイザー」です。詳細は作者本人による解説記事に丁寧に書いてありますので、そちらをご覧ください。特にMeCabやKyTeaなどと目的や課題意識が違う点はしっかり認識しておきましょう。

 今回は大きくないテキストで学習を試し、そのモデルを用いて文分割ができるまでを確認しています。


Sentence Pieceでモデルを学習して単語分割を実施

if (!reticulate::py_module_available(module = "sentencepiece")) {

on.exit()
}

# 言語処理100本ノックの課題として公開されている『吾輩は猫である』をデータに利用
# 事前にダウンロードしておく
download.file(
url = "http://www.cl.ecei.tohoku.ac.jp/nlp100/data/neko.txt",
destfile = "neko.txt"
)

# ライブラリ読み込み
spm <- reticulate::import(module = "sentencepiece")

# BPEを指定して学習を実行(途中表示は長いために省略しています)
spm$SentencePieceTrainer$Train("--input=neko.txt --model_prefix=m --model_type=bpe --vocab_size=8000")
#> [1] TRUE

# モデルを読み込んで分割
s <- spm$SentencePieceProcessor()
s$Load("m.model")
#> [1] TRUE

s$EncodeAsPieces(input = "吾輩は猫である。")
#> [1] "▁吾輩は" "猫である" "。"



Pytorch

 Pytorchはテンソル操作や機械学習に関する処理が実装されたPythonライブラリです。TensorFlowやChainerと同じくDeep Learningのフレームワークのひとつと認知されており、研究者が積極的に利用しているらしいです(詳しくはないです)。

 ここでは下記にある公式チュートリアルコードをreticulateパッケージを用いたRから実行できるように、テンソル同士の四則演算とスライシングによる値更新を関数に置き換えております(Tensorとndarrayの演算がサポートされていないみたいな内容をちらっと読みましたが、詳しくないので中身はあまり考えていません)。


Pytorchのチュートリアルのひとつを実行

if (!reticulate::py_module_available(module = "torch")) {

on.exit()
}

# ライブラリ読み込み
torch <- reticulate::import(module = "torch")
print(torch$`__version__`)
#> [1] "0.4.1"

# チュートリアルのコードをRで実行できるように適宜変更
DTYPE <- torch$float
DEVICE <- torch$device("cpu")

SIZE_DIM <- list(
# batch size
N = 64L,
# input/hidden/output dimension
IN = 1000L, H = 100L, OUT = 10L
)
LEARNING_RATE <- 1e-6

# Create random input and output data
x <- torch$randn(SIZE_DIM$N, SIZE_DIM$IN, device = DEVICE, dtype = DTYPE)
y <- torch$randn(SIZE_DIM$N, SIZE_DIM$OUT, device = DEVICE, dtype = DTYPE)

# Randomly initialize weights
w1 <- torch$randn(SIZE_DIM$IN, SIZE_DIM$H, device = DEVICE, dtype = DTYPE)
w2 <- torch$randn(SIZE_DIM$H, SIZE_DIM$OUT, device = DEVICE, dtype = DTYPE)

result_loss <- foreach::foreach(
t = iterators::iter(obj = seq_len(length.out = 500)),
.combine = c
) %do% {

h <- x$mm(w1)
h_relu <- h$clamp(min = 0L)
y_pred <- h_relu$mm(w2)

# Compute and print loss
loss <- y_pred$sub(y)$pow(2L)$sum()$item()
if ((t %% 50) == 0) {
stringr::str_interp(
string = "ITER: ${iter}, LOSS:$[.6f]{loss}",
env = list(iter = t, loss = loss)
) %>%
print()
}

# Backprop to compute gradients of w1 and w2 with respect to loss
grad_y_pred <- y_pred$sub(y)$mul(2.0)
grad_w2 <- h_relu$t()$mm(grad_y_pred)
grad_h_relu <- grad_y_pred$mm(w2$t())
grad_h <- grad_h_relu$clone()

# grad_h[h < 0] = 0
grad_h$`__setitem__`(h$lt(0L), grad_h[h$lt(0L)]$zero_())

grad_w1 <- x$t()$mm(grad_h)

# Update weights using gradient descent
w1 <- w1$sub(grad_w1$mul(LEARNING_RATE))
w2 <- w2$sub(grad_w2$mul(LEARNING_RATE))

return(loss)
}
#> [1] "ITER: 50, LOSS:14831.459961"
#> [1] "ITER: 100, LOSS:645.757385"
#> [1] "ITER: 150, LOSS:53.407681"
#> [1] "ITER: 200, LOSS:5.303453"
#> [1] "ITER: 250, LOSS:0.563666"
#> [1] "ITER: 300, LOSS:0.062138"
#> [1] "ITER: 350, LOSS:0.007259"
#> [1] "ITER: 400, LOSS:0.001106"
#> [1] "ITER: 450, LOSS:0.000277"
#> [1] "ITER: 500, LOSS:0.000103"



AllenNLP

 AllenNLPは前述のPytorchで作られた自然言語処理ライブラリです。様々な言語タスクに対する新しい学習モデル(主にDeep Learningによるアプローチ)の開発に利用されています。

 本記事ではチュートリアル記事にあるコードをRで実行して、事前学習されたELMoの分散表現の取得を試しています(allennlp.commands.elmo.ElmoEmbedderクラスを呼び出し)。BERTは誰かに任せたいです。


AllenNLPで事前学習されたELMoの分散表現を取得

if (!reticulate::py_module_available(module = "allennlp")) {

on.exit()
}

# ライブラリ読み込み
allennlp <- reticulate::import(module = "allennlp")
print(allennlp$`__version__`)
#> [1] "0.7.0"

ElmoEmbedder <- reticulate::import(module = "allennlp.commands.elmo")$ElmoEmbedder

# 初回実行時はファイルダウンロードが発生して時間がかかる
elmo <- ElmoEmbedder()
vectors1 <- elmo$embed_sentence(
sentence = list("I", "ate", "an", "apple", "for", "breakfast")
)
vectors2 <- elmo$embed_sentence(
sentence = list("I", "ate", "a", "carrot", "for", "breakfast")
)
# 得られた分散表現の次元を確認
dim(x = vectors1)
#> [1] 3 6 1024

# チュートリアル通りにscipyライブラリでコサイン類似度を計算
scipy_cosine <- reticulate::import(module = "scipy.spatial.distance")$cosine
scipy_cosine(vectors1[3, 4, ], vectors2[3, 4, ])
#> [1] 0.1802062

# text2vecパッケージでも確認
1 - text2vec::sim2(
x = t(x = as.matrix(vectors1[3, 4, , drop = FALSE])),
y = t(x = as.matrix(vectors2[3, 4, , drop = FALSE])),
method = "cosine"
)[1, 1]
#> [1] 0.1802062



まとめ

 ここまででPythonが書かれたファイルやライブラリなどを呼び出し・評価して、Rで結果を得る方法を示しました。これにより、Pythonで開発された有用なライブラリがPythonを書けないというRユーザーにも広く使われることでしょう。

 先ほどのSpaCyを用いたspacyrパッケージを始め、reticulateパッケージでPythonライブラリを利用したRパッケージは増えてきています。前書きで触れたtensorflowパッケージやkerasパッケージ(これらはreticulateパッケージを開発しているRStudio社による)を筆頭に以下で挙げるようなパッケージがCRAN公開されており、今後も目が離せないです。


事前準備

 事前準備としてPythonとPythonライブラリをインストールし、reticulateパッケージで指定したバージョンが呼び出せるように設定します(事前準備が済んでいる場合はこの章は飛ばしてください)。


reticulateで用いるPython環境の準備

 reticulateパッケージでは「バージョン2.7以降のPython」が事前にインストールされている必要があります。

 ソースコードからビルドまたはyumやaptなどのパッケージ管理システムでインストールしたPython以外にvirtualenvやconda環境を指定でき、その場合はuse_virtualenv()use_condaenv()を用います。

 本記事ではuse_python()でパス指定していますが、以下の公式サイトを参考に自身の環境に応じて適宜設定ください。

 PythonライブラリであるNumpy(>=1.6)とPandasも事前にインストールしておきましょう(Scipyもあるとよいでしょう)。これらはR-Python間の型変換で用いられます。

 なお、reticulateパッケージではvirtualenvやconda環境の構築する関数群も提供されており、それらを利用する場合はpy_install()でPythonライブラリのインストールも行えます(今回はこれらの環境を使わないので、参照するライブラリはあらかじめpipで入れておきました)。


reticulateで用いるPython環境を指定

 使用するPython環境が用意できたらreticulateパッケージで呼び出せる状態になっているか確認します。ここではパッケージ管理システムでインストールしたPython3.6のバイナリをuse_python()でパス指定し、py_config()で設定状況を表示しています。

 すでに確認済みで問題なく指定バージョンを参照できていれば、py_config()py_available()は飛ばしても構いません。


システムのPythonを確認

> which python python3

/usr/bin/python
/usr/local/bin/python3


使用するPythonの指定と確認

library(reticulate)

reticulate::use_python(
python = reticulate:::python_unix_binary(bin = "python3.6"),
required = TRUE
)
# reticulateパッケージで使われるPythonの情報が表示
reticulate::py_config()
#> python: /usr/local/bin/python3.6
#> libpython: /usr/local/opt/python/Frameworks/Python.framework/Versions/3.6/lib/python3.6/config-3.6m-darwin/libpython3.6.dylib
#> pythonhome: /usr/local/opt/python/Frameworks/Python.framework/Versions/3.6:/usr/local/opt/python/Frameworks/Python.framework/Versions/3.6
#> version: 3.6.5 (default, Jun 17 2018, 12:13:06) [GCC 4.2.1 Compatible Apple LLVM 9.1.0 (clang-902.0.39.2)]
#> numpy: /usr/local/lib/python3.6/site-packages/numpy
#> numpy_version: 1.15.2
#>
#> NOTE: Python version was forced by use_python function

# reticulate::py_configで変数設定されば「TRUE」を返す
reticulate::py_available()
#> [1] TRUE



指定したPythonが使われない場合

 指定したPythonを使わせるのに苦しむ場合も少なくありません。

 私が遭遇した事例として、py_config()use_python()の前に呼び出してしまうとreticulateパッケージ内の変数を上書きして、use_python()で指定したPythonではなくシステムデフォルトが利用されました(そのうち修正されることを期待しています。利用するPythonの指定が一度しかされないためという可能性もあります)。


指定バージョンが使われない場合

# 環境変数PATH中にあるバージョンがデフォルトで利用される

Sys.which(names = "python")
#> python
#> "/usr/bin/python"

# .rs.restartR()
library(reticulate)

# Python2が表示
reticulate::py_config()
#> python: /usr/bin/python
#> libpython: /System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/config/libpython2.7.dylib
#> pythonhome: /System/Library/Frameworks/Python.framework/Versions/2.7:/System/Library/Frameworks/Python.framework/Versions/2.7
#> version: 2.7.10 (default, Oct 6 2017, 22:29:07) [GCC 4.2.1 Compatible Apple LLVM 9.0.0 (clang-900.0.31)]
#> numpy: /Library/Python/2.7/site-packages/numpy
#> numpy_version: 1.13.3
#>
#> python versions found:
#> /usr/bin/python
#> /usr/local/bin/python3

# Python3.6を指定
reticulate::use_python(
python = reticulate:::python_unix_binary(bin = "python3.6"),
required = TRUE
)

# importするライブラリはPython2を参照している
reticulate::import_main(convert = TRUE)$sys$version
#> [1] "2.7.10 (default, Oct 6 2017, 22:29:07) \n[GCC 4.2.1 Compatible Apple LLVM 9.0.0 (clang-900.0.31)]"

# reticulate::py_config()
# reticulate:::.globals$py_config

# use_pythonで指定したバージョンが出てくる
reticulate:::.globals$required_python_version
#> [1] "/usr/local/bin/python3.6"
reticulate:::.globals$use_python_versions
#> [1] "/usr/local/bin/python3.6"
reticulate:::reticulate_python_versions()
#> [1] "/usr/local/bin/python3.6"


 py_discover_config()では影響が出ません。


指定バージョンが使われる場合

# .rs.restartR()

library(reticulate)

# Python2が表示
reticulate::py_discover_config()
#> python: /usr/bin/python
#> libpython: /System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/config/libpython2.7.dylib
#> pythonhome: /System/Library/Frameworks/Python.framework/Versions/2.7:/System/Library/Frameworks/Python.framework/Versions/2.7
#> version: 2.7.10 (default, Oct 6 2017, 22:29:07) [GCC 4.2.1 Compatible Apple LLVM 9.0.0 (clang-900.0.31)]
#> numpy: /Library/Python/2.7/site-packages/numpy
#> numpy_version: 1.13.3
#>
#> python versions found:
#> /usr/bin/python
#> /usr/local/bin/python3

# Python3.6を指定
reticulate::use_python(
python = reticulate:::python_unix_binary(bin = "python3.6"),
required = TRUE
)

# Python3を参照している
reticulate::import_main(convert = TRUE)$sys$version
#> [1] "3.6.5 (default, Jun 17 2018, 12:13:06) \n[GCC 4.2.1 Compatible Apple LLVM 9.1.0 (clang-902.0.39.2)]"
# Python3が表示
#> reticulate::py_discover_config()
#> python: /usr/local/bin/python3.6
#> libpython: /usr/local/opt/python/Frameworks/Python.framework/Versions/3.6/lib/python3.6/config-3.6m-darwin/libpython3.6.dylib
#> pythonhome: /usr/local/opt/python/Frameworks/Python.framework/Versions/3.6:/usr/local/opt/python/Frameworks/Python.framework/Versions/3.6
#> version: 3.6.5 (default, Jun 17 2018, 12:13:06) [GCC 4.2.1 Compatible Apple LLVM 9.1.0 (clang-902.0.39.2)]
#> numpy: /usr/local/lib/python3.6/site-packages/numpy
#> numpy_version: 1.15.2
#>
#> NOTE: Python version was forced by use_python function

# use_pythonで指定したバージョンが出てくる
# reticulate:::.globals$py_config
reticulate:::.globals$required_python_version
#> [1] "/usr/local/bin/python3.6"
reticulate:::reticulate_python_versions()
#> [1] "/usr/local/bin/python3.6"


 RMarkdownでreticulateパッケージを利用するときに別チャンク内でpy_config()を呼び出した場合も影響するので、呼び出しているPythonバージョンには注意した方がいいかもしれません。

 また、use_python()で指定したPythonは、一度設定するとセッション中は変更できそうにないのでしょうか。Pythonバージョンを横断して使用するケースは少ないでしょうが、一応留意しておきましょう。


使いたいPythonの変更は不可

# .rs.restartR()

library(reticulate)

# Python3が表示
reticulate::py_discover_config()
#> python: /usr/local/bin/python3.6
#> libpython: /usr/local/opt/python/Frameworks/Python.framework/Versions/3.6/lib/python3.6/config-3.6m-darwin/libpython3.6.dylib
#> pythonhome: /usr/local/opt/python/Frameworks/Python.framework/Versions/3.6:/usr/local/opt/python/Frameworks/Python.framework/Versions/3.6
#> version: 3.6.5 (default, Jun 17 2018, 12:13:06) [GCC 4.2.1 Compatible Apple LLVM 9.1.0 (clang-902.0.39.2)]
#> numpy: /usr/local/lib/python3.6/site-packages/numpy
#> numpy_version: 1.15.2
#>
#> NOTE: Python version was forced by use_python function

# Python3が設定されている状態でPython2を指定
reticulate::use_python(
python = reticulate:::python_unix_binary(bin = "python")
)

# Python3を参照している
reticulate::import(module = "sys")$version
#> [1] "3.6.5 (default, Jun 17 2018, 12:13:06) \n[GCC 4.2.1 Compatible Apple LLVM 9.1.0 (clang-902.0.39.2)]"
# Python3が表示
reticulate::py_discover_config()
#> python: /usr/local/bin/python3.6
#> libpython: /usr/local/opt/python/Frameworks/Python.framework/Versions/3.6/lib/python3.6/config-3.6m-darwin/libpython3.6.dylib
#> pythonhome: /usr/local/opt/python/Frameworks/Python.framework/Versions/3.6:/usr/local/opt/python/Frameworks/Python.framework/Versions/3.6
#> version: 3.6.5 (default, Jun 17 2018, 12:13:06) [GCC 4.2.1 Compatible Apple LLVM 9.1.0 (clang-902.0.39.2)]
#> numpy: /usr/local/lib/python3.6/site-packages/numpy
#> numpy_version: 1.15.2
#>
#> NOTE: Python version was forced by use_python function



その他いろいろ

 reticulateパッケージは今回のような使い方の他にRMarkdownのPython設定(eng_python())や、Rセッション中からのインタラクティブ実行(repl_python())もできます。

 また、現在プレビュー版であるRStudio 1.2ではreticulateパッケージを用いたPython利用をサポートする機能が導入されています。

 こうしたreticulateパッケージでPythonライブラリを活用するRパッケージを開発したい場合は、公式サイトを参考にしてください。


まとめと所感

 RからPythonを活用するパッケージであるreticulateについて、定義された関数やPythonライブラリに適用した結果を表示して動作を確認しました。今回はそもそも動いてくれるかどうかを検証するために簡易な実装を複数のPythonライブラリで試しましたが、見たところ問題はなさそうですので、今後はもっと本格的なモデルを動かしてもいいかもしれません(ただし、その場合はGPU環境を検討すべきでしょう)。

 Pytorchに関しては自動微分の例もreticulateパッケージで書き換えてやってみたのですが、withのところでうまくいきませんでした。もしかしたらPytorch側の問題であろうかと調べてみたら下記のissueがあったので、しばらく様子を見ることにしました。

 個人的にはPythonライブラリをRで用いる行為は学習済みモデルを別タスクに使う感覚に近く、この流れが進めば実装という壁は意味をなさなくなっていくのだろうかなとか感じました(GraalVMベースのRの話も興味深い)。

 以前から「ビジネス - データ - アルゴリズム - 実装 - インフラ」というレイヤーのうち、ビジネスとアルゴリズム以外はよしなにできてしまえて楽できるかと思ったものですが(データは学習済みモデル、実装は今回の話、インフラはクラウド)、そんな未来はまだ来そうにないです。

 少なくとも言語が違う点で争うのはもうやめにしましょう。


参考


実行環境


R実行環境

> devtools::session_info()

Session info ────────────────────────────────────────────────────────────────────────────────────
setting value
version R version 3.5.1 (2018-07-02)
os macOS High Sierra 10.13.6
system x86_64, darwin15.6.0
ui RStudio
language (EN)
collate ja_JP.UTF-8
ctype ja_JP.UTF-8
tz Asia/Tokyo
date 2018-11-09

Packages ─────────────────────────────────────────────────────────────────────────────────────────────────
package * version date lib source
assertthat 0.2.0 2017-04-11 [1] CRAN (R 3.5.1)
backports 1.1.2 2017-12-13 [1] CRAN (R 3.5.1)
base64enc 0.1-3 2015-07-28 [1] CRAN (R 3.5.1)
bindr 0.1.1 2018-03-13 [1] CRAN (R 3.5.1)
bindrcpp 0.2.2 2018-03-29 [1] CRAN (R 3.5.1)
broom 0.5.0 2018-07-17 [1] CRAN (R 3.5.1)
callr 3.0.0 2018-08-24 [1] CRAN (R 3.5.1)
cellranger 1.1.0 2016-07-27 [1] CRAN (R 3.5.1)
cli 1.0.1 2018-09-25 [1] CRAN (R 3.5.1)
clipr 0.4.1 2018-06-23 [1] CRAN (R 3.5.1)
codetools 0.2-15 2016-10-05 [1] CRAN (R 3.5.1)
colorspace 1.3-2 2016-12-14 [1] CRAN (R 3.5.1)
crayon 1.3.4 2017-09-16 [1] CRAN (R 3.5.1)
data.table 1.11.8 2018-09-30 [1] CRAN (R 3.5.1)
desc 1.2.0 2018-05-01 [1] CRAN (R 3.5.1)
devtools 2.0.0 2018-10-19 [1] CRAN (R 3.5.1)
digest 0.6.18 2018-10-10 [1] CRAN (R 3.5.1)
dplyr * 0.7.7 2018-10-16 [1] CRAN (R 3.5.1)
evaluate 0.12 2018-10-09 [1] CRAN (R 3.5.1)
forcats * 0.3.0 2018-02-19 [1] CRAN (R 3.5.1)
foreach * 1.5.0 2018-08-01 [1] local
formatR 1.5 2017-04-25 [1] CRAN (R 3.5.1)
fs 1.2.6 2018-08-23 [1] CRAN (R 3.5.1)
futile.logger 1.4.3 2016-07-10 [1] CRAN (R 3.5.1)
futile.options 1.0.1 2018-04-20 [1] CRAN (R 3.5.1)
ggplot2 * 3.0.0 2018-07-03 [1] CRAN (R 3.5.1)
glue 1.3.0 2018-07-17 [1] CRAN (R 3.5.1)
gtable 0.2.0 2016-02-26 [1] CRAN (R 3.5.1)
haven 1.1.2 2018-06-27 [1] CRAN (R 3.5.1)
hms 0.4.2 2018-03-10 [1] CRAN (R 3.5.1)
htmltools 0.3.6 2017-04-28 [1] CRAN (R 3.5.1)
httpuv 1.4.5 2018-07-19 [1] CRAN (R 3.5.1)
httr 1.3.1 2017-08-20 [1] CRAN (R 3.5.1)
iterators * 1.0.10 2018-08-01 [1] local
jsonlite 1.5 2017-06-01 [1] CRAN (R 3.5.0)
knitr 1.20 2018-02-20 [1] CRAN (R 3.5.1)
lambda.r 1.2.3 2018-05-17 [1] CRAN (R 3.5.1)
later 0.7.5 2018-09-18 [1] CRAN (R 3.5.1)
lattice 0.20-35 2017-03-25 [1] CRAN (R 3.5.1)
lazyeval 0.2.1 2017-10-29 [1] CRAN (R 3.5.1)
lubridate 1.7.4 2018-04-11 [1] CRAN (R 3.5.1)
magrittr 1.5 2014-11-22 [1] CRAN (R 3.5.1)
Matrix * 1.2-14 2018-04-13 [1] CRAN (R 3.5.1)
memoise 1.1.0 2017-04-21 [1] CRAN (R 3.5.1)
mime 0.6 2018-10-05 [1] CRAN (R 3.5.1)
miniUI 0.1.1.1 2018-05-18 [1] CRAN (R 3.5.1)
mlapi 0.1.0 2017-12-17 [1] CRAN (R 3.5.1)
modelr 0.1.2 2018-05-11 [1] CRAN (R 3.5.1)
munsell 0.5.0 2018-06-12 [1] CRAN (R 3.5.1)
nlme 3.1-137 2018-04-07 [1] CRAN (R 3.5.1)
pillar 1.3.0 2018-07-14 [1] CRAN (R 3.5.1)
pkgbuild 1.0.2 2018-10-16 [1] CRAN (R 3.5.1)
pkgconfig 2.0.2 2018-08-16 [1] CRAN (R 3.5.1)
pkgload 1.0.1 2018-10-11 [1] CRAN (R 3.5.1)
plyr 1.8.4 2016-06-08 [1] CRAN (R 3.5.1)
prettyunits 1.0.2 2015-07-13 [1] CRAN (R 3.5.1)
processx 3.2.0 2018-08-16 [1] CRAN (R 3.5.1)
promises 1.0.1 2018-04-13 [1] CRAN (R 3.5.1)
pryr * 0.1.4 2018-02-18 [1] CRAN (R 3.5.1)
ps 1.2.0 2018-10-16 [1] CRAN (R 3.5.1)
purrr * 0.2.5 2018-05-29 [1] CRAN (R 3.5.1)
R6 2.3.0 2018-10-04 [1] CRAN (R 3.5.1)
Rcpp 0.12.19 2018-10-01 [1] CRAN (R 3.5.1)
RcppParallel 4.4.1 2018-07-19 [1] CRAN (R 3.5.1)
readr * 1.1.1 2017-05-16 [1] CRAN (R 3.5.1)
readxl 1.1.0 2018-04-20 [1] CRAN (R 3.5.1)
remotes 2.0.1 2018-10-19 [1] CRAN (R 3.5.1)
reprex 0.2.1 2018-09-16 [1] CRAN (R 3.5.1)
reticulate * 1.10 2018-08-05 [1] CRAN (R 3.5.1)
RevoUtils * 11.0.1 2018-08-01 [1] local
rlang 0.2.2 2018-08-16 [1] CRAN (R 3.5.1)
rmarkdown 1.10 2018-06-11 [1] CRAN (R 3.5.1)
rprojroot 1.3-2 2018-01-03 [1] CRAN (R 3.5.1)
rstudioapi 0.8 2018-10-02 [1] CRAN (R 3.5.1)
rvest 0.3.2 2016-06-17 [1] CRAN (R 3.5.1)
scales 1.0.0 2018-08-09 [1] CRAN (R 3.5.1)
sessioninfo 1.1.0 2018-09-25 [1] CRAN (R 3.5.1)
shiny * 1.1.0 2018-05-17 [1] CRAN (R 3.5.1)
stringi 1.2.4 2018-07-20 [1] CRAN (R 3.5.1)
stringr * 1.3.1 2018-05-10 [1] CRAN (R 3.5.1)
text2vec 0.5.1 2018-01-11 [1] CRAN (R 3.5.1)
tibble * 1.4.2 2018-01-22 [1] CRAN (R 3.5.1)
tidyr * 0.8.1 2018-05-18 [1] CRAN (R 3.5.1)
tidyselect 0.2.5 2018-10-11 [1] CRAN (R 3.5.1)
tidyverse * 1.2.1 2017-11-14 [1] CRAN (R 3.5.1)
usethis 1.4.0 2018-08-14 [1] CRAN (R 3.5.1)
whisker 0.3-2 2013-04-28 [1] CRAN (R 3.5.1)
withr 2.1.2 2018-03-15 [1] CRAN (R 3.5.1)
xml2 1.2.0 2018-01-24 [1] CRAN (R 3.5.1)
xtable 1.8-3 2018-08-29 [1] CRAN (R 3.5.1)

[1] /Library/Frameworks/R.framework/Versions/3.5.1-MRO/Resources/library

# RStudio
> rstudioapi::versionInfo()$version
[1] 1.2.1060



Python実行環境

> pip3 list --format=columns | grep -e "^numpy " -e "scipy" -e "pandas"

numpy 1.15.2
pandas 0.23.4
scipy 1.1.0