使用预训练的模型BERT来完成对整个竞赛的数据分析
lf0517
发表于 2023-1-14 22:04:15
1009
0
0
导入需要的库
import numpy as np
import pandas as pd, T: J7 |0 }5 R! h/ H% b
from math import ceil, floor
import tensorflow as tf
import tensorflow.keras.layers as L
from tensorflow.keras.initializers import TruncatedNormal
from sklearn import model_selection4 M* k& t1 C% \4 ~5 I& j4 {; p
from transformers import BertConfig, TFBertPreTrainedModel, TFBertMainLayer8 L; G4 e+ `. U: _4 d, u1 [: v$ z
from tokenizers import BertWordPieceTokenizer" }7 x, N; t, ~# X5 R3 i
读取并解释数据
在竞赛中,对数据的理解是非常关键的。因此我们首先要做的就是读取数据,然后查看数据的内容以及特点。 }1 w' i0 R! Y8 h
先用pandas来读取csv数据,
train_df = pd.read_csv('train.csv')( |0 q3 ~. [; i ]
train_df.dropna(inplace=True), t8 Q8 e4 V4 P. ^0 X; V$ F
test_df = pd.read_csv('test.csv')
test_df.loc[:, "selected_text"] = test_df.text.values9 x8 D( q _& Z2 ]4 C# [* e
submission_df = pd.read_csv('sample_submission.csv')
再查看下我们的数据的数量,我们一共有27485条训练数据,3535条测试数据,/ x4 N1 L" j0 X, N1 i
print("train numbers =", train_df.shape)
print("test numbers =", test_df.shape)
紧接着查看训练数据和测试数据前10条表单的字段跟数据,表单中包含了一下几个数据字段:
textID: 文本数据记录的唯一ID;1 S' E6 v! M4 A; s4 W" O
% B; V" C0 Y8 T. u" g# v- `
text: 原始语句;) d2 u; E {, m- L* ]! R
! l! _$ b/ A7 Q3 a8 H
selected_text: 表示情感的语句;6 K" [+ E H1 S
sentiment: 情感类型, neutral中立, positive积极, negative消极;* {/ P+ B: P& `) ]+ M2 V
. C* u& m+ a' v+ l
0 ~, h1 \3 [/ f3 ^( ^1 r
从数据中我们可以得出,目标就是根据现有的情感从原本是的语句中选出能代表这个情感的语句部分。
train_df.head(10)
test_df.head(10)
定义常量
# bert预训练权重跟数据存放的目录% [. A7 D- u* ^
PATH = "./bert-base-uncased/"( m& c" C ]+ _; M' Z
# 语句最大长度5 v% a5 l# A# T, g* ]
MAX_SEQUENCE_LENGTH = 128. G5 a% H) F" S2 ?
载入词向量: a" D" @$ D- h# P/ g& d
BERT是依据一个固定的词向量来进行训练的。因此在竞赛中需要先使用BertWordPieceTokenizer来加载这些词向量,其中的lowercase=True表示所有的词向量都是小写。设置大小写不敏感可以减少模型对资源的占用。) v5 S. Y# U# Q, P: F
TOKENIZER = BertWordPieceTokenizer(f"{PATH}/vocab.txt", lowercase=True)
定义数据加载器
定义数据预处理函数+ S& J, }! ]) w% e
) H M- W7 @. P+ S' H" ~
def preprocess(tweet, selected_text, sentiment):
' R0 c4 n e. w; u: a( g
# 将被转成byte string的原始字符串转成utf-8的字符串4 O9 R. V, T' ^3 }4 f. M
tweet = tweet.decode('utf-8')5 g# b; m' ^0 f F, m
selected_text = selected_text.decode('utf-8')
sentiment = sentiment.decode('utf-8')
tweet = " ".join(str(tweet).split())% E6 e& p1 Y) D8 E8 h
selected_text = " ".join(str(selected_text).split())# p- ]* _! O% i- v- ?) i
) `, C: P3 h9 I) U9 s
# 标记出selected text和text共有的单词1 h% m* N8 o( y+ j/ V3 h. x+ y
idx_start, idx_end = None, None
for index in (i for i, c in enumerate(tweet) if c == selected_text[0]):" I1 [& r4 Q$ O( v. n9 r9 z% V$ Y
if tweet[index:index+len(selected_text)] == selected_text:3 A; \. g( e0 [& Y9 w2 G4 z
idx_start = index' C+ o7 h! T6 g6 U
idx_end = index + len(selected_text)' Q6 r, B* l# S' D
break
intersection = [0] * len(tweet)& n0 v+ c: ]# i' v5 l- |2 x8 S
if idx_start != None and idx_end != None:
for char_idx in range(idx_start, idx_end):
intersection[char_idx] = 1
# 对原始数据用词向量进行编码, 这里会返回原始数据中的词在词向量中的下标
# 和原始数据中每个词向量的单词在文中的起始位置跟结束位置
enc = TOKENIZER.encode(tweet)$ m: Y, r- y& D
input_ids_orig, offsets = enc.ids, enc.offsets
target_idx = [], F! E" W2 S5 {8 d. c
for i, (o1, o2) in enumerate(offsets):
if sum(intersection[o1: o2]) > 0:
target_idx.append(i)
target_start = target_idx[0]
target_end = target_idx[-1]2 u( A/ P1 L1 V, l; h
sentiment_map = {
'positive': 3893,
'negative': 4997,! e; Z! Y+ I2 }0 p
'neutral': 8699,& q4 ?5 s5 g, L+ {
}8 K3 Y# C0 K5 b3 E$ ?
/ F( E8 {$ g3 d2 K u) V) r
# 将情感标签和原始的语句的词向量组合在一起组成我们新的数据
input_ids = [101] + [sentiment_map[sentiment]] + [102] + input_ids_orig + [102]
input_type_ids = [0] * (len(input_ids_orig) + 4)3 @. t- R' f- ?. S
attention_mask = [1] * (len(input_ids_orig) + 4)+ s4 H a b7 F7 {
offsets = [(0, 0), (0, 0), (0, 0)] + offsets + [(0, 0)]7 C0 F2 u' W1 z, {( s
target_start += 3
target_end += 3' N/ v) O1 C- }1 Q: r0 D E
# 计算需要paddning的长度, BERT是以固定长度进行输入的,因此对于不足的我们需要做pandding
padding_length = MAX_SEQUENCE_LENGTH - len(input_ids)4 C. ~ H, B9 }" E; n/ n
if padding_length > 0:
input_ids = input_ids + ([0] * padding_length)
attention_mask = attention_mask + ([0] * padding_length)
input_type_ids = input_type_ids + ([0] * padding_length)
offsets = offsets + ([(0, 0)] * padding_length)0 L; |* y p& J5 |
elif padding_length ) F" ~5 M# A0 @+ S$ a3 \
定义数据加载器
class TweetDataset(tf.data.Dataset):
outputTypes = (
tf.dtypes.int32, tf.dtypes.int32, tf.dtypes.int32,
tf.dtypes.int32, tf.dtypes.float32, tf.dtypes.float32,
tf.dtypes.string, tf.dtypes.string, tf.dtypes.string,
)& u1 y( w$ ] C4 |9 z6 M& t: b# i- _) V" e
% V3 f7 Z0 y1 w
outputShapes = (
(128,), (128,), (128,), 4 w2 k! T r n. k# H, E
(128, 2), (), (),
(), (), (),- ^6 b# j- R7 B# Z) l7 S
)
def _generator(tweet, selected_text, sentiment):" D3 n. ]$ i1 \3 m+ l% C g
for tw, st, se in zip(tweet, selected_text, sentiment):* I0 }. ^% R2 Z0 V
yield preprocess(tw, st, se)
def __new__(cls, tweet, selected_text, sentiment):7 c! M N) Q, r2 f, K2 n
return tf.data.Dataset.from_generator(
cls._generator, k% C7 V; K2 S8 n
output_types=cls.outputTypes,
output_shapes=cls.outputShapes,8 K/ L+ Q. B- H" l
args=(tweet, selected_text, sentiment). m( u3 A5 G# V3 l! R2 @
)
0 g5 M h: t* ?) Q7 O# _0 |( C9 M! y
@staticmethod" X5 [2 Q8 R1 u! D
def create(dataframe, batch_size, shuffle_buffer_size=-1):& c8 a; Z5 O/ \) h
dataset = TweetDataset(
dataframe.text.values, - y0 O- [3 o" Q# B8 F+ f3 Y
dataframe.selected_text.values, 4 d" M) E( L% r i5 _
dataframe.sentiment.values) ]; U7 V0 u p/ S' e8 k& d. E
)
dataset = dataset.cache()
if shuffle_buffer_size != -1:5 n, i# C* L* I0 s& d7 b% Y: Z
dataset = dataset.shuffle(shuffle_buffer_size)& \5 _. w, M& k _. e
dataset = dataset.batch(batch_size)
dataset = dataset.prefetch(tf.data.experimental.AUTOTUNE)
return dataset0 z& p8 [5 o( s3 h6 c' F- n, b
定义模型
我们使用BERT模型来进行这次竞赛,这里对BERT模型做一些简单的介绍。
BERT的全称是Bidirectional Encoder Representation from Transformers,即双向Transformer的Encoder,因为decoder是不能获要预测的信息的。4 ^( a' i8 A% E w! K
模型的主要创新点都在pre-train方法上,即用了Masked LM和Next Sentence Prediction两种方法分别捕捉词语和句子级别representation。
BERT主要特点如下:2 `/ u$ _7 k, z
使用了Transformer作为算法的主要框架,Trabsformer能更彻底的捕捉语句中的双向关系;: r0 F" X% C k# r% u
- S* C6 I, @1 w; j0 [$ [
使用了Mask Language Model 和 Next Sentence Prediction的多任务训练目标;
- c7 M; E' g( Y) c# l- l
使用更强大的机器训练更大规模的数据,Google开源了BERT模型,我们可以直接使用BERT作为Word2Vec的转换矩阵并高效的将其应用到自己的任务中。/ E: G& U$ d2 y
BERT的本质是在海量的语料基础上,运行自监督学习方法让单词学习得到一个较好的特征表示。3 \( v* A6 d$ g. G' `
在之后特定任务中,可以直接使用BERT的特征表示作为该任务的词嵌入特征。所以BERT提供的是一个供其它任务迁移学习的模型,该模型可以根据任务微调或者固定之后作为特征提取器。
在竞赛中,我们定义了一个BertModel类,里面使用TFBertPreTrainedModel来进行推理。
BERT的输出我们保存在hidden_states中,然后将这个得到的hidden_states结果在加入到Dense Layer,最后输出我们需要提取的表示情感的文字的起始位置跟结束位置。$ |+ D& J5 }4 x$ Y/ K: N: k8 B
这两个位置信息就是我们需要从原文中提取的词向量的位置。
class BertModel(TFBertPreTrainedModel):
# drop out rate, 防止过拟合
dr = 0.1* C& S+ v- u" v9 _* H3 q* b. o: \
# hidden state数量+ J# g6 X& }0 L. e
hs = 28 f" `2 u% Y \8 [+ e6 B
def __init__(self, config, *inputs, **kwargs):
super().__init__(config, *inputs, **kwargs)- z2 S; s4 I4 w0 r
% \, m n$ J2 k. l" `* z
self.bert = TFBertMainLayer(config, name="bert")
self.concat = L.Concatenate()7 ^1 F3 [& K4 v* m: p
self.dropout = L.Dropout(self.dr)
self.qa_outputs = L.Dense(: V9 E/ r2 L' p; u' C! Q4 K6 L
config.num_labels,
kernel_initializer=TruncatedNormal(stddev=config.initializer_range),
dtype='float32',5 }- {" Z- ` L4 t' B
name="qa_outputs")
@tf.function
def call(self, inputs, **kwargs):/ L6 \+ J) ?8 J6 R
_, _, hidden_states = self.bert(inputs, **kwargs)6 U( ?* w1 z$ Z! e0 l
" P7 ]. R& M( \6 x6 x5 {- p
hidden_states = self.concat([4 h) u I$ B! {" m4 C3 A
hidden_states[-i] for i in range(1, self.hs+1)
])
. }; p, r( {/ D' x+ }$ ~+ y$ f% _9 ^
hidden_states = self.dropout(hidden_states, training=kwargs.get("training", False))
logits = self.qa_outputs(hidden_states)
start_logits, end_logits = tf.split(logits, 2, axis=-1), ]4 N; q7 m( x& u- \1 R* Z
start_logits = tf.squeeze(start_logits, axis=-1)5 S0 f$ T& k z7 ?" G. _4 L( Y
end_logits = tf.squeeze(end_logits, axis=-1)8 J; G& }/ {5 u1 d% L, ^, x
( ]+ [4 W' [+ [( q% P# t; n
return start_logits, end_logits
定义训练函数1 W0 P. I7 S+ `% ~
def train(model, dataset, loss_fn, optimizer):
@tf.function# z5 s/ b0 ^2 a0 v( I5 p7 p5 n
def train_step(model, inputs, y_true, loss_fn, optimizer):# q* L1 F0 d/ M' @+ V! R/ c
with tf.GradientTape() as tape:, o8 q0 X4 I4 a& {4 {) ^$ ~
y_pred = model(inputs, training=True): q: R+ \9 y" o( ^( M2 X1 n# A
loss = loss_fn(y_true[0], y_pred[0])
loss += loss_fn(y_true[1], y_pred[1])( X9 B* k% X9 `& v
scaled_loss = optimizer.get_scaled_loss(loss)
/ N) f! E: h, n
scaled_gradients = tape.gradient(scaled_loss, model.trainable_variables)
gradients = optimizer.get_unscaled_gradients(scaled_gradients)# [6 X2 o2 w3 Q
optimizer.apply_gradients(zip(gradients, model.trainable_variables))
return loss, y_pred& p: U$ \( e% z0 R5 t
epoch_loss = 0.9 \% U$ o, K I' f
for batch_num, sample in enumerate(dataset):
loss, y_pred = train_step(model, sample[:3], sample[4:6], loss_fn, optimizer)
epoch_loss += loss
print(
f"training ... batch {batch_num+1:03d} : "
f"train loss {epoch_loss/(batch_num+1):.3f} ",
end='\r')
定义预制函数
def predict(model, dataset, loss_fn, optimizer):8 p2 h. {) R5 q+ A6 d
@tf.function
def predict_step(model, inputs):6 \$ F& h$ s$ v% {2 \
return model(inputs)
def to_numpy(*args):! R% p/ E# M, V7 ?2 A& t
out = []
for arg in args:
if arg.dtype == tf.string:
arg = [s.decode('utf-8') for s in arg.numpy()]! a7 x' ]' D9 C1 v1 u. }6 \) B, d
out.append(arg)
else:
arg = arg.numpy()- |3 ]1 Z4 a+ \2 J
out.append(arg)$ F1 H% Q5 M& `% b
return out
offset = tf.zeros([0, 128, 2], dtype=tf.dtypes.int32)
text = tf.zeros([0,], dtype=tf.dtypes.string)
selected_text = tf.zeros([0,], dtype=tf.dtypes.string)6 T& a y6 }1 l3 R8 I3 M/ A" ?
sentiment = tf.zeros([0,], dtype=tf.dtypes.string)
pred_start = tf.zeros([0, 128], dtype=tf.dtypes.float32)/ v0 M6 U) A! n5 x$ n
pred_end = tf.zeros([0, 128], dtype=tf.dtypes.float32)
for batch_num, sample in enumerate(dataset):
( `- c: ~3 |" ^" l' \) d
print(f"predicting ... batch {batch_num+1:03d}"+" "*20, end='\r')! L& n; w1 r: C
* e2 k9 N! M! J8 R4 x. f: [- s9 D
y_pred = predict_step(model, sample[:3])
# C8 s' k5 |8 m" j' ]7 [ y/ h
# add batch to accumulators, P1 l' F3 W3 o. T$ f+ b! N
pred_start = tf.concat((pred_start, y_pred[0]), axis=0)
pred_end = tf.concat((pred_end, y_pred[1]), axis=0)+ X7 @1 N, f- L( W
offset = tf.concat((offset, sample[3]), axis=0)) }' p( X# X. l2 y: r3 c' G
text = tf.concat((text, sample[6]), axis=0)# j+ I/ u$ x9 z4 j9 @4 N
selected_text = tf.concat((selected_text, sample[7]), axis=0)
sentiment = tf.concat((sentiment, sample[8]), axis=0)( r- r) r% g/ ]) c; k
5 J1 j$ q) H! ]* r% ]9 u, n
pred_start, pred_end, text, selected_text, sentiment, offset = \
to_numpy(pred_start, pred_end, text, selected_text, sentiment, offset)( G' d$ j, G' k4 b4 j* T
return pred_start, pred_end, text, selected_text, sentiment, offset
判断函数 W; R0 R1 P; l! r
这个竞赛采用单词级Jaccard系数,计算公式如下
3 b2 A9 k" Z& c# [7 _
Jaccard系数计算的是你预测的单词在数据集中的个数,! S& I3 A* A2 c, v
def jaccard(str1, str2):
a = set(str1.lower().split())5 J0 \3 W) N. I- B3 e- y
b = set(str2.lower().split())5 n' ?" c# m8 U. v' a) o0 [" X4 g
c = a.intersection(b)
return float(len(c)) / (len(a) + len(b) - len(c))% a0 B# p/ c2 P5 R8 t
定义预测结果解码函数
解码函数通过模型预测拿到的start和end的index位置信息,然后和之前拿到的词向量在样本句子中的位置进行比较,将这个区间内的所有的单词都提取出来作为我们的预测结果。- t- p Q8 o" d
% }5 i) D1 d- _8 w
def decode_prediction(pred_start, pred_end, text, offset, sentiment):
def decode(pred_start, pred_end, text, offset): k6 G! F: o* @* V( g7 h, ^ {7 z
decoded_text = ""
for i in range(pred_start, pred_end+1):3 s9 v$ r8 q7 u
decoded_text += text[offset[0]:offset[1]]
if (i+1) idx_end:
idx_end = idx_start
decoded_text = str(decode(idx_start, idx_end, text, offset))
if len(decoded_text) == 0:& f) ~4 V( K0 S; [$ D" i2 G
decoded_text = text# i) y! J/ @8 |2 i' }; f# S. Y, i
decoded_predictions.append(decoded_text)
return decoded_predictions y0 o6 F. ~" E5 `! T) M4 }5 |1 c; {
开始训练7 I# v2 B4 ]7 Y* m! H* {; ^
将训练数据分成5个folds,每个folds训练5个epoch,使用adam优化器,learning rate设置成3e-5,batch size使用32。
num_folds = 59 E( N, T' V$ `3 }9 [, Q4 d
num_epochs = 5% ~# A: f' D5 A0 E
batch_size = 329 [: A" l6 } r6 l1 N
learning_rate = 3e-5
optimizer = tf.keras.optimizers.Adam(learning_rate)
optimizer = tf.keras.mixed_precision.experimental.LossScaleOptimizer(( e. J( \3 E3 _2 G( g
optimizer, 'dynamic')* B% B8 \# H! L* h8 G5 |- N
config = BertConfig(output_hidden_states=True, num_labels=2)
model = BertModel.from_pretrained(PATH, config=config)
loss_fn = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)1 }3 w8 K+ a2 M- w. {1 S
kfold = model_selection.KFold(- I5 o! Z' I3 d) e/ y0 h" }) l1 r4 o
n_splits=num_folds, shuffle=True, random_state=42)
test_preds_start = np.zeros((len(test_df), 128), dtype=np.float32)
test_preds_end = np.zeros((len(test_df), 128), dtype=np.float32)% a8 ~% W9 G/ `
for fold_num, (train_idx, valid_idx) in enumerate(kfold.split(train_df.text)):' X+ r- b+ t- u! Z
print("\nfold %02d" % (fold_num+1))" @+ p* K9 u* M) T( e) I ]
# 创建train, valid, test数据集0 x, J i$ V9 q; t0 @8 }- o4 N, S
train_dataset = TweetDataset.create(2 V; D0 `) p9 Q7 J% Z( ]
train_df.iloc[train_idx], batch_size, shuffle_buffer_size=2048)+ A$ M, s2 P: x2 @( z
valid_dataset = TweetDataset.create(
train_df.iloc[valid_idx], batch_size, shuffle_buffer_size=-1)/ [' `- s# D+ z d
test_dataset = TweetDataset.create(
test_df, batch_size, shuffle_buffer_size=-1)
6 O: o7 Q8 h7 j" M2 z5 y
best_score = float('-inf')
for epoch_num in range(num_epochs):) m0 o4 V) ?# B' P* A, g' j2 N
print("\nepoch %03d" % (epoch_num+1))
train(model, train_dataset, loss_fn, optimizer)) N( _; h0 U& ]$ ^2 x
$ P, F R, O6 }* K0 _& V2 D
pred_start, pred_end, text, selected_text, sentiment, offset = \
predict(model, valid_dataset, loss_fn, optimizer)
selected_text_pred = decode_prediction(
pred_start, pred_end, text, offset, sentiment)
jaccards = []
for i in range(len(selected_text)):
jaccards.append(
jaccard(selected_text, selected_text_pred)) r) ^2 D+ M& ~! I" F
score = np.mean(jaccards)3 P' U3 ?5 {1 y# u* N) \4 @$ N
print(f"valid jaccard epoch {epoch_num+1:03d}: {score}"+" "*15)1 U: }. V( G9 V$ J: G$ h
) ]' ?- q- I; ]) i
if score > best_score:" P; M/ {4 S, @0 H0 |4 ~
best_score = score
* `6 T7 d# I7 [
# predict test set! n* c6 n T9 {- I
test_pred_start, test_pred_end, test_text, _, test_sentiment, test_offset = \
predict(model, test_dataset, loss_fn, optimizer)
6 @+ _. P- V$ w- Y% h7 J/ l
test_preds_start += test_pred_start * 0.2
test_preds_end += test_pred_end * 0.2
# 重置模型,避免OOM( L" @$ P; x% Q: i* j& \% A% Y
session = tf.compat.v1.get_default_session()
graph = tf.compat.v1.get_default_graph()
del session, graph, model5 I0 P }7 |; S9 r6 {7 k" M
model = BertModel.from_pretrained(PATH, config=config)& c: K; P; ]$ y& W3 r: P
预测测试数据,并生成提交文件/ x. J3 m: m5 ?9 o+ y, J
selected_text_pred = decode_prediction(
test_preds_start, test_preds_end, test_text, test_offset, test_sentiment)
def f(selected): P* Z5 M- o$ G% O5 c! a9 m0 ~! R
return " ".join(set(selected.lower().split()))
submission_df.loc[:, 'selected_text'] = selected_text_pred
submission_df['selected_text'] = submission_df['selected_text'].map(f)
submission_df.to_csv("submission.csv", index=False) r. @) W' Q! z' X
这个方案在提交的时候在553个队伍中排名153位, 分数为0.68。
成为第一个吐槽的人