第4章 画像も文章も数値の並びになる

こんにちは。ゆうせいです。

第3章までで、スカラー、ベクトル、行列、テンソルという入れ物が揃いました。第4章では、その入れ物に、実際のデータを詰めていきます。写真、文章、音声、表計算のデータ。これらは、私たちから見ればまったく異なる性質を持っています。ところが、ディープラーニングのモデルは、これらを同じような構造で処理します。なぜそんなことが可能なのでしょうか。答えは、すべてのデータをいったん数値の並びに変換してしまうからです。この変換こそが、線形代数がディープラーニングの共通言語として機能する出発点になっています。

この章の位置づけ

第2章と第3章では、線形代数の道具を確認しました。しかし、道具だけあっても、扱う対象がなければ意味がありません。

この章は、線形代数の技術を新しく学ぶ章ではありません。なぜデータが線形代数の対象になるのかを、納得してもらうための章です。

計算の話は第5章から本格化します。この章では、具体例を多めに、データの姿を見ていきます。

画像は数値の格子である

モノクロ画像の場合

まず、白黒の画像から考えます。

デジタル画像は、画素という小さな点の集まりでできています。各画素には、明るさを表す数値が入っています。

一般的には、0から255までの整数で表現されます。0が真っ黒、255が真っ白、その中間が灰色です。

たとえば、4画素四方の小さな画像は、次のような行列になります。

\begin{pmatrix} 0 & 50 & 100 & 150 \\ 50 & 100 & 150 & 200 \\ 100 & 150 & 200 & 255 \\ 150 & 200 & 255 & 255 \end{pmatrix}

左上が暗く、右下に向かって明るくなるグラデーションです。

つまり、モノクロ画像は、そのまま行列です。第3章で学んだ2次元の入れ物に、そのまま収まります。

手書き数字の認識でよく使われるMNISTというデータでは、画像が28画素四方です。したがって、1枚の画像は28行28列の行列になります。

カラー画像の場合

カラー画像では、各画素が3つの数値を持ちます。赤、緑、青の強さです。

この三色を組み合わせることで、あらゆる色を表現できます。赤が255、緑が0、青が0であれば、鮮やかな赤色です。三つとも255であれば白、三つとも0であれば黒になります。

したがって、カラー画像は、モノクロ画像が3枚重なった構造になります。赤の強さを表す行列、緑の強さを表す行列、青の強さを表す行列です。

この3枚を、チャンネルと呼びます。

形としては、次の3次元テンソルになります。

(channels, height, width)

224画素四方のカラー画像であれば、(3, 224, 224) です。

正規化という前処理

実際にモデルに入力する際には、0から255という値をそのまま使わず、変換することが一般的です。

最も単純な方法は、255で割ることです。これにより、値が0から1の範囲に収まります。

x_{norm} = \frac{x}{255}

さらに、平均を引いて標準偏差で割る、という処理を加えることもあります。

x_{std} = \frac{x - \mu}{\sigma}

なぜこの処理が必要なのでしょうか。

理由は、値の範囲が大きすぎると、学習が不安定になるためです。0から255という範囲の数値を、そのまま何層も掛け算していくと、値が急速に大きくなり、計算が破綻しやすくなります。

小さな範囲に揃えておくことで、安定した計算ができます。この考え方は、後の章で扱う正規化の話にもつながります。

画像を1列に伸ばす場合

全結合層に画像を入力する場合、行列を1列のベクトルに変換することがあります。

28行28列の画像であれば、784個の数値を1列に並べた、784次元のベクトルにします。

28 \times 28 = 784

この操作を、平坦化と呼びます。

ただし、この変換には代償があります。縦横の位置関係の情報が失われるのです。隣り合っていた画素が、ベクトルの中では離れた位置に来ることがあります。

畳み込みニューラルネットワークが画像認識で強いのは、この位置関係を保ったまま処理できるからです。ただし、その仕組みの詳細は、この連載の範囲を超えます。

文章は番号の並びからベクトルへ

手順1:文章をトークンに分割する

文章は、そのままでは数値になりません。まず、意味のある単位に分割します。

「猫が魚を食べた」という文であれば、たとえば次のように分割されます。

「猫」「が」「魚」「を」「食べ」「た」

この分割された単位を、トークンと呼びます。

分割の方法にはいくつかの流儀があり、単語ごとに分ける方法、文字ごとに分ける方法、その中間のサブワードという単位で分ける方法があります。現在の言語モデルでは、サブワードによる分割が主流です。

手順2:トークンに番号を割り当てる

あらかじめ、扱うすべてのトークンに番号を振っておきます。この対応表を、語彙と呼びます。

「猫」が3782番、「が」は12番、というように、それぞれに番号が割り当てられます。

これにより、文章が数値の並びになります。

[3782, 12, 5041, 8, 2913, 5]

ただし、この番号には、まだ意味がありません。3782番と3783番が、意味的に近いわけではないのです。単なる登録順の番号です。

手順3:番号をベクトルに変換する

そこで、各番号を、意味を表すベクトルに変換します。

この変換に使うのが、埋め込み行列と呼ばれる巨大な対応表です。

行数は語彙の数、列数は埋め込みの次元数です。GPT-2であれば、50257行768列の行列です。

トークン番号が3782であれば、この行列の3782行目にある768個の数値を取り出します。これが、そのトークンを表すベクトルです。

結果としてのデータの形

6トークンの文章であれば、6本の768次元ベクトルが得られます。

これを縦に並べると、6行768列の行列になります。

さらに、複数の文章をまとめて処理する場合、軸が1本増えます。16件の文章を、それぞれ最大512トークンとして処理するなら、形は次のようになります。

(16, 512, 768)

第3章で例に挙げた形が、ここで具体的な意味を持ちました。

埋め込みベクトルが意味を持つ理由

なぜ、768個の数値の並びが、単語の意味を表せるのでしょうか。

この対応表の中身は、人間が作るのではありません。大量の文章を使った学習によって、自動的に決まります。

学習の過程で、似た文脈で使われる単語は、似たベクトルになるように調整されていきます。「猫」と「犬」は、どちらも「〜が餌を食べた」のような文に登場するため、ベクトルが近づいていくのです。

その結果、ベクトル空間の中での距離や方向が、意味の近さを反映するようになります。この性質を、第6章の内積のところで、あらためて扱います。

表形式データはそのまま行列になる

最も素直に行列になるのが、表計算ソフトで扱うような表形式のデータです。

顧客の情報を扱う例で考えます。

顧客ID年齢年収(万円)勤続年数購入回数
135550812
242720153
328410425

顧客IDを除いた部分が、そのまま行列になります。

X = \begin{pmatrix} 35 & 550 & 8 & 12 \\ 42 & 720 & 15 & 3 \\ 28 & 410 & 4 & 25 \end{pmatrix}

3行4列の行列です。行がサンプル、つまり顧客に対応し、列が特徴量に対応しています。

スケールの違いという問題

ここで、実務上の重要な問題があります。

年齢は35前後、年収は550前後という値です。桁が違います。

このまま計算すると、年収という特徴量が、年齢より圧倒的に大きな影響を持ってしまいます。値が大きいというだけの理由で、重要度が高いかのように扱われてしまうのです。

そこで、各特徴量を、同じような範囲に揃える前処理が行われます。

代表的な方法は、標準化です。各特徴量について、平均を引き、標準偏差で割ります。

z = \frac{x - \mu}{\sigma}

これにより、すべての特徴量が、平均0、標準偏差1に揃います。

画像の正規化と、考え方は同じです。値の範囲を揃えることで、公平に、そして安定して計算できるようにしています。

カテゴリデータの扱い

数値でないデータは、どう扱うのでしょうか。

たとえば、「東京」「大阪」「名古屋」という居住地の情報があるとします。

単純に、東京を1、大阪を2、名古屋を3とする方法が思い浮かびます。しかし、これには問題があります。名古屋は東京の3倍である、という意味のない大小関係が生まれてしまうのです。

そこで、One-Hot表現という方法が使われます。

カテゴリの数だけ列を用意し、該当する列だけを1、他を0とします。

\begin{pmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \end{pmatrix}

1行目が東京、2行目が大阪、3行目が名古屋に対応します。

これにより、大小関係が生まれず、すべてのカテゴリが対等に扱われます。

なお、言語モデルにおける単語の扱いも、原理的にはこのOne-Hot表現から出発しています。語彙の数だけの長さを持つベクトルで、該当する単語の位置だけが1になる。それに埋め込み行列を掛けることで、意味を持つベクトルに変換される、という構造です。

音声も数値の並びになる

参考までに、音声についても簡単に触れます。

音は、空気の振動です。マイクで録音すると、時間ごとの振動の強さが記録されます。

1秒間に何回記録するかを、サンプリング周波数と呼びます。CDの音質であれば、1秒間に44100回です。

したがって、10秒の音声は、441000個の数値が並んだベクトルになります。

44100 \times 10 = 441000

実務では、この生の波形をそのまま使うより、周波数の成分に変換してから扱うことが多くあります。この変換により、時間と周波数の2つの軸を持つ行列になります。見た目は画像に近い形になるため、画像認識の技術が応用できるようになります。

共通する構造

ここまで見てきた四種類のデータを、整理します。

データの種類変換後の形各軸の意味
モノクロ画像(height, width)縦位置、横位置
カラー画像(3, height, width)色、縦位置、横位置
文章(sequence_length, embedding_dim)何番目のトークンか、意味の各成分
表形式データ(num_samples, num_features)何件目か、どの特徴量か
音声(周波数変換後)(time, frequency)いつか、どの高さの音か

いずれも、数値が規則的に並んだ構造になっています。

そして、バッチ処理のために軸を1本増やせば、すべて同じ枠組みで扱えます。

この共通化があるからこそ、画像認識で開発された技術が言語処理に転用されたり、その逆が起きたりします。Transformerという仕組みが、当初は翻訳のために作られながら、現在では画像や音声にも使われているのは、この共通の土台があるためです。

Pythonで確認する

import numpy as np

print("=== モノクロ画像 ===")
# 4x4のグラデーション画像
mono = np.array([
    [0, 50, 100, 150],
    [50, 100, 150, 200],
    [100, 150, 200, 255],
    [150, 200, 255, 255]
], dtype=np.uint8)
print(f"形: {mono.shape}, 軸の数: {mono.ndim}")
print(mono)

# 正規化
mono_normalized = mono / 255.0
print(f"\n正規化後の範囲: {mono_normalized.min():.2f} 〜 {mono_normalized.max():.2f}")

print("\n=== カラー画像 ===")
# 2x2のカラー画像(チャンネル、高さ、幅)
color = np.zeros((3, 2, 2), dtype=np.uint8)
color[0] = [[255, 0], [0, 255]]   # 赤チャンネル
color[1] = [[0, 255], [0, 0]]     # 緑チャンネル
color[2] = [[0, 0], [255, 0]]     # 青チャンネル
print(f"形: {color.shape}, 軸の数: {color.ndim}")
print(f"左上の画素の色(RGB): {color[:, 0, 0]}")
print(f"右上の画素の色(RGB): {color[:, 0, 1]}")

print("\n=== 平坦化 ===")
flat = mono.flatten()
print(f"平坦化前: {mono.shape}")
print(f"平坦化後: {flat.shape}")
print(f"値: {flat}")

print("\n=== 表形式データと標準化 ===")
data = np.array([
    [35, 550, 8, 12],
    [42, 720, 15, 3],
    [28, 410, 4, 25],
    [51, 890, 22, 7],
    [33, 620, 10, 18]
], dtype=np.float32)
print(f"形: {data.shape}")
print(f"元のデータ:\n{data}")
print(f"\n各列の平均: {data.mean(axis=0).round(2)}")
print(f"各列の標準偏差: {data.std(axis=0).round(2)}")

standardized = (data - data.mean(axis=0)) / data.std(axis=0)
print(f"\n標準化後:\n{standardized.round(3)}")
print(f"標準化後の各列の平均: {standardized.mean(axis=0).round(6)}")
print(f"標準化後の各列の標準偏差: {standardized.std(axis=0).round(6)}")

print("\n=== One-Hot表現 ===")
categories = ["東京", "大阪", "名古屋"]
labels = ["東京", "名古屋", "大阪", "東京"]

one_hot = np.zeros((len(labels), len(categories)), dtype=int)
for i, label in enumerate(labels):
    one_hot[i, categories.index(label)] = 1

print(f"元のラベル: {labels}")
print(f"One-Hot表現(形{one_hot.shape}):\n{one_hot}")

print("\n=== 文章データの形 ===")
batch_size = 16
seq_len = 512
embed_dim = 768
text_data = np.zeros((batch_size, seq_len, embed_dim), dtype=np.float32)
print(f"形: {text_data.shape}")
print(f"意味: {batch_size}件の文章、各{seq_len}トークン、各トークンが{embed_dim}次元")
print(f"要素数: {text_data.size:,}")
print(f"メモリ: {text_data.nbytes / 1024 / 1024:.2f} MB")

初学者がつまずきやすい点

つまずき1:番号をそのまま特徴量として使ってしまう

カテゴリデータに1、2、3と番号を振って、そのまま入力してしまう誤りは、実務でよく見かけます。

意味のない大小関係が持ち込まれるため、モデルが誤った関係を学習します。

ただし、例外もあります。順序に意味があるカテゴリ、たとえば「小」「中」「大」のようなものであれば、1、2、3という番号付けが適切な場合もあります。順序があるかどうかで判断してください。

つまずき2:正規化や標準化を忘れる

前処理を忘れると、学習がまったく進まない、あるいは非常に遅くなります。

しかも、エラーにはなりません。損失が下がらないという症状として現れるため、原因の特定に時間がかかります。

学習がうまくいかないときに、最初に確認すべき項目の一つです。

つまずき3:平坦化で情報が失われることに気づかない

画像を1列に伸ばすと、縦横の位置関係が失われます。

単純なモデルであれば、それでも一定の性能は出ます。しかし、位置関係を活かせるモデルと比べると、性能に差が出ます。

なぜ画像には畳み込みニューラルネットワークが使われるのか、という問いへの答えの一つが、ここにあります。

演習

演習1:データの形を答える

次のそれぞれについて、テンソルの形を答えてください。

64画素四方のモノクロ画像を、32枚まとめたもの。PyTorch形式とします。

128画素四方のカラー画像を、16枚まとめたもの。PyTorch形式とします。

10トークンの文章を、埋め込み次元512で表現したもの。1件のみとします。

100件の顧客データ、それぞれ15個の特徴量。

演習2:メモリ量を見積もる

224画素四方のカラー画像を、バッチサイズ64で処理する場合を考えます。

入力データのテンソルの形と、要素数、float32でのメモリ量を計算してください。

また、メモリが足りない場合、バッチサイズをいくつにすれば半分に収まるでしょうか。

演習3:前処理の効果を確認する

import numpy as np

# スケールが大きく異なるデータ
data = np.array([
    [25, 3000000, 0.5],
    [40, 5500000, 0.8],
    [33, 4200000, 0.3],
    [55, 8000000, 0.9],
    [29, 3800000, 0.6]
], dtype=np.float32)

feature_names = ["年齢", "年収(円)", "評価スコア"]

print("=== 前処理なし ===")
print(f"{'特徴量':>12} | {'平均':>12} | {'標準偏差':>12} | {'範囲':>20}")
print("-" * 64)
for i, name in enumerate(feature_names):
    col = data[:, i]
    print(f"{name:>12} | {col.mean():>12.2f} | {col.std():>12.2f} | {col.min():>8.2f} 〜 {col.max():>8.2f}")

# 標準化
standardized = (data - data.mean(axis=0)) / data.std(axis=0)

print("\n=== 標準化後 ===")
print(f"{'特徴量':>12} | {'平均':>12} | {'標準偏差':>12} | {'範囲':>20}")
print("-" * 64)
for i, name in enumerate(feature_names):
    col = standardized[:, i]
    print(f"{name:>12} | {col.mean():>12.6f} | {col.std():>12.6f} | {col.min():>8.2f} 〜 {col.max():>8.2f}")

# 仮に同じ重みを掛けた場合の影響を比較
weights = np.array([1.0, 1.0, 1.0])
print("\n=== すべての特徴量に同じ重み1.0を掛けた場合の寄与 ===")
print("前処理なし:", (data * weights).sum(axis=1).round(2))
print("標準化後:  ", (standardized * weights).sum(axis=1).round(4))
print("\n前処理なしでは、年収の値がほぼすべてを支配していることが分かります。")

演習の解答例と解説

演習1の解答は、次のとおりです。

64画素四方のモノクロ画像32枚は、(32, 1, 64, 64) です。モノクロでもチャンネル軸を1として持たせるのが、PyTorchの慣例です。

128画素四方のカラー画像16枚は、(16, 3, 128, 128) です。

10トークンの文章を埋め込み次元512で表現したものは、(10, 512) です。バッチ軸を含めるなら (1, 10, 512) です。

100件の顧客データ、15個の特徴量は、(100, 15) です。

演習2の解答は、次のとおりです。

テンソルの形は、(64, 3, 224, 224) です。

要素数は、次のように計算されます。

64 \times 3 \times 224 \times 224 = 9633792

約963万個です。

float32でのメモリ量は、次のようになります。

9633792 \times 4 = 38535168

約38.5メガバイトです。

メモリを半分にするには、バッチサイズを32にすればよいことになります。バッチサイズとメモリ量が比例するためです。

なお、実際の学習では、この入力データ以外に、各層の中間出力も保持する必要があります。したがって、実際に必要なメモリは、入力データの何倍にもなります。

演習3では、前処理をしない場合に、年収という特徴量がほぼすべての寄与を占めてしまうことが確認できます。年齢や評価スコアの値は、年収の数百万という値に埋もれてしまいます。

標準化後は、三つの特徴量が同等に扱われるようになります。

これが、前処理が必要な理由の、最も直接的な説明です。

講師向けの補足

この章は、実物を見せることが最も効果的です。

可能であれば、実際の画像ファイルを読み込み、その数値を表示させてください。写真が数値の格子であるという事実は、言葉で説明されるより、実際に数値を見たときのほうが、はるかに強い印象を残します。

matplotlibを使って、数値の配列から画像を描画してみせるのも有効です。数値を書き換えると画像が変わる、という体験が、両者の同一性を実感させます。

演習3は、必ず実行してもらってください。前処理の必要性は、頭で理解するより、数値で見たほうが納得されます。

理解度を確認する問い

第4章の内容を、次の問いで確認してみてください。

カラー画像が3次元テンソルになる理由を説明してみてください。

文章を数値に変換する手順を、三段階に分けて説明してみてください。

カテゴリデータに1、2、3という番号を振って入力してはいけないのは、なぜでしょうか。また、どのような場合には例外となるでしょうか。

画像、文章、表形式データという異なる種類のデータが、同じ枠組みで扱えるのは、なぜでしょうか。

まとめ

あらゆる種類のデータは、数値の並びに変換されることで、線形代数の対象になります。

画像は、各画素の明るさを表す数値の格子です。モノクロ画像は行列、カラー画像は赤緑青の3枚が重なった3次元テンソルになります。値の範囲を0から1などに揃える正規化が、前処理として行われます。

文章は、トークンへの分割、番号の割り当て、埋め込み行列によるベクトルへの変換という三段階を経て、数値の並びになります。埋め込みベクトルの値は学習によって獲得され、似た文脈で使われる単語が似たベクトルになります。

表形式データは、行がサンプル、列が特徴量の行列としてそのまま扱えます。ただし、特徴量ごとにスケールが異なるため、標準化という前処理が必要です。カテゴリデータには、One-Hot表現が使われます。

これらのデータは、変換後はいずれも規則的に並んだ数値の構造を持ちます。この共通性があるからこそ、ある分野で開発された技術が、別の分野にそのまま転用できるのです。

次の第5章から、いよいよ計算の話に入ります。第5章では、ベクトルの足し算とスカラー倍という、最も基本的な演算を扱います。単純な計算ですが、情報を混ぜる、情報を強めるという、ディープラーニングの中で繰り返し現れる操作の原型になっています。

第5章に進む前に、演習3のコードを実行し、前処理の有無で寄与がどう変わるかを確認しておいてください。値の範囲を揃えるという操作の重要性は、この後の章でも繰り返し登場します。

セイ・コンサルティング・グループでは新人エンジニア研修のアシスタント講師を募集しています。

投稿者プロフィール

山崎講師
山崎講師代表取締役
セイ・コンサルティング・グループ株式会社代表取締役。
岐阜県出身。
海外放浪の末、2000年創業、2004年会社設立。
IT企業向け人材育成研修歴業界歴20年以上。
すべての無駄を省いた費用対効果の高い「筋肉質」な研修を提供します!
この記事に間違い等ありましたらぜひお知らせください。

学生時代は趣味と実益を兼ねてリゾートバイトにいそしむ。長野県白馬村に始まり、志賀高原でのスキーインストラクター、沖縄石垣島、北海道トマム。高じてオーストラリアのゴールドコーストでツアーガイドなど。現在は野菜作りにはまっている。