BASE開発チームブログ

Eコマースプラットフォーム「BASE」( https://thebase.in )の開発チームによるブログです。開発メンバー積極募集中! https://www.wantedly.com/companies/base/projects

ALOCCを使った「文字画像」を判別する試み

どうもお久しぶりです。BASEビール部部長の氏原です。最近急に涼しくなりましたね。ハイアルなベルギービールでも飲んで温まるといい季節ですよ。

さて、今回もビールの話はとりあえず置いておいて現在Data Storategy Groupで取り組んでいる内容として、今年に出たらしい論文「Adversarially Learned One-Class Classifier for Novelty Detection」を実装して商品画像フィルタにならないか試してみたことについてお話しようと思います。

One-Class Classifierとはあるクラスに属するか否かの判別器です。例えばある画像に写っているものがQRコードか否かとか、水着か否かとかです。

今回の話は一行で言えば、ショッピングアプリ「BASE」の商品検索などから意図と異なる画像をフィルタリングするためにAdversarially Learned One-Class Classifier(ALOCC)が使えないか試したという内容です。

f:id:yuhei_kagaya:20181107105046p:plain:w800

背景

皆さんショッピングアプリ「BASE」を使ったことはおありでしょうか?数多くの商品が並んでいて、私は米とか肉とか買ってます。そんな中こんな商品たちを見かけたことはないでしょうか?

f:id:yuhei_kagaya:20181107105604p:plain:h300

またはおすすめショップのところのこういうのとか

f:id:beerbierbear:20181106161536p:plain:h200

こちらは、お知らせをひとつの商品として登録するとこのように表示されます。本来的な商品画像の使用法とは異なりますが、登録自体は可能です。

これらの商品ではない商品がショップのページに出てくるのは特に問題ないのですが、もし検索とかで出てきたら探している商品と違うなぁと思ってしまうのではないでしょうか?

この問題を解決するために商品ではない商品を自動で発見して検索等には出ないようにする機能を開発しています。そしてQRコード画像については現在検索等からの除外を開始しています。

今回お話するのはお知らせ等の文字画像の検出について今取り組んでいることの紹介です。

Adversarially Learned One-Class Classifier(ALOCC) for Novelty Detectionについて

単純に文字画像か否かの判別器を作ることを考えると以下のような問題があります。

  • 文字画像ではない画像、という教師データをどう集める?
    • 文字画像以外の画像では幅が広すぎる
  • 文字画像とはなにか?がはっきりとわからない。人によって違う。
    • 文字中心のポスターや、文字っぽいロゴや、サイズ表はどう扱うべきか
  • そもそも文字画像のサンプルが少ない
    • 圧倒的に教師データが不足している

これらの問題がAdversarially Learned One-Class Classifier for Novelty Detectionでは軽減できそうだったので、使えそうかどうか試してみました。

実験結果に行く前にこの手法を簡単に解説しておきます。

ネットワーク構造

論文に乗ってた画像そのまま上げます。

https://raw.githubusercontent.com/khalooei/ALOCC-CVPR2018/master/imgs/architecture.jpg

(M Sabokrou, "Adversarially Learned One-Class Classifier for Novelty Detection", arXiv.org, 2018 )

構造としてはGANにインスパイアされたものになります。 D がDiscriminatorなのはGANと同じで、GANでGeneratorだった部分がReinforcerとなっています。ReInforcerは画像(元の画像にノイズを追加したもの)を与えられて、何かしらの画像を出力します。

DiscriminatorはReinforcerが出力した画像と元の画像を見分けるように学習させ、ReinforcerはDiscriminatorを騙すように学習します。

学習の目的

さて、では上記ネットワーク構造は何を意図しているのでしょうか?これは元画像との違いを検出する検出器を作ろうとしているのです。

学習に使う画像は検出したいターゲットのクラスの画像だけ、今回の私の用途で言えば文字画像だけいいのです。文字画像以外の画像というものを教師として揃える必要がありません。これはありがたいです。 教師を作るためにこれは文字画像、これは文字画像ではない…と延々とやってるとこれはどっちだろうかという画像が出てきてだんだん混乱してきます。なのでこれは文字画像にしたいなってものを集めるだけでいいのはとても楽です。

R はターゲットに似た画像を生成するようになります。でも特定のターゲットクラスの画像しか学習してないので、ターゲットのクラスではない画像を渡すとぐちゃぐちゃな画像が生成される、ということを期待しています。今回は文字画像を学習させるので文字画像っぽいものは綺麗に再構築されるけど、それ以外はぐちゃっとなってほしいというわけです。

D は再構築された画像、つまりターゲットから少しでも違う画像を見分けようとするため、違いに敏感になるように学習されます。この結果 D はターゲットとの違い、つまり新規性の検出器となります。 文字画像を1として学習させれば、D が返す結果が1に近いほど新規性がない、つまり学習した文字画像に近い画像ということになり、0に近いほど新規性の高い見たことがない画像ということになります。

https://github.com/khalooei/ALOCC-CVPR2018/blob/master/imgs/overview.jpg?raw=true

(M Sabokrou, "Adversarially Learned One-Class Classifier for Novelty Detection", arXiv.org, 2018 )

論文ではペンギン画像を学習させています。

画像を R に通した結果、ペンギン画像はペンギン画像として再構築されていますが、ペンギンでない画像はなにか汚い画像になっています。この再構築した画像を D に渡すと元の画像を渡すよりも綺麗にペンギンとそれ以外を判別できるようになると主張されています。

実装

さて、一応論文の著者の方のgithubはあったのですが、tensorflowがゴリゴリでわかりづらかったのでpytorchを使って実装してみました。

Reinforcer

Reinforcerの中身は受け取った画像をConv2dで畳み込むencoderと、Deconvするdecorderです。GANだとパラメータ受け取ってDeconvしていくだけなので、ここがちょっと違います。

import torch.nn as nn

class Reinforcer(nn.Module):
    def __init__(self):
        super(Reinforcer, self).__init__()
        self.encoder = nn.Sequential(
            nn.Conv2d(3, 64, 3, stride=1),
            nn.ReLU(),
            nn.BatchNorm2d(64, 0.8),
            nn.Conv2d(64, 128, 3, stride=1),
            nn.ReLU(),
            nn.BatchNorm2d(128, 0.8),
            nn.Conv2d(128, 256, 3, stride=1),
            nn.ReLU(),
            nn.BatchNorm2d(256, 0.8),
            nn.Conv2d(256, 512, 3, stride=1),
            nn.ReLU(),
            nn.BatchNorm2d(512, 0.8),
        )
        self.decoder = nn.Sequential(
            nn.ConvTranspose2d(512, 256, 3, stride=1),
            nn.ReLU(),
            nn.BatchNorm2d(256, 0.8),
            nn.ConvTranspose2d(256, 128, 3, stride=1),
            nn.ReLU(),
            nn.BatchNorm2d(128, 0.8),
            nn.ConvTranspose2d(128, 64, 3, stride=1),
            nn.ReLU(),
            nn.BatchNorm2d(64, 0.8),
            nn.ConvTranspose2d(64, 3, 3, stride=1),
            nn.Tanh(),
        )

    def forward(self, x):
        x = self.encoder(x)
        x = self.decoder(x)
        return x

Discriminator

DiscriminatorはGANと何も変わりません。受け取った画像を畳み込んで全結合層に渡すだけですね。

import torch.nn as nn

class Discriminator(nn.Module):
    def __init__(self):
        super(Discriminator, self).__init__()
        self.model = nn.Sequential(
            nn.Conv2d(3, 64, 3, stride=2),
            nn.BatchNorm2d(64, 0.8),
            nn.LeakyReLU(0.2, inplace=True),
            nn.Dropout2d(0.25),
            nn.Conv2d(64, 128, 3, stride=2),
            nn.BatchNorm2d(128, 0.8),
            nn.LeakyReLU(0.2, inplace=True),
            nn.Dropout2d(0.25),
            nn.Conv2d(128, 256, 3, stride=2),
            nn.BatchNorm2d(256, 0.8),
            nn.LeakyReLU(0.2, inplace=True),
            nn.Dropout2d(0.25),
            nn.Conv2d(256, 512, 3, stride=2),
            nn.BatchNorm2d(512, 0.8),
            nn.LeakyReLU(0.2, inplace=True),
            nn.Dropout2d(0.25),
        )
        self.adv_layer = nn.Sequential(
            nn.Linear(512*9, 1),
            nn.Sigmoid(),
        )

    def forward(self, img):
        out = self.model(img)
        out = out.view(out.shape[0], -1)
        validity = self.adv_layer(out)
        return validity

学習

学習の仕方もGANとほぼ変わりません。

import torch
import torch.nn as nn
import torch.optim as optim
import torchvision.datasets as dset

...

device = torch.device("cuda:0")
netR = Reinforcer().to(device)
netD = Discriminator().to(device)

dataset = dset.ImageFolder(....)
dataloader = torch.utils.data.DataLoader(dataset)

criterion = nn.BCELoss()
optimizerR = optim.Adam(netR.parameters(), lr=..., betas=...)
optimizerD = optim.Adam(netD.parameters(), lr=..., betas=...)

...

for epoch in range(n_epochs):
    for data in dataloader:
        images = data[0]
        
        # dset.ImageFolderのtransformでtorchvision.transforms.Lambdaつかって
        # 元画像とノイズ画像両方とれるようにごにょごにょしてる
        real = images["real"].to(device) # 元画像
        noised = images["noisy"].to(device) # ノイズかけた画像
        batch_size = real.size(0)
        
        ############################
        # Update D network
        ###########################
        netD.zero_grad()
        # 本物
        label = torch.full((batch_size,1), 1, device=device)
        output = netD(real)
        errD_real = criterion(output, label)
        errD_real.backward()
        # 偽物
        fake = netR(noised)
        label.fill_(0)
        output = netD(fake.detach())
        errD_fake = criterion(output, label)
        errD_fake.backward()
        optimizerD.step()

        ############################
        # Update R network
        ###########################
        netR.zero_grad()
        # 偽物でDを騙す
        label.fill_(1)
        output = netD(fake)
        errR = criterion(output, label)
        errR.backward()
        optimizerR.step()

結果

文字画像を学習させてみた結果、文字画像はそこそこ綺麗に再構築され、それ以外は結構ぐちゃっとなるようになったようです。

f:id:beerbierbear:20181106155210p:plain:h400

accuracy false positive false negative
D(X) 0.92 0.06 0.02
D(R(X)) 0.87 0.03 0.10

むしろ判別性能はR をかました方が落ちてます。ただ、false positive、つまり本来文字画像じゃないのに文字画像と認識してしまった率は多少改善されました。文字画像は出したくないのですが、本来出したい商品画像が落とされてしまうのは避けたいのでfalse positiveはできる限り抑えたいところです。そういう意味ではそこそこ意味はあるのかなと思います。

感想

  • 文字画像はそこまで綺麗にいかない、というか論文の結果の画像が綺麗にできすぎ。何か書いてない(もしくは私がちゃんと読めてない)工夫があるのかも。
  • Reinforcerは論文にはない工夫を追加した(まだ実験中なので内容は内緒)。そうしないとただのGANにしかならない。ターゲット画像以外の画像渡しても普通にそこそこ綺麗な画像が再構築される。
  • Batch Normarizationは偉大。

まとめ

文字画像を正確に判別できるようになるまではまだまだ道のりが長そうです。 false positive 3%といっても画像は1日に何万枚と上げられてきますので、間違いの絶対数はそこそこ多くなります。理想的には桁をもう一つ下げたいところです。

でも今回の判別器では、単純に文字画像判別器を作ったときに弾かれがちだった文字Tシャツはかなり高精度で文字画像ではなく商品であると認識できるようになりました。

こういうやつ

f:id:beerbierbear:20181106155246p:plain:h300

(RACCOONS ONLINE STOREより)

引き続き良きユーザー体験を提供できるように頑張っていきます。

Git(Hub)+CircleCI+Slack で実現する静的コンテンツ配信システム

BASEでエンジニアリングマネージャーを担当している加賀谷です。普段は採用に携わったり、1on1での経験学習の促進などを通じて、個人と組織のアウトプットが大きくなるようにサポートする仕事をしています。また、サービス開発に関わる体験を良くしていくこともしています。その中で今回は、静的コンテンツのCI/CDでしていることを紹介したいと思います。

静的コンテンツのホスティング

静的コンテンツは、サーバサイドでリクエストに応じてレスポンスする内容を作成しないデータです。主に、サイト内で使う画像、CSS、JS、ランディングページなどのHTMLファイルになります。これらのファイルはよく、AWSのS3に置いてホスティングして前段にはCDNを配置し、Webブラウザの同時接続数を考慮してサービスとは別のホストに分散したりしますが、BASEでもそうしています。

f:id:yuhei_kagaya:20181030205749p:plain

静的コンテンツ用のGitリポジトリを用意

CSSやJSが成果物となる開発はBASEにおいては主にデザイナーとフロントエンドエンジニアが担っています。以前はPHPもJSもCSSも同じリポジトリで開発していたのですが、今ではサービスのメインGitリポジトリとは別のリポジトリで開発〜デプロイをするようにしています。もちろんメインGitリポジトリからいっさいのJSやCSSを無くしているわけではなく、TwitterのBootstrapのようにコンポーネントとなるCSSやJS、ランディングページなど、メインから独立できるファイル群をこのような別リポジトリに入れています。

git pushでCircleCIからaws s3 syncする

デプロイ先はS3で、git push を契機にCircleCI上から aws s3 sync しています。以前は手動で本番S3に画像をアップロードする属人的な場面も少なからずあったのですが、今ではリポジトリの中に画像を入れてCircleCI経由でS3に配置するようにしています。

f:id:yuhei_kagaya:20181030205912p:plain

開発ワークフローと環境別のURL

静的コンテンツリポジトリの開発ワークフローはメインのGitリポジトリと同じようにしています。

  1. develop ブランチから開発用ブランチfeature/xxx を切って開発
  2. developへプルリクエスト&マージ
  3. リリース時は develop から mastergit-pr-release でリリース用プルリクエストを作成
  4. masterを本番環境へデプロイ

環境ごとに静的コンテンツのURLが欲しいので、S3はそれぞれ用意してCircleCIが回るブランチでaws s3 sync 先を変えています。 develop ブランチの時にはステージング用のS3、master ブランチの時には本番、それ以外のブランチは開発用S3へ対応させています。

f:id:yuhei_kagaya:20181030205930p:plain

.circleci/config.yml

CircleCIではだいたい以下のようなことをしています。

  1. checkout
  2. awscliをインストール
  3. リポジトリ内の特定ディレクトリ配下の各ファイルにACLとcache-control、content-typeをつけてaws s3 sync
  4. syncしたファイルのパスのCloudFrontのキャッシュを削除

また、アップロードされたファイルのURLなどをSlackへ通知して気付けるようにもしています。

version: 2
jobs:
  build:
    docker:
      - image: docker:17.12.0-ce-git
    environment:
      - TZ: "/usr/share/zoneinfo/Asia/Tokyo"
      - SYNC_OPTIONS: "--cache-control \"max-age=86400\" --acl public-read --size-only --no-progress --delete"
      - S3: "static-example-net"
    steps:
      - checkout
      - run:
          name: Install dependencies
          command: |
            apk add --no-cache \
              py-pip=9.0.1-r1 \
              curl \
              curl-dev \
              openssl
            pip install \
              awscli==1.14.40
      - run:
          name: Upload files to S3
          command: |
            set -x
            tmpfile=`mktemp`
            for i in `cat .s3ignore | grep -v "^#"`
            do
                IGNORE_OPTIONS="${IGNORE_OPTIONS} --exclude \"**/${i}\""
            done
            # リポジトリ内webroot/配下の各ファイルを適切なcontent-typeをつけてsync
            eval `printf "aws s3 sync webroot/ s3://%s/ %s %s --exclude \"*\" --include \"%s\" %s --content-type \"%s\"" "${S3}" "${AWS_PROFILE}" "${SYNC_OPTIONS}" *.css "${IGNORE_OPTIONS}" text/css` | tee -a $tmpfile
            eval `printf "aws s3 sync webroot/ s3://%s/ %s %s --exclude \"*\" --include \"%s\" %s --content-type \"%s\"" "${S3}" "${AWS_PROFILE}" "${SYNC_OPTIONS}" *.js "${IGNORE_OPTIONS}" application/javascript` | tee -a $tmpfile
            eval `printf "aws s3 sync webroot/ s3://%s/ %s %s --exclude \"*\" --include \"%s\" %s --content-type \"%s\"" "${S3}" "${AWS_PROFILE}" "${SYNC_OPTIONS}" *.json "${IGNORE_OPTIONS}" application/json` | tee -a $tmpfile
            eval `printf "aws s3 sync webroot/ s3://%s/ %s %s --exclude \"*\" --include \"%s\" %s --content-type \"%s\"" "${S3}" "${AWS_PROFILE}" "${SYNC_OPTIONS}" *.html "${IGNORE_OPTIONS}" text/html` | tee -a $tmpfile
            eval `printf "aws s3 sync webroot/ s3://%s/ %s %s --exclude \"*\" --include \"%s\" %s --content-type \"%s\"" "${S3}" "${AWS_PROFILE}" "${SYNC_OPTIONS}" *.png "${IGNORE_OPTIONS}" image/png` | tee -a $tmpfile
            eval `printf "aws s3 sync webroot/ s3://%s/ %s %s --exclude \"*\" --include \"%s\" %s --content-type \"%s\"" "${S3}" "${AWS_PROFILE}" "${SYNC_OPTIONS}" *.jpg "${IGNORE_OPTIONS}" image/jpeg` | tee -a $tmpfile
            if [ -s $tmpfile ]; then
              tmpdir=`mktemp -d`
              split -l 30 $tmpfile $tmpdir/
              for splited in `find $tmpdir -maxdepth 1 -type f`; do
                # CloudFrontからのキャッシュを削除
                PATHS=`grep -ao "s3://${S3}/.*$" ${splited} | sed "s/^s3:\/\/${S3}//g"`
                aws cloudfront create-invalidation ${AWS_PROFILE} --distribution-id ${CDN_DISTRIBUTION_ID} --paths ${PATHS}
              done
            fi

サーバサイドのリポジトリと分けて開発、デプロイするメリット

1日に何度も本番環境へデプロイをする状況下においても、1度にデプロイするコード量が減り確認範囲が狭くなることで、よりカジュアルにデプロイできることがメリットかなと思います。サーバサイドと同じリポジトリで開発していたときには、例えばランディングページのちょっとしたスタイル変更にも、CircleCIでサーバサイドの全テストを回してBlue-Green Deploymentリリースフローをする流れを必要とするために比較的変更の反映に時間がかかっていましたが、これもなくなり手続き的にも早いデプロイができるようになりました。

Slackからgit-pr-release

リリース用プルリクエストを作るときに、git-pr-release コマンドを実行していますが、これをSlack Botでもできるようにしています。スマホのSlackアプリからも実行できるので便利です。GitHubのアカウントをSlackのアカウントに変換してメンションさせたり、リリースのサマリをSlackへ通知して変更がざっくり共有できるように工夫しています。

1. Botにメンションするとセレクトボックスを返すのでデプロイするリポジトリを選ぶ

f:id:yuhei_kagaya:20181031130502p:plain

2. 選択したリポジトリをその場で確認される

f:id:yuhei_kagaya:20181030210242p:plain

3. Yesを押すとgit-pr-releaseを実行

f:id:yuhei_kagaya:20181030210340p:plain

4. スレッドでメンションが返ってくる

f:id:yuhei_kagaya:20181030210552p:plain

f:id:yuhei_kagaya:20181030210639p:plain

5. 作成されたリリース用プルリクエストのURLが通知されるので、確認してmasterマージするとCircleCI経由で本番デプロイ

f:id:yuhei_kagaya:20181030210709p:plain

まとめ

今回は、GitHubとCircleCIでS3へデプロイするワークフローを紹介しました。まだリポジトリ内には画像のようなバイナリファイルがあまり多くないのでリポジトリが肥大化してgit pull/pushが苦になることはありませんが、多くなってきたら Git LFS も検討してみたいと思います。

BASEの開発チームでは良いサービスをつくっていくために開発体験の改善も楽しみながら活動しています。ご興味を持たれた方いらっしゃいましたら是非ご連絡いただければ幸いです。

jobs.binc.jp

機械学習にアノテーションを活用して、商品検索の関連キーワード機能を作る

DataStrategyの齋藤(@pigooosuke)です。

ネットショップ作成サービス「BASE」は60万店舗のショップが利用しており、ショッピングアプリ「BASE」のユーザーは、新着商品、キーワード検索、関連商品、商品特集などを介して気になる商品を見つけることができます。今回、新機能として、検索ワードに関連するキーワードを表示することで、ユーザーの興味のありそうな商品にたどり着ける動線を機械学習を活用して実装しました。

DataStrategyチームは発足して間もなく、サービスドメインに適応した単語辞書がなかったので、新規で作成するところから始まりました。機械学習におけるデータセットのアノテーションについての知見が共有される機会が少ない印象もあり、折角なので今回私達が行ったデータ作りから実装までの流れをご紹介します。

概要

f:id:HifiroleLorum:20181016175341p:plain

今回、どんなキーワードも意味的に近ければ、サジェストしても良いとはせず、ホワイトリストに登録されたキーワードのみをサジェストし、サービス品質を保てることを最低条件としています。また、関連キーワードにはいくつか定義が存在しますが、今回は類義語をサジェストする関連キーワードの開発を行いました。

大まかな手順は以下となります。

  1. 「BASE」で過去検索されたキーワードのデータからキーワードのリストを取得
  2. 前処理として、不適切な検索キーワード、優先度が低いキーワードを一定のルールで削除
  3. 有用なキーワードに対して、アプリに表示可能な単語かをアノテーション
  4. チェックがOKだったキーワードを登録したMeCab辞書を活用し、商品のタイトル・紹介文を分かち書きに分解
  5. 商品情報(タイトル+説明文)に対してWord2vecを通し、類似度の高い単語を調査し、関連キーワードとして表示

f:id:HifiroleLorum:20181016175427p:plain

Word2vecについて

文字の通り、word(文字)をvector(ベクトル:n次元)で表現するために深層学習を行うモデルです。それぞれの単語はその周辺に出現する単語によって決められているという仮説に基づいて学習を行います。今回の記事のテーマではないので詳細は割愛します。

# 学習語彙での類似度の高い単語を出力
model.wv.most_similar(positive='ワンピース', topn=6)
> [('フレアワンピース', 0.8115994334220886),
>  ('ロングワンピース', 0.7681916952133179),
>  ('ミニワンピース', 0.7396647930145264),
>  ('aラインワンピース', 0.7346851825714111),
>  ('vネックワンピース', 0.7129236459732056),
>  ('レースワンピース',0.7060198783874512)]

アノテーション

実際にアノテーションを進めていく前に、きちんとルールを決めておくことがかなり重要です。 いくらルールを厳格に決めておいても、「このケースはOKで通してみたけれど、アノテーションを進めて全体感がつかめてくると、あれはNGのケースだった。」という手戻りは少なからず発生してしまいます。 ましてや、集団でアノテーションを実施する時の難易度はかなり高くなると感じています。 今回は、(幸いなことに?)アノテーション作業を一人で行ったので、可能な限りルールの統一が出来たと思います。

参考までに、今回設定したルールとしては下記の通りになります。 今回は、各キーワードについて3つのラベルをつけていきます。

NG単語ラベル

アプリで表示するに耐えうる表現・ユーザーのUXを損なうことのない表現かをチェックしていきます。 OKとなった(ホワイトリストに登録された)キーワードのみアプリ上で表示されます。

  1. 基本的に最小商品単位になりうるものはOK
  2. 修飾語が3語以上つながっているものはNG(バックロゴ付き薄手コート:バック,ロゴ付き,薄手
  3. ブランド名が含まれているものは、社内ルール的にNG(ルイヴィトン,ディズニー
  4. 年号・数字・期間限定のキーワードが含まれているものはNG(例外で、季節感のあるものはOKとしているサマーサンダル,スプリングコート
  5. 色を表すものはNG(全色対応など、候補のノイズになる可能性が高いため)
  6. 口語表現がついているものはNG(ゆったり,もこもこ
  7. 単語単体で何を検索しているのか不明瞭なものはNG( ,クーポン,名刺

ゆらぎ表現ラベル

単語表現のゆらぎは検索エンジンにはつきものです。類似表現が出てきた時、両単語を表示することはせず、どちらかの単語のみを表示します。これをゆらぎ表現として別途登録して入出力制御に活用します。 例:Pコート,ピーコート,キャミソール,キャミなど。

  1. ゆらぎ表現は検索件数が多いものを正解とする
  2. 鞄でいう、〇〇バッグ,XXバックのように語尾が統一されないジャンルが存在するが、検索実績が多いキーワードを正解とする

カテゴリーラベル

これは個人の主観が大きく入るのと、全体のバランスを見つつゼロからラベルをつけていくので、大きく手戻りが発生する作業なかなか難しい作業です。こちらのラベルも出力制御に活用します。BASEで設定しているカテゴリーを参考にしつつ作業を進めました。

  1. 各単語について、何のカテゴリーなのかを手動でラベリング(コート,,
  2. カテゴリー階層は最大3階層まで

アノテーションデータはこのように構成されています f:id:HifiroleLorum:20181016175504p:plain

入出力の制御(アノテーションラベルの活用)

f:id:HifiroleLorum:20181016175514p:plain

Word2vecは学習に使用された語彙に対して、それぞれベクトルが設定されています。上図のようにアノテーションで属性が判明している単語もあれば、まだ調査していない未知語も入力として入ってくる可能性があります。未知語はシャットアウトすれば良いという判断もありますが、対応語を増やすため未知語でも入力を認めることにしました。ただ、Word2vecにより大量の文章を学習しており、未知語の関連語として何が出力されるのかブラックボックス化されてしまう懸念がありました。この不透明さを解消するために活用したのがこれまでアノテーションしてきたラベルです。

NGフィルタは、ホワイトリストに登録された単語のみを出力するように配置されています。ゆらぎ表現フィルタは、ホワイトリスト内の単語を片寄せするために配置されています。カテゴリーフィルタは、出力するカテゴリーを統一させ、出力ノイズを減らすために設置されています。カテゴリーフィルタは、特に未知語や学習が不十分な語彙に対して大きく影響を与えます。

下でカテゴリーフィルタ設定前後を比較しています。

# カテゴリー補正前
model.wv.most_similar(positive='ポット', topn=6)
>  [('ピッチャー',0.6643306016921997),
>   ('ティーポット',0.62236487865448),
>   ('ボウル',0.6212266087532043),
>   ('鉢',0.5690277814865112),
>   ('ティーカップ',0.5430924296379089),
>   ('花器',0.5429580211639404),

# カテゴリー補正後
model.wv.most_similar(positive='ポット', topn=6)
>  [('ピッチャー',0.6643306016921997),
>   ('ティーポット',0.62236487865448),
>   ('ボウル',0.6212266087532043),
>   ('ティーカップ',0.5430924296379089),
>   ('コーヒードリッパー',0.5426920056343079),
>   ('トレイ',0.5033057332038879),

花器といった、容器ではあるものの、キッチン周りにはふさわしくない園芸向けのキーワードの出現が抑制されるようになっています。結果、類似度の閾値制御だけでは弾くことが難しいケースもカテゴリーを設定することによって解決することができました。

まとめ

今回は、アノテーションを機械学習の制御に活用してみたという内容をお送りしました。機械学習の学習結果をそのまま信用して活用するのではなく、アノテーションの結果を活用することで品質向上を行うことができました。ただ、アノテーションを闇雲に行うのではなく、どう活用していくのか事前の課題設定をきちんと立てることも重要そうですね。

BASEでは一緒にネットショップ作成サービスを開発・改善するエンジニアを募集してます。 機械学習のチームでは、様々なデータや技術を使ってECならではの開発を続けています。 ご興味のある方はぜひ遊びにきてください!!

jobs.binc.jp

BASE BANK コーポレートロゴ誕生のデザインプロセス

f:id:yoshiokachang:20180925185620j:plain こんにちは、BASEのDesign Groupに所属している吉岡です。

ネットショップ作成サービス「BASE」のデザインや、2018年1月に設立されたBASE株式会社の100%子会社であるBASE BANKの株式会社立ち上げにデザイナーとして携わっています。

BASE BANK株式会社は、「銀行をかんたんに、全ての人が挑戦できる世の中に」をミッションとし、現在は関連事業の立ち上げを行っています。代表はBASE社と同じく鶴岡裕太が務め、メンバーはBASEと兼務している者もいます。オフィスも同じフロアにあり、カルチャーや思想なども含めてBASE社とかなり近いと言えます。

先日、BASE BANK社のロゴを作成しましたのでデザインプロセスの経緯を書き留めておきたいと思います。この記事を通じてBASE BANKがどのような会社なのか伝われば嬉しいです。

デザインの経緯

はじめに仮置きされていたロゴは下記画像の右のロゴでした。 f:id:yoshiokachang:20180925184738j:plain

左側がBASEのコーポレートロゴで、BASEロゴに合わせて、タイポグラフィをアウトライン化して入れたものでした。

'SE'に対して、BANKの'NK'がフォントボディが大きく、上手くはまりません。

綺麗にデザインしよう、ということでここからロゴの作成をスタートします。代表の鶴岡に時間をもらいながらヒアリング/調整を行なっていきました。

まず、BASE BANKの立ち位置と印象を考える

ヒアリングしたところの概要をまとめると、

  • 「銀行をかんたんに」をミッションとするので安心感を与えるロゴにしたい
  • ただ、固すぎないような自由なデザインも欲しい
  • 印象としてはストリート感が少し欲しい

という内容でした。

それを受けてデザインした初稿がこちらです。 f:id:yoshiokachang:20180925184921j:plain

なんとなく、Aが良いかな...という感じで、もう少し安定感のあるフォントが良いという流れに。ただ、この段階では、しっくりくる提案ができていないような感触がありました。

迷うロゴデザイン

ここから2週間ほどかけて3回ほどアップデートしていくのですが、なかなか良い形で決めることができませんでした。

フィードバックとしては、

  • BASEとの親和性が感じられない
  • ロゴの立ち位置が中途半端になっているのでフォントを変える意味がなさそう

などでした。

この時点でスタートから既に3週間は掛かっていました。しかし、BASE社のタイポグラフィやテイストに捉われすぎて、壊せていない部分があったので、今まで作ったロゴは全て壊してしまおうという結果に。

他のプロジェクトと並行して行なってきたのですが、この段階でロゴの作成に注力していきます。

コンセプト設計へ立ち返る

BASE BANK のロゴはどういう立ち位置なのか。パーソナリティとしてどういう人なのか?を定めて、それに対しロゴを考えるフローに戻しました。

BASE BANKは何をする会社なのか?

MTGやブレストで上がった内容から、BASE BANKはどういう会社なのかまとめました。

  • もっと銀行を簡単にしたい
  • ITとデータを活用する
  • 多くの人にチャレンジしてほしい
  • 成長をフォローしていく
  • 新しく立ち上げた会社

パーソナリティを決める

どういう性格なのか、を決めます。

  • 革新的(銀行を簡単にする、新しく立ち上げた会社)
  • クール(ITとデータの活用)
  • 安心感(チャレンジして欲しい、成長をフォローしていく)

以上の3点を踏まえた上で、ロゴを再考しました。

ラフスケッチしていく

まずは手書きでラフスケッチしていきます。 パーソナリティを考慮し、すっきりとしつつ、安定感のあるようなロゴの形を簡単にスケッチしていきます。一旦思いつくままに書き込みます。

あまり綺麗ではありませんが...100~150個位は書いたかと思います。 後々の閃きにも繋がってくるので、どんなアイデアでも書き留めることが重要です。 f:id:yoshiokachang:20180925184959j:plain

データに落とし込む

手書きラフから、イメージに合うものを選び、Illustratorで描いていきます。手書きでは問題なさそうな案も、データにしてみるとイメージが違った..などの案はこの段階で絞り込んでいきます。 f:id:yoshiokachang:20180925185038j:plain

ベースと、方向性の決定

f:id:yoshiokachang:20180925185103p:plain

brandon grotesqueという割と新しいフォント。1930年代のgeometricフォントの歴史的なものを継承しつつ、ディテールに新しさを感じます。可読性もあり、安定感と革新性を感じられるものを選びました。

このベースを元にデザインの方向性を決定しました。

洗練された印象を持たせる白をベースに、フォントの持つ安定感は残し、角丸は消す

BASEとの親和性を持たせるためにスクエアにこだわる

成長をフォローしていくイメージで、斜めのラインモチーフを入れる

ロゴだけに注力し始めて2週間...出来上がったアウトプットがこちらです。安定感のあるボディと革新的な部分を共存させるようなイメージで作り上げました。 f:id:yoshiokachang:20180925185255j:plain

ここから、角度をつけた部分やディテールを調整していきました。

角度をつけた部分が急に見えるので30度から15度に変更したり、安定感が欲しい...ということで'A'のバー位置を下げたり、もう少し落ち着かせたい...角度をつけるロゴは1つに限定する、などの変更をしました。

スタートから1ヶ月半、遂に完成したロゴはこちらです...! f:id:yoshiokachang:20180925185205j:plain アウトプットとしては、単にロゴだけなのですがBASE BANK社のこれから成し遂げたい事や思いを全て詰め込んでロゴを作れたかな..と思います。会社について、ひたすら考えることができた1ヶ月半でした。

文字領域と四角の領域は黄金比率1:1.618を使ってレイアウトを組んでいます。

全てをグリッドに合わせ過ぎると、小さく見える文字があるので若干調整しつつ、斜めの角度を0.5%単位、線幅など最終0.4ptの細かい調整をしています。

最後に

コーポレートのロゴを作成する、という経験はインハウスデザイナーでもなかなかできないので良い経験になりました...!自ら手を上げれば、色々なデザインに携わることができる環境だと思います。

この記事でBASE BANKのチームで一緒に働くことに興味が出たという方は以下の募集からご連絡ください!

jobs.binc.jp

TerraformでNGTのポータブル環境を作った

はじめまして、BASEでSREに所属している浜谷です。現在は主にAWSを使用したインフラ構築と運用を担当しています。
そこで今回は前回好評だったBASEビール部部長が語ってくれた「Yahoo!の近傍探索ツールNGTを使って類似商品APIをつくる」のインフラ環境の構築についてお話をしようかと思います。

1. 背景

BASEでは機械学習の環境以前に今本番で何が動作しているのか、又その全体を把握するにはAWSのコンソールにログインして調査する方法しかありませんでした。 AWSの運用をしているとよくある事かと思います。
そんな中BASEではシステム全体構成図の見直しやサーバの一覧を自動化したりとインフラの見える化を進めています。
そこで次はインフラの構成管理を行っていこうといった流れがあり、まずはData Strategyチームの環境をTerraformで構成管理しようと相成りました。

2. 機械学習の環境

f:id:jhamatani:20180912101224p:plain

環境をサクッと説明するとS3に画像がアップロードされたら、SNSにイベントを送付します。 SNSからSQSやLambdaに振り分けて情報を蓄積し、蓄積したデータを元にECSのDockerで機械学習した結果をAPIで返すといったインフラ環境となります。 類似商品APIって何と詳細が気になった人はBASEビール部部長の記事を読んでみてください。

3. そもそも構成管理は必要?

運用をしていく為に構成を把握する事は必要不可欠です。 ただ私自身構成管理ツールの必要性をあまり感じていませんでした。

というのはSIerの出身なので設計書、手順書、運用のドキュメントは納品物なので時間を掛けて作るのは当たり前でした。 SIerのエンジニアはoffice製品でのドキュメント作りや成果物の承認に時間を割くことが多く、その上構成管理のツールまで必要なのって感じている人は少なくないと思います。 またドキュメントでの構成管理は構築を始めるまでに時間が掛かるのは当然といった考え方が前提としてあります。

しかし、BASEの行動指針の一つに「MOVE FAST」があります。 このMOVE FASTを実現するためには、時間が掛かるのは当然といった考え方を捨てる必要があります。 そこで構成管理ツールは必要不可欠なのです! 構成管理ツールを使用するとインフラをコードで管理できるので設計から構築までを一気通貫に実現できます。

4. なぜTerraformなの?

現在BASEではAWSをメインに利用しているので、AWS CloudFormationの方が良いかもしれません。
機械学習の環境においてGCPのBigQueryを使用する場面が今後発生してくるかと考えています。
その際にマルチプラットフォームに対応した構成管理ツールを選ぶ必要がありました。

5. コードでのインフラ管理

AWSを管理コンソールで構築しているとGUIベースで確認するか、ドキュメントベースで環境を把握する必要があります。
しかし、Terraform等の構成管理ソフトを使用するとコードベースで管理できます。

VPCをTerraformでコード定義すると以下のようになります。

resource "aws_vpc" "ds-image-processing-vpc" {
  cidr_block = "10.1.0.0/16"

  tags {
    Name = "ds-image-processing-vpc"
  }

  # We explicitly prevent destruction using terraform. Remove this only if you really know what you're doing.
  lifecycle {
    prevent_destroy = true
  }
}

コードで管理することでGitHubを利用した管理が出来ます。
GitHubはアプリエンジニアだと日常的に使用しますが、インフラエンジニアにとっては少し敷居が高く感じます。 私もBASEに入社するまでは殆どGitHubは使用していませんでしたが、今では普通に使用してレビューなどの履歴も残すことが出来てとても便利に感じています。 今まではインフラ構築のレビューをシステム構成図や設計書ベースでのレビューをしていたので、別途レビュー票を起こしたり等の手間が大幅に減りました。

f:id:jhamatani:20180912101551p:plain

またインフラ構成の変更をAWSのコンソールでを漏れなくチェックするのは非常に困難です。
しかし、GitHubを使用することで差分の確認をすることで、何を変更したのかも一目瞭然です。

f:id:jhamatani:20180912101546p:plain

6. 実際にTerraformを使ってみよう!

それではいよいよ実際にTerraformを使用してみましょう。
今回はハンズオンとして、RDSのAuroraとEC2のLinuxインスタンスを実際に作ってみます。

①まずはTerraformを実行出来る環境の準備をします。
 Terraformのダウンロード(URL:https://www.terraform.io/downloads.html
  ※バージョンは適時ダウンロードしたファイルに読み替えてください。
 ダウンロードしたファイルを展開して、環境変数にパスを通す。

$ mv terraform_0.11.4_linux_amd64.zip /usr/local/bin
$ unzip terraform_0.11.4_linux_amd64.zip
$ ls -l terraform 
 ※実行権限があることを確認
$ env | grep PATH 
 ※PATHに/usr/local/binが通っていることを確認
$ mkdir -p <作業ディレクトリ> 
$ cd <作業ディレクトリ> 

②GitHubにサンプルを用意しましたので、ダウンロードします。
 GitHubのリポジトリ:https://github.com/baseinc/Hands-on-Terraform
 Terraformのドキュメント:https://www.terraform.io/docs/providers/aws/

$ git clone git@github.com:baseinc/Hands-on-Terraform.git
$ terraform init
$ vi terraform.tfvars

 必要に応じて以下の内容を書き換えてください。

aws_access_key = "<AWSのアクセスキー>"
aws_secret_key = "<AWSのシークレットキー>"
aws_region = "ap-northeast-1"

main_aurora_root_user = "<auroraへのアクセスユーザ>"
main_aurora_root_password = "<auroraへのアクセスパスワード>"

external_ip = "<EC2にアクセス出来るIP>"
host_ssh_key = "<EC2にアクセスする際のキーペア名>"

③準備ができたので、Terraformを実行するだけです。

  • 実行前の動作確認(DryRun): $ terraform plan
Plan: 20 to add, 0 to change, 0 to destroy.

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

 上記の様に「20 to add」と表示されればOKです。
 20個のリソースがコマンド一つで作成されます。

  • 実行:$ terraform apply
Plan: 20 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

 「yes」と回答します。

aws_iam_role.db-main-aurora-monitoring: Creating...

 リソースの作成が開始します。

④リソース作成完了までは少し待ちます。

aws_rds_cluster_instance.db-main-aurora-instance.0: Still creating... (12m50s elapsed)
aws_rds_cluster_instance.db-main-aurora-instance[0]: Creation complete after 12m57s (ID: db-main-aurora-instance-0)

Apply complete! Resources: 20 added, 0 changed, 0 destroyed.

 これでAWS上にAuroraとLinuxのインスタンスが立ち上がりました。  どうですか、サクッと作れてしまいましたね。
 リソースが出来たかどうか、実際にAWSコンソールにログインして確認してみましょう!

f:id:jhamatani:20180912101538p:plain f:id:jhamatani:20180912101543p:plain

6. リソースを作った後は。。。

こんなに簡単に作れてしまうと、次から次へとリソースを作ってしまいますね。
そして放置なんてことになったら、AWSに多大な貢献をしてしまうことになりかねません。
ですので不要になったらきちんとお片付けを実施します。

  • 削除 :$ terraform destroy

コマンドを実行して削除完了と思ったら。。。。

Error: Error running plan: 1 error(s) occurred:

aws_route.db-vpc-route-external: aws_route.db-vpc-route-external: 
the plan would destroy this resource, but it currently has lifecycle.prevent_destroy set to true. 
To avoid this error and continue with the plan, either disable lifecycle.prevent_destroy or adjust the scope of the plan using the -target flag.

あれ?なんでだろう。。。エラーが出てしまいます。
理由は削除保護が有効になっていたからです。 VPCやDB等は間違えて消してしまうと想定外の影響が出てしまう可能性がある為、簡単に消せない様に削除保護prevent_destroy = trueをリソースに定義していました。
ただ、今回は消してしまいたいのでリソース上の定義を全てprevent_destroy = falseに変更して再度実行してみましょう。

Do you really want to destroy all resources?
  Terraform will destroy all your managed infrastructure, as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value: yes

「全てのリソースを削除しますが、良いですか」と聞かれます。
「yes」と回答します。

…
aws_subnet.db-vpc-subnet-d1: Destroying... (ID: subnet-0bae4cb4032d56f93)
aws_subnet.db-vpc-subnet-a1: Destroying... (ID: subnet-0a9d2118812674f12)
aws_subnet.db-vpc-subnet-c1: Destruction complete after 0s
aws_subnet.db-vpc-subnet-d1: Destruction complete after 1s
aws_subnet.db-vpc-subnet-a1: Destruction complete after 1s
aws_security_group.db-main-aurora-security-group: Destruction complete after 2s
aws_vpc.db-vpc: Destroying... (ID: vpc-0790adf505d9e6ac3)
aws_vpc.db-vpc: Destruction complete after 0s

Destroy complete! Resources: 20 destroyed.

今度は消えましたね。 また必要になったら、$ terraform applyで作り直しましょう。

まとめ

Terraformを使用することで新規作成から削除までコマンドで簡単に出来るようになりました。 今後、既存の本番環境のTerraform化や新規にプロダクトを開発している子会社のBASE BANKもTerraformを利用したりしているので、また機会があれば続きを書ければなぁと思います。