{ "cells": [ { "cell_type": "markdown", "id": "note-1", "metadata": {}, "source": [ "# はじめてのLLM実験室\n", "\n", "**AI手帖のColab教材** · 確認日:2026年10月6日\n", "\n", "公開されている小型言語モデル **Qwen3-0.6B** をColabのランタイムに読み込み、実際に文章を生成します。事前に用意された回答を表示する静的なデモではなく、APIキーの登録や設定も不要です。\n", "\n", "この教材で体験する内容:\n", "1. 日本語で指示を入力し、モデルに回答文を生成させる\n", "2. プロンプト(指示文)の書き方による出力の違いを比較する\n", "3. temperature(生成温度)を調整し、出力のばらつきや多様性を確認する\n", "4. 入力した文章がどのようなトークン列に分割されるかを観察する\n", "5. 外部の短い参考資料を与え、モデルが資料に基づいて回答できるか検証する\n", "\n", "Webサイト:[AI手帖](https://ai.technology-developer.com/)\n" ] }, { "cell_type": "markdown", "id": "note-2", "metadata": {}, "source": [ "## 実行の準備\n", "\n", "Googleアカウントにログインした状態でColabを開き、必要に応じてメニューの **[ファイル]→[ドライブにコピーを保存]** を実行してください。各コードセルの左側にある再生ボタン(▶)を上から順番にクリックして進めます。フォーム形式のセルでは、テキストや数値を編集して再実行できます。\n", "\n", "- **CPU環境のままでも動作します。** より高速に動かすためGPUを利用する場合は、メニューの[ランタイム]→[ランタイムのタイプを変更]からGPUを選択してください。ただし無料枠のGPUは利用状況によって割り当てられない場合があります。なお、CPU環境では文章の生成に数十秒から数分ほど時間がかかります。\n", "- 初回実行時にモデルの重みファイルをダウンロードします。通信環境によって完了まで数分程度かかります。\n", "- このノートブックに入力した文章や実行結果は、Google Colabの実行環境内で処理されます。個人情報や業務上の機密情報は入力せず、練習用の文章を使用してください。Googleドライブへのマウント操作や外部APIキーの設定は必要ありません。\n", "- Qwen3-0.6Bはパラメータ数が約6億の小型モデルです。そのため、複雑な日本語の指示を正しく解釈できなかったり、誤った情報を生成したりすることがあります。出力された内容の正確性を確認しながら進めてください。\n", "\n", "Colabの利用条件や仕様:[Google公式FAQ](https://research.google.com/colaboratory/faq.html)\n" ] }, { "cell_type": "code", "id": "code-3", "metadata": { "cellView": "form" }, "source": [ "# @title 1. 必要なライブラリを準備する\n", "%pip -q install \"transformers==4.57.6\" \"accelerate==1.12.0\" \"torch>=2.6,<3\" \"safetensors>=0.5,<1\"\n" ], "execution_count": null, "outputs": [] }, { "cell_type": "code", "id": "code-4", "metadata": { "cellView": "form" }, "source": [ "# @title 2. モデルを読み込む(初回だけ実行)\n", "import torch\n", "import transformers\n", "from transformers import AutoModelForCausalLM, AutoTokenizer, set_seed\n", "from IPython.display import display, Markdown\n", "\n", "MODEL_ID = \"Qwen/Qwen3-0.6B\"\n", "MODEL_REVISION = \"c1899de289a04d12100db370d81485cdf75e47ca\"\n", "device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n", "dtype = torch.float16 if device == \"cuda\" else torch.float32\n", "if device == \"cpu\":\n", " torch.set_num_threads(min(2, torch.get_num_threads()))\n", "\n", "tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, revision=MODEL_REVISION)\n", "model = AutoModelForCausalLM.from_pretrained(\n", " MODEL_ID,\n", " revision=MODEL_REVISION,\n", " torch_dtype=dtype,\n", " low_cpu_mem_usage=True,\n", " use_safetensors=True,\n", ").to(device)\n", "model.eval()\n", "print(f\"モデル:{MODEL_ID}\")\n", "print(f\"実行環境:{device} / Transformers {transformers.__version__} / PyTorch {torch.__version__}\")\n", "print(\"準備できました。次のセルへ進んでください。\")\n" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "id": "note-5", "metadata": {}, "source": [ "### このノートブックの生成関数\n", "\n", "次のセルに定義された関数は、入力したプロンプトをトークナイザーで処理してモデルへ渡し、新しく生成されたトークン部分のみを抽出して文字列に変換します。ここでは推論速度や出力を分かりやすく保つため、Qwen3の「thinking」モード(思考プロセスの出力)は使わず、回答本文のみを直接生成するように設定しています。コードの詳細な実装は後から確認しても問題ありません。まずはこのセルを実行し、次の実験へ進んでください。\n" ] }, { "cell_type": "code", "id": "code-6", "metadata": {}, "source": [ "def ask_llm(question, *, temperature=0.7, max_new_tokens=128, seed=42):\n", " if not isinstance(question, str) or not question.strip():\n", " raise ValueError(\"質問文を入力してください。\")\n", " if not 0.1 <= float(temperature) <= 2.0:\n", " raise ValueError(\"temperatureは0.1〜2.0にしてください。\")\n", " if not 1 <= int(max_new_tokens) <= 512:\n", " raise ValueError(\"出力上限は1〜512トークンにしてください。\")\n", " messages = [\n", " {\"role\": \"system\", \"content\": \"あなたは日本語で答えるアシスタントです。質問に短く答えてください。\"},\n", " {\"role\": \"user\", \"content\": question},\n", " ]\n", " text = tokenizer.apply_chat_template(\n", " messages, tokenize=False, add_generation_prompt=True, enable_thinking=False\n", " )\n", " inputs = tokenizer(text, return_tensors=\"pt\").to(device)\n", " if inputs.input_ids.shape[1] > 2048:\n", " raise ValueError(\"質問文が長すぎます。教材では2,048入力トークン以内で試してください。\")\n", " set_seed(int(seed))\n", " with torch.inference_mode():\n", " outputs = model.generate(\n", " **inputs,\n", " max_new_tokens=int(max_new_tokens),\n", " do_sample=True,\n", " temperature=float(temperature),\n", " top_p=0.8,\n", " top_k=20,\n", " pad_token_id=tokenizer.eos_token_id,\n", " eos_token_id=tokenizer.eos_token_id,\n", " )\n", " generated = outputs[0, inputs.input_ids.shape[1]:]\n", " return {\n", " \"answer\": tokenizer.decode(generated, skip_special_tokens=True).strip(),\n", " \"input_tokens\": int(inputs.input_ids.shape[1]),\n", " \"output_tokens\": int(generated.shape[0]),\n", " \"hit_limit\": int(generated.shape[0]) >= int(max_new_tokens),\n", " }\n", "\n", "def show_answer(result):\n", " print(result[\"answer\"] or \"(空の回答でした。質問を変えて再実行してください。)\")\n", " print(f\"\\n入力 {result['input_tokens']}トークン / 出力 {result['output_tokens']}トークン\")\n", " if result[\"hit_limit\"]:\n", " print(\"出力上限に達しました。文が途中で終わった場合は、上限を少し増やして試してください。\")\n" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "id": "note-7", "metadata": {}, "source": [ "## 実験1:日本語で質問する\n", "\n", "まずは初期状態で設定されている質問文をそのまま実行し、モデルが文章を生成する様子を確認してみましょう。回答が出力されたら、入力フォームの質問文を別の内容に書き換えて再度実行してください。事実関係が正確であるかに加え、文字数や出力形式などの指定をどの程度反映できているかも確認します。\n" ] }, { "cell_type": "code", "id": "code-8", "metadata": { "cellView": "form" }, "source": [ "# @title 3. 質問してみる\n", "question = \"大規模言語モデルとは何ですか。初めて聞く人向けに、日本語で2文以内で説明してください。\" # @param {type:\"string\"}\n", "output_limit = 128 # @param {type:\"slider\", min:32, max:256, step:32}\n", "first_result = ask_llm(question, max_new_tokens=output_limit)\n", "show_answer(first_result)\n" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "id": "note-9", "metadata": {}, "source": [ "**次に試す質問の例**\n", "\n", "- 「雨の日に持っていく物を、理由付きで3つ挙げてください。」\n", "- 「APIを初めて聞く人に、身近な例を使って説明してください。」\n", "- 「次の文章を20文字以内に短くしてください:今日は雨が降っているので、傘を持って外出します。」\n", "\n", "小型モデルの場合、指定した文字数や項目数を正確に守れないケースが珍しくありません。生成された文章の文字数や要素数を実際に数えてみて、どのような制約条件の遵守が苦手かを観察してみましょう。\n" ] }, { "cell_type": "markdown", "id": "note-10", "metadata": {}, "source": [ "## 実験2:依頼の書き方を変える\n", "\n", "同じトピックを題材にして、「大まかな指示文」と「条件を具体的に指定した指示文」で出力結果を比較します。指示文以外の要因によるブレを排除するため、temperature・最大生成トークン数・乱数シード(seed)の値は同一に揃えて実行します。ただし、シード値を固定していても、入力テキストが異なれば生成される文章全体は変わります。\n" ] }, { "cell_type": "code", "id": "code-11", "metadata": { "cellView": "form" }, "source": [ "# @title 4. プロンプトを比べる\n", "brief_prompt = \"APIについて説明して。\" # @param {type:\"string\"}\n", "specific_prompt = \"プログラミングを始めたばかりの人向けに、APIを日本語で説明してください。レストランの注文を例にし、説明は3文以内にしてください。\" # @param {type:\"string\"}\n", "prompt_results = {}\n", "for label, prompt in [(\"短い依頼\", brief_prompt), (\"具体的な依頼\", specific_prompt)]:\n", " print(f\"\\n--- {label} ---\")\n", " result = ask_llm(prompt, temperature=0.7, max_new_tokens=128, seed=42)\n", " prompt_results[label] = result\n", " show_answer(result)\n" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "id": "note-12", "metadata": {}, "source": [ "**見比べるポイント**\n", "\n", "1. 指定した読者層(前提知識の有無など)に合わせて、わかりやすい言葉遣いで説明されているか。\n", "2. 指示に含めた具体例や箇条書きなどの出力形式が反映されているか。\n", "3. 事実の誤認や、文脈が通じない不自然な日本語が含まれていないか。\n", "\n", "プロンプトを細かく書けばあらゆるケースで望ましい結果になるとは限りません。期待通りに動作しなかった場合は、指示の条件を一度にすべて変えるのではなく、1点ずつ変更して出力を比較してみてください。\n" ] }, { "cell_type": "markdown", "id": "note-13", "metadata": {}, "source": [ "## 実験3:temperatureを変える\n", "\n", "temperatureは、次の単語(トークン)を予測する際の確率分布の平坦さを調整するパラメータです。数値が低いと確率が最も高いトークンが安定して選ばれやすくなり、数値を高くすると多様な候補が選ばれやすくなります。ここでは同一の質問に対して、3段階のtemperatureで生成結果を比較します。再現性やばらつきを検証する際は、seedの値も変更して再実行してみてください。1回だけの出力結果では、パラメータによるばらつきの傾向を正しく判断できないためです。\n" ] }, { "cell_type": "code", "id": "code-14", "metadata": { "cellView": "form" }, "source": [ "# @title 5. temperatureの違いを比べる\n", "comparison_question = \"雨の日に読みたくなる短編小説の題名を、日本語で3つ考えてください。題名だけを箇条書きにしてください。\" # @param {type:\"string\"}\n", "comparison_seed = 42 # @param {type:\"integer\"}\n", "temperature_results = {}\n", "for value in [0.3, 0.7, 1.2]:\n", " print(f\"\\n--- temperature = {value} / seed = {comparison_seed} ---\")\n", " result = ask_llm(comparison_question, temperature=value, max_new_tokens=96, seed=comparison_seed)\n", " temperature_results[str(value)] = result\n", " show_answer(result)\n" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "id": "note-15", "metadata": {}, "source": [ "**観察すること**\n", "\n", "- 設定値によって、同じタイトルの重複や似たフレーズの繰り返しが発生するか。\n", "- 提案されるタイトルの視点や表現のバリエーションにどのような変化が現れるか。\n", "- 箇条書きの書式や指定した件数が正しく維持されているか。\n", "\n", "この実験は、生成される情報の正確性を測るものではありません。temperatureの値を低く設定しても、誤った情報(ハルシネーション)が正しい内容に修正されるわけではない点に注意してください。\n" ] }, { "cell_type": "markdown", "id": "note-16", "metadata": {}, "source": [ "## 実験4:実際のトークンを確認する\n", "\n", "Qwen3に付属するトークナイザーを使用し、入力した日本語テキストが内部でどのようにトークンIDへ分割されているかを可視化します。LLMが処理する単位であるトークンの数は、文字数や単語数と1対1で一致するとは限りません。また、使用するモデルやトークナイザーの語彙表が異なれば、全く同じ日本語の文章でも分割される位置やトークン数は変化します。\n" ] }, { "cell_type": "code", "id": "code-17", "metadata": { "cellView": "form" }, "source": [ "# @title 6. 文章をトークンに分ける\n", "sample_text = \"AIの仕組みを、ひとつずつ理解する。\" # @param {type:\"string\"}\n", "token_ids = tokenizer.encode(sample_text, add_special_tokens=False)\n", "print(f\"文字数:{len(sample_text)} / トークン数:{len(token_ids)}\")\n", "print(\"\\nID 1トークンを個別にデコードした表示\")\n", "for token_id in token_ids[:100]:\n", " print(f\"{token_id:<6} {tokenizer.decode([token_id])!r}\")\n", "if len(token_ids) > 100:\n", " print(\"(表示は先頭100トークンまで)\")\n", "print(\"\\n全部をまとめてデコード:\", tokenizer.decode(token_ids))\n", "assert tokenizer.decode(token_ids) == sample_text\n" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "id": "note-18", "metadata": {}, "source": [ "分割されたトークンを1つずつ単独で文字列にデコードした際、画面上に「�」(置換文字)が表示されることがあります。これはマルチバイト文字(日本語など)を構成するUTF-8バイト列の途中でトークンが分割されており、単一のトークンだけでは1つの完全な文字として復元できないためです。文全体をまとめてデコードした正常なテキストと見比べてみてください。この表示は、モデル内部のトークン分割と実際の文字コードの対応関係を理解するためのものです。\n" ] }, { "cell_type": "markdown", "id": "note-19", "metadata": {}, "source": [ "## 実験5:資料を渡して答えてもらう\n", "\n", "備品の貸出規定を模した架空のテキストデータから、質問文に含まれるキーワードに基づいて関連する資料を抽出し、プロンプトに埋め込んでモデルに回答させます。ここでの資料選択は単純な部分一致による文字列検索で行っており、ベクトル検索(Embedding)は使用していません。抽出された資料をもとに、実際のLLMが文脈を解釈して回答を生成します。\n", "\n", "提示された資料の記述に基づいて正しく回答できているか、また資料に書かれていない勝手な条件を付け足していないかを確認してください。小型モデルの場合、適切なコンテキストを与えても記述内容を読み違えて回答してしまうことがあります。\n" ] }, { "cell_type": "code", "id": "code-20", "metadata": { "cellView": "form" }, "source": [ "# @title 7. 資料を探して、回答の材料にする\n", "documents = [\n", " {\"id\": \"資料01\", \"keyword\": \"ノートパソコン\", \"text\": \"ノートパソコンの貸し出し期間は7日です。延長には管理者の承認が必要です。\"},\n", " {\"id\": \"資料02\", \"keyword\": \"プロジェクター\", \"text\": \"プロジェクターは前日までに予約フォームから申請してください。\"},\n", " {\"id\": \"資料03\", \"keyword\": \"会議室\", \"text\": \"会議室は予約終了時刻までに片付けを終え、返却してください。\"},\n", "]\n", "rag_question = \"ノートパソコンは何日借りられますか。\" # @param {type:\"string\"}\n", "retrieved = [doc for doc in documents if doc[\"keyword\"] in rag_question]\n", "print(\"取り出した資料:\")\n", "for doc in retrieved:\n", " print(f\"{doc['id']}:{doc['text']}\")\n", "\n", "if not retrieved:\n", " print(\"該当する資料がありません。今回はモデルを呼び出さず、確認できないことを伝えます。\")\n", " rag_result = None\n", "else:\n", " context = \"\\n\".join(f\"{doc['id']}:{doc['text']}\" for doc in retrieved)\n", " grounded_prompt = (\n", " \"次の資料だけを使い、質問に日本語で短く答えてください。\"\n", " \"資料にない情報は補わないでください。回答の最後に根拠の資料番号を示してください。\\n\\n\"\n", " f\"【資料】\\n{context}\\n\\n【質問】\\n{rag_question}\"\n", " )\n", " print(\"\\nモデルが生成した回答:\")\n", " rag_result = ask_llm(grounded_prompt, temperature=0.7, max_new_tokens=128)\n", " show_answer(rag_result)\n" ], "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "id": "note-21", "metadata": {}, "source": [ "**試してみること**\n", "\n", "- 質問を「プロジェクターはどう予約しますか。」に変更して実行する。\n", "- 資料01に記載されている「7日」という数値を「3日」に書き換えて、同一の質問を再実行する。\n", "- 質問を「タブレットは借りられますか。」に変更し、キーワードに合致する資料が存在しない場合の出力を観察する。\n", "\n", "資料内の数値を変更した際、生成された回答にもその変更が正しく反映されたかを確認してください。もし古い数値のまま答えていたり関係のない内容を出力したりした場合は、プロンプト内での資料の配置や指示の明確さを見直します。実運用のRAG(検索拡張生成)システムでは、検索ステップで正しい資料を取得できているかという評価と、取得した資料をモデルが正しく活用できているかという評価を切り分けて検証します。\n" ] }, { "cell_type": "markdown", "id": "note-22", "metadata": {}, "source": [ "## 実験を終える\n", "\n", "実行結果を手元に残しておきたい場合は、Colabノートブックの変更内容を保存してください。作業完了後は、メニューの **[ランタイム]→[接続を解除して削除]** を実行して仮想マシンを停止し、不要なコンピューティングリソースの消費を防ぎます。なお、このノートブックでは事前学習済みのモデルを推論モードで読み込んで使用しており、モデルへの追加学習(ファインチューニング)は行っていません。\n", "\n", "### 参照した資料\n", "- [Qwen3-0.6Bの公式モデルカード](https://huggingface.co/Qwen/Qwen3-0.6B)(Apache-2.0)\n", "- [Hugging Face Transformers](https://huggingface.co/docs/transformers/index)\n", "- [Google Colab FAQ](https://research.google.com/colaboratory/faq.html)\n", "\n", "### 関連記事\n", "- [LLMの基本](https://ai.technology-developer.com/notes/llm-basics/)\n", "- [プロンプトの書き方](https://ai.technology-developer.com/notes/prompt-design/)\n", "- [temperatureの仕組み](https://ai.technology-developer.com/notes/temperature/)\n", "- [トークンと料金](https://ai.technology-developer.com/notes/tokens-and-cost/)\n", "- [RAGの基本](https://ai.technology-developer.com/notes/rag-basics/)\n", "\n", "※ 本教材の解説文の作成には生成AIを活用しています。また、言語モデルによる生成テキストは実行環境やパラメータによって変動するため、ノートブック内に固定的な回答例は掲載していません。\n" ] } ], "metadata": { "colab": { "name": "AI手帖 はじめてのLLM実験室.ipynb", "toc_visible": true }, "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python", "version": "3.11" } }, "nbformat": 4, "nbformat_minor": 5 }