StatsBeginner: 初学者の統計学習ノート

統計学およびR、Pythonでのプログラミングの勉強の過程をメモっていくノート。たまにMacの話題。

MacのAcrobatで見開きPDFを1ページごとに分割する

後でまた使うことがありそうなのでメモしておく。

  1. 見開きが複数ある場合は、まず、1見開き1ファイルに分割する
  2. 個々のファイルをAcrobatで開く
  3. ToolsのOrganize Pagesを開く
  4. Moreのメニューから「Set page Boxes」を開く
  5. インチで分かりにくければmmに変更し、プレビューの下に表示されている横幅の数値を2で割る
  6. Apply to 「CropBox」で、左半分を消したいときはLeftのところに、上記の2で割った数字を入れる
  7. OKを押して、保存する

ChatGPTにプログラミングを教わるとは、こういう感じです。

ChatGPTとのやり取りで一番楽しいのは、やっぱり、プログラミングを教えてもらうことですね。
今回は、とあるアニメーションの作り方を教えてもらいました。下にそのやりとりを掲載しておきますが、コードの中身は読み飛ばして、文章でのやり取りの部分と出力されたアニメーション動画を見るだけでも、ChatGPTにプログラミングを教わるとはどういうことなのか雰囲気がある程度分かるんじゃないでしょうか。
 
ポイントは、一発で何かが出来上がるのではなく、疑問をぶつけながら一緒にコードを完成させていく感じになるところですかね。今回の例だともはや、単にアニメーションの作成を依頼してるだけになってて、私はコードをほとんどみてませんが(笑)
 
ちなみに私はChatGPTに、
 

  • 言葉で指示した内容を実現するコードを書いてもらう
  • 読み解くのがめんどくさいコードを与えて意味や構造を説明してもらう
  • あるコードを実行してエラーが出た時に、コードとエラーメッセージを与えて解決方法を考えてもらう

 
というのを何度か試したのですが、人間に教わってるのとあまり変わらんというか、返事が一瞬で返ってくるぶん人間より有能かも知れません。やってて驚くのは、提案してくれるコードそのものよりも、「会話が成り立つこと」です。自然言語で頼んだことを、概ね理解してくれるのが凄い。
 
以下のやり取りでは、ChatGPT(GPT-4)に、セルラー・オートマトンの可視化のコードをつくってもらいました。最近Pythonでも似たようなことを試して、ライブラリのせいだと思いますが表示があんま綺麗じゃなかったので、今回はRのggplot2でやっています。
教えてもらったコードを実行して出力されたアニメーションも掲載しておきます。アニメーションは何回か修正してもらって、5回目で意図したとおりのものができました。
 

セルラー・オートマトンのルール30を、Rのggplot2で描画したいのですが、どのようなコードが考えられますか。
 

ChatGPT

セルラー・オートマトンのルール30を描画するために、R言語とggplot2パッケージを使用することができます。以下は、ルール30を実装し、結果をggplot2で描画する方法の一例です。
まず、必要なパッケージをインストールして読み込みます。

install.packages("ggplot2")
library(ggplot2)

次に、セルラー・オートマトンのルール30を実装する関数を定義します。

rule30 <- function(input) {
  n <- length(input)
  output <- numeric(n)
  for (i in 1:n) {
    left <- ifelse(i == 1, input[n], input[i - 1])
    center <- input[i]
    right <- ifelse(i == n, input[1], input[i + 1])
    
    output[i] <- as.integer(left != (center | right))
  }
  output
}
次に、初期状態を定義し、繰り返しルール30を適用して結果を格納します。
n_steps <- 100
initial_state <- numeric(n_steps)
initial_state[n_steps %/% 2] <- 1
states <- matrix(0, nrow = n_steps, ncol = n_steps)
states[1, ] <- initial_state

for (i in 2:n_steps) {
  states[i, ] <- rule30(states[(i - 1), ])
}
最後に、ggplot2を使って結果を描画します。
cells <- data.frame(
  x = rep(1:n_steps, times = n_steps),
  y = rep(1:n_steps, each = n_steps),
  state = as.factor(as.vector(states))
)

plot <- ggplot(cells, aes(x = x, y = y, fill = state)) +
  geom_tile(color = "black") +
  scale_fill_manual(values = c("white", "black")) +
  theme_minimal() +
  theme(panel.grid = element_blank(),
        axis.ticks = element_blank(),
        axis.text = element_blank())

print(plot)これにより、セルラー・オートマトンのルール30がggplot2で描画されます。上記のコードは、100回のステップで実行されるルール30の例ですが、n_steps変数を変更することで、異なるステップ数で実行することができます。
 

出力結果


 

1秒に4ステップずつ追加して表示を更新していくような、アニメーションっぽいことはできますか?
 

ChatGPT

アニメーションのように表示を更新するために、gganimateパッケージを使用できます。まず、gganimateパッケージをインストールし、ggplot2とともに読み込みます。

install.packages("gganimate")
library(gganimate)
library(ggplot2)

gganimateを使用して1秒に4ステップずつ追加して表示を更新するには、以下のコードを実行します。

n_steps <- 100
n_steps_anim <- n_steps * 4
initial_state <- numeric(n_steps_anim)
initial_state[n_steps_anim %/% 2] <- 1
states <- matrix(0, nrow = n_steps_anim, ncol = n_steps_anim)
states[1, ] <- initial_state

for (i in 2:n_steps_anim) {
  states[i, ] <- rule30(states[(i - 1), ])
}

cells <- data.frame(
  x = rep(1:n_steps_anim, times = n_steps_anim),
  y = rep(1:n_steps_anim, each = n_steps_anim),
  state = as.factor(as.vector(states)),
  time = rep(1:n_steps_anim, each = n_steps_anim)
)

plot <- ggplot(cells, aes(x = x, y = y, fill = state)) +
  geom_tile(color = "black") +
  scale_fill_manual(values = c("white", "black")) +
  theme_minimal() +
  theme(panel.grid = element_blank(),
        axis.ticks = element_blank(),
        axis.text = element_blank()) +
  transition_manual(frames = time) +
  enter_fade() +
  exit_fade() +
  end_pause(25)

anim <- animate(plot, nframes = n_steps * 4, duration = n_steps, end_pause = 25, width = 600, height = 600)
anim_save("rule30_animation.gif", anim)

これにより、rule30_animation.gifという名前のGIFアニメーションファイルが作成され、1秒に4ステップずつ表示が更新されます。n_steps変数を変更することで、異なるステップ数でアニメーションを実行できます。また、duration引数を変更することで、アニメーションの全体の長さを調整できます。
 

end_pauseって何の関数ですか?
 

ChatGPT

申し訳ありませんが、説明が誤っていました。end_pauseは関数ではなく、gganimateパッケージのanimate関数の引数です。end_pause引数は、アニメーションの最後に追加のフレーム数を指定して、アニメーションが一定時間止まるようにするために使用されます。これにより、アニメーションの最後に一時停止が発生し、アニメーション全体がより見やすくなります。
しかし、私の以前のコード例では、end_pause引数をanimate関数に渡す代わりに、誤ってend_pause()関数のように記述していました。正しいコード例は以下のようになります。

anim <- animate(plot, nframes = n_steps * 4, duration = n_steps, end_pause = 25, width = 600, height = 600)

この例では、アニメーションの最後に25フレームの一時停止が追加されます。end_pauseの値を調整することで、この一時停止の長さを変更することができます。
 

要するにさっきのコードの

+
  end_pause(25)

を削除すればいいですか?
 

ChatGPT

はい、おっしゃる通りです。end_pause(25)を削除し、animate関数のend_pause引数に25を設定すれば、正しいコードになります。以下が修正済みのコード例です。

plot <- ggplot(cells, aes(x = x, y = y, fill = state)) +
  geom_tile(color = "black") +
  scale_fill_manual(values = c("white", "black")) +
  theme_minimal() +
  theme(panel.grid = element_blank(),
        axis.ticks = element_blank(),
        axis.text = element_blank()) +
  transition_manual(frames = time) +
  enter_fade() +
  exit_fade()

anim <- animate(plot, nframes = n_steps * 4, duration = n_steps, end_pause = 25, width = 600, height = 600)
anim_save("rule30_animation.gif", anim)

この修正により、アニメーションの最後に25フレームの一時停止が追加されます。end_pause引数の値を調整することで、一時停止の長さを変更することができます。
 

以下のエラーが出ました。
> anim_save("rule30_animation.gif", anim)
Error: The animation object does not specify a save_animation method
 

ChatGPT

すみません。このエラーは、gganimateが指定されたファイル形式でアニメーションを保存するためのメソッドがないことを示しています。GIF形式でアニメーションを保存するには、gifskiとpngパッケージをインストールして、animate()関数のrenderer引数にgifski_renderer()を指定する必要があります。以下の手順で修正できます。
まず、gifskiとpngパッケージをインストールします。

install.packages("gifski")
install.packages("png")

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

library(gifski)

animate()関数のrenderer引数にgifski_renderer()を指定してアニメーションを作成し、anim_save()関数でGIFファイルに保存します。

anim <- animate(plot, nframes = n_steps * 4, duration = n_steps, end_pause = 25, width = 600, height = 600, renderer = gifski_renderer())
anim_save("rule30_animation.gif", anim)

これで、エラーが解決され、アニメーションがGIF形式で保存されるはずです。rule30_animation.gifファイルが作成され、1秒に4ステップずつ表示が更新されます。
 

gifskiではなくavパッケージをつかってvideoファイルを作成するにはどうすればいいですか?
 

ChatGPT

avパッケージを使用してアニメーションをビデオファイルとして保存するには、まずavパッケージをインストールし、読み込みます。

install.packages("av")
library(av)

次に、animate()関数でアニメーションを作成し、renderer引数にav_renderer()を指定してビデオファイル(例えば、MP4形式)に保存します。

library(gganimate)
library(ggplot2)
library(av)

# ...(セルラー・オートマトンのルール30を描画するためのコード)

plot <- ggplot(cells, aes(x = x, y = y, fill = state)) +
  geom_tile(color = "black") +
  scale_fill_manual(values = c("white", "black")) +
  theme_minimal() +
  theme(panel.grid = element_blank(),
        axis.ticks = element_blank(),
        axis.text = element_blank()) +
  transition_manual(frames = time) +
  enter_fade() +
  exit_fade()

anim <- animate(plot, nframes = n_steps * 4, duration = n_steps, end_pause = 25, width = 600, height = 600, renderer = av_renderer("rule30_animation.mp4"))

これで、rule30_animation.mp4という名前のMP4ビデオファイルが作成されます。アニメーションは1秒に4ステップずつ表示が更新され、最後に25フレームの一時停止が追加されます。
もちろん、他のビデオ形式(例えば、WebMやAVI)にも対応しています。av_renderer()関数の引数に適切なファイル名を指定することで、異なる形式でビデオを保存できます。
 

出力結果


 

アニメーションはできました。しかしこのやり方だと、セルラーオートマトンが一行ずつ表示されているのですが、一般的には、一度表示された行は表示しっぱなしにして、だんたん画面がセルラーオートマトンで埋め尽くされていくようにします。そのように修正してもらえますか?あとついでにいうと、一般的には、一行目の真ん中が黒、他は白という初期状態からスタートします。
 

ChatGPT

ご指摘の通り、一般的にセルラーオートマトンは、新しい行が表示される度に既存の行がそのまま表示され続け、画面が徐々に埋め尽くされるように動作します。また、初期状態は真ん中のセルが黒で、他のセルが白です。この動作に修正するには、以下のようにコードを変更します。

n_steps <- 100
initial_state <- numeric(n_steps)
initial_state[n_steps %/% 2] <- 1
states <- matrix(0, nrow = n_steps, ncol = n_steps)
states[1, ] <- initial_state

for (i in 2:n_steps) {
  states[i, ] <- rule30(states[(i - 1), ])
}

cells <- data.frame(
  x = rep(1:n_steps, times = n_steps),
  y = rep(1:n_steps, each = n_steps),
  state = as.factor(as.vector(states)),
  time = rep(1:n_steps, each = n_steps)
)

plot <- ggplot(cells, aes(x = x, y = y, fill = state)) +
  geom_tile(color = "black") +
  scale_fill_manual(values = c("white", "black")) +
  theme_minimal() +
  theme(panel.grid = element_blank(),
        axis.ticks = element_blank(),
        axis.text = element_blank()) +
  transition_manual(frames = time)

anim <- animate(plot, nframes = n_steps * 4, duration = n_steps, end_pause = 25, width = 600, height = 600, renderer = av_renderer("rule30_animation.mp4"))

この修正により、アニメーションは一般的なセルラーオートマトンの動作に従い、新しい行が追加される度に既存の行が表示され続けます。また、初期状態は真ん中のセルが黒で、他のセルが白になります。このコードでアニメーションが正しく作成されるはずです。
 

出力結果


 

いや、これだとまだ、y=1の行、y=2の行、y=3の行…が順に1行ずつ表示される感じになっています。一度表示した行が消えてしまってます。また、そもそも、このセルラーオートマトンは、x軸がperiodに対応していて左から右に進んでいくものですよね?であれば、表示も、まずx=1の列(真ん中だけ黒くなっている)を表示し、次にその列は表示したままx=2の列を加え……というふうに左から右に広がっていくようにしたいです。
 

ChatGPT

申し訳ありません、理解しました。セルラーオートマトンが左から右に広がっていくように表示を修正するには、以下のようにコードを変更します。

n_steps <- 100
initial_state <- numeric(n_steps)
initial_state[n_steps %/% 2] <- 1
states <- matrix(0, nrow = n_steps, ncol = n_steps)
states[1, ] <- initial_state

for (i in 2:n_steps) {
  states[i, ] <- rule30(states[(i - 1), ])
}

cells <- data.frame(
  x = rep(1:n_steps, times = n_steps),
  y = rep(1:n_steps, each = n_steps),
  state = as.factor(as.vector(states)),
  time = rep(1:n_steps, each = n_steps)
)

cells$time <- pmin(cells$x, cells$time)

plot <- ggplot(cells, aes(x = x, y = y, fill = state)) +
  geom_tile(color = "black") +
  scale_fill_manual(values = c("white", "black")) +
  coord_flip() +
  theme_minimal() +
  theme(panel.grid = element_blank(),
        axis.ticks = element_blank(),
        axis.text = element_blank()) +
  transition_manual(frames = time)

anim <- animate(plot, nframes = n_steps * 4, duration = n_steps, end_pause = 25, width = 600, height = 600, renderer = av_renderer("rule30_animation.mp4"))

この修正により、アニメーションは左から右に広がっていくように表示されます。また、既存の列が表示されたまま、新しい列が追加されます。このコードでアニメーションが正しく作成されるはずです。
 

出力


 

いや、これもおかしいです。3つの意味でおかしいです。
1.セルラーオートマトンが下から上に展開するようになっています。つまり、period(generation, step)がy軸になってしまっています。
2.最初にx=1の列とy=1の行が表示され、次にそれらが消えて、x=2の列とy=2の行が表示され…というふうに、L字型に表示されるようになってしまっています。
3.依然として、既存の表示が残らず、各ステップの情報しか表示されないです。
 

ChatGPT

失礼しました。ご指摘の通り、私の修正は問題点を解決していませんでした。以下のコードで、お求めのアニメーションを正しく作成できるはずです。

n_steps <- 100
initial_state <- numeric(n_steps)
initial_state[n_steps %/% 2] <- 1
states <- matrix(0, nrow = n_steps, ncol = n_steps)
states[1, ] <- initial_state

for (i in 2:n_steps) {
  states[i, ] <- rule30(states[(i - 1), ])
}

cells <- expand.grid(x = 1:n_steps, y = 1:n_steps)
cells$state <- as.factor("white")
cells$time <- 1

for (t in 1:n_steps) {
  current_states <- data.frame(
    x = 1:n_steps,
    y = rep(t, n_steps),
    state = as.factor(ifelse(states[t, ] == 0, "white", "black")),
    time = t
  )
  cells <- rbind(cells, current_states)
}

plot <- ggplot(cells, aes(x = x, y = y, fill = state)) +
  geom_tile(color = "black") +
  scale_fill_manual(values = c("white", "black")) +
  coord_flip() +
  theme_minimal() +
  theme(panel.grid = element_blank(),
        axis.ticks = element_blank(),
        axis.text = element_blank()) +
  transition_manual(frames = time)

anim <- animate(plot, nframes = n_steps * 4, duration = n_steps, end_pause = 25, width = 600, height = 600, renderer = av_renderer("rule30_animation.mp4"))

このコードでは、以下のようにアニメーションが正しく機能するはずです。
セルラーオートマトンが左から右に展開します(x軸がステップ数に対応)。
最初にx=1の列(真ん中のセルだけ黒)が表示され、次にその列が表示されたまま、x=2の列が追加され、以降は左から右に広がっていきます。
既存の表示が残り、各ステップの情報が重ねられて表示されます。
このコードで、期待されるセルラーオートマトンのアニメーションが作成されるはずです。
 

出力結果


 

惜しいです!セルラーオートマトン自体は左から右に展開しており、適切に修正されています。しかし依然として、各ステップの列だけが表示されるので、全ステップが表示から消えてしまっています。そもそも、前のステップが残るようにする上で、コードのどの部分を工夫されようとしてますか?修正意図を教えていただいたら、こっちで解決できるかも知れません。
 

ChatGPT

失礼しました。こちらで再度修正いたしました。今回は、transition_manual()を使わずに、transition_states()とshadow_mark()を使って各ステップで前のステップが残るようにしました。以下のコードが、期待されるセルラーオートマトンのアニメーションを作成できるはずです。

n_steps <- 100
initial_state <- numeric(n_steps)
initial_state[n_steps %/% 2] <- 1
states <- matrix(0, nrow = n_steps, ncol = n_steps)
states[1, ] <- initial_state

for (i in 2:n_steps) {
  states[i, ] <- rule30(states[(i - 1), ])
}

cells <- expand.grid(x = 1:n_steps, y = 1:n_steps)
cells$state <- as.factor("white")
cells$time <- 1

for (t in 1:n_steps) {
  current_states <- data.frame(
    x = 1:n_steps,
    y = rep(t, n_steps),
    state = as.factor(ifelse(states[t, ] == 0, "white", "black")),
    time = t
  )
  cells <- rbind(cells, current_states)
}

plot <- ggplot(cells, aes(x = x, y = y, fill = state)) +
  geom_tile(color = "black") +
  scale_fill_manual(values = c("white", "black")) +
  coord_flip() +
  theme_minimal() +
  theme(panel.grid = element_blank(),
        axis.ticks = element_blank(),
        axis.text = element_blank()) +
  transition_states(states = time, transition_length = 1, state_length = 1) +
  shadow_mark()

anim <- animate(plot, nframes = n_steps * 4, duration = n_steps, end_pause = 25, width = 600, height = 600, renderer = av_renderer("rule30_animation.mp4"))

このコードにより、以下の動作が期待できます。
セルラーオートマトンが左から右に展開する。
各ステップで前のステップが残る。
今回の修正で、お求めのアニメーションが作成されるはずです。
 

出力結果


 

これで完璧になりました。ありがとうございます。
 

ChatGPT

お手伝いできてうれしいです!正しくアニメーションが作成できたとのことで安心しました。
これからも何か質問やサポートが必要であれば、遠慮なくお聞きください。引き続きお手伝いできることがあれば喜んで対応いたします。

TransformerやAttentionの分かりにくい点についてのメモ

 ChatGPTの「GPT」はGenerative Pretrained Transformerの略であり、TransformerというのはGoogleが2017年に発表した『Attention is all you need』という論文で提案されたディープラーニングの画期的なアーキテクチャで、その論文のタイトル通り「Attention」という機構だけを使い倒している点が特徴的である。


 ……という話は色々なところで紹介されているのだが、私のような素人が読むと誤解してしまうような記述も少なくない。
 「仕組みを分かっていない人がいい加減な説明をしている」というよりは、「仕組みをよく知っている人が、素人の気持ちをあまり考えずに説明している」という感じで、悪気は全くないと思うし誤解するのは不勉強なこちらが悪いのだが、読むほうとしては困ることも多い。
 そこで、にわか機械学習ユーザである自分がall you need論文や巷の解説を読んでいて理解に詰まってしまった点について、簡単にメモしておきたい。ただし以下は走り書きなので、他の人が読んで分かりやすいように配慮はしていないし(笑)、間違ってるかも知れません。


 All you need論文はこちら:
[1706.03762] Attention Is All You Need

 入門的な解説はこのシリーズが詳しいかな:
Transformers Explained Visually (Part 1): Overview of Functionality | by Ketan Doshi | Towards Data Science


「アテンション機構のみ」で構成されてるわけではない

 これは中身を知っている人にとっては当たり前すぎて「揚げ足取り」のように思われそうだが、よく知らない人が「トランスフォーマーはアテンションという仕組みだけを使った画期的な手法である」と聞けば、やはり誤解したり混乱したりするのではないだろうか。
 「Attention is all you need」というのは、要するに「RNN(や畳込み)を使わない」という意味だ*1
 トランスフォーマーの内部では、順伝播型のニューラルネットがアテンション機構と同じぐらいたくさん出てきて、そこで色々学習しているので、全く「アテンションのみ」ではない。そもそもトランスフォーマーの場合、狭義のアテンション部分は学習パラメータを持ってないし。

「重要な情報に注目する仕組み」では何も説明できていない

 Attentionは「注意」「注目」といった意味であり、アテンション機構は「入力データのどの部分に注目すればよいかを判断している」と説明されることが多い。言い換えると、データの中で「どの部分が重要か」を判断し、情報に重み付けをしているということだ。
 しかし、「重要な情報に注目して重み付けする」のはただの順伝播型ニューラルネットでも同じなので、「アテンションは重要な情報に注目する仕組みです!」と言われても、何が凄いのかさっぱり分からない。
 実際の意味合いとしては、ふつうのニューラルネットにベクトルを投入して学習されるパラメータは「ベクトル内の各要素についての重み付け」を表しているのに対し、アテンションがやっているのは、「ベクトルがたくさんならんだ系列データ」を扱う際に、ベクトル間の関係を重み付けする仕事という感じだが、なんかうまい言い方を考える必要があると思う。

Attentionには系列「全体」の意味を捉えるという役割もある

 アテンション機構は、系列内のとある要素(ベクトル)から見て、ほかの要素のどれが重要なもの(強い関連性を持つ)であるかを判断している。しかし同時にそれを通じて、「系列全体の情報を取り込む=混ぜ合わせる」という役割を果たしてもいる。この両面性こそが重要なのだと考えるべきだろう。
 「アテンションは重要な情報に注目する仕組みです」と言われてしまうと、情報全体の特定の部分だけを活用する仕組みのように思ってしまう人が多いのではないだろうか。それはそれで、ある意味では正しい。しかしこの処理は、「重要度に重みをつけながら、系列全体の要素間関係を捉える」処理だとも言えて、「部分に向かう」と「全体に向かう」の両方向な役割を持っているのである。

「エンコーダ-デコーダ」モデルから説明しないと何も分からない

 上記2点については、Transformerが出てくる前の背景としてRNNベースの「エンコーダ-デコーダ」モデルが頭に入っていると誤解はしないのだが、その説明をすっ飛ばしてしまうと、たぶんあまり仕組みが理解できないと思う。だからもちろん、背景から説明されていることも多いのだが、そこがショートカットされている説明も散見される。


 もともと、RNNベースのエンコーダ-デコーダモデルにおいては、

  1. 1つ目の単語から順次処理して次に送っていく過程で、最初のほうの単語の情報が薄まってしまう(長期記憶が弱い)
  2. エンコーダからデコーダに渡される文脈ベクトルが固定次元なので表現力に乏しい
  3. 逐次処理なので計算に時間がかかる

などの問題があった。で、エンコーダとデコーダをアテンション機構*2でつなげることで大域的な文脈を失わずに処理するという工夫が提案され、1つ目と2つ目の問題に対処することが可能になった。そしてトランスフォーマーは、それをさらに押し進めてRNNブロック自体を排除してしまい、セルフアテンション等を用いて系列内の単語間関係を捉えるようになっている。また、RNNがいなくなったことで、計算も並列化できて速くなったとされる。

クロスアテンションのQに投入するのは「1つ前の単語」

 トランスフォーマーには2種類のアテンションが使われていて、一つが「self-attention」、もう一つは「encoder-decoder attention」と呼ばれている。後者は、source-target attentionとかcross-attentionとか呼ばれることもある。以下では、短くて呼びやすいので後者を「クロスアテンション」と呼ぶことにする。


 セルフアテンションは、入力又は出力の各系列の内部で単語間の関係を捉えるのに使われており、クロスアテンションは、入力の系列に含まれる単語と出力の系列に含まれる単語の関係を捉えるのに使われている。セルフアテンションはトランスフォーマーで一番重要とも言える機構であり、何をしているのかは分かりやすい。
 一方、後者のクロスアテンションの仕組みは、たとえば「This is a pen」→「これはペンです」という翻訳において「pen」と「ペン」が強く関係することを表現しているのだと説明される。それはまぁそうなのだが、このイメージを強く持ってしまうと、実際の処理をみていくときに混乱する。というのも、デコーダのクロスアテンション層でQ(クエリ)に投入されるのは、出力しようとしている単語の「1つ前」の単語の情報だからだ*3
 つまり、「ペン」を生成するときは、「は」を表すベクトル*4をクエリとして、「this」「is」「a」「pen」のそれぞれとの関連の強さを計算し、その強さで重み付けしながらバリューを足し合わせたものがアテンション情報として使われる*5。このアテンション情報が「ペン」の生成に使われることになるので、「ペン」との対応関係をみていると説明しても全く間違いではないのだろうが、「ペン」をクエリにするわけではないことに留意を促さないと、わけがわからなくなる。

何が入ってきて何が出ていくのか

 トランスフォーマーはけっこう部品が多いので、各部品の入口と出口で何が流れていくのかをいちいちハッキリさせておかないと、頭がこんがらがる。出入りするのがベクトルなのか行列なのかとか、次元は何なのかとかを毎度確認しないと、自分の理解が合ってるのかどうか不安になるのだが、そこが述べられていない説明を目にすることは多い。


 で、結局のところ、エンコーダにせよデコーダにせよ、トランスフォーマーブロックの中では常に「単語数×埋め込み次元数」の行列が流れていくと考えておけばよいと思う*6
 embedding層の前では各行が語彙数次元のone-hotベクトルになっていたり、最後の出力時には語彙数次元の確率分布ベクトルだったりするのだが、トランスフォーマーブロックの中では、「単語数×埋め込み次元数」の行列の各行にひたすら色々な変換が施されていく感じになっている(ただし各行が独立に処理されるという意味ではなく、セルフアテンションする時には行間=単語間で互いに影響し合うことになる)。
 逆にいうと、RNNの隠れ状態のような文脈ベクトル、つまり文意を1つのベクトルで表したものは生成されない*7
 あと、デコーダについては、仮に出力文のj番目の単語を出力しようとしている時には、

  • 入力されるのは、BOSトークンからj-1番目の単語までの意味ベクトル
  • 出力されるのは、1番目の単語からj番目の単語までの意味ベクトル

 というふうに「1個右にズレる」という点も、確認しておかないと頭がこんがらがるかもしれない。

KとVは同じもの(セルフアテンションではQも同じ)

 アテンション層の「Q」「K」「V」はクエリ、キー、バリューなのだが、データベース用語のイメージをあまり持ちすぎないほうがいいと思う。とくにセルフアテンション層ではQもKもVも同じ行列が投入されるので、「データベースにクエリを投げて、キーで探してバリューを取ってくる」みたいなイメージを持っていると、何をしてるのかわけが分からなくなる。
 クロスアテンション層では、KとVはどちらも「エンコーダの出力行列」であり、これはすなわち、各単語の意味ベクトル(を色々変換しまくったもの)を重ねたもので、全く同じものがKとVに投入される。ただし、アテンション層の入り口に線形層があって、ここでKとVには別々の変換が施されることになるので、結果的には別物になる。なお、Qはデコーダ側の入力単語ベクトルを重ねたものである。
 セルフアテンション層に至っては、Qも同じものが投入される。つまり、全く同じ行列を3つの入口に入れて、それぞれ「別の線形変換をした上で」アテンション処理が行われるという流れになっている。

逐次処理がなくなったわけではない

 推論時のデコーダ部分では、最初にBOSを入力すると1つめの単語が出力され、その1つ目の単語を入力すると2つ目の単語が出力されるというように、逐次的なループ処理になっている。並列的に処理する方法も考案されているのだが、ループさせたほうが精度は高いらしい。
 つまり、RNNを取り除いたと言っても、完全に逐次処理がなくなったわけではない(エンコーダと、学習時のデコーダは並列処理になっている)。

マルチヘッド処理では埋め込みベクトル自体が分割されてる

 トランスフォーマーのアテンションは「マルチヘッド・アテンション」と呼ばれ、系列を構成するベクトル間の関係を「複数の観点で」捉えるようになっている。
 イメージでいうと、「今日は大学で二次試験が行われていた」という文において、「二次試験」という語は文法的には「行う」との間に動詞-目的語という強い関係を持っているが、「二次試験」と「大学」の間にも、場面や文脈上の重要な関係がある。
 この「複数の観点」の学習は、ヘッドを複数用意するだけでいい感じにやってくれるんだろうか?と一瞬思ったのだが、もちろんそんなわけはなく、単語の埋め込みベクトル自体をチョン切って個々のヘッドに与えている。だから、ある意味もはや「別のデータ」を扱っているとも言える。
 たぶん、この「チョン切り」にうまく適応するように、埋め込み層の処理も学習されていくので、埋め込みベクトル自体が意味不明な数字の羅列ではなく、ある観点で重要な特徴次元が近い位置に集まるんだろう。面白い。

*1:論文では「RNNや畳込みを使わず、セルフアテンションに全面的に依拠してデータを処理する、初めての系列変換モデルである」と書かれている。

*2:ちなみに当初のアテンションは、Transformerのアテンションとは仕組みがけっこう違う。

*3:ちなみにRNNベースのエンコーダ・デコーダモデルでも、デコーダ出力の1つ前の隠れ状態と入力系列の関係がアテンションとして計算される

*4:セルフアテンションで自分より前の単語との関係が情報として織りこまれるので、もはや純粋に「は」を表すベクトルであるとは言えないかも知れないが。

*5:正確にいうと、デコーダにはから出力単語の1個前までのベクトルを重ねた行列が投入され、(BOSの次にくる)文頭単語から出力単語までのベクトルを重ねた行列が出てくるようになっており、ここではその最後の行だけに着目している。

*6:マルチヘッド化する時に一時的にちょん切られるが、すぐに結合される。

*7:細かいことを言うと、BERTで文章の区切り部分に入るトークン(CLS)のベクトルなんかは、文全体の意味を表しているとも言われるが。

Rで距離行列を求める高速な関数

Rでクラスター分析などをするときに距離行列を求める必要があるが、次元が大きくなると(万単位とか)けっこう時間がかかる。
で、

  • 標準で入ってる{stats}のdist()
  • {Rfast}のDist()
  • {wordspace}のmatrix.dist()

を比較してみたところ、3つ目のやつが一番速く、100倍ぐらい高速になった。
以下は、5000行かける768列の元データに対して、各行のあいだの距離を求めた例です。
いずれも、いろんな距離を求めるオプションがあるが、{wordspace}のmatrix.distだけはデフォルトがコサイン類似度になってるので、ユークリッド距離にするならmethod='euclidean'をかく必要がある。

ちなみにstats::distの場合でいうと、1000行なら8秒だったのが、5000行にすると5分近くかかるようになるので、探索的に(階層的)クラスター分析をする上ではここの速度はかなり重要。

> t <- proc.time()
> distance <- stats::dist(mat)
> proc.time() - t
   user  system elapsed 
288.061   0.004 287.934 
> t <- proc.time()
> distance <- Rfast::Dist(mat)
> proc.time() - t
   user  system elapsed 
  7.982   0.000   7.979 
> t <- proc.time()
> distance <- wordspace::dist.matrix(mat, method='euclidean')
> proc.time() - t
   user  system elapsed 
  2.131   0.164   2.297 

細かい違いも一応メモっておく。

  • 入力データ
    • stats::distは、matrix型でもdata.frame型でもよい
    • Rfast::Distは、matrix型でもdata.frame型でもよい
    • wordspace::matrix.distは、は、matrix型じゃないとダメ
  • 出力データ(stats::hclustやfastcluster::hclustに与えるときの話*1
    • stats::distは、dist型なのでそのままhclustに与えてOK
    • Rfast::Distは、as.dist()してからhclustに与える
    • wordspace::matrix.distは、は、as.dist()してからhclustに与える
  • デフォルト
    • stats::distは、ユークリッド距離
    • Rfast::Distは、ユークリッド距離
    • wordspace::matrix.distは、コサイン類似度

*1:fastcluster::hclustは自分がやった範囲ではstats::hclustとあまり変わらない。

configure: error: gsl-config not found, is GSL installed? ERROR: configuration failed for package ‘RcppGSL’

こういう環境構築系の作業をいちいち書いてたらキリがないのだが、ググってすぐに解決しなかったものについては、後で助かる人がいるかもと思ってなるべく書いています。
以下はAWS(Amazon Linux)上のRでの話です。
{Rfast}というパッケージを入れる際に{RcppGSL}が必要で、

configure: error: gsl-config not found, is GSL installed?
ERROR: configuration failed for package ‘RcppGSL’

というエラーが出たので、以下のようにGSLというライブラリを入れたのだが、

$ sudo yum install gsl

下記のページに書いてるように、gsl-configが入ってないのでこれだけではダメとなる。
How to fix RcppGSL installation error gsl-config: Command not found - TechOverflow

で、Amazon LinuxはcentOS系なのでパッケージ管理がapt-getではなくyumであり、yumで入れる方法が必要なのだが、libgsl2ではなくgsl-develというのを入れればよいようだ(ここに書いてあった)。

$ sudo yum install gsl-devel

あとはこれで、

install.packages('Rfast')

すればよい。

文章を「カギ括弧に挟まれている場合以外は句点で区切る」のってどうすればいいの?

日本語のパラグラフをセンテンス単位に分割するのって、もちろんいろんなパターンをプログラムで書いていけばできると思うのだが、シンプルなコードでやろうとすると、どうなるんだろうか。
普通に考えると、

  • 「。」で区切る
  • 「?」や「!」でも区切る(これらは連続するときもある。まあ句点の連続も媒体によってはあり得るが。。。)
  • カギ括弧等(丸括弧や二重カギ括弧なども含む)に挟まれている「。!?」では区切らない
  • 改行で区切る

ぐらいのルールで区切れば、だいたいの場合はカバーできそうな気はする。もちろんこれら以外に、

  • 文中で改行されていると思われる場合は改行を無視する(素材によってはそういうものは存在しないと前提できることも多い。)
  • カギ括弧が入れ子になっている場合の対処

などを考え始めるともっとややこしくなるが。


で、Rの文字列操作で、「カギ括弧で挟まれている場合以外は句点で区切る」というのを正規表現でやろうとしたのだが、正規表現になれていないこともあり、頭がこんがらがってなかなか上手くいかない。
とりあえず、{stringr}のstr_extract_allで、


「カギ括弧開くから最短で閉じるまで or 句点を含まない文字列」の1回以上繰り返し or 句点を含まない文字列


という条件で抽出してみると、1センテンスに複数回セリフが出てくる場合や、セリフのなかで句点が複数回出る場合も含めて、抽出することができた。
以下の最後の一文のように、カギ括弧が入れ子になってると、最短マッチにしてるので崩壊する。

文例:
今日、高速道路を走っていたら後ろからパトカーが迫ってきて、「はいグレーのフリード左に寄せて停まってください。」と言うので、恐る恐る路側帯に停車した。お巡りさんが2人、降りてきた。1人が「今、進路変更するとき合図出てなかったですね。」と言い、もう1人は「ちょっとスピードも出過ぎですね。まぁ、20キロぐらいしかオーバーしてないし、今日のところは合図不履行だけで切っときますけど。」と言う。私は、「すいません、出したつもりだったんですけどね……」と答えた。最近、立て続けに「シートベルト」「一時停止」で違反を取られていて、もううんざりだ。興味本位でお巡りさんに、「そういえば昔、茨城県警に「40キロ以上オーバーしないとスピード違反は切らないよ。」と言われましたね。大阪府警も似たような方針ですか。」と訊いてみたのだが、ノーコメントだった。

> x1 <- "今日、高速道路を……省略……ノーコメントだった。"
> x2 <- str_extract_all(x1, "((「.*?」)|([^。]))+|[^。]+")
> x3 <- paste(x2[[1]], '。', sep='')  # お尻に。を付けなおす
> print(x3)
[1] "今日、高速道路を走っていたら後ろからパトカーが迫ってきて、「はいグレーのフリード左に寄せて停まってください。」と言うので、恐る恐る路側帯に停車した。"                                                        
[2] "お巡りさんが2人、降りてきた。"                                                                                                                                                                               
[3] "1人が「今、進路変更するとき合図出てなかったですね。」と言い、もう1人は「ちょっとスピードも出過ぎですね。まぁ、20キロぐらいしかオーバーしてないし、今日のところは合図不履行だけで切っときますけど。」と言う。"
[4] "私は、「すいません、出したつもりだったんですけどね……」と答えた。"                                                                                                                                            
[5] "最近、立て続けに「シートベルト」「一時停止」で違反を取られていて、もううんざりだ。"                                                                                                                          
[6] "興味本位でお巡りさんに、「そういえば昔、茨城県警に「40キロ以上オーバーしないとスピード違反は切らないよ。」と言われましたね。"                                                                                
[7] "大阪府警も似たような方針ですか。"                                                                                                                                                                            
[8] "」と訊いてみたのだが、ノーコメントだった。"  


!や?も入れる場合は、「!?」とその繰り返しにも対応するように区切り文字を[^[。!?]+]*1というふうにしておくといいが、

"((「.*?」)|([^[。!?!?]+]))+|[^[。!?!?]+]+"

その場合は「お尻に句点を付け直す」のは無駄なのでやめる。
入れ子問題以外にもいろいろ落とし穴がありそうだが、単に句点で区切るよりはだいぶマシではある。いま、新聞記事のコーパスを解析していて、新聞はそんなに変な表現が出てこないから、これぐらいシンプルなルールでもまぁまぁ行けそう。係り受け解析器とかも駆使するとさらに正確にできるのかもしれないが、知識が追いついてないのと、今やってるタスクではクオリティよりも高速で処理することのほうが優先なので、ひとまずこれ以上は考えずに進めようかな。


入れ子問題も、要するに一番外側のカッコだけ見て、その中身は何でもいいと判断すれば良いわけなので、ある程度単純なルールで行けそうな気はするのだが、自分のスキルではパッと思いつかない。「一番外側のカッコ」を最長マッチで見てしまうと、
●●「◯◯」●●。●●「◯◯」●●。
という文が一文になってしまう。先読みとか戻り読みとかを使うんかな。カッコの開閉の個数が一致することと、上位階層のクラスをまたいで開閉することはできないというルールを入れる必要があるので、正規表現でやることではないのかもしれないが。


参考記事:
日本語の文章をいい感じに文区切りするライブラリを作った - Qiita

*1:これの内側の[]を()にしてもちゃんと動いたのだが、なんで…?