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項目。保證乾貨滿滿。