即戦力化　ディープラーニング実習（第五週）

# 【第５週】対話生成と意味解析（chatbot / txt2vec）

講義目録：<a href="https://qiita.com/takenakayoichi/items/3b7435f4b155a181e39f">即戦力化　ディープラーニング実習 </a>


## はじめに

第5週では、**AIによる会話生成（チャットボット）**と、**テキストの意味をベクトルで表現する技術（意味ベクトル、埋め込み）**の2つを学びます。

前週までに扱った生成（txt2txt）や分類（txt2cls）は「文章を生成する」「ラベルをつける」といったタスクでしたが、  
今週は「文脈に応じた返答を作る」能力や、「文章の意味を数値として表現する」能力に注目します。

この週のテーマは大きく分けて2つです：

1. **chatbot（対話生成）**  
　ユーザの発言に対して、AIが自然な応答を返すような処理を行います。  
　たとえば、「おすすめの本ある？」と聞けば「最近人気のミステリーはいかがですか？」といった返答が返ってきます。

2. **txt2vec（意味ベクトル）**  
　文章を、類似性や意味的な近さを計算できる「ベクトル（数値の配列）」に変換します。  
　たとえば、「美味しいカレー」と「うまいインド料理」が似たベクトルになる、といった使い方です。

これらは、**会話AI・FAQ検索・意味検索・レコメンドシステム**など、実社会の応用で頻出する技術です。

今週もColab上で手を動かしながら、体験していきましょう！

**Google Colabへのリンク**
- [対話生成のcolab](https://colab.research.google.com/drive/147mzncDYHZpPtSz6BMg5fMrC6XH8nsuB?usp=sharing)
- [意味ベクトルのcolab](https://colab.research.google.com/drive/1Cxn2KYND6JFZBGDzvqim3tuxOHuIhhzm?usp=sharing)


---

## 1. 実習の目的

- **Chat形式のテキスト生成（chatbot）**を実装する  
- 会話の履歴をもとに、自然な応答を生成する方法を学ぶ  
- **テキストをベクトル（数値列）に変換**する仕組みを理解する  
- 意味ベクトルを使って、**類似文章の検索**や**意味的な可視化**を体験する

特に今週は、「文章の意味を“数値”で表現するとはどういうことか？」という直感を養ってください。  
それが、今後の検索や推薦などの応用に繋がります。

---

### 🤖 Chatbotと「会話履歴」の構造

チャットボットでは、1回の応答で完結するのではなく、  
**複数ターンの会話（履歴）を踏まえて自然な返答を行う**ことが重要です。

そのために、以下の3種類のロールが使われます：

- `system`：AIの性格・ふるまい・目的を最初に定義します（例：「親切なアシスタントです」など）
- `user`：ユーザの発言です。実際の質問や指示に相当します
- `assistant`：AIの応答。過去の返答を履歴として明示的に残すことで、文脈を記憶させます

---

### 📘 例1：ビジネス向けの会話履歴（業務サポート）

**目的**：会議のリマインダーを確認するビジネスアシスタントとしての応答

**会話の構造**：

- **1ターン目**：ユーザ「明日の予定を教えて」  
- **2ターン目**：アシスタント「明日は10時から営業会議、14時から技術レビューです。」  
- **3ターン目**：ユーザ「営業会議はどこでやるの？」  
- → **4ターン目**：これまでの流れを踏まえて、会議場所を案内する

**Chat形式**：

```python
messages = [
  {"role": "system", "content": "あなたはビジネス予定管理を行う日本語アシスタントです。"},
  {"role": "user", "content": "明日の予定を教えて"},                                  # 1ターン目
  {"role": "assistant", "content": "明日は10時から営業会議、14時から技術レビューです。"},     # 2ターン目
  {"role": "user", "content": "営業会議はどこでやるの？"}                             # 3ターン目
  # → 次に続く4ターン目の出力をモデルに生成させる
]
```

このように、ユーザの過去の質問とアシスタントの返答をすべて渡すことで、  
AIは「文脈を踏まえた自然な応答」が可能になります。

---

### 😅 例2：ツンデレ風アシスタントでの会話履歴（キャラクター応答）

**目的**：キャラクター性を持ったアシスタントによる対話を継続させる

**会話の構造**：

- **1ターン目**：ユーザ「今日の天気は？」  
- **2ターン目**：アシスタント「べ、別にあんたのために教えるんじゃないけど……晴れよ。」  
- **3ターン目**：ユーザ「ありがとう」  
- → **4ターン目**：この会話を受けた、ツンデレ風な追加返答を出力

**Chat形式**：

```python
messages = [
  {"role": "system", "content": "あなたはツンデレ風の日本語アシスタントです。"},
  {"role": "user", "content": "今日の天気は？"},                            # 1ターン目
  {"role": "assistant", "content": "べ、別にあんたのために教えるんじゃないけど……晴れよ。"},  # 2ターン目
  {"role": "user", "content": "ありがとう"}                               # 3ターン目
  # → 次に続く 4ターン目の出力をモデルに生成させる
]
```

この構造により、AIは「自分が以前どう返したか（assistant）」を含む、  
すべての履歴を文脈として理解し、キャラクター性を保ったまま返答できます。

---
### 🧪 実習：Google ColabでChat形式の応答を試してみよう

以下のコードをGoogle Colabに貼り付けて実行してください。  
ここでは、ビジネス用アシスタントとして、**3ターン分の履歴**を与えたうえで、**4ターン目の応答**を生成させます。

---

**想定される会話の流れ：**

1. ユーザ：「明日の予定を教えて」  
2. アシスタント：「明日は10時から営業会議、14時から技術レビューです。」  
3. ユーザ：「営業会議はどこでやるの？」  
4. → ここに対するAIの返答を生成します（例：「営業会議は第3会議室です」など）

---

```python
!pip install -q transformers accelerate

from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline
import torch

# モデルの読み込み
model_id = "owner203/japanese-llama-3.1-8b-instruct-2"
tokenizer = AutoTokenizer.from_pretrained(model_id, use_fast=False)
model = AutoModelForCausalLM.from_pretrained(model_id, device_map="auto", torch_dtype=torch.float16)
generator = pipeline("text-generation", model=model, tokenizer=tokenizer, device_map="auto")

# 会話履歴（3ターン）
messages = [
    {"role": "system", "content": "あなたはビジネス予定管理を行う日本語アシスタントです。"},
    {"role": "user", "content": "明日の予定を教えて"},
    {"role": "assistant", "content": "明日は10時から営業会議、14時から技術レビューです。"},
    {"role": "user", "content": "営業会議はどこでやるの？"}
]

# 会話テンプレートを適用して応答生成
prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
output = generator(prompt, max_new_tokens=200, do_sample=True, temperature=0.7)

# 応答の表示

# "assistant" という文字の最後の出現位置を基準に応答を抽出
import re
match = re.search(r"<\|start_header_id\|>assistant<\|end_header_id\|>\n\n(.*?)<\|eot_id\|>", generated, re.DOTALL)
if match:
    print("✅ 4ターン目の応答:")
    print(match.group(1).strip())
else:
    print("⚠️ 応答が見つかりませんでした")
```

---

**✅ 実行結果のチェックポイント：**

- 「営業会議はどこでやるの？」という直前の質問に対して自然な返答ができているか
- 過去の発言（予定内容）を踏まえた文脈ある応答になっているか


このように「assistantロール」を履歴に含めることで、  
AIは過去の返答を“自分の発言”として再認識でき、**継続性のある会話**が可能になるのです。

---

### 💬 実習課題

- `system` のロールを変えてAIの性格を調整してみよう  
- `assistant` の履歴を除くとどうなるか試してみよう（文脈がリセットされる）  
- 同じユーザ発言に対して、前回のAI返答を踏まえた違いが出るか確認しよう  

---

## 3. 意味ベクトルとは？（txt2vec）

意味ベクトル（semantic vector, text embedding）とは、  
**文章や単語を、意味的な特徴を保ったまま数値のベクトルに変換したもの**です。

このようなベクトル表現を使うことで：

- 類似した意味を持つ文が、**近い数値のベクトル**になる
- 検索や分類を、**数値的な距離計算**で行える
- 次元削減して、**視覚的に配置**（可視化）できる

といった、多くの応用が可能になります。

---

### 🧠 なぜ意味ベクトルが重要なのか？

たとえば以下の2つの文を見てください：

- 文1：「カレーが食べたい」
- 文2：「インド料理を食べたい」

人間なら「なんとなく似てる」と感じますよね？  
しかしAIにとっては、単語がまったく違うので、**表面的な一致では似ていない**と判断してしまいます。

そこで、「意味的に近いものはベクトルとしても近づけよう」とするのが意味ベクトル（txt2vec）の考え方です。

---

### 🧪 実習：日本語文をベクトルに変換し、意味の距離を比べてみよう

ここでは、**日本語に特化した Sentence-BERT モデル**  
[`sonoisa/sentence-bert-base-ja-mean-tokens`](https://huggingface.co/sonoisa/sentence-bert-base-ja-mean-tokens) を使って、文の類似度を調べてみましょう。

```python
!pip install -q sentence-transformers fugashi ipadic

from sentence_transformers import SentenceTransformer, util

# 日本語用 Sentence-BERT モデル
model = SentenceTransformer("sonoisa/sentence-bert-base-ja-mean-tokens")

# 比較文
sentences = [
    "カレーが食べたい",            # 文A
    "インド料理を食べたい",        # 文B（意味が近い）
    "フレンチトーストを作りたい",  # 文C（料理だが違う）
    "昨日はスポーツ観戦に行った"   # 文D（意味が遠い）
]

# ベクトル取得と類似度計算
embeddings = model.encode(sentences, convert_to_tensor=True)

print("文A: 「カレーが食べたい」 に対する類似度")
for i in range(1, len(sentences)):
    score = util.cos_sim(embeddings[0], embeddings[i]).item()
    print(" - vs 文{}: 「{}」 → 類似度: {:.4f}".format(i, sentences[i], score))
```

### ✅ 実行結果の見方

上記コードでは、2文間の「cos類似度（cosine similarity）」が表示されます：

- 1に近いほど「意味的に似ている」
- 0に近いと「無関係」
- 負の値は「意味が真逆」

このように、**人間の感覚に近い“意味の近さ”**を数値化しています。

---

### ✅ 実行結果の確認

類似度は以下のような傾向になるはずです：

- 文B：「インド料理を食べたい」 → 類似度 0.598 （非常に近い）  
- 文C：「フレンチトーストを作りたい」 → 類似度 0.379（ジャンルは同じ）  
- 文D：「昨日はスポーツ観戦に行った」 → 類似度 0.181（ほぼ無関係）

このように、**意味が離れるほどcos類似度は0に近づいていく**ことがわかります。

---

### 📝 追加実習

次の文を追加して、自分で類似度を比べてみましょう：

- 「ラーメンが食べたい」  
- 「スポーツ選手の引退ニュース」  
- 「京都の寺を観光したい」

それぞれ、「カレーが食べたい」との意味的な距離を観察してみてください。

## 4. 類似文検索とその必要性

意味ベクトルを使うと、文と文の「意味的な近さ」を数値で比較できるようになります。  
この仕組みを活かすことで、**ある質問や問い合わせに「意味の似た過去の文」を探す**ことができます。

---

### 🔍 たとえば以下のような用途があります：

- **FAQ検索**：「返品できますか？」という質問に対して、「商品の返送手続きは可能ですか？」など類似した回答を返す  
- **チャットボットの強化**：ユーザの入力と似た過去の発言履歴を検索し、自然な返答を導く  
- **文章推薦やナレッジ検索**：報告書・ニュース記事などの中から「意味的に関連する文」を見つける

---

## 🧪 実習：検索対象文の中から、クエリに最も近い文を探す

以下の例では、ユーザのクエリ文に対して「意味的に最も近い文」を1つ見つけ出します。

```python
!pip install -q sentence-transformers fugashi ipadic

from sentence_transformers import SentenceTransformer, util

# モデル読み込み（日本語対応）
model = SentenceTransformer("sonoisa/sentence-bert-base-ja-mean-tokens")

# 検索対象の文（過去のFAQや履歴などを想定）
corpus = [
    "返品したいのですが可能ですか？",
    "支払い方法を変更できますか？",
    "注文内容をキャンセルしたいです",
    "配達予定日はいつですか？",
    "商品の返送手続きについて教えてください"
]

# クエリ（ユーザの入力）
query = "返品できますか？"

# ベクトルに変換
corpus_embeddings = model.encode(corpus, convert_to_tensor=True)
query_embedding = model.encode(query, convert_to_tensor=True)

# 類似度の計算
cos_scores = util.cos_sim(query_embedding, corpus_embeddings)[0]

# 最も類似度が高い文を取得
top_result = cos_scores.argmax().item()
print("🔍 クエリ: ", query)
print("✅ 最も近い文: ", corpus[top_result])
print("📊 類似度スコア: {:.4f}".format(cos_scores[top_result].item()))
```

---

### ✅ 出力例

```
🔍 クエリ: 返品できますか？
✅ 最も近い文: 商品の返送手続きについて教えてください
📊 類似度スコア: 0.922
```

このように、「単語が一致していなくても意味が似ていれば検出できる」のが意味ベクトルの強みです。

---

次は、複数件を一度に検索し、上位N件をランキングで表示する方法も試してみましょう。


## 5. 類似文検索（応用編）：トップ3の結果を表示しよう

前回は、1件だけ最も近い文を返す検索を行いました。  
ここでは、**意味的に近い上位3件をスコア順に並べて表示**します。

---

### 🧪 実習：類似度スコア順にトップ3を出力する

```python
!pip install -q sentence-transformers fugashi ipadic

from sentence_transformers import SentenceTransformer, util

# モデル読み込み
model = SentenceTransformer("sonoisa/sentence-bert-base-ja-mean-tokens")

# 検索対象の文（コーパス）
corpus = [
    "返品したいのですが可能ですか？",
    "支払い方法を変更できますか？",
    "注文内容をキャンセルしたいです",
    "配達予定日はいつですか？",
    "商品の返送手続きについて教えてください",
    "商品のサイズ交換はできますか？",
    "領収書の発行は可能ですか？"
]

# クエリ（ユーザ入力）
query = "返品できますか？"

# ベクトル変換
corpus_embeddings = model.encode(corpus, convert_to_tensor=True)
query_embedding = model.encode(query, convert_to_tensor=True)

# 類似度スコア計算
cos_scores = util.cos_sim(query_embedding, corpus_embeddings)[0]

# スコア順に上位3件を抽出
top_k = 3
top_results = torch.topk(cos_scores, k=top_k)

# 表示
print("🔍 クエリ:", query)
print("✅ 類似文ランキング（上位3件）:")
for score, idx in zip(top_results.values, top_results.indices):
    print(f" - 類似度: {score:.4f} ｜ 文: {corpus[idx]}")
```

---

### ✅ 実行結果の例

```
🔍 クエリ: 返品できますか？
✅ 類似文ランキング（上位3件）:
 - 類似度: 0.9226 ｜ 文: 商品の返送手続きについて教えてください
 - 類似度: 0.6864 ｜ 文: 商品のサイズ交換はできますか？
 - 類似度: 0.6473 ｜ 文: 注文内容をキャンセルしたいです
```

---

## 🎯 精度を上げるための工夫

類似文検索の精度を向上させるためには、以下の工夫が有効です：

### ✅ クエリ側の工夫

- **ユーザの入力を前処理する**（例：ひらがな⇔漢字変換、不要語の除去）  
- **入力文を正規化する**（句読点や空白の整理）  
- **クエリ拡張**：同義語や関連語も含めて検索（例：「返品」「返送」「キャンセル」）

### ✅ コーパス側の工夫

- **表記ゆれの統一**（「できますか？」「できますでしょうか？」など）  
- **類似文の事前クラスタリング**（検索対象をグループ化）  
- **重要文だけを対象とするフィルタリング**（問い合わせ対応履歴のうち代表例だけを使う）

### ✅ モデル選定の工夫

- **特定ドメインで再学習・微調整**（例：FAQデータでファインチューニング）  
- **より大規模なモデルへ切り替え**（例：`sentence-bert-large-ja`）

---

検索システムを構築する際は、**ユーザ入力・コーパス・モデル**の3点を見直すことで精度を高めることができます。

---

### 🧭 精度向上の工夫について

類似文検索は非常に強力な手法ですが、**対象とする文書の種類によってはそのままではうまくいかないことがあります。**

たとえば筆者のように、**戦前の法律文書**を扱う場合には、  
- 旧仮名遣い（例：「けふ」「ひろし」）  
- カタカナ表記（例：「ホウリツ」）  
- 歴史的助詞や漢文的語順  

などの理由から、現代的な表記で書かれたクエリと一致しづらくなります。

こうした場合、**原文のまま**検索してもベクトルがうまく一致せず、低スコアになってしまう**ことがあります。

このように、検索制度を最大限に発揮するためには、**「前処理」や「表記の正規化」**などの工夫が欠かせません。

---

## 🎯 精度を上げるための工夫

類似文検索の精度を向上させるためには、以下の工夫が有効です：

### ✅ クエリ側の工夫

- **ユーザの入力を前処理する**（例：ひらがな⇔漢字変換、不要語の除去）  
- **入力文を正規化する**（句読点や空白の整理）  
- **クエリ拡張**：同義語や関連語も含めて検索（例：「返品」「返送」「キャンセル」）

### ✅ コーパス側の工夫

- **表記ゆれの統一**（「できますか？」「できますでしょうか？」など）  
- **類似文の事前クラスタリング**（検索対象をグループ化）  
- **重要文だけを対象とするフィルタリング**（問い合わせ対応履歴のうち代表例だけを使う）

### ✅ モデル選定の工夫

- **特定ドメインで再学習・微調整**（例：FAQデータでファインチューニング）  
- **より大規模なモデルへ切り替え**（例：`sentence-bert-large-ja`）

---

あなたが検索システムを構築する際には、**ユーザ入力・コーパス・モデル**の3点を見直すことで精度を高めることができるかもしれませんよ。

## 🔚 第5週のまとめ：対話生成と意味ベクトル

今週は以下の2つのテーマに取り組みました：

---

### 💬 1. 対話生成（chatbot）

- メッセージ形式（role付き構造）を用いた会話生成を学びました
- 過去の履歴（assistantロール）を活用することで、文脈に沿った自然な返答が可能になることを体験しました
- Chat形式の入力には、`system`（性格や口調）、`user`（質問）、`assistant`（返答）という3つのロールを活用します

---

### 🧠 2. 意味ベクトル（txt2vec）

- 文章の意味を数値ベクトルで表現し、「意味的に近い文同士はベクトルも近くなる」ことを確認しました
- `Sentence-BERT` を用いたベクトル化で、日本語の文同士の距離を比較できることを実感しました
- 類似度（cosine similarity）に基づき、FAQ検索や問い合わせ対応に応用できることを学びました
- 精度向上のための前処理（表記の統一やクエリ変換）の重要性も確認しました

---

## 🔭 次回予告：第6週「情報抽出（Named Entity Recognition, txt2seq）」

次週は、文章中から**特定の情報を抜き出す**というタスクに挑戦します。

たとえば：

- 「人物名」「組織名」「日付」「場所」などを自動で抽出する  
- 形式的な文章から、**重要なキーワードだけを抽出**する  
- 文の中から、AIが「これは固有名詞だ」と判断する根拠を体験する

このようなタスクは、**Named Entity Recognition（固有表現抽出）**と呼ばれ、実務でも幅広く利用されています。

---

次週も、Colabを使って「ディープラーニング」を一緒に体験していきましょう！