u++の備忘録

第85回R勉強会@東京 #TokyoR にて "R言語で「言語処理100本ノック 2020」" の題目で発表しました

第85回R勉強会@東京 #TokyoR にて、LT発表しました。2019年1月開催の第75回以来*1、久々の参加でした。

tokyor.connpass.com

発表の題目は "R言語で「言語処理100本ノック 2020」" で、4月に取り組んでいた「言語処理100本ノック 2020」の紹介*2R言語でのデモという内容でした。

発表資料

デモ用のKaggle Notebook

www.kaggle.com

YouTube動画

発表の様子を再現したYouTube動画も公開しています。

Jupyter Notebook の CSS 要素を編集する

Jupyter Notebook の CSS 要素を編集する方法に関するTipsです。IPython.core.displayを用いて、次のようにCSS要素を編集できます。

from IPython.core.display import display, HTML
display(HTML("<style>.cm-s-ipython span.cm-comment { color: red; }</style>"))

CSSを当てるClass名については、ブラウザの開発者ツールなどで確認できます。

f:id:upura:20200516005241p:plain

Kaggle Notebook も公開しています。編集ページと公開ページでClass名が異なるので、公開ページでは色が編集されていません。

www.kaggle.com

公開ページでも色を変えたい場合は、公開ページのClass名の要素を変更する次のコードを実行しておくと良いでしょう。

display(HTML("<style>.highlight .c1 { color: red; }</style>"))

www.kaggle.com

「BERT応用勉強会」参加録 #xpaperchallenge

「BERT応用勉強会」にオンライン参加しました。簡単な発表概要と個人的な所感をメモしておきます。発表動画のアーカイブは、YouTube後日公開されるそうですました。slidoとYouTubeコメントでの質疑応答はSpreadsheetにまとめてみました。

nlpaper-challenge.connpass.com

医療言語処理へのBERTの応用 --BioBERT, ClinicalBERT, そして--

発表資料

概要

  • BERTの登場が医療言語処理に与えた影響について、放射線科専攻医&NLPを専攻する大学院生の視点からサーベイ
  • 医療ドメインに特化したBERTが存在
    • 適用事例として、固有表現抽出+病名正規化で、辞書ベースからのf1スコアの大幅な改善
    • その他に質問応答、要約、再入院予測、カルテの固有表現抽出+関係認識、含意関係認識など
  • BERTを非言語医療データへ応用する「BeHRt」
    • 各受診時の診断と年齢を[SEP]区切りで入力することで診断の埋め込み表現を獲得
  • 言語データと非言語医療データの統合
    • 言語データ:医療記録をBioBERT+BiGRUで要約して獲得したベクトル化
    • 非言語医療データ:ICD、薬剤、処理のコードをWord2vecでベクトル化
  • 東京大学の医療AI開発学講座が日本語診療記録で事前学習したBERTを公開

所感

  • BERTの応用として医療ドメインに寄せていく中で、非言語情報を取り込もうとしている流れはとても興味深い
  • 特定のドメインの中で重要視される非言語情報は存在するはずで、BERTに使える形に落とし込むアイディアは参考にできそう

Multilingual BERTの二言語領域適応に基づく対訳文同定

概要

  • NLP2020でも発表したBERTを用いた研究の報告
  • ニューラル機械翻訳に必要な質・量ともに優れた対訳文を、BERTを用いて獲得する
    • ケーススタディとして、日米の特許文書データを利用
    • 対訳文同定の具体的な手順については、論文の図1が分かりやすい
  • Wikipediaで事前学習済のモデルを特許データで再学習する領域適用の工夫をしている
  • さらに対訳文の分類器をfine-tuning

所感

  • 実際にNLPに取り組む際にはデータセットの準備が大きな課題となるため、意義深いタスク
  • タスクを解くための問題設計の工夫が面白かった
  • 質疑応答の中でも言及されたが、教師あり学習の要素を排除できると、より活用しやすくなると感じた
    • 教師あり学習の部分がどの程度性能に貢献するかの評価が気になった

BERTのMulti Modalタスクへの活用

発表資料

概要

  • マルチモーダルな領域でのBERTについてのサーベイ
    • 今回はVision+Languageに限定
  • 各モーダルのEncode方法、事前学習の方法などの観点で、さまざまなモデルをまとめている
    • VilBERT
    • LXMERT
    • VL-BERT
    • Unicoder-VL
    • UNITER

所感

  • 「なんかいっぱいモデル出てるなあ」くらいの理解だったモデル群についてサラッと学べて良かった
  • 個人的には、いまKaggleでマルチモーダルなコンペが開催された場合に、どんなwinner solutionになるかは気になる
    • 2019年3月のコンペではモーダル別に特徴量を抽出&結合して勾配ブースティング決定木に突っ込むのが主流だった

BERTをブラウザで動かしたい!―MobileBERTとTensorFlow.js―

発表資料

概要

  • TensorFlow.jsの「MobileBERT」のQ&Aモデルの紹介
    • MobileBERT: モバイル端末向けに、汎用性と精度を保ちつつ軽量化・高速化したBERT
  • 蒸留を用いたBERT-LARGEからの学習方法やアーキテクチャの工夫点、組み込み方や実例などをまとめている

所感

  • ブラウザで動くウェブアプリケーションが手軽に作成できそうで、TensorFlow.jsが提供するNLPモデルには注目している(日本語のモデルの登場にも期待したい)
  • 性能の高さを大量のハイパーパラメータで追求する流れとともに、蒸留などを用いた軽量化の流れも実用の面から間違いなく捨て置けないので、今後の動向を押さえておきたい

テキスト生成の評価 × BERT

発表資料

概要

  • テキスト生成タスクの評価にBERTを利用する方法についてのサーベイ
  • 意味での類似度や単語の一致度など、いろいろな評価観点が存在
    • 現状は自動評価に加えて人手評価も実行する場合が多いが、コストの高さや評価の保証に課題がある
    • 既存の著名な「BLEU」「ROUGE」などの評価指標は、意味的な違いを取れていない、タスク依存するなどの問題がある
  • BERTを駆使した評価手法が提案されている
    • BERTScore: BERTの出力で類似度を測る
    • MoverScore: 「Word Mover's Distance」をBERT出力に適用
      • BERTScoreでは最も類似度の高い単語だけを参照しているが、MoverScoreではどのように分布を移動させればよいかを計算する
    • BLEURT: BERTをテキスト生成評価用に事前学習&fine tuning

所感

  • BERTScoreの論文はidfでの重み付けなどの工夫や広範な実験が網羅されている印象
  • 文書をBERTでベクトル化してコサイン類似度で近傍を探索するような簡易的な検索システムを構築する上でも参考になると感じた(腰を据えて読み込みたい)
  • BERTScoreはpip installできるらしいので、いろいろ遊んでみたい

おわりに

今回、YouTubeライブで参加しました。初のオンライン開催ながら「Cluster」+YouTubeライブという、なかなか野心的な挑戦だったと思います。素敵な勉強会をありがとうございました。

f:id:upura:20200515191009p:plain

企業名認識のデータセット「JCLdic」で学習したEncoder-Decoderモデル

TISが公開している企業名認識のためのデータセット「JCLdic」*1を用いて、Encoder-Decoderモデルを学習させてみました。

結果と考察

学習・検証に利用していないデータに対して適応した結果を下図に示します。統計的な出現頻度に基づくので当然な気がしますが①「ヤ」→「ャ」に修正②「有限会社」を明示しない場合は「株式会社」を付与ーーしています。

f:id:upura:20200505232127p:plain

Encoder-Decoderモデルを用いた正規化は、クックパッドのブログ*2を読んで以来、試してみたいと考えていました。

今回は簡単のため「JCLdic」をそのまま活用しましたが「株式会社」を前に付けるか後に付けるかなどは、統計的に処理するのは不可能なタスクなように感じます。学習前のtgt側のデータから「株式会社」「有限会社」などを削除しておくことで、会社名部分のみの正規化というタスクに変換する方が理にかなっていそうです。

実装

実装には「OpenNMT-py」*3を利用しました。学習に利用したNotebookなどは、GitHub*4で公開しています。

言語処理100本ノック 2020「49. 名詞間の係り受けパスの抽出」

問題文

nlp100.github.io

問題の概要

問題文に提示された仕様に従って出力します。第5章は2015年版と同様なので、先駆者のコード*1を流用しつつ実装しました。

class Morph:
    def __init__(self, dc):
        self.surface = dc['surface']
        self.base = dc['base']
        self.pos = dc['pos']
        self.pos1 = dc['pos1']


class Chunk:
    def __init__(self, morphs, dst):
        self.morphs = morphs    # 形態素(Morphオブジェクト)のリスト
        self.dst = dst          # 係り先文節インデックス番号
        self.srcs = []          # 係り元文節インデックス番号のリスト


def parse_cabocha(block):
    def check_create_chunk(tmp):
        if len(tmp) > 0:
            c = Chunk(tmp, dst)
            res.append(c)
            tmp = []
        return tmp

    res = []
    tmp = []
    dst = None
    for line in block.split('\n'):
        if line == '':
            tmp = check_create_chunk(tmp)
        elif line[0] == '*':
            dst = line.split(' ')[2].rstrip('D')
            tmp = check_create_chunk(tmp)
        else:
            (surface, attr) = line.split('\t')
            attr = attr.split(',')
            lineDict = {
                'surface': surface,
                'base': attr[6],
                'pos': attr[0],
                'pos1': attr[1]
            }
            tmp.append(Morph(lineDict))

    for i, r in enumerate(res):
        res[int(r.dst)].srcs.append(i)
    return res


def convert(s):
    pl, nl = [], [c for c in s if '名詞' in [m.pos for m in c.morphs]]
    for i in range(len(nl) - 1):
        st1 = [''.join([m.surface if m.pos != '名詞' else 'X' for m in nl[i].morphs])]
        for e in nl[i + 1:]:
            dst, p = nl[i].dst, []
            st2 = [''.join([m.surface if m.pos != '名詞' else 'Y' for m in e.morphs])]
            while int(dst) != -1 and dst != s.index(e):
                p.append(s[int(dst)])
                dst = s[int(dst)].dst
            if len(p) < 1 or p[-1].dst != -1:
                mid = [''.join([m.surface for m in c.morphs if m.pos != '記号']) for c in p]
                pl.append(st1 + mid + ['Y'])
            else:
                mid, dst = [], e.dst
                while not s[int(dst)] in p:
                    mid.append(''.join([m.surface for m in s[int(dst)].morphs if m.pos != '記号']))
                    dst = s[int(dst)].dst
                ed = [''.join([m.surface for m in s[int(dst)].morphs if m.pos != '記号'])]
                pl.append([st1, st2 + mid, ed])
    return pl


filename = 'ch05/ai.ja.txt.cabocha'
with open(filename, mode='rt', encoding='utf-8') as f:
    blocks = f.read().split('EOS\n')
blocks = list(filter(lambda x: x != '', blocks))
blocks = [parse_cabocha(block) for block in blocks]

for b in blocks:
    pl = (convert(b))
    for p in pl:
        if isinstance(p[0], str):
            print(' -> '.join(p))
        else:
            print(p[0][0], ' -> '.join(p[1]), p[2][0], sep=' | ')

言語処理100本ノック 2020「48. 名詞から根へのパスの抽出」

問題文

nlp100.github.io

問題の概要

問題文に提示された仕様に従って出力します。

class Morph:
    def __init__(self, dc):
        self.surface = dc['surface']
        self.base = dc['base']
        self.pos = dc['pos']
        self.pos1 = dc['pos1']


class Chunk:
    def __init__(self, morphs, dst):
        self.morphs = morphs    # 形態素(Morphオブジェクト)のリスト
        self.dst = dst          # 係り先文節インデックス番号
        self.srcs = []          # 係り元文節インデックス番号のリスト


def parse_cabocha(block):
    def check_create_chunk(tmp):
        if len(tmp) > 0:
            c = Chunk(tmp, dst)
            res.append(c)
            tmp = []
        return tmp

    res = []
    tmp = []
    dst = None
    for line in block.split('\n'):
        if line == '':
            tmp = check_create_chunk(tmp)
        elif line[0] == '*':
            dst = line.split(' ')[2].rstrip('D')
            tmp = check_create_chunk(tmp)
        else:
            (surface, attr) = line.split('\t')
            attr = attr.split(',')
            lineDict = {
                'surface': surface,
                'base': attr[6],
                'pos': attr[0],
                'pos1': attr[1]
            }
            tmp.append(Morph(lineDict))

    for i, r in enumerate(res):
        res[int(r.dst)].srcs.append(i)
    return res


filename = 'ch05/ai.ja.txt.cabocha'
with open(filename, mode='rt', encoding='utf-8') as f:
    blocks = f.read().split('EOS\n')
blocks = list(filter(lambda x: x != '', blocks))
blocks = [parse_cabocha(block) for block in blocks]

for b in blocks:
    for m in b:
        text = []
        if '名詞' in [s.pos for s in m.morphs] and int(m.dst) != -1:
            current_chunk = m
            text.append(''.join([m.surface for m in current_chunk.morphs]))
            next_chunk = b[int(current_chunk.dst)]
            while int(current_chunk.dst) != -1:
                text.append(''.join([m.surface for m in next_chunk.morphs]))
                current_chunk = next_chunk
                next_chunk = b[int(next_chunk.dst)]
            print(*text, sep=' -> ')