Fargateでradikoを録音してS3に保存するCloudFormation

前にdockerでradikoを録音するようにした。
これを定期実行するためにVPSを借りているのだが、録音は1日のごくわずかな時間しか行わないので、録音していない時間は余計なコストがかかってしまう。また低スペックのVPSのためか、複数番組を同時に録音すると失敗することがある(まれに)。

Fargateでは実行している間だけしか金がかからないので安いはず。それに同時録音の問題も起きないだろう。というわけでFargateに乗り換えてみることにした。

やりたいこと

  • Fargateを使ってradikoを録音するスクリプトを定期実行する
  • 音声ファイルはS3に保存する
  • 一連の構成はCloudFormationによって管理する

前提条件

  • dockerコマンドが入っている
  • AWSアカウントを持っている
    CloudFormationでIAMの設定をしたりするので、強い権限が必要。
  • AWS CLIが入っている
  • S3の保存先バケットが作成されている
  • 録音したい番組は関東エリアで放送されているもの
    Fargateが東京にしかないので。(radikoプレミアムに対応したスクリプト使えばいけるはず。)

できたもの

docker_rec_radiko

前に作ったやつにFargate版を追加した。CloudFormationはcfディレクトリ下にある。

作業手順

ECRにDockerイメージを登録

  1. リポジトリ作成
    aws ecr create-repository --repository-name <REPOSITORY_NAME>
    

    <REPOSITORY_NAME> には好きなリポジトリ名を入れる(たとえば docker_rec_radiko)。
    結果としてjsonが返る。その中の repositoryUri をあとで使う。

  2. dockerイメージをビルド・タグ付け

    docker build . -t <REPOSITORY_URI>
    

    <REPOSITORY_URI> には上で取得した repositoryUri を入れる。

  3. docker login認証コマンド文字列を取得

    aws ecr get-login --no-include-email
    
  4. ログイン
    上で取得したdocker login認証コマンド文字列をそのまま実行。
    これでECRにpushできるようになる。

  5. dockerイメージをECRにpush

    docker push <REPOSITORY_URI>
    
  6. ECRに登録されたことを確認
    AWSのコンソールを見てもいいが、コマンドでもわかる。

    aws ecr describe-images --repository-name <REPOSITORY_NAME>
    

CloudFormationの設定

各yamlファイルには Parameters セクションがあり、ここで名前などを設定している。基本的に何も変えなくてもいけるはずだが、問題がある場合は変更する。ただしタスク・スケジュールのyamlは修正必要。
また、yamlファイルが4つあるが、ここまで分ける必要はないかもしれない。

  1. ネットワーク(network.yaml)
    VPC、サブネット、セキュリティグループを作成。さらにゲートウェイやルートテーブルなどなども作成した。あれこれ設定しているが、たぶんこれが必要最小限のはず…。CIDRとかは適当に設定しているので問題があれば変える。

  2. ロール(roles.yaml)
    IAM関連はCloudFormationでなく、コンソールで管理すれば十分な気もする。今回はECSタスクを実行するロールなどの作成で試行錯誤していたらカオスになったのでCloudFormationで管理するようにした。

  3. ECSクラスタとLambda(cluster.yaml)
    ここで作成するLambdaは、ECSタスクを実行するだけのもの。本当はCloudWatchのイベントでECSタスクを実行したいのだが、現在はまだCloudFormationでその設定ができないようなので、間にLambdaを挟んでいる。

  4. ECSタスクとスケジュール(task.template.yamlをtasks/mytask.yamlとしてコピーして修正)
    Parametersセクションを必ず修正する。ECRのイメージ、cronパターン、放送局と録音時間、S3バケットなどを指定する。このyamlファイル1つがcronの1行に対応するイメージ。なので番組をいくつも録音したいときはその数だけyamlファイルを作る。
    CPUやメモリは現在最小限にしているが、番組によっては足りないかも。

    • ECSCommand
      <放送局ID>, <録音時間(分)>, <ファイルプレフィクス> の形式。放送局IDはここ参照。
    • ECSTaskSchedulerPattern
      cronパターンを書く。癖があるのでドキュメント参照。
      絶対にUTCで書く! 日本時間での記述には対応していない(めんどくせえ)。
    • ECSTaskName
      タスク名を書く。なんでもいいが、わかりやすく短い名前にした方がいい。
    • ImageName
      ECRで登録したイメージ名。
    • S3BucketName
      あらかじめ作成したS3バケット名を書く。

CloudFormationでリソース作成

ここではAWS CLIで実行する場合を書く。コンソール画面からそのままやっても問題ない。
が、いずれにせよ作成する順番はこの通りにすること(ネットワークとロールは入れ替え可能)。ネットワークとロールが作成完了したあとでないとECSクラスタなどを作成できない。
以下ではスタック名は適当につけている。

  1. ネットワーク
    aws cloudformation create-stack --stack-name docker-rec-radiko-network-stack --template-body file://./cf/network.yaml
    
  2. ロール
    IAM関連なので、コンソール画面で作成するときにはチェックボックスに確認のチェックをする作業がある。AWS CLIでもそれに対応するオプションをつける。

    aws cloudformation create-stack --stack-name docker-rec-radiko-roles-stack --template-body file://./cf/roles.yaml --capabilities CAPABILITY_NAMED_IAM
    
  3. ECSクラスタとLambda
    aws cloudformation create-stack --stack-name docker-rec-radiko-cluster-stack --template-body file://./cf/cluster.yaml
    
  4. ECSタスクとスケジューラ
    aws cloudformation create-stack --stack-name docker-rec-radiko-task-stack --template-body file://./cf/tasks/mytask.yaml
    
  5. 確認
    ECSタスクまで作成成功すれば録音できるようになっているはず。指定した時間にECSでタスクが実行され、録音終了すればS3に保存される。ログはCloudWatchで見れる。

課題とか

  • Lambdaを使わずに済ませたい
    これはAWSが対応してくれることを待つしかないようだ。
  • 日本時間でcronを書きたい
    これもAWSが対応するのを待つしかない(たぶんされない)。
    現在はUTCで書くしかない。計算が面倒。
  • CloudFormationスタック間の情報受け渡し
    今回はスタックで作成したリソース(たとえばサブネットやロール)をパラメータストアに入れることで、他のスタックから参照できるようにしている。
    だがこれだとスタックを作り直すと、それに依存するスタックも作り直さないといけない(パラメータストア内の値は、作り直す前のものなので)。
    たとえばネットワークやロールのスタックを作り直すと、クラスタやタスクのスタックも作り直さないといけない。
    何かうまい方法があればいいのだが…。
    あとProjectNameなどを複数のスタックでそれぞれ定義し直しているのも気持ち悪い。
  • CloudFormationスタック分割のベストプラクティスがわからない
    今回は練習の意味もあり、わりと細かく分割した。
    本来ならどのくらい粒度で分割するのがいいかよくわからない。
    たとえばECSタスクは1つ1つ別のスタックにしたが、タスクはすべて1つのスタックにまとめてしまうというのもありだと思う。

参考

ECR

CloudFormation

公式

各種記事

スケジュール関連

VPC

MeCab+NEologdを使って日本語を分かち書きにするツールを作った

Word2VecとかするときにMeCab + NEologdを使って分かち書きする。だがインストールがやや面倒。Dockerを使って、手軽に分かち書きするツールを作った。

やりたいこと

  1. MeCab + NEologdで分かち書きする
  2. ディレクトリ構造を保つ
    Word2Vecで全テキストを1つにまとめてしまうときは問題ないが、Doc2Vecとかするときには1ファイルごとに分かち書きしたい。そのときディレクトリ構造を変えられると面倒なので、同じになるようにする。
    たとえば以下のような感じ。inputに分かち書きしたいファイルをディレクトリごと入れたら、その構造を保ったままoutputに結果が出る。

    ├── input
    │   ├── 1
    │   │   ├── 11
    │   │   │   └── c.txt
    │   │   └── b.txt
    │   ├── 2
    │   │   └── d.txt
    │   └── a.txt
    └── output
       ├── 1
       │   ├── 11
       │   │   └── c.txt
       │   └── b.txt
       ├── 2
       │   └── d.txt
       └── a.txt
    

できたもの

sankaku/docker-mecab

実行例

./mount/input に上のツリー構造の通りファイルを入れる。中身はすべて同じで、NEologdにある例と同じにした。
そしてDockerイメージを作成し、 ./split.sh を実行。

入力

$ for f in `ls mount/input/**/*.txt`; do echo $f; cat $f; done
mount/input/1/11/c.txt
8月3日に放送された「中居正広の金曜日のスマイルたちへ」(TBS系)で、1日たった5分でぽっこりおなかを解消するというダイエット方法を紹介。キンタロー。のダイエットにも密着。
mount/input/1/b.txt
8月3日に放送された「中居正広の金曜日のスマイルたちへ」(TBS系)で、1日たった5分でぽっこりおなかを解消するというダイエット方法を紹介。キンタロー。のダイエットにも密着。
mount/input/2/d.txt
8月3日に放送された「中居正広の金曜日のスマイルたちへ」(TBS系)で、1日たった5分でぽっこりおなかを解消するというダイエット方法を紹介。キンタロー。のダイエットにも密着。
mount/input/a.txt
8月3日に放送された「中居正広の金曜日のスマイルたちへ」(TBS系)で、1日たった5分でぽっこりおなかを解消するというダイエット方法を紹介。キンタロー。のダイエットにも密着。

出力

$ for f in `ls mount/output/**/*.txt`; do echo $f; cat $f; done
mount/output/1/11/c.txt
8月3日 に 放送 さ れ た 「 中居正広の金曜日のスマイルたちへ 」( TBS 系 ) で 、 1日 たった 5分 で ぽっこり おなか を 解消 する という ダイエット方法 を 紹介 。 キンタロー。 の ダイエット に も 密着 。 
mount/output/1/b.txt
8月3日 に 放送 さ れ た 「 中居正広の金曜日のスマイルたちへ 」( TBS 系 ) で 、 1日 たった 5分 で ぽっこり おなか を 解消 する という ダイエット方法 を 紹介 。 キンタロー。 の ダイエット に も 密着 。 
mount/output/2/d.txt
8月3日 に 放送 さ れ た 「 中居正広の金曜日のスマイルたちへ 」( TBS 系 ) で 、 1日 たった 5分 で ぽっこり おなか を 解消 する という ダイエット方法 を 紹介 。 キンタロー。 の ダイエット に も 密着 。 
mount/output/a.txt
8月3日 に 放送 さ れ た 「 中居正広の金曜日のスマイルたちへ 」( TBS 系 ) で 、 1日 たった 5分 で ぽっこり おなか を 解消 する という ダイエット方法 を 紹介 。 キンタロー。 の ダイエット に も 密着 。

ちゃんとファイルごとに分かち書きできてる。
ディレクトリ構造もそのまま。


$ tree mount/ mount/ ├── input │   ├── 1 │   │   ├── 11 │   │   │   └── c.txt │   │   └── b.txt │   ├── 2 │   │   └── d.txt │   └── a.txt └── output ├── 1 │   ├── 11 │   │   └── c.txt │   └── b.txt ├── 2 │   └── d.txt └── a.txt

WikipediaのデータでWord2Vecを試す

いまさらWord2Vecをやってみる。

やりたいこと

Wikipediaの全日本語記事をWord2Vecする。これにより、単語を単語ベクトルに変換できる。
その後、単語の関連度とかを見て遊ぶ。

環境

  • OS
    Ubuntu 16.04 LTS
  • Python
    3.5.2
  • gensim
    3.7.3

流れ

Word2Vec使うときはいつもこんな感じっぽい。

  1. テキストを用意する
  2. 分かち書きにする
  3. Word2Vecにかける

テキストを用意する

ダンプデータのダウンロード

Wikipediaのダンプデータについてはここに書いてある。
これをたどると記事全体のダンプデータのリンクがわかる。以下のようにしてダウンロードする。(-o でダウンロード先を指定している。)

curl -o ~/Downloads/wikipedia.bz2 https://dumps.wikimedia.org/jawiki/latest/jawiki-latest-pages-articles.xml.bz2

ダンプデータをテキストにする

上で取得したダンプデータの中身はXMLであり、かつWiki記法なので、これを単純なテキストにする。
WikiExtractorというツールを使う。
python3 WikiExtractor.py -o <OUTPUT_DIRECTORY> <INPUT_FILE> という使い方。解凍不要で、bz2ファイルのまま使える。
けっこう時間がかかる。

git clone https://github.com/attardi/wikiextractor.git
cd wikiextractor
python3 WikiExtractor.py -o ~/Downloads/wikidata ~/Downloads/wikipedia.bz2

これで ~/Downloads/wikidata にテキストができるが、1MBごとにファイルに分割されている。これを1つのファイルにまとめる。

cd ~/Downloads/wikidata
find . -name "wiki_*" | xargs cat > ~/Downloads/wikidata_all_tmp.txt

また、WikiExtractorで変換したファイルには

<doc id="5" url="https://ja.wikipedia.org/wiki?curid=5" title="アンパサンド">
...
</doc>

のようなタグが残っているので、これを削除する。

grep -v '^<doc id.*>$' ~/Downloads/wikidata_all_tmp.txt | grep -v '^</doc>$' > ~/Downloads/wikidata_all.txt

(参考) ダンプデータとテキストの確認

上で取得したダンプデータは bzip2 -d wikipedia.bz2 で解凍して中身を見れる。こんな感じ。

  <page>
    <title>アンパサンド</title>
    <ns>0</ns>
    <id>5</id>
    <revision>
      <id>71254632</id>
      <parentid>70334050</parentid>
      <timestamp>2019-01-10T10:45:42Z</timestamp>
      <contributor>
        <username>タバコはマーダー</username>
        <id>631644</id>
      </contributor>
      <minor />
      <comment>曖昧さ回避の体裁</comment>
      <model>wikitext</model>
      <format>text/x-wiki</format>
      <text xml:space="preserve">{{redirect|&}}
{{WikipediaPage|「アンパサンド (&)」の使用|WP:JPE#具体例による説明}}
{{記号文字|&amp;}}
{{複数の問題|出典の明記=2018年10月8日 (月) 14:50 (UTC)|独自研究=2018年10月8日 (月) 14:50 (UTC)}}
[[Image:Trebuchet MS ampersand.svg|right|thumb|100px|[[Trebuchet MS]] フォント]]
'''アンパサンド''' ('''&amp;'''、英語名:{{lang|en|ampersand}}) とは並立助詞「…と…」を意味する[[記号]]である。[[ラテン語]]の {{lang|la|"et"}} の[[合字]]で、[[Trebuchet MS]]フォントでは、[[
ファイル:Trebuchet MS ampersand.svg|10px]]と表示され "et" の合字であることが容易にわかる。ampersa、すなわち "and per se and"、その意味は"and [the symbol which] by itself [is] an
d"である。

WikiExtractorによってテキストにすると、この部分は次のようになる。

<doc id="5" url="https://ja.wikipedia.org/wiki?curid=5" title="アンパサンド">
アンパサンド

アンパサンド (&、英語名:) とは並立助詞「…と…」を意味する記号である。ラテン語の の合字で、Trebuchet MSフォントでは、と表示され "et" の合字であることが容易にわかる。ampersa、すなわち "and per se and"、その意味は"and [the symbol which] by itself [is] and"である。

分かち書きにする

Word2Vecを使うには単語に分割されてないといけない。
英語は空白などで分割されているのですぐにわかるが、日本語はそのままだと分割されていないので、分かち書きにする作業が必要。

分かち書きにするためのライブラリとしては形態素解析器のMeCabを使う。
他にはJUMAN++Janomeというのもあるらしい。

また、標準の辞書だと新語に弱い。1語として扱ってほしい語を複数単語にされたりする。これを避けるためにNEologdという辞書を使う。
この辞書によって結果がどう変わるかはNElogdのREADMEに例が書いてあるのでそれを見れば一発でわかる。

インストール

NEologdのREADMEに書いてある通りにやる。

sudo apt update
sudo apt install mecab libmecab-dev mecab-ipadic-utf8 git make curl xz-utils file
git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git
cd mecab-ipadic-neologd
./bin/install-mecab-ipadic-neologd -n
# yes とタイプする

これでインストールが完了する。

辞書のパスは以下でわかる。

echo `mecab-config --dicdir`"/mecab-ipadic-neologd"

自分の環境では /usr/lib/mecab/dic/mecab-ipadic-neologd だった。

標準の辞書とNEologdの辞書で形態素解析すると、それぞれこんな感じになる。

$ echo 令和元年 | mecab
令   名詞,一般,*,*,*,*,令,リョウ,リョー
和   名詞,一般,*,*,*,*,和,ワ,ワ
元年  名詞,副詞可能,*,*,*,*,元年,ガンネン,ガンネン
EOS

$ echo 令和元年 | mecab -d /usr/lib/mecab/dic/mecab-ipadic-neologd
令和元年    名詞,固有名詞,一般,*,*,*,2019年,レイワガンネン,レイワガンネン
EOS

Wikipediaのテキストを分かち書きにする

これも時間がかかる。

mecab ~/Downloads/wikidata_all.txt -d /usr/lib/mecab/dic/mecab-ipadic-neologd -Owakati -o ~/Downloads/wikidata_all.wakati.txt -b 16384

オプションの意味は以下。
+ -d
使用する辞書を指定。上で調べたパスを入れる。
+ -Owakati
分かち書きにする。これを指定しない場合、普通に形態素解析される。
+ -o
出力ファイルを指定。
+ -b
バッファサイズを指定。

なお最後の -b 16384

input-buffer overflow. The line is split. use -b #SIZE option.

というエラーが途中で出たので追加した。デフォルトでは8192(mecab -hでわかる)。

変換後はバイナリファイルになるという話だったが、大丈夫そう。

$ file wikidata_all.wakati.txt
wikidata_all.wakati.txt: UTF-8 Unicode text, with very long lines

(参考) 分かち書きしたファイル

こんな感じになる。

アンパサンド

アンパサンド (&、 英語名 :) と は 並立 助詞 「 … と … 」 を 意味 する 記号 で ある 。 ラテン語 の の 合字 で 、 Trebuchet MS フォント で は 、 と 表示 さ れ " et " の 合字 で ある こと が 容易 に わかる 。 ampersa 、 すなわち " and per se and "、 その 意味 は " and [ the symbol which ] by itself [ is ] and " で ある 。

Word2Vecにかける

ここまでで分かち書きテキストを用意できたので、ようやく本題。

gensimというライブラリを使う。

gensimのインストール

cythonも入れた方が早いらしいので入れる。

sudo pip3 install --upgrade gensim cython

モデルの作成

wikidata_all.wakati.txt があるディレクトリに移動して python3 を実行し、そのままREPLで以下を全部実行するのが手軽。
時間がかかるので注意。

from gensim.models import Word2Vec
from gensim.models.word2vec import LineSentence
import logging
logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)

sentences = LineSentence('./wikidata_all.wakati.txt')
model = Word2Vec(sentences, size=200, window=20, min_count=5, workers=4)
model.save('./wiki_word2vec.model')

wiki_word2vec.model というファイルが保存される。
これがWikipediaのWord2Vecモデル。

ハイパーパラメータの設定について

モデル作成時に一番重要なのは以下の部分。値は調整必要。他にも指定したいパラメータがあればここを見ながらやる。

model = Word2Vec(sentences, size=200, window=20, min_count=5, workers=4)
  • size
    単語ベクトルの次元の数。
  • window
    この単語数だけ、前後の単語を見る。
  • min_count
    テキスト中に現れる回数がこの数未満の単語は無視。
  • workers
    訓練時に使うスレッド数。

使ってみる

モデルの読み込み

いろいろ試す前に、まずモデルを読み込んでおく必要がある。

(REPLでモデルの作成をした直後は、既にメモリ上にモデルが載っているので、改めて読み込まなくていい。)

from gensim.models import Word2Vec

model = Word2Vec.load('wiki_word2vec.model')
wv = model.wv

なおwvのクラスは以下になる。このクラスのドキュメントに使い方が載っている。

>>> type(wv)
<class 'gensim.models.keyedvectors.Word2VecKeyedVectors'>

似た単語の表示

wv.similar_by_word('foo')

でわかる。

>>> wv.similar_by_word('猫')
[('ネコ', 0.7882304787635803), ('ウサギ', 0.7559791207313538), ('子猫', 0.7516933679580688), ('黒猫', 0.7485260367393494), ('野良猫', 0.7470539808273315), ('犬', 0.7432234883308411), ('飼い猫', 0.7408008575439453), ('愛猫', 0.7271249890327454), ('柴犬', 0.6979021430015564), ('飼い主', 0.6935858726501465)]

>>> wv.similar_by_word('犬')
[('猟犬', 0.7577184438705444), ('猫', 0.7432234883308411), ('犬種', 0.7246831059455872), ('飼い主', 0.7098195552825928), ('牧羊犬', 0.7084189653396606), ('仔犬', 0.7059577703475952), ('番犬', 0.6994990110397339), ('プードル', 0.6932131052017212), ('子犬', 0.6915150880813599), ('柴犬', 0.688454270362854)]

>>> wv.similar_by_word('令和')
[('新元号', 0.8433001041412354), ('平成最後', 0.6003873348236084), ('生前退位', 0.5694811940193176), ('令和元年', 0.5659081935882568), ('天皇誕生日', 0.5473066568374634), ('建国記念の日', 0.5416209697723389), ('改元', 0.5325936079025269), ('退位特例法', 0.5113210678100586), ('即位礼正殿の儀', 0.5051107406616211), ('明治節', 0.49450594186782837)]

>>> wv.similar_by_word('Apple')
[('アップル', 0.7637709975242615), ('iPhone', 0.6721816062927246), ('Macintosh', 0.6700955033302307), ('VisiCalc', 0.6614382863044739), ('iMac', 0.6558083295822144), ('アップルコンピュータ', 0.6533167958259583), ('スティーブ・ジョブズ', 0.6456233263015747), ('Xerox', 0.619143009185791), ('Apple社', 0.614047110080719), ('iPad', 0.6136564016342163)]


>>> wv.similar_by_word('リンゴ')
[('サクランボ', 0.6949737668037415), ('イチゴ', 0.6846523284912109), ('りんご', 0.6731216907501221), ('果物', 0.6545817852020264), ('トマト', 0.6475458145141602), ('シードル', 0.6408026218414307), ('プルーン', 0.6374067664146423), ('レタス', 0.6371461153030396), ('ジャガイモ', 0.6367175579071045), ('ピーナッツ', 0.6305103898048401)]

2語の関連度

wv.similarity('foo', 'bar')

でわかる。

>>> wv.similarity('猫', '犬')
0.74322355

>>> wv.similarity('猫', '夏目漱石')
0.19329126

>>> wv.similarity('吾輩は猫である', '夏目漱石')
0.73291266

単語の和・差

wv.most_similar(positive=['foo', 'bar'], negative=['baz'])

これで foo + bar – baz がわかる。

>>> wv.most_similar(positive=['王様', '女'], negative=['男'])
[('お姫様', 0.6474927663803101), ('王女', 0.601335346698761), ('白雪姫', 0.5954098701477051), ('シンデレラ', 0.589481770992279), ('貴婦人', 0.5760519504547119), ('女王', 0.5691637992858887), ('妖精', 0.5630465149879456), ('花嫁', 0.5587092638015747), ('魔女', 0.5510756969451904), ('オーベロン', 0.5459994077682495)]

>>> wv.most_similar(positive=['日本', '北京'], negative=['東京都'])
[('中国', 0.6868176460266113), ('上海', 0.6344862580299377), ('天津', 0.566441535949707), ('香港', 0.5649573802947998), ('中国本土', 0.563534677028656), ('広州', 0.549053430557251), ('台湾', 0.5482343435287476), ('中華', 0.5481294393539429), ('厦門', 0.547395646572113), ('中華圏', 0.5437194108963013)]

感想

やってみるだけならわりと簡単にできた。
でも wv.similar_by_word() の精度は良くない感じ。エポック数を増やしたり、前処理を厳密にやらないといけない。
wv.most_similar() は想定通りの結果になった。

参考

WikipediaデータをWord2Vecする件

WikiExtractor

MeCab

NEologd

gensim