始める前に
Pythonで使える形態素解析ライブラリには、Mecab,Janome,SudachiPy,GiNZAなどありますが、Mecabを使用して実装していきます。
第四章
30. 動詞
import MeCab
def get_verb(text):
# MeCab Taggerオブジェクトを作成
mecab = MeCab.Tagger()
nodes = mecab.parseToNode(text)
tokens=[]
while nodes:
#品詞などの詳細情報
features=nodes.feature.split(',')
if nodes.surface != "" and features[0] == '動詞':
#動詞のみ単語を格納
tokens.append(nodes.surface)
nodes=nodes.next
return tokens
tokens=get_verb(text)
print(tokens)
形態素解析をするために、MeCabをインポートし、MeCab.Tagger()でオブジェクトを生成します。
nodes=parseToNode(text)は与えられた文を解析し、Nodeオブジェクトを返します。このNodeオブジェクトは、解析した一つの単語を指し、品詞などの詳細情報はnodes.feature・単語自体を見るにはnodes.surfaceを使います。
この時、featuresは文字列になっているので、区切り文字を使い区切り、features[0]にある品詞を取得します。
そして、nodesは先頭から順にnodes=nodes.nextで解析した文を走査することができます。
これらを使い、動詞のみをtokensに格納し、出力します。
31. 動詞の原型
def get_base_form_verb(text):
# MeCab Taggerオブジェクトを作成
mecab = MeCab.Tagger()
nodes = mecab.parseToNode(text)
tokens=[]
while nodes:
#品詞などの詳細情報
features=nodes.feature.split(',')
#動詞のみ
if nodes.surface != "" and features[0] == '動詞':
#動詞の基本形を格納
tokens.append(features[6] if len(features) > 7 else nodes.surface)
nodes=nodes.next
return tokens
tokens=get_base_form_verb(text)
print(tokens)
動詞を基本形にするには、features[6]で取得することができます。
もしこのインデックスがなかったらエラーになるため、len(features)>7で条件分岐させます。
32. 「AのB」
def get_compound_noun(text):
mecab = MeCab.Tagger()
nodes = mecab.parseToNode(text)
tokens = []
while nodes:
features = nodes.feature.split(',')
#(単語、品詞)を格納
tokens.append((nodes.surface , features[0]))
nodes = nodes.next
return tokens
tokens=get_compound_noun(text)
for i in range(len(tokens)-2):
if tokens[i][1]=="名詞" and tokens[i+1][0] == "の" and tokens[i+2][1]=="名詞":
print(tokens[i][0],tokens[i+2][0])
nodesのままだとリストのようにindexアクセスができないため、条件付けが少し難しくなります。
そのため、一旦(単語、品詞)をリストに格納し、最後にif文で出力します。
33. 係り受け解析
#33
import spacy
# 日本語モデルをロード
nlp = spacy.load("ja_core_news_sm")
# 解析
doc = nlp(text)
# 係り受け関係を出力(タブ区切り)
for token in doc:
if token.head.text != token.text:
print(f"{token.text}\t{token.head.text}")
構文解析を行うために、spaCyという自然言語処理ライブラリを使用します。
日本語の構文解析を行うために、日本語モデル(ja_core_news_sm)をロードします。
次に、for token in doc: のようにして文から各トークン(単語)を順に取り出し、token.head(係り先) が自分自身 と同じ場合除外します。token.text で単語(トークン)の文字列を、token.head.text でその単語が係っている先の単語を取得できます。
34. 主述の関係
#34
import spacy
# 日本語モデルをロード
nlp = spacy.load("ja_core_news_sm")
# 解析
doc = nlp(text)
# 係り受け関係を出力(タブ区切り)
for token in doc:
if token.text == "メロス" and token.dep_ == "nsubj":
print(f"{token.head.text}")
token.dep_ は構文上の依存関係を文字列で取得できます。今回はメロスが主語であるという条件が必要なため、token.dep_ == “nsubj(名詞主語)”とします。
このラベリングについては以下の論文でわかりやすくまとめられていました。
35. 係り受け木
#35
from spacy import displacy
# 日本語モデルの読み込み
nlp = spacy.load("ja_core_news_sm")
doc = nlp("メロスは激怒した。")
# 係り受け木を描画
displacy.render(doc, style="dep", jupyter=True)
係り受け木を描画するには、displacyをインポートします。
displacyには、nlpで作ったオブジェクトを渡すことで係り関係を視覚化します。
また、style="dep"は文の係り受け構造(依存関係)を矢印付きで可視化する指定です。jupyter=TrueはJupyter NotebookやColab上で結果を直接表示する設定で、HTML文字列ではなく図として描画されまた。
36. 単語の出現頻度
from collections import Counter
import json
import re
def clean_text(value):
#強調リンクの除去
value = re.sub(r"'{2,5}", "", value)
# 内部リンクの除去 [[リンク先|表示文字]] → 表示文字
value = re.sub(r'\[\[(?!ファイル:)(?:[^\]]*\|)?([^\]]+)\]\]', r'\1', value)
# 外部リンクの除去 [URL 表示文字] → 表示文字
value = re.sub(r'\[http[^\s]*\s?([^\]]*)\]', r'\1', value)
# ファイルリンクの除去 [[File:xxx|...]] または [[ファイル:xxx|...]] → ファイル名
value = re.sub(r'\[\[(?:File|ファイル):([^\]|]+)(?:[^\]]*)\]\]', r'\1', value)
# HTMLタグの除去 <ref>...</ref>, <br />, <small>...</small>
value = re.sub(r'<.*?>', '', value)
# {{lang|...}} の簡易除去(テンプレート内言語マークアップ)
value = re.sub(r'{{.*?}}', '', value)
return value
def tokenizer(text):
# MeCab Taggerオブジェクトを作成
mecab = MeCab.Tagger()
nodes = mecab.parseToNode(text)
tokens=[]
while nodes:
tokens.append(nodes.surface)
nodes=nodes.next
return tokens
def get_token_array(line):
data=json.loads(line)
text=clean_text(data["text"])
text=tokenizer(text)
return text
with open("jawiki-country.json","r", encoding="utf-8") as f:
tokens=[]
for line in f:
tokens+=get_token_array(line)
token_dict=Counter(tokens)
print(token_dict.most_common(20))
第三章のコードを使って、マークアップを除去するclean_text関数を実装しています。そして、json化、マークアップ除去、トークン取得をget_token_array関数にまとめました。
配列の要素の頻度を辞書としてまとめるには、Counterを使います。このCounterに先ほど作った配列を渡し、most_common(20)で出現頻度が高い20個を出力します。
37. 名詞の出現頻度
#37
def get_noun(text):
# MeCab Taggerオブジェクトを作成
mecab = MeCab.Tagger()
nodes = mecab.parseToNode(text)
tokens=[]
while nodes:
features = nodes.feature.split(',')
if features[0] == "名詞":
tokens.append(nodes.surface)
nodes=nodes.next
return tokens
def get_noun_array(line):
data=json.loads(line)
text=clean_text(data["text"])
text=get_noun(text)
return text
with open("jawiki-country.json","r", encoding="utf-8") as f:
tokens=[]
for line in f:
tokens+=get_noun_array(line)
token_dict=Counter(tokens)
print(token_dict.most_common(20))
37と違うのは、名詞の頻出度に着目する点です。つまり、トークン化する関数を名詞のみ取得できるトークン関数に変更します。
今回では、30を参考にfeatures[0] == “名詞”にして、トークンを取り出します。
38. TF・IDF
from sklearn.feature_extraction.text import TfidfVectorizer
from collections import defaultdict,Counter
import pandas as pd
import math
def get_df_tfidf(docn_freq, jn_freq, doc_cnt, jn_cnt):
data = [] # 各単語の結果を入れるリスト
for noun, tf in jn_freq.items():
idf = math.log(doc_cnt / docn_freq[noun])
tf_norm = tf / jn_cnt
tfidf = tf_norm * idf
data.append({
'単語': noun,
'TF': tf_norm,
'IDF': idf,
'TF-IDF': tfidf
})
df = pd.DataFrame(data)
# TF-IDFの高い順に並べ替え(任意)
df = df.sort_values(by='TF-IDF', ascending=False).reset_index(drop=True)
return df
mecab = MeCab.Tagger()
japan_cnt=0
total_docs = 0
doc_freq =defaultdict(int)
japan_noun_freq =Counter()
with open("jawiki-country.json","r", encoding="utf-8") as f:
for line in f:
total_docs += 1
data = json.loads(line)
# マークアップを除去
text = clean_text(data["text"])
node = mecab.parseToNode(text)
doc_nouns = set()
while node:
if node.feature.split(",")[0] == "名詞":
noun = node.surface
doc_nouns.add(noun)
# 日本に関する記事の場合、出現頻度をカウント
if data["title"] == "日本":
japan_cnt+=1
japan_noun_freq[noun] += 1
node = node.next
# 文書頻度を更新
for noun in doc_nouns:
doc_freq[noun] += 1
df= get_df_tfidf(doc_freq,japan_noun_freq,total_docs,japan_cnt)
print(df.head(20))
tf-idfについては下記に簡単にまとめましたので、確認したい方はご参考にどうぞ。
今回の問題を解くに当たって、問題に2つの解釈があると思います。一つ目は、全記事のtf-idfから日本の記事の名詞のスコアを出す。2つ目は日本における記事だけでtf-idfを求める。
この問題では、前者に基づいて問題を解いていきます。(文章一個の単位を一つの国とします。)
tf-idfを計算するために、日本での名詞数japan_cnt、日本での名詞の割合japan_noun_freq 、文章(国)の数total_docs 、全文章中での名詞が含まれる頻度doc_freqを用意し、with open内で実装しています。
実際のtf-idfの計算は、get_df_tfidf関数内でjapan_noun_freqをイテレーションし、tf,idf,tf-idfを計算します。
そして、pandasでデータを扱うためにも、dataという配列にタプルで格納します。
tf-idfとは
$$
\mathrm{tf}(t, d) = \frac{\text{文章d中の単語tの出現回数}}{\text{文章dの総単語数}}
$$
$$
\mathrm{idf}(t) = \log \frac{\text{全文章数 } N}{\text{単語 } t \text{ が含まれる文章数 } }
$$
tf・idfの役割を簡単に言うと、
tfが「文章に占める単語の割合」、idfが「ある単語が含まれる文章の確率(の逆数)」です。
これらの積が文章d中の単語のスコアとなります。
特に、注意したいのが、tfは特定の文章d中に着目し、idfは文章全体に着目します。これは、用意したコーパス内では、idfは共通のものとして利用しますが、tfは文章ごとに異なる値になっているので、実装時には気を付けましょう。
39. Zipfの法則
from sklearn.feature_extraction.text import TfidfVectorizer
from collections import defaultdict,Counter
import pandas as pd
import math
mecab = MeCab.Tagger()
word_freq =Counter()
with open("jawiki-country.json","r", encoding="utf-8") as f:
for line in f:
data = json.loads(line)
# マークアップを除去
text = clean_text(data["text"])
node = mecab.parseToNode(text)
while node:
# if node.feature.split(",")[0] == "名詞":
noun = node.surface
word_freq[noun] += 1
node = node.next
data=[]
for noun,value in word_freq.items():
data.append({
'word': noun,
'freq': value
})
df = pd.DataFrame(data)
df = df.sort_values(by='freq', ascending=False).reset_index(drop=True)
df.index = df.index + 1
import numpy as np
import matplotlib.pyplot as plt
"""
対数グラフ
"""
x = df.index
y = df.freq
plt.plot(x, y)
ax = plt.gca()
ax.set_yscale('log') # y軸をlogスケールで描く
ax.set_xscale('log') # x軸をlogスケールで描く
plt.title('コーパスにおける単語の出現頻度順位')
plt.xlabel('X',fontsize=18)
plt.ylabel('Y',fontsize=18)
plt.show()
Zipfの法則とは、「単語の出現頻度と順位の間に一定の法則がある」という言語統計の経験則です。これを39では両対数グラスにプロットし、確かめていきます。
pandasまでは、38と同様に単語の頻度を数え上げて、データを格納していきます。
両対数グラフをプロットするには、matplotlibを使用します。
先ほど取得した順位と出現頻度をそれぞれx,yに入れて、plt.plotで作図します。そして、両対数グラフにするために、ax = plt.gca()で描画に関する細かい設定を行います。
ここから、ax.set_yscale(‘log’) 、ax.set_xscale(‘log’) を使いx、y軸に対して対数化をすることで簡単に両対数グラフを出力することができるのです。
まとめ
形態素解析や構文解析、tf-idfと自然言語処理の基礎となる部分を問題を通して身につけることができたと思います。
これらの知識を復習と発展を繰り返し、次回の「第5章: 大規模言語モデル」に挑みましょう!

コメント