<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【NLP】保姆級教程:手把手帶你CNN文本分類(附代碼)

          共 13434字,需瀏覽 27分鐘

           ·

          2021-10-02 08:21


          分享一篇老文章,文本分類的原理和代碼詳解,非常適合NLP入門!

          寫在前面

          本文是對經典論文《Convolutional Neural Networks for Sentence Classification[1]》的詳細復現(xiàn),(應該是)基于TensorFlow 1.1以及python3.6。從數(shù)據(jù)預處理、模型搭建、模型訓練預測以及可視化一條龍講解,旨在為剛接觸該領域不知道如何下手搭建網絡的同學提供一個參考。廢話不說直接進入主題吧

          NLP中的CNN

          論文中是使用的CNN框架來實現(xiàn)對句子的分類,積極或者消極。當然這里我們首先必須對CNN有個大概的了解,可以參考我之前的這篇【Deep learning】卷積神經網絡CNN結構。目前主流來看,CNN主要是應用在computer vision領域,并且可以說由于CNN的出現(xiàn),使得CV的研究與應用都有了質的飛躍。

          目前對NLP的研究分析應用最多的就是RNN系列的框架,比如RNN,GRU,LSTM等等,再加上Attention,基本可以認為是NLP的標配套餐了。但是在文本分類問題上,相比于RNN,CNN的構建和訓練更為簡單和快速,并且效果也不差,所以仍然會有一些研究。

          那么,CNN到底是怎么應用到NLP上的呢?

          不同于CV輸入的圖像像素,NLP的輸入是一個個句子或者文檔。句子或文檔在輸入時經過embedding(word2vec或者Glove)會被表示成向量矩陣,其中每一行表示一個詞語,行的總數(shù)是句子的長度,列的總數(shù)就是維度。例如一個包含十個詞語的句子,使用了100維的embedding,最后我們就有一個輸入為10x100的矩陣。

          在CV中,filters是以一個patch(任意長度x任意寬度)的形式滑過遍歷整個圖像,但是在NLP中,filters會覆蓋到所有的維度,也就是形狀為 [filter_size, embed_size]。更為具體地理解可以看下圖,輸入為一個7x5的矩陣,filters的高度分別為2,3,4,寬度和輸入矩陣一樣為5。每個filter對輸入矩陣進行卷積操作得到中間特征,然后通過pooling提取最大值,最終得到一個包含6個值的特征向量。

          弄清楚了CNN的結構,下面就可以開始實現(xiàn)文本分類任務了。

          數(shù)據(jù)預處理

          原論文中使用了好幾個數(shù)據(jù)集,這里我們只選擇其中的一個——Movie Review Data from Rotten Tomatoes[2]。該數(shù)據(jù)集包括了10662個評論,其中一半positive一半negative。

          在數(shù)據(jù)處理階段,主要包括以下幾個部分:

          1、load file

          def load_data_and_labels(positive_file, negative_file):
          #load data from files
          positive_examples = list(open(positive_file, "r", encoding='utf-8').readlines())
          positive_examples = [s.strip() for s in positive_examples]
          negative_examples = list(open(negative_file, "r", encoding='utf-8').readlines())
          negative_examples = [s.strip() for s in negative_examples]
          # Split by words
          x_text = positive_examples + negative_examples
          x_text = [clean_str(sent) for sent in x_text]
          # Generate labels
          positive_labels = [[0, 1] for _ in positive_examples]
          negative_labels = [[1, 0] for _ in negative_examples]
          y = np.concatenate([positive_labels, negative_labels], 0)
          return [x_text, y]

          2、clean sentences

          def clean_str(string):
          string = re.sub(r"[^A-Za-z0-9(),!?\'\`]", " ", string)
          string = re.sub(r"\'s", " \'s", string)
          string = re.sub(r"\'ve", " \'ve", string)
          string = re.sub(r"n\'t", " n\'t", string)
          string = re.sub(r"\'re", " \'re", string)
          string = re.sub(r"\'d", " \'d", string)
          string = re.sub(r"\'ll", " \'ll", string)
          string = re.sub(r",", " , ", string)
          string = re.sub(r"!", " ! ", string)
          string = re.sub(r"\(", " \( ", string)
          string = re.sub(r"\)", " \) ", string)
          string = re.sub(r"\?", " \? ", string)
          string = re.sub(r"\s{2,}", " ", string)
          return string.strip().lower()

          模型實現(xiàn)

          論文中使用的模型如下所示其中第一層為embedding layer,用于把單詞映射到一組向量表示。接下去是一層卷積層,使用了多個filters,這里有3,4,5個單詞一次遍歷。接著是一層max-pooling layer得到了一列長特征向量,然后在dropout 之后使用softmax得出每一類的概率。

          在一個CNN類中實現(xiàn)上述模型

          class TextCNN(object):
          """
          A CNN class for sentence classification
          With a embedding layer + a convolutional, max-pooling and softmax layer
          """

          def __init__(self, sequence_length, num_classes, vocab_size,
          embedding_size, filter_sizes, num_filters, l2_reg_lambda=0.0)
          :

          """

          :param sequence_length: The length of our sentences
          :param num_classes: Number of classes in the output layer(pos and neg)
          :param vocab_size: The size of our vocabulary
          :param embedding_size: The dimensionality of our embeddings.
          :param filter_sizes: The number of words we want our convolutional filters to cover
          :param num_filters: The number of filters per filter size
          :param l2_reg_lambda: optional

          這里再注釋一下filter_sizes和num_filters。filters_sizes是指filter每次處理幾個單詞,num_filters是指每個尺寸的處理包含幾個filter。

          1. Input placeholder

          tf.placeholder是tensorflow的一種占位符,與feeed_dict同時使用。在訓練或者測試模型階段,我們可以通過feed_dict來喂入輸入變量。

          # set placeholders for variables
          self.input_x = tf.placeholder(tf.int32, [None, sequence_length], name='input_x')
          self.input_y = tf.placeholder(tf.float32, [None, num_classes], name='input_y')
          self.dropout_keep_prob = tf.placeholder(tf.float32, name='dropout_keep_prob')

          tf.placeholder函數(shù)第一個參數(shù)是變量類型,第二個參數(shù)是變量shape,其中None表示sample的個數(shù),第三個name參數(shù)用于指定名字。

          dropout_keep_prob變量是在dropout階段使用的,我們在訓練的時候選取50%的dropout,在測試時不使用dropout。

          2. Embedding layer

          我們需要定義的第一個層是embedding layer,用于將詞語轉變成為一組向量表示。

           # embedding layer
          with tf.name_scope('embedding'):
          self.W = tf.Variable(tf.random_uniform([vocab_size, embedding_size], -1.0, 1.0), name='weight')
          self.embedded_chars = tf.nn.embedding_lookup(self.W, self.input_x)
          # TensorFlow’s convolutional conv2d operation expects a 4-dimensional tensor
          # with dimensions corresponding to batch, width, height and channel.
          self.embedded_chars_expanded = tf.expand_dims(self.embedded_chars, -1)

          W 是在訓練過程中學習到的參數(shù)矩陣,然后通過tf.nn.embedding_lookup來查找到與input_x相對應的向量表示。tf.nn.embedding_lookup返回的結果是一個三維向量,[None, sequence_length, embedding_size]。但是后一層的卷積層要求輸入為四維向量(batch, width,height,channel)。所以我們要將結果擴展一個維度,才能符合下一層的輸入。

          3. Convolution and Max-Pooling Layers

          在卷積層中最重要的就是filter?;仡櫛疚牡牡谝粡垐D,我們一共有三種類型的filter,每種類型有兩個。我們需要迭代每個filter去處理輸入矩陣,將最終得到的所有結果合并為一個大的特征向量。

          # conv + max-pooling for each filter
          pooled_outputs = []
          for i, filter_size in enumerate(filter_sizes):
          with tf.name_scope('conv-maxpool-%s' % filter_size):
          # conv layer
          filter_shape = [filter_size, embedding_size, 1, num_filters]
          W = tf.Variable(tf.truncated_normal(filter_shape, stddev=0.1), name='W')
          b = tf.Variable(tf.constant(0.1, shape=[num_filters]), name='b')
          conv = tf.nn.conv2d(self.embedded_chars_expanded, W, strides=[1,1,1,1],
          padding='VALID', name='conv')
          # activation
          h = tf.nn.relu(tf.nn.bias_add(conv, b), name='relu')
          # max pooling
          pooled = tf.nn.max_pool(h, ksize=[1, sequence_length-filter_size + 1, 1, 1],
          strides=[1,1,1,1], padding='VALID', name='pool')
          pooled_outputs.append(pooled)


          # combine all the pooled fratures
          num_filters_total = num_filters * len(filter_sizes)
          self.h_pool = tf.concat(pooled_outputs, 3) # why 3?
          self.h_pool_flat = tf.reshape(self.h_pool, [-1, num_filters_total])

          這里W 就是filter矩陣, tf.nn.conv2d是tensorflow的卷積操作函數(shù),其中幾個參數(shù)包括

          • strides表示每一次filter滑動的距離,它總是一個四維向量,而且首位和末尾必定要是1,[1, width, height, 1]
          • padding有兩種取值:VALID和SAME。
            • VALID是指不在輸入矩陣周圍填充0,最后得到的output的尺寸小于input;
            • SAME是指在輸入矩陣周圍填充0,最后得到output的尺寸和input一樣;

          這里我們使用的是‘VALID’,所以output的尺寸為[1, sequence_length - filter_size + 1, 1, 1]。

          接下去是一層max-pooling,pooling比較好理解,就是選出其中最大的一個。經過這一層的output尺寸為 [batch_size, 1, 1, num_filters]。

          4. Dropout layer

          這個比較好理解,就是為了防止模型的過擬合,設置了一個神經元激活的概率。每次在dropout層設置一定概率使部分神經元失效, 每次失效的神經元都不一樣,所以也可以認為是一種bagging的效果。

          # dropout
          with tf.name_scope('dropout'):
          self.h_drop = tf.nn.dropout(self.h_pool_flat, self.dropout_keep_prob)

          5. Scores and Predictions

          我們可以通過對上述得到的特征進行運算得到每個分類的分數(shù)score,并且可以通過softmax將score轉化成概率分布,選取其中概率最大的一個作為最后的prediction

          #score and prediction
          with tf.name_scope("output"):
          W = tf.get_variable('W', shape=[num_filters_total, num_classes],
          initializer = tf.contrib.layers.xavier_initializer())
          b = tf.Variable(tf.constant(0.1, shape=[num_classes]), name='b')
          l2_loss += tf.nn.l2_loss(W)
          l2_loss += tf.nn.l2_loss(b)
          self.score = tf.nn.xw_plus_b(self.h_drop, W, b, name='scores')
          self.prediction = tf.argmax(self.score, 1, name='prediction')

          6. Loss and Accuracy

          通過score我們可以計算得出模型的loss,而我們訓練的目的就是最小化這個loss。對于分類問題,最常用的損失函數(shù)是cross-entropy 損失

           # mean cross-entropy loss
          with tf.name_scope('loss'):
          losses = tf.nn.softmax_cross_entropy_with_logits(logits=self.score, labels=self.input_y)
          self.loss = tf.reduce_mean(losses) + l2_reg_lambda * l2_loss

          為了在訓練過程中實時觀測訓練情況,我們可以定義一個準確率

          # accuracy
          with tf.name_scope('accuracy'):
          correct_predictions = tf.equal(self.prediction, tf.argmax(self.input_y, 1))
          self.accuracy = tf.reduce_mean(tf.cast(correct_predictions, 'float'), name='accuracy')

          到目前為止,我們的模型框架已經搭建完成,可以使用Tensorboardd來瞧一瞧到底是個啥樣

          模型訓練

          接下去我們就要開始使用影評數(shù)據(jù)來訓練網絡啦。

          創(chuàng)建圖和session

          對于Tensorflow有兩個重要的概念:Graph和Session。

          • Session會話可以理解為一個計算的環(huán)境,所有的operation只有在session中才能返回結果;
          • Graph圖就可以理解為上面那個圖片,在圖里面包含了所有要用到的操作operations和張量tensors。

          PS:在一個項目中可以使用多個graph,不過我們一般習慣只用一個就行。同時,在一個graph中可以有多個session,但是在一個session中不能有多個graph。

          with tf.Graph().as_default():
          session_conf = tf.ConfigProto(
          # allows TensorFlow to fall back on a device with a certain operation implemented
          allow_soft_placement= FLAGS.allow_soft_placement,
          # allows TensorFlow log on which devices (CPU or GPU) it places operations
          log_device_placement=FLAGS.log_device_placement
          )
          sess = tf.Session(config=session_conf)

          Initialize CNN

          cnn = TextCNN(sequence_length=x_train.shape[1],
          num_classes=y_train.shape[1],
          vocab_size= len(vocab_processor.vocabulary_),
          embedding_size=FLAGS.embedding_dim,
          filter_sizes= list(map(int, FLAGS.filter_sizes.split(','))),
          num_filters= FLAGS.num_filters,
          l2_reg_lambda= FLAGS.l2_reg_lambda)
          global_step = tf.Variable(0, name='global_step', trainable=False)
          optimizer = tf.train.AdamOptimizer(1e-3)
          grads_and_vars = optimizer.compute_gradients(cnn.loss)
          train_op = optimizer.apply_gradients(grads_and_vars, global_step=global_step)

          這里train_op的作用就是更新參數(shù),每運行一次train_op,global_step都會增加1。

          Summaries

          Tensorflow有一個特別實用的操作,summary,它可以記錄訓練時參數(shù)或者其他變量的變化情況并可視化到tensorboard。使用tf.summary.FileWriter()函數(shù)可以將summaries寫入到硬盤保存到本地。

          # visualise gradient
          grad_summaries = []
          for g, v in grads_and_vars:
          if g is not None:
          grad_hist_summary = tf.summary.histogram('{}/grad/hist'.format(v.name),g)
          sparsity_summary = tf.summary.scalar('{}/grad/sparsity'.format(v.name), tf.nn.zero_fraction(g))
          grad_summaries.append(grad_hist_summary)
          grad_summaries.append(sparsity_summary)
          grad_summaries_merged = tf.summary.merge(grad_summaries)

          # output dir for models and summaries
          timestamp = str(time.time())
          out_dir = os.path.abspath(os.path.join(os.path.curdir, 'run', timestamp))
          print('Writing to {} \n'.format(out_dir))

          # summaries for loss and accuracy
          loss_summary = tf.summary.scalar('loss', cnn.loss)
          accuracy_summary = tf.summary.scalar('accuracy', cnn.accuracy)

          # train summaries
          train_summary_op = tf.summary.merge([loss_summary, accuracy_summary])
          train_summary_dir = os.path.join(out_dir, 'summaries', 'train')
          train_summary_writer = tf.summary.FileWriter(train_summary_dir, sess.graph)

          # dev summaries
          dev_summary_op = tf.summary.merge([loss_summary, accuracy_summary])
          dev_summary_dir = os.path.join(out_dir, 'summaries', 'dev')
          dev_summary_writer = tf.summary.FileWriter(dev_summary_dir, sess.graph)

          Checkpointing

          checkpointing的作用就是可以保存每個階段訓練模型的參數(shù),然后我們可以根據(jù)準確率來選取最好的一組參數(shù)。

          checkpoint_dir = os.path.abspath(os.path.join(out_dir, 'checkpoints'))
          checkpoint_prefix = os.path.join(checkpoint_dir, 'model')
          if not os.path.exists(checkpoint_dir):
          os.makedirs(checkpoint_dir)
          saver = tf.train.Saver(tf.global_variables(), max_to_keep=FLAGS.num_checkpoints)

          Initializing the variables

          在開始訓練之前,我們通常會需要初始化所有的變量。一般使用 tf.global_variables_initializer()就可以了。

          Defining a single training step

          我們可以定義一個單步訓練的函數(shù),使用一個batch的數(shù)據(jù)來更新模型的參數(shù)

          def train_step(x_batch, y_batch):
          """
          A single training step
          :param x_batch:
          :param y_batch:
          :return:
          """

          feed_dict = {
          cnn.input_x: x_batch,
          cnn.input_y: y_batch,
          cnn.dropout_keep_prob: FLAGS.dropout_keep_prob
          }
          _, step, summaries, loss, accuracy = sess.run(
          [train_op, global_step, train_summary_op, cnn.loss, cnn.accuracy],
          feed_dict=feed_dict
          )
          time_str = datetime.datetime.now().isoformat()
          print("{}: step {}, loss {:g}, acc {:g}".format(time_str, step, loss, accuracy))
          train_summary_writer.add_summary(summaries, step)

          這里的feed_dict就是我們前面提到的同placeholder一起使用的。必須在feed_dict中給出所有placeholder節(jié)點的值,否則程序就會報錯。

          接著使用sess.run()運行前面定義的操作,最終可以得到每一步的損失、準確率這些信息。

          類似地我們定義一個函數(shù)在驗證集數(shù)據(jù)上看看模型的準確率等

          def dev_step(x_batch, y_batch, writer=None):
          """
          Evaluate model on a dev set
          Disable dropout
          :param x_batch:
          :param y_batch:
          :param writer:
          :return:
          """

          feed_dict = {
          cnn.input_x: x_batch,
          cnn.input_y: y_batch,
          cnn.dropout_keep_prob: 1.0
          }
          step, summaries, loss, accuracy = sess.run(
          [global_step, dev_summary_op, cnn.loss, cnn.accuracy],
          feed_dict=feed_dict
          )
          time_str = datetime.datetime.now().isoformat()
          print("{}: step {}, loss {:g}, acc {:g}".format(time_str, step, loss, accuracy))
          if writer:
          writer.add_summary(summaries, step)

          Training loop

          前面都定義好了以后就可以開始我們的訓練了。我們每次調用train_step函數(shù)批量的訓練數(shù)據(jù)并保存:

          # generate batches
          batches = data_process.batch_iter(list(zip(x_train, y_train)), FLAGS.batch_size, FLAGS.num_epochs)
          # training loop
          for batch in batches:
          x_batch, y_batch = zip(*batch)
          train_step(x_batch, y_batch)
          current_step = tf.train.global_step(sess, global_step)
          if current_step % FLAGS.evaluate_every == 0:
          print('\n Evaluation:')
          dev_step(x_dev, y_dev, writer=dev_summary_writer)
          print('')
          if current_step % FLAGS.checkpoint_every == 0:
          path = saver.save(sess, checkpoint_prefix, global_step=current_step)
          print('Save model checkpoint to {} \n'.format(path))

          最后輸出的效果大概是這樣的

          Visualizing Results

          我們可以在代碼目錄下打開終端輸入以下代碼來啟動瀏覽器的tensorboard:

          tensorboard --logdir /runs/xxxxxx/summaries

          小結

          當然這只是一個利用CNN進行NLP分類任務(文本分類,情感分析等)的baseline,可以看出準確率并不是很高,后續(xù)還有很多可以優(yōu)化的地方,包括使用pre-trained的Word2vec向量、加上L2正則化等等。

          完整代碼:

          https://github.com/KaiyuanGao/text_claasification/tree/master/cnn_classification


          往期精彩回顧




          本站qq群851320808,加入微信群請掃碼:
          瀏覽 38
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  中文字幕乱码在线 | 五月色婷婷中文字幕在线 | 菠萝视频一区二区 | 欧美黄A片免费视频www | 成人在线18av |