nlp_projects

A collection of NLP-related code

https://github.com/pavchristian/nlp_projects

Science Score: 31.0%

This score indicates how likely this project is to be science-related based on various indicators:

  • CITATION.cff file
    Found CITATION.cff file
  • codemeta.json file
    Found codemeta.json file
  • .zenodo.json file
  • DOI references
  • Academic publication links
  • Academic email domains
  • Institutional organization owner
  • JOSS paper metadata
  • Scientific vocabulary similarity
    Low similarity (5.2%) to scientific vocabulary
Last synced: 10 months ago · JSON representation ·

Repository

A collection of NLP-related code

Basic Info
  • Host: GitHub
  • Owner: PavChristian
  • Language: Python
  • Default Branch: main
  • Size: 35.2 KB
Statistics
  • Stars: 0
  • Watchers: 2
  • Forks: 0
  • Open Issues: 0
  • Releases: 0
Created almost 2 years ago · Last pushed over 1 year ago
Metadata Files
Readme Citation

README.md

NLP_projects

  • Citation analysis This folder contains several modified code snippets that utilize scraping + API to obtain large amounts of textual data and feed them into a training pipeline.

    • GNNs Code for preprocessing and training graph neural network on edge and node features
    • embeddings Code for creating text embeddings of large academic articles
    • citations Code for generating appropriate node and edge features in a graph
    • organizer Code for obtaining the data using scraping and API and applying the appropriate format guidelines
  • Car reviews This notebook contains code that leverages HF models to translate and summarize car reviews. The dataset is substantially smaller than the original version due to privacy concerns.

Owner

  • Name: Pavel-Christian
  • Login: PavChristian
  • Kind: user
  • Location: Europe

Citation (citation_analysis/GNNs.py)

'''
torch must be 2.2.0 or lower to be compatible with dgl
update scipy
'''
import dgl, torch, random
import json, logging, sys, os, itertools, copy
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
import scipy.sparse as sp
from sklearn.metrics import roc_auc_score
import dgl.function as fn
import pandas as pd
from rclone_python import rclone
from dgl.nn import SAGEConv



class nn_files:
    def __init__(self, corpus_path, id_path, sampler_path,
                 log_file):
        #Path to wos files
        self.corpus_path = corpus_path
        #Path to a folder where ids should be saved
        self.id_path = id_path 
        #Path to sampler files 
        self.sampler_path = sampler_path 
        #Import logs 
        self.log_file = log_file
        

def obtain_ids_from_sample(self, trial_name):
    '''
    This function should be used on a subsample of data
    self (obj) -- class value 
    trial_name (str) -- name of the trial from which an article dictionary is pulled
    NOTE: Only apply to samples within the same year
    Returns 
    A dictionary with new unique ids
    DGL graph network objects only allow using numbers as ids (as opposed to strings)
    '''
    #Creating new ids
    pair_df = pd.DataFrame(columns = ['id1', 'id2', "year"])
    cor_dict = {}
    reformed_dict = {}
    #Read the author dictionary
    with open(f"{self.sampler_path}/temp/{trial_name}/temp_authors/auths_from_{year}.json", "r") as file:
        data = json.load(file)
    id_list = [r_id.split("_")[0] for r_id in list(data.keys())]
    id_set = list(set(id_list))
    for i, r_id in enumerate(id_set):
        cor_dict[r_id] = i
    for key in data.keys():
        fake_key = key.split("_")[0]
        year = key.split("_")[1]
        for co_au in data[key]['year_co_auths']:
                if co_au != "":
                    new_row = pd.DataFrame([{"id1": cor_dict[fake_key], "id2" : cor_dict[co_au],
                                             "year": year}])
                    pair_df = pd.concat([pair_df, new_row], ignore_index = True)
    #Rejoin new id numbers
    #Effectively reorganizes the old dictionary by the new id
    for key in data.keys():
        fake_key = key.split("_")[0]
        reformed_dict[cor_dict[fake_key]] = copy.deepcopy(data[key])
        reformed_dict[cor_dict[fake_key]]['r_id'] = copy.deepcopy(fake_key)
    return pair_df, reformed_dict, data

def get_new_ids(key, d):
    '''
    key (str) -- original r_id
    d (obj) -- reference dictionary 
    Converts old r_ids into numeric ids 
    '''
    return d[key]
    
def obtain_raw_wos_ids(self, year):
    '''
    Collects r_ids directly from the Web of Science corpus (rather than sub-samples)
    Useful for creating network objects from the original dataset
    January version
    Takes a long time to run
    '''
    pair_df = pd.DataFrame(columns = ['id1', 'id2', "year"])
    cor_dict = {}
    folder = f"{self.corpus_path}/{year}/authors"
    files = [os.path.join(folder, file['Path']) for file in rclone.ls(folder)]
    for file in files:
        print(f"Working on {file}", flush = True)
        with open(file, "r") as f:
            data = json.load(f)
        for key in data.keys():
            if key not in cor_dict:
                cor_dict[key] = 1
            co_authors = copy.deepcopy(data[key]['collabs'])
            for co_au in co_authors:
                if co_au != "":
                    new_row = pd.DataFrame([{"id1": key, "id2" : co_au,
                                            "year": year}])
                    pair_df = pd.concat([pair_df, new_row], ignore_index = True)
                if pair_df.shape[0] % 10000 == 0:
                    print(pair_df.shape[0], flush = True)
                
    print("Updating dataframe", flush = True)
    pair_df['id1'] = pair_df['id1'].apply(lambda x: get_new_ids(x, cor_dict))
    pair_df['id2'] = pair_df['id2'].apply(lambda x: get_new_ids(x, cor_dict))
    return pair_df, cor_dict
    
                        

def create_dgl_graph(df, src_col, dst_col, weight_col=None):
    '''
    df (obj) -- dataframe from the previous function (dyadic pairs)
    src_obj (str) -- name of the origin column 
    dst_col (str) -- name of the destination column
    weight_col (str) -- name of the weight column (if present)
    
    '''
    df[src_col] = df[src_col].astype(np.int64)
    df[dst_col] = df[dst_col].astype(np.int64)
    src = df[src_col].values
    dst = df[dst_col].values
    graph = dgl.graph((src, dst))
    
    if weight_col:
        weights = torch.tensor(df[weight_col].values, dtype=torch.float32)
        graph.edata['weight'] = weights
    
    return graph

def check_graph_sanity(graph):
    '''
    graph (str) -- a dgl graph object 
    Checks various attributes of the graph 
    '''    
    print(graph.num_edges())
    print(graph.num_nodes())
    #Out Degrees of the central node
    print(graph.out_degrees(0))
    #in Degrees of the central node 
    print(graph.in_degrees(0))
    #The latter two should be the same since is the graph is not directed
    
    

def sum_values(d, n, k):
    '''
    Sums values less than n (used in the next function)
    d (obj) -- dictionary
    n (int) -- number
    k (int) -- step back
    '''
    return sum(value for key, value in d.items() if int(key) < n and int(key) >= n - k)

def replace_nan(value):
    """
    Replaces NaN with zero.
    
    Parameters:
    value: The input value which may be a float or any type.
    
    Returns:
    The original value if it is not NaN, or zero if it is NaN.
    """
    if value != value:  # NaN is not equal to itself
        return 0
    return value

def create_node_features(auth_meta, year, rand_gender = True):
    '''
    Turns author metadata into node features (arranged in tensors)
    auth_meta (str) -- a dictionary with author metadata
    year (int) -- target year of the sample
    rand_gender (bool) -- should a random gender dummy be assigned
    Only use this on smaller samples (writing to disk is advised for larger datasets)
    '''
    features_dict = {}
    #Iterate over the dictionary
    for key, value in auth_meta.items():
        if key.split("_")[1] != str(year):
            continue
        #Turn citations into a tensor  
        citations = value.get('yearly_cit')
        sorted_values = [citations[key] for key in sorted(citations.keys())]
        #Convert the list of sorted values to a tensor
        feat_tensor = torch.tensor(sorted_values)
        #Previous 3 years (non_inclusive)
        t_minus_3 = torch.tensor([replace_nan(sum_values(citations, year, 3))])
        feat_tensor = torch.cat((feat_tensor, t_minus_3))
        #Previous 5 years
        t_minus_5 = torch.tensor([replace_nan(sum_values(citations, year, 5))])
        feat_tensor = torch.cat((feat_tensor, t_minus_5))
        #Total citations (over all time)
        total_citations = torch.tensor([sum(value for key, value in citations.items())])
        feat_tensor = torch.cat((feat_tensor, total_citations))
        #Add a random gender dummy
        if rand_gender: 
            gender = copy.deepcopy(random.random())
            auth_meta[key]['gender'] = gender
            feat_tensor = torch.cat((feat_tensor, torch.tensor([gender])))
        #Academic age
        ac_age = int(auth_meta[key]['year']) - int(auth_meta[key]['fy'])
        feat_tensor = torch.cat((feat_tensor, ac_age))
        features_dict[key] = feat_tensor
        
    return auth_meta, features_dict
    
def check_addresses_old(ad1, ad2):
    '''
    Checks whether two authors have the same zipcode, insitution
    ad1 (dict) -- addresses of the first author in a dyad 
    ad2 (dict) -- addresses of the second author in a dyad
    NOTE: This works only on the uncleaned address dictionary (addresses field in auth_dict)
    '''
    z_dummy = 0
    i_dummy = 0
    for a1 in ad1:
        zip1 = a1.get('zip')
        inst1 = a1.get('organization').split(";")[1]
        for a2 in ad2:
            zip2 = a2.get('zip')
            inst2 = a2.get('organization').split(";")[1]
            if zip1 == zip2 and zip1 != "" and z_dummy == 0:
                z_dummy = 1
            else:
                pass
            if inst1 == inst2 and inst1 != "" and i_dummy == 0:
                i_dummy = 1
            else:
                pass
    return [z_dummy, i_dummy]

    
def create_edge_features(auth_meta, pair_df, feature_data, author_similarity, write_path):
    '''
    Using author metadata, create edge features for training
    auth_meta (dict) -- output from create_node_features
    pair_df (df) -- output from obtain ids from sample
    feature_data (dict) -- dictionary with the necessary features (it should come from the co-citation file)
    Immediately writing to disk to avoid memory problems
    '''
    i = 0
    with open(write_path) as json_file:
        for index, row in pair_df.iterrows():
            id1, id2, year = row['id1'], row['id2'], row['year']
            ad_values = check_addresses_old(auth_meta[id1],
                                            auth_meta[id2])
            #Dummies for similar addresses
            feat_tensor = torch.tensor(ad_values)
            #finding unique id
            unique_id = id1 + "+" + id2 + "_" + year
            selected_keys = ['com_cit', "co-cit", "fr_cit"]
            mini_dict = [feature_data[key] for key in feature_data[unique_id] if key in selected_keys]
            feat_tensor = torch.cat(feat_tensor, torch.tensor(mini_dict))
            feat_tensor = torch.cat(feat_tensor, torch.tensor([author_similarity[unique_id]]))
            #Appending the dictionary to a list
            json_line = json.dumps({id1: {"pair": id2, "edge_features": feat_tensor}})
            json_file.write(json_line + "/n")  
            i +=1 
            if i % 10000 == 0:
                print(f"{i} iterations have been completed")
    
def assign_random_features(graph, num_node_feat, num_edge_feat):
    '''
    NOTE: This function is only for testing and small samples
    Assigns random features to the graph (should later be replaced with actual features)
    graph (obj) -- a dgl graph object
    num_node_feat (int) -- number of node features
    num_edge_feat (int) -- number of edge features
    Matrices can also be assigned 
    '''
    n_nodes = graph.num_nodes()
    n_edges = graph.num_edges()
    graph.ndata['feat'] = torch.randn(n_nodes, num_node_feat)
    graph.edata['feat'] = torch.randn(n_edges, num_edge_feat)
    return graph

def pre_proc_and_split(g, test_prop = 0.1):
    '''
    NOTE: This function is only for testing and small samples. Do not use on large samples/graphs
    Create test and train splits for GNN training
    g (obj) -- a dgl graph object 
    test_prop (float) -- proportion of observations in the test sample
    '''
    u, v = g.edges()
    eids = np.arange(g.number_of_edges())
    eids = np.random.permutation(eids)
    test_size = int(len(eids) * test_prop)
    train_size = g.number_of_edges() - test_size
    test_pos_u, test_pos_v = u[eids[:test_size]], v[eids[:test_size]]
    train_pos_u, train_pos_v = u[eids[test_size:]], v[eids[test_size:]]
    
    # Find all negative edges and split them for training and testing
    adj = sp.coo_matrix((np.ones(len(u)), (u.numpy(), v.numpy())))
    adj_neg = 1 - adj.todense() - np.eye(g.number_of_nodes())
    neg_u, neg_v = np.where(adj_neg != 0)

    neg_eids = np.random.choice(len(neg_u), g.number_of_edges())
    test_neg_u, test_neg_v = neg_u[neg_eids[:test_size]], neg_v[neg_eids[:test_size]]
    train_neg_u, train_neg_v = neg_u[neg_eids[test_size:]], neg_v[neg_eids[test_size:]]
    #Remove test set egdes
    train_g = dgl.remove_edges(g, eids[:test_size])
    #Create positive and negative edges for training 
    #Positive edges -- edges where a link between two nodes exists 
    #Negative edges -- two nodes without an edge
    train_pos_g = dgl.graph((train_pos_u, train_pos_v), num_nodes=g.number_of_nodes())
    train_neg_g = dgl.graph((train_neg_u, train_neg_v), num_nodes=g.number_of_nodes())

    test_pos_g = dgl.graph((test_pos_u, test_pos_v), num_nodes=g.number_of_nodes())
    test_neg_g = dgl.graph((test_neg_u, test_neg_v), num_nodes=g.number_of_nodes())

    return train_g, train_pos_g, train_neg_g, test_pos_g, test_neg_g

# ----------- 2. create model -------------- #
# build a two-layer GraphSAGE model
class GraphSAGE(nn.Module):
    def __init__(self, in_feats, h_feats):
        super(GraphSAGE, self).__init__()
        self.conv1 = SAGEConv(in_feats, h_feats, 'mean')
        self.conv2 = SAGEConv(h_feats, h_feats, 'mean')

    def forward(self, g, in_feat):
        h = self.conv1(g, in_feat)
        h = F.relu(h)
        h = self.conv2(g, h)
        return h
    
class DotPredictor(nn.Module):
    def forward(self, g, h):
        with g.local_scope():
            g.ndata['h'] = h
            # Compute a new edge feature named 'score' by a dot-product between the
            # source node feature 'h' and destination node feature 'h'.
            g.apply_edges(fn.u_dot_v('h', 'h', 'score'))
            # u_dot_v returns a 1-element vector for each edge so you need to squeeze it.
            return g.edata['score'][:, 0]
        
        
def compute_loss(pos_score, neg_score):
    scores = torch.cat([pos_score, neg_score])
    labels = torch.cat([torch.ones(pos_score.shape[0]), torch.zeros(neg_score.shape[0])])
    return F.binary_cross_entropy_with_logits(scores, labels)


def compute_auc(pos_score, neg_score):
    scores = torch.cat([pos_score, neg_score]).numpy()
    labels = torch.cat(
        [torch.ones(pos_score.shape[0]), torch.zeros(neg_score.shape[0])]).numpy()
    return roc_auc_score(labels, scores)


def launch_training_loop(model, pred, optimizer, train_g, train_pos_g, train_neg_g,
                         test_pos_g, test_neg_g):
    all_logits = []
    for e in range(100):
        # forward
        h = model(train_g, train_g.ndata['feat'])
        pos_score = pred(train_pos_g, h)
        neg_score = pred(train_neg_g, h)
        loss = compute_loss(pos_score, neg_score)

        # backward
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        if e % 5 == 0:
            print('In epoch {}, loss: {}'.format(e, loss))
    #Check results
    from sklearn.metrics import roc_auc_score
    with torch.no_grad():
        pos_score = pred(test_pos_g, h)
        neg_score = pred(test_neg_g, h)
        print('AUC', compute_auc(pos_score, neg_score))


def main():
    pavel_network = nn_files(corpus_path= "modal_code/wos/file_system/jsons",
                             id_path = None,
                             sampler_path = "modal_code/sampler",
                             log_file = "modal_code/graph_nets/logs/log_out.log")
    logging.basicConfig(filename=pavel_network.log_file, level=logging.INFO)
    sys.stdout = sys.stderr = open(pavel_network.log_file, 'a')
    with open(pavel_network.log_file, 'w'):
        pass       
    pair_df, ref_dict, auth_dict = obtain_ids_from_sample(pavel_network, "trial2")
    g = create_dgl_graph(pair_df, "id1", "id2")
    check_graph_sanity(g)
    auth_dict, features_dict = create_node_features(auth_dict, 2010)
    print(features_dict)
    g = assign_random_features(g, 3, 3)
    train_g, train_pos_g, train_neg_g, test_pos_g, test_neg_g = pre_proc_and_split(g)
    #Create the model
    model = GraphSAGE(train_g.ndata['feat'].shape[1], 16)
    #Define calculation method 
    pred = DotPredictor()
    #Define the optimizer
    optimizer = torch.optim.Adam(itertools.chain(model.parameters(), pred.parameters()), lr=0.01)
    #Run the training process
    launch_training_loop(model, pred, optimizer, train_g, train_pos_g, train_neg_g,
                         test_pos_g, test_neg_g)
    
    
    
    
if __name__ == "__main__":
    main()
    print("Done")
    
    
        
        

GitHub Events

Total
  • Push event: 1
Last Year
  • Push event: 1