機械学習でセーフサーチを高品質化するための試行錯誤

[Supership株式会社 Advent Calendar 2018](https://qiita.com/advent-calendar/2018/supership) の17日目の記事になります。

Supership株式会社の中野です。業務では[サイト内検索ソリューション S4](https://www.s4p.jp/)の開発・運用をしています。
今日はS4の機能の一部である「セーフサーチ」を機械学習で改善すべく試行錯誤した話を書こうと思います。

# セーフサーチ

### セーフサーチとは

"エロ排除検索" です。

S4では一部のサイト向けに、"性的表現や暴力表現等を含む一部コンテンツの表示を制限する機能" を提供しています。今回題材とするサービスには暴力表現がほとんどなく、実質的にアダルトコンテンツの検知と表示制限を行う機能となっています。

### 実現方法

当初は入稿される全てのコンテンツにサイト運用者(人間)が目を通し、表示制限対象にアダルトフラグを付与していました。
ただ、この方法では時間と人手がかかりすぎてチェックが全く間に合いません。そのため、某クラウドサービスの画像認識APIを使い、コンテンツのサムネイル画像でアダルト判定を行う方法に変更されました。

### 課題

サイトと画像認識APIの判断基準が違うためか、判定結果が期待と違うことが多々ありました。また、画像だけを見て判定しているため、アダルト感の強い文章を非表示にできません。そのため、セーフサーチ有効時にもアダルトコンテンツを完全に除去できないことがありました。
絶対にアダルトコンテンツを表示してはいけない面についてはヒューリスティックな絞り込みを追加して強引にアダルトコンテンツを除去しており、巻き添えで非表示になるコンテンツが多数発生して表示件数が極端に少なくなってしまっていました。

# 改善を試みる

## 目標設定

画像・文章を個別にアダルト/非アダルトに2値分類したとき、人間と分類結果が一致するアプリケーションを作ることを目指しました。データを眺めると人間でも判断に迷うコンテンツがあり100%正解は無理だと分かったので、正解が明らかなコンテンツを確実に判定できることを期待して

* 画像: 正解率(accuracy)9割 => 間違い率10%
* 文章: 正解率95% => 間違い率5%

を目標としました。

実際にはprecision、recall、間違え方の内容、学習速度、推論速度も評価していたのですが、紙幅の都合で主にaccuracyについて書きます。また、コンテンツの著作権を持っていないため、具体例を出せないことをご了承ください。

## 達成プラン

とにかく教師データが必要です。これは人間が手で作るしかありません。データさえ作ってしまえば後は簡単。
画像判定はCNNで解決するでしょう。多数公開されている素晴らしいモデル達を使えばOK。
文章は単語で概ね判定できそうなので、分かち書きしてTF-IDFで特徴量化してSVMで分類できるでしょう。

...サクッと終わらせるつもりでした。舐めてました。

## 教師データ作成

全データからランダムに取得した4000件(根拠はない)を目で見て アダルト/非アダルト に分類し、分類結果を学習用と評価用に分割して使用しました。

元々は最初に4000件のデータを作り、そこで培った判断基準や作業手順を元にデータを増やしていくつもりでした。実際には最初の4000件で私の精神は限界を迎え、データを追加することはありませんでした。というのも

* 判断に迷うデータが多い
    - ちょいエロ、お色気狙いのコンテンツが多数
    - 普段接する機会のない分野につては、エロスを感じるのかどうかの判断が難しい。特に文章
* 途中で判断基準の修正が発生する
    - 過去に分類済みのデータもやり直しになる。何度も何度もやり直した
* 周りの視線が気になる
    - エロ画像、エロテキストを画面に多数映し、真剣な眼差しで見続ける作業
    - 客観的に見て、会社でひたすらエロ画像を探してるヤバイやつ
    - この時私は試用期間中

で、分類が終わった時点で疲労困憊だったためです。

(本来はもっとデータを増やすべきであることは重々承知しております)

## 画像アダルト判定

CNN一択。教師データは作った。あとはディープラーニングってやつがなんとかしてくれるはず!!
<img width="798" alt="img-p1.png" src="https://qiita-image-store.s3.amazonaws.com/0/208309/a09c3017-8389-50a0-74dd-381e29fb5bb6.png">
ダメじゃん!!

後発のモデルほど精度が高いことに技術進歩の恩恵を感じられますね。CNNの実装にはchainerを使用したのですが、当時は`Inception-V4`と`Inception-ResNet-V2`のchainer実装が見つからず論文を読みながら再現実装しました(実装間違ってるかも...)。精度が高いので時間をかけた甲斐はあったはあったと思います。目標には全然届いてないけど。

ImageNet用に作られたモデルは表現力が高く今回の問題では過学習が激しい様子が伺えたので、パラメータを削った縮小版モデルを作って再挑戦することにしました。また、軽量なモデルを内製して検証対象に加えてみました。
<img width="799" alt="img-p2.png" src="https://qiita-image-store.s3.amazonaws.com/0/208309/be0d4233-f815-1157-f12f-9e5042ff14bc.png">
パタメータを減らしたほうが精度が高くなる様子が伺えました。軽量モデルの性能も悪くありません。
パラメータ減で処理時間を減らせたため、複数のモデルでアンサンブルで判定できるようになり...
<img width="799" alt="img-p3.png" src="https://qiita-image-store.s3.amazonaws.com/0/208309/53795984-fb15-03c8-7ca3-91f19c6afb9f.png">
なんとか目標の精度を達成できました。

この後、[Google AutoML Vision](https://cloud.google.com/vision/)が登場したりImageNetで学習済モデルのfine tuningを試したりしましたが、今のところ軽量モデルのアンサンブルが目標性能を達成できる唯一の手段です。
<img width="799" alt="img-p4.png" src="https://qiita-image-store.s3.amazonaws.com/0/208309/d025e440-376f-e1de-ee5c-487d8842b7a2.png">
苦労して作ったアンサンブルモデルに近い性能をAutoMLは15分でサクッ出してきました。すごい(小並感)。すごいけど、サーバがアメリカにあってリクエストの度に2秒待たされるのが辛いので東京か大阪のリージョンに来るまで待とうと思っています。
AutoMLとアンサンブルモデルの間違いは非常に似通っていました。恐らく間違いを誘発するような教師データがあり、これ以上の精度を求めるなら教師データを追加・修正するしかないのでしょう。

## 文章アダルト判定

### 候補とした手法

下記の2つの理由から、形態素解析による分かち書きありきで検討しています。

* S4では検索結果を高品質化するために分かち書きや類義語の辞書を内製している。この辞書の強みを活かしたい
* 人間から見て解釈性が高そう

#### 0. 事前準備(共通)

1. サイト内で検索対象としている文書全てを使ってコーパスを作成
2. 形態素解析器で分かち書き
3. ルールベースでトークンや文字列の結合・分離・正規化

#### 1. 文章を直接ベクトル化し、分類器に与える手法

1. コーパス全体を基にTF-IDFモデルとLSIモデルを学習させ、文章を固定長のベクトルに変換するモデルを作る
2. 教師データの文章をベクトルに変換
3. 文章ベクトルを分類するようSVM等の分類器を学習させる

TF-IDFとLSIにはgensimを、SVMにはscikit-learnを使用しました。
この手法で目標精度を達成できるつもりで検証を始めたのですが、チューニングを重ねても目標値に届く気配がなく、別の手法を検討することになりました。

#### 2. 文章を単語単位でベクトル化し、単語ベクトルから文章のベクトルを作って分類機に与える手法

1. コーパス全体からword2vecモデルを学習させる
2. 教師データの文章を単語ベクトルの可変長のリストに変換
3. 単語ベクトルの重み付き平均を文章のベクトルとする
4. 文章ベクトルを分類するようSVM等の分類器を学習させる

word2vecにはgensimを使用しました。
単語ベクトルの重み付き平均は、単純平均とTF-IDF加重平均の2種類を試しました。

#### 3. 文章を単語単位でベクトル化し、RNNに与える手法

1. コーパス全体からword2vecモデルを学習させる
2. 教師データの文章を単語ベクトルの可変長のリストに変換
3. ベクトルのリストを分類するようLSTMを学習させる

LSTMの実装にはchainerを使用しました。

#### 4. その他

本命ではありませんでしたが、興味本位で以下の方法も試しました。

* gensim.models.doc2vec => SVM
* 単語ベクトルのリストを0埋めで固定長のマトリックスに変換しCNN

### 結果

多層LSTMをアンサンブル学習させたモデルを採用しました。最も精度が高かったことに加え、単語の前後関係を取り扱えないと正しい判定が難しい文章が少数ながら存在し、LSTMだけが正しく判定できたことが決め手となりました。(本来はword2vec + CNNもできるはずなのですが)
<img width="815" alt="txt-1.png" src="https://qiita-image-store.s3.amazonaws.com/0/208309/8646ce8c-9210-d9f0-4cc0-04165a345cef.png">
悔しいことに当初目標としていた精度には到達していません。GloVeとか[AutoML Natural Language](https://cloud.google.com/natural-language/)とかBERTとかAttentionとか色々聞こえてきますがキャッチアップできていないので、勉強した後再挑戦しようと思っています。

### 不採用になった手法

精度改善に向けて様々な変更を加えましたが、ほとんどは精度改善につながらず採用されませんでした。

* 分かち書き
    - 辞書をmecab-ipadic-NEologdに変更する
* 単語ベクトル化
    - 公開されたWord2Vecの学習済みモデルを使う
    - FastTextで単語をベクトル化
* RNN
    - LSTMをGRUに変更
    - 双方向LSTM
    - 前方向LSTMと後方向LSTMを別々に学習させてアンサンブル

etc.

# 商用環境への投入

### 使用方法

`絶対にアダルトコンテンツを表示してはいけない面` でヒューリスティックな絞り込みを外すために使いました。
以前よりは正確に判定できるとはいえ精度は100%からは程遠いため、某クラウド画像API・内製の画像判定器・内製の文章判定器の全てが非アダルトと判定したコンテンツだけを表示するようにしました。それでもチェックをすり抜けてしまうコンテンツが数件あったため、軽めの絞り込みはかけています。

### 結果

まず、強引な絞り込みで減っていた表示件数が大幅に向上(というか正常化)しました。
<img width="787" alt="imp.png" src="https://qiita-image-store.s3.amazonaws.com/0/208309/ee103a80-9041-ce1a-8ecc-73111772dfba.png">
豊富なコンテンツからクエリにマッチしたコンテンツを出せるようになったため、CTRも向上しました。
<img width="787" alt="ctr.png" src="https://qiita-image-store.s3.amazonaws.com/0/208309/09efaf43-01fd-3bf5-4fca-97e380b575fd.png">
結果、コンテンツのクリック数が10倍に🎉
<img width="787" alt="click.png" src="https://qiita-image-store.s3.amazonaws.com/0/208309/b20f9a28-3830-83d0-68a9-20048b459349.png">
判定バッチは既存バッチサーバの空き時間に動作させており、運用に関する追加コストはデータ用に増やしたディスク容量分くらいです。アダルトコンテンツが出ているという指摘もなく、順調に稼働を続けています。やったね😆

開発工数? 人件費? 費用対効果? あーあーあー聞こえなーい

# おわりに

私の見積もりが甘いのはいつものことですが、今回は本当に甘かったです。こんなに手間暇がかかるとは想像できておらず、「期待値を最初に上げすぎないこと」という重要な教訓を得ました。試行錯誤を重ねたことで技術的には勉強になりました。得たノウハウを他の案件に転用していきたいと思っています。
おそらく今回の内容は技術的にマズい部分が多々あるでしょう。お手柔らかにご指摘いただけると助かります。