ChatGPTと画像生成AI作ってみた！

# 初めに
うい
Akira_0809です、
最近、Stable diffusionで画像生成することが多いので作ってみようと思います
今回の先生はGPT-4oさんです！
分からない単語は調べたりしてます、

# チャットログ
GPT-4oとのチャットの履歴を置いときます

https://chatgpt.com/share/e8be36f9-caf7-4746-b4ea-05cf8269b702

# 授業
コードや画像を貼ります、
## 最初
最初のコードをGoogle Colabで実行してみます
```python
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader

# データセットのロード
transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,))])
dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
dataloader = DataLoader(dataset, batch_size=64, shuffle=True)

# モデルの定義
class Generator(nn.Module):
    def __init__(self):
        super(Generator, self).__init__()
        self.main = nn.Sequential(
            nn.Linear(100, 256),
            nn.ReLU(True),
            nn.Linear(256, 512),
            nn.ReLU(True),
            nn.Linear(512, 1024),
            nn.ReLU(True),
            nn.Linear(1024, 784),
            nn.Tanh()
        )

    def forward(self, input):
        return self.main(input).view(-1, 1, 28, 28)

class Discriminator(nn.Module):
    def __init__(self):
        super(Discriminator, self).__init__()
        self.main = nn.Sequential(
            nn.Linear(784, 512),
            nn.LeakyReLU(0.2, inplace=True),
            nn.Linear(512, 256),
            nn.LeakyReLU(0.2, inplace=True),
            nn.Linear(256, 1),
            nn.Sigmoid()
        )

    def forward(self, input):
        return self.main(input.view(-1, 784))

# モデルのインスタンス作成
generator = Generator()
discriminator = Discriminator()

# 損失関数と最適化関数の定義
criterion = nn.BCELoss()
optimizer_g = optim.Adam(generator.parameters(), lr=0.0002)
optimizer_d = optim.Adam(discriminator.parameters(), lr=0.0002)

# トレーニングループ
for epoch in range(50):
    for i, (images, _) in enumerate(dataloader):
        # ディスクリミネーターのトレーニング
        optimizer_d.zero_grad()
        real_labels = torch.ones(images.size(0), 1)
        fake_labels = torch.zeros(images.size(0), 1)
        outputs = discriminator(images)
        d_loss_real = criterion(outputs, real_labels)
        z = torch.randn(images.size(0), 100)
        fake_images = generator(z)
        outputs = discriminator(fake_images.detach())
        d_loss_fake = criterion(outputs, fake_labels)
        d_loss = d_loss_real + d_loss_fake
        d_loss.backward()
        optimizer_d.step()

        # ジェネレーターのトレーニング
        optimizer_g.zero_grad()
        outputs = discriminator(fake_images)
        g_loss = criterion(outputs, real_labels)
        g_loss.backward()
        optimizer_g.step()

    print(f'Epoch [{epoch+1}/50], d_loss: {d_loss.item()}, g_loss: {g_loss.item()}')

# 生成された画像の表示
import matplotlib.pyplot as plt
with torch.no_grad():
    z = torch.randn(64, 100)
    fake_images = generator(z).reshape(-1, 28, 28)
    plt.figure(figsize=(8, 8))
    for i in range(64):
        plt.subplot(8, 8, i+1)
        plt.imshow(fake_images[i], cmap='gray')
        plt.axis('off')
    plt.show()
```
エラーが出ないで実行できた！
### 生成された画像
![ダウンロード.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3241738/8817df28-cede-765a-2eba-85156ba920fe.png)
生成出来てるぽい

## コードの説明
端折るのできちんと読みたい方はチャットログを見てね

### ジェネレーター
ランダムノイズから画像を生成する関数
```python
class Generator(nn.Module):
    def __init__(self):
        super(Generator, self).__init__()
        self.main = nn.Sequential(
            nn.Linear(100, 256),
            nn.ReLU(True),
            nn.Linear(256, 512),
            nn.ReLU(True),
            nn.Linear(512, 1024),
            nn.ReLU(True),
            nn.Linear(1024, 784),
            nn.Tanh()
        )

    def forward(self, input):
        return self.main(input).view(-1, 1, 28, 28)
```
なるほど、
ジェネレーターは生成する画像をディスクリミネーターに見破られないように生成する感じか

#### __init__メソッド
```nn.Linear(in_features, out_features)```は全結合層（線形層）を定義している
```nn.Linear(100, 256)```は入力ベクトルのサイズが100で出力ベクトルのサイズが256の全結合層
```nn.ReLU(True)``` はReLU（Rectified Linear Unit）活性化関数を適用する
ReLUは、負の値を0にし、正の値をそのままにする
```nn.Tanh()``` はTanh活性化関数を適用する
Tanhは入力を-1から1の範囲にスケーリングする

次元を大きくすることでAIが画像を理解するのか
例えば、服のしわとか重力的な感じかな

ReLU活性化関数で負の値を0にすることでデータに非線形性が導入され、複雑なパターンの学習が可能になるのか
非線形変換が無い場合だと、いくつ線形変換を積み重ねても、結局１つの線形変換になってしまう

Tanh活性化関数では出力を[-1, 1]に収める
これによって後続の処理や表示で扱いやすくなるらしい

**ここでミスが発覚**

画像を表示する際に出力を[0, 255]の範囲にスケーリングする必要があった
このコードを画像を表示する所に挿入し、再チャレンジ！
```python
fake_images = (fake_images + 1) / 2
fake_images = fake_images * 255    
fake_images = fake_images.numpy().astype("uint8")
```

![download.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3241738/7fa85537-8831-750e-68fd-2dbf906f42b9.png)
ん？
なんか変わった？

どうやら`matplotlib.imshow`が内部で適切な正規化を行っているらしい
有能すぎる、、

#### forwardメソッド
```python
def forward(self, input):
    return self.main(input).view(-1, 1, 28, 28)
```
inputにはランダムノイズベクトルを受け取る
viewは左からバッチサイズ、チャネル数、高さ、幅
- バッチサイズ
    - 機械学習などのトレーニングで一度に処理するデータのサンプル数
    - メモリ効率の改善、計算の効率化、トレーニングの安定化などの役割がある
    - -1 だとPyTorchが自動的に他の次元のサイズに基づいて計算してくれる
- チャネル数
    - 白黒画像だと 1
    - カラー画像だと 3
- 高さ、幅
    - 画像の高さと幅

### ディスクリミネーター
入力画像が本物か偽物か判別する
```python
class Discriminator(nn.Module):
    def __init__(self):
        super(Discriminator, self).__init__()
        self.main = nn.Sequential(
            nn.Linear(784, 512),
            nn.LeakyReLU(0.2, inplace=True),
            nn.Linear(512, 256),
            nn.LeakyReLU(0.2, inplace=True),
            nn.Linear(256, 1),
            nn.Sigmoid()
        )

    def forward(self, input):
        return self.main(input.view(-1, 784))
```

本物の画像には1に近い値を、偽物の画像には0に近い値を出力する

#### __init__メソッド

`nn.Linear(784, 512)`は全結合層として次元を圧縮することで特徴を抽出しているらしい
`nn.LeakyReLU(0.2, inplace=True)`はLeakyReLU活性化関数として負の値を0.2にする

`LeakyReLU`は負の値の情報を活用するために0.2に変換してるぽい

ジェネレーターに`ReLU`を使ったのは、生成する際の自由度を重視しているから
ディスクリミネーターに`LeakyReLU`を使ったのは、判定の厳密性を重視しているから

`inplace`はPyTorchでテンソルのメモリを効率的に管理するやつ
けど、勾配計算やデータの再利用に関する制約が存在するからジェネレーターでは使われてない感じ

`nn.Linear(256, 1)`からスコアを受け取った`nn.Sigmoid()`が[0, 1]に変換することで出力が確率として解釈できるようになるらしい

#### forwardメソッド
```python
def forward(self, input):
    return self.main(input.view(-1, 784))
```
28x28の画像を784次元の1次元ベクトルに変換させる
本物の画像だと1に近い値が出力される
偽物の画像だと0に近い値が出力される

### 損失関数と最適化関数の定義
```python
criterion = nn.BCELoss()
optimizer_g = optim.Adam(generator.parameters(), lr=0.0002)
optimizer_d = optim.Adam(discriminator.parameters(), lr=0.0002)
```
#### 損失関数
モデルの予測と実際の目標値の差（誤差）を定量化するもの
GANでは`nn.BCELoss()`の二値クロスエントロピー損失がよく使用されるらしい
この損失を最小化するのが学習

#### 最適化関数
モデルのパラメータを更新するアルゴリズム
損失を最小化するようにパラメータを調整するらしい
損失関数の勾配（損失関数に対するパラメータの微分）を計算し、その勾配を使用してパラメータを調整するらしい
`Adam`では勾配の1次モーメント（平均）と2次モーメント（分散）を使用して、各パラメータごとに適応的な学習率を調整する

`lr=0.0002`は学習率
- 大きくすると
    - 収束が速くなるが不安定
        - 学習率が大きいと、パラメータの更新幅が大きくなる
    - 局所最適解を乗り越える
        - グローバル最適解に近づく可能性がある
- 小さくすると
    - 収束が遅くなるが安定
        - 学習がゆっくり進むため、パラメータ更新が安定する
    - 細かい調整が可能
        - 最適解に近づきやすい
        - 局所最適解に停滞する可能性がある

`Adam`は有能なので学習率を自動で調整してくれるらしい

### トレーニングループ
```python 
for epoch in range(50):
    for i, (images, _) in enumerate(dataloader):
        # ディスクリミネーターのトレーニング
        optimizer_d.zero_grad()
        real_labels = torch.ones(images.size(0), 1)
        fake_labels = torch.zeros(images.size(0), 1)
        outputs = discriminator(images)
        d_loss_real = criterion(outputs, real_labels)
        z = torch.randn(images.size(0), 100)
        fake_images = generator(z)
        outputs = discriminator(fake_images.detach())
        d_loss_fake = criterion(outputs, fake_labels)
        d_loss = d_loss_real + d_loss_fake
        d_loss.backward()
        optimizer_d.step()

        # ジェネレーターのトレーニング
        optimizer_g.zero_grad()
        outputs = discriminator(fake_images)
        g_loss = criterion(outputs, real_labels)
        g_loss.backward()
        optimizer_g.step()

    print(f'Epoch [{epoch+1}/50], d_loss: {d_loss.item()}, g_loss: {g_loss.item()}')
```
ディスクリミネーターとジェネレーターのトレーニングループ
`epoch`はトレーニングデータを全体を学習するサイクル
`images`はバッチサイズ分のデータ
`_`はラベル情報（今回は使用していないので無視）

#### ディスクリミネーターのトレーニング
`optimizer_d.zero_grad()`
ディスクリミネーターの勾配をリセットする
これにより、前回の勾配が残らず、今回の勾配のみが適用される

`real_labels = torch.ones(images.size(0), 1)`
本物の画像に対するラベルを1（本物）に設定する
バッチサイズ分の1を持つテンソル

`fake_labels = torch.zeros(images.size(0), 1)`
偽物の画像に対するラベルを0（偽物）に設定する
バッチサイズ分の0を持つテンソル

`outputs = discriminator(images)`
本物の画像をディスクリミネーターに入力し、本物か偽物かの判別結果（確率）を得る

`d_loss_real = criterion(outputs, real_labels)`
本物の画像に対するディスクリミネーターの損失を計算する

`z = torch.randn(images.size(0), 100)`
ランダムノイズベクトルを生成される、バッチサイズ分のノイズ（100次元）

`fake_images = generator(z)`
ジェネレーターにノイズを入力して、偽物の画像を生成する

`outputs = discriminator(fake_images.detach())`
偽画像をディスクリミネーターに入力し、本物か偽物かの判別結果（確率）を得る
`detach()` : ジェネレーターの計算グラフから切り離し、ディスクリミネーターのみの勾配計算にする
ディスクリミネーターの学習中にジェネレーターのパラメータを更新しないように

`d_loss_fake = criterion(outputs, fake_labels)`
偽画像に対するディスクリミネーターの損失を計算する

`d_loss = d_loss_real + d_loss_fake`
ディスクリミネーターの全体の損失を計算する
本物と偽物の両方に対する損失の合計

`d_loss.backward()`
ディスクリミネーターの損失に対する勾配を計算する
パラメータの更新に必要な勾配情報を取得する

`optimizer_d.step()`
計算された勾配を使用して、ディスクリミネーターのパラメータを更新する

#### ジェネレーターのトレーニング
`optimizer_g.zero_grad()`
ジェネレーターの勾配をリセットする
これにより、前回の勾配が残らず、今回の勾配のみが適用される

`outputs = discriminator(fake_images)`
偽画像をディスクリミネーターに入力し、本物か偽物かの判別結果（確率）を得る
この結果を使用して、ジェネレーターの性能を評価する

`g_loss = criterion(outputs, real_labels)`
偽画像を本物として認識させるために、ディスクリミネーターを欺くようにジェネレーターを学習させる損失を計算する

`g_loss.backward()`
ジェネレーターの損失に対する勾配を計算する
パラメータの更新に必要な勾配情報を取得する

`optimizer_g.step()`
計算された勾配を使用して、ジェネレーターのパラメータを更新する

### 生成された画像の表示
```python 
import matplotlib.pyplot as plt
with torch.no_grad():
    z = torch.randn(64, 100)
    fake_images = generator(z).reshape(-1, 28, 28)
    fake_images = (fake_images + 1) / 2
    fake_images = fake_images * 255
    fake_images = fake_images.numpy().astype("uint8")
    plt.figure(figsize=(8, 8))
    for i in range(64):
        plt.subplot(8, 8, i+1)
        plt.imshow(fake_images[i], cmap='gray')
        plt.axis('off')
    plt.show()
```
`with torch.no_grad():`
このブロックでの勾配計算を無効に

8x8のブロックに分けて画像を配置

![download.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3241738/7fa85537-8831-750e-68fd-2dbf906f42b9.png)

# 最後に
意外と簡単に作れた！
分からないところを無限に聞けるし、ちょっと期間が開いても答えてくれる！
最高です！

次はtext2imageみたいに数字を入れたらその数字を生成出来るように改造します！

あざした！

ミスがあったらコメントください、