# Steam review sentiment classification > [王軒, 0816095] ## I. Introduction: 主要是因為對NLP有點興趣所以想嘗試看看做簡單的評論情緒分類。 主要的想法是利用遊戲的tag結合這款遊戲的評論來預測這則評論是正面的還是負面的。匹如說一款恐怖遊戲的留言可能像這樣:"this is a terrifying game.",文字中包含了terrifying這個字或許就是在誇獎這個恐怖遊戲"很恐怖",因此是正面的,terrifying這個字在給小孩玩的遊戲上就不是個正面的詞語,這是我一開始的想法。 ## II. Data Collection dataset是從網路上爬下來的,而我爬資料的方式是先把steam store遊戲列上的前100款遊戲的所有屬性都抓下來,包含tag、genre、id、price或甚至是否打折。再從這100款遊戲去抓他們的評論。因為遊戲評論在絕大多數遊戲上都是正面的,所以一開始爬下來的dataset非常不平均。 因此我後來決定自己挑選遊戲。去steam的網站把100款評論褒貶不一或特爛的遊戲網址全複製下來,然後把他們的屬性都抓下來,再把他們的評論都抓下來,就終於準備好我的dataset了。 爬steam評論的截圖: ![](https://i.imgur.com/nU908Lw.png) ## III. Preprocessing 因為data爬下來是.jl檔,因此用pd.read_jason()把data讀進來並只留下需要的column,然後把所有有null的row都捨棄。 ```python= df1 = pd.read_json('review.jl', lines=True) df2 = pd.read_json('products.jl', lines=True) df1 = df1[['product_id', 'recommended', 'text']] df1 = df1.dropna() df2['tags'] = df2['tags'].apply(','.join) df2 = df2[['id', 'tags']] df2 = df2.dropna() df2 = df2.rename({'id':'product_id'}, axis='columns') ``` 把pruduct的資料和review的資料用product_id join再一起,就形成一個有留言又有遊戲tag的data了。 ```python= df3=pd.merge(df2,df1,on='product_id',how='inner') df3 = df3.drop(['product_id'], axis = 1) ``` ![](https://i.imgur.com/f44xDIv.png) 把所有包含非ascii碼的留言去除。 ```python= df3 = df3[df3.text.map(lambda x: x.isascii())] ``` 即使經過我的努力挑選,正面的評論的量還是佔了大多數,因此我決定再刪掉一些正面評論。 調整前及調整後對留言是正面和負面的畫出留言長度數量的長條圖: ![](https://i.imgur.com/ROhOxeg.png) ![](https://i.imgur.com/IGgxiVf.png) 調整前及調整後正面評論和負面評論的數量圓餅圖: ![](https://i.imgur.com/LoWuoly.png) ![](https://i.imgur.com/7sbbltS.png) 結果顯示最長的留言長達7993。 ![](https://i.imgur.com/6VChsl8.png) 在檢查data的時候有發現很多人都喜歡用點排出圖片的樣子,或是打一堆讚或倒讚,因此我推測許多過長的留言都是一些無意義的文字,為了防止干擾model把過長的留言去除。 ```python= df3 = df3[df3['length_review']<2000] df3.describe() ``` ![](https://i.imgur.com/nfcYZqM.png) 我做了兩種preprocessing: * 用tensorflow的tokenizer把文字都轉成數字,不一樣長的都補0。 * 用sklean的TfidfVectorizer把文字的權重用詞語出現的頻率以及詞語在多少個data裡面出現來算出 因此總共有四種不同data可以比較: * 遊戲tag及評論本身 * 評論本身 * 用tf-idf做遊戲tag及評論本身 * 用tf-idf做評論本身 將每個文字編碼為數字,只取最常見的30000個字,在predict的時候沒看過的字都編碼為OOV。 ```python= tokenizer = Tokenizer(num_words = 30000, oov_token="<OOV>") tokenizer.fit_on_texts(X_train['text']) tokenizer.fit_on_texts(X_train['tags']) word_index = tokenizer.word_index sequences = tokenizer.texts_to_sequences(X_train['text']) reviews = pad_sequences(sequences, padding='post', maxlen=411) ``` 用sklearn的TfidfVectorizer算出權重。 ```python= tf_idf = TfidfVectorizer() ``` ## IV. Models 我總共用了四個model: * Multinomial Naive Bayes * Logistic Regression * ANN * Bidirectional Encoder Representations from Transformers(簡稱Bert) ### 1. Without tf-idf #### I. Multinomial Naive Bayes 直接套sklearn的MultinomialNB: ```python= mnb = MultinomialNB() mnb.fit(X_train_tf, y_train) ``` ![](https://i.imgur.com/G8vSqy8.png)![](https://i.imgur.com/KpI7BHj.png) ![](https://i.imgur.com/hWY95oR.png) 出來的結果實在不盡理想,沒有遊戲tag的accuracy只有0.6395。 有遊戲tag的accuracy也只有0.6435。 但令我感到開心的是有遊戲tag的準確度是比較高的(雖然也只高一點點XD),驗證了我一開始的想法。 #### II. Logistic Regression 直接套sklearn的LogisticRegression: ```python= lr=LogisticRegression() lr.fit(X_train_tf, y_train) ``` ![](https://i.imgur.com/EE6VZzm.png)![](https://i.imgur.com/CGOhzgh.png) ![](https://i.imgur.com/T5xFDMI.png) 結果比Multinomial Naive Bayes稍微好一點 有遊戲tag的準確度還是稍微高於沒有tag。 #### III. ANN 基本上跟hw5差不多,只調了一點參數。使用adaptive learning rate比免overfit,在fit時放入validation data來觀察performance: ```python= def build_model(): model = Sequential() model.add(Dense(1024, input_dim=X_train_tf.shape[1], activation='relu')) model.add(Dense(2, activation='softmax')) optimizer = keras.optimizers.Adam(learning_rate=5e-5) model.compile(optimizer=optimizer, loss='categorical_crossentropy', metrics=['accuracy']) model.summary() return model print("Compile model ...") callback = tf.keras.callbacks.ReduceLROnPlateau(monitor="val_accuracy", patience=2, verbose=1, factor=0.5, min_lr=5e-5) estimator = KerasClassifier(build_fn=build_model, epochs=5, batch_size=512) estimator.fit(X_train_tf, y_train, validation_data=(X_test_tf, y_test_transform), callbacks=[callback]) ``` ![](https://i.imgur.com/KGZJNPq.png)![](https://i.imgur.com/c6brM2E.png) ![](https://i.imgur.com/FXqrfgZ.png) ![](https://i.imgur.com/KTkCzJQ.png) 結果是最爛的,~~有可能是我不會調model。~~ 有遊戲tag的準確度還是稍微高於沒有tag。 ### 2. With tf-idf #### I. Multinomial Naive Bayes 一樣直接套sklearn的MultinomialNB: ![](https://i.imgur.com/B7w2yM9.png)![](https://i.imgur.com/t7bTq6Y.png) ![](https://i.imgur.com/GSCB6Oy.png) 出來的結果直接跳一大階,雖然是意料之中的結果但還是很爽XD。 但是經過tf-idf轉換後遊戲tag反而成為累贅了,沒有tag準確度遠高於有tag的data,我的猜測是經過轉換後因為tag中的字詞出現次數太多,匹如說"Action"或"First person shooter"很多遊戲都有這樣的tag,因此他們出現在幾乎所有的data中,導致權重非常低以至於成為像噪音的存在,最後反而導致performace降低。 #### II. Logistic Regression 一樣直接套sklearn的LogisticRegression: ![](https://i.imgur.com/BNH0DPn.png)![](https://i.imgur.com/UaUOxxf.png) ![](https://i.imgur.com/8yvfhWR.png) Logistic Regression一樣又比MultinomialNB強了一點。 因為一樣也是tf-idf所以有tag的performance也是低了點。 不過0.87這數字幾乎可以說是可以跟人判得差不多準了。 #### III. ANN ![](https://i.imgur.com/j895YkR.png)![](https://i.imgur.com/516Fcki.png) ![](https://i.imgur.com/ly1nmRr.png) ![](https://i.imgur.com/erRUII8.png) 這次ANN的performance跟Logistic Regression不相上下甚至可以說是更好,都達到了0.87的accuracy,但ANN的f1 score不論有沒有tag都比Logistic Regression還高。 ### Bidirectional Encoder Representations from Transformers(簡稱Bert) 要做NLP一定會先想到NLP界的巨人Bert,因此我也想來嘗試試看Bert的威力,因為training過程太久因此單獨做,並沒有使用結合遊戲tag的方式,單純用評論本身去判斷。 #### I. Preprocessing preprocessing的部分跟之前差不多,用pd.read_jason讀檔,刪掉不要的column及包含null的row。 從data中分出training, validation, testing。 ![](https://i.imgur.com/gcLlrki.png) training就是拿來training用的,validation是拿來觀察每個epoch的accuracy情況,testing是最後選出最好的model拿來測試用的。 最終的比例是0.8:0.1:0.1。 ![](https://i.imgur.com/ITlDhCC.png) #### II. Training ~~最輕鬆也最痛苦的部分~~ 用for迴圈去跑每個epoch,如果accuracy比之前的都高就把當前的state及accuracy存起來。 ```python= def save_model(best_accuracy, best_state_dict): global filename acc_string = "{:.2f}".format(best_accuracy) filename = f"{EMOTION}-{dt.now().strftime('%Y-%m-%d-%H-%M-%S')}-{acc_string}.pkl" torch.save(best_state_dict, filename) filename = "" history = defaultdict(list) best_accuracy = 0.0 best_state_dict = {} for epoch in range(EPOCHS): print(f'Epoch {epoch + 1}/{EPOCHS}') print('-' * 10) train_acc, train_loss = train_epoch( model, train_data_loader, loss_fn, optimizer, device, scheduler, len(df_train) ) print(f'[Training] Loss: {train_loss} Accuracy: {train_acc}') val_acc, val_loss = eval_model( model, val_data_loader, loss_fn, device, len(df_val) ) print(f'[Validation] Loss: {val_loss} Accuracy: {val_acc}') print() history['train_acc'].append(train_acc) history['train_loss'].append(train_loss) history['val_acc'].append(val_acc) history['val_loss'].append(val_loss) if val_acc > best_accuracy: best_accuracy = val_acc best_state_dict = model.state_dict() save_model(best_accuracy, best_state_dict) ``` 到這邊colab的效能已經不夠了,因此只能請擁有超強電腦的同學幫忙train,經過了漫長的兩個多小時,最終給出的結果不令人失望,準確率來到了0.89! ![](https://i.imgur.com/BWqZIWT.png) training的過程用折線圖展現。 ![](https://i.imgur.com/ZKywC8V.png) 用之前存好最好的model拿來預測test data,準確度終於突破0.9大關了,這樣的準確度連我自己都甘拜下風。 ![](https://i.imgur.com/oLheyK9.png) performace: ![](https://i.imgur.com/mvDDNyG.png) confusion matrix: ![](https://i.imgur.com/mvo2zXk.png) ## V. Results 結果的部分在model那邊已經講得差不多了。 主要就是: 1. 沒有tf-idf LogisticRegression > MultinomialNB > ANN 2. 經過tf-idf ANN > LogisticRegression > MultinomialNB 3. Bert >>> All ## VI. Conclusion NLP真的不是一個好做的東西,單純的把文字編碼成數字的效果奇低,除非做一些轉換不然文字的深奧不是隨隨便便就能駕馭的。 在做final project的過程讓我學到許多有關NLP的相關知識,也讓我對這方面更有興趣了,當作為了下學期的自然語言概論鋪路。 ## Conclusion & Application 感覺單純只看到accuracy和f1 score有點空虛,因此我做了一個簡單互動式的classifier,這樣可以更好的測試及了解model的性能。因此我就拿了基本三個model中表現最好的tf-idf加ANN。 功能就是輸入一則評論,機器會回復"positive review!"或"negative review :("。 先輸入了一些常見的評論,大部分都是對的,除了少數比較容易混淆的像是"not bad",本意為不錯,但兩個字都是負面的,因此機器錯誤判斷為負面評論。 ![](https://i.imgur.com/6O4CBNS.png) 再來從dataset裡面找一些評論,基本上都是對的,除了"I prefer L4D"這句幾乎都是中性詞語的句子,很難辨別出正負,但對人類來說非常簡單,可見單純的tf-idf也有許多做不到的地方。 ![](https://i.imgur.com/TB8UlV4.png)