パーソナライズ栄養管理アプリ
個人の健康情報にもとづいて食事プランを提案するアプリです!
開発の軸としては、 日本語対応した Gemma のモデルを使うことを主に置いています。
栄養に関する計算方法はパッと使えるものを持ってきました。RAGなどの検索機能も使っていないです🤦♂️
機能の概要
このアプリは、4つの機能があります。
-
ユーザー情報の登録
年齢、性別、体重、身長、活動レベル、目標を入力して、個人の健康データを管理 -
PFC バランス表示
BMIや基礎代謝量から、必要なタンパク質、脂質、炭水化物の量を計算 -
栄養計算
入力した食材の栄養データの生成
-
食事プラン
個人の目標と好みに応じたダイエットプランを自動生成
準備
プロジェクトの構成はこんな感じです。
app
│ app.py
│ helper.py
│ config.py
│ style.css
│ requirements.txt
└
必要なパッケージ
streamlit
で実装するので以下をインストールします。
streamlit
hydralit-components
streamlit_option_menu
import streamlit as st
import hydralit_components as hc
from streamlit_option_menu import option_menu
from config import *
from helper import *
import certifi
import requests
Gemma モデルについて
Hugging Face にモデルが公開されています。日本語版は jpn
と書かれています。
投稿時点ではデプロイ方法には Inference API (Inference Providers内) がないので HF Inference Endpoints を利用してエンドポイントを取得しました。
Hugging Face と HF Inference Endpoints で API を利用する方法はこちらに書きました!
エンドポイントを取得できると以下のようになります。インスタンスは AWS を利用しました。
実装するときはシンプルに requests
を使えばOKです🙆♂️
import requests
API_URL = "https://YOUR_ENDPOINT.aws.endpoints.huggingface.cloud"
headers = {
"Accept" : "application/json",
"Authorization": "Bearer hf_XXXXX",
"Content-Type": "application/json"
}
def query(payload):
response = requests.post(API_URL, headers=headers, json=payload)
return response.json()
output = query({
"inputs": "Can you please let us know more details about your ",
"parameters": {
"max_new_tokens": 150
}
})
実装方法
ユーザー情報の登録フォーム
サイドバーにフォームを配置し、年齢、性別、体重、身長、活動レベル、目標を入力・選択できるようにします。
ソースコード
# サイドバー
with st.sidebar:
with st.form("form"):
st.header("ユーザー情報")
col1, col2 = st.columns(2)
with col1:
st.header("年齢")
st.header("性別")
st.header("体重 (kg)")
st.header("身長 (cm)")
st.header("活動レベル")
st.header("目標")
with col2:
age = st.number_input("年齢を入力してください", min_value=5, value=person_info["年齢"], step=1, label_visibility="collapsed")
gender = option = st.selectbox("性別を選んでください", gender_list, label_visibility="collapsed")
weight = st.number_input("体重を入力してください", value=person_info["体重"], min_value=20, step=1, label_visibility="collapsed")
height = st.number_input("身長を入力してください", value=person_info["身長"], min_value=100, step=1, label_visibility="collapsed")
activity_level = st.selectbox('活動レベルを選択してください', activity_level, help=activity_details, label_visibility="collapsed")
goal = st.selectbox('目標を選択してください', goal_list, label_visibility="collapsed")
c = st.columns((1,4,1))
with c[1]:
submitted = st.form_submit_button("登録", type="primary")
person_info["年齢"] = age
person_info["性別"] = gender
person_info["体重"] = weight
person_info["身長"] = height
person_info["活動レベル"] = activity_level
person_info["目標"] = goal
activity_details = """
とても低い: 全く運動しない \n
やや低い: 週に1,2回程度の運動 \n
普通: 週に3-5回程度の運動 \n
やや高い: 週に6,7回程度の運動 \n
とても高い: ハードなエクササイズや肉体労働
"""
gender_list = ['男性', '女性', 'その他']
goal_list = ['筋肉増量', '体重減少', '維持']
activity_level = ["とても低い", "やや低い", "普通", "やや高い", "とても高い"]
特に工夫すべき点はありませんが、入力フォームは色々なアプリに使えるので気になる方はソースコードを見てください!
BMIとPFCバランス計算
登録されたデータを元にBMIや基礎代謝量(BMR)を計算します。ここでは、ハリス・ベネディクトの式 という計算式を使っています。
def calculate_bmi(person_info):
bmi = person_info["体重"] / (person_info["身長"] / 100) ** 2
if bmi < 18.5:
bmi_class = "低体重"
elif bmi < 25:
bmi_class = "標準体重"
elif bmi < 30:
bmi_class = "やや肥満"
else:
bmi_class = "肥満"
return bmi, bmi_class
def energy_calc(person_info):
if person_info["性別"] == "男性":
bmr = 88.362 + (13.397 * person_info["体重"]) + (4.799 * person_info["身長"] / 100) - (5.677 * person_info["年齢"])
else:
bmr = 447.593 + (9.247 * person_info["体重"]) + (3.100 * person_info["身長"] / 100) - (4.330 * person_info["年齢"])
tdee = bmr * activity_level_multipliers[person_info["活動レベル"]]
return bmr, tdee
def macro_perc(person_info, calories):
if person_info["目標"].lower() == '体重減少':
protein_percentage = 30
fat_percentage = 25
elif person_info["目標"].lower() == '維持':
protein_percentage = 25
fat_percentage = 30
elif person_info["目標"].lower() == '筋肉増量':
protein_percentage = 35
fat_percentage = 20
else:
raise ValueError("無効な目標")
carb_percentage = 100 - (protein_percentage + fat_percentage)
protein = (protein_percentage / 100) * calories / 4
fat = (fat_percentage / 100) * calories / 9
carbs = (carb_percentage / 100) * calories / 4
return {'タンパク質': protein, '脂質': fat, '炭水化物': carbs}
栄養計算
プロンプトは次の通りです。ポイントは上手くテーブルでデータを表示できるように誘導することです。
より本格的なものを目指す方は RAG などでデータベースを参照できると良いと思います
def diet(person_info):
search = st.text_input("食べた物を入力してください!", placeholder="焼きそば")
f = st.button("決定", type="primary")
if f:
with st.spinner(f"**{search}** について情報を探しています...."):
prompt = f"""
表形式で栄養データを生成してください。テキストではなくテーブルだけを返すようにします。
対象は{search}についてです。
テーブルは栄養データとして、以下の6つの特徴を必ず持ちます:
1.量
2.材料
3.タンパク質
4.脂質
5.炭水化物
6.カロリー
"""
res = get_response(prompt)
res = res[0]["generated_text"].split("\n\n ")[1]
st.write(res)
placeholder
や spinner
を使うと見やすくなります。
ダイエットプランの生成
こちらもプロンプトを紹介します。住んでいる国や食の好みは実験的に入れてみました。
def plan(person_info):
with st.form("食事プラン"):
st.header("あなたの情報")
cols = st.columns(3)
with cols[0]:
loc = st.selectbox('あなたの住んでいる国はどこですか?', ["日本", "日本以外の国"])
with cols[1]:
remarks = st.text_input("食事の好み", placeholder="例:野菜が好き、肉が嫌い")
s = st.form_submit_button("食事プランをつくる", type="primary")
if s:
with st.spinner('プランを生成中です....'):
bmi, bmi_class = calculate_bmi(person_info)
bmr, tdee = energy_calc(person_info)
prompt = f"""
表形式で食事プランを生成してください。テキストではなくテーブルだけを返すようにします。
対象者の性別は{person_info["性別"]}、{loc}に住む人で、BMRが {bmr} kcal、 BMIは{bmi}、1日の消費エネルギーは{tdee} kcalの人です。
そして、食事プランのメニューに反映させるべき次の嗜好を持っています:{remarks}。
目標は、{person_info["目標"]}です。
テーブルは食事プランとして、以下の特徴を必ず持ちます:
1.食事時間(朝食、昼食、夕食の3回)
2.メニュー
3.材料
4.栄養素(g)
5.カロリー(kcal)
"""
res = get_response(prompt)
res = res[0]["generated_text"].split("```")[1]
st.write(res)
まとめ
食事プランの提案に関してはあまり実用的ではなかったですが、Gemma モデルとしては問題なく日本語でのやり取りができました👍
ソースコード全体
import streamlit as st
import hydralit_components as hc
from config import *
from helper import *
from streamlit_option_menu import option_menu
import warnings
import requests
headers = {
"Accept" : "application/json",
"Authorization": f"Bearer {HF_TOKEN}",
"Content-Type": "application/json",
}
warnings.filterwarnings('ignore')
# ページセットアップ
st.set_page_config(
page_title=PAGE_TITLE,
layout='wide'
)
def query(payload):
response = requests.post(API_URL, headers=headers, json=payload, verify=False)
return response.json()
def get_response(prompt):
res = query({
"inputs": prompt,
"parameters": {
"max_new_tokens": 500
}
})
print(res)
return res
# CSS
local_css("style.css")
# サイドバー
with st.sidebar:
with st.form("form"):
st.header("ユーザー情報")
col1, col2 = st.columns(2)
with col1:
st.header("年齢")
st.header("性別")
st.header("体重 (kg)")
st.header("身長 (cm)")
st.header("活動レベル")
st.header("目標")
with col2:
age = st.number_input("年齢を入力してください", min_value=5, value=person_info["年齢"], step=1, label_visibility="collapsed")
gender = option = st.selectbox("性別を選んでください", gender_list, label_visibility="collapsed")
weight = st.number_input("体重を入力してください", value=person_info["体重"], min_value=20, step=1, label_visibility="collapsed")
height = st.number_input("身長を入力してください", value=person_info["身長"], min_value=100, step=1, label_visibility="collapsed")
activity_level = st.selectbox('活動レベルを選択してください', activity_level, help=activity_details, label_visibility="collapsed")
goal = st.selectbox('目標を選択してください', goal_list, label_visibility="collapsed")
c = st.columns((1,4,1))
with c[1]:
submitted = st.form_submit_button("登録", type="primary")
person_info["年齢"] = age
person_info["性別"] = gender
person_info["体重"] = weight
person_info["身長"] = height
person_info["活動レベル"] = activity_level
person_info["目標"] = goal
# メインコンテンツ
def home(person_info):
bmi, bmi_class = calculate_bmi(person_info)
bmr, tdee = energy_calc(person_info)
macros_req = macro_perc(person_info, tdee)
hc_theme = {'bgcolor': '#f9f9f9','title_color': 'orange','content_color': 'orange','icon_color': 'orange', 'icon': 'fa fa-question-circle'}
cols = st.columns(3)
with cols[0]:
if bmi_class == "標準体重":
hc.info_card(title='BMI', content=f'{round(bmi, 2)}' + " kcal", sentiment='good')
else:
hc.info_card(title='BMI', content=f'{round(bmi, 2)}', sentiment='bad')
with cols[1]:
hc.info_card(title='基礎代謝', content=f'{round(bmr, 2)} kcal',theme_override=hc_theme)
st.header("PFCバランス")
cols = st.columns(3)
theme_neutral = {'bgcolor': '#EFF8F7','title_color': 'green','content_color': 'green','icon_color': 'green', 'icon': 'fa fa-check-circle'}
with cols[0]:
hc.info_card(title='タンパク質', content=f'{round(macros_req["タンパク質"], 2)} g', theme_override=theme_neutral)
with cols[1]:
hc.info_card(title='脂質', content=f'{round(macros_req["脂質"], 2)} g',theme_override=theme_neutral)
with cols[2]:
hc.info_card(title='炭水化物', content=f'{round(macros_req["炭水化物"], 2)} g',theme_override=theme_neutral)
def diet(person_info):
search = st.text_input("食べた物を入力してください!", placeholder="焼きそば")
f = st.button("決定", type="primary")
if f:
with st.spinner(f"**{search}** について情報を探しています...."):
prompt = f"""
表形式で栄養データを生成してください。テキストではなくテーブルだけを返すようにします。
対象は{search}についてです。
テーブルは栄養データとして、以下の6つの特徴を必ず持ちます:
1.量
2.材料
3.タンパク質
4.脂質
5.炭水化物
6.カロリー
"""
res = get_response(prompt)
res = res[0]["generated_text"].split("\n\n ")[1]
st.write(res)
def plan(person_info):
with st.form("食事プラン"):
st.header("あなたの情報")
cols = st.columns(3)
with cols[0]:
loc = st.selectbox('あなたの住んでいる国はどこですか?', ["日本", "日本以外の国"])
with cols[1]:
remarks = st.text_input("食事の好み", placeholder="例:野菜が好き、肉が嫌い")
s = st.form_submit_button("食事プランをつくる", type="primary")
if s:
with st.spinner('プランを生成中です....'):
bmi, bmi_class = calculate_bmi(person_info)
bmr, tdee = energy_calc(person_info)
prompt = f"""
表形式で食事プランを生成してください。テキストではなくテーブルだけを返すようにします。
対象者の性別は{person_info["性別"]}、{loc}に住む人で、BMRが {bmr} kcal、 BMIは{bmi}、1日の消費エネルギーは{tdee} kcalの人です。
そして、食事プランのメニューに反映させるべき次の嗜好を持っています:{remarks}。
目標は、{person_info["目標"]}です。
テーブルは食事プランとして、以下の特徴を必ず持ちます:
1.食事時間(朝食、昼食、夕食の3回)
2.メニュー
3.材料
4.栄養素(g)
5.カロリー(kcal)
"""
res = get_response(prompt)
res = res[0]["generated_text"].split("```")[1]
st.write(res)
selected = option_menu(
menu_title=None,
options=["ホーム", "栄養計算", "食事プラン"],
icons=["house", "calculator", "envelope"],
menu_icon="cast",
default_index=0,
orientation="horizontal",
)
# 画面遷移
if selected == "ホーム":
home(person_info)
elif selected == "栄養計算":
diet(person_info)
elif selected == "食事プラン":
plan(person_info)
else:
st.error("あなたの情報を登録してください")
from config import *
# BMI判定
def calculate_bmi(person_info):
bmi = person_info["体重"] / (person_info["身長"] / 100) ** 2
if bmi < 18.5:
bmi_class = "低体重"
elif bmi < 25:
bmi_class = "標準体重"
elif bmi < 30:
bmi_class = "やや肥満"
else:
bmi_class = "肥満"
return bmi, bmi_class
# カロリー計算
def energy_calc(person_info):
if person_info["性別"] == "男性":
bmr = 88.362 + (13.397 * person_info["体重"]) + (4.799 * person_info["身長"] / 100) - (5.677 * person_info["年齢"])
else:
bmr = 447.593 + (9.247 * person_info["体重"]) + (3.100 * person_info["身長"] / 100) - (4.330 * person_info["年齢"])
tdee = bmr * activity_level_multipliers[person_info["活動レベル"]]
return bmr, tdee
# PFC計算
def macro_perc(person_info, calories):
if person_info["目標"].lower() == '体重減少':
protein_percentage = 30
fat_percentage = 25
elif person_info["目標"].lower() == '維持':
protein_percentage = 25
fat_percentage = 30
elif person_info["目標"].lower() == '筋肉増量':
protein_percentage = 35
fat_percentage = 20
else:
raise ValueError("無効な目標")
carb_percentage = 100 - (protein_percentage + fat_percentage)
protein = (protein_percentage / 100) * calories / 4
fat = (fat_percentage / 100) * calories / 9
carbs = (carb_percentage / 100) * calories / 4
return {'タンパク質': protein, '脂質': fat, '炭水化物': carbs}
# CSS読み込み
def local_css(file_name):
with open(file_name) as f:
st.markdown(f"<style>{f.read()}</style>", unsafe_allow_html=True)
PAGE_TITLE = "ダイエット支援LLMアプリ"
activity_level_multipliers = {
"とても低い": 1.2,
"やや低い": 1.375,
"普通": 1.55,
"やや高い": 1.725,
"とても高い": 1.9,
}
activity_details = """
とても低い: 全く運動しない \n
やや低い: 週に1,2回程度の運動 \n
普通: 週に3-5回程度の運動 \n
やや高い: 週に6,7回程度の運動 \n
とても高い: ハードなエクササイズや肉体労働
"""
gender_list = ['男性', '女性', 'その他']
goal_list = ['筋肉増量', '体重減少', '維持']
activity_level = ["とても低い", "やや低い", "普通", "やや高い", "とても高い"]
# Define the macronutrient percentages
macronutrient_percentages = {
"炭水化物": (45, 65),
"タンパク質": (10, 35),
"脂質": (20, 35),
}
# Set the person's information
person_info = {
"年齢": 25,
"性別": "Male",
"身長": 165,
"体重": 70,
"活動レベル": "普通",
"目標": "体重減少"
}
API_URL = "https://YOUR_ENDPOINT.endpoints.huggingface.cloud"
HF_TOKEN = "hf_XXXX"