2018-10-20に更新

bosyuの募集一覧を勝手に作ってみた(機械学習解説付き)

bosyu.meの一覧ページを作成してみました。ちなみに僕はbosyuの運営とは何の関わりもありません。

概要

ちょっと前にbosyuという募集を行うことができるサービスがリリースされました。応募が来るとサービス上で簡単一覧できる便利なサービスです。

ただ、Twitter上で自分のフォロワーさん宛に募集を行う形での利用を想定されて作られている感じのため、募集の一覧ページなどがありません。

そのため、募集一覧ページを勝手に作ってみました。

bosyu.meの募集一覧

ただ、ただほんとに一覧するだけだとTwitter上で#bosyuというハッシュタグを使って見ればいいだけなので、独自の機能としてカテゴリ分類機能を追加し、カテゴリ毎の募集を見られるようにしてみました。(カテゴリの自動分類方法詳細は後述)

環境

  • PHP7.2
  • Laravel5.6
  • Vue
  • Vue Material
  • Twitter API
  • Google Compute Engine(f1-micro)

ツイートの取得

1時間ごとくらいにツイートの保存処理を行っています。

こんな感じで初期化&取得しています。特に複雑な点はなく、基本通りそのままです。URLとハッシュタグで検索しているので、Twitter上での検索よりは精度が高いと思います。TwitterではURLで検索しても検索結果が出てきませんのでここはAPIのみの利点です。

        $connection = new TwitterOAuth(
            env('TWITTER_CONSUMER_KEY'),
            env('TWITTER_CONSUMER_SECRET'),
            env('TWITTER_ACCESS_TOKEN'),
            env('TWITTER_ACCESS_TOKEN_SECRET')
        );

        $params = [
            'q' => 'bosyu.me/users #bosyu',
            'count' => 100,
        ];
        if ($maxId = Tweet::getMaxId()) {
            $params['since_id'] = $maxId;
        }
        $tweets = $connection->get("search/tweets", $params);

下記のような感じでリツイートや既に取得済みのツイートなどをフィルタリングしています。


public static function isAvailableStatus($status) { if (strpos($status->text, '#bosyu') === false) { return false; } if (!empty($status->retweeted_status)) { return false; } $bosyuId = self::getBosyuId($status); if (!$bosyuId) { return false; } return true; }

検索フォーム

Vue Materialで表示を行っているので、検索フォームもVue Materialだけで実装しました。

8ce5b9af-16f5-d75a-536d-b62a6e57971e.png

カテゴリは選択たらすぐに画面が切り替わるのですが、Date Pickerはどうも選択時のcallbackが無いようだったので、仕方なく検索ボタンを押してもらう形にしました。デザインは綺麗なのに肝心な機能が抜けていたりして微妙ですね。誰か本家にプルリクエストを送って実装してください。

別にフォームまるまるコンポーネント化しようとは思ってなかったのですが、上記理由でv-modelを利用した値の取得方法しかなかったため、コンポーネント化されています。全部v-modelで管理してボタンでlocation.hrefするだけのシンプルなコンポーネントです。

<template>
  <div>
    <div class="row">
      <div class="col">
        <md-field>
          <md-select v-model="currentCategoryId" name="category" id="category" placeholder="Category" @md-selected="selectCategory">
            <md-option :value="0">Category</md-option>
            <md-option v-for="category in categories" :value="category.id" :key="category.id">{{category.name}}</md-option>
            <md-option :value="-1">その他</md-option>
          </md-select>
        </md-field>
      </div>
    </div>
    <div class="row">
      <div class="col-5 col">
        <md-datepicker v-model="currentDate" @md-selected="console.log('changed')" />
      </div>
      <div class="col-5 col">
        <md-field>
          <label>Keyword</label>
          <md-input v-model="currentKeyword"></md-input>
        </md-field>
      </div>
      <div class="col-2 col">
        <md-button class="md-icon-button md-raised md-primary" @click="search()">
          <md-icon>search</md-icon>
        </md-button>
      </div>
    </div>
  </div>
</template>

<script>
import * as moment from 'moment';

export default {
  props: {
    categories: {
      type: Array,
      required: true
    },
    categoryId: {
      type: Number,
      default: 0
    },
    date: {
      type: String,
      default: null
    },
    keyword: {
      type: String,
      default: ''
    }
  },

  data() {
    return {
      currentCategoryId: this.categoryId,
      currentDate: this.date === null ? null : moment(this.date).toDate(),
      currentKeyword: this.keyword
    }
  },

  methods: {
    selectCategory(categoryId) {
      this.currentCategoryId = categoryId;
      this.search();
    },

    search() {
      let parts = [];

      if (this.currentCategoryId !== 0) {
        parts.push(`category_id=${this.currentCategoryId}`);
      }
      if (this.currentDate !== null) {
        parts.push('date=' + moment(this.currentDate).format('YYYY-MM-DD'));
      }
      const keyword = this.currentKeyword.trim();
      if (keyword !== '') {
        parts.push('keyword=' + encodeURIComponent(keyword));
      }

      location.href = '/?' + parts.join('&');
    }
  }
}
</script>

機械学習によるカテゴリの自動分類

今回作るサービスの肝です。これがなければTwitterで情報を見ることができるのでほとんど意味がなくなってしまいます。

とりあえずざっと思いついた流れは下記のようなものです。

  • カテゴリをいくつか作る。
  • あらかじめツイートを集めて学習させる。
  • 機械学習の結果を用いて取得したツイートの本文でカテゴリ分けする。

この機能はそのまま クラス分類(Classification) というらしいです。機械学習としては恐らくよく用いられる分類だと思いますので、既にいくつも簡単に実装できるAPIやライブラリなどが数多く見つかりました。せっかくなので見つけて検討した順番にここで紹介していきます。

Google Cloud Natural Language API

Google Cloud Natural Language

とりあえずGoogleなら何かあるだろ、と思って調べたら全くそのままのものがありました。これのclassifyTextメソッドを利用した「コンテンツの分類」という機能になります。しかもこれは学習させる必要がなく、予めGoogle側で定義されているカテゴリに分けてくれるというすぐれものです。しかもツイートであれば3万件ほどまで無料(多分)。

しかし試してみましたが、下記の問題がありました。

  • 日本語対応してない
  • 短い文章は無理

ということで一番手軽だったのですが、これは諦めることになりました…。

A3RT Text Classification API

A3RT Text Classification API

よくわからないのですがRecruitが無料で公開しているAPIです。こんなすごいものを無料で公開し続けるというのはいまいち良く分からないです。APIになっているのでインフラも無料では済まないでしょうし。どうなってるんだろう…。

機能的にはこれで問題なかったのですが、とりあえず最初にGoogleのAPIを知ってしまったので学習が要らないAPIを引き続き探しました。このAPIはデフォルトで求人関連のモデルが入っているので、その他の分類を行うためには自分でモデルを作る必要があります(簡単ですがデータを用意する必要があります)。

Watson Natural Language Classifier

Watson Natural Language Classifier (自然言語分類)

これもA3RTと一緒だと思います。IBMが公開しているもので有料ですが無料枠があります。A3RTと同じ理由で次を探しました。

fastText

facebookresearch/fastText: Library for fast text representation and classification

こちらはfacebookが公開しているもので、A3RT、Watsonと同じクラス分類の機能があります。

なんかもうこれでいいんじゃないかな…と思ってこれを試しました。色々探しましたがGoogleのもののように良い感じに最初からカテゴリ分けされてるっぽいものはなさそうですし、ツイートであれば学習データを集めるのは特に難しいことではないし、学習させるというのも大事なことだと思ったためです。

また、下記のようなわかりやすい記事を見つけたのも試そうと思ったきっかけです。 機械学習で大量のテキストをカテゴリ別に分類してみよう!

具体的には

  1. 学習データのテキストファイルを作成
  2. テキストをモデルに変換
  3. モデルによりテキストを分類

という感じで非常に簡単をクラス分類を行うことができるライブラリになっており、割とすんなり実装できました。

ちなみに僕は結局途中からpipで準備するのが面倒になったのでpythonもやめて直接コマンドを叩く形で実装しました。

モデル作成

./fasttext supervised -input data.txt -output model

クラス分類実行

./fasttext predict-prob model.bin 分類する文章が保存されているテキストファイルのパス

試してみた

なんか、うまくいきませんでした。ツイートなので文章が全体的に短すぎるのか、何時間かしかかけずに集めたデータが少なすぎたのか、おかしなカテゴリに入ってしまうものが多発しました。

ということでここまで調べて実装しましたが機械学習でカテゴリ分けをするのはやめました

実際に行ったカテゴリ分けの方法

ツイートに含まれるキーワードによりカテゴリ分けをする方法です。ひとつのカテゴリにいくつでもキーワードを設定できるようにしました。非常に的確にカテゴリ分類ができるようになりました。しかも非常に簡単です。そして非常にダサいです。

でも、今回の場合は募集ツイートに限るものだし、さほどバリエーションがあるとは思えません。文章も短く、精度も低くなりがちです。こういう場合にはわざわざ機械学習を導入せず、シンプルな方法を選択することも必要な場面はあると思います。今回はそれで良さそうな気がしています。分類されないものは全部「その他」カテゴリに入れています。

よろしければ実際に見てみてください。本家が一覧ページを作ったら消え去る運命のサイトです。

bosyu.meの募集一覧


dala00

Crieitの開発者です。 主にLAMPで開発しているWebエンジニアです(在宅)。大体10年程。 業務依頼、同業種の方からのコンタクトなどお気軽にご連絡ください。 業務経験有:PHP, MySQL, Laravel5, CakePHP3, JavaScript, RoR 趣味:Elixir, Phoenix, Node, Nuxt, Express, Vue等色々

Crieitはαバージョンで開発中です。進捗は公式Twitterアカウントをフォローして確認してください。 興味がある方は是非記事の投稿もお願いします! どんな軽い内容でも嬉しいです。
なぜCrieitを作ろうと思ったか
関連記事

コメント