のんびりしているエンジニアの日記

ソフトウェアなどのエンジニア的な何かを書きます。

日本語自然言語処理のData Augmentation

Sponsored Links

皆さんこんにちは
お元気ですか。週末の発表資料も作っています。

本記事は自然言語処理アドベントカレンダー第13日です。

qiita.com

画像処理ではデータをアフィン変換などで変形して、
画像を拡張する処理(=Data Augmentation)が知られています。
変換などをかけて過学習を防ぐ役割が1つとしてあります。

今回は、自然言語処理でもData Augmentationを実現したいと思います。

テキストのData Augmentationの意義

自分でデータセットを作る場合に、全てを網羅して1つ1つの単語が変わった場合を
作り上げるのは非常に困難で、時間がかかります。

そのため、一定の問題ない範囲(=結果が変わらない範囲)で
データを拡張する試みをすることで精度やロバストな学習ができる可能性があります。

WordNetの説明

Wikipediaより

WordNet(わーどねっと)は英語の概念辞書(意味辞書)である。WordNetでは英単語がsynsetと呼ばれる同義語のグループに分類され、簡単な定義や、他の同義語のグループとの関係が記述されている。 WordNetの目的は直感的に使うことのできる辞書とシソーラスが組み合わされた成果物を作ること、および自動的文書解析や人工知能のアプリケーションの実現を支援することにある。WordNetのデータベースやソフトウェアはBSDライセンスによって公開され、自由にダウンロードして用いることができる。データベースはオンラインで参照することもできる。

WordNetは日本語にもあり、国立研究開発法人情報通信研究機構NICT)が開発を進めています。
単語と単語の関係性、例えば、類義語や上位語などがこのWordNetからわかります。

類義語は病気と風邪、上位語の関係が牛乳と乳製品のような関係になります。

WordNetの環境を構築する。

まずは、WordNet(日本語)をダウンロードします。
http://nlpwww.nict.go.jp/wn-ja/

ダウンロードしたファイルを展開すると「wnjpn.db」が展開されるので
このファイルを使ってWordNetの中身を確認します。

WordNetを使ってデータを拡大する。

さて、ここからが本題です。WordNetを使ってデータセットを人工的に生成します。

文書拡張アルゴリズムの試み

まずはサンプルを用意します。元々のボットを拡張用に少し修正を加えている文章です。

そんな、とてもきつい風邪だから仕方ないよ。
名詞の類義語置換

まずは形態素解析で区切り、解析することを目指します。
そこで得られた単語をベースに他の単語で置換します。

そんな、とてもきつい風邪だから仕方ないよ。
そんな	連体詞,*,*,*,*,*,そんな,ソンナ,ソンナ
、	記号,読点,*,*,*,*,、,、,、
とても	副詞,助詞類接続,*,*,*,*,とても,トテモ,トテモ
きつい	形容詞,自立,*,*,形容詞・アウオ段,基本形,きつい,キツイ,キツイ
風邪	名詞,一般,*,*,*,*,風邪,カゼ,カゼ
だ	助動詞,*,*,*,特殊・ダ,基本形,だ,ダ,ダ
から	助詞,接続助詞,*,*,*,*,から,カラ,カラ
仕方	名詞,ナイ形容詞語幹,*,*,*,*,仕方,シカタ,シカタ
ない	助動詞,*,*,*,特殊・ナイ,基本形,ない,ナイ,ナイ
よ	助詞,終助詞,*,*,*,*,よ,ヨ,ヨ
。	記号,句点,*,*,*,*,。,。,。
EOS
類義語

上記データを類義語を使って置換します。
ただ、単純に全ての類義語を検索することに意味はなさそうなので絞ります。
名詞は非常に有効そうなので、名詞に絞込み別の単語に置き換えます。

WordNetから類義語を取得します。類義語は以下のコードを用いれば取得可能です。
wordテーブルからwordのidを取得します。
そして、次にsenseからwordが所属するsynset(類義語のネットワーク)を検索します。
最後に、senseから同じネットワークに存在する単語を取得し、類義語一覧を獲得しています。

def get_synonym_word(word):
    cur = conn.execute("select * from word where lemma=?", (word,))
    word_list = [row for row in cur]
    synonym_list = []
    for word in word_list:
        cur = conn.execute("select * from sense where wordid=?", (word[0],))
        synnet_list = [row for row in cur]
        for synnet in synnet_list:
            cur = conn.execute("select * from sense, word where synset = ? and word.lang = 'jpn' and sense.wordid = word.wordid;", (synnet[0],))
            synonym_list += [row for row in cur]
    return synonym_list

最初に取り上げた文章には「病気」の名詞があります。
その類義語は次のようになります。

感冒
風邪ひき
風邪
咳気
上位語への置換

WordNetは上位の言葉への言葉も登録されているため、その言葉を使う置換方式も可能そうに見えます。
上位語は次の方式で検索できます。因みに病気だと、'病み煩い', '疾病', '杜撰'が検出されます。難しい。
こちらの内容を参考にし、実装しています。

A frontend of WordNet-Ja database file (sqlite3 format) which is available on http://nlpwww.nict.go.jp/wn-ja/ · GitHub

from collections import namedtuple

Word = namedtuple('Word', 'wordid lang lemma pron pos')

def getWords(lemma):
  words = []
  cur = conn.execute("select * from word where lemma=?", (lemma,))
  row = cur.fetchone()
  while row:
    words.append(Word(*row))
    row = cur.fetchone()
  return words

def getWord(wordid):
  cur = conn.execute("select * from word where wordid=?", (wordid,))
  return Word(*cur.fetchone())
 
Sense = namedtuple('Sense', 'synset wordid lang rank lexid freq src')

def getSenses(word):
  senses = []
  cur = conn.execute("select * from sense where wordid=?", (word.wordid,))
  row = cur.fetchone()
  while row:
    senses.append(Sense(*row))
    row = cur.fetchone()
  return senses

def getSense(synset, lang='jpn'):
  cur = conn.execute("select * from sense where synset=? and lang=?",
      (synset,lang))
  row = cur.fetchone()
  if row:
    return Sense(*row)
  else:
    return None

SynLink = namedtuple('SynLink', 'synset1 synset2 link src')

def getSynLinks(sense, link):
  synLinks = []
  cur = conn.execute("select * from synlink where synset1=? and link=?", (sense.synset, link))
  row = cur.fetchone()
  while row:
    synLinks.append(SynLink(*row))
    row = cur.fetchone()
  return synLinks

def abstract_word(lemma):
    result = []
    for word in getWords(lemma):
        for sense in getSenses(word):
            if sense.src != 'hand': continue
            for synlink in getSynLinks(sense, 'hype'):
                abst_sense = getSense(synlink.synset2)
                if abst_sense and word.wordid != abst_sense.wordid:
                    result.append(getWord(abst_sense.wordid).lemma)
    return result
副詞・形容詞の削除

副詞と形容詞を削除します。
副詞と形容詞は強調する表現になるため削除してもあまり意味はなさそうです。
文章内だと、「とても」と「きつい」になります。

http://www.anlp.jp/proceedings/annual_meeting/2017/pdf_dir/P10-4.pdf

※自動的に付け加える方法は要検討ですね。

実際にどの程度データが増えるのかやってみた。

さて、実際に作ります。コードとしての手順は次の通りです。

  1. 形態素解析を行う。
  2. 品詞を抽出し適切な処理を行う。(これまでの処理)
  3. 全てのパタンを列挙する。

これらを使ったDataAugmentationのコードは次の通りです。

def data_augmentation_for_text(sentence):
    sentence_list_origin = []
    nodes = word_and_class(sentence)
    get_word(nodes,sentence_list=sentence_list_origin)
    return sentence_list_origin

def word_and_class(doc):
    doc_ex = doc
    doc_ex = doc
    print (doc_ex)

    # Execute class analysis
    
    result = tagger.parseToNode(doc_ex)

    # Extract word and class
    word_class = []
    while result:
        word = result.surface
        clazz = result.feature.split(',')[0]
        if clazz != u'BOS/EOS':
            word_class.append((word, clazz))
        result = result.next

    return word_class

def get_word(nodes, index=0,sentence="", sentence_list=[]):
    if len(nodes) == index:
        sentence_list.append(sentence)
        return None
    
    next_index = index + 1
    if nodes[index][1] == "副詞" or nodes[index][1] == "形容詞":
        get_word(nodes, index=next_index, sentence=sentence, sentence_list=sentence_list)
        get_word(nodes, index=next_index, sentence=sentence + nodes[index][0], sentence_list=sentence_list)
    
    elif nodes[index][1] == "名詞":
        candidate_words = get_synonym(nodes[index][0] )
        candidate_words += abstract_word(nodes[index][0] )
        get_word(nodes, next_index, sentence + nodes[index][0], sentence_list=sentence_list)
        for candidate_word in candidate_words:
            get_word(nodes, next_index, sentence + candidate_word, sentence_list=sentence_list)
    else:
        get_word(nodes, next_index, sentence + nodes[index][0], sentence_list=sentence_list)

結果

そんな、とてもきつい風邪だから仕方ないよ。
そんな、風邪だから仕方ないよ。
そんな、風邪だから流儀ないよ。
そんな、風邪だから遣り口ないよ。
そんな、風邪だから手ないよ。
そんな、風邪だから仕様ないよ。
そんな、風邪だから筆法ないよ。
そんな、風邪だからスタイルないよ。
そんな、風邪だから仕樣ないよ。
そんな、風邪だから様式ないよ。
そんな、風邪だからやり口ないよ。

なんか意味変わっていないか!?といった部分もありますね。

あなたのことが好き
あなたのことが好き
あなたのことが好み
あなたのことがお好
あなたのことが御好み
あなたのことがお好み
あなたのことが御好
あなたのことが好き
あなたのことが温かさ
あなたのことが慈しみ

少し近い気がします。何か日本語的にピンとこない内容もありますが、
若干誤字っぽければこうなるのでしょうか。

考察

意味が変わっている単語があり、もう少し検討の余地はありそうです。
もう少し単語の形を捉えることが必要でしょうか。