この前はBERTを使って含意関係認識をやってみました。
前回は何も考えずにとにかくBERTに突っ込んで、とりあえずやってみたって感じでした。 今回は、もう少し泥臭い方法で含意関係認識をやってみたいと思います。
今回参考にさせていただいたのはこちら。
こちらの記事を参考にやっていきたいと思います。
LIghtGBM
実は前にも使ったことあるんですが、今回はLightGBMを使ってみたいと思います。
ここ数年、主にKaggleを始めとするデータサイエンスコンペでよく目にするライブラリです。 私もあまり詳しくないのですが、他の勾配ブースティング系のライブラリに比べて高速に学習ができるのが特徴のようです。
今回はこちらを使用して含意関係認識をやってみたいと思います。
いろいろ準備
データセット
今回も含意関係認識なので、京都大学の黒橋・褚・村脇研究室で公開しているTextual Entailment評価データを使用します。
このデータセットなかったら、ここ最近の記事全部書けてなかったです。本当にありがとうございます。
特徴量
NLP系の特徴量については、あまり詳しくなかったので、今回はこちらの文献を参考に特徴量を作っていきたいと思います。
https://www.unisys.co.jp/tec_info/ik15po000006di7t-att/12702.pdf
こちらの文献では特徴量を、
- 数量/時間表現一致
- 固有表現一致
- 内容語一致率
- 内容語の先頭文字の一致率
- word2vecコサイン距離
- 排他語の有無
- 内容語不一致率
としています。
今回はこちらを参考にしつつ、簡易的な特徴量を作成してそれをLightGBMへ突っ込んでみたいと思います。
やってみる
特徴量エンジニアリング
なにはともあれ、特徴量を作っていきたいと思います。
数量/時間表現一致
この辺りは、GiNZAを使えば簡単に取得できます。
抽出用のクラスはこんな感じになりました。
import pandas as pd import ginza import spacy from spacy import displacy class FeatureNumber: def __init__(self): super().__init__() self.nlp = spacy.load('ja_ginza') def get_quantity(self, text: str): """数量の抽出""" doc = self.nlp(text) result = [] word = "" for sent in doc.sents: for token in sent: if token._.ne.endswith(("QUANTITY", "TIME")): # 節にまとめる if token._.ne.startswith("B_") and word != "": # 新規の数詞のとき result.append(word) word = "" word = word + token.orth_ if word != "": result.append(word) return result def get(self, primary_text: str, secondary_text: str) -> float: """数値の矛盾が存在するか検知する""" t1 = self.get_quantity(primary_text) t2 = self.get_quantity(secondary_text) f1 = 0.9 for word in t2: if word not in t1: f1 = 0.1 return f1
固有表現一致
ちょっと参考元の記事とはずれますが、GiNZAで固有表現としてとれたものすべて抽出してみたいと思います。
import pandas as pd import ginza import spacy from spacy import displacy class FeatureNamedEntity: def __init__(self): super().__init__() self.nlp = spacy.load('ja_ginza') def get_named_entity(self, text: str): """固有表現の抽出""" doc = self.nlp(text) result = [] word = "" for sent in doc.sents: for token in sent: if token._.ne != "": # 何らかの固有表現だったら # 節にまとめる if token._.ne.startswith("B_") and word != "": # 新規の数詞のとき result.append(word) word = "" word = word + token.orth_ if word != "": result.append(word) return result def get(self, primary_text: str, secondary_text: str) -> float: """同じ固有表現が存在するか判定する""" t1 = self.get_named_entity(primary_text) t2 = self.get_named_entity(secondary_text) f2 = 0.9 for word in t2: if word not in t1: f2 = 0.1 return f2
内容語一致率
こちらは、2つの文の間で共通の単語を見つけます。 参考元の文献では、wikipediaを元に関連語を推測してそこまで判定しているようですが、今回は簡易的に完全一致の単語にしぼりたいと思います。
import pandas as pd import ginza import spacy from spacy import displacy class FeatureIntersectionWords: def __init__(self): super().__init__() self.hinsi = ["NOUN", "VERB", "ADJ", "ADV"] self.nlp = spacy.load('ja_ginza') def get_words(self, text: str): """内容語の抽出""" doc = self.nlp(text) result = [] for sent in doc.sents: for token in sent: if token._.ne == "" and token.pos_ in self.hinsi: result.append(token.lemma_) return result def get(self, primary_text: str, secondary_text: str) -> float: """内容語の一致率を取得する""" t1 = self.get_words(primary_text) t2 = self.get_words(secondary_text) count = 0 for word in t2: if word not in t1: count += 1 if len(t2) != 0: f3 = count / len(t2) else : f3 = 0 return f3
内容語の先頭文字の一致率
内容語の先頭文字の一致率も特徴量とします。
import pandas as pd import ginza import spacy from spacy import displacy class FeatureInitialCharactor: def __init__(self): super().__init__() self.hinsi = ["NOUN", "VERB", "ADJ", "ADV"] self.nlp = spacy.load('ja_ginza') def get_words(self, text: str): """内容語の先頭文字の抽出""" doc = self.nlp(text) result = [] for sent in doc.sents: for token in sent: if token._.ne == "" and token.pos_ in self.hinsi: result.append(token.orth_[0]) return result def get(self, primary_text: str, secondary_text: str) -> float: """内容語の先頭文字の一致率を取得する""" t1 = self.get_words(primary_text) t2 = self.get_words(secondary_text) count = 0 for word in t2: if word not in t1: count += 1 if len(t2) != 0: f4 = count / len(t2) else: f4 = 0 return f4
word2vecコサイン距離
こちらはGiNZAに付属している学習済みのword2vecのベクトルを使用して、単語の数だけ平均します。
import pandas as pd import ginza import spacy from spacy import displacy import math class FeatureW2V: def __init__(self): super().__init__() self.hinsi = ["NOUN", "VERB", "ADJ", "ADV"] self.nlp = spacy.load('ja_ginza') def get_words(self, text: str): """内容語の抽出""" doc = self.nlp(text) result = [] for sent in doc.sents: for token in sent: if token._.ne == "" and token.pos_ in self.hinsi: result.append(token.lemma_) return result def get(self, primary_text: str, secondary_text: str) -> float: """W2Vのコサイン距離の最大値の平均を取得する""" t1 = self.get_words(primary_text) t2 = self.get_words(secondary_text) l_similarity = [] for w2 in t2: w_t2= self.nlp(w2) similarity = 0 for w1 in t1: w_t1= self.nlp(w1) if similarity < w_t1.similarity(w_t2): similarity = w_t1.similarity(w_t2) l_similarity.append(similarity) if len(l_similarity) != 0: f5 = sum(l_similarity) / len(l_similarity) else: f5 = 1 return f5
排他語の有無
元の文献では排他語を日本語 WordNetを使って判定しています。 ちょっとめんどくさかったので、こちらは省略します。
内容語不一致率
内容語の一致率とは反対に、不一致率についても計算します。
import pandas as pd import ginza import spacy from spacy import displacy class FeatureNonIntersectionWords: def __init__(self): super().__init__() self.hinsi = ["NOUN", "VERB", "ADJ", "ADV"] self.nlp = spacy.load('ja_ginza') def get_words(self, text: str): """内容語の抽出""" doc = self.nlp(text) result = [] for sent in doc.sents: for token in sent: if token._.ne == "" and token.pos_ in self.hinsi: result.append(token.lemma_) return result def get(self, primary_text: str, secondary_text: str) -> float: """内容語の不一致率を取得する""" t1 = self.get_words(primary_text) t2 = self.get_words(secondary_text) count = 0 for word in t2: if word not in t1: count += 1 if len(t2) != 0: f7 = len(t2) - count / len(t2) else : f7 = 1 return f7
LightGBMへ突っ込む
最後に作った特徴量をLightGBMへ突っ込んでいきます。
import lightgbm as lgb import pandas as pd class LightGBMClassifer: def __init__(self): # LightGBMのハイパーパラメータを設定 self.lgb = lgb.LGBMClassifier() def train(self, X_train, y_train, X_valid, y_valid): train_data = lgb.Dataset(X_train, label=y_train) validation_data = lgb.Dataset(X_valid, label=y_valid, reference=train_data) params = { 'metric': 'gbdt', # GBDTを指定 'objective': 'binary', # 多クラス分類を指定 'metric': "binary_logloss", # 多クラス分類の損失(誤差) 'learning_rate': 0.05, # 学習率 'num_leaves': 21, # ノードの数 'min_data_in_leaf': 3, # 決定木ノードの最小データ数 'early_stopping_rounds':100, # 'num_iteration': 1000} # 予測器(決定木)の数:イタレーション self.model = lgb.train(params, train_set=train_data, valid_sets=validation_data, num_boost_round=10000, early_stopping_rounds=100, verbose_eval=50) return self.model
結果
結果としては精度はこんなもんになりました。
precision recall f1-score support 0 0.3529 0.1348 0.1951 89 1 0.6402 0.8616 0.7346 159 accuracy 0.6008 248 macro avg 0.4966 0.4982 0.4649 248 weighted avg 0.5371 0.6008 0.5410 248
まあ、こんなもんなんでしょう。 いかに前回のBERTの精度が良かったかがわかる結果になりました。
使ったコード
今回使った残骸はこちら。
今回は諸事情によりMetaflowを使ってコードを書いていますので、その点はご注意ください。
参考にさせていただいた記事
この記事を書くにあたって、下記の記事を参考にさせていただきました。
感想
前回は、過去にこのタスクでSOTAをこともあるBERTをいきなり使ったので、出てきた結果がどれだけすごいのかわかりませんでしたが、今回やってみて改めてただBERTに突っ込むだけでかなり良いスコアが出ることがわかりました。
実際には、BERTを始めとするNN系のアルゴリズムとLightGBMなどの木系のアルゴリズムはアンサンブルするとさらに効果が上がることも期待されるので、組み合わせて使うのがこの先やることになるのかなと思います。