backstage

合唱音源の新着情報の舞台裏

【Azure Cognitive Services】画像認識でalt属性の説明文を自動生成してみた

引越して1ヶ月半、ようやく家にネットが通ったので何かコードを書こうと思いました。

Microsoft Azure Cognitive Servicesには便利そうなたくさんAPIがあります。 そのうちComputer Vision APIを使うと、画像を解析して説明文などを自動生成できます。

参考:過去の記事 s2terminal.hatenablog.com

Computer Vision APIが吐き出す情報は英語か中国語です。 今回はさらにこれをTranslator Text APIで日本語化し、imgタグにして吐き出すプログラムを書いてみました。

f:id:s2terminal:20170814035609p:plain:w360

Public Domain Picturesから適当にいくつか画像を取ってきて、説明文を付けてもらいました。

「木からぶら下がって緑のバナナの束」

木からぶら下がって緑のバナナの束

<img alt="木からぶら下がって緑のバナナの束" src="http://www.publicdomainpictures.net/pictures/130000/velka/banana-bunch-1442837481wI7.jpg#.WZCXAvp82Mc.link">

画像のURLを入力するだけで、こういったalt属性のテキストを自動で書いてくれます。 精度はなかなか良い感じです。

「フェンスの横にゼブラ立って」

フェンスの横にゼブラ立って

<img alt="フェンスの横にゼブラ立って" src="http://www.publicdomainpictures.net/pictures/230000/velka/zebra-at-groenkloof-picnic-spot.jpg#.WZCX2R_ZqTY.link">

フランス料理の添え物みたいな言い回しが気になりますが、内容は完璧です。

「白砂のビーチに立っている人」

白砂のビーチに立っている人

<img alt="白砂のビーチに立っている人" src="http://www.publicdomainpictures.net/pictures/30000/velka/drink-on-beach.jpg#.WZCRVmnOXuE.link">

心霊写真でしょうか。

実装

ReactTypeScriptwebpackという、ほとんど私に馴染みのない技術の組み合わせで作りました。よく調べもせずノリで技術選定したので、生産性の低さが半端無いです。

こんな感じで環境が用意できます。準備は簡単です。

$ echo "{}" > package.json
$ npm install --save-dev webpack typescript awesome-typescript-loader source-map-loader
$ npm install --save react react-dom @types/react @types/react-dom
$ npm install --save superagent @types/superagent

実装時間の8割は、ReactでAPIキーみたいなセキュアな設定情報をどこにどう書けば良いのか悩んでいました。

残りの2割はドラゴンクエスト11でマジスロを回しながらコードを書いたので我ながらかなり適当です。はぐれメタルヘルムがもう1個欲しいので仕方ないです。

メイン処理になるmain.tsxはこんな感じです。

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import * as SuperAgent from 'superagent';
import * as config from './config';

interface ImageFormProps {
}
interface ImageFormState {
  image_src: string;
  textarea_value: string;
  header: string;
}

class ImageForm extends React.Component<ImageFormProps, ImageFormState> {
  constructor(props) {
    super(props);
    this.state = { image_src: '', textarea_value: '', header: '画像のURLを入力して下さい' };
    this.handleChangeURL = this.handleChangeURL.bind(this);
  }
  handleChangeURL(event) {
    var image_url = event.target.value;
    this.setState({image_src: image_url,header: "(解析しています...)"});
    SuperAgent
      .post(config.computer_vision.api_url)
      .set('Ocp-Apim-Subscription-Key', config.computer_vision.api_key)
      .set('Content-Type', 'application/json')
      .send({url:image_url})
      .end(function(error, response){
        if (error) { return this.setState({header: response.text}); }
        var desc = JSON.parse(response.text).description.captions[0].text;

        this.setState({header: `(翻訳APIのアクセストークンを取得しています...)`});
        SuperAgent
          .post(config.translator_text.issue_token_url)
          .set('Content-Type', 'application/json')
          .set('Accept', 'application/jwt')
          .set('Ocp-Apim-Subscription-Key', config.translator_text.api_key)
          .send()
          .end(function(error, response_token){
            if (error) { return this.setState({header: response_token.text}); }
            var token = response_token.text;
            this.setState({header: `(「${desc}」を翻訳しています...)`});

            SuperAgent
              .get(config.translator_text.api_url)
              .query({
                'appid': 'Bearer ' + token,
                'text': desc,
                'to': 'ja',})
              .set('Accept', 'application/xml')
              .end(function(error, response_trans){
                if (error) { return this.setState({textarea_value: response_trans.text}); }
                var parser = new DOMParser();
                desc = parser.parseFromString(response_trans.text, 'text/xml').firstElementChild.textContent;

                this.setState({header: desc, textarea_value: `<img alt="${desc}" src="${image_url}">`});
              }.bind(this));
          }.bind(this));
      }.bind(this));
  }
  render() {
    return (
      <div>
        <h1>{this.state.header}</h1>
        <form>
          <input
            type="url" placeholder="http://www.example.com/" size={40}
            onBlur={this.handleChangeURL}
          />
        </form>
        <textarea
          placeholder="ここにimgタグが出力されます" rows={5} cols={40}
          value={this.state.textarea_value}
        />
        <img src={this.state.image_src} width="100%" />
      </div>
    );
  }
}

ReactDOM.render(<ImageForm />, document.querySelector('#app'));

handleChangeURL()のコールバックをみると残念な気持ちになってきます。 納品用コードはちゃんとPromiseか何か使ってきれいに書いたほうが良いと思います。

ソースコードの全文はGitHubに上げました。

github.com

まとめ

これくらいなら人間の手で説明文を書いたほうが早いと思いました。

参考文献

【IIJmio×mineo×WiMAX】モバイル回線3種でスプラトゥーン2してみた

f:id:s2terminal:20170716150553j:plain

先日、スプラトゥーン2 前夜祭が開催されました。 データが本編には引き継がれず勝敗が関係ないので、試しにモバイル回線を使ってみることにしました。

格安SIMであるmineoIIJmioテザリング、およびUQ WiMAXモバイルルーターを使って、それぞれ3試合ずつしてみました。

回線のスペック

- WiMAX2+ mineo IIJmio
下り 14.25 Mbps 2.09 Mbps 1.42 Mbps
上り 6.66 Mbps 3.38 Mbps 4.03 Mbps
ping 67 ms 108 ms 113 ms

(測定環境: 大阪市内 休日 日中 RBB SPEED TESTを利用)

注目すべきはping応答速度で、WiMAXの67msに対して、テザリングのmineoとIIJmioは2倍近い差をつけられています。 WiMAXなら比較的ラグの少ない環境で通信対戦できると言って良いと思います。

ナワバリバトル結果

f:id:s2terminal:20170716150612j:plain

3回線での合計9試合中、WiMAXで2勝、残りは全敗という散々な結果に終わりました。

単純に、私の得意武器が使えずスプラシューターコラボを使うしかなかったのもありますが、ちょっとひどい戦績です。

f:id:s2terminal:20170716150659j:plain

プレイ中、ゲームが止まったりワープするといった、ラグによるあからさまな異常は起きませんでした。 安定した通信さえできていれば、最低限のマナーを守って遊ぶことができると言えます。

シビアな格闘戦を制することもでき、スペシャルウェポンのジェットパックを使えば次々とキルを取ることができ、普通に楽しむことはできました。 しかし塗りやダメージの反映がちょっと遅れている感じはして、その結果相打ちになったりはしました。ある程度は枷になっていると思います。

またIIJmio利用中に1度だけ、マッチング中に通信エラーが起きてしまい対戦できない時がありました。

通信量について

1回のナワバリバトルに使用する通信量は約10MB/3分程度でした。 秒間に直すと55KB程度なので、通信速度は大抵の4G回線ならば問題ないと思います。

100試合で1GBと考えると、ちょっとずつ遊ぶ分にはモバイル回線の月間通信量が大きく不足することは無いでしょう。 ただしフェスでカンストまで進めたり、ガチマッチをやり込んだりすると、足りなくなると思います。

まとめ

f:id:s2terminal:20170716150815j:plain

  • モバイル回線でも遊べないことはない
  • やり込むなら固定回線必須

NintendoSwitchのスプラトゥーン2は前作WiiUと異なり携帯モードで遊ぶこともでき、必ずしも自宅など固定回線がある環境でなくてもプレイができます。 出先でちょっと遊ぶくらいなら4Gスマホのモバイル回線テザリングでも可能です。 しかし、本格的に楽しむためにはやはりモバイル回線ではなく、家庭の高速で安定した固定回線が必要になってくると思いました。

個人用mastodonサーバーを構築する

f:id:s2terminal:20170423131210p:plain

ニンジャスレイヤーのmastodonサーバーができたようですが、私はmastodonアカウントを持っていないのでフォローできませんでした。

diehardtales.com

上記記事より引用

あなた自身でサーバーを立てて象になることもできますが、それにはある程度のUNIX知識が求められます。

ではせっかくなので、個人用にサーバーを立てて象になってみました。

なおUNIX知識が求められるそうですが普通にLinux(CentOS7)で構築します。

用意したもの

mastodonAWS S3に対応しているそうなので、特に理由が無ければサーバーもAWS EC2で用意するのが無難だと思います。 真面目にやるならAWS ECS+S3+ElastiCache+PostgreSQL on RDSみたいな構成になるのではと思いますが、今回は個人用なので適当です。

サーバーにはdockerやnginxなど必要なものを入れておきます。

アプリケーションサーバーの準備

jtwp470.hatenablog.jp

上記記事などに沿ってdockerイメージのビルド、RailsのアセットプリコンパイルとDBマイグレーションまで終わらせます。 ちょっと時間がかかりますが、気長に待ちます。

最終的に、$ docker-compose up -d すればOKです。

SSL証明書の取得

qiita.com

この作業のときにサーバーの443番ポートをパブリックに開放する必要があります。 (逆に、このとき以外はファイアウォールで必要なアクセス元以外を閉めておけば個人利用には問題ありません)

Let'sEncryptの証明書は3ヶ月で切れますが、3ヶ月も経てば飽きて辞めるか本格運用のためイチから作り直すと思うので自動更新とかはしなくて良いと思います。

メールサーバーの設定

個人利用ならばSMTPサーバーは用意しなくてもDBを直接操作でOKみたいですが、今回はせっかくなのでSendGridを使ってメール送信設定をします。

s2terminal.hatenablog.com

SendGridのアカウント情報を.env.productionに書いておきます。

シングルユーザーモードに設定

qiita.com

上記を参考に、自分のアカウントに管理者権限を付けて、 .env.productionSINGLE_USER_MODE=trueをコメントインし、docker-composeの再ビルドを実行します。

ファイアウォールの設定

今回は個人の検証用途なので、443番ポートは個人のIPアドレス以外に対して基本的に閉じています。IDCFクラウドIPアドレス仮想ルーターファイアウォールで設定しています。

他に、フォローしたいアカウントのあるサーバーからのアクセスを許可する必要があります。今回紹介したようなシンプルな構成だとドメインのAレコードIPとゲートウェイIPアドレスは一緒だと思うので、たとえばニンジャスレイヤーをフォローするには$ dig a dhtls.netした結果をファイアウォールの設定に貼り付けていますが、この運用には限界があります。

本格的に使いたいならば443番ポートをパブリックにできる環境を用意する必要があると思います。

参考

本当にサイトの離脱を減らすべきか - 書評『逆説のスタートアップ思考』

ゼルダの伝説 ブレス オブ ザ ワイルド』で忙しい中ですが、『逆説のスタートアップ思考』(中公新書ラクレ)という本を読みました。

f:id:s2terminal:20170320181927j:plain:w320

半年くらい前に公開された同名のスライドの書籍化で、多くの内容が重複しています。SlideShareに目を通して完璧に内容を理解できたという方ならば特に本書を購入するメリットは乏しいと思いますが、私はそうではなかったので今回書籍を購入しました。

www.slideshare.net

著者である馬田隆明氏は元MicrosoftのVisualStudioエバンジェリストだったそうです。

medium.com

例: Google検索のKPI

突然ですが、もし自分がGoogle検索のサイト運営者だったとしたら、何を考えるでしょうか。

Google検索のKPI…PV,UU,継続率,滞在時間,離脱率,回遊率などなど…を置いて、それらに目標数値を設定して、改善に向けてPDCAを回して、というのは容易に想像できます。

  • PVを上げるにはどうしたら良いか?
  • 継続率を上げるにはどうしたら良いか?
  • 離脱率を下げるにはどうしたら良いか?
  • サイト滞在時間を上げるにはどうしたら良いか?

私だったら、こんな事を日々悶々と考えながら、仮設をいっぱい立てて、検証して、という事をすると思います。

しかしこの時点でGoogleとは考え方が異なっているそうです。いったい何が違っているのでしょうか。

本書の後半で、実際に製品の指標の設定の仕方について語られています。

そう考えていくと、メトリクスは製品がいずれたどり付きたいビジョンにしたがって設計するべき、と言えます。

(『逆説のスタートアップ思考』第三章『プロダクト――多数の「好き」より少数の「愛」を』P.173より引用)

Googleの掲げる使命は「世界中の情報を整理し、世界中の人々がアクセスできて使えるようにする」ことです。

www.google.com

つまりGoogle検索も、これを達成できるような数値目標を設計するべきです。

Webページの内容はおおきく2つに分けられます。 「コンテンツ」 と、コンテンツに到達するための 「ナビゲーション」 です。 Google検索とは、まさにインターネット全体の「ナビゲーション」に該当するわけです。

ではGooleのミッションが達成されるために、Google検索はどういうナビゲーションであるべきかというと

  • たくさんの人がGoogle検索を利用する
  • どんな場面でも日常的にGoogle検索を利用できる
  • Google検索を利用して、必要なコンテンツに早くアクセスできる

つまり、Google検索をたくさん使って、さっさと目的のWebページに移動してもらいたいので

  • PVを上げる
  • 継続率を上げる
  • 離脱率を上げる
  • 滞在時間を下げる

という運営が必要になってきます。

正気のサイト運営者ならば自身のサイトをたくさん見てほしいはずなので、 「離脱率を上げる」「滞在時間を下げる」 というサイト運営は、直感に反するものです。 より端的にいうと、 狂っています

ですが、こうした"狂った"考え方によってGoogle検索は他の類似サービスを出し抜き、多くの顧客を獲得しました。

ポータルサイト全盛の1990年代後半、多くの会社が自社サイト内でのユーザー滞在時間をいかに増やすかを考えていた時に、Googleは精度のよい検索エンジンを作り、あえて自社サイトへの滞在時間を減らす事業を展開していました。(中略)これも、他人から見ればさぞ不合理なアイデアに見えたはずです。

(『逆説のスタートアップ思考』第一章『アイデア――「不合理」なほうが合理的』P.44より引用)

実際に、PVを追っていった結果不必要なリンク遷移を増やしてユーザにとって使いにくいサイトになったり、検索順位向上を目指して倫理上問題のある記事を量産したメディアの例は枚挙に暇がありません。これらの良し悪しを語ることはしませんが、言いたい事はKPI設計ひとつ取ってもWebサイトを変えてしまうという事です。

立ち上げ当時のGoogleは検索サイトとしては後発の弱小サイトだったそうです。 しかしその結果Googleがどういう成長を描いたのかは、私が説明するまでもありません。 世界最強の検索エンジンの根底にあるもののひとつは狂ったKPI設計だったのかもしれません。

書籍『逆説のスタートアップ思考』について

先述のGoogleの例のように、使命を達成するための「説明しづらい」「不合理に見える」「狂った」アイディアこそがスタートアップにおける成功の法則である、といった意味合いで「逆説的である」ので 『逆説のスタートアップ思考』 というタイトルだそうです。

この本は2時間程度で読める新書ですが、著者直々に「時間が無い人向けの読み方」も公開されています。

medium.com

まずはざっと内容をつかみたい方や時間がない人は、
- 「太字になっているところだけ」さらっと読む
- 「気になる太字の前後だけ」を読む
というのがお勧めです。

medium.com

私は平成生まれのデジタル・ネイティブなはずなのですがなんか頭の中が古臭いのか、電子媒体から入ってきた情報を活かすのが苦手です。SlideShareを「なるほどふ~ん」と適当に読んでも次ゼルダの伝説を遊ぶ頃には頭から抜けていそうです。 そのため書籍化によって手元に置いておけるようになったのは嬉しい限りでした。読みやすいものの理解しにくいないようなので、何回か繰り返し読みたい一冊です。

不確実性を楽しむ

本書には様々な「逆説的な」考え方が紹介されています。しかし「この考え方はあくまでスタートアップという領域に限られた物であり、世間一般に普遍的に適用されるべきものではない」というスタンスも取っています。実際に毎日毎日みんながみんな狂ったアイディアばかりを口にしていたら何が真実なのかさっぱり分からなくなりそうです。

論理的思考で誰が見ても正しい結論を導き、合議して進めることで明らかな成果を確実に出すという事が多くのシーンでは重要であり、そこに逆説的な思考は本来ならば存在し得ません。

スタートアップは誰にでもお勧めできる選択肢ではないと考えています。 というのも、スタートアップを始めると、ほぼ例外なく大きなストレスを抱えることになります。実際、シリコンバレーでは起業家の鬱が社会問題になっているほどです(面白いのはそれを解決するメンタルヘルスケアのスタートアップまで多数あるところですが)。

(『逆説のスタートアップ思考』終章『逆説のキャリア思考』P.247より引用)

一方で本書では「不確実性の高まっている昨今の世界情勢において、“不確実性を利用して大きな成果を出す”というスタートアップの考え方は広く理解されるべき」としています。これこそが本書の主題だと私は受け取りました。

つまりこの本の正体は起業の本でもなんでもなくて、予測できない状況をできるだけ前向きに楽しむためのマインドセットなのだと思います。

例えば、スタートアップの話ではないですが「Twitterで翻訳小説を連載する」だとか「可愛い絵柄で血みどろの魔法少女アニメを作る」といった狂ったアイディアは、本書の定義において逆説的であると思います。

逆説の思考を毎回必ず採択すべきとは到底思いません。むしろ有効な場面のほうが少ないと思います。全員がこういった思考をするべきとも思わず、得手不得手が極端に出ることだと思います。

ですが逆説的な考え方には「説明しづらい」という欠点があります。そこで「こういった考え方もある」という事を多くの人が理解し、世に認められるべきだと思いました。私自身こう言っていても本書の内容はまるで血肉になっていないので、もっと勉強しようと思います。

まとめ

逆説のスタートアップ思考 (中公新書ラクレ 578)

逆説のスタートアップ思考 (中公新書ラクレ 578)

逆説のスタートアップ思考を理解でき、まわりにも理解されていれば、不確実性の高い未来に対して少しでも希望を見いだせるようになり、論理的な思考では超えられない壁を打破できるようになるかもしれません。 すると世の中の見え方が、すこし良い方向に変わってくるのではないかと思いました。

MMORPGに見る順序数(Ordinal)と基数(Cardinal)

突然ですが英語を書いてみます。

「このMMORPGには3つのステージがあります」
「There are three stages in this MMORPG

「私はこのMMORPGで3番目に強いプレイヤーです」
「I am the third strongest player in this MMORPG

おなじ3でも、英語だと「three」と「third」に分かれます。

自然数のもつ意味

1,2,3…という自然数そのものには意味はありませんが、数に持たせる意味によって、呼び名が変わってきます。

「three」のように、物を数える数を 基数(Cardinal Number) と呼びます。

「third」のように、物の位置を特定する数を 順序数(Ordinal Number) と呼びます。

自然数が持つ、この2つの意味について考えてみます。

基数(Cardinal Number)

濃度 (数学) - Wikipediaより引用

それぞれの集合 A には A の濃度(|A|、card(A)、#A などで表される)と呼ばれるものが一つ割り当てられており、次の性質をみたす:

1.集合 A から集合 B への全単射が存在するとき、またそのときに限り |A| = |B|。

2.A が有限集合のとき、|A| は A の要素の個数に等しい。

ある集合の濃度であるものを基数と呼ぶ。

たとえば、あるMMORPGにおけるステージの集合をA、 そのMMORPGがサービス終了したあと別のMMORPG内に移された新生ステージの集合Bとおくと、 このときAからBへの全単射が存在するためcard(A)=card(B)となります。

ちなみに、自然数はすべて基数です。

順序数(Ordinal Number)

順序数 - Wikipediaより引用

整列集合 (A, <) に対して、A を定義域とする関数 G を超限再帰によって G(a) = { G(x) | x < a } と定義したとき、G の値域 ran(G) を (A, <) の順序数といい、これを ord(A, <) で表す。ある整列集合の順序数であるような集合を順序数と呼ぶ。

あるMMORPGのプレイヤー同士に強弱<が存在し、<の順番で並んだプレイヤーの集合Bにおいて 「自分よりも強いプレイヤーの集合」の集合がord(B, <)となります。

誰もログインしていないMMORPGにおいては順序数は空集合∅、つまりord(B, <) = ∅です。 コレが「最小の順序数」ということで、ゼロと定義します。0 = ∅

1位までのプレイヤーからなる集合ならば、「自分よりも強いプレイヤー」は空集合∅なので、ord(B, <) = {∅} となります。 コレは「最小の次の順序数」ということで、イチとします。1 = {∅}

2位までのプレイヤーからなる集合ならば、「空集合∅」と、「空集合の集合{∅}」なので、2 = {∅,{∅}} となります。ちょっと無理矢理ですね。

ちなみに、自然数はすべて順序数です。

基数と順序数の違い

ここで基数と順序数の違いを見ていきます。

  • 基数card(A)は、Aの要素の個数になります。
  • 順序数ord(A,<)は、Aの要素の個数になります。

同じ です。おわり。

しかし、この議論は重要な前提条件をひとつ見落としています。 それは Aが有限集合である ということです。

Aが有限集合とは限らない、つまり 無限集合 だった場合は、どうなるでしょうか。

無限集合における基数と順序数

濃度 (数学) - Wikipediaの「基数と順序数」より引用

すべての自然数を 0, 1, 2, 3, … と並べた場合の濃度は  \aleph_{0}、順序数は ω である。これを並べ替えて 2, 3, …, 1 とした場合、濃度は変わらないが、順序数は ω + 1 になり、ω より大きくなる。

まず基数について見てみます。

あるMMORPGの全ステージの集合をAとします。 このMMORPGは仮に75面までしか確認されていないとすると、そのステージ数card(a)は有限であるとは断言できません。 仮にステージ数が無限だったとして、 card(A) =  \aleph_{0} です。

そして、ステージ数は無限ですが75面以降無限にあるステージを全部すっ飛ばして最終面100面に到達したとします。 このときもcard(A) =  \aleph_{0}です。

基数は変わりませんでした。一方で、順序数を見てみます。

あるMMORPGの全プレイヤーが強い順に並んでいる集合を(B,<)とします。 もしかしたらNPCとか幽霊も参加していたりするかもしれないので無限のプレイヤーがいるとします。 このときord(B,<)=ωです。

ここで、最弱だったプレイヤーが無限人を追い抜いて最強になったとします。 このときord(B,<)=ω+1となります。

当然、ω < ω+1 です。 基数と順序数によって、振る舞いが異なります。

結論

基数(Cardinal Number)によって支配されているMMORPGは順番を無視しても難易度が変わるような事態は起こりません。

しかし順序数(Ordinal Number)によって支配されているMMORPGは順序をひっくり返すことで想像を超える強さを手にしたことになるかもしれないので、なんか結果的に想定外の事態とかが起きる危険性があるので悪の計画とかに利用するなら十分気をつけたほうが良いと思います。たぶん。

参考文献