CCF BDCI 劇本角色情感識別:多目標學習開源方案

1、賽題名稱

劇本角色情感識別
比賽鏈接:https://www.datafountain.cn/competitions/518

2、賽題背景

劇本對影視行業的重要性不言而喻。一部好的劇本,不光是好口碑和大流量的基礎,也能帶來更高的商業回報。劇本分析是影視內容生產鏈條的第一環,其中劇本角色的情感識別是一個非常重要的任務,主要是對劇本中每句對白和動作描述中涉及到的每個角色從多個維度進行分析並識別出情感。相對於通常的新聞、評論性文本的情感分析,有其獨有的業務特點和挑戰。

3、賽題任務

本賽題提供一部分電影劇本作爲訓練集,訓練集數據已由人工進行標註,參賽隊伍需要對劇本場景中每句對白和動作描述中涉及到的每個角色的情感從多個維度進行分析和識別。該任務的主要難點和挑戰包括:1)劇本的行文風格和通常的新聞類語料差別較大,更加口語化;2)劇本中角色情感不僅僅取決於當前的文本,對前文語義可能有深度依賴。

4 數據簡介

比賽的數據來源主要是一部分電影劇本,以及愛奇藝標註團隊的情感標註結果,主要用於提供給各參賽團隊進行模型訓練和結果驗證使用。

數據說明

訓練數據:訓練數據爲txt格式,以英文製表符分隔,首行爲表頭,字段說明如下:

字段名稱 類型 描述 說明
id String 數據ID -
content String 文本內容 劇本對白或動作描寫
character String 角色名 文本中提到的角色
emotion String 情感識別結果(按順序) 愛情感值,樂情感值,驚情感值,怒情感值,恐情感值,哀情感值

備註:
  1)本賽題的情感定義共6類(按順序):愛、樂、驚、怒、恐、哀;
  2)情感識別結果:上述6類情感按固定順序對應的情感值,情感值範圍是[0, 1, 2, 3],0-沒有,1-弱,2-中,3-強,以英文半角逗號分隔;
  3)本賽題不需要識別劇本中的角色名;
  文件編碼:UTF-8 無BOM編碼

5 評估標準

本賽題算法評分採用常用的均方根誤差(RMSE)來計算評分,按照“文本內容+角色名”識別出的6類情感對應的情感值來統計。


score = 1/(1 + RMSE)

其中是yi,j預測的情感值,xi,j是標註的情感值,n是總的測試樣本數。
最終按score得分來排名。

6 基於預訓練模型的對目標學習

這個題目可操作的地方有很多,一開始見到這個比賽的時候見想到了multi outputs的模型構建,這裏給大家分享下這個基線,希望有大佬能夠針對這個思路優化上去~

6.1 加載數據

首先讀取數據

with open('data/train_dataset_v2.tsv', 'r', encoding='utf-8') as handler:
    lines = handler.read().split('\n')[1:-1]

    data = list()
    for line in tqdm(lines):
        sp = line.split('\t')
        if len(sp) != 4:
            print("ERROR:", sp)
            continue
        data.append(sp)

train = pd.DataFrame(data)
train.columns = ['id', 'content', 'character', 'emotions']

test = pd.read_csv('data/test_dataset.tsv', sep='\t')
submit = pd.read_csv('data/submit_example.tsv', sep='\t')
train = train[train['emotions'] != '']

提取情感目標

train['emotions'] = train['emotions'].apply(lambda x: [int(_i) for _i in x.split(',')])

train[['love', 'joy', 'fright', 'anger', 'fear', 'sorrow']] = train['emotions'].values.tolist()

6.2 構建數據集

數據集的標籤一共有六個:

class RoleDataset(Dataset):
    def __init__(self,texts,labels,tokenizer,max_len):
        self.texts=texts
        self.labels=labels
        self.tokenizer=tokenizer
        self.max_len=max_len
    def __len__(self):
        return len(self.texts)
    
    def __getitem__(self,item):
        """
        item 爲數據索引,迭代取第item條數據
        """
        text=str(self.texts[item])
        label=self.labels[item]
        
        encoding=self.tokenizer.encode_plus(
            text,
            add_special_tokens=True,
            max_length=self.max_len,
            return_token_type_ids=True,
            pad_to_max_length=True,
            return_attention_mask=True,
            return_tensors='pt',
        )
        
#         print(encoding['input_ids'])
        sample = {
            'texts': text,
            'input_ids': encoding['input_ids'].flatten(),
            'attention_mask': encoding['attention_mask'].flatten()
        }
        for label_col in target_cols:
            sample[label_col] = torch.tensor(label[label_col], dtype=torch.float)
        return sample
        

6.3 模型構建

class EmotionClassifier(nn.Module):
    def __init__(self, n_classes):
        super(EmotionClassifier, self).__init__()
        self.bert = BertModel.from_pretrained(PRE_TRAINED_MODEL_NAME)
        self.out_love = nn.Linear(self.bert.config.hidden_size, n_classes)
        self.out_joy = nn.Linear(self.bert.config.hidden_size, n_classes)
        self.out_fright = nn.Linear(self.bert.config.hidden_size, n_classes)
        self.out_anger = nn.Linear(self.bert.config.hidden_size, n_classes)
        self.out_fear = nn.Linear(self.bert.config.hidden_size, n_classes)
        self.out_sorrow = nn.Linear(self.bert.config.hidden_size, n_classes)
    def forward(self, input_ids, attention_mask):
        _, pooled_output = self.bert(
            input_ids=input_ids,
            attention_mask=attention_mask,
            return_dict = False
        )
        love = self.out_love(pooled_output)
        joy = self.out_joy(pooled_output)
        fright = self.out_fright(pooled_output)
        anger = self.out_anger(pooled_output)
        fear = self.out_fear(pooled_output)
        sorrow = self.out_sorrow(pooled_output)
        return {
            'love': love, 'joy': joy, 'fright': fright,
            'anger': anger, 'fear': fear, 'sorrow': sorrow,
        }

6.4 模型訓練

迴歸損失函數直接選取 nn.MSELoss()

EPOCHS = 1 # 訓練輪數

optimizer = AdamW(model.parameters(), lr=3e-5, correct_bias=False)
total_steps = len(train_data_loader) * EPOCHS

scheduler = get_linear_schedule_with_warmup(
  optimizer,
  num_warmup_steps=0,
  num_training_steps=total_steps
)

loss_fn = nn.MSELoss().to(device)

模型總的loss爲六個目標值的loss之和

def train_epoch(
  model, 
  data_loader, 
  criterion, 
  optimizer, 
  device, 
  scheduler, 
  n_examples
):
    model = model.train()
    losses = []
    correct_predictions = 0
    for sample in tqdm(data_loader):
        input_ids = sample["input_ids"].to(device)
        attention_mask = sample["attention_mask"].to(device)
        outputs = model(
            input_ids=input_ids,
            attention_mask=attention_mask
        )
        loss_love = criterion(outputs['love'], sample['love'].to(device))
        loss_joy = criterion(outputs['joy'], sample['joy'].to(device))
        loss_fright = criterion(outputs['fright'], sample['fright'].to(device))
        loss_anger = criterion(outputs['anger'], sample['anger'].to(device))
        loss_fear = criterion(outputs['fear'], sample['fear'].to(device))
        loss_sorrow = criterion(outputs['sorrow'], sample['sorrow'].to(device))
        loss = loss_love + loss_joy + loss_fright + loss_anger + loss_fear + loss_sorrow
        
        
        losses.append(loss.item())
        loss.backward()
        nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
        optimizer.step()
        scheduler.step()
        optimizer.zero_grad()
#     return correct_predictions.double() / (n_examples*6), np.mean(losses)
    return np.mean(losses)

線上提交0.67+

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章