1.コンセプト
1.1、リカレントニューラルネットワーク
再帰型ニューラルネットワーク(RNN)は、入力としてシーケンスデータを取り、シーケンスの展開方向に再帰を実行し、すべてのノード(再帰ユニット)がチェーンで接続されている一種の再帰型ニューラルネットワークです。
畳み込みネットワークの入力は入力データXのみであり、入力データXに加えて、再帰型ニューラルネットワークの各ステップの出力が次のステップの入力として使用されます。このサイクルでは、同じアクティベーション関数とパラメーターが毎回使用されます。各サイクルで、x0に係数Uを乗算してs0を取得し、次に係数Wを介して次回に入力します。これにより、リカレントニューラルネットワークの順方向伝播が形成されます。
逆伝播では、損失関数EのパラメーターWへの導関数が必要であり、右下の式は連鎖導関数ルールによって取得できます。
リカレントニューラルネットワークは、畳み込みニューラルネットワークと比較されます。畳み込みニューラルネットワークは、ネットワークを介して出力を生成する出力です。リカレントニューラルネットワークは、1入力複数出力(画像説明の生成)、複数入力1出力(テキスト分類)、複数入力複数出力(機械翻訳、ビデオ解説)を実現できます。
RNNは日焼け活性化関数を使用し、出力は-1から1の間で、勾配は簡単に消えます。出力から離れたステップは、勾配にほとんど寄与しません。
最下層の出力は上層の入力として使用され、多層RNNネットワークを形成します。また、上層もその間を通過でき、残りの接続を使用して過適合を防止できます。
1.2長期および短期記憶ネットワーク
RNNの各伝播の間にパラメーターWは1つしかありません。このパラメーターを使用して多数の複雑な情報要件を記述することは困難です。この問題を解決するために、Long Short Term Memory(LSTM)が導入されました。このネットワークは、選択的なメカニズムを実行し、使用する必要のある情報を選択的に入力および出力し、不要な情報を選択的に忘れることができます。選択メカニズムの実現はシグモイドゲートによって実現されます。シグモイド関数の出力は0と1の間で、0は忘却を表し、1は記憶を表し、0.5は記憶を表します50%
LSTMネットワーク構造を以下に示します。
上の右の図に示すように、これは現在の操作ラウンドの暗黙的な状態です。現在の状態は、前の状態と忘却ゲートの結果のドット積に加えて、
次の図は、忘却ゲートの構造を示しています。前のラウンドの出力ht-1とデータxtは、忘却ゲートを通過して、忘却するかどうかを選択し、忘却結果が生成されます。
次の図は入力ゲートの構造を示しています。忘却ゲートの後のht-1とxtの結果はそれであり、tanhの結果Ctは内積演算を受けてこの演算の入力を取得します。
次の図は、出力ゲートの構造を示しています。ht-1とxtがforgetゲートを通過した結果はotで、現在の状態はこの積を生成するための内積です。
次のようにLSTMネットワークを実装するには、最初に_generate_params関数を定義して各ゲートに必要なパラメーターを生成し、この関数を呼び出して入力ゲート、出力ゲート、忘却ゲート、および中間状態tanhのパラメーターを定義します。各ゲートのパラメータは3つです。xとhの重みとオフセット値を入力します。
次に、LSTMループ計算の各ラウンドを開始します。入力ゲート計算では、入力embedded_input行列に入力ゲートパラメーターx_inを乗算し、さらにhと対応するパラメーターを乗算した結果を加え、最後にオフセット値b_inを追加してシグモイドを介して入力を取得します。ドアの結果。
同様に、行列乗算とオフセット演算が実行されて、忘却ゲートと出力ゲートの結果が得られます。中間状態のtanhは、最終的にtanh関数を通過することを除いて、3つのゲートの動作に似ています。
最後の隠し状態にforgetゲートと入力ゲートを加えたものに中間状態を掛けて、現在の隠し状態を取得します
現在の状態をtanh関数に渡し、出力ゲートを追加して現在の出力hを取得します。
複数回の入力サイクルの後に得られるのは、LSTMネットワークの最終出力です。
# 实现LSTM网络
# 生成Cell网格所需参数
def _generate_paramas(x_size, h_size, b_size):
x_w = tf.get_variable('x_weight', x_size)
h_w = tf.get_variable('h_weight', h_size)
bias = tf.get_variable('bias', b_size, initializer=tf.constant_initializer(0.0))
return x_w, h_w, bias
scale = 1.0 / math.sqrt(embedding_size + lstm_nodes[-1]) / 3.0
lstm_init = tf.random_uniform_initializer(-scale, scale)
with tf.variable_scope('lstm_nn', initializer=lstm_init):
# 输入门参数
with tf.variable_scope('input'):
x_in, h_in, b_in = _generate_paramas(
x_size=[embedding_size, lstm_nodes[0]],
h_size=[lstm_nodes[0], lstm_nodes[0]],
b_size=[1, lstm_nodes[0]]
)
# 输出门参数
with tf.variable_scope('output'):
x_out, h_out, b_out = _generate_paramas(
x_size=[embedding_size, lstm_nodes[0]],
h_size=[lstm_nodes[0], lstm_nodes[0]],
b_size=[1, lstm_nodes[0]]
)
# 遗忘门参数
with tf.variable_scope('forget'):
x_f, h_f, b_f = _generate_paramas(
x_size=[embedding_size, lstm_nodes[0]],
h_size=[lstm_nodes[0], lstm_nodes[0]],
b_size=[1, lstm_nodes[0]]
)
# 中间状态参数
with tf.variable_scope('mid_state'):
x_m, h_m, b_m = _generate_paramas(
x_size=[embedding_size, lstm_nodes[0]],
h_size=[lstm_nodes[0], lstm_nodes[0]],
b_size=[1, lstm_nodes[0]]
)
# 两个初始化状态,隐含状态state和初始输入h
state = tf.Variable(tf.zeros([batch_size, lstm_nodes[0]]), trainable=False)
h = tf.Variable(tf.zeros([batch_size, lstm_nodes[0]]), trainable=False)
# 遍历LSTM每轮循环,即每个词的输入过程
for i in range(max_words):
# 取出每轮输入,三维数组embedd_inputs的第二维代表训练的轮数
embedded_input = embedded_inputs[:, i, :]
# 将取出的结果reshape为二维
embedded_input = tf.reshape(embedded_input, [batch_size, embedding_size])
# 遗忘门计算
forget_gate = tf.sigmoid(tf.matmul(embedded_input, x_f) + tf.matmul(h, h_f) + b_f)
# 输入门计算
input_gate = tf.sigmoid(tf.matmul(embedded_input, x_in) + tf.matmul(h, h_in) + b_in)
# 输出门
output_gate = tf.sigmoid(tf.matmul(embedded_input, x_out) + tf.matmul(h, h_out) + b_out)
# 中间状态
mid_state = tf.tanh(tf.matmul(embedded_input, x_m) + tf.matmul(h, h_m) + b_m)
# 计算隐含状态state和输入h
state = state * forget_gate + input_gate * mid_state
h = output_gate + tf.tanh(state)
# 最后遍历的结果就是LSTM的输出
last_output = h
1.3、テキスト分類
テキスト分類問題は、入力テキスト文字列を分析して判断し、結果を出力することです。文字列を直接RNNネットワークに入力することはできないため、入力前にテキストを単一のフレーズに分割し、フレーズをベクトルにエンコードして、毎ラウンドフレーズを入力する必要があります。最後のフレーズが入力されると、出力結果もベクトル。埋め込みは単語をベクトルに対応させ、ベクトルの各次元は浮動小数点値に対応します。これらの浮動小数点値は動的に調整され、埋め込みコードが単語の意味に関連付けられます。このように、ネットワークの入力と出力はすべてベクトルであり、最終的な完全接続操作はさまざまな分類に対応できます。
RNNネットワークが必然的にもたらす問題は、最終出力が最新の入力によって影響を受け、さらに遠くにある入力が結果に影響を与えない可能性があることです。これは情報のボトルネック問題です。この問題を解決するために、双方向LSTMが導入されています。双方向LSTMは、情報の逆伝播を増加させるだけでなく、各ラウンドに出力があり、それらは結合されて、完全に接続されたレイヤーに送信されます。
別のテキスト分類モデルはHAN(階層アテンションネットワーク)です。最初に、テキストが文と単語レベルに分割されます。入力された単語がエンコードされて追加され、文コードが取得されます。次に、文コードが追加され、最終的なテキストコードが取得されます。注意とは、コードの各レベルが累積される前に加重値を追加し、異なる加重に従ってコードを累積することを指します。
入力テキストの長さが均一ではないため、ニューラルネットワークを直接学習に使用することはできません。この問題を解決するために、入力テキストの長さを最大値に統一でき、畳み込みニューラルネットワークはほとんど学習に使用されません。つまり、TextCNNです。テキストコンボリューションネットワークのコンボリューションプロセスは、マルチチャネルの1次元コンボリューションを使用します。2次元コンボリューションと比較すると、1次元コンボリューションは、コンボリューションカーネルが1方向にのみ移動することを意味します。たとえば、左の図に示すように、1×1 + 5×2 + 2×2 + 4×3 + 3×3 + 3×4 = 48となり、たたみ込みカーネルは1グリッド下に移動して45になります。下の右図に示すように、長さが異なる複数の単語を入力します。最初にそれらすべてを6チャネルの埋め込み配列に入力し、次に6チャネルの1次元畳み込みカーネルを使用して上から下に畳み込み、1次元配列を取得します。次に、プーリングレイヤーと完全に接続されたレイヤーを通過した後に出力します。
CNNネットワークは、異なる入力長のシリアル問題を完全に処理することはできませんが、複数のフレーズを並行して処理できるため、より効率的であり、RNNは、2つの利点を組み合わせてシリアル入力をより適切に処理できます。R-CNNモデルを構成します。最初に、双方向RNNネットワークを介して入力に対して特徴抽出が実行され、次にCNNを使用してさらに抽出されます。次に、各ステップの特徴がプーリングレイヤーを通じて融合され、最後に完全に接続されたレイヤーを通じて分類されます。
埋め込みを使用して入力をベクトルに変換する必要があるモデルに関係なく、入力が大きすぎると、変換された埋め込みレイヤーのパラメーターが大きすぎて、ストレージにつながるだけでなく、オーバーフィッティングが発生するため、埋め込みレイヤーを圧縮する必要があります。元の埋め込みコードは入力に対応するパラメーターです。たとえば、waitはパラメーターx1に対応し、forはx2に対応し、はx3に対応します。入力が多すぎると、エンコードパラメータが非常に大きくなります。2つのパラメータペアを使用して入力をエンコードできます。たとえば、待機は(x1、x2)に対応し、forは(x1、x3)...に対応するため、最大化できます。保存パラメータの数は共有圧縮です。
2.テキストRNNによるテキスト分類
2.1、データの前処理
インターネットにダウンロードされたテキスト分類データセットファイルは次のとおりです。テストセットとトレーニングセットデータに分けられます。各トレーニングセットの下に4つのフォルダーがあり、各フォルダーは分類です。各分類には1000のtxtファイルがあり、それぞれファイルに分類のテキストがあります
os.walkを介してすべてのトレーニングセットファイルを反復処理し、分類されたテキストを、スペースで区切られたjiebaライブラリを介して単一のフレーズに分割します。次に、分類テキストをタブで区切って最初に追加し、最後に結果をtrain_segment.txtに出力します。
# 将文件中的句子通过jieba库拆分为单个词
def segment_word(input_file, output_file):
# 循环遍历训练数据集的每一个文件
for root, folders, files in os.walk(input_file):
print('root:', root)
for folder in folders:
print('dir:', folder)
for file in files:
file_dir = os.path.join(root, file)
with open(file_dir, 'rb') as in_file:
# 读取文件中的文本
sentence = in_file.read()
# 通过jieba函数库将句子拆分为单个词组
words = jieba.cut(sentence)
# 文件夹路径最后两个字即为分类名
content = root[-2:] + '\t'
# 去除词组中的空格,排除为空的词组
for word in words:
word = word.strip(' ')
if word != '':
content += word + ' '
# 换行并将文本写入输出文件
content += '\n'
with open(output_file, 'a') as outfile:
outfile.write(content.strip(' '))
結果は次のとおりです。
一部のフレーズは出現が少なく統計的に有意ではないため、それらを除外する必要があり、各フレーズの出現頻度はget_list()メソッドによってカウントされます。Pythonに付属するディクショナリデータタイプを使用すると、フレーズデータの統計を簡単に取得できます。形式は{"keyword":frequency}で、頻度はキーワードの出現回数を記録します。フレーズが新しく出現した場合は、新しいエントリとして辞書に追加されます。それ以外の場合は、頻度の値は+1になります。
# 统计每个词出现的频率
def get_list(segment_file, out_file):
# 通过词典保存每个词组出现的频率
word_dict = {}
with open(segment_file, 'r') as seg_file:
lines = seg_file.readlines()
# 遍历文件的每一行
for line in lines:
line = line.strip('\r\n')
# 将一行按空格拆分为每个词,统计词典
for word in line.split(' '):
# 如果这个词组没有在word_dict词典中出现过,则新建词典项并设为0
word_dict.setdefault(word, 0)
# 将词典word_dict中词组word对应的项计数加一
word_dict[word] += 1
# 将词典中的列表排序,关键字为列表下标为1的项,且逆序
sorted_list = sorted(word_dict.items(), key=lambda d: d[1], reverse=True)
with open(out_file, 'w') as outfile:
# 将排序后的每条词典项写入文件
for item in sorted_list:
outfile.write('%s\t%d\n' % (item[0], item[1]))
統計結果は次のとおりです。
2.2、データ読み取り
フレーズを直接コーディング学習に使用することはできません。フレーズを埋め込みコーディングに変換する必要があります。生成されたtrain_listリストに従って、各フレーズに前から後ろの順に番号を付けます。フレーズの頻度がしきい値未満の場合は、除外されます。Word_listクラスを使用してトレーニングデータとテストデータのフレーズオブジェクトを作成し、クラスコンストラクタ__init __()にフレーズエンコーディングを実装します。また、クラスメソッドのセンテンス2idを定義して、分割されたフレーズを対応するid配列に変換します。単語がフレーズリストにない場合は、値を-1に設定します。
クラスを定義する前に、後で使用するためにいくつかのハイパーパラメーターを指定します。
# 定义超参数
embedding_size = 32 # 每个词组向量的长度
max_words = 10 # 一个句子最大词组长度
lstm_layers = 2 # lstm网络层数
lstm_nodes = [64, 64] # lstm每层结点数
fc_nodes = 64 # 全连接层结点数
batch_size = 100 # 每个批次样本数据
lstm_grads = 1.0 # lstm网络梯度
learning_rate = 0.001 # 学习率
word_threshold = 10 # 词表频率门限,低于该值的词语不统计
num_classes = 4 # 最后的分类结果有4类
class Word_list:
def __init__(self, filename):
# 用词典类型来保存需要统计的词组及其频率
self._word_dic = {}
with open(filename, 'r',encoding='GB2312',errors='ignore') as f:
lines = f.readlines()
for line in lines:
word, freq = line.strip('\r\n').split('\t')
freq = int(freq)
# 如果词组的频率小于阈值,跳过不统计
if freq < word_threshold:
continue
# 词组列表中每个词组都是不重复的,按序添加到word_dic中即可,下一个词组id就是当前word_dic的长度
word_id = len(self._word_dic)
self._word_dic[word] = word_id
def sentence2id(self, sentence):
# 将以空格分割的句子返回word_dic中对应词组的id,若不存在返回-1
sentence_id = [self._word_dic.get(word, -1)
for word in sentence.split()]
return sentence_id
train_list = Word_list(train_list_dir)
TextDataクラスを定義してデータの読み取りと管理を完了し、__ init __()関数で処理されたばかりのtrain_segment.txtファイルを読み取り、カテゴリタグと文句をタブ文字に従って分割し、カテゴリと文を数値IDに変換します。文のフレーズが最大しきい値を超える場合、超過分は切り捨てられ、十分でない場合は、-1で埋められます。データのクリーンアップにはクラス関数_shuffle_data()、バッチごとにデータとラベルを返すにはnext_batch()、フレーズの総数を返すにはget_size()を定義します。
class TextData:
def __init__(self, segment_file, word_list):
self.inputs = []
self.labels = []
# 通过词典管理文本类别
self.label_dic = {'体育': 0, '校园': 1, '女性': 2, '出版': 3}
self.index = 0
with open(segment_file, 'r') as f:
lines = f.readlines()
for line in lines:
# 文本按制表符分割,前面为类别,后面为句子
label, content = line.strip('\r\n').split('\t')[0:2]
self.content_size = len(content)
# 将类别转换为数字id
label_id = self.label_dic.get(label)
# 将句子转化为embedding数组
content_id = word_list.sentence2id(content)
# 如果句子的词组长超过最大值,截取max_words长度以内的id值
content_id = content_id[0:max_words]
# 如果不够则填充-1,直到max_words长度
padding_num = max_words - len(content_id)
content_id = content_id + [-1 for i in range(padding_num)]
self.inputs.append(content_id)
self.labels.append(label_id)
self.inputs = np.asarray(self.inputs, dtype=np.int32)
self.labels = np.asarray(self.labels, dtype=np.int32)
self._shuffle_data()
# 对数据按照(input,label)对来打乱顺序
def _shuffle_data(self):
r_index = np.random.permutation(len(self.inputs))
self.inputs = self.inputs[r_index]
self.labels = self.labels[r_index]
# 返回一个批次的数据
def next_batch(self, batch_size):
# 当前索引+批次大小得到批次的结尾索引
end_index = self.index + batch_size
# 如果结尾索引大于样本总数,则打乱所有样本从头开始
if end_index > len(self.inputs):
self._shuffle_data()
self.index = 0
end_index = batch_size
# 按索引返回一个批次的数据
batch_inputs = self.inputs[self.index:end_index]
batch_labels = self.labels[self.index:end_index]
self.index = end_index
return batch_inputs, batch_labels
# 获取词表数目
def get_size(self):
return self.content_size
# 训练数据集对象
train_set = TextData(train_segment_dir, train_list)
# print(data_set.next_batch(10))
# 训练数据集词组条数
train_list_size = train_set.get_size()
2.3、計算グラフモデルを作成する
関数create_modelを定義して、計算グラフモデルの構築を実現します。最初に、モデル入力のプレースホルダーを定義します。これは、入力テキスト入力、出力ラベル出力、およびドロップアウトkeep_probの比率です。
最初に埋め込み層を構築し、入力コードを抽出してそれらを行列にスプライスします。たとえば、入力[1,8,3]、次に埋め込み[1]、埋め込み[8]、埋め込み[3]を抽出して行列にスプライスします。
次に、LSTMネットワークを構築します。ここでは、2層ネットワークを構築し、各層のノード数を前のパラメーターlstm_node []配列で定義します。各セルの構築は、関数tf.contrib.rnn.BasicLSTMCellによって実現され、ドロップアウト操作を実行します。次に、2つのセルをLSTMネットワークにマージし、関数tf.nn.dynamic_rnnを介してembedded_inputsをLSTMネットワークに入力し、出力rnn_outputを取得します。これは3次元配列で、2番目の次元はトレーニングステップの数を表し、最後の次元の結果のみを取得します。つまり、添え字の値は-1です。
次に、完全に接続されたレイヤーを構築し、tf.layers.dense関数を介して完全に接続されたレイヤーを定義し、ドロップアウト操作後に出力をカテゴリーにマップします。カテゴリータイプのパラメーターnum_classesは推定値logitsを取得します
次に、損失や精度などの評価値を見つけることができます。予測値ロジットとラベル値出力の間のクロスエントロピー損失値を計算し、次にarg_maxを介して予測値を計算してから、正解率を見つけます
次に、トレーニング方法を定義し、変数に勾配クリッピングを適用して、勾配が消えないようにします。
最後に、プレースホルダーや損失などの入力評価値、およびその他のトレーニングパラメーターが、呼び出し元の関数の外部に返されます。
# 创建计算图模型
def create_model(list_size, num_classes):
# 定义输入输出占位符
inputs = tf.placeholder(tf.int32, (batch_size, max_words))
outputs = tf.placeholder(tf.int32, (batch_size,))
# 定义是否dropout的比率
keep_prob = tf.placeholder(tf.float32, name='keep_rate')
# 记录训练的总次数
global_steps = tf.Variable(tf.zeros([], tf.float32), name='global_steps', trainable=False)
# 将输入转化为embedding编码
with tf.variable_scope('embedding',
initializer=tf.random_normal_initializer(-1.0, 1.0)):
embeddings = tf.get_variable('embedding', [list_size, embedding_size], tf.float32)
# 将指定行的embedding数值抽取出来
embedded_inputs = tf.nn.embedding_lookup(embeddings, inputs)
# 实现LSTM网络
scale = 1.0 / math.sqrt(embedding_size + lstm_nodes[-1]) / 3.0
lstm_init = tf.random_uniform_initializer(-scale, scale)
with tf.variable_scope('lstm_nn', initializer=lstm_init):
# 构建两层的lstm,每层结点数为lstm_nodes[i]
cells = []
for i in range(lstm_layers):
cell = tf.contrib.rnn.BasicLSTMCell(lstm_nodes[i], state_is_tuple=True)
# 实现Dropout操作
cell = tf.contrib.rnn.DropoutWrapper(cell, output_keep_prob=keep_prob)
cells.append(cell)
# 合并两个lstm的cell
cell = tf.contrib.rnn.MultiRNNCell(cells)
# 将embedded_inputs输入到RNN中进行训练
initial_state = cell.zero_state(batch_size, tf.float32)
# runn_output:[batch_size,num_timestep,lstm_outputs[-1]
rnn_output, _ = tf.nn.dynamic_rnn(cell, embedded_inputs, initial_state=initial_state)
last_output = rnn_output[:, -1, :]
# 构建全连接层
fc_init = tf.uniform_unit_scaling_initializer(factor=1.0)
with tf.variable_scope('fc', initializer=fc_init):
fc1 = tf.layers.dense(last_output, fc_nodes, activation=tf.nn.relu, name='fc1')
fc1_drop = tf.contrib.layers.dropout(fc1, keep_prob)
logits = tf.layers.dense(fc1_drop, num_classes, name='fc2')
# 定义评估指标
with tf.variable_scope('matrics'):
# 计算损失值
softmax_loss = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=logits, labels=outputs)
loss = tf.reduce_mean(softmax_loss)
# 计算预测值,求第1维中最大值的下标,例如[1,1,5,3,2] argmax=> 2
y_pred = tf.argmax(tf.nn.softmax(logits), 1, output_type=tf.int32)
# 求准确率
correct_prediction = tf.equal(outputs, y_pred)
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
# 定义训练方法
with tf.variable_scope('train_op'):
train_var = tf.trainable_variables()
# for var in train_var:
# print(var)
# 对梯度进行裁剪防止梯度消失或者梯度爆炸
grads, _ = tf.clip_by_global_norm(tf.gradients(loss, train_var), clip_norm=lstm_grads)
# 将梯度应用到变量上去
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate)
train_op = optimizer.apply_gradients(zip(grads, train_var), global_steps)
# 以元组的方式将结果返回
return ((inputs, outputs, keep_prob),
(loss, accuracy),
(train_op, global_steps))
# 调用构建函数,接收解析返回的参数
placeholders, matrics, others = create_model(train_list_size, num_classes)
inputs, outputs, keep_prob = placeholders
loss, accuracy = matrics
train_op, global_steps = others
2.4。トレーニング
セッションを介して計算グラフモデルを実行し、train_setからトレーニングセットデータをバッチで取得してプレースホルダーに入力し、sess.runを実行して、損失値、精度率、印刷などの中間値を取得します
# 进行训练
init_op = tf.global_variables_initializer()
train_keep_prob = 0.8 # 训练集的dropout比率
train_steps = 10000
with tf.Session() as sess:
sess.run(init_op)
for i in range(train_steps):
# 按批次获取训练集数据
batch_inputs, batch_labels = train_set.next_batch(batch_size)
# 运行计算图
res = sess.run([loss, accuracy, train_op, global_steps],
feed_dict={inputs: batch_inputs, outputs: batch_labels,
keep_prob: train_keep_prob})
loss_val, acc_val, _, g_step_val = res
if g_step_val % 20 == 0:
print('第%d轮训练,损失:%3.3f,准确率:%3.5f' % (g_step_val, loss_val, acc_val))
データセットで10,000ラウンドのトレーニングを行った後、トレーニングセットの精度は約90%で推移しました
ソースコードと関連データファイル:https : //github.com/SuperTory/MachineLearning/tree/master/TextRNN