Transformer

ひと昔前、ニューラル機械翻訳では時系列的な入力を扱えるRNN (Recurrent Neural Network)ベースのsequence-to-sequenceが用いられていました。この時もAttentionというメカニズムは使われていましたが、2017年にGoogleから発表された論文 “Attention is All you need”では、AttentionだけでかつてのRNNの性能を大きく上回る機械翻訳精度を達成し、今では自然言語処理分野の技術は、ほぼAttentionベースの技術(Transformer, BERT, GPT等)が主流となっております。

このGoogleから発表されたAttentionベースのニューラル機械翻訳技術がTransformerです。BERTやGPTも、このTransformerの仕組みと共通しているので、このTransformerの理解が自然言語処理の分野では最重要だと思います。

RNNのAttention

では、まず Transformerで重要となる Attentionについて説明します。このAttentionは、従来のRNNベースの機械翻訳でも使われているため、まずRNNベースで、Attentionとは何なのかを説明します。

RNNの内部状態だけだと、最初の方で入力した単語の情報ほど情報が薄くなっていってしまいます。Attentionとは、デコーダで出力する単語が、エンコーダの入力単語のどの単語を注目すれば良いかを示す指標になります。これは、デコーダ側で所定の位置の単語の内部状態において、エンコーダ側の各入力における内部状態との相関(類似度)を計算し、各入力単語の内部状態に重みをつけて合算し、Context Vectorをいうものを作ります。このContext Vectorをデコーダの出力層に入力し、出力単語の尤度計算に影響させることで、Attentionを考慮したsequence-to-sequenceが可能になります。

このAttentionメカニズムは、query, key, valueに分けて説明されることが多いです。上のRNNの場合、queryはTarget側のベクトルになります。keyとvalueはSource側のベクトルになります。

まず、queryと各keyの内積をとってSoftmaxで正規化したものがAlignment Weightというベクトルになります。これはTarget側の所定の位置の単語が、どの入力単語と相関が高いかを示し指標です。

次に、そのAlignment Weightと各valueを掛けたものが、Context Vectorとなり、デコーダの出力層に影響を与えます。相関が高い単語ほど大きく影響を与えることができるようになります。

TransformerのAttention

では、Transformerの説明に入ります。TransformerはRNNのように前の入力の内部状態を引き継いでいく構造では無いです。すべてAttentionでの計算になります。エンコーダ側もしくはデコーダ側だけでAttentionを計算するものを Self Attentionといいます。また、RNNの時と同じように、デコーダの所定の位置の計算にエンコーダのAttentionを計算するものを Source Target Attentionといいます。

Self Attentionは、上の図だと、ある層において、各ポジションごとに他の入力とのAttention Weightを計算し、上の層の内部状態を計算するようになっています。

Source-Target Attentionは、基本的にエンコーダの最後の層(一番上)の内部状態を使います。こちらはRNNと同じようにAttentionを計算し、デコーダの次の層(上の層)を計算していきます。デコーダの最終層(一番上)から、出力単語を推定します。

RNNを使わないと、もはや語順とか関係ないんじゃないかと思うかもしれませんが、入力の部分にポジションエンコーディングという機構があり、この単語は全体の何番目に入力された単語かを示すベクトルが付与されています。

Transformerの全体の構成を論文から引用します。

左側がエンコーダで右側がデコーダになってます。それぞれN層積んで構成されます。この中で新たにでてくるのが、Masked Attentionと、Multi-Head Attentionかと思います。

Masked Attentionとは、デコーダでは未来の単語がわからない状態で推定しなければいけないため、未来の単語へのAttentionは使わないという意味でMaskします。

Multi-Head Attentionですが、TransformerはAttentionしか使わないので、Attentionを全体で行うと入力文章の意味をあまり理解せずに、単にエンコーダの単語からカンニングするような翻訳がでてしまう可能性があるため、エンコーダの単語群をいくつかのグループにわけて、いろんな視点からAttentionを計算することにより、単なるカンニングよりも深い翻訳ができるようにしているイメージです。

ここまでがTransformerの概要ですが、最近、大規模言語モデルとして注目されているBERTやGPTも、このTransformerがわかると理解できます。(というかTransformerの一種という位置付けです)BERTは、Transformerのエンコーダ部分で構成されており、文書分類、ラベリング等、さまざまな言語処理が可能です。GPTは、Transformerのデコーダ部分だけで構成されており、デコーダの先頭に文章を入れることで、その続きを生成してくれます。この後は、ChatGPTで注目され始めたGPTについても説明します。

GPT (Generative Pre-trained Transformer)

GPTとは、OpenAIなどが研究開発している Generative Pre-trained Transformerという大規模な生成AIの言語モデルで、タスクに特化した学習が必要なく自然な文章を生成できることが特徴です。

オリジナルのGPTモデルは、2018年にリリースされて、1億1700万のパラメータを持つ巨大なモデルでした。その後、GPT-2、GPT-3とスケールアップし、GPT-2では10倍の15億のパラメータ、GPT-3ではさらにその100倍の1750億のパラメータの超巨大な言語モデルとなっています。GPT-3.5で、ChatGPTがリリースされてテレビのニュースなどでもよく取り上げられるようになりました。

GPT-3の論文はこちらです。 Language Models are Few-Shot Learners

GPTですが、基本的にはTransformerのDecoderだけを使います。このDecoderのプレフィックスとして、タスクやFew shot Learningのサンプルを入れ、最後に本題となる質問を入れると、その後を生成してくれるイメージです。下にイメージ図を書きます。

これはTransformerのDecoderだけの構成なのですが、Few shot Learningでは、いくつかのサンプルを学習させる際にモデルの再学習やファインチューニングをするわけではなく、デコーダのプレフィックスとしてタスクやサンプルを入れます。そして本題の質問を入れると、これまでの入力の流れから推測し自然に続く文章を生成する過程で、質問の回答を生成してくれるというようなイメージです。

動画や論文などで紹介されている例を示します。

Translate English to French:   (Task Description)
sea otter =>  loutre de mar    (Example 1)
peppermint =>  menthe poivree  (Example 2)
plush giraffe =>  giraffe peluche  (Example 3)
cheese =>             (Question)

この例では、Task Descriptionとして、”Translate English to French:” としています。英語をフランス語に訳すというタスクを頼むという宣言です。

そしてその後に、サンプルとして3つほど、英語からフランス語に訳した例を入力します。

最後に cheeseは?という質問を入力しています。プロンプトの ”=>”で終わると、これまでの経験から cheeseのフランス語を答えればいいのだなとわかってくれます。そして、fromage(フランス語のチーズ)という単語を生成してくれます。文章の場合は、この後も1単語ずつTransformerのデコーダと同じように生成していきます。

さて、GPT-3を使ってみた感想ですが、文章だけみると本当に人間が書いたような文章だと思うぐらい自然な文章を生成してくれます。内容に関してはまだ弱い部分もあるようですが、自然な文章を生成する能力においてはかなり人間レベルに近いのではないかと思いました。

さらにChatGPTになると、Instruct-GPTという人手の採点による強化学習も組み込まれており、人が生成するにふさわしい文章がでてくるようになっております。言語能力としての汎用性能も上がり、Few Shot Learningしなくても、プロンプトに好きな指令を自然な文章で書けば、それに対応した文章を生成してくれます。内容も人間を超えてると思えるものがでてきたりします。誰でも使える魔法のようなチャットボットが誕生したわけです。

さらには、GPT-4もでてきました。より難しい依頼にも対応できるようになっていたり、言語としての美しさに磨きがかかっております。

今後、人間に求められるのは、創造性やオリジナリティの部分なのかなと思いました。

M系列符号で自己同期

人類の全ての活動がリモートで可能かどうか試されている今、リモートで再生される音声を同期させたいということもあるかと思います。リモート演奏等。

そこでM系列符号で自己同期を取る方法を思い出しましたので、簡単にご紹介します。

M系列符号とは

M系列符号とは、nビットのシフトレジスタから生成される周期が2^n-1の符号列なのですが、とても便利な特徴があります。

  • 自己相関のピークが1周期に1回(ぴったり合った時)だけあり、それ以外は-1(相関ほぼ無し)になる。
  • 0と1の発生確率がほぼ等しい。(擬似ノイズとしても使える)

この自己相関の特徴が、同期を取るのに大変便利なので、同期処理や測位など幅広く使われています。

自己相関のイメージ図を書きます。3ビットのシフトレジスタから生成された7ビットのM系列符号の例です。

7ビットのM系列符号を1ビットずつずらし、各ビットをXOR計算して加算したものが自己相関です。(XORで0の場合に-1を加算します。)上の図のようにぴったり合ってる場所だけが自己相関値が7になり、その他は-1(3勝4敗)になります。ほぼ無相関です。これを使えば、どこが始まりかわからない信号でも先頭を検出できます。

M系列符号生成のサンプルプログラム

M系列符号は、生成多項式から構成されるシフトレジスタの出力で作れるのですが、下記のようなプログラムで生成できます。生成多項式x^8 + x^4 + x^3 + x^2 + 1で生成する255ビットのM系列符号の例です。

#define SpreadingRate  (255)  
#define Generator    (11101)
#define MaxBit       (0x0100)
#define XorBit       (0x011d)

char * code_generator()
{
  int i;
  char *code;
  unsigned int shift = Generator;

  code = (char *)calloc(SpreadingRate, sizeof(char));

  for(i = 0; i < SpreadingRate; i++){
    if(shift & 0x01){
      code[i] = 1;
    }else{
      code[i] = 0;
    }
    shift = shift << 1;
    if(shift & MaxBit){
      shift = shift ^ XorBit;
    }
  }
  return(code);
}

SpreadingRateは符号ビット長になり、生成多項式の次元数nの2^n-1になります。Generatorは、シフトレジスタの初期値なので何でも良いです。変えると符号ビット列が巡回します。MaxBitは最高次のみ1とするビット列で、XorBitは最高次を含めた各係数のビット列です。

M系列符号の自己同期で遊んでみました

M系列符号を音声信号にしてスピーカから送信し、マイクで録音して自己同期をとるという遊びをしてみました。図に書きます。

encoderは、M系列符号を音響信号にしてWaveファイルを生成します。このWaveファイルをスピーカで再生し、マイクで録音してWaveファイルにします。この録音Waveファイルをdecoderで自己相関計算し、ピーク(同期ポイント)が検出できるかをやってみました。

こちらが1サンプルずつずらして自己相関を計算した相関値です。あるポイントだけ鋭いピークが立っているので、ここがM系列符号の始まりだということがわかります。反射波などがあれば、他の場所にも少しピークが立ちます。つまり音響経路のインパルス応答と見なせます。

ちなみにM系列符号は、下図の左側のようにそのまま矩形波で送っても良いですし、右側のように位相変調して送っても同じ信号で相関を取ればよいのでどちらでも可能です。

この遊びのサンプルプログラムをこちらにおきますので、何かお役に立てそうでしたらお使いください。

Msequence-sample.zip

ReactをApacheで動かす

ブラウザのアプリ開発もSPA (Single Page Application)が主流となりつつあります。Slackとかブラウザで動作させていても快適ですね。

このSPAのアプリを開発するJavascriptのフレームワークとして、Angular、React、Vue.js等があります。今回は、このReactをApacheで動かすまでのお話です。

Reactとは

まずReact等のSPA用のライブラリがなぜ便利なのかですが、基本的にはコンポーネントを分離して開発しやすいのが大きなメリットだと思います。まず前提としてはページに何か更新があった時には、ページ全体を更新(DOMの再構築)するのではなく、更新した部分だけを変更します。

ページはヘッダ部やサイド部などいろいろなコンポーネントに分かれていると思いますが、例えばサイド部のボタンを押して何か更新したときに、ヘッダ部やコンテンツ部にも変更がおよぶことがあると思います。これがとても面倒なことだということは何となくわかるかと思います。

Reactライブラリを使った場合、ボタンを押した結果を再描画する場合は、Reactライブラリに「全体再描画」という依頼をするだけで、差分のある部分だけを更新してくれます。これにより、各コンポーネントを独立して開発しやすくなります。

前準備

では、実際にReactを使うための説明に入ります。Reactの開発をするためには、node.jsやnpmコマンドが必要になってきますので、これらをインストールします。

Linuxの場合 (Ubuntu)

# apt install nodejs npm

Mac OSの場合 (まずnodebrewというnpmやnode.jsを管理するプログラムを入れます)

# brew install nodebrew
# nodebrew install-binary latest

でいけるかと思います。

Create ReactAppのインストール

Create ReactAppは、新しいReactアプリを作成するのに便利なツールで、Reactを学習するための快適な環境があります。まず、これをインストールします。

# npm install -g create-react-app

Reactのサンプルアプリの作成

ReactのHelloWorld的なサンプルアプリは、上記でインストールしたCreate ReactAppを使って簡単に作成できます。

# npx create-react-app react-sample

これで、カレントディレクトリにreact-sampleをというフォルダができて、その下に初期プログラムができています。試しにこれが動くかどうかを確認してみましょう。react-sampleというフォルダに移動して、下記のコマンドを実行します。

# npm run start

3000番ポートでWebサーバが立ち上がり、Reactのアプリが閲覧できるようになります。自動でブラウザが立ち上がると思いますが、http://localhost:3000/ にアクセスすると閲覧できると思います。

こんなページがでたら成功です。

Apacheから閲覧できるようにする

先ほどのテストは独自のWebサーバを3000番ポートで立ち上げて確認できましたが、実際にreactアプリを運用するにあたっては、HTTP(80番ポート)でアクセスできるようにしたいです。ですが大抵の場合WebサーバはApacheやnginxがWebサーバとして動いているので、reactだけの専用サーバを80番ポートで立ち上げられない場合が多いです。

ここでは、Apacheで上記のreactアプリを動かす手順を書きます。

まず、先ほどのreactアプリをbuildする必要があるのですが、デプロイするURLを設定する必要があります。これは、package.jsonという設定ファイルに追記します。

{
  "name": "react-sample",
  "version": "0.1.0",

このような記載で始まっていると思いますが、nameの下にでもデプロイするURLを下記のように追記します。

{
  "name": "react-sample",
  "homepage": "http://localhost/react-sample/",
  "version": "0.1.0",

そして、buildします。

# npm run build

そうすると、上記のURLにデプロイする用にbuildされ、buildフォルダが作成されます。そのbuildフォルダを Apacheのドキュメントルートから参照できる場所にコピーすれば Apacheから閲覧できようになります。Apacheのドキュメントルートが /usr/local/htdocs の場合は、

# cp -R build /usr/local/htdocs/react-sample

これで、http://localhost/react-sample のURLで閲覧できるようになります。

Hello World的なものを作ってみる

ここまでだと、何のファイルがどういう役割で表示されているのかわからないです。src/ の下にあるファイルを編集して、buildし、buildフォルダをコピーすれば変更が反映されますが、どこを編集すれば良いかを説明します。

まず、index.jsからスタートします。

ReactDOM.render(
  <react.strictmode>
    <app>
  </app></react.strictmode>,
  document.getElementById('root')
);

このようなスクリプトが書いてあると思います。これはこのままで良いのですが、ここで <App />というタグがあり、Appが実行されます。Appは App.jsというファイルに書かれていまして、それをimportしております。

App.jsを見ると、下記のようなスクリプトがあり、divで囲まれた中が描画されます。サンプルは画像を表示するものでしたが、ここを書き換えてHello Wolrdを表示してみます。

function App(){
    return (
        <div classname="App">
           <h1> Hello World </h1>
           ここに書いたものが描画されます。
        </div>
    );
}

divタグの中にHTML的なものを書いていけば、それが表示されます。これだと普通にHTMLを書くのと変わりないので、RouterやBrowserRouterという機能を使って、コンポーネントを組み合わせてページを作るところまで説明します。

まず、react-router-domというものをインストールする必要があると思います。

# npm install react-router-dom

これでインストールできると思います。そして、ページをHeader部、Content部、Footer部にわけて表示してみます。 App.jsを下記のように書いて、Header, Content, Footerを読み込むようにします。各コンポーネントは、Header.js, Content.js, Footer.js に記載します。

import { Route ,BrowserRouter} from 'react-router-dom';
import Header from './Header';
import Content from './Content';
import Footer from './Footer';

function App(){
    return (
        <div classname="App">
         <h1>Hello World</h1>
         <browserrouter>
           <route path="/" component="{Header}/">
           <route path="/" component="{Content}/">
           <route path="/" component="{Footer}/">
         </browserrouter>
        </div>
    );
}
export default App;

Header.js、Content.js、Footer.jsは下記のように書いておきます。(Header.jsだけの例)

import React, { Component } from 'react';

class header extends Component{
    render(){
        return(
            <div>
              ここはヘッダ部です。
            </div>
        );
    } 
}
export default header;

こんな感じでContent.jsもFooter.jsも書いて buildすると、各コンポーネントを読み込んだHTMLを描画してくれます。

SPAを作るためにRouter機能は必要となってくるので、ここまでイメージができていると Reactに入っていきやすくなります。