読者です 読者をやめる 読者になる 読者になる

サナギわさわさ.json

サナギさんとキルミーベイベーとプログラミングが好きです

OpenRestyとTorch7でREST APIを作る

Torch OpenResty プログラミング

前回の続きですが、Torchのインストール先を/usr/localに変更した都合上、Torchのインストールからやり直しています。 kakakazuma.hatenablog.com

Torchをインストール

今回はOpenRestyからTorchが見える必要があるので、ユーザーディレクトリ配下ではなく/usr/local配下にインストールします。

$ curl -s https://raw.githubusercontent.com/torch/ezinstall/master/install-deps | bash
$ git clone https://github.com/torch/distro.git ~/torch --recursive
$ cd ~/torch
$ vi install.sh
PREFIX="/usr/local" #Torchのインストール先を/usr/localに変更
$ sudo ./install.sh
$ source ~/.bashrc
$ th #Torchのコンソールが立ち上がる事を確認
$ cd /usr/local/lib
$ sudo ln -s libluajit.so libluajit-5.1.so #OpenRestyから読み込む時用のLuaJitシンボリックリンク作成

OpenRestyをインストール

通常ではOpenRestyのインストール時にLuaJitもインストールされるのですが、今回はTorchのインストール時に入れたLuaJitを使うように指定してインストールします。

$ wget http://openresty.org/download/ngx_openresty-1.7.10.1.tar.gz
$ tar xzvf ngx_openresty-1.7.10.1.tar.gz
$ cd ngx_openresty-1.7.10.1
$ sudo apt-get install libreadline-dev libncurses5-dev libpcre3-dev libssl-dev perl make build-essential

ngx_openresty-1.7.10.1/configureを書き換え、コンパイル時にTorchの方のLuaJitを参照するようにします。かなり強引なやり方なので、もう少し綺麗なやり方があると思います。

#env LUAJIT_LIB => $lib;が記載されている2箇所の直後に以下を記述
print "export LUAJIT_LIB='/usr/local/lib'\n";
print "export LUAJIT_INC='/usr/local/include'\n";
$ENV{'LUAJIT_LIB'} = '/usr/local/lib';
$ENV{'LUAJIT_INC'} = '/usr/local/include';
$ ./configure --with-luajit=/usr/local
$ make
$ sudo make install
$ ldd /usr/local/openresty/nginx/sbin/nginx|grep -i lua #LuaJitのパスが/usr/local/lib/libluajit.soになっていることを確認

/etc/init.d/nginxにhttps://gist.github.com/vdel26/8805927を配置

$ chmod a+x /etc/init.d/nginx
$ sudo service nginx start

http://localhost/にアクセスできる事を確認

テストAPI作成

CaffeのImage Net学習済みモデルを使って、画像URLを渡すとImage Netでの分類結果を返してくれるAPIを作ってみます。

CUDA及びloadcaffeのインストール

CaffeのモデルをTorchで使うために、CUDAとloadcaffeパッケージをインストールします。 この辺は前回の記事を参照してください。
Vagrant上のUbuntuでTorch7のCPU開発環境を作る - サナギわさわさ.json

サンプルプログラムの配置

https://github.com/torch/tutorials/tree/master/7_imagenet_classificationのTutoroalをAPI化してみます。 以下の3つのファイルを配置し、sudo service nginx restartを行います。

/usr/local/openresty/nginx/lua/init.lua
rocks = require "luarocks.loader"
torch = require "torch"
path = require 'paths'
cjson = require 'cjson'
require 'loadcaffe'
require 'image'

-- Loads the mapping from net outputs to human readable labels
function load_synset()
  local file = io.open '/usr/local/openresty/nginx/lua/synset_words.txt'
  local list = {}
  while true do
    local line = file:read()
    if not line then break end
    table.insert(list, string.sub(line,11))
  end
  return list
end

-- Setting up networks and downloading stuff if needed
local synset_name = '/usr/local/openresty/nginx/lua/synset_words.txt'
local proto_name = '/usr/local/openresty/nginx/lua/deploy.prototxt'
local model_name = '/usr/local/openresty/nginx/lua/nin_imagenet.caffemodel'
local img_mean_name = '/usr/local/openresty/nginx/lua/ilsvrc_2012_mean.t7'

local synset_url = 'https://raw.githubusercontent.com/torch/tutorials/master/7_imagenet_classification/synset_words.txt'
local prototxt_url = 'http://git.io/vIdRW'
local model_url = 'https://www.dropbox.com/s/0cidxafrb2wuwxw/nin_imagenet.caffemodel'
local img_mean_url = 'https://www.dropbox.com/s/p33rheie3xjx6eu/ilsvrc_2012_mean.t7'

if not paths.filep(synset_name) then os.execute('wget '..synset_url..' -O '..synset_name) end
if not paths.filep(proto_name) then os.execute('wget '..prototxt_url..' -O '..proto_name) end
if not paths.filep(model_name) then os.execute('wget '..model_url..' -O '..model_name)    end
if not paths.filep(img_mean_name) then os.execute('wget '..img_mean_url..' -O '..img_mean_name) end

-- Using network in network http://openreview.net/document/9b05a3bb-3a5e-49cb-91f7-0f482af65aea
net = loadcaffe.load(proto_name, model_name)
net.modules[#net.modules] = nil -- remove the top softmax

-- as we want to classify, let's disable dropouts by enabling evaluation mode
net:evaluate()

img_mean = torch.load(img_mean_name).img_mean:transpose(3,1)

synset_words = load_synset()
/usr/local/openresty/nginx/lua/classify.lua
-- Imagenet classification with Torch7 demo
-- Converts an image from RGB to BGR format and subtracts mean
function preprocess(im, img_mean)
  -- rescale the image
  local im3 = image.scale(im,224,224,'bilinear')*255
  -- RGB2BGR
  local im4 = im3:clone()
  im4[{1,{},{}}] = im3[{3,{},{}}]
  im4[{3,{},{}}] = im3[{1,{},{}}]

  -- subtract imagenet mean
  return im4 - image.scale(img_mean, 224, 224, 'bilinear')
end

local image_url = 'http://upload.wikimedia.org/wikipedia/commons/e/e9/Goldfish3.jpg'
local basepath = '/usr/local/openresty/nginx/lua/tmpimage/'

local args = ngx.req.get_uri_args()
for key, val in pairs(args) do
    if type(val) == "table" then
        val = table.concat(val, ", ")
    else
    end
    if key == "url" then
      image_url = val
    end
end

if image_url ~= "" then
  local tmp_image_name = basepath..ngx.now().."."..paths.extname(image_url)
  os.execute('wget '..image_url..' -O '..tmp_image_name)
  
  print '==> Loading image and imagenet mean'
  local im = image.load(tmp_image_name)

  print '==> Preprocessing'
  -- Have to resize and convert from RGB to BGR and subtract mean
  local I = preprocess(im, img_mean)

  -- Propagate through the network and sort outputs in decreasing order and show 5 best classes
  local score,classes = net:forward(I):view(-1):float():sort(true)
  local resArray = {}
  for i=1,5 do
    local resjson = {}
    resjson["class"] = synset_words[classes[i]]
    resjson["score"] = score[i]
    resArray[i] = resjson
    --ngx.say('predicted class '..tostring(i)..': ', synset_words[classes[i]]..' score : '..score[i])
  end
  ngx.say(cjson.encode(resArray))
end
/usr/local/openresty/nginx/conf/nginx.conf
http {
    init_by_lua_file 'lua/init.lua';
        server {
            location /classify {
                default_type 'text/plain';
                content_by_lua_file "lua/classify.lua";
            }

nginx再起動後、http://localhost/classify?url=任意の画像urlにアクセスし、 分類結果が以下のように帰って来ればOKです。うどんの画像をやってみたら以下のようになりました。

[
    {"class":"soup bowl","score":45.646766662598},
    {"class":"consomme","score":38.72269821167},
    {"class":"caldron, cauldron","score":37.106609344482},
    {"class":"hot pot, hotpot","score":36.95849609375},
    {"class":"mixing bowl","score":36.407737731934}
]

以上です。Image Netの分類は少し使いにくいので、次回はファインチューニングしたいです。