Et andet spørgsmål er om vi kan bruge genrerummet til at forbedre metadata. En hypotese er emneord ligge i klumper i genrerummet. I så fald kan vi bruge dette til at have et mål for hvor typisk et materiale er for et emne.
Lad os først kigge på hvorledes metadata ser ud:
import bibdata
from collections import Counter
import numpy
bibdata.meta[1234]
Som vi kan se er der, for hvert materiale et antal emneord subject-term
.
Lad os fokusere på de mest populære emneord. Vi kan finde disse ved at lave en liste af alle anvendte emneord subjects
, og derefter finde de mest hyppige af disse. Listen af alle emneord finder vi ved at gennemløbe alle bibliografiske poster bib
og for hver af disse tilføje samtlige emneord subject
til listen af alle emneord subjects
. Herefter kan den indbyggede Counter
i Python finde de hyppigst brugte emneord:
subjects = []
for bib in bibdata.meta:
for subject in bib.get('subject-term', []):
subjects.append(subject)
Counter(subjects).most_common(10)
Vi er i stand til at finde bøgerne med et givent emneord, ved at gennemløbe alle bøgerne (book_id
), og kun beholde dem der har det pågældende emneord. Hvis vi eksempelvis vil finde børnefilmene kan vi gøre det således:
films = [book_id for book_id in range(len(bibdata.meta))
if 'Børnefilm' in bibdata.meta[book_id].get('subject-term', [])]
I koden herover er len(bibdata.meta)
antallet af materialer, og range(n)
returnere en liste med n
tal, i.e. [0,1,2,...]
. Når vi bruger .get('subject-term', [])
får vi enten en liste af emneord, eller en tom liste hvis metadata mangler, - ellers ville koden fejle hvis vi manglede metadata.
Nu da vi har listen af børnefilm kan vi finde punkterne i genrerummet for disse:
Vi kan herefter udskrive de først 10 film:
print([bibdata.title_creator(film) for film in films[:10]])
Hver af disse film svare til et punkt(vektor) i genrerummet, som vi kan finde således:
vectors = [bibdata.genres[film] for film in films]
Vi forestiller os at materialerne med samme emneord, ligger i en klump i nærheden af hinanden i genrerummet. I så fald er midten af klumpen gennemsnits-punktet(mean
), og størrelsen af klumpen den statistiske spredning(std
= standard deviation), hvilket let findes i Python:
mean = numpy.mean(vectors, axis=0)
std = numpy.std(vectors, axis=0)
Vi kan herefter finde ud af hvor tæt et materiale ligger på klumpen, ved at tage afstanden i genrerummet, vægtet med klumpens størrelse (spredning). I.e. vi tager punktet for materialet(bibdata.genres[book_id]
), trækker gennemsnitspunktet fra klumpen fra(mean
), dividere med størrelsen(std
) og tager længden af dette numpy.linalg.norm(...)
. Så for alice i eventyrland, cirkeline(616) og turen går til berlin(149) er deres børnefilms-agtighed (jo lavere jo bedre):
print('alice i eventyrland(film)',
numpy.linalg.norm((bibdata.genres[810] - mean) / std))
print('cirkeline',
numpy.linalg.norm((bibdata.genres[616] - mean) / std))
print('berlin',
numpy.linalg.norm((bibdata.genres[149] - mean) / std))
Vi kan nu lave en liste af biblioteksmaterialer, kombineret med hvor meget de minder om børnefilm. Hvis vi derefter sorterer disse, finder vi de mest børnefilms-agtige materialer:
weighted_books = [
(numpy.linalg.norm((bibdata.genres[book_id]-mean)/std), book_id)
for book_id in range(len(bibdata.meta))
]
weighted_books.sort()
print(weighted_books[:10])
‐ hvilket er følgende materialer:
[bibdata.title_creator(book_id) for (weight, book_id) in weighted_books][0:10]
Nu har vi sorteret biblioteksmaterialerne efter børnefilms-agtighed, - dette kunne være praktisk at kunne, for hvilket som helst emneord. Derfor definere vi en ny funktion, som samler denne funktionalitet:
def books_by_subject(subject):
books = [book_id
for book_id in range(len(bibdata.meta))
if subject in bibdata.meta[book_id].get('subject-term', [])]
vectors = [bibdata.genres[book] for book in books]
mean = numpy.mean(vectors, axis=0)
std = numpy.std(vectors, axis=0)
weighted_books = [
(numpy.linalg.norm((bibdata.genres[book_id]-mean)/std), book_id)
for book_id in range(10000)]
sorted_books = [
book_id for (weight, book_id) in sorted(weighted_books)]
return sorted_books
Hvis vi tager de første materialer fra listen, som ikke har emneordet Børnefilm
blandt dets emneord(subject-term
), kan det være at vi finder nogle bibliografiske poster, som kunne beriges med disse metadata:
[bibdata.title_creator(book_id)
for (weight, book_id) in weighted_books
if not 'Børnefilm' in bibdata.meta[book_id].get('subject-term', [])][:10]
Konklusionen fra dette ekperiment er vist, at genrerummet godt kan bruges til at finde kandidater til berigelse af metadata.
Tilsvarende kan vi også finde de Børnefilm, der er mindst børnefilms-agtige:
[bibdata.title_creator(book_id)
for (weight, book_id) in reversed(weighted_books)
if 'Børnefilm' in bibdata.meta[book_id].get('subject-term', [])][0:5]
Hvilket stadigt er børnefilm, så med dette konkrete eksperiment fandt vi ikke nogle underlige materialer i forhold til emnet.
Det kunne dog være interessant, at lave lignende eksperimenter med andre emneord end "børnefilm", så lad os omskrive koden til en funktion så vi lettere kan ekspementere med det. Her er en funktion, som givet et emneord finder 1) de materialer, som ikke har emneordet, men minder mest om det i genrerummet, og 2) de materialer, som har emneordet, men minder mindst om det:
def subject_outliers(subject):
print('Without subject "' + subject + '":')
print('\n'.join(
[' ' + bibdata.title_creator(book_id)
for book_id in books_by_subject(subject)
if not subject in bibdata.meta[book_id].get('subject-term', [])
][:5]))
print('With subject "' + subject + '":')
print('\n'.join(
[' ' + bibdata.title_creator(book_id)
for book_id in reversed(books_by_subject(subject))
if subject in bibdata.meta[book_id].get('subject-term', [])
][:5]))
subject_outliers('eventyr')
Hvis vi skal gå videre på opdagelse i materialer, med givne emneord, kan det være praktisk med en liste med de mest almindelige emneord, hvliket findes således:
"; ".join([subject for (subject, count) in Counter(subjects).most_common(200)])
Nu har vi set hvorledes, vi kan finde materialer, der ligger tæt ved, eller langt fra, et givent emneord. Det kunne også være sjovt at vende det om, og se hvilket emneord, der ligger tæt på et materiale ud fra hvor det ligger i genre rummet.
Første trin er at lave en statistisk model for populære emneord, eksempelvis emneord, der er tildelt til mere end 10 materialer. Lad os allerførst lave en liste over alle emneord, og hvilke materialer der har dem:
subject_dict = {}
for book in bibdata.meta:
for subject in book.get('subject-term', []):
subject_dict[subject] = subject_dict.get(subject, []) + [book.get('idx')]
subject_dict = {
subject: book_ids
for subject, book_ids in subject_dict.items()
if len(book_ids) >= 10
}
Vi kan herefter lave finde gennemsnittet og spredningen, for hvert emne.
subjects = []
for subject, book_ids in subject_dict.items():
vectors = [bibdata.genres[book_id] for book_id in book_ids]
mean = numpy.mean(vectors, axis=0)
std = numpy.std(vectors, axis=0)
std = std / numpy.linalg.norm(std)
subjects.append({"name": subject, "mean": mean, "std": std})
Givet et book_id
kan vi herefter sortere emnerne efter hvor godt de passer til en given bog. Herunder tager vi 10 tilfældige bøger, og udskriver hvilke emner der passer bedst til dem i genrerummet, samt viser hvad det er for en bog, og hvilke metadata den har fået tildelt.
Vi finder tilfældige bøger via random
, og random.seed(1)
sikre at vi får de samme bøger hver gang vi kører koden. Afstanden mellem bøger og emner bliver fundet på samme vis som tidligere herover. Denne gang er det dog emnerne, og ikke bøgerne, vi sorterer.
import random; random.seed(1)
for book_id in [random.randint(0,10000) for i in range(10)]:
sorted_subjects = sorted([(
numpy.linalg.norm(
(bibdata.genres[book_id] - subject.get('mean')) /
subject.get('std')),
subject.get("name")) for subject in subjects])
print(book_id, bibdata.title_creator(book_id))
print('metadata:', bibdata.meta[book_id].get('subject-term'))
print('genrerum:', [name for weight, name in sorted_subjects[:10]])
print()
Dette eksperiment, giver os både mere indsigt i metadata såvel som genrerum. Når vi ser på det første par bøger, rammer genrerummet ret godt plet:
Det er at bemærke at den også skyder forkert. Eksempelvis: når der er geografi/tidsangivelser/etc. har betydning i metadata, kan dette også dukke op i genrerummet, men ofte skudt forkert, - i.e.: forkerte lande, vikinger omkring år 1200 i stedet for legender om riddere omkring år 400, etc.
Konklusionen er at genrerummet godt kan bruges til inspiration om metadata, men ofte også tager fejl, så man kan ikke automatisk udtrække emneord for materialet.
Dette leder også til overvejelser at vi kan få anbefalinger til andre emneord ud fra et givent emneord. Afstanden mellem emnerord findes på samme vis som afstandend mellem emneord og bøger, dog med den tilføjelse at der normaliseres med begge emneords spredning.
subject_name = 'fantastiske fortællinger'
subject1 = [subject
for subject in subjects
if subject.get('name') == subject_name
][0]
sorted_subjects = sorted([(
numpy.linalg.norm(
(subject1.get('mean') - subject2.get('mean')) /
(subject1.get('std') * subject2.get('std'))),
subject2.get("name")) for subject2 in subjects])
sorted_subjects[:20]
Konklusionen af stikprøven herover viser, at genrerummet også kan bruges til at finde relaterede emneord.
sex
"ede bøger, "tv-serier
", "Fodbold
", "Biografier af enkelte personer
", etc. som er outliers, enten fordi de ligner dette emne og ikke har emneorder, - eller har emneordet og ikke ligner de andre indenfor emnet. Kunne det give mening at fjerne/tilføje emneordet for disse?