レシピ構造化について

フードテック

今年度新入社員になり、食関連で以下の2冊を読みました。 一つはフードテック革命、もう一つはキッチン・インフォマティクスです。

shop.nikkeibp.co.jp

www.ohmsha.co.jp

業務と興味が重なる分野で、とても楽しく読めました。

私は、自分にとっていい本を以下のように定義しています。

いい本=

読んでいるときにアイデアや疑問が次々に浮かび、頭がすごく回る本

本から得るものはヒントで、知識ではないと思います。

この意味でとてもおもしろい本達でした。

中でも興味深かった、「レシピ構造化」について書いてみようと思います。

レシピ構造化

現在、様々な企業がレシピ構造化に取り組んでいます。

その目的は、レシピサイトに投稿されたレシピの自動分類、レシピパーソナライゼーション、果てはロボティックキッチンの実現です。 つまり、「レシピ構造化」とは、人間が自然言語テキストや写真・動画で表現したレシピを、システムが認識できる形式に変換(ソフトウェア化)する処理です。

レシピ構造化には、自然言語処理や画像認識、それらを組み合わせるクロスモーダルなアプローチがあります。 レシピサイトでは、投稿されたレシピテキストを自然言語処理により解析することで、レシピの分類などを行っています。 また、自社サイトのレシピをアカデミックに公開し、レシピテキストの解析などの研究を支援しています。 また、食事管理アプリでは、料理写真を画像認識により、料理の推定を行っています。

個人的な意見として、自然言語処理・画像処理・そのクロスモーダルドメインは、料理ドメインをあんまりカバーしていないと考えています。

その大きな要因は、料理が人間にとって身近すぎることだと考えています。 そのために、その表現が構造化されておらず、前提知識・感覚が占める割合が大きくなります。

例えば、「かつらむき」という切り方があります。

ネットで調べると、以下のように説明されています。

大根やにんじん、きゅうりなど、円柱形のものを回しながら、切れないように長くむいていく方法です。飾り切りの一種ですが、皮をむくときにも活用できます。

桂むき | 基本のキ(切り方編) | ハウス食品

しかし、これだけ読んでも、実際に見ないことにはイメージできないですよね。 「大根やにんじん、きゅうり」を見たり触ったりしたことがなければ、なおさらです。 テキストだけで「かつらむき」を構造化することは難しいです。 逆に言うと、特殊な単語に大きな情報が押し込まれています。

また実際の利用では、「かつらむき」を切り方として定義したうえで、「かつらむき」が可能な食材条件、「かつらむき」による効果(火が通りやすい・食材を巻けるなど)をデータベースとして持つ必要があります。

アプローチ

これまで見てきたように、料理は身近過ぎるために、一から機械学習するのは難しいと考えられます。 また、料理に関する単語は定義可能で有限であるため、学習するのは非効率です。 そのため、それらをデータベースとして持つことが重要と考えます。

しかし、全て人手でデータベースを構築するのは難しい。。 そこで、既存のデータベースを統合する、ユーザインタラクションによりデータを追加できるハイブリッドシステムが有効だと思います。

そこで、自然言語処理の出番があります。 機械学習によりデータベースを構築するのではなく、データベースの枠を作り、その中身を機械学習と人手のハイブリッドシステムで埋めていくイメージです。

結論

今回は、「レシピ構造化」の課題とそれに対する自身の考えについて書いてみました。 今後は、今回考えたことをもとに、レシピ構造化・生成システムを作っていこうと思います。

目標は、

ユーザの食在庫にあるもの・好み・栄養摂取状況と、合う食材、調理方法をもとにレシピを生成するシステムを作る

です。

その過程で、Python機械学習自然言語処理など)・システム構築を学べればいいなと思います。

Pythonでベクトル表現・演算を行う【Word2Vec】

Pythonで単語のベクトル表現・演算を行う

Word2Vecは、自然言語の単語の意味をベクトル表現する手法です。 また、以下のような意味的な演算が可能であることが有名です。

「King (王) – Masculinity (男性) + Femininity (女性) = Queen (女王)」

モチベーション

前回GiNZAを紹介しました。

このときに、単語ベクトルを見て、Word2Vecを思い出しました。

また、単語のベクトル表現について、Word2Vecを試してみようかなと思いました。 例えば、「焼く+水=煮る・茹でる」になるのかなど。 ただ、「水を入れて火にかける」という説明を学習データに含めないと、マッピングができないかもなと思っています。

harukary7518.hatenablog.com

そこで、今回はWord2Vecを試し、料理に関する単語についてベクトル演算が可能かを確かめてみました。

Word2Vec

主に以下のサイトを参考に試しました。

qiita.com

また、収集したレシピテキストをもとにWord2Vecを再学習し、調理に関する単語の演算を試しました。 今回は、Word2Vecの内容は特に調べていません。

使用方法

インストール

pip install gensim

上のコマンドで簡単にインストールができます。 また、今回は形態素解析Janomeを用いるため、これもインストールします。

pip install janome

Word2Vec学習、ベクトル演算処理

  • インポート
import json
from janome.tokenizer import Tokenizer
from gensim.models import word2vec

必要なモジュールをインポートします。 今回は、事前に取得しJson形式で保存したレシピデータを使うので、jsonモジュールをインポートしています。

  • テキスト準備
path = 'data/recipe_chefgohan.json'
df = json.load(open(path, 'r', encoding='utf-8'))
# print(df)
text = ''
for _,r in df['recipes'].items():
    # print(r['title'])
    start = r['title']+'を作ります。'
    end = r['title']+'が完成しました。'
    instructions = ''.join(r['instructions']).replace('\n','')
    text += start + instructions + end + '\n'
print(text[:100])

レシピの手順テキストを使いますが、手順には料理名が含まれないので、 以下の文字列を前後に追加します。

これでちゃんとした文章になるかな?

「○○を作ります。」

「○○が完成しました。」

t = Tokenizer()
def extract_words(text):
    tokens = t.tokenize(text)
    return [token.base_form for token in tokens 
        if token.part_of_speech.split(',')[0] in['名詞', '動詞']]

sentences = text.split('。')
word_list = [extract_words(sentence) for sentence in sentences]

準備したテキストを形態素解析し、名詞・動詞・形容詞のみを抽出します。

これで、準備が整いました。

  • Word2Vecの学習
model = word2vec.Word2Vec(word_list, vector_size=100,min_count=5,window=5,epochs=100)

学習は、1行で行えます。 引数がわからないときは、以下のコードで確認できます。

参考にしたブログと引数名が変わっていたため、この方法で確認して書き換えました。

print(help(word2vec.Word2Vec()))

結果表示

  • 肉ベクトル

コード:

print(model.__dict__['wv']['肉'])

ベクトルをそのままリストで取得し、表示します。

出力:

[-0.86755484 -0.1890513   2.70156    -0.76835483  1.2123765  -1.7350413
  1.2958905  -1.2852497  -0.27418274  0.02897273 -1.6959829   1.7419792
 -0.9828316  -0.10096513 -0.08968378 -0.48344788  0.42379144 -0.6499651
 ・・・
 -1.6139066  -0.20777668  0.66221493 -1.0789928  -0.3463647   0.8860949 ]

一応ベクトルができていますね。

  • 焼く

コード:

ret = model.wv.most_similar(positive=['焼く'], topn=5) 
for item in ret:
    print(item[0], item[1])

次に、ターゲットのベクトルに最も類似したベクトルを持つ単語を表示します。

出力:

焼ける 0.4931952655315399
キツネ 0.45121780037879944
トースター 0.44726747274398804
焼き上げる 0.430612713098526
返す 0.4264180660247803

うーん、この時点で怪しい。 「焼く」という単語の近くに出てくるであろう単語が出力されています。

  • 焼く+水

コード:

results = model.wv.most_similar(positive=['焼く','水'], topn=5)
for result in results:
    print(result[0])

positiveに足したい単語の文字列をリストで与えることで、ベクトルの足し算ができます。

出力:

焦げる
g
70
焼き上げる
テフロン

うーん。「g」「70」は水の近くにある分量だと思います。 「焼く」-「水」で焦げるだったら理解できるけど、今回は足し算だしな。

  • 煮る-水

コード:

results = model.wv.most_similar(positive=['煮る'], negative=['水'], topn=5)
for result in results:
    print(result[0])

negativeに引きたい単語の文字列をリストで与えることで、ベクトルの引き算ができます。

出力:

煮込む
タイ
店
俺
蒸し器

「煮る」-「水」は「煮込む」! 今回これだけそれっぽいですね。 「水」がいくらか減るまで「煮る」=「煮込む」

ただ次の候補が「タイ」。。

  • 煮る-焼く

コード:

results = model.wv.most_similar(positive=['煮る'], negative=['焼く'], topn=5)
for result in results:
    print(result[0])

最後に、「煮る」-「焼く」。 もちろん「水」を期待してのテストです。

出力:

顆粒
だし
A
煮汁
ペースト

完全敗北。 「A」は調味料や下ごしらえした食材を表すAですね。

  • 肉+玉ねぎ

コード:

results = model.wv.most_similar(positive=['肉','たまねぎ'], topn=5)
for result in results:
    print(result[0])

最後に食材を足して、何かしらの料理が出てくるかを試しました。

出力:

きゅうり
挽き肉
ひき肉
ローリエ
セロリ

なんか食材いっぱい出てきた。 もしかしたら名詞のうち、食材はクラスターになっている? そうだとしたら今後使い道があるかもしれません。

結論

今回は、Word2Vecを使った調理単語ベクトル表現を試しました。 しかし、上の通り、全くうまくいきませんでした。

原因としては、Word2Vecが単語間の位置関係しか見ていないことがあると思います。

レシピ手順テキストの特徴として、以下があります。 * 主語や動詞の対象が省略されることが多い * 調理の結果を明示的に表現しないため、「煮る=水分が多い状態で加熱する」を学習できるようなデータになっていない

そのため、文章構造解析の結果をもとに、主語を補完する、 また、「煮る」とは何かがわかるデータを学習する、などが必要かもしれません。

しかし、「煮る」などは定義可能であるため、わざわざ学習せずに調理データベースを構築したほうがよいかもしれません。

それでも、かなり手間がかかる作業です。。難しい。

レシピテキスト解析特有の問題やその対応がまとめられたものなどあれば読んでみたいですね。 現状は、自然言語処理の分野もレシピ構造化についても背景が少なくて難しい。

今回用意した学習データでは全くうまくいきませんでした。 Word2Vecについて「使えた」「理解した」とは言えないので、Word2Vec学習でよく使われる、Wikipediaテキストを試したり、理論を学んだりもしてみたいと思います。

参考文献

[1] 15分でできる日本語Word2Vec - Qiita

[2] models.word2vec – Word2vec embeddings — gensim

レシピ生成システムの開発

前回、レシピ構造化について考えをまとめました。 これについてより深く考えるため、自分でシステムを作ってみたいと思います。 こちらの記事で大まかな目的やアーキテクチャを説明します。

Issues

一応ターゲットを決めたいと思います。自分です。

レシピ通りに作れない人です。

私が料理を作るとき:

レシピを探して、

「あこれ作りたいな」

買い物に行くと、

「あこれ安いな」

「あ冷蔵庫にあったあれも使い切りたいな」

作り始めると、

「この調味料量るのめんどくさいな」

「ちょっと余るより全部入れちゃえ」

私はよくこんな感じになります。

最終的にレシピ通り作れたことがない。 こうなる理由は2つあると考えています。

  • 料理が好き(料理の科学が好き)

料理が上手な人ではなく、好きな人です。 また、手間をかけてレシピ通りに作りたい人ではありません。

むしろ逆で、新しい組み合わせを試したくなったりするために、レシピ通りに作れない人です。

  • 一人暮らしなど作る人前が少ない

一人前だと、複数品目作るのは大変なので、一品に栄養を詰め込みたくなります。 また、冷蔵庫の食材を使い切ることが重要なので、レシピの分量よりも、残っている分量を優先してしまいます。

食材の分量が変わると調味料の分量も変わる。 全部k倍したわけじゃないので、もはや調味料の分量は計算できません。 そこで、適当になっちゃいます。

Purpose

自由にカスタマイズできるレシピを生成するシステムを目指します。

  • レシピ構造化

まずレシピを構造化する必要があります。

材料を食材の栄養成分や風味成分に紐づけたり、レシピ特徴を抽出したり。

  • 栄養・好み・食在庫をもとにパーソナライズ

次に、ユーザデータと紐づけてパーソナライズしたいと思っています。

ただ、これはユーザがいないと実証ができないので、追々です。

食事管理ツールとか作れれば。

System

開発するシステムのアーキテクチャを描いてみました。

f:id:harukary7518:20210831215406p:plain

食材データベース

まずは、食材に関するデータを統合します。

調べたところ、食材の栄養成分や風味成分などがまとまっている日本語データベースはありませんでした。

そのため、複数DBを統合する必要がありそうです。 そのためにはデータベース間で同一食材の認識(Entity Recognition)が必要です。

また、実は英語なら食材の全てと言っていいほどの情報を持つデータベースがあり、これを部分的に使うことも考えています。その場合は翻訳も。。

FooDBというデータベースなんですが、データが大きすぎて単純なテーブルじゃなく、使うのも難しそうです。 誰か使ったことある人いれば教えてほしい。。

レシピデータベース

次に、レシピデータの収集、食材データと紐づけたり、レシピ特徴を抽出します。

レシピデータは、研究室単位じゃないとダウンロードできないものが多いです。 なので、スクレイピングで集めることにします。

食材データの紐づけは、食材データベース間と同様の処理が必要です。 また、レシピから主食材や主手順などを抽出することで、レシピ特徴を表現します。

このような構造化をすることで、レシピのカスタマイズなどが可能になります。

レシピ生成

最後に、構造化レシピをもとに、カスタマイズしたレシピを生成します。

栄養素をもとに代替食材提案をしたり、2つのレシピを融合したりする予定です。

ここで、

「鶏むね肉 → 鶏もも肉+豆腐」

のように2つ以上の食材で代替できたらおもしろいですね。 サブグラフマッチングとか使えないかな。

レシピの融合については、あるレシピフレームワークを別のレシピに適用する感じをイメージしています。

例えば、生姜焼きのフレームワーク(豚薄切り・玉ねぎ×しょうが・甘辛×炒め)をジャーマンポテト(ベーコン・じゃがいも・玉ねぎの炒め)に適用します。

バランスをとるために肉じゃがっぽくにんじん追加、彩りのスナップエンドウ追加、みたいな。

イメージ画像です。

f:id:harukary7518:20210831215345j:plain

実は実際に自分で作ってみた料理なんですが、おいしかった。。

こういう提案をしてくれたら選ぶ楽しさとか、どうやったらおいしく調理できるかを考える楽しさがあっていいかもなと思うんです。

最終的には、ユーザインタラクションにつながるんですが、ユーザデータに基づく自動提案とかできたらいいですね。 上の提案を、食在庫、栄養、好み情報をもとにさらにパーソナライズする感じです。

ユーザインタラクション

ユーザデータを収集し、そこから好みや癖を推定します。

また、Entity Recognitionやレシピ生成をユーザインタラクションを利用するハイブリッドシステムにできたらいいなと思います。

ユーザが入力した材料のエンティティ候補を出し、選択されたエンティティにユーザ入力のメンションを追加する、みたいな。

Conclusion

「自由にカスタマイズできるレシピを生成するシステム」のコンポーネントを説明しました。 現状はミニマルなシステムと、半分夢見たいものが入った計画があります。 ここからミニマルシステムについて何回かに分けて紹介していこうと思います。

harukary7518.hatenablog.com

上の記事に「自然言語処理ではレシピ構造化は解けない」みたいなこと書いているんですけど、実際作っていて自然言語処理が必要な部分が多そうです。

なので勉強していこうと思い、GiNZAやWord2Vecを試しています。

harukary7518.hatenablog.com

harukary7518.hatenablog.com

でも、まだまだ理想は書ききれていなくて。

直観的・プログラミング的にレシピ構築できるツールを作ってデータを集めたい、とかね。

例えば、炒め物という入れ物に冷蔵庫にある食材を入れると、それに合う食材・調味料が候補として出てくる。 その中から選んでレシピ、買い物リスト生成する。

最近それに近いアプリみつけて、うおおってなってた。これもブログに書こう。

とにかく、自分の頭が回るものを作って勉強していこうと思います。

Link

今回開発するシステムのソースコードはこのリポジトリにあります。

github.com

また、この中で使われている処理について、勉強がてらまとめているリポジトリがこちらです。

github.com

Pythonで日本語テキストを解析する【GiNZA】

Pythonで日本語テキストを解析する

日本語テキストを形態素解析係り受け関係解析を行うためのPythonライブラリを探していました。

今回見つけたGiNZAを試してみたので、その仕組みと使い方を紹介します。

megagonlabs.github.io

形態素解析とは

形態素とは意味を持つ最小の言語単位であり、形態素解析とは与えられた文を形態素単位に区切り、各形態素に品詞などの情報を付与する処理である [1]

係り受け関係解析とは

係り受けとは、文節間の意味的な修飾関係のことであり、格関係など意味的な関係を捉える上で重要である [1]

モチベーション

「レシピ構造化をやりたい」というのが、個人的なモチベーションです。 材料リストから主材料・主調理工程を抽出したり、調理フローグラフを取得したりです。

GiNZA

GiNZAとは、リクルートMegagon研究所と国立国語研究所が共同開発を行っているPure Pythonの日本語NLPライブラリです。 spaCyという、Explosion AI社の開発する自然言語処理ライブラリをベースとしています。 spaCyのパイプラインは、 Tagger→Parser→Nerとなっています。 GiNZAでは、その前後に処理を加えることで、日本語対応させています。

GiNZAによる日本語自然言語処理

形態素解析について、英語だとスペースで分割すればいいので、品詞分類から始まります。 しかし、日本語の場合、辞書にある単語を認識・分割する(トークン化)という処理が必要になります。

(日本語のように自立語に複数の付属語が分かち書きされずに結合される構造を持つ言語を「膠着語」と言うそうです。)

そのためGiNZAでは、SudachiPyという日本語トークン化の処理(Tokenizer)をTaggerの前に追加されています。

また、依存関係ラベリングと用法曖昧性解決処理(CompoundSplitter, BunsetuRecognizer)を最後に追加することで、解析性能を向上されています。[3]

使用方法

インストール

pip install ginza

調べると、-Uのオプションを追加したりするコマンドが出てきましたが、 最終的にこのコマンドだけでインストールができました。

解析処理

import spacy
nlp = spacy.load("ja_ginza")
doc = nlp(
    "ひき肉に、炒めて冷ましたたまねぎ、パン粉と牛乳と卵を混ぜる。"
)

これだけです。すごく簡単ですね。

instruction_list = [
  "ひき肉に、炒めて冷めたたまねぎ、パン粉と牛乳と卵を混ぜる。",
  "小判の形に成形し、冷蔵庫で1時間寝かせる。",
  "熱したフライパンにサラダ油を入れて焼く。"
]
docs = list(nlp.pipe(instruction_list))

文字列リストをこのように入力することもできます。

“内部的にバッチ化されるので、より効率的な処理が可能” [3]だそうです。

結果表示

コード:

print('\n'.join([token.text for token in doc]))

出力:

ひき肉
に
、
炒め
て
冷まし
た
たまねぎ
、
パン粉
と
牛乳
と
卵
を
混ぜる
。
  • 名詞抽出

コード:

print(', '.join([n.text for n in doc.noun_chunks]))

出力:

ひき肉, 炒めて冷ましたたまねぎ, パン粉, 牛乳, 卵
  • 固有表現

コード:

for ent in doc.ents:
  print(ent.text, ent.start_char, 
        ent.end_char, ent.label_)

出力:

たまねぎ 12 16 Food_Other
パン粉 17 20 Food_Other
牛乳 21 23 Food_Other
卵 24 25 Food_Other
  • 単語ベクトル

コード:

token = doc[3]
print(token)
print(token.vector)

出力:

炒め
[ 0.08003599 -0.12529491  0.41058803 -0.02664721  0.04041548  0.78141636
 -0.23311277 -0.15073359  0.00955659 -0.5930606   0.34050038  0.1425334
  0.15794998  0.37364605 -0.00280757 -0.24731137 -0.01622627 -0.17575407
・・・
 -0.40049294  0.32097352  0.05157278 -0.0260973  -0.20139831  0.08696187
  0.4490269  -0.05321052 -0.29156387  0.08571155  0.05183061  0.42766365
  0.01349372  0.45254493  0.304076   -0.24617903  0.01391126  0.3111292 ]

コード:

for sent in doc.sents:
    for token in sent:
          print(ent.text, ent.start_char, 
                   ent.end_char, ent.label_)

出力:

0 ひき肉 ひき肉 NOUN obl 15
1 に に ADP case 0
2 、 、 PUNCT punct 0
3 炒め 炒める VERB advcl 5
4 て て SCONJ mark 3
5 冷まし 冷ます VERB acl 7
6 た た AUX aux 5
7 たまねぎ たまねぎ NOUN obl 15
8 、 、 PUNCT punct 7
9 パン粉 パン粉 NOUN nmod 11
10 と と ADP case 9
11 牛乳 牛乳 NOUN nmod 13
12 と と ADP case 11
13 卵 卵 NOUN obj 15
14 を を ADP case 13
15 混ぜる 混ぜる VERB ROOT 15
16 。 。 PUNCT punct 15

コード:

from spacy import displacy

for sent in doc.sents:
    svg = displacy.render(sent, style="dep", options={"compact":True})

出力: f:id:harukary7518:20210825181221p:plain

結論

日本語の自然言語処理を数行のコードで素早く行えるGiNZAを紹介しました。 また、以下のリンク [3]では、単語ベクトルなどデータセットを入れ替えたり、各処理で手法を入れ替えたりしており、カスタマイズ性も高いといえます。

個人的には、係り受け関係木をもとに調理フローグラフを取得できるか試してみようと思います。 少し試した範囲では、レシピ手順の文章は主語がなく、崩れているため難しいかもしれません。

また、単語のベクトル表現について、Word2Vecを試してみようかなと思いました。

例えば、「焼く+水=煮る・茹でる」になるのかなど。 ただ、「水を入れて火にかける」という説明を学習データに含めないと、マッピングができないかもなと思っています。

参考文献

[1] Python による日本語自然言語処理

[2] NLP2019 松田寛 - GiNZA

[3] はじめての自然言語処理 spaCy/GiNZA を用いた自然言語処理 | オブジェクトの広場