アクトインディ開発者ブログ

子供とお出かけ情報「いこーよ」を運営する、アクトインディ株式会社の開発者ブログです

お手軽にdeployしたかったのでFabricを試してみました

ohataです。

最近子供がバスケットボールを始めて、運動不足解消がてら一緒にバスケしたら全然ついていけず。。。
これからはダンコたる決意で一生懸命バスケの練習をしようと思います!

さて本題ですが、今回はプライベートでちょっとイジったdeployツール fabricについてです。

Fabricって?

Pythonで記述できるデプロイツールです.
Fabric

Rubyで言えばCapistranoと同じような役割なのですが、
今回は以下の条件で、Fabricを使ってみようと思いました。

  • そこまで大したことはしない
  • 構成自体をAnsibleを使っている (Pythonを使っている)
  • アプリケーション自体がjs (Rubyを使ってない)

fabric と言えば 真っ先に思いつくのが こちらだと思います。
アプリ運用するには結構有益なツールですよね。
こちらで言うとfastlaneの役割に近いです。
話逸れましたね、今回はこっちではないです。

準備

Pythonのインストール

Pythonが必要なので、インストールします。
version管理を楽にしたいので pyenv で管理します。

$ brew install pyenv

現在のインストール可能リストから3.7.0を入れます

$ pyenv install --list
$ pyenv install 3.7.0

xcodeが入ってないとエラーになる場合があるので、 その場合は xcode-select --install してください

※ Mojaveの場合に zipimport.ZipImportError: can't decompress data; zlib not available' が出てしまいました。
解消時にこちらを参考にさせていただきました。(感謝です) https://qiita.com/zreactor/items/c3fd04417e0d61af0afe

xcodeの対応状況の問題みたいなので versionアップで解消されるとは思います 現時点(2018/10/26)で最新なのでglobalに設定しました。

$ pyenv global 3.7.0
$ python --version

system内のpythonから切り替わらない事もあるので ~/.bash_profileに以下を追記します

eval "$(pyenv init -)"
$ python --version
Python 3.7.0

これでPythonはOKです

fabricのインストール

前まではPython3で動作するFabricがなくforkされたFabric3というプロジェクトを利用する必要があったのですが、現在は対応しているようなので、本家のプロジェクトを利用します
(ドキドキ)

$ pip install fabric

無事インストールできるのですが、以前はfabric.apiが実行時に動かないという状態があったのでここではまだ安心できません。 自分の環境だけだかもしれませんが、実行してみるとやはりエラーが出てしまいました。 f:id:k-ohata:20181028200536p:plain

今回は、fabric3を利用します

$ pip install fabric3

アプリのビルド

今回は Vue.jsのアプリをビルド&デプロイする場合の設定をしてみました
以下のようなフローで動くように設定していきます。

  1. local環境にdeploy用のディレクトリを作成
  2. githubからclone or pull してくる
  3. vueのbuildを行う
  4. buildファイルをサーバーにあげる

ファイル構成

fabfile
  |- __init__.py
  |- enviroments.py
  |- deploy.py

中身はPythonなので、もっと凝った作りにすることも簡単にできます。

init.py

これは通常のPythonと同じ、利用するモジュールを宣言する箇所です

from fabfile.enviroments import prod, dev
from fabfile.deploy import build, deploy

enviroments.py

環境ごとの設定をここに定義します

from fabric.api import env
from fabric.decorators import task

repo_name = 'hoge' #githubのリポジトリ名
github_url = "git@github.com:hoge/hogehoge.git" #githubのリポジトリ

@task
def prod():
    env.repo_name = repo_name
    env.environment = "prod"
    env.github = github_url
    env.branch = 'master'
    env.deploy_user = 'deploy_user'
    env.deploy_hosts = ['xxx.xxx.xxx.xxx']
    env.work_dir = '/tmp/hoge/prod/'
    env.app_name = 'hoge_vue'

@task
def dev():
    env.repo_name = repo_name
    env.environment = "dev"
    env.github = github_url
    env.branch = 'develop'
    env.deploy_user = 'deploy_user'
    env.deploy_hosts = ['xxx.xxx.xxx.xxx']
    env.work_dir = '/tmp/hoge/dev/'
    env.app_name = 'hoge_vue'    

deploy.py

実行する処理を記載します

from fabric.api import run, abort, env, cd, local, lcd, settings, put
from fabric.decorators import task

class Build(object):

    def web(self):
        app_name = env.app_name
        self.__work_dir()
        with lcd(env.work_dir):
            self.__pull_github()
            with lcd(env.repo_name):
                self.__build_apps()

    def __work_dir(self):
        local('mkdir -p {}'.format(env.work_dir))

    def __pull_github(self):
        with settings(warn_only=True):
            if local('ls -la {}'.format(env.repo_name)).failed:
                local('git clone {}'.format(env.github))
            with lcd(env.repo_name):
                local('git checkout {}'.format(env.branch))
                local('git pull')

    def __build_apps(self):
        with lcd(env.repo_name):
            local('npm install')
            local('npm run build')

class Deploy(object):

    def web(self):
        app_name = env.app_name
        put("{0}/{1}/{2}/dist/*".format(env.work_dir, env.repo_name, app_name), "/var/www/{}/".format(app_name))

    def restart_server(self):
        with cd('/var/www/{}/'.format(env.app_name)):
            run('forever start')

@task #@task ここに記載したものがコマンド実行時のタスク
def build():
    local = Build()
    local.web()
    print("build success!!")

@task
def deploy():
    local = Build()
    local.web()
    remote = Deploy()
    remote.web()
    remote.restart_server()
    print("deploy success!!")

実行

build実行

fab dev build

deploy実行

fab dev deploy

使ってみた感想

ほとんどシェル的な感覚で作れるので学習コストはかなり低いと思います。 Ansibleみたいな冪等性がないので、ちょっと面倒な箇所もありましたが、 結局Pythonなので、抵抗なくすんなり受け入れられました。

軽くサクッとやりたいみたいなときにいいかもしれません

最後に

アクトインディには [バスケ部(非公式)] もあるので、
[先生、コードが書きたいです。。。]
のエンジニアの方、お待ちしています!

Word2Vecを使ってみました!

こんにちは!!こんにちは!! moriyamaです。
今回は、お試しでWord2Vecを触ってみた感想などを記事にします!


いこーよにサジェスト機能を実装したい...

いきなりですが、検索する時って『サジェスト』が便利ですよね!
検索窓に文字を打ち込むと出てくる、奴らです。『関連ワード』なんて呼ばれたりしますね。

検索窓があるWEBサービスで、サジェストが表示されると、とても検索しやすいですね!
ユーザーとして検索しやすいのは嬉しいですが、運営側としてはサジェスト機能を作ること、特に辞書を作ることが非常に大変なんです。

いこーよはSolrを使っているので、部分一致だけならEdge N-Gram TokenizerN-Gram Tokenizer を駆使するだけでいい感じに実装できるかと思います。

そうです。部分一致だけなら実装は容易なのです。
問題になるのは類義語や略語、意味解析です。


特に意味解析が難しい

例えば「雨の日 暖かい」で検索されたとしたら、ユーザーは「屋内」の施設を探していると推測できます。
「屋内」に関係のある"雨"で始まるタグ情報を見ると、下記のようなものがあります。

しかし、"雨"で始まりつつも一概に屋内とは言い切れないタグも存在します。

上記の4例を見ると、"雨"だからといって機械的に「屋内」に関連させることは難しいですし、なにより意味解析自体がとても難しいですね。
そこで目をつけたのが Word2Vec です。


Word2Vec って何?

Word2Vec を説明する際に、よく例示されるのは以下の式ですね。

「king」ー「man」+「woman」=「queen」

「王」から「男性」を引いて「女性」を足せば「女王」になる、というように言葉の意味で計算式を作ることができます。

単語をベクトル化することで定量的に扱うことがきるようになり、単語同士の類似度を出したり、単語間で計算が可能になります。
調べたところ、レコメンドに適応させたり感情分析に活用したりも出来るようです。


問題は学習方法

個人的に機械学習あるあるだと思うのですが、最初にデータソースを揃えるのが頭を悩ませますね。
学習に使う、整ったデータを収集する必要があります。学習する手法によっては大量に必要になりますね。

今回は下記のような手法で、データソースを収集しました。
f:id:setsuna82001:20181011162616j:plain

  • Rails から いこーよ のデータを取得する
  • Solr で形態素解析を実施した後に わかち書き.txt ファイルを作成する
  • Pythonわかち書き.txt ファイルから 学習モデル を生成する

処理としては以上ですね!簡単。


いざ学習!

まずは Rails から解析したいテキストデータを Solr ( Sunspot ) に投げます。

# 解析したいテキスト一覧を取得
text_list = Model.pluck(:dump_text)

# SolrのURLを生成
solr_url = Sunspot.config.solr.url
uri = URI::parse "#{solr_url}/analysis/field"

# ベースになるリクエストパラメータを生成
base_params = {wt: :json, 'analysis.fieldtype' => :text_w2v}

# ファイル開く
File::open("わかち書き.txt", 'w') do |fp|
  # 各テキストの処理
  text_list.each do |text|
    # 1件ずつ処理して解析用エンドポイントを作る
    params = base_params.merge({'analysis.fieldvalue' => text})
    uri.query= URI::encode_www_form params

    # パースして結果を取得
    json = JSON::parse Net::HTTP.get uri rescue next

    # ほしいのは最終行のみ
    analysis = json.dig *%w(analysis field_types text_w2v index)
    nodes = analysis.last.map{|o| o['text']}

    # 空白繋ぎで書き出し
    fp.puts nodes.join(' ')
  end
end

この時使った Solr のフィールド( text_w2v )は下記のようにしています。

<fieldType name="text_w2v" class="solr.TextField" omitNorms="false" autoGeneratePhraseQueries="true" positionIncrementGap="100" >
  <analyzer>
    <!-- 文字置換 -->
    <charFilter class="solr.MappingCharFilterFactory" mapping="mapping-japanese.txt"/>
    <!-- ユーザ辞書の適応 -->
    <tokenizer class="solr.JapaneseTokenizerFactory" mode="search" userDictionary="lang/userdict_ja.txt"/>
    <!-- 品詞種別の基本形変換 -->
    <filter class="solr.JapaneseBaseFormFilterFactory"/>
    <!-- 品詞フィルタリング -->
    <filter class="solr.JapanesePartOfSpeechStopFilterFactory" tags="lang/stoptags_ja.txt" enablePositionIncrements="true"/>
  </analyzer>
</fieldType>

作成したわかち書き.txtword2vec を使って学習させ、学習モデルを生成します。

import os
from gensim.models import word2vec

# 読み込み
data = word2vec.Text8Corpus('わかち書き.txt')

# 学習の実施
model = word2vec.Word2Vec(data, size=100)
model.save('sample_model')

試してみる

今の季節のオススメが知りたいので、下記を試してみました。

「秋」+「オススメ」+「お出かけ」=?

その結果がこちらです!
f:id:setsuna82001:20181011170026p:plain

花見!!
潮干狩り!!

……明らかに季節外れですね。
現状で考えている問題点は下記の3つです。

  • 形態素解析時にデフォルトのユーザー辞書を使用したこと
  • わかち書きの内容を精査してないこと
  • ひらがな・カタカナの表記揺れを放置していること

結局は学習に有用なデータソースをきちんと揃えられてなかったことが最大の要因ですね!
上記を念頭に対策して、再度試して面白い結果が見てみたいですね。


最後に

アクトインディでは一緒にサービスを育てたいエンジニアを募集しています!

actindi.net

Colaboratory + Foliumで地図マッピングしてみる

morishitaです。
Alexa スキル「いこーよのおでかけナビ」では、ユーザの自宅を起点に探したお出かけ先を提案します。

いこーよのおでかけナビ

いこーよのおでかけナビ

  • 発売日: 2019/08/27
  • メディア: アプリ

東京ではたくさんの施設を紹介できますが、そうでない地域もあります。
一応、人口の多い都市ではどれくらいの紹介できる施設の母数があるか確認して、検索範囲や条件を調整しています。

しかし、やはり全国的な施設の分布を感覚知として知っておきたくなり、やってみました。

地図へのマッピング

各施設の緯度経度はわかっているので、それを地図上にマッピングしてみようと思いました。

最初に手っ取り早い手段として、Google マイマップを使ってみました。
やってみてわかったのですが、マイマップは1レイヤー 2,000 ポイントしか登録できないようです。 2,000 件毎に作業を繰り返すにはデータが多かったので別の手段を探すことにしましした。

Leafletを使って HTML と JS でサクッとできるかなとも思ったのですが、 Colaboratory + Folium を使ってもできそうだ思い、どうせならやってみたことがないものでやってみることにしました。

Folium とは?

foliumは Python のライブラリで、Leafletを使ってデータを地図上に可視化できます。
これを Colaboratory 上で使うと、Python 側で作った座標を持ったデータを地図上に簡単にマッピングできます。

Colaboratory + folium をやってみる。

Google Drive 上のスプレッドシートに可視化したいデータを用意します。
それを読み込んで folium で表示した地図上にマーカーとして表示します。

スプレッドシートの読み込み

最初にお決まりの手順、次の2行で Google アカウントの認証を通します。

from google.colab import auth
auth.authenticate_user()

続いて、次のコードで認証されたgspreadオブジェクトを取得します。

!pip install gspread
import gspread
from oauth2client.client import GoogleCredentials

gc = gspread.authorize(GoogleCredentials.get_application_default())

そして次のコードで値を読み取って DataFrameに格納します。
一旦文字列として値を読み込んでしまうので、最後にfloatに変換しています。

spreadsheet_key = '<スプレッドシートのID>'
worksheet = gc.open_by_key(spreadsheet_key).sheet1
table = worksheet.get_all_values()

import pandas as pd
df = pd.DataFrame(table[1:], columns=table[0])
df = df.astype({'id': int, 'lat': float, 'lng': float}) # 型の変換

ここまでで、スプレッドシートからのデータの読み出しは終了。

地図へのマッピング

次のコードだけで、地図を表示して、読み込んだデータをマーカーとしてマッピングしてくれます。

!pip install folium
import folium
m = folium.Map(location=[35.681382, 139.76608399999998], zoom_start=5) #東京駅の緯度経度

for index, data in df.iterrows():
  folium.Marker(location=[data["lat"], data["lng"]]).add_to(m)
m

簡単ですね。

しかし、重い

いこーよの登録施設全部を読み込もうとしました。
スプレッドシートの読み込みは問題ないのですが、地図にマッピングしようとしても一向に表示されません。

データが多すぎるのかなぁと思い、データを間引いて 3,000 件くらいに絞り込んだら、なんとか待っていられる時間に地図が表示されるようになりました。
ただ、表示されるだけでズームやスクロールはまともに動きません。

f:id:HeRo:20180920093710p:plain

1,500 くらいまで減らして、もっさりした動作ですがなんとかズームやスクロールができる状況です。

登録施設が全国に分布しているのはわかります。
わかるけど、そもそも、3,000だの1,500だの、そんな小さい数に間引いたデータを可視化したかったのではないのです。
だいたい、Google マイマップの1レイヤーより少ないやん。

MarkerCluster

folium には Leaflet.markerclusterも組み込まれていることがわかりました。 これで表示するマーカーを減らしてやれば少しはマシにならないかなと思ってやってみました。

f:id:HeRo:20180927205154g:plain

しかし、やっぱり 3,000 件くらいまで減らさないと初期表示で待っていられないほど時間がかかるというのは変わりませんでした。

leaflet を使う限り自分で実装しても変わらないだろうなぁ。

まとめ

  • Colaboratory で Google スプレッドシートを読み込むのは難しくない
  • Colaboratory + folium で地図マッピングも簡単
  • ただし、3,000 ポイント程度まで

結局、QGISを使って当初の目的を果たしたのでした。

最後に

アクトインディではエンジニアを募集しています。

actindi.net

ColaboratoryでBigQueryを検索してみた

morishitaです。

アクトインディでは、ログを Google BigQuery にログを格納しています。 BigQuery は Web コンソールから簡単に検索できます。
Web コンソールはクエリを実行するだけなら便利なのですが、業務に使うには不十分と思っていました。

BigQuery をどう使っているのか?

ログを BigQuery に格納しているので、お客様や他部署からシステム挙動の問い合わせを受けた時に、どのような使われ方がされていたのかを調べるのに利用しています。

調査なのでクエリの検索結果だけでなく、次の項目も合わせて整理しておきたいのです。

  • 調査の目的や経緯
  • 検索クエリ(後で検証が必要になる場合もあり必須)
  • 検索結果
  • 結果から導き出される考察、結論

これまで Redmine 上で上記をまとめつつ、 クエリ実行しては BigQuery の Web コンソールから Redmine にコピー&ペーストする作業をしていました。

これらを1つのドキュメントとしてまとめながら調査できるツールがあれば便利なのにと思っていました。

そんなとき、Colaboratory から BigQuery にアクセスできることを知りました。
これは使えるのではと思い、やってみました。

Colaboratory とは

Google Drive 上で提供されている Google のサービスです。
Jupyter Notobook をベースにしており、ほぼ同じ操作が可能な上に、各種 Google のサービスに接続しやすいように拡張されています。

特徴は次の通りです。

  • Google Drive 上でノートブックを共有できる
  • BigQuery や Google Drive のファイルにもアクセス可能
  • ランタイムは Python2.7 系と Python3.6 系を選択できる
  • TensorFlow はもちろん使えるし、pipでライブラリのインストールも可能
  • GPU が使える
  • そして無料1

Colaboratory については公式ドキュメントを御覧ください。ドキュメントも Colaboratory で作られています。

機械学習の学習に最適

機械学習に注目が集まっており、Jupyter を使って解説する入門書もいくつかあります。 そういった本を写経しながら学習するにも Jupyter の環境を用意するのは面倒ですし、GPU まで用意できないと、機械学習モデルのトレーニングに時間がかかり過ぎて嫌になったりします。

Colaboratoryで注目すべきは GPU が使えることです。
実行環境をまるっと用意してくれる上に GPUまで無料で使えます。 上記のような経験をした機械学習の学習者にも最適なサービスでしょう
(GPU を使うとトレーニングが速いですよー)

さて、このエントリのテーマは機械学習ではなくて、 BigQuery を Colaboratory から使ってみることなので話を戻します。

BigQuery を使ってみる

Python のライブラリが普通にインストールして使えるので pandas.gbqgoogle.cloud.bigqueryでも BigQuery のクエリを実行できます。
しかし、便利なマジックコマンドが用意されているのでそれを利用して実行するのが最も簡単そうです。

初めてのクエリ実行

やってみた結果は次のとおりです。

Colaboratory+BigQuery最初の一歩

ポイントを説明します。

Google アカウントの認証

最初に Google アカウントの認証を通します。 このセルを実行すると URL が表示されます。その URL にアクセスすると認証コードが発行されます。 表示されている入力フィールドに認証コードを入力すると、認証完了です。

from google.colab import auth
auth.authenticate_user()

BigQuery のクエリ実行

マジックコマンド%%bigqueryを利用して BigQuery のクエリを実行します。 引数にはプロジェクト ID と結果を格納する変数名(この例ではdf)を指定します。

%%bigquery --project your_project_id df
SELECT
  COUNT(*) as total_rows
FROM `bigquery-public-data.samples.gsod`

このセルを実行すると検索結果が表示されます。とても簡単にできることがわかりました。

結果を格納している変数の型はpandasDataFrameなので 結果を後で加工しやすそうなところもいいですね。

また、セルにはフォームから値を入力させるインタフェースも簡単に追加できます。
フォームを使えば検索条件や、検索範囲の日付などその都度変わる部分をフォームから入力させることもでき便利そうです。

調査ドキュメントのサンプル

さて、Colaboratory が使えるのではないかと思ったのは、文章とクエリとその結果をひとまとめにした調査ドキュメントが作りやすそうだと思ったことです。
もともと、Jupyter Notobook とはそのようなツールなのでできるでしょう。

いこーよのログはお見せできないので、Google が BigQuery 上で公開している公開データセットを使います。その中から Stackoverflow のデータ使って実際に調査ドキュメントのサンプルを作ってみました。
なお、データは 2018/09/08 に更新されたものです。

作ったものを GIST にアップして、貼り付けたのが次です。

Stackoverflowにはいつ質問すれば早く回答を得られやすいのか

検索してみた結果だけでは面白くないので matplotlibseabornを使ってヒートマップを描いてみました。 BigQueryのWebコンソールでは結果を見ることしかできないですが、Colaboratoryでは結果を加工して分析しやすいですね2

f:id:HeRo:20180918083239p:plain

共有も簡単

Colaboratory のドキュメントは Google Drive 上にあるので社内の共有も簡単です3。 ヘルプには複数ユーザで同時編集しても変更が即時反映されると書いているので、共同作業もやりやすいと思います。

また、すでにこのブログでも貼り付けていますが、Jupyter notebook 同様 Github や Gist にアップして共有することも可能です4

まとめ

  • Colaboratoryは無料でGPUまで使える
  • BigQueryにアクセスするのも容易
  • Google DriveやGistなどで共有も容易

最後に

アクトインディではまだまだデータの活用はまたまだこれからの課題です。 一緒に分析してサービスを成長させてくれるエンジニアを募集しています。


  1. 「研究プロジェクト」という位置づけなので無料と思われます。また、BigQueryなど有料サービスを使うとそれには料金が発生します。

  2. β公開中の新しいBigQueryのWebコンソールでは Data Studioに結果を渡してグラフとか描けるようです。

  3. 弊社では全社で Google Apps を利用しています。

  4. このブログも今日は Colaboratory で書いて全面 Gist 貼り付けにしようかと思いましたが、iframe 内でスクロールが発生して見やすくなかったのでやめました。