用lstm實現nlp情感分析(roman urdu小語種爲例)代碼+原理詳解

1 賽題描述

link: https://www.kesci.com/home/competition/5c77ab9c1ce0af002b55af86/content/1
本練習賽所用數據,是名爲「Roman Urdu DataSet」的公開數據集。
這些數據,均爲文本數據。原始數據的文本,對應三類情感標籤:Positive, Negative, Netural。
本練習賽,移除了標籤爲Netural的數據樣例。因此,練習賽中,所有數據樣例的標籤爲Positive和Negative。
本練習賽的任務是「分類」。「分類目標」是用訓練好的模型,對測試集中的文本情感進行預測,判斷其情感爲「Negative」或者「Positive」。

本文全部代碼上傳至我的github:
https://github.com/willinseu/kesci-urdu-sentiment-analysis
覺得對你有幫助的話,請幫我點一下star,多謝!

2.lstm解法

讀取數據:

df_train = pd.read_csv('train.csv',lineterminator='\n')
df_test = pd.read_csv('test.csv',lineterminator='\n')
df_train['label'] = df_train['label'].map({'Negative':0,'Positive':1})
df_train.head(20)

在這裏插入圖片描述
由於是小語種,所以我們也看不懂上面的話是什麼意思,但是標籤我們還是看的懂的,並用map函數編碼。

#test if nan exists
df_train.isnull().sum()

檢查是否有缺失值:
在這裏插入圖片描述

df_test.head()

查看測試集,顯然是沒有標籤的:
在這裏插入圖片描述

numpy_array = df_train.as_matrix()
numpy_array_test = df_test.as_matrix()

將訓練集,測試集轉爲矩陣形式:
在這裏插入圖片描述
(其實沒有必要這麼做,直接對df表格操作即可。但是在此不做深究了。)
可以看到每一行被我們拆分成了id,text,label的形式。

#two commom ways to clean data
def cleaner(word):
  word = re.sub(r'\#\.', '', word)
  word = re.sub(r'\n', '', word)
  word = re.sub(r',', '', word)
  word = re.sub(r'\-', ' ', word)
  word = re.sub(r'\.', '', word)
  word = re.sub(r'\\', ' ', word)
  word = re.sub(r'\\x\.+', '', word)
  word = re.sub(r'\d', '', word)
  word = re.sub(r'^_.', '', word)
  word = re.sub(r'_', ' ', word)
  word = re.sub(r'^ ', '', word)
  word = re.sub(r' $', '', word)
  word = re.sub(r'\?', '', word)

  return word.lower()
def hashing(word):
  word = re.sub(r'ain$', r'ein', word)
  word = re.sub(r'ai', r'ae', word)
  word = re.sub(r'ay$', r'e', word)
  word = re.sub(r'ey$', r'e', word)
  word = re.sub(r'ie$', r'y', word)
  word = re.sub(r'^es', r'is', word)
  word = re.sub(r'a+', r'a', word)
  word = re.sub(r'j+', r'j', word)
  word = re.sub(r'd+', r'd', word)
  word = re.sub(r'u', r'o', word)
  word = re.sub(r'o+', r'o', word)
  word = re.sub(r'ee+', r'i', word)
  if not re.match(r'ar', word):
    word = re.sub(r'ar', r'r', word)
  word = re.sub(r'iy+', r'i', word)
  word = re.sub(r'ih+', r'eh', word)
  word = re.sub(r's+', r's', word)
  if re.search(r'[rst]y', 'word') and word[-1] != 'y':
    word = re.sub(r'y', r'i', word)
  if re.search(r'[bcdefghijklmnopqrtuvwxyz]i', word):
    word = re.sub(r'i$', r'y', word)
  if re.search(r'[acefghijlmnoqrstuvwxyz]h', word):
    word = re.sub(r'h', '', word)
  word = re.sub(r'k', r'q', word)
  return word

def array_cleaner(array):
  # X = array
  X = []
  for sentence in array:
    clean_sentence = ''
    words = sentence.split(' ')
    for word in words:
      clean_sentence = clean_sentence +' '+ cleaner(word)
    X.append(clean_sentence)
  return X

上面定義的是一個常用的nlp語句清洗的函數。同時會把一個array轉爲一個list,這個list我們下面會看到。

X_test = numpy_array_test[:,1]
X_train = numpy_array[:, 1]
# Clean X here
X_train = array_cleaner(X_train)
X_test = array_cleaner(X_test)
y_train = numpy_array[:, 2]

利用上面定義的函數進行清洗:
得到而list:
在這裏插入圖片描述
現在好像看不到這樣清洗有什麼作用,但需要說明的是,確實是有用的。會去除掉一些沒用的符號,但是這裏也沒有很好的針對urdu特別設置,所以還是有瑕疵的。

print(len(X_train))
print(len(X_test))
print(len(y_train))

打印長度,確認中間沒有出錯。
在這裏插入圖片描述

y_train = np.array(y_train)
y_train = y_train.astype('int8')
y_train[:6]

轉化y_train的格式。
在這裏插入圖片描述

X_all = X_train + X_test # Combine both to fit the tokenizer.
lentrain = len(X_train)

下面就開始編碼了,我們採用的是keras.preprocessing.text.Tokenizer

tokenizer = Tokenizer(
    nb_words=2000, 
                      filters='!"#$%&()*+,-./:;<=>?@[\\]^_`{|}~\t\n',
                                   lower=True,split=' ')
tokenizer.fit_on_texts(X_all)

這時候tokenizer乾的事其實很簡單,就是把它看到的單詞以空格劃分,然後用數字來一一對應,然後我們取前2000個出現頻率最高的詞,其他的當做不認識。
如想很詳細的瞭解,我整理了一些資料鏈接:
在這裏插入圖片描述
來自:https://blog.csdn.net/edogawachia/article/details/79446354
此時X_all沒有變化。
在這裏插入圖片描述

X = tokenizer.texts_to_sequences(X_all)
# X = pad_sequences(X)
X[:2]

現在開始text–>sequence。
在這裏插入圖片描述
可以看到長短不一,我們需要pad填充。

X = pad_sequences(X)
X[:2]

在這裏插入圖片描述
被填充成了最大的長度。
在這裏插入圖片描述
可以看到到目前爲止,我們的X變爲了(9040,219)的數據維度。
下面就開始 了embedding以及lstm。

embed_dim = 128
lstm_out = 256
batch_size = 32

model = Sequential()
model.add(Embedding(2000,embed_dim, input_length=X.shape[1],dropout = 0.2))
model.add(LSTM(lstm_out, dropout_U = 0.2, dropout_W = 0.2,return_sequences=True))
model.add(Flatten())
model.add(Dense(2,activation='softmax'))
model.compile(loss = 'categorical_crossentropy', optimizer='adam',metrics = ['accuracy'])
# model.compile(loss = 'binary_crossentropy',optimizer='adam',metrics = ['accuracy'])
print(model.summary())

在這裏插入圖片描述

3.深究embedding層。

因爲代碼實現的話,很簡單,add,add的就完事了 ,但是其中的細節與原理我認爲纔是最重要的。但是本人能力有限,有錯誤請及時批評指正。

3.1爲什麼需要embedding層?直接輸入不行嗎?

1.從原理上講,是這樣的。因爲在進入網絡之前,我們雖然將數據處理成了整齊的(9040,219)形式,但是這219維向量彼此之間是沒有關係的,也就是說你這樣處理只是乾巴巴的把文字變成了數字,但是前後文的關係被你丟失了,而lstm我們都知道它會考慮前後文的關係,而你這時候的數據已經缺失了上下文關係,所以需要經過一種手段,重新還原原來的上下文關係,這種手段就是embedding層。embedding層和word2vec是一樣的,無論是Skip-gram 還是CBOW 模型,他們都是由上下文與當前互相推斷,所以考慮了前後文關係。在此我們不展開講,我之前整理過一些連接:
https://blog.csdn.net/ssswill/article/details/88319996

3.2網絡參數爲什麼是256000個?

這個問題其實不難回答,因爲word2vec就是把one-hot向量轉爲了你指定的embedding層維度。
也就是說:你的數據流向是這樣的:
(9040,219,1)–》(9040,219,2000),這是onehot表示。這一步不是embedding層做的事。
下面就是從(9040,219,2000)–》(9040,219,128),這個128是自己設置的維度,它代表一個詞的維度。這就是embedding層做的事了。中間權重矩陣就很好寫了:(2000,128)。所以參數個數爲:2000*128=256000個。

3.3embedding層的輸入輸出?

從上面的分析,我們很容易看出來了,就是把(9040,219,1)變爲了(9040,219,128)。也就是9040個句子,每個句子包含219個詞,每個詞的維度爲128.。
在這裏插入圖片描述
https://blog.csdn.net/ssswill/article/details/88319996

4.深究lstm層。

4.0一些background

我們lstm層的實現也只有瀟瀟灑灑兩句話:

model.add(LSTM(lstm_out, dropout_U = 0.2, dropout_W = 0.2,return_sequences=True))
model.add(Flatten())

return_sequences=True該參數聲明爲True之後,需要加一個flatten層。我嘗試過,return_sequences=True之後更好。
至於它是幹啥的,爲啥加了它就需要flatten層?我們下面慢慢講。

4.1從簡化版的lstm講起

embed_dim = 128
lstm_out = 256
batch_size = 32

model2 = Sequential()
model2.add(Embedding(2000,embed_dim, input_length=X.shape[1],dropout = 0.2))
model2.add(LSTM(lstm_out, dropout_U = 0.2, dropout_W = 0.2))
# model2.add(Flatten())
model2.add(Dense(2,activation='softmax'))
model2.compile(loss = 'categorical_crossentropy', optimizer='adam',metrics = ['accuracy'])
# model.compile(loss = 'binary_crossentropy',optimizer='adam',metrics = ['accuracy'])
print(model2.summary())

在這裏插入圖片描述
我們從embedding層的輸出開始,它的輸出是(32,219,128)。因爲我們batch_size是32。同時我們的lstm_out=256,也就是units參數爲256,這裏可不是說有256個lstm的cell,而是指的是cell裏隱藏層的神經元個數是4*256,也就是輸出是256維的。

4.2參數爲什麼是394240?

(128+256)*256+256=98560。而lstm單元一共有4個權重矩陣,所以參數是4乘以98560=394240.至於原因:
https://blog.csdn.net/ssswill/article/details/88429794

4.3輸出爲什麼是(None,256)?

因爲輸入是(32,219,128)。32是batch_size。219不僅是句子長度,也是一個timestep參數。即每219個時刻後更新一次參數。在時刻1,32個句子的第一個單詞輸入到lstm中,即輸入是(32,128)。contact後變爲(32,128+256),也就是(32,384)。經過權重矩陣(384+1,256),其中+1是bias。也就是每個時刻輸出的是(32,256)。這樣就解釋了輸出爲什麼是(None,256)。

4.4那麼本代碼中爲啥輸出是

在這裏插入圖片描述

一個三維向量?

很好理解,在初始版lstm中,我們每個時刻都會輸出一個(32,256),而句子長度都是固定的219,所以219個時刻一共會輸出219個(32,256),而沒有加return_sequences=True參數之前,他只會保留最後一個時刻的輸出,所以是(32,256),但是加了return_sequences=True參數之後,每個時刻的輸出都會保留,那麼輸出就是219個(32,256),也就是(32,219,256)。但是每個時刻的輸出不變,所以參數不變。

同時,保留多個時刻的輸出,經過我的驗證,效果是有的。

4.5flatten層作用?

現在就很好理解了,就是爲了方面後面全連接層的連接而已,把一個(219,256)的二維矩陣壓扁成一維。

4.6理解了嗎?

因爲lstm的話,我這裏原理沒講,所以如果沒有理清,請到我上一篇博客找一些思路,相信你一定可以搞懂。連接:
https://blog.csdn.net/ssswill/article/details/88429794

5.一些後續


後面的代碼就是預測,提交了。我不再展開了,寫的真的很累,可以去我的github裏看一下,順便點一下star~感謝 ,說明下,我在這裏加了一個keras輸出auc指標的模塊,直接用就行。

同時,我覺得是由於數據量比較小,而且embedding層語料不豐富,所以lstm在此表現0.83左右,不是很理想。

6.2019年5月9日更新

很高興我的博客幫助到了一些人,如果大家對於nlp有興趣,或者不滿足於本文提出的基礎解法。歡迎到我第二個nlp大型博文。它同樣是解決情感分析問題的,但是情況更復雜,我們也會用到更復雜的技術。由於時間關係我並沒有更新完整。
對應的github在:
https://github.com/willinseu/kaggle-Jigsaw-Unintended-Bias-in-Toxicity-Classification-solution

裏面我用本文的方法實現了對於另外一個數據集的情感分析。
至於提升版本的,還沒有上傳,但是其中一些關鍵性技術我已經寫完了,但是沒有給串成一個整體。可以先看一下:
在這裏插入圖片描述
在後面我一定會更新完的,可以watch一下我的github項目。保證乾貨滿滿。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章