From 65d8630c782ff123e9db99059ec722f49dd39bcc Mon Sep 17 00:00:00 2001 From: PrateethChagari <prateethchagari@gmail.com> Date: Wed, 18 Dec 2019 19:25:52 -0600 Subject: [PATCH] Add files for search engine --- CommonUtilities.py | 4 + IndexBuilder.py | 55 ++++++++ README.md | 23 ++++ Search.py | 264 ++++++++++++++++++++++++++++++++++++ SearchEngine.py | 58 ++++++++ SearchResult.py | 21 +++ corpus_processor.py | 99 ++++++++++++++ data.py | 45 +++++++ index.html | 105 +++++++++++++++ label_finder.py | 81 +++++++++++ label_ranker.py | 233 ++++++++++++++++++++++++++++++++ label_topic.py | 129 ++++++++++++++++++ neuralnetregressor.py | 71 ++++++++++ passenger_wsgi.py | 1 + plsaNeuralNetModel.model | Bin 0 -> 64297 bytes pmi.py | 111 +++++++++++++++ text.py | 74 ++++++++++ train_academicdata.py | 284 +++++++++++++++++++++++++++++++++++++++ 18 files changed, 1658 insertions(+) create mode 100644 CommonUtilities.py create mode 100644 IndexBuilder.py create mode 100644 Search.py create mode 100644 SearchEngine.py create mode 100644 SearchResult.py create mode 100644 corpus_processor.py create mode 100644 data.py create mode 100644 index.html create mode 100644 label_finder.py create mode 100644 label_ranker.py create mode 100644 label_topic.py create mode 100644 neuralnetregressor.py create mode 100644 passenger_wsgi.py create mode 100644 plsaNeuralNetModel.model create mode 100644 pmi.py create mode 100644 text.py create mode 100644 train_academicdata.py diff --git a/CommonUtilities.py b/CommonUtilities.py new file mode 100644 index 0000000..400c41e --- /dev/null +++ b/CommonUtilities.py @@ -0,0 +1,4 @@ +class CommonUtilities: + @staticmethod + def obj_dict(obj): + return obj.__dict__ \ No newline at end of file diff --git a/IndexBuilder.py b/IndexBuilder.py new file mode 100644 index 0000000..5536a29 --- /dev/null +++ b/IndexBuilder.py @@ -0,0 +1,55 @@ +import os, os.path +from whoosh.index import create_in +from whoosh.fields import * +import xml.dom.minidom as dom +import xml.etree.ElementTree as ET +from whoosh.qparser import MultifieldParser, OrGroup, QueryParser + +class IndexBuilder: + def __init__(self): + self.schema = Schema(paper=ID(stored=True), abstract=TEXT(stored=True), title=TEXT(stored=True), introduction=TEXT(stored=True)) + self.ix = create_in("index", schema) + self.path = "papers_to_index/" + + def build(self): + file_list = [] + for (dirpath, dirnames, filenames) in os.walk(path): + file_list.extend(filenames) + + writer = ix.writer() + for file in file_list: + fileNew = dirpath+file + count += 1 + paperId = os.path.splitext(file) + f = open(fileNew, encoding="utf8") + for line in f: + xmltree = dom.parseString(line) + if (xmltree.getElementsByTagName('abstract')): + if xmltree.getElementsByTagName('abstract')[0].firstChild is None: + abstractField = "" + else: + abstractField = xmltree.getElementsByTagName('abstract')[0].firstChild.nodeValue + + elif (xmltree.getElementsByTagName('title')): + if xmltree.getElementsByTagName('title')[0].firstChild is None: + titleField = "" + else: + titleField = xmltree.getElementsByTagName('title')[0].firstChild.nodeValue + + elif (xmltree.getElementsByTagName('introduction')): + if (xmltree.getElementsByTagName('introduction')[0].firstChild is None): + introductionField = "" + else: + introductionField = xmltree.getElementsByTagName('introduction')[0].firstChild.nodeValue + + writer.add_document(paper = paperId, abstract = abstractField, title = titleField, introduction = introductionField) + + writer.commit() + writer.close() + +def build(): + indexbuilder = IndexBuilder() + indexbuilder.build() + +if __name__== "__main__": + build() diff --git a/README.md b/README.md index cff8103..94f64c5 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,25 @@ # cs510-project +Before running the software, here are a few points about the data files +1. We have not provided the dataset and corresponding datafiles explicitly due to size constraints +2. The dataset (academic papers) need to be placed in a folder ‘papers_to_index’ +3. Each document is expected to be an xml file with the following fields: + Paper Abstract Title Introduction + +4. Some other files are expected to be present before the code can run successfully: +5. docs.json: a json file consisting of all the documents. Each line represents one document and contains, at the least, keyPhrases, paperAbstract, title, introduction, docno (document number), numKeyReferences (number of key references), numCitedBy and numKeyCitations. These fields are used for generating features for training our neural network. +6. train_queries.json: Each line is of the form {"qid": "the query id", "query": "the query string", "ana": {the annotated entity id and frequency}} +7. train_queries_qrel: relevance judgement for the training queries. +8. The above 3 files are from Freebase and are similar to the files used in the searchengine assignment +9. supervisedTrain.txt: The file which is generated on training the neural net with the training data. Contains the feature values. + +The code is provided at the github link given at the beginning of this report +The UI can be viewed directly by accessing the link at the beginning of the report. +To run the software, use the following steps: +1. Assuming that the data is placed as explained above: +2. To build the index for the dataset, run `python3 IndexBuilder.py`. The index will be built from the documents present in ‘papers_to_index’ in the current directory; and creates the index in a folder ‘index’. Create an empty folder ‘index’ if the code automatically doesn’t create it. +3. Once the index is built (might take a while), we want to generate our features for training the neural network. Ensure that the necessary files are placed. The files are similar to the files provided for search engine assignment and named ‘docs.json’, ‘train_queries.json’ and ‘train_queries_qrel’. The features will be created in a file ‘supervisedTrain.txt’ when the following command is executed: ‘python3 train_academicdata.py’ +4. Execute `python3 neuralnetregressor.py` to train the model on the features generated and dumps the model into a file `plsaNeuralNetModel.model` +5. After the neural net is trained, the software is ready to be run. Execute `python3 SearchEngine.py` which is currently configured to run on localhost. +6. Open the index.html file on the browser which is configured to connect to the localhost. +7. Search for the query. Topic labelling will run every time a query is searched, and appropriate results are returned diff --git a/Search.py b/Search.py new file mode 100644 index 0000000..d5edd73 --- /dev/null +++ b/Search.py @@ -0,0 +1,264 @@ +from whoosh.index import create_in, open_dir +from whoosh.fields import * +from whoosh.qparser import MultifieldParser, OrGroup, QueryParser +from whoosh import scoring +from joblib import load +from label_topic import TopicLabels +from collections import defaultdict + +class Myclass: + def __init__(self): + self.abBm25 = 0.0 + self.introBm25 = 0.0 + self.tiBm25 = 0.0 + self.abTf = 0.0 + self.introTf = 0.0 + self.tiTf = 0.0 + self.abPl2 = 0.0 + self.introPl2 = 0.0 + self.tiPl2 = 0.0 + self.abDf = 0.0 + self.introDf = 0.0 + self.tiDf = 0.0 + self.abstarct = "" + self.introduction = "" + self.title = "" + self.paper = "" + self.neuralScore = 0.0 + +class Search: + + def getResultDocs(self, query): + docs = [] + indexDir = open_dir("index/") + og = OrGroup.factory(0.9) + indexSearcher = indexDir.searcher() + + # Parser for multiple fields + abQueryParser = QueryParser("abstract", indexSearcher.schema, group=og) + abQueryObject = abQueryParser.parse(query) + introQueryParser = QueryParser("introduction", indexSearcher.schema, group=og) + introQueryObject = introQueryParser.parse(query) + tiQueryParser = QueryParser("title", indexSearcher.schema, group=og) + tiQueryObject = tiQueryParser.parse(query) + + # Extracting features + features = defaultdict() + abResults = indexSearcher.search(abQueryObject, limit = 300) + introResults = indexSearcher.search(introQueryObject, limit = 300) + tiResults = indexSearcher.search(tiQueryObject, limit = 300) + for result in abResults: + paper = result["paper"] + if (paper in features): + features[paper].abBm25 = result.score + else: + temp = Myclass() + temp.abBm25 = result.score + temp.abstarct = result["abstract"] + temp.title = result["title"] + temp.introduction = result["introduction"] + features[paper] = temp + + for result in introResults: + paper = result["paper"] + if (paper in features): + features[paper].introBm25 = result.score + else: + temp = Myclass() + temp.pabm25 = result.score + temp.abstarct = result["abstract"] + temp.title = result["title"] + temp.introduction = result["introduction"] + features[paper] = temp + + for result in tiResults: + paper = result["paper"] + if (paper in features): + features[paper].tiBm25 = result.score + else: + temp = Myclass() + temp.tiBm25 = result.score + temp.abstarct = result["abstract"] + temp.title = result["title"] + temp.introduction = result["introduction"] + features[paper] = temp + + + w = scoring.TF_IDF() + idfIndexSearcher = indexDir.searcher(weighting=w) + abIdfResults = idfIndexSearcher.search(abQueryObject, limit = 300) + introIdfResults = idfIndexSearcher.search(introQueryObject, limit = 300) + tiIdfResults = idfIndexSearcher.search(tiQueryObject, limit = 300) + for result in abIdfResults: + paper = result["paper"] + if (paper in features): + features[paper].abTf = result.score + else: + temp = Myclass() + temp.abTf = result.score + temp.abstarct = result["abstract"] + temp.title = result["title"] + temp.introduction = result["introduction"] + features[paper] = temp + + for result in introIdfResults: + paper = result["paper"] + if (paper in features): + features[paper].introTf = result.score + else: + temp = Myclass() + temp.introTf = result.score + temp.abstarct = result["abstract"] + temp.title = result["title"] + temp.introduction = result["introduction"] + features[paper] = temp + + for result in tiIdfResults: + paper = result["paper"] + if (paper in features): + features[paper].tiTf = result.score + else: + temp = Myclass() + temp.tiTf = result.score + temp.abstarct = result["abstract"] + temp.title = result["title"] + temp.introduction = result["introduction"] + features[paper] = temp + + w = scoring.PL2() + plIndexSearcher = indexDir.searcher(weighting=w) + abPlResults = plIndexSearcher.search(abQueryObject, limit = 300) + introPlResults = plIndexSearcher.search(introQueryObject, limit = 300) + tiPlResults = plIndexSearcher.search(tiQueryObject, limit = 300) + for result in abPlResults: + paper = result["paper"] + if (paper in features): + features[paper].abPl2 = result.score + else: + temp = Myclass() + temp.abPl2 = result.score + temp.abstarct = result["abstract"] + temp.title = result["title"] + temp.introduction = result["introduction"] + features[paper] = temp + + for result in introPlResults: + paper = result["paper"] + if (paper in features): + features[paper].introPl2 = result.score + else: + temp = Myclass() + temp.introPl2 = result.score + temp.abstarct = result["abstract"] + temp.title = result["title"] + temp.introduction = result["introduction"] + features[paper] = temp + + for result in tiPlResults: + paper = result["paper"] + if (paper in features): + features[paper].tiPl2 = result.score + else: + temp = Myclass() + temp.tiPl2 = result.score + temp.abstarct = result["abstract"] + temp.title = result["title"] + temp.introduction = result["introduction"] + features[paper] = temp + + w = scoring.DFree() + dfIndexSearcher = indexDir.searcher(weighting=w) + abDfResults = dfIndexSearcher.search(abQueryObject, limit = 300) + introDfResults = dfIndexSearcher.search(introQueryObject, limit = 300) + tiDfResults = dfIndexSearcher.search(tiQueryObject, limit = 300) + for result in abDfResults: + paper = result["paper"] + if (paper in features): + features[paper].abDf = result.score + else: + temp = Myclass() + temp.abDf = result.score + temp.abstarct = result["abstract"] + temp.title = result["title"] + temp.introduction = result["introduction"] + features[paper] = temp + + for result in introDfResults: + paper = result["paper"] + if (paper in features): + features[paper].introDf = result.score + else: + temp = Myclass() + temp.introDf = result.score + temp.abstarct = result["abstract"] + temp.title = result["title"] + temp.introduction = result["introduction"] + features[paper] = temp + + for result in tiDfResults: + paper = result["paper"] + if (paper in features): + features[paper].tiDf = result.score + else: + temp = Myclass() + temp.tiDf = result.score + temp.abstarct = result["abstract"] + temp.title = result["title"] + temp.introduction = result["introduction"] + features[paper] = temp + + model = load('plsaNeuralNetModel.model') + for key, val in features.items(): + ab = set(val.abstarct.split()) + intro = set(val.introduction.split()) + ti = set(val.title.split()) + q = set(query.split()) + val.neuralScore = model.predict([[val.abBm25, val.introBm25, val.tiBm25, \ + val.abTf, val.introTf, val.tiTf, \ + val.abPl2, val.introPl2, val.tiPl2, \ + val.abDf, val.introDf, val.tiDf, \ + len(ab), len(intro), len(ti), len(q), \ + len(q.intersection(ab)), len(q.intersection(intro)), len(q.intersection(ti))]])[0] + + + + + + results = dict(sorted(features.items(), key=lambda x: x[1].neuralScore, reverse=True)[:100]) + + searchResults = list() + + for key, val in results.items(): + contents = dict() + contents["paper"] = key + contents["abstract"] = val.abstarct + + docs.append(val.abstarct) + contents["title"] = val.title + contents["introduction"] = val.introduction + contents["topics"] = "" + searchResults.append(contents) + + doc_topics, labels = TopicLabels.get_topic_labels(docs, + n_topics=10, + n_top_words=20, + preprocessing_steps=['tag'], + n_cand_labels=100, + label_min_df=5, + label_tags=['NN,NN', 'JJ,NN'], + n_labels=10, + lda_random_state=12345, + lda_n_iter=400) + + topic_labels = dict() + for key, val in doc_topics.items(): + for i, label in enumerate(labels): + if i == val: + topic_labels[key] = label + + for i, result in enumerate(searchResults): + # TO-DO: format the output, remove stemming + result["topics"] = str(topic_labels[i]) + + return searchResults + diff --git a/SearchEngine.py b/SearchEngine.py new file mode 100644 index 0000000..9fce754 --- /dev/null +++ b/SearchEngine.py @@ -0,0 +1,58 @@ +from flask import Flask, jsonify, Response, send_file, request +from flask_cors import CORS +from Search import Search +from SearchResult import SearchResult +from CommonUtilities import CommonUtilities +from logging import Formatter, FileHandler +import json +import logging + + +app = Flask(__name__) +CORS(app) +logger = None + + +@app.route('/') +def index(): + return send_file('index.html') + +@app.route('/search/', methods=['GET', 'POST']) +def get_results(): + global search + json_data = request.get_data(as_text=True) + data = json.loads(json_data) + query = data['query'] + search = Search() + results = search.getResultDocs(query) + queryResult = list() + maxResultCount = int(data['page_count']) * 10 + for result in results[maxResultCount - 10 : maxResultCount]: + queryResult.append(SearchResult(result, query)) + + response = Response(response=json.dumps(queryResult, default=CommonUtilities.obj_dict), status=200, mimetype='application/json') + response.headers.add('Access-Control-Allow-Origin', '*') + return response + +@app.route('/relevance/', methods=["POST"]) +def relevance(): + json_data = request.get_data(as_text=True) + data = json.loads(json_data) + # logger.info(json_data) + response = Response(response=json.dumps("OK", default=CommonUtilities.obj_dict), status=200, mimetype='application/json') + return response + +def setLogger(): + global logger + logger = logging.getLogger('relevance') + logger.setLevel(logging.INFO) + ch = logging.FileHandler('Relevance.json') + ch.setLevel(logging.INFO) + formatter = logging.Formatter('%(message)s') + ch.setFormatter(formatter) + logger.addHandler(ch) + +if __name__ == '__main__': + search = Search() + setLogger() + app.run(debug=True) \ No newline at end of file diff --git a/SearchResult.py b/SearchResult.py new file mode 100644 index 0000000..a25e2c8 --- /dev/null +++ b/SearchResult.py @@ -0,0 +1,21 @@ +class SearchResult: + def __init__(self, result, query): + self.id = 'paperid' + str(result['paper'][0][:8].encode('utf-8')).replace(" ", "") + self.title = result['title'] + self.abstract = self.highlightQuery(result['abstract'], query) + self.topics = result['topics'] + self.url = self.getUrlFormat(result['paper'][0]) + + def getUrlFormat(self, file): + return "http://www.aclweb.org/anthology/" + file[0:8] + ".pdf" + + def highlightQuery(self, text, query): + keywords = [w.lower() for w in query.split()] + words = text.split() + + for i, word in enumerate(words): + for keyword in keywords: + if keyword in word.lower(): + words[i] = '<b>' + word + '</b>' + break + return ' '.join(words).rstrip("\"").rstrip(".") + "." \ No newline at end of file diff --git a/corpus_processor.py b/corpus_processor.py new file mode 100644 index 0000000..97db47b --- /dev/null +++ b/corpus_processor.py @@ -0,0 +1,99 @@ +import nltk +from toolz.functoolz import partial +from nltk.stem.porter import PorterStemmer + + +class CorpusBaseProcessor(object): + """ + Class that processes a corpus + """ + def transform(self, docs): + """ + Parameter: + ----------- + docs: list of (string|list of tokens) + input corpus + + Return: + ---------- + list of (string|list of tokens): + transformed corpus + """ + raise NotImplemented + + +class CorpusWordLengthFilter(CorpusBaseProcessor): + def __init__(self, minlen=2, maxlen=35): + self._min = minlen + self._max = maxlen + + def transform(self, docs): + """ + Parameters: + ---------- + docs: list of list of str + the tokenized corpus + """ + assert isinstance(docs[0], list) + valid_length = (lambda word: + len(word) >= self._min and + len(word) <= self._max) + filter_tokens = partial(filter, valid_length) + return list(map(filter_tokens, docs)) + + +porter_stemmer = PorterStemmer() + + +class CorpusStemmer(CorpusBaseProcessor): + def __init__(self, stem_func=porter_stemmer.stem): + """ + Parameter: + -------------- + stem_func: function that accepts one token and stem it + """ + self._stem_func = stem_func + + def transform(self, docs): + """ + Parameter: + ------------- + docs: list of list of str + the documents + + Return: + ------------- + list of list of str: the stemmed corpus + """ + assert isinstance(docs[0], list) + stem_tokens = partial(map, self._stem_func) + docs = [[porter_stemmer.stem(token) for token in doc] for doc in docs] + return docs + + +class CorpusPOSTagger(CorpusBaseProcessor): + def __init__(self, pos_tag_func=nltk.pos_tag): + """ + Parameter: + -------------- + pos_tag_func: pos_tag function that accepts list of tokens + and POS tag them + """ + self._pos_tag_func = pos_tag_func + + def transform(self, docs): + """ + Parameter: + ------------- + docs: list of list of str + the documents + + Return: + ------------- + list of list of str: the tagged corpus + """ + assert isinstance(docs[0], list) + import nltk + nltk.download('averaged_perceptron_tagger') + docs = [nltk.pos_tag(doc) for doc in docs] + return docs diff --git a/data.py b/data.py new file mode 100644 index 0000000..6eef8d0 --- /dev/null +++ b/data.py @@ -0,0 +1,45 @@ +import os +import nltk +import itertools +import codecs +from toolz.functoolz import compose +import _pickle as pickle + +CURDIR = os.path.dirname(os.path.realpath(__file__)) + + +def load_line_corpus(path, tokenize=True): + import nltk + nltk.download('punkt') + docs = [] + with codecs.open(path, "r", "utf8") as f: + for l in f: + if tokenize: + sents = nltk.sent_tokenize(l.strip().lower()) + docs.append(list(itertools.chain(*map( + nltk.word_tokenize, sents)))) + else: + docs.append(l.strip()) + return docs + + +def load_nips(years=None, raw=False): + # load data + if not years: + years = xrange(2008, 2015) + files = ['nips-{}.dat'.format(year) + for year in years] + + docs = [] + for f in files: + docs += load_line_corpus('{}/datasets/{}'.format(CURDIR, f), + tokenize=(not raw)) + + return docs + + +def load_lemur_stopwords(): + with codecs.open(CURDIR + '/datasets/lemur-stopwords.txt', + 'r', 'utf8') as f: + return map(lambda s: s.strip(), + f.readlines()) diff --git a/index.html b/index.html new file mode 100644 index 0000000..f230000 --- /dev/null +++ b/index.html @@ -0,0 +1,105 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +<title>Search Engine for Research Papers</title> +<meta charset="utf-8"> +<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script> +<script> + var page_count + var ip_address + var URL="http://127.0.0.1:5000/" + + function relevant(relevance, paperId) { + $('input[name="'+paperId+'"]').attr('disabled', 'disabled'); + $.ajax({ + type: 'POST', + url: URL + "relevance/", //add endpoint API + data: JSON.stringify({query: query, relevance: relevance, id: paperId, ip_address: ip_address}), + }); + + } + + function search(){ + + $.ajax({ + type: "POST", + url: URL + "search/", + data: JSON.stringify({query: query, page_count: page_count}), + contentType: "applicaton/json; charset=utf-8", + success: function(results){ + var resultLength = results.length + + for (var i=0; i < resultLength; ++i) + { + var paperId = results[i].id + $(".searchResults").append("<br>"); + $(".searchResults").append("<table>"); + $(".searchResults").append("<tr><td>"); + $(".searchResults").append("<p style=\"font-size:18px;\">" + "<a target=\"_blank\" href=\"" + results[i].url + "\">"+results[i].title+"</a>" + "</p>"); + $(".searchResults").append("</td></tr>"); + $(".searchResults").append("<tr><td>"); + $(".searchResults").append("<input type=\"radio\" name=\""+paperId+"\" onclick=\"relevant(true, name)\"><label> Relevant</label> ") + $(".searchResults").append(" <input type=\"radio\" name=\""+paperId+"\" onclick=\"relevant(false, name)\"><label> Non-Relevant</label>") + $(".searchResults").append("</td></tr>"); + $(".searchResults").append("<tr><td>"); + $(".searchResults").append("<p style=\"border-style: groove;font-size:14px;\"> <b>Topic labels: </b>"+ results[i].topics +"</p>"); + $(".searchResults").append("</td></tr>"); + $(".searchResults").append("<pre style=\"white-space: pre-wrap;\">"+results[i].abstract+"</pre>") + $(".searchResults").append("</td></tr>"); + $(".searchResults").append("</table>"); + + + } + if(resultLength == 0) { + $(".searchResults").append("<br>"); + $(".searchResults").append("<p style=\"font-size:18px; color:Red\"> There are no (more) results to show for the given query </p>"); + } + if (resultLength < 10) + $("#more").hide(); + else + $("#more").show(); + + } + }); + + function callback(response){ + console.log(response); + } + } + $(document).ready(function(){ + $("#searchButton").click(function(){ + $(".searchResults").empty() + page_count = 1 + query = document.getElementById("querytext").value; + search(); + }); + $("#more").click(function(){ + page_count++ + query = document.getElementById("querytext").value; + search(); + }); + + $.getJSON('https://api.ipify.org?format=json', function(data){ + ip_address = data.ip; + }); + + + }); +</script> + +</head> + +<body> +<center> +<h2>Find Research Papers</h2> + +<input id="querytext" type="text" placeholder="Search.." size=80/> + +<button type="button" id="searchButton"> Search </button> + +<div class="searchResults"> </div> +<br><br> +<button type="button" id="more" style="display:none;"><b>More results</b></button> +</center> +</body> +</html> diff --git a/label_finder.py b/label_finder.py new file mode 100644 index 0000000..fd2a664 --- /dev/null +++ b/label_finder.py @@ -0,0 +1,81 @@ +import nltk +from nltk.collocations import BigramCollocationFinder +from toolz.itertoolz import get +from toolz.functoolz import partial + + +class BigramLabelFinder(object): + def __init__(self, measure='pmi', + min_freq=20, + pos=[('NN', 'NN'), ('JJ', 'NN')]): + """ + measure: str + the measurement method, 'pmi'or 'chi_sq' + + min_freq: int + minimal frequency for the label to be considered + + pos: list of (str, str) + the POS tag contraint + """ + self.bigram_measures = nltk.collocations.BigramAssocMeasures() + assert measure in ('pmi', 'chi_sq') + self._measure_method = measure + + self._min_freq = min_freq + self._pos = pos + + def find(self, docs, top_n, strip_tags=True): + """ + Parameter: + --------------- + + docs: list of tokenized documents + + top_n: int + how many labels to return + + strip_tags: bool + whether return without the POS tags or not + + Return: + --------------- + list of tuple of str: the bigrams + """ + # if apply pos constraints + # check the pos properties + if self._pos: + assert isinstance(self._pos, list) + for pair in self._pos: + assert isinstance(pair, tuple) or isinstance(pair, list) + assert len(pair) == 2 # because it's bigram + + score_func = getattr(self.bigram_measures, + self._measure_method) + + finder = BigramCollocationFinder.from_documents(docs) + finder.apply_freq_filter(self._min_freq) + + if self._pos: + valid_pos_tags = set([pair for pair in self._pos]) + valid_bigrams = [] + bigrams = map(partial(get, 0), # get the bigram + finder.score_ngrams(score_func)) + cnt = 0 + for bigram in bigrams: + if tuple(map(partial(get, 1), bigram)) in valid_pos_tags: + valid_bigrams.append(bigram) + cnt += 1 + if cnt == top_n: # enough + break + + if strip_tags: + valid_bigrams = [tuple(map(partial(get, 0), bigram)) + for bigram in valid_bigrams] + + return valid_bigrams + else: + bigrams = finder.nbest(score_func, + top_n) + return bigrams + diff --git a/label_ranker.py b/label_ranker.py new file mode 100644 index 0000000..e61664a --- /dev/null +++ b/label_ranker.py @@ -0,0 +1,233 @@ +""" +Reference: +--------------------- + +Qiaozhu Mei, Xuehua Shen, Chengxiang Zhai, +Automatic Labeling of Multinomial Topic Models, 2007 +""" +import numpy as np +from scipy.stats import entropy as kl_divergence + + +class LabelRanker(object): + """ + + """ + def __init__(self, + apply_intra_topic_coverage=True, + apply_inter_topic_discrimination=True, + mu=0.7, + alpha=0.9): + self._coverage = apply_intra_topic_coverage + self._discrimination = apply_inter_topic_discrimination + self._mu = mu + self._alpha = alpha + + def label_relevance_score(self, + topic_models, + pmi_w2l): + """ + Calculate the relevance scores between each label and each topic + + Parameters: + --------------- + topic_models: numpy.ndarray(#topics, #words) + the topic models + + pmi_w2l: numpy.ndarray(#words, #labels) + the Point-wise Mutual Information(PMI) table of + the form, PMI(w, l | C) + + Returns; + ------------- + numpy.ndarray, shape (#topics, #labels) + the scores of each label on each topic + """ + assert topic_models.shape[1] == pmi_w2l.shape[0] + return np.asarray(np.asmatrix(topic_models) * + np.asmatrix(pmi_w2l)) + + def label_discriminative_score(self, + relevance_score, + topic_models, + pmi_w2l): + """ + Calculate the discriminative scores for each label + + Returns: + -------------- + numpy.ndarray, shape (#topics, #labels) + the (i, j)th element denotes the score + for label j and all topics *except* the ith + """ + assert topic_models.shape[1] == pmi_w2l.shape[0] + k = topic_models.shape[0] + return (relevance_score.sum(axis=0)[None, :].repeat(repeats=k, axis=0) + - relevance_score) / (k-1) + + def label_mmr_score(self, + which_topic, + chosen_labels, + label_scores, + label_models): + """ + Maximal Marginal Relevance score for labels. + It's computed only when `apply_intra_topic_coverage` is True + + Parameters: + -------------- + which_topic: int + the index of the topic + + chosen_labels: list<int> + indices of labels that are already chosen + + label_scores: numpy.ndarray<#topic, #label> + label scores for each topic + + label_models: numpy.ndarray<#label, #words> + the language models for labels + + Returns: + -------------- + numpy.ndarray: 1D of length #label - #chosen_labels + the scored label indices + + numpy.ndarray: same length as above + the scores + """ + chosen_len = len(chosen_labels) + if chosen_len == 0: + # no label is chosen + # return the raw scores + return (np.arange(label_models.shape[0]), + label_scores[which_topic, :]) + else: + kl_m = np.zeros((label_models.shape[0]-chosen_len, + chosen_len)) + + # the unchosen label indices + candidate_labels = list(set(range(label_models.shape[0])) - + set(chosen_labels)) + candidate_labels = np.sort(np.asarray(candidate_labels)) + for i, l_p in enumerate(candidate_labels): + for j, l in enumerate(chosen_labels): + kl_m[i, j] = kl_divergence(label_models[l_p], + label_models[l]) + sim_scores = kl_m.max(axis=1) + mml_scores = (self._alpha * + label_scores[which_topic, candidate_labels] + - (1 - self._alpha) * sim_scores) + return (candidate_labels, mml_scores) + + def combined_label_score(self, topic_models, pmi_w2l, + use_discrimination, mu=None): + """ + Calculate the combined scores from relevance_score + and discrimination_score(if required) + + Parameter: + ----------- + use_discrimination: bool + whether use discrimination or not + mu: float + the `mu` parameter in the algorithm + + Return: + ----------- + numpy.ndarray, shape (#topics, #labels) + score for each topic and label pair + """ + rel_scores = self.label_relevance_score(topic_models, pmi_w2l) + + if use_discrimination: + assert mu != None + discrim_scores = self.label_discriminative_score(rel_scores, + topic_models, + pmi_w2l) + label_scores = rel_scores - mu * discrim_scores + else: + label_scores = rel_scores + + return label_scores + + def select_label_sequentially(self, k_labels, + label_scores, label_models): + """ + Return: + ------------ + list<list<int>>: shape n_topics x k_labels + """ + n_topics = label_scores.shape[0] + chosen_labels = [] + + # don't use [[]] * n_topics ! + for _ in xrange(n_topics): + chosen_labels.append(list()) + + for i in xrange(n_topics): + for j in xrange(k_labels): + inds, scores = self.label_mmr_score(i, chosen_labels[i], + label_scores, + label_models) + chosen_labels[i].append(inds[np.argmax(scores)]) + return chosen_labels + + def top_k_labels(self, + topic_models, + pmi_w2l, + index2label, + label_models=None, + k=5): + """ + Parameters: + ---------------- + + index2label: dict<int, object> + mapping from label index in the `pmi_w2l` + to the label object, which can be string + + label_models: numpy.ndarray<#label, #words> + the language models for labels + if `apply_intra_topic_coverage` is True, + then it's must be given + + Return: + --------------- + list<list of (label, float)> + top k labels as well as scores for each topic model + + """ + + assert pmi_w2l.shape[1] == len(index2label) + + label_scores = self.combined_label_score(topic_models, pmi_w2l, + self._discrimination, + self._mu) + + if self._coverage: + assert isinstance(label_models, np.ndarray) + # TODO: can be parallel + chosen_labels = self.select_label_sequentially(k, label_scores, + label_models) + else: + chosen_labels = np.argsort(label_scores, axis=1)[:, :-k-1:-1] + return [[index2label[j] + for j in topic_i_labels] + for topic_i_labels in chosen_labels] + + def print_top_k_labels(self, topic_models, pmi_w2l, + index2label, label_models, k): + res = u"Topic labels:\n" + for i, labels in enumerate(self.top_k_labels( + topic_models=topic_models, + pmi_w2l=pmi_w2l, + index2label=index2label, + label_models=label_models, + k=k)): + res += u"Topic {}: {}\n".format( + i, + ', '.join(map(lambda l: ' '.join(l), + labels)) + ) + return res diff --git a/label_topic.py b/label_topic.py new file mode 100644 index 0000000..9692fa4 --- /dev/null +++ b/label_topic.py @@ -0,0 +1,129 @@ +import argparse +import lda +import itertools +import numpy as np +import nltk +from sklearn.feature_extraction.text import (CountVectorizer + as WordCountVectorizer) +from text import LabelCountVectorizer +from label_finder import BigramLabelFinder +from label_ranker import LabelRanker +from pmi import PMICalculator +from corpus_processor import (CorpusWordLengthFilter, + CorpusPOSTagger, + CorpusStemmer) +from data import (load_line_corpus, load_lemur_stopwords) + +class TopicLabels: + def get_topic_labels(doc, + n_topics, + n_top_words, + preprocessing_steps, + n_cand_labels, label_min_df, + label_tags, n_labels, + lda_random_state, + lda_n_iter): + """ + Refer the arguments to `create_parser` + """ + + docs = [] + for d in doc: + sents = nltk.sent_tokenize(d.strip().lower()) + docs.append(list(itertools.chain(*map(nltk.word_tokenize, sents)))) + if 'wordlen' in preprocessing_steps: + print("Word length filtering...") + wl_filter = CorpusWordLengthFilter(minlen=3) + docs = wl_filter.transform(docs) + + if 'stem' in preprocessing_steps: + print("Stemming...") + stemmer = CorpusStemmer() + docs = stemmer.transform(docs) + + if 'tag' in preprocessing_steps: + print("POS tagging...") + tagger = CorpusPOSTagger() + tagged_docs = tagger.transform(docs) + + tag_constraints = [] + if label_tags != ['None']: + for tags in label_tags: + tag_constraints.append(tuple(map(lambda t: t.strip(), + tags.split(',')))) + + if len(tag_constraints) == 0: + tag_constraints = None + + print("Tag constraints: {}".format(tag_constraints)) + + print("Generate candidate bigram labels(with POS filtering)...") + finder = BigramLabelFinder('pmi', min_freq=label_min_df, + pos=tag_constraints) + if tag_constraints: + assert 'tag' in preprocessing_steps, \ + 'If tag constraint is applied, pos tagging(tag) should be performed' + cand_labels = finder.find(tagged_docs, top_n=n_cand_labels) + else: # if no constraint, then use untagged docs + cand_labels = finder.find(docs, top_n=n_cand_labels) + + print("Collected {} candidate labels".format(len(cand_labels))) + + print("Calculate the PMI scores...") + + pmi_cal = PMICalculator( + doc2word_vectorizer=WordCountVectorizer( + min_df=5, + stop_words=load_lemur_stopwords()), + doc2label_vectorizer=LabelCountVectorizer()) + + pmi_w2l = pmi_cal.from_texts(docs, cand_labels) + + print("Topic modeling using LDA...") + model = lda.LDA(n_topics=n_topics, n_iter=lda_n_iter, + random_state=lda_random_state) + model.fit(pmi_cal.d2w_) + + print("\nDocument coverage:") + doc_topic = model.doc_topic_ + doc_topics = {} + for i in range(100): + doc_topics[i] = doc_topic[i].argmax() + # print("{} (top topic: {})".format(i, doc_topic[i].argmax())) + + print("\nTopical words:") + print("-" * 20) + for i, topic_dist in enumerate(model.topic_word_): + top_word_ids = np.argsort(topic_dist)[:-n_top_words:-1] + topic_words = [pmi_cal.index2word_[id_] + for id_ in top_word_ids] + # print('Topic {}: {}'.format(i, ' '.join(topic_words))) + + ranker = LabelRanker(apply_intra_topic_coverage=False) + + return doc_topics, ranker.top_k_labels(topic_models=model.topic_word_, + pmi_w2l=pmi_w2l, + index2label=pmi_cal.index2label_, + label_models=None, + k=n_labels) + + # if __name__ == '__main__': + # labels = get_topic_labels(docs = [], + # n_topics=10, + # n_top_words=20, + # preprocessing_steps=['wordlen', 'stem', 'tag'], + # n_cand_labels=100, + # label_min_df=5, + # label_tags=['NN,NN', 'JJ,NN'], + # n_labels=10, + # lda_random_state=12345, + # lda_n_iter=400) + + # print("\nTopical labels:") + # print("-" * 20) + # for i, labels in enumerate(labels): + # print(u"Topic {}: {}\n".format( + # i, + # ', '.join(map(lambda l: ' '.join(l), labels)) + # )) + diff --git a/neuralnetregressor.py b/neuralnetregressor.py new file mode 100644 index 0000000..66449cf --- /dev/null +++ b/neuralnetregressor.py @@ -0,0 +1,71 @@ +from sklearn.linear_model import LinearRegression +from sklearn.neural_network import MLPRegressor +from sklearn import preprocessing as pre +from math import sin +import numpy as np +import csv +from collections import defaultdict + +def relevanceScore(intercept, coefs, scores): + relScore = intercept + for index, score in enumerate(scores): + relScore += (float(score) * coefs[index]) + return relScore + +trainData = list() +f = open("./supervisedTrain.txt", "r") +for line in f: + words = line.split(",") + trainData.append(words) + +trainData = np.array(trainData) +trainRel = np.array(trainData[:,0], dtype='float') +trainFeatures = np.array(trainData[:,1:-1], dtype='float') + +scaler = pre.StandardScaler() +trainFeaturesScaled = scaler.fit_transform(trainFeatures) + +testData = list() +tf = open("./neuralNetFeaturesTest.txt", "r") +for line in tf: + words = line.split(",") + testData.append(words) + +# Train model....Got good results for 3 hidden layers with regression data +mlp = MLPRegressor(hidden_layer_sizes=(3, 3), + activation='tanh', + solver='adam', + learning_rate='invscaling', + max_iter=1000, + learning_rate_init=0.001, + alpha=0.001, + random_state=0, + shuffle=True) +mlp.fit(trainFeaturesScaled, trainRel) + +testData = np.array(testData) +testDataQid = testData[:,0] +testDataDocNo = testData[:,-1] +testDataFeatures = np.array(testData[:,1:-1], dtype='float') +testDataFeaturesScaled = scaler.fit_transform(testDataFeatures) +testDataRel = mlp.predict(testDataFeaturesScaled) + + +testDict = defaultdict(list) +for index, rel in enumerate(testDataRel): + temp = [rel, testDataDocNo[index]] + testDict[testDataQid[index]].append(temp) + +tf = open("./NeuralNetRegressionFeaturesTrainResults.txt", "w") +finalDict = defaultdict(list) +for key, value in testDict.items(): + value.sort(key=lambda x:x[0], reverse=True) + finalDict[key] = value[:100] + +for key, value in finalDict.items(): + for v in value: + tf.write(key + "\t" + v[1][:-1] + "\t" + str(v[0]) + "\n" ) + +# TODO Add evaluation methods to be invoked on test data set results. +from joblib import dump, load +dump(mlp, "plsaNeuralNetModel.model") \ No newline at end of file diff --git a/passenger_wsgi.py b/passenger_wsgi.py new file mode 100644 index 0000000..8056eb0 --- /dev/null +++ b/passenger_wsgi.py @@ -0,0 +1 @@ +# from SearchEngine import app as application \ No newline at end of file diff --git a/plsaNeuralNetModel.model b/plsaNeuralNetModel.model new file mode 100644 index 0000000000000000000000000000000000000000..555fa1568b3b1386dc40ce22e22c76d0a153b8d0 GIT binary patch literal 64297 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 V!DC0{Wfc`<m3$_#&;Cxb{}0lNdg=fG literal 0 HcmV?d00001 diff --git a/pmi.py b/pmi.py new file mode 100644 index 0000000..b7cd658 --- /dev/null +++ b/pmi.py @@ -0,0 +1,111 @@ +import numpy as np +from scipy.sparse import issparse + +import logging +logging.basicConfig(level=logging.DEBUG) + + +class PMICalculator(object): + """ + Parameter: + ----------- + doc2word_vectorizer: object that turns list of text into doc2word matrix + for example, sklearn.feature_extraction.test.CountVectorizer + """ + def __init__(self, doc2word_vectorizer=None, + doc2label_vectorizer=None): + self._d2w_vect = doc2word_vectorizer + self._d2l_vect = doc2label_vectorizer + + self.index2word_ = None + self.index2label_ = None + + def from_matrices(self, d2w, d2l, pseudo_count=1): + """ + Parameter: + ------------ + d2w: numpy.ndarray or scipy.sparse.csr_matrix + document-word frequency matrix + + d2l: numpy.ndarray or scipy.sparse.csr_matrix + document-label frequency matrix + type should be the same with `d2w` + + pseudo_count: float + smoothing parameter to avoid division by zero + + Return: + ------------ + numpy.ndarray: #word x #label + the pmi matrix + """ + denom1 = d2w.T.sum(axis=1) + denom2 = d2l.sum(axis=0) + + # both are dense + if (not issparse(d2w)) and (not issparse(d2l)): + numer = np.matrix(d2w.T > 0) * np.matrix(d2l > 0) + denom1 = denom1[:, None] + denom2 = denom2[None, :] + # both are sparse + elif issparse(d2w) and issparse(d2l): + numer = ((d2w.T > 0) * (d2l > 0)).todense() + else: + raise TypeError('Type inconsistency: {} and {}.\n' + + 'They should be the same.'.format( + type(d2w), type(d2l))) + + # dtype conversion + numer = np.asarray(numer, dtype=np.float64) + denom1 = np.asarray( + denom1.repeat(repeats=d2l.shape[1], axis=1), + dtype=np.float64) + denom2 = np.asarray( + denom2.repeat(repeats=d2w.shape[1], axis=0), + dtype=np.float64) + + # smoothing + numer += pseudo_count + + return np.log(d2w.shape[0] * numer / denom1 / denom2) + + def from_texts(self, docs, labels): + """ + Parameter: + ----------- + docs: list of list of string + the tokenized documents + + labels: list of list of string + + Return: + ----------- + numpy.ndarray: #word x #label + the pmi matrix + """ + d2w = self._d2w_vect.fit_transform(map(lambda sent: ' '.join(sent), + docs)) + + # save it to avoid re-computation + self.d2w_ = d2w + + d2l = self._d2l_vect.transform(docs, labels) + + # remove the labels without any occurrences + indices = np.asarray(d2l.sum(axis=0).nonzero()[1]).flatten() + d2l = d2l[:, indices] + + indices = set(indices) + labels = [l + for i, l in self._d2l_vect.index2label_.items() + if i in indices] + + self.index2label_ = {i: l + for i, l in enumerate(labels)} + + if len(self.index2label_) == 0: + logging.warn("After label filtering, there is nothing left.") + + self.index2word_ = {i: w + for w, i in self._d2w_vect.vocabulary_.items()} + return self.from_matrices(d2w, d2l) diff --git a/text.py b/text.py new file mode 100644 index 0000000..a5a277f --- /dev/null +++ b/text.py @@ -0,0 +1,74 @@ +from scipy.sparse import (csr_matrix, lil_matrix) +from scipy import int64 + + +class LabelCountVectorizer(object): + """ + Count the frequency of labels in each document + """ + + def __init__(self): + self.index2label_ = None + + def _label_frequency(self, label_tokens, context_tokens): + """ + Calculate the frequency that the label appears + in the context(e.g, sentence) + + Parameter: + --------------- + + label_tokens: list|tuple of str + the label tokens + context_tokens: list|tuple of str + the sentence tokens + + Return: + ----------- + int: the label frequency in the sentence + """ + label_len = len(label_tokens) + cnt = 0 + for i in range(len(context_tokens) - label_len + 1): + match = True + for j in range(label_len): + if label_tokens[j] != context_tokens[i+j]: + match = False + break + if match: + cnt += 1 + return cnt + + def transform(self, docs, labels): + """ + Calculate the doc2label frequency table + + Note: docs are not tokenized and frequency is computed + based on substring matching + + Parameter: + ------------ + + docs: list of list of string + tokenized documents + + labels: list of list of string + + Return: + ----------- + scipy.sparse.csr_matrix: #doc x #label + the frequency table + """ + labels = sorted(labels) + self.index2label_ = {index: label + for index, label in enumerate(labels)} + + ret = lil_matrix((len(docs), len(labels)), + dtype=int64) + for i, d in enumerate(docs): + for j, l in enumerate(labels): + cnt = self._label_frequency(l, d) + if cnt > 0: + ret[i, j] = cnt + return ret.tocsr() + diff --git a/train_academicdata.py b/train_academicdata.py new file mode 100644 index 0000000..a50ae67 --- /dev/null +++ b/train_academicdata.py @@ -0,0 +1,284 @@ +import json +import xml.etree.ElementTree as ET +from whoosh.index import create_in, open_dir +from whoosh.fields import * +from whoosh.qparser import MultifieldParser, OrGroup, QueryParser +from whoosh.scoring import BaseScorer +from whoosh import scoring +from collections import defaultdict + +class Myclass: + def __init__(self): + self.rel = 0 + self.kpbm25 = 0.0 + self.pabm25 = 0.0 + self.tibm25 = 0.0 + self.kptf = 0.0 + self.patf = 0.0 + self.titf = 0.0 + self.kppl2 = 0.0 + self.papl2 = 0.0 + self.tipl2 = 0.0 + self.kpdf = 0.0 + self.padf = 0.0 + self.tidf = 0.0 + self.references = 0 + self.citatedBy = 0 + self.citations = 0 + + +count = 0 + +rf = open("./supervisedTrain.txt", "w") +f = open("./train_queries.json", encoding = "utf-8") +relevance = open("./train_queries_qrel", encoding = "utf-8") +train_queries = defaultdict(dict) +for line in relevance: + words = line.split() + train_queries[words[0]][words[1]] = words[2] + + +fileWrite = defaultdict(dict) +queries = defaultdict(str) +for line in f: + document = json.loads(line) + query = document["query"] + query = query.replace("#combine( ", '') + query = query.replace(')', '') + queries[document["qid"]] = query + indexDir = open_dir("index/") + og = OrGroup.factory(0.9) + #QueryParser("content", schema) + #queryParser = MultifieldParser(["keyPhrases", "paperAbstract", "title"], indexSearcher.schema, group=og) + indexSearcher = indexDir.searcher() + kpqueryParser = QueryParser("keyPhrases", indexSearcher.schema, group=og) + kpqueryObject = kpqueryParser.parse(query) + paqueryParser = QueryParser("paperAbstract", indexSearcher.schema, group=og) + paqueryObject = paqueryParser.parse(query) + tiqueryParser = QueryParser("title", indexSearcher.schema, group=og) + tiqueryObject = tiqueryParser.parse(query) + + + kpresults = indexSearcher.search(kpqueryObject, limit = None) + paresults = indexSearcher.search(paqueryObject, limit = None) + tiresults = indexSearcher.search(tiqueryObject, limit = None) + for result in kpresults: + docNo = "".join(result["docno"]) + rel = train_queries[str(count)].get(docNo, -1) + + if rel != -1: + if (docNo in fileWrite[str(count)]): + fileWrite[str(count)][docNo].kpbm25 = result.score + else: + temp = Myclass() + temp.rel = rel + temp.kpbm25 = result.score + fileWrite[str(count)][docNo] = temp + + for result in paresults: + docNo = "".join(result["docno"]) + rel = train_queries[str(count)].get(docNo, -1) + + if rel != -1: + if (docNo in fileWrite[str(count)]): + fileWrite[str(count)][docNo].pabm25 = result.score + else: + temp = Myclass() + temp.rel = rel + temp.pabm25 = result.score + fileWrite[str(count)][docNo] = temp + + for result in tiresults: + docNo = "".join(result["docno"]) + rel = train_queries[str(count)].get(docNo, -1) + + if rel != -1: + if (docNo in fileWrite[str(count)]): + fileWrite[str(count)][docNo].tibm25 = result.score + else: + temp = Myclass() + temp.rel = rel + temp.tibm25 = result.score + fileWrite[str(count)][docNo] = temp + + + w = scoring.TF_IDF() + idfIndexSearcher = indexDir.searcher(weighting=w) + #idfResults = idfIndexSearcher.search(queryObject, limit = 8541) + kpidfResults = idfIndexSearcher.search(kpqueryObject, limit = None) + paidfResults = idfIndexSearcher.search(paqueryObject, limit = None) + tiidfResults = idfIndexSearcher.search(tiqueryObject, limit = None) + for result in kpidfResults: + docNo = "".join(result["docno"]) + rel = train_queries[str(count)].get(docNo, -1) + + if rel != -1: + if (docNo in fileWrite[str(count)]): + fileWrite[str(count)][docNo].kptf = result.score + else: + temp = Myclass() + temp.rel = rel + temp.kptf = result.score + fileWrite[str(count)][docNo] = temp + + for result in paidfResults: + docNo = "".join(result["docno"]) + rel = train_queries[str(count)].get(docNo, -1) + + if rel != -1: + if (docNo in fileWrite[str(count)]): + fileWrite[str(count)][docNo].patf = result.score + else: + temp = Myclass() + temp.rel = rel + temp.patf = result.score + fileWrite[str(count)][docNo] = temp + + for result in tiidfResults: + docNo = "".join(result["docno"]) + rel = train_queries[str(count)].get(docNo, -1) + + if rel != -1: + if (docNo in fileWrite[str(count)]): + fileWrite[str(count)][docNo].titf = result.score + else: + temp = Myclass() + temp.rel = rel + temp.titf = result.score + fileWrite[str(count)][docNo] = temp + + w = scoring.PL2() + plIndexSearcher = indexDir.searcher(weighting=w) + #plResults = plIndexSearcher.search(queryObject, limit = 8541) + kpplResults = plIndexSearcher.search(kpqueryObject, limit = None) + paplResults = plIndexSearcher.search(paqueryObject, limit = None) + tiplResults = plIndexSearcher.search(tiqueryObject, limit = None) + + for result in kpplResults: + docNo = "".join(result["docno"]) + rel = train_queries[str(count)].get(docNo, -1) + + if rel != -1: + if (docNo in fileWrite[str(count)]): + fileWrite[str(count)][docNo].kppl2 = result.score + else: + temp = Myclass() + temp.rel = rel + temp.kppl2 = result.score + fileWrite[str(count)][docNo] = temp + + for result in paplResults: + docNo = "".join(result["docno"]) + rel = train_queries[str(count)].get(docNo, -1) + + if rel != -1: + if (docNo in fileWrite[str(count)]): + fileWrite[str(count)][docNo].papl2 = result.score + else: + temp = Myclass() + temp.rel = rel + temp.papl2 = result.score + fileWrite[str(count)][docNo] = temp + + for result in tiplResults: + docNo = "".join(result["docno"]) + rel = train_queries[str(count)].get(docNo, -1) + + if rel != -1: + if (docNo in fileWrite[str(count)]): + fileWrite[str(count)][docNo].tipl2 = result.score + else: + temp = Myclass() + temp.rel = rel + temp.tipl2 = result.score + fileWrite[str(count)][docNo] = temp + + w = scoring.DFree() + dfIndexSearcher = indexDir.searcher(weighting=w) + #plResults = plIndexSearcher.search(queryObject, limit = 8541) + kpdfResults = dfIndexSearcher.search(kpqueryObject, limit = None) + padfResults = dfIndexSearcher.search(paqueryObject, limit = None) + tidfResults = dfIndexSearcher.search(tiqueryObject, limit = None) + + for result in kpdfResults: + docNo = "".join(result["docno"]) + rel = train_queries[str(count)].get(docNo, -1) + + if rel != -1: + if (docNo in fileWrite[str(count)]): + fileWrite[str(count)][docNo].kpdf = result.score + else: + temp = Myclass() + temp.rel = rel + temp.kpdf = result.score + fileWrite[str(count)][docNo] = temp + + for result in padfResults: + docNo = "".join(result["docno"]) + rel = train_queries[str(count)].get(docNo, -1) + + if rel != -1: + if (docNo in fileWrite[str(count)]): + fileWrite[str(count)][docNo].padf = result.score + else: + temp = Myclass() + temp.rel = rel + temp.padf = result.score + fileWrite[str(count)][docNo] = temp + + for result in tidfResults: + docNo = "".join(result["docno"]) + rel = train_queries[str(count)].get(docNo, -1) + + if rel != -1: + if (docNo in fileWrite[str(count)]): + fileWrite[str(count)][docNo].tidf = result.score + else: + temp = Myclass() + temp.rel = rel + temp.tidf = result.score + fileWrite[str(count)][docNo] = temp + + count += 1 + +def getJsonValue(jsonValue, key): + try: + if key != "docno": + value = " ".join(jsonValue[key]) + else: + value = jsonValue[key] + return value + except: + return " " + +docs = open("../data/Academic_data/docs.json", encoding="utf-8") +docReferences = defaultdict() +for line in docs: + document = json.loads(line) + kp = getJsonValue(document, "keyPhrases") + pa = getJsonValue(document, "paperAbstract") + ti = getJsonValue(document, "title") + docNo = document["docno"] + refs = document["numKeyReferences"][0] + citatedBy = document["numCitedBy"][0] + citations = document["numKeyCitations"][0] + docReferences[docNo] = [refs, citatedBy, citations, kp, pa, ti] + +for key, value in fileWrite.items(): + for k, v in value.items(): + kp = set(docReferences[k][3].split()) + pa = set(docReferences[k][4].split()) + ti = set(docReferences[k][5].split()) + q = set(queries[key].split()) + #rf.write(str(v[0]) + "\tqid:" + key + "\t1:" + str(v[1]) + "\t2:" + str(v[2]) + "\t3:" + str(v[3]) + "\t#docid = " + str(k) + "\n") + rf.write(str(v.rel) + "," + str(v.kpbm25) + "," + str(v.pabm25) + "," + str(v.tibm25) + "," + \ + str(v.kptf) + "," + str(v.patf) + "," + str(v.titf) + "," + \ + str(v.kppl2) + "," + str(v.papl2) + "," + str(v.tipl2) + "," + \ + str(v.kpdf) + "," + str(v.padf) + "," + str(v.tidf) + "," + \ + str(docReferences[k][0]) + "," + str(docReferences[k][1]) + "," + str(docReferences[k][2]) + "," + \ + str(len(kp)) + "," + str(len(pa)) + "," + str(len(ti)) + "," + \ + str(len(q)) + "," + str(len(q.intersection(kp))) + "," + str(len(q.intersection(pa))) + "," + str(len(q.intersection(ti))) + "," + \ + str(k) + "\n") +f.close() +print(count) + -- GitLab