Day 2 (11/4)

大規模言語モデルとは

大規模言語モデル(Large Language Model; LLM)とは、単純に言えば「文字を受け取り、文字を出力する、機械学習モデル」です。みなさんにとってはおなじみであるChatGPTのように、自然言語を入力して自然言語で返事をしてくれるAIサービスの基盤となっている技術が、LLMです。LLMは大量の文字データによって学習されており、様々な複雑な処理を実行することが出来ます。例えば、翻訳、要約、調査、コード生成、などなどです。

LLMの単純な概要図は上図のようになります。LLMは文字列を受け取り、次に来るべき単語を予測します。例えば「むかしむかし、あるところに」という文字列が入力されたとして、次に来る文字列の候補として

  • ラーメン大好きな大学院生が

  • おじいさんとおばあさんが

  • にがい珈琲が

の3つを考えると、明らかに2つ目の「おじいさんと・・」が自然です。自然というのは、そのような文章がより多く存在しており、故に、LLMの学習に使用されており、LLMはそれの確率が高いと解釈する、という意味です。

LLMは技術的にも応用的な使われ方も非常に高度で幅広いですが、基本的な考え方は上図のようにシンプルです。このシンプルさが、LLMが広く発展している理由と言えるでしょう。上記のように

  • 文字列入力・文字列出力

  • (訓練データによって定義される「自然さ」において)自然なものを返す

という枠組みは、翻訳や要約に直接的に応用できます。

歴史

さて、LLMの歴史をすこし振り返ってみましょう。「大規模」言語モデルというぐらいなので、もともと言語を扱う研究ははるか昔から存在しています。そのように文字列を扱う研究分野は「自然言語処理」です。自然言語処理はLLMの基盤であるTransformerが生まれ進化していった分野です。

Bag-of-Words(単語の個数をカウントしてまとめたもの)やword2vec(単語をベクトルとして表現したもの)といった古典的かつ重要な技術が生まれた後、2017年に現在のTransformerの火付け役となるAttention is All You Needという論文が出版されました。それを元にBidirectional Encoder Representations from Transformers (BERT)やGenerative Pre-trained Transformer (GPT)といったモデルが生まれました。これらが2018年ごろの出来事です。

元々の考え方では、BERTは文章を理解するもの(エンコーダーのみモデル)であり、文章を特徴量に変換するようなイメージです。今でも広く用いられます。GPTは逆に特徴量から文章を復元するもの(デコーダーのみモデル)であり、文章を生成するイメージです。

Tips

自然言語処理そのものに興味がある方は、上記でも紹介した荒瀬先生・岡崎先生、また電気系の鶴岡先生も著者である以下の教科書が大変オススメです。

また、LLMに関する技術的な最新の詳細については岡崎先生のスライドが大変オススメです。

その後、GPTの改良版であるGPT-2(2019年)、GPT-3(2020年)が登場し、2022年にChatGPTがリリースされました。ChatGPTはGPT-3.5をベースとしています。「GPT」という場合はアルゴリズムやモデルといった技術スタックの名称ですが、「ChatGPT」というのは対話インタフェースを備えたウェブサービスの名前になります。「ChatGPT」というサービス上で「GPT-3.5」や「GPT-4」といったモデルを指定して使用する、ということになります。

Transformerが本格的に導入されたのが2017年、GPTが生まれたのが2018年、ChatGPTが生まれたのが2022年ということを考えると、わずか数年のうちに言語モデルの世界は大きく発展し、社会の在り方を変えるほど人々に使われる技術になっていったことがわかります。

Tips

ChatGPTをはじめ、様々なAIサービスでは、古くなったモデルはすぐに使えなくなる(deprecation)ことが一般的です。これは最新の有用な技術が使えるに使えるようになるという意味では嬉しいですが、同じサービスを使い続けられないとか、再現性がなくなるといった問題もあります。

ChatGPTの素晴らしかったところは、まさにチャット形式のUIで自然言語で問い合わせ、自然言語で答えを聞くことが出来るところだったと思います。これにより、自然言語処理に関する専門的な知識を一切もたない人でも、LLMを簡単に使うことが出来るようになりました。ChatGPTが当たり前となっている今ではこの点は自明に思えるかもしれないですが、素晴らしい発明だったと考えられます。技術の進展は優れたUI/UXとセットであることが多々ありますが、ChatGPTにおいてもそうだったと言えるでしょう。

ChatGPTの登場以降、LLMおよびそれを用いたAIサービスは急速に広まっていきました。様々な会社が、オープン・クローズドに関わらず多くのモデルを開発・利用しています。2025年現在、LLMの世界は誰がトップをとるかわからない、非常に激しい競争が繰り広げられているのです。

演習(15分)

荒瀬先生や岡崎先生のスライドを眺めて、自然言語処理の歴史や基本的な技術について触れてみましょう

オープンモデル・クローズドモデル

LLMのモデルには大きく分けてオープンモデルと、クローズド(プロプライエタリ)モデルがあります。この区別は重要ですので、詳しく理解しておきましょう。モデルの違いを理解するために、まず最初にモデルを使わない、すなわち非機械学習なアルゴリズムモデルを使うが、LLM以前のアルゴリズムをおさらいし、その後LLMのオープン・クローズドモデルについて述べます。

モデルを使わないアルゴリズム

まず、モデルを使わない、すなわち非機械学習なアルゴリズムについておさらいしましょう。近年の機械学習を使う世界では多くの場合モデルを使うので忘れがちですが、非機械学習の世界(極めて多くの数学・工学分野)では、モデルという概念はありません。例えばデータの集合が与えられたときにそれをソートする問題では、モデルという概念は無く、その場でデータをソートします。ここではマージソートやクイックソートなどのアルゴリズムが適用されます。また、例えば測定点の集合が与えられたときにその関係を得る多項式を得たい場合は、データに対して最小二乗法を適用して多項式の係数を得ます。ここではモデルという概念はありません。

Tips

そんな自明なことをなぜわざわざ述べるかというと、「簡単な問題を、大きすぎる方法で解いてしまう」ことがあるからです。例えば、最適化の知識を持っていれば、今目の前に与えられた問題は二次計画法で表現でき、ソルバーに投げることが出来る、といった手順をとれます。機械学習を使わなくても解ける問題であれば、常にそれをまず試すべきです。

モデルを使うが、LLM以前のアルゴリズム

次に、モデルを使うが、LLM以前のアルゴリズムについておさらいしましょう。例えば画像認識分野を考えます。ResNetといった画像認識モデルは深層学習によって作られたものであり、画像を入力として特徴量が計算され、それを元に画像のラベル(犬とか猫とか)を当てることが出来ます。

ResNetそのものは畳み込み層や全結合層の組み合わせで構成されています。それにはパラメータが存在します。例えば皆さんは前期実験の信号処理などで一次元信号に対するカーネルの畳み込みを勉強したと思います。そのカーネルの要素がパラメータです。ResNetなどのモデルにおいては、大量の画像(多くの場合、ImageNetといった、広く用いられる大規模画像データセット)を用いてこのパラメータが学習されます。このパラメータをまとめたものを、ウェイトとかモデルとも呼びます。(ウェイトという場合はパラメータとほぼ同義で数値そのものですが、モデルという場合は構造そのものを意味することもあります。「モデルのウェイト」というような表現も行います)

ImageNetなどの大規模データセットでパラメータを学習したResNetなどのモデルを、事前学習済みモデルと呼びます。実際に自分のデータに対しResNetを使いたい(例えば自分が普段iPhoneで撮影しているお昼ご飯のラーメンの画像について、トンコツか塩かなどを自動で認識したい)ときは、この事前学習済みモデルを元に、自分のデータを微調整します。この微調整をファインチューニングと呼びます。ファインチューニングでは、ramen.pngという画像データと、「塩味」といったラベル情報のペアを大量に用意し、それらを元に実行できます。

上記のパラダイムにおいて重要なことは、「事前学習済みモデルをどう入手するか」ということです。ファインチューニングは手元で(すなわち自分が持っているGPUサーバで)実行できます。しかし、ImageNetを用いた訓練は大規模な計算機資源を必要としますし、そもそも誰かが1回実行して得られたモデルがあれば、それを使うほうが効率的です。このような考え方を元に、誰かが事前学習済みモデルを作り、それを配布するという考え方が生まれました。そして現在は、PyTorchといったフレームワーク制作者、あるいはHugging Faceといったプラットフォームが事前学習済みモデルを公開します(訓練は、公式が行うか、あるいは個人が行ってアップロードします)

この世界観では、モデルはダウンロードされて使われなければ意味がないので、必然的にモデル=オープンモデルとなります。クローズドモデルが存在したとしても、それをユーザが使うことは基本的にあまり出来ないため、議論になることはありませんでした。例えばGoogleがGoogle専用の画像認識モデルを作っていたとしても、それをユーザが使う場合は例えばGoogleの画像アップロードAPIを用いて手元の画像をアップロードし、結果を得るということになりますが、これはファインチューンできません。また、その精度がオープン版モデルと比べて段違いにすごいということも基本的には無いという印象です。この点が次に述べるLLMの「クローズドモデルが非常に性能が良い」との対比になります。

オープンモデル

さて、いよいよLLMのオープンモデルについて説明します。

まず、LLMの世界を考えるとき、上記の画像認識モデルのファインチューニングとの違いは何でしょうか。その1つは「LLMはよりジェネラルなものである」ということがあります。ResNetはそもそも入力と出力の形式からして固定であり、画像処理のためだけにしか使えません。一方、LLMは自然言語入力・自然言語出力であり、何もしなくても翻訳や要約といった様々なタスクに用いることが可能です。なので、「性能のいいLLMモデル」を作って公開すれば、それ1つで様々なことが出来て嬉しいということになります。

よって、ResNetのときと同様に、誰かLLMモデルを訓練し、それを公開することがあります。これをオープンモデルと呼びます。有名な例としてはMetaのLlamaシリーズや、後述するGPTのオープン版であるgpt-ossなどがあります。日本のチームとしては、llm-jpによるモデルも有名です。ここでオープンと言っているのは、モデルの構造とウェイトが公開されているという意味です。ユーザは自分の手元にダウンロードし、それを自由に使うことが出来ます。

Day 1でLlamaを実行しましたが、これはオープンモデルをHugging Faceライブラリを経由してダウンロードし、手元で使ったということになります(手元といっても、Colab上なのでリモートサーバ上です)。

オープンと言ってもオープンソースではない場合が多いので注意してください。このあたりの権利問題についても常々論争の的となります。

また、ResNetをImageNetで訓練していた牧歌的な時代(ImageNetによる訓練は、頑張れば例えば卒論生でも出来る)と異なり、LLMはたとえオープンモデルであっても事前に個人や研究室が訓練することは全く不可能なほど、大量のデータと大量の計算機資源を必要とします。

Tips

CVPR2025のキーノートはLlama4の作り方についてのものでしたが、Llama4を作ることは「ロケット工学」のようなものだと述べられています。それは大量の専門家が含まれる超大規模なプロジェクトです。以下の動画でその様子がわかります。 Laurens Van der Maaten, "The Llama Herd of Models: System 1, 2, 3 Go!", CVPR 2025 Keynote

クローズド(プロプライエタリ)モデル

次に、クローズド(プロプライエタリ)モデルについて説明します。

クローズドモデルでは、モデルのウェイトや構造が非公開です。多くの場合、企業が自身で集めたデータを用いて訓練したものです。ユーザはそのモデルを直接入手することは出来ないので、以下の2通りの方法で利用します。

  • サービスとして利用:これがそもそもの用途です。ChatGPTのように、ウェブサービスという形で提供されたり、GitHub CopilotのようにIDEプラグインとして提供されたりします。

  • APIとして利用:OpenAI APIのように、ウェブAPIとしてモデルの機能が提供されます。学術利用ではこの使われ方が多いです。Day 1でAzure OpenAI APIを使ったのはこの例です。通常は従量課金でお金がかかります。

クローズドモデルの最も有名なものはOpenAIのGPTシリーズです。これに対し、GoogleのGeminiシリーズやAnthropicのClaudeシリーズなどが対抗(?)であり、各社が競って新しいモデルを開発しています。また、同じ会社からオープン版が出されることもあります。OpenAIは最近gpt-ossというオープン版モデルを公開しました。

画像認識の場合と違い、LLMは入力・出力が自然言語であるうえに一つのモデルで様々な用途に使えるため、ウェブAPIを経由するブラックボックスとしてモデルの機能を提供することが出来ます。これがLLM時代のクローズドモデルという世界観です。

オープンモデルとクローズドモデルの比較

オープンモデルとクローズドモデルの比較が以下になります。

オープン

クローズド

性能

トップではないことが多い

トップであることが多い

誰でも使える?

Yes

No

アクセス

ダウンロードして手元で実行

ウェブサービスやAPI経由で利用

透明性

モデル構造とウェイトが公開

非公開

コスト・運用

DLして自分でホスト。計算機資源が必要だが従量課金ではない

サービス利用料がかかる。従量課金制が多い

Llama, Mistral, gpt-oss

GPT5, Gemini, Claude

まず何より重要なのが、クローズドモデルのほうが高性能であるという点です。一般にGPT5やGeminiといった、大企業が訓練したクローズドモデルは最高性能を誇ります。これがLLM以前の科学の世界から変わってしまった点です。LLM以前では、機械学習を使うにせよ、最高性能のブラックボックスを企業が保持してAPIだけ貸し出すという状況はなかったと思います。

また、アクセス・運用・コストの形態も大きく異なります。オープンモデルは手元にダウンロードする必要がありますがそれ以降は自由に使える一方、GPUなどインフラコストを自分で負担する必要があります。クローズドモデルはAPIコールが出来るのでインフラは必要ない一方、従量課金であったり、通信のことを考える必要があったり、また「GPTの調子が悪い」と言った形でサービス元に諸々が依存してしまいます。また、再現性が無いという問題もあります。

LLMを用いて研究開発を行う場合は、これらのメリット・デメリットを理解する必要があります。

演習(60分)

  • DL以前の(GPUを使わない、モデルをDLしない)機械学習としては、scikit-learnライブラリやSciPyライブラリがあります。これらでどこまでのことが出来るか、知っておく必要があります。例として、scikit-learnライブラリのチュートリアルを実行し、どのようなものなのか体験してみましょう。Colab上で実行可能です。

  • Hugging Face上のResNet-18を実行してみましょう。これにより、LLM以前のモデルの世界観を知っておきましょう。これもcolab上で実行可能です。

  • Day 1のコードを改造して、様々なオープンモデル・クローズドモデルを使ってみましょう。特に、「軽量なもの」とか「セーフガードがついたもの」など、色々なモデルがあります。

  • また、配布形態として、Hugging Face経由で公開されているものが多いですが、GitHubから直接配布されたりしているものもあります。色々と調べて実行してみましょう。

様々な実行形態

前章で触れたモデルのオープン・クローズドにも関係しますが、LLMの実行形態としては極めておおざっぱに分類して以下の3つがあります。LLMは実行に大変なお金がかかることが多いので、自分の利用用途にあわせて適切な実行形態を選ぶことが重要です。

オンプレサーバ実行

研究室がGPUサーバを保持している前提で、そのサーバにLLMモデルをダウンロードして実行する形態です。GPUサーバを「手元」といったりします(GPUサーバにはSSHして入るので物理的な手元ではないのですが)。これはResNetなどの画像認識モデルを手元で実行するのと同じです。オープンモデルを実行するために概念的に一番簡単な方法はこれです。

メリット

  • ひとたびモデルをダウンロードして実行する準備が整えば、いくらでも無料で実行できる(電気代はかかる)

デメリット

  • 自分でGPUサーバを構築・運営しなければならない

  • GPUが足りなくなりうる(締め切り前には混む)

現実的に、オンプレサーバで大量のLLM実験を行うことが出来る研究室は限られてくるため、必然的に複数研究室で大きなグループを作ったり、小規模な実験はオンプレで行いつつ大型の実験は以下のクラウド実行を選ぶようなケースが多いと思います。

クラウド実行

AWS, GCP, Azureなどのクラウドサービスを利用し、そこにLLMモデルをダウンロードして実行する形態です。国内の大学の場合、歴史がありノウハウがある・コスト的に有利・使いやすいなどの観点から産総研のGPUクラウド群であるABCIは極めてよく使われます。スパコン系列としてはmdxMiyabiなどがあります。

これらのクラウドサービスはログインしてオープンモデルをダウンロードして実行するという点でおオンプレサーバと似ていますが、自分でGPUサーバを構築運営する必要がないという点がメリットです。また、AWSなどではインスタンス数を増減させることで、ABCIなどスパコンタイプは必要なだけのジョブを発行するということで、使った分だけの支払いを行うことが出来ます(初期投資をして使わなかった、ということはない) また、分類させるとすればGoogle Colabもこのクラウド実行になります。

また、クラウドサービスによっては最初から各種モデルやライブラリ等が使いやすいようにインストールされていることもあります。Day 1のLlamaの例はここに分類されます。そこではみなさんはCUDAのローレベルな設定などはしなくても良かったことを覚えているかと思います。

メリット

  • 自分でGPUサーバを構築・運営する必要がない

  • 必要なときに必要な分だけ使える

  • 生の計算が出来る(APIコールでは生の計算が出来ない)

デメリット

  • 使った分だけお金がかかり、とても高額になりえる

  • サービスが混んでしまったり、急に使えなくなることがあり得る

  • 一般論として大量のデータを扱う際に問題が起こりえる。また、秘密データなどは扱いづらい場合がある

オンプレGPUサーバで検証を行い、大規模実験はABCIで行う、というパターンが日本の研究室では多いのではないかと思います。

API実行

APIを介してモデルを利用する形態です。例えば、OpenAIのAPIを使ってChatGPTを呼び出すといった使い方です。クラウド実行はGPUマシンを使った時間に課金されることが多いですが、API実行の場合はAPIコールに応じた課金になるため、ちょっとしたことを行う際はとても少額で実行できます。一方、少し大きなことを実行すると途端に割高になりえます。

また、API実行では手元でGPU環境をセットアップする必要もないため、非常に手軽にLLMを利用できます。Day 1のChatGPTの例はここに分類されます。ChatGPTの例は「CPU」インスタンスからも実行できることを確認してみましょう。

Tips

ちなみに、普通はChatGPTのAPIを用いる際にはOpenAI公式のAPIを用いて実行します。東大にはAzureと連携したUTokyo Azureがあるため、Day 1ではAzure OpenAI APIを用い、かつ皆さんそれぞれに負担がいかないようにしました。2025年現在OpenAIはMicrosoftと連携しているため、Azure側からも使うことが出来ます。

現状、クローズドモデルを実行するにはこのAPI実行しかありません。

メリット

  • 自分でGPUサーバを構築・運営する必要がない

  • 必要なときに必要な分だけ使える

  • とても少額で実行できる場合がある

デメリット

  • 使った分だけお金がかかり、とても高額になりえる

  • 普通は「訓練」には使わず、「推論」のためのもの)

  • 一般に再現性を担保することがとても難しい

  • 「通信」といった概念を考える必要がある

APIコールかクラウド実行かで悩む場面は結構あります。たとえば手元に文章データがあり、それらから文章特徴量を抽出しようとする場合、特徴量抽出のAPIコールを叩けばその場で何もせずに特徴量を得ることが出来ます。一方、この方式は例えば実験条件を変えるたびにAPIコールをし直さねばならないです。一方同等の作業をクラウド実行できる可能性もあります。その場合、初期設置が一番の問題になるでしょう。

研究をするという観点からいうと、上記のように実行形態が変わっても問題が無いようにコードやモデルやデータを準備しておけるかどうかが重要になってくるでしょう。また、プログラム中の関数単位でGPUを呼ぶModalという新しい取り組みなども生まれてきています。また、たとえオープンモデルであっても、ローカル実行するのではなくクラウドサーバにホストして使うという使い方もできます。Hugging Face Inference Providersはその例です。これはHugging Faceのモデルページを見たときに右側のパネルで試せるお試し領域そのものなのですが、これをAPIコールで実行することもできます。

LLMはその応用先が面白いだけでなく、このようにコンピューティングの根本のレベルから様々な変化をもたらしています。皆さんはどのような新しい環境でも、まずは使ってみて感覚をつかむという姿勢を大事にしてください。

演習(15分)

Day 1で行ったLlamaの実行を振り返り、どのような実行形態があるか考えてみましょう。また、それをオンプレやクラウド実行、API実行する場合に、どの程度使うとどれくらいのお金がかかるか、どれくらい使うならオンプレ・API実行に移行するほうがいいか、見積もってみましょう。

基本構造

さて、それではLLMの基本的な構造についてみてみましょう。本演習ではLLMの内部の詳細な実装については触れない(最後の自由課題で調べたり比較したり改善したりしてみてもらえるのは歓迎です)のですが、基本構造部分についてもう一歩理解しておきましょう。

Day 1ではLLMは完全にブラックボックスとなり、「文字列・文字列出力」のみを扱いました。しかし実際のLLMは、少なくとも以下のパイプラインをとることが多いです。

  • 文字の前処理

    • トークナイザにより、入力文字列をトークン化する。すなわち、トークンIDという整数の列にする

    • トークンIDから、埋め込みベクトルを得る。これにより、入力文字列はベクトルの列になる。

  • 推論処理

    • ベクトル列をLLMモデル本体に入力し、次にくるべきトークンの確率分布を得る

    • 確率分布から、出力すべきトークンIDをサンプリングする

  • 後処理

    • トークンIDを文字列に変換する

この流れを下記に述べます。これはColab上で実行可能です。

#!/usr/bin/env python
# coding: utf-8

# # LLMの内部構造と処理の流れ
# 
# このノートブックでは、言語モデル(LLM)がどのように動いているのかを体験的に理解します。
# 
# ## LLMのパイプライン
# 1. **トークナイズ**: 文字列を数値IDに変換
# 2. **埋め込み**: IDをベクトル(数値の列)に変換
# 3. **推論**: 次に来る単語の「確率分布」を取得
# 4. **サンプリング**: 確率分布から「1つの単語ID」を決定
# 5. **後処理**: 単語IDを文字列に変換
# 
# (発展) **埋め込みベクトルの性質**: 単語間の意味の近さを見る
# 
# ---

# ## セットアップ
# 
# 必要なライブラリをインストールします(Google Colabの場合、最初の1回だけ実行)。

# In[1]:


get_ipython().run_line_magic('pip', 'install torch transformers accelerate scikit-learn matplotlib')


# ## モデルとトークナイザのロード
# 
# Meta社の軽量な言語モデル「Llama 3.2 1B」を使用します。
# - **1B** = 10億パラメータ(ChatGPTは数千億〜数兆パラメータ)
# - 実行には数分かかる場合があります

# In[ ]:


from huggingface_hub import login
login()


# In[3]:


import torch
import torch.nn.functional as F
from transformers import AutoModelForCausalLM, AutoTokenizer
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA

# モデルID(軽量な1B版)
model_id = "meta-llama/Llama-3.2-1B-Instruct"

print(f"モデルをロード中: {model_id}")
print("(初回は数分かかる場合があります)")

try:
    tokenizer = AutoTokenizer.from_pretrained(model_id)
    if tokenizer.pad_token is None:
        tokenizer.pad_token = tokenizer.eos_token
    model = AutoModelForCausalLM.from_pretrained(
        model_id,
        dtype=torch.float16,
        device_map="auto",
    )
    print("\n✅ モデルのロードが完了しました!")
    print(f"デバイス: {model.device}")
except Exception as e:
    print(f"\n🚨 エラー: モデルのロードに失敗しました")
    print("GPUランタイムが有効か確認してください(Google Colabの場合)")
    print(f"詳細: {e}")


# ---
# ## ステップ1:トークナイズ(文字列→数値ID)
# 
# LLMは文字列を直接扱えません。まず「トークン」という単位に分割し、それぞれに数値IDを割り当てます。

# In[4]:


print("--- ステップ1: トークナイズ ---\n")

prompt = """You are a pirate chatbot who always responds in pirate speak!
User: Who are you?
Assistant:"""
print(f"入力テキスト: {prompt}")

tokens = tokenizer(prompt, return_tensors="pt")
print("=== トークンID ===")
print(f"トークン数: {len(tokens['input_ids'][0])}")
print(tokens["input_ids"])

print("\n=== 各トークン ===")
for tid in tokens["input_ids"][0]:
    print(f"ID {tid.item():6d}{tokenizer.decode([tid])!r}")


# ### 💡 観察ポイント
# - **単語が複数のトークンに分割されることがある**: 例えば' chatbot'は' chat'と'bot'に分割されています。
# - **スペースが単語に含まれる**: スペースは独立したトークンにならず、' are' や ' pirate' のように次の単語の先頭に含まれてトークン化されています。
# - **大文字と小文字は区別される**: 'You' (ID 2675) と 'you' (ID 499) は、異なるトークンIDとして扱われています。
# - **特殊トークンが追加される**: 文の開始を示す '<|begin_of_text|>' (ID 128000) が自動的に付与されています。

# ---
# ## ステップ2:埋め込みベクトル(ID→ベクトル)

# In[5]:


print("--- ステップ2: 埋め込みベクトル ---\n")

input_ids = tokens["input_ids"].to(model.device)
embeddings = model.model.embed_tokens(input_ids)

print(f"埋め込みベクトルの形状: {embeddings.shape}") # バッチサイズ, トークン数, ベクトル次元数
print(f"最初のトークンのベクトル(先頭10次元):")
print(embeddings[0, 0, :10].detach().cpu().numpy())


# ---
# ## ステップ3:推論(確率分布の取得)

# In[6]:


print("--- ステップ3: 推論実行 ---\n")
prompt = """You are a pirate chatbot who always responds in pirate speak!
User: Who are you?
Assistant:"""
print(prompt)

inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
with torch.no_grad():
    outputs = model(**inputs)
    logits = outputs.logits
print("=== Logits ===")
print(f"Logits shape: {logits.shape}") # バッチサイズ, トークン数, 語彙数


# In[7]:


next_token_logits = logits[:, -1, :] # 最後のトークン(Assistantの後の':')に続くトークンの確率分布
probs = F.softmax(next_token_logits, dim=-1)

topk = torch.topk(probs[0], k=10)
topk_tokens = [tokenizer.decode([i]) for i in topk.indices]
topk_probs = topk.values.cpu().numpy()

for t, p in zip(topk_tokens, topk_probs):
    print(f"{t!r:<15} : {p:.4f}")


# In[8]:


plt.figure(figsize=(12, 6))
plt.bar(range(len(topk_tokens)), topk_probs)
plt.xticks(range(len(topk_tokens)), topk_tokens, rotation=45, ha='right')
plt.ylabel('Probability')
plt.title('Top 10 Next Token Probabilities')
plt.show()


# ---
# ## ステップ4:サンプリング(IDの決定)

# In[10]:


sampled_token_id = torch.multinomial(probs, num_samples=1)[0][0]
greedy_token_id = torch.argmax(probs, dim=-1)[0]

print(f"確率的サンプリング: {sampled_token_id}")
print(f"貪欲法: {greedy_token_id}")


# ---
# ## ステップ5:後処理(ID→文字列)

# In[11]:


print(f"生成されたトークン: {tokenizer.decode(greedy_token_id)}")


# ---
# ## 発展:埋め込みベクトルの意味

# In[12]:


words = ["quick", "fast", "slow", "king", "queen", "man", "woman"]
token_ids = tokenizer(words, return_tensors="pt", padding=True)["input_ids"].to(model.device)
vecs = model.model.embed_tokens(token_ids).mean(dim=1)
sims = F.cosine_similarity(vecs.unsqueeze(0), vecs.unsqueeze(1), dim=-1)
print("=== 'quick'との類似度 ===")
for w, s in zip(words, sims[0]):
    print(f"quick vs {w:<6}: {s.item():.3f}")


# In[13]:


pca = PCA(n_components=2)
coords = pca.fit_transform(vecs.detach().cpu().numpy())
plt.figure(figsize=(8, 6))
plt.scatter(coords[:, 0], coords[:, 1])
for i, w in enumerate(words):
    plt.text(coords[i, 0]+0.02, coords[i, 1]+0.02, w, fontsize=12)
plt.title("Word Embeddings (PCA)")
plt.grid(True)
plt.show()

演習(30分)

上記をColabに写経し、いろいろいじってみましょう。コピペではなく写経することをお勧めします。

トークナイザ

さて、上記のコードのトークナイザの部分を少し深堀りしてみましょう。

トークナイザとは文字列の前処理にあたり、文字列をLLMが理解しやすい形態に変換する処理です。トークナイザによる処理はGPUを用いる前段階にあたるため、CPUで実行できる決定的な処理です。トークナイザはシンプルかつ最初の処理ですが、奥が深いです。

また、トークナイザは各LLMモデルに紐づいているものなので、たとえばLlama用のトークナイザとGPT-4用のトークナイザは異なります。モデルを変える場合はトークナイザも変える必要があることに注意してください。

ここではOpenAI Platform Tokenizerが非常にわかりやすい可視化を提供しているので、そちらを使っていろいろと文字列をトークナイズしてみてください。

エンコード(文字列から整数列)

ここではまずエンコード(文字列を整数列に変換)の処理を考えます。例として以下の文字列を考えましょう。

He said, "Replaying the video made me laugh."

この文字列を、GPT-4oのトークナイザでトークナイズする場合、まず文字列が次で可視化されるように分割されます。この分割されたそれぞれをトークンと呼びます。(画像およびトークンIDはOpenAI Platform Tokenizerによる、GPT-4oの処理結果です)

そして、各トークンは整数IDと紐づいています。この整数IDをトークンIDと呼びます。これにより、上記は次のように書き直せます。

[2066, 2059, 11, 392, 720, 86433, 290, 3823, 2452, 668, 25053, 3692]

すなわち、Heというトークンは2066というトークンIDに紐づいています。このようにして文字列を整数列で表現することで、コンピュータが文字列をうまく表現できるようになります。

この分割の仕方には様々な方式、単語レベルのもの、語レベルのもの、サブワードレベルのもの、などなどです。上記を見ると、単語や、コンマなどの記号には、1つのトークンが割り当てられています。一方で、ReplayingReplayingという二つのトークンに分けられています。これがサブワードレベルの分割です。これらの粒度をどのように設定するかは、トークナイザの設計における重要なポイントです。細かく分類しすぎると意味をとらえきれないことがあるしトークンID列が長くなってしまいます。一方、粗すぎると、大量のトークンIDの種類が必要になってしまいます(例えば全ての動詞に対し-ing系を別物として覚える必要がある)

埋め込み(整数列からベクトル列)

トークンIDに対し、LLMは埋め込みを保持しています。埋め込みとは、各トークンIDに対し、実数ベクトルが割り当てられているものです。例えば上記のHeというトークンID2066に対し、384次元のベクトル \(\mathbf{x}_{2066} \in \mathbb{R}^{384}\) 以下のようなベクトルが割り当てられています。これは言語モデルが学習の過程で獲得したものです。

これを用いて、上記のトークンID列は以下のように埋め込みベクトルの列に変換できます。これはベクトル集合に対するテーブルルックアップですぐさま得られるものです。

\[ (\mathbf{x}_{2066}, \mathbf{x}_{2059}, \mathbf{x}_{11}, \mathbf{x}_{392}, \mathbf{x}_{720}, \mathbf{x}_{86433}, \mathbf{x}_{290}, \mathbf{x}_{3823}, \mathbf{x}_{2452}, \mathbf{x}_{668}, \mathbf{x}_{25053}, \mathbf{x}_{3692}) \]

これにて入力文字列はベクトル列になったので、LLMに直接入力することが出来るようになりました。

デコード(整数列から文字列)

最後に、LLMの出力であるトークンID列を文字列に変換する処理について説明します。これはエンコードの逆の処理です。LLMによる処理の結果、次のようなトークンID列が出力されたとします。

[13684, 52846, 326, 43761, 11, 392, 15390, 17103, 481, 15309, 480, 3692]

このトークンID列をデコード(トークンに戻す)すると、以下のような文字列になります。

She smiled and replied, "I'm glad you enjoyed it."

以上がトークナイザの基本的な内容です。

演習(15分)

「基本構造」のコードのトークナイザ部分を色々と変更して、様々なモデルのトークナイザを試し、色々な文字列をエンコード・デコードしてみてください。日本語のようなマルチバイト文字列とか、特殊な記号とかは、どうなるでしょうか。