単語をベクトル化する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
UNIX系OSでの .gz ファイルの展開コマンド
たまに使おうとすると忘れていたりするのでメモ
.gz ファイルの展開
$ gunzip file.gz
$ gzip -d file.gz
もともファイルを保持しつつ展開
$ gunzip -k file.gz
$ gzip -dk file.gz
.gz ファイルを展開せずに標準出力
$ gzip -dc file.gz
$ gzcat file.gz
Word Cloud + kuromoji + Processing で日本語文章をビジュアライズ
完成品:
Word Cloud
文章中で出現頻度に重みを付け、その重みに応じた大きさで単語群を図示する手法。
Processing で行うには WordCram が手っ取り早く、デザインも良い。
いかにして日本語文章中の単語を抜き出すか
WordCram に何の手も加えていない日本語文章を読み込ませたところで、日本語単語への分かち書き方法を知らないため、不自然な Word Cloud が出来上がる。
そこで、日本語解析器である kuromoji を使い、対象文章を解析し、名詞を読みこませることにした。
下準備
WordCram
さて、まずは外部ライブラリである WordCram を 手元の Processing にインストールする。
- こちらから Processing 3.0 に対応した WordCram 1.0.0 の zip ファイルをダウンロードし、解凍する。
- Processing の sketchbook フォルダを開く。(
Processing
>Preferences
>Sketchbook location
)
- そのフォルダ内の
libraries
に、解凍した WordCram フォルダをコピーする。
基本的な使い方は次の通り:
import wordcram.*; // Set up the Processing sketch size(1000, 600); colorMode(HSB); background(230); // Make a wordcram from a random wikipedia page. new WordCram(this) .fromWebPage("https://en.wikipedia.org/wiki/Special:Random") .withColors(color(30), color(110), color(random(255), 240, 200)) .sizedByWeight(5, 120) .withFont("Copse") .drawAll();
kuromoji
kuromoji は Java 用のライブラリなので少し手を加える必要がある。
- こちらから zip ファイルをダウンロードし解凍する。
lib
フォルダ内にkuromoji-0.7.7.jar
ファイルがあるので、これをkuromoji.jar
にリネーム。- sketchbook フォルダの
libraries
にkuromoji/libraries
を作成し、先ほどのkuromoji.jar
ファイルを配置する。
基本的な使い方は次の通り:
import org.atilika.kuromoji.Token; import org.atilika.kuromoji.Tokenizer; import java.util.*; Tokenizer tokenizer = Tokenizer.builder().build(); List<Token> tokens = tokenizer.tokenize("吾輩は猫である。"); for (Token token : tokens) { println("PartOfSpeech:" + token.getPartOfSpeech()); println("surfaceForm:" + token.getSurfaceForm()); println("baseForm:" + token.getBaseForm()); }
出力:
PartOfSpeech:名詞,代名詞,一般,* surfaceForm:吾輩 baseForm:吾輩 PartOfSpeech:助詞,係助詞,*,* surfaceForm:は baseForm:は PartOfSpeech:名詞,一般,*,* surfaceForm:猫 baseForm:猫 PartOfSpeech:助動詞,*,*,* surfaceForm:で baseForm:だ PartOfSpeech:助動詞,*,*,* surfaceForm:ある baseForm:ある PartOfSpeech:記号,句点,*,* surfaceForm:。 baseForm:。
日本語文章を Word Cloud でビジュアライズ
これら2つのライブラリを活用して、ホッテントリのフィードを解析、Word Cloud を作成する。
String URL = "http://feeds.feedburner.com/hatena/b/hotentry"; XML[] items; String contents; PrintWriter output; String words; import wordcram.*; import org.atilika.kuromoji.Token; import org.atilika.kuromoji.Tokenizer; import java.util.*; void setup() { size(1000, 600); colorMode(HSB); background(230); XML xml = loadXML(URL); items = xml.getChildren("item"); for(int i = 0; i < items.length; i++){ contents += items[i].getChild("title").getContent(); contents += items[i].getChild("description").getContent(); } Tokenizer tokenizer = Tokenizer.builder().build(); List<Token> tokens = tokenizer.tokenize(contents); for (Token token : tokens) { String[] partOfSpeech = token.getPartOfSpeech().split(","); if (partOfSpeech[0].equals("名詞") && !token.getSurfaceForm().equals("こと")) { words += token.getSurfaceForm() + "\n"; } } output = createWriter("words.txt"); output.println(words); output.close(); drawWordCloud(); } void draw() { } void mousePressed() { drawWordCloud(); } void drawWordCloud() { println("creating a new word clound"); background(230); new WordCram(this) .fromTextFile("./words.txt") .withColors(color(30), color(110), color(random(255), 240, 200)) .sizedByWeight(5, 120) .withFont("Copse") .drawAll(); }
結果はこんな感じ:
WordCram にかける前にある程度意味のない頻出単語をフィルタしたほうが良い。今回は Processing での正規表現がわからなかったので、!token.getSurfaceForm().equals("こと")
のように kuromoji で弾くようにしている。
Rails5 API + devise でユーザーの認証と作成機能を実装した API を作成する
2017/04/26 curl部分に間違いがあったので修正、ついでにログインの必要な動作を追記
目標
Rails v5.0.0 から追加されたapi
オプションを使い、ユーザーの作成と認証機能を実装したベーシックな Rails API を作る
rails new
まずはプロジェクトを作成します
$ rails new devise-api --api --skip-bundle
Gemfile
に次の gem
を追加し, bundle install
gem 'devise' gem 'active_model_serializers'
devise
devise を立ち上げます
$ rails generate devise:install create config/initializers/devise.rb create config/locales/devise.en.yml
User
モデルを作成します
$ rails generate devise User invoke active_record create db/migrate/20160710134334_devise_create_users.rb create app/models/user.rb invoke test_unit create test/models/user_test.rb create test/fixtures/users.yml insert app/models/user.rb route devise_for :users
token 認証
access_token
カラムをユーザーテーブルに追加します
$ rails generate migration add_access_token_to_user
class AddAccessTokenToUser < ActiveRecord::Migration[5.0] def change add_column :users, :access_token, :string end end
サンプルとしてトークンの生成にユーザーのid
とdeviseが生成するトークン自身を使用します。 devise
モジュールの説明についてはこちら
# app/models/user.rb class User < ApplicationRecord # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable and :omniauthable # :recoverable, :rememberable, :trackable devise :database_authenticatable, :registerable, :validatable after_create :update_access_token! validates :email, presence: true def update_access_token! self.access_token = "#{self.id}:#{Devise.friendly_token}" save end end
ユーザー認証のロジックは application_controller
に配置されます。
具体的には、ヘッダーのAuthorization
にてトークンを受け取って、データベースのアクセストークンと照合しています。
# app/controllers/application_controller.rb class ApplicationController < ActionController::API include AbstractController::Translation before_action :authenticate_user_from_token! respond_to :json ## # User Authentication # Authenticates the user with OAuth2 Resource Owner Password Credentials def authenticate_user_from_token! auth_token = request.headers['Authorization'] if auth_token authenticate_with_auth_token auth_token else authenticate_error end end private def authenticate_with_auth_token auth_token unless auth_token.include?(':') authenticate_error return end user_id = auth_token.split(':').first user = User.where(id: user_id).first if user && Devise.secure_compare(user.access_token, auth_token) # User can access sign_in user, store: false else authenticate_error end end ## # Authentication Failure # Renders a 401 error def authenticate_error render json: { error: t('devise.failure.unauthenticated') }, status: 401 end end
sessions_controller
に対して login
ルートを割り当てます
# config/routes.rb Rails.application.routes.draw do devise_for :user, only: [] namespace :v1, defaults: { format: :json } do resource :login, only: [:create], controller: :sessions end end
sessions_controller
は次のようにログインリクエストを処理します。
skip_before_action :authenticate_user_from_token!
によって、認証処理をスキップします。
# app/controllers/v1/sessions_controller.rb module V1 class SessionsController < ApplicationController skip_before_action :authenticate_user_from_token! # POST /v1/login def create @user = User.find_for_database_authentication(email: params[:email]) return invalid_email unless @user if @user.valid_password?(params[:password]) sign_in :user, @user render json: @user, serializer: SessionSerializer, root: nil else invalid_password end end private def invalid_email warden.custom_failure! render json: { error: t('invalid_email') } end def invalid_password warden.custom_failure! render json: { error: t('invalid_password') } end end end
session_serializer.rb
によって、オブジェクトを整形することができます
# app/serializers/v1/session_serializer.rb module V1 class SessionSerializer < ActiveModel::Serializer attributes :email, :token_type, :user_id, :access_token def user_id object.id end def token_type 'Bearer' end end end
ユーザーのサインアップ
次にユーザーの作成プロセスを実装します
# app/controllers/v1/users_controller.rb module V1 class UsersController < ApplicationController skip_before_action :authenticate_user_from_token!, only: [:create] # POST # Create an user def create @user = User.new user_params if @user.save! render json: @user, serializer: V1::SessionSerializer, root: nil else render json: { error: t('user_create_error') }, status: :unprocessable_entity end end private def user_params params.require(:user).permit(:email, :password) end end end
# config/routes.rb Rails.application.routes.draw do devise_for :user, only: [] namespace :v1, defaults: { format: :json } do resource :login, only: [:create], controller: :sessions resource :users, only: [:create] end end
動作確認
データベースを作成、マイグレーション(rake db:create db:migrate
)して、サーバーを立ち上げ(rails server
)ます。
コンソールからユーザーを作成、ログインをテストします。
まずはPOST /v1/users
:
パラメーターをjson形式で送ります。
$ curl localhost:3000/v1/users --data '{"user": {"email": "user@example.com", "password": "mypass"}}' -v -H "Accept: application/json" -H "Content-type: application/json" * Trying ::1... * TCP_NODELAY set * Connected to localhost (::1) port 3000 (#0) > POST /v1/users HTTP/1.1 > Host: localhost:3000 > User-Agent: curl/7.51.0 > Accept: application/json > Content-type: application/json > Content-Length: 61 > * upload completely sent off: 61 out of 61 bytes < HTTP/1.1 200 OK < X-Frame-Options: SAMEORIGIN < X-XSS-Protection: 1; mode=block < X-Content-Type-Options: nosniff < Content-Type: application/json; charset=utf-8 < ETag: W/"fc7ab698e44a0b4687a7d41dc95e8c7a" < Cache-Control: max-age=0, private, must-revalidate < X-Request-Id: 85aa1e62-ac17-4074-b593-7ad45e40e166 < X-Runtime: 0.192480 < Transfer-Encoding: chunked < * Curl_http_done: called premature == 0 * Connection #0 to host localhost left intact {"email":"user@example.com","token_type":"Bearer","user_id":1,"access_token":"1:ZsQVFPi8qvc_zLe2Zjtj"}
ユーザーが作成できたので、次にPOST /v1/login
$ curl localhost:3000/v1/login --data 'email=user@example.com&password=mypass' {"email":"user@example.com","token_type":"Bearer","user_id":1,"access_token":"1:ZsQVFPi8qvc_zLe2Zjtj"}
ちゃんとaccess_token
が返ってくることを確認できました。
認証が必要なメソッドの作成
access_token
を用いて認証しなければ、使うことの出来ないメソッドを作ってみます。
User
コントローラーにindex
メソッドを追加します:
module V1 class UsersController < ApplicationController skip_before_action :authenticate_user_from_token!, only: [:create] def index render json: User.all, each_serializer: V1::UserSerializer end # POST # Create an user def create @user = User.new user_params if @user.save! render json: @user, serializer: V1::SessionSerializer, root: nil else render json: { error: t('user_create_error') }, status: :unprocessable_entity end end private def user_params params.require(:user).permit(:email, :password) end end end
このとき、skip_before_action :authenticate_user_from_token!
には何も追加しないことで、index
メソッドが認証を必要とするメソッドとなります。逆に認証を必要としないメソッドを作成したい場合は追加するようにしましょう。
ルートも忘れずに設定しましょう:
resources :users, only: [:index, :create]
user_serializer.rb
を追加します:
# app/serializers/v1/user_serializer.rb module V1 class UserSerializer < ActiveModel::Serializer attributes :id, :email, :created_at, :updated_at end end
ではここで、コマンドからGET /v1/users
を行ってみましょう:
$ curl localhost:3000/v1/users {"error":"You need to sign in or sign up before continuing."
ちゃんと、application_controller
内のAuthenticate Failureが返ってきています。
次に認証を行ってみましょう。
まずはログインしてaccess_token
を取得:
curl localhost:3000/v1/login --data 'email=user@example.com&password=mypass' {"email":"user@example.com","token_type":"Bearer","user_id":1,"access_token":"1:ZsQVFPi8qvc_zLe2Zjtj"}
そのaccess_token
をヘッダーのAuthorization
に入れ、リクエストを送る:
curl localhost:3000/v1/users -v -H "Authorization: 1:ZsQVFPi8qvc_zLe2Zjtj" * Trying ::1... * TCP_NODELAY set * Connected to localhost (::1) port 3000 (#0) > GET /v1/users HTTP/1.1 > Host: localhost:3000 > User-Agent: curl/7.51.0 > Accept: */* > Authorization: 1:ZsQVFPi8qvc_zLe2Zjtj > < HTTP/1.1 200 OK < X-Frame-Options: SAMEORIGIN < X-XSS-Protection: 1; mode=block < X-Content-Type-Options: nosniff < Content-Type: application/json; charset=utf-8 < ETag: W/"6e18487e56deb9bae7d6fa2bebffc7af" < Cache-Control: max-age=0, private, must-revalidate < X-Request-Id: 1f7e86b8-ff99-45be-a818-69c34605ac62 < X-Runtime: 0.006412 < Transfer-Encoding: chunked < * Curl_http_done: called premature == 0 * Connection #0 to host localhost left intact [{"id":1,"email":"user@example.com","created_at":"2017-04-26T01:44:16.104Z","updated_at":"2017-04-26T01:44:16.104Z"}]
このようにユーザーの一覧がリストになって返ってきていれば正しい動作です。
実際にフロントエンドからリクエストを行う際にはユーザーの作成やログイン後に、access_token
をローカルストレージ等に保存しておくのが良いでしょう。
CORS
フロントエンドのアプリケーションをこの API に対しリクエストを送る形で作成するのですが、通常の設定では別ドメインからのリクエストは拒否されてしまいます。
これを解決するには rack-cors を使ってクロスドメイン通信を許可します。
gem 'rack-cors'
設定を config/initializers/cors.rb
に追加します:
config.middleware.insert_before 'Rack::Runtime', 'Rack::Cors' do allow do origins '*' resource '*', headers: :any, methods: [:get, :put, :post, :patch, :delete, :options] end end
この設定のままではすべてのドメインからのリクエストを許可しているので、セキュリティ面は脆弱です
まとめ
難しいトークン認証も、deviseを用いれば結構簡単に実装できますね。不明点やなにか間違っていること等ありましたら、コメントいただければと思います。
Rails5: データベースがrakeタスクからdropできない時の対処法
問題点
Rails5になってrakeタスクのdrop動作に大きな変更があったらしく, Rails4からアップデートして初めての rake db:drop
がエラーを吐いてしまう.
$ bundle exec rake db:drop Mysql2::Error: Error dropping database (can't rmdir './db_name', errno: 66): DROP DATABASE IF EXISTS `db_name` Couldn't drop database 'db_name'
mysqlから直接ドロップも出来ない
mysql> DROP DATABASE db_name; ERROR 1010 (HY000): Error dropping database (can't rmdir './db_name', errno: 66)
解決策: データベースのファイルを根こそぎ削除する
まずはデータベースの入っているディレクトリを調べる:
mysql> select @@datadir; +-----------------------+ | @@datadir | +-----------------------+ | /usr/local/var/mysql/ | +-----------------------+ 1 row in set (0.00 sec)
あるいは:
$ mysql -e "select @@datadir" +-----------------------+ | @@datadir | +-----------------------+ | /usr/local/var/mysql/ | +-----------------------+
データベースディレクトリに移動して, 該当ファイルを削除する:
$ cd /usr/local/val/mysql/ $ sudo rm -rf db_name
あとはデータベースを作りなおせば良い:
rake db:create