機械学習・自然言語処理の勉強メモ

学んだことのメモやまとめ

Pytorch:CNNで文字表現の抽出

NLPにCNNを利用したモデルはすっかり当たり前になりました。

少し前に「CNNでテキスト分類」という記事を書きましたが、
その時はPytorchでCNNを扱うための基本的なコードだけに留まっていました。
kento1109.hatenablog.com

今回はNERなどで用いられる文字情報をCNNで表現する際のコーディングについて書こうと思います。

以下は単語を文字単位に分割してその分散表現を獲得する場合のイメージです。

f:id:kento1109:20190930103525p:plain
End-to-end Sequence Labeling via Bi-directional LSTM-CNNs-CRFより引用

今回はこれをPytorchで書いてみたいと思います。

Char Embedding

簡単のため、文字列は既にpadding済であることを前提とします。

vocab_size = 100
char_emb_size = 30

char_emb = nn.Embedding(num_embeddings=vocab_size, embedding_dim=char_emb_size)

として、Embeddingクラスを定義しておきます。

ここに、入力文を渡します。

max_sent = 20
max_char = 10

input = torch.randint(0, vocab_size, (max_sent, max_char))
char_embeded = char_emb(input)
char_embeded.size()
# torch.Size([20, 10, 30])

CNN

各単語は「文字列×分散表現の次元数」の行列で表現されています。
これにCNNに渡します。
今回は、window size=3で畳み込みます。

window_size = 3
cnn = nn.Conv2d(in_channels=1, 
                out_channels=char_emb_size, 
                kernel_size=(window_size,char_emb_size), 
                stride=window_size-1,
                padding=(window_size-1,0))

stride=window_size-1は、上の図の畳み込み操作のイメージ通りで、2文字列単位でフィルターをスライドさせます。
また、文字の前後にwindow_size-1の行列をpaddingしておきます。

char_embeded = char_embeded.unsqueeze(1)
feature_map = cnn(char_embeded)
feature_map.size()
torch.Size([20, 30, 6, 1])

最初にチャネル数の次元を追加します。

出力は(N, C_{out}, H_{out}, W_{out}) の次元となります。

畳み込み操作をイメージで表すと以下のようになります。
f:id:kento1109:20190930113643p:plain

max_pooling

さて、畳み込み操作により得た特徴マップをプーリング層に渡します。

m = nn.MaxPool1d(kernel_size=feature_map.size(2))

kernel_sizeは最大値を取る範囲を指定します。
今回は各チャネルの特徴マップ全てからmax_poolingを行うので、上記のように書きます。

最後に特徴マップをプーリング層に渡します。

char_feat = m(feature_map.squeeze()).squeeze()
char_feat.size()
# torch.Size([20, 30])

これで、「単語×次元数」の分散表現を得ることが出来ました。
NER等の場合、これを後続のLSTMなどに渡してあげるとOKです。

BERTなどが主流のなか、あえて少し前のベーシックなモデルについて復習しました。

pytorch-transformersを触ってみる⑤

はじめに

前回はBERTのfine tuningということで、NER(固有表現認識)を試してみました。
kento1109.hatenablog.com

今回はfine tuningではなく、BERTの事前学習について見ていきたいと思います。

pre-training from scratch

ただ、pytorch-transformersでの事前学習を調べると、早々に壁にぶつかりました。
ほとんどの内容がfine tuning向けなので、pre-trainingの内容があまり見つかりませんでした。
Issuesを見てみると、既に同様の質問をしている人がいました。
github.com

ここでpytorch-transformersの位置づけについて、開発者は以下のように述べています。

pytorch-pretraned-BERT was mostly designed to provide easy and fast access to pretrained models.
If you want to train a BERT model from scratch you will need a more robust code base for training and data-processing than the simple examples that are provided in this repo.

要は、事前学習済のモデルをfine tuningして利用することを目的としたライブラリなので、モデルのpre-training用として汎用化されたものではない、と言うことです。

ただ、全くpre-training用として使えないかというとそんなことはありません。
pytorch-transformersにはlm_finetuningなるスクリプトが用意されており、これは大規模なコーパスでpre-trainingしたモデルについて、ドメインに特化した小規模コーパスでpre-trainingしたモデルをfine tuningするためのスクリプトです。
pytorch-transformers/examples/lm_finetuning at master · huggingface/pytorch-transformers · GitHub

これを使うことでpre-trainingも可能になるのではと思います。
(実際に先ほどのIssuesでもそのようなことを示唆しているコメントもありました。)

ただし、pytorch-transformersでpre-trainingする必要はなく、Facebook researchやNVIDIAがBERTのpre-trainingに関するコードを公開しているので、そっちを利用するのもアリです。
GitHub - facebookresearch/XLM: PyTorch original implementation of Cross-lingual Language Model Pretraining.
DeepLearningExamples/TensorFlow/LanguageModeling/BERT at master · NVIDIA/DeepLearningExamples · GitHub

今回はpytorch-transformersのlm_finetuningを触ってみて、次回以降にその他のpre-trainingに関するコードも見れたらと思います。

lm_finetuning

データセットを作る場合、pregenerate_training_data.pyを動かすのがよさそうです。
ということで、pregenerate_training_data.pyを少し見てみます。

pregenerate_training_data.py

データセットのフォーマットとしては、ドキュメント単位にブランク行を入れるようです。
例えば、以下のようなドキュメントがあったとします。

リンゴは、バラ科リンゴ属の落葉高木樹。またはその果実のこと。植物学上はセイヨウリンゴと呼ぶ。春、白または薄紅の花が咲く。

バナナはバショウ科バショウ属のうち、果実を食用とする品種群の総称。また、その果実のこと。いくつかの原種から育種された多年性植物。種によっては熟すまでは毒を持つものもある。

その場合、データセットのテキストファイルは以下のようにします。

リンゴは、バラ科リンゴ属の落葉高木樹。
植物学上はセイヨウリンゴと呼ぶ。
春、白または薄紅の花が咲く。

バナナはバショウ科バショウ属のうち、果実を食用とする品種群の総称。
いくつかの原種から育種された多年性植物。
種によっては熟すまでは毒を持つものもある。

このスクリプトでは、1文書をdocリスト(各文はtokenize済)で保持し、それをまとめてdocsクラスで保持します。

Next Sentence Prediction

ドキュメントをdocsクラスに保持した後、create_training_file()でdocsを処理していきます。
実際の処理は、create_instances_from_document()でdoc毎に処理(インスタンスの生成)を行います。
create_instances_from_document()は、BERTの訓練データを作るうえで重要なので丁寧にみていきます。

BERTの訓練データでは、Next Sentence Predictionのために、文のペア(sentences A and B)を1インスタンスとします。
sentences Aペアの作成がNext Sentence Predictionの精度を高めるため、少し工夫されています。
ここでは、sentenceを選択するのではなく、sentenceの境界を無作為に選択します。
そして、その境界までのsentencesをtokens_aとして保持します。

例えば、docが4 sentencesから構成される場合、境界は1,2,3のいずれかになります。
1,2,3の境界から3が選択された場合、先頭から3 sentencesまでがtokens_aとして保持します。

token_bは50%の割合で実際の後続するsentences、50%の割合で関係の無いsentenceと入れ替えます。
(この割合は原著論文と同じです。)

Masked language modeling (LM)

次に、Masked LMのタスクのため、tokensの一部をMASKします。
これは、create_masked_lm_predictions()によって行われます。

簡単のため、以下の文でのMASK例を考えます。

my dog is hairy

最初にtokensのうち、MASKするtokenの数を決めます。(今回は「1」)
次に、tokensのindexをシャッフルして順番に取り出します。
今回は系列番号3(hairy)が先頭に出現したとします。
その系列を割合に基づいて以下のtokenに置き換えます。

  • 80% を [MASK] に置換(e.g., my dog is [MASK])
  • 10% を置換しない(e.g., my dog is hairy)
  • 10% を辞書内の任意のtokenに置換(e.g., my dog is apple

(この割合は原著論文と同じです。)

最後に置換されたtokensと、置換されたtokenのindex(4)とそのtoken(hairy)を返します。

create_instances_from_document()にMASK後のtokenを返した後、これらの情報をinstance辞書に格納します。
create_training_file()にdocのinstance辞書を返した後、json形式でファイルとして保存します。

pregenerate_training_data.pyの役割は以上です。

finetune_on_pregenerated.py

pre-trained model

モデルをインスタンス化します。

model = BertForPreTraining.from_pretrained(args.bert_model)

今回使用するモデルは、BertForPreTrainingクラスです。
名前がややこしいですが、BertPreTrainedModelを継承したクラスです。
このモデルにてMasked LM及びNext Sentence Predictionの学習を行います。

data loading

作成したjsonファイルを利用してpre-training(fine tuning)を行います。
これは、finetune_on_pregenerated.pyにより実行されます。

まず、先ほどの2つのjsonファイルのあるパスをPregeneratedDatasetクラスに渡してデータセットを作成します。
基本的な処理としては、epoch_{0}.json(maskされたtokensデータを含んだファイル)を1行(1インスタンス)毎に読み込み、InputFeaturesクラスを生成します。
InputFeaturesクラスは以下のクラス変数から成ります。

  • input_ids:tokensの系列(padding済)
  • input_mask:input_idsのpaddingを表す系列
  • segment_ids:input_idsのsentences A, Bを表す系列
  • lm_label_ids:input_idsのmaskされたtokenの元のtoken(正解ラベル)を並べた系列
  • is_next:sentences Bが元のsentences Aに続く文か、ランダムに挿入された文か(True / False)

lm_label_idsですが、convert_example_to_featuresで少し工夫されていたのを見逃していました。
lm_label_idsはinput_idsの系列を全て「-1」で初期化して、maskされた位置のみ元のtokenのラベルで置き換えます。

例えば、「my dog is hairy」の例で考えると以下のようになります。

input_ids = ['my', 'dog', 'is', '[MASK]']
lm_label_ids = [-1, -1, -1, 20] # {20: hairy}

※paddingやid変換などは考慮していないため、正確ではありません。

これを最後の行まで読み込んだデータをPregeneratedDatasetクラスで保持します。

PregeneratedDatasetをSamplerクラス→DataLoaderクラスと順番に渡していきます。

DataLoaderクラスまで渡したら、学習の準備が完了です。

後はモデルにInputFeaturesクラスをバッチ化したものを渡してforwardを行います。

outputs = model(input_ids, segment_ids, input_mask, lm_label_ids, is_next)

BertForPreTrainingのforwardを確認します。

outputs = self.bert(input_ids, position_ids=position_ids, token_type_ids=token_type_ids,
                    attention_mask=attention_mask, head_mask=head_mask)

BertForPreTrainingですが、ネットワークの中核となるのはBertModelです。
BertModelは、encoderの役割(隠れ層の値を出力)を果たします。
入力としては以下を受け取ります。

  • input_ids:InputFeaturesクラスのinput_ids
  • token_type_ids: InputFeaturesクラスのsegment_ids
  • attention_mask:InputFeaturesクラスのinput_mask
sequence_output, pooled_output = outputs[:2]
prediction_scores, seq_relationship_score = self.cls(sequence_output, pooled_output)  #  BertPreTrainingHeads

BertForPreTrainingでは、以下のBertModelの出力を利用します。
1. sequence_output:隠れ層の値
2. pooled_output :隠れ層のLinear→Tanhした値

これをBertPreTrainingHeadsクラスの入力として渡します。
更に、BertPreTrainingHeadsでは以下の2つの処理を行います。
1. sequence_outputをBertLMPredictionHeadに渡す
2. pooled_outputをLinear(n=2)

BertLMPredictionHeadを確認します。
これは、sequence_output(隠れ層の値)を受け取り、BertPredictionHeadTransformにて変換後、Linear(n=vocab)を行います。

さて、BertForPreTrainingのforwardに戻り、損失計算の部分を確認します。
正解となるラベル情報は以下を利用します。

  • masked_lm_labels:InputFeaturesクラスのlm_label_ids
  • next_sentence_label:InputFeaturesクラスのis_next
outputs = (prediction_scores, seq_relationship_score,) + outputs[2:]

loss_fct = CrossEntropyLoss(ignore_index=-1)
masked_lm_loss = loss_fct(prediction_scores.view(-1, self.config.vocab_size), masked_lm_labels.view(-1))
next_sentence_loss = loss_fct(seq_relationship_score.view(-1, 2), next_sentence_label.view(-1))
total_loss = masked_lm_loss + next_sentence_loss
outputs = (total_loss,) + outputs

prediction_scoresのshapeは「batch × tokens × vocab」です。
seq_relationship_scoreのshapeは「batch × 2」です。

masked_lm_labelsは、maskされたラベル以外が-1の系列なので、maskされた位置のみの損失が計算されます。
next_sentence_labelは0,1のラベル(1がランダム)です。

masked_lm_loss + next_sentence_lossを足し合わせた損失をtotal_loss として返します。

その後は、finetune_on_pregenerated.pyのmain()でloss.backward()して重みを調整していきます。

fine tuning For pre-training

さて、BERTのfine tuningについて確認しました。
これが出来ると、1からpre-trainingする理由があまり無いような気もします。
しかし、以下のような場合はやはり1からpre-trainingが必要と思います。

  • 大規模なpre-training modelがそもそも存在しない
  • pre-training modelは存在するが、自作tokenizerを利用したい

特にTokenClassificationの場合、tokenizerが望ましい分割をしてくれないとタスクが困難となります。
では、1からpre-trainingを行いたい場合はどうすれば良いでしょうか。

単純にpre-trained modelをロードしなければ良いのではと思いました。
最初に挙げたIssuesでも似たようなにコメントしている人もいました。

If you construct and initialize a new model instead of loading from pretrained, you can use the simple_lm_finetuning script to train on new data.

finetune_on_pregenerated.pyでは、237行目で以下のようにしてpre-trained modelを読み込んでいます。

model = BertForPreTraining.from_pretrained(args.bert_model)

これは、pre-trained modelが無い場合は利用できません。

その場合は、以下のようにしてBertForPreTrainingをインスタンス化すればOKです。

config = BertConfig.from_pretrained('bert-base-uncased')
model = BertForPreTraining(config)

BertForPreTrainingのコンストラクタを見ると以下のように記述されています。

def __init__(self, config):
    super(BertForPreTraining, self).__init__(config)

    self.bert = BertModel(config)
    self.cls = BertPreTrainingHeads(config)

    self.apply(self.init_weights)
    self.tie_weights()

2行目のBertModel(config)により、configの値に基づき、BERTのモデルが構築されます。
4行目のself.apply(self.init_weights)が実行されると、BERTの重みが初期化されます。

当然、tokenizerを自作したい場合は、別途tokenizerの作成が必要となります。
それ以外の点では、大きな変更は要らないはずです。

おわりに

pytorch-transformersのライブラリを活用して、pre-trainedモデルを学習する方法について確認しました。
今後は、Facebook researchやNVIDIAのBERTの学習方法についても見ていければと思います。

pytorch-transformersを触ってみる④

はじめに

前回はfine tuningということで、GLUEタスクのSST-2に取り組みました。
kento1109.hatenablog.com

また、GLUEタスクのfine tuningを実行するためのスクリプトrun_glue.py )のコードを眺めました。

今回は、CoNLL(NERの共通タスク)のためのfine tuningにチャレンジします。
BERT-NERですが、以下の内容が参考となりました。
https://www.depends-on-the-definition.com/named-entity-recognition-with-bert/
BERT-NER/run_ner.py at experiment · kamalkraj/BERT-NER · GitHub
GitHub - kyzhouhzau/BERT-NER: Use Google's BERT for named entity recognition (CoNLL-2003 as the dataset).


ただし、pytorch-pretrained-bertを利用している点に留意する必要があります。

準備

データの入手及びロードを行います。

データの入手

今回は「CoNLL 2003 shared task (NER) data」を利用します。

データは以下から入手可能です。
NER/corpus/CoNLL-2003 at master · synalp/NER · GitHub

このデータセットは、以下のようなフォーマットとなっています。

-DOCSTART- -X- O O

CRICKET NNP I-NP O
- : O O
LEICESTERSHIRE NNP I-NP I-ORG
TAKE NNP I-NP O
OVER IN I-PP O
AT NNP I-NP O
TOP NNP I-NP O
AFTER NNP I-NP O
INNINGS NNP I-NP O
VICTORY NN I-NP O
. . O O

LONDON NNP I-NP I-LOC
1996-08-30 CD I-NP O

-DOCSTART-で文章が区切られており、各文は空行で区切られています。
NERタスクの場合、4列目をラベルとして抽出します。

データのロード

これらのデータをロードしておきます。
自前でloaderを作っても良いですが、ここではtorchtextのloaderを利用します。

torchtextを利用したCoNLLのデータセットのloaderについては以前まとめました。
Pytorch:テキストの前処理(torchtext)③ - 機械学習・自然言語処理の勉強メモ

ただし、CoNLL2003は-DOCSTART-が含まれていますが、これはNERでは不要です。
デフォルトのSequenceTaggingDatasetではこれを考慮できていないので少し工夫が必要です。

具体的には、SequenceTaggingDatasetのコードをコピーして少し手を加えた以下のloaderを用意します。

from torchtext import data, datasets
class SequenceTaggingDataset(data.Dataset):
    @staticmethod
    def sort_key(example):
        for attr in dir(example):
            if not callable(getattr(example, attr)) and \
                    not attr.startswith("__"):
                return len(getattr(example, attr))
        return 0

    def __init__(self, path, fields, encoding="utf-8", separator="\t", **kwargs):
        examples = []
        columns = []

        with open(path, encoding=encoding) as input_file:
            for line in input_file:
                line = line.strip()
                # add start
                if line.startswith('-DOCSTART-'):  
                    continue
                # add end
                if line == "":
                    if columns:
                        examples.append(data.Example.fromlist(columns, fields))
                    columns = []
                else:
                    for i, column in enumerate(line.split(separator)):
                        if len(columns) < i + 1:
                            columns.append([])
                        columns[i].append(column)

            if columns:
                examples.append(data.Example.fromlist(columns, fields))
        super(SequenceTaggingDataset, self).__init__(examples, fields,
                                                     **kwargs)

これで以下のようにして、CoNLLのデータセットが簡単に読み込めるようになります。

WORD = data.Field()
POS1 = data.Field()
POS2 = data.Field()
LABEL = data.Field()
train_ds, valid_ds, test_ds = SequenceTaggingDataset.splits(fields=[('word', WORD), 
                                                                    ('pos1', POS1), 
                                                                    ('pos2', POS2), 
                                                                    ('label', LABEL)],
                                                            path='CoNLL-2003' ,
                                                            separator=" ",
                                                            train="eng.train", 
                                                            validation="eng.testa", 
                                                            test="eng.testb"
                                                           )

また、後々使うのでラベルリストを取得しておくといいです。

LABEL.build_vocab(train_ds, valid_ds, test_ds)
label_list = list(LABEL.vocab.freqs)
# ['I-ORG', 'O', 'I-MISC', 'I-PER', 'I-LOC', 'B-LOC', 'B-MISC', 'B-ORG']

fine tuning

NERデータセットを用いて訓練するまでに必要な処理をまとめます。

pretrained modelの読み込み

以下のようにて各クラスをインスタンス化します。

from pytorch_transformers import BertConfig, BertTokenizer, BertForTokenClassification
model_name = 'bert-base-uncased'
config = BertConfig.from_pretrained(model_name, num_labels=len(label_list))
tokenizer = BertTokenizer.from_pretrained(model_name, do_lower_case=True)
model = BertForTokenClassification.from_pretrained(model_name, config=config)

※今回はNERなので、モデルはTokenClassificationとなります。

BERTの入力に変換

読み込んだデータセットは、BERTの入力形式に変換してあげる必要があります。
前回のGLUEタスクで利用したconverterを流用する形で実装していきたいと思います。
入力に関しては元のスクリプトもInputExampleクラスで定義している大した修正は不要です。

ただし、NERはTokenClassificationなので、出力ラベルについては、入力系列と同じ長さの系列を作るように修正が必要です。しかし、BERTの入力系列は少し特殊なので、その点を考慮しないといけません。
論文の図では、NERの入出力系列は以下のように表現されています。

f:id:kento1109:20190824072049p:plain
論文より引用

また、入力系列がWordPiece tokenizerにより分割されることについては、以下のように説明しています。

We feed each CoNLL-tokenized input word into our WordPiece tokenizer and use the hidden state corresponding to the first sub-token as input to the classifier.

Jim Hen ##son was a puppet ##eer
I-PER I-PER X O O O X

Where no prediction is made for X. Since the WordPiece tokenization boundaries are a known part of the input, this is done for both training and test.

要は、WordPiece tokenizerの先頭以外は予測は不要ということです。
後、先頭の[CLS]トークンについては、それに対応するCラベルを付与しています。
また、入力は1文単位であるため末尾の[SEP]トークンは不要となります。

この点を考慮した修正ができればOKです。
こちらのコードでは、それらを入力の作り方をしているので、これを持ってきてもOKです。

BERTの訓練・評価

入力データをBERTのTokenClassification用に整形できました。
では、train()を呼び出して訓練を行います。

global_step, tr_loss = train(args, dataset, model, tokenizer)

しばらく待てば学習が完了します。

完了後、結果を評価してみます。

precision : 0.9560
recall : 0.9596
f1-score : 0.9577

良い感じで学習出来ているのではないでしょうか。
(今回は正確な評価が目的ではないので、[CLS]Xのラベルが含まれた状態で評価しています
。)

正解例と予測結果を見比べてみます。

['[CLS]', 'soccer', '-', 'japan', 'get', 'lucky', 'win', ',', 'china', 'in', 'surprise', 'defeat', '.']
TRUE : ['[CLS]', 'O', 'O', 'I-LOC', 'O', 'O', 'O', 'O', 'I-PER', 'O', 'O', 'O', 'O']
PRED : ['[CLS]', 'O', 'O', 'I-LOC', 'O', 'O', 'O', 'O', 'I-LOC', 'O', 'O', 'O', 'O']

この場合、chinaがズレていますが、アノテーションが間違っているのではないかという気もします。

おわりに

今回はBERTのfine tuningとしてCoNLLタスクに取り組んでみました。
結果を見る限りでは、fine tuningだけで十分の結果が出せたと思います。
次回は、これを日本語に適用してみたいと思います。

pytorch-transformersを触ってみる③

はじめに

前回は日本語でのpytorch-transformersの扱い方についてまとめました。
kento1109.hatenablog.com

これまでpytorch-transformersの基本的なところを英語・日本語で動かしてみました。

今回はもう一歩進んでfine tuningについてまとめてみます。

ドキュメントについては、以下を参考とします。
Examples — pytorch-transformers 1.0.0 documentation

準備

今回はGLUEタスクのうち、SST-2(評判分析)のデータセットで動かしてみます。
データセットについては、こちらのスクリプトを実行して入手しておきます。

実行

ものすごく簡単です。run_bert_classifier.pyに適切な引数を与えて実行するだけです。
と思ったのですが、github上にこのようなファイル名のスクリプトは存在しません。

このissueについては、既に以下で解決済です。
github.com

run_bert_classifier.pyは前のpytorch-pretrained-bertの時のスクリプトのようで、
今は、run_glue.pyという名称に代わっているみたいです。
githubのREADMEにもGLUEのfine tuningはrun_glue.pyと書いていますね。。

export GLUE_DIR=/path/to/glue
export TASK_NAME=MRPC

python ./examples/run_glue.py \
    --model_type bert \
    --model_name_or_path bert-base-uncased \
    --task_name $TASK_NAME \
    --do_train \
    --do_eval \
    --do_lower_case \
    --data_dir $GLUE_DIR/$TASK_NAME \
    --max_seq_length 128 \
    --per_gpu_eval_batch_size=8   \
    --per_gpu_train_batch_size=8   \
    --learning_rate 2e-5 \
    --num_train_epochs 3.0 \
    --output_dir /tmp/$TASK_NAME/

GPU環境の場合、1時間もしないうちに学習が終わります。
最後に、以下のような評価結果が表示されればOKかと思います。

08/22/2019 10:59:29 - INFO - __main__ -   ***** Eval results  *****
08/22/2019 10:59:29 - INFO - __main__ -     acc = 0.9231651376146789

環境設定などでハマらなければとても簡単です。

run_glue.py

GLUEのタスクでは、用意されたスクリプトを実行するだけで結果が得られました。
ほとんど頭で何か考える必要はありませんでした。
しかし、GLUE以外のタスク(自身で用意したデータセットなど)に適用する場合、このスクリプトは使えません。
なので、run_glue.pyでどうやってfine tuningを行ったかをコードを少し見て勉強しようと思います。
具体的には、run_glue.py内でこれまでに見たpretrained modelをどのように扱っているのか学びます。
※distributed training(分散学習)に関する部分は今回は扱いません。

model Instantiate

スクリプト内では、辞書形式で引数に応じたクラスを定義しています。

MODEL_CLASSES = {
    'bert': (BertConfig, BertForSequenceClassification, BertTokenizer),
    'xlnet': (XLNetConfig, XLNetForSequenceClassification, XLNetTokenizer),
    'xlm': (XLMConfig, XLMForSequenceClassification, XLMTokenizer),
    'roberta': (RobertaConfig, RobertaForSequenceClassification, RobertaTokenizer),
}

引数model_typeにてbertと与えられたら、bertのクラスが呼ばれるようになっています。
valueに存在するクラスについては、以前に扱いました。

BertForSequenceClassificationの出力層の数は「2」なので、分類問題用のモデルとなっています。

これらのクラスを以下のようにして変数にセットします。

config_class, model_class, tokenizer_class = MODEL_CLASSES[args.model_type]

まず、config_classの設定です。

 config = config_class.from_pretrained(args.config_name if args.config_name else args.model_name_or_path, num_labels=num_labels, finetuning_task=args.task_name)

デフォルトのハイパーパラメータ以外に以下を設定します。

  • num_labels
  • finetuning_task

finetuning_taskはドキュメントによると、checkpointの名前で用いるみたいです。

次に、tokenizerを設定します。

tokenizer = tokenizer_class.from_pretrained(args.tokenizer_name if args.tokenizer_name else args.model_name_or_path, do_lower_case=args.do_lower_case)

対象はbert-base-uncasedなので、do_lower_case=Trueとします。

最後にclass methodのfrom_pretrained()により、モデルをインスタンス化します。

model = model_class.from_pretrained(args.model_name_or_path, from_tf=bool('.ckpt' in args.model_name_or_path), config=config)
dataset

次に、datasetについてみていきます。
BERTなどは入力のdatasetが特殊なので、工夫が必要となります。
ちなみに元々の入力データは以下のようになっています。

it 's a charming and often affecting journey . 	1
unflinchingly bleak and desperate 	0

これをどのように変換するか確認します。
訓練データのload & tokenize & id変換はload_and_cache_examples()で行います。

train_dataset = load_and_cache_examples(args, args.task_name, tokenizer, evaluate=False)

load_and_cache_examplesを読んでみます。

examples = processor.get_dev_examples(args.data_dir) if evaluate else processor.get_train_examples(args.data_dir)
features = convert_examples_to_features(examples, label_list, args.max_seq_length, tokenizer, output_mode,
                                        cls_token_at_end=bool(args.model_type in ['xlnet']),
                                        # xlnet has a cls token at the end
                                        cls_token=tokenizer.cls_token,
                                        cls_token_segment_id=2 if args.model_type in ['xlnet'] else 0,
                                        sep_token=tokenizer.sep_token,
                                        sep_token_extra=bool(args.model_type in ['roberta']),
                                        # roberta uses an extra separator b/w pairs of sentences, cf. github.com/pytorch/fairseq/commit/1684e166e3da03f5b600dbb7855cb98ddfcd0805
                                        pad_on_left=bool(args.model_type in ['xlnet']),  # pad on the left for xlnet
                                        pad_token=tokenizer.convert_tokens_to_ids([tokenizer.pad_token])[0],
                                        pad_token_segment_id=4 if args.model_type in ['xlnet'] else 0,
                                        )

1行目で入力データからexamplesを作成します。
2行目でexamplesを引数として、各入力の形式に変換します。
2つ目のlabel_listはラベルが取り得る値のリストです。

label_list = ["0", "1"]

各exampleの処理を確認します。(一部コードを省略しています。)

features = []
for (ex_index, example) in enumerate(examples):

    tokens_a = tokenizer.tokenize(example.text_a)

    # The convention in BERT is:
    # (a) For sequence pairs:
    #  tokens:   [CLS] is this jack ##son ##ville ? [SEP] no it is not . [SEP]
    #  type_ids:   0   0  0    0    0     0       0   0   1  1  1  1   1   1
    # (b) For single sequences:
    #  tokens:   [CLS] the dog is hairy . [SEP]
    #  type_ids:   0   0   0   0  0     0   0
    
    tokens = tokens_a + [sep_token]  # sep_token = '[SEP]'
    segment_ids = [sequence_a_segment_id] * len(tokens)

    tokens = [cls_token] + tokens
    segment_ids = [cls_token_segment_id] + segment_ids

    input_ids = tokenizer.convert_tokens_to_ids(tokens)

    # The mask has 1 for real tokens and 0 for padding tokens.
    input_mask = [1 if mask_padding_with_zero else 0] * len(input_ids)
    input_ids = input_ids + ([pad_token] * padding_length)  # pad_token=0
    input_mask = input_mask + ([0 if mask_padding_with_zero else 1] * padding_length)
    segment_ids = segment_ids + ([pad_token_segment_id] * padding_length)

    label_id = label_map[example.label]

    features.append(
        InputFeatures(input_ids=input_ids,
                      input_mask=input_mask,
                      segment_ids=segment_ids,
                      label_id=label_id))

return features

class InputFeatures(object):
    """A single set of features of data."""

    def __init__(self, input_ids, input_mask, segment_ids, label_id):
        self.input_ids = input_ids
        self.input_mask = input_mask
        self.segment_ids = segment_ids
        self.label_id = label_id

やっていることは、大まかに言うと
1. tokenize
2. [SEP], [CLS]の挿入
3. id変換
4. padding
5. mask
です。(2.を除いて一般的なNLPタスクとほとんど同じです。)

Training

変換した訓練データと定義済のモデルでfine tuningを行います。

global_step, tr_loss = train(args, train_dataset, model, tokenizer)

train()内では、samplerとdataloaderを定義します。

train_sampler = RandomSampler(train_dataset) if args.local_rank == -1 else DistributedSampler(train_dataset)
train_dataloader = DataLoader(train_dataset, sampler=train_sampler, batch_size=args.train_batch_size)

samplerとdataloaderについては以下が分かりやすいです。
PytorchのDataloaderとSamplerの使い方 - Qiita

そっから、ごにょごにょした後、エポックを回します。
イテレーションの内容は、一般的な学習とほとんど同じです。(一部コードを省略しています。)

tr_loss, logging_loss = 0.0, 0.0
model.zero_grad()
train_iterator = trange(int(args.num_train_epochs), desc="Epoch", disable=args.local_rank not in [-1, 0])
set_seed(args)  # Added here for reproductibility (even between python 2 and 3)
for _ in train_iterator:
    epoch_iterator = tqdm(train_dataloader, desc="Iteration", disable=args.local_rank not in [-1, 0])
    for step, batch in enumerate(epoch_iterator):
        model.train()
        batch = tuple(t.to(args.device) for t in batch)
        inputs = {'input_ids': batch[0],
                  'attention_mask': batch[1],
                  'token_type_ids': batch[2] if args.model_type in ['bert', 'xlnet'] else None,
                  # XLM don't use segment_ids
                  'labels': batch[3]}
        outputs = model(**inputs)
        loss = outputs[0]  # model outputs are always tuple in pytorch-transformers (see doc)

        loss = loss.mean() # mean() to average on multi-gpu parallel training

        loss.backward()
        torch.nn.utils.clip_grad_norm_(model.parameters(), args.max_grad_norm)  # default=1.0
      
        tr_loss += loss.item()

modelはBertForSequenceClassificationクラスですので、outputsには「(loss), logits, (hidden_states), (attentions)」が格納されています。
モデルの出力に関しては、ここを確認しておくと良いと思います。
Migrating from pytorch-pretrained-bert — pytorch-transformers 1.0.0 documentation

Saving best-practices

fine tuning後のモデルなどを保存しておきます。

model_to_save = model.module if hasattr(model, 'module') else model  # Take care of distributed/parallel training
model_to_save.save_pretrained(args.output_dir)
tokenizer.save_pretrained(args.output_dir)
# torch.save(model_to_save.state_dict(), output_model_file)

# Good practice: save your training arguments together with the trained model
torch.save(args, os.path.join(args.output_dir, 'training_args.bin'))

save_pretrainedでは、torch.save()を呼んで保存しています。

Evaluation

基本的に訓練時と同じです。
ただ、Samplerのところはランダムではなくシーケンシャルです。

# Note that DistributedSampler samples randomly
eval_sampler = SequentialSampler(eval_dataset) if args.local_rank == -1 else DistributedSampler(eval_dataset)
eval_dataloader = DataLoader(eval_dataset, sampler=eval_sampler, batch_size=args.eval_batch_size)

あと、lossと一緒にlogitも取り出す必要があります。

eval_loss = 0.0
nb_eval_steps = 0
preds = None
for batch in tqdm(eval_dataloader, desc="Evaluating"):
    outputs = model(**inputs)
    eval_loss += tmp_eval_loss.mean().item()

    if preds is None:
        preds = logits.detach().cpu().numpy()
        out_label_ids = inputs['labels'].detach().cpu().numpy()
    else:
        preds = np.append(preds, logits.detach().cpu().numpy(), axis=0)
        out_label_ids = np.append(out_label_ids, inputs['labels'].detach().cpu().numpy(), axis=0)

    eval_loss = eval_loss / nb_eval_steps
    preds = np.argmax(preds, axis=1)

    result = compute_metrics(eval_task, preds, out_label_ids)
    results.update(result)

ここらへんが理解できれば、別のタスクでもfine tuningできると思いました。

おわりに

run_glue.pyを走らせて、fine tuningを触ってみました。
run_glue.pyは動かすだけで、GLUEのfine tuningが出来るので凄く簡単です。
しかし、別のタスクに取り組むためには、run_glue.pyに頼らないコーディングが必要です。
次回は、run_glue.pyに存在しないタスク(CoNLL)に挑戦したいと思います。

pytorch-transformersを触ってみる②

はじめに

前回はの入門ということで、QuikStartの内容を触ってみました。
kento1109.hatenablog.com

前回は英語でしたが、日本語のテキストを扱う場合はそのまま使うことは出来ません。
ということで、今回はpytorch-transformersで日本語のテキストを扱ってみようと思います。

Pretrained model

日本語でのPretrained modelとしては、京大の黒橋・河原研究室が公開しているものが有名です。
BERT日本語Pretrainedモデル - KUROHASHI-KAWAHARA LAB

このリソースを利用した既存のやってみたシリーズとしては以下などが参考となります。
Pytorchで日本語のbert学習済みモデルを動かすまで - Qiita
pytorchでBERTの日本語学習済みモデルを利用する - 文章埋め込み編 - Out-of-the-box

今回はこちらのリソースを活用して色々と触っていきます。

準備

ホームページより、Japanese_L-12_H-768_A-12_E-30_BPEをダウンロードします。
Pretrained modelは、Jumann++で形態素解析を行っているので、Jumann++をインストールしておきます。
また、Jumann++をpythonから利用するため、pyknp もインストールしておきます。

以下のようにpythonから形態素解析が出来れば準備OKです。

from pyknp import Juman
jumanpp = Juman()

text = "すもももももももものうち"
result = jumanpp.analysis(text)
print([mrph.midasi for mrph in result.mrph_list()])
# ['すもも', 'も', 'もも', 'も', 'もも', 'の', 'うち']

ただし、今回は触ってみることが目的なので、前処理の形態素解析は必ずしも必要でないです。

読み込み

前回は、以下のように文字列を指定してをロードしました。

model = BertModel.from_pretrained('bert-base-uncased')

今回は、ダウンロードしたpytorch_model.binを指定して読み込みます。
※学習は、BERT(BASE)と同じ設定 (12-layer, 768-hidden, 12-head)で行ったそうです。

model = BertModel.from_pretrained('bert-base-uncased')
# UnicodeDecodeError: 'utf-8' codec can't decode byte 0x80 in position 0: invalid start byte

しかし、この場合UnicodeDecodeErrorが発生します。

上に挙げた記事はpytorch_pretrained_bertというライブラリを使っていたので、
少々勝手が異なるみたいです。

エラー箇所を見ると、

Constructs a `BertConfig` from a json file of parameters.

に関連する箇所が怪しかったので、以下のようにしてconfigclasssを引数に与えました。

config = BertConfig.from_json_file('Japanese_L-12_H-768_A-12_E-30_BPE/bert_config.json')
model = BertModel.from_pretrained('Japanese_L-12_H-768_A-12_E-30_BPE/pytorch_model.bin', config=config)

とりあえず、エラーは出ていないので問題なさそうです。

bertForMaskedLM

前回同様に、文の一部をMASKする問題を試してみます。

model = BertForMaskedLM.from_pretrained('Japanese_L-12_H-768_A-12_E-30_BPE/pytorch_model.bin', config=config)

この場合の出力は単語数となります。modelの最後の層を見てみると、

(decoder): Linear(in_features=768, out_features=32006, bias=False)

となっているのが分かります。

では、「僕は友達とサッカーをすることが好きだ」の文を形態素解析します。

from pyknp import Juman
jumanpp = Juman()
text = "僕は友達とサッカーをすることが好きだ"
result = jumanpp.analysis(text)
tokenized_text = [mrph.midasi for mrph in result.mrph_list()]
print(tokenized_text)
# ['僕', 'は', '友達', 'と', 'サッカー', 'を', 'する', 'こと', 'が', '好きだ']

となります。
続いて、BERT用の入力に整形し、「サッカー」をMASKしてみます。

tokenized_text.insert(0, '[CLS]')
tokenized_text.append('[SEP]')
masked_index = 5
tokenized_text[masked_index] = '[MASK]'
print(tokenized_text)
['[CLS]', '僕', 'は', '友達', 'と', '[MASK]', 'を', 'する', 'こと', 'が', '好きだ', '[SEP]']

日本語の辞書ファイルを読み込みます。

tokenizer = BertTokenizer("Japanese_L-12_H-768_A-12_E-30_BPE/vocab.txt",
                          do_lower_case=False, do_basic_tokenize=False)

一応、入力系列を確認してみます。

indexed_tokens = tokenizer.convert_tokens_to_ids(tokenized_text)
tokens_tensor = torch.tensor([indexed_tokens])
print(tokens_tensor)
#tensor([[    2,  5020,     9, 10729,    12,     4,    10,    22,    30,    11,    30808,     3]])

ここまで来れば、言語は関係ありません。

model.eval()

tokens_tensor = tokens_tensor.to('cuda')
model.to('cuda')

with torch.no_grad():
    outputs = model(tokens_tensor)
    predictions = outputs[0]

_, predicted_indexes = torch.topk(predictions[0, masked_index], k=5)
predicted_tokens = tokenizer.convert_ids_to_tokens(predicted_indexes.tolist())
print(predicted_tokens)
# ['話', '仕事', 'キス', 'ゲーム', 'サッカー']

5番目にサッカーと予測できています。その他も、MASKとして妥当そうです。

[SEP]は必要??

ここで、思ったのですが、単文の場合は末尾に[SEP]は必要なのでしょうか。
試しに[SEP]無しで予測した結果が以下の通りです。

print(predicted_tokens)
['話', '仕事', 'キス', '会話', 'セックス']

サッカーは挙がってきませんが、概ね結果は同じです。
(サッカーの代わりにセックスと予測したのはなかなか面白いですね。。)

ここから、[SEP]が予測結果に影響を与えていることが分かります。
また、[SEP]は付けておいた方が無難かと思います。(他では結果が異なるかもしれませんが・・)

おわりに

今回は、pytorch-transformersで日本語のテキストを触ってみました。
次回は、fine tuningで目的のタスクに応用してみたいと思います。

pytorch-transformersを触ってみる①

今更ながら、pytorch-transformersを触ってみます。
このライブラリはドキュメントが充実していて、とても親切です。
なので、今回はドキュメントに基づいて触ってみただけの備忘録です。
以下、有名どころのBERTで試してます。

詳しいことはここなどを参照してください。
huggingface.co

はじめに

以下で、入手できます。簡単です。

pip install pytorch-transformers

インストールしたら、以下でimportします。

import torch
from pytorch_transformers import BertTokenizer, BertModel

pytorch-transformersの基本は以下の3つのクラスで構成されます。

model classes モデル本体
configuration classes モデルのパラメータを設定するクラス(デフォルト設定の場合は不要)
tokenizer classes 入力文のtokenizeや辞書に関連するクラス

model classes

各モデル毎にfine tuningのための以下の7つのクラスが準備されています。
例えば、BERTの場合は以下のクラスがあります。

  • BertModel
  • BertForPreTraining
  • BertForMaskedLM
  • BertForNextSentencePrediction
  • BertForSequenceClassification
  • BertForMultipleChoice
  • BertForTokenClassification
  • BertForQuestionAnswering

理解を深めるため、クラスのコードを追っかけてみます。
modeling_bert.pyを見ると、全てBertPreTrainedModelの派生クラスということが分かります。
なので、基本的な振る舞いはほとんど同じです。
更に、BertPreTrainedModelはPreTrainedModelの派生クラスということが分かります。
PreTrainedModelが定義されているmodeling_utils.pyを見ると、
torch.nn.Modulesを継承しているということが分かります。
torch.nn.Modulesは、モデルを構築する時に継承するあれです。

では、BertForMaskedLMのアーキテクチャを見てみます。

from pytorch_transformers import BertForMaskedLM
model = BertForMaskedLM.from_pretrained('bert-base-uncased')
BertForMaskedLM(
  (bert): BertModel(
    (embeddings): BertEmbeddings(
      (word_embeddings): Embedding(30522, 768, padding_idx=0)
      (position_embeddings): Embedding(512, 768)
      (token_type_embeddings): Embedding(2, 768)
      (LayerNorm): BertLayerNorm()
      (dropout): Dropout(p=0.1)
    )
    (encoder): BertEncoder(
      (layer): ModuleList(
        (0): BertLayer(
        .
        .
        .
  (cls): BertOnlyMLMHead(
    (predictions): BertLMPredictionHead(
      (transform): BertPredictionHeadTransform(
        (dense): Linear(in_features=768, out_features=768, bias=True)
        (LayerNorm): BertLayerNorm()
      )
      (decoder): Linear(in_features=768, out_features=30522, bias=False)
    )
  )
)

最後のdecoderの部分に着目すると、

(decoder): Linear(in_features=768, out_features=30522, bias=False)

30522クラスへの線形変換を行っていることが分かります。
30522は単語の数なので、単語を出力するためのモデルとなっていることが分かります。

同様にBertForSequenceClassificationの出力層を見てみます。

from pytorch_transformers import BertForSequenceClassification
model = BertForSequenceClassification.from_pretrained('bert-base-uncased')
(classifier): Linear(in_features=768, out_features=2, bias=True)

2クラスの線形変換であり、分類問題ということが分かります。
このように、タスクに応じて適切なモデルが用意されています。

では、BertModelとは何なんでしょうか。出力層を見てみます。

model = BertModel.from_pretrained('bert-base-uncased')
  (pooler): BertPooler(
    (dense): Linear(in_features=768, out_features=768, bias=True)
    (activation): Tanh()
  )

良く分かりませんが、説明では

use BertModel to encode our inputs in hidden-states

と書いていますので、この出力を別のモデルに連結する場合に使用するのでしょうか。
(用途が良く分かっていません。。)

Pretrained models

上ではモデルをロードする際に以下のように書いていました。

model = BertModel.from_pretrained('bert-base-uncased')

bert-base-uncased とは何なんでしょうか。

これは学習済のパラメータをロードするための記述です。
例えば、bert-base-uncasedと指定した場合は、12-layer, 768-hidden, 12-headsで小文字英文での事前学習済のパラメータがロードされます。
*論文中のBERT(BASE)に相当します。

以下のように自身で学習したパラメータのロードも可能です。

model = BertModel.from_pretrained('./test/saved_model/')

from_pretrainedについても、少しだけ追っかけてみました。
先ほど、BertModelはPreTrainedModelを継承していると書きました。
このfrom_pretrainedもPreTrainedModelのclassmethodとして定義されています。
(modeling_utils.pyの375行目あたり)
ロードするモデルについては、以下のように記述されています。

# Load model
if pretrained_model_name_or_path in cls.pretrained_model_archive_map:
    archive_file = cls.pretrained_model_archive_map[pretrained_model_name_or_path]
elif os.path.isdir(pretrained_model_name_or_path):
    if from_tf:
        # Directly load from a TensorFlow checkpoint
        archive_file = os.path.join(pretrained_model_name_or_path, TF_WEIGHTS_NAME + ".index")
    else:
        archive_file = os.path.join(pretrained_model_name_or_path, WEIGHTS_NAME)

cls.pretrained_model_archive_mapは以下のような辞書ファイルです。

BERT_PRETRAINED_MODEL_ARCHIVE_MAP = {
    'bert-base-uncased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-uncased-pytorch_model.bin", 
    # ...
}

なので、引数をbert-base-uncasedとした場合は、Amazon S3 のストレージ上のmodelファイルを見に行くのですね。
(ローカルダウンロード後はそちらを見に行きます。)
model_archive_mapに存在しない文字列の場合は、ローカルのmodelファイルを見に行きます。
その後、指定したmodelファイルを以下のようにロードします。

if state_dict is None and not from_tf:
    state_dict = torch.load(resolved_archive_file, map_location='cpu')

このstate_dictもオプショナルな引数のようで、指定して与えることも可能です。

configuration classes

BertConfigなどのConfigクラスは、PretrainedConfigクラスを継承しています。
PretrainedConfigクラスはclassmethodとして以下のような関数があります。

  • from_dict
  • from_json_file
  • from_pretrained

from_dict, from_json_fileなどはハイパーパラメータを与える形式の違いということが想定できます。
ややこしいのが、from_pretrainedです。Modelクラスにもfrom_pretrainedとの違いについて少し掘り下げてみます。
BertConfigも同じようにPretrainedConfigのclassmethodとして定義されています。
(modeling_utils.pyの109行目あたり)
ここも、BertModelとやっていることはほとんど同じです。

config = BertConfig.from_pretrained('bert-base-uncased') 

のように記述された場合、以下の辞書ファイルに従ってAmazon S3 のストレージ上のjsonファイルを見に行きます。

BERT_PRETRAINED_CONFIG_ARCHIVE_MAP = {
    'bert-base-uncased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-uncased-config.json",
    # ...

ちなみに、こっちはJSON形式なので、ブラウザから参照可能です。
(bert-base-uncasedは以下のようなJSONファイルです。)

{
  "attention_probs_dropout_prob": 0.1,
  "hidden_act": "gelu",
  "hidden_dropout_prob": 0.1,
  "hidden_size": 768,
  "initializer_range": 0.02,
  "intermediate_size": 3072,
  "max_position_embeddings": 512,
  "num_attention_heads": 12,
  "num_hidden_layers": 12,
  "type_vocab_size": 2,
  "vocab_size": 30522
}

少し、込み入ったので内容を整理すると、

model = BertModel.from_pretrained('bert-base-uncased')

のようにしてmodelクラスのfrom_pretrainedが呼ばれると、引数にconfigがない場合は
1. from_pretrainedの文字列でconfig.from_pretrainedを呼び出し
2. config.from_pretrainedの結果をconfigにセット
という手順でハイパーパラメータの値を得ます。
そのconfigの値で、モデルをインスタンス化します。
その後、読み込んでおいたmodelファイルを_load_from_state_dict()でセットします。

tokenizer classes

tokenizer classesにもfrom_pretrained()を通じてインスタンス化可能です。
tokenizer classのfrom_pretrainedも同様に

tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')

のように記述された場合、Amazon S3 のストレージ上のtxtファイルを見に行きます。

このtxtファイルはtokenが行単位でズラーっと記述されたものです。

.
skin
foreign
opening
governor
okay
.
引数について

tokenizerクラスには以下のような引数があります。

  • do_lower_case .. Whether to lower case the input. Only has an effect when do_wordpiece_only=False
  • do_basic_tokenize .. Whether to do basic tokenization before wordpiece.

※日本語の場合、do_basic_tokenize =False としおかないと文字単位に分割されてしまいます。

実験

最後に、いくつかのモデルを触ってみます。

bertForMaskedLM

ある文(文章)の一部をMASKして、それを予測する言語モデルです。
以下の文で試してみます。

I like to play football with my friends

ここで、footballをMASKして予測できるか試してみます。

まず、BERT用に先頭[CLS]、末尾に[SEP]を付けます。

text = "[CLS] I like to play football with my friends [SEP] ."

このtextをtokenizerに与えます。

tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
tokenized_text = tokenizer.tokenize(text)
print(tokenized_text)
# ['[CLS]', 'i', 'like', 'to', 'play', 'football', 'with', 'my', 'friends', '[SEP]', '.']

次に、footballの箇所をMASKします。

masked_index = 5
tokenized_text[masked_index] = '[MASK]'
print(tokenized_text)
# ['[CLS]', 'i', 'like', 'to', 'play', '[MASK]', 'with', 'my', 'friends', '[SEP]', '.']

最後に単語を対応するidに変換したら入力系列の出来上がりです。

indexed_tokens = tokenizer.convert_tokens_to_ids(tokenized_text)
tokens_tensor = torch.tensor([indexed_tokens])

これをモデルに与えます。
このあたりは、通常のニューラルネットと同じ感じでいけます。

model = BertForMaskedLM.from_pretrained('bert-base-uncased')
model.eval()

tokens_tensor = tokens_tensor.to('cuda')
segments_tensors = segments_tensors.to('cuda')
model.to('cuda')

with torch.no_grad():
    outputs = model(tokens_tensor)
    predictions = outputs[0]
_, predicted_indexes = torch.topk(predictions[0, masked_index], k=5)
predicted_tokens = tokenizer.convert_ids_to_tokens(predicted_indexes.tolist())
print(predicted_tokens)
# ['golf', 'games', 'football', 'chess', 'tennis']

footballが3番目に来てます。良い感じです。
今回の文では、どれも正解と言えると思います。

BertForNextSentencePrediction

2つの文が連続しているかどうかを判定します。

まずは、ドキュメント通りに試してみます。

### Classify next sentence using ``bertForNextSentencePrediction``
# Going back to our initial input
text = "[CLS] Who was Jim Henson ? [SEP] Jim Henson was a puppeteer [SEP]"
tokenized_text = tokenizer.tokenize(text)
indexed_tokens = tokenizer.convert_tokens_to_ids(tokenized_text)
segments_ids = [0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1]
tokens_tensor = torch.tensor([indexed_tokens])
segments_tensors = torch.tensor([segments_ids])

model = BertForNextSentencePrediction.from_pretrained('bert-base-uncased')
model.eval()

tokens_tensor = tokens_tensor.to('cuda')
segments_tensors = segments_tensors.to('cuda')
model.to('cuda')

# Predict the next sentence classification logits
with torch.no_grad():
    next_sent_classif_logits = model(tokens_tensor, segments_tensors)

print(torch.softmax(next_sent_classif_logits[0], dim=1))
# tensor([[1.0000e+00, 2.8665e-06]], device='cuda:0')

0が連続していると考えると、正解しています。

次に、連続していない文章を与えてみます。

text = "[CLS] Japan has many traditional areas [SEP] Mike goes to library to study [SEP]"

予測結果を見てみます。

torch.softmax(next_sent_classif_logits[0], dim=1)
# tensor([[1.3438e-05, 9.9999e-01]], device='cuda:0')

さっきとは逆に1の方の確率が高くなっています。これも正解です。

おわりに

pytorch-transformersの簡単なところを触ってみました。
次回は、日本語の学習済モデルを使って同様のことを試してみたいと思います。

*コードはの行数などは記事を書いた時点のものなので、今後変更されるかもしれません。

tensorboardXの基本的な使い方①

はじめに

今更ですが、tensorboardを触ってみました。

普段TensorFlowを使わないので、何となく避けてたのですが、
tensorboardXは「tensorboard for pytorch」ということで、
触ってみました。

結論から言うとめちゃくちゃ簡単で便利でした。
なので、備忘録として残そうと思います。

きっちりと理解したい人は公式のGithubやDocumentを見るのが良いと思います。
github.com

tensorboardx.readthedocs.io

ここでは、利用頻度が高いと思わるものについて限定してまとめます。

基本

基本的な使い方は以下の通りです。

from tensorboardX import SummaryWriter

writer = SummaryWriter()

# build network
# training

# data to tensorboard
writer.add_XXX

writer.close()

作った後は以下をシェルで起動します。

tensorboard --logdir runs

立ち上がった後はブラウザから確認可能です。

http://localhost:6006/

add_scalar

1次元(スカラー値)の連続データを描画します。

add_scalar(tag, scalar_value, global_step=None, walltime=None)

サンプルはこんな感じです。

from tensorboardX import SummaryWriter
writer = SummaryWriter()
x = range(100)
for i in x:
    writer.add_scalar('y=2x', i * 2, i)
writer.close()

f:id:kento1109:20190706173125p:plain:w300

損失値の描画に使うとこんな感じになります。

for n_iter in range(epochs):
    train_loss = train(...)
    test_loss = test(...)
    writer.add_scalar('data/scalar1', train_loss, n_iter)
    writer.add_scalar('data/scalar2', test_loss, n_iter)
writer.close()

f:id:kento1109:20190706173644p:plain

add_scalars

訓練データとテストデータの損失値を合わせて描画したいです。
そんな場合は「add_scalars」を使うと便利です。

add_scalars(main_tag, tag_scalar_dict, global_step=None, walltime=None)

サンプルはこんな感じです。

from tensorboardX import SummaryWriter
writer = SummaryWriter()
r = 5
for i in range(100):
    writer.add_scalars('run_14h', {'xsinx':i*np.sin(i/r),
                                    'xcosx':i*np.cos(i/r),
                                    'tanx': np.tan(i/r)}, i)
writer.close()

f:id:kento1109:20190706173937p:plain:w300

さて、訓練データとテストデータの損失値を合わせて描画します。

for n_iter in range(epochs):
    train_loss = train(...)
    test_loss = test(...)
    writer.add_scalars('data/scalars',{'train': train_loss,
                                       'test': test_loss
                                       }, n_iter)
writer.close()

f:id:kento1109:20190706174419p:plain

ちなみにタグは「/」でグループ化できるみたいです。
テストの精度を含めてグループ化してみます。

for n_iter in range(epochs):
    train_loss = train(...)
    test_loss, test_acc = test(...)
    writer.add_scalars('model1/loss',{'train': train_loss,
                                       'test': test_loss
                                       }, n_iter)
    writer.add_scalar('model1/acc', test_acc, n_iter)
writer.close()

f:id:kento1109:20190706175550p:plain

今回、紹介したのは add_scalar, add_scalars だけですが、
とりあえず、基本的な学習時の値を描画するのは十分かと思います。

本日、用いたコードは以下に置きました。
repo/tensorX.py at master · kento1109/repo · GitHub

次回は、Embeddingについてまとめたいと思います。