2021の11日目の記事になります。12/11日のアドベントカレンダーです。毎日思うことなのですが、何食べようかなと結構悩みます。悩んでも分からないので毎日同じもの食べたり、似たようなものを食べているような気がします。いつ何が起こるか分からない人生でいつも無難な選択をしてしまうのは何か勿体無いようにも思います。されるのは結構酷な話です。いう問題を機械学習が解決してくれないだろうかと考えたりします。自分が好きな料理とそうでもない料理を集めたらそこそこの結果に応えてくれるモデルが作れそうではないでしょうか。料理の特徴量とは何でしょうか?見た目という意味では画像や動画もありますが、やはり料理の過程を説明しているということで、自然言語で書かれたレシピがより良く特徴を表していると思います。最近は改めて説明するまでもなく、大規模言語モデルをfine-tuningしてドメインを学習するようなBertやTransformerを中心とした機械学習のモデルが大きな成果をあげています。何よりもfine-tuningして、それほど学習に多くのリソースを使わなくてもドメインに適応できる可能性が広がったことは私にとっても大変嬉しいことです。この何が食べたいのかという問題においてもまずはBertを使ったfine-tuningで上手くいかないだろうかと考えることは自然なことです。機械学習では分類のモデルを考えます。ラベルをつけて境界を引いて好きな食べもの、嫌いな食べ物を分けるやり方もありだと思います。どのくらい好きか、どのくらい嫌いかを表現できるモデルかというとそこは難しいのではないかと思います。Learningなど距離を学習するモデルをよく耳にします。同じカテゴリにしたい特徴量のベクトルが近くなるように学習する手法です。この手法では良くcos類似度で距離によって似ている似ていないを表現することが出来ます。そこで今回はBERTを使ったfine-tuningでレシピを学習して、cos類似度でレシピが自分の好きな料理、食べ物にどれだけ近いかを表現できるモデルを作っていこうと考えました。レシピが似ているとは?レシピは文章から構成されています。文章を特徴量にする方法を考えなくてはなりません。BERTはToken毎の特徴量は出力出来ますが、文章単位ではありません。文章単位でBERTの表現力を持った特徴量が使えるにはどうしたら良いでしょうか。そこでSentence-Bertという論文が2019年に発表されています。これはBERTの出力の後にPooling層を設けてここでBERTの特徴量を文章単位に埋め込んでいます。この出力を損失関数に渡して学習を進めます。BERT-Networks↑この上の図がほぼ全てなのですが、今回使おうとしているのは右のネットワークです。Lossを使いました。LossについてはSentence-bertの論文でも引用されていますが、FaceNetの論文の図が分かり易かったです。FaceNetはコンピュータビジョンの話で自然言語処理ではないですが。Lossの学習では特徴量を3つに分けます。negativeというデータセットを用意します。anchorとpositiveのベクトルが近づくように、anchorとnegativeのベクトルが離れうように学習します。今扱おうとしているレシピであれば、好きなレシピをanchorとpositiveに使って、あまり好きではないレシピをnegativeのデータセットに用意します。Clusteringどういうデータが集まれば問題が解決できそうかある程度の方針が決まったのでデータを集めようとします。今回は白ご飯.comとキッコーマンのレシピを使います。この2つを選んだ理由は特にありません。白ご飯.comを学習データにしてキッコーマンのレシピで評価します。positiveに使うために15レシピずつ、negativeのデータのために15レシピ選んでいます。私が好きかどうかが基準で個人的な好みで選んでいます。好きなレシピのURLをjsonで書き込んでいきます。#これをPythonで取得してこのURLをクローリングしていきます。いうことでデータを集めていきます。データを集めるPythonのScrapyというクローリングのフレームワークを使っていきます。pythonは3.9です。プロジェクトを作って、ライブラリをインストールします。what_should_i_eatspiderというディレクトリの中にクローラーとパーサーを書いていきます。テストできるのでクローラーを全部回さなくていいので便利です。ScrapyのContractという機能のようでコメントにテスト用のプロパティを書くみたいです。parse引き続き実装していきます。和風カレーのレシピ(ルー不使用のあっさり系!):白ごはん.comrecipe和風カレーの材料は、豚肉に定番の野菜を用意して作ります。しめじや椎茸、えのきなどのきのこ類、いんげんやグリーンピースなどの青みを入れても美味しく、和風っぽさもアップします。※香りづけに生姜とにんにくも使っています。これは生のものがなければチューブのおろしたものでも代用可です。玉ねぎは5~6mm幅に、にんじんは小さめの乱切りに、じゃがいもは少し大きめに切って水にさらしておきます。いんげんは3~4cm幅に、しめじは石づきを切り落とし、生姜とにんにくは生のものを使うのであれば粗いみじん切りにしておきます(生姜は短めのせん切りでもOK)。※豚肉は、こま切れ肉が長いようなら3cm幅ほどに切り、バラ肉なら3~4cm幅に切っておきましょう。ルー代わりのAの材料3つも(野菜を煮込むタイミングでもいいので)容器に水100mlと混ぜ合わせておきます。合わせ、はじめに半分強くらいの水を入れてよく練ってから、後から残りの水を加えて混ぜる、こうするとダマになりにくいです(箸でも混ぜやすいです)。0.867sOK一つのURLに対して実行出来ることが確認できたのでURLのリストを作ります。ここは自動で作成すると自分の好きな食べ物嫌いな食べ物を選んでいるのか分からないので手動で選んでjsonに書き込んでいきます。negにそれぞれ15ずつ選んでレシピを入れていきます。posには好きな食べ物、negには嫌いな食べ物を入れていきます。好きな食べ物を210レシピ、嫌いな食べ物を105レシピ集めます。(各カテゴリから15レシピずつ)それでは集めます。だいたい240レシピで3~4時間ほどかかりました。だいたい画像の見た目だけで選んでいたので、もしこのモデルの精度がよかったらただの食わず嫌いじゃないかと思ったのですが、あれこれ考えるときりがないのでそのまま進めます。好きな食べものと嫌いな食べものに分けてjsonファイルを作りました。これをもとにクローリングしていこうと思います。ファイルからURLを読み込んでクローリング出来るようにします。サーバーの負荷がかからないようにリクエストの間隔を3秒にしておきます。os.path.abspath(os.path.dirname(__file__))PROJECT_ROOTに設定したディレクトリにinputというディレクトリを作ってlikes.jsonとdislikes.jsonを配置します。ファイルを読み込めるようにソースを書き換えます。itemそしてテストします。かぼちゃの炊き込みご飯のレシピ/作り方:白ごはん.comrecipeまず、かぼちゃの下ごしらえの前に、米3合を研いで炊飯器の内釜にセットし、2合強くらいの水を加え、30分〜1時間ほど置いて浸水させます(←後から調味料を加えるので、その分を考えてはじめに加える水を少なめにします)。かぼちゃは、種をスプーンで軽く取り除いてから1/4個のかぼちゃをさらに半分に切ります。かぼちゃの切り口をまな板に付け、ぐらつかないよう手でしっかりと押さえて切りましょう。続けて、かぼちゃの皮にイボや色の変わった硬い皮の部分があれば、ピーラーや包丁で薄く削り落とします。包丁の刃先よりも持ち手に近い刃元の方が力が伝わりやすいので、真ん中から刃元に近い部分で切るとよいです。和風カレーのレシピ(ルー不使用のあっさり系!):白ごはん.comrecipe和風カレーの材料は、豚肉に定番の野菜を用意して作ります。しめじや椎茸、えのきなどのきのこ類、いんげんやグリーンピースなどの青みを入れても美味しく、和風っぽさもアップします。※香りづけに生姜とにんにくも使っています。これは生のものがなければチューブのおろしたものでも代用可です。玉ねぎは5~6mm幅に、にんじんは小さめの乱切りに、じゃがいもは少し大きめに切って水にさらしておきます。いんげんは3~4cm幅に、しめじは石づきを切り落とし、生姜とにんにくは生のものを使うのであれば粗いみじん切りにしておきます(生姜は短めのせん切りでもOK)。※豚肉は、こま切れ肉が長いようなら3cm幅ほどに切り、バラ肉なら3~4cm幅に切っておきましょう。ルー代わりのAの材料3つも(野菜を煮込むタイミングでもいいので)容器に水100mlと混ぜ合わせておきます。合わせ、はじめに半分強くらいの水を入れてよく練ってから、後から残りの水を加えて混ぜる、こうするとダマになりにくいです(箸でも混ぜやすいです)。11.616sOK実際にクローリングをしていきます。settings.pyのpipelinesの設定のコメントアウトを外します。---最終的にキッコーマンのSpiderも作りました。itempipelines.pyはこうなりました。Workbenchを使って学習していきます。必要なライブラリをインストールしていきます。scikit-learnなどはあらかじめインストールされていました。SentencesDataset嫌いな食べ物データを読み込みます。df_dislike.dropna()好きな食べ物データも読み込みます。df_like.dropna()データをシャッフルします。negにデータを分けます。index=False,)学習データができたので学習していこうと思います。output_path="./sbert",)ここでTransformerはそのままのTransformerです。Lossなのですが、コードを見るとなんとなくわかります。featuresPooling.pyのforwardの関数だけ抜粋しています。Poolingもいくつかの方法を試したと論文で言っていて、tokenをmax,meanでPoolingするか最初のトークンを取得するか、全部盛りにするか試して、平均が一番よかったようなので私も平均でPoolingしています。self.pooling_mode_mean_tokensのところが平均でPoolingしているところです。token_embeddingをsumして1次元にしてmaskの数で割って平均しています。torch.clampは1e-9以下にならないようにして学習が進まなくなるのを避けているのだと思います。losses.mean()処理はforwardのところでTransformerとPoolingを通ったsentenceをembeddingした特徴量がposとnegに分けられて、posはanchorと近づくように、negはanchorと離れるように設計されているのが分かります。今回はanchorのデータセットと評価用のキッコーマンのデータセットの類似度を測ってキッコーマンのデータセットから好きなレシピが見つけられるかを確かめたいと思います。sbert.encode(eval_caption)嫌いな食べ物(スコアの下位)好きな食べ物(スコアの上位)5つを抽出しました。ascending=False)私の好き嫌いを誰もしらないと思うのでコメントしておきます。