AWS Lambda with EFS で MeCab, NEologd & CaboCha を動かす （2020年7月時点）

# はじめに
過去に以下のような記事を書きました。
[AWS Lambda で MeCab を動かす (2018年9月時点）](https://qiita.com/rtaguchi/items/e6cb594196fa8186e9a2)

この記事で最後に
>「Neologdも入れたかったけれども、Lambdaのtemp領域の制限でアウト。
>（中略）
> そうこう言っている間にLambdaのtemp領域の制限が一気に3GBくらいになってくれないかな。

と書いて終わりました。それから約2年。Lambda の temp 領域制限は変わりませんが、代わりに（？）Lambda から EFS にアクセスできるようになりました。これでようやく念願の NEologd を手軽に Lambda 上で動かせるようになりましたので、その方法をまとめます。

まぁ、ホントに EFS に MeCab と NEologd を突っ込むだけなので特別なことはしていません。

# NEologdとは
正式には`mecab-ipadic-NEologd`という名称で、多数のWeb上の言語資源から得た新語を追加することでカスタマイズした MeCab 用のシステム辞書です。
（詳しい説明は GitHub の [README](https://github.com/neologd/mecab-ipadic-neologd/blob/master/README.ja.md) を参照ください。）

Lambda で外部のライブラリを利用する場合、そのライブラリを Lambda 関数と一緒に Zip で固めてアップロードするか、もしくは Lambda Layer を利用する必要があります。そして、Lambda 関数、Lambda Layer ともに容量に制限があり、基本的にはサイズの大きいライブラリを使うことは難しい状況でした。特に NEologd は新語辞書なので 1GB 以上の容量があり、到底 Lambda で取り回せるサイズではありませんでした。

このサイズ制限の問題を EFS を利用することによって回避することがこの記事の目的です。

なお、EFS を Lambda から利用する場合、Lambda は VPC 内に置く必要があります。なんとなく VPC が絡む時点で、以前ほど気軽ではなくなってしまう気がしますが、EFS を使う以上避けられないのです。

# MeCab&NEologd を動かす
## EFS を作成する
パフォーマンスモードやネットワークアクセス、ファイルシステムポリシーなど真面目にやると設定する項目はたくさんありますが、とりあえず MeCab と NEologd を動かすことが目的なので全てデフォルト設定で作成します。VPC もデフォルトのものをそのまま使います。

## EFS のアクセスポイントを作成する
Lambda からアクセスするためのアクセスポイントを作ります。
所有者ユーザーID:`1001`、所有者グループID:`1001`、アクセス許可:`7777`、パス:`/lambda`で作成します。ここもとりあえず最低限動かすことだけを考えた設定です。

## ビルド用の EC2 を作成する
MeCab、NEologd をビルドするための EC2 を作成します。
基本的にここも特に特別な設定は不要でデフォルトのままで良いのですが、NEologd のビルドが結構メモリを食うのでインスタンスタイプはやや大きめを指定します。（私は t2.large を使いました。）先ほど作成した EFS をマウントするので VPC はやはりデフォルトのものを利用します。OS は Amazon Linux 2 を使います。

## Python3.8 のインストール
Amazon Linux 2 に最初から入っているのは、Python2.7 なので、まず Python3.8 をインストールします。インストール方法は以下の記事を参考にしてください。ここで Amazon Linux 2 のアップデートも行います。
[EC2(Amazon Linux 2)にPython 3.8, Pip 3.8をインストールする](https://qiita.com/hitomatagi/items/e63dd8c4b879de156628)

## EFS のマウント
先ほど作成した EFS をビルド用の EC2 にマウントします。EFS の IP はコンソールから確認できます。権限は一旦777で。

```bash:EFSのマウント
sudo mount -t nfs \
    -o nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2 \
    <EFSのIP アドレス>:/ /mnt
sudo chmod 777 /mnt
```

## MeCab & IPA辞書 インストール
まず、MeCab, IPA辞書 のインストールに必要なプログラムをインストールしておきます。

```bash:前準備
sudo yum -y install gcc-c++ git patch
```

次に MeCab 本体を Google ドライブからダウンロードしてインストールします。`--prefix=/mnt/lambda` でインストール先に先ほどマウントした EFS を指定します。
なお、URL は古くなる可能性があるので、最新のダウンロード先は[作者のサイト](http://taku910.github.io/mecab/#download)を確認してください。

```bash:MeCabのダウンロードとインストール
cd ~
curl -L "https://drive.google.com/uc?export=download&id=0B4y35FiV1wh7cENtOXlicTFaRUE" -o mecab-0.996.tar.gz
tar -zxvf mecab-0.996.tar.gz
cd mecab-0.996
sudo ./configure --prefix=/mnt/lambda --with-charset=utf8
sudo make
sudo make install
```
そして MeCab 標準で推奨されている IPA 辞書をダウンロードしてインストールします。先ほどと同様に`==prefix=/mnt/lambda`で EFS をインストール先に指定します。NEologd だけ使う場合でも、NEologd のインストール後のテストに利用されるのでインストール必須のようです。
なお、こちらの URL も古くなる可能性があるので、MeCab 本体と同様に確認してください。

```bash:IPA辞書のダウンロードとインストール
cd ~
curl -L "https://drive.google.com/uc?export=download&id=0B4y35FiV1wh7MWVlSDBCSXZMTXM" -o mecab-ipadic-2.7.0-20070801.tar.gz
tar -zxvf mecab-ipadic-2.7.0-20070801.tar.gz
cd mecab-ipadic-2.7.0-20070801
sudo ./configure --prefix=/mnt/lambda --with-charset=utf8 --with-mecab-config=/mnt/lambda/bin/mecab-config
sudo make
sudo make install
```

後続の NEologd インストール時に MeCab の実行が必要なのであらかじめパスを通しておきます。

```bash:パスの設定
export PATH=$PATH:/mnt/lambda/bin
```

## NEologd インストール
ここで本題の NEologd をインストールします。詳細なインストール方法については前述の GitHub を参照して頂きたいですが、ただ単にインストールするだけなら、下記の通りやることはいたって簡単です。
なお、インストール先は、オプション未指定の場合、先にインストールした MeCab のコンフィグファイル`mecab-config`に従って決まります。今回は未指定でも勝手に MeCab と同じパスにインストールされます。また、インストールオプションがあり、オプションによっては一部の辞書ファイルのインストールをスキップしてサイズを小さくできるようです。が、今回は細かいことは気にせず全部入りでインストールします。なにせインストール先は **Elastic** File System なので。




```bash:NEologdのダウンロードとインストール
cd ~
git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git
cd mecab-ipadic-neologd
./bin/install-mecab-ipadic-neologd -n -a
```

## mecab-python3 インストール
最後に Python から MeCab を呼び出すためのライブラリを同じく`/mnt/lambda`配下にインストールします。
（直接`/mnt/lambda`にインストールしようとすると、Permission error になってしまうので、一旦ホームディレクトリにインストールしてから`/mnt/lambda`にコピーしています。なんかもっとスマートな方法がある気がするのですが…。）

```bash:mecab-python3のインストール
pip3 install mecab-python3 -t ~/temp
sudo cp -r ~/temp/* /mnt/lambda
```

これで MeCab と NEologd の準備は完了です。

## Lambda の作成
MeCab と NEologd がインストールされた EFS を利用する Lambda 関数を作成します。

### Lambda の設定
- 環境変数
    - `PYTHONPATH`に`/mnt/lambda`を指定
- ランタイム
    - Python3.8
- VPC
    - EFS, EC2で利用したデフォルトのVPC
    - デフォルトVPCに紐つく各AZのサブネット
    - EC2作成時にウィザードから作られたセキュリティグループを指定
- ファイルシステム
    - 上記で作成したファイルシステムとアクセスポイントを指定
    - ローカルマウントパスには`/mnt/lambda`を指定
- ロール
    - ポリシー`AmazonElasticFileSystemClientReadWriteAcces`を追加

なお、MeCab の処理はそこそこ時間とメモリを食うのでデフォルトの値だと足りません。必要量は対象とするテキストの長さによるので動かして調整してみてください。


### ソースコード
本当に必要最低限の処理だけ書いて、とりあえず NEologd を参照して分かち書きが出来ていることを確認します。トリガは API Gateway の想定です。

```python:lambda_function.py
import json
import MeCab
# IPA辞書を利用
ipadic_tagger = MeCab.Tagger('-O wakati -r /dev/null -d /mnt/lambda/lib/mecab/dic/ipadic')
# NEologdを利用
neologd_tagger = MeCab.Tagger('-O wakati -r /dev/null -d /mnt/lambda/lib/mecab/dic/mecab-ipadic-neologd')


def lambda_handler(event, context):
    print(event['body'])
    text = json.loads(event['body'])['text']

    return {
        "statusCode": 200,
        "headers": {
          "Content-Type": "application/json",
          "Access-Control-Allow-Origin": "*"
        },
        "body": json.dumps({
            "ipadic": ipadic_tagger.parse(text),
            "neologd": neologd_tagger.parse(text)
        })
    }
```

### 結果確認
Lambda 関数に API Gateway (HTTP API) のトリガを設定し、[Postman](https://www.postman.com/) からテスト実行します。
分かち書きの対象としたテキストは [MeCab 実行結果の例](https://github.com/neologd/mecab-ipadic-neologd/blob/master/README.ja.md#mecab%E3%81%AE%E5%AE%9F%E8%A1%8C%E7%B5%90%E6%9E%9C%E3%81%AE%E4%BE%8B)で使われている以下の一文です。
>8月3日に放送された「中居正広の金曜日のスマイルたちへ」(TBS系)で、1日たった5分でぽっこりおなかを解消するというダイエット方法を紹介。キンタロー。のダイエットにも密着。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91332/95b77aae-6ba6-f937-0089-c803cce6d862.png)
文章がスペースで区切られ、分かち書きがされていることがわかります。

IPA 辞書を使った場合と NEologd を使った場合とを比較すると、後者ではきちんと`中居正広の金曜日のスマイルたちへ`が一語と認識されていることがわかります。そのほかにも「ぽっこりおなか」「ダイエット方法」なども NEologd の方では一語になっています。

ちなみに MeCab の出力形式は上記だけでなく、例えば品詞、活用形、読みなどをあわせて出力することもできます。詳細は公式サイトを参照してください。

ということで、無事に MeCab と NEologd を Lambda 上で動かすことができました。

# CaboCha も動かす
さて、無事に MeCab と NEologd を使って分かち書きができるようになったのはイイのですが、問題はここから先これを使って何をするかです。
私の場合は、とあるアンケートの解答テキストが手元にたくさんあって、これをちょろっと分析してみようと思って、とりあえず分かち書きしてみたのですが、これだけだと結局何にもわかんないんですよね。せいぜいワードクラウドが作れるとかその程度で。

なかでも困るのは「XXXは適切だと思うか？」という問いに対して、「YYYYなので適切だと思う」「ZZZのため適切ではない」という2つの解答があった場合、これを単純に分かち書きするとどちらも「適切」という言葉が抽出されてしまうということです。「適切でないとは言い切れない」「適切ではないかと考える」みたいなさらに複雑なパターンもあり、これらを単純な分かち書きで処理するのは難しそうです。

こうした問題に対処できるかも、と考えて今回一緒に導入しようとしたのが日本語係り受け解析器の **CaboCha** です。
[CaboCha/南瓜: Yet Another Japanese Dependency Structure Analyzer](https://taku910.github.io/cabocha/)

これを使うことにより、文章を形態素に分けた後に単語間の修飾関係を解析することができるそうです。単純に MeCab で分かち書きだけするより、一歩先に進んだ分析ができるような気がします。

ということで MeCab, NEologd と同様に CaboCha も EFS にインストールして Lambda から使えるようにします。

## CaboCha のインストール
詳しいインストール方法は前述の公式サイトに載っていますので参照してください。ここでは簡単にインストールする方法を紹介します。

まず前提となる CRF++ のダウンロードとインストールを行います。

```bash:CRF++のダウンロードとインストール
cd ~
wget "https://drive.google.com/uc?export=download&id=0B4y35FiV1wh7QVR6VXJ5dWExSTQ" -O CRF++-0.58.tar.gz
tar zxfv CRF++-0.58.tar.gz
cd CRF++-0.58
./configure --prefix=/mnt/lambda
sudo make
sudo make install
```

次に CaboCha をダウンロードします。
（CaboCha のダウンロードだけやたらゴチャゴチャしてますが、色々試した結果これが一番スマートな方法でした。もっとスマートな方法があったら教えてください。）

```bash:CaboChaのダウンロード
cd ~
FILE_ID=0B4y35FiV1wh7SDd1Q1dUQkZQaUU
FILE_NAME=cabocha-0.69.tar.bz2
curl -sc /tmp/cookie "https://drive.google.com/uc?export=download&id=${FILE_ID}" > /dev/null
CODE="$(awk '/_warning_/ {print $NF}' /tmp/cookie)"  
curl -Lb /tmp/cookie "https://drive.google.com/uc?export=download&confirm=${CODE}&id=${FILE_ID}" -o ${FILE_NAME}
```

CaboCha インストール時に`crfpp.h`と`mecab.h`が`/usr/include`配下に必要になるので、これを`/mnt/lambda/include`からコピーします。
（なお、詳細は後述しますが、この部分が Lambda 実行時に一工夫必要になってしまう要因なんじゃないかという気がします…。）

```bash:ヘッダファイルのコピー
sudo cp -r /mnt/lambda/include/* /usr/include
```

CaboCha 本体をインストールします。

```bash:CaboChaのインストール
cd ~
tar jxvf cabocha-0.69.tar.bz2
cd cabocha-0.69
./configure --prefix=/mnt/lambda --with-mecab-config=`which mecab-config` --with-charset=UTF8
sudo make
sudo make install
```

最後に CaboCha に同梱の Python バインディングをインストールします。mecab-python3 と同様にテンポラリのディレクトリにインストールしてから`/mnt/lambda`にコピーします。

```bash:CaboChaのPythonバインディングのインストール
pip3 install ~/cabocha-0.69/python/ -t ~/temp2
sudo cp -r ~/temp2/* /mnt/lambda
```

## Lambda 関数のソースコード更新
EFS に CaboCha のインストールが完了したので、後は Lambda のコードで`import CaboCha`してやれば良いだけ、と思いきやそのままやると以下のエラーで動きません。

> [ERROR] Runtime.ImportModuleError: Unable to import module 'lambda_function': libcabocha.so.5: cannot open shared object file: No such file or directory

`libcabocha.so`ライブラリが無いと言われています。実際には`/mnt/lambda/lib`配下にあるのですが。`import MeCab`したときは、`/mnt/lambda/lib`配下の`libmecab.so`が認識できている（と思われる）のに、なぜか CaboCha の場合は認識できないようです。
そこで、`import CaboCha`する前に`ctypes.cdll.LoadLibrary`を使って`libmecab.so`ライブラリを直接読み込みます。

```python:lambda_function.py
import json
import os
import ctypes
path, directory, files = next(os.walk('/mnt/lambda/lib'))
for file in files:
    if file.startswith('libcabocha.so'):
        ctypes.cdll.LoadLibrary(os.path.join(path, file))

import CaboCha
# IPA辞書を利用
ipadic_parser = CaboCha.Parser("-d /mnt/lambda/lib/mecab/dic/ipadic")
# NEologdを利用
neologd_parser = CaboCha.Parser("-d /mnt/lambda/lib/mecab/dic/mecab-ipadic-neologd")


def lambda_handler(event, context):
    print(event['body'])
    text = json.loads(event['body'])['text']

    return {
        "statusCode": 200,
        "headers": {
          "Content-Type": "application/json",
          "Access-Control-Allow-Origin": "*"
        },
        "body": json.dumps({
            "ipadic": ipadic_parser.parseToString(text),
            "neologd": neologd_parser.parseToString(text)
        })
    }
```

## 結果確認
MeCab のときと同様に Postman から実行して結果を確認します。対象となるテキストも MeCab のときと同様のものを使います。（後半の「キンタロー。のダイエットにも密着。」は省略しましたが。）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91332/bc45dd7c-29cb-2328-9e4f-6dbf02f06c93.png)
無事に実行できました。が、このままだとなんのこっちゃかわかりませんので、結果を整形します。

```bash:IPA辞書利用
             8月-D                            
             3日に-D                          
          放送された-----------------------D  
          「中居正広の---D                 |  
                金曜日の-D                 |  
            スマイルたちへ-----D           |  
                 」(TBS系)で---D           |  
                           1日-D           |  
                          たった---------D |  
                             5分で---D   | |  
                                ぽっ-D   | |  
                                  こり---D |  
                                おなかを-D |  
                            解消するという-D  
                            ダイエット方法を-D
                                          紹介
```

```bash:NEologd利用
                                     8月3日に-D              
                                     放送された-D            
    「中居正広の金曜日のスマイルたちへ」(TBS系)で-------D    
                                          1日たった-----D    
                                                5分で---D    
                                       ぽっこりおなかを-D    
                                           解消するという-D  
                                           ダイエット方法を-D
                                                         紹介
```

それぞれ IPA 辞書を使った場合と NEologd を使った場合というように辞書を指定して係り受け解析ができていることがわかります。前者は、形態素の段階で「ぽっこり」が一語として扱われていないので「ぽっ」が「こり」にかかっていることになっています（笑）

ちなみに CaboCha も MeCab 同様に出力モードがあり、係り受けの精度など、細かい分析をするための情報を出力することができます。こちらも詳細は公式サイトを参照してください。

ということで無事に CaboCha も Lambda 上で動かすことができました。

# さいごに
Lambda with EFS で MeCab, NEologd & CaboCha を動かす一例でした。前回似たような記事を書いたからという理由だけで、特に深く考えて EFS に NEologd を入れたわけではありませんでした。が、こうした辞書データは頻繁に更新が入るため、EFS で外付けにしておくことで本体の Lambda 関数とは別に更新がかけられて便利かもしれません。Lambda with EFS の有効な使い道の一つのパターンなのかもな、と思いました。

とはいえ、Lambda の temp 領域サイズアップもいまだに期待しています。よろしくお願いします。

# 参考記事
https://qiita.com/hitomatagi/items/e63dd8c4b879de156628
https://qiita.com/osyou84/items/4e2f686d82bf9e1166e8
https://qiita.com/ayuchiy/items/c3f314889154c4efa71e