ruby1.9.3でseleniumを動かす

超レガシー環境のruby1.9.3で動かしていたスクレイピングseleniumを使ったものに対応させる必要性があり、その時のメモです。

Dockerfile

FROM ruby:1.9.3

RUN apt-get update
RUN apt-get install -y ttf-freefont chromium unzip libgconf-2-4

ADD Gemfile .
ADD Gemfile.lock .
RUN bundle install

RUN wget https://chromedriver.storage.googleapis.com/2.28/chromedriver_linux64.zip && unzip chromedriver_linux64.zip
RUN rm -f /usr/local/bundle/bin/chromedriver && mv chromedriver /usr/local/bin/ && chmod 755 /usr/local/bin/chromedriver

RUN mkdir /noto
ADD https://noto-website.storage.googleapis.com/pkgs/NotoSansCJKjp-hinted.zip /noto
WORKDIR /noto
RUN unzip NotoSansCJKjp-hinted.zip && \
    mkdir -p /usr/share/fonts/noto && \
    cp *.otf /usr/share/fonts/noto && \
    chmod 644 -R /usr/share/fonts/noto/ && \
    fc-cache -fv
RUN rm -rf /noto

docker-compose.yml

version: '3'
services:
  ruby_selenium:
    build: .
    image: ruby_selenium
    container_name: ruby_selenium
    working_dir: /root/src
    volumes:
      - ./src/:/root/src/
    tty: true

src/scrape.rb

# coding: utf-8

require 'selenium-webdriver'

driver = Selenium::WebDriver.for :chrome, switches: ["--headless", "--no-sandbox"]

driver.navigate.to 'https://www.yahoo.co.jp/'
driver.save_screenshot './screenshot.png'

動かし方

コンテナを起動し、コンテナ内のシェルに入ります。

$ docker-compose build
$ docker-compose up -d
$ docker-compose exec ruby_selenium sh

シェルの中でサンプルスクリプトを動かすと

# ruby scrape.rb

次のように yahoo.co.jp のスクリーンショットが撮れます。

f:id:sanshonoki:20200213220552p:plain

手こずった点

ruby1.9.3だと selenium-webdriver の バージョンを指定する必要がある

selenium-webdriver のバージョン 2.53.4 じゃないとruby1.9 にはインストールできません

selenium-webdriver 2.53.4 だと Selenium::WebDriver::Chrome::Options がない

rubyで headless chrome を動かすコードをWebで調べると以下のようなサンプルコードが簡単に見つかるのですが、

options = Selenium::WebDriver::Chrome::Options.new
options.add_argument('--headless')

driver = Selenium::WebDriver.for :chrome, options: options

selenium-webdriver 2.53.4 ではこのコードは動きません..

How to use Chrome Options in Ruby Selenium? - Stack Overflow

に助けられました

chromiumのバージョンが古い

apt-get でインストールした chromiumブラウザ はかなりバージョンが古いです。

# /usr/bin/chromium --version
Chromium 57.0.2987.98 Built on 8.7, running on Debian 8.0

このバージョンに適合する chromedriver を探してくるのに苦労しました。

chromedriverのダウンロードページ だと ChromeDriver 2.41、Supports Chrome v67-69 までの情報しかのってなくv57 は見つかりません..

いろいろググっていたところ以下の記事が見つかり、助けられました。

javascript - Which ChromeDriver version is compatible with which Chrome Browser version? - Stack Overflow

この表から v57 に適合するのは 2.28 と分かり、対応するchromedriveをインストールできました

# /usr/local/bin/chromedriver --version
ChromeDriver 2.28.455506 (18f6627e265f442aeec9b6661a49fe819aeeea1f)

今回のコード

今回の記事のコードはこちらにあります github.com

参考にした記事

Heroku Schedulerの怪しい挙動

WebアプリでHeroku Schedulerでバッチ処理をしていたのですがたまに実行されないことが幾度かありました。

あれ?と思いつつも自分しか使わないサービスだし、たまにだったので手動再実行で対応していました。。

が、やはり気持ち悪いので重い腰をあげて今回調べてみることにしました

エラーは出てないので怪しいと思ったのは同じ時刻に2つのタスクが登録されていたこと

ということで、実験で再現するか調べます

実験

(1) 同じ時間に3タスク f:id:sanshonoki:20190929215716p:plain

(2) 30分ずつずらす f:id:sanshonoki:20190929215735p:plain

タスクの内容はslackに文字列を送るというタスクです (tsk1であれば "tsk1"というメッセージを送る)

交互に7日間ずつ実行しました

結果

(1)のときは 以下のように 2タスク分しか実行されないときが 2日(/7日)ありました

f:id:sanshonoki:20190929223207p:plain

結論

やはり同時刻に複数タスクが登録されていると実行されない場合があるようです

letter_opener_webをRails3.2+ruby1.9.3の環境で使う

送信メール確認用にdevelopment環境ではletter_openerを使っていたのですが dockerへ移行したので動かなくなりました。。

そこで、Webインターフェースで送信メールを確認できるletter_opener_webに乗り換えました。 ただ、Rails3.2+ruby1.9.3という古い環境では簡単にはいかなかったのでメモを残します。

結論

以下の環境で無事動きました

Gemfile

group :development do
  gem 'public_suffix', '~> 1.4.6' 
  gem "letter_opener", '1.4.1' 
  gem 'letter_opener_web', '1.2.2' 
end

docker-compose.yml

services:
  web:
    # 他は省略
    environment:
      - BROWSER=echo

動くようになるまでの道筋

おまけです。

public_suffixのインストールに失敗

依存gemの1つ、public_suffixのインストールで失敗しました。

public_suffix requires Ruby version >= 2.0.

そこで、ruby1.9.3をサポートしているバージョン '1.4.6' にダウングレードします。

参考にしたページ

Ruby 1.9.3 build broken due to public_suffix gem not compatible + Selenium Error · Issue #6885 · travis-ci/travis-ci · GitHub

/letter_openerを開くとエラー

/usr/local/bundle/gems/letter_opener_web-1.3.4/app/controllers/letter_opener_web/letters_controller.rb:8: unknown type of %string
    before_action :load_letter, only: %i[show attachment destroy]
  • %i の記法は ruby2.0からのサポート
  • before_action は Rails4からの機能

このため、letter_opener_web を '1.2.2' にダウングレードしました

Launchy::CommandNotFoundError のエラーが出る

このエラーは letter_opener_web の バージョン1.3.3 から rescue節でハンドリングすることで出ないようになっているのですが 1.2.2 にダウングレードしているためエラーが出てしまいます..

該当のコミット

https://github.com/fgrehm/letter_opener_web/commit/661ef92c9c83afa47e913b5ddad264b39f22ad09#diff-b83fce5786125deb6a48e47051caa2fe

環境変数 BROWSER をセットすることで回避します。 ブラウザでなくともコマンドにURLを渡すことさえできればエラーは出ないので 環境変数 BROWSER=echo をセットします。

メール送信時に undefined method `html_part'

letter_opener がエラーを出しています。 以下のページによると バージョン1.4.xではエラーが出ないようなので 1.4.1 にダウングレードします。

undefined method `html_part' for #<String:0x00007f5b16161520> · Issue #147 · ryanb/letter_opener · GitHub

Heroku上のRails3アプリの延命対策

プライベートユースでHeroku上で運用しているWebアプリ(Rails3.2 + ruby1.9.3)があるのですが

Cedar-14 stack end-of-life window begins 1 May 2019 | Heroku Dev Center

のアナウンスがあり、来年5月からビルドができなくなってしまうとのこと

ならば、アップグレードしておくかと

アップグレードガイド Upgrading to the Latest Stack | Heroku Dev Center に従って

$ heroku stack:set heroku-18
$ git commit --allow-empty -m "Upgrading to heroku-18"
$ git push heroku master

とheroku-18 stackベースでデプロイしてみたところ、buildエラーに...。

remote:  !     An error occurred while installing ruby-1.9.3-p551
remote:  !
remote:  !     Heroku recommends you use the latest supported Ruby version listed here:
remote:  !     https://devcenter.heroku.com/articles/ruby-support#supported-runtimes
remote:  !
remote:  !     For more information on syntax for declaring a Ruby version see:
remote:  !     https://devcenter.heroku.com/articles/ruby-versions

なんと..

こりぁアカン..

どうやらruby-1.9.3は新しいstackでサポートされてないようです..

ということでcontainerでデプロイするように作戦変更しました。

Rails/rubyの段階的アップグレードはとうの昔に放棄)

containerベースへの移行方法

Dockerfileを作ってローカル環境にてdocker上で動くようにできたら、

あとは

$ heroku container:login
$ heroku container:push web   # コンテナイメージのpush
$ heroku container:release web   # pushしたイメージのリリース

です。

既存のCedar-14 stackでのアプリケーションが稼働中であっても問題なく上書き?でデプロイできました。

コードの修正点は

config/database.yml

production:
  <<: *default
  url: <%= ENV['DATABASE_URL'] %>

それ以外としては、

.dockerignore

tmp/pids/server.pid

を追加しておくとよいと思います。

トラブルシューティング

やはりというかいくつか問題が発生したのでその対策です。 もっとスマートな方法はあるはず。。

トラブル1: Boot timeoutエラー

Error R10 (Boot timeout) -> Web process failed to bind to $PORT within 60 seconds of launch

どうやらアプリケーションがPORTにアクセスできてないようです

Qiitaの記事 DockerでRailsの環境構築してHerokuへデプロイする - Qiita では

CMD ["rails", "server", "-b", "0.0.0.0"]

でサーバー起動していますが同様にしたら上記のエラーになってしまいました

どうやら自分の環境ではWebrickを使っているからのようです。。 (Rails5だとデフォルトサーバーのpumaの設定ファイル内で環境変数PORTにアクセスしているから問題ない)

本来ならばpumaを使うようにするべきだと思いますが、とりあえず次のように修正して解決しました。

CMD bundle exec rails server -b 0.0.0.0 -p $PORT

ただ、

配列形式の

CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0", "-p", "$PORT"]

にすると

2019-07-17T13:43:33.652404+00:00 heroku[web.1]: Starting process with command `bundle exec rails s -p \8224 -b 0.0.0.0

というエラーになりNGでした...

謎。。

トラブル2: assets precompileされていなくアプリケーションエラー

An ActionView::Template::Error occurred in page#index:

  application.css isn't precompiled

が出ます

assets precompileが必要です。(今まではHerokuデプロイ時に自動でやってくれていた)

Dockerfile に以下の行を追加して解決しました

RUN bundle exec rake assets:precompile

Rakefile を ADD/COPY する前だと No Rakefile found (looking for: rakefile, Rakefile, rakefile.rb, Rakefile.rb) が出るので要注意です

トラブル3: コンパイルしたassetsにアクセスできない

ActionController::RoutingError (No route matches [GET] "/assets/application-bf61d1c6485bd814e710bb58a3622bea.css"):

コンパイルされたCSSにアクセスできず、レイアウトが崩れてしまいました

Rails 4+ Asset Pipeline on Heroku | Heroku Dev Center を参考に

Gemfile に

gem 'rails_12factor', group: :production

を追加して解決しました

トラブル4: development環境用にインストールしたgemが悪さをする

development環境用のgemがインストールされていてproduction環境で悪さをしているということがありました

production環境用のgemのみインストールするのは

# RUN bundle install  # を
RUN bundle install --without test development #  に変更

でよいのですがこれだとローカルの開発環境でdockerビルドしたときに開発用のgemがインストールされません

ARGを使って使い分けるようにしました

Dockerfile

ARG bundle_install_opt="--without test development"
RUN bundle install ${bundle_install_opt}

docker-compose.yml

version: '2'

services:
  web:
    build:
      context: .
      args:
        - bundle_install_opt

というようにすると、ローカル環境では docker-compose build とすれば開発用のgemがインストールされたイメージをビルドできます


とりあえず、引き続きメンテナンスできそうで一安心です

Azureデータサイエンス仮想マシンでハマったこと

東大松尾研のDL4USのLesson7 Section4は学習時間が相当かかるのでGoogle Colab上での実行が困難です。 (実際に40時間以上かかりました。。)

このため、このSectionだけは Azureにインスタンスを作って動かしました。

そのときのメモです。

一言でいうと、インスタンスの作成時の認証は SSH公開鍵キー」 ではなく 「パスワード」 にしたほうが良いかも? という話です。

この画面です

f:id:sanshonoki:20190704214732p:plain

Azureデータサイエンス仮想マシンのイメージには機械学習に必要なツール、ライブラリが一通りインストールされていて環境構築の手間が省けてとても助かります。

作成方法は 公式ページ の通りですが、イメージ選択で「Data Science Virtual Machine for Linux (Ubuntu)」を選択します。

なぜパスワード認証がよい?

この仮想マシンイメージには JupyterHub が最初からインストールされていて自動起動します。

なので、https://<仮想マシンのIPアドレス>:8000 で JupyterHub にアクセスできるのですが SSH公開鍵認証の場合は、そのパスフレーズで JupyterHubにログインできないのです... (UNIXパスワードをもつユーザーでないとログインできない)

f:id:sanshonoki:20190704212742p:plain:w400

パスワード認証でインスタンス作成するとUNIXパスワードをもつユーザーが作成されるのでそのパスワードでJupyterHubにログインできます。

なお、パスワード認証でインスタンス作成してSSH認証したいときはインスタンス作成後、~/.ssh/authorized_keys に公開鍵を置けばオーケーです。

ローカルPCからだと次の1行で登録できます。

$ cat 公開鍵のパス | ssh ユーザー名@IPアドレス 'cat >> .ssh/authorized_keys'

ユーザー名はパスワード認証にしたときのユーザー名です

SSH公開鍵認証でインスタンスを作ってしまった場合

ご安心あれ。Jupyter notebookの設定ファイルを作って Jupyter notebookを起動すれば大丈夫です。

通常通りの手順ですね。

  1. Jupyter Notebookの設定ファイルを作る

    $ jupyter notebook --generate-config
    
  2. .jupyter/jupyter_notebook_config.py を編集する

    1. パスワードのハッシュを作成する

      $ python -c 'from notebook.auth import passwd; print(passwd())'
      
    2. パスワードを設定する

      c.NotebookApp.password = 'sha1:XXX'  # 上で生成したハッシュ
      

あとは jupyter notebook で起動し、ポート8888にアクセスすればオーケー

ただし、このままだと暗号化されていないhttp通信になるので sshポートフォワーディングでログインしてからjupyter起動するのがよいです。

$ ssh -i 秘密鍵へのパス -L ローカルのポート番号:localhost:8888 ユーザー名@インスタンスのIPアドレス

そして http://localhost:ポート番号 にアクセスすればjupyterにアクセスできます。

リスクを承知でポート転送しないときは追加で

  • 設定ファイルで c.NotebookApp.ip = '0.0.0.0' を設定する
  • インスタンスの 受信ポートの規則 で 8888 の TCP を追加する

が必要です

東大松尾研のDL4USをやってみた感想

東大松尾研が公開しているエンジニア向けDeep Learning講座、DL4USの評判が良さそうなので一通りやってみました。

やってみての感想

これまでディープラーニングのオンライン講座は fast.ai が一番だと思ってました。

ただ、

  • オリジナルのライブラリで抽象化されすぎていて実装がよくわからん(概念は理解できるけど、、特にv2 !)
  • モデルの学習でGPUを使って数時間以上〜がちょいちょいある(という記憶)
  • YouTubeの教材をまじめに聴くとそれなりに長い(英語の勉強にもなっていいんだけど)

と、学習時間がそこそこ必要という不満もありました、個人的に。

(v1とv2を一通りやりました。今はv3のようですがv3のことはよくわかりません。。)


それに比べて、DL4USは

  • GANや強化学習まで含めて一通り網羅されていて
  • Google Colab上でサクっと動かせ、1セクションにかかる所要時間も短い
  • また、コードがシンプルなので自分で実装するときに参考になる


素晴らしい !! その気になれば数日で終わらせることもできる

基本的に以下のとても参考になる記事に従っていれば問題なく進められます。

qiita.com

うまくいかなかった箇所

MNISTの学習でなぜか発散する(Lesson1 Section1、Lesson2 Section2、Lesson4 Section2等)

もともとのコードでは最適化アルゴリズムoptimizer='sgd' になっていますがそのままでは自分の環境ではなぜかAccuracyが0.11となって学習が発散してしまいました。。

optimizer='adam'optimizer='rmsprop' にしてやるとAccuracyも98%ぐらいになってうまくいきました。

MS-COCOをunzipするときのTips (Lesson5 Section1)

Colab上でMS-COCOのデータを解凍するとファイル数が多すぎるため標準出力に大量にメッセージ出力され、 それに伴いMacのファンが唸り始めて一向に処理が終わる気配がありませんでした..

以下のように標準出力を出さないようにスクリプトを書き換えてやると問題なく解凍できました

! sed -i -e 's/-d download\//-d download 1>\/dev\/null/' download_mscoco.sh
! sed -i -e 's/N_TRAIN_DATA = 50000/N_TRAIN_DATA = 30000/' preprocess_mscoco.py

その他参考になったこと

パラメータの初期化 (Lesson1 Section3)

特にGlorotの初期化法は活性化関数が全て線形な場合の解析結果であり、中央付近が線形とみなせるsigmoid関数やtanh関数に適していると言えます。 また、Heの初期化法は活性化関数がReLUであるときに適しています。

巷のKerasのコードでは活性化関数がReLuであってもデフォルト値であるGlorot(Xavier)の初期化になっている場合が多いですが、ちゃんとHeの初期化になっていて感心しました

ちなみに、normal と uniform の使い分けはどうなの? と気になって調べてみたのですが 基本的には差はないようですね。。

【参考記事】

によると

  • 基本的には personal choice である
  • Bengioは uniform を好んで使っていた、一方、Hintonは normal を使っていた
  • ResNet では he_normal が使われている
  • DLライブラリは uniform を使っているのが多い (のでKerasもそれをデフォルトとしたとCholletさんが言っている)

そうです。

BatchNormalization (Lesson2 Section3)

⋆ 後続の研究で、Batch Normalizationは内部共変量シフトを軽減させる効果は実はほとんどないことが実験的に示されています。 一方でBatch Normalizationにより損失関数・勾配の形状が緩やかになり、勾配法などによる最適化が容易になることが実験的に・一部理論的に示されています。 出典: S. Santurkar et al. "How Does Batch Normalization Help Optimization? (No, It Is Not About Internal Covariate Shift)". arXiv. 2018

うーん、そうだったのか、、これは知らなかった、、

論文読んでみたいと思います

初仔の成績の分析

現3歳世代、ジェンティルドンナヴィルシーナといった名牝の初仔はPOG的には不発でした..(秋や古馬になって活躍するかもしれませんが)

初仔は走らないのでしょうか.. ??

そういうことどこかで聞いたことあるよなぁ、、と思いつつ調べてみると、

どうやら「初仔の牝馬は走らない」というのが定説となっているようです

手元のデータでも検証してみます。

データ取得

  • 3世代(2016-2017、2017-2018、2018-2019)の成績から2勝馬以上の母を抽出
  • ただし、産駒の1頭以上がN勝以上(年度に応じて2〜4)をあげた母馬に絞ります
  • 各産駒のPOG期間の勝利数をカウント
  • ただし、[地]や(地)といった地方所属馬は勝利数が多めに出てしまうので除外します

分析

産駒の平均勝利数(x軸)と初仔の勝利数(y軸)をプロットしてみます。

以下のようなグラフが得られました。

f:id:sanshonoki:20190615182743p:plain

直線は  y=x なので

  • この直線より下にプロットされた点は 初仔はその母馬の全産駒の平均より成績が悪い
  • 逆に直線より上にプロットされた点は 初仔はその母馬の全産駒の平均より成績が良い

ということを表しています。

これを見ると大半のプロットは直線の下に位置しており、初仔の成績はその母馬の産駒の平均的な成績に比べるとあまり奮ってないということになります。

牡馬と牝馬の比較

ここから性別ごとの比較をしていきます。

さきほどのグラフを牡馬、牝馬で分けて出すと次のようになります。

初仔の勝利数 割合(牡馬) 割合(牝馬
0 0.727 0.816
1 0.236 0.122
2 0.036 0.061

これを見ると、牝馬は牡馬に比べて0勝馬に終わる可能性が高いということは言えそうです

デビュー時の体重の比較

初仔の成績があまり良くない理由としては生物学的な要因以外にもいくつか考えられます

  • その産駒の育成のノウハウがたまっていない
  • 小柄に生まれる(?)

など。

育成に関してはデータのとりようがないので 2番目の体重説に関して、初仔のデビュー体重と2年目以降の産駒のデビュー体重の分布を出して検証してみました

牝馬の場合は、初仔のデビュー時の体重が2年目以降の産駒のデビュー体重と比べて明らかに小さいことが分かります。 おそらくこれも一因でしょう

平均勝利数が上位の母馬

データ分析の副産物として得られたPOG期間の産駒平均勝利数が上位の母馬のリストです 対象産駒数3以上だと信頼度としてはかなり高いと思います

母名 平均勝利数 対象産駒数
ヴィアメディチ 3.500000 2
スタセリタ 2.333333 3
ラドラーダ 2.250000 4
グロッタアズーラ 2.000000 3
ミスアンコール 2.000000 5
スターアイル 1.800000 5
ナスケンアイリス 1.666667 3
ラヴズオンリーミー 1.666667 6
エアトゥーレ 1.500000 8
ドバイマジェスティ 1.500000 4
ライラックアンドレース 1.500000 4
ナイスカット 1.400000 5
シンハリーズ 1.375000 8
ミスチフ 1.333333 3
ポールポジションII 1.250000 4
シーザリオ 1.222222 9
チャチャリーノ 1.200000 5
スパイオブラヴ 1.200000 5
フサイチパンドラ 1.142857 7

その他、興味深い記事

特別に初仔に着目した分析ではないですが興味深い記事を見つけました。

これによると、

繁殖牝馬は第2仔~第6仔において最高の産駒を送り出す可能性が非常に高い

そして、      

概して、第7仔以降の牝馬において競走能力と同様に繁殖能力にも衰えが見られ始める。

だそうです。また、

牝馬が繁殖入りしたばかりのときは、その牝馬を見て身体的に補完すると考えられる種牡馬と交配させます。その結果生まれてきた産駒が、予想とまったく異なることが時々あります。多くの場合、繁殖牝馬が初仔を出産する前に2年目の種牡馬はすでに予約されています。そのため、ほとんどの場合、種牡馬選びに磨きをかける機会があるのは、3年目の種牡馬を選ぶときなのです」。

3番目はなるほどなと思いました

来年以降の作戦にぜひ活かしたいと思います