#CNTK 2.2 Python API 解説 (3) - <言語理解> 双方向リカレント・ネットワークでスロットタギング
##0. はじめに
◆ CNTK ( Microsoft Cognitive Toolkit ) 2.2 の Python API 解説第3弾です。
言語理解 (スロットタギング) をテーマに、埋め込みを利用した標準的な LSTM モデルをベースに双方向リカレント・ネットワーク (Bidirectional RNN) を CNTK で実装します。
スロットタギング (= slot tagging) は自然言語処理の課題の一つです。
顧客からのクエリ (問い合わせ) 文を分解して得られた、単語群の個々の単語をそれぞれクラスにタグ付けします。クラスはラベルとして提供されます。シンプルですが様々な応用が考えられる課題です。
また、CNTK の RNN モデルにおいてシークエンス処理の理解は必須ですので、(シークエンスも含む) CNTK CTF フォーマットの完全な仕様についても解説しておきました。
本記事の内容 :
- 動作環境と Jupyter Notebook について
- スロットタギング概要と ATIS データセット
- CNTK テキストフォーマット詳細
- ATIS データフォーマット
- モデル作成
- データ・リーディング
- トレーニングと評価
- モデルの改良
本記事は以下のチュートリアルとドキュメントを参考にしています :
1. 動作環境と Jupyter Notebook について
動作環境
動作環境の構築が必要な場合には、Cognitive Toolkit 2.2 を Azure Linux GPU 仮想マシンにインストール を参考にしてください。Azure ポータルと Ubuntu Linux にある程度慣れていれば、30 分程度で以下のような環境が構築できるかと思います :
- Azure NC 仮想マシン with NVIDIA Tesla® K80 GPU
- Ubuntu 16.04 LTS
- NVIDIA CUDA 8.0 & cuDNN 6.0
- Anaconda 3 4.1.1
- CNTK 2.2 (for GPU)
Jupyter Notebook
また、本記事でも CNTK チュートリアルでも Jupyter Notebook を多用します。
Jupyter Notebook の利用方法については「CNTK 2.2 Python API 入門 (2)」の記事中の Jupyter Notebook の活用 を参照してください。
##2. スロットタギング概要と ATIS データセット
最初にスロットタギングについて簡単に説明した後、題材とするデータセットをダウンロードします。
※ この章から Jupyter Notebook の利用を想定しています。
###2-1 スロットタギング
スロットタギング (= slot tagging) は自然言語処理の課題の一つです。
顧客からのクエリ (問い合わせ) 文を分解して得られた、単語群の個々の単語をそれぞれクラスにタグ付けします。クラスはラベルとして提供されます。シンプルですが様々な応用が考えられる課題です。
本記事では、スロットタギングのタスクを解くために、CNTK でどのようにリカレント・ニューラルネットワーク (RNN) を実装してテキスト処理を遂行するかを示します。
最初のモデルは、単純な (線形) 単語埋め込みにリカレント LSTM そして Dense 層をスタックします。
そしてこの基本モデルにバッチ正規化が追加され、更には近傍の単語を先読み (= lookahead) してそして双方向に (bidirectionally) 実行するように拡張されます。いわゆる Bidirectional RNN を得ます。
※ リカレント・ニューラルネットワーク と テキスト埋め込み についての基礎知識があると読みやすいでしょう。
###2-2 データセットのダウンロード
スロットタギングのタスクのために利用する題材は ATIS (Air Travel Information Services) データセットです。
このデータセットは ARPA-SLS (Advanced Research Projects Agency Spoken Language Systems) 技術開発プログラムの後援のもとで収集されたスピーチと自然言語データのコーパスを含みます。コーパスは ATIS ドメインにおけるデータを含みます。
※ コーパスは、一般には、自然言語処理の研究を主目的とする、自然言語の文章や音声の構造化データベースを意味します。
◆ ATIS データセットは (生データではなく) 簡単に前処理されたバージョンを使用します。
下の Jupyter セルを実行すればそのデータセットを自動的にダウンロードできます。
-
このコードブロックでは CNTK の Python API は利用しません。
-
新規の文の上でモデルがどのように予測するかを見るためには、クエリ (queries.wl) と スロット (slots.wl) のための語彙ファイルも必要ですので併せてダウンロードします。
-
ATIS 訓練データ (atis.train.ctf) と テストデータ (atis.test.ctf) ファイルを手動でダウンロードして Notebook と同じフォルダにそれらを置いても良いです。
from __future__ import print_function # Use a function definition from future version (say 3.x from 2.7 interpreter)
import requests
import os
def download(url, filename):
""" utility function to download a file """
response = requests.get(url, stream=True)
with open(filename, "wb") as handle:
for data in response.iter_content():
handle.write(data)
locations = ['Tutorials/SLUHandsOn', 'Examples/LanguageUnderstanding/ATIS/BrainScript']
data = {
'train': { 'file': 'atis.train.ctf', 'location': 0 },
'test': { 'file': 'atis.test.ctf', 'location': 0 },
'query': { 'file': 'query.wl', 'location': 1 },
'slots': { 'file': 'slots.wl', 'location': 1 }
}
for item in data.values():
location = locations[item['location']]
path = os.path.join('..', location, item['file'])
if os.path.exists(path):
print("Reusing locally cached:", item['file'])
# Update path
item['file'] = path
elif os.path.exists(item['file']):
print("Reusing locally cached:", item['file'])
else:
print("Starting download:", item['file'])
url = "https://github.com/Microsoft/CNTK/blob/release/2.2/%s/%s?raw=true"%(location, item['file'])
download(url, item['file'])
print("Download completed")
Starting download: atis.train.ctf
Download completed
Starting download: atis.test.ctf
Download completed
Starting download: query.wl
Download completed
Starting download: slots.wl
Download completed
◆ ダウンロードされたファイルは、Jupyter Notebook を実行しているディレクトリに保存されます。
簡単に内容を確認してみましょう :
$ ls -lh *.ctf *.wl
-rw-rw-r-- 1 masao masao 485K Nov 10 23:54 atis.test.ctf
-rw-rw-r-- 1 masao masao 2.9M Nov 10 23:54 atis.train.ctf
-rw-rw-r-- 1 masao masao 5.8K Nov 10 23:54 query.wl
-rw-rw-r-- 1 masao masao 2.4K Nov 10 23:54 slots.wl
$ wc -l *.ctf *.wl
10984 atis.test.ctf
66547 atis.train.ctf
943 query.wl
129 slots.wl
78603 total
atis.train.ctf
, atis.test.ctf
は CTF テキスト・フォーマットです。カラムの意味ついては 4 章で説明されます :
$ head -n 25 atis.train.ctf
0 |S0 178:1 |# BOS |S1 14:1 |# flight |S2 128:1 |# O
0 |S0 479:1 |# i |S2 128:1 |# O
0 |S0 902:1 |# want |S2 128:1 |# O
0 |S0 851:1 |# to |S2 128:1 |# O
0 |S0 431:1 |# fly |S2 128:1 |# O
0 |S0 444:1 |# from |S2 128:1 |# O
0 |S0 266:1 |# boston |S2 48:1 |# B-fromloc.city_name
0 |S0 240:1 |# at |S2 128:1 |# O
0 |S0 168:1 |# 838 |S2 35:1 |# B-depart_time.time
0 |S0 210:1 |# am |S2 100:1 |# I-depart_time.time
0 |S0 215:1 |# and |S2 128:1 |# O
0 |S0 236:1 |# arrive |S2 128:1 |# O
0 |S0 482:1 |# in |S2 128:1 |# O
0 |S0 351:1 |# denver |S2 78:1 |# B-toloc.city_name
0 |S0 240:1 |# at |S2 128:1 |# O
0 |S0 27:1 |# 1110 |S2 14:1 |# B-arrive_time.time
0 |S0 482:1 |# in |S2 128:1 |# O
0 |S0 827:1 |# the |S2 128:1 |# O
0 |S0 606:1 |# morning |S2 12:1 |# B-arrive_time.period_of_day
0 |S0 179:1 |# EOS |S2 128:1 |# O
1 |S0 178:1 |# BOS |S1 14:1 |# flight |S2 128:1 |# O
1 |S0 916:1 |# what |S2 128:1 |# O
1 |S0 429:1 |# flights |S2 128:1 |# O
1 |S0 228:1 |# are |S2 128:1 |# O
1 |S0 244:1 |# available |S2 128:1 |# O
query.wl
は文の先端・終端記号: BOS
, EOS
を含む単語のリストです :
$ cat query.wl | tail -n 770 | head -n 25
928
932
934
950
98
BOS
EOS
a
aa
abbreviation
abbreviations
able
about
ac
across
actually
advertises
after
afternoon
afternoons
afterwards
again
air
aircraft
slots.wl
はスロット・ラベルを含みます。これについても 4 章で説明されます :
$ head -n 20 slots.wl
B-aircraft_code
B-airline_code
B-airline_name
B-airport_code
B-airport_name
B-arrive_date.date_relative
B-arrive_date.day_name
B-arrive_date.day_number
B-arrive_date.month_name
B-arrive_date.today_relative
B-arrive_time.end_time
B-arrive_time.period_mod
B-arrive_time.period_of_day
B-arrive_time.start_time
B-arrive_time.time
B-arrive_time.time_relative
B-booking_class
B-city_name
B-class_type
B-compartment
$ tail -n 20 slots.wl
I-fromloc.airport_name
I-fromloc.city_name
I-fromloc.state_name
I-meal_code
I-meal_description
I-mod
I-restriction_code
I-return_date.date_relative
I-return_date.day_number
I-return_date.today_relative
I-round_trip
I-state_name
I-stoploc.city_name
I-time
I-today_relative
I-toloc.airport_name
I-toloc.city_name
I-toloc.state_name
I-transport_type
O
##3. CNTK テキストフォーマット詳細
(前章でダウンロードした) ATIS データセットのフォーマットは、シークエンスを含む CNTK CTF フォーマット として前処理されています。CTF フォーマットについては以下の記事で基本事項を説明していますが :
- CNTK 2.2 Python API 入門 (3) - MNIST 総集編 「2. MNIST データセットを CNTK CTF フォーマットでセーブする」
シークエンスを含む CTF フォーマットについてはまだふれていません。
CNTK の RNN モデルにおいてシークエンス処理の理解は必須ですので、
本題に入る前にここで (シークエンスを含む) CTF フォーマットの完全な仕様について解説しておきます。
###3-1 CNTK テキストフォーマット (CTF)
CNTK テキストフォーマット (以後、CTF) に整形された入力ファイルの各行は、
(一つまたはそれ以上の入力ストリームのための) 一つのサンプルデータを含みます。
そして 総ての行は (明示的であれ暗黙的であれ) シークエンスにアタッチされます ので、それは一つまたはそれ以上の関係性を定義します。
※ 暗黙的とは、シークエンス Id が省略されている場合を意味します。
各入力行は次のようなフォーマットでなければなりません :
[Sequence_Id](Sample or Comment)+
ここで
Sample=|Input_Name (Value )*
Comment=|# some content
(仕様に記号の説明がないのですが、) [Sequence_Id]
の角括弧 []
はシークエンス Id が省略可能であることを示しているものと思われます。記号 +
, *
はおそらく正規表現的な解釈で良いのでしょう、+
は (直前のトークンの) 1 回以上の繰り返し、*
は 0 回以上の繰り返しを意味します。
念のため、上のシンタックスを日本語に直して記載します :
[シークエンス_Id](サンプル または コメント)+
シークエンス Id は省略可能、サンプルまたはコメントの1回以上の繰り返しで行が構成されます。ここで
サンプル=|入力_名 (値 )*
コメント=|# 何某かの内容
入力_名は、通常は特徴やラベルの名前になります。
見た目は非常にシンプルなのですが、以下のような規則が設けられています :
-
各行はシークエンス id で始まり、一つまたはそれ以上のサンプルを含みます (換言すれば、各行はサンプルの順序づけられていないコレクションです)。
-
シークエンス id は数値です。それは省略可能で、その場合には行番号がシークエンス id として使用されます。
-
各サンプルは Key-Value ペアです。各サンプルは入力名と相当する値ベクトルから成る、効果的な Key-Value ペアです。
-
各サンプルはパイプ・シンボル (
|
) で始まり (スペースなしに) 入力名が続き、ホワイトスペース・デリミタそして値のリストが続きます。 -
各値は数値か、またはスパース入力のためのインデックスをプレフィックスとする数値です。
-
タブとスペースの両者はデリミタとして互換可能的に使用できます。
-
コメントはハッシュ・シンボルが直ちに続くパイプで始まります:
|#
、そしてコメントの内容 (本体) が実際に続きます。
本体は任意のキャラクタを含むことができますが、本体の内部のパイプライン・シンボルはそれにハッシュ・シンボルを付加することによりエスケープされる必要があります (後述のサンプルを見てください)。
コメント本体は行末か次のエスケープされていないパイプまで続きます。
###3-2 単純な例
仕様だけでは直感的に分かりにくいので、具体例をあげていきます。
以下の例は CTF 仕様の最小セットに基づいたものです :
|B 100:3 123:4 |C 8 |A 0 1 2 3 4 |# a CTF comment
|# another comment |A 0 1.1 22 0.3 54 |C 123917 |B 1134:1.911 13331:0.014
|C -0.001 |# a comment with an escaped pipe: '|#' |A 3.9 1.11 121.2 99.13 0.04 |B 999:0.001 918918:-9.19
この入力フォーマットについては以下の点に注意してください :
-
|Input_Name
or|入力_名
は各入力サンプルの始まりを識別します。この要素は必須で、相当する値ベクトルが続きます。 -
密ベクトル は浮動小数点値の単なるリストです ; 疎ベクトル は
index:value
タプルのリストです (疎ベクトルは上の例の1行目のB
の値ベクトルが該当しています)。 -
タブとスペースの両者が (入力ベクトル内の) 値デリミタそして (入力間の) 入力デリミタとして許されます。
-
各別々の行はそれぞれ長さ 1 の シークエンス を構成しています (可変長シークエンスは次節の "拡張された例" で説明されます)。
-
各入力識別子は単一の行に一度だけ出現できます。
-
行内の入力サンプルの順序は重要ではありません (概念的には、各行は Key-Value ペアの順序づけられていないコレクションです)。
-
各整形式 (well-formed) 行は "ラインフィード" \n または "キャリッジリターン, ラインフィード" \r\n シンボルで終わらなければなりません。
###3-3 拡張された例
可能な様々なフォーマット・オプション、特にシークエンス ID を明示的に利用したケース (i.e. 可変長シークエンス) の入力ファイルの例は次のようなものです :
100 |a 1 2 3 |b 100 200
100 |a 4 5 6 |b 101 201
100 |b 102983 14532 |a 7 8 9
100 |a 7 8 9
200 |b 300 400 |a 10 20 30
333 |b 500 100
333 |b 600 -900
400 |a 1 2 3 |b 100 200
|a 4 5 6 |b 101 201
|a 4 5 6 |b 101 201
500 |a 1 2 3 |b 100 200
前節の例で議論されたオプションはここでも適用されています。それに加えて次の追加の特徴を導入します :
シークエンス ID
(既に言及したように、) 入力ファイルの各別々の行は、入力 (ストリーム) のための単一サンプルを含むシークエンスを表しています。
けれども、もし行が負でない数字でプレフィックスされた場合には、この数字は相当するシークエンス id として使用されます。
同じシークエンス id を共有する、続く総ての行は一緒にマージ (統合) されて同じシークエンスの一部となります。
それゆえに、N 行のために同じ数字のプレフィックスを繰り返すことは、1 から N までのサンプルを持つ、複数サンプルのシークエンスを構成することを可能にします。
2番目そしてそれに続く行のシークエンス・プレフィックスは省略可能で、その場合にも同じ効果を持ちます。
従って、上の例のデータセットは id 100, 200, 333, 400 と 500 を持つ5つのシークエンスを定義します。
シークエンスを使用するときに考慮すべき最後の 2, 3 のポイントは :
-
シークエンス id は一意 (= uniq ) でなければなりません。
-
id プレフィックスは連続的な行のためだけに繰り返すことができます (飛び飛びになってはいけません)。
-
行 (群) におけるシークエンス長 (i.e. 同じ id プレフィックスを共有する行数) は、このシークエンスのサンプルの最大入力長 (i.e. 入力のサンプル数) を超えてはなりません。
※ 三番目は分かりにくいですが、これは実装の都合のようです。
例えば、次のデータセットは不正です :
100 |a 1 2 3 |b 100 200
200 |a 4 5 6 |b 101 201
100 |b 102983 14532 |a 7 8 9
123 |a 1 2 3 |b 100 200
456 |a 4 5 6
456 |b 101 201
※ id プレフィックス 100 が飛び飛びになっています。一意でないとも解釈できます。
※ 456 のシークエンス長 (= 2) がサンプル数 (= 1) を超えています。
###3-4 幾つかの現実世界の例
最後に実際に利用される例を幾つか見てみます :
- 分類問題: 総ての行は、ラベルと特徴から成るサンプルを含みます。
シークエンス ID は必要ありません、何故ならば総ての行は長さ 1 のそれ自身が "シークエンス" だからです。
|class 23:1 |features 2 3 4 5 6 |class 13:1 |features 1 2 0 2 3 ...
- DSSM (Deep Semantic Similarity Model) : 総ての行は source-target ドキュメントのペアを含みます。これらは bag of words を通して表現され、疎ベクトルとしてエンコードされます。
|src 12:1 23:1 345:2 45001:1 |tgt 233:1 766:2 234:1
|src 123:1 56:1 10324:1 18001:3 |tgt 233:1 2344:2 8889:1 2234:1 253434:1
- 品詞タグ付け (= Part-of-speech tagging) : シークエンスは総ての要素を相当するラベルにマップします。シークエンスは垂直に整列されます (行毎に一つの word + tag)。
0 |word 234:1 |tag 12:1
0 |word 123:1 |tag 10:1
0 |word 123:1 |tag 13:1
1 |word 234:1 |tag 12:1
1 |word 123:1 |tag 10:1
...
- シークエンス分類: シークエンスが単一のラベルにマップされます。
シークエンスは垂直に整列されます; "クラス" ラベルは同じシークエンス id を持つどの行でも出現することができます。
0 |word 234:1 |class 3:1
0 |word 123:1
0 |word 890:1
1 |word 11:1 |class 2:1
1 |word 344:1
[注意] 現時点で行数は最長シークエンスの長さを超えることはできません。
これはラベルはそれ自身の行で出現することはできないことを意味します。これは実装の細部で将来的には向上されるでしょう。
- Sequence to sequence: ソース・シークエンスをターゲット・シークエンスにマップします。2つの文が垂直に整列されてそして、最も簡単なケースでは、他方の後に出力表示されるだけです。それらは同じ "シークエンス ID" を持つことにより結合されます (そしてそれはこの場合には "ワーク・ユニット ID" になります)。
0 |sourceWord 234:1 |targetWord 344:1
0 |sourceWord 123:1 |targetWord 456:1
0 |sourceWord 123:1 |targetWord 2222:1
0 |sourceWord 11:1
1 |sourceWord 123:1
...
[注意] 現時点では行数は最長シークエンスの長さを超えてはいけません。
これはシークエンスは垂直に整列されなければならないことを意味します。これは実装の細部で将来的には向上されるでしょう。
- ランキング学習: "シークエンス" はクエリを表わし、総てのサンプルは手動でラベル付けされたレーティングを持つドキュメントです。
この場合、"文" は (ランキング学習の損失関数のコンテキストでは) 順序を持たない単なるマルチセットです。
0 |rating 4 |features 23 35 0 0 0 21 2345 0 0 0 0 0
0 |rating 2 |features 0 123 0 22 44 44 290 22 22 22 33 0
0 |rating 1 |features 0 0 0 0 0 0 1 0 0 0 0 0
1 |rating 1 |features 34 56 0 0 0 45 1312 0 0 0 0 0
1 |rating 0 |features 45 45 0 0 0 12 335 0 0 0 0 0
2 |rating 0 |features 0 0 0 0 0 0 22 0 0 0 0 0
...
##4. ATIS データフォーマット
(シークエンスを含む) CTF フォーマットの説明が完了したので、本題に戻ります。
本記事でアプローチしたいタスクは ATIS コーパス を題材とするスロットタギング (= slot tagging) でした。
ATIS は Air Travel Information Services のドメインからの (ヒューマン->コンピュータ) クエリを含み、
そしてスロットタギングのタスクは、クエリの各単語についてそれが情報の特定の項目 (スロット) に属するかどうか、そしてどの一つであるか注釈をつける (タグ付ける) ことです。
データフォーマット
作業フォルダのデータファイルは簡単に前処理されて既に CTF フォーマットに変換されています。
テストデータセット・ファイル atis.test.ctf
からのシークエンス Id 19 のサンプルを見てみましょう :
19 |S0 178:1 |# BOS |S1 14:1 |# flight |S2 128:1 |# O
19 |S0 770:1 |# show |S2 128:1 |# O
19 |S0 429:1 |# flights |S2 128:1 |# O
19 |S0 444:1 |# from |S2 128:1 |# O
19 |S0 272:1 |# burbank |S2 48:1 |# B-fromloc.city_name
19 |S0 851:1 |# to |S2 128:1 |# O
19 |S0 789:1 |# st. |S2 78:1 |# B-toloc.city_name
19 |S0 564:1 |# louis |S2 125:1 |# I-toloc.city_name
19 |S0 654:1 |# on |S2 128:1 |# O
19 |S0 601:1 |# monday |S2 26:1 |# B-depart_date.day_name
19 |S0 179:1 |# EOS |S2 128:1 |# O
このファイルは 7 カラムを持ちます :
-
シークエンス id (19)。このシークエンス id を持つ 11 エントリ (行) があります。これはシークエンス 19 は 11 個のトークンから構成されていることを意味しています;
-
カラム
S0
、これは数値の単語インデックスを含んでいます; 入力データは one-hot ベクトルとしてエンコードされて疎ベクトルとして扱われています。
語彙には 943 単語がありますので、各単語はその単語を表すために選択されたベクトル・インデックスのみ 1 を持ち他の総ての要素が 0 の 943 要素ベクトルです。例えば単語 "from" はそのベクトルにおいてインデックス 444 で 1、そして他の総ての場所では 0 を持つものとして表現されます。単語 "monday" はそのベクトルにおいてインデックス 601 で 1、そして他の総ての場所では 0 を持つものとして表現されます。 -
#
で示されるコメント・カラム、数値の単語インデックスが何を表すかを人間の読み手が知ることを可能にするためのものです ; コメント・カラムはシステムから無視されます。
BOS
(Beginning Of Sentence) とEOS
(End Of Sentence) はそれぞれ文の始まりと終わりを示すための特殊な単語です ; -
カラム
S1
はインテント (= intent) ラベルです ; -
もう一つのコメントカラムで、これは数値のインテント・インデックスの人間に読める (= human-readable) ラベルを示します ;
-
カラム
S2
はスロット・ラベルで、数字のインデックスとして表現されます ; -
(そして最後のカラムとなる、) もう一つのコメントカラムで、これは数値のラベル・インデックスの人間に読めるラベルを示します。
ニューラルネットワークのタスクはクエリ (カラム: S0
) を見てスロットラベル (カラム: S2
) を予測することです。
上の例から見て取れるように、入力の各単語は空ラベル O
かスロットラベルが割り当てられます。
スロットラベルは最初の単語のためには B-
で開始されて、そして任意の追加の続く単語のために I-
で、これは同じスロットに属します。
※ ちなみに、本記事では使用しませんが、intent.wl
は以下のような内容になっています :
$ cat intent.wl
abbreviation
aircraft
aircraft+flight+flight_no
airfare
airfare+flight
airfare+flight_time
airline
airline+flight_no
airport
capacity
cheapest
city
day_name
distance
flight
flight+airfare
flight+airline
flight_no
flight_no+airline
flight_time
ground_fare
ground_service
ground_service+ground_fare
meal
quantity
restriction
インポート
取り敢えず、必要となるコンポーネント: math と numpy そして CNTK をインポートしておきましょう。
CNTK の Python モジュールは io
, learner
と layers
のような幾つかのサブモジュールを含んでいます。
また必要であれば NumPy も使用します、何故ならば CNTK で返される結果は NumPy 配列のように動作するからです :
import math
import numpy as np
import cntk as C
import cntk.tests.test_utils
cntk.tests.test_utils.set_device_from_pytest_env() # (only needed for our build system)
C.cntk_py.set_fixed_random_seed(1) # fix a random seed for CNTK components
##5. モデル作成
次にモデル作成に移ります。
最初のモデルは標準的なもので、埋め込み層、リカレント LSTM セル、そして事後確率を計算するための Dense 層から構成されます :
※ 下図ではボトムに入力側がありますので注意してください。
slot label "O" "O" "O" "O" "B-fromloc.city_name"
^ ^ ^ ^ ^
| | | | |
+-------+ +-------+ +-------+ +-------+ +-------+
| Dense | | Dense | | Dense | | Dense | | Dense | ...
+-------+ +-------+ +-------+ +-------+ +-------+
^ ^ ^ ^ ^
| | | | |
+------+ +------+ +------+ +------+ +------+
0 -->| LSTM |-->| LSTM |-->| LSTM |-->| LSTM |-->| LSTM |-->...
+------+ +------+ +------+ +------+ +------+
^ ^ ^ ^ ^
| | | | |
+-------+ +-------+ +-------+ +-------+ +-------+
| Embed | | Embed | | Embed | | Embed | | Embed | ...
+-------+ +-------+ +-------+ +-------+ +-------+
^ ^ ^ ^ ^
| | | | |
w ------>+--------->+--------->+--------->+--------->+------...
BOS "show" "flights" "from" "burbank"
そして CNTK でモデル定義を行ないますが、利用する Python API を先にまとめておきましょう。層ライブラリ・リファレンス も参考にしてください :
API | 説明 |
---|---|
sequence.input_variable |
ネットワークの入力を作成します: 特徴やラベルのようなデータが提供されるべき場所です。(7 章でも詳述されます。) |
layers.Sequential |
入力上に層のシーケンス (または任意の関数) を適用する合成 (= composite) を作成する層ファクトリ関数です。(8 章でも詳述されます。) |
layers.Embedding |
embedding (埋め込み) 層を作成するための層ファクトリ関数です。 |
layers.Recurrence |
RNN, LSTM, そして GRU を含む、リカレント・モデルを実装する層ファクトリ関数です。 |
layers.LSTM |
リカレンスの内側での使用のための LSTM ブロックを作成するための層ファクトリ関数です。 |
layers.Dense |
完全結合線形層のインスタンスを作成するための層ファクトリ関数です。 |
# 語彙の単語、スロット・ラベル, そしてインテント・ラベルの数
vocab_size = 943 ; num_labels = 129 ; num_intents = 26
# モデル次元
input_dim = vocab_size
label_dim = num_labels
emb_dim = 150
hidden_dim = 300
# 入力特徴 (x) とラベル (y) のためのコンテナを作成します。
x = C.sequence.input_variable(vocab_size)
y = C.sequence.input_variable(num_labels)
def create_model():
with C.layers.default_options(initial_state=0.1):
return C.layers.Sequential([
C.layers.Embedding(emb_dim, name='embed'),
C.layers.Recurrence(C.layers.LSTM(hidden_dim), go_backwards=False),
C.layers.Dense(num_labels, name='classify')
])
最初の層は embed
という名前の埋め込み層です。ここでは CNTK デフォルトである、線形埋め込みを使用します。
これは次元 (入力としての単語エンコーディング x 出力としての射影された (= projected) 次元) を持つ単純な行列です。
Python オブジェクトの任意の他の属性のようにそのパラメータ E
(そこで埋め込みがストアされています) にアクセスできます。
その shape は -1
を含みこれはこの (入力次元を持つ) パラメータがまだ完全には指定されていないことを示し、その一方で出力次元は emb_dim ( = 150) に設定されています。
そして classify
という名前の Dense 層のバイアス・ベクトルの値もまた併せて確認しましょう。
Dense 層は多層パーセプトロンの基礎的な合成のユニットです。Dense 層は重みとバイアス・パラメータの両者について Dense 層毎にそれぞれ一つずつ持ち、バイアス項はデフォルトで 0 に初期化されます (必要であればそれを変更するやり方もありますが)。
CNTK ではモデル属性は Python から完全にアクセス可能ですが、モデルを作成する時に層コンポーネントに名前を付けるべきです。そうすればここで示すように 名前を利用して層のパラメータにアクセス可能です :
# peek
z = create_model()
print(z.embed.E.shape)
print(z.classify.b.value)
(-1, 150)
[ 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
0. 0. 0.]
この場合、埋め込み層は長さ 943 の one-hot エンコードされたベクトルとしての入力を持ち、出力次元 emb_dim
は 150 にセットされます。
下のコードでは入力変数 x
をモデル z
に渡します。これはモデルを既知の shape の入力データとバインドします。
この場合、入力 shape は入力語彙のサイズになります。
この変更によって、埋め込み層から返されるパラメータは (943, 150) と完全に指定されます :
# Pass an input and check the dimension
z = create_model()
print(z(x).embed.E.shape)
(943, 150)
※ 埋め込み行列を Word2Vec か GloVe を使用して事前計算されたベクトルで初期化することもできます。
##6. データ・リーディング
トレーニングのためにはデータリーダの作成が必要ですが、その前にデータセットをどのように作成したかについて簡単に説明しておきましょう。
コーパスは2つのステップで作成されています :
-
生データをプレーンテキストファイルに変換します、それはスペース区切り (space-separated) テキストのタブ区切りカラムから成ります。例えば :
BOS show flights from burbank to st. louis on monday EOS (TAB) flight (TAB) O O O O B-fromloc.city_name O B-toloc.city_name I-toloc.city_name O B-depart_date.day_name O
-
それを次のコマンドで CTF フォーマットに変換します :
python [CNTK root]/Scripts/txt2ctf.py --map query.wl intent.wl slots.wl --annotated True --input atis.test.txt --output atis.test.ctf
ここで3つの
.wl
ファイルは語彙をプレーン・テキストファイルとして与えるもので、行毎に一つの単語を含みます。
◆ CTF テキストを読むために CTFDeserializer
を使用しますが、これは入力データに特定のフォーマットであることを想定しています。
今回使用する CTF ファイルではカラムは S0
, S1
, そして S2
とラベル付けられていますが、これらはリーダー定義の対応する行により実際のネットワーク入力に接続されます :
def create_reader(path, is_training):
return C.io.MinibatchSource(C.io.CTFDeserializer(path, C.io.StreamDefs(
query = C.io.StreamDef(field='S0', shape=vocab_size, is_sparse=True),
intent_unused = C.io.StreamDef(field='S1', shape=num_intents, is_sparse=True),
slot_labels = C.io.StreamDef(field='S2', shape=num_labels, is_sparse=True)
)), randomize=is_training, max_sweeps = C.io.INFINITELY_REPEAT if is_training else 1)
どのストリーム・ソースもスパースとして設定されている点に注意しましょう。
そして実際にリーダーを作成します :
# peek
reader = create_reader(data['train']['file'], is_training=True)
reader.streams.keys()
dict_keys(['intent_unused', 'query', 'slot_labels'])
##7. トレーニングと評価
トレーニングのためにはまた、トレーニング評価 (= criterion) i.e. 損失関数、そして進捗を追跡するためのエラーメトリクスを定義しなければなりません。
通常は入力次元と対応するラベルが知られていて、損失とエラー関数を直接的に作成します。
ここでも同じようにしますが、少し寄り道をしてプレースホルダーの概念について復習します。このコンセプトは特に (モデル拡張の) タスク 3 に対して有用です。
プレースホルダーの概念
プログラミング言語で標準的な関数を書くときに引数に名前をつけることが便利であるように、
プレースホルダーを持つことはとても便利で、これは引数、あるいは再利用される必要があるローカル計算への参照となります。
最終的には、通常のプログラミング言語において関数がその引数にバインドされた具体的な値とともに呼び出されるのと同じように、何某かの他のコードがこれらのプレースホルダーを他の知られたデータと置き換えるでしょう。
具体的には、モデル作成の章で作成した入力変数
x = C.sequence.input_variable(vocab_size)
はvocab_size
で事前定義されたデータを保持します。
そのような、即時の具現化・インスタンス化が困難であるかあるいは可能でないところでは、プレースホルダーの使用は論理的に正しい選択です。
今現在作成しているコードは実際には重たい計算を実行してはいない点にも注意してください。
あくまで単に関数を指定しているだけで、トレーニングやテストの間にデータ上で計算することをそれらに期待しています。
プレースホルダーを持つことはデータを持つであろう後の時間に引数の使用を延期することを可能にします。
下の例はプレースホルダーの利用を示しています :
def create_criterion_function(model):
labels = C.placeholder(name='labels')
ce = C.cross_entropy_with_softmax(model, labels)
errs = C.classification_error (model, labels)
return C.combine ([ce, errs]) # (features, labels) -> (loss, metric)
criterion = create_criterion_function(create_model())
criterion.replace_placeholders({criterion.placeholders[0]: C.sequence.input_variable(num_labels)})
Composite(Combine): Input('Input2300', [#, *], [129]), Placeholder('labels', [???], [???]) -> Output('Block2270_Output_0', [#, *], [1]), Output('Block2290_Output_0', [#, *], [])
※ ここで C.(ops.)combine(*operands)
は新しい関数インスタンスを作成します、
これは operands
関数の指定されたリストの出力を単に結合したもので、その結果、新しい関数の出力は指定された operands
関数の各々の出力の結合 (= union) になります。
例えば、分類モデルを作成する時、典型的には CrossEntropy 損失関数と ClassificationError 関数は計算グラフの2つのルートを構成します、これは2つの出力を持つ単一の関数を作成するために結合 (= combine) することができます; 出力はすなわち、CrossEntropy 損失と ClassificationError 出力です。
◆ 上の Jupyter セルは上手く動作しますが、ネットワーク作成で定義された入力パラメータを持つときにはそれは読みやすさを損ないます。
そのため以下のように示される関数を作成するほうが好ましいでしょう :
def create_criterion_function_preferred(model, labels):
ce = C.cross_entropy_with_softmax(model, labels)
errs = C.classification_error (model, labels)
return ce, errs # (model, labels) -> (loss, error metric)
◆ criterion 関数が定義できたので、トレーニングを遂行するための train
関数を作成します。
これは今までの記事で使用したトレーナーと殆ど同じもので標準的です :
def train(reader, model_func, max_epochs=10):
# モデル関数をインスタンス化します; x は入力 (特徴) 変数です。
model = model_func(x)
# 損失とエラー関数をインスタンス化します。
loss, label_error = create_criterion_function_preferred(model, y)
# トレーニング config
epoch_size = 18000 # 18000 samples はデータセット・サイズの半分です。
minibatch_size = 70
# エポックに渡る LR スケジュール
# In CNTK, an epoch is how often we get out of the minibatch loop to
# do other stuff (e.g. checkpointing, adjust learning rate, etc.)
lr_per_sample = [3e-4]*4+[1.5e-4]
lr_per_minibatch = [lr * minibatch_size for lr in lr_per_sample]
lr_schedule = C.learning_rate_schedule(lr_per_minibatch, C.UnitType.minibatch, epoch_size)
# Momentum スケジュール
momentum_as_time_constant = C.momentum_as_time_constant_schedule(700)
# このデータセット上で上手く動作することが知られている Adam オプティマイザを使用します。
# Feel free to try other optimizers from
# https://www.cntk.ai/pythondocs/cntk.learner.html#module-cntk.learner
learner = C.adam(parameters=model.parameters,
lr=lr_schedule,
momentum=momentum_as_time_constant,
gradient_clipping_threshold_per_sample=15,
gradient_clipping_with_truncation=True)
# 進捗アップデータをセットアップします。
progress_printer = C.logging.ProgressPrinter(tag='Training', num_epochs=max_epochs)
# Uncomment below for more detailed logging
#progress_printer = ProgressPrinter(freq=100, first=10, tag='Training', num_epochs=max_epochs)
# trainer をインスタンス化します。
trainer = C.Trainer(model, (loss, label_error), learner, progress_printer)
# process minibatches and perform model training
C.logging.log_number_of_parameters(model)
t = 0
for epoch in range(max_epochs): # エポックに渡るループ
epoch_end = (epoch+1) * epoch_size
while t < epoch_end: # エポック上、ミニバッチに渡るループ
data = reader.next_minibatch(minibatch_size, input_map={ # fetch minibatch
x: reader.streams.query,
y: reader.streams.slot_labels
})
trainer.train_minibatch(data) # それでモデルを更新します。
t += data[y].num_samples # samples so far
trainer.summarize_training_progress()
※ 注意点として、CNTK では、エポックは 他のこと (e.g. チェックポインティング、学習率の調整 etc.) を行なうために
どのくらいの頻度でミニバッチ・ループから脱出するかを意味している ことです。
trainer を実行する
これで完全なレシピができました。実行してみましょう :
def do_train():
global z
z = create_model()
reader = create_reader(data['train']['file'], is_training=True)
train(reader, z)
do_train()
Training 721479 parameters in 6 parameter tensors.
Learning rate per minibatch: 0.020999999999999998
Finished Epoch[1 of 10]: [Training] loss = 1.740198 * 18010, metric = 28.02% * 18010 5.470s (3292.5 samples/s);
Finished Epoch[2 of 10]: [Training] loss = 0.665177 * 18051, metric = 14.30% * 18051 3.135s (5757.9 samples/s);
Finished Epoch[3 of 10]: [Training] loss = 0.526256 * 17941, metric = 11.34% * 17941 3.108s (5772.5 samples/s);
Finished Epoch[4 of 10]: [Training] loss = 0.395405 * 18059, metric = 8.22% * 18059 3.166s (5704.0 samples/s);
Learning rate per minibatch: 0.010499999999999999
Finished Epoch[5 of 10]: [Training] loss = 0.293512 * 17957, metric = 6.20% * 17957 3.157s (5688.0 samples/s);
Finished Epoch[6 of 10]: [Training] loss = 0.264932 * 18021, metric = 5.73% * 18021 3.085s (5841.5 samples/s);
Finished Epoch[7 of 10]: [Training] loss = 0.217258 * 17980, metric = 4.69% * 17980 3.070s (5856.7 samples/s);
Finished Epoch[8 of 10]: [Training] loss = 0.209614 * 18025, metric = 4.55% * 18025 3.113s (5790.2 samples/s);
Finished Epoch[9 of 10]: [Training] loss = 0.165851 * 17956, metric = 3.84% * 17956 3.071s (5847.0 samples/s);
Finished Epoch[10 of 10]: [Training] loss = 0.157653 * 18039, metric = 3.41% * 18039 3.135s (5754.1 samples/s);
これは学習が (データを通過する) エポックに渡りどのように進むかを示します。
例えば、4 エポック後、交差エントロピー評価基準である損失はこのエポックの ~18000 サンプル上の計測として 0.395 に達し、そしてそれらと同じ 18000 訓練サンプル上でエラー率は 8.22 % を示します。
エポックサイズはモデルチェックポイント間に処理するためのサンプル数です。単語トークンとしてカウントされます。文ではありませんので注意しましょう。
ひとたびトレーニングが完了すれば次のような出力を見るでしょう :
Finished Epoch[10 of 10]: [Training] loss = 0.157653 * 18039, metric = 3.41% * 18039 3.135s (5754.1 samples/s);
これは最終エポックに渡って平均された損失 (交差エントロピー) とメトリクス (分類エラー) です。
###モデルを評価する
(train()
関数の場合と同様に) テストデータの複数ミニバッチに渡るエラーを計算することによって、テストセット上の精度を計測するための関数 evaluate()
を定義します。
ファイルから読んだ小さなサンプル上で評価するために、サンプルサイズを反映したミニバッチサイズをセットして、そのデータ・インスタンス上で Evaluator
の test_minibatch
メソッドを実行することができます :
API | 説明 |
---|---|
C.eval.Evaluator |
指定された評価関数に対するミニバッチの評価のためのクラスです。 |
evaluate()
関数の定義です :
def evaluate(reader, model_func):
# モデル関数をインスタンス化します; x は入力 (特徴) 変数です。
model = model_func(x)
# 損失とエラー関数を作成します。
loss, label_error = create_criterion_function_preferred(model, y)
# ミニバッチを処理して評価を遂行します。
progress_printer = C.logging.ProgressPrinter(tag='Evaluation', num_epochs=0)
while True:
minibatch_size = 500
data = reader.next_minibatch(minibatch_size, input_map={ # ミニバッチを取得します。
x: reader.streams.query,
y: reader.streams.slot_labels
})
if not data: # until we hit the end
break
evaluator = C.eval.Evaluator(loss, progress_printer)
evaluator.test_minibatch(data)
evaluator.summarize_test_progress()
◆ それでは評価を実行してみましょう。ついでに classify
(Dense) 層のバイアス・ベクトルの値も確認します :
def do_test():
reader = create_reader(data['test']['file'], is_training=False)
evaluate(reader, z)
do_test()
z.classify.b.value
Finished Evaluation [1]: Minibatch[1-23]: metric = 0.34% * 10984;
array([ -1.86572317e-02, -8.51036608e-03, 1.38878692e-02,
-1.95176248e-02, -2.78977817e-03, -1.23168333e-02,
-6.16774801e-03, -1.48157952e-02, -5.82035957e-03,
-2.99133118e-02, -1.39552727e-02, -2.11144108e-02,
-1.13499388e-02, -1.25011476e-02, -8.19398905e-04,
-5.26462449e-03, -2.67275441e-02, -1.80703981e-04,
-3.97866173e-03, -2.99989935e-02, -1.00385491e-02,
-6.81575155e-03, -2.65348870e-02, -2.01367717e-02,
-2.63106003e-02, 4.22888016e-03, 5.74267702e-03,
-1.56373549e-02, 7.71283812e-04, -2.11507780e-03,
-9.64713190e-03, -1.98590998e-02, -1.32136503e-02,
7.97898881e-03, -1.76088717e-02, 9.19442438e-03,
1.30801992e-02, -3.85360024e-03, 1.86732947e-03,
-5.96518535e-03, -3.07163689e-02, -3.04673286e-03,
-3.46862944e-04, -1.29565236e-03, -4.47261287e-03,
-1.29292831e-02, -1.05356872e-02, -9.16025974e-03,
6.08765893e-03, -3.75505001e-03, -2.08706558e-02,
-6.74075587e-03, -1.62283499e-02, -1.54837342e-02,
-4.45737131e-03, -2.18946058e-02, -7.09120184e-03,
-2.59322803e-02, -7.19474023e-03, -2.38050390e-02,
-2.12035105e-02, -1.92295518e-02, -1.78258158e-02,
-2.89904382e-02, -2.11317427e-02, -1.59252975e-02,
1.15247760e-02, -6.23733085e-03, -2.34362725e-02,
-2.94410102e-02, -2.90733539e-02, -2.31353994e-02,
-2.56022997e-02, -2.99183521e-02, -3.48846018e-02,
-2.37278938e-02, -1.23830149e-02, -8.28805566e-03,
-7.39322277e-03, -2.71228235e-02, -1.66217834e-02,
-2.01343894e-02, -7.25648273e-03, -1.39272464e-02,
-6.12456165e-03, -1.73326638e-02, -2.00424399e-02,
-6.42115762e-03, -1.77380089e-02, 2.44812854e-05,
-2.94576567e-02, 3.32167814e-03, -2.08815075e-02,
-1.13182059e-02, -1.59333460e-02, -1.49212955e-02,
5.97879710e-03, -1.84684638e-02, -2.37341989e-02,
-3.12264990e-02, 5.03905630e-03, -3.30699719e-02,
-2.31159888e-02, -9.83369444e-03, -2.43863873e-02,
-1.25425272e-02, -2.47525703e-02, -9.63981636e-03,
-1.55018922e-02, -9.93501581e-03, -1.19379526e-02,
-5.87523868e-03, -1.70155391e-02, -2.29082536e-02,
-1.84413474e-02, -1.43948747e-02, -1.95573755e-02,
-1.57539546e-02, -1.90414656e-02, -9.15751234e-03,
-2.89104730e-02, -1.02876537e-02, -2.83453427e-02,
-1.30685000e-02, -5.12228068e-03, -1.68853514e-02,
-1.10401753e-02, -7.05095194e-03, 1.51731325e-02], dtype=float32)
次のコードブロックは単一のシークエンスをどのように評価するかを示します。
併せて、NumPy 配列を使用して情報をどのように渡すことができるかも示しています :
# 辞書をロードします。
query_wl = [line.rstrip('\n') for line in open(data['query']['file'])]
slots_wl = [line.rstrip('\n') for line in open(data['slots']['file'])]
query_dict = {query_wl[i]:i for i in range(len(query_wl))}
slots_dict = {slots_wl[i]:i for i in range(len(slots_wl))}
# let's run a sequence through
seq = 'BOS flights from new york to seattle EOS'
w = [query_dict[w] for w in seq.split()] # 単語インデックスに変換します。
print(w)
onehot = np.zeros([len(w),len(query_dict)], np.float32)
for t in range(len(w)):
onehot[t,w[t]] = 1
#x = C.sequence.input_variable(vocab_size)
pred = z(x).eval({x:[onehot]})[0]
print(pred.shape)
best = np.argmax(pred,axis=1)
print(best)
list(zip(seq.split(),[slots_wl[s] for s in best]))
[178, 429, 444, 619, 937, 851, 752, 179]
(8, 129)
[128 128 128 48 110 128 78 128]
[('BOS', 'O'),
('flights', 'O'),
('from', 'O'),
('new', 'B-fromloc.city_name'),
('york', 'I-fromloc.city_name'),
('to', 'O'),
('seattle', 'B-toloc.city_name'),
('EOS', 'O')]
##8. モデルの改良
この章では、CNTK 構成を改良するタスクとソリューションが与えられますが、タスクを始める前に Sequential
の概念について説明しておきます :
Sequential()
実行したばかりのモデルをもう一度見てみましょう。
モデルは関数合成 (= function-composition) スタイルと呼ばれる作法で記述されています。
Sequential([
Embedding(emb_dim),
Recurrence(LSTM(hidden_dim), go_backwards=False),
Dense(num_labels)
])
他の深層学習ツールキットで Sequential
表記に慣れているかもしれませんが、
Sequential()
は、層の列を通して入力を伝播することによって入力が処理されるような、
ニューラルネットワークにおいて非常に共通的なシチュエーションをコンパクトに表現することを可能にするパワフルな演算です。
Sequential()
は引数として関数のリストを取り、新しい関数を返します。
その新しい関数は、これらの関数を順番に呼び出して、その都度一つの出力を次へと渡します。
例えば :
FGH = Sequential ([F,G,H])
y = FGH (x)
は次と同じことを意味します :
y = H(G(F(x)))
これは 関数合成 (function composition) として知られ、ニューラルネットワークを表現するために特に便利で、それはしばしば次のような形式を持ちます :
+-------+ +-------+ +-------+
x -->| F |-->| G |-->| H |--> y
+-------+ +-------+ +-------+
実行したモデルに戻れば、Sequential
表現はそのモデルが次のような形式を持っていることを示しています :
+-----------+ +----------------+ +------------+
x -->| Embedding |-->| Recurrent LSTM |-->| DenseLayer |--> y
+-----------+ +----------------+ +------------+
タスク (1) : バッチ正規化 (= Batch Normalization) の追加
さて、モデルを改良するタスクに入りましょう。
天下り的ですが、最初にモデルに新しい層 - バッチ正規化を追加します。
バッチ正規化は収束を早めるためのポピュラーなテクニックです。
それは良く画像処理のために使用されますが、リカレント・モデルのためにも有効であることを確認しましょう。
次は 5 章で作成済みのモデル定義と 7 章におけるトレーニング/評価結果です :
# Your task: Add batch normalization
def create_model():
with C.layers.default_options(initial_state=0.1):
return C.layers.Sequential([
C.layers.Embedding(emb_dim),
C.layers.Recurrence(C.layers.LSTM(hidden_dim), go_backwards=False),
C.layers.Dense(num_labels)
])
# Enable these when done:
#do_train()
#do_test()
基本モデル | |
---|---|
パラメータ数 | 721479 |
エポック数 | 10 |
訓練損失 | 0.157653 |
訓練精度 | 3.41% |
テスト精度 | 0.34% |
このモデル定義にバッチ正規化層を追加します。
ソリューション
ソリューションはリカレント LSTM 層の前後にバッチ正規化層: BatchNormalization()
を挿入することです。
モデルを修正した上でトレーニングを実行してみましょう。損失とメトリクスについて収束スピードが改善されることが分かります :
def create_model():
with C.layers.default_options(initial_state=0.1):
return C.layers.Sequential([
C.layers.Embedding(emb_dim),
C.layers.BatchNormalization(),
C.layers.Recurrence(C.layers.LSTM(hidden_dim), go_backwards=False),
C.layers.BatchNormalization(),
C.layers.Dense(num_labels)
])
do_train()
do_test()
Training 722379 parameters in 10 parameter tensors.
Learning rate per minibatch: 0.020999999999999998
Finished Epoch[1 of 10]: [Training] loss = 0.898960 * 18010, metric = 14.71% * 18010 11.869s (1517.4 samples/s);
Finished Epoch[2 of 10]: [Training] loss = 0.193879 * 18051, metric = 3.98% * 18051 3.442s (5244.3 samples/s);
Finished Epoch[3 of 10]: [Training] loss = 0.137649 * 17941, metric = 2.90% * 17941 3.374s (5317.4 samples/s);
Finished Epoch[4 of 10]: [Training] loss = 0.095852 * 18059, metric = 2.22% * 18059 3.506s (5150.9 samples/s);
Learning rate per minibatch: 0.010499999999999999
Finished Epoch[5 of 10]: [Training] loss = 0.057004 * 17957, metric = 1.24% * 17957 3.459s (5191.4 samples/s);
Finished Epoch[6 of 10]: [Training] loss = 0.056456 * 18021, metric = 1.33% * 18021 3.414s (5278.6 samples/s);
Finished Epoch[7 of 10]: [Training] loss = 0.049044 * 17980, metric = 1.26% * 17980 3.404s (5282.0 samples/s);
Finished Epoch[8 of 10]: [Training] loss = 0.045483 * 18025, metric = 1.18% * 18025 3.459s (5211.0 samples/s);
Finished Epoch[9 of 10]: [Training] loss = 0.030897 * 17956, metric = 0.81% * 17956 3.383s (5307.7 samples/s);
Finished Epoch[10 of 10]: [Training] loss = 0.033648 * 18039, metric = 0.81% * 18039 3.432s (5256.1 samples/s);
Finished Evaluation [1]: Minibatch[1-23]: metric = 0.26% * 10984;
基本モデル | BN 層 | |
---|---|---|
パラメータ数 | 721479 | 722379 |
エポック数 | 10 | 10 |
訓練損失 | 0.157653 | 0.033648 |
訓練精度 | 3.41% | 0.81% |
テスト精度 | 0.34% | 0.26% |
###タスク (2) : Lookahead (先読み) の追加
定義したリカレント・モデルには構造上の欠陥があります : リカレンスは左から右へ動作しますので、スロットラベルのための決定は次にやってくる (= upcoming) 単語についての情報は持ちません。つまりモデルは少し不均等 (= lopsided) です。
次のタスクは、リカレンスへの入力が現在の単語だけでなく、次の一つ (lookahead) からも構成されるようにモデルを修正することです。
ソリューションは関数合成スタイルであるべきですから、次を遂行する Python 関数を書く必要があります :
- 入力引数は取りません。
- プレースホルダー (シークエンス) 変数を作成します。
-
sequence.future_value()
演算を使用してこのシークエンスにおける "次の値" を計算します。そして -
splice
を使用して現在と次の値を埋め込み次元の 2 倍のベクトルに結合します。
そしてこの関数を (埋め込み層の直後に) Sequential()
のリストに挿入すれば良いです。
ソリューション
def OneWordLookahead():
x = C.placeholder()
apply_x = C.splice(x, C.sequence.future_value(x))
return apply_x
def create_model():
with C.layers.default_options(initial_state=0.1):
return C.layers.Sequential([
C.layers.Embedding(emb_dim),
OneWordLookahead(),
C.layers.Recurrence(C.layers.LSTM(hidden_dim), go_backwards=False),
C.layers.Dense(num_labels)
])
do_train()
do_test()
Training 901479 parameters in 6 parameter tensors.
Learning rate per minibatch: 0.020999999999999998
Finished Epoch[1 of 10]: [Training] loss = 1.618926 * 18010, metric = 26.40% * 18010 3.465s (5197.7 samples/s);
Finished Epoch[2 of 10]: [Training] loss = 0.572762 * 18051, metric = 12.46% * 18051 3.287s (5491.6 samples/s);
Finished Epoch[3 of 10]: [Training] loss = 0.420728 * 17941, metric = 8.57% * 17941 3.225s (5563.1 samples/s);
Finished Epoch[4 of 10]: [Training] loss = 0.297996 * 18059, metric = 6.28% * 18059 3.351s (5389.1 samples/s);
Learning rate per minibatch: 0.010499999999999999
Finished Epoch[5 of 10]: [Training] loss = 0.224015 * 17957, metric = 4.81% * 17957 3.295s (5449.8 samples/s);
Finished Epoch[6 of 10]: [Training] loss = 0.207126 * 18021, metric = 4.61% * 18021 3.179s (5668.8 samples/s);
Finished Epoch[7 of 10]: [Training] loss = 0.170268 * 17980, metric = 3.69% * 17980 3.223s (5578.7 samples/s);
Finished Epoch[8 of 10]: [Training] loss = 0.164910 * 18025, metric = 3.65% * 18025 3.256s (5535.9 samples/s);
Finished Epoch[9 of 10]: [Training] loss = 0.126314 * 17956, metric = 2.92% * 17956 3.164s (5675.1 samples/s);
Finished Epoch[10 of 10]: [Training] loss = 0.122896 * 18039, metric = 2.67% * 18039 3.230s (5584.8 samples/s);
Finished Evaluation [1]: Minibatch[1-23]: metric = 0.38% * 10984;
基本モデル | BN 層 | Lookahead | |
---|---|---|---|
パラメータ数 | 721479 | 722379 | 901479 |
エポック数 | 10 | 10 | 10 |
訓練損失 | 0.157653 | 0.033648 | 0.122896 |
訓練精度 | 3.41% | 0.81% | 2.67% |
テスト精度 | 0.34% | 0.26% | 0.38% |
タスク (3) : 双方向 (= Bidirectional) リカレントモデル
未来の単語の知識が助けとなることが分かっています。
それでは、一つの単語の先読み (= lookahead) の代わりに、後方 (= backward) リカレンスを通して文の終わりまでずっと先読みしてみたらどうでしょう?
これが双方向 (= bidirectional) モデルです。
このタスクは、データに渡り前方及び後方 (= forward/backward) リカージョンを遂行して出力ベクトルを結合する新しい層を実装することです。
ソリューションとしては、Reccurence
API の go_backwards
引数を使用すれば簡単です。
go_backwards
が True
に設定された場合、開始するためにシークエンスの終わりからリカレンスを実行します。
ソリューション
def BiRecurrence(fwd, bwd):
F = C.layers.Recurrence(fwd)
G = C.layers.Recurrence(bwd, go_backwards=True)
x = C.placeholder()
apply_x = C.splice(F(x), G(x))
return apply_x
def create_model():
with C.layers.default_options(initial_state=0.1):
return C.layers.Sequential([
C.layers.Embedding(emb_dim),
BiRecurrence(C.layers.LSTM(hidden_dim//2),
C.layers.LSTM(hidden_dim//2)),
C.layers.Dense(num_labels)
])
do_train()
do_test()
Training 541479 parameters in 9 parameter tensors.
Learning rate per minibatch: 0.020999999999999998
Finished Epoch[1 of 10]: [Training] loss = 1.886776 * 18010, metric = 30.06% * 18010 3.902s (4615.6 samples/s);
Finished Epoch[2 of 10]: [Training] loss = 0.683211 * 18051, metric = 14.83% * 18051 3.666s (4923.9 samples/s);
Finished Epoch[3 of 10]: [Training] loss = 0.521379 * 17941, metric = 11.42% * 17941 3.594s (4991.9 samples/s);
Finished Epoch[4 of 10]: [Training] loss = 0.394698 * 18059, metric = 8.11% * 18059 3.755s (4809.3 samples/s);
Learning rate per minibatch: 0.010499999999999999
Finished Epoch[5 of 10]: [Training] loss = 0.288926 * 17957, metric = 6.06% * 17957 3.662s (4903.6 samples/s);
Finished Epoch[6 of 10]: [Training] loss = 0.267000 * 18021, metric = 5.73% * 18021 3.625s (4971.3 samples/s);
Finished Epoch[7 of 10]: [Training] loss = 0.215379 * 17980, metric = 4.69% * 17980 3.609s (4982.0 samples/s);
Finished Epoch[8 of 10]: [Training] loss = 0.206970 * 18025, metric = 4.37% * 18025 3.688s (4887.5 samples/s);
Finished Epoch[9 of 10]: [Training] loss = 0.160564 * 17956, metric = 3.39% * 17956 3.609s (4975.3 samples/s);
Finished Epoch[10 of 10]: [Training] loss = 0.154584 * 18039, metric = 3.20% * 18039 3.662s (4926.0 samples/s);
Finished Evaluation [1]: Minibatch[1-23]: metric = 0.38% * 10984;
10 エポックでは期待したほどの数字にならないので、100 エポック訓練してみます :
def do_train100():
global z
z = create_model()
reader = create_reader(data['train']['file'], is_training=True)
train(reader, z, max_epochs=100)
do_train100()
do_test()
Training 541479 parameters in 9 parameter tensors.
Learning rate per minibatch: 0.020999999999999998
Finished Epoch[1 of 100]: [Training] loss = 1.886776 * 18010, metric = 30.06% * 18010 3.904s (4613.2 samples/s);
Finished Epoch[2 of 100]: [Training] loss = 0.683211 * 18051, metric = 14.83% * 18051 3.652s (4942.8 samples/s);
Finished Epoch[3 of 100]: [Training] loss = 0.521379 * 17941, metric = 11.42% * 17941 3.623s (4952.0 samples/s);
Finished Epoch[4 of 100]: [Training] loss = 0.394698 * 18059, metric = 8.11% * 18059 3.752s (4813.2 samples/s);
Learning rate per minibatch: 0.010499999999999999
Finished Epoch[5 of 100]: [Training] loss = 0.288926 * 17957, metric = 6.06% * 17957 3.667s (4896.9 samples/s);
Finished Epoch[6 of 100]: [Training] loss = 0.267000 * 18021, metric = 5.73% * 18021 3.614s (4986.4 samples/s);
Finished Epoch[7 of 100]: [Training] loss = 0.215379 * 17980, metric = 4.69% * 17980 3.644s (4934.1 samples/s);
Finished Epoch[8 of 100]: [Training] loss = 0.206970 * 18025, metric = 4.37% * 18025 3.716s (4850.6 samples/s);
Finished Epoch[9 of 100]: [Training] loss = 0.160564 * 17956, metric = 3.39% * 17956 3.624s (4954.7 samples/s);
Finished Epoch[10 of 100]: [Training] loss = 0.154584 * 18039, metric = 3.20% * 18039 3.654s (4936.8 samples/s);
Finished Epoch[11 of 100]: [Training] loss = 0.155797 * 17966, metric = 3.32% * 17966 3.634s (4943.9 samples/s);
Finished Epoch[12 of 100]: [Training] loss = 0.126862 * 18041, metric = 2.52% * 18041 3.588s (5028.1 samples/s);
Finished Epoch[13 of 100]: [Training] loss = 0.121897 * 17984, metric = 2.57% * 17984 3.732s (4818.9 samples/s);
Finished Epoch[14 of 100]: [Training] loss = 0.111596 * 17976, metric = 2.21% * 17976 3.574s (5029.7 samples/s);
Finished Epoch[15 of 100]: [Training] loss = 0.100368 * 18030, metric = 2.12% * 18030 3.686s (4891.5 samples/s);
Finished Epoch[16 of 100]: [Training] loss = 0.090011 * 18014, metric = 1.80% * 18014 3.672s (4905.8 samples/s);
Finished Epoch[17 of 100]: [Training] loss = 0.082379 * 18018, metric = 1.58% * 18018 3.590s (5018.9 samples/s);
Finished Epoch[18 of 100]: [Training] loss = 0.078752 * 17948, metric = 1.50% * 17948 3.611s (4970.4 samples/s);
Finished Epoch[19 of 100]: [Training] loss = 0.069187 * 18033, metric = 1.32% * 18033 3.651s (4939.2 samples/s);
Finished Epoch[20 of 100]: [Training] loss = 0.070238 * 17965, metric = 1.40% * 17965 3.610s (4976.5 samples/s);
Finished Epoch[21 of 100]: [Training] loss = 0.064317 * 18046, metric = 1.16% * 18046 3.707s (4868.1 samples/s);
Finished Epoch[22 of 100]: [Training] loss = 0.055218 * 17997, metric = 1.06% * 17997 3.636s (4949.7 samples/s);
Finished Epoch[23 of 100]: [Training] loss = 0.052224 * 17988, metric = 0.95% * 17988 3.616s (4974.6 samples/s);
Finished Epoch[24 of 100]: [Training] loss = 0.047114 * 17963, metric = 0.91% * 17963 3.747s (4794.0 samples/s);
Finished Epoch[25 of 100]: [Training] loss = 0.050017 * 18000, metric = 0.93% * 18000 3.631s (4957.3 samples/s);
Finished Epoch[26 of 100]: [Training] loss = 0.045261 * 18006, metric = 0.86% * 18006 3.682s (4890.3 samples/s);
Finished Epoch[27 of 100]: [Training] loss = 0.040950 * 18033, metric = 0.73% * 18033 3.648s (4943.3 samples/s);
Finished Epoch[28 of 100]: [Training] loss = 0.041430 * 17982, metric = 0.75% * 17982 3.673s (4895.7 samples/s);
Finished Epoch[29 of 100]: [Training] loss = 0.035652 * 17996, metric = 0.79% * 17996 3.588s (5015.6 samples/s);
Finished Epoch[30 of 100]: [Training] loss = 0.036110 * 18036, metric = 0.77% * 18036 3.561s (5064.9 samples/s);
Finished Epoch[31 of 100]: [Training] loss = 0.031425 * 17943, metric = 0.52% * 17943 3.679s (4877.1 samples/s);
Finished Epoch[32 of 100]: [Training] loss = 0.034445 * 18015, metric = 0.77% * 18015 3.655s (4928.9 samples/s);
Finished Epoch[33 of 100]: [Training] loss = 0.025389 * 18033, metric = 0.59% * 18033 3.586s (5028.7 samples/s);
Finished Epoch[34 of 100]: [Training] loss = 0.027840 * 17971, metric = 0.48% * 17971 3.565s (5041.0 samples/s);
Finished Epoch[35 of 100]: [Training] loss = 0.029494 * 18026, metric = 0.63% * 18026 3.728s (4835.3 samples/s);
Finished Epoch[36 of 100]: [Training] loss = 0.030346 * 17977, metric = 0.63% * 17977 3.631s (4951.0 samples/s);
Finished Epoch[37 of 100]: [Training] loss = 0.022197 * 18023, metric = 0.47% * 18023 3.602s (5003.6 samples/s);
Finished Epoch[38 of 100]: [Training] loss = 0.019879 * 17978, metric = 0.36% * 17978 3.584s (5016.2 samples/s);
Finished Epoch[39 of 100]: [Training] loss = 0.026748 * 18031, metric = 0.56% * 18031 3.680s (4899.7 samples/s);
Finished Epoch[40 of 100]: [Training] loss = 0.020068 * 17965, metric = 0.38% * 17965 3.650s (4921.9 samples/s);
Finished Epoch[41 of 100]: [Training] loss = 0.021445 * 17978, metric = 0.44% * 17978 3.699s (4860.2 samples/s);
Finished Epoch[42 of 100]: [Training] loss = 0.017036 * 18036, metric = 0.29% * 18036 3.652s (4938.7 samples/s);
Finished Epoch[43 of 100]: [Training] loss = 0.021070 * 18001, metric = 0.45% * 18001 3.627s (4963.1 samples/s);
Finished Epoch[44 of 100]: [Training] loss = 0.020840 * 17978, metric = 0.48% * 17978 3.633s (4948.5 samples/s);
Finished Epoch[45 of 100]: [Training] loss = 0.017604 * 17994, metric = 0.36% * 17994 3.752s (4795.8 samples/s);
Finished Epoch[46 of 100]: [Training] loss = 0.016141 * 18011, metric = 0.31% * 18011 3.614s (4983.7 samples/s);
Finished Epoch[47 of 100]: [Training] loss = 0.016015 * 18004, metric = 0.29% * 18004 3.740s (4813.9 samples/s);
Finished Epoch[48 of 100]: [Training] loss = 0.016259 * 18034, metric = 0.28% * 18034 3.684s (4895.2 samples/s);
Finished Epoch[49 of 100]: [Training] loss = 0.014139 * 17941, metric = 0.27% * 17941 3.545s (5060.9 samples/s);
Finished Epoch[50 of 100]: [Training] loss = 0.014236 * 18038, metric = 0.30% * 18038 3.710s (4862.0 samples/s);
Finished Epoch[51 of 100]: [Training] loss = 0.013786 * 18030, metric = 0.27% * 18030 3.730s (4833.8 samples/s);
Finished Epoch[52 of 100]: [Training] loss = 0.012934 * 17993, metric = 0.24% * 17993 3.676s (4894.7 samples/s);
Finished Epoch[53 of 100]: [Training] loss = 0.011978 * 18002, metric = 0.21% * 18002 3.627s (4963.3 samples/s);
Finished Epoch[54 of 100]: [Training] loss = 0.011586 * 17953, metric = 0.25% * 17953 3.604s (4981.4 samples/s);
Finished Epoch[55 of 100]: [Training] loss = 0.013491 * 17998, metric = 0.33% * 17998 3.686s (4882.8 samples/s);
Finished Epoch[56 of 100]: [Training] loss = 0.010133 * 17986, metric = 0.17% * 17986 3.629s (4956.2 samples/s);
Finished Epoch[57 of 100]: [Training] loss = 0.007916 * 18016, metric = 0.16% * 18016 3.756s (4796.6 samples/s);
Finished Epoch[58 of 100]: [Training] loss = 0.010658 * 18013, metric = 0.18% * 18013 3.682s (4892.2 samples/s);
Finished Epoch[59 of 100]: [Training] loss = 0.012098 * 17988, metric = 0.28% * 17988 3.582s (5021.8 samples/s);
Finished Epoch[60 of 100]: [Training] loss = 0.008225 * 18041, metric = 0.13% * 18041 3.604s (5005.8 samples/s);
Finished Epoch[61 of 100]: [Training] loss = 0.009790 * 17961, metric = 0.19% * 17961 3.556s (5050.9 samples/s);
Finished Epoch[62 of 100]: [Training] loss = 0.009539 * 18026, metric = 0.19% * 18026 3.759s (4795.4 samples/s);
Finished Epoch[63 of 100]: [Training] loss = 0.007312 * 17978, metric = 0.12% * 17978 3.587s (5012.0 samples/s);
Finished Epoch[64 of 100]: [Training] loss = 0.006723 * 17978, metric = 0.09% * 17978 3.660s (4912.0 samples/s);
Finished Epoch[65 of 100]: [Training] loss = 0.006878 * 18043, metric = 0.12% * 18043 3.664s (4924.4 samples/s);
Finished Epoch[66 of 100]: [Training] loss = 0.007771 * 17962, metric = 0.14% * 17962 3.704s (4849.4 samples/s);
Finished Epoch[67 of 100]: [Training] loss = 0.008043 * 18001, metric = 0.14% * 18001 3.566s (5048.0 samples/s);
Finished Epoch[68 of 100]: [Training] loss = 0.006506 * 18007, metric = 0.14% * 18007 3.625s (4967.4 samples/s);
Finished Epoch[69 of 100]: [Training] loss = 0.006000 * 18011, metric = 0.08% * 18011 3.606s (4994.7 samples/s);
Finished Epoch[70 of 100]: [Training] loss = 0.007305 * 18032, metric = 0.14% * 18032 3.608s (4997.8 samples/s);
Finished Epoch[71 of 100]: [Training] loss = 0.006651 * 17983, metric = 0.12% * 17983 3.684s (4881.4 samples/s);
Finished Epoch[72 of 100]: [Training] loss = 0.005215 * 18007, metric = 0.06% * 18007 3.729s (4828.9 samples/s);
Finished Epoch[73 of 100]: [Training] loss = 0.004891 * 17982, metric = 0.05% * 17982 3.742s (4805.5 samples/s);
Finished Epoch[74 of 100]: [Training] loss = 0.005229 * 17996, metric = 0.08% * 17996 3.657s (4921.0 samples/s);
Finished Epoch[75 of 100]: [Training] loss = 0.004953 * 18024, metric = 0.06% * 18024 3.658s (4927.3 samples/s);
Finished Epoch[76 of 100]: [Training] loss = 0.004891 * 18009, metric = 0.04% * 18009 3.733s (4824.3 samples/s);
Finished Epoch[77 of 100]: [Training] loss = 0.004821 * 17994, metric = 0.05% * 17994 3.694s (4871.1 samples/s);
Finished Epoch[78 of 100]: [Training] loss = 0.004322 * 18001, metric = 0.07% * 18001 3.639s (4946.7 samples/s);
Finished Epoch[79 of 100]: [Training] loss = 0.003744 * 17963, metric = 0.02% * 17963 3.652s (4918.7 samples/s);
Finished Epoch[80 of 100]: [Training] loss = 0.004675 * 18042, metric = 0.07% * 18042 3.640s (4956.6 samples/s);
Finished Epoch[81 of 100]: [Training] loss = 0.004928 * 18005, metric = 0.11% * 18005 3.702s (4863.6 samples/s);
Finished Epoch[82 of 100]: [Training] loss = 0.003902 * 17985, metric = 0.04% * 17985 3.679s (4888.6 samples/s);
Finished Epoch[83 of 100]: [Training] loss = 0.003412 * 17962, metric = 0.02% * 17962 3.611s (4974.2 samples/s);
Finished Epoch[84 of 100]: [Training] loss = 0.003803 * 18015, metric = 0.03% * 18015 3.738s (4819.4 samples/s);
Finished Epoch[85 of 100]: [Training] loss = 0.003938 * 18004, metric = 0.05% * 18004 3.605s (4994.2 samples/s);
Finished Epoch[86 of 100]: [Training] loss = 0.002721 * 17986, metric = 0.01% * 17986 3.727s (4825.9 samples/s);
Finished Epoch[87 of 100]: [Training] loss = 0.003320 * 17999, metric = 0.02% * 17999 3.706s (4856.7 samples/s);
Finished Epoch[88 of 100]: [Training] loss = 0.003732 * 18028, metric = 0.04% * 18028 3.609s (4995.3 samples/s);
Finished Epoch[89 of 100]: [Training] loss = 0.003462 * 18009, metric = 0.04% * 18009 3.561s (5057.3 samples/s);
Finished Epoch[90 of 100]: [Training] loss = 0.002843 * 18005, metric = 0.03% * 18005 3.726s (4832.3 samples/s);
Finished Epoch[91 of 100]: [Training] loss = 0.002787 * 18004, metric = 0.02% * 18004 3.686s (4884.4 samples/s);
Finished Epoch[92 of 100]: [Training] loss = 0.002895 * 17985, metric = 0.01% * 17985 3.596s (5001.4 samples/s);
Finished Epoch[93 of 100]: [Training] loss = 0.002528 * 17966, metric = 0.01% * 17966 3.704s (4850.4 samples/s);
Finished Epoch[94 of 100]: [Training] loss = 0.002570 * 18050, metric = 0.01% * 18050 3.781s (4773.9 samples/s);
Finished Epoch[95 of 100]: [Training] loss = 0.002495 * 17954, metric = 0.02% * 17954 3.598s (4990.0 samples/s);
Finished Epoch[96 of 100]: [Training] loss = 0.002952 * 18009, metric = 0.03% * 18009 3.697s (4871.2 samples/s);
Finished Epoch[97 of 100]: [Training] loss = 0.002366 * 18010, metric = 0.01% * 18010 3.688s (4883.4 samples/s);
Finished Epoch[98 of 100]: [Training] loss = 0.002457 * 17978, metric = 0.02% * 17978 3.748s (4796.7 samples/s);
Finished Epoch[99 of 100]: [Training] loss = 0.002152 * 17999, metric = 0.02% * 17999 3.669s (4905.7 samples/s);
Finished Epoch[100 of 100]: [Training] loss = 0.002303 * 18001, metric = 0.01% * 18001 3.631s (4957.6 samples/s);
Finished Evaluation [1]: Minibatch[1-23]: metric = 0.23% * 10984;
基本モデル | BN 層 | Lookahead | bidirectional | |
---|---|---|---|---|
パラメータ数 | 721479 | 722379 | 901479 | 541479 |
エポック数 | 10 | 10 | 10 | 100 |
訓練損失 | 0.157653 | 0.033648 | 0.122896 | 0.002303 |
訓練精度 | 3.41% | 0.81% | 2.67% | 0.01% |
テスト精度 | 0.34% | 0.26% | 0.38% | 0.23% |
(本記事ではエポック数が異なるものの、) この bidirectional RNN モデルは不思議なほど上手く動作することが分かっています。
このモデルはテスト精度 0.23 % を獲得して前述の lookahead モデルよりも良い精度を持ちますが、
それ以上に大切な点は bidirectional モデルは lookahead モデルよりもパラメータが 40 % も少ないことです!
但し注意深く観察すれば、lookahead モデルは約 30 % 速く訓練されていることが分かります。
これは lookahead モデルはより少ない水平依存 (2つのリカレンスの代わりに一つ) とより巨大な行列積を持つために、より高い並列性を獲得するためです。
以上