鶴見教育工学研究所

{transforEmotion}と{atrrr}でBlueSkyのセンチメント分析

担当しているRの講義で、毎年テキストマイニングを扱っています。その中で、SNSからのデータ収集や、いわゆる「ポジネガ分析」についても触れています。しかし、X APIは有償化して、個人の興味関心の範囲ではデータの収集ができなくなりました。“ツイート” 取得のデファクトスタンダードとなっていた{rtweet} パッケージも開発が終了してしまいました。

また、ポジネガ分析については、当初たまたまGitHubで見つけたPythonのsentiment_jaライブラリを {reticulate}1 パッケージを使って呼び出していました。しかし、ライブラリ自体がGitHubからなくなり、自分の手元にはたまたま削除される直前にCloneしたものが残っていたのですが、作者が削除したものをいつまでも使うのもな、と思っていました。そもそも私自身、どういうモデルなのかよく分からず、とりあえず感情価が返ってくるので使っている、というだけでしたし。

また、年明けにテキストマイニングの回がやってくるので、この辺りをアップデートしようと、いろいろ調べてみました。

Blueskyからポストを取得する

RからBluesky APIにアクセスできるパッケージ

APIに限らない、X社の方針転換などで、さまざまな代替SNSが公開されています。その中で、比較的ユーザー数が多く、Xの雰囲気に近いと思われるものにBlueskyがあります。また、Blueskyはオープンであることを重視しており、APIも無償で公開されています。そこで、RからBlueskyのAPIにアクセスできるパッケージがないかな、と探してみました。すると、以下の2つがありました。

どちらも、機能的にはほとんど同じで、Blueskyへの投稿、いいね、リポストやフォロー・アンフォローなどの操作ができるようです。そして、今回の主目的である、他者のポストを取得する機能もあります。なお、XにおけるStreaming APIと同様に、BlueskyにもFirehoseというAPIがあり、リアルタイムのポストを取得できる仕組みはありますが、いずれのパッケージもまだサポートしていないようです。2実際、ランダムに取得したポストでポジネガ分析してもあまり意味がないので、今回はキーワードを取得して、そのテキストを処理対象とします。

{atrrr} パッケージのインストール

上記の2つのパッケージのどちらでも、キーワードを指定したポストの検索、取得はできますが、今回は {atrrr} パッケージを使います。3まずその前に、Blueskyにアカウントを作成する必要があります。特に迷うようなこともないので、ここでは省略します。

何年ぶりかに🤪APIアクセス以外に使用する予定はないですが

何年ぶりかに🤪APIアクセス以外に使用する予定はないですが

{atrrr} パッケージはCRANからインストールできます。

1
install.packages("atrrr")

そして、パッケージを読み込みます。

1
library(atrrr)

{atrrr} パッケージによるユーザー認証

はじめに、APIにアクセスするために auth() 関数で認証を得ます。XではAPIにアクセスするためにアクセストークンを取得する必要がありますが、Blueskyではユーザー名とパスワードでアクセスできます。

1
auth(user = "ユーザー名", password = "パスワード")

ポストの取得

次に、APIにアクセスし、キーワードで検索したポストを取得するには search_post() 関数を使います。ここでは、たまたま今日 (12/28) 開催された中央競馬のGI、ホープフルステークスについてのポストを取得してみましょう。

1
2
3
4
# ホープフルステークスやホープフルSなど表記が揺れるので
# 前半だけをキーワードとして指定
res <- search_post(q = "ホープフル since:2024-12-28 until:2024-12-29", limit = 200)
head(res)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
## # A tibble: 6 × 20
##   uri                                                                   
##   <chr>                                                                 
## 1 at://did:plc:5cbdxxui4cmo2cytfahvafyl/app.bsky.feed.post/3leehkzsx7s2c
## 2 at://did:plc:itkjw3kfno5jvsjf5ptlu6ss/app.bsky.feed.post/3leeheyydb226
## 3 at://did:plc:dp3fdt3yreehru3pouwvaams/app.bsky.feed.post/3leegul2ar22d
## 4 at://did:plc:4urr3lljj6b2gca5bf4hyzt7/app.bsky.feed.post/3leegl5xbgc2m
## 5 at://did:plc:c3itjpvm5hamy7up773xna6a/app.bsky.feed.post/3leegfzu4cs2m
## 6 at://did:plc:kdkik6uosu3x4btwq2klnmrv/app.bsky.feed.post/3leeg2ekubc2l
##   cid                                                        
##   <chr>                                                      
## 1 bafyreigxm7shursdnjawezjom5467c6nkbxfxedki6ayzmyfepk7lkjcpa
## 2 bafyreibhsw75brxdokw2p4jgxkxerhtmtobxuoufxukdadfc6yr2tp7b2q
## 3 bafyreifly3pjba3ceantpcbeliqfy345pvesbl6wme46vgebt35unlroxa
## 4 bafyreiavsq6zsxkoeie3y7rnxl5kv4tsu6y6kyu33a5amhnig3k7avtvvu
## 5 bafyreiahf7oicub72c42qobtk32wtm7sigmuwf25w4lgv6ajrzet4hcdcu
## 6 bafyreia6jgc7lfuystkxbekzlpbs2t6atb7jtq53odgmopytoryc4ebyem
##   author_handle           author_name                      
##   <chr>                   <chr>                            
## 1 tee0.bsky.social        てちゃ                           
## 2 d27-abc.bsky.social     🤖マシンキラー阿彦🤖             
## 3 yukimiyosi.bsky.social  見吉 幸 クルクル福岡両時間いた人
## 4 neothakaoka.bsky.social 三葉虫                           
## 5 azuyahi.bsky.social     あずやひ                         
## 6 tsukimasite.bsky.social つきま                           
##   text                                                                          
##   <chr>                                                                         
## 1 "ホープフルの予想だけして、まだレースは見てないけど結果は見て、やっぱりクロワ!!!ってなってる人です!!!!!あとでレース見るね🫶"……
## 2 "ホープフルS面白かった〜とか競馬場楽しい〜とかフォロワーと会えてよかった〜とか諸々を呟いて行こうと兄の家に帰ったら早速兄がリゼロを見始めてくれてて冷や汗…
## 3 "今夜のホープフルステークス後夜祭で、前後夜祭は開催を辞めます。\n人も集まらないし、本人も忘れている事もありますし。気まぐれで開催するかもです。"……
## 4 "私のホープフル ホープレスステークスでした\nでも北村友一さまが勝ったのはうれしい こっちまで泣きそうになってしまった おめでとうございます クロワの単…
## 5 "こんばんは!\nホープフル外しました😅"                                        
## 6 "2024ホープフルステークス"                                                    
##   author_data      post_data        embed_data       reply_count repost_count
##   <list>           <list>           <list>                 <int>        <int>
## 1 <named list [8]> <named list [4]> <NULL>                     0            0
## 2 <named list [7]> <named list [4]> <NULL>                     0            0
## 3 <named list [8]> <named list [4]> <NULL>                     0            0
## 4 <named list [8]> <named list [4]> <NULL>                     0            0
## 5 <named list [8]> <named list [5]> <NULL>                     0            0
## 6 <named list [8]> <named list [5]> <named list [2]>           0            0
##   like_count indexed_at         
##        <int> <dttm>             
## 1          0 2024-12-28 12:04:35
## 2          0 2024-12-28 12:01:12
## 3          0 2024-12-28 11:52:02
## 4          0 2024-12-28 11:46:45
## 5          0 2024-12-28 11:43:53
## 6          4 2024-12-28 11:37:31
##   in_reply_to                                                           
##   <chr>                                                                 
## 1 <NA>                                                                  
## 2 <NA>                                                                  
## 3 <NA>                                                                  
## 4 <NA>                                                                  
## 5 at://did:plc:wlpww6oubzebkigukd3dacdn/app.bsky.feed.post/3led7cvcbqk2y
## 6 <NA>                                                                  
##   in_reply_root                                                          quotes
##   <chr>                                                                  <chr> 
## 1 <NA>                                                                   <NA>  
## 2 <NA>                                                                   <NA>  
## 3 <NA>                                                                   <NA>  
## 4 <NA>                                                                   <NA>  
## 5 at://did:plc:c3itjpvm5hamy7up773xna6a/app.bsky.feed.post/3led37ktv5k2t <NA>  
## 6 <NA>                                                                   <NA>  
##   tags   mentions links  langs      labels    
##   <list> <list>   <list> <list>     <list>    
## 1 <NULL> <NULL>   <NULL> <list [1]> <list [0]>
## 2 <NULL> <NULL>   <NULL> <list [1]> <list [0]>
## 3 <NULL> <NULL>   <NULL> <list [1]> <list [0]>
## 4 <NULL> <NULL>   <NULL> <list [1]> <list [0]>
## 5 <NULL> <NULL>   <NULL> <list [1]> <list [0]>
## 6 <NULL> <NULL>   <NULL> <list [1]> <list [0]>

ポストがデータフレームとして取得できました。このうち、text 列に実際に分析対象となるテキストが格納されています。

Rでセンチメント分析をする

次に、テキストのセンチメント分析をする方法を (講義のために) アップデートします。上述のように、昨年まではPythonのsentiment_jaライブラリを使っていました。また、それ以前はいくつかの公開された感情表現辞書を使う方法が一般的でした。しかし近年では、生成AIの基盤技術でもある、Transformerを使った言語モデルでセンチメント分析を行うことが主流のようです。

RでTransformerモデルを利用できるパッケージ

しかし、残念ながらそれらの研究開発領域ではPythonが主流で、Rからそれらの高度なモデルをネイティブに扱えるパッケージはないようです。CRANに公開されている、Transformer系のパッケージはいずれも、{reticulate} パッケージを使い、実質的にはRの裏でPythonを動かして処理するものばかりです。その中で、今回は目的にフィットした {transforEmotion} パッケージを使用します。

このパッケージは、Hugging Faceで公開されている、“Zero shot classification” に対応した任意のモデルをダウンロードし、{reticulate} パッケージでPyTorchやTensorflowを動かし、任意のラベル (後述) で判定ができます。

…まぁTransformerもHugging FaceもPyTorchも何もかもよくわかっていないんですが🤪

{transforEmotion} パッケージのインストール

{transforEmotion} パッケージはCRANからインストールできます。

1
install.packages("transforEmotion")

パッケージをインストールした後、パッケージが利用するPythonの仮想環境をminicondaで作成する必要があります。ドキュメントにも記載されていますが、setup_miniconda() 関数を実行します。

1
2
3
library(transforEmotion)

setup_miniconda()

基本的には、自動で必要なPythonライブラリ類がダウンロードされ、環境が構築されます。なお、Macでは ~/Library/r-miniconda-arm64/envs/transforEmotion 下に保存されます。

英語テキストでのテスト

環境が構築できたら、まずは英語のテキストでテストしてみましょう。{transforEmotion} パッケージでは、デフォルトで cross-encoder/nli-distilroberta-base モデルが使用されます。4公式サイトのサンプルのままですが、以下を実行してみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Load data
data(neo_ipip_extraversion)

# Example text 
text <- neo_ipip_extraversion$friendliness[1:5]

# Cross-Encoder DistilRoBERTa
transformer_scores(
 text = text,
 classes = c(
   "friendly", "gregarious", "assertive",
   "active", "excitement", "cheerful"
 )
)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
## $`Make friends easily`
##   friendly gregarious  assertive     active excitement   cheerful 
##    0.52954    0.08775    0.09368    0.08829    0.06882    0.13192 
## 
## $`Warm up quickly to others`
##   friendly gregarious  assertive     active excitement   cheerful 
##    0.05750    0.03237    0.17586    0.17694    0.30525    0.25208 
## 
## $`Feel comfortable around people`
##   friendly gregarious  assertive     active excitement   cheerful 
##    0.73451    0.03259    0.04983    0.04678    0.01982    0.11646 
## 
## $`Act comfortably around people`
##   friendly gregarious  assertive     active excitement   cheerful 
##    0.44115    0.04934    0.14065    0.22977    0.02672    0.11237 
## 
## $`Cheer people up`
##   friendly gregarious  assertive     active excitement   cheerful 
##    0.08325    0.13463    0.10779    0.07430    0.33212    0.26791

テキストの感情を6つの尺度で判定し、結果が割合で出力されます。とりあえず結果が出力されれば、環境構築はうまくいっているでしょう。

多言語に対応したモデルのインストール

上記のモデルは、英語にのみ対応しています。そのため、日本語テキストを分析するには、対応したモデルをダウンロードし、指定する必要があります。日本語を含めた多くの言語に対応したモデルとして、MoritzLaurer/mDeBERTa-v3-base-xnli-multilingual-nli-2mil7がありました。5

{transforEmotion} パッケージでは、モデルを transformer_scores() 関数の transformer オプションで指定すれば、自動的にダウンロード、インストールをしてくれます。以下のようなコードで、日本語のセンチメント分析ができます。また、Transformerによるモデルでは、任意のラベル (クラス) に対する推論ができます。ここでは、シンプルに「ポジティブ」か「ネガティブ」のどちらがもっともらしいか判定させます。初回は、モデルのダウンロード、インストールがあるため、かなりの時間がかかります。

1
2
3
4
5
6
7
text <- c("お正月がとても楽しみだ", "バレンタインは本当に憂鬱だ", "卒論が出せるか不安だ")

transformer_scores(
 text = text,
 classes = c("ポジティブ", "ネガティブ"),
 transformer = "MoritzLaurer/mDeBERTa-v3-base-xnli-multilingual-nli-2mil7"
)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
## $お正月がとても楽しみだ
## ポジティブ ネガティブ 
##    0.97903    0.02097 
## 
## $バレンタインは本当に憂鬱だ
## ポジティブ ネガティブ 
##    0.04791    0.95209 
## 
## $卒論が出せるか不安だ
## ポジティブ ネガティブ 
##    0.02826    0.97174

そもそもわかりやすいテキストですが、きちんと判定できているようです。

日本語に特化したモデルのインストール

計算環境に余裕がある場合や、多言語のテキストを扱いたい場合は、上記の手順で十分ですが、日本語だけで十分だ、という時には、もう少し小さいモデルが使えるかもしれません。Hugging Faceで検索してみると、1つだけ6モデルがありました。akiFQC/bert-base-japanese-v3_nli-jsnli-jnli-jsickモデルは、著名な日本語モデルをさらにファインチューニングしたもの (のよう) です。今度はこちらを使ってみましょう。

fugashiライブラリのインストール

ただ、こちらのモデルは前処理にMeCabを使っており、利用にはfugashiライブラリが必要です。Rから transformer_scores() 関数を実行すると、ただ “object ‘classifier’ not found” とエラーが返ってくるだけで、何が悪いのかよくわかりませんでしたが、reticulate::repl_python() 関数でPythonコンソールを起動し、そちらでモデルの読み込み処理を実行すると、fugashiがインストールされていない、というエラーの原因がわかりました。

Rからminicondaの仮想環境でライブラリをインストールする、というややこしい形になりますが、以下のようにすることでグローバルではなく {transforEmotion} 用の環境にインストールできました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
library(transforEmotion)
library(reticulate)

repl_python()

# 以下はPythonコンソール

!pip install fugashi

exit

これで、日本語に特化したモデルが動かせるようになります。試してみましょう。初回はモデルのダウンロード、インストールに時間がかかります。

1
2
3
4
5
6
7
text <- c("お正月がとても楽しみだ", "バレンタインは本当に憂鬱だ", "卒論が出せるか不安だ")

transformer_scores(
 text = text,
 classes = c("ポジティブ", "ネガティブ"),
 transformer = "akiFQC/bert-base-japanese-v3_nli-jsnli-jnli-jsick"
)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
## $お正月がとても楽しみだ
## ポジティブ ネガティブ 
##     0.2756     0.7244 
## 
## $バレンタインは本当に憂鬱だ
## ポジティブ ネガティブ 
##    0.98643    0.01357 
## 
## $卒論が出せるか不安だ
## ポジティブ ネガティブ 
##    0.93838    0.06162

こちらは、どうも値の大小が逆になっているようです。0が最大、1が最小なのかもしれません。 その振る舞いを理解していれば、多言語モデルよりは多少小さなモデルとして活用できます。

Blueskyのテキストをセンチメント分析する

ということで、環境が整ったので、Blueskyに投稿されたポストをセンチメント分析してみましょう。先ほどの search_post() 関数の実行結果が残っているので、transformer_scores() 関数と組み合わせます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
sentiment_res <- transformer_scores(
  text = res$text,
  classes = c("喜び", "怒り", "哀しみ", "楽しみ"),
  transformer = "MoritzLaurer/mDeBERTa-v3-base-xnli-multilingual-nli-2mil7" 
)

df <- sentiment_res |>
  bind_rows() |>
  rowid_to_column(var = "id") |>
  pivot_longer(cols = c(-id), names_to = "感情") |>
  mutate(感情 = factor(感情, levels = c("喜び", "怒り", "哀しみ", "楽しみ")))

head(df)
1
2
3
4
5
6
7
8
9
## # A tibble: 6 × 3
##      id 感情    value
##   <int> <fct>   <dbl>
## 1     1 喜び   0.173 
## 2     1 怒り   0.388 
## 3     1 哀しみ 0.172 
## 4     1 楽しみ 0.266 
## 5     2 喜び   0.0981
## 6     2 怒り   0.412

200件のポストについて、M3 Macで1分弱かかりました。専門的知識を駆使して🤪ポストの内容と判定結果を見ると、概ね「外した」とか「遅い」といった単語を「哀しみ」「怒り」と判定しているなど、妥当な判定をしているように思います。グラフにしてみましょう。

1
2
3
4
ggplot(df, aes(x = value, y = 感情, colour = 感情)) +
  geom_jitter() +
  scale_y_discrete(limits = rev) +
  scale_colour_fivethirtyeight()
1
2
3
ggplot(df, aes(x = value, fill = 感情)) +
  geom_histogram(position = "identity", binwidth = 0.025, alpha = 0.5) +
  scale_fill_fivethirtyeight()

言語学ではない、Rの講義でハンズオンとして実行する分には、こんなものでよいのではないでしょうか (?)。

…なんかちゃんとした記事になったので、アドベントカレンダーの時期にやればよかったですね。そして、{transforEmotion} パッケージとモデルが講義で使っているPosit Cloudで動くのかが不安です🤪多分メモリもストレージも足りないんだろうなぁ。


  1. 何年か前に、この { } で囲う記法は推奨されない、とかそんな議論があったように思いますが、R以外のソフトウェア、ライブラリと区別するために、ここでは囲っています。 ↩︎

  2. {atrrr}の方は、開発者が実装に向けて協力を求めています。https://github.com/JBGruber/atrrr/issues/23 ↩︎

  3. {bskyr} は、ドキュメントを見ると機能が豊富そうですが、実際手元で試すと、最初の認証からうまくいきませんでした。 ↩︎

  4. 例によってなんだかよくわかっていませんが。 ↩︎

  5. 例によって(ry ↩︎

  6. 2つありますが、同じ作者で「長い名前の方を推奨します」と書いてあるので。https://huggingface.co/akiFQC/bert-base-japanese-v3_nli-jsnli ↩︎