PythonからGoogle Cloud Natural Language APIを使って感情分析

Google Cloud Natural Language APIを使って、Pythonで日本語文章の感情分析 (ポジティブ/ネガティブの判定) を行います。

このAPIは自然言語処理のためのGCPサービスで、感情分析以外にも、構文解析やエンティティ解析が提供されています。

cloud.google.com

準備

Google Cloud Natural Language APIを使い始めるには以下作業が必要です。

  • Google Cloud Natural Language APIの有効化
  • サービスアカウントのアクセスキーを取得

環境変数GOOGLE_APPLICATION_CREDENTIALSにアクセスキーのパスを設定しておきます。

$ env GOOGLE_APPLICATION_CREDENTIALS=~/.gcp/credential.json

実装

Pythonでの実装に入っていきます。

まずはパッケージをインストールします。

pip install --upgrade google-cloud-language

Pythonでの実装は以下のとおりです。

  • LanguageServiceClientを使ってAPIにリクエストする
    • analyze_sentimentメソッドで感情分析を行う
  • リクエストにDocumentオブジェクトを詰めることで文書を渡します
    • PLAIN_TEXT以外にはTYPE_UNSPECIFIEDHTMLがあります
  • レスポンスのscoreにポジティブ/ネガティブの度合い、magnitudeに文章内でのポジティブ/ネガティブの振れ幅がそれぞれ格納されている
    • scoreは-1.0〜1.0で、値が大きいほどポジティブ

今日は天気が良かったので散歩しにでかけた。はscoreが0.6なので、ポジティブ寄りの文章と判定されます。

from google.cloud import language
from google.cloud.language import enums
from google.cloud.language import types

# クライアントの作成
client = language.LanguageServiceClient()

# リクエスト (解析する文書) の作成
text = """
今日は天気が良かったので散歩しにでかけた。
"""
document = types.Document(
    content=text,
    type=enums.Document.Type.PLAIN_TEXT)

# APIをコールして結果を得る
result = client.analyze_sentiment(document=document)

print('score: {}, magnitude: {}'.format(result.document_sentiment.score, result.document_sentiment.magnitude))
# score: 0.6000000238418579, magnitude: 0.6000000238418579

試しに、自分のツイートからいくつか文章を拾ってscoreを見てみます。
下の文章だとscoreは-0.5で、ネガティブ寄りの判定。

ここ数日、起床時間がガチャガチャしてて、調子が良くない。
数日ならまだ良いけど、夜中に起こされることが常態化すると、不眠になったり、逆にまともに時間に起きられなくなったりするので、注意しないと。

下はscoreが0.3で、ややポジティブ寄りです。

頭の片隅でグズグズ渦巻いている悩みの一つを、消し去れた。
少し身が軽くなった気がする。

レスポンスのsentencesに文 (sentence) ごとのscoreが入っています。

for sentence in result.sentences:
    print(sentence.text.content, sentence.sentiment.score)
# 頭の片隅でグズグズ渦巻いている悩みの一つを、消し去れた。 0.10000000149011612
# 少し身が軽くなった気がする。 0.6000000238418579

scipy.statsでカーネル密度推定 (KDE)

scipy.statsでカーネル密度推定 (KDE) を行う方法のメモです。

カーネル密度推定は、標本データから確率密度を推定するものです。
要するにヒストグラムをなめらかにすることで、データの傾向を捉えやすくします。

2017/1/1〜2017/12/31 (365日) の東京の日別平均気温を使います。気象庁のサイトからダウンロードできます。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy.stats import gaussian_kde

print(data)
#[7.4,
# 7.2,
# 8.0,
# 8.5,
# ...
# 6.2,
# 3.8]

plt.hist(data, bins=31, range=(0, 31))
plt.show()

5〜10度と20〜25度が山になっていることがわかります。

f:id:ohke:20181006212723p:plain

scipy.statsでカーネル密度推定をするためには、gaussian_kdeクラスを使います。

  • インスタンス生成時にデータを渡します
  • インスタンスに推定したい範囲を渡すと、密度の推定値が計算されて返されます
# gaussian_kdeのインスタンスを生成
kde = gaussian_kde(data)

# 0〜31で密度推定
estimates = kde(np.linspace(0, 31, num=32))
print(estimates)
# [0.00409969 0.00757112 0.0128535  0.01999006 0.02838034 0.03670379
#  0.04325381 0.04661941 0.04635048 0.04318489 0.03868037 0.03447436
#  0.03161084 0.03028665 0.03006976 0.03036525 0.03082537 0.03149862
#  0.03267868 0.03457151 0.03699876 0.03934671 0.04082662 0.04088853
#  0.03948617 0.03698289 0.03378769 0.03005188 0.02569315 0.02069462
#  0.01536906 0.01032863]

# グラフで表示
plt.plot(kde(np.linspace(0, 31, num=32)))
plt.xlabel('temerature [C]')
plt.ylabel('kde')
plt.show()

双峰性になっていることがわかります。

f:id:ohke:20181006212804p:plain

なお、gaussian_kdeにはbw_methodというパラメータがあります。これは各点の密度の推定に使う値の範囲 (バンド幅) を決めるためのメソッドや数値を渡すことができます。
大きな数値を渡すと、広い範囲の値を使って計算されるようになるので、全体的に鈍い形に近づいていきます。

0.05, 0.2, 0.6の3つの値で密度推定してみます。

kde_05 = gaussian_kde(data, bw_method=0.05)
kde_2 = gaussian_kde(data, bw_method=0.2)
kde_6 = gaussian_kde(data, bw_method=0.6)

plt.plot(kde_05(np.linspace(0, 31, num=32)), label='bw=0.05')
plt.plot(kde_2(np.linspace(0, 31, num=32)), label='bw=0.2')
plt.plot(kde_6(np.linspace(0, 31, num=32)), label='bw=0.6')

plt.xlabel('temerature [C]')
plt.ylabel('kde')
plt.legend()
plt.show()

値が大きくなるほど、山が消え、平坦に近づくことがわかります。

f:id:ohke:20181006221618p:plain

小ネタ: PandasでCSV文字列を分割して列にする

PandasでCSV形式の文字列のカラムを、それをカンマ区切りで分割して、別々の列にする方法のメモです。
例えば、1行目なら"123"と"456"と"789"を3つの列に分割します。

import pandas as pd

df = pd.DataFrame({'name': ['A', 'B'], 'csv': ['123,456,789', 'abc,def,ghi']})

Series.str.splitメソッドを使うと、3つの列に分割されたDataFrame (tmp) が生成されます。最後にもとのDataFrame (df) に3つの列を追加しています。

  • 第1引数は区切り文字
  • expand引数は列に展開するかどうか (Falseの場合、列は分割されずリストになる)
# カンマ区切りで分割
tmp = df['csv'].str.split(',', expand=True)

# 列を追加
df['value1'] = tmp[0]
df['value2'] = tmp[1]
df['value3'] = tmp[2]