对知识点进行了学习

中文情感分析

code environment

在 python3.x & Tensorflow1.x 下工作正常

语料的准备

语料的选择为 谭松波老师的评论语料,正负例各2000。属于较小的数据集

词向量的准备

本实验使用开源词向量chinese-word-vectors

选择知乎语料训练而成的Word Vector

模型:CNN

结构:
  1. 中文词Embedding
  2. 多个不同长度的定宽卷积核
  3. 最大池化层,每个滤波器输出仅取一个最大值
  4. 全连接

模型主要代码解析

数据预处理

def parse_fn(line_words, line_tag):
    # Encode in Bytes for TF
    words = [w.encode() for w in line_words.strip().split()]
    tag = line_tag.strip().encode()
    return words, tag
定义了一个名为parse_fn的函数,它接收line_wordsline_tag作为输入。它将line_words字符串按空格分隔成单词列表,去除每个单词的首尾空白,并使用encode()方法将其编码为字节类型。同样地,line_tag也去除了首尾空白并编码为字节类型。函数返回一个元组(words, tag)
def generator_fn(words, tags):
    with Path(words).open('r', encoding='utf-8') as f_words, Path(tags).open('r', encoding='utf-8') as f_tags:
        for line_words, line_tag in zip_words, f_tags):
            yield parse_fn(line_words, line_tag)
定义了一个名为generator_fn的函数,它接收wordstags作为输入。使用Path打开两个指定路径的文件以供读取(采用UTF-8编码)。通过使用zip同时迭代这两个文件的每行。对于每一行,使用parse_fnline_wordsline_tag进行处理,并使用yield返回结果。该函数作为生成器,产生包含(words, tag)元组的值。
def input_fn(words_path, tags_path, params=None, shuffle_and_repeat=False):
    params = params if params is not None else {}
这个函数定义了输入数据的函数。它接受words_pathtags_path作为输入参数,params用于指定其他参数,默认为None
shapes = ([None], ())  # shape of every sample
types = (tf.string, tf.string)
defaults = ('<pad>', '')
定义了数据样本的形状(shapes),类型(types)和默认值(defaults)。每个样本由一个长度可变的字符串列表和一个字符串标签组成。
dataset = tf.data.Dataset.from_generator(
    functools.partial(generator_fn, words_path, tags_path),
    output_shapes=shapes, output_types=types).map(lambda w, t: (w[:params.get('nwords', 300)], t))
创建了一个tf.data.Dataset对象。使用tf.data.Dataset.from_generator从生成器函数generator_fn创建数据集。functools.partialwords_pathtags_path作为参数传递给generator_fn。指定了输出的形状和类型。通过map方法对每个样本进行处理,只保留前params.get('nwords',300)个单词,并保留原始的标签。
if shuffle_and_repeat:
    dataset = dataset.shuffle(params['buffer']).repeat(params['epochs'])
如果shuffle_repeatTrue,则对数据集进行洗牌和重复操作。使用shuffle方法对数据集进行洗牌,并设置缓冲区大小为params['buffer']。然后使用repeat方法对数据集进行重复操作,重复次数为params['epochs']
dataset = (dataset
           .padded_batch(params.get('batch_size', 20), ([params.get('nwords', 300)], ()), defaults)
           .prefetch(1))
对数据集进行填充批处理操作。使用padded_batch方法将数据集分成指定的批次大小,并在不同批次中对长度不同的样本进行填充。批次大小由params.get('batch_size', 20)指定,默认为20。填充的形状为([params.get('nwords', 300)], ()),其中params.get('nwords', 300)表示单词的最大数量,默认为300。未指定填充值的情况下,将使用默认值('<pad>', '')进行填充。最后,使用prefetch方法提前加载一批数据用于后续训练的处理。
return dataset
返回处理后的数据集。

模型定义

def model_fn(features, labels, mode, params):
    if isinstance(features, dict):
        features = features['words']
定义了模型函数model_fn,它接收featureslabelsmodeparams作为输入参数。如果features是一个字典,则将其赋值为features['words']
dropout = params.get('dropout', 0.5)
training (mode == tf.estimator.ModeKeys.TRAIN)
vocab_words = tf.contrib.lookup.index_table_from_file(
    params['words'], num_oov_buckets=params['num_oov_buckets'])
params中获取dropout的值(默认为0.5)。根据mode是否为训练模式,设置training变量为TrueFalse。使用tf.contrib.lookup.index_table_from_file根据文件params['words']创建一个单词的索引表,并指定了OOV(Out-of-vocabulary)桶的数量为params['num_oov_buckets']
with Path(params['tags']).open() as f:
    indices = [idx for idx, tag in enumerate(f)]
    num_tags = len(indices)
使用Path打开文件params['tags'],并遍历其中的行,获取每个标签的索引。计算得到标签的数量num_tags
word_ids = vocab_words.lookup(features)
w2v = np.load(params['w2v'])['embeddings']
w2v_var = np.vstack([w2v, [[0.] * params['dim']]])
w2v_var = tf.Variable(w2v_var, dtype=tf.float32, trainable=False)
embeddings = tf.nn.embedding_lookup(w2v_var, word_ids)
使用单词索引表vocab_words将输入的features转换预训练的词向量w2v,并创建一个tf.Variable变量w2v_var保存词向量。使用tf.nn.embedding_lookup根据单词ID查找对应的词向量,并得到嵌入矩阵embeddings
embeddings = tf.layers.dropout(embeddings, rate=dropout, training=training)
embeddings_expanded = tf.expand_dims(embeddings, -1)
使用tf.layers.dropout对嵌入矩阵embeddings进行丢弃操作,以减少过拟合。然后,使用tf.expand_dims在最后一维上添加一个维度。
pooled_outputs = []
for i, filter_size in enumerate(params['filter_sizes']):
    conv2 = tf.layers.conv2d(embeddings_expanded, params['num_filters'], kernel_size=[filter_size, params['dim']],
                             activation=tf.nn.relu, name='conv-{}'.format(i))
    pooled = tf.layers.max_pooling2d(inputs=conv2, pool_size=[params['nwords'] - filter_size + 1, 1],
                                     strides=[1, 1], name='pool-{}'.format(i))
    pooled_outputs.append(pooled)
定义一个空列表pooled_outputs,用于存储卷积层输出的池化结果。使用tf.layers.conv2d进行卷积操作,指定卷积核的大小为[filter_size, params['dim']],激活函数为ReLU,并命名tf.layers.max_pooling2d进行最大池化操作,指定池化窗口的大小为[params['nwords_size + 1, 1],步幅为[ 1],命名为'pool-{}'.format(i)。将池化结果pooled加入到pooled_outputs列表中。
num_total_filters = params['num_filters'] * len(params['filter_sizes'])
h_poll = tf.concat(pooled_outputs, 3)
output = tf.reshape(h_poll, [-1,```
计算总的过滤器数量`num_total_filters`,即`params['num_filters']`乘以卷积核尺寸的种类数。使用`tf.concat`在第三维上将所有池化结果拼接起来,得形状为`[batch_size, nwords, num_total_filters]`的`h_pool`。然后,使用``将`h_pool`展平成形状为`[-1, num_total_filters]`的张量。最后,再次使用`tf.layers.dropout`对输出进行丢弃操作。

```python
logits = tf.layers.dense(output, num_tags)
pred_ids = tf.argmax(input=logits, axis=1)
使用全连接层tf.layers.dense将输出转换为具有num_tags个输出节点的向量。使用tf.argmaxaxis=1上找出预测值索引,得到最终的预测标签索引pred_ids
if mode == tf.estimator.ModeKeys.PREDICT:
    reversed_tags = tf.contrib.lookup.index_to_string_table_from_file(params['tags'])
    pred_labels = reversed_tags.lookup(tf.argmax(inputits, axis=1))
    predictions = {
        'classes_id': pred        'labels': pred_labels
    }
    return tf.estimator.EstimatorSpec(mode, predictions=predictions)
else:
    tags_table = tf.contrib.lookup.index_table_from_file(params['tags tags = tags_table.lookup(labels)
    loss = tf.losses.sparse_softmax_cross_entropy(labels=tags, logits=logits)
如果是预测模式(mode == tf.estimator.ModeKeys.PREDICT),则加载标签反查表reversed_tags,将预测标签索引转换为实际标签,并构建预测结果字典predictions。之后,返回一个tf.estimator.EstimatorSpec对象,其中包含预测结果。如果不是预测模式,则使用标签索引表tags_table将实际标签转换为索引形式,并通过sparse_softmax_cross_entropy计算损失值loss
metrics = {
    'acc': tf.metrics.accuracy(tags, pred_ids),
    'precision': tf.metrics.precision(tags, pred_ids),
    'recall': tf.metrics.recall(tags, pred_ids)
}

for metric_name, op in metrics.items():
    tf.summary.scalar(metric_name, op[1])
定义了评估指标metrics,包括准确率('acc'),精确度('precision')和召回率('recall')。使用tf.metrics.accuracytf.metrics.precisiontf.metrics.recall`计算这些指标,并将其添加到TensorBoard的摘要中。
if mode == tf.estimator.ModeKeys.TRAIN:
    train_op = tf.train.AdamOptimizer().minimize(
        loss, global_step=tf.train.get_or_create_global_step())
    return tf.estimator.EstimatorSpec(mode, loss=loss, train_op=train_op)
elif mode == tf.estimator.ModeKeys.EVAL:
    return tf.estimator.EstimatorSpec(
        mode, loss=loss, eval_metric_ops=metrics)
如果是训练模式,则创建一个Adam优化器并最小化损失函数。返回一个tf.estimator.EstimatorSpec对象,其中包括损失值loss和训练操作train_op。如果是评估模式,则返回一个包含损失值loss和评估指标eval_metric_opstf.estimator.EstimatorSpec对象。

训练过程

if __name__ == '__main__':
	params = {
    'dim': 300,
    'nwords': 300,
    'filter_sizes': [2, 3, 4],
    'num_filters': 64,
    'dropout': 0.6,
    'num_oov_buckets': 1,
    'epochs': 50,
    'batch_size': 20,
    'buffer': 3500,
    'words': str(Path(DATA_DIR, 'vocab.words.txt')),
    'tags': str(Path(DATA_DIR, 'vocab.labels.txt')),
    'w2v': str(Path(DATA_DIR, 'w2v.npz'))
}
定义一个名为params的字典,其中包含了一组模型超参数。这些超参数包括词向量的维度dim、句子最大长度nwords、卷积核大小的列表filter_sizes、卷积核数量num_filters、丢弃率dropout、OOV桶的数量num_oov_buckets、训练轮数epochs、批处理大小batch_size、缓冲区大小buffer以及用于存储词汇表、标签和词向量的文件路径。
with Path('../../../Mutual-AI/model/chinese_sentiment_analysis/params.json').open('w') as f:
    json.dump(params, f, indent=4, sort_keys=True)
使用Path打开文件'../../../Mutual-AI/model/chinese_sentiment_analysis/params.json',以写式打开文件。然后使用json.dump以JSON格式写入文件中,缩进为4个空格,按键排序。
def fwords(name):
    return str(Path(DATA_DIR, '{}.words.txt'.format(name)))
定义了一个名为fwords的函数,它接收一个字符串参数name,并返回使用name构造路径的结果。路径由DATA_DIR{}.words.txt组成,其中{}将被替换为函数的参数name
def ftags(name):
    return str(Path(DATA_DIR, '{}.txt'.format(name)))
定义了一个名为ftags的,它接收一个字符串参数name,并返回使用name构造路径的结果。路径由{}将被替换为函数的参数name
train_inpf = functools.partial(input_fn, fwords('train'),
                               params, shuffle_and_repeat=True)
eval_inpf = functools.partial(input_fn, fwords('eval'), ftags('eval'))
创建了两个特殊函数``eval_inpf,它们是通过部分应input_fn函数获得的。train_inpf用于训练数据集,会将fwords('train')ftags('train')作为输入路径,并附带其他参数paramsshuffle_and_repeat=Trueeval_inpf用于评估数据集,会将fwords('eval')ftags('eval')作为输入路径。
cfg = tf.estimator.RunConfig(save_checkpoints_secs=10)
estimator = tf.estimator.Estimator(model_fn, 'results/model', cfg, params)
Path(estimator.eval_dir()).mkdir(parents=True, exist_ok=True)
创建了一个tf.estimator.RunConfig对象cfg,用于配置训练的运行环境,设置每隔10秒保存检查点。然后,使用这个运行配置、模型函数model_fn、模型保存路径'results/model'和超参数params创建了一个tf.estimator.Estimatorestimator.eval_dir()获取评估结果的保存路径,并使用Path`创建该路径的父目录,确保路径存在。
train_spec = tf.estimator.TrSpec(input_fn=train_inpf)
eval_spec = tf.EvalSpec(input_fn=eval_inpf, throttle_secs=10)
创建了训练规格train_spec和评估规格eval_specTrainSpectrain_inpf作为输入函数进行进行评估,其中还设置了每隔10秒进行一次.train_and_evaluate(estimator, train_spec, eval_spec) tf.estimator.train_and_evaluate函数同时进行训和eval_spec`传递给该函数。
def write_predictions(name):
    Path('results/score').mkdir(parents=True, exist_ok=True)
    with Path('results/score/{}.preds.txt'.format(name)).open('wb') as f:
        test_inpf = functools.partial(input_fn, fwords(name), ftags(name))
        golds_gen = generator_fn(fwords(name), ftags(name))
        preds_gen = estimator.predict(test_inpf)
        for golds, preds in zip(golds_gen, preds_gen):
            (words, tag) =
            f.write' '.join([tag, preds['labels'], b''.join(words)]) + b'\n')


for name in ['train', 'eval']:
    write_predictions(name)
定义了一个名为write_predictions的函数,用于将预测结果写入文件。先创建保存预测结果的目录results/score,然后使用Path打开文件results/score/{}.preds.txt进行写操作。在循环中,通过部分应用的方式创建输入函数test_inpf,并分别生成原始标签和预测结果的生成器。通过遍历这两个生成器,将数据写入文件中。
最后,对['train', 'eval']中的每个名称调用write_predictions函数,分别生成训练集和评估集的预测结果文件。
模型效果输出:
precision    recall  f1-score   support

         POS       0.91      0.87      0.89       400
         NEG       0.88      0.91      0.89       400

   micro avg       0.89      0.89      0.89       800
   macro avg       0.89      0.89      0.89       800
weighted avg       0.89      0.89      0.89       800