|
| 1 | +# Licensed to the Apache Software Foundation (ASF) under one |
| 2 | +# or more contributor license agreements. See the NOTICE file |
| 3 | +# distributed with this work for additional information |
| 4 | +# regarding copyright ownership. The ASF licenses this file |
| 5 | +# to you under the Apache License, Version 2.0 (the |
| 6 | +# "License"); you may not use this file except in compliance |
| 7 | +# with the License. You may obtain a copy of the License at |
| 8 | +# |
| 9 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 10 | +# |
| 11 | +# Unless required by applicable law or agreed to in writing, software |
| 12 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 13 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 14 | +# See the License for the specific language governing permissions and |
| 15 | +# limitations under the License. |
| 16 | +# ============================================================================= |
| 17 | + |
| 18 | +import re |
| 19 | +import os |
| 20 | +import pickle |
| 21 | +import urllib |
| 22 | +import tarfile |
| 23 | +import numpy as np |
| 24 | +import pandas as pd |
| 25 | +import nltk |
| 26 | +from nltk.stem import PorterStemmer |
| 27 | +from nltk.tokenize.toktok import ToktokTokenizer |
| 28 | +from gensim.models.keyedvectors import KeyedVectors |
| 29 | +from sklearn.model_selection import train_test_split |
| 30 | +from bs4 import BeautifulSoup |
| 31 | +''' |
| 32 | + data collection preprocessing constants |
| 33 | +''' |
| 34 | +download_dir = '/tmp/' |
| 35 | +preprocessed_imdb_data_fp = download_dir + 'imdb_processed.pickle' |
| 36 | +imdb_dataset_link = "https://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz" |
| 37 | +google_news_pretrain_embeddings_link = "https://s3.amazonaws.com/dl4j-distribution/GoogleNews-vectors-negative300.bin.gz" |
| 38 | + |
| 39 | + |
| 40 | +def pad_batch(b, seq_limit): |
| 41 | + ''' convert a batch of encoded sequence |
| 42 | + to pretrained word vectors from the embed weights (lookup dictionary) |
| 43 | + ''' |
| 44 | + batch_seq = [] |
| 45 | + batch_senti_onehot = [] |
| 46 | + batch_senti = [] |
| 47 | + for r in b: |
| 48 | + # r[0] encoded sequence |
| 49 | + # r[1] label 1 or 0 |
| 50 | + encoded = None |
| 51 | + if len(r[0]) >= seq_limit: |
| 52 | + encoded = r[0][:seq_limit] |
| 53 | + else: |
| 54 | + encoded = r[0] + [0] * (seq_limit - len(r[0])) |
| 55 | + |
| 56 | + batch_seq.append(encoded) |
| 57 | + batch_senti.append(r[1]) |
| 58 | + if r[1] == 1: |
| 59 | + batch_senti_onehot.append([0, 1]) |
| 60 | + else: |
| 61 | + batch_senti_onehot.append([1, 0]) |
| 62 | + batch_senti = np.array(batch_senti).astype(np.float32) |
| 63 | + batch_senti_onehot = np.array(batch_senti_onehot).astype(np.float32) |
| 64 | + batch_seq = np.array(batch_seq).astype(np.int32) |
| 65 | + return batch_seq, batch_senti_onehot, batch_senti |
| 66 | + |
| 67 | + |
| 68 | +def pad_batch_2vec(b, seq_limit, embed_weights): |
| 69 | + ''' convert a batch of encoded sequence |
| 70 | + to pretrained word vectors from the embed weights (lookup dictionary) |
| 71 | + ''' |
| 72 | + batch_seq = [] |
| 73 | + batch_senti_onehot = [] |
| 74 | + batch_senti = [] |
| 75 | + for r in b: |
| 76 | + # r[0] encoded sequence |
| 77 | + # r[1] label 1 or 0 |
| 78 | + encoded = None |
| 79 | + if len(r[0]) >= seq_limit: |
| 80 | + encoded = r[0][:seq_limit] |
| 81 | + else: |
| 82 | + encoded = r[0] + [0] * (seq_limit - len(r[0])) |
| 83 | + |
| 84 | + batch_seq.append([embed_weights[idx] for idx in encoded]) |
| 85 | + batch_senti.append(r[1]) |
| 86 | + if r[1] == 1: |
| 87 | + batch_senti_onehot.append([0, 1]) |
| 88 | + else: |
| 89 | + batch_senti_onehot.append([1, 0]) |
| 90 | + batch_senti = np.array(batch_senti).astype(np.float32) |
| 91 | + batch_senti_onehot = np.array(batch_senti_onehot).astype(np.float32) |
| 92 | + batch_seq = np.array(batch_seq).astype(np.float32) |
| 93 | + return batch_seq, batch_senti_onehot, batch_senti |
| 94 | + |
| 95 | + |
| 96 | +def check_exist_or_download(url): |
| 97 | + ''' download data into tmp ''' |
| 98 | + name = url.rsplit('/', 1)[-1] |
| 99 | + filename = os.path.join(download_dir, name) |
| 100 | + if not os.path.isfile(filename): |
| 101 | + print("Downloading %s" % url) |
| 102 | + urllib.request.urlretrieve(url, filename) |
| 103 | + return filename |
| 104 | + |
| 105 | + |
| 106 | +def unzip_data(download_dir, data_gz): |
| 107 | + data_dir = download_dir + 'aclImdb' |
| 108 | + if not os.path.exists(data_dir): |
| 109 | + print("extracting %s to %s" % (download_dir, data_dir)) |
| 110 | + with tarfile.open(data_gz) as tar: |
| 111 | + tar.extractall(download_dir) |
| 112 | + return data_dir |
| 113 | + |
| 114 | + |
| 115 | +def strip_html(text): |
| 116 | + ''' lambda fn for cleaning html ''' |
| 117 | + soup = BeautifulSoup(text, "html.parser") |
| 118 | + return soup.get_text() |
| 119 | + |
| 120 | + |
| 121 | +def remove_between_square_brackets(text): |
| 122 | + ''' lambda fn for cleaning square brackets''' |
| 123 | + return re.sub('\[[^]]*\]', '', text) |
| 124 | + |
| 125 | + |
| 126 | +def remove_special_characters(text, remove_digits=True): |
| 127 | + ''' lambda fn for removing special char ''' |
| 128 | + pattern = r'[^a-zA-Z0-9\s]' |
| 129 | + text = re.sub(pattern, '', text) |
| 130 | + return text |
| 131 | + |
| 132 | + |
| 133 | +def simple_stemmer(text): |
| 134 | + ''' lambda fn for stemming ''' |
| 135 | + ps = PorterStemmer() |
| 136 | + text = ' '.join([ps.stem(word) for word in text.split()]) |
| 137 | + return text |
| 138 | + |
| 139 | + |
| 140 | +def remove_stopwords(text, tokenizer, stopword_list, is_lower_case=False): |
| 141 | + ''' lambda fn for removing stopwrods ''' |
| 142 | + tokens = tokenizer.tokenize(text) |
| 143 | + tokens = [token.strip() for token in tokens] |
| 144 | + if is_lower_case: |
| 145 | + filtered_tokens = [ |
| 146 | + token for token in tokens if token not in stopword_list |
| 147 | + ] |
| 148 | + else: |
| 149 | + filtered_tokens = [ |
| 150 | + token for token in tokens if token.lower() not in stopword_list |
| 151 | + ] |
| 152 | + filtered_text = ' '.join(filtered_tokens) |
| 153 | + return filtered_text |
| 154 | + |
| 155 | + |
| 156 | +def tokenize(x): |
| 157 | + ''' lambda fn for tokenize sentences ''' |
| 158 | + ret = [] |
| 159 | + for w in x.split(" "): |
| 160 | + if w != '': |
| 161 | + ret.append(w) |
| 162 | + return ret |
| 163 | + |
| 164 | + |
| 165 | +def encode_token(words, wv, w2i): |
| 166 | + ''' lambda fn for encoding string seq to int seq |
| 167 | + args: |
| 168 | + wv: word vector lookup dictionary |
| 169 | + w2i: word2index lookup dictionary |
| 170 | + ''' |
| 171 | + ret = [] |
| 172 | + for w in words: |
| 173 | + if w in wv: |
| 174 | + ret.append(w2i[w]) |
| 175 | + return ret |
| 176 | + |
| 177 | + |
| 178 | +def preprocess(): |
| 179 | + ''' collect and preprocess raw data from acl Imdb dataset |
| 180 | + ''' |
| 181 | + nltk.download('stopwords') |
| 182 | + |
| 183 | + print("preparing raw imdb data") |
| 184 | + data_gz = check_exist_or_download(imdb_dataset_link) |
| 185 | + data_dir = unzip_data(download_dir, data_gz) |
| 186 | + |
| 187 | + # imdb dirs |
| 188 | + # vocab_f = data_dir + '/imdb.vocab' |
| 189 | + train_pos_dir = data_dir + '/train/pos/' |
| 190 | + train_neg_dir = data_dir + '/train/neg/' |
| 191 | + test_pos_dir = data_dir + '/test/pos/' |
| 192 | + test_neg_dir = data_dir + '/test/neg/' |
| 193 | + |
| 194 | + # nltk helpers |
| 195 | + tokenizer = ToktokTokenizer() |
| 196 | + stopword_list = nltk.corpus.stopwords.words('english') |
| 197 | + |
| 198 | + # load pretrained word2vec binary |
| 199 | + print("loading pretrained word2vec") |
| 200 | + google_news_pretrain_fp = check_exist_or_download( |
| 201 | + google_news_pretrain_embeddings_link) |
| 202 | + wv = KeyedVectors.load_word2vec_format(google_news_pretrain_fp, binary=True) |
| 203 | + |
| 204 | + # parse flat files to memory |
| 205 | + data = [] |
| 206 | + for data_dir, label in [(train_pos_dir, 1), (train_neg_dir, 0), |
| 207 | + (test_pos_dir, 1), (test_neg_dir, 0)]: |
| 208 | + for filename in os.listdir(data_dir): |
| 209 | + if filename.endswith(".txt"): |
| 210 | + with open(os.path.join(data_dir, filename), |
| 211 | + "r", |
| 212 | + encoding="utf-8") as fhdl: |
| 213 | + data.append((fhdl.read(), label)) |
| 214 | + |
| 215 | + # text review cleaning |
| 216 | + print("cleaning text review") |
| 217 | + imdb_data = pd.DataFrame(data, columns=["review", "label"]) |
| 218 | + imdb_data['review'] = imdb_data['review'].apply(strip_html) |
| 219 | + imdb_data['review'] = imdb_data['review'].apply( |
| 220 | + remove_between_square_brackets) |
| 221 | + imdb_data['review'] = imdb_data['review'].apply(remove_special_characters) |
| 222 | + imdb_data['review'] = imdb_data['review'].apply(simple_stemmer) |
| 223 | + imdb_data['review'] = imdb_data['review'].apply(remove_stopwords, |
| 224 | + args=(tokenizer, |
| 225 | + stopword_list)) |
| 226 | + imdb_data['token'] = imdb_data['review'].apply(tokenize) |
| 227 | + |
| 228 | + # build word2index and index2word |
| 229 | + w2i = dict() |
| 230 | + i2w = dict() |
| 231 | + |
| 232 | + # add vocab <pad> as index 0 |
| 233 | + w2i["<pad>"] = 0 |
| 234 | + i2w[0] = "<pad>" |
| 235 | + |
| 236 | + idx = 1 # start from idx 1 |
| 237 | + for index, row in imdb_data['token'].iteritems(): |
| 238 | + for w in row: |
| 239 | + if w in wv and w not in w2i: |
| 240 | + w2i[w] = idx |
| 241 | + i2w[idx] = w |
| 242 | + assert idx < 28241 |
| 243 | + idx += 1 |
| 244 | + assert len(w2i) == len(i2w) |
| 245 | + print("vocab size: ", len(w2i)) |
| 246 | + |
| 247 | + # encode tokens to int |
| 248 | + imdb_data['encoded'] = imdb_data['token'].apply(encode_token, |
| 249 | + args=(wv, w2i)) |
| 250 | + |
| 251 | + # select word vector weights for embedding layer from vocab |
| 252 | + embed_weights = [] |
| 253 | + for w in w2i.keys(): |
| 254 | + val = None |
| 255 | + if w in wv: |
| 256 | + val = wv[w] |
| 257 | + else: |
| 258 | + val = np.zeros([ |
| 259 | + 300, |
| 260 | + ]) |
| 261 | + embed_weights.append(val) |
| 262 | + embed_weights = np.array(embed_weights) |
| 263 | + print("embedding layer lookup weight shape: ", embed_weights.shape) |
| 264 | + |
| 265 | + # split into train and test |
| 266 | + train_data = imdb_data[['encoded', 'label']].values |
| 267 | + train, val = train_test_split(train_data, test_size=0.33, random_state=42) |
| 268 | + |
| 269 | + # save preprocessed for training |
| 270 | + imdb_processed = { |
| 271 | + "train": train, |
| 272 | + "val": val, |
| 273 | + "embed_weights": embed_weights, |
| 274 | + "w2i": w2i, |
| 275 | + "i2w": i2w |
| 276 | + } |
| 277 | + print("saving preprocessed file to ", preprocessed_imdb_data_fp) |
| 278 | + with open(preprocessed_imdb_data_fp, 'wb') as handle: |
| 279 | + pickle.dump(imdb_processed, handle, protocol=pickle.HIGHEST_PROTOCOL) |
| 280 | + |
| 281 | + |
| 282 | +if __name__ == "__main__": |
| 283 | + preprocess() |
0 commit comments