I am trying to use LSH algorithm for recommendation model. How can I careate model.tar.gz from that to be used for aws sagemaker later. Here's the ciurrent code: ```import numpy as np
import pandas as pd
from sklearn.random_projection import SparseRandomProjection
from sklearn.metrics.pairwise import cosine_similarity
import time

class LSH:
    def __init__(self, n_components, n_hashes):
        
        # Dimensionalty of projected data
        self.n_components = n_components
        
        # number of buckets
        self.n_hashes = n_hashes
        
        # https://scikit-learn.org/stable/modules/generated/sklearn.random_projection.SparseRandomProjection.html
        self.hash_tables = [SparseRandomProjection(n_components=n_components) for _ in range(n_hashes)]

    def fit(self, data):
        if isinstance(data, pd.DataFrame):
            self.item_ids = data.index.tolist()
            data = data.values
        else:
            self.item_ids = np.arange(len(data))
        
        # initialize the hash buckets
        self.hash_buckets = [{} for _ in range(self.n_hashes)]
        for i, hash_table in enumerate(self.hash_tables):
            # For each hash table the function applies SparseRandomProjection. 
            # This step reduce the dimentionality of the data while tryigng to preserve the relative distance between points.
            # Purpose: approximate high-dimentional to low-dimensional data and similar items stays together.
            projections = hash_table.fit_transform(data)
            for j, projection in enumerate(projections):
                # After projecting data for each hash table, the funtion iterates over these projections.
                # For each data points projection, it creates a hash key. The key is generate by the sign of each component.
                # The idea is if two points are close in high-dimensional space, their projections must also be closed.
                # Ex projection: [ 0.72199595 -0.6867862  -0.5098184   0.55619763  4.24497602  1.08506354
                # -2.26010595 -2.79241899  6.01301565 -4.82428779  1.05509869 -2.29027826
                # -2.13740946  1.06065566 -1.57524544  0.58627834]
                # Ex hash_key: (1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1)
                hash_key = tuple(np.where(projection > 0, 1, 0))
                if hash_key not in self.hash_buckets[i]:
                    # Create the item with the hash_key
                    self.hash_buckets[i][hash_key] = []
                
                # Append to that specific hash_bucket, j, with the unique and shared hash_key 
                self.hash_buckets[i][hash_key].append(j)
        return self.hash_buckets

    def query(self, point):
        if isinstance(point, pd.Series):
            point = point.to_numpy().reshape(1, -1)  # Correct way to handle Series
        elif isinstance(point, np.ndarray):
            point = point.reshape(1, -1)
        
        # To eliminate the chance of duplicates
        similar_items = set()
        for i, hash_table in enumerate(self.hash_tables):
            projection = hash_table.transform(point)
            hash_key = tuple(np.where(projection > 0, 1, 0)[0])
            similar_items.update(self.hash_buckets[i].get(hash_key, []))

        similar_item_ids = [self.item_ids[i] for i in similar_items]
        return similar_item_ids

def find_top_similar(data, query_input, similar_inputs, top_n=10):
    if isinstance(data, pd.DataFrame):
        query_item = data.loc[query_input].values.reshape(1, -1)
    else:
        query_item = data[query_input].reshape(1, -1)

    similarities = []
    for input_ in similar_inputs:
        if input_ != query_input:
            if isinstance(data, pd.DataFrame):
                item = data.loc[input_].values.reshape(1, -1)
            else:
                item = data[input_].reshape(1, -1)
            similarity_score = cosine_similarity(query_item, item)[0][0]
            similarities.append((input_, similarity_score))

    sorted_similarities = sorted(similarities, key=lambda x: x[1], reverse=True)
    top_similar_indices = [item[0] for item in sorted_similarities[:top_n]]
    return top_similar_indices

# Params
n_components = int(0.1 * n_features)
n_hashes = 15

# DataFrame
lsh = LSH(n_components=n_components, n_hashes=n_hashes)
hash_buckets = lsh.fit(data)

top_n = 20
query_id = data.index[2]
start_time = time.time()
similar_ids = lsh.query(data.loc[query_id])
end_time = time.time()
print("Time taken to query similar items (DataFrame):", end_time - start_time, "seconds")

top_n_similar_ids = find_top_similar(data, query_id, similar_ids, top_n=top_n)
print(f"Top {top_n} Similar Items (DataFrame) for {query_id}:", top_n_similar_ids)```