[[194511]] 序文 テキスト分類は、自動記事分類、自動メール分類、スパム識別、ユーザー感情分類など、自然言語処理における最も一般的なアプリケーションの 1 つです。実生活には多くの例があります。この記事では、主に従来の学習とディープラーニングの 2 つの側面からテキスト分類子を作成する方法について説明します。 テキスト分類方法 従来のテキスト手法の主なプロセスは、いくつかの特徴を手動で設計し、元の文書から特徴を抽出し、LR や SVM などの分類器を指定してモデルをトレーニングし、記事を分類することです。従来の特徴抽出方法には、頻度法、tf-idf、相互情報量法、N-Gram などがあります。 ディープラーニングが普及した後、多くの人が CNN や LSTM などの古典的なモデルを使用して特徴を抽出し始めました。この記事では、テキスト分類におけるいくつかの実験について簡単に説明します。 従来のテキスト分類方法 ここでは主に、頻度法、tf-idf、相互情報量、N-Gram という 2 つの特徴抽出方法について説明します。 頻度法 頻度法は、その名の通り、非常にシンプルです。各記事の頻度分布を記録し、その分布を機械学習モデルに入力して、このタイプのデータを分類するのに適した分類モデルをトレーニングします。頻度分布を数える場合、比較的頻度の低い単語は記事の分類にほとんど影響を与えないと想定するのが妥当です。したがって、閾値を合理的に想定し、閾値未満の頻度の単語を除外し、特徴空間の次元を減らすことができます。 TF-IDF 頻度法と比較して、TF-IDF にはさらに考慮すべき点があります。単語の出現回数は、ある程度記事の特徴を反映できます。つまり、TF であり、TF-IDF はいわゆる逆文書頻度を追加します。単語が特定のカテゴリでより多く出現し、テキスト全体では比較的少ない回数出現する場合、この単語は文書を区別する能力が強いと考えられます。TF-IDF は、頻度と逆文書頻度の 2 つの要素を総合的に考慮します。 相互情報量法 相互情報量法は、文書に出現する単語と文書カテゴリの相関関係、つまり相互情報量を計算する統計手法でもある。 Nグラム N-Gram ベースの方法は、サイズ N のウィンドウを通じて記事シーケンスのグループを形成し、これらのグループをカウントし、頻度の低いグループを除外し、これらのグループを特徴空間に形成し、分類のために分類器に渡すというものです。 ディープラーニング手法 CNNに基づくテキスト分類法 - 最も一般的な CNN ベースの方法は、Keras の例を使用して感情分析を実行することです。Conv1D に接続し、記事をトラバースするためのウィンドウ サイズを指定し、maxpool を追加し、それらをいくつか接続して特徴表現を取得し、最終的な分類出力に FC を追加します。
- 最も有名な CNN ベースのテキスト分類方法は、2014 Emnlp の「Convolutional Neural Networks for Sentence Classification」でしょう。これは、さまざまなフィルターを備えた CNN ネットワークを使用し、maxpool を追加してそれらを連結します。
- このタイプの CNN 方式は、異なるウィンドウ サイズを設計することで、異なるスケールの関係をモデル化しますが、コンテキスト関係のほとんどが失われることは明らかです。テキスト分類用の再帰型畳み込みニューラル ネットワークは、各単語のベクトル表現を形成するときに、前後のコンテキストの情報を追加します。各単語の表現は次のとおりです。
全体の構造フレームワークは次のとおりです。 - たとえば、「サウス バンク沿いの夕暮れの散歩には、見事な眺望ポイントが数多くあります」という文の場合、散歩の表現には c_l(stroll)、pre_word2vec(stroll)、c_r(stroll) が含まれます。c_l(stroll) は夕暮れの意味をエンコードし、cr_r(stroll) はサウス バンク沿いの夕暮れには、見事な眺望ポイントが数多くあるという情報をエンコードします。各単語はこのように処理されるため、通常の CNN 手法でコンテキスト情報が欠落するのを回避できます。
LSTMベースの方法 - CNN に基づく最初の方法と同様に、LSTM は埋め込み後に直接かつ激しく追加され、分類のために FC に出力されます。LSTM ベースの方法も特徴抽出方法であり、時系列の特性をよりモデル化する傾向があると思います。
- ブルートフォース法に加えて、A C-LSTM Neural Network for Text Classificationでは、埋め込み出力をLSTMに直接接続するのではなく、CNNに接続し、CNNを通じていくつかのシーケンスを取得し、これらのシーケンスをLSTMに接続します。そうすることで分類の精度が向上すると記事では述べています。
コーディング練習 コーパスとタスクの紹介 トレーニング コーパスは、約 31 のニュース カテゴリのニュース コーパスから取得されますが、ニュースの数が比較的少ないカテゴリもあるため、比較的ニュースの数が多い上位 20 のニュース アナロジーのニュース コーパスを使用しました。各ニュース リリースの単語数は数百から数千に及びます。タスクは、適切な分類器をトレーニングし、ニュースをさまざまなカテゴリに分類することです。 弓 Bow はコーパスを処理してトークン セットを取得します。 - __get_all_tokens(自分自身)を定義します:
- "" "コーパスのすべてのトークンを取得します
- 「」 「 」
- fwrite = open ( self.data_path.replace ( "all.csv" 、 "all_token.csv" )、 'w' )
- と open ( self.data_path, "r" ) をfreadとして実行します:
- 私 = 0
- #真の場合:
- fread.readlines()の行の場合:
- 試す:
- line_list = line.strip().split( "\t" )
- ラベル = 行リスト[0]
- self.labels.append(ラベル)
- テキスト = 行リスト[1]
- text_tokens = self.cut_doc_obj.run(テキスト)
- self.corpus.append( ' ' . join (text_tokens)) を追加
- self.dictionary.add_documents([テキストトークン])
- fwrite.write(ラベル+ "\t" + "\\".join(text_tokens)+" \n")
- 私+=1
- BaseExceptionを除き、 e:
- メッセージ = トレースバック.format_exc()
- メッセージを印刷
- print "=====>読み取り完了<======"
- 壊す
- self.token_len = self.dictionary.__len__()
- 「すべてのトークンの長さ」 + str(self.token_len)を出力します。
- 自己.num_data = i
- fwrite.close () 関数
次に、トークン セットは頻度しきい値によってフィルタリングされ、各記事はベクトル化のために処理されます。 - def __filter_tokens(self, しきい値番号=10):
- small_freq_ids = [tokenidの場合はtokenid 、 docfreq の場合は self.dictionary.dfs.items()内のdocfreq (threshold_num 未満)]
- self.dictionary.filter_tokens(小さい頻度ID)
- 自己辞書をコンパクト化します()
-
- 定義vec(self):
- "" " vec:弓のvec表現を取得します
- 「」 「 」
- 自己.__get_all_tokens()
- 「フィルター前のトークンの長さ: {0}」を出力します。.format(self.dictionary.__len__())
- 自己.__filter_tokens()
- print "フィルター後のトークンの長さ: {0}" .format(self.dictionary.__len__())
- 自己弓 = []
- self.corpusのfile_tokenの場合:
- file_bow = self.dictionary.doc2bow(file_token)
- 自己.bow.append(file_bow)
- # 弓のベクトルをファイルに書き込む
- bow_vec_file = open ( self.data_path.replace ( "all.csv" 、 "bow_vec.pl" 、 'wb' ) )
- pickle.dump(self.bow,bow_vec_file)
- bow_vec_file.close ( ) 関数
- bow_label_file = open ( self.data_path.replace ( "all.csv" 、 "bow_label.pl" 、 'wb' ) )
- pickle.dump(self.labels,bow_label_file)
- bow_label_file.close ( ) 関数
最後に、各記事の弓ベクトルを取得します。このコードはラップトップで実行されるため、直接実行するとメモリを大量に消費します。トークン セット内の各記事の表現は非常にまばらであるため、CSR 表現に変換してからモデルをトレーニングし、CSR に変換して、中間結果コードを次のように保存することができます。 - to_csr(self)を定義します。
- self.bow = pickle.load ( open ( self.data_path.replace ( " all.csv " 、 "bow_vec.pl" 、 'rb' ))
- self.labels = pickle.load ( open ( self.data_path.replace ( "all.csv" 、 "bow_label.pl" 、 'rb' ) )
- データ = []
- 行数= []
- 列 = []
- 行数 = 0
- self.bow内の行の場合:
- 行の要素について:
- 行.append(行数)
- cols.append(要素[0])
- データを追加します(要素[1])
- 行数 += 1
- 「辞書の形状 ({0},{1})」を印刷します。.format(line_count, self.dictionary.__len__())
- bow_sparse_matrix = csr_matrix((データ、(行、列))、形状=[行数、self.dictionary.__len__()])
- 「bow_sparse 行列の形状:」を印刷します
- bow_sparse_matrix.shape を印刷する
- # rarray = np.random.random(サイズ= 行数)
- self.train_set、self.test_set、self.train_tag、self.test_tag = train_test_split(bow_sparse_matrix、self.labels、test_size=0.2)
- 「列車セットの形状:」を印刷します
- self.train_set.shape を印刷する
- train_set_file = open ( self.data_path.replace ( "all.csv" 、 "bow_train_set.pl" 、 'wb' ) )
- pickle.dump(self.train_set、train_set_file) をダンプします。
- train_tag_file = open ( self.data_path.replace ( "all.csv" 、 "bow_train_tag.pl" 、 'wb' ) )
- pickle.dump(self.train_tag,train_tag_file)
- test_set_file = open ( self.data_path.replace ( "all.csv" 、 "bow_test_set.pl" )、 'wb' )
- pickle.dump(自己テストセット、テストセットファイル)
- test_tag_file = open ( self.data_path.replace ( "all.csv" 、 "bow_test_tag.pl" 、 'wb' ) )
- pickle.dump(自己.テストタグ、テストタグファイル)
***トレーニング モデル コードは次のとおりです。 - def train(self):
- 「モデルのトレーニングを開始する」を印刷する
- lr_model = ロジスティック回帰()
- lr_model.fit(self.train_set、self.train_tag) を使います。
- 「今すぐ終了し、テストデータセットでモデルを評価します」と印刷します
- # 「平均精度: {0}」を出力します。.format(lr_model.score(self.test_set, self.test_tag))
- y_pred = lr_model.predict(自己テストセット)
- 分類レポートを印刷します(self.test_tag, y_pred)
- 混乱行列を印刷します(self.test_tag, y_pred)
- print "トレーニングしたモデルを lr_model.pl に保存します"
- joblib.dump(lr_model、self.data_path.replace ( "all.csv" 、 "bow_lr_model.pl" ) )
TF-IDF TF-IDF と Bow は、ベクトル化に tf-idf メソッドが使用される点を除いて、非常によく似た動作をします。 - 定義vec(self):
- "" " vec:弓のvec表現を取得します
- 「」 「 」
- 自己.__get_all_tokens()
- 「フィルター前のトークンの長さ: {0}」を出力します。.format(self.dictionary.__len__())
- ベクターライザー = CountVectorizer(min_df=1e-5)
- トランスフォーマー = TfidfTransformer()
- # 疎行列
- self.tfidf = transformer.fit_transform(vectorizer.fit_transform(self.corpus))
- 単語 = vectorizer.get_feature_names()
- 「単語の長さ: {0}」を出力します。.format(len(words))
- # self.tfidf[0] を印刷する
- 「tfidf シェイプ ({0},{1})」を印刷します。.format(self.tfidf.shape[0], self.tfidf.shape[1])
-
- # tfidf vec をファイルに書き込む
- tfidf_vec_file = open ( self.data_path.replace ( "all.csv" 、 "tfidf_vec.pl" 、 'wb' )
- pickle.dump(self.tfidf、tfidf_vec_file) をダンプします。
- tfidf_vec_file.close ()関数
- tfidf_label_file = open ( self.data_path.replace ( "all.csv" 、 "tfidf_label.pl" 、 'wb' )
- pickle.dump(self.labels,tfidf_label_file)
- tfidf_label_file.close ()関数
どちらの方法もうまく機能し、98% 以上の精度を達成できます。 CNN コーパス処理の方法は従来のものと似ています。単語分割後、事前学習済みの word2vec を使用します。ここで落とし穴に遭遇しました。最初は単語分割に自信がありすぎて、*** モデルが収束できませんでした。その後、グループの博士課程の学生に相談しました。単語分割の単語シーケンスの多くは、事前学習済みの word2vec に存在しない可能性が高いため、この部分を直接破棄しました。考えられるすべての問題、単語分割に辞書を追加し、事前学習済みの word2vec に存在しない単語に対してランダム初期化を行ったところ、収束することができました。学習中!!! word2vec モデルをロードして CNN ネットワークを構築するためのコードは次のとおりです (いくつかの BN およびドロップアウト メソッドが追加されています)。 - gen_embedding_matrix を定義します(self、load4file = True ):
- "" " gen_embedding_matrix: 埋め込み行列を生成する
- 「」 「 」
- load4fileの場合:
- 自己.__get_all_tokens_v2()
- それ以外:
- 自己.__get_all_tokens()
- print "フィルター前、トークンの長さ: {0}" .format(
- 自己辞書.__len__())
- 自己.__filter_tokens()
- print "フィルター後、トークンの長さ: {0}" .format(
- 自己辞書.__len__())
- 自己シーケンス= []
- self.corpusのfile_tokenの場合:
- temp_sequence = [xはx、yはself.dictionary.doc2bow(file_token)]
- temp_sequence を印刷する
- 自己シーケンスの追加(temp_sequence)
-
- self.corpus_size = len(self.dictionary.token2id)
- self.embedding_matrix = np.zeros((self.corpus_size, EMBEDDING_DIM))
- 「コーパスサイズ: {0}」を出力します。.format(len(self.dictionary.token2id))
- のために self.dictionary.token2id.items()のキー、v :
- key_vec = self.w2vec.get(キー)
- key_vecが なしではない:
- 自己埋め込み行列[v] = キーベクトル
- それ以外:
- 自己埋め込み行列[v] = np.random.rand(EMBEDDING_DIM) - 0.5
- "embedding_matrix len {0}"を出力します。.format(len(self.embedding_matrix))
-
- def __build_network(自己):
- 埋め込みレイヤー = 埋め込み(
- 自己.corpus_size、
- 埋め込み寸法、
- 重み=[self.embedding_matrix],
- 入力長さ=MAX_SEQUENCE_LENGTH、
- 訓練可能 = False )
- # 1Dの畳み込みネットワークを訓練する グローバル最大プーリング
- シーケンス入力 = 入力(shape=(MAX_SEQUENCE_LENGTH, ), dtype= 'int32' )
- 埋め込みシーケンス = 埋め込みレイヤー(シーケンス入力)
- x = 畳み込み1D(128, 5)(埋め込みシーケンス)
- x = バッチ正規化()(x)
- x = アクティベーション( 'relu' )(x)
- x = MaxPooling1D(5)(x)
- x = 畳み込み1D(128, 5)(x)
- x = バッチ正規化()(x)
- x = アクティベーション( 'relu' )(x)
- x = MaxPooling1D(5)(x)
- 「256より前」を印刷、x.get_shape()
- x = 畳み込み1D(128, 5)(x)
- x = バッチ正規化()(x)
- x = アクティベーション( 'relu' )(x)
- x = 最大プーリング1D(15)(x)
- x = フラット化()(x)
-
- x = 密(128)(x)
- x = バッチ正規化()(x)
- x = アクティベーション( 'relu' )(x)
- x = ドロップアウト(0.5)(x)
- x.get_shape() を印刷する
- preds = Dense(self.class_num, activation= 'softmax' )(x)
- preds.get_shape() を印刷する
- アダム = アダム(lr=0.0001)
- self.model = モデル(シーケンス入力、予測値)
- 自己.モデル.コンパイル(
- 損失= 'categorical_crossentropy' 、オプティマイザー=adam、メトリック=[ 'acc' ])
別のネットワーク構造、韓国の記事では、ネットワーク構造は次のとおりです。 - def __build_network(自己):
- 埋め込みレイヤー = 埋め込み(
- 自己.corpus_size、
- 埋め込み寸法、
- 重み=[self.embedding_matrix],
- 入力長さ=MAX_SEQUENCE_LENGTH、
- 訓練可能 = False )
- # 1Dの畳み込みネットワークを訓練する グローバル最大プーリング
- シーケンス入力 = 入力(shape=(MAX_SEQUENCE_LENGTH, ), dtype= 'int32' )
- 埋め込みシーケンス = 埋め込みレイヤー(シーケンス入力)
- 変換ブロック = []
- self.filter_sizesのszの場合:
- 畳み込み1D(
- 自己.num_filters、
- sz、
- アクティベーション = "relu" 、
- パディング = '有効' 、
- ストライド=1)(埋め込みシーケンス)
- 変換 = MaxPooling1D(2)(変換)
- conv = フラット化()(conv)
- conv_blocks.append(conv)
- z = マージ(
- 変換ブロック、
- mode = 'concat' ) len(conv_blocks) > 1 の場合、それ以外の場合はconv_blocks[0]
- z = ドロップアウト(0.5)(z)
- z = Dense(self.hidden_dims, activation= "relu" )(z)
- preds = Dense(self.class_num, activation= "softmax" )(z)
- rmsprop = RMSprop(lr=0.001)
- self.model = モデル(シーケンス入力、予測値)
- 自己.モデル.コンパイル(
- 損失 = 'カテゴリクロスエントロピー' 、
- オプティマイザー=rmsprop、
- メトリック=[ 'acc' ])
LSTM 私のタスクは記事を分類することなので、シーケンスが長すぎて、LSTM に接続するとすぐにメモリが爆発してしまいます。そのため、Conv1D + MaxPool1D の 2 つのレイヤーを直接接続して、低次元のベクトル表現を抽出し、LSTM に接続しました。ネットワーク構造コードは次のとおりです。 - def __build_network(自己):
- 埋め込みレイヤー = 埋め込み(
- 自己.corpus_size、
- 埋め込み寸法、
- 重み=[self.embedding_matrix],
- 入力長さ=MAX_SEQUENCE_LENGTH、
- 訓練可能 = False )
- # 1Dの畳み込みネットワークを訓練する グローバル最大プーリング
- シーケンス入力 = 入力(shape=(MAX_SEQUENCE_LENGTH, ), dtype= 'int32' )
- 埋め込みシーケンス = 埋め込みレイヤー(シーケンス入力)
- x = 畳み込み1D(
- self.num_filters、5、アクティベーション = "relu" )(埋め込みシーケンス)
- x = MaxPooling1D(5)(x)
- x = Convolution1D(self.num_filters, 5, activation= "relu" )(x)
- x = MaxPooling1D(5)(x)
- x = LSTM(64、ドロップアウトW=0.2、ドロップアウトU=0.2)(x)
- preds = Dense(self.class_num, activation= 'softmax' )(x)
- preds.get_shape() を印刷する
- rmsprop = RMSprop(lr=0.01)
- self.model = モデル(シーケンス入力、予測値)
- 自己.モデル.コンパイル(
- 損失 = 'カテゴリクロスエントロピー' 、
- オプティマイザー=rmsprop、
- メトリック=[ 'acc' ])
CNNの結果: C-LSTM の結果: 実験全体の結果は次のとおりです。ディープラーニング部分は会社のリソースで実行されたため、パフォーマンスを向上させるためにパラメータを調整するための実際のトリックは使用されていません。ここでのコード内のすべてのネットワーク構成とパラメータは参考用です。より深い作業には、パラメータを最適化するためにより多くの時間が必要になります。 PS: ここでkeras1.2.2のバグが見つかりました。コールバック関数TensorBoardを記述する際、histogram_freq=1のとき、グラフィックカードの使用量が著しく増加し、M40の24gでは足りませんでした。個人的にはバグであるべきだと思いますが、2.0ではなく1.2.2であることを考えると、2.0は後で最適化されるかもしれません。 すべてのコードはgithubにあります: tensorflow-101/nlp/text_classifier/scripts 要約と展望 この記事の実験結果では、ディープラーニングベースの方法が従来の方法に比べて優れている点はないものの、いくつかの理由が考えられます。 - 事前学習済みの Word2vec モデルはニュースから分割された単語をカバーしておらず、その割合はかなり高いです。オンラインニュースコーパスを使用して、より正確な事前学習済みの Word2vec を学習できれば、効果は大幅に向上するはずです。
- モデルトレーニングの収束トリックとオプティマイザーを追加して、精度が向上するかどうかを確認できます。
- これまでのところ、ネットワーク モデルのパラメータは十分に最適化されていません。
|