Παράρτημα Γ

Πιθανοτική Λανθάνουσα Σημασιολογική Ανάλυση

Το παράρτημα Γ παρουσιάζει την εφαρμογή του μοντέλου πιθανοτικής λανθάνουσας σημασιολογικής ανάλυσης (PLSA) για την αναζήτηση ιδεολογικά φορτισμένων αποσπασμάτων κειμένων, μέσω της ποσοτικής ανάλυσης και εξόρυξης (mining) σημασιολογικών γειτόνων των χαρακτηριστικών όρων (terms) που αναδείχθηκαν κατά την ποιοτική ανάλυση της συλλογής (βλ. Παράρτημα Β).

Η ποσοτική ανάλυση στα πλαίσια της μελέτης, ακολουθεί μία συνήθη πρακτική αναπαράστασης της διαδικασίας παραγωγής του κάθε εγγράφου με ένα πιθανοτικό μοντέλο. Σε ένα τέτοιο μοντέλο, ο ιδεολογικά φορτισμένος λόγος αναπαρίσταται από μία πολυωνυμική κατανομή των όρων που τον απαρτίζουν, σε ένα προσεγγιστικό γλωσσικό μοντέλο (approximative language model - unigram). Έτσι, το πιθανοτικό μοντέλο του λόγου αποτελεί μία μικτή κατανομή k συναρτήσεων, όπου η κάθε συνάρτηση αντιπροσωπεύει μία διακριτή ιδεολογική κατηγορία.

Στην PLSA, οι όροι των κειμένων της συλλογής εμφανίζονται ως παρατηρήσεις ενός μικτού μοντέλου πιθανοτήτων, το οποίο αποτελείται από τις επιμέρους κατανομές πιθανοτήτων των όρων ανά ιδεολογική κατηγορία. Η βαρύτητα που δίνεται σε κάθε κατανομή ελέγχεται από συγκεκριμένες παραμέτρους (mixing weights), οι οποίες ταυτίζονται στην περίπτωση που οι όροι ανήκουν στο ίδιο κείμενο. Συμπληρωματικά, παρέχεται μία επιπλέον συνάρτηση αναπλαισίωσης των δεδομένων (background component) η οποία αποδίδει τη λειτουργία του μη ιδεολογικού λόγου σε μία ακόμη κατανομή πιθανοτήτων.

Συγκεκριμένα, υποθέτοντας ότι οι κατανομές θ1,...,θk είναι τα προσεγγιστικά γλωσσικά μοντέλα (unigrams) των όρων και θΒ η συνάρτηση αναπλαισίωσης στο σύνολο της συλλογής C, ένας όρος w σε ένα έγγραφο d εμφανίζεται ως δείγμα του παρακάτω μικτού μοντέλου πιθανοτήτων:

όπου c(w,d) είναι ο αριθμός εμφανίσεων του όρου w στο έγγραφο d.

Οι τιμές του μοντέλου εκτιμώνται με τον αλγόριθμο μεγιστοποίησης προσδοκίας (expectation maximisation - EM), έναν γενικό αλγόριθμο εκτίμησης της μέγιστης πιθανοφάνειας που χρησιμοποιείται σε περιπτώσεις ελλιπών δεδομένων ή λανθανόντων μεταβλητών. Οι έννοιες των ελλιπών δεδομένων και των λανθανόντων μεταβλητών συνδέονται, καθώς η παρουσία μίας λανθάνουσας μεταβλητής υποδηλώνει την έλλειψη δεδομένων παρατήρησης, ενώ αντίστοιχα τα ελλιπή δεδομένα μας οδηγούν στη σύνδεση μίας λανθάνουσας μεταβλητής με τα ελλείποντα δεδομένα (τα οποία θεωρούμε ότι λανθάνουν μεταξύ των δεδομένων παρατήρησης).

Ο αλγόριθμος EM ξεκινά με μία τυχαία ανάθεση τιμών σε όλες τις παραμέτρους που πρέπει να εκτιμηθούν. Στη συνέχεια, εναλλάσσονται κατ'επανάληψη δύο βασικά βήματα: το βήμα προσδοκίας (E) και το βήμα μεγιστοποίησης (M). Στο E, υπολογίζεται η αναμενόμενη πιθανοφάνεια του συνόλου των δεδομένων (συνάρτηση σφάλματος Q), όπου η προσδοκία αφορά στη δεσμευμένη κατανομή των λανθανόντων μεταβλητών που έχει υπολογιστεί, με βάση τις ρυθμίσεις των παραμέτρων ελέγχου και την μερική (ελλιπή) παρατήρηση των δεδομένων μας. Στο M, επανεκτιμώνται οι τιμές όλων των παραμέτρων ελέγχου με τη μεγιστοποίηση της συνάρτησης σφάλματος Q.

Το βήμα Ε επαναλαμβάνεται στη συνέχεια, με τις νέες τιμές των παραμέτρων ελέγχου που εκτιμήθηκαν στο προηγούμενο βήμα Μ και η διαδικασία συνεχίζεται έως ότου η συνάρτηση πιθανοφάνειας συγκλίνει σε ένα τοπικό μέγιστο. Έτσι, αυτό που επιτυγχάνει η EM είναι η διαδοχική αύξηση των δεδομένων παρατήρησης, δίνοντας τυχαίες τιμές στις λανθάνουσες μεταβλητές και εκτιμώντας στη συνέχεια τις τιμές των παραμέτρων ελέγχου με την υπόθεση ότι οι τυχαίες τιμές των λανθανόντων μεταβλητών είναι σωστές.

Συνεπώς, αναζητούμε την τιμή της παραμέτρου Λ η οποία μεγιστοποιεί τη συνάρτηση λογαριθµικής πιθανοφάνειας της συλλογής C:

Εισάγουμε μία μεταβλητή {zd,w} η οποία αντιπροσωπεύει τους όρους που λανθάνουν μεταξύ των δεδομένων παρατήρησης. Η P(zd,w = B) είναι η πιθανότητα εμφάνισης του όρου w του εγγράφου d στο μη ιδεολογικό λόγο της συλλογής, ενώ η P(zd,w = j) είναι η πιθανότητα εμφάνισης του όρου w του εγγράφου d στα ιδεολογικά φορτισμένα αποσπάσματα, εφόσον όμως δεν εμφανίζεται καθόλου στο μη ιδεολογικό λόγο της συλλογής. Τα βήματα Ε και Μ διαμορφώνονται επομένως:

Για την εφαρμογή του μοντέλου PLSA στη θεματική συλλογή της μελέτης, χρησιμοποιήθηκε το περιβάλλον ανάπτυξης εφαρμογών υπολογιστικής γλωσσολογίας Lemur. Η υλοποίηση των αλγορίθμων βασίστηκε σε κώδικα πηγής των Andrew Schein και Alexandrin Popescul (PennAspect):

‘/// Probabilistic Latent Semantic Analysis
/*
* This application will either perform PLSA on a collection, building three probability tables: P(z), P(d|z), and P(w|z) where z in Z are the latent variables (categories), d in D are the documents in the collection, and w in W are the terms in the vocabulary over the collection, or open those tables and read them into memory to illustrate their potential use.
*
* The parameter doTrain (true|false) determines whether the tables are constructed or read.
*
* The other parameters accepted by PLSA are:
*
* index -- the index to use.
* numCats -- the number of latent variables (categories) to use.
* beta -- The value of beta for Tempered EM (TEM).
* betaMin -- The minimum value for beta, TEM iterations stop when beta falls below this value.
* eta -- Multiplier to scale beta before beginning a new set of TEM iterations. Must be less than 1.
* annealcue -- Minimum allowed difference between likelihood in consecutive iterations.
* If the difference is less than this, beta is updated.
* numIters -- Maximum number of iterations to perform.
* numRestarts -- Number of times to recompute with different random seeds.
* testPercentage -- Percentage of events (d,w) to hold out for validation.
* doTrain -- whether to construct the probability tables or read them in. Default is true.
*/

#include "IndexManager.hpp"
#include "PLSA.hpp"
#include "PLSAParam.hpp"

using namespace lemur::api;
using namespace lemur::cluster;

// get application parameters
void GetAppParam() {
PLSAParam::get();
}

int AppMain(int argc, char * argv[]) {
Index *myIndex;
myIndex = IndexManager::openIndex(PLSAParam::databaseIndex);
lemur::cluster::PLSA *plsa;
int numDocs = myIndex->docCount() + 1;
int numCats = PLSAParam::numCats;
int numRestart = PLSAParam::numRestarts;
int numIter = PLSAParam::numIters;
double beta = PLSAParam::beta;
double betaMin = PLSAParam::betaMin;
double eta = PLSAParam::eta;
double annealcue = PLSAParam::annealcue;
int testPercentage = PLSAParam::testPercentage;
bool doTrain = PLSAParam::doTrain;
int i, d, w, z;

if (doTrain) {
plsa = new lemur::cluster::PLSA(*myIndex, testPercentage, numCats,
numIter, numRestart,
beta, betaMin, annealcue, eta);
plsa->iterateWithRestarts();
} else {
plsa = new lemur::cluster::PLSA(*myIndex);
if (!plsa->readArrays()) {
std::cerr << "Failed to load probability tables." << std::endl;
exit (1);
}
int numTerms = plsa->numWords();
numDocs = plsa->numDocs();
numCats = plsa->numCats();
double lim = 1.0/numTerms;
cout << "P(d,w):" << endl;
for(d = 1; d < numDocs; d++) { // DOC
cout << myIndex->document(d) << ": ";
for (w = 1; w < numTerms; w++) {
double prob = plsa->getProb(d,w);
if (prob > lim) cout << myIndex->term(w) << "(" << prob << ") ";
}
cout << endl;
}
double **p_d_z = plsa->get_p_d_z();
double **p_w_z = plsa->get_p_w_z();
double *p_z = plsa->get_p_z();
cout << "P(w|z):" << endl;
for(w = 1; w < numTerms; w++) { // TERM
cout << myIndex->term(w) << ": ";
for (z = 0; z < numCats; z++) {
cout << z << "(" << p_w_z[w][z] << ") ";
}
cout << endl;
}
cout << "P(d|z):" << endl;
for(d = 1; d < numDocs; d++) { // DOC
cout << myIndex->document(d) << ": ";
for (z = 0; z < numCats; z++) {
cout << z << "(" << p_d_z[d][z] << ") ";
}
cout << endl;
}
cout << "P(z):" << endl;
for(i = 0; i < numCats; i++) {
cout << "cat " << i << ": " << p_z[i] << endl;
}
}
delete(plsa);
delete(myIndex);
return 0;
}
/*==========================================================================
* Copyright (c) 2003 University of Massachusetts. All Rights Reserved.
*
* Use of the Lemur Toolkit for Language Modeling and Information Retrieval
* is subject to the terms of the software license set forth in the licence
* file included with this software, and also available at
* http://www.lemurproject.org/license.html
* *==========================================================================
*/
#include "PLSA.hpp"
#include <float.h> // for -DBL_MAX
#ifdef WIN32
#define drand48() ((rand() % 1000000) / 1000000.0)
#endif

// Probabilistic Latent Semantic Analysis
lemur::cluster::PLSA::PLSA(const lemur::api::Index &dbIndex, int numCats, lemur::utility::HashFreqVector **train,
lemur::utility::HashFreqVector **validate, int numIter,
int numRestarts, double betastart,
double betastop, double anneal, double betaMod) :
ind(dbIndex), data(train), testData(validate), numberOfIterations(numIter),
numberOfRestarts(numRestarts), startBeta(betastart), beta(betastart),
betaMin(betastop), annealcue(anneal), betaModifier(betaMod), ’ ‘sizeZ(numCats) {
init();
initR();
}

void lemur::cluster::PLSA::initR() {
// initialize R - total count
// and w->d index
// 1-based indexing
invIndex = new set<int, less<int> > [sizeW];
R = 0;
for(int i = 1; i < sizeD; i++) { // DOC
lemur::utility::HashFreqVector *freqs = data[i];
freqs->startIteration();
while (freqs->hasMore()) { // TERM
lemur::api::TERMID_T termId;
int freq;
freqs->nextFreq(termId, freq);
R += freq;
invIndex[termId].insert(i);
}
}
}

void lemur::cluster::PLSA::init() {
bestOnly = false;
ownMem = false;

sizeD = ind.docCount() + 1;
sizeW = ind.termCountUnique() + 1;
bestA = 0;
bestTestLL = -DBL_MAX;

p_z_prev = new double[sizeZ];
p_z_current = new double[sizeZ];
p_z_best = new double[sizeZ];

p_w_z_prev = new double *[sizeW];
p_w_z_current = new double *[sizeW];
p_w_z_best = new double *[sizeW];
p_d_z_prev = new double *[sizeD];
p_d_z_current = new double *[sizeD];
p_d_z_best = new double *[sizeD];

p_w_z_prev[0] = NULL;
p_w_z_current[0] = NULL;
p_w_z_best[0] = NULL;

p_d_z_prev[0] = NULL;
p_d_z_current[0] = NULL;
p_d_z_best[0] = NULL;

// 1-based indexing
for (int w = 1; w < sizeW; w++) {
p_w_z_prev[w] = new double [sizeZ];
p_w_z_current[w] = new double [sizeZ];
p_w_z_best[w] = new double [sizeZ];
}
// 1-based indexing
for (int d = 1; d < sizeD; d++){
p_d_z_prev[d] = new double [sizeZ];
p_d_z_current[d] = new double [sizeZ];
p_d_z_best[d] = new double [sizeZ];
}
}

void lemur::cluster::PLSA::selectTestTrain(int testPercentage) {
// set up data and testData.
lemur::utility::HashFreqVector **train = new lemur::utility::HashFreqVector *[sizeD];
lemur::utility::HashFreqVector **test = new lemur::utility::HashFreqVector *[sizeD];
train[0] = NULL;
test[0] = NULL;
lemur::utility::FreqCount ct;
for(int i = 1; i < sizeD; i++) { // DOC
train[i] = new lemur::utility::HashFreqVector;
test[i] = new lemur::utility::HashFreqVector;
lemur::api::TermInfoList *tList = ind.termInfoList(i);
lemur::api::TermInfo *info;
tList->startIteration();
while (tList->hasMore()) { // TERM
info = tList->nextEntry();
ct.key = info->termID();
// K % test
if ((rand() % testPercentage) == 0)
test[i]->add(ct, info->count());
else
train[i]->add(ct, info->count());
}
delete tList;
}
data = train;
testData = test;
ownMem = true;
}


// Probabilistic Latent Semantic Analysis
lemur::cluster::PLSA::PLSA(const lemur::api::Index &dbIndex, int testPercentage, int numCats,
int numIter,
int numRestarts, double betastart,
double betastop, double anneal, double betaMod) :
ind(dbIndex), numberOfIterations(numIter),
numberOfRestarts(numRestarts), startBeta(betastart), beta(betastart),
betaMin(betastop), annealcue(anneal), betaModifier(betaMod),
sizeZ(numCats) {
init();
selectTestTrain(testPercentage);
initR(); }

/// Make an empty one ready to read in precomputed probability tables.
lemur::cluster::PLSA::PLSA(const lemur::api::Index &dbIndex) :
ind(dbIndex), data(NULL), testData(NULL), numberOfIterations(0),
numberOfRestarts(0), startBeta(0), beta(0),
betaMin(0), annealcue(0), betaModifier(0), sizeZ(0), sizeW(0),
sizeD(0) {
bestOnly = true;
ownMem = false;
p_z_prev = NULL;
p_z_current = NULL;
p_z_best = NULL;
p_w_z_prev = NULL;
p_w_z_current = NULL;
p_w_z_best = NULL;
p_d_z_prev = NULL;
p_d_z_current = NULL;
p_d_z_best = NULL;
invIndex = NULL;
/// pass in a filestem.
// if (!readArrays()) cerr << "Failed to Read arrays." << endl;}

lemur::cluster::PLSA::~PLSA() {
if (! bestOnly) {
// pass in a filestem
writeArrays();
}
for (int w = 0; w < sizeW; w++) {
if (! bestOnly) {
delete[](p_w_z_prev[w]);
delete[](p_w_z_current[w]);
}
delete[](p_w_z_best[w]);
}
for (int d = 0; d < sizeD; d++){
if (! bestOnly) {
delete[](p_d_z_prev[d]);
delete[](p_d_z_current[d]);
}
delete[](p_d_z_best[d]);
}
if (! bestOnly) {
delete[](p_z_prev);
delete[](p_w_z_prev);
delete[](p_d_z_prev);
delete[](p_z_current);
delete[](p_w_z_current);
delete[](p_d_z_current);
delete[](invIndex);’ ‘}
delete[](p_z_best);
delete[](p_w_z_best);
delete[](p_d_z_best);
if (ownMem) {
for(int i = 1; i < sizeD; i++) {
delete(data[i]);
delete(testData[i]);
}
delete[](data);
delete[](testData);
}
}

// performs one EM iteration, returns log likelihood of training data
double lemur::cluster::PLSA::interleavedIterationEM() {
int d, z, i, j;
lemur::api::TERMID_T w;

double* denominatorMaxStep = new double[sizeZ];
double sum = 0.0, sum_pz = 0;
for(z = 0; z < sizeZ; z++) { // CAT
for(d = 1; d < sizeD; d++) {// DOC
lemur::utility::HashFreqVector *freqs = data[d];
freqs->startIteration();
while (freqs->hasMore()) { // TERM
int freq;
freqs->nextFreq(w, freq);
// sum += tf(w) * p(z) * exp(p(d,z)*p(w,z), beta)/jointBeta(d,w)
sum += freq * p_z_prev[z] *
pow(p_d_z_prev[d][z] * p_w_z_prev[w][z], beta) /
jointEstimateBeta(d, w);
}
}
denominatorMaxStep[z] = sum;
sum = 0.0;
}
// M step of P(w | z)
sum = 0.0;
for(w = 1; w < sizeW; w++) // TERM
for(z = 0; z < sizeZ; z++) { // CAT
set<int, less<int> >::iterator it;
for (it = invIndex[w].begin(); it != invIndex[w].end(); it++) {
d = *it;
lemur::utility::HashFreqVector *freqs = data[d]; // DOC
int freq;
freqs->find(w, freq);
// sum += tf(w) * p(z) * exp(p(d,z)*p(w,z), beta)/jointBeta(d,w)
sum += freq * p_z_prev[z] *
pow(p_d_z_prev[d][z] * p_w_z_prev[w][z], beta) /
jointEstimateBeta(d, w);
}
p_w_z_current[w][z] = sum/denominatorMaxStep[z];
sum = 0.0;
}


// M step of P(d | z)
sum = 0.0;
for(d = 1; d < sizeD; d++) // DOC
for(z = 0; z < sizeZ; z++) { // CAT
lemur::utility::HashFreqVector *freqs = data[d];
freqs->startIteration();
while (freqs->hasMore()) { // TERM
int freq;
freqs->nextFreq(w, freq);
// sum += tf(w) * p(z) * exp(p(d,z)*p(w,z), beta)/jointBeta(d,w)
sum += freq * p_z_prev[z] *
pow(p_d_z_prev[d][z] * p_w_z_prev[w][z], beta) /
jointEstimateBeta(d, w);
}
p_d_z_current[d][z] = sum/denominatorMaxStep[z];
sum = 0.0;
}

// M step of P(z)
for(z = 0; z < sizeZ; z++) {// CAT
double val = denominatorMaxStep[z]/R;
p_z_current[z] = val;
sum_pz += val; // normalizer
}

// normalize parameters
for(i = 0; i < sizeZ; i++) // CAT
p_z_current[i] /= sum_pz;
// ------------------------------
for(j = 0; j < sizeZ; j++) { // CAT
sum = 0.0;
for(i = 1; i < sizeW; i++) // TERM
sum += p_w_z_current[i][j];
for(i = 1; i < sizeW; i++) // TERM
p_w_z_current[i][j] /= sum;
}
// -----------------------------
for(j = 0; j < sizeZ; j++) { // CAT
sum = 0.0;
for(i = 1; i < sizeD; i++) // DOC
sum += p_d_z_current[i][j];
for(i = 1; i < sizeD; i++) // DOC
p_d_z_current[i][j] /= sum;
}
// end normalizing
delete[] denominatorMaxStep;
return logLikelihood();
}

void lemur::cluster::PLSA::setPrevToCurrent() {
int i, j;
for(i = 0; i < sizeZ; i++)
p_z_prev[i] = p_z_current[i];
for(i = 1; i < sizeW; i++)
for(j = 0; j < sizeZ; j++)
p_w_z_prev[i][j] = p_w_z_current[i][j];
for(i = 1; i < sizeD; i++)
for(j = 0; j < sizeZ; j++)
p_d_z_prev[i][j] = p_d_z_current[i][j];
}

void lemur::cluster::PLSA::setCurrentToBest() {
int i, j;
for(i = 0; i < sizeZ; i++)
p_z_current[i] = p_z_best[i];
for(i = 1; i < sizeW; i++)
for(j = 0; j < sizeZ; j++)
p_w_z_current[i][j] = p_w_z_best[i][j];
for(i = 1; i < sizeD; i++)
for(j = 0; j < sizeZ; j++)
p_d_z_current[i][j] = p_d_z_best[i][j];
}

void lemur::cluster::PLSA::setBestToCurrent() {
int i, j;
for(i = 0; i < sizeZ; i++)
p_z_best[i] = p_z_current[i];
for(i = 1; i < sizeW; i++)
for(j = 0; j < sizeZ; j++)
p_w_z_best[i][j] = p_w_z_current[i][j];
for(i = 1; i < sizeD; i++)
for(j = 0; j < sizeZ; j++)
p_d_z_best[i][j] = p_d_z_current[i][j];
}

void lemur::cluster::PLSA::setBestToPrev() {
int i, j;
for(i = 0; i < sizeZ; i++)
p_z_best[i] = p_z_prev[i];
for(i = 1; i < sizeW; i++)
for(j = 0; j < sizeZ; j++)
p_w_z_best[i][j] = p_w_z_prev[i][j];
for(i = 1; i < sizeD; i++)
for(j = 0; j < sizeZ; j++)
p_d_z_best[i][j] = p_d_z_prev[i][j];
}

void lemur::cluster::PLSA::setPrevToBest() {
int i, j;
for(i = 0; i < sizeZ; i++)
p_z_prev[i] = p_z_best[i];
for(i = 1; i < sizeW; i++)
for(j = 0; j < sizeZ; j++)
p_w_z_prev[i][j] = p_w_z_best[i][j];
for(i = 1; i < sizeD; i++)
for(j = 0; j < sizeZ; j++)
p_d_z_prev[i][j] = p_d_z_best[i][j];


// Calculate the average likelihood of an event
double lemur::cluster::PLSA::getAverageLikelihood() {
double answer = 0.0;
int d;
lemur::api::TERMID_T w;
int events = 0;
for (d = 1; d < sizeD; d++) {
lemur::utility::HashFreqVector *freqs = testData[d];
freqs->startIteration();
while (freqs->hasMore()) { // TERM
int freq;
freqs->nextFreq(w, freq);
answer += jointEstimateCurrent(d, w);
events++;
}
}
answer /= events;
return answer;
}

// Calculate the average likelihood of an event using prev parameter values
double lemur::cluster::PLSA::getAverageLikelihoodPrev() {
double answer = 0.0;
int d;
lemur::api::TERMID_T w;
int events = 0;
for (d = 1; d < sizeD; d++) {
lemur::utility::HashFreqVector *freqs = testData[d];
freqs->startIteration();
while (freqs->hasMore()) { // TERM
int freq;
freqs->nextFreq(w, freq);
answer += jointEstimate(d, w);
events++;
}
}
answer /= events;
return answer;
}

// Calculates likelihood using previous parameter estimates
double lemur::cluster::PLSA::jointEstimate (int indexD, int indexW) {
int z;
double sum = 0.0;
for(z = 0; z < sizeZ; z++)
sum += p_z_prev[z]*p_w_z_prev[indexW][z]*p_d_z_prev[indexD][z];
return sum;
}

// Calculates likelihood using current parameter estimates
double lemur::cluster::PLSA::jointEstimateCurrent (int indexD, int indexW) {
int z;
double sum = 0.0;
for(z = 0; z < sizeZ; z++)
sum += p_z_current[z]*p_w_z_current[indexW][z]*p_d_z_current[indexD][z];
return sum;
}

// Calculates likelihood using best parameter estimates found
// (over all restarts)
double lemur::cluster::PLSA::jointEstimateBest(int indexD, int indexW) {
int z;
double sum = 0.0;
for(z = 0; z < sizeZ; z++)
sum += p_z_best[z]*p_w_z_best[indexW][z]*p_d_z_best[indexD][z];
return sum;
}

// jointestimateBeta is joint estimate where terms in summation
// were raised to power beta.
double lemur::cluster::PLSA::jointEstimateBeta (int indexD, int indexW) {
int z;
double sum = 0.0;
for(z = 0; z < sizeZ; z++)
sum += p_z_prev[z]*pow(p_w_z_prev[indexW][z]*p_d_z_prev[indexD][z], beta);
return sum;
}

// This routine gets the ball rolling
void lemur::cluster::PLSA::iterateWithRestarts() {
iterate();
cerr << "RESTART\tLogLikelihood\tTestDataLogLikelihood\n";
cerr << 0 << "\t" << logLikelihood() << "\t" << validateDataLogLikelihood()
<< endl;
for(int count = 0; count < numberOfRestarts - 1 ; count++) {
iterate();
cerr << "RESTART\tLogLikelihood\tTestDataLogLikelihood\n";
cerr << (count + 1) << "\t" << logLikelihood() << "\t"
<< validateDataLogLikelihood() << endl;
}
setPrevToBest();
setCurrentToBest();
cerr << " FINAL LOG-LIKELIHOOD " << validateDataLogLikelihood()
<< " Avg. Likelihood: " << getAverageLikelihood() << endl;
}

// This is the main routine of the model training
void lemur::cluster::PLSA::iterate() {
double lastA; // These two variables help check for
double newA = 1; // numerical errors due to overfitting
int iterNum = 0;
initializeParameters();
beta = startBeta;
lastA = getAverageLikelihoodPrev();
cerr << "Starting average likelihood: " << lastA << endl;
cerr << "iteration\ttLL\ttestLL\tAvgLL" << endl;
double LL = -DBL_MAX;
double prevTestLL = validateDataLogLikelihood();
double currentTestLL = -DBL_MAX;
newA = 1;

while(iterNum < numberOfIterations && beta > betaMin) {
LL = interleavedIterationEM();
currentTestLL = validateCurrentLogLikelihood();
newA = getAverageLikelihood();
cerr << iterNum << "\t" << LL << "\t" << currentTestLL << "\t"
<< newA << endl;
if((currentTestLL - annealcue) < prevTestLL || newA < lastA) {
currentTestLL = prevTestLL;
beta *= betaModifier;
iterNum--;
} else {
lastA = newA;
setPrevToCurrent();
prevTestLL = currentTestLL;
}
iterNum++;
}
// Only update best parameters if there is clear
// improvement
// Do we want to look at current?
if (newA > lastA && newA > bestA && currentTestLL > prevTestLL) {
if (currentTestLL > bestTestLL) {
setBestToCurrent();
bestTestLL = currentTestLL;
bestA = newA;
}
} else {
if (lastA > bestA) {
if (prevTestLL > bestTestLL ) {
bestTestLL = prevTestLL;
setBestToPrev();
bestA = lastA;
}
}
}
}

void lemur::cluster::PLSA::initializeParameters() {
int i, j;
double sum;
// initializing prev
// Make sure we can use drand48 w/ win...
// initialize p_z_prev
sum = 0.0;
for(i = 0; i < sizeZ; i++) {
p_z_prev[i] = drand48()*drand48();
sum += p_z_prev[i];
}
// normalize:
for(i = 0; i < sizeZ; i++)
p_z_prev[i] = p_z_prev[i]/sum;
// initialize p_w_z_prev
for(j = 0; j < sizeZ; j++) {
sum = 0.0;
double rnd;
for(i = 1; i < sizeW; i++) {
rnd = drand48()*drand48();
sum += rnd;
p_w_z_prev[i][j] = rnd;
}
for(i = 1; i < sizeW; i++)
p_w_z_prev[i][j] = p_w_z_prev[i][j]/sum;
}
// initialize p_d_z_prev
for(j = 0; j < sizeZ; j++) {
sum = 0.0;
double rnd;
for(i = 1; i < sizeD; i++) {
rnd = drand48()*drand48();
sum += rnd;
p_d_z_prev[i][j] = rnd;
}
for(i = 1; i < sizeD; i++)
p_d_z_prev[i][j] = p_d_z_prev[i][j]/sum; }
}

/* logLikelihood calculates the log-likelihood of the training data using the "previous" parameter estimates. Note that events with probability zero are thrown out of the likelihood calculation. This is useful when we have a word in our dictionary that doesn't occur in any document.

This technique of not using zero probability events in the log-likelihood is dangerous, since we can overfit so much that some events get zero probability when they shouldn't. This artificially lowers the log-likelihood. We use an additional metric, the Breese R value to catch this case and monitor for potential overfitting. "*/

// Calculate the log likelihood of a given data set using the supplied joint estimate method.
double lemur::cluster::PLSA::doLogLikelihood(jointfuncType jointFunc,
lemur::utility::HashFreqVector **&myData) {
int d;
lemur::api::TERMID_T w;
double sum = 0.0;
double logJoint = 0.0;
for (d = 1; d < sizeD; d++) {
lemur::utility::HashFreqVector *freqs = myData[d];
freqs->startIteration();
while (freqs->hasMore()) { // TERM
int freq;
freqs->nextFreq(w, freq);
logJoint = log((this->*jointFunc)(d, w));
if (logJoint > -HUGE_VAL) {
sum += freq * logJoint;
}
}
}
return sum;
}

// Calculate the training data log-likelihood using prev parameters
double lemur::cluster::PLSA::logLikelihood() {
return doLogLikelihood(&lemur::cluster::PLSA::jointEstimate, data);
}
// Calculate the hold out data log-likelihood using prev parameters
double lemur::cluster::PLSA::validateDataLogLikelihood() {
return doLogLikelihood(&lemur::cluster::PLSA::jointEstimate, testData);
}

// Calculate the held out data log-liklihood using current parameters
double lemur::cluster::PLSA::validateCurrentLogLikelihood() {
return doLogLikelihood(&lemur::cluster::PLSA::jointEstimateCurrent, testData);
}

// Calculate the hold out data log-likelihood using the best parameters
double lemur::cluster::PLSA::bestDataLogLikelihood() {
return doLogLikelihood(&lemur::cluster::PLSA::jointEstimateBest, testData);
}

bool lemur::cluster::PLSA::readArrays() {
/// filestem passed in!
bool res = true;
ifstream if1("p_z.bin", ios_base::in | ios_base::binary);
ifstream if2("p_w_z.bin", ios_base::in | ios_base::binary);
ifstream if3("p_d_z.bin", ios_base::in | ios_base::binary);
res &= readArray(if1, lemur::cluster::PLSA::P_Z);
res &= readArray(if2, lemur::cluster::PLSA::P_W_Z);
res &= readArray(if3, lemur::cluster::PLSA::P_D_Z);
if1.close();
if2.close();
if3.close();
return res;
}

bool lemur::cluster::PLSA::readArray(ifstream& infile, enum pType which) {
bool retVal = true;
// initialize contents of best
int dim1, dim2;
int w, j;
switch (which) {
case P_Z:
infile.read((char*) &dim1, sizeof(int));
if (!(infile.gcount() == sizeof(int)))
return false;
p_z_best = new double[dim1];
sizeZ = dim1;
infile.read((char*) p_z_best, sizeof(double) * dim1);
if (!(infile.gcount() == (sizeof(double) * dim1))) {
delete[](p_z_best);
p_z_best = NULL;
return false;
}
break;
case P_W_Z:
infile.read((char*) &dim1, sizeof(int));
if (!(infile.gcount() == sizeof(int)))
return false;
infile.read((char*) &dim2, sizeof(int));
if (!(infile.gcount() == sizeof(int)))
return false;
p_w_z_best = new double *[dim1];
sizeW = dim1;
sizeZ = dim2;
p_w_z_best[0] = NULL;
// 1-based indexing
for (w = 1; w < dim1; w++) {
p_w_z_best[w] = new double [dim2];
infile.read((char*) p_w_z_best[w], sizeof(double) * dim2);
if (!(infile.gcount() == (sizeof(double) * dim2))) {
for (j = 1; j <= w; j++)
delete[](p_w_z_best[j]);
delete[](p_w_z_best);
p_w_z_best = NULL;
return false;
}
}
break;
case P_D_Z:
infile.read((char*) &dim1, sizeof(int));
if (!(infile.gcount() == sizeof(int)))
return false;
infile.read((char*) &dim2, sizeof(int));
if (!(infile.gcount() == sizeof(int)))
return false;
sizeD = dim1;
sizeZ = dim2;
p_d_z_best = new double *[dim1];
p_d_z_best[0] = NULL;
// 1-based indexing
for (w = 1; w < dim1; w++) {
p_d_z_best[w] = new double [dim2];
infile.read((char*) p_d_z_best[w], sizeof(double) * dim2);
if (!(infile.gcount() == (sizeof(double) * dim2))) {
for (j = 1; j <= w; j++)
delete[](p_d_z_best[j]);
delete[](p_d_z_best);
p_d_z_best = NULL;
return false;
}
}
break;
default:
retVal = false;
break;
}
return retVal;
}

void lemur::cluster::PLSA::writeArrays() {
// pass in a filestem
ofstream of1("p_z.bin", ios_base::out | ios_base::binary);
ofstream of2("p_w_z.bin", ios_base::out | ios_base::binary);
ofstream of3("p_d_z.bin", ios_base::out | ios_base::binary);
writeArray(of1, lemur::cluster::PLSA::P_Z);
writeArray(of2, lemur::cluster::PLSA::P_W_Z);
writeArray(of3, lemur::cluster::PLSA::P_D_Z);
of1.close();
of2.close();
of3.close();
}

void lemur::cluster::PLSA::writeArray(ofstream& ofile, enum pType which) {
int w;
switch (which) {
case P_Z:
ofile.write((const char*) &sizeZ, sizeof(int));
ofile.write((const char*) p_z_best, sizeof(double) * sizeZ);
break;
case P_W_Z:
ofile.write((const char*) &sizeW, sizeof(int));
ofile.write((const char*) &sizeZ, sizeof(int));
for (w = 1; w < sizeW; w++) {
ofile.write((const char*) p_w_z_best[w], sizeof(double) * sizeZ);
}
break;
case P_D_Z:
ofile.write((const char*) &sizeD, sizeof(int));
ofile.write((const char*) &sizeZ, sizeof(int));
for (w = 1; w < sizeD; w++) {
ofile.write((const char*) p_d_z_best[w], sizeof(double) * sizeZ);
}
break;
default:
break;
}
}

// getProb(int d, int w) calculates \Pr(d,w)
double lemur::cluster::PLSA::getProb(int d, int w) const {
double total = 0.0;
for (int z = 0; z < sizeZ; z++) {
total += p_z_best[z] * p_d_z_best[d][z] * p_w_z_best[w][z];
} return total;
}
/*==========================================================================
* Copyright (c) 2001 Carnegie Mellon University. All Rights Reserved.
*
* Use of the Lemur Toolkit for Language Modeling and Information Retrieval
* is subject to the terms of the software license set forth in the licence
* file included with this software, and also available at
* http://www.lemurproject.org/license.html
*==========================================================================
*/

#ifndef _PLSAPARAMETER_HPP
#define _PLSAPARAMETER_HPP

#include "Param.hpp"
/// General PLSA related parameters
namespace PLSAParam {
/// @name PLSA parameters
//@{
/// database index
static std::string databaseIndex;
/// number of categories
static int numCats;
/// Beta for TEM
static double beta, betaMin;
/// eta for TEM (beta = eta * beta;)
static double eta;
/// annealcue value (delta)
static double annealcue;
/// How many iterations
static int numIters;
/// How many restarts
static int numRestarts;
/// Test percentage for splitting (10%)
static int testPercentage;
/// Are we training?
static bool doTrain;
//@}

static void get() {
databaseIndex = lemur::api::ParamGetString("index","");
numCats = lemur::api::ParamGetInt("numCats", 20);
beta = lemur::api::ParamGetDouble("beta", 1);
betaMin = lemur::api::ParamGetDouble("betaMin", 0.6);
eta = lemur::api::ParamGetDouble("eta", 0.92);
annealcue = lemur::api::ParamGetDouble("annealcue", 0);
numIters = lemur::api::ParamGetInt("numIters", 100);
numRestarts = lemur::api::ParamGetInt("numRestarts", 1);
testPercentage = lemur::api::ParamGetInt("testPercentage", 10);
std::string s = lemur::api::ParamGetString("doTrain", "true");
doTrain = (s == "true");
}
};







#endif
/*==========================================================================
* Copyright (c) 2003 University of Massachusetts. All Rights Reserved.
*
* Use of the Lemur Toolkit for Language Modeling and Information Retrieval
* is subject to the terms of the software license set forth in the licence
* file included with this software, and also available at
* http://www.lemurproject.org/license.html
*==========================================================================
*/

#ifndef _PLSA_HPP
#define _PLSA_HPP
#include "common_headers.hpp"
#include <cmath>
#include <set>
#include "Index.hpp"
#include "FreqVector.hpp"
namespace lemur
{
namespace cluster
{

//forward declaration.
class PLSA;

// Needed to declare member function parameter (not sure why...).
/// Joint probability function pointer.
typedef double (PLSA::*jointfuncType)(int, int);


/// Probabilistic Latent Semantic Analysis
/// Java Reference implementation from Andrew Schein and Alexandrin Popescul
/// (Penn). PennAspect (GPL).
// Need both transient and permanent versions (store model components).
class PLSA {
public:
// need to pass in many parameters. Use sensible defaults...
// document arguments.
/// building with provided train/test partitions
PLSA(const lemur::api::Index &dbIndex, int numCats,
lemur::utility::HashFreqVector **train,
lemur::utility::HashFreqVector **validate, int numIter,
int numRestarts, double betastart,
double betastop, double anneal, double betaMod);

/// building without provided train/test partitions
PLSA(const lemur::api::Index &dbIndex, int testPercentage, int numCats, int numIter,
int numRestarts, double betastart,
double betastop, double anneal, double betaMod);
/// for using prebuilt tables.
PLSA(const lemur::api::Index &dbIndex);
virtual ~PLSA();

/// Start things going.
void iterateWithRestarts();

/// get the final values
/// P(z) vector
double *get_p_z() const {return p_z_best;}
/// P(w|z) matrix
double **get_p_w_z() const {return p_w_z_best;}
/// P(d|z) matrix
double **get_p_d_z() const {return p_d_z_best;}
/// P(d,w)
double getProb(int d, int w) const ;
// get the matrix sizes
/// number of terms
int numWords() const {return sizeW;}
/// number of docs
int numDocs() const {return sizeD;}
/// number of categories
int numCats() const {return sizeZ;}
/// should these be public?
/// On ctor if not constructing.
bool readArrays();

private:
// attributes
/// Index to use
const lemur::api::Index &ind;
/// number of categories
int sizeZ;
/// number of documents
int sizeD;
/// number of words
int sizeW;

/// train d->w freq list
lemur::utility::HashFreqVector **data; // passed in
/// test (validation) d->w freq list
lemur::utility::HashFreqVector **testData; // passed in
/// w->d inverted index for M step of P(w | z)
set<int, less<int> > *invIndex; // constructed

/// Beta for TEM
double startBeta, beta, betaMin;
/// eta for TEM (beta = eta * beta;)
double betaModifier;
/// annealcue value (delta)
double annealcue;
/// used in M step for p_z
int R;
/// How many iterations
int numberOfIterations;
/// How many restarts
int numberOfRestarts;
/// Best log likelihood on the test data so far.
double bestTestLL;
/// Best average log likelihood on the test data so far.
double bestA;
/// have we only loaded existing tables from files
bool bestOnly;
/// did we allocate the test/train vectors?
bool ownMem;
/// P(z) vector current iteration
double *p_z_current;
/// P(w|z) matrix current iteration
double **p_w_z_current;
/// P(d|z) matrix current iteration
double **p_d_z_current;

/// P(z) vector previous iteration
double *p_z_prev;
/// P(w|z) matrix previous iteration
double **p_w_z_prev;
/// P(d|z) matrix previous iteration
double **p_d_z_prev;

/// P(z) vector best iteration
double *p_z_best;
/// P(w|z) matrix best iteration
double **p_w_z_best;
/// P(d|z) matrix best iteration
double **p_d_z_best;

// methods
/// Copy current iteration data to previous iteration data
void setPrevToCurrent();
/// Copy best iteration data to current iteration data
void setCurrentToBest();
/// Copy current iteration data to best iteration data
void setBestToCurrent();
/// Copy previous iteration data to best iteration data
void setBestToPrev();
/// Copy best iteration data to previous iteration data
void setPrevToBest();

/// Calculate the average likelihood of an event in the
/// test data using the current iteration estimates.
double getAverageLikelihood();
/// Calculate the average likelihood of an event in the
/// test data using the previous iteration estimates.
double getAverageLikelihoodPrev();

/// Estimates P(d,w) using previous parameter estimates
double jointEstimate (int indexD, int indexW);
/// Estimates P(d,w) using current parameter estimates
double jointEstimateCurrent (int indexD, int indexW);
/// Estimates P(d,w) using best parameter estimates
double jointEstimateBest (int indexD, int indexW);
/// Joint estimate using previous parameter estimates where
/// the terms in summation are raised to power beta.
double jointEstimateBeta (int indexD, int indexW);

/// main routine for model training.
void iterate();
/// Initialize the prev probability arrays to random values.
void initializeParameters();

/// Calculate the log likelihood of a given data set using the supplied
/// joint estimate method.
double doLogLikelihood(jointfuncType, lemur::utility::HashFreqVector **&myData);
/// Calculate the training data log-likelihood using prev parameters
double logLikelihood();
/// Calculate the hold out data log-likelihood using prev parameters
double validateDataLogLikelihood();
/// Calculate the hold out data log-likelihood using current parameters
double validateCurrentLogLikelihood();
/// Calculate the hold out data log-likelihood using the best parameters
double bestDataLogLikelihood();
/// performs one EM iteration, returns log likelihood of training data
double interleavedIterationEM();
/// Select training/test events
void selectTestTrain(int testPercent);
/// Initialize attributes.
void init();
/// Initialize R and w->d inverted list.
void initR();
/// read/write array options.
enum pType {P_Z = 0, P_W_Z = 1, P_D_Z = 2};
/// write out all the arrays to file.
void writeArrays();
/// Read a probability array (matrix) from a file
bool readArray(ifstream& infile, enum pType which);
/// Write a probability array (matrix) to a file
void writeArray(ofstream& ofile, enum pType which);
};
}
}
#endif /* _PLSA_HPP */’

Κατά την εισαγωγή του συνόλου των δεδομένων της συλλογής (άνω των 15.000 κειμένων σε μορφή .txt, .html, .doc και .pdf) στο περιβάλλον Lemur, ακολουθήθηκαν ορισμένα βήματα προ-επεξεργασίας τους, προκειμένου να βελτιωθεί η ποιότητα, η ακρίβεια και η αποτελεσματικότητα της ποσοτικής ανάλυσης.

Τα δεδομένα καθαρίστηκαν, με την αφαίρεση όλων των αποσπασμάτων που δεν αποδίδονταν στους πρωτότυπους δημιουργούς αλλά αφορούσαν σε μεταγενέστερα σχόλια τρίτων και άλλα τεχνικά δεδομένα και μεταδεδομένα των εγγράφων, αλλά και την αφαίρεση των τετριμμένων λέξεων (stop-words) σύμφωνα με τη λίστα του Fox για την αγγλική γλώσσα. Στη συνέχεια, ελέγχθηκαν για την ύπαρξη διπλοτύπων και μειώθηκαν ανάλογα.

Μετά τον καθαρισμό και τη μείωση των δεδομένων, δημιουργήθηκε ένα ευρετήριο Indri των κειμένων και των όρων τους, με τη διαδοχική χρήση των αλγορίθμων Krovetz και Porter για την αναγνώριση των ριζών των λέξεων (stemming), συνδυάζοντας την ακρίβεια των αποτελεσμάτων του πρώτου αλγορίθμου με την αποδοτικότητα του δεύτερου.

Ο μηχανισμός Indri επιλέχθηκε:

α. για τη δυνατότητα κλιμάκωσης (scalability) του ευρετηρίου στα 500 εκατομμύρια έγγραφα, ώστε να υποστηρίζει το μελλοντικό εμπλουτισμό των δεδομένων με άλλες θεματικές συλλογές και τη συγκριτική ποσοτική ανάλυσή τους με δεδομένα του Διαδικτύου, καθώς και

β. για τη δυνατότητα διανομής του ευρετηρίου (index distribution) σε συστάδες υπολογιστών (computer clusters), προκειμένου να πραγματοποιούνται συνεργατικά οι πολύπλοκοι μαθηματικοί υπολογισμοί της μεθόδου PLSA, μειώνοντας έτσι ραγδαία τον απαιτούμενο χρόνο εκτέλεσης ενός ερωτήματος αναζήτησης στο ευρετήριο.

Η αξιολόγηση του κατηγοριοποιητή PLSA βασίστηκε στη μέθοδο hold-out. Το σύνολο των δεδομένων της συλλογής χωρίστηκε τυχαία σε δύο ανεξάρτητα σύνολα, ένα σύνολο εκπαίδευσης με το 70% των δεδομένων και ένα σύνολο δοκιμής με το 30% των δεδομένων. Τα δεδομένα εκπαίδευσης χρησιμοποιήθηκαν για τον ορισμό του μοντέλου PLSA, ενώ η ακρίβεια της κατηγοριοποίησης υπολογίστηκε με τα δοκιμαστικά δεδομένα. Η διαδικασία αξιολόγησης επαναλήφθηκε μέχρι να οριστικοποιηθούν οι παράμετροι ελέγχου που αποδίδουν τη μέγιστη ακρίβεια στο μοντέλο.