Transformerに自分の好みのDLsite音声作品を学習させて、癖に刺さる新作を毎日通知するシステムを作った話

# 作ったもの
**DLsiteの新作音声作品をクローリング -> 好みかどうか推論 -> 好みならSlack通知**をするシステムを完全サーバーレス（AWS SAM）で構築しました。さらなる精度向上のため、Slackメッセージのボタンをもとに教師データを蓄積する処理も作りました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119757/792e01f9-28f1-f742-bac0-266789cdb919.png)

https://www.dlsite.com/home/

## デモ（ぼかしMAX）
とてもわかりにくいですが、好みであろう作品がPOSTされているSlackの画面です。各メッセージについている「興味あり！」「別に…」ボタンを押すとLambdaが起動し、DynamoDBに新たな教師データとして保存されます。
![demo2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119757/cbffaa65-d8fa-5f2a-2c89-252121625778.gif)


# なぜ作ったのか

- DLsiteが好き、以上。

・・・

- もう少し真面目に書くと、
    - 会社でテキストデータに触れることが多いので、うまく扱えるようになりたい
    - 音声作品はシチュエーションや台詞の一部が説明文に書かれていることが多く、自然言語処理との相性が良さそう
    - AWSや機械学習の勉強はしてるけど、Hello Worldやタイタニックではなくダイレクトに自分の生活に役立つものを作りたい（そのほうが身につくものも多そう）

という背景です。

ちなみに、とある作品（全年齢対象）の説明文には下のようなテキストが書かれています。個人的には主にこういう情報をもとに買うかどうかを判断しているので、今回のモデルの教師データにはこのあたりのテキストを使いました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119757/cd7224ed-6ea4-c59f-5142-e58451021a01.png)

https://www.dlsite.com/home/work/=/product_id/RJ437205.html

# やったこと
- DLsiteの音声作品のうち、比較的新しい約1600作品に対して、**自分の好み or NOT**をアノテーションした
- それを[Hugging Face AutoTrain](https://huggingface.co/autotrain)で学習させ、**Accuracy: 0.90, F-score: 0.82**の精度で好みの作品を当ててくれる機械学習モデルを作った
    - 数日運用してみた感想：「ふーん、お前わかってんじゃん」という作品を出してくることが多くて優秀
- 4つのLambda関数を実装し、**新作をクローリング -> 推論 -> 好みの作品をSlack通知 -> 新たな教師データ蓄積**をするシステムを完全サーバーレスで構築した
- ほぼAWS無料枠の範囲内なので、コストは**約50円/月**（Lambdaコンテナ用のイメージを保存するECRのストレージ代＋ほんの少しだけS3）

それぞれ順に書いていきます。


# アノテーション、教師データ作成
## アノテーション
好みの要素がまったくない作品が大量に含まれるのは訓練データとして良くない気がしたので、特定のキーワードで検索して出てきた作品＋過去に自分が買ったor閲覧した作品を対象にしました。
また効率的にアノテーションするために、Seleniumと`input()`を使って、ある作品をアノテーションしたら自動的に次の作品ページに遷移するようなスクリプトを書きました。

実際にアノテーションしてる様子がこちらで、ターミナルに`0（好みじゃない） or 1（好み）`を入力すると自動的に次のページにいくようになっています。
![annotation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119757/d9d63396-f39e-1779-363b-0659dd5dbcad.gif)

およそ1週間アノテーションした結果、約1600作品（うち約25%が好み）のデータが集まりました。

## 教師データ作成
Dlsiteの音声作品には、説明文だけでなく作品タイトルや声優など、様々なテキスト項目があります。そのうち今回は、以下の項目を単純に結合したものを入力テキストとしました。

- タイトル
- サークル名（販売している団体名）
- シリーズ名
- 声優
- 作者
- シナリオライター
- イラストレーター
- ジャンル
- 説明文（の一部）
    - HTML上ではいくつかの`div`要素に分かれているが、そのうち特定のキーワード群（~~いろんなえっちワード~~）が最も多く含まれている要素だけを抽出して教師データに入れた
    - 全文を入れたときと比べて、Accuracyがざっくり0.8 -> 0.9に上がった

この入力テキストに対して、好みかどうかの`0 or 1`を付けたものを教師データとしました。


# Hugging Face AutoTrainで学習
https://huggingface.co/autotrain

これに関してはGUIにデータをドラッグ＆ドロップするだけなのと、[別で記事を書いている](https://qiita.com/oddgai/items/0d86e13014f898b6f811)ので省略します。
もともとの想定では、AutoTrainでベースラインモデルを作ったうえで、それを超えるように自分でもモデルを組むつもりでしたが、ベースラインの時点で`Accuracy: 0.90, F-score: 0.82`と想像以上の精度が出たので、ありがたくAutoTrainでできたモデルを使うことにしました。

現状だと訓練はWEBの画面からしかできないので、「自動で新しいデータを食わせてモデルを更新する」のようなことはできないのですが、もしAPIが提供されたら簡易的なMLOpsが組めそうです。

# Lambdaの実装
4つの処理を実装したので、順に説明します。一部は別で記事を書いてるので、興味があれば読んでみてください。


1. 定期的にDLsiteをクローリングする
1. 作品情報を入力すると、機械学習モデルをもとに好みかどうかを返す
1. 新着作品を検知し、好みと推論された作品をSlackに通知する
1. Slackメッセージ上のボタンをもとに教師データをDBに保存する

## クローリング
これはこちらに書きました。

https://qiita.com/oddgai/items/2a62b978e8c35488cfed

今回は「音声作品を発売日順に100作品クローリングする」という処理にしていて、十分なアクセス間隔を空けても5分くらいで処理が終わります。
処理が終わってクローリングデータがS3にアップされると、それをトリガーとして2つ先の「新着作品を検知し、好みの作品をSlack通知する」Lambdaが動きます。

## 推論
これもこちらに書いています。

https://qiita.com/oddgai/items/0d86e13014f898b6f811

20行くらいの推論処理を書いて、`sam init && sam build && sam deploy`するだけで推論APIができてしまうのは、本当にHuggingFaceとAWS様々だなと思いました。

## 新着作品を検知し、好みの作品をSlack通知

ここは単純に、下のように実装しました。

1. S3から当日と前日のクローリングデータ（json）を取得
1. 前日になくて当日にある作品を抽出
1. それらすべて（多くても30作品くらい？）を推論Lambdaに投げ、`好み`とみなされた作品はSlackに通知する
    1. その際、本当に好みかどうかをフィードバックするためのボタンを付ける（やり方は次の記事を参照）

一番苦戦したのはFB用のボタンを付けるところで、詳しくは下の記事に書きました。

## ボタンのFBをもとに教師データをDBに保存

Slackの実装部分はこちらに書きました。

https://qiita.com/oddgai/items/67da0321635e902b10f8

DynamoDBに保存する部分は、簡単ですがこのように書いています。

```python
from datetime import datetime
from zoneinfo import ZoneInfo

import boto3

JST = ZoneInfo("Asia/Tokyo")

ddb = boto3.resource("dynamodb")
table = ddb.Table("テーブル名")

table.put_item(
    Item={
        "item_id": item_id,
        "created_at": f"{datetime.now(JST):%Y-%m-%d %H:%M:%S}",
        "is_interested": is_interested,  # 好みかどうかの0 or 1
    }
)
```

なお細かい部分だと、間違えて違うボタンを押してしまったときのために、再びボタンを押すとその作品の教師データが上書きされるようにしました（パーティションキーを`item_id`（作品ID）にしているので、何もしなくても同じキーに対する`put_item()`は上書きされますが）。

以上の4つを組み合わせると、毎日自動で作品がおすすめされる世界になります。

# まとめ
**AWSとHugging Faceはすごい！**

1ヶ月くらい前にパッと思いついたものを、モチベーションを失わずそのままの勢いで作ることができました。数年前だったらこんなに速く・低コストで作れるものではなかったと思うので、AWSとHugging Face（とPytorch）のエコシステムに感謝です。

前にこういうものも作ったんですが、それと根っこの思想は変わらず、技術的には順当に進歩したものが作れた気がするので個人的にはとても満足しています。

https://qiita.com/oddgai/items/b37b98b6721c3a375fac