諸々

信号処理・機械学習に興味を持っているエンジニア見習いのメモです。技術的なことから趣味のことを書いていきます。

素人がBERT触ってみた

はじめに

NLPド素人の私が、少しBERTに興味が湧いたので動かしてみました。(最近物忘れが酷く、備忘録代わりです...)

今回は日本語版BERTを使って、似たような意味合いの文の文ベクトル・単語ベクトルを算出して確認していきます。

コードはこちら

この記事の最後に、最近Bandai Namco Research様が公開されたDistilBERTを動かしてみた結果も記載しております。ただ、こちら動かしかたが悪かったのか期待する結果が得られませんでした...(もし何か間違っているようでしたら、教えていただけると幸いです)

実施環境

  • MacOS Mojave 10.14.5
  • Anaconda3-5.0.0

環境構築

$conda create -n {env_name} python=3.7
$source activate {env_name}
$pip install torch torchvision
$pip install transformers==2.4.1

source activateのときにPyenv関係で引っかかる場合は、pyenv which activateactivateのパスを確認して、それを叩くするようにすればOK。

transformers==2.4.1は、バージョン指定しないとインストールできなかったため、公式リポジトリのIssueで書いてあったバージョン(2.4.1)を指定しています。

日本語版BERTをとりあえず動かす

transformersの公式サンプルに沿って、文書を推論させてみます。

まずは事前準備として、必要な関数を作ります。

import torch
import numpy as np
from transformers.tokenization_bert_japanese import BertJapaneseTokenizer
from transformers import BertModel

# 日本語トークナイザ
tokenizer = BertJapaneseTokenizer.from_pretrained('bert-base-japanese')
# 事前学習済みBert
model = BertModel.from_pretrained('bert-base-japanese')

def cos(x1, x2):
    """
    引数の2つベクトルの内積を計算する
    """
    return np.dot(x1, x2) / (np.linalg.norm(x1) * np.linalg.norm(x2))

def create_vec(text):
    """ 入力されたテキストを推論する """
    input_batch = [text]

    # トークナイズでテキストを分割する
    encoded_data = tokenizer.batch_encode_plus(
        input_batch, pad_to_max_length=True, add_special_tokens=True)

    # 単語ベクトルをIDに変換する
    input_ids = torch.tensor(encoded_data["input_ids"])

    # 推論
    with torch.no_grad():
        outputs = model(input_ids)

    # 最終層の情報と、分割したトークンを返す
    last_hidden_states = outputs[0]
    return last_hidden_states.detach().numpy(), tokenizer.convert_ids_to_tokens(input_ids[0].tolist())

準備ができたら、実際に試してみます!

text = "私は明日から自宅勤務です。腰を傷めないようにお高い椅子を買おうと思います"
last_hidden_states, token = create_vec(text)
print(last_hidden_sates.shape)
> (1, 27, 765)

ちゃんと計算してくれました!(バッチサイズ, 文字数, 特徴数)みたいな感じで返してくれるみたいですね。 (文字数には、[CLS], [SEP]が文頭と文末に追加されているっぽい。)

last_hidden_states[:, 0, :]  # [CLS]のベクトル
last_hidden_states[:, -1, :] # [SEP]のベクトル

文ベクトルを比較してみる

次に、文ベクトルがどうなるかを試してみようかと思います。

ここで比較する文書は、

text1 = "IT企業ではないアマゾンにはよく釣りに行く"
text2 = "南米のアマゾン川は危険な生き物がたくさんいる"
text3 = "アマゾンで椅子を買ってリモートワーク環境を作る"

この3つです!

text1とtext2, text1とtext3, text2とtext3の文書ベクトルを比較してみます。

個人的希望では、text1とtext2の類似度がtext1とtext3より大きくなってくれるとうれしいなぁと思います。

ここでの文ベクトルは、

https://qiita.com/ichiroex/items/6e305a5d5bed7d715c2f

こちらを参考にさせて頂いて、[CLS]のベクトルを使います!

具体的には、

# batch_size=3として計算できるが、今回はわけて計算しています。
vec1, _ = create_vec(text1)
vec2, _ = create_vec(text2)
vec3, _ = create_vec(text3)

# 文ベクトルの類似度を出力
print("「{}...」と「{}...」の文ベクトル類似度:{:.5f}".format(text1[:5], text2[:5], cos(vec1[0, 0, :], vec2[0, 0, :])))
print("「{}...」と「{}...」の文ベクトル類似度:{:.5f}".format(text1[:5], text3[:5], cos(vec1[0, 0, :], vec3[0, 0, :])))
print("「{}...」と「{}...」の文ベクトル類似度:{:.5f}".format(text3[:5], text2[:5], cos(vec3[0, 0, :], vec2[0, 0, :])))

> 「IT企業で...」と「南米のアマ...」の文ベクトル類似度:0.72820
> 「IT企業で...」と「アマゾンで...」の文ベクトル類似度:0.71819
> 「アマゾンで...」と「南米のアマ...」の文ベクトル類似度:0.68089

text1とtext2の類似度が一番高い結果になりました!

「IT企業ではないアマゾン...」といった否定形のように書けば、「南米のアマゾン...」と同じ意味だと捉えれくれたのでしょうか?(そこまで類似度に差はないけれど...)

ただ、text2とtext3の類似度は他に比べ低くなっているので、そのあたりはちゃんと認識してくれてるっぽい...?

text3の文章をちょこっと変えたら、類似度逆転しました...この辺りはまだまだ難しいのでしょうかね?

ちなみに、全然関係ない文との関係は?

上ではアマゾン系統の文章を使いましたが、全く関係ない文章との比較も試しました!

text1 = "アマゾンで椅子を買ってリモートワーク環境を作る"
text2 = "アマゾンのクラウドを利用してシステムを構築する"
except_text = "明日の晩御飯は豚の生姜焼きともやし炒めにする"

vec1, _ = create_vec(text1)
vec2, _ = create_vec(text2)
ex_vec, _ = create_vec(except_text)

# 文ベクトルの類似度を出力
print("「{}...」と「{}...」の文ベクトル類似度:{:.5f}".format(text1[:5], text2[:5], cos(vec1[0, 0, :], vec2[0, 0, :])))
print("「{}...」と「{}...」の文ベクトル類似度:{:.5f}".format(text1[:5], except_text[:5], cos(vec1[0, 0, :], ex_vec[0, 0, :])))
print("「{}...」と「{}...」の文ベクトル類似度:{:.5f}".format(except_text[:5], text2[:5], cos(ex_vec[0, 0, :], vec2[0, 0, :])))

> 「アマゾンで...」と「アマゾンの...」の文ベクトル類似度:0.81488
> 「アマゾンで...」と「明日の晩御...」の文ベクトル類似度:0.67456
> 「明日の晩御...」と「アマゾンの...」の文ベクトル類似度:0.65609

割とはっきり結果が出るもんですねー。

特定単語ベクトルを比較してみる

お次は、文章の中の特定の単語のベクトルに違いが出るのか試してみます!

ここで使う文章も、

text1 = "IT企業ではないアマゾンにはよく釣りに行く"
text2 = "南米のアマゾン川は危険な生き物がたくさんいる"
text3 = "アマゾンのクラウドサービスをよく利用している"

この3つです!

そして、特定の単語は「アマゾン」にしてみます。個人的には、text1とtext2が一番類似度高くなってほしいですね!

vec1, token1 = create_vec(text1)
vec2, token2 = create_vec(text2)
vec3, token3 = create_vec(text3)

# 対象の単語
TARGET = "アマゾン"

# 単語ベクトルの類似度を出力
# TARGETのインデックスを取得し、それに対応するベクトルで計算する
print("「{}...」と「{}...」の文ベクトル類似度:{:.5f}".format(text1[:5], text2[:5], cos(vec1[0, token1.index(TARGET), :], vec2[0, token2.index(TARGET), :])))
print("「{}...」と「{}...」の文ベクトル類似度:{:.5f}".format(text1[:5], text3[:5], cos(vec1[0, token1.index(TARGET), :], vec3[0, token3.index(TARGET), :])))
print("「{}...」と「{}...」の文ベクトル類似度:{:.5f}".format(text3[:5], text2[:5], cos(vec3[0, token3.index(TARGET), :], vec2[0, token2.index(TARGET), :])))

> 「IT企業で...」と「南米のアマ...」の文ベクトル類似度:0.65961
> 「IT企業で...」と「アマゾンで...」の文ベクトル類似度:0.71137
> 「アマゾンで...」と「南米のアマ...」の文ベクトル類似度:0.68076

うーん...難しかったのでしょうか、なかなか期待通りにはなってくれないです。

でも、ただただWord2Vec使うよりかは断然BERT使ったほうが良さげですね!

おわりに

超初心者がBERTを単純に動かしてみました!

Word2Vecに比べると、ちゃんと文章を理解してくれているのかなぁといった結果が見て取れました。

案件で少し使ってみようかと検討していたので、今回試すことができてよかったです!

余談:DistilBERTも少し動かしてみた

公式クイックスタート読んで環境構築して、以下のようなコードを動かしてみました。

間違いなどありましたら、ご指摘お願いいたします。

from transformers import AutoModel, AutoTokenizer, AutoConfig
import torch
import numpy as np

tokenizer = AutoTokenizer.from_pretrained('bert-base-japanese-whole-word-masking')
model = AutoModel.from_pretrained("./")

def cos(x1, x2):
    return np.dot(x1, x2) / (np.linalg.norm(x1) * np.linalg.norm(x2))

def create_vec(text):
    input_batch = [text]
    encoded_data = tokenizer.batch_encode_plus(
        input_batch, pad_to_max_length=True, add_special_tokens=True)
    input_ids = torch.tensor(encoded_data["input_ids"])
    with torch.no_grad():
        outputs = model(input_ids)
    last_hidden_states = outputs[0]
    return last_hidden_states.detach().numpy(), outputs[1]

text1 = "IT企業ではないアマゾンにはよく釣りに行く"
text2 = "南米のアマゾン川は危険な生き物がたくさんいる"
text3 = "アマゾンで椅子を買ってリモートワーク環境を作る"

vec1, _ = create_vec(text1)
vec2, _ = create_vec(text2)
vec3, _ = create_vec(text3)

# 文ベクトルの類似度を出力
print("「{}...」と「{}...」の文ベクトル類似度:{:.5f}".format(text1[:5], text2[:5], cos(vec1[0, 0, :], vec2[0, 0, :])))
print("「{}...」と「{}...」の文ベクトル類似度:{:.5f}".format(text1[:5], text3[:5], cos(vec1[0, 0, :], vec3[0, 0, :])))
print("「{}...」と「{}...」の文ベクトル類似度:{:.5f}".format(text3[:5], text2[:5], cos(vec3[0, 0, :], vec2[0, 0, :])))

> 「IT企業で...」と「南米のアマ...」の文ベクトル類似度:0.99377
> 「IT企業で...」と「アマゾンで...」の文ベクトル類似度:0.99441
> 「アマゾンで...」と「南米のアマ...」の文ベクトル類似度:0.99102

BERTに比べると類似度に差異があまりないですね...(やっぱりどこかミスってるのか?)

TouchDesignerで機械学習(ObjectDetection)動かしてみた

はじめに

勉強ついでに適当に作ってみたモデルをTouchDesigner内で動かしてみたくなったのでやってみました。

実際やってみたらこんな感じ!(この記事で使ったモデルとは違うけれど、だいたいこんな感じです)

www.youtube.com

サンプルのリポジトリこちら

ちなみに、今回はWindows前提で記事を書いていますがMacでも動作可能です!

TouchDesignerのPythonについて

TouchDesignerでは、拡張機能としてPythonを使用することができます(私の環境だと内部でPython3.7.2が動作するようです)。

Dialogs > Textport and DATs からPythonインタプリタを使うことができます。

プリインストールとして、標準ライブラリとOpenCV、numpyなんかが使えます。

f:id:T_sumida:20200404150722p:plain

今回はこれに加えて機械学習でよく使われるライブラリを使って、TouchDesginer内でObjectDetectionさせたいと思います。

どうやるかというと、Edit > Preferenceの「Add External Python to Search Path」に外部ライブラリへのパスを指定してTouchDesignerで使えるようにします。

ObjectDetectionについて

ObjectDetectionの詳しい説明は省きますが、下の画像みたいな人やモノなどのオブジェクトを検出(四角で囲んで、これが何かを示す)する感じです。

f:id:T_sumida:20200404150805p:plain

今回はできるだけ処理の軽いアルゴリズムを採用してなんとかFPSを稼ごうという魂胆のもと、モバイル端末でも動いてくれるMobileNetv2-SSDを使ってみます!

Python機械学習(主にDNN)を扱うライブラリ(フレームワーク)としては、

なんかが存在します。

私が普段扱うのはTensorflow/Kerasですが(たまにPytorchも使う)、今回はONNXを使います。(正確にはONNXRuntimeという推論環境を使います)

理由は、なぜか私の環境だとTouchDesigner内のPythonからTensorflowやPytorchを使うとエラーメッセージも吐かずにTouchDesignerが落ちてしまうからです。

そんなことがあった後でダメ元でONNX試したら動いてくれたのでこれを使います!(MXNetはわからない...)

肝心のモデルは、Tensorflowで作ったモデルをONNX形式に書き出したものを用意しました。([リポジトリ](https://github.com/T-Sumida/ObjectDetection4TouchDesginer

)内に含めています)

作業環境

  • Windows10 home
  • Anaconda
  • TouchDesigner 64-Bit Build 2020.20625

1. 環境構築

作業環境が揃ったら、さっそくONNXをインストールしてみます!

1-1. Anacondaで仮想環境を作る

$conda create -n touchdesigner python=3.7.2

1-2. 仮想環境内にONNXをインストールする

$source activate touchdesigner
$pip install onnxruntime==1.1.0

1-3. TouchDesignerに仮想環境のパスを通す

Edit > Preferences をクリックし環境設定を開きます。

以下の画像のように「Add External Python to Search Path」にチェックを入れ、「Python 64-bit Module Path」にAnacondaの仮想環境へのパスを通します。

f:id:T_sumida:20200404150618p:plain

私の環境だと以下のようなパスを通すことになります。

C:/Users/T-Sumida/Anaconda3/envs/touchdesigner/Lib/site-packages

これを参考にしてパスを通してみてください!

2. ONNXの準備

それではONNXがTouchDesiger上で動作することを確認します!

Dialogs > Textport and DATs からPythonインタプリタを起動し、そこに以下のコードを貼り付けて実行してみてください。

※モデルへのパスと、試したい画像のパスは適宜適切に設定してください。

import cv2
import numpy as np
import onnxruntime

# cocoデータセットのクラス番号:クラス名
coco_classes = {
    1: 'person',
    2: 'bicycle',
    3: 'car',
    4: 'motorcycle',
    5: 'airplane',
    6: 'bus',
    7: 'train',
    8: 'truck',
    9: 'boat',
    10: 'traffic light',
    11: 'fire hydrant',
    12: 'stop sign',
    13: 'parking meter',
    14: 'bench',
    15: 'bird',
    16: 'cat',
    17: 'dog',
    18: 'horse',
    19: 'sheep',
    20: 'cow',
    21: 'elephant',
    22: 'bear',
    23: 'zebra',
    24: 'giraffe',
    25: 'backpack',
    26: 'umbrella',
    27: 'handbag',
    28: 'tie',
    29: 'suitcase',
    30: 'frisbee',
    31: 'skis',
    32: 'snowboard',
    33: 'sports ball',
    34: 'kite',
    35: 'baseball bat',
    36: 'baseball glove',
    37: 'skateboard',
    38: 'surfboard',
    39: 'tennis racket',
    40: 'bottle',
    41:'wine glass',
    42: 'cup',
    43: 'fork',
    44: 'knife',
    45: 'spoon',
    46: 'bowl',
    47: 'banana',
    48: 'apple',
    49: 'sandwich',
    50: 'orange',
    51: 'broccoli',
    52: 'carrot',
    53: 'hot dog',
    54: 'pizza',
    55: 'donut',
    56: 'cake',
    57: 'chair',
    58: 'couch',
    59: 'potted plant',
    60: 'bed',
    61: 'dining table',
    62: 'toilet',
    63: 'tv',
    64: 'laptop',
    65: 'mouse',
    66: 'remote',
    67: 'keyboard',
    68: 'cell phone',
    69: 'microwave',
    70: 'oven',
    71: 'toaster',
    72: 'sink',
    73: 'refrigerator',
    74: 'book',
    75: 'clock',
    76: 'vase',
    77: 'scissors',
    78: 'teddy bear',
    79: 'hair drier',
    80: 'toothbrush'
}

# モデルを読み込む
session = onnxruntime.InferenceSession("ONNXモデルへのパスを指定")

# 画像を読み込む
img = cv2.imread("試したい画像のパスを指定")

# OpenCVは画像データをBGRで読み込むので、BGRに変換する
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

width, height = img.shape[0], img.shape[1]

img_data = np.expand_dims(img, axis=0)

# モデルの推論の準備
input_name = session.get_inputs()[0].name   # 'image'
output_name_boxes = session.get_outputs()[0].name     # 'boxes'
output_name_classes = session.get_outputs()[1].name   # 'classes'
output_name_scores = session.get_outputs()[2].name    # 'scores'
output_name_num = session.get_outputs()[3].name       # 'number of detections'

# 推論
outputs_index = session.run(
    [output_name_num, output_name_boxes, output_name_scores, output_name_classes],
    {input_name: img_data}
)

# 結果を受け取る
output_num = outputs_index[0] # 検出した物体数
output_boxes = outputs_index[1] # 検出した物体の場所を示すボックス
output_scores = outputs_index[2] # 検出した物体の予測確率
output_classes = outputs_index[3] # 検出した物体のクラス番号

# 予測確率の閾値
threshold = 0.6

# 推論結果を画像に変換
img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
for detection in range(0, int(output_num[0])):
    if output_scores[0][detection] > threshold:
        classes = output_classes[0][detection]
        boxes = output_boxes[0][detection]
        scores = output_scores[0][detection]
        top = boxes[0] * width
        left = boxes[1] * height
        bottom = boxes[2] * width
        right = boxes[3] * height

        top = max(0, top)
        left = max(0, left)
        bottom = min(width, bottom)
        right = min(height, right)
        img = cv2.rectangle(img, (int(left), int(top)), (int(right), int(bottom)), (0,0,255), 3)
        img = cv2.putText(
            img, "{}: {:.2f}".format(coco_classes[classes], scores),
            (int(left), int(top)),
            cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2, cv2.LINE_AA
        )

cv2.imshow('img', img)
k = cv2.waitKey(0)

実行すると以下のような画像が描画されるかと思います。

f:id:T_sumida:20200404150805p:plain

これが問題なく動けば一旦は大丈夫です!

3. TouchDesignerでPythonを書く

続いてTouchDesigerのパッチで動くようにしていきます。

こちらの記事を参考にさせて頂き(ありがとうございます!)、カメラ画像からの入力に対してObjectDetectionさせようと思います。

https://qiita.com/komakinex/items/5b84b88d537d393afc98

プロジェクト自体は非常にシンプルなもので、「Video Device In」からの入力を「OP Execute」で受け取りOpencvの機能で描画するようなモノにしました。

「OP Execute」の中身は以下のようにしました。

# me - this DAT.
# changeOp - the operator that has changed
#
# Make sure the corresponding toggle is enabled in the OP Execute DAT.

import cv2
import numpy as np
import onnxruntime

# 上のテストと一緒なので省略します(使うときは上からコピーして使ってください)
coco_classes = {
    1: 'person',
    2: 'bicycle',
    ...
}

session = onnxruntime.InferenceSession("C:/Users/TomoyukiSumida/Documents/Hatena/ObjetDetection4TD/ssdlite_mobilenetv2.onnx")

def onPreCook(changeOp):
    return

def onPostCook(changeOp):
    # 画像の読み込み
    frame = changeOp.numpyArray(delayed=True)
    arr = frame[:, :, 0:3]
    arr = arr * 255
    arr = arr.astype(np.uint8)
    arr = np.flipud(arr)
    width, height = arr.shape[0:2]
    image_data = np.expand_dims(arr, axis=0)

    # モデルの推論の準備
    input_name = session.get_inputs()[0].name   # 'image'
    output_name_boxes = session.get_outputs()[0].name     # 'boxes'
    output_name_classes = session.get_outputs()[1].name   # 'classes'
    output_name_scores = session.get_outputs()[2].name    # 'scores'
    output_name_num = session.get_outputs()[3].name       # 'number of detections'

    # 推論
    outputs_index = session.run([output_name_num, output_name_boxes,
                                output_name_scores, output_name_classes],
                                {input_name: image_data})

    # 結果を受け取る
    output_num = outputs_index[0] # 検出した物体数
    output_boxes = outputs_index[1] # 検出した物体の場所を示すボックス
    output_scores = outputs_index[2] # 検出した物体の予測確率
    output_classes = outputs_index[3] # 検出した物体のクラス番号

    # 予測確率の閾値
    threshold = 0.6

    # 推論結果を画像に変換
    for detection in range(0, int(output_num[0])):
        if output_scores[0][detection] > threshold:
            classes = output_classes[0][detection]
            boxes = output_boxes[0][detection]
            scores = output_scores[0][detection]
            top = boxes[0] * width
            left = boxes[1] * height
            bottom = boxes[2] * width
            right = boxes[3] * height

            top = max(0, top)
            left = max(0, left)
            bottom = min(width, bottom)
            right = min(height, right)
            arr = cv2.rectangle(arr, (int(left), int(top)), (int(right), int(bottom)), (0,0,255), 3)
            arr = cv2.putText(
                arr, "{}: {:.2f}".format(coco_classes[classes], scores),
                (int(left), int(top)),
                cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2, cv2.LINE_AA)
    arr = cv2.cvtColor(arr, cv2.COLOR_RGB2BGR)
    cv2.imshow('img', arr)
    return

def onDestroy():
    return

def onFlagChange(changeOp, flag):
    return

def onWireChange(changeOp):
    return

def onNameChange(changeOp):
    return

def onPathChange(changeOp):
    return

def onUIChange(changeOp):
    return

def onNumChildrenChange(changeOp):
    return

def onChildRename(changeOp):
    return

def onCurrentChildChange(changeOp):
    return

def onExtensionChange(changeOp, extension):
    return

これが書けたら、「OP Execute」のMonitor OPsに「Video Device In」を指定し、Post CookをONにしてください。

動いた!

f:id:T_sumida:20200404150820p:plain

おわりに

今回はTouchDesigner内でONNXを使ってObjectDetectionを動かしてみました。何故かTensorflowが動かなくて困りましたが、なんとか動くものができてよかったです。

ただ、TouchDesigner内でObjectDetection(ひいては機械学習)を動かすことには以下のようなデメリットがあります。 - TouchDesignerがシングルスレッドのため、プロジェクト全体のFPSが制限される - 私が確認した中だと、ONNXに変換されたモデルしか使えない(他のよいアルゴリズムが使えない場合がある)

なので、「TouchDesignerの後ろでPythonプロセスを起動しそこからOSCで情報を送る」などのほうが良さげではあります(今後TouchDesignerでマルチスレッドが使えるようになれば有用性はあるかもしれません)。

ONNXのモデルについても今回は私の自作モデルで試しましたが、onnx/modelからモデルファイルをダウンロードして使っても大丈夫です。(私もそこからmask-rcnnをダウンロードして動くことを確認しました)

また、GPUを使っての高速な推論も可能です。(今回のモデルはCPUでも高速に動くものを使いました)

$pip install onnxruntime==1.1.0

これの代わりに

$pip install onnxruntime-gpu==1.1.0

これをインストールしてやればGPU上で推論が行われるようになります。色々試してみても面白いのでしょうか!

表現側は難して手を出せていないですが、今後はその辺りも触れればいいなぁと思います!