アイソモカ

知の遊牧民の開発記録

BERT は毎朝味噌汁を作るか 【BertForMaskedLM】

BertMaskedLMで遊んでいて「毎朝あなたのために[MASK]を作ります。」で[MASK]部分に入る語を予測したら 1位が「詩」(確率0.078)、2位が「番組」(確率0.077) …ってなってて、えーそうなん???となった。Wikipediaで学習したBERTは味噌汁作れへんねや。

…と思ったら、これは「味噌汁」がボキャブラリに入ってないためでは?と…

いろいろ文脈を試しても「味噌汁」はどうして出なくて、「[MASK]汁」にしても「味噌」は出なくて、「味噌[MASK]」にすると「# # 汁」が出る。これはなに?

後で元の単語に復元できるよう、単語の先頭でないサブワードに「##」のような記号を付けることが多いので、「味噌汁」が1単語扱いで「味噌」と「##汁」というサブワードに分割されていたように取れます。

たしかにそうだ。

$ grep -n "味噌" BERT-base_mecab-ipadic-bpe-32k/vocab.txt
16023:味噌
$ grep -n "汁" BERT-base_mecab-ipadic-bpe-32k/vocab.txt
16504:汁
30629:##汁

「味噌汁」は、味噌 ##汁という2つのサブワードに分割される。

ということは、穴埋め問題を 毎 ##朝 あなた の ため に [MASK] ##汁 を 作り ます 。 の ように変更して、[MASK] に「味噌」が入ると予測されるかどうか?などしか確認できないな…

上で確認した数字は行番号。(行番号 - 1)がIDになるので、##汁のID 30628を直接指定して MaskedLM に入力しよう。

# bert3_maskedlm_misoshiru.py
import torch
from transformers import BertJapaneseTokenizer, BertForMaskedLM

tknz = BertJapaneseTokenizer.from_pretrained("cl-tohoku/bert-base-japanese")
ids = [2, 979, 28956, 6968, 5, 82, 7, 4, 30628, 11, 2580, 2610, 8, 3]
print(tknz.convert_ids_to_tokens(ids)) 
# ['[CLS]', '毎', '##朝', 'あなた', 'の', 'ため', 'に', '[MASK]', '##汁', 'を', '作り', 'ます', '。', '[SEP]']

mskpos = ids.index(tknz.mask_token_id)
print(mskpos) 
# 7

model = BertForMaskedLM.from_pretrained("cl-tohoku/bert-base-japanese")
ids_tensor = torch.LongTensor(ids).unsqueeze(0)
out = model(ids_tensor) # モデルの出力は要素が1つのタプル

topk = torch.topk(out[0][0][mskpos], k=5)
print(topk[0]) # 上位k個の値 
# tensor([17.4937, 14.9048, 14.6679, 13.3002, 12.9762], grad_fn=<TopkBackward>)
print(topk[1]) # 上位k個のindex
#tensor([16022,   883, 10102,  6792, 10117])
pred_tokens = tknz.convert_ids_to_tokens(topk[1])
print(pred_tokens)
#['味噌', '果', '豚', '灰', '墨']

よし、無事に「味噌」が1位にきた。

んー、ところで、topk[0] で出てる値ってなんなんですかね。loss? デモ版で出ている確率値(?)とはどうも違う…?

開発記録 210916 BertJapaneseTokenizer をさわってみる

『Pytorch自然言語処理プログラミング』の1章が終わって、間を飛ばして5章をやってます。

book.impress.co.jp

東北大BERT cl-tohoku/bert-base-japanese · Hugging Face と BertJapaneseTokenizer を使って文を単語列に分割していきましょう。

...
ModuleNotFoundError: You need to install fugashi to use MecabTokenizer. See https://pypi.org/project/fugashi/ for installation.

お。

$ pip install fugashi

インストーりました。

fugashi GitHub - polm/fugashi: A Cython MeCab wrapper for fast, pythonic Japanese tokenization and morphological analysis.MeCabを使うためのwrapperなんですね、 mecab-python3 みたいなやつかな。

...
ModuleNotFoundError: The ipadic dictionary is not installed. See https://github.com/polm/ipadic-py for installation.

お。

$ pip install ipadic

はい、こっちもインストーりました。

IPA辞書の説明 GitHub - polm/ipadic-py: IPAdic packaged for easy use from Python. を見るとデカデカと You Shouldn’t Use This って書いてて、もう更新されていないから UniDic を使ってねらしいけど… bert-base-japanese の説明では

Tokenization The texts are first tokenized by MeCab morphological parser with the IPA dictionary and then split into subwords by the WordPiece algorithm. The vocabulary size is 32000.

ということなので、IPA辞書でいくのがいいってことで。

# bert2.py 
from transformers import BertJapaneseTokenizer
tknz = BertJapaneseTokenizer.from_pretrained("cl-tohoku/bert-base-japanese")
print(tknz.tokenize("関西国際空港駅に行きます"))
#['関西', '##国際空港', '駅', 'に', '行き', 'ます']

文を単語列に分割してはる。

ところで以前、係り受け解析がうまくいかないと思ったら形態素解析に使用する辞書の違いだったことがあり 開発記録 191013 Sun (CaboChaが文節を区切ってくれない件、一応解決) - アイソモカ開発記録191124 Sun (100本ノック#041, UniDic) - アイソモカ 、懲りたので、辞書を指定して動かしてみる。

まずはターミナルから MeCab を直接。

# unidic
$ mecab -d /usr/local/lib/mecab/dic/unidic-cwj-2.3.0/ -Owakati
関西国際空港駅に行きます
関西 国際 空港 駅 に 行き ます 

# ipadic
$ mecab -d /usr/local/lib/mecab/dic/ipadic/ -Owakati
関西国際空港駅に行きます
関西国際空港 駅 に 行き ます

次に、Python からIPA辞書を指定して fugashi を使ってみる。

辞書の指定方法は、.../.pyenv/versions/3.7.4/lib/python3.7/site-packages/transformers/models/bert_japanese/tokenization_bert_japanese.pyを参考にしました。

import os
from fugashi import GenericTagger
import ipadic

dic_dir = ipadic.DICDIR
mecab_option = "-Owakati"
mecabrc = os.path.join(dic_dir, "mecabrc")
print(mecabrc)
# .../.pyenv/versions/3.7.4/lib/python3.7/site-packages/ipadic/dicdir/mecabrc

mecab_option = f'-d "{dic_dir}" -r "{mecabrc}" ' + mecab_option
tagger = GenericTagger(mecab_option)

text = "関西国際空港駅に行く"
print(tagger.parse(text))
# 関西国際空港 駅 に 行く

大丈夫そ。

MeCab + ipadic で '関西国際空港', '駅', 'に', '行き', 'ます’分かち書きされ、 そこで未知語になった「関西国際空港」が WordPiece により分割されて '関西', '##国際空港', '駅', 'に', '行き', 'ます’ になるという理解です。 ## は、 WordPiece で分割する前にはこの前の単語にくっついてたよ〜の印っぽい。

# bert2.py 
from transformers import BertJapaneseTokenizer
tknz = BertJapaneseTokenizer.from_pretrained("cl-tohoku/bert-base-japanese")
print(tknz.tokenize("関西国際空港駅に行きます"))
print(tknz.encode("関西国際空港駅に行きます"))
print(tknz.decode(tknz.encode("関西国際空港駅に行きます")))

#$ python bert2.py 
#['関西', '##国際空港', '駅', 'に', '行き', 'ます']
#[2, 3013, 23783, 235, 7, 2609, 2610, 3]
#[CLS] 関西国際空港 駅 に 行き ます [SEP]

ははん、未知語は WordPiece によって分割されても、decode すると ## が消えて元に戻るんやね…

怠け者の根性なしではなかったっぽい(精神ソフトウェア不具合対応記録)

この記事は7月上旬〜8月上旬に下書きしたものを少し編集し、8月下旬に投稿したものです。

 6月末に文字通り泣きながらこの一連のツイートを書き、自分にとって自分の不得意な部分の何が問題なのか改めて考え、カウンセリングルームと精神科に行き、服薬と検査を開始することができた。

これはホンマに文章として出力するのに勇気がいる話なんですけど、修士課程の途中で「ア、ぼく研究者向いてないな」と気づいて諦めたつもりだったのに、研究への未練タラタラでそれを考えるたびに文字通り涙出てくるの、そろそろどうにかせなアカンと思っていて。

「怠け者で根性がないどうしようもない奴だ」ではなかったのだととても安心した。どうにかうまくいくかもしれないと希望が持てるようになった。

目次

  • 目次
  • 主な不具合
    • 今考えてみると
    • いったんは諦めたつもり
  • 明らかになってきた経緯
  • ツイートしてから
  • 不具合対応の専門家に会う
    • カウンセリング
    • 精神科
  • 不具合まとめ
続きを読む

疑問に思ったことを聞いただけなのに、怒られがち

やっちまった。

どうやらこのダブルコンボに、それまでの話の流れを把握していなくて論点がずれているというのも加わって、トリプルコンボを踏んでしまったようだ。

反省している。実は、この手の怒られが発生したのは、一度や二度ではない。失敗を繰り返さないために、なにがどうなっているのかを考えるのが今回の記事である。

なお、ぼくは人が口から出す(あるいは体で表現する)「ことば」と、頭の中にある「考え」を分けて考えている。 怒らせるつもりがなかった、傷つけるつもりがなかったとしても、口から出たことばが問題なのであって、どういうつもりだったかは(他者には観測できないので)関係がない。 思いやりを持ちましょう、敬意を持ちましょうみたいな、そういう外から見えない部分にはあまり興味がない。思想・良心の自由は人間の権利だからだ(憲法でも保障されている)。

  • はじめに:人間がわかりたい
  • 仮説:2つの文化
  • 対応策:謝って、敵意がないことを示す
  • 今後の課題
    • あちら側の文化の話
      • 指摘や疑問を否定だと捉えるのは、どうしてなのか
      • 論点をずらす、重箱の隅をつつく
    • 殴り合いというメタファー
    • 謝罪とその周辺
  • おわりに
続きを読む

はじめての語用論 第1章〜第3章

文脈や場面なしにことばを理解することはできないのではないかと思い、語用論に興味を持ち、教科書を読んでいます。端的にいうと空気が読めたい。

章末の練習問題の解答を書いてみます。問題を解くと、さらさらっと読んだだけではあまり理解できていなかった部分を読み直せて考えられるので良いですね。正解かどうかは自信がないので、これ違うんじゃない? というのがあったら、ぜひ教えてください(あるいは議論しましょう)。

  • 第1章 語用論とは何か
    • 練習問題
      • 問1
      • 問2
  • 第2章 グライス語用論
    • 練習問題
      • 問1
      • 問2
  • 第3章 情報語用論
    • 練習問題
      • 問1
      • 問2
続きを読む

麻辣燙(麻辣湯、まーらーたん)を食べましょう

昨年末に初めて食べてから、麻辣燙が好きです。

ぼくはまだ日本でしか食べたことがないので、日本のお店の話です。

どんな食べ物?

中華圏で人気の、具沢山で麺入りの辛いスープです。お店によって香辛料たっぷり薬膳だったり、博多とんこつラーメンっぽい白いスープだったりします。

特徴は、具材と麺が選べること。辛さも調節できます。

全部スープの色が違いますね。

どうやって注文するの?

お店に入ると、大きい冷蔵庫があります。まずは荷物を置いて、具材を選びましょう。

このお店では、具材が1人分ずつ串に刺さっていました。

こちらのお店では、具材は1人分ずつ小さいポリ袋に入っていました。衛生的。

冷蔵庫の横辺りに、トレイかボウルかカゴのような容器が置いてあるので、食べたい食材を入れていきます。肉、魚介、野菜、卵、豆製品などなど。5種類も選べば充分でしょう。いっぱい食べたい人はいっぱい入れよう。

選び終わったら、店員さんに渡します。すると、麺の種類を聞かれるので、答えます(インスタント麺が王道らしいですが、つるつる麺や春雨もあります。個人的には太めの春雨が好きです)。辛さも選びます。

店員さんが厨房で調理して、どんぶりで提供されます。いただきます。

お値段は、具材が1本100円とか、1袋何円とか、そんなかんじで、麺とスープの基本料金もかかるので、具材5種類+麺+スープで1000円程度になるのではないでしょうか。

中国語分からないんですけど?

お店により、店員さんにより、日本語のレベルはさまざまです。日本語母語話者の場合もあるし、カタコトだったり、全部中国語だったり。日本語非母語話者の店員さんが話すのがぶっきらぼうに聞こえるかも知れないですが、怒っているわけではなく普通なので心配しないで。

英語より日本語のほうが通じる可能性が高いと思います。店員さんも日本で暮らしている人なので、最低限の日本語はできるはず。

全部中国語の店でも、具材は実物を見れば大体何か分かるし、指差したり身振り手振りで話したりすればなんとかなります。ちょっとした旅行の気分です。数字や挨拶だけでも聞き取れれば、より気楽になるでしょう。

不安な場合は、日本人がやっている店を選べば安心かも知れません。

どこにあるの?

東京なら池袋や西川口、大阪なら難波・日本橋にお店があります。ほかの地域にもあるかも。 詳しくは Google Map で検索しましょう。

以下、ぼくが行ったお店です。

王道鸭脖 池袋店(池袋)

このお店は、麻辣燙以外に火鍋などもありました。

無限麻辣湯 食べる薬膳スープ(日本橋

オタロードのすぐ近くです。

メニューも店員さんも日本語でした。初心者にやさしいセットメニューもあります。香辛料たっぷりで体に良さそうな薬膳スープ。

張亮麻辣燙 3号店(難波)

メニューも店員さんもお客さんも中国語で、旅行の気分。茶葉蛋(お茶味の煮卵)が味しみしみで美味しかった。

今後もあちこち行ってみたいと思います。

食べましょう

おひとり様で行って気楽に食べるも良し、複数人で行ってワイワイ食べるも良し。

なかなか海外旅行に行けそうにない今、ちょっとでも旅行気分を味わうのにぴったりではないでしょうか!!!

開発記録 200311 Wed (データフレーム欠損位置を他のデータフレームにも反映)

自主課題(ということにしておきましょう。オシゴトでポチョポチョやっている開発で、どうするかなあと考えていて、家に帰って作った)。

課題

2つのデータフレームを比較したい。2つのデータフレームの行と列は共通だが、1つのデータフレームには欠損値が含まれている。欠損値があると比較できないので、前処理のひとつとして、片方の欠損値の位置をもう片方に反映する。

実行結果

raw_df1 は欠損のないデータフレームで、raw_df2 は欠損のあるデータフレーム。欠損値 Nan は、位置 (1, 2), (2, 1), (3, 0) にある。

df1 は欠損位置を反映した後のデータフレーム。位置 (1, 2), (2, 1), (3, 0) が Nan になっている。 なお、df2raw_df2 のまま。

$ python mask_nan_pandas.py 
raw_df1:
      0    1    2
0  1.0  2.0  3.0
1  1.0  2.0  3.0
2  1.0  2.0  3.0
3  1.0  2.0  3.0 

raw_df2:
      0    1    2
0  2.0  4.0  6.0
1  2.0  4.0  NaN
2  2.0  NaN  6.0
3  NaN  4.0  6.0 

--------------------
df1:
      0    1    2
0  1.0  2.0  3.0
1  1.0  2.0  NaN
2  1.0  NaN  3.0
3  NaN  2.0  3.0 

df2:
      0    1    2
0  2.0  4.0  6.0
1  2.0  4.0  NaN
2  2.0  NaN  6.0
3  NaN  4.0  6.0 

メモ:欠損値 np.nan かどうかを判定する

欠損値同士を == で比較しても False となってしまうため、欠損値かどうかを判定するには np.isnan()math.isnan() を使う。

👉 NumPyの配列ndarrayの欠損値np.nanを他の値に置換 | note.nkmk.me

メモ:df.itertuples() で行ごとに読む

参考 👉 pandas.DataFrameのforループ処理(イテレーション) | note.nkmk.me

for i, row in enumerate(df.itertuples()):
        print(row)

 ↓

Pandas(Index=0, _1=2.0, _2=4.0, _3=6.0)
Pandas(Index=1, _1=2.0, _2=4.0, _3=nan)
Pandas(Index=2, _1=2.0, _2=nan, _3=6.0)
Pandas(Index=3, _1=nan, _2=4.0, _3=6.0)

コード

もっとスマートでシンプルなやり方があるんじゃないかと思いつつ、分からなくてこうなった。

# mask_nan_pandas.py
import numpy as np
import pandas as pd


# 生のデータフレームを作る
def make_df():
    df1 = pd.DataFrame({\
        '0':[1, 1, 1, 1],\
        '1':[2, 2, 2, 2],\
        '2':[3, 3, 3, 3]\
    }, dtype=np.float32)

    df2 = pd.DataFrame({\
        '0':[2, 2, 2, np.nan],\
        '1':[4, 4, np.nan, 4],\
        '2':[6, np.nan, 6, 6]\
    }, dtype=np.float32)

    return df1, df2


# データフレーム内の欠損値を探し、リストで返す
def get_iloc_nan(df):
    iloc_nan_list = []
    for i, row in enumerate(df.itertuples()):
        for j, value in enumerate(row[1:]):
            if np.isnan(value):
                iloc_nan_list.append((i, j))
    return iloc_nan_list


# 位置を指定して NaN で置き換える
def mask(df1, df2):
    for locat in get_iloc_nan(df2):
        df1.iloc[locat[0], locat[1]] = np.nan
    return df1, df2


if __name__ == '__main__':
    raw_df1, raw_df2 = make_df()
    print('raw_df1:\n', raw_df1, '\n')
    print('raw_df2:\n', raw_df2, '\n')
    print('-'*20)
    df1, df2 = mask(raw_df1, raw_df2)
    print('df1:\n', df1, '\n')
    print('df2:\n', df2, '\n')