mixi engineer blog

ミクシィ・グループで、実際に開発に携わっているエンジニア達が執筆している公式ブログです。様々なサービスの開発や運用を行っていく際に得た技術情報から採用情報まで、有益な情報を幅広く取り扱っています。

Char2Vec で文字の特性について調べてみた

ミクシィ Vantage スタジオのAI・ロボットチームで自然言語処理関連の研究開発に関わっている原(@toohsk)です.

Vantage スタジオでは人の感情に寄り添った会話ができるAIの研究開発を通じて,新しいコミュニケーションサービスを生み出そうとしています. 今回, Char2Vec を用いた,文字毎の特性について実験を行いましたので,紹介したいと思います.

Word2Vec とは

Word2Vec は単語をベクトル表現に変換する方法です. これまでは自然言語処理の分野では単語を扱う場合, one-hot の形式で文章内の単語を表現することが多かったです.
しかし,自然言語を機械学習で扱う場合や論文では,最近では必ずといっていいほど Embedding された状態,すなわち単語をベクトルに変換してから機械学習のアルゴリズムに与えています.
ではなぜ one-hot の形式ではなく, Embedding された状態に変換するのでしょうか? それは one-hot の形式では,単語同士の関係性や意味が考慮されていないためです. そこで単語同士の関係性や意味を学習するために Mikolov らにより, Word2Vec の手法が提案されました.
Word2Vec には大きく2つの手法があり, cbow と skipgram の2つが提案 1 されています. この2つの手法での大きな違いは, cbow は着目している単語の周辺語を入力に与え, skipgram は着目している単語を入力に与えるといったものがあります. これらの違いにより, cbow はたくさんの分布情報をなだらかにする効果があり,小さなデータセットでよく使われます. 一方, skipgram は着目する単語と文脈を新しい観点で扱うため,大きなデータセットに有効であると一般的に言われています.
また,Word2Vec 以降も GloVe 2 や FastText 3 というアルゴリズムが提案されています.

Char2Vecとは

Word2Vecを学習させる対象言語が英語の場合,各単語がスペースで区切られているため,スペースやピリオドなどで単語区切りのデータセットを作成します. 一方,日本語の場合は文章中に区切り文字がないため,一般的には分かち書きや形態素解析を行い,必要な品詞だけを抽出して学習させます. しかし,Word2Vec にはアルゴリズムの違いだけでなく,言語モデルに与えるデータセットの種類を変えた Doc2Vec や Paragraph2Vec, Char2Vec があります.
今回,私達は対話文を文字単位に分割し,文字のベクトル表現を学習させる Char2Vec に取り組みました.また,学習は Gensim4 の Word2Vec ライブラリを利用し, cbow として学習させました.

学習結果

学習した Char2Vec の結果を文字分布図とアナロジータスクの2つで見てみたいと思います. 今回の実験では,全・半角や数字,アルファベット,平仮名,カタカナ,漢字など3,731種類の文字を対象としています.

文字分布図

文字分布図はある文字に近い文字を可視化する分布図です.
今回は,'1','a','あ','悲'の4文字についてそれぞれに近い上位50個の文字を表示しました. すこしウォーリーを探せ感があるのですが,頑張って探してみてください.笑

まずは,数字をベクトル表現にした場合です.

f:id:mixi_engineers:20171005183859p:plain

数字が近くに集まっているのがわかるかと思います.図だとわかりにくいのですが,図の中に同じ数字が出てきています.その理由は全角と半角の違いによるものです. しかし,全角は全角同士,半角は半角同士で集まっているので,区別して学習していることが分かります.

次に,アルファベットをベクトル表現にした場合です.

f:id:mixi_engineers:20171005183920p:plain

アルファベットは単体で使われることもありますが,文章においてはアルファベット同士が組み合わさって意味が表現されます.したがって,アルファベットはアルファベットとして集まって表現されているのだと分かります.

そして,平仮名をベクトル表現にした場合です.

f:id:mixi_engineers:20171005183937p:plain

'あ' の近くには平仮名は集まっていないことがわかります.平仮名は単体では意味を表さず,文章中においては漢字と組み合わせて意味を表すので,分布図として漢字の中に紛れているのだと思われます.

最後に,漢字をベクトル表現にした場合です.

f:id:mixi_engineers:20171005183951p:plain

今回は'悲'という漢字を選択しました.目的として,感情や形容する言葉に使われる漢字なので,その近くには同様の漢字がある場合,漢字の意味をベクトルとして表現できているのではないかと考えられます.実際に図を見ると,'悲'という漢字の近くには,'楽'や'優','寂'など似た使われ方をする漢字が近くに集まることが分かりました.

アナロジータスク

アナロジータスクとは,単語の性質を演算として計算できるタスクのことです.
Word2Vec の魅力はたくさんありますが,単語をベクトル表現にできたことで演算が可能になったことは,その一つだと思います. このアナロジータスクの例としてよくあげられるのが, king - man + woman = queen というものです. 今回の Char2Vec でも同様のものができるか確認してみます.
Gensim でアナロジータスクを解く場合, most_similar メソッドを利用します. most_similar メソッドの引数に positive として与えているものは加算, negative として与えているものは減算するものです.
それでは,実験結果を見てみましょう.

まずは,数字でのアナロジータスクを見てみましょう. 今回の例として, 5 + 1 -2 を計算してみました.

model.most_similar(positive=["5", "1"],["2"]) 

[('4', 0.9669662117958069),
 ('3', 0.9577144980430603),
 ('6', 0.9576835036277771),
 ('7', 0.952031672000885),
 ('8', 0.9488403797149658),
 ('0', 0.9165981411933899),
 ('9', 0.8589234352111816),
 ('5', 0.8185327053070068),
 ('7', 0.7915964722633362),
 ('8', 0.7892774939537048)]

驚くことに,正しく計算されています.
数字の組み合わせということもあり,以下の条件でありうる440件分のデータセットを作成し,正答率がどの程度か計算しました.

  • "positive" と "negative" の中の数字は一致しない
  • Char2Vec のため "positive" と "negative" の計算結果は 0~9 までの値域とする
def create_analogy_sets():
    
    analogy_sets = []
    answer_sets = []
    
    for p1 in range(0, 10):
        for p2 in range(0, 10):
            for n1 in range(0, 10):
                
                # p1, p2, n1 が同じ数字になる場合 skip する
                if p1 == p2 or p1 == n1 or n1 == p2:
                    continue
                    
                # p1+p2-n1 が負の整数の場合 skip する
                answer = p1+p2-n1
                if answer < 0:
                    continue
                
                # p1+p2-n1 が10より大きい正の整数の場合 skip する
                if answer > 9:
                    continue
                    
                answer_sets.append(answer)
                analogy_sets.append([p1,p2,n1])
        
    return analogy_sets, answer_sets

def precision(analogy_sets, answer_sets, topn=1):
    correct = 0
    not_num = 0

    for i in range(len(analogy_sets)):
        p1, p2, n1 = analogy_sets[i]
        for predict in model.most_similar([str(p1), str(p2)], [str(n1)], topn=topn):
            try:
                if int(predict[0]) == answer_sets[i]:
                    correct += 1
            except:
                print(predict)
                not_num += 1
                continue

    return correct, not_num

モデルの戻り値を top1 と top3 の2パターンで評価しました.
まず,戻り値の種類についてですが, top1 と top3 の両方において,半角数字が返された率は100\%でした.
次に,計算結果についてですが, top1 での計算結果では,26.8\%の正答率となり, top3 での計算結果では,56.8\%の正答率となりました.
ちなみにですが,数字に限定した上でランダムに回答した場合, top1 では10\%, top3 では 1 - 9/10 * 8/9 * 7/8 = 0.3,すなわち30\%となるはずなので,ランダムで回答するよりも良い結果になっていることが分かります.
学習データが対話データなのでほとんど数字の関係性を教えられておらず, また,文字として数を与え,候補として3,000文字以上ある中から正解できていることに驚きを感じます.

ちなみに,漢数字の場合は,

model.most_similar_cosmul(["五", "一"],["二"]) 

[('四', 0.7392055988311768),
 ('染', 0.7125757932662964),
 ('1', 0.7093852758407593),
 ('先', 0.709115743637085),
 ('三', 0.7078965306282043),
 ('4', 0.706843912601471),
 ('3', 0.7037045955657959),
 ('6', 0.7036187052726746),
 ('九', 0.7021587491035461),
 ('敏', 0.6988415718078613)]

これも回答できています.近しいアナロジーとして漢数字や全角の数字が挙がっていることは驚くべき結果です.ちなみに先程の計算用のデータセットの場合,漢数字が返答されている確率は55.9\%,計算結果の正答率は 9.5\% でした.

最後に,漢字同士によるアナロジータスクです.
漢字は偏や旁などの部首とそれ以外の部分から構成されます. 今回は,その知識を活かしてアナロジータスクをしてみたいと思います.

model.most_similar(["木", "林"])

[('森', 0.6414894461631775),
 ('谷', 0.6174930334091187),
 ('雲', 0.6121721267700195),
 ('川', 0.6072961688041687),
 ('松', 0.6014201045036316),
 ('崎', 0.5943500995635986),
 ('熊', 0.5926244258880615),
 ('村', 0.5882901549339294),
 ('峰', 0.5817473530769348),
 ('冠', 0.5709230303764343)]
model.most_similar(["山", "石"])

[('岩', 0.6268459558486938),
 ('熊', 0.6220624446868896),
 ('松', 0.6124925017356873),
 ('芹', 0.610385000705719),
 ('藍', 0.5979625582695007),
 ('木', 0.5863777995109558),
 ('滝', 0.5861614346504211),
 ('韮', 0.5789919495582581),
 ('井', 0.5788655281066895),
 ('森', 0.5647031664848328)]

驚くことに漢字の構成を組み合わせると意図する漢字が返されました. しかし,必ずしもそうなるわけではありません.

model.most_similar(positive=["草", "楽"])

[('悲', 0.5380357503890991),
 ('憎', 0.47937360405921936),
 ('惜', 0.4682950973510742),
 ('苦', 0.4587554335594177),
 ('休', 0.44919806718826294),
 ('寂', 0.44706428050994873),
 ('忙', 0.4312727451324463),
 ('欲', 0.4239286184310913),
 ('鼻', 0.42341917753219604),
 ('嬉', 0.4128988981246948)]

残念なことに,'草'+'楽'='薬'を期待したのですが, '悲','憎','苦'などの漢字が抽出されました. (薬で辛い経験をしたニューラルネットワークなのでしょうか...)

まとめ

今回は, Char2Vec についてと学習させて得られた興味深い知見を紹介させていただきました.
全体として,概ね数字やアルファベットなどの種類や漢字が持つ意味はある程度近い距離にまとまっており,うまく Enbedding されているようでした. 今回のデータセットは,対話データをベースとしていましたが,データセットを変えることでまた異なる知見が得られるかもしれません.

最後に,弊社では一緒に人の感情に寄り添った会話ができる AI を創出したい自然言語処理 エンジニアを募集しています.ご応募お待ちしております.


新卒研修の受講レポート~git編~

はじめに

はじめまして、2017年新卒エンジニアの親川と玄馬です。 本記事では、git研修でおこなった内容や得た学びについて紹介したいと思います。

そもそもgitとは何なのか、という方は以下のサイトを参考にしてください。

Gitを使ったバージョン管理【Gitの基本】 | サルでもわかるGit入門 〜バージョン管理を使いこなそう〜 | どこでもプロジェクト管理バックログ

研修の様子

前半は、先輩社員による座学形式の研修でした。gitを楽しく学ぼう、ということで内容は

  • gitの使い方

  • commitとbranchについての解説

  • 歴史の取り込み方(merge, rebase)

となっていました。 研修で使用した資料は以下のページで見ることができます。

すごいGit楽しく学ぼう // Speaker Deck

gitの使い方ではリポジトリの作り方から、変更のステージング方法、commit・pushの操作、branchの操作を学びました。
操作だけでなく、commitやbranchはどういったデータ構造で表現されているかを知ることができ、この後に学ぶmerge操作の理解における大きな助けとなりました。
歴史の取り込み方の学習として、merge操作でのオプションによる動作の違いを学びました。
後半は演習として、gitを使う上で起こるトラブルを解決する「git challenge」の過去問に挑戦しました。問題は難易度別に分けて出題されるので、自分のレベルにあったものから解答することができました。
前半で学んだ操作を使いつつ、理解不足な情報は調べたり先輩社員や同期に質問して各自のペースで進めていきました。

f:id:mixi_engineers:20170828132541j:plain

ちなみに実際のgit challengeは、gitに関する問題にチームで挑戦し、時間内にいくつ解けるか!…という弊社主催の学生向けの技術イベントです。(第5回大会では4時間で18問に挑戦したそうです。)

問題の解き進め方

git challengeは、1問ごとに、以下のような流れで進めていきます。

  1. 問題のリポジトリをclone

  2. 問題の指示通りの状態になるようにリポジトリを修正

  3. push

すると、採点サーバで自動的に問題の正誤が判定され、全体のランキングページで結果を閲覧できるようになります。
「pushが出来ない」という同僚を助けるようなシチュエーションや、そもそもcloneすらできないリポジトリもあり、様々な問題に対して解決案を模索していきます。
最後まで解けないような難しい問題もありましたが、終了後に解説をしてもらえたので非常に勉強になりました。

ちなみに、git challengeの問題の解き方だけでなく、自動採点などのインフラの話が気になる方は、以下のブログを是非読んでみてください。

git challengeの自動採点高速化に向けたインフラのハナシ - mixi engineer blog

得た学び

今回の研修でcommitについての理解を深めることができました。ここでは、commitについて学んだことを紹介したいと思います。

皆さんはgitの仕組みについて、どのように理解しているでしょうか?
私は、「変更があった差分情報を時系列に保存し、いつでも過去の状況に戻れる」といった大まかな理解しかしていませんでした。
git研修が始まり、commitが持つ「Revision」という値が、gitの仕組みを理解する上で重要だということに気づきました。
commitの大まかなデータ構造は、以下の通りです。

commitのデータ構造

Revision commitのSHA-1ハッシュ
Tree ファイルのスナップショット
Parent ひとつ前のcommitのRevision
Author commitを作成した人
Committer commitを適用した人

Revisionとは、commitに対応する以下のようなハッシュ値です。
6fe9db43763ded8bbfd0b428894baa9bfc0b7d42
gitの操作をする上で、このRevisionは多くの場所で登場します。

commitのデータ構造の中にも、Parentという値にひとつ前のcommitのRevisionが入っています。
ひとつ前のcommit、そのひとつ前のcommit・・・と辿って行くことで、一番最初のcommitまで見ることができます。

branchのデータ構造にもRevisionが登場します。
私はbranchについて、枝を伸ばす・枝を分けるといったイメージを持っていました。しかしbranchの正体は、あるcommitのRevisionを指すポインタのようなものでした。
branchが指しているRevisionを新しいcommitのRevisionに変更することで、枝が伸びる・枝が分かれるような処理を実現しています。
意外と単純な仕組みだと思いませんか?これを知って、私はgitの仕組みの理解がしやすくなりました。

普段gitを使う際に、私はこれらのことをそれほど意識せずに使っていました。
しかし、データ構造や実際に行なわれる処理といった仕様を知ることで、branchやmergeについて曖昧だった部分も理解しやすくなります。
そういった理解ができていることで、gitで困った時の対応力が大きく改善されると実感しました。

おわりに

今まではgitのコマンドの機能や実行結果を把握している程度でしたが、改めて詳細な仕様を知り、実際に行われている処理を考えながらgitを使えるようになりました。
この記事を見て、gitに興味を持った方は、ぜひgit challengeにご参加下さい!
また、git challengeの概要や今までの様子などがまとまっている以下のページもぜひご覧ください。

mixi GROUP presents「git challenge」

問題の一部は以下に公開されていますので、興味がある学生の方は挑戦してみてはいかがでしょうか?
第1回git challengeの出題内容を一部公開します - mixi engineer blog

新卒研修の受講レポート~AWS編~

AWS研修

みなさま、はじめまして!ミクシィ2017年新卒エンジニアの田村と金銅です。

今回は5月10日から3日間に渡っておこなった新卒エンジニア対象のAWS研修について書きたいと思います。 研修はミクシィのオフィスにAWSの外部講師をお招きし3日間みっちりとAWSの基礎から、実例を踏まえた応用まで教わりました。

まず、「AWSとは?」という方もいらっしゃると思いますので軽く説明しますと、AWSはウェブサービス、モバイルアプリケーション、ゲームなど様々なサービスの基盤となるインフラです。従来、大規模なインターネットサービスを運営しようとするとサーバ用に専用のハードウェアを購入し、データセンターに場所を借りて設置しに行く必要がありました。しかし、このAWSはいわゆるクラウドサービスなので実際のハードウェアを意識せず開発者がアプリケーションの開発や運用に集中できるというメリットがあります。

研修を大きく分けると講義と実際に習ったことを構築する実践形式のラボ、そしてチームで行ったインフラ設計のディスカッションといった内容でした。

初日

講義では可用性について学びました。

アプリケーションのインフラでは可用性がとても重要です。可用性とはシステムがいかにダウンせずに利用できる状態を保てるかという指標で、これが高ければ高いほどインフラの質が良いことを示します。

これには、急激なアクセスの増加によって負荷が増大した際にも耐えられるスケーラブルなインフラ構成をとる必要があり、まさにクラウド型のサービスであるAWSはこのような要求にうってつけです。

初日のラボではAWSのサービス群の中からVPC, EC2, DynamoDB, S3, IAMを使用し、障害を見据えたスケーラブルなウェブアプリケーション環境を構築しました。

f:id:mixi_engineers:20170822142839j:plain

二日目

二日目は、すでに自社が運用しているインフラ環境を想定し、それをクラウド上に持って行くフォークリフトという作業をチームでディスカッションし、その内容をホワイトボードに書いて発表しました。

ここで同期や講師の上原さんから鋭いツッコミが入り、実運用を考えた上で改善すべき点を知ることができました。

また、講義ではLambdaを使ったイベント駆動のオートスケールするインフラ設計を学び、ラボでは実践でその構築方法を身につけました。これがクラウドの特徴であり、必要な時に必要な分だけの処理能力を利用することでコストを最低限に保つことができるのです。

最終日

ここまで二日間を通して可用性の重要さについて学びました。 例えば、災害でデータセンターに火災や停電が起こったらどうでしょうか。最終日の講義では、そのような滅多に起こらない場合についても冗長化を図り、サービスの提供を継続できる強固なインフラ設計を学びました。

AWSは世界中にリージョンと呼ばれる地理的に分離したデータセンターを持っており、東京リージョンで障害が発生した際には北カリフォルニアリージョンにあらかじめ作っている同じ構成のインフラに処理を引き継ぐことができます。

最後に行ったハンズオンでは、サーバレスというアーキテクチャに沿って掲示板のウェブアプリケーションを作りました。サーバレスといってもサーバが存在しないわけではなく、AWSに管理された(フルマネージドな)サービスを使うことで全くコードを書くことなくアプリケーションの実装ができる構成のことを指します。しかし、これにはデメリットもあり、フルマネージドサービスは容易に利用できる反面カスタマイズ性が低く、細かい要求には応えられないケースがあります。そのため講師の方もおっしゃっていましたが、バランスが重要なのです。 f:id:mixi_engineers:20170822142832j:plain

まとめ

今回AWS研修を通じて、インフラは障害が起こることを前提に設計することが大切だと感じました。また、急激な変化に耐えられるようにスケーラブルな構成をとることで絶やさずサービスを提供し続けることができ、事業者もユーザもハッピーになります。

以上、AWS研修三日間のレポートでした! f:id:mixi_engineers:20170822143337j:plain

新卒研修の受講レポート~データベース編~

17新卒エンジニアデータベース研修

今回は、XFLAG事業本部 SREグループの清水さん(@isaoshimizu)によるデータベース研修で学んだことについて、新卒エンジニアの左野と坂本がレポートしていきます。

f:id:mixi_engineers:20170824103326j:plain

↑研修中の様子です

研修内容

講義は以下の内容で進んでいきました! - MySQLの基本的な話 - データベースの基本的な話 - インデックス - 負荷対策 - 運用の話 - 演習

今回は研修内容については深く掘り下げませんが、研修を受けて得られた学びと感想について私たちが感じたことを書いていきます!

今年の新卒が使ったことのあるフレームワーク

1位. Ruby on Rails

2位. Sinatra

3位. FuelPHP

いきなり蛇足ですが、研修前に扱ったことがあるフレームワークについて事前にアンケートがありまして、Ruby on Railsが人気のようでした。ORMはActive Recordが人気でした。

照合順序と寿司とビールの話

前半は、MySQLを題材にデータベースの基礎的な部分について学習しました。ACID特性やトランザクション、正規化、インデックスなどです。聞いた話の中で印象に残ったのは、寿司ビール問題と言われているもので、例えばMySQLで設定しているUnicodeの照合順序によっては🍣や🍺などの絵文字が同一扱いになったりならなかったりする問題です。詳しくは以下の記事が参考になります。

MySQL と寿司ビール問題 - かみぽわーる http://blog.kamipo.net/entry/2015/03/23/093052

インデックスの話

わたくし坂本は、過去にMySQLを利用したサービスの開発やISUCONに参加したことがあるのですが、その際、速度向上を狙いインデックスを貼ることがありました。しかし実際にインデックスがどのように作用して速度向上するのか、ということは全く理解せずにインデックスを追加していました。 今回の研修では改めてインデックスの挙動について学ぶことが出来ました。 例えば、「関数や式、否定構文、LIKEではインデックスは効かない(LIKEの前方一致では効く)」「LIMIT OFFSETはとても遅いのでWHEREで絞り込むと良い」などの注意すべき点を教わりました。これまでは、こういった点を知らずにクエリを発行していましたが、今後はどのようなクエリを発行するのか、学んだことを踏まえたインデックス設計を行いたいと思いました。

演習してみた話

最後に、演習として実際に効率の良いクエリ発行ができるか、の確認をしました。 データセットとしては、 https://github.com/datacharmer/test_db 内の employees.sql を利用しました。

演習1

USE employees;
SELECT SQL_NO_CACHE * FROM employees WHERE hire_date = '1985-02-01' AND birth_date = '1963-08-02';

このクエリを実行すると全件スキャンがおこなわれ、検索に時間がかかります。 最も短い時間で検索が行われるように工夫してください。

まずは、このまま実行をするとどうなるのかを確認します。

mysql> SELECT SQL_NO_CACHE * FROM employees WHERE hire_date = '1985-02-01' AND birth_date = '1963-08-02';
+--------+------------+------------+-----------+--------+------------+
| emp_no | birth_date | first_name | last_name | gender | hire_date  |
+--------+------------+------------+-----------+--------+------------+
|  20539 | 1963-08-02 | Poorav     | Gecsei    | M      | 1985-02-01 |
+--------+------------+------------+-----------+--------+------------+
1 row in set (0.13 sec)

この時点で0.13secかかっており既に遅いということが分かります。実際にどの程度スキャンされているのかを explain を用いて確認します。

mysql> explain SELECT SQL_NO_CACHE * FROM employees WHERE hire_date = '1985-02-01' AND birth_date = '1963-08-02';
+----+-------------+-----------+------------+------+---------------+------+---------+------+--------+----------+-------------+
| id | select_type | table     | partitions | type | possible_keys | key  | key_len | ref  | rows   | filtered | Extra       |
+----+-------------+-----------+------------+------+---------------+------+---------+------+--------+----------+-------------+
|  1 | SIMPLE      | employees | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 299290 |     1.00 | Using where |
+----+-------------+-----------+------------+------+---------------+------+---------+------+--------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

rows の項目を見ると分かるように、30万件近いスキャンが走っていることが分かります。

この場合は、インデックスを追加することでスキャンする数を減らすことができます。 今回はインデックスを hire_date に追加します。

mysql> alter table employees add index hire_date_index(hire_date);
Query OK, 0 rows affected (0.48 sec)
Records: 0  Duplicates: 0  Warnings: 0

その後、同じクエリを実行すると以下のようになります。

mysql> SELECT SQL_NO_CACHE * FROM employees WHERE hire_date = '1985-02-01' AND birth_date = '1963-08-02';
+--------+------------+------------+-----------+--------+------------+
| emp_no | birth_date | first_name | last_name | gender | hire_date  |
+--------+------------+------------+-----------+--------+------------+
|  20539 | 1963-08-02 | Poorav     | Gecsei    | M      | 1985-02-01 |
+--------+------------+------------+-----------+--------+------------+
1 row in set (0.01 sec)

最初0.13secかかっていた時間が、0.01secまで短縮できました!explainでスキャン数も確認してみます。

mysql> explain SELECT SQL_NO_CACHE * FROM employees WHERE hire_date = '1985-02-01' AND birth_date = '1963-08-02';
+----+-------------+-----------+------------+------+-----------------+-----------------+---------+-------+------+----------+-------------+
| id | select_type | table     | partitions | type | possible_keys   | key             | key_len | ref   | rows | filtered | Extra       |
+----+-------------+-----------+------------+------+-----------------+-----------------+---------+-------+------+----------+-------------+
|  1 | SIMPLE      | employees | NULL       | ref  | hire_date_index | hire_date_index | 3       | const |   15 |    10.00 | Using where |
+----+-------------+-----------+------------+------+-----------------+-----------------+---------+-------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

30万件近くスキャンしていたのが、15件まで減らすことができました! key を見ると先程設定した hire_date_index が利用されていることも確認できました。

演習2

次の演習は以下のような課題です

USE employees;
SELECT SQL_NO_CACHE * FROM employees WHERE birth_date > '1959-01-01' ORDER BY hire_date LIMIT 5;

上のクエリを実行すると、ファイルソートが発生して検索速度が遅いことが分かります。ファイルソートが起きないように工夫をしてみましょう。

mysql> SELECT SQL_NO_CACHE * FROM employees WHERE birth_date > '1959-01-01' ORDER BY hire_date LIMIT 5;
+--------+------------+-------------+-----------+--------+------------+
| emp_no | birth_date | first_name  | last_name | gender | hire_date  |
+--------+------------+-------------+-----------+--------+------------+
| 111400 | 1959-11-09 | Arie        | Staelin   | M      | 1985-01-01 |
| 110725 | 1961-03-14 | Peternela   | Onuegbe   | F      | 1985-01-01 |
| 111035 | 1962-02-24 | Przemyslawa | Kaelbling | M      | 1985-01-01 |
| 110085 | 1959-10-28 | Ebru        | Alpin     | M      | 1985-01-01 |
|  87761 | 1960-08-19 | Shir        | Munck     | F      | 1985-02-01 |
+--------+------------+-------------+-----------+--------+------------+
5 rows in set (0.14 sec)

実行結果です。0.14 sec とクエリが遅いことが確認できました。EXPLAINを使ってクエリを確認してみましょう。

mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM employees WHERE birth_date > '1959-01-01' ORDER BY hire_date LIMIT 5;
+----+-------------+-----------+------------+------+---------------+------+---------+------+--------+----------+-----------------------------+
| id | select_type | table     | partitions | type | possible_keys | key  | key_len | ref  | rows   | filtered | Extra                       |
+----+-------------+-----------+------------+------+---------------+------+---------+------+--------+----------+-----------------------------+
|  1 | SIMPLE      | employees | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 299157 |    33.33 | Using where; Using filesort |
+----+-------------+-----------+------------+------+---------------+------+---------+------+--------+----------+-----------------------------+
1 row in set, 1 warning (0.00 sec)

ここで Extra という項目に注目してみましょう。Using filesort からファイルソートが発生していることが確認できます。これはテーブルにインデックスが存在しないために ORDER BY で指定されたソートを行うためのメモリ使用量が増加しており、一時的な記憶領域としてファイルを利用するためクエリが遅くなります。1つ目の演習と同じように hire_date にインデックスを追加してみましょう。

mysql> alter table employees add index index_name(hire_date);
Query OK, 0 rows affected (0.40 sec)
Records: 0  Duplicates: 0  Warnings: 0

再びクエリを実行してみます。

mysql> SELECT SQL_NO_CACHE * FROM employees WHERE birth_date > '1959-01-01' ORDER BY hire_date LIMIT 5;
+--------+------------+-------------+-----------+--------+------------+
| emp_no | birth_date | first_name  | last_name | gender | hire_date  |
+--------+------------+-------------+-----------+--------+------------+
| 110085 | 1959-10-28 | Ebru        | Alpin     | M      | 1985-01-01 |
| 110725 | 1961-03-14 | Peternela   | Onuegbe   | F      | 1985-01-01 |
| 111035 | 1962-02-24 | Przemyslawa | Kaelbling | M      | 1985-01-01 |
| 111400 | 1959-11-09 | Arie        | Staelin   | M      | 1985-01-01 |
|  20539 | 1963-08-02 | Poorav      | Gecsei    | M      | 1985-02-01 |
+--------+------------+-------------+-----------+--------+------------+
5 rows in set (0.00 sec)

0.00 sec まで改善できました。EXPLAINでファイルソートが起きていないか確認してみましょう。

mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM employees WHERE birth_date > '1959-01-01' ORDER BY hire_date LIMIT 5;
+----+-------------+-----------+------------+-------+---------------+------------+---------+------+------+----------+-------------+
| id | select_type | table     | partitions | type  | possible_keys | key        | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-----------+------------+-------+---------------+------------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | employees | NULL       | index | NULL          | index_name | 3       | NULL |    5 |    33.33 | Using where |
+----+-------------+-----------+------------+-------+---------------+------------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

Using filesort が消えてファイルソートが起きなくなったことを確認できました。

感想

坂本: 今までデータベースについていろいろなアンチパターンや高速化について勉強をしてきましたが、今回の研修を通じて体系的に学べたおかげで点と点が線になり、今後につながる学びが得られたと思います。今後は、業務で役に立つようにさらに勉強を重ねて自分の武器となるようにしていきたいです。

左野: 今回の研修ではデータベースについて知らないことばかりだと痛感しました。データベースの知識は業務で欠かせないものになると思うので、講義資料を読み込んで勉強していきます。

git challengeの自動採点高速化に向けたインフラのハナシ

git challengeのインフラを担当している2016年度新卒エンジニアの轟 (@tdrk18) と、2017年度新卒エンジニアでSREの見習いをやっております佐藤 (@jtwp470) です。今回は、git challengeという技術競技イベントの自動採点の高速化に向けたインフラのお話です。

続きを読む

伝承進化し続ける技術イベント: git challenge開催記念インタビュー

こんにちは、ミクシィグループ 人事部です。

今回で第6回目*1を迎える、競技型技術イベント「git challenge (ギット・チャレンジ)」開催にあたり、運営に携わるエンジニア社員の国分さんと轟さんにお話を聞いてきました。
過去5回の大会を振り返るとともに、git challengeへの想いを語っていただきます。 #mixi_git

 

f:id:mixi_engineers:20170727135539j:plain 

続きを読む

新卒研修受講レポート~テスト編~

はじめまして、17新卒エンジニアの村上と林です。

今回は「テスト」という「プログラムが正しく動作しているかチェックするためのプログラム」についての研修を受けたので、まとめていきたいと思います。

 

本研修の内容は以下のようなものでした。

・テスト/設計についての簡単な講義

・ペアプログラミングでの実習

  • 車窓からのTDD
  • 円の面積を求めるプログラムをTDD
  • emailアドレスの正誤判定プログラムをTDD

 

テストについての簡単な講義

 

テスト研修の目的

まず、なんでこんな事やるの?という事で、研修の目的について述べたいと思います。

研修の目的は「良い設計を覚える」、これだけです。

 

良い設計ってなんなの?

そこで、良い設計とは?となると思います。

疎結合?可読性?

色々あると思いますが

「経営的な要求・条件に応えられること」です!

経営判断としてスピードが求められる場合には可読性よりも開発スピードを優先する場合もあります。良い設計に重要なものはその時の状況によって変わるということです。

 

しかしながら、突然のメンバー変更や仕様変更などが往々にして発生し、保守しづらいコードでは何をしているのかよく分からなくなるため、結果的に保守しやすい設計の方が(引き継ぎなども含めて)開発スピードが早くなります。

なので今回の研修では「保守のしやすさ」に焦点を当てます。

 

いい(保守しやすい)設計方法

保守しやすい設計方法として、テスト駆動開発(test-driven development:TDD)を利用します。この手法では、大きく3段階の手順を踏みます。

  1. テストを書く
  2. テストを失敗させる
  3. 実装してテストを成功させる

この3つの手順を実施することにより保守しやすい設計になります。


TDDのメリット

メリットは以下の3点です。

・テストを先に書くので、テストしづらいコードを書けない

・テストしやすいコードは疎結合で副作用もなく分岐も少ない

・この作業をやっていくと自然に保守しやすい設計になる

 

ペアプログラミングでの実習

ここまでの講義内容をペアプロで実践しました!

(※ペアプログラミング: 2人でプログラミングすること。常に片方がレビューをやっているので様々なメリットがある)

f:id:mixi_engineers:20170510162530j:plain

車窓からのTDD

「車窓からのTDD」はweb上に公開されており、内容としても簡単なのでTDDを体験してみたい方はリンク先を確認してみてください!

http://objectclub.jp/technicaldoc/testing/stack_tdd.pdf

 

「車窓からのTDD」には対話形式でTDDのやり方が書かれており、手順通りに進めていくだけで良いため、手始めのTDDの理解にとても役立ちました。

 

「車窓からのTDD」はスタックを構築するだけの簡単な内容で、上記したTDDの流れの通り、テスト作成し失敗、実装、再度テストを実行させ成功、のサイクルで行います。これはTDDでは基本の流れになります。今回の研修では、各自経験のある言語(RubyやPythonなど)で作業を進めていきましたが、どの言語でも基本の流れを抑えれば大丈夫です。

 

テストを書いてから実装をするので、実装の目的が明確になることで無駄なコードが減り綺麗なコードになると感じました。


円の面積を求めるプログラムをTDD

この課題では、「標準入力経由で円の半径が渡されるので、円の面積を求め四捨五入して整数に丸める」というプログラムを書いていきました。

入力が外から入ってくるため入力値に対するテストが必要になります。

この課題の採点が一回しか行われないという条件だったため、絶対に正解するように多くのテストを書きました。

(テストケースをたくさん書いて置くことで途中でコードの間違いを素早く見つけることができました。間違いなく実装するという点においてもすごくテストが役に立ち、重要なんだと実感しました!)

 

また、今回の課題でのポイントは標準入力でデータが渡ってくるという点です。テスト(今回の場合ユニットテスト)はロジックだけをテストしたいので依存性がないものが理想です。標準入力に依存しないように、スタブやモックオブジェクトで切り離す方が良いでしょう。

 

emailアドレスの正誤判定プログラムをTDD

「emailアドレスが"RFC5312 addr-spec"の部分的な仕様として正当なものであればok、違えばng」のテストを作成しました。

今回の場合、大量のテストケース(ngになるメールアドレス、okなメールアドレス)が必要になります。

なので、どのテストケースで失敗したかを分かりやすくする工夫が必要でした。

 

テストの速度は上げつつ、どこで失敗したかを判別したいのですが様々な方法があるようです。Rubyの黒魔術的な方法もあるようですが、後日調べたところこのような書き方もあるようです。

qiita.com

 (テスト書いててどんなテストケースあるかなってなったとき、キレイに書いてると「このケースもあったな」となりやすいので、保守性と速度が確保されると感じました)

 

まとめ

今回の研修ではテストの重要性について学びました。

保守しやすい設計で、引き継ぎの方や後輩達に、先輩はいいコード書くやろ?ってドヤ顔しましょう!!!!