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

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

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:これの内側の[]を()にしてもちゃんと動いたのだが、なんで…?

AWSのR4.0.2に{devtools'}が入らない

AWSに設置したRStudio Serverで{devtools}のインストールに失敗し、以下のように依存関係のエラーが出る。(途中の行を省略している)

> ERROR: configuration failed for package ‘textshaping’
> ERROR: dependency ‘textshaping’ is not available for package ‘ragg’
> ERROR: dependency ‘ragg’ is not available for package ‘pkgdown’
> ERROR: dependency ‘pkgdown’ is not available for package ‘devtools’


たどっていくと、OS側でfribidiとかlibtiff4がないとかいう話なのだが、似たような問題で躓いてる人のコメントをググりながら以下のようなライブラリを全部インストールしたら問題が解決した。どれが効いたかは知らんw

$ sudo yum install libfreetype6-dev freetype-devel libpng-dev libjpeg-dev libtiff-devel libtiff5-dev libjpeg-turbo-devel


Rで以下のパッケージを入れる。

install.packages(c('textshaping’, 'ragg', 'pkgdown'))


すると{devtools}が入るようになる。

install.packages('devtools')


Amazon LinuxはCentOS 7に似ているとのことだが(リンク)、ツール類が最低限に絞られているようで、何かが足りなくて使いたいソフトのインストールが出来ないという現象にけっこう出くわす。Amazon LinuxではなくUbuntuやCentOSのAMIを選択したほうがストレスがないのかもしれない。

Rで{snow}と{parallel}の並列化を少し試してみた

Rで計算を高速化したいとき、

  • なるべくベクトル計算にしてforで頭からみていくような処理を避ける
  • 自作関数をコンパイルする
  • 並列化(マルチコアの利用)

などの手法があり、ベクトル化とコンパイルに関してはケースによって何が有効かというのは難しい。
ただ、forで頭から順に見ていく処理(前後の依存関係がなく、処理順を入れ替えたり、カタマリに分割しても問題がないようなもの)をやる場合に、複数のスレッドに分けて並行処理すればその分速くなるというのは直観的に理解しやすい。


色々試したわけではないので、どういう場合にどの程度並列化が有効であるかとかはよく分かってないのだが、取り急ぎコードの書き方を忘れないため程度のメモとして、以下に書き写しておく。
なお、並列化すると、時間を測った場合のuserとsystemの値がおかしくなるが、elapsedの値は合っている。(一応ストップウォッチで確認した。)

> library(snow)
> library(parallel)
> detectCores(logical = FALSE)  # 物理コア数の確認
[1] 4
> detectCores(logical = TRUE)  # 論理コア数の確認
[1] 8
> 
> # 処理の中身はどうでもいいが、ここでやっている処理が何かというと、
> # faid_allという文字列のベクトルがあり、これを頭から順にみていって、
> # d_all3というデータフレームのfile_article_idという列の値と一致する
> # 行の、content_without_tagという列に入っているテキストを連結して
> # text_allという新しいベクトルに追加していく。

> # 並列化しない場合
> t <- proc.time()
> text_all <- c()
> for(i in 1:length(faid_all)) {
+   target.rows <- which(d_all3$file_article_id==faid_all[i])
+   text.sep <- d_all3[target.rows,]$content_without_tag
+   text.paste <- paste(text.sep, collapse = '\n')
+   text_all <- c(text_all2, text.paste)
+ }
> proc.time()-t
   user  system elapsed 
 64.819  12.420  77.050 

> # {snow}のsocketで並列化
> cl <- makeCluster(8, type="SOCK")  # クラスターを8つ立ち上げ
> clusterExport(cl, c('d_all3','faid_all')) # オブジェクトをクラスターにコピー
> # クラスターを立ち上げてオブジェクトをコピーするところで10秒ぐらいかかるので、時間はここから測る。
> t <- proc.time()
> text_all <- parSapply(cl, faid_all, function(p){
+   target.rows <- which(d_all3$file_article_id==p)
+   text.sep <- d_all3[target.rows,]$content_without_tag
+   text.paste <- paste(text.sep, collapse = '\n')
+   return(text.paste)
+ })
> stopCluster(cl)  # クラスターを立ち上げ(忘れないように!)
> proc.time()-t
   user  system elapsed 
  0.098   0.054  16.365 

> # {parallel}のforkingで並列化
> t <- proc.time()
> text4 <- mclapply(faid_all, function(p){
+   target.rows <- which(d_all3$file_article_id==p)
+   text.sep <- d_all3[target.rows,]$content_without_tag
+   text.paste <- paste(text.sep, collapse = '\n')
+   return(text.paste)
+ }, mc.cores = 8)
> proc.time()-t
   user  system elapsed 
 36.652   8.208  16.077 


forkingとsocketの違いはあまり理解できてないが、socketは使用する変数を全て、各コアにコピーしなければならないらしく、そこの処理で少し余計に時間がかかる。
自分の場合、Macの物理コアが4、論理コアが8で、4本に並列化すると4倍ぐらいの速度にはなった。上の例のように8本にしても8倍にはならないのだが、4本よりは少し速かった。


以下は、AWSでvCPUが32個のマシンを借りて、とある処理にかかる時間を並列化の本数ごとに計測したもの。1回ずつしか測ってないので誤差がある。
途中からほとんど水平だが、並列化しても意味がないというより、たぶん「とある処理」の中で並列化と関係ない部分で30秒ぐらいかかってるんだろう。(vCPUは仮想CPUのことなので物理的なコア数のように比例的に増えないというのもあるかもしれないが。)