2000名以上の日本人英語学習者のデータからあなたに必要な学習時間を計算してくれるWebアプリ、作りました

エンジニアインターン生として働いているサウスピークにて、新たにWebアプリを作成しました。公開できるものとしては初のアプリですね。

どんなものか

英語学習時間シミュレーター

目標の英語力と現在の英語力から必要な英語時間を算出するWebアプリです。

使い方としては、まず、目標の英語力を選択します。

アルゴリズム的な話をすると、選択肢の英語力はTOEICの点数に換算されます。

現在の英語力の選択肢(下図)もすべてTOEICの点数に換算されます。

で、この2つの情報とサウスピークの卒業生のデータを照らし合わせ、到達に必要な英語学習時間を導き出します。

アプリ内では「一日あたり2時間の勉強で〇〇日の計算になります」と言ってはいますが、"お金を出して留学するほどやる気があり""フィリピン人からのレッスンを受け""英語学習のスペシャリストが作成したカリキュラムに沿って勉強する"サウスピークの生徒のデータから作成しているので、一般的な英語学習者のデータから作成したものと比較すれば、短く見積もられているはずです。なので英語学習の道しるべ程度にどうぞ。

使用したもの

フロントにはReact、サーバーフレームワークはExpressのJSオンリーの構成になっています。

グラフの生成にはRechartを使用。使いやすくて好きなライブラリです。

悩んだところ

SNSのシェアがうまく行かず、やたらと時間を食いました。

当初はreact-helmetで、フロント側で結果の画像毎に動的にOpen Graph用にmetaタグを埋め込んでいました。

<Helmet
  meta={[
    {"property": "og:title", "content": "英語学習時間シュミレーター"},
    {"property": "og:type", "content": "website"},
    {"property": "og:image", "content": imageId},
    {"property": "og:url", "content": "http://simulator.souspeak.com/"}
  ]}
/>

が、駄目。FacebookやTwitterのスクレイパーは正しく読み込んでくれませんでした。

ググってみると、全く同様のことで悩んでいる方がいらしたので、その方の解決法を参考にサーバーサイドで対処しています。ありがとうございます。

気づいたこと

データ活用のアルゴリズムについては一人であーでもないこーでもないと悩みながら作っていたんですが、最初はコードがやたら汚くなってしまっていました。そこらへんもレビューしてもらいながら改善していって、デザインをすごくきれいにしてもらって、作り始めてから1ヶ月少しで公開となりました。

コードが汚いまま公開して、公開日に重大なバグ発見、公開取りやめってのは考えうる中で最悪のシナリオですが、当初はコードをきれいにする必要性を理解してもらえず、早く公開しろ、とせっつかれ、納期に迫られるエンジニアの気持ちが少しわかりました。上司がエンジニアではない場合、デモ見せの時点でコードをきれいにして、バグを見つけやすく保守しやすくしておく方がいいのかも。

もう一つ気づいたこと、というか前から思っていたんですが、プログラマーには基礎的な数学の教養が必要です。

プログラマーは、与えられたデータからほしい結果を得るための計算問題を考えて、それをコードに落とし込むことで、アプリケーションの核であるアルゴリズムを作ります。そのために、最低限の数学の教養、高3までの数学の知識ぐらいは必要ではないかと思いました。

だとするとプログラマーは文系が不利かって言うとそうでもなく、大学で数学やコンピューターサイエンスを修めるような人には人工知能やインターネットの高速化のアルゴリズムなんかを研究してもらって、文系は世界中の数学好きが発表するアルゴリズム(人工知能フレームワークとか高速化されたインフラとか)を、コントリビューションまではいかなくとも、英語と最低限の数学の知識でがんがん吸収して使えるようにして、アプリとして利用できるようにすれば、住み分けができるんじゃないかと。

まあ、プログラマー1年目の意見なので、数年後には真逆のことを言っているかもしれませんが。

最後に

さて、あと数日でプログラミングを学び始めて、2年目に入ります。アプリのアイデアがいくつかあるので、2年目はそれを実現していきます。

npmに代わるNodeパッケージマネージャーyarn

yarn

https://yarnpkg.com/

FacebookとExponent, Google, Tildeとの共同チームによって生まれた新しいパッケージマネージャー

Yarn pulls packages from registry.yarnpkg.com, which allows them to run experiments with the Yarn client. This is a proxy that pulls packages from the official npm registry, much like npmjs.cf.

http://blog.npmjs.org/post/151660845210/hello-yarn

新しいレジストリも作るのかと一瞬思ったけど、どうやらそうではなく、npmレジストリを利用するための新しいクライアントのようだ。よかった。

npmjsのブログを見る限りはyarnのリリースにかなり好意的。でもnpm Inc.がyarn開発からハブられたのはなぜなのか。

特徴

https://bower.io/blog/2016/using-bower-with-yarn/

bowerのブログを参考にまとめる。ただ、yarnとbowerは今は共存してない様子。

Lockfile

ユーザーやデバイス間にまたがるプロジェクトのライブラリのバージョン違いをなくす。npmでいうshrinkwrap機能。チーム開発していると頻発するので嬉しい。

Security

パッケージをインストールする前にパッケージをチェックしてセキュリティを高める。キャッシュを使うのもセキュリティ面で効果があるんだろう。

Offline

洗練されたキャッシュシステムで、パッケージのインストールにかかる時間を大幅に減らし、またオフラインでも使用可能になる。速い。

インストール

npm install --global yarn

pipeasy_installしたのを思い出した。

使い方

npmとのコマンド対応表

https://yarnpkg.com/en/docs/migrating-from-npm#toc-cli-commands-comparison

npm Yarn
npm install yarn install
(N/A) yarn install --flat
(N/A) yarn install --har
(N/A) yarn install --no-lockfile
(N/A) yarn install --pure-lockfile
npm install [package] (N/A)
npm install --save [package] yarn add [package]
npm install --save-dev [package] yarn add [package] --dev
(N/A) yarn add [package] --peer
npm install --save-optional [package] yarn add [package] --optional
npm install --save-exact [package] yarn add [package] --exact
(N/A) yarn add [package] --tilde
npm install --global [package] yarn global add [package]
npm uninstall [package] (N/A)
npm uninstall --save [package] yarn remove [package]
npm uninstall --save-dev [package] yarn remove [package]
npm uninstall --save-optional [package] yarn remove [package]
(N/A) yarn upgrade [package]

Git Large File Storage

参考

Git Large File Storage (Git LFS) とは、大容量ファイルを扱うためのGit拡張。オーディオ・ビデオ・データセット・グラフィクスといったファイルをリモートサーバーに格納し、テクストポインタとしてGitで管理することができる。

環境

OS X El Capitan Version 10.11.6
git version 2.10.0

インストール

$ brew install git-lfs
$ git lfs install

使ってみる

新規プロジェクトの場合

まずはバイナリファイルを用意し、これを管理することにする:

$ touch README.md
$ ls > large.bin

git-lfsで管理するファイルのパターンを設定する:

$ git lfs track '*.bin'
Tracking *.bin

'*.bin'のパターンにマッチするすべてのファイルが対象となる。

git-lfsに管理されているファイルパターンを確認するには、引数無しでgit lfs trackを実行する:

$ git lfs track
Listing tracked paths
    *.bin (.gitattributes)

また、pre-pushフックと.gitattributesが作成されている:

$ cat .git/hooks/pre-push
#!/bin/sh
command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting .git/hooks/pre-push.\n"; exit 2; }
git lfs pre-push "$@"

$ cat .gitattributes
*.bin filter=lfs diff=lfs merge=lfs -text

Herokuなんかはgit-lfsに対応していないので、このpre-pushフックを削除しないとうまくビルドできないので注意。

次にこの.gitattributesをコミットする必要がある:

$ git add .gitattributes
$ git add large.bin
$ git commit -m "Added bin"

git lfs ls-filesコマンドでgit-lfs対象のファイルを確認できる:

$ git lfs ls-files
02c2d7a18e * large.bin

既存のプロジェクトの場合

すでにバイナリファイルをgit-lfsを使わずにコミットしており、途中からgit-lfsで管理したいとする:

$ git init .
$ ls > bar.bin
$ ls > foo.bin
$ git add .
$ git commit -m "initial commit"
$ ls > foo.bin
$ git add foo.bin
$ git commit -m "Second commit"

トラック対象のファイルを定義する:

$ git lfs track '*.bin'
$ git add .gitattributes
$ git commit -m "Now tracking bin files"
$ git tag not_working

対象のファイルを定義した.gitattributesをコミットしただけなので、バイナリファイルがlsfオブジェクトに変換されたわけではない。

すでにコミットしたファイルのキャッシュをクリアし、もう一度コミットし直す:

$ git rm --cached *.bin
$ git add *.bin
$ git commit -m "Convert last commit to LFS"

git lfs ls-filesコマンドで対象のファイルを確認する:

$ git lfs ls-files
4665a5ea42 * bar.bin
4665a5ea42 * foo.bin

最新の履歴のfoo.binファイルの中身を確認すると、lfsオブジェクトに変換されていることがわかる:

$ git show HEAD:foo.bin
version https://git-lfs.github.com/spec/v1
oid sha256:4665a5ea423c2713d436b5ee50593a9640e0018c1550b5a0002f74190d6caea8
size 36

過去(バイナリファイルをコミットし直す前)の履歴のfoo.binファイルでは、まだ変換されていない:

$ git show not_working:foo.bin
bar.bin
foo.bin

過去の履歴のファイルも変換するには、git-lfs-migrateを使うらしい。

単語をベクトル化するword2vec(gensim)を使い、指定した二単語間の関連度を算出する

word2vec

2014年から2015年辺りに流行った、単語をベクトル化して評価する手法。 有名なのは、

king – man + woman = queen

学習データとなるコーパスを準備する

無料かつ簡単に手に入るWikipediaのdumpファイルから持ってきます。

https://dumps.wikimedia.org/jawiki/latest/ の jawiki-latest-pages-articles.xml.bz2 をダウンロードします。

xmlファイルのままでは使えないので、 これをwp2txtを使ってplain.txtに変換します:

$ gem install wp2txt
$ wp2txt jawiki-latest-pages-articles.xml.bz2

ファイルが大量に作成されるので、次のように連結します:

$ cat jawiki-latest-pages-articles.xml-* > jawiki.txt

word2vecでは、単語ごとにスペースで区切られている必要があるので、日本語形態素解析器であるMecabを使って分かち書きします。

まずは、Mecabと標準辞書(IPA)をインストールします:

$ brew install mecab mecab-ipadic

さらにmecab-ipadic-NEologdというカスタム辞書をインストールします:

$ git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git
$ cd mecab-ipadic-neologd
$ ./bin/install-mecab-ipadic-neologd -n

Mecabと辞書のインストールが完了しました。まずはカスタム辞書のインストール先を調べます:

$ echo `mecab-config --dicdir`"/mecab-ipadic-neologd"
/usr/local/lib/mecab/dic/mecab-ipadic-neologd

Mecabの -d オプションにこのパスを指定して、分かち書きを行います:

$ mecab -d /usr/local/lib/mecab/dic/mecab-ipadic-neologd \
-Owakati jawiki.txt > jawiki_wakati.txt

これで学習データの準備が整いました。

word2vecの実装

参考

  • https://radimrehurek.com/gensim/models/word2vec.html
  • http://rare-technologies.com/word2vec-tutorial/
  • http://tjo.hatenablog.com/entry/2014/06/19/233949
  • http://sucrose.hatenablog.com/entry/2013/10/29/001041

Pythonのgensimを使って、word2vecを使用します。cythonを入れると学習時間が短縮されるみたいです。

$ easy_install gensim numpy scipy
$ pip install cython

まずは、学習のためのスクリプトを記述、実行します:

train.py

# -*- coding: utf-8 -*-
from gensim.models import word2vec
import logging

logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)

sentences = word2vec.Text8Corpus('jawiki_wakati.txt')

model = word2vec.Word2Vec(sentences, size=200, min_count=20, window=15)

model.save("jawiki_wakati.model")
$ python train.py

あまりにも時間がかかりすぎる場合はファイルを分割します。最終的に100MBほどに分割して学習させました。

これでモデルができました。ちょっと動作を確かめてみましょう。

similarity.py

# -*- coding: utf-8 -*-
from gensim.models import word2vec
import logging
import sys

logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)

model = word2vec.Word2Vec.load("jawiki_wakati.model")
argvs = sys.argv
print model.similarity(argvs[1], argvs[2])

これを実行します。

$ python similarity.py 日本 フィリピン
2016-09-09 21:57:52,064 : INFO : loading Word2Vec object from jawiki_wakati.model
2016-09-09 21:58:03,569 : INFO : loading syn0 from jawiki_wakati.model.syn0.npy with mmap=None
2016-09-09 21:58:03,956 : INFO : loading syn1 from jawiki_wakati.model.syn1.npy with mmap=None
2016-09-09 21:58:04,573 : INFO : setting ignored attribute syn0norm to None
0.262511504266

学習量が少ないので正確性にはかけるのでしょうが、ちゃんと単語類似度を返しています。

APIとして使う

ほかのプログラミング言語から使用しやすいように、APIとして使えるようにしてみます。

フレームワークにはFalconを使用、WSGIサーバーのgunicornも一緒にインストールします。

$ pip install falcon gunicorn

簡単に、2つのトークンを受け取って、類似度をJSONで返すAPIを作成します:

server.py

# -*- coding:utf-8 -*-
import json
import falcon
from gensim.models import word2vec
import logging

class Server(object):

    def __init__(self):
        self.logger = logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)
        self.model = word2vec.Word2Vec.load("jawiki_wakati.model")

    def on_get(self, req, res):

        s1 = req.get_param('s1')
        s2 = req.get_param('s2')

        content = {
            "similarity": self.model.similarity(s1, s2)
        }

        res.body = json.dumps(content)

app = falcon.API()
app.add_route("/", Server())

サーバーを動かしてみます:

$ gunicorn server:app

次のようにGETリクエストを送ると、JSONで返ってきます:

$ curl "127.0.0.1:8000?s1=日本&s2=フィリピン"
{"similarity": 0.26251150426566316}

Docker

デプロイしやすいようにDockerで動かしてみます。

現在の環境はこれ:

Dockerファイルを追加:

Dockerfile

FROM python:2.7.9

RUN pip install --upgrade pip

WORKDIR /gensim-api
COPY requirements.txt /gensim-api
RUN  pip install -r requirements.txt
COPY . /gensim-api

管理が楽なのでDocker Composeを使用:

docker-compose.yml

api:
    image: gensim-api
    command: gunicorn --reload -b 0.0.0.0:5000 server:app
    volumes:
        - .:/gensim-api
    ports:
        - "5000:5000"

次のコマンドでDockerイメージを作成し、コンテナを起動します:

$ docker build -t gensim-api .
$ docker-compose up

最後に

上記のように、gensimを使えば簡単に単語の類似度が算出できることが分かりました。学習データの準備さえ乗り越えれば、あとはどうってことないと思います。

ソースコードはこちら:

https://github.com/uraway/gensim-api

今のところ、学習量は100MBなんですが、続けて学習させることも可能っぽいので、適度に更新しておきます。

単語の類似度以外にもいろいろ出来そうです。

Atom linter-rubocop でのエラー

linter-rubocop 0.5.0

Error: /Users/uraway/.rvm/rubies/ruby-2.3.1/lib/ruby/site_ruby/2.3.0/rubygems.rb:270:in `find_spec_for_exe': can't find gem rubocop (>= 0.a) (Gem::GemNotFoundException)
    from /Users/uraway/.rvm/rubies/ruby-2.3.1/lib/ruby/site_ruby/2.3.0/rubygems.rb:298:in `activate_bin_path'
    from /Users/uraway/.rvm/gems/ruby-2.3.1/bin/rubocop:22:in `<main>'
    from /Users/uraway/.rvm/gems/ruby-2.3.1/bin/ruby_executable_hooks:15:in `eval'
    from /Users/uraway/.rvm/gems/ruby-2.3.1/bin/ruby_executable_hooks:15:in `<main>'
    at /Users/uraway/.atom/packages/linter-rubocop/lib/index.coffee:57:15
    at process._tickCallback (internal/process/next_tick.js:103:7)

linter-rubocopが参照しているrubocopの場所がどうもおかしい。

とりあえずlinter-rubocopのドキュメントを見ると、解決法が:

rubocopのパスを調べて

which rubocop
/Users/uraway/.rvm/gems/ruby-2.3.1/bin/rubocop

bin/wrappers/に置き変えて、linter-rubocopのexecutablePathにペースト:

/Users/uraway/.rvm/gems/ruby-2.3.1/wrappers/rubocop

エラーはでなくなった。

"KeyError: key not found"

問題

Railsにて、新たに設定した環境変数が機能しない:

myproject/config/initializers/constants.rb:13:in `fetch': key not found: "CONSTANT" (KeyError)

原因

RailsのPreloaderであるspringを入れていたため、環境変数が更新されていなかった。

解決法

$ ./bin/spring stop

Python: プロジェクト別に依存モジュールの管理

requirements.txtのみで管理するのは複雑すぎて無理。 pip-toolsを使って楽に管理しよう。

pip-tools

インストール

$ pip install --upgrade pip
$ pip install pip-tools

pip-compile

requirements.inにプロジェクトに使用するモジュールのリストを作る:

gensim==0.9.1
numpy
scipy
falcon
gunicorn

次のようにコマンドを実行すると、依存モジュールのリストrequirements.txtが作成される:

$ pip-compile requirements.in
#
# This file is autogenerated by pip-compile
# To update, run:
#
#    pip-compile --output-file requirements.txt requirements.in
#
falcon==1.0.0
gensim==0.9.1
gunicorn==19.6.0
numpy==1.11.1
python-mimeparse==1.5.2   # via falcon
scipy==0.18.0
six==1.4.1                # via falcon

pip-sync

requirements.txtに書いてあるとおりに、依存モジュールを環境にインストール・アップグレード・アンインストールする。 npm installbundle installのようなもの。

開発用モジュールを分けて管理しているときは同時に指定する:

$ pip-sync dev-requirements.txt requirements.txt