0.寫在前面
0.1本文配套github:
https://github.com/willinseu/kaggle-Jigsaw-Unintended-Bias-in-Toxicity-Classification-solution
如果你覺得本文對你有幫助,或者有提高,請點一個star以表支持,感謝~
同時與上一篇博文的github項目是對接的:
https://github.com/willinseu/kesci-urdu-sentiment-analysis
0.2 數據集
數據:
https://www.kaggle.com/c/jigsaw-unintended-bias-in-toxicity-classification/data
0.3一些說明
本文要求你具有神經網絡,pandas,keras的基礎知識。
如果你是一個nlp基礎較爲薄弱的人,建議先看本文的兄弟篇:
用lstm實現nlp情感分析(roman urdu小語種爲例)代碼+原理詳解,再看本篇(提高篇)
不然會對tokenizer等用法很陌生,而且對整體的nlp思路也會不清晰。
如果你基礎紮實,對於各種nlp手法都很熟悉,建議直接去kaggle比賽頁閱讀相關內容,本文僅是爲了幫助一些初學者。
0.4文章截圖說明
文中會出現多處jupyter代碼截圖。截圖均來自我的github。如需要請前往文中開頭的鏈接。
0.5 關於原代碼
代碼大部分來自:https://www.kaggle.com/thousandvoices/simple-lstm?scriptVersionId=12556909
但是其實對於初學者而言,原代碼很難看懂,所以這就是我寫這篇博客的目的。
並且github中用notebook寫了下注釋。
1.賽題分析
比賽呢,簡單說來還是一個情感分析的二分類問題。但說複雜點了,就是一個多輸出的二分類問題。
我們先看一眼這個數據集,再做討論。
我們這裏默認target>0.5的爲1,<0.5的爲0,這樣問題就是一個二分類問題了。
我們在上一篇博文中見到的只有圖中的紅框。意思很簡單,給你一句話,那你就要給我它的輸出是0還是1,經典的情感分析問題。
當然了,對於本數據集,你也完全可以忽略掉全部,只看紅框,然後搭建網絡做判別,我在我的github上也實現了這種初級解法,auc可以達到0.9左右。
1.1只關注紅框與關注紅+藍的區別到底是什麼?
這其實是一個很重要的問題!!!但是經常有人會說:這不顯而易見嗎?你用到的數據多了,模型肯定好。feature越多越好木問題的~
請思考的更深入一些。Think Deep.
下面開始我們的分析:
我們可以很明顯的發現,你只關注紅框,那麼數據集你沒有完全利用到,藍框的數據你都沒用到。說專業點,你的網絡只關注到了1或者0,或者說你只告訴了它是不是toxic,那他的表現自然不能太好。但是如果你告訴網絡,這句話的toxic程度爲0.8,insult(辱罵)程度爲0.7,…那麼這句話就真正被網絡fit進去了。
或者換句話說,一句話toxic程度是0.6,另一句是0.9,而我們都認爲是1。那麼其中的差異性就被我們丟失了,所以關於在這一個二分類問題中關注這些連續型變量就是這個目的。所以我們需要把float形式的target加進去。剩下的feature加進去也是一個道理。
所以我們的問題是一個多輸出問題,訓練集的構造也應該是這樣:
x_train = preprocess(train['comment_text'])
y_train = np.where(train['target'] >= 0.5, 1, 0)
y_aux_train = train[['target', 'severe_toxicity', 'obscene', 'identity_attack', 'insult', 'threat']]
x_test = preprocess(test['comment_text'])
1.2代碼思路說明
一、預處理階段
- 文本預處理,去除特殊符號等
- 利用glove/fasttext 進行數據的編碼,生成權重矩陣
二、模型構建以及訓練
2.文本預處理
在本代碼中,我們只進行了簡單的處理:
def preprocess(data):
'''
Credit goes to https://www.kaggle.com/gpreda/jigsaw-fast-compact-solution
'''
punct = "/-'?!.,#$%\'()*+-/:;<=>@[\\]^_`{|}~`" + '""“”’' + '∞θ÷α•à−β∅³π‘₹´°£€\×™√²—–&'
def clean_special_chars(text, punct):
for p in punct:
text = text.replace(p, ' ')
return text
data = data.astype(str).apply(lambda x: clean_special_chars(x, punct))
return data
可以看到僅僅是去除了一些字符而已。我不打算對這個函數多講,因爲不復雜,而且不是本文重點。
如果想看到一些稍微複雜的預處理,我做了一些,在我的github的V1-fundamental中。
https://github.com/willinseu/kaggle-Jigsaw-Unintended-Bias-in-Toxicity-Classification-solution/blob/master/v1-fundamental.ipynb
3.如何使用glove生成權重矩陣。
對應代碼:
def get_coefs(word, *arr):
return word, np.asarray(arr, dtype='float32')
def load_embeddings(path):
with open(path) as f:
return dict(get_coefs(*line.strip().split(' ')) for line in f)
def build_matrix(word_index, path):
embedding_index = load_embeddings(path)
embedding_matrix = np.zeros((len(word_index) + 1, 300))
for word, i in word_index.items():
try:
embedding_matrix[i] = embedding_index[word]
except KeyError:
pass
return embedding_matrix
我這裏的權重矩陣指的是:
x = Embedding(*embedding_matrix.shape, weights=[embedding_matrix], trainable=False)(words)
中出現的embedding_matrix。
關於本小節內容,同樣爲了使本文不過於臃腫,我寫在了:
如何使用glove,fasttext等詞庫進行word embedding?(原理篇)
如何使用glove,fasttext等詞庫進行word embedding?(代碼篇)
使用的數據集正是本比賽的數據,所以不用擔心不對應的問題。
4模型構建⭐️
這是本文旨在重點寫的地方。
首先,看一下模型構建的代碼:
def build_model(embedding_matrix, num_aux_targets):
words = Input(shape=(MAX_LEN,))
x = Embedding(*embedding_matrix.shape, weights=[embedding_matrix], trainable=False)(words)
x = SpatialDropout1D(0.3)(x)
x = Bidirectional(CuDNNLSTM(LSTM_UNITS, return_sequences=True))(x)
x = Bidirectional(CuDNNLSTM(LSTM_UNITS, return_sequences=True))(x)
hidden = concatenate([
GlobalMaxPooling1D()(x),
GlobalAveragePooling1D()(x),
])
hidden = add([hidden, Dense(DENSE_HIDDEN_UNITS, activation='relu')(hidden)])
hidden = add([hidden, Dense(DENSE_HIDDEN_UNITS, activation='relu')(hidden)])
result = Dense(1, activation='sigmoid')(hidden)
aux_result = Dense(num_aux_targets, activation='sigmoid')(hidden)
model = Model(inputs=words, outputs=[result, aux_result])
print(model.summary())
model.compile(loss='binary_crossentropy', optimizer='adam')
return model
用了一個函數封裝模型,確實是很值得借鑑的方法。
我們一點點分析:
首先,模型接受到的是輸入數據是(None,220)的數據,其中220是填充長度。之後喂進去Embedding層,是一個什麼操作過程呢?其實這些我在上一篇nlp博文說過,首先會做一個onehot的變化,變成(None,220,100000)。
其中100000在此處提及過:
它代表我們認爲的不同的單詞數。之後,我們的權重矩陣形狀是(100000,300)的形狀的。爲什麼?請看第三節。
二者相乘,所以Embedding層的輸出是(None,220,300)。下面是一個Dropout操作,也是爲了讓每一個epoch喂進去的數據都稍有差異,或者你可以通俗的理解成防止過擬合。不過這裏用的是SpatialDropout1D。這是個啥?
同樣,我另開了一篇博客說它,參見:神經網絡高階技巧4–關於SpatialDropout1D
現在你可以理解成特殊形式的dropout,並且請牢記此dropout多用在Embedding後。這層輸出肯定仍是(None,220,300),只不過會有一些維度會被置0。形狀不會改變的。
之後是雙層的bilstm
x = Bidirectional(CuDNNLSTM(LSTM_UNITS, return_sequences=True))(x)
x = Bidirectional(CuDNNLSTM(LSTM_UNITS, return_sequences=True))(x)
hidden = concatenate([
GlobalMaxPooling1D()(x),
GlobalAveragePooling1D()(x),
])
hidden = add([hidden, Dense(DENSE_HIDDEN_UNITS, activation='relu')(hidden)])
hidden = add([hidden, Dense(DENSE_HIDDEN_UNITS, activation='relu')(hidden)])
其中的一些難以琢磨的點我也都寫在了別的博文中
關於什麼是CuDNNLSTM,參見:神經網絡高階技巧2–採用CuDNNLSTM,別再用LSTM了!
關於lstm的參數return_sequences=True,參見本博客開頭提到了兄弟篇章。
關於add與concatenate,參見:
神經網絡高階技巧3–對層操作之add與concatenate以及keras的summary中[0][0]的解釋
關於 GlobalMaxPooling1D()(x),
GlobalAveragePooling1D()(x),我寫在了:
神經網絡高階技巧5–關於GolbalAveragePooling與GlobalMaxPooling
所以說。。。想把一篇代碼講明白,真不是一件簡單的事。。。
你把一系列再穿起來,就不難理解了。
由於代碼中有一句話:
embedding_matrix = np.concatenate(
[build_matrix(tokenizer.word_index, f) for f in EMBEDDING_FILES], axis=-1)
就是同時利用了glove/fasttext兩個300維的矩陣,合併成了600.所以下面出現的數字和我剛纔說的不同,不是300,而是600.
模型長這樣:
5.參數個數詳解
爲什麼Embedding層參數是那麼多?
Sorry.這裏我之前寫錯了一點,
這個變量雖然定義了但是沒有用。但是本質沒變
所以這裏的權重矩陣是(327576,600)。327576*600=196545600。327576是整個數據集不同的詞的個數。 但是這是不可訓練的參數,因爲這個矩陣我們提前導入進去了。或者說訓練好了。
剩餘參數計算請參照我開頭提到的兄弟博客。
6模型訓練及預測⭐️
checkpoint_predictions = []
weights = []
for model_idx in range(NUM_MODELS):
model = build_model(embedding_matrix, y_aux_train.shape[-1])
for global_epoch in range(EPOCHS):
model.fit(
x_train,
[y_train, y_aux_train],
batch_size=BATCH_SIZE,
epochs=1,
verbose=2,
callbacks=[
LearningRateScheduler(lambda epoch: 1e-3 * (0.6 ** global_epoch))
]
)
checkpoint_predictions.append(model.predict(x_test, batch_size=2048)[0].flatten())
weights.append(2 ** global_epoch)
predictions = np.average(checkpoint_predictions, weights=weights, axis=0)
關於其中的LearningRateScheduler,請參見:
神經網絡高階技巧1–Learning Rate Scheduler
至於其中的權重與學習率,其實不難理解。
權重爲2的0次方,1次方。。。。。
也就是[1,2,4,8,1,2,4,8]以此作爲8個epoch輸出的權重。
然後每個epoch的學習率,參見剛纔的鏈接。
總結下,就是每個epoch的預測輸出都會被存儲在checkpoint_predictions裏。然後每個epoch的輸出都有一個權重,顯然隨着epoch的增大,權重要增大。所以是1,2,4,8的增序。
至於爲什麼要循環兩遍,僅僅是爲了獲得更加魯棒的輸出而已,在這兩遍的循環中,執行內容是一樣的。