# 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評論的截圖:

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

把所有包含非ascii碼的留言去除。
```python=
df3 = df3[df3.text.map(lambda x: x.isascii())]
```
即使經過我的努力挑選,正面的評論的量還是佔了大多數,因此我決定再刪掉一些正面評論。
調整前及調整後對留言是正面和負面的畫出留言長度數量的長條圖:


調整前及調整後正面評論和負面評論的數量圓餅圖:


結果顯示最長的留言長達7993。

在檢查data的時候有發現很多人都喜歡用點排出圖片的樣子,或是打一堆讚或倒讚,因此我推測許多過長的留言都是一些無意義的文字,為了防止干擾model把過長的留言去除。
```python=
df3 = df3[df3['length_review']<2000]
df3.describe()
```

我做了兩種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)
```


出來的結果實在不盡理想,沒有遊戲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)
```


結果比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])
```



結果是最爛的,~~有可能是我不會調model。~~
有遊戲tag的準確度還是稍微高於沒有tag。
### 2. With tf-idf
#### I. Multinomial Naive Bayes
一樣直接套sklearn的MultinomialNB:


出來的結果直接跳一大階,雖然是意料之中的結果但還是很爽XD。
但是經過tf-idf轉換後遊戲tag反而成為累贅了,沒有tag準確度遠高於有tag的data,我的猜測是經過轉換後因為tag中的字詞出現次數太多,匹如說"Action"或"First person shooter"很多遊戲都有這樣的tag,因此他們出現在幾乎所有的data中,導致權重非常低以至於成為像噪音的存在,最後反而導致performace降低。
#### II. Logistic Regression
一樣直接套sklearn的LogisticRegression:


Logistic Regression一樣又比MultinomialNB強了一點。
因為一樣也是tf-idf所以有tag的performance也是低了點。
不過0.87這數字幾乎可以說是可以跟人判得差不多準了。
#### III. ANN



這次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。

training就是拿來training用的,validation是拿來觀察每個epoch的accuracy情況,testing是最後選出最好的model拿來測試用的。
最終的比例是0.8:0.1:0.1。

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

training的過程用折線圖展現。

用之前存好最好的model拿來預測test data,準確度終於突破0.9大關了,這樣的準確度連我自己都甘拜下風。

performace:

confusion matrix:

## 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",本意為不錯,但兩個字都是負面的,因此機器錯誤判斷為負面評論。

再來從dataset裡面找一些評論,基本上都是對的,除了"I prefer L4D"這句幾乎都是中性詞語的句子,很難辨別出正負,但對人類來說非常簡單,可見單純的tf-idf也有許多做不到的地方。
