作者归属是一种文本分类问题。不同于在疾病文本分类问题中是根据主题来分类,这次的目标是根据作者来分类文本。尝试解决这类问题的内在假设是,不同作者之间的风格存在差异,并且模型可以辨别出这种差异。BERT之类的模型能做到这一点吗?语言模型真的能理解写作风格吗?
1 数据集
- 下载地址:作者归属识别数据集
1.1 背景
《联邦党人文集》是一组在1787到1788年间,由Alexander Hamilton
、James Madison
和John Jay
撰写的论文。它们最初以笔名Publius
发表,目的是为了推动当时新宪法在美国的批准。在后来的一些年里,这85篇文章的作者逐一被识别出来。然而,仍有一部分文章的作者尚不确定。《联邦党人文集》的作者归属问题在过去曾是统计自然语言处理研究的重要课题。现在,我们尝试使用你自己基于BERT的项目模型来解决这个问题。
具体来说,问题就是要识别出每一篇存在争议的文章,是Alexander Hamilton还是James Madison撰写的。在这个练习中,你可以假设每篇文章只有一个作者,也就是说没有合作写作(尽管这并不能百分之百确认),并且每位作者在所有被确认的文章中都有一个清晰明确的写作风格。
数据集
本项目中有标注好的train.tsv
和dev.tsv
数据集。测试集有10个,分别对应每一篇有争议的文章。所有数据集都位于data/federalist_papers_HM
目录下。
每一个句子实际上是一组大约256个词的句子集合。标签中 ‘0’ 表示HAMILTON
,‘1’ 表示MADISON
。示例文件中包含的Hamilton
的文章数量多于Madison
。验证集中的标签分布大致与训练集保持一致。
1.2 查看数据
我们先来看一下有哪些文件:
# 指定数据所在的目录
DATA_DIR = '/dli/task/data/federalist_papers_HM'
# 列出该目录下的所有文件
!ls $DATA_DIR
输出如下:
dev.tsv
test.tsv
test49.tsv
test50.tsv
test51.tsv
test52.tsv
test53.tsv
test54.tsv
test55.tsv
test56.tsv
test57.tsv
test62.tsv
train.tsv
- 其中
dev.tsv
为验证集,train.tsv
为训练集,test.tsv
为测试集,将测试集test.tsv
预处理完,分割得到test*.tsv
文件。
其中train.tsv
数据如下:
这些数据的格式不符合NeMo文本分类的要求,即它有一个标题行/列名(sentence label
),现在我们去掉标题行,并将新的数据集保存为train_nemo_format.tsv
和dev_nemo_format.tsv
。
import pandas as pd
# 设置文件路径
train_file_path = DATA_DIR + '/train.tsv'
dev_file_path = DATA_DIR + '/dev.tsv'
train_nemo_format_path = DATA_DIR + '/train_nemo_format.tsv'
dev_nemo_format_path = DATA_DIR + '/dev_nemo_format.tsv'
# 读取原始文件为 DataFrame
train_df = pd.read_csv(train_file_path, sep='\t', header=0)
dev_df = pd.read_csv(dev_file_path, sep='\t', header=0)
# 以 NeMo 需要的格式保存为 tsv 文件(不含列名)
train_df.to_csv(train_nemo_format_path, sep='\t', index=False, header=False)
dev_df.to_csv(dev_nemo_format_path, sep='\t', index=False, header=False)
2 配置
2.1 模型配置
查看默认的模型配置文件text_classification_config.yaml。
# 查看配置文件中模型部分的默认配置
CONFIG_DIR = "/dli/task/nemo/examples/nlp/text_classification/conf"
CONFIG_FILE = "text_classification_config.yaml"
# 加载配置
config = OmegaConf.load(CONFIG_DIR + "/" + CONFIG_FILE)
# 打印模型配置为 YAML 格式
print(OmegaConf.to_yaml(config.model))
输出如下:
nemo_path: text_classification_model.nemo
tokenizer:
tokenizer_name: ${model.language_model.pretrained_model_name}
vocab_file: null
tokenizer_model: null
special_tokens: null
language_model:
pretrained_model_name: bert-base-uncased
lm_checkpoint: null
config_file: null
config: null
classifier_head:
num_output_layers: 2
fc_dropout: 0.1
class_labels:
class_labels_file: null
dataset:
num_classes: ???
do_lower_case: false
max_seq_length: 256
class_balancing: null
use_cache: false
train_ds:
file_path: null
batch_size: 64
shuffle: true
num_samples: -1
num_workers: 3
drop_last: false
pin_memory: false
validation_ds:
file_path: null
batch_size: 64
shuffle: false
num_samples: -1
num_workers: 3
drop_last: false
pin_memory: false
test_ds:
file_path: null
batch_size: 64
shuffle: false
num_samples: -1
num_workers: 3
drop_last: false
pin_memory: false
optim:
name: adam
lr: 2.0e-05
betas:
- 0.9
- 0.999
weight_decay: 0.01
sched:
name: WarmupAnnealing
warmup_steps: null
warmup_ratio: 0.1
last_epoch: -1
monitor: val_loss
reduce_on_plateau: false
infer_samples:
- by the end of no such thing the audience , like beatrice , has a watchful affection
for the monster .
- director rob marshall went out gunning to make a great one .
- uneasy mishmash of styles and genres .
查看可用的BERT类语言模型列表:
from nemo.collections import nlp as nemo_nlp
nemo_nlp.modules.get_pretrained_lm_models_list()
输出如下:
['megatron-bert-345m-uncased',
'megatron-bert-345m-cased',
'megatron-bert-uncased',
'megatron-bert-cased',
'biomegatron-bert-345m-uncased',
'biomegatron-bert-345m-cased',
'bert-base-uncased',
'bert-large-uncased',
'bert-base-cased',
'bert-large-cased',
'bert-base-multilingual-uncased',
'bert-base-multilingual-cased',
'bert-base-chinese',
'bert-base-german-cased',
'bert-large-uncased-whole-word-masking',
'bert-large-cased-whole-word-masking',
'bert-large-uncased-whole-word-masking-finetuned-squad',
'bert-large-cased-whole-word-masking-finetuned-squad',
'bert-base-cased-finetuned-mrpc',
'bert-base-german-dbmdz-cased',
'bert-base-german-dbmdz-uncased',
'cl-tohoku/bert-base-japanese',
'cl-tohoku/bert-base-japanese-whole-word-masking',
'cl-tohoku/bert-base-japanese-char',
'cl-tohoku/bert-base-japanese-char-whole-word-masking',
'TurkuNLP/bert-base-finnish-cased-v1',
'TurkuNLP/bert-base-finnish-uncased-v1',
'wietsedv/bert-base-dutch-cased',
'distilbert-base-uncased',
'distilbert-base-uncased-distilled-squad',
'distilbert-base-cased',
'distilbert-base-cased-distilled-squad',
'distilbert-base-german-cased',
'distilbert-base-multilingual-cased',
'distilbert-base-uncased-finetuned-sst-2-english',
'roberta-base',
'roberta-large',
'roberta-large-mnli',
'distilroberta-base',
'roberta-base-openai-detector',
'roberta-large-openai-detector',
'albert-base-v1',
'albert-large-v1',
'albert-xlarge-v1',
'albert-xxlarge-v1',
'albert-base-v2',
'albert-large-v2',
'albert-xlarge-v2',
'albert-xxlarge-v2']
2.2 参数配置
完成以下代码中的 #FIXME行,并运行保存代码块。
# 设置模型相关参数
NUM_CLASSES = 2 # 设置分类数为2(Hamilton 和 Madison)
MAX_SEQ_LENGTH = 256 # 设置最大序列长度
BATCH_SIZE = 64 # 设置训练和验证的批量大小
PATH_TO_TRAIN_FILE = "/dli/task/data/federalist_papers_HM/train_nemo_format.tsv"
PATH_TO_VAL_FILE = "/dli/task/data/federalist_papers_HM/dev_nemo_format.tsv"
PRETRAINED_MODEL_NAME = 'bert-base-uncased' # 预训练模型名称,可按需更改
LR = 2.0e-5 # 学习率,可按需调整
- 类别数(
NUM_CLASSES
):在这个项目中我们只关心HAMILTON和MADISON。John Jay的文章已被从数据集中排除。 - 最大序列长度(
MAX_SEQ_LEN
):可使用的最大序列长度为64、128或256。较大的模型(如BERT-large、Megatron)可能需要更短的序列长度来避免内存溢出。 - 批量大小(
BATCH_SIZE
):更大的批量可以加速训练,但大型语言模型容易耗尽内存。 - 语言模型(
PRETRAINED_MODEL_NAME
):你可以尝试不同的语言模型来更好地识别写作风格。尤其是当大小写信息很重要时,建议使用“cased”模型。 - 学习率(
LR
):学习率控制模型参数每次更新的步长,若设置过小,模型收敛缓慢、训练时间长;若过大,容易导致损失震荡甚至不收敛,常用范围为1e-5
到5e-5
。通常情况下,较大的batch size可以配合稍高的学习率以加快训练速度。
2.3 Trainer配置
查看Trainer和exp_manager的默认配置。
# 打印 Trainer 配置
print(OmegaConf.to_yaml(config.trainer))
# 打印 exp_manager 配置
print(OmegaConf.to_yaml(config.exp_manager))
输出如下:
gpus: 1
num_nodes: 1
max_epochs: 100
max_steps: null
accumulate_grad_batches: 1
gradient_clip_val: 0.0
amp_level: O0
precision: 32
accelerator: ddp
log_every_n_steps: 1
val_check_interval: 1.0
resume_from_checkpoint: null
num_sanity_val_steps: 0
checkpoint_callback: false
logger: false
exp_dir: null
name: TextClassification
create_tensorboard_logger: true
create_checkpoint_callback: true
将自动混合精度设置为level 1,并使用FP16精度。将MAX_EPOCHS设置为一个合理的值,建议5到20之间。
- PRECISION = 16:启用混合精度训练,让模型在合适的地方使用更快、更省显存的FP16计
- AMP_LEVEL = “O1”:表示使用自动混合精度的推荐策略,自动决定哪些操作用FP16,哪些保留FP32,以兼顾训练速度和数值稳定性。
# 设置 Trainer 的参数
MAX_EPOCHS = 5 # 设置训练轮数
AMP_LEVEL = "O1" # 设置自动混合精度级别
PRECISION = 16 # 设置精度为16位(FP16)
3 训练和推理
3.1 训练
我们通过下面代码来获取最新保存的模型文件路径:
def get_latest_model():
# 搜索所有 .nemo 模型文件
nemo_model_paths = glob.glob('nemo_experiments/TextClassification/*/checkpoints/*.nemo')
# 按时间从新到旧排序
nemo_model_paths.sort(reverse=True)
return nemo_model_paths[0]
现在我们使用脚本text_classification_with_bert.py开始训练:
%%time
TC_DIR = "/dli/task/nemo/examples/nlp/text_classification"
!python $TC_DIR/text_classification_with_bert.py \
model.dataset.num_classes=$NUM_CLASSES \
model.dataset.max_seq_length=$MAX_SEQ_LENGTH \
model.train_ds.file_path=$PATH_TO_TRAIN_FILE \
model.validation_ds.file_path=$PATH_TO_VAL_FILE \
model.infer_samples=[] \
trainer.max_epochs=$MAX_EPOCHS \
model.language_model.pretrained_model_name=$PRETRAINED_MODEL_NAME \
trainer.amp_level=$AMP_LEVEL \
trainer.precision=$PRECISION \
model.train_ds.batch_size=$BATCH_SIZE \
model.validation_ds.batch_size=$BATCH_SIZE
输出如下:
3.2 推理
运行以下推理代码块,查看并保存结果。
from nemo.collections import nlp as nemo_nlp
# 从最近的 .nemo 检查点恢复模型
model = nemo_nlp.models.TextClassificationModel.restore_from(get_latest_model())
# 指定数据目录
DATA_DIR = '/dli/task/data/federalist_papers_HM'
# 要推理的测试文件列表,每个对应一篇有争议的文章
test_files = [
'test49.tsv',
'test50.tsv',
'test51.tsv',
'test52.tsv',
'test53.tsv',
'test54.tsv',
'test55.tsv',
'test56.tsv',
'test57.tsv',
'test62.tsv',
]
# 用于存储每个文件的预测结果
results = []
# 遍历每个测试文件进行推理
for test_file in test_files:
# 读取文件并移除标题行
filepath = os.path.join(DATA_DIR, test_file)
with open(filepath, "r") as f:
lines = f.readlines()
del lines[0]
# 对每个测试样本进行文本分类
results.append(model.classifytext(lines, batch_size = 1, max_seq_length = 256))
# 打印所有结果(每个测试文件的预测分数)
print(results)
输出如下:
[[0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 1, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]
下面通过计算每篇文章中所有句子的平均预测概率来判断整篇文章的作者。若平均概率小于0.5,则判为HAMILTON(label 0
);否则判为MADISON(label 1
)。
author = []
for result in results:
avg_result = sum(result) / len(result)
if avg_result < 0.5:
author.append("HAMILTON")
print("HAMILTON")
else:
author.append("MADISON")
print("MADISON")
输出如下:
HAMILTON
HAMILTON
HAMILTON
HAMILTON
HAMILTON
HAMILTON
HAMILTON
HAMILTON
HAMILTON
HAMILTON
可以推测出这些测试文本大概率是由HAMILTON写的。
4 总结
本项目探索了利用BERT模型进行作者归属识别的可行性,验证了语言模型在识别写作风格差异方面的潜力。通过构建分类模型对《联邦党人文集》中的争议文章进行分析,展示了深度学习方法在文本风格识别任务中的实际应用能力。