使用预训练的模型BERT来完成对整个竞赛的数据分析
lf0517
发表于 2023-1-14 22:04:15
979
0
0
导入需要的库6 Y7 u5 V/ f1 T$ I" }$ P
import numpy as np: o) q: `' d; K, T4 B
import pandas as pd
from math import ceil, floor
import tensorflow as tf4 P7 s' N& K5 o: ^ b
import tensorflow.keras.layers as L' B- V9 b. T2 V9 n' q% P: K
from tensorflow.keras.initializers import TruncatedNormal
from sklearn import model_selection
from transformers import BertConfig, TFBertPreTrainedModel, TFBertMainLayer! n8 K# y7 q0 F g( T0 ~
from tokenizers import BertWordPieceTokenizer( F( Q% ?6 o* y3 x0 X3 k1 R
读取并解释数据
在竞赛中,对数据的理解是非常关键的。因此我们首先要做的就是读取数据,然后查看数据的内容以及特点。
先用pandas来读取csv数据,
train_df = pd.read_csv('train.csv')
train_df.dropna(inplace=True)
test_df = pd.read_csv('test.csv'). G" w; w) l! N' ]
test_df.loc[:, "selected_text"] = test_df.text.values
submission_df = pd.read_csv('sample_submission.csv')
再查看下我们的数据的数量,我们一共有27485条训练数据,3535条测试数据,
print("train numbers =", train_df.shape)6 c) D; j. p3 E" Z, q. b
print("test numbers =", test_df.shape)
紧接着查看训练数据和测试数据前10条表单的字段跟数据,表单中包含了一下几个数据字段:
textID: 文本数据记录的唯一ID;5 }3 g3 z/ |% ~( v+ p2 i
text: 原始语句;! X& t$ |, Z. f" x2 t
. k* A& G8 _* M: p; ~: G
selected_text: 表示情感的语句;; ]8 h2 j' I# k9 A3 i K
sentiment: 情感类型, neutral中立, positive积极, negative消极;
$ [. o4 x5 |6 ^/ x2 D
从数据中我们可以得出,目标就是根据现有的情感从原本是的语句中选出能代表这个情感的语句部分。5 J! e6 S, o8 _! \7 ]# _' |, t
train_df.head(10)+ N" O4 f( u8 i9 C( T8 ]8 F P. d
test_df.head(10)
定义常量+ e& F; _; f- G, s1 D: i# i: O* e" ]
# bert预训练权重跟数据存放的目录0 ~3 Z+ C& f2 Z v" U$ }. n2 Z" i
PATH = "./bert-base-uncased/". q: Q3 P1 T+ o
# 语句最大长度, U9 a, ^7 H A3 \
MAX_SEQUENCE_LENGTH = 128! j J4 i$ I7 l1 Z/ Z+ |) B* O* h
载入词向量9 _# B5 f/ h. t% J
BERT是依据一个固定的词向量来进行训练的。因此在竞赛中需要先使用BertWordPieceTokenizer来加载这些词向量,其中的lowercase=True表示所有的词向量都是小写。设置大小写不敏感可以减少模型对资源的占用。. N: s+ H5 ^: f$ G; i
TOKENIZER = BertWordPieceTokenizer(f"{PATH}/vocab.txt", lowercase=True)5 [- Q( v0 V. k
定义数据加载器
定义数据预处理函数0 `! Y% `3 U* @4 W1 r
def preprocess(tweet, selected_text, sentiment):( r* z/ ?! w9 b' O% j( e; e
# 将被转成byte string的原始字符串转成utf-8的字符串
tweet = tweet.decode('utf-8')) p8 G3 J3 Z0 ~, \/ j. K1 L" O
selected_text = selected_text.decode('utf-8')
sentiment = sentiment.decode('utf-8')
tweet = " ".join(str(tweet).split())
selected_text = " ".join(str(selected_text).split())" ^8 Q+ o1 ` t! [3 M3 S6 |1 T
# 标记出selected text和text共有的单词
idx_start, idx_end = None, None3 F" K( Q! S; \% o! P m2 f
for index in (i for i, c in enumerate(tweet) if c == selected_text[0]):
if tweet[index:index+len(selected_text)] == selected_text:
idx_start = index
idx_end = index + len(selected_text)
break
intersection = [0] * len(tweet)
if idx_start != None and idx_end != None:
for char_idx in range(idx_start, idx_end):
intersection[char_idx] = 1
# 对原始数据用词向量进行编码, 这里会返回原始数据中的词在词向量中的下标0 r6 |0 B Q: r7 x( z1 D* s
# 和原始数据中每个词向量的单词在文中的起始位置跟结束位置9 X4 O6 G7 r2 L- o2 _
enc = TOKENIZER.encode(tweet)
input_ids_orig, offsets = enc.ids, enc.offsets
target_idx = []
for i, (o1, o2) in enumerate(offsets):! S/ K! Q4 t6 E# E) _7 u6 w- V2 k
if sum(intersection[o1: o2]) > 0:
target_idx.append(i)
target_start = target_idx[0]8 }2 R5 [/ J+ r5 n: L0 J' `8 c- w
target_end = target_idx[-1]
sentiment_map = {$ I) e. T0 E* R# F
'positive': 3893,' ^3 [5 C4 ]1 G. X( v* w: K
'negative': 4997,
'neutral': 8699,- V8 N3 c+ f( o. Y
}
# 将情感标签和原始的语句的词向量组合在一起组成我们新的数据
input_ids = [101] + [sentiment_map[sentiment]] + [102] + input_ids_orig + [102]$ b8 |* d9 c8 m" d8 z
input_type_ids = [0] * (len(input_ids_orig) + 4)
attention_mask = [1] * (len(input_ids_orig) + 4)) [. O/ u$ w; z3 \8 R i3 M
offsets = [(0, 0), (0, 0), (0, 0)] + offsets + [(0, 0)]
target_start += 3
target_end += 35 R5 T1 N; M0 f# c7 `2 r" I2 ]* o$ I
# 计算需要paddning的长度, BERT是以固定长度进行输入的,因此对于不足的我们需要做pandding2 D& r$ O5 A& _5 Y
padding_length = MAX_SEQUENCE_LENGTH - len(input_ids)* `' L% C* g: P6 i! E: A: c- W6 i
if padding_length > 0:) b9 Z. F- z* b5 O& K$ ^0 E
input_ids = input_ids + ([0] * padding_length)
attention_mask = attention_mask + ([0] * padding_length)" Y: q, p2 A5 j3 ^8 o$ G1 @* b
input_type_ids = input_type_ids + ([0] * padding_length)
offsets = offsets + ([(0, 0)] * padding_length)
elif padding_length
定义数据加载器
class TweetDataset(tf.data.Dataset):0 p: u3 |+ J* J
! O( B* k0 P, W4 y! B; E
outputTypes = (" e' U6 _( j7 @/ ]
tf.dtypes.int32, tf.dtypes.int32, tf.dtypes.int32, & ^) X( @3 Z8 E5 Q' ~6 g4 U, z
tf.dtypes.int32, tf.dtypes.float32, tf.dtypes.float32,. Q* M" e1 Z2 S! w) X+ R2 ^$ [
tf.dtypes.string, tf.dtypes.string, tf.dtypes.string,
); g0 }) k8 f G$ {, E
outputShapes = () v& x8 [/ _# T& C' M8 n, z8 r3 J
(128,), (128,), (128,), " a$ r: x5 ]/ G& c. j$ \" q* u+ l
(128, 2), (), (),
(), (), (),( }; O/ _ l6 h. b9 m- `: E% C h
)& U1 f6 H% v/ ]4 _ Y. {, T& r
def _generator(tweet, selected_text, sentiment):* \" t& p. R( k, A- ?
for tw, st, se in zip(tweet, selected_text, sentiment):- k0 w& a ]! R) x( |' {. n5 r
yield preprocess(tw, st, se)
def __new__(cls, tweet, selected_text, sentiment):2 |: Y0 W' N. j! b
return tf.data.Dataset.from_generator(! r. Q9 G4 P q7 S" @
cls._generator,
output_types=cls.outputTypes,
output_shapes=cls.outputShapes,3 h, c% Q: h/ }0 k' q+ B, h) t8 p
args=(tweet, selected_text, sentiment)
) ]1 A' c# }! K u. g
@staticmethod
def create(dataframe, batch_size, shuffle_buffer_size=-1):- i# W5 n" S7 \8 ^5 q4 u1 u
dataset = TweetDataset( B0 U$ @, S( k' S
dataframe.text.values,
dataframe.selected_text.values, 9 a8 ]8 F3 f$ P
dataframe.sentiment.values1 O- o6 U, F" k$ g% j9 @
)9 Z( h1 \) Q& }/ o4 F5 I- Q" R
dataset = dataset.cache()
if shuffle_buffer_size != -1:( w4 _4 T/ N7 `9 X
dataset = dataset.shuffle(shuffle_buffer_size)& J- ?/ S# \0 I" l; r% O5 T
dataset = dataset.batch(batch_size): ~: T5 X$ o9 z1 g: [, R5 t$ t* t
dataset = dataset.prefetch(tf.data.experimental.AUTOTUNE)* p$ Y/ u# s% k% w! A6 c! V* z
return dataset
定义模型
我们使用BERT模型来进行这次竞赛,这里对BERT模型做一些简单的介绍。0 l/ A6 B+ P) f6 q% h% |+ B5 {9 @
BERT的全称是Bidirectional Encoder Representation from Transformers,即双向Transformer的Encoder,因为decoder是不能获要预测的信息的。
模型的主要创新点都在pre-train方法上,即用了Masked LM和Next Sentence Prediction两种方法分别捕捉词语和句子级别representation。( |) ?2 h, N3 a8 j
BERT主要特点如下:
Z7 N0 V, W! I& k. b0 v: F$ c& |
使用了Transformer作为算法的主要框架,Trabsformer能更彻底的捕捉语句中的双向关系;" ~/ V& r' A. P p4 ~1 ~, J
& O& h- i2 X) n- G' k
使用了Mask Language Model 和 Next Sentence Prediction的多任务训练目标;) [% w7 Q; u$ ]/ X+ t) {/ H9 }
使用更强大的机器训练更大规模的数据,Google开源了BERT模型,我们可以直接使用BERT作为Word2Vec的转换矩阵并高效的将其应用到自己的任务中。& }7 I' b: p0 K
BERT的本质是在海量的语料基础上,运行自监督学习方法让单词学习得到一个较好的特征表示。
在之后特定任务中,可以直接使用BERT的特征表示作为该任务的词嵌入特征。所以BERT提供的是一个供其它任务迁移学习的模型,该模型可以根据任务微调或者固定之后作为特征提取器。
在竞赛中,我们定义了一个BertModel类,里面使用TFBertPreTrainedModel来进行推理。$ [5 [# ^/ x& D7 W# Y
BERT的输出我们保存在hidden_states中,然后将这个得到的hidden_states结果在加入到Dense Layer,最后输出我们需要提取的表示情感的文字的起始位置跟结束位置。
这两个位置信息就是我们需要从原文中提取的词向量的位置。
class BertModel(TFBertPreTrainedModel):
# drop out rate, 防止过拟合% E* m" l! R' ]* I: b5 o
dr = 0.1
# hidden state数量1 ]1 y3 t3 p% A. t! @
hs = 28 R1 C# k9 [$ W
3 q% @- Q( }5 D' o& v
def __init__(self, config, *inputs, **kwargs):
super().__init__(config, *inputs, **kwargs)
self.bert = TFBertMainLayer(config, name="bert")
self.concat = L.Concatenate()* E! t8 ]3 b. _5 r$ N" D. e
self.dropout = L.Dropout(self.dr)3 V0 l) M: ?. j# R5 H
self.qa_outputs = L.Dense(! v* g& q. N; u4 u# c1 c5 d
config.num_labels, & p4 x: _8 L; j# N
kernel_initializer=TruncatedNormal(stddev=config.initializer_range),
dtype='float32',4 V( l" g2 l- g" a
name="qa_outputs")
6 t- q7 h0 G+ w5 e+ O1 m
@tf.function
def call(self, inputs, **kwargs):" g2 Y7 C' e' ^: M* m
_, _, hidden_states = self.bert(inputs, **kwargs)/ n2 y+ M1 g/ |& e1 [' |/ y+ h
hidden_states = self.concat([
hidden_states[-i] for i in range(1, self.hs+1)1 n @5 u" }2 d* l
]): x$ [: V# |' w% W7 a. Y
hidden_states = self.dropout(hidden_states, training=kwargs.get("training", False)), e9 N9 i/ u- h9 R, l
logits = self.qa_outputs(hidden_states)* W- {" b2 N* R. b
start_logits, end_logits = tf.split(logits, 2, axis=-1)
start_logits = tf.squeeze(start_logits, axis=-1)
end_logits = tf.squeeze(end_logits, axis=-1)
3 [3 Y1 [9 S$ [
return start_logits, end_logits1 O/ C7 R5 f9 G1 d
定义训练函数
( P/ W7 e" J8 |7 X) @
def train(model, dataset, loss_fn, optimizer):
, c+ G. k0 |+ y+ |9 L+ E& l0 G
@tf.function
def train_step(model, inputs, y_true, loss_fn, optimizer):7 L, Y' Y# w, [' I
with tf.GradientTape() as tape:
y_pred = model(inputs, training=True)
loss = loss_fn(y_true[0], y_pred[0]): `' L' r8 W- |
loss += loss_fn(y_true[1], y_pred[1])
scaled_loss = optimizer.get_scaled_loss(loss)8 A6 [* W z/ q6 U* Q
scaled_gradients = tape.gradient(scaled_loss, model.trainable_variables)0 o; |2 ^) z& W3 h6 S5 i
gradients = optimizer.get_unscaled_gradients(scaled_gradients)
optimizer.apply_gradients(zip(gradients, model.trainable_variables))5 B& t, }( @4 X, |3 }; d9 H
return loss, y_pred
epoch_loss = 0.
for batch_num, sample in enumerate(dataset):
loss, y_pred = train_step(model, sample[:3], sample[4:6], loss_fn, optimizer)7 z% t1 z2 U& X9 G
epoch_loss += loss) a' l& \2 W h. [
print(
f"training ... batch {batch_num+1:03d} : "$ ~5 r. j: x0 K
f"train loss {epoch_loss/(batch_num+1):.3f} ",
end='\r')
定义预制函数( b3 K8 s; V2 h! r- d
def predict(model, dataset, loss_fn, optimizer):! y; t. {' c9 O
@tf.function
def predict_step(model, inputs):' p `1 J. @7 k( u9 F- S
return model(inputs)+ Y: F$ `: }, n3 V; ^
def to_numpy(*args):
out = []2 J h. [ T' _6 G, Y7 y
for arg in args:* F! D! y5 p9 |& Z# v3 [+ Y
if arg.dtype == tf.string:! q0 X% N5 ^$ ]. ?/ _+ N
arg = [s.decode('utf-8') for s in arg.numpy()]
out.append(arg)
else:& O. \$ \- f& i: _4 E9 [1 @) W
arg = arg.numpy()/ }1 T# G# C A& m
out.append(arg)1 Q) H8 Z2 ~$ D- ]* `: ~6 o) J4 l# Q
return out. a+ u# t& F) N
1 D. j1 E6 C) ^ k4 B5 G
offset = tf.zeros([0, 128, 2], dtype=tf.dtypes.int32)/ h" Q) E8 E( K+ O( x$ i
text = tf.zeros([0,], dtype=tf.dtypes.string)
selected_text = tf.zeros([0,], dtype=tf.dtypes.string)
sentiment = tf.zeros([0,], dtype=tf.dtypes.string)3 g% o; n+ O7 ]. B1 P6 T
pred_start = tf.zeros([0, 128], dtype=tf.dtypes.float32)* Y( [3 r6 R5 y! U7 ]9 e+ j1 f; m3 x
pred_end = tf.zeros([0, 128], dtype=tf.dtypes.float32)
8 p: W) w6 R. u
for batch_num, sample in enumerate(dataset):
; b& |/ u: v6 \' l+ Y
print(f"predicting ... batch {batch_num+1:03d}"+" "*20, end='\r')
* a4 a6 e; m2 C1 E8 w$ j
y_pred = predict_step(model, sample[:3]). ]9 x6 s5 a" d0 }
8 L! w0 |/ D# g7 i' V
# add batch to accumulators1 W( |( g9 u0 g2 N/ ]
pred_start = tf.concat((pred_start, y_pred[0]), axis=0)! a- { G- W& P9 `- o2 j3 z' N
pred_end = tf.concat((pred_end, y_pred[1]), axis=0)
offset = tf.concat((offset, sample[3]), axis=0)
text = tf.concat((text, sample[6]), axis=0)- |6 Z& B' C2 i. D
selected_text = tf.concat((selected_text, sample[7]), axis=0). i4 f* ~) [1 g9 {
sentiment = tf.concat((sentiment, sample[8]), axis=0)
pred_start, pred_end, text, selected_text, sentiment, offset = \+ q8 v! h1 P! M6 V( z. v9 v
to_numpy(pred_start, pred_end, text, selected_text, sentiment, offset)8 ^8 p z6 W; O
return pred_start, pred_end, text, selected_text, sentiment, offset4 t J% g1 G6 w& F
判断函数
这个竞赛采用单词级Jaccard系数,计算公式如下$ c$ Q5 Z1 X, G/ R) {& @& T& r
; w. \( z# m0 y1 `. ^
Jaccard系数计算的是你预测的单词在数据集中的个数,: F* ^3 R" I8 U7 J
def jaccard(str1, str2):
a = set(str1.lower().split())" v0 i- H6 P) C. T
b = set(str2.lower().split()) y' U- d% u; _$ ~2 O
c = a.intersection(b)
return float(len(c)) / (len(a) + len(b) - len(c))& o! M0 g3 @$ J f
定义预测结果解码函数% j, b% i4 \$ P" n2 ~
解码函数通过模型预测拿到的start和end的index位置信息,然后和之前拿到的词向量在样本句子中的位置进行比较,将这个区间内的所有的单词都提取出来作为我们的预测结果。, L' t; N) R: M4 P; N+ A) c: V
! W& I: B. l! \+ _) u1 A- s
def decode_prediction(pred_start, pred_end, text, offset, sentiment):: S- \& O6 q& c9 c
def decode(pred_start, pred_end, text, offset):; }! M I2 p* X; Z
decoded_text = ""
for i in range(pred_start, pred_end+1):0 W2 V6 Q$ w2 e% o/ Z) [- w7 A
decoded_text += text[offset[0]:offset[1]]
if (i+1) idx_end:; Y% ]. A% H8 w: y9 O2 S. s& L3 F: K
idx_end = idx_start
decoded_text = str(decode(idx_start, idx_end, text, offset))/ A- w: C; B9 b6 N
if len(decoded_text) == 0:: Q+ P9 u; x0 Y, ~4 r7 |
decoded_text = text5 d: w$ N' y$ e
decoded_predictions.append(decoded_text)9 R4 J# V7 b# ^
- t3 E# _0 Q Q+ L2 S3 Z- F* }
return decoded_predictions
开始训练' m5 p$ |# f4 `) v* M
将训练数据分成5个folds,每个folds训练5个epoch,使用adam优化器,learning rate设置成3e-5,batch size使用32。
num_folds = 5
num_epochs = 56 V5 `( [. `: o3 p5 O
batch_size = 32. y Z/ u5 J" V" G. L: S/ `
learning_rate = 3e-5
optimizer = tf.keras.optimizers.Adam(learning_rate)7 n3 S- `8 l1 m# I3 \: W
optimizer = tf.keras.mixed_precision.experimental.LossScaleOptimizer(. m/ m* o3 r M
optimizer, 'dynamic')
config = BertConfig(output_hidden_states=True, num_labels=2)+ V8 w# v+ i( r L) C2 _
model = BertModel.from_pretrained(PATH, config=config)
loss_fn = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)+ `7 w& q; O8 \4 L' v6 O$ V: W
kfold = model_selection.KFold(# \3 m J- h5 R* J v3 p ]* r
n_splits=num_folds, shuffle=True, random_state=42)
test_preds_start = np.zeros((len(test_df), 128), dtype=np.float32)! ?9 y8 C, z9 H' D
test_preds_end = np.zeros((len(test_df), 128), dtype=np.float32)7 a! u9 w- z* U9 G) f9 ^
for fold_num, (train_idx, valid_idx) in enumerate(kfold.split(train_df.text)):5 m: D7 X6 {4 [2 p" V
print("\nfold %02d" % (fold_num+1))
9 h4 y& g C c/ t' B9 x2 V
# 创建train, valid, test数据集6 N) C0 L- U, E+ ]
train_dataset = TweetDataset.create(
train_df.iloc[train_idx], batch_size, shuffle_buffer_size=2048), Z( v) K8 N9 N+ f1 t5 r
valid_dataset = TweetDataset.create(4 Z& _5 r& q* h1 ]% m) f
train_df.iloc[valid_idx], batch_size, shuffle_buffer_size=-1)
test_dataset = TweetDataset.create( I! d- C! E0 n1 {, z
test_df, batch_size, shuffle_buffer_size=-1)
& P7 b9 B2 q# ~/ O: |- P/ ?8 i
best_score = float('-inf')' }* |4 G f, @
for epoch_num in range(num_epochs):; A9 l2 O+ U$ x$ E
print("\nepoch %03d" % (epoch_num+1))2 ]4 @1 j! ~9 A7 `
train(model, train_dataset, loss_fn, optimizer)
pred_start, pred_end, text, selected_text, sentiment, offset = \" c' y# A' ?- r9 U- w5 C
predict(model, valid_dataset, loss_fn, optimizer)( s, n4 E4 l2 b0 y5 e0 C
selected_text_pred = decode_prediction(# g2 c: L5 m8 J' B2 G. M! W
pred_start, pred_end, text, offset, sentiment)
jaccards = []; ?- V+ A) ~2 J8 v7 B; }# B3 \
for i in range(len(selected_text)):* c8 M/ u0 B* H" _" b1 @, | j" w; s
jaccards.append(
jaccard(selected_text, selected_text_pred))
score = np.mean(jaccards)9 r* y/ j& Q$ b1 F# m
print(f"valid jaccard epoch {epoch_num+1:03d}: {score}"+" "*15)+ W H3 \: z5 \/ ~$ @2 J
if score > best_score:
best_score = score
# predict test set
test_pred_start, test_pred_end, test_text, _, test_sentiment, test_offset = \, X: s6 P9 P+ M- R$ w
predict(model, test_dataset, loss_fn, optimizer)
test_preds_start += test_pred_start * 0.2
test_preds_end += test_pred_end * 0.2! A6 h1 _7 Q6 r3 i% Q! |7 w. y# G# l
# ^3 Z8 M& U. E" l9 _% X5 s
# 重置模型,避免OOM4 \3 u8 ]1 m7 r m& E8 V9 f
session = tf.compat.v1.get_default_session()
graph = tf.compat.v1.get_default_graph()
del session, graph, model
model = BertModel.from_pretrained(PATH, config=config)& d" r' X; h9 X
预测测试数据,并生成提交文件. R6 v: O. V' J( q
selected_text_pred = decode_prediction(# @" A, t# G K, p
test_preds_start, test_preds_end, test_text, test_offset, test_sentiment)% c% U! X* s5 z9 _9 o6 E
def f(selected):
return " ".join(set(selected.lower().split()))
submission_df.loc[:, 'selected_text'] = selected_text_pred6 y+ g9 E5 Q% `! o2 b
submission_df['selected_text'] = submission_df['selected_text'].map(f)+ s/ u6 n" @( i3 m: E2 p# O
submission_df.to_csv("submission.csv", index=False)
这个方案在提交的时候在553个队伍中排名153位, 分数为0.68。
成为第一个吐槽的人