目次
知乎では、2022年の捜狐キャンパスNLPアルゴリズムコンペティションで1位の感情分析計画の共有と共有 を見ました。計画は非常にシンプルでエレガントであると同時に、迅速な学習のヒントがあったと感じました(厳密に言えば、迅速な学習ではありませんでした)、効果は非常に良かったです。彼らはまた、ソリューション共有でpytorch-lightningに基づいたより詳細なアイデアとコードを提供しましたが、一部の詳細は十分に明確ではなく、コードは理解しにくいので、ブログでより明確な説明を行い、より簡潔に共有してください(より良いトーチベースの)コードを理解する。
1.競争とプログラムの理解
このコンテストのタスクは、エンティティ指向のテキスト記述感情の極性と色の強度の分析です。感情の極性と強さは、5つの状況に分けられます:非常にポジティブ、ポジティブ、ニュートラル、ネガティブ、そして非常にネガティブ。競技者は、与えられた各エンティティオブジェクトのテキスト記述の観点から、エンティティの感情的な極性と強度を分析する必要があります。
データは次のとおりです。
{"id":7410、 "content": "このような素晴らしいネットで、ファンや専門家はプレーオフで彼に大きな期待を抱くことができますか?したがって、シーズンを通して、誰もが予測するのは合理的です。今年のシーズンのイースタンカンファレンスファイナルプレーオフは昨年と同じであり、ネッツとバックスは予定通りに会うことが期待されています。今日のネッツビッグスリーは究極ではありませんが、「ディンジエニウを調理する」ことも簡単です。ブルズはまだ長い間、NBAはまだスーパースターが話すステージです! "、"エンティティ ":{"ネット ":1、"プレーオフ ":0}}{"id":88679、 "content": "2014.09海南省党委員会常任委員、大州市党委員会書記、楊浦経済開発区作業委員会副書記2014.10常任委員海南省党委員会の委員および三亜市党委員会の書記2016.11海南州党委員会の常任委員会の委員および海口市党委員会の書記は2019年9月に検査された。"、"エンティティ ":{"事務局長市政党委員会」:0、「ハイナン州党委員会」:0}}
上記のデータのコンテンツテキストと特定のエンティティについて、コンテンツに含まれる感情的な色をそれぞれ分析します。明らかに、これは分類タスクです。この質問を見たとき、頭に浮かんだ解決策は、彼らが提供したベースラインとまったく同じでした。
[CLS] content [SEP] entity_0 [SEP]
[CLS] content [SEP] entity_1 [SEP]
[CLS] content [SEP] entity_2 [SEP]
.....。
[CLS] content [SEP] entity_n [SEP]
上記のようにコンテンツと各エンティティをつなぎ合わせた後、bertモデルに送信されて文のベクトルが抽出され、分類子を通過します。これでタスクが完了します。このスキームは競技会でも使用されていると言われています。効果はあまり満足のいくものではありません。コンテストの最初の場所の計画は次のとおりです。
ベースラインの欠陥
下の図に示すように(コンテストの作者の計画共有の図を引用)
各データのエンティティデータは等しくないため、ベースラインのようなスプライシングスキームにより、モデルはコンテンツテキストを異なる方法で表示し、最終的な効果に影響を与える可能性があります。同時に、各データは次のようになります。エンティティの数をコピーしたため、トレーニングデータが多すぎて効率が低下しました。もう1つの問題は、モデルによって取得された文ベクトルの選択にも特定のエラーがあることです。ベースラインスキームでは、clsまたはすべてのトークン埋め込みがmeanPoolingに使用され、最終結果にも特定の影響を与えます。最後に、各エンティティが個別にスプライスされているため、各エンティティ間の接続が少し弱くなっているように感じます。これは、最終結果に一定の影響を及ぼします。
そもそも計画
上の図に示すように(コンテストの作成者が共有した写真を参照)、各データのエンティティは[MASK]でスプライスされ、次に[SEP]を使用してコンテンツテキストでスプライスされます。データの一部で分類を効率的に構築できるように、ベースラインのようにデータごとにタスクを複数回繰り返す必要はありません。同時に、最後の文ベクトルの選択もここでは回避され、[MASK]に対応する埋め込みは、各エンティティの感情の分類埋め込みとして直接使用されます。このスキームでの[MASK]の導入も、その中に迅速な学習のヒントがあり、著者は効果がより良いと述べました。一方、厳密なプロンプト学習ではなく、[Mask]の特定のトークンが何であるかを予測してからクラスマッピングを行う必要はありません。つまり、プロンプトアンサーの構築を行う必要はありません。スペースマッピング(Verbalizer)、プロンプトテンプレート(テンプレート)コンストラクトを実行するだけです。
一般的に、このスキームは確かによりエレガントであり、もちろん効果はより優れており、人々は一見少しさわやかな気分になります。もちろん、もっと多くの論文を読んだら(迅速な学習)、同様の解決策を考えることができるはずです。コードに実装されているいくつかの詳細-マトリックスの次元変換は、より明確な説明を提供し、スキーム全体を理解しやすくなります。
データディメンションの変更
データのバッチ
[CLS] content_0 [SEP] entity_0_0 [MASK] entity_0_1 [MASK] entity_0_2 [MASK] [SEP]
[CLS] content_1 [SEP] entity_1_0 [MASK] entity_1_1 [MASK] [SEP]
[CLS] content_2 [SEP] entity_2_0 [MASK] [SEP]
[CLS] content_3 [SEP] entity_3_0 [MASK] entity_3_1 [MASK] [SEP]
[CLS] content_4 [SEP] entity_4_0 [MASK] entity_4_1 [MASK] entity_4_2 [MASK] [SEP]
.....。
[CLS] content_(batch_size-1)[SEP] entity_(batch_size-1)_0 [MASK] entity_(batch_size-1)_1 [MASK] [SEP]
トークナイザーの後、トークンはディクショナリに対応するIDにマップされます。各データのinput_ids、attention_mask、mask_tokens、entity_count、およびlabelを記録する必要があります。対応するディメンションは次のように変化します。
input_ids:[batch、seq_length]
[
[101、******、102、**、103、**、103,102、、0,0,0,0,0]、
[101、******、102、**、103、**、103,102]、
[101、******、102、**、103,102,0,0,0,0,0]、
.....。
[101、******、102、**、103、**、103、**、103、**、103,0,0]
]
注意マスク:[バッチ、seq_length]
[
[1、******、1、**、1、**、1,1、、0,0,0,0,0]、
[1、******、1、**、1、**、1,1]、
[1、******、1、**、1,1,0,0,0,0,0]、
.....。
[1、******、1、**、1、**、1、**、1、**、1,0,0]
]
mask_tokens:[batch、seq_length]
[
[0、******、0、**、1、**、1,0、、0,0,0,0,0]、
[0、******、0、**、1、**、1,0]、
[0、********、0、**、1,0,0,0,0,0,0]、
.....。
[0、******、0、**、1、**、1、**、1、**、1,0,0]
]
ラベルはリストによって維持されます
[
[-2,2]、
[1,2]、
[-2]、
.....。
[2、-2、0、-1]
]
バッチ内のエンティティの数がmの場合、ラベルの行列は[m]です。
[-2,2,1,2、...、2、-2,0、-1]
input_ids + attention_maskがbertを通過した後に得られた結果:
#m表示バッチ内有mTLS体 is_masked = input ['is_masked']。bool() inputs = {k:v for k、v in input.items()if k in ["input_ids"、 "attention_mask"]} output = self.bert(** input、return_dict = True、output_hidden_states = True) #[batch、seq_length、768] output = output.last_hidden_state #[m、768] masked_outputs = output [is_masked] #[m、5] logits = self.classifier(masked_outputs)
第二に、コードの実装
ナンバーワンのコード
作者はpytorch-lightningに基づいたコードを提供しましたが、カプセル化は比較的高く、理解しにくいと思います。これに基づいて、トーチに基づいたバージョンのコードを実装しました。
モデルコード
from transformers import BertPreTrainedModel,BertModel
import torch.nn as nn
class SentiClassifyBertPrompt(BertPreTrainedModel):
def __init__(self,config):
super(SentiClassifyBertPrompt,self).__init__(config)
self.bert = BertModel(config=config)
self.classifier = nn.Sequential(
nn.Linear(config.hidden_size, config.hidden_size),
nn.LayerNorm(config.hidden_size),
nn.LeakyReLU(),
nn.Dropout(p=config.dropout),
nn.Linear(config.hidden_size, config.output_dim),
)
def forward(self,inputs):
# m表示batch内有m个实体
is_masked = inputs['is_masked'].bool()
inputs = {k: v for k, v in inputs.items() if k in ["input_ids", "attention_mask"]}
outputs = self.bert(**inputs,return_dict=True, output_hidden_states=True)
# [batch, seq_length, 768]
outputs = outputs.last_hidden_state
# [m,768]
masked_outputs = outputs[is_masked]
# [m,5]
logits = self.classifier(masked_outputs)
return logits
データ読み込みコード
import torch
from torch.utils.data import Dataset
from tqdm import tqdm
import json
class DataReader(Dataset):
def __init__(self,file_path,tokenizer,max_langth):
self.file_path = file_path
self.tokenizer = tokenizer
self.max_length = max_langth
self.data_list = self.texts_tokeniztion()
self.allLength = len(self.data_list)
def texts_tokeniztion(self):
with open(self.file_path,'r',encoding='utf-8') as f:
lines = f.readlines()
res = []
for line in tqdm(lines,desc='texts tokenization'):
line_dic = json.loads(line.strip('\n'))
content = line_dic['content']
entity = line_dic['entity']
prompt_length = 0
prompts = ""
label = []
en_count = len(entity)
for k,v in entity.items():
prompt_length += len(k) + 1
#标签化为 0-4的整数
label.append(v+2)
prompts += k +"[MASK]"
#直接最大长度拼接
content = content[0:self.max_length-prompt_length-1-10]
text = content + "[SEP]" + prompts
input_ids,attention_mask,masks = self.text2ids(text)
input_ids = torch.tensor(input_ids,dtype=torch.long)
attention_mask = torch.tensor(attention_mask,dtype=torch.long)
masks = torch.tensor(masks, dtype=torch.long)
#记录每条数据有多少个实体,方便推理的时候batch推理
en_count = torch.tensor(en_count,dtype=torch.long)
temp = []
temp.append(input_ids)
temp.append(attention_mask)
temp.append(masks)
temp.append(label)
temp.append(en_count)
res.append(temp)
return res
def text2ids(self,text):
inputs = self.tokenizer(text)
input_ids = inputs['input_ids']
attention_mask = inputs['attention_mask']
masks = [ int(id==self.tokenizer.mask_token_id) for id in input_ids]
return input_ids, attention_mask, masks
def __getitem__(self, item):
input_ids = self.data_list[item][0]
attention_mask = self.data_list[item][1]
masks = self.data_list[item][2]
label = self.data_list[item][3]
en_count = self.data_list[item][4]
return input_ids, attention_mask, masks, label, en_count
def __len__(self):
return self.allLength
モデルトレーニングコード
from data_reader.reader import DataReader
import torch
from torch.utils.data import DataLoader
from transformers import BertTokenizer,BertConfig
from torch.optim import AdamW
from model import SentiClassifyBertPrompt
from torch.optim.swa_utils import AveragedModel, SWALR
from torch.nn.utils.rnn import pad_sequence
from log.log import Logger
from tqdm import tqdm
import torch.nn.functional as F
import os
os.environ['CUDA_VISIBLE_DEVICES'] = "1"
def collate_fn(batch):
input_ids, attention_mask, masks, label, en_count = zip(*batch)
input_ids = pad_sequence(input_ids,batch_first=True,padding_value=0)
attention_mask = pad_sequence(attention_mask,batch_first=True,padding_value=0)
masks = pad_sequence(masks, batch_first=True, padding_value=0)
labels = []
for ele in label:
labels.extend(ele)
labels = torch.tensor(labels,dtype=torch.long)
en_count = torch.stack(en_count,dim=0)
return input_ids, attention_mask, masks, labels, en_count
def dev_validation(dev_loader,device,model):
total_correct = 0
total = 0
model.eval()
with torch.no_grad():
for step, batch in enumerate(tqdm(dev_loader, desc="dev_validation")):
batch = [t.to(device) for t in batch]
inputs = {"input_ids": batch[0], "attention_mask": batch[1], "is_masked": batch[2]}
label = batch[3]
logits = model(inputs)
preds = torch.argmax(logits,dim=1)
correct = (preds==label).sum()
total_correct += correct
total += label.size()[0]
acc = total_correct/total
return acc
def set_seed(seed = 1):
torch.cuda.manual_seed_all(seed)
torch.manual_seed(seed)
torch.backends.cudnn.deterministic = True
if __name__ == '__main__':
set_seed()
log_level = 10
log_path = "logs/train_bert_prompt_AdamW_swa.log"
logger = Logger(log_name='train_bert_prompt', log_level=log_level, log_path=log_path).logger
pretrain_model_path = "./pretrained_models/chinese-bert-wwm-ext"
batch_size = 16
epochs = 10
tokenizer = BertTokenizer.from_pretrained(pretrain_model_path)
config = BertConfig.from_pretrained(pretrain_model_path)
config.dropout = 0.2
config.output_dim = 5
config.batch_size = batch_size
device = "cuda" if torch.cuda.is_available() else "cpu"
model = SentiClassifyBertPrompt.from_pretrained(config=config,pretrained_model_name_or_path = pretrain_model_path)
model.to(device)
optimizer = AdamW(params=model.parameters(),lr=1e-6)
# 随机权重平均SWA,实现更好的泛化
swa_model = AveragedModel(model=model,device=device)
# SWA调整学习率
swa_scheduler = SWALR(optimizer, swa_lr=1e-6)
train_dataset = DataReader(tokenizer=tokenizer, max_langth=512, file_path='./data/train_split.txt')
train_loader = DataLoader(dataset=train_dataset, shuffle=True, batch_size=batch_size, collate_fn=collate_fn)
dev_dataset = DataReader(tokenizer=tokenizer, max_langth=512, file_path='./data/dev_split.txt')
dev_loader = DataLoader(dataset=dev_dataset, shuffle=True, batch_size=batch_size, collate_fn=collate_fn)
for epoch in range(epochs):
model.train()
for step,batch in enumerate(tqdm(train_loader,desc="training")):
batch = [ t.to(device) for t in batch]
inputs = {"input_ids":batch[0],"attention_mask":batch[1],"is_masked":batch[2]}
label = batch[3]
logits = model(inputs)
loss = F.cross_entropy(logits,label)
loss.backward()
optimizer.step()
optimizer.zero_grad()
swa_model.update_parameters(model)
swa_scheduler.step()
acc = dev_validation(dev_loader,device,model)
swa_acc = dev_validation(dev_loader,device,swa_model)
logger.info('Epoch %d acc is %.6f'%(epoch,acc))
logger.info('Epoch %d swa_acc is %.6f' % (epoch, swa_acc))
プロジェクトディレクトリは次のとおりです。
swa-平均体重
上記のトレーニングコードには、これまで見たことも使用したこともないトレーニングトリック(swa)の平均体重があります。その核となるアイデアは、検証ではなく、トレーニングプロセスで保持される最後のモデルであることに言及する必要があります。セットに対する最良の効果は、すべてのエポックによってトレーニングされたモデルの平均重みであるため、トレーニングされたモデルは、最高の一般化能力と最高の効果を持ちます。重量の平均を自分で計算する方法を実装する必要はありません。トーチにはすでに標準化されたプロセスとコードがあります。具体的な効果は実験で検証する必要があります(sgd + swaが効果的であると言う人もいます)。
......
optimizer = AdamW(params=model.parameters(),lr=1e-6)
# 随机权重平均SWA,实现更好的泛化
swa_model = AveragedModel(model=model,device=device)
# SWA调整学习率
swa_scheduler = SWALR(optimizer, swa_lr=1e-6)
for epoch in range(epochs):
model.train()
for step,batch in enumerate(tqdm(train_loader,desc="training")):
......
#正常训练
logits = model(inputs)
loss = F.cross_entropy(logits,label)
loss.backward()
optimizer.step()
optimizer.zero_grad()
#每个epoch后swa_model模型更新参数
swa_model.update_parameters(model)
#调整学习率
swa_scheduler.step()
ベースラインコード
効果を簡単に確認するために、ベースラインスキームも実行しました。コードは次のとおりです。
import torch
from torch.utils.data import Dataset
from tqdm import tqdm
import json
from transformers import BertPreTrainedModel,BertModel
import torch.nn as nn
class SentiClassifyBert(BertPreTrainedModel):
def __init__(self,config):
super(SentiClassifyBert,self).__init__(config)
self.bert = BertModel(config=config)
self.classifier = nn.Sequential(
nn.Linear(config.hidden_size, config.hidden_size),
nn.LayerNorm(config.hidden_size),
nn.LeakyReLU(),
nn.Dropout(p=config.dropout),
nn.Linear(config.hidden_size, config.output_dim),
)
def forward(self,inputs):
inputs = {k: v for k, v in inputs.items() if k in ["input_ids", "attention_mask"]}
outputs = self.bert(**inputs,return_dict=True, output_hidden_states=True)
outputs = outputs.last_hidden_state
cls_output = outputs[:,0:1,:].squeeze()
logits = self.classifier(cls_output)
return logits
class DataReader(Dataset):
def __init__(self,file_path,tokenizer,max_langth):
self.file_path = file_path
self.tokenizer = tokenizer
self.max_length = max_langth
self.data_list = self.texts_tokeniztion()
self.allLength = len(self.data_list)
def texts_tokeniztion(self):
with open(self.file_path,'r',encoding='utf-8') as f:
lines = f.readlines()
res = []
for line in tqdm(lines,desc='texts tokenization'):
line_dic = json.loads(line.strip('\n'))
content = line_dic['content']
entity = line_dic['entity']
for k,v in entity.items():
# 直接最大长度拼接
content = content[0:self.max_length - len(k) - 1 - 10]
text = content + "[SEP]" + k
input_ids, attention_mask, masks = self.text2ids(text)
input_ids = torch.tensor(input_ids, dtype=torch.long)
attention_mask = torch.tensor(attention_mask, dtype=torch.long)
label = torch.tensor(v+2, dtype=torch.long)
temp = []
temp.append(input_ids)
temp.append(attention_mask)
temp.append(label)
res.append(temp)
return res
def text2ids(self,text):
inputs = self.tokenizer(text)
input_ids = inputs['input_ids']
attention_mask = inputs['attention_mask']
masks = [ int(id==self.tokenizer.mask_token_id) for id in input_ids]
return input_ids, attention_mask, masks
def __getitem__(self, item):
input_ids = self.data_list[item][0]
attention_mask = self.data_list[item][1]
label = self.data_list[item][2]
return input_ids, attention_mask, label
from data_reader.reader import DataReader
import torch
from torch.utils.data import DataLoader
from transformers import BertTokenizer,BertConfig
from torch.optim import AdamW,SGD
from model import SentiClassifyBert
from torch.optim.swa_utils import AveragedModel, SWALR
from torch.nn.utils.rnn import pad_sequence
from log.log import Logger
from tqdm import tqdm
import torch.nn.functional as F
import os
os.environ['CUDA_VISIBLE_DEVICES'] = "0"
def collate_fn(batch):
input_ids, attention_mask, label = zip(*batch)
input_ids = pad_sequence(input_ids,batch_first=True,padding_value=0)
attention_mask = pad_sequence(attention_mask,batch_first=True,padding_value=0)
label = torch.stack(label,dim=0)
return input_ids, attention_mask, label
def dev_validation(dev_loader,device,model):
total_correct = 0
total = 0
model.eval()
with torch.no_grad():
for step, batch in enumerate(tqdm(dev_loader, desc="dev_validation")):
batch = [t.to(device) for t in batch]
inputs = {"input_ids": batch[0], "attention_mask": batch[1]}
label = batch[2]
logits = model(inputs)
preds = torch.argmax(logits,dim=1)
correct = (preds==label).sum()
total_correct += correct
total += label.size()[0]
acc = total_correct/total
return acc
def set_seed(seed = 1):
torch.cuda.manual_seed_all(seed)
torch.manual_seed(seed)
torch.backends.cudnn.deterministic = True
if __name__ == '__main__':
set_seed()
log_level = 10
log_path = "logs/train_bert_adamW_swa_20220718.log"
logger = Logger(log_name='train_bert', log_level=log_level, log_path=log_path).logger
pretrain_model_path = "./pretrained_models/chinese-bert-wwm-ext"
batch_size = 16
epochs = 20
tokenizer = BertTokenizer.from_pretrained(pretrain_model_path)
config = BertConfig.from_pretrained(pretrain_model_path)
config.dropout = 0.2
config.output_dim = 5
config.batch_size = batch_size
device = "cuda" if torch.cuda.is_available() else "cpu"
model = SentiClassifyBert.from_pretrained(config=config,pretrained_model_name_or_path = pretrain_model_path)
model.to(device)
optimizer = AdamW(params=model.parameters(),lr=1e-6)
# optimizer = SGD(params=model.parameters(), lr=1e-5,momentum=0.9)
# 随机权重平均SWA,实现更好的泛化
swa_model = AveragedModel(model=model,device=device)
# SWA调整学习率
swa_scheduler = SWALR(optimizer, swa_lr=1e-6)
train_dataset = DataReader(tokenizer=tokenizer, max_langth=512, file_path='./data/train_split.txt')
train_loader = DataLoader(dataset=train_dataset, shuffle=True, batch_size=batch_size, collate_fn=collate_fn)
dev_dataset = DataReader(tokenizer=tokenizer, max_langth=512, file_path='./data/dev_split.txt')
dev_loader = DataLoader(dataset=dev_dataset, shuffle=True, batch_size=batch_size, collate_fn=collate_fn)
for epoch in range(epochs):
model.train()
for step,batch in enumerate(tqdm(train_loader,desc="training")):
batch = [ t.to(device) for t in batch]
inputs = {"input_ids":batch[0],"attention_mask":batch[1]}
label = batch[2]
logits = model(inputs)
loss = F.cross_entropy(logits,label)
loss.backward()
optimizer.step()
optimizer.zero_grad()
swa_model.update_parameters(model)
swa_scheduler.step()
acc = dev_validation(dev_loader,device,model)
swa_acc = dev_validation(dev_loader,device,swa_model)
logger.info('Epoch %d acc is %.6f'%(epoch,acc))
logger.info('Epoch %d swa_acc is %.6f' % (epoch, swa_acc))
トレーニングでは、約9Wデータのトレーニングセットを検証セットとして1Wデータに分割し、Chinese-bert-wwm-extを事前トレーニングモデルとして使用して、20エポックをトレーニングします。SGDとAdamWの効果オプティマイザーを比較します。ベースラインと1位のスキームの効果を比較します。もちろん、テストセットがないため、swaの効果が良いかどうかを結論付けることはできません。
3.エフェクト表示
最初の計画:
a、adamW + swa
検証セットの精度は、AdamWオプティマイザーを使用した20エポック内で0.929579でした。swaは0.928673でした。テストセットでのパフォーマンスはあまり明確ではありません。
b、sgd + swa
精度に関しては、sgdの収束は比較的遅く、19 epcohsの正解率は低く、最高値に達し、正解率はAdamW(89.7)ほど高くはありませんが、完全に収束していません。長い時間がかかりました。AdamWのようなスマートオプティマイザーは、オプティマイザーパラメーターの調整があまり得意ではない私のような人々に適しているようです。
ベースラインスキーム
それに比べて、ベースラインの効果は少し悪いです。1位のソリューションは確かに効果的です。2つの主要なポイントがあります。1つは、データ分布の変化を引き起こす繰り返しのスプライシングがないことです。同時に、モデルエンティティ間の直接的な関係を学習するのに適している可能性があります。2つ目は、文ベクトルの選択がより適切であるということです。埋め込みにはclsもmeanPoolingも選択されていませんが、[MASK]に対応する埋め込みの方が正確です。これは本質的に変更です。事前トレーニングと微調整の間のギャップが小さく、抽出された埋め込みがより正確であり、すべての効果が良好です。
プログラムはエレガントで、学ぶ価値があります!
参考記事