SF Event Recommender utilizzando LLM

Feb 26 2023
I Large Language Models possono essere utilizzati in ogni fase del processo per lo sviluppo di un sistema di raccomandazioni. In questo progetto ho utilizzato i modelli di OpenAI per sintetizzare, formattare, archiviare e applicare incorporamenti sui dati per personalizzare i consigli per me stesso.

I Large Language Models possono essere utilizzati in ogni fase del processo per lo sviluppo di un sistema di raccomandazioni. In questo progetto ho utilizzato i modelli di OpenAI per sintetizzare, formattare, archiviare e applicare incorporamenti sui dati per personalizzare i consigli per me stesso.

San Francisco ha un sacco di eventi, ma sfortunatamente i residenti devono cercare molto per trovare eventi su base settimanale. La maggior parte degli eventi può essere trovata sui siti Web delle sedi o sui siti di pubblicazione di eventi gratuiti. Tuttavia, affinché la maggior parte dei siti riceva consigli personalizzati, devi pagare. In quanto residente a San Francisco, volevo affrontare il problema di creare il mio sistema di raccomandazione di eventi senza dover fare affidamento su servizi a pagamento o affrontare la briga di setacciare numerosi siti Web basati su eventi e pagine Instagram.

Ho raschiato informazioni sull'evento da SF Fun Cheap, Eventbrite e 19hz (una pagina di eventi di musica di fantascienza incentrata su EDM e techno). Ho aggiunto una rapida demo dell'interfaccia frontend che ho creato per me stesso.

Cerchiamo di capire cosa sta accadendo nel backend di questa semplice interfaccia per capire meglio come questi eventi vengono abbinati alle mie preferenze e aggiornati dinamicamente.

Elaborazione dati con LLM

La prima sfida nella creazione di questa app è l'analisi dei dati raschiati in un formato standardizzato. Un LLM può essere molto utile non solo per formattare i dati, ma anche per creare più funzionalità per il modello. Ad esempio, in ciascuno degli scraper di eventi ho utilizzato GPT-3 per analizzare i dati in un formato JSON.

def generate_prompt(event):
    prompt = """Extract information from the event below and store it into a 
JSON with the following structure 
{venue: venue_name, date: date_and_time, age: age_requirement, price: price, location: address}""" + 
' and enclose each key and value in the JSON structure with double quotes.' + 
'\n' + "Event: " + event
    return prompt


sf_events = pd.DataFrame()

for link,headline in zip(hrefs,headlines):
  headline = headline.strip()
  if link is not None and len(headline) > 0: 
    response = requests.get(link)
    html = response.content
    soup = BeautifulSoup(html, 'html.parser')
    p_tags = soup.find_all('p')
    event_name = soup.find('h1')
    event_name = event_name.get_text()
    text = ''
    div = soup.find('div', {'id': 'stats'})
    text += div.get_text()
    
    response = openai.Completion.create(
    model="text-davinci-003",
    prompt=generate_prompt(text),
    temperature=0.0,
    max_tokens = 200,
    )

    response_text = response['choices'][0]['text'].strip()
    response_dict = json.loads(response_text)

    sf_events = sf_events.append(response_dict, ignore_index=True)

def find_venue_type(venue):
    prompt = f'Provide a short description of the following venue in SF? 
Utilize the venues website or online search to find the answer.' + '\n' + 
'Venue: ' + venue
    return prompt

venue_description = {}
for index, row in sf_events.iterrows():
    if row['venue'] not in venue_description:
        response = openai.Completion.create(
            model="text-curie-001",
            prompt=find_venue_type(row['venue']),
            temperature=0.3,
            max_tokens = 100,
        )
        response_text = response['choices'][0]['text'].strip()
        venue_description[row['venue']] = response_text
        sf_events.loc[index, 'venue_description'] = response_text

    else:
        sf_events.loc[index, 'venue_description'] = venue_description[row['venue']]

Poiché ho a che fare con un solo utente, il sistema di raccomandazione viene elaborato tramite una ricerca di somiglianza. Per eseguire una ricerca di somiglianza, i dati dell'evento e dell'utente devono essere convertiti in un formato numerico noto anche come incorporamenti. Per ciascuna delle origini dati eseguo il seguente processo di incorporamento.

import pandas as pd
import tiktoken
from openai.embeddings_utils import get_embedding
import os
import openai

openai.apikey = os.getenv("OPENAI_API_KEY")
# embedding model parameters
embedding_model = "text-embedding-ada-002"
embedding_encoding = "cl100k_base"  # this the encoding for text-embedding-ada-002
max_tokens = 5000  # the maximum for text-embedding-ada-002 is 8191

fc_df_embeddings = sf_events.applymap(str)
encoding = tiktoken.get_encoding(embedding_encoding)
sf_events["combined"] = (
    "Venue: " + fc_df_embeddings.venue.str.strip() + "; Venue Description: " + fc_df_embeddings.venue_description.str.strip() + 
    "; Event: " + fc_df_embeddings.event_name.str.strip() +  "; Event Description: " + fc_df_embeddings.event_description.str.strip() + 
    "; Price: " + fc_df_embeddings.price.str.strip() + "; Neighborhood: " + fc_df_embeddings.neighborhood.str.strip()
)

sf_events["n_tokens"] = sf_events.combined.apply(lambda x: len(encoding.encode(x)))
sf_events["embedding"] = sf_events.combined.apply(lambda x: get_embedding(x, engine=embedding_model))

event_embeddings = sf_events.embedding.apply(eval).apply(np.array)
event_embeddings = np.array(list(event_embeddings))
# User Embeddings

interests = ['Live music', 'DJ performances', 'Dancing', 'Karaoke', 'Comedy shows', 'Trivia nights', 'Art exhibits', 'Film screenings', 'Wine tasting', 'Craft beer tasting', 'Mixology classes', 'Food events', 'Sports viewing parties', 'Networking events', 'Tech events']
venue_types = ['Speakeasy Bar', 'Dive Bar', 'Nightclubs', 'Music Venue', 'Comedy Club', 'Theaters', 'Art Gallery', 'Film theaters', 'Wineries', 'Breweries', 'Cocktail Bars', 'Sports Bars', 'Restaurants with Live Music', 'Rooftop bars', 'Convention Centers']
price_range_pref = ['free', '$10-$20', '$20-$50', '$50-$100', 'over $100']
time_of_day_pref = ['daytime', 'evening', 'night']
neighborhoods_of_interest = ['Financial District', 'Nob Hill', 'Pacific Heights', 'Russian Hill', 'North Beach', 'The Mission', 'Castro', 'Haight-Ashbury', 'SoMa', 'Marina']

user_interests = ['Live music', 'DJ performances', 'Comedy shows', 'Food events', 'Networking events', 'Tech events']
user_venues = ['Speakeasy Bar', 'Dive Bar','Music Venue', 'Comedy Club', 'Cocktail Bars', 'Theaters']
user_prices = ['free', '$10-$20', '$20-$50']
user_time = ['evening', 'night']
user_neighborhoods = ['Nob Hill', 'Pacific Heights', 'Russian Hill', 'North Beach', 'The Mission', 'Castro', 'Haight-Ashbury']
encoding = tiktoken.get_encoding(embedding_encoding)
combined = ("Event Preferences: " + str(user_interests) +  "; Venue Preferences: " + str(user_venues) + 
    "; Price Range Preferences: " + str(user_prices) +  "; Time of Day Preferences: " + str(user_time) + 
     "; Neighborhoods of Interest: " + str(user_neighborhoods))

user_embedding = get_embedding(combined, engine=embedding_model)
user_embedding = np.array(user_embedding)

Una volta che le preferenze dell'utente sono state combinate e incorporate insieme a ciascun evento dalle pagine degli eventi raschiate. Posso quindi applicare una formula di somiglianza del coseno sugli incorporamenti. La somiglianza del coseno misura la somiglianza tra due vettori utilizzando la seguente formula:

coseno_somiglianza(a, b) = (a · b) / (||a|| ||b||)

I punteggi di somiglianza sono compresi tra [-1, 1] dove 1 significa che i vettori sono gli stessi. Ho calcolato la matrice di somiglianza con la seguente funzione e quindi ho archiviato i dati degli eventi insieme alla somiglianza e alla distanza dall'evento in un database MySQL.

Ogni 2 settimane eseguo uno script di automazione per raschiare i dati e ricreare la tabella degli eventi nel mio database.

from sklearn.metrics.pairwise import cosine_similarity
similarity_matrix = cosine_similarity(user_embedding.reshape(1, -1), event_embeddings)


conn = pymysql.connect(host='localhost', user='root', password=mysql_pass, db='mysfevents')
cursor = conn.cursor()

create_table_query = """CREATE TABLE events (
                        id INT AUTO_INCREMENT PRIMARY KEY,
                        venue VARCHAR(150),
                        event VARCHAR(150),
                        date VARCHAR(150),
                        age VARCHAR(150),
                        price VARCHAR(150),
                        location VARCHAR(150),
                        venue_desc VARCHAR(3000),
                        event_desc VARCHAR(5000),
                        neighborhood VARCHAR(200),
                        distance FLOAT,
                        similarity FLOAT
                        );"""


cursor.execute(create_table_query)
conn.commit()

sql_query = "INSERT INTO events (venue, event, date, age, price, location, venue_desc, event_desc, neighborhood, distance, similarity) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)"

data = database_df.to_records(index=False).tolist()


cursor.close()
conn.close()


from math import radians, sin, cos, sqrt, atan2
from shapely.geometry import Point
from shapely.wkt import loads


my_loc = Point(37.7652844,-122.4329059)


def distance_in_miles(loc1, loc2):

    loc2 = loads(loc2)
    """Calculate the distance in miles between two geolocation points using the Haversine formula."""
    # Convert coordinates to radians
    lon1, lat1 = radians(loc1.x), radians(loc1.y)
    lon2, lat2 = radians(loc2.x), radians(loc2.y)


    # Haversine formula
    dlon = lon2 - lon1
    dlat = lat2 - lat1
    a = sin(dlat / 2) ** 2 + cos(lat1) * cos(lat2) * sin(dlon / 2) ** 2
    c = 2 * atan2(sqrt(a), sqrt(1 - a))
    distance_miles = 3963.0 * c # Radius of Earth in miles

    return float(round(distance_miles,3))

df['distance_miles'] = df.apply(lambda row: distance_in_miles(my_loc, row['geolocation']), axis=1)

Dopo aver caricato i dati in un database ho creato un framework frontend e backend utilizzando Node.js e Javascript per visualizzare gli eventi classificati in base ai punteggi di somiglianza. Volevo che i miei consigli imparassero dinamicamente e si adattassero mentre interagivo con il frontend (simile alla maggior parte dei sistemi di raccomandazione). La maggior parte delle aziende come Netflix e Tiktok utilizza questo concetto di intelligenza artificiale invisibile che tiene conto delle interazioni degli utenti con la loro interfaccia utente visibile per personalizzare i consigli. Tiktok utilizza la cronologia delle visualizzazioni e delle ricerche per consigliare il video successivo, mentre Netflix utilizza la cronologia delle visualizzazioni, gli elenchi dei Mi piace degli utenti e il filtro collaborativo per consigliare film e programmi in tempo reale.

Nella mia applicazione ho creato un pulsante Mi piace in cui ogni volta che mi piace un evento sulla mia pagina principale, aggiunge EventID a una tabella degli eventi apprezzati ed esegue uno script separato in parallelo che incorpora gli eventi apprezzati nei miei incorporamenti per ricalcolare la ricerca di similitudine L'obiettivo è adattare i consigli alle mie preferenze e agli eventi che mi sono piaciuti finora. Ecco come realizzo questi cambiamenti in modo dinamico.

# This is the front end component 
<tbody id="events-table">
      <% events.forEach(event => { %>
        <tr>
          <td><%= event.id %></td>
          <td><%= event.venue %></td>
          <td><%= event.event %></td>
          <td><%= event.event_desc %></td>
          <td><%= event.date %></td>
          <td><%= event.price %></td>
          <td><%= event.location %></td>
          <td><%= event.distance %></td>
          <td>
            <form action="/like" method="post" class="like-form">
              <input type="hidden" name="eventId" value="<%= event.id %>">
              <button type="submit" class="like-button">Like</button>
            </form>
          </td>
        </tr>
      <% }) %>
    </tbody>
# This is the backend component

const { spawn } = require('child_process');

app.post('/like', (req, res) => {
  console.log(req.body)
  const eventId = req.body.eventId;
  const query = 'INSERT INTO liked_events (event_id) VALUES (?)';
  connection.query(query, [eventId], (err, result) => {
    if (err) throw err;
    const pyScript = spawn('python', ['./update_recs.ipynb']);
    # This is where I call the python script to reassess similarity scores

    pyScript.stdout.on('data', (data) => {
      console.log(`stdout: ${data}`);
    });

    pyScript.stderr.on('data', (data) => {
      console.error(`stderr: ${data}`);
    });

    pyScript.on('close', (code) => {
      console.log(`child process exited with code ${code}`);
      res.redirect('/');
    });
  });
});
# Invisible AI Component

liked_events_mask = np.isin(event_ids, liked_event_ids)
masked_event_embeddings = np.copy(event_embeddings)
masked_event_embeddings[liked_events_mask] = 0.0
user_embedding_reshaped = np.reshape(user_embedding, (1, -1))

all_embeddings = np.concatenate([user_embedding_reshaped, masked_event_embeddings])

similarity_scores = cosine_similarity(all_embeddings)[0][1:]

for i, event_id in enumerate(event_ids):
    similarity = similarity_scores[i]
    sql = "UPDATE events SET similarity = %s WHERE id = %s"
    val = (similarity, event_id)
    cursor.execute(sql, val)

  1. Il codice quindi crea una copia dell'array event_embeddingse la assegna alla masked_event_embeddingsvariabile.
  2. Il codice imposta gli incorporamenti di tutti gli eventi che sono stati apprezzati dall'utente (come indicato dall'array liked_events_mask) su zero nell'array masked_event_embeddings. Questo effettivamente "maschera" gli incorporamenti di questi eventi in modo che non contribuiscano al calcolo della somiglianza.
  3. Il codice quindi rimodella l' user_embeddingarray in una matrice a 1 riga e lo assegna alla user_embedding_reshapedvariabile.
  4. Infine, il codice concatena la user_embedding_reshapedmatrice rimodellata e la masked_event_embeddingsmatrice (che ora contiene gli embedding degli eventi mascherati), creando una nuova matrice all_embeddingsche rappresenta le preferenze dell'utente e gli embedding di tutti gli eventi che non sono stati apprezzati dall'utente.

Conclusione e prossimi passi

L'obiettivo di questo progetto era creare un utile motore di raccomandazione di eventi per me stesso per trovare eventi sia professionali che divertenti a livello locale. Volevo anche fornire informazioni sulla crescita dei modelli di trasformatore e su come questi possono essere utilizzati in ogni fase di un'applicazione end-to-end. Ho utilizzato un LLM per pre-elaborare i dati scartati, archiviarli, creare funzionalità, incorporarli e infine creare consigli con tali incorporamenti. Sebbene ci siano dei costi per l'utilizzo di questi modelli, questo progetto evidenzia quanto questi modelli possano essere importanti se incorporati correttamente nell'architettura complessiva di un sistema.

Repository GitHub:https://github.com/GauravMohan1/sfevents

Linkedin:https://www.linkedin.com/in/gaurav-mohan/