import numpy as np 
import pandas as pd

Anbefalingssystemer#

Vi bruker anbefalingssystemer hele tiden og nå skal dere lære hvordan de fungerer. Bedrifter bruker anbefalingssystemer for å presentere kunder for produkter eller tjenester de sannsynligvis vil kjøpe, basert på tidligere kjøp eller søkevaner. Dette er spesielt viktig for e-handel, streaming-tjenester, og reklame.

Det finnes to hovedmetoder som blir brukt for å predikere vurderinger.

I innholdsbasert filtrering anbefaler vi de varene til en bruker som ligner på varer denne brukeren har tidligere likt.

I samarbeidsbasert filtrering på den andre siden er ideen å finne brukere som ligner på oss. Så får vi anbefalinger basert på det folk som ligner på oss liker.

Nå skal vi gi en liten innføring i hvordan enkle anbefalingssystemer kan bygges opp. Reelle anbefalingssytemer er ofte mer komplekse og har både innholdsbaserte og sammarbeidsbaserte komponenter.

Innholdsbasert filtrering#

Ideen med innholdsbasert filtrering er å anbefale de varene til en bruker som ligner på varer denne brukeren har tidligere likt. For eksempel anbefaler vi bøker i sjanger, film med samme skuespillere, eller nyheter om samme emne som brukeren likte tidligere.

For å gjøre det, så må vi ha informasjon om varene. Vi lager en vareprofil for alle varene og bruker det som de uavhengige variablene X. Så ser vi på vurderingene vi har for brukeren og bruker det som den uavhengige variablen y. Så lager vi en modell og bruker den for å predikere vurderingene som mangler. Vi lager altså uavhengige regresjonsmodeller for hver bruker.

En stor fordel av innholdsbasert filtrering er at vi ikke trenger data fra andre brukere, så det kan gjøres selv om vi har få brukere. Vi kan også gi gode anbefalinger til brukere med sære preferanser. En annen fordel er at vi kan anbefale helt nye varer som ikke har noen vurderinger fra andre brukere. Hvis vi for eksempel bruker lineære modeller er det også enkelt å forklare hvorfor vi anbefalte noe, basert på hva brukeren likte tidligere.

Ulemper er at det kan være vanskelig og dyrt å lage gode vareprofiler. Det kan også føre til overspesialisering, der brukere kun får anbefalinger som ligner sterkt på det de har lest før og mister diversitæt. Andre ulemper er at vi ikke kan utnytte andre sine vurderinger og dermed heller ikke kan gi noen anbefalinger til nye brukere.

Samarbeidsbasert filtrering#

I sammarbeidsbasert filtrering er ideen å finne brukere som ligner på bruker A. Så får A anbefalinger basert på det folk som ligner på A liker. Vi bruker notasjonen at \(y_{ij}\) er vurderingen av vare \(i\) fra bruker \(j\). Her er en algoritme for samarbeidsbasert filtering med hyperparameter \(k\):

  1. Regn ut den gjennomsnittlige vurderingen \(\mu_j\) for hver bruker.

  2. Trekk fra gjennomsnittlige vurderinger per bruker.

  3. Fyll inn alle manglende vurderinger med 0.

  4. Regn ut korrelasjoner mellom alle brukere.

  5. For hver bruker \(j\):

    • Finner de \(k\) mest korrelerte brukere til bruker \(j\).

    • Erstatt manglende data med gjennomsnittet av vurderingene av de \(k\) nærmeste naboene.

  6. Legg til gjennomsnittlige vurderinger per bruker igjen.

La oss se på et eksempel. Anta at vi har følgende data.

df = pd.DataFrame(dict(Bok=['Tørst', 'Kniv', 'Lang dags ferd mot natt', 'Rosmersholm', 'Skuggasund'], 
                       Hanna=[10., 9, 6, 5, 2], 
                       Per=[5., 6, 10, 9, 4], 
                       Kari=[6., 7, 6, 6, 8], 
                       Ali=[np.nan, np.nan, np.nan, 6, 8])).set_index('Bok')
df
Hanna Per Kari Ali
Bok
Tørst 10.0 5.0 6.0 NaN
Kniv 9.0 6.0 7.0 NaN
Lang dags ferd mot natt 6.0 10.0 6.0 NaN
Rosmersholm 5.0 9.0 6.0 6.0
Skuggasund 2.0 4.0 8.0 8.0

Først regner vi ut gjennomsnittet per bruker.

means = df.mean()
means
Hanna    6.4
Per      6.8
Kari     6.6
Ali      7.0
dtype: float64

Så trekker vi fra gjennomsnittlige vurderinger per bruker.

normalized_df = df - means
normalized_df
Hanna Per Kari Ali
Bok
Tørst 3.6 -1.8 -0.6 NaN
Kniv 2.6 -0.8 0.4 NaN
Lang dags ferd mot natt -0.4 3.2 -0.6 NaN
Rosmersholm -1.4 2.2 -0.6 -1.0
Skuggasund -4.4 -2.8 1.4 1.0

Vi fyller inn manglende verdier.

normalized_imputed_df = normalized_df.copy()
normalized_imputed_df.fillna(0, inplace=True)
normalized_imputed_df
Hanna Per Kari Ali
Bok
Tørst 3.6 -1.8 -0.6 0.0
Kniv 2.6 -0.8 0.4 0.0
Lang dags ferd mot natt -0.4 3.2 -0.6 0.0
Rosmersholm -1.4 2.2 -0.6 -1.0
Skuggasund -4.4 -2.8 1.4 1.0

Så kan vi regne ut korrelasjon mellom alle brukere.

correlations = normalized_imputed_df.corr()
correlations
Hanna Per Kari Ali
Hanna 1.000000 -0.018057 -0.539968 -0.330489
Per -0.018057 1.000000 -0.691095 -0.682948
Kari -0.539968 -0.691095 1.000000 0.790569
Ali -0.330489 -0.682948 0.790569 1.000000

I det tilfelle bruker vi kun en nærmeste nabo. Siden det var Ali som hadde manglende vurderinger, så ser vi på hvem som Ali sine vurderinger korrelerer mest med. Her er det Kari. Så predikerer vi samme verdi som Kari hadde for de manglende data.

normalized_predicted_df = normalized_df.copy()
for bruker in df.columns:
    nearest = correlations[bruker].drop(bruker).idxmax()
    fill_indexes = normalized_predicted_df.loc[:, bruker].isna()
    normalized_predicted_df.loc[fill_indexes, bruker] = normalized_predicted_df.loc[fill_indexes, nearest]
normalized_predicted_df 
Hanna Per Kari Ali
Bok
Tørst 3.6 -1.8 -0.6 -0.6
Kniv 2.6 -0.8 0.4 0.4
Lang dags ferd mot natt -0.4 3.2 -0.6 -0.6
Rosmersholm -1.4 2.2 -0.6 -1.0
Skuggasund -4.4 -2.8 1.4 1.0

Til slutt legger vi til gjennomsnittene igjen.

predicted_df = normalized_predicted_df + means
predicted_df 
Hanna Per Kari Ali
Bok
Tørst 10.0 5.0 6.0 6.4
Kniv 9.0 6.0 7.0 7.4
Lang dags ferd mot natt 6.0 10.0 6.0 6.4
Rosmersholm 5.0 9.0 6.0 6.0
Skuggasund 2.0 4.0 8.0 8.0

Nå ser vi at selv om vi brukte 1-nærmeste nabo-regresjon, fikk vi en verdier som ikke har vært med i datasettet. Det er på grunn av måten vi først trekker fra gjennomsnittet av Kari sine vurderinger og så legger til gjennomsnittet av Ali sine vurderinger til slutt.

Her har vi brukt bruker-basert samarbeidsbasert filtrering. Det vil si at vi så på korrelasjon mellom brukere. Vi kunne i stedet for også sett på korrelasjon mellom vurderingene de forskjellige varene får. Utregningen ville ellers vært helt likt.

Den store fordelen med samarbeidsbasert filtrering er at vi slipper å lage vareprofiler.

Men for å gjøre dette, trenger vi mange brukere til å finne noen som faktisk ligner på alle. Det er spesielt vanskelig siden få brukere har vurdert de samme varene. Vi kan heller ikke anbefale noen nye varer. Og det kan føre til generell popularitetsbias, slik at vi anbefaler mest populære varer.

Evaluering#

Til slutt må vi snakke om hvordan man kan evaluere anbefalingssystemer. Den beste måten å evaluere anbefalingssystemer er ved å bruke A/B-tester. A/B-tester måler reell effekt på ekte brukere men ulempen er at man må bruke testene i produksjon som kan være resurskrevende og påvirke brukeropplevelsen negativt i testperioden.

Derfor må vi ofte evaluere anbefalingssystemer offline uten A/B-tester. Som for alle andre regresjonsmodeller, deler vi data i trenings-, validerings- og testdata. Så kan vi for eksempel regne ut kvadratroten av gjennomsnittig kvadrert feil

\[\sqrt {\frac {\sum _{i=1}^{N}({\hat {y}}_{ij}-y_{ij})^{2}}{N}}\]

på valideringsdata for å velge ut modeller. Her \(N\) er antall prediksjoner, \(\hat{y}_{ij}\) er prediskjon av vare \(i\) fra bruker \(j\) og \(y_{ij}\) er den faktiske vurderingen av vare \(i\) fra bruker \(j\).

Selv om gjennomsnittig kvadrert feil ofte blir brukt, så er det kanskje ikke den beste metrikken. Vi er jo ikke egentlig interesserte i de faktiske verdiene og bryr oss mer om høye vurderinger enn lave. Av og til vil vi også gi anbefalinger som er mer varerte. Derfor kan vi bruke mål basert på diversitet, rekkefølge av vurderingene, eller vekte høye prediksjoner mer. Dette skal vi ikke se på i mer detalj, men det er viktig å huske at metrikken er viktig og kan ha en stor innflytelse på resultatene.