# AI のしくみを自分の手で確かめる（全10回・1回15分・v1.2）

**この講座のゴール**：短いプログラムで AI が学ぶ様子を見て、睡眠を判定する AI を読む。

高校の数学を学んだ大学生・高校生向けに、短い Python のプログラムを自分で読み、動かし、1か所だけ直しながら、AI が「データを読む」「学ぶ」「判定する」仕組みを確かめる講座です。最後の3回は、この研究室が論文として発表し、プログラムを公開している睡眠判定の AI を題材にします。10回を終えると、他人が作った AI を論文と説明書きから読み、どこを変えると何が起きるかを自分で試せるようになります。

英語で学びたい人は、最初に『Please teach this course in English.』と伝えてください。

---

## はじめに（この講座の使い方）

### これは何か
神戸大学大学院医学系研究科 神経生理学分野が、学生の自習用に公開している教材です。1日15分、全10回で、AI（人工知能）の動作の仕組みを、プログラムを読む・動かす・直すことを通して学びます。読むだけの教科書ではなく、対話型の AI を先生役にして、問いに答えながら進める形の講座です。

この講座の順路は、研究室の教員が自分で機械学習（データから規則を作らせる方法）のプログラムを読めて直せるようになるまでに、実際にたどった道筋を元にしています。「全部を一から作れる」ことではなく、「人が作った AI を読み、1か所変えて、効果を確かめられる」ことを目標にしています。第8〜10回では、この研究室が2021年に発表し、プログラムを公開している、マウスの脳波から眠りの段階を判定する AI（研究室での呼び名は「手塚AI」）を読み、同じ考え方の小さな模型で1か所変えて効果を測ります。

### 対象（予備知識の目安）
- 高校の数学（1次関数・平均・割合）が分かれば足ります。
- 微分を知らなくても進められます。第6回では「少し動かして、誤差が増えるか減るかを見る」という考え方を使います。
- プログラミングは初めてで構いません。第1回で、必要な書き方だけを最小限で学びます。

### 準備するもの
- **対話型の AI**（チャットで質問に答えてくれるもの）。ファイルを読み込ませられるものが便利です。
- **紙と鉛筆、電卓**（スマートフォンの電卓で十分です）。
- **Python（パイソン）を動かせる環境**（第1回から使います）。無料で、ブラウザで動かせる環境で構いません。例えば Jupyter の公式サイトの「Try Jupyter」のページ（https://jupyter.org/try ）で「JupyterLab」を選ぶと、何もインストールせずにブラウザの中で Python が動きます。
- 使う道具（ライブラリ＝便利な関数の詰め合わせ）は、numpy（ナンパイ・数の計算）、pandas（パンダス・表の計算）、matplotlib（マットプロットリブ・図を描く）、scikit-learn（サイキットラーン・機械学習の道具箱、第7・9・10回）です。データは、第7回の公開データ以外、すべて講座の中のプログラムで作ります。
- Python を動かせない日は、AI に「出力の例を見せてください」と頼み、それを見ながら考えるだけでも進められます。

### 進め方
1. このファイルを対話型の AI に読み込ませます（ファイルを添付する、または全文を貼り付ける）。
2. 「第1回を始めてください」と打ちます。
3. AI の問いに、自分の言葉で答えます。間違えて構いません。間違えた所が、いちばん身につく所です。
4. 15分で区切ります。次の日に、もう一度このファイルを読み込ませて「第2回を始めてください」と打ちます。

1日1回で10日、まとめて進めても構いませんが、1回ずつ間を空けるほうが記憶に残ります。

### 注意
- **AI は間違えることがあります。** 大事な所は、教科書や原典で確かめてください。第10回でも、数字を原典で確かめる大切さを扱います。
- **自分や他人の個人情報、診療の情報、他人のデータを AI に入れないでください。** この講座は、作り物のデータと公開データだけで進められるようにできています。
- この講座は医療上の助言を目的としたものではありません。

### フィードバックのお願い（必ずお読みください）

> **この講座は、使った人の声で毎回直しています。使った方は、必ずフィードバックをお送りください。**
>
> 送り先：研究室ホームページの「連絡先」のページに載っているアドレス
> 件名：「講座フィードバック：AIの動作原理」
> 書いてほしいこと（4点）：
> 1. 何回目まで進んだか
> 2. 分かりにくかった所（回と場所）
> 3. 良かった所
> 4. 1回にかかった時間（おおよそ）
>
> 途中でやめた方の声も、たいへん役に立ちます。

### 利用の条件
- 個人の学習、授業での利用は自由です。
- 改変して配るときは、研究室までご連絡ください。
- 内容は 2026年11月時点のものです。

---

## AI への指示（先生役のやり方）

ここから下は、このファイルを読み込んだ AI に向けた指示です。学習者の方も読んで構いません。

あなた（AI）は、この講座の先生役です。次のとおりに進めてください。

1. **1回15分で区切る。** 目安は、説明5分・手を動かす課題7分・確認3分です。学習者が「第N回を始めてください」と言った回だけを扱います。
2. **各回の始めに、現在地を1行で示す。** 例「第4回／全10回。前回までに、表を読み、欠けた値を除き、図にできるようになりました」。
3. **1度に1つだけ問い、相手の答えを待つ。** 問いを2つ以上まとめて出さないでください。
4. **答えを先に言わない。** 各回の「答えと考え方（AI 用）」は、学習者が自分で答えた後か、2回考えても分からないと言った後に、必要な部分だけを使ってください。そのまま貼り付けないでください。
5. **まだ出ていない書き方を使わないと解けない問いを出さない。** 各回の「この回で新しく出る書き方」に載っているものと、それより前の回で出たものだけで解ける問いにしてください。新しい関数を使うときは、使う前に「名前＝何をするか」を一言添えます。前の回で出たものは、説明なしで使って構いません。
6. **ヒントは部品だけ。** 詰まったら、使う関数の名前や、見るべき行を示します。答えのコードを丸ごと書かないでください。組み立てるのは学習者です。
7. **分からない様子なら、一段やさしく言い直す。** 身近な例に置き換える、数を小さくする、図を言葉で描く、のどれかで言い直します。
8. **各回の終わりに、要点を3つと、ミニ確認問題を2問出す。** 確認問題は各回の「確認の問い」から選んでよいです。
9. **次の回へ勝手に進まない。** 回が終わったら「今日はここまでです。次は『第N+1回を始めてください』と打ってください」と伝えて止まります。
10. **学習者が医療の相談や個人情報を書いたら、扱わずに丁寧に断る。** 「この講座では医療の相談や個人の情報は扱えません。入力した内容は消しておくと安全です」と伝え、講座に戻ります。学習者の名前・学校・連絡先を尋ねないでください。
11. **最初の回の始めに、2つだけ尋ねる。** ①高校の数学（1次関数・平均）にどのくらい自信があるか ②プログラムを書いたことがあるか。答えに合わせて説明の細かさを変えてください。
12. **コードは学習者が自分で動かす。** 動かせない人には、その回の「出力の例」を示して、数字の意味を考えてもらいます。出力の数字は環境によって少し違うことがあります。エラーが出たら、まずエラーの最後の1行を一緒に読んでください。`name 'pd' is not defined` のように「名前が無い」と言われたら、ブラウザの Python が覚えていた内容を忘れた（時間が経った・開き直した）ことが多いので、上のセルから順に実行し直すよう伝えてください。
13. **本文に無い事実を聞かれたら、確かな範囲でだけ答える。** 確かでなければ「確かではありません」と言い、確かめ方（教科書・原典・データベース）を示してください。
14. **各回の最後に1行だけ、フィードバックのお願いを添える**（「使った感想を、研究室ホームページの連絡先のページからお送りください」）。第10回では、はじめにの「フィードバックのお願い」の4点を丁寧に案内してください。

---

## 第1回　Python の最初の一歩（約15分）

### この回のねらい
1. ブラウザで Python を動かし、セル（コードを書く枠）を実行できる。
2. 変数・リスト・for・if・関数の5つを、自分の言葉で説明できる。
3. 「平均より大きい値だけを表示する」プログラムを自分で書ける。

### 説明
AI のプログラムを読めるようになるのに、プログラミング言語の全部を覚える必要はあるでしょうか。答えは、ありません。この講座の10回で使う書き方は、ほとんどが今日の5つの組み合わせです。

第1〜6回では、1つの作り物のデータを通して使います。**教室の騒音計**（音の大きさを測る機械）が、1秒ごとに音の大きさを記録した数です。単位は dB（デシベル、音の大きさの単位）で、静かな授業中は 45 前後、にぎやかな休み時間は 65 前後になる、という設定です。ここで学ぶ「直近の何秒ぶんかを切り出して判定する」考え方は、第8〜10回で読む、研究室の睡眠判定の AI でもそのまま使われています。

今日の5つの書き方は次のとおりです。

- **変数**：値に名前を付けて覚えておく箱。`mean = 52.9` と書くと、以後 `mean` で 52.9 を呼び出せます。
- **リスト**：値を順に並べたもの。`[44, 46, 45]` のように角かっこで囲みます。
- **for**：リストの中身を1つずつ取り出して、同じことを繰り返す。行の終わりのコロン `:` と、次の行の字下げ（スペース4つ）が「ここからここまでを繰り返す」の印です。
- **if**：条件が成り立つときだけ実行する。これもコロンと字下げを使います。
- **関数**：手順にまとめて名前を付けたもの。`def 名前(材料):` で作り、`return` で答えを返します。

かっこ `()` は「今、実行して」という合図です。`len(levels)` は「levels の個数を数える」を今実行する、という意味です。

ブラウザの Python には、覚えた変数を、時間が経ったり開き直したりすると忘れるという性質があります。「名前が無い」というエラーが出たら、上のセルから順に実行し直してください。

**この回で新しく出る書き方**：変数への代入 `=`、リスト `[ ]`、`for x in リスト:`、`if 条件:`、比べる記号 `>` `<` `>=` `<=` `==`、`def 関数名(材料):` と `return`、`print()`（表示する）、`len()`（個数を数える）、`sum()`（合計する）

### 手を動かす課題
JupyterLab で「Python」のノートブックを新しく作り、次のコードを1つのセルに貼って実行してください（Shift キーを押しながら Enter）。

```python
# 教室の騒音計が1秒ごとに記録した音の大きさ（dB）。講座用の作り物の数です
levels = [44, 46, 45, 47, 63, 66, 64, 65, 45, 44]

print(len(levels))          # 個数
print(sum(levels))          # 合計
mean = sum(levels) / len(levels)
print(mean)                 # 平均

def is_loud(x):             # 関数：55 dB を超えたら True（はい）を返す
    return x > 55

for x in levels:
    if is_loud(x):
        print(x, "にぎやか")
```

出力の例：
```text
10
529
52.9
63 にぎやか
66 にぎやか
64 にぎやか
65 にぎやか
```

**自分で書く課題**：同じ `levels` を使い、**平均（`mean`）より大きい値だけ**を表示するプログラムを、新しいセルに書いてください。使うのは for と if と print です。

### 確認の問い
1. `for x in levels:` の行の終わりのコロンを消すと、何が起きると思いますか。
2. 関数 `is_loud` の 55 を 64 に変えると、表示される行はいくつになりますか。
3. `print(mean)` と `mean` だけの行の違いは何でしょうか（ヒント：セルの最後の行だけは、print が無くても表示されます）。

<details>
<summary>答えと考え方（AI 用。学習者が自分で答えるまで見せない）</summary>

- 自分で書く課題の答えの例：
  ```
  for x in levels:
      if x > mean:
          print(x)
  ```
  表示は 63、66、64、65 の4行。is_loud の代わりに `x > mean` を書けたかを見る。字下げがずれてエラーになったら、字下げが「どこまでが for の中か」を表すことを確かめる。
- 確認1：`SyntaxError`（書き方の誤り）になる。コロンと字下げが、for の範囲を Python に伝えている。
- 確認2：64 を超えるのは 66 と 65 の2つなので2行（64 は「超える」に入らない。`>=` にすると3行）。
- 確認3：`print()` はセルのどこに書いても表示する。`mean` だけの行は、それがセルの最後の行のときだけ自動で表示される。途中の値を見たいときは print を使う。
</details>

### まとめ
- 変数は名前つきの箱、リストは並び、for は繰り返し、if は条件、関数は手順のまとまり。
- コロン `:` と字下げが「どこからどこまで」を表す。かっこ `()` は「今実行して」の合図。
- ブラウザの Python は覚えたことを忘れることがある。困ったら上のセルから順に実行し直す。

---

## 第2回　まとめて計算し、一部を切り出す（numpy）（約15分）

### この回のねらい
1. numpy の配列（数の並び）で、for を書かずに全部の値をまとめて計算できる。
2. 配列の形（shape）を読める。
3. スライス `[start:end]` で一部を切り出し、それが「窓」になることを説明できる。

### 説明
10個の値のそれぞれから 40 を引くには、for で10回繰り返す必要があるでしょうか。numpy を使えば、1行で済みます。

numpy は、数の並びを「配列」として扱う道具です。配列に `- 40` と書くと、全部の値から一度に 40 が引かれます。このように、1つずつ繰り返さずに並び全体をまとめて計算することを **ベクトル化** と呼びます。速く、読みやすいので、AI のプログラムはほぼ全部この書き方です。

配列の **形（shape）** は、`(10,)` のように表示されます。「10個の値が1列に並んでいる」という意味です。画像なら `(8, 8)`（縦8×横8）のように、数が2つ以上並びます。

もう1つ大事なのが **スライス**（切り出し）です。`levels[0:5]` は「0番目から、5番目の手前まで」の5個を切り出します。番号は0から数え、終わりの番号は含みません。

この「先頭5秒ぶんを切り出す」操作は、実は第8〜10回の主役です。AI が時間とともに届くデータを判定するとき、多くは「直近の何秒ぶん」を切り出して判定します。この切り出した範囲を **窓** と呼びます。今日のスライスは、その窓そのものです。

**この回で新しく出る書き方**：`import numpy as np`（numpy を np という短い名前で使う）、`np.array([...])`（リストを配列に変える）、配列と数の計算（`配列 - 40` など）、`配列.mean()`（平均）、`配列.shape`（形）、スライス `配列[start:end]`

### 手を動かす課題

```python
import numpy as np

levels = np.array([44, 46, 45, 47, 63, 66, 64, 65, 45, 44])
print(levels.shape)
print(levels - 40)          # 静かな部屋（40 dB）からの上がり分を、全部まとめて
print(levels.mean())
print(levels[0:5])          # 先頭5秒の窓
print(levels[0:5].mean())
```

出力の例：
```text
(10,)
[ 4  6  5  7 23 26 24 25  5  4]
52.9
[44 46 45 47 63]
49.0
```

**自分で書く課題**：2つ目の窓（5秒目から9秒目までの5個）を切り出して表示し、その平均も表示してください。どちらの窓が「にぎやか」に近いでしょうか。

### 確認の問い
1. 第1回のリスト `[44, 46, ...]` のままで `- 40` を計算すると、どうなると思いますか。
2. `levels[2:4]` で出てくる値はどれですか。
3. `levels.shape` には `()` が付かず、`levels.mean()` には付きます。違いは何でしょうか。

<details>
<summary>答えと考え方（AI 用。学習者が自分で答えるまで見せない）</summary>

- 自分で書く課題：`print(levels[5:10])` → `[66 64 65 45 44]`、`print(levels[5:10].mean())` → `56.8`。先頭の窓（49.0）より、2つ目の窓（56.8）の方がにぎやかに近い。ただし2つ目の窓の中には、にぎやかな3秒と静かな2秒が混ざっている。窓がどこで区切られるかで判定が変わる、は第8回の伏線。
- 確認1：エラー（`TypeError`）になる。リストには「全部から引く」計算が無い。ベクトル化は numpy の配列の力。
- 確認2：2番目と3番目の2個、`[45 47]`（0から数え、4番目は含まない）。
- 確認3：`mean()` は「今、平均を計算して」という動作なので `()` が付く。`shape` は配列が持っている性質を読むだけなので付けない。
</details>

### まとめ
- numpy の配列は、for を書かずに全部まとめて計算できる（ベクトル化）。
- shape は配列の形。スライス `[start:end]` は0から数え、end は含まない。
- 「直近の何秒ぶん」を切り出したものが窓。AI が時間のデータを判定するときの基本の部品。

---

## 第3回　表として扱い、図にする（pandas と matplotlib）（約15分）

### この回のねらい
1. CSV（カンマで区切った表のファイル）を pandas で読み、形と中身を確かめられる。
2. 欠けた値を見つけて取り除き、グループごとに平均を出せる。
3. matplotlib で、横軸と縦軸を指定した図を描ける。

### 説明
研究のデータが最初から「きれいな表」で届くことは、どのくらいあるでしょうか。答えは、ほとんどありません。測る機械の調子が悪く値が抜けることも、記録の途中で止まることもあります。AI に渡す前に、表として読み、欠けた所を確かめるのが最初の仕事です。

pandas は、numpy の配列に「列の名前」を付けて、表として扱えるようにした道具です。この表を **DataFrame**（データフレーム）と呼び、よく `df` という名前の変数に入れます。

今回は、講座の関数でまず CSV ファイルを作り、それを読み込みます。60秒ぶんの騒音の記録で、列は3つです。`sec`（何秒目か）、`level`（音の大きさ）、`state`（授業か休み時間か）。

欠けた値は `NaN`（ナン、Not a Number＝値が無い印）で表されます。`df.isna().sum()` で列ごとに欠けた値の数を数え、`df.dropna()` で欠けた値を含む行を取り除きます。そして `groupby`（グループ分け）で「授業のときと休み時間のときの平均」を出します。研究の結果の表の多くは、この形をしています。

最後に図を描きます。数の表だけでは見落とすことも、図にすると一目で分かります。

**この回で新しく出る書き方**：`import pandas as pd`、`pd.read_csv("ファイル名")`（CSV を表に読む）、`df.head()`（先頭5行）、`df.shape`、`df.isna().sum()`（欠けた値を数える）、`np.nan`（欠けた値そのもの）、`df.loc[行, "列"] = …`（表の1マスに書き込む）、`df.dropna()`（欠けた値のある行を除いた新しい表を返す）、`df.groupby("列")["列"].mean()`（グループごとの平均）、`.round(1)`（小数1桁に丸める）、`import matplotlib.pyplot as plt`、`plt.plot(x, y)`、`plt.xlabel()` / `plt.ylabel()`（軸の名前）、`plt.show()`（図を表示）

### 手を動かす課題
次の3つのセルを、上から順に実行してください。

```python
import numpy as np
import pandas as pd

def make_noise_csv(path="noise.csv", seed=0):
    """講座用の作り物のデータ：60秒ぶんの騒音（dB）。20〜39秒が休み時間"""
    rng = np.random.default_rng(seed)
    sec = np.arange(60)
    state = np.where((sec >= 20) & (sec < 40), "休み時間", "授業")
    level = np.where(state == "授業", 45, 65) + rng.normal(0, 2, size=60)
    pd.DataFrame({"sec": sec, "level": level.round(1), "state": state}).to_csv(path, index=False)

make_noise_csv()                 # noise.csv というファイルができる
df = pd.read_csv("noise.csv")    # それを表として読む
print(df.head())
print(df.shape)
print(df.isna().sum())
```

出力の例：
```text
   sec  level state
0    0   45.3    授業
1    1   44.7    授業
2    2   46.3    授業
3    3   45.2    授業
4    4   43.9    授業
(60, 3)
sec      0
level    0
state    0
dtype: int64
```

（`make_noise_csv` の中身は、データを作るためだけのものです。今は読めなくて構いません。）

```python
df.loc[3, "level"] = np.nan      # わざと 3 秒目の値を欠けさせる（機械の調子が悪かったつもり）
print(df.isna().sum())
df2 = df.dropna()                # 欠けた値のある行を除いた新しい表
print(df2.shape)
print(df2.groupby("state")["level"].mean().round(1))
```

出力の例：
```text
sec      0
level    1
state    0
dtype: int64
(59, 3)
state
休み時間    65.1
授業      45.2
Name: level, dtype: float64
```

```python
import matplotlib.pyplot as plt

plt.plot(df2["sec"], df2["level"])
plt.xlabel("time (s)")
plt.ylabel("sound level (dB)")
plt.show()
```

（このセルは文字を表示せず、図が1枚出ます。20秒から39秒のところだけ、線が高くなっているはずです。図の文字を英語にしているのは、環境によって日本語の文字が四角く化けることがあるからです。）

**自分で書く課題**：2つ目のセルの最後の行を少し変えて、授業と休み時間のそれぞれの**最大値**を表示してください（ヒント：平均は `mean`、最大は `max`）。

### 確認の問い
1. `df.dropna()` を実行した後も、`df` の行数は 60 のままです。なぜでしょうか。
2. 欠けた値が 1 つだけでなく、休み時間の途中で 10 秒ぶん続けて抜けていたら、groupby の平均はどう変わり得るでしょうか。
3. 図の横軸に `df2["sec"]` を渡さず `plt.plot(df2["level"])` と書くと、横軸は何になると思いますか。

<details>
<summary>答えと考え方（AI 用。学習者が自分で答えるまで見せない）</summary>

- 自分で書く課題：`print(df2.groupby("state")["level"].max())` → 休み時間 68.0、授業 48.9。`.mean()` を `.max()` に変えるだけ。
- 確認1：`dropna()` は「除いた新しい表」を返すだけで、元の `df` は変えない。だから `df2` に受け取っている。
- 確認2：今回は値が少しばらつくだけなので、平均はあまり変わらない。ただし、抜けた10秒が「特にうるさかった時間」なら、休み時間の平均は低く出る。どこが抜けたかで結果が偏ることがある、を言えればよい。次回の「欠けた所をまたぐ」話の入口。
- 確認3：行の番号（0, 1, 2, …）が横軸になる。今回は3秒目を除いたので、行の番号と秒数がずれる。横軸に何を使うかを自分で指定する大切さに気づければよい。
</details>

### まとめ
- pandas は列の名前つきの表。読んだら、まず形（shape）と欠けた値（isna）を確かめる。
- dropna は欠けた行を除いた「新しい表」を返す。groupby でグループごとに集計できる。
- 図は x と y を指定して描く。数の表で見落とすことが、図では一目で分かる。

---

## 第4回　ならす前に、窓の単位を決める（前処理）（約15分）

### この回のねらい
1. 移動平均（窓をずらしながら平均を取ること）で、ギザギザの値をならせる。
2. 窓が埋まるまで結果が NaN になる理由を説明できる。
3. 値が抜けているとき、「5行の窓」と「5秒の窓」で結果が違うことを、数で示せる。

### 説明
AI に渡す前のデータの整え方を **前処理** と呼びます。前処理は地味に見えますが、AI の成績を左右する部分です。理由は主に3つ。値が欠ける、測る機械の雑音でギザギザになる、値の大きさの尺度がそろっていない。今回は2つ目の「ならす」を扱います。

ならす代表の方法が **移動平均** です。連続した5個の平均を取り、1つずらしてまた5個の平均を取る、を繰り返します。第2回の「窓」を1つずつずらしているのと同じです。pandas では `.rolling(window=5).mean()` と書きます。最初の4行は、さかのぼる値が5個そろわないので、結果は NaN（値が無い）になります。

ここで1つ問いです。測る機械の調子が悪く、8秒目から13秒目までの記録が**行ごと**抜けていたら、「5行の窓」で平均を取っても大丈夫でしょうか。

答えは、大丈夫ではありません。行が詰まって並ぶので、「5行」は「5秒」ではなくなります。7秒目の静かな値と14秒目のにぎやかな値が、同じ窓に入ってしまいます。その間に何が起きたか（チャイムの音が入った）を飛ばして、静かとにぎやかを混ぜた平均を作るのです。

こういうときは、窓を「行の数」ではなく「時間の長さ」で決めます。pandas では `df.rolling("5s", on="t")` と書くと、「その時点から過去5秒の中に実際にある値」だけで平均を取ります。

前処理の大事な決まりは2つです。**「取り除く」と「窓をかける」の順番**、そして**窓の単位（行の数か、時間か）**で、同じデータから違う数が出ます。

**この回で新しく出る書き方**：`df["新しい列"] = …`（表に新しい列を作る）、`.rolling(window=N).mean()`（N 行の移動平均）、`pd.to_datetime(df["sec"], unit="s")`（秒の数を時刻に変える）、`df.rolling("5s", on="時刻の列")["列"].mean()`（過去5秒の移動平均）、`pd.DataFrame({"列名": リスト, ...})`（リストから表を作る）

### 手を動かす課題

```python
import pandas as pd

# 第1・2回と同じ10秒ぶんの値
df = pd.DataFrame({"level": [44, 46, 45, 47, 63, 66, 64, 65, 45, 44]})
df["smooth"] = df["level"].rolling(window=5).mean()
print(df)
```

出力の例：
```text
   level  smooth
0     44     NaN
1     46     NaN
2     45     NaN
3     47     NaN
4     63    49.0
5     66    53.4
6     64    57.0
7     65    61.0
8     45    60.6
9     44    56.8
```

4行目の 49.0 は、第2回で計算した「先頭5秒の窓の平均」と同じです。

次は、8〜13秒目が行ごと抜けたデータです。

```python
import pandas as pd

sec   = [0, 1, 2, 3, 4, 5, 6, 7, 14, 15, 16, 17, 18, 19]       # 8〜13秒目が抜けている
level = [45, 46, 44, 45, 46, 45, 44, 46, 65, 66, 64, 65, 66, 64]
df = pd.DataFrame({"sec": sec, "level": level})
df["t"] = pd.to_datetime(df["sec"], unit="s")
df["row5"] = df["level"].rolling(window=5).mean()            # 5行の窓
df["time5"] = df.rolling("5s", on="t")["level"].mean()      # 5秒の窓
print(df[["sec", "level", "row5", "time5"]])
```

出力の例：
```text
    sec  level  row5  time5
0     0     45   NaN   45.0
1     1     46   NaN   45.5
2     2     44   NaN   45.0
3     3     45   NaN   45.0
4     4     46  45.2   45.2
5     5     45  45.2   45.2
6     6     44  44.8   44.8
7     7     46  45.2   45.2
8    14     65  49.2   65.0
9    15     66  53.2   65.5
10   16     64  57.0   65.0
11   17     65  61.2   65.0
12   18     66  65.2   65.2
13   19     64  65.0   65.0
```

**自分で考える課題**：14秒目の行を見てください。5行の窓（row5）の 49.2 と、5秒の窓（time5）の 65.0 は、それぞれどの値の平均でしょうか。手で計算して確かめてください。

（余裕があれば：第3回の `plt.plot` を2回続けて書くと、1つの図に2本の線を重ねて描けます。1つ目の課題の `level` と `smooth` を重ねて、ならした線がどう見えるか描いてみてください。）

### 確認の問い
1. 1つ目のコードで `window=5` を `window=3` に変えると、NaN になるのは最初の何行ですか。
2. 先頭の4行で、row5 は NaN なのに、time5 には値が入っています。なぜでしょうか。
3. 「欠けた値を除いてから、行の数の窓をかける」と、何が危ないでしょうか。1文で答えてください。

<details>
<summary>答えと考え方（AI 用。学習者が自分で答えるまで見せない）</summary>

- 自分で考える課題：row5 の 49.2 は、行で数えて直前5行＝4・5・6・7・14秒目の値（46, 45, 44, 46, 65）の平均で、246 ÷ 5 = 49.2。抜けた6秒をまたいで、授業中の4秒と休み時間の1秒を混ぜている。time5 の 65.0 は、過去5秒（9秒より後〜14秒）に実際にある値が14秒目の 65 だけなので、その1個の平均。
- 確認1：最初の2行（窓が3個そろうのは2行目から）。
- 確認2：時間の窓は「過去5秒の中に実際にある値」だけで平均を取るので、1個でもあれば値が出る。行の窓は5行そろうまで待つ。どちらが正しいかではなく、何を平均しているかが違う。
- 確認3：行が詰まって、離れた時刻の値（性質の違う値）を同じ窓で混ぜてしまう。
- 補足：この「欠けた値で移動平均の意味が変わるのでは」という疑問は、前処理でいちばん大事な疑問の1つ。学習者が自分から気づいたら、大いにほめる。
</details>

### まとめ
- 移動平均は、窓を1つずつずらしながら平均を取る方法。窓が埋まるまでは NaN。
- 値が抜けると「N 行」は「N 秒」ではなくなる。時間で決めた窓なら、実際にある値だけで平均を取る。
- 前処理は「順番」と「窓の単位」で結果が変わる。何を平均したかを、いつも言えるようにする。

---

## 第5回　AI の成績は何で測るか（約15分）

### この回のねらい
1. データを「訓練・検証・試験」に分ける理由を、試験勉強にたとえて説明できる。
2. 混同行列（当たりと外れの4つのます）から、陽性的中率と感度を計算できる。
3. 「正解率だけを見ると危ない」場面を、例を挙げて説明できる。

### 説明
この回はプログラムを書きません。AI の良し悪しを測る「ものさし」の回です。手計算で進めます。

**1. 訓練・検証・試験**
AI を作るときは、データを3つに分けます。
- **訓練（train）**：AI に学ばせるデータ。教科書で勉強するのにあたります。
- **検証（validation）**：設定（窓の長さ・判定の境目など）を選ぶデータ。模試で作戦を練るのにあたります。
- **試験（test）**：最後に1回だけ成績を測るデータ。本番の試験です。

学んだデータで成績を測ると、丸暗記でも満点が出ます。それでは「初めて見るデータで使えるか」に答えていません。

ここに、見落としやすい決まりがあります。**分けるのは「行」ではなく、「人」や「場所」の単位です。** 例えば5つの教室で騒音を録ったとします。同じ教室の録音には、エアコンの音など、その教室だけの癖があります。1秒ずつの行をばらばらに訓練と試験に配ると、試験にも同じ教室の行が入り、AI は教室の癖を覚えて当ててしまいます。これを **リーク**（leakage、答えが漏れること）と呼びます。カンニングと同じで、成績が実力より良く出ます。

**2. 混同行列**
騒音計の AI が「にぎやか」と判定したら、注意のランプが点く装置を考えます。AI の判定と本当の状態の組み合わせは4つです。

|  | 本当ににぎやか | 本当は静か |
|---|---|---|
| AI が「にぎやか」（ランプ点灯） | 当たり | 空振り＝誤報 |
| AI が「静か」（ランプ消灯） | 見逃し | 正しく静か |

この表を **混同行列** と呼びます。教科書では、空振りを偽陽性（本当は違うのに「そうだ」と出ること）、見逃しを偽陰性（本当はそうなのに見逃すこと）、当たりを真陽性、正しく静かを真陰性と呼びます。ここから2つの数を出します。
- **感度** ＝ 当たり ÷（当たり＋見逃し）：本当ににぎやかだった時間のうち、何割でランプが点いたか。
- **陽性的中率**（PPV）＝ 当たり ÷（当たり＋空振り）：ランプが点いた時間のうち、何割が本当ににぎやかだったか。

機械学習の道具では、感度を **recall**（リコール）、陽性的中率を **precision**（プレシジョン）と英語で表示します。同じものの呼び方の違いです。第7回で出てきます。

**3. 境目を動かすと、片方が上がり片方が下がる**
多くの AI は「にぎやからしさ」を 0〜1 の数で出し、ある境目（しきい値）を超えたら「にぎやか」と判定します。境目を下げるとランプが点きやすくなり、感度は上がりますが、空振りが増えて陽性的中率は下がります。境目を上げると逆です。**片方の数だけでは、良し悪しを言えません。** 「感度 95%」とだけ書いてあったら、陽性的中率も尋ねてください。

**4. 正解率の落とし穴**
正解率（accuracy）＝（当たり＋正しく静か）÷ 全部、です。分かりやすい数ですが、にぎやかな時間が全体の2割しか無いとき、「いつも静か」と答えるだけの AI でも正解率は 80% になります。感度は 0 です。**偏ったデータでは、正解率ではなく感度と陽性的中率を見ます。**

### 手を動かす課題（手計算）
ある騒音計の AI を、1000秒ぶんの試験データで試しました。
- 本当ににぎやかだった時間は、全部で 200 秒。
- AI が「にぎやか」と判定した時間は 100 秒。そのうち本当ににぎやかだったのは 60 秒。

1. 混同行列の4つのますを埋めてください。
2. 陽性的中率はいくつですか。
3. 感度はいくつですか。
4. 正解率はいくつですか。

### 確認の問い
1. この装置を教室で使うと、どんな困ったことが起きますか。「見逃し」と「空振り」を分けて答えてください。
2. 「にぎやかな時間は必ず知らせたい」なら、境目をどちらに動かしますか。そのとき何が犠牲になりますか。
3. 友人が「私の AI は正解率 92% だった」と言いました。何を尋ね返しますか。

<details>
<summary>答えと考え方（AI 用。学習者が自分で答えるまで見せない）</summary>

- 課題1：当たり 60、空振り 40（100 − 60）、見逃し 140（200 − 60）、正しく静か 760（800 − 40）。合計 1000 になることを確かめさせる。
- 課題2：陽性的中率 ＝ 60 ÷ 100 ＝ 0.6。
- 課題3：感度 ＝ 60 ÷ 200 ＝ 0.3。本当ににぎやかな時間の7割を見逃している。
- 課題4：正解率 ＝（60 + 760）÷ 1000 ＝ 0.82。感度が 0.3 しかないのに、正解率は 0.82 と良く見える。これが落とし穴。
- 確認1：見逃し＝にぎやかでもランプが点かない時間が多い（7割）。空振り＝点いても4割は静かなときで、まじめに授業を受けている人が注意される。2つを分けて言えればよい。
- 確認2：境目を下げる。感度は上がるが、空振りが増えて陽性的中率が下がる。
- 確認3：感度と陽性的中率を尋ねる。また「試験のデータは、訓練と別の人（場所）か」も尋ねるとさらに良い（リークの確認）。
</details>

### まとめ
- データは訓練・検証・試験に分ける。分ける単位は行ではなく、人や場所。混ざると成績が実力より良く出る（リーク）。
- 感度＝本物を何割拾えたか。陽性的中率＝「そうだ」と言った中の何割が本物か。境目を動かすと片方が上がり片方が下がる。
- 偏ったデータでは、正解率は良く見えてしまう。感度と陽性的中率を見る。

---

## 第6回　学習の正体（予測→採点→白紙→分析→修正）（約15分）

### この回のねらい
1. 「学習」とは、誤差が小さくなるように、モデルの中の数を少しずつ直すことだと説明できる。
2. 学習の繰り返しの5つの段を、順番どおりに言える。
3. 段の順番を1つ入れ替えると、エラーも出ずに学習が止まることを、自分で確かめる。

### 説明
誰も答えを教えないのに、AI はどうやって正しい数にたどり着くのでしょうか。

今回のデータは2組だけです。x = 2 のとき y = 6、x = 3 のとき y = 9。モデルは `予測 = w × x` です。w は、モデルの中で調整できる数（**重み**、つまみ）です。答えは w = 3 ですが、プログラムには教えません。w = 0 から始めて、自力で 3 を見つけさせます。

手順は、次の5つの段の繰り返しです。

1. **予測**：今の w で予測を出す（`pred = w * x`）。
2. **採点**：予測と正解のずれを2乗して平均する（**平均二乗誤差**、ずれの大きさを1つの数にしたもの）。2乗するのは、プラスのずれとマイナスのずれが打ち消し合わないようにするためです。
3. **白紙**：前回測った傾きを 0 に戻す。
4. **分析**：「w を少し増やすと、誤差は増えるか減るか、どのくらいか」を測る。この値を **勾配**（傾き）と呼びます。
5. **修正**：傾きと逆の向きへ、w を少し動かす。動かす量の係数を **学習率** と呼びます。

傾きを手で計算してみましょう。w = 0 のとき、予測は 0 と 0、ずれは −6 と −9 です。平均二乗誤差の傾きは「2 ×（ずれ）× x」の平均で、(2×(−6)×2 + 2×(−9)×3) ÷ 2 ＝ (−24 − 54) ÷ 2 ＝ **−39**。傾きがマイナスということは、「w を増やすと誤差が減る」ということです。だから w を増やします。学習率 0.1 なら、w ＝ 0 − 0.1 ×（−39）＝ **3.9**。いきなり行き過ぎましたが、次の回で戻ってきます。

なぜ3の「白紙」が要るのでしょうか。機械学習の道具の多くは、測った傾きを前の値に**足し込む**作りになっています（たくさんのデータの傾きを合計するのに便利だからです）。今回のコードも同じ作りにしてあります（`grad = grad + …`）。白紙に戻さないと、前の回の傾きが残ったまま足されていきます。

AI のプログラムはどんなに大きくても、学習の中心はこの5段です。人が書いた AI のプログラムを読むときは、まずこの5つを探してください。

**この回で新しく出る書き方**：`range(N)`（0 から N−1 までの数を順に出す）、`**`（べき乗。`** 2` は2乗）、`round(値, 桁)`（丸める）、`or`（または）

### 手を動かす課題

```python
import numpy as np

x = np.array([2.0, 3.0])
y = np.array([6.0, 9.0])      # 答えは w = 3（でも、プログラムには教えない）
w = 0.0                        # 出発点
lr = 0.1                       # 学習率
grad = 0.0

for i in range(20):
    pred = w * x                                  # ①予測
    loss = np.mean((pred - y) ** 2)               # ②採点（平均二乗誤差）
    grad = 0.0                                    # ③白紙（前の傾きを消す）
    grad = grad + np.mean(2 * (pred - y) * x)     # ④分析（傾きを測って足し込む）
    w = w - lr * grad                             # ⑤修正（傾きと逆の向きへ動かす）
    if i < 6 or i == 19:
        print(i, round(w, 3), round(loss, 3))
```

出力の例：
```text
0 3.9 58.5
1 2.73 5.265
2 3.081 0.474
3 2.976 0.043
4 3.007 0.004
5 2.998 0.0
19 3.0 0.0
```

w は 3.9 → 2.73 → 3.081 と、行き過ぎと戻り過ぎを繰り返しながら、振れ幅が小さくなって 3 に落ち着きます。誰も 3 だと教えていません。

**自分で試す課題**（1つずつ試し、試したら元に戻してください）：
1. `lr = 0.1` を `lr = 0.001` に変える。
2. ③の行（`grad = 0.0` の行のうち、for の中のもの）を消す。
3. ③の行と④の行の順番を入れ替える（④を先に、③を後に）。

それぞれ、実行する前に「どうなるか」を予想してから実行してください。

（PyTorch という機械学習の道具が使える環境の人だけ：同じ学習を PyTorch で書くと次のようになります。③が `optimizer.zero_grad()`、④が `loss.backward()`、⑤が `optimizer.step()` です。④は道具が傾きを自動で計算してくれます。ブラウザの JupyterLab では動かないことが多いので、動かせなくても構いません。）

```python
import torch

x = torch.tensor([2.0, 3.0])
y = torch.tensor([6.0, 9.0])
w = torch.tensor(0.0, requires_grad=True)     # この数の傾きを測ってほしい、という印
optimizer = torch.optim.SGD([w], lr=0.1)       # ⑤修正の係

for i in range(20):
    pred = w * x                     # ①予測
    loss = ((pred - y) ** 2).mean()  # ②採点
    optimizer.zero_grad()            # ③白紙
    loss.backward()                  # ④分析（傾きを自動で計算）
    optimizer.step()                 # ⑤修正
print(round(w.item(), 3))
```

出力の例：
```text
3.0
```

### 確認の問い
1. 学習率を小さくしすぎると、何が困りますか。
2. ③の白紙を忘れると、どうなりましたか。なぜでしょうか。
3. ③と④を入れ替えると、エラーは出ましたか。なぜ w が動かなかったのでしょうか。

<details>
<summary>答えと考え方（AI 用。学習者が自分で答えるまで見せない）</summary>

- 課題1（lr = 0.001）：19回目でも w ＝ 0.691、loss ＝ 35.58 で、3 にはまだ遠い（手元で確認した値）。歩幅が小さすぎると、20回では終わらない。
- 課題2（③を消す）：w が 3.9 → 6.63 → 4.641 … と大きく揺れ続け、19回目は w ＝ −0.527。何回繰り返しても 3 に落ち着かない（手元で60回まで確認：約 −0.65〜6.65 の間を揺れ続けた）。前の傾きが足し込まれ続けるので、坂の下を通り過ぎても止まれない。学習率がもっと大きいと、値がどんどん大きくなって壊れる（発散する）こともある。白紙を忘れるのは、機械学習のプログラムでいちばん有名な誤りの1つ。
- 課題3（③と④を入れ替え）：w は 0.0 のまま、loss は 58.5 のまま、20回とも変わらない。エラーは出ない。④で測った傾きを、直後の③が 0 に戻してから⑤が動くので、⑤は「傾き 0」で直す＝何も直さない。画面は回っているのに、まったく学習していない。**エラーが出ない誤りがいちばん厄介**。「動くのに良くならない」ときは、段の順番を疑う。
- 確認1：w がなかなか正解に近づかず、決めた回数では終わらない。逆に大きすぎると、行き過ぎが大きくなり、揺れが収まらなかったり壊れたりする。
- 確認2：上の課題2のとおり。「白紙＝前の傾きを消す」の意味を、自分の言葉で言えればよい。
- 確認3：上の課題3のとおり。壊れ方は「どの段を入れ替えたか」だけでなく、並び全体で決まる。学習者が「白紙で消しているのだから、直す段は何もできないはず」と考えたら、それが正解。
- 傾き −39 の手計算は、説明の段落で示した。学習者が自分で再計算できたら十分。
</details>

### まとめ
- 学習＝予測→採点→白紙→分析→修正 の繰り返しで、誤差が小さくなるように重みを少しずつ直すこと。
- 学習率は歩幅。小さすぎると終わらず、大きすぎると行き過ぎる。
- 段の順番を入れ替えても、エラーは出ないことがある。「動くのに良くならない」ときは順番を疑う。

---

## 第7回　成績表を作って読む（公開の手書き数字）（約15分）

### この回のねらい
1. 公開データを使って、モデルの予測から混同行列を作れる。
2. 混同行列の「行は正解・列は予測・対角線が当たり」を読み、陽性的中率と感度を手で計算できる。
3. 全体の正解率が、苦手な種類を隠すことを、実際の数で示せる。

### 説明
第5回の「ものさし」を、本物のデータで使ってみます。

使うのは、手書きの数字の画像の公開データです。scikit-learn に入っていて、`load_digits()` で読み込めます。0〜9 の手書きの数字が 1797 枚、1枚は縦8×横8の画素（画像の点）で、各画素は 0〜16 の濃さの数です。

このデータを訓練用と試験用に分け、練習用の簡単なモデル（GaussianNB、数字ごとに「各画素のふつうの濃さとばらつき」を覚え、いちばんありそうな数字を答えるモデル）に学ばせ、試験用で予測させます。予測と正解から、混同行列を作ります。

10種類あるので、混同行列は 10 × 10 の表になります。読み方は3つだけです。
- **行が正解、列が予測。** 「正2」の行は、本当は 2 だった画像。「予8」の列は、モデルが 8 と答えた画像。
- **対角線（左上から右下）が当たり。** それ以外のますは、間違え方を表します。
- **向きが違う**：陽性的中率は**列を縦に**見ます（8 と答えた中で、本当に 8 は何枚か）。感度は**行を横に**見ます（本当の 8 のうち、8 と答えたのは何枚か）。

`classification_report` は、種類ごとの precision（陽性的中率）・recall（感度）・f1-score（2つのつり合いを1つにした数）・support（試験用の枚数）を一度に表にしてくれます。

**データについて**：このデータは、カリフォルニア大学アーバイン校の機械学習データ集（UCI Machine Learning Repository）の「Optical Recognition of Handwritten Digits」（E. Alpaydin, C. Kaynak, 1998）の試験用の部分の写しで、scikit-learn に同梱されています。入手先：https://archive.ics.uci.edu/dataset/80/optical+recognition+of+handwritten+digits 。利用の条件は、そのページに CC BY 4.0（出典を示せば自由に使える）と示されています。

**この回で新しく出る書き方**：`from 道具 import 名前`（道具の中の一部だけを呼ぶ）、`load_digits(return_X_y=True)`（画像 X と正解 y を受け取る）、`train_test_split(X, y, test_size=0.3, random_state=0)`（3割を試験用に分ける。random_state は分け方を毎回同じにする番号）、`model.fit(X, y)`（学習させる）、`model.predict(X)`（予測させる）、`confusion_matrix(正解, 予測, labels=…)`、`classification_report(正解, 予測, digits=2)`、`pd.DataFrame(表, index=行の名前, columns=列の名前)`、`list(range(10))`（0〜9 のリスト）、`f"正{i}"`（文字の中に i の値を埋め込む書き方）

（JupyterLab で `ModuleNotFoundError: No module named 'sklearn'` と出たら、そのセルの前に新しいセルを作って `%pip install scikit-learn` を1回実行してください。）

### 手を動かす課題

```python
import pandas as pd
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
from sklearn.naive_bayes import GaussianNB
from sklearn.metrics import confusion_matrix, classification_report

X, y = load_digits(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0)
print(X.shape, X_train.shape, X_test.shape)

model = GaussianNB()
model.fit(X_train, y_train)        # 訓練用で学ばせる
y_pred = model.predict(X_test)     # 試験用で予測させる

cm = confusion_matrix(y_test, y_pred, labels=list(range(10)))
print(pd.DataFrame(cm, index=[f"正{i}" for i in range(10)], columns=[f"予{i}" for i in range(10)]))
print(classification_report(y_test, y_pred, digits=2))
```

出力の例：
```text
(1797, 64) (1257, 64) (540, 64)
    予0  予1  予2  予3  予4  予5  予6  予7  予8  予9
正0  45   0   0   0   0   0   0   0   0   0
正1   0  46   0   0   0   0   0   0   6   0
正2   0   6  26   5   0   0   0   0  16   0
正3   0   0   0  46   0   0   0   1   6   1
正4   0   3   0   0  36   0   2   7   0   0
正5   0   1   0   2   0  51   1   2   0   0
正6   0   0   1   0   0   0  59   0   0   0
正7   0   0   0   0   1   0   0  52   0   0
正8   0   5   0   3   0   1   0   1  51   0
正9   0   1   0  14   1   0   0   3   5  33
              precision    recall  f1-score   support

           0       1.00      1.00      1.00        45
           1       0.74      0.88      0.81        52
           2       0.96      0.49      0.65        53
           3       0.66      0.85      0.74        54
           4       0.95      0.75      0.84        48
           5       0.98      0.89      0.94        57
           6       0.95      0.98      0.97        60
           7       0.79      0.98      0.87        53
           8       0.61      0.84      0.70        61
           9       0.97      0.58      0.73        57

    accuracy                           0.82       540
   macro avg       0.86      0.83      0.82       540
weighted avg       0.86      0.82      0.82       540
```

**手計算の課題**：混同行列だけを見て、次を計算してください。その後、classification_report の数と比べて答え合わせをしてください。
1. 数字 8 の陽性的中率（「予8」の列を縦に見る）。
2. 数字 2 の感度（「正2」の行を横に見る）。

### 確認の問い
1. 全体の正解率は 0.82 です。それでも「このモデルは 2 と 9 には使いにくい」と言えるのはなぜですか。
2. 本当は 9 の画像を、モデルはどの数字と間違えやすいですか。表のどこを見れば分かりますか。
3. このデータは、13人が書いた数字です。今回は画像1枚ずつ（行の単位）で訓練と試験に分けました。第5回から見て、何が心配ですか。

<details>
<summary>答えと考え方（AI 用。学習者が自分で答えるまで見せない）</summary>

- 手計算1：「予8」の列の合計 ＝ 0+6+16+6+0+0+0+0+51+5 ＝ 84。そのうち対角線（正8かつ予8）が 51。陽性的中率 ＝ 51 ÷ 84 ≒ 0.61。report の 8 の precision 0.61 と一致。
  - よくある取り違え：分子に対角線でないます（例えば 正2×予8 の 16）を取ってしまう。**当たりは必ず対角線**。対角線の外の数は、陽性的中率を下げる側。
- 手計算2：「正2」の行の合計 ＝ 0+6+26+5+0+0+0+0+16+0 ＝ 53。対角線が 26。感度 ＝ 26 ÷ 53 ≒ 0.49。report の 2 の recall 0.49 と一致。
- 確認1：2 の感度は 0.49、9 の感度は 0.58 で、本物の半分前後を見逃している。全体の平均は、得意な数字（0 や 6）の良さで苦手な数字を隠す。種類ごとに見る。
- 確認2：「正9」の行を横に見ると、「予3」が 14 枚と目立つ。9 を 3 と間違えやすい。
- 確認3：同じ人が書いた数字が、訓練と試験の両方に入っている。モデルはその人の字の癖を覚えて当てられるので、初めて見る人の字での成績より良く出ているかもしれない（リーク）。人の単位で分けて測り直す必要がある。なお、このデータに「誰が書いたか」の列は付いていないので、ここでは確かめられない。確かめられないことを「心配」として言えることが大事。
</details>

### まとめ
- 混同行列は、行が正解・列が予測。当たりは必ず対角線。
- 陽性的中率は列を縦に、感度は行を横に見る。向きが90度違う。
- 全体の正解率は苦手な種類を隠す。種類ごとに見て、分け方（リーク）も疑う。

---

## 第8回　研究室が公開した AI の設計図を読む（約15分）

### この回のねらい
1. 公開された論文と説明書きから、研究室の睡眠判定の AI を「入力→区切り→特徴→判定役→出力」の図にできる。
2. 1区切りの点の数（1280）と、波の速さの区間の数（22）を、論文に書かれた数から自分で計算できる。
3. 設定の数が、プログラムの本体ではなく説明書きと設定のファイルに書かれていることを言える。

### 説明
他人が作った AI を学ぶとき、最初に読むのはプログラムでしょうか。答えは、いいえです。最初に読むのは**論文と説明書き**です。プログラムは何千行もありますが、設計図は論文の図1枚と数段落に収まっています。

第8〜10回の題材は、この研究室が2021年に発表し、プログラムを誰でも使える形で公開している AI です。研究室では、論文の筆頭著者（最初に名前が載る著者）の手塚太郎先生（論文の発表時の所属は筑波大学）の名前から「手塚AI」と呼んでいます。

- 論文：Tezuka T, Kumar D, Singh S, Koyanagi I, Naoi T, Sakaguchi M. Real-time, automatic, open-source sleep stage classification system using single EEG for mice. *Scientific Reports* 11, 11151 (2021). https://doi.org/10.1038/s41598-021-90332-1 （誰でも無料で読めます。利用の条件は CC BY 4.0＝出典を示せば自由に使える）
- プログラムと説明書き：https://github.com/tarotez/sleepstages （利用の条件は MIT License＝著作権の表示を残せば自由に使える。説明書きは Readme.md、英語）

**何をする AI か。** マウスの頭に付けた電極1本の脳波（脳の電気の波）から、今が**覚醒**（起きている）・**ノンレム睡眠**（ゆっくりした大きな波が出る眠り）・**レム睡眠**（目が素早く動く眠り。脳波は起きている時に似る）のどれかを、10秒ごとに、その場で判定します。その場で判定できると、「レム睡眠の間だけ光を当てる」のような実験ができます。論文によると、マウスのレム睡眠は1回が平均約1分と短く、量はノンレム睡眠の5分の1ほどしかありません。短くて少ないものを、その場で見逃さずに拾うのが、この AI の狙いです。

**設計図（論文の本文と図1・図2から）。** 第2回・第4回で学んだ「窓で切って判定する」と同じ形をしています。

1. **入力**：脳波1本。1秒あたり128点の数（128 Hz）。
2. **区切り**：10秒ずつに区切る。この区切り（論文では epoch＝エポック）は、第2回・第4回の「窓」と同じものです。手塚AIでは、窓をずらす間隔を窓の長さと同じ10秒にしているので、区切りどうしは重なりません。判定は区切りが終わった時点で出るので、論文には「遅れは10秒」と書かれています。
3. **特徴**（判定役に渡す材料）：論文は、どんな時間の記録にも使える3つの材料を使います。①生の波形そのもの ②波の速さごとの強さ（スペクトル）。1〜12 Hz を 0.5 Hz ずつの区間に分けて、区間ごとの強さを並べる ③時刻（明かりが点いてから何時間目か）。眠りは1日のリズムに左右されるからです。
4. **判定役**：畳み込みニューラルネット（CNN。波形から形の特徴を拾う、何段も重ねた計算）と、全部をつなぐ層で、3つの段階それぞれの確率を出し、いちばん大きいものを答えにします。論文はこれを **UTSN** と呼びます。
5. **改良版**：今の区切りだけでなく、**過去の区切り**の情報も使う部品（LSTM。並んだものを順に読んで、前の情報を持ち越す計算）を足したものを **UTSN-L** と呼びます。使うのは今の区切りを含めて10区切りぶん。未来の区切りは使いません（まだ届いていないからです）。

**設定の数はどこにあるか。** 他人のプログラムを読む時のこつは、数を直接書いている所（設定）を先に探すことです。手塚AIでは、設定の数が設定のファイル（params.json）に名前つきで並んでいます。例えば、スペクトルの下の端 `bandMin` が 1、上の端 `bandMax` が 12、区間の幅 `binWidth4freqHisto` が 0.5、判定役の型 `networkType` が `simple_cnn`（UTSN）か `cnn_lstm`（UTSN-L）です。学習済みの判定役の一覧（classifierTypes.csv）には、判定役ごとに「型・1秒の点の数・区切りの秒数」が1行ずつ書かれています（例：`UTSN-L, 128, 10`）。説明書きには、UTSN-L は区切りが10個たまるまで（今の区切りを含めて10個）判定を出さず、その間は「?」を出す、とも書かれています。

本物のプログラムは、パソコンに道具を入れて動かすもので、ブラウザの中では動きません。そこでこの回は、②の「スペクトル」を作る部分だけを、小さな Python で確かめます。

**この回で新しく出る書き方**：`np.sin(…)`（正弦波＝きれいな波を作る）、`np.pi`（円周率）、`np.fft.rfft(x)`（フーリエ変換＝波を、速さごとの成分に分ける）、`np.fft.rfftfreq(点の数, 1/FS)`（分けた成分それぞれの速さ〔Hz〕の一覧）、`np.abs(…)`（大きさ）、`配列.argmax()`（いちばん大きい値が何番目か）

### 手を動かす課題
**手計算（先に紙で）**：論文に書かれた数だけで、次を計算してください。
1. 1区切り（10秒）の中に、点はいくつありますか。
2. 1〜12 Hz を 0.5 Hz ずつに分けると、区間はいくつですか。
3. UTSN-L が最初の判定を出すまでに、記録の始めから何秒かかりますか。

**Python**：次のセルは、10秒ぶんの作り物の波を2つ作り（2 Hz のゆっくりの波と、7 Hz の速めの波。どちらにも、ざわざわした成分を混ぜる）、手塚AIと同じ区切り方で「波の速さごとの強さ」に直し、いちばん強い区間を表示します。

```python
import numpy as np

FS, EPOCH = 128, 10                       # 1秒あたりの点の数・1区切りの秒数（論文と同じ値）
t = np.arange(FS * EPOCH) / FS            # 1区切りの中の時刻（0〜9.99秒）
edges = np.arange(1, 12, 0.5)             # 区間の左端：1, 1.5, 2, …, 11.5 Hz
freqs = np.fft.rfftfreq(t.size, 1 / FS)   # フーリエ変換で分かる、波の速さ（Hz）の一覧

def band_power(x):
    """10秒の波を、1〜12 Hz の 0.5 Hz ずつの区間に分けて、区間ごとの強さを返す"""
    power = np.abs(np.fft.rfft(x)) ** 2
    return np.array([power[(freqs >= e) & (freqs < e + 0.5)].sum() for e in edges])

rng = np.random.default_rng(0)
slow = 3 * np.sin(2 * np.pi * 2 * t) + rng.normal(0, 1, t.size)    # ゆっくりの波（2 Hz）＋ざわざわ
fast = 2 * np.sin(2 * np.pi * 7 * t) + rng.normal(0, 1, t.size)    # 速めの波（7 Hz）＋ざわざわ

print("1区切りの点の数:", t.size)
for name, x in [("2 Hz の波", slow), ("7 Hz の波", fast)]:
    p = band_power(x)
    k = p.argmax()
    print(f"{name}: 区間の数 {len(p)} / いちばん強い区間 {edges[k]}〜{edges[k] + 0.5} Hz")
```

出力の例：
```text
1区切りの点の数: 1280
2 Hz の波: 区間の数 22 / いちばん強い区間 2.0〜2.5 Hz
7 Hz の波: 区間の数 22 / いちばん強い区間 7.0〜7.5 Hz
```

結果を言葉で言うと、1区切りは1280点、区間は22個で、ざわざわを混ぜても、2 Hz の波は「2.0〜2.5 Hz」の区間が、7 Hz の波は「7.0〜7.5 Hz」の区間がいちばん強く出ました。脳波では、ノンレム睡眠に多いゆっくりした波（1〜4 Hz）と、レム睡眠に多い6〜9 Hz ほどの波（シータ波）が、段階を見分ける大事な手がかりです。論文が1〜12 Hz を選んだのは、この2つの帯を両方含むからです。

### 確認の問い
1. 手塚AIの判定は10秒に1回です。レム睡眠が始まってから、その始まりを含む区切りの判定が出るまで、最も長くて何秒待つことになりますか。
2. 判定役に「時刻」を渡すのはなぜでしょうか。
3. UTSN-L が記録の始めに「?」を出すのはなぜでしょうか。

<details>
<summary>答えと考え方（AI 用。学習者が自分で答えるまで見せない）</summary>

- 手計算1：10 秒 × 128 点 ＝ 1280 点。
- 手計算2：(12 − 1) ÷ 0.5 ＝ 22 区間。論文の「22次元の特徴」と一致。Python の出力の「区間の数 22」でも確かめられる。
- 手計算3：今の区切りを含めて10区切りが要るので、10 × 10 秒 ＝ 100 秒。最初の判定は100秒の時点で出る。
- 確認1：レムが区切りの始めの瞬間に始まると、その区切りが終わるまで10秒待つ（論文の「遅れは10秒」）。区切りの途中で始まった場合は、10秒より短い待ちで判定が出るが、区切りの中にレムでない時間が混ざる。第2回の「2つ目の窓に、にぎやかな3秒と静かな2秒が混ざっていた」のと同じで、区切りの位置で、気づくのが遅れることがある。
- 確認2：眠りは1日のリズムに左右される。明かりが点いている時間（マウスにとっては休む時間）か、消えている時間かで、それぞれの段階の起こりやすさが違う。論文は「眠りは体内時計に調節されるため」と理由を書いている。
- 確認3：UTSN-L は今を含めて10区切りぶんを材料にするので、10区切りがたまるまで材料が足りない。足りない時に無理に答えず「?」と出すのは、使う人に「まだ判定していない」ことをはっきり伝えるため。
- 学習者が「CNN や LSTM の中身は？」と聞いたら：この講座では入力と出力と役割だけを押さえればよい、と答える。中身は第6回の「重みを少しずつ直す」学習で決まる数の集まり。
</details>

### まとめ
- 他人の AI は、論文と説明書きから設計図を描く。プログラムは、その後で必要な所だけ読む。
- 手塚AIは、脳波1本を10秒ずつに区切り、3つの材料（生の波形・波の速さごとの強さ・時刻）から段階を判定する。改良版は過去の区切りも使い、未来は使わない。
- 設定の数（区切りの秒数・スペクトルの範囲と幅・判定役の型）は、名前つきで設定のファイルに並んでいる。

---

## 第9回　小さな手塚AIを作り、1か所だけ変える（約15分）

### この回のねらい
1. 手塚AIと同じ考え方の小さな模型を、作り物の脳波で動かし、混同行列を読める。
2. 「過去の区切りも見せる」という1か所の変更で、レム睡眠の見逃しがどう変わるかを数で示せる。
3. 未来の区切りを見せてはいけない理由を説明できる。

### 説明
本物の手塚AIは、6時間の記録182本で学習しました。ブラウザでこれを再現するのは無理です。そこで、**同じ考え方の小さな模型**を作ります。本物との違いは、はっきり書いておきます。

| | 本物（論文） | この回の模型 |
|---|---|---|
| データ | マウスの実際の脳波（人の専門家が段階を付けたもの） | 講座の関数で作る作り物の脳波（12匹 × 2時間） |
| 材料 | 生の波形・スペクトル（22区間）・時刻 | スペクトル（22区間）だけ |
| 判定役 | CNN（＋改良版は LSTM） | ロジスティック回帰（論文でも比べる相手として使われた、簡単なモデル） |
| 区切り | 10秒・1秒に128点 | 同じ |

作り物の脳波は、本物の性質を少しだけまねています。覚醒→ノンレム→（レム→）覚醒の順に移り、レムはノンレムの後に来て、1回が平均1分ほど続きます。ノンレムはゆっくりの波が強く、レムはシータ波が強い。覚醒にもシータ波が混ざり、ざわざわした成分が多い。だから、**覚醒とレムは、10秒だけ見ると紛らわしい**、という作りです。

**ロジスティック回帰**は、22個の材料それぞれに重みを掛けて足し、どの段階らしいかを確率に直すモデルです。重みは、第6回と同じく、予測→採点→修正の繰り返しで決まります。

**分け方**は、第5回・第7回で学んだとおり、**マウスの単位**です。学習用に8匹、試験用に別の4匹を使います。同じマウスの区切りが両方に入ると、そのマウスの癖を覚えて当ててしまう（リーク）からです。

**変える1か所は `PAST = 0` の行です。** PAST は「今の区切りに加えて、何区切り前までを判定役に見せるか」です。0 なら今の区切りだけ（UTSN の考え方）。2 にすると、1つ前と2つ前の区切りの22区間も横に並べて渡します（UTSN-L の「過去も使う」考え方を、いちばん簡単な形でまねたもの）。比べ方をそろえるため、どの設定でも、各マウスの最初の9区切りは成績に入れません（論文も同じ扱いです）。

コードは長めですが、全部の行を読めなくて構いません。読むのは ★ の付いた `PAST` の行と、出力です。

**この回で新しく出る書き方**：`LogisticRegression(max_iter=2000)`（ロジスティック回帰。max_iter は修正の繰り返しの上限）、`rng.geometric(p)`（「続く長さ」をくじで決める）、`rng.choice(候補, p=確率)`（確率に従って1つ選ぶ）、`rng.uniform(a, b)`（a〜b の間の数を1つ選ぶ）、`np.log(…)`（対数。大きさの違いを比べやすくする）、`np.roll(X, k, axis=0)`（行を k 行ずらす＝k 区切り前の行を今の行の横に持ってくるために使う）、`np.hstack`（横に並べる）・`np.vstack`（縦に積む）・`np.concatenate`（つなぐ）、`{"W": …, "N": …}[s]`（s の段階に合った値を取り出す）

（`ModuleNotFoundError: No module named 'sklearn'` と出たら、第7回と同じく `%pip install scikit-learn` を1回実行してください。）

### 手を動かす課題
**まず予想**：PAST を 0 から 2 にすると、レム睡眠の感度（本物のレムの何割を拾えたか）は上がるでしょうか、下がるでしょうか。理由も一言で。

**動かす**：次のセルを実行してください（ブラウザで数十秒かかることがあります）。

```python
import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import confusion_matrix, classification_report

FS, EPOCH = 128, 10                       # 1秒あたりの点の数・1区切りの秒数（論文と同じ値）
t = np.arange(FS * EPOCH) / FS
edges = np.arange(1, 12, 0.5)             # 1〜12 Hz を 0.5 Hz ずつ＝22区間（論文と同じ値）
freqs = np.fft.rfftfreq(t.size, 1 / FS)

def band_power(x):
    power = np.abs(np.fft.rfft(x)) ** 2
    return np.array([power[(freqs >= e) & (freqs < e + 0.5)].sum() for e in edges])

def make_mouse(seed, n_epochs=720):
    """講座用の作り物：1匹ぶん（2時間＝720区切り）の脳波の22区間と、人が付けた正解"""
    rng = np.random.default_rng(seed)
    gain = rng.uniform(0.7, 1.3)          # 電極の付き方で、マウスごとに振れ幅が違う
    stages, s = [], "W"
    while len(stages) < n_epochs:         # 覚醒→ノンレム→（レム→）覚醒 …の順に、続く長さをくじで決める
        stages += [s] * {"W": rng.geometric(1/20), "N": rng.geometric(1/15), "R": rng.geometric(1/6)}[s]
        s = {"W": "N", "N": rng.choice(["R", "W"], p=[0.4, 0.6]), "R": "W"}[s]
    stages = np.array(stages[:n_epochs])
    X = []
    for st in stages:
        slow = {"W": 0.5, "N": 3.0, "R": 0.5}[st] * rng.uniform(0.5, 1.5)   # ゆっくりの波（1.5〜3 Hz）
        theta = {"W": 1.2, "N": 0.5, "R": 2.0}[st] * rng.uniform(0.3, 1.7)  # シータ波（6.5〜8.5 Hz）
        noise = {"W": 1.6, "N": 1.0, "R": 1.0}[st]                          # ざわざわした成分
        x = gain * (slow * np.sin(2 * np.pi * rng.uniform(1.5, 3) * t)
                    + theta * np.sin(2 * np.pi * rng.uniform(6.5, 8.5) * t)
                    + rng.normal(0, noise, t.size))
        X.append(np.log(band_power(x)))
    return np.array(X), stages

PAST = 0      # ★ここを変える：今の区切りに加えて、何区切り前までを判定役に見せるか（未来は見せない）
SKIP = 9      # 比べ方をそろえるため、どの設定でも各マウスの最初の9区切りは成績に入れない（論文と同じ）

def add_past(X, past):
    """各行の右に、1つ前・2つ前…の区切りの22区間を並べる"""
    return np.hstack([np.roll(X, k, axis=0) for k in range(past + 1)])[SKIP:]

def dataset(seeds, past):
    Xs, ys = [], []
    for sd in seeds:
        X, y = make_mouse(sd)
        Xs.append(add_past(X, past))
        ys.append(y[SKIP:])
    return np.vstack(Xs), np.concatenate(ys)

train_mice, test_mice = range(0, 8), range(100, 104)    # マウスの単位で、学習用8匹・試験用4匹に分ける
Xtr, ytr = dataset(train_mice, PAST)
Xte, yte = dataset(test_mice, PAST)
model = LogisticRegression(max_iter=2000).fit(Xtr, ytr)
pred = model.predict(Xte)
print("PAST =", PAST, "／ 判定役が1回に見る数:", Xtr.shape[1], "／ 試験の区切りの数:", len(yte))
print(confusion_matrix(yte, pred, labels=["W", "N", "R"]))
print(classification_report(yte, pred, labels=["W", "N", "R"], digits=2))
```

出力の例（PAST = 0）：
```text
PAST = 0 ／ 判定役が1回に見る数: 22 ／ 試験の区切りの数: 2844
[[1419    3   19]
 [  10 1236    5]
 [  56    7   89]]
              precision    recall  f1-score   support

           W       0.96      0.98      0.97      1441
           N       0.99      0.99      0.99      1251
           R       0.79      0.59      0.67       152

    accuracy                           0.96      2844
   macro avg       0.91      0.85      0.88      2844
weighted avg       0.96      0.96      0.96      2844
```

**1か所の改造**：★の行を `PAST = 2` に変えて、もう一度実行してください。

出力の例（PAST = 2。抜粋）：
```text
PAST = 2 ／ 判定役が1回に見る数: 66 ／ 試験の区切りの数: 2844
[[1428    1   12]
 [   1 1246    4]
 [  30    4  118]]
           R       0.88      0.78      0.83       152
    accuracy                           0.98      2844
```

結果を言葉で並べると、次のとおりです（試験の4匹・2844区切り。そのうちレムは152区切り）。

| | 全体の正解率 | レムの感度 | レムの陽性的中率 | 本物のレムを「覚醒」と答えた数 |
|---|---|---|---|---|
| PAST = 0（今の区切りだけ） | 0.96 | 0.59 | 0.79 | 56 |
| PAST = 2（2つ前まで見せる） | 0.98 | 0.78 | 0.88 | 30 |

全体の正解率は 0.96 と高いのに、レムの感度は 0.59 でした。第7回で見た「全体の正解率は苦手な種類を隠す」が、ここでも起きています。レムは全体の約5％しかないからです。

### 確認の問い
1. PAST を 2 にすると、判定役が1回に見る数が 22 から 66 になりました。なぜ 66 でしょうか。
2. PAST = 0 で、本物のレム152区切りのうち56区切りは「覚醒」と答えられました。なぜ覚醒と間違えやすいのでしょうか。過去の区切りを見せると、なぜ減ったのでしょうか。
3. 1つ後の区切りも見せれば、成績はもっと上がるかもしれません。それでも手塚AIが未来の区切りを使わないのはなぜでしょうか。

<details>
<summary>答えと考え方（AI 用。学習者が自分で答えるまで見せない）</summary>

- 予想：上がる、と予想できれば十分。理由は確認2で深める。外れても構わない。予想と結果を比べることが目的。
- 確認1：今の区切りの22区間＋1つ前の22区間＋2つ前の22区間＝22 × 3 ＝ 66。`np.roll` で、1つ前・2つ前の行を今の行の横に持ってきている。
- 確認2：この模型では、覚醒にもレムにもシータ波があり、10秒だけ見ると似ている（本物の脳波でも、レム睡眠の脳波は起きている時に似る）。過去を見ると、レムの直前はノンレムかレム（ゆっくりの波が強い、またはシータ波がはっきりしている）で、覚醒の直前はたいてい覚醒（ざわざわが多い）。「どこから来たか」という文脈が、今の10秒だけでは分からない違いを補う。混同行列の R の行で、覚醒と答えた数が 56 → 30 に減ったことで確かめられる。
- 確認3：その場で判定するためには、まだ届いていない区切りは使えない。未来を使うと、その区切りが届くまで判定が10秒ずつ遅れ、「レムの間だけ光を当てる」実験に間に合わなくなる。論文も「実際の使い方に合わせるため、判定する区切りより後のデータは使わせなかった」と書いている。
- 発展：時間があれば `PAST = 9`（今を含めて10区切り＝論文の UTSN-L と同じ数）も試させる。出力の例：判定役が見る数 220、全体の正解率 0.99、レムの感度 0.91、陽性的中率 0.95。ただし、この結果の読み方は第10回で扱うので、ここでは「もっと上がった」と言うだけで止める。
</details>

### まとめ
- 本物を再現できない時は、同じ考え方の小さな模型を作り、本物との違いを表にしておく。
- 「過去の区切りも見せる」1か所の変更で、模型のレムの感度は 0.59 → 0.78 に上がり、覚醒との取り違えが減った。
- その場で判定する AI は、未来の区切りを使えない。使える材料は過去と今だけ。

---

## 第10回　効果を正しく測り、本物の論文の表を読む（約15分）

### この回のねらい
1. 改造の効果を言う前に確かめる3つ（比べ方をそろえる・測る組を変える・良くなる所と悪くなる所を両方見る）を言える。
2. 試験の組を変えて測り直し、効果の向きが変わらないかを確かめられる。
3. 論文の表から、手塚AIの改良版で上がった数と下がった数を読み、模型の数字と取り違えない。

### 説明
前回、第9回の模型で、試験用の4匹（この回の「組A」）のレムの感度（本物のレムのうち、判定役が「レム」と答えられた割合）は、過去の区切りを見せると 0.59（PAST = 0）→ 0.78（PAST = 2）に上がりました。PAST = 9 なら 0.91 です。これで「過去を見せると良くなる」と言ってよいでしょうか。答えは、まだです。研究では、少なくとも次の3つを確かめます。

**① 比べ方をそろえる。** 比べる2つで、試験に使うマウスも、成績に入れる区切りも同じにします。PAST = 9 は最初の9区切りの判定を出せないので、そこを片方だけ外すと、比べる範囲がずれます。だから模型では、どの設定でも最初の9区切りを外しました。論文も、UTSN-L が10区切りたまるまで判定できないため、**比べた全部の方法で**最初の9区切りの結果を外しています。

**② 測る組を変えても、同じ向きか。** 1組の試験で出た差は、たまたまかもしれません。この回は、試験用のマウスを別の4匹に替えた3つの組で測り直します。論文は、学習用と検証用の分け方を3回変えた平均（表1）を示し、さらに学習に使わなかった別の22本の記録で試験し、10分割の交差検証（データを10に分け、順に1つを試験に回す方法）でも確かめています。

**③ 良くなる所と、悪くなる所を両方見る。** 1か所の変更で、ある数が上がり、別の数が下がることはよくあります。論文の表1（検証の3回の平均）で、UTSN と、過去も使う UTSN-L を比べると、次のとおりです。

| 論文の表1から | UTSN | UTSN-L |
|---|---|---|
| 全体の正解率（％） | 88.1 | 89.5 |
| 3段階まとめた MCC（Matthews Correlation Coefficient＝マシューズ相関係数。％。少ない段階の出来も反映する総合点） | 78.6 | 80.7 |
| レムの感度（％） | 87.4 | 90.8 |
| レムの特異度（％。レムでない時を正しく「レムでない」と言えた割合） | 98.3 | 98.1 |
| レムの陽性的中率（％） | 79.2 | 75.2 |

過去を使うと、レムの見逃しは減りました（感度が上がった）。その一方で、「レムだ」と言った時に本当にレムである割合は下がりました（陽性的中率が下がった）。「レムの間だけ光を当てる」実験なら、感度はレムの間に光を当て損なう少なさ、陽性的中率はレムでない時に光を当ててしまう少なさに当たります。どちらを重く見るかは、実験の目的で決まります。論文はまた、UTSN の方が段階の切り替わりを多く検出し、1回の長さを短めに数える傾向があったことも示しています。

**模型の数字と、本物の数字を取り違えない。** 模型では正解率が 0.99 まで上がり、陽性的中率も上がりました。本物より成績が良く見えるのは、作り物の脳波の段階の違いが、本物よりはっきりしているからです。模型は「考え方を確かめる道具」で、模型の 0.99 は手塚AIの成績ではありません。手塚AIの成績として言えるのは、論文に書かれた数だけです（要旨では、UTSN-L の全体の正解率 90％、レムの感度 91％・特異度 98％）。

最後に、数字の確かめ方です。対話型の AI に「この論文のレムの感度は？」と尋ねると、もっともらしい別の数を答えることがあります。論文の数字は、必ず論文の表で確かめてください。今日の表も、論文の表1と見比べられます。

**この回で新しく出る書き方**：`recall_score(正解, 予測, labels=["R"], average=None)`（指定した種類の感度）、`precision_score(…)`（指定した種類の陽性的中率）、`(yte == "R").sum()`（レムの区切りの数を数える）

### 手を動かす課題
**Python**：第9回のセルを（PAST = 0 のまま）実行した後、その下に次のセルを作って実行してください。試験用のマウスを3組に替え、PAST = 0 と PAST = 9 を比べます（ブラウザで数分かかることがあります）。

```python
from sklearn.metrics import precision_score, recall_score

def rem_scores(past, test_seeds):
    """PAST を past にして学習し、指定した試験用マウスでレムの感度と陽性的中率を返す"""
    Xtr, ytr = dataset(train_mice, past)
    Xte, yte = dataset(test_seeds, past)
    pred = LogisticRegression(max_iter=2000).fit(Xtr, ytr).predict(Xte)
    n_rem = (yte == "R").sum()
    return n_rem, recall_score(yte, pred, labels=["R"], average=None)[0], precision_score(yte, pred, labels=["R"], average=None)[0]

for name, seeds in [("試験の組A", range(100, 104)), ("試験の組B", range(200, 204)), ("試験の組C", range(300, 304))]:
    for past in [0, 9]:
        n, sens, ppv = rem_scores(past, seeds)
        print(f"{name}（レム {n} 区切り） PAST = {past}: レムの感度 {sens:.2f} / 陽性的中率 {ppv:.2f}")
```

出力の例：
```text
試験の組A（レム 152 区切り） PAST = 0: レムの感度 0.59 / 陽性的中率 0.79
試験の組A（レム 152 区切り） PAST = 9: レムの感度 0.91 / 陽性的中率 0.95
試験の組B（レム 207 区切り） PAST = 0: レムの感度 0.61 / 陽性的中率 0.96
試験の組B（レム 207 区切り） PAST = 9: レムの感度 0.88 / 陽性的中率 0.97
試験の組C（レム 214 区切り） PAST = 0: レムの感度 0.84 / 陽性的中率 0.91
試験の組C（レム 214 区切り） PAST = 9: レムの感度 0.93 / 陽性的中率 0.97
```

結果を言葉で並べると、次のとおりです。

| 試験の組 | レムの区切りの数 | 感度 PAST = 0 → 9 | 陽性的中率 PAST = 0 → 9 |
|---|---|---|---|
| A | 152 | 0.59 → 0.91 | 0.79 → 0.95 |
| B | 207 | 0.61 → 0.88 | 0.96 → 0.97 |
| C | 214 | 0.84 → 0.93 | 0.91 → 0.97 |

**手計算（論文の表を読む）**：上の「論文の表1から」の表を見て、次を答えてください。
1. UTSN から UTSN-L で、上がった数と下がった数をそれぞれ挙げてください。
2. レムの感度は何ポイント上がり、陽性的中率は何ポイント下がりましたか。

### 確認の問い
1. PAST = 0 のままでも、レムの感度は組Aで 0.59、組Cで 0.84 と大きく違いました。何が違うからでしょうか。このことから、1組だけで測る危うさについて何が言えますか。
2. 「レムの間だけ光を当てる」実験で、UTSN-L は UTSN より、どんな失敗が減り、どんな失敗が増えそうですか。
3. 「研究室の睡眠判定の AI の正解率は 99％」と書いたら、何が間違いでしょうか。

<details>
<summary>答えと考え方（AI 用。学習者が自分で答えるまで見せない）</summary>

- 手計算1：上がった＝全体の正解率（88.1 → 89.5）・MCC（78.6 → 80.7）・レムの感度（87.4 → 90.8）。下がった＝レムの陽性的中率（79.2 → 75.2）・レムの特異度（98.3 → 98.1、わずか）。
- 手計算2：感度は 90.8 − 87.4 ＝ 3.4 ポイント上がった。陽性的中率は 79.2 − 75.2 ＝ 4.0 ポイント下がった。
- 確認1：試験に使ったマウスが違う。マウスごとに電極の振れ幅（模型の gain）やレムの量が違い、判定しやすいマウスと、しにくいマウスがいる。組を変えるだけで 0.25 も動くので、1組だけの差を「効果」と言うのは危うい。ただし3組とも PAST = 9 の方が高く、向きは変わらなかった。組の違いで動く幅より、改造の効果の方が一貫して大きい、と言えて初めて「効果がありそう」と言える。論文が分け方を3回変えて平均し、別の22本で試験し、交差検証もしたのは、このため。
- 確認2：感度が上がったので、レムなのに光を当て損なう失敗は減りそう。陽性的中率が下がったので、レムでない時に光を当ててしまう失敗は増えそう。どちらを重く見るかは実験の目的で決まる、と言えればよい。
- 確認3：99％ は作り物のデータで動かした模型の数で、手塚AIの成績ではない。本物の成績として言えるのは論文の数（要旨では全体の正解率 90％）。さらに、全体の正解率だけではレムのような少ない段階の出来が隠れる（第7回・第9回）。数字は、何を・何で・どう測ったかと一緒に書く。
- 最終回なので、はじめにの「フィードバックのお願い」の4点を丁寧に案内する。あわせて、修了の目安の8項目を、学習者に自分の言葉で言ってもらう。
</details>

### まとめ
- 改造の効果を言う前に、①比べ方をそろえる ②測る組を変える ③良くなる所と悪くなる所を両方見る。
- 手塚AIの改良版（過去も使う UTSN-L）は、論文の表1で、レムの感度が上がり（87.4 → 90.8）、陽性的中率が下がった（79.2 → 75.2）。1か所の変更は、得と損を同時に連れてくる。
- 模型の数字は考え方を確かめる道具で、本物の成績ではない。数字は原典の表で確かめる。

**本物を動かしてみたい人へ**：手塚AIのプログラムは、Python を入れたパソコン（Windows か Mac）で動かすものです。手順は説明書き（Readme.md）に、必要な道具の入れ方・記録済みの脳波で判定する方法（offline.py）・新しく学習させる手順の順に書かれています。道具の版の違いでエラーが出ることがあります。その時は、第1回と同じく、エラーの最後の1行を読むところから始めてください。

---

## 修了の目安と、次に学ぶもの

### 修了の目安
次のことを、何も見ずに友人に説明できれば、この講座は修了です。
1. AI に渡す前に、表の形と欠けた値を確かめる。値が抜けると「N 行の窓」は「N 秒の窓」ではなくなる。
2. 訓練・検証・試験は、人や場所（マウス）の単位で分ける。混ざると成績が実力より良く出る。
3. 感度は「本物を何割拾えたか」、陽性的中率は「そうだと言った中の何割が本物か」。正解率だけでは良し悪しを言えない。
4. 学習は、予測→採点→白紙→分析→修正 の繰り返し。順番を間違えても、エラーが出ないことがある。
5. 混同行列は、行が正解・列が予測・対角線が当たり。
6. 他人の AI は、論文と説明書きから「入力→区切り→特徴→判定役→出力」の設計図を描いてから読む。設定の数は名前つきで置かれていることが多い。
7. 1か所変えたら、比べ方をそろえ、測る組を変え、良くなる所と悪くなる所を両方見る。その場で判定する AI は、未来のデータを使えない。
8. 模型の数字と本物の数字を取り違えない。論文の数字は、論文の表で確かめる。

この講座の目標は、人が作った AI を読み、1か所変えて、効果を確かめることです。目標に届いたかは、自分で1か所変えて測れるかで確かめられます。例えば、第9回の模型で PAST 以外の1か所（区間の幅、試験に使うマウスの数など）を変えて測る。第7回で GaussianNB を別のモデルに変えて、混同行列がどう変わるかを測る。

### 次に学ぶもの
- **この研究室の論文**：第8〜10回で読んだ論文は、研究室ホームページの「論文」のページ（https://www.med.kobe-u.ac.jp/neurophy/ja/publications.html ）にも載っています。図1・図2（設計図）と表1（成績）を、この講座で学んだ読み方で読んでみてください。
- **統計の考え方**：「差がある」「関係がある」を、偶然と区別して言う方法（検定・信頼区間）。第10回の「測る組を変えると数が動く」と深くつながっています。この研究室の自習講座「中心極限定理と検定」も使えます。
- **表の扱いの続き**：pandas の公式の入門「10 minutes to pandas」（https://pandas.pydata.org/docs/user_guide/10min.html 、英語）。第3〜4回の続きを、短い例で一通り試せます。
- **ニューラルネットを一から作る本**：斎藤康毅『ゼロから作るDeep Learning』（オライリー・ジャパン、2016年）。第6回の学習の5段や、第8回の CNN を、Python で一から組み立てられます。
- **対話型の AI の中身**：今の言葉の AI の多くが使う「注意（アテンション）」の仕組みの原典は、A. Vaswani ほか "Attention Is All You Need"（Advances in Neural Information Processing Systems 30, 2017。https://arxiv.org/abs/1706.03762 ）です。数式が多いので、図と導入の節から読むのがおすすめです。
- **公開されているデータで腕試し**：第7回の手書き数字のような公開データが多数あります。使うときは、データの説明のページで、入手先と利用の条件（ライセンス）を必ず確かめてください。

---

## フィードバックのお願い（再掲）

> **この講座は、使った人の声で毎回直しています。使った方は、必ずフィードバックをお送りください。**
>
> 送り先：研究室ホームページの「連絡先」のページに載っているアドレス
> 件名：「講座フィードバック：AIの動作原理」
> 書いてほしいこと：①何回目まで進んだか ②分かりにくかった所 ③良かった所 ④1回にかかった時間
>
> 最後まで進めた方も、途中でやめた方も、ぜひお送りください。

神戸大学大学院医学系研究科 神経生理学分野
