Jigsaw Unintended Bias in Toxicity Classification競賽bilstm+glove embedding解法

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代碼思路說明

一、預處理階段

  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的增序。
至於爲什麼要循環兩遍,僅僅是爲了獲得更加魯棒的輸出而已,在這兩遍的循環中,執行內容是一樣的。

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