Sansan Tech Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

【Techの道も一歩から】第18回「アルゴリズムをコンテナ化しWebAPIとして利用可能に」

こんにちは。 DSOC R&D グループの高橋寛治です。

最近はスマブラSPの研究活動に熱心に取り組んでおり、少しずつ成果が出始めておりホッとしているところです。

さて、研究開発活動では何かしらのモデルやアルゴリズムを開発し、それを共有します。 共有する際に、環境の違いにより動作しなかったり、そもそもの利用方法がわからなかったりします。

こういった問題に対して、Docker コンテナにより WebAPI として提供することで、環境を問わず、かつ利用方法も明確にする方法について紹介します。

API提供するコンテナの作成

アルゴリズムを組み込むための Docker コンテナを作成します。

ディレクトリ構成

$ tree
.
├── Dockerfile
├── api.py
└── requirements.txt

Dockerfile

FROM python:3.6

WORKDIR /app

COPY requirements.txt .
RUN pip install -r requirements.txt

COPY . .

CMD ["hug", "-f", "api.py"]

requirements.txt

hug==2.4.1

api.py

import hug


@hug.get("/")
def health_check():
    return {
        "message": "Good."
    }

hug は Python で簡単に API を提供するためのフレームワークです。 デコレータで任意のメソッドについてラップするだけで、APIエンドポイントとして動作します。 アルゴリズムのAPI化においてラップするだけで動作するというのは魅力的です。

ファイルを準備したら、コンテナをビルドして動かしてみましょう。

$ docker build -t tech_blog_18
Sending build context to Docker daemon  4.096kB
Step 1/6 : FROM python:3.6
...
Successfully tagged tech_blog_18:latest
$ docker run -it --rm -p 8000:8000 tech_blog_18
...
Serving on :8000...

さてコンテナが稼働中ですので、実際に動作確認をしてみます。 curl コマンドで localhost:8000 を叩くことで、メッセージが返ってくるはずです。

$ curl -XGET "http://localhost:8000"
{"message": "Good."}

WebAPI としての動作をしていることが確認できました。 この関数内で任意のアルゴリズムを動作させることで、コンテナ化します。

続きを読む

壁を登るエンジニアたちについての考察

f:id:s_yuka:20190404141033j:plain
Sansan事業部マーケティング部フロントエンドエンジニアの吉田です。

弊社には「よいこ」*1という社内の部活動制度があり、筆者はボルダリング部に所属しています。
ボルダリング部は部長である弊社CTOの藤倉を筆頭に、プログラミング等を生業にしているITエンジニアの占める割合が非常に多い。

実際、筆者もボルダリングにはまって、なるほどこのスポーツはエンジニアに向いている箇所が多分にあるなと思ったので考察してみました。

ボルダリングとは?

ボルダリングは壁についたホールドと呼ばれる石を使って壁を登っていくスポーツです。
壁一面にホールドが散りばめられているのですが、どのホールドを使ってもよいというものではなく、「スタート」の印があるホールドから決められたホールドを伝って行き、「ゴール」の印があるホールドを両手でつかめばクリアという、ゲーム要素のあるスポーツです。

伝っていくホールドの数や形によって難易度が変わってくるため、初心者から上級者までが同じ壁で楽しめるスポーツです。

ボルダリングとは

オブザベーション

ボルダリングは課題と呼ばれる各コースがあり、上級者になるほど、いきなり登り始めることはしません。
スタートホールドと伝っていくホールド、そしてゴールホールドを観察し、どのように体を使って登っていけばその課題をクリアできそうかを、まずは頭の中でシミュレーションします。
その行為を「オブザベーション」といいます。

一方、エンジニアは日々の仕事で、設計書(仕様書)を元に機能を実装していくということをしています。
いきなりプログラムを書き始めるということはなく、まずはどのようにプログラミングして組み立てていけば仕様どおりのものができあがるかを、脳内や紙に書き出すなどしてシミュレーションします。

ムーブ

やさしい課題はハシゴを上る要領で難なくクリアできてしまいます。しかし難易度が上がると様々な「ムーブ」といわれるテクニックを駆使してひとつひとつホールドを掴み登攀(とうはん)して行かなければなりません。

一方、エンジニアも「Hello World!」を表示するだけ、というような簡単なことであればさっくりと実現できてしまいますが、基本的に仕事は容易くないのが常です。
エンジニアとして自分の持つ全ての知識とテクニックを振り絞って、求められるものをキーボードを打って創り出して行かなければなりません。

トライアル・アンド・エラー

簡単な課題ならサラリとクリアできてしまうのですが、実力以上の課題に臨むと一度で成功するとは限りません。途中で落下してしまいます。
失敗してもその原因を考え、どのようにすれば今までの失敗を繰り返さずにゴールまでたどり着けるかを考察し、再チャレンジし、成功するまで繰り返します。

エンジニアも複雑な要件を実装する場合、頭で考えたようにプログラミングしても、途中でエラーにあたったりして一度で上手く行くとは限りません。
試行錯誤を繰り返し、コードを正解へと導き創りあげます。

成功、喜び

そして、失敗し、試行錯誤を繰り返し、ついに課題がクリアできたときの喜びは格別です。
それはまたエンジニアの仕事もしかり。


というわけで、ボルダリングとプログラミングの共通点を列挙してみましたが、ボルダリングはエンジニアであろうがなかろうが楽しいし、運動不足は解消できるし筋力はつくし痩せるしでいいところしか見当たりません。
一度体験してみることをおすすめします。

*1:「よいこ」とは「よいコミュニティ」の略で、メンバー同士のコミュニケーションの活性化や一体感の向上など、より良いコミュニティを作るために必要な各種施策を従業員発で企画し、実施していく制度です。

【ML Tech RPT. 】第5回 不均衡データ学習 (Learning from Imbalanced Data) を学ぶ (2)

f:id:ssatsuki040508:20181210005017p:plain
Sansan DSOC 研究員の吉村です. 気づけば入社から一年が経ち, 社会人二年目に突入しました. 今年もコツコツとブログを継続していこうと思っております.

さて, 本題に入っていきましょう.
今回は不均衡データ学習のアプローチの一つである Cost-Sensitive Learning についての話をします. 本記事は前回の記事の続編という立ち位置なので, 前回の記事をご覧になっていない方は, 是非下記の記事から読んでみてください.
buildersbox.corp-sansan.com

Notation

説明をする前に, 今回の記事で用いる変数についてこちらでまとめておきます.

 x_i  i番目の事例. データ点.
 y_i  i番目の事例に対応する真のラベル. 二値分類の場合は,  y_i \in \{-1, 1\}. 多クラス分類の場合は,  y\in\{1, 2, \cdots, M\}
 \hat{y}  i番目の事例のラベルの予測値.
 f 判別関数.  f \in \mathbb{R}
 L, \phi 損失関数.
 L_i  i番目の事例に対応する損失関数.
 C Cost Matrix.
 S 事例に対応するスコアベクトル.  S=(S_1, S_2, \cdots, S_M)\top.  S_kはラベル kに対応するスコア

Cost-Sensitive Learningの概要

Cost-Sensitive Learning (コスト考慮型学習) とは, 不均衡データ学習に対処するためのアプローチの一つで, 許容し難い誤りには大きなコストを, 許容できる誤りには小さなコストを課す損失関数でラベル予測をする方法です.

例えば, 製造した自動車の部品が不良品か良品かを機械学習を用いて判別するとき, 普通の分類問題としてモデルを学習する場合には, 実際は不良品のものを良品と識別することも, 実際は良品のものを不良品と識別することも同等の影響があるとみなします. つまり, このときの損失関数 L L= \sum_i \mathbb{I}[y_i \neq \hat{y}_i]のように表すことができます. ただし,  \mathbb{I}は指示関数を,  y_iは製品 iの真の品質(良品なら +1, 不良品なら -1), \hat{y}_iは製品 iの識別結果を表します. しかし, この問題は人によっては次のように考える人もいるかもしれません.

「製造しているのが自動車の部品であることから, もし不良品が出荷されると死亡事故につながる恐れがあるため絶対に不良品を良品とモデルで識別することは避けたい. しかし, 良品を不良品と識別することはその不良品を一つ製造するコストが無駄になるだけなのである程度は許容できる. 」

仮にこれを「不良品を良品と識別することは良品を不良品と識別することよりも n倍の影響がある」と判断したとすれば, このときの損失関数 L L=\sum_i (n \mathbb{I} [(y_i=-1) \land (\hat{y}_i=+1) ] + \mathbb{I}[(y_i=+1) \land (\hat{y}_i=-1)])と書くことができます.

このように各種誤りの影響度をコストとして与えることで考慮するのがCost-Sensitive Learningです. 各種誤りに対するコストを損失関数に反映させるための方法の一つとしては, Cost Matrix(コスト行列)を用いる方法があります.

続きを読む

try!Swiftで次の世代の開発者やプロダクトのことを考えた - Diversity in Developer -

こんにちは。Eight事業部でiOSエンジニアをしています アマゾネス です。

東京で開催された try!Swift というカンファレンスに参加して、すごく大事な話を聞いたので共有します。

f:id:zankief:20190325134930j:plain

try!Swiftとは?

try! SwiftはSwift言語での開発における最新の応用事例について集まる国際コミュニティです。このイベントは世界中から人々が集まり、Swiftのスキルを向上させるための、高度な知識やテクニックを共有し、協力しあうことを目的としています。

TOKYO - try! Swift Conference から引用

Swift は、2014年に apple から発表されたプログラミング言語で、主に iOS、MacOS のアプリケーションを作ることに使用されています。私はiOS版の Eight を実装するために、Swift を書いています。 


カンファレンスには、世界各国から900人を超えるエンジニアが集まりました。私は初めてこのような大規模なカンファレンスに参加したのですが、日本以外からの参加者の多さにとても驚きました。


登壇内容はどれも素晴らしく、知らないことだらけで、とても勉強になりました。登壇で使用された資料をオンラインで公開されている方が多いので、興味がある方は是非ググったり、Twitter等で検索してみてください。

Female YouTuber Mayuko Inoue

技術系のお話がほとんどかと思ったのですが、「Developerとして」というような技術から少し離れたお話もいくつかありました。
中でも、Mayuko Inoueさんがお話されていた「次へつなごう— Extending a hand to the next generation of Apple developers」がとても心に残ったので、その内容についてお話をしたいと思います。


まず、Mayukoさんについて。

iOSエンジニア📱 Youtubeでテック業界で働くエンジニアとしての生活について動画も作ってます。 🇺🇸生まれ、🇺🇸育ちの二世の関西弁訛りの日本人です。

helloMayuko_jp (@helloMayuko_jp) | Twitter より

現在は Netflix でiOSエンジニアとして働かれているそうです。
美人、語学堪能、おまけにプログラミングも一流なんて...とにかく凄すぎる。交流会で少しだけお話する機会があったのですが、とても気さくで、本当に素晴らしい方でした。

彼女自身の1日の様子を収めた動画↓がとても有名です。

www.youtube.com

開発者の多様性について

彼女は過去、エンジニアへの応募に人種的な多様性がないことを疑問に思い、自身の Twitter でモバイルエンジニア、特にiOSのエンジニアが少ないのは何故かと言うことを問いかけたそうです。

開発するためのPCが高い、モバイル端末が高い、など金銭面に関わることや、どうやってなればいいのかわからない、などのレスポンスがあったそうです。


私はラッキーな形でプログラミングの世界に入ったと思います。
1人プログラムを書いている友達がいたので、その人に教えてもらえたし、開発するためのMacを買えました。でも、世の中には様々な問題から、エンジニアの世界に入ることができない人がいるのです。


このような状況を鑑みると、今はまだ「開発者の世界は多様性に富んでいる!」とは言うことはできなさそうです。

次の世代の開発者のために今の開発者ができること

Diversity (多様性) が謳われる現代において、特定の人々で作るプロダクトよりも、様々な人で作った方がより良いプロダクトになるはずである、そのような状態を実現するために、Mayukoさんは以下の二つの提案をされていました。

- イメージを作る

自分のしたいこと、自分にしかできないこと、など自分自身が持つ多様性と向き合い、そこからどのようなことができるかイメージを作りましょう、ということかと、私は思います。具体的な例として「プログラミングを教える」「自分の知識を共有する」などがあげられていました。イメージをつくることによって、次の世代に繋げるための、行動が生まれるのではないでしょうか。

- あなたのことをしってもらおう

文字通り、開発者である自分のことを発信することによって、色んな人に開発者ってこんな感じなんだ! ということを知ってもらおうということ。知る、ということは、興味を持つきっかけにもなるし、開発者になりたい人への参考にもなりますので、間口が広がるでしょう。 

私が考えたストーリーと感じた事

既に開発者である私たちが行動する  → 色んな人に開発者になるチャンスが生まれる  →  開発者になる  →  様々なプロダクトが生まれる  → 沢山の人が喜ぶ!


開発者になる入り口を広げることにより、今、そして、次の世代では多様性豊かな開発者が沢山生まれ、その結果として、新しくそして素敵なプロダクトが世に出る。
これは一人一人が違う社会の中で、大事なことだと思います。


Mayukoさんのスピーチを聞いて、開発者として技術を高めるだけでなく、他にもやるべき事があることに気づかされました。弊社のエンジニアにもメンターとしてプログラミングを教えたりしている者がいますが、私はとても尊敬します。


また、これは開発者だけでなく、他の業種の人にも言えることなのではないかな、と思います。プロフェッショナルになることはもちろん大事なことですが、それだけでは良い製品は生まれないのかもしれません。


今回は、現在開発者である自分にできることの一つとして、この内容を寄稿させていただきました。


2019年のtry!Swiftに参加できて本当に良かったです。

f:id:zankief:20190325135001j:plain

補足:

Mayukoさんの登壇の様子は後日 try Swift! の YouTubeチャンネルでシェアされるそうです! また、登壇の内容を文章にしてくださっている方がいらっしゃいました! 本当に感謝です。シェアさせていただきます。

次へつなごう— Extending a hand to the next generation of Apple developers | try! Swift Tokyo 2019 2-16 - niwatakoのはてなブログ

 

チームにE2Eテストの文化を広めた話

こんにちは。Sansan 事業部プロダクト開発部の奥野です。

私は中途採用で昨年11月に入社して4ヶ月ほどになる、関西支店で勤務するエンジニアです。MAIDO という開発チーム*1 に所属しています。

今回の記事では、1月に私が主催した Selenium のチーム内勉強会を起点にして始まった、E2E テスト導入に向けた動きについてレポートします。なお、E2E テスト(End to End テスト、詳細は後述)の技術的側面については深掘りせず、新しい文化の導入に主題を置きます。

Seleniumとは

まず、勉強会の題材とした Selenium について簡単に紹介します。

Selenium とは、ブラウザを使用する E2E テストを自動化するテストフレームワークです。その歴史は2004年から始まっているため*2、ご存知の方も多いかと思います。

E2E テストは、システム全体を一通り動作させて行うテストであり、機能テストや結合テストといったテストフェーズで行われます。 Web アプリケーションの場合、ユーザがブラウザ操作を起点にして、クライアント処理・サーバ処理を通して画面再描画に至る過程を最初から最後までテストします。

E2E テストはユニットテストに比べると、ビジネスや要件の観点からのテストが可能であり効果的です。
一方で、E2E テストはシステム全体を動作させるため、テストの事前条件としてDBデータを揃えておく必要があるなどのセットアップが大変、テストに時間がかかるというデメリットがあります。

なぜ E2E テストをチームに導入しようと思ったか

Sansan はリリースしてから10年以上経過しているプロダクトであり、いわゆるレガシーシステムです。

仕様や設計が複雑化しており、仕様を把握するのも、テストをするのも大変です。少なからず技術的負債も蓄積しており、どのように向き合っていくかは常に問題になっています。また、大規模なリファクタリング、新しいアーキテクチャへの移行についても議論が盛んです。

一方で、MAIDO チームで開発している管理者向け機能では、テストコードによる保護、テスト自動化が積極的に行われていませんでした。個々のビジネスロジックに対するユニットテストはテストコード化されていましたが、機能テストレベルのテストコードは存在していませんでした。つまり、UI制御やDBアクセスを含めた機能仕様観点からテスト仕様は過去のテスト設計書から掘り起こさなければならず、テスト自動化されていませんでした。

そのような中で入社数ヶ月の身としては、プロダクト知識が不足しており、ソースコードを変更した際にそれがデグレを起こしていないか、不安で仕方がありませんでした。リグレッションテストをしようにも、何をテストしていいのかすら分からない始末でした。

「ここにテストコードがあれば、ある程度のリグレッションテストを行うことができるのに…」

私は前職で、Selenium を活用した E2E テストの自動化に取り組んでいました。それを主たる業務としていたわけではないですが、それなりに思い入れがあり、その導入効果も把握していたので、E2E テスト自動化の導入に向けて声を上げようと思った次第です。

*1:私が所属する関西支店の開発チーム MAIDO については、チームメンバーの加藤が紹介している記事がありますのでそちらをご覧ください。

*2:https://www.seleniumhq.org/docs/01_introducing_selenium.jsp#selenium-history

続きを読む

単語埋め込みを単語埋め込みに埋め込む -後編-

こんにちは、DSOC R&Dグループ インターン生の荒居です。 この記事は、単語埋め込みに単語埋め込みを埋め込む-前編-の続きの記事です。

前編では、ベースとなる 単語埋め込み になかった単語のベクトル表現を別の単語埋め込みから輸入することを実験的に行い定性的に評価をしました。 今回は同じ手法でベースとなる単語埋め込みを拡張し、文書分類のタスクを解かせてみることでその有効性の検証を行います。

振り返り

前回の記事では、2つの単語埋め込みの両方に含まれる単語のベクトル表現の対応関係を線形変換として表現し、その線形変換を片方の単語埋め込みにしか存在しない単語ベクトル表現にも適用することで、単語埋め込みの拡張を行いました。

f:id:koukyo1213:20190319152325p:plain
2つの単語埋め込みの積集合を用いて単語ベクトル空間の間の変換を取得する
f:id:koukyo1213:20190326110130p:plain
単語ベクトル表現を別の単語埋め込みから「輸入」する

続きを読む

単語埋め込みを単語埋め込みに埋め込む -前編-

こんにちは、DSOC R&Dグループ インターン生の荒居と申します。 今年の1月から自然言語処理のインターン生としてお世話になっています。

インターンでは文書分類のタスクを扱っていたのですが、単語埋め込み を用いるようなディープラーニングベースの手法において、しばしば単語埋め込みのボキャブラリに、扱う文書中の単語が含まれていないという問題(out of vocabulary, OOV)に行き当たりました。

本稿ではOOVとなる単語を減らすために複数の単語埋め込みを用いて単語埋め込みを拡張するという手法を考え、実験してみた結果を紹介させていただきます。

検証に用いたコードなどはGitHubにて公開しております。

github.com

OOVについて

OOVとなるような単語にはどのようなものがあるでしょうか?

OOVが発生するのは単語埋め込みを学習する際に、学習に用いたコーパス中に入っていなかった、あるいは出現頻度が低かった単語が存在するからです。したがって珍しい単語、一般的ではない単語がOOVを発生させやすいと言えます。また、学習させるコーパスが違うならばOOVとなる単語も変わるということもあり得るわけです。

具体例を考えてみましょう。ここでは、Wikipediaコーパスで学習したfastText(300d) と 求人データで学習したword2vec(200d) を学習済み単語埋め込みとして用いて、 Livedoorニュースコーパスを例に説明させていただきます。素晴らしいデータをありがとうございます。

Livedoorニュースコーパスにはユニークな単語は何語くらい入っているのでしょうか?

NElogd で分ち書きに直してカウントしてみたところ、およそ9万語のユニークな単語が含まれていることがわかりました。

# NEologdを使う
tagger = MeCab.Tagger(
    "-Ochasen -d /usr/local/lib/mecab/dic/mecab-ipadic-neologd/")

def nn_tokenizer(text):
    # 全角を半角へ
    text = mojimoji.zen_to_han(text.replace("\n", ""), kana=False)

    # URL除去
    text = re.sub(r'https?://[\w/:%#\$&\?\(\)~\.=\+\-…]+', "", text)
    parsed = tagger.parse(text).split("\n")
    parsed = [t.split("\t") for t in parsed]

    # 空文字とEOSを除く
    parsed = list(filter(lambda x: x[0] != "" and x[0] != "EOS", parsed))
    parsed = [p[2] for p in parsed]
    return parsed


loader = DataLoader("../input/text/")  # リポジトリを参照
loader.tokenize(nn_tokenizer)

data = loader.data
tk = keras.preprocessing.text.Tokenizer(lower=True, filters="")
tk.fit_on_texts(data.tokenized.map(lambda x: " ".join(x)))

word_idx = tk.word_index
print(len(word_idx))  # => 90866

続いて、Livedoorニュースコーパスに含まれる単語の中で、2つの学習済み単語埋め込みに含まれていない単語がないか調べてみましょう。

def load_embedding(path):
    binary = False
    if "bin" in path:
        binary = True
    emb = KeyedVectors.load_word2vec_format(path, binary=binary)
    return emb


stanby = load_embedding(
    "/path/to/where/you/put/stanby-jobs-200d-word2vector.bin")  # Bizreachの200d word2vec
wiki = load_embedding(
    "/path/to/where/you/put/model.vec")  # WikiコーパスのfastText

stanby_words = set(stanby.vocab.keys())
wiki_words = set(wiki.vocab.keys())
livedoor_words = set(word_idx.keys())

oov_stanby = livedoor_words - stanby_words
oov_wiki = livedoor_words - wiki_words

print(len(oov_stanby))  # => 48371
print(len(oov_wiki))  # => 30477

かなりの単語がOOVとなっていることがわかります。実際にはどのような単語が抜けているのでしょうか? 少し単語を眺めてみましょう。

print(oov_stanby)

# => {
#    'オリバー・ストーン', 'ナショナルスタジアム', '小川純', 
#    '19cm', '証人', '決意表明', '武家', '数え上げる', '富士通グループ', 
#    'ブロードバンドルータ', 'powert', '商用電源', '聖光学院中学校・高等学校',
#    ...}

print(oov_wiki)

# => {
#    ...
#    '倉敷市芸文館', '不倫は文化', '3杯目', '首根っこ', 'エコポンパ', 
#    'happiness!!!', 'tanita', 'ベイベ', 
#    'appbundle', 'viewカード', 'ヴォルフスブルグ', '狡辛い',
#    ...}

なかなか特徴的な単語が多いですね。fastText の方には入っていて word2vec には入っていない単語や、その逆の単語はどの程度あるのでしょうか?

in_stanby = oov_wiki - oov_stanby
in_wiki = oov_stanby - oov_wiki

print(len(in_stanby))  # => 4279
print(len(in_wiki))  # => 22173

今操作してきた単語集合を図で表すとこのようになります。

f:id:koukyo1213:20190319144350p:plain
単語集合の関係図

続きを読む

© Sansan, Inc.