SF Event Recommender mit LLMs

Feb 26 2023
Große Sprachmodelle können in jedem Schritt des Prozesses zur Entwicklung eines Empfehlungssystems verwendet werden. In diesem Projekt habe ich die Modelle von OpenAI verwendet, um die Daten zu synthetisieren, zu formatieren, zu speichern und Einbettungen anzuwenden, um Empfehlungen für mich selbst anzupassen.

Große Sprachmodelle können in jedem Schritt des Prozesses zur Entwicklung eines Empfehlungssystems verwendet werden. In diesem Projekt habe ich die Modelle von OpenAI verwendet, um die Daten zu synthetisieren, zu formatieren, zu speichern und Einbettungen anzuwenden, um Empfehlungen für mich selbst anzupassen.

In San Francisco gibt es jede Menge Veranstaltungen, aber leider müssen die Bewohner jede Woche sehr lange suchen, um Veranstaltungen zu finden. Die meisten Veranstaltungen finden Sie auf den Websites der Veranstaltungsorte oder auf kostenlosen Websites zur Veröffentlichung von Veranstaltungen. Allerdings müssen die meisten Websites bezahlen, um personalisierte Empfehlungen zu erhalten. Als SF-Bewohner wollte ich das Problem angehen, mein eigenes Event-Empfehlungssystem zu erstellen, ohne auf kostenpflichtige Dienste angewiesen zu sein oder mir die Mühe zu machen, zahlreiche veranstaltungsbasierte Websites und Instagram-Seiten zu durchforsten.

Ich habe Veranstaltungsinformationen von SF Fun Cheap, Eventbrite und 19hz (einer EDM- und Techno-fokussierten SF-Musikveranstaltungsseite) zusammengetragen. Ich habe eine kurze Demo der Frontend-Schnittstelle hinzugefügt, die ich für mich selbst erstellt habe.

Lassen Sie uns verstehen, was im Backend dieser einfachen Schnittstelle passiert, um besser zu verstehen, wie diese Ereignisse an meine Präferenzen angepasst und dynamisch aktualisiert werden.

Datenverarbeitung mit LLMs

Die erste Herausforderung beim Erstellen dieser App besteht darin, die extrahierten Daten in ein standardisiertes Format zu übertragen. Ein LLM kann nicht nur sehr hilfreich sein, um Daten zu formatieren, sondern auch mehr Funktionen für das Modell zu erstellen. Beispielsweise habe ich in jedem der Event-Scraper GPT-3 verwendet, um die Daten in ein JSON-Format zu analysieren.

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']]

Da ich es nur mit einem Nutzer zu tun habe, erfolgt die Abwicklung des Empfehlungssystems über eine Ähnlichkeitssuche. Um eine Ähnlichkeitssuche durchzuführen, müssen die Ereignis- und Benutzerdaten in ein numerisches Format, auch Einbettungen genannt, konvertiert werden. Für jede der Datenquellen führe ich den folgenden Einbettungsprozess durch.

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)

Sobald die Benutzereinstellungen kombiniert und zusammen mit jedem Ereignis von den gecrackten Ereignisseiten eingebettet sind. Ich kann dann eine Kosinus-Ähnlichkeitsformel auf die Einbettungen anwenden. Die Kosinusähnlichkeit misst die Ähnlichkeit zwischen zwei Vektoren mithilfe der folgenden Formel:

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

Die Ähnlichkeitswerte liegen zwischen [-1, 1], wobei 1 bedeutet, dass die Vektoren gleich sind. Ich habe die Ähnlichkeitsmatrix mit der folgenden Funktion berechnet und dann die Ereignisdaten zusammen mit der Ähnlichkeit und Entfernung vom Ereignis in einer MySQL-Datenbank gespeichert.

Alle zwei Wochen führe ich ein Automatisierungsskript aus, um die Daten zu extrahieren und die Ereignistabelle in meiner Datenbank neu zu erstellen.

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)

Nachdem ich die Daten in eine Datenbank geladen hatte, erstellte ich mit Node.js und Javascript ein Frontend- und Backend-Framework, um die Ereignisse nach Ähnlichkeitswerten sortiert anzuzeigen. Ich wollte, dass meine Empfehlungen dynamisch lernen und sich anpassen, während ich mit dem Frontend interagiere (ähnlich wie bei den meisten Empfehlungssystemen). Die meisten Unternehmen wie Netflix und Tiktok nutzen dieses Konzept der unsichtbaren KI , das Benutzerinteraktionen mit ihrer sichtbaren Benutzeroberfläche berücksichtigt, um maßgeschneiderte Empfehlungen zu erstellen. Tiktok nutzt den Anzeige- und Suchverlauf, um das nächste Video zu empfehlen, während Netflix den Anzeigeverlauf, die Like-Listen des Benutzers und die kollaborative Filterung nutzt, um Filme und Sendungen in Echtzeit zu empfehlen.

In meiner Anwendung habe ich eine „Gefällt mir“-Schaltfläche erstellt, die jedes Mal, wenn ich ein Ereignis auf meiner Hauptseite mit „Gefällt mir“ markiert, die EventID zu einer Tabelle mit „Gefällt mir“-Ereignissen hinzufügt und parallel ein separates Skript ausführt, das die „Gefällt mir“-Ereignisse in meine Einbettungen einbezieht, um sie neu zu berechnen die Ähnlichkeitssuche. Ziel ist es, die Empfehlungen auf meine Vorlieben und die Veranstaltungen abzustimmen, die mir bisher gefallen haben. So erreiche ich diese Änderungen dynamisch.

# 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. Der Code erstellt dann eine Kopie des event_embeddingsArrays und weist sie der masked_event_embeddingsVariablen zu.
  2. Der Code setzt die Einbettungen aller Ereignisse, die dem Benutzer gefallen haben (wie durch das liked_events_maskArray angegeben), im masked_event_embeddingsArray auf Null. Dadurch werden die Einbettungen dieser Ereignisse effektiv „maskiert“, sodass sie nicht zur Ähnlichkeitsberechnung beitragen.
  3. Anschließend formt der Code das user_embeddingArray in eine einzeilige Matrix um und weist sie der user_embedding_reshapedVariablen zu.
  4. Schließlich verkettet der Code die umgeformte user_embedding_reshapedMatrix und die masked_event_embeddingsMatrix (die jetzt die maskierten Ereigniseinbettungen enthält) und erstellt so eine neue Matrix, all_embeddingsdie die Präferenzen des Benutzers und die Einbettungen aller Ereignisse darstellt, die dem Benutzer nicht gefallen haben.

Fazit und nächste Schritte

Das Ziel dieses Projekts bestand darin, eine nützliche Event-Empfehlungsmaschine für mich zu entwickeln, um sowohl beruflich ausgerichtete als auch unterhaltsame Events vor Ort zu finden. Ich wollte auch Einblicke in die Entwicklung von Transformatormodellen geben und wie diese in jedem Schritt einer End-to-End-Anwendung genutzt werden können. Ich habe ein LLM verwendet, um Scraped-Daten vorzuverarbeiten, zu speichern, Features zu erstellen, sie einzubetten und schließlich Empfehlungen mit diesen Einbettungen zu erstellen. Obwohl die Verwendung dieser Modelle mit Kosten verbunden ist, zeigt dieses Projekt, wie wichtig diese Modelle sein können, wenn sie korrekt in die Gesamtarchitektur eines Systems integriert werden.

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

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