わ〜い! 自然言語処理ごっこ

複数の小規模なコーパスを用いた、Web議論掲示板における投稿の自動分類の研究についてのブログ。

Qiitaの記事における「いいね!」の数を色々な情報から自動で推定する

この記事は、以前使用していたブログのエントリの移植です。

はじめに

背景

近年、Qiitaはエンジニア間の情報共有インフラとして有効に活用されているシステムの一つである。

しかし、Qiitaに投稿された記事のうち非常に有用な記事は限られる。

そのため、Qiitaにおける良い記事を探す方法の一つとして、「いいね!」の数が多い記事のみを閲覧するという方法がある。

しかし、投稿されて間もない記事は「いいね!」が付くまでに時間が必要であり、新しい記事に対する「いいね!」の数の推定が必要である。

目的

本記事では、Qiitaに投稿された記事における「いいね!」の数を機械学習で推定することを目的とする。

原理

機械学習とは、人間が既知データから学習して未知データへの推定を行う一連の流れを、機械にやらせようというものである。

教師あり学習・教師なし学習

機械学習の問題は以下の3つに分けられる。

  • 教師あり学習
    事前に学習用のデータが与えられる問題
  • 教師なし学習
    事前に学習用のデータが与えられない問題
  • 強化学習

今回は、Qiitaからスクレイピングしたデータを用いて、教師あり学習を行う。

分類と回帰

また、機械学習の問題の分け方として、次のようなものもある。

  • 分類
    各データをクラス別に分類する問題
    例えば「この果物は、みかんか?りんごか?それ以外か?」など
  • 回帰 各データのスコアを推定する問題
    例えば「この学生の顔面は何点か?」「この学生の年齢はいくつか?」など

今回はQiitaの記事における「いいね」の数を推定するので、回帰問題である。

結果とお気持ち

実装なんてわざわざ見るのも億劫だと思うので、先に結果から示す。 色々な特徴素を使って「いいね!」の数を推定した結果、次のような結果が得られた。 なお、「予測値と実測値の相関係数」は、高ければ高いほどうまく推定できていることを示している。

用いた特徴素 予測値と実測値の相関係数
投稿日時 -0.046
タグ名 0.18
タイトル 0.23
タイトル+タイトルの長さ 0.24
タイトル+タイトルの長さ+タグ+タグの数 0.26

意外にも、投稿日時からは「いいね!」の数は予想できない事が分かった。
また、タグよりもタイトルの方が「いいね!」の数を予想する上で重要であることも分かった。
タグをちゃんと設定した方が「いいね!」が伸びるのは事実だが、結局はタイトルで読まれるかどうかが決まるわけだから、Qiitaで承認欲求を満たしたいならば一番気にかけるべきなのはタイトルである、ということを示しているのかもしれない。

今後の展望としては、記事に含まれている情報として本文の情報を用いたり、投稿者の投稿記事数や読者数などといった記事に含まれていない情報を用いることでより高い精度を望めると考える。

なお、この記事をQiitaにもし投稿した場合の「いいね!」の数は34個と推定された。本当にそんだけもらえたら苦労しねぇよ。

実装

スクレイピング

スクレイピングについてはこの記事を参考にした。 qiita.com

ただし、参照先の記事では取得したデータを加工しているが、本記事では加工せず、以下のようにそのままJSON形式で保存することとした。 また、今回は本記事投稿日の3ヶ月前(2016/09/24)から10万件の記事を取得した。

[
    {
        "created_at_as_seconds": 1474728608,
        "title": "【初心者向け】Swift3で爆速コーディングその3(ボタンクリックとイベント)",
        "tags": [
            {
                "following": false,
                "icon_url": "https://s3-ap-northeast-1.amazonaws.com/qiita-tag-image/7d705cc5b20b094a52f4a328019a74d180ddb1f9/medium.jpg?1474336232",
                "name": "iOS",
                "url_name": "ios"
            },
            {

                "following": false,
                "icon_url": "https://s3-ap-northeast-1.amazonaws.com/qiita-tag-image/8924010780db484a83145542a3e49c6c2084ecb7/medium.jpg?1401738498",
                "name": "Swift",
                "url_name": "swift"
            },
            {
                "following": false,
                "icon_url": "https://s3-ap-northeast-1.amazonaws.com/qiita-tag-image/237a736fdb6ee6fa662330aa795600f26cf7f7af/medium.jpg?1426773533",
                "name": "playground",
                "url_name": "playground"
            },
            {
                "following": false,
                "icon_url": "//cdn.qiita.com/assets/icons/medium/missing.png",
                "name": "Swift3.0",
                "url_name": "swift3.0"
            },
            {
                "following": false,
                "icon_url": "//cdn.qiita.com/assets/icons/medium/missing.png",
                "name": "Xcode8",
                "url_name": "xcode8"
            }
        ],
        "comment_count": 0,
        "user": {
            "following": false,
            "profile_image_url": "https://qiita-image-store.s3.amazonaws.com/0/55077/profile-images/1478700761",
            "url_name": "teradonburi",
            "id": 55077
        },
        "private": false,
        "uuid": "381aabdf547977953d48",
        "id": 425552,
        "tweet": true,
        "stock_count": 7,
        "stocked": false,
        "gist_url": null,
        "created_at_in_words": "3ヶ月",
        "updated_at": "2016-09-26 08:35:06 +0900",
        "liked": false,
        "url": "http://qiita.com/teradonburi/items/381aabdf547977953d48",
        "public_likes_count": 8,
        "updated_at_in_words": "3ヶ月",
        "created_at": "2016-09-24 23:50:08 +0900"
    },
]

学習と推定

オンライン学習のためのライブラリ「Jubatus」を用いて推定する。

Regression チュートリアル (Python) — Jubatus

Jubatusでは、おおよそ

  1. クライアント側でデータを整形してサーバ側へ送信
  2. サーバ側で学習器に学習させたり推定させたりする
  3. サーバ側が結果をクライアント側に返す

という流れで処理が行われる。 本記事では得られた記事データのうちの90%を教師データ(学習させるためのデータ)とし、残りの10%をテストデータ(性能を測るためのデータ)とした。 得られた予測値と実測値を比較し、相関係数を求めた。

クライアント側のソースコードclient.py およびサーバ側の設定ファイルserver_config.jsonは以下の通りである。 以下のコードを次のように実行すればおそらく動く。

$ jubaregression --configpath config.json 
$ python client.py

client.py

#!/usr/bin/env python
# -*- coding:utf-8 -*-

import json
import random
import numpy as np

from jubatus.common import Datum
from jubatus.regression.client import Regression
from jubatus.regression.types import *

from prettyprint import pp

def split_articles(articles):
  index_to_parse = int(float(len(articles))*0.9)
  random.shuffle(articles)
  train_articles = articles[:index_to_parse]
  test_articles = articles[index_to_parse:]

  return (train_articles, test_articles)

def article2data(article):
  feature = Datum({
    'title': article['title'], # 記事タイトル
    'tags': " ".join([tag['name'] for tag in article['tags']]), # 記事タグ
    'created_at_as_seconds': float(article['created_at_as_seconds']) # 投稿日時
  })
  score = float(article['public_likes_count'])
  return (score, feature)

def main():
  # サーバとつなぐためのクライアント
  client = Regression('127.0.0.1', 9199, '')

  # スクレイピングして得たデータを開く
  articles = json.load(open('articles.json', 'r'))

  # 記事データを教師用データとテスト用データに分割
  train_articles, test_articles = split_articles(articles)

  # 学習を行う
  for a in train_articles:
    datas = [article2data(a)]
    client.train(datas)


  pred_result = [] # 予測値
  true_result = [] # 実測値

  # 推定を行う
  for a in test_articles:
    score, feature = article2data(a)
    result = client.estimate([feature])

    # 予測結果をぶちこむ
    pred_result.append(result[0])
    # 実際のいいねの数をぶちこむ
    true_result.append(score)

  # 相関係数が出力される
  print np.corrcoef(pred_result, true_result).tolist()[0][1]

if __name__ == '__main__':
    main()

server_config.json

{
  "method": "AROW",
  "converter": {
    "num_filter_types": {},
    "num_filter_rules": [],
    "string_filter_types": {},
    "string_filter_rules": [],
    "num_types": {},
    "num_rules": [
      { "key": "*", "type": "num" }
    ],
    "string_types": {
      "mecab": {
        "method": "dynamic",
        "function": "create",
        "path": "/usr/local/lib/jubatus/plugin/libmecab_splitter.dylib",
        "arg": "-d /usr/local/lib/mecab/dic/ipadic",
        "base": "true",
        "include_features": "*"
      }
    },
    "string_rules": [
      { "key": "title", "type": "mecab", "sample_weight": "bin", "global_weight": "bin" },
      { "key": "tags", "type": "space", "sample_weight": "bin", "global_weight": "bin" }
    ]
  },
  "parameter": {
    "sensitivity": 0.1,
    "regularization_weight": 0.1
  }
}