12. 自然言語処理にむけて

画像認識は、もともと画像データが多次元ベクトルで表現されており、 よく似た画像が近いベクトルで表現されるなど、機械学習で処理しやすい前提が整っていました。 一方、自然言語などのテキストは、文字コードが近くても意味が近いわけではありません。 自然言語を機械学習で処理するためには、 テキストの特徴量を多次元ベクトルでうまく表現することが鍵になります。

40206df03c794bb2ba478b121fc4904e

最後のまとめとして、 自然言語処理を機械学習で扱う方法を考えていきましょう。

モジュールの準備

[1]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
try:
    import japanize_matplotlib #matplotlibの日本語化
except ModuleNotFoundError:
    !pip install japanize_matplotlib
    import japanize_matplotlib
sns.set(font="IPAexGothic") #日本語フォント設定

12.1. 形態素解析

言語における意味の基本単位は語 (word) です。まず、語を取り出す方法からみていきましょう。

12.1.1. 英語と日本語

自然言語処理は、言語の種類によって難しさや扱い方が異なります。

  • (英語文) I bought a book

  • (日本語文) 私は本を買った

英語は、空白で区切られたものを語と考えることができます。 したがって、Python の標準文字列ライブラリだけで、簡単に語を取り出すことができます。

英語の字句解析

[2]:
s = "I bought a book"
s.split()
[2]:
['I', 'bought', 'a', 'book']

日本語では、まず語の区切りを判定する必要があります。 しかし、この語の区切りを判定するのがかなりの難処理です。

形態素解析ツール(ライブラリ)

日本語文から、語の区切りを決定するツール(ライブラリ)

12.1.2. spaCy/GINZA

spaCy は、Explosion AI 社の開発するオープンソースの自然言語処理ライブラリです。2019 年に、 リクルート AI 研究所と国立言語研究所の研究成果である GiNZA が登場し、実用的な日本語処理が手軽に利用できるようになりました。

まずは、GiNZA の導入から始めましょう。pip install ginza を入力するだけで、 形態素解析も含め、自然言語処理に必要なライブラリがまとめてインストールされます。

!pip install ginza
import pkg_resources, imp
imp.reload(pkg_resources)

SpaCy/GINZAは、各種自然言語処理をパイプライン化したツールになっています。パイプラインを調整することで、最終的な処理を切り替えることができます。

単純に形態素解析したいときは、次の通り、使います。

[3]:
import spacy
nlp = spacy.load('ja_ginza')

doc = nlp("私は本を買った") #形態素解析
for word in doc:
    print(word.i, word.orth_, word.lemma_, word.pos_, word.tag_)
0 私 私 PRON 代名詞
1 は は ADP 助詞-係助詞
2 本 本 NOUN 名詞-普通名詞-一般
3 を を ADP 助詞-格助詞
4 買っ 買う VERB 動詞-一般
5 た た AUX 助動詞

プロパティ

情報

orth_

入力語

lemma_

原型

pos_

品詞(Part of Speech)

tag_

品詞タグ

vector

単語ベクトル

日本語文を単語単位に分割する関数wakachi(s)を定義しておきましょう。

[4]:
def wakachi(s):
    doc = nlp(s)
    return [word.lemma_ for word in doc]  # word.lemma_ は標準形

print(wakachi('私は本を買った'))

['私', 'は', '本', 'を', '買う', 'た']

(時間があったら)Let’s try

自然言語処理と形態素解析の良い練習問題は、「自然言語処理100本ノック」にあります。

http://www.cl.ecei.tohoku.ac.jp/nlp100/

Web上には、解説記事がたくさん掲載されていますので、参考にしながら解いてみると実力がつきます。

12.2. アンケート分析

不動産屋による「まちづくりに関するアンケート」に基づいて、アンケート解析を試していきましょう。 アンケードには、アンケートの回答日、コメント自由記述形式、満足度(5段階評価: 1 不満 - 5 満足)が記載されています。

データの取り寄せ

本データは、下山らによる「Python 実践データ分析 100 本ノック」から講義用に編集したものを利用します。

!wget https://KuramitsuLab.github.io/data/survey.csv
[5]:
data = pd.read_csv("survey.csv")
data.head() #最初の5行を表示
[5]:
日付 コメント 満足度
0 2019/3/11 駅前に若者が集まっている 1
1 2019/2/25 スポーツできる場所があるのが良い 5
2 2019/2/18 子育て支援が嬉しい 5
3 2019/4/9 保育園に入れる(待機児童なし) 4
4 2019/1/6 駅前商店街が寂しい 2

12.2.1. コメントを眺める

今まで、様々なデータを扱ってきましたが、 今回のデータは、自由記述形式のテキストが入っているのが特徴です。

まず、アンケート中のコメントの分量を把握してみましょう。 文字数を数えて、新しいカラム(文字数)を作って格納します。

[8]:
data["文字数"] = data["コメント"].str.len()
data.head()
[8]:
日付 コメント 満足度 文字数
0 2019/3/11 駅前に若者が集まっている 1 12
1 2019/2/25 スポーツできる場所があるのが良い 5 16
2 2019/2/18 子育て支援が嬉しい 5 9
3 2019/4/9 保育園に入れる(待機児童なし) 4 15
4 2019/1/6 駅前商店街が寂しい 2 9
[7]:
plt.hist(data["文字数"])
[7]:
(array([12., 24., 22., 10.,  6.,  6.,  5.,  0.,  0.,  1.]),
 array([ 4. ,  8.6, 13.2, 17.8, 22.4, 27. , 31.6, 36.2, 40.8, 45.4, 50. ]),
 <BarContainer object of 10 artists>)
_images/ds12nlp_15_1.svg

以上に長いコメントがありますね。

コメントの長さ順に並べ変えてみる

[9]:
data.sort_values(by="文字数")
[9]:
日付 コメント 満足度 文字数
41 2019/2/25 特になし 3 4
69 2019/1/4 特になし 2 4
25 2019/1/21 道が綺麗 4 4
18 2019/3/15 夜道が暗い 1 5
19 2019/2/20 ゴミ処理が楽 4 6
... ... ... ... ...
62 2019/3/19 アンケートをちゃんと確認して街づくりに反映してくれている姿勢が良い 5 33
56 2019/4/13 歩行者用信号が変わるのが早い。老人や子供の事を考えて設定してほしい 2 33
44 2019/2/28 信号のない交差点が近くにあり事故が起きそうで怖い、信号を付けて欲しい。 1 35
39 2019/3/11 変なおじさんに声を掛けられた事がある。警察の巡回をもっと強化してほしい 1 35
43 2019/3/11 最近川の氾濫被害が大きく取り扱われているが、この町ではどのような氾濫防止を取っているか説明し... 3 50

86 rows × 4 columns

12.2.2. 単語レベルの解析

コメントから単語を抽出して、どのような単語が使われているか調べてみましょう。

  • 標準形変換: 活用のある単語(例. 「買った」)は、買うのように標準形に変換する

  • ストップワード除外: 解析の精度を上げるために不要な記号や単語を取り除く

ここでは、動詞、形容詞、名詞だけに着目してみます。

[10]:
words = []
for text in data["コメント"]:
    doc = nlp(text)
    for word in doc:
        # 動詞(VERB), 名詞(NOUN), 形容詞(ADJ)のみ抽出
        if word.pos_ == 'VERB' or word.pos_ == 'NOUN' or word.pos_ == 'ADJ':
            words.append(word.lemma_)
print(len(words))
print(words[:30]) #先頭30語だけ
375
['駅前', '若者', '集まる', 'スポーツ', '場所', 'ある', '良い', '子育て', '支援', '嬉しい', '保育園', '入れる', '待機児童', 'なし', '駅前', '商店街', '寂しい', '生活', '便利', '遊ぶ', '場所', 'ない', '遊ぶ', '場所', 'ない', '商業', '施設', '出来る', '欲しい', '病院']

これで、コメント文の中で用いられている名詞と動詞をすべて取り出すことができました。 しかし、まだどれの単語が重要なのかわかりません。各単語の出現頻度を調べてみましょう。

[11]:
pd.DataFrame({"words":words}).value_counts()

[11]:
words
欲しい      15
ほしい      14
少ない       7
駅前        7
良い        6
         ..
大丈夫       1
奇麗        1
姿勢        1
子ども       1
高齢者       1
Length: 228, dtype: int64

12.2.3. (寄り道)単語頻度の視覚化

ワードクラウドは、テキストデータを視覚的に表現する方法です。ちょっと寄り道をして表示してみましょう。

まず、日本語フォントをインストールしておきます。(これがないと文字化けします。)

Colab上でのIPA日本語フォントのインストール

!apt-get -y install fonts-ipafont-gothic
[18]:
## !pip install wordcloud

from wordcloud import WordCloud
fpath = '/usr/share/fonts/truetype/fonts-japanese-gothic.ttf'
#fpath = 'fonts-japanese-gothic.ttf'
word_chain = ' '.join(words)

model = WordCloud(width=800, height=600, background_color='white', colormap='bone', font_path=fpath)
W = model.generate(' '.join(words))

plt.imshow(W)
plt.axis('off')
plt.show()
_images/ds12nlp_23_0.svg

これで不動産の満足度に影響を与えているキーワードが見えてきました。しかし、まだどのキーワードがプラスの評価なのか、マイナスの評価なのかわかりません。

Let’s try

満足度の高いキーワードを抽出してみよう

12.2.4. 満足度の高いキーワード

今回のアンケート調査の素晴らしいことは、不動産の満足度が 5 段階評価で回答されている点です。 各キーワードとこの5段階評価を紐付けてみると、キーワードの満足度が見えて来るかもしれません。

コメント内の単語と満足度をペアにして取り出してみます。

[19]:
words = []
scores = []
for text, score in zip(data["コメント"], data["満足度"]):
    doc = nlp(text)
    for word in doc:
        if word.pos_ in ['VERB', 'NOUN', 'ADJ']:
            words.append(word.lemma_)
            scores.append(score)
print(words[:30])
print(scores[:30])
['駅前', '若者', '集まる', 'スポーツ', '場所', 'ある', '良い', '子育て', '支援', '嬉しい', '保育園', '入れる', '待機児童', 'なし', '駅前', '商店街', '寂しい', '生活', '便利', '遊ぶ', '場所', 'ない', '遊ぶ', '場所', 'ない', '商業', '施設', '出来る', '欲しい', '病院']
[1, 1, 1, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 2, 2, 2, 3, 3, 3, 3, 3, 2, 2, 2, 3, 3, 3, 3, 4]

Pandas を使って表データにまとめておきましょう。出現数1回のカラムを作って、groupbyしてみましょう。満足度は平均値をとることにします。

[20]:
keyword = pd.DataFrame({"キーワード": words, "満足度": scores, "出現数": [1]*len(words)})
keyword.groupby('キーワード').agg({'満足度': np.mean, '出現数': sum}).sort_values(by='出現数')
[20]:
満足度 出現数
キーワード
bbb 2.000000 1
暗い 1.000000 1
暮らせる 5.000000 1
最高 5.000000 1
有名 3.000000 1
... ... ...
良い 4.833333 6
少ない 1.142857 7
駅前 1.428571 7
ほしい 2.000000 14
欲しい 2.200000 15

228 rows × 2 columns

Let’s try

満足度の高いキーワードと満足度の低いキーワードのトップ 5を出してみよう

今回の分析は、出現頻度のあまりに低い単語を除外した方が良いです。このように、データサイエ ンティスト(分析者)のセンスで、結果は少し変わります。

極性辞書とセンティメント解析

極性辞書は、ある単語が一般的にネガティブなのか、ポジティブなのかを、-1(ネガティブ)から1(ポジティブ)までのスコアの形で表現したものです。

センティメント分析などに便利です。(今回の満足を極性辞書と比較してみましょう。)

12.3. 文章のベクトル化

次は、いよいよ文章のベクトル化を考えていきます。

ポイントは、意味や内容が似ている文が近くなるようにベクトル化することです。 もともと、類似文章検索として研究されてきました技術になります。

まず、準備として、わかち書きされたカラムを作っておきましょう。

[21]:
data['わかち書き'] = data['コメント'].map(lambda x: ' '.join(wakachi(x)))
data.head()
[21]:
日付 コメント 満足度 文字数 わかち書き
0 2019/3/11 駅前に若者が集まっている 1 12 駅前 に 若者 が 集まる て いる
1 2019/2/25 スポーツできる場所があるのが良い 5 16 スポーツ できる 場所 が ある の が 良い
2 2019/2/18 子育て支援が嬉しい 5 9 子育て 支援 が 嬉しい
3 2019/4/9 保育園に入れる(待機児童なし) 4 15 保育園 に 入れる ( 待機児童 なし )
4 2019/1/6 駅前商店街が寂しい 2 9 駅前 商店街 が 寂しい

12.3.1. BOW

BOW(Bag of Words) は最も古典的な文書の特徴量を捉えてベクトル化する手法です。 出現する単語の個数を \(N\) とすると、各コメント文は出現した単語には 1 を入れた \(N\) 次元のベクトルで表現します。

BOW のポイントは、文章の構造は全て無視し、「どの単語が含まれているか」だけに注目している点です。そして、一旦、コメント文をベクトルで表現できれば、コサイン類似度 (cosine similarity)を用いて、類似度を求めることができます。

BOW の原理は、難しくありません。sklearnモジュールのCountVectorizerを使って、楽に BOW を求めることができます。ただし、sklearn は、英語圏で開発されたライブラリなので、入力文は英単語のように空白で区 切られているという前提になっています。日本語は、形態素解析を使って前処理して、テキストを空白区切りの形式に変換しておく必要があります。

[22]:
from sklearn.feature_extraction.text import CountVectorizer
docs = np.array(data['わかち書き'])
model = CountVectorizer()
bags = model.fit_transform(docs)

print(bags.toarray())
[[0 0 0 ... 0 0 0]
 [0 0 1 ... 0 0 0]
 [0 0 0 ... 0 0 0]
 ...
 [0 0 0 ... 0 0 0]
 [0 0 0 ... 0 0 0]
 [0 0 0 ... 0 0 0]]

Pandasで表データとマージして、コメントがどのようにベクトル化されたかみてみましょう。

[23]:
pd.DataFrame(bags.toarray(),columns=model.get_feature_names(),index=data['コメント']).head()
[23]:
bbb あまり ある いる おじ お祭り から くる くれる けど ... 隣町 集まる 電話 頻繁 駅前 駐車場 駐輪場 高い 高速道路 高齢者
コメント
駅前に若者が集まっている 0 0 0 1 0 0 0 0 0 0 ... 0 1 0 0 1 0 0 0 0 0
スポーツできる場所があるのが良い 0 0 1 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
子育て支援が嬉しい 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
保育園に入れる(待機児童なし) 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
駅前商店街が寂しい 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 1 0 0 0 0 0

5 rows × 237 columns

BOWは、単語の並びを無視しています。語順を無視すると重要な情報が飛んでしまいそうですが、不思議なことに類似文書検索では、十分精度がでます。

12.3.2. コサイン類似度

コサイン類似度は、文書ベクトルの類似度を測る尺度としてよく使われます。 ベクトルの向きがどの程度同じ方向を向いているか?という指標で、\(-1\)\(1\)の範囲をとります。

コサイン類似度を数式で記述すると以下のようになります。

\[cos(\mathbf{x}, \mathbf{y}) = \frac{\mathbf{x} \cdot \mathbf{y}}{|\mathbf{x}| \cdot|\mathbf{y}|} = \frac{\sum_{i=1}^{|V|} x_i y_i}{\sqrt{\sum_{i=1}^{|V|} x_i^2} \cdot \sqrt{\sum_{i=1}^{|V|} y_i^2}}\]

なんかそろそろNumPyの方が読みやすくなってきましたね

[25]:
def cosine_similarity(x, y):
    return np.dot(x, y)/(np.sqrt(np.dot(x, x))*np.sqrt(np.dot(y, y)))

X = np.array([0.7, 0.5, 0.3,0.1])
Y = np.array([0.8, 0.5, 0.2, 0.222])
print(cosine_similarity(X, Y))

0.9837126278799047

もちろん、skleranモジュールにもコサイン類似度のライブラリは含まれています。 こちらは、ユニバーサル関数バージョンになっているので気をつけましょう。

[33]:
from sklearn.metrics.pairwise import cosine_similarity
X = np.array([[0.7,0.5,0.3,0.1], [0.1,0.2,0.9,0.9]])
Y = np.array([[0.8, 0.5, 0.2, 0.2], [0.1,0.2,0.9,0.9]])

print(cosine_similarity(X, Y))
[[0.98597181 0.44748449]
 [0.42427716 1.        ]]

実際に、「スポーツできる場所があるのが良い(index=1)」と類似しているコメントを探してみましょう。

[37]:
def print_sim(index):
    vec = bags.toarray()
    similarity = cosine_similarity(vec[index:index+1], vec)[0]
    top10 = np.argsort(similarity)[::-1][:10]
    for i in top10:
        print(similarity[i], data['コメント'][i])
print_sim(1)
1.0 スポーツできる場所があるのが良い
0.39999999999999997 ランニングとか運動できる場所が多い
0.3162277660168379 サイクリングコースが良い
0.25819888974716115 都内へのアクセスが良い
0.25819888974716115 遊ぶ場所がない
0.22360679774997896 市長が若くて活気がある
0.22360679774997896 消防団が活発で安心できる
0.19999999999999998 子どもが安全に遊ぶ場所がない
0.19999999999999998 歩道が広い道が多くて安心できる
0.19999999999999998 近くに公園があって住みやすい

12.3.3. TF/IDF

BOW は、単語の出現を見るだけで、重要度を考慮に入れていません。

TF-IDF(Term Frequency-Inverse Document Freequency: 単語頻度-逆文書頻度) は、 よくある一般的な単語と特徴のある重要な単語の区別をつける指標です。

単語 w が n 回現われるとき、TF(Term Frequence)

\[TF = \frac{n}{N}\]

単語 w を含む文が d 個あるとき、IDF(Inverse Document Frequency)

\[IDF = - \log{\frac{d}{D}} = \log{\frac{D}{d}}\]

TF-IDF は、\(TF\)\(IDF\) の積によって求まる。

\[\mbox{TF-IDF} = \frac{n}{N} \log{\frac{D}{d}}\]

IDF は一種の一般語フィルタとして働き、多くの文書に出現する語(一般的な語)は重要度が下が り、特定の文書にしか出現しない単語の重要度を上げる役割を果たします。

TF/IDF を用いることで、重要度の重みつけされたベクトルが得られます。(本当は、自分で計算してみましょうとしたいのですが、)sklearn のライブラリを用いてお手軽に計算してみます。

[38]:
from sklearn.feature_extraction.text import TfidfVectorizer
# tf-idf

vectorizer = TfidfVectorizer(max_df=0.9) #文書全体の90%以上で出現する単語は無視する
X = vectorizer.fit_transform(data['わかち書き'])
print('feature_names:', vectorizer.get_feature_names())
print('X:')
print(X.toarray())

feature_names: ['bbb', 'あまり', 'ある', 'いる', 'おじ', 'お祭り', 'から', 'くる', 'くれる', 'けど', 'この', 'ごみ', 'さん', 'すぎる', 'する', 'そう', 'たくさん', 'ちゃんと', 'つく', 'できる', 'とき', 'とても', 'どう', 'どのような', 'ない', 'なし', 'なる', 'なん', 'ひどい', 'ほしい', 'ます', 'まち', 'まで', 'もう', 'もっと', 'やすい', 'やめる', 'よう', 'よく', 'よる', 'られる', 'れる', 'アクセス', 'アンケート', 'ゴミ', 'サイクリングコース', 'サポート', 'スポット', 'スポーツ', 'スポーツジム', 'スーパー', 'デート', 'ナンバー', 'バス', 'ホームページ', 'マスコット', 'ママ', 'ランニング', 'リーズナブル', '下さる', '不便', '不安', '不正', '並木', '事件', '事故', '交差点', '他県', '付ける', '企業', '住む', '作る', '便利', '保育園', '信号', '備蓄', '働く', '充実', '先月', '入る', '入れる', '公共', '公園', '公害', '冬場', '凍結', '処理', '出る', '出張所', '出来る', '分かる', '利用', '利用料金', '助かる', '動物園', '反映', '取り扱う', '取る', '商店街', '商業', '喫茶店', '土日', '地域', '場所', '増やす', '変わる', '多い', '夜道', '夜間', '大きい', '大丈夫', '大変', '奇麗', '姿勢', '嬉しい', '子ども', '子供', '子育て', '安全', '安心', '家賃', '寂しい', '対応', '対策', '小学校', '少し', '少ない', '屋根', '巡回', '市長', '広い', '強化', '役所', '待機児童', '後押し', '心配', '怖い', '急行', '投稿', '担当者', '拡幅', '掃除', '掛ける', '支援', '料金', '施設', '早い', '映画館', '暗い', '暮らせる', '最近', '最高', '有名', '校庭', '桜並木', '欲しい', '止まる', '歩行者', '歩道', '氾濫', '活動', '活性化', '活気', '活発', '消防団', '渋滞', '災害', '無い', '無愛想', '特に', '状況', '狭い', '生活', '病院', '盛り上げる', '相談', '確認', '糞害', '細い', '経路', '綺麗', '繋がる', '老人', '考える', '職員', '自治体', '自然', '自転車', '良い', '芝生', '若い', '若者', '落ち葉', '行く', '行政', '街づくり', '街路樹', '街頭', '表示', '被害', '親身', '観光', '観光地', '設定', '詳細', '説明', '警察', '豊か', '走る', '起きる', '足りる', '路地', '路線', '路面', '近く', '追加', '遊び道具', '遊ぶ', '運動', '過ごす', '避難', '都内', '野良猫', '開く', '防止', '防犯', '降る', '隣町', '集まる', '電話', '頻繁', '駅前', '駐車場', '駐輪場', '高い', '高速道路', '高齢者']
X:
[[0.         0.         0.         ... 0.         0.         0.        ]
 [0.         0.         0.41652097 ... 0.         0.         0.        ]
 [0.         0.         0.         ... 0.         0.         0.        ]
 ...
 [0.         0.         0.         ... 0.         0.         0.        ]
 [0.         0.         0.         ... 0.         0.         0.        ]
 [0.         0.         0.         ... 0.         0.         0.        ]]
[39]:
pd.DataFrame(X.toarray(),columns=vectorizer.get_feature_names(),index=data['コメント']).head()
[39]:
bbb あまり ある いる おじ お祭り から くる くれる けど ... 隣町 集まる 電話 頻繁 駅前 駐車場 駐輪場 高い 高速道路 高齢者
コメント
駅前に若者が集まっている 0.0 0.0 0.000000 0.397231 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.580016 0.0 0.0 0.411545 0.0 0.0 0.0 0.0 0.0
スポーツできる場所があるのが良い 0.0 0.0 0.416521 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.000000 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0
子育て支援が嬉しい 0.0 0.0 0.000000 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.000000 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0
保育園に入れる(待機児童なし) 0.0 0.0 0.000000 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.000000 0.0 0.0 0.000000 0.0 0.0 0.0 0.0 0.0
駅前商店街が寂しい 0.0 0.0 0.000000 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.000000 0.0 0.0 0.490089 0.0 0.0 0.0 0.0 0.0

5 rows × 237 columns

Let’s try

TF/IDFの場合の類似コメントを探してみよう。

12.3.4. LSA

LSA(潜在的意味解析)では、 トピックという文書と単語の間に存在する抽象的な概念を導入し、各文書の BOW あるいは TF-IDF ベクトルを行とする文書数×単語数の行列を特異値分解(SVD)し、文書数×トピック数に次元削減します。

8次元に減らしてみます

[40]:
from sklearn.decomposition import TruncatedSVD
np.set_printoptions(suppress=True)
# SVD
svd = TruncatedSVD(n_components=8, n_iter=7, random_state=0)
svd.fit(X.toarray())
X = svd.transform(X.toarray())

[41]:
pd.DataFrame(X,index=data['コメント'])
[41]:
0 1 2 3 4 5 6 7
コメント
駅前に若者が集まっている 7.731770e-02 0.055635 0.221337 0.258149 -0.183296 -0.000305 0.136036 -0.064076
スポーツできる場所があるのが良い 9.653416e-02 0.232015 0.242801 -0.275236 0.224877 -0.002571 0.282702 0.372138
子育て支援が嬉しい 1.530323e-01 0.241557 -0.164930 0.098121 0.093775 -0.002967 -0.276363 0.062022
保育園に入れる(待機児童なし) 1.993400e-07 -0.000112 0.000043 0.000067 -0.000063 0.508183 -0.004800 0.000048
駅前商店街が寂しい 1.475882e-01 -0.066456 0.235868 0.196006 -0.265106 0.001276 -0.074943 0.076161
... ... ... ... ... ... ... ... ...
小学校の校庭が芝生なのでとても良い 3.357989e-02 0.109346 0.002445 -0.019058 0.079640 0.002153 0.071718 0.297933
ホームページからアンケートを投稿できるようにしてほしい 2.061109e-01 0.200964 -0.030146 -0.113776 -0.090526 -0.001596 0.202042 0.033840
公園に遊び道具が少なすぎる 1.735468e-01 -0.023439 0.093533 0.130378 -0.037278 0.001100 0.007060 0.216022
もっと公園を増やしてほしい 5.883562e-01 -0.202565 -0.184834 -0.231458 -0.253843 -0.003732 -0.064550 0.201424
駅前に駐車場が少ない、不便 1.226444e-01 0.017410 0.509561 0.516643 -0.314527 -0.003270 -0.066299 0.261483

86 rows × 8 columns

では、「スポーツできる場所があるのが良い(index=1)」と類似しているコメントをみてみましょう。

[44]:
def print_sim(index):
    similarity = cosine_similarity(X[index:index+1], X)[0]
    top10 = np.argsort(similarity)[::-1][:10]
    for i in top10:
        print(similarity[i], data['コメント'][i])
print_sim(1)
0.9999999999999998 スポーツできる場所があるのが良い
0.9704742874252414 ランニングとか運動できる場所が多い
0.9271749843766424 市長が若くて活気がある
0.8850136813887487 消防団が活発で安心できる
0.8794676152538797 歩道が広い道が多くて安心できる
0.8696169842692176 サイクリングコースが良い
0.8678878385469999 冬場、路面凍結で事故が多い、対応できませんか
0.8541713596444912 有名な企業が多い
0.836653242687093 隣町にできたごみ処理施設が心配、公害は大丈夫?
0.8337364715521739 都内へのアクセスが良い

参考資料: https://www.ogis-ri.co.jp/otc/hiroba/technical/similar-document-search/part1.html

12.4. 単語分散表現

文書中の単語出現数を元に文書ベクトルを紹介してきましたが、最後に単語の持つ意味的な情報を用いる手法として、単語分散表現(単語ベクトル)について紹介します。

単語分散表現では、単語を多次元空間上の座標にマッピングすることで、単語同士の類似度を比較したり、加減算したりすることができるようになります。

単語分散表現は、さまざまな方法で求められます。 しかし、2013年にGoogle研究所が発表した Word2Vecが有名です。 これは、「同じ文脈で登場する単語は似た意味を持つ」という分布仮説をベースにして、ニューラルネットワークで計算されます。

12.4.1. 単語ベクトル

GiNZAは、形態素解析したときに単語ベクトルがvoctorプロパティで付与されています。

単語ベクトルを確認してみる

[45]:
doc = nlp('スポーツ 良い')
print(doc[0].vector.shape, doc[0].vector) # 「スポーツ」の単語ベクトル
#print(doc[1].vector.shape, doc[1].vector) # 「良い」の単語ベクトル
(300,) [ 0.13375778 -0.17257443  0.02516939  0.1324365  -0.05213964  0.36968458
 -0.40936273  0.11759301 -0.01370322  0.16887937  0.25840095 -0.03895048
 -0.12300318 -0.16547137  0.14446233 -0.04904341 -0.38537306  0.2110346
 -0.22277047 -0.06400058 -0.21376547  0.0176675  -0.00410596 -0.01469426
 -0.02343989 -0.11121087  0.23225866  0.03399521  0.0003097  -0.17104886
 -0.05210022  0.04949599  0.24797468  0.25802103 -0.12333041  0.31917045
  0.17388728  0.2035169   0.06582559 -0.25228208  0.18981704  0.15296002
 -0.3062748  -0.13662805 -0.25415638  0.14236008  0.116825    0.00469154
 -0.05889941  0.07863956  0.4675573   0.06081422  0.2529209  -0.07001508
  0.34606618  0.2717276   0.25792852 -0.2984004  -0.07375812 -0.09873105
  0.14780869  0.0873313   0.10026332 -0.10008292 -0.3316018  -0.02629723
  0.5940275   0.30866838 -0.08813549 -0.32104307  0.03274322  0.10554635
 -0.30488187  0.377073   -0.0811692   0.20839754  0.38205898  0.02628802
  0.18617581  0.19931976  0.00599956  0.06431035  0.09854034  0.02887992
 -0.08011294 -0.03455874  0.08992771  0.06116451  0.19689974 -0.00615429
 -0.15785006 -0.01781378  0.00281635  0.34449962  0.09425399 -0.07273636
  0.1780002  -0.34851024  0.37838387 -0.13598311  0.2519176  -0.34066144
 -0.01075775 -0.14786543 -0.04548219 -0.02431995  0.10417668  0.11535702
 -0.09153163  0.01274134 -0.08331569 -0.07227844  0.10793906  0.01709241
 -0.33268264  0.03616779  0.18477437 -0.054786    0.11694523  0.2861517
 -0.1814767  -0.2492276  -0.3056511   0.10268316  0.02496759 -0.03885837
 -0.44996476 -0.13723636  0.02233196  0.01498503  0.19116491  0.02721791
 -0.09347356 -0.22418728  0.20053582 -0.02321318 -0.03963188  0.07721978
  0.3976757  -0.16255735 -0.2857601   0.01022803 -0.11282344 -0.03161858
  0.10372707 -0.04063642  0.13277282 -0.14350836  0.03758351  0.28988007
 -0.15129718 -0.07004785 -0.11414956  0.01055098 -0.2234786   0.20059532
 -0.10320012  0.06600995  0.10613239 -0.03008373 -0.34670264 -0.19242819
 -0.09134309 -0.03962347  0.30269867  0.24795845 -0.05786613 -0.08064044
  0.33049724 -0.35176003 -0.0745642  -0.14381081 -0.19127265 -0.2308729
  0.10868629  0.12795904  0.26682377  0.39642188  0.25173983  0.06555948
  0.08945418  0.28307387 -0.20416422  0.20722027 -0.35040206  0.11971842
 -0.22560261 -0.09891736  0.20061518 -0.01337475 -0.05774496  0.07784817
  0.0190405  -0.03751906 -0.27015632  0.02247851 -0.05720989  0.20762147
  0.27350608  0.13747151 -0.00919761  0.10923077 -0.33616212  0.08090062
  0.09826039  0.13366058 -0.08271509  0.3381954   0.13788582 -0.06729469
 -0.09151109  0.14015338 -0.14572589  0.29161888  0.02844842  0.07236236
 -0.033314   -0.16876915 -0.09799793 -0.01827359  0.31428993 -0.06787148
 -0.04007914  0.1423875   0.24909684  0.07624617 -0.31010824  0.24325818
 -0.04150698 -0.04732224  0.4020983  -0.13875155 -0.3239038  -0.08643212
  0.13847236 -0.09768677 -0.20165052 -0.27315557 -0.43414918  0.12716582
  0.40271717  0.11295483 -0.06921897 -0.02047522  0.05772358 -0.20754315
  0.27045983 -0.3403588  -0.28104421 -0.04287485  0.16783538 -0.05046352
 -0.02322021  0.80653805 -0.49147975  0.14961116  0.02781115 -0.13225512
 -0.05151397 -0.43092707 -0.01291704  0.200752    0.5579526  -0.02965733
 -0.2158913   0.04157935  0.33107662  0.19664446 -0.275344   -0.35029644
  0.15709329 -0.046059   -0.47471583 -0.05245955 -0.32224137  0.12703025
 -0.07285262 -0.40086585 -0.0947476   0.15882298  0.35492867 -0.2662322
 -0.26684946  0.06684791 -0.17897451 -0.06707234 -0.10908245  0.13751653
 -0.38477394 -0.21492186 -0.08163136 -0.12800853  0.42276528 -0.02782323
 -0.03355124  0.0600879  -0.08807893  0.10318359  0.11266886 -0.12545823]
(300,) [ 0.13375778 -0.17257443  0.02516939  0.1324365  -0.05213964  0.36968458
 -0.40936273  0.11759301 -0.01370322  0.16887937  0.25840095 -0.03895048
 -0.12300318 -0.16547137  0.14446233 -0.04904341 -0.38537306  0.2110346
 -0.22277047 -0.06400058 -0.21376547  0.0176675  -0.00410596 -0.01469426
 -0.02343989 -0.11121087  0.23225866  0.03399521  0.0003097  -0.17104886
 -0.05210022  0.04949599  0.24797468  0.25802103 -0.12333041  0.31917045
  0.17388728  0.2035169   0.06582559 -0.25228208  0.18981704  0.15296002
 -0.3062748  -0.13662805 -0.25415638  0.14236008  0.116825    0.00469154
 -0.05889941  0.07863956  0.4675573   0.06081422  0.2529209  -0.07001508
  0.34606618  0.2717276   0.25792852 -0.2984004  -0.07375812 -0.09873105
  0.14780869  0.0873313   0.10026332 -0.10008292 -0.3316018  -0.02629723
  0.5940275   0.30866838 -0.08813549 -0.32104307  0.03274322  0.10554635
 -0.30488187  0.377073   -0.0811692   0.20839754  0.38205898  0.02628802
  0.18617581  0.19931976  0.00599956  0.06431035  0.09854034  0.02887992
 -0.08011294 -0.03455874  0.08992771  0.06116451  0.19689974 -0.00615429
 -0.15785006 -0.01781378  0.00281635  0.34449962  0.09425399 -0.07273636
  0.1780002  -0.34851024  0.37838387 -0.13598311  0.2519176  -0.34066144
 -0.01075775 -0.14786543 -0.04548219 -0.02431995  0.10417668  0.11535702
 -0.09153163  0.01274134 -0.08331569 -0.07227844  0.10793906  0.01709241
 -0.33268264  0.03616779  0.18477437 -0.054786    0.11694523  0.2861517
 -0.1814767  -0.2492276  -0.3056511   0.10268316  0.02496759 -0.03885837
 -0.44996476 -0.13723636  0.02233196  0.01498503  0.19116491  0.02721791
 -0.09347356 -0.22418728  0.20053582 -0.02321318 -0.03963188  0.07721978
  0.3976757  -0.16255735 -0.2857601   0.01022803 -0.11282344 -0.03161858
  0.10372707 -0.04063642  0.13277282 -0.14350836  0.03758351  0.28988007
 -0.15129718 -0.07004785 -0.11414956  0.01055098 -0.2234786   0.20059532
 -0.10320012  0.06600995  0.10613239 -0.03008373 -0.34670264 -0.19242819
 -0.09134309 -0.03962347  0.30269867  0.24795845 -0.05786613 -0.08064044
  0.33049724 -0.35176003 -0.0745642  -0.14381081 -0.19127265 -0.2308729
  0.10868629  0.12795904  0.26682377  0.39642188  0.25173983  0.06555948
  0.08945418  0.28307387 -0.20416422  0.20722027 -0.35040206  0.11971842
 -0.22560261 -0.09891736  0.20061518 -0.01337475 -0.05774496  0.07784817
  0.0190405  -0.03751906 -0.27015632  0.02247851 -0.05720989  0.20762147
  0.27350608  0.13747151 -0.00919761  0.10923077 -0.33616212  0.08090062
  0.09826039  0.13366058 -0.08271509  0.3381954   0.13788582 -0.06729469
 -0.09151109  0.14015338 -0.14572589  0.29161888  0.02844842  0.07236236
 -0.033314   -0.16876915 -0.09799793 -0.01827359  0.31428993 -0.06787148
 -0.04007914  0.1423875   0.24909684  0.07624617 -0.31010824  0.24325818
 -0.04150698 -0.04732224  0.4020983  -0.13875155 -0.3239038  -0.08643212
  0.13847236 -0.09768677 -0.20165052 -0.27315557 -0.43414918  0.12716582
  0.40271717  0.11295483 -0.06921897 -0.02047522  0.05772358 -0.20754315
  0.27045983 -0.3403588  -0.28104421 -0.04287485  0.16783538 -0.05046352
 -0.02322021  0.80653805 -0.49147975  0.14961116  0.02781115 -0.13225512
 -0.05151397 -0.43092707 -0.01291704  0.200752    0.5579526  -0.02965733
 -0.2158913   0.04157935  0.33107662  0.19664446 -0.275344   -0.35029644
  0.15709329 -0.046059   -0.47471583 -0.05245955 -0.32224137  0.12703025
 -0.07285262 -0.40086585 -0.0947476   0.15882298  0.35492867 -0.2662322
 -0.26684946  0.06684791 -0.17897451 -0.06707234 -0.10908245  0.13751653
 -0.38477394 -0.21492186 -0.08163136 -0.12800853  0.42276528 -0.02782323
 -0.03355124  0.0600879  -0.08807893  0.10318359  0.11266886 -0.12545823]

12.4.2. 文章ベクトル

文章ベクトルは、単語ベクトルから算出されます。

この算出方法は色々あります。GiNZAでは、各単語の平均値ベクトルとして算出されるようです。

[47]:
doc = nlp('ポーツできる場所があるのが良い')
print(doc.vector.shape, doc.vector) # 単語ベクトル

(300,) [-0.02920693 -0.05320758 -0.0522105  -0.08731095 -0.03517435 -0.02590578
  0.00206362 -0.1063849  -0.09927486 -0.04500962  0.04499383 -0.06481141
 -0.0288375   0.04145418 -0.11855906 -0.13537505 -0.07372829 -0.005988
 -0.1166847   0.00200363 -0.07982864  0.0653679  -0.02242133  0.03441318
 -0.09967872 -0.10132443 -0.17745972 -0.00701735 -0.01890183 -0.02232861
  0.00425247 -0.09943429  0.1189977   0.11469252 -0.01476635 -0.00565097
 -0.08678276  0.08944558 -0.06026531  0.01860928  0.07967254  0.03719408
 -0.0694015  -0.03305633 -0.04918492 -0.06143166 -0.00042069  0.01650344
 -0.12821096  0.01242355 -0.01574672  0.02855304 -0.01309349  0.06785982
  0.02561383  0.0167272  -0.02429795 -0.01248624 -0.05111359 -0.00523373
 -0.03138427  0.03268416  0.03163368  0.00560306  0.00034257 -0.06571576
  0.12110818  0.18030974  0.10791279  0.01394656  0.0255538   0.07066533
  0.013614    0.06444446 -0.05641092  0.04407748  0.03320919  0.0004552
  0.02246881 -0.01736393 -0.07054626 -0.07443018 -0.01807081  0.02475807
  0.02234626  0.00154515  0.00551776  0.00626367  0.07205774  0.00634133
 -0.00399228  0.00767399 -0.03283146  0.07159147  0.06482697  0.10085686
  0.09856593 -0.04808494  0.03084285  0.00029174  0.00425797 -0.0495946
  0.11755088 -0.06460506 -0.04854925 -0.07223612  0.09228191  0.09676455
  0.07876915  0.02307541  0.06704701 -0.07946542 -0.00074483 -0.02177041
  0.018668   -0.12127856  0.07543771 -0.02609853  0.08425374  0.02380007
 -0.10355288 -0.09578987  0.01285531 -0.08888882  0.09946763 -0.00273043
 -0.04580917  0.03583116  0.05751135 -0.00427228 -0.08011837 -0.09800892
 -0.06978434 -0.07361989 -0.03381431 -0.0487395   0.02674628  0.0191588
  0.00247293 -0.08118343 -0.09112532  0.01042639  0.01473123  0.03303504
  0.0201649   0.00025361 -0.04324289  0.04405454  0.04665114  0.01007584
 -0.0546068  -0.08137256  0.0397726  -0.02145665 -0.21151868  0.14583382
 -0.08519368 -0.07310313 -0.04868405  0.05399269  0.02925398 -0.01777376
 -0.04635568  0.0677426  -0.03870093 -0.03328982 -0.1661798   0.07733109
  0.02625116 -0.07757658  0.05397995  0.01525526  0.00298676  0.06089411
  0.03646235 -0.01055191  0.080689    0.10871259  0.07923553  0.02947542
 -0.07205083 -0.0860265  -0.08119814  0.07206785  0.02596396  0.0130357
  0.0931971   0.04475599  0.02308878  0.01526667  0.03803923  0.02801875
 -0.06903288  0.05913321 -0.05807992  0.04792631 -0.0087816  -0.09529499
  0.01481394 -0.01695308  0.04447183 -0.07569    -0.07110366 -0.03407736
 -0.04950442  0.07128353 -0.03751003  0.02298016  0.13305187  0.02586973
 -0.10390261  0.01455116  0.07440154  0.05090108  0.00680628  0.0967969
 -0.02308619  0.00649277 -0.02494653 -0.00940593  0.04059135 -0.00369758
  0.03812422  0.06031455  0.06031232 -0.0071035  -0.02450604  0.01896761
  0.03082364  0.01141291  0.01425576  0.01331456 -0.06570377  0.1441507
  0.02328368  0.06463818 -0.11780812  0.07823002  0.0098581   0.02152279
  0.06506702 -0.04175583 -0.12819394 -0.01864348 -0.00585906  0.07083139
 -0.05241846 -0.07188665 -0.05962841  0.06958519  0.01034061 -0.0433907
 -0.03215906  0.02310069 -0.17440806  0.09636591  0.00652586 -0.04734528
  0.02170709 -0.12133204  0.09089412 -0.0492343   0.04126725  0.02021465
 -0.0496464  -0.08032804 -0.09103935 -0.06628747  0.06904414 -0.10683578
  0.09802075  0.12269397 -0.08747786  0.02755496 -0.05856017  0.03572648
  0.00165045 -0.03348825  0.0763365   0.04421256  0.01006745 -0.05442426
  0.06049172 -0.02185039 -0.0914305  -0.09839067  0.0950931  -0.02670297
  0.00542849  0.00232094 -0.06282385  0.02534583  0.01359846 -0.03921163
  0.01357794  0.10529844 -0.10408807 -0.03580856 -0.03502506 -0.02003236]

このような文書ベクトルを使うことでも文書類似度検索を行うことができます。

Let’s try

GiNZA の文書ベクトルを用いて、コサイン類似度から類似度検索をしてみよう。 TF/IDF と比較してみると面白いかも..

現在の自然言語処理では、TransformerによるBERTと呼ばれる文脈を含んだ ベクトル化が発明されて、人工知能や機械翻訳の精度が大きく向上しています。 さらに詳しく勉強したい人は、一緒に研究しましょう。

12.5. コースワーク

今回のアンケートでは、自由形式のコメントと満足度を同時に回答するようになっていたため、満 足度の高いキーワードを抽出できました。また、コメント文をベクトル化することで、コメント間 の類似度が求められることも見えてきました。

疑問(文書分類)

満足度は予測できるのでしょうか?

これは、エントリーシートから(採用後の)満足度は予想できるのでしょうか?と同じ質問になります。 皆さんは AI がエントリーシートを判定しているという噂を聞いたことがありますね。

演習(エントリシート)

企業がどのように AI を活用して、エントリーシートを分析しているか考察してみよう。 (可能であれば、今回のコメント文から満足度を予測するモデルを構築してみよう。)

今まで学んできた知識を総動員して、もし足りなかったら追加で調査して考えてみましょう。