李宏毅老师的作业四也同样是不好对付的,这次我仍然用 TensorFlow 实现一遍,记录踩坑过程。

迫于心疼我的笔电,这次作业在 Kaggle 编写程式、而本次作业的数据集,Kaggle 上也有现成的。

附上课程作业 4 的 Kaggle 地址:点击前往

Introduction for HomeWork

给的数据文件夹总共有三个档案
在这里插入图片描述

  • training_label.txt:有 label 的 training data(句子配上 0-negative or 1-postive,+++$+++ 是分隔符)

  • e.g., 1 +++$+++ are wtf … awww thanks !

  • training_nolabel.txt:沒有 label 的 training data(只有句子),用做 semi-supervised learning

  • e.g: hates being this burnt !! ouch

  • testing_data.txt:你要判断 testing data 里的句子是 0 or 1

    id,text

    0,my dog ate our dinner . no , seriously … he ate it .

    1,omg last day sooon n of primary noooooo x im gona be swimming out of school wif the amount of tears am gona cry

    2,stupid boys .. they ‘ re so .. stupid !

Load Data and Word2Vector

拷贝 ExampleCode 的函数式:

def load_training_data(path='data/training_label.txt'):
    if 'training_label' in path:
        with open(path, 'r') as f:
            lines = f.readlines()
            lines = [line.strip('\n').split(' ') for line in lines]
        x = [line[2:] for line in lines]
        y = [line[0] for line in lines]
        return x, y
    else:
        with open(path, 'r') as f:
            lines = f.readlines()
            x = [line.strip('\n').split(' ') for line in lines]
        return x
def load_testing_data(path='data/testing_data'):
    with open(path, 'r') as f:
        lines = f.readlines()
        X = ["".join(line.strip('\n').split(",")[1:]).strip() for line in lines[1:]]
        X = [sen.split(' ') for sen in X]
    return X
print("loading training data ...")
train_x, train_y = load_training_data('/kaggle/input/ml2020spring-hw4/training_label.txt')
train_x_no_label = load_training_data('/kaggle/input/ml2020spring-hw4/training_nolabel.txt')
print("loading testing data ...")
test_x = load_testing_data('/kaggle/input/ml2020spring-hw4/testing_data.txt')
print("loading data end")

读出完成数据之后,Print 数据的一项、查看读取出来的数据格式:
在这里插入图片描述
读取数据的函数将一个 sentence 被转化成了一个 wordlist,但我们不能直接将 wordlist 输入我们的 RNN,我们需要将每一个 word 转化成向量。

但为了将其转化成向量,我们需要一个word_vector_dict,这个字典记录了每一个 word 和 vector 的映射关系,但我们并没有这个字典,看题目的要求,我们需要手动训练得到这个字典,想必training_nolabel.txt这个文件的意义就是如此了。

对于用什么模型去训练出这样一个字典,在课程作业介绍视频里推荐我们使用 gensim,在查阅了在 Google 上找的文档后:点我前往,我实现的代码如下:

from gensim.models import word2vec
def train_word2vec(x):
    model = word2vec.Word2Vec(x, size=250, window=5, min_count=5, workers=12, iter=10, sg=1)
    return model
print("trainning model ...")
# model = train_word2vec(train_x + test_x + train_x_no_label)
model = train_word2vec(train_x + test_x)
print("trainning model end ...")

因为用的 kaggle,有 GPU 了就要好好利用,为了确保字典足够大,我一开始把三个数据集的 wordlist 全都丢进去了。然而运行过程中,GPU 的使用率为 0%,我意识到该模型的训练并没有使用到 GPU、我的内心是崩溃的,于是为了取巧(反正我又不要给老师打分),索性就把 train_x_no_label 舍弃掉了,这样可以大大缩减我们的训练时间,并且得到对于 train 和 test 来说质量都很好的字典,顺带一提训练的过程大概需要十分钟。为了避免每次都要训练该模型,我们可以将模型保存下来。

print("saving model ...")
model.save('./w2v_all.model'))
print("save model end")

当我们需要模型的时候,进行导入:

from gensim.models import Word2Vec
embedding = Word2Vec.load('./w2v_all.model')
embedding_dim = embedding.vector_size
print(embedding,embedding_dim)

但是我发现,这个 embedding 模型有个卵用,每个词 train 出来的向量实在是太复杂了,也许是现在我还不理解 gensim 的强大,但好在他将字符按照出现的频次排序了,我用很笨的方法自己生成了字典。

index_from = 3
word_index = {}
index_word = []
# generate word2index index2word and embedding_martix
for i, word in enumerate(embedding.wv.vocab):
    print('get words #{}'.format(i+1+index_from), end='\r')
    word_index[word] = len(word_index)+index_from+1

word_index['<PAD>'] = 0
word_index['<START>'] = 1
word_index['<UNK>'] = 2
word_index['<END>'] = 3
index_word = dict(
    [(value, key) for key, value in word_index.items()]
)

执行以上函数式、即可生成 word_index、index_word 这两个映射关系,顺便写了个函数验证:
在这里插入图片描述
完美!

然后我们需要将原来的 wordlist 转换成 vectorlist。

def transform_wordlist_ids(wordlist, word_index_dict):
    word_ids = []
    for word in wordlist:
        word_ids.append(word_index_dict.get(word, 2))
    return word_ids

def transform_sentense_list_id_list(sentence_list, word_index_dict):
    id_list = []
    for wordlist in sentence_list:
        id_list.append(transform_wordlist_ids(wordlist, word_index_dict))
    return id_list

train_data = transform_sentense_list_id_list(train_x, word_index)
train_labels = train_y
test_data = transform_sentense_list_id_list(test_x, word_index)

此时,train_data 的每个单元就变成了整型数组了。

最后,我们要对其 train 的每一组向量,为他们指定最大长度,使用 keras 内置的函数能很轻松的实现。

max_length = 50 # Max sentence length
train_data = keras.preprocessing.sequence.pad_sequences(train_data,
                                                        maxlen=max_length,
                                                        value=word_index['<PAD>'],
                                                        padding='post')
test_data = keras.preprocessing.sequence.pad_sequences(test_data,
                                                        maxlen=max_length,
                                                        value=word_index['<PAD>'],
                                                        padding='post')
print(len(train_data),len(train_labels))
print(len(test_data))
print(train_data[9])

在这里插入图片描述
那数据的读取和向量化就到这里,接下来我们开始训练模型了。

FirstModel - Embedding

首先用最简单的模型构建、Embedding+AveragePooling 就完事了

batch_size = 128
model = keras.models.Sequential()
# define an input matrix is vocab_size * embedding_dim
# output Matrix Shape will be (batch_size, input_length, output_dim)
model.add(keras.layers.Embedding(input_dim=vocab_size, output_dim=embedding_dim,
                                input_length=max_length))
model.add(keras.layers.GlobalAveragePooling1D())
model.add(keras.layers.Dense(64, activation='relu'))
model.add(keras.layers.Dense(1, activation='sigmoid'))

model.summary()

model.compile(optimizer='adam',
             loss='binary_crossentropy',
              metrics=['accuracy']
             )

SecondModel - RNN

在 Keras 里有简单实现 RNN 的方法,在 Embedding 后增加一层 SimpleRNN 的 layer。

batch_size = 128
model = keras.models.Sequential()
model.add(keras.layers.Embedding(input_dim=vocab_size, output_dim=embedding_dim,
                                input_length=max_length))

model.add(keras.layers.Bidirectional(keras.layers.SimpleRNN(units=64, return_sequences=False)))
model.add(keras.layers.Dense(64, activation='relu'))
model.add(keras.layers.Dense(1, activation='sigmoid'))

model.summary()

model.compile(optimizer='adam',
             loss='binary_crossentropy',
              metrics=['accuracy']
             )

但是仅增加一层 SimpleRNN 的效果不见好,因为 RNN 需要双向训练,在 keras 里也有简单实现的方法,如上文。

LastModel - LSTM

而对于 LSTM、实现的代码与 RNN 几乎相同。

batch_size = 128
model = keras.models.Sequential()
model.add(keras.layers.Embedding(input_dim=vocab_size, output_dim=embedding_dim,
                                input_length=max_length))

model.add(keras.layers.Bidirectional(keras.layers.LSTM(units=64, return_sequences=False)))
model.add(keras.layers.Dense(64, activation='relu'))
model.add(keras.layers.Dense(1, activation='sigmoid'))

model.summary()

model.compile(optimizer='adam',
             loss='binary_crossentropy',
              metrics=['accuracy']
             )

Summary

可能读者会发现,文中没有给出训练结果。其实是因为写到一半睡觉去了,醒来发现文中训练词向量的文件不见了,这是 Kaggle 的第一次踩坑,所以就没有再重新花时间训练该模型。

但网络的构建是正确的,希望尝试本文做法的读者能够顺利完成作业!