Amazon Web Services ブログ

Amazon のレビューデータでランキング学習を学んでみた – SageMaker Studio Lab

Update

2023/09/07 Amazon Review Datasetは現在公開を停止しています。実際に利用される場合は他の他のデータセットをご用意ください。

Amazon や AWS が機械学習のインフラだけでなくデータも提供していることはご存じでしょうか。実は Amazon の商品レビューデータも Amazon が一般に公開しています。またそれらのデータは多くの人に使われており、先日 NLP 若手の会にも提供させていただきました。このブログでは NLP 若手の会で使われたベースライン実装を見ながら、ランキング学習の実装方法を無料で使える Amazon Sagemaker Studio Lab で学んでいこうと思います。

はじめに

こんにちは!AWS Japan ソリューションアーキテクトの関谷侑希です。皆さんはどんなデータを使って機械学習の勉強をしていますか?

駆け出しの初心者機械学習エンジニアの方の中には、「そもそもどこにデータがあるのかわからない」「MNIST ではなくもっとリアルな生のデータを使いたい」「実用的なタスクに取り組みたい」と感じている方もいらっしゃるのではないでしょうか?

実は AWS が提供しているのはインフラだけではありません。Registry of Open Data on AWSでは様々なデータセットを公開しています。例えば COVID-19 に関連するデータや人工衛星から撮影された高解像度の画像などがあります。また Data Exchange を使えばデータの売買も可能です。さらに Amazon Customer Reviews Dataset では EC サイトの Amazon のレビューデータを使うことが出来ます。

そして実は NLP 若手の会2022 のハッカソンでもこのデータが使われました。NLP 若手の会は自然言語処理関連の若手研究者を対象とした研究シンポジウムです。

NLP 若手の会は,自然言語処理,計算言語学および関連分野の,若手研究者および技術者の学問研究および技術開発の促進をはかり,参加者の相互交流および成長の場を提供し,培われた学問研究および技術開発の成果が実社会に応用されることを奨励し,この分野の学問および産業の進歩発展に貢献することを目的として,年に1度,研究シンポジウムを開催しています.
https://yans.anlp.jp/entry/yans2022 より

NLP 若手の会2022 では研究発表だけでなく、実際に参加者が手を動かすハッカソンも行っています。今回のテーマは Amazon の商品レビューのランキングをするというものでした。仮想的なものではなく実際のデータを対象にしている点、ランキングという問題設定などからも分かる通りかなり興味深いものです。またベースラインとして、サンプルの実装があるためこのブログの読者のみなさんも体験し学んで頂くことが出来ます。webで検索してもこういった問題設定はまだそう多くなく、このサンプル実装そのものがかなり貴重な資料だと思います。
そこで、このブログでは NLP若手の会2022 で使われたデータセットとベースラインのコードをなぞって、データセットの使い方を勉強していきます。

またそのハッカソンでは Amazon として紹介した「実際のApplied Scientistならどう解くか」というお話も合わせて紹介しますので、ぜひ最後までご覧下さい。

データセット

ご存知の方も多いかと思いますが、AmazonではECサイトを運用しています。Amazon ではお客様の声を大事にしており、商品レビューは重要な機能の1つです。そして今回扱うのは、Amazon の商品ページのレビューをもとに作られたデータセットです。また具体的には、次のような情報が含まれています。このうち review_body を主に使います。

列名 詳細
marketplace レビューが書かれた市場の 2 文字の国コード。
customer_id 1 人の著者が書いたレビューを集約するために使用できるランダムな識別子。
review_id レビューの一意の ID。
product_id レビューに関連する一意の製品 ID。多言語データセットでは、レビュー、異なる国で同じ製品を同じ product_id でグループ化できます。
product_parent 同じ製品のレビューを集計するために使用できるランダムな識別子。
product_title 商品のタイトル。
product_category レビューのグループ化に使用できる広範な製品カテゴリ (データセットを一貫した部分にグループ化するためにも使用されます)。
star_rating レビューの星 1 ~ 5 の評価。
useful_votes 有益な投票の数。
total_votes レビューが受け取った総投票数。
vine レビューは Vine プログラムの一部として書かれました。
verified_purchase レビューは確認済みの購入に関するものです。
review_headline レビューのタイトル。
review_body レビュー テキスト。
review_date レビューが書かれた日付。

さらに、以下のフォーマットでデータが入っています

{
 'marketplace': 'JP',
 'product_title': 'イノセンス スタンダード版 [DVD]',
 'product_category': 'Video DVD',
 'star_rating': 5,
 'helpful_votes': 5,
 'vine': 'N',
 'verified_purchase': 'N',
 'review_headline': '見る人を選ぶ傑作',
 'review_body': '深く考える事に抵抗がある人は<br />見ないで下さい。<br />そんな人たちのこの映画に対する批判は、<br />うんざりです。<br />「わからなかった」ではなく<br />「本当にわかろうとする事が出来なかった」のでしょう。<br />わからない人にはわからない、<br />考えられる人、考えるのが好きな人にとっては、<br />一生考えていられる大きなテーマ達、たまりません。<br />まさにこの作品はは偉大な問題群でした。<br />美しい映像はそれを美しく表現してくれましたが、<br />断じて単独の主役ではありません。<br />この作品で私は一生楽しめる。<br />そんな作品です。',
 'review_date': '2004-09-18',
 'review_idx': 0,
 'product_idx': 32179,
 'customer_idx': 7903,
 'sets': 'training'
}

問題設定

このハッカソンでは商品ごとにレビューの役立つ順をランキングするという問題設定になっています。

詳細についてはハッカソンのSlideshareを御覧ください。

Getting Started

このハッカソンではすでにベースラインのプログラムが提供されているので、このブログではそれを実際になぞっていきます。

Step1: Studio Lab にアクセス

このブログでは Amazon Sagemaker Studio Lab を利用します。下記リンクからStudio Labにアクセスできます。
https://studiolab.sagemaker.aws/

もしお持ちでない方は、この手順書を元にアカウントを作成できます。

ログインができたら、Compute typeGPU にした上で、start runtime ボタンをクリックし、ランタイムを起動します。
次にopen project ボタンをクリックし、Jupyter Notebook にアクセスします。

さらに、+ ボタンを押して、Other セクションから Terminal ボタンをクリックします。

Step2: 環境構築

ここからは GitHub にあるインストールの手順を実際に入力していきつつ、コードを読みながら理解していきます。

この環境構築のステップでは、Terminal ウィンドウに次のコマンドを入力し必要な環境を Studio Lab 上に準備します

git clone https://github.com/Kosuke-Yamada/yans2022-hackathon-baseline.git
cd yans2022-hackathon-baseline/
conda env create --file environment.yml
conda activate yans2022-hackathon
python -m pip install -r requirements.txt
tar Jxfv ./data/dataset_shared_initial.tar.xz -C ./data/

Step3: 前処理

次のコマンドを入力し、前処理を行います。

sh ./script/preprocessing.sh

期待通り実行されても、特に標準出力は出ないようです。
では、実際にどんな事が書かれているか見てみましょう。

./script/preprocessing.sh を見てみると、preprocessing.py を実行しているとわかります。
では、更に Dive Deep して preprocessing.py を見てみましょう。

if __name__ == "__main__":
    # 引数を受け取る
    parser = argparse.ArgumentParser()
    parser.add_argument("--input_file", type=str, required=True)
    parser.add_argument("--output_dir", type=str, required=True)
    parser.add_argument("--n_train", type=int)
    parser.add_argument("--n_val", type=int, default=100)
    parser.add_argument("--random_state", type=int, default=0)
    args = parser.parse_args()
    
    # main関数を呼ぶ
    main(args)

たくさんの関数が定義されていますが、実際呼ばれているのは、59行目以降のようです。
引数を読んだあと、main 関数が呼ばれていますね。ではmain 関数ではどんな処理がされてるのでしょうか?

31行目以降の main 関数を見てみましょう。

def main(args):
    # 学習用データを読む
    df = pd.read_json(args.input_file, orient="records", lines=True)
    
    # 前処理
    df_sets = decide_sets(df, args.n_train, args.n_val, args.random_state)

    # 学習セットと評価セットとして出力する
    df_sets.to_json(
        args.output_dir + "training.jsonl",
        orient="records",
        force_ascii=False,
        lines=True,
    )

    df_tr = df_sets[df_sets["sets"].str.contains("-train")]
    df_tr.to_json(
        args.output_dir + "training-train.jsonl",
        orient="records",
        force_ascii=False,
        lines=True,
    )

    df_val = df_sets[df_sets["sets"].str.contains("-val")]
    df_val.to_json(
        args.output_dir + "training-val.jsonl",
        orient="records",
        force_ascii=False,
        lines=True,
    )

jsonファイルをPandasで読んだあとに、decide_sets 関数を呼び、training-train.jsontraining-val.jsonl として出力されています。

decide_sets 関数ではどのような処理が行われているのでしょうか?

def decide_sets(df, n_train, n_val, random_state):
    # 商品IDを集約
    df_product = df.groupby("product_idx").count()
    # 商品IDのリストを作成
    product_idx_list = sorted(set(df_product.index))

    # random シードを初期化
    random.seed(random_state)
    # 商品IDのリストをシャッフル
    random.shuffle(product_idx_list)

    # 商品IDのリストを指定の比率で分割
    val_list = product_idx_list[:n_val]
    train_list = (
        product_idx_list[n_val:] # 学習セットの数が指定されていなければ(全体-n_val)
        if n_train is None
        else product_idx_list[n_val : n_val + n_train] # 学習セットの数が指定されていれば(全体-n_val)からn_train個だけに絞る
    )

    # {商品id: 学習/評価ラベル}のkey-valueデータを作成
    sets_mapping = {}
    sets_mapping.update({i: "training-train" for i in train_list})
    sets_mapping.update({i: "training-val" for i in val_list})
    # sets列の値をsets_mappingの値に書き換え、sets_mappingになければ不要なデータとする
    df["sets"] = df["product_idx"].map(sets_mapping)
    df["sets"] = df["sets"].fillna("disuse")
    return df

decide_sets 関数では DataFrame と学習数、評価数、ランダムシードを引数に、DataFrame を返しています。

更に処理を読んでいくと、まずレビューから商品一覧を作り、商品一覧を特定の比率で分割しているとわかります。

その後 sets カラムの値を training-train, training-val, disuse の 3 種類に書き換えています。

つまり、DataFrame の sets カラムの値をもとに分割していたわけです。

Step4: 学習

では前処理を行ったので次に学習をします。

sh ./script/train.sh

もし pytorch_lightning.utilities.exceptions から始まる下記のようなエラーが出たは、GPUがないためにエラーになっています。ランタイムの設定のステップまで戻って、再度実行して下さい。


pytorch_lightning.utilities.exceptions.MisconfigurationException: GPUAccelerator can not run on your system since the accelerator is not available. The following accelerator(s) is available and can be passed into `accelerator` argument of `Trainer`: ['cpu'].

以下のような出力が出ると学習終了です。

train.sh のスクリプトを見てみると、実際のコードは train.py にあるようです。
では、更に Dive Deep して train.py を見てみましょう。

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--input_file", type=str, required=True)
    parser.add_argument("--output_model_dir", type=str, required=True)
    parser.add_argument("--output_csv_dir", type=str, required=True)
    parser.add_argument("--output_mlruns_dir", type=str, required=True)

    parser.add_argument("--experiment_name", type=str, default="predict_helpful_votes")
    parser.add_argument(
        "--run_name", type=str, default="cl-thooku_bert-base-japanese_lr1e-5"
    )

    parser.add_argument(
        "--model_name", type=str, default="cl-tohoku/bert-base-japanese"
    )

    parser.add_argument("--batch_size", type=int, default=16)

    parser.add_argument("--learning_rate", type=float, default=1e-5)
    parser.add_argument("--max_epochs", type=int, default=3)
    parser.add_argument("--gpus", type=int, nargs="+", default=[0])
    args = parser.parse_args()
    main(args)

たくさんの関数が定義されています。

引数の中には、--batch_size--learning_rate, --max_epochs などがあり、パラメータの変更は容易にできそうだとわかります。

更にその後に main(args) とあり、実際に学習をしているのは main 関数のようですね。次に main 関数を見ていきます。

def main(args):
    # データセットを定義
    dm = ReviewDataModule(args)
    # モデルを定義
    net = ReviewRegressionNet(args)

    output_model_dir = args.output_model_dir + args.experiment_name + "/"

    # パラメータを設定
    trainer = pl.Trainer(
        gpus=args.gpus,
        max_epochs=args.max_epochs,
        callbacks=[
            pl.callbacks.EarlyStopping(monitor="val_rmse", patience=3, mode="min"),
            pl.callbacks.ModelCheckpoint(
                dirpath=output_model_dir,
                filename=args.run_name,
                verbose=True,
                monitor="val_rmse",
                mode="min",
                save_top_k=1,
            ),
        ],
        logger=[
            pl.loggers.csv_logs.CSVLogger(
                save_dir=args.output_csv_dir,
                name=args.experiment_name,
                version=args.run_name,
            ),
            pl.loggers.mlflow.MLFlowLogger(
                tracking_uri=args.output_mlruns_dir,
                experiment_name=args.experiment_name,
                run_name=args.run_name,
            ),
        ],
    )
    # 学習
    trainer.fit(net, dm)

main 関数では ReviewDataModule でデータセットを定義し、ReviewRegressionNet でモデルを定義しています。
そして trainer.fit(net, dm) で学習をしていますね。

また PyTorch Lighting の Logger の機能を使ってロギングをしており、CSV 出力があることもわかります。

ここまでで読んだことを整理します。

* データの前処理をカスタマイズするなら、preprocessing.py ファイルの main 関数内の df = pd.read_json以降で前処理を実装し df_sets = decide_sets の行でデータの分割をすればよい
* モデルをカスタマイズするには train.py ファイルの main 関数内の net の定義を変えれば良い
* ログ出力は train.sh の引数 --output_csv_dirを見れば良い

Step5: 可視化

ではここまでのわかったことを活用し、epoch 数を増やした上で、学習の推移を可視化します。
yans2022-hackathon-baseline/script/train.sh 内の max_epochs の値を 10にして、再度実行します。

実行後 yans2022-hackathon-baseline と同じディレクトリに新しいノートブックを立ち上げて、ログ出力を可視化します。

import pandas as pd

# データの準備
log_data = "yans2022-hackathon-baseline/data/train/csv/predict_helpful_votes/cl-tohoku_bert-base-japanese_lr1e-5/metrics.csv"
df = pd.read_csv(log_data)

# loss を表示
df.groupby("epoch").max()[["val_loss", "train_loss"]].plot()

この青色の線が下がるようなモデルを作っていくことになります、読者の皆さん、是非トライしてみて下さい。

Applied Scientistならどう解くか

NLP若手の会2022 のハッカソンでは結果発表の際に Amazon の Kiryo より Applied Scientist ならどう解くかについて紹介しました。具体的にはどういうアプローチを取るか、データの前処理をどうするべきか、実装上の工夫の3つについてコメントでした。

まずアプローチについて、

LightGBM か Transformer で SOTA に近い InfoXLM の利用を考えます。今回はコンペティションなので両方使うことを考えますが、業務だと 2 つは使わずどちらかに絞るでしょう。また Amazon だとレビューの記載言語は多様なので、多言語のモデルを使用したり、 Learning to Rank を扱うモデルも試してみます。評価指標に合わせた学習方法かという点もモデル選択の軸になります。

さらにデータの前処理について

レビューのデータは前処理が重要になります。特に HTML のエスケープや改行などを処理します。また Transformer のインプットには上限があります。それを超えるような長いレビューをどう扱うかについて検討しなければいけませんね。コンペティションであれば Truncate しない XLM 系のモデルとそうでないモデルを試す。もしくは TF-IDF のような軽量で且つ大域の特徴が取れるものを使うことも考えられるかもしれません。

また実装について

モデルの実装と実験コードを分けるのは最も重要な点ですね。プロダクトにするときは必ず分かれるためです。また学習と推論のコードも可能であれば分けたいですね。

最後に総評として

このハッカソンではどのチームもベースラインから特徴量を工夫していてとてもいいなと思いました。うまく行ったチームが特別なことをやったというよりかは、交差検証のような教科書に書かれている内容を試しきれたかが、差になっていると感じました。最近の KDD のコンペティションの上位チームはクローリングをしていましたが、このハッカソンでは追加データを使ってはいけないという制約がありました。そんな中、みなさんかなり頑張っていたと思います。

まとめ

このブログでは Amazon のレビューデータセットを紹介しました。またその使い方として NLP若手の会2022 のベースラインコードを読んでいき、エポック数を増やした上で可視化を行いました。また Applied Scientist ならどう解くかについてもご紹介しました。

リアルなデータを使いたい方、ランキングシステムを作りたい方にとって、はじめの第一歩を踏み出すことが出来たのではないでしょうか。次のステップとして皆さんの手でより良いモデルの開発にも是非トライしてみて下さい。またこのブログが役に立ったら、ぜひこのブログをツイートしてください。

また AWS では お問い合わせページ から 相談も承っております。(AWSの導入に関するお問い合わせを選択ください) 要件がまとまっていないところからでも AWS のプロフェッショナルのアドバイスを受けられます。ML システムを AWS で構築したいときの相談相手としてぜひご活用ください。