Welchen Vorteil haben wir durch TF-IDF?
Benötigen wir zum Beispiel eine Bewertung von Suchbegriffen in einem Formular, so können wir hier eine Vorauswahl anhand der Wichtung des einzelnen Wortes treffen. Dies bietet die Möglichkeit, die Suchergebnisse von relevant bis unrelevant zu sortieren.
Wie könnte ein solches Ergebnis aussehen?
Nachfolgend unser Ergebnis nach der Formelanwendung. Die Key's entsprechen hier den Wörtern, welche gewichtet werden sollen. 1 und 2 sind die Dokumente, in denen die Gewichtung statt findet. Der zugewiesene Float-Wert entspricht letzten Endes unserem w (dem Gewicht).
array 'Lorem' => array 1 => float 0.69314718055995 2 => float 0.17328679513999 'sit' => array 1 => float 0.69314718055995 2 => float 0.17328679513999 'ipsum' => array 1 => float 0.69314718055995 2 => float 0.17328679513999 'gubergren' => array 1 => float 0.69314718055995 'Platzhalter' => array 2 => float 0.69314718055995
Die Texte 1 und 2 hierzu sind:
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
und:
„Lorem ipsum dolor sit amet, consectetur, adipisci velit …“ (vollständiger Text →Weblinks) ist ein Blindtext, der nichts bedeuten soll, sondern als Platzhalter im Layout verwendet wird. Die Verteilung der Buchstaben und der Wortlängen des pseudo-lateinischen Textes entspricht in etwa der natürlichen (lateinischen) Sprache. Der Text ist (absichtlich) unverständlich, damit der Betrachter nicht durch den Inhalt abgelenkt wird.
Wie lauten die Formeln zur Berechnung der Wichtung?
Die Vorkommenshäufigkeit tfi,j gibt an, wie häufig der Term i im Dokument j vorkommt. Wäre das Dokument 5 der aufgeführte Satz:
- Das rote Auto hält an der roten Ampel.
dann würde folgendes gelten:
tfrot,5 = 2.
Können wir nun die inverse Dokumenthäufigkeit berechnen?
Ja und zwar mit der Formel:

und der Formel:

(siehe http://de.wikipedia.org/wiki/TF-IDF)
Kommen wir nun zur Umsetzung.
Zunächst benötigen wir ein gewisses Setup.
$wordsToCheckWith = array_unique(array('Lorem', 'sit', 'ipsum', 'gubergren', 'Platzhalter'));
$documents = array('Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.',
'„Lorem ipsum dolor sit amet, consectetur, adipisci velit …“ (vollständiger Text →Weblinks) ist ein Blindtext, der nichts bedeuten soll, sondern als Platzhalter im Layout verwendet wird. Die Verteilung der Buchstaben und der Wortlängen des pseudo-lateinischen Textes entspricht in etwa der natürlichen (lateinischen) Sprache. Der Text ist (absichtlich) unverständlich, damit der Betrachter nicht durch den Inhalt abgelenkt wird.');
$w_max = 0;
$N = count($documents);
$result = $result_max = array();
Wir haben also Wörter und Dokumente um die Wertigkeit der Wörter innerhalb der Dokumente auszurechnen. $N ist hier die Anzahl der Dokumente, also 2. Wenn wir nun die Dokumente mit einer Schleife durchlaufen und innerhalb dieser Dokumentschleife die Wörter zählen und abgleichen wollen (Frequenz), erhalten wir die nächsten Codezeilen:
for ($j=0; $j<$N; $j++)
{
foreach ($wordsToCheckWith as $word) {
$frequency = substr_count(strtolower($documents[$j]), strtolower($word));
if ($frequency > 0) {
$result[$j][$word] = (! isset($result[$j][$word]))
? $frequency
: ($result[$j][$word] + $frequency);
if (!isset($result_max[$word])) {
$result_max[$word] = $result[$j][$word];
} elseif ($result[$j][$word] > $result_max[$word]) {
$result_max[$word] = $result[$j][$word];
}
$result_words[$word][$j] = true;
}
}
}
Nun haben wir also die Dokumente, in denen die durchlaufenen Wörter am häufigsten vor kommen.
Jetzt müssen wir leider die Schleifendurchläufe erneut durchführen um die Gewichtung der Wörter zu berechnen. Das machen wir, indem wir die beiden Formeln kombinieren und wie folgt umsetzen:
$w_count = array();
for ($j=0; $j<$N; $j++)
{
$temp = array();
foreach ($wordsToCheckWith as $word)
{
$w_cache = 0;
if (isset($result[$j][$word])) {
$w_cache = ($result[$j][$word] / $result_max[$word]) * log(($N / $result_words[$word][$j]));
$w[$word][$j + 1] = $w_cache;
if (!isset($w_max) || $w_max < $w_cache) {
$w_max = $w_cache;
$valence[$word][$j] = $w_cache;
$temp[] = $j;
} elseif (($w_max == $w_cache) && (!in_array($j, $temp))) {
$temp[] = $j;
$valence[$word][$j] = $w_cache;
}
}
if (isset($w_count[$j])) {
$w_count[$j] = $w_count[$j] + $w_cache;
} else {
$w_count[$j] = $w_cache;
}
}
}
Nun wären wir eigentlich fertig. Lassen wir uns nun $w ausgeben, erhalten wie die obig aufgeführte Auflistung der Wörter und deren Wertigkeit im Dokumentkorpus. Nun soll das ganze Beispiel auch noch in einem gewissen Kontext aufgezeigt werden. Hierzu habe ich folgendes Klassenbeispiel und dessen Anwendung für Sie:
<?php
class App_TFIDF
{
/**
* Words to get the valence for.
*
* @var array
*/
protected $_words = array();
/**
* The documents to handle with.
*
* @var array
*/
protected $_documents = array();
/**
* Temporary array
*
* @var array
*/
private $_temp = array();
/**
* Number of documents.
*
* @var integer
*/
private $_N = 0;
/**
* Valence for provided $_words.
*
* @var array
*/
private $_w = array();
/**
* Sets documents and words if provided.
*
* First parameter should be a document to add.
* The second parameter should be an array of words.
*
* @param mixed $options
*
* @return void
*/
public function __construct($options = array())
{
if (func_num_args() > 1) {
$args = func_get_args();
$documents = array_shift($args);
if (is_array($documents)) {
$this->setDocuments($documents);
} else {
$this->addDocument($documents);
}
if (!empty($args)) {
$words = array_shift($args);
if (is_array($words)) {
$this->addWords($words);
} else {
$this->addWord($words);
}
}
} elseif (is_string($options)) {
$this->addDocument($options);
} elseif (is_array($options)) {
if (isset($options['documents'])) {
$this->setDocuments($options['documents']);
}
if (isset($options['words'])) {
$this->addWords($options['words']);
}
}
}
/**
* Gets $_documents.
*
* @return array
*/
public function getDocuments()
{
return $this->_documents;
}
/**
* Set multiple documents at once.
*
* @param array $documents
*
* @return App_TFIDF
*/
public function setDocuments(array $documents)
{
$this->_documents = array();
foreach ($documents as $document)
{
$this->addDocument($document);
}
return $this;
}
/**
* Add a document to $_documents.
*
* @param string $document
*
* @return App_TFIDF
*/
public function addDocument($document)
{
if (is_string($document)) {
$this->_documents[] = $document;
}
$this->_N = count($this->_documents);
return $this;
}
/**
* Reset $_documents.
*
* @return App_TFIDF
*/
public function clearDocuments()
{
$this->_documents = array();
$this->_N = 0;
return $this;
}
/**
* Gets $_words.
*
* @return array
*/
public function getWords()
{
return $this->_words;
}
/**
* Add multiple words at once to $_words.
*
* @param array $words
*
* @return App_TFIDF
*/
public function addWords(array $words)
{
foreach ($words as $word)
{
$this->addWord($word);
}
return $this;
}
/**
* Add $word to $_words if not already done.
*
* @param string $word
*
* @return App_TFIDF
*/
public function addWord($word)
{
if (is_string($word) || !in_array($word, $this->_words)) {
$this->_words[] = $word;
}
return $this;
}
/**
* Reset $_words.
*
* @return App_TFIDF
*/
public function clearWords()
{
$this->_words = array();
return $this;
}
/**
* Removes $word from $_words.
*
* @param string $word
*
* @return App_TFIDF
*/
public function removeWord($word)
{
if (in_array($word, $this->_words)) {
unset($this->_words[$word]);
}
return $this;
}
/**
* Gets $_w.
*
* Processes formula and returns an array with valence for each word.
*
* @return array
*/
public function process()
{
$w_max = 0;
$result_max = array();
for ($j=0; $j<$this->_N; $j++)
{
// for each word from users question
foreach ($this->_words as $word)
{
// frequency ij
$frequency = substr_count(strtolower($this->_documents[$j]),
strtolower($word));
if ($frequency > 0) {
$result[$j][$word] = (!isset($result[$j][$word]))
? $frequency
: $result[$j][$word] + $frequency;
// max(frequency ij)
if (!isset($result_max[$word])) {
$result_max[$word] = $result[$j][$word];
} elseif ($result[$j][$word] > $result_max[$word]) {
$result_max[$word] = $result[$j][$word];
}
// count($result_words[$word]) == n
$result_words[$word][$j] = true;
}
}
}
if (!isset($result_words)) {
return $this->_w;
}
$w_count = array();
for ($j=0; $j<$this->_N; $j++)
{
$temp = array();
foreach ($this->_words as $word)
{
$w_cache = 0;
if (isset($result[$j][$word])) {
// Now lets calculate the valence.
$w_cache = ($result[$j][$word] / $result_max[$word])
* log(($this->_N / $result_words[$word][$j]));
// Count +1 because the first document should not be 0 :)
$this->_w[$word][$j + 1] = $w_cache;
if (!isset($w_max) || $w_max < $w_cache) {
$w_max = $w_cache;
// Reset $max_entry to this value.
$valence[$word][$j] = $w_cache;
$temp[] = $j;
} elseif (($w_max == $w_cache) && (!in_array($j, $temp))) {
// Check if already stored in $max_entry.
$temp[] = $j;
$valence[$word][$j] = $w_cache;
}
}
if (isset($w_count[$j])) {
$w_count[$j] = $w_count[$j] + $w_cache;
} else {
$w_count[$j] = $w_cache;
}
}
}
return $this->_w;
}
}
Benutzt werden könnte die Klasse nun so:
$options = array('documents' => array(
'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.',
'„Lorem ipsum dolor sit amet, consectetur, adipisci velit …“ (vollständiger Text →Weblinks) ist ein Blindtext, der nichts bedeuten soll, sondern als Platzhalter im Layout verwendet wird. Die Verteilung der Buchstaben und der Wortlängen des pseudo-lateinischen Textes entspricht in etwa der natürlichen (lateinischen) Sprache. Der Text ist (absichtlich) unverständlich, damit der Betrachter nicht durch den Inhalt abgelenkt wird.',
),
'words' => array(
'Lorem',
'sit',
'ipsum',
'gubergren',
'Platzhalter',
));
$tfidf = new App_TFIDF($options);
$w = $tfidf->process();
var_dump($w);
2 Kommentar(e) zu “TF-IDF - inverse Dokumenthäufigkeit”