CordovaによるReact.jsアプリのネイティブアプリ化でハマったところ

React.jsで作ったスライドショーアプリをCordovaでネイティブアプリ化しました。ƪ(•◡•ƪ)

f:id:sanshonoki:20170815204337p:plain:w200

github.com

いくつかハマった点があったのでまとめておきます。

なお、アプリ化の手順に関しては以下のサイトが大変参考になりました。♪(・ω・)ノ

qiita.com

Cordovaの導入やコマンドの使い方に関してはこのあたり

ハマった点… ><

react-routerを使ったときに起動ページのパスが違う

対策

各環境ごとに起動ページのパスが違うのでルーティングを複数用意する必要があります。

起動ページのパス
React.jsアプリ /
cordova serve ios /ios/www/index.html
cordova emulate ios /Users/xxx/Library/Developer/CoreSimulator/De…25B7D/CordovaReactSlickExample.app/www/index.html

なので、react-routerを使うときは

<Router>
    <Route exact path="/" component={App} /> <!-- Reactのwebアプリでの確認用 -->
    <Route path="*/index.html" component={App} /> <!-- cordovaアプリ用 -->
</Router>

のように2種類の初期ページ表示用のルーティングをもっておく必要がありました。

public/以下のassetsへのアクセス

これに関連して、React.jsアプリのpublic以下のassetsへの参照も少し工夫する必要がありました。

  1. 初期ページを起動時パス取得用のダミーページ(Homeコンポーネント)にする

     <Router>
         <Route exact path="/" component={Home} />
         <Route path="*/index.html" component={Home} />
     </Router>
    
  2. ダミーページで起動時のパスを取得し、Globalな state に保存する

     class Home extends React.Component {
         constructor(props) {
             super(props)
         }
    
         componentDidMount() {
             // 起動時のパスを取得する
             const root = path.dirname(this.props.location.pathname)
    
             // stateに保存するアクションを呼ぶ
             this.props.setRootPath(root)
         }
    
         render() {
             // 実際の初期ページへリダイレクトする
             return (
                 <Redirect to="/login" />
             )
         }
     }
    
     const mapDispatchToProps = dispatch => {
         return {
             setRootPath: (path) => dispatch(setRoot(path)) // 起動時パスを保存するアクション
         }
     }
     export default connect(null, mapDispatchToProps)(Home)
    
  3. assetsにアクセスするときは起動時パスを考慮した形で行う

    const root = getState().home.root // 起動時のパスをstateから取得
    const url = `${root === '/' ? '' : root}/images/slick/pictures.json`
    fetch(url)
    

Fetch API cannot load file:///android_asset/www/xx/xxx.json. URL scheme “file” is not supported のエラーが出た(Android

対策

fetchは file:// をサポートしてないらしく XMLHttpRequest を使って記述します。

https://github.com/github/fetch/pull/92#issuecomment-140665932

function fetchLocal(url) {
  return new Promise(function(resolve, reject) {
    var xhr = new XMLHttpRequest
    xhr.onload = function() {
      resolve(new Response(xhr.responseText, {status: xhr.status}))
    }
    xhr.onerror = function() {
      reject(new TypeError('Local request failed'))
    }
    xhr.open('GET', url)
    xhr.send(null)
  })
}

fetchをfetchLocalに置き換えればok。

このエラーはiOSのときには出ませんがfetchLocal置き換えの副作用はありません。

Unable to post message to https://www.youtube.com. Recipient has origin file://.のエラーが出た

対策

config.xml に以下を追加

<allow-navigation href="https://*youtube.com/*" />

https://cordova.apache.org/docs/en/latest/reference/cordova-plugin-whitelist/

モバイルでのYouTube埋め込み動画の自動再生

先日、React.jsの練習Webアプリ(スライドショー)でBGM機能としてYouTube埋め込み動画の自動再生を実装しました。

自動再生は以下のように埋め込みURLにautoplay=1のパラメータをつけることで実現できます。

<iframe width="854" height="480" src="https://www.youtube.com/embed/B2fPYlGKdXM?autoplay=1" frameborder="0" allowfullscreen></iframe>

が、モバイル端末では自動再生されないことが判明… 。

どうやら仕様のようです。(´Д`。)

YouTube Player API Reference for iframe Embeds  |  YouTube IFrame Player API  |  Google Developers

ChromeSafari などのモバイル ブラウザでは、HTML5 <video> 要素を再生するには、ユーザーの操作(プレーヤーをタップするなど)による起動が必要です。以下は、Apple のドキュメントの抜粋です。

「警告: ユーザーが費用負担する携帯電話ネットワーク経由で要求していないダウンロードを防止するために、iOSSafari では組み込みメディアを自動再生できません。必ずユーザーが自分で再生します。」

この制限があるため、autoplay、playVideo()、loadVideoById() などの関数およびパラメータはすべてのモバイル環境では動作しません。

結論

結論から書きましょう。

  • 音声をミュートした状態ではモバイル端末でも自動再生は可能
  • 音声再生ありだとモバイル端末では自動再生はできない

ということになります。

モバイル端末での埋め込み動画の自動再生

この記事に書いてあるように無音の状態であれば自動再生は可能です。

qiita.com

ただし、player.unMute()した時点で残念ながら自動再生はストップしてしまいます。

再生ボタンをそれとなく表示する

ならばユーザーに再生ボタンを押してもらうしかありません。

最初にトライしたのは以下の記事に書いてあるiframe埋め込み動画にオーバーレイして再生ボタンを表示させる方法です。

Youtube Iframe API not working for mobile devices? - Stack Overflow

  <style>
    iframe#player {
      position: absolute;
      left: 0x;
      top: 0x;
      width: 200px;
      height: 100px;
    }

    #play_button {
      position: absolute;
      left: 0px;
      top: 0px;
      opacity 0;
      width: 200px;
      height: 100px;
      pointer-events: none;
    }
  </style>

のように自前の再生ボタンをiframe埋め込み動画と同じ位置に同じサイズでオーバーレイします。 pointer-events: none;によって再生ボタン上でのクリックイベントはiframe埋め込み動画の要素上で受け取れ、動画を再生できます。

オーバーレイした状態 オーバーレイしない状態(参考)
f:id:sanshonoki:20170814051612p:plain:w100 f:id:sanshonoki:20170814051622p:plain:w100

クリックした場所によって挙動が違う..?!

自前の再生ボタンのクリックした場所によって挙動が違うという現象に出くわしました..。

  • 想定通りにインラインで動画が再生され、オーディオも流れる
  • なぜか新しいタブを開いてフルスクリーンで動画が再生される

調べてみると、

f:id:sanshonoki:20170814054505j:plain

曲名の部分が https://www.youtube.com/watch?v=... へのリンクとなっており、ここをクリックすると新しいタブで開いてしまいました。

ユーザーがボタンのどのエリアをクリックするかは分かりません。。

なので、このままではNGです。

showinfo=0を指定すると曲名のリンクは非表示にできますが代わりにYouTubeロゴのブランディングが表示されてこれがhttps://www.youtube.com/watch?v=...へのリンクになって同じことになります。。

f:id:sanshonoki:20170814085851j:plain

Playerのパラメータを以下のようにチューニングするとPC閲覧時にはYouTubeロゴを非表示にできましたがモバイル閲覧ではYouTubeログは表示されてしまいます..。(>д<;)

playerVars: {
    showinfo: 0,
    controls: 2,
    modestbranding: 0,
    playsinline: 1,
},

最終的に行き着いた方法

YouTubeの再生ボタンをそのまま利用することにしました。

具体的には表示サイズをYouTubeの埋め込み動画の再生ボタンを同じ大きさにすることで再生ボタンのエリアのみが露出するようにしました。

const opts = {
  width: '40', // same size of play button
  height: '30', // same size of play button
  playerVars: {
    showinfo: 0,
    controls: 2,
    modestbranding: 0,
    playsinline: 1,
  },
}

この設定にすることでモバイル端末での表示は以下のようになります。

f:id:sanshonoki:20170814090934j:plain

この状態だとボタンのどこをクリックしても新しいタブで再生することなくinlineで動画(音声)が再生されます。:-)

ただ、iOSiPhoneの場合は動画再生は全画面表示となるらしくこの方法でもインラインでのBGM再生はできませんでした.. (iOS10からはインライン再生できるようです)

React.jsでBGMつきスライドショーを作る

React.jsの練習としてスライドショーアプリを作ってみました。

React Slideshow Sample f:id:sanshonoki:20170801050429p:plain

単なるスライドショーでは面白くないのでBGMをYouTubeから検索して流せるようにしました。

検索ボックスに曲名などのキーワードを入れてボタンをクリックすればYouTubeから検索してそれを自動再生するようにしてあります。お気に入りである AIさんの Story をテキストボックスの初期値に入れています。:-)

他にも

あたり今回のサンプル写真のテーマ(子どもの成長)にハマるかなと思っています。 他にもハマる曲があったらぜひ教えてください

コードはこちらにあり、public/images/slick/ 以下に好きな写真を置けばその写真を使ってスライドショーができるので興味ある方はご覧ください

github.com

BGM検索&再生

やっていることはシンプルです。

  1. YouTubeでキーワード検索する (GET https://www.youtube.com/results?search_query=...
  2. 検索結果のhtmlから曲のId(data-context-item-id)とタイトルを抜き出す
  3. iframeで表示する(https://www.youtube.com/embed/{曲Id}?autoplay=1

React.js側からYouTubeページを取得しようとすると No 'Access-Control-Allow-Origin' header is present on the requested resourceエラーが出てアクセスできないのでYouTube検索はRails側でやっています。

スライドショー

スライドショー機能は React対応した react-slick があったのでそれを使いましたが表示で少し苦戦したのでメモ

写真が真ん中に表示されず左寄せになってしまう

以下の設定をCSSに加えることにより中央に表示されるようになりました。 これで正しいやり方なのかはよく分かりません..

  div.slick-initialized div.slick-slide {
    display: flex;
    justify-content: center;
  }
左右のArrowが表示されない

slick-theme.cssでなぜか color: transparent になってました。 なのでCSSを上書きすると表示されるようになりました。

  .slick-next:before, .slick-prev:before {
    color: gray;
  }

会議と打ち合わせ

最近、「博報堂のすごい打ち合わせ」という本を読みました。

f:id:sanshonoki:20170714041701j:plain https://www.amazon.co.jp/dp/4797391340

「5割雑談でも最高の結論を導き出す博報堂の打ち合わせ術」というそそる内容で始まります。。

面白い内容で通勤の帰りの電車の中で一気に読破してしまったのですがその中でもああ、そうかと思って頭に一番残ったのが「会議」と「打ち合わせ」は違うというものです。

  • 会議: 情報の共有(報告、連絡、相談)のために行われる
  • 打ち合わせ: 考えやアイデアを出し合い、積み上げていくために行われる

自分は「会議」よりも「打ち合わせ」していきたいです。

そして、それを頭に焼き付けるために今回、それぞれに別名(ラベル)をつけてみました。

  • 会議とは・・・「予定調和」である
  • 打ち合わせとは・・・「真剣勝負」である

これを見ると「打ち合わせ」したくなりませんか?w

さぁ、真剣勝負しましょう♪

これから必要なABCDE!

ABCDE! って何よ?!

って思われたと思いますが

イノベーションを生むのに必要になってくると思われる資質や要素を昔考えてみたことがあって今日はそれというかその覚え方を紹介してみます。

A ・・・ Analysis、Abduction、AI

B ・・・ Business domain knowledge、Business Intelligence

C ・・・ Coding、Communication、Collaboration

D ・・・ Design、Design thinking

E ・・・ Engineering、Experience、English

!・・・ Inspiration


いっぱいありますね。。(;^_^A

この中で Abduction は 耳慣れない単語だと思いますが 仮説推論 という意味です。つまり、仮説を生み出す力です。

アブダクション - Wikipedia

結果や結論を説明するための仮説を形成することを言うこともある。哲学やコンピュータの分野でも定義づけされた言葉として使われている。アブダクションの意味や思考法は、演繹法帰納法ともまた異なるものであり、失敗の原因を探ったり、計画を立案したり、暗黙的な仮説を形成したりすることにも応用できる。例えば、プログラムの論理的な誤りを探し出し直すという過程では、アブダクティヴな解釈と推論が行われており、一般的な立証論理の手法と通じるものがある

AI は当時は入れてなかったのですが今回のブログで書くにあたって足しました。 あと、Communication・Collaboration や English もやはり大事… ということでリストに追加しました。 身につけるべきことがどんどん増えていきますね。。大変な世の中です。

Experience は資質ではないですが新しいアイディア、概念を生むにはいろいろな経験をすることが必要だという意味で入っています。

自分もあらためてこれらのスキル、要素を意識していこうと思います。┗(  ̄◇ ̄)

パドック画像から距離適性を推測する(転移学習編)

果たして転移学習で改善するのか?! やってみました! ((p・ω・q))

chainerで転移学習をするにあたって以下の記事を参考にしました。

転移学習でハマってかつ学習結果も惨敗… でしたがchainerで転移学習する方法の勉強になりました。。

ハマった点

  1. 学習済みAlexnetのパラメータがcopyされない

    • 対策: L.Classifierのモデル(model)でなく model.predictor を copy_model の引数として渡す
    • 対策: モデルを定義するときに 入力次元として None を使わない
    • chainerのAlexnetを用いてFine Tuningをする | TOMMY NOTES 」の記事の中の copy_model 関数はインデントずれがあってそのままコピペするとうまく動作しません…

    chainerでは入力次元を None としたときは前方向への伝搬(Forward propagation)を計算する中で動的に次元を計算するので計算をしてない状態では次元が不定となり、copy_modelの中での次元比較の際ミスマッチとなりコピーされません。

    あと、参考にした記事ではcopy_model(original_model, model)でコピーできてそうなのですが私のコードではcopy_model(original_model, model.predictor)としないとコピーできませんでした。。

  2. FC層だけ学習させるときにEvaluatorでエラーがでる

    • 対策: hook関数を使って重みを更新しないレイヤの勾配を削除する

    最初に https://github.com/chainer/chainer/issues/724https://groups.google.com/forum/#!searchin/chainer/Finetuning/chainer/H4IWqcMBA2w/8cxt58YrBwAJ でやられている volatileフラグを制御する方法でやってたのですが、これだとEvaluatorの実行時に ValueError: ON and OFF flags cannot be mixed. というエラーが出てしまいました…。(Evaluatorをオフにするとエラーは出ずに学習できました)

    最終的に、Evaluatorでもエラーを出さずにうまくいった方法は「Chainerでfine-tuningを行う - Qiita 」にあった hook関数を使って勾配をリセットするというものでした。ちなみに、この方法はすべてのレイヤで勾配を一度計算することになるので計算時間は短縮されません。。(volatileを使うと勾配は計算しなくなるので計算時間が短くなります)

    あと、chainer v2から導入された chainer.no_backprop_mode()のスコープを使うとうまくいきそうです。v1.x.xでもこのスコープは(なぜか)使えてしまうのですがただ重みは全レイヤで更新されてしまいここで時間を食ってしまいました..。以下のMNISTでは chainer v2では特定のレイヤだけ重み更新できることを確認しています。


MNISTで特定のレイヤだけ重みを更新するサンプル github.com

Alexnetを転移学習した結果

誤差は以下のようになりました。

f:id:sanshonoki:20170626044354p:plain

学習データの誤差が減らずうまく学習できないことが伺えます…。[´゚Д゚`]

最後は発散してどんな入力に対しても同じ出力値(4.2 = おおよその中央値)が出るようになってしまいました…。 通常の学習の検証用データでの誤差が同じ値を出し続ける場合の誤差よりも大きいということで何とも切ないです.. ( ;∀;)

ちなみにFC層だけ学習させるのではなく、全階層で学習させたら通常と同等の誤差まで減っていきました。

なので、Kerasで学ぶ転移学習 で書かれている

データが少ない・似ていない これは転移学習が困難なパターンです。データが少ないので過学習を防ぐために上層だけを学習させたいところですが、似ていないデータを使って学習しているため、上層の特徴を使ってもうまく学習できないと考えられます。

というパターンなのでしょう。。

一般的な物体認識問題とはかなり異なる問題設定だったということでしょう、そりゃあそうですよねという気はしますが残念です。。でも、勉強になりました。

最後に、誰かの役に立つかもしれないので転移学習に関係する箇所のコードをのせておきます。

– train_ft.py –

class DelGradient(object):
    name = 'DelGradient'

    def __init__(self, delTgt):
        self.delTgt = delTgt

    def __call__(self, opt):
        for name, param in opt.target.namedparams():
            for d in self.delTgt:
                if d in name:
                    grad = param.grad
                    with chainer.cuda.get_device(grad):
                        grad *= 0

def copy_model(src, dst):
    assert isinstance(src, chainer.Chain)
    assert isinstance(dst, chainer.Chain)
    for child in src.children():
        if child.name not in dst.__dict__: continue
        dst_child = dst[child.name]
        if type(child) != type(dst_child): continue
        if isinstance(child, chainer.Chain):
            copy_model(child, dst_child)
        if isinstance(child, chainer.Link):
            match = True
            for a, b in zip(child.namedparams(), dst_child.namedparams()):
                if a[0] != b[0]:
                    match = False
                    break
                if a[1].data.shape != b[1].data.shape:
                    match = False
                    break
            if not match:
                print('Ignore %s because of parameter mismatch' % child.name)
                continue
            for a, b in zip(child.namedparams(), dst_child.namedparams()):
                b[1].data = a[1].data
            print('Copy %s' % child.name)

def main():
    ...

    model = L.Classifier(alexnet.FromCaffeAlexnet(1), lossfun=F.mean_squared_err
or)

    original_model = pickle.load(open('alexnet.pkl', 'rb'))
    copy_model(original_model, model.predictor)
    model.compute_accuracy = False

    ...

    optimizer.add_hook(DelGradient(["conv1", "conv2", "conv3", "conv4", "conv5"]))

    ...

– alexnet.py –

class FromCaffeAlexnet(chainer.Chain):
    insize = 128
    def __init__(self, n_out):
        super(FromCaffeAlexnet, self).__init__(
            # conv1=L.Convolution2D(None, 96, 11, stride=2),
            # conv2=L.Convolution2D(None, 256, 5, pad=2),
            # conv3=L.Convolution2D(None, 384, 3, pad=1),
            # conv4=L.Convolution2D(None, 384, 3, pad=1),
            # conv5=L.Convolution2D(None, 256, 3, pad=1),
            # my_fc6=L.Linear(None, 4096),
            # my_fc7=L.Linear(None, 1024),
            # my_fc8=L.Linear(None, n_out),

            # Don't use None when you copy parameters
            conv1=L.Convolution2D(3, 96, 11, stride=2),
            conv2=L.Convolution2D(96, 256, 5, pad=2),
            conv3=L.Convolution2D(256, 384, 3, pad=1),
            conv4=L.Convolution2D(384, 384, 3, pad=1),
            conv5=L.Convolution2D(384, 256, 3, pad=1),
            # my_fc6=L.Linear(None, 4096),
            # my_fc7=L.Linear(None, 1024),
            # my_fc8=L.Linear(None, n_out),
            my_fc6=L.Linear(256 * 7 * 7, 4096),
            my_fc7=L.Linear(4096, 1024),
            my_fc8=L.Linear(1024, n_out),
        )
        self.train = True
 
    def __call__(self, x):
        # for chainer v1.x.x 
        h = F.max_pooling_2d(F.local_response_normalization(
            F.relu(self.conv1(x))), 3, stride=2)
        h = F.max_pooling_2d(F.local_response_normalization(
            F.relu(self.conv2(h))), 3, stride=2)
        h = F.relu(self.conv3(h))
        h = F.relu(self.conv4(h))
        h = F.max_pooling_2d(F.relu(self.conv5(h)), 3, stride=2)
        h = F.dropout(F.relu(self.my_fc6(h)), train=self.train)
        h = F.dropout(F.relu(self.my_fc7(h)), train=self.train)
        h = self.my_fc8(h)

        # for chainer v2.x.x
        # You don't need to use DelGradient hook.

        # with chainer.no_backprop_mode():
        #     h = F.max_pooling_2d(F.local_response_normalization(
        #         F.relu(self.conv1(x))), 3, stride=2)
        #     h = F.max_pooling_2d(F.local_response_normalization(
        #         F.relu(self.conv2(h))), 3, stride=2)
        #     h = F.relu(self.conv3(h))
        #     h = F.relu(self.conv4(h))
        #     h = F.max_pooling_2d(F.relu(self.conv5(h)), 3, stride=2)
        #     with chainer.force_backprop_mode():
        #         h = F.dropout(F.relu(self.my_fc6(h)), train=self.train)
        #         h = F.dropout(F.relu(self.my_fc7(h)), train=self.train)
        #         h = self.my_fc8(h)

        return h

コード全体はこちらにあります。 github.com

FloydHubを使う

FloydHubディープラーニング向けのHerokuという位置付けのPasSサービスです。 Herokuはいつもお世話になっているので(無料プランだけど…)この触れ込みを聞くと試さずにはいられません。。 ( ̄ー ̄)

ということで、使ってみました。

FloydHub とは

  • 簡単にクラウド上で学習が実行できる (実際に簡単だった! ^^)
  • クレジットカードなしでも登録できトライアルとして100時間分GPUが利用できる
  • コストはAWSの約50%で課金は秒単位でリーズナブル

Every one who signups to Floydhub will receive 100 hours of free CPU / GPU time for running your projects

とあるのでもしかしたら CPUとGPUを合わせて100時間かもしれません。 利用時間はダッシュボードで確認できます。

私が好きなChainer含めて主要なフレームワークが使用できます。 http://docs.floydhub.com/home/environments/

Chainerは↓の表の通り世界的に見るとまだまだ普及してないですがそのChainerもサポートされているのは嬉しいです。 TensorFlow以外は基本的に最新版のみのようです。

使えるFramework Github star
TensorFlow 60651
Caffe 18509
Keras 16571
MXNet 10051
Torch 6995
Theano 6453
PyTorch 5459
chainer 2594
Kur 537

使い方

公式ページのGet started 通りやればできます。

ジョブの実行

$ floyd init YourProjectNameでプロジェクトを作成したら $ floyd run "python train.py"のように floyd runに続けて実行するスクリプトを渡します。 必ずしもpythonを使う必要はなく $ floyd run "ls -la ." といったこともできます。

ジョブをrunするたびにディレクトリ全体がアップロードされるので不要なファイルが プロジェクトディレクトリの中にないか気をつける必要があります。 アップロードしたくないファイルは .floydignoreに記述できます。

なので、学習に使う巨大なデータはdataコマンドを使って使い回し可能なデータセットとしてアップロードしておく必要があります。

シェルスクリプトには実行属性がつかないので $ floyd run "./yourtest.sh" はNGです。 $ floyd run "sh ./yourtest.sh"とします。

GPUインスタンス

floyd run に --gpuオプションをつけて実行するとGPUインスタンス、何もつけないとCPUインスタンスです。

TensorFlowは基本的に自動的にGPUを使ってくれますが chainerだと通常、プログラム側の引数にも --gpu で渡してやる必要があります。

$ floyd run --gpu --env chainer "python train.py --gpu 0"

な感じです。TensorFlowがデフォルトとなっていてそれ以外は--envオプションでフレームワークを指定しないとエラーになります。

ログ

プログラムの出力ログは $ floyd logs [-t] <RUN_ID> で参照できます。 ダッシュボードからだと見つかりにくいのでコマンドラインで確認するのが良さそうです。

ダッシュボード上でログを参照する方法

f:id:sanshonoki:20170616214717j:plain f:id:sanshonoki:20170616214738j:plain

Experimentsの各ジョブを開いたときのVIEW LOGボタンではプログラムの出力ログは見れません。。

出力ファイル

プログラム上で /output ディレクトリにファイル出力すると $ floyd output <RUN_ID> で出力ファイルを参照できます。 $ floyd output <DATA_ID> は NG です。

DATA_ID は $ floyd info <RUN_ID> で調べます。

データセット

巨大なデータはデータセットとして一度アップロードすると学習プロジェクトの実行時に再アップロードなしに何度でも参照できます。

手順は

  1. データを置いてあるディレクトリに移動
  2. データプロジェクト作成 $ floyd data init YourDataName
  3. アップロードする $ floyd data upload

あとは $ floyd run --data <DATA_ID> "python train.py"のように --dataオプションで DATA_ID を渡すとプログラムの中で /inputディレクトリとして参照できます。

データセットをブラウザで確認するのは $ floyd data output <DATA_ID>、データセットを削除するのは $ floyd data delete [-y] <DATA_ID> です。

すべてのデータセットの一覧は $ floyd data status で可能です。

また、floyd run した各ジョブの出力結果も DATA_ID を持っており --data <DATA_ID> で 出力ディレクトリを /input ディレクトリとして使えます。

Jupyter Notebook

$ floyd run --mode jupyter とすると Jupyter Notebookも使えます。Jupyter Notebookを立ち上げている間は何もしなくても課金対象となるのでスクリプトを実行するのに比べるとちょっと勿体無い感はありますね。。

注意点としては明示的に stop しないとずっと課金され続けてしまう点です。

使い終わったら $ floyd stop <RUN_ID> を忘れずに !! (あるいは、ダッシュボード上で停止ボタンをクリックする)

公式ページでも

Once you have experimented with your code, you need to manually stop your “job”. Run the stop command for this. Remember Jupyter notebooks are charged for the entire duration they are up, not just when you execute code. So make sure the stop the notebooks when you are no longer working on them.

と注意書きがありますが目立つように書いてないです。 何か勘ぐってしまうのは私だけでしょうか。。

保存したNotebookは /output に出力されますので $ floyd output <RUN_ID>で参照できます。

古いジョブやデータセットの削除

  • ジョブの削除: $ floyd delete [-y] <RUN_ID> <RUN_ID>...
  • データの削除: $ floyd data delete [-y] <DATA_ID> <DATA_ID>...

で削除できますがジョブIDやデータIDを明示的に指定してやる必要があります。 最近になって複数個同時に削除できるようになったみたいですがIDを調べるのがかなり面倒です。

プロジェクト単位で一括で削除するコマンドは今のところないようです。

Priceページ

Floyd stores any output files generated by the project and stored under /output directory at run time. You will be charged for the size of data genarated by your project. Pricing details below.

と書いてあるようにジョブ実行時の出力ファイルも課金対象となってしまうのでプロジェクト単位での一括削除は近いうちに対応されるのではないかと思います。

ジョブやデータを一括で削除する方法

statusコマンドとシェルスクリプトを組み合わせれば一括で削除することが可能です。

  • ジョブの一括削除

    $ floyd status 2>&1 | awk '{print $1}' | tail +3 | xargs floyd delete -y

  • データの一括削除:

    $ floyd data status 2>&1 | awk '{print $1}' | tail +3 | xargs floyd data delete -y

なお、ジョブの一括削除の場合はプロジェクトディレクトリ上で実行する必要があります。

ジョブを削除してもジョブが出力したデータは残り続けるのでこれまた盲点です。

価格比較

AWSとの比較

$1 = 110円 として

インスタンス コスト(1時間) スペック GPU
AWS EC2 g2.2xlarge 98.78円 8コア、メモリ15GB GPUメモリ4GB ストレージ60GB GRID K520
FloydHub G1 47.52円 4コア、メモリ61GB GPUメモリ12GB ストレージ100GB Tesla K80

AWSのEC2 GPUインスタンスを使うのに比べて約50%の料金でスペック差も歴然です。 EC2は時間単位で課金されるので 1秒でも使うと1時間分課金されますが FloydHub は秒単位で課金されるので変なストレスもかかりません。 (´◡`)

自作PCとの比較

自分のPCに積んでいる GeForce GTX 950 と 個人ユースでは高性能にあたる GeForce GTX 1080 の2パターンで試算してみました。

参考記事

電気代 = 28円 / 1kWh として

GPUボード
(推奨システム電力)
コスト(1時間) スペック ボード価格
GeForce GTX 950
(350W)
約9.8円 GPUメモリ2 GB 約1.5 - 2万円
GeForce GTX 1080
(500W)
約14円 GPUメモリ8GB 約7万円

「趣味用に安く深層学習PCを作った」の初期コストは約12万円なので 3580時間以上で 自作PCの総コスト(イニシャル+電気代)< FloydHubのコスト となります。約10-15円/時間と比べるとFloydHubの 47.52円/時間 はそれでもやっぱりまだ高いよなぁ..という印象を持ちますが初期コスト、構築の手間とメンテナンスのことも考えると、十分アリなんじゃないかなと思います。

まとめ

FloydHub、本当に簡単にディープラーニングGPU学習を始められます。 仕事で使うとあっという間に100時間分の無料枠は使いきると思いますがこれからディープラーニング始める人は挫折するかしないかの分かれ目までは無料枠内でいけると思うのでぜひ使ったほうがいいと思います。 (*´д`)o