2018年の砂場活動振り返り

インフラやミドルウェアにとにかく苦手意識があるが、仕事的にいつまでもそう言ってられない。そこで、最悪全部ぶっ壊れても大丈夫な砂場を作り、そこを土台に活動をするというのを2018年の目標に設定していた。

結構な時間をかけたこともあり、それなりの砂場と活動ができて、自分としても勉強になってよかった点が多かったので振り返りを書きます。一個一個ちゃんとエントリ書いていたので、振り返りが楽で助かった。

完成系はML Newsだけど、2018年1月時点では

  • そもそもWebアプリですらなくCLIアプリだった
  • データの管理もデータベースではなくテキストファイル

という素朴な作りだった。

インフラ編

最初はCLIアプリをWebアプリにする活動をやったが、その後はAWS上にインフラ部分の構築を進めた。

次に一台のEC2をAWSコンソールから立てて、sshでログインしてyumコマンドを打って...という10年前くらいのサーバー構築をやった。しかし、EC2のスペックを変えて作り直すのはあまりにもダルかったので、ansibleでミドルウェアのインストールや必要な設定をやらせるように。

一台のEC2にWebアプリもredisもpostgresも同居してという状況だと辛い状況(公開する関係でWebアプリはpublicサブネットに置きたいけど、DBもpublicサブネットに置いてる...)になるのは目に見えていたので、managedサービスが存在するものはそれを使うようにした。AWS上でのリソース構築は会社でCloudFormationを使うことが多かったので、自分も使うことにした。

CloudFormationはまあ書くだけという感じだけど、VPCやサブネット、セキュリティグループをどう設定するとよいかをイチから自分で考えるのは普通に勉強になったので、やってよかったですね。CloudFormationのテンプレートはどういう粒度で分割すればいいのかは未だに分からない。CloudFormationのyamlを編集するのも辛くなってきたのでaws-cdkも試しましたが、こちらはまだまだ開発中という感じだったのでしばらく放置しています。

WebアプリをEC2上に立てていたが、世の中コンテナ化が進んできていてどういうよいところ悪いところがあるのか知りたい自分も知りたい、仕事的にも知っておく必要があるということでコンテナ化を進めた。

最初は自分でEC2を立ててECSのクラスタにjoinさせる、という形を取っていたが「いい加減EC2の管理をしたくないなぁ...」と思ってきたため、途中からFargateを使うように。FargateだとEC2を立てておかなくてもdesired-countを変えればアプリケーションサーバーの個数も変えられるので楽になった一方、EC2より若干お高いとかログ周りが微妙(fluentdとの連携とか)、Fargateの起動は若干もたつくなどが試して分かってきました。この辺から必要なリソースをある程度自由に変更できるようになってきたため、砂場感が出てきました。

コンテナ化したご利益でAWS Batchも使えるようになりました。普段のWebアプリのワークロードと異なる機械学習のモデルの学習などはAWS Batchに投げ込むことができるようになったため、バッチが走っている間レイテンシーが落ちる...といったことを心配しなくてよくなりました。AWS Batchは仕事でも使っていますが、便利ですね。

AWS Batchで複数のジョブを管理していると、そのバッチの順序依存関係をcron職人芸ではなくもっと賢い方法で管理したくなりました。同僚が使っていたStep Functionsが便利そうだったので導入したのもこの頃でした。試した内容はLTで発表も行ないました。

機械学習をやるためにデータ基盤周りのことを考えることが増えてきましたが、その辺も試してみようということでCloudWatch LogsからKinesisを通してS3にログを転送して、Athenaで分析するといったこともやりました。やってみたのはよかったですが、ログの流量がまだまだ少ないのでどういうところで困るかはまだまだ見えてこないという状況ではありました...。

ミドルウェア編

インフラ編である程度下周りは整備したので、パフォーマンス改善の活動を通してミドルウェアのことを知る活動も行ないました。ISUCON勢には当たり前すぎるところから、という感じではあります。当たり前のことを当たり前にできるようになっていきたい。

チューニングをした結果、改善したのかそうでないのかを判断するためのモニタリングをやれるようにしたり、DBで必要なインデックスを張るとか、コネクションプールを適切に設定するとか、そもそもボトルネックきちんと把握できてる??といったことを少しずつやっていきました。AWSの課金額が順調に増えてきてしまったのもこの辺だ😇

チューニングではないけれど、Nginxの設定をあれこれいじったりということもやっていました。

フロントエンド編

砂場活動のメインの目的はインフラ方面の知識および経験の強化ですが、Webアプリを作る過程でフロントエンドでも勉強になりました。Vue.jsを導入してSPAにしたり、Cognitoを使って認証させたりといったことをやっていました。以前は趣味でjavascriptを書く機会が皆無だったので、趣味でもjavascriptを書く機会ができたのはよかったなと思います。当初は特に予定がなかったサーバーサイドレンダリングも導入成功していた。

まとめ

2018年の砂場活動を振り返りました。正直、よい設計に落し込むまでめちゃくちゃ遠周りしているし、AWSへの月々の課金も無視はできない金額になっています(現実的なところに落と込んだら課金も減りつつある)。しかしながら、以下のような大きなメリットがありました。

  • 遠回りするとどのアプローチがどうダメだったかが身を持って分かる
    • 多少痛い目に合わないとどれくらい困るかがイメージが湧かない性格...
  • 多少データがぶっ飛ぼうが困るのは自分だけなので、思い切ってあれこれ試せる
    • 精神的な障壁を低くするという砂場活動の目的を十分果たせていたと思います

インフラ/ミドルウェアの苦手意識はまだまだ払拭できてはいませんが、2018年に作った砂場を足場に2019年には多少の武器にしていけるようにしていきたいなと思います。

飽きっぽい自分としては、年始に立てた目標を年末までちゃんと継続的に取り組めていたことに驚いていますが、エンジニア1on1で適切にフィードバックをくれたid:shiba_yu36さんの力が大きかったなぁと思います。ありがとうございます!

Amazon Web Services 基礎からのネットワーク&サーバー構築 改訂版

Amazon Web Services 基礎からのネットワーク&サーバー構築 改訂版

最近の砂場活動その11: Nuxt.jsでサーバーサードレンダリング

タイトルの通りやりましたというお話です。以前は$http_user_agentを見て、googlebotだったらrendertronを経由させてレンダリングした結果を見せることでお茶を濁して凌いでいた。

この方法、SPA自体は全く変更する必要なくbotにレンダリングされた結果を見せることができるというメリットがある一方で、以下のようなデメリットもあること分かってきた(分かってはいたものもある)。

  • レイテンシーが大きい: rendertronが結果を返すまでに3-4秒程度かかる
    • rendertron自体に手は入れたくないため、お手軽な改善方法がない
    • botにならある程度レイテンシーかかっても問題ないという話はありそう
  • rendertronがそこそこメモリを食う
    • めちゃくちゃ食うというわけでもないが、メインのAPIサーバーよりメモリを食っていて何だかな感がある
  • 人間とbotにhtmlを出し分けている
    • 描画されるものが決定的に異なるわけではないが、若干気持ち悪い
    • 両方のテストが必要になる

rendertronで対応するのはそもそも邪道だなとは思っていたので、Nuxt.jsで真面目にサーバーサイドレンダリングを行なうようにした。変更に対応するPull Requestは以下。

package-lock.jsonを入れていなかったのでdiffがやたら大きいですが、本質的にはhtmlテンプレートファイルの置き場所が変わったくらいです。

対応方法

元々はVue.jsを使ったSPAだったので、そこまで大きな変更点はないです。覚えている範囲でNuxt.js用に変えた点としては以下の通り。

  • テンプレートファイルの置き場所変更
    • Nuxt.jsはlayoutpagescomponentsなどある程度ディレクトリに関して規約があるため、それに従うようにします
    • 特に整理できていなかったので見通しはよくなった
    • routingも基本的にpagesのディレクトリに従う形
  • クライアントサイドでデータ取得している箇所をサーバーサイドで取得するように
    • メインの変更点。asyncDataを使うようにする
    • Vue.jsのときはrouterに必要な情報が入っていたが、Nuxt.jsではcontextに必要な情報が入っている。context.route.params.MyParamという感じ。あまりドキュメントに書かれていないので、console.logで出力しながら必要なものを探した
  • htmlのmeta情報の置き換え
    • vue-headfulを使っていたが、Nuxtが面倒を見てくれるようになったのでそちらを使う
  • 設定をnuxt.config.jsに書く
    • webpackであれこれ書いていたこと(minifyとか)をnuxtがある程度面倒を見てくれるようだった
    • そもそもwebpackも雰囲気でしか書けていなかった...
  • Node.jsで動かない記法やライブラリの置き換え
    • developer toolに出力させているつもりが、terminalに出ていたというのがちょくちょくあったので、クライアントサイドで動くものかサーバーサイドで動くものかはきっちり意識しながら書く必要がある

これで大体動いた、Fetch as Googleで確認。慣れは必要だけど、めっちゃ大変という感じではない。Vue.jsをある程度触ったことがあれば大丈夫。初回のhtmlはサーバーサイドで生成して、それ以降の画面遷移はSPAのときと同じような感じで動くので、SPAで書いていたものもほとんど無駄にはならなかった。

rendertronはなくせたものの、サーバーサイドでレンダリングするNode.jsは相変わらず必要。AWS ECSで動かしているので、専用のDockerfileを用意する。といっても必要なファイルをコピーする、くらいしかやっていない。ECSのタスク定義にもよしなに修正を入れる。

FROM node:11.6.0-slim
ENV NODE_ENV production
WORKDIR /app
COPY .nuxt /app/.nuxt
COPY ./nuxt.config.js /app/nuxt.config.js
COPY ./package.json /app/package.json
COPY ./package-lock.json /app/package-lock.json
RUN npm install 

ENV HOST 0.0.0.0
EXPOSE 3000

ENTRYPOINT ["npm","run-script","start"]

静的ファイル(今回の場合はjsしかないけど)はnginxで配信させるようにしたかったので、nginxも適当に書き換える。

upstream local-nuxt {
    server localhost:3000;
}

server {
    location / {
        proxy_pass http://local-nuxt;
    }
    location /_nuxt/ {
        root /www/app;
        proxy_set_header Host $http_host;
        expires 1d;
        gzip on;
        gzip_types text/css application/javascript;
    }
}

docker-componentsを頑張って書いて手元でNode.js/APIサーバー/Nginxがそれぞれ連携して動いているかを確かめられるようにすべきと思いつつ、面倒くさくなって本番に導入しながら確認した。

導入後の所感

  • Nuxt.jsなかなかよい、規約などに従ったほうがカオスにならないのでサーバーサードレンダリングせずにSPAするだけでも導入して損はあまりなさそう
  • 人間とbotに出し分けしなくてよくなったので、見ないといけないポイントが減った
  • rendertronと比較してレイテンシーは改善された(そりゃそうだ)
  • とはいえ、短時間にアクセスが集中すると200を返せなくなるときが増える

Nuxt.jsビギナーズガイド―Vue.js ベースのフレームワークによるシングルページアプリケーション開発

Nuxt.jsビギナーズガイド―Vue.js ベースのフレームワークによるシングルページアプリケーション開発

Vue.js入門 基礎から実践アプリケーション開発まで

Vue.js入門 基礎から実践アプリケーション開発まで

  • 作者: 川口和也,喜多啓介,野田陽平,手島拓也,片山真也
  • 出版社/メーカー: 技術評論社
  • 発売日: 2018/09/22
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログを見る

最近の砂場活動その10: songmu/horensoを使ってバッチの処理時間をMackerelのサービスメトリックに記録する

機械学習の前処理をやるバッチの処理時間が以前は20分くらいだったのにいつの間にか2時間かかっていた...ということがありました。処理時間を短かくする修正は入れたものの、処理時間がどれくらいかかっているかを継続的に監視する仕組みがそもそもないのが厳しいなと思ったのでそれについて書きます。完全にN番煎じなので、特に新規性はないです。

songmu/horensoとは

バッチの処理時間を記録したいけど、大掛りな仕組みは導入したくない。horensoでは実行したいバッチをwrapするような形で簡単に導入できる。

実行したい元々の処理が以下だとすると

/app/go-active-learning.bin add --input-filename input.txt

こんな感じでラップすればよい。

horenso -t add_recent_urls -T --reporter "/app/mackerel_service_metric_reporter go-active-learning-staging" -- /app/go-active-learning.bin add --input-filename input.txt

reporterは終了時に実行されるコマンド。自分の好きな言語で書ける。開始時刻と終了時刻から実行時間を計算して、Mackerelのサービスメトリックに投稿するreporterをGolangで書いた。horenso自体もGolangで書かれているし、MackerelのAPIをGolangから叩くmackerel-client-goもあるので、Golangだとしゅっと書ける。

失敗したときにSlackに送るreporterとかも追加で指定することができる。元々のコマンド自体は何も変える必要がないのでgood。

Mackerelで監視する

実行時間がX分以上だったらSlackに通知する、といったreporterを直接書いてもよい。しかし、Mackerelに実行時間を投稿しておくと過去の傾向が分かるし、X分以上だったらSlackに通知するといった処理をMackerelに任せることができるので気楽。

最近の砂場活動その9: Cognitoで認証済みのユーザーでAPI Gatewayを呼び出す

ML NewsのWeb UIにアノテーションボタンを付けたい、でもユーザー管理機能は作りたくない(アノテーションするのは自分だけなので)。AWSのCognitoとAPI Gatewayを組み合せると簡単にできると聞いたので、やってみました。色々てこずったので正直あまり簡単ではなかったけど、やりたかったことはできたのでメモとして書き残しておきます。

やりたいことは以下。

  • AWSの仕組みでいい感じにユーザー認証されたい
    • 認証の仕組みを自前でやりたくない
  • 認証されたユーザーのみが叩けるエンドポイントを工数少なく実現したい

API Gatewayで所望のエンドポイントを生やす

認証をどうやるかは後回しにして、ひとまずエンドポイントをAPI Gatewayで生やしていきます。API Gatewayに生やしておくと後々の認証(Cognito)との接続がよいので、これを使います。

API Gatewayが何かを一言で説明するのは難しいけど、今回の用途に限れば裏側のロジックをLambdaで書きつつ、それをhttpで叩けるエンドポイントになってくれる君と思っておけばよい。別にlambdaじゃなくてもよいけど、今回はlambda。その他、リクエストをキャッシュさせたりといったこともできるようだが、今回の用途にはあまり関係ない。

ロジックをLambdaに書く

処理の本体はLambdaに書きます。Lambdaは色んな言語で書けるけど、手慣れているGolangで書きます。

Lambdaに書くロジック

package main

import (
    "net/http"
    "github.com/aws/aws-lambda-go/lambda"
    "github.com/aws/aws-lambda-go/events"
    "os"
    "log"
    "fmt"
    "encoding/json"
)

type MyRequest struct {
    Url string `json:"url"`
    Label model.LabelType `json:"label"`
}

var errorLogger = log.New(os.Stderr, "ERROR ", log.Llongfile)

func UpdateExampleLabel(req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
    myReq := new(MyRequest)
    if err := json.Unmarshal([]byte(req.Body), myReq); err != nil {
        return clientError(http.StatusUnprocessableEntity)
    }
    // ここにロジックを書いていく

    return events.APIGatewayProxyResponse{
        StatusCode: http.StatusOK,
        Body:       `{"hoge": 1}`,
        Headers: map[string]string{
            "Access-Control-Allow-Origin" : "*", // CORS対策
        },
    }, nil
}

func serverError(err error) (events.APIGatewayProxyResponse, error) {
    errorLogger.Println(err.Error())

    return events.APIGatewayProxyResponse{
        StatusCode: http.StatusInternalServerError,
        Body:       http.StatusText(http.StatusInternalServerError),
    }, nil
}

func clientError(status int) (events.APIGatewayProxyResponse, error) {
    return events.APIGatewayProxyResponse{
        StatusCode: status,
        Body:       http.StatusText(status),
    }, nil
}

func main() {
    lambda.Start(UpdateExampleLabel)
}

API Gatewayの設定

ロジックが書けたらaws-sam-cliを使って手元でテストしたりするとよい。sam local start-apiとかやると手元でサーバーが走ります。

動作確認ができたらsamを使ってLambdaにデプロイしていく。今回の用途ではprivate subnet内のRDSに接続する必要があったため、VPC内にLambdaを作った。samのテンプレートはこんな感じ。

samのテンプレート

  UpdateExampleLabel:
    Type: AWS::Serverless::Function
    Properties:
      Description: "事例のラベルを更新する"
      Runtime: go1.x
      Handler: update_example_label
      CodeUri: update_example_label/build # ビルドファイル設置ディレクトリ
      Timeout: 10
      Policies:
        - VPCAccessPolicy: {}
      VpcConfig:
        SecurityGroupIds:
          - Fn::ImportValue:
              !Sub "${VPCStackName}:BatchSecurityGroup"
        SubnetIds:
          - subnet-xxx
          - subnet-yyy
      Environment:
        Variables:
          POSTGRES_HOST: XXXXX.YYYYY.us-east-1.rds.amazonaws.com
      Events:
        GetEvent: # 任意のイベント名
          Type: Api
          Properties:
            Path: /update_example_label
            Method: post
  LambdaPermission:
    Type: "AWS::Lambda::Permission"
    Properties:
      FunctionName: !Ref UpdateExampleLabel
      Action: "lambda:InvokeFunction"
      Principal: "apigateway.amazonaws.com"

samをデプロイすると、API Gatewayのリソースも作られていると思う。

API Gatewayで作ったエンドポイントと元々のドメイン(www.machine-learning.news)は異なるため、CORS(Cross-Origin Resource Sharing)を有効にする必要がある。API Gatewayの画面から「CORSの有効化」を選択、200のレスポンスヘッダーに以下のものを追加しておく。

Cognitoの設定

ここここを参考にしながらAWSコンソールでボタンをポチポチ押していく。ユーザープールとIDプールを作成していく。注意するところとしてこんな感じかな。

  • 認証プロバイダはCognitoだけで特に問題ない
  • ユーザーの登録はcliでやっておく。そうしないとパスワード変更画面を作らないといけなくなるので、一手間増えてしまう
    • 新規ユーザーを受け付ける場合はちゃんと画面を作りましょう...
    • アカウントのステータスがCONFIRMEDになっていればok
  • 全般設定 => アプリクライアントからアプリクライアントを作っておく
    • アプリの統合 => アプリクライアントの設定から「有効なIDプロバイダ」でcognitoを選択しておく

Amplifyで認証されたユーザーのみにアノテーションボタンを見せる

作成したユーザーがサインイン/アウトする画面はぐぐると色々出てくると思うので、それを参考に作る。今回は新しくユーザーがサインアップできる必要はなく、自分用のユーザーはcliで作成しているのでサインアップ画面は特に作らない。currentSessionかどうかを見てisAdminをtrue or falseにして、それによって出し分けする。

ボタンの出し分けに必要なJSの断片

import Amplify, {
  Auth,
  API,
} from 'aws-amplify';

Amplify.Logger.LOG_LEVEL = 'DEBUG';
Amplify.configure({
  Auth: {
    region: 'us-east-XXX',
    identityPoolId: 'us-east-XXX:YYY',
    userPoolId: 'ZZZ',
    userPoolWebClientId: 'AAAAA',
  },
});

export function IsAdmin() {
  return Auth.currentSession().then(_ => true).catch(_ => false);
}

export default {
  data () {
    return {
      ...,
      isAdmin: false,
    }
  },
  mounted() {
    IsAdmin().then(isAdmin => this.isAdmin = isAdmin);
  },
}

Cognitoで認証されたユーザーのみAPI Gatewayを呼び出せるようにする

画面の上では認証されたユーザーのみにアノテーションボタンを見せられるようになったが、API Gateway自体はインターネットで公開されているエンドポイントになっているため、誰でもエンドポイントを叩けばアノテーションをいじれてしまう。それでは困るので、API自体もCognitoで認証済みのユーザーのみ叩けるようにする。

API Gatewayの設定

Cognitoを設定したときに自動的に作られてるIAM Role(arn:aws:iam::XXXXX:role/Cognito_YYYYYAuth_Roleのようなやつ)に追加で権限を付与する。invokeの権限を今回許可したいエンドポイントのみに与える。

{
    "Sid": "VisualEditor1",
    "Effect": "Allow",
    "Action": "execute-api:Invoke",
    "Resource": "arn:aws:execute-api:us-east-1:XXXXX:YYYYY/*/POST/update_example_label"
}

次にAPI Gatewayの画面のオーソライザーから「新しいオーソライザーの作成」を選択。自分が作成したユーザープールを指定する。

オーソライザーが作成できるとリソースの認証方法のCognitoが指定できるようになる。こんな感じ。

必須ではないと思うけど、「HTTPリクエストヘッダー」でAuthorizationを必須にしておくとよさそう。

ここまで設定したらAPIのデプロイを行なって変更点を反映する。

クライアント側の設定

クライアント側は簡単。localStorageから必要なtokenを取得してきて、リクエストヘッダーに付けるだけ。

idTokenを取得してAPI Gatewayを叩くクライアント側のコード

export default {
  props: ['example'],
  methods: {
    updateLabel(example, label) {
      let idToken = localStorage.getItem("CognitoIdentityServiceProvider.XXXXX.YYYYY.idToken");
      let headers = { headers: { 'Authorization': idToken } };
      axios.post(
        "https://ZZZZZ.execute-api.us-east-1.amazonaws.com/Prod/my_endpoint", 
        {
          url: example.Url,
          label: label,
        },
        headers
      ).then(response => {
        example.Label = label;
      }).catch(function (error) {
        alert(`Failed to annotate "${example.Title}"`);
      })
    },
  }
}

困ってるところ

めちゃくちゃ困ってるというわけでもないけど、しばらくLambdaを起動させていなかった後の初回起動に時間がかかる。VPC内にLambdaを作った関係でENI(Elastic Network Interface)を作成する必要があるからだそうな。回避策としては定期的にポーリングしておくとよいらしいが、そこまで困ってるわけでもないので困りだしたらやる。

参考

Pretraining Sentiment Classifiers with Unlabeled Dialog Dataを読んだ

論文読み会をやるので久しぶりに論文を読みました。久しぶりじゃダメなんだけど...。今年のACL2018でYahoo! JAPAN Researchの方が発表された内容です。

以下は雑なメモです。

問題点

  • 教師あり学習である程度いい精度を出すには教師データが10万件程度必要だけど、集めるのが大変
    • 3値分類、3人のアノテーターでmajority voting
    • 大体300人月
    • 24Kドル(240万くらいを高いと見るか安いと見るか)
    • 8時間 * 300日 * 時給1000円
    • もっとスキルのあるアノテータにやってもらうともっとかかる
    • アノテーション基準を勉強してもらう時間などは含んでいない
  • 実験データではラベル付きが8万件、ラベルなしが2000万件
    • 8万件じゃ精度はまだまだサチらない

提案法

  • 提案法は2段階
  • 1: pretrainingとして教師なしの対話データから(tweet-reply pairs)対話モデルを作る
    • encoder-decoder
  • 2: sentiment classifier(encoder-labeler)を教師ありデータを使って学習
    • 1で作ったencoder-decoderモデルのencoderパートを初期値として与え、pretrainingとして生かす
  • 言語モデルをベースにした既存手法もあるが、replyが付くようなtweetは感情に基づくことが多いのではないか?という仮説

モデル

  • dialog modelのベースラインはencoder-decoder
    • 入力は単語レベルじゃなくて文字レベル(絵文字とか顔文字とか多いからこっちのほうが性能高い)

比較した手法

  • 事前学習あり
    • Dial: 提案法(pretrainあり)
    • Lang: tweetのペアなしで言語モデルとして学習したLSTMをpretrainとして使う
    • SeqAE: sequence autoencoderをpretrainとして使う
    • Emo2M, Emo6M: この絵文字/顔文字が入っていたら正例/負例と見なすという一種のdistant supervisionで疑似的な学習データを使ったもの
  • 事前学習なし
    • Default: pretrainなし
    • LogReg, LinSVM: unigramとbigramのbag-of-words

実験結果

  • 学習データを5000件から8万件に変化させたときのF値の変化を見る
  • 学習データ量のサイズに関わらず、提案手法のDialが一貫して他の方法よりよい性能
  • Emo2Mは学習データにノイズが入るためDefaultより低い精度になっているっぽい
    • 例: うらやましいです。おめでとうございます orzがorzが入ってるので負例になってしまう
  • 古典的な機械学習のLogRegLinSVMはデータ量が少ないときにはDefaultよりもよい性能を出している
    • とはいえ、事前学習したDialLangのほうが性能が高い
  • 事前学習されたencoder-decoderの生成結果も感情に基づいている感じで、結果がよくなるのも納得

ゼロから作るDeep Learning ? ―自然言語処理編

ゼロから作るDeep Learning ? ―自然言語処理編

ついに開幕したTリーグを観戦しに行きました

10/24に開幕した卓球リーグ、Tリーグに観戦してきたので日記を書きます。

京都から名古屋の武田テバオーシャンアリーナに、以下の3試合を観戦しにいきました。

  • 10/27 TT彩たま vs 岡山リベッツ
  • 10/27 木下マイスター東京 vs 琉球アスティーダ
  • 10/28 TT彩たま vs 琉球アスティーダ

練習時間の写真撮影は許可されていて、こんな感じでした。圧倒的迫力があった。

よかった点

  • ライブで見れる
    • YouTubeやテレビ中継でも試合動画が見れる世の中になってきたけど、開場の熱量や試合の緊張感はやはりライブならではのものがある
  • 圧倒的にプレーの質が高い
    • 世界ランクトップレベルの選手ばかりなので当たり前といえば当たり前だけど、これをライブで見れる幸せ
    • 回りの人も言っていたけど、練習を見ているだけでチケットの半額は余裕で元を取れるレベル
  • めちゃくちゃ選手との距離感が近い
    • これはアリーナ南の最前列(6000円くらい出すといける席)だったけど、これはもはやチームのベンチ入りをしているといっても過言ではないのではという距離感
    • 選手が目の前の通路を通っていく、うっかりサインをお願いしてしまう人も
      • セキュリティ的に大丈夫なんかという気もしつつ、これだけ近いのはよいですね
  • 開場と同時に入場しても選手が開場で練習しており、間近で練習風景が眺められる
  • 小中学生以下の子どもの観戦が多い
    • 小さいときに一流のプレーを見て衝撃を受けるのはどういう道に進むのであれよい影響を受けると思っています
  • 試合が終わった後、めっちゃ笑顔になる
    • これだけいいプレーを見せてもらったら自然と笑顔になってしまう

改善されるとよさそうな点

あれこれ書いてますが、開場でもアンケートを取っていたのでそちらでも伝えています。クレームとかではなく、Tリーグがこれからも継続的に続く事業であって欲しいなと思うので、こういうところを改善されるといいんじゃないかと思ったところについても書いておきます。

  • ダブルスも3セット先取でいいのではないか
    • 2セット先取だとあまりに一瞬で終わってしまった印象がある。試合全体が長くなりすぎないように考慮してのこともあるかもしれない
  • ハーフタイムは必要かどうか
    • 卓球経験者からするとハーフタイムが入ると緊張感が途切れてしまうので、ハーフタイムがないほうがいいなぁと感じた
    • 一方で、未経験者の人からすると2時間以上の試合を一気に見るのは集中力を使うため、観客への配慮でハーフタイムが導入されているのかもなというもした
  • グッズ売り場が狭い
    • 結構人気でサンプルを見るのに近づけない感じだった。売り場をもう少し広くして欲しい
    • 来客者数が読めなかったので、安全側に倒したんだろうなとは思う
  • 来場者数、もっと増えて欲しい!
    • 水谷、張本が出場する木下マイスター東京の試合はそれなりにお客さんがいたが、その他の試合は正直もっと席が埋まって欲しい。特に自由席
    • 回りの会話を聞いている感じだと卓球経験者の割合が多かったが、未経験者でも楽しめるリーグを目指して欲しい
    • 色々な見方はあると思うけど、収益が上がって事業が継続できるだけのお客さんがこないと厳しい
  • 試合が思ったよりしゅっと始まった
    • 卓球経験者だと「この選手はこういうこういう特徴がある選手だからこの辺に注目するとより試合が楽しめそう」というのが分かるけど、未経験者だとそうではない
    • TVの解説だと選手の特徴をまとめた15秒くらいのPVとかがあったりすると思うけど、そういうのがあると未経験者でもその選手のことが分かるのではないか
    • 開場で配っているパンフレットも戦型、世界ランクくらいしか書いてないので、そういったところがもっと書いてあると楽しみ方が分かるのではないか
  • あまりにいい席すぎてカメラが前にきて見えない時間が多少あった
    • 2日目では改善されていて、カメラを低い位置にして観戦の邪魔になるべくならないように、という配慮がされていました!
  • 黄鎮廷はペンだけど、それ以外の出場選手は大体シェイクドライブで大分偏りがあった
    • 自分はカットマンなので、村松選手の試合を見るのを楽しみにしたけど、今回は出場がなかった

ミーハー的感想

  • 吉村vs丹羽の試合、見てて面白かった
    • お互いチキータを警戒して、フォア前やサイドを切ったところにサーブを出して、あえてレシーブで打たせて3球目でカウンターを狙いにいく、それをいかに防ぐレシーブをするかの読み合いが熱かった
    • 全然甘くない吉村のループを丹羽がサイドを切るコースにカウンターかましていてわけが分からないよ
  • チョンヨンシクのバックブロック、固い
  • どのチームを応援というよりとにかくいいプレー、緊張感のある試合を見たかったので、ひたすらもつれた試合になることを願いながら観戦していた
  • 未経験者の観客をあれだけ定常的に動員できるサッカーや野球はやっぱりすごいんだなぁというのをひしひしと感じた

負ける人は無駄な練習をする―卓球王 勝者のメンタリティー

負ける人は無駄な練習をする―卓球王 勝者のメンタリティー

最近の砂場活動その8: Nginxとrendertronでprerenderingされた結果をクローラに見せる

趣味サイトをVue.jsを使ってSPAとして作っている。人間が見る分には問題ないが、GoogleBotがきたときにレンダリングされていなくて解釈された結果が悲しい感じになっている。

Vue.jsでもサーバーサイドレンダリングができたりするようだが、アプリケーションサーバーはnodeではなくGolangで書いている。そのため、導入が面倒そう。仕方ないので諦めていたが、レンダリングされた結果を返してくれるHTTPサーバーとして振るまってくれるrendertronというものがあるそうだ。Headless Chromeを中で使っている様子。なるほど、こういう使い方ができるのか。

早速導入してみた。nginxでこういう感じで場合分けさせる。

  • 人間がきた場合: nginx(port: 7777) => アプリサーバー(port: 7778)
  • Botがきた場合: nginx(port: 7777) => rendertron(port: 3000) => アプリサーバー(port: 7778)

設定ファイルはこんな感じ。nginx初心者なので、書き方があんまりよくないかもしれない...。

upstream local-backend {
    server localhost:7778;
}

upstream local-rendertron {
    server localhost:3000;
}

server {
    listen 7777 default_server;
    set $prerender 0;
        if ($http_user_agent ~* "googlebot") { # 他にも追加したい場合にはいい感じに正規表現を書く
        set $prerender 1;
    }

    location / {
        if ($prerender = 1) {
            rewrite ^(.*)$ /render/https://www.machine-learning.news$1 break;
            proxy_pass http://local-rendertron;
        }
        root /www/app;
        proxy_set_header Host $http_host;
        expires 1d;
        gzip on;
        gzip_types text/css application/javascript;
    }
    location /api {
        proxy_pass http://local-backend;
        gzip on;
        gzip_types application/json;
    }
}

nginxもアプリサーバーもrendertronもコンテナ使って動かしていて、ECSの1つのタスクの中に同居している。CloudFormationでの設定例はこんな感じ。

  ServerTaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: 'go-active-learning'
      Cpu: 256
      Memory: 2048
      NetworkMode: awsvpc
      ExecutionRoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/ecsTaskExecutionRole"
      RequiresCompatibilities:
        - FARGATE
      ContainerDefinitions:
        - Name: "go-active-learning-app"
          PortMappings:
            - ContainerPort: 7778
          Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/go-active-learning:latest"
          EntryPoint:
            - "/app/go-active-learning.bin"
            - "serve"
          Essential: "true"
        - Name: "go-active-learning-nginx"
          PortMappings:
            - ContainerPort: 7777
          Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/go-active-learning-nginx:latest"
          Essential: "true"
        - Name: "rendertron"
          Image: "nkonev/rendertron-docker:latest"
          PortMappings:
            - ContainerPort: 3000
          Essential: "true"

これでGoogleBotもいい感じに解釈してくれるようになった。

問題点

トップページはGoogleBotもレンダリングされた結果を解釈してくれるようになってめでたいが、トップページ以外はVue.jsのルーティングがうまく動くようにまだ書けていない。いい感じにする方法があれば教えてください...。

参考