8本目!Design of Chatbot with 3D Avatar, Voice Interface, and Facial Expression

とりあえず、この論文は実際の実装ではなく、文献的を参考にした設計です。

まあ、作りたいやつの参考になりそうなので読んでみました。

構成としては Chatbot, Speech Recognition, Speech Sythesis, Avatar の設計と、その組み合わせの構成となっている。

まあ、ゆるりゆるりと見ていきましょう。

Chatbot

まあ、テキストを入力としてテキストを出力する部分。

既存のChatbotでは語彙が貧弱となって、しかも何か発言内容が攻撃的や性的になるらしい。参考はここ。まあ、なんか自覚があるからなんとも言えない(´・ω・`)

Speech Recognition

いわゆる音声認識。こいつのプロセスは以下のようになる。


STT ┳ front end process ┳ convert audio stream to numaric values
        ┃                             ┣  separate numaric values to voice segments
        ┃                             ┗  charactarize the vocal segments
        ┃
        ┗ back end process ┳ recognize the speech pattern with the acoustic model
                                          ┣ extract pronunciation of each word from lexicon lists
                                          ┗ recognize how the words are combined with the languagemodel

基本的には音声をデジタル変換して、データを分割後にそれぞれの単語をマルコフ決定過程をもちいて、単語を認識するっぽい。

他には 動的時間収縮法とか、深層学習とかを使ったものがあるらしい。 GoogleのSTTは深層学習のWavenetの改良を使ってたりするんかな?

Speech Synthesis

いわゆる音声合成

まず、Front end の処理について。はじめにテキストを単語分割をするらしい。んで、単語にそれぞれ発音記号を当てはめたあとに、単語間の関係をフレーズ、句、文の関係のかかり受け分析をするみたい。

次に、Back end の処理について。Front endで作られた構造データを利用して、単語の音声変換、音調の調節、音素の持続時間の計算とかして、音声を合成するらしい。

TD-PSOLAとかCELPとかHMMとかいろんなモデルがあるみたい。GoogleがWavenetを使っていることしかわからん(笑)。

Avatar

2D or 3Dで表現。表情をつけると、Avatarがより人間に近く知覚されるらしいね。

Body Languageとかはどうなんだろう?

Methodology

  1. Input voice
  2. convert voice to input sentence
  3. process the sentence to make the response
  4. convert the response to out voice
  5. synthesis the voice
  6. output the avatar with the voice using lipsync
  7. express emotions with voice, face and body language

みたいな流れ。

うーん。なんかしょぼい気がする。

7本目!The Desing and Implementation of XioIce, an Empathetic Social Chatbot

さーて、ようやくやってまいりました、Microsoft謹製のChatbotである、XiaoIceです!

XiaoIceって聞いたことはないと思いますが、じつを言うとこの子は日本ではRinnaと呼ばれています。そうっ!あのRinnaに関しての論文ですっ!

ということで、程々に気合を入れてがんばりましょう!

Design Principle

RinnaのデザインのコンセプトとしてはIQ(Intelligent Quarity)とEQ(Emotinal Quarity)を統合したものだそうだ。IQによりユーザーの要求するタスク(質問とか)に答えて、EQによりユーザーの感動などの感情的欲求を満たすことができるらしい。これの元論文は A theory of human motivationっていう、なんと戦前の研究がもとになっている。余談だけど、バリバリこの研究とか徴兵令に利用されている気がする。

そして、このEQとIQを組み合わせたモデルにPersonalityを組み込んだものが、Rinnaとなる。

IQ + EQ + Personality

IQのコンポーネントに含まれるのは、知識と記憶のモデリング、画像と言語についての理解、意味づけ、生成、推測であるらしい。んで、Rinnaは2015年時点で230種類以上のこのIQに関するスキルを持っていたらしい。スゴっ!

まあ、一番重要なのは Open-Domainの会話をつづけるタスクらしいけど。 (Open-Domainはあらゆる質問って意味。)

EQは共感と社会的技能がコンポーネントに含まれるみたい。

Empathyは相手の感情を理解することで、このタスクの実行のために、感情の特定、感情の発展状況の特定、感情に起因するユーザの求めているものの特定、が行われているそうな。

社会的技能に関しては、ユーザのバックグラウンドやそのときに置かれている状況や個性に合わせて、ユーザーが喜ぶような解答をするらしい。あと、ユーモアだとかホメコトバとかもSocial Skillに含まれるそうで、また会話が途切れたときに新しい会話を始めるのもSocial Skillだそうだ。

Personality は個人に固有の特性のことで、信用と長期的なつながりを作るのに必要らしい。 個性の設計というか、XiaoIceの設定は18歳の信用でき、共感的で、愛情深くて、しかもユーモアのセンスもあるっていう、超絶最強ヒロインの設定にしたらしい。

まあ、男性好きの男性よりも女性好きの女性の方が多そうだし、人気の出そうな年齢と性別の最大尤度のところを取ったっぽいな。

あと、個人的な質問やセクハラ発言が来た場合はうまくスルーするように設計されているらしい。すげーな(笑)。

Social Chatbot Metrics: CPS

Meena ではSSAっていう、どれだけ具体的な会話が続けられるかっていうことを指標にしていたけど、Rinnaっではただ単にどのくらいの期間、どれだけの会話が続けられたのかってことを指標にしているらしい。

まあ、Meenaの目的がChatbotの問題点である、曖昧な解答をしがちっていうのを回避することだったけど、Rinnaの目的は長期的な会話の持続が目的だったから、その点では良いのかな?

Social Chat as Hierachical Decision-Making

Rinnaでは top-level process managerって言うのが、まずskillを選んで、会話モデルの制御をしているみたい。

まあ、このskillの選び方が気になる。

んで、このhierachical decision-making process をマルコフ決定仮定に置き換えるらしい。 ってことは強化学習か!

マルコフ決定の基本式はTを遷移関数、aを行動、sを状態、gを報酬として、


T = P(s_{t+1} = s | s_t = a, a_t = a) \\
g(s, a)

と表される。今回で言えば、sがユーザーの状態で、aがskillに当たるのかな? でも、報酬関数gの計算がわからん。gを計算して、どうTにフィードバックするんだっけ?

調べたら、すべての状態sと行動aについて、確率分布Qを予め作っておき、(この時点のでの、Q値の表はsの数をN個、aの数をM個として、NM個できる)、ある時点tにおいて、状態s_tと行動a_tが発生した場合、状態s_t+1に遷移する。そのときに、以下のようにQ値を更新するらしい。


Q(s_t, a_t) \leftarrow (1 - \alpha)Q(s_t, a_t) + \alpha(r_t + \gamma \; max_a \; Q(s_{t+1}, a)
)

まあ、ユーザーとの会話内容から、sをどうやって決めるのか?とか、aはどんだけあんのか?とか、Qの初期値はどうするんだ?とか報酬rの決め方はどうするんだ?とかQ値の更新後のactionの決め方はどうするんだ?とか色々と疑問は尽きない(笑)

actionの決め方についてはε-greedy方とか、ボルツマン選択とかあるらしい。

ε-greedy選択: εの確率でランダム。それ以外は最大のQ値を持つ行動の選択。

ボルツマン選択 : exp(Q(s,a)/ T)に比例した割合で、行動選択する。Tは時間とともに0に地下づくパラメータ。

状態s_t+1とQ値を使った行動選択はあんのかな?

ああ、 max_a Q(s_{t+1}, a)でε-greedy法を使えば良いのかっ!

あと、多分εの確率で新しいskillやDialogue policyを提供するみたい。

System Architecture

f:id:kaya-takashiro:20210831174548p:plain
rinna architecture

User experience Layer

Full DuplexとMessage-based conversationがあるみたい。 Message-based conversationが普通に思い浮かべるRinnaとのLINEでの会話で、Full Duplexが2018年末ごろの「リンナと音声通話」っていう機能らしい。 全く知らんかった(笑)

Converstioan Engine layer

Dialogue Manager がMDPに基づいて、(1 - ε)の確率で、既存の skill か Core Chat を選ぶか、εの確率で新規のskillかCore Chatを選ぶかを決めているらしい。 んで、Empathetic Computing moduleでユーザの感情を測定し、Dialogue Managerのstateの選択に反映しているらしい。

Data layer

知識グラフだとか、Topic Indexだとか、検索ベース用のデータとか、ユーザープロフィールとか、Rinnaのプロフィールだとか、画像検索用のデータとかがあるみたい。 知識グラフとかどう使うねん(笑)。

Implementation of Conversation Engine

この会話エンジンはDialogue managerとempathetic computingとCore Chatとskillで構成されているみたい。

会話エンジンの実装の評価にはA/Bテストが用いられている。えっ?A/Bテストって?

なんか、新しく実装したパターンをいくつかランダムにユーザーに提示して、一番良かったやつを採用する手法らしい。評価方法はNumber of Active UsersとConversation-turns Per Sessionが用いられたみたい。

Dialogue Manager

Dialogue Manager 
┣ Global State Tracker
┗ Dialogue Policy
┣ Selectiong either Core Chat or skill to activate
┣ A set of low-level policies for each skill
┗ Topic manager for Core Chat

まず、Dialogue Manager は基本的にマルコフ決定仮定に基づいているため、現在ある状態sにあるとして、方策πを用いて、 \pi(s) = a として、行動aを決定する。この行動aはskillの選択か、Core Chatで、ユーザーの入力やskill固有の方策による応答で決まるらしい。

よくわからん。

Global State Tracker

Global State Trackerはユーザーの入力とRinnaの応答を固有表現と感情のラベルを組み合わせて保存するらしい。

てか、なんの固有表現を抽出するんだ?名前?職業?

Dialogue Policy

2種類あって、skill か Core Chatを選ぶパターンと、skillを使用して、conversation segment にあったときに、skill 固有のactionを選ぶパターンみたい。

なんか、skill or Core Chat を選ぶときはskill triggerによって実行されるらしいんだが、このskill triggerってのがわからん。rule baseで候補を選んで、あとは確率をQ値で出して実行とか?ちゃうか...。

  1. ユーザーの入力
  2. keyword抽出
  3. まず、ユーザーの入力から、Topic Mangerとか、Domain Chat を呼び出す。
  4. keyword を用いて、rule ベースでskillを呼び出す。

low-level policies についてはCore Chatとskillについてのものがあるらしけど、違いがわからん。skill呼び出したら、そのskill内でpolicyを呼び出すってことでいいのかな?

疑問点として、何回かTurnが必要なskillとかがあったらどうすんだろう? そうしたら、次の方策を強制的にそのskillに変えるとか?

high level policyの流れ

  1. もし、ユーザの入力がテキストだった場合、Core Chatが開始され、Topic ManagerかGeneral Chatが応答される。
  2. ユーザの入力がビデオとか画像なら、Image Commentingのskill使用。
  3. 特定の入力と会話の文脈により、でTask Completion, Deep Engagement, Content Creation
  4. 複数のskillがTriggerされた場合、triggerの信用スコア、事前に設定した優先度、会話のsessionに基づいて決定するらしい。

会話のsessionってなんぞや?

Topic Manager

Topic Manager はNeural Networkによって、以下の条件を満たしたと判別される場合、実行されるらしい。

  1. Core Chatが失敗して、Editorial Responseってやつが実行された場合。
  2. Core Chat で生成された文章がユーザーの入力とおんなじだったり、ただの相槌の場合。
  3. ユーザーの入力が相槌だけだった場合。

英語だと、相槌は殆ど無いけど、日本語はかなり相槌が多いのはどうカスタマイズしてんだろう?

多分、このただの相槌の判定にNeural Networkが用いられているんだろうな。

んで、このtopicの選び方について。まず、Topicのデータベースって言うのがあるらしい。アメリカならInstagram, 中国なら Doubanらしい。日本はTwitterかな?知らんけど。 でも、どうやってTopicを集めたんだろう?Hashタグ?

empathetic computing moduleで作られた状態s = (Q_c, C, e_Q, e_R) にQueryとして、上位のtopicを選んで、その中でrankづけするらしい。rankづけのやり方は、

  • 文脈とtopicとの関係性
  • ニュースに関連するTopic の新鮮さ
  • プロフィールに基づいたユーザーの興味
  • インターネットやRinnaでのTopicの人気度
  • Rinna全体にTopicが受け付けられた割合

以上に基づいてrankづけして、Adaboostで最適化されるらしい。

Empathetic Computing

  1. ユーザーの入力Qがあったとして、以前の文脈Cから補完して、Q_cに書き換える。
  2. ユーザの感情や状態を抜き出して、e_Qに書き出す。
  3. ボットの感情や状態を抜き出して、 e_Rに書き出す。
  4. 状態 s = ( Q_c, C, e_Q, e_R)として、Dialogue Managerにぶち込む。

疑問としては、日本語の文章補完と、ユーザーの感情や状態って何を抜き出すのかってこと。

Contextual Query Understanding

ようするに、前の文章から情報を抜き出して、省略とか埋めましょうってやつ。

忖度が命の日本語ではそうとう難しいタスク(笑)

主語はほぼ省略するし、倒置法とか擬人化もバリバリ使う言語だし...(;_;)。

まあ、それはおいておいて基本的なタスクは3つ。

  1. 固有表現を抜き出して、メモリーに保存する。
  2. 「彼」とかの代名詞の共参照を置き換える。
  3. 前の文章を抜き出して、付け足す。

疑問点としては、固有表現の保存って何を保存するんだろう?

論文を呼んだ感じ、Occupationとか、Genderとかがそれに当たるのかな?多分違うな。わからん。

User Understandig

Q_cとCからe_Qを作るやつらしい。

e_Qっていうのは

f:id:kaya-takashiro:20210901193930p:plain
persona

みたいな表のことらしい。

それぞれの項目について、

  • Topic: 現在のTopicについて。論文だと、ユーザーが新しくTopicを始める場合があるっていうんだけど、その場合はどうやってTopicとか抽出してるんだろう? Dialogue Managerにはempathetic computing からできた状態sを入れるわけだから、topic manager でtopicの抽出をするわけじゃないし。

  • Intent: ユーザの対話行為の場合分け。挨拶だとか質問だとかetx...。 どうやって11種類に場合分けしてんだろう?ルールベースなわけないし、Neural Networkかな?

  • Sentiment : ユーザーの感情の場合分け。幸福、悲しみ、怒り、通常、感情の変化の合わせて、5種類。 疑問点としては、どうやって場合分けしてんのかってことと、感情の変化っていうひとくくりにして良いのかってこと。

  • Opiniton: ユーザのtopicに対する姿勢でpositive, negative, neuralの3種類あるみたい。 小説の話をしていて、この作家が嫌い(negative)とか好き(positive)とかの違いとか?

  • persona: AgeとかGenderとか。正直どう使うのかわからん。

persona以外の項目はデータの個数が決まっているから、

[e_Q = ([201, 5, 2, 1])]みたいになんのかな?

Topicの個数が決まっているのかは知らんけど。

なんか、どんどん疑問が増えていく(笑)

Interpersonal Response Generation

e_Rについて作成するコンポーネントらしい。

Topicは同じ、Opinionはpositive, Sentimentはhappyで作られているらしい。

でも、自分がnegativeかつsadのときに相手がめちゃくちゃpositiveかつhappyだと、くそムカつく気がする(笑)

Evaluation

構成としては、15種類のLABELの固有表現抽出機、共参照解決エンジン、ユーザーの理解機の3つで構成。というか、classifierってことはそれぞれ、sequenceの入力で、Labelの出力をしてんのか。

ユーザー理解機には10000個の会話セッションが教師データとして、使用されたみたい。 多分、固有表現抽出機と同じように学習したんかな?

ってことは、Topic抽出はもともとの教師データとして、ラベル付されていたってことか。

「明日[Time]は、ついに人生初の夏[Season]のコミケ[Topic]に参加して、Noesis[Named Entitie]のコーナーに行くんだ。」[Happy]

みたいな、教師データかな?

Core Chat

構成としてはGeneral ChatとDomain Chatの2つで構成されていて、General ChatはOpen Domeinの入力に対して、応答を返すらしい。

じゃあ、Domain Chatはなんぞやって言うと、Deep Conversations で使用されるやつで、音楽とかアニメとかの特定のDomainについての会話の制御を行うらしい。

特定のDomainについての会話はskillの方だと思ってたんだけど、ちゃうんかな?

General Chat と Domain Chat について、エンジンの実行方法は同じで、使用するDBだけが違うみたい。

っていうか、どうやってGeneral Chatか、Domain Chatなのかを決めてんだろう?

まあ、そういったことはおいておいて、一連の流れとしては、状態s = (Q_c, C, e_Q, e_R)の入力。応答の候補たちの出力。応答のrankづけと出力。となっている。

応答の候補は手動で作ったやつをDBから引っ張ってくるか、Neral Networkを使って出力するらしい。

というか、RNNやん。DecoderのHidden層にe_Qとe_Rの情報を埋め込んで、EncoderにQ_cを入力したあとの出力とをぶち込むらしい。

Retrieval-based Generator using Paired Data

会話のペアを用意して、それぞれのe_Q, e_Rを抜き出して、DBに(Q_c, R, e_Q, e_R)の組み合わせで、保存するらしい。

んで、このDBからデータを引っ張ってくるときは、Q_cをKeyとして、Luceneを使って、候補を400個まで引っ張ってくるらしい。

Lucenceをつかって。とあるが、実際はどういった仕組みで動いてんだろう?

ていうか、Rinnaの方はsentiment = happy, opinion = positiveっていう縛りがあるけど、こいつもどうしてんだろう? >> とりあえず、無視してRankづけのときに、sentiment = happy, opinion = positiveとなるような、回答候補を選ぶっぽい。

Q_cの固有表現?でも、それだけだと、文脈に沿っているかなんてわかんないし、わからん。

あと、基本的に Retrival-based Generator using Paired Dataが一番の候補で、それで決まらなかったら、Neural Reposne GeneratorかRetrival-based Generator using Unpaired Dataを使うっぽい。

Neural Response Generator

モデルはやっぱりRNNでGRU-RNNってモデルを使うみたい。LSTMやないんやね。

beam searchを使って、解答候補を20個ほど、生成するってあるけど、beam searchってなんなん?

  • beam search *

今回の場合は20個の解答候補を取り出すのか。を入力して、確率の高い20個の解答候補Tokenを出力して、その20個を入力として、400個の解答候補を取り出して、400個の中からベイズの定理を用いて、確率の高い20個を選んで、その20個を入力して、400個の解答候補を取り出して、400個の中から確率の高い20個を選んで、...をを持った候補が20個揃うまでやるとか?そんな感じっぽい。

まあ、例えば3トークンまで情報を保持していれば結果は変わるかもしれないけど、8000個の確率をいちいち計算するのも大変だし、2トークンまでの保持みたい。

Retrieval-based Generator using Unpaired Data

ペアになっていないデータから、DBを検索して解答候補を作るらしい。

Unpiared Dataからは(R, e_R)のデータしか作れんのにどうすんの?

何か、知識グラフを使用して、queryの拡大をするってあるけど...。知識グラフのデータは(head, relation, tail)ってあるけど、このheadとtailを利用するらしい。

Q_cにheadかtailのどっちかが含まれていたら、もう一方をkeyとして、Rから探し出すとか?

違ってた。

  1. Q_cのTopicを抜き出す。「進撃について教えて。」 >> topicが「進撃」
  2. 知識グラフから20個までの関連topicを取り出す。 「巨人」、「バハムート」、「中学生」、「アニメ」、「マガジン」、「ゲーム」・・・。
  3. Q_cの「進撃」と知識グラフの「巨人」を検索keyとして、e_RのTopicを検索して、Rを400個まで取り出す。

でも、これって結構的はずれなこと言いそうな気がする。でも、Neural Networkを使用したGenerator短くてそっけない応答よりも、長くて有益な情報を含んだ回答を返すらしい。

つまり、品質的には Paired Data > Unpaired Data > Neural Networkの順みたい。

Response Candidate Ranker

3つのGenerator (Retrieval-based using Paired Data, Retrieval-based using Unpaired Data, Neural Response Generator)から、生成された回答をランキング付する機構。boosted tree rankerってのを使っているらしい。論文はAdapting boosting for information retrieval measureesっていうやつ。時間があったらみてみよう。っていうか、これAdaBoostじゃね?

与えられた情報は状態s = (Q_c, C, e_Q, e_R)と回答候補 R' 。この R' の中から、応答を選ばないといけない。どうやって?

  1. 会話ペア間の一貫性の評価。 R' 中のそれぞれの回答候補に対して、Q_cとの関連性を調べるらしい。

  2. 文脈間での一貫性の評価。

R' 中のそれぞれの回答候補に対して、(Q_c, C)との関連性を調べるらしい。 とくに、Q_cが「確かに」、「そうだね。」みたいな、曖昧で短い入力のときに有効らしい。

  1. 共感性の適合率

R' 中の回答候補rに対して(r, e_r)を計算して、Rinnaのペルソナと比較するらしい。つまり、Rinnaのpersonaである、sentiment = happy, opinion = positiveに近い候補を取り出すってことか。

  1. 検索の適合率

Paired Dataから作られた回答候補のみに適用するやつらしい。単語単位でQ_cとデータの(Q_c, R)の比較をするらしい。でも、Q_cを入力として、DBから回答を引っ張ってきてんだから、なんかめちゃくちゃ違和感がある。

上記の4つのscoring system を使って、回答候補を3段階に分けるらしい。

  • 0 : つまんない回答。というか、話を聞いていない時の頓珍漢な回答。
  • 1: ふつうの回答
  • 2: Rinnaの性格に適合していて、かつユーモアあふれる回答

Adaboostを使うってことは、それぞれweak classiferとして登録して、各回答候補に対して0~2をscoringして、そいつをAdaboostに突っ込むんだな。きっと。

0と1が応答される可能性があるってことは、Rinnaの性格にあってない回答をする可能性も十分にありえるのか(笑)。

でも、実際に使用しているユーザーの意見としては、0の回答をよく返されている気がする(笑)。

Editorial Respose

Core Chatが回答候補の生成に失敗したときに返されるやつ。

ユーザーの入力が「わけわかんない」や「興味ない」みたいな回答だったり、性的な質問だったり、すべてのGeneratorが失敗した時用。「どう思っているの?」とか、「別の話をしよう!」みたいな、回答になるらしい。

そういえば、これはRinnaにスタンプ爆弾したときになった(´・ω・`)。

Evaluation

Core Chatに関しては検索ベースと生成ベースの2つを組み合わせたHybrid Systemで作った回答が一番2の評価を得やすかったらしい。というか、Paired Dataだけで、結構2の評価の2/3を占めてるってすごいな。

あと、pearsona を組み合わせただけで、大幅にBLEU scoreが向上知るのもびっくり!

Image Commenting

画像についてのコメント付けで、skillのひとつ。 なんか、画像についてのDescriptionするコメントだけでは不十分で、画像を上げた人の感情とかを読み取ったコメントが必要らしい。

自撮り画像をuploadして、「うーん、ハンサムだね。」とか、コメントせにゃならんらしい。マジか。 人間でも難しいのにwww。

Image Commenting のArchitectureはCore Chatとほぼ同じで、画像からコメントの候補を生成して、そこコメントをランキング付けしてから、選び出すらしい。んで、コメントの候補の生成方法も検索ベースと生成ベースの2つともしようするそうだ。

検索ベースの生成方法では、まずInstagramとかFacebookから、画像とコメントのペアを選び出すらしい。 ここでのコメントは他人からのコメントなのかな? それから、コメントについてpositiveでhappyなものだけを選び出すらしい。 実際に使用するときはCNNを通して、最も近い画像を3つ選んで、そのコメントを回答候補とするそうだ。

生成ベースのやり方では、画像テキスト生成のGeneratorを使っているらしい。Microsoft Image Captioning system に感情とスタイルの制御機構を積んだらしい。スタイルの抽出ってことはモデルはGANのなんかだな。

回答候補のランキングづけもCore Chatとおんなじように、4つのScoring Systemを使った、Adaboostによる最適化をしているらしい。ただ、入力のQ_cは画像だから、Deep Multimodal Similarity Modelってやつで、R'とかの近似度を図っているらしい。

っていうか、Global CoherenceのCはどうすんだべ?データ取得時にCも含めて、Scrapingすりゃいいか。

Evaluation

Image Commentingのあると、画像が登場するChatの長さが2倍になるらしい。ってことは、画像の話をしたら、他の話には発展しづらいのかな?

RinnaはこれとおんなじことをLINE STAMP でもやればいいのに。

Dialogue Skills

XioIceは230のskillを持っていて、コンテンツの作成(content creation)、深い対話(deep engagement)、タスクの実行(task completion)の3つのカテゴリに分類できるそうな。

Evaluation

Dialogue skillっていうのはとっても限定的なものらしいね。ほぼほぼ、手作りのDialogue PolicyとAIMLみたいなテンプレートベースの応答使っているらしい。

評価方法はCore Chatみたいに毎回のようにCallされるものじゃないから、ユーザーの満足度と1日単位や1週間単位でのskillのtriggerされる割合で評価したみたい。

Content Creation

詩の作成、歌を歌ったり読み聞かせの作成、童話の作成とか...。

何やっとんねん(笑)。でも、こういうのを呼び出すときは「make an FM program for [name]」とか「kids story factory」っていうコマンドをいちいち入力せにゃならんらしい(笑)。

Deep Engagement

特定のTopicや設定に対して、ユーザーと深く関わることを目的として作られているらしい。

食べ物の写真からカロリーの計算をするシステム、ちょーネガティブな発言をしたらめちゃくちゃ慰めてくれるシステム、寝るときに羊を数えてくれるシステム、早口言葉を言ってくれるシステムとかいろいろあるらしい。

というか、中国でも寝るときに羊数えんだ(笑)。

Task Completion

いわゆるAlexsaとかCortanaとかGoogle AssistantとかSiriとかのアシスタントボットに追加されている機能。「東京・渋谷の天気は?」とか「10分ほど音楽を流して」とかのリクエストに対して答えるやつ。

ここにKnowledge-Baseの質問も入るらしい。「日本の面積は世界で何番目ですか?」とかの質問。

Open-Domain Questionの一種やないんや。どうやって、他の質問と識別してんだろう?「何番目」とかのKeyword質問とかかな?

わからないこと一覧。

  • 方策πについて。skillとCore Chatについて、actionを選んでいるらしいけど、どういった感じでactionを選んでいるんだろう? Triggerが発火したやつから、確率論で選んでいるのかな?

でも、skillはめちゃくちゃ Domain-specificだから、確率論もクソもない気がするんだけど。

  • skill についてもDialogue Policyというか方策がそれぞれあるみたいな書き方がしてあんだけど、どういうこと?

  • Full Duplex(全二重通信)のときのシステムの概要がさっぱりわからん(笑)

  • Global State Trackerはtextと伴に 固有表現 感情ラベル がつくらしいんだけど。こいつらをどう使っているのかがわからん。

  • Topic ManagerがTopicを検出しているのか、Empaty Computing がTopicを検出してんのかわからん。

なんか、Core Chat用のtopicはempathy computingが検出して、 skillのTask CompletionとかContent Creatioとか、Image CommentingのtopicはDialogue Managerが検出している気がする。

  • Topic ManagerのTopicを選ぶ時のrankづけがいまいちわからん。状態sを使うってあるけど、状態s = (Q_c, C, e_Q, e_R)の何を使ってTopicをDBから選んでるんやろう?

TopicがRになるわけだから、Q_cによる検索もなんか違う気がするし、わからん。ユーザーの興味はe_Qに入っているから、それを使ったり?

  • empathetic computingのtopicがわからん。Topic DBにあるやつのTopicを取り出してるのかな? 論文を見ると、大ドメインと小ドメインの2つにわかれている感があるけど。でも、どんなやり方でinstagramとか、doubanのHashタグから抜き出すんだろう?固有表現抽出?

  • e_Qとe_Rで使用されるIntent、Setiment、Opinionはどうやって識別してんだろう? それぞれ、別の学習機からの出力?

Intentで使われるDialogue Actsって2000年のDialogue Act Modeling for Automatic Taggin and Recogntion of Conversational Speechが元論文になっている気がするんだけど、この論文のDialogue Actsって42種類もあるから、こっから11種類をどうやって選んだのかわからん。

Sentimentは感情の変化をひとくくりにしているけど、「怒り->通常」とか、「悲しみ->怒り」とかもおんなじ扱いになるのが気になる。 あと、論文の図ではsad, nervusっていう項目があるけど、このnervusって何者?

Opinionは普通にQ_cのnegative, positive, neuralを識別するだけかな?一度でも、negative or positiveになったら、つぎにneural が来てもそのままで行く気がするが。

topic, sentiment, intent, entityについて調べてみた。

  • Entity (Google Cloud と Azureで()がついていないのは共通)*
  • Unknown (G)
  • Person (GA)
  • PersonType (A)
  • Location (GA)
  • Organization (GA)
  • Event (GA)
  • Product (A)
  • Skill (A)
  • Adress (GA)
  • Phone Numer (GA)
  • Email (A)
  • URL (A)
  • IPAdress (A)
  • DateTime (GA)
  • Quantity (A)
  • Work of Art (G)
  • Cosumer good (G)
  • Other (G)
  • Price (G)

  • Topic (Google Cloud) *

多いのでURLだけ貼っておく

cloud.google.com

もとの論文っぽそうなのが Topic Aware Neural Response Generationっていう論文。

テキスト中のEntityについてpositive, negative, neuralを識別するAPI。EntityにTopicが含まれていれば、それがOpinionになるってことか。

  • Opinion (Azure)

テキストそのものがpositive, negative, neuralかを判定するやつ。topicにはnegativeでも、文章全体がpositiveなら、positiveになるっぽい。 あと、Googleみたいに各Entityに対して、OpinionをつけるのはOpinion Miningっていうやつでやれるみたい。

テキストそのものがpositive, negative, neuralかを判定するやつ。AzureのOpinitonとおんなじ。

とりあえず、Big5のAPIでEmotionを識別するやつは見つかんなかった。 一応、Indico APIっていう英語だけだけど、'anger', 'joy', 'fear', 'sadness', 'surprise'の5つを識別できるっぽい。でも、日本の美しき感情に嬉し泣きっていうのがあるから、そいつはどう判定されんのやろう?

  • Retrival-Generatorで使用されるluceneってなに? >> Elastic Searchの内部エンジン。あーElastic Searchで検索してんのか。なんとなくわかった気がする。

元論文である Learning Distributed Representations of data in Community question Answering for Question Retrival のAbstractによると、文章のカテゴリと単語を分散表現で表して、これをQueryにして検索をかけるみたい。つまり、Q_c内のEntityとe_Q内のTopicをvectorになおして、ElasticSearchで検索しているみたい。

  • Retrival-Generatorで引っ張ってきたResponseのQueryとの関連性とかどうやって計算してんだろう?

Learning deep structured semantic models for web search using clickthrough datalってやつで実装はここなんだけど、見た感じ文章の一貫性を判別するやつじゃない気がする。

なんか、気になるから調べてみるか...。

6本目!Neural Machine Translation of Rare Words with Subword Units

では、今回はいろんな論文に登場するBPEについて見ていきたいと思います!

こいつはTokenzierとしてよく使うsentence pieceの元となった論文ですな。

ちなみにsentece pieceとbpeの違いは、sentence pieceはユニグラム言語モデルっていうやつを使って、subwordを構築したらしい。このおかげで日本語や中国語みたいに、空白文字列のない言語モデルでもきれいに分割ができるようになったみたい。

つまり、日本語を使うならsentece pieceを使いましょうってことらしい。

まあ、Huggin FaceのTokenizerは基本的にsentence piece っぽいし。 日本語でmecab ベースの toknenizerを使うなら、

tokenizer = BertTokenizer.from_pretrained("cl-tohoku/bert-large-japanese")

でいいし、sentence piece ベースのtokenizerを使うなら、rinnaのgithubからgoogle_sp.modelをDLしてきて、

    tokenizer = T5Tokenizer(
        vocab_file="google_sp.model",
        bos_token="<s>",
        eos_token="</s>",
        unk_token="<unk>",
        pad_token="[PAD]",
        cls_token="[CLS]",
        sep_token="[SEP]",
        mask_token="[MASK]",
        extra_ids=0,
        additional_special_tokens=(),
        do_lower_case=True
    )

みたいな感じでいいみたい。

もし、カスタムで学習したい場合はこのサイトとかを参考にするといいみたい。

tech.mntsq.co.jp

Tokenizerのコンポーネントとうか、toknezerのPipelineのためのReferenceはここ。

huggingface.co

でも、普通のTokenizerとBert Tokenizerの違いが判らん。

実装を見てみると、

        self.wordpiece_tokenizer = WordpieceTokenizer(vocab=self.vocab, unk_token=self.unk_token)

    def _tokenize(self, text):
        split_tokens = []
        if self.do_basic_tokenize:
            for token in self.basic_tokenizer.tokenize(text, never_split=self.all_special_tokens):

                # If the token is part of the never_split set
                if token in self.basic_tokenizer.never_split:
                    split_tokens.append(token)
                else:
                    split_tokens += self.wordpiece_tokenizer.tokenize(token)
        else:
            split_tokens = self.wordpiece_tokenizer.tokenize(text)
        return split_tokens

ってなっている。つまり、内部でBertを使っているとかじゃなくて、ただBertの論文に会わせて、WordpieceをつかうTokenizerとして実装しているみたい。

もし、T5-tokenizerのほうのモデルを改良したかったら、sentence pieceのライブラリから作るみたい。

    import sentencepiece
    sentencepiece.SentencePieceTrainer.Train(r'--input=ja.txt, --model_prefix=test, --vocab_size=32000', --character_coverage=0.9995)
    tokenizer = T5Tokenizer('test.model')

ただし、1文が4192文字以内で、空行を除いたファイルを作らないといけないから、ちょい面倒かも?

まあ、Tokenizerの実装についてはこのへんでおいておいて実際の論文の中身に入っていきたいと思います。

Byte Pair Encoding

そもそも、Byte Pair Encdingとは文字列の圧縮方法として提案された手法です。

例えば、「AAAABBBBBAAABBB」みたいな単語の塊が見られます。

これを「BB」を「Z」に置き換えると「AAAAZZBAAAZB」、それだけで文字数が3文字圧縮できます。さらにこれを「AA」を「Y」に置き換えると「YYZZBYAZB」となり、さらに3文字圧縮できます。これから「YY」を「X」、「ZZ」を「U」に変換すれば圧縮の完了で、「XUBYAZB」となり、結果的に8文字も圧縮でき、約46%の圧縮率となります。

解凍時にははこの変換ルールを逆変換すればいいので、シンプルながらも強力な圧縮方法です。

で、こいつをどう NLP の問題に適用したかというと、このアルファベットの代わりに各言語の文字で考えます。

  1. 単語に分割する。
  2. 各単語に end-of-word symbol ()を付け足す。
  3. も一つの文字と考えて、各文字のペアをカウントする。
  4. 最もよく出現するペアを一つにまとめて、新しい文字として考える。
  5. また、2から初めて文字のペアを数え直す。 新しく文字が追加されたので、ペアの数は減るが、ペアの種類は増える可能性も減る可能性もある!
  6. 文字の種類がある値になるまでこれを繰り返す。

要点としては、ペアの数、ペアの種類、文字の数、文字の種類の4つをきちんと区別して考えましょうってこと。

目的は文字の種類の最小化。

そのためにペアの種類の最大値を用いるってこと。

Algorythmとしてはこんな感じで実装はこう。

import re, collections

def get_stats(vocab):
    pairs = collections.defaultdict(int)
    for word, freq in vocab.items():
        symbols = word.split()
        for i in range(len(symbols) -1):
            pairs[symbols[i], symbols[i+1]] += freq
    return pairs


def merge_vocab(pair, v_in):
    v_out = {}
    bigram = re.escape(' '.join(pair))
    p = re.compile(r'(?<!\S)' + bigram + r'(?!\S)')
    for word in v_in:
        w_out = p.sub(''.join(pair), word)
        v_out[w_out] = v_in[word]
    return v_out

def main():
    vocab = {'t e s t </w>': 5, 't r i p </w>': 8, 'w e s t </w>': 4, 't r i m </w>': 2}
    num_merges = 10

    for i in range(num_merges):
        pairs = get_stats(vocab)
        print(pairs)
        best = max(pairs, key=pairs.get)
        print(best)
        vocab = merge_vocab(best, vocab)
        print(vocab)

    print('End ', vocab)

if __name__=='__main__':
    main()

で、なんでend-of-word symbolが必要かって言うと、「trip」と「ripping」みたいな単語があったとして、「rip」の部分について、別物として考えるためらしい。

まあ、日本語でも「おはよう」と「ようこそ」の「よう」はおんなじと考えてほしくないしね。

Unicode Regulation

いろんな文字コードを一定に変換する方法らしい。

日本語はNFKCっっぽい。sentence pieceではDefaultでこのNFKCが使われているみたい。

じゃあ、単語の区切り方はどうやってんのかな?

Sentence Pieceの論文のAbstractを除いてみると、

While existing subword segmentation tools assume that the input is pre-tokenized into word sequences, SentencePiece can train subword models directly from raw sentences, which allows us to make a purely end-to-end and language independent system

ってある。つまり、この論文のように pre-tokenizationが必要ないそうだ。

今度、じっくりと呼んでみよう。

5本目!Towards a Human-like Open-Domain Chatbot

今回がかの巨人であるGoogleが開発した Chatbotに関する論文です。

その子の名前はMeena!というか、なんて読むの?メーナ?ミーナ? まあ、ミーナの方が可愛いのでミーナってことで。なんか、ミーナって聞いたことがある気がする。調べたら、進撃の登場人物だった。あと、ミーアでティアムーンの広いんだから、たぶんどっちかと勘違いしたのかな?

それは置いておいて、MeenaはMulti-turn Open-Domain Chatbotってやつらしい。Mulit-Turnが会話の往復って意味で、Open-Domainがどのような内容に対しても回答できるっていう意味らしい。

つまり、人間のように文脈を把握して、あらゆる話題に対応しようっていう、Chatbotだそうな。

Introduction

まず、Open-Domain Chatbotの多さよ。MILABOT, XioIce, Gunrock, Mituku, Cleverbot, etc...とあるらしい。 こいつらは 知識ベース、検索ベース、ルールベースの3つを組み合わせて応答文章を作っているらしい。問題点としては、ちょー複雑なモデルであること。 一方で、Neural Networkを使用したモデルではシンプルに学習することができるが、問題点として、抽象的だったり意味をなさない応答を返すことがあることらしい。

比較サンプルはココ

まあ、数年前に一から実装したTransformerはずっと無言だったしね('ω')

ミーナちゃんは超巨大なGenerative型のlow-perplexityなChatbotだそうだ。

low-perplexityとは何ぞや?答えの候補の確率の逆数らしい。例えば10000語の知識があって、「私が好きなエロゲはKeyの」とあったとして、次の単語が10個に単語を絞れたら、


Perplexity = \frac{1}{\frac{1}{10}} = 10

となるらしい。今回は平均して、preplexity = 10.2で単語数がsubwordsで8000個だそうだ。 やべーな。

そして、今回使用したモデルは Seq2seqでSequence to Sequence Learning with Neural Networksっていう論文とNeural Machine Translation by Jontly Learning to Align and Translateっていう論文のモデルをEvolved TransformerっていうNeural Networkのアーキテクチャ検索をするモデルで最適化したやつを使っているらしい。

まじで意味わからん。Google AI のBlogをあさっていたら、こんなモデルが出てきた。

f:id:kaya-takashiro:20210829193637j:plain
evolvedTransformer

Evolved Transformerの中にSeq2seqが入っているのかな?

んで、今回の指標にはSSA(Sensibleness and Specific Average)ってやつを使ったそうだ。 指標としては

1.文の意味を成しているのか? 2.はっきりとした答えになっているのか?

の2つの指標を組み合わせたものらしい。でも、人間で97%って...。多分、お茶を濁しやすい日本人なら、ミーナに負けるかも(笑)

1の指標は今まで通りで、2の指標はNeuralModelの問題点であるあいまいな回答に対する指標みたい。

こいつをTest-Caseにぶち込んだ時の結果の評価と、実際の会話での評価の2通りで使用して、各Chatbotの性能を測定したそうだ。

入力文としては最大7つまでのDialogue Blockをぶち込んで、次の文章を応答としたみたい。

入力としては、A<EOS>B<EOS>...<EOS>A<EOS>みたいな感じなのかな?

Meena chatbot

モデルを高性能にする方法について論じている。

もっと、training dataを増やし、パラメータを増やせばいいのか?、それともモデルを他の検索ベースとかのモデルにくっつけた方がいいのか?

Googleの出した答えは...

超大規模なモデルを作ろう!ってことで決まったみたい。

データの下処理については

1.2単語未満 or 128単語以上の文を除く 2. 70%以下の英語の文を除く 3.URLを除く 4.Botの発言を除く 5.100以上繰り返さた文を除く(I don't knowとか?) 6.前の文とおんなじことを言っているやつを除く(海に行きたい!海に行くのはいいね。)みたいな? 7.社会的に問題のある文を除く

これで8億6700万個の(context, response)のペアができたっていうんだけど、マジでやばい。

どうやってそんなに会話を集めたんねん(笑)

日本でやれるのはLINEぐらいじゃね?

Model Architecture

ミーナちゃんのModel ArchitectureはTransformer seq2seq modelで2.6Bのパラメータを持つらしい。

1つのEncoderBlockと13のDecoderBlockってことは、Encoderのhidden層は最小のDecoderBlockにぶち込むだけ?それとも、全部?

まあ、Evolved Transformerを調べてみればいいか。

でも、一番でかいGPT-2 modelのパラメータサイズが1.5Bってことはマジでバケモンやん。

隠れ層が2560, Attention Headsが32ってでかすぎワロタ。

Decoding

Sample-and-Rank Decodingってのを採用しているらしい。

こいつは単語を選ぶ時のsoft-maxを改良したものらしい


p_i = \frac{exp(z_i / T )}{\sum_{j} exp(z_j / T)}

なんか、よくわかんないけどTを大きくすると、固有名詞とかの特定の単語が出やすくなって、Tを小さくすると、前置詞とか冠詞が出やすくなるみたい。

ここに上式の論拠となった元の論文があるから、時間があれば見てみよう。

Further Advancing SSA

ミーナの改良のやり方らしい。

#### Advancing Decoding

Decodingのときに temperature Tとtop-kのパラメータを変化させて、SSAを調節したらしい。

T = 0.88, k= 40, N=20にすると、一番良かったみたい。 Tが前の式のTで、kがtop-kで使用されるkで、NがSample-and-Rank Decodingで選ぶtokenの数らしい。

Addressing Cross-turn Repetitions

以前に登場した文を繰り返すことをCross-turn というらしい。

このCross-turnを減らすことでSSAが向上するそうな。

あと、「あんたなんて大嫌い、だから手をつないで帰ろう。」みたいな、矛盾した文章はミーナではほとんどないらしい。すげー。

ただ、共感性を持つような温かみのある会話とかはできないっぽい。

こういうのはI KNOW THE FEELING: LEARNING TO CONVERSE WITH EMPATHっていう論文とか、The Design and Implementaion of XiaoIce an Empathetic Socal Chatbotっていう論文にあるから、読んでみよう。

4本目!BERT:Pre-training 0f Deep Bidirectional Transformers for Language Understanding.

ではでは、さーて、gpt-2で使われていた論文に行きたいと思います。

まず、こいつがどんな使われ方を確認しましょう!

参照元の論文のもう一方ではRTEにおいて、<Premise><SEP><Hypothesis>という入力をして、この文章がNeural, Contradiction, Entailmentのどれに当てはまるのかの識別をするタスクを行っていました。

でも、GPT-2では識別タスクの他にも生成タスク(Question-Answering)を行っています。 つまり、生成タスクの元となった論文がこれらしいので、調べてみましょう!

...というか、これはBERTの元論文じゃん( ゚Д゚)

Introduction

まず、Pre-Training modelを教師あり学習に適用する方法にはFine-TuningとFeature-Basedの2種類あるそうな。

Fine-TuningはGPT-2でも用いられている方法で、モデルをあまり変更せずに、入力の方を調整する方法。

Feature-BasedはELMoで用いられている方法で、Pre-trainingのModelのパラメータを固有のArchitectureに組み込む方法らしい。

Fine-Tuningのデメリットは単方向の文章の流れしか、学習できないので、今回のタスクではFine-Tuningのタスクを教師なし学習の方法を変更して、双方向(Bidirectional)にしよう!ということらしい。

実装はここ

Pre-trainingの方法としては、文章の単語をMaskしてその単語を当てよう!というタスク(Masked Language Model)と、次の文章の予測しよう!というタスク(Next Sentence Prediction)の2つを行う。

BERT

Model Architecture

Modelのベースとなっているのはやはりというか、何というか、やっぱりAttention is All You Need.である。

ここで、BERT Transformerはbidirectional self-attentionを使っている、とあるのだがどんなAttentionなんだろう?

実装を見ても、Encoderのみ残っていてDecoderが見当たらない。

maskについても attention_maskがあるだけで、subsequent_maskは見つからない。

def transformer_model(input_tensor,
                      attention_mask=None,
                      hidden_size=768,
                      num_hidden_layers=12,
                      num_attention_heads=12,
                      intermediate_size=3072,
                      intermediate_act_fn=gelu,
                      hidden_dropout_prob=0.1,
                      attention_probs_dropout_prob=0.1,
                      initializer_range=0.02,
                      do_return_all_layers=False):
  """Multi-headed, multi-layer Transformer from "Attention is All You Need".
  This is almost an exact implementation of the original Transformer encoder.
  See the original paper:
  https://arxiv.org/abs/1706.03762
  Also see:
  https://github.com/tensorflow/tensor2tensor/blob/master/tensor2tensor/models/transformer.py
  Args:
    input_tensor: float Tensor of shape [batch_size, seq_length, hidden_size].
    attention_mask: (optional) int32 Tensor of shape [batch_size, seq_length,
      seq_length], with 1 for positions that can be attended to and 0 in
      positions that should not be.
    hidden_size: int. Hidden size of the Transformer.
    num_hidden_layers: int. Number of layers (blocks) in the Transformer.
    num_attention_heads: int. Number of attention heads in the Transformer.
    intermediate_size: int. The size of the "intermediate" (a.k.a., feed
      forward) layer.
    intermediate_act_fn: function. The non-linear activation function to apply
      to the output of the intermediate/feed-forward layer.
    hidden_dropout_prob: float. Dropout probability for the hidden layers.
    attention_probs_dropout_prob: float. Dropout probability of the attention
      probabilities.
    initializer_range: float. Range of the initializer (stddev of truncated
      normal).
    do_return_all_layers: Whether to also return all layers or just the final
      layer.
  Returns:
    float Tensor of shape [batch_size, seq_length, hidden_size], the final
    hidden layer of the Transformer.
  Raises:
    ValueError: A Tensor shape or parameter is invalid.
  """
  if hidden_size % num_attention_heads != 0:
    raise ValueError(
        "The hidden size (%d) is not a multiple of the number of attention "
        "heads (%d)" % (hidden_size, num_attention_heads))

  attention_head_size = int(hidden_size / num_attention_heads)
  input_shape = get_shape_list(input_tensor, expected_rank=3)
  batch_size = input_shape[0]
  seq_length = input_shape[1]
  input_width = input_shape[2]

  # The Transformer performs sum residuals on all layers so the input needs
  # to be the same as the hidden size.
  if input_width != hidden_size:
    raise ValueError("The width of the input tensor (%d) != hidden size (%d)" %
                     (input_width, hidden_size))

  # We keep the representation as a 2D tensor to avoid re-shaping it back and
  # forth from a 3D tensor to a 2D tensor. Re-shapes are normally free on
  # the GPU/CPU but may not be free on the TPU, so we want to minimize them to
  # help the optimizer.
  prev_output = reshape_to_matrix(input_tensor)

  all_layer_outputs = []
  for layer_idx in range(num_hidden_layers):
    with tf.variable_scope("layer_%d" % layer_idx):
      layer_input = prev_output

      with tf.variable_scope("attention"):
        attention_heads = []
        with tf.variable_scope("self"):
          attention_head = attention_layer(
              from_tensor=layer_input,
              to_tensor=layer_input,
              attention_mask=attention_mask,
              num_attention_heads=num_attention_heads,
              size_per_head=attention_head_size,
              attention_probs_dropout_prob=attention_probs_dropout_prob,
              initializer_range=initializer_range,
              do_return_2d_tensor=True,
              batch_size=batch_size,
              from_seq_length=seq_length,
              to_seq_length=seq_length)
          attention_heads.append(attention_head)

        attention_output = None
        if len(attention_heads) == 1:
          attention_output = attention_heads[0]
        else:
          # In the case where we have other sequences, we just concatenate
          # them to the self-attention head before the projection.
          attention_output = tf.concat(attention_heads, axis=-1)

        # Run a linear projection of `hidden_size` then add a residual
        # with `layer_input`.
        with tf.variable_scope("output"):
          attention_output = tf.layers.dense(
              attention_output,
              hidden_size,
              kernel_initializer=create_initializer(initializer_range))
          attention_output = dropout(attention_output, hidden_dropout_prob)
          attention_output = layer_norm(attention_output + layer_input)

      # The activation is only applied to the "intermediate" hidden layer.
      with tf.variable_scope("intermediate"):
        intermediate_output = tf.layers.dense(
            attention_output,
            intermediate_size,
            activation=intermediate_act_fn,
            kernel_initializer=create_initializer(initializer_range))

      # Down-project back to `hidden_size` then add the residual.
      with tf.variable_scope("output"):
        layer_output = tf.layers.dense(
            intermediate_output,
            hidden_size,
            kernel_initializer=create_initializer(initializer_range))
        layer_output = dropout(layer_output, hidden_dropout_prob)
        layer_output = layer_norm(layer_output + attention_output)
        prev_output = layer_output
        all_layer_outputs.append(layer_output)

  if do_return_all_layers:
    final_outputs = []
    for layer_output in all_layer_outputs:
      final_output = reshape_from_matrix(layer_output, input_shape)
      final_outputs.append(final_output)
    return final_outputs
  else:
    final_output = reshape_from_matrix(prev_output, input_shape)
    return final_output

注釈を読むと、TransformerのEncoderがbidirectional Transformerで、Decoderがleft-context-only Transformerみたいな書き方をしているけど、それでいいのかな?

でも、そう考えるとGPT-2はどうやって、DecoderをEncoderに変更したんだろう?マジでわからん。

Input/Output Representations

入力については 1文 or 2文を<SEP>tokenで結び付けたものの先頭に<CLS>tokenを当てはめたものを使用する。

<CLS>tokenの入力で、2文が連続したものどうかの判定を行うみたい。

他にはtokenizationにはsentence pieceをつかっている。sentence pieceについては、ほとんどわかってないからあとで確認したい。

Embeddingに関しては Token embedding + Segment embedding + Positional embeddingとなっている。

Token embedding と Segment embedding に関しては、Attention is All You Needと同じだとして、Segment embedding って何?

実装を見てみると、

        (self.embedding_output, self.embedding_table) = embedding_lookup(
            input_ids=input_ids,
            vocab_size=config.vocab_size,
            embedding_size=config.hidden_size,
            initializer_range=config.initializer_range,
            word_embedding_name="word_embeddings",
            use_one_hot_embeddings=use_one_hot_embeddings)

        # Add positional embeddings and token type embeddings, then layer
        # normalize and perform dropout.
        self.embedding_output = embedding_postprocessor(
            input_tensor=self.embedding_output,
            use_token_type=True,
            token_type_ids=token_type_ids,
            token_type_vocab_size=config.type_vocab_size,
            token_type_embedding_name="token_type_embeddings",
            use_position_embeddings=True,
            position_embedding_name="position_embeddings",
            initializer_range=config.initializer_range,
            max_position_embeddings=config.max_position_embeddings,
            dropout_prob=config.hidden_dropout_prob)

っていう風になっていて、この実装が

def embedding_lookup(input_ids,
                     vocab_size,
                     embedding_size=128,
                     initializer_range=0.02,
                     word_embedding_name="word_embeddings",
                     use_one_hot_embeddings=False):
  """Looks up words embeddings for id tensor.
  Args:
    input_ids: int32 Tensor of shape [batch_size, seq_length] containing word
      ids.
    vocab_size: int. Size of the embedding vocabulary.
    embedding_size: int. Width of the word embeddings.
    initializer_range: float. Embedding initialization range.
    word_embedding_name: string. Name of the embedding table.
    use_one_hot_embeddings: bool. If True, use one-hot method for word
      embeddings. If False, use `tf.gather()`.
  Returns:
    float Tensor of shape [batch_size, seq_length, embedding_size].
  """
  # This function assumes that the input is of shape [batch_size, seq_length,
  # num_inputs].
  #
  # If the input is a 2D tensor of shape [batch_size, seq_length], we
  # reshape to [batch_size, seq_length, 1].
  if input_ids.shape.ndims == 2:
    input_ids = tf.expand_dims(input_ids, axis=[-1])

  embedding_table = tf.get_variable(
      name=word_embedding_name,
      shape=[vocab_size, embedding_size],
      initializer=create_initializer(initializer_range))

  flat_input_ids = tf.reshape(input_ids, [-1])
  if use_one_hot_embeddings:
    one_hot_input_ids = tf.one_hot(flat_input_ids, depth=vocab_size)
    output = tf.matmul(one_hot_input_ids, embedding_table)
  else:
    output = tf.gather(embedding_table, flat_input_ids)

  input_shape = get_shape_list(input_ids)

  output = tf.reshape(output,
                      input_shape[0:-1] + [input_shape[-1] * embedding_size])
  return (output, embedding_table)


def embedding_postprocessor(input_tensor,
                            use_token_type=False,
                            token_type_ids=None,
                            token_type_vocab_size=16,
                            token_type_embedding_name="token_type_embeddings",
                            use_position_embeddings=True,
                            position_embedding_name="position_embeddings",
                            initializer_range=0.02,
                            max_position_embeddings=512,
                            dropout_prob=0.1):
  """Performs various post-processing on a word embedding tensor.
  Args:
    input_tensor: float Tensor of shape [batch_size, seq_length,
      embedding_size].
    use_token_type: bool. Whether to add embeddings for `token_type_ids`.
    token_type_ids: (optional) int32 Tensor of shape [batch_size, seq_length].
      Must be specified if `use_token_type` is True.
    token_type_vocab_size: int. The vocabulary size of `token_type_ids`.
    token_type_embedding_name: string. The name of the embedding table variable
      for token type ids.
    use_position_embeddings: bool. Whether to add position embeddings for the
      position of each token in the sequence.
    position_embedding_name: string. The name of the embedding table variable
      for positional embeddings.
    initializer_range: float. Range of the weight initialization.
    max_position_embeddings: int. Maximum sequence length that might ever be
      used with this model. This can be longer than the sequence length of
      input_tensor, but cannot be shorter.
    dropout_prob: float. Dropout probability applied to the final output tensor.
  Returns:
    float tensor with same shape as `input_tensor`.
  Raises:
    ValueError: One of the tensor shapes or input values is invalid.
  """
  input_shape = get_shape_list(input_tensor, expected_rank=3)
  batch_size = input_shape[0]
  seq_length = input_shape[1]
  width = input_shape[2]

  output = input_tensor

  if use_token_type:
    if token_type_ids is None:
      raise ValueError("`token_type_ids` must be specified if"
                       "`use_token_type` is True.")
    token_type_table = tf.get_variable(
        name=token_type_embedding_name,
        shape=[token_type_vocab_size, width],
        initializer=create_initializer(initializer_range))
    # This vocab will be small so we always do one-hot here, since it is always
    # faster for a small vocabulary.
    flat_token_type_ids = tf.reshape(token_type_ids, [-1])
    one_hot_ids = tf.one_hot(flat_token_type_ids, depth=token_type_vocab_size)
    token_type_embeddings = tf.matmul(one_hot_ids, token_type_table)
    token_type_embeddings = tf.reshape(token_type_embeddings,
                                       [batch_size, seq_length, width])
    output += token_type_embeddings

  if use_position_embeddings:
    assert_op = tf.assert_less_equal(seq_length, max_position_embeddings)
    with tf.control_dependencies([assert_op]):
      full_position_embeddings = tf.get_variable(
          name=position_embedding_name,
          shape=[max_position_embeddings, width],
          initializer=create_initializer(initializer_range))
      # Since the position embedding table is a learned variable, we create it
      # using a (long) sequence length `max_position_embeddings`. The actual
      # sequence length might be shorter than this, for faster training of
      # tasks that do not have long sequences.
      #
      # So `full_position_embeddings` is effectively an embedding table
      # for position [0, 1, 2, ..., max_position_embeddings-1], and the current
      # sequence has positions [0, 1, 2, ... seq_length-1], so we can just
      # perform a slice.
      position_embeddings = tf.slice(full_position_embeddings, [0, 0],
                                     [seq_length, -1])
      num_dims = len(output.shape.as_list())

      # Only the last two dimensions are relevant (`seq_length` and `width`), so
      # we broadcast among the first dimensions, which is typically just
      # the batch size.
      position_broadcast_shape = []
      for _ in range(num_dims - 2):
        position_broadcast_shape.append(1)
      position_broadcast_shape.extend([seq_length, width])
      position_embeddings = tf.reshape(position_embeddings,
                                       position_broadcast_shape)
      output += position_embeddings

  output = layer_norm_and_dropout(output, dropout_prob)
  return output

embedding_lookupは普通のembeddingの処理で、実際のいろいろな情報の埋め込みはembedding_postprocessorが行っているみたい。

ここで問題となってくるのがembedding_postprocessorで使われている、token_typeとposition_embeddingsについて。

まず、token_typeについて。

こいつは、token_type_idsってやつをone_hot vector に変換して、それにtoken_type_tableっていう学習パラメータを掛けて作っている。つまり、tokenの種類によって、token_idによるembedding以外でも学習しているってこと。

ちなみにこのtoken_type_idsってやつを調べると、run_classiferでsegment_idsってやつを元として使われている。

create_pretraining_data.pyを見てみると、

        tokens = []
        segment_ids = []
        tokens.append("[CLS]")
        segment_ids.append(0)
        for token in tokens_a:
          tokens.append(token)
          segment_ids.append(0)

        tokens.append("[SEP]")
        segment_ids.append(0)

        for token in tokens_b:
          tokens.append(token)
          segment_ids.append(1)
        tokens.append("[SEP]")
        segment_ids.append(1)

ってな具合で最初の<CLS>から<SEP>までを0, 次のtokenから最後の<SEP>までを1として、segment_idsを作っている。

ってことはtoken_type_idsって2種類しかないのね。 つまり、1文目と2文目についての埋め込み表現を学習して、Embeddingに加えると...。

つぎにposition_embeddingについて。最初はあのPositional Encodingと同じかなと思ったけど、違うみたい。 最大のsequence長さ以下のEmbedding表現を作って、各Batchの最大長に合わせて切り出してくっつけている。

つまり、Positional_encodingも学習パラメータってこと。でも、これだと1文目の長さの違いとか、1文目と2文目の位置の関係性とか本当に学習できてるんかな?

例えば、<今日はどこに行ってきたの?><今日は山梨県に行ってきた。>と<どこに今日は行ってきたの?><山梨県に今日は行ってきた。>の2つの例文として、単語間の距離は、1文目の「今日」と1文目の「どこ」、2文目の「山梨県」と1文目の「どこ」が一定になってるけど、そういった1文目の中だけじゃなく、1文目と2文目の相対的な位置のの情報とかも記録できんのかな?

Pre-training BERT

Maked LM

Taskはマスクされた単語の予測。これだけだとよくわからないが、例えば<私 が 大好きな [Mask] は Code:Geass だ> みたいな文章を入力したとして、出力は<私 が 大好きな アニメ は Code:Geass だ >となるように出力するタスクである。

この時に注意するのが、アニメの出力以外の「私」とか 「だ」みたいな、マスクされていない単語の予測のLossは計算しないということ。

GPTの論文にあったようにこれを学習すると、ただ単に入力文を垂れ流しにするあほみたいなモデルが出きるっぽい(笑)

実際には[Mask]のtokenに置き換えるのが80%で、ランダムな単語に置き換えるのが10%、あと10%はそのままで学習するらしい。

何で、すべてのやつをMaskしないかというと、実際の入力には[Mask]が存在しないからだそうだ。

だったら、[Mask]のところをすべてランダムな単語に置き換えて、間違っている単語を訂正しましょう!っていうタスクでもいい気が済んだけど、駄目なのかな?

Next Sentence Prediction

<CLS>をぶち込んだところの出力がNonNextかNextかを当てるタスク。これにより、言語モデルにおいて、2文間の連続性について学習できるそうだ。

っていうことはDialogueみたいな長文は学習できないのか。

ここら辺はもしかしたら、GPTに軍配が上がるかも?

Question-AnweringとNLIの精度向上に役立つらしい。

ちなみにNLIっていうのは、前に紹介した論文にもあったタスクで、

PremiseとHypothesisの関係がcontradiction, neural, entailmentのいずれかを識別するタスク。

Fine-tuning BERT

ていうか、なんだか想定したいたQuestion-AnsweringのTaskと違う気がしてきたから、調べてみたら案の定違ったorz...

予測していたTask

Trainingにおいて<Question, Answer>を入力し、どうにかして学習し、Preditionにおいて、QuestionをInputとして、Answerを出力する。

実際のTask

Trainingにおいて<Question, Answer>を入力しAnswerの中のQuestionの回答となるtokenの確率分布を出力する。Preditionにおいて<Question, Answer>を入力しAnswerの中のQuestionの回答となるtokenの確率分布を出力する。

つまり、どうにかしてQuestion-Answeringでは、問題と答えを含む文章を用意しないといけないのかー。

会話とかどうすんねん(笑)

調べたら、Google から MeenaっていうHuman Likeなchatbotを作成するPaperが出ていたので、次に調べよう!

3本目!Attention Is All You Need.

ではでは、いよいよかの有名なAttention Is All You Need について見ていきたいと思います。

まあ、モデルの詳しい説明はググればいっぱいあるので、そっちに置いておいて、訳の分かりにくくなるところを調べていきたいと思います。

Model Architecture

Transformerはgpt-2とは異なり、Encoder-Decoderのモデルです。

まず、Dataについて整理してみると、Encoderのinputがx = (x_1, ... x_n)でoutputが z = (z_1, ..., z_n)となっており、これをDecoderのMulti-Head-AttentionにFeedすることで、y = (y_1, .., y_n)の出力をします。

Encoder and Decoder

EncoderとDecoderは同じ数のN=6で設定されています。それぞれSublayer_1, ..., Sublayer_6まで分かれているっていうことで、EncoderのSublayer_iのoutputをDecoderのSublayer_iへ入力するみたい。

というか、ここでもResidual Connectionを採用していて、本当にResidual Networkは偉大なんだなーと思う。

でも、疑問点としてはDecoderに登場する Masked Multi-Head Attentionとやらがわからん。

実装を見てみよう!

        src_mask = get_pad_mask(src_seq, self.src_pad_idx)
        trg_mask = get_pad_mask(trg_seq, self.trg_pad_idx) & get_subsequent_mask(trg_seq)
def get_pad_mask(seq, pad_idx):
    return (seq != pad_idx).unsqueeze(-2)


def get_subsequent_mask(seq):
    ''' For masking out the subsequent info. '''
    sz_b, len_s = seq.size()
    subsequent_mask = (1 - torch.triu(
        torch.ones((1, len_s, len_s), device=seq.device), diagonal=1)).bool()
    return subsequent_mask

get_pad_maskはmask = [1, 1, 1, 1, ..., 1, 1, 0, 0, 0, ..., 0]とか出力して、tokenがPaddingかどうかのチェックを行なうやつ。というか、Batchでpaddingするときとかに使うやつ。

それでget_subsequent_maskは sequence_length * sequence_lengthの

[tex: \begin{pmatrix}
0&0&..&0&0\\
1&0&..&0&0\\
1&1&0&...&0\\
...\\
1&1&...&0&0\\
1&1&...&1&0\\
\end{pmatrix}
]

という行列ができる。そいで、これがBatch_size個重なっている。

このmaskの使われ方を見てみると両方ともMulti-Head-Attentionで使われており、

self.attention = ScaledDotProductAttention(temperature=d_k ** 0.5)

def forward(self, q, k, v, mask=None):

    d_k, d_v, n_head = self.d_k, self.d_v, self.n_head
    sz_b, len_q, len_k, len_v = q.size(0), q.size(1), k.size(1), v.size(1)

  residual = q

        # Pass through the pre-attention projection: b x lq x (n*dv)
        # Separate different heads: b x lq x n x dv
    q = self.w_qs(q).view(sz_b, len_q, n_head, d_k)
    k = self.w_ks(k).view(sz_b, len_k, n_head, d_k)
    v = self.w_vs(v).view(sz_b, len_v, n_head, d_v)

    # Transpose for attention dot product: b x n x lq x dv
 q, k, v = q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2)

    if mask is not None:
        mask = mask.unsqueeze(1)   # For head axis broadcasting.

    q, attn = self.attention(q, k, v, mask=mask)

    # Transpose to move the head dimension back: b x lq x n x dv
    # Combine the last two dimensions to concatenate all the heads together: b x lq x (n*dv)
    q = q.transpose(1, 2).contiguous().view(sz_b, len_q, -1)
    q = self.dropout(self.fc(q))
    q += residual

    q = self.layer_norm(q)

    return q, attn

class ScaledDotProductAttention(nn.Module):
    ''' Scaled Dot-Product Attention '''

    def __init__(self, temperature, attn_dropout=0.1):
        super().__init__()
        self.temperature = temperature
        self.dropout = nn.Dropout(attn_dropout)

    def forward(self, q, k, v, mask=None):

        attn = torch.matmul(q / self.temperature, k.transpose(2, 3))

        if mask is not None:
            attn = attn.masked_fill(mask == 0, -1e9)

        attn = self.dropout(F.softmax(attn, dim=-1))
        output = torch.matmul(attn, v)

        return output, attn

つまり、ScaledDotProductAttentionというところで、inputと同サイズのattnに対して、mask_token == 0なら、Attentionを 10^ {-9}にする。別の言い方をすればAttentionをほぼかけないという仕様になっている。

pytorchのmasked_fillについて見てみると、

Tensor.masked_fill_(mask, value) Fills elements of self tensor with value where mask is True. The shape of mask must be broadcastable with the shape of the underlying tensor.

と書いてある。つまり、Pytorch君が勝手にBrodcastするから、次元のばらつきは1を採用しても、次元が同じならいいよということである。 pad_maskについてはいいとして、subsequenct_maskについてはよくわかっていない。

最終的にはpad_maskのサイズは[Batch_size, 1, 1, Sequence_length]となり、subsequence_maskのサイズは[Batch_size, 1, Sequence_Length, Sequence_Length]となる。

で、maskをかける時点でのAttentionのサイズは[Batch_size, N_head, Len_Q, Len_K]となっている。 N_headでBroadcastするとして、subsequent_maskについては、 k = dec_input, q = dec_input, v=dec_inputなので、Len_Q == Len_K == Len_V == Sequence_Lengthとなっている。

まあ、サイズ的にはOKですね。

ただ、疑問点としては多分このsubsequence_maskはDecoderへの入力を順番に行うっていう意味で、Maskしてると思うんだけど、どうしてこの実装でDecoderのn番目の入力時にn+1番以降をMaskできるんだろう?

モデルの学習方法について見ていくと…。

タスクとしては

input = 私 の こと が 好き です か ?

target = あなた が 世界 で 一番 大好き です 。

として、EncoderにInputを代入し、Decoderには 「 あなた が 世界 で 一番 大好き です 。」を入力して、 「あなた が 世界 で 一番 大好き です 。 」を出力するように学習する。

つまり、両方に共通する「あなた が 世界 で 一番 大好き です 。 」をそのまま出力しないようにしないといけないってことかー。 GPT-2のPre-Trainingはどうなんだろう?

追記:調べたらGPT-2のPre-trainingでも同じ方法をとってましたー。

まあ、それは置いておいて、実際に使用する際は\<BOS>だけをDecoderに突っ込んで、あとは順番に回帰して\<EOS>が出るまで出力を繰り返せばいいのか。

なんとなくだけど、Taskについてよく考えたらMaskの意味が分かるかも?

ていうか、この写真がわかりやすい。

f:id:kaya-takashiro:20210827224352j:plain
Attention

Encoderは別にAttentionを取り出すだけだから、同じような出力をする、Targetの入力のみにSubsequence_maskが適用されるのか?Seq2Seqみたいに1tokenづつ入力するわけじゃないから、あんなに学習が早いのか!納得。

Multi-Head Attention

まあ、Attentionについてはいろいろとあるから、割愛。 でも、Multi-Headとかよくわからん。

実装はさっき見た通り。ただ、Tensorflowの実装のほうが見やすいのでそっちを見ると、

  def call(self, v, k, q, mask):
    batch_size = tf.shape(q)[0]

    q = self.wq(q)  # (batch_size, seq_len, d_model)
    k = self.wk(k)  # (batch_size, seq_len, d_model)
    v = self.wv(v)  # (batch_size, seq_len, d_model)

    q = self.split_heads(q, batch_size)  # (batch_size, num_heads, seq_len_q, depth)
    k = self.split_heads(k, batch_size)  # (batch_size, num_heads, seq_len_k, depth)
    v = self.split_heads(v, batch_size)  # (batch_size, num_heads, seq_len_v, depth)

    # scaled_attention.shape == (batch_size, num_heads, seq_len_q, depth)
    # attention_weights.shape == (batch_size, num_heads, seq_len_q, seq_len_k)
    scaled_attention, attention_weights = scaled_dot_product_attention(
        q, k, v, mask)

    scaled_attention = tf.transpose(scaled_attention, perm=[0, 2, 1, 3])  # (batch_size, seq_len_q, num_heads, depth)

    concat_attention = tf.reshape(scaled_attention, 
                                  (batch_size, -1, self.d_model))  # (batch_size, seq_len_q, d_model)

    output = self.dense(concat_attention)  # (batch_size, seq_len_q, d_model)

    return output, attention_weights

つまり、Multi-Headとはd_modelのサイズの行列をnum_headsとdepthの大きさに分けて、Attentionを計算することみたい。

何故?

Attentionの分割をしても、別にinputの分割をするわけではないし、ただattention内の重みの影響が互いに少なくなったくらいの影響しかないのかと思った。というか、なんか、それぞれのHeadの重みが異なるっていうのがなんか信じられない。

調べてみる

This allows the model to attend to different “representation sub-spaces” at different positions, akin to using different filters to create different features maps in a single layer in a CNN.

ということで、CNNのFilterと同じような作用をしているってことらしいんだけど、CNNでつかうFilterのmapとか固定値なのに、QueryもKeyもValueも全部可変値やん!とか突っ込み入れたくなる。 なんか、Mulit-Headにすると精度は上がるけど、実際はBlackboxっぽい。

EncoderのAttentionについて Q = K = V = EncoderInputとなってる。本当に self-Attentionだ。 DecoderのAttentionについては、あのsubsequent_maskを突っ込むやつは Q = K= V = Decoder_Inputで、もう一方のEncoderのOutputを突っ込むやつは Q = Decoder_output, K =Encoder_output, V = Encoder_outputとなっている。

QとKで入力文と出力文の関係を見つけ出す!っていうのはいいんだけど、実際Valueはどういった役割してんのかはわからん。QとKの積をsoftmaxかけるってことはそれぞれのHead内で、なんの確率を出してんだ?

たぶん、これが本当の意味でのAttentionの確率っていうことかな。つまり、どこのEncoder_inputの単語に着目するのかっていう。

それをValue=Encoder_inputに掛けることでDecoderの予測値を出すのか?

そういえば、実装を見てみると、

     self.trg_word_prj = nn.Linear(d_model, n_trg_vocab, bias=False)

    def forward(self, src_seq, trg_seq):

        src_mask = get_pad_mask(src_seq, self.src_pad_idx)
        trg_mask = get_pad_mask(trg_seq, self.trg_pad_idx) & get_subsequent_mask(trg_seq)

        enc_output, *_ = self.encoder(src_seq, src_mask)
        dec_output, *_ = self.decoder(trg_seq, trg_mask, enc_output, src_mask)
        seq_logit = self.trg_word_prj(dec_output)
        if self.scale_prj:
            seq_logit *= self.d_model ** -0.5

        return seq_logit.view(-1, seq_logit.size(2))

っていうことだから、出力は[Batch_size, Decoder_sequence_Length, Vocab_size]ってことかな?

まあ、それは置いておいて、DecoderにおけるDecoderのSelf-AttentionとEncoder-Decoder Attentionの組み合わせ方は以下の通り。

  def call(self, x, enc_output, training, 
           look_ahead_mask, padding_mask):
    # enc_output.shape == (batch_size, input_seq_len, d_model)

    attn1, attn_weights_block1 = self.mha1(x, x, x, look_ahead_mask)  # (batch_size, target_seq_len, d_model)
    attn1 = self.dropout1(attn1, training=training)
    out1 = self.layernorm1(attn1 + x)

    attn2, attn_weights_block2 = self.mha2(
        enc_output, enc_output, out1, padding_mask)  # (batch_size, target_seq_len, d_model)
    attn2 = self.dropout2(attn2, training=training)
    out2 = self.layernorm2(attn2 + out1)  # (batch_size, target_seq_len, d_model)

    ffn_output = self.ffn(out2)  # (batch_size, target_seq_len, d_model)
    ffn_output = self.dropout3(ffn_output, training=training)
    out3 = self.layernorm3(ffn_output + out2)  # (batch_size, target_seq_len, d_model)

    return out3, attn_weights_block1, attn_weights_block2

つまり、自分自身にAttentionをた結果に対して、Encoderの結果からAttentionを作成し、そのAttentionをEncoderの結果に掛けているのだね。マジで頭の中がこんがらがる(笑)。

Point-wise Feedforward-Network

Attention以外にも全結合層を入れてる。表現力を高めているのかな?なんか、Transformerってこうして見てみると、Seq2seqよりもCNNに近い気がする。

Positinal Encoding

やってまいりました。一番わけのわかんないやつ。 ReccurentもCNNもない(つまり、ベースはFNNだけ)のモデルがTransformerであり、Tokenの相対位置or絶対位置を入れるために、入れたとある。

よくわからんから、実装を見てみる。

def get_angles(pos, i, d_model):
  angle_rates = 1 / np.power(10000, (2 * (i//2)) / np.float32(d_model))
  return pos * angle_rates

def positional_encoding(position, d_model):
  angle_rads = get_angles(np.arange(position)[:, np.newaxis],
                          np.arange(d_model)[np.newaxis, :],
                          d_model)

  # 配列中の偶数インデックスにはsinを適用; 2i
  angle_rads[:, 0::2] = np.sin(angle_rads[:, 0::2])

  # 配列中の奇数インデックスにはcosを適用; 2i+1
  angle_rads[:, 1::2] = np.cos(angle_rads[:, 1::2])

  pos_encoding = angle_rads[np.newaxis, ...]

  return tf.cast(pos_encoding, dtype=tf.float32)

これをグラフに直すとこうなるらしい。

f:id:kaya-takashiro:20210827215839j:plain
positionalEncoding

グラフのpositionがtokenの位置で、DepthがEmbedding次の次元?んなわけないか。

    self.embedding = tf.keras.layers.Embedding(input_vocab_size, d_model)
    self.pos_encoding = positional_encoding(maximum_position_encoding, 
                                            self.d_model)

実装の中では上記のように定義していて

    # 埋め込みと位置エンコーディングを合算する
    x = self.embedding(x)  # (batch_size, input_seq_len, d_model)
    x *= tf.math.sqrt(tf.cast(self.d_model, tf.float32))
    x += self.pos_encoding[:, :seq_len, :]

っていう感じで使用している。 つまり、d_modelのサイズ = positonal_encodingのdepth, seq_len = positional_encodingのpositionってことか。

つまり、各Depthにおいて、token間の距離 = positionの相対差が一定になっているんじゃね? 問題は各Depth間の関係がわからん。

あっ、条件は2つだけか! 1.各positionのVectorは重複しない一意なものであること。 2.同じ距離のPostion間のVectorは一定であること。

なんか、これを満たしてるっぽい気がする。

実験してみる。

    encoding = positional_encoding(10000, 1024)

    print(encoding[0] - encoding[1])
    print(encoding[100] - encoding[101])

    print(np.linalg.norm(encoding[0] - encoding[1]))
    print(np.linalg.norm(encoding[100] - encoding[101]))

    print(np.linalg.norm(encoding[0] - encoding[100]))
    print(np.linalg.norm(encoding[1000] - encoding[1100]))

Result
[-8.41470985e-01  4.59697694e-01 -8.31705202e-01 ...  5.37303912e-09
 -1.01815172e-04  5.18316468e-09]
[-9.58391428e-01 -2.96859975e-02  2.35073541e-01 ...  1.07996133e-06
 -1.01809842e-04  1.04179791e-06]
5.208103571044113
5.208103571044114
24.015006646071527
24.01500664607152

確かに距離が一定である!すげー。マジで。

つまり、word2vecでの king - man = queen - woman と同じようにVectorがマップされるのか。Wow!

次回はBERT: Pre-training of Deep Bidirectional Transformers for Language Understandingっていう論文に行こう!

そうしたら、Dialogue系の論文探すか…。

2本目! REASONING ABOUT ENTAILMENT WITH NEURAL ATTENTION

前回のgpt-2の引用の論文であるImproving Language Understanding by Generative Pre-Trainingで使われていたこの論文。

まずは、どうこの論文がImproving Language Understanding by Generative Pre-Trainingで使われていたのかを復習すると..。

通常

input ->Encoder -> output h_n from Encoder ->input h_n to Decoder & input token to Decoder -> output predicted token from Decoder

gpt-2

input -> Decoder -> output predicted token from Decoder

つまり、Encoderを介さずに、Decoderに直接ぶち込んで、出力文を予測していました。 この時の inputは  input sentence label data などの tokenで分割したデータの入力となっています。

今回の論文により、Encoderでh_nを出力して、Decoderに入力しなくても、token分割により学習できることが示されています。

では、詳しく見ていきましょう!

Introduction

今回のTaskについて。今回の論文におけるタスクはRTEが用いられています。

RTEは初めて聞いたので、調べてみると、前提の文章から導かれる、仮定の分が1.対比、2.無関係、3.因果関係 のどれかを判定するというTaskでした。

githubのコードで調べてみると。

def load_dataset(dataset_dir):
    print("Loading SNLI dataset")
    dataset = {}
    for split in ['train', 'dev', 'test']:
        split_path = os.path.join(dataset_dir, 'snli_1.0_{}.txt'.format(split))
        df = pd.read_csv(split_path, delimiter='\t')
        dataset[split] = {
            "premises": df[["sentence1"]].values,
            "hypothesis": df[["sentence2"]].values,
            "targets": df[["gold_label"]].values
        }

    return dataset

っていう感じで、データを集めているみたい。

で、この SNLI dataset について調べてみると、こんな感じ。 SNLI = The Stanford Natural Language Inference Corpusの略称。

中身を取り出してみるとこう。

gold_label   sentence1_binary_parse  sentence2_binary_parse  sentence1_parse sentence2_parse sentence1   sentence2   captionID   pairID  label1  label2  label3  label4  label5
neutral ( ( ( A person ) ( on ( a horse ) ) ) ( ( jumps ( over ( a ( broken ( down airplane ) ) ) ) ) . ) ) ( ( A person ) ( ( is ( ( training ( his horse ) ) ( for ( a competition ) ) ) ) . ) )  (ROOT (S (NP (NP (DT A) (NN person)) (PP (IN on) (NP (DT a) (NN horse)))) (VP (VBZ jumps) (PP (IN over) (NP (DT a) (JJ broken) (JJ down) (NN airplane)))) (. .)))   (ROOT (S (NP (DT A) (NN person)) (VP (VBZ is) (VP (VBG training) (NP (PRP$ his) (NN horse)) (PP (IN for) (NP (DT a) (NN competition))))) (. .)))    A person on a horse jumps over a broken down airplane.  A person is training his horse for a competition.   3416050480.jpg#4    3416050480.jpg#4r1n neutral             
contradiction   ( ( ( A person ) ( on ( a horse ) ) ) ( ( jumps ( over ( a ( broken ( down airplane ) ) ) ) ) . ) ) ( ( A person ) ( ( ( ( is ( at ( a diner ) ) ) , ) ( ordering ( an omelette ) ) ) . ) ) (ROOT (S (NP (NP (DT A) (NN person)) (PP (IN on) (NP (DT a) (NN horse)))) (VP (VBZ jumps) (PP (IN over) (NP (DT a) (JJ broken) (JJ down) (NN airplane)))) (. .)))   (ROOT (S (NP (DT A) (NN person)) (VP (VBZ is) (PP (IN at) (NP (DT a) (NN diner))) (, ,) (S (VP (VBG ordering) (NP (DT an) (NN omelette))))) (. .))) A person on a horse jumps over a broken down airplane.  A person is at a diner, ordering an omelette.   3416050480.jpg#4    3416050480.jpg#4r1c contradiction               
entailment  ( ( ( A person ) ( on ( a horse ) ) ) ( ( jumps ( over ( a ( broken ( down airplane ) ) ) ) ) . ) ) ( ( A person ) ( ( ( ( is outdoors ) , ) ( on ( a horse ) ) ) . ) ) (ROOT (S (NP (NP (DT A) (NN person)) (PP (IN on) (NP (DT a) (NN horse)))) (VP (VBZ jumps) (PP (IN over) (NP (DT a) (JJ broken) (JJ down) (NN airplane)))) (. .)))   (ROOT (S (NP (DT A) (NN person)) (VP (VBZ is) (ADVP (RB outdoors)) (, ,) (PP (IN on) (NP (DT a) (NN horse)))) (. .)))   A person on a horse jumps over a broken down airplane.  A person is outdoors, on a horse.   3416050480.jpg#4    3416050480.jpg#4r1e entailment              
neutral ( Children ( ( ( smiling and ) waving ) ( at camera ) ) )   ( They ( are ( smiling ( at ( their parents ) ) ) ) )   (ROOT (NP (S (NP (NNP Children)) (VP (VBG smiling) (CC and) (VBG waving) (PP (IN at) (NP (NN camera)))))))  (ROOT (S (NP (PRP They)) (VP (VBP are) (VP (VBG smiling) (PP (IN at) (NP (PRP$ their) (NNS parents)))))))   Children smiling and waving at camera   They are smiling at their parents   2267923837.jpg#2    2267923837.jpg#2r1n neutral             
entailment  ( Children ( ( ( smiling and ) waving ) ( at camera ) ) )   ( There ( ( are children ) present ) )  (ROOT (NP (S (NP (NNP Children)) (VP (VBG smiling) (CC and) (VBG waving) (PP (IN at) (NP (NN camera)))))))  (ROOT (S (NP (EX There)) (VP (VBP are) (NP (NNS children)) (ADVP (RB present)))))   Children smiling and waving at camera   There are children present  2267923837.jpg#2    2267923837.jpg#2r1e entailment              

スッゲー見づらいけど、実際に使われているのはgold_label, sentence1, sentence2の3つだけ。いたって普通。

っていうか、「馬に乗った男が墜落した飛行機を飛び越える」とかファンタジーすぎる(笑)。

Methods

LSTM with Attetionのモデルを採用しています。

LSTMはググればいっぱい出てくるので省略。

今回の論文の肝は

1.Conditional Encoding

2.Attencion

3.Word by Word Attention

の3つらしい。

f:id:kaya-takashiro:20210825204009j:plain
model

labelの出力 o _ Nを計算するのには、 o _ N = \sigma (W^ o H + b^ o)という計算式が必要で  H = [x _ t, h _ {N-1}]という式からHが導かれる。つまり、Attentionをかけるのは h_{N-1}だけに掛ければいいみたい。

ちなみに、実装を見てみると2.Attentionは実装されておらず、Conditinal EncodingとWord by Word Attentionの2つだけ実装されているみたい。

1.Conditinal Encoding

Conditinal Encoding はPremiseとHypotheisをtokenで結んで入力するっていうことらしい。

Encoderのoutputである、h_nとかは使用せずにPremiseで学習した、c_i (Cell memory)を使用するからOKということらしい。

ここでの疑問点はこの論文でのTask はSequenceの入力でLabelの出力なのに、GPT-2ではSequenceの入力でSequenceを出力するQuestion-AnsweringのTaskとかをこなしていることである。マジでなんで?

2.Attention

Premiseの文の入力に対して割り当てるAttentionの重みづけらしい。

つまり、Premiseでの単語token_1 ~ token_Lから、Attentionでの重みをつけて、それをHypothesisのtoken_Nに割り当てるみたい。

3.Word-by-word Attention

Hypothesis のh_tの出力に対してのみ適用するAttention。 前の単語から出力したAttentionの重みづけを利用して計算するみたい。

ただし、hypothesisのhidden_outputすべてに対して、重みづけをするわけではなく、h_nに対してのみ重みづけをするみたい。

つまり、AttentionをかけるかWord-by-Word Attentionのどちらかでしか Attentionをかけられないってこと。

このほかにも、Two-way Attentionってのがあるけどよくわからん。

premiseのAttentionをhypothesisに掛けるのと同時に、hypothesisのAttentionをpremiseに掛けるらしい。

相変わらずAttentionがよくわかってないので、つぎは大本命のAttention is All You Needを見ていこうと思う。

てか、マジでなんで教師あり学習のGeneration Modelに対して、GPT-2がどうやってるのかがわからん!

GPT-2の論文を読み直すと、BERT: Pre-training of Deep Bidirectional Transformers for Language Understandingっていう論文も根拠になってたから、次の次で読もうと思う。