---
# System prepended metadata

title: Steam review sentiment classification

---

# 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)




