人工知能でTwitterのタイムラインとみんなの心を光で満たしたい

はじめまして!Aidemy研修生の紫垣と申します!

僕はよく、Twitterを眺めててネガティブなツイート(病みツイとか)を見かけて、気分がどんよりしてしまうことがあります・・・

じゃあそいつのアカウントミュートなりブロックなりしろよ!と言われるかもしれません。

だけど付き合い上そういうことができない方も多いですし、実際一つ一つミュートとかしてもキリないですよね?

そこで!!!!人工知能の力をお借りしたいと思います!!!!!

ツイートがポジティブなものかネガティブなものかを人工知能が判別して、ネガティブなツイートをタイムライン上に表示しないようなアプリを開発すれば、僕たちの心は光で満ち溢れるのではないかと思いました!!

まずこの記事では、第一歩として、ツイートがポジティブかネガティブかを判別する機械学習モデルを開発します!

というわけで、さっそく開発の流れを書いていきます!!

実行環境

まず、今回の実行環境です。

Google Colabotaroryを使用しました。


OS : ubuntu 17.10
CPU : Intel(R) Xeon(R) CPU @ 2.30GHz
GPU : Tesla K80
メモリ : 12GB

データ集め

方法

機械学習をおこなうためには、まず大量のデータが必要です!今回必要とするデータは

  • ポジティブなツイート
  • ネガティブなツイート

この2つです!

ポジティブなツイートをどうやって探そうかと考えたところ、こんな感じのポジティブなつぶやきばかりするbotを複数発見しました!

データ収集に使用するtwitter apiでは、1つのアカウントから1週間分のツイートしか集めることができないため、このようなアカウントを複数、収集の対象としました!

ネガティブなツイートについても、こんな感じのbotを複数集めてきました!

あああ!!心がしんどい・・・

ですがこれも開発のためです!頑張ってデータ集めしていきたいと思います!

プログラム

今回、pythonで記述していきます!
こちらがデータ集め用のプログラムです!


def get_twitter_api(CK, CS, AK, AT):
    auth = tweepy.OAuthHandler(CK, CS)
    auth.set_access_token(AK, AT)
    api = tweepy.API(auth)
    return api
def get_tweet(user_id,count):
    API = get_twitter_api(CK, CS, AK, AT)
    data = API.user_timeline(id=user_id, count=count)
    tweets = []
    for tweet in data:
        tweets.append(format_text(tweet.text))
    return tweets,data[-1].id

def get_tweet_with_id(user_id,count,next_max_id):
    API = get_twitter_api(CK, CS, AK, AT)
    data = API.user_timeline(id=user_id, count=count, max_id=next_max_id-1)
    tweets = []
    for tweet in data:
        tweets.append(format_text(tweet.text))
    if len(data) == 0:
        return tweets,0
    return tweets,data[-1].id

こちらがツイート収集用プログラムです。
これらの関数を利用して、実際にデータ収集させるプログラムがこちらです!


def get_positive_tweets():
    pos_tweets = []
    positive_ids =[
                  "positive_bot_00",
                  "positivekk_bot",
                  "botpositive", 
                  "positive_mot",
                  "kami_positive",
                  "positive_bot",
                  "jinseiplusbot",
                  "syuzou_genki",
                  "genki_kotoba_m"
    ]
for pos_id in positive_ids:
    tmp,max_id = get_tweet(pos_id,200)
    tmp = list(set(tmp))
    for i in range(len(tmp)):
        pos_tweets.append(tmp[i])
    while True:
        tmp,max_id = get_tweet_with_id(pos_id,100,max_id)
        tmp = list(set(tmp))
        for i in range(len(tmp)):
            pos_tweets.append(tmp[i])
        if max_id == 0:
            break
    return pos_tweets

def get_negative_tweetes():

    neg_tweets = []
    negative_ids = [
                  "negatizibu_bot",
                  "inmydream19",
                  "lewyfDanf",
                  "positive_act_me",
                  "pgmtmw",
                  "yamik_bot",
                  "cool_aroma",
                  "nega_bot",
                  "negativedbot",
                  "H4Za5",
                  "ymibot"
    ]
    for neg_id in negative_ids:
        tmp,max_id = get_tweet(neg_id,200)
        tmp = list(set(tmp))
        for i in range(len(tmp)):
            neg_tweets.append(tmp[i])
        while True:
            tmp,max_id = get_tweet_with_id(neg_id,100,max_id)
            tmp = list(set(tmp))
        for i in range(len(tmp)):
            neg_tweets.append(tmp[i])
        if max_id == 0:
            break
    return neg_tweets

こちらの関数でポジティブツイート、ネガティブツイートの収集を行っています。positive_ids,negative_ids内のIDのアカウントの、1週間分のツイートを収集できるようになっています!set()関数を使うことでbotで頻繁に表れる重複ツイートを取り除いています。そして残ったツイートをpos_tweets、neg_tweetsに格納して、返り値としています!

これでポジティブツイート、ネガティブツイートのデータは集まりましたね。
次はこれらのツイートを、機械が学習しやすいように整形していきます!

データの整形

不要な文字の削除

まず、URLとかよくツイートの中に入っていますが、これは学習の際、ジャマですよね!下のプログラムで削除します。

プログラム
def format_text(text):

    text=re.sub(r'https?://[\w/:%#\


amp;\?\(\)~\.=\+\-…]+', "", text)
text=re.sub('RT', "", text)
text=re.sub('お気に入り', "", text)
text=re.sub('まとめ', "", text)
text=re.sub(r'[!-~]', "", text)#半角記号,数字,英字
text=re.sub(r'[︰-@]', "", text)#全角記号
text=re.sub('\n', " ", text)#改行文字

return text
format_text()の中に文字列を引数として突っ込むと、その文字列の中のURLっぽい表現や、絶対必要ないであろう表現を削除した文字列に整形して返してあげるプログラムです。
これでいい感じのデータになりましたが、まだまだいいデータに整形できます。そこで役立つのが、「形態素解析」です!!

形態素解析

形態素解析とは!
ざっくりかみ砕いていうと、文章を単語ごとに細かく切って、それらの単語の品詞(名詞とか動詞とか)などの情報を得ることです!
例えば、「明日は晴れるでしょう。」という文章を形態素解析すると、こんな感じになります!


明日     アシタ    明日     名詞-副詞可能        
は      ハ      は      助詞-係助詞        
晴れる    ハレル    晴れる    動詞-自立    一段       基本形
でしょ    デショ    です     助動詞      特殊・デス    未然形
う      ウ      う      助動詞      不変化型     基本形
。      。      。      記号-句点

なんかいろいろ書いてあって難しそうですが、左から4列目を見てください!今回、こちらに書いてある品詞の情報を使って、データを整形していきます!

プログラム
def tokenize(tweets):
  t = Tokenizer()
  tokenized_tweets = []
  for tw in tweets:
    tokens = t.tokenize(tw)
    tmp = ""
    for token in tokens:
      
      noun_flag = 0
      
      partOfSpeech = token.part_of_speech.split(",")[0]
      
      if partOfSpeech == "名詞":
        noun_flag = 1
      if partOfSpeech == "動詞":
        noun_flag = 1
      if partOfSpeech == "形容詞":
        noun_flag = 1
      if partOfSpeech == "形容動詞":
        noun_flag = 1
      if partOfSpeech == "感動詞":
        noun_flag = 1
      
      if noun_flag == 1:
        tmp += token.surface + " "
        
    tmp = tmp.rstrip(" ")    
    tokenized_tweets.append(tmp)
    
  return tokenized_tweets

まず、janomeライブラリの中のTokenizer()というインスタンスを作成します。このインスタンスが形態素解析をおこなってくれます。
次に、Tokenizer()インスタンスのtokenize()関数を使って、集めたツイート一つ一つを、先ほどの文章みたいに細かく切り刻みます。
そしてfor文で1つ1つの単語の品詞を変数partOfSpeechに代入し、その品詞が学習で使えそうな品詞(名詞、動詞、形容詞、形容動詞のいずれか)なら、noun_flagというフラグを1(このフラグの値は元々0)にします。
最後に、1つ1つの単語に対してその単語のnoun_flagが1なら(上に挙げた品詞に該当する)残す、0なら(上に挙げた品詞に該当しない)その単語を削除、といった具合にツイートをゴリゴリ削っていきます!また、今後のために各単語を半角スぺース” “で区切っています。
そうして整形されたツイートを返り値として返す関数になっています!
例えば、「明日は晴れるでしょう。」というツイートがもしあった場合、こうなります。
「明日 晴れる」
これで機械に学習させる準備は整いました!早速学習させていきましょう!

機械学習

ランダムフォレスト

学習モデルにはいろいろありますが、今回、ランダムフォレストという学習モデルを使用します。なぜこのモデルを選んだかについては後述します。
このランダムフォレストを理解するためには、まず決定木分析について知ることが必要です!

決定木分析とは?

下の図のような決定木と呼ばれるものをデータの学習によって作成し、条件分岐によってグループ(ツイートの集まり)を分割し、グループ内の各要素(ツイート)を分類する(ポジティブか?ネガティブか?)手法です。
f:id:shigashan:20180924183518p:plain
上の決定木だと、「ゲーム楽しい!最高!」というツイートはPositive!に分類されます。
また、「ゲーム楽しいけど、負けるとイライラする…」というツイートはNegative…に分類されます。
この決定木は元の学習データ(ツイート)と、教師データ(そのツイートがポジティブなものかネガティブなものかの答え)によって作成されます。
膨大な量の学習データ、教師データから決定木を作成し、人間がポジティブかネガティブかを教えずにツイートを与え、先ほどの学習によって作られた決定木によってポジティブかネガティブかを判定させる、これを決定木分析といいます。

ランダムフォレストとは?

上記の決定木分析で作られる決定木は一本のみです。しかし、ランダムフォレストでは、この決定木を複数作成します。
そうして得られた複数の決定木から一番判定の精度が良いものを選抜し、ポジティブかネガティブかがわからないツイートの判定に用いる。この学習方法をランダムフォレストといいます。

BOW_tf-idfによる文字→数値表現

ランダムフォレストは、文字を解析することはできません。そのため、文字をうまい具合に数値で表現する必要があります。そこで有効なのが、BOWという表現方法です。BOWの表現方法の中でも、単語の出現頻度・その単語の希少性に着目した数値表現をしてくれる、tf-idfという表現方法を用います。

学習開始!

それではランダムフォレストを使って学習を開始していきます!

プログラム
pos_tweets = get_positive_tweets()
neg_tweets = get_negative_tweets()
tokenized_pos_tweets = tokenize(pos_tweets)
tokenized_neg_tweets = tokenize(neg_tweets)

まず前項のプログラムの関数を総動員して、ポジティブ、ネガティブなツイートを取得、そして整形します。
そうして得られる整形済みツイートをtokenized_pos_tweets,tokenized_neg_tweetsに格納します。

tweet_datas = []
tweet_labels = []

for tw in tokenized_pos_tweets:
  tweet_datas.append(tw)
  tweet_labels.append("positive")
for tw in tokenized_neg_tweets:
  tweet_datas.append(tw)
  tweet_labels.append("negative")

その次に、学習用ツイートデータ配列tweet_datas,教師データtweet_labelsを用意し、先ほど得られた整形済みツイートを格納していきます。tokenized_pos_tweetsのデータが格納されたときはtweet_labelsに”positive”を、tokenized_neg_tweetsが格納されたときは”negative”を格納しています。学習データの順番と教師データの順番が対応するよう格納しています。

train_data, test_data, train_labels, test_labels = train_test_split(
tweet_datas, tweet_labels, test_size=0.2,random_state=0)

そして得られた学習用データ、教師用データを、トレーニングデータ、テストデータに分割します。
トレーニングデータとは、学習モデルを構築するためのデータです。
テストデータとは、構築された学習モデルの精度を判定するためのデータです。
今回がトレーニングデータ:テストデータ=8:2で分割します。

vectorizer = TfidfVectorizer(use_idf=True, token_pattern="(?u)\\b\\w+\\b")
train_matrix =  vectorizer.fit_transform(train_data)
test_matrix = vectorizer.transform(test_data)

BOW_tf-idf表現を用いて、ツイートの各単語を数値で表現します。

clf = RandomForestClassifier(n_estimators=1000,random_state=20,max_depth=75)
clf.fit(train_matrix, train_labels)

ランダムフォレストで先ほどの文字→数値表現データを学習させます。
n_estimatorsは決定木の候補の本数、random_stateはデータの取り出し方や分割方法、max_depthは決定木の深さを決定します。シミュレーションで得られた最も精度の良い値を採用しています。

print("トレーニングデータ精度:",clf.score(train_matrix, train_labels))
print("テストデータ精度:",clf.score(test_matrix, test_labels))

得られた学習モデルの、トレーニングデータ、テストデータの精度を出力します。

test_tweets = ["生まれてきて良かった",
"このツイート最高に面白い!すごくいいね!",
"君のおかげでいろいろ助かった!ありがとう!",
"ごめんなさい今そういう気分じゃないんです",
"最近いろいろつまらんわ",
"なんか何もかもやる気が出ない・・・",
"君が友達で俺は幸せだ!",
"イライラして壁破壊してしまいそう",
"あの人なんか気持ち悪い・・・",
"君のことを考えるだけで目の前が光で包まれるよ!最高だ!"
]
test_tokenized = tokenize(test_tweets)
test_matrix2 = vectorizer.transform(test_tokenized)
test_result = pd.DataFrame()
test_result["テスト対象ツイート"] = test_tweets
test_result["判定"] = clf.predict(test_matrix2)
test_result

試験用に考えた各ツイートの判定結果を表にして出力します。

以上!プログラムでした!

実際に動かしてみた

それでは、完成したプログラムを動かしてみたいと思います!
こちらです!!
f:id:shigashan:20180925155635p:plain

トレーニングデータ、テストデータに対する出力はなかなかなものではないでしょうか!
試験用ツイートの判定もまあまあといったところですね。

考察

誤判定について

今回、試験用ツイートに2つ誤判定がありました。
「君が友達で俺は幸せだ!」
「君のおかげでいろいろ助かった!ありがとう!」
この2つはpositiveとして判定してほしかったのですが、negativeとして判定されてしまいました。

誤ツイート1は、”幸せ”という如何にもポジティブそうな単語があるにも関わらず、negative判定されています。
誤ツイート2は、”ありがとう”とかのフレーズはポジティブっぽいのにnegaitve判定されています。

これらの誤判定の原因を3つ考えました。

原因(予想)1. 単語の学習不足?

今回収集したポジティブツイートの数は15551ツイートですが、その中に”幸せ” “ありがとう” これらの単語が含まれたツイートが少ないため、学習モデルがこれらの単語をポジティブな単語として認識してくれなかったのではないかと考えました。
また、”君”という言葉をネガティブな単語として判定しているのでは?とも感じました。しかし、
「君のことを考えるだけで目の前が光で包まれるよ!最高だ!」というツイートはpositive判定されています。これは、”光” “最高” など、ポジティブ要素が非常に強い単語でネガティブ要素を相殺しているため、positive判定されたのではないか、と考えました。

原因(予想)2. 決定木が不完全?

“幸せ”という単語が、決定木のより先端に近い方に配置されていて、”幸せ”という単語があるかないかの判定をされる前に他の関係のない単語でnegative判定されてしまっているのではないかと考えました。

原因(予想)3. 単語同士の繋がり(文脈)が考慮されていない

今回の学習方法は、単語1つ1つを学習に用いています。
「君のおかげでいろいろ助かった!ありがとう!」
こちらのツイートは、一見ポジティブ感がありますが、このツイートを上のプログラムで整形してみると、
「君 おかげ 助かっ ありがとう」
となり、”ありがとう”以外の単語は、ポジティブともなんともいえない単語です。
そのため、学習不足などにより”ありがとう”がpositive判定されないと、他の単語の影響により、一気にnegative判定の方へといってしまいそうです。

今後の展望

以上の考察から、今後の展望として、
・学習ツイート数を増やす(twitter apiの制限をなんとかしないといけない)
・決定木の再構築(random_stateの値を見直す)
・どうにかして単語同士のつながりも特徴量に含まれるようなアルゴリズムを考える(Word2VecとBOWの併用とか?)

このあたりをなんとかしていけたらいいなと思います!

最後まで読んでいただき、ありがとうございました!