在處理大量數(shù)據(jù)時(shí),有必要將具有特征的空間壓縮為向量。一個(gè)例子是文本嵌入,它是幾乎所有 NLP 模型創(chuàng)建過(guò)程中不可或缺的一部分。不幸的是,使用神經(jīng)網(wǎng)絡(luò)處理這種類型的數(shù)據(jù)遠(yuǎn)非總是可能的——例如,原因可能是擬合或推理率低。
下面是我提出一種有趣的方法來(lái)使用,這個(gè)方法就是很少有人知道的梯度提升。
數(shù)據(jù)資料
在最近一項(xiàng)有關(guān)于卡格爾的比賽結(jié)束了,在那里展示了一個(gè)包含文本數(shù)據(jù)的小數(shù)據(jù)集。我決定將這些數(shù)據(jù)用于實(shí)驗(yàn),因?yàn)楸荣惐砻鲾?shù)據(jù)集標(biāo)記得很好,而且我沒(méi)有遇到任何令人不快的意外。
列:
- id - 摘錄的唯一 ID
- url_legal - 來(lái)源網(wǎng)址
- license - 源材料許可
- excerpt - 預(yù)測(cè)閱讀難易度的文本
- target - 更容易理解
- standard_error -測(cè)量每個(gè)摘錄的多個(gè)評(píng)分員之間的分?jǐn)?shù)分布
作為數(shù)據(jù)集中的目標(biāo),它是一個(gè)數(shù)值變量,提出解決回歸問(wèn)題。但是,我決定用分類問(wèn)題代替它。主要原因是我將使用的庫(kù)不支持在回歸問(wèn)題中處理文本和嵌入。我希望開(kāi)發(fā)者在未來(lái)能夠消除這個(gè)不足。但無(wú)論如何,回歸和分類的問(wèn)題是密切相關(guān)的,對(duì)于分析來(lái)說(shuō),解決哪個(gè)問(wèn)題沒(méi)有區(qū)別。
讓我們通過(guò) Sturge 規(guī)則計(jì)算 bin 的數(shù)量:
num_bins = int(np.floor(1 + np.log2(len(train))))
train['target_q'], bin_edges = pd.qcut(train['target'],
q=num_bins, labels=False, retbins=True, precision=0)
但是,首先,我清理數(shù)據(jù)。
train['license'] = train['license'].fillna('nan')
train['license'] = train['license'].astype('category').cat.codes
在一個(gè)小的自寫函數(shù)的幫助下,我對(duì)文本進(jìn)行了清理和詞形還原。函數(shù)可能很復(fù)雜,但這對(duì)于我的實(shí)驗(yàn)來(lái)說(shuō)已經(jīng)足夠了。
def clean_text(text):
table = text.maketrans(
dict.fromkeys(string.punctuation))
words = word_tokenize(
text.lower().strip().translate(table))
words = [word for word in words if word not in
stopwords.words ('english')] lemmed = [WordNetLemmatizer().lemmatize(word) for word in words]
return " ".join(lemmed)
我將清理后的文本另存為新功能。
train['clean_excerpt'] = train['excerpt'].apply(clean_text)
除了文本之外,我還可以選擇 URL 中的單個(gè)單詞并將這些數(shù)據(jù)轉(zhuǎn)換為新的文本功能。
def getWordsFromURL(url):
return re.compile(r'[\:/?=\-&.]+',re.UNICODE).split(url)
train['url_legal'] = train['url_legal'].fillna("nan").apply(getWordsFromURL).apply(
lambda x: " ".join(x))
我從文本中創(chuàng)建了幾個(gè)新特征——這些是各種統(tǒng)計(jì)信息。同樣,有很大的創(chuàng)造力空間,但這些數(shù)據(jù)對(duì)我們來(lái)說(shuō)已經(jīng)足夠了。這些功能的主要目的是對(duì)基線模型有用。
def get_sentence_lengths(text):
tokened = sent_tokenize(text) lengths
= []
for idx,i in enumerate(tokened):
splited = list(i.split(" "))
lengths.append(len(splited))
return (max (長(zhǎng)度),
min(lengths),
round(mean(lengths), 3))
def create_features(df):
df_f = pd.DataFrame(index=df.index)
df_f['text_len'] = df['excerpt'].apply(len)
df_f['text_clean_len']= df['clean_excerpt']。 apply(len)
df_f['text_len_div'] = df_f['text_clean_len'] / df_f['text_len']
df_f['text_word_count'] = df['clean_excerpt'].apply(
lambda x : len(x.split(') ')))
df_f[['max_len_sent','min_len_sent','avg_len_sent']] = \
df_f.apply(
lambda x: get_sentence_lengths(x['excerpt']),
axis=1, result_type='expand')
return df_f
train = pd.concat(
[train, create_features(train)], axis=1, copy=False, sort=False)
basic_f_columns = [
'text_len'、'text_clean_len'、'text_len_div'、'text_word_count'、
'max_len_sent'、'min_len_sent'、'avg_len_sent']
當(dāng)數(shù)據(jù)稀缺時(shí),很難檢驗(yàn)假設(shè),結(jié)果通常也不穩(wěn)定。因此,為了對(duì)結(jié)果更有信心,我更喜歡在這種情況下使用 OOF(Out-of-Fold)預(yù)測(cè)。
基線
我選擇Catboost作為模型的免費(fèi)庫(kù)。Catboost 是一個(gè)高性能的開(kāi)源庫(kù),用于決策樹(shù)上的梯度提升。從 0.19.1 版開(kāi)始,它支持開(kāi)箱即用的 GPU 分類文本功能。主要優(yōu)點(diǎn)是 CatBoost 可以在您的數(shù)據(jù)中包含分類函數(shù)和文本函數(shù),而無(wú)需額外的預(yù)處理。
在非常規(guī)情緒分析:BERT 與 Catboost 中,我擴(kuò)展了 Catboost 如何處理文本并將其與 BERT 進(jìn)行了比較。
這個(gè)庫(kù)有一個(gè)殺手锏:它知道如何使用嵌入。不幸的是,目前,文檔中對(duì)此一無(wú)所知,很少有人知道 Catboost 的這個(gè)優(yōu)勢(shì)。
!pip install catboost
使用 Catboost 時(shí),我建議使用 Pool。它是一個(gè)方便的包裝器,結(jié)合了特征、標(biāo)簽和進(jìn)一步的元數(shù)據(jù),如分類和文本特征。
為了比較實(shí)驗(yàn),我創(chuàng)建了一個(gè)僅使用數(shù)值和分類特征的基線模型。
我寫了一個(gè)函數(shù)來(lái)初始化和訓(xùn)練模型。順便說(shuō)一下,我沒(méi)有選擇最佳參數(shù)。
def fit_model_classifier(train_pool, test_pool, **kwargs):
model = CatBoostClassifier(
task_type='GPU',
iterations=5000,
eval_metric='AUC',
od_type='Iter',
od_wait=500,
l2_leaf_reg=10,
bootstrap_type='Bernoulli ',
subsample=0.7,
**kwargs
)
return model.fit(
train_pool,
eval_set=test_pool,
verbose=100,
plot=False,
use_best_model=True)
對(duì)于OOF的實(shí)現(xiàn),我寫了一個(gè)小而簡(jiǎn)單的函數(shù)。
def get_oof_classifier(
n_folds, x_train, y, embedding_features,
cat_features, text_features, tpo, seeds,
num_bins, emb=None, tolist=True):
ntrain = x_train.shape[0]
oof_train = np.zeros((len(seeds), ntrain, num_bins))
models = {}
for iseed, seed in enumerate(seeds):
kf = StratifiedKFold(
n_splits=n_folds,
shuffle=True,
random_state=seed)
for i, (tr_i, t_i) in enumerate(kf.split(x_train, y)):
if emb and len(emb) > 0:
x_tr = pd.concat(
[x_train.iloc[tr_i, :],
get_embeddings(
x_train.iloc[tr_i, :], emb, tolist)],
axis=1, copy=False, sort=False)
x_te = pd.concat(
[x_train.iloc[t_i, :],
get_embeddings(
x_train.iloc[t_i, :], emb, tolist)],
axis=1, copy=False, sort=False)
columns = [
x for x in x_tr if (x not in ['excerpt'])]
if not embedding_features:
for c in emb:
columns.remove(c)
else:
x_tr = x_train.iloc[tr_i, :]
x_te = x_train.iloc[t_i, :]
columns = [
x for x in x_tr if (x not in ['excerpt'])]
x_tr = x_tr[columns]
x_te = x_te[columns]
y_tr = y[tr_i]
y_te = y[t_i]
train_pool = Pool(
data=x_tr,
label=y_tr,
cat_features=cat_features,
embedding_features=embedding_features,
text_features=text_features)
valid_pool = Pool(
data=x_te,
label=y_te,
cat_features=cat_features,
embedding_features=embedding_features,
text_features=text_features)
model = fit_model_classifier(
train_pool, valid_pool,
random_seed=seed,
text_processing=tpo
)
oof_train[iseed, t_i, :] = \
model.predict_proba(valid_pool)
models[(seed, i)] = model
oof_train = oof_train.mean(axis=0)
return oof_train, models
我將在下面寫關(guān)于get_embeddings函數(shù),但它現(xiàn)在不用于獲取模型的基線。
我使用以下參數(shù)訓(xùn)練了基線模型:
columns = ['license', 'url_legal'] + basic_f_columns
oof_train_cb, models_cb = get_oof_classifier(
n_folds=5,
x_train=train[columns],
y=train['target_q'].values,
embedding_features=None,
cat_features=['license'],
text_features=['url_legal'],
tpo=tpo,
seeds=[0, 42, 888],
num_bins=num_bins
)
訓(xùn)練模型的質(zhì)量:
roc_auc_score(train['target_q'], oof_train_cb, multi_class="ovo")
AUC:0.684407
現(xiàn)在我有了模型質(zhì)量的基準(zhǔn)。從數(shù)字來(lái)看,這個(gè)模型很弱,我不會(huì)在生產(chǎn)中實(shí)現(xiàn)它。
嵌入
您可以將多維向量轉(zhuǎn)換為嵌入,這是一個(gè)相對(duì)低維的空間。因此,嵌入簡(jiǎn)化了大型輸入的機(jī)器學(xué)習(xí),例如表示單詞的稀疏向量。理想情況下,嵌入通過(guò)在嵌入空間中將語(yǔ)義相似的輸入彼此靠近放置來(lái)捕獲一些輸入語(yǔ)義。
有很多方法可以獲得這樣的向量,我在本文中不考慮它們,因?yàn)檫@不是研究的目的。但是,以任何方式獲得嵌入對(duì)我來(lái)說(shuō)就足夠了;最重要的是他們保存了必要的信息。在大多數(shù)情況下,我使用目前流行的方法——預(yù)訓(xùn)練的 Transformer。
from sentence_transformers import SentenceTransformer
STRANSFORMERS = {
'sentence-transformers/paraphrase-mpnet-base-v2': ('mpnet', 768),
'sentence-transformers/bert-base-wikipedia-sections-mean-tokens': ('wikipedia', 768)
}
def get_encode(df, encoder, name):
device = torch.device(
"cuda:0" if torch.cuda.is_available() else "cpu")
model = SentenceTransformer(
encoder,
cache_folder=f'./hf_{name} /'
)
model.to(device)
model.eval()
return np.array(model.encode(df['excerpt']))
def get_embeddings(df, emb=None, tolist=True):
ret = pd.DataFrame(index=df.index)
for e, s in STRANSFORMERS.items():
if emb and s[0] not in emb:
continue
ret[s[0]] = list(get_encode(df, e, s[0]))
if tolist:
ret = pd.concat(
[ret, pd.DataFrame(
ret[s[0]].tolist(),
columns=[f'{s[0]}_{x}' for x in range(s[1])],
index=ret.index)],
axis=1, copy=False, sort=False)
return ret
現(xiàn)在我有了開(kāi)始測(cè)試不同版本模型的一切。
楷模
我有幾種擬合模型的選項(xiàng):
- 文字特征;
- 嵌入特征;
- 嵌入特征,如分離的數(shù)字特征列表。
我一直在訓(xùn)練這些選項(xiàng)的各種組合,這使我能夠得出嵌入可能有多有用的結(jié)論,或者,這可能只是一種過(guò)度設(shè)計(jì)。
例如,我給出了一個(gè)使用所有三個(gè)選項(xiàng)的代碼:
columns = ['license', 'url_legal', 'clean_excerpt', 'excerpt']
oof_train_cb, models_cb = get_oof_classifier(
n_folds=FOLDS,
x_train=train[columns],
y=train['target_q'].values,
embedding_features=['mpnet', 'wikipedia'],
cat_features=['license'],
text_features= ['clean_excerpt','url_legal'],
tpo=tpo, seed
=[0, 42, 888],
num_bins=num_bins,
emb=['mpnet', 'wikipedia'],
tolist=True
)
有關(guān)更多信息,我在 GPU 和 CPU 上訓(xùn)練了模型;并將結(jié)果匯??總在一張表中。
令我震驚的第一件事是文本特征和嵌入的極差交互。不幸的是,我對(duì)這個(gè)事實(shí)還沒(méi)有任何合乎邏輯的解釋——在這里,需要在其他數(shù)據(jù)集上對(duì)這個(gè)問(wèn)題進(jìn)行更詳細(xì)的研究。同時(shí),請(qǐng)注意,將文本和嵌入用于同一文本的組合使用會(huì)降低模型的質(zhì)量。
對(duì)我來(lái)說(shuō)另一個(gè)啟示是在 CPU 上訓(xùn)練模式時(shí)嵌入不起作用。
現(xiàn)在是一件好事——如果你有一個(gè) GPU 并且可以獲得嵌入,那么最好的質(zhì)量是當(dāng)你同時(shí)使用嵌入作為一個(gè)特征和一個(gè)單獨(dú)的數(shù)字特征列表時(shí)。
總結(jié)
在這篇文章中,我:
- 選擇了一個(gè)小的免費(fèi)數(shù)據(jù)集進(jìn)行測(cè)試;
- 為文本數(shù)據(jù)創(chuàng)建了幾個(gè)統(tǒng)計(jì)特征,以使用它們來(lái)創(chuàng)建基線模型;
- 測(cè)試了嵌入、文本和簡(jiǎn)單特征的各種組合;
- 得到了一些不明顯的見(jiàn)解。