This notebook shows how to build a retrieval model that can be used to retrieve recipes for a food recipe recommender system.
Introduction
In this analysis we will build a food recipe recommender system. We will build a user-item recommender system that can recommend recipes to users based on previous interactions. We will use collaborative filtering with implicit feedback where rated recipes are used as positive examples for a given user. So given a user input ID, the system will return some recipe IDs that might be of interest to that user based on that user’s rated recipes.
Recommender systems used in production typically consist of two phases:
The retrieval stage plays a crucial role in picking an initial set of several hundred candidates out of a vast pool of options. Its primary aim is to swiftly eliminate all candidates that fail to pique the user’s interest. Given that the retrieval model might need to sift through millions of candidates, it must be computationally efficient.
The next stage is the ranking stage, which further fine-tunes the outputs from the retrieval model to identify the optimal few recommendations. It will further narrow down the list of candidates retrieved during the retrieval stage and create a (ranked) shortlist of candidates.
In this analysis we will focus on building a retrieval model that can used for the retrieval stage.
Load Data
code
import osimport pprintimport sysfrom ast import literal_evalfrom typing import Dict, Textimport matplotlib.pyplot as pltimport numpy as npimport pandas as pdimport seaborn as snsimport tensorflow as tfimport warningswarnings.filterwarnings("ignore")
code
!pip install tensorflow-recommenders
Looking in indexes: https://pypi.org/simple, https://us-python.pkg.dev/colab-wheels/public/simple/
Collecting tensorflow-recommenders
Downloading tensorflow_recommenders-0.7.3-py3-none-any.whl (96 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 96.2/96.2 kB 3.1 MB/s eta 0:00:00
Requirement already satisfied: absl-py>=0.1.6 in /usr/local/lib/python3.9/dist-packages (from tensorflow-recommenders) (1.4.0)
Requirement already satisfied: tensorflow>=2.9.0 in /usr/local/lib/python3.9/dist-packages (from tensorflow-recommenders) (2.12.0)
Requirement already satisfied: tensorflow-io-gcs-filesystem>=0.23.1 in /usr/local/lib/python3.9/dist-packages (from tensorflow>=2.9.0->tensorflow-recommenders) (0.32.0)
Requirement already satisfied: flatbuffers>=2.0 in /usr/local/lib/python3.9/dist-packages (from tensorflow>=2.9.0->tensorflow-recommenders) (23.3.3)
Requirement already satisfied: h5py>=2.9.0 in /usr/local/lib/python3.9/dist-packages (from tensorflow>=2.9.0->tensorflow-recommenders) (3.8.0)
Requirement already satisfied: typing-extensions>=3.6.6 in /usr/local/lib/python3.9/dist-packages (from tensorflow>=2.9.0->tensorflow-recommenders) (4.5.0)
Requirement already satisfied: numpy<1.24,>=1.22 in /usr/local/lib/python3.9/dist-packages (from tensorflow>=2.9.0->tensorflow-recommenders) (1.22.4)
Requirement already satisfied: six>=1.12.0 in /usr/local/lib/python3.9/dist-packages (from tensorflow>=2.9.0->tensorflow-recommenders) (1.16.0)
Requirement already satisfied: jax>=0.3.15 in /usr/local/lib/python3.9/dist-packages (from tensorflow>=2.9.0->tensorflow-recommenders) (0.4.8)
Requirement already satisfied: wrapt<1.15,>=1.11.0 in /usr/local/lib/python3.9/dist-packages (from tensorflow>=2.9.0->tensorflow-recommenders) (1.14.1)
Requirement already satisfied: libclang>=13.0.0 in /usr/local/lib/python3.9/dist-packages (from tensorflow>=2.9.0->tensorflow-recommenders) (16.0.0)
Requirement already satisfied: setuptools in /usr/local/lib/python3.9/dist-packages (from tensorflow>=2.9.0->tensorflow-recommenders) (67.7.2)
Requirement already satisfied: tensorboard<2.13,>=2.12 in /usr/local/lib/python3.9/dist-packages (from tensorflow>=2.9.0->tensorflow-recommenders) (2.12.2)
Requirement already satisfied: astunparse>=1.6.0 in /usr/local/lib/python3.9/dist-packages (from tensorflow>=2.9.0->tensorflow-recommenders) (1.6.3)
Requirement already satisfied: gast<=0.4.0,>=0.2.1 in /usr/local/lib/python3.9/dist-packages (from tensorflow>=2.9.0->tensorflow-recommenders) (0.4.0)
Requirement already satisfied: packaging in /usr/local/lib/python3.9/dist-packages (from tensorflow>=2.9.0->tensorflow-recommenders) (23.1)
Requirement already satisfied: tensorflow-estimator<2.13,>=2.12.0 in /usr/local/lib/python3.9/dist-packages (from tensorflow>=2.9.0->tensorflow-recommenders) (2.12.0)
Requirement already satisfied: keras<2.13,>=2.12.0 in /usr/local/lib/python3.9/dist-packages (from tensorflow>=2.9.0->tensorflow-recommenders) (2.12.0)
Requirement already satisfied: termcolor>=1.1.0 in /usr/local/lib/python3.9/dist-packages (from tensorflow>=2.9.0->tensorflow-recommenders) (2.2.0)
Requirement already satisfied: grpcio<2.0,>=1.24.3 in /usr/local/lib/python3.9/dist-packages (from tensorflow>=2.9.0->tensorflow-recommenders) (1.53.0)
Requirement already satisfied: protobuf!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<5.0.0dev,>=3.20.3 in /usr/local/lib/python3.9/dist-packages (from tensorflow>=2.9.0->tensorflow-recommenders) (3.20.3)
Requirement already satisfied: opt-einsum>=2.3.2 in /usr/local/lib/python3.9/dist-packages (from tensorflow>=2.9.0->tensorflow-recommenders) (3.3.0)
Requirement already satisfied: google-pasta>=0.1.1 in /usr/local/lib/python3.9/dist-packages (from tensorflow>=2.9.0->tensorflow-recommenders) (0.2.0)
Requirement already satisfied: wheel<1.0,>=0.23.0 in /usr/local/lib/python3.9/dist-packages (from astunparse>=1.6.0->tensorflow>=2.9.0->tensorflow-recommenders) (0.40.0)
Requirement already satisfied: ml-dtypes>=0.0.3 in /usr/local/lib/python3.9/dist-packages (from jax>=0.3.15->tensorflow>=2.9.0->tensorflow-recommenders) (0.1.0)
Requirement already satisfied: scipy>=1.7 in /usr/local/lib/python3.9/dist-packages (from jax>=0.3.15->tensorflow>=2.9.0->tensorflow-recommenders) (1.10.1)
Requirement already satisfied: markdown>=2.6.8 in /usr/local/lib/python3.9/dist-packages (from tensorboard<2.13,>=2.12->tensorflow>=2.9.0->tensorflow-recommenders) (3.4.3)
Requirement already satisfied: tensorboard-plugin-wit>=1.6.0 in /usr/local/lib/python3.9/dist-packages (from tensorboard<2.13,>=2.12->tensorflow>=2.9.0->tensorflow-recommenders) (1.8.1)
Requirement already satisfied: tensorboard-data-server<0.8.0,>=0.7.0 in /usr/local/lib/python3.9/dist-packages (from tensorboard<2.13,>=2.12->tensorflow>=2.9.0->tensorflow-recommenders) (0.7.0)
Requirement already satisfied: google-auth-oauthlib<1.1,>=0.5 in /usr/local/lib/python3.9/dist-packages (from tensorboard<2.13,>=2.12->tensorflow>=2.9.0->tensorflow-recommenders) (1.0.0)
Requirement already satisfied: google-auth<3,>=1.6.3 in /usr/local/lib/python3.9/dist-packages (from tensorboard<2.13,>=2.12->tensorflow>=2.9.0->tensorflow-recommenders) (2.17.3)
Requirement already satisfied: requests<3,>=2.21.0 in /usr/local/lib/python3.9/dist-packages (from tensorboard<2.13,>=2.12->tensorflow>=2.9.0->tensorflow-recommenders) (2.27.1)
Requirement already satisfied: werkzeug>=1.0.1 in /usr/local/lib/python3.9/dist-packages (from tensorboard<2.13,>=2.12->tensorflow>=2.9.0->tensorflow-recommenders) (2.2.3)
Requirement already satisfied: rsa<5,>=3.1.4 in /usr/local/lib/python3.9/dist-packages (from google-auth<3,>=1.6.3->tensorboard<2.13,>=2.12->tensorflow>=2.9.0->tensorflow-recommenders) (4.9)
Requirement already satisfied: pyasn1-modules>=0.2.1 in /usr/local/lib/python3.9/dist-packages (from google-auth<3,>=1.6.3->tensorboard<2.13,>=2.12->tensorflow>=2.9.0->tensorflow-recommenders) (0.2.8)
Requirement already satisfied: cachetools<6.0,>=2.0.0 in /usr/local/lib/python3.9/dist-packages (from google-auth<3,>=1.6.3->tensorboard<2.13,>=2.12->tensorflow>=2.9.0->tensorflow-recommenders) (5.3.0)
Requirement already satisfied: requests-oauthlib>=0.7.0 in /usr/local/lib/python3.9/dist-packages (from google-auth-oauthlib<1.1,>=0.5->tensorboard<2.13,>=2.12->tensorflow>=2.9.0->tensorflow-recommenders) (1.3.1)
Requirement already satisfied: importlib-metadata>=4.4 in /usr/local/lib/python3.9/dist-packages (from markdown>=2.6.8->tensorboard<2.13,>=2.12->tensorflow>=2.9.0->tensorflow-recommenders) (6.4.1)
Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.9/dist-packages (from requests<3,>=2.21.0->tensorboard<2.13,>=2.12->tensorflow>=2.9.0->tensorflow-recommenders) (3.4)
Requirement already satisfied: urllib3<1.27,>=1.21.1 in /usr/local/lib/python3.9/dist-packages (from requests<3,>=2.21.0->tensorboard<2.13,>=2.12->tensorflow>=2.9.0->tensorflow-recommenders) (1.26.15)
Requirement already satisfied: charset-normalizer~=2.0.0 in /usr/local/lib/python3.9/dist-packages (from requests<3,>=2.21.0->tensorboard<2.13,>=2.12->tensorflow>=2.9.0->tensorflow-recommenders) (2.0.12)
Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.9/dist-packages (from requests<3,>=2.21.0->tensorboard<2.13,>=2.12->tensorflow>=2.9.0->tensorflow-recommenders) (2022.12.7)
Requirement already satisfied: MarkupSafe>=2.1.1 in /usr/local/lib/python3.9/dist-packages (from werkzeug>=1.0.1->tensorboard<2.13,>=2.12->tensorflow>=2.9.0->tensorflow-recommenders) (2.1.2)
Requirement already satisfied: zipp>=0.5 in /usr/local/lib/python3.9/dist-packages (from importlib-metadata>=4.4->markdown>=2.6.8->tensorboard<2.13,>=2.12->tensorflow>=2.9.0->tensorflow-recommenders) (3.15.0)
Requirement already satisfied: pyasn1<0.5.0,>=0.4.6 in /usr/local/lib/python3.9/dist-packages (from pyasn1-modules>=0.2.1->google-auth<3,>=1.6.3->tensorboard<2.13,>=2.12->tensorflow>=2.9.0->tensorflow-recommenders) (0.4.8)
Requirement already satisfied: oauthlib>=3.0.0 in /usr/local/lib/python3.9/dist-packages (from requests-oauthlib>=0.7.0->google-auth-oauthlib<1.1,>=0.5->tensorboard<2.13,>=2.12->tensorflow>=2.9.0->tensorflow-recommenders) (3.2.2)
Installing collected packages: tensorflow-recommenders
Successfully installed tensorflow-recommenders-0.7.3
code
import tensorflow_recommenders as tfrs
code
from google.colab import drivedrive.mount('/content/drive')
For this analysis we will the food recipe data set that was provided by [Majumder et al]. This dataset consists of 180K+ recipes and 700K+ recipe reviews covering 18 years of user interactions and uploads on Food.com.
The dataset can be downloaded from https://www.kaggle.com/datasets/shuyangli94/food-com-recipes-and-user-interactions
For this proof-of-concept analysis we will only use the first 10000 user interactions and recipes in order to speed up the analysis.
# Download dataset using Kaggle CLI# !kaggle datasets download shuyangli94/food-com-recipes-and-user-interactions
Exploratory data Analysis
Recommender systems typically suffer from the ‘long-tail’ problem. This refers to the situation where a large number of items have very few interactions or ratings, while a small number of items have a large number of interactions or ratings. This leads to a skewed distribution of data, where the majority of the recommendations are focused on popular items, and the niche or less popular items are often neglected. This poses a challenge for recommender systems because they need to provide relevant recommendations to users for all items, not just the popular ones.
For our food recipe recommender system this would mean that a large fraction of recipes have received few ratings and a small fraction of recipes has received a high number of ratings.
When we plot the number of reviews per recipe using a density plot, we can see that the food recipes data set also exhibits the long-tail pattern.
sns.kdeplot(counts_df)plt.xlabel('Number of reviews')plt.title('Number of reviews per recipe');
We can further zoom in and see that many reviews have 20 reviews or less, with a majority having received rating 0 or 1.
code
sns.histplot( counts_df, bins=800, discrete=True,)plt.xlabel('Number of reviews')plt.ylabel('Nr of recipes')plt.xlim(0,20)plt.title('Number of reviews per recipe (<20)');
Load item data (recipes)
We will load the recipe data into a Tensorflow data set and extract the recipe_id column. Note that the recipe IDs are imported as a string rather than an integer, which will become important when passing the values into the user model for our retrieval system.
code
def load_recipes(file_path=RECIPES_RAW, nrows=NROWS):"""Load the recipes data into Tensorflow dataset Args: file_path (str, optional): path to csv file with recipes data. nrows (int, optional): nr of rows to import. Defaults to 1000. Returns: Tensorflow.dataset: Tensorflow dataset """ recipes_df = pd.read_csv( file_path, usecols=["id", "name", "description", "tags", "steps", "ingredients"], nrows=nrows, ) recipes_df["name"] = recipes_df["name"] recipes_df["tags"] = recipes_df["tags"].apply(literal_eval).str.join(" ") recipes_df["description"] = recipes_df["description"] recipes_df["ingredients"] = ( recipes_df["ingredients"].apply(literal_eval).str.join(" ") ) recipes_df["steps"] = recipes_df["steps"].apply(literal_eval).str.join(" ") recipes_df = recipes_df[ ["id", "name", "tags", "description", "ingredients", "steps"] ] recipes_df.rename(columns={"id": "recipe_id"}, inplace=True) recipes_df["recipe_id"] = recipes_df["recipe_id"].astype(str) recipes_df.fillna("", inplace=True) recipes_ds = tf.data.Dataset.from_tensor_slices(dict(recipes_df))return recipes_ds
We wil alos load the user interacting data and extract columns user_id and recipe_id. Similarly as the recipe data, the user and recipe IDs are imported as strings rather than IDs.
We will shuffle the user interaction data and divide it into a training and test dataset using a 80/20 split.
code
NR_TRAIN =int(np.floor(0.8* NR_INTERACTIONS))NR_TEST = NR_INTERACTIONS - NR_TRAINprint(f"{NR_TRAIN} samples in train set")print(f"{NR_TEST} samples in test set")
7998 samples in train set
2000 samples in test set
For building a retrieval model, we will use a two-tower model as neural network architecture. The two-tower model is a type of neural network architecture that is commonly used in recommendation systems. It is so-called because it consists of two “towers” or branches of neural networks that process two different types of inputs.
The first tower is called the user tower (or query tower), and it takes in user-specific data, such as past behavior, and ratings history. The input data is typically represented as a sparse vector, where each entry corresponds to a particular feature or attribute of the user. The user tower maps this input vector into a lower-dimensional latent space, where each user is represented as a dense vector of continuous values.
The second tower is called the item tower (or candidate tower) and it takes in item-specific data, such as item descriptions, metadata, and attributes. The item tower similarly maps the input data into the same latent space as the user tower, where each item is represented as a dense vector of continuous values.
Once the user and item vectors are obtained, they are combined and passed through a final neural network layer to predict the user’s preference or likelihood of interacting with the item. This can be done using a variety of techniques, such as dot product or cosine similarity between the user and item vectors.
The two-tower model is powerful because it allows for efficient processing of sparse and high-dimensional input data, while also enabling the learning of non-linear interactions between user and item features.
Embedding dimensions
Before building the user and item models, we will get all unique user and recipe IDs, which will be used as fixed vocabularies for the StringLookup layer and are also necessary to determine the dimension of the subsequent Embedding layer.
Nr of unique recipes: 10000
Nr of unique users: 6451
code
embedding_dim =32
User tower
For the user tower we will build a user representation model which consists of two Keras layers: i.e. a StringLookup and an Embedding layer. The StringLookup layer is a preprocessing layer which maps string features (i.e. the user IDs) to integer indices. For creating the lookup indices, the unique user IDs are used as a fixed vocbulary, where each user ID in the vocabulary maps to a specific index. Those indices are then passed into the Embedding layer, which is a dense layer with a fixed dimensionality. We will also add \(L2\) regularization to the embedding layer to reduce the risk of overfitting the model.
This user model can then be used as query model, which computes the query representation (i.e. fixed-dimensionality embedding vector) and passes the features to the next layer.
code
user_model = tf.keras.Sequential( [ tf.keras.layers.StringLookup(vocabulary=unique_user_ids, mask_token=None), tf.keras.layers.Embedding(# add 1 to embedding dimension to account for unknown tokenslen(unique_user_ids) +1, embedding_dim, embeddings_regularizer=tf.keras.regularizers.L2(0.2) ), ])
Item tower
Similarly as the user tower, we will build an item model for the recipes, which will map all recipe IDs into an fixed-size embedding.
Thiis item model will then be used as candidate model, which computes the candidate representation for the recipes (i.e. fixed-dimensionality embedding vector) and passes the features to the next layer.
code
item_model = tf.keras.Sequential( [ tf.keras.layers.StringLookup(vocabulary=unique_recipe_ids, mask_token=None), tf.keras.layers.Embedding(# add 1 to embedding dimension to account for unknown tokenslen(unique_recipe_ids) +1, embedding_dim, embeddings_regularizer=tf.keras.regularizers.L2(0.2) ), ])
Retrieval Model
We can now put eveything together into the a TFRS model. TFRS classes are derived from Tensorflow base classes that can be used to combine an item model, user model and retrieval task into a retrieval model.
The retrieval task is an additional Keras layer that will be used as final layer to combine the user and item embeddings and compute the loss function and metrics.
The retrievavl task subsequently computes the tfrs.metrics.FactorizedTopK metric to compare candidate pairs and retrieve the top K candidates.
Essentially, the training dataset consists of pairs of positive (user ID, recipe ID) matches. To assess the efficacy of our model, we must compare the affinity score computed by the model for this pair against the scores of every other potential candidate pairs. If the score assigned to the positive pair surpasses that of all other candidates, it indicates high precision and accuracy in our model.
code
class RetrievalModel(tfrs.Model):"""Food recipe prediction model"""def__init__(self, user_model, item_model, batch_size=BATCH_SIZE):# Use init method from base class to set up model architecture.super().__init__()# Add recipe representations modelself.item_model = item_model# Add user representation mdodelself.user_model = user_model# Set batch sizeself.eval_batch_size = batch_size# The task computes the loss and metricsself.task = tfrs.tasks.Retrieval( metrics=tfrs.metrics.FactorizedTopK( candidates=recipes_ds.map(lambda x: x["recipe_id"]).batch(self.eval_batch_size).map(self.item_model) ) )def compute_loss(self, features: Dict[Text, tf.Tensor], training=False) -> tf.Tensor:# Get user features and pass them into the user model user_embeddings =self.user_model(features["user_id"])# Get recipe features and pass them into the recipe model item_embeddings =self.item_model(features["recipe_id"])# Compute the loss and the metrics.returnself.task(user_embeddings, item_embeddings)
We can initialize the model using the previously defined user and item models. We will also add the Adagrad optimizer to train the retrieval model.
code
model = RetrievalModel(user_model, item_model)model.compile(optimizer=tf.keras.optimizers.Adagrad(learning_rate=0.1))
Train retrieval model
We ca now fit the retrieval model on the training data.
# The retrieval model can be loaded from the saved weight as follows:#loaded_model = RetrievalModel(user_model, item_model)#loaded_model.load_weights('/content/drive/MyDrive/projects/food_recsys/retrieval_model.weights.tf)
We can plot the loss and top K accuracy to assess training performance. We can see that loss is decreasing and accuracy is increasing during training.
sns.barplot(results_df)plt.title('Performance on test set', fontsize=18, fontweight='bold')plt.xlabel('top k', fontsize=14)plt.ylabel('categorical accuracy', fontsize=14);
We can observe that our retrieval model performs worse on the test set compared to the training set. than training performance. This is mainly due to the following two factors:
The model is overfitting because it is memorizing data that is has seen. Models with many parameters are prone to overfitting. We can mediate this by increasing the regularization in the embedding layers in the user and item models. We could also add an additional Dropout layer to both models.
The model is re-recommending recipes that some users already rated. These known positives can push out the new test recipes from the top K recommendations. This could be mitigated by removing previously seen recipes from the test dataste.
Model predictions
We can now use the retrieval model to recommend recipe IDs for a given user ID.
To annotate the retrieved recipe IDs, we will create a lookup table which maps recipe IDs to their description.
Additionally we will still need to create an index for our dataset, which can then be used to retrieve the entires as predicted by the retrieval model.
code
def recommend_recipes( user_id, item_ds=recipes_ds, item_id='recipe_id', mapping_df=mapping_df, retrieval_model=model, nr_recommendations=10, batch_size=BATCH_SIZE): index = tfrs.layers.factorized_top_k.BruteForce( retrieval_model.user_model, nr_recommendations )# Recommends recipes out of the entire recipe itemset. index.index_from_dataset( tf.data.Dataset.zip( ( item_ds.map(lambda x: x[item_id]).batch(batch_size), item_ds.map(lambda x: x[item_id]) .batch(batch_size) .map(retrieval_model.item_model), ) ) )# Get recommended recipes IDs from index _, ids = index(tf.constant([user_id])) id_list = ids[0:nr_recommendations].numpy().astype("int64").tolist()[0]# Annotate retrieved IDs with metadatareturn mapping_df[mapping_df["id"].isin(id_list)][["id", "name", "description"]]
We can now take a random user ID from the test set and recommend some recipe IDs, together with recipe name and description.
a wonderful light dessert recipe from the people at kraft canada and adapted by me to be feed a few more. very tasty. (cooking time is refrigeration time)
446919
30 minute chili
this chili tastes remarkably good and can be made for dinner when you are pressed for time. the recipe comes from the beefitswhatfordinner.com website, but i originally saw the recipe in the newpaper. i have made this several times, used whatever beef i had on hand. i've also added more beans and tomatoes to the same quantity of beef, and it still tasted great.
404323
a different porridge
from recipes+ magazine
229794
a very interesting smoked salmon sandwich
an unusual combination of ingredients make a nice change as antony worrall thompson shares his recipe for a sublime open sandwich, piled high with smoked salmon, crispy bacon, cream cheese and more!
171619
algerian cucumber salad
posted for the zaar world tour. a yummy salad!
213671
all bran pancake
healthy
131359
all in one warm artichoke bread dip
this is called 'all in one', as the ingredients are all measured in "ones". a nice change from the standard spinach bread dip. great for parties. i also change the monterey jack cheese from time to time, trying farmer's cheese, mozzarella, havarti, (pretty much any mild white cheese will do!)
295701
all purpose house seasoning
this is my very own all purpose seasoning blend that i use on a daily basis. will store on the counter up to 2 months. will keep in the freezer for up to 12 months. use anyplace you would use regular salt and pepper. feel free to experiment with your favorite dried herbs and spices. i also keep smaller batches (just split a recipe) on the counter with 1 strip of dried lemon and another with 1 strip of dried lime in it. simply peel a strip of the zest and leave it on the counter overnight to dehydrate it. i have yet another with dried mint in it.
201614
almond tea cookies
these easy crispy almond cookies are perfect with a cup of tea.
63426
american blessing mix
i found this on the net
24357
anglesey eggs
anglesey, separated from north wales by the menai strait, is a large island extending into the irish sea. anglesey eggs, a simple and tasty way of using up left over potatoes and heels of cheese, have been enjoyed for tea or supper by many welsh and english vacationers here.
323576
apple and celery salad
use a good flavored apple for this old favorite such as fuji, gala, granny smith, jonathan, cortland, empire or winesap. from the mississippi valley chapter of the united states regional cookbook, culinary arts institute of chicago, 1947. chilling time not included in preparation time.
367080
apple blossom
a recipe from the bartender's guide. it has a great taste but very potent. feel free to add more apple juice. it's not sweet at all. so adjust to your own taste.
344382
apple pie a la mode shake
breakfast or dessert! yum
232077
apricot and coconut balls
easy and yummy treat
428522
artichoke caviar
this tart and tangy appetizer is made from finely chopped artichokes combined with other tasty greek favorites.
64945
artichoke dip
yummm.... posted in reply to request.
92932
arugula rocket and parmesan salad
from the sopranos family cookbook
304974
asian noodle salad
this is yet another version of asian salad. i love snow peas when they are in season so i cannot resist adding them to this salad. we enjoy this dish in the summer and spring for our main dish, and absolutely love it.
354530
asian pasta with tofu shiitake mushrooms and broccoli
a great healthy recipe from the ny times.
(you can prepare the ingredients and blanch the broccoli hours ahead of cooking the dish. the stir-frying is a last-minute operation.)
Bibliography
Majumder, Bodhisattwa Prasad, et al. “Generating personalized recipes from historical user preferences.” arXiv preprint arXiv:1909.00105 (2019).