BERT մոդելի հրապարակումը 2018 թվականին [1] հեղափոխություն էր NLP աշխարհում, քանի որ BERT-ի նման Լեզուների խոշոր մոդելները ձեռք են բերում գերժամանակակից արդյունքներ NLP-ի բազմաթիվ առաջադրանքներում: Ցույց տալու համար, թե ինչպես կարող է BERT-ն օգտագործվել գործնականում, մենք մշակում ենք տեքստի դասակարգման օրինակ: Այս դեպքերում մենք ասում ենք, որ BERT-ը, որը պարունակում է լեզվական կարողություններ և համաշխարհային գիտելիքներ իր բազմաթիվ աղբյուրներից, որոնցով նա վերապատրաստվել է (Վիքիպեդիա, գրքեր,…), օգտագործվում է Փոխանցման ուսուցման համար, մինչդեռ դասակարգման վարժությունը, որը մենք կատարում ենք, կոչվում է Fine-Tuning:

«Fine-Tuning»-ը չի նշանակում, որ մենք կարգավորում ենք BERT մոդելը: Մենք օգտագործում ենք BERT-ն այնպես, ինչպես կա, և ավելացնում ենք «գլուխ» BERT-ի վերևում: BERT-ն օգտագործվում է տեքստի որոշ հատկություններ «կոդավորելու» վեկտորի (կամ թենզորի) մեջ: Դասակարգման վարժությունը բաղկացած է այդ վեկտորների դասակարգումից: Այս կոդավորված արժեքների դասակարգումը վեկտորների մեջ դառնում է դասական դասակարգման մեքենայական ուսուցման խնդիր, որը կարող է իրականացվել կամ դասական մոտեցմամբ (լոգիստիկ ռեգրեսիա, միամիտ Բեյս,…) կամ նեյրոնային ցանցով:

Ավելի ճշգրիտ լինելու համար, մենք ուղղակիորեն չենք աշխատի BERT-ի հետ, այլ DistilBERT-ի հետ, որը պարզեցված մոդել է (ունի BERT-ի պարամետրերի միայն կեսը), որը նմանակում է BERT-ի վարքագծին: DistilBERT-ի կատարումը շատ մոտ է BERT-ի կատարմանը (և երբեմն նույնիսկ ավելի լավ), չնայած պարամետրերի միայն կեսին: Այս մոտեցման մասին լրացուցիչ մանրամասների համար կարող եք կարդալ [2]: Սույն հոդվածում մենք կօգտագործենք BERT բառը շատ ժամանակ, նույնիսկ եթե օգտագործենք DistilBERT:

ԲԵՐՏ-ի միջոցով տեքստի դասակարգման մոդել կառուցելու համար մենք կարող ենք կիրառել երկու (մի փոքր) տարբեր ռազմավարություններ: Ինչպես նշվեց վերևում, BERT-ն օգտագործվում է տեքստերը վեկտորի մեջ կոդավորելու համար: Դասակարգման մոդելը, որը մենք կառուցել ենք BERT-ի վրա, բաղկացած է այս վեկտորների դասակարգումից՝ օգտագործելով ML ալգորիթմները: Այսպիսով, մենք կարող ենք կամ.

  • Կիրառեք BERT-ը տեքստերի վրա որպես նախնական մշակման քայլեր, այնուհետև կառուցեք ML մոդել, որը դասակարգում է այս վեկտորները
  • Կառուցեք մոդել, որը սկսվում է BERT-ով (որտեղ մենք սառեցնում ենք պարամետրերը մոդելը վարժեցնելիս), այնուհետև BERT-ի վերևում կառուցում ենք շերտ, որը դասակարգում է այս վեկտորները:

Առաջին մեթոդի առավելությունն այն է, որ մենք պետք է տեքստը (վերապատրաստման և թեստի տվյալները) փոխանցենք միայն մեկ անգամ BERT-ի միջոցով, և ոչ թե յուրաքանչյուր դարաշրջանի համար՝ մարզումների ժամանակ՝ զգալիորեն նվազեցնելով ուսուցման ժամանակը: Երկրորդ մեթոդի առավելությունն այն է, որ մենք կարիք չունենք խողովակաշարին հավելյալ քայլ կառուցել, քանի որ ծայրից մինչև վերջ (տեքստից մինչև դասակարգում) տրամադրվում է նշանաբանը (տես ստորև՝ զարգացումներում, թե ինչ է սա) և մոդելն ինքնին։

Մանկավարժական նպատակով մենք կգնանք երկրորդ մոտեցմամբ. Իրական դեպքում մոտեցման ընտրությունը կախված կլինի մոդելի կիրառությունից: Եթե ​​մեզ անհրաժեշտ լինի կանոնավոր կերպով վարժեցնել մոդելը, ես կնախընտրեի առաջին մոտեցումը: Եթե ​​մոդելը պետք է վերապատրաստվի միայն մեկ անգամ, ես կգնամ երկրորդ մոտեցմանը:

Մենք կօգտագործենք DistilBERT մոդելը Python գրադարանի տրանսֆորմատորներից, որը տրամադրում է Hugging Face ընկերությունը։

Տվյալների հավաքածու

Այս հոդվածում մենք կքննարկենք մի փոքր տվյալների բազա՝ BBC-ի նորությունները՝ դասակարգված ըստ թեմայի՝ https://www.kaggle.com/sainijagjit/bbc-dataset: Այս կորպուսը պարունակում է մոտ 2k գրառում, որը բավական փոքր է ստանդարտ համակարգչի վրա գործարկվելու համար:

# Libraries needed for data preparation
import pandas as pd
import numpy as np

# Download the dataset and put it in subfolder called data
datapath = "data/bbc-text.csv"
df = pd.read_csv(datapath)
df = df[["category", "text"]]

# Show the data
df.head()

Եկեք վերլուծենք տվյալները՝ նախքան որևէ տեքստի դասակարգում կատարելը.

print('Total number of news: {}'.format(len(df)))
print(40*'-')
print('Split by category:')
print(df["category"].value_counts())
print(40*'-')
nr_categories = len(df["category"].unique())
print("Number of categories: {n}".format(n=nr_categories))
Total number of news: 2225
----------------------------------------
Split by category:
sport            511
business         510
politics         417
tech             401
entertainment    386
Name: category, dtype: int64
----------------------------------------
Number of categories: 5

Մենք ստանում ենք 2'225 գրառումների ընդհանուր թիվը, որոնք համեմատաբար հավասարաչափ բաշխված են հինգ կատեգորիաների վրա: Սա մեզ թույլ է տալիս կիրառել ստանդարտ մեթոդներ, քանի որ կարիք չկա ավելորդ կամ թերակշռելու որոշ կատեգորիաներ:

Ի վերջո, կոնկրետ տպավորություն ստանալու համար եկեք դիտարկենք կոնկրետ օրինակ.

# You can adjust n:
n=100
print('Category: ',df['category'][n])
print(100*'-')
print('Text:')
print(df['text'][n])
Category:  entertainment
----------------------------------------------------------------------------------------------------
Text:
housewives lift channel 4 ratings the debut of us television hit desperate housewives has helped lift channel 4 s january audience share by 12% compared to last year.  other successes such as celebrity big brother and the simpsons have enabled the broadcaster to surpass bbc two for the first month since last july. bbc two s share of the audience fell from 11.2% to 9.6% last month in comparison with january 2004. celebrity big brother attracted fewer viewers than its 2002 series.  comedy drama desperate housewives managed to pull in five million viewers at one point during its run to date  attracting a quarter of the television audience. the two main television channels  bbc1 and itv1  have both seen their monthly audience share decline in a year on year comparison for january  while five s proportion remained the same at a slender 6.3%. digital multi-channel tv is continuing to be the strongest area of growth  with the bbc reporting freeview box ownership of five million  including one million sales in the last portion of 2004. its share of the audience soared by 20% in january 2005 compared with last year  and currently stands at an average of 28.6%.

Լեզուների մեծ մոդելների համար (LLM) կարիք չկա տվյալների հետագա ձեռքով մշակման, քանի որ մոդելի հետ գալիս են նշանները, այսինքն՝ շերտերը, որոնք տեքստը կոդավորում են «թվերով»:

Պիտակը պետք է վերածվի թվի՝ «ինդեքսի»: Արի անենք դա.

# Renaming, Input -> X, Output -> y
X = df['text']
y=np.unique(df['category'], return_inverse=True)[1]
print(y)
[4 0 3 ... 1 2 3]

Եկեք նաև օրինականացնենք BERT նշանաբանը, որը կօգտագործվի ավելի ուշ: Նույն նշանաբանը կարող է օգտագործվել ինչպես TensorFlow, այնպես էլ PyTorch շրջանակների համար:

# distilBERT tokenizer
import transformers
tokenizer = transformers.DistilBertTokenizer.from_pretrained('distilbert-base-uncased')

Այսուհետ մենք տարբերակելու ենք TensorFlow-ը և PyTorch-ը։ Սկզբունքները դեռ նույնն են, սակայն օբյեկտների բնույթը կփոխվի։ TensorFlow-ը և PyTorch-ն ունեն իրենց սեփական թենզորային օբյեկտները:

Տեքստի դասակարգման կառուցում TensorFlow-ում

Պատրաստեք տվյալների հավաքածուն

Նշենք, որ TensorFlow-ն ունի իր սեփական Tensor-ները (TensorFlow tensors) և տվյալների բազայի իր տեսակները: Բայց TensorFlow շրջանակը կարող է օգտագործվել նաև NumPy զանգվածների հետ, որն իմ կարծիքով ավելի հեշտ է օգտագործել: Սա այն է, ինչ մենք պատրաստվում ենք օգտագործել ներկա օրինակում: Դա էական տարբերություն է PyTorch-ի հետ, որտեղ մենք գրեթե ստիպված ենք օգտագործել PyTorch թենսորները, PyTorch տվյալների հավաքածուները և PyTorch տվյալների բեռնիչը:

Մենք նախ մուտքագրված տվյալները (տեքստը) կվերափոխենք NumPy զանգվածների՝ օգտագործելով DistilBERT-ի նշանաբանը:

Մենք նաև կբաժանենք տվյալների բազան գնացքի և փորձնական տվյալների բազայի: Դա անելիս մենք հոգում ենք, որ պահպանվեն նույն համամասնությունները՝ ըստ կատեգորիաների, վերապատրաստման և թեստային տվյալների հավաքածուների միջև:

import tensorflow as tf
X_tf = [tokenizer(text, padding='max_length', max_length = 512, truncation=True)['input_ids'] for text in X]
X_tf = np.array(X_tf, dtype='int32')
# Train/test split
from sklearn.model_selection import train_test_split
X_tf_train, X_tf_test, y_tf_train, y_tf_test = train_test_split(X_tf, y, test_size=0.3, random_state=42, stratify=y)
print('Shape of training data: ',X_tf_train.shape)
print('Shape of test data: ',X_tf_test.shape)
Shape of training data:  (1557, 512)
Shape of test data:  (668, 512)

Կառուցեք մոդելը

Եկեք կառուցենք մոդելը. Դրա համար մենք նախ պետք է ստանանք BERT շերտը տրանսֆորմատորային գրադարանից: Մենք կարգավորում ենք այն այնպես, որ դրա պարամետրերը չեն վերապատրաստվի վերապատրաստման ընթացքում:

# Get BERT layer
config = transformers.DistilBertConfig(dropout=0.2, attention_dropout=0.2)
dbert_tf = transformers.TFDistilBertModel.from_pretrained('distilbert-base-uncased', config=config, trainable=False)
Some layers from the model checkpoint at distilbert-base-uncased were not used when initializing TFDistilBertModel: ['vocab_layer_norm', 'vocab_transform', 'vocab_projector', 'activation_13']
- This IS expected if you are initializing TFDistilBertModel from the checkpoint of a model trained on another task or with another architecture (e.g. initializing a BertForSequenceClassification model from a BertForPreTraining model).
- This IS NOT expected if you are initializing TFDistilBertModel from the checkpoint of a model that you expect to be exactly identical (initializing a BertForSequenceClassification model from a BertForSequenceClassification model).
All the layers of TFDistilBertModel were initialized from the model checkpoint at distilbert-base-uncased.
If your task is similar to the task the model of the checkpoint was trained on, you can already use TFDistilBertModel for predictions without further training.

Դուք կարող եք նախազգուշական հաղորդագրություն ստանալ: Մի անհանգստացեք դրա մասին: Այն համատեքստում, որտեղ մենք կիրառում ենք մոդելը, այս հաղորդագրությունը կարող է անտեսվել:

Փորձենք ավելի լավ հասկանալ այս մոդելը՝ ավելի մոտիկից նայելով դրա արդյունքին: Դրա համար եկեք նմուշ վերցնենք մեր ուսումնական տվյալների բազայից (մենք վերցնում ենք հինգ չափի նմուշ) և նայենք արդյունքին մոդելի միջոցով:

# Let's create a sample of size 5 from the training data
sample = X_tf_train[0:5]
print('Object type: ', type(dbert_tf(sample)))
print('Output format (shape): ',dbert_tf(sample)[0].shape)
print('Output used as input for the classifier (shape): ', dbert_tf(sample)[0][:,0,:].shape)
Object type:  <class 'transformers.modeling_tf_outputs.TFBaseModelOutput'>
Output format (shape):  (5, 512, 768)
Output used as input for the classifier (shape):  (5, 768)

Արդյունքը Python-ի հատուկ օբյեկտ է: Ի թիվս այլ տեղեկությունների, մենք ստանում ենք չափի տենզոր (N, M, S), որտեղ N-ը տվյալների հավաքածուի չափն է (մեր դեպքում հինգ օրինակ), M-ը նմուշի երկարությունն է (տեքստի բառերի քանակը), իսկ S-ը ելքային վեկտորի չափն է (մոդելի ելքը): Սովորաբար, ինչպես նշվում է Devlin et al. [1] դասակարգման առաջադրանքի համար մենք օգտագործում ենք նախադասության առաջին ելքային վեկտորը որպես մուտքագրում դասակարգման մնացած մոդելի համար, քանի որ այս առաջին վեկտորը «կոդավորում է» տեղեկատվություն ընդհանուր տեքստի մասին։ Որպես տարբերակ դասակարգչի համար կարող է օգտագործվել նաև բոլոր ելքային վեկտորների միավորման միջինը:

Այժմ ժամանակն է կառուցել դասակարգման մոդելը: Այն բաղկացած կլինի.

  • Ներածման շերտ. մոդելին ասել, թե որ մուտքային ձևաչափը պետք է ակնկալել, որպեսզի մոդելն իմանա, թե ինչ է սպասվում: Սա հատուկ է TensorFlow-ին
  • Distil Bert մոդելը. մուտքային տվյալները կոդավորել վեկտորների նոր հաջորդականության մեջ (դա BERT-ի ելքն է): Այս հաջորդականության միայն առաջին վեկտորը կօգտագործվի որպես մուտքագրում մնացած դասակարգչի համար
  • Բաց թողնված շերտը` կանոնավորացման համար
  • Խիտ շերտ (ռելիու ակտիվացման ֆունկցիայով, 64 նեյրոններով)՝ լուծելու դասակարգման հատուկ խնդիրը
  • Խիտ շերտ (softmax ակտիվացման ֆունկցիայով). յուրաքանչյուր պիտակի համար հավանականության բաշխման համար

Dropout շերտը օգտագործվում է միայն մարզումների ժամանակ։ Շերտերի միջև որոշ կապեր միտումնավոր զրոյացվում են, որպեսզի «հարևանները» ստանձնեն դրա դերը: Սա ընդհանուր կանխատեսումն ավելի ամուր է դարձնում: Եզրակացության (կանխատեսման) համար մոդելն օգտագործելիս անտեսվում են բաց թողնված շերտերը, և ելքը համապատասխանաբար վերամասնակցվում է: Նկատի ունեցեք, որ սա պատճառներից մեկն է, թե ինչու մենք պետք է մոդելին ասենք՝ այն գտնվում է վերապատրաստման, թե գնահատման ռեժիմում:

TensorFlow-ում մենք նաև պետք է «կազմենք» մոդելը՝ նրան տրամադրելով օպտիմիզատոր, կորուստ և ուսուցման համար օգտագործվող չափումներ: Այս երեք օբյեկտների համար կան ստանդարտներ, որոնք կարող են մուտքագրվել տեքստի տեսքով («ադամ», «նոսր_կատեգորիայի_խաչսենտրոպիա», «ճշգրտություն»)։ Բայց այս բոլոր երեք օբյեկտների տեսակները կարող են նաև օրինակելի լինել որպես օբյեկտներ, այնուհետև հարմարեցվել:

Մոդելը ստեղծելուց հետո մենք այն կդիտարկենք TensorFlow-ի ամփոփիչ ֆունկցիայի միջոցով:

from tensorflow.keras import models, layers, metrics

input_ids_in = layers.Input(shape=(512,), name='input_token', dtype='int32')

x = dbert_tf(input_ids=input_ids_in)[0][:,0,:]
x = layers.Dropout(0.2, name='dropout')(x)
x = layers.Dense(64, activation='relu', name='dense')(x)
x = layers.Dense(5, activation='softmax', name='classification')(x)

model_tf = models.Model(inputs=input_ids_in, outputs = x, name='ClassificationModelTF')

model_tf.compile(optimizer='adam',loss='sparse_categorical_crossentropy', metrics=[metrics.SparseCategoricalAccuracy()])
model_tf.summary()
Model: "ClassificationModelTF"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_token (InputLayer)    [(None, 512)]             0         
                                                                 
 tf_distil_bert_model (TFDis  TFBaseModelOutput(last_h  66362880 
 tilBertModel)               idden_state=(None, 512,             
                             768),                               
                              hidden_states=None, att            
                             entions=None)                       
                                                                 
 tf.__operators__.getitem (S  (None, 768)              0         
 licingOpLambda)                                                 
                                                                 
 dropout (Dropout)           (None, 768)               0         
                                                                 
 dense (Dense)               (None, 64)                49216     
                                                                 
 classification (Dense)      (None, 5)                 325       
                                                                 
=================================================================
Total params: 66,412,421
Trainable params: 49,541
Non-trainable params: 66,362,880
_________________________________________________________________

Եկեք արագ ստուգենք, թե արդյոք վերապատրաստվող պարամետրերի քանակը (որոնք միայն խիտ շերտերում են, BERT-ը «սառեցված» է) իմաստ ունի: Վեկտորը, որը դուրս է գալիս BERT-ից, 768 չափսի մեկ վեկտոր է (ըստ BERT մոդելի սահմանման): Այս տարրերից յուրաքանչյուրը կապված է խիտ շերտի 64 նեյրոններից յուրաքանչյուրին, ինչը հանգեցնում է 768x64=49'152 պարամետրերի: Յուրաքանչյուր նեյրոն ունի լրացուցիչ պարամետր՝ կողմնակալություն, այսինքն՝ 64 պարամետր: Խիտ շերտի ելքը բաղկացած է 64 տարրից, որոնք միանում են դասակարգման շերտի բոլոր հինգ տարրերին, այսինքն՝ 64x5: Դասակարգման շերտը նույնպես ունի 5 կողմնակալություն.

Ընդհանուր առմամբ, վերապատրաստման ենթակա պարամետրերի քանակը՝ 768x64+64+64*5+5 = 49'541: Մենք այնտեղ ենք :-)

Վարժեցրեք մոդելին

Այժմ ժամանակն է մարզել մոդելին: Եկեք դա անենք՝ նկատի ունենալով, որ մենք ցանկանում ենք չափել կատարումը (ինչպես ճշգրտության, այնպես էլ մարզման ժամանակի առումով), այնպես որ, եկեք չափումը տեղադրենք տեղում:

TensorFlow-ն ունի իր մոդելները վարժեցնելու շատ գործնական եղանակ՝ մի քանի տող կոդով: Մոդելը «համապատասխանում է» վերապատրաստման տվյալներին, ինչպես դա անում է scikit Learn գրադարանը: Վերապատրաստման գործընթացը նաև հետևում է որոշ միջանկյալ տվյալների, ինչը թույլ է տալիս հետևել ուսուցման ընթացքին: Այնուամենայնիվ, այս հարմարավետությունը գալիս է հենց վերապատրաստման վերաբերյալ ավելի քիչ թափանցիկության գնով: Ավելի ուշ կտեսնենք, որ PyTorch-ն այս առումով տարբերվում է:

from datetime import datetime
# Train the model
start_time = datetime.now()
history = model_tf.fit(X_tf_train, y_tf_train, batch_size=32, shuffle=True, epochs=5, validation_data=(X_tf_test, y_tf_test))
end_time = datetime.now()

training_time_tf = (end_time - start_time).total_seconds()
Epoch 1/5
49/49 [==============================] - 769s 16s/step - loss: 0.9628 - sparse_categorical_accuracy: 0.6930 - val_loss: 0.4008 - val_sparse_categorical_accuracy: 0.9207
Epoch 2/5
49/49 [==============================] - 773s 16s/step - loss: 0.3232 - sparse_categorical_accuracy: 0.9287 - val_loss: 0.1771 - val_sparse_categorical_accuracy: 0.9536
Epoch 3/5
49/49 [==============================] - 790s 16s/step - loss: 0.1987 - sparse_categorical_accuracy: 0.9538 - val_loss: 0.1264 - val_sparse_categorical_accuracy: 0.9611
Epoch 4/5
49/49 [==============================] - 839s 17s/step - loss: 0.1492 - sparse_categorical_accuracy: 0.9615 - val_loss: 0.1089 - val_sparse_categorical_accuracy: 0.9656
Epoch 5/5
49/49 [==============================] - 844s 17s/step - loss: 0.1549 - sparse_categorical_accuracy: 0.9538 - val_loss: 0.0996 - val_sparse_categorical_accuracy: 0.9701
from matplotlib import pyplot as plt

fig, ax = plt.subplots(nrows=1, ncols=2, figsize=(15, 5))
ax[0].set(title='Loss')
ax[0].plot(history.history['loss'], label='Training')
ax[0].plot(history.history['val_loss'], label='Validation')
ax[0].legend(loc="upper right")

ax[1].set(title='Accuracy')
ax[1].plot(history.history['sparse_categorical_accuracy'], label='Training')
ax[1].plot(history.history['val_sparse_categorical_accuracy'], label='Validation')
ax[1].legend(loc="lower right")
<matplotlib.legend.Legend at 0x7fd70820fed0>

accuracy_tf = history.history['val_sparse_categorical_accuracy'][-1]
print('Accuracy Training data: {:.1%}'.format(history.history['sparse_categorical_accuracy'][-1]))
print('Accuracy Test data: {:.1%}'.format(history.history['val_sparse_categorical_accuracy'][-1]))
print('Training time: {:.1f}s (or {:.1f} minutes)'.format(training_time_tf, training_time_tf/60))
Accuracy Training data: 95.4%
Accuracy Test data: 97.0%
Training time: 4015.3s (or 66.9 minutes)

Ուսուցման ժամանակը մեկ ժամից ավելի է (պրոցեսորի վրա) և ճշգրտությունը լավ տեսք ունի (›95%): Մենք նաև նկատում ենք, որ ճշգրտությունը մեծանում է դարաշրջանների հետ, և փորձարկման և վավերացման ճշտությունները մոտ են միմյանց, ինչը նշանակում է, որ կարծես թե ավելորդ համապատասխանություն չկա: Մենք չենք ուսումնասիրի այն գրառումները, որոնք տվել են սխալ կանխատեսում, քանի որ դա չէ այս հոդվածի նպատակը:

Մոդելը վերապատրաստվել է։ Այժմ ժամանակն է փրկել մոդելը: Եթե ​​մենք չկատարենք այս քայլը, մեր բոլոր մարզումների ջանքերը կկորչեն…

Պահպանեք մոդելը

Մենք կարող ենք պահպանել մոդելը TensofFlow-ի առաջարկած ձևաչափով (ընդլայնում՝ h5): Մեզ լրացուցիչ քայլ է պետք, քանի որ մենք օգտագործում ենք հատուկ օբյեկտ (BERT շերտը, որը գալիս է Գրկախառնված դեմքը գրադարանից): Տեսեք, թե ինչպես կարող ենք պահպանել մոդելը և բեռնել այն նոր մոդելում: Նկատի ունեցեք, որ դա անելիս մենք կորցնում ենք որոշ հատկություններ (օրինակ՝ չմարզվող պարամետրեր ունենալը): Զգույշ եղեք դա անելիս:

model_tf.save('model_tf.h5', save_format='h5')
model_tf2 = models.load_model('model_tf.h5', custom_objects={'TFDistilBertModel': dbert_tf})
model_tf2.summary()
WARNING:tensorflow:Error in loading the saved optimizer state. As a result, your model is starting with a freshly initialized optimizer.


WARNING:tensorflow:Error in loading the saved optimizer state. As a result, your model is starting with a freshly initialized optimizer.


Model: "ClassificationModelTF"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_token (InputLayer)    [(None, 512)]             0         
                                                                 
 tf_distil_bert_model_3 (TFD  TFBaseModelOutput(last_h  66362880 
 istilBertModel)             idden_state=(None, 512,             
                             768),                               
                              hidden_states=None, att            
                             entions=None)                       
                                                                 
 tf.__operators__.getitem (S  (None, 768)              0         
 licingOpLambda)                                                 
                                                                 
 dropout (Dropout)           (None, 768)               0         
                                                                 
 dense (Dense)               (None, 64)                49216     
                                                                 
 classification (Dense)      (None, 5)                 325       
                                                                 
=================================================================
Total params: 66,412,421
Trainable params: 66,412,421
Non-trainable params: 0
_________________________________________________________________

Տեքստի դասակարգման կառուցում PyTorch-ում

Պատրաստեք տվյալների հավաքածուն

Հիշենք, որ մենք արդեն պատրաստել ենք տվյալների հավաքածուն (տե՛ս վերևում, Տվյալների հավաքածու): Մենք նաև արդեն ներկայացրել ենք Tokenizer (որը ունիվերսալ է TensorFlow-ի և PyTorch-ի համար): Այն, ինչ մենք դեռ պետք է անենք, տվյալների բազան հարմարեցնելն է PyTorch-ին: Այս նպատակով PyTorch-ն ունի դաս (Dataset և DataLoader դասեր): Իմ գիտելիքներով պարտադիր է օգտագործել Datasets և Dataloaders մոդելը պատրաստելու համար: Մենք չենք կարող մոդել պատրաստել՝ օգտագործելով NumPy զանգվածները:

Առաջին քայլում մենք տվյալների հավաքածուն կվերածենք ջահի տենսորների, այնուհետև դրանք փաթեթավորելու ենք Dataset դասի մեջ և վերջապես տվյալների հավաքածուն կտեղադրենք Dataloader-ում:

import torch

X_list=X.to_list()
X_pt = tokenizer(X_list, padding='max_length', max_length = 512, truncation=True, return_tensors='pt')["input_ids"]

y_list=y.tolist()
y_pt = torch.Tensor(y_list).long()

Եկեք բաժանենք տվյալների բազան վերապատրաստման և փորձարկման տվյալների:

X_pt_train, X_pt_test, y_pt_train, y_pt_test = train_test_split(X_pt, y_pt, test_size=0.3, random_state=42, stratify=y_pt)

Մենք ստեղծում ենք Տվյալների հավաքածուի դաս և կներկայացնենք ինչպես ուսուցման, այնպես էլ թեստային տվյալների հավաքածուների համար:

# Convert data to torch dataset
from torch.utils.data import Dataset, DataLoader
class BBCNewsDataset(Dataset):
    """Custom-built BBC News dataset"""

    def __init__(self, X, y):
        """
        Args:
            X, y as Torch tensors
        """
        self.X_train = X
        self.y_train = y
        

    def __len__(self):
        return len(self.y_train)

    def __getitem__(self, idx):
        return self.X_train[idx], self.y_train[idx]
# Get train and test data in form of Dataset class
train_data_pt = BBCNewsDataset(X=X_pt_train, y=y_pt_train)
test_data_pt = BBCNewsDataset(X=X_pt_test, y=y_pt_test)

Մենք տվյալների հավաքածուները տեղադրում ենք տվյալների բեռնիչի մեջ, որպեսզի պատրաստենք տվյալների բազան, որն օգտագործվելու է վերապատրաստման համար:

# Get train and test data in form of Dataloader class
train_loader_pt = DataLoader(train_data_pt, batch_size=32)
test_loader_pt = DataLoader(test_data_pt, batch_size=32)

Այժմ մենք ավարտեցինք տվյալների բազայի պատրաստումը: Դա մի փոքր ավելի մեծ ջանքեր էր, քան TensorFlow-ի համար, բայց արժե օգտագործել PyTorch-ի շրջանակային առաջարկները տվյալների հավաքածուների համար:

Կառուցեք մոդելը

Այսուհետ մենք շատ ուշադիր կհետևենք TensorFlow-ի համար օգտագործած մոտեցմանը և կփորձենք հավատարիմ մնալ նույն պարամետրերին, որտեղ դա հնարավոր է:

Եկեք կառուցենք մոդելը. Դրա համար մենք նախ պետք է ստանանք BERT շերտը տրանսֆորմատորային գրադարանից: Մենք կարգավորում ենք այն այնպես, որ դրա պարամետրերը չեն վերապատրաստվի վերապատրաստման ընթացքում:

Նշենք, որ TensorFlow մոդելը կառուցելիս մենք արդեն սահմանել ենք կոնֆիգուրացիայի օբյեկտ։ Հետևաբար, ստորև բերված առաջին տողը անհրաժեշտ չէ: Մենք ավելացրել ենք միայն, որպեսզի թույլ տանք գործարկել երկու մոդելները (TensorFlow և PyTorch) անկախ:

config = transformers.DistilBertConfig(dropout=0.2, attention_dropout=0.2)
dbert_pt = transformers.DistilBertModel.from_pretrained('distilbert-base-uncased', config=config)
Some weights of the model checkpoint at distilbert-base-uncased were not used when initializing DistilBertModel: ['vocab_layer_norm.bias', 'vocab_transform.bias', 'vocab_projector.bias', 'vocab_layer_norm.weight', 'vocab_transform.weight', 'vocab_projector.weight']
- This IS expected if you are initializing DistilBertModel from the checkpoint of a model trained on another task or with another architecture (e.g. initializing a BertForSequenceClassification model from a BertForPreTraining model).
- This IS NOT expected if you are initializing DistilBertModel from the checkpoint of a model that you expect to be exactly identical (initializing a BertForSequenceClassification model from a BertForSequenceClassification model).

Դուք կարող եք նախազգուշական հաղորդագրություն ստանալ: Մի անհանգստացեք դրա մասին: Այն համատեքստում, որտեղ մենք կիրառում ենք մոդելը, այս հաղորդագրությունը կարող է անտեսվել:

Փորձենք ավելի լավ հասկանալ այս մոդելը՝ ավելի մոտիկից նայելով դրա արդյունքին: Դրա համար եկեք նմուշ վերցնենք մեր ուսումնական տվյալների բազայից (մենք վերցնում ենք հինգ չափի նմուշ) և նայենք արդյունքին մոդելի միջոցով:

# Let's create a sample of size 5 from the training data
sample = X_pt_train[0:5]
print('Object type: ', type(dbert_pt(sample)))
print('Output format (shape): ',dbert_pt(sample)[0].shape)
print('Output used as input for the classifier (shape): ', dbert_pt(sample)[0][:,0,:].shape)
Object type:  <class 'transformers.modeling_outputs.BaseModelOutput'>
Output format (shape):  torch.Size([5, 512, 768])
Output used as input for the classifier (shape):  torch.Size([5, 768])

Հրաշալի լուր։ Մենք ստանում ենք նույն ձևաչափը, ինչ TensorFlow-ում: Բայց դուք կարող եք տեսնել, որ այս անգամ տենզորը ջահի թենզոր է:

Արդյունքը Python-ի հատուկ օբյեկտ է: Ի թիվս այլ տեղեկությունների, մենք ստանում ենք չափի տենզոր (N, M, S), որտեղ N-ը տվյալների հավաքածուի չափն է (մեր դեպքում հինգ օրինակ), M-ը նմուշի երկարությունն է (տեքստի բառերի քանակը), իսկ S-ը ելքային վեկտորի չափն է (մոդելի ելքը): Սովորաբար, ինչպես նշվում է Devlin et al. [1], դասակարգման առաջադրանքի համար մենք օգտագործում ենք նախադասության առաջին ելքային վեկտորը որպես մուտքագրում դասակարգման մնացած մոդելի համար, քանի որ այս առաջին վեկտորը «կոդավորում է» տեղեկատվություն ընդհանուր նախադասության մասին։ Որպես տարբերակ դասակարգչի համար կարող է օգտագործվել նաև բոլոր ելքային վեկտորների միավորման միջինը:

Այժմ ժամանակն է կառուցել դասակարգման մոդելը: Այն բաղկացած կլինի.

  • Distil Bert մոդելը. մուտքային տվյալները կոդավորել վեկտորների նոր հաջորդականության մեջ (դա BERT-ի ելքն է): Այս հաջորդականության միայն առաջին վեկտորը կօգտագործվի որպես մուտքագրում մնացած դասակարգչի համար
  • Բաց թողնված շերտը` կանոնավորացման համար
  • Խիտ շերտ (ռելիու ակտիվացման ֆունկցիայով, 64 նեյրոններով)՝ լուծելու դասակարգման հատուկ խնդիրը
  • Խիտ շերտ (softmax ակտիվացման ֆունկցիայով). յուրաքանչյուր պիտակի համար հավանականության բաշխման համար

Dropout շերտը օգտագործվում է միայն մարզումների ժամանակ։ Շերտերի միջև որոշ կապեր միտումնավոր զրոյացվում են, որպեսզի «հարևանները» ստանձնեն դրա դերը: Սա ընդհանուր կանխատեսումն ավելի ամուր է դարձնում: Եզրակացության (կանխատեսման) համար մոդելն օգտագործելիս անտեսվում են բաց թողնված շերտերը, և արդյունքը համապատասխանաբար վերամասշտաբացվում է: Նկատի ունեցեք, որ սա պատճառներից մեկն է, թե ինչու մենք պետք է մոդելին ասենք՝ այն գտնվում է վերապատրաստման, թե գնահատման ռեժիմում:

PyTorch-ում մենք պետք է սահմանենք շերտերը, այնուհետև սահմանենք առաջընթաց ֆունկցիա, որն օգտագործում է շերտերը: Ի տարբերություն TensorFlow-ի՝ մենք մուտքային շերտ չենք ավելացնում։ Հիշեք, որ TensorFlow-ին անհրաժեշտ էր մուտքային շերտ՝ իմանալու համար, թե ինչ չափեր են ունենալու տարբեր թենզորները: PyTorch-ի դեպքում յուրաքանչյուր շերտ ստանում է Input-ի չափը և Output-ի չափը՝ որպես շփոթության պարամետրեր, ուստի թենզորների չափսերի մասին տեղեկատվությունը արդեն պարունակվում է հենց մոդելում: Օրինակ, «linear1» շերտը որպես մուտքագրում ստանում է 768 չափի վեկտոր, և որպես ելք վերադարձնում է 64 չափի վեկտոր: Այս երկու թվերը նրա սահմանման մի մասն են:

Նկատի ունեցեք նաև, որ PyTorch-ում մենք չենք սահմանում softmax շերտ (որը կօգտագործվի ելքը նորմալացնելու համար, որպեսզի ընդհանուր ելքը գումարի 1): Փոխարենը արդյունքի բնույթը (որը հավանականություն է և, հետևաբար, պետք է գումարվի 1-ի) կներառվի չափանիշի սահմանման մեջ (տե՛ս ստորև՝ վերապատրաստման բաժնում):

Մոդելը ստեղծելուց հետո մենք կդիտարկենք այն TensorFlow-ի տպման ֆունկցիայի միջոցով:

from torch import nn
# Get cpu or gpu device for training.
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Using {device} device")

class DistilBertClassification(nn.Module):
    def __init__(self):
        super(DistilBertClassification, self).__init__()
        self.dbert = dbert_pt
        self.dropout = nn.Dropout(p=0.2)
        self.linear1 = nn.Linear(768,64)
        self.ReLu = nn.ReLU()
        self.linear2 = nn.Linear(64,5)

    def forward(self, x):
        x = self.dbert(input_ids=x)
        x = x["last_hidden_state"][:,0,:]
        x = self.dropout(x)
        x = self.linear1(x)
        x = self.ReLu(x)
        logits = self.linear2(x)
        # No need for a softmax, because it is already included in the CrossEntropyLoss
        return logits

model_pt = DistilBertClassification().to(device)
Using cpu device
print(model_pt)
DistilBertClassification(
  (dbert): DistilBertModel(
    (embeddings): Embeddings(
      (word_embeddings): Embedding(30522, 768, padding_idx=0)
      (position_embeddings): Embedding(512, 768)
      (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
      (dropout): Dropout(p=0.2, inplace=False)
    )
    (transformer): Transformer(
      (layer): ModuleList(
        (0): TransformerBlock(
          (attention): MultiHeadSelfAttention(
            (dropout): Dropout(p=0.2, inplace=False)
            (q_lin): Linear(in_features=768, out_features=768, bias=True)
            (k_lin): Linear(in_features=768, out_features=768, bias=True)
            (v_lin): Linear(in_features=768, out_features=768, bias=True)
            (out_lin): Linear(in_features=768, out_features=768, bias=True)
          )
          (sa_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
          (ffn): FFN(
            (dropout): Dropout(p=0.2, inplace=False)
            (lin1): Linear(in_features=768, out_features=3072, bias=True)
            (lin2): Linear(in_features=3072, out_features=768, bias=True)
            (activation): GELUActivation()
          )
          (output_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
        )
        (1): TransformerBlock(
          (attention): MultiHeadSelfAttention(
            (dropout): Dropout(p=0.2, inplace=False)
            (q_lin): Linear(in_features=768, out_features=768, bias=True)
            (k_lin): Linear(in_features=768, out_features=768, bias=True)
            (v_lin): Linear(in_features=768, out_features=768, bias=True)
            (out_lin): Linear(in_features=768, out_features=768, bias=True)
          )
          (sa_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
          (ffn): FFN(
            (dropout): Dropout(p=0.2, inplace=False)
            (lin1): Linear(in_features=768, out_features=3072, bias=True)
            (lin2): Linear(in_features=3072, out_features=768, bias=True)
            (activation): GELUActivation()
          )
          (output_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
        )
        (2): TransformerBlock(
          (attention): MultiHeadSelfAttention(
            (dropout): Dropout(p=0.2, inplace=False)
            (q_lin): Linear(in_features=768, out_features=768, bias=True)
            (k_lin): Linear(in_features=768, out_features=768, bias=True)
            (v_lin): Linear(in_features=768, out_features=768, bias=True)
            (out_lin): Linear(in_features=768, out_features=768, bias=True)
          )
          (sa_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
          (ffn): FFN(
            (dropout): Dropout(p=0.2, inplace=False)
            (lin1): Linear(in_features=768, out_features=3072, bias=True)
            (lin2): Linear(in_features=3072, out_features=768, bias=True)
            (activation): GELUActivation()
          )
          (output_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
        )
        (3): TransformerBlock(
          (attention): MultiHeadSelfAttention(
            (dropout): Dropout(p=0.2, inplace=False)
            (q_lin): Linear(in_features=768, out_features=768, bias=True)
            (k_lin): Linear(in_features=768, out_features=768, bias=True)
            (v_lin): Linear(in_features=768, out_features=768, bias=True)
            (out_lin): Linear(in_features=768, out_features=768, bias=True)
          )
          (sa_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
          (ffn): FFN(
            (dropout): Dropout(p=0.2, inplace=False)
            (lin1): Linear(in_features=768, out_features=3072, bias=True)
            (lin2): Linear(in_features=3072, out_features=768, bias=True)
            (activation): GELUActivation()
          )
          (output_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
        )
        (4): TransformerBlock(
          (attention): MultiHeadSelfAttention(
            (dropout): Dropout(p=0.2, inplace=False)
            (q_lin): Linear(in_features=768, out_features=768, bias=True)
            (k_lin): Linear(in_features=768, out_features=768, bias=True)
            (v_lin): Linear(in_features=768, out_features=768, bias=True)
            (out_lin): Linear(in_features=768, out_features=768, bias=True)
          )
          (sa_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
          (ffn): FFN(
            (dropout): Dropout(p=0.2, inplace=False)
            (lin1): Linear(in_features=768, out_features=3072, bias=True)
            (lin2): Linear(in_features=3072, out_features=768, bias=True)
            (activation): GELUActivation()
          )
          (output_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
        )
        (5): TransformerBlock(
          (attention): MultiHeadSelfAttention(
            (dropout): Dropout(p=0.2, inplace=False)
            (q_lin): Linear(in_features=768, out_features=768, bias=True)
            (k_lin): Linear(in_features=768, out_features=768, bias=True)
            (v_lin): Linear(in_features=768, out_features=768, bias=True)
            (out_lin): Linear(in_features=768, out_features=768, bias=True)
          )
          (sa_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
          (ffn): FFN(
            (dropout): Dropout(p=0.2, inplace=False)
            (lin1): Linear(in_features=768, out_features=3072, bias=True)
            (lin2): Linear(in_features=3072, out_features=768, bias=True)
            (activation): GELUActivation()
          )
          (output_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
        )
      )
    )
  )
  (dropout): Dropout(p=0.2, inplace=False)
  (linear1): Linear(in_features=768, out_features=64, bias=True)
  (ReLu): ReLU()
  (linear2): Linear(in_features=64, out_features=5, bias=True)
)

Մենք դեռ պետք է սահմանենք մոդելի BERT մասի պարամետրերը «անպատրաստելի»:

for param in model_pt.dbert.parameters():
    param.requires_grad = False

Դիտարկենք պարամետրերի քանակը (ուսուցանվող և չմարզվող).

total_params = sum(p.numel() for p in model_pt.parameters())
total_params_trainable = sum(p.numel() for p in model_pt.parameters() if p.requires_grad)
print("Number of parameters: ", total_params)
print("Number of trainable parameters: ", total_params_trainable)
Number of parameters:  66412421
Number of trainable parameters:  49541

Հիանալի! Մենք ստանում ենք նույն ընդհանուր թվով պարամետրերը և վերապատրաստվող պարամետրերի քանակը, քան TensorFlow մոդելի համար: Այսպիսով, մենք, հավանաբար, բոլորովին սխալ չենք: Ընթերցողի համար, ով բաց է թողել TensorFlow բաժինը, մենք վերարտադրում ենք վերլուծությունը պարամետրերի քանակի վերաբերյալ.

Եկեք արագ ստուգենք, թե արդյոք վերապատրաստվող պարամետրերի քանակը (որոնք միայն խիտ շերտերում են, BERT-ը «սառեցված» է) իմաստ ունի: Վեկտորը, որը դուրս է գալիս BERT-ից, 768 չափի մեկ վեկտոր է (ըստ BERT մոդելի սահմանման): Այս տարրերից յուրաքանչյուրը կապված է խիտ շերտի 64 նեյրոններից յուրաքանչյուրին, ինչը հանգեցնում է 768x64=49'152 պարամետրերի: Յուրաքանչյուր նեյրոն ունի լրացուցիչ պարամետր՝ կողմնակալություն, այսինքն՝ 64 պարամետր: Խիտ շերտի ելքը բաղկացած է 64 տարրից, որոնք միանում են դասակարգման շերտի բոլոր հինգ տարրերին, այսինքն՝ 64x5: Դասակարգման շերտը նույնպես ունի 5 կողմնակալություն.

Ընդհանուր առմամբ, վերապատրաստման ենթակա պարամետրերի քանակը՝ 768x64+64+64*5+5 = 49'541: Մենք այնտեղ ենք :-)

Վարզել մոդելին

Այժմ ժամանակն է մարզել մոդելին: Եկեք դա անենք՝ նկատի ունենալով, որ մենք ցանկանում ենք չափել կատարողականը (ինչպես ճշգրտության, այնպես էլ մարզման ժամանակի առումով), այնպես որ եկեք չափումը տեղադրենք տեղում:

Այս փուլում մենք կնկատենք հիմնարար տարբերություն PyTorch-ի և TensorFlow-ի միջև։ Թեև TensorFlow-ն ունի «հարմարեցված» մեթոդ, որը թույլ է տալիս մարզվել կոդերի մի քանի տողում, PyTorch-ը պահանջում է ավելի շատ ջանք. Բայց ուսուցման օղակները ինքներս կոդավորելով՝ սա մեզ լիարժեք թափանցիկություն է ապահովում արվողի վերաբերյալ:

Մենք ցանկանում ենք հետևել ուսուցման առաջընթացին, ինչպես TensorFlow-ում հասանելի: Ահա թե ինչու ենք ներկայացնում «պատմություն» բառարանը, որը հավաքում է ուսուցման ընթացքում կատարողականի տարբեր հիմնական ցուցանիշներ:

Նախքան ուսուցումը սկսելը, մենք պետք է կարգավորենք դարաշրջանների քանակը, չափանիշը (ինչ է նվազագույնի հասցնելու գործառույթը) և օպտիմիզատորը:

Խնդրում ենք անցնել ստորև բերված կոդը, որը պետք է ինքնաբացատրելի լինի:

epochs = 5
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model_pt.parameters())
from tqdm import tqdm
# Define the dictionary "history" that will collect key performance indicators during training
history = {}
history["epoch"]=[]
history["train_loss"]=[]
history["valid_loss"]=[]
history["train_accuracy"]=[]
history["valid_accuracy"]=[]

# Measure time for training
start_time = datetime.now()

# Loop on epochs
for e in range(epochs):
    
    # Set mode in train mode
    model_pt.train()
    
    train_loss = 0.0
    train_accuracy = []
    
    # Loop on batches
    for X, y in tqdm(train_loader_pt):
        
        # Get prediction & loss
        prediction = model_pt(X)
        loss = criterion(prediction, y)
        
        # Adjust the parameters of the model
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        
        train_loss += loss.item()
        
        prediction_index = prediction.argmax(axis=1)
        accuracy = (prediction_index==y)
        train_accuracy += accuracy
    
    train_accuracy = (sum(train_accuracy) / len(train_accuracy)).item()
    
    # Calculate the loss on the test data after each epoch
    # Set mode to evaluation (by opposition to training)
    model_pt.eval()
    valid_loss = 0.0
    valid_accuracy = []
    for X, y in test_loader_pt:
         
        prediction = model_pt(X)
        loss = criterion(prediction, y)

        valid_loss += loss.item()
        
        prediction_index = prediction.argmax(axis=1)
        accuracy = (prediction_index==y)
        valid_accuracy += accuracy
    valid_accuracy = (sum(valid_accuracy) / len(valid_accuracy)).item()
    
    # Populate history
    history["epoch"].append(e+1)
    history["train_loss"].append(train_loss / len(train_loader_pt))
    history["valid_loss"].append(valid_loss / len(test_loader_pt))
    history["train_accuracy"].append(train_accuracy)
    history["valid_accuracy"].append(valid_accuracy)    
        
    print(f'Epoch {e+1} \t\t Training Loss: {train_loss / len(train_loader_pt) :10.3f} \t\t Validation Loss: {valid_loss / len(test_loader_pt) :10.3f}')
    print(f'\t\t Training Accuracy: {train_accuracy :10.3%} \t\t Validation Accuracy: {valid_accuracy :10.3%}')
    
# Measure time for training
end_time = datetime.now()
training_time_pt = (end_time - start_time).total_seconds()
100%|██████████| 49/49 [08:46<00:00, 10.74s/it]


Epoch 1 		 Training Loss:      1.099 		 Validation Loss:      0.537
		 Training Accuracy:    67.373% 		 Validation Accuracy:    90.419%


100%|██████████| 49/49 [08:31<00:00, 10.43s/it]


Epoch 2 		 Training Loss:      0.414 		 Validation Loss:      0.224
		 Training Accuracy:    91.908% 		 Validation Accuracy:    94.910%


100%|██████████| 49/49 [09:38<00:00, 11.81s/it]


Epoch 3 		 Training Loss:      0.220 		 Validation Loss:      0.155
		 Training Accuracy:    95.440% 		 Validation Accuracy:    94.760%


100%|██████████| 49/49 [09:16<00:00, 11.35s/it]


Epoch 4 		 Training Loss:      0.174 		 Validation Loss:      0.130
		 Training Accuracy:    95.633% 		 Validation Accuracy:    95.210%


100%|██████████| 49/49 [09:18<00:00, 11.40s/it]


Epoch 5 		 Training Loss:      0.143 		 Validation Loss:      0.109
		 Training Accuracy:    96.596% 		 Validation Accuracy:    96.407%
from matplotlib import pyplot as plt

fig, ax = plt.subplots(nrows=1, ncols=2, figsize=(15, 5))
ax[0].set(title='Loss')
ax[0].plot(history['train_loss'], label='Training')
ax[0].plot(history['valid_loss'], label='Validation')
ax[0].legend(loc="upper right")

ax[1].set(title='Accuracy')
ax[1].plot(history['train_accuracy'], label='Training')
ax[1].plot(history['valid_accuracy'], label='Validation')
ax[1].legend(loc="lower right")
<matplotlib.legend.Legend at 0x7fd708b60610>

accuracy_pt = history['valid_accuracy'][-1]
print('Accuracy Training data: {:.1%}'.format(history['train_accuracy'][-1]))
print('Accuracy Test data: {:.1%}'.format(history['valid_accuracy'][-1]))
print('Training time: {:.1f}s (or {:.1f} minutes)'.format(training_time_pt, training_time_pt/60))
Accuracy Training data: 96.6%
Accuracy Test data: 96.4%
Training time: 3604.6s (or 60.1 minutes)

Ուսուցման ժամանակը տևում է մեկ ժամ (պրոցեսորի վրա), ինչը այն դարձնում է ավելի արագ, քան TensorFlow-ը: Նշենք, որ մենք մի քանի անգամ անցկացրինք թրեյնինգը և տատանումներ նկատեցինք 10 տոկոսի տևողության մեջ։ Ճշգրտությունը լավ տեսք ունի (›95%): Մենք նաև նկատում ենք, որ ճշգրտությունը մեծանում է դարաշրջանների հետ, և փորձարկման և վավերացման ճշտությունները մոտ են միմյանց, ինչը նշանակում է, որ կարծես թե ավելորդ համապատասխանություն չկա: Մենք չենք ուսումնասիրի այն գրառումները, որոնք տվել են սխալ կանխատեսում, քանի որ դա չէ այս հոդվածի նպատակը:

Մեր մոդելը վերապատրաստված է: Այս մոդելը պահպանելու համար եկեք խնայենք այն:

Պահպանեք մոդելը

Մոդելը պահպանելու երկու մոտեցում կա, և մենք երկուսն էլ կանդրադառնանք այս բաժնում:

Հնարավոր է պահպանել միայն մոդելի պարամետրերը։ Այնուհետև մոդելը բեռնելու համար մենք նախ պետք է օրինականացնենք մոդելը (DistilBertClassification մոդելը), այնուհետև բեռնենք դրա բոլոր (ուսուցանված) պարամետրերը այս մոդելում: Սա մոդելը պահելու հարմար եղանակ է, սակայն դա հնարավոր է միայն այն դեպքում, եթե դուք ունեք ամբողջական մանրամասներ բնօրինակ մոդելի ճարտարապետության մասին:

Այլընտրանք է ողջ մոդելը փրկել: Դրանով ավելի հեշտ է բեռնել մոդելը իր պահպանված վայրից: Դիտարկենք երկու մոտեցումները.

# Save only the parameters of the model but not the model itself, and get it back
torch.save(model_pt.state_dict(), 'PyModel.sd')
model_reloaded = DistilBertClassification()
model_reloaded.load_state_dict(torch.load('PyModel.sd'))
model_reloaded.eval()
DistilBertClassification(
  (dbert): DistilBertModel(
    (embeddings): Embeddings(
      (word_embeddings): Embedding(30522, 768, padding_idx=0)
      (position_embeddings): Embedding(512, 768)
      (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
      (dropout): Dropout(p=0.2, inplace=False)
    )
    (transformer): Transformer(
      (layer): ModuleList(
        (0): TransformerBlock(
          (attention): MultiHeadSelfAttention(
            (dropout): Dropout(p=0.2, inplace=False)
            (q_lin): Linear(in_features=768, out_features=768, bias=True)
            (k_lin): Linear(in_features=768, out_features=768, bias=True)
            (v_lin): Linear(in_features=768, out_features=768, bias=True)
            (out_lin): Linear(in_features=768, out_features=768, bias=True)
          )
          (sa_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
          (ffn): FFN(
            (dropout): Dropout(p=0.2, inplace=False)
            (lin1): Linear(in_features=768, out_features=3072, bias=True)
            (lin2): Linear(in_features=3072, out_features=768, bias=True)
            (activation): GELUActivation()
          )
          (output_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
        )
        (1): TransformerBlock(
          (attention): MultiHeadSelfAttention(
            (dropout): Dropout(p=0.2, inplace=False)
            (q_lin): Linear(in_features=768, out_features=768, bias=True)
            (k_lin): Linear(in_features=768, out_features=768, bias=True)
            (v_lin): Linear(in_features=768, out_features=768, bias=True)
            (out_lin): Linear(in_features=768, out_features=768, bias=True)
          )
          (sa_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
          (ffn): FFN(
            (dropout): Dropout(p=0.2, inplace=False)
            (lin1): Linear(in_features=768, out_features=3072, bias=True)
            (lin2): Linear(in_features=3072, out_features=768, bias=True)
            (activation): GELUActivation()
          )
          (output_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
        )
        (2): TransformerBlock(
          (attention): MultiHeadSelfAttention(
            (dropout): Dropout(p=0.2, inplace=False)
            (q_lin): Linear(in_features=768, out_features=768, bias=True)
            (k_lin): Linear(in_features=768, out_features=768, bias=True)
            (v_lin): Linear(in_features=768, out_features=768, bias=True)
            (out_lin): Linear(in_features=768, out_features=768, bias=True)
          )
          (sa_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
          (ffn): FFN(
            (dropout): Dropout(p=0.2, inplace=False)
            (lin1): Linear(in_features=768, out_features=3072, bias=True)
            (lin2): Linear(in_features=3072, out_features=768, bias=True)
            (activation): GELUActivation()
          )
          (output_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
        )
        (3): TransformerBlock(
          (attention): MultiHeadSelfAttention(
            (dropout): Dropout(p=0.2, inplace=False)
            (q_lin): Linear(in_features=768, out_features=768, bias=True)
            (k_lin): Linear(in_features=768, out_features=768, bias=True)
            (v_lin): Linear(in_features=768, out_features=768, bias=True)
            (out_lin): Linear(in_features=768, out_features=768, bias=True)
          )
          (sa_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
          (ffn): FFN(
            (dropout): Dropout(p=0.2, inplace=False)
            (lin1): Linear(in_features=768, out_features=3072, bias=True)
            (lin2): Linear(in_features=3072, out_features=768, bias=True)
            (activation): GELUActivation()
          )
          (output_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
        )
        (4): TransformerBlock(
          (attention): MultiHeadSelfAttention(
            (dropout): Dropout(p=0.2, inplace=False)
            (q_lin): Linear(in_features=768, out_features=768, bias=True)
            (k_lin): Linear(in_features=768, out_features=768, bias=True)
            (v_lin): Linear(in_features=768, out_features=768, bias=True)
            (out_lin): Linear(in_features=768, out_features=768, bias=True)
          )
          (sa_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
          (ffn): FFN(
            (dropout): Dropout(p=0.2, inplace=False)
            (lin1): Linear(in_features=768, out_features=3072, bias=True)
            (lin2): Linear(in_features=3072, out_features=768, bias=True)
            (activation): GELUActivation()
          )
          (output_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
        )
        (5): TransformerBlock(
          (attention): MultiHeadSelfAttention(
            (dropout): Dropout(p=0.2, inplace=False)
            (q_lin): Linear(in_features=768, out_features=768, bias=True)
            (k_lin): Linear(in_features=768, out_features=768, bias=True)
            (v_lin): Linear(in_features=768, out_features=768, bias=True)
            (out_lin): Linear(in_features=768, out_features=768, bias=True)
          )
          (sa_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
          (ffn): FFN(
            (dropout): Dropout(p=0.2, inplace=False)
            (lin1): Linear(in_features=768, out_features=3072, bias=True)
            (lin2): Linear(in_features=3072, out_features=768, bias=True)
            (activation): GELUActivation()
          )
          (output_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
        )
      )
    )
  )
  (dropout): Dropout(p=0.2, inplace=False)
  (linear1): Linear(in_features=768, out_features=64, bias=True)
  (ReLu): ReLU()
  (linear2): Linear(in_features=64, out_features=5, bias=True)
)
# Save the entire model, and get it back
torch.save(model_pt, 'PyModelComplete.pt')
model_reloaded2 = torch.load('PyModelComplete.pt')
model_reloaded2.eval()
DistilBertClassification(
  (dbert): DistilBertModel(
    (embeddings): Embeddings(
      (word_embeddings): Embedding(30522, 768, padding_idx=0)
      (position_embeddings): Embedding(512, 768)
      (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
      (dropout): Dropout(p=0.2, inplace=False)
    )
    (transformer): Transformer(
      (layer): ModuleList(
        (0): TransformerBlock(
          (attention): MultiHeadSelfAttention(
            (dropout): Dropout(p=0.2, inplace=False)
            (q_lin): Linear(in_features=768, out_features=768, bias=True)
            (k_lin): Linear(in_features=768, out_features=768, bias=True)
            (v_lin): Linear(in_features=768, out_features=768, bias=True)
            (out_lin): Linear(in_features=768, out_features=768, bias=True)
          )
          (sa_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
          (ffn): FFN(
            (dropout): Dropout(p=0.2, inplace=False)
            (lin1): Linear(in_features=768, out_features=3072, bias=True)
            (lin2): Linear(in_features=3072, out_features=768, bias=True)
            (activation): GELUActivation()
          )
          (output_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
        )
        (1): TransformerBlock(
          (attention): MultiHeadSelfAttention(
            (dropout): Dropout(p=0.2, inplace=False)
            (q_lin): Linear(in_features=768, out_features=768, bias=True)
            (k_lin): Linear(in_features=768, out_features=768, bias=True)
            (v_lin): Linear(in_features=768, out_features=768, bias=True)
            (out_lin): Linear(in_features=768, out_features=768, bias=True)
          )
          (sa_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
          (ffn): FFN(
            (dropout): Dropout(p=0.2, inplace=False)
            (lin1): Linear(in_features=768, out_features=3072, bias=True)
            (lin2): Linear(in_features=3072, out_features=768, bias=True)
            (activation): GELUActivation()
          )
          (output_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
        )
        (2): TransformerBlock(
          (attention): MultiHeadSelfAttention(
            (dropout): Dropout(p=0.2, inplace=False)
            (q_lin): Linear(in_features=768, out_features=768, bias=True)
            (k_lin): Linear(in_features=768, out_features=768, bias=True)
            (v_lin): Linear(in_features=768, out_features=768, bias=True)
            (out_lin): Linear(in_features=768, out_features=768, bias=True)
          )
          (sa_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
          (ffn): FFN(
            (dropout): Dropout(p=0.2, inplace=False)
            (lin1): Linear(in_features=768, out_features=3072, bias=True)
            (lin2): Linear(in_features=3072, out_features=768, bias=True)
            (activation): GELUActivation()
          )
          (output_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
        )
        (3): TransformerBlock(
          (attention): MultiHeadSelfAttention(
            (dropout): Dropout(p=0.2, inplace=False)
            (q_lin): Linear(in_features=768, out_features=768, bias=True)
            (k_lin): Linear(in_features=768, out_features=768, bias=True)
            (v_lin): Linear(in_features=768, out_features=768, bias=True)
            (out_lin): Linear(in_features=768, out_features=768, bias=True)
          )
          (sa_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
          (ffn): FFN(
            (dropout): Dropout(p=0.2, inplace=False)
            (lin1): Linear(in_features=768, out_features=3072, bias=True)
            (lin2): Linear(in_features=3072, out_features=768, bias=True)
            (activation): GELUActivation()
          )
          (output_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
        )
        (4): TransformerBlock(
          (attention): MultiHeadSelfAttention(
            (dropout): Dropout(p=0.2, inplace=False)
            (q_lin): Linear(in_features=768, out_features=768, bias=True)
            (k_lin): Linear(in_features=768, out_features=768, bias=True)
            (v_lin): Linear(in_features=768, out_features=768, bias=True)
            (out_lin): Linear(in_features=768, out_features=768, bias=True)
          )
          (sa_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
          (ffn): FFN(
            (dropout): Dropout(p=0.2, inplace=False)
            (lin1): Linear(in_features=768, out_features=3072, bias=True)
            (lin2): Linear(in_features=3072, out_features=768, bias=True)
            (activation): GELUActivation()
          )
          (output_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
        )
        (5): TransformerBlock(
          (attention): MultiHeadSelfAttention(
            (dropout): Dropout(p=0.2, inplace=False)
            (q_lin): Linear(in_features=768, out_features=768, bias=True)
            (k_lin): Linear(in_features=768, out_features=768, bias=True)
            (v_lin): Linear(in_features=768, out_features=768, bias=True)
            (out_lin): Linear(in_features=768, out_features=768, bias=True)
          )
          (sa_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
          (ffn): FFN(
            (dropout): Dropout(p=0.2, inplace=False)
            (lin1): Linear(in_features=768, out_features=3072, bias=True)
            (lin2): Linear(in_features=3072, out_features=768, bias=True)
            (activation): GELUActivation()
          )
          (output_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
        )
      )
    )
  )
  (dropout): Dropout(p=0.2, inplace=False)
  (linear1): Linear(in_features=768, out_features=64, bias=True)
  (ReLu): ReLU()
  (linear2): Linear(in_features=64, out_features=5, bias=True)
)

Համեմատություն TensorFlow-ն ընդդեմ PyTorch-ի

Մենք անցել ենք դասակարգման մոդել՝ օգտագործելով նախապես պատրաստված BERT մոդելը և ճշգրտել այն Classificaiton վարժության համար: Մենք դա արել ենք թե՛ TensorFlow, թե՛ PyTorch շրջանակներում: Այս բաժնում մենք ընդգծում ենք երկու շրջանակների հիմնական տարբերությունները: Համեմատությունը միշտ որոշակի սուբյեկտիվություն է պարունակելու, ուստի խնդրում եմ տեղյակ եղեք, որ այն, ինչ հետևում է, հիմնականում ներկայացնում է իմ կարծիքը:

Ծրագրավորում

Տվյալները պատրաստելիս TensorFlow-ն ապացուցել է, որ շատ բազմակողմանի է, քանի որ հնարավոր է օգտագործել NumPy զանգվածներ, որոնք ձևաչափ են, որոնցում առկա են բազմաթիվ տվյալների հավաքածուներ և շատ հեշտ է շահարկել: TensorFlow-ն ունի նաև իր սեփական տենսորները (TensorFlow tensors) և տվյալների հավաքածուների իր ձևաչափերը (որոնք մենք չենք անդրադարձել այս հոդվածում, կարող եք մանրամասներ գտնել [ref], գլխում [ref]): PyTorch-ը, մեր լավագույն գիտելիքներով, պահանջում է օգտագործել իր սեփական PyTorch թենսորները, տվյալների հավաքածուները և տվյալների բեռնիչները: Սա ավելի խիստ է դարձնում տվյալների հավաքածուների պատրաստումը, բայց դա միանշանակ պայմանավորված է ավելի քիչ ճկունության գնով:

Վերապատրաստման համար TensorFlow-ն առաջարկում է բարձր մակարդակի շրջանակ (Keras), որը հատկապես դյուրին է դարձնում ուսուցումը, օգտագործելով «պիտանի» գործառույթը, որը նման է այն ամենին, ինչ Մեքենայի ուսուցման բոլոր ինժեներները գիտեն scikit Learning շրջանակից: TensorFlow-ի միջոցով հնարավոր է ավելի ցածր մակարդակի անցնել մարզումների ժամանակ, սակայն ոչ այն մակարդակի, որն առաջարկում է PyTorch-ը: PyTorch-ում ուսուցումը պետք է պատրաստվի ինքներդ, ցածր մակարդակի վրա, որտեղ անհրաժեշտ է սահմանել նույնիսկ դարաշրջանների և խմբաքանակների միջև եղած օղակները: Մարզման տարբեր քայլերը (առաջ, հետընթաց, պարամետրերի ճշգրտում) կատարվում են շատ հատիկավոր կերպով: PyTorch-ում մարզվելիս մենք զգում ենք, որ «մենք գիտենք, թե ինչ է կատարվում», ինչը շատ դրական է: Բայց, ցավոք, PyTorch-ում չկա բարձր մակարդակի շրջանակ (որքան մենք գիտենք), որը թույլ կտա արագ փորձարկել նոր մոդելներ:

Ուսուցում և մոդել

Բացի երկու շրջանակում ծրագրավորման տարբերությունը գնահատելուց, եկեք վերլուծենք ուսուցման ժամանակը և մոդելները, հատկապես մոդելների ճշգրտությունը:

framework = ['TensorFlow', 'PyTorch']
accuracy = [accuracy_tf, accuracy_pt]
accuracy = [str(round(acc*100, 1))+'%' for acc in accuracy]
training_time = [round(training_time_tf,1), round(training_time_pt,1)]
training_time_rounded = [str(round(tt,1)) for tt in training_time]
training_time = np.array(training_time)
training_time_x = list((training_time/min(training_time)-1)*100)
training_time_x = [str(round(ttx,1))+'%' for ttx in training_time_x]

dict = {'Framework' : framework,
        'Accuracy' : accuracy,
        'Training Time [s]' : training_time_rounded,
        'Training Time [% from best]' : training_time_x}
df = pd.DataFrame(dict)
display(df.style.hide_index())

Ուսուցման ժամանակը երկու շրջանակների համար է մոտ մեկ ժամ, PyTorch-ի համար մի փոքր ավելի արագ, քան TensorFlow-ի համար: Ճշգրտությունը մի փոքր ավելի լավ է TensorFlow-ի համար, քան PyTorch-ի համար: Ընդհանուր առմամբ, մենք կարող ենք համարել, որ երկու շրջանակներն էլ ունեն նմանատիպ կատարում՝ թե՛ ուսուցման ժամանակի և թե՛ ճշգրտության մեջ: Էական տարբերություն չկա։

Հղումներ

[1]: Devlin et al. (2018). «ԲԵՐՏ. Խորը երկկողմանի տրանսֆորմատորների նախնական ուսուցում լեզուների ըմբռնման համար» (https://arxiv.org/abs/1810.04805)

[2]: V. Sanh et al (2019), «DistilBERT, BERT-ի թորած տարբերակ. ավելի փոքր, արագ, էժան և թեթև» (https://arxiv.org/abs/1910.01108)