チュートリアル 2: 自然言語処理と大規模言語モデル(LLM)
第3週1日目:時系列と自然言語処理
Neuromatch Academyによる
コンテンツ作成者: Lyle Ungar, Jordan Matelsky, Konrad Kording, Shaonan Wang, Alish Dipani
コンテンツレビュアー: Shaonan Wang, Weizhe Yuan, Dalia Nasr, Stephen Kiilu, Alish Dipani, Dora Zhiyu Yang, Adrita Das
コンテンツ編集者: Konrad Kording, Shaonan Wang
制作編集者: Konrad Kording, Spiros Chavlis, Konstantine Tsafatinos
チュートリアルの目的
このチュートリアルでは、現代の自然言語処理(NLP)について包括的に解説します。影響力のある2つのNLPアーキテクチャであるBERTとGPTを紹介し、基盤となるNLPパイプラインを詳しく探ります。参加者はこれらのアーキテクチャの基本概念、機能、応用について学び、プロンプトエンジニアリングやGPTの現在および将来の展開についての洞察を得ることができます。
# @title Tutorial slides
from IPython.display import IFrame
link_id = "spuj8"
print(f"If you want to download the slides: https://osf.io/download/{link_id}/")
IFrame(src=f"https://mfr.ca-1.osf.io/render?url=https://osf.io/{link_id}/?direct%26mode=render%26action=download%26mode=render", width=854, height=480)
セットアップ
# @title Install dependencies
# @markdown **WARNING**: There may be *errors* and/or *warnings* reported during the installation. However, they are to be ignored.
# @title Install and import feedback gadget
from vibecheck import DatatopsContentReviewContainer
def content_review(notebook_section: str):
return DatatopsContentReviewContainer(
"", # No text prompt
notebook_section,
{
"url": "https://pmyvdlilci.execute-api.us-east-1.amazonaws.com/klab",
"name": "neuromatch_dl",
"user_key": "f379rz8y",
},
).render()
feedback_prefix = "W3D1_T2"
# Imports
import random
import numpy as np
from typing import Iterable, List
from tqdm.notebook import tqdm
from typing import Dict
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader, Dataset
from tokenizers import Tokenizer, Regex, models, normalizers, pre_tokenizers, trainers, processors
# @title Set random seed
# @markdown Executing `set_seed(seed=seed)` you are setting the seed
# for DL its critical to set the random seed so that students can have a
# baseline to compare their results to expected results.
# Read more here: https://pytorch.org/docs/stable/notes/randomness.html
# Call `set_seed` function in the exercises to ensure reproducibility.
import random
import numpy as np
def set_seed(seed=None):
if seed is None:
seed = np.random.choice(2 ** 32)
random.seed(seed)
np.random.seed(seed)
print(f'Random seed {seed} has been set.')
set_seed(seed=2023) # change 2023 with any number you like
# @title Set device (GPU or CPU). Execute `set_device()`
# Inform the user if the notebook uses GPU or CPU.
def set_device():
"""
Set the device. CUDA if available, CPU otherwise
Args:
None
Returns:
Nothing
"""
device = "cuda" if torch.cuda.is_available() else "cpu"
if device != "cuda":
print("WARNING: For this notebook to perform best, "
"if possible, in the menu under `Runtime` -> "
"`Change runtime type.` select `GPU` ")
else:
print("GPU is enabled in this notebook.")
return device
DEVICE = set_device()
SEED = 2021
set_seed(seed=SEED)
セクション1: NLPアーキテクチャ
RNN/LSTMからトランスフォーマーへ。
# @title Video 1: Intro to NLPs and LLMs
from ipywidgets import widgets
from IPython.display import YouTubeVideo
from IPython.display import IFrame
from IPython.display import display
class PlayVideo(IFrame):
def __init__(self, id, source, page=1, width=400, height=300, **kwargs):
self.id = id
if source == 'Bilibili':
src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'
elif source == 'Osf':
src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'
super(PlayVideo, self).__init__(src, width, height, **kwargs)
def display_videos(video_ids, W=400, H=300, fs=1):
tab_contents = []
for i, video_id in enumerate(video_ids):
out = widgets.Output()
with out:
if video_ids[i][0] == 'Youtube':
video = YouTubeVideo(id=video_ids[i][1], width=W,
height=H, fs=fs, rel=0)
print(f'Video available at https://youtube.com/watch?v={video.id}')
else:
video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,
height=H, fs=fs, autoplay=False)
if video_ids[i][0] == 'Bilibili':
print(f'Video available at https://www.bilibili.com/video/{video.id}')
elif video_ids[i][0] == 'Osf':
print(f'Video available at https://osf.io/{video.id}')
display(video)
tab_contents.append(out)
return tab_contents
video_ids = [('Youtube', 'PCz527-WbxY'), ('Bilibili', 'BV15V4y1a7Xu')]
tab_contents = display_videos(video_ids, W=854, H=480)
tabs = widgets.Tab()
tabs.children = tab_contents
for i in range(len(tab_contents)):
tabs.set_title(i, video_ids[i][0])
display(tabs)
自然言語処理の核心的な原理の一つは、単語をベクトルとして埋め込むことです。関連するベクトル空間内では、意味が似ている単語同士が近くに位置します。
古典的なトランスフォーマーシステムでは、エンコードとデコードが核心的な原理です。入力シーケンスをベクトルとしてエンコード(読み取った内容を暗黙的に符号化)し、そのベクトルをデコードして新しい文を生成します。例えば、シーケンス・トゥ・シーケンス(文の翻訳など)システムは、単語が埋め込まれた文を読み取り、それを全体のベクトルとしてエンコードします。次に、その文のエンコード結果をデコードして翻訳文を生成します。
現代のトランスフォーマーシステム(GPTなど)では、すべての単語が並列に処理されます。この意味で、トランスフォーマーはエンコード/デコードの考え方を一般化しています。この戦略の例として、GPTのような現代の大規模言語モデルが挙げられます。
# @title Submit your feedback
content_review(f"{feedback_prefix}_Intro_to_NLPs_and_LLMs_Video")
セクション2: NLPパイプライン
トークン化、事前学習、ファインチューニング
# @title Video 2: NLP pipeline
from ipywidgets import widgets
from IPython.display import YouTubeVideo
from IPython.display import IFrame
from IPython.display import display
class PlayVideo(IFrame):
def __init__(self, id, source, page=1, width=400, height=300, **kwargs):
self.id = id
if source == 'Bilibili':
src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'
elif source == 'Osf':
src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'
super(PlayVideo, self).__init__(src, width, height, **kwargs)
def display_videos(video_ids, W=400, H=300, fs=1):
tab_contents = []
for i, video_id in enumerate(video_ids):
out = widgets.Output()
with out:
if video_ids[i][0] == 'Youtube':
video = YouTubeVideo(id=video_ids[i][1], width=W,
height=H, fs=fs, rel=0)
print(f'Video available at https://youtube.com/watch?v={video.id}')
else:
video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,
height=H, fs=fs, autoplay=False)
if video_ids[i][0] == 'Bilibili':
print(f'Video available at https://www.bilibili.com/video/{video.id}')
elif video_ids[i][0] == 'Osf':
print(f'Video available at https://osf.io/{video.id}')
display(video)
tab_contents.append(out)
return tab_contents
video_ids = [('Youtube', 'uPnTVbc4qUE'), ('Bilibili', 'BV1TM4y1E7ab')]
tab_contents = display_videos(video_ids, W=854, H=480)
tabs = widgets.Tab()
tabs.children = tab_contents
for i in range(len(tab_contents)):
tabs.set_title(i, video_ids[i][0])
display(tabs)
# @title Submit your feedback
content_review(f"{feedback_prefix}_NLP_pipeline_Video")
トークナイザー
本日は埋め込み技術を実践し、現代のNLPスタックの重要な発展の一つであるトークン化について議論しながら、大規模言語モデルとトランスフォーマーに向けて進みます。トークナイザーは入力を離散的なトークンの集合に変換します。
学習目標
- トークン化の概念とその有用性を理解する。
- コンテキストを活用してゼロからトークナイザーを書く方法を学ぶ。
- 産業界で使われているいくつかの事前学習済みトークナイザーを使って、現代のトークナイザーの仕組みを直感的に理解する。
データセットの生成
「実用レベル」のNLPに近づくにつれて、HuggingFaceのような業界標準のライブラリを使い始めます。HuggingFaceは現代の深層学習システムの様々な側面の交換を促進する大手企業です。
まずはトレーニング用データセットを生成します。hfには便利なdatasetsモジュールがあり、Wikipediaテキストコーパス$など様々なデータセットをダウンロードできます。これを使ってWikipediaのテキストデータセットを生成します。
from datasets import load_dataset
dataset = load_dataset("wikitext", "wikitext-103-raw-v1", split="train")
print(dataset[41492])
def generate_n_examples(dataset, n=512):
"""
Produce a generator that yields n examples at a time from the dataset.
"""
for i in range(0, len(dataset), n):
yield dataset[i:i + n]['text']
これから実際のTokenizerを作成します。hf.Tokenizerプロトコル$に準拠します。(標準プロトコルに準拠することで、HuggingFaceエコシステム内の任意のトークナイザーと入れ替えたり、自作トークナイザーを任意のモデルに適用したりできます。)
トークナイザーを書く手順を概略します。解決すべき課題は2つです:
- 文字列を受け取り、それをトークンのリストに分割する。
- 認識できない単語があっても、何らかの方法でトークン化する。
これは、より豊かな語彙を持つワンホットエンコーダーを再発明しているように感じるかもしれません。なぜ語彙サイズのベクトルを出力するワンホットエンコーダーでは不十分で、語彙サイズのインデックスリストを出力するトークナイザーが十分なのでしょうか?答えは、エンコーダーは単語を高次元空間に埋め込む役割を担っていましたが、トークナイザーはそうではなく、文字列を語彙内の要素に分割することに成功している点にあります。あるワークフローでは、トークナイザーの直後に埋め込み器を付け加えることもあります。(これは現代のトランスフォーマーモデルで採用されている戦略そのものです。)
トークンはほぼ常に単語とは異なります。例えば、「don't」を「do」と「n't」に分割したり、「do」と「not」に分割したり、あるいは「d」「o」「n」「t」に分割したりすることも可能です。ここでは任意の戦略を選べます。**Word2Vecとは異なり、トークナイザーは英単語ごとに1つのベクトルを出力する制限はありません。**ここでは、後述する市販のサブワード分割器を使用します。
VOCAB_SIZE = 12_000
# Create a tokenizer object that uses the "WordPiece" model. The WorkPiece model
# is a subword tokenizer that uses a vocabulary of common words and word pieces
# to tokenize text. The "unk_token" parameter specifies the token to use for
# unknown tokens, i.e. tokens that are not in the vocabulary. (Remember that the
# vocabulary will be built from our dataset, so it will include subchunks of
# English words.)
tokenizer = Tokenizer(models.WordPiece(unk_token="[UNK]"))
トークナイザーの機能
まずはテキストのクリーンアップから始めましょう。この処理は正式には「正規化」と呼ばれ、NLPパイプラインの重要なステップです。句読点を除去し、すべて小文字に変換します。また、テキストからダイアクリティカルマーク(アクセント記号)も除去します。
# Think of a Normalizer Sequence the same way you would think of a PyTorch
# Sequential model. It is a sequence of normalizers that are applied to the
# text before tokenization, in the order that they are added to the sequence.
tokenizer.normalizer = normalizers.Sequence([
normalizers.Replace(Regex(r"[\s]"), " "), # Convert all whitespace to single space
normalizers.Lowercase(), # Convert all text to lowercase
normalizers.NFD(), # Decompose all characters into their base characters
normalizers.StripAccents(), # Remove all accents
])
次にプレトークナイザーを追加します。プレトークナイザーは正規化後、トークン化の前にテキストに適用されます。プレトークナイザーはテキストをトークン化しやすいチャンクに分割するのに役立ちます。例えば、句読点や空白で区切ってチャンクに分割できます。
tokenizer.pre_tokenizer = pre_tokenizers.Sequence([
pre_tokenizers.WhitespaceSplit(), # Split on whitespace
pre_tokenizers.Digits(individual_digits=True), # Split digits into individual tokens
pre_tokenizers.Punctuation(), # Split punctuation into individual tokens
])
注意: 実際にはプレトークナイザーを使う必要はありませんが、ここでは説明のために使います。例えば「2-3」は「23」とは異なるため、句読点を除去したり数字や句読点を分割したりするのは良くない場合があります。さらに、現在のトークナイザーは句読点にも十分対応できる強力なものです。
最後に、データセットを使ってトークナイザーを訓練します。トークナイザーはこのデータセットでうまく機能することが望ましいです。トークナイザーの訓練にはいくつかのアルゴリズムがあります。代表的なものは以下の2つです:
- BPEアルゴリズム:データセット内の各文字を語彙として開始し、語彙内のすべてのペアを調べて、データセットで最も頻度の高いペアをマージします。語彙サイズに達するまで繰り返します(例えば英語コーパスでは「ee」が「zf」よりもマージされやすい)。
- トップダウンWordPieceアルゴリズム:データセット内の各単語のすべての部分文字列を生成し、トレーニングデータでの出現回数をカウントします。閾値以上の頻度の文字列を保持し、語彙サイズに達するまで繰り返します(このプロセスの詳細はTensorFlowガイド$を参照)。
次のセルではWordPieceを使います。
tokenizer_trainer = trainers.WordPieceTrainer(
vocab_size=VOCAB_SIZE,
# We have to specify the special tokens that we want to use. These will be
# added to the vocabulary no matter what the vocab-building algorithm does.
special_tokens=["[PAD]", "[UNK]", "[CLS]", "[SEP]", "[MASK]"],
show_progress=True,
)
特殊トークン
トークナイザーには、以下のような特定の概念を表す特殊トークンがよくあります:
- [PAD]:バッチ内の入力シーケンスの長さを揃えるため、短い入力の末尾に追加される
- [START]:シーケンスの開始
- [END]:シーケンスの終了
- [UNK]:語彙に存在しない未知の文字
- [BOS]:文の開始
- [EOS]:文の終了
- [SEP]:シーケンス内の2つの文の区切り
- [CLS]:分類タスクでシーケンス全体を表すために使われるトークン
- [MASK]:BERTのようなモデルの事前学習フェーズでマスク言語モデルタスクに使われる
これらの特殊トークンは、WordPieceの訓練プロセスにおいてフレーズ、マスク、未知トークンの扱い方を指示するため重要です。
注意: 必要に応じて、[CITE]のような独自の特殊トークンを追加することもできます。例えば、テキスト内の引用の存在を予測するモデルを訓練したい場合に使います。訓練には少し時間がかかります。
sample_ratio = 0.2
keep = int(len(dataset)*sample_ratio)
dataset_small = load_dataset("wikitext", "wikitext-103-raw-v1", split=f"train[:{keep}]")
tokenizer.train_from_iterator(generate_n_examples(dataset_small), trainer=tokenizer_trainer, length=len(dataset_small))
# In "real life", we'd probably want to save the tokenizer to disk so that we
# can use it later. We can do this with the "save" method:
# tokenizer.save("tokenizer.json")
# Let's try it out!
print("Hello, world!")
print(
*zip(
tokenizer.encode("Hello, world!").tokens,
tokenizer.encode("Hello, world!").ids,
)
)
# Can we also tokenize made-up words?
print(tokenizer.encode("These toastersocks are so groommpy!").tokens)
(##はトークンが前のチャンクの続きであることを示します。)
ハイパーパラメータやトークン化アルゴリズムをいじって、トークナイザーの出力がどう変わるか試してみてください。大きな違いが出ることもあります!
まとめると、以下のトークナイザーパイプラインを作成しました:
- テキストの正規化(句読点やダイアクリティカルマークの除去)
- テキストをチャンクに分割(空白や句読点を利用)
- データセットを使ってトークナイザーを訓練(WordPieceアルゴリズム使用)
一般的には、これが現代のNLPパイプラインの最初のステップです。次のステップはトークナイザーの後に埋め込み器を追加し、高次元空間をモデルに入力できるようにすることです。しかしWord2Vecとは異なり、トークン化と埋め込みを分離できるため、エンコード/埋め込み処理をタスク固有に、下流のニューラルネットアーキテクチャに合わせてカスタマイズできます。
Think 2.1! トークナイザーの良い実践
トークナイザーは語彙外の単語も扱えるため、ワンホットエンコーダーより優れていることがわかりました。しかし、もし語彙をすべての2文字の組み合わせにしたワンホットエンコーディングを作ったらどうでしょう?それでもトークナイザーに利点はありますか?
ヒント: BPEとWordPieceアルゴリズムのセクションを再読し、トークンの選択方法を考えてみてください。
# @title Submit your feedback
content_review(f"{feedback_prefix}_Tokenizer_good_practices_Discussion")
Think 2.2: 中国語と英語のトークナイザー
中国語のような言語を考えてみましょう。中国語の単語は英語に比べて文字数が少ない(例えば「hungry」は6文字のUnicodeですが、「饿」は1文字のUnicode)ですが、英語のアルファベットよりはるかに多くのユニークな中国文字があります。
1〜2文の概要で、中国語トークナイザーに望ましい特性は何でしょうか?
# @title Submit your feedback
content_review(f"{feedback_prefix}_Chinese_and_English_tokenizer_Discussion")
セクション3: BERTの使用
このセクションでは、huggingfaceのBERTモデルの使い方を学びます。
学習目標
- BERTの基本的な考え方を理解する
- 事前学習とファインチューニングの概念を理解する
- ネットワークの一部を固定(フリーズ)することの有用性を理解する
# @title Video 3: BERT
from ipywidgets import widgets
from IPython.display import YouTubeVideo
from IPython.display import IFrame
from IPython.display import display
class PlayVideo(IFrame):
def __init__(self, id, source, page=1, width=400, height=300, **kwargs):
self.id = id
if source == 'Bilibili':
src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'
elif source == 'Osf':
src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'
super(PlayVideo, self).__init__(src, width, height, **kwargs)
def display_videos(video_ids, W=400, H=300, fs=1):
tab_contents = []
for i, video_id in enumerate(video_ids):
out = widgets.Output()
with out:
if video_ids[i][0] == 'Youtube':
video = YouTubeVideo(id=video_ids[i][1], width=W,
height=H, fs=fs, rel=0)
print(f'Video available at https://youtube.com/watch?v={video.id}')
else:
video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,
height=H, fs=fs, autoplay=False)
if video_ids[i][0] == 'Bilibili':
print(f'Video available at https://www.bilibili.com/video/{video.id}')
elif video_ids[i][0] == 'Osf':
print(f'Video available at https://osf.io/{video.id}')
display(video)
tab_contents.append(out)
return tab_contents
video_ids = [('Youtube', 'u4D-84Z1Fxs'), ('Bilibili', 'BV17u411b7gJ')]
tab_contents = display_videos(video_ids, W=854, H=480)
tabs = widgets.Tab()
tabs.children = tab_contents
for i in range(len(tab_contents)):
tabs.set_title(i, video_ids[i][0])
display(tabs)
# @title Submit your feedback
content_review(f"{feedback_prefix}_BERT_Video")
セクション4: GPTによる自然言語生成(NLG)
このセクションでは、Generative Pretrained Transformersを用いた自然言語生成について学びます。
学習目標
- GPTを使った言語生成の方法を理解する
# @title Video 4: NLG
from ipywidgets import widgets
from IPython.display import YouTubeVideo
from IPython.display import IFrame
from IPython.display import display
class PlayVideo(IFrame):
def __init__(self, id, source, page=1, width=400, height=300, **kwargs):
self.id = id
if source == 'Bilibili':
src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'
elif source == 'Osf':
src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'
super(PlayVideo, self).__init__(src, width, height, **kwargs)
def display_videos(video_ids, W=400, H=300, fs=1):
tab_contents = []
for i, video_id in enumerate(video_ids):
out = widgets.Output()
with out:
if video_ids[i][0] == 'Youtube':
video = YouTubeVideo(id=video_ids[i][1], width=W,
height=H, fs=fs, rel=0)
print(f'Video available at https://youtube.com/watch?v={video.id}')
else:
video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,
height=H, fs=fs, autoplay=False)
if video_ids[i][0] == 'Bilibili':
print(f'Video available at https://www.bilibili.com/video/{video.id}')
elif video_ids[i][0] == 'Osf':
print(f'Video available at https://osf.io/{video.id}')
display(video)
tab_contents.append(out)
return tab_contents
video_ids = [('Youtube', 'vwFMHitq-FY'), ('Bilibili', 'BV1Hu411b7dx')]
tab_contents = display_videos(video_ids, W=854, H=480)
tabs = widgets.Tab()
tabs.children = tab_contents
for i in range(len(tab_contents)):
tabs.set_title(i, video_ids[i][0])
display(tabs)
# @title Submit your feedback
content_review(f"{feedback_prefix}_NLG_Video")
最先端(SOTA)モデルの利用
独自の実験的な深層学習研究を書いている場合(そして場合によってはそれ以外でも)、現在ではHuggingFaceのモデルライブラリを使って最先端モデルを素早くインポートし、すぐに作業を始めることがはるかに一般的です。このセクションでは、その方法を紹介します。
hfのtransformersライブラリから事前学習済みのテキスト生成モデルをダウンロードします。次に、hf.datasetsライブラリとHuggingFaceのTrainerクラスを使って別のデータセットでファインチューニングを行い、このプロセスをできるだけ簡単にします。これらはわずか数行のメンテナンスしやすいコードで実現可能です。
最終的に、コード生成のための_動作する_ジェネレーターを手に入れます。
まずはトークナイザーを選びます。いくつかのオプションはこちら$で確認できます。今回はBPEトークナイザーであるCodeParrotトークナイザーを使いますが、もし挑戦してみたいなら別のものを選ぶ(または作る)こともできます!
from transformers import AutoTokenizer
from datasets import load_dataset
tokenizer = AutoTokenizer.from_pretrained("codeparrot/codeparrot-small")
Think 4.1! トークナイザー
なぜ元々使われていたものとは異なるトークナイザーを使うことができるのでしょうか?このタスクに使う別のトークナイザーにはどんな要件が必要でしょうか?
# @title Submit your feedback
content_review(f"{feedback_prefix}_Tokenizers_Discussion")
次に、事前構築されたモデルアーキテクチャをダウンロードします。CodeParrot(モデル)はGPT-2モデルで、トランスフォーマーベースの言語モデルです。いくつかのオプションはこちら$で確認できますが、別のものを選ぶ(または作る)ことも可能です!
codeparrot/codeparrot (https://huggingface.co/codeparrot/codeparrot) は約7GBのダウンロードが必要なので(時間がかかるか、無料のColab環境では容量オーバーになる可能性があります)、代わりに小さいモデルである codeparrot/codeparrot-small (https://huggingface.co/codeparrot/codeparrot-small) を使います。こちらは約500MBです。
トークナイズ、モデル、デトークナイズをまとめて実行するには、transformersのpipeline関数を使うことができます。
from transformers import AutoModelForCausalLM
from transformers import pipeline
model = AutoModelForCausalLM.from_pretrained("codeparrot/codeparrot-small")
generation_pipeline = pipeline(
"text-generation", # The task to run. This tells hf what the pipeline steps are
model=model, # The model to use; can also pass the string here;
tokenizer=tokenizer, # The tokenizer to use; can also pass the string name here.
)
input_prompt = '''\
def simple_add(a: int, b: int) -> int:
"""
Adds two numbers together and returns the result.
"""'''
# Return tensors for PyTorch:
inputs = tokenizer(input_prompt, return_tensors="pt")
これらのトークンはトークナイザーの語彙内の整数インデックスであることを思い出してください。トークナイザーを使ってこれらのトークンを文字列にデコードし、モデルが生成したものを確認できます。
input_token_ids = inputs["input_ids"]
input_strs = tokenizer.convert_ids_to_tokens(*input_token_ids.tolist())
print(*zip(input_strs, input_token_ids[0]))
(クイック知識チェック:変な表示になっている文字は何を表していますか?)
このモデルはすでに使える状態です!試してみましょう。(inputsは初期のトークナイズ手順を示すために生成しただけで、実際には使いません。)
ここでは先ほど作成したpipelineを使ってすべてのコンポーネントを組み合わせています。もしCopilotのようなコード補完ツールを作るなら、この1行をきれいなAPIでラップして呼び出すだけで済みます!
ハイパーパラメータをいじって、どんな出力が得られるか試してみてください。Temperatureはモデルの予測にどれだけランダム性を加えるかを示します。温度が高いほどランダム性が増え、低いほど減ります。潜在空間のランダム性が増すと、より自由で創造的な予測が得られる可能性があります。最初の目安は0.2です。また、生成されるコードの長さを制御するmax_lengthパラメータを変えてみるのも良いでしょう(ただしモデルは途中で「停止」トークンを入れることもあるので、必ずしも指定したトークン数を生成するとは限りません)。
outputs = generation_pipeline(input_prompt, max_length=100, num_return_sequences=1, temperature=0.2, truncation=True)
print(outputs[0]["generated_text"])
今度はモデルを騙せるか試してみましょう!huggingfaceのドキュメントによると、codeparrotモデルはPythonコードを生成するように訓練されています(ドキュメント)。JavaScriptコードを生成させてみましょう。
input_prompt = "class SimpleAdder {"
print(generation_pipeline(input_prompt, max_length=100, num_return_sequences=1, temperature=0.2)[0]["generated_text"])
うわっ!あなたの環境で何が生成されたかわかりませんが、私のところでは以下のようなコードが生成されました:
class SimpleAdder {
public:
class SimpleAdder(object):
def __init__(self, a, b):
self.a = a
self.b = b
def __call__(self, x):
return self.a + x
うーん! これは多くの点で間違っています。でも理解はできます:モデルは訓練されたドメイン外のことをうまく一般化できません。おそらくPythonファイルの中に他言語の構文(例えば他のコードのジェネレーター)が混ざっていたのでしょう。モデルは中括弧を使う謎の構文があることは知っていますが、それ以外はよくわかっていません。(プログラミング言語好きの方へ:publicという表記はC系やJava系の構文を真似しようとしているように見えますが、JavaScriptではありません。)
主な観察点は?
- 生成される構文はすぐにPythonに戻ってしまい、Python以外の文字は数文字しか予測できません。
- Python構文の部分は有効で役立ちそうなクラス定義に見えます(ただしよく見ると
b属性は特に使われていません)。これはモデルが問題領域を「理解」しているが、新しい問題を解くための正しいデータで訓練されていないことを示しています。
Think 4.2! 最先端モデルの利用
生成されたコードについて他にどんな観察がありますか?あなたはトランスフォーマーの仕組みを理解しています。
- 機械学習の実務者として、このエラーを本番システムで診断する立場ならどんな観察をするでしょうか?
- 非専門家のユーザーはこの問題をどう解釈するでしょうか?
- この出力に対するモデルの信頼度は高い、低い、それとも中間だと思いますか?
# @title Submit your feedback
content_review(f"{feedback_prefix}_Using_SOTA_models_Discussion")
ファインチューニング
さて、コードを生成できるモデルができました。次はJavaScriptを生成するようにファインチューニングします。
データが大きすぎてColabのディスクに収まらないことを想定し、load_dataset関数でデータセットの一部だけをダウンロードします。codeparrotデータセットにはJavaScriptのサブセットがあり、例として使いますが、好きなデータセットを使って構いません!タスクカテゴリ(例:テキスト生成)でフィルタリングして関連性の高いデータセットを選ぶことをおすすめします。もちろん、データローダーを設定できればどんなデータセットでも使えます。(例えばこちらなど。)
HuggingFaceデータセットライブラリからデータセットを選んでください。
# Unlike _some_ code-generator models on the market, we'll limit our training data by license :)
dataset = load_dataset(
"codeparrot/github-code",
streaming=True,
split="train",
languages=["JavaScript"],
licenses=["mit", "isc", "apache-2.0"],
trust_remote_code=True,
)
# Print the schema of the first example from the training set:
print({k: type(v) for k, v in next(iter(dataset)).items()})
どのモデルを訓練する場合でも、訓練ループと評価指標を定義する必要があります。
これはtransformersライブラリで非常に簡単にできます。以下のコードを見て、huggingfaceのインフラを使わずに済む部分がどれだけあるか確認してください。(過去にはPyTorch Lightningを使っていましたが、こちらも似たような訓練ループの抽象化があります。どちらのライブラリが好みですか?)
モデルのファインチューニングコードを実装しよう
以下で行う主な処理は:
TrainingArgumentsオブジェクトの作成。 これはシリアライズ可能(メモリやディスクに保存可能)で、同じハイパーパラメータで再現可能にモデルを訓練しやすくします(ノートブック内にグローバル変数をたくさん置くよりずっと良いです)。- データセットのエンコード。 実質的にすべてのデータをトークナイザーに通し、シーケンスの末尾をパディングトークンで埋める処理です。
- 評価指標の定義。 ここでは
accuracy(正確度)を使います(コードセルの4行目を参照)。 - データコラレータの作成。 これは例のリストを受け取り、バッチを返す関数です。
DataCollatorForLanguageModelingクラスが便利です。 Trainerオブジェクトの作成。 訓練ループをラップし、モデルの訓練を簡単にします。PyTorch LightningのTrainerに似ていますが、より柔軟で非PyTorchモデルにも対応しています。
from transformers import TrainingArguments, Trainer, DataCollatorForLanguageModeling
from evaluate import load
metric = load("accuracy")
# Trainer:
training_args = TrainingArguments(
output_dir="./codeparrot",
max_steps=100,
per_device_train_batch_size=1,
report_to="none",
)
tokenizer.pad_token = tokenizer.eos_token
encoded_dataset = dataset.map(
lambda x: tokenizer(x["code"], truncation=True, padding="max_length"),
batched=True,
remove_columns=["code"],
)
# Metrics for loss:
def compute_metrics(eval_pred):
predictions, labels = eval_pred
predictions = np.argmax(predictions, axis=-1)
return metric.compute(predictions=predictions, references=labels)
# Data collator:
data_collator = DataCollatorForLanguageModeling(
tokenizer=tokenizer, mlm=False,
)
# Trainer:
trainer = Trainer(
model=model,
args=training_args,
train_dataset=encoded_dataset,
processing_class=tokenizer,
compute_metrics=compute_metrics,
data_collator=data_collator,
)
# Run the actual training:
trainer.train()
コーディング演習 4.1: ファインチューニング後のテキスト生成コードを実装しよう
テキストを生成するには、モデルに入力トークンを与え、次のトークンを生成させてそれを入力トークンに追加します。この処理を望む出力長に達するまで繰り返します。
def generate_text(model, input_prompt):
# Number of tokens to generate
num_tokens = 100
# Move the model to the CPU for inference
model.to("cpu")
# Print input prompt
print(f'Input prompt: \n{input_prompt}')
#################################################
# Implement a the correct tokens and outputs
raise NotImplementedError("Text Generation")
#################################################
# Encode the input prompt
# https://huggingface.co/docs/transformers/en/main_classes/tokenizer
input_tokens = ...
# Turn off storing gradients
with torch.no_grad():
# Keep iterating until num_tokens are generated
for tkn_idx in tqdm(range(num_tokens)):
# Forward pass through the model
# The model expects the tensor to be of Long or Int dtype
output = ...
# Get output logits
logits = output.logits[-1, :]
# Convert into probabilities
probs = nn.functional.softmax(logits, dim=-1)
# Get the index of top token
top_token = ...
# Append the token into the input sequence
input_tokens.append(top_token)
# Decode and print the generated text
# https://huggingface.co/docs/transformers/en/main_classes/tokenizer
decoded_text = ...
return decoded_text
# print(f'Generated text: \n{generate_text(model, input_prompt)}')
もちろん結果は少し異なるかもしれません。私のところでは以下のようになりました:
class SimpleAdder {
constructor(a, b) {
this.a = a;
this.b = b;
}
add(
ずっと良くなりました!モデルはもうPythonコードを生成せず、Python風の構文を他言語に無理に押し込もうともしません。まだ完璧ではありませんが、以前よりずっと良いです!(もちろんこれは小さなモデルで短時間しか訓練していないので、もっと長く訓練するか大きなモデルを使うとさらに良くなります。)
# @title Submit your feedback
content_review(f"{feedback_prefix}_FineTune_the_model_Exercise")
Think 4.3! 正確度評価指標の観察
なぜ正確度はこのタスクにとって悪い評価指標かもしれませんか?
ヒント:このタスクで「正確である」とはどういう意味でしょう?
# @title Submit your feedback
content_review(f"{feedback_prefix}_Accuracy_metric_observations_Discussion")
セクション5: GPTの現在と未来
現在のモデルの限界について。
# @title Video 5: Conclusion
from ipywidgets import widgets
from IPython.display import YouTubeVideo
from IPython.display import IFrame
from IPython.display import display
class PlayVideo(IFrame):
def __init__(self, id, source, page=1, width=400, height=300, **kwargs):
self.id = id
if source == 'Bilibili':
src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'
elif source == 'Osf':
src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'
super(PlayVideo, self).__init__(src, width, height, **kwargs)
def display_videos(video_ids, W=400, H=300, fs=1):
tab_contents = []
for i, video_id in enumerate(video_ids):
out = widgets.Output()
with out:
if video_ids[i][0] == 'Youtube':
video = YouTubeVideo(id=video_ids[i][1], width=W,
height=H, fs=fs, rel=0)
print(f'Video available at https://youtube.com/watch?v={video.id}')
else:
video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,
height=H, fs=fs, autoplay=False)
if video_ids[i][0] == 'Bilibili':
print(f'Video available at https://www.bilibili.com/video/{video.id}')
elif video_ids[i][0] == 'Osf':
print(f'Video available at https://osf.io/{video.id}')
display(video)
tab_contents.append(out)
return tab_contents
video_ids = [('Youtube', 'n1T8X0NiFqo'), ('Bilibili', 'BV1Ha4y1w73S')]
tab_contents = display_videos(video_ids, W=854, H=480)
tabs = widgets.Tab()
tabs.children = tab_contents
for i in range(len(tab_contents)):
tabs.set_title(i, video_ids[i][0])
display(tabs)
# @title Submit your feedback
content_review(f"{feedback_prefix}_Conclusion_Video")
LLMを試してみよう
- LLMのAPIを使って、例えばGPT-2のAPIで与えられた文脈からテキストを拡張するタスクを試してみましょう。そのためにはHuggingFaceアカウントを作成し、APIトークンを取得してください。
import requests
def query(payload, model_id, api_token):
headers = {"Authorization": f"Bearer {api_token}"}
API_URL = f"https://api-inference.huggingface.co/models/{model_id}"
response = requests.post(API_URL, headers=headers, json=payload)
return response.json()\
model_id = "gpt2"
api_token = "hf_****" # get yours at hf.co/settings/tokens
data = query("The goal of life is", model_id, api_token)
print(data)
- ChatGPT(ウェブアクセスなしのGPT3.5)と、クリエイティブモードのGPTBing(ウェブアクセスありのGPT4、Microsoft Edgeのインストールが必要)で以下の質問を試してみましょう。
あまり有名すぎない(マスクやトランプではない)ウェブ上に情報がありそうな知り合いを1人選び、その2段落の略歴をGPTに書かせてみてください。どのくらい良いでしょうか?
「過去10年間の米国、英国、ドイツ、中国、日本の一人当たり所得は?単一の図にプロットしてください」(実行環境によっては生成されたPythonコードをColabに貼り付ける必要があります)。データや「一人当たり所得」の定義について質問してみてください。どのくらい良いでしょうか?
# @title Submit your feedback
content_review(f"{feedback_prefix}_Play_around_with_LLMs_Activity")
まとめ
このチュートリアルでは、現代の自然言語処理(NLP)アーキテクチャに慣れ親しみました。これらのアーキテクチャのコアコンセプト、機能、応用について学びました。また、プロンプトエンジニアリングの知見を得て、GPTについても学びました。
ボーナスセクション: 大規模言語モデル(LLM)の活用
このビデオでは、大規模言語モデルが現在どのように使われているか、そしてあなたがどう使えるかを紹介します。例えば、個別指導、言語練習、文章改善、試験準備、執筆支援、データサイエンスなどです。
# @title Video 6: Using GPT
from ipywidgets import widgets
from IPython.display import YouTubeVideo
from IPython.display import IFrame
from IPython.display import display
class PlayVideo(IFrame):
def __init__(self, id, source, page=1, width=400, height=300, **kwargs):
self.id = id
if source == 'Bilibili':
src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'
elif source == 'Osf':
src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'
super(PlayVideo, self).__init__(src, width, height, **kwargs)
def display_videos(video_ids, W=400, H=300, fs=1):
tab_contents = []
for i, video_id in enumerate(video_ids):
out = widgets.Output()
with out:
if video_ids[i][0] == 'Youtube':
video = YouTubeVideo(id=video_ids[i][1], width=W,
height=H, fs=fs, rel=0)
print(f'Video available at https://youtube.com/watch?v={video.id}')
else:
video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,
height=H, fs=fs, autoplay=False)
if video_ids[i][0] == 'Bilibili':
print(f'Video available at https://www.bilibili.com/video/{video.id}')
elif video_ids[i][0] == 'Osf':
print(f'Video available at https://osf.io/{video.id}')
display(video)
tab_contents.append(out)
return tab_contents
video_ids = [('Youtube', 'JdXfuj6RP4Y'), ('Bilibili', 'BV1eX4y1v7c8')]
tab_contents = display_videos(video_ids, W=854, H=480)
tabs = widgets.Tab()
tabs.children = tab_contents
for i in range(len(tab_contents)):
tabs.set_title(i, video_ids[i][0])
display(tabs)
# @title Submit your feedback
content_review(f"{feedback_prefix}_What_models_Video")