LoginSignup

This article is a Private article. Only a writer and users who know the URL can access it.
Please change open range to public in publish setting if you want to share this article with other users.

More than 1 year has passed since last update.

その言葉、ツイッターでどんな言葉や感情と一緒に呟かれているの?+ネガポジ分析に取り組んでみた

Last updated at Posted at 2021-08-26

【対象読者】

 本投稿は下記のようなテーマについて実装を絡めて学びたいという方向けにまとめました。

マーケティングやキャッチコピーなどで使う予定の単語がどう呟かれているか知っておきたい。

スクレイピングを体験してみたい。

自然後処理の前処理の作法を軽く体験してみたい

トピックモデルの「LDA」を実装し、可視化する体験をしてみたい

【自己紹介】

 ほぼ完全初心者です。
 キーワードに注目したマーケティングで商品の売り込みやSEOに取り組む部署にいます。

【なぜこの分析が必要と考えるか】

 この世界は意外とアナログで、データをみて「こういうことだろう」で済ませてしまう人が意外と多く、実際に分析と言えるレベルまで進まないことも多々あります。
 そもそも一緒に呟かれているキーワードがネガティブな意味だった場合、それに寄せたマーケティングをしたら逆効果です。でも、人間、自分に都合よく解釈してしまうものです。
 今回はそんな事態を防ぐため、ネガポジ分析をしたうえでデータを可視化し、「データでみるとこうなんです」と言えるようにするための内容を目指しています。

image.png

 

【前提知識】

スクレイピング

 ざっくり、Web上に存在するデータから必要なものを集めてくる・・・という理解で大丈夫かと思います。
今回はTwitter上でつぶやかれているデータから「必要なものをかき集めてくる」ためのプログラムになっています。
 ここでいう「必要なもの」とは、こちらで指定したキーワードが含まれているツイートになります。

教師なし学習

 学習データにこちらから正解や視点を与えてそれにそった分析をしてもらうわけではなく、モデル自体に分析・学習させる方法です。人間だと気づかないパターンを見つけ出してくれるのですが、どういう視点(心情)からそのパターンを分析してくれたのかまではよくわかりません。
 「機械側にパターン分けさせたらこうなった」の先の解釈はこちら次第なんでしょう。

自然言語処理

 機械学習の一分野で、人間が日常的に使っている言語を機械側で処理します。ただ、前処理が必要です。「です」など意味が特段ないのに大量に出てくる単語をはじくためです。

トピックモデル

 対象にした文章がどんなトピック(話題・観点・視点)なのかを判定する機械学習のモデルです。
文章と言っても、政治的な話からスポーツや芸能ネタなど色々な切り口のものがあります。
 今回はTwitterのつぶやきの内容を、人間が見て「この内容は政治系だなー」といった判断をするのではなく、トピックモデルで機械側に分析してもらうようにしています。

LDA

 今回使用したトピックモデルの1種類です。英語でいうと"Latent Dirichlet Allocation”の頭文字をとったもので、ざっくり言うと対象の文章中に「ある単語が出現する確率」を推定するモデルになります。
 文章と言ってもかならず一つのトピックに収れんするわけではありません。70%は政治マターだけど30%はそれを皮肉るコメディマターかもしれません。
 なのでこの手法では各文章は「複数のトピックに属している」という前提に立って分析をします。この場合の「複数の」の部分の数自体はこちらで任意の数だけ設定できます。なので様々な角度から見た場合、というのが試せることになります。

実装にあたって

【開発環境】:Jupyter lab

 機微に触れるキーワードを調べたいというケースもあると思うので、Jupyter labを選んでいます。

【日本語解析ツール】:mecab-python3-0.7

 mecabについては「pip install mecab-python3==0.7」をコマンドプロンプト上で実践すればできます。

【スクレイピング用のツール】:twint

 pip install twint で大丈夫です。Twitter社にAPIを申請したりなんだりといった作業なくTwitterのスクレイピングができるのは驚くばかりです。

Jupyter lab上で完結させたい

 まずコマンドプロンプトでtwintを使って、関連する呟きを集めたCSVファイルを作って、それをJupyter labで分析するという手もありました。
 例えば下記のような方法です。

twint -s "調べたいキーワード" --since 2020-01-01 --min-likes 5 -o keyword_sinse2020.csv --csv

twintの使用法は下記ご参照ください。
https://github.com/twintproject/twint/wiki/Basic-usage

上記の場合を日本語で書くと
 ▼調べたいキーワードについて
 ▼2020年1月1日のつぶやきから
 ▼いいね数が5件以上あるものだけにしぼって
 ▼keyword_sinse2020.csvという名前の
 ▼csvファイルにして出力してくれ
 ということになります。他にも様々な機能がありますが、とりあえずこれが分かれば、後は条件部分を入れ替えれば大丈夫。「調べたいキーワード部分」に複数の文字を入れたい場合は「ラーメン,東京」など「,」で区切って入れれば両方含まれているものを探してくるようになります。

 いいね数5件については、最低限の支持を集めているものだけに絞るためです。極端な意見が大量に垂れ流されているのがTwitterの特徴でもありますので。

でも抽出はコマンドプロント→分析はJuptyer lab・・・は面倒くさい

 そのため、今回は上記のデータ抽出の部分からJupyter labに入れ込むことを目的としました。

まず一つめのセルは今回必要なライブラリのインポートです。入っていない方はpipでインストールしてから始めた方が良いと思います。

import pandas as pd
import MeCab
import gensim
import numpy as np
import matplotlib.pyplot as plt

問題は次

 Jupyter labでコマンドプロンプト上の抽出挙動を再現するには、下記のように書いてみました。

import nest_asyncio
nest_asyncio.apply()
import twint
c = twint.Config()
request_word = input("調べたい単語")
c.Search = request_word
c.Store_csv = True
request_start = input("クロール開始地点2021-07-01のような形で")
request_end = input("クロール終了地点2021-07-01のような形で")
c.Since = request_start
c.Until = request_end
c.Output = request_word+""+request_start+""+request_end+".csv"
c.Min_likes = 5
twint.run.Search(c)

こう書くと
▼inputボックスにいれた単語を「request_word」という変数に代入
 →つまり調べたいキーワードをJupyter lab上で入力できる

▼次のinputボックスで「request_start」という変数でスクレイピング的なことを始める時点
▼そして続くinputボックス「request_end」の変数でスクレイピング的なことの終わりを入れる。
 →つまりスクレイピングの対象期間もJupyter lab上で入力できる

 ということになります。ついでにcsvとして排出されるときに、その名前が「キーワードと調査対象期間」になるようにしました。
 「ロッキー」について2021年1月1日から8月26日までの呟きをしらべると
「ロッキー_2021-01-01_2021-08-26.csv」という名前のcsvファイルができるようになります。

※今回はなんとなく「ロッキー」について調べたくなりました。気分です。なんでもいいです。「,」を使えば複数のキーワードが含まれている単語を調べることも普通にできます。「ロッキー4,炎の友情,ディレクターズカット」でもいいです。

ロッキーと言えばボクシング映画の金字塔ですが

image.png

でも今は某車会社の某ブランドのイメージの方が強いのかも。

image.png

今回はどちらに軍配があがるのでしょうか。

image.png

これでキーワード取得を楽にしたところでいよいよ分析です。

まず普通にこの状態で回すとずらーっと膨大な呟きがでます。

いいよ。実にいい。

昔大好きだったロッキーを見た。 1をちゃんと見たことがない事に気づいたがそんな事はさておきやっぱいい!! ハチャメチャやし、古臭い😅でも明日の活力になる!元気が出る!! #しかしスタローン若いな

車派もやはり多い。

とくに車での用事も無いけど 何となくロッキーくんで仕事行ってくる式🤧 皆さんおはらっこです𓏸𓂂 𓈒 🦦 𓈒 𓂂𓏸

ただ、このままだとただ大量の記述を眺めるだけになってしまいます。

そこでまずスクレイピングで収集したcsvファイルをDateFrameにします。
df = pd.read_table("ロッキー2021-07-01.csv")

ここでprint(df)とやると分析に直接必要のない大量のカラムがあることが分かります。
[1420 rows x 36 columns]

というわけで、必要なさげなカラムを落としていきましょう。usernameとか普通に分かるのが地味に怖い。

df = df.drop(['id', 'conversation_id', 'created_at', 'timezone', 'user_id', 'name', 'place', 'geo', 'source', 'user_rt_id', 'user_rt', 'retweet_id', 'reply_to', 'retweet_date', 'translate', 'trans_src', 'trans_dest'], axis=1)

df = df.drop(['language', 'mentions', 'urls', 'photos','hashtags', 'cashtags', 'link', 'retweet', 'quote_url', 'video', 'thumbnail', 'near'], axis=1)

横に長すぎて一度に全部落とすのがしんどかったので、2回に分けました。axis=1でカラムですね。

次にここでポジティブな言葉とネガティブな言葉に振り分けます。

そのためのコードは

%%capture capt
from transformers import AutoTokenizer, AutoModelForSequenceClassification
from transformers import pipeline
tokenizer = AutoTokenizer.from_pretrained("daigo/bert-base-japanese-sentiment")
model = AutoModelForSequenceClassification.from_pretrained("daigo/bert-base-japanese-sentiment")

いちおう実験だけしてみます。

print(pipeline("sentiment-analysis",model="daigo/bert-base-japanese-sentiment",tokenizer="daigo/bert-base-japanese-sentiment")("私は幸福である。"))

これで回すと
[{'label': 'ポジティブ', 'score': 0.9843042492866516}]
と出ました。一応ネガポジ分析はしてくれているみたいですね。

ただ、毎回このネガポジ用の学習をされると時間がかかって仕方が無いので、これは別ファイル「Learning.ipynb」に保存しておき、本番のプログラムでは引用するだけで言いように仕様と思います。
なのでLearning.ipynbの最後は

import pickle
with open('tokenizer.pkl', mode='wb') as f:
    pickle.dump(tokenizer, f)
with open('model.pkl', mode='wb') as f:
    pickle.dump(model, f)

で締めて、分析用のプログラムでは

#お勉強部分をmodel.pklに保存してあり、それをここで引用できる。一から勉強し直さなくていい。
import pickle
with open('tokenizer.pkl', mode='rb') as f:
    tokenizer = pickle.load(f)
with open('model.pkl', mode='rb') as f:
    model = pickle.load(f)

として引き出すだけで良いようにしました。

from transformers import pipeline
#Learning.ipynbで勉強させたところで
#"daigo/bert-base-japanese-sentiment"はmodelに
#"daigo/bert-base-japanese-sentiment"はtokenizerになっている
#そのため、それぞれをmodel、tokenizerに再代入
sentiment_analyzer = pipeline("sentiment-analysis",model=model,tokenizer=tokenizer)
list_text = df['tweet'].tolist()
list(map(sentiment_analyzer, list_text))

というわけでここからポジティブとネガティブを分けます。

positives = []
negatives = []
for text in list_text:
    result = sentiment_analyzer(text)[0]
    label = result['label']
    if label == 'ポジティブ':
        positives.append(text)
    else:
        negatives.append(text)

ここから先は文章を構成する形態素解析用の関数を作ります。


def parse(tweet_temp):
    t = MeCab.Tagger()
    temp1 = t.parse(tweet_temp)
    temp2 = temp1.split("\n")
    t_list = []
    for keitaiso in temp2:
        if keitaiso not in ["EOS",""]:
            word,hinshi = keitaiso.split("\t")
            t_temp = [word]+hinshi.split(",")
            if len(t_temp) != 10:
                t_temp += ["*"]*(10 - len(t_temp))
            t_list.append(t_temp)

    return t_list

def parse_to_df(tweet_temp):
    return pd.DataFrame(parse(tweet_temp),
                        columns=["単語","品詞","品詞細分類1",
                                 "品詞細分類2","品詞細分類3",
                                 "活用型","活用形","原形","読み","発音"])

ただ、センテンスには様々なつなぎ用だけの言葉があるので、つぶやきたちから一般名詞と固有名詞のみをえらんで、それらの単語だけをBag-of-Wordsで出力する関数を作ってみます。


def make_docs_for_lda(texts):
    docs = []
    for i, text in enumerate(texts):
        print(str(i+1) + " th parse START")
        df = parse_to_df(text)
        extract_df = df[(df["品詞"]+"/"+df["品詞細分類1"]).isin(["名詞/一般","名詞/固有名詞"])]
        extract_df = extract_df[extract_df["原形"]!="*"]
        doc = []
        for genkei in extract_df["原形"]:
            doc.append(genkei)
        docs.append(doc)
    return docs

ここからポジティブワードだけを対象にします。


texts = df["tweet"].values # ndarrayにしてあげる
docs = make_docs_for_lda(positives)

import pickle

with open("./docs_01", mode="wb") as jugemu:
    pickle.dump(docs, jugemu)

"""
docs = []
with open("./docs_01", mode="rb") as gokoh: 
    docs = pickle.load(gokoh)
"""

この後はLDAで分析してもらうための辞書とコーパスを仕込みます。gensimを使います。


dictionary = gensim.corpora.Dictionary(docs)

corpus = [dictionary.doc2bow(doc) for doc in docs]
corpus = []
for i, doc in enumerate(docs): # gonna rep 92908 times
    corpus.append(dictionary.doc2bow(doc))
    print(str(i+1) + " th doc2bow DONE")


with open("./coupus_01", mode="wb") as kaijarisuigyo:
    pickle.dump(corpus, kaijarisuigyo)

"""
corpus = []
with open("./coupus_01", mode="rb") as suigyomatsu: 
    corpus = pickle.load(suigyomatsu)
"""

ようやくLDA学習に入ります・・・ロッキーもボクシングの試合部分の尺は10~15分程度なことが多いです。そこに至るまでの過程が大事ということですね。

トピック数は任意なのですが、トピック数の最適さもPython様だと可視化できるということでトライ。


from tqdm import tqdm
#ここでn_topicsに入る数字の最適値を出すためのグラフプロットをする
import matplotlib
import matplotlib.pylab as plt
font = {'family': 'TakaoGothic'}
matplotlib.rc('font', **font)
start = 2
limit = 50
step = 2
coherence_vals = []
perplexity_vals = []
for n_topic in tqdm(range(start, limit, step)):
    lda_model = gensim.models.ldamodel.LdaModel(corpus=corpus, id2word=dictionary, num_topics=n_topic, random_state=0)
    perplexity_vals.append(np.exp2(-lda_model.log_perplexity(corpus)))
    coherence_model_lda = gensim.models.CoherenceModel(model=lda_model, texts=docs, dictionary=dictionary, coherence='c_v')
    coherence_vals.append(coherence_model_lda.get_coherence())
x = range(start, limit, step)
fig, ax1 = plt.subplots(figsize=(12,5))
c1 = 'darkturquoise'
ax1.plot(x, coherence_vals, 'o-', color=c1)
ax1.set_xlabel('Num Topics')
ax1.set_ylabel('Coherence', color=c1); ax1.tick_params('y', colors=c1)
c2 = 'slategray'
ax2 = ax1.twinx()
ax2.plot(x, perplexity_vals, 'o-', color=c2)
ax2.set_ylabel('Perplexity', color=c2); ax2.tick_params('y', colors=c2)
ax1.set_xticks(x)
fig.tight_layout()
plt.show()

結果は下記のようなグラフに

image.png

Perplexityはは各単語に関する選択肢の頻度的なやつで低ければ低いほどよい

一方、Coherenceは分析するトピックの品質=「人間がいかに理解しやすいトピックか=トピック内に含まどれだけどれだけ一貫性があるか」を示すということで、高ければ高いほど良いとされています

これを見る限り、トピック数4か14が良さそうですね。14はちょっとブログには多すぎるので4で行きます。


n_topics = int(input("トピック数を入力"))
lda = gensim.models.ldamodel.LdaModel(corpus=corpus,
                                      id2word=dictionary,
                                      num_topics=n_topics,
                                      random_state=0)

なかなか数字でバシッと示せないので、定性的に可視化する方向でいきます。
その際に使えるのがWordCloud。見た目でどの単語がどれくらい強く関係があるのかわかるからです。

pre>
def normarize_then_square(x, alpha, axis=None):
#2次元配列を列方向に正規化し2乗する関数
x_min = x.min(axis=axis, keepdims=True)
x_max = x.max(axis=axis, keepdims=True)
return ((x - x_min) / (x_max - x_min))**2 + alpha

def weight_dict(dict, alpha):

dict_arr = np.random.randn(1,2)
#print(dict_arr.shape)

for i, value in dict.items():
    row_i = np.array([i, value]).reshape([1,2])
    dict_arr = np.block([[dict_arr], [row_i]])
    #print(dict_arr.shape)

dict_arr = dict_arr[1:,:]
#print(dict_arr.shape)
#print(dict_arr)

keys = dict_arr[:,0] .reshape([len(dict),1])
values = dict_arr[:,1].reshape([len(dict),1]).astype('float64')

values_weighted = normarize_then_square(values, alpha, axis=0)
arr_weighted = np.concatenate([keys, values_weighted], 1)
#print(arr_weighted)
#dict_weighted = dict(arr_weighted)

return arr_weighted</code></pre>


from wordcloud import WordCloud
import math
FONT = "C:/Windows/Fonts/07YasashisaAntique_0.otf"
ncols = math.ceil(n_topics/2)
nrows = math.ceil(lda.num_topics/ncols)
fig, axs = plt.subplots(ncols=ncols, nrows=nrows, figsize=(15,7))
axs = axs.flatten()
def color_func(word, font_size, position, orientation, random_state, font_path):
    return 'darkturquoise'

 とここまできたところで一点注意です。このままだと単語群に検索に使ったキーワードが含まれた状態です(今回はロッキー)。これを取り除いてあげないと、どのトピックモデル分析でも真ん中にロッキーがどかんと鎮座してしまいます。
 なので辞書単語群から「ロッキー」を取り除く作業が必要です。

キーを使ってdictから安全に複数の要素を削除する関数

def entries_to_remove(entries, the_dict):
    for key in entries:
        if key in the_dict:
            del the_dict[key]


for i, t in enumerate(range(lda.num_topics)):
    print(str(i)+ ":" + str(t))
    x = dict(lda.show_topic(t, 40))
    #ここでrequest_word.split()としておくことで、リスト型になりキーワードに半角スペース区切りで複数単語が入った場合にも対応可能
    entries_to_remove(request_word.split(), x)
    print(x)
    #x = dict(weight_dict(x,0.01))
    print(x)
    im = WordCloud(
        font_path=FONT,
        background_color='white',
        color_func=color_func,
        random_state=0
    ).generate_from_frequencies(x)
    axs[i].imshow(im)
    axs[i].axis('off')
    axs[i].set_title('Topic '+str(t))
plt.tight_layout()
plt.savefig("./visualize.png")

結果は・・・

image.png

「映画」「テーマ」(おそらく「ロッキーのテーマ」をさしている)、「ダイハツ」はやはりはいっています。
ただ、「川村」ってなんでしょうか?エイドリアンより重要な人物のようです。

次に変数positivesをnegativesに入れ替えて全く同じことをやってみます。

入れ替えただけなのでコードは割愛しますが、下記のような結果になりました。

image.png

これでみるとネガティブはかなりばらつきがありますね。16がお得なようです。正直ブログに載せるには多すぎますが、ここまできたらやるしかない感じでしょうか。

image.png

Topic0の政治色が半端ないです。なぜか。
ちなみにダイハツはネガティブな意味でも捉えられているようです。
「やる気」とか「汗」とかもは一散るのは、ロッキー=脳筋と受け止められているのかもしれません。
「ロード」とかは31アイスクリームの「ロッキーロード」なのかも。

なんかもやもやしたので「ロッキー4,ロッキー」でやってみました。これなら色々混じらないだろ。

今回、ロッキーというフレーズが車や31アイスクリームの名称に含まれていることから、分析の足を引っ張っていると考えました。

ポジティブの方

image.png

こりゃトピック数は2しかないですね。

image.png

片方は「友情」とか「男」とか。若干暑苦しいワードとの関係が良好な方々に寄せたマーケティングに使えそうですね。

もう片方は「ダイ・ハード」「ジャッキー」「ターミネーター」など。「ポーター」はおそらく「トランス」とリンクすると、ジェイソンステイサムの「トランスポーター」ではないかと思われます。アクション映画好きに寄せていくのに使えそうです。

★筋肉映画の数々

image.png

image.png

ネガティブの方のトピック数はある種すがすがしい結果に・・・

image.png

これはポジティブと同じトピック数2ですね。その結果がこちら・・・・

image.png

「筋肉」「ドーピング」という言葉が光ります。

 ネガティブの方は2分類してもちょっとにているんですが、要は、ロッキー4だと、ロッキーはろくな機材もない土田舎の山小屋で雪の中トレーニングをしなきゃいけないのたいして、相手のソ連のボクサーは最先端のトレーニングルームの中で、ドーピング注射うちまくりながら高速ランニングマシンを使って筋トレするという対比のシーンがあります。

image.png
image.png

 まあ、そういうアンフェアなのが嫌い、ということなのか、そういうステレオタイプなソ連像を「印象」づけてくるこの映画が嫌いと言うことなのか、という感じです。そういう人向けに広告が飛ばないようにするキーワード選びや広告媒体選びが必要そうですね。

 確かに嫌な人には嫌なんでしょう。
image.png

 ただ、ネガティブ分析前者の「筋肉」はポジティブに受け止められることもあるので、避けにくいキーワードです。
 
 一方、後者の分析からは、スタローン本人は前面に出さないで、あくまでも「ロッキー」というキャラクターを前面に出す方がよさそうですね。スタローン自身にもステロイド使用疑惑があり、それとドーピングがつながってしまっている可能性も捨てきれないので。どのみち、「スタローン」という単語自体はほぼ空気みたいな存在感ですし。
 いかにこの映画は「ロッキー」というキャラクターで成り立っているかがよく分かります。ロッキーは知っていても、主演俳優の名前を知らない人がいるんじゃないかというくらいです。

【今回の結果について】

 とりあえずやってみて、今回の感想はトピックの数自体を探れるというのが非常に大きいと思いました。市販のテキストマイニングツールなどではカバーしきれない部分かと思います。

【今回の達成度】

 個人的には60%というところです。
 コードの中に、調べたいキーワードに交じってくるであろう「ノイズ」を取り除く箇所を設定するべきでした。今回でいうと「ダイハツ」や「ロード」(おそらく31アイスクリームのロッキーロード)です。それは今後実装していきたいです。
 「ロッキー」と「ロッキー4」でこれだけ変わるということから、上記の重要さを実感しました。

 実は「あぶない刑事」でもトライしたのですが、「あぶない刑事」自体を図から取り除くコードを書いても、「刑事」というワードが図に残ってしまいます。それも改善したいと思います。

abunai.png

【今後の目標】

●今回はtwintという強力なライブラリーを使用しましたが、いつBANされるかわかりませんので、Twitter社のAPI申請を通して、堂々と同じことができるようにしようと思います。

●Twitterに限らず、他のSNSやGoogleなどでもこの分析が使えないか模索してみようと思います。データの取り出し部分以外は、同じ作業なので。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up