$$\mbox{Sebastian Schmid}$$$$\mbox{s0543196}$$

Search Engine

Zweite Belegaufgabe aus dem Fach Contentmanagement / Text- und Suchtechnologien (WT-2) an der HTW Berlin bei Herrn Prof. Dr. Zhang.


In [1]:
from subprocess import check_output
import json
from pprint import pprint
import numpy as np
import pandas as pd
from math import log
import re 
!ls


graded02.pdf	my_spider.log	      __pycache__	  stop_words.txt
index.txt	my_spider.py	      rank.txt		  tfidf_search.txt
my_spider.json	pageranke_search.txt  SearchEngine.ipynb

Einleitung

Im Rahmen dieser Belegarbeit sollte eine einfache Suchengine implementiert werden.

Dabei waren folgende Schritte abzuarbeiten:

  1. Implementierung eines Crawlers zur Erfassung einer Link und Textstruktur .
  2. Implementierung eines Algorithmus zur Berechnung der Pageranks.
  3. Implementierung eines Scoringverfahren nach TF-IDF
  4. Erweitern des TF-IDF Scorings mit Pagerank

In [2]:
from IPython.display import IFrame
IFrame("./graded02.pdf", width=900, height=1250)


Out[2]:

a ) Der Crawler

Der Crawler wurde mit der Bibliothek Scrapy umgesetzt. Es wurde sich hier gegen eine Einrichtung eines Projektes und für eine Scriptbasierte Lösung entschieden. Um die Ausführung über das Notebook zu regeln, wurde über das MagicFlag %%writefile die Klasse in eine Datei geschrieben und der Aufruf über von Scrapy über subprocess umgesetzt.

Die Klasse HtwSpider implementiert über die Einstiegspunkte, die Extraktionsanweisungen sowie dem gewünschten Link-Verhalten den Crawler.

Dabei werden der Titel, der Text und die ausgehenden Links aller Seiten gespeichert, die von den EInstiegspunkten aus eirreichbar sind.


In [3]:
%%writefile ./my_spider.py
# %%writefile ./my_spider.py
# %load ./my_spider.py

import scrapy
import json

class HtwSpider(scrapy.Spider):
    name = 'htw_spider'
    
    start_urls = [
        'http://people.f4.htw-berlin.de/~zhangg/pages/teaching/pages/d01.html',
        'http://people.f4.htw-berlin.de/~zhangg/pages/teaching/pages/d06.html',
        'http://people.f4.htw-berlin.de/~zhangg/pages/teaching/pages/d08.html'
    ]

    def parse(self, response):
         
        # - Extracting the needed attributes.
        title = response.xpath('//title/text()').extract()[0]
        text = ' '.join([t.strip() for t in response.xpath('//body/text()').extract()]).strip()
        hrefs =  [l.split('.')[0] for l in response.xpath('//a/@href').extract()]
                      
        # - Printing the extracted data for logging. 
        print("\n ----DATA\n\n%s\n" % json.dumps({title : {'text': text, 
                        'refs' : hrefs }}))
        
        # - Yielding the extracted dataset as dict
        yield {'title': title,
               'text': text, 
               'refs' : hrefs }
        # - Here we find the actual spider.
        # - This goes through all a_hrefs and scrapes the needed data.
        for next_page in response.xpath('//a/@href').extract():
            if next_page is None:
                continue
            next_page = response.urljoin(next_page)
            yield scrapy.Request(next_page, callback=self.parse)


Overwriting ./my_spider.py

b) Ausführung

Die Ausführung erfolgt außerhalb des Notebooks über subprocess.check_output().


In [4]:
# - Here we call the scrapy process.
# 
# - This is needed to call this inside the notebook.
# - Scrapy concatenates all outputs (with destroying the json)
#   so we need to remove it first.
!rm ./my_spider.json

# - Executing the spider.
pout = check_output('scrapy runspider my_spider.py -o my_spider.json', shell=True)
# - Writing the output into logfile.
with open('./my_spider.log', 'w') as logfile:
    logfile.write(pout.decode("utf-8"))

Einlesen der Daten


In [5]:
# - Reading the Data from json into python obj-structure.-
with open('./my_spider.json') as infile:
    data = json.loads(''.join([line.strip() for line in infile.readlines()]))   
    
# - Removing all duplicates
data = list({d['title']:d for d in data}.values())

df = pd.DataFrame.from_dict(data)
df.index = df['title']
df


Out[5]:
refs text title
title
d01 [d02, d03, d04] Given a character sequence and a defined docum... d01
d06 [d07] In text classification, we are given a descrip... d06
d08 [] s is a spam page. tokens stopwords index posti... d08
d04 [d01, d02, d03, d05] To gain the speed benefits of indexing at retr... d04
d03 [d04, d01, d02, d05] For English, an alternative to making every to... d03
d02 [d03, d04, d01, d05] Token normalization is the process of canonica... d02
d07 [d06] Using a supervised learning method or learning... d07
d05 [d04] Index the documents that each term occurs in b... d05

c) Berechnung des Page Rank

Definition der Datenstrukturen

Um die Berechung zu vereinfachen, wurde die Datenhaltung in Form eines Graphen angeordnet.

Dabei hält jeder Knoten die ein- und ausgehenden Kanten, seinen Namen und die für die Berechung benötigten Platzhalter für das Zwischenspeichern der Ergebnisse. Auf Grund der rekursiven Struktur der Berechnungen müssen die Werte bereits belegt sein.

Hierbei ist es wichtig, dass die Variablen rank eine vollständige Wahrscheinlichkeitsverteilung $\left(\sum n \in N = 1\right)$ beschreiben.


In [6]:
delta = 0.04 
N = len(data)
t = 0.05
d = 1-t

graph = [   # - The graph of the web.
            #     Rank' is mocked with the purpose of simplifying the calculations.
        {
            'node':  d['title'], 
            'out':   d['refs'], 
            'in' :   [n['title'] for n in data if d['title'] in n['refs']],
            'rank':  1.0 / N,          # (1 / n) \leq 1 \forall n \in \mathbb{N}
            'rank_': 1.0 + delta       # mocking rank_
        } 
    for d in data ]

print('\n\nInitial graph:\n')
pd.DataFrame.from_dict(graph)



Initial graph:

Out[6]:
in node out rank rank_
0 [d04, d03, d02] d01 [d02, d03, d04] 0.125 1.04
1 [d07] d06 [d07] 0.125 1.04
2 [] d08 [] 0.125 1.04
3 [d01, d03, d02, d05] d04 [d01, d02, d03, d05] 0.125 1.04
4 [d01, d04, d02] d03 [d04, d01, d02, d05] 0.125 1.04
5 [d01, d04, d03] d02 [d03, d04, d01, d05] 0.125 1.04
6 [d06] d07 [d06] 0.125 1.04
7 [d04, d03, d02] d05 [d04] 0.125 1.04

Definition des Algorithmus zur Berechung des PageRank eines Knoten

$$ r_{k+1}(p_i) = d \cdot \left( \sum_{pj \in B_{p_i}} \frac{r_k(pj)}{|pj|} + \sum_{pj,\,|pj|=0} \frac{r_k(p_j)}{N} \right) + \frac{t}{N} $$

In [7]:
# - Get the amount of all outgoing refs of pj
out_len = lambda pj: [len(page['out']) for page in graph if page['node'] == pj][0]

# - Extract the rank of pj. 
get_rank = lambda pj: [p['rank'] for p in graph if p['node'] == pj][0]

# - Calculate pagerank of node pi.
_r = lambda pi: ( 
    d * (
        np.sum( [ get_rank(pj) / out_len(pj) for pj in pi['in'] ] ) + 
        np.sum( [ get_rank(pj['node']) / N for pj in graph if out_len(pj['node']) == 0 ] )
    ) + (t / N) )

Berechnungen in Iterationen

Die Berechnung erfolgt so lange bis die Abbruchbedingung erfüllt ist.

Es gilt folgende Abbruchbedingung:

$$ \left(\sum \left(| r^{(i)} -r'^{(i)}|\right) >= \delta\right);\,\,\,\, \forall r, r' \in g $$

In [8]:
# - The iterations of the pageranks
while not np.sum( [ abs(g['rank'] - g['rank_']) for g in graph ] ) < delta :
    for node in graph:
        node['rank_'], node['rank'] = node['rank'], _r(node)

Ausgabe der errechneten Daten


In [9]:
# - Put the calcuated data inside a DataFrame.
df = pd.DataFrame(graph)
df.index = df['node']
# ... writing it to file ...
df[['node', 'rank']].to_csv('rank.txt', index=False)
# ... and do some printing.
pprint(df['rank'])
print("Σ ----------------")
pprint(np.sum(df['rank']))


node
d01    0.121327
d06    0.143281
d08    0.008755
d04    0.220873
d03    0.128147
d02    0.128602
d07    0.143407
d05    0.120725
Name: rank, dtype: float64
Σ ----------------
1.0151158974735404

d) Aufbau eines inversed Index

Tokenizing

Folgende Arbeitsschritte wurden hier abgearbeitet:

  1. Entfernen aller Sonderzeichen
  2. Alle großgeschriebenen Buchstaben werden durch ihre kleingeschriebenen ersetzt
  3. Entfernen aller als Stopwörter definierten Terme

In [10]:
# - Extracting stopwords from file 
with open('./stop_words.txt') as infile:
    lines = infile.readlines()
stopwords = [word.strip("'").strip() 
             for line in lines 
             for word in line.split(', ') 
             if word.strip("'").strip() != '']

tokenize = lambda d: [re.compile('[\W_]+').sub('', w.lower()) for w in d.split() if w not in stopwords]
docs = [{'title': d['title'], 'words': tokenize(d['text'])} for d in data ]

Indexing

Es wird ein inversed Index aufgebaut.

Dabei werden die absoluten Vorkommen der Therme $w$ in den jeweiligen Dokumenten $d$ gespeichert.


In [11]:
words = [word for doc in docs for word in doc['words']]

iindx = pd.DataFrame.from_dict({ doc['title']: [ 
    doc['words'].count(word) for word in words ]
    for doc in docs })
iindx.index = words

iindx = iindx[~iindx.index.duplicated(keep='first')]
iindx.to_csv('index.txt')
iindx.head(10)


Out[11]:
d01 d02 d03 d04 d05 d06 d07 d08
given 1 0 0 0 0 1 0 0
character 1 1 0 0 0 0 0 0
sequence 1 0 0 0 0 0 0 0
defined 1 0 0 0 0 0 0 0
document 1 0 0 1 0 1 0 0
unit 1 0 0 0 0 0 0 0
tokenization 1 0 0 0 0 0 0 0
task 1 0 0 0 0 0 0 0
chopping 1 0 0 0 0 0 0 0
up 1 0 0 0 0 0 0 0

Scoring und Suche

e) TF - IDF

Term Freqency

Hier wurde sich für eine logarithmisch normierte Term Freqency entschieden.

Damit gilt:

$$ \mbox{log_freq}(w, d) = \begin{cases} 1 + log10 tf(w,d) & \mbox{für}\,\,\,\,\,\,\,\,\,\,\,\,\,\, tf(w,d) > 0 \\ 0 & \mbox{sonst} \\ \end{cases} $$

bei $tf(w,d) := $ absolutes Vorkommen des Terms $w$ in Dokument $d$.

Die Term Frequency steht bereits im Index und muss nur ausgelesen werden.

Inversed Document Frequency

Ist definiert als $$\frac{\mbox{Anzahl der Dokumente} (N)}{\mbox{Anzahl der Dokumente welche Term w enthalten }(df(w)) }$$

Eine Normierung erfolgt mit $\log_{10} df(w)$

Scoring und Suchfunktion

Das scoring der Dokumente eines spezifischen Terms kann über die Funktion search(words) ermittelt werden.

Dabei ist es auch möglich, Listen von Termen anzugeben. Diese werden über $A \land B$ verbunden.


In [12]:
idx = pd.read_csv('./index.txt', index_col=0)

def log_freq(word, doc):
    tf = int(idx[doc].loc[word])   
    return 1+log(tf, 10) if tf > 0 else 0
    
def idf(word):
    N = idx.shape[0]
    df = np.sum(idx.loc[word].map(lambda x: 0 if x == 0 else 1))
    return log (  (N / df), 10)

tfidf = lambda w,d : (log_freq(w, d) * idf(w))
weights = lambda w : [ {d: tfidf(w,d)} 
    for d in idx.columns.values.tolist() ]

pd.DataFrame.from_dict([{word: weights(word) for word in words}])

tfiidx = pd.DataFrame.from_dict({ doc['title']: [ 
    tfidf(word, doc['title']) for word in words ]
    for doc in docs })
tfiidx.index = words
tfiidx = tfiidx[~tfiidx.index.duplicated(keep='first')]

In [13]:
def search (words):
    assert type(words) == list
    # - The weights are calculatet by 

    return pd.DataFrame([tfiidx.loc[w] for w in words]).product()
    
s_terms = [['tokens'], ['index'], ['classification'], ['tokens', 'classification']]

search_df = pd.DataFrame([search(t) for t in s_terms])
search_df.index=[' AND '.join(s) for s in s_terms]

In [14]:
search_df


Out[14]:
d01 d02 d03 d04 d05 d06 d07 d08
tokens 1.342423 1.746532 1.342423 1.746532 0.000000 0.000000 0.000000 2.150642
index 0.000000 0.000000 0.000000 1.564271 2.035164 0.000000 0.000000 2.506057
classification 0.000000 0.000000 0.000000 0.000000 0.000000 1.564271 1.564271 2.506057
tokens AND classification 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 5.389630

In [15]:
search_df.to_csv('tfidf_search.txt')

f) Scoring tfidf und pagerank

Um tfidf und pagerank in einem Wert zu vereinen, werden diese über eine Multiplikation zusammengebracht.

$$\mbox{score} = \mbox{tf_idf}(w, d) \cdot \mbox{rank}(d)$$

In [16]:
r = pd.read_csv('rank.txt', index_col=0)
s = pd.read_csv('tfidf_search.txt', index_col=0)

score = pd.DataFrame(r.values * s.T.values, index=r.index, columns=s.index).T
score.to_csv('pageranke_search.txt')
score


Out[16]:
node d01 d06 d08 d04 d03 d02 d07 d05
tokens 0.162872 0.250245 0.011753 0.385762 0.0000 0.000000 0.000000 0.259636
index 0.000000 0.000000 0.000000 0.345506 0.2608 0.000000 0.000000 0.302543
classification 0.000000 0.000000 0.000000 0.000000 0.0000 0.201168 0.224327 0.302543
tokens AND classification 0.000000 0.000000 0.000000 0.000000 0.0000 0.000000 0.000000 0.650662