サービスの概要とモチベーション
この度、Go,Nuxt,Python(機械学習)を用いてアプリケーション開発をしました。
作成したサービスは映画れこめんでーたーという映画推薦サービスです。
私はよく、映画を観る前にYahoo映画などで映画の評価を確認します。2時間近い時間を使うのに失敗したくないからです。レビューを見てしまうとネタバレになるので点数だけを参考にするのですが、評価の高い映画でも「なんか自分が観たかった映画と違った」ということがしばしばあります。レビュワーがその映画を評価しているポイントがバラバラだからです。
そんな時、レビューを見ずに、「レビューの文章から映画を検索できたらいいのにな。。。」と思いました。
この思いから開発したのが「映画れこめんでーたー」です。
映画れこめんでーたーは映画評価サイトであるIMDBの映画ランキングトップ250にランクインしている映画情報を収録しています。この中からおすすめ映画を提案します。
具体的には、入力した文章を文ベクトルに変換し、DB内の映画レビュー情報に対してコサイン類似度を計算し、類似度の高い映画トップ10を提案する、という仕組みになっています。
映画レビューは、各映画につきAmazonPrimeのレビュー上位10件を取得しDBに保存しています。
開発概要
インフラ
サーバーサイドはAWSのEC2を利用しています。インスタンスタイプはt3a.smallです。
本来はGPU付きでメモリももっと大きいインスタンスを選択したかったのですが、予算の関係でこのインスタンスにしています。
フロントエンドはFirebase Hostingを使用しています。
DB
MySQLを利用しています。
本当はElasticsearchを利用したかったところですが、Elasticsearchはメモリ使用量が大きいということで、普通にMySQLを使用することにしました。
クローラー
IMDB映画ランキングトップ250のAmazonPrimeのURLを取得し、AmazonPrimeのレビュー上位10件を取得するクローラーを開発しました。クローラーはRubyで作成しました。
文ベクトル生成API(sentence-vector-generator)
こちらになります。バックエンドからこのAPIと通信し、入力した文章を文ベクトルに変換します。送信されてきた文章を文ベクトルに変換するだけの単純なAPIです。
Pythonを使用して、フレームワークはFastAPIで作成しています。また、文ベクトルの生成にはSentence BERTを使用しています。
バックエンド(movie_searcher)
こちらになります。APIとの通信やコサイン類似度の計算等はここで行っています。
Go言語を使用し、フレームワークはEchoで作成しています。
フロントエンド(movie_searcher_front)
こちらになります。Vue.jsを使用して、フレームワークはNuxt.jsで作成しています。
アーキテクチャ図
ER図
ER図は以下のようになっています。非常に単純で映画情報をmovieに、レビューの情報をreviewsに保存しています。reviewsのsentence_vectorカラムがJSON型カラムでそのレビュー文章のベクトル情報を保管しています。
movieのaverage_vectorがそのレコードに紐づくreviewsのsentence_vectorの平均値を保管しているカラムです。
開発手順
クローラーの開発
↓
IMDB及びAmazonをクロールしてデータ取得
↓
文ベクトル生成API開発
↓
レビューの文章を全て文ベクトルに変換して保存
↓
映画ごとにレビューの文ベクトルの平均ベクトル計算して保存
↓
バックエンド開発
↓
フロントエンド開発
↓
デプロイ
使用技術、各技術の選定理由
インフラ(サーバーサイド)
サーバー:AWS EC2
インスタンスタイプ:t3a.small
ボリュームサイズ:8Gib
OS:Amazon Linux2
今回はできるだけ予算を抑えて、サーバー上にDBもアプリケーションも載せたかったのでEC2を選択しました。t3a.smallを選択した理由も予算です。
今回開発したアプリケーションはSentence BERTを使用して文ベクトルの生成を行うので、高速化を考えると本来はGPU付きでメモリがもっと大きいインスタンスを選択したかったです。
ただ、GPU付きインスタンスは個人で利用する上では高額でだったので、予算の範囲内で最大限の性能を出せるt3a.smallを選択しました。
インフラ(フロントエンド)
ホスティングサービス:Firebase Hosting
Firebase Hostingを使用してみました。選定理由は以下の2つです。
①自動でhttps化されたドメインを用意してくれる
デプロイした段階で自動でhttpsなドメインを用意してくれます。SEOなど考えると独自のドメインを用意した方がもちろん良いですが、個人開発サービスの初期としてこれで十分だと思いました。
②簡単にGItHubActionsを使用した自動デプロイ環境が構築できる
非常に簡単に自動デプロイ環境が構築できました。
DB
システム:MySQL
環境:docker-compose
本当はElasticsearchを使ってみたかったです。Elasticsearch7.0からはベクトル型のカラムを格納することができるようになっており、Elasticsearch7.3からはElasticsearch内でコサイン類似度を計算できるようになっているようです。
コサイン類似度の計算をDB側で済ませられるのはとても魅力的でしたが、t3a.smallでdocker-composeを使用しElasticsearchを使用しようとしたところ落ちてしまったので断念しました。
クローラー
言語:Ruby
IMDBおよびAmazonのクロールはRubyでクローラーを作成して行いました。普段Rubyを使用しており、直感的に作成しやすかったためRubyを選択しました。また取得したデータをActiveRecordを使用して簡単にDBに保存できるのも良いポイントでした。
文ベクトル生成API(sentence-vector-generator)
言語:Python
フレームワーク:FastAPI
文ベクトル生成モデル:Sentence BERT
環境:docker-compose
まず、文ベクトルの生成にSentence BERTを使用した理由は日本語情報の多さと精度です。
日鉄ソリューションズ株式会社さんが日本語モデルを公開してくれており、すぐに使うことができました。また文ベクトルを生成する方法としてFasttextなどで単語ベクトルを生成してそれらの平均を文ベクトルとする方法がありますが、この方法と比べて文脈を考慮した文ベクトル生成ができるSentence BERTは精度が高く、こちらを採用しました。
FastAPIは非常に簡単にPythonのAPIを作成できるということだったので採用しました。実際に、非常に簡単にAPIを作成できました。
バックエンド(movie_searcher)
言語:Go言語
フレームワーク:Echo
ORM:Gorm
Go言語を選定した理由は4つです。
①Goを使ってみたかった。勉強したかった。
Goを使って開発がしたかったです。
②非常に高速である
今回のアプリケーションはコサイン類似度の計算など時間のかかる処理があります。そこを可能な限り高速化したく、インタプリタ言語であるRubyではなくコンパイル言語であるGoを採用しました。
③Goルーチンで並行処理のプログラムが簡単に組める
②と少し被りますが、サーバーサイドの処理時間をできるだけ短くする手段としてGoルーチンを使えるGoを採用しました。実際に非常に強力だと思いました。
④httpsなサーバーを簡単に作成できる
Goの標準機能としてLet’s Encryptを利用したhttps通信を実装できる機能があることは開発の軽減という点からも非常に魅力的でした。
Echo、Gormは使わないで標準ライブラリだけで実装してもよかったのですが、今後のための勉強と思い、使ってみました。
フロントエンド(movie_searcher_front)
言語:Vue.js
フレームワーク:Nuxt.js
状態管理:Vuex
デザイン:Vuetify
すべて実務で使用実績があったため採用しました。Nuxtはルーティングの設定やコンポーネントのimportが不要でVue.jsの面倒な部分を軽減できるため、今回も採用すべきだと考えました。
工夫した点
①文ベクトルの平均を計算
DBでは1件の映画につき10件のレビューデータを保存しています。約2500件のレビューデータに対してコサイン類似度を計算しているとメモリ使用率が大きく、かつ計算に時間がかかりレスポンスが遅くなることが懸念されたため、各映画ごとに「10件のレビューの文ベクトルの平均」を計算しDBにあらかじめ保存するようにしました。
この平均の文ベクトルに対して類似度計算を行うことで対象データ数が約250件となり、レスポンスの速度を落とさないように工夫しました。
①Goルーチンを利用して検索処理を高速化
Goルーチンを使用して、入力文章を文ベクトルに変換する処理とデータベースから各映画の平均文ベクトルデータを取り出す作業を並行処理化しました。
具体的には以下のように実装しています。
var wg sync.WaitGroup
wg.Add(2)
// 入力文を文ベクトルに変換する
ch_vec := make(chan []float64)
go nlp.FetchSentenceVector(request.Text, &wg, ch_vec)
input_vec := <-ch_vec
// DBからMovieの全データを取得する
ch_db := make(chan []movie.Movie)
dbs := c.Get("dbs").(*middlewares.DatabaseClient)
go getAllMovies(dbs, &wg, ch_db)
movies := <-ch_db
wg.Wait()
これにより、当初5.3秒かかっていた検索部分の実行時間を3.9秒に短縮することができました。
Goルーチン使用前↓
goos: darwin
goarch: amd64
pkg: movie_searcher/controllers/api
cpu: Intel(R) Core(TM) i5-1038NG7 CPU @ 2.00GHz
BenchmarkFetchSimilarMovies-8 1 5357443573 ns/op
PASS
ok movie_searcher/controllers/api 5.739s
Goルーチン使用後↓
goos: darwin
goarch: amd64
pkg: movie_searcher/controllers/api
cpu: Intel(R) Core(TM) i5-1038NG7 CPU @ 2.00GHz
BenchmarkFetchSimilarMovies-8 1 3919489945 ns/op
PASS
ok movie_searcher/controllers/api 4.178s
デモンストレーション
文章を入力して検索をクリックします。
泣けそうな映画を出すことができました。スコアはコサイン類似度です。
課題、改善ポイント
①レコメンド精度向上
今はレビューの文ベクトルの平均に対してコサイン類似度を計算しているため、本来は類似度が高くなるレビューを持つ映画が提案されない状況です。
レビューの文ベクトルの平均に対してコサイン類似度を計算するのではなく、レビューの文章に対してコサイン類似度を計算するようにしたいです。
ただこの場合、計算量が大きくなってしまう課題があります。その対策として、 あらかじめ全レビュー文章をクラスタリングしておき、入力文章と同じ分類のレビューのみに対して検索をかけるなどしたいです。
②高可用、高セキュリティアーキテクチャ化
マルチAZにし、EC2を2台立て冗長化構成を取りたいです。またパブリックサブネットとプライベートサブネットをわけ、プライベートサブネットにEC2を配置、パブリックサブネットにはELB
を配置し負荷分散を図りたいです。
③サーバー性能強化
今は文ベクトルを生成するアプリケーションもCPUで動かしています。GPU付きインスタンスにしてより高速に文ベクトルを生成できるようにしたいです。